DEV Community

loading...

Do your own Python's @property

mzsrtgzr2 profile image Moshe Roth ・2 min read

After reading this chapter in "Expert Python Programming" -
Advanced Attrbiute Access patterns - https://subscription.packtpub.com/book/application_development/9781789808896/4/ch04lvl1sec39/advanced-attribute-access-patterns
decided to implement @property partially.
Here is the final result - https://gist.github.com/mzsrtgzr2/46975900b8d7a8e6f8cc51d1a40ca940

class myproperty:
    def __init__(self, func):
        self.f = func

    def setter(self, func):
        self.setter_func = func
        return self # (1) why do we need this? read below on 

    def __get__(self, instance, klass):
        return self.f(instance or klass)

    def __set__(self, instance, value):
        self.setter_func(instance, value)


class Foo:
    def __init__(self):
        self._val=0

    @myproperty
    def temperature(self):
        return self._val

    @temperature.setter
    def temperature(self, value):  # (2) why need same name as getter?
        print('setting to', value)
        self._val = value

ins = Foo()

print(int.temperature)  # 0
ins.temperature = 6
print(int.temperature)  # 6

It's important to understand how decorators work. This is the key to understand this code. The decorated function is REPLACED with an instance of your decorator (or a function in case you use a decorator function and not class). That being said, you can add functionality to your decorator class like __get__ and __set__, like in the code above.
__get__ - doing a "read" operation on the decorator class.
__set__ - doing a "write" operation on the decorator class.

This is another way to look at decorators - it can be used for "data descriptors" (when you access a field, as in ins.temperature) and not just as function wrappers (implemented with __call__, as in ins.temperature()).

Important - why is the decorated set method should also be named temperature?

A decorator works like this:

@dec 
def func1:
   pass

actually converts to

func1 = dec(func1)

so the name of the function is important.

What happens if we don't use the same name in (2) or drop the return self statement at the end of our decorator: what used to be temperature in our class is overridden to None - losing all the functionality we thought we built. We really happens:

class Foo:
    def __init__(self):
        self._val=0

    def temperature(self):
        return self._val
    temperature = myproperty(temperature)  # instantiating myproperty class

    def temperature(self, value):  # (2) why need same name as getter?
        print('setting to', value)
        self._val = value

    temperature = temperature.setter(temperature)

In this example, the property field temperature is always the reference name and can't change it.

Discussion

pic
Editor guide
Collapse
byrro profile image
Renato Byrro

Nice explanation! This is indeed an important feature to understand. I believe "replaced" might not be the best description here, though. Perhaps "wrapped with" would be a better descriptor of what's going on?

Collapse
mzsrtgzr2 profile image
Moshe Roth Author

thanks mate!
regarding the "replaced"... it looks like "wrapping" indeed but it's important to understand that the value of this class attribute that used to be what you wrote as 'temperature' is replaced with the value of what's returned from the decorator. this is why i used "replaced". i think the word "wrapping" is not strict enough.