Jste zde

Co se mi nelíbí na Pythonu

Neberte mě špatně - Python je skvělý jazyk. Je mým oblíbeným jazykem a programuji v něm s přestávkami od roku 2007 (někdy od verze 2.5). Žádný jazyk ale není perfektní a Python není žádnou výjimkou. V dnešním příspěvku bych se s vámi chtěl podělit o skutečnosti, které se mi na Pythonu příliš nelíbí.

V příspěvku se budu zabývat pouze poslední verzí trojkové řady Pythonu, tedy Pythonem 3.4.2. Dvojkovou řadu, ve které by se toho, co se mi nelíbí, našlo mnohem více, vynechám.

1) Privátní/interní viditelnost je založena na znepřehledňující konvenci (podtržítka v názvu)

V Pythonu je vše veřejné a nic takového, jako private/protected/internal v něm nenajdete. Pokud chcete nějakou funkci/třídu/proměnnou udělat privátní/interní, lze tak učinit pouze pomocí konvence, a to prefixováním názvu jedním či dvěma podtržítky (vysvětlení rozdílu). Ať se ale snažím jak se snažím, tak na toto si pořád nemohu zvyknout a kód využívající tyto konvence mně přijde nepřehledný. Příklad:

class A:
    def public_method(self):
        self._internal_method()
        self.__private_method()
 
    def _internal_method(self):
        ...
 
    def __private_method()
        ...
 
def _internal_func():
    ...
 
_internal_var = 5

Pokud je volání takových metod/funkcí více, tak kód spíše než Python připomíná implementaci standardních knihoven pro C a C++:

#if defined __USE_ISOC99 || defined __USE_UNIX98
__BEGIN_NAMESPACE_C99
/* Maximum chars of output to write in MAXLEN.  */
extern int snprintf (char *__restrict __s, size_t __maxlen,
             const char *__restrict __format, ...)
     __THROWNL __attribute__ ((__format__ (__printf__, 3, 4)));
 
extern int vsnprintf (char *__restrict __s, size_t __maxlen,
              const char *__restrict __format, _G_va_list __arg)
     __THROWNL __attribute__ ((__format__ (__printf__, 3, 0)));
__END_NAMESPACE_C99
#endif

Navíc se jedná pouze o konvenci, takže k takovým proměnným/třídám/metodám se dá tak či tak dostat a nikdo vám v tom nezabrání. Některé takové "interní" proměnné se dokonce používají v reálném kódu (typickým případem je sys._getframe(), která je dokonce popsaná v oficiální dokumentaci).

Myšlenka za přístupem Pythonu je "we're all consenting adults here". Tento přístup ale nesdílím. Když se totiž k něčemu lze dostat, tak si můžete být jisti, že to někdo udělá. Už se mi stalo, že jeden z vývojařů v týmu použil "interní" funkci, kterou jsem měl ve svém modulu pouze pro implementační účely. Při změně implementace jsem ji vyhodil, protože už nebyla potřeba a jednalo se přece o "interní" funkci. Pak jsme se jen divili, proč nám kód přestal fungovat...

Škoda, že v Pythonu není vestavěná podpora pro interní funkce/metody atd., která by zaručila, že se k nim externě nebude přistupovat a která by nevyžadovala používání ošklivých podtržítek. Ruby, kde jsi?

2) Global Interpreter Lock (GIL)

Toto se technicky netýká Pythonu jakožto jazyka, ale standardní implementace CPython, která je patrně nejpoužívanější implementací. Pokud nevíte, kterou implementaci používáte, tak je to téměř jistě právě CPython. Některé jiné implementace, např. Jython (Python pro Java platformu) GIL nepoužívají.

O co jde? GIL je mechanismus, kterým si CPython zaručuje, že v jeden okamžik je bytekód vykonáván jediným vláknem. Co to pro nás znamená? Že v CPythonu nemá smysl rozdělovat výpočetně náročné úlohy na vlákna, protože tato vlákna nikdy nepoběží paralelně na více jádrech/procesorech. V jeden okamžik vždy poběží jen jediné vlákno. GIL má některé výhody, především zrychlení činnosti programů s jedním vláknem a jednodušší integrace s Céčkovými knihovnami, které nejsou thread-safe. Nevýhoda je ta, že vlákna se v CPythonu hodí pouze na konkurentní úlohy, nikoliv paralelní úlohy (vysvětlení rozdílu). Typicky je tak lze využít pouze při vstupně/výstupních operacích (např. čtení/zápis souborů, síťová komunikace, čekání na události) či operacích, které nejsou výpočetně náročné. Dále kvůli GILu může docházet ke kuriózním situacím, kdy skript využívající vlákna je rychlejší na jednoprocesorovém systému než na víceprocesorovém systému. Pokud by vás zajímaly detaily, doporučuji toto video od Davida Beazleyho.

