Lecture 4

OOP in Python

Classes, objects, inheritance

Problem statement

You launch Minecraft

Your character is Steve.
He has: a name, health, coordinates on the map, an inventory.

How do you program this?

First attempt

The variables approach

player_name = "Steve" player_hp = 20 player_x = 0 player_y = 64

One player - works great.

What about 1000 of them?

Scaling the approach

p1_name, p1_hp, p1_x = "Steve", 20, 0 p2_name, p2_hp, p2_x = "Alex", 20, 5 p3_name, p3_hp, p3_x = ... # ... 997 more lines ...

And there are mobs around. And villages. And animals. Something went wrong.

Better, but not perfect

The dictionaries approach

p1 = {"name": "Steve", "hp": 20, "x": 0} p2 = {"name": "Alex", "hp": 20, "x": 5}

Better already. But a player has to walk, break blocks, eat food.

Each time - a separate function that takes a dictionary. And what if someone passes the "wrong" dictionary?

Class and object

The idea of OOP

Programmers are lazy people.
They came up with: let's pack the player's data and what they can do into one box.

This box is called a class.

Concept

A class is a blueprint

A class is a description of how a player is built.
By itself it does nothing.

It is a template from which real players are later made.

Concept

An object is an instance of a class

An object is what was born from the blueprint.
One object = one real player with their own name and hp.

From a single blueprint you can make as many objects as you want.

The key idea

One blueprint, a thousand players

┌───────────────┐ │ class Player │ ◄── blueprint └───────┬───────┘ │ ┌───────┼───────┐ ▼ ▼ ▼ Steve Alex Notch hp 20 hp 20 hp 20

Each with its own data. One blueprint.

What an object consists of

The two parts of an object

1

What the object knows about itself

These are attributes - data.

2

What the object can do

These are methods - actions.

All of OOP is about these two words.

What an object consists of

Attributes: the object's data

name health coordinate inventory speed

An attribute is a variable "attached" to an object.

Each object has its own attributes.

What an object consists of

Methods: the object's actions

walk break a block eat jump attack

A method is a function "attached" to an object.

All objects of the same class can do the same things.

Creating your first class

A minimal class

class Player: def __init__(self, name, hp): self.name = name self.hp = hp

That's the whole class. Three lines of body.

Syntax

Line-by-line breakdown

class Player: ← the blueprint follows def __init__(self, ...): ← the setup function self.name = name ← put the name "in a pocket" self.hp = hp ← put the health

The class name starts with a capital letter. That's a convention.

Constructor

The purpose of __init__

class Player: ← the blueprint follows def __init__(self, ...): ← the setup function self.name = name ← put the name "in a pocket" self.hp = hp ← put the health

When we tell Python "make a player", it:

  1. takes the Player blueprint
  2. makes an empty blank
  3. runs it through __init__
  4. the result - a finished player with a name and hp

__init__ - from the word initialize, "to set up".

The self parameter

The pronoun "my"

class Player: ← the blueprint follows def __init__(self, ...): ← the setup function self.name = name ← put the name "in a pocket" self.hp = hp ← put the health

When we write the blueprint, we don't yet know
what a specific player is called.

self is a pronoun. Inside a class it means "mine, this specific player's".

The self parameter

Binding to the object

class Player: ← the blueprint follows def __init__(self, ...): ← the setup function self.name = name ← put the name "in a pocket" self.hp = hp ← put the health
When we work with Steveself.name = "Steve" When with Alexself.name = "Alex"

self is the object's way to reach its own pockets.

Creating the first player

Steve from the blueprint

class Player: ← the blueprint follows def __init__(self, ...): ← the setup function self.name = name ← put the name "in a pocket" self.hp = hp ← put the health
steve = Player("Steve", 20) print(steve.name) print(steve.hp)
Steve 20

The dot reads as "of": "Steve's name", "Steve's hp".

Multiple objects

Independence of objects

class Player: ← the blueprint follows def __init__(self, ...): ← the setup function self.name = name ← put the name "in a pocket" self.hp = hp ← put the health
steve = Player("Steve", 20) alex = Player("Alex", 20) print(steve.name, alex.name)
Steve Alex

