5. OOP Paradigm

5.1. Everything is an object

  • W Pythonie wszystkie rzeczy są obiektem.
  • Każdy element posiada swoje metody, które możemy na nim uruchomić.
  • W dalszej części tych materiałów będziemy korzystali z polecenia help() aby zobaczyć jakiego z jakiego typu obiektem mamy okazję pracować oraz co możemy z nim zrobić.

5.2. Duck typing

W językach programowania można doszukać się wielu systemów typowania. System typowania informuje kompilator o obiekcie oraz o jego zachowaniach. Ponadto niesie za sobą informację na temat ilości pamięci, którą trzeba dla takiego obiektu zarezerwować. Istnieje nawet cała gałąź zajmująca się systemami typów. Obecnie najczęściej wykorzystywane języki programowania dzielą się na statycznie - silnie typowane (JAVA, C, C++ i pochodne) oraz dynamicznie - słabo typowane (Python, Ruby, PHP itp.). Oczywiście mogą znaleźć się rozwiązania hybrydowe oraz z tzw. inrefencją typów itp.

W naszym przypadku skupmy się na samym mechanizmie dynamicznego typowania. Określenie to oznacza, że język nie posiada typów zmiennych i obiektów, które jawnie trzeba deklarować. Inicjując zmienną nie musimy powiedzieć, że jest to int. Co więcej po chwili do tej zmiennej możemy przypisać dowolny obiekt, np. łańcuch znaków i kompilator nie powie nam złego słowa. Kompilator podczas działania oprogramowania niejawnie może zmienić typ obiektu i dokonać na nim konwersji.

Wśród programistów popularne jest powiedzenie “jeżeli chodzi jak kaczka i kwacze jak kaczka, to musi być to kaczka”. Od tego powiedzenia wzięła się nazwa Duck typing. Określenie to jest wykorzystywane w stosunku do języków, których typy obiektów rozpoznawane są po metodach, które można na nich wykonać. Nie zawsze takie zgadywanie jest celne i jednoznacznie i precyzyjnie określa typ. Może się okazać, że obiekt np. Samochód posiada metody uruchom_silnik() i jedz_prosto() podobnie jak Motor. Jeden i drugi obiekt będzie zachowywał się podobnie. Języki wykorzystujące ten mechanizm wykorzystują specjalne metody porównawcze, które jednoznacznie dają informację kompilatorowi czy dwa obiekty są równe.

