Encapsulation with Python Properties

If you ever created a class in Python, you probably accessed it using dot notation (i.e. instance_name.attribute_name).

That’s python’s way of calling getattr by means of an alias:

class A:
    var = 10
    pass

a = A()
# this is how Python accesses attributes
getattr(a, 'var')
10
a.__getattribute__('var') # above is an alias for this
10

The most “pythonic” way of getting and setting attributes is using dot notation:

A.var = 11
print(A.var)
11

which is short for the dunder getattribute method

However, if you’re familiar with any other languagee, you’d immediately think of “getter” and “setter” methods. Here’s an example from Java:

public class Airplane {
  private String flightNumber; // private = restricted access

  // Getter
  public String getFlightNumber() {
    return flightNumber;
  }

  // Setter
  public void setFlightNumber(String newNumber) {
    this.flightNumber = newNumber;
  }
}

Why is this important? Because of encapsulation. The entire idea behind this is to ensure “sensitive” data is not directly accessible by end users. Although the example above is quite trivial, these setter and getter methods may contain validation for inputs, as well as check for (e.g.) the existence of an authentication key prior to returning a value.

And I just wasn’t satisfied with vanilla dot-notation in Python.

property to the rescue!

Python 2 introduced property, which facilitates the management of class attributes.

It’s signature is as follows:

property(fget=None, fset=None, fdel=None, doc=None)

fget is the “getter” function, fset is the “setter” function, fdel is the deleter and doc specifies a custom docstring (similar to what you’d see in namedtuple).

When fset is not defined, the attribute becomes read-only:

# using property
class MyClass:
    def __init__(self, ):
        self.__var = 'some value' 

    def get_var(self,):
        print('get_var run')
        return self.__var

    var = property(get_var,)
my_instance = MyClass() 
my_instance.var # this runs
get_var run

'some value'
my_instance.var = 'some other value' # this does not!
---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

/storage/projects/notes/metaprogramming/properties.ipynb Cell 12 in <module>
----> <a href='vscode-notebook-cell:/storage/projects/notes/metaprogramming/properties.ipynb#X33sZmlsZQ%3D%3D?line=0'>1</a> my_instance.var = 'some other value'


AttributeError: can't set attribute

To make it set-able, we need to define a “setter”:

class MyClass:
    def __init__(self, var):
        self.__var = var

    def get_var(self, ):
        return self.__var

    def set_var(self, var):
        self.__var = var

    var = property(get_var, set_var)
my_instance = MyClass(var=10)
my_instance.var # this works
my_instance.var = 11 # so does this!

set_var is run even in the constructor, showing that the last line property(get_var, set_var) run

Some syntactic sugar!

class MyClass:
    def __init__(self, var):
        self.var = var

    @property
    def var(self):
        print('getter run')
        return self.__var

    @var.setter
    def var(self, var):
        print('setter run')
        self.__var = var

my_instance = MyClass(var=11)
setter run
my_instance.var # here the getter is run
getter run

11

The beauty of the above is that I can do validation on the inputs, for example if I have a Person class:

class Person:
    def __init__(self, name, age):
        self.__name = name
        self.__age = age

    @property
    def age(self, ):
        return self.__age

    @age.setter
    def age(self, age):
        if age < 0:
            raise ValueError('Age must be non-negative')
        self.__age = age

a_person = Person(name='Skywalker', age=11)
a_person.age # this works
11
# we get validation whilst maintaining Pythonic dot-notation!
a_person.age = -1 
---------------------------------------------------------------------------

ValueError                                Traceback (most recent call last)

/storage/projects/notes/metaprogramming/properties.ipynb Cell 22 in <module>
----> <a href='vscode-notebook-cell:/storage/projects/notes/metaprogramming/properties.ipynb#X43sZmlsZQ%3D%3D?line=0'>1</a> a_person.age = -1


/storage/projects/notes/metaprogramming/properties.ipynb Cell 22 in Person.age(self, age)
     <a href='vscode-notebook-cell:/storage/projects/notes/metaprogramming/properties.ipynb#X43sZmlsZQ%3D%3D?line=9'>10</a> @age.setter
     <a href='vscode-notebook-cell:/storage/projects/notes/metaprogramming/properties.ipynb#X43sZmlsZQ%3D%3D?line=10'>11</a> def age(self, age):
     <a href='vscode-notebook-cell:/storage/projects/notes/metaprogramming/properties.ipynb#X43sZmlsZQ%3D%3D?line=11'>12</a>     if age < 0:
---> <a href='vscode-notebook-cell:/storage/projects/notes/metaprogramming/properties.ipynb#X43sZmlsZQ%3D%3D?line=12'>13</a>         raise ValueError('Age must be non-negative')
     <a href='vscode-notebook-cell:/storage/projects/notes/metaprogramming/properties.ipynb#X43sZmlsZQ%3D%3D?line=13'>14</a>     self.__age = age


ValueError: Age must be non-negative

A property factory

Using the logic above, we can build our own “factory” for properties. For example, let’s say we have a bunch of attributes that need be validated with a common validation (let’s say they all need to be of a given length and start with the pattern ‘0x’)

def quantity(storage_name):
    def _getter(instance):
        return instance.__dict__[storage_name]

    def _setter(instance, value):
        if len(value) != 10:
            raise ValueError('value must be of length 10') 
        if not value.startswith('0x'):
            raise ValueError('value must start with 0x')
        instance.__dict__[storage_name] = value

    return property(_getter, _setter)

class MyClass:
    a = quantity('a')

    def __init__(self, a):
        self.a = a
my_instance = MyClass(a='0x00000000')
my_instance.a
'0x00000000'
my_instance.a = '0x3' # neither of these work
my_instance.a = '0000000000'
---------------------------------------------------------------------------

ValueError                                Traceback (most recent call last)

/storage/projects/notes/metaprogramming/properties.ipynb Cell 27 in <module>
      <a href='vscode-notebook-cell:/storage/projects/notes/metaprogramming/properties.ipynb#X13sZmlsZQ%3D%3D?line=0'>1</a> # my_instance.a = '0x3' # neither of these work
----> <a href='vscode-notebook-cell:/storage/projects/notes/metaprogramming/properties.ipynb#X13sZmlsZQ%3D%3D?line=1'>2</a> my_instance.a = '0000000000'


/storage/projects/notes/metaprogramming/properties.ipynb Cell 27 in quantity.<locals>._setter(instance, value)
      <a href='vscode-notebook-cell:/storage/projects/notes/metaprogramming/properties.ipynb#X13sZmlsZQ%3D%3D?line=6'>7</a>     raise ValueError('value must be of length 10') 
      <a href='vscode-notebook-cell:/storage/projects/notes/metaprogramming/properties.ipynb#X13sZmlsZQ%3D%3D?line=7'>8</a> if not value.startswith('0x'):
----> <a href='vscode-notebook-cell:/storage/projects/notes/metaprogramming/properties.ipynb#X13sZmlsZQ%3D%3D?line=8'>9</a>     raise ValueError('value must start with 0x')
     <a href='vscode-notebook-cell:/storage/projects/notes/metaprogramming/properties.ipynb#X13sZmlsZQ%3D%3D?line=9'>10</a> instance.__dict__[storage_name] = value


ValueError: value must start with 0x

The above was a short, admittedly convoluted example of what you get do with getters/setters in Python, however I think that the point is clear: if we wish to maintain the Pythonic pattern of dot-notations whilst doubly adhering to the rules of encapsuation, property greatly assists in our ability to manage class attributes