from __future__ import annotations
/ / / 阅读数:11266最近做了一些豆瓣的产品业务代码的 Python 3 迁移相关的准备工作。首先当然是要去改那些基础 Model,除了代码符合 Python3 语法要求,这种基础的、核心的代码也要加上类型注解,结果一上来就遇到个问题,和大家分享下。 下面是一个模拟的简单版本的例子(豆瓣厂工和前厂工会理解这种写法😋)
from typing import Union from dataclasses import dataclass import pymysql.cursors connection = pymysql.connect(host='localhost', user='root', password='', db='test', charset='utf8mb4') @dataclass class Subject: id: int cat_id: int title: str kind: int = 0 @classmethod def get(cls, id: int) -> Union[Subject, None]: with connection.cursor() as cursor: cursor.execute( "select id, cat_id, title, kind from subject where id=%s", id) rs = cursor.fetchone() if not rs: return None return cls(*rs) def set_cover(self, cover: ImageCover): ... @dataclass class ImageCover: id: int identifier: str |
我简单介绍下上面这段代码要做的事情:
- 用 dataclass 装饰器可以帮你生成__init__、__repr___和比较相关的各种魔术方法、同时添加字段验证等等,极高了提高了生产力。具体可以看我之前写的 attrs 和 Python3.7 的 dataclasses ,上例 Subject 包含了 id、title、kind、cat_id 四个字段
- Subject 包含一个 get 方法,通过 id 可以拿到对应的 Subject 实例,如果数据库找不到会返回 None,所以类型注解中用了 Union。
Python 是一种动态类型化的语言,不会强制使用类型提示,所以我们要借用外部的工具 mypy 做类型检查:
➜ pip3 install mypy --user
➜ mypy ~/workspace/movie/movie/models/base.py
mypy 的执行结果没有返回内容,说明我写的类型注解没有问题。但是运行不了
In [1]: from movie.models.base import Subject
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-1-b027a5ff7e8f> in <module>
----> 1 from a import Subject
~/workspace/movie/movie/models/base.py in <module>
12
13
---> 14 @dataclass
15 class Subject:
16 id: int
~/workspace/movie/movie/models/base.py in Subject()
20
21 @classmethod
---> 22 def get(cls, id: int) -> Union[Subject, None]:
23 with connection.cursor() as cursor:
24 cursor.execute(
NameError: name 'Subject' is not defined
即使把Union[Subject, None]
这部分去掉,下面的 ImageCover 那部分也会抛错:
29 return cls(*rs)
30
---> 31 def set_cover(self, cover: ImageCover):
32 ...
33
NameError: name 'ImageCover' is not defined
这个问题可以看 PEP 563 ,简单地说是因为类型注解部分求值太早了,那个时候类还没创建完!
怎么办呢?让类型注解「延迟求值」,使用 Python 3.7 新加入的方案:
from __future__ import annotations
from typing import Union
from dataclasses import dataclass
...
添加第一行,这样就可以了。
使用字符串替代类
第二次更新: 2018-12-30 19:55:57
文章发出后,@abc.zxy 和 @杨恺 Thomas Young 同学都提出了另外一个解决方案,就是使用对应字符串作为类型值:
@dataclass class Subject: id: int cat_id: int title: str kind: int = 0 @classmethod def get(cls, id: int) -> Union['Subject', None]: # Subject是字符串 with connection.cursor() as cursor: cursor.execute( "select id, cat_id, title, kind from subject where id=%s", id) rs = cursor.fetchone() if not rs: return None return cls(*rs) def set_cover(self, cover: 'ImageCover'): # ImageCover是字符串 ... |
可以看到不使用from __future__ import annotations
,这样的写法也是正常运行的。具体的可以看延伸阅读链接的 mypy 方案。而 PEP 484 也进行过讨论
这就引起了我的求知欲,既然 mypy 已经提供了解决方案,哪官方为什么要强烈的实现「延迟求值」这个特性呢?
看了下延伸阅读链接 2 里面「Typing Enhancements」部分做的解释,我汇总下观点:
Python 是一个动态语言,运行时不会做类型检查。本来在代码中添加类型注释是不应该响应性能的,但是很不幸,如果使用了 typing 模块就会影响到。
在之前的例子中,get 方法可能返回 None 或者一个 Subject 对象,就得用到 typing.Union 了。为什么会影响性能呢?
原来 typing 模块是标准库中最慢的模块了 !!所以在 Python3.7 大体做了 2 个角度的优化:
虽然前面用字符串替代类的方式可用。但是性能上可能会有影响,如果你已经在用 Python3.7 或者更高版本,理应选择from __future__ import annotations
这种方式。
google/pytype
三次更新: 2018-12-31 18:00:00
感谢 @laike9m 推荐的https://github.com/google/pytype,可以用它做静态检查和推断未注释 Python 代码的类型:
➜ cat foo.py def make_greeting(user_id): return 'hello, user' + user_id def print_greeting(): print(make_greeting(0)) ➜ pytype-single foo.py File "foo.py", line 2, in make_greeting: unsupported operand type(s) for +: 'str' and 'int' [unsupported-operands] Function __add__ on str expects str Called from (traceback): line 5, in print_greeting For more details, see https://github.com/google/pytype/blob/master/docs/errors.md#unsupported-operands. |
除了 mypy 也可以考虑下 Google 的 pytype ,毕竟是在生产环境下大厂成熟应用案例。
✍🏻✍🏻