Object oriented features in Python

Access modifiers

You can't have oop without clases, python does support clases, it's support many features of oop, so can you guess if it support access modifiers like, private, public, protected?

You guess it, it doesn't. In python you don't have private, protected or public.

What we have are naming conventions:

class A:
    _a = 4
    __b =5

print(A._a) # 4
pprint(A.__dict__)
# mappingproxy({'_A__b': 5,
#               '__dict__': <attribute '__dict__' of 'A' objects>,
#               '__doc__': None,
#               '__module__': '__main__',
#               '__weakref__': <attribute '__weakref__' of 'A' objects>,
#               '_a': 4})
print(A._A__b) # 5
print(A.__b) # raises AttributeError

A variable with one single underscore can be used for private, but it's not hiding anything we can always access it, after all is just a naming convention but it has one usecase when we import from a module:

_single_leading_underscore: weak "internal use" indicator. E.g. from M import * does not import objects whose names start with an underscore. ( PEP 8)

From our example A.__b raises an exception, because the attrite name is _A__b and we get an 5.

Variable with double leading underscores in classes become _classname__variablename.

__double_leading_underscore: when naming a class attribute, invokes name mangling PEP 8)

 Encapsulation

While true, we don't have private, we can still write classes and have methods and do everything in a class and is hidden away from other classes, is also true that nothing is private in python.
So is it supported, yes or no? Well, if you want exatcly like Java, C#, no it is not, nothing is private in python no matter what convention you use.

Polymorphism

Let's see some Java code first,

import java.util.ArrayList;
class A {}
class B extends A {}
class C extends A {}
public class Example{
     public static void main(String []args){
        ArrayList<A> objects = new ArrayList<>();
        objects.add(new A());
        objects.add(new B());
        objects.add(new C());
        System.out.println(objects.size());
     }
}

The output is 3, meaning that the code is compiling and running. We have an array named objects, that we specify by using generics that only objects of type A should be allowed in this list. Because of polymorphism, classes that inherit from class A, can also be added to the objects array.

In Java we need to specify every variable's data type because is a strong typed language, python is dynamically typed language. We can easily do the following in python:

my_list = [HttpServer(), 1, True, "string", DatabaseConnection(), MysqlConnector(), open('/etc/file.txt')]

In a list in python we can add what we want, primiteves, all kind of objects, nobody stops us.

So, polymorphism in python can't be proven, because is dynamically typed language.

Inheritance, diamond problem and mro

Python does support single inheritance  and multiple inheritance.
class A:

    def hello(self):
        print('A')


class B(A):
    pass


B().hello() # A
print(isinstance(B(), A)) # True

Class B, doesn't have any method defined, so hello() is inheritaded from A, there is printing A.

Using isinstance() we can see that, instances of B are also instances of A, becauses B inherits from A and this is good, using inheritanecs and Liskov substitution principle we can build better software, the principles says that subclasses can replace the parent clases without breaking anything.

Let's try the diamond problem in Python and see what happens:

class A:

    def hello(self):
        print('A')


class B(A):

    def hello(self):
        print('B')


class C(A):

    def hello(self):
        print('C')


class D(B, C):
    pass

D().hello() # B
help(D)
#output from help(D)
# Help on class D in module __main__:
#
# class D(B, C)
#  |  Method resolution order:
#  |      D
#  |      B
#  |      C
#  |      A
#  |      builtins.object
#  |
#  |  Methods inherited from B:
#  |
#  |  hello(self)
#  |
#  |  ----------------------------------------------------------------------
#  |  Data descriptors inherited from A:
#  |
#  |  __dict__
#  |      dictionary for instance variables (if defined)
#  |
#  |  __weakref__
#  |      list of weak references to the object (if defined)
print(D.__mro__) # (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)

The code runs and calling hello() on D, prints B. How does python knows what method to call from B or C? It's using the C3 linearization algorithm to determine the method resolution order (mro).

We can run help() and the Method resolution order is printed or we can use the __mro__ attribute.

So this is all nice and interesting but can we do something usefull with this? 

Dependency injection using multiple inheritance

Let's look at this code:

class FileStorage:

    def store_data(self, data):
        print(f"Writing {data} to file")


class Message(FileStorage):

    def __init__(self, text, encoding):
        self.text = text
        self.encoding = encoding

    def save(self):
        """
        Writes the message to a persistent storage
        :return:
        """
        # do some processing, calcualtion, add to a queue and then store it
super().store_data(json.dumps({ 'text': self.text.decode('utf-8'), 'encoding': self.encoding })) message = Message(text=base64.b64encode(b'AAAa'), encoding='base64') message.save() # Writing {"text": "QUFBYQ==", "encoding": "base64"} to file

We have a class Message, that has a method save(). In save() we can do some processing and then we call super().store_data() to save the message to a persistant storage system. Very simple.

