Érdekességek Python programozásból – dekorátorok

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.

Python szintaxis

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:

  • A blokkokat { és } helyett beljebb kezdés jelöli (és egy kettőspont a blokkot nyitó utasítás végén).
  • Az utasítások végét a sorvége jelöli ki pontosvessző helyett.
  • Nem kell kiírni a változók típusát. A függvények definícióját def vezeti be.

Például:

In [1]:
def repeat(string):
    if "ni" in string:
        print('Aaargh!')
        return string*3
    else:
        return string*6

print(repeat("spam "))
print(repeat("ni "))
spam spam spam spam spam spam 
Aaargh!
ni ni 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:

In [2]:
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")
Sir Robin (42) 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.

Dekorátorok

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:

In [3]:
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:

In [4]:
@run_immediately
def greet():
    print("Üdvözöllek, dicső lovag!")
    
print("spam, spam, spam")
greet()
greet()
Üdvözöllek, dicső lovag!
spam, spam, spam
Üdvözöllek, dicső lovag!
Üdvözöllek, dicső lovag!

Á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):

In [5]:
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)            
<__main__.cached object at 0x7f96281bb7f0>
x értéke? abc
y értéke? def
['abc', 'def', 'abc']

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:

  • Ez a 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.
  • Itt találkoztunk még egy dupla-aláhúzásjeles névvel: a __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:

In [6]:
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)            
<function cached.<locals>.wrapper at 0x7f9628232e18>
x értéke? abc
y értéke? def
['abc', 'def', 'abc']

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:

In [7]:
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
<function ask_for_value at 0x7f962820a730>
Fontos metaadatok:
Név: ask_for_value
Dokumentáció: Így szokás dokumentációt írni Pythonban.

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):

In [8]:
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)]
Osztás eredménye: 1.0
Osztás eredménye: -2.0
Osztás eredménye: 0.125
Out[8]:
[1.0, -2.0, 0.125]

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:

In [9]:
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)]
Összeadás eredménye: 4
Összeadás eredménye: -2
Összeadás eredménye: 6
Out[9]:
[4, -2, 6]

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):

In [10]:
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)
True False True True False

Property-k használata

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:

In [11]:
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)
0.6435011087932844 5.0

Ezek most csak olvasható adattagként viselkednek:

In [12]:
p.r = 10
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-12-0c2e875cc88f> in <module>()
----> 1 p.r = 10

AttributeError: can't set attribute

... de definiálhatóak hozzájuk setterek is:

In [13]:
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)
8.0 6.0

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.)

Property-k megvalósításának részletei

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.)

Adattagok olvasása: a __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)
  • az a példány, aminek az adattagját kérjük illetve None, amikor közvetlen osztálytól kérjük az adattagot
  • az érintett osztály (aminek a példányának az adattagját illetve aminek az adattagját lekérjük)

A következő példa bemutatja, hogy milyen helyzetben mik ezek a paraméterek:

In [14]:
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
__get__ meghívva, paraméterek: inst = None cls = <class '__main__.Base'>
one
__get__ meghívva, paraméterek: inst = None cls = <class '__main__.Deriv'>
one
__get__ meghívva, paraméterek: inst = None cls = <class '__main__.Deriv'>
two
__get__ meghívva, paraméterek: inst = <__main__.Base object at 0x7f9628231d68> cls = <class '__main__.Base'>
one
__get__ meghívva, paraméterek: inst = <__main__.Deriv object at 0x7f9628231780> cls = <class '__main__.Deriv'>
one
__get__ meghívva, paraméterek: inst = <__main__.Deriv object at 0x7f9628231780> cls = <class '__main__.Deriv'>
two
Prop object containing three

Adattagok írása: a __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)
  • az az objektum, akinek az adattagjához értéket akarunk rendelni
  • az érték

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):

In [15]:
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
(4, 5)
p.y módosítása után: (4, 8)
Az x koordináta valójában p._x-ben van tárolva, értéke =  4
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-15-5598d77e7add> in <module>()
     28 print("p.y módosítása után:", p)
     29 print("Az x koordináta valójában p._x-ben van tárolva, értéke = ", p._x)
---> 30 p.x = "ez nem szám" # hibát okoz

<ipython-input-15-5598d77e7add> in __set__(self, inst, value)
     11     def __set__(self, inst, value):
     12         if not isinstance(value, int):
---> 13             raise TypeError('the value of {} must be an integer'.format(self.name))
     14         setattr(inst, self.stored_as, value)
     15 

TypeError: the value of x must be an integer

Metódusok

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:

In [16]:
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"))
Osztályból elérve: <class 'function'> 42
Ugyanez máshogy: <class 'function'> 42
Példányból elérve: <class 'method'> 42
Ugyanez máshogy: <class 'method'> 42
... és meghívunk metódusként: (<__main__.C object at 0x7f96281b22b0>, 'spam spam spam')

A függvényeknek ezt a viselkedését könnyen utánozhatnánk, ha akarnánk:

In [17]:
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"))
<class 'functools.partial'>
(<__main__.SomeClass object at 0x7f96281ae438>, 'spam spam spam')

Statikus és osztályhoz kötődő metódusok

Pythonban is vannak statikus metódusok, ezeket – micsoda meglepetés – egy dekorátor segítségével vezetjük be:

In [18]:
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)
(-3.0, 3.6739403974420594e-16)
Also callable from instances: (1.4142135623730951, 1.414213562373095)
The real object: <staticmethod object at 0x7f9628143358>
Getting from the class: <function Point.polar at 0x7f96281ac0d0>
Getting from an instance: <function Point.polar at 0x7f96281ac0d0>

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:

In [19]:
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)
(-3.0, 3.6739403974420594e-16)
Also callable from instances: (1.4142135623730951, 1.414213562373095)
The real object: <__main__.my_staticmethod object at 0x7f96281aee80>
Getting from the class: <function Point.polar at 0x7f96281a91e0>
Getting from an instance: <function Point.polar at 0x7f96281a91e0>

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:

In [20]:
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)
42
(-3.0, 3.6739403974420594e-16)
67
-3.0 3.6739403974420594e-16
67
Also callable from instances: 1.4142135623730951 1.414213562373095
The real object: <classmethod object at 0x7f96281490f0>
Getting from the class: <bound method Point.polar of <class '__main__.Point'>>
Getting from another class: <bound method Point.polar of <class '__main__.BarePoint'>>
Getting from an instance: <bound method Point.polar of <class '__main__.BarePoint'>>

Házi feladat: implementáljuk ezt is tiszta Python-ban!