Python3的“函数注释”有什么好的用处


159

功能注释:PEP-3107

我碰到了一段代码,展示了Python3的功能注释。这个概念很简单,但是我想不起来为什么要用Python3来实现它们或对其有很好的用途。也许可以启发我吗?

这个怎么运作:

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}

拥有此功能有什么意义?



6
@SilentGhost:不幸的是,许多与实际用例有关的链接都已断开。有什么地方可以存储内容,或者内容永远消失了?
最大

16
foo.func_annotations 应该foo.__annotations__在python3中吗?
zhangxaochen 2014年

2
注释没有特殊意义。Python唯一要做的就是将它们放入注释字典中。其他任何动作都取决于您。
N兰达瓦

什么def foo(a: 'x', b: 5 + 6, c: list) -> max(2, 9):意思
阿里SH

Answers:


90

我认为这实际上很棒。

来自学术背景,我可以告诉您,注释已证明对启用像Java这样的语言的智能静态分析器非常有用。例如,您可以定义语义,例如状态限制,允许访问的线程,体系结构限制等,然后有很多工具可以读取这些内容并进行处理,以提供超出编译器的保证。您甚至可以编写检查前提条件/后置条件的东西。

我觉得这样的事情在Python中特别需要,因为它的输入较弱,但是实际上没有任何结构可以使它简单明了,并且成为正式语法的一部分。

注解还有其他用途,无法保证。我可以看到如何将基于Java的工具应用于Python。例如,我有一个工具,可让您为方法分配特殊警告,并在调用它们时向您提供指示,指示您应阅读其文档(例如,假设您有一个不能用负值调用的方法,但是从名称上不直观)。通过注释,我可以为Python技术性地编写类似的内容。同样,如果存在正式语法,则可以编写基于标签将大型方法组织起来的工具。


34
ISTM这些是理论上的好处,只有在标准库和第三方模块都使用功能注释并以一致的含义使用它们并使用经过深思熟虑的注释系统时,才能实现。在那一天之前(永远不会到来),Python函数注释的主要用法将是其他答案中所述的一次性用法。暂时,你可以忘掉智能静态分析,编译器保证,基于Java的工具链等
雷蒙德赫廷杰

4
即使没有使用函数注释的所有内容,您仍然可以在代码中使用它们进行静态分析,这些代码在其输入上并调用其他具有类似注释的代码。在较大的项目或代码库中,这仍然可能是用于执行基于注释的静态分析的非常有用的代码体。
gps

1
在AFAICT中,您可以使用装饰器来完成所有这些工作,装饰器要早于注释。因此,我仍然看不到好处。我对这个问题的一个稍微不同的看法:stackoverflow.com/questions/13784713/...
allyourcode

9
快进到2015年,python.org / dev / peps / pep - 0484mypy-lang.org开始证明所有反对者都是错误的。
Mauricio Scheffer

1
它还进一步揭示了Python对Swift的影响。
uchuugaka 2015年

92

函数批注就是您对它们所做的。

它们可以用于文档:

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/了解实现类型检查的方法。


18
这比文档字符串或函数中的显式类型检查好吗?这似乎毫无理由地使语言复杂化。
endolith 2013年

10
@endolith我们当然可以不带功能注释。它们只是提供访问注释的标准方法。这使它们可以访问help()和工具提示,并可以进行内省。
Raymond Hettinger

4
而不是围绕数字路过你可以创建类型MassVelocity代替。
2014年

1
为了充分说明这一点,我还必须def kinetic_energy(mass: 'in kilograms', velocity: 'in meters per second') -> float:显示返回类型。这是我最喜欢的答案。
汤米

使用您的代码,有没有办法验证return注释?它似乎没有出现在locals
user189728 '18

46

这是一个较晚的答案,但是AFAICT(当前对功能注释的最佳使用)是PEP-0484MyPy

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


另请参阅pytype-考虑到PEP-0484构建的另一个静态分析器。
gps

