10 pastí jazyka Python


(C) 2003 Hans Nowak. Written: 2003.08.13. Last update: 2003.09.05.
Díky Blake Winton, Joe Grossberg, Steve Ferg, Lloyd Kvam za hodnotné připomínky.

(C) 2005 Překlad Pavel Kosina a Petr Přikryl. Přeloženo: srpen 2005
anglický originál leží http://zephyrfalcon.org/labs/python_pitfalls.html


Nejde nutně o vady na kráse či nedodělky. Jsou to spíše vedlejší účinky vlastností jazyka, o které často zakopávají nováčci a někdy i zkušení programátoři. Při neúplném pochopení podstatných rysů chování jazyka Python se můžete spálit.

Tento dokument má být jakýmsi průvodcem pro ty, pro které je jazyk Python něčím novým. Dozvědět o pastech a léčkách brzy je lepší, než narazit na ně ve vytvářeném kódu těsně před termínem odevzdání :-} Tento dokument není míněn jako kritika jazyka. Jak jsem již řekl, většina těchto pastí není způsobena vadami jazyka.

1. Nepořádné odsazování

Možná trochu laciné téma na úvod. Nicméně, mnozí nováčci mají zkušenost s jazyky, kde mezery "nehrají roli". Cestou slepých uliček[1] mohou dojít až k nemilému překvapení, že je Python trestá za zlozvyk nepořádného odsazování.

Řešení: Dodržujte správné odsazování. Používejte buď mezery, nebo tabulátory[2], ale nikdy to nemíchejte. Kvalitní editor pomáhá.

2. Přiřazení, neboli jména a objekty

Lidé přicházející od staticky typovaných jazyků, jako je Pascal nebo C, často předpokládají, že Python zachází s proměnnými a s přiřazováním stejně, jako jejich oblíbený jazyk. Na první pohled to tak skutečně vypadá:

a = b = 3
a = 4
print a, b  # 4, 3

Dostávají se však do potíží, když začnou používat měnitelné objekty. Často pak hned přicházejí s tvrzením, že Python zachází s měnitelnými a neměnitelnými objekty rozdílně.

a = [1, 2, 3]
b = a
a.append(4)
print b
# b je nyní také [1, 2, 3, 4]

Stalo se to, že výraz a = [1, 2, 3] provedl dvě věci: 1. vytvořil objekt, v tomto případě seznam s hodnou [1, 2, 3]; 2. svázal ho se jménem a v lokálním prostoru jmen. Výraz b = a pak svázal jméno b s tím samým seznamem (na který již odkazuje a)[3]. Jakmile si toto uvědomíte, bude již méně obtížné pochopit, co vlastně a.append(4) dělá... že mění seznam, na který se odkazuje jak a tak b.

Domněnka, že se s měnitelnými a neměnitelnými objekty při přiřazování zachází rozdílně, je mylná. Při přiřazování a = 3 a b = a se děje přesně stejná věc, jako u výše uvedeného seznamu. Jména a i b nyní odkazují na stejný objekt — na číslo s hodnotou 3. Protože však čísla jsou neměnitelná (immutable), nepozorujete žádný vedlejší efekt [4].

Řešení: Přečtěte si toto. Abyste se zbavili nechtěných vedlejších efektů, kopírujte (používejte metodu copy, operátory řezu (slice), atd). Python nikdy implicitně nekopíruje.

3. Operátor +=

V jazycích jako třeba C, jsou rozšířené přiřazovací operátory jako += zkratkami pro delší výrazy. Například,

x += 42;

je syntaktická pomůcka (angličani říkají syntactic sugar) pro

x = x + 42;

Takže byste si mohli myslet, že tomu tak bude i v jazyce Python. Na první pohled to tak skutečně vypadá:

a = 1
a = a + 42
# a je 43
a = 1
a += 42
# a je 43

Ale u měnitelných objektů (mutable) zápis x += y nemusí nutně vyjadřovat totéž, jako zápis x = x + y. Uvažujme seznamy:

>>> z = [1, 2, 3]
>>> id(z)
24213240
>>> z += [4]
>>> id(z)
24213240
>>> z = z + [5]
>>> id(z)
24226184

Příkaz x += y mění seznam na místě a má stejný důsledek jako metoda extend. Příkaz z = z + y vytváří nový seznam a sváže ho se znovu použitým jménem z, což je ale něco jiného, než u předchozího příkazu. Jde o jemný rozdíl vedoucí k delikátním a těžko zachytitelným chybám.

Aby toho nebylo dost, vede to také k překvapivému chování, když se míchají měnitelné a neměnitelné kontejnery:

>>> t = ([],)
>>> t[0] += [2, 3]
Traceback (most recent call last):
  File "<input>", line 1, in ?
TypeError: object doesn't support item assignment
>>> t
([2, 3],)

