首页 > 解决方案 > Adding properties dynamically to a Python class that point to items in a dictionary

问题描述

I am trying to attach properties dynamically to a class (Registry) for the sake of easy access to values in a dict. I am using defaultdict to define the dictionary, with the default value as an empty list.

But because of the way I am accessing the list values in the dictionary while defining the property, I end up with all properties pointing to the same list object.

Gist: https://gist.github.com/subhashb/adb75a3a05a611c3d9193da695d46dd4


from collections import defaultdict
from enum import Enum


class ElementTypes(Enum):
    PHONE = "PHONE"
    CAR = "CAR"


class Registry:
    def __new__(cls, *args, **kwargs):
        cls.setup_properties()
        instance = super(Registry, cls).__new__(cls, *args, **kwargs)
        return instance

    def __init__(self):
        self._elements = {}

    def register(self, element_type, item):
        if element_type.value not in self._elements:
            self._elements[element_type.value] = []

        self._elements[element_type.value].append(item)

    def get(self, element_type):
        return self._elements[element_type.value]

    @classmethod
    def setup_properties(cls):
        for item in ElementTypes:
            prop_name = item.value
            prop = property(lambda self: getattr(self, "get")(item))

            setattr(Registry, prop_name.lower(), prop)


registry = Registry()
registry.register(ElementTypes.PHONE, "phone1")
registry.register(ElementTypes.PHONE, "phone2")
registry.register(ElementTypes.CAR, "car1")
registry.register(ElementTypes.CAR, "car2")

assert dict(registry._elements) == {
    "CAR": ["car1", "car2"],
    "PHONE": ["phone1", "phone2"],
}
assert hasattr(registry, "phone")
assert hasattr(registry, "car")

assert registry.car == ["car1", "car2"]
assert registry.phone == ["phone1", "phone2"]  # This fails

How do I define the code withing the property to be truly dynamic and get access to the individual list values in the dict?

标签: pythonpython-3.x

解决方案


First, don't setup properties in __new__, that gets called for every Registry object created! Instead, just assign the properties outside the class definition.

Secondly, this trips a lot of people up, but if you use a lambda inside a for-loop and you want to use the item variable, you need to make sure that you add an argument called item with the default value of item, otherwise all the properties will refer to the last item of the loop.

class Registry:
    def __init__(self):
        self._elements = defaultdict(list)

    def register(self, element_type, item):
        self._elements[element_type.value].append(item)

    def get(self, element_type):
        return self._elements[element_type.value]


for item in ElementTypes:
    prop_name = item.value
    prop = property(lambda self, item=item: self.get(item))

    setattr(Registry, prop_name.lower(), prop)

推荐阅读