Lab 2

De la WikiLabs
Jump to navigationJump to search

Lab 2 — Object-Oriented Programming in Python

Objectives

This laboratory introduces object-oriented programming in Python to students who already know object-oriented programming in C++.

The goal is not to relearn classes, inheritance, encapsulation or polymorphism, but to understand how these concepts are expressed in Python and how Python's object model differs from C++.

After completing this laboratory, you should be able to:

  • define and instantiate Python classes;
  • understand the role of self;
  • distinguish between instance and class attributes;
  • define constructors and methods;
  • understand Python's conventions for public, protected and private members;
  • use properties;
  • use class methods and static methods;
  • use inheritance and super();
  • understand method overriding and duck typing;
  • prefer composition where appropriate;
  • use @dataclass;
  • implement common special methods such as __str__, __repr__, __eq__, __len__ and __iter__;
  • recognize common C++ habits that should not be translated directly into Python.

Throughout this laboratory, familiar C++ constructs are presented next to their Python equivalents.

1. Defining a class

A simple class declaration is similar conceptually, but Python uses class followed by an indented block.

C++ Python
class Student {
public:
    std::string name;
    int age;
};
class Student:
    pass

The Python class above does not yet define any attributes. In Python, instance attributes are commonly created in the constructor.

2. Creating objects

Creating an object is straightforward in both languages.

C++ Python
Student student;
student = Student()

In Python, the class itself is called like a function to create a new instance.

3. Constructors

In C++, constructors have the same name as the class.

Python uses the special method __init__().

C++ Python
class Student {
public:
    Student(
        const std::string& name,
        int age
    )
        : name(name),
          age(age)
    {
    }

    std::string name;
    int age;
};
class Student:
    def __init__(self, name, age):
        self.name = name
        self.age = age

An object can then be created as follows:

C++ Python
Student student("Alice", 21);
student = Student("Alice", 21)

4. The meaning of self

The first parameter of an instance method is conventionally named self.

It refers to the current object.

Conceptually, it plays a role similar to this in C++.

C++ Python
class Counter {
public:
    void increment()
    {
        this->value++;
    }

private:
    int value = 0;
};
class Counter:
    def __init__(self):
        self.value = 0

    def increment(self):
        self.value += 1

There is one important syntactic difference:

C++ Python
this is implicit in the method parameter list self is explicitly written as the first parameter
this->value self.value

When calling the method, self is not supplied explicitly:

C++ Python
counter.increment();
counter.increment()

Python automatically passes the object as the first argument.

5. Instance attributes

Instance attributes belong to individual objects.

C++ Python
class Student {
public:
    Student(std::string name)
        : name(name)
    {
    }

    std::string name;
};
class Student:
    def __init__(self, name):
        self.name = name

Each instance has its own value:

C++ Python
Student a("Alice");
Student b("Bob");

std::cout << a.name << "\n";
std::cout << b.name << "\n";
a = Student("Alice")
b = Student("Bob")

print(a.name)
print(b.name)

6. Class attributes

Python class attributes are shared by all instances of the class.

They are similar to static data members in C++.

C++ Python
class Student {
public:
    static int count;

    Student()
    {
        count++;
    }
};

int Student::count = 0;
class Student:
    count = 0

    def __init__(self):
        Student.count += 1

Usage:

C++ Python
Student a;
Student b;

std::cout << Student::count;
a = Student()
b = Student()

print(Student.count)

Class attributes should normally be accessed through the class when they represent class-wide state.

7. Instance methods

Instance methods operate on one particular object.

C++ Python
class Rectangle {
public:
    Rectangle(double width, double height)
        : width(width), height(height)
    {
    }

    double area() const
    {
        return width * height;
    }

private:
    double width;
    double height;
};
class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

Calling the method:

C++ Python
Rectangle r(3.0, 4.0);

std::cout << r.area();
r = Rectangle(3.0, 4.0)