N-tice, samozřejmě, nepodporují přiřazování svých prvků. Jenže po provedení += se seznam uvnitř změnil! Důvod je opět v tom, že += mění seznam na místě. Přiřazení prvku n-tice sice nefunguje, ale když se stane výjimka, tak prvek již byl na místě změněn.

Tuto léčku já osobně považuji za vadu na kráse[5]? :-).

Řešení: podle vašeho postoje k tomuto problému můžete buď se kompletně používání vyvarovat += nebo to používat jen pro čísla anebo se s tím naučit žít ...

4. Atributy tříd versus atributy instancí

Zde se chybuje nejméně ve dvou věcech. Tak za prvé, nováčci pravidelně přidávají atributy do třídy (místo do instance) a diví se, když jsou pak tyto atributy sdíleny mezi instancemi:

>>> class Foo:
...     bar = []
...     def __init__(self, x):
...         self.bar.append(x)
...     
>>> f = Foo(42)
>>> g = Foo(100)
>>> f.bar, g.bar
([42, 100], [42, 100])

Nejde o vadu, ale šikovný rys, kterého můžeme v řadě situací využít. Nepochopení vyplývá z faktu, že byly použity atributy třídy a ne atributy instance. Může to být i tím, že se atributy instancí v Pythonu vytvářejí jinak, než v dalších jazycích. V jazycích C++, Object Pascal a dalších se deklarují ve těle třídy.

Další (malá) léčka spočívá v tom, že self.foo může odkazovat na dvě věci: na atribut instance foo nebo — pokud tento neexistuje — na atribut třídy foo. Porovnejte:

>>> class Foo:
...     a = 42
...     def __init__(self):
...         self.a = 43
...     
>>> f = Foo()
>>> f.a
43

a druhý případ

>>> class Foo:
...     a = 42
...     
>>> f = Foo()
>>> f.a
42

V prvním příkladě f.a odkazuje na atribut instance s hodnotou 43. Má přednost před atributem třídy s hodnotou 42. V druhém příkladě žádný atribut instance a neexistuje, takže f.a odkazuje na atribut třídy.

Následující ukázka oba případy kombinuje:

>>> class Foo:
...     
...     bar = []
...     def __init__(self, x):
...         self.bar = self.bar + [x]
...     
>>> f = Foo(42)
>>> g = Foo(100)
>>> f.bar
[42]
>>> g.bar
[100]

V příkazu self.bar = self.bar + [x] neodpovídají zápisy obou self.bar stejnému odkazu... Druhý zápis odkazuje na atribut třídy bar. Výsledek je poté svázán s atributem instance.

Řešení: Tento rozdíl může být matoucí, ale není nepochopitelný. Atributy tříd používejte v situacích, když chcete něco sdílet mezi více instancemi třídy. Abyste se vyhnuli nejednoznačnosti, můžete se na ně odkazovat zápisem self.__class__.jmeno místo zápisu self.jmeno i v případech, kdy neexistuje žádný atribut instance tohoto jména. Pro atributy, které mohou v každé instanci nabývat jiné hodnoty, používejte atributy instancí a odkazujete se na ně přes self.jmeno.

Aktualizace: Vícero lidí poznamenává, že pasti číslo 3 a 4 se dají kombinovat do ještě zábavnějšího hlavolamu:

>>> class Foo:
...     bar = []
...     def __init__(self, x):
...             self.bar += [x]
...             
>>> f = Foo(42)
>>> g = Foo(100)
>>> f.bar
[42, 100]
>>> g.bar
[42, 100]

Důvod pro toto chování je ten, že self.bar += něco není stejné jako self.bar = self.bar + něco. Zápis self.bar zde vyjadřuje odkaz na Foo.bar, takže f i g aktualizují stejný seznam.

5. Měnitelné implicitní argumenty

Tato past trápí začátečníky znovu a znovu. Ve skutečnosti to je varianta pasti číslo 2, kombinovaná s neočekávaným chováním implicitních argumentů. Uvažujme následující funkci:

>>> def popo(x=[]):
...     x.append(666)
...     print x
...     
>>> popo([1, 2, 3])
[1, 2, 3, 666]
>>> x = [1, 2]
>>> popo(x)
[1, 2, 666]
>>> x
[1, 2, 666]

To se dalo čekat. Ale teď:

>>> popo()
[666]
>>> popo()
[666, 666]
>>> popo()
[666, 666, 666]

