Reputation: 5005
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.
Is there a way to mimic the property getter/setter decorators dynamically? (Possibly without extra imports) Is there another way to do it?
Extra: why super doesn't work without attributes? super().x(v)
-> TypeError: super(type, obj): obj must be an instance or subtype of type
EDIT:
Upvotes: 3
Views: 2190
Reputation: 5005
References
Important
python 3.9
(otherwise maybe problems with super
or others)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
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"
Don't explicitly define the getter. This way, all access to x
will be delegated to the actual class A
.
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)))
...
...
c.x.setter
is as documented:
A property object has
getter
,setter
, anddeleter
methods usable as decorators
.fset
is as documented:
fset
is a function for setting an attribute value... The returned property object also has the attributesfget
,fset
, andfdel
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