Why would someone use @property if no setter or deleter are defined?
Asked Answered
M

5

11

In python code I often see the use of @property.

If I understand correctly, with the property function a getter setter and deleter can be defined.

Why would one use @property if the setter and deleter are not defined (@x.setter, @x.deleter)? Isn't this the same as not using @property at all?

Metastasis answered 8/6, 2011 at 6:46 Comment(0)
D
10

It creates an API that does not allow a value to be set. This is similar in other languages to a constant.

Demi answered 8/6, 2011 at 6:47 Comment(4)
A constant? The getter is a function that can give a different result every time it's invoked. Or does the property function only make it evaluate once in the lifetime of the object, that does not make sense. I do however understand now what the advantage is, but I don't think it's similar to a constant.Metastasis
There are absolutely differences, but there is also some overlap, I am using the literal meaning of similar here.Demi
I assume this is only if you prefix the variable with double underscore, otherwise you can still just set it with or without @property. I also got here as I wondered if there is any point to using @property on something non-constant that should not have a setter.Inconsequent
To add on to the discussion, this concept has been given the name "computed property" in some frameworks and programming languages (Swift is a prime example).Telescopic
C
4

Defining a property with a getter function but without a setter can be very useful in certain scenarios. Lets say you have a model as below in django; a model is essentially a database table with entries called fields. The property hostname is computed from one or more fields in the model from the database. This circumvents needing another entry in the database table that has to be changed everytime the relevant fields are changed.

The true benefit of using a property is calling object.hostname() vs. object.hostname. The latter is passed along with the object automatically so when we go to a place like a jinja template we can call object.hostname but calling object.hostname() will raise an error.

The example below is a virtualmachine model with a name field and an example of the jinja code where we passed a virtualmachine object.

# PYTHON CODE
class VirtualMachine(models.Model):
    name = models.CharField(max_length=128, unique=True)

    @property
    def hostname(self):
        return "{}-{}.{}".format(
            gethostname().split('.')[0],
            self.name,
            settings.EFFICIENT_DOMAIN
        )

# JINJA CODE
...start HTML...
Name: {{ object.name }}

# fails
Hostname: {{ object.hostname() }}

# passes
Hostname: {{ object.hostname }}
...end HTML...
Ceria answered 27/6, 2018 at 17:57 Comment(1)
This is a good answer. You can also modify the value of your property based on other kwargs. If you create an self._hostname and need to modify the hostname based on other kwargs - you can easily do this in your method declaration -- ``` @property def scheme(self): if not self.certificate: return f'{self._scheme}' return f'{self._scheme}+{self.certificate}'```Navicular
T
2

TL;DR

So if you have heavy logic in the @property function, be aware that it will be running the entire logic each time you access the property. In this case I would suggest using a getter with a setter

Verbose

Another aspect which I don't feel has been explored is that the @property which is a getter, could be and most likely will be called multiple times where as the setter will most likely be called once when you instantiate the object.

IMO, this model should be used if the @property function is not doing too much heavy lifting. In the example below, we are just concatenating some strings to generate an email address.

class User:
    DOMAIN = "boulder.com"

    def __init__(self, first_name: str, last_name: str) -> None:
        self.first_name = first_name
        self.last_name = last_name

    @property
    def email(self) -> str:
        return "{}_{}@{}".format(self.first_name, self.last_name, self.DOMAIN)

But if you are going to add some extended or heavy logic to the function, then I would recommend creating a getter for it so that it is only run once. For example, lets say we need to check whether the email is unique, this logic would be better served in a getter other wise you will run the logic to check for uniqueness of the email each time you want to access it.

class User:
    DOMAIN = "boulder.com"

    def __init__(self, first_name: str, last_name: str) -> None:
        self.first_name = first_name
        self.last_name = last_name

    @property
    def email(self) -> str:
        return self._email

    @email.setter
    def email(self) -> None:
        proposed_email = "{}_{}@{}".format(self.first_name, self.last_name, self.DOMAIN)

        if is_unique_email(proposed_email):
            self._email = proposed_email
        else:
            random_suffix = get_random_suffix()
            self._email = "{}_{}_{}@{}".format(
                self.first_name, self.last_name, random_suffix, self.DOMAIN
            )
Tropho answered 5/10, 2022 at 10:37 Comment(1)
For the second example, I think you mixed up "getter" and "setter" in your text description. You want the logic in the setter (runs once), as the code example shows. Cannot propose an edit because "edits must be at least 6 characters" (facepalm).Deer
N
0

This is a good answer. Additionally, you can also modify the value of your property based on other kwargs and do this within the same method declaration. If you create a self._hostname instance variable, you can also modify the value based on other kwargs or variables. You can also obtain the value from your property and use it within other methods as self.scheme (see below) is syntactically pleasing and simple :).


class Neo4j(Database):
    def __init__(self, label, env, username, password, hostname, port=None, routing_context=False, policy=None, scheme=None, certificate=None):
        super().__init__(label, env)
        
        self.username = username
        self._password = password
        self.hostname = hostname
        self.port = port # defaults, 7687
        self._scheme = scheme # example - neo4j, bolt
        self.routing_context = routing_context # self.policy = policy policy=None,
        self.policy = policy # Examples, europe, america
        self.certificate = certificate # examples, None, +s, +ssc

    @property
    def scheme(self):
        if not self.certificate:
            return f'{self._scheme}'
        return f'{self._scheme}+{self.certificate}'

    def __repr__(self) -> str:
        return f'<{self.scheme}://{self.hostname}:{self.port}>' #if self.ro


db = Neo4j(label='test', env='dec', username='jordan', password='pass', hostname='localhost', port=7698, scheme='neo4j', certificate='ssc')

print(db.scheme) >>> neo4j+ssc
Navicular answered 6/11, 2021 at 7:27 Comment(0)
L
0

You can think of properties as supplying virtual values. One use of a getter is to return a calculated value.

For example:

class Circle:
    def __init__(self, radius):
        self.radius = radius

    @property
    def area(self):
        return math.pi * self.radius ** 2

    @property
    def circumference(self):
        return math.pi * self.radius * 2
    
test = Circle(10)

print(test.circumference)   #   62.83185307179586
print(test.area)            #   314.1592653589793

Here you have two properties, area and circumference which can be accessed like calculated attributes. It would be a mistake to make them real attributes since you’re effectively duplicating data.

Of course, if you also add a setter, you can use this to reverse engineer the original value:

class Circle:
    def __init__(self, radius):
        self.radius = radius
    @property
    def area(self):
        return math.pi * self.radius ** 2
    @area.setter
    def area(self, value):
        self.radius = math.sqrt(value / math.pi)
    @property
    def circumference(self):
        return math.pi * self.radius * 2
    @circumference.setter
    def circumference(self,value):
        self.radius = value / math.pi / 2

        
test = Circle(10)

test.area = 1240
print(test.radius)          #   19.867165345562018
test.circumference = 60
print(test.radius)          #   9.549296585513721
Langer answered 12/6 at 8:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.