如何在 python 函数范围内获取类型注释?

How to get type annotation within python function scope?

例如:

def test():
    a: int
    b: str
    print(__annotations__)
test()

此函数调用引发 NameError: name '__annotations__' is not defined 错误。

我想要的是在函数test中获取类型注解,比如全局作用域或class作用域中返回的注解字典。

有什么方法可以实现吗?

如果不可能,为什么存在这种语法?

在函数中,局部变量的注解保留,因此无法在函数中访问。只有 模块和 class 级别 的变量注解会导致附加 __annotations__ 对象。

来自PEP 526 specification

Annotating a local variable will cause the interpreter to treat it as a local, even if it was never assigned to. Annotations for local variables will not be evaluated[.]

[...]

In addition, at the module or class level, if the item being annotated is a simple name, then it and the annotation will be stored in the __annotations__ attribute of that module or class[.]

仅当定义了实际模块级注释时才设置__annotations__全局; data model states it is optional:

Modules
[...] Predefined (writable) attributes: [...]; __annotations__ (optional) is a dictionary containing variable annotations collected during module body execution; [...].

定义后,您可以从模块内的函数或通过 globals() function.

访问它

如果您在 class 语句内的函数中尝试此操作,则知道 class 正文命名空间是 not part of the scope of nested functions:

The scope of names defined in a class block is limited to the class block; it does not extend to the code blocks of methods – this includes comprehensions and generator expressions since they are implemented using a function scope.

您将改为通过对 class 的引用来访问 class 命名空间。您可以通过使用 class 全局名称或通过 type(self) 的内部绑定方法,通过 cls 参数在 class 方法内部获得此类引用。在这种情况下只需使用 ClassObject.__annotations__

如果您必须有权访问函数局部主体中的注释,则您需要自己解析源代码。 Python AST 确实保留了本地注释:

>>> import ast
>>> mod = ast.parse("def foo():\n    a: int = 0")
>>> print(ast.dump(mod.body[0], indent=4))
FunctionDef(
    name='foo',
    args=arguments(
        posonlyargs=[],
        args=[],
        kwonlyargs=[],
        kw_defaults=[],
        defaults=[]),
    body=[
        AnnAssign(
            target=Name(id='a', ctx=Store()),
            annotation=Name(id='int', ctx=Load()),
            value=Constant(value=0),
            simple=1)],
    decorator_list=[])

上面显示了带有单个注释的函数体的文本表示; AnnAssign 节点告诉我们 a 被注释为 int。您可以通过以下方式收集此类注释:

import inspect
import ast

class AnnotationsCollector(ast.NodeVisitor):
    """Collects AnnAssign nodes for 'simple' annotation assignments"""

    def __init__(self):
        self.annotations = {}

    def visit_AnnAssign(self, node):
        if node.simple:
            # 'simple' == a single name, not an attribute or subscription.
            # we can therefore count on `node.target.id` to exist. This is
            # the same criteria used for module and class-level variable
            # annotations.
            self.annotations[node.target.id] = node.annotation

def function_local_annotations(func):
    """Return a mapping of name to string annotations for function locals

    Python does not retain PEP 526 "variable: annotation" variable annotations
    within a function body, as local variables do not have a lifetime beyond
    the local namespace. This function extracts the mapping from functions that
    have source code available.
 
    """
    source = inspect.getsource(func)
    mod = ast.parse(source)
    assert mod.body and isinstance(mod.body[0], (ast.FunctionDef, ast.AsyncFunctionDef))
    collector = AnnotationsCollector()
    collector.visit(mod.body[0])
    return {
        name: ast.get_source_segment(source, node)
        for name, node in collector.annotations.items()
    }

上面的 walker 在函数对象的源代码中找到所有 AnnAssignment 注释(因此需要有可用的源文件),然后使用 AST 源行和列信息来提取注释来源。

给定你的测试函数,上面的结果是:

>>> function_local_annotations(test)
{'a': 'int', 'b': 'str'}

类型提示未解析,它们只是字符串,因此您仍然需要使用 typing.get_type_hints() function 将这些注释转换为类型对象。

我从别人那里复制的另一个简单的解决方案。

import re
def fn(q: int):
    a: int = 1
def get_types(fn):
    source = inspect.getsource(fn)
    var_tps = re.findall("  +([a-z0-9]+) *?: *([a-z0-9]+) *=", source)
    return var_tps

get_types(fn) # [('a', 'int')]