Python profesionálně: co jazyk nabízí

Python logo

V předchozích dílech tohoto seriálu jsme si ukázali spoustu syntaktických tipů. Dnes už necháme syntaktické tipy být a posuneme se trochu dál: vestavěné funkce, užitečné metody slovníku, na co si dát pozor u defaultních parametrů funkce, zajímavé moduly a další.

Seriál: Programujte v Pythonu profesionálně (5 dílů)

  1. Python profesionálně: úvod 3.4.2012
  2. Python profesionálně: dynamické parametry, generátory, lambda funkce a with 10.4.2012
  3. Python profesionálně: co jazyk nabízí 16.4.2012
  4. Python profesionálně: návrhové vzory 14.5.2012
  5. Python profesionálně: metatřídy 21.5.2012

Iterace instančními proměnnými

Nedávno jsem narazil na problém, že mám něco jako slovníkovou instanci, ale přesto to slovník není. Tedy mám instanci s key-value hodnotami, ale neexistují metody items či iteritems. Konkrétně se jednalo o instanci Values, kterou vrací metoda parse_args z instance třídy OptionParser. Raději zopakuju technicky:

from optparse import OptionParser
parser = OptionParser()
# ...
options, args = parser.parse_args()

options # <Values at 0xb730b46c: {...}>
options.items() # AttributeError

# `options` je neco jako:
class C:
    def __init__(self):
        self.arg = 'val' # Nejaky argument z prikazove radky.
options = C()

Je to modul, který (hezky) pomáhá rozparsovat argumenty z příkazové řádky (nutno podotknout, že od Pythonu 2.7 je označen za deprecated a nahrazuje ho argparse). Jak je vidět, svádí to k tomu, že se jedná o slovník a přesto to slovník není. Nyní, když bych chtěl proiterovat všemi proměnnými, které byly nastaveny (ať přes příkazový řádek nebo defaultně), … tak nemám jak, poněvadž pro to není žádná metoda. Proměnné jsou nastaveny jako instanční proměnné a žádné jiné se tam nepletou, takže by šla použít speciální proměnná __dict__, jenže to spíš vypadá jako hack.

options.__dict__ # {...}

V Pythonu existuje built-in funkce vars, která bez parametru dělá totéž, co locals. S parametrem (jakýkoliv objekt) však vrací právě ten zmíněný slovník toho objektu, tedy to samé, co příkaz výše.

vars(options) # {...}

Mně osobně se více libí tato varianta. Nyní už můžu procházet nastavené parametry z příkazové řádky…

for key, val in vars(options).iteritems():
    # Do some stuff.

Chybějící klíče

Mějme problém: potřebujeme v nějakém textu spočítat početnost slov. Třeba abychom si ověřili, že náš textař splnil podmínky pro PR článek. :-) Můžeme na to jít jednoduše:

words = 'hello world hello python'.split(' '):

count = {}
for word in words:
    if word not in count:
        count[word] = 1
    else:
        count[word] += 1

Ale je to takové nemastné neslané. Chtělo by to vylepšit a pomoct může defaultdict z modulu  collections.

from collections import defaultdict

count = defaultdict(int)
for word in words:
    count[word] += 1

Hned je o podmínku méně a tím i menší pravděpodobnost potenciální chyby. Bohužel nám tento speciální slovník nepomůže ve složitějších situacích, protože jako parametr přijímá pouze callable objekt. Například pokud bychom chtěli počítat od jedničky a ne od nuly, bude to tak trochu hloupé, ale lze to. Přijdete na řešení sami dřív, než se podíváte o kousek níže?

count = defaultdict(lambda: 1)

Například zde se hodí lambda funkce bez parametrů (o lambda funkcích jsme psali posledně). Jenže není to úplně šťastné (= čitelné) řešení, a proto nekončíme a ještě to trochu poladíme.

Poladíme to naším vlastním speciálním slovníkem obohaceným o speciální metodu, o které se nemluví a nepíše. V dokumentaci na ni téměř nenarazíte; lze ji spatřit pouze u dokumentace zmíněného defaultdict u nebo v článku What’s New in Python 2.5, jinak nikde. Ostatní speciální názvy metod jsou k nalezení v dokumentaci se speciálními metodami. Jedná se o speciální metodu  __missing__.

class MyDict(dict):
    def __missing__(self, key):
        return 1

count = MyDict()
for word in words:
    count[word] += 1

Od Pythonu 2.7 lze pro tento konkrétní případ jednoduše použít třídu Counter z modulu  collections.

Defaultní hodnoty slovníku

Na předchozí tip navážu více méně podobným tipem. Ten už jsem sice popisoval v dřívějším článku o optimalizacích v Pythonu, ale opakování je matka moudrosti… Jde o to, proč dělat věci složitě nejen pro nás, ale i pro server. Místo

d = {}
if key not in d:
    d[key] = 0
d[key] += 1

d = {}
if key not in d:
    d[key] = []
d.append(element)

lze jednodušeji napsat

d = {}
d[key] = d.get(key, 0) + 1

d = {}
d.setdefault(key, []).append(element)

Vidíte to? Předchozí tip můžeme řešit ještě jinak. Všechno lze řešit různými způsoby a je jen na vás a na okolnostech, který je v danou chvíli nejlepší. Já jen ukazuji možnosti.

Zpět k ukázce: První metoda slovníku, get, zkusí najít ve slovníku klíč z prvního parametru (a hodnotu toho klíče vrátí) a v případě neúspěchu se vrátí hodnota předaná v druhém parametru. Druhá metoda se chová podobně, jen v tom rozdílu, že v případě neúspěchu navíc nový klíč vytvoří a nastaví. Z ukázky je patrné, která metoda se k čemu více hodí.

Defaultní parametry nemusejí být kamarádi

Python, stejně jako spousta jiných jazyků, umí definovat parametry s přednastavenou hodnotou. Je to velmi dobrá funkčnost, ale v Pythonu si s ní můžete nevědomky natlouct nos. Podívejte se na následující ukázku:

def foo(x=[]):
    x.append(1)
    print x

foo()
foo()
foo()

Co myslíte, že se stane? Myslíte, že se třikrát vytiskne na standardní výstup seznam s jednou položkou a to číslem jedna? Tak to jste na omylu! V prvním zavolání se vytiskne seznam s jednou položkou, jak je očekávatelné. Druhé volání však už seznam s dvěma položkami, obě budou jedničky. Třetí volání už se třemi a takhle by to pokračovalo klidně dále.

>>> foo()
[1]
>>> foo()
[1, 1]
>>> foo()
[1, 1, 1]

Proč tomu tak je? Nu proto, že se defaultní hodnota parametru zkonstruuje při vytváření funkce. V průběhu programu zůstává vytvořená instance stejná – stále stejná reference na stejnou instanci. Samozřejmě pokud při volání funkce předáme nějaký parametr, tak se to nestane; stejná reference se předá pouze při volání bez parametru. A díky tomu lze vygenerovat velmi zajímavé chyby a tím i strávit příjemné večery jejich hledáním. :-)

Jelikož v Pythonu jsou jedna jednoduchá pravidla, která jsou aplikovaná na všechno (místy mírně upravená, samozřejmě), je to tak logické, i když se to nemusí na první pohled zdát. Obrana proti tomu je velmi jednoduchá a existují prakticky dvě možnosti.

První možností je u neprimitivních datových typech používat jako defaultní hodnotu hodnotu None a až na začátku těla funkce si vytvořit potřebnou instanci:

def foo(x=None):
    if x is None:
        x = []
    x.append(1)
    print x

foo() # [1]
foo() # [1]
foo() # [1]

Bohužel tohle řešení má dvě vady. Jedna je ta, že None hodnota může mít speciální význam. Šla by sice použít jiná hodnota, třeba nějaký vlastní objekt EMPTY_VALUE = object(), ale stále to není pravé ořechové. A druhou vadou na kráse je lhaní v našeptávačích – až po přečtení nápovědy bych se dozvěděl (pokud existuje!) skutečnou defaultní hodnotu místo nesprávné None hodnoty.

Tyto vady řeší druhé řešení, které definici ponechává zachovanou, a na začátek těla funkce musíme přidat trochu jiný kód a to kopii instance. U datových typů typu seznam nebo slovník (jiné bych ani jako defaultní hodnotu nedával) to je primitivní:

def foo(x=[]):
    x = list(x)
    x.append(1)
    print x

foo() # [1]
foo() # [1]
foo() # [1]

Trochu jiný switch

Jak všichni jistě víme, switch v Pythonu není. Opravdu, neukážu vám u tohoto tipu nic takového. On totiž ani není potřeba, posteskl jsem si nad jeho neimplementací za čtyři roky pythonování snad jen jednou. Chtěl bych však ukázat, že lze v Pythonu dělat opravdu téměř všechno, klidně switchnout metody za běhu…

class C(object):
    def __f(self):
        print 'calling of f'

    def f(self):
        print 'first calling of f'
        self.f = self.__f

c = C()
c.f() # first calling of f
c.f() # calling of f
c.f() # calling of f

Tím chci poukázat na to, že v Pythonu je téměř vše reference na nějaký objekt a ta reference lze kdykoliv zaměnit. Python skousne hodně, zkuste experimentovat. Samozřejmě ale ne každý experiment se hodí do produkčního kódu. Například v konstruktoru vytvářet jinou instanci (ano, to také lze) je dle mého názoru už za pomyslnou hranicí; musely by být hodně dobré argumenty, aby se mi taková věc líbila.

Kde se tohle konkrétně může hodit, ještě ukážu.

Matematické operace s množinami

V běžném programování to není tak běžné, ale stejně se občas hodí využít nějaké ty množinové operace, které nás učili ve škole. Pokud jste se s takovou situací už někdy setkali, musíte vědět, že pomocí obyčejného seznamu nebo tuplu to nelze provést. Řešení přináší až datový typset (či frozenset, obdoba pro tuple) nebo česky množina.

a = set([1, 2, 3])
b = set([3, 4])

a.union(b) # set([1, 2, 3, 4])
a.intersection(b) # set([3])
a.difference(b) # set([1, 2])
b.difference(a) # set([4])
a.symmetric_difference(b) # set([1, 2, 4])

Kdo umí dobře anglicky (nebo alespoň matematické výrazy), nebude mít problém. Python se však snaží práci vždy ulehčovat, proto existuje i pohodlnější varianta pro nejazykovce:

a | b # set([1, 2, 3, 4])
a & b # set([3])
a - b # set([1, 2])
b - a # set([4])
a ^ b # set([1, 2, 4])

Ukázané metody mají i své sourozence s příponou _update (až na union, který má pouze update). Z názvu je zřejmě patrné, že příkaz nevrátí novou množinu, ale upraví instanci, ze které je metoda volána. A i pro tyto metody existuje pohodlnější varianta, stačí přidat rovnítko.

a.update(b)
a |= b

a.intersection_update(b)
a &= b

a.differenece_update(b)
a -= b

a.symmetric_difference_udpate(b)
a ^= b

Mimochodem v Pythonu 3 se množina dá zapsat pohodlněji pomocí složených závorek. Od slovníku se to liší absencí klíčů.

a = {1, 2, 3}
b = {3, 4}

Ale pozor! Kvůli zpětné kompatibilitě zůstávají prázdné chlupaté závorky stále prázdným slovníkem. Prázdná množina se musí deklarovat postaru.

a = {1, 2} # Mnozina.
b = {} # Prazdny slovnik.
c = set() # Prazdna mnozina.

Debug složitých struktur

Poměrně často se mi stává, že potřebuji zdebugovat nějakou část kódu, kde se pracuje s opravdu složitými strukturami. Samozřejmě si vypomáhám zpětnými lomítky (které v Pythonu 3 už nenalezneme, ale to nevadí, protože to je pouze alias pro built-in funkci repr) a obyčejným  print em.

>>> repr(var)
"[{'username': 'admin', 'rights': ('create', 'update', 'list', 'detail'), 'surname': 'Admin', 'id': 1, 'name': 'Admin'}, {'username': 'user', 'rights': ('list', 'detail'), 'surname': '', 'id': 2, 'name': 'User'}]"

V této struktuře se dá ještě dobře zorientovat a mohl by repr postačovat, jenže stačí někde přidat další zanořený objekt a už to začne být těžké pro čtení. A hlavně těžké zjistit, co v tom chybí (nebo přebývá) a způsobuje nějakou chybu. Je možnost si výpis jednoduše zkopírovat do chytrého editoru a zformátovat do „JSON Beautify“, ale s Pythonem je jednodušší způsob díky modulu pprint:

>>> import pprint
>>> pprint.pprint(var)
[{'id': 1,
  'name': 'Admin',
  'rights': ('create', 'update', 'list', 'detail'),
  'surname': 'Admin',
  'username': 'admin'},
 {'id': 2,
  'name': 'User',
  'rights': ('list', 'detail'),
  'surname': '',
  'username': 'user'}]

Nyní se to čte mnohem pohodlněji. Za zmínku také stojí, že lze předat parametrem stream na který se má výpis přesměrovat.

Šmrnc pro konzolové aplikace

Řekl bych, že Python je velmi vhodným jazykem pro konzolové skriptíky (samozřejmě záleží hlavně na preferencích a zkušenostech). Když už se ale Python použijete, zamyslete se nad využitím modulu readline, který uživatelům zpříjemní zadávání vstupu, hlavně opakovaného. Konkrétně pomáhá s historií, tedy opětovným použitím vstupu bez potřeby ho znovu celý napsat. Historii lze i ukládat a zase načítat. Více info v dokumentaci.

import readline

while True:
    in_ = raw_input(': ')
    if in_ == 'q':
        break

Předcházející ukázkou si můžete funkčnost jednoduše vyzkoušet – při zadávání vstupu v druhém a dalším cyklu zkuste stisknout šipku nahoru. K tomu lze rovnou přidat modul rlcompleter, který se spojením s readline dokáže našeptávat po stisknutí klávesy tabulátor (známé z příkazové řádky). Přesněji se jedná o našeptávač pro pythoní interaktivní interprety. Zkuste:

>>> import readline
>>> import rlcompleter
>>> readline.parse_and_bind("tab: complete")
>>> # ted zkuste programovat a vyuzivejte tabulator

Dobré doporučení je si do .bashrc přidat řádek export PYTHONSTARTUP=/home/me/startup.py a do tohoto souboru následující kód:

try:
    import readline
except ImportError:
    print('Module readline not available.')
else:
    import rlcompleter
    readline.parse_and_bind('tab: complete')

Tím se pokaždé před spuštěním interpretu Pythonu tento kód vyvolá a vždy bude aktivní našeptávač.

Závěr

Dnešním dílem jsme si řekli vše, co jsem chtěl sdělit za takové klasické tipy, které by měl určitě každý programátor v Pythonu znát, aby mohl využít co nejvíc z tohoto jazyka. Není to samozřejmě všechno, dalo by se toho napsat víc, ale to už nechám na vašem samostudiu. Příště se přesuneme k tipům, jak lze v Pythonu elegantně použít několik návrhových vzorů.

Dnešek opět zakončím několika odkazy k dalšímu studiu:

  • Doporučuju si projít seznam všech vestavěných funkcí, protože s jistotou narazíte na nějaké funkce, které řeší vaše každodenní problémy mnohem jednodušeji.
  • Není od věci si také projít, jaké všechny speciální metody lze použít.
  • Také je dobré si projít, co vše nabízejí vestavěné datové typy.
  • Než budete řešit nějaký problém, zkuste se podívat na seznam oficiálních modulů, jestli náhodou nějaký neřeší váš problém.
  • Python má vlastní hosting pro pythoní knihovny nazvaný jednoduše Python Package Index. Najdete tam téměř každou pythoní knihovnu a určitě byste tam měli dávat i ty svoje.

Michal dělá team leadera v Seznam.cz a hraje si na BOObook.cz. Jeho nejoblíbenějším jazykem je Python, ale nevadí mu třeba ani JavaScript a rád zkouší nové jazyky i technologie. Ve volném čase cestuje, fotí, píše, ale taky plave, jezdí na kole či tancuje.

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Komentáře: 34

Přehled komentářů

perl Pocitani slov
monitor Re: Pocitani slov
perl Re: Pocitani slov
perl Re: Pocitani slov
MW Re: Pocitani slov
Riff Re: Pocitani slov
PERL Re: Pocitani slov
tom našeptávač - ipython
Honza Kral Re: našeptávač - ipython
Whit Re: našeptávač - ipython
dingo Díky za tipy
Laik Je to super
KArol Re: Je to super
Martin Hassman Re: Je to super
Laik Re: Je to super
Skalimil Vuk Re: Je to super
jiri.vrany Re: Je to super
jiri.vrany Pěkné
osramek Chybka
Honza Kral Hacky
pavel dynamicke parametry
Ales Zoulek Neslo by to aspon ciste?
whiskybar Python sraz
Martin Hassman Re: Python sraz
3socks ipython
pepa dotaz
Honza Kral Re: dotaz
pepa Re: dotaz
Honza Kral Re: dotaz
dik python 3 a změna hodnoty atributu objektu z metody
Havri Re: python 3 a změna hodnoty atributu objektu z metody
dik Re: python 3 a změna hodnoty atributu objektu z metody
Havri Re: python 3 a změna hodnoty atributu objektu z metody
dík Re: python 3 a změna hodnoty atributu objektu z metody
Zdroj: http://www.zdrojak.cz/?p=3624