UnboundLocalError im Closure

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
Benutzeravatar
snafu
User
Beiträge: 6873
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Hallo Forum!

Im Rahmen eines kleinen Hobby-Projekts habe ich mir einen Validator gebastelt, der vom Benutzer entsprechend konfiguriert werden kann:

Code: Alles auswählen

def choice(low, high, converter=None):
    def validator(value):
        if converter is None:
            converter = type(low)
        converted = converter(value)
        if not low <= converted <= high:
            raise ValueError("Invalid choice")
        return converted
    return validator
Ich denke, das Vorgehen ist klar. Die zurück gelieferte Funktion wird dann einer anderen Funktion als Parameter übergeben und von ihr an der passenden Stelle aufgerufen, um eine Benutzereingabe zu überprüfen. An sich sollte das ja funktionieren, da im Closure weiterhin der ursprüngliche Namensraum gelten müsste. Zu meiner Überraschung wirft das aber einen Fehler:

Code: Alles auswählen

<ipython-input-3-9c9420254249> in validator(value)
      1 def choice(low, high, converter=None):
      2     def validator(value):
----> 3         if converter is None:
      4             converter = type(low)
      5         converted = converter(value)

UnboundLocalError: local variable 'converter' referenced before assignment
Es klappt aber, wenn ich den Namen in die Signatur des Closures ziehe: ``def validator(value, converter=converter):``. Mit dem low und high hat er keine Probleme. Warum ist das so? Liegt es am Keyword-Argument?
Benutzeravatar
snafu
User
Beiträge: 6873
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Hab die Lösung schon. Es liegt daran, dass ich in meinem if-Block den Namen ``converter`` neu belege. Wird hier ganz gut erklärt: https://stackoverflow.com/a/29639807
Benutzeravatar
__blackjack__
User
Beiträge: 14076
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Wenn ich keinen Konverter angebe hätte ich ja erwartet das auch nichts konvertiert wird. Was ist denn wenn sich der Datentyp von `low` gar nicht dafür eignet eine Konvertierung durchzuführen?

Code: Alles auswählen

def choice(low, high, converter=lambda value: value):
    def validator(value):
        converted = converter(value)
        if not low <= converted <= high:
            raise ValueError("Invalid choice")
        return converted
    return validator
`choice()` wäre nicht meine Wahl für den Namen, denn ob das nun eine Auswahl war die der Benutzer da übergibt, hängt ja vom Kontext ab wo der Wert her kommt.
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
Benutzeravatar
snafu
User
Beiträge: 6873
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

__blackjack__ hat geschrieben: Montag 10. Oktober 2022, 19:53 Wenn ich keinen Konverter angebe hätte ich ja erwartet das auch nichts konvertiert wird. Was ist denn wenn sich der Datentyp von `low` gar nicht dafür eignet eine Konvertierung durchzuführen?
Ich hatte das nicht erwähnt: Die Eingaben sind immer Strings. Es geht vorrangig darum, eine Menüauswahl zu realisieren oder Zahlen nur in einem bestimmten Bereich zu erlauben. Also sowas wie choice("a", "f") oder choice(1, 10).

Als Vorbelegung für den converter-Parameter schwanke ich gerade zwischen ``str`` und der von dir gezeigten Identitätsfunktion. Letzteres wäre mathematisch gesehen sauberer, ist aber auch etwas sperriger in der Lesbarkeit. str() verhält sich ja bei Strings im Endeffekt genau so (``str(s) is s``), wobei das wahrscheinlich ein Implementierungsdetail ist.

Ich wollte halt eigentlich vermeiden, dass man choice(1, 10, int) schreiben muss, auch wenn das automatische Ermittelns des Typs etwas magischer erscheint. Es ist andererseits auch eine Vereinfachung, weil man nicht jedes Mal daran denken müsste, am Ende die Typangabe zu machen.

Und um deine Frage zu beantworten: Wenn ein ungeeigneter Datentyp übergeben wird, dann sollte es mit einem TypeError krachen. Das wäre hier auch das von mir gewünschte Verhalten. Daher beschränke ich mich auf den ValueError bei der Ausnahmebehandlung.
Benutzeravatar
DeaD_EyE
User
Beiträge: 1244
Registriert: Sonntag 19. September 2010, 13:45
Wohnort: Hagen
Kontaktdaten:

Alternative:

Code: Alles auswählen

def choice(low, high, converter=None):
    converter = converter or type(low)
    
    def validator(value):
        converted = converter(value)
        if not low <= converted <= high:
            raise ValueError("Invalid choice")
        return converted

    return validator
