探究Python类型注解

创建于更新于文集:车轮滚滚

最近业余时间在尝试造一些轮子,为了记录造轮子过程中的一些问题,准备开一个新系列,既然是造轮子,那这个系列就叫车轮滚滚吧~

ORM

个人开发体验中,Python中的几个主流ORM库都有些让我不太舒服的点,最近的项目中用到了Prisma,催生了我在Python下撸一个ORM的想法。Prisma通过DSL定义数据模型,生成类型安全的客户端代码,可以借鉴一些想法在Python下试试。

类型注解

Python是一门动态强类型语言,动态类型语言的特点就是灵活,比如在Java中有一种多态形式叫函数重载,多个函数可以有相同的名字,但是参数数量和类型不同,但是在Python中,函数参数的类型是动态的,数量也可以是动态的:

def add(lhs, rhs):
    return lhs + rhs

def init(*args, **kwargs):
    pass

这种灵活的特性可以让我们的代码十分简洁,但这也是有代价的,比如a函数明明需要一个字符串参数,而我们却将一个返回值可能为空的b函数的返回值不加判断地作为a的参数,虽然在运行时会抛出错误,但是这个错误明明在运行前就可以避免的:

普通函数

添加类型注解

类型提示

虽然没有类型注解的版本给greet函数传递空值一样会报错,但是只有在运行后才会看到,而添加类型注解之后,编辑器就可以提前检测到类型问题。

并且,当我们给greet函数的参数name添加类型注解后,编辑器的自动补全体验也会更好,编辑器会帮助提示该类型所拥有的属性和方法。

dataclass

前面提了类型注解的一些优点,但其实官方明确表示过,类型注解不会做运行时推断:

例如,将greet函数作为一个类的静态方法:

class A:
    greet = staticmethod(greet)

# 不会给出提示
A.greet(12)

没有手动写注解的地方,不会有提示,比如下面的嵌套调用:

# person加注解和不加,结果不同
def hof(person):
    greet(person.name)

不过值得一提的是,类型注解的实现上并没有做到官方宣称的对运行时没有影响:

class Foo:
    @classmethod
    def create(cls, *args) -> Foo:
        ...

这段代码会带来运行时错误,解决办法见此

Python3.7带来了一个新功能,叫做Data Class,如下代码:

from dataclasses import dataclass


@dataclass
class Point:
    x: float
    y: float

这个装饰器会自动为用户定义的类生成__init____repr__等方法,自动生成的__init__方法居然带有类型注解,这种类定义像不像在定义一个ORM的model?看看Django的模型定义:

from django.db import models

class Person(models.Model):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=30)

如果能直接为这种数据类生成带类型注解的createupdate等API,岂不是很方便?可是我不论是用装饰器,还是元类,动态添加的方法的类型注解都无法被PyCharm识别,所以dataclass这东西有什么黑魔法?知道我看到了这个问题How to support dynamic type hint in self code?

PyCharm understands @dataclass decorator, but doesn't understand your custom decorator @mydataclass. You could use @dataclass for MyTest and change it to be similar to Test.

If it is required to have custom decorators this way, then the only option is to write some plugin for PyCharm.

好吧,自己写个第三方插件当然可以分析代码,实现更强的类型推断,但这也太不体面了……看来还得考虑代码生成

TypedDict

Python中的函数有着不定长度的可变参数定义:

def foo(*args, **kwargs):
    ...

分别代表不限长度的位置参数和不限长度的关键字参数:

foo(a, b, c, d, e=1, f=2, g=3)

两种参数会被当成元组和字典处理,而在类型注解中有一个TypedDict,可以定义固定类型的字典:

class Point2D(TypedDict):
    x: int
    y: int
    label: str

a: Point2D = {'x': 1, 'y': 2, 'label': 'good'}  # OK
b: Point2D = {'z': 3, 'label': 'bad'}           # Fails type check

assert Point2D(x=1, y=2, label='first') == dict(x=1, y=2, label='first')

与之类似的还有NamedTuple,那可不可以在父类中用它们来限定可变参数类型呢?按照模式匹配的思路,应该可以这样写:

def foo(*args: *Args, **kwargs: **KArgs): ...

事实证明不行,这个Issue是2018年开的,我写这篇文章的时候已经2021了……

Copyright © 2020-2021 公子政的宅日常