Možná jste čekali, že výstup bude ve všech případech [666]?... vždyť když voláme popo() bez argumentů, bere se přeci [] jako implicitní, že jo? Ne. Implicitní argument se volá *jednou* a to když se funkce *vytváří* a ne když se volá. (Jinými slovy, u funkce f(x=[]) se x nepřiřazuje pokaždé, když se funkce volá. Do x se přiřadí [], jen když se funkce definuje[6]. Pokud se jedná o měnitelný objekt, a ten se změnil, bude příští volání funkce za svůj implicitní argument považovat stejný seznam, který už ale má jiný obsah.

Řešení: Toto chování může být někdy užitečné. Ale obecně byste si na tyto vedlejší efekty měli dávat pozor.

6. UnboundLocalError

Tato chyba se podle manuálu objeví v případě, kdy se jméno "odkazuje na lokální proměnnou, která ještě dosud nebyla navázána (bound)". To zní tajemně. Nejlepší to bude ukázat na malém příkladě:

>>> def p():
...     x = x + 2
...     
>>> p()
Traceback (most recent call last):
  File "<input>", line 1, in ?
  File "<input>", line 2, in p
UnboundLocalError: local variable 'x' referenced before assignment

Uvnitř p nemůže být výraz x = x + 2 proveden, protože x ve výrazu x + 2 ještě nemá žádnou hodnotu. To zní rozumně. Nemůžete se odkazovat na jméno, které ještě neexistuje. Ale zvažme následující:

>>> x = 2
>>> def q():
...     print x
...     x = 3
...     print x
...     
>>> q()
Traceback (most recent call last):
  File "<input>", line 1, in ?
  File "<input>", line 2, in q
UnboundLocalError: local variable 'x' referenced before assignment

Tento úsek kódu by se vám mohl zdát správný -- nejprve se vytiskne 2 (hodnota globální proměnné x), pak se lokální proměnné x přiřadí 3 a její hodnota se vytiskne (3). Takhle to však nefunguje. Je to dáno pravidly pro rozsah viditelnosti (platnosti, použitelnosti jmen). Jsou vysvětlena v referenční příručce:

Pokud je vazba jména provedena uvnitř bloku, jde o lokální proměnnou tohoto bloku. Pokud je vazba jména[7] provedena na úrovni modulu, jde o globální proměnnou. (Proměnné bloku kódu modulu jsou lokální a globální.) Pokud je proměnná použita v kódu bloku, ale není zde definovaná, jedná se o volnou proměnnou.

Pokud není jméno vůbec nalezeno, vyvolá se výjimka NameError. Pokud se jméno odkazuje na lokální proměnnou, pro kterou dosud nebyla provedena vazba, vyvolá se výjimka UnboundLocalError.

Jinými slovy: Uvnitř funkce může proměnná být lokální nebo globální, ale ne obojetná. (Nezáleží na tom, jestli později provedete změnu vazby.) Ve výše uvedeném příkladu Python určí, že proměnná x je lokální (na základě zmíněných pravidel). Ale při následném provádění funkce se narazí na příkaz print x a x ještě nemá žádnou hodnotu... a tudíž je to chyba.

Povšimněte si, že pokud by bylo tělo funkce složeno pouze z jednoho řádku print x nebo z řádků x = 3; print x bylo by to naprosto v pořádku.

Řešení: Používání lokálních a globálních proměnných tímto způsobem nemíchejte.

7. Chyby při zaokrouhlování desetinných čísel

Při tisku hodnot desetinných čísel (float) může být výsledek někdy překvapující. Aby byly věci ještě zajímavějšími, mohou se reprezentace vracené funkcemi str() a repr() lišit. Ukázka říká vše:

>>> c = 0.1
>>> c
0.10000000000000001
>>> repr(c)
'0.10000000000000001'
>>> str(c)
'0.1'

V dvojkové soustavě (kterou používá procesor) nelze řadu čísel vyjádřit přesně. Skutečná hodnota se hodnotě zapsané v desítkové soustavě pouze blíží.

Řešení: Více informací se dozvíte z následujícího tutoriálu.

8. Spojování řetězců

Poznámka překladatele: Odstraněno, již neplatí. Uvádělo se zde, že je méně výhodné několikanásobné slučování (sčítání) řetězců. Místo toho se radilo převézt řetězec na seznam, pak několikanásobné append u seznamu a zpětný převod na řetězec. Následující script ukazuje neplatnost tohoto pravidla u Python 2.5, pravděpodobně vlivem efektivnějšího provádění smyček při operacích s řetězci.

import timeit

def f():
    s = ""
    for i in range(100000):
        s = s + "abcdefg"[i % 7]
    

t=timeit.Timer("f()","from __main__ import f")
print t.timeit(1)

# vysledek python 2.5: 0.0705371772111


def g():
    z = []
    for i in range(100000):
        z.append("abcdefg"[i % 7])
    return ''.join(z)
    
t=timeit.Timer("g()","from __main__ import g")
print t.timeit(1)

# vysledek python 2.5: 0.0885903096623

9. Binární režim pro soubory

Nebo spíše, používání binárního režimu není tím, co způsobuje zmatek. Některé operační systémy, jako Windows, dělají rozdíl mezi binárními a textovými soubory. Pro ilustraci si uveďme, jak lze v jazyce Python otvírat soubory v binárním nebo textovém režimu:

f1 = file(jmenosouboru, "r")  # text
f2 = file(jmenosouboru, "rb") # binárně

V textovém režimu, mohou být řádky zakončované znakem "nová řádka" a/nebo "návrat vozíku" (\n, \r, nebo \r\n). Binární režim si na něco takového nehraje. Když ve Windows čteme ze soubor v textovém režimu, reprezentuje Python konce řádku znakem \n (universální). Ale v binárním režimu dostaneme \r\n. Při čtení dat proto můžeme v každém z těchto režimů získat velmi rozdílné výsledky.

Existují systémy, které nerozlišují mezi textovým a binárním režimem. Například v Unixu jsou soubory otevírány vždy v binárním módu. Díky tomu může kód psaný pro Unix a otevírající soubor v režimu 'r' dávat jiné výsledky při spuštění pod Windows. Může se také stát, že někdo přicházející z Unixu může použít příznak 'r' i ve Windows a bude nemile překvapen výsledky.

Řešení: Používejte správné flagy — 'r' pro textový režim (i na Unixu), 'rb' na binární režim.

10. Zachytávání několika výjimek najednou

Někdy potřebujete v jednom except zachytit několik výjimek v jednom. Automaticky nás napadne nás, že by mohlo fungovat následující:

try:
    ...něco co vyvolá chybu...
except IndexError, ValueError:
    # "mělo by" zachytit chyby IndexError a ValueError
    # špatně!

Tohle bohužel nefunguje. Důvody se stanou jasnějšími při porovnání s kódem:

>>> try:
...     1/0
... except ZeroDivisionError, e:
...     print e
...     
integer division or modulo by zero

První "argument" v klauzuli except uvádí třídu výjimky, druhý uvádí volitelné jméno, které bude navázáno na aktuální objekt vyvolané výjimky. Takže v předchozím chybném kódu by klauzule except zachytila IndexError a jméno ValueError by svázala s objektem výjimky. To asi není, co jsme chtěli. ;-)

