函数注解
变量注解
inspect
pydantic

注解


python语言的缺点

  • python是动态语言,变量可被赋值不同类型,不能做类型检查,变量的类型是在运行期间决定的
  • 解决方案
    • 增加文档说明docmentation string
    • py3.5引入函数注解
    • py3.6引入变量注解

初识函数注解和变量注解

def foo(x: int, y: int) -> tuple:  # 函数注解,非强制
    z: list[int] = [1]  # 变量注解,非强制
    z.append(y)
    return x + y, z


print(foo(1, 2))
print(foo('a', 'b'))
# print(foo(1, 'c')) # 不能够进行类型检查,报错TypeError: unsupported operand type(s) for +: 'int' and 'str'
print(foo.__annotations__)  # 函数注解保存在__annotations__属性中
---------------------------------------------------------------------
(3, [1, 2])
('ab', [1, 'b'])
{'x': <class 'int'>, 'y': <class 'int'>, 'return': <class 'tuple'>}
  • 函数注解:对函数的参数和返回类型进行类型注解
  • 变量注解:对变量进行注解,同时仍可指定默认值
  • 注解只是一个辅助说明,并不对函数参数进行类型检查,对变量进行强制要求
  • 函数注解信息保存在__annotations__属性中,返回值为字典类型

注解的作用

  • 对参数进行辅助说明
  • 提供给第三方工具做代码分析,比如类型检查器、集成开发环境、静态检查器等
  • 在IDE中能够代码联想

函数注解

参考文档

基本语法

  1. 函数参数
def foo(a: expression, b: expression = 5):
    ...
def foo(*args: expression, **kwargs: expression):
    ...
def foo((x1, y1: expression),
        (x2: expression, y2: expression)=(None, None)):
    ...

使用:语句将信息附加到变量或函数参数中

  1. 返回值
def sum() -> expression:
    ...

->运算符用于将信息附加到函数/方法的返回值中

  1. lambda表达式不支持注解
  2. 函数注解信息保存在__annotations__属性中,返回值为字典类型,包括返回值类型的声明

变量注解

from typing import ClassVar, Dict
class Starship:
    captain: str = 'Picard'
    damage: int
    stats: ClassVar[Dict[str, int]] = {}

    def __init__(self, damage: int, captain: str = None):
        self.damage = damage
        if captain:
            self.captain = captain  # Else keep the default

    def hit(self):
        Starship.stats['hits'] = Starship.stats.get('hits', 0) + 1

enterprise_d = Starship(3000)
enterprise_d.stats = {} # Flagged as error by a type checker
Starship.stats = {} # This is OK
  • 同一函数范围内注释受全局变量global或非局部变量nonlocal影响的变量是非法的
def f():
    global x: int  # SyntaxError

def g():
    x: int  # Also a SyntaxError
    global x

类型检查

通过上面的分析,python 运行时并不强制标注函数和变量类型,注解也不会进行类型检查。接下来介绍几种类型检查的方式

inspect模块

提供获取对象信息的函数,可以检查函数和类、类型检查

文档

inspect使用

  • inspect.signature(callable):获取签名(函数签名包含了一个函数的信息,包括函数名、它的参数类型、它所在的类和名称空间及其他信息)
import inspect

def add(x: int, y: int, *args, **kwargs) -> int:
    return x + y

sig = inspect.signature(add)
print(1, add.__annotations__)
print(2, sig, type(sig))  # 函数签名
print('3 params : ', sig.parameters)  # OrderedDict
print('4 return : ', sig.return_annotation)
print(5, sig.parameters['y'], type(sig.parameters['y']))
print(6, sig.parameters['x'].annotation)
print(7, sig.parameters['args'])
print(8, sig.parameters['args'].annotation)  # 无注解  <class 'inspect._empty'>
print(9, sig.parameters['kwargs'])
print(10, sig.parameters['kwargs'].annotation)
--------------------------------------
1 {'x': <class 'int'>, 'y': <class 'int'>, 'return': <class 'int'>}
2 (x: int, y: int, *args, **kwargs) -> int <class 'inspect.Signature'>
3 params :  OrderedDict([('x', <Parameter "x: int">), ('y', <Parameter "y: int">), ('args', <Parameter "*args">), ('kwargs', <Parameter "**kwargs">)])
4 return :  <class 'int'>
5 y: int <class 'inspect.Parameter'>
6 <class 'int'>
7 *args
8 <class 'inspect._empty'>
9 **kwargs
10 <class 'inspect._empty'>
  • is函数

