Datenbank schließen, bevor Objekt stirbt

Installation und Anwendung von Datenbankschnittstellen wie SQLite, PostgreSQL, MariaDB/MySQL, der DB-API 2.0 und sonstigen Datenbanksystemen.
Antworten
Benutzeravatar
NoPy
User
Beiträge: 158
Registriert: Samstag 28. Dezember 2013, 12:39

Hi,

Ich bin gerade dabei, so eine Art "Bildverwaltung" als Klasse zu schreiben.

Man möge ihr ein Bild vorwerfen.
Die Klasse
- prüft, ob sie das Bild schon hat
- wertet das Bild aus (erst mal die EXIF- Daten)
- kopiert es an die "richtige" Stelle
- aktualisiert die Datenbank

Daraus ergibt sich für mich, dass beim Initialisieren eines solchen Objektes die DB geöffnet werden sollte. Und gleichermaßen sollte dann - wenn das Objekt nicht mehr benötigt wird - die DB definiert geschlossen werden.
Reicht es dazu, das "connection.close()" in die __del__ - Methode zu schreiben?
Was passiert, wenn das Programm vorher (an anderer Stelle) abstürzt, wie kann ich sicherstellen, dass die Konsistenz gewährleistet ist? Es ist ja zumindest nicht üblich, Objekte explizit zu zerstören, oder? Und wenn ich einfach nur den Platzhalter neu belege, bleibt es ja dem Garbage- Collektor überlassen, wann genau das Objekt zerstört wird.

Bin ich da auf dem Holzweg?

Also mein gewöhnlicher Ansatz wäre

Code: Alles auswählen

class DBVerwaltung(object):
    def __init__(self):
        self.conn = sqlite3.connect('Test.db')

    def __del__(self):
        self.conn.commit()
        self.conn.close()

    def nimm_dies(bild):
        self.conn.execute("INSERT INTO BILDARCHIV (BILDNAME) VALUES ('{}')".format(bild.name))
richtig/falsch?
Benutzeravatar
pillmuncher
User
Beiträge: 1482
Registriert: Samstag 21. März 2009, 22:59
Wohnort: Pfaffenwinkel

NoPy hat geschrieben:Bin ich da auf dem Holzweg?
Ja, bist du.

__del__() ist kein Destruktor. In Python nimmt man für solche Open/Close Sachen normalerweise einen Context Manager. Siehe https://docs.python.org/3/library/stdty ... ager-types und https://docs.python.org/3/library/contextlib.html.

Strings formatiert man nicht einfach in SQL-Statements, sondern übergibt beim Aufruf von execute() passende Parameter: https://docs.python.org/3/library/sqlit ... or.execute.

Sonst droht einem das hier: https://xkcd.com/327/.

Siehe auch: https://de.wikipedia.org/wiki/SQL-Injection.

Ich würde statt alldem gleich SQLAlchemy verwenden: http://www.sqlalchemy.org/
In specifications, Murphy's Law supersedes Ohm's.
Benutzeravatar
NoPy
User
Beiträge: 158
Registriert: Samstag 28. Dezember 2013, 12:39

pillmuncher hat geschrieben:
NoPy hat geschrieben:Bin ich da auf dem Holzweg?
Ja, bist du.

__del__() ist kein Destruktor. In Python nimmt man für solche Open/Close Sachen normalerweise einen Context Manager. Siehe https://docs.python.org/3/library/stdty ... ager-types und https://docs.python.org/3/library/contextlib.html.
[OFFTOPIC]
Zu dem ganzen SQL- Zeug: Ich bin seit Jahr(zehnt)en in diesem Geschäft, ich weiß das. Ich bin ja froh, dass Du den Sinn des Insert- Statements nicht auch angegriffen hast, denn der war ebenfalls sinnarm. Es ging mir nur um das Prinzip, das heißt ein echter Ansatz ist auf die wesentlichen Zusammenhänge zusammengestrichen worden.
[/OFFTOPIC]

