1

I have this code, wherein the method get_all_properties() returns a dictionary mapping property names to the instance's values for those properties:

class Policy:
    def __init__(self):
        self._policyDefinitionId = "value_policy_id"
        self._policyDefinitionReferenceId = "value_def_reference_id"
        self._displayName = "value_display_name"

    def some_method(self):
        pass

    @property
    def policyDefinitionId(self):
        return self._policyDefinitionId

    @property
    def displayName(self):
        return self._displayName

    @property
    def policyDefinitionReferenceId(self):
        return self._policyDefinitionReferenceId

    def get_all_properties(self) -> dict:
        return {
            'policyDefinitionId': self.policyDefinitionId,
            'displayName': self.displayName,
            'policyDefinitionReferenceId': self.policyDefinitionReferenceId,
        }

How can I generate such a dictionary programmatically, without manually specifying dictionary pairs? I tried using the introspection functionality of __dir__ and __dict__, but couldn't figure it out.

Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153
4snok
  • 62
  • 5
  • "tried using introspection functions dir and dict with no luck" **How** did you try? **What happened** when you tried that? **How is that different** from what you want? – Karl Knechtel Dec 05 '22 at 12:07

1 Answers1

2

The __dict__ of the class gives us class attributes, which includes the methods (which are plain functions when we look them up directly in the class) and properties:

>>> Policy.__dict__
mappingproxy({'__module__': '__main__', '__init__': <function Policy.__init__ at 0x7f26fb4e9310>, 'some_method': <function Policy.some_method at 0x7f26fb4e93a0>, 'policyDefinitionId': <property object at 0x7f26fb4e2e50>, 'displayName': <property object at 0x7f26fb4e7040>, 'policyDefinitionReferenceId': <property object at 0x7f26fb4e7090>, 'get_all_properties': <function Policy.get_all_properties at 0x7f26fb4e95e0>, '__dict__': <attribute '__dict__' of 'Policy' objects>, '__weakref__': <attribute '__weakref__' of 'Policy' objects>, '__doc__': None})

Notice how the properties are represented as <property object at ...>. This is because the @property decorator actually creates instances of a built-in property class, and those instances (through the usual decorator process) replace the original functions. The name property is exposed as a builtin, so we can trivially use it for type checking:

>>> {k: v for k, v in Policy.__dict__.items() if isinstance(v, property)}
{'policyDefinitionId': <property object at 0x7f26fb4e2e50>, 'displayName': <property object at 0x7f26fb4e7040>, 'policyDefinitionReferenceId': <property object at 0x7f26fb4e7090>}

In the get_all_properties method, the goal is to apply the logic of each property to the self instance. There are two approaches we can take:

Using the property object directly

We need to understand a little about how it works, first:

>>> prop = Policy.__dict__['displayName']
>>> dir(prop)
['__class__', '__delattr__', '__delete__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__isabstractmethod__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__set__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'deleter', 'fdel', 'fget', 'fset', 'getter', 'setter']

See the fget attribute? That stores the original function. Through this chain of lookup, it's still a function, not a method; so we need to pass an instance explicitly:

>>> policy = Policy()
>>> prop.fget(policy)
'value_display_name'

So now we know everything we need in order to implement get_all_properties - we just pass self explicitly, to functions that we access as the .fget of properties that are found in the __dict__:

def get_all_properties(self) -> dict:
    return {
        k: v.fget(self)
        for k, v in Policy.__dict__.items()
        if isinstance(v, property)
    }

Using dynamic lookup

Since we already know the names of the properties from the filtered __dict__ result, we can dynamically look up the attribute with that name on self. That looks like:

def get_all_properties(self) -> dict:
    return {
        k: getattr(self, k)
        for k, v in Policy.__dict__.items()
        if isinstance(v, property)
    }

Either way gets us the desired result:

>>> Policy().get_all_properties()
{'policyDefinitionId': 'value_policy_id', 'displayName': 'value_display_name', 'policyDefinitionReferenceId': 'value_def_reference_id'}
Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153
  • Tested this and it works even if there are `@property.setter` (and `@property. deleter` probably too) if they are defined with the same name. If someone was to use a different name for the setter, like `def displayNameSetter(self, value):`, is there a way to check if the property is specifically a "getter"? – Tzane Dec 05 '22 at 12:42
  • 1
    Setters defined with a different name don't actually work properly in the first place; they'll create a second property that re-uses the first property's get logic and also makes it settable. Since it *has* get logic, it is just as much a distinct property. There isn't a simple way to filter it out of the result, and it's not clear that it *should* be filtered out. After all, using either name to access the value will work. – Karl Knechtel Dec 05 '22 at 12:54
  • Looked into this a bit and one way could be to check the values of `.fget`, `.fset` and `.fdel`. https://stackoverflow.com/a/49943617/14536215 – Tzane Dec 05 '22 at 13:11
  • Yes, those are the relevant attributes of the `property`. You can inspect them according to your exact needs, although I doubt you actually need to do anything special. – Karl Knechtel Dec 05 '22 at 13:15