Python3的“函数注释”有哪些好用途?

172

函数注解:PEP-3107

我看到了一段展示Python3函数注释的代码片段。这个概念很简单,但我不知道为什么在Python3中会实现它们,也不知道有什么好的用途。也许SO能给我启示?

它是如何工作的:

def foo(a: 'x', b: 5 + 6, c: list) -> max(2, 9):
    ... function body ...

冒号后跟随参数的内容被称为“注释”,而在->后面的信息是函数返回值的注释。

foo.func_annotations会返回一个字典:

{'a': 'x',
 'b': 11,
 'c': list,
 'return': 9}

有这个可用性的重要性是什么?


15
http://www.python.org/dev/peps/pep-3107/#use-cases - SilentGhost
6
@SilentGhost:不幸的是,许多带有实际用例的链接已经失效。这些内容是否存放在其他地方,或者它们已经永远消失了? - max
17
在Python3中,应该使用foo.__annotations__而不是foo.func_annotations,这是一种更现代和推荐的注释语法。 - zhangxaochen
3
注解没有特别的意义。Python做的唯一事情就是将它们放在__annotations__字典中。任何其他操作都取决于你。 - N Randhawa
1
def foo(a: 'x', b: 5 + 6, c: list) -> max(2, 9): 这段代码的意思是什么? - Ali SH
12个回答

102

函数注释的意义在于你自己定义。

它们可以用于文档编写:

def kinetic_energy(mass: 'in kilograms', velocity: 'in meters per second'):
     ...

它们可以用于进行前置条件检查:

def validate(func, locals):
    for var, test in func.__annotations__.items():
        value = locals[var]
        msg = 'Var: {0}\tValue: {1}\tTest: {2.__name__}'.format(var, value, test)
        assert test(value), msg


def is_int(x):
    return isinstance(x, int)

def between(lo, hi):
    def _between(x):
            return lo <= x <= hi
    return _between

def f(x: between(3, 10), y: is_int):
    validate(f, locals())
    print(x, y)


>>> f(0, 31.1)
Traceback (most recent call last):
   ... 
AssertionError: Var: y  Value: 31.1 Test: is_int

另外请参阅http://www.python.org/dev/peps/pep-0362/,了解一种实现类型检查的方法。


23
这种方法相比于使用函数说明文档(docstring)或在函数中进行显式类型检查,有什么优势?这似乎只是让语言变得更加复杂而已。 - endolith
11
我们可以不使用函数注解。它们只是提供了一种标准的方式来访问注解。这使它们能够被help()和工具提示访问,并可用于内省。 - Raymond Hettinger
4
与其传递数字,你可以创建类型MassVelocity - user1804599
1
为了充分展示这一点,我会写成 def kinetic_energy(mass: '以千克为单位', velocity: '以米/秒为单位') -> float:,同时也展示返回类型。这是我在这里的最爱答案。 - Tommy
1
@user189728 您是正确的。要么将返回值保存到变量中,要么将整个函数包装在一个验证装饰器中。 - Raymond Hettinger
显示剩余2条评论

95

我认为这实际上是非常好的。

作为一个学术背景的人,我可以告诉您,注解已经证明了它们在使像Java这样的语言的智能静态分析器变得不可或缺。例如,您可以定义像状态限制、允许访问的线程、体系结构限制等语义,然后有相当多的工具可以读取这些并处理它们,提供比编译器更高的保证。甚至可以编写检查前置条件/后置条件的内容。

我觉得这样的东西在Python中特别需要,因为它的类型较弱,但实际上没有任何构造使其简单明了地成为官方语法的一部分。

注解除了保证外还有其他用途。我可以看到如何将我的基于Java的工具应用于Python。例如,我有一个工具,可以为方法指定特殊警告,并在调用它们时提示您应该阅读其文档(例如,想象一下您有一个不能用负值调用的方法,但从名称上不直观)。通过注释,我可以在Python中技术上编写类似的东西。同样,如果有官方语法,可以编写一个根据标记组织大类中方法的工具。