print(r.area())

8. Public, protected and private members

C++ enforces access control through public, protected and private.

Python primarily uses naming conventions.

Meaning C++ Python convention
public public: name
internal / protected-like protected: _name
private private: __name

Example:

C++ Python
class Account {
public:
    std::string owner;

protected:
    int account_id;

private:
    double balance;
};
class Account:
    def __init__(self):
        self.owner = ""
        self._account_id = 0
        self.__balance = 0.0

A single leading underscore means:

This member is intended for internal use.

It is a convention, not an access restriction.

9. Name mangling

Names beginning with two underscores are transformed internally by Python.

C++ Python
class Account {
private:
    double balance = 0.0;
};
class Account:
    def __init__(self):
        self.__balance = 0.0

Python internally transforms:

__balance

into a name similar to:

_Account__balance

This mechanism is called name mangling.

It is primarily designed to avoid accidental name conflicts in subclasses. It is not a security mechanism.

10. Getters and setters

A direct C++ translation might use explicit getter and setter methods.

C++ Python — direct style
class Person {
public:
    int getAge() const
    {
        return age;
    }

    void setAge(int value)
    {
        if (value < 0) {
            throw std::invalid_argument(
                "age cannot be negative"
            );
        }

        age = value;
    }

private:
    int age = 0;
};
class Person:
    def __init__(self):
        self._age = 0

    def get_age(self):
        return self._age

    def set_age(self, value):
        if value < 0:
            raise ValueError(
                "age cannot be negative"
            )

        self._age = value

The Python version works, but explicit getters and setters are usually not the preferred interface.

11. Properties

Python properties provide controlled attribute access while keeping normal attribute syntax.

C++ Python
class Person {
public:
    int getAge() const
    {
        return age;
    }

    void setAge(int value)
    {
        if (value < 0) {
            throw std::invalid_argument(
                "age cannot be negative"
            );
        }

        age = value;
    }

private:
    int age = 0;
};
class Person:
    def __init__(self):
        self._age = 0

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, value):
        if value < 0:
            raise ValueError(
                "age cannot be negative"
            )

        self._age = value

Usage:

C++ Python
Person p;

p.setAge(21);

std::cout << p.getAge();
p = Person()

p.age = 21

print(p.age)

The Python interface looks like direct attribute access even though validation is performed internally.

12. Do not create properties unnecessarily

In C++, private data members with getters and setters are common.

In Python, plain public attributes are acceptable when no validation or additional behavior is required.

C++ style Python style
class Point {
public:
    double getX() const
    {
        return x;
    }

    void setX(double value)
    {
        x = value;
    }

private:
    double x = 0.0;
};
class Point:
    def __init__(self, x=0.0):
        self.x = x

A property can always be introduced later without changing how callers access the attribute.

13. Static methods

A static method does not operate on a particular instance.

C++ Python
class Math {
public:
    static int square(int x)
    {
        return x * x;
    }
};
class Math:
    @staticmethod
    def square(x):
        return x * x

Usage:

C++ Python
int result = Math::square(5);
result = Math.square(5)

A static method receives neither self nor cls automatically.

14. Class methods

Python also provides class methods.

A class method receives the class itself as its first argument, conventionally called cls.

A common use is defining alternative constructors.

C++ Python
class Point {
public:
    Point(double x, double y)
        : x(x), y(y)
    {
    }

    static Point origin()
    {
        return Point(0.0, 0.0);
    }

private:
    double x;
    double y;
};
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    @classmethod
    def origin(cls):
        return cls(0.0, 0.0)

Usage:

C++ Python
Point p = Point::origin();
p = Point.origin()

Using cls rather than the explicit class name makes the method behave correctly when inherited by subclasses.

15. Instance method vs class method vs static method

Method type First automatic argument Typical purpose
instance method self operate on an object
class method cls operate on the class / alternative constructors
static method none utility logically associated with the class

Example:

class Example:
    def instance_method(self):
        ...

    @classmethod
    def class_method(cls):
        ...

    @staticmethod
    def static_method():
        ...

16. Inheritance

Inheritance is declared by placing the base class in parentheses.

C++ Python
class Animal {
public:
    void eat()
    {
        std::cout << "Eating\n";
    }
};

class Dog : public Animal {
};
class Animal:
    def eat(self):
        print("Eating")


class Dog(Animal):
    pass

The subclass inherits members from the base class.

C++ Python
Dog dog;
dog.eat();
dog = Dog()
dog.eat()

17. Calling the base-class constructor

C++ usually invokes a base-class constructor through the initializer list.

Python commonly uses super().

C++ Python
class Person {
public:
    Person(std::string name)
        : name(name)
    {
    }

protected:
    std::string name;
};

class Student : public Person {
public:
    Student(
        std::string name,
        int year
    )
        : Person(name),
          year(year)
    {
    }

private:
    int year;
};
class Person:
    def __init__(self, name):
        self.name = name


class Student(Person):
    def __init__(self, name, year):
        super().__init__(name)
        self.year = year

18. Method overriding

A subclass can provide its own implementation of an inherited method.

C++ Python
class Animal {
public:
    virtual void speak() const
    {
        std::cout << "Animal\n";
    }
};

class Dog : public Animal {
public:
    void speak() const override
    {
        std::cout << "Woof\n";
    }
};
class Animal:
    def speak(self):
        print("Animal")


class Dog(Animal):
    def speak(self):
        print("Woof")

Python does not require virtual or override keywords.

Method lookup is dynamic.

19. Polymorphism

The same interface can be used with objects of different classes.

C++ Python
void makeSpeak(const Animal& animal)
{
    animal.speak();
}

Dog dog;
makeSpeak(dog);
def make_speak(animal):
    animal.speak()


dog = Dog()
make_speak(dog)

In Python, the object does not necessarily need to inherit from a particular base class.

20. Duck typing

Python often relies on duck typing.

If an object provides the operations that are required, its exact class may not matter.

C++ — inheritance-oriented Python — duck typing
class Speaker {
public:
    virtual void speak() const = 0;
};

void makeSpeak(const Speaker& s)
{
    s.speak();
}
def make_speak(obj):
    obj.speak()

For example:

class Dog:
    def speak(self):
        print("Woof")


class Robot:
    def speak(self):
        print("Beep")


make_speak(Dog())
make_speak(Robot())

Dog and Robot do not need a common Python base class for this function to work.

21. isinstance()

When an explicit runtime type check is actually required, Python provides isinstance().

C++ Python
Animal* animal = ...;

if (dynamic_cast<Dog*>(animal) != nullptr) {
    // It is a Dog.
}
if isinstance(animal, Dog):
    # It is a Dog.
    ...

However, frequent explicit type checks can indicate that polymorphism or another design should be used instead.

22. Composition

Composition means constructing a class from other objects rather than inheriting behavior.

C++ Python
class Engine {
public:
    void start()
    {
        std::cout << "Engine started\n";
    }
};

class Car {
public:
    void start()
    {
        engine.start();
    }

private:
    Engine engine;
};
class Engine:
    def start(self):
        print("Engine started")


class Car:
    def __init__(self):
        self.engine = Engine()

    def start(self):
        self.engine.start()

Composition is often preferable when the relationship is:

has-a

rather than:

is-a

23. Dataclasses

Many classes exist primarily to store data.

In C++, such classes or structures often require explicit constructors and comparison code.

Python provides @dataclass.

C++ Python
struct Student {
    std::string name;
    int age;

    Student(
        std::string name,
        int age
    )
        : name(name),
          age(age)
    {
    }
};
from dataclasses import dataclass


@dataclass
class Student:
    name: str
    age: int

Creation is the same as for a normal class:

C++ Python
Student s("Alice", 21);
s = Student("Alice", 21)

The dataclass automatically generates useful methods such as __init__() and __repr__().

24. Dataclass equality

Dataclasses also provide value-based equality by default.

C++ Python
struct Point {
    int x;
    int y;

    bool operator==(const Point& other) const
    {
        return x == other.x &&
               y == other.y;
    }
};
from dataclasses import dataclass


@dataclass
class Point:
    x: int
    y: int

Now:

a = Point(1, 2)
b = Point(1, 2)

print(a == b)  # True

25. Special methods

Python classes can implement special methods, often called dunder methods because their names begin and end with two underscores.

They allow user-defined objects to participate in normal Python syntax.

Examples include:

Python method Purpose
__init__ initialize an object
__str__ user-friendly string representation
__repr__ developer-oriented representation
__eq__ equality comparison
__len__ support len(obj)
__iter__ allow iteration
__contains__ support x in obj

26. __str__()

C++ commonly overloads operator<< for printable objects.

Python commonly implements __str__().

C++ Python
class Point {
public:
    Point(int x, int y)
        : x(x), y(y)
    {
    }

    friend std::ostream& operator<<(
        std::ostream& os,
        const Point& p
    )
    {
        return os
            << "("
            << p.x
            << ", "
            << p.y
            << ")";
    }

private:
    int x;
    int y;
};
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __str__(self):
        return f"({self.x}, {self.y})"

Usage:

C++ Python
Point p(2, 3);

std::cout << p;
p = Point(2, 3)

print(p)

27. __repr__()

__repr__() should normally return a representation useful to a programmer.

C++ Python
// No exact language-level equivalent.
// Usually implemented through
// debug output or operator<<.
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return (
            f"Point(x={self.x}, "
            f"y={self.y})"
        )

Example:

p = Point(2, 3)

print(repr(p))

Output:

Point(x=2, y=3)

28. __eq__()

Python uses __eq__() to define the behavior of ==.

C++ Python
class Point {
public:
    bool operator==(const Point& other) const
    {
        return x == other.x &&
               y == other.y;
    }

private:
    int x;
    int y;
};
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __eq__(self, other):
        if not isinstance(other, Point):
            return NotImplemented

        return (
            self.x == other.x
            and self.y == other.y
        )

In many simple data-holding classes, using @dataclass avoids writing this manually.

29. __len__()

A custom class can define what len(obj) means.

C++ Python
class Course {
public:
    std::size_t size() const
    {
        return students.size();
    }

private:
    std::vector<std::string> students;
};
class Course:
    def __init__(self):
        self.students = []

    def __len__(self):
        return len(self.students)

Usage:

C++ Python
std::cout << course.size();
print(len(course))

30. __iter__()

Implementing __iter__() allows an object to be used directly in a for loop.

C++ Python
class Course {
public:
    auto begin()
    {
        return students.begin();
    }

    auto end()
    {
        return students.end();
    }

private:
    std::vector<std::string> students;
};
class Course:
    def __init__(self):
        self.students = []

    def __iter__(self):
        return iter(self.students)

Now the class can be used naturally:

C++ Python
for (const auto& student : course) {
    std::cout << student << "\n";
}
for student in course:
    print(student)

31. Operator overloading

Python maps operators to special methods.

Operation C++ Python
addition operator+ __add__
subtraction operator- __sub__
multiplication operator* __mul__
equality operator== __eq__
less than operator< __lt__

Example:

C++ Python
class Vector2D {
public:
    Vector2D(double x, double y)
        : x(x), y(y)
    {
    }

    Vector2D operator+(
        const Vector2D& other
    ) const
    {
        return Vector2D(
            x + other.x,
            y + other.y
        );
    }

private:
    double x;
    double y;
};
class Vector2D:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        return Vector2D(
            self.x + other.x,
            self.y + other.y,
        )

Usage:

a = Vector2D(1, 2)
b = Vector2D(3, 4)

c = a + b

32. Attributes can be added dynamically

Python objects are usually more dynamic than C++ objects.

C++ Python
class Student {
public:
    std::string name;
};

Student s;

// Not allowed unless "grade"
// was declared in Student.
// s.grade = 10;
class Student:
    pass


s = Student()

s.name = "Alice"
s.grade = 10

Although Python permits this, creating expected instance attributes in __init__() is generally clearer.

33. Type hints in classes

Python allows type hints for attributes and methods.

C++ Python
class Student {
public:
    Student(
        std::string name,
        int year
    )
        : name(name),
          year(year)
    {
    }

    std::string description() const
    {
        return name;
    }

private:
    std::string name;
    int year;
};
class Student:
    def __init__(
        self,
        name: str,
        year: int,
    ) -> None:
        self.name: str = name
        self.year: int = year

    def description(self) -> str:
        return self.name

Type hints improve readability and allow static analysis tools to detect some errors before runtime.

34. Example — Course and Student classes

Consider a small system containing students and a course.

C++ Python
#include <string>
#include <vector>

class Student {
public:
    Student(
        std::string name,
        double grade
    )
        : name(name),
          grade(grade)
    {
    }

    std::string getName() const
    {
        return name;
    }

    double getGrade() const
    {
        return grade;
    }

private:
    std::string name;
    double grade;
};

class Course {
public:
    void addStudent(const Student& student)
    {
        students.push_back(student);
    }

    double average() const
    {
        double sum = 0.0;

        for (const auto& student : students) {
            sum += student.getGrade();
        }

        return sum / students.size();
    }

private:
    std::vector<Student> students;
};
from dataclasses import dataclass


@dataclass
class Student:
    name: str
    grade: float


class Course:
    def __init__(self):
        self.students = []

    def add_student(self, student):
        self.students.append(student)

    def average(self):
        total = sum(
            student.grade
            for student in self.students
        )

        return total / len(self.students)

The Python implementation uses:

  • a dataclass for the simple data-holding Student class;
  • direct attribute access;
  • a normal class for Course;
  • a generator expression for calculating the total.

35. Example — Adding Python protocol behavior

We can make the Course class behave more naturally in Python.

Conventional explicit API Pythonic API
class Course:
    def __init__(self):
        self.students = []

    def get_student_count(self):
        return len(self.students)

    def get_students(self):
        return self.students
class Course:
    def __init__(self):
        self.students = []

    def __len__(self):
        return len(self.students)

    def __iter__(self):
        return iter(self.students)

The second version allows:

print(len(course))

for student in course:
    print(student)

This is a recurring Python design principle:

Prefer implementing the protocol expected by Python instead of inventing unnecessary custom method names.

36. C++ habits to reconsider

C++ habit Typical Python approach
create getters and setters for every field use public attributes unless behavior or validation is required
enforce access through private use naming conventions and properties
create a class for every small data structure consider @dataclass
require a common base class for polymorphism consider duck typing
create methods such as getSize() implement __len__() when appropriate
create methods such as toString() implement __str__()
create explicit collection access methods consider __iter__()
use inheritance to reuse implementation consider composition

37. Exercise 1 — Rectangle

Implement a Rectangle class.

The equivalent C++ design is:

C++ Python
class Rectangle {
public:
    Rectangle(double width, double height)
        : width(width),
          height(height)
    {
    }

    double area() const
    {
        return width * height;
    }

    double perimeter() const
    {
        return 2 * (width + height);
    }

private:
    double width;
    double height;
};

Your implementation:

# TODO

Requirements:

  • store width and height;
  • implement area();
  • implement perimeter();
  • implement __str__();
  • reject negative dimensions using properties.

38. Exercise 2 — Bank account

Implement a BankAccount class.

The class should contain:

  • owner name;
  • account number;
  • current balance.

