Reputation: 11
I have the following code involving a decorator decorating a property getter into a property-like object that reimplements mutation dunders to also invoke a specified function attribute fetched from the self object upon mutation (kind of a on_update callback registration as a decorator). I have been trying to make it work with some simple test code and it just wont work giving out the following error:
The error:
Exception has occurred: AttributeError
instance(<function foo.x at 0x000001DA0891EAC0>) has no attribute update
File "code\decorators\main.py", line 235, in wrapped_fget
raise AttributeError(f'instance({self}) has no attribute {fupdate}')
File "code\decorators\main.py", line 237, in wrapper
return wrapped_fget(fget)
^^^^^^^^^^^^^^^^^^
File "code\decorators\main.py", line 257, in foo
@on_update('update')
^^^^^^^^^^^^^^^^^^^
The output
updatable called with fupdate=update
wrapper called with fget=<function foo.x at 0x000001DA0891EAC0>
wrapped_fget called with self=<function foo.x at 0x000001DA0891EAC0>
The library code:
import functools as ft
class updatable:
"""
A descriptor class that allows updating of attributes.
Args:
ifget: The getter function for the attribute.
ifupdate: The update function for the attribute.
Attributes:
fget: The getter function for the attribute.
fupdate: The update function for the attribute.
fdel: The delete function for the attribute.
fset: The setter function for the attribute.
fadd: The addition function for the attribute.
fsub: The subtraction function for the attribute.
fmul: The multiplication function for the attribute.
iadd: The in-place addition function for the attribute.
isub: The in-place subtraction function for the attribute.
imul: The in-place multiplication function for the attribute.
"""
def __init__(self, ifget=None, ifupdate=None):
print(f'init called with ifget={ifget} and ifupdate={ifupdate}')
self.fget = ifget
self.fupdate = ifupdate
self.fdel = getattr(ifget, '__del__', None)
self.fset = getattr(ifget, '__set__', None)
self.fadd = getattr(ifget, '__add__', None)
self.fsub = getattr(ifget, '__sub__', None)
self.fmul = getattr(ifget, '__mul__', None)
self.iadd = getattr(ifget, '__iadd__', None)
self.isub = getattr(ifget, '__isub__', None)
self.imul = getattr(ifget, '__imul__', None)
def __update(self, val=None):
print(f'__update called with val={val}')
if self.fupdate is None:
raise AttributeError('cant update using None')
self.fupdate()
return val
def __get__(self, obj, objtype=None):
print(f'__get__ called with obj={obj} and objtype={objtype}')
if obj is None:
print('__get__ - obj is None')
return self
if self.fget is None:
raise AttributeError("unreadable attribute")
return self.fget(obj)
def __set__(self, obj, value):
print(f'__set__ called with obj={obj} and value={value}')
if self.fset is None:
raise AttributeError("can't set attribute")
self.fset(obj, value)
self.__update()
def __delete__(self, obj):
print(f'__delete__ called with obj={obj}')
if self.fdel is None:
raise AttributeError("can't delete attribute")
self.fdel(obj)
def __add__(self, val2):
print(f'__add__ called with val2={val2}')
if self.fadd is None:
raise AttributeError('no add')
return self.fadd(val2)
def __sub__(self, val2):
print(f'__sub__ called with val2={val2}')
if self.fsub is None:
raise AttributeError('no sub')
return self.fsub(val2)
def __mul__(self, val2):
print(f'__mul__ called with val2={val2}')
if self.fmul is None:
raise AttributeError('mul')
return self.fmul(val2)
def __iadd__(self, val):
print(f'__iadd__ called with val={val}')
print('iadd called')
if self.iadd is None:
raise AttributeError('iadd')
return self.__update(self.iadd(val))
def __imul__(self, val):
print(f'__imul__ called with val={val}')
if self.imul is None:
raise AttributeError('imul')
return self.__update(self.imul(val))
def __isub__(self, val):
print(f'__isub__ called with val={val}')
if self.isub is None:
raise AttributeError('isub')
return self.__update(self.isub(val))
def updater(self, fupdate2):
print(f'updater called with fupdate2={fupdate2}')
return type(self)(self.fget, fupdate2)
def getter(self, getter):
print(f'getter called with getter={getter}')
return type(self)(getter, self.fupdate)
def on_update(fupdate=None):
"""
Decorator that adds an update function to a getter method.
Args:
fupdate: The update function.
Returns:
The wrapped getter method.
"""
print(f'updatable called with fupdate={fupdate}')
def wrapper(fget):
print(f'wrapper called with fget={fget}')
@ft.wraps(fget)
def wrapped_fget(self,*args, **kwargs):
print(f'wrapped_fget called with self={self}')
if not hasattr(self, '__dict__'):
raise AttributeError('instance has no attribute __dict__')
if not hasattr(self, fupdate):
#the test code ends up raising this exception
raise AttributeError(f'instance({self}) has no attribute {fupdate}')
return updatable(fget, self.getattr(fupdate)).getter
return wrapped_fget(fget)
return wrapper
The test code:
class foo:
"""
A sample class with an updatable attribute.
Attributes:
__x: The private attribute.
"""
def __init__(self):
self.__x = 0
def update(self):
"""
Update function for the attribute.
"""
print('update')
#usage of the decorator (wrapping the updatable(None,self.update).getter method arround the x attribute getter)
@on_update('update')
def x(self):
"""
Getter method for the attribute.
Returns:
The value of the attribute.
"""
return self.__x
print()
xx = foo()
print(xx.x.fget)
xx.x+=1
PLEASE, help me figure out the issue
I tried just about anything to understand why the heck i am not able to retrieve the self argument, but I cant figure this one out guys.
Upvotes: 1
Views: 121
Reputation: 1030
DISCLAIMER: I don't recommend this for production use. The example is written for "educational" purposes only.
Let's start with why you are not getting the correct self
object. The problem is in your decorator:
def on_update(fupdate=None):
"""
Decorator that adds an update function to a getter method.
Args:
fupdate: The update function.
Returns:
The wrapped getter method.
"""
print(f'updatable called with fupdate={fupdate}')
def wrapper(fget):
print(f'wrapper called with fget={fget}')
@ft.wraps(fget)
def wrapped_fget(self,*args, **kwargs):
print(f'wrapped_fget called with self={self}')
if not hasattr(self, '__dict__'):
raise AttributeError('instance has no attribute __dict__')
if not hasattr(self, fupdate):
#the test code ends up raising this exception
raise AttributeError(f'instance({self}) has no attribute {fupdate}')
return updatable(fget, self.getattr(fupdate)).getter
return wrapped_fget(fget)
return wrapper
wrapped_fget
function by passing the arguments fget
, fget = foo.x
. So in the wrapped_fget
function, self = foo.x
.updatable(fget, self.getattr(fupdate)).getter
, However, even if it were an instance of the class itself, you will still get an error in this line. Nowhere in the code, you have the getattr
method. Perhaps you wanted to do getattr(self, fupdate)
!What I understood from your code and comments to this question is that you would like to create a decorator for a method that initially does not use @property
so that it behaves the same way in the end. While also displaying information whether a certain method was called inside the object that is in the __x
variable when it changed. To make this work as you intend, you need to use a wrapper for your attribute in addition to the decorator for the method. I will emphasize again that this is written for familiarization purposes only. And also it will not work with embedded objects such as: None
, True
, False
.
Okay, now let's move on to the code:
from functools import wraps
def _object_wrapper(obj, listen_methods, callback):
def method_wrapper(base):
@wraps(base)
def wrapper(*args, **kwargs):
callback(base.__name__)
return base(*args, **kwargs)
return wrapper
obj_type = type(obj)
def patch_exists_methods():
for method_name in listen_methods:
if hasattr(obj_type, method_name):
base_method = getattr(obj, method_name)
setattr(obj, method_name, method_wrapper(base_method))
return obj
# for builtins objects
def create_new_mimic_obj():
new_methods = {}
for method_name in listen_methods:
if hasattr(obj_type, method_name):
base_method = getattr(obj_type, method_name)
new_methods[method_name] = method_wrapper(base_method)
return type(obj_type.__name__, (obj_type,), new_methods)(obj)
try:
return patch_exists_methods()
except AttributeError:
try:
return create_new_mimic_obj()
except TypeError:
# when `__slots__` is used
# or the class does not accept `self-like` as an argument to
# `__init__` or more than 1 argument to `__init__` is expected
# or possibly something else.
return obj
def log_function(method_name):
print(f'method `{method_name}` was called')
def on_update(attr_name, callback, listen_methods):
def get_property_name(obj):
if attr_name.startswith('__'):
return f'_{get_class_name(obj)}{attr_name}'
return attr_name
def get_class_name(obj):
return f'{type(obj).__name__}'
def debug(method_called, obj):
print(
f'{method_called} {get_class_name(obj)}.{get_property_name(obj)}'
)
def wrapper(method):
wrapped_obj = None
def _getter(self):
nonlocal wrapped_obj
debug('getter', self)
if wrapped_obj is None:
wrapped_obj = _object_wrapper(
obj=method(self),
listen_methods=listen_methods,
callback=callback,
)
return wrapped_obj
def _setter(self, new_value):
nonlocal wrapped_obj
debug('setter', self)
wrapped_obj = new_value
setattr(self, get_property_name(self), new_value)
def _deleter(self):
nonlocal wrapped_obj
wrapped_obj = None
debug('deleter', self)
delattr(self, get_property_name(self))
return property(fget=_getter, fset=_setter, fdel=_deleter)
return wrapper
In this code, the following happens. The on_update
decorator takes three arguments:
attr_name
- the name of the attribute variable used as getter
. This is necessary to be able to use setter
and deleter
.callback
- function that will be called when one of listen_methods
is called.listen_methods
- iterable object with methods after calling of which the function - callback
should be called.The on_update
decorator itself substitutes the original function and returns a property
object instead. So you can work with it as if you were using decorators: property
, x.setter
, x.deleter
.
But the most interesting thing is the object_wrapper
function, it takes the original object that references the __x
variable and replaces it with a similar object that behaves and looks the same as the original, but with modification of the methods specified in the listen_methods
argument. After calling one of the listen_methods
methods, the callback
function will always be called.
And I'll say it again, it's unreliable! But it will work. Here's an example.
class Foo:
def __init__(self):
self.__x = 0
@on_update('__x', log_function, ['__add__', '__sub__', '__mul__'])
def x(self):
return self.__x
foo = Foo()
print(foo.x)
print('*************')
foo.x += 1
print('*************')
foo.x -= 2
print('*************')
foo.x *= 10
print('*************')
print(foo.__dict__)
Console output:
getter Foo._Foo__x
0
*************
getter Foo._Foo__x
method `__add__` was called
setter Foo._Foo__x
*************
getter Foo._Foo__x
method `__sub__` was called
setter Foo._Foo__x
*************
getter Foo._Foo__x
method `__mul__` was called
setter Foo._Foo__x
*************
{'_Foo__x': -10}
Upvotes: 0
Reputation: 142985
Frankly, I don't understand idea of this code. I don't understand what it has to do. For me it seems too complicated. But I found some mistakes and I put it as answer.
The biggest problem is that decorators are executed when code is loaded - so on_update()
is executed before instance of class Foo
is created and wrapper(fget)
can't have access to self
but (as for me) it should return new updatable...getter
instead of wrapped_fget
. But only new wrapped_fget()
may have access self
because it is executed when instance is created - xxx = Foo()
, xx.x()
.
So as for me decorator is wrong idea. updatable
should be created in Foo.__init__
.
At this moment code runs for me without errors if I use
return wrapped_fget
instead of return wrapped_fget(fget)
and it returns
return Updatable(fget, getattr(self, fupdate)).getter(lambda:fget(self))
and later run it as
print( xx.x().fget() + 1 )
But all this create new Updatable
every time when x()
is created and this may not be good idea because it resets x
to 0
.
Upvotes: 0