Podpora funkcionálního programování v Pythonu a knihovna functools

27. 7. 2023
Doba čtení: 26 minut

Sdílet

 Autor: Depositphotos
Python je multiparadigmatickým jazykem, což znamená, že lze psát prakticky čistě imperativně, ale i objektově. Navíc v Pythonu nalezneme poměrně velké množství vlastností převzatých z funkcionálních jazyků.

Obsah

1. Podpora funkcionálního programování v Pythonu a knihovna functools

2. Čistě funkcionální jazyky vs. hybridní jazyky

3. Funkcionální vlastnosti programovacího jazyka Python

4. Funkce vyššího řádu akceptující jinou funkci jako parametr

5. Funkce vyššího řádu vracející jinou funkci

6. Malá odbočka – implementace všech operátorů Pythonu formou funkcí

7. Typ „funkce“ z pohledu typového systému Pythonu

8. Typy funkcí vyšších řádů z předchozích demonstračních příkladů

9. Klasické funkce vyššího řádu: map, filter a reduce/fold

10. Funkce vyššího řádu map

11. Předání vlastní pojmenované či anonymní funkce do funkce vyššího řádu map

12. Funkce vyššího řádu filter

13. Ukázky použití funkce filter

14. Generátorové notace vs. funkce vyššího řádu mapfilter

15. Náhrada funkce map za generátorovou notaci

16. Náhrada funkce filter za generátorovou notaci

17. Funkce vyššího řádu reduce

18. Obsah navazujícího článku

19. Repositář s demonstračními příklady

20. Odkazy na Internetu

1. Podpora funkcionálního programování v Pythonu a knihovna functools

Programovací jazyk Python je multiparadigmatickým programovacím jazykem, což v praxi znamená, že menší skripty a nástroje lze psát prakticky čistě imperativně (ovšem strukturovaně) a pro rozsáhlejší aplikace Python podporuje objektově orientované programování. To ovšem není vše, protože i v Pythonu nalezneme poměrně velké množství vlastností převzatých z funkcionálních jazyků (asi nejviditelnější vlastnost: funkce jsou plnohodnotnými typy a tím pádem jsou v Pythonu podporovány funkce vyššího řádu, lokální funkce atd. atd.) a dokonce pro něho vznikly knihovny určené pro podporu funkcionálního přístupu.

Poznámka: na tomto místě je vhodné poznamenat, že původní autor Pythonu (v současnosti na dlouhé dovolené :-) se k funkcionálním prvkům přidaným do Pythonu stavěl poněkud rezervovaně, o čemž se ostatně zmíníme dále.

V dnešním článku se seznámíme s některými základními koncepty funkcionálního programování; zaměříme se ovšem na ty koncepty, které lze najít v Pythonu (proto je nutné – alespoň prozatím – vynechat podporu pro neměnitelné hodnoty). Kvůli tomu, že některé důležité funkcionální prvky jsou přesunuty do standardních balíčků, seznámíme se dnes, i když prozatím pouze ve stručnosti, i se standardním balíčkem nazvaným velmi příhodně: functools.

2. Čistě funkcionální jazyky vs. hybridní jazyky

Termín funkcionální programování resp. ještě více funkcionální programovací jazyk je dnes do určité míry zneužíván a používán v nesprávném kontextu (ovšem mnohem hůře je na tom termín objektově orientované programování, na jehož významu se prakticky nikdo nedokáže shodnout :). Vraťme se však k funkcionálním programovacím jazykům. Tyto jazyky je vhodné rozdělit do dvou kategorií – čistě funkcionální jazyky a hybridní jazyky. Mezi čistě funkcionální jazyky patří především Haskell, ale také Hope nebo Miranda. A do skupiny hybridních jazyků patří spíše praktičtěji orientované jazyky typu LISP (ten skutečně není čistě funkcionální), Clojure (to má k čistě funkcionálním jazykům blíže), Scheme, Standard ML a (podle mého názoru) velmi povedený jazyk F#, k němuž se ještě na Rootu několikrát vrátíme.

