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.