Python typing

By | 7月 18, 2023

Python typing模块是3.5引入的,Python解释器没有强制要求添加类型,类型注解只是为了给type checkers, IDE,linters等用的。

PyCharm和VS Code都支持类型检查,Eclipse需要配置mypy,运行mypy来检查。

类型别名

对复杂类型,可以起个别名,方便重复使用。

from typing import List, Callable

Vector = List[int]
CallBack = Callable[[int], str]

def mymap(nums: Vector, action: CallBack):
    action(len(nums))
    return [i*2 for i in nums]

NewType

Derived = NewType('Derived', Original)

静态检查器把Derived当成Original的子类,意味着Original类型的值不能用于Derived类型的值需要的地方。

运行时NewType(‘Derived’, Original)创建一个Derived函数,该函数立即返回传给它的任何参数;意味着不会创建一个新的类,没有引入很多开销。

下面的例子中 UserId 是 int 类型,print_id()函数传入int值,运行没有问题,不过编译器会报错。类型别名是可以互相替换的,感觉NewType是更严格的类型别名。

UserId = NewType('UserId', int)

def print_id(id: UserId):
    print('The ID is', id)

# Error: Expected type 'UserId', got 'int' instead 
print_id(1234)
print_id(UserId(1234))

Callable

用于定义函数的类型 Callable[[Arg1Type, Arg2Type], ReturnType]

from typing import Callable

def feeder(get_next_item: Callable[[], str]) -> None:
    # Body

def async_query(on_success: Callable[[int], None],
                on_error: Callable[[int, Exception], None]) -> None:
    # Body

泛型(Generic)

使用TypeVar()生成一个泛型类型。

泛型函数

from typing import Sequence, TypeVar

T = TypeVar('T')      # Declare type variable

def first(l: Sequence[T]) -> T:   # Generic function
    return l[0]

# Expected type 'list', got 'int' instead 
value: List = first([1, 2, 3])

泛型类

from typing import TypeVar, Generic

T = TypeVar('T')

class LoggedVar(Generic[T]):
    def __init__(self, value: T, name: str) -> None:
        self.name = name
        self.value = value

    def set(self, new: T) -> None:
        self.log('Set ' + repr(self.value))
        self.value = new

    def get(self) -> T:
        self.log('Get ' + repr(self.value))
        return self.value

    def log(self, message: str) -> None:
        print('%s: %s', self.name, message)


var = LoggedVar(888, 'Record')
# Expected type 'int' (matched generic type 'T'), got 'str' instead 
var.set('1234')
var.get()

限制类型只能是int或str

from typing import TypeVar, Generic

T = TypeVar('T', int, str)
# Expected type 'T', got 'tuple[int, int]' instead 
val: T = (1, 2)

继承了Generic的类,还能同时继承其它类,支持多重继承。

from typing import TypeVar, Generic, Sized

T = TypeVar('T')

class LinkedList(Sized, Generic[T]):
    ...

继承多个参数的泛型类时,可以固定某些类型,例如固定Mapping的key是str。

from typing import TypeVar, Mapping

T = TypeVar('T')

class MyDict(Mapping[str, T]):
    ...

没有指定类型的泛型类会被假设为Any,下例的Iterable等同于Iterable[Any]。

from typing import Iterable

class MyIterable(Iterable): # Same as Iterable[Any]
    ...

类型别名中如果有泛型类型,该类型别名也是泛型的,使用时要加上类型。

from typing import  TypeVar, Iterable, Tuple, Union

S = TypeVar('S')
Response = Union[Iterable[S], int]

# Return type here is same as Union[Iterable[str], int]
def response(query: str) -> Response[str]:
    ...


T = TypeVar('T', int, float, complex)
Vec = Iterable[Tuple[T, T]]

# Same as Iterable[Tuple[T, T]]
def inproduct(v: Vec[T]) -> T:
    return sum(x*y for x, y in v)

Any 类型

Any表示任意类型,可以执行任何操作。只有不知道类型时才定义成Any,否则就失去类型检查的意义了。

from typing import Any
a: Any = 1
# No type issue but fail at runtie
a.adsfs(1)

静态类型和鸭子类型

静态类型

Python引入的typing模块属于静态类型,也叫名义类型(nominal type),如果B是A的子类,则B可以用在需要A的地方。下面的Bucket类是个迭代器:

from typing import Sized, Iterable, Iterator

class Bucket(Sized, Iterable[int]):
    ...
    def __len__(self) -> int: ...
    def __iter__(self) -> Iterator[int]: ...

鸭子类型

鸭子类型,也叫结构化类型(structural type),只要类定义了某类型的方法,它就可以当该类型使用。例如类定义了__len__方法和__iter__方法,就是迭代器。下面的例子,如果注释掉__iter__方法,最后一行调用处,PyCharm会报错。

from typing import Iterator, Iterable

class Bucket:  # Note: no base classes
    ...
    def __len__(self) -> int: ...
    def __iter__(self) -> Iterator[int]: ...

def collect(items: Iterable[int]) -> int: ...
result = collect(Bucket())  # Passes type check

Pycharm类型检查很智能,即支持静态类型又支持鸭子类型。

自定义鸭子类型 (Protocol)

用于定义自己的鸭子类型,工具如PyCharm会自动检查。

A继承了Protocol类,某类只要具有和A相同的方法,就可以当成A类型。下面的例子C类的方法和Proto类一样,就可以当成Proto类型。注意:只检查任何def声明的方法(包括__init__方法);手动给self添加的属性,不会被检查。

from typing import  Protocol

class Proto(Protocol):
    def meth(self) -> int:
        ...

class C:
    def meth(self) -> int:
        return 0

def func(x: Proto) -> int:
    return x.meth()

func(C())  # Passes static type check

Protocol类也可以是泛型的。

from typing import  TypeVar, Protocol

T = TypeVar('T')
class GenProto(Protocol[T]):
    def meth(self) -> T:
        ...

常用类型声明

list[int] / typing.List[T]

val: list[int] = [1, 2, 3, 3]

typing.Tuple[int, str]

from typing import Tuple
val: Tuple[int, str] = (1, 'abc')

typing.Sequence[int]

from typing import Sequence
val: Sequence[int] = [1, 2, 3]

typing.Iterable[int]

from typing import Iterable
val: Iterable[int] = [1, 2, 3]
for i in val:
    print(i)

set[int] / typing.Set[int]

val: set[int] = set()
val.add(1)
val.add(1)

frozenset[int] / typing.FrozenSet[int]

# There's no add method
val: frozenset[int] = frozenset([1, 2, 3])

bytes / bytesarray

val: bytes = b'hello'
val2: bytearray = bytearray(b'hello')
val2.append(12)
val2.append(80)

dict[str, int] / typing.Dict[str, int]

第一个为key类型,第二个为value类型。

# Expected type 'dict[str, int]', got 'dict[str, str | tuple[int, int]]' instead 
val: dict[str, int] = {
    'name': 'alex',
    'other': (1, 2)
}

字典就如Java里的Map,通过方括号直接添加元素。

num_map: dict[str, int] = {}
num_map['num1'] = 1
num_map['num2'] = 2

num_map.keys()
num_map.values()
num_map.items()

typing里的其它类

typing.Any

任何类型,表示没有类型限制。

typing.NoReturn

标记函数没有返回值。

from typing import NoReturn

def stop() -> NoReturn:
    raise RuntimeError('no way')

typing.Union

联合类型;Union[X, Y],以为着是X类型,或者Y类型。Optional[X] 等于 Union[X, None]。

typing.Optional

Optional[X]表示为X类型或者None。

def foo(arg: Optional[int] = None) -> None:
    ...

typing.Final

表明变量不能再次赋值,或者不能在子类中被重载。类似Java中的final。

from typing import Final

MAX_SIZE: Final[int] = 9000
MAX_SIZE += 1  # Error reported by type checker

class Connection:
    TIMEOUT: Final[int] = 10

class FastConnector(Connection):
    TIMEOUT = 1  # Error reported by type checker

typing.Literal

用于声明字面量类型。

  • Literal[True] 表示只能是 True
  • Literal[‘r’, ‘rb’, ‘w’, ‘wb’] 表示只能输入’r’, ‘rb’, ‘w’或’wb’
from typing import Literal, Any

def validate_simple(data: Any) -> Literal[True]:  # always returns True
    ...

MODE = Literal['r', 'rb', 'w', 'wb']
def open_helper(file: str, mode: MODE) -> str:
    ...

open_helper('/some/path', 'r')  # Passes type check
open_helper('/other/path', 'typo')  # Error in type checker