cards
cards

Reputation: 5005

Dynamically add/overwrite the setter and getter of property attributes

I need to decorate dynamically a getter and setter pair methods in a subclass using/mimic the syntactic sugar syntax.

I am struggling with the setter implementation.

class A:

    def __init__(self, x):
        print('init')
        self.__x = x

    @property
    def x(self):
        print('getter')
        return self.__x

    @x.setter
    def x(self, v):
        print('setter')
        self.__x = v


class Dec:
    def __init__(self):
        print('init - dec')

    def __call__(self, cls):
        c = type('A_Dec', (cls,), {})
        # super-init
        setattr(c, '__init__', lambda sub_self, x: super(type(sub_self), sub_self).__init__(x))
        # getter
        setattr(c, 'x', property(lambda sub_self: super(type(sub_self), sub_self).x))
        
        # setter - see below

        return c

dec_A = Dec()(A)
dec_a = dec_A('p')
print(dec_a.x)

Output

init - dec
init
getter
p

If I try to implement the setter method in Dec, dec_a.x = 'p', with the following methods I collect the following errors:

    # setter-statements of __call__

    # Attempt 1
    setattr(c, 'x', property(fset=lambda sub_self, v: super(type(sub_self), sub_self).x(v)))
    # AttributeError: unreadable attribute
    
    # Attempt 2 - auxiliary function
    def m(sub_self, v):
       print('--> ', sf, super(type(sub_self), sub_self))
       super(type(sub_self), sub_self).x = v
    
    # Attempt 2.A
    setattr(c, 'x', eval('x.setter(m)'))
    # NameError: name 'x' is not defined
    
    # Attempt 2.B
    setattr(c, 'x', property(fset=lambda sf, v: m(sf, v)))
    # AttributeError: unreadable attribute
    
    # Attempt 2.C: !! both at once, `fget`and `fset` so, in case, comment the getter in the above code to avoid conflicts
    setattr(c, 'x', property(fget=lambda sub_self: super(type(sub_self), sub_self).x, fset=m))
    # AttributeError: 'super' object has no attribute 'x'
    
    # Attempt 2.D
    p = property(fget=lambda sub_self: super(type(sub_self), sub_self).x, fset=m)
    setattr(c, 'x', p)
    # AttributeError: 'super' object has no attribute 'x'

Attempt 1 raises an error because (I guess) setting the attribute with brackets. So in Attempt 2 I make use of an auxiliary function, since lambda doesn't allow initialization , '=' statements, again with no success.

EDIT:

Upvotes: 3

Views: 2190

Answers (2)

cards
cards

Reputation: 5005

References

Important

  • working with python 3.9 (otherwise maybe problems with super or others)
  • recall from the doc The zero argument form [super()] only works inside a class definition[...].

Briefly case 2.3 represent the solution of the question, the others case-study show possible alternatives and (maybe) useful information on how to reach it


Step 0: Parent class (reference class equipped with a descriptor protocol)

class A:

    def __init__(self, x): self.__x = x

    @property
    def x(self): return self.__x

    @x.setter
    def x(self, v): self.__x = v

Step 1: review - Statically overwriting getter/setter, two ways

Way 1.1 - syntactic sugar

class B(A):

    def __init__(self, x): super().__init__(x)

    @property
    def x(self): return super().x # still not sure if there isn't a better way

    @x.setter
    def x(self, v): super(B, B).x.fset(self, v)

b = B('x')
print(b.x)
b.x = 'xx'
print(b.x)

# Output
x
xx

Way 1.2 - property as class attribute

class C(A):

    def __init__(self, x): super().__init__(x)

    def x_read(self): return super().x

    def x_write(self, v): super(C, C).x.fset(self, v) # notice the arguments of super!!!

    x = property(x_read, x_write)

c = C('x')
print(c.x)
c.x = 'xx'
print(c.x)

# Output
x
xx

Step 2: Dynamically overwriting getter/setter - two ways

Way 2.1 Static decorator (this is how the solution looks like in a static setting)

class DecStatic:

    def __init__(self):
        print('init - static dec')

    def __call__(self, cls):
        class A_Dec(cls):
            def __init__(sub_self, x):
                super().__init__(x)

            @property
            def x(sub_self):
                return super().x

            @x.setter
            def x(sub_self, v):
                super(type(sub_self), type(sub_self)).x.fset(sub_self, v)

        return A_Dec

dec_A = DecStatic()(A)
dec_a = dec_A('x')
print(dec_a.x)
dec_a.x = 'xx'
print(dec_a.x)

# Output
init - static dec
x
xx

Way 2.2 Dynamic decorator - overwrite the property as class attribute

class DecDynamicClsAttr:
    def __init__(self):
        print('init - dynamic dec - class attr')

    def __call__(self, cls):
        DecA = type('DecA', (cls,), {})
        # super-init
        setattr(DecA, '__init__', lambda sub_self, x: super(type(sub_self), sub_self).__init__(x))
        # property
        setattr(DecA, 'x', property(fget=lambda sub_self: super(type(sub_self), sub_self).x, fset=lambda sub_self, v: super(type(sub_self), type(sub_self)).x.fset(sub_self, v)))

        return DecA

dec_A = DecDynamicClsAttr()(A)
dec_a = dec_A('x')
print(dec_a.x)
dec_a.x = 'xx'
print(dec_a.x)

# Output
init - dynamic dec - class attr
x
xx

Way 2.3 Dynamic decorator - the (bitter) syntactic sugar syntax (<- solution)

class DecDynamicSS:
    def __init__(self):
        print('init - dynamic dec - syntactic sugar')

    def __call__(self, cls):
        DecA = type('A_Dec', (cls,), {})
        # super-init
        setattr(DecA, '__init__', lambda sub_self, x: super(type(sub_self), sub_self).__init__(x))

        # getter
        setattr(DecA, 'x', property(lambda sub_self: super(type(sub_self), type(sub_self)).x.fget(sub_self)))
        # setter
        setattr(DecA, 'x', DecA.x.setter(lambda sub_self, v: super(type(sub_self), type(sub_self)).x.fset(sub_self, v)))

        return DecA

dec_A = DecDynamicSS()(A)
dec_a = dec_A('x')
print(dec_a.x)
dec_a.x = 'xx'
print(dec_a.x)

# Output
init - dynamic dec - syntactic sugar
x
xx

Remark

  • From 2.2 it is clear that the property, characterized by the getter/setter descriptors, is bounded to the class and not to the instance so in 2.3 so one should keep track of the class as well:

       `x.setter` --> `cls.x.setter`
    
  • I am thankful to @chepner for the comment and to the answer of @Niel Godfrey Ponciano for their useful informations

Upvotes: 0

Niel Godfrey P. Ponciano
Niel Godfrey P. Ponciano

Reputation: 10719

The property setter wasn't correctly set. To visualize this, if a setter isn't set for a property explicitly, the attribute becomes read-only as documented.

class Parrot:
    def __init__(self):
        self._voltage = 100000

    @property
    def voltage(self):
        """Get the current voltage."""
        return self._voltage

The @property decorator turns the voltage() method into a “getter” for a read-only attribute with the same name

Let's say we have this:

class A:
    def __init__(self, x):
        self.__x = x

    @property
    def x(self):
        return self.__x

a = A(123)

print(a.x)  # will display "123"
a.x = 456  # will display "AttributeError: can't set attribute"

In your original code, you created a new type A_Dec. You explicitly set the getter:

# getter
setattr(c, 'x', property(lambda sub_self: super(type(sub_self), sub_self).x))

But you didn't explicitly set any setter, thus making the x attribute read-only. This leads to error in this code:

dec_a.x = 'new value!'  # will display "AttributeError: can't set attribute"

Solution 1

Don't explicitly define the getter. This way, all access to x will be delegated to the actual class A.

Solution 2

If you defined the getter, then also define the setter.

...
class Dec:
    ...
    def __call__(self, cls):
        ...
        # setter
        x_property = getattr(c, 'x')
        x_setter = getattr(x_property, 'setter')
        setattr(c, 'x', x_setter(lambda sub_self, v: super(type(sub_self), type(sub_self)).x.fset(sub_self, v)))
        ...
...
  • The usage of c.x.setter is as documented:

    A property object has getter, setter, and deleter methods usable as decorators

  • The usage of .fset is as documented:

    fset is a function for setting an attribute value... The returned property object also has the attributes fget, fset, and fdel corresponding to the constructor arguments.

So adding the below lines would be successful:

dec_a.x = 'new value!'
print(dec_a.x)

Output:

setter
getter
new value!

Further references:

Upvotes: 2

Related Questions