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 即可获得所需内容。
考虑以下 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 即可获得所需内容。