Can I use setattr to dynamically create a property?

Viewed 42

I'm creating a class that takes a variable length set of keys and creates attributes for each of them:

class C:
  def __init__(self, keys_values: Dict[str, int]):
    for k, v in keys_values.item():
      setattr(self, k, v)

I want to take this a step further and set custom setters and getters for each of these attributes. I tried:

class C:
  def __init__(self, keys_values):
    for k, v in keys_values.items():
      setattr(self, f'_{k}', v)
      setattr(self, k, property(
        lambda: self._getter(k),
        lambda v: self._setter(k, v)
      ))

  def _getter(self, k):
    return getattr(self, f'_{k}')

  def _setter(self, k, v):
    # Do some custom stuff
    setattr(self, f'_{k}', v)


c = C({'a': 1, 'b': 2})
print(c._a)
print(c.a)

but the c.a prints out as a property object <property object at 0x7fe9dc1009a0>. Is there an extra step I need to do to have it register is a property of c? And overall is there a better pattern for this?

1 Answers

Yes you can but you probably shouldn't:

class C:
    def __init__(self, keys_values):
        for k, v in keys_values.items():
            setattr(self, f'_{k}', v)
            prop = property(lambda self, k=k: C._getter(self, k))
            prop = prop.setter(lambda self, v, k=k: C._setter(self, k, v))
            setattr(C, k, prop)

    def _getter(self, k):
        print(f'Getter called for {k}')
        return getattr(self, f'_{k}')
    
    def _setter(self, k, v):
        print(f'Getter called for {k}')
        setattr(self, f'_{k}', v)


c = C({'a': 1, 'b': 2})
print(c._a) # 1
print(c.a) # Getter called for a; 1
c.a = 2 # Setter called for a
print(c.a) # Getter called for a; 2

A property is set on the class, not the instance.

This means you need use setattr on the class.

A property also returns a property object with a getter, setter and deleter method. Those methods are typically wrapping the classes methods.

So this creates a property object wrapping C._getter with it's getter method.

prop = property(lambda self, k=k: C._getter(self, k))

This wraps the setter method on that property with C._setter.

prop = prop.setter(lambda self, v, k=k: C._setter(self, k, v))

And finally using setattr(C, k, prop) to set a class attribute as that property object.

A k=k hack is required for the lambdas since they are late-binding and would otherwise all refer to the same k.

This falls short when you want to pass different keys for keys_values since you're setting the propertys on the class, not each individual instance.

What that would mean, is all instances would have access to the same attributes, or propertys as other instances but would sometimes raise an AttributeError. (Depending on whether the setter was called first or not).

Related