Kommandos inkommensurabel

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.
hcshm
User
Beiträge: 48
Registriert: Dienstag 11. Februar 2020, 08:23

Code nachfolgend. Beide letztgenannten print-Befehle funktionieren - aber nur jeweils separat. Wenn ich sie gleichzeitig aufrufe, erhalte ich für den letzten print-Befehl den Hinweis:
TypeError: strptime() argument 1 must be str, not datetime.datetime
Ich habe hin- und herüberlegt, gegoogelt etc. - aber komme nicht weiter.
Kann mir bitte (erneut) jemand helfen?
Vielen Dank!

Code: Alles auswählen

import csv
from datetime import datetime, timedelta

class Mitglied:
    def __init__(self, vorname, geburtstag, groesse, gewicht):
        """ Klasse, die aus csvDatei instantiiert wird, Beispielzeile
        der csv-Datei: jp,28.10.1981,178,85.0"""
        self.vorname = vorname
        self.geburtstag = geburtstag
        self.groesse = groesse
        self.gewicht = gewicht

    def alter(self):
        self.geburtstag = datetime.strptime(self.geburtstag, "%d.%m.%Y")
        aktuellesdatum = datetime.today()
        res = (aktuellesdatum - self.geburtstag) // timedelta(days=365.2425)
        return res

    def __str__(self):
        return f'{self.vorname} hat am {self.geburtstag} Geburtstag, ist also ' \
               f'{self.alter()} Jahre ' \
               f'alt, ist {self.groesse} cm groß und wiegt {self.gewicht} kg.'

def read_family(filename):
    '''CSV-Datei mit Familienmitgliedern lesen'''
    result = {}
    with open(filename, encoding="utf8", newline="") as input:
        reader = csv.reader(input)
        for entry in reader:
            member = Mitglied(*entry)
            result[member.vorname] = member
    return result

if __name__ == '__main__':

    familie = read_family("familie.csv")

    for mitglied in familie.values():
        print(mitglied)

    katrin = familie['katrin']
    print(katrin)
Benutzeravatar
kbr
User
Beiträge: 1487
Registriert: Mittwoch 15. Oktober 2008, 09:27

strptime möchte als erstes Argument einen string, der entsprechend dem zweiten Argument geparst wird und dann ein datatime-Objekt liefert. Bei dem zweiten Aufruf von alter() hat somit self.geburtstag einen anderen Type.
hcshm
User
Beiträge: 48
Registriert: Dienstag 11. Februar 2020, 08:23

Vielen Dank! Dass es hier um Typfragen geht, konnte ich erkennen, daher mein Hinweis "inkommensurabel". Weiter als bis zu dieser Feststellung komme ich leider nicht ... (trotz intensiver Bemühung)
hcshm
User
Beiträge: 48
Registriert: Dienstag 11. Februar 2020, 08:23

Vielen Dank erneut, ich habe eine Lösung gefunden. Allerdings weiß ich weiterhin nicht, warum die beiden Kommandos nicht GLEICHZEITIG funktionieren.
Benutzeravatar
__blackjack__
User
Beiträge: 13100
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@hcshm: Wenn Du das erste mal `alter()` aufrufst ist `self.geburtstag` eine Zeichenkette. Das ist das was `strptime()` erwartet. Du ersetzt `self.geburtrag` dann in der Methode durch ein `datetime.datetime`-Objekt. Und das ist es dann halt auch wenn Du `alter()` noch mal aufrufst, und damit kann `strptime()` nichts anfangen, und teilt das durch die Ausnahme mit. Darum funktioniert das halt beim ersten Aufruf und beim zweiten Aufruf nicht mehr.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
sparrow
User
Beiträge: 4193
Registriert: Freitag 17. April 2009, 10:28

@hcshm: Ein gutes Beispiel, warum es schwierig ist, wenn man Dinge an Stellen ändert, an denen man nicht damit rechnet. In dem andren Thread wurde ja als "Feature" verkauft, dass man "self" zurückgeben kann. Auch das würde man nicht erwarten und des ist deshalb schwierig.