Nun zu dem Kozept Context- Manager: Wenn ich das richtig interpretiere, dann ist das nur etwas overhead für

Code: Alles auswählen

class DBVerwaltung(object):
    def __init__(self):
        pass
        #self.conn = sqlite3.connect('Test.db') soll ich hier nicht machen

    def __del__(self):
        pass
        #self.conn.commit() soll ich hier erst recht nicht machen
        #self.conn.close() soll ich hier erst recht nicht machen

    def LosGehts(self):
        self.conn = sqlite3.connect('Test.db') 

     def FertigIst(self):
         self.conn.commit() soll ich hier erst recht nicht machen
         self.conn.close() soll ich hier erst recht nicht machen

     def nimm_dies(bild):
        self.conn.execute("INSERT INTO BILDARCHIV (BILDNAME) VALUES ('{}')".format(bild.name))

cbverwaltung = DBVerwaltung()
cbverwaltung.LosGehts()
cbverwaltung.nimm_dies(buntestes_bild)
cbverwaltung.FertigIst()
Abgesehen davon, dass ich beim Context- Manager mittels Dekoratoren meine Klasse einer Library anvertraue, die - wenn sie selbst planmäßig das Zeitliche segnet - vorher noch ihre Kinder beerdigt. Aber vermutlich irre ich mich wieder, oder?
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

Ein Context Manager ist einfach ein Objekt dass die __enter__() und __exit__() Methoden implementiert. So ein Objekt kannst du mit dem with statement nutzen, welches __enter__() am Anfang und __exit__() am Ende aufgeruft.

Das Konzept ist vollkommen unabhängig von Memory Management.

In deinem Fall würde ich das öffnen und schliessen der Datenbankverbindung nicht in der DBVerwaltung Klasse machen. Ich würde einfach die schon offene Datenverbindung an die DBVerwaltung übergeben und eine reinen Context Manager erstellen, der sich die Datenbankverbindung erstellt und deine DBVerwaltung zurückgibt.

Das ganze würde dann in etwa so aussehen:

Code: Alles auswählen

from contextlib import contextmanager

@contextmanager
def open_database(database_url):
    with sqlite3.connect(database_url) as connection:
        yield DBVerwaltung(connection)

# benutzt man dann so:
with open_database('Test.db') as db:
    db.nimm_dies()
Sirius3
User
Beiträge: 17712
Registriert: Sonntag 21. Oktober 2012, 17:20

@NoPy: "Ich bin seit Jahren Autofahrer. Ich weiß, dass man nicht in die Gegenrichtung auf die Autobahn fährt. Das war nur so um was ganz anderes zu zeigen". Und sicher weißt Du auch, dass das erste Argument einer Methode immer self heißen sollte. Contextmanager sorgen dafür, dass etwas innerhalb eines Blocks gekapselt ist, dass man, wenn man den Block verläßt, egal wie, alle Resourcen wieder aufgeräumt werden. Hat also nichts mit Overhead zu tun, sondern mit strukturierter Programmierung:

Code: Alles auswählen

class DBVerwaltung(object):
    def __enter__(self):
        self.conn = sqlite3.connect('Test.db')
        return self
    
    def __exit__(self, exc_type, exc_value, traceback):
        if exc_type is not None:
            self.conn.rollback()
        else:
            self.conn.commit()
        self.conn.close()

     def nimm_dies(bild):
        self.conn.execute("INSERT INTO BILDARCHIV (BILDNAME) VALUES (?)", (bild.name,))

with DBVerwaltung() as cbverwaltung:
    cbverwaltung.nimm_dies(buntestes_bild)
BlackJack

@NoPy: Ich glaube das wurde jetzt hier zu `__del__()` nicht noch mal extra gesagt: Es gibt keine Garantie das diese Methode *überhaupt* aufgerufen wird *und* das Vorhandensein einer `__del__()`-Methode kann unter Umständen sogar dafür sorgen dass das Objekt *nie* von der Speicherbereinigung betroffen ist. Von `__del__()` sollte man besser die Finger lassen.

