While learning the basics of object oriented programming in Python, I came across something I cannot search effectively to find matching keywords:
I wrote validation checks for a basic class Course
, which has a protected attribute _level
. The getters and setters treat this as a protected attribute. The validation makes sure self._level
are between 0 and 100.
- But the validation checks in the setter are evaded:
class Course:
def __init__(self, level=None):
self._level = level if level else 0
@property
def level(self):
return self._level
@level.setter
def level(self, value):
if not isinstance(value, int):
raise TypeError('The value of level must be of type int.')
if value < 0:
self._level = 0
elif value > 100:
self._level = 100
else:
self._level = value
###
courses = [Course(), Course(10), Course(-10), Course(150)]
for c in courses:
print(c.level)
>>> 0
>>> 10
>>> -10
>>> 150
- I then change the initialization from a protected
self._level
to a public attributeself.level
. My setters and getters still refer to a protected attributeself._level
.
class Course:
def __init__(self, level=None):
self.level = level if level else 0
@property
def level(self):
return self._level
@level.setter
def level(self, value):
if not isinstance(value, int):
raise TypeError('The value of level must be of type int.')
if value < 0:
self._level = 0
elif value > 100:
self._level = 100
else:
self._level = value
courses = [Course(), Course(10), Course(-10), Course(150)]
for c in courses:
print(c.level)
>>> 0
>>> 10
>>> 0
>>> 100
I was surprised that not only does the class realize it is the same attribute, but now my validations are implemented!
How does Python reconcile self.level
with self._level
, and why must I initialize it as a public attribute in order for my validations to kick in?
CodePudding user response:
The issue is you aren't using your property
in the __init__
, so of course, the property setter is never called!
self._level = level if level else 0
Should be:
self.level = level if level else 0
Some advice, stop thinking in terms of "protected" vs "public". The Python runtime recognizes no such thing, there are no access modifiers in Python. These are merely naming conventions. I think this naming convention is confusing you. There is no reason why if you have
@property
def level(self):
...
Should modify a variable called _level
, again, this is merely a convention. You could call it whatever you want. So, consider the following:
class Course:
def __init__(self, level=None):
self.foo = level if level else 0
@property
def level(self):
return self.foo
@level.setter
def level(self, value):
if not isinstance(value, int):
raise TypeError('The value of level must be of type int.')
if value < 0:
self.foo = 0
elif value > 100:
self.foo = 100
else:
self.foo = value
This should hopefully make it more obvious why your __init__
never actually calls the setter -- because you don't use the property! The property is level
, not foo
. Of course, you would probably never use a naming scheme like the above because it would be confusing, but it should illuminate what is happening with your code.
Now, it should become obvious why if we change the __init__
to:
def __init__(self, level=None):
self.level = level if level else 0
The property is called. Because self.level
is the property, not self.foo
. The property setters/getters will only be called if you use the property, which is self.level
, not self.foo