Implement:

deposit(amount)
withdraw(amount)

Rules:

  • deposits must be positive;
  • withdrawals must be positive;
  • a withdrawal cannot exceed the current balance;
  • invalid operations should raise an exception;
  • the balance should not be modified directly from outside the class.

Suggested interface:

C++ idea Python idea
class BankAccount {
public:
    void deposit(double amount);
    void withdraw(double amount);

    double getBalance() const;

private:
    double balance = 0.0;
};
class BankAccount:
    def deposit(self, amount):
        ...

    def withdraw(self, amount):
        ...

    @property
    def balance(self):
        ...

39. Exercise 3 — Inheritance

Create the following hierarchy:

Shape
├── Rectangle
└── Circle

Each shape must provide:

area()

Then write a function:

def print_area(shape):
    ...

that works with either a rectangle or a circle.

Compare the design mentally with the equivalent C++ solution based on a virtual function.

40. Exercise 4 — Composition

Create:

Engine
Car

The Engine class should provide:

start()
stop()

The Car class should contain an Engine object rather than inherit from Engine.

Implement:

car.start()
car.stop()

so that these methods delegate to the contained engine.

41. Exercise 5 — Dataclass

Create a Student dataclass containing:

  • name;
  • student ID;
  • year;
  • grade.

Then:

  1. create at least three students;
  2. place them in a list;
  3. print them;
  4. compare two students using ==;
  5. determine the student with the highest grade.

Start from:

from dataclasses import dataclass


@dataclass
class Student:
    # TODO
    ...

42. Exercise 6 — Custom collection

Implement a Course class that stores Student objects.

The class should support:

course.add(student)

len(course)

for student in course:
    ...

print(course)

To achieve this, implement:

  • add();
  • __len__();
  • __iter__();
  • __str__().

43. Exercise 7 — Vector2D

Implement a two-dimensional vector class.

The following operations should be possible:

a = Vector2D(1, 2)
b = Vector2D(3, 4)

c = a + b

print(c)
print(a == b)

Implement:

  • __init__();
  • __add__();
  • __eq__();
  • __str__().

The equivalent C++ concepts are constructor definition and operator overloading.

44. Exercise 8 — Refactoring C++ style into Python style

Consider the following Python class:

class Student:
    def __init__(self):
        self._name = ""
        self._grade = 0.0

    def get_name(self):
        return self._name

    def set_name(self, name):
        self._name = name

    def get_grade(self):
        return self._grade

    def set_grade(self, grade):
        self._grade = grade

Refactor it into a more idiomatic Python design.

Questions to consider:

  • Are explicit getters necessary?
  • Are explicit setters necessary?
  • Would a dataclass be appropriate?
  • Is validation required?
  • Which attributes should be public?

45. Summary

The main mappings introduced in this laboratory are:

C++ Python
constructor __init__()
this self
static data member class attribute
static member function @staticmethod
no direct equivalent @classmethod
getter / setter often direct attribute access or @property
private naming convention / name mangling
base-class constructor super().__init__()
virtual method normal dynamically dispatched method
value-oriented struct @dataclass
operator<< __str__()
operator== __eq__()
size() __len__()
begin() / end() __iter__()
operator overloading special methods such as __add__()

The central idea of this laboratory is:

Python supports object-oriented programming, but idiomatic Python does not attempt to reproduce C++ class design exactly. Use Python's object model, conventions and protocols rather than mechanically translating C++ patterns.

46. Preparation for Lab 3

Before the next laboratory:

  1. complete the exercises from this laboratory;
  2. review Python lists, dictionaries and comprehensions;
  3. be comfortable creating classes and methods;
  4. understand the difference between instance and class attributes;
  5. understand inheritance and composition;
  6. review exception handling from Lab 1.

In Lab 3, the focus will move from individual classes to the structure of complete Python applications: modules, packages, files, exceptions, type hints and project organization.