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 +
.
Zápis něco
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ýjimkaUnboundLocalError
.
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:
- zpětné lomítka v řetězcích (bez raw), obzvláště u cest Windows/DOS
- dělení celých čísel (v dalších verzích bude snad změněno)
Příbuzné odkazy:
- Python Gotchas od Steve Ferg
- Python Warts od Andrew Kuchling
- Python anti-pitfalls od Richard Jones
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. |