inspect.isfunction(add),是否是函数
inspect.ismethod(add)),是否是类的方法
inspect.isgenerator(add)),是否是生成器对象
inspect.isgeneratorfunction(add)),是否是生成器函数
inspect.isclass(add)),是否是类
inspect.ismodule(inspect)),是否是模块
inspect.isbuiltin(print)),是否是内建对象
还有很多is函数,需要的时候查阅inspect模块帮助

  • Parameter对象
    • 保存在元组中,是只读的
    • name,参数的名字
    • annotation,参数的注解,可能没有定义
    • default,参数的缺省值,可能没有定义
    • _empty,特殊的类,用来标记default属性或者注释annotation属性的空值
    • kind,实参如何绑定到形参,就是形参的类型
      • POSITIONAL_ONLY,值必须是位置参数提供
      • POSITIONAL_OR_KEYWORD,值可以作为关键字或者位置参数提供
      • VAR_POSITIONAL,可变位置参数,对应*args
      • KEYWORD_ONLY,keyword-only参数,对应*或者*args之后的出现的非可变关键字参数
      • VAR_KEYWORD,可变关键字参数,对应**kwargs
import inspect

def add(x, y: int = 7, *args, z, t=10, **kwargs) -> int:
    return x + y

sig = inspect.signature(add)
print(sig)
print('params : ', sig.parameters)  # 有序字典

for i, item in enumerate(sig.parameters.items()):
    name, param = item
    print(i + 1, name, param.annotation, param.kind, param.default)
    print(param.default is param.empty, end='\n\n')

类型检查示例

import inspect

def check(fn):
    def wrapper(*args, **kwargs):
        sig = inspect.signature(fn)
        params = sig.parameters
        values = list(params.values())
        for i, p in enumerate(args):
            param = values[i]
            if param.annotation is not param.empty and not isinstance(p, param.annotation):
                print(p, '!==', values[i].annotation)
        for k, v in kwargs.items():
            if params[k].annotation is not inspect._empty and not isinstance(v, params[k].annotation):
                print(k, v, '!===', params[k].annotation)
        return fn(*args, **kwargs)

    return wrapper

@check
def add(x, y: int = 7) -> int:
    return x + y

add(20, 10)
add(20, y=10)
add(y=10, x=20)

pydantic模块

from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, ValidationError


class User(BaseModel):
    id: int
    name = 'John Doe'
    signup_ts: Optional[datetime] = None
    friends: List[int] = []


external_data = {
    'id': '123',
    'signup_ts': '2019-06-01 12:22',
    'friends': [1, 2, '3'],
}
user = User(**external_data)
print(user.id)
print(repr(user.signup_ts))
print(user.friends)
print(user.dict())

try:
    User(signup_ts='broken', friends=[1, 2, 'not number'])
except ValidationError as e:
    print(e)
-----------------------------------------------
{'id': 123, 'signup_ts': datetime.datetime(2019, 6, 1, 12, 22), 'friends': [1, 2, 3], 'name': 'John Doe'}
3 validation errors for User
id
  field required (type=value_error.missing)
signup_ts
  invalid datetime format (type=value_error.datetime)
friends -> 2
  value is not a valid integer (type=type_error.integer)
  • 示例2:@validator自定义errors
from pydantic import BaseModel, ValidationError, validator

class UserModel(BaseModel):
    name: str
    username: str
    password1: str
    password2: str

    @validator('name')
    def name_must_contain_space(cls, v):
        if ' ' not in v:
            raise ValueError('must contain a space')
        return v.title()

    @validator('password2')
    def passwords_match(cls, v, values, **kwargs):
        if 'password1' in values and v != values['password1']:
            raise ValueError('passwords do not match')
        return v

    @validator('username')
    def username_alphanumeric(cls, v):
        assert v.isalnum(), 'must be alphanumeric'
        return v


user = UserModel(
    name='samuel colvin',
    username='scolvin',
    password1='zxcvbn',
    password2='zxcvbn',
)
print(user) # name='Samuel Colvin' username='scolvin' password1='zxcvbn' password2='zxcvbn'

try:
    UserModel(
        name='samuel',
        username='scolvin',
        password1='zxcvbn',
        password2='zxcvbn2',
    )
except ValidationError as e:
    print(e)
    """
    2 validation errors for UserModel
    name
      must contain a space (type=value_error)
    password2
      passwords do not match (type=value_error)
    """

更多用法可参考pydantic官方手册

其他

扩展阅读


参考


  • magedu