Classes#

  • Classes provide a means of bundling data and functionality together.

  • Creating a new class creates a new type of object.

  • Assigned variables are new instances of that type.

  • Each class instance can have attributes attached to it.

  • Class instances can also have methods for modifying its state.

  • Python classes provide the class inheritance mechanism.

Use class to store data#

  • A empty class can be used to bundle together a few named data items.

  • You can easily save this class containing your data in JSON file.

class Car:
    pass

mycar = Car()  # Create an empty car record

# Fill the fields of the record
mycar.brand = 'Peugeot'
mycar.model = 308
mycar.year = 2015
mycar.__dict__
{'brand': 'Peugeot', 'model': 308, 'year': 2015}

namedtuple#

from collections import namedtuple

Car = namedtuple('Car', 'brand, model, year')
mycar = Car('Peugeot', 308, 2015)
mycar
Car(brand='Peugeot', model=308, year=2015)
mycar.year
2015
# Like tuples, namedtuples are immutable:
import sys
try:
    mycar.model = 3008
except:
    print(sys.exc_info()[0])
<class 'AttributeError'>
class Car:

    "A simple example class Animal with its name, weight and age"

    def __init__(self, brand, model, year):  # constructor
        self.brand = brand
        self.model = model
        self.year = year

    def age(self):  # method
        import datetime
        now = datetime.datetime.now()
        return now.year - self.year
mycar = Car('Peugeot', 308, 2015) # Instance
print(f' {mycar.brand} {mycar.model} {mycar.year}')
print(f' {mycar.age()} years old')
 Peugeot 308 2015
 8 years old
mycar.year = 2017
mycar.age()
6
  • mycar is an instance of Car Class.

  • mycar.age() is a method of Car instance mycar.

  • brand and model are attributes of Car instance mycar.

Convert method to attribute#

Use the property decorator

class Car:

    "A simple example class Car with its model, brand and year"

    def __init__(self, brand, model, year):  # constructor
        self.model = model
        self.brand = brand
        self.year = year

    @property
    def age(self):  # method
        import datetime
        now = datetime.datetime.now()
        return now.year - self.year
mycar = Car('Peugeot', 308, 2015)
mycar.age  # age can now be used as an attribute
8
try:
    mycar.age = 3 # a protected attribute
except:
    print(sys.exc_info()[0])
<class 'AttributeError'>

The new Python 3.7 DataClass#

from dataclasses import dataclass

@dataclass
class Car:

    brand: str
    model: int
    year: int

    @property
    def age(self) -> int:
        import datetime
        now = datetime.datetime.now()
        return now.year - self.year
mycar = Car('Peugeot', 308, 2015)
mycar
Car(brand='Peugeot', model=308, year=2015)
myothercar = Car('BMW', "1 Series", 2009)
myothercar
Car(brand='BMW', model='1 Series', year=2009)

Method Overriding#

  • Every Python classes has a __repr__() method used when you call print() function.

class Car:
    """Simple example class with method overriding """

    def __init__(self, brand, model, year):
        self.brand = brand
        self.model = model
        self.year = year

    def __repr__(self):
        return f"{self.year} {self.brand} {self.model} {self.__class__.__name__}"
mycar = Car('Peugeot', 308, 2015)
print(mycar)
2015 Peugeot 308 Car

Inheritance#

class Rectangle():  # Parent class is defined here

    def __init__(self, width, height):
        self.width, self.height = width, height
    @property
    def area(self):
        return self.width * self.height
    
class Square(Rectangle):
    
    def __init__(self, edge):
        super().__init__(edge, edge)  # Call method in the parent class


r = Rectangle(2, 3)
print(f"Rectangle area \t = {r.area:7.3f}")
s = Square(4)
print(f"Square area \t = {s.area:7.3f}")
Rectangle area 	 =   6.000
Square area 	 =  16.000

Private Variables and Methods#

class DemoClass:
    " Demo class for name mangling "

    def public_method(self):
        return 'public!'

    def __private_method(self):  # Note the use of leading underscores
        return 'private!'


object3 = DemoClass()
object3.public_method()
'public!'
try:
    object3.__private_method()
except:
    print(sys.exc_info()[0])
<class 'AttributeError'>
[ s for s in dir(object3) if "method" in s]
['_DemoClass__private_method', 'public_method']
object3._DemoClass__private_method()
'private!'
object3.public_method
<bound method DemoClass.public_method of <__main__.DemoClass object at 0x7f5a74b847c0>>

Use class as a Function.#

class Polynomial:
    
   " Class representing a polynom P(x) -> c_0+c_1*x+c_2*x^2+..."
    
   def __init__(self, coeffs):
      self.coeffs = coeffs
        
   def __call__(self, x):
      return sum(coef*x**exp for exp,coef in enumerate(self.coeffs))

p = Polynomial([2,4,-1])
p(2) 
6

Exercise: Polynomial#

  • Improve the class above called Polynomial by creating a method diff(n) to compute the nth derivative.

  • Override the __repr__() method to output a pretty printing.

Hint: f"{coeff:+d}" forces to print sign before the value of an integer.

Operators Overriding#

class MyComplex:
    " Simple class representing a complex"
    width = 7
    precision = 3

    def __init__(self, real=0, imag=0):
        self.real = real
        self.imag = imag

    def __repr__(self): 
        return (f"({self.real:{self.width}.{self.precision}f},"
                f"{self.imag:+{self.width}.{self.precision}f}j)")

    def __eq__(self, other):  # override '=='
        return (self.real == other.real) and (self.imag == other.imag)

    def __add__(self, other):  # override '+'
        return MyComplex(self.real+other.real, self.imag+other.imag)

    def __sub__(self, other):  # override '-'
        return MyComplex(self.real-other.real, self.imag-other.imag)

    def __mul__(self, other):  # override '*'
        if isinstance(other, MyComplex):
            return MyComplex(self.real * other.real - self.imag * other.imag,
                             self.real * other.imag + self.imag * other.real)

        else:
            return MyComplex(other*self.real, other*self.imag)
u = MyComplex(0, 1)
v = MyComplex(1, 0)
print('u=', u, "; v=", v)
u= (  0.000, +1.000j) ; v= (  1.000, +0.000j)
u+v, u-v, u*v, u==v
((  1.000, +1.000j), ( -1.000, +1.000j), (  0.000, +1.000j), False)

We can change the class attribute precision.

MyComplex.precision=2
print(u.precision)
print(u)
2
(   0.00,  +1.00j)
v.precision
2

We can change the instance attribute precision.

u.precision=1
print(u)
(    0.0,   +1.0j)
print(v)
(   1.00,  +0.00j)
MyComplex.precision=5
u # set attribute keeps its value
(    0.0,   +1.0j)
v # unset attribute is set to the new value
(1.00000,+0.00000j)

Rational example#

class Rational:
    " Class representing a rational number"

    def __init__(self, n, d):
        assert isinstance(n, int) and isinstance(d, int)

        def gcd(x, y):
            if x == 0:
                return y
            elif x < 0:
                return gcd(-x, y)
            elif y < 0:
                return -gcd(x, -y)
            else:
                return gcd(y % x, x)

        g = gcd(n, d)
        self.numer, self.denom = n//g, d//g

    def __add__(self, other):
        return Rational(self.numer * other.denom + other.numer * self.denom,
                        self.denom * other.denom)

    def __sub__(self, other):
        return Rational(self.numer * other.denom - other.numer * self.denom,
                        self.denom * other.denom)

    def __mul__(self, other):
        return Rational(self.numer * other.numer, self.denom * other.denom)

    def __truediv__(self, other):
        return Rational(self.numer * other.denom, self.denom * other.numer)

    def __repr__(self):
        return f"{self.numer:d}/{self.denom:d}"
r1 = Rational(2,3)
r2 = Rational(3,4)
r1+r2, r1-r2, r1*r2, r1/r2
(17/12, -1/12, 1/2, 8/9)

Exercise#

Improve the class Polynomial by implementing operations:

  • Overrides ‘+’ operator (add)

  • Overrides ‘-’ operator (neg)

  • Overrides ‘==’ operator (eq)

  • Overrides ‘*’ operator (mul)