Čistě funkcionální jazyky obecně nedovolují modifikace datových struktur, což vede k tomu, že se v nich nepoužívají proměnné (ve standardním smyslu tohoto slova), ale hodnoty, které jsou z pohledu programátora konstantní. Naproti tomu u jazyků hybridních existuje možnost použití modifikovatelných proměnných; většinou jsou ovšem možnosti modifikace poměrně striktně hlídány a řízeny (příkladem může být opět Clojure nebo na druhé straně F#). Ovšem obě skupiny jazyků mají minimálně jednu vlastnost společnou – funkce jsou v nich plnohodnotným typem a všude tam, kde lze zadat nějakou hodnotu (parametr jiné funkce, člen ve výrazu, návratová hodnota funkce atd.) je možné zadat i funkci. Navíc se s funkcemi dají typicky provádět i další operace, než jejich pouhá definice a volání. Například je umožněn „currying funkcí“, k čemuž se dnes ještě vrátíme.

3. Funkcionální vlastnosti programovacího jazyka Python

V dnešním článku se budeme primárně zabývat programovacím jazykem Python, který ovšem není řazen do skupiny funkcionálních programovacích jazyků, protože mu chybí některé důležité vlastnosti (resp. přesněji řečeno mu naopak některé vlastnosti „přebývají“, například možnost modifikace datových struktur). Nicméně i přesto v Pythonu některé důležité funkcionální prvky nalezneme. Především se v Pythonu s funkcemi pracuje jako s plnohodnotnými datovými typy, i když zde poněkud uměle existují rozdíly mezi plnohodnotnými pojmenovanými funkcemi na straně jedné a omezenými lambda výrazy na straně druhé (to ovšem znamená, že funkce mohou být vytvořeny a použity i lokálně, v uzávěru atd!). Z toho, že jsou funkce plnohodnotnými typy, plyne i fakt, že jsou podporovány funkce vyššího řádu, tj. funkce, které jako své parametry akceptují jiné funkce či jiné funkce vrací. A ještě jeden z této vlastnosti plynoucí fakt – jsou podporovány uzávěry (closure), k nimiž se ještě vrátíme.

Některé funkce vyššího řádu, například map a filter, nalezneme přímo v základní knihovně Pythonu. Python navíc umožňuje manipulaci s funkcemi s využitím dekorátorů. Zbývají nám další dvě důležité vlastnosti, které lze ve funkcionálních jazycích nalézt – podporu pro kompozici funkcí a podporu pro currying. Jak se tyto dvě vlastnosti v Pythonu využívají, si ukážeme v navazujícím textu.

4. Funkce vyššího řádu akceptující jinou funkci jako parametr

Termínem funkce vyššího řádu se označují ty funkce (ať již definované v knihovně nebo uživatelem), které jako své parametry akceptují jiné funkce či naopak vrací funkci jako svoji návratovou hodnotu. Nejprve si ukažme první variantu funkcí vyššího řádu, tedy funkci, která akceptuje jinou funkci jako svůj parametr. Tuto funkci, která v našem konkrétním případě akceptuje implementaci libovolného binárního „operátoru“ zapsaného formou funkce a navíc ještě akceptuje dva číselné parametry, nazveme calc, protože provede vyhodnocení operátoru s dosazením obou předaných parametrů:

def calc(operator, x, y):
    return operator(x, y)
 
 
def add(x, y):
    return x + y
 
 
def mul(x, y):
    return x * y
 
 
def less_than(x, y):
    return x < y
 
 
z = calc(add, 10, 20)
print(z)
 
z = calc(mul, 10, 20)
print(z)
 
z = calc(less_than, 10, 20)
print(z)
Poznámka: zdrojový kód tohoto demonstračního příkladu naleznete na adrese https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/binary_operator.py.

Výsledky získané po spuštění tohoto skriptu nejsou příliš překvapivé:

30
200
True

5. Funkce vyššího řádu vracející jinou funkci

Druhá varianta funkce vyššího řádu naopak funkci vrací jako svoji návratovou hodnotu. Pokud zůstaneme u našich příkladů s operátory, může se jednat o funkci get_operator, která vrací funkci, popř. hodnotu None:

def get_operator(symbol):
    if symbol == "+":
        return add
    elif symbol == "*":
        return mul
    else:
        return None
 
 
def calc(operator, x, y):
    return operator(x, y)
 
 
def add(x, y):
    return x + y
 
 
def mul(x, y):
    return x * y
 
 
def less_than(x, y):
    return x < y
 
 
z = calc(get_operator("+"), 10, 20)
print(z)
 
z = calc(get_operator("*"), 10, 20)
print(z)
 
z = calc(less_than, 10, 20)
print(z)

Výsledky:

30
200
True
Poznámka: zdrojový kód tohoto demonstračního příkladu naleznete na adrese https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/get_operator1.py.

Aby byl fakt, že funkce jsou plnohodnotnými hodnotami ještě více zdůrazněn, můžeme předchozí demonstrační příklad přepsat do podoby založené na použití slovníků (dictionary), v němž jsou funkce hodnotami:

def get_operator(symbol):
    operators = {
            "+": add,
            "*": mul,
            "<": less_than,
    }
    return operators[symbol]
 
 
def calc(operator, x, y):
    return operator(x, y)
 
 
def add(x, y):
    return x + y
 
 
def mul(x, y):
    return x * y
 
 
def less_than(x, y):
    return x < y
 
 
z = calc(get_operator("+"), 10, 20)
print(z)
 
z = calc(get_operator("*"), 10, 20)
print(z)
 
z = calc(get_operator("<"), 10, 20)
print(z)
Poznámka: zdrojový kód tohoto demonstračního příkladu naleznete na adrese https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/get_operator2.py.
Poznámka: výsledky budou v obou případech totožné.

6. Malá odbočka – implementace všech operátorů Pythonu formou funkcí

Právě v případě, kdy využíváme funkcionální vlastnosti programovacího jazyka Python, se ukazují poměrně zásadní rozdíly mezi klasickými funkcemi (popř. lambda výrazy) a operátory. Z tohoto důvodu nalezneme ve standardní knihovně Pythonu i balíček nazvaný operator, v němž jsou všechny operátory přepsány do formy funkcí. Použití tohoto balíčku v našem (upraveném) demonstračním příkladu je triviální:

from operator import *
 
 
def get_operator(symbol):
    operators = {
            "+": add,
            "*": mul,
            "^": pow,
            "<": lt,
            ">": gt,
    }
    return operators[symbol]
 
 
def calc(operator, x, y):
    return operator(x, y)
 
 
z = calc(get_operator("+"), 10, 20)
print(z)
 
z = calc(get_operator("*"), 10, 20)
print(z)
 
z = calc(get_operator("^"), 10, 20)
print(z)
 
z = calc(get_operator("<"), 10, 20)
print(z)
 
z = calc(get_operator(">"), 10, 20)
print(z)
Poznámka: zdrojový kód tohoto demonstračního příkladu naleznete na adrese https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/standard_operators.py.

7. Typ „funkce“ z pohledu typového systému Pythonu

Jak jsme si již několikrát řekli v předchozích odstavcích, jsou funkce plnohodnotnými hodnotami, s nimiž se v Pythonu může pracovat naprosto stejným způsobem, jako s jakýmikoli jinými hodnotami. To ovšem znamená, že tyto hodnoty musí být nějakého typu. Typ funkce je – nezávisle na jejím těle – odvozen pouze z typů parametrů a z typu návratové hodnoty, přičemž výchozím typem je v obou případech Any. Plné typové určení funkce se zapisuje následujícím způsobem:

Callable[[typ_parametru1, typ_parametru_2, ...] typ_návratové_hodnoty]
Poznámka: Python sice syntakticky umožňuje, aby funkce vracela více hodnot, sémanticky se však jedná o n-tici a tedy o jedinou hodnotu.

S typovými anotacemi funkcí (i dalších hodnot) pracuje například nástroj Mypy, s nímž jsme se již setkali v následujících článcích:

  1. Statické typové kontroly zdrojových kódů Pythonu prováděné nástrojem Mypy
    https://www.root.cz/clanky/staticke-typove-kontroly-zdrojovych-kodu-pythonu-provadene-nastrojem-mypy/
  2. Statické typové kontroly zdrojových kódů Pythonu prováděné nástrojem Mypy (2.část)
    https://www.root.cz/clanky/staticke-typove-kontroly-zdrojovych-kodu-pythonu-provadene-nastrojem-mypy-2-cast/
  3. Statické typové kontroly zdrojových kódů Pythonu prováděné nástrojem Mypy (3)
    https://www.root.cz/clanky/staticke-typove-kontroly-zdrojovych-kodu-pythonu-provadene-nastrojem-mypy-3/

8. Typy funkcí vyšších řádů z předchozích demonstračních příkladů

Nyní již víme, jakým způsobem je možné zapsat typ nějaké funkce na základě typu jejich parametrů i typu návratové hodnoty. Podívejme se tedy na způsob úpravy příkladů z předchozích kapitol tak, aby obsahovaly přesné a úplné typové informace. Začneme příkladem s funkcí vyššího řádu calc, která akceptuje jinou funkci jako svůj parametr:

from typing import Callable
 
 
def calc(operator: Callable[[int, int], int], x: int, y: int) -> int:
    return operator(x, y)
 
 
def add(x: int, y: int) -> int:
    return x + y
 
 
def mul(x: int, y: int) -> int:
    return x * y
 
 
def less_than(x: int, y: int) -> bool:
    return x < y
 
 
z = calc(add, 10, 20)
print(z)
 
z = calc(mul, 10, 20)
print(z)
 
z = calc(less_than, 10, 20)
print(z)
Poznámka: zdrojový kód tohoto demonstračního příkladu naleznete na adrese https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/binary_operator_types.py.

Druhý příklad obsahuje funkci vyššího řádu get_operator, která naopak vrací jinou funkci jako svoji návratovou hodnotu. Plná typová deklarace této funkce vyššího řádu může vypadat takto:

from typing import Callable
 
 
def get_operator(symbol: str) -> Callable[[int, int], int]:
    operators = {
            "+": add,
            "*": mul,
    }
    return operators[symbol]
 
 
def calc(operator: Callable[[int, int], int], x: int, y: int) -> int:
    return operator(x, y)
 
 
def add(x: int, y: int) -> int:
    return x + y
 
 
def mul(x: int, y: int) -> int:
    return x * y
 
 
def less_than(x: int, y: int) -> bool:
    return x < y
 
 
z = calc(get_operator("+"), 10, 20)
print(z)
 
z = calc(get_operator("*"), 10, 20)
print(z)
 
z = calc(less_than, 10, 20)
print(z)
Poznámka: zdrojový kód tohoto demonstračního příkladu naleznete na adrese https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/get_operator_types.py.

Oba výše uvedené příklady obsahují všechny nutné typové informace, o čemž se můžeme velmi snadno přesvědčit:

$ mypy --strict binary_operator_types.py get_operator_types.py 
 
Success: no issues found in 2 source files

9. Klasické funkce vyššího řádu: map, filter a reduce/fold

Zatímco v běžných imperativních programovacích jazycích se seznamy, popř. n-tice či obecné sekvence zpracovávají prvek po prvku s využitím nějaké formy programové smyčky, popř. smyčky „skryté“ například v generátorové notaci seznamu, ve funkcionálních programovacích jazycích se setkáme spíše s aplikací několika funkcí vyššího řádu, které jako svůj vstup akceptují seznam/n-tici/sekvenci a nějakou funkci, která je postupně aplikována buď na prvky sekvence, nebo na prvek sekvence a takzvaný akumulátor, jehož hodnota se postupně při zpracovávání jednotlivých prvků sekvence mění. Výsledkem bývá buď nová sekvence, nebo výsledná hodnota akumulátoru. Tyto funkce se většinou nazývají map, filter a reduce či foldl. Tyto funkce vyššího řádu nalezneme i v Pythonu, přičemž dvě z nich jsou umístěny ve výchozím jmenném prostoru (a nemusí se tedy importovat), zatímco funkci reduce najdeme v již zmíněném balíčku functools.

Poznámka: funkce filter je v některých jazycích a knihovnách nazvána remove-if a má tedy otočený (resp. přesněji řečeno spíše negovaný) význam podmínky, jak ostatně uvidíme dále.

10. Funkce vyššího řádu map

Podívejme se nyní na funkci map. Tato funkce prochází prvky sekvence a aplikuje na ně nějakou další uživatelem zvolenou funkci, podobně jako ve smyčce for-each nebo spíše v generátorové notaci seznamu či n-tice, ovšem namísto využití vedlejšího efektu se na základě návratových hodnot funkce map vytváří nová sekvence prvků.

Poznámka: interně se tedy musí celý algoritmus realizovat přes programovou smyčku, rekurzi či koncovou rekurzi, to nás ovšem jako uživatele funkce map nemusí trápit.

Příkladem použití funkce map může být výpočet délky všech slov ve vstupním textu. Ten lze realizovat snadno – namapováním standardní funkce len na jednotlivá slova získaná rozdělením řetězce metodou split:

message = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
words = message.split()
 
lengths = map(len, words)
print(list(lengths))

Výsledek:

[5, 5, 5, 3, 5, 11, 10, 5, 3, 2, 7, 6, 10, 2, 6, 2, 6, 5, 6]

Povšimněte si, že výslednou sekvenci převádíme zpět na seznam pomocí konstruktoru list.

Poznámka: zdrojový kód tohoto demonstračního příkladu naleznete na adrese https://github.com/tisnik/most-popular-python-libs/blob/master/functools/map1.py.

Vstupem do funkce map ovšem nemusí být pouze seznam, ale například i objekt typu range:

values = range(-10, 11)
 
converted = map(abs, values)
print(list(converted))

Výsledek:

[10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Poznámka: zdrojový kód tohoto demonstračního příkladu naleznete na adrese https://github.com/tisnik/most-popular-python-libs/blob/master/functools/map2.py.

11. Předání vlastní pojmenované či anonymní funkce do funkce vyššího řádu map

Samozřejmě nám nic nebrání v tom, aby „transformační“ funkce předávaná do funkce vyššího řádu map byla získána ze standardní knihovny. Můžeme použít i uživatelskou funkci s jedním parametrem (a jednou výstupní hodnotou – ovšem lze využít i implicitní hodnotu None). Například můžeme získat informace o tom, jaké celočíselné hodnoty jsou „uloženy“ v sekvenci představované objektem typu range:

def sign(value):
    if value < 0:
        return "negative"
    elif value > 0:
        return "positive"
    else:
        return "zero"
 
 
values = range(-10, 11)
 
converted = map(sign, values)
 
for c in converted:
    print(c)

Výsledek získaný po spuštění tohoto skriptu by měl vypadat následovně:

negative
negative
negative
negative
negative
negative
negative
negative
negative
negative
zero
positive
positive
positive
positive
positive
positive
positive
positive
positive
positive
Poznámka: zdrojový kód tohoto demonstračního příkladu naleznete na adrese https://github.com/tisnik/most-popular-python-libs/blob/master/functools/map3.py.

Velmi často se do funkcí vyššího řádu map, filter a reduce předávají různé jednoduché a ad-hoc vytvářené krátké funkce. V mnoha případech nemusíme takové funkce definovat blokem def (a rozšiřovat tak počet identifikátorů ve jmenném prostoru), ale můžeme využít možnost předání lambda výrazu (což v Pythonu ovšem není plnohodnotná anonymní funkce). Předchozí skript lze s využitím konstrukce lambda přepsat do následujícího tvaru:

values = range(-10, 11)
 
converted = map(lambda x: "negative" if x < 0 else "positive" if x > 0 else "zero", values)
 
for c in converted:
    print(c)

Opět se podívejme na výsledek získaný po spuštění tohoto skriptu:

negative
negative
negative
negative
negative
negative
negative
negative
negative
negative
zero
positive
positive
positive
positive
positive
positive
positive
positive
positive
positive
Poznámka: zdrojový kód tohoto demonstračního příkladu naleznete na adrese https://github.com/tisnik/most-popular-python-libs/blob/master/functools/map4.py.

12. Funkce vyššího řádu filter

Kromě funkce vyššího řádu map, s níž jsme se seznámili v předchozím textu, mají vývojáři k dispozici i další poměrně užitečnou funkci, která se příhodně jmenuje filter. I filter je funkcí vyššího řádu, kde funkce předaná uživatelem (ať už pojmenovaná či anonymní) určuje svojí návratovou hodnotou, zda daný prvek z původního seznamu (n-tice, sekvence atd.) má být přenesen do seznamu vytvářeného. Jedná se tedy o obdobu klauzule WHERE v programovacím jazyce SQL. Mimochodem – funkce předávaná do filter se nazývá predikát, protože rozhodnutí, zda se prvek ze vstupu má použít i na výstupu, se provádí na základě pravdivostní hodnoty vrácené predikátem. A konkrétně v Pythonu se zde aplikují všechna pravidla o tom, jaké hodnoty jsou považovány za pravdu a jaké za nepravdu. Všechny hodnoty kromě hodnot zmíněných níže jsou považovány za pravdu:

  • False
  • None
  • 0 (long)
  • 0.0 (double)
  • 0j (complex
  • [] (prázdný seznam)
  • () (prázdná n-tice)
  • {} (prázdný slovník)
  • set() (prázdná množina)
  • "" (prázdný řetězec)
  • range(0) (prázdná sekvence)

13. Ukázky použití funkce filter

V prvním demonstračním příkladu založeném na funkci vyššího řádu filter získáme ze seznamu slov ta slova, jejichž délka je menší než čtyři znaky. A navíc provedeme ještě jednu filtraci, tentokrát naopak pro slova s délkou větší nebo rovnou čtyřem. Výsledkem filtrace je opět (podobně jako u funkce map) sekvence, kterou můžeme pro účely tisku převést na seznam:

message = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
words = message.split()
 
filtered = filter(lambda word: len(word) > 4, words)
print(list(filtered))
 
filtered = filter(lambda word: len(word) <= 4, words)
print(list(filtered))

Výsledkem činnosti tohoto skriptu jsou dva seznamy – první s dlouhými slovy, druhý se slovy krátkými (povšimněte si problému s počítáním čárky do délky slova):

['Lorem', 'ipsum', 'dolor', 'amet,', 'consectetur', 'adipiscing', 'elit,', 'eiusmod', 'tempor', 'incididunt', 'labore', 'dolore', 'magna', 'aliqua']
['sit', 'sed', 'do', 'ut', 'et']
Poznámka: zdrojový kód tohoto demonstračního příkladu naleznete na adrese https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/filter1.py.

Ve druhém demonstračním příkladu rozdělíme sekvenci numerických hodnot 0 až 10 (včetně) do sekvence obsahující lichá čísla a do sekvence obsahující naopak čísla sudá. Predikátem budou v tomto případě uživatelsky definované funkce:

def odd(value):
    return value % 2 == 1
 
 
def even(value):
    return not odd(value)
 
 
data = range(0, 11)
 
filtered = filter(odd, data)
print(list(filtered))
 
filtered = filter(even, data)
print(list(filtered))

Výsledek zobrazený po spuštění tohoto skriptu by měl vypadat takto:

[1, 3, 5, 7, 9]
[0, 2, 4, 6, 8, 10]
Poznámka: zdrojový kód tohoto demonstračního příkladu naleznete na adrese https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/filter2.py.

A konečně – předchozí skript můžeme v případě potřeby přepsat takovým způsobem, aby se namísto uživatelských pojmenovaných funkcí s jediným příkazem ve svém těle použily lambda výrazy:

data = range(0, 11)
 
filtered = filter(lambda value : value %2 == 1, data)
print(list(filtered))
 
filtered = filter(lambda value : value %2 == 0, data)
print(list(filtered))

Výsledek by měl být stejný, jako tomu je u předchozího příkladu:

[1, 3, 5, 7, 9]
[0, 2, 4, 6, 8, 10]
Poznámka: zdrojový kód tohoto demonstračního příkladu naleznete na adrese https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/filter3.py.

14. Generátorové notace vs. funkce vyššího řádu mapfilter

Funkce map a filter, které jsme si popsali v předchozích kapitolách, pochází z klasických funkcionálních jazyků a mají pochopitelně i své využití v Pythonu. Ovšem Python programátorům nabízí i alternativní způsob zápisu algoritmů založených na operacích map a filter, tedy na aplikaci nějaké transformace na všechny prvky sekvence, popř. na výběr prvků ze sekvence na základě nějakého predikátu. Tento alternativní způsob zápisu je pokládán za více idiomatický a nazývá se generátorová notace, což je poněkud nepřesně přeložený anglický termín (list/tuple) comprehension.

Základní způsob zápisu generátorové notace seznamu vypadá takto:

[item * 2 for item in range(10)]
 
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

Navíc můžeme přidat i podmínku a tím pádem realizovat filter:

[item * 2 for item in range(10) if item % 3 == 0]
 
[0, 6, 12, 18]
Poznámka: podmínka se vztahuje nikoli k vypočteným výsledkům, ale ke „vstupnímu“ prvku item.

Jak uvidíme příště, je tento způsob zápisu sice možná na první způsob elegantní, ale má poněkud omezené vyjadřovací schopnosti.

15. Náhrada funkce map za generátorovou notaci

Pro zajímavost se nyní podívejme na způsob přepisu demonstračních příkladů z desáté a jedenácté kapitoly tak, aby se namísto funkce vyššího řádu map použila generátorová notace (seznamu). Výsledkem budou v tomto případě nikoli sekvence, ale přímo seznamy, které lze tisknout:

message = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
words = message.split()
 
lengths = [len(word) for word in words]
print(lengths)
values = range(-10, 11)
 
converted = [abs(value) for value in values]
print(converted)
def sign(value):
    if value < 0:
        return "negative"
    elif value > 0:
        return "positive"
    else:
        return "zero"
 
 
values = range(-10, 11)
 
converted = [sign(value) for value in values]
 
for c in converted:
    print(c)
values = range(-10, 11)
 
converted = ["negative" if x < 0 else "positive" if x > 0 else "zero" for x in values]
 
for c in converted:
    print(c)
Poznámka: zápis je sice (alespoň oficiálně) více idiomatický, ovšem vyžaduje určitý trénink pro rozpoznání použitého vzoru, zatímco zápisem map se daný algoritmus přímo pojmenuje.

16. Náhrada funkce filter za generátorovou notaci

Ve třinácté kapitole jsme si ukázali několik demonstračních příkladů, v nichž se používala funkce vyššího řádu filter. I tuto funkci můžeme nahradit za zápis založený na generátorové notaci. Pokusme se tedy přepsat všechny tři příklady ze třinácté kapitoly do idiomatického Pythonního kódu:

message = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
words = message.split()
 
filtered = [word for word in words if len(word) > 4]
print(list(filtered))
 
filtered = [word for word in words if len(word) <= 4]
print(list(filtered))
def odd(value):
    return value % 2 == 1
 
 
def even(value):
    return not odd(value)
 
 
data = range(0, 11)
 
filtered = [value for value in data if odd(value)]
print(filtered)
 
filtered = [value for value in data if even(value)]
print(filtered)
data = range(0, 11)
 
filtered = [value for value in data if value %2 == 1]
print(filtered)
 
filtered = [value for value in data if value %2 == 0]
print(filtered)

17. Funkce vyššího řádu reduce

Ve většině programovacích jazyků inspirovaných funkcionálním programováním se velmi často setkáme i s funkcí nazvanou reduce nebo fold, popř. s různými alternativami s podobnými operacemi. Základní operací tohoto typu je funkce vyššího řádu nazvaná reduce, která postupně zpracovává všechny prvky seznamu, n-tice, sekvence nebo slovníku zleva doprava a aplikuje na každý prvek a akumulovanou hodnotu nějakou funkci (a právě to tedy mj. znamená, že reduce je funkcí vyššího řádu). Výsledkem je v každé iteraci nová hodnota akumulátoru a po projití celé vstupní sekvence je výsledná hodnota uložená v akumulátoru současně i návratovou hodnotou funkce reduce. Alternativně je možné specifikovat počáteční hodnotu akumulátoru (ne ve všech implementacích, ovšem Pythonní implementace reduce do této skupiny patří. Tuto funkci můžeme využít například při výpočtu faktoriálu, protože při výpočtu faktoriálu nějakého n postačuje pomocí range vytvořit pole o n prvcích a posléze jeho prvky postupně pronásobit.

Poznámka: na rozdíl od výše zmíněných funkcí map a filter není funkce reduce umístěna ve výchozím jmenném prostoru. Nalezneme ji ve standardním balíčku functools. Důvody jsou zmíněny například zde (osobně ovšem těm důvodům moc nerozumím, ale to je osobní názor založený na odlišném chápání role jmen a vzorů či idiomů v programovacích jazycích).

Podívejme se nyní na způsob použití funkce vyššího řádu reduce. Následující skript po svém spuštění provede tento výpočet:

(((((((((1*2)*3)*4)*5)*6)*7)*8)*9)*10)

To znamená, že se provede výpočet faktoriálu pro n=10:

from functools import reduce
 
 
def multiply(x, y):
    return x * y
 
 
x = range(1, 11)
print(x)
 
y = reduce(multiply, x)
print(y)

Výsledek získaný po spuštění:

range(1, 11)
3628800
Poznámka: zdrojový kód tohoto demonstračního příkladu naleznete na adrese https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/reduce1.py.

Samozřejmě je možné celý výpočet přepsat takovým způsobem, aby se namísto pojmenované funkce multiply použil kratší lambda výraz:

from functools import reduce
 
 
x = range(1, 11)
print(x)
 
y = reduce(lambda a, b: a*b, x)
print(y)

Výsledek bude shodný:

range(1, 11)
3628800
Poznámka: zdrojový kód tohoto demonstračního příkladu naleznete na adrese https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/reduce2.py.

Relativně snadno lze předchozí skript upravit do takové podoby, aby se vypočetla tabulka faktoriálů pro n=0 až n=10. Třetím nepovinným parametrem funkce reduce je počáteční hodnota akumulátoru. Výsledná podoba skriptu bude vypadat následovně:

from functools import reduce
 
 
def factorial(n):
    return reduce(lambda a, b: a*b, range(1, n+1), 1)
 
 
for n in range(0, 11):
    print(n, factorial(n))

Výsledky:

0 1
1 1
2 2
3 6
4 24
5 120
6 720
7 5040
8 40320
9 362880
10 3628800
Poznámka: zdrojový kód tohoto demonstračního příkladu naleznete na adrese https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/reduce3.py.

Ovšem i samotnou funkci factorial lze zapsal lambda výrazem (což již ovšem nemusí být příliš čitelné a ani to není v Pythonu idiomatické):

from functools import reduce
 
 
factorial = lambda n: reduce(lambda a, b: a*b, range(1, n+1), 1)
 
for n in range(0, 11):
    print(n, factorial(n))
Poznámka: zdrojový kód tohoto demonstračního příkladu naleznete na adrese https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/reduce4.py.

A konečně si ukažme „funkcionální“ variantu předchozího skriptu, v níž se nevyskytují programové smyčky a v níž se výsledek uloží do sekvence (ovšem tento zápis nemusí být zcela čitelný)

from functools import reduce
 
 
n = range(0, 11)
 
factorials = map(lambda n: reduce(lambda a, b: a*b, range(1, n+1), 1), n)
 
print(list(factorials))

Výsledkem je sekvence převedená na seznam:

ict ve školství 24

[1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800]
Poznámka: zdrojový kód tohoto demonstračního příkladu naleznete na adrese https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/reduce5.py.

18. Obsah navazujícího článku

V navazujícím článku se zaměříme na ty nástroje dostupné ve standardním balíčku functools, které jsou určeny pro pokročilejší manipulace s funkcemi. Bude se jednat zejména o takzvaný currying a taktéž o možnost zapamatování návratových hodnot funkcí v cache (což je pro čisté funkce samozřejmě možné). Zmíníme se i o takzvaném point-free programování, jímž jsme se obecně zabývali v článku Programovací technika nazvaná tacit programming.

19. Repositář s demonstračními příklady

Všechny Pythonovské skripty, které jsme si v dnešním článku ukázali, naleznete na adrese https://github.com/tisnik/most-popular-python-libs. Následují odkazy na jednotlivé příklady (pro jejich spuštění je nutné mít nainstalovánu některou z podporovaných verzí Pythonu 3, žádné další balíčky nejsou zapotřebí):

# Příklad Stručný popis Adresa
1 binary_operator.py ukázka funkce vyššího řádu, která jako parametr akceptuje jinou funkci https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/binary_operator.py
2 get_operator1.py ukázka funkce vyššího řádu, která vrací jinou funkci https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/get_operator1.py
3 get_operator2.py ukázka funkce vyššího řádu, která vrací jinou funkci https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/get_operator2.py
4 standard_operators.py použití standardních operátorů přepsaných do formy funkce https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/standard_operators.py
       
5 binary_operator_types.py varianta příkladu binary_operator.py s plnými typovými deklaracemi https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/binary_operator_types.py
6 get_operator_types.py varianta příkladu get_operator2.py s plnými typovými deklaracemi https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/get_operator_types.py
       
7 map1.py příklad použití funkce map: výpočet délky všech slov v textu https://github.com/tisnik/most-popular-python-libs/blob/master/functools/map1.py
8 map2.py příklad použití funkce map: výpočet absolutní hodnoty všech členů posloupnosti https://github.com/tisnik/most-popular-python-libs/blob/master/functools/map2.py
9 map3.py příklad použití funkce map: aplikace vlastní pojmenované funkce https://github.com/tisnik/most-popular-python-libs/blob/master/functools/map3.py
10 map4.py příklad použití funkce map: aplikace vlastního lambda výrazu https://github.com/tisnik/most-popular-python-libs/blob/master/functools/map4.py
       
11 map_list_comprehension1.py přepis skriptu map1.py tak, aby se použila generátorová notace https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/map_list_comprehension.py
12 map_list_comprehension2.py přepis skriptu map2.py tak, aby se použila generátorová notace https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/map_list_comprehension.py
13 map_list_comprehension3.py přepis skriptu map3.py tak, aby se použila generátorová notace https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/map_list_comprehension.py
14 map_list_comprehension4.py přepis skriptu map4.py tak, aby se použila generátorová notace https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/map_list_comprehension.py
       
15 filter1.py filtrace dat na základě délky řetězce https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/filter1.py
16 filter2.py filtrace numerických dat podle toho, zda se jedná o sudá či lichá čísla https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/filter2.py
17 filter3.py přepis předchozího příkladu s využitím lambda výrazu https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/filter3.py
       
18 filter_list_comprehension1.py přepis skriptu filter_list_comprehension1.py tak, aby se použila generátorová notace https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/filter_list_comprehensi­on1.py
19 filter_list_comprehension2.py přepis skriptu filter_list_comprehension2.py tak, aby se použila generátorová notace https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/filter_list_comprehensi­on2.py
20 filter_list_comprehension3.py přepis skriptu filter_list_comprehension3.py tak, aby se použila generátorová notace https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/filter_list_comprehensi­on3.py
       
21 reduce1.py výpočet faktoriálu s využitím funkce vyššího řádu reduce https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/reduce1.py
22 reduce2.py přepis předchozího příkladu s využitím lambda výrazu https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/reduce2.py
23 reduce3.py tisk tabulky faktoriálů https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/reduce3.py
24 reduce4.py přepis předchozího příkladu s využitím lambda výrazu https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/reduce4.py
25 reduce5.py přepis předchozího příkladu s využitím generátorové notace https://github.com/tisnik/most-popular-python-libs/blob/master/functool­s/reduce5.py

20. Odkazy na Internetu

  1. functools — Higher-order functions and operations on callable objects
    https://docs.python.org/3/li­brary/functools.html
  2. Functional Programming HOWTO
    https://docs.python.org/3/how­to/functional.html
  3. Functional Programming in Python: When and How to Use It
    https://realpython.com/python-functional-programming/
  4. Functional Programming With Python
    https://realpython.com/learning-paths/functional-programming/
  5. Awesome Functional Python
    https://github.com/sfermigier/awesome-functional-python
  6. Coconut: funkcionální jazyk s pattern matchingem kompatibilní s Pythonem
    https://www.root.cz/clanky/coconut-funkcionalni-jazyk-s-pattern-matchingem-kompatibilni-s-pythonem/
  7. A HITCHHIKER'S GUIDE TO functools
    https://ep2021.europython­.eu/media/conference/slides/a-hitchhikers-guide-to-functools.pdf
  8. Coconut aneb funkcionální nadstavba nad Pythonem (2.část)
    https://www.root.cz/clanky/coconut-aneb-funkcionalni-nadstavba-nad-pythonem-2-cast/
  9. Knihovny pro zpracování posloupností (sekvencí) v Pythonu
    https://www.root.cz/clanky/knihovny-pro-zpracovani-posloupnosti-sekvenci-v-pythonu/
  10. clj – repositář s knihovnou
    https://github.com/bfontaine/clj
  11. clj 0.1.0 – stránka na PyPi
    https://pypi.python.org/py­pi/clj/0.1.0
  12. Coconut: Simple, elegant, Pythonic functional programming
    http://coconut-lang.org/
  13. coconut (Python package index)
    https://pypi.python.org/pypi/coconut/
  14. Coconut Tutorial
    http://coconut.readthedoc­s.io/en/master/HELP.html
  15. Coconut FAQ
    http://coconut.readthedoc­s.io/en/master/FAQ.html
  16. Coconut Documentation
    http://coconut.readthedoc­s.io/en/master/DOCS.html
  17. Coconut na Redditu
    https://www.reddit.com/r/Pyt­hon/comments/4owzu7/coconut_fun­ctional_programming_in_pyt­hon/
  18. Repositář na GitHubu
    https://github.com/evhub/coconut
  19. Object-Oriented Programming — The Trillion Dollar Disaster
    https://betterprogramming.pub/object-oriented-programming-the-trillion-dollar-disaster-92a4b666c7c7
  20. Goodbye, Object Oriented Programming
    https://cscalfani.medium.com/goodbye-object-oriented-programming-a59cda4c0e53
  21. So You Want to be a Functional Programmer (Part 1)
    https://cscalfani.medium.com/so-you-want-to-be-a-functional-programmer-part-1–1f15e387e536
  22. So You Want to be a Functional Programmer (Part 2)
    https://cscalfani.medium.com/so-you-want-to-be-a-functional-programmer-part-2–7005682cec4a
  23. So You Want to be a Functional Programmer (Part 3)
    https://cscalfani.medium.com/so-you-want-to-be-a-functional-programmer-part-3–1b0fd14eb1a7
  24. So You Want to be a Functional Programmer (Part 4)
    https://cscalfani.medium.com/so-you-want-to-be-a-functional-programmer-part-4–18fbe3ea9e49
  25. So You Want to be a Functional Programmer (Part 5)
    https://cscalfani.medium.com/so-you-want-to-be-a-functional-programmer-part-5-c70adc9cf56a
  26. So You Want to be a Functional Programmer (Part 6)
    https://cscalfani.medium.com/so-you-want-to-be-a-functional-programmer-part-6-db502830403
  27. Why Programmers Need Limits
    https://cscalfani.medium.com/why-programmers-need-limits-3d96e1a0a6db
  28. Infographic showing code complexity vs developer experience
    https://twitter.com/rossi­pedia/status/1580639227313676288
  29. Python's reduce(): From Functional to Pythonic Style
    https://realpython.com/python-reduce-function/
  30. What is the problem with reduce()?
    https://stackoverflow.com/qu­estions/181543/what-is-the-problem-with-reduce
  31. The fate of reduce() in Python 3000
    https://www.artima.com/we­blogs/viewpost.jsp?thread=98196
  32. Reading 16: Map, Filter, Reduce
    http://web.mit.edu/6.031/www/sp22/clas­ses/16-map-filter-reduce/

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.