<<

Department of Physics Object-Oriented Programming

Scientific Programming with Python Andreas Weiden

Based on talks by Niko Wilbert and Roman Gredig This work is licensed under the Creative Commons Attribution-ShareAlike 3.0 License.

24/06/2019 Page 1 Department of Physics

Outline

What is OOP?

Fundamental Principles of OOP

Specialities in Python

Science Examples

Design Patterns

24/06/2019 Page 2 Department of Physics

Setting the scene

Object-oriented programming is a programming paradigm.

I Imperative programming

I Object-oriented I Procedural

I Declarative programming

I Functional I Logic

24/06/2019 Page 3 Department of Physics

What is Object-Oriented Programming?

Aim to segment the program into instances of different classes of objects:

I Instance variables to describe the state of the object

I Methods to model the behaviour of the object The definition of a class can be considered like a blue print. The program will create instances of classes and execute methods of these instances.

24/06/2019 Page 4 Department of Physics

Why might OOP be a good idea?

DRY (Don’t repeat yourself): KIS (Keep it simple): OOP means to create the functionality of The OOP paradigm allows to split the classes once with the possibility to use functionality of programs into the basic them repeatedly in different programms. building blocks and the algorithm In addition inheritance in OOP allows us to invoking them. Thus it creates a natural easily create new classes by extending structure within your code. existing classes (see below).

At one point the problem to solve becomes so complicated that a single sequence of program instructions is not sufficient to effectively maintain the code.

24/06/2019 Page 5 Department of Physics

Example of a class

class Dog: I Started with class keyword. def __init__(self, color="brown"): self.color = color I Methods defined as functions in class scope with at least one argument def make_sound(self): (usually called self). print("Wuff!") I Special method __init__ called when # create an instance’snoopy’ of the class Dog snoopy = Dog() a new instance is created. I Always define your data attributes first # first argument(self) is bound # to this Dog instance in __init__. snoopy.make_sound()

# change snowy’s color snoopy.color ="yellow"

24/06/2019 Page 6 Department of Physics

Fundamental Principles of OOP (I)

Encapsulation In Python:

I Only what is necessary is exposed I No explicit declaration of (public interface) to the outside. variables/functions as private or public.

I Implementation details are hidden to I Usually parts supposed to be private provide abstraction. Abstraction should start with an underscore _.

not leak implementation details. I Python works with documentation and I Abstraction allows to break up a large conventions instead of enforcement. problem into understandable parts.

24/06/2019 Page 7 Department of Physics

Example of Encapsulation class Dog: def __init__(self, color="brown"): self.color = color I The author of the class Dog self._mood = 5 wants you to pat and beat the def _change_mood(self, change): dog to change its mood. self._mood += change self.make_sound() I Do not use the _mood variable or the _change_mood method def make_sound(self): directly. if self._mood < 0: print("Grrrr!") else: print("Wuff!")

def pat(self): self._change_mood(1)

def beat(self): self._change_mood(-2)

24/06/2019 Page 8 Department of Physics

Fundamental Principles of OOP (II)

Inheritance In Python:

I Define new classes as subclasses that I Inherit from one or multiple classes are derived from / inherit / extend a (latter one not recommended!)

parent class. I Invocation of parent methods with I Override parts with specialized super function.

behavior and extend it with additional I All classes are derived from object, functionality. even if this is not specified explicitly.

24/06/2019 Page 9 Department of Physics

Example of Inheritance class Mammal: from mammal import Mammal def __init__(self, color="grey"): self.color = color class Dog(Mammal): self._mood = 5 def __init__(self, color="brown"): super().__init__(color) def _change_mood(self, change): self._mood += change def make_sound(self): self.make_sound() if self._mood < 0: print("Grrrr!") def make_sound(self): else: raise NotImplementedError print("Wuff!")

def pat(self): I super().__init__(color) is the call self._change_mood(1) to the parent constructor.

def beat(self): I super allows also to explicitly access self._change_mood(-2) methods of the parent class.

