python3.5起python提供了类型标注支持(pep 484).之后的每个版本几乎都有对这一特性的改进.到了python3.10类型标注这一特性已经基本稳定.
ps:类型注释只是注释,python解释器并不会处理它,要让它有类型检验的功能还要有其他工具配合.
通常如果需要用一些更高版本的类型注释特性,我们可以使用typing_extensions这个包做补充.
目前的类型标注可以注释:
- 函数签名
- 变量
- 类中的属性
函数是最基本的类型声明场景.函数的参数使用:
指定类型,返回值则使用->
.需要注意,lambda函数无法声明其签名,我们只能在为期绑定变量时声明这个变量对应的签名
一个最基础的函数签名结构如下:
def func(arg:int)->int:
return arg
python的函数是很灵活的,可以有默认值,可以为None,也可以有多种可能的类型.typing提供了如下几种修饰描述:
Union
,用于描述或
关系,表示这个被声明的变量可以几种之一.我们可以使用|
作为简写
from typing import Union,Sequence
def handle_employees(e: Union[int, Sequence[int]]) -> None:
if isinstance(e, Employee):
e = [e]
from typing import Union,Sequence
def handle_employees_s(e: int| Sequence[int]) -> None:
if isinstance(e, Employee):
e = [e]
Optional
,相当于Union[类型, None]]
,表示这个变量可以为空.
from typing import Optional
def option_demo(x:int,y: Optional[int]=None) -> int:
if y:
return x+y
else:
return x
NoReturn
,只能用于描述返回值,表示这个函数永远不会有返回值,需要注意的是正常函数不写return
依然会返回None
,NoReturn
的确切含义其实是这个函数无论如何都会抛出一个错误.
from typing import NoReturn
def stop() -> NoReturn:
raise RuntimeError('no way')
-
overload
,声明函数或方法会重载已经定义的函数或方法.overload
是一个装饰器.@overload
装饰器可以修饰支持多个不同参数类型组合的函数或方法.@overload
装饰定义的一系列方法或函数必须紧跟在一个非@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):
-
final
,声明被装饰的方法不能被覆盖,且被装饰的类不能作为子类的装饰器.这通常用于在定义基类时使用.class Base: @final def done(self) -> None: ... class Sub(Base): def done(self) -> None: # Error reported by type checker ... @final class Leaf: ... class Other(Leaf): # Error reported by type checker ...
-
*Never
[3.11],用于作为函数的参数,描述这个函数不该被调用,一般用在还没实现的函数上
rom typing import Never
def never_call_me(arg: Never) -> None:
pass
类型标注既可以标注模块中常量全局变量,也可以标注函数方法中的内部变量以及类中的字段.变量声明使用:
语法,变量后面接:
然后是声明的类型,我们也可以再在后面加上= value
来直接为其赋个初值.
# 全局变量声明
CONST_A: int
CONST_B: int = 2
def fn_a(c:int)-> int:
# 局部变量声明
a: int = 1
b: int = 2
return a+b+c
类中属性分为类属性和实例属性,正常标注的都是实例属性,类属性需要使用typing.ClassVar
显式的声明出来
from typing import ClassVar
class CLZ_A:
captain: ClassVar[str] = 'Picard'# 类属性
damage: int # 实例属性
我们可以指定一个对象,通过调用标准库inspect
中的get_annotations(obj)
来获取模块,函数,类其中的类型声明,需要注意内部变量的声明无法获取.
import inspect
inspect.get_annotations(fn_a)
{'c': int, 'return': int}
inspect.get_annotations(CLZ_A)
{'captain': typing.ClassVar[str], 'damage': int}
Any类型和ts中一样,代表任意类型都可以.
from typing import Any
a: Any = 1
AnyStr相当于TypeVar('AnyStr', str, bytes)
,它可以用于描述字符串类型
from typing import AnyStr
a: AnyStr = "测试"
Text类型是用于和python2中进行兼容的类型,在python3中是str
的别名,Python 2
中是unicode
的别名.现在除了老旧代码维护已经基本用不到了.
用于指定变量的值等价于给定字面量(或多个字面量之一)的类型.这通常用在特定选项的情况下
from typing import Literal
b: Literal["1","2","3"]
b = 3
用于申明变量或参数是一个标准库io
中定义的I/O流的类型.typing.IO
是一个泛型类,它有两个实例子类:
typing.TextIO
对应io.StringIO
及对应的字符串为内容的其他流typing.BinaryIO
对应io.BytesIO
及对应的字节串为内容的其他流
from typing import TextIO
inio: TextIO
正则表达式操作中使用的对应类型,分为:
typing.Pattern
对应re.compile()
返回的类型re.Pattern
typing.Match
对应re.match()
返回的类型re.Match
这个类型的声明可以直接使用re
模块下的对应类型实现,减少对typing
的引用
import re
rem: re.Match
Type[C]
或者type[C]
表示C
的类型,而C
指代一个特定类型.举个例子.type(10)
的字面量是int
,如果x = type(10)
那么就可以这样声明x:type[int]
.
Type[C]
是一个协变量(Covariant),类型参数的关系满足协变性.在协变性中如果一个类型A
是另一个类型B
的子类型(或者可以看作A
拥有B
的所有行为和能力),那么泛型类型参数在A
中使用时可以替换为B
.换句话说协变性保持了类型参数的子类型关系.
协变性的关键特性是--可以将子类型的实例赋值给父类型的引用,而不会产生类型错误.这在某些情况下可以提供更灵活的类型使用和更好的代码复用.
利用协变性我们可以用它声明如下几种情况:
-
类方法中的类变量
class SelfTestClz: @classmethod def new_one(clz:type[Self])->Self: return clz()
-
参数必须是特定类子类的的实例的情况
from flask.views import MethodView
class APIView:
...
def register(self, url: str) -> Callable[[Type[MethodView]], Type[MethodView]]:
def wrap(clz: Type[MethodView]) -> Type[MethodView]:
self.restapi.add_url_rule(url, view_func=clz.as_view(clz.__name__))
return clz
return wrap
...
声明方法时每个实例方法都有一个self
变量,在Python 3.11
之前我们要么不声明类型要么用类名的字面量字符串来声明类型,在Python 3.11
中新增了占位类型Self
可以用于表示当前所在的实例的类型,因此我们可以像下面这样声明类中方法了
from typing_extensions import Self
class SelfTestClz:
@classmethod
def new_one(clz:type[Self])->Self:
return clz()
def __init__(self:Self)->None:
pass
注意类方法中的clz声明为了type[Self]
,其含义是Self
指代的实例类型的类型
容器可以分为具象容器类型和抽象容器类型. 具象容器类型会限制死特定的容器,而抽象容器类型则只会限制容器需要满足特定接口. 我们可以使用typing中的类型来申明也可以直接使用对应容器的工厂函数来申明,更加推荐使用对应容器的工厂函数来申明,这样可以少import很多东西,代码更整洁.
typing中的类型 | 对应容器工厂函数 | 满足的抽象容器类型 |
---|---|---|
Dict |
dict |
Mapping ,MutableMapping |
List |
list |
Sequence ,Iterable |
Tuple |
tuple |
--- |
NamedTuple |
collections.namedtuple |
--- |
Set |
set |
AbstractSet |
FrozenSet |
frozenset |
AbstractSet |
DefaultDict |
collections.defaultdict |
Mapping ,MutableMapping |
OrderedDict |
collections.OrderedDict |
Mapping ,MutableMapping |
ChainMap |
collections.ChainMap |
Mapping ,MutableMapping |
Counter |
collections.Counter |
Dict ,Mapping ,MutableMapping |
Deque |
collections.deque |
Sequence ,MutableSequence |
typing中的类型 | 对应容器工厂函数 | 特殊说明 |
---|---|---|
AbstractSet |
collections.abc.Set |
--- |
ByteString |
collections.abc.ByteString |
bytes ,bytearray ,memoryview 等字节序列类型 |
Collection |
collections.abc.Collection |
--- |
Container |
collections.abc.Container |
--- |
ItemsView |
collections.abc.ItemsView |
--- |
KeysView |
collections.abc.KeysView |
--- |
Mapping |
collections.abc.Mapping |
--- |
MappingView |
collections.abc.MappingView |
--- |
MutableMapping |
collections.abc.MutableMapping |
--- |
MutableSequence |
collections.abc.MutableSequence |
--- |
MutableSet |
collections.abc.MutableSet |
--- |
Sequence |
collections.abc.Sequence |
--- |
ValuesView |
collections.abc.ValuesView |
--- |
Iterable |
collections.abc.Iterable |
--- |
Iterator |
collections.abc.Iterator |
--- |
Generator |
collections.abc.Generator |
--- |
Hashable |
collections.abc.Hashable |
--- |
Reversible |
collections.abc.Reversible |
--- |
Sized |
collections.abc.Sized |
--- |
元组容器的声明语法是tuple[type,type,...]
元组的每一位可以是不同类型,因此元组有几位就需要声明出每一位的类型
test_tuple: tuple[str,int] = ("Tom",10)
NamedTuple
可以作为基类用于声明具名元组,这样声明的具名元组与用collections.namedtuple
构造的一样,而且可以包含声明信息.用的时候类似类实例化
from typing import NamedTuple
class Student(NamedTuple):
name: str
age: int
s1:Student
s1 = Student(name="Tom",age=10)
另一种简便声明方式是直接使用NamedTuple
的__call__
方法,其形式为变量名 = NamedTuple(具名元组名, [(字段名, 字段类型),...])
Employee = NamedTuple('Employee', [('name', str), ('id', int)])
e1:Employee
e1 = Employee(name="Tom",id=10)
映射型容器的声明形式为容器类型[键元素类型,值元素类型]
,python中默认只能声明同构映射,即容器为统一类型描述的序列,当然了我们可以用Union
或者Optional
修饰元素类型或者直接用Any
放松校验要求从而达到兼容异构映射的目的.
映射容器最常见的是dict
,但应当注意,在声明一些不太严格接口的的场合,比较好的方式是使用Mapping
.
from typing import Mapping
mp1:Mapping[str,int] = {"a":1,"b":2}
mq2:dict[str,int] = {"aa":1,"bb":2}
在定义接口时一种情况是倾向于给出宽泛的要求,比如一个接口可以传dict
,也可以传collections.defaultdict
,那我们就应该声明参数类型为Mapping
.但一些接口,尤其是涉及外部传参的接口,比如读取的配置后根据配置执行一些操作,那就会是另一种倾向,我们会希望指明字典中有特定我们关心的字段以及对应的类型.这种需求可以使用TypedDict
实现.
from typing import TypedDict
class Point2D(TypedDict):
x: int
y: int
label: str
p1:Point2D = {"x":0,"y":0,"label":"p1"}
另一种简便写法如下:
Point2D = TypedDict('Point2D', {'x': int, 'y': int, 'label': str})
这种方式非常适合定义一些有不符合python类字段命名要求键的字典.
像上面这样定义,参数就必须仅包含x
,y
,label
这几个字段,但有的时候我们希望字段是更灵活的形式,也就是
- 可以存在并没有被声明的字段
- 一些被声明的字段可以没有,但如果有就必须是指定类型
我们可以用参数total
来声明是否声明的字段就是全部允许的字段
class Point2D(TypedDict,total=False):
x: int
y: int
label: str
或
Point2D = TypedDict('Point2D', {'x': int, 'y': int, 'label': str}, total=False)
total
可以解决存在并没有被声明的字段的字段以及一些被声明的字段可以没有的问题,但它并不能解决特定字段必须有特定字段可以没有的问题.
在python 3.11中新增了修饰词Required
和NotRequired
用来声明指定字段的限制
class Point2D(TypedDict,total=False):
x: Required[int]
y: Required[int]
label: NotRequired[str]
或
Point2D = TypedDict('Point2D', {'x': Required[int], 'y': Required[int], 'label': NotRequired[str]}, total=False)
需要注意的是如果total
为True,则其中的字段默认为Required
,反之则默认为NotRequired
序列容器声明的形式为容器类型[元素类型]
,python中只能声明同构序列,即容器为统一类型描述的序列,当然了我们可以用Union
或者Optional
修饰元素类型或者直接用Any
放松校验要求从而达到兼容异构序列的目的.
序列容器最常用的是List
,但应当注意,在声明一些不太严格接口的的场合,比如作为一个要和比如numpy对接的接口时,比较好的方式是使用Sequence
.
from typing import Sequence
sq1:Sequence[int] = [1,2,3,4,5]
sq2:list[int] = [6,7,8,9,10]
对于生成器,它满足Generator
,Iterable
,Iterator
接口,因此可以根据实际情况声明其类型,
Generator[YieldType, SendType, ReturnType]
Iterable[YieldType]
Iterator[YieldType]
生成器函数一般指被调用后返回值是一个生成器的函数,其声明形式如下:
from typing import Generator
def my_generator_func(param1: int, param2: str) -> Generator[int, str, None]:
# 函数体逻辑
for i in range(param1):
yield f'{param2} {i}'
# 使用生成器函数
gen:Generator[int, str, None] = my_generator_func(5, 'Hello')
for item in gen:
print(item)
Hello 0
Hello 1
Hello 2
Hello 3
Hello 4
对于上下文管理器contextlib.AbstractContextManager
,可以使用ContextManager
来声明,它是一个泛型类,需要指定类型,且被指定的类型必须是满足上下文管理器的接口要求的上下文管理器类:
from typing import ContextManager
class MyContextManager:
def __enter__(self):
# 获取资源的逻辑
# 返回资源对象
pass
def __exit__(self, exc_type, exc_value, traceback):
pass
my_manager: ContextManager[MyContextManager] = MyContextManager()
当用于声明由@contextlib.contextmanager
装饰器构造的上下文管理器函数时ContextManager
中的参数则是上下文管理器函数中yied
出来的对象的类型
from typing import ContextManager
from contextlib import contextmanager
@contextmanager
def my_context_manager():
# 在进入上下文之前的逻辑
try:
# 获取资源的逻辑
resource = ... # 资源对象
yield resource
finally:
# 释放资源的逻辑
# 声明上下文管理器的类型
my_manager: ContextManager[<资源类型>] = my_context_manager()
这是上面上下文管理器函数的声明方法:
@contextmanager
def my_context_manager()->ContextManager[<资源类型>]:
# 在进入上下文之前的逻辑
try:
# 获取资源的逻辑
resource = ... # 资源对象
yield resource
finally:
# 释放资源的逻辑
协程类型使用Coroutine
或collections.abc.Coroutine
进行声明,其形式为Coroutine[YieldType, SendType, ReturnType]
from collections.abc import Coroutine
c: Coroutine[list[str], str, int] # Some coroutine defined elsewhere
x = c.send('hi') # Inferred type of 'x' is list[str]
async def bar() -> None:
y = await c
一个典型的异步函数如下:
from typing import Any
from collections.abc import Coroutine
async def format_string(tag: str, count: int) -> str:
return f'T-minus {count} ({tag})'
my_coroutine:Coroutine[Any, Any, str] = format_string("Millennium Falcon", 5)
await my_coroutine
'T-minus 5 (Millennium Falcon)'
常见的异步函数是不管YieldType
和 SendType
的
AsyncIterator
是一个专用于声明一步迭代器的泛型类,使用的时候需要指定它每步抛出的数据类型
from typing import Optional, AsyncIterator
import asyncio
class arange(AsyncIterator[int]):
def __init__(self, start: int, stop: int, step: int) -> None:
self.start = start
self.stop = stop
self.step = step
self.count = start - step
def __aiter__(self) -> AsyncIterator[int]:
return self
async def __anext__(self) -> int:
self.count += self.step
if self.count == self.stop:
raise StopAsyncIteration
else:
return self.count
async def countdown(tag: str, n: int) -> str:
async for i in arange(n, 0, -1):
print(f'T-minus {i} ({tag})')
await asyncio.sleep(0.1)
return "Blastoff!"
await countdown("tagtest",5)
T-minus 5 (tagtest)
T-minus 4 (tagtest)
T-minus 3 (tagtest)
T-minus 2 (tagtest)
T-minus 1 (tagtest)
'Blastoff!'
异步生成器AsyncGenerator
或者collections.abc.AsyncGenerator
使用AsyncGenerator[YieldType, SendType]
的形式,需要注意异步生成器没有返回值,所以形式和普通生成器不同.
from collections.abc import AsyncGenerator
ag: AsyncGenerator[int, float]
调用产生异步生成器的函数就是异步生成器函数,它的返回值是异步生成器
from collections.abc import AsyncGenerator
async def infinite_stream(start: int) -> AsyncGenerator[int, None]:
while True:
yield start
start = await increment(start)
类似普通上下文管理器,异步上下文管理器AsyncContextManager
或者contextlib.AbstractAsyncContextManager
也是一个泛型类,它需要指定资源类型来确定.
from contextlib import AbstractAsyncContextManager
class MyAsyncContextManager:
async def __aenter__(self):
# 获取资源的逻辑
# 返回资源对象
pass
async def __aexit__(self, exc_type, exc_value, traceback):
pass
my_manager: AbstractAsyncContextManager[MyAsyncContextManager] = MyAsyncContextManager()
当用于声明由@contextlib.asynccontextmanager
装饰器构造的上下文管理器函数时AsyncContextManager
中的参数则是上下文管理器函数中yied
出来的对象的类型
from contextlib import AbstractAsyncContextManager,asynccontextmanager
@asynccontextmanager
async def my_async_context_manager():
# 在进入上下文之前的逻辑
try:
# 获取资源的逻辑
resource = ... # 资源对象
yield resource
finally:
# 释放资源的逻辑
# 声明上下文管理器的类型
my_manager: AbstractAsyncContextManager[<资源类型>] = my_async_context_manager()
这是上面异步上下文管理器函数的声明方法:
@contextmanager
def my_context_manager()->AbstractAsyncContextManager[<资源类型>]:
# 在进入上下文之前的逻辑
try:
# 获取资源的逻辑
resource = ... # 资源对象
yield resource
finally:
# 释放资源的逻辑
可调用类型泛指那些可以被调用的类型,函数,方法,lambda,有__call__
方法的类实例都可以用可调用类型来描述.
其形式为Callable[[参数1,参数2],返回]
.如果参数为不定参数,可以使用...
表示,如果参数为空,可以用[]
表示,如果返回值为空,可以使用None
表示.
from typing import Callable
def feeder(get_next_item: Callable[[], str]) -> None:
pass
def async_query(on_success: Callable[[int], None],
on_error: Callable[[int, Exception], None]) -> None:
pass
不定参数我们可以使用...
表示,但这种方式有点抽象,如果参数只为*args,**kwargs
就可以使用参数规范变量ParamSpec
声明一个函数的参数,就像下面这样:
from collections.abc import Callable
from typing import ParamSpec
import logging
P = ParamSpec('P')
def testParamSpec(f: Callable[P, str],*args: P.args, **kwargs: P.kwargs) -> str:
return f(*args,**kwargs)
testParamSpec(lambda x: f"echo {str(x)}","hello")
'echo hello'
需要注意ParamSpec的实例不是Callable参数位置中填写的类型,而是参数位置本身.
一些情况我们可能需要描述的可调用对象除了有不定参数*args,**kwargs
,也有一些指定了的函数.这种时候我们就可以使用Concatenate
将正常参数类型和参数规范变量进行连接
from collections.abc import Callable
from typing import ParamSpec, Concatenate, Any
import logging
P = ParamSpec('P')
def testConcatenate(f: Callable[Concatenate[int,str,P], str],*args: P.args, **kwargs: P.kwargs) -> str:
return f(1,"Concatenate",*args,**kwargs)
testConcatenate(lambda a,b,*args,**kwargs: f"echo a: {a} b:{b},args:{args}","hello")
"echo a: 1 b:Concatenate,args:('hello',)"
装饰器作为一类使用函数作为参数且返回函数的函数可以大量的使用到ParamSpec
和Concatenate
,比如下面:
from collections.abc import Callable
from threading import Lock
from typing import Concatenate, ParamSpec, TypeVar
P = ParamSpec('P')
R = TypeVar('R')
# Use this lock to ensure that only one thread is executing a function
# at any time.
my_lock = Lock()
def with_lock(f: Callable[Concatenate[Lock, P], R]) -> Callable[P, R]:
'''A type-safe decorator which provides a lock.'''
def inner(*args: P.args, **kwargs: P.kwargs) -> R:
# Provide the lock as the first argument.
return f(my_lock, *args, **kwargs)
return inner
@with_lock
def sum_threadsafe(lock: Lock, numbers: list[float]) -> float:
'''Add a list of numbers together in a thread-safe manner.'''
with lock:
return sum(numbers)
# We don't need to pass in the lock ourselves thanks to the decorator.
sum_threadsafe([1.1, 2.2, 3.3])
6.6
一些时候我们希望给特定类型一个别名以明确其含义,这时可以直接使用
Url = str
def retry(url: Url, retry_count: int) -> None:
pass
更加推荐的是使用TypeAlias
显式的声明
from typing import TypeAlias
factors: TypeAlias = list[int]
类型别名毕竟只是别名,比如上面的例子,在url参数中直接填一个str类型的数据不会有任何问题,一些时候我们希望更加明确的声明一个新类型以避免函数在调用时被传入旧类型,这种时候就可以使用NewType
来实现.注意NewType
仅是一个声明,只会在类型检验时有效,运行时和别名行为一致.
from typing import NewType
UserId = NewType('UserId', int)
some_id = UserId(524313)
def get_user_name(user_id: UserId) -> str:
return str(user_id)
类型注释可以直接使用系统自带的类和自己定义的类,但对于泛型注解就力不从心了,对于这种需求,python内置了typing模块来帮助泛型注释
用过强类型编程语言的都应该知道泛型,泛型指的是一个描述类型的类型,通常泛型是和多态一起的,泛型是多态的一个实现方式.python天然多态,泛型就似乎有点脱裤子放屁了.但也不是全无用处,它起码可以在同一个上下文中明确类型不变.比如我们想声明一个从序列中找出第一个item的函数,这时候就可以像下面这样声明:
from typing import Sequence, TypeVar
T = TypeVar('T') # Declare type variable
def first(l: Sequence[T]) -> T: # Generic function
return l[0]
泛型更常用的方法是受限泛型,我们可以明确这个泛型的类型可以在特定的一个范围内.
from typing import TypeVar
AnyStr = TypeVar('AnyStr', str, bytes)#必须是str或者bytes
def concat(x: AnyStr, y: AnyStr) -> AnyStr:
return x + y
用户定义的类可以定义为泛型类.
from typing import TypeVar, Generic
from typing import Iterable
class Logger:
pass
T = TypeVar('T')
class LoggedVar(Generic[T]):
def __init__(self, value: T, name: str, logger: Logger) -> None:
self.name = name
self.logger = logger
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:
self.logger.info('{}: {}'.format(self.name,message))
def zero_all_vars(vars: Iterable[LoggedVar[int]]) -> None:
for var in vars:
var.set(0)
其中继承的Generic[T]
表示这是一个类是泛型类,且T
可以在这个函数的定义上下文中被统一.使用的时候T可以被替换为定义时圈定范围内的类型.
泛型类需要在使用时声明其中的泛型具体是什么类型,比如定义如下函数:
from collections.abc import Iterable
def zero_all_vars(vars: Iterable[LoggedVar[int]]) -> None:
for var in vars:
var.set(0)
LoggedVar[int]
就明确了这个函数中泛型类中泛型的专指int类型.
python是鸭子类型,协议是其鸭子类型的底层数据模型,一些时候我们希望静态检测按照协议而非类型运行,这时就可以使用Protocol
来定义,该特性可以参考pep-544.
简单来说我们可以继承Protocol
来规定一个协议类,这个协议类中可以定义字段也可以定义方法,当使用这个协议类作为约束时静态校验器就会检查实例否有对应的字段和方法,从而判断是否满足协议.
这种用法有点类似go中的接口interface
的设定.协议本身不实现功能,仅作为约定存在.
from typing import Protocol, List, runtime_checkable
from abc import abstractmethod
from typing_extensions import Self
@runtime_checkable
class Template(Protocol):
name: str # This is a protocol member
value: int = 0 # This one too (with default)
def method_with_implement(self:Self) -> str:
return "deep blue"
def method_with_pass_implement(self) -> None:
...
@abstractmethod
def abstractmethod_without_implement(self) -> int:
return 0
我们可以使用runtime_checkable
装饰Protocol
的子类,这样就可以在运行时使用isinstance()
来检测对象是否符合协议了
def test_template(t:Template)->str:
return t.method_with_implement()
isinstance(Template,int)
False
可以使用cast
函数强行将一个对象重新声明为特定类型
from typing import cast
value = 15
newvalue = cast(NewType,value)
newvalue
15
python解释器并不会做静态类型检验,我们可以利用mypy来实现
%%writefile examples/typehints/mypytest.py
from typing import Callable
def twice(i: int, next: Callable[[int], int]) -> int:
return next(next(i))
def add(i: int) -> str:#写成返回str,这样就会报错!
return i + 1
print(twice(3, add)) # 5
Overwriting examples/typehints/mypytest.py
!mypy examples/typehints/mypytest.py
examples/typehints/mypytest.py:8: �[1m�[31merror:�[m Incompatible return value type (got �[m�[1m"int"�[m, expected �[m�[1m"str"�[m) �[m�[33m[return-value]�[m
examples/typehints/mypytest.py:10: �[1m�[31merror:�[m Argument 2 to �[m�[1m"twice"�[m has incompatible type �[m�[1m"Callable[[int], str]"�[m; expected �[m�[1m"Callable[[int], int]"�[m �[m�[33m[arg-type]�[m
�[1m�[31mFound 2 errors in 1 file (checked 1 source file)�[m
如果我们要在不改变python脚本源码的情况下为其声明接口类型,可以通过stub files
的形式来实现.所谓stub files
指的是纯用于声明接口类型的的一类文件,使用后缀为.pyi
.
stub files
存放位置有两种:
-
放在对应
.py
文件同目录下 -
按实现模块的文件层次结构将对应文件的
stub files
存放在固定文件夹中,比如项目根目录在~/work/myproject
.模块在~/work/myproject/pkg1
,stub files
可以存放在~/work/myproject/stubs
,在校验时添加环境变量export MYPYPATH=~/work/myproject/stubs
即可
无论哪种方式存放,stub files
中的声明都会覆盖.py
实现文件中的声明,这样如果有个库没有类型声明,你可以自己给他加上stub files
从而为其提供本地的类型声明
stub files
中的内容只有接口声明不包含实现
-
全局变量声明:
x: int
-
函数声明:
def func1(a: int, b: int = ...) -> int: ... def func2(a: int, b: int = ...) -> int: raise NotImplementedError() def func3(a: int, b: int = ...) -> int: """Some docstring.""" pass
在
stub files
中函数的实现部分通常使用...
表示;如果确定这个函数并未实现,我们也可以在申明处直接抛出NotImplementedError()
;如果要将函数的docstring
也写在存根文件中则应该在docstring
下面写上pass
忽略实现部分.同时有默认值的参数,默认值也使用...
表示以避免和实现部分冲突. -
类声明
class Resource: bar: str def ok_1(self, foo: list[str] = ...) -> None: ... def ok_2(self, foo: list[str] = ...) -> None: raise NotImplementedError() def ok_3(self, foo: list[str] = ...) -> None: """Some docstring""" pass
类中的方法和函数遵循同样的规则.
标准库自带的typing只能用于静态检测,当我们需要运行时检测时可以借助typeguard来实现.typeguard
使用装饰器语法,
它提供了装饰器 @typechecked
用于运行时进行类型检测.同时提供了工具check_type
来对对象的类型和指定声明类型进行比较.
还提供了install_import_hook
用于全局打开运行时类型检测.
不过type hints设计的出发点就是静态检验,运行时进行类型检验势必会拉慢python程序的运行速度.python本就慢,python 3.11花了一整个版本的开发时间也就整体提速了30%,我们实在是没有必要在这种地方拖慢运行时间,得不偿失.