3

Below is a class from this tutorial designed to demonstrate the functionality of the property in Python.

The class sets and gets temperature in Celsius and also converts to Fahrenheit:

class Celsius:
    def __init__(self, current_temp = 25):
        self.temperature = current_temp

    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 = 50

    temperature = property(get_temperature,set_temperature)

Question: Is it true that self.temperature (an instance variable) is actually referencing the class variable temperature? If so, then why?

Example:

obj = Celsius()
print(obj._temperature, obj.temperature)

Returns:

Setting value
Getting value
50 50

I just don't understand how an instance variable with an assigned value (current_temp) could be actually referencing a class variable. Sorry if I have something mistaken.

DSH
  • 1,038
  • 16
  • 27
  • Why does your setter unconditionally set `self._temperature` to `50`? – user2357112 Apr 01 '20 at 01:13
  • See [this](https://www.digitalocean.com/community/tutorials/understanding-class-and-instance-variables-in-python-3) really nice blog about instance and class variables in Python. That should help you understand. – Sunny Patel Apr 01 '20 at 01:14
  • @user2357112supportsMonica I wanted to demonstrate that `self.temperature` is set via the property and not via assignment during init. I just updated the example. Confused still... – DSH Apr 01 '20 at 01:16
  • "Is it true that self.temperature (an instance variable) is actually referencing the class variable temperature?" - referencing? No. The property object is involved, though. The "how" is the [descriptor protocol](https://docs.python.org/3/reference/datamodel.html#implementing-descriptors), but you don't have to dive into that to use properties. – user2357112 Apr 01 '20 at 01:19
  • It sounds like you've got a very specific idea about how attributes work, but attribute behavior is a lot more customizable than that. "Instance *variable*" isn't a great way to think about them - that implies a lot of things that aren't always true. – user2357112 Apr 01 '20 at 01:21
  • @user2357112supportsMonica "The property object is involved, though" - but how precisely? If you change the class var `temperature` to another name, then `self.temperature` is equal to 25 (because the property is not assigned). Does the class var `temperature` override the instance variable? – DSH Apr 01 '20 at 01:24
  • 1
    Python searches the class and all ancestors in method resolution order, looking for a descriptor for the attribute. It finds one (the property) and invokes the property's setter to handle the operation. – user2357112 Apr 01 '20 at 01:31
  • @user2357112supportsMonica "looking for a descriptor for the attribute. It finds one (the property)" - but *why* is the property (named `temperature`) a descriptor for `self.temperature`? I.e. if you assign the property a new name such as `NEW_NAME = property(get_temperature,set_temperature)`, then self.temperature is equal to 25 (and not 50). So Python must be assigning the descriptor to `self.temperature` based on name, yes? Thoughts? – DSH Apr 01 '20 at 01:40
  • 1
    "why is the property (named temperature) a descriptor for self.temperature?" *exactly because it has that name*. If you used `NEW_NAME` instead as the name, then you would be able to do things like `self.NEW_NAME = 30` and it would call `set_temperature`. – Karl Knechtel Apr 01 '20 at 01:43

1 Answers1

1

Is it true that self.temperature (an instance variable) is actually referencing the class variable temperature?

You mean attribute, not variable. But yes.

If so, then why?

Because instances of the class do not have a temperature attribute (they only have a _temperature attribute, and so after Python fails to find the attribute in the instance, it checks the class as well.

Perhaps you are being thrown off because the __init__ method assigns to self.temperature. However, this does not create a temperature attribute on the instance, because the property is found first.

There are complicated rules for how the lookup of attributes works; they're designed to make it possible to do what you want - for example, to make it possible for property to work.

I just don't understand how an instance variable with an assigned value (current_temp) could be actually referencing a class variable.

I don't see current_temp in your code, so I can't help with that.


Part of why the code is confusing is because it does things in a very non-standard way. Normally, property is used in its decorator form, which looks like this:

class Celsius:
    def __init__(self, current_temp = 25):
        # set the underlying value directly; don't use the property
        # although you *can*, but it's arguably clearer this way
        self._temperature = current_temp

    def to_fahrenheit(self):
        return (self.temperature * 1.8) + 32

    # the getter is given the name that the property should have.
    @property
    def temperature(self):
        print("Getting value")
        return self._temperature

    # The setter is "attached" to the getter with this syntax
    @temperature.setter
    # It doesn't matter that this has the same name, because of how the
    # internal attachment logic works.
    def temperature(self, value):
        if value < -273:
            raise ValueError("Temperature below -273 is not possible")
        print("Setting value")
        self._temperature = value # good idea to actually use the value!

Bonus: arguably, a better design is to have separate celcius and fahrenheit properties, and the ability to create an instance from either a celcius or fahrenheit value (use staticmethod to create factory methods). That looks like:

class Temperature:
    # Secret implementation detail: we store the value in celcius.
    # We could have chosen fahrenheit instead, and do the conversions
    # the other way around. Users should not call this directly.
    def __init__(self, c):
        # Actually, I lied. One good reason to use the property in
        # the init method is to enforce bounds checking.
        self.celcius = c

    # We offer factory methods that let the user be explicit about
    # how the initial value is provided.
    @staticmethod
    def in_celcius(c):
        return Temperature(c)

    @staticmethod
    def in_fahrenheit(f):
        return Temperature((f - 32) * 5/9)

    # Now we define properties that let us get and set the value
    # as either a celcius value or a fahrenheit one.
    @property
    def celcius(self):
        return self._celcius

    @celcius.setter
    def celcius(self, c):
        if c < -273:
            raise ValueError("below absolute zero")
        self._celcius = c

    @property
    def fahrenheit(self):
        return (self._celcius * 9/5) + 32

    @fahrenheit.setter
    def fahrenheit(self, f):
        # use the other property to reuse the bounds-checking logic.
        self.celcius = (f - 32) * 5/9
Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153
  • good answer! Three follow ups: 1) what's the difference between an instance *variable* and instance *attribute* (or class variable and class attribute)? 2) `current_temp` was a parameter to `init`. 3) "However, this does not create a temperature attribute on the instance, because the property is found first". Why is property found before `self.temperature` is assigned to `current_temp`? – DSH Apr 01 '20 at 01:49
  • 1
    1. There are no "instance variables". In fact, `variable` is not really the best terminology for Python code in the first place; we have *names*, and attributes are a kind of name. For more details, try for example https://nedbatchelder.com/text/names1.html . 2. Oops, glossed over that. Anyway, I hope the idea is clear. 3. See the link about lookup rules. – Karl Knechtel Apr 01 '20 at 01:55