python 是否保留其他方法的注释顺序?

Does python preserve order of annotations with other methods?

考虑以下 class:

@dataclass
class Point:
   id: int

   x: int
   y: int
   @property
   def distance_from_zero(self): return (self.x**2 + self.y**2)**0.5

   color: tuple #RGB or something...

我知道我可以从 __annotations__ 变量中获取注释,或者从 dataclasses.fields 中按顺序获取字段函数。 我也知道可以使用 dir 或使用 __dict__ 方法读取任何对象的普通方法。

但我所追求的是可以正确顺序给我的东西,在上面的例子中,它会是这样的:

>>>get_all_fields(Point)
['id', 'x', 'y', 'distance_from_zero', 'color']

我唯一能想到的就是使用 inspect 模块之类的东西来读取实际代码并以某种方式找到顺序。但这听起来真的很讨厌。

好吧,这是我迄今为止找到的最好的解决方法,想法是当创建 class 时,__annotations__ 对象将开始一个一个地填充,所以一个选项是在创建 class 期间跟踪属性。它并不完美,因为它迫使您使用替代装饰器而不是 属性,并且它也不能对函数方法做同样的事情(但我现在不关心那个)。在我的实现中,您还必须装饰整个 class 以附加一个实际输出订单的 classmethod

import inspect
def ordered_property( f ):
    if isinstance(f, type):
        @classmethod
        def list_columns( cls ):
            if not list_columns.initiated:
                for annotation in cls.__annotations__:
                    if annotation not in cls.__columns__:
                        cls.__columns__.append(annotation)
                list_columns.initiated = True
            return cls.__columns__

        list_columns.initiated = False
        f.list_columns = list_columns
        return f
    else:
        #Two stacks from the start, is the class object that's being constructed.
        class_locals = inspect.stack()[1].frame.f_locals
        class_locals.setdefault('__columns__', [])

        for annotation in class_locals['__annotations__']:
            if annotation not in class_locals['__columns__']:
                class_locals['__columns__'].append(annotation)

        class_locals['__columns__'].append(f.__name__)
        return property(f)

问题中的示例必须更改为:

@dataclass
@ordered_property
class Point:
   id: int

   x: int
   y: int
   @ordered_property
   def distance_from_zero(self): return (self.x**2 + self.y**2)**0.5

   color: tuple #RGB or something...

最后的输出将是这样的:

>>>Point.list_columns()
['id', 'x', 'y', 'distance_from_zero', 'color']

(我不会将此标记为答案,因为它有一些占用空间,并且不考虑 class 中的可调用方法)

可以通过自省构建一个干净的解决方案,因为 python 足以使负责解析 python 代码 (ast) 的模块可供我们使用。

api 需要一些时间来适应,但这里有一个针对 inspect.getsource 问题的修复程序和一些自定义 ast-node walker:

import ast
import inspect


class AttributeVisitor(ast.NodeVisitor):
    def visit_ClassDef(self, node):
        self.attributes = []
        for statement in node.body:
            if isinstance(statement, ast.AnnAssign):
                self.attributes.append(statement.target.id)
            elif isinstance(statement, ast.FunctionDef):
                # only consider properties
                if statement.decorator_list:
                    if "property" in [d.id for d in statement.decorator_list]:
                        self.attributes.append(statement.name)
            else:
                print(f"Skipping {statement=}")

# parse the source code of "Point", so we don't have to write a parser ourselves
tree = ast.parse(inspect.getsource(Point), '<string>')

# create a visitor and run it over the tree line by line
visitor = AttributeVisitor()
visitor.visit(tree)

# print result, should be ['id', 'x', 'y', 'distance_from_zero', 'color']
print(visitor.attributes)

使用此解决方案意味着您无需以任何方式更改 Point class 即可获得所需内容。