Classes

Motivation

We aim at avoiding code where the data is scattered. For instance:

import math

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

def norm(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

The C language offers struct in order to group 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

But the functions applied to the data are still scattered.

A class = struct + methods

A class mixes both data (fields and attributes) and methods (= functions applied to data). This is called encapsulation. A class desfines a type of objects.

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
import math

class Vector2D:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        
    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

The variable v is (contains/points to) an object. We say that v is an instance of the class Vector2D. Of course, we can have several instances, and each instance contains its own data:

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.

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 says miaou.
Garfield says MIAOU.

Dataclasses

Dataclasses are specific to Python and helps to define classes without redefining the same `standard' methods over and over again.

Problems

When we defined a class Vector2D, equalities remain naïve by default.

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 0x7f7ff414ce80>

Cumbersome solution...

A cumbersome solution would be to write the magic method __eq__ ourselves in order to make the equality == work properly.

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 a nice 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 automatically define these standard behaviors. See https://peps.python.org/pep-0557/. We do not have to write __eq__, __repr__, ourselves anymore. Python automatically defines them. Even more, Python automatically defines __init__, etc. and many other standard magic functions for obtaining standard behaviors for free. 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.

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[26], 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.