Níže uvedený text pochází z prvního vydání. Nad tímto textem se nachází aktuální stav po revizi směřující k druhému vydání.
V této kapitole se podíváme na to, jak jazyk Python podporuje další styl programování, takzvané funkcionální programování. Podobně jako u rekurze jde vyloženě o téma pro pokročilé, které v tomto okamžiku můžete jednoduše ignorovat. Techniky funkcionálního programování mají své použití i v každodenní programátorské praxi. Zastánci funkcionálního programování věří, že představuje od základů lepší způsob vývoje software.
Funkcionální programování by se nemělo zaměňovat s příkazovým (nebo také procedurálním) programováním. Nepodobá se ani objektově orientovanému programování. Jde o něco jiného. Ne sice zcela jiného, protože koncepty, se kterými budeme pracovat, patří k známým konceptům programování. Jsou pouze vyjádřeny jiným způsobem. Mírně se liší i základní filozofie způsobu používání těchto konceptů při řešení problémů.
Funkcionální programování je založené na výrazech (ve smyslu
matematickém). Funkcionální programování bychom vlastně mohli popsat
termínem programování orientované na výrazy, protože se zde vše
redukuje právě na výrazy. Asi byste si mohli vzpomenout, že výraz je složen
z operací a proměnných takovým způsobem, že jeho výsledkem je jedna hodnota.
Takže například x == 5
je boolovský výraz. Zápis
5 + (7 - y)
představuje aritmetický výraz.
A u zápisu string.search("Ahoj, vy tam!", "Aho")
jde o
řetězcový výraz. V posledním uvedeném případě jde současně o volání funkce v
rámci modulu string
. Jak později uvidíme, hrají funkce v rámci
funkcionálního programování velmi důležitou roli. (To vás asi podle jména už
napadlo.)
S funkcemi se ve funkcionálním programování zachází jako s objekty. To
znamená, že jsou uvnitř programu často předávány podobným způsobem, jako se
předávají proměnné. Příklady jsme viděli v našich programech s grafickým
uživatelským rozhraním, kde jsme jméno funkce přiřazovali atributu
command
ovládacího prvku Button
(tlačítko). S
funkcí pro obsluhu události jsme zacházeli jako s objektem a odkaz na ni
jsme předávali prvku tlačítka. Myšlenka předávání funkcí v rámci našeho
programu je pro funkcionální programování myšlenkou klíčovou.
Funkcionální programy bývají rovněž silně orientovány na práci se seznamy.
Závěrem rekněme, že se funkcionální programování snaží soustředit spíše na co, než jak chceme řešit. To znamená, že funkcionální program by měl spíše popisovat řešený problém, než aby se soustředil na vlastní mechanismus řešení. Jazyků, které o sobě tvrdí, že pracují tímto způsobem, existuje několik. Jedním z nejrozšířenějších je Haskell. Na jeho webovém serveru (www.haskell.org) naleznete řadu článků, které popisují jak filosofii funkcionálního programování, tak samotný jazyk Haskell. (Podle mého osobního mínění jsou zmíněné cíle, jakkoliv jsou chvályhodné, zastánci funkcionálního programování poněkud přeceňovány.)
Struktura čistě funkcionálního programu je definována výrazem, který zachycuje požadovaný cíl. Každý term (viz následující poznámka) daného výrazu je zase vyjádřením určité charakteristiky řešeného problému. Vyhodnocením každého z těchto termů nakonec dospějeme k řešení.
Poznámka překladatele: Term neboli terminální podvýraz je v rámci zápisu výrazu jeho dále nedělitelná součást. Můžeme si zde představit volání pojmenované funkce.
Dobrá. To je tedy teoretický pohled. A funguje to vůbec? Ano, někdy to funguje velmi dobře. Pro řešení určitých typů problémů jde o přirozenou a mocnou techniku. Pro řadu dalších problémů tento přístup naneštěstí vyžaduje poměrně abstraktní způsob myšlení, který je velmi ovlivněn matematickými principy. Výsledný kód je pro laického programátora často velmi nečitelný. Ale výsledný kód bývá často mnohem kratší a spolehlivější, než odpovídající kód zapsaný příkazovým stylem. A právě posledně zmiňované kvality přitáhly ke studiu funkcionálního programování mnoho programátorů, kteří používali procedurální nebo objektově orientovaný způsob programování. Dokonce i když se mu nebudete věnovat s plným nasazením, poskytuje funkcionální programování několik mocných nástrojů, které mohou využívat všichni.
Python poskytuje několik funkcí, které umožňují uplatnění funkcionálního přístupu k programování. Tyto funkce patří k rysům, které byly zavedeny hlavně kvůli usnadnění práce, protože bychom je v jazyce Python mohli docela snadno zapsat sami. Mnohem důležitější je ovšem přímé vyjádření záměru, dané jejich existencí. Jde zejména o záměr poskytnout programátorovi v jazyce Python možnost práce ve stylu funkcionálního programování — pokud si přeje jej využít.
Podíváme se na některé z těchto funkcí a uvidíme, jak budou pracovat s některými vzorovými funkcemi, které definujeme jako:
spam = ['pork', 'ham', 'spices'] numbers = [1, 2, 3, 4, 5] def eggs(item): return item
Tato funkce aplikuje pythonovskou funkci funkce
na každý z
členů posloupnosti posloupnost
. Mějme výraz:
L = map(eggs, spam) print L
Výsledem je nový seznam (v tomto případě totožný se seznamem
spam
), který je vrácen v L
.
Stejného efektu bychom mohli dosáhnout zápisem:
for i in spam: L.append(i) print L
Ale povšimněte si skutečnosti, že použitím funkce map()
odstraníme potřebu zápisu vnořeného bloku kódu. Z určitého pohledu tím
snižujeme složitost programu o jednu úroveň. Uvidíme, že jde o jakési
opakující se téma funkcionálního programování. Při používání těchto funkcí
se relativní složitost kódu snižuje odstraňováním bloků.
Jak již jméno napovídá, funkce filter()
vybírá všechny prvky
posloupnosti posloupnost
, pro které funkce
vrací
hodnotu True
. Uvažujme náš seznam čísel numbers
.
Pokud chceme vytvořit nový seznam, který se skládá pouze z lichých čísel,
pak jej můžeme vytvořit takto:
def jeLiche(n): return (n%2 != 0) # použijeme operátor modulo
L = filter(jeLiche, numbers)
print L
Řešení bychom mohli napsat i takto:
def jeLiche(n): return (n%2 != 0) for i in numbers: if jeLiche(i): L.append(i) print L
Opět si povšimněte, že konvenční kód vyžaduje k dosažení stejného výsledku použití dvou úrovní odsazení. A opět, zvýšení úrovně odsazení je příznakem zvýšené složitosti kódu.
Význam funkce reduce()
je poněkud méně zřejmý. Tato funkce
redukuje seznam na jedinou hodnotu tím, že jeho prvky kombinuje pomocí
dodané funkce. Mohli bychom například sečíst hodnoty prvků seznamu a vrátit
hodnotu celkového součtu takto:
def secti(i, j): return i + j print reduce(secti, numbers)
Stejně jako v předchozích případech bychom mohli použít konvenční zápis:
vysledek = 0
for i in range(len(numbers)): # použijeme indexování
vysledek = vysledek + numbers[i]
print vysledek
I když v tomto případě bude výsledek stejný, vždycky to není tak
přímočaré. Funkce reduce()
ve skutečnosti dělá to, že
volá dodanou funkci, přičem jí předává první dva členy posloupnosti.
Výsledek uloží místo nich. Jinými slovy, přesnější reprezentace funkce
reduce()
vypadá takto:
def reduce(numbers): L = numbers[:] # vytvoř kopii originálu while len(L) >= 2: i, j = L[0], L[1] # použijeme násobné přiřazení L = [i+j] + L[2:] # součet prvních dvou a zbytek return L[0]
Opět vidíme, že technika funkcionálního programování redukuje složitost kódu odstraněním nutnosti použití odsazených bloků kódu.
Jedním z rysů, kterého jste si v dosud uvedených příkladech mohli
všimnout, je skutečnost, že funkce, které předáváme jako argument funkcí pro
podporu funkcionálního programování, bývají velmi krátké. Často jde o jediný
řádek kódu. Abychom si ušetřili námahu při definování velkého množství velmi
malých funkcí, nabízí nám Python další pomocnou funkci pro podporu
funkcionálního programování — lambda
.
Pojem lambda výraz se používá pro referenci na anonymní funkci, to znamená pro blok kódu, který lze provést stejným způsobem, jako kdyby se jednalo o funkci, která ovšem nemá jméno. Lambda výrazy mohou být definovány uvnitř programu všude tam, kde se může vyskytovat legální pythonovský výraz. To znamená, že je můžeme používat i uvnitř zmiňovaných pomocných funkcí pro podporu funkcionálního stylu programování.
Zápis lambda výrazu vypadá takto:
lambda <seznam parametrů> : <pythonovský výraz používající parametry>
Takže výše zmíněnou funkci secti
bychom mohli přepsat
jako:
secti = lambda i, j: i + j
Použití řádku s definicí funkce se můžeme úplně vyhnout zápisem lambda
výrazu přímo uvnitř volání funkce reduce()
:
print reduce(lambda i, j: i+j, numbers)
Podobným způsobem můžeme přepsat naše příklady používající funkce
map
a filter
:
L = map(lambda i: i, spam) print L L = filter(lambda i: (i%2 != 0), numbers) print L
Poznámka překladatele: Pojem list comprehension (čti list komprihenšn) je obtížně přeložitelný tak, aby byl výsledek překladu krátký a přesný. Je založen na jednom z významů slova comprehension, pro který existuje synonymum comprise (čti kemprais). Vyjadřuje skutečnost něco obsahovat, být z něčeho poskládán, složen. Slovo list se zde jednoznačně překládá jako seznam. Pojem list comprehension se používá pro syntaktickou konstrukci, která předepisuje způsob vygenerování seznamu z jiné kolekce. Vulgárně bychom jej tedy mohli přeložit jako sestavovač seznamů. V odborné terminologii by se možná dal použít pojem funkcionální konstruktor seznamu. Ale raději zůstaňme u originálního pojmu. Smiřte se s tím, že naučit se programovat do značné míry znamená také učit se anglicky.
Mechanismus list comprehension představuje techniku pro vytváření nových seznamů, která pochází z jazyka Haskell a v jazyce Python byla uvedena od verze 2.0. Má poněkud zatemňující syntaxi, podobající se matematickému zápisu množin. Vypadá takto:
[<výraz> for <proměnná> in <kolekce> if <podmínka>]
Můžeme ji vyjádřit ekvivalentním zápisem:
L = [] for proměnná in kolekce: if podmínka: L.append(výraz)
Stejně jako u ostatních konstrukcí z funkcionálního programování i zde dochází k úspoře řádků a dvou úrovní odsazení. Podívejme se na nějaké praktické příklady.
Nejdříve si vytvoříme seznam sudých čísel:
>>> [n for n in range(10) if n % 2 == 0 ] [0, 2, 4, 6, 8]
Tento zápis říká, že chceme seznam hodnot (n
), kde se
n
pohybuje v rozmezí 0 až 9 a současně platí, že n
je sudé (tedy n % 2 == 0
).
Podmínka, uvedená na konci, může být samozřejmě nahrazena funkcí — za předpokladu, že tato funkce vrací hodnotu, kterou může Python interpretovat jako hodnotu boolovskou. Takže pokud se znovu podíváme na předchozí příklad, můžeme jej přepsat následovně:
>>> def jeSude(n): return ((n % 2) == 0) >>> [ n for n in range(10) if jeSude(n) ] [0, 2, 4, 6, 8]
Vytvořme nyní seznam druhých mocnit prvních pěti čísel:
>>> [n * n for n in range(5)] [0, 1, 4, 9, 16]
Povšimněte si, že část if
nemusíme povinně použít. Úvodní
výraz zde má podobu n * n
a používáme pro něj všechny hodnoty
ze zadaného intervalu.
Na závěr použijme místo funkce range()
již existujíci
kolekci:
>>> hodnoty = [1, 13, 25, 7] >>> [x for x in hodnoty if x < 10] [1, 7]
Takový zápis bychom mohli použít jako náhradu za funkci
filter
:
>>> filter(lambda x: x < 10, hodnoty) [1, 7]
To, který ze stylů se zdá přirozenější nebo vhodnější, je čistě subjektivní věc. Pokud budujeme novou kolekci z již existující kolekce, můžeme použít buď dříve uvedené funkce z oblasti funkcionálního programování, nebo můžeme použít list comprehension. Pokud vyloženě vytváříme úplně novou kolekci, pak je obvykle snazší použít list comprehension.
Když to shrneme, tak ačkoliv se nám tyto konstrukce mohou zdát přitažlivé, může pro dosažení požadovaného výsledku tento přístup vést k tak složitým výrazům, že je prostě jednodušší rozepsat je na tradiční pythonovské ekvivalenty. Není to žádná ostuda. Dobrá čitelnost je vždy lepší, než zatemňování věcí jen proto, abychom vypadali chytře.
Zmíněné funkce jsou samozřejmě užitečné samy o sobě, ale přesto nestačí k tomu, aby v jazyce Python umožnily plně funkcionální styl programování. Funkcionálním způsobem musíme změnit, nebo alespoň nahradit, používání řídicích struktur jazyka. Jednou z cest, kterou toho můžeme dosáhnout, je využití vedlejšího efektu toho, jak Python vyhodnocuje boolovské výrazy.
Protože Python používá zkrácené vyhodnocování boolovských výrazů, lze některých vlastností těchto výrazů využít. Připomeňme si zkrácené vyhodnocování: Pokud se vyhodnocuje boolovský výraz, začíná vyhodnocování na levé straně výrazu a postupuje směrem vpravo. Vyhodnocování končí v okamžiku, kdy již pro určení konečného výsledku není další vyhodnocování nutné.
Pro zviditelnění zkráceného vyhodnocení použijeme zvláštní postup. Nejdříve nadefinujeme dvě funkce, které nám oznámí, že jsou volány, a poté vrátí hodnotu, odpovídající jejich jménům:
>>> def TRUE(): ... print 'TRUE' ... return True # True se ve verzi 2.2 tiskne jako 1 ... >>>def FALSE(): ... print 'FALSE' ... return False # False se ve verzi 2.2 tiskne jako 0 ...
Nyní je použijeme, abychom zjistili, jak se vyhodnocování boolovských výrazů provádí:
>>> print TRUE() and FALSE() TRUE FALSE 0 >>> print TRUE() and TRUE() TRUE TRUE 1 >>> print FALSE() and TRUE() FALSE 0 >>> print TRUE() or FALSE() TRUE 1 >>> print FALSE() or TRUE() FALSE TRUE 1 >>> print FALSE() or FALSE() FALSE FALSE 0
Poznámka překladatele: Boolovský typ s hodnotami
False
a True
byl zaveden ve verzi Python 2.2.
Jména False
a True
však pouze pojmenovávala
konstanty 0
a 1
, což se projevovalo při tisku.
Předchozí verze jazyka Python boolovský typ vůbec neznaly a místo
boolovských hodnot se používaly hodnoty 0
a 1
,
jako například v jazyce C. Řada programátorů si přesto definovala konstanty
False
a True
. Od verze 2.3 se boolovský typ stal
plnohodnotným zabudovaným typem jazyka. Místo hodnot 0
a
1
příkaz print
vytiskne False
nebo
True
. Od verze 2.3 tedy bude výstup vypadat takto:
>>> print TRUE() and FALSE() TRUE FALSE False >>> print TRUE() and TRUE() TRUE TRUE True >>> print FALSE() and TRUE() FALSE False >>> print TRUE() or FALSE() TRUE True >>> print FALSE() or TRUE() FALSE TRUE True >>> print FALSE() or FALSE() FALSE FALSE False
Povšimněte si, že u logického součinu (operátor
and
) dochází k vyhodnocování druhé části výrazu tehdy a jen
tehdy, když nabývá první část výrazu hodnoty True (pravda). Pokud první
část nabývá logické hodnoty False (nepravda), pak se druhá část
nevyhodnocuje, protože výraz jako celek již nemůže nabýt hodnoty
True.
Podobně je tomu u logického součtu (operátor or
) v
případě, kdy první část nabývá hodnoty True. V takovém případě již druhá
část nemusí být vyhodnocována, protože celek musí nabývat hodnoty
True.
Při vyhodnocování boolovského výrazu se uplatňuje ještě jeden rys,
kterého můžeme využít. Jde o to, že při vyhodnocování výrazu v boolovském kontextu
Python nevrací jednoduše hodnotu 1
nebo 0
(nebo True
či
False
). Místo toho se vrací skutečná hodnota výrazu.
Takže pokud testujeme řetězec na prázdnost (prázdný řetězec se chápe jako
False
) takto:
if "Tento retezec neni prazdny": print "Neni prazdny" else: print "Prazdny retezec"
... Python prostě vrací testovaný řetězec.
Této vlastnosti můžeme využít k realizaci chování, které se podobá větvení. Dejme tomu, že máme například následující úsek kódu:
if TRUE(): print "Je to pravda (True)" else: print "Je to nepravda (False)"
Můžeme jej nahradit konstrukcí ve stylu funkcionálního programování:
V = (TRUE() and "Je to pravda (True)") or ("Je to nepravda (False)") print V
Zkuste tento příklad provést a poté nahraďte volání TRUE()
voláním FALSE()
.
To znamená, že jsme při využití zkráceného vyhodnocování boolovských
výrazů nalezli způsob, jak z našich programů odstranit konvenční příkazy
if
/else
. Možná si vzpomínáte, že jsme u tématu rekurze zjistili, že rekurzi můžeme použít k
nahrazení konstrukce cyklu. Takže kombinací těchto dvou efektů můžeme z
našeho programu odstranit všechny konvenční řídicí struktury a nahradit je
čistými výrazy. Jde o velký krok směrem možnosti řešení problémů čistě ve
stylu funkcionálního programování.
Abychom to vše ukázali na praktickém příkladě, napišme si čistě
funkcionálním stylem program pro výpočet faktoriálu, který používá rekurzi
místo cyklu a zkrácené vyhodnocování výrazu místo if/else
:
def factorial(n): return ((n <= 1) and 1) or (factorial(n - 1) * n)
To je opravdu celé. Možná to není tak dobře srozumitelné jako konvenčnější pythonovský kód, ale funguje to a jde o funkci zapsanou čistě ve funkcionálním stylu — jde tedy o čistý výraz.
V tomto místě se možná pozastavujete nad tím, co je vlastně cílem toho všeho? A nejste sami. I když funkcionální programování přitahuje řadu teoretiků v oblasti výpočetních věd (Computer Science) — a často matematiky —, zdá se, že většina praktických programátorů používá techniky funkcionálního programování velmi zřídka. Navíc používají jakýsi hybridní přístup, kdy tyto techniky kombinují s tradičnějším, příkazovým stylem podle toho, jak to považují za vhodné.
Poznámka překladatele: Podle mého názoru může mít takové používání technik funkcionálního programování příčiny jak subjektivní, tak objektivní. Mezi subjektivní příčiny bych zařadil ten fakt, že se jedná přece jen o téma pro pokročilé. Do hloubky se s ním na vysokoškolské úrovni seznámil jen zlomek těch, kteří pracují jako praktičtí programátoři. Mezi objektivní příčiny bych zařadil skutečnost, že model, na kterém je funkcionální programování založeno, neodpovídá přesně modelu, který se používá pro výstavbu běžných počítačů. Von Neumannovská koncepce počítače s pamětí pro data a pro program a s procesorem, který vykonává instrukce, je většinou mnohem efektivněji využitelná pro programy napsané procedurálním stylem. Jinými slovy, dobře napsaný procedurální program na těchto počítačích bude typicky výkonnější, než dobře napsaný odpovídající program zapsaný ve funkcionálním jazyce. Z matematického (teoretického) pohledu je ale tato skutečnost nevýznamná. Proto se řada matematicky zaměřených programátorů, kteří typicky neřeší problémy denní praxe, otázkami skutečného výkonu příliš nezabývá. Funkcionální přístup je z hlediska matematického určitě jednodušší, lépe uchopitelný matematickými nástroji.
Pokud musíme na prvky seznamu aplikovat operace, které lze přirozeně
vyjádřit pomocí map
, reduce
nebo
filter
, pak je v každém případě použijte. Občas můžete dokonce
zjistit, že rekurze je v daném případě vhodnější, než konvenční cyklus. V
ještě řidších případech můžete najít použití i pro zkrácené vyhodnocování
místo konvenčního if
/else
— zejména při
použití uvnitř výrazu. Tak jako je tomu u každého programátorského nástroje,
nenechte se unést jen určitou filozofií. Pro řešení konkrétního zadání se
raději snažte použít nejvhodnější nástroj, ať už je jakéhokoliv charakteru.
Přinejmenším si buďte vědomi toho, že existují alternativy.
K otázce lambda výrazů zbývá poznamenat ještě jeden závěr. I
když neuvažujeme ve stylu funkcionálního programování, existuje jedna
oblast, ve které lze dobře použít operátor lambda
. Jde o
definice funkcí pro obsluhu událostí (event handler), se kterými se
setkáváme při programování uživatelského rozhraní. Tyto funkce jsou často
velmi krátké, případně se v nich volají nějaké rozsáhlejší funkce, kterým se
předává několik napevno použitých hodnot argumentů. V těchto případech lze v
roli funkce pro obsluhu událostí použít lambda funkci. Tím se vyhneme
nutnosti definovat mnoho malých funkcí, které by nám zaplňovaly prostor
jmény, použitými jen jednou. Vzpomeňte si, že příkaz lambda
vrací objekt typu funkce. Právě tento objekt (funkce) je předán
prvku typu widget a je volán v době, kdy se vyskytne příslušná událost.
Pokud si vzpomenete, jak se v Tkinter definuje prvek typu
Button
(tlačítko), pak můžeme lambda použít následovně:
def write(s): print s b = Button(rodic, text="Stiskni mne", command = lambda : write("Stiskl jsi mne!")) b.pack()
V tomto případě bychom samozřejmě mohli dosáhnout téhož efektu tím, že
bychom definovali přednastavenou hodnotu parametru funkce
write()
. Atributu command
prvku typu
Button
bychom pak jednoduše přiřadili write
(bez použití závorek).
Nicméně i v takto jednoduchém případě nám použití lambda
přináší tu výhodu, že jediná definice funkce write()
může být
použita pro více tlačítek tak, že jí přes lambda
předáme různé
řetězce. To znamená, že můžeme přidat druhé tlačítko takto:
b2 = Button(rodic, text="Nebo mne", command = lambda : write("Stiskl jsi mne. (Tvoje druhe tlacitko.)")) b2.pack()
Příkaz lamda
můžeme použít i u techniky, kdy používáme metodu
ovládacího prvku bind()
, které jako argument předáváme objekt
pro ošetření události:
b3 = Button(rodic, text="Mne taky stiskni") b3.bind(<Button-1>, lambda ev : write("Stisknuto"))
To je vše, co se zde o funkcionálním programování dozvíte. Pokud se na ně chcete podívat trochu hlouběji, existuje celá řada dalších informačních zdrojů. Některé z nich jsou uvedeny níže.
comp.lang.functional
.Pokud kdokoliv nalezne dobrý odkaz, pošlete mi zprávu prostřednictvím níže uvedeného odkazu. (Poznámka překladatele: Můžete použít odkaz z anglického originálu, ale i z českého překladu. V druhém případě jej autorovi předám.)
Pokud vás napadne, co by se dalo na překladu této kapitoly vylepšit, zašlete e-mail odklepnutím Tím budou do dopisu automaticky vloženy informace o tomto HTML dokumentu.
$Id: cztutfctnl.html,v 1.5 2004/08/31 11:55:13 prikryl Exp $