Pythons »any« und »all« mal schön »lazy«

Pythons eingebaute Funktionen »any« und »all« sind tolle Hilfsmittel für das Entwickeln von gut les- und wartbarem Code. Ich möchte nicht mehr auf sie verzichten, auch, wenn sie ein winziges Problem mit sich bringen: Sie untergraben die Idee der Short Circuit Evaluation.

Man stelle sich folgendes einfaches Beispiel vor:

x = None

if x is not None and len(x) > 10:
    print('ok')
else:
    print('nah')

Korrekterweise wird hier nah zurückgegeben. Durch die sogenannte Short Circuit Evaluation bricht die Auswertung der Kriterien bereits beim ersten Kriterium ab, denn dies ist False und so kann eine And-Verknüfung niemals mehr True werden.

Dieses Verhalten setzen wir bewusst ein und setzen somit voraus, dass es auch so funktioniert.

Bei der einfachen Verknüpfung von zwei Bedingungen ist das noch sehr überschaubar. Muss man nun aber aus irgendeinem Grund mehrere Bedingungen verknüpfen, werden solche Terme schnell unübersichtlich. Python hat für und-verknüpfte Terme die eingebaute Funktion »all« und für oder-verknüpfte Terme die eingebaute Funktion »any« im Gepäck. Hiermit lassen sich eine größere Anzahl von Bedingungen bequem aufschreiben:

if all([
    bedingung1,
    bedingung2,
    bedingungN,
]):
    print('ok')

Im Gegensatz zur Verwendung von and werden hier jedoch beim Aufbau der Liste, die an all() übergeben wird, bereits im Vorfeld alle Bedingungen ausgewertet. Die Liste enthält also vor der Verwendung innerhalb der all()-Methode nur noch Trues und Falses. Dies ist manchmal sehr unpraktisch. Betrachten wir dieses einfache Beispiel, welches unserem erstes Beispiel, nur eben mit einem all, entspricht:

x = None

if all([x is not None, len(x) > 10]):
    print('ok')
else:
    print('nah')

Hier kommt es zu einem Error:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: object of type 'NoneType' has no len()

Was aber, wenn man aber ein solches »schönes« Konstrukt nutzen, aber nicht auf die Kurzschlussauswertung verzichten will? Hier bieten uns Lambda-Funktionen, die sich »später« ausführen lassen. Diese funktionieren allerdings nicht mit any oder all, so dass wir uns selbst dafür Methoden entwickeln müssen:

def lazy_all(list_of_conditions: list) -> bool:
    for condition in list_of_conditions:
        if not condition():
            return False
    return True

bzw. für any:

def lazy_any(list_of_conditions: list) -> bool:
    for condition in list_of_conditions:
        if condition():
            return True
    return False

Wenn wir nun das Beispiel von oben wiederholen, erhalten wir keinen Error mehr, dafür aber eine korrekte Auswertung:

x = None

if lazy_all([lambda: x is not None, lambda: len(x) > 10]):
    print('ok')
else:
    print('nah')

Jetzt werden allerdings nur Funktionszeiger bzw. Lambdas verstanden. Schicker wäre jedoch, wenn unser lazy_any bzw. lazy_all auch normale boolesche Angaben verkraftet, so dass wir nicht immer eine Lambda-Funktion aufbauen müssen. Auch hier hilft uns die Python-Standardbibliothek weiter. Lambdas werden vom Typ <class 'function'> erkannt, boolesche Ausdrucksergebnisse als <class 'bool'>. Entsprechend können wir die Methoden nun umarbeiten:

import types

def lazy_all(list_of_conditions: list) -> bool:
    for condition in list_of_conditions:
        if isinstance(condition, bool):
            condition_value = condition
        elif isinstance(condition, types.FunctionType):
            condition_value = condition()
        else:
            continue
        if not condition_value:
            return False
    return True

def lazy_any(list_of_conditions: list) -> bool:
    for condition in list_of_conditions:
        if isinstance(condition, bool):
            condition_value = condition
        elif isinstance(condition, types.FunctionType):
            condition_value = condition()
        else:
            continue
        if condition_value:
            return True
    return False

Jetzt haben wir also ein paar einfache Werkzeuge an der Hand, um die Vorteile von »any« und »all« mit den Vorteilen der Short Circuit Evaluation zu verbinden.

Ich gehe davon aus, dass ich selbst solche Konstrukte häufiger brauchen werde. Daher habe ich sie als Paket laa (kurz für: Lazy Any All) zusammengefasst und über den Python Package Index (PyPI) zur Verfügung gestellt. Die Quellen gibt es wie immer bei Github. Pull Requests sind willkommen.


Kommentare

Noch keine Kommentare.