Skip to content

Latest commit

 

History

History
1137 lines (722 loc) · 31.5 KB

静态类型检测.md

File metadata and controls

1137 lines (722 loc) · 31.5 KB

*类型注释和检验

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类型

Any类型和ts中一样,代表任意类型都可以.

from typing import Any
a: Any = 1

AnyStr类型

AnyStr相当于TypeVar('AnyStr', str, bytes),它可以用于描述字符串类型

from typing import AnyStr
a: AnyStr = "测试"

Text类型

Text类型是用于和python2中进行兼容的类型,在python3中是str的别名,Python 2中是unicode的别名.现在除了老旧代码维护已经基本用不到了.

Literal类型

用于指定变量的值等价于给定字面量(或多个字面量之一)的类型.这通常用在特定选项的情况下

from typing import Literal

b: Literal["1","2","3"]
b = 3

IO类型

用于申明变量或参数是一个标准库io中定义的I/O流的类型.typing.IO是一个泛型类,它有两个实例子类:

  • typing.TextIO对应io.StringIO及对应的字符串为内容的其他流
  • typing.BinaryIO对应io.BytesIO及对应的字节串为内容的其他流
from typing import TextIO

inio: TextIO

re类型

正则表达式操作中使用的对应类型,分为:

  • typing.Pattern对应re.compile()返回的类型re.Pattern
  • typing.Match对应re.match()返回的类型re.Match

这个类型的声明可以直接使用re模块下的对应类型实现,减少对typing的引用

import re

rem: re.Match

Type类型

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
    ...

*[3.11]Self类型

声明方法时每个实例方法都有一个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这几个字段,但有的时候我们希望字段是更灵活的形式,也就是

  1. 可以存在并没有被声明的字段
  2. 一些被声明的字段可以没有,但如果有就必须是指定类型

我们可以用参数total来声明是否声明的字段就是全部允许的字段

class Point2D(TypedDict,total=False):
    x: int
    y: int
    label: str

Point2D = TypedDict('Point2D', {'x': int, 'y': int, 'label': str}, total=False)

*使用修饰词修饰限制字典的字段[3.11]

total可以解决存在并没有被声明的字段的字段以及一些被声明的字段可以没有的问题,但它并不能解决特定字段必须有特定字段可以没有的问题.

在python 3.11中新增了修饰词RequiredNotRequired用来声明指定字段的限制

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:
        # 释放资源的逻辑

协程类型的申明

协程类型使用Coroutinecollections.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)'

常见的异步函数是不管YieldTypeSendType

异步迭代器声明

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

Callable中参数*args,**kwargs的声明

不定参数我们可以使用...表示,但这种方式有点抽象,如果参数只为*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参数位置中填写的类型,而是参数位置本身.

Callable中含*args,**kwargs的参数声明

一些情况我们可能需要描述的可调用对象除了有不定参数*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',)"

装饰器声明

装饰器作为一类使用函数作为参数且返回函数的函数可以大量的使用到ParamSpecConcatenate,比如下面:

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存放位置有两种:

  1. 放在对应.py文件同目录下

  2. 按实现模块的文件层次结构将对应文件的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%,我们实在是没有必要在这种地方拖慢运行时间,得不偿失.