跳至主要內容

Python 类型体操训练(三)-- 高级篇

bbruceyuan大约 5 分钟python-type-challengepython-typing-tutorialpython-type-tutorial

阅读提示

  • 面向读者群体
    • 有一定Python基础,需要进阶开发中大型项目
    • 有其他静态类型语言开发经验的人,需要快速了解 Python 类型注释(type hint)
    • 如果没有太多基础,可以先阅读前两篇文章
  • 你能学到什么?
    • Python 如何定义 protocol
    • Python 如何重载类方法和函数签名
    • Python 前向推导、生成器、Nerver等类型的使用
    • ...
    • 推荐自己完成 Python-Type-Challengesopen in new window 上面的练习。

这篇文章按照 Python-Type-Challengesopen in new window[1]库的划分,一共分为四个部分。

Python Type 高级类型

Protocol - 协议

Protocol 定义方式有点像 abc类,表示这个类型下面有某些方法。

  • 看例子学习,Duck 类下面有一个方法 quack
from typing import Protocol

# SupportsQuack 是一个有 quack 方法的类型
class SupportsQuack(Protocol):
    def quack(self) -> None:
        ...

class Duck:
    def quack(self) -> None:
        print('quack!')

duck: SupportsQuack = Duck()  # 正确

class Dog:
    def bark(self) -> None:
        print("bark!")
dog: SupportsQuack = Dog()   # 错误,因为 dog 类没有 `quack` 方法

重载

override - 类方法重载

重载这个特性在其他语言里面是被大量使用的,表示子类需要重载父类的方法。直接看

  • 例子
class Animal:
    def say(self) -> str:
        return 'hello world'

class Dog(Animal):
    # 正确
    def say(self) -> str:
        return "bake bake!!"

class Duck(Animal):
    # 这里可能写错了方法的名字,类型检查器也不会报错
    def sey(self) -> str:
        return "quack quack!!"

animal1: Animal = Dog()
animal1.say()  # 返回 'bake bake!!',对 say 方法进行重载了

animal2: Animal = Duck()
animal2.say()  # 返回 'hello world',因为没有正确的对 say 重载

而现在有了 override 关键字之后,就不会发生上面的问题了

class Animal:
    def say(self) -> str:
        return 'hello world'

class Dog(Animal):
    # 正确,重载 Animal.say 方法
    @override
    def say(self) -> str:
        return "bake bake!!"

class Duck(Animal):
    # !!!!这里会报错,因为 Animal 类里面没有 sey 方法
    @override
    def sey(self) -> str:
        return "quack quack!!"

overload -函数签名重载

这里的重载并不是真正的函数重载,因为重载的时候并不需要做真正的实现,而仅仅是重载签名。

from typing import overload

@overload
def process(response: None) -> None:
    ...

@overload
def process(response: int) -> tuple[int, str]:
    ...

@overload
def process(response: bytes) -> str:
    ...

def process(response: int | bytes | None) -> str | None | tuple[int, str]:
    ...


from typing import assert_type

assert_type(process(b"42"), str)
assert_type(process(42), tuple[int, str])
assert_type(process(None), None)

assert_type(process(42), str)  # expect-type-error
assert_type(process(None), str)  # expect-type-error
assert_type(process(b"42"), tuple[int, str])  # expect-type-error
assert_type(process(None), tuple[int, str])  # expect-type-error
assert_type(process(42), str)  # expect-type-error
assert_type(process(None), str)  # expect-type-error

ForwardRef -前向推导类型

  • Example 1, 我们使用一个类型的时候,可能这个类型还没有完成定义,但是我们又想定义内部的返回值。这个时候就需要使用前向推导,语法为「引号包裹变量名」,比如下面的 copy 方法返回 "MyClass"
class MyClass:
    def __init__(self, x: int) -> None:
        self.x = x

    def copy(self) -> "MyClass":
        copied_object = MyClass(x=self.x)
        return copied_object

from typing import assert_type
inst = MyClass(x=1)
assert_type(inst.copy(), MyClass)  
# 这两个是同一个类型
# 前向推导一般使用 引号将类名 包裹起来,从而达到前向推导的目的。
  • Example 2, 循环定义类型。定义一个 名叫 Tree 的字典,key 是 str, value 还是 Tree
type Tree = dict[str, "Tree"]

Generator - 生成器

用法: Generator[YieldType, SendType, ReturnType],详情见例子

def echo_round() -> Generator[int, float, str]:
    sent = yield 0
    while sent >= 0:
        sent = yield round(sent)
    return 'Done'
# 解释:
# yield 后都是返回 int 类型
# round 函数接受一个 float 类型
# 最终的 return 是 `Done`,类型是 str

Never

这通常用于表示一个函数永远不会被调用或者一个函数没有返回值。

  • Example 1, 永远不会被调用
from typing import Never

def never_call_me(arg: Never) -> None:
    pass

def int_or_str(arg: int | str) -> None:
    never_call_me(arg)  # type checker error
    match arg:
        case int():
            print("It's an int")
        case str():
            print("It's a str")
        case _:
            never_call_me(arg)  # OK, arg is of type Never
  • Example 2, 没有返回值
from typing import Never

def stop() -> Never:
    raise RuntimeError("")

from typing import assert_never

assert_never(stop())

TypeGuard

一般用于把 Python 类型缩窄。用 TypeGuard 定义会告诉类型检查器两个信息

  • 返回值是一个布尔类型(boolean)
  • 如果返回 True ,说明类型是 TypeGuard 内的类型。
from typing import Any, TypeGuard

def is_string(value: Any) -> TypeGuard[str]:
    return isinstance(value, str)

TupleVar

Generic(泛化)的高级用法,表示接受多个参数化泛化。

def move_first_element_to_last[T, *Ts](tup: tuple[T, *Ts]) -> tuple[*Ts, T]:
    return (*tup[1:], tup[0])

# T 被绑定为 int, Ts 被绑定为 ()
# 最终类型是 tuple[int], 返回值为 (1, )
move_first_element_to_last(tup=(1,))

# T 被绑定为 int, Ts 被绑定为 (str, )
# 返回值为 ('spam', 1), 返回类型为 tuple[str, int]
move_first_element_to_last(tup=(1, 'spam'))

# T 绑定为 int, Ts 绑定为 (str, float)
# 返回值为 ('spam', 3.0, 1), 返回类型为 tuple[str, float, int]
move_first_element_to_last(tup=(1, 'spam', 3.0))

# 类型检查和运行都会出错,至少需要一个值
# tuple[()] 和 tuple[T, *Ts] 是不同的
move_first_element_to_last(tup=())

ParamSpec

这也是 Generic 的高级用法,一般用于参数的传递。常用于高阶函数的参数传递、修改,比如 decorator 输入是一个函数,具体例子如下

from collections.abc import Callable
import logging

def add_logging[T, **P](f: Callable[P, T]) -> Callable[P, T]:
    '''A type-safe decorator to add logging to a function.'''
    def inner(*args: P.args, **kwargs: P.kwargs) -> T:
        logging.info(f'{f.__name__} was called')
        return f(*args, **kwargs)
    return inner

@add_logging
def add_two(x: float, y: float) -> float:
    return x + y

如果没有 ParamSpec 就只能写成 Callable[..., Any],这样的注释只能知道这是一个函数,不知道推断出函数的具体类型。

Reference