35
我认为这些都是理论上的好处,只有在标准库和第三方模块都使用函数注释,并且使用一套经过深思熟虑的、具有一致性含义的注释系统时才能实现。在那一天(永远不会到来)之前,Python 函数注释的主要用途将是其他答案中描述的一次性用途。目前,您可以忘记智能静态分析器、编译保证、基于 Java 的工具链等。 - Raymond Hettinger
6
即使并非所有内容都使用函数注释,您仍然可以在其输入上具有注释的代码中使用它们进行静态分析,并调用其他同样注释的代码。在较大的项目或代码库中,这仍然可以是一个显着有用的代码块,可用于执行基于注释的静态分析。 - gps
9
快进到2015年,https://www.python.org/dev/peps/pep-0484/和http://mypy-lang.org/开始证明了所有的怀疑论者是错误的。 - Mauricio Scheffer
1
它还更加展现了Python对Swift的影响。 - uchuugaka
2
@DustinWyatt 很高兴我对那个预测错了 :-) 我们从 PEP 484 中获得了标准化类型,并且有了一个大部分注释的标准库和 typeshed。然而,OP 对于类似 Java 的工具的期望大多还没有实现。 - Raymond Hettinger
显示剩余2条评论

49

这是一份晚来的回答,但据我所知,目前最好的使用函数注释的方法是PEP-0484MyPy。此外,微软的PyRight也可用于VSCode并通过CLI提供。

MyPy是Python的可选静态类型检查器。您可以使用在Python 3.5 beta 1中引入的类型注释标准(PEP 484)添加类型提示到Python程序中,并使用mypy对它们进行静态类型检查。

如下所示使用:

from typing import Iterator

def fib(n: int) -> Iterator[int]:
    a, b = 0, 1
    while a < n:
        yield a
        a, b = b, a + b

2
更多示例请参见Mypy ExamplesHow You Can Benefit from Type Hints - El Ruso
另外还可以查看 pytype - 这是另一个专为 PEP-0484 设计的静态分析器。 - gps
不幸的是,类型没有被强制执行。如果我使用您的示例函数键入list(fib('a')),Python 3.7会愉快地接受参数,并抱怨没有办法比较字符串和整数。 - Denis de Bernardy
@DenisdeBernardy 根据 PEP-484 的解释,Python 只提供类型注释。要强制执行类型,您必须使用 mypy。 - Dustin Wyatt

25

仅为了从我的答案中添加一个好的使用示例,加上装饰器可以实现一种简单的多方法机制。

# This is in the 'mm' module

registry = {}
import inspect

class MultiMethod(object):
    def __init__(self, name):
        self.name = name
        self.typemap = {}
    def __call__(self, *args):
        types = tuple(arg.__class__ for arg in args) # a generator expression!
        function = self.typemap.get(types)
        if function is None:
            raise TypeError("no match")
        return function(*args)
    def register(self, types, function):
        if types in self.typemap:
            raise TypeError("duplicate registration")
        self.typemap[types] = function

def multimethod(function):
    name = function.__name__
    mm = registry.get(name)
    if mm is None:
        mm = registry[name] = MultiMethod(name)
    spec = inspect.getfullargspec(function)
    types = tuple(spec.annotations[x] for x in spec.args)
    mm.register(types, function)
    return mm

并提供一个使用示例:

from mm import multimethod

@multimethod
def foo(a: int):
    return "an int"

@multimethod
def foo(a: int, b: str):
    return "an int and a string"

if __name__ == '__main__':
    print("foo(1,'a') = {}".format(foo(1,'a')))
    print("foo(7) = {}".format(foo(7)))

可以通过向装饰器添加类型来实现此操作,就像Guido的原始发布中所示,但注释参数本身更好,因为它避免了错误匹配参数和类型的可能性。

注意:在Python中,您可以访问注释作为function.__annotations__而不是function.func_annotations,因为func_*样式已在Python 3中删除。


2
有趣的应用,但我担心当涉及到子类时,function = self.typemap.get(types) 将无法工作。在这种情况下,您可能需要使用 isinstance 循环遍历 typemap。我想知道 @overload 是否正确处理了这个问题。 - Tobias Kienzler
如果函数有返回类型,我认为这可能出现了问题。 - zenna
1
__annotations__ 是一个 dict,它不能保证参数的顺序,因此这段代码有时会失败。我建议将 types = tuple(...) 更改为 spec = inspect.getfullargspec(function),然后 types = tuple([spec.annotations[x] for x in spec.args]) - xoolive
你说得很正确,@xoolive。为什么不编辑答案并添加你的修复呢? - Muhammad Alkarouri
@xoolive: 我注意到了。有时编辑人员在管理编辑方面会过于严格。我已经编辑了问题,包括你的修复内容。 实际上,我已经就此进行了讨论,但是没有办法取消修复请求的拒绝。顺便感谢你的帮助。 - Muhammad Alkarouri

24

Uri已经给出了正确的答案,那么这里是一个不太严肃的回答:这样你可以让你的文档字符串更短。


2
喜欢它。+1。然而,最终,编写文档字符串仍然是我使代码可读的首要方式,但是,如果您要实现任何类型的静态或动态检查,拥有这个是很好的。也许我会找到它的用途。 - Warren P
8
我不建议将注释作为Args:部分或@param行或类似项在文档字符串中的替代品(无论您选择使用什么格式)。虽然文档注释可以提供漂亮的示例,但它会玷污注释的潜在威力,因为它可能会妨碍其他更强大的用途。此外,与文档字符串和assert语句可以在运行时省略注释以减少内存消耗(python -OO)不同,您无法省略注释。 - gps
2
@gps:就像我说的那样,这只是一个不太严肃的回答。 - JAB
3
认真地说,这是一种更好的记录你期望的类型的方式,同时仍然遵循鸭子类型。 - Marc
1
@gps 我不确定在99.999%的情况下docstrings的内存消耗是否值得担忧。 - Tommy
不是这样的。我有一个0.001%的情况,哈哈。 - gps

14

我第一次看到注解时,想到“太好了!终于可以选择一些类型检查!”当然,我没有注意到注解实际上并没有被强制执行。

因此,我决定编写一个简单的函数装饰器来强制执行它们

def ensure_annotations(f):
    from functools import wraps
    from inspect import getcallargs
    @wraps(f)
    def wrapper(*args, **kwargs):
        for arg, val in getcallargs(f, *args, **kwargs).items():
            if arg in f.__annotations__:
                templ = f.__annotations__[arg]
                msg = "Argument {arg} to {f} does not match annotation type {t}"
                Check(val).is_a(templ).or_raise(EnsureError, msg.format(arg=arg, f=f, t=templ))
        return_val = f(*args, **kwargs)
        if 'return' in f.__annotations__:
            templ = f.__annotations__['return']
            msg = "Return value of {f} does not match annotation type {t}"
            Check(return_val).is_a(templ).or_raise(EnsureError, msg.format(f=f, t=templ))
        return return_val
    return wrapper

@ensure_annotations
def f(x: int, y: float) -> float:
    return x+y

print(f(1, y=2.2))

>>> 3.2

print(f(1, y=2))

>>> ensure.EnsureError: Argument y to <function f at 0x109b7c710> does not match annotation type <class 'float'>

我把它添加到Ensure库中。

我和许多人一样,当得知 Python 终于加入类型检查时感到非常兴奋,但事实证明这只是一个令人失望的谣言。我们不得不继续使用自己编写的类型检查实现。 - Hibou57

3

Python 3.X(仅限)还将函数定义泛化,允许使用对象值注释参数和返回值用于扩展

它的元数据解释了函数值,使其更加明确。

注释在参数名称后面并在默认值之前编码为:value,在参数列表后面编码为->value

它们被收集到函数的__annotations__属性中,但在Python本身中不被视为特殊内容:

>>> def f(a:99, b:'spam'=None) -> float:
... print(a, b)
...
>>> f(88)
88 None
>>> f.__annotations__
{'a': 99, 'b': 'spam', 'return': <class 'float'>}

来源:Python口袋参考,第五版

例子:

typeannotations 模块提供了一组用于类型检查和类型推断的工具。它还提供了一组有用于注释函数和对象的类型。

这些工具主要设计用于静态分析器,例如 linters、代码自动补全库和 IDE。此外,还提供了用于运行时检查的装饰器。在 Python 中,并不总是适合进行运行时类型检查,但在某些情况下它非常有用。

https://github.com/ceronman/typeannotations

类型提示如何帮助编写更好的代码

