admin管理员组

文章数量:1122832

I have a class that performs some useful job in my project

class Test:
    def __init__(self):
        self.__value = None

    def set_value(self, value):
        print(f"set_value(): value={value}")
        self.__value = value

    def get_value(self):
        print(f"get_value()")
        return self.__value

    value = property(get_value, set_value)

Among regular functions it has some getters/setters that is handy to group into properties for simpler client code. E.g.

t.value = 123

Now, in some parts of the application I need to do some extra actions (a print() call in my case) on every access to the Test object, on each method call. So I did this:

class Wrapper:
    def __init__(self, wrapped):
        self.wrapped = wrapped

    def __getattr__(self, attr):
        print(f"Wrapper::__getattr__(): {attr}")
        return getattr(self.wrapped, attr)

This is very similar to what decorators do, but this approach has 2 benefits:

  • Original class is untouched, and I can use it separately from the Wrapper
  • I can add as many new methods to the Test class, without having to add new decorators in Wrapper

Now the usage code:

t = Test()
w = Wrapper(t)

w.set_value(123)
print(f"Value: {w.get_value()}")

w.value = 234
print(f"Value: {w.value}")

Direct set/get calls work as expected. Unfortunately w.value syntax no longer work, and silently assign a value of 234 to the value name, instead of calling getters and setters.

How this issue can be fixed, so that Wrapper respects Test class properties?

I have a class that performs some useful job in my project

class Test:
    def __init__(self):
        self.__value = None

    def set_value(self, value):
        print(f"set_value(): value={value}")
        self.__value = value

    def get_value(self):
        print(f"get_value()")
        return self.__value

    value = property(get_value, set_value)

Among regular functions it has some getters/setters that is handy to group into properties for simpler client code. E.g.

t.value = 123

Now, in some parts of the application I need to do some extra actions (a print() call in my case) on every access to the Test object, on each method call. So I did this:

class Wrapper:
    def __init__(self, wrapped):
        self.wrapped = wrapped

    def __getattr__(self, attr):
        print(f"Wrapper::__getattr__(): {attr}")
        return getattr(self.wrapped, attr)

This is very similar to what decorators do, but this approach has 2 benefits:

  • Original class is untouched, and I can use it separately from the Wrapper
  • I can add as many new methods to the Test class, without having to add new decorators in Wrapper

Now the usage code:

t = Test()
w = Wrapper(t)

w.set_value(123)
print(f"Value: {w.get_value()}")

w.value = 234
print(f"Value: {w.value}")

Direct set/get calls work as expected. Unfortunately w.value syntax no longer work, and silently assign a value of 234 to the value name, instead of calling getters and setters.

How this issue can be fixed, so that Wrapper respects Test class properties?

Share Improve this question asked Nov 22, 2024 at 19:29 Oleksandr MasliuchenkoOleksandr Masliuchenko 8452 gold badges9 silver badges19 bronze badges 2
  • Does it work if you use @property and @value.setter decorators? – Barmar Commented Nov 22, 2024 at 19:39
  • 2 You need to override the __setattr__ magic function. Otherwise, when you set value on wrapper it just uses normal semantics to set the name on your wrapper and not your wrapped/test object. There will be some edge cases where the behaviour is not exactly reproduced. eg. Test.value = ... vs Wrapper.value = ... (ie class attributes). – Dunes Commented Nov 22, 2024 at 19:51
Add a comment  | 

2 Answers 2

Reset to default 3

You can define a __setattr__ method to manage assignments. Note that it has to treat the wrapped attribute separately to avoid infinite recursion in __init__.

class Test:
    def __init__(self):
        self.__value = None

    def set_value(self, value):
        print(f"set_value(): value={value}")
        self.__value = value

    def get_value(self):
        print(f"get_value()")
        return self.__value

value = property(get_value, set_value)


class Wrapper:
    def __init__(self, wrapped):
        self.wrapped = wrapped

    def __getattr__(self, attr):
        print(f"Wrapper::__getattr__(): {attr}")
        return getattr(self.wrapped, attr)
    
    def __setattr__(self, attr, value):
        if attr == 'wrapped':
            super().__setattr__('wrapped', value)
        else:
            print(f"Wrapper::__setattr__(): {attr}")
            setattr(self.wrapped, attr, value)
    

    
    
    t = Test()
    w = Wrapper(t)
    
    w.set_value(123)
    print(f"Value: {w.get_value()}")
    print()
    w.value = 234
    print(f"Value: {w.value}")

Output:

Wrapper::__getattr__(): set_value
set_value(): value=123
Wrapper::__getattr__(): get_value
get_value()
Value: 123

Wrapper::__setattr__(): value
set_value(): value=234
Wrapper::__getattr__(): value
get_value()
Value: 234

Properties are descriptors and descriptors are looked up in the class, not in an instance --> data model / descriptors.

Instance Binding

If binding to an object instance, a.x is transformed into the call: type(a).__dict__['x'].__get__(a, type(a)).

The easiest way to have Test descriptors (properties) in the Wrapper is to inherit them.

Further, there is a better place to monitor almost all access than __getattr__. It's the __getattribute__--> data model / attribute access

object.__getattribute__(self, name)

Called unconditionally to implement attribute accesses for instances of the class. If the class also defines __getattr__(), the latter will not be called unless __getattribute__() either calls it explicitly or raises an AttributeError.


Result:

...

class Wrapper(Test):
    def __getattribute__(self, attr):
        print(f"Wrapper::__getattribute__(): {attr}")
        return super().__getattribute__(attr)

w = Wrapper()

...

本文标签: decoratorHow can getattr() respect python class propertiesStack Overflow