Jak to obejít? Použitím procesů místo vláken, tedy modulu multiprocessing místo threading. Samozřejmě se všemi důsledky, které s tím souvisí (každý proces je samostatný interpret Pythonu a má tedy odlišný paměťový prostor, procesy mají typicky větší režii při vytváření/zanikání než vlákna atd.).

Poznámka: Referenční implementace jazyka Ruby, Ruby MRI, napsaná v Céčku, má taky GIL.

3) super()

Standardní funkce super() slouží pro delegování volání metod na bázové třídy (angl. base classes) či sourozenecké třídy (angl. sibling classes). Příklad použití:

class C(B):
    def method(self, arg):
        super().method(arg)  # Zavolá B.method(self, arg). A nebo ne?

Problémy s touto funkcí jsou dobře známé (viz např. tento a tento článek, tento příspěvek a strany 1041-1064 v páté edici Learning Python). Konkrétně bych vypíchl následující skutečnosti:

  • Nemusí vždy zavolat nadtřídu. Funkce super() má dvě typická použití. V případě jednoduché dědičnosti ji lze využít k zavolání metody v bázové třídě bez toho, aby bylo nutno explicitně specifikovat název bázové třídy. U vícenásobné dědičnosti se pak využívá k podpoře toho, aby při delegování nedocházelo k vícenásobnému volání metody v některé z nadtříd (např. u tzv. diamantu) a došlo k zavolání metod u sourozeneckých tříd. Nemusí tedy vždy dojít k zavolání metody z nadtřídy, jak by se např. Java programátoři mohli mylně domnívat. Ve skutečnosti volaná třída závisí na class.__mro__, což je atribut obsahující třídy a určující pořadí, v jakém se vyhledávají atributy/metody v třídních hierarchiích. Je to zkratka z Method Resolution Order. Vhodnější pojmenování by tak bylo next_method(), jako to má Dylan.
  • Syntaktická odlišnost od běžného Python kódu. Začínající programátoři v Pythonu se velmi brzy naučí, že k tomu, abyste zavolali metodu, musíte před ní napsat self.:
    class A:
        def foo(self):
            bar()       # NameError: name 'bar' is not defined
            self.bar()  # OK.
     
        def bar(self):
            ...

    Když pak uvidí použití super(), tak jim (zcela oprávněně!) nejde do hlavy, proč tam nemusí být self:

    class B(A):
        def foo(self):
            super().foo()  # Bez self?!

    Důvodem, proč tam nemusí být self, je ten, že Python si ho najde sám (ovšem pouze v tomto případě!). Implementačně je to na úrovni Pythonu řešeno pomocí černé magie. Konkrétně to funguje tak, že se interpret podívá do aktuálního stack framu pro detekci self, zjistí třídu, na které má být metoda zavolána a vytvoří speciální proxy objekt, který slouží pro zavolání této metody. Toto podle mě jde proti filosofii Pythonu ("explicit is better than implicit").

  • Vyžaduje si konzistentní použití napříč celou třídní hierarchií. Pokud některá (byť jediná) ze tříd v hierarchii super() nepoužívá, nebude to fungovat tak, jak očekáváte.
  • Nefunguje pro operátory. Zatímco B.__getitem__(self, i) funguje, super()[i] nikoliv.

4) Nekonzistence pojmenování

Toto časem potká každý jazyk, u kterého není jasně stanovená, rozšířená a dodržovaná konvence pojmenování. I přes existenci PEP8 (Style Guide for Python Code) se nekonzistence pojmenování Pythonu nevyhnula. Uveďme si to na příkladu pojmenování některých funkcí/metod ze standardní knihovny. U nich je dle PEP8 doporučeno použít snake_case:

Bohužel, takových případů je ve standardní knihovně celá řada... Celkem to zdržuje, protože mnohdy člověk zapomene, že se v názvu funkce podtržítka nepoužívají či naopak používají.

5) Nesystematická a mnohdy nepřehledná dokumentace

