Matematikai Algoritmusok és Felfedezések II.

8. Dekorátorok

2021 November 4.

Vajon mi történik, ha ezt lefuttatjuk?

In [1]:
def test(n):
    if n>1:
        return test(n-1)+1
    if n==1:
        return 1

print(test(30))
print(test(300))
print(test(3000))
30
300
---------------------------------------------------------------------------
RecursionError                            Traceback (most recent call last)
<ipython-input-1-33457e184b3b> in <module>
      7 print(test(30))
      8 print(test(300))
----> 9 print(test(3000))

<ipython-input-1-33457e184b3b> in test(n)
      1 def test(n):
      2     if n>1:
----> 3         return test(n-1)+1
      4     if n==1:
      5         return 1

... last 1 frames repeated, from the frame below ...

<ipython-input-1-33457e184b3b> in test(n)
      1 def test(n):
      2     if n>1:
----> 3         return test(n-1)+1
      4     if n==1:
      5         return 1

RecursionError: maximum recursion depth exceeded in comparison
In [2]:
import sys
sys.setrecursionlimit(3500)
In [3]:
print(test(3000))
3000

Milyen gyorsan fut le a következő kód?

In [4]:
%%time
def fibo(n):
    if n>2:
        return fibo(n-1)+fibo(n-2)
    if n==1 or n==2: 
        return 1

print(fibo(10))
print(fibo(11))
print(fibo(38))
55
89
39088169
Wall time: 30.2 s

Fibo javítás:

In [5]:
my_cache={}

def fibo(n):
    if n in my_cache:
        #print("reading cache")
        return my_cache[n]
    if n>2:
        ans=fibo(n-1)+fibo(n-2)
       
    if n==1 or n==2:
        ans=1
    my_cache[n]=ans
    return ans
In [6]:
%%time
fibo(38)
Wall time: 0 ns
Out[6]:
39088169

Még mindig nem az igazi. Miért nem menti el a python magától az eredményeket?

Egy függvény kimenetele feltétlen csak a bemenetektől függ! Így nem menthetők el az eredmények. Viszont megkérhetjük a pythont, hogy mentsen.

In [8]:
from functools import lru_cache
In [9]:
@lru_cache(10000)
def fibo(n):
    if n>2:
        return fibo(n-1)+fibo(n-2)
    if n==1 or n==2:
        return 1
In [10]:
%%time
fibo(1000)
Wall time: 0 ns
Out[10]:
43466557686937456435688527675040625802564660517371780402481729089536555417949051890403879840079255169295922593080322634775209689623239873322471161642996440906533187938298969649928516003704476137795166849228875
In [13]:
fibo_called=0

@lru_cache(10000)
def fibo(n):
    global fibo_called
    fibo_called=fibo_called+1
    if n>2:
        return fibo(n-1)+fibo(n-2)
    if n==1 or n==2:
        return 1
In [14]:
fibo(10)
fibo_called
Out[14]:
10

Cache

Mentsük el azokat az eredményeket, melyek csak a bemenettől függenek. A gond, hogy ezekből túl sok van, ezért néha törölni kell.

Különböző stratégiák léteznek arra, hogy mit dobjunk ki, ha már túl sokat mentettünk el:

név törölni
First-In/First-Out (FIFO) A legrégebben elmentett elemet
Last-In/First-Out (LIFO) A legfrissebben elmentett elemet
Least Recently Used (LRU) A legrégebben használt elmentett elemet
Most Recently Used (MRU) A legfrissebben használt elmentett elemet
Least Frequently Used (LFU) A legkevésbé gyakran használt elemet

És természetesen ezek mindenféle kombinációja is hasznos lehet. Például idő és tárhely közös figyelembevételével való törlés.

Python függvények

Python a függvények "first class citizen"-ek. Ez azt jelenti, hogy ugyanolyan objektumok, mint bármi más.

In [15]:
def negyzet(n):
    return(n**2)

print(negyzet(10))
ez_is_negyzet=negyzet
ez_is_negyzet(11)     # ez is meghívató függvényként
100
Out[15]:
121
In [16]:
def map_to_list(f,lista):   # át lehet adni egy függvényt
    ans=[]
    for i in lista:
        ans.append(f(i))
    return ans

map_to_list(negyzet,[1,2,3,4])
Out[16]:
[1, 4, 9, 16]

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.

In [17]:
def first_decorator(func):
    def inner(x, y):
        print("< Függvényhívás előtt")
        func(x, y)
        print("< Függvényhívás után")

    return inner


def foo(x, y):
    
    print("A paraméterek: ", x, y)



method = first_decorator(foo)
method("Run", 120)
< Függvényhívás előtt
A paraméterek:  Run 120
< Függvényhívás után
In [18]:
def first_decorator(func):
    def inner(x, y):
        print("< Függvényhívás előtt")
        func(x, y)
        print("< Függvényhívás után")

    return inner

@first_decorator
def foo(x, y):
    print("A paraméterek: ", x, y)

    
def foo2(x, y):
    print("A paraméterek: ", x, y)


# Dekoralt funkcio meghivasa
foo("First run", 100)

method = first_decorator(foo2)
method("Second run", 120)
< Függvényhívás előtt
A paraméterek:  First run 100
< Függvényhívás után
< Függvényhívás előtt
A paraméterek:  Second run 120
< Függvényhívás után

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)

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 [19]:
def run_immediately(func):
    func()
    return func
In [20]:
@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!

Felhasználás 1

Általában azonban olyan dolgokat akarunk dekorátorként használni, amelyek valahogy módosítják az éppen definiált függvényt.

In [21]:
def cached(func):
    cache = {}
    def wrapper(arg):
        try:
            return cache[arg]
        except KeyError:
            result =  func(arg)
            cache[arg] = result
            return result
    return wrapper
In [22]:
@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 0x000001CE556F9E58>
x értéke? 1
y értéke? 34
['1', '34', '1']

Felhasználás 2

Vegyük észre, hogy a függvény 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 [23]:
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
In [24]:
@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 0x000001CE55709168>
Fontos metaadatok:
Név: ask_for_value
Dokumentáció: Így szokás dokumentációt írni Pythonban.

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.

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:

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

Felhasználás 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):

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

Felhasználás 4

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 [29]:
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 [30]:
p.r = 10
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-30-0c2e875cc88f> in <module>
----> 1 p.r = 10

AttributeError: can't set attribute

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

In [32]:
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ó).

Mire jó ez az egész?

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.

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

Felhasználás 5

Típus ellenőrzés

def myMethod(ID, name):
    if not (myIsType(ID, 'uint') and myIsType(name, 'utf8string')):
        raise BlaBlaException() ...


@accepts(uint, utf8string)
def myMethod(ID, name):

Fehasználás 6. Függvény regisztrálás, jogosultságok

Discord bot

@client.event
async def on_ready():
    guild = discord.utils.get(client.guilds, name=GUILD)
    print(
        f'{client.user} is connected to the following guild:\n'
        f'{guild.name}(id: {guild.id})'
    )
bot = commands.Bot(command_prefix='!')

@bot.command(name='99')
async def nine_nine(ctx):
    brooklyn_99_quotes = [
        'I\'m the human form of the 💯 emoji.',
        'Bingpot!',
        (
            'Cool. Cool cool cool cool cool cool cool, '
            'no doubt no doubt no doubt no doubt.'
        ),
    ]

    response = random.choice(brooklyn_99_quotes)
    await ctx.send(response)
@bot.command(name='roll_dice', help='Simulates rolling dice.')
async def roll(ctx, number_of_dice, number_of_sides):
    dice = [
        str(random.choice(range(1, number_of_sides + 1)))
        for _ in range(number_of_dice)
    ]
    await ctx.send(', '.join(dice))
@bot.command(name='create-channel')
@commands.has_role('admin')
async def create_channel(ctx, channel_name='real-python'):
    guild = ctx.guild
    existing_channel = discord.utils.get(guild.channels, name=channel_name)
    if not existing_channel:
        print(f'Creating a new channel: {channel_name}')
        await guild.create_text_channel(channel_name)

Felhasználás 7. logolás

import functools

def log(logger, level='info'):
    def log_decorator(fn):
        @functools.wraps(fn)
        def wrapper(*a, **kwa):
            getattr(logger, level)(fn.__name__)
            return fn(*a, **kwa)
        return wrapper
    return log_decorator

# later that day ...
@log(logging.getLogger('main'), level='warning')
def potentially_dangerous_function(times):
    for _ in xrange(times): rockets.get_rocket(NUCLEAR=True).fire()

Felhasználás 8. Mérések

Mennyi ideig fut egy függvény? Mennyi memóriát használunk? Stb...

In [34]:
from functools import wraps
import tracemalloc
from time import perf_counter 


def measure_performance(func):
    '''Measure performance of a function'''

    @wraps(func)
    def wrapper(*args, **kwargs):
        tracemalloc.start()
        start_time = perf_counter()
        func(*args, **kwargs)
        current, peak = tracemalloc.get_traced_memory()
        finish_time = perf_counter()
        print(f'Function: {func.__name__}')
        print(f'Method: {func.__doc__}')
        print(f'Memory usage:\t\t {current / 10**6:.6f} MB \n'
              f'Peak memory usage:\t {peak / 10**6:.6f} MB ')
        print(f'Time elapsed is seconds: {finish_time - start_time:.6f}')
        print(f'{"-"*40}')
        tracemalloc.stop()
    return wrapper
In [35]:
@measure_performance
def make_list1():
    '''Range'''

    my_list = list(range(100000))


@measure_performance
def make_list2():
    '''List comprehension'''

    my_list = [l for l in range(100000)]
    
@measure_performance
def make_list3():
    '''Append'''
 
    my_list = []
    for item in range(100000):
        my_list.append(item)


@measure_performance
def make_list4():
    '''Concatenation'''

    my_list = []
    for item in range(100000):
        my_list = my_list + [item]
In [36]:
print(make_list1())
print(make_list2())
print(make_list3())
print(make_list4())
Function: make_list1
Method: Range
Memory usage:		 0.000904 MB 
Peak memory usage:	 3.693756 MB 
Time elapsed is seconds: 0.081127
----------------------------------------
None
Function: make_list2
Method: List comprehension
Memory usage:		 0.148078 MB 
Peak memory usage:	 3.765826 MB 
Time elapsed is seconds: 0.100013
----------------------------------------
None
Function: make_list3
Method: Append
Memory usage:		 0.000000 MB 
Peak memory usage:	 3.617236 MB 
Time elapsed is seconds: 0.107031
----------------------------------------
None
Function: make_list4
Method: Concatenation
Memory usage:		 0.147821 MB 
Peak memory usage:	 4.540657 MB 
Time elapsed is seconds: 48.821504
----------------------------------------
None

Felhasználás 9

Saját függvény definiálható egy osztállyal is:

In [37]:
class MyFunc:

   
    def __call__(self, *args, **kwargs):
        return 12
In [38]:
f=MyFunc()
In [39]:
f()
Out[39]:
12
In [41]:
import requests


class LimitQuery:

    def __init__(self, func):
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.limit = args[0]
        if self.count < self.limit:
            self.count += 1
            return self.func(*args, **kwargs)
        else:
            print(f'No queries left. All {self.count} queries used.')
            return
In [47]:
@LimitQuery
def get_coin_price(limit):
    '''View the Bitcoin Price Index (BPI)'''
    
    url = requests.get('https://api.coindesk.com/v1/bpi/currentprice.json')

    if url.status_code == 200:
        text = url.json()
        return f"${float(text['bpi']['USD']['rate_float']):.2f}"


print(get_coin_price(5))
print(get_coin_price(5))
print(get_coin_price(5))
print(get_coin_price(5))
print(get_coin_price(5))
print(get_coin_price(5))
$61635.11
$61635.11
$61635.11
$61635.11
$61635.11
No queries left. All 5 queries used.
None