Programming in Python

Creating Your Own Data Types

Gerald Senarclens de Grancy

Object Oriented Programming (OOP)

Disclaimer - this is not an OOP course!

  • In the real world, there is an infinite variation of data types
  • OOP allows you to model your own
  • We have used objects extensively, but our code was procedural
  • Most modern programming languages allow object orientation

Problems of a purely procedural approach

    Using a tuple or list, it isn't obvious what the elements represent

    cirle = (25, 15, 7)

    Using a dictionary, data integrity is not guaranteed

    circle = {"x": 25, "y": 15, "radius": -7}  # invalid radius
    circle["y"] = "fifteen"  # invalid y coordinate

    Inability to guarantee the validity of an object is likely the worst downside of purely procedural approaches

Purpose and Advantages

  • Like dictionaries, classes keep related data in one place
  • They define behavior by associating methods with your data
  • And can ensure the integrity and coherence of your data
  • By hiding data and using methods for valid operations (encapsulation)
  • User defined classes can be composed of existing classes (composition)
  • Inheritance allows using existing classes as basis for new classes
  • Facilitates maintenance by providing re-usable classes
  • Doing OOP almost always simplifies non-trivial programs

Classes

  • User defined data types
  • Heart of object oriented programming
  • Provides custom data members ("fields") (resembling dictionaries)
  • Behavior is defined by the class' methods
  • Classes can be thought of as object factories (or templates)
class <identifier>:
    <suite>

Class Instance | Class Object | Object

  • Variables with the type of a class are called objects
  • Like modules, classes and objects also serve as namespaces
  • Objects are created by "calling" the class' name
new_object = ClassName()

Arguments to instantiate a Python class are the parameters to its __init__(.) method (excluding self)

new_object = ClassName(arg1, arg2, ...)

Defining Python Classes

Python uses special methods for class operations

By convention, only special methods start and end with two underscores

#!/usr/bin/env python3
class Circle:
    def __init__(self, x, y, radius):  # special method to initialize object
        self.x = x  # self is a reference to the current instance
        self.y = y
        self.radius = radius

    def __str__(self):  # special method to represent object as string
        return (f'Circle(x={self.x}, y={self.y}, radius={self.radius})')


# an instance is created by "calling" the class with its required parameters
a_circle = Circle(25, 15, 7)  # ok
print(a_circle)
another_circle = Circle(25, 15, -7)  # still invalid
print(another_circle)

Visualize execution

Method

  • Function associated with a class
  • Public methods constitute the class' interface
  • This public interface defines what one can do with instances

Defining Methods in Python

#!/usr/bin/env python3
"""
Implementation of a Circle class offering area and circumference methods.
"""
import math


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

    def __str__(self):
        return (f'Circle(x={self.x}, y={self.y}, radius={self.radius})')

    def area(self):
        """Return the area of the circle."""
        return math.pi * self.radius * self.radius

    def circumference(self):
        """Return the circumference of the circle."""
        return 2 * math.pi * self.radius


if __name__ == '__main__':
    a_circle = Circle(25, 15, 7)
    print(a_circle)
    print(f'area: {a_circle.area():.2f}, '
          f'circumference: {a_circle.circumference():.2f}')

Visualize execution

Encapsulation

Allows to restrict access to internal data

Data can only be changed by using methods that guarantee valid operations

Python provides @property decorators for encapsulting data fields

Limitation: Python does not offer a bulletproof way of controlling data access

Example: Encapsulation

#!/usr/bin/env python3

"""
Implementation of a Circle class offering area and circumference methods.
"""
import math


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

    def __str__(self):
        return (f'Circle(x={self.x}, y={self.y}, radius={self.radius})')

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

    @radius.setter
    def radius(self, radius):
        """Ensure that `radius` is valid."""
        assert radius >= 0, 'radius must be greater than or equal 0'
        self.__radius = radius

    def area(self):
        """Return the area of the circle."""
        return math.pi * self.radius * self.radius

    def circumference(self):
        """Return the circumference of the circle."""
        return 2 * math.pi * self.radius