Das Problem hier ist ählich: Funktionen und Methoden sollten immer nach ihrer Tätigkeit benannt werden. Eine Ausnahme sind Properties. Das sind eigentlich Werte, die aber erst bei dem Abruf errechnet werden. Eigentlich wäre "alter" also ein property.
Der Kern ist: der Name der Methode in einer Klasse ist immer auch eine Beschreibung von dem, was es tut. Bei einer Funktion "starte_motor" würde niemand damit rechnen, das der Warnblinker angeht oder der Tank geleert wird. Deshalb sind Namen immer wichtig. Und in deinem Fall ändert die Methode "alter" in ihrer ersten Zeile den Wert von "self.geburtstag". Das ist unerwartet (denn man braucht das nicht um das Alter zu errechnen) und auch fehlerhaft, denn diese Änderung sorgt dafür, dass die Methode nicht ein zweites Mal ausgeführt werden kann.

Lange Rede kurzer Sinn: Der Code ist fehlerhaft und ein schönes Beispiel für unerwartete Änderungen von Werten. Das Parsen des Strings, damit ein datetime-Objekt daraus wird, gehört nicht in die Methode sondern in die __init__. Dort wird es einmalig ausgeführt und du kannst es in allen Methoden der Klasse verwenden.
hcshm
User
Beiträge: 48
Registriert: Dienstag 11. Februar 2020, 08:23

Vielen Dank an die beiden Experten! Nach diesen Erläuterungen frage ich doch noch mal nach:
Ist diese Lösung so i.O.?

Code: Alles auswählen


    def alter(self):
        eigen_geburtstag = datetime.strptime(self.geburtstag, "%d.%m.%Y")
        aktuellesdatum = datetime.today()
        res = (aktuellesdatum - eigen_geburtstag) // timedelta(days=365.2425)
        return res

Benutzeravatar
__blackjack__
User
Beiträge: 13100
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Ich würde sagen, dass das vielleicht nicht so gut in der `__init__()` oder `alter()` aufgehoben ist. Die `__init__()` würde ich in der Regel so simpel wie möglich halten und der bereits ein `datetime`-Objekt übergeben, und die Umwandlung dort hin verschieben wo die Daten das Programm betreten, also hier zum Einlesen. Auch Grösse und Gewicht würde ich da nicht als Zeichenketten übergeben, sondern an der Schnittstelle schon in Zahlen umwandeln.

Die Altersberechnung ist etwas ungewöhnlich. Ist das irgendwie domänenspezifisch? Üblicherweise zieht man ja einfach die Jahre voneinander ab und passt dann im 1 an, je nachdem ob der Geburtstag in dem aktuellen Jahr schon war oder nicht.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Sirius3
User
Beiträge: 17747
Registriert: Sonntag 21. Oktober 2012, 17:20

Daten sollte man immer schon beim ersten Kontakt ins richtige Format bringen, also hier:

Code: Alles auswählen

def read_family(filename):
    '''CSV-Datei mit Familienmitgliedern lesen'''
    result = {}
    with open(filename, encoding="utf8", newline="") as file:
        reader = csv.reader(file)
        for vorname, geburtstag, groesse, gewicht in reader:
            member = Mitglied(vorname, datetime.strptime(geburtstag, "%d.%m.%Y"), float(groesse), float(gewicht))
            result[member.vorname] = member
    return result
hcshm
User
Beiträge: 48
Registriert: Dienstag 11. Februar 2020, 08:23

Vielen Dank!
LukeNukem
User
Beiträge: 232
Registriert: Mittwoch 19. Mai 2021, 03:40

Sirius3 hat geschrieben: Mittwoch 26. Mai 2021, 07:54 Daten sollte man immer schon beim ersten Kontakt ins richtige Format bringen, also hier:

Code: Alles auswählen

            member = Mitglied(vorname, datetime.strptime(geburtstag, "%d.%m.%Y"), float(groesse), float(gewicht))
