Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Variables starting with underscore for property decorator

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.

like image 671
watchtower Avatar asked Jul 14 '18 17:07

watchtower


1 Answers

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:

  • How do Python properties work?
  • How does the @property decorator work in Python?
  • What's the pythonic way to use getters and setters?

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']
like image 167
Patrick Artner Avatar answered Oct 12 '22 23:10

Patrick Artner