Vývojáři mají skvělý dar. Optimalizovat to, co je nejvíc zbytečné. Aneb příklad z praxe. :-)
Potřebovali jsme funkci batches
s následujícím chováním podporující
jakýkoliv iterable:
>>> list(batches.batches(range(10), 5))
[[0, 1, 2, 3, 4], [5, 6, 7, 8, 9]]
>>> list(batches.batches(range(10), 3))
[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]
Udělali jsem tedy základní implementaci během minutky:
def batches(iterable, batch_size):
bucket = []
for item in iterable:
bucket.append(item)
if len(bucket) == batch_size:
yield bucket
bucket = []
if bucket:
yield bucket
Jenže… jenže to nevypadá sexy. Přeci to musí jít udělat nějak cool! A taky
efektivněji. Například pro velký batch_size
to musí být náročné. Pojďme
zkusit udělat generátor generátorů.
def batches(iterable, batch_size):
def batch_generator():
for x in range(batch_size):
yield next(iterable)
while iterable:
yield batch_generator()
Vypadá to docela hezky. Jenže to má zásadní problém. Předchozí ukázka totiž
nikdy neskončí. Jakmile se dojede vstupní iterable
, funkce bude generovat
prázdné generátory do nekonečna. To je potřeba nějak vyřešit…
První nápad je odchytávat StopIteration
. Co ale dál? Během chvilky jsme se
dostali do velmi nehezkých konstrukcí a stejně zůstali v nekonečné smyčce.
Kolegu napadlo si z vnitřního iterátoru předávat, zda bude něco následovat
nebo ne. Idea je taková, že si první elementy ohandluju ručně nějak takto:
def batches(iterable, batch_size):
def batch_generator():
try:
first = next(iterable)
except StopIteration:
yield False # Hele, uz nic neprijde, tak to stopni.
else:
yield True # Jasne, jeste neco prijde.
yield first
for x in range(1, batch_size):
yield next(iterable)
while True:
gen = batch_generator()
if next(gen): # Prvni polozka je rucne vlozena.
yield gen
else:
break
Super! Máme generátory. Generují dokonce správně. Jenže… jenže to vypadá
opravdu ošklivě. Navíc je podporován jen takový vstup, který zvládne next
.
Zkusili jsme testy, zda jdeme dobrým směrem a… horší než jednoduchá jasná
implementace! Možná to šetří pamětí, ale podstatně trpí výkon kvůli samému
generování.
Tak znovu na začátek a jinak a lépe. itertools
obsahují funkci groupby
. S
pomocí Google jsme došli k následujícímu řešni:
def batches(iterable, batch_size):
iterable = iter(iterable)
counter = itertools.count()
def func(key):
return next(counter) // batch_size
for key, group in itertools.groupby(iterable, func):
yield group
Vypadá elegantně, ale pokud k tomu nebude výklad třikrát tak dlouhý, jako celá
funkce, nebude jasné, co se děje. A výsledek? Taky pomalejší. Výkon s
groupby
se od předchozího kódu moc neliší.
Závěr: neoptimalizujte. Optimalizujte až v případě, kdy je to opravdu potřeba. Raději pište čitelný kód. A pokud už bude potřeba optimalizace, ten čitelný kód nemažte – nechte ho vedle toho optimalizovaného. Aspoň budete moci rychle přečíst, co daná funkce dělá, a především zkontrolovat, zda ta zoptimalizovaná dělá skutečně to, co má.