Merkwürdigkeit mit Decorator

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
SeBu
User
Beiträge: 16
Registriert: Samstag 4. Januar 2014, 11:36

Hallo,

das Problem konnte ich lösen, allerdings hoffe ich das mir jemand sagen kann, warum die Lösung funktioniert hat... :oops:

Der Decorator soll die Funktion bei einer Exception neu starten (gedacht für eine API Abfrage mit unzuverlässigen Servern).

Mit "retryonfail_broken" bekomme ich die Fehlermeldung:

Code: Alles auswählen

UnboundLocalError: local variable 'attempts' referenced before assignment
Wenn allerdings mit

Code: Alles auswählen

loopcount = attempts
eine neue Variable eingesetzt wird funktioniert es plötzlich. :shock:

Wird

Code: Alles auswählen

attempts -= 1 
aus retryonfail_broken entfernt, geht es wieder und man bleibt in while gefangen.


Mich würde wirklich interessieren, ob jemand die Logik kennt die dahinter steckt und sie mir erklären kann. :)

Code: Alles auswählen

def retryonfail(attempts=5):
    def multitry(func):
        def connecttry(*args, **kwargs):
            returnresults = None
            successful = False
            loopcount = attempts # <-- Ohne das funktioniert es nicht, aber warum?
            while loopcount > 0:
                try:
                    returnresults = func(*args, **kwargs)
                    successful = True
                    break
                
                except Exception as e:
                    loopcount -= 1
            if successful:
                return returnresults                
            else:
                raise Exception("Das war wohl nix")
        return connecttry
    return multitry

def retryonfail_broken(attempts=5):
    def multitry(func):
        def connecttry(*args, **kwargs):
            returnresults = None
            successful = False
            while attempts > 0: # Hier kommt der UnboundLocalError. Aber nur, wenn weiter unten attempts -= 1 steht. Ansonsten kommt man in while rein.
                try:
                    returnresults = func(*args, **kwargs)
                    successful = True
                    break
                
                except Exception as e:
                    attempts -= 1 # <-- Ohne die Zeile wird "while" betreten und läuft weiter  :shock: 
            if successful:
                return returnresults                
            else:
                raise Exception("Das war wohl nix")
        return connecttry
    return multitry


@retryonfail_broken(2)
def testme(text):
    if text in "test":
        print(text)
    else:
        raise ValueError("Fehlerteufel")
        
testme("test")
Benutzeravatar
__blackjack__
User
Beiträge: 13079
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@SeBu: Wenn irgendwo innerhalb einer Funktion eine Zuweisung an einen Namen steht, dann ist das eine lokale Variable. `retryonfail_broken() hast Du in `connecttry()` eine Zuweisung an `attempts` und damit hat `connecttry()` eine lokale Variable `attempts` die eine andere ist als die in `retryonfail_broken()`.

Ich würde ja aus der ``while``-Schleife eine ``for``-Schleife machen statt `attempts` zu verändern und das `successful` ist überflüssig wenn man ``else`` zu der Schleife verwendet.

Von der API wäre es auch besser wenn da nicht irgendeine generische Ausnahme ausgelöst wird, sondern mindestens mal die letzte die tatsächlich aufgetreten ist. Sonst wird das lustig mit der Fehlersuche.

In sämlichen Funktionsnamen fehlern Unterstriche, dennesistdoofzulesenwennmanWorteeinfachsozusammenklebt.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
SeBu
User
Beiträge: 16
Registriert: Samstag 4. Januar 2014, 11:36

Dankeschön! :P

Stimmt, for reicht ja auch. :?
Es sind Funktionen ohne Rückgabewerte dabei, weswegen ich das else brauche.
Ich hab es zu einem Minimalbeispiel geändert. Tatsächlich wird auf die klassenspezifischen Exceptions der Library geprüft und alle Fehler in einer Liste gespeichert.
Ich gelobe Besserung bei den Namen, möge pep8 mit mir sein! :mrgreen:
Benutzeravatar
__blackjack__
User
Beiträge: 13079
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@SeBu: Was meinst Du mit Funktionen ohne Rückgabewerte? So etwas gibt es in Python nicht. Jeder Aufruf gibt was zurück. Wenn nicht explizit, dann implizit ein `None`.

Hier ein Lösungsansatz der die Eigenschaften wie Signatur, Docstring etc. der dekorierten Funktion erhält:

Code: Alles auswählen

#!/usr/bin/env python3
from decorator import decorator


class MultiException(RuntimeError):
    def __init__(self, message, exceptions):
        RuntimeError.__init__(self, message)
        self.exceptions = exceptions


def retry_on_fail(attempts=5):
    def caller(func, *args, **kwargs):
        errors = []
        for _ in range(attempts):
            try:
                return func(*args, **kwargs)
            except Exception as error:
                errors.append(error)

        raise MultiException("Das war wohl nix", errors)

    return decorator(caller)


@retry_on_fail(2)
def test_me(text):
    if text in "test":
        print(text)
    else:
        raise ValueError("Fehlerteufel")


test_me("test")
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Antworten