【问题标题】:How to get type annotation within python function scope?如何在 python 函数范围内获取类型注释?
【发布时间】:2021-06-09 11:06:25
【问题描述】:

例如:

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

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

我想要的是在函数test 中获取类型注释,就像在全局范围或类范围内返回的注释字典一样。

有什么方法可以做到这一点吗?

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

【问题讨论】:

    标签: python python-3.x annotations type-hinting


    【解决方案1】:

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

    来自PEP 526 specification

    注释局部变量将导致解释器将其视为局部变量,即使它从未被分配给。不会评估局部变量的注释[.]

    [...]

    此外,在模块或类级别,如果被注释的项目是一个简单的名称,那么它和注释将存储在该模块或类的__annotations__属性中[。]

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

    模块
    [...] 预定义(可写)属性:[...]; __annotations__(可选)是一个字典,包含在模块主体执行期间收集的变量注释; [...].

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

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

    类块中定义的名称范围仅限于类块;它没有扩展到方法的代码块——这包括理解和生成器表达式,因为它们是使用函数范围实现的。

    您将改为通过对类的引用来访问类命名空间。您可以通过使用类全局名称或内部绑定方法(通过type(self))获得这样的引用,在类方法内部通过cls 参数。在这种情况下,只需使用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 将这些注释转换为类型对象。

    【讨论】:

    • 感谢您提供的超级信息回答!您是否知道为什么函数体中的注释在运行时不可用?仅仅是性能问题吗?我希望将注释(作为字符串)保存到变量的属性中会很便宜(并且可能可以由解释器进一步优化)。
    • @max:变量本身不是对象;它们只是构成命名空间的字典中的字符串键。对于函数,本地命名空间甚至不是字典;名称被编译为数组中的索引。
    • @max 在外部命名空间注释存储为函数(用于函数参数和返回值)、类(用于类和实例属性)以及单独的全局名称的单独字典在全局命名空间中(实际上是模块的一个属性)。它们在那里供其他代码使用,以回答这个函数期望接收什么之类的问题。函数中的局部变量是“隐藏的”,因为它们仅在函数运行时存在,因此外部的任何人都没有必要自省其注释。
    • 谢谢!我在运行时读取本地对象类型提示的用例并不好。我有一个函数f(x: Union[List[int], List[str]]),它的行为基于x 的运行时类型而有所不同。由于无法推断空列表的类型,我想要求用户将类型注释变量传递给f(无需任何计算)。即使我可以阅读类型注释,它也不会起作用,因为没有(干净的)方法可以找到传递给函数的对象的名称。更不用说即使可行,这也是一个丑陋的解决方案。
    【解决方案2】:

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-05-21
      • 2016-11-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-11-11
      相关资源
      最近更新 更多