B01 OOP Pillars
Abstraction
# Abstraction: hiding details with abstract base classes
# -----------------------------------------------------------------------------
# Abstraction defines a common interface while hiding implementation details.
# Using an abstract base class forces subclasses to implement specific
# behaviours without revealing how they will work.
from abc import ABCMeta, abstractmethod
from six import with_metaclass
class PersonAbc(with_metaclass(ABCMeta)):
"""An abstract base class defining what a person should be able to do."""
def __init__(self):
# Some common attributes that every Person has
self.name = 'Bob'
self.age = 42
self.weight = 80
self.height = 180
@abstractmethod
def walk(self):
# Still abstract, because we don't know how a specific person walks.
pass
@abstractmethod
def talk(self):
# Still abstract, because we don't know how a specific person talks.
pass
@abstractmethod
def eats(self):
# Still abstract, because we don't know how a specific person eats.
pass
Access Modifiers
# Access modifiers and name mangling
# -----------------------------------------------------------------------------
# Public attributes have no leading underscores and can be accessed from
# anywhere. A single leading underscore marks an attribute as "protected" by
# convention, signalling it should only be used by the class and its
# subclasses. A double underscore triggers name mangling which makes the
# attribute effectively private to the class. This prevents accidental
# access from subclasses or external code.
class AccessModifiers(object):
def __init__(self):
# Public: Accessible from anywhere
self.public = "public"
# Protected: Accessible from the class and subclasses
self._protected = "protected"
# Private attribute -- the name will be mangled to _AccessModifiers__private
# and is intended for use only inside this class
self.__private = "private"
class AccessModifiersChild(AccessModifiers):
def __init__(self):
super(AccessModifiersChild, self).__init__()
# Public: Accessible from anywhere
print(self.public)
# Protected: Accessible from the class and subclasses
print(self._protected)
# Private: name is mangled so direct access fails in the child class
try:
print(self.__private)
except AttributeError as e:
print(e)
test = AccessModifiersChild()
Aggregation
# Aggregation: objects hold references to independent parts
# -----------------------------------------------------------------------------
# Aggregation occurs when one object keeps references to other, independent
# objects. The referenced parts can exist on their own and are not owned by
# the aggregator. Here the `Rocket` receives an `Engine` instance that can
# outlive the rocket itself.
class Engine(object):
def __init__(self, engine_type, engine_model):
self.engine_type = engine_type
self.engine_model = engine_model
def start(self):
print("{} engine started".format(self.engine_type))
def stop(self):
print("{} engine stopped".format(self.engine_type))
class SolidFuelEngine(Engine):
def __init__(self, engine_model):
super(SolidFuelEngine, self).__init__("solid fuel", engine_model)
class LiquidFuelEngine(Engine):
def __init__(self, engine_model):
super(LiquidFuelEngine, self).__init__("liquid fuel", engine_model)
class Rocket(object):
def __init__(self, engine):
# This is aggregation: the Rocket keeps a reference to an Engine
# that was created outside. The engine is not owned by the Rocket
# and could be reused elsewhere or exist on its own.
self.engine = engine
def launch(self):
self.engine.start()
rocket1 = Rocket(SolidFuelEngine("model 1"))
rocket1.launch()
rocket2 = Rocket(LiquidFuelEngine("model 2"))
rocket2.launch()
Association
# Association: cooperating without ownership
# -----------------------------------------------------------------------------
# Association is a loose coupling between otherwise independent objects. They
# collaborate to accomplish a task but neither owns the lifetime of the other.
# In this example the `Calculator` uses a `Battery`, a `Display`, and the
# utility class `Math` without being responsible for their existence.
class Math(object):
@staticmethod
def add(a, b):
return a + b
class Battery(object):
def __init__(self, model):
self.model = model
def charge(self):
print("Battery {} charging".format(self.model))
def discharge(self):
print("Battery {} discharging".format(self.model))
class FourLineDisplay(object):
def __init__(self, model):
self.model = model
def display(self):
print("Display {} displaying".format(self.model))
def turn_off(self):
print("Display {} turning off".format(self.model))
def turn_on(self):
print("Display {} turning on".format(self.model))
class Calculator(object):
def __init__(self, display):
self.display = display
self.battery = Battery("default")
def add(self, a, b):
# The Calculator is associated with Math only to perform this
# calculation. Neither object owns the other.
result = Math.add(a, b)
return result
def replace_battery(self, battery):
self.battery = battery
calc = Calculator(display=FourLineDisplay(model="EL-W506T"))
calc.add(1, 1)
Class Abstract Properties
# Stacking decorators
# ------------------------------------------------------------------------------
# Multiple decorators can be stacked on a single attribute. In this file a
# property is defined using @property together with @abstractmethod. Derived
# classes must supply the concrete implementation for this decorated property.
from abc import ABCMeta, abstractmethod
from six import with_metaclass
class DeviceAbc(with_metaclass(ABCMeta)):
"""Example abstract class
Usage:
# Optional
@property, @staticmethod, @classmethod
+
# Obligatory decorator
@abstractmethod
Example:
# Defines and abstract property
@property
@abstractmethod
def prop(self):
...
"""
def __init__(self):
self._bar = "bar"
@property
@abstractmethod
def bar(self):
pass
@abstractmethod
def foo(self):
pass
class Samsung(DeviceAbc):
@property
def bar(self):
return self._bar
def foo(self):
print('foo')
test = Samsung()
print(test.bar)
test.foo()
Class Abstract Python2
# Abstract class using the six library for python 2
# ------------------------------------------------------------------------------
# The six library helps define abstract base classes that remain compatible with
# Python 2. The metaclass provided by six works with decorators such as
# @abstractmethod so subclasses must implement the required methods.
from six import with_metaclass
from abc import ABCMeta, abstractmethod
class CalculatorAbc(with_metaclass(ABCMeta)):
def __init__(self, mode="basic"):
self.mode = mode
@abstractmethod
def add(self, *args, **kwargs):
raise NotImplementedError
@abstractmethod
def subtract(self, *args, **kwargs):
raise NotImplementedError
class Calculator(CalculatorAbc):
def add(self, a, b):
return a + b
def subtract(self, a, b):
return a - b
calc = Calculator()
print(calc.add(1, 2))
print(calc.mode)
Class Abstract Python3
# Abstract class in python 3+
# ------------------------------------------------------------------------------
# Python 3 provides native support for abstract base classes. The ABC and
# abstractmethod decorators ensure that child classes implement required
# behavior. Instances cannot be created until the abstract methods are
# overridden.
from abc import ABC, abstractmethod
class CalculatorAbc(ABC):
def __init__(self, mode="basic"):
self.mode = mode
@abstractmethod
def add(self, *args, **kwargs):
raise NotImplementedError
@abstractmethod
def subtract(self, *args, **kwargs):
raise NotImplementedError
class Calculator(CalculatorAbc):
def add(self, a, b):
return a + b
def subtract(self, a, b):
return a - b
calc = Calculator()
print(calc.add(1, 2))
print(calc.mode)
Composition
# Composition: owned components live and die with the owner
# -----------------------------------------------------------------------------
# Composition means that an object is made up of other objects which it
# owns completely. When the container is destroyed so are its parts.
# The `Rocket` creates and manages its own `FuelTank`, which does not exist
# independently.
class FuelTank(object):
def __init__(self, level=100):
# Start the tank with a given fuel level
self.level = level
def fill(self, level):
self.level = level
def empty(self):
self.level = 0
class Rocket(object):
def __init__(self, fuel_level):
# This is composition: the Rocket creates and owns the FuelTank.
# When the rocket is destroyed the tank goes with it.
self.tank = FuelTank(fuel_level)
def launch(self):
if self.tank.level == 100:
print("Fuel tank is full")
else:
raise ValueError("Fuel tank is not full")
def refill(self, level):
self.tank.fill(level)
Dependency
# Dependency: relying on other classes to get work done
# -----------------------------------------------------------------------------
# A dependency is when a class uses another class to get its job done. The
# dependent class doesn't own the other object, it simply relies on it at
# runtime. Here the `Calculator` depends on the `Math` helper to perform the
# actual addition.
class Math(object):
@staticmethod
def add(a, b):
return a + b
class Calculator(object):
def __init__(self, model="default"):
self.model = model
def add(self, a, b):
# Dependency relationship
result = Math.add(a, b)
return result
calc = Calculator()
calc.add(1, 1)
Encapsulation
# Encapsulation: guarding internal state with getters and setters
# -----------------------------------------------------------------------------
# Encapsulation hides internal state behind methods. Properties provide a
# controlled interface so that validation logic in setters and getters can
# protect the data from invalid values or direct manipulation.
class Person(object):
"""Represents a person with controlled access to internal state."""
def __init__(self, name, age):
# Modify the access to the internal state
self.__name = name
self.__age = age
@property
def name(self):
""" The getter for the name """
return self.__name
@name.setter
def name(self, name):
""" The setter for the name """
# Protection logic
if name is None:
raise ValueError("name cannot be None")
self.__name = name
@property
def age(self):
""" The getter for the age """
return self.__age
@age.setter
def age(self, age):
""" The setter for the age """
# Protection logic
if age is None:
raise ValueError("age cannot be None")
elif age < 0:
raise ValueError("age cannot be negative")
elif age > 150:
raise ValueError("age cannot be greater than 150")
self.__age = age
person = Person("John", 30)
print(person.name)
print(person.age)
Inheritance
# Inheritance: deriving behavior from a base class
# -----------------------------------------------------------------------------
# Inheritance lets a class reuse and extend the behavior of a base class.
# Derived classes such as `Dog` and `Cat` inherit the methods from `Animal`
# and override them when needed.
class Animal(object):
def __init__(self, name):
self.name = name
def speak(self):
print("I am an animal")
def __str__(self):
return "Animal: {}".format(self.name)
class Dog(Animal):
def speak(self):
print("I am a dog")
def __str__(self):
return "Dog: {}".format(self.name)
class Cat(Animal):
def speak(self):
print("I am a cat")
def __str__(self):
return "Cat: {}".format(self.name)
animal = Animal("Animal")
animal.speak()
dog = Dog("Dog")
dog.speak()
cat = Cat("Cat")
cat.speak()
Polymorphism
# Polymorphism: shared interface, different implementations
# -----------------------------------------------------------------------------
# Polymorphism lets different classes implement the same interface in
# their own way. Subclasses of `Animal` provide specific versions of
# `talk` and `eat` while client code can treat them uniformly.
class Animal(object):
def __init__(self, name):
self.name = name
# Abstract method that supports overriding, but the linter will complain on overloading
def talk(self):
raise NotImplementedError
# Abstract method that supports both overriding and overloading
def eat(self, *args, **kwargs):
raise NotImplementedError
class Cat(Animal):
# Override the talk method
def talk(self):
print("Meow!")
def eat(self):
print("Cat is eating")
class Dog(Animal):
# Overload the talk method
def talk(self, name):
print("Woof!")
def eat(self, what):
print("Dog is eating {}".format(what))
cat = Cat("Kitty")
cat.talk()
cat.eat()
dog = Dog("Doggy")
dog.talk("Doggy")
dog.eat("bone")