A funkcionális programozás (ls. még a wikipediát) kapcsán szót ejtünk a programozási paradigmákról. A nyelvek többsége nem sorolható be egyetlen paradigma alá, inkább több különbözőből tartalmaznak elemeket, mint a Python is. A következő osztályozás leegyszerűsítő ugyan, de a legfontosabbak szerepelnek benne:
Egy tisztán funkcionális programban egy függvény adott bemenetre mindig ugyanazt a kimenetet adja, és nem változtatja a program állapotát.
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/procedúrák (gépi utasítások) egymás utánjai. Innen a név, funkcionális programozás.
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.
A funkcionális programok helyessége könnyebben igazolható, könnyebben tesztelhetők, modulárisabbak,...
Az első és legfontosabb funkcionális nyelvi elem a Pythonban az iterátor, amiről már volt szó. Végigmenni egy bejárható objektum elemein, kiválogatni közülük bizonyosakat valamilyen feltétel szerint és tenni velük valamit a funkcionális programozás legfőbb eleme. Pythonban a listaértelmezés (list comprehension) és a generátorkifejezés (generator expression) használható e feladatokra.
A listaértelmezés egy listát ad vissza, a generátorkifejezés egy iterátort. A szintaktikájuk azonos, kivéve hogy az előbbit []
-be, utóbbit ()
-be tesszük.
Példa: 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.
lines = [' line1 ', 'line2\t', ' ', ' line3 \n ', '\n']
stripped_iter = (line.strip() for line in lines)
for i in stripped_iter:
print(i, end="")
line1line2line3
[line.strip() for line in lines]
['line1', 'line2', '', 'line3', '']
[line.strip() for line in lines if line.strip() != ""]
['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 seqN:
if not (conditionN):
continue
expression
[(x, y, x * y) for x in (1, 2, 3) for y in [1, 2, 3, 4] if x < y]
[(1, 2, 2), (1, 3, 3), (1, 4, 4), (2, 3, 6), (2, 4, 8), (3, 4, 12)]
l = []
for x in (1, 2, 3):
for y in [1, 2, 3, 4]:
if x < y:
l.append((x, y, x*y))
l
[(1, 2, 2), (1, 3, 3), (1, 4, 4), (2, 3, 6), (2, 4, 8), (3, 4, 12)]
Feladat: Írjuk ki az 1890 és 1915 közti szökőéveket:
[ev for ev in range(1890, 1915)];
[ev for ev in range(1890, 1915) if ev%4 == 0]
[1892, 1896, 1900, 1904, 1908, 1912]
[ev for ev in range(1890, 1915)
if (ev%4 == 0 and ev%100 != 0) or ev%400 == 0]
[1892, 1896, 1904, 1908, 1912]
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
def negyzet(x):
return x * x
negyzet(5)
25
negyzet2 = lambda x: x * x
negyzet2(5)
25
Akár nevet sem kell adni az objektumnak:
(lambda x: x**2)(5)
25
A lambda-függvénynek több argumentuma is lehet:
kif = lambda a, b, c: a + b*c
kif(2, 4, 5)
22
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})$$
l = [1, 5, 8]
print(map(negyzet, l))
list(map(negyzet, l))
<map object at 0x7fe91c33c910>
[1, 25, 64]
d = {1: 5, 2: 6, 3: 7}
list(map(negyzet, d))
[1, 4, 9]
list(map(negyzet, d.values()))
[25, 36, 49]
list(map(lambda x: x.capitalize(), ['pápa', 'tata']))
['Pápa', 'Tata']
Listaértelmezéssel:
[x**2 for x in range(5)]
[0, 1, 4, 9, 16]
[x.capitalize() for x in ['pápa', 'tata']]
['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ása 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$$
list(map(pow, [.5, 1, 1.5], [-1, 2, 3]))
[2.0, 1, 3.375]
list(map(pow, [.5, 1], [-1, 2, 3]))
[2.0, 1]
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
x = -4
x if x>0 else -x
4
def abszolut(x):
return x if x >= 0 else -x
abszolut(5), abszolut(-5)
(5, 5)
list(map(lambda x: x if x>0 else 0, range(-5,5)))
[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:
def korosztaly(x):
if x < 8:
return "gyerek"
elif x < 15:
return "fiatal"
elif x >= 18:
return "felnőtt"
korosztaly(16)
def korosztaly(x):
return "gyerek" if x < 8 else ("fiatal"
if x < 15 else ("felnőtt"
if x >= 18 else "egyik sem"))
korosztaly(16)
'egyik sem'
A filter
függvény leszűr egy bejárható objektumot egy adott bool-függvény szerint.
A feltétel függvény egy $A\mapsto \{True, False\}$ függvény kell legyen.
$$\mathrm{filter} : (A\mapsto \{True, False\})\times A^n\mapsto A^m \text{ ahol } m\leq n$$list(filter(lambda x: x % 2 == 0, range(10)))
[0, 2, 4, 6, 8]
[x for x in range(10) if x % 2 == 0]
[0, 2, 4, 6, 8]
list(filter(str.isupper, "aAbBcCöŐ"))
['A', 'B', 'C', 'Ő']
zip()
¶list(zip(['a', 'b', 'c'], (1, 2, 3)))
[('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.
any([1, 2, 0, 1])
True
all([1, 2, 0, 1])
False
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]
True
all([math.cos(i*math.pi) == 1.0 for i in range(3)])
False
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)$$
from functools import reduce
osszeg = lambda a, b: a+b
reduce(osszeg, [1, 2, 3, 4])
10
reduce(osszeg, [4])
4
reduce(osszeg, [], 0)
0
osztas = lambda a, b: a/b
print(reduce(osztas, [2, 3, 4], 1))
1/24
0.041666666666666664
0.041666666666666664
Ezzel az összegzés tétele elvileg bármikor elvégezhető és a szélsőérték keresés ennek egy alesete.
nagyobb = lambda x, y: x if x>y else y
reduce(nagyobb, [0, 1, 10, -10, 2], float("-inf"))
10
kisebb = lambda x, y: x if x<y else y
reduce(kisebb, [0, 1, 10, -10, 2], float("inf"))
-10
reduce(kisebb, [], float("inf"))
inf
def skalar(a, b):
return sum(map(lambda x, y: x*y, a, b))
skalar([0.5, 1, 1.5], [-1, 1, -1])
-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.
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])
-1.0
inner("abc", [1, 3, 2], f=tuple)
('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.
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])
-1.0
inner("abc", [1, 3, 2], i='')
'abbbcc'
inner([1, 11, 4], [3, 2, 6], g=min, f=max, i=float("-Inf"))
4
inner([1, 11, 4], [3, 2, 6], g=max, f=min, i=float("Inf"))
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:
def or_(a, b): return a or b
def and_(a, b): return a and b
from operator import or_, and_
inner([0, 0, 1, 1], [0, 1, 0, 1], g=or_, f=and_, i=1)
0
inner([0, 0, 1, 1], [0, 1, 0, 1], g=and_, f=or_, i=0)
1
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
$$|i - j| \ne |x[i] - x[j]|,$$
ahol $x$ egy permutációja a $0,1,\dots,7$ számoknak.
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:
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
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)