One blueprint - two different objects.

Multiple objects

Two separate boxes in memory

steve ──► ┌────────────────┐ │ name = "Steve" │ │ hp = 20 │ └────────────────┘ alex ──► ┌────────────────┐ │ name = "Alex" │ │ hp = 20 │ └────────────────┘

These are two different boxes. Physically different.

Multiple objects

Addresses and size in memory

print(id(steve)) print(id(alex)) print(steve is alex) import sys print(sys.getsizeof(steve))
140234567890 140234571248 False 48 // bytes

id() - the unique address of an object in memory. is compares by address.

Tens of bytes per object. 1000 players ≈ 50 KB - objects are cheap.

Multiple objects

A variable is a label on a box

steve ──► ┌────────────────┐ hero ──► │ name = "Steve" │ │ hp = 20 │ └────────────────┘
hero = steve # a second label hero.hp = 5 print(steve.hp) print(steve is hero)
5 True

b = a - not a copy, but a second name for the same box.

Multiple objects

Changes do not overlap

steve.hp = 0 print(alex.hp)
20 // he is alive

Objects are not linked to one another.

Changing one does not affect the others.

Multiple objects

Creating many objects

server = [Player(f"Player_{i}", 20) for i in range(1000)] print(server[0].name) print(server[999].hp)
Player_0 20

A thousand players. Each with its own data.

This is what it was all started for.

Class methods

A method with no parameters

class Player: def __init__(self, name, hp): self.name = name self.hp = hp def say(self, text): print(f"<{self.name}> {text}")

A method is a function inside a class. Its first parameter is always self.

Class methods

Calling a method with the dot

steve = Player("Steve", 20) steve.say("Hello, world!")
<Steve> Hello, world!

A call with the dot. Python supplies self itself.

Under the hood

How a method call works

When you write

steve.say("Hello, world!")

Python internally translates it into

Player.say(steve, "Hello, world!")

steve automatically takes the place of self.

Class methods

A method with a parameter

class Player: def __init__(self, name, hp): self.name = name self.hp = hp def take_damage(self, amount): self.hp -= amount
steve = Player("Steve", 20) steve.take_damage(5) print(steve.hp)
15

In the parentheses there is only amount. Python supplies self itself.

Class methods

A method with a return value

class Player: def __init__(self, name, hp): self.name = name self.hp = hp def take_damage(self, amount): self.hp -= amount def is_alive(self): return self.hp > 0
print(steve.is_alive()) steve.take_damage(100) print(steve.is_alive())
True False

A method can return a value - just like an ordinary function.

Class methods

Calling a method from a method

class Player: def __init__(self, name, hp): self.name = name self.hp = hp def say(self, text): print(f"<{self.name}> {text}") def eat(self, food_hp): self.hp += food_hp if self.hp > 20: self.hp = 20 self.say(f"ate food, hp: {self.hp}")

Inside a class, methods freely communicate through self.

Magic method

__str__: controlling print()

class Player: def __init__(self, name, hp): self.name = name self.hp = hp def __str__(self): return f"<{self.name}> hp: {self.hp}"
steve = Player("Steve", 20) print(steve)
<Steve> hp: 20

Without __str__ you would see <__main__.Player object at 0x7f...> - an address in memory.

Double underscores are a convention for "service" methods that Python calls itself.

OOP in the standard libraries

OOP in Python code

df.head() df.groupby("city") "hello".upper() [1, 2, 3].append(4)

Each dot is a call to a method on an object.

You have been using OOP since the very first lecture, you just didn't call it that.

Example

A string as an object

"hello".upper()

A string is an object of the str class.
It has an upper method.

It's the same as steve.say(...). The str class was simply written by the Python developers.

Example

A DataFrame as an object of a class

df = pd.read_csv("data.csv") df.head() df.shape

df - an object of the DataFrame class.

Methods: head, groupby, describe. Attributes: shape, columns.

Built exactly like our Player - only with hundreds of methods.

Example

sklearn models as classes

model = LinearRegression() model.fit(X, y) model.predict(X_test)

