Nacházíte se zde: Domů Ponořme se do Pythonu 3

Úroveň obtížnosti: ♦♦♢♢♢

Generátorová notace

Our imagination is stretched to the utmost, not, as in fiction, to imagine things which are not really there, but just to comprehend those things which are.
(Naše představivost je napjatá do krajnosti. Ne jako u fikce, abychom si představili věci, které zde nejsou, ale proto, abychom jen obsáhli věci, které jsou zde.)
Richard Feynman

 

Ponořme se

V každém programovacím jazyce najdeme určitý rys, který záměrně zjednodušuje nějakou komplikovanou věc. Pokud přicházíte se zkušenostmi z jiného jazyka, můžete to snadno přehlédnout, protože váš starý jazyk právě tu určitou věc nezjednodušoval (protože dalo práci místo toho zjednodušit něco jiného). V této kapitole se seznámíme s generátorovou notací seznamů (list comprehensions), s generátorovou notací slovníků (dictionary comprehensions) a s generátorovou notací množin (set comprehensions). Jde o tři související koncepty, jejichž jádrem je jedna velmi mocná technika. Ale nejdříve si uděláme malou odbočku ke dvěma modulům, které vám usnadní orientaci ve vašem lokálním souborovém systému.

Práce se soubory a s adresáři

Python 3 se dodává s modulem zvaným os, což je zkratka pro „operační systém“. Modul os obsahuje spoustu funkcí pro získávání informací o lokálních adresářích, souborech, procesech a proměnných prostředí — a v některých případech s nimi umožňuje manipulovat. Python se snaží co nejlépe, aby pro všechny podporované operační systémy nabízel jednotné API (aplikační programové rozhraní). Cílem je, aby vaše programy běžely na libovolném počítači a aby přitom obsahovaly co nejméně kódu, který by byl závislý na platformě.

Aktuální pracovní adresář

Pokud s Pythonem právě začínáte, strávíte ještě hodně času v pythonovském shellu. V celé knize se budete setkávat s příklady, jako je tento:

  1. Importujte jeden z modulů nacházejících se v adresáři examples (příklady).
  2. Zavolejte funkci z tohoto modulu.
  3. Vysvětlete výsledky.

Pokud o aktuálním pracovním adresáři nic nevíte, pak krok 1 pravděpodobně selže a objeví se výjimka ImportError. Proč? Protože Python se bude po modulu dívat ve vyhledávací cestě pro import, ale nenajde jej, protože adresář examples se v žádném adresáři z vyhledávací cesty nenachází. Aby to prošlo, můžete udělat jednu ze dvou věcí:

  1. Adresář examples přidáte do vyhledávací cesty pro import.
  2. Změníte aktuální pracovní adresář na examples.

Aktuální pracovní adresář je neviditelný údaj, který si Python neustále udržuje v paměti. Aktuální pracovní adresář existuje vždy — ať už jste v pythonovském shellu, spouštíte svůj vlastní pythonovský skript z příkazového řádku nebo spouštíte pythonovský CGI skript na nějakém webovém serveru.

Pro vypořádání se s aktuálním pracovním adresářem nabízí modul os dvě funkce.

>>> import os                                            
>>> print(os.getcwd())                                   
C:\Python31
>>> os.chdir('/Users/pilgrim/diveintopython3/examples')  
>>> print(os.getcwd())                                   
C:\Users\pilgrim\diveintopython3\examples
  1. Modul os je součástí Pythonu. Můžete jej importovat kdykoliv a kdekoliv.
  2. Informaci o aktuálním pracovním adresáři získáte použitím funkce os.getcwd(). Pokud používáte grafický pythonovský shell, pak se aktuální pracovní adresář zpočátku nachází v adresáři, ve kterém je umístěn spustitelný program pythonovského shellu. Při práci pod Windows to záleží na tom, kam jste Python nainstalovali. Výchozí adresář je c:\Python31. Pokud používáte konzolový pythonovský shell, pak se aktuální pracovní adresář zpočátku nachází v adresáři, ve kterém jste spustili python3.
  3. Aktuální pracovní adresář můžeme měnit použitím funkce os.chdir().
  4. Při volání funkce os.chdir() jsem použil cestu v linuxovém stylu (normální lomítka, žádné písmeno disku), i když pracuji pod Windows. To je právě jedno z míst, kde se Python snaží zamaskovat rozdíly mezi operačními systémy.

