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 řadí mezi deklarativní 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 objektové 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.

Více např. na http://docs.python.org/3/howto/functional.html

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 [9]:
def pr(s): 
    return s

x = 3
res = (x==1 and pr('one')) or (x==2 and pr('two')) or (pr('other'))
print(res)
other
In [10]:
x = 2
res = (x==1 and pr('one')) or (x==2 and pr('two')) or (pr('other'))
print(res)
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 [11]:
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 [12]:
# 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 [13]:
# definujeme funkci foo
def foo(x):
    return bool(x)

# a přiřadíme do proměnné boo
boo = foo
In [14]:
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 [ ]:
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
In [17]:
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 [18]:
def calculations(a, b):
    def add(x):
        return x * (a + b)

    return add
In [20]:
f12 = calculations(1, 2)
f34 = calculations(3, 4)
print(f12(2))
print(f34(2))
6
14

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

In [21]:
def calculations(a, b):
    return a, b, lambda x: x * (a + b)
In [22]:
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 [23]:
def calc(f):
    return lambda x: f(x + 1)
In [24]:
foo = calc(lambda x: x**2)
print(foo)
# výsledná funkce je (x + 1)**2
print(foo(2))
<function calc.<locals>.<lambda> at 0x7fdfaa5c2680>
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 [25]:
# list pomocí [ ... ]
[i+1 for i in [0, 1, 2, 3]]
Out[25]:
[1, 2, 3, 4]
In [26]:
# generator (nikoli tuple) pomocí ( ... )
(i+1 for i in [0, 1, 2, 3])
Out[26]:
<generator object <genexpr> at 0x7fdfaa452c50>
In [27]:
# generátor je iterable
for i in (i for i in range(4) if i % 2):
    print(i)
1
3
In [28]:
# dict se tvoří pomocí { ... }
{i: i+1 for i in range(5)}
Out[28]:
{0: 1, 1: 2, 2: 3, 3: 4, 4: 5}
In [29]:
# a set také pomocí { ... }
{k for k in {i: i+1 for i in range(5)}.keys()}
Out[29]:
{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í iterátor výsledků -- map objekt.
  • functools.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 [30]:
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 0x7fdfaa34a1d0>
<map object at 0x7fdfaa34a950>
In [31]:
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 0x111afb710>

Většinou se v Python komunitě doporučuje používat 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
In [32]:
print([str(x) for x in range(10)])
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
In [33]:
print([x for x in range(10) if (x % 3) == 0])
[0, 3, 6, 9]

Cvičení

  1. Naprogramujte generátorovou funkci slowmap(func, iterable, speed=1), která bude dělat to samé jako map(func, iterable), ale mezi každou iterací bude čekat pomocí funkce time.sleep 1/speed vteřin.
  2. Vyzkoušejte pomocí slowmap aplikovat lambda funkci, která vrací třetí mocninu argumentu, na range(10). Výsledek nakonec uložte do proměnné typu list.
  3. Napište funkci pown(n), která bude vracet funkci vracející n-tou mocninu argumentu. Tj. např. pown(3)(2) vrátí výsledek 8. Tuto funkci použijte místo lambda funkce v části 2.
In [ ]:
 

Komentáře

Comments powered by Disqus