if __name__ == '__main__':
    a_circle = Circle(25, 15, 7)  # ok
    print(a_circle)
    another_circle = Circle(25, 15, -7)  # not allowed anymore

Visualize execution

Aggregation/ Composition

  • User defined classes can be composed of existing classes
  • Subobjects are components of their parent object
  • Models has-a relationships
  • E.g. a graph consists of vertices and edges

Example: Aggregation/ Composition

Using Point as center of a Circle

#!/usr/bin/env python3

import math


class Point:
    def __init__(self, x, y):
        self.__x = x
        self.__y = y

    def __str__(self):
        return (f'Point(x={self.x}, y={self.y})')

    @property
    def x(self):
        return self.__x

    @property
    def y(self):
        return self.__y


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

    def __str__(self):
        return (f'Circle(center={self.center}, radius={self.radius})')

    @property
    def center(self):
        return self.__center

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

    @radius.setter
    def radius(self, radius):
        """Ensure that `radius` is valid."""
        assert radius >= 0, 'radius must be greater than or equal 0'
        self.__radius = radius

    def area(self):
        """Return the area of the circle."""
        return math.pi * self.radius * self.radius

    def circumference(self):
        """Return the circumference of the circle."""
        return 2 * math.pi * self.radius


if __name__ == "__main__":
    a_circle = Circle(Point(25, 15), 7)
    print(a_circle.center)
    print(a_circle)

Visualize execution

Inheritance

  • Allows using existing classes as basis for new classes
    subtypes are established from existing classes
  • A parent class is usually referred to as base or super class
    Child classes can also be referred to as sub- or derived classes
  • Allows to specialize (subclass) a common base class
  • Models is-a relationships
    e.g. a directed graph is a graph, a circle is a shape
  • Methods defined in the super class can be used in the subclass
  • Methods can be overridden - they can be reimplemented in the subclass
  • Built-in super() function accesses overridden base class methods

Example: Inheritance

Circles and Triangles are Shapes

#!/usr/bin/env python3

import collections
import math


class Shape:
    """Every shape has an area and a circumference."""
    def area(self):
        raise NotImplementedError

    def circumference(self):
        raise NotImplementedError


class Point:
    def __init__(self, x, y):
        self.__x = x
        self.__y = y

    def __str__(self):
        return (f'Point(x={self.x}, y={self.y})')

    @property
    def x(self):
        return self.__x

    @property
    def y(self):
        return self.__y

    def distance(self, other):
        """Return the euclidean distance between self and other."""
        delta_x = self.x - other.x
        delta_y = self.y - other.y
        return math.hypot(delta_x, delta_y)


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

    def __str__(self):
        return ('Circle(center={self.center}, radius={self.radius})')

    @property
    def center(self):
        return self.__center

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

    @radius.setter
    def radius(self, radius):
        """Ensure that `radius` is valid."""
        assert radius >= 0, 'radius must be greater than or equal 0'
        self.__radius = radius

    def area(self):
        """Return the area of the circle."""
        return math.pi * self.radius * self.radius

    def circumference(self):
        """Return the circumference of the circle."""
        return 2 * math.pi * self.radius


class Triangle(Shape):
    def __init__(self, A, B, C):
        self.A = A
        self.B = B
        self.C = C

    def __str__(self):
        return f'Triangle({self.A}, {self.B}, {self.C})'

    def circumference(self):
        return (self.A.distance(self.B) + self.B.distance(self.C) +
                self.C.distance(self.A))

    def area(self):
        """
        Return the area of a triangle.

        The area is defined as area = base * height / 2. Alternatively,
        a * b * sin(gamma) / 2 can be used.
        See http://en.wikipedia.org/wiki/Triangle#Using_trigonometry and
        http://en.wikipedia.org/wiki/Angle#Dot_product_and_generalisation.
        """
        a = self.C.distance(self.B)
        b = self.C.distance(self.A)
        Vector = collections.namedtuple('Vector', ['x', 'y'])
        CB = Vector(self.B.x - self.C.x, self.B.y - self.C.y)
        CA = Vector(self.A.x - self.C.x, self.A.y - self.C.y)
        CA_dot_CB = CA.x * CB.x + CA.y * CB.y
        gamma = math.acos(CA_dot_CB / (a * b))
        return a * b * math.sin(gamma) / 2


if __name__ == '__main__':
    a_triangle = Triangle(Point(0, 0), Point(4, 0), Point(0, 3))
    a_circle = Circle(Point(25, 15), 7)
    shapes = [a_triangle, a_circle]
    for shape in shapes:
        print(shape)
        print(f'area: {shape.area()}, circumference: {shape.circumference()}')

Visualize execution

Polymorphism

  • Objects of a subclass can be used wherever a base class object is expected
  • E.g. a circle does everything a shape does and more

Example: Polymorphism

Circles and Triangles are Shapes

#!/usr/bin/env python3

import collections
import math


class Shape:
    """Every shape has an area and a circumference."""
    def area(self):
        raise NotImplementedError

    def circumference(self):
        raise NotImplementedError


class Point:
    def __init__(self, x, y):
        self.__x = x
        self.__y = y

    def __str__(self):
        return (f'Point(x={self.x}, y={self.y})')

    @property
    def x(self):
        return self.__x

    @property
    def y(self):
        return self.__y

    def distance(self, other):
        """Return the euclidean distance between self and other."""
        delta_x = self.x - other.x
        delta_y = self.y - other.y
        return math.hypot(delta_x, delta_y)


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

    def __str__(self):
        return ('Circle(center={self.center}, radius={self.radius})')

    @property
    def center(self):
        return self.__center

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

    @radius.setter
    def radius(self, radius):
        """Ensure that `radius` is valid."""
        assert radius >= 0, 'radius must be greater than or equal 0'
        self.__radius = radius

    def area(self):
        """Return the area of the circle."""
        return math.pi * self.radius * self.radius

    def circumference(self):
        """Return the circumference of the circle."""
        return 2 * math.pi * self.radius


class Triangle(Shape):
    def __init__(self, A, B, C):
        self.A = A
        self.B = B
        self.C = C

    def __str__(self):
        return f'Triangle({self.A}, {self.B}, {self.C})'

    def circumference(self):
        return (self.A.distance(self.B) + self.B.distance(self.C) +
                self.C.distance(self.A))

    def area(self):
        """
        Return the area of a triangle.

        The area is defined as area = base * height / 2. Alternatively,
        a * b * sin(gamma) / 2 can be used.
        See http://en.wikipedia.org/wiki/Triangle#Using_trigonometry and
        http://en.wikipedia.org/wiki/Angle#Dot_product_and_generalisation.
        """
        a = self.C.distance(self.B)
        b = self.C.distance(self.A)
        Vector = collections.namedtuple('Vector', ['x', 'y'])
        CB = Vector(self.B.x - self.C.x, self.B.y - self.C.y)
        CA = Vector(self.A.x - self.C.x, self.A.y - self.C.y)
        CA_dot_CB = CA.x * CB.x + CA.y * CB.y
        gamma = math.acos(CA_dot_CB / (a * b))
        return a * b * math.sin(gamma) / 2


if __name__ == '__main__':
    a_triangle = Triangle(Point(0, 0), Point(4, 0), Point(0, 3))
    a_circle = Circle(Point(25, 15), 7)
    shapes = [a_triangle, a_circle]
    for shape in shapes:
        print(shape)
        print(f'area: {shape.area()}, circumference: {shape.circumference()}')

Visualize execution

Summary

  • User defined types are available using classes
  • Classes are instantiated with the same syntax like calling functions
  • Arguments to instantiate a Python class are the parameters to its __init__(.) method (excluding self)
  • If you want to understand and properly use classes, you need a designated textbook!

Questions
and feedback...

Literature

Paul Barry and David Griffiths Head First Programming O'Reilly (2009)
Mark Pilgrim Dive Into Python 3 (2nd edition) Apress (October 23, 2009)
Python Software Foundation Python Documentation http://docs.python.org/3/