Generelles Vorgehen beim Programmieren

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.
Benutzeravatar
__blackjack__
User
Beiträge: 14012
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@schnickalot: Das Klassen bzw. Objekte gepicklet werden können, da braucht man auf nicht allzuviel achten. Das ist der Normalfall würde ich mal sagen. Ausnahmen sind eher Werte die *nicht* gepicklet werden können. Zum Beispiel Dateiobjekte, weil die eine externe Abhängigkeit die man nicht sichern kann (und eigentlich auch nicht will). Nämlich die (offene) Datei auf Betriebssystemebene und deren Zustand, also kompletten Inhalt + Position bei der man gerade ist (falls die Datei ”seekable” ist).

Das Problem ist das man eingeschränkt wird was man an den Klassen noch ändern darf/kann ohne das es zu Problemen kommt wenn man nach Änderungen ältere Pickle-Dateien wieder einlesen will. Wenn man die Klasse umbenennt oder in ein anderes Modul verschiebt kann man keine Exemplare davon mehr entpickeln, weil die Klasse nicht mehr gefunden werden kann. Die Struktur der Klasse darf man dann auch nicht mehr verändern.

Pickle ist gut und nützlich um kurzfristig Python-Objekte zu serialisieren oder wenn es kein Problem ist, wenn das deserialisieren nicht mehr klappt. Zum Beispiel wenn man Objekte zu einem anderen Prozess übertragen möchte oder berechnete Werte extern Cachen möchte, die man zur Not einfach neu berechnen kann.

Für längerfristige Speicherung sollte man explizit Code schreiben der die Daten in einem standardisierten Format (de)serialisiert. Am besten auch mit einer Versionsnummer versehen, so das man bei Änderungen am Format erkennt in welcher Version die gespeicherten Daten vorliegen und man entsprechend auch unterschiedlichen Code für die Versionen haben kann, also auch alte Daten noch einlesen kann, sofern die Änderungen nicht so stark sind, das man das nicht automatisch konvertieren kann, oder zumindest mit ein paar Rückfragen an den Benutzer.

”Pythonisch” ist es möglichst einfach und verständlich zu halten. Properties einzusetzen wo es nur geht gehört da sicher nicht dazu, denn das ist ganz viel Code der nichts macht. Wenn Du triviale Getter und Setter hast, dann bringt das doch effektiv gar keinen Unterschied dazu einfach die Attribute öffentlich zu machen, denn das sind sie effektiv ja. Wenn man für ein Attribut einen Getter und einen Setter hat, dann ist da nichts gekapselt. Wenn man das dann auch noch als Property verfügbar macht, dann ändert sich ja nicht einmal am Code der das benutzt etwas. Das ist einfach nur viel Schreibarbeit für *nichts*.

klein_mit_unterstrichen betrifft übrigens auch Methodennamen.

So wie Du `property` verwendest macht das heute auch niemand mehr. Seit dem es die Dekoratorsyntax gibt, schreibt man das so:

Code: Alles auswählen

    @property
    def some_value(self):
        return self._some_value + 1
    
    @some_value.setter
    def some_value(self, value):
        self._some_value = value - 1
Properties sind einer der Gründe mit denen die Dekoratorsyntax eingeführt wurde, damit man das gleich am Anfang sieht und nicht erst nach zwei Methoden ein Attribut eingeführt wird. Die Getter und Setter in der Art wie Du sie geschrieben hast, wären bei mir übrigens ein Kandidat für einen führenden Unterstrich gewesen, sonst hätte man zwei offizielle Wege das gleiche zu tun. Das stellt einen bei der Verwendung dann vor die eigentlich unnötige Frage ob man nun `some_objekt.get_some_value()` schreibt oder `some_objekt.some_value`. Wenn man ein Property einführt, will man ja eigentlich letzteres, sonst hätte man sich das sparen können.

Ja, Datenkapselung ist eine gute Idee, aber eben nur da wo es Sinn macht und nochmal: Wenn man triviale Getter/Setter hat, dann ist da nix gekaspelt. Weil es effektiv ja überhaupt gar keinen Unterschied gibt ob so ein Wert nun als Attribut vorliegt, oder ob man den kleinen Umweg über Methoden geht – man kann an den Wert von aussen problemlos heran kommen und man kann ihn problemlos setzen.

