Python3 的“函数注释”有啥好的用途?
Posted
技术标签:
【中文标题】Python3 的“函数注释”有啥好的用途?【英文标题】:What are good uses for Python3's "Function Annotations"?Python3 的“函数注释”有什么好的用途? 【发布时间】:2011-03-03 13:19:36 【问题描述】:函数注释: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
提供这个有什么意义?
【问题讨论】:
python.org/dev/peps/pep-3107/#use-cases @SilentGhost:不幸的是,许多与实际用例的链接都被破坏了。是否有任何地方可能存储了内容,或者它已经永远消失了? 在 python3 中foo.func_annotations
不应该是 foo.__annotations__
吗?
注解没有特殊意义。 Python 唯一要做的就是将它们放入 annotations 字典中。任何其他操作都取决于您。
def foo(a: 'x', b: 5 + 6, c: list) -> max(2, 9):
是什么意思?
【参考方案1】:
函数注释就是你对它们所做的。
它们可用于文档:
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/ 了解实现类型检查的方法。
【讨论】:
这比用于文档的文档字符串或函数中的显式类型检查更好吗?这似乎无缘无故地使语言复杂化。 @endolith 我们当然可以不用函数注释。它们只是提供访问注释的标准方式。这使得它们可以访问 help() 和工具提示,并使它们可用于自省。 您可以创建类型Mass
和 Velocity
而不是传递数字。
为了充分证明这一点,我会让def kinetic_energy(mass: 'in kilograms', velocity: 'in meters per second') -> float:
也显示返回类型。这是我最喜欢的答案。
@user189728 你是对的。要么需要将返回值保存到变量中,要么需要将整个函数包装在验证装饰器中。【参考方案2】:
我认为这实际上很棒。
来自学术背景,我可以告诉您,注释已证明它们对于为 Java 等语言启用智能静态分析器非常宝贵。例如,您可以定义诸如状态限制、允许访问的线程、架构限制等语义,并且有相当多的工具可以读取并处理它们,以提供超出您从编译器获得的保证的保证。你甚至可以编写检查前置条件/后置条件的东西。
我觉得在 Python 中特别需要这样的东西,因为它的类型较弱,但实际上没有任何结构可以让这种简单直接成为官方语法的一部分。
除了保证之外,注释还有其他用途。我可以看到如何将基于 Java 的工具应用到 Python。例如,我有一个工具可以让您为方法分配特殊警告,并在您调用它们时提示您应该阅读它们的文档(例如,假设您有一个不能以负值调用的方法,但它是从名称上看不直观)。使用注释,我可以在技术上为 Python 编写类似的东西。同样的,如果有官方的语法,也可以写出一个基于标签组织大类中方法的工具。
【讨论】:
ISTM 这些是理论上的好处,只有在标准库和第三方模块都使用函数注释并以一致的含义使用它们并使用经过深思熟虑的注释系统时才能实现。直到那一天(永远不会到来),Python 函数注释的主要用途将是其他答案中描述的一次性用途。暂时你可以忘记智能静态分析器、编译器保证、基于 java 的工具链等。 即使没有使用函数注释的所有内容,您仍然可以在代码中使用它们进行静态分析,这些代码在其输入中包含它们并正在调用具有类似注释的其他代码。在更大的项目或代码库中,这仍然是一个非常有用的代码体,可以在其上执行基于注释的静态分析。 AFAICT,您可以使用装饰器完成所有这些工作,这些装饰器早于注释;因此,我仍然看不到好处。我对这个问题的看法略有不同:***.com/questions/13784713/… 快进到 2015 年,python.org/dev/peps/pep-0484 和 mypy-lang.org 开始证明所有反对者都是错误的。 @DustinWyatt 我很高兴那个预测是错误的 :-) 我们确实从 PEP 484 和一个带有 typeshed 的大部分带注释的标准库中获得了标准化类型。然而,OP 对 Java 风格工具的愿望清单大多尚未实现。【参考方案3】:这是一个迟到的答案,但是 AFAICT,目前最好的函数注释使用是 PEP-0484 和 MyPy。还有来自 Microsoft 的 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
【讨论】:
更多例子在这里Mypy Examples和这里How You Can Benefit from Type Hints 另请参阅pytype - 另一个静态分析器正在构建时考虑到 PEP-0484。 不幸的是,该类型没有被强制执行。如果我在您的示例函数中键入list(fib('a'))
,Python 3.7 会愉快地接受该参数并抱怨无法比较字符串和 int。
@DenisdeBernardy 正如 PEP-484 解释的那样,Python 仅提供类型注释。要强制执行类型,您必须使用 mypy。【参考方案4】:
只是从我的回答here 中添加一个很好用的具体示例,再加上装饰器,可以完成多方法的简单机制。
# 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's original post 所示,但是对参数本身进行注释更好,因为它可以避免参数和类型匹配错误的可能性。
注意:在 Python 中,您可以使用 function.__annotations__
而不是 function.func_annotations
访问注释,因为在 Python 3 中删除了 func_*
样式。
【讨论】:
有趣的应用程序,虽然我担心function = self.typemap.get(types)
在涉及子类时不起作用。在这种情况下,您可能必须使用isinnstance
循环通过typemap
。我想知道@overload
是否正确处理了这个问题
如果函数有返回类型,我认为这会被破坏
__annotations__
是一个 dict
,它不能确保参数顺序,所以这个 sn-p 有时会失败。我建议将types = tuple(...)
更改为spec = inspect.getfullargspec(function)
然后types = tuple([spec.annotations[x] for x in spec.args])
。
你说的很对,@xoolive。你为什么不编辑答案来添加你的修复?
@xoolive:我注意到了。有时,编辑在管理编辑时会使用繁重的手。我已编辑问题以包含您的修复。实际上,我对此有过 a discussion 的意见,但没有办法拒绝修复。顺便感谢您的帮助。【参考方案5】:
Uri 已经给出了正确的答案,所以这里有一个不太严重的答案:所以你可以让你的文档字符串更短。
【讨论】:
喜欢它。 +1。但是,最后,编写文档字符串仍然是使代码可读的第一方法,但是,如果您要实现任何类型的静态或动态检查,那么拥有它是很好的。也许我会发现它的用途。 我不建议在您的文档字符串中使用注释来替代 Args: 部分或 @param 行或类似内容(无论您选择使用什么格式)。虽然文档注释是一个很好的例子,但它会损害注释的潜在力量,因为它可能会妨碍其他更强大的用途。此外,您不能在运行时省略注释以减少内存消耗(python -OO),就像使用文档字符串和断言语句一样。 @gps:就像我说的,这是一个不那么严肃的答案。 说真的,这是记录您期望的类型的更好方法,同时仍然遵守 DuckTyping。 @gps 我不确定 99.999% 的情况下文档字符串的内存消耗是否值得担心。【参考方案6】:第一次看到注释时,我觉得“太好了!我终于可以选择进行类型检查了!”当然,我没有注意到实际上并没有强制执行注释。
所以我决定write a simple function decorator to enforce them:
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 终于有了类型检查之后,我也有同样的失望。最终将不得不继续进行自制的类型检查实施。【参考方案7】:自从有人问这个问题以来已经有很长时间了,但是问题中给出的示例 sn-p 是(如那里所述)来自 PEP 3107,并且在 PEP 示例的末尾还给出了可能回答问题的用例从 PEP 的角度来看 ;)
以下引自PEP3107
用例
在讨论注释的过程中,提出了许多用例。这里介绍了其中一些,按它们传达的信息类型分组。还包括可以使用注释的现有产品和包的示例。
提供打字信息 类型检查([3],[4]) 让 IDE 显示函数期望和返回的类型 ([17]) 函数重载/泛型函数 ([22]) 外语桥梁([18]、[19]) 适应([21],[20]) 谓词逻辑函数 数据库查询映射 RPC 参数封送 ([23]) 其他信息 参数和返回值文档 ([24])请参阅PEP 了解有关特定点的更多信息(以及它们的参考)
【讨论】:
如果投反对票的人至少留下一个简短的评论是什么导致了投反对票,我真的很感激。这真的对(至少我)有很大的改进。【参考方案8】: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 Pocket Reference,第五版
示例:
typeannotations
模块提供了一组用于 Python 代码类型检查和类型推断的工具。它还提供了一组用于注释函数和对象的类型。
这些工具主要设计用于静态分析器,例如 linter、代码完成库和 IDE。此外,还提供了用于进行运行时检查的装饰器。运行时类型检查在 Python 中并不总是一个好主意,但在某些情况下它可能非常有用。
https://github.com/ceronman/typeannotations
打字如何帮助编写更好的代码
键入可以帮助您进行静态代码分析以捕获类型错误 在您将代码发送到生产环境并阻止您执行某些操作之前 明显的错误。有 mypy 之类的工具,您可以将其添加到您的 工具箱作为软件生命周期的一部分。 mypy 可以检查 通过部分或全部运行您的代码库来正确类型。 mypy 还可以帮助您检测错误,例如检查 None 类型 当值从函数返回时。打字有助于使您的 代码清理器。而不是使用 cmets 记录您的代码,其中 您在文档字符串中指定类型,您可以使用没有任何类型的类型 性能成本。
干净的 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,“用于扩展”不清楚? 很清楚,但恕我直言,没有回答问题。这就像用“用于程序中的用途”来回答“类有什么好的用途?”。这很清楚,正确,但是询问方对于 具体 的真正用途并不是很明智。您的答案再通用不过了,其示例与 question 中已有的示例基本相同。【参考方案9】:尽管此处描述了所有用途,但注释的一种可强制使用且最有可能强制使用的是type hints。
这目前没有以任何方式强制执行,但是从 PEP 484 来看,未来版本的 Python 将只允许类型作为注释的值。
引用What about existing uses of annotations?:
我们确实希望类型提示最终会成为注解的唯一用途,但这需要在 Python 3.5 首次推出类型模块后进行额外的讨论和弃用期。在 Python 3.6 发布之前,当前的 PEP 将具有临时状态(参见 PEP 411)。可以想到的最快方案将在 3.6 中引入非类型提示注释的静默弃用,在 3.7 中完全弃用,并将类型提示声明为 Python 3.8 中唯一允许使用的注释。
虽然我在 3.6 中还没有看到任何无声的弃用,但这很可能会被撞到 3.7,而不是。
因此,即使可能有其他一些很好的用例,如果您不想在未来有此限制的情况下更改所有内容,最好只保留它们用于类型提示。
p>【讨论】:
【参考方案10】:作为一个延迟的答案,我的几个包(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
对文档字符串或类型转换函数的“裸”支持允许更轻松地与其他支持注释的库混合。 (即有一个使用类型转换的 Web 控制器,它也恰好作为命令行脚本公开。)
编辑添加:我还开始使用 TypeGuard 包,使用开发时断言进行验证。好处:在启用“优化”(-O
/ PYTHONOPTIMIZE
env var)的情况下运行时,会省略可能很昂贵(例如递归)的检查,因为您认为您已经在开发中正确测试了您的应用程序,因此检查在生产中应该是不必要的。
【讨论】:
【参考方案11】:注释可用于轻松模块化代码。例如。我正在维护的程序的模块可以定义一个方法,例如:
def run(param1: int):
"""
Does things.
:param param1: Needed for counting.
"""
pass
我们可以向用户询问一个名为“param1”的东西,它是“需要计数”并且应该是一个“int”。最后我们甚至可以将用户给的字符串转换成想要的类型,以获得最轻松的体验。
请参阅our function metadata object 了解一个开源类,它可以帮助解决这个问题,并且可以自动检索所需的值并将它们转换为任何所需的类型(因为注释是一种转换方法)。甚至 IDE 也能正确显示自动完成功能,并假设类型是根据注释进行的 - 非常合适。
【讨论】:
【参考方案12】:如果您查看 Cython 的优点列表,其中一个主要优点是能够告诉编译器 Python 对象是哪种类型。
我可以预见未来 Cython(或编译一些 Python 代码的类似工具)将使用注释语法来发挥它们的魔力。
【讨论】:
RPython Annotator 是一种适合 Python 风格的方法示例;在生成应用程序的图形后,它可以计算出每个变量的类型并(对于 RPython)强制执行单一类型安全。 OTOH 它需要“装箱”或其他解决方案/变通方法以允许动态丰富的值。当'na' * 8 + ' batman!'
完全有效时,我是谁来强制我的multiply
函数只对整数起作用? ;)以上是关于Python3 的“函数注释”有啥好的用途?的主要内容,如果未能解决你的问题,请参考以下文章