Object orientation

What is Object Orientation?

What is Object Orientation?

  • Describe the "real world" with code
  • Each thing is represented by an object
  • Plenty of possible object relations

Example:

calculate_circle_area(radius=10)
draw_circle(radius=10)

can be transformed into

circle = Circle(radius=10)
circle.calculate_area()
circle.draw()
What is Object Orientation?

Terminology

  • Class: the code that describes what the object is and should do
  • Instance(s): one or many created objects
  • Attribute: variable of an object
  • Method: function of an object
  • Constructor: special method that is called during instantiation
What is Object Orientation?

Procedural vs. object-oriented programming

ðŸ§Ū Procedural programming:

  • Function-centered design
  • Suitable for small projects to respect the YAGNI principle
  • Easy to follow the control flow

ðŸ’ŧ Object-oriented programming:

  • Data-centered design
  • Multiple instances of a class can exist
  • Wisely chosen class structure can result in clean code
Class syntax

Simple class

  • Constructor __init__ to declare attributes and set initial values
class Circle:
    def __init__(self, radius):
        self.radius = radius

circle1 = Circle(2)
circle2 = Circle(10)
print(circle2.radius)   # 10
Class syntax

Object methods

  • First argument is the instance and should be called self in Python
  • Access to all attributes and methods
from numpy import pi

class Circle:
    def __init__(self, radius):
        self.radius = radius

    def calculate_area(self):
        return pi * self.radius**2

circle = Circle(10)
circle.calculate_area()
Class syntax

Private attributes

  • Access from outside like circle.radius should be banned
  • Use the __ (double underscore) prefix
  • Use getters and setters to read and write private attributes
  • Also methods can be made private
from numpy import pi
class Circle:
    def __init__(self, radius):
        self.__radius = radius

    def calculate_area(self):
        return pi * self.__radius**2

    def get_radius(self):
        return self.__radius

    def set_radius(self, radius):
        self.__radius = radius

circle = Circle(10)
circle.set_radius(11)
Class syntax

Private attributes

  • @property decorator makes getters and setters look like an attribute
  • Access as circle.radius from outside
from numpy import pi

class Circle:
    def __init__(self, radius):
        self.__radius = radius

    def calculate_area(self):
        return pi * self.__radius**2

    @property
    def radius(self):
        return self.__radius

    @radius.setter
    def radius(self, radius):
        self.__radius = radius

circle = Circle(10)
circle.radius = 11
Class syntax

Static methods

  • Methods that do not depend on the instance
  • Use @staticmethod decorator to remove the self argument
from numpy import pi, round

class Circle:
    def __init__(self, radius):
        self.radius = radius

    @staticmethod
    def pi(precision):
        return round(pi, precision)

Circle.pi(3)   # == 3.142
Class syntax

Class methods

  • Methods that do not depend on the instance, but on the class
  • Use @classmethod decorator to let the first argument be a class
class Circle:
    def __init__(self, radius):
        self.radius = radius

    @classmethod
    def from_string(cls, input):
        try:
            return cls(float(input))
        except ValueError:
            return None

circle = Circle.from_string("10")
Class syntax

Special methods in Python

  • Python supports many special methods
  • Can be used to implement built-in syntax features
class Circle:
    def __init__(self, radius):
        self.radius = radius

    def __add__(self, other):
        return Circle(self.radius + other.radius)

    def __lt__(self, other):
        return self.radius < other.radius

circle1 = Circle(2)
circle2 = Circle(10)
circle3 = circle1 + circle2
circle1 < circle2   # == True
Class syntax

Special methods in Python: Iterators

  • Provide values in a for loop by implementing __next__ and __iter__
class PiIterator:
    from numpy import pi

    def __init__(self):
        self.digits = [int(digit) for digit in str(pi)[2:][::-1]]

    def __next__(self):
        if len(self.digits) == 0:
            raise StopIteration
        return self.digits.pop()

    def __iter__(self):
        return self

for digit in PiIterator():
    print(digit)
Class syntax

Special methods in Python: Context managers

  • Provide a custom with statement by implementing __enter__ and __exit__
class Timing:
    from time import time

    def __enter__(self):
        self.start = self.time()

    def __exit__(self, *args):
        elapsed = self.time() - self.start
        print('Elapsed time: {} seconds'.format(elapsed))