LinearRegression - a class. model - an object. fit/predict - methods.

When you understand OOP, you understand how large libraries are built.

Practice #1

Task 1: an extended Player class

  • attributes: name, hp (default 20), inventory (an empty list)
  • method pickup(item) - add an item to the inventory
  • method has(item) - whether the item is present
  • method __str__ - <Steve> hp: 20, items: 3
10 minutes

Practice #1

Task 2: BankAccount

The same thing, but from real life:

  • attributes: owner, balance (default 0)
  • methods deposit(amount), withdraw(amount)
  • if there isn't enough money - print "Insufficient funds"

Not a game - but built the same way.

Walkthrough

Solution to task 1

class Player: def __init__(self, name, hp=20): self.name = name self.hp = hp self.inventory = [] # ← important: in __init__ def pickup(self, item): self.inventory.append(item) def has(self, item): return item in self.inventory def __str__(self): return f"<{self.name}> hp: {self.hp}, items: {len(self.inventory)}"

Break

Coffee break

5–10 min

Next: night falls, mobs appear - and we will understand
why we write one method instead of a hundred.

The duplication problem

A new task: mobs

Mobs come out of the darkness:

Zombie

moves slowly and hits in melee

Skeleton

shoots a bow from a distance

Creeper

quietly approaches and explodes

They all have in common: a name, hp, a coordinate, the ability to take damage.
The only difference is how they attack.

The brute-force solution

The copy-paste approach

class Zombie: def __init__(self, name, hp): self.name = name; self.hp = hp def take_damage(self, amount): self.hp -= amount def is_alive(self): return self.hp > 0 def attack(self, target): ... class Skeleton: def __init__(self, name, hp): self.name = name; self.hp = hp def take_damage(self, amount): self.hp -= amount def is_alive(self): return self.hp > 0 def attack(self, target): ...

And the same again for Creeper. Seven lines copied three times.

Why you can't do this

The problems with duplication

  • The same code in several places
  • An Enderman, Spider, Witch will appear - copy it again
  • You find a bug in take_damage - you fix it in Zombie, you forget it in Skeleton

We will not do it this way.

Inheritance

The idea of inheritance

The common part - into one parent class.
The unique part - into the children.

Children get the common part automatically.

Inheritance

The parent class

class Mob: def __init__(self, name, hp): self.name = name self.hp = hp def take_damage(self, amount): self.hp -= amount def is_alive(self): return self.hp > 0

The common part is gathered in one place.

Inheritance

The child class

class Mob: ← parent def __init__(self, name, hp): ... def take_damage(self, amount): ... def is_alive(self): ...
class Zombie(Mob): ← child def attack(self, target): print(f"{self.name} bites {target.name}") target.take_damage(3)

Zombie(Mob) - "Zombie inherits from Mob". Inside - only the new ability.

What you got for free

Using the child class

class Mob: ← parent def __init__(self, name, hp): ... def take_damage(self, amount): ... def is_alive(self): ... class Zombie(Mob): ← child def attack(self, target): ...
z = Zombie("Zombie-1", 20) print(z.name) # Zombie-1 - attribute from the parent print(z.is_alive()) # True - method from the parent z.attack(steve) # its own ability

We defined only attack, but everything else works.

Extending __init__

Extending the constructor

class Mob: ← parent def __init__(self, name, hp): self.name = name self.hp = hp
class Creeper(Mob): def __init__(self, name, hp, fuse): super().__init__(name, hp) # call the parent self.fuse = fuse # add our own

super().__init__(name, hp) - "do everything the parent does, and then I'll add my own".

Attention

A common mistake: without super()

class Mob: ← parent, name and hp are created here def __init__(self, name, hp): self.name = name self.hp = hp
class Creeper(Mob): def __init__(self, name, hp, fuse): self.fuse = fuse # forgot to call the parent c = Creeper("Creeper-1", 20, 3) print(c.name) # AttributeError: 'Creeper' object has no attribute 'name'

You overrode __init__ - make super().__init__(...) the first line.

Words

Terminology

Mob

Parent class

