Python类型注解
在Python世界里,类型注解早就不是什么新鲜东西了。
在2006年发布的PEP3107中就有了明确的定义,而去年加入标准库的typing才为类型注解提供了最有力的帮助。当然,在这篇文章里我并不是要介绍如何使用typing去编写类型注解,只是为了讲讲类型注解的好处(算是布道),以及一点实际运用。
为什么要用类型注解
What's this? What's this? And what is the fuck this?
当我们使用别人编写或者很久之前自己编写的函数时,一般情况下会问出三个问题——它输入是什么?输出是什么?作用是什么?
或许注释和好的命名能解决这个问题。
但命名一向是计算机世界里最困难的两个问题之一,并且并不是所有人的英语都那么好,就像日本人很可能会把sort
拼成solt
,中国人可能想不出来变量名就直接上拼音(bian
这个变量到底是bi an
还是bian
?)。如果依赖于变量名去判断类型,对使用者的心智负担就太重了。
“那还有注释啊”可能会有人这么说。
诚然,为每个函数打注释是一个很好的习惯。然而随着项目的膨胀,你很难保证每个注释都是真实有效的,更多的时候,代码改了,注释没改。不是所有人都有那么充足的时间和热情去为每个参数都去写/改注释。
但类型注释能很容易的随着代码变动做相应的修改——因为人人都想要更好的代码提示。
看下面的代码
def wtf(left, right): left.norightfunctionandlonglongname() right.noleftinthis()
假设我现在要给这个函数加点功能,嗯,这他妈的left和right是什么?好,再假设我有正确的注释能看。我现在知道left和right是什么了。
但我要用的那个函数怎么拼来着?left.fuckfunctionname
?好像不对。于是我必须去看left
这个对象有哪些函数。
当我用现代编辑器的查找功能找到目标,看完之后,终于把工作完成了。但……shit,新调用的函数又是要传递什么玩意!
相信以上这种情况对于经常写Python的人来说并不少见。
那么如果有类型注解呢?
def wtf(left: Session, right: Local) -> None: left.norightfunctionandlonglongname() right.noleftinthis()
一眼看过去,好的,知道是什么类型了。接下来调用left
的一个函数,唔,好像函数名里有func
,顺着代码提示,直接调用了想要的函数。如果被调用的函数也有类型注解!不需要再点进去看,按照类型注解输入就行了。
强制类型检查
以下分别是借助类型注解进行类型检查与指定类型进行类型检查的两个装饰器。
import typing from inspect import signature from functools import wraps __DEBUG__ = True def typeassert(func: typing.Callable) -> typing.Callable: """ Force check parameter type by type hint * Parameters that use default values will not be checked * Parameters without type hint will not be checked """ # If in optimized mode, disable type checking if not __DEBUG__: return func @wraps(func) def wrapper(*args, **kwargs) -> typing.Any: # check params sig = signature(func) bound_values = sig.bind(*args, **kwargs) for name, value in bound_values.arguments.items(): parameter = sig.parameters[name] if ( parameter.annotation is parameter.empty or parameter.annotation == typing.Any ): continue if value is parameter.default: continue if not isinstance(value, parameter.annotation): raise TypeError( f"Argument {name} must be {parameter.annotation} but got {type(value)}" ) # check return result = func(*args, **kwargs) if "return" in func.__annotations__: return_type = func.__annotations__["return"] if result is return_type: pass elif not isinstance(result, return_type): raise TypeError(f"Return must be {return_type} but got {type(result)}") return result return wrapper def typeasserts(*ty_args, **ty_kwargs): """ Type checking without parameter annotation * Cannot be used to check the return value """ def decorate(func): # If in optimized mode, disable type checking if not __DEBUG__: return func # Map function argument names to supplied types sig = signature(func) bound_types = sig.bind_partial(*ty_args, **ty_kwargs).arguments @wraps(func) def wrapper(*args, **kwargs) -> typing.Any: bound_values = sig.bind(*args, **kwargs) # Enforce type assertions across supplied arguments for name, value in bound_values.arguments.items(): if name in bound_types: if not isinstance(value, bound_types[name]): raise TypeError( f"Argument {name} must be {bound_types[name]} but got {type(value)}" ) return func(*args, **kwargs) return wrapper return decorate
使用类型检查的好处在于——你在编写函数时不再需要考虑函数参数的类型错误。
一旦传入类型出现偏差,那么typeassert
就会直接抛出一个异常,函数将不会被执行。
@typeassert def mult(a: int, b: typing.Iterable[int]) -> typing.Iterable[int]: return list(map(lambda x: a*x, b)) print(mult("1", [1, 2, 3]))
如果没有类型检查,它的结果会是['a', 'aa', 'aaa']
,而当这个函数混在其他代码里使用的时候可能没办法直接找到是这里出现了错误。但现在我们加了类型检查,它会直接抛出一个错误——TypeError: Argument a must be <class 'int'> but got <class 'str'>
。立刻就能知道是调用这个函数的地方参数类型不对。
或许大型项目拥有完整的单元测试,不需要这么去检查类型,例如Instagram就有一套自己的测试系统。
但Python也被大量用于制作仅仅使用一次的脚本,这些脚本可能会执行一些用于操作系统或者服务的危险代码。为了一次性使用的脚本去做单元测试,仿佛太亏了。可是如果不去检查类型,一个不期待的对象传递进来,可能整个服务就被破坏了。这种时候,提前检查类型还是很重要的。
mypy
对于需要对整个项目或者脚本都进行类型检查的情况,手动一个个加装饰器太不现实了。可以考虑使用mypy去执行项目。
它与Python的关系,类似于TypeScript与JavaScript的关系,但又有不同。如果使用mypy去开发项目,它并不会更改你的代码,只是忠实的抛出的一些类型错误——你依旧在编写最原始的Python,运行的也依旧是你编写的每一行代码。