Edit detail for PastiJazykaPython revision 2 of 1

2
Editor: mol
Time: 2013/09/04 21:58:25 GMT+2
Note: fixed wikilink

changed:
-
  <h1>10 pastí jazyka Python</h1>

<hr>
  <p>(C) 2003 <a href=
  "mailto:hans@zephyrfalcon.org?subject=Python%20pitfalls">Hans
  Nowak</a>. Written: 2003.08.13. Last update: 2003.09.05.<br>
  Díky Blake Winton, Joe Grossberg, Steve Ferg, Lloyd Kvam za
  hodnotné připomínky.</p>

  <p>(C) 2005 Překlad <a href="http://www.geon.wz.cz">Pavel Kosina</a>
   a <a href="http://www.skil.cz/python">Petr Přikryl</a>. Přeloženo: srpen 2005<br>
  anglický originál leží
  http://zephyrfalcon.org/labs/python_pitfalls.html</p>
  
<hr>

  <p>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.</p>

  <p>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 <b>není</b> míněn jako kritika jazyka.
  Jak jsem již řekl, většina těchto pastí není způsobena vadami
  jazyka.</p>

  <h3>1. Nepořádné odsazování</h3>

  <p>Možná trochu laciné téma na úvod. Nicméně, mnozí nováčci mají
  zkušenost s jazyky, kde mezery "nehrají roli". <em>Cestou slepých
  uliček</em><sup class="trnote" id="B_indent"
  title="... jak formuloval jeden nedoceněný český velikán" 
  ><a href="#P_indent">[1]</a></sup> mohou dojít až k
  nemilému překvapení, že je Python trestá za zlozvyk nepořádného
  odsazování.</p>

  <p><strong>Řešení</strong>: Dodržujte správné odsazování. Používejte
  buď mezery, nebo tabulátory<sup class="trnote" id="B_spaces"
  title="Doporučuje se dávat přednost mezerám."><a href="#P_spaces"
  >[2]</a></sup>, ale nikdy to nemíchejte. Kvalitní editor pomáhá.</p>

  <h3 id="pitfall2">2. Přiřazení, neboli jména a objekty</h3>

  <p>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á:</p>
  
<pre>
a = b = 3
a = 4
print a, b  # 4, 3
</pre>

  <p>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ě.</p>
  
<pre>
a = [1, 2, 3]
b = a
a.append(4)
print b
# b je nyní také [1, 2, 3, 4]
</pre>

  <p>Stalo se to, že výraz <code>a = [1, 2, 3]</code> 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 <code>a</code> v lokálním prostoru
  jmen. Výraz <code>b = a</code> pak svázal jméno
  <code>b</code> s tím samým seznamem (na který již odkazuje
  <code>a</code>)<sup class="trnote" id="B_assign"
  title="a ještě jinými slovy..."><a href="#P_assign">[3]</a></sup>. 
  Jakmile si toto uvědomíte, bude již méně obtížné pochopit, co
  vlastně <code>a.append(4)</code> dělá... že mění seznam, na který se
  odkazuje jak <code>a</code> tak <code>b</code>.</p>

  <p>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í <code>a =
  3</code> a <code>b = a</code> se děje přesně stejná věc, jako u výše
  uvedeného seznamu. Jména <code>a</code> i <code>b</code> nyní
  odkazují na stejný objekt &mdash; na číslo s hodnotou 3. Protože
  však čísla jsou neměnitelná (immutable), nepozorujete žádný vedlejší
  efekt <sup class="trnote" id="B_assign2" title="...detaily"><a 
  href="#P_assign2">[4]</a></sup>.</p>

  <p><strong>Řešení</strong>: Přečtěte si <a href=
  "ObjektyJazykaPython" title="">toto</a>. 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.</p>

  <h3>3. Operátor +=</h3>

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

  <p>je syntaktická pomůcka (angličani říkají <em>syntactic sugar</em>) 
  pro</p>
<pre>
x = x + 42;
</pre>

  <p>Takže byste si mohli myslet, že tomu tak bude i v jazyce Python. 
  Na první pohled to tak skutečně vypadá:</p>