Tohle funguje lépe:

try:
    ...něco co vyvolá chybu...
except (IndexError, ValueError):
    # správně zachytí IndexError a ValueError

Řešení: Když odchytáváte několik výjimek v jedné klausuli except, používejte závorky na vytvoření n-tice s výjimkami.


Jaké další nástrahy tu jsou? Napadají mne snad:

Příbuzné odkazy:


Poznámky překladatele:

[1] "... Tudy cesta nevede. Vyfukováním kouře do umyvadla s vodou zlato opravdu nevzniká." – J.C.
[2] S mezerami nikdy problémy nebyly. S tabulátory ano. Někteří si myslí, že se tabulační pozice nemají nastavovat po 8 sloupcích.
[3] Takže v tuto chvíli existuje jeden seznam, jeden objekt, na který ukazují dvě jména.
[4] Přiřazením a = 4 se zruší vazba na celočíselný objekt s hodnotou 3 a vznikne vazba na celočíselný objekt s hodnotou 4.
[5] N-tice je sice neměnná, ale udržuje pouze odkazy na jiné objekty. Odkazy se skutečně měnit nemohou. Zápis t[0] reprezentuje odkaz na seznam. Nikde se ale neříká, že by n-tice nemohla obsahovat odkazy na měnitelné objekty, které mohou být navíc navázány i na jiná jména a tudíž měněny odjinud. Chyba tedy nespočívá v tom, že se změnil obsah seznamu, ale v tom, že vůbec vznikla výjimka.
[6] ... když Python při spuštění programu poprvé kód zpracovává a u funkcí si zapamatovává právě tyto implicitní argumenty (a také proměnné i další deklarace uvnitř funkcí). Vytváří se při tom vnitřní objekt, který reprezentuje zkompilovanou funkci. A o tom to je.
[7] Jak již bylo vysvětleno u pasti číslo 2, proměnnou se v Pythonu rozumí jméno, které se odkazuje na objekt. Přiřazením hodnoty proměnné se provede pouze svázání jména proměnné s uvedeným objektem tak, že se ve vnitřním slovníku vytvoří dvojice (jméno, odkaz na objekt). Této akci se říká provedení vazby jména. Pokud ve vnitřním slovníku neexistuje položka s klíčem odpovídajícím jménu, pak vazba nebyla provedena. Zmíněných vnitřních slovníků, které Python využívá, je více. V jednom z nich jsou zachyceny vazby jmen globálních proměnných. Pro každou lokální úroveň je vytvořen příslušný (jiný, oddělený, další) vnitřní slovník.



subject:
  ( 112 subscribers )