不幸的是该类型没有被强制执行。如果我list(fib('a'))用您的示例函数键入内容,Python 3.7会愉快地接受该参数,并抱怨无法比较字符串和整数。
Denis de Bernardy

@DenisdeBernardy PPE-484解释说,Python仅提供类型注释。要强制执行类型,您必须使用mypy。
达斯汀·怀亚特

23

我想补充从我的回答很好地利用的一个具体的例子在这里,加上装饰可以做的多方法的简单机制。

# 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)如果涉及到子类,恐怕将无法使用。在这种情况下,您可能必须遍历typemap使用isinnstance。我想知道@overload处理是否正确
Tobias Kienzler 2013年

我认为如果函数具有返回类型
无效

1
__annotations__ 是一个dict不保证参数顺序,因此这个片段有时会失败。我建议将更types = tuple(...)改为spec = inspect.getfullargspec(function)then types = tuple([spec.annotations[x] for x in spec.args])
xoolive

您说得很对,@ xoolive。您为什么不编辑答案以添加您的修订?
穆罕默德·阿尔卡鲁里

@xoolive:我注意到了。有时,编辑人员会费力地管理编辑。我已对问题进行了编辑,以包括您的修复程序。实际上,我已经对此进行了讨论,但是没有办法不拒绝该修复程序。谢谢您的帮助。
穆罕默德·阿尔卡鲁里

22

Uri已经给出了正确的答案,所以下面是一个不太严重的答案:这样您可以缩短文档字符串。


2
爱它。+1。但是,最后,编写文档字符串仍然是我使代码可读的一种方式,但是,如果您要实现任何类型的静态或动态检查,则很高兴这样做。也许我会发现它的用处。
沃伦·P

8
我不建议您在文档字符串(无论选择使用哪种格式)中使用注释来替代Args:节或@param行或类似内容。尽管文档注释为一个很好的例子,但它会破坏注释的潜在功能,因为它可能会阻碍其他更强大的用法。同样,您不能像运行docstrings和assert语句那样在运行时省略注释以减少内存消耗(python -OO)。
gps 2012年

2
@gps:就像我说的那样,这是一个不太严重的答案。
2012年

2
认真地说,这是记录所需类型的更好的方法,同时仍然遵守DuckTyping。
Marc

1
@gps我不确定在99.999%的情况下文档字符串的内存消耗是否值得担心。
汤米

13

第一次看到注释时,我以为“很棒!最后我可以选择进行类型检查!” 当然,我没有注意到注解实际上并没有执行。

因此,我决定编写一个简单的函数装饰器来实施它们

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'>

我已将其添加到“ 确保”库中。


在我退出Python终于有了类型检查之后,我也感到同样的失望。最终将不得不使用自制的类型检查实现。
Hibou57

3

自问起以来已经有很长时间了,但问题中给出的示例摘录(也如此处所述)来自PEP 3107,并且在thas PEP示例结尾处也给出了用例,它们可能会从PEP角度回答问题。查看;)

以下引自PEP3107

用例

在讨论注释的过程中,提出了许多用例。其中一些按其传达的信息进行分组。还包括可以利用注释的现有产品和包装的示例。

  • 提供打字信息
    • 类型检查([3],[4])
    • 让IDE显示函数期望和返回的类型([17])
    • 函数重载/泛型函数([22])
    • 外语桥梁([18],[19])
    • 改编([21],[20])
    • 谓词逻辑功能
    • 数据库查询映射
    • RPC参数封送([23])
  • 其他资讯
    • 参数和返回值的文档([24])

有关特定点(及其参考)的更多信息,请参见PEP


如果投票者至少发表简短评论,我将不胜感激。这确实会(至少对我)有很大帮助。
klaas

2

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

用其META数据进行解释,以更明确地了解函数值。

注释的编码方式是: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 Pocket Reference,第五版

例:

typeannotations模块提供了一组用于Python代码的类型检查和类型推断的工具。它还提供了一组用于注释功能和对象的类型。

这些工具主要设计用于静态分析器,如linter,代码完成库和IDE。另外,提供了用于进行运行时检查的装饰器。在Python中,运行时类型检查并不总是一个好主意,但在某些情况下,它可能非常有用。