Was SQL-Injections angeht: *Du* magst das wissen, der nächste Anfänger der hier vorbei schaut nimmt Deins dann aber als Beispiel für sein Programm. Gerade wenn er dann auch mitbekommt das Du Dich mit Datenbanken auskennst ist das ungünstig. ;-)
Benutzeravatar
NoPy
User
Beiträge: 158
Registriert: Samstag 28. Dezember 2013, 12:39

Sirius3 hat geschrieben:@NoPy: "Ich bin seit Jahren Autofahrer. Ich weiß, dass man nicht in die Gegenrichtung auf die Autobahn fährt. Das war nur so um was ganz anderes zu zeigen".
So Unterschiedlich können Interpretationen sein. Um beim Auto zu bleiben: Ich befinde mich allein mit Dir auf dem Fahrschulübungsplatz, um auszuprobieren, ob die Verkabelung meiner Autoelektronik funktioniert oder mir unter bestimmten Umständen das ganze Auto abfackelt und Du erklärst mir umfangreich, dass und warum man blinkt, was weder Bestandteil des Themas noch nennenswerter Bestandteil des Lösungsansatzes ist.
BlackJack hat geschrieben:Was SQL-Injections angeht: *Du* magst das wissen, der nächste Anfänger der hier vorbei schaut nimmt Deins dann aber als Beispiel für sein Programm. Gerade wenn er dann auch mitbekommt das Du Dich mit Datenbanken auskennst ist das ungünstig. ;-)
Ich glaube zwar nicht, dass jemand, der sich über die korrekte Verwendung von SQL informieren will, eben diesen Beitrag liest, aber einverstanden, das wäre mit unwesentlich mehr Aufwand besser zu machen gewesen.
Sirius3 hat geschrieben:Und sicher weißt Du auch, dass das erste Argument einer Methode immer self heißen sollte.
Weiß ich, mach ich auch immer. Aber den Code auf der Spielwiese teste ich nun mal nicht immer, daher rutscht so etwas durch.

Nun aber zum Thema:
Sirius3 hat geschrieben:Contextmanager sorgen dafür, dass etwas innerhalb eines Blocks gekapselt ist, dass man, wenn man den Block verläßt, egal wie, alle Resourcen wieder aufgeräumt werden. Hat also nichts mit Overhead zu tun, sondern mit strukturierter Programmierung:

Code: Alles auswählen

class DBVerwaltung(object):
    def __enter__(self):
        self.conn = sqlite3.connect('Test.db')
        return self
    
    def __exit__(self, exc_type, exc_value, traceback):
        if exc_type is not None:
            self.conn.rollback()
        else:
            self.conn.commit()
        self.conn.close()

     def nimm_dies(bild):
        self.conn.execute("INSERT INTO BILDARCHIV (BILDNAME) VALUES (?)", (bild.name,))

with DBVerwaltung() as cbverwaltung:
    cbverwaltung.nimm_dies(buntestes_bild)
Für mich sieht das eher so aus, dass die Konsequenz der Unwissenheit über Lebensdauer von Objekten an dieser Stelle durch eine Konstruktion, die ihren Ursprung anscheinend in der multiprozess- Programmierung hat, korrigiert wird.
Letztlich definierst Du mit cbverwaltung über die with- Konstruktion ein Objekt, dessen Lebensdauer Dich nicht interessiert, da Du weißt, dass - ähnlich einem kritischen Abschnitt, zumindest in der Wortwahl - es betreten und verlassen wird. Das könnte man ja auch völlig ohne __enter__/__exit__- Magie schreiben, indem man die Funktionen zu Beginn und am Ende aufruft. Was also unterscheidet diesen Ansatz von dem von mir zuletzt skizzierten? (nochmal in der Codebox, wegen des HiLite)

Code: Alles auswählen

