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



考虑以下类:

@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模块的东西来读取实际代码并以某种方式找到顺序。但这听起来真的很恶心。

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

api需要一些时间来适应,但这里有一个解决inspect.getsource和一个小的自定义ast节点助行器问题的方法:

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类即可获得所需内容。

还有一个涉及元类和__prepare__钩子的解决方案。诀窍是使用它来提供一个自定义字典,该字典将在整个类创建过程中使用,跟踪fields条目中的新添加,并且在向其添加__annotations__时也会传播字段的累积。

class _FieldAccumulatorDict(dict):
def __init__(self, fields=None):
if fields is None:
# class dict, add a new `fields` list to the dictionary
fields = []
super().__setitem__("fields", fields)
self.__fields = fields
def __setitem__(self, key, value):
if not key.startswith("__"):
self.__fields.append(key)
elif key == "__annotations__":
# propagate accumulation when the `__annotation__` field is set internally
value = _FieldAccumulatorDict(self.__fields)
super().__setitem__(key, value)

class FieldAccumulatorMetaclass(type):
def __prepare__(metacls, *args, **kwargs):
return _FieldAccumulatorDict()

@dataclass
class Point(metaclass=FieldAccumulatorMetaclass):
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...

print(Point.fields)  # will print ['id', 'x', 'y', 'distance_from_zero', 'color']

还要考虑元类是继承的,因此元类可以只分配给根类,然后由层次结构中的所有类使用。

好吧,这是我迄今为止找到的最好的解决方法,其想法是,当创建类时,__annotations__对象将开始逐个填充,因此一个选项是在创建类的过程中跟踪属性。它并不完美,因为它迫使你使用一个替代的装饰器而不是属性,而且它也不能对函数方法做同样的事情(但我现在不在乎(。在我的实现中,您还必须装饰整个类,以便附加一个实际输出订单的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']

(我不会把它标记为答案,因为它有点占地面积,并且不考虑类中的可调用方法(

最新更新