Search code examples
pythonpropertiesdecorator

How to export to dict some properties of an object


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?


Solution

  • 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.

    Keep a list of exported properties in a class attribute

    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]}.


    Properties with setters

    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}