
离谱的项目体验不吐不快真的被公司的 xx 项目折磨太久了。包括但不限于以下方面:类或者方法缺少说明参数类型注释缺失结构划分不合理常常出现循环引用随意的变量名混乱的逻辑和重复出现的代码糟糕的性能(例如批量任务串行, 数据库表设计不合理导致的多表连接以及字段冗余等)最令人痛苦的莫过于第二点参数类型注释的缺失。举个例子:api.route(/server/create,methods[post])defserver_create():bodyrequest.json providerbody.get(provider)ifprovideraliyun:aliyun_ecs.create_server(**body)elifproviderhuawei:huawei_ecs.create_server(**body)...上面这段代码起码有 3 个地方需要改进确定 body 的字段封装成class对传入的 body 校验利用工厂模式或者适配器统一不同云商调用body是一个字典类型至于里面的字段就不得而知了。而且不同云商body还不一样重构的时候真是让人头大。虽然 python作为动态类型编程语言运行时不需要指定变量类型。但是那样也会对代码的可维护性带来挑战。下面聊聊为什么要写参数类型注释为什么需要类型注释就像上面描述的情况良好的类型注释可以使代码更容易阅读维护和维护。除此之外还能带来以下几点便利编辑器提示例如上面的hello函数在声明name为str类型后编辑器可以自动列出/补全变量或者方法。但是在bye函数中就无法补全而且也无法显示capitalize()方法文档编辑器类型检查如果你的编辑器开启了类型检查linter, 例如mypy。再或者使用的是vscode编辑器并且安装了 python 插件可以在设置中打开type checking mode。可以提示类型错误帮助你尽早发现异常。[!Tip]运行时解释器CPython不会尝试在运行时推断类型信息或者验证基于此传递的参数。运行时解释器CPython不使用类型信息来优化生成的字节码以获得安全性或性能。执行Python脚本时类型提示被视为注释解释器自动忽略。python 类型注释发展历史大家可能会好奇以前写代码的时候基本都没有类型注释啊, 甚至各种python教程中几乎都没有和类型注释相关内容怎么现在突然开始提倡了因为早期的python版本 (3.0之前) 这方面的规范还不完善没有统一标准规定类型注释怎么写。如果要实现类型注释以及检测这类的功能只能依赖第三方组件例如mypy。 社区也意识到这个问题同时受到第三方linter启发逐渐形成了当前的注释规范。接下来简单盘点下python 类型注释的发展历程pep-3107 提出注释语法 (2006)在2006年提出的 PEP-3107 中介绍了函数注释的写法deffoo(a:expression,b:expression)-expression:...此时并未提及有关类型提示的相关的内容只是规定了注释的定义。所以早期的python 代码中基本上是没有写类型注释的习惯。 因为python 本身就是动态语言不需要提前知道传入值的类型。规范一点的项目可能会在定义方法的时候增加文档说明。如果要获取函数注释可以通过__annotations__获取。例如defhello(name:your first namejack)-None:print(hello,name)hello.__annotations__# {name: your first name, return: None}值得一提的是lambda匿名函数不支持注释。pep-484 提出类型提示 (2014年9月, python3.5)受python 类型检测lintermypy的启发 2014年9月 pep-484 在python3.5版本中引入了typing包里面定义了几乎所有的python class例如None,Any,Union,Callable等。同时也允许用户自定义泛型defsend_http_request(url:str,timeout:int)-bool:...pep-0562 提出变量类型提示 (2016年8月, python3.6)由于pep-484中只定义了针对方法参数类型提示的规范如果想为变量声明类型又该怎么做呢2016年8月 pep-562 中定义了这一规范。例如我想声明一个数字列表:some_list:List[int][]pep-0563 延迟注释评估 (2017/09 python3.7)延迟注释评估简单来说就是类型提示包含的类还未被声明如果直接使用会产生错误。例如:classA:classmethoddefinit(cls)-A:returnA()aA()# NameError: name A is not defined其实在 pep-484 Forward references 中提到了一种叫做正向引用的方案, 即利用引号将类型定义为一个字符串这会使得它在类型声明完成后再解析。所以上面示例代码可以这样改动:classA:classmethoddefinit(cls)-A:returnA()aA()# its ok!在pep-0563中上面代码也可以这样写:from__future__importannotationsclassA:classmethoddefinit(cls)-A:returnA()通过引入from __future__ import annotations来实现延迟。一旦解释器解析脚本语法树后它会识别类型提示并绕过评估它将其保留为原始字符串。这种机制使得类型提示发生在需要它们的地方由linter来进行类型检查。 在Python 4中这种机制将成为默认行为。既然在pep-484中可以通过加引号的方式实现延迟效果那为什么还需要再出一个方案呢其一为上面提到的延迟评估 (用引号不就变字符串了吗有点反直觉。大部分人还是更倾向直接用类名虽然背后原理还是保存为字符串…)其二为优化type hints性能关于性能问题在pep-0560 有提及。主要是因为下标的泛型类型导致的 例如:fromtypingimportTypeVar,Generic TTypeVar(T)classMyGenericClass(Generic[T]):def__init__(self,value:T)-None:self.valuevalue因为泛型类创建时会调用GenericMeta.__new__这会导致性能下降。除此之外常规情况下注释在变量或者方法声明的时候会进行解析和执行这个操作也是有代价的。所以pep-0563 规定了在函数和变量类型注释不需要再定义的时候进行评估了取而代之的是会在__annotations__变量中增加一个描述类型的字符串。下面示例展示常规和延迟评估的区别创建名为anno.py的文件包含一个hello()方法:# 常规类型注释defhello(name:print(Now!))-None:print(hello,name)常规导入importanno Now!anno.hello.__annotations__{name:None}anno.hello(Jack)hello Jack可以看到在导入anno时打印了Now!。 这表明类型注释内容被解析并执行使用pep-0563延迟类型注释评估from__future__importannotationsdefhello(name:print(Now!))-None:print(hello,name)测试importannoanno.hello.__annotations__{name:print(Now!)}anno.hello(Jack)hello Jack可以发现没有打印Now!, 这是因为在__annotations__里面是已字符串的形式保存了注释。如果需要获取真正的注释内容可以通过typing.get_type_hints()或者eval()importtypingtyping.get_type_hints(anno.hello)Now!{name:classNoneType}eval(anno.hello.__annotations__[name])Now!pep-0649 使用描述符对注释进行延迟评估2021/1 python3.14)在未发布的python3.14版本中计划废弃pep-0563提案。即下面这种写法在3.14以及之后的版本中将不复存在。from__future__importannotations转而采用一种更优的延迟评估方案这就是pep-0649。 先留个坑毕竟3.14还未普及。之后有机会再补上python3.14 what’s newpep-0649其它补充到这里python的类型注释就差不多接近完全体了当然后续还有一些补充规范的pep。可以参考typing topic一些使用建议避免type hints导致的循环引用type hints 一大缺陷就是会另项目导入一堆依赖的类型即使它们根本不在运行的时候使用仅仅用作类型提示。某些情况下可能会导致循环引用的问题frombimportPointdefpointer_checker(p:Point)-bool:returnisinstance(p.x,int)andisinstance(p.y,int)fromaimportpointer_checerclassPoint:def__init__(self,x:int,y:int)-None:self.xx self.yyifnotpointer_checker(self):raiseValueError(x, y should be integer!)由于a.py,b.py二者互相导入。因此会引起循环导入问题, 可以通过修改a.py解决:from__future__importannotationsfromtypingimportTYPE_CHECKINGifTYPE_CHECKING:frombimportPointdefpointer_checker(p:Point)-bool:returnisinstance(p.x,int)andisinstance(p.y,int)测试:frombimportpointer_checker,Pointpointer_checker.__annotations__{p:Point,return:bool}pPoint(1,2)pPoint(x,y)Traceback(most recent call last):Filestdin,line1,inmoduleFile/Users/walkerliu/projects/py_practise/b.py,line18,in__init__raiseValueError(x,y should be integer!)ValueError:x,y should be integer!容易被忽略的Callable适用于接受参数为函数类型或者实现了__call__方法的类常用情况有装饰器内置函数filter等defdecorator(f:Callable)-Callable:defwrapper(*args,**kwargs):returnf(*args,**kwargs)returnwrapper当然也能对回调函数加入类型提示:# syntax is Callable[[Arg1Type, Arg2Type], ReturnType]Callable[[int],int]Union 的用途Union 即联合类型语法为Union[X, Y]。意味着类型需要为X或者Ydefdouble(x:Union[int,float]):print(double x,x*2)在python3.10版本之后Union[X,Y]可以被简写为X | Y为什么要避免使用AnyAny是一种特殊类型有点类似golang中的空接口。它可以兼容任何类型。当未指定参数类型或者返回值类型是都会隐式的使用Any如果你不确定参数或者返回值类型的时候可以使用Any。除了少数特殊场景否则应该避免使用Any为什么要使用泛型泛型主要是针对那些在定义时尚不清楚具体的传入类型只有在使用时才能确定类型的情况。泛型可以通过typing.TypeVar的工厂方法来创建fromtypingimportSequence,TypeVar TTypeVar(T)deffirst(l:Sequence[T])-T:returnl[0]如上述例子l可能为类型的序列。利用泛型T表示其中T类型由所传入的序列类型决定。值得注意的是T必须全程保持一致如果传入的是[1,2,3]此时T取值为int,意味着返回值也必须为int, 若返回非整形类型则会出错。如果你用的是python3.12或者更高的版本也可以使用下面的简化版写法:fromtypingimportSequencedeffirst[T](l:Sequence[T])-T:returnl[0]None vs NoReturntyping里面提供了一个NoRetrun的类型。这个类型比较特殊只能用于返回值类型提示。例如:# okdefexit()-NoReturn:sys.exit(1)# not okdefhello(name:NoReturn)-None:print(Hello,name)需要注意的是NoReturn意味的函数没有返回值这和返回值为空是有本质区别的。因此NoReturn常用与抛出异常或者sys.exit等不需要返回值的场景。# okdefpanic()-NoReturn:raiseRuntimeError(panic!)# not ok# 所声明的返回类型为 NoReturn 的函数无法返回 Nonedefpanic2()-NoReturn:print(panic!)panic2()虽然没有显示声明return None, 其实二者效果都是一样。此时编辑器会提示错误。当然如果你直接调用的话还是能正常运行就像上面所说的类型提示并不会对程序运行产生影响编译器会将它们当成注释一般忽略。即使你使用了错误的类型提示如何在类中返回当前类型大概有4种写法使用引号 (pep-484 python3.5)classA:classmethoddefinit(self)-A:returnA()使用{python}from __future__ import annotations(pep-562 python3.6)from__future__importannotationsclassA:classmethoddefinit(self)-A:returnA()使用TypeVar(pep-484 python3.5)fromtypingimportTypeVar TypeATypeVar(TypeA,boundA)classA:classmethoddefinit(self)-TypeA:returnA()使用Self(pep-673 python3.11)fromtypingimportSelfclassA:classmethoddefinit(cls)-Self:returnA()参考资料全面理解Python中的类型提示Type Hintspython3.7 typing enhancementspep typing topic泛型pep 3107 - Function Annotationspep 484 - Type Hintspep 526 - Syntax forVariable Annotationspep 560 - Core support for typing module and generic typingpep 563 - Postponed Evaluation of Annotationspep 604 - Allow writing union types as X|Ypep 673 - Self Type