Sam mechanizm dynamicznego typowania jest dość kontrowersyjny, ze względu na możliwość bycia nieścisłym. W praktyce okazuje się, że rozwój oprogramowania wykorzystującego ten sposób jest dużo szybszy. Za to zwolennicy statycznego typowania, twierdzą, że projekty wykorzystujące duck typing są trudne w utrzymaniu po latach. Celem tego dokumentu nie jest udowadnianie wyższości jednego rozwiązania nad drugim. Zachęcam jednak do zapoznania się z wykładem “The Unreasonable Effectiveness of Dynamic Typing for Practical Programs”, którego autorem jest “Robert Smallshire”. Wykład zamieszczonym został w serwisie InfoQ (http://www.infoq.com/presentations/dynamic-static-typing). Wykład w ciekawy sposób dotyka problematyki porównania tych dwóch metod systemu typów. Wykład jest o tyle ciekawy, że bazuje na statystycznej analizie projektów umieszczonych na https://github.com a nie tylko bazuje na domysłach i flamewar jakie programiści lubią prowadzić.

Code Listing 5.13. Duck typing
{}  # dict
{1}  # set
{1, 2}  # set
{1: 2}  # dict
{1: 1, 2: 2}  # dict

my_data = {}
isinstance(my_data, (set, dict))  # True

isinstance(my_data, dict)  # True
isinstance(my_data, set)  # False

my_data = {1}
isinstance(my_data, set)  # True
isinstance(my_data, dict)  # False

my_data = {1: 1}
isinstance(my_data, set)  # False
isinstance(my_data, dict)  # True

5.3. Private, protected, public?!

  • Brak pól protected i private
  • Wszystkie pola są public
  • _nazwa - pola prywatne (tylko konwencja)
  • __nazwa__ - funkcje systemowe
  • nazwa_ - używane przy kolizji nazw
Code Listing 5.14. _ and __ - Private, protected, public?!
class Astronaut:
    first_name = ''     # public
    last_name = ''      # public
    _agency = None      # private

    def print_(self):   # avoid name collision with print
        print(self.__str__())

    def __str__(self):  # system function
        return f'My name... {self.name}'

5.4. Inheritance vs. Composition (Mixin Classes)

  • Kompozycja ponad dziedziczenie!
Code Listing 5.15. Composition (Mixin Classes)
class JSONSerializable:
    def to_json(self):
        import json
        return json.dumps(self.__dict__)


class PickleSerializable:
    def to_pickle(self):
        import pickle
        return pickle.dumps(self)


class Connection(JSONSerializable, PickleSerializable):
    def __init__(self, host, user, password=None):
        self.host = host
        self.user = user
        self.password = password


connection = Connection(
    host='localhost',
    user='admin',
    password='admin'
)

connection.to_json()
# {"host": "localhost", "user": "admin", "password": "admin"}

connection.to_pickle()
# b'\x80\x03c__main__\nServer\nq\x00)\x81q\x01}q\x02(X\x04\x00\x00\x00hostq\x03X\t\x00\x00\x00localhostq\x04X\x04\x00\x00\x00userq\x05X\x05\x00\x00\x00adminq\x06X\x08\x00\x00\x00passwordq\x07h\x06ub.'

5.5. Polymorphism

Code Listing 5.16. Switch moves business logic to the execution place
agency = 'NASA'

if agency == 'NASA':
    print('Howdy from NASA')
elif agency == 'Roscosmos':
    print('Privyet z Roscosmos')
elif agency == 'ESA':
    print('Guten Tag aus ESA')
else:
    raise NotImplementedError
Code Listing 5.17. Polymorphism on Function
class Bear(object):
    def sound(self):
        print('Groarrr')


class Dog(object):
    def sound(self):
        print('Woof woof!')


def makeSound(animal):
    animal.sound()


koala = Bear()
hart = Dog()

makeSound(koala)
makeSound(hart)
Code Listing 5.18. Polymorphism on Classes
class Astronaut:
    agency = None

    def __init__(self, name):
        self.name = name

    def say_hello(self):
        raise NotImplementedError


class NASAAstronaut(Astronaut):
    agency = 'NASA'

    def say_hello(self):
        print(f'Howdy from {self.agency}')


class ESAAstronaut(Astronaut):
    agency = 'ESA'

    def say_hello(self):
        print(f'Guten Tag aus {self.agency}')


class RoscosmosAstronaut(Astronaut):
    agency = 'Roscosmos'

    def say_hello(self):
        print(f'Privyet z {self.agency}')


crew = [
    NASAAstronaut('José Jiménez'),
    RoscosmosAstronaut('Ivan Иванович'),
    ESAAstronaut('Alex Vogel'),
    NASAAstronaut('Mark Watney'),
]

for astronaut in crew:
    astronaut.say_hello()
    # Howdy from NASA
    # Privyet z Roscosmos
    # Guten Tag aus ESA
    # Howdy from NASA

5.6. Interfaces

  • Nie można tworzyć instancji
  • Wszystkie metody muszą być zaimplementowane przez potomków
  • Tylko deklaracje metod
  • Metody nie mogą mieć implementacji
Code Listing 5.19. Interfaces
class MLModelInterface:
    def fit(self, features, labels):
        raise NotImplementedError

    def predict(self, data):
        raise NotImplementedError


class KNeighborsClassifier(MLModelInterface):
    def fit(self, features, labels):
        pass

    def predict(self, data):
        pass


class LinearRegression(MLModelInterface):
    def fit(self, features, labels):
        pass

    def predict(self, data):
        pass

class LogisticsRegression(MLModelInterface):
    pass


# Imput to the classifier
features = [
    (5.1, 3.5, 1.4, 0.2),
    (4.9, 3.0, 1.4, 0.2),
    (4.7, 3.2, 1.3, 0.2),
    (7.0, 3.2, 4.7, 1.4),
    (6.4, 3.2, 4.5, 1.5),
    (6.9, 3.1, 4.9, 1.5),
    (6.3, 3.3, 6.0, 2.5),
    (5.8, 2.7, 5.1, 1.9),
    (7.1, 3.0, 5.9, 2.1),
]

# 0: I. setosa
# 1: I. versicolor
# 2: I. virginica
labels = [0, 0, 0, 1, 1, 1, 2, 2, 2]

to_predict = [
    (5.7, 2.8, 4.1, 1.3),
    (4.9, 2.5, 4.5, 1.7),
    (4.6, 3.4, 1.4, 0.3),
]

model = LinearRegression()
model.fit(features, labels)
model.predict(to_predict)
# [1, 2, 0]

5.7. Abstract Classes

  • Nie można tworzyć instancji
  • Można tworzyć implementację metod
Code Listing 5.20. Abstract Class
from abc import ABC, abstractmethod


class Document(ABC):
    def __init__(self, filename):
        self.filename = filename

    @abstractmethod
    def display(self):
        with open(self.filename) as file:
            return file.read()


class PDFDocument(Document):
    def display(self):
        content = super().display()
        # display ``content`` as PDF Document


class WordDocument(Document):
    def display(self):
        content = self.display()
        # display ``content`` as Word Document


file1 = PDFDocument('filename.pdf')
file1.display()

file2 = Document('filename.txt')  # TypeError: Can't instantiate abstract class Document with abstract methods display

5.8. Good Engineering Practises

5.8.1. Tell - don’t ask

  • Tell-Don’t-Ask is a principle that helps people remember that object-orientation is about bundling data with the functions that operate on that data.
  • It reminds us that rather than asking an object for data and acting on that data, we should instead tell an object what to do.
  • This encourages to move behavior into an object to go with the data.
Code Listing 5.21. Tell - don’t ask
# Good
class Rocket:
    status = 'off'

    def ignite(self):
        self.status = 'on'


soyuz = Rocket()
soyuz.ignite()


# Bad
class Rocket:
    status = 'off'


soyuz = Rocket()
soyuz.status = 'on'

5.8.2. Do not run methods in __init__()

  • Nie powinniśmy uruchamiać innych metod na obiekcie.
  • Obiekt nie jest jeszcze w pełni zainicjalizowany!
  • Konstruktor się nie wykonał do końca.
  • Dopiero jak się skończy __init__ to możemy uruchamiać metody obiektu
Code Listing 5.22. Do not run methods in __init__()
class Server:

    def __init__(self, host, user, password=None):
        self.host = host
        self.user = user
        self.password = password
        self.login()  # You should not do this way

    def login(self):
        print('Logging...')


localhost = Server(
    host='localhost',
    user='admin',
    password='admin'
)

# to jest poprawne wywołanie
localhost.login()

5.9. S.O.L.I.D.

5.9.1. Single responsibility principle

a class should have only a single responsibility (i.e. changes to only one part of the software’s specification should be able to affect the specification of the class)

The single responsibility principle is a computer programming principle that states that every module or class should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class. All its services should be narrowly aligned with that responsibility. Robert C. Martin expresses the principle as, “A class should have only one reason to change.”

5.9.2. Open/closed principle

software entities … should be open for extension, but closed for modification

The name open/closed principle has been used in two ways. Both ways use generalizations (for instance, inheritance or delegate functions) to resolve the apparent dilemma, but the goals, techniques, and results are different.

5.9.3. Liskov substitution principle

objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program. See also design by contract.

Substitutability is a principle in object-oriented programming stating that, in a computer program, if S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e. an object of type T may be substituted with any object of a subtype S) without altering any of the desirable properties of the program (correctness, task performed, etc.).

5.9.4. Interface segregation principle

many client-specific interfaces are better than one general-purpose interface

The interface-segregation principle (ISP) states that no client should be forced to depend on methods it does not use. ISP splits interfaces that are very large into smaller and more specific ones so that clients will only have to know about the methods that are of interest to them. Such shrunken interfaces are also called role interfaces. ISP is intended to keep a system decoupled and thus easier to refactor, change, and redeploy. ISP is one of the five SOLID principles of object-oriented design, similar to the High Cohesion Principle of GRASP.

5.9.5. Dependency inversion principle

one should depend upon abstractions, [not] concretions

In object-oriented design, the dependency inversion principle refers to a specific form of decoupling software modules. When following this principle, the conventional dependency relationships established from high-level, policy-setting modules to low-level, dependency modules are reversed, thus rendering high-level modules independent of the low-level module implementation details. The principle states:

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
  2. Abstractions should not depend on details. Details should depend on abstractions.

By dictating that both high-level and low-level objects must depend on the same abstraction this design principle inverts the way some people may think about object-oriented programming.

5.10. GRASP

General responsibility assignment software patterns (or principles), abbreviated GRASP, consist of guidelines for assigning responsibility to classes and objects in object-oriented design.

The different patterns and principles used in GRASP are controller, creator, indirection, information expert, high cohesion, low coupling, polymorphism, protected variations, and pure fabrication. All these patterns answer some software problem, and these problems are common to almost every software development project. These techniques have not been invented to create new ways of working, but to better document and standardize old, tried-and-tested programming principles in object-oriented design.