Ansonsten gäbe es noch das Keyword nonlocal, dass so ähnlich wie global ist, sich aber auf die darüberliegende Funktion bezieht.
Kennen die wenigsten und wird kaum verwendet.
sourceserver.info - sourceserver.info/wiki/ - ausgestorbener Support für HL2-Server
Benutzeravatar
__blackjack__
User
Beiträge: 14076
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@snafu: Gerade das was Du vermeiden willst, klappt ja nicht mit allen Datentypen und es erscheint mir ein bisschen willkürlich, das der Code bei einem `low` Wert automagisch klappt dessen `__init__()` mit nur einem Argument aufgerufen werden kann, und dann auch noch mit dem Typ/Wert von `value` klar kommt, aber das nicht funktioniert wenn das zufälligerweise nicht zutrifft. Die Typen von `low` und in was konvertiert werden soll, müssen doch gar nicht auf diese Weise kompatibel sein. Einfache Fälle wären ich will ein `float` habe aber ganze Zahlen als Grenzen. Das ich dann die Wahl zwischen ``choice(1, 10, float)`` und ``choice(1.0, 10)`` habe ist mir zu schräg und zu subtil. Und wenn mir die Untergrenze egal ist, würde ich -unendlich einsetzen, müsste dann aber `int` als Konverter angeben wenn ich ganze Zahlen will. Während ich das nicht machen müsste wenn ich eine konkrete ganze Zahl als Untergrenze und +unendlich als Obergrenze angebe.
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
Benutzeravatar
snafu
User
Beiträge: 6873
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Habe mich unter Einbeziehung der berechtigten Einwände nun zu einem OOP-Ansatz entschieden:

Code: Alles auswählen

class Converter:
    def __init__(self, value_type):
        self.value_type = value_type

    def __call__(self, value):
        return self.convert(value)

    def convert(self, value):
        return self.value_type(value)


class Choice(Converter):
    def __init__(self, value_type, low, high, casefold=False):
        Converter.__init__(self, value_type)
        self.low = low
        self.high = high
        self.casefold = casefold

    def _get_operands(self, value):
        operands = [self.low, value, self.high]
        if self.casefold:
            operands = [op.casefold() for op in operands]
        return operands

    def convert(self, value):
        value = Converter.convert(self, value)
        low, choice, high = self._get_operands(value)
        if not low <= choice <= high:
            raise ValueError("Invalid choice")
        return choice
Ist natürlich mehr Code, aber dafür IMHO auch sauberer umgesetzt.
Sirius3
User
Beiträge: 18278
Registriert: Sonntag 21. Oktober 2012, 17:20

Die Klasse `Convert` ist ziemlich kompliziert `value_type` ausgedrückt.
Und ein Choice ist kein Converter. Die ist-ein-Beziehung durch die Vererbung ist daher falsch.
Die _get_operands-Methode ist auch sehr undurchsichtig, dadurch, dass da eine Liste erzeugt wird, und zwar aus einer Mischung aus Attributen und dem übergebenen value.
Die Casefold-Funktionalität sollte man besser in die Convertierfunktion einbauen.

Code: Alles auswählen

class Choice:
    def __init__(self, convert, low, high):
        self.convert = convert
        self.low = convert(low)
        self.high = convert(high)

    def __call__(self, value):
        value = self.convert(value)
        if not self.low <= value <= self.high:
            raise ValueError("Invalid choice")
        return value


choice = Choice(str.casefold, 'A', 'F')
Benutzeravatar
snafu
User
Beiträge: 6873
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

@Sirius3
Jepp, dein Code macht die Sache deutlich einfacher.

Danke für eure Hilfe!
Benutzeravatar
__blackjack__
User
Beiträge: 14076
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Wobei wir bei einer Klasse die nur eine `__init__()` und eine `__call__()` hat, ja eigentlich schon wieder bei einer Funktion wären die zur Klasse aufgepustet wurde. Im Grunde braucht man noch nicht einmal ein Closure schreiben wenn man `functools.partial()` nimmt um die Argumente zu binden.

Code: Alles auswählen

from functools import partial


def validate(convert, low, high, value):
    value = convert(value)
    if not low <= value <= high:
        raise ValueError("Invalid choice")
    return value


choice = partial(validate, str.casefold, "A", "F")
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
Benutzeravatar
snafu
User
Beiträge: 6873
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Und so schließt sich der Kreis:

Code: Alles auswählen

def choice(convert, low, high):
    def validate(user_string):
        value = convert(user_string)
        if not low <= value <= high:
            raise ValueError("Invalid choice")
        return value
    return validate
Antworten