A funkcionális programozás alapelemei

Programozási paradigmák

A funkcionális programozás (ls. még a wikipediát) kapcsán szót ejtünk a programozási paradigmákról:

Procedurális

A nyelvek jó része ilyen, ahol a program utasítások sorozata, melyek leírják, hogy a program mit tegyen a bemenettel. (Pl. C, bash)

Deklaratív

Itt leírjuk a problémát, és a program találja ki, konstruálja meg a szükséges lépéseket. (Pl. az adatbáziskezelő nyelvek ilyenek: SQL)

OOP

Objektumorientált programozás – az objektumok állapotainak kezelése köré szervezi a programot, alapjaival megismerkedtünk korábban. (Pl. JAVA)

Funkcionális

A probléma függvényekre, illetve ezek kompozíciójára van bontva. Egy függvény adott bemenetre mindig ugyanazt a kimenetet adja, és a tisztán funkcionális programban nem változtatja a program állapotát. (Pl. Haskell, Standard ML)

A fenti feltétel nem teljesül, ha változóknak értéket adok majd azokat megváltoztatom, például az alábbi függvény más eredményt adhat akkor is, ha a paramétere ugyan az.

def f(y):
        return x + y

    x = 1
    print(f(5))
    x = -5
    print(f(5))

Ha ezt elkerüljük, akkor a függvényeink matematikai értelemben is függvények lesznek, nem pedig eljárások (gépi utasítások) egymás utánjai. Innen a név, funkcionális programozás.

Történelem

Guido van Rossum eredetileg ki kívánta dobni a funkcionális programozási nyelvekből ismert alapfüggvényeket a Python 3 core-ból a listaértelmezéssel helyettesíthetőnek tartva a fontosabbakat. A funkcionális programozás híveinek kemény ellenállását látva végül a lambda, map(), filter() maradt, de a reduce() kikerült az functools csomagba.

Előnyök

A funkcionális programok helyessége könnyebben igazolható, könnyebben tesztelhetők, modulárisabbak,...

Listaértelmezés, generátorkifejezés

A generátorkifejezés egy iterátort ad vissza, a listaértelmezés egy listát. Előbbit()-be, utóbbit []-be tesszük.

Sztringek listáját kapjuk egy fájlból beolvasott sorokból. A .strip() metódust használva vegyük le mindegyik sor elejéről és végéről a white space karaktereket.

In [1]:
lines = ['  line1 ', 'line2\t', '', ' line3 \n ']
stripped_iter = (line.strip() for line in lines)
In [2]:
for i in stripped_iter:
    print(i, end="")
line1line2line3
In [3]:
[line.strip() for line in lines]
Out[3]:
['line1', 'line2', '', 'line3']
In [4]:
[line.strip() for line in lines if line != ""]
Out[4]:
['line1', 'line2', 'line3']

A generátorkifejezés általános alakja:

(expression for expr1 in seq1
            if condition1
            for expr2 in seq2
            if condition2
                ...
            for exprN in seqN
            if conditionN )

A listaértelmezésé ugyanez, csak []-ben. Ez a következő kóddal ekvivalens:

for expr1 in seq1:
    if not (condition1):
        continue
    for expr2 in seq2:
        if not (condition2):
            continue 
        ...
        for exprN in sequenceN:
            if not (conditionN):
                continue
            expression
In [5]:
[(x, y, x * y) for x in (1, 2, 3) for y in [1, 2, 3, 4] if x < y]
Out[5]:
[(1, 2, 2), (1, 3, 3), (1, 4, 4), (2, 3, 6), (2, 4, 8), (3, 4, 12)]
In [6]:
for x in (1, 2, 3):
    for y in [1, 2, 3, 4]:
        if x < y:
            print((x, y, x*y), end=", ")
(1, 2, 2), (1, 3, 3), (1, 4, 4), (2, 3, 6), (2, 4, 8), (3, 4, 12), 

Írjuk ki az 1890 és 1915 közti szökőéveket:

In [7]:
[ev for ev in range(1890, 1915)];
In [8]:
[ev for ev in range(1890, 1915) if ev%4 == 0]
Out[8]:
[1892, 1896, 1900, 1904, 1908, 1912]
In [9]:
[ev for ev in range(1890, 1915) 
    if (ev%4 == 0 and ev%100 != 0) or ev%400 == 0]
Out[9]:
[1892, 1896, 1904, 1908, 1912]

Lambda függvények

Ha egy egyszerű kifejezést szeretnénk kiértékelni, akkor ezekhez külön függvényt írni nem feltétlenül szükséges, lokálisan használhatunk egy egyszerűbb módon definiált függvényt, ez a lambda-függvény. Ennek alakja

lambda arguments: expression
In [10]:
def negyzet(x):
    return x * x

negyzet(5)
Out[10]:
25
In [11]:
negyzet = lambda x: x * x
negyzet(5)
Out[11]:
25

Akár nevet sem kell adni az objektumnak:

In [12]:
(lambda x: x**2)(5)
Out[12]:
25

A lambda-függvénynek több argumentuma is lehet:

In [13]:
kif = lambda a, b, c: a + b*c
kif(2, 4, 5)
Out[13]:
22

map

A map egy iterálható objektum minden elemére alkalmazza a megadott függvényt és visszaad egy iterátort. (A Python korábbi változataiban egy listát adott vissza). Matematizált formulával valahogy így nézne ki: $$\mathop{\mathrm{map}}:(A\mapsto B)\times A^n\mapsto B^n\ \ (n\in \mathbb{N})$$

In [14]:
l = [1, 5, 8]
print(map(negyzet, l))
list(map(negyzet, l))
<map object at 0x7f19452a3ef0>
Out[14]:
[1, 25, 64]
In [15]:
d = {1: 5, 2: 6, 3: 7}
list(map(negyzet, d))
Out[15]:
[1, 4, 9]
In [16]:
list(map(negyzet, d.values()))
Out[16]:
[25, 36, 49]
In [17]:
list(map(lambda x: x.capitalize(), ['pápa', 'tata']))
Out[17]:
['Pápa', 'Tata']

Listaértelmezéssel:

In [18]:
[x**2 for x in range(5)]
Out[18]:
[0, 1, 4, 9, 16]
In [19]:
[x.capitalize() for x in ['pápa', 'tata']]
Out[19]:
['Pápa', 'Tata']

A map többváltozósként is használható. Ha az $f$ függvény több változós, akkor több lista megadásá szükséges és a map mindegyiken szimultán halad és hattatja az $f$ függvényt.

Formalizálva valahogy így: $$\mathop{\mathrm{map}}: (A \mapsto X)\times A^n \mapsto X^n$$ $$\mathop{\mathrm{map}}: (A\times B \mapsto X) \times A^n\times B^n \mapsto X^n$$ $$\mathop{\mathrm{map}}: (A\times B \times C\mapsto X)\times A^n\times B^n\times C^n \mapsto X^n$$ $$\vdots$$

In [20]:
list(map(pow, [.5, 1, 1.5], [-1, 2, 3]))
Out[20]:
[2.0, 1, 3.375]
In [21]:
list(map(pow, [.5, 1], [-1, 2, 3]))
Out[21]:
[2.0, 1]

Feltételes kifejezések

lambda függvényekben használhatunk feltételes kifejezéseket is. A feltételes kifejezések formája a következő:

kifejezés if feltétel else kifejezés a feltétel hamis esetre
In [22]:
x = -4
x if x>0 else -x
Out[22]:
4
In [23]:
def abszolut(x): 
    return x if x >= 0 else -x

print(abszolut(5))
print(abszolut(-5))
5
5
In [24]:
list(map(lambda x: x if x>0 else 0, range(-5,5)))
Out[24]:
[0, 0, 0, 0, 0, 0, 1, 2, 3, 4]

Feltételes kifejezésekben az else használata kötelező. Ennek egy következménye, hogy nem lesznek lefedetlen esetek a kódban.

Példa lefedetlen esetekre:

In [25]:
def korosztaly(x):
    if x < 8:
        return "gyerek"
    elif x < 15:
        return "fiatal"
    elif x >=18:
        return "felnőtt"

korosztaly(16)
In [26]:
def korosztaly(x):
    return "gyerek" if x < 8 else ("fiatal" 
            if x < 15 else ("felnőtt" 
                if x >= 18 else "egyik sem"))

korosztaly(16)
Out[26]:
'egyik sem'

filter

A filter függvény leszűr egy listát egy adott bool-függvény szerint.

A feltétel függvény egy $A\mapsto \{True, False\}$ függvény kell legyen.

$$filter : (A\mapsto \{True, False\})\times A^n\mapsto A^m \text{ ahol } m\leq n$$
In [27]:
list(filter(lambda x: x % 2 == 0, range(10)))
Out[27]:
[0, 2, 4, 6, 8]
In [28]:
[x for x in range(10) if x % 2 == 0]
Out[28]:
[0, 2, 4, 6, 8]
In [29]:
list(filter(str.isupper, "aAbBcCöŐ"))
Out[29]:
['A', 'B', 'C', 'Ő']

zip()

In [30]:
list(zip(['a', 'b', 'c'], (1, 2, 3)))
Out[30]:
[('a', 1), ('b', 2), ('c', 3)]

any() és all()

A kvantoroknak megfelelő függvények eldöntik, hogy egy bejárható objektum elemei közt van-e True, illetve hogy mindegyik True értékű-e.

In [31]:
any([1,2,0,1])
Out[31]:
True
In [32]:
all([1,2,0,1])
Out[32]:
False
In [33]:
import math
print([math.cos(i*math.pi) for i in range(3)])
any([math.cos(i*math.pi) == 1.0 for i in range(3)])
[1.0, -1.0, 1.0]
Out[33]:
True
In [34]:
all([math.cos(i*math.pi) == 1.0 for i in range(3)])
Out[34]:
False

reduce

A reduce (a functools csomag függvénye) egy 2-változós függvényt alkalmaz egy iterálható objektum (pl. lista) elemeire balról jobbra haladva, míg végül egyetlen értéket nem kap. Alakja:

reduce(*függvény*, *iterálható*[, *kezdőérték*])

A kétváltozós függvény $A\times B\mapsto A$ alakú kell legyen, az iterálható elemek $B$ beliek, a kezdőérték $A$-beli. A kezdőértéket a többi elem elé teszi. Ez a visszaadott érték üres lista esetén. Ha nincs kezdőérték, és a lista egyelemű, akkor azt az elemet adja vissza.

Egy $(a, b, c)$ tuple-re és egy $x$ kezdőértékre a reduce a következőt adja: $$\mathop{\mathrm{reduce}} (f, (a,b,c)) = f(f(a, b), c)$$ $$\mathop{\mathrm{reduce}} (f, (a,b,c), x) = f(f(f(x, a), b), c)$$

In [35]:
from functools import reduce
osszeg = lambda a, b: a+b
reduce(osszeg, [1, 2, 3, 4])
Out[35]:
10
In [36]:
reduce(osszeg, [4])
Out[36]:
4
In [37]:
reduce(osszeg, [], 0)
Out[37]:
0
In [38]:
osztas = lambda a, b: a/b
print(reduce(osztas, [2, 3, 4], 1.0))
1/24.0
0.041666666666666664
Out[38]:
0.041666666666666664

Ezzel az összegzés tétele elvileg bármikor elvégezhető és a szélsőérték keresés ennek egy alesete.

In [39]:
nagyobb = lambda x, y: x if x>y else y
reduce(nagyobb, [0, 1, 10, -10, 2], float("-inf"))
Out[39]:
10
In [40]:
kisebb = lambda x, y: x if x<y else y
reduce(kisebb, [0, 1, 10, -10, 2], float("inf"))
Out[40]:
-10
In [41]:
reduce(kisebb, [], float("inf"))
Out[41]:
inf

Két példa

A belső szorzat általánosítása

A skalár (belső) szorzat egy lehetséges implementációja:

In [42]:
def skalar(a, b):
    return sum(map(lambda x, y: x*y, a, b))

skalar([0.5, 1, 1.5], [-1, 1, -1])
Out[42]:
-1.0

Általában az $f(g(x_1, y_1), g(x_2, y_2), \ldots g(x_n, y_n))$ akalú kifejezéseket lehet a skalár szorzás általánosításának nevezni.

Könnyű implementálni funkcionálisan.

In [43]:
def inner(a, b, g=lambda x, y: x*y, f=sum):
    return f(list(map(g, a, b)))

inner([0.5, 1, 1.5], [-1, 1, -1])
Out[43]:
-1.0
In [44]:
inner("abc", [1, 3, 2], f=list)
Out[44]:
['a', 'bbb', 'cc']

Vagy ha a $g$ függvény asszociatív, akkor lehet két változósnak is tekinteni és reduce-al számolni.

In [45]:
def inner(a, b, g=lambda x, y: x*y, f=lambda a, b: a+b, i=0):
    return reduce(f, list(map(g, a, b)), i)

inner([0.5, 1, 1.5], [-1, 1, -1])
Out[45]:
-1.0
In [46]:
inner("abc", [1, 3, 2], i='')
Out[46]:
'abbbcc'
In [47]:
inner([1,11,4], [3,2,6], g=min, f=max, i=float("-Inf"))
Out[47]:
4
In [48]:
inner([1,11,4], [3,2,6], g=max, f=min, i=float("Inf"))
Out[48]:
3

Tegyük ugyanezt a 'vagy'-ok 'és'-ével és az 'és'-ek 'vagy'-ával. Ehhez az or és and műveletek függvényváltozata kell. Vagy csinálunk ilyet, vagy az operator modulból betöltjük:

In [49]:
or_ = lambda a, b: a or b
and_ = lambda a, b: a and b
In [50]:
from operator import or_, and_
In [51]:
inner([0,0,1,1], [0,1,0,1], g=or_, f=and_, i=1)
Out[51]:
0
In [52]:
inner([0,0,1,1], [0,1,0,1], g=and_, f=or_, i=0)
Out[52]:
1

8 királynő

Hányféleképp lehet nyolc királynőt föltenni egy sakktáblára, hogy semelyik kettő ne üsse egymást?

Az ilyen bástyaelhelyezések száma 8! = 40320. A királynők azonban átlósan is üthetik egymást. Az i-edik és j-edik sorban lévő királynő pontosan akkor nem üti egymást, ha abs(i - j) != abs(x[i] - x[j]). A permutációk listázásához töltsük be az itertools csomag permutations függvényét, mely a for ciklus minden ciklusában ad egy következő permutációt, amíg a végére nem ér. Így a megoldás:

In [53]:
from itertools import permutations
s = 0
for x in permutations(list(range(8))):
    if all([i == j or abs(i - j) != abs(x[i] - x[j])
            for i in range(8) for j in range(8)]):
        s += 1
print(s)
92
In [54]:
for x in permutations(list(range(8))):
    if all([i == j or abs(i - j) != abs(x[i] - x[j])
            for i in range(8) for j in range(8)]):
        print(x)
(0, 4, 7, 5, 2, 6, 1, 3)
(0, 5, 7, 2, 6, 3, 1, 4)
(0, 6, 3, 5, 7, 1, 4, 2)
(0, 6, 4, 7, 1, 3, 5, 2)
(1, 3, 5, 7, 2, 0, 6, 4)
(1, 4, 6, 0, 2, 7, 5, 3)
(1, 4, 6, 3, 0, 7, 5, 2)
(1, 5, 0, 6, 3, 7, 2, 4)
(1, 5, 7, 2, 0, 3, 6, 4)
(1, 6, 2, 5, 7, 4, 0, 3)
(1, 6, 4, 7, 0, 3, 5, 2)
(1, 7, 5, 0, 2, 4, 6, 3)
(2, 0, 6, 4, 7, 1, 3, 5)
(2, 4, 1, 7, 0, 6, 3, 5)
(2, 4, 1, 7, 5, 3, 6, 0)
(2, 4, 6, 0, 3, 1, 7, 5)
(2, 4, 7, 3, 0, 6, 1, 5)
(2, 5, 1, 4, 7, 0, 6, 3)
(2, 5, 1, 6, 0, 3, 7, 4)
(2, 5, 1, 6, 4, 0, 7, 3)
(2, 5, 3, 0, 7, 4, 6, 1)
(2, 5, 3, 1, 7, 4, 6, 0)
(2, 5, 7, 0, 3, 6, 4, 1)
(2, 5, 7, 0, 4, 6, 1, 3)
(2, 5, 7, 1, 3, 0, 6, 4)
(2, 6, 1, 7, 4, 0, 3, 5)
(2, 6, 1, 7, 5, 3, 0, 4)
(2, 7, 3, 6, 0, 5, 1, 4)
(3, 0, 4, 7, 1, 6, 2, 5)
(3, 0, 4, 7, 5, 2, 6, 1)
(3, 1, 4, 7, 5, 0, 2, 6)
(3, 1, 6, 2, 5, 7, 0, 4)
(3, 1, 6, 2, 5, 7, 4, 0)
(3, 1, 6, 4, 0, 7, 5, 2)
(3, 1, 7, 4, 6, 0, 2, 5)
(3, 1, 7, 5, 0, 2, 4, 6)
(3, 5, 0, 4, 1, 7, 2, 6)
(3, 5, 7, 1, 6, 0, 2, 4)
(3, 5, 7, 2, 0, 6, 4, 1)
(3, 6, 0, 7, 4, 1, 5, 2)
(3, 6, 2, 7, 1, 4, 0, 5)
(3, 6, 4, 1, 5, 0, 2, 7)
(3, 6, 4, 2, 0, 5, 7, 1)
(3, 7, 0, 2, 5, 1, 6, 4)
(3, 7, 0, 4, 6, 1, 5, 2)
(3, 7, 4, 2, 0, 6, 1, 5)
(4, 0, 3, 5, 7, 1, 6, 2)
(4, 0, 7, 3, 1, 6, 2, 5)
(4, 0, 7, 5, 2, 6, 1, 3)
(4, 1, 3, 5, 7, 2, 0, 6)
(4, 1, 3, 6, 2, 7, 5, 0)
(4, 1, 5, 0, 6, 3, 7, 2)
(4, 1, 7, 0, 3, 6, 2, 5)
(4, 2, 0, 5, 7, 1, 3, 6)
(4, 2, 0, 6, 1, 7, 5, 3)
(4, 2, 7, 3, 6, 0, 5, 1)
(4, 6, 0, 2, 7, 5, 3, 1)
(4, 6, 0, 3, 1, 7, 5, 2)
(4, 6, 1, 3, 7, 0, 2, 5)
(4, 6, 1, 5, 2, 0, 3, 7)
(4, 6, 1, 5, 2, 0, 7, 3)
(4, 6, 3, 0, 2, 7, 5, 1)
(4, 7, 3, 0, 2, 5, 1, 6)
(4, 7, 3, 0, 6, 1, 5, 2)
(5, 0, 4, 1, 7, 2, 6, 3)
(5, 1, 6, 0, 2, 4, 7, 3)
(5, 1, 6, 0, 3, 7, 4, 2)
(5, 2, 0, 6, 4, 7, 1, 3)
(5, 2, 0, 7, 3, 1, 6, 4)
(5, 2, 0, 7, 4, 1, 3, 6)
(5, 2, 4, 6, 0, 3, 1, 7)
(5, 2, 4, 7, 0, 3, 1, 6)
(5, 2, 6, 1, 3, 7, 0, 4)
(5, 2, 6, 1, 7, 4, 0, 3)
(5, 2, 6, 3, 0, 7, 1, 4)
(5, 3, 0, 4, 7, 1, 6, 2)
(5, 3, 1, 7, 4, 6, 0, 2)
(5, 3, 6, 0, 2, 4, 1, 7)
(5, 3, 6, 0, 7, 1, 4, 2)
(5, 7, 1, 3, 0, 6, 4, 2)
(6, 0, 2, 7, 5, 3, 1, 4)
(6, 1, 3, 0, 7, 4, 2, 5)
(6, 1, 5, 2, 0, 3, 7, 4)
(6, 2, 0, 5, 7, 4, 1, 3)
(6, 2, 7, 1, 4, 0, 5, 3)
(6, 3, 1, 4, 7, 0, 2, 5)
(6, 3, 1, 7, 5, 0, 2, 4)
(6, 4, 2, 0, 5, 7, 1, 3)
(7, 1, 3, 0, 6, 4, 2, 5)
(7, 1, 4, 2, 0, 6, 3, 5)
(7, 2, 0, 5, 1, 4, 6, 3)
(7, 3, 0, 2, 5, 1, 6, 4)
In [ ]: