Zbytečné optimalizace

cs v kategorii code • 7 min. čtení

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á.



Sdílejte:   Facebook   Twitter   Reddit   Tumblr   Pinterest




Může se vám také líbit

en What Makes Good Program?, November 20, 2018
en Old Code, October 31, 2018
en Fast JSON Schema for Python, October 1, 2018
en Open Source Responsibilities, September 6, 2018
en Deployment of Python Apps, August 15, 2018


Populární v kategorii code

en Makefile with Python, November 6, 2017
en Deployment of Python Apps, August 15, 2018
cs Jasně, umím Git…, August 6, 2014
cs Checklist na zabezpečení webových aplikací, March 1, 2016
cs Pokročilé regulární výrazy, August 17, 2014