我有一个 python 类,它有几个属性。我想实现一个方法,它将返回一些属性作为字典。我想用装饰器标记属性。这是一个例子:
class Foo:
@export_to_dict # I want to add this property to dict
@property
def bar1(self):
return 1
@property # I don't want to add this propetry to dict
def bar2(self):
return {"smth": 2}
@export_to_dict # I want to add this property to dict
@property
def bar3(self):
return "a"
@property
def bar4(self):
return [2, 3, 4]
def to_dict(self):
return ... # expected result: {"bar1": 1, "bar3": "a"}
实现它的一种方法是使用export_to_dict装饰器为属性设置一个附加属性,如下所示:
def export_to_dict(func):
setattr(func, '_export_to_dict', True)
return func
并在调用
_export_to_dict
时搜索具有 to_dict
属性的属性。
还有其他方法可以完成任务吗?
标记每个属性会强制
to_dict
在每次调用时扫描所有方法/属性,这是缓慢且不优雅的。这是一个独立的替代方案,可以使您的示例用法保持相同。
通过将
export_to_dict
创建为一个类,我们可以使用 __set_name__
(Python 3.6+) 来获取 Foo 类的引用并向其添加新属性。现在 to_dict
确切地知道要提取哪些属性,并且由于该列表是针对每个类的,因此您可以注释不同的类而不会发生冲突。
当我们这样做时,我们可以让
export_to_dict
自动为任何具有导出属性的类生成 to_dict
函数。
装饰器还会在类创建后恢复原始属性,因此属性可以正常工作,不会影响性能。
class export_to_dict:
def __init__(self, property):
self.property = property
def __set_name__(self, owner, name):
if not hasattr(owner, '_exported_properties'):
owner._exported_properties = []
assert not hasattr(owner, 'to_dict'), 'Class already has a to_dict method'
owner.to_dict = lambda self: {prop.__name__: prop(self) for prop in owner._exported_properties}
owner._exported_properties.append(self.property.fget)
# We don't need the decorator object anymore, restore the property.
setattr(owner, name, self.property)
class Foo:
@export_to_dict # I want to add this property to dict
@property
def bar1(self):
return 1
@property # I don't want to add this propetry to dict
def bar2(self):
return {"smth": 2}
@export_to_dict # I want to add this property to dict
@property
def bar3(self):
return "a"
@property
def bar4(self):
return [2, 3, 4]
# to_dict is not needed anymore here!
print(Foo().to_dict())
{'bar1': 1, 'bar3': 'a'}
如果您不希望 Foo 类具有额外的属性,您可以将映射存储在静态字典中
export_to_dict.properties_by_class = {class: [properties]}
。
如果您需要支持属性设置者,情况会稍微复杂一些,但仍然可行。传递
property.setter
是不够的,因为 setter 替换了 getter 并且 __set_name__
没有被调用(毕竟它们具有相同的名称)。
这可以通过拆分注释过程并为
property.setter
创建包装类来解决。
class export_to_dict:
# Used to create setter for properties.
class setter_helper:
def __init__(self, setter, export):
self.setter = setter
self.export = export
def __set_name__(self, owner, name):
self.export.annotate_class(owner)
setattr(owner, name, self.setter)
def __init__(self, property):
self.property = property
@property
def setter(self):
return lambda fn: export_to_dict.setter_helper(self.property.setter(fn), self)
def annotate_class(self, owner):
if not hasattr(owner, '_exported_properties'):
owner._exported_properties = []
assert not hasattr(owner, 'to_dict'), 'Class already has a to_dict method'
owner.to_dict = lambda self: {prop.__name__: prop(self) for prop in owner._exported_properties}
owner._exported_properties.append(self.property.fget)
def __set_name__(self, owner, name):
self.annotate_class(owner)
# We don't need the decorator object anymore, restore the property.
setattr(owner, name, self.property)
class Foo:
@export_to_dict # I want to add this property to dict
@property
def writeable_property(self):
return self._writeable_property
@writeable_property.setter
def writeable_property(self, value):
self._writeable_property = value
foo = Foo()
foo.writeable_property = 5
print(foo.to_dict())
{'writeable_property': 5}
我个人会采用你原来的方法。我认为这是最干净的,并且未来维护的量最少:
import inspect
EXPORTABLE = "_exportable"
def export_to_dict(prop):
if prop.fget:
setattr(prop.fget, EXPORTABLE, True)
return prop
class Foo:
@export_to_dict
@property
def bar1(self):
return 1
@property
def bar2(self):
return {"smth": 2}
@export_to_dict
@property
def bar3(self):
return "a"
@property
def bar4(self):
return [2, 3, 4]
def to_dict(self):
statics = {inspect.getattr_static(self, prop) for prop in dir(self)}
props = {prop for prop in statics if isinstance(prop, property) and prop.fget is not None}
return {prop.fget.__name__: prop.fget(self) for prop in props if hasattr(prop.fget, EXPORTABLE)}
免责声明:仅在EDIT 3和EDIT 2中实现了完整的读/写协议。特别是,EDIT 2没有“副作用”[请参阅我对BoppreH的答案的评论]。
[编辑 3] 自定义属性
如果需要完整的读/写/...协议,则通过对内置类型property
进行子类化来创建您的
属性。请参阅 doc 了解标准实现的完整描述。
然后使用 custom 属性
PropertyTracker
来跟踪您需要的方法,并为您不介意的方法使用默认的 property
。
这里只是一个示例,请注意
to_dict
是一个属性而不是方法,并且只有在显式触发 custom 属性后才可以访问它。
class PropertyTracker(property):
PROPERTY_TRACKER_ID = 'to_dict' # it will be an attribute (not a method!) of the target class
def __get__(self, obj, objtype=None):
value = super().__get__(obj, objtype)
if not hasattr(obj, self.PROPERTY_TRACKER_ID):
setattr(obj, self.PROPERTY_TRACKER_ID, {})
getattr(obj, self.PROPERTY_TRACKER_ID).update({self.fget.__name__: value})
return value
def __set__(self, obj, value):
super().__set__(obj, value)
p_name = self.fget.__name__
getattr(obj, self.PROPERTY_TRACKER_ID).update({p_name: value})
class Foo:
@PropertyTracker # I want to add this property to dict
def bar1(self):
return 1
@property # I don't want to add this propetry to dict
def bar2(self):
return {"smth": 2}
@PropertyTracker # I want to add this property to dict
def bar3(self):
return self._bar3
@bar3.setter
def bar3(self, v):
self._bar3 = v
@property
def bar4(self): # I don't want to add this propetry to dict
return [2, 3, 4]
f = Foo()
# print(f.to_dict) # <- will raise an error, "to_dict" doesn't exist yet!
f.bar1 # <- after calling a method decorated with the custom property the "to_dict" will become available
f.bar2
print(f.to_dict)
#{'bar1': 1}
f.bar3 = 3
print(f.to_dict)
#{'bar1': 1, 'bar3': 3}
f.bar3 = 333
print(f.to_dict)
#{'bar1': 1, 'bar3': 333}
[编辑2]类装饰器+组合方法装饰器装饰类属性,Python >= 3.8。
支持读/写协议无任何副作用。
初始化时间线
__init__
record
s__call__
dict
class PropertyReader:
# add "to_dict" method and "_recorded_props" to the target class
def __init__(self):
self._recorded_props = {}
def record(self, prop) -> property:
self._recorded_props[prop.fget.__name__] = None # default value
return prop
def __call__(self, target_cls):
# dynamically add "to_dict" method and "recorded_props" attr
setattr(target_cls, self.to_dict.__name__, self.to_dict)
setattr(target_cls, '_recorded_props', self._recorded_props)
# apply getter/setter decorations to the target class
for prop_name in self._recorded_props:
self.decorated_descriptors(target_cls, prop_name)
return target_cls
@staticmethod
def to_dict(target_cls) -> dict:
# don't show the attributes set to None
return {k: v for k, v in target_cls._recorded_props.items() if v is not None}
return target_cls._recorded_props # all key-value pairs, also those with None
@staticmethod
def setter_wrapper(prop):
# setter wrapper to be applied to
def __setter_wrapper(target_cls, v):
prop.fset(target_cls, v)
target_cls._recorded_props.update({prop.fget.__name__: v})
return __setter_wrapper
@classmethod
def decorated_descriptors(cls, target_cls, prop_name) -> None:
# add to the decorated class the new getter/setter
prop = getattr(target_cls, prop_name)
if prop.fset:
setattr(target_cls, prop_name, prop.setter(cls.setter_wrapper(prop)))
else:
target_cls._recorded_props.update({prop_name: prop.fget(target_cls)})
@(prop:=PropertyReader()) # walrus decoration
class Foo:
@prop.record
@property
def bar1(self):
return 1
@property
def bar2(self):
return {"smth": 2}
@prop.record
@property
def bar3(self):
return self._bar3
@bar3.setter
def bar3(self, v):
self._bar3 = v
f = Foo()
print(f.to_dict())
#{'bar1': 1}
f.bar1
f.bar3 = 333
print(f.to_dict())
#{'bar1': 1, 'bar3': 333}
f.bar3 = 300
print(f.to_dict())
#{'bar1': 1, 'bar3': 300}
f.bar3 = 333
print(f.to_dict())
#{'bar1': 1, 'bar3': 333}
[编辑1]类装饰器,带有装饰类属性的参数。
装饰器动态地将实例方法
to_dict
添加到类中。
class PropertyReader:
# add "to_dict" method to a class
@staticmethod
def meta_to_dict(target_cls, prop_dict):
# contains a function that will be the method of the class
def to_dict(self):
# method of the instance
return {k: prop(self) for k, prop in prop_dict.items()}
setattr(target_cls, to_dict.__name__, to_dict)
def __init__(self, *method_names):
self.method_names = method_names
def __call__(self, cls):
# filter attributes by property and identifier
props_dict = {} # dictionary of callable getters
for attr_name in dir(cls):
attr = getattr(cls, attr_name)
if isinstance(attr, property):
if attr_name in self.method_names:
props_dict[attr_name] = attr.fget # callable!
# bind method to the class
self.meta_to_dict(cls, props_dict)
return cls
@PropertyReader('bar1', 'bar3')
class Foo:
...
print(Foo().to_dict())
[原创]使用函数来装饰类。
def export_to_dict(*method_names):
def __wrapper(cls):
def meta_to_dict(prop_dict):
# contains a function that will be the method of the class
def to_dict(self):
# method of the instance
return {k: prop(self) for k, prop in prop_dict.items()}
setattr(cls, to_dict.__name__, to_dict)
# filter attributes by property and identifier
props_dict = {}
for attr_name in dir(cls):
attr = getattr(cls, attr_name)
if isinstance(attr, property):
if attr_name in method_names:
props_dict[attr_name] = attr.fget # callable!
# bind method to the class
meta_to_dict(props_dict)
return cls
return __wrapper
@export_to_dict('bar1', 'bar3')
class Foo:
@property
def bar1(self):
return 1
@property # I don't want to add this propetry to dict
def bar2(self):
return {"smth": 2}
@property
def bar3(self):
return "a"
def bar4(self):
return [2, 3, 4]
f = Foo()
print(f.to_dict())
# {'bar1': 1, 'bar3': 'a'}
可以通过使用类来使装饰器变得“更漂亮”,以避免难以阅读嵌套函数的声明。
如果使用 attrs 是您的一个选择,它可以提供一个非常简洁的解决方案:
import attrs
EXPORT_METADATA = '__export_metadata'
def my_field(default=attrs.NOTHING, validator=None, repr=True, eq=True, order=None, hash=None, init=True,
metadata=None, converter=None, export=False):
metadata = metadata or {}
metadata[EXPORT_METADATA] = export
return attrs.field(default=default, validator=validator, repr=repr, eq=eq, order=order, hash=hash, init=init,
metadata=metadata, converter=converter)
def export_filter(attribute, value):
return attribute.metadata.get(EXPORT_METADATA, False)
@attrs.frozen
class Foo:
bar1 = my_field(default=1, init=False, export=True)
bar2 = my_field(default={"smth": 2}, init=False)
bar3 = my_field(default="a", init=False, export=True)
bar4 = my_field(default=[2, 3, 4], init=False)
def to_dict(self):
return attrs.asdict(self, filter=export_filter)
print(Foo().to_dict())
改编自 attrs 的 元数据示例。
class Exportable:
export_to_dict = ()
def to_dict(self):
return {prop: getattr(self, prop) for prop in self.export_to_dict}
class Foo(Exportable):
export_to_dict = ["bar1", "bar2"]
@property
def bar1(self):
return 1
@property # I don't want to add this propetry to dict
def bar2(self):
return {"smth": 2}
@property
def bar3(self):
return "a"
@property
def bar4(self):
return [2, 3, 4]
assert Foo().to_dict() == {"bar1": 1, "bar2": {"smth": 2}}
将
Exportable
替换为以下内容:
from convtools import conversion as c
class Exportable:
export_to_dict = ()
def to_dict(self):
raise NotImplementedError
def __init_subclass__(cls, **kwargs):
cls.to_dict = c(
{prop: c.attr(prop) for prop in cls.export_to_dict}
).gen_converter()
在底层,
gen_converter
生成以下代码,其中不包含循环:
def converter(data_):
try:
return {"bar1": data_.bar1, "bar2": data_.bar2}
except __exceptions_to_dump_sources:
__convtools__code_storage.dump_sources()
raise