Práce se jmény souborů a adresářů

Když už se bavíme o adresářích, chtěl bych vás upozornit na modul os.path. Ten obsahuje funkce pro manipulace se jmény souborů a adresářů.

>>> import os
>>> print(os.path.join('/Users/pilgrim/diveintopython3/examples/', 'humansize.py'))              
/Users/pilgrim/diveintopython3/examples/humansize.py
>>> print(os.path.join('/Users/pilgrim/diveintopython3/examples', 'humansize.py'))               
/Users/pilgrim/diveintopython3/examples\humansize.py
>>> print(os.path.expanduser('~'))                                                               
c:\Users\pilgrim
>>> print(os.path.join(os.path.expanduser('~'), 'diveintopython3', 'examples', 'humansize.py'))  
c:\Users\pilgrim\diveintopython3\examples\humansize.py
  1. Funkce os.path.join() sestaví cestu z jedné nebo více částí cesty. V tomto případě jednoduše spojí řetězce.
  2. Tento příklad už není tak jednoduchý. Funkce os.path.join() před napojením jména souboru navíc přidá k cestě jedno lomítko. Místo obyčejného lomítka použila zpětné lomítko, protože jsem tento příklad pouštěl pod Windows. Pokud byste stejný příklad zkoušeli na systémech Linux nebo Mac OS X, použilo by se normální lomítko. Nepárejte se s lomítky. Používejte vždy os.path.join() a nechejte na Pythonu, aby udělal, co je správné.
  3. Funkce os.path.expanduser() rozepíše cestu, která pro vyjádření domácího adresáře aktuálního uživatele používá znak ~. Funguje to na libovolné platformě, kde mají uživatelé přidělený svůj domácí adresář, tedy na Linuxu, Mac OS X a ve Windows. Vrácená cesta neobsahuje koncové lomítko, ale to funkci os.path.join() nevadí.
  4. Kombinováním těchto technik můžeme snadno konstruovat cesty do adresářů a k souborům, které se nacházejí v uživatelově domácím adresáři. Funkce os.path.join() přebírá libovolný počet argumentů. Jakmile jsem to zjistil, skákal jsem radostí, protože při přípravě mých nástrojů v nějakém novém jazyce je addSlashIfNecessary() (přidejLomítkoPokudJeToNutné) jednou z těch otravných malých funkcí, které si musím vždy znovu napsat. V Pythonu takovou funkci nepište. Chytří lidé už se o to postarali za vás.

Modul os.path obsahuje také funkce, které umí rozdělit plné cesty, jména adresářů a souborů na jejich podstatné části.

>>> pathname = '/Users/pilgrim/diveintopython3/examples/humansize.py'
>>> os.path.split(pathname)                                        
('/Users/pilgrim/diveintopython3/examples', 'humansize.py')
>>> (dirname, filename) = os.path.split(pathname)                  
>>> dirname                                                        
'/Users/pilgrim/diveintopython3/examples'
>>> filename                                                       
'humansize.py'
>>> (shortname, extension) = os.path.splitext(filename)            
>>> shortname
'humansize'
>>> extension
'.py'
  1. Funkce split rozdělí plnou cestu a vrátí n-tici, která obsahuje zvlášť cestu a zvlášť jméno souboru.
  2. Pamatujete si, že jsme se bavili o možnosti vracet více hodnot z funkce přiřazením hodnot více proměnným najednou? Funkce os.path.split() dělá přesně tohle. Výsledek funkce split přiřadíme do n-tice s dvěma proměnnými. Každá z proměnných získá hodnotu odpovídajícího prvku vracené dvojice.
  3. První proměnná, dirname, obdrží hodnotu prvního prvku n-tice, kterou vrací funkce os.path.split(), a sice cestu k souboru.
  4. Druhá proměnná, filename, obdrží hodnotu druhého prvku n-tice vracené funkcí os.path.split(), jméno souboru.
  5. Modul os.path obsahuje také funkci os.path.splitext(), která rozdělí jméno souboru a vrací dvojici obsahující jméno souboru bez přípony a příponu. Pro jejich přiřazení do oddělených proměnných použijeme stejnou techniku.

