函数注释是Python 3的一项功能,可让您向函数参数添加任意元数据并返回值。 它们是原始Python 3.0规范的一部分。
在本教程中,我将向您展示如何利用通用功能注释并将其与装饰器结合使用。 您还将了解功能注释的优缺点,何时使用它们以及何时最好使用其他机制,例如文档字符串和普通修饰符。
功能注释
功能注释在PEP-3107中指定。 主要动机是提供一种将元数据关联到函数参数和返回值的标准方法。 许多社区成员发现了新颖的用例,但是使用了不同的方法,例如自定义装饰器,自定义docstring格式以及向功能对象添加自定义属性。
重要的是要理解Python不会使用任何语义来祝福注释。 它纯粹为关联元数据提供了一个不错的语法支持,并提供了一种简单的访问方式。 另外,注释完全是可选的。
让我们看一个例子。 这是一个函数foo() ,它接受三个称为a,b和c的参数并输出其总和。 注意foo()不返回任何内容。 第一个参数a不带注释。 第二个参数b用字符串“ annotating b”注释,第三个参数c用int类型注释。 返回值用float类型注释。 请注意用于注释返回值的“->”语法。
def foo(a, b: 'annotating b', c: int) -> float:
print(a + b + c)
注释对函数的执行没有任何影响。 让我们两次调用foo() :一次使用int参数,一次使用string参数。 在这两种情况下, foo()都会做正确的事,并且注释将被忽略。
foo('Hello', ', ', 'World!')
Hello, World!
foo(1, 2, 3)
6
默认参数
在注解后指定默认参数:
def foo(x: 'an argument that defaults to 5' = 5):
print(x)
foo(7)
7
foo()
5
访问功能注释
函数对象具有称为“ 批注 ”的属性。 它是将每个参数名称映射到其注释的映射。 返回值注释映射到键“ return”,该键不能与任何参数名称冲突,因为“ return”是一个保留字,不能用作参数名称。 请注意,可以将名为return的关键字参数传递给函数:
def bar(*args, **kwargs: 'the keyword arguments dict'):
print(kwargs['return'])
d = {'return': 4}
bar(**d)
4
让我们回到第一个示例并检查其注释:
def foo(a, b: 'annotating b', c: int) -> float:
print(a + b + c)
print(foo.__annotations__)
{'c': <class 'int'>, 'b': 'annotating b', 'return': <class 'float'>}
这很简单。 如果用参数数组和/或关键字参数数组对函数进行注释,则显然不能注释单个参数。
def foo(*args: 'list of unnamed arguments', **kwargs: 'dict of named arguments'):
print(args, kwargs)
print(foo.__annotations__)
{'args': 'list of unnamed arguments', 'kwargs': 'dict of named arguments'}
如果您阅读了PEP-3107中有关访问功能注释的部分,则说明您通过功能对象的'func_annotations'属性访问它们。 从Python 3.2开始,这已经过时了。 不要困惑。 它只是“ 注解 ”属性。
您可以使用注释做什么?
这是个大问题。 注释没有标准含义或语义。 有几种通用用途。 您可以将它们用作更好的文档,并将参数和返回值文档移出文档字符串。 例如,此功能:
def div(a, b):
"""Divide a by b
args:
a - the dividend
b - the divisor (must be different than 0)
return:
the result of dividing a by b
"""
return a / b
可以转换为:
def div(a: 'the dividend',
b: 'the divisor (must be different than 0)') -> 'the result of dividing a by b':
"""Divide a by b"""
return a / b
在捕获相同信息的同时,注释版本有很多好处:
- 如果重命名参数,则文档文档字符串版本可能已过期。
- 更容易查看是否未记录参数。
- 无需在文档字符串中提出要由工具解析的参数文档的特殊格式。 注解属性提供了直接的标准访问机制。
我们稍后将讨论的另一种用法是可选类型。 Python是动态类型的,这意味着您可以将任何对象作为函数的参数传递。 但是函数通常需要参数为特定类型。 通过注释,您可以非常自然地在参数旁边指定类型。
请记住,仅指定类型不会强制执行它,并且将需要其他工作(很多工作)。 尽管如此,即使仅指定类型也可以使意图比在文档字符串中指定类型更具可读性,并且可以帮助用户理解如何调用该函数。
与文档字符串相比,注释的另一个好处是您可以将不同类型的元数据作为元组或字典附加。 同样,您也可以使用docstring进行此操作,但是它将基于文本,并且需要特殊的解析。
最后,您可以附加很多元数据,这些元数据将由特殊的外部工具使用,或在运行时通过装饰器使用。 我将在下一部分中探讨此选项。
多个注释
假设您想用一个类型和一个帮助字符串来注释一个参数。 使用批注非常容易。 您可以简单地用具有两个键的dict注释自变量:'type'和'help'。
def div(a: dict(type=float, help='the dividend'),
b: dict(type=float, help='the divisor (must be different than 0)')
) -> dict(type=float, help='the result of dividing a by b'):
"""Divide a by b"""
return a / b
print(div.__annotations__)
{'a': {'help': 'the dividend', 'type': float},
'b': {'help': 'the divisor (must be different than 0)', 'type': float},
'return': {'help': 'the result of dividing a by b', 'type': float}}
结合Python注释和装饰器
注解和装饰器齐头并进。 有关Python装饰器的一个很好的介绍,请查看我的两个教程:“ 深入研究Python装饰器”和“ 编写您自己的Python装饰器” 。
首先,注释可以完全实现为装饰器。 您只需定义一个@annotate装饰器,并使其使用参数名称和Python表达式作为参数,然后将其存储在目标函数的注解属性中即可。 对于Python 2也可以做到这一点。
但是,装饰器的真正功能是它们可以对注释起作用。 当然,这需要对注释的语义进行协调。
让我们来看一个例子。 假设我们要验证参数是否在一定范围内。 注释将是一个元组,其中每个参数的最小值和最大值。 然后,我们需要一个装饰器,该装饰器将检查每个关键字参数的注释,验证该值是否在范围内,否则引发异常。 让我们从装饰器开始:
def check_range(f):
def decorated(*args, **kwargs):
for name, range in f.__annotations__.items():
min_value, max_value = range
if not (min_value <= kwargs[name] <= max_value):
msg = 'argument {} is out of range [{} - {}]'
raise ValueError(msg.format(name, min_value, max_value))
return f(*args, **kwargs)
return decorated
现在,让我们定义函数并使用@check_range装饰器对其进行装饰。
@check_range
def foo(a: (0, 8), b: (5, 9), c: (10, 20)):
return a * b - c
让我们用不同的参数调用foo() ,看看会发生什么。 当所有参数都在其范围内时,就没有问题。
foo(a=4, b=6, c=15)
9
但是,如果将c设置为100(超出(10,20)范围),则会引发异常:
foo(a=4, b=6, c=100)
ValueError: argument c is out of range [10 - 20]
什么时候应该使用装饰器而不是注释?
在许多情况下,装饰器比附加元数据的注释要好。
一种明显的情况是您的代码是否需要与Python 2兼容。
另一种情况是,如果您有很多元数据。 如您先前所见,虽然可以通过使用dict作为注释来附加任何数量的元数据,但它相当麻烦并且实际上会损害可读性。
最后,如果元数据应该由特定的修饰器操作,则最好将元数据关联为修饰器本身的参数。
动态注释
注释只是函数的dict属性。
type(foo.__annotations__)
dict
这意味着您可以在程序运行时即时修改它们。 有哪些用例? 假设您想确定是否使用了参数的默认值。 每当使用默认值调用该函数时,都可以增加注释的值。 或者,也许您想总结所有返回值。 动态方面可以在函数本身内部或由装饰器完成。
def add(a, b) -> 0:
result = a + b
add.__annotations__['return'] += result
return result
print(add.__annotations__['return'])
0
add(3, 4)
7
print(add.__annotations__['return'])
7
add(5, 5)
10
print(add.__annotations__['return'])
17
结论
功能注释功能多样且令人兴奋。 他们有可能进入自省工具的新时代,以帮助开发人员掌握越来越复杂的系统。 它们还为更高级的开发人员提供了一种标准且可读的方法,可将元数据直接与参数和返回值关联,以创建自定义工具并与装饰器进行交互。 但是需要一些工作才能从中受益并发挥其潜力。
无论您是刚刚起步还是想学习新技能的经验丰富的程序员,都可以通过我们完整的python教程指南学习Python。
翻译自: https://code.tutsplus.com/tutorials/python-3-function-annotations--cms-25689