AberSheeran
Aber Sheeran
I know nothing except the fact of my ignorance.

Python类型注解

起笔自
所属文集: 杂记
共计 4670 个字符
落笔于

在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):
            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,运行的也依旧是你编写的每一行代码。

如果你觉得本文值得,不妨赏杯茶
Python文件的热重载
Python的元类