<pre>
a = 1
a = a + 42
# a je 43
a = 1
a += 42
# a je 43
</pre>

  <p>Ale u měnitelných objektů (mutable) zápis <code>x += y</code>
  nemusí nutně vyjadřovat totéž, jako zápis
  <code>x&nbsp;=&nbsp;x&nbsp;+&nbsp;y</code>. Uvažujme seznamy:</p>
  
<pre>
&gt;&gt;&gt; z = [1, 2, 3]
&gt;&gt;&gt; id(z)
24213240
&gt;&gt;&gt; z += [4]
&gt;&gt;&gt; id(z)
24213240
&gt;&gt;&gt; z = z + [5]
&gt;&gt;&gt; id(z)
24226184
</pre>

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

  <p>Aby toho nebylo dost, vede to také k překvapivému chování,
  když se míchají měnitelné a neměnitelné kontejnery:</p>
  <pre>
&gt;&gt;&gt; t = ([],)
&gt;&gt;&gt; t[0] += [2, 3]
Traceback (most recent call last):
  File "&lt;input&gt;", line 1, in ?
TypeError: object doesn't support item assignment
&gt;&gt;&gt; t
([2, 3],)
</pre>

  <p>N-tice, samozřejmě, nepodporují přiřazování svých prvků. Jenže po
  provedení <code>+=</code> se seznam uvnitř <b>změnil</b>! Důvod je
  opět v tom, že <code>+=</code> 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.</p>
  
  <p>Tuto léčku já osobně považuji za vadu na kráse<sup class="trnote"
  id="B_tmod" title="Ale co je vlastně špatně?"><a
  href="#P_tmod">[5]</a></sup> :-).</p>

  <p><strong>Řešení</strong>: 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 ...</p>

  <h3>4. Atributy tříd versus atributy instancí</h3>

  <p>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:</p>
  
  <pre>
&gt;&gt;&gt; class Foo:
...     bar = []
...     def __init__(self, x):
...         self.bar.append(x)
...     
&gt;&gt;&gt; f = Foo(42)
&gt;&gt;&gt; g = Foo(100)
&gt;&gt;&gt; f.bar, g.bar
([42, 100], [42, 100])
</pre>

  <p>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.</p>

  <p>Další (malá) léčka spočívá v tom, že <code>self.foo</code> může
  odkazovat na dvě věci: na atribut instance <code>foo</code> nebo
  &mdash; pokud tento neexistuje &mdash; na atribut třídy
  <code>foo</code>. Porovnejte:</p>
  
  <pre>
&gt;&gt;&gt; class Foo:
...     a = 42
...     def __init__(self):
...         self.a = 43
...     
&gt;&gt;&gt; f = Foo()
&gt;&gt;&gt; f.a
43
</pre>

  <p>a druhý případ</p>
  <pre>
&gt;&gt;&gt; class Foo:
...     a = 42
...     
&gt;&gt;&gt; f = Foo()
&gt;&gt;&gt; f.a
42
</pre>

  <p>V prvním příkladě <code>f.a</code> 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 <code>a</code> neexistuje,
  takže <code>f.a</code> odkazuje na atribut třídy.</p>

  <p>Následující ukázka oba případy kombinuje:</p>
  <pre>
&gt;&gt;&gt; class Foo:
...     
...     bar = []
...     def __init__(self, x):
...         self.bar = self.bar + [x]
...     
&gt;&gt;&gt; f = Foo(42)
&gt;&gt;&gt; g = Foo(100)
&gt;&gt;&gt; f.bar
[42]
&gt;&gt;&gt; g.bar
[100]
</pre>

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

  <p><strong>Řešení</strong>: 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
  <code>self.__class__.<em>jmeno</em></code> místo zápisu
  <code>self.<em>jmeno</em></code> 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.<em>jmeno</em>.</p>

  <p><strong>Aktualizace:</strong> Vícero lidí poznamenává, že pasti
  číslo 3 a 4 se dají kombinovat do ještě zábavnějšího hlavolamu:</p>
  
  <pre>
&gt;&gt;&gt; class Foo:
...     bar = []
...     def __init__(self, x):
...             self.bar += [x]
...             
&gt;&gt;&gt; f = Foo(42)
&gt;&gt;&gt; g = Foo(100)
&gt;&gt;&gt; f.bar
[42, 100]
&gt;&gt;&gt; g.bar
[42, 100]
</pre>

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

  <h3>5. Měnitelné implicitní argumenty</h3>

  <p>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:</p>
  <pre>
