Deco­ra­tors bie­ten eine neue und bequeme Mög­lich­keit für alles, vom Caching bis zum Sen­den von Benachrichtigungen.

Zunächst ist es das Ziel eines jeden Ent­wick­lers, die Dinge zum Lau­fen zu brin­gen. Lang­sam machen wir uns Gedan­ken über Les­bar­keit und Ska­lier­bar­keit. Das ist der Zeit­punkt, an dem wir anfan­gen, über Deko­ra­to­ren nachzudenken.

Deko­ra­to­ren sind eine her­vor­ra­gende Mög­lich­keit, einer Funk­tion zusätz­li­ches Ver­hal­ten zu ver­lei­hen. Und es gibt kleine Dinge, die wir Daten­wis­sen­schaft­ler oft in eine Funk­ti­ons­de­fi­ni­tion ein­bauen müssen.

Sie wer­den über­rascht sein, wie sehr Sie mit Deko­ra­to­ren die Wie­der­ho­lung von Code redu­zie­ren und die Les­bar­keit ver­bes­sern kön­nen. Ich habe es jeden­falls getan.

Hier sind die fünf häu­figs­ten, die ich bei fast jedem daten­in­ten­si­ven Pro­jekt verwende.

1. Der Wie­der­ho­lungs­de­ko­ra­tor
In Data-Sci­ence-Pro­jek­ten und Soft­ware­ent­wick­lungs­pro­jek­ten gibt es so viele Fälle, in denen wir von exter­nen Sys­te­men abhän­gig sind. Wir haben nicht immer die Kon­trolle über die Dinge

Wenn ein uner­war­te­tes Ereig­nis ein­tritt, möch­ten wir viel­leicht, dass unser Code eine Weile war­tet, damit das externe Sys­tem sich selbst kor­ri­gie­ren und den Vor­gang wie­der­ho­len kann.

Ich ziehe es vor, diese Wie­der­ho­lungs­lo­gik in einem Python-Deko­ra­tor zu imple­men­tie­ren, so dass ich jede Funk­tion mit Anmer­kun­gen ver­se­hen kann, um das Wie­der­ho­lungs­ver­hal­ten anzuwenden.

Hier ist der Code für einen Retry-Dekorator.

import time
from functools import wraps
def retry(max_tries=3, delay_seconds=1):
    def decorator_retry(func):
        @wraps(func)
        def wrapper_retry(*args, **kwargs):
            tries = 0
            while tries < max_tries:
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    tries += 1
                    if tries == max_tries:
                        raise e
                    time.sleep(delay_seconds)
        return wrapper_retry
    return decorator_retry
@retry(max_tries=5, delay_seconds=2)
def call_dummy_api():
    response = requests.get("https://jsonplaceholder.typicode.com/todos/1")
    return response

In dem obi­gen Code ver­su­chen wir, eine API-Ant­wort zu erhal­ten. Wenn dies fehl­schlägt, ver­su­chen wir die­selbe Auf­gabe 5 Mal erneut. Zwi­schen jedem erneu­ten Ver­such war­ten wir 2 Sekun­den lang.

2. Zwi­schen­spei­che­rung von Funk­ti­ons­er­geb­nis­sen
Einige Teile unse­rer Code­ba­sis ändern ihr Ver­hal­ten nur sel­ten. Den­noch kann dies einen gro­ßen Teil unse­rer Rechen­leis­tung in Anspruch neh­men. In sol­chen Situa­tio­nen kön­nen wir einen Deko­ra­tor ver­wen­den, um Funk­ti­ons­auf­rufe zwischenzuspeichern.

Die Funk­tion wird nur ein­mal aus­ge­führt, wenn die Ein­ga­ben iden­tisch sind. Bei jedem wei­te­ren Durch­lauf wer­den die Ergeb­nisse aus dem Zwi­schen­spei­cher geholt. Daher müs­sen wir nicht stän­dig teure Berech­nun­gen durchführen.

def memoize(func):
    cache = {}
    def wrapper(*args):
        if args in cache:
            return cache[args]
        else:
            result = func(*args)
            cache[args] = result
            return result
    return wrapper

Der Deco­ra­tor ver­wen­det ein Wör­ter­buch, spei­chert die Funk­ti­ons-Args und gibt Werte zurück. Wenn wir diese Funk­tion aus­füh­ren, prüft der Deko­ra­tor das Wör­ter­buch auf vor­he­rige Ergeb­nisse. Die eigent­li­che Funk­tion wird nur auf­ge­ru­fen, wenn vor­her kein Wert gespei­chert wurde.

Das fol­gende Bei­spiel ist eine Fibo­nacci-Zahl, die eine Funk­tion berech­net. Da es sich um eine wie­der­keh­rende Funk­tion han­delt, wird die­selbe Funk­tion mehr­mals auf­ge­ru­fen. Mit Caching kön­nen wir die­sen Pro­zess jedoch beschleunigen.

