12

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.

Aran-Fey
  • 39,665
  • 11
  • 104
  • 149
watchtower
  • 4,140
  • 14
  • 50
  • 92
  • If you have a property called `foo`, what do you think will happen when its implementation accesses `foo`? – Ignacio Vazquez-Abrams Jul 14 '18 at 17:08
  • Why would you have a "public "property and a getter/setter at the same time? If you have no additional logic needed I would just use the property and access this. If you have additional logic I would create it as "private" and present getter/setter as entrypoints that capsulate my additional logic. I would not ever present _both_ to the outside, thats just confusing and poses a way to circumvent my getters/setters logic from the outside. Which is against the intent of creating them in the first place... why would you do it? – Patrick Artner Jul 14 '18 at 17:30
  • @PatrickArtner - I don't know why plain `name` variables or methods don't work. In fact, if I remove all `_`, then the code would compile, but I would get stackoverflow error. I am really not sure. All the examples I have seen on the Internet and in Lutz's book use variables with `_` as pre-fix or methods with `_` as pre-fix. I am not sure why. – watchtower Jul 14 '18 at 19:01
  • think about what Ignacio commented. What happens if foo calls foo calls foo calls foo calls foo calls foo calls foo calls foo ad infinum (or until the stack overflows...) - you would have to dig into what putting an @property as decorator on a function does – Patrick Artner Jul 14 '18 at 19:08
  • @Patrick - very respectfully, I am not sure. I am just reading the book. I'd appreciate if you could explain this with an example. It's my understanding that `self.name=name` in `__init__` will call `name` property once. I am unsure why there would be recursion. – watchtower Jul 14 '18 at 19:19
  • 1
    there’s nothing special about the underscore going on. the editor may be suggesting it, but python itself doesn’t care. what’s happening is that “name” access will grab what is called “name”. if that’s the property then great. if the property then tries to access “name” it hits whatever is name. i.e. itself. the property needs to either fully compute on its own. or it needs to access some other instance variable. maybe “NAME”, upper case. however, Python _conventions_ generally says \_ means private, so \_name is a good cache. Editor is warning of a goof but also too prescriptive. – JL Peyret Jul 15 '18 at 03:25
  • 1
    also back in the days of Chandler (a failed open source PIM) someone wrote an essay *Python is not Java*- getters and setters are a Python anti-pattern unless they actively **do** something. That might be picking a random first name from all Oscar Award winners. It might keeping name from being changed. But a straight get/set has little justification on its own, including leaving open class for further private modifications – JL Peyret Jul 15 '18 at 03:39

1 Answers1

5

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)
  • You define self._name = name - thats your backing field.
  • You define a method def name(self) - and attribute it with @property.
  • You create an instance of your class by 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']
Patrick Artner
  • 50,409
  • 9
  • 43
  • 69