with Timing():
    print('test')

Exercise

Inheritance

Inheritance

Each class can have multiple instances.

ðŸĪ” Is there a way to create a class B that is a specialized version of class A?

Example: ðŸĶ‰ ðŸĶ† ðŸĶĒ ðŸ§ 🐓 are all birds

Inheritance
  • Polymorphism: "B is a special kind of A"
  • All public attributes and methods from the base class can be inherited
  • Subclasses can overwrite inherited or implement additional attributes and methods
class Bird:
    def fly(self):
        print('in the sky')

class Seagull(Bird):
    pass

class Sparrow(Bird):
    pass

birds = [Seagull(), Sparrow()]

for bird in birds:
    bird.fly()
Inheritance

ðŸĪ”

class Bird:
    def fly(self):
        print('in the sky')

    def dive(self):
        print('in the ocean')

class Seagull(Bird):
    def dive(self):
        pass

class Sparrow(Bird):
    def dive(self):
        pass

class Penguin(Bird):
    def fly(self):
        pass

class Puffin(Bird):
    pass

birds = [Seagull(), Sparrow(), Penguin(), Puffin()]

for bird in birds:
    bird.fly()
    bird.dive()
Composition

Composition

Inheritance often violates the Open-Closed principle.

ðŸ’Ą Favor composition over inheritance!

  • Implement a behavior that an object should have
  • Combine all behaviors you need
  • Often named ...able
Composition
class Flyable:
    def fly(self):
        print('in the sky')

class Diveable:
    def dive(self):
        print('in the ocean')

class Seagull(Flyable):
    pass

class Sparrow(Flyable):
    pass

class Penguin(Diveable):
    pass

class Puffin(Flyable, Diveable):
    pass

birds = [Seagull(), Sparrow(), Penguin(), Puffin()]

for bird in birds:
    if isinstance(bird, Flyable):
        bird.fly()
    if isinstance(bird, Diveable):
        bird.dive()
Design patterns

Design patterns

ðŸĪ” How can we avoid pitfalls when creating class relations? Somebody must have worked this out already.

ðŸ’Ą Design patterns are blueprints how to combine classes. They are tested solutions and applied throughout the community.

Design patterns

Biased selection of important design patterns

  • Factory
  • Builder
  • State
  • Strategy
Design patterns

Factory

  • Move the actual instantiation into a Factory
  • Acknowledges the Open-Closed principle
class Bird:
    pass

class Penguin(Bird):
    pass

class Puffin(Bird):
    pass

class BirdFactory:
    @staticmethod
    def create_bird():
        # Some complicated decision logic here
        return Puffin()

bird = BirdFactory.create_bird()
Design patterns

Builder

  • Set object attributes without cluttering the constructor
  • Provides documentation of the possible configuration
  • Characteristic return self
class Circle:
    def with_radius(self, radius):
        self.radius = radius
        return self

    def with_position(self, position):
        self.position = position
        return self

circle = Circle().with_radius(10).with_position([1, 2])
Design patterns

State

  • Avoid using long if ... elif ... elif decision logic
  • Change the behavior of an object according to its internal state
class BirdState:
    pass

class Flying(BirdState):
    def can_sleep(self):
        return False

class Swimming(BirdState):
    def can_sleep(self):
        return True

class Bird:
    def __init__(self):
        self.state = Swimming()

    def fly(self):
        self.state = Flying()

    def can_sleep(self):
        return self.state.can_sleep()

bird = Bird()
bird.can_sleep()   # == True
bird.fly()
bird.can_sleep()   # == False
Design patterns

Strategy

  • Select a certain implementation of an algorithm
  • Difference to the State pattern: various manifestations of the same aspect
class FlyingStrategy:
    pass

class FastFlyingStrategy(FlyingStrategy):
    def fly(self):
        print('fast')

class SlowFlyingStrategy(FlyingStrategy):
    def fly(self):
        print('slow')

class Bird:
    def __init__(self, flying_strategy):
        self.flying_strategy = flying_strategy

    def fly(self):
        self.flying_strategy.fly()

bird = Bird(FastFlyingStrategy())
bird.fly()   # 'fast'
Further reading

Further reading

https://refactoring.guru/design-patterns
Freeman & Robson: Head First - Design Patterns