3. Dunder metody, dekorátory tříd a iterátory
Dunder metody
Speciální metody sloužící k implementaci podpory pro vestavěné funkce Pythonu a jinou rozšiřující funkcionalitu (např. __init__
jakožto konstruktor). Přehled dunder metod je dostupný zde.
String reprezentace
Implementací dunder metod __repr__
a __str__
implementujeme chování funkcí repr()
a str()
class CreditAccount:
"""Account with stored credits."""
def __init__(self, owner, initial_credits=0):
"""Creates credit account with given owner and initial credits.
Args:
owner: owner of the account
initial_credits (optional): credit balance. Defaults to 0.
"""
self.owner = owner
self.balance = initial_credits
def __repr__(self):
return f"CreditAccount({self.owner}, {self.balance})"
def __str__(self):
return self.__repr__()
from credit_account import CreditAccount
credit_account_1 = CreditAccount("Lukas Novak")
credit_account_2 = CreditAccount("Pepa Novak", initial_credits=200)
credit_account_1
print(credit_account_1) # CreditAccount(Lukas Novak, 0)
repr(credit_account_1) # 'CreditAccount(Lukas Novak, 0)'
Převod na jiné datové typy
__bool__ # chování funkce bool()
__complex__ # chování funkce complex()
__int__ # chování funkce int()
__float__ # chování funkce float()
__hash__ # chování funkce hash()
Následující příklad dále implementuje dunder metody __bool__
a __int__
.
class CreditAccount:
"""Account with stored credits."""
def __init__(self, owner, initial_credits=0):
"""Creates credit account with given owner and initial credits.
Args:
owner: owner of the account
initial_credits (optional): credit balance. Defaults to 0.
"""
self.owner = owner
self.balance = initial_credits
def __repr__(self):
return f"CreditAccount({self.owner}, {self.balance})"
def __str__(self):
return self.__repr__()
def __bool__(self):
return bool(self.balance)
def __int__(self):
return int(self.balance)
from credit_account import CreditAccount
credit_account_1 = CreditAccount("Lukas Novak")
credit_account_2 = CreditAccount("Pepa Novak", initial_credits=200)
assert bool(credit_account_1) == False
assert bool(credit_account_2) == True
assert int(credit_account_1) == 0
Unární číselné operátory
__abs__ # chování funkce abs()
__neg__ # chování unárního minus
__pos__ # chování unárního plus
Porovnávání
__lt__ # chování <
__le__ # chování <=
__eq__ # chování ==
__ne__ # chování !=
__gt__ # chování >
__ge__ # chování >=
Následující příklad dále implementuje dunder metodu __lt__
.
class CreditAccount:
"""Account with stored credits."""
def __init__(self, owner, initial_credits=0):
"""Creates credit account with given owner and initial credits.
Args:
owner: owner of the account
initial_credits (optional): credit balance. Defaults to 0.
"""
self.owner = owner
self.balance = initial_credits
def __repr__(self):
return f"CreditAccount({self.owner}, {self.balance})"
def __str__(self):
return self.__repr__()
def __bool__(self):
return bool(self.balance)
def __int__(self):
return int(self.balance)
def __lt__(self, other):
if isinstance(other, CreditAccount):
return self.balance < other.balance
return NotImplemented
from credit_account import CreditAccount
credit_account_1 = CreditAccount("Lukas Novak")
credit_account_2 = CreditAccount("Pepa Novak", initial_credits=200)
credit_account_1 < credit_account_2 # True
credit_account_2 < credit_account_1 # False
Aritmetické operátory
__add__ # chování +
__sub__ # chování -
__mul__ # chování *
__truediv__ # chování /
__floordiv__ # chování //
__mod__ # chování %
__divmod__ # chování divmod()
__pow__ # chování ** nebo pow()
__round__ # chování round()
Následující příklad dále implementuje dunder metodu __add__
. V případě, že pro nějaký typ nejsou dunder metody implementovány, je nutné vracet konstantu NotImplemented
.
class CreditAccount:
"""Account with stored credits."""
def __init__(self, owner, initial_credits=0):
"""Creates credit account with given owner and initial credits.
Args:
owner: owner of the account
initial_credits (optional): credit balance. Defaults to 0.
"""
self.owner = owner
self.balance = initial_credits
def __repr__(self):
return f"CreditAccount({self.owner}, {self.balance})"
def __str__(self):
return self.__repr__()
def __bool__(self):
return bool(self.balance)
def __int__(self):
return int(self.balance)
def __lt__(self, other):
if isinstance(other, CreditAccount):
return self.balance < other.balance
return NotImplemented
def __add__(self, other):
if isinstance(other, CreditAccount):
return self.balance + other.balance
return NotImplemented
from credit_account import CreditAccount
credit_account_1 = CreditAccount("Lukas Novak")
credit_account_2 = CreditAccount("Pepa Novak", initial_credits=200)
credit_account_1 + credit_account_2 # 200
Aritmetické operátory je možné implementovat pro "oba směry". V situaci kdy vyhodnocujeme x + y
Python hledá implementaci x.__add__
nebo y.__radd__
.
__radd__
__rsub__
__rmul__
__rtruediv__
__rfloordiv__
__rmod__
__rdivmod__
__rpow__
Kombinované operátory přiřazení s aritmetickými operacemi
Je běžné, ne však nutné, aby výsledná metoda vracela self
.
__iadd__ # chování +=
__isub__ # chování -=
__imul__ # chování *=
__itruediv__ # chování /=
__ifloordiv__ # chování //=
__imod__ # chování %=
__ipow__ # chování **=
Následující příklad dále implementuje dunder metodu __iadd__
. V případě, že pro nějaký typ nejsou dunder metody implementovány, je nutné vracet konstantu NotImplemented
.
class CreditAccount:
"""Account with stored credits."""
def __init__(self, owner, initial_credits=0):
"""Creates credit account with given owner and initial credits.
Args:
owner: owner of the account
initial_credits (optional): credit balance. Defaults to 0.
"""
self.owner = owner
self.balance = initial_credits
def __repr__(self):
return f"CreditAccount({self.owner}, {self.balance})"
def __str__(self):
return self.__repr__()
def __bool__(self):
return bool(self.balance)
def __int__(self):
return int(self.balance)
def __lt__(self, other):
if isinstance(other, CreditAccount):
return self.balance < other.balance
return NotImplemented
def __add__(self, other):
if isinstance(other, CreditAccount):
return self.balance + other.balance
return NotImplemented
def __iadd__(self, value):
if isinstance(value, int):
self.balance += value
return self
return NotImplemented
from credit_account import CreditAccount
credit_account_1 = CreditAccount("Lukas Novak")
credit_account_2 = CreditAccount("Pepa Novak", initial_credits=200)
credit_account_1 += 200 # 200
# CreditAccount(Lukas Novak, 400)
credit_account_1 += credit_account_2
Bitové operátory
__invert__ # chování ~
__lshift__ # chování <<
__rshift__ # chování >>
__and__ # chování &
__or__ # chování |
__xor__ # chování ^
Emulace kolekcí
Důležité, dostaneme se k nim později.
__index__ # chování převodu na integer například při slicingu
__len__ # chování len()
__getitem__ # chování x[20]
__setitem__ # chování x[20] = 2
__delitem__ # chování del x[20]
__contains__ # chování in
Použití implementovaných dunder metod
Implementovanou funkcionalitu můžeme použít rovněž v rámci třídy.
class CreditAccount:
"""Account with stored credits."""
def __init__(self, owner, initial_credits=0):
"""Creates credit account with given owner and initial credits.
Args:
owner: owner of the account
initial_credits (optional): credit balance. Defaults to 0.
"""
self.owner = owner
self.balance = initial_credits
def __repr__(self):
return f"CreditAccount({self.owner}, {self.balance})"
def __str__(self):
return self.__repr__()
def __bool__(self):
return bool(self.balance)
def __int__(self):
return int(self.balance)
def __lt__(self, other):
if isinstance(other, CreditAccount):
return self.balance < other.balance
return NotImplemented
def __add__(self, other):
if isinstance(other, CreditAccount):
return self.balance + other.balance
return NotImplemented
def __iadd__(self, value):
if isinstance(value, int):
self.balance += value
return self
return NotImplemented
def __isub__(self, value):
if isinstance(value, int):
self.balance -= value
return self
return NotImplemented
def transfer_to(self, other, value):
"""Transfer credit into another credit account. Negative balance is allowed.
Args:
other: Target of credit transfer.
value: Amount of credit to be transfered.
"""
self -= value
other += value
from credit_account import CreditAccount
credit_account_1 = CreditAccount("Lukas Novak")
credit_account_2 = CreditAccount("Pepa Novak", initial_credits=200)
credit_account_1.transfer_to(credit_account_2, 200)
credit_account_1 # CreditAccount(Lukas Novak, -200)
credit_account_2 # CreditAccount(Pepa Novak, 400)
Vestavěné funkce
Dunder metody představují elegantní řešení pro implementaci podpory vestavěných funkcí. Většinou je tedy lepší využívat implementace těchto metod pro podporu len()
než implementování vlastní metody object.length()
.
Protipříkladem je pomyslná třída Vector
ve které chceme implementovat výpočet délky vektoru. Pozor, použití len()
na instanci třídy Vector
není vhodné, funkci len()
používáme v kontextu kolekcí pro zjištění počtu prvků. U třídy Vector
požadujeme jinou sémantiku. V takovém případě je tedy vhodnější zvolit implementaci metody Vector.length()
, rovněž jsem se setkal s implementace dunder metody __abs__()
a následné používání funkce abs()
.
Dekorátory ve třídách
@property
V jazyce Python nepoužíváme klasické (například Javovské) gettery, settery (tedy metody s názvyget_temperature
a set_temperature
). To plyne z vlastnosti veřejné dostupnosti všech hodnot objektu (narozdíl od jazyků jako je třeba Java). Vraťme se k jednoduchému příkladu třídy CreditAccount
. Vlastnost CreditAccount.balance
je dostupná pomoci tečkového operátoru.
class CreditAccount:
"""Account with stored credits."""
def __init__(self, owner, initial_credits=0):
"""Creates credit account with given owner and initial credits.
Args:
owner: owner of the account
initial_credits (optional): credit balance. Defaults to 0.
"""
self.owner = owner
self.balance = initial_credits
from credit_account import CreditAccount
credit_account = CreditAccount("Lukas Novak")
credit_account.balance
Co můžeme dělat pokud chceme například ověřovat, že balance
nemůže být nastavena na zápornou hodnotu?
# špatné, ale bohužel časté řešení
class CreditAccount:
"""Account with stored credits."""
def __init__(self, owner, initial_credits=0):
"""Creates credit account with given owner and initial credits.
Args:
owner: owner of the account
initial_credits (optional): credit balance. Defaults to 0.
"""
self.owner = owner
self.balance = 0
self.set_balance(initial_credits)
def set_balance(self, new_balance):
if new_balance < 0:
raise ValueError("Balance cannot be negative number!")
self.balance = new_balance
from credit_account import CreditAccount
credit_account = CreditAccount("Lukas Novak", 0)
credit_account.set_balance(0)
# negativní hodnotu balance můžeme stále nastavit
credit_account.balance = -100
Pro příklady kdy chceme modifikovat chování přístupu k vlastnosti třídy je nutné použít dekorátor @property
.
# správně
class CreditAccount:
"""Account with stored credits."""
def __init__(self, owner, initial_credits=0):
"""Creates credit account with given owner and initial credits.
Args:
owner: owner of the account
initial_credits (optional): credit balance. Defaults to 0.
"""
self.owner = owner
self._balance = 0
self.balance = initial_credits
@property
def balance(self):
return self._balance
@balance.setter
def balance(self, new_balance):
"""Sets new value of balance, new value cannot be negative."""
if new_balance < 0:
raise ValueError("Balance cannot be negative number!")
self._balance = new_balance
@balance.deleter
def balance(self):
self._balance = 0
from credit_account import CreditAccount
credit_account = CreditAccount("Lukas Novak", 0)
credit_account.balance = 10
# volani deleteru, balance bude 0
del credit_account.balance
credit_account.balance
# volani setteru
credit_account.balance = -100
@classmethod vs @staticmethod
Nejprve se podívejme na dekorátor @classmethod
. Tento dekorátor lze použít v situaci kdy je nutné metodám předat odkaz na celou třídu. Demonstrovat jej můžeme na příkladu metod from_*
, tedy metod které umí vytvořit instanci třídy různými způsoby.
class CreditAccount:
"""Account with stored credits."""
def __init__(self, owner, initial_credits=0):
"""Creates credit account with given owner and initial credits.
Args:
owner: owner of the account
initial_credits (optional): credit balance. Defaults to 0.
"""
self.owner = owner
self.balance = initial_credits
@classmethod
def from_csv(cls, input_string, separator=","):
"""Creates CreditAccount class from csv string"""
owner, initial_credits = input_string.split(separator)
return cls(owner, int(initial_credits))
from credit_account import CreditAccount
credit_account = CreditAccount.from_csv("Lukas Novak,200")
credit_account.owner
credit_account.balance
Speciální dekorátor @staticmethod
naopak nevyžaduje (a nemá) přístup ke své třídě.
class CreditAccount:
"""Account with stored credits."""
def __init__(self, owner, initial_credits=0):
"""Creates credit account with given owner and initial credits.
Args:
owner: owner of the account
initial_credits (optional): credit balance. Defaults to 0.
"""
self.owner = owner
self.balance = initial_credits
@staticmethod
def credit_to_money(credit, exchange_rate):
"""Calculates money value of credits.
Args:
credit: amount of credits
exchange_rate: how many money per one credit
Returns: money value
"""
return credit * exchange_rate
from credit_account import CreditAccount
# dostupné z třídy
CreditAccount.credit_to_money(100, 20)
credit_account = CreditAccount("Lukas Novak", 200)
# dostupné rovněž z objektu
credit_account.credit_to_money(100, 20)
@dataclass
Pro rychlé vytvoření třídy lze použít dekorátor @dataclass
.
from dataclasses import dataclass
@dataclass
class CreditAccount:
"""Account with stored credits."""
owner: str
balance: int = 0
@dataclass
přidá automaticky konstruktor:
def __init__(self, owner: str, balance: int = 0):
self.owner = owner
self.balance = balance
@dataclass
je mnohem komplexnější, celkový popis je možné nalézt zde. Pravděpodobně nás zaskočilo, že uvádíme datový typ u jednotlivých vlastností. Jedná se o nápovědu typování, o této možnosti v jazyku Python si řekneme v budoucnu.
Pořadí definic metod
Neexistuje žádný jeden správný způsob v jakém pořadí metody třídy definovat. Populární možnost je následující:
class MyClass:
# Dunder metohods
# @staticmethod a @classmethod
# @property
# _private_method(self)
# public_method(self)
Iterátory
Protokol iterování (Iteration protocol)
Na předchozích semináři jsme mohli vidět použití protokolu iterování v cyklu for
.
for x in [1, 2, 3, 4]:
print(x)
Díky protokolu iterování funguje cyklus for
(a všechny nástroje využívající postupující zleva doprava např. while
, list comprehensions - ty uvidíme později) pro všechny objekty které jsou iterovatelné (takzvané iterable).
Koncept iterable je generalizací pojmu sekvence. Objekt je považován za iterovatelný pokud je fyzicky uložen jako sekvence nebo se jedná o objekt který produkuje během iterace jednu hodnotu v jeden okamžik (například během for
loopu).
Terminologie: iterable vs iterator
- iterable - objekt podporující volání
iter()
- iterator - objekt vrácený voláním
iter()
podporující volánínext()
iterator = iter([1, 2, 3])
next(iterator)
next(iterator)
next(iterator)
# exception StopIteration
next(iterator)
Pro podporu funkce iter()
je nutné implementovat dunder metodu __iter__
. Iterátor produkovaný funkcí iter()
musí implementovat dunder metody __next__
a vyvolat StopIteration
na konci produkce hodnot.
S iterable (iterovatelný objekt) jsme se již setkali mnohokrát:
point = {"x": 10, "y": 20}
point.keys() # view, neni iterator, stejne tak .items(), .values()
# iterátor lze ale jednoduše získat
iterator = iter(point)
next(iterator)
next(iterator)
# exception StopIteration
next(iterator)
Rovněž zde vidíme důvod proč bylo nutné použít list()
pro zobrazení celého výsledku range()
.
list(range(5))
iterator = iter(range(5))
next(iterator)
Stejně tak pro enumerate()
.
list(enumerate(range(5)))
iterator = iter(enumerate(range(5)))
next(iterator)
Funkce zip()
, enumerate()
, filter()
vrací iterable a rovněž iterable přijímají, lze je tedy zanořovat.
list(zip(enumerate(range(10)), range(10)))
# [((0, 0), 0),
# ((1, 1), 1),
# ((2, 2), 2),
# ((3, 3), 3),
# ((4, 4), 4),
# ((5, 5), 5),
# ((6, 6), 6),
# ((7, 7), 7),
# ((8, 8), 8),
# ((9, 9), 9)]
Comprehension
Comprehension spolu s cykly jsou nejčastější případy použití iteračního protokolu.
Pokud budeme chtít umocnit seznam čísel, můžeme napsat klasický for
cyklus.
squares = []
for number in numbers:
squares.append(number ** 2)
Použití list comprehension:
squares = [number ** 2 for number in numbers]
Obecně platí předpis:
new_list = [expression for member in iterable]
Výsledkem list comprehension je vždy nový seznam. Comprehension je tedy dobré používat pouze pokud chceme vytvořit nový seznam hodnot. Dále pak platí, že pokud je list comprehension delší než dva řádky, je vhodnější (čitelnější) použít klasický for
cyklus.
squares_minus = [number ** 2 for number in [number - 5 for number in numbers]]
Podmínky v list comprehension
FiltrováníJeden ze způsobů použití podmínek v comprehension je filtrování. Nejprve se podíváme jak bychom napsali řešení klasicky:
sentence = 'the rocket came back from mars'
vowels = []
for char in sentence:
if char in 'aeiou':
vowels.append(char)
vowels # ['e', 'o', 'e', 'a', 'e', 'a', 'o', 'a']
A nyní řešení pomoci list comprehension:
vowels = [i for i in sentence if i in 'aeiou']
Obecný předpis je tedy:
new_list = [expression for member in iterable (if conditional)]
Úprava prvků
Dále lze podmínky použít ke změně hodnot v seznamu. Nejprve klasické řešení:
numbers = [10, 20, -5, 10]
abs_numbers = []
for number in numbers:
if number > 0:
abs_numbers.append(number)
else:
abs_numbers.append(abs(number))
abs_numbers # [10, 20, 5, 10]
A nyní řešení pomoci list comprehension:
numbers = [10, 20, -5, 10]
abs_numbers = [number if number > 0 else abs(number) for number in numbers]
Obecný předpis je tedy:
new_list = [expression (if conditional) for member in iterable]
Zanořování list comprehension
List comprehension můžeme zanořovat, situaci demonstrujeme výpočtem kartézského součinu. Nejprve klasickým způsobem:
colors = ['red', 'green', 'blue']
sizes = ['S', 'M', 'L', 'XL']
tshirts = []
for color in colors:
for size in sizes:
tshirts.append((color, size))
A nyní řešení pomoci list comprehension:
tshirts_by_color = [(color, size) for color in colors for size in sizes]
# všimněme si, že na pořadí samozřejmné záleží
tshirts_by_size = [(color, size) for size in sizes for color in colors]
assert tshirts_by_color != tshirts_by_size
Obecně pak můžeme použít předpis:
[expression for target1 in iterable1 if condition1
for target2 in iterable2 if condition2 ...
for targetN in iterableN if conditionN]
Set comprehension
Jak jsme viděli, v případě list comprehension je výsledkem vždy seznam. Pokud požadujeme aby výsledkem byla množina, můžeme použít set comprehension.
sentence = 'the rocket came back from mars'
unique_vowels = {i for i in sentence if i in 'aeiou'}
Dict comprehension
Podobně pak v případě slovníku dict comprehension.
sentence = 'the rocket came back from mars'
word_len = {word: len(word) for word in sentence.split(' ')}
Poznámka k rozsahu platnosti
V případě comprehension nejsou lokální proměnné dostupné:
[x for x in range(10)]
# nedostupná proměnná
x
# oproti tomu klasický for cyklus
output = []
for x in range(10):
output.append(x)
# dostupná proměnná
x
Poznámka k výkonu
Ve většině případů comprehensions přinesou znatelné zrychlení (často až dvojnásobné). Iterace comprehension jsou v rámci interpretu prováděny rychlostí jazyka C (narozdíl od běžného for cyklu).
Především pro případy, kdy iterujeme přes velká data, je vždy lepší použít comprehension, pozor však na čitelnost kódu.
Generátorové funkce a výrazy
V novějších verzích jazyka Python je "prokrastinace" využívána mnohem častěji než dříve. Rovněž poskytují propracovanější nástroje pro jeho podporu. Jak se odložený výpočet projevuje? Namísto produkování celého výsledku najednou, jsou jednotlivé prvky (například prvky seznamu) produkovány v čase přístupu.
big_data = range(100000000000000)
small_data = range(1)
# 180 ns ± 14.4 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
%timeit zip(small_data, small_data)
# 184 ns ± 13.5 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
%timeit zip(big_data, big_data)
big_zip = zip(big_data, big_data)
# jednotlivé hodnoty jsou generovány postupně
# zde se vypočítá první hodnota
next(big_zip)
# zde se vypočítá druhá hodnota
next(big_zip)
# nelze, tato hodnota neexistuje (generatory obecne nejsou indexovatelné)
big_zip[100000]
Generátorové funkce
Narozdíl od normálních funkcí, které skončí výpočet a vrátí hodnotu (pomoci příkazu return
), generátorové funkce automaticky uspávají a probouzejí jejich vykonávání (pomoci příkazu yield
).
Generátorové funkce jsou úzce spojené s iteračním protokolem. Aby bylo možné iterační protokol používat, jsou funkce obsahující yield
kompilovány jako generátory. Nejedná se tedy o klasické funkce. Jejich architektura zaručuje, aby vracely objekt podporující iterační protokol. Později, při volání, vrací generátorový objekt, který podporuje iterační rozhraní (automaticky vytvoří metodu __next__
).
V rámci generátorové funkce můžeme použít rovněž příkaz return
, ten pak slouží na předčasné ukončení výpočtu (interně pomoci StopIteration
).
# klasická funkce
def calculate_squares(n):
for i in range(n):
return i ** 2
# generátorová funkce
def generate_squares(n):
for i in range(n):
yield i ** 2
# 683 ns ± 52 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
%timeit calculate_squares(20000)
# 244 ns ± 6.12 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
%timeit generate_squares(20000)
Generátorové funkce je nejvhodnější používat tehdy, když nevíme, zda budeme potřebovat celý výsledek nebo pouze jeho část.
# v rámci for loopu
for square in generate_squares(20000000):
print(square)
if square > 100:
break
# jako generátor
generator = generate_squares(20000000)
next(generator)
next(generator)
next(generator)
Generátory jsou single-iteration objekty. To znamená, že je přes ně možné iterovat pouze jedenkrát. Pokud se chceme k výsledkům vracet, je nutné je ukládat.
generator = generate_squares(20)
for square in generator:
print(square)
# zde se již nic nevypíše, generátor je prázdný
for square in generator:
print(square)
Pokud výsledky uložíme do seznamu, můžeme je procházet vícekrát.
generator = list(generate_squares(20))
for square in generator:
print(square)
# zde se vypise
for square in generator:
print(square)
Generátorové výrazy
Obdoba list comprehension v kontextu iterátorů. Generátory můžeme vytvářet jednodušeji než definováním funkce.
# klasicky list comprehension (výpočet proběhne okamžitě)
[number ** 2 for number in range(10)]
# vrací - [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
# generátorový výraz
(number ** 2 for number in range(10))
# vrací - at 0x106b73ac0>
generator = (number ** 2 for number in range(10))
# zde se vypočítá první hodnota
next(generator)
# zde se vypočítá druhá hodnota
next(generator)
Příkaz yield from
Ve verzi 3.3 byl přidán příkaz yield from
na delegování generátoru. Pokud máme nějaký generátor a chceme z něj vytvořit další generátor je možné napsat funkci:
def my_gen(gen):
yield from gen
Příkaz yield from
je rovněž elegantním způsobem na vytvoření generátoru (iterátoru) z uložené sekvence.
def g(x):
yield from range(x, 0, -1)
yield from range(x)
list(g(5))
# vysledek: [5, 4, 3, 2, 1, 0, 1, 2, 3, 4]
Sekvence vs Iterátory
Nyní se podíváme na vytvoření vlastní sekvence pomoci implementování dunder metod __len__
a __getitem__
.
class Sentence:
"""Represents one sentence."""
def __init__(self, text):
self.text = text
self.words = text.split(' ')
def __repr__(self):
return f"Sentence({self.text})"
def __len__(self):
return len(self.words)
def __getitem__(self, index):
return self.words[index]
sentence = Sentence("Ahoj svete jak se mas")
# magie
for word in sentence:
print(word)
sentence[2]
len(sentence)
Nyní se podíváme na stejnou věc pohledem iterátoru. Zde je důležité rozlišit iterable a iterator. Třída Sentence
je iterable, třída SentenceIterator
je pak iterátor, který vrací metoda Sentence.__iter__
.
Pro vytvoření iterátoru je tedy nutné implementovat dunder metody __iter__
a __next__
.
class Sentence:
"""Represents one sentence."""
def __init__(self, text):
self.text = text
self.words = text.split(' ')
def __repr__(self):
return f"Sentence({self.text})"
def __iter__(self):
return SentenceIterator(self.words)
class SentenceIterator():
def __init__(self, words):
self.words = words
self.index = 0
def __next__(self):
try:
word = self.words[self.index]
except IndexError:
raise StopIteration
self.index += 1
return word
def __iter__(self):
return self
sentence = Sentence("Ahoj svete jak se mas")
# magie
for word in sentence:
print(word)
len(sentence)
Další možností je situaci implementovat jako generátor. Tady pouze upozorním, že nevyužíváme opravdového postupného výpočtu, pro demonstraci je však tento příklad dostačující.
class Sentence:
"""Represents one sentence."""
def __init__(self, text):
self.text = text
self.words = text.split(' ')
def __repr__(self):
return f"Sentence({self.text})"
def __iter__(self):
yield from self.words
sentence = Sentence("Ahoj svete jak se mas")
# magie
for word in sentence:
print(word)
iterator = iter(sentence)
next(iterator)
next(iterator)
next(iterator)
next(iterator)
next(iterator)
# StopIteration
next(iterator)
Jak využít toho, aby byl výpočet vyhodnocován postupně? Zkusme navrhnout generátor opravdový.
def split_generator(text, sep=[" "]):
"""Creates generator for splitted text by given separator"""
word = []
for char in text:
if char in sep:
if word:
yield "".join(word)
word = []
else:
word.append(char)
if word:
yield "".join(word)
class Sentence:
"""Represents one sentence."""
def __init__(self, text):
self.text = text
def __repr__(self):
return f"Sentence({self.text})"
def __iter__(self):
yield from split_generator(self.text):
# zde je rovněž možné použít generator expression
# return (word for word in split_generator(self.text))
# nejlepším řešením je však vrátit samotný generátor
# return split_generator(self.text)
sentence = Sentence("Ahoj svete jak se mas")
# magie
for word in sentence:
print(word)
if word == "svete":
break
iterator = iter(sentence)
next(iterator)
next(iterator)
next(iterator)
next(iterator)
next(iterator)
# StopIteration
next(iterator)
Zápočtové úkoly
Nevíte si rady? Přečtěte si "Jak pracovat s Github Classroom?".
- H02: Data 2 Náhled, Příjmout úkol
- H03: Data 3 Náhled, Příjmout úkol
Dobrovolné Úkoly
- L03E01: Credit Account Náhled, Příjmout úkol
- L03E02: Generator function Náhled, Příjmout úkol