使用类型提示可以帮助您进行静态代码分析以捕获类型错误,在将代码发送到生产环境之前避免一些明显的错误。有像 mypy 这样的工具,您可以将其作为软件生命周期的一部分添加到工具箱中。mypy 可以通过部分或全部地针对您的代码库运行来检查正确的类型。mypy 还可以帮助您检测诸如从函数返回值时检查 None 类型等错误。类型提示有助于使您的代码更清晰。您可以使用类型而无需付出任何性能成本,而不是使用注释来记录您的代码。

Clean Python: Elegant Coding in Python ISBN: ISBN-13 (pbk): 978-1-4842-4877-5

PEP 526 -- 变量注释的语法

https://www.python.org/dev/peps/pep-0526/

https://www.attrs.org/en/stable/types.html


@BlackJack,“用于扩展”的意思不清楚吗? - The Demz
这很明显,但在我看来并没有回答问题。这就像用“用于程序”回答“类的好用处是什么?”一样。它很清楚,正确,但是提问者对于到底有哪些好的具体用途并不更加明智。你的答案不能再更加通用了,而且举的例子与问题中已经给出的例子本质上是相同的。 - BlackJack

3
很久以前就有人提出过这个问题,但是问题中给出的示例片段(正如在那里所述)来自PEP 3107,在该PEP示例的末尾还提供了使用案例,这可能从PEP的角度回答了问题 ;)
以下摘自PEP3107
用例
在讨论注释的过程中,提出了许多用例。其中一些在此处呈现,按其传达的信息类型分组。还包括可以利用注释的现有产品和软件包的示例。
提供键入信息
- 类型检查 ([3],[4]) - 让IDE显示函数期望和返回的类型([17]) - 函数重载/通用函数 ([22]) - 外语桥梁 ([18],[19]) - 适应性 ([21],[20]) - 谓词逻辑函数 - 数据库查询映射 - RPC参数编组([23])
其他信息
- 参数和返回值的文档([24])

查看PEP获取有关特定点的更多信息(以及它们的参考资料)


如果投票者能留下至少一个简短的评论说明导致投票反对的原因,我将不胜感激。这将有助于(至少是我)大大改进。 - klaas

1
作为稍有延迟的答案,我的几个软件包(marrow.script、WebCore等)在可能的情况下使用注释来声明类型转换(即从网络传入值进行转换、检测哪些参数是布尔开关等),以及执行其他参数的附加标记。
Marrow Script可为任意函数和类构建完整的命令行界面,并允许通过注释定义文档、强制类型转换和基于回调的默认值,在旧运行时中使用装饰器。我所有使用注释的库都支持以下形式:
any_string  # documentation
any_callable  # typecast / callback, not called if defaulting
(any_callable, any_string)  # combination
AnnotationClass()  # package-specific rich annotation object
[AnnotationClass(), AnnotationClass(), …]  # cooperative annotation

"Bare"支持文档字符串或类型转换函数,使其更容易与其他注释感知库混合使用(例如,使用类型转换的Web控制器同时也作为命令行脚本公开)。编辑后添加:我还开始使用TypeGuard包,使用开发时断言进行验证。好处是:在启用“优化”(-O / PYTHONOPTIMIZE环境变量)运行时,可能昂贵(例如递归)的检查被省略,因为你已经在开发中正确测试了应用程序,所以在生产中不需要进行检查。

1
尽管此处描述了所有用途,但注释的一种可执行且最有可能被执行的用途将是类型提示。目前尚未以任何方式执行此操作,但根据PEP 484的判断,Python的未来版本将仅允许类型作为注释的值。
引用注释的现有用途是什么?
我们希望类型提示最终将成为注释的唯一用途,但这需要额外的讨论和在Python 3.5中使用typing模块后的弃用期。当前PEP将具有临时状态(请参见PEP 411),直到发布Python 3.6为止。最快的方案将在3.6中引入非类型提示注释的静默弃用,在3.7中进行完全弃用,并在Python 3.8中声明类型提示是注释的唯一允许用途。

虽然我还没有在3.6中看到任何静默降级,但这很可能会被推迟到3.7。

因此,即使可能存在其他好的用例,如果您不想在未来需要遵守此限制,最好将它们仅用于类型提示。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接