A Python jelenleg talán a legjelentősebb szkriptnyelv (TIOBE index 4. hely Java, C és C++ után). Szkritpnyelvként a Python lehetővé teszi, hogy magasabb szintű, absztraktabb, elegánsabb kódot írjunk, azon az áron, hogy a program lassabban fog futni. Ahogyan egyre gyorsabbak a gépek, egyre több és több olyan feladat van, ahol egyszerűen nincs igény a nyers vas sebességére – ezeknél nincs okunk arra, hogy ne használjunk Pythont.
Az előadásomban megpróbálom demonstrálni, hogy mit adnak nekünk a parancsértelmezők.
A Python egy objektumorientált nyelv, „alapvetően” ugyanolyan utasítások vannak benne, mint mondjuk C++-ban. Főbb szintaktikai különbségek:
{
és }
helyett beljebb kezdés jelöli (és egy kettőspont a blokkot nyitó utasítás végén).def
vezeti be.Például:
def repeat(string):
if "ni" in string:
print('Aaargh!')
return string*3
else:
return string*6
print(repeat("spam "))
print(repeat("ni "))
Az osztályokat class
vezeti be, a metódusokat osztályon belül def
-fel kell definiálni.
Bizonyos speciális funkciókat (konstruktor, operátorok felülírása) olyan metódusok látnak el, amelyeknek a neve két aláhúzásjellel kezdődik és végződik:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return '{} ({})'.format(self.name, self.age)
def do_something(self, thing):
print(self, thing+'!')
class Knight(Person):
def __str__(self):
return "Sir "+super().__str__()
robin = Knight("Robin", 42)
robin.do_something("bátran elszaladt")
Itt két dupla-aláhúzásjeles speciális nevet használtunk:
__init__
az osztály konstruktora; új objektum létrehozásához egyszerűen meghívjuk paraméterekkel az osztályt (mint a 14. sorban) és ennek hatására hívja meg a rendszer az __init__
metódust (ebben a példában a Knight(...)
hívás a Person
-tól örökölt __init__
-et hívja).__str__
a sztring-konverzió metódus. Ezt automatikusan meghívja (többek között például) a beépített print
függvény, aminek sztringgé kell alakítani a paramétereit, mielőtt a képernyőre írhatná azokat.Egy függvény definíciója (a def
utasítás) két dolgot csinál: létrehoz egy függvény objektumot és azt eltárolja olyan néven, amit megadtunk. A dekorátorok lehetővé teszik, hogy valamit „beszúrjunk” eközé a két lépés közé: létrejön a függvény objektum, meghívódik a dekorátor és megkapja paraméterként az éppen létrejött függvény objektumot, majd a dekorátor visszatérési értéke eltárolódik olyan néven, amit a függvény definíciójánál megadtunk.
A dekorátorokat kukac karakterrel kell bevezetni:
@callable_used_as_decorator
def new_function(arguments):
#... function body
Ahogyan fentebb leírtuk, ez nagyjából annak felel meg, mintha azt írtuk volna, hogy:
def _temporary_function_object(arguments):
#... function body
new_function = callable_used_as_decorator(_temporary_function_object)
(Leszámítva persze azt, hogy a dekorátornál nem tárolódik el ideiglenes változóban (_temporary_function_object
) a függvényünk.)
Példaképpen ez a (gyakorlatban nem túl hasznos) függvény meghívja a megkapott függvény objektumot, majd módosítás nélkül visszakapja azt:
def run_immediately(func):
func()
return func
Ha ezt dekorátorként használjuk, akkor az történik, amit ígértünk – a függvény azonnal lefut a definiálása után és később úgy használható, mintha mi sem történt volna:
@run_immediately
def greet():
print("Üdvözöllek, dicső lovag!")
print("spam, spam, spam")
greet()
greet()
Általában azonban olyan dolgokat akarunk dekorátorként használni, amelyek valahogy módosítják az éppen definiált függvényt. A függvény „módosítására” két módszer is van, először azt mutatjuk be, ami C++-ban is létezik (viszont Pythonban ritkábban használt). Itt azt csináljuk, hogy valójában nem is függvényt adunk vissza, hanem egy olyan objektumot, ami függvényként használható, mert felülírta a függvényhívás operátort (van neki __call__
metódusa):
class cached:
def __init__(self, func):
self.func = func
self.cache = {} # üres hash-tábla
def __call__(self, arg):
try:
return self.cache[arg]
except KeyError:
result = self.cache[arg] = self.func(arg)
return result
@cached
def ask_for_value(name):
return input(name+" értéke? ")
print(ask_for_value)
results = []
results.append(ask_for_value("x"))
results.append(ask_for_value("y"))
results.append(ask_for_value("x"))
print(results)
Ahogyan láthatjuk, ask_for_value
néven valójában nem egy függvény, hanem egy cached
típusú objektum van eltárolva, ami azt csinálja, amit szeretnénk: bekéri x
értékét, bekéri y
értékét, majd nem kéri be újra x
értékét, mert azt már megmondtuk neki.
További megjegyzések:
cached
implementáció csak egyparaméteres függvényeket kezel, de nem lett volna sokkal bonyolultabb olyat írni, ami akárhány paramétert kezel.__call__
metódus akkor hívódik meg, ha a megfelelő típusú objektumot függvényként kezeljük és paramétereket adunk át neki.A második megoldás azon alapul, hogy Pythonban „jól” lehet függvényen belül függvényeket definiálni: Ha a belső függvény használja a külső függvény egy lokális változóját, akkor egy closure jön létre és a belső függvény „elkapja és magával viszi” azokat a lokális változókat.
Ennek a bonyolult folyamatnak az az eredménye, hogy a következő kód ugyanúgy gyorsítótárazást valósít meg, mint az előző példa:
def cached(func):
cache = {}
def wrapper(arg):
try:
return cache[arg]
except KeyError:
result = cache[arg] = func(arg)
return result
return wrapper
@cached
def ask_for_value(name):
return input(name+" értéke? ")
print(ask_for_value)
results = []
results.append(ask_for_value("x"))
results.append(ask_for_value("y"))
results.append(ask_for_value("x"))
print(results)
Vegyük észre, hogy mostmár valóban egy függvény típusú dolog van eltárolva az ask_for_value
néven, azonban ennek a metaadatai (például neve) nem stimmelnek.
Ennek az esztétikai problémának a korrigálására lehet importálni a functools.wraps
függvényt, ami helyreteszi a metaadatokat:
import functools
def cached(func):
cache = {}
@functools.wraps(func)
def wrapper(arg):
try:
return cache[arg]
except KeyError:
result = cache[arg] = func(arg)
return result
return wrapper
@cached
def ask_for_value(name):
'''Így szokás dokumentációt írni Pythonban.'''
return input(name+" értéke? ")
print(ask_for_value)
print("Fontos metaadatok:")
print("Név:", ask_for_value.__name__)
print("Dokumentáció:", ask_for_value.__doc__)
#futtatás kihagyva, ugyanúgy működne, mint előbb
Persze kézzel is átállítgathattuk volna a metaadatokat (aki nagyon kíváncsi: itt a functools modul forráskódja), de ez így elegánsabb és rövidebb kód.
Ez a példa illusztrálja, hogy a dekorátor kijelölésekor lehet adattag-elérést (pont operátor) és függvényhívást alkalmazni. (Más operátorokat viszont nem, lásd a def
utasítás leírását a Python Language Reference-ben.)
Pontosabban fogalmazva functools.wraps
nem egy dekorátor, hanem egy dekorátor factory: paraméterül kap egy függvényt (ahonnan veszi a metaadatok értékeit) és a visszatérési értékét fogjuk dekorátorként használni.
Mi is tudunk ilyen dekorátor factory-t írni, bár ehhez kicsit sok egymásba ágyazott függvény fog kelleni. A példánk azt valósítja meg, hogy a dekorált függvény minden meghívása után az eredmény legyen naplózva (egy bizonyos fájlba, bizonyos üzenettel felcímkézve):
from functools import wraps
def logged(file, msg):
def decorator(func):
@wraps(func)
def wrapper(*args, **kw):
result = func(*args, **kw)
file.write(msg + str(result) + "\n")
return result
return wrapper
return decorator
import sys
@logged(sys.stderr, "Osztás eredménye: ")
def divide(x, y):
return x/y
[divide(2,2), divide(16,-8), divide(1,8)]
Itt sys.stderr a sztenderd hiba kimenet, amit békés rózsaszín háttérrel jelenít meg a Jupyter rendszer.
Ezt is megvalósíthatjuk closure-ök nélkül, C++ stílusban, de ehhez meglehetősen sokat kell írni:
class ResultLogger:
def __init__(self, func, file, msg):
self.func = func
self.file = file
self.msg = msg
def __call__(self, *args, **kw):
result = self.func(*args, **kw)
self.file.write(self.msg + str(result) + "\n")
return result
class logged:
def __init__(self, file, msg):
self.file = file
self.msg = msg
def __call__(self, func):
return ResultLogger(func, self.file, self.msg)
import sys
@logged(sys.stderr, "Összeadás eredménye: ")
def add(x, y):
return x+y
[add(2,2), add(6,-8), add(3,3)]
Dekorátorokat nem csak függvényekre, hanem osztályokra is lehet alkalmazni. Például a rendezési operátorok definícióját megcsinálja nekünk a functools.total_ordering
dekorátor (csak az egyenlőséget és egy egyenlőtlenséget kell nekünk definiálnunk):
import functools
@functools.total_ordering
class Results:
def __init__(self, win, loss):
self.win = win
self.loss = loss
def adventage(self):
return self.win-self.loss
def __eq__(self, oth):
""" operator==() """
return self.win == oth.win and self.loss == oth.loss
def __lt__(self, oth):
""" operator<() """
return (self.adventage(), self.win) < (oth.adventage(), oth.win)
x = Results(6,3)
y = Results(4,2)
z = Results(4,1)
w = Results(3,0)
print(x>=y, x<=z, x!=w, w<x, x<x)
Egy property egy olyan dolog, ami egy közönséges adattagnak látszik, de valójában valamilyen függvényeket hív meg, amikor adatot írnak bele/adatot olvasnak ki belőle. Egy property legegyszerűbben dekorátorok segítségével hozható létre:
import math
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
@property
def angle(self):
return math.atan2(self.x, self.y)
@property
def r(self):
return math.sqrt(self.x**2 + self.y**2)
p = Point(3,4)
print(p.angle, p.r)
Ezek most csak olvasható adattagként viselkednek:
p.r = 10
... de definiálhatóak hozzájuk setterek is:
import math
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
@property
def angle(self):
return math.atan2(self.x, self.y)
@angle.setter
def angle(self, value):
r = self.r
self.x = math.cos(value)*r
self.y = math.sin(value)*r
@property
def r(self):
return math.sqrt(self.x**2 + self.y**2)
@r.setter
def r(self, value):
angle = self.angle
self.x = math.cos(angle)*value
self.y = math.sin(angle)*value
p = Point(3,4)
p.r = 10
print(p.x, p.y)
Ahogyan látható, bármilyen számításokat elrejthetünk a property mögött, ennek persze az az ára, hogy a Python rendszer nem tudja és nem akarja ellenőrizni azt, hogy a property valóban kulturált adattagként viselkedik-e (például ha beleírunk egy értéket, akkor utána ugyanaz az érték lesz-e kiolvasható).
A propertyk létezésének nagy előnye, hogy nekik köszönhetően Pythonban egy osztály „publikus” interfészében nyugodtan lehetnek publikus adattagok (viszont nem illik get_foobar()
/ set_foobar()
jellegű gettereket és settereket írni).
Ha egy adattaghoz később extra funkcionalitást akarunk csatolni (például egy beállítás-fájlból akarjuk kiolvasni vagy ellenőrizni akarjuk, hogy csak megfelelő értéket lehessen beleírni stb.), akkor bármikor lecserélhetjük egy property-re. (Az adattagok többségénél viszont ez sohasem fog bekövetkezni és azoknál élvezhetjük, hogy nem hígítják fel getter-setter metódusok a kódunkat.)
A property
típus (aminek a konstruktorát az előbb dekorátorként használtuk) nem egyedülálló – hasonló működésre képes bármilyen más típus, ami megvalósítja a megfelelő dupla-aláhúzásjeles metódusokat. (Ugyanúgy, mint ahogy rendezési operátorok alkalmazhatóak minden objektumra, ami __le__
, __lt__
, stb. metódusokkal rendelkezik és függvényekhez hasonlóan meghívható minden objektum, ami __call__
metódust definiál.)
Figyeljük meg az előző példákban, hogy a property objektumok egy osztálynak az adattagjai voltak (C++ szóhasználattal static member
-ek) és olyankor viselkedtek furcsán, amikor az adattag-elérés operátor (pont operátor) segítségével „piszkáltuk” őket.
Összesen négy metódus felel ezért a viselkedésért, de ezek közül csak kettő igazán fontos: a __get__
és __set__
metódusok. (A másik kettő a __delete__
és a __set_name__
metódus, ezek leírása megtalálható dokumentációban.)
__get__
metódus¶Egy $P$ objektumnak a __get__
metódusa akkor kaphat szerepet, ha valamikor az adattag-elérés (pont operátor) a $P$ objektumot adná vissza eredményül. Ilyenkor meghívódik a $P$.__get__
metódus és a pont operátor ennek a metódushívásnak az eredményét fogja visszaadni (ha $P$-nek nem lett volna __get__
metódusa, akkor maga $P$ lett volna az eredmény).
Hasonlat a folyamat illusztrálására:
- Van raktároknak egy hosszú sora (az adattagok).
- Legtöbb raktárban ládák vannak (
__get__
metódus nélküli objektumok), de van néhány raktár, ahol ehelyett kigyúrt hordárok várakoznak (__get__
metódussal rendelkező objektumok).- Jön egy teherautó egy bizonyos raktárhoz a sorból (adattag-elérés operátor); a sofőr bekiabál, hogy „Hozzátok az árukat!” (megpróbálja meghívni a
__get__
metódust).- Ha hordárok voltak a raktárban, akkor azok kijönnek, összeszedik innen-onnan a cuccot és bepakolják a teherautóba, majd tovább várakoznak a raktárban (az eredmény az adattag
__get__
metódusának a visszatérési értéke).- Ellenkező esetben a sofőr kénytelen-kelletlen kiszáll és a raktárban lévő dolgokat bepakolja a teherautóba (az eredmény maga az adattag).
Fontos korlátozás: Ahogyan fentebb is említettük, csak az osztályok adattagjaként (C++ szóhasználattal nagyjából: static member) tárolt objektumoknak a __get__
metódusai kaphatnak szerepet; nem-osztály objektum adattagjaként (C++ szóhasználattal nagyjából: instance member) tárolt objektum __get__
metódusai nem kaphatnak szerepet.
A __get__
metódust mindig (egy+)kettő paraméterrel hívja meg a rendszer:
self
, a property-objektum)None
, amikor közvetlen osztálytól kérjük az adattagotA következő példa bemutatja, hogy milyen helyzetben mik ezek a paraméterek:
class Prop:
def __init__(self, content):
self.content = content
def __get__(self, inst, cls):
print("__get__ meghívva, paraméterek: inst =", inst, "cls =", cls)
return self.content
def __str__(self):
return "Prop object containing "+str(self.content)
class Base:
p1 = Prop("one")
class Deriv(Base):
p2 = Prop("two")
def __init__(self):
self.p3 = Prop("three")
#adattag-elérés osztályon keresztül:
print(Base.p1)
print(Deriv.p1)
print(Deriv.p2)
#adattag-elérés példányokon keresztül:
b = Base()
print(b.p1)
d = Deriv()
print(d.p1)
print(d.p2)
print(d.p3) #ez nem hívja meg a __get__ metódust, mert p3 nem egy osztálynak az adattagja
__set__
metódus¶Ha egy __set__
metódussal rendelkező $P$ objektum egy $C$ osztály adattagjaként (C++ szóhasználattal nagyjából: static member) van tárolva, akkor az „ügyintézőként” őrzi azt az adattag-nevet és intézkedik, ha a $C$ osztály egy $I$ példányának olyan nevű adattagjához rendel értéket egy utasítás.
Az objektum.adattag = valami
értékadás normális esetben az objektum
saját adattagjai között tárolná el a valami
értéket (az adattag
név alatt), de ez előtt megnézi, hogy objektum
típusa tárol-e adattag
név alatt egy __set__
metódussal rendelkező „ügyintézőt”. Ha van ilyen ügyintéző objektum, akkor (a normális ügymenet helyett) meghívódik annak a __set__
metódusa, ami ott tárolja el a kapott értéket, ahol akarja.
A __set__
metódus (egy+)kettő paraméterrel hívódik meg:
self
, a property-objektum)A következő kód csinál egy property-típust, ami típustesztelő adattagokat valósít meg ezt a két speciális metódust és a __set_name__
metódust használva (ami akkor hívódik meg, amikor az objektum egy osztálynak a statikus adattagjává válik valamilyen név alatt):
class IntMember:
def __set_name__(self, cls, name):
self.name = name
@property
def stored_as(self):
return "_"+self.name
def __get__(self, inst, cls):
if inst is None:
return self
return getattr(inst, self.stored_as)
def __set__(self, inst, value):
if not isinstance(value, int):
raise TypeError('the value of {} must be an integer'.format(self.name))
setattr(inst, self.stored_as, value)
class Point:
x = IntMember()
y = IntMember()
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return '({}, {})'.format(self.x, self.y)
p = Point(4,5)
print(p)
p.y = 8
print("p.y módosítása után:", p)
print("Az x koordináta valójában p._x-ben van tárolva, értéke = ", p._x)
p.x = "ez nem szám" # hibát okoz
Ahogyan eddig is folyamatosan használtam, Python-ban az osztályoknak vannak metódusaik, amelyeket az osztály törzsén belül kell definiálni, ugyanazzal a def
kulcsszóval, ami a hagyományos függvényeket is definiálja.
Valójában az a helyzet, hogy a függvény típus rendelkezik __get__
metódussal és emiatt ha egy osztálynak van egy függvény típusú adattagja és azt egy példányon keresztül érjük el, akkor a __get__
metódus „beilleszti” a példányt a függvény első paraméterének.
Például:
class C:
def f(self):
return 42
print("Osztályból elérve:", type(C.f), C.f("akármi"))
# C.f egy függvény – bármit megadhatunk első paraméterként
print("Ugyanez máshogy:", type(C.f.__get__(None, C)), C.f.__get__(None, C)("akármi"))
c = C()
print("Példányból elérve:", type(c.f), c.f())
print("Ugyanez máshogy:", type(C.f.__get__(c, C)), C.f.__get__(c, C)())
def make_pair(x, y):
"Ez egy teljesen közönséges függvény..."
return (x, y)
C.make_pair = make_pair #... amit eltárolunk az osztály adattagjaként
print("... és meghívunk metódusként:", c.make_pair("spam spam spam"))
A függvényeknek ezt a viselkedését könnyen utánozhatnánk, ha akarnánk:
from functools import partial
class MyMakePair:
"""A függvény típust _alaposan_ utánzó saját típus.
A függvényhívás operátor mellett a __get__ viselkedését
is utánozza."""
def __call__(self, x, y):
return (x, y)
def __get__(self, inst, cls):
if inst:
return partial(self, inst)
# inst beszúrása első paraméterként
else:
return self
class SomeClass:
custom_method = MyMakePair()
obj = SomeClass()
print(type(obj.custom_method))
print(obj.custom_method("spam spam spam"))
Pythonban is vannak statikus metódusok, ezeket – micsoda meglepetés – egy dekorátor segítségével vezetjük be:
import math
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
@staticmethod
def polar(r, angle):
return Point(r*math.cos(angle), r*math.sin(angle))
def __str__(self):
return f'({self.x}, {self.y})'
print(Point.polar(3, math.pi))
p = Point(5,3)
print("Also callable from instances:", p.polar(2,math.pi/4))
print("The real object:", Point.__dict__["polar"])
print("Getting from the class:", Point.polar)
print("Getting from an instance:", p.polar)
A staticmethod
típus C-ben van definiálva (és azonnal elérhető, nem kell hozzá importálni semmit), de Python-ban is könnyen megírható lenne:
class my_staticmethod:
def __init__(self, func):
self.func = func
def __get__(self, inst, cls):
return self.func
import math
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
@my_staticmethod
def polar(r, angle):
return Point(r*math.cos(angle), r*math.sin(angle))
def __str__(self):
return f'({self.x}, {self.y})'
print(Point.polar(3, math.pi))
p = Point(5,3)
print("Also callable from instances:", p.polar(2,math.pi/4))
print("The real object:", Point.__dict__["polar"])
print("Getting from the class:", Point.polar)
print("Getting from an instance:", p.polar)
Megjegyzés: az __str__
függvényben egy formázott sztringliterált lett alkalmazva. Ez egy frissen (Python 3.6-ban) bevezetett nyelvi elem, amivel tömören lehet leírni sztringek összebarkácsolását.
Python-ban van még egy metódusfajta, a classmethod
, ami az aktuális osztályt kapja meg paraméterként. Ennek a staticmethod
-hoz hasonló szerepe van, azonban „jól viselkedik” öröklődés során.
Egy olyan osztály, aminek csak statikus adattagjai és classmethod
-jai vannak, gyakorlatilag úgy viselkedik, mintha egy szingleton objektum lenne.
Az előző példaprogramban a staticmethod
-ot classmethod
-ra cserélve a következő lesz a program:
import math
class Point:
X = 42
def __init__(self, x, y):
self.x = x
self.y = y
@classmethod
def polar(cls, r, angle):
print(cls.X)
return cls(r*math.cos(angle), r*math.sin(angle))
def __str__(self):
return f'({self.x}, {self.y})'
class BarePoint(Point):
X = 67
def __str__(self):
return f'{self.x} {self.y}'
print(Point.polar(3, math.pi))
print(BarePoint.polar(3, math.pi))
p = BarePoint(5,3)
print("Also callable from instances:", p.polar(2,math.pi/4))
print("The real object:", Point.__dict__["polar"])
print("Getting from the class:", Point.polar)
print("Getting from another class:", BarePoint.polar)
print("Getting from an instance:", p.polar)
Házi feladat: implementáljuk ezt is tiszta Python-ban!