5.11. Dependency injection

Dependency injection, as a software design pattern, has number of advantages that are common for each language (including Python):

  • Dependency Injection decreases coupling between a class and its dependency.
  • Because dependency injection doesn’t require any change in code behavior it can be applied to legacy code as a refactoring. The result is clients that are more independent and that are easier to unit test in isolation using stubs or mock objects that simulate other objects not under test. This ease of testing is often the first benefit noticed when using dependency injection.
  • Dependency injection can be used to externalize a system’s configuration details into configuration files allowing the system to be reconfigured without recompilation (rebuilding). Separate configurations can be written for different situations that require different implementations of components. This includes, but is not limited to, testing.
  • Reduction of boilerplate code in the application objects since all work to initialize or set up dependencies is handled by a provider component.
  • Dependency injection allows a client to remove all knowledge of a concrete implementation that it needs to use. This helps isolate the client from the impact of design changes and defects. It promotes reusability, testability and maintainability.
  • Dependency injection allows a client the flexibility of being configurable. Only the client’s behavior is fixed. The client may act on anything that supports the intrinsic interface the client expects.
Code Listing 5.23. Dependency injection
from datetime import timedelta


class Cache:
    def __init__(self, expiration=timedelta(days=30), location=None):
        self.expiration = expiration
        self.location = location

    def get(self):
        raise NotImplementedError

    def set(self):
        raise NotImplementedError

    def is_valid(self):
        raise NotImplementedError


class CacheFilesystem(Cache):
    """Cache using files"""


class CacheMemory(Cache):
    """Cache using memory"""


class CacheDatabase(Cache):
    """Cache using database"""


class HTTP:
    def __init__(self, cache):
        # Inject Cache object
        self._cache = cache

    def _fetch(self, url):
        return ...

    def get(self, url):
        if self._cache.is_valid():
            # Use cached data
            self._cache.get(url)
        else:
            data = self._fetch(url)
            self._cache.set(url, data)


if __name__ == '__main__':
    database = CacheDatabase(location='sqlite3://http-cache.sqlite3')
    filesystem = CacheFilesystem(location='/tmp/http-cache.txt')
    memory = CacheMemory(expiration=timedelta(hours=2))

    http1 = HTTP(cache=database)
    http1.get('http://python.astrotech.io')

    http2 = HTTP(cache=filesystem)
    http2.get('http://python.astrotech.io')

    http3 = HTTP(cache=memory)
    http3.get('http://python.astrotech.io')