Obsah
1. Podpora funkcionálního programování v Pythonu a knihovna functools (2. část)
2. Funkce jakožto plnohodnotný datový typ
3. Další nutný důsledek – uzávěry
5. Nekorektní implementace čítače založeného na uzávěru
6. Korektní implementace čítače – přístup k nelokální proměnné
7. Nelokální funkce a uzávěry (umělý příklad)
8. Curryfikace (currying) a částečně vyhodnocené funkce
9. Funkce partial z balíčku functools
10. Příklad základního použití funkce partial
11. Transformace funkce se třemi parametry s využitím partial
12. Transformace funkce s dosazením většího množství parametrů s využitím partial
13. Několikanásobná transformace původní funkce na několik nových funkcí
14. Postupná transformace již ztransformovaných funkcí
15. Typ originální funkce i funkcí získaných s využitím transformace pomocí partial
16. Jméno funkce, poziční argumenty funkce a pojmenované argumenty funkce
17. Transformace partial a pojmenované argumenty původní funkce
18. Podrobnější informace o transformovaných funkcích s pojmenovanými argumenty
19. Repositář s demonstračními příklady
1. Podpora funkcionálního programování v Pythonu a knihovna functools (2. část)
Na úvodní článek o podpoře funkcionálního programování v jazyku Python dnes navážeme. Již minule jsme se zmínili o existenci standardní knihovny nazvané functools, která vývojářům nabízí některé funkcionální techniky. Konkrétně jsme se seznámili s funkcí vyššího řádu reduce, která je zde definována (což je ostatně zajímavé, protože její „sesterské“ funkce map a filter jsou umístěny ve výchozím jmenném prostoru Pythonu a není je tedy nutné importovat – jednoduše se dají přímo zavolat).
Dnes se seznámíme s dalšími funkcemi, které v balíčku functools nalezneme. Ostatně přímo z interaktivního shellu jazyka Python (tedy z REPLu) lze snadno získat jména veřejných symbolů, které v tomto balíčku existují. Je to snadné (a pro zajímavost zde použijeme generátorovou notaci):
import functools print("\n".join(name for name in dir(functools) if name[0]!="_"))
Výsledek by mohl vypadat následovně:
RLock WRAPPER_ASSIGNMENTS WRAPPER_UPDATES cached_property cmp_to_key get_cache_token lru_cache namedtuple partial partialmethod recursive_repr reduce singledispatch singledispatchmethod total_ordering update_wrapper wraps
Symbol | Verze Pythonu |
---|---|
wraps, update_wrapper, partial | 2.5 |
reduce | 3.0 |
total_ordering, cmp_to_key | 3.2 |
partialmethod, singledispatch | 3.4 |
cached_property, singledispatchmethod | 3.8 |
cache | 3.9 |
2. Funkce jakožto plnohodnotný datový typ
Již několikrát jsme si v předchozím článku řekli, že ve funkcionálních programovacích jazycích jsou funkce plnohodnotnými datovými typy. Stejně je tomu tak i v případě Pythonu. Ovšem co toto tvrzení znamená v praxi? V případě Pythonu poměrně velké množství vlastností, které z tohoto tvrzení přímo či nepřímo vycházejí. Pokusme se vyjmenovat alespoň ty nejdůležitější vlastnosti:
- Funkce je možné mít přístupné přes globální symbol (v daném jmenném prostoru). To je zcela jistě nejznámější způsob definice (pojmenovaných) funkcí a v Pythonu pro tento účel existuje vyhrazené slovo def (některé funkcionální jazyky ovšem speciální klíčové slovo nepotřebují).
- Funkce je ovšem možné deklarovat i lokálně, tj. v nějakém bloku. Viditelnost takové funkce se řídí stejnými pravidly, jako viditelnost jakékoli jiné hodnoty.
- Navíc je ovšem možné přistupovat k nelokální funkci (v Pythonu pro přístup, resp. modifikaci nelokálních symbolů existuje klíčové slovononlocal), což se pravděpodobně nepoužívá (alespoň jsme to nikde v praxi neviděl), ale sémantika Pythonu to umožňuje.
- Funkce může být předána jako parametr jiné funkci. To již dobře známe, protože jsme si popsali například funkce vyššího řádu map, filter a reduce, které skutečně akceptují jako svůj parametr jinou funkci.
- Funkce může být vrácena jako návratová hodnota jiné funkce. To již opět známe, protože jsme na toto téma měli několik demonstračních příkladů (výpočet výsledku na základě zvoleného operátoru atd.).
- Funkce může být uložena do atributu třídy. V OOP potom mluvíme o metodách.
- Funkce může být uložena do atributu objektu.
- Funkce může být uložena do libovolného kontejneru (n-tice, seznam, množina, slovník).
Některé funkcionální jazyky navíc umožňují další, řekněme pokročilejší, manipulace s funkcemi. Jedná se o formy transformace funkcí (což umožňují zejména jazyky postavené na LISPu), dále o vytváření nových funkcí s využitím jejich kompozice, obalení funkce nějakou formou cache a v neposlední řadě některé programovací jazyky podporují takzvaný currying, s jehož variantou pro Python se seznámíme v navazujícím textu.
3. Další nutný důsledek – uzávěry
Pojďme dále – víme, že funkce jsou plnohodnotné typy a tedy mohou být vytvořeny lokálně (tím vznikne hodnota). A v Pythonu můžeme jakoukoli hodnotu z funkce vrátit konstrukcí return. To není nic nového, takže si to můžeme vyzkoušet:
def foo(): def bar(): print("BAR") return bar x = foo() x()
Po spuštění tohoto příkladu se podle očekávání vypíše „BAR“.
BAR
Jenže ve většině programovacích jazyků, a to včetně Pythonu, platí, že z vnitřního bloku (a tedy i z lokální funkce) se můžeme odkazovat na proměnné deklarované ve vnějším bloku. A můžeme tedy psát například:
def foo(): message = "FOO-BAR" def bar(): print(message) return bar x = foo() x()
Funkce bar, která je vrácena z funkce foo, si „pamatuje“ hodnotu lokální proměnné message a dokáže ji využít (a jak uvidíme dále, tak i měnit – což je z mnoha důvodů problematické). Toto navázání funkce na své prostředí se nazývá uzávěr neboli closure.
4. Uzávěry v Pythonu
Samotné použití uzávěrů je sice v dnes již starodávném Pythonu 2.x poněkud problematické (což ostatně uvidíme v navazujícím textu), ovšem v Pythonu 3.x je již tento nedostatek související se sémantikou rozpoznání lokálních a nelokálních proměnných odstraněn a tak mají uzávěry v Pythonu prakticky stejnou vyjadřovací sílu, jako například v programovacích jazycích Lua či JavaScript; nehledě již na většinu funkcionálních jazyků, které samozřejmě práci s uzávěry ve většině případů taktéž podporují (zde byly „objeveny“). Uzávěry jsou navíc tak důležitou součástí Pythonu, že pro jejich implementaci jsou v bajtkódu Python VM rezervovány dvě instrukce nazvané LOAD_CLOSURE a MAKE_CLOSURE.
První demonstrační příklad s uzávěrem je velmi prostý, protože je zde funkce (která tvoří základ uzávěru) navázána na hodnotu parametru předaného do funkce, v níž se uzávěr vytváří. Pro lepší čitelnost je funkce tvořící základ uzávěru pojmenována, ve skutečnosti by však bylo možné v jednodušších případech použít i anonymní funkce vytvořené s využitím klíčového slova lambda (zde však programovací jazyk Python omezuje těla takových funkcí na jediný výraz, což může být někdy příliš striktní, ostatně právě proto si ukazujeme použití vnitřní neanonymní funkce):
def dummyAdder(delta): def add(n): return delta + n return add # # Spusteni testu. # def main(): adder1 = dummyAdder(0) adder2 = dummyAdder(42) for i in range(1,11): result1 = adder1(i) result2 = adder2(i) print("Iteration #%d" % i) print(" Adder1: %d" % result1) print(" Adder2: %d" % result2)
Po spuštění tohoto demonstračního příkladu je patrné, že se skutečně každý uzávěr navázal na jinou hodnotu parametru předaného do funkce vytvářející uzávěr:
Iteration #1 Adder1: 1 Adder2: 43 Iteration #2 Adder1: 2 Adder2: 44 Iteration #3 Adder1: 3 Adder2: 45 Iteration #4 Adder1: 4 Adder2: 46 Iteration #5 Adder1: 5 Adder2: 47 Iteration #6 Adder1: 6 Adder2: 48 Iteration #7 Adder1: 7 Adder2: 49 Iteration #8 Adder1: 8 Adder2: 50 Iteration #9 Adder1: 9 Adder2: 51 Iteration #10 Adder1: 10 Adder2: 52
5. Nekorektní implementace čítače založeného na uzávěru
Pokusme se nyní uzávěr využít pro implementaci čítače, tedy funkce, která po svém zavolání vrátí celé číslo, které se bude s každým voláním postupně zvyšovat. Bude se tedy jednat o funkci se stavem:
def createCounter(): counter = 0 def next(): counter += 1 return counter return next # # Spusteni testu. # def main(): counter1 = createCounter() counter2 = createCounter() for i in range(1,11): result1 = counter1() result2 = counter2() print("Iteration #%d" % i) print(" Counter1: %d" % result1) print(" Counter2: %d" % result2)
Ve skutečnosti však takto naprogramovaný uzávěr nebude funkční, protože k vázané proměnné counter sice může uzávěr přistupovat při čtení, ale nikoli už při zápisu (modifikaci). Proč dojde k chybě lze zjednodušeně řečeno vysvětlit tak, že interpret Pythonu musí mít informaci o tom, že proměnná counter není interní proměnnou uzávěru, ale vázanou (tedy nelokální) proměnnou. Z tohoto důvodu při spuštění tohoto příkladu dojde k běhové výjimce. Proto je zapotřebí dávat pozor na to, že ne všechny uzávěry implementované v programovacích jazycích jakými jsou Lua či JavaScript lze bez problémů (resp. bez přemýšlení) přímo přepsat do Pythonu:
Traceback (most recent call last): File "counter_clojure_1.py", line 24, in main() File "counter_clojure_1.py", line 17, in main result1 = counter1() File "counter_clojure_1.py", line 4, in next counter += 1 UnboundLocalError: local variable 'counter' referenced before assignment
6. Korektní implementace čítače – přístup k nelokální proměnné
Aby bylo možné vytvářet plnohodnotné uzávěry s modifikovatelným prostředím (environment) i v Pythonu, bylo do verze 3.x přidáno nové klíčové slovo nonlocal. Tímto klíčovým slovem je možné ve vnitřní funkci – tedy ve vlastním „jádru“ uzávěru – označit proměnnou, která nemá být chápána jako proměnná lokální, ale ke které chceme přistupovat. Ovšem ve skutečnosti můžeme uzávěr implementující čítač vytvořit i v Pythonu 2, a to pomocí malého triku: namísto skalární (celočíselné) proměnné se použije jednorozměrný seznam. Zde již interpret nebude mít problém s rozeznáním lokální proměnné od proměnné vázané, neboť význam řádků counter += 1 a counter[0] += 1 je sémanticky odlišný (interpret si je ve druhém případě jistý, že se nejedná o deklaraci nové lokální proměnné):
# # Jednoduchy uzaver v Pythonu. # def createCounter(): counter = [0] def next(): counter[0] += 1 return counter[0] return next # # Spusteni testu. # def main(): counter1 = createCounter() counter2 = createCounter() for i in range(1,11): result1 = counter1() result2 = counter2() print("Iteration #%d" % i) print(" Counter1: %d" % result1) print(" Counter2: %d" % result2) # # Ukazka disasembleru. # (prekladu funkci do bajtkodu Python VM). # def disassemble(): from dis import dis print("\ncreateCounter():") dis(createCounter) print("\nmain():") dis(main) main()
Z následujícího výpisu je patrné, že tento demonstrační příklad skutečně funguje, a to i v Pythonu 2:
Iteration #1 Counter1: 1 Counter2: 1 Iteration #2 Counter1: 2 Counter2: 2 Iteration #3 Counter1: 3 Counter2: 3 Iteration #4 Counter1: 4 Counter2: 4 Iteration #5 Counter1: 5 Counter2: 5 Iteration #6 Counter1: 6 Counter2: 6 Iteration #7 Counter1: 7 Counter2: 7 Iteration #8 Counter1: 8 Counter2: 8 Iteration #9 Counter1: 9 Counter2: 9 Iteration #10 Counter1: 10 Counter2: 10
A pro úplnost se podívejme na způsob použití již zmíněného klíčového slova nonlocal:
def createCounter(): counter = 0 def next(): nonlocal counter counter += 1 return counter return next # # Spusteni testu. # def main(): counter1 = createCounter() counter2 = createCounter() for i in range(1,11): result1 = counter1() result2 = counter2() print("Iteration #%d" % i) print(" Counter1: %d" % result1) print(" Counter2: %d" % result2) main()
Výsledek je totožný:
Iteration #1 Counter1: 1 Counter2: 1 Iteration #2 Counter1: 2 Counter2: 2 Iteration #3 Counter1: 3 Counter2: 3 Iteration #4 Counter1: 4 Counter2: 4 Iteration #5 Counter1: 5 Counter2: 5 Iteration #6 Counter1: 6 Counter2: 6 Iteration #7 Counter1: 7 Counter2: 7 Iteration #8 Counter1: 8 Counter2: 8 Iteration #9 Counter1: 9 Counter2: 9 Iteration #10 Counter1: 10 Counter2: 10
7. Nelokální funkce a uzávěry (umělý příklad)
V předchozích čtyřech kapitolách jsme si vypsali několik vlastností programovacího jazyka Python, které souvisí s funkcemi. Mnohé z těchto vlastností si můžeme ukázat na zcela umělém demonstračním příkladu, který je založen na použití uzávěrů (closure), lokálně definovaných funkcí i na možnosti vrácení funkce z jiné funkce s využitím návratové hodnoty (tedy tak, jak bychom to udělali s jakoukoli jinou návratovou hodnotou). Nejdříve se podívejme na zdrojový kód tohoto příkladu (opět připomínám – jedná se o zcela umělou konstrukci, kterou s velkou pravděpodobností v takové podobě nikdy nepoužijete):
def foo(): def bar(): print("original BAR") def other_bar(): print("modified BAR") def baz(modify): nonlocal bar if modify: bar = other_bar return bar return baz x = foo() print(x) x(False)() x(True)()
Povšimněte si, že hodnoty uložené do symbolů bar, other_bar a baz nejsou zapomenuty při odchodu z funkce foo, protože celé prostředí (environment) může být použito později. Jedná se tedy o uzávěr, tedy o konstrukci, s níž jsme se seznámili v předchozí kapitole.
Pojďme si nyní otestovat, co se stane po spuštění tohoto skriptu v interpretru Pythonu:
<function foo.<locals>.baz at 0x7f56cecf7950> original BAR modified BAR
Jak je z předchozích tří vypsaných řádků patrné, došlo ke třem operacím:
- Zavoláním funkce foo se lokálně vytvořily tři funkce bar, other_bar a baz. Došlo k vrácení funkce baz.
- Zavoláním funkce x(False) se ve skutečnosti zavolala funkce baz a její návratovou hodnotou je lokální funkce bar, která byla zavolána a vypsala „original BAR“.
- Zavoláním funkce x(True) se ve skutečnosti zavolala funkce baz, která změnila nelokální hodnotu uloženou do bar a její návratovou hodnotou je lokální funkce other_bar, která byla zavolána a vypsala „modified BAR“.
8. Curryfikace (currying) a částečně vyhodnocené funkce
„Typically, developers learn new languages by applying what they know about existing languages. But learning a new paradigm is difficult – you must learn to see different solutions to familiar problems.“
Ve druhé části dnešního článku si ukážeme, jakým způsobem se v programovacím jazyku Python provádí takzvaná curryfikace (anglicky currying). Pod tímto termínem se v teorii programovacích jazyků (ovšem i obecně v matematice) označuje proces, jímž se transformuje funkce, která má více než jeden parametr, do řady vložených funkcí, přičemž každá z nich má jen jediný parametr (jen na okraj – čistou funkci bez parametrů lze nahradit konstantou). Curryfikaci si můžeme představit jako postupnou transformaci funkce s n parametry na jinak zkonstruovanou funkci s n-1 parametry atd. až rekurzivně dojdeme k funkci s jediným parametrem:
x = f(a,b,c) → h = g(a) i = h(b) x = i(c)
Nebo na jediném řádku:
x = f(a,b,c) → g(a)(b)(c)
To zní sice velmi složitě, ale v praxi je (například v jazyku ML, ale i dalších jazycích) proces curryfikace realizován z pohledu programátora automaticky již samotným zápisem funkce s větším množstvím parametrů. To nám umožňuje realizovat částečné vyhodnocení funkce (partial application), konkrétně zavoláním nějaké funkce (například funkce akceptující dva parametry) ve skutečnosti pouze s jediným parametrem. Jenže – co má být výsledkem volání takové funkce? Určitě ne výsledek implementované operace, protože nám chybí jeden parametr pro to, aby byl výsledek vypočten a vrácen volajícím kódu. Ovšem můžeme provést částečný výpočet dosazením (jediného) předaného parametru a výsledek – tento částečný výpočet – vrátit. Výsledkem je tedy obecně částečně aplikovaná funkce (tedy například funkce, které byly v předchozím příkladu označeny symboly g a h). Jedná se o jeden ze způsobů, jak programově (za běhu aplikace) vytvářet nové funkce.
9. Funkce partial z balíčku functools
V programovacím jazyku Python je částečné vyhodnocení funkce realizováno funkcí nazvanou partial, kterou nalezneme v balíčku functools. Funkce partial je funkcí vyššího řádu, protože se jí předává původní funkce (například definovaná uživatelem) a některé parametry této funkce. Výsledkem bude nová funkce, ve které jsou již tyto parametry „zapamatovány“ a tudíž se jí už nemusí a vlastně ani nemohou předávat. To znamená, že z nějaké více univerzální funkce touto transformací vytvoříme specializovanější funkci s menším množstvím parametrů. Interně bude funkce partial vypadat přibližně takto (ve skutečnosti je to ovšem nepatrně složitější):
def partial(func, /, *args, **keywords): def newfunc(*fargs, **fkeywords): newkeywords = {**keywords, **fkeywords} return func(*args, *fargs, **newkeywords) newfunc.func = func newfunc.args = args newfunc.keywords = keywords return newfunc
10. Příklad základního použití funkce partial
Funkce partial sice může zpočátku vypadat poněkud komplikovaně, ovšem práce s ní je relativně přímočará. Abychom pochopili všechny vlastnosti partial, ukážeme si několik demonstračních příkladů, které vlastnosti partial ukazují v různých podobách.
V prvním demonstračním příkladu můžeme vidět definici funkce nazvané mul, která akceptuje dva parametry. Tyto parametry jsou vynásobeny a výsledek tohoto součinu je současně i návratovou hodnotou funkce mul. S využitím partial se tato funkce se dvěma parametry transformuje na novou funkci pojmenovanou doubler, která ovšem již akceptuje pouze jediný parametr y, neboť původní první parametr x byl nahrazen za dvojku. Následně se již funkce doubler volá s jediným parametrem:
from functools import partial def mul(x, y): return x * y print(mul(6, 7)) print() doubler = partial(mul, 2) for i in range(11): print(i, doubler(i))
Tento příklad si samozřejmě můžeme velmi snadno otestovat:
42 0 0 1 2 2 4 3 6 4 8 5 10 6 12 7 14 8 16 9 18 10 20
Z tohoto výpisu je patrné, že funkce doubler skutečně pracuje podle předpokladů.
11. Transformace funkce se třemi parametry s využitím partial
Transformace funkcí s využitím partial ve skutečnosti není omezeno pouze na funkce se dvěma parametry. Můžeme se pokusit například o transformaci funkce se třemi parametry. V následujícím demonstračním příkladu se pokoušíme o transformaci funkce mul se třemi parametry x, y a z tak, že za první parametr x se v nové funkci doplní hodnota 2:
from functools import partial def mul(x, y, z): return x * y * z print(mul(2, 3, 7)) print() doubler = partial(mul, 2) for i in range(11): print(i, doubler(i))
Ve skutečnosti funkci double voláme špatně – s jediným parametrem y, i když se očekávají dva parametry y a z. Na tento problém nás pochopitelně upozorní interpret programovacího jazyka Python:
42 Traceback (most recent call last): File "partial_2.py", line 16, in <module> print(i, doubler(i)) TypeError: mul() missing 1 required positional argument: 'z'
Korektní způsob použití by mohl vypadat například následovně – funkci doubler nyní namísto jediného parametru předáváme dva parametry, které jsou vynásobeny mezi sebou a navíc je výsledek zdvojnásoben (resp. přesněji řečeno je pořadí operací odlišné, to nás však nyní nemusí příliš trápit):
from functools import partial def mul(x, y, z): return x * y * z print(mul(2, 3, 7)) print() doubler = partial(mul, 2) for i in range(11): print(i, doubler(i, 10))
Výsledky získané po spuštění tohoto příkladu:
42 0 0 1 20 2 40 3 60 4 80 5 100 6 120 7 140 8 160 9 180 10 200
12. Transformace funkce s dosazením většího množství parametrů s využitím partial
Prozatím jsme si ukázali, jakým způsobem je možné transformovat funkci se dvěma či třemi (obecně tedy s n) parametry na jinou funkci s jedním či dvěma (obecně s n-1 parametry). Ve skutečnosti však můžeme s využitím partial dosadit i větší množství parametrů. Tato možnost je ukázána v následujícím demonstračním příkladu, v němž vytváříme (resp. přesněji řečeno transformujeme) funkci mul na funkci doubleDoubler, a to dosazením dvou parametrů. Navíc je v tomto demonstračním příkladu ukázáno, že původní funkce může akceptovat libovolný a předem neznámý počet parametrů (a ještě k tomu jsme se vrátili k funkci vyššího řádu reduce):
import operator from functools import partial, reduce def mul(*args): return reduce(operator.mul, args, 1) print(mul(2, 3, 7)) print() doubler = partial(mul, 2) for i in range(11): print(i, doubler(i, 10)) print() doubleDoubler = partial(mul, 2, 2) for i in range(11): print(i, doubleDoubler(i, 10))
Výsledky získané po spuštění tohoto demonstračního příkladu ukazují funkčnost celého řešení:
42 0 0 1 20 2 40 3 60 4 80 5 100 6 120 7 140 8 160 9 180 10 200 0 0 1 40 2 80 3 120 4 160 5 200 6 240 7 280 8 320 9 360 10 400
13. Několikanásobná transformace původní funkce na několik nových funkcí
Původní funkce není transformací s využitím partial nijak dotčena; z tohoto pohledu je neměnitelná (immutable). To nám umožňuje původní funkci mul (nyní upravenou do podoby akceptující čtveřici parametrů) transformovat vícekrát, pokaždé s jiným počtem doplněných parametrů:
from functools import partial def mul(x, y, z, w): return x * y * z * w f1 = mul print(f1) f2 = partial(mul, 2) print(f2) f3 = partial(mul, 2, 3) print(f3) f4 = partial(mul, 2, 3, 4) print(f4) f5 = partial(mul, 2, 3, 4, 5) print(f5) f6 = partial(mul, 2, 3, 4, 5, 6) print(f6) print() print(f1(2, 3, 4, 5)) print(f2(3, 4, 5)) print(f3(4, 5)) print(f4(5)) print(f5()) print(f6())
Tento skript po svém spuštění nejdříve vypíše sedm hodnot typu funkce, což bude fungovat podle očekávání:
<function mul at 0x7fa22791fea0> functools.partial(<function mul at 0x7fa22791fea0>, 2) functools.partial(<function mul at 0x7fa22791fea0>, 2, 3) functools.partial(<function mul at 0x7fa22791fea0>, 2, 3, 4) functools.partial(<function mul at 0x7fa22791fea0>, 2, 3, 4, 5) functools.partial(<function mul at 0x7fa22791fea0>, 2, 3, 4, 5, 6)
Následně budeme jak původní funkci, tak i funkce získané transformacemi volat. To se ovšem nepovede u funkce f6, neboť až v této chvíli Python zjistí, že původní funkci mul předáváme pět parametrů, i když se očekávají jen čtyři parametry:
120 120 120 120 120 Traceback (most recent call last): File "partial_5.py", line 33, in <module> print(f6()) TypeError: mul() takes 4 positional arguments but 5 were given
14. Postupná transformace již ztransformovaných funkcí
Interně se sice funkce získané s využitím partial poněkud liší od běžných funkcí, ovšem v naprosté většině případů s nimi můžeme pracovat stejným způsobem, jako s „normálními“ funkcemi. To mj. znamená, že takové funkce můžeme volat, popř. je opět předat do funkce vyššího řádu partial pro transformaci do podoby nové funkce. A právě tuto druhou operace si nyní ukážeme v následujícím demonstračním příkladu:
from functools import partial def mul(x, y, z, w): return x * y * z * w f1 = mul print(f1) f2 = partial(f1, 2) print(f2) f3 = partial(f2, 3) print(f3) f4 = partial(f3, 4) print(f4) f5 = partial(f4, 5) print(f5) f6 = partial(f5, 6) print(f6) print() print(f1(2, 3, 4, 5)) print(f2(3, 4, 5)) print(f3(4, 5)) print(f4(5)) print(f5()) print(f6())
Tento demonstrační příklad postupně transformuje původní funkci mul, přičemž „mezivýsledky“ transformace jsou využity jako nové funkce s menším počtem parametrů. Celé chování si můžeme velmi snadno otestovat (včetně očekávané chyby u funkce f6):
<function mul at 0x7fe108411ea0> functools.partial(<function mul at 0x7fe108411ea0>, 2) functools.partial(<function mul at 0x7fe108411ea0>, 2, 3) functools.partial(<function mul at 0x7fe108411ea0>, 2, 3, 4) functools.partial(<function mul at 0x7fe108411ea0>, 2, 3, 4, 5) functools.partial(<function mul at 0x7fe108411ea0>, 2, 3, 4, 5, 6) 120 120 120 120 120 Traceback (most recent call last): File "partial_6.py", line 33, in <module> print(f6()) TypeError: mul() takes 4 positional arguments but 5 were given
15. Typ originální funkce i funkcí získaných s využitím transformace pomocí partial
Vraťme se na chvíli k nástroji Mypy, s nímž jsme se již na stránkách Rootu setkali v následující trojici článků:
- 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/ - 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/ - 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/
Připomeňme si, že tento nástroj vývojářům umožňuje použít pseudofunkci nazvanou reveal_type pro získání typových informací o vybrané (libovolné) hodnotě, a to včetně funkcí. Takže se pokusme tento nástroj použít na funkce získané transformací původní „normální“ funkce mul:
from functools import partial def mul(x, y, z, w): return x * y * z * w f1 = mul reveal_type(f1) f2 = partial(mul, 2) reveal_type(f2) f3 = partial(mul, 2, 3) reveal_type(f3) f4 = partial(mul, 2, 3, 4) reveal_type(f4) f5 = partial(mul, 2, 3, 4, 5) reveal_type(f5) f6 = partial(mul, 2, 3, 4, 5, 6) reveal_type(f6)
Z výsledků je patrné, jak se postupně mění typ funkce (i to, že bychom měli přidat typové informace):
partial_7.py:9: note: Revealed type is "def (x: Any, y: Any, z: Any, w: Any) -> Any" partial_7.py:12: note: Revealed type is "functools.partial[Any]" partial_7.py:15: note: Revealed type is "functools.partial[Any]" partial_7.py:18: note: Revealed type is "functools.partial[Any]" partial_7.py:21: note: Revealed type is "functools.partial[Any]" partial_7.py:24: note: Revealed type is "functools.partial[Any]"
Zkusme si tedy stejný příklad, ovšem s přidanými typovými informacemi:
from functools import partial def mul(x: int, y: int, z: int, w: int) -> int: return x * y * z * w f1 = mul reveal_type(f1) f2 = partial(mul, 2) reveal_type(f2) f3 = partial(mul, 2, 3) reveal_type(f3) f4 = partial(mul, 2, 3, 4) reveal_type(f4) f5 = partial(mul, 2, 3, 4, 5) reveal_type(f5) f6 = partial(mul, 2, 3, 4, 5, 6) reveal_type(f6)
Výsledek bude podle očekávání odlišný:
partial_B.py:9: note: Revealed type is "def (x: builtins.int, y: builtins.int, z: builtins.int, w: builtins.int) -> builtins.int" partial_B.py:12: note: Revealed type is "functools.partial[builtins.int]" partial_B.py:15: note: Revealed type is "functools.partial[builtins.int]" partial_B.py:18: note: Revealed type is "functools.partial[builtins.int]" partial_B.py:21: note: Revealed type is "functools.partial[builtins.int]" partial_B.py:24: note: Revealed type is "functools.partial[builtins.int]"
16. Jméno funkce, poziční argumenty funkce a pojmenované argumenty funkce
Nové funkce získané transformací s využitím funkce vyššího řádu partial obsahují mj. i atributy nazvané func, args a keywords. První z těchto atributů obsahuje novou volatelnou funkci, druhý atribut obsahuje n-tici s již naplněnými pozičními parametry (resp. s jejich názvy) a poslední atribut pak slovník s pojmenovanými parametry, které byly taktéž naplněny (k nim se ještě vrátíme v navazující kapitole). Pojďme si tedy zkusit všechny tři zmíněné argumenty vytisknout pro všechny transformované funkce získané přes partial. Použijeme tento skript:
from functools import partial def mul(x, y, z, w): return x * y * z * w def displayInfo(name, obj): print("name: ", name) print("function: ", obj.func) print("arguments: ", obj.args) print("keywords: ", obj.keywords) print() f2 = partial(mul, 2) displayInfo("f2", f2) f3 = partial(mul, 2, 3) displayInfo("f3", f3) f4 = partial(mul, 2, 3, 4) displayInfo("f4", f4) f5 = partial(mul, 2, 3, 4, 5) displayInfo("f5", f5) f6 = partial(mul, 2, 3, 4, 5, 6) displayInfo("f6", f6)
Z vypsaných výsledků je patrné, že se postupně přidávají další a další poziční parametry, které jsou již (před)vyplněny a při volání transformované funkce se tedy znovu nesmí uvádět:
name: f2 function: <function mul at 0x7fd316e79ea0> arguments: (2,) keywords: {} name: f3 function: <function mul at 0x7fd316e79ea0> arguments: (2, 3) keywords: {} name: f4 function: <function mul at 0x7fd316e79ea0> arguments: (2, 3, 4) keywords: {} name: f5 function: <function mul at 0x7fd316e79ea0> arguments: (2, 3, 4, 5) keywords: {} name: f6 function: <function mul at 0x7fd316e79ea0> arguments: (2, 3, 4, 5, 6) keywords: {}
17. Transformace partial a pojmenované argumenty původní funkce
Funkce vyššího řádu partial lze v Pythonu (ovšem nikoli ve všech dalších jazycích!) použít i ve chvíli, kdy transformovaná funkce používá pojmenované argumenty. Tyto argumenty je nutné pojmenovat i při volání partial, což může vypadat například následovně:
from functools import partial def mul(x=1, y=1, z=1, w=1): return x * y * z * w f1 = mul print(f1()) f2 = partial(mul, x=2) print(f2()) f3 = partial(mul, y=2) print(f3()) f4 = partial(mul, y=2, z=2) print(f4()) f5 = partial(mul, x=2, y=2, z=2) print(f5()) f6 = partial(mul, x=2, y=2, z=2, w=2) print(f6())
A takto bude vypadat výsledek získaný po spuštění tohoto skriptu:
1 2 2 4 8 16
18. Podrobnější informace o transformovaných funkcích s pojmenovanými argumenty
Opět se podívejme na to, jaké podrobnější informace lze získat o transformovaných funkcích s pojmenovanými argumenty. Pro tento účel si opět zobrazíme atributy func, args a keywords přiřazené k funkcím vytvořeným funkcí vyššího řádu partial:
from functools import partial def mul(x=1, y=1, z=1, w=1): return x * y * z * w def displayInfo(name, obj): print("name: ", name) print("function: ", obj.func) print("arguments: ", obj.args) print("keywords: ", obj.keywords) print() #f1 = mul #displayInfo(f1()) f2 = partial(mul, x=2) displayInfo("f2", f2) f3 = partial(mul, y=2) displayInfo("f3", f3) f4 = partial(mul, y=2, z=2) displayInfo("f4", f4) f5 = partial(mul, x=2, y=2, z=2) displayInfo("f5", f5) f6 = partial(mul, x=2, y=2, z=2, w=2) displayInfo("f6", f6)
A takto bude vypadat výsledek – povšimněte si zejména atributů keywords:
name: f2 function: <function mul at 0x7effcf0ebea0> arguments: () keywords: {'x': 2} name: f3 function: <function mul at 0x7effcf0ebea0> arguments: () keywords: {'y': 2} name: f4 function: <function mul at 0x7effcf0ebea0> arguments: () keywords: {'y': 2, 'z': 2} name: f5 function: <function mul at 0x7effcf0ebea0> arguments: () keywords: {'x': 2, 'y': 2, 'z': 2} name: f6 function: <function mul at 0x7effcf0ebea0> arguments: () keywords: {'x': 2, 'y': 2, 'z': 2, 'w': 2}
19. Repositář s demonstračními příklady
Všechny Pythonovské skripty, které jsme si ukázali minule i dnes, 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í):
20. Odkazy na Internetu
- functools — Higher-order functions and operations on callable objects
https://docs.python.org/3/library/functools.html - Functional Programming HOWTO
https://docs.python.org/3/howto/functional.html - Functional Programming in Python: When and How to Use It
https://realpython.com/python-functional-programming/ - Functional Programming With Python
https://realpython.com/learning-paths/functional-programming/ - Awesome Functional Python
https://github.com/sfermigier/awesome-functional-python - Currying
https://en.wikipedia.org/wiki/Currying - Currying in Python – A Beginner’s Introduction
https://www.askpython.com/python/examples/currying-in-python - Fundamental Concepts in Programming Languages
https://en.wikipedia.org/wiki/Fundamental_Concepts_in_Programming_Languages - When should I use function currying?
https://stackoverflow.com/questions/24881604/when-should-i-use-function-currying - Toolz
https://github.com/pytoolz/toolz/tree/master - Coconut: funkcionální jazyk s pattern matchingem kompatibilní s Pythonem
https://www.root.cz/clanky/coconut-funkcionalni-jazyk-s-pattern-matchingem-kompatibilni-s-pythonem/ - A HITCHHIKER'S GUIDE TO functools
https://ep2021.europython.eu/media/conference/slides/a-hitchhikers-guide-to-functools.pdf - Coconut aneb funkcionální nadstavba nad Pythonem (2.část)
https://www.root.cz/clanky/coconut-aneb-funkcionalni-nadstavba-nad-pythonem-2-cast/ - Knihovny pro zpracování posloupností (sekvencí) v Pythonu
https://www.root.cz/clanky/knihovny-pro-zpracovani-posloupnosti-sekvenci-v-pythonu/ - clj – repositář s knihovnou
https://github.com/bfontaine/clj - clj 0.1.0 – stránka na PyPi
https://pypi.python.org/pypi/clj/0.1.0 - Coconut: Simple, elegant, Pythonic functional programming
http://coconut-lang.org/ - coconut (Python package index)
https://pypi.python.org/pypi/coconut/ - Coconut Tutorial
http://coconut.readthedocs.io/en/master/HELP.html - Coconut FAQ
http://coconut.readthedocs.io/en/master/FAQ.html - Coconut Documentation
http://coconut.readthedocs.io/en/master/DOCS.html - Coconut na Redditu
https://www.reddit.com/r/Python/comments/4owzu7/coconut_functional_programming_in_python/ - Repositář na GitHubu
https://github.com/evhub/coconut - Object-Oriented Programming — The Trillion Dollar Disaster
https://betterprogramming.pub/object-oriented-programming-the-trillion-dollar-disaster-92a4b666c7c7 - Goodbye, Object Oriented Programming
https://cscalfani.medium.com/goodbye-object-oriented-programming-a59cda4c0e53 - 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 - 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 - 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 - 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 - 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 - 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 - Why Programmers Need Limits
https://cscalfani.medium.com/why-programmers-need-limits-3d96e1a0a6db - Infographic showing code complexity vs developer experience
https://twitter.com/rossipedia/status/1580639227313676288 - Python's reduce(): From Functional to Pythonic Style
https://realpython.com/python-reduce-function/ - What is the problem with reduce()?
https://stackoverflow.com/questions/181543/what-is-the-problem-with-reduce - The fate of reduce() in Python 3000
https://www.artima.com/weblogs/viewpost.jsp?thread=98196 - Reading 16: Map, Filter, Reduce
http://web.mit.edu/6.031/www/sp22/classes/16-map-filter-reduce/ - Currying
https://sw-samuraj.cz/2011/02/currying/ - Používání funkcí v F#
https://docs.microsoft.com/cs-cz/dotnet/fsharp/tutorials/using-functions - Funkce vyššího řádu
http://naucte-se.haskell.cz/funkce-vyssiho-radu - Currying (Wikipedia)
https://en.wikipedia.org/wiki/Currying - Currying (Haskell wiki)
https://wiki.haskell.org/Currying - Haskell Curry
https://en.wikipedia.org/wiki/Haskell_Curry - Moses Schönfinkel
https://en.wikipedia.org/wiki/Moses_Sch%C3%B6nfinkel - ML – funkcionální jazyk s revolučním typovým systémem
https://www.root.cz/clanky/ml-funkcionalni-jazyk-s-revolucnim-typovym-systemem/ - Funkce a typový systém programovacího jazyka ML
https://www.root.cz/clanky/funkce-a-typovy-system-programovaciho-jazyka-ml/ - Curryfikace (currying), výjimky a vlastní operátory v jazyku ML
https://www.root.cz/clanky/curryfikace-currying-vyjimky-a-vlastni-operatory-v-jazyku-ml/