Decorator um Funktionsargumente einzufügen?

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
p90
User
Beiträge: 198
Registriert: Donnerstag 22. Juli 2010, 17:30

Hi,

vorne weg. Bin leider etwas unbedarft was Pythons Decorators angeht und versuche nun
seit mehreren Stunden meinen Kopf drum zu wickeln. Kann also sein, dass ich hier etwas total blödes versuche
zu dem Decorators nicht geeignet sind und ich es einfach nicht verstehe.

Nun zum Problem:
Ich möchte mehrere Funktionen mit einem partiell gleichen Argumentenaufruf austatten also:

Code: Alles auswählen

func1(a,b, common):
    #do stuff with a,b
    #do common stuff with common

func2(c, d, common):
    #do different stuff with c,d
    #do common stuff with common
Um nun nicht überall Code der das selbe tut hinzuschreiben, dachte ich, dass man einen
Decorator verwenden müsse um dieses Problem zu lösen und man am ende nur noch schreiben muss:

Code: Alles auswählen

def wrapper(common):
    #do common stuff

@wrapper
func1(a, b):
    #do ab stuff

@wrapper
func2(c, d):
    #do c d stuff
Nun Wrappt ein Decorator aber nur, ich würde also bei:

Code: Alles auswählen

func1(a,b,common)
->wrapper(func1)(a, b, common)
statt:
->wrapper(common)(func1)(a, b)
landen.
Wie komme ich nun von wrapper auf common, entferne common aus den *args und **kwargs (und wenn ich das richtig sehe müsste man ja auch noch vlt. gesetzte Default-Werte entfernen die aber von dem was ich gelesen habe nur als Tuple vorliegen und nicht den Namen der Variable enthalten)? An die gesetzten Variablen sollte ich ja wie hier kommen https://wiki.python.org/moin/PythonDeco ... _Arguments aber damit habe ich ja noch nicht die möglichen Defaultwerte oder?

Noch etwas zu common:
Letztendlich geht es hier um eine Retry funktion wie in http://www.python-forum.de/viewtopic.ph ... ry#p242410 nur das ich die Parameter für die Retry Funktion beim Aufrufen der Funktion haben möchte statt über der Funktionsdefinition.