I This is usually done when extending a method of the parent class.

24/06/2019 Page 10 Department of Physics

Fundamental Principles of OOP (III)

Polymorphism In Python:

I Different subclasses can be treated I Python is a dynamically typed like the parent class, but execute their language, which means that the type specialized behavior. (class) of a variable is only known when the code runs. I Example: When we let a mammal make a sound that is an instance of the I Duck Typing: No need to know the dog class, then we get a barking sound. class of an object if it provides the required methods: “When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck.”

I Type checking can be performed via the isinstance function, but generally prefer duck typing and polymorphism. 24/06/2019 Page 11 Department of Physics

Example of Polymorphism

from animals import Dog, Cat, Bear I caress would work for all objects having a method pat, def caress(mammal, number_of_pats): if isinstance(mammal, Bear): not just mammals. raise TypeError("Bad Idea!") isinstance(mammal, Bear) for_ in range(number_of_pats): I mammal.pat() checks if mammal is a bear. , c, b = Dog(), Cat(), Bear() I Dynamic typing makes proper caress(d, 3)#"Wuff!" (3x) function overloading caress(c, 3)#"Purr!" (3x) impossible! caress(b, 3)# raises TypeError

24/06/2019 Page 12 Department of Physics

Python Specialities – Magic Methods

class Dog: I Magic methods (full list here) def __init__(self, color="brown"): start and end with two self.color = color self._mood = 5 underscores (“dunder”). They customise standard def __repr__(self): I returnf"This isa{self.color} dog" Python behavior (e.g. string representation or operator snowy = Dog ("white") print(snowy)# This isa white dog definition).

24/06/2019 Page 13 Department of Physics

Python Specialities – Property class Dog: def __init__(self, color="brown"): self.color = color I property has upto four arguments: self._mood = 5 1. Getter def _get_mood(self): 2. Setter if self._mood < 0: 3. Deleter return"angry" 4. Documentation string return"happy" I Access calculated values as if they def _set_mood(self, value): were stored data attributes. if not -10 <= value <= 10: raise ValueError("Bad range!") I Define read-only “data attributes”. self._mood = value I Check if value assigned to “data mood = property(_get_mood, _set_mood) attribute” fullfills conditions. snowy = Dog ("white") I Can also be used as a Python print("Snowy is", snowy.mood)# Snowy is happy decorator. snowy.mood = -3 print("Snowy is", snowy.mood)# Snowy is angry 24/06/2019 Page 14 Department of Physics

Python Specialities – Property class Dog: def __init__(self, color="brown"): self.color = color I property has upto four arguments: self._mood = 5 1. Getter @property 2. Setter def mood(self): 3. Deleter if self._mood < 0: 4. Documentation string return"angry" return"happy" I Access calculated values as if they were stored data attributes. @mood.setter def mood(self, value): I Define read-only “data attributes”. if not -10 <= value <= 10: raise ValueError("Bad range!") I Check if value assigned to “data self._mood = value attribute” fulfils conditions.

# create an instance’snowy’ of the class Dog I Can also be used as a Python snowy = Dog ("white") decorator. print("Snowy is", snowy.mood) snowy.mood = 100 24/06/2019 Page 15 Department of Physics

Python Specialities – Classmethods

class Dog: I A classmethod takes as its first def __init__(self, name, color="brown"): argument a class instead of an instance self.name = name self.color = color of the class. It is therefore called cls self._mood = 5 instead of self.

@classmethod I The method should return an object of def from_string(cls, s): the class. name, *color = s.split(",") if color: I This allows you to write multiple return cls(name, color) constructors for a class, e.g.: return cls(name) I The default __init__ constructor. I One constructor from a serialized snowy = Dog.from_string("snowy,white") string. I One that reads it from a database or file. I ...

24/06/2019 Page 16 Department of Physics

Python Specialities – Class attributes

class Dog: I A class can also have attributes, not legs = 4 only an instance. all_dogs = set() I All instances have (at initialization) the def __init__(self, name, color="brown"): same attribute as the class. self.name = name self.color = color I If you change the attribute, only the self._mood = 5 Dog.all_dogs.add(self) attribute of the instance changes. I Beware if the class attribute is def __repr__(self): return self.name mutable! In this case inplace operations change the class attribute, snowy = Dog ("snowy","white") which is visible in all instances. This snowy.legs = 3 print(Dog.legs, snowy.legs)#43 can be a good or bad thing. print(Dog.all_dogs)#{snowy}

24/06/2019 Page 17 Department of Physics

Advanced OOP Techniques

There many advanced techniques that we didn’t cover:

I Multiple inheritance: Deriving from multiple classes; it can create a real mess. Need to understand the Method Resolution Order (MRO) to understand super.

I Monkey patching: Modify classes and objects at runtime, e.g. overwrite or add methods.

I Abstract Base Classes: Enforce that derived classes implement particular methods from the base class.

I Metaclasses: (derived from type), their instances are classes.

I Great way to dig yourself a hole when you think you are clever.

I Try to avoid these, in most cases you would regret it. (KIS)

24/06/2019 Page 18 Department of Physics

Science Examples – Vector

class Vector3D: from vector import Vector3D def __init__(self, x, y, z): self.x, self.y, self.z = x, y, z v1 = Vector3D(0, 1, 2) v2 = Vector3D(1,-3, 0) def __add__(self,other): v3 = v1 + v2 return Vector3D(self.x+other.x, print(v3.length)# 3.0 self.y+other.y, self.z+other.z) @property I Variable type with optimized def length(self): return (self.x**2+self.y**2 behaviour. +self.z**2)**0.5 I Add custom functionality

24/06/2019 Page 19 Department of Physics

Science Examples – Dataset import numpy as np class Dataset: from dataset import Dataset mandatory_metadata = ["label","color","marker"] def __init__(self, datafile, **): ds = Dataset("data_0.csv", for key in self.mandatory_metadata: label ="calibration", if key not in metadata: color ="r", raise KeyError("Missing metadata", key) marker ="+") self.metadata = metadata print(ds.label) self.data = np.loadtxt(datafile, delimiter=",") self.validate() I Store additional info with data. def validate(self): if self.data.shape != (4, 10): I Validate data on load. raise ValueError("Bad shape of data.") I Calculated specific quantities. @property def label(self): return self.metadata["label"]

def peak_row(self): 24/06/2019 return self.data.max(axis=1).argmax() Page 20 Department of Physics

Science Examples – Sensors from urllib.request import urlopen class Sensor: from sensors import WebSensor def __init__(self, offset=0, scale_factor=1): self.offset = offset sensor = WebSensor( self.scale = scale_factor "https://crbn.ch/sensor", 273 ) def get_value(self): print(sensor.get_value()) return (self._get_raw()+self.offset)*self.scale

def _get_raw(self): I Store configuration with raise NotImplementedError functionality. class WebSensor(Sensor): def __init__(self, url, *args, **kwargs): I Allow sensors with different super().__init__(*args, **kwargs) access methods. self._url = url

def _get_raw(self): res = urlopen(self._url) return float(res.read()) 24/06/2019 Page 21 Department of Physics

Science Examples – Value with Uncertainty

from numpy import sqrt from uncertval import UncertVal class UncertVal: a = UncertVal(2, 0.3) def __init__(self, value, uncertainty=0): b = UncertVal(3, 0.4) self.val = value print(a+b)#5 +/- 0.5 self.sd = uncertainty

def __str__(self): I Group several values. returnf"{self.val} +/-{self.uc}" I Manage access to values. def add(self, other, corr=0): return UncertVal(self.val+other.val, I Define operators respecting sqrt(self.sd**2 + other.sd**2 + relations between values. 2* self.sd * other.sd * corr))

def __add__(self, other): return self.add(other)

24/06/2019 Page 22 Department of Physics

Object-Oriented Design Principles and Patterns

How to do Object-Oriented Design right: How design principles can help:

I KIS & iterate: When you see the same I Design principles tell you in an abstract pattern for the third time it might be a way what a good design should look good time to create an abstraction like (most come down to loose (refactor). coupling).

I Sometimes it helps to sketch with pen I Design Patterns are concrete solutions and paper. for reoccurring problems.

I Classes and their inheritance often have no correspondence to the real-world, be pragmatic instead of perfectionist.

I Testability (with unittests) is a good design criterium.

24/06/2019 Page 23 Department of Physics

Some Design Principles

Scope of classes: How to design (programming) interfaces: I One class = one single clearly defined responsibility. I Principle of least knowledge. Each unit should have only limited I Favor composition over inheritance. Inheritance is not primarily intended knowledge about other units. Only talk for code reuse, its main selling point is to your immediate friends. polymorphism. “Do I want to use these I Minimize the surface area of the subclasses interchangeably?” interface.

I Identify the aspects of your I Program to an interface, not an application that vary and separate implementation. Do not depend upon them from what stays the same. concrete classes. Classes should be “open for extension, closed for modification” (Open-Closed Principle). 24/06/2019 Page 24 Department of Physics

Design Patterns

Purpose & background: Examples:

I Idea of concrete design approach for I Decorator pattern recurring problems. I Strategy pattern I Closely related to the rise of the I Factory pattern traditional OOP languages C++ and ... Java. I A comprehensive list can be found here. I More important for compiled languages (Open-Closed principle stricter!) and those with stronger enforcement of encapsulation.

24/06/2019 Page 25 Department of Physics

Decorator Pattern

24/06/2019 Page 26 Department of Physics

Decorator Pattern – Motivation

Challenge: class Beverage: I How to modify the behaviour # imagine some attributes like of an individual object . . . # temperature, amount left,... name ="beverage" I . . . and allowing for multiple cost = 0.00 modifications. def __str__(self): Example: Implement a range of return self.name products of a coffee house chain class Coffee(Beverage): name ="coffee" But what about the beloved cost = 3.00 add-ons? class Tea(Beverage): name ="tea" ...

24/06/2019 Page 27 Department of Physics

Decorator Pattern – First try class Coffee(Beverage): name ="coffee" Solution: cost = 3.00 I Implementation via class CoffeeWithMilk(Coffee): subclasses name ="coffee with milk" Issue: Number of subclasses cost = 3.20 explodes to allow for multiple class CoffeeWithSugar(Coffee): modifications (e.g. name ="coffee with sugar" CoffeeWithMilkAndSugar). ...

24/06/2019 Page 28 Department of Physics

Decorator Pattern – Second try class Coffee(Beverage): def __init__(self, milk=False, sugar=False): Solution: self._with_milk = milk self._with_sugar = sugar I Implementation with switches Issue: No additional add-ons def __str__(self): implementable without changing desc ="coffee" if self._with_milk: the class (violation of the desc +=", with milk" open-close principle!). if self._with_sugar: desc +=", with sugar" return desc

@property def cost(self): price = 3.00 if self._with_milk: price += 0.20 if self._with_sugar: price += 0.30 return price 24/06/2019 Page 29 Department of Physics

Decorator Pattern – Implementation

Solution: class DecoratedBeverage(Beverage): I Create a class that is a def __init__(self, beverage): beverage and wraps a self.beverage = beverage beverage itself. class Milk(DecoratedBeverage): def __str__(self): I Possibility to create a chain of return str(self.beverage) +", with milk" decorators. @property I Composition solves the def cost(self): problem. return self.beverage.cost + 0.30

I Downside: Need to coffee_with_milk = Milk(Coffee()) implement all functions (some are potentially just fed through the decorator).

24/06/2019 Page 30

Do not confuse the decorator pattern with Python’s function decorators! Department of Physics

Strategy Pattern

24/06/2019 Page 31 Department of Physics

Strategy Pattern – Motivation (I)

Let’s implement a duck . . . class Duck: def __init__(self): # for simplicity this example # class is stateless def quack(self): print("Quack!")

def display(self): print("Boring looking duck.")

def take_off(self): print("Run fast, flap wings.")

def fly_to(self, destination): print("Fly to", destination)

def land(self): print("Extend legs, touch down.")

24/06/2019 Page 32 Department of Physics

Strategy Pattern – Motivation (II)

. . . and different types of ducks! class RedheadDuck(Duck): Oh, no! The rubber duck should def display(self): print("Duck witha read head.") not fly! We need to overwrite all the methods about flying. class RubberDuck(Duck): def quack(self): I What if we want to introduce a print("Squeak!") DecoyDuck as well? def display(self): I What if a normal duck suffers print("Small yellow rubber duck.") a broken wing? ⇒ It makes more sense to abstract the flying behaviour.

24/06/2019 Page 33 Department of Physics

Strategy Pattern – Implementation (I)

I Create a class to class FlyingBehavior: describe the flying def take_off(self): print("Run fast, flap wings.") behaviour . . . def fly_to(self, destination): . . . give Duck an print("Fly to", destination) I def land(self): instance of it . . . print("Extend legs, touch down.")

I . . . and handle all class Duck: the flying stuff via def __init__(self): this instance self.flying_behavior = FlyingBehavior() def take_off(self): self.flying_behavior.take_off() def fly_to(self, destination): self.flying_behavior.fly_to(destination) def land(self): self.flying_behavior.land() # display, quack as before...

24/06/2019 Page 34 Department of Physics

Strategy Pattern – Implementation (II)

I Other example of class NonFlyingBehavior(FlyingBehavior): composition over def take_off(self): print("It’s not working:-(") inheritance. def fly_to(self, destination): Encapsulation of raise Exception("I’m not flying.") I def land(self): function print("That won’t be necessary.") implementation in class RubberDuck(Duck): def __init__(self): the strategy object. self.flying_behavior = NonFlyingBehavior() def quack(self): I Useful pattern to print("Squeak!") e.g. define def display(self): optimisation print("Small yellow rubber duck.") algorithm at class DecoyDuck(Duck): def __init__(self): runtime. self.flying_behavior = NonFlyingBehavior() # different display, quack implementation...

24/06/2019 Page 35 Department of Physics

Take-aways

I Object-oriented programming offers a powerful pradigm to structure your code.

I Inheritance, design principles and patterns allow to avoid repetitions (DRY).

I But do not overcomplicate things and always ask yourself if applying a particular functionality makes sense in the given context!

24/06/2019 Page 36 Department of Physics

Extra Department of Physics

Stop Writing Classes?

There are good reasons for not writing classes:

I A class is a tightly coupled piece of code, can be an obstacle for change. Complicated inheritance hierarchies hurt.

I Tuples can be used as simple data structures, together with stand-alone functions.

I Introduce classes later, when the code has settled.

I Functional programming can be very elegant for some problems, coexists with object oriented programming.

(see “Stop Writing Classes” by Jack Diederich)

24/06/2019 OOP Page 38 Department of Physics

Functional Programming

There are good reasons for not writing classes:

I Pure functions have no side effects. (mapping of arguments to return value, nothing else)

I Great for parallelism and distributed systems. Also great for unittests and TDD (Test Driven Development).

I It’s interesting to take a look at functional programming languages (e.g. Haskell, J) to get a fresh perspective.

24/06/2019 OOP Page 39 Department of Physics

Functional Programming in Python

Python supports functional def get_hello(name): programming to some extend: return"hello" + name Functions are just objects, a = get_hello I print(a("world"))# prints"hello world" pass them around! def apply_twice(f, x): I Functions can be nested and return f(f(x)) remember their context at the print(apply_twice(a,"world")) time of creation (closures, # prints"hello hello world" nested scopes). def get_add_n(n): def _add_n(x): returnx+n return _add_n add_2 = get_add_n(2) add_3 = get_add_n(3) add_2 (1)# returns3 add_3 (1)# returns4

24/06/2019 OOP Page 40