Funkcionální programování v Pythonu

Jedna z (volnějších) definic funkcionálního programování říká, že pokud nahradíme v kódu volání funkce výsledky tohoto volání, je program (jazyk) funkcionální. Jinými slovy, funkcionální programování znamená, že volaní funkce pouze produkuje výstup, nemá žádné vedlejší účinky a ani nemění její interní stav. Funkcionální program je v podstatě rekurzivním vyhodnocením výrazů. Mezi funkcionální jazyky patří např. Haskell, Clojure, Lisp nebo Erlang. Funkcionální programování se zařazuje mezi deklrativní programovací techniky (kam patří např. i SQL) a je téměř opakem objektově orientovaného programování, kde volání metod mění stav objektů. Python je spíše oběktové orientovaný jazyk, nicméně je v něm možné používat technik funkcionálního programování a jednodušše tyto dva odlišné způsoby programování kombinovat.

A proč vlastně programovat funkcionálně? Jednoduchá, či spíše zjednodušující, odvěď je vyhnout se vedlejším efektům. To má svě výhody, předvším jednoduchochou paralelizaci a vyhýbání se chybám typu přiřazení špatné hodnoty do proměnné. Zatím to ovšem vypadá, že čistě funkcionální programování nepřináší až tak velký užitek, neboť mezi programátory není příliš rozšířené. Vhodně použité funkcionální prvky v jinak procedurálním, objektově orientovaném jazyku mohou být velice užitečné. A toto nám Python poměrně dobře umožňuje.

Zdroje: http://docs.python.org/2/howto/functional.html, http://ua.pycon.org/static/talks/kachayev/

Základní kameny funkcionálního programování

  • First-class functions, anonymní (lambda) funkce
  • Nemění se stav (programu)
  • Immutable data
  • Closure (uzávěr)
  • Rekurze
  • Řady
  • ...

Podpora v Pythonu

  • Funkce map, reduce, filter
  • Zkrácené logické výrazy
  • Generátorová notace (pro tuple, list, set, dict)
  • Iterátory
  • Moduly operator, itertools, functools

Pojďme se na to podívat prakticky.

Vyhýbáme se blokům if - else blokům a smyčkám

Tento kód:

if <cond1>:
    func1()
elif <cond2>:
    func2()
else:
    func3()

je ekvivalentní

(<cond1> and func1()) or (<cond2> and func2()) or (func3())

Příklad:

In [1]:
x = 3
def pr(s): return s
res = (x==1 and pr('one')) or (x==2 and pr('two')) or (pr('other'))
print(res)
x = 2
res = (x==1 and pr('one')) or (x==2 and pr('two')) or (pr('other'))
print(res)
other
two

Pozor: v tomto příkladu může dojít k nesprávnému vyhodnocení -- přijdete na to v jakém případě? (Nápověda: souvisí to s funkcí pr, resp. její návratovou hodnotou.) Lepší je proto použít podmíněné výrazy (conditional expression) (ternární logické operátory).

While smyčka může být nahrazena rekurzí:

In [2]:
n = 1
while n < 5:
    n += 1
    print("n = {}".format(n))
print("výsledek je {}".format(n))
n = 2
n = 3
n = 4
n = 5
výsledek je 5
In [3]:
# to samé rekurzivně
def get_n(n=1):
    n += 1
    print(n)
    # použijeme podmíněný výraz
    return n if n >= 5 else get_n(n)
print("výsledek je {}".format(get_n()))
2
3
4
5
výsledek je 5

First-class funkce, uzávěry, lambda

Pokud je možné funkce používat ekvivalentně jako proměnné, např. předávat je jako parametry funkcím nebo vracet funkce jako výsledek jiné funkce, mluvíme o first-class funkcích. S tímto konceptem se vážou i anonymní neboli lambda funkce. Opět to ukážeme na příkladech.

In [4]:
# definujeme funkci foo
def foo(x):
    return bool(x)
# a přiřadíme do proměnné boo
boo = foo
def simply_apply(func, val):
    # v proměnné func předpokládáme funkci
    # simply_apply je "higher-order function"
    print("the result is: %s" % str(func(val)))
# nyní použijeme funkci simply_apply pro vyhodnocení jiné funkce
# (konkrétně funkce foo, kterou máme v proměnné boo
simply_apply(boo, ())  # všiměte si i druhého argumentu
the result is: False

Funkci lze vytvořit v rámci jiné funkce a vrátit jako její výsledek. Tato funkce může obsahovat i uzávěr (closure), který zachová aktuální kontext.

In [5]:
def calculations(a, b):
    # tady se vytvoří uzávěr, aktuální hodnoty a, b se uchovávají ve funkci add
    def add():
        return a + b

    return add
f = calculations(1, 2)
print(f())
f = calculations(3, 2)
print(f())
3
5

Vytvořená funkce add může mít i parametr.

In [6]:
def calculations(a, b):
    def add(x):
        return x * (a + b)

    return add
f = calculations(1, 2)
ff = calculations(3, 4)
print(f(2))
print(ff(2))
6
14

Pro vyvoření nové funkce můžeme také použít anonymní (lambda) funkci.

