Search code examples
pythonpython-decoratorsclass-decorator

Python class decorator "self" seems wrong


I am trying to work out how I can change functionality of __setattr__ of a class using a decorator on the class, but I am running into issue when trying to access self inside the function that replaces __setattr__. If I change the problmatic line to not access self, e.g. replacing it with val = str(val), I get the expected behaviour.

I see similar problems in other questions here, but they use a different approach, where a class is used as a decorater. My approach below feels less complicated, so I'd love to keep it like that if possible.

Why might a not be defined on self/foo where I expect it to be?

# Define the function to be used as decorator
# The decorator function accepts the relevant fieldname as argument
# and returns the function that wraps the class
def field_proxied(field_name):

    # wrapped accepts the class (type) and modifies the functionality of
    # __setattr__ before returning the modified class (type)
    def wrapped(wrapped_class):
        super_setattr = wrapped_class.__setattr__

        # The new __setattr__ implementation makes sure that given an int,
        # the fieldname becomes a string of that int plus the int in the
        # `a` attribute
        def setattr(self, attrname, val):
            if attrname == field_name and isinstance(val, int):
                val = str(self.a + val)  # <-- Crash. No attribute `a`
                super_setattr(self, attrname, val)
        wrapped_class.__setattr__ = setattr
        return wrapped_class
    return wrapped

@field_proxied("b")
class Foo(object):
    def __init__(self):
        self.a = 2
        self.b = None

foo = Foo()
# <-- At this point, `foo` has no attribute `a`
foo.b = 4
assert foo.b == "6"  # Became a string

Solution

  • The problem is simple, you just need one line change.

    def setattr(self, attrname, val):
        if attrname == field_name and isinstance(val, int):
            val = str(self.a + val)
        super_setattr(self, attrname, val)  # changed line
    

    The reason is, in your original method, you will only call super_setattr when attrname == field_name. So self.a = 2 in __init__ doesn't work at all as "a" != "b".