Und in Python ist man generell nicht so paranoid alles verbieten/erzwingen zu wollen. Man geht von einem mündigen Programmierer aus, der weiss was er tut, und mit den Konsequenzen seines Handelns klar kommt. Man könnte beispielweise für `score` ein reines `get`-Property schreiben und eine `increase_score()` Methode, damit auch ja niemand etwas anderes machen kann als `score` um 1 zu erhöhen. Oder man lässt `score` öffentlich und bietet vielleicht eine `increase_score()` Methode an – oder auch nicht.

Mein Beispiel ist doch nicht wirklich fortgeschritten. Ich finde das im Gegenteil sehr einfach, und es unterscheidet sich bei dem was da steht auch kaum von der ebenfalls einfachen Klasse die Sirius3 geschrieben hat. Der Vorteil vom `attr`-Modul gegenüber so einfachen selbst geschriebenen Klassen ist, das man neben der Schreibarbeit die man sich in der `__init__()` spart, das man auch eine `__repr__()` und die ganzen Vergleichsfunktionen frei Haus bekommt: https://www.attrs.org/en/stable/why.htm ... en-classes
“The best book on programming for the layman is »Alice in Wonderland«; but that's because it's the best book on anything for the layman.” — Alan J. Perlis
schnickalot
User
Beiträge: 22
Registriert: Dienstag 13. August 2019, 14:38

das Pickle nicht mit Änderungen klar kommt, habe ich auch schon erleben dürfen. Hab aber erstmal drüber hinweggesehen, weil ich ja noch am üben bin und dachte wenn erstmal alles steht, sollten auch keine Änderungen mehr kommen. Da sieht man mal wie falsch man liegt. War ja schon froh überhaupt mal eine Datei geschrieben zu haben.

Vielen Dank für eure Geduld. Ich seh schon an euren Antworten, dass ich noch eine ganze Ecke weg bin von jeglicher Programmiertechnik. Ich bastel wohl noch an einzelnen Schritten rum, wobei ich wohl auch oft die falschen Module/alte Schreibweisen verwende. Schon allein die Konventionen.. war mir so sicher, dass Attribute klein geschrieben werden, Methoden klein und gross weiter und Klassen gross und gross weiter. Aber ok.. wo solls auch herkommen - in Anfängerbüchern steht das alles nicht bzw. ist evtl. veraltet. Ich wünschte ich könnte mal eine Woche bei einem Profi Praktikum machen und dumme und doofe Fragen stellen dürfen. Ich frage mich wo man all diese Sachen lernen kann? Wie kommt man auf gewisse Techniken ohne in einem Forum zu fragen oder zu googlen? Will euch nicht mit jedem einzelnen Thema mit 2stelligen Nachfragen nerven. Habt ihr ein Tipp wie man da effektiv vorankommt?
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Doch, durch Foren. Und dann recherchieren. Wenn hier wer „nach PEP8 werden Methoden klein_mit_untertstrich“ schreibt, kann man eben mal nach PEP8 suchen. Und liest über all die anderen Dinge darin etwas. Auch „neue“ Features wie eben die properties mit .setter kann man dadurch entdecken. Dazu muss man eben dran bleiben, sich die Probleme und Lösungen anderer Leute hier oder anderswo (ich habe hauptsächlich mit der comp.lang.python newsgroup gelernt, aber news ist so 0er Jahre. Wenn überhaupt ;) ) aktiv anschauen & aus denen etwas lernen, das eben nicht unmittelbar jetzt und ganz gleich den eigenen Problemen entspricht. Aber einen Hinweis für die Zukunft gibt.

Und bezüglich konkreter sprach / Library Features ist das „what’s new in python X“ das mit jeder Version kommt sehr wertvoll. Allerdings ist das im Verhältnis zu Techniken, Bibliotheken, Algorithmen und Datenstrukturen eher irrelevant. Auch mit old style settern oder selbst geschriebenen Klassen statt attrs kommt man zum Ziel. Die wichtigen Dinge sind aber prinzipielle Lösungsansätze und Konzepte, und die werden eben hier für alles mögliche diskutiert.
schnickalot
User
Beiträge: 22
Registriert: Dienstag 13. August 2019, 14:38

