为什么要 “静态”? 在一切开始前,我们可以先看一个小例子:
1 2 3 4 5 6 7 8 9 10 11 def get_weapons_status (): weapon_status = { "LeviathanAxe" : True , "BladesofChaos" : False } return weapon_status, weapons_status = get_weapons_status() for weapon, status in weapons_status.items(): if status: print (f"{weapon} is ready" )
在我们的预想里,上述代码应该会输出 LeviathanAxe is ready
,但实际运行则会报错:AttributeError: 'tuple' object has no attribute 'items'
眼尖的同学肯定发现了,这段代码的问题是在 get_weapons_status
的return
语句后多加了一个 ,
,所以函数的返回值并不是预期的 dict
类型,而是 tuple
。然而对于这样的写法,IDE 可能不会有明显提示,所以在我们做大段的 代码重构或迁移时,它们很容易被忽略,直到运行时才会冒出来。
我们常把这类问题归结于自己的粗心大意,但是没有想过它们的“原罪”其实应该是 Python 的动态 。
诚然,Python 的动态给我们带来了诸多酷炫的特性:monkey_patch
、各种魔法方法、极为方便的 mock 测试…..但在逻辑分层设计、参数校验、代码补全时我们又无比渴望一些 “静态” 特性。
所以,如果 Python 能够 “静态” 一些,将会给我们带来几个明显的增益:
大幅度提升代码的可读性
能够将参数传递时的 类型错误
扼杀在摇(biān)篮(mǎ)中
能够最大程度利用 IDE 提供的代码提示,减少 typo
那么该如何拥有这些“静态”的特性呢?下面我们将时间留给本文的主角——typing annotaion
。
Typing Annotation 从 Python 3.5 开始, type annotation
已经正式被引入了。
我们可以对任何变量进行类型注解,无论是赋值之前 还是函数传参和返回
1 2 3 4 5 6 7 nine_realms: List [str ] = ["Niflheim" , "Muspelheim" , "Asgard" , "Midgard" , "Jotunheim" , "Vanaheim" , "Alfheim" , "Svartalfheim" , "Helheim" ] class Yggdrasill : def transport (realm: str ) -> bool : pass
类型注释? 除了“注解”,我们也可以使用类型注释
相较于“注解”,注释对代码的侵入性更小,但同时可读性更差,只适用于不支持注解的场景。官方推荐使用类型注解,所以类型注释的内容就不再展开了。
Gradual typing 类型注解只会生存于“编码时”,并不影响运行时,我们可以放心大胆地为旧代码添加注解,而不用担心对实际功能产生影响。当然对于大型项目(只要注解本身不写错),我们可以采取“渐进式注解”,对一些关键的核心模块先进行改造,详见 gradual typing
Types 和 Classes 需要额外说明的是,在类型注解中,我们经常会使用 “类(class)” 和 “类型(type)” 做注解:
1 2 3 4 name: str = "Atreus" from typing import List names: List [str ] = ["Atreus" , "Loki" ]
其中 str
既是一种 “类”,同时也是一种 “类型”,而 List
只是一种 “类型”,我们是不能够在 “运行时” 使用它们的,例如:
1 class Names (List [str ]): ...
简单来说,任何一种 “类” 都可以被当作一种 ”类型“,反过来 ”类型” 却不一定能被当作 “类” 使用。
各种使用场景 下面我们主要将常用的类型注解结合工程场景举例
基本类型 1 2 def hey_boy (name: str ) -> str :return 'Boy: ' + name
正如上面提到,任何基本类型都可以直接当做注解使用,包括但不限于:int
、str
、list
、dict
、set
、None
、bool
、tuple
等等,
容器类型 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from typing import List def print_names (names: List ) -> None : pass def print_names (names: List [str ] ) -> None : pass print_names(['Kratos' , 'Thor' ]) print_names(['any_type' , 128 , {1 , 2 , 8 }, (1 , 2 , 8 )]) def print_names (names: Dict [str , str ] ) -> None : pass print_names({'Blues' : 'Yu' })
类型别名 容器内元素会非常复杂,我们可能会有类似 Dict[str, Dict[str: SomeObject]]
这种不容易阅读的写法,此时我们也可以用类型别名
来简化。
1 2 3 4 5 6 7 from typing import Dict Point = Dict [str : SomeObject] def print_points (points: Dict [Point] ): for point in points: print ("X:" , point[0 ], " Y:" , point[1 ])
函数类型 在类似装饰器、回调等场景下,我们需要对函数参数作注解,基本的格式为:Callable[[Arg1Type, Arg2Type], ReturnType]
。其中 [Arg1Type, Arg2Type]
是输入参数列表,ReturnType
是返回内容。
1 2 3 4 5 6 7 8 9 from typing import Callable def foo (func: Callable [[int ], str ], argument: str ) -> None : print (func(argument)) def bar (name: int ) -> str : return f"Hello {name} " foo(create_greeting, "Jekyll" )
“泛” 型 Any 有时候我们对于某些“泛”型变量,我们可以用 Any
来表明
1 2 3 4 5 6 from typing import TypeVar, AnyStr, Any def concat (a: Any , b: Any ) -> Any : pass
TypeVar TypeVar
表示 Type
类型。简单来说,她就是“类型”的“类型”。和 type
可以创造 “类” 一样,我们可以通过 TypeVar
来创造 “类型”。
例如 typing
中提供的 AnyStr
其实就是通过 TypeVar('AnyStr', Text, bytes)
来表示的。
1 2 3 4 5 6 7 8 9 10 11 def concat (a: AnyStr, b: AnyStr ) -> AnyStr: return a + b concat(u"foo" , u"bar" ) concat(b"foo" , b"bar" ) concat(u"foo" , b"bar" )
函数的“泛”型 有时候,函数可能需要支持多种类型输入输出,我们可以通过多种方法来实现注解。
使用 typing.overload
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from typing import overload, List @overload def switch_names (developers: str ) -> List :... @overload def switch_names (developers: List [str ] ) -> str :... def switch_names (developers ): if isinstance (developers, str ): developers = developers.split("," ) return developers elif isinstance (developers, List ): developers = ',' .join(developers) return developers
使用 Union[Type1, Type2]
1 2 3 4 5 6 7 8 9 10 def switch_names (name: Union [str , List ] ): if isinstance (developers, str ): developers = developers.split("," ) return developers elif isinstance (developers, List ): developers = ',' .join(developers) return developers
Union 和 Optional 1 2 3 4 5 6 7 8 9 10 11 12 13 14 from typing import Union def print_grade (grade: Union [int , str ] ): if isinstance (grade, str ): print (grade + ' percent' ) else : print (str (grade) + ' percent' ) from typing import Optional def foo (optional_info: Optional [Dict ] = None ): return optional_info or {}
Django model 1 2 3 4 5 6 7 8 9 from typing import Union , List from django.db.models import QuerySetfrom my_app.models import MyModeldef foo (records: Union [QuerySet, List [MyModel]] ): pass
Protocol 协议类型是一个特殊的结构化类型,它表示一种静态的鸭子类型,有点像是 Go 中的接口定义,但依旧只存在于“编码时”。Protocol
的好处是,我们可以很明确的定义和使用鸭子类型,而不用人工核对方法的输入输出。
举个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 from typing import Union , Optional , Protocolclass Exitable (Protocol ): def exit (self ) -> int : ... class Quittable (Protocol ): def quit (self ) -> Optional [int ]: ... def finish (task: Union [Exitable, Quittable] ) -> int : ...class QuitJob : def quit (self ) -> int : return 0 class ExitJob : def exit (self ) -> int : return 0 class Server : def run_forever (self ): -> NoReturn: """no return""" finish(QuitJob()) finish(ExitJob()) finish(Server())
在这个例子中,我们定义了两种协议,Exitable
和 Quittable
,同时在传参时支持它们俩的并集,那么只要实现了 exit(self) -> int
或 quit(self) -> int
的对象都会被接受。
关于 Protocol
的其他定义和示例,可以通过 PEP 544 了解更多,这里就不再展开了。
自定义类型 普通用法 可以使用 Type
来接受子类
1 2 3 4 5 6 7 8 9 10 from typing import Type class User : ...class BasicUser (User ): ...class ProUser (User ): ...class TeamUser (User ): ...def make_new_user (user_class: Type [User] ) -> User: return user_class()
类型自定义时 不使用 TypeVar
1 2 3 4 class Animal :@classmethod def newborn (cls, name: str ) -> 'Animal' : return cls(name, date.today())
正如上面提到,我们还可以用 TypeVar
来创造类型,
1 2 3 4 5 6 TAnimal = TypeVar("TAnimal" ) class Animal (Generic ("TAnimal" )): @classmethod def newborn (cls, name: str ) -> TAnimal: return cls(name, date.today())
倘若此时还有继承,需要添加参数 bound
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from datetime import datefrom typing import Type , TypeVarTAnimal = TypeVar("TAnimal" , bound="Animal" ) class Animal : @classmethod def newborn (cls: Type [TAnimal], name: str ) -> TAnimal: return cls(name, date.today()) class Dog (Animal ): def bark (self ) -> None : print (f"{self.name} says woof!" ) fido = Dog.newborn("Fido" ) fido.bark()
辅助函数 有时候我们并不想在简单类型上多费精力,可以直接使用辅助函数来快捷创建类型。
1 2 3 4 5 6 7 8 from typing import NewTypeUserId = NewType('UserId' , int ) some_id = UserId(524313 ) ProUserId = NewType('ProUserId' , UserId) some_pro_id = ProUserId(12345 )
typing-extensions 由于 typing
模块已经被引入标准库,其发布频率需要和 Python 发行版保持一致,所以有很多新类型被放到了 typing-extensions
中,我们可以通过 pip
安装它。
1 pip install typing-extensions
typing-extensions
中有很多特殊类型,下面我们简单举一个小例子,其他的大家可以自行探索。
实例:简化的枚举类型 1 2 3 4 from typing_extensions import Literal def fetch_data (raw: Literal ["red" , "blue" , "yellow" ] ) -> bytes : ...
原则上这里就只能输入字符串 “red” “blue” “yellow”,某些简单的场景下,我们不用额外定义枚举类型。
工程技巧 避免循环引用 A 模块:
1 2 3 4 5 6 7 8 from B import AppFooControllerclass App : def __init__ (name: str ): self .name = name def create_app (): controller = AppFooController(App('bar' ))
在这个场景下,B 模块并不是真正的需要引用 A 模块的内容,而是需要获得 A 模块内容的 typing hint
,如果直接引用会导致循环 import,可以通过如下方法规避:
1 2 3 4 5 6 7 8 from typing import TYPE_CHECKINGif TYPE_CHECKING: from A import App class AppFooController : app: 'App'
这里的 TYPING_CHECKING
在运行时将永远为 False
,所以并不会真正 import A 模块,同时也能起到 hint
的作用。
mypy mypy 原来是一个兼容大部分 Python 语法的静态类型的 Python 发行版,后来在官方受到启发,并加入类型注解之后,mypy 已经演化成了一个静态类型检查器,我们可以通过 pip 来安装
在某些场景下,我们可以通过 mypy 来对项目进行全局扫描
1 2 3 4 5 6 7 8 9 10 11 12 13 ➜ mypy some-python-project-path/ foo/utils/sanitizer.py:18: error: Cannot find module named 'bleach.encoding' foo/utils/sanitizer.py:19: error: Cannot find module named 'bleach.sanitizer' foo/fake/tee/gitlab/client.py:5: error: Cannot find module named 'bar' foo/fake/tee/gitlab/client.py:6: error: Cannot find module named 'bar' foo/fake/tee/gitlab/client.py:7: error: Cannot find module named 'bar.objects' foo/accessories/fake/tags.py:48: error: "type" has no attribute "tag_type" foo/utils/log.py:4: error: Cannot find module named 'logstash' foo/utils/log.py:5: error: No library stub file for module 'redis' foo/utils/detector.py:32: error: Need type annotation for 'detect_map' foo/utils/basic.py:3: error: Cannot find module named 'past.utils' foo/utils/basic.py:9: error: Cannot find module named 'bar' foo/utils/basic.py:10: error: Cannot find module named 'aenum'
在我们项目整体类型注解完善的情况下,可以考虑将 mypy 放到 CI 的流程中。
比如 Tornado 的源码基本已经做到 100% 静态了,扫描整个工程只有两个小问题:
1 2 3 ➜ mypy tornado/ tornado/testing.py:263: error: Return type of "run" incompatible with supertype "TestCase" tornado/testing.py:270: error: Incompatible return value type (got "Optional[TestResult]" , expected "TestCase" )
再让我们看看那个例子 以下是在 Pycharm 里提示的样子:
好了,只要加上了类型注解,那个 企图让所有变量都变成元组 的”元凶”——逗号,就无处遁形了 xD
结语 类型注解给 Python 带来了全新的开发体验,相信此刻的你也有了答案,让 Python “静态” 起来吧。
参考文档: