user26109373
user26109373

Reputation: 11

Decorator with argument trying to access the self variable doesn't seem to extract self, but self.x instead

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

Answers (2)

Serhii Fomenko
Serhii Fomenko

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
  1. You call the wrapped_fget function by passing the arguments fget, fget = foo.x. So in the wrapped_fget function, self = foo.x.
  2. 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:

  1. attr_name - the name of the attribute variable used as getter. This is necessary to be able to use setter and deleter.
  2. callback - function that will be called when one of listen_methods is called.
  3. 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

furas
furas

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

Related Questions