class DBVerwaltung(object):
    def LosGehts(self):
        self.conn = sqlite3.connect('Test.db') 

    def FertigIst(self):
        self.conn.commit() soll ich hier erst recht nicht machen
        self.conn.close() soll ich hier erst recht nicht machen

    def nimm_dies(self, bild):
        self.conn.execute("INSERT INTO BILDARCHIV (BILDNAME) VALUES (?)", (bild.name,))

dbverwaltung = DBVerwaltung()
dbverwaltung.LosGehts()
dbverwaltung.nimm_dies(buntestes_bild)
dbverwaltung.FertigIst()
Zuletzt geändert von NoPy am Dienstag 1. Dezember 2015, 12:49, insgesamt 1-mal geändert.
BlackJack

@NoPy: `__exit__()` wird aufgerufen egal *warum* der Block verlassen wird. Also auch wenn der in einer Schleife steht die aus dem Block heraus mit ``break`` verlassen wird, oder wenn der Block mit ``return`` verlassen wird, oder auch wenn der Block durch eine Ausnahme verlassen wird. Im speziellen Beispiel wird bei einer Ausnahme auch noch etwas anderes gemacht als wenn keine aufgetreten ist.

Jetzt programmier das mal mit ``try``/``except``/``finally`` ordentlich aus und stell Dir vor Du musst das nicht nur einmal im Programm machen. Viele machen das dann auch nicht, nicht mal einmal, sondern kürzen den Code ab und nehmen damit in Kauf dass das `close()` nicht aufgerufen wird, wie Du ja in Deinem Beispiel auch.
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

Das with statement bringt auch eine sehr hilfreiche visuelle Komponente mit sich. Man erkennt dadurch sofort dass dort irgendeine Resource verwendet werden soll und welcher Teil des Codes etwas mit der Resource macht.

Desweiteren kann man als API Autor sicherstellen dass Resourcen immer freigegeben werden, in dem man konsequent auf Context Manager setzt und so dem Nutzer gar nicht erst die Möglichkeit gibt dabei Fehler zu machen.
Benutzeravatar
NoPy
User
Beiträge: 158
Registriert: Samstag 28. Dezember 2013, 12:39

BlackJack hat geschrieben:@NoPy: `__exit__()` wird aufgerufen egal *warum* der Block verlassen wird. Also auch wenn der in einer Schleife steht die aus dem Block heraus mit ``break`` verlassen wird, oder wenn der Block mit ``return`` verlassen wird, oder auch wenn der Block durch eine Ausnahme verlassen wird. Im speziellen Beispiel wird bei einer Ausnahme auch noch etwas anderes gemacht als wenn keine aufgetreten ist.
o.k., das ist natürlich ein echter Mehrwert
BlackJack hat geschrieben:Jetzt programmier das mal mit ``try``/``except``/``finally`` ordentlich aus und stell Dir vor Du musst das nicht nur einmal im Programm machen. Viele machen das dann auch nicht, nicht mal einmal, sondern kürzen den Code ab und nehmen damit in Kauf dass das `close()` nicht aufgerufen wird, wie Du ja in Deinem Beispiel auch.
Schon klar (wobei es gewagt ist zu behaupten, dass ich das "in Kauf" nehmen wollte, denn dann gäbe es ja diesen Thread nicht)
Mein Beispiel setzt demgegenüber voraus, dass die Methode "nimm_dies" ohne exception verlassen wird, die Methode müsste also alle exceptions selbst behandeln und dürfte keine eigenen werfen.
Aber jetzt mal ganz ehrlich: gefällt euch das? ich verstehe unter objektorientiert auch, dass ein Objekt für seine komplette Lebenszeit eigenverantwortlich tätig ist. Das bedeutet für mich auch, dass es aus eigener Kraft hintersich aufräumt, egal, ob durch irgend einen garbage- collektor getriggert oder explizit. So muss ich das Objekt auf spezielle Weise verwenden, damit hinterher alles wieder ordentlich ist.
Oder habe ich das mit der with- Konstruktion missverstanden? Das ganze leitet einen Block ein, oder? Wenn ich also das Getreide durch mehrere Mühlen schicke, dann müsste ich alle innerhalb dieses with aufrufen, nur damit sich das Objekt nicht verabschiedet, ohne auf Wiedersehen zu sagen.
Benutzeravatar
pillmuncher
User
Beiträge: 1482
Registriert: Samstag 21. März 2009, 22:59
Wohnort: Pfaffenwinkel