What if we want now to save the message to a temporary file directory?

class TemporaryFileStorage(FileStorage):

    def store_data(self, data):
        print(f"Writing {data} to temp storage")


class TemporaryStoredMessage(Message, TemporaryFileStorage):
    """A message that is stored into the temp storage"""


message = TemporaryStoredMessage(text=base64.b64encode(b'AAAa'), encoding='base64')
message.save()
# Writing {"text": "QUFBYQ==", "encoding": "base64"} to temp storage

We create our TemporaryFileStorage class and our TemporaryStoredMessage inherits from both Message and TemporaryFileStorage.

The call to super() inside Message.save() will search the method in the mro chain and when found it calls it, that's why the output is saying "to temp storage". Let's print the mro chain:

pprint(TemporaryStoredMessage.__mro__)
# (<class '__main__.TemporaryStoredMessage'>,
#  <class '__main__.Message'>,
#  <class '__main__.TemporaryFileStorage'>,
#  <class '__main__.FileStorage'>,
#  <class 'object'>)

First is our child class TemporaryStoredMessage, then from left to right, Message and then our TemporaryFileStorage.  When we are in the save() method in Message(), super() will begin it's search at TemporaryFileStorage and uses the method from there instead from FileStorage class.

Abstraction

Python doesn't have a keyword like abstract but it has the abc module.

This code below is copy paste from the python documentation.

class C(ABC):
    @abstractmethod
    def my_abstract_method(self, ...):
        ...
    @classmethod
    @abstractmethod
    def my_abstract_classmethod(cls, ...):
        ...
    @staticmethod
    @abstractmethod
    def my_abstract_staticmethod(...):
        ...

    @property
    @abstractmethod
    def my_abstract_property(self):
        ...
    @my_abstract_property.setter
    @abstractmethod
    def my_abstract_property(self, val):
        ...

    @abstractmethod
    def _get_x(self):
        ...
    @abstractmethod
    def _set_x(self, val):
        ...
    x = property(_get_x, _set_x)

We inherit from the ABC class and the we use decorators like @abstractmethod to mark an method abstract that the implementation has to be provided by the child class. You can also have abstractproperty.

Besides inheritance we can also use register() but there is something to keep in mind about register.

from abc import *


class StorageSystem(ABC):

    @abstractmethod
    def read(self):
        pass


class Database:

    def write(self, data):
        print(f"Writing {data} to database")


class FileStorage(StorageSystem):

    def read(self, file):
        print(f"Reading data from {file}")

So let's say that we have those classes. Let's play around a little bit.

StorageSystem.register(Database)
database = Database()
print(StorageSystem.__subclasses__()) # [<class '__main__.FileStorage'>]
print(isinstance(database, StorageSystem)) # True
database.read('data') # AttributeError: 'Database' object has no attribute 'read'

First is the call to register(). Now the class Database is what python calls a "“virtual subclass”  of StorageSystem. This means that our isinstance() returns True when we ask if our database object is a instance of type StorageSystem.

But virtual subclasses are not part of  __subclasses__(), they are not returned in the list only subclasses using inheritance are.

Another imporant thing is that now we have Database being child of StorageSystem but StorageSystem has read() implemented as abstract and we don't have it in Database and we don't have to have it, there is no checking of the method existing, the call to read() fails with an AttributeError. 

When using register() there is no checking being done by python.

Class method, variables and @staticmethod

Some code first:
class User:

    count = 0

    def __init__(self, name):
        self.name = name
        User.count += 1

    @classmethod
    def number_of_users(cls):
        return cls.count

    @classmethod
    def from_dict(cls, user):
        return cls(user['name'])

    @staticmethod
    def is_valid(name):
        return len(name) > 3

user1 = User('user1')
user2 = User('user2')
user3 = User.from_dict({
    'name': 'user3'
})
print(User.number_of_users()) # 3
print(User.is_valid('aa')) # False

We have classmethod and staticmethod. Classmethod is want in another language we defined by using the static keyword. In python all @classmethod have first argument the class. (name convention cls).

Also the count variable is a class variable not instance. Self is used to refer to instance variable and the name of the class User.count or cls.count is used to refer to class variable.

In number_of_users we have cls.count that returns the class variable.

In __init__ we use the class name because we can't access the class is another way.

In from_dict we return a instance of the class from a dict, using cls(), classmethods are great for doing factory methods like from_something.

The static methods don't have the cls argument, they are just function that are related to the class but work independently of the class, they could be in a separate file but are in the class because they relate to the  logic of class, so is better to have them in the same file,  you can always transform it into a class method.

Python doesn't have interfaces, we can use the abc module and define a method as abstractmethod and use that as a replacement for a interface.

If you want a video on super() you can look at Raymond Hettinger (python core developer) talk at pycon2015
Raymond Hettinger - Super considered super! - PyCon 2015

Comments