Lab 2
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
Studentclass; - 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:
- create at least three students;
- place them in a list;
- print them;
- compare two students using
==; - 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:
- complete the exercises from this laboratory;
- review Python lists, dictionaries and comprehensions;
- be comfortable creating classes and methods;
- understand the difference between instance and class attributes;
- understand inheritance and composition;
- 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.