Entschuldigt bitte, wenn ich etwas verwirrt klinge aber irgendwie verstehe ich gerade nicht wirklich viel :(
Vlt. kann mich ja jemand etwas erleuchten
Freue mich auf Antworten und ein frohes neues Jahr!

p91
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

Dein Beispiel ist in Bezug auf Decoratoren unpassend, da Du nicht erwähnst, was ``common`` eigentlich so tut. Ist das eine Funktion, die immer nach ``func1`` und ``func2`` aufgerufen werden soll? Benötigt diese Parameter, die sich aus irgendwelchen Berechnungen / irgendwie aus den Parametern der Funktionen ``func1`` und ``func2`` ergeben? Kann man die Funktionen ``func1`` und ``func2`` prinzipiell auch *ohne* die Abläufe aus ``common`` nutzen, oder gehört beides fest verdrahtet zusammen?
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
BlackJack

@p90: Soll es vielleicht so etwas hier werden?

Code: Alles auswählen

def decoration(func): 
    def wrapper(x, y, common):
        func(x, y)
        # 
        # Do common stuff.
        # 
    return wrapper

@decoration
def func1(a, b):
    #do a, b stuff
 
@decoration
def func2(c, d):
    #do c, d stuff
p90
User
Beiträge: 198
Registriert: Donnerstag 22. Juli 2010, 17:30

Hi,

erst mal Danke für die Antworten:
Es soll wie gesagt eine Retry Funktion werden.
Also sowas in etwa:

Code: Alles auswählen

def func1(a,b, retries=5, wait_time=1):
    while retries:
        retries -= 1
        try:
            #do stuff with a,b
        except Exception:
             pass 

def func2(c,d, retries=5, wait_time=1):
    while retries:
        retries -= 1
        try:
            #do stuff with c,d 
        except Exception:
             pass 
hier wäre nun retries und wait_time das was zuvor unter common zusammen gefasst war.
Am Ende sollte es so aussehen:

Code: Alles auswählen


def wrapper(retries=5, wait_time=1)_ #defaultwerte falls nicht im Funktionsaufruf drin
    #logig für die retries

@wrapper
def func1(a,b):
    #do stuff with a,b

@wrapper
def func2(c,d):
    #do stuff with c,d

#aufruf func 1
func1(a,b) -> defaultwerte für den wrapper verwendet
func1(a,b, retries = 10) -> defaultwert für wait_time, retries ist 10
Hier ist die Retires Funktion noch ziemlich einfach, später will ich da halt mehrere Parameter drin haben die dann
automatisch für alle Dekorierten Funktionen zu Verfügung stehen.
Bisher habe ich nur Lösungen gesehen die es halt so machen:

Code: Alles auswählen

@wrapper(retries=10)
def func2(c,d):
    #do stuff with c,d
func2(c,d) -> retries ist immer 10
Soweit ich es nun verstehe muss ich einen wrapper schreiben der eine Funktion zurück gibt die die Parameter, die ich für den Wrapper brauche aus, *args und **kwargs rausholt (was schon schwierig werden kann weil die Funktionen alle unterschiedliche Parameter Zahl haben und die Defaultwerte ja erst irgendwo im Wrapper stehen und nicht in *args und **kwargs der dekorierten Funktion), dann die gesuchten Parameter korrekt entfernt (also alle Parameter nach dem ersten Wrapper Parameter in *args und alle Keywords die ich für den Wrapper brauche aus **kargs) und dann die nun veränderten *args und **kargs an die dekorierte Funktion übergibt.
BlackJack

@p90: Dann also so etwas hier?

Code: Alles auswählen

def retry(func):
    def wrapper(*args, **kwargs):
        retries = kwargs.pop('retries') if 'retries' in kwargs else 5
        wait_time = kwargs.pop('wait_time') if 'wait_time' in kwargs else 1
        for _ in xrange(retries):
            try:
                func(*args, **kwargs)
                time.sleep(wait_time)
            except Exception:
                # 
                # TODO Better log here.  Anything but swallowing the exception 
                # completely.
                # 
                pass
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

Ist zwar hübsch, was man so alles in Python umsetzen kann, aber ich frage mich hier, ob bei so einer generischen Anforderung der "Cross Cutting Concern" und sein Abfangen nicht zu sehr verschleiert werden... Als Benutzer der API habe ich ja keine Chance mehr, die inneren Funktionen auch separat zu benutzen.

Was spricht denn in diesem Falle für oder gegen eine Template-Funktion a la ``sorted``?
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
p90
User
Beiträge: 198
Registriert: Donnerstag 22. Juli 2010, 17:30

@BlackJack

ja, genau sowas, nur muss ich nicht auch noch in *args reingucken falls jemand

Code: Alles auswählen

func1(1,2,3)
statt

Code: Alles auswählen

func1(a = 1, b = 2, common = 3)
schreibt?
Und dann muss ich doch erst mal zählen wie viele Elemente *args hat, wie viele Paramater die Undekorierte Funktion func1 hat usw. oder?

@Hyperion
Kann natürlich sein, dass ich das hier gerade das Pferd von hinten aufziehe. Was meinst du den mit Template-Funktion?
In Python finde ich unter Templates nur String Templates die hier ja wohl nicht gemeint sind. Oder meinst du die C++ Templates?
Also eine Funktion die eine Funktion und deren Argumente als Argument bekommt und genau dann diese Funktion wrapped?
Also so?

Code: Alles auswählen

Retry(func1, args, retry= 5, wait_time=10)
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

p90 hat geschrieben: @Hyperion
Kann natürlich sein, dass ich das hier gerade das Pferd von hinten aufziehe. Was meinst du den mit Template-Funktion?
...
Also eine Funktion die eine Funktion und deren Argumente als Argument bekommt und genau dann diese Funktion wrapped?
Also so?

Code: Alles auswählen

Retry(func1, args, retry= 5, wait_time=10)
Ja, so in der Art meinte ich das. Es gibt dazu auch ein "formales" Muster der GoF. In Python (und vielen anderen Sprachen) braucht man dafür natürlich keine Klassen, da Funktionen in Python wie jedes andere Objekt behandelt werden und man sie ergo als Parameter an andere Funktionen übergeben kann. Das nennt man auch Higher order functions. Sortierfunktionen sind da immer ein beliebtes Beispiel aus der Praxis, weil man bei diesen gerne die Vergleichsfunktionalität offen für beliebige Objekte halten möchte (Wie sortiere ich eine Liste von Elefanten?). Anstelle für jeden denkbaren Typen nun immer wieder einen Sortieralgorithmus zu schreiben (PHP bevorzugt(e) diesen Unsinn iirc), schreibt man lediglich einen, dem man die Vergleichsoperation mitgeben kann, wenn diese keinem "Standard" entspricht.

Wie gesagt, es mag auf den Anwendungsfall ankommen, was nun besser geeignet ist. Will man die Funktionen wirklich immer dekorieren? Braucht man sie auch mal undekoriert? Oder soll man diese gar niemals undekoriert nutzen? Das sind sicherlich Fragen, die man klären muss.
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
BlackJack

@p90: Ich bin davon ausgegangen das die Retry-Argumente grundsätzlich als Schlüsselwortargumente übergeben werden müssen. Ansonsten wird es richtig hässlich und eventuell auch mehrdeutig. Ich finde das sowieso schon nicht so schön das dort in einer Argumentliste Argumente von zwei verschiedenen Funktionen vermengt werden.

Ich denke Dein letztes Beispiel meint Hyperion. Ist auch sauberer bezüglich der Argumente.
p90
User
Beiträge: 198
Registriert: Donnerstag 22. Juli 2010, 17:30

Hi,

ich glaube, ich habs jetzt:
Habe zuerst eine Funktion Retry wie von Hyperion vorgeschlagen definiert und dank BlackJacks Beispiel dann kapiert wie ich daraus einen Dekorator baue.
Das ganze sieht so aus (und hoffentlich habe ich keinen all zu großen Mist gebaut :) )

Code: Alles auswählen

from time import sleep
def example_hook(try_number, exception):
    """Return True to stop retrying"""
    pass

class MultipleExceptions(Exception):
    def __init__(self, exceptions):
        self.exceptions = exceptions
    def __str__(self):
        tmp = []
        for e in self.exceptions:
            tmp.append(str(e))
        return "\n".join(tmp)

class RetriesFailed(MultipleExceptions):
    """Helper Class as we somehow have to raise 
    multiple exceptions as every try may raise one."""
    pass

def retry(*args, function, tries=1, timeout=0, caught_exceptions = (Exception,), exception_hook = None, **kwargs):
    exception_list = []
    while tries > 0:
        try:
            return function(*args, **kwargs)
        except caught_exceptions as E:
            exception_list.append(E)
            if exception_hook is not None and exception_hook(tries, E): break
        tries -= 1
        if tries:
            sleep(timeout)
    else:
        raise RetriesFailed(exception_list)

def decoretry(func):
    def wrapper(*args, **kwargs):
        return retry(function = func, *args, **kwargs)
    return wrapper

@decoretry
def foobar(a):
    print(a)
[EDIT]
Da war noch ein "not" zu viel.
Antworten