Classes

Classes

import time

class Timer:
    def __init__(self):
        self.started_at = time.monotonic()

    def elapsed(self) -> float:
        return time.monotonic() - self.started_at


timer = Timer()
time.sleep(1)
print(f"Took {timer.elapsed():.3f} seconds")

Constructor: __init__ — a function that initialises an object

Timer() calls the constructor and returns a new object

self is always the first argument and refers to the object

Method: elapsed — a function available for each object of this class

Property: self.started_at, also available as timer.started_at from outside

Bonus fact A method is actually a real function accepting self as an argument: one can call Timer.elapsed(timer) instead of timer.elapsed() But please don’t do it!

Bonus question What’s the difference between time.time() and time.monotonic()?

Dataclasses

from dataclasses import dataclass

@dataclass
class Song:
    title: str
    artist: str
    duration_seconds: int

song = Song("Bohemian Rhapsody", "Queen", 354)

Dataclasses

class Playlist:
    def __init__(self, name: str):
        self.name = name
        self.songs = []

    def add(self, song: Song):
        self.songs.append(song)

    def duration_seconds(self) -> int:
        return sum(song.duration_seconds for song in self.songs)

playlist = Playlist("Road Trip")

playlist.add(Song("Bohemian Rhapsody", "Queen", 354))
playlist.add(Song("Take On Me", "a-ha", 225))
playlist.add(Song("Smells Like Teen Spirit", "Nirvana", 301))

print(playlist.duration_seconds())

Methods can accept arguments besides self, just like regular functions

Magic methods

playlist = Playlist("Road Trip")
print(playlist)

Magic methods

class Playlist:
    def __init__(self, name: str):
        self.name = name
        self.songs = []

    def add(self, song: Song):
        self.songs.append(song)

    def duration_seconds(self) -> int:
        return sum(song.duration_seconds for song in self.songs)

    def __str__(self):
        return f"Playlist \"{self.name}\" with {len(self.songs)} songs and a duration of {self.duration_seconds()} seconds"


playlist = Playlist("Road Trip")
playlist.add(Song("Bohemian Rhapsody", "Queen", 354))
playlist.add(Song("Take On Me", "a-ha", 225))
playlist.add(Song("Smells Like Teen Spirit", "Nirvana", 301))

print(playlist)

Magic methods

  • __len__ — enables len(obj)
  • __bool__ — controls truthiness in if obj
  • __eq__, __lt__, __le__, __gt__, __ge__ — comparisons
  • __hash__ — enables use as a dictionary key or set member
  • __contains__ — enables item in obj
  • __call__ — enables obj()
  • __add__, __sub__, __mul__, and friends — arithmetic operators
  • And others, but we’ll talk about them later

Protected and private methods

class BankAccount:
    def __init__(self, owner: str, balance: int = 0):
        self.owner = owner
        self.balance = balance

    def deposit(self, amount: int):
        self.balance += amount

    def withdraw(self, amount: int):
        if amount > self.balance:
            print(f"Not enough money to withdraw {amount}: only {self.balance} is available")
        else:
            self.balance -= amount

    def __str__(self):
        return f"Account {self.owner}: {self.balance}"


account = BankAccount("Andrey")
account.deposit(100)
account.withdraw(200)

Protected and private methods

account = BankAccount("Andrey")
account.balance = 500000
print(account)

Nobody prevents us from modifying the internal structure of an object. How can we protect ourselves?

Protected and private methods

class BankAccount:
    def __init__(self, owner: str, balance: int = 0):
        self._owner = owner
        self._balance = balance

What happens?

account = BankAccount("Andrey")
account._balance = 500000
print(account)

Protected and private methods

class BankAccount:
    def __init__(self, owner: str, balance: int = 0):
        self.__owner = owner
        self.__balance = balance


account = BankAccount("Andrey")
account.__balance = 500000
print(account)
account._BankAccount__balance = 500000
print(account)

Inheritance

class CreditAccount(BankAccount):
    def __init__(self, owner: str, balance: int = 0, credit_limit: int = 1000):
        super().__init__(owner, balance)
        self._credit_limit = credit_limit

    def withdraw(self, amount: int):
        if amount > self._balance + self._credit_limit:
            print(f"Credit limit exceeded for {self._owner}: {amount} requested, {self._balance} balance, {self._credit_limit} limit")
        else:
            self._balance -= amount

account = CreditAccount("Taylor Swift", 0, 1000)
account.withdraw(500)
print(account)

Inheritance

accounts = [
    BankAccount("Taylor Swift", 54000),
    CreditAccount("Ed Sheeran", -250, 200),
    BankAccount("Beyoncé", 100),
    CreditAccount("Bad Bunny", 1250, 3000),
]

for account in accounts:
    account.withdraw(500)
    print(account)