Výpis adresářů

Dalším nástrojem z pythonovské standardní knihovny je modul glob. Umožní nám z programu snadno získat obsah nějakého adresáře. Používá typ zástupných znaků (wildcards), které už asi znáte z práce na příkazovém řádku.

>>> os.chdir('/Users/pilgrim/diveintopython3/')
>>> import glob
>>> glob.glob('examples/*.xml')                  
['examples\\feed-broken.xml',
 'examples\\feed-ns0.xml',
 'examples\\feed.xml']
>>> os.chdir('examples/')                        
>>> glob.glob('*test*.py')                       
['alphameticstest.py',
 'pluraltest1.py',
 'pluraltest2.py',
 'pluraltest3.py',
 'pluraltest4.py',
 'pluraltest5.py',
 'pluraltest6.py',
 'romantest1.py',
 'romantest10.py',
 'romantest2.py',
 'romantest3.py',
 'romantest4.py',
 'romantest5.py',
 'romantest6.py',
 'romantest7.py',
 'romantest8.py',
 'romantest9.py']
  1. Modul glob zpracovává masku se zástupným znakem a vrací cesty ke všem souborům a adresářům, které masce se zástupným znakem odpovídají. V tomto příkladu je maska složena z cesty do adresáře a z „*.xml“. Budou jí odpovídat všechny .xml soubory v podadresáři examples.
  2. Teď jako aktuální pracovní adresář zvolíme podadresář examples. Funkce os.chdir() umí pracovat i s relativními cestami.
  3. Ve vzorku pro funkci glob můžeme použít více zástupných znaků. Tento příklad nalezne v aktuálním pracovním adresáři všechny soubory, které končí příponou .py a kdekoliv ve jméně souboru obsahují slovo test.

Získání dalších informací o souboru

Každý moderní souborový systém ukládá o každém souboru metadata, jako jsou: datum vytvoření, datum poslední modifikace, velikost souboru atd. Pro zpřístupnění těchto metadat poskytuje Python jednotné API. Soubor se nemusí otevírat. Vše, co potřebujete znát, je jeho jméno.

>>> import os
>>> print(os.getcwd())                 
c:\Users\pilgrim\diveintopython3\examples
>>> metadata = os.stat('feed.xml')     
>>> metadata.st_mtime                  
1247520344.9537716
>>> import time                        
>>> time.localtime(metadata.st_mtime)  
time.struct_time(tm_year=2009, tm_mon=7, tm_mday=13, tm_hour=17,
  tm_min=25, tm_sec=44, tm_wday=0, tm_yday=194, tm_isdst=1)
  1. Aktuálním pracovním adresářem je složka examples.
  2. feed.xml je soubor ve složce examples. Voláním funkce os.stat() získáme objekt, který obsahuje několik různých typů informací o souboru (metadat).
  3. st_mtime zachycuje čas poslední modifikace, ale není uložen ve tvaru, který by byl moc použitelný. (Z technického pohledu je to počet sekund od Epochy, kde Epocha je definována jako první sekunda 1. ledna 1970. Vážně!)
  4. Modul time je součástí standardní pythonovské knihovny. Obsahuje funkce pro převody mezi různými reprezentacemi času, pro formátování času do řetězcové podoby a pro hraní si s časovými zónami.
  5. Funkce time.localtime() převádí hodnotu času ze sekund-od-Epochy (z položky st_mtime objektu vraceného funkcí os.stat()) na použitelnější strukturu obsahující rok, měsíc, den, hodinu, minutu, sekundu atd. Tento soubor byl naposledy změněn 13. července 2009 přibližně v 17 hodin a 25 minut.
# pokračování předchozího příkladu
>>> metadata.st_size                              
3070
>>> import humansize
>>> humansize.approximate_size(metadata.st_size)  
'3.0 KiB'
  1. Funkce os.stat() vrací také velikost souboru, a to v položce st_size. Soubor feed.xml obsahuje 3070 bajtů.
  2. Položku st_size můžeme předat funkci approximate_size().

Jak vytvořit absolutní cesty

V předcházející podkapitole jsme voláním funkce glob.glob() získali seznam s relativními cestami. V prvním příkladu jsme získali cesty jako 'examples\feed.xml'. V druhém příkladu jsme získali dokonce ještě kratší relativní cesty jako 'romantest1.py'. Za předpokladu, že zůstaneme ve stejném pracovním adresáři, můžeme tyto relativní cesty používat pro otevření souborů nebo pro získávání jejich metadat. Ale pokud chceme vytvořit absolutní cestu — tj. takovou, která obsahuje jména všech adresářů až po kořenový adresář nebo včetně jména disku —, budeme potřebovat funkci os.path.realpath().

>>> import os
>>> print(os.getcwd())
c:\Users\pilgrim\diveintopython3\examples
>>> print(os.path.realpath('feed.xml'))
c:\Users\pilgrim\diveintopython3\examples\feed.xml

Generátorová notace seznamu

Generátorová notace seznamu (anglicky list comprehension [list komprihenšn]) umožňuje stručný zápis vytvoření seznamu z jiného seznamu aplikováním funkce na všechny prvky zdrojového seznamu. (Poznámka překladatele: Pojem „list comprehension“ je znám z deklarativních jazyků a má charakter syntaktické konstrukce. V jazyce Python se „vnitřku“ deklarativního zápisu podobá generátorový výraz. Tímto způsobem byl odvozen český pojem „generátorová notace“. Někdy je pojem „list comprehension“ použit v procedurálním, dynamickém smyslu. V takové situaci můžeme uvažovat o pojmu „generátor seznamu“. Pokud se bavíme o jeho výsledku, můžeme uvažovat i o pojmu „generovaný seznam“. Vzhledem k tomu, že zavedený český pojem pro tuto konstrukci asi neexistuje — studentům příslušných oborů vysokých škol přijde po krátké chvíli anglický pojem srozumitelný —, budu volněji používat některou z uvedených variant. Někdy budu poněkud dlouhý pojem „generátorová notace seznamu“ zkracovat. Kritériem volby bude dobrá srozumitelnost.)

>>> a_list = [1, 9, 8, 4]
>>> [elem * 2 for elem in a_list]           
[2, 18, 16, 8]
>>> a_list                                  
[1, 9, 8, 4]
>>> a_list = [elem * 2 for elem in a_list]  
>>> a_list
[2, 18, 16, 8]
  1. Aby nám to začalo dávat smysl, podívejme se na zápis zprava doleva. Seznam a_list je zde zdrojem zobrazení. Interpret jazyka Python prochází seznam a_list po jednom prvku a dočasně přiřazuje jeho hodnotu do proměnné elem. Poté Python aplikuje funkci elem * 2 a připojí výsledek na konec cílového seznamu.
  2. Generátorová notace produkuje nový seznam. Původní seznam zůstává nezměněný.
  3. Výsledek generátoru seznamu můžeme bezpečně přiřadit do proměnné, která zachycovala původní seznam. Python nejdříve vytvoří nový seznam v paměti a teprve po dokončení jeho generování přiřadí výsledek do původní proměnné.

V generátorové notaci seznamu můžeme využít libovolný pythonovský výraz, včetně funkcí z modulu os, které slouží k manipulaci se soubory a adresáři.

>>> import os, glob
>>> glob.glob('*.xml')                                 
['feed-broken.xml', 'feed-ns0.xml', 'feed.xml']
>>> [os.path.realpath(f) for f in glob.glob('*.xml')]  
['c:\\Users\\pilgrim\\diveintopython3\\examples\\feed-broken.xml',
 'c:\\Users\\pilgrim\\diveintopython3\\examples\\feed-ns0.xml',
 'c:\\Users\\pilgrim\\diveintopython3\\examples\\feed.xml']
  1. Toto volání vrací seznam všech .xml souborů v aktuálním pracovním adresáři.
  2. Tato generátorová notace přebírá předchozí seznam .xml souborů a transformuje jej na seznam jmen s plnou cestou.

Generátorová notace seznamu může navíc předepisovat i filtraci položek. To znamená, že může vyprodukovat výsledek, který bude kratší než původní seznam.

>>> import os, glob
>>> [f for f in glob.glob('*.py') if os.stat(f).st_size > 6000]  
['pluraltest6.py',
 'romantest10.py',
 'romantest6.py',
 'romantest7.py',
 'romantest8.py',
 'romantest9.py']
  1. Filtraci seznamu provedeme vložením podmínky if na konec generátorové notace. Pro každou položku seznamu bude vyhodnocen výraz za klíčovým slovem if. Pokud je výsledkem výrazu True, pak bude položka zahrnuta do výstupu. Tato generátorová notace seznamu předepisuje zpracování všech souborů s příponou .py v aktuálním adresáři. Výraz za if zajišťuje filtraci seznamu testováním, zda je velikost každého souboru větší než 6000 bajtů. Takových souborů je šest, takže generátorová notace produkuje seznam se šesti jmény souborů.

Všechny předchozí příklady generátorové notace seznamu používaly jen jednoduché výrazy — násobení čísla konstantou, volání jedné funkce, nebo jednoduše vracely původní položky seznamu (po filtraci). Ale generátorová notace seznamu může být libovolně složitá.

>>> import os, glob
>>> [(os.stat(f).st_size, os.path.realpath(f)) for f in glob.glob('*.xml')]            
[(3074, 'c:\\Users\\pilgrim\\diveintopython3\\examples\\feed-broken.xml'),
 (3386, 'c:\\Users\\pilgrim\\diveintopython3\\examples\\feed-ns0.xml'),
 (3070, 'c:\\Users\\pilgrim\\diveintopython3\\examples\\feed.xml')]
>>> import humansize
>>> [(humansize.approximate_size(os.stat(f).st_size), f) for f in glob.glob('*.xml')]  
[('3.0 KiB', 'feed-broken.xml'),
 ('3.3 KiB', 'feed-ns0.xml'),
 ('3.0 KiB', 'feed.xml')]
  1. Tato generátorová notace nalezne v aktuálním pracovním adresáři všechny soubory s příponou .xml, zjistí velikost každého z nich (voláním funkce os.stat()) a vytvoří dvojice obsahující jméno souboru a absolutní cestu k souboru (voláním funkce os.path.realpath()).
  2. Tento generátorový zápis seznamu vychází z předchozího. Pro velikost každého .xml souboru se volá funkce approximate_size().

Generátorová notace slovníku

Generátorová notace slovníku (anglicky dictionary comprehension [dikšenri komprihenšn]) se podobá generátorové notaci seznamu, ale místo seznamu popisuje vytvoření slovníku.

>>> import os, glob
>>> metadata = [(f, os.stat(f)) for f in glob.glob('*test*.py')]    
>>> metadata[0]                                                     
('alphameticstest.py', nt.stat_result(st_mode=33206, st_ino=0, st_dev=0,
 st_nlink=0, st_uid=0, st_gid=0, st_size=2509, st_atime=1247520344,
 st_mtime=1247520344, st_ctime=1247520344))
>>> metadata_dict = {f:os.stat(f) for f in glob.glob('*test*.py')}  
>>> type(metadata_dict)                                             
<class 'dict'>
>>> list(metadata_dict.keys())                                      
['romantest8.py', 'pluraltest1.py', 'pluraltest2.py', 'pluraltest5.py',
 'pluraltest6.py', 'romantest7.py', 'romantest10.py', 'romantest4.py',
 'romantest9.py', 'pluraltest3.py', 'romantest1.py', 'romantest2.py',
 'romantest3.py', 'romantest5.py', 'romantest6.py', 'alphameticstest.py',
 'pluraltest4.py']
>>> metadata_dict['alphameticstest.py'].st_size                     
2509
  1. Toto není generátorová notace slovníku, ale generátorová notace seznamu. Nalezne všechny soubory s příponou .py, které ve svém jméně obsahují podřetězec test. Pak se vytvoří dvojice obsahující jméno souboru a jeho metadata (voláním funkce os.stat()).
  2. Každá položka výsledného seznamu je dvojice.
  3. Ale toto už je generátorová notace slovníku. Až na dva rozdíly se syntaxe podobá generátorové notaci seznamu. Zaprvé, místo do hranatých závorek je celá uzavřena do složených závorek. Zadruhé, pro každou položku místo jednoho výrazu obsahuje dva výrazy oddělené dvojtečkou. Výraz před dvojtečkou (v našem případě f) představuje klíč slovníku. Výraz za dvojtečkou (v našem případě os.stat(f)) je hodnota.
  4. Generátorová notace slovníku produkuje slovník.
  5. Klíče uvedeného slovníku zachycují jména souborů, která se vrátila z volání glob.glob('*test*.py').
  6. Hodnotou přidruženou ke každému klíči je hodnota vrácená funkcí os.stat(). To znamená, že v tomto slovníku můžeme na základě jména souboru „vyhledat“ jeho metadata. Jednou z částí metadat je st_size, zachycující velikost souboru. Soubor alphameticstest.py obsahuje 2509 bajtů.