https://github.com/ceronman/typeannotations

键入如何帮助编写更好的代码

键入可以帮助您进行静态代码分析,以在将代码发送到生产环境之前捕获类型错误,并防止出现一些明显的错误。有些工具例如mypy,可以将其添加到工具箱中,作为软件生命周期的一部分。mypy可以通过部分或完全针对您的代码库运行来检查类型是否正确。mypy还可以帮助您检测错误,例如从函数返回值时检查None类型。键入有助于使代码更整洁。您可以在不增加性能成本的情况下使用类型,而不必使用注释在文档字符串中指定类型的方式来记录代码。

干净的Python: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,“用于扩展名”不清楚吗?
Demz,

很明显,但是没有回答恕我直言的问题。这就好比用“用于程序中”来回答“类的良好用法”。这是很清楚,正确的,但是询问方对于好的具体用途有什么用途并不明智。您的答案不可能更通用,举个例子与问题中已经存在的例子基本相同。
BlackJack

1

尽管在此描述了所有用法,但注释的一种可执行且最有可能的强制使用将是类型提示

目前尚未以任何方式强制执行此操作,但从PEP 484判断,Python的未来版本将仅允许类型作为注释的值。

引用注释的现有用法如何?

我们确实希望类型提示最终将成为注释的唯一用法,但这在使用Python 3.5首次键入类型模块之后,将需要进行额外的讨论和弃用期。当前的PEP将具有临时状态(请参阅PEP 411),直到发布Python 3.6。最快的可能方案将在3.6中引入对非类型提示注释的静默弃用,在3.7中引入完全弃用,并将类型提示声明为Python 3.8中唯一允许使用的注释。

尽管我还没有在3.6中看到任何过时的贬值,但是很可能会升至3.7。

因此,即使可能还有其他一些很好的用例,如果您不想在将来有此限制的情况下四处更改所有内容,最好还是仅将它们保留为类型提示。


1

作为一个延迟回答的问题,我的一些软件包(marrow.script,WebCore等)也使用了注释来声明类型转换(即,转换来自Web的传入值,检测哪些参数是布尔开关等)。以执行其他参数标记。

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

对文档字符串或类型转换功能的“裸露”支持可简化与其他可识别注释的库的混合。(即,有一个使用类型转换的Web控制器,它也恰巧作为命令行脚本公开。)

编辑添加:我还开始使用TypeGuard包,该包使用开发时断言进行验证。好处:在启用“优化”(-O/ PYTHONOPTIMIZEenv var)的情况下运行时,可能会很昂贵(例如,递归)的检查被省略,因为您已经在开发中正确测试了应用程序,因此在生产中不必要检查。


-2

注释可用于轻松地模块化代码。例如,我要维护的程序模块可以只定义以下方法:

def run(param1: int):
    """
    Does things.

    :param param1: Needed for counting.
    """
    pass

我们可以要求用户输入一个名为“ param1”的东西,该东西“需要计数”并且应该是“ int”。最后,我们甚至可以将用户提供的字符串转换为所需的类型,以获取最轻松的体验。

请参阅我们的函数元数据对象以获取开放源代码类,该类对此有所帮助,并且可以自动检索所需的值并将其转换为任何所需的类型(因为注释是一种转换方法)。甚至IDE都显示正确的自动完成功能,并假定类型符合注释-非常合适。


-2

如果您查看Cython的好处列表,那么主要的一项功能就是能够告诉编译器Python对象的类型。

我可以预见一个未来,Cython(或编译某些Python代码的类似工具)将使用注释语法来发挥作用。


所述RPython标注器是感觉适当Python化的方法的一个例子; 生成应用程序图之后,它可以计算出每个变量的类型,并且(对于RPython)可以实施单一类型的安全性。OTOH它需要“装箱”或其他解决方案/变通办法,以获取动态的丰富价值。multiply'na' * 8 + ' batman!'完全有效时,我该强迫谁对整数起作用?;)
amcgregor
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.