Anstatt das im Konstruktor zu machen, wo der Entwickler auch verschiedene Parsingstrategien ausprobieren und eine ordentliche Fehlerbehandlung machen könnte, willst Du das beim Aufruf des Konstruktors machen? Was passiert denn dann, wenn der Nutzer Deiner Klasse Deine merkwürdigen Aufrufkonventionen nicht kennt oder nicht beachtet? Ach ja, dann hast Du kaputte Daten... Im Konstruktor könnte man dann gleich auch eine Validierung der Werte einbauen, so daß ein Mensch mit minus drei Kilogramm, einer Größe von vier Metern oder einem Alter von 2500 Jahren direkt abgewiesen wird. Übrigens könnte man die Daten anstatt im Konstruktor zu prüfen auch elegant über Setter-Methoden von Properties oder über die __set__()-Methoden von Deskriptoren abbilden, konvertieren und validieren (intern sind Properties ja ohnehin Deskriptoren).

Zudem kenne ich aus der bisweilen leidvollen und schmerzhaften Erfahrung mit Unternehmen, die große Datenbestände verwalten, zwei verschiedene Strategien beim Handling von Datums- und Zeitangaben. Die einen normalisieren alle Datums- und Zeitangaben in ein einheitliches Format. Das ist meistens ein String nach ISO 8601, kann aber in seltenen Fällen auch eine auf UNIX-Epoch bezogene Fließkommazahl sein, die dann üblicherweise auf Greenwich Mean Time (GMT) normalisiert wird. Letzteres spart ein bisschen Speicherplatz auf Kosten der Les- und Vergleichbarkeit, erfordert dafür aber bei Lesezugriffen oft eine Konvertierung in ein besser lesbares Format. Andere speichern einfach die empfangenen Rohdaten als Strings, was bei der Ingestion ein bisschen Zeit spart, allerdings einen hohen Aufwand beim Zusammenführen von Daten aus verschiedenen Quellen und leider nicht selten auch spät entdeckte Inkonsistenzen provoziert, wenn beispielsweise ein Einlieferer von Daten unbemerkt sein Datumsformat ändert, sei es durch eine Änderung am Code oder durch ein Update von Systemen, Applikationen, oder Bibliotheken.

Meine Empfehlung ist, Datums- und Zeitangaben als ISO 8601-Strings und bei Zeitangaben immer inklusive Zeitzone zu speichern. Dieses Format hat neben der Standardisierung noch weitere Vorzüge, zum Beispiel, daß man sie ganz einfach und ohne Parsing mit normalen Stringfunktionen sortieren und direkt vergleichen kann, und daß nahezu alle Programme und Bibliotheken das Standardformat problemlos akzeptieren, lesen und schreiben können.

Zuletzt sei daran erinnert, daß das Python-Standardmodul "datetime" eine recht limitierte Funktionalität hat, die durch die externen Bibliotheken "python-dateutil" [1] und "arrow" [2] jedoch deutlich erweitert werden kann. Unter anderem ist im Untermodul "dateutil.parser" eine sehr mächtige Parsing-Funktion parse() versteckt, die ohne vorherigen Angabe eines Formates eine ganze Reihe gebräuchlicher Formate parsen kann. Auch "arrow" hat eine schicke Parser- und eine Reihe weiterer angenehmer Funktionen.


[1] https://dateutil.readthedocs.io/en/stable/
[2] https://arrow.readthedocs.io/en/latest/
Sirius3
User
Beiträge: 17747
Registriert: Sonntag 21. Oktober 2012, 17:20

@LukeNukem: eine Funktion read_csv kennt das Format, das gelesen werden soll. Der Konstruktor weiß nichts, woher die Daten kommen. Sollte er auch nicht. Denn dann zwingst Du den Nutzer dazu, eine seltsame Aufrufkonvention einzuhalten. Bei mir ist ein Gewicht ist eine Zahl, ein Datum ist ein Datum. eine unmerkwürdigere Aufrufkonvention gibt es gar nicht.
Benutzeravatar
__blackjack__
User
Beiträge: 13100
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Das würde dann beispielsweise wenn die Daten aus einer Datenbank kommen, zu der komischen Situation führen, dass man ein Datum als `datetime`/`date`-Objekt bekommt, das in eine Zeichenkette wandeln muss, nur damit die in der `__init__()` dann wieder in das `datetime`/`date`-Objekt umgewandelt werden müsse was man ja schon mal hatte.

Daten sollten beim betreten und verlassen des Programms umgewandelt werden, und innerhalb des Programms einen dem Datum entsprechenden Typ haben. Wenn man das *nicht* macht, *dann* fangen die Probleme an, das man immer schauen muss in welchem Format beispielsweise ein Geburtsdatum vorliegt oder eine Gewichtsangabe und ob man umwandeln muss oder nicht wenn man das irgendwo hin übergibt, oder von irgendwo als Wert bekommt.

Das eine `__init__()` die Argumente hat, deren Namen und Datentypen den Attributen entsprechen, ist auch alles andere als eine ”merkwürdige Aufrufkonventionen”. Das ist die Regel und nicht die Ausnahme. Falls da jemand etwas falsches übergibt, dann hat das halt falsche Daten oder Ausnahmen zur Folge. Wie immer wenn man etwas falsches irgendwo übergibt.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
LukeNukem
User
Beiträge: 232
Registriert: Mittwoch 19. Mai 2021, 03:40

__blackjack__ hat geschrieben: Mittwoch 26. Mai 2021, 18:29 Das würde dann beispielsweise wenn die Daten aus einer Datenbank kommen, zu der komischen Situation führen, dass man ein Datum als `datetime`/`date`-Objekt bekommt, das in eine Zeichenkette wandeln muss, nur damit die in der `__init__()` dann wieder in das `datetime`/`date`-Objekt umgewandelt werden müsse was man ja schon mal hatte.
Du würdest ein datetime.datetime() in einen String wandeln, um es danach wieder in ein datetime.datetime() konvertieren zu können? Das erinnert mich irgendwie an einen alten Rant über Programmiersprachen, hier ganz besonders C++: https://www-users.cs.york.ac.uk/susan/joke/foot.htm
__blackjack__ hat geschrieben: Mittwoch 26. Mai 2021, 18:29 Daten sollten beim betreten und verlassen des Programms umgewandelt werden,
Des Programms?
__blackjack__ hat geschrieben: Mittwoch 26. Mai 2021, 18:29 Das eine `__init__()` die Argumente hat, deren Namen und Datentypen den Attributen entsprechen, ist auch alles andere als eine ”merkwürdige Aufrufkonventionen”.
Das stimmt, aber "Konstruktor(dings=ichprokelmireinenab(dings))"...
__blackjack__ hat geschrieben: Mittwoch 26. Mai 2021, 18:29 Falls da jemand etwas falsches übergibt, dann hat das halt falsche Daten oder Ausnahmen zur Folge.
Ja, das stimmt, wenn man... ach, egal. In meinem komischen Paralleluniversum sieht die Sache allerdingsigens ein bisschen anders aus.

So ein Datum, weißt Du, das kann ja ganz unterschiedliche Formate haben. Das kann ein datetime.datetime() sein. Oder ein datetime.datetime().date(). Oder ein String- oder Bytes-Objekt, in so einer Form wie "18.5.1971" oder "1971-05-18" oder "1971/05/18" -- oder sogar eine float() bezogen auf einen beliebigen Nullpunkt -- gerne genommen: UNIX-Epoch -- und eine nicht minder beliebige Zeitzone.

Mein Konstruktor wäre deswegen ein bisschen intelligenter. Denn das, was zählt, ist ja das Endergebnis: daß meine Instanz am Ende a) valide und b) weiterverarbeitbare Daten hat. Damit sie mir nicht irgendwann in fünfzehn Monaten in meinen knackigen Popo tritt, wenn $Kunde mir auf einmal Müll liefert.

Was kann ich also tun, um sicherzustellen, daß meine Attribute (oder meinetwegen: Eigenschaften) das richtige Format haben? Hm, da gibt es doch sowas, wie hieß das noch? Ach ja: Konvertierung. Und Exception Handling.

Mein Konstruktor würde deswegen erstmal versuchen, das als "geburtstag" Übergebene in ein datetime.date() zu konvertieren, wenn es nicht schon eines ist. Das ist sogar erstaunlich einfach, und zum Spaß an der Freude zeige ich mal ein Minimalbeispiel:

Code: Alles auswählen

#!/usr/bin/env python
import datetime
from dateutil.parser import parse
from dateutil.relativedelta import relativedelta

class Ding:
    def __init__(self, name, geburtstag):
        self.name = name
        if isinstance(geburtstag, datetime.date):
            self.geburtstag = geburtstag
        elif isinstance(geburtstag, datetime.datetime):
            self.geburtstag = geburtstag.date()            
        elif isinstance(geburtstag, (str, bytes)):
            self.geburtstag = parse(geburtstag)
        elif isinstance(geburtstag, float):
            self.geburtstag = datetime.date.fromtimestamp(geburtstag)
        else:
            raise ValueError(
                'Sorry, cannot convert {} into a datetime.date object'
                .format(geburtstag)
            )

    def __str__(self):
        return '{!r} is {} years old'.format(
            self.name,
            relativedelta(datetime.datetime.now(),self.geburtstag).years)


if __name__ == '__main__':
    datalist = (
        ('a', '18.05.1971'),        
        ('b', '19.5.1971'),
        ('c', '1971-05-20'),
        ('d', 0.2423),
        ('e', datetime.datetime.now().date()),
        ('f', (datetime.datetime.now().date() - datetime.timedelta(days=2500))),
        ('g', 'a'),
    )

    for data in datalist:
        print(Ding(*data))
Jetzt Du. Na los, komm', zeig' mal 'was. Viel Erfolg!

Lieber TO hcshm, bitte entschuldige, ich wollte Deinen Thread nicht hijacken. Und ich hoffe, Du kannst an meinem Beispiel etwas darüber lernen, wie (halbwegs) professioneller Code in Python aussieht -- in der Realität würde ich das noch ein bisschen anders machen, aber die Richtung geht schon. Wenn Du etwas (noch) nicht verstehst, stehe ich Dir (fast jederzeit) sehr gerne zur Verfügung. Viel Vergnügen und Erfolg!
Benutzeravatar
sparrow
User
Beiträge: 4193
Registriert: Freitag 17. April 2009, 10:28

@LukeNukem: Ohne jetzt deinen Wall of Text zu lesen: Die Art, wie du da die __init__ geschrieben hast, ist nun völlig kaputt und enttarnt dich als Troll.

Ich mag an der Kommunikation im Internet ist, dass man andere Gesprächsteilnehmer ohne ihre körperlichen Eigenschaften bewerten kann. Geschlecht, Alter, alles egal. Man sieht ob jemand weiß, was er tut oder nicht ö.
Das hier fühlt sich für mich an, als würdest du verzweifelt Trollen wollen, mut völlig absurden Aussagen. Das ist leider die Kehrseite dieser Kommunikation. Ich persönlich frage mich ja immer, was Leute antreibt ihre Zeit dafür aufzubringen. Fachlich würdest du bereits bloßgestellt, mit der __init__ zeigst du, dass es dir auch gar nicht darum geht vernünftigen Code zu zeigen.

Aber es ist ein guter Schritt, dass du anfängst dich zu entschuldigen, denke ich. Denn dem Threadstsrter ist längst geholfen.
LukeNukem
User
Beiträge: 232
Registriert: Mittwoch 19. Mai 2021, 03:40

sparrow hat geschrieben: Donnerstag 27. Mai 2021, 05:02 @LukeNukem: Ohne jetzt deinen Wall of Text zu lesen: Die Art, wie du da die __init__ geschrieben hast, ist nun völlig kaputt und enttarnt dich als Troll.
Aha. Du kannst also bestimmt was Besseres zeigen, laß' doch mal sehen.

Weil, bis Du das hinbekommen hast... Tut mir leid, bis dahin bist Du mein Troll: Große Klappe, nix dahinter. ;-)
hcshm
User
Beiträge: 48
Registriert: Dienstag 11. Februar 2020, 08:23

Da ich gerade angesprochen worden bin:
- Ja, zu meiner Frage ist mir erschöpfend geholfen - vielen Dank!
- Allerdings folge ich dem Thread auch weiterhin, weil die Expertendiskussion zahlreiche Impulse vermittelt, für die ich mich sehr bedanke!

Bevor ich eine Frage stelle, hier mein bisheriges Verständnis:
- Formatvereinheitlichung sollte beim Betreten und Verlassen von Programmen erfolgen (oder Programmteilen, Modulen - also jeweils in sich geschlossenen Einheiten)
- Hierbei ist auf den Input zu achten - Unstimmigkeiten müssen abgefangen werden (Vielen Dank für diesen Mehrwert, LukeNukem!)
- Über einen Klassenkonstruktor kann das zumindest in der vorgestellten Form nicht erfolgen (meine vorläufige Einschätzung, nachdem ich mich als ziemlicher Anfänger etwas in das Wesen und die Struktur von Konstruktoren eingelesen habe).

Nun meine Frage - da der von LukeNukem aufgeworfene Punkt für einen Anfänger hoch interessant ist:
- Wie würde man Input-Unstimmigkeiten effektiv und "pythonisch" im konkreten Datumsfall abfangen?
Benutzeravatar
__blackjack__
User
Beiträge: 13100
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@hcshm: Du musst entsprechend auf Fehler/Ausnahmen reagieren. Was entsprechend ist, hängt davon ab was in solchen Fällen passieren soll. Wenn man es nicht behandelt, bricht das Programm einfach mit einer Ausnahme und einem Traceback ab wenn eine Zeichenkette nicht als Datum geparst werden kann. Falls es geparst werden kann, aber Du noch Einschränkungen/Prüfungen auf den Wertebereich machen möchtest, müsstest Du da entsprechende Tests programmieren. Die würden dann eher zur Klasse in die `__init__()` gehören, denn so eine Prüfung ist ja unabhängig davon wo die Daten her kommen. Wenn man das beim Einlesen aus der CSV-Datei machen würde, und dann alternativ noch eine Funktion schreiben würde, die die Daten aus einer Datenbank holt, müsste man dort dann ja den gleichen Code zum prüfen reinschreiben.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
LukeNukem
User
Beiträge: 232
Registriert: Mittwoch 19. Mai 2021, 03:40

hcshm hat geschrieben: Donnerstag 27. Mai 2021, 07:01 Bevor ich eine Frage stelle, hier mein bisheriges Verständnis:
- Formatvereinheitlichung sollte beim Betreten und Verlassen von Programmen erfolgen (oder Programmteilen, Modulen - also jeweils in sich geschlossenen Einheiten)
- Hierbei ist auf den Input zu achten - Unstimmigkeiten müssen abgefangen werden (Vielen Dank für diesen Mehrwert, LukeNukem!)
- Über einen Klassenkonstruktor kann das zumindest in der vorgestellten Form nicht erfolgen (meine vorläufige Einschätzung, nachdem ich mich als ziemlicher Anfänger etwas in das Wesen und die Struktur von Konstruktoren eingelesen habe).

Nun meine Frage - da der von LukeNukem aufgeworfene Punkt für einen Anfänger hoch interessant ist:
- Wie würde man Input-Unstimmigkeiten effektiv und "pythonisch" im konkreten Datumsfall abfangen?
Naja, das ist letzten Endes auch eine philosophische Frage, aber vor allem eine des konkreten Anwendungsfalls. Grundsätzlich ist es jedenfalls eine hervorragende Idee, sicherzustellen, daß die Datentypen in den Instanzen bzw. den Attributen der Instanzen einheitliche Datentypen haben, also daß der "geburtstag" in all Deinen Mitglied-Instanzen immer denselben Datentyp hat.

Einerseits gibt es dabei jene Philosophie, die ihren Konstruktoren am Liebsten gleich passend konvertierte Daten mitgeben wollen. Das führt dann zu so einem Code, wie von Sirius3 in diesem Beitrag [1] gezeigt und kann sinnvoll sein, wenn man auf jeden Fehler individuell reagieren will und von außen die maximal Kontrolle behalten will.

Dann gibt es die von mir bevorzugte Philosophie, die die Daten im Konstruktor einfach konvertiert und ggf. prüft -- aus meiner Sicht ist ein Konstruktor (unter anderem) dazu da, so entwickelt man robuste und womöglich fehlertolerante Software. Nun ist das bei Datums- und Zeitangeben leider nicht ganz so einfach, denn wie schon in meinem vorherigen Beitrag erklärt, können diese in recht unterschiedlichen Formaten repräsentiert werden. Einige davon siehst Du in der "datalist" im nachfolgenden Code. Deswegen folge ich lieber dem "Robustness Principle" und versuche, in meinem Konstruktor verschiedene valide Formate in das korrekte datetime.date() zu konvertieren. Das schließt den Ansatz von Sirius3 gleichzeitig aber auch nicht aus: wenn Du möchtest, kannst Du meinem Konstruktor jederzeit auch ein bereits vorher konvertiertes datetime.date() übergeben. Für mich ist dabei nur wichtig, daß die Daten am Ende im korrekten und einheitlichen Datentyp vorliegen, und daß die Benutzer meiner Klasse kontrollieren können, was in jenen Fehlerfällen geschehen soll.

In solchen Fehlerfällen kann man ja, je nach konkretem Anwendungsfall, sehr unterschiedlich reagieren. In einigen Fällen kann es sinnvoll sein, beim Auftreten von kaputten Eingabedaten den Benutzer darauf hinzuweisen, daß er bitte die Eingabedaten korrigieren möge, und die weitere Verarbeitung danach abzubrechen. Das ist in solchen Anwendungsfällen wichtig, wo Daten nur in ihrer Gesamtheit zu einem sinnvollen Ergebnis verarbeitet werden können. In manchen, wenn nicht sogar den meisten Anwendungsfällen ist es jedoch sinnvoll, kaputte Datensätze zu verwerfen, aber trotzdem mit den korrekten Datensätzen weiterzuarbeiten. Das nehme ich einmal für Deine einfache Familiendatenbank an, allerdings mußt Du beachten, daß die kaputten Datensätze dann fehlen und zum Beispiel Aggregate wie das Durchschnittsalter deswegen verfälschte Ergebnisse liefern (müssen).

Deswegen habe ich meinen Beispielcode ein bisschen erweitert; zum Einen läßt sich über das Standardmodul "warnings" präzise und flexibel steuern, was im Falle solcher Warnungen passieren soll: sollen sie immer ausgegeben werden ('always'), sollen sie zu Fehlern (Exceptions) konvertiert werden ('error'), soll jede Fehlermeldung nur einmal ausgegeben werden ('once', die Voreinstellung), und so weiter. Zudem verwende ich hier das Standardmodul "logging", das ebenfalls präzise und flexibel konfiguriert werden kann; in diesem Fall werden die Logdaten mit einem Loglevel von DEBUG und darüber auf sys.stderr ausgegeben, und auch die UserWarnings, die warnings.warn() standardmäßig erzeugt, werden dorthin geloggt. Du könntest Deine Logdaten aber auch in eine Datei oder die Logging-Facility Deines Betriebssystems ausgeben (unter Windows das Eventlog, unter UNIXoiden oft (r)syslog oder unter modernen Linux-Systemen systemd's journald). Oder Du könntest das Logging anweisen, nur Fehler ab einem Loglevel von WARN zu loggen und alles darunter, wie DEBUG-Meldungen, einfach zu ignorieren.

Außerdem wirst Du eine weitere kleine Erweiterung sehen, nämlich die Klasse "ValidDing". Die benutzt den Konstruktor von "Ding", um die Daten womöglich in die korrekten Datentypen zu konvertieren, und prüft dann die Wertebereiche der Daten. Bei meiner Implementierung wird für alle Geburtsdaten, die vor dem 1.1.1920 oder erst in der Zukunft geboren werden, eine Exception geworfen. Bitte beachte auch, daß ich es mir hier etwas einfacher mache, die Fehlermeldungen zu fangen, indem sie alle von der Builtin-Exception "ValueError" erben, so daß ich mit einem "except ValueError ..." nicht nur meine selbstdefinierten Fehlermeldungen ValidationError und ConversionError, sondern auch die von dateutil.parser.parse() geworfene Exception "ParserError" fangen kann.

Ansonsten halte ich meine Lösung für pythonisch, korrekt und robust. Okay, die Kommentare fehlen weitestgehend und in einer realen Implementierung würde ich eher mit Deskriptoren oder Properties arbeiten, aber... ist ja Beispielcode zum Lernen, ne, da halte ich die Komplexität gerne überschaubar. Aber ich bin unbesorgt: die üblichen Verdächtigen werden sicherlich wieder etwas finden, an dem sie sich hochziehen können. ;-)

Code: Alles auswählen

#!/usr/bin/env python
import sys
import datetime
import warnings
import logging
from dateutil.parser import parse, ParserError
from dateutil.relativedelta import relativedelta

def warning_formatter(message, category, filename, lineno, file=None, line=None):
    return '{}({}): {}: {}'.format(filename, lineno, category.__name__, message)

warnings.formatwarning = warning_formatter
warnings.simplefilter('always')
logging.basicConfig(
    stream=sys.stderr,
    level=logging.DEBUG,
    format='%(asctime)s - %(levelname)s - %(message)s'
)
logging.captureWarnings(True)


class ConversionError(ValueError): pass

class Ding:
    def __init__(self, name, geburtstag):
        if isinstance(name, str):
            self.name = name.encode('utf-8')
        elif isinstance(name, bytes):
            # consider chardet or UnicodeDammit?
            self.name = name
        else:
            raise ConversionError('name is neither a str nor a bytes object')
        
        if isinstance(geburtstag, datetime.date):
            self.geburtstag = geburtstag
        else:
            warnings.warn('"geburtstag" is not a date: trying to convert')
            if isinstance(geburtstag, datetime.datetime):
                self.geburtstag = geburtstag.date()
            elif isinstance(geburtstag, (str, bytes)):
                self.geburtstag = parse(geburtstag).date()
            elif isinstance(geburtstag, (int, float)):
                self.geburtstag = datetime.date.fromtimestamp(geburtstag)
            else:
                raise ConversionError(
                    'Sorry, cannot convert {} into a datetime.date object'
                    .format(geburtstag)
                )

    def __str__(self):
        return '{!r} is {} years old (geburtstag={})'.format(
            self.name, self.alter(), self.geburtstag)

    def alter(self):
        return relativedelta(datetime.datetime.now(), self.geburtstag).years
    

class ValidationError(ValueError): pass

class ValidDing(Ding):
    def __init__(self, name, geburtstag):
        super().__init__(name, geburtstag)
        lower_border = datetime.datetime(1920, 1, 1).date()
        upper_border = datetime.datetime.now().date()
        if self.geburtstag < lower_border or self.geburtstag > upper_border:
            raise ValidationError('Sorry, {} is not between {} and {}'.format(
                self.geburtstag.isoformat(), lower_border.isoformat(), upper_border.isoformat()))

if __name__ == '__main__':
    datalist = (
        ('a', '18.05.1971'),
        ('b', '19.5.1971'),
        ('c', '1971-05-20'),
        ('d', 0),
        ('e', 200000000.2423),
        ('f', -2000000000.2423),
        ('g', datetime.datetime.now().date()),
        ('h', (datetime.datetime.now().date() - datetime.timedelta(days=999))),
        ('i', 'a'),
        ('j', 'May 18, 1971')
    )

    mitglieder = list()  # maybe rather a set()?
    for data in datalist:
        try:
            mitglieder.append(ValidDing(*data))
        except ValueError as e:
            logging.error(str(e))
        except Exception as e:
            logging.Exception(e)
            raise

    for mitglied in mitglieder:
        print(mitglied)
[1] viewtopic.php?f=1&t=52210#p388262

PS: Wer Variablennamen findet, darf sie mit nach Hause nehmen.

PPS: Wer Kritik üben will, darf sie begründen und dazu seinen eigenen Code zeigen. ;-)
narpfel
User
Beiträge: 645
Registriert: Freitag 20. Oktober 2017, 16:10

@LukeNukem: Das automatische Parsen sieht für mich nach einem Rezept für Datums-Mojibake aus. Woher weiß dein Programm, ob "01/02/03" entweder 2001-02-03, 2003-01-02 oder 2003-02-01 ist? Und wie ist das „korrekt und robust“?
Antworten