Také u generátorové notace slovníků (podobně jako u generátorové notace seznamů) můžeme přidat podmínku if, která zajistí filtraci vyhodnocením výrazu pro každou položku vstupní posloupnosti.

>>> import os, glob, humansize
>>> metadata_dict = {f:os.stat(f) for f in glob.glob('*')}                                  
>>> humansize_dict = {os.path.splitext(f)[0]:humansize.approximate_size(meta.st_size) \     
...                   for f, meta in metadata_dict.items() if meta.st_size > 6000}          
>>> list(humansize_dict.keys())                                                             
['romantest9', 'romantest8', 'romantest7', 'romantest6', 'romantest10', 'pluraltest6']
>>> humansize_dict['romantest9']                                                            
'6.5 KiB'
  1. Tato generátorová notace konstruuje seznam všech souborů v aktuálním pracovním adresáři (glob.glob('*')), získává metadata každého souboru (os.stat(f)) a vytváří slovník, jehož klíči jsou jména souborů a k nim přiřazené hodnoty jsou metadata každého souboru.
  2. Tato generátorová notace vychází z předchozí. Odfiltrovává soubory menší než 6000 bajtů (if meta.st_size > 6000) a takto přefiltrovaný seznam používá k vytvoření slovníku. Jeho klíče tvoří jména souborů bez přípony (os.path.splitext(f)[0]) a hodnotami jsou přibližné velikosti těchto souborů (humansize.approximate_size(meta.st_size)).
  3. V předchozím příkladu jsme si ukázali, že těchto souborů je šest. Z toho vyplývá, že slovník bude mít šest položek.
  4. Hodnotou každého klíče je řetězec vrácený funkcí approximate_size().

Další legrácky s generátorovou notací slovníků

Následující trik využívající generátorové notace slovníku se nám jednoho dne může hodit. Jde o vzájemnou záměnu klíčů a hodnot slovníku.

>>> a_dict = {'a': 1, 'b': 2, 'c': 3}
>>> {value:key for key, value in a_dict.items()}
{1: 'a', 2: 'b', 3: 'c'}

Bude to samozřejmě fungovat jen v případě, kdy jsou hodnoty ve slovníku neměnitelného typu (immutable), jako jsou řetězce nebo n-tice. Pokud totéž zkusíte se slovníkem, který obsahuje seznamy, dojde k velkolepé havárii.

>>> a_dict = {'a': [1, 2, 3], 'b': 4, 'c': 5}
>>> {value:key for key, value in a_dict.items()}
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in <dictcomp>
TypeError: unhashable type: 'list'

Generátorová notace množin

Neměli bychom opomenout, že i syntaxe pro množiny zahrnuje generátorovou notaci. Pozoruhodně se podobá syntaxi pro generátorový zápis slovníků. Jediný rozdíl spočívá v tom, že množiny mají místo párů klíč: hodnota jen hodnoty.

>>> a_set = set(range(10))
>>> a_set
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
>>> {x ** 2 for x in a_set}           
{0, 1, 4, 81, 64, 9, 16, 49, 25, 36}
>>> {x for x in a_set if x % 2 == 0}  
{0, 8, 2, 4, 6}
>>> {2**x for x in range(10)}         
{32, 1, 2, 4, 8, 64, 128, 256, 16, 512}
  1. Vstupem generátorové notace množiny může být množina. Tato generátorová notace množiny vyhodnocuje druhé mocniny prvků z množiny čísel od 0 do 9.
  2. Generátorové notace množin (stejně jako generátorové notace seznamů a slovníků) mohou obsahovat podmínku if, která vstupní položky před zařazením do výsledné množiny filtruje.
  3. Vstupem generátorové notace množiny ale nemusí být množina. Může jí být jakákoliv posloupnost.

Přečtěte si

© 2001–11 Mark Pilgrim