@memoize
def fibonacci(n):
    if n <= 1:
        return n
    else:
        return fibonacci(n-1) + fibonacci(n-2)

Hier sind die Aus­füh­rungs­zei­ten für diese Funk­tion mit und ohne Caching. Beach­ten Sie, dass die zwi­schen­ge­spei­cherte Ver­sion nur den Bruch­teil einer Mil­li­se­kunde zur Aus­füh­rung benö­tigt, wäh­rend die nicht zwi­schen­ge­spei­cherte Ver­sion fast eine Minute benötigte.

Function slow_fibonacci took 53.05560088157654 seconds to run.
Function fast_fibonacci took 7.772445678710938e-05 seconds to run.

Die Ver­wen­dung eines Wör­ter­buchs zur Spei­che­rung von Daten aus frü­he­ren Aus­füh­run­gen ist ein ein­fa­cher Ansatz. Es gibt jedoch eine aus­ge­feil­tere Methode zum Spei­chern von Caching-Daten. Sie kön­nen eine In-Memory-Daten­bank, wie Redis, verwenden.

3. Timing-Funk­tio­nen
Die­ser Punkt ist keine Über­ra­schung. Wenn wir mit daten­in­ten­si­ven Funk­tio­nen arbei­ten, wol­len wir unbe­dingt wis­sen, wie lange die Aus­füh­rung dauert.

Nor­ma­ler­weise wer­den dazu zwei Zeit­stem­pel gesam­melt, einer am Anfang und einer am Ende der Funk­tion. Wir kön­nen dann die Dauer berech­nen und sie zusam­men mit den Rück­ga­be­wer­ten ausgeben.

Aber dies immer wie­der für meh­rere Funk­tio­nen zu tun, ist sehr mühsam.

Statt­des­sen kön­nen wir das von einem Deko­ra­tor erle­di­gen las­sen. Wir kön­nen jede Funk­tion, die eine Dauer aus­ge­ben muss, mit einem Kom­men­tar versehen.

Hier ist ein Bei­spiel für einen Python-Deko­ra­tor, der die Lauf­zeit einer Funk­tion aus­gibt, wenn sie auf­ge­ru­fen wird:

import time

def timing_decorator(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"Function {func.__name__} took {end_time - start_time} seconds to run.")
        return result
    return wrapper

Sie kön­nen die­sen Deko­ra­tor ver­wen­den, um die Aus­füh­rung einer Funk­tion zu verzögern:

@timing_decorator
def my_function():
    # some code here
    time.sleep(1)  # simulate some time-consuming operation
    return

Der Auf­ruf der Funk­tion würde die Zeit aus­ge­ben, die sie zur Aus­füh­rung benötigt.

my_function()

>>> Function my_function took 1.0019128322601318 seconds to run.

4. Funk­ti­ons­auf­rufe pro­to­kol­lie­ren
Die­ser Deko­ra­tor ist eine Erwei­te­rung des vor­he­ri­gen Deko­ra­tors. Aber er hat einige beson­dere Anwen­dun­gen.
Wenn Sie die Prin­zi­pien des Soft­ware-Designs befol­gen, wer­den Sie das Prin­zip der ein­zi­gen Ver­ant­wor­tung zu schät­zen wis­sen. Dies bedeu­tet im Wesent­li­chen, dass jede Funk­tion eine und nur eine Ver­ant­wor­tung hat.

Wenn Sie Ihren Code auf diese Weise gestal­ten, möch­ten Sie auch die Aus­füh­rungs­in­for­ma­tio­nen Ihrer Funk­tio­nen pro­to­kol­lie­ren. Hier kom­men die Log­ging-Deko­ra­to­ren ins Spiel.

Das fol­gende Bei­spiel ver­an­schau­licht dies.

import logging
import functools

logging.basicConfig(level=logging.INFO)

