Classes

Motivation

We aim at avoiding code like this:

import math

x = 2
y = 3
xList = [1, 2, 3, 4]
yList = [1, 2, 3, 4]

def norm2(x, y):
    return math.sqrt(self.x ** 2 + self.y ** 2)
  • Are variables x and y?
  • Are xList and yList together mean list of points?

A first answer: struct

Making a struct like in C by grouping data together.

import math

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

v = Vector2D(2, 3)
L = [Vector2D(1, 1), Vector2D(2, 2), Vector2D(3, 3), Vector2D(4, 4)]

def norm(v: Vector2D):
    return math.sqrt(v.x ** 2 + v.y ** 2)

norm(v)
3.605551275463989

A class = struct + methods

A class mixes both data (fields and attributes) and methods (= functions applied to data).

Advantages:

  • Functions are no more lost and the code is more organized (kind of namespace)
  • More obvious to know on what the method is applied on

This is called encapsulation.

import math

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

    @property
    def norm(self):
        return math.sqrt(self.x ** 2 + self.y ** 2)
    
    def scale(self, scalar):
        self.x *= scalar
        self.y *= scalar
v = Vector2D(2, 1)
v.scale(0.5)
print(v.norm)
v.scale(4)
print(v.norm)
print(v.x)
print(v.y)
1.118033988749895
4.47213595499958
4.0
2.0



---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

Cell In[8], line 8
      6 print(v.x)
      7 print(v.y)
----> 8 v.norm = 3


AttributeError: can't set attribute

v is (points to) an object. We say that v is an instance of the class* Vector2D. Of course, we can have several instances:

v = Vector2D(2, 1)
w = Vector2D(1, 1)

The concept of class (and of object-oriented programming) appears in many languages: C++, Java, Javascript, C#, etc.

Dataclasses

Problems

When we defined a class Vector2D, equalities is naïve.

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

Vector2D(1, 2) == Vector2D(1, 2) # oh no!!
False

Also printing a Vector2D is not very informative.

print(Vector2D(1, 2))
<__main__.Vector2D object at 0x7f69bc14aac0>

Cumbersome solution...

A cumbersome solution would be to write the magic method __eq__ ourselves.

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

    def __eq__(self, other):
        return (self.x == other.x) and (self.y == other.y)
    
Vector2D(2, 3) == Vector2D(2, 3)
True

In the same way, we can define the magic method __repr__ for having pretty printing.

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

    def __eq__(self, other):
        return (self.x == other.y) and (self.y == other.y)
    
    def __repr__(self):
        return f"(x={self.x}, y={self.y})"
    
print(Vector2D(1, 2))
(x=1, y=2)

Pythonic solution

Python provides dataclasses for that. See https://peps.python.org/pep-0557/. We do not have to write __eq__, but actually we do not have to write __init__, __repr__, etc. and many other standard magic functions. Look:

from dataclasses import dataclass

@dataclass
class Vector2D:
    """Class for representing a point in 2D."""
    x: float
    y: float
    
v = Vector2D(1, 2) 
w = Vector2D(1, 2)
print(v == w)
print(v)
True
Vector2D(x=1, y=2)

@dataclass is a class decorator.

Methods

A method is a function applied on an object and defined in a class. In object-oriented object, the goal is to have different objects interacting via message-passing. For instance speak asks the cat to speak.

class Cat():
    name = "Garfield"
    def speak(self):
        print(f"{self.name} says miaou.")

c = Cat()
c.speak()

c.name = "Felix"
c.speak()
Garfield says miaou.
Felix says miaou.

Of course, methods can have parameters than the object itself.

class Cat():
    name = "Garfield"
    def speak(self, strength):
        print(f"{self.name} says {'miaou' if strength < 5 else 'MIAOU'}.")

c = Cat()
d = Cat()
c.speak(2)
d.speak(5)
Garfield
graou says miaou.
Garfield says MIAOU.

Abstraction

Abstraction consists in hidding the low-level details of an object from the user of the object.

  • In some languages, e.g. C++ and Java, we have keywords like private VS public to explicitely say whether a field/method is private/public.
  • In Python, we implicitely inform the user that an attribute/method is private by starting its name with __ (and not finishing with __ otherwise you get a magic function 😉). See https://peps.python.org/pep-0008/
class Animal:
    def __init__(self, health):
        self.__health = health

    def showHealth(self):
        print(self.__health)

a = Animal(5)
a.showHealth()
a.__health
5



---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

Cell In[2], line 10
      8 a = Animal(5)
      9 a.showHealth()
---> 10 a.__health


AttributeError: 'Animal' object has no attribute '__health'

Actually, it creates a field _Animal__health. This is super ugly!

a._Animal__health
5

Exercice: Write a class for a grid graph created with an image.