I am new to Python. So, please forgive me if this is a basic question. I researched this topic on the Internet and SO, but I couldn't find an explanation. I am using Anaconda 3.6 distribution.
I am trying to create a simple getter and setter for an attribute. I will walk you through the errors I get.
class Person:
def __init__(self,name):
self.name=name
bob = Person('Bob Smith')
print(bob.name)
This prints the first name I agree that I haven't overridden print
or getattribute
method. Also, there is no property here. This was to test whether the basic code works.
Let's modify the code to add property:
class Person:
def __init__(self,name):
self.name=name
@property
def name(self):
"name property docs"
print('fetch...')
return self.name
bob = Person('Bob Smith')
print(bob.name)
As soon as I write above code in PyCharm, I get a yellow bulb icon, stating that the variable must be private. I don't understand the rationale.
Ignoring above, if I run above code, I get:
Traceback (most recent call last): File "C:\..., in run_code exec(code_obj, self.user_global_ns, self.user_ns) File "<ipython-input-25-62e9a426d2a9>", line 2, in <module> bob = Person('Bob Smith') File "<ipython-input-24-6c55f4b7326f>", line 4, in __init__ self.name=name AttributeError: can't set attribute
Now, I researched this topic, and I found that there are two fixes (without knowing why this works):
Fix #1: Change the variable name
to _name
class Person:
def __init__(self,name):
self._name=name #Changed name to _name
@property
def name(self):
"name property docs"
print('fetch...')
return self._name #Changed name to _name
bob = Person('Bob Smith')
print(bob.name)
This works well in that it prints the output correctly.
Fix #2: Change property name to from name(self)
to _name(self)
and revert variable name from _name
to name
class Person:
def __init__(self,name):
self.name=name #changed to name
@property
def _name(self): #Changed to _name
"name property docs"
print('fetch...')
return self.name #changed to name
bob = Person('Bob Smith')
print(bob.name)
Now, this works prints as expected.
As a next step, I created setter
, getter
, and deleter
properties using decorators. They follow similar naming conventions as described above--i.e. either prefix _
to the variable name or the method name:
@_name.setter
def _name(self,value):
"name property setter"
print('change...')
self.name=value
@_name.deleter
def _name(self):
print('remove')
del self.name
bob = Person('Bob Smith')
print(bob.name)
bob.name = 'Bobby Smith'
print(bob.name)
del bob.name
Question: I am not really sure why Python 3.x is enforcing adding _
to variable name or method name.
As per Python property with public getter and private setter, What is the difference in python attributes with underscore in front and back, and https://www.python.org/dev/peps/pep-0008/#naming-conventions, an underscore prefix is a weak indicator to the user that this variable is a private variable, but there is no extra mechanism in place (by Python, similar to what Java does) to check or correct such behavior.
So, the big question at hand is that why is it that I need to have underscores for working with properties? I believe those underscore prefixes are just for users to know that this is a private variables.
I am using Lutz's book to learn Python, and above example is inspired from his book.
Lets take your code Fix 1:
class Person:
def __init__(self,name):
self._name=name #Changed name to _name
@property
def name(self):
"name property docs"
print('fetch...')
return self._name #Changed name to _name
bob = Person('Bob Smith')
print(bob.name)
self._name = name
- thats your backing field.def name(self)
- and attribute it with @property
.bob = Person('Bob Smith')
Then you do print(bob.name)
- what are you calling here?
Your variable is called self._name
- and a "non-property" method would be called by bob.name()
.. why does bob.name
still work - its done by the @property decorator.
What happens if you define:
def tata(self):
print(self.name) # also no () after self.name
bob = Person('Bob Smith')
bob.tata()
It will also call your @property method as you can inspect by your 'fetch...'
output. So each call of yourclassinstance.name
will go through the @property accessor - thats why you can not have a self.name
"variable" together with it.
If you access self.name
from inside def name(self)
- you get a circular call - hence: stack overflow.
This is pure observation - if you want to see what exactly happens, you would have to inspect the @property
implementation.
You can get more insight into the topics here:
As pointed out in the comment, using getters/setters is an anti-pattern unless they actually do something:
class Person:
"""Silly example for properties and setter/deleter that do something."""
def __init__(self,name):
self._name = name # bypass name setter by directly setting it
self._name_access_counter = 0
self._name_change_counter = 0
self._name_history = [name]
@property
def name(self):
"""Counts any access and returns name + count"""
self._name_access_counter += 1
return f'{self._name} ({self._name_access_counter})'
@name.setter
def name(self, value):
"""Allow only 3 name changes, and enforce names to be CAPITALs"""
if value == self._name:
return
new_value = str(value).upper()
if self._name_change_counter < 3:
self._name_change_counter += 1
print(f'({self._name_change_counter}/3 changes: {self._name} => {new_value}')
self._name_history.append(new_value)
self._name = new_value
else:
print(f"no change allowed: {self._name} => {new_value} not set!")
@name.deleter
def name(self):
"""Misuse of del - resets counters/history for example purposes"""
self._name_access_counter = 0
self._name_change_counter = 0
self._name_history = self._name_history[:1] # keep initial name
self._name = self._name_history[0] # reset to initial name
print("deleted history and reset changes")
@property
def history(self):
return self._name_history
Usage:
p = Person("Maria")
print(list(p.name for _ in range(5)))
for name in ["Luigi", "Mario", 42, "King"]:
p.name = name
print(p.name) # counter will count ANY get access
print(p.history)
del (p.name)
print(p.name)
print(p.history)
Output:
# get 5 times and print as list
['Maria (1)', 'Maria (2)', 'Maria (3)', 'Maria (4)', 'Maria (5)']
# try to change 4 times
(1/3 changes: Maria => LUIGI
LUIGI (6)
(2/3 changes: LUIGI => MARIO
MARIO (7)
(3/3 changes: MARIO => 42
42 (8)
no change allowed: 42 => KING not set!
42 (9)
# print history so far
['Maria', 'LUIGI', 'MARIO', 'KING']
# delete name, print name and history after delete
deleted history and reset changes
Maria (1)
['Maria']
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With