def log_execution(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        logging.info(f"Executing {func.__name__}")
        result = func(*args, **kwargs)
        logging.info(f"Finished executing {func.__name__}")
        return result
    return wrapper

@log_execution
def extract_data(source):
    # extract data from source
    data = ...

    return data

@log_execution
def transform_data(data):
    # transform data
    transformed_data = ...

    return transformed_data

@log_execution
def load_data(data, target):
    # load data into target
    ...

def main():
    # extract data
    data = extract_data(source)

    # transform data
    transformed_data = transform_data(data)

    # load data
    load_data(transformed_data, target)

Der obige Code ist eine ver­ein­fachte Ver­sion einer ETL-Pipe­line. Wir haben drei sepa­rate Funk­tio­nen, um jedes Extra­hie­ren, Trans­for­mie­ren und Laden zu behan­deln. Wir haben jede von ihnen mit unse­rem log_execution Deko­ra­tor umhüllt.

Wenn der Code nun aus­ge­führt wird, sehen Sie eine Aus­gabe ähn­lich der folgenden:

INFO:root:Executing extract_data
INFO:root:Finished executing extract_data
INFO:root:Executing transform_data
INFO:root:Finished executing transform_data
INFO:root:Executing load_data
INFO:root:Finished executing load_data

Wir könn­ten auch die Aus­füh­rungs­zeit inner­halb die­ses Deko­ra­tors dru­cken las­sen. Aber ich würde sie gerne beide in sepa­ra­ten Deko­ra­to­ren haben. Auf diese Weise kann ich wäh­len, wel­chen (oder beide) ich für eine Funk­tion ver­wen­den möchte.

Hier ist, wie man meh­rere Deko­ra­to­ren für eine ein­zige Funk­tion verwendet.

@log_execution
@timing_decorator
def my_function(x, y):
    time.sleep(1)
    return x + y

5. Benach­rich­ti­gungs­de­ko­ra­tor
Schließ­lich ist ein sehr nütz­li­cher Deko­ra­tor in Pro­duk­ti­ons­sys­te­men der Benachrichtigungsdekorator.

Auch hier gilt, dass selbst bei meh­re­ren Wie­der­ho­lungs­ver­su­chen selbst eine gut getes­tete Code­ba­sis ver­sagt. Und wenn das pas­siert, müs­sen wir jeman­den dar­über infor­mie­ren, um schnell han­deln zu können.

Das ist nicht neu, wenn Sie jemals eine Daten­pipe­line gebaut haben und gehofft haben, dass sie für immer gut funktioniert.

Der fol­gende Deco­ra­tor sen­det eine E‑Mail, wenn die Aus­füh­rung der inne­ren Funk­tion fehl­schlägt. Es muss in Ihrem Fall nicht unbe­dingt eine E‑Mail-Benach­rich­ti­gung sein. Sie kön­nen ihn so kon­fi­gu­rie­ren, dass er eine Team­s/S­lack-Benach­rich­ti­gung sendet.

import smtplib
import traceback
from email.mime.text import MIMEText

def email_on_failure(sender_email, password, recipient_email):
    def decorator(func):
        def wrapper(*args, **kwargs):
            try:
                return func(*args, **kwargs)
            except Exception as e:
                # format the error message and traceback
                err_msg = f"Error: {str(e)}\n\nTraceback:\n{traceback.format_exc()}"
                
                # create the email message
                message = MIMEText(err_msg)
                message['Subject'] = f"{func.__name__} failed"
                message['From'] = sender_email
                message['To'] = recipient_email
                
                # send the email
                with smtplib.SMTP_SSL('smtp.gmail.com', 465) as smtp:
                    smtp.login(sender_email, password)
                    smtp.sendmail(sender_email, recipient_email, message.as_string())
                    
                # re-raise the exception
                raise
                
        return wrapper
    
    return decorator

@email_on_failure(sender_email='your_email@gmail.com', password='your_password', recipient_email='recipient_email@gmail.com')
def my_function():
    # code that might fail

Fazit
Deco­ra­tors sind eine sehr bequeme Mög­lich­keit, unse­ren Funk­tio­nen ein neues Ver­hal­ten zu ver­lei­hen. Ohne sie gibt es eine Menge Code-Wiederholungen.

In die­sem Bei­trag habe ich meine am häu­figs­ten ver­wen­de­ten Deko­ra­to­ren bespro­chen. Sie kön­nen diese für Ihre spe­zi­el­len Bedürf­nisse erwei­tern. Zum Bei­spiel kön­nen Sie einen Redis-Ser­ver ver­wen­den, um Cache-Ant­wor­ten anstelle von Wör­ter­bü­chern zu spei­chern. Dadurch erhal­ten Sie mehr Kon­trolle über die Daten, z. B. über die Per­sis­tenz. Oder Sie könn­ten den Code so anpas­sen, dass die War­te­zeit im Retry-Deko­ra­tor schritt­weise erhöht wird.

In allen mei­nen Pro­jek­ten ver­wende ich eine Ver­sion die­ser Deko­ra­to­ren. Obwohl sich ihr Ver­hal­ten leicht unter­schei­det, sind dies die gemein­sa­men Ziele, für die ich Deko­ra­to­ren häu­fig verwende.

Ich hoffe, die­ser Bei­trag hilft Ihnen.

Quelle: medium

Erfah­ren Sie mehr über Lösun­gen im Bereich Data Manage­ment oder besu­chen Sie eines unse­rer kos­ten­lo­sen Web­i­nare.