&gt;&gt;&gt; def popo(x=[]):
...     x.append(666)
...     print x
...     
&gt;&gt;&gt; popo([1, 2, 3])
[1, 2, 3, 666]
&gt;&gt;&gt; x = [1, 2]
&gt;&gt;&gt; popo(x)
[1, 2, 666]
&gt;&gt;&gt; x
[1, 2, 666]
</pre>

  <p>To se dalo čekat. Ale teď:</p>
  <pre>
&gt;&gt;&gt; popo()
[666]
&gt;&gt;&gt; popo()
[666, 666]
&gt;&gt;&gt; popo()
[666, 666, 666]
</pre>

  <p>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
  <code>f(x=[])</code> se x nepřiřazuje pokaždé, když se funkce volá.
  Do <code>x</code> se přiřadí [], jen když se funkce definuje<sup
  class="trnote" id="B_mda" title="... opakování je matka
  moudrosti"><a href="#P_mda">[6]</a></sup>. 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.</p>

  <p><strong>Řešení</strong>: 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.</p>

  <h3>6. !UnboundLocalError</h3>

  <p>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ě:</p>
  <pre>
&gt;&gt;&gt; def p():
...     x = x + 2
...     
&gt;&gt;&gt; p()
Traceback (most recent call last):
  File "&lt;input&gt;", line 1, in ?
  File "&lt;input&gt;", line 2, in p
UnboundLocalError: local variable 'x' referenced before assignment
</pre>

  <p>Uvnitř <code>p</code> nemůže být výraz <code>x = x + 2</code>
  proveden, protože <code>x</code> ve výrazu <code>x + 2</code>
  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í:</p>
  <pre>
&gt;&gt;&gt; x = 2
&gt;&gt;&gt; def q():
...     print x
...     x = 3
...     print x
...     
&gt;&gt;&gt; q()
Traceback (most recent call last):
  File "&lt;input&gt;", line 1, in ?
  File "&lt;input&gt;", line 2, in q
UnboundLocalError: local variable 'x' referenced before assignment
</pre>

  <p>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:</p>

  <blockquote>
  
    <p>Pokud je vazba jména provedena uvnitř bloku, jde o lokální
    proměnnou tohoto bloku. Pokud je vazba jména<sup class="trnote" 
    id="B_namebinding" title="Podrobnější vysvětlení..."><a 
    href="#P_namebinding">[7]</a></sup> 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.</p>
    
    <p>Pokud není jméno vůbec nalezeno, vyvolá se výjimka
    <code>NameError</code>. Pokud se jméno odkazuje na lokální
    proměnnou, pro kterou dosud nebyla provedena vazba, vyvolá se
    výjimka <code>UnboundLocalError</code>.</p>
  
  </blockquote>

  <p>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á <code>x</code> 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
  <code>print&nbsp;x</code> a <code>x</code> ještě nemá žádnou
  hodnotu... a tudíž je to chyba.</p>

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

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

  <h3>7. Chyby při zaokrouhlování desetinných čísel</h3>

  <p>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:</p>
  
  <pre>
&gt;&gt;&gt; c = 0.1
&gt;&gt;&gt; c
0.10000000000000001
&gt;&gt;&gt; repr(c)
'0.10000000000000001'
&gt;&gt;&gt; str(c)
'0.1'
</pre>

  <p>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íží.</p>

  <p><strong>Řešení</strong>: Více informací se dozvíte z následujícího
  <a href="http://vik.sh.cvut.cz/~tnt/python/tut/node14.html">tutoriálu</a>.

  <h3>8. Spojování řetězců</h3>

  <p><strong>Poznámka překladatele:</strong> 
  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. 
  <pre>
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

</pre>

  <h3>9. Binární režim pro soubory</h3>

  <p>Nebo spíše, používání binárního režimu <b>není</b> 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:</p>
  
  <pre>