NoPy hat geschrieben:Aber jetzt mal ganz ehrlich: gefällt euch das? ich verstehe unter objektorientiert auch, dass ein Objekt für seine komplette Lebenszeit eigenverantwortlich tätig ist. Das bedeutet für mich auch, dass es aus eigener Kraft hintersich aufräumt, egal, ob durch irgend einen garbage- collektor getriggert oder explizit. So muss ich das Objekt auf spezielle Weise verwenden, damit hinterher alles wieder ordentlich ist.
Das folgt dem Zen of Python: Explicit is better than implicit.
NoPy hat geschrieben:Oder habe ich das mit der with- Konstruktion missverstanden? Das ganze leitet einen Block ein, oder? Wenn ich also das Getreide durch mehrere Mühlen schicke, dann müsste ich alle innerhalb dieses with aufrufen, nur damit sich das Objekt nicht verabschiedet, ohne auf Wiedersehen zu sagen.
Das musst du doch sowieso. Nochmal: das with-Statement formalisiert ein Code-Pattern, das immer wieder auftaucht:

Code: Alles auswählen

resource = initialize_resoure(some, args)
try:
    do_something_with(resource)
finally:
    close(resource)
Das with-Statement dazu:

Code: Alles auswählen

with initialize_resource(some, args) as resource:
    do_something_with(resource)
WIe man sieht, muss do_something_with(resource) immer in einem Kontext laufen, wo resource am Leben ist, und ob man das so wie im ersten oder wie im zweiten Beispiel macht, ist egal. Im zweiten Fall verwendet man halt ein Sprachkonstrukt, das genau für diesen Fall gedacht ist und das einem das Leben einfacher macht.

Dass man das Aufräumen in einen Destruktor packt, ergibt nur Sinn in Sprachen, in denen es Destruktoren gibt, also etwa in C++, aber nicht in Python. Keine dieser beiden unterschiedlichen Herangehensweisen ist mehr oder weniger OOP als die andere, sondern es sind einfach verschiedene Wege, dasselbe zu erreichen.
In specifications, Murphy's Law supersedes Ohm's.
BlackJack

@NoPy: Mir gefällt das. :-) Selbst wenn es einen Destruktor gäbe, auf den man sich verlassen könnte, dass er wenigstens auf jeden Fall überhaupt aufgerufen wird, hätte ``with`` immer noch den Vorteil das ich weiss wie lange der Kontextmanager ”aktiv” ist. Und das `__exit__()` auch an der Stelle aufgerufen wird, selbst wenn ich noch laaaaange danach irgendwo eine Referenz auf das Objekt habe. Ausserdem ist `__exit__()` ja nicht immer gleichbedeutend mit dem ”tot” des Objekts. Es gibt auch Kontextmanager bei denen man sinnvoll mehrfach ``with`` anwenden kann. Sperren für nebenläufige Programmierung beispielsweise, da wäre es sogar sinnfrei wenn das Objekt nur ein ``with`` ”überlebt”. Auch Dein Objekt könnte man nach einem ``with``-Block ja noch mal in einem Weiteren verwenden.

Das `nimm_dies()` ohne Ausnahme verlassen wird kann man im allgemeinen Fall nicht garantieren. Nur in dem man dort wirklich alle Ausnahmen behandelt, also ist die Frage ob man dort überhaupt alle *sinnvoll* behandeln kann, oder ob bestimmte nicht weiter oben in der Aufrufhierarchie behandelt werden können/sollten. Wenn man das so machen würde, müsste man an allen Stellen wo dieses Muster vorkommt den ”Fluss” von Ausnahmen stoppen. Oder eben mindestens jedes mal konsequent ein ``try``/``finally`` für die Aufräumarbeiten einfügen. Was kaum jemand macht, weil einem das auch nirgends so wirklich gezeigt/eingebläut wird. Eigentlich müssten ja alle Programme die eine Datei öffnen und etwas damit machen so aussehen:

Code: Alles auswählen

lines = open('test.txt')
try:
    for line in lines:
        do_something(line)
finally:
    lines.close()
Da schlampen aber so ziemlich alle Programmierer gerne (ich auch bis es ``with`` gab!) und Anfänger sehen so etwas nicht in freier Wildbahn, und in Tutorials kommen in der Regel Dateien in Beispielen bevor Ausnahmebehandlung gezeigt wird.

In Python kann ein Objekt nicht so einfach eigenverantwortlich hinter sich aufräumen weil es keine verlässlichen Garantien gibt wann und ob die Speicherbereinigung das Objekt abräumt. Diese Garantien gibt es nicht um die Art der Speicherverwaltung nicht festzulegen. Die ist bei CPython anders als bei Jython und anders als bei IronPython. Bei .NET weiss ich es jetzt nicht, aber die JVM hat da ja einen ähnlichen Ansatz. Da gibt's auch `Object.finalize()` von dessen Implementierung abgeraten wird, weil das nicht wirklich deterministisch ist.

Den letzten Absatz habe ich nicht ganz verstanden‽ Damit sich das Objekt nicht verabschiedet reicht es das erreichbar zu halten. Da es ja anscheinend *gebraucht* wird, muss es das ja auch sein solange es gebraucht wird. Das wird sich also nicht einfach so verabschieden können, in dem Sinne das vor Ende des Vorgangs *weg* ist. Ist es ja nach Ende des ``with``-Blocks auch nicht. Das kann sowohl vor als auch nach dem Block problemlos weiter existieren wie jedes andere Objekt auch. Einfaches Beispiel dafür wären wieder Sperrobjekte.

Ganz allgemein können Kontextmanager für ”symmetrische” Aufrufe verwendet und haben erst einmal überhaupt nichts mit der Lebenszeit des Objekts zu tun. Das sorgt erst einmal nur dafür das zu jedem a() am Anfang des Blocks auch ein b() am Ende des Blocks erfolgt, egal warum der Block verlassen wird. Also `open()`/`close()`, `aquire()`/`release()`, `start_tag()`/`end_tag()`, und so weiter. a() und b() *können* die einzig ”nützliche” Lebenszeit eines Objekts betreffen, müssen das aber nicht.
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

@BlackJack: Ich kenne die Fundamente in in der CIL von .NET nicht, aber in C# gibt es für den Aufruf eines Destruktors analog zu Python *keine* Garantie. Daher gibt es das ``IDisposable``-Interface, welches analog zu Pythons ``with`` sogenannte ``using``-Blöcke ermöglicht. Zusätzlich gibt es noch "semi manuellen" Eingriff in den GC mittels Finalizers. Hier gibt es eine recht hübsche Übersicht mit weiter führenden Links und hier ein Beispiel für IDisposable.
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
Benutzeravatar
NoPy
User
Beiträge: 158
Registriert: Samstag 28. Dezember 2013, 12:39

Vielen Dank für Eure Mühe.
Im Moment finde ich das noch nicht wirklich schick, aber die Argumente passen zusammen. Schauen wir mal, wie ich damit umgehe. Ich werde mich wohl mal mit dem Konzept Kontext- Manager beschäftigen.

Hat jemand ein gutes (etwas komplexeres) Einstiegsbeispiel (auch abstrakt)?
also ein
class ...
def ...
def ...
ist mir zu knapp, das zeigt ja im Grunde nur die Syntax, nicht so sehr die Verwendung, also etwas, was über diese Quelle hinausgeht:
https://en.wikibooks.org/wiki/Python_Pr ... t_Managers
BlackJack

@NoPy: Ich weiss nicht ob's Einteigerbeispiele enthält, aber Beispiele sind drin: PEP 343 - The ``with`` statement.
Antworten