How do getters and setters work in Python?

Question:

I am learning getters and setters , what I understand is that they are used so that no one could change the object’s attributes directly. In the example

class Person:
    def __init__(self, name, age):
        self._name = name
        self._age = age

    def get_age(self):
        return self._age

    def set_age(self, new_age):
        if isinstance(new_age, int) & new_age>0 & new_age<120:
            self._age = new_age

    def get_name(self):
        return self._name
    
    def __str__(self):
        return 'Person[' + self._name + '] is ' + str(self._age)
    
p1 = Person("Sandeep", 49)

I created an object p1 where I set the age 49. As I have made a set_age function so I expect we can change the age of p1 through set_age only, not through the routine way. But it is not happening, I am able to change the age of p1 through , for example, p1._age = 35 as well. Then, what is the advantage to make set_age function, if I am still able to access the attributes directly?

I think, I am missing something, please help.

Asked By: prashant sharma

||

Answers:

what I understand is that they are used so that no one could change the object’s attributes directly.

Your understanding is wrong. There is no magic rule that tells the python interpreter oh, this class has a setter, so direct access to the fields aren't allowed any more.

In other words: this is merely "works by convention". When you see a python class that has setter methods, then you know you should not access the fields directly, although you still can do that.

And note: python is mostly rather lenient on such things. A lot of things are "by convention" only. It is just "the nature" of python: you can do a lot of things that other languages, especially statically typed ones like Java do not allow.

Answered By: GhostCat

You need to tell python how to associate the getter and setter with the actual variable name. To do this you can use the builtin property function like so:

class Person
    def __init__(self, name, age):
        self._name = name
        self._age = age

    def get_age(self):
        return self._age

    def set_age(self, new_age):
        if isinstance(new_age, int) & new_age>0 & new_age<120:
            self._age = new_age

    def get_name(self):
        return self._name
    name = property(get_name)
    age = property(get_age, set_age)

    def __str__(self):
        return 'Person[' + self.name + '] is ' + str(self.age)
    
p1 = Person("Sandeep", 49)

Then instead of referring to _name and _age use name and age

Answered By: Advay168

Now I realise that I asked a silly question. The reason is obvious of using setters, creating functions for setting a value is a very convenient way to apply the constraints on it. In the example above, the constraint is that the age of the person should be positive and less than 120. Implementation of such constraints is not possible without setters.

Answered By: prashant sharma

The reason to use a getter and setter, is if you want to do something more complex than just set and attribute with foo.bar. In your case, set_age has an

isinstance(new_age, int) & new_age>0 & new_age<120

check, which is not possible to do with a raw attribute. (Side-note: you should use and instead of &.)

Yes, someone can still do p1._age = -1, and then their code won’t work, but why would they? It just makes their code not work.

Your get_name function is less useful than the age one. It basically makes name read-only, which might or might not be useful.

When creating setters and getters in Python, it is usual to use the @property decorator. This means the functions can be called as if they were attributes, so instead of p1.get_name() you can just do p1.name. Similarly p1.set_age(3) becomes p1.age = 3.

You probably want to use the age setter in __init__, because then the age of the Person is validated when it is created.

Here is a version that makes these changes (and a couple of other readability improvements).

class Person:
    def __init__(self, name, age):
        self._name = name
        self.age = age

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, new_age):
        if isinstance(new_age, int) and 0 < new_age < 120:
            self._age = new_age
    
    @property
    def name(self):
        return self._name

    def __str__(self):
        return f"Person[{self.name}] is {self.age}"
    
p1 = Person("Sandeep", 49)
Answered By: LeopardShark

Generally speaking you do not code getters and setters in python unless you specifically need them, right now. (But what if you need them later?? I’ll explain that in a moment.)

Most of the time you should just define the instance variables, and then in the code that needs to use those variables, just access them directly, i.e.:

p1 = Person("Sandeep",49)
print("%s is %d years old." % (p1._name, p1._age))
# prints "Sandeep is 49 years old."
p1._age = 50
print("Now %s is %d years old." % (p1._name, p1._age))
# prints "Now Sandeep is 50 years old."

You only add getter/setter methods later, when you actually need them.

When you add the getter/setter methods later, you do not need to change any of the existing code that directly accesses the instance variables. Instead, use the @property decorator, shown in LeopardShark’s answer above (https://stackoverflow.com/a/71080050/1813403).

The @property decorator does some magic code generation that makes your new getter/setter methods intercept when the existing code tries to directly access the instance variables, and send the request through your getter/setter methods instead. So the existing code will keep working but will call your new getter/setter methods instead of just messing directly with the attribute.

Answered By: Steven J Owens
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.