In [7]:
def calculations(a, b):
    return a, b, lambda x: x * (a + b)
a, b, f = calculations(1, 2)
aa, bb, ff = calculations(3, 4)
print(f(2))
print(ff(2))
6
14

Higher ordered functions (neboli funkcionály) přijímají jako parametr funkci.

In [8]:
def calc(f):
    return lambda x: f(x + 1)
foo = calc(lambda x: x**2)
print(foo)
# výsledná funkce je (x + 1)**2
print(foo(2))
<function calc.<locals>.<lambda> at 0x7f8a08647488>
9

Konec hraní, používáme závorky, map, reduce, filter

Jak už jsme ukázali dříve, Python obsahuje koncept iterátorů, který je uplatněn pro vestavěné kontejnery, jako jsou list, tuple, dict, set, a objevuje se i u datových toků (stream), tedy např. souborů. Dále Python obsahuje generátory a generátorovou notaci. Iterátory, jakožto metody objektů, mohou ovšem měnit vnitřní stav, proto úplně nezapadají do funkcionálního stylu programování.

Generátorová notace

Pomocí závorek a několika klíčových slov for a in, případně také if, můžeme jednoduše vytvářet kontejnery.

In [9]:
# list pomocí [ ... ]
[i+1 for i in [0, 1, 2, 3]]
Out[9]:
[1, 2, 3, 4]
In [10]:
# generator (nikoli tuple) pomocí ( ... )
(i+1 for i in [0, 1, 2, 3])
Out[10]:
<generator object <genexpr> at 0x7f8a0865e150>
In [11]:
# generátor je iterable
for i in (i for i in range(4) if i % 2):
    print(i)
1
3
In [12]:
# dict se tvoří pomocí { ... }
{i: i+1 for i in range(5)}
Out[12]:
{0: 1, 1: 2, 2: 3, 3: 4, 4: 5}
In [13]:
# a set také pomocí { ... }
{k for k in {i: i+1 for i in range(5)}.keys()}
Out[13]:
{0, 1, 2, 3, 4}

map -- reduce (-- filter)

Map -- reduce princip je velice rozšířený ve funkcionálním programování a hojně se také využívá v (paralelním) zpracování dat, především pokud je jich mnoho. V Pythonu máme funkce map a reduce, které moho být aplikovány na jakékoli iterable obejkty.

  • map(function, object) - aplikuje funkci function postupně na prvky object (pomocí iterací). function musím mít jeden argument (další mohou být nepovinné). Vrátí pole výsledků -- v Python 2 jako list, v Python 3 jako map objekt.
  • reduce(function, object) - aplikuje funkci kumulativně na dva prvky obejktu, výsledkem je jedna hodnota podledního volání funkce. Reduce jde zapsat rekurzivně jako f[1] = function(object[0], object[1]), f[n+1] = function[f[n], object[n+1]).
  • filter(function, object) vrátí elementy, pro které je funkce True.

Ukážeme si to na příkladech.

In [14]:
from operator import ifloordiv
# aplikujeme funkci str
print(map(str, range(10)))
# aplikujee lambda funkci, která vrací celočíselné dělení 3
print(map(lambda x: ifloordiv(x, 3), range(10)))
<map object at 0x7f8a085f1320>
<map object at 0x7f8a085f1a20>
In [15]:
from operator import add
from functools import reduce
# teď "zredukujeme" předchozí výsledky pomocí add (sčítání)
print(reduce(add, map(str, range(10))))
print(reduce(add, map(lambda x: ifloordiv(x, 3), range(10))))
0123456789
12
In [16]:
# čísla dělitelná třemi
print(filter(lambda x: (x % 3) == 0, range(10)))
<filter object at 0x7f8a085f19e8>

Python 3 přináší důležité změny:

  • map a filter vrací iterátory (nikoli list).
  • reduce není vestavěná funkce, je ale dostupná v modulu functools.

Nejen proto je jepší použít v mnoha případech použít závorky neboli generátorovou notaci.

  • místo map použijeme [function(x) for x in object]
  • místo filter máme [x for x in object if function(x)]
  • místo reduce můžeme v mnoha případech použít vestavěné sum, all, any

(v obou případech výše můžeme použít kulaté závorky pro vytvoření generátoru).

In [17]:
print([str(x) for x in range(10)])
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
In [18]:
print([x for x in range(10) if (x % 3) == 0])
[0, 3, 6, 9]

Cvičení

  1. Pomocí lambda funkce vytvořte funkci mean, která vypočítá průměr listu (nebo tuple apod). Využijte vestavěné funkce.
  2. Využijte výsledek k výpočtu součtu druhých mocnin lichých čísel < 100.
  3. Naprogramujte funkci sum pomocí reduce a volitelného operátoru pro sčítání (výchozí bude +). Využijte modul operator.
  4. Využijte předchozí funkci pro implementaci modulo(12) sumace, nebo-li sčítání/odčítání hodin. Např. sum(range(6)) = 3 (mod 12). Použijte lambda funkci.
In [ ]:
 

Komentáře

Comments powered by Disqus