最近做了一些豆瓣的产品业务代码的 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

我简单介绍下上面这段代码要做的事情:

  1. 用 dataclass 装饰器可以帮你生成__init__、__repr___和比较相关的各种魔术方法、同时添加字段验证等等,极高了提高了生产力。具体可以看我之前写的 attrs 和 Python3.7 的 dataclasses ,上例 Subject 包含了 id、title、kind、cat_id 四个字段
  2. 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 个角度的优化:

  1. 通过实现 PEP 560 让 typing 模块大大提速
  2. 通过实现 PEP 563 让类型注解延迟求值,因为类型提示没有执行

虽然前面用字符串替代类的方式可用。但是性能上可能会有影响,如果你已经在用 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 ,毕竟是在生产环境下大厂成熟应用案例。

延伸阅读

  1. https://mypy.readthedocs.io/en/latest/cheat_sheet_py3.html#miscellaneous
  2. https://realpython.com/python37-new-features/
  3. https://www.python.org/dev/peps/pep-0563/