Oproti některým jiným dokumentacím (např. k C++ či PHP) mně dokumentace k Pythonu přijde nesystematická a mnohdy chaotická. Mrkněte např. na dokumentaci k subprocess.Popen. Popis jednotlivých parametrů je provázán s doplňkovým textem, který upozorňuje na rozdíly mezi POSIXovou a Windowsovou verzí a na možné problémy. Schválně, jak dlouho vám bude trvat, než najdete popis parametru preexec_fn? "Use CTRL+F, young padawan!"

Ona dokumentace k Pythonu není vyloženě špatná. Když si na ni zvyknete, tak se to dá přežít. Mnohem více by se mi ale líbilo, kdyby byla dodržena nějaká konvence. Např. ke každé funkci uvést její signaturu, poté vysvětlení, k čemu slouží, následně popis parametrů v seznamu, jeden za druhým, popis návratové hodnoty, popis side efektů, příklad použití a pak komentáře. Mnohdy totiž dokumentace není vyčerpávající (co se stane, když...) a příklad by vydal za tisíce slov.

6) Závislost na operačním systému

Python je do značné míry závislý na operačním systému. Mnoho modulů a funkcí má odlišné chování na Linuxu a na Windows. Některé moduly/funkce pak jsou dostupné pouze na jednom z těchto systémů. Psát platformně nezávislý kód tak sice možné je, ale je to plné úskalí a znesnadňuje to tvorbu vysoce kvalitního kódu. Vezměme si např. výše zmiňovaný modul multiprocessing. Ten se na Linuxu chová diametrálně odlišně, než na Windows. Důvodem je, že na Linuxu se ke spouštění procesů používá fork + exec, kdežto na Windows se používá CreateProcess. Oba způsoby se chovají rozdílně, což má vliv na funkcionalitu kódu využívající multiprocessing. Jediná možnost, jak se alespoň částečně ujistit, že kód poběží stejně na Linuxu i na Windows, je pomocí multiprocessing.set_start_method() nastavit metodu spouštění procesů natvrdo na spawn. Ta je defaultní metodou na Windows a v Linuxu ji je možné od Pythonu 3.4 simulovat. Když tuto metodu nenastavíte, tak se na Linuxu použije metoda fork.

Dále, i když se Python tváří jako vysokoúrovňový jazyk, tak některé funkce závisí na nízkoúrovňových detailech operačního systému. Např. chceme spustit externí příkaz a odchytit si výstup do řetězce:

import io
import subprocess
 
stdout_str = io.StringIO()  # file-like objekt ukládající data do řetězce
p = subprocess.Popen(['program', 'arg1', 'arg2'], stdout=stdout_str)
p.wait()

Zde ovšem program skončí na výjimce io.UnsupportedOperation: fileno, protože StringIO (obdoba std::stringstream v C++) nemá na úrovni operačního systému reprezentaci ve formě file deskriptoru. Samozřejmě, lze namítnout, že k získání výstupu lze použít subprocess.check_output(), ale co když chcete výstup odchytávat průběžně a něco s ním dělat? "So long, and thanks for all the fish."

Kéž by Python byl natolik vysokoúrovňový, že by tyto věci nebylo potřeba řešit... Alespoň, že je tak kód rychlejší, než kdyby bylo všechno abstrahované.

7) for-else

Méně známá skutečnost o Pythonu je, že za for lze napsat else větev. Problém vidím v tom, že kód využívající tuto konstrukci nabízí dvě možné interpretace, z nichž osobně mi přijde intuitivnější ta, jak se to nechová. Vezměme si následující kód:

for item in some_list:
    if item == 5:
        break
else:
    print("X")

Kdy dojde k vypsání X? Když (a) some_list neobsahuje žádné prvky, nebo když (b) není nalezen prvek s hodnotou 5? Takovýto kód zbytečně zatěžuje čtenáře nutnou znalostí obskurní syntaxe a přímo svádí k tomu, že zůstane nepochopen těmi, kdo kód spravují. Dokonce bych si troufal tvrdit, že tato konstrukce jde proti filosofii Pythonu. Tak proč vůbec takovouto konstrukci v Pythonu mít?

P.S. Správná odpověď je ta, že se X vypíše v případě, kdy v cyklu nenastane break, čili some_list neobsahuje pětku. Místo else by tedy bylo vhodnější něco jako nobreak.

Příspěvky ostatních s podobným námětem

Přidat komentář