parent, base, superclass

Zombie, Skeleton, Creeper

Child classes

child, descendants, subclasses

The notation Zombie(Mob) reads as: "Zombie is a Mob plus a bit more".

Overriding and polymorphism

Overriding a method

class Zombie(Mob): def attack(self, target): print(f"{self.name} bites {target.name}") target.take_damage(3) class Skeleton(Mob): def attack(self, target): print(f"{self.name} shoots at {target.name}") target.take_damage(2) class Creeper(Mob): def attack(self, target): print(f"{self.name}: SSSSS... BOOM!") target.take_damage(15)

The same method name - a different implementation.

Which version to call?

Choosing the implementation at call time

z = Zombie("Zombie-1", 20) s = Skeleton("Skeleton-1", 20) c = Creeper("Creeper-1", 20, 3) z.attack(steve) s.attack(steve) c.attack(steve)
Zombie-1 bites Steve Skeleton-1 shoots at Steve Creeper-1: SSSSS... BOOM!

Python looks at the object's type and calls the right version of attack.

Magic in a single loop

Polymorphism in practice

night = [ Zombie("Z-1", 20), Skeleton("S-1", 20), Creeper("C-1", 20, 3), Zombie("Z-2", 20), ] for mob in night: if mob.is_alive(): mob.attack(steve)

The loop doesn't care who is inside. It just calls .attack().

This is exactly what is called polymorphism. The idea is simple: one interface, different behavior.

OOP in real projects

Polymorphism in sklearn

from sklearn.linear_model import LinearRegression from sklearn.tree import DecisionTreeRegressor models = [LinearRegression(), DecisionTreeRegressor()] for m in models: m.fit(X, y) m.predict(X_test)

Different models - one interface, fit/predict.

The same loop we had with the mobs.

Also inheritance

The exception hierarchy

Exception ├── ValueError ├── KeyError ├── TypeError └── ...

ValueError - a kind of Exception.

When you write except Exception - all of them are caught, because of inheritance.

Common mistakes

Mistake 1: a missing self

class Player: def __init__(self, name): name = name # ⚠ created a local variable steve = Player("Steve") print(steve.name) # AttributeError

An attribute is created only through self.name = ...

Common mistakes

Mistake 2: a missing super().__init__()

class Creeper(Mob): def __init__(self, name, hp, fuse): self.fuse = fuse # ⚠ the parent is not called c = Creeper("Creeper-1", 20, 3) print(c.name) # AttributeError

You overrode __init__ - make super().__init__(...) the first line.

Common mistakes

Mistake 3: a mutable class attribute

class Player: inventory = [] # ⚠ one for all players def pickup(self, item): self.inventory.append(item) steve.pickup("sword") print(alex.inventory) # ['sword'] - Alex has it too!

Lists and dictionaries - only in __init__, not at the class level.

Practice #2

Task: an Animal hierarchy

Without Minecraft - reinforcing the technique on a simple example.

  • Animal(name) with a method sound() = "..."
  • Dog(Animal) overrides sound() = "Woof"
  • Cat(Animal) overrides sound() = "Meow"
  • A method describe() in Animal: "{name} says {sound()}"
  • Create a list of animals and iterate over it
10 minutes

Walkthrough

Expected result

zoo = [Dog("Rex"), Cat("Murka")] for a in zoo: print(a.describe())
Rex says Woof Murka says Meow

The same technique as with Zombie/Skeleton/Creeper. The same loop. Just different names.

Summary

What we learned

  • class - a blueprint
  • object - what was born from the blueprint
  • attributes and methods - what an object knows and can do
  • self - "mine" inside a class
  • __str__ - controlling how an object is printed
  • inheritance - the common part in the parent, the unique part in the children
  • super() - calling the parent
  • polymorphism - one interface, different behavior

Summary

Applying it in real work

When you see df.groupby(...) or model.fit(...), you now know how it is built inside.

  • You can dig into the source code
  • You can write your own classes on top of libraries
  • Most Python code is classes

Closing

Questions?

Telegram: @gokalqurt

RU UZ EN
Python · Lecture 4
1 / 62