ok verstanden. Danke nochmals an alle für die Fülle an Informationen. Ich denke wir werden uns dann des öfteren unterhalten. Ich versuch die Tage erstmal mit den bisherigen Information mein Progrämmchen umzuschreiben. Bin mir sicher da kommen noch die ein oder anderen Nachfragen. Dann kann ich auch mit mehr fertigen Codeteilen meine Anliegen darstellen.

Sehr angenehmes Forum! Sehr geduldige und ausführliche Helfer! Thumbs up! Man fühlt sich hier gleich wohl ;-)
schnickalot
User
Beiträge: 22
Registriert: Dienstag 13. August 2019, 14:38

Hab mir gestern abend mal JSON angeschaut. Ist ja nicht wirklich anders zu benutzen wie Pickle. Versteh ich das richtig, dass man json.dumps() benutzt um Python Objekte zu JSON Strings zu konvertieren und json.dump(), um es auch noch zusätzlich in eine Datei zu schreiben (also 2 Schritte in einem)?

Habs jetzt mal so:

Code: Alles auswählen

import json
import os

class Players:
	
	def __init__(self):
		self.players = {}
		self.file = "players.json"
		self.load()

	def addPlayer(self, fullname, player):
		self.players[fullname] = player
		
	def load():
		if not os.access(self.file, os.R_OK):
			return
			
		with open(self.file) as f:
			self.players = json.load(f)
		
	def save():
		with open(self.file, 'w') as f:
			json.dump(self.players, f)
Benutzeravatar
__blackjack__
User
Beiträge: 14012
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@schnickalot: Da muss man schon etwas mehr machen, denn das funktioniert ja nur wenn man ausschliesslich Werte hat, deren Typ vom JSON-Modul standardmässig in JSON-Werte umwandeln kann und nicht mit beliebigen Datentypen. Das JSON-Modul wüsste nicht was es beim Speichern mit einem `Player`-Objekt machen sollte. Und beim laden werden aus JSON-Daten auch nicht einfach so `Player`-Objekte. Da braucht man in beide Richtungen jeweils mindestens einen Zwischenschritt der Python-Objekte in eine ”JSON-kompatible” Struktur überführt die man dann speichern kann, und die JSON-Datenstruktur die man geladen hat, wieder in Python-Objekte.

Die `__init__()` sollte wesentlich simpler sein. Die sollte nicht versuchen irgendetwas vom Dateisystem zu laden, sondern einfach nur ein ”leeres” Objekt erstellen. Es wäre praktisch wenn man optional Spieler übergeben könnte, die hinzugefügt werden. Und zwar als iterierbares Objekt das Spieler liefert, also beispielsweise eine Liste, und nicht als Wörterbuch. Das ist die interne Speicherung die Redundanz enthält, denn der volle Spielername ist ja bereits Bestandteil jedes einzelnen Spielers.

An der Stelle ist auch auch die Signatur von `addPlayer()` falsch – den vollen Namen sollte man nicht übergeben, denn dann kann man ja so komische Sachen machen wie ``players.add_player('Peter Meier', Player('Anna', 'Schulze'))``. Man will doch sicher nicht Anna Schulze über 'Peter Meier' ansprechen, sondern über 'Anna Schulze'.

`file` ist ein passender Name für eine Datei, aber nicht für einen Datei*namen*. Wenn ich was habe was `file` heisst, dann erwartet der Leser, dass das eine `read()` und/oder `write()`-Methode hat und ist verwirrt wenn das als Argument für `open()` verwendet wird – denn `open()` bekommt keine Datei als Argument sondern hat eine Datei als Rückgabewert.

Ich würde den Dateinamen gar nicht als Attribut von `Players` speichern sondern sowohl bei `load()` als auch bei `save()` als Argument übergeben.

Und `load()` würde ich als Klassenmethode implementieren die nicht ein Objekt füllt, sondern ein neues `Players`-Objekt liefert.

Textdateien sollte man immer mit einer expliziten Kodierung öffnen. Auch wenn JSON eigentlich nur für ASCII spezifiziert ist, würde ich UTF-8 verwenden. Denn von Hand schreiben die wenigsten Leute Escape-Sequenzen für ”Sonderzeichen” in solche Dateien.
“The best book on programming for the layman is »Alice in Wonderland«; but that's because it's the best book on anything for the layman.” — Alan J. Perlis
Sirius3
User
Beiträge: 18255
Registriert: Sonntag 21. Oktober 2012, 17:20

@schnickalot: zusätzlich zu dem was __blackjack__ schon geschrieben hat: eingerückt wird immer mit 4 Leerzeichen pro Ebene, nicht Tabs. In `load` auf irgendeine Dateieigenschaft zu prüfen und im Fehlerfall stillschweigend nichts zu machen, ist sehr verwirrend. Einfach probieren, ob `open` klappt, und falls nicht, wird es eine Exception geben, die Du an passender Stelle verarbeiten kannst.
Benutzeravatar
__blackjack__
User
Beiträge: 14012
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Folgender Code erwartet das `Player` eine Methode `to_dict()` und eine Klassenmethode `from_dict()` hat (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python3
import json


class Players:
    def __init__(self, players=()):
        self._players = {}
        for player in players:
            self.add(player)

    def add(self, player):
        self._players[player.full_name] = player

    def save_json(self, filename):
        data = {
            "players": [player.to_dict() for player in self._players.values()]
        }
        with open(filename, "w", encoding="utf8") as file:
            json.dump(data, file)

    @classmethod
    def load_json(cls, filename):
        with open(filename, encoding="utf8") as file:
            data = json.load(file)
        return cls(map(Player.from_dict, data["players"]))
Man könnte das laden und speichern aber auch als Funktionen schreiben die generell mit Typen umgehen können die die genannten beiden Methoden haben. Der Funktion zum Laden müsste man dann noch entweder den Datentyp oder besser noch die `from_dict()`-Methode – oder eben generell etwas aufrufbares mitgeben was das gewünschte Objekt liefert.
“The best book on programming for the layman is »Alice in Wonderland«; but that's because it's the best book on anything for the layman.” — Alan J. Perlis
schnickalot
User
Beiträge: 22
Registriert: Dienstag 13. August 2019, 14:38

Danke euch beiden für die Analyse!

@Blackjack: kannst Du mir Dein Beispiel erklären? Mit Klassenmethoden habe ich es noch nicht so. Ich komm nicht dahinter wie das genau funktionieren soll.
Benutzeravatar
__blackjack__
User
Beiträge: 14012
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@schnickalot: Methoden bekommen das Objekt auf dem sie aufgerufen wurden als erstes Argument, per Konvention `self` genannt. Und Klassenmethoden bekommen die Klasse als erstes Argument, per Konvention `cls` genannt. Und normalerweise ruft man Klassenmethoden auch auf der Klasse auf. Also hier beispielsweise:

Code: Alles auswählen

    players = Players.load_json("players.json")
    players.add(Player("Peter", "Müller"))
    players.save_json("more_players.json")
“The best book on programming for the layman is »Alice in Wonderland«; but that's because it's the best book on anything for the layman.” — Alan J. Perlis
schnickalot
User
Beiträge: 22
Registriert: Dienstag 13. August 2019, 14:38

danke Blackjack,

also instanziere ich Players über den return-Wert der Klassenmethode?! Macht man das generell so bei Klassen, wo man Daten einlädt?

Und in die JSON Datei fliesst ein Dict mit einem Key "players" und als Value eine Liste mit den Player-Objekten, die in to_dict() in JSON-Strings umgewandelt werden?! Hab ich das richtig verstanden?
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Man macht das, weil man es ermoeglichen will, ein Players-Objekt auch so zu konstruieren. Ohne das da gleich ein dicker Lade-Mechanismus anspringt, fuer den man eine Menge an Randbedingungen erfuellen muss. So koennte man zB spaeter auch eine SQLite DB benutzen, oder die Klasse fuer Tests ohne alles instantiieren.

Und ich denke mal du hast das richtig verstanden. Denn so steht's ja auch in BJs code.
Sirius3
User
Beiträge: 18255
Registriert: Sonntag 21. Oktober 2012, 17:20

nein, von außen sieht es so aus, als ob Du eine Instanz der Klasse erzeugst, indem Du eine Klassenmethode aufrufst. Klassenmethoden helfen, wenn man verschiedene Arten hat, wie man Instanzen erzeugen könnte.

Das Umwandeln in einen String findet erst bei `json.dump` statt.
schnickalot
User
Beiträge: 22
Registriert: Dienstag 13. August 2019, 14:38

__blackjack__ hat geschrieben: Freitag 16. August 2019, 12:39 @schnickalot: Da muss man schon etwas mehr machen, denn das funktioniert ja nur wenn man ausschliesslich Werte hat, deren Typ vom JSON-Modul standardmässig in JSON-Werte umwandeln kann und nicht mit beliebigen Datentypen. Das JSON-Modul wüsste nicht was es beim Speichern mit einem `Player`-Objekt machen sollte. Und beim laden werden aus JSON-Daten auch nicht einfach so `Player`-Objekte. Da braucht man in beide Richtungen jeweils mindestens einen Zwischenschritt der Python-Objekte in eine ”JSON-kompatible” Struktur überführt die man dann speichern kann, und die JSON-Datenstruktur die man geladen hat, wieder in Python-Objekte.
Dann wird die "JSON-kompatible" Struktur in to_dict() erstellt? Wenn ja, wie muss das aussehen?
Sirius3
User
Beiträge: 18255
Registriert: Sonntag 21. Oktober 2012, 17:20

`to_dict` muß ein Wörterbuch zurückliefern, dessen Schlüssel Strings sind und die Werte nur aus String, Int, Float, oder Listen und Wörterbücher, die diese Grunddatentypen enthalten, bestehen.
schnickalot
User
Beiträge: 22
Registriert: Dienstag 13. August 2019, 14:38

ok danke.. also muss jedes Attribut, welches Daten hat, die ich speichern möchte, in ein erneutes dict geschrieben werden?

Mal ganz billig so?:

Code: Alles auswählen

def to_dict(self):
    dict = {}
    dict["first_name"] = self.first_name
    dict["last_name"] = self.last_name
    dict["team"] = self.team.name
    dict["games_won"] = self.games_won
    dict["games_lost"] = self.games_lost
    dict["frames_won"] = self.frames_won
    dict["frames_lost"] = self.frames_lost
return dict

So sieht meine Player-Klasse aus:

Code: Alles auswählen

class Player:

    def __init__(self, first_name, last_name, team):
        self.first_name = first_name
        self.last_name = last_name
        self.team = team

        self.score = 0

        self.games_won = 0
        self.games_lost = 0
        self.frames_won = 0
        self.frames_lost = 0

        @property
        def full_name(self):
            return "{} {}".format(self.first_name, self.last_name)

        @property
        def games_total(self):
            return self.games_won + self.games_lost

        @property
        def frames_total(self):
            return self.frames_won + self.frames_lost
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

dict solltest du das nicht nennen, denn so heisst der Typ. Und ein bisschen umstaendlich machst du das auch. Einfacher:

Code: Alles auswählen

def to_dict(self):
    return dict(
        first_name=self.first_name,
        frames_lost=self.frames_lost,
        ...
    )
ACHTUNG: ich BENUTZE dict, den Typ, als Konstruktor. Einen Namen hat das Ding nicht.
schnickalot
User
Beiträge: 22
Registriert: Dienstag 13. August 2019, 14:38

ok prima. Vielen Dank.

zu from_dict():
Dann return ich dort cls(first_name, last_name, team, games_won=games_won, games_lost=games_lost... und weitere named args, die ich dann auch in der __init__ übergeben muss:

class Player:
def __init__(self, first_name, last_name, team, **kwargs)

richtig?
Sirius3
User
Beiträge: 18255
Registriert: Sonntag 21. Oktober 2012, 17:20

Benutze kein **kwargs, sondern gib alle Argumente explizit an.
schnickalot
User
Beiträge: 22
Registriert: Dienstag 13. August 2019, 14:38

:-) hatte ich schon so da stehen, dachte aber ist professioneller mit **kwargs.

Danke euch allen. Hab's langsam kapiert
Antworten