Changing __setattr__ behaviour via decorator

Question:

How can apply a decorator to __setattr__ method? I’ve tried this way but print inside of wrapper doesn’t work. (I know we can change setattr via inheritance, but need to do it via decorator / other way). Thanks in advance.


from functools import wraps

class A():
    pass

x = A()
x.a = 2

def setattr_decorator(func):
    @wraps(func)
    def wrapper(self, name: str, value):
        print('called')
        object.__setattr__(self, 'custom_' + name, value)
    return wrapper

x.__setattr__ = setattr_decorator(x.__setattr__)
x.b = 2
x.custom_b # invokes AttributeError
Asked By: arseniybelkov

||

Answers:

Define the __setattr__ within the class & apply the decorator there. You also need to actually call the wrapped function within the wrapper:

from functools import wraps


def setattr_decorator(func):
    @wraps(func)
    def wrapper(self, name: str, value):
        print('called')
        func(self, name, value)
        object.__setattr__(self, 'custom_' + name, value)

    return wrapper


class A():
    @setattr_decorator
    def __setattr__(self, key, value):
        object.__setattr__(self, key, value) # or pass if you don't want to set the 'a' or 'b' attribute


x = A()
x.a = 1
x.b = 2
print(x.custom_a)
print(x.custom_b)

Result:

called
called
1
2

Edit:

In case you can’t change the class A itself, you can apply the decorator on the class method manually:

A.__setattr__ = setattr_decorator(A.__setattr__)

Which should work in the same way. Note: the decorator needs to be applied on the class, not the object.

Answered By: rdas

By turning the decorator into a function you can achieve the same goal and avoid redundant syntax.

from functools import wraps


def load_setattr(target_cls, prefix='custom_'):
    # modify inplace of the target class 
    def wrapper(self, name: str, value):
        print('called')
        object.__setattr__(self, prefix + name, value)
    
    target_cls.__setattr__ = wraps(target_cls.__setattr__)(wrapper)


class A:
    pass


load_setattr(A)

x = A()
x.a = 1
#called
x.b = 2
#called
print(x.custom_a)
#1
print(x.custom_b)
#2
Answered By: cards
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.