How does the @property decorator work in Python?

后端 未结 13 2108
闹比i
闹比i 2020-11-21 04:49

I would like to understand how the built-in function property works. What confuses me is that property can also be used as a decorator, but it only

相关标签:
13条回答
  • 2020-11-21 05:39

    property is a class behind @property decorator.

    You can always check this:

    print(property) #<class 'property'>
    

    I rewrote the example from help(property) to show that the @property syntax

    class C:
        def __init__(self):
            self._x=None
    
        @property 
        def x(self):
            return self._x
    
        @x.setter 
        def x(self, value):
            self._x = value
    
        @x.deleter
        def x(self):
            del self._x
    
    c = C()
    c.x="a"
    print(c.x)
    

    is functionally identical to property() syntax:

    class C:
        def __init__(self):
            self._x=None
    
        def g(self):
            return self._x
    
        def s(self, v):
            self._x = v
    
        def d(self):
            del self._x
    
        prop = property(g,s,d)
    
    c = C()
    c.x="a"
    print(c.x)
    

    There is no difference how we use the property as you can see.

    To answer the question @property decorator is implemented via property class.


    So, the question is to explain the property class a bit. This line:

    prop = property(g,s,d)
    

    Was the initialization. We can rewrite it like this:

    prop = property(fget=g,fset=s,fdel=d)
    

    The meaning of fget, fset and fdel:

     |    fget
     |      function to be used for getting an attribute value
     |    fset
     |      function to be used for setting an attribute value
     |    fdel
     |      function to be used for del'ing an attribute
     |    doc
     |      docstring
    

    The next image shows the triplets we have, from the class property:

    __get__, __set__, and __delete__ are there to be overridden. This is the implementation of the descriptor pattern in Python.

    In general, a descriptor is an object attribute with “binding behavior”, one whose attribute access has been overridden by methods in the descriptor protocol.

    We can also use property setter, getter and deleter methods to bind the function to property. Check the next example. The method s2 of the class C will set the property doubled.

    class C:
        def __init__(self):
            self._x=None
    
        def g(self):
            return self._x
    
        def s(self, x):
            self._x = x
    
        def d(self):
            del self._x
    
        def s2(self,x):
            self._x=x+x
    
    
        x=property(g)
        x=x.setter(s)
        x=x.deleter(d)      
    
    
    c = C()
    c.x="a"
    print(c.x) # outputs "a"
    
    C.x=property(C.g, C.s2)
    C.x=C.x.deleter(C.d)
    c2 = C()
    c2.x="a"
    print(c2.x) # outputs "aa"
    
    0 讨论(0)
  • Here is another example:

    ##
    ## Python Properties Example
    ##
    class GetterSetterExample( object ):
        ## Set the default value for x ( we reference it using self.x, set a value using self.x = value )
        __x = None
    
    
    ##
    ## On Class Initialization - do something... if we want..
    ##
    def __init__( self ):
        ## Set a value to __x through the getter / setter... Since __x is defined above, this doesn't need to be set...
        self.x = 1234
    
        return None
    
    
    ##
    ## Define x as a property, ie a getter - All getters should have a default value arg, so I added it - it will not be passed in when setting a value, so you need to set the default here so it will be used..
    ##
    @property
    def x( self, _default = None ):
        ## I added an optional default value argument as all getters should have this - set it to the default value you want to return...
        _value = ( self.__x, _default )[ self.__x == None ]
    
        ## Debugging - so you can see the order the calls are made...
        print( '[ Test Class ] Get x = ' + str( _value ) )
    
        ## Return the value - we are a getter afterall...
        return _value
    
    
    ##
    ## Define the setter function for x...
    ##
    @x.setter
    def x( self, _value = None ):
        ## Debugging - so you can see the order the calls are made...
        print( '[ Test Class ] Set x = ' + str( _value ) )
    
        ## This is to show the setter function works.... If the value is above 0, set it to a negative value... otherwise keep it as is ( 0 is the only non-negative number, it can't be negative or positive anyway )
        if ( _value > 0 ):
            self.__x = -_value
        else:
            self.__x = _value
    
    
    ##
    ## Define the deleter function for x...
    ##
    @x.deleter
    def x( self ):
        ## Unload the assignment / data for x
        if ( self.__x != None ):
            del self.__x
    
    
    ##
    ## To String / Output Function for the class - this will show the property value for each property we add...
    ##
    def __str__( self ):
        ## Output the x property data...
        print( '[ x ] ' + str( self.x ) )
    
    
        ## Return a new line - technically we should return a string so it can be printed where we want it, instead of printed early if _data = str( C( ) ) is used....
        return '\n'
    
    ##
    ##
    ##
    _test = GetterSetterExample( )
    print( _test )
    
    ## For some reason the deleter isn't being called...
    del _test.x
    

    Basically, the same as the C( object ) example except I'm using x instead... I also don't initialize in __init - ... well.. I do, but it can be removed because __x is defined as part of the class....

    The output is:

    [ Test Class ] Set x = 1234
    [ Test Class ] Get x = -1234
    [ x ] -1234
    

    and if I comment out the self.x = 1234 in init then the output is:

    [ Test Class ] Get x = None
    [ x ] None
    

    and if I set the _default = None to _default = 0 in the getter function ( as all getters should have a default value but it isn't passed in by the property values from what I've seen so you can define it here, and it actually isn't bad because you can define the default once and use it everywhere ) ie: def x( self, _default = 0 ):

    [ Test Class ] Get x = 0
    [ x ] 0
    

    Note: The getter logic is there just to have the value be manipulated by it to ensure it is manipulated by it - the same for the print statements...

    Note: I'm used to Lua and being able to dynamically create 10+ helpers when I call a single function and I made something similar for Python without using properties and it works to a degree, but, even though the functions are being created before being used, there are still issues at times with them being called prior to being created which is strange as it isn't coded that way... I prefer the flexibility of Lua meta-tables and the fact I can use actual setters / getters instead of essentially directly accessing a variable... I do like how quickly some things can be built with Python though - for instance gui programs. although one I am designing may not be possible without a lot of additional libraries - if I code it in AutoHotkey I can directly access the dll calls I need, and the same can be done in Java, C#, C++, and more - maybe I haven't found the right thing yet but for that project I may switch from Python..

    Note: The code output in this forum is broken - I had to add spaces to the first part of the code for it to work - when copy / pasting ensure you convert all spaces to tabs.... I use tabs for Python because in a file which is 10,000 lines the filesize can be 512KB to 1MB with spaces and 100 to 200KB with tabs which equates to a massive difference for file size, and reduction in processing time...

    Tabs can also be adjusted per user - so if you prefer 2 spaces width, 4, 8 or whatever you can do it meaning it is thoughtful for developers with eye-sight deficits.

    Note: All of the functions defined in the class aren't indented properly because of a bug in the forum software - ensure you indent it if you copy / paste

    0 讨论(0)
  • 2020-11-21 05:47

    Below is another example on how @property can help when one has to refactor code which is taken from here (I only summarize it below):

    Imagine you created a class Money like this:

    class Money:
        def __init__(self, dollars, cents):
            self.dollars = dollars
            self.cents = cents
    

    and an user creates a library depending on this class where he/she uses e.g.

    money = Money(27, 12)
    
    print("I have {} dollar and {} cents.".format(money.dollars, money.cents))
    # prints I have 27 dollar and 12 cents.
    

    Now let's suppose you decide to change your Money class and get rid of the dollars and cents attributes but instead decide to only track the total amount of cents:

    class Money:
        def __init__(self, dollars, cents):
            self.total_cents = dollars * 100 + cents
    

    If the above mentioned user now tries to run his/her library as before

    money = Money(27, 12)
    
    print("I have {} dollar and {} cents.".format(money.dollars, money.cents))
    

    it will result in an error

    AttributeError: 'Money' object has no attribute 'dollars'

    That means that now everyone who relies on your original Money class would have to change all lines of code where dollars and cents are used which can be very painful... So, how could this be avoided? By using @property!

    That is how:

    class Money:
        def __init__(self, dollars, cents):
            self.total_cents = dollars * 100 + cents
    
        # Getter and setter for dollars...
        @property
        def dollars(self):
            return self.total_cents // 100
    
        @dollars.setter
        def dollars(self, new_dollars):
            self.total_cents = 100 * new_dollars + self.cents
    
        # And the getter and setter for cents.
        @property
        def cents(self):
            return self.total_cents % 100
    
        @cents.setter
        def cents(self, new_cents):
            self.total_cents = 100 * self.dollars + new_cents
    

    when we now call from our library

    money = Money(27, 12)
    
    print("I have {} dollar and {} cents.".format(money.dollars, money.cents))
    # prints I have 27 dollar and 12 cents.
    

    it will work as expected and we did not have to change a single line of code in our library! In fact, we would not even have to know that the library we depend on changed.

    Also the setter works fine:

    money.dollars += 2
    print("I have {} dollar and {} cents.".format(money.dollars, money.cents))
    # prints I have 29 dollar and 12 cents.
    
    money.cents += 10
    print("I have {} dollar and {} cents.".format(money.dollars, money.cents))
    # prints I have 29 dollar and 22 cents.
    

    You can use @property also in abstract classes; I give a minimal example here.

    0 讨论(0)
  • 2020-11-21 05:47

    This point is been cleared by many people up there but here is a direct point which I was searching. This is what I feel is important to start with the @property decorator. eg:-

    class UtilityMixin():
        @property
        def get_config(self):
            return "This is property"
    

    The calling of function "get_config()" will work like this.

    util = UtilityMixin()
    print(util.get_config)
    

    If you notice I have not used "()" brackets for calling the function. This is the basic thing which I was searching for the @property decorator. So that you can use your function just like a variable.

    0 讨论(0)
  • 2020-11-21 05:48

    This following:

    class C(object):
        def __init__(self):
            self._x = None
    
        @property
        def x(self):
            """I'm the 'x' property."""
            return self._x
    
        @x.setter
        def x(self, value):
            self._x = value
    
        @x.deleter
        def x(self):
            del self._x
    

    Is the same as:

    class C(object):
        def __init__(self):
            self._x = None
    
        def _x_get(self):
            return self._x
    
        def _x_set(self, value):
            self._x = value
    
        def _x_del(self):
            del self._x
    
        x = property(_x_get, _x_set, _x_del, 
                        "I'm the 'x' property.")
    

    Is the same as:

    class C(object):
        def __init__(self):
            self._x = None
    
        def _x_get(self):
            return self._x
    
        def _x_set(self, value):
            self._x = value
    
        def _x_del(self):
            del self._x
    
        x = property(_x_get, doc="I'm the 'x' property.")
        x = x.setter(_x_set)
        x = x.deleter(_x_del)
    

    Is the same as:

    class C(object):
        def __init__(self):
            self._x = None
    
        def _x_get(self):
            return self._x
        x = property(_x_get, doc="I'm the 'x' property.")
    
        def _x_set(self, value):
            self._x = value
        x = x.setter(_x_set)
    
        def _x_del(self):
            del self._x
        x = x.deleter(_x_del)
    

    Which is the same as :

    class C(object):
        def __init__(self):
            self._x = None
    
        @property
        def x(self):
            """I'm the 'x' property."""
            return self._x
    
        @x.setter
        def x(self, value):
            self._x = value
    
        @x.deleter
        def x(self):
            del self._x
    
    0 讨论(0)
  • 2020-11-21 05:48

    Let's start with Python decorators.

    A Python decorator is a function that helps to add some additional functionalities to an already defined function.

    In Python, everything is an object. Functions in Python are first-class objects which means that they can be referenced by a variable, added in the lists, passed as arguments to another function etc.

    Consider the following code snippet.

    def decorator_func(fun):
        def wrapper_func():
            print("Wrapper function started")
            fun()
            print("Given function decorated")
            # Wrapper function add something to the passed function and decorator 
            # returns the wrapper function
        return wrapper_func
    
    def say_bye():
        print("bye!!")
    
    say_bye = decorator_func(say_bye)
    say_bye()
    
    # Output:
    #  Wrapper function started
    #  bye
    #  Given function decorated
    

    Here, we can say that decorator function modified our say_hello function and added some extra lines of code in it.

    Python syntax for decorator

    def decorator_func(fun):
        def wrapper_func():
            print("Wrapper function started")
            fun()
            print("Given function decorated")
            # Wrapper function add something to the passed function and decorator 
            # returns the wrapper function
        return wrapper_func
    
    @decorator_func
    def say_bye():
        print("bye!!")
    
    say_bye()
    

    Let's Concluded everything than with a case scenario, but before that let's talk about some oops priniciples.

    Getters and setters are used in many object oriented programming languages to ensure the principle of data encapsulation(is seen as the bundling of data with the methods that operate on these data.)

    These methods are of course the getter for retrieving the data and the setter for changing the data.

    According to this principle, the attributes of a class are made private to hide and protect them from other code.

    Yup, @property is basically a pythonic way to use getters and setters.

    Python has a great concept called property which makes the life of an object-oriented programmer much simpler.

    Let us assume that you decide to make a class that could store the temperature in degree Celsius.

    class Celsius:
    def __init__(self, temperature = 0):
        self.set_temperature(temperature)
    
    def to_fahrenheit(self):
        return (self.get_temperature() * 1.8) + 32
    
    def get_temperature(self):
        return self._temperature
    
    def set_temperature(self, value):
        if value < -273:
            raise ValueError("Temperature below -273 is not possible")
        self._temperature = value
    

    Refactored Code, Here is how we could have achieved it with property.

    In Python, property() is a built-in function that creates and returns a property object.

    A property object has three methods, getter(), setter(), and delete().

    class Celsius:
    def __init__(self, temperature = 0):
        self.temperature = temperature
    
    def to_fahrenheit(self):
        return (self.temperature * 1.8) + 32
    
    def get_temperature(self):
        print("Getting value")
        return self.temperature
    
    def set_temperature(self, value):
        if value < -273:
            raise ValueError("Temperature below -273 is not possible")
        print("Setting value")
        self.temperature = value
    
    temperature = property(get_temperature,set_temperature)
    

    Here,

    temperature = property(get_temperature,set_temperature)
    

    could have been broken down as,

    # make empty property
    temperature = property()
    # assign fget
    temperature = temperature.getter(get_temperature)
    # assign fset
    temperature = temperature.setter(set_temperature)
    

    Point To Note:

    • get_temperature remains a property instead of a method.

    Now you can access the value of temperature by writing.

    C = Celsius()
    C.temperature
    # instead of writing C.get_temperature()
    

    We can further go on and not define names get_temperature and set_temperature as they are unnecessary and pollute the class namespace.

    The pythonic way to deal with the above problem is to use @property.

    class Celsius:
        def __init__(self, temperature = 0):
            self.temperature = temperature
    
        def to_fahrenheit(self):
            return (self.temperature * 1.8) + 32
    
        @property
        def temperature(self):
            print("Getting value")
            return self.temperature
    
        @temperature.setter
        def temperature(self, value):
            if value < -273:
                raise ValueError("Temperature below -273 is not possible")
            print("Setting value")
            self.temperature = value
    

    Points to Note -

    1. A method which is used for getting a value is decorated with "@property".
    2. The method which has to function as the setter is decorated with "@temperature.setter", If the function had been called "x", we would have to decorate it with "@x.setter".
    3. We wrote "two" methods with the same name and a different number of parameters "def temperature(self)" and "def temperature(self,x)".

    As you can see, the code is definitely less elegant.

    Now,let's talk about one real-life practical scenerio.

    Let's say you have designed a class as follows:

    class OurClass:
    
        def __init__(self, a):
            self.x = a
    
    
    y = OurClass(10)
    print(y.x)
    

    Now, let's further assume that our class got popular among clients and they started using it in their programs, They did all kinds of assignments to the object.

    And One fateful day, a trusted client came to us and suggested that "x" has to be a value between 0 and 1000, this is really a horrible scenario!

    Due to properties it's easy: We create a property version of "x".

    class OurClass:
    
        def __init__(self,x):
            self.x = x
    
        @property
        def x(self):
            return self.__x
    
        @x.setter
        def x(self, x):
            if x < 0:
                self.__x = 0
            elif x > 1000:
                self.__x = 1000
            else:
                self.__x = x
    

    This is great, isn't it: You can start with the simplest implementation imaginable, and you are free to later migrate to a property version without having to change the interface! So properties are not just a replacement for getters and setter!

    You can check this Implementation here

    0 讨论(0)
提交回复
热议问题