Understanding Properties and their Decorator implementation

I’m hoping to get a clearer understanding of properties and why they can be implemented as decorators, but the naming conventions of examples are making them more confusing than necessary. I attempt removing unnecessary information in my own notebook but hope to see how someone else would teach this. Disambiguating the variable names is top priority for me.

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)

Above is an example from https://www.programiz.com/python-programming/property. What is confusing is the variable temperature appears 3x and i’m sure they are different things under the hood. First, __init__, it takes as an input temperature. My own tests show i can totally delete this input and hardcode a constant and still have property work.
Secondly, there is self.temperature, then i realized i can totally delete __init__ and the whole thing still works. So why did the author add these 2 temperature? Is init self.temperature here important for something later?
Next there is also _temperature. Can i understand that self.temperature is a higher level abstraction of _temperature? So _temperature is used by python interpreter, while temperature is called by user?

Moving on to the decarator implementation:

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

Now, in __init__, the previous self.temperature is now self._temperature. Again this whole __init__ can be deleted and still have property concept work.
I don’t understand how temperature = property(get_temperature,set_temperature) can be written as @property because i thought decorators do func = decorator(func) (the pattern being it must be assigned back to same name it decorated, which is not the case for property taking 2 inputs and assigning to 1). Also, what is the importance of @temperature.setter being named exactly that? Why not @property.setter. Is there some dependence (like 1 must be defined above in the script/ run before in real time) between @property and @temperature.setter?

HI Hanqi!

The __init__ is a constructor method for your class. As it is not required, you can indeed delete it. The parameter temperature could be named anything you want, or completely omitted. The _init_ method is simply a convenient way to give your object a set of attributes when you instantiate it.

temperature is the parameter that you pass to your object constructor when you create a new Celsius object (or if you don’t, it defaults to 0). What then happens within the __init__ method (which is simply a method that gets called anytime you create a new instance of a class) is that your Celsius object’s temperature attribute is set to the value of this parameter. As you observed, this is optional as this value could be hardcoded or omitted altogether. Again, note that both the name of the parameter and the name of the attribute are completely arbitrary. This does exactly the same job:

Class Celsius:
    def __init__(self, some_parameter = 0):
       self.some_attribute_name = some_parameter

Now what’s with the self._temperature thing?

Some programming languages (of which Java is a prime example) strictly control access to instance attributes. Essentially, what this would mean here is that the user would have no way to directly access or modify the temperature attribute - he, ore she, would have to do this using the getter and setter methods. What this achieves is one of the key concepts of OOP, encapsulation, by hiding the details of the implementation of a method from the user.

User here should be taken to mean the programmer using the class - not the end user of a software or web application. So for instance, when you use any Python built-in method such as str.join(), all you need to know is how to use it and what it does - not how it’s implemented internally. The internal implementation could very well change multiple times as the language evolves but the user would not be impacted as the method’s user interface would stay the same.

Anyhow - Python does not enforce any access control so if you want to convey in your code that an attribute should not be accessed directly, then beginning an attribute name with an underscore is the way to go. To the python interpreter, the underscore means nothing - it’s simply a naming convention just like starting class names with a capital letter. In your example, the user could still access the temperature attribute:

>>> room_temp = Celsius(20)
>>> room_temp._temperature
20

As an aside, this is an interesting article on the use and meaning of underscores in Python.

I’m not quite sure if everything makes sense so far so I’m going to stop here for now. :upside_down_face:

Thanks Slavina for the detailed effort!
I understand completely what you have just said, but i’m looking for a better explanation of what the author was trying to teach when he included __init__. Is there some concept of properties that cannot be fully explained unless __init__ is used there?
Also do you have any idea about the questions in the last paragraph?

I believe the author included the __init__ method because for most if not all classes, it is standard to include a constructor method. It’s just much more convenient to set all your attributes at the time of creation of a new object, rather than creating it and then having to set all attributes using separate setter methods. Also hardcoding variables whose values are likely to change doesn’t make much sense.

Also note that the author is most likely just using getter and setter methods as a way to build up to the second part of the article - the use of @property and @temperature.setter which is the “preferred way” of getting/setting attributes in Python.

I’ll try to explain this better a bit later - need to get back to work :upside_down_face:

@hanqi, the questions you ask in your original post make me think that you are trying to address the problem from the bottom up, that is, focusing on the details of the author’s example (not a very good one if you ask me - it had to be said :)) instead of trying to understand the bigger picture first - a bit like someone trying to understand how a whole forest ecosystem works by wandering around looking at the vegetation with a magnifying lens.

The essence of the @property decorator is this: it lets you access methods of a class instance as if they were attributes, without cluttering your classes with getters/setters/deleters, and without using the () that come with method calls.

Let’s take a look at another example that makes this easier to understand.

class Student:
    def __init__(self, first, last):
        self.first = first
        self.last = last
        self.email = first + '.' + last + '@dataquest.com'

>>> student_1 = Student('Jane', 'Doe')
>>> student_1.email
[email protected]

# Jane Doe just got married and we have to change her last name
>>> student_1.last = 'Smith' 
>>> student_1.email
[email protected] # Oops! This doesn't update automatically! 

The email doesn’t update automatically because the __init__ method only runs once: at object creation. We could of course just update the email manually but that would mean 1) unnecessary extra work for us, and 2) it would be error prone as we might spell the email wrong without realizing.

Okay then, let’s just delete the email attribute altogether and create a method instead to get around this problem

class Student:
    def __init__(self, first, last):
        self.first = first
        self.last = last

    def email(self):
        return f'{self.first}.{self.last}@dataquest.com'

>>> student_1 = Student('Jane', 'Doe')
>>> student_1.email
[email protected]

>>> student_1.last = 'Smith' 
>>> student_1.email()
[email protected]

Great! Now we can update the first and last names all we want, and the email address will update automatically. However, note that we now have to include the () because email is no longer an attribute but a method call. If other programmers are using our class in their own code, we just broke the code for everyone!

Ideally, we would want to keep accessing students’ emails by typing student.email, even though we have fundamentally changed the implementation. This is where the built-in @property decorator comes in and does its magic!

class Student:
    def __init__(self, first, last):
        self.first = first
        self.last = last
    
    @property
    def email(self):
        return f'{self.first}.{self.last}@dataquest.com'

>>> student_1 = Student('Jane', 'Doe')
>>> student_1.last = 'Smith' 
>>> student_1.email
[email protected]

Pheww! Other developers using our class are no longer angry and cursing at us over the phone. Remember when I said earlier that “the @property decorator lets you access methods of a class instance as if they were attributes?” Well, this is it.

To be continued…

Let me know if this is helpful to you.

Thanks! I do understand perfectly the big picture, and why people use it, and it’s benefits. I was trying to reconcile the syntax of this decorator (property) to my past knowledge of other decorators, and in so doing hoping to expand my knowledge of how decorators work/ how to design them.

My current understanding of decorators is they have 2 syntax for use.
One is: func = do_twice(func), another is

@do_twice
def func():

But this does not match what i am seeing with @property. The author says

@property
    def temperature(self):

is equivalent to temperature = property(get_temperature,set_temperature), so this doesn’t fit my current understanding of how this is equivalent.

For example. I can break down the previous decorator into a python function i can understand below.

def do_twice(func):
    def wrapper_do_twice(*args, **kwargs):
        func(*args, **kwargs)
        func(*args, **kwargs)
    return wrapper_do_twice

But what is the equivalent of the above expansion for temperature = property(get_temperature,set_temperature)? This doesn’t follow the take in func variable(possibly with args), return same func variable pattern that i currently understand decorators to be

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)

The built-in property() function (or actually class) takes 4 arguments, all optional:

class property( fget=None , fset=None , fdel=None , doc=None )

In the temperature example above, only the getter and setter have been defined, the two remaining optional args are set to None.

The @property decorator decorates multiple functions at once, if you will.

I found the Python documentation on the topic quite helpful, maybe you will too!

    @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

This is, as the author points out, the “syntactic sugar” equivalent of

temperature = property(get_temperature, set_temperature)

You could also define a @temperature.deleter if you needed one.

You have to name your decorators in the way used in the example:

  • @property decorates your getter method. You can name this method however you want.
  • @your_getter_method_name.setter decorates your setter method.
  • @your_getter_method_name.deleter decorates your deleter method.

Why?

Because your getter method, when modified by the @property decorator, becomes a special property object. The .setter and .deleter are simply methods of this property object.