staging.inyokaproject.org

Lernfrage: Best Practice Test / Reaktion auf Funktionen, die einen Fehler zurückgeben

Status: Ungelöst | Ubuntu-Version: Ubuntu 20.04 (Focal Fossa)
Antworten |

jAIk

Avatar von jAIk

Anmeldungsdatum:
7. Juni 2007

Beiträge: 254

Hallo zusammen,

ich bin mir sicher, dass ich nicht der Erste bin, der danach sucht. Wenn mir jemand ein Schlagwort dazu nennen möchte, reicht das natürlich vollkommen aus.

Meine Frage: Wie schreibe ich robusten / stabilen Code, der Fehler in meinen Funktionen abfängt? Folgendes Beispiel:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
def do_something():
    if something_works:
        return 0
    else:
        return 1

def main():
    if do_something() == 0:
        print("It works")
        if do_something() == 0:
            print("It works")
            if do_something() == 0:
                print("It works")
                if do_something() == 0:
                    print("It works")
                    #...
    else:
        print("Something went wrong")

if __name__ == "__main__":
    main()

Gehe ich hier in guter Best-Practice Manier immer weiter in die IF-Schleife rein? Oder nutze ich ein Statement wie z.B.

1
2
if do_something() != 0:
    return 1

Ich würde mich über einen Link / Hinweis zur passenden Literatur sehr freuen. Über eine kurze Erklärung natürlich auch... 😀

VG jaik

seahawk1986

Anmeldungsdatum:
27. Oktober 2006

Beiträge: 11278

Wenn do_something() immer wieder aufgerufen werden soll, kann man das durch eine while-Schleife ersetzen:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def do_something():
    if something_works:
        return True

def main():
    while do_something():
        print("It works")
    else:
        print("Something went wrong")

if __name__ == "__main__":
    main()

Generell würde ich da aber lieber zu Exceptions greifen: https://docs.python.org/3/tutorial/errors.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
def do_something():
    if not something_works:
        raise ValueError("Something did not work!")

def do_something_else():
    if not something_else_works:
       raise ValueError("Something else did not work!")

def main():
    try:
        while True:
            do_something()
            print("It works!")
            do_something_else()
            print("This worked, too!")
    except ValueError as err:
        print(f"Error: {err}")
    except KeyboardInterrupt:
        print("User interrupted the main loop, bye!")

if __name__ == "__main__":
    main()

jAIk

(Themenstarter)
Avatar von jAIk

Anmeldungsdatum:
7. Juni 2007

Beiträge: 254

Mmm, vielleicht war die Frage von mir ungünstig gestellt...

Es geht allgemein darum, dass ich einige verschiedene Funktionen aufrufe. Auch innerhalb der einen oder anderen Methode greife ich auf try-except zurück.

Die Frage dahinter ist eher: Gibt es hier "anerkannte"(tm) best Practices, wie ich so einen Code formatiere?

Vielleicht ein etwas besseres Beispiel. Ich kann den folgenden Code beispielsweise in dieser Form schreiben: Funktion:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
def do_something1():
    try:
        #something
        return 0
    except:
        return 1

def do_something2():
    try:
        #something
        return 0
    except:
        return 1

def do_something3():
    try:
        #something
        return 0
    except:
        return 1

main: Möglichkeit 1

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def main():
    if do_something1() == 0:
        if do_something2() == 0:
            if do_something3() == 0:
                print("Hurra! Everything worked")
            else:
                print("Error")
        else:
            print("Error")
    else:
        print("Error")

if __name__ == "__main__":
    main()

main: Möglichkeit 2

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def main():
    if do_something1() != 0:
        print("Error")
    if do_something2() != 0:
        print("Error")
    if do_something3() != 0:
        print("Error")

    print("Hurra! Everything worked")

                
if __name__ == "__main__":
    main()

Gibt es hier irgend eine Art von Style-Guide oder Best Practice, wie man seinen Code strukturieren sollte?

Für mich spricht eigentlich nichts gegen eine der beiden Möglichkeiten. Oder übersehe ich da etwas?

VG jaik

seahawk1986

Anmeldungsdatum:
27. Oktober 2006

Beiträge: 11278

Ein nacktes except sollte man nach Möglichkeit vermeiden, sondern sich genau ansehen, welche Exceptions der Code werfen kann und wo man welche Abfangen will. Der ganze Sinn der Fehlerbehandlung ist ja, dass man abhängig davon, was schief gegangen ist entscheiden kann, wie man weiter verfahren will und dementsprechend sieht dann der Code aus.

In Möglichkeit 1 willst du die nachfolgenden Funktionen nur dann aufrufen, wenn die vorherige geklappt hat - da macht es eigentlich keinen Sinn innerhalb der Funktion ein try ... except zu nutzen, wenn man den Fehler ausschließlich weiter oben im Call-Stack auswerten möchte bzw. man könnte dann mit einem raise eine Exception erneut werfen, wenn man will, dass übergeordnete Funktionen das mitbekommen.

Möglichkeit 1 würde ich - insbesondere, wenn da die Ergebnisse vieler Funktionsaufrufe ausgewertet werden sollen - anders formulieren und da z.B. früh mit einem return False aussteigen oder eben gleich eine Exception aus der aufgerufenen Methode den Stack soweit hochkommen lassen, bis es Sinn macht sie zu fangen.

In Möglichkeit 2 werden alle Funktionen nacheinander unabhängig von ihren Rückgabewert bzw. Fehlerstatus aufgerufen.

Tendenziell würde ich mich auch von 0 und 1 als Rückgabewerte verabschieden, Python-Funktionen ohne explizites return-Statement geben None zurück und es gibt Booleans (True und False), die man nutzen kann, wenn man eine Fallunterscheidung ausdrücken will. Einen Fehler, der nicht von der Funktion selbst sinnvoll gehandhabt werden kann würde ich an übergeordneter Stelle abfangen lassen und dort darauf reagieren.

jAIk

(Themenstarter)
Avatar von jAIk

Anmeldungsdatum:
7. Juni 2007

Beiträge: 254

Danke für deine Antwort, die einen großen Teil meiner Frage bereits beantwortet hat.

Einen Fehler, der nicht von der Funktion selbst sinnvoll gehandhabt werden kann würde ich an übergeordneter Stelle abfangen lassen und dort darauf reagieren.

Vollständig nachvollziehbar.

Möglichkeit 1 würde ich - insbesondere, wenn da die Ergebnisse vieler Funktionsaufrufe ausgewertet werden sollen - anders formulieren und da z.B. früh mit einem return False aussteigen...

Ebenfalls verstanden.

...oder eben gleich eine Exception aus der aufgerufenen Methode den Stack soweit hochkommen lassen, bis es Sinn macht sie zu fangen.

Und genau das geht zum Kern meiner Frage: Wie würdest du das konkret machen? Ich finde viel Beispielcode aus guten Quellen, der auf try except eingeht[1]. Aber irgendwie ist mir noch nicht genau klar, wie ich besagte Fehler weiter nach oben im Stack geben kann, wenn sinnvoll.

[1] https://docs.python.org/3/tutorial/errors.html

VG jaik

seahawk1986

Anmeldungsdatum:
27. Oktober 2006

Beiträge: 11278

jAIk schrieb:

Aber irgendwie ist mir noch nicht genau klar, wie ich besagte Fehler weiter nach oben im Stack geben kann, wenn sinnvoll.

Einfach den Code-Teil in einen try … except Block packen, der den Fehler fangen soll - hier mal ein kleines Beispiel, um nach einer Münze zu suchen und sie dann zu werfen:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#!/usr/bin/env python3
import sys

class CoinThrowError(Exception):
    """custom error when flipping a coin fails"""

class Coin:
    def heads_or_tails(self):
        raise CoinThrowError("coin landed on it's side")

pockets = {
    "trousers_right": Coin()
}

def get_coin_from_pocket(pocket_location):
    return pockets[pocket_location]  # raises a KeyError if the key is not in the dictionary

def get_coin():
    for pocket in ("shirt", "trousers_left", "trousers_right"):
        try:
            coin = get_coin_from_pocket(pocket)
        except KeyError:
            print(f"no coin in {pocket} pocket :(")
        else:
            print(f"found coin in {pocket} pocket :)")
            return coin
    raise AttributeError("No coin found in any pockets!")

def main():
    try:
        print("searching for a coin...")
        coin = get_coin()
        print("flipping the coin...")
        coin.heads_or_tails()
    except (CoinThrowError, AttributeError) as err:
        sys.exit(f"critical error: {err}")
    except Exception as err:
        sys.exit(f"unexpected error: {err}")

if __name__ == '__main__':
   main()
Antworten |