f1 = file(jmenosouboru, "r")  # text
f2 = file(jmenosouboru, "rb") # binárně
</pre>

  <p>V textovém režimu, mohou být řádky zakončované znakem "nová
  řádka" a/nebo "návrat vozíku" (<code>\n</code>, <code>\r</code>,
  nebo <code>\r\n</code>). 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 <code>\n</code> (universální). Ale v binárním režimu
  dostaneme <code>\r\n</code>. Při čtení dat proto můžeme v každém z
  těchto režimů získat velmi rozdílné výsledky.</p>

  <p>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.</p>

  <p><strong>Řešení</strong>: Používejte správné flagy &mdash; 'r' pro
  textový režim (i na Unixu), 'rb' na binární režim.</p>

  <h3>10. Zachytávání několika výjimek najednou</h3>

  <p>Někdy potřebujete v jednom <code>except</code> zachytit několik 
  výjimek v jednom. Automaticky nás napadne nás, že by mohlo fungovat
  následující:</p>
  
  <pre>
try:
    ...něco co vyvolá chybu...
except IndexError, ValueError:
    # "mělo by" zachytit chyby IndexError a ValueError
    # špatně!
</pre>

  <p>Tohle bohužel nefunguje. Důvody se stanou jasnějšími při
  porovnání s kódem:</p>
  <pre>
&gt;&gt;&gt; try:
...     1/0
... except ZeroDivisionError, e:
...     print e
...     
integer division or modulo by zero
</pre>

  <p>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
  <code>except</code> zachytila <code>IndexError</code> a jméno
  <code>ValueError</code> by svázala s objektem výjimky. To asi není,
  co jsme chtěli. ;-)</p>

  <p>Tohle funguje lépe:</p>
  <pre>
try:
    ...něco co vyvolá chybu...
except (IndexError, ValueError):
    # správně zachytí IndexError a ValueError
</pre>

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

  <h3>Jaké další nástrahy tu jsou? Napadají mne snad:</h3>

  <ul>
    <li>zpětné lomítka v řetězcích (bez raw), obzvláště u cest
    Windows/DOS</li>

    <li>dělení celých čísel (v dalších verzích bude snad
    změněno)</li>
  </ul>

  <h3>Příbuzné odkazy:</h3>

  <ul>
    <li><a href="http://www.ferg.org/projects/python_gotchas.html"
    title="">Python Gotchas</a> od Steve Ferg</li>

    <li><a href="http://www.amk.ca/python/writing/warts.html"
    title="">Python Warts</a> od Andrew Kuchling</li>

    <li><a href="http://mechanicalcat.net/cgi-bin/log/2003/09/02#anti-pitfalls"
    title="">Python anti-pitfalls</a> od Richard Jones</li>

  </ul>



<hr>

  <p>Poznámky překladatele:</p>
  
  <table summary="Poznámky překladatele.">
  <tr id="P_indent"><td><a href="#B_indent">[1]</a></td>
  <td><em>"... Tudy cesta nevede. Vyfukováním kouře do umyvadla s vodou 
    zlato opravdu nevzniká."</em> &ndash; J.C.</td>
  </tr>
  <tr id="P_spaces"><td><a href="#B_spaces">[2]</a></td>
  <td>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.</td>
  </tr>
  <tr id="P_assign"><td><a href="#B_assign">[3]</a></td>
  <td>Takže v tuto chvíli existuje jeden seznam, jeden objekt, na 
    který ukazují dvě jména.</td>
  </tr>
  <tr id="P_assign2"><td><a href="#B_assign2">[4]</a></td>
  <td>Přiřazením <code>a = 4</code> se zruší vazba na celočíselný 
   objekt s hodnotou 3 a vznikne vazba na celočíselný objekt s 
   hodnotou 4.</td>
  </tr>
  <tr id="P_tmod"><td><a href="#B_tmod">[5]</a></td>
  <td>N-tice je sice neměnná, ale udržuje pouze odkazy na jiné objekty. 
    Odkazy se skutečně měnit nemohou. Zápis <code>t[0]</code> 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.</td>
  </tr>
  <tr id="P_mda"><td><a href="#B_mda">[6]</a></td>
  <td>... 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.</td>
  </tr>
  <tr id="P_namebinding"><td><a href="#B_namebinding">[7]</a></td>
  <td>Jak již bylo vysvětleno u <a href="#pitfall2">pasti číslo 2</a>,
    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 <em>(jméno, odkaz na objekt)</em>. 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.</td>
  </tr>
  </table>

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:

  • 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:


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.