Reputation: 71
The following is a snippet of code from CS50P.
I don't understand how it works and cannot seem to find an adequate explanation.
Firstly, as soon as the Student constructor is called with user inputted arguments name and home, the init method is subsequently called with the same arguments. Now, it is unclear to me what exactly happens in the following two lines:
self.name = name
self.house = house
Essentially, from what I understand, since "name" in self.name matches with name(self, name) under @name.setter, self.name = name calls name(self, name) with the value of name as the argument. No return value is given, but instead, a new instance variable, namely _name is created, which is assigned to the same value of name (if the error check is passed). I do not understand why it is necessary to create this new variable with the underscore at the beginning in place of name. Also, I would like a more "under the hood" explanation of what "self.name" really does because I think my understanding is quite limited and might even be incorrect.
In addition, I was introduced to the idea of getters and setters out of nowhere, apart from being given the explanation that they allow user data to be validated when attribute values are set, whether they be set in the init function or outside of the class altogether. However, what do they really mean "under the hood" and what is the significance of the setter having a reference to the instance variable, but not the getter? Where do the names "property" and "name.setter" come from, and what about the "@" at the beginning? It's my first time seeing this syntax, so it is quite confusing and ilogical to me.
class Student:
def __init__(self, name, house):
self.name = name
self.house = house
def __str__(self):
return f"{self.name} from {self.house}"
@property
def name(self):
return self._name
@name.setter
def name(self, name):
if not name:
raise ValueError("Invalid name")
self._name = name
@property
def house(self):
return self._house
@house.setter
def house(self, house):
if house not in ["Gryffindor", "Hufflepuff", "Ravenclaw", "Slytherin"]:
raise ValueError("Invalid house")
self._house = house
def main():
student = get_student()
print(student)
def get_student():
name = input("Name: ")
house = input("House: ")
return Student(name, house)
if __name__ == "__main__":
main()
Upvotes: 4
Views: 450
Reputation: 532003
The proposed duplicate, I think, provides much the same information as this answer. However, as you said you had trouble understanding it, perhaps part of your confusion stems from how the property
type takes advantage of decorator syntax, so I'll try to start with an equivalent definition that doesn't use decorators at all. (For brevity, I'll omit the house
property altogether to focus on just the name
property, and skip __init__
and __str__
, which don't change from the original definition.)
class Student:
...
def _name_getter(self):
return self._name
def _name_setter(self, name):
if not name:
raise ValueError("Invalid name")
self._name = name
name = property(_name_getter, _name_setter)
del _name_getter, _name_setter
Due to how the descriptor protocol works, accessing the class attribute name
via an instance invokes the property
's __get__
method, which calls the getter. Similarly, assignment to the class attribute via an instance invokes the property
's __set__
method, which calls the setter. (The linked HOWTO also provides a pure Python definition of the property
class so you can see exactly how the descriptor protocol applies here.)
In brief,
s.name
becomes Student.__dict__['name'].__get__(s, Student)
, which calls _name_getter_(s)
.s.name = "Bob"
becomes Student.__dict__['name'].__set__(s, "Bob")
, which calls _name_setter(s, "Bob")
.Not that _name_getter
and _name_setter
, though defined like instance methods, are never actually used as instance methods. That's why I delete both names from the class namespace before the class gets created. They are just two regular functions that the property
will call for us.
Now, we can make use of some helper methods defined by property
to shift back to the original decorator-based definition. property.setter
is an instance method that takes a setter function as its argument, and returns a new instance that uses all the functions of the original property, but replaces any existing setter with the new one. With that in mind, we can change our definition to
class Student:
...
def _name_getter(self):
return self._name
def _name_setter(self, name):
if not name:
raise ValueError("Invalid name")
self._name = name
# Define a read-only property, one with only a getter
name = property(_name_getter)
# Replace that with a new property that also has a setter
name = name.setter(_name_setter)
del _name_getter, _name_setter
Both properties are assigned to the same attribute, name
. If we used a different name for the second assignment, our class would have two different properties that operated on self._name
: one that only has a getter, and one that has a getter and a setter.
property
is applied to only one argument, so we can move back to using decorator syntax. Instead of defining _name_getter
first, then applying property
to it, we'll name the getter name
to begin with and decorate it with property
. (The name name
will thus immediately have its original function value replaced with a property
value that wraps the original function.)
class Student:
...
@property
def name(self):
return self._name
def _name_setter(self, name):
if not name:
raise ValueError("Invalid name")
self._name = name
name = name.setter(_name_setter)
del _name_setter
Likewise, we can replace the explicit call of name.setter
on the pre-defined _name_setter
function with a decoration of a setter also named name
. (Because of how decorators are actually implemented, the new function name
will be defined after name.setter
is evaluated to get the decorator itself, but before the decorator is called, so that we use the old property to define the new one before finally assigning the new property
to the name name
.)
class Student:
...
@property
def name(self):
return self._name
@name.setter
def name(self, name):
if not name:
raise ValueError("Invalid name")
self._name = name
Upvotes: 2