In [1]:
# code for loading the format for the notebook
import os

# path : store the current path to convert back to it later
path = os.getcwd()
os.chdir(os.path.join('..', 'notebook_format'))

from formats import load_style
load_style(plot_style=False)
Out[1]:
In [2]:
os.chdir(path)

# 1. magic to print version
# 2. magic so that the notebook will reload external python modules
%load_ext watermark
%load_ext autoreload 
%autoreload 2

%watermark -a 'Ethen' -d -t -v
Ethen 2018-08-19 02:10:18 

CPython 3.6.4
IPython 6.4.0

Working with Python Classes

Encapsulation is seen as the bundling of data with the methods that operate on that data. It is often accomplished by providing two kinds of methods for attributes: The methods for retrieving or accessing the values of attributes are called getter methods. Getter methods do not change the values of attributes, they just return the values. The methods used for changing the values of attributes are called setter methods.

Public, Private, Protected

There are two ways to restrict the access to class attributes:

  1. protected. First, we can prefix an attribute name with a leading underscore "_". This marks the attribute as protected. It tells users of the class not to use this attribute unless, somebody writes a subclass.
  2. private. Second, we can prefix an attribute name with two leading underscores "__". The attribute is now inaccessible and invisible from outside. It's neither possible to read nor write to those attributes except inside of the class definition itself.
In [3]:
class A:
    
    def __init__(self):
        self.__priv = "I am private"
        self._prot = "I am protected"
        self.pub = "I am public"

x = A()
print(x.pub)

# Whenever we assign or retrieve any object attribute 
# Python searches it in the object's __dict__ dictionary
print(x.__dict__)
I am public
{'_A__priv': 'I am private', '_prot': 'I am protected', 'pub': 'I am public'}

When the Python compiler sees a private attribute, it actually transforms the actual name to _[Class name]__[private attribute name]. However, this still does not prevent the end-user from accessing the attribute. Thus in Python land, it is more common to use public and protected attribute, write proper docstrings and assume that everyone is a consenting adult, i.e. won't do anything with the protected method unless they know what they are doing.

Class Decorators

  • @property The Pythonic way to introduce attributes is to make them public, and not introduce getters and setters to retrieve or change them.
  • @classmethod To add additional constructor to the class.
  • @staticmethod To attach functions to classes so people won't misuse them in wrong places.

@Property

Let's assume one day we decide to make a class that could store the temperature in degree Celsius. The temperature will be a private method, so our end-users won't have direct access to it.

The class will also implement a method to convert the temperature into degree Fahrenheit. And we also want to implement a value constraint to the temperature, so that it cannot go below -273 degree Celsius. One way of doing this is to define a getter and setter interfaces to manipulate it.

In [4]:
class Celsius:
    
    def __init__(self, temperature = 0):
        self.set_temperature(temperature)

    def to_fahrenheit(self):
        return (self.get_temperature() * 1.8) + 32

    def get_temperature(self):
        return self._temperature

    def set_temperature(self, value):
        if value < -273:
            raise ValueError('Temperature below -273 is not possible')
        
        self._temperature = value
In [5]:
# c = Celsius(-277) # this returns an error
c = Celsius(37)
c.get_temperature()
Out[5]:
37

Instead of that, now the property way. Where we define the @property and the @[attribute name].setter.

In [6]:
class Celsius:
    
    def __init__(self, temperature = 0):
        self._temperature = temperature

    def to_fahrenheit(self):
        return (self.temperature * 1.8) + 32
    
    # have access to the value like it is an attribute instead of a method
    @property
    def temperature(self):
        return self._temperature
    
    # like accessing the attribute with an extra layer of error checking
    @temperature.setter
    def temperature(self, value):
        if value < -273:
            raise ValueError('Temperature below -273 is not possible')
        
        print('Setting value')
        self._temperature = value
In [7]:
c = Celsius(37)

# much easier to access then the getter, setter way
print(c.temperature)

# note that you can still access the private attribute
# and violate the temperature checking, 
# but then it's the users fault not yours
c._temperature = -300
print(c._temperature)

# accessing the attribute will return the ValueError error
# c.temperature = -300
37
-300

@classmethod and @staticmethod

@classmethods create alternative constructors for the class. An example of this behavior is there are different ways to construct a dictionary.

In [8]:
print(dict.fromkeys(['raymond', 'rachel', 'mathew']))
{'raymond': None, 'rachel': None, 'mathew': None}
In [9]:
import time

class Date:
    # Primary constructor
    def __init__(self, year, month, day):
        self.year = year
        self.month = month
        self.day = day

    # Alternate constructor
    @classmethod
    def today(cls):
        t = time.localtime()
        return cls(t.tm_year, t.tm_mon, t.tm_mday)

# Primary
a = Date(2012, 12, 21) 
print(a.__dict__)

# Alternate
b = Date.today() 
print(b.__dict__)
{'year': 2012, 'month': 12, 'day': 21}
{'year': 2018, 'month': 8, 'day': 19}

The cls is critical, as it is an object that holds the class itself. This makes them work with inheritance.

In [10]:
class NewDate(Date):
    pass

# Creates an instance of Date (cls=Date)
c = Date.today()      
print(c.__dict__)

# Creates an instance of NewDate (cls=NewDate)
d = NewDate.today()   
print(d.__dict__)
{'year': 2018, 'month': 8, 'day': 19}
{'year': 2018, 'month': 8, 'day': 19}

The purpose of @staticmethod is to attach functions to classes. We do this to improve the findability of the function and to make sure that people are using the function in the appropriate context.

In [11]:
class Date:
    # Primary constructor
    def __init__(self, year, month, day):
        self.year = year
        self.month = month
        self.day = day

    # Alternate constructor
    @classmethod
    def today(cls):
        t = time.localtime()
        return cls(t.tm_year, t.tm_mon, t.tm_mday)
    
    # the logic belongs with the date class
    @staticmethod
    def show_tomorrow_date():
        t = time.localtime()
        return t.tm_year, t.tm_mon, t.tm_mday + 1
In [12]:
Date.show_tomorrow_date()
Out[12]:
(2018, 8, 20)

For those interested, the following link contains a much more in-depth introduction into @classmethod and @staticmethod. Blog: Python's Instance, Class, and Static Methods Demystified