Loops mit Else-Blöcken finden

Code-Stücke können hier veröffentlicht werden.
Antworten
Schard
User
Beiträge: 16
Registriert: Freitag 25. Dezember 2020, 01:23
Wohnort: Hannover

mCoding hat kürzlich ein Video veröffentlicht, in dem die Else-Clauses von while- und for-Loops als Code Smells dargestellt werden.
Für diejenigen unter euch, die wie ich diese Einschätzung teilen, habe ich ein Skript geschrieben, dass das aktuelle Verzeichnis nach Python-Modulen mit diesem Code Smell durchsucht und die entsprechenden Dateinamen mit Zeilennummern ausgibt:

Code: Alles auswählen

#! /usr/bin/env python
"""Find for- and while-loops with else clauses."""

from ast import For, Module, While, parse, walk
from logging import getLogger
from pathlib import Path
from typing import Iterator

import os


LOGGER = getLogger(__file__)


def loops(module: Module) -> Iterator[For | While]:
    """Yield loop nodes of the given module."""

    for node in walk(module):
        if isinstance(node, (For, While)):
            yield node


def load_module(filename: Path) -> Module:
    """Load an AST Module from the given file."""

    with filename.open('r') as file:
        return parse(file.read())


def iter_files(root: Path) -> Iterator[Path]:
    """Recursively iterate over all files in the root directory."""

    for base_dir, _, files in os.walk(root):
        base_dir = Path(base_dir)

        for file in files:
            yield base_dir / file


def python_modules(root: Path) -> Iterator[tuple[Path, Module]]:
    """Recursively yield file names and Python modules
    loaded from files within the given root directory.
    """

    for filename in filter(lambda f: f.suffix == '.py', iter_files(root)):
        try:
            module = load_module(filename)
        except PermissionError:
            LOGGER.error('Insufficient permissions to read: %s', filename)
        except SyntaxError:
            LOGGER.error('Invalid syntax in: %s', filename)
        else:
            yield filename, module


def main():
    """Run the program."""

    for filename, module in python_modules(Path.cwd()):
        for loop_with_else in filter(lambda loop: loop.orelse, loops(module)):
            print(filename, loop_with_else.lineno, sep=': ')


if __name__ == '__main__':
    main()
Gist: https://gist.github.com/conqp/6056221a2 ... 515595d104
Sirius3
User
Beiträge: 16373
Registriert: Sonntag 21. Oktober 2012, 17:20

Wieder einmal ein typischer Fall von, "ich kann etwas auch falsch einsetzen, also sollte man es ganz verbieten".
Natürlich gibt es für Fälle, in denen es eine bessere Lösung gibt, keinen Grund, warum man else-Blöcke verwenden soll, aber das ist eine nicht-Aussage.
Aber jemand, der Fehlerbehandlung per Flag und nicht per Exception macht, sollte niemandem versuchen zu erklären, was ein Code-Smell ist.

Code: Alles auswählen

def process_for_5_seconds():
    start = time.perf_counter()
    try:
        while True:
            time_elapsed = time.perf_counter() - start
            print(f"keep working, it's only been {time_elapsed:.2f}s")
            if time_elapsed >= 5:
                break
            if random_error():
                raise RuntimeError()
        print("done!")
    except RuntimeError:
        print("handling error...")
Wenn man logging benutzt, muß man auch einen Logger initialisieren, sonst nutzt der nichts.
In Deinem Code benutzt Du os.walk. Besser wäre Path.rglob. Statt open und read kann man gleich read_text benutzen.
Die Verwendung von filter in den for-Zeilen sind für mich sehr schwer zu lesen.
Bei so allgemeinen Funktionen wie walk oder parse lese ich gerne, dass sie aus dem ast-Modul stammen.

Code: Alles auswählen

#!/usr/bin/env python
"""Find for- and while-loops with else clauses."""

from ast import For, While
import ast
import logging
from pathlib import Path

LOGGER = logging.getLogger(__file__)


def iter_loops(module):
    """Yield loop nodes of the given module."""
    for node in ast.walk(module):
        if isinstance(node, (For, While)):
            yield node


def python_modules(root):
    """Recursively yield file names and Python modules
    loaded from files within the given root directory.
    """
    for filename in root.rglob('*.py'):
        try:
            module = ast.parse(filename.read_text())
        except PermissionError:
            LOGGER.error('Insufficient permissions to read: %s', filename)
        except SyntaxError:
            LOGGER.error('Invalid syntax in: %s', filename)
        else:
            yield filename, module


def main():
    """Run the program."""
    logging.basicConfig()
    for filename, module in python_modules(Path.cwd()):
        for loop in iter_loops(module):
            if loop.orelse:
                print(f"{filename}: {loop.lineno}")


if __name__ == '__main__':
    main()
Schard
User
Beiträge: 16
Registriert: Freitag 25. Dezember 2020, 01:23
Wohnort: Hannover

1) Gutes Refactoring! Ich kannte `read_text()` nicht. Aber warum hast du die Type Hints entfernt?
2) Ich glaube nicht, dass James in seinen Videos irgendwo propagiert hat, dass Flag-Variablen eine Gute Idee seien. Vielmehr sagt er, mMn zu Recht, dass early-return oftmals eine bessere Lösung ist.
Sirius3
User
Beiträge: 16373
Registriert: Sonntag 21. Oktober 2012, 17:20

Zu 1) wegen der Lesbarkeit
Zu 2) doch, in seinem process_for_5_seconds-Beispiel propagiert er Flags.
Benutzeravatar
__blackjack__
User
Beiträge: 10870
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Schard: Bezüglich der Typannotationen vielleicht auch noch der Hinweis, dass nicht jeder die aktuellsten Python-Versionen verwendet. Python 3.7 wird noch bis Mitte nächsten Jahres supported, ist also durchaus noch ”aktuell”. Ich schreibe jetzt üblicherweise für Python 3.8, habe aber beruflich auch noch mit älteren Versionen zu tun. Kunden setzen gerne mal wirklich langlebige, stabile Distributionen wie CentOS ein, da leben dann auch Python-Versionen deutlich länger als sie von den Python-Entwicklern selbst supported werden.

Code: Alles auswählen

Sheep := 0;
WHILE NOT Asleep DO Inc(Sheep);
Benutzeravatar
__blackjack__
User
Beiträge: 10870
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Im Video und durch den Titel wird IMHO auch sehr übertrieben und falsch dargestellt das Guido van Rossum (GvR) dieses Sprachfeature dermassen schlimm findet. Er würde das nicht neu einbauen wenn es das noch nicht gäbe, aber er sieht auch nicht die Gefahr die in der Konversation von 2009 von anderen gesehen wird. Letzterer Satz wird im Video zufällig(?) von dem fetten Rahmen verdeckt den der Video-Autor über den Text legt den *er* für den wichtigsten Teil des Absatzes hält.

Daraus die Formulierung „Python's Erschaffer wünschte dieses Feature hätte nie existiert“ ist ja schon fast das Niveau von BILD-Zeitungs-Überschriften.

Im `process_for_5_seconds()` ist das ``else`` unnötig, ja, aber das `errored`-Flag ist ebenfalls unnötig:

Code: Alles auswählen

def process_for_5_seconds():
    random.seed(4)
    start = time.perf_counter()
    target_time = start + 5

    while (now := time.perf_counter()) < target_time:
        print(f"keep working, it's only been {now - start:.2f}s")
        if random_error():
            print("handling error...")
            return

    print("done!")

Code: Alles auswählen

Sheep := 0;
WHILE NOT Asleep DO Inc(Sheep);
Schard
User
Beiträge: 16
Registriert: Freitag 25. Dezember 2020, 01:23
Wohnort: Hannover

Das Video ist sicher kontrovers und es gibt sicherlich Argumente für die Verwendung der entsprechenden else-Clauses.
Allerdings sollte es hier gar nicht darum gehen. Ich wollte ein kleines Programm vorstellen, das entsprechende Verwendungen findet.
Dies richtet sich an Leute, die deren Verwendung als Code-Smell einstufen und vermeiden wollen.
Ich zwinge niemanden dazu, das Programm zu verwenden oder ihren Code zu verändern.
Ich nutze in meinen Projekten immer diejenige Python Version, die meine Linux Distribution unterstützt und welche mich am besten bei der Programmierung unterstützt.
Auf der Arbeit verwende ich Python 3.9 wegen unserer nicht immer aktuellen Debian Kiste, privat aktuell 3.10.
Dabei geht es mir nicht darum aus Prinzip die neueste Version zu verwenden, sondern ich nutze ggf. neue Features, wenn ich diese für sinnvoll halte, auch wenn diese nicht von alten Python Versionen unterstützt werden.
Ich habe kein Problem damit, in meinen privaten Projekten potentielle Nutzer mit alten Python Versionen abzuhängen oder alte Zöpfe rigoros abzuschneiden.
Klingt hart, erleichtert mir allerdings ungemein die Entwicklungsarbeit.
Benutzeravatar
kbr
User
Beiträge: 1370
Registriert: Mittwoch 15. Oktober 2008, 09:27

Ich würde zwischen code-smell und persönlichen Präferenzen unterscheiden wollen: "for ... else" ist aus meiner Sicht kein code-smell und wenn es jemand anders nicht mag und vermeidet, dann ist auch das kein code-smell.
Antworten