I have a python class which has several properties. I want to implement a method which will return some properties as a dict. I want to mark the properties with a decorator. Here's an example:
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"}
One way to implement it is to set an additional attribute to the properties with the export_to_dict decorator, like this:
def export_to_dict(func):
setattr(func, '_export_to_dict', True)
return func
and to search for properties with the _export_to_dict
attribute when to_dict
is called.
Is there another way to accomplish the task?
Marking each property forces to_dict
to scan all methods/attributes on each invocation, which is slow and inelegant. Here's a self-contained alternative that keeps your example usage identical.
By making export_to_dict
a class, we can use __set_name__
(Python 3.6+) to get a reference of the Foo class and add a new attribute to it. Now to_dict
knows exactly which properties to extract, and because the list is per-class, you can annotate different classes without conflict.
And while we're at it, we can make export_to_dict
automatically generate the to_dict
function to any class that has exported properties.
The decorator also restores the original property after the class is created, so the properties work as normal without any performance impact.
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'}
If you don't want to your Foo class to have an extra attribute, you can store the mapping in a static dict export_to_dict.properties_by_class = {class: [properties]}
.
If you need to support property setters, the situation is a bit more complicated but still doable. Passing property.setter
through is not sufficient, because the setter replaces the getter and __set_name__
is not called (they have the same name, after all).
This can be fixed by splitting the annotation process and creating a wrapper class for 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}