__str__ und file.write

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.
mechanicalStore
User
Beiträge: 91
Registriert: Dienstag 29. Dezember 2009, 00:09

Hallo Zusammen,

nach sehr langer Zeit will ich mich mal wieder mit Python beschäftigen. Schön, dass es meinen Account nach langer Zeit noch gibt. Hab dazu mal das "Quick Python Book" von Manning durchgelesen / rumprobiert / etc. Wollte nun mal etwas Praktisches machen (eine Stempeluhr, die meine Projektzeiten erfasst). Und stolpere gleich über __str__

Code: Alles auswählen

from datetime import datetime

class MyEntry():

    my_entry_list = []
    FILE_PRAEFIX = '.txt'

    def __init__(self, project, pr_date, pr_start, pr_stop = ""):
        self.project = project
        self.pr_date = pr_date
        self.pr_start = pr_start
        self.pr_stop = pr_stop

    def __str__(self):
        return "|".join([self.project, self.pr_date, self.pr_start, self.pr_stop])
    
    @classmethod
    def create_entry(cls, project):
        pr_date = datetime.now().strftime('%Y-%m-%d')
        pr_start = datetime.now().strftime('%H:%M:%S')
        pr_stop = datetime.now().strftime('%H:%M:%S')
        my_entry = cls(project, pr_date, pr_start, pr_stop)
        cls.my_entry_list.append(my_entry)

    @classmethod
    def save_entries(cls, file_name):
        full_file_name = file_name + cls.FILE_PRAEFIX
        with open(full_file_name, "w", encoding="utf-8") as my_file:
            for entry in cls.my_entry_list:
                print(entry)
                my_file.write(str(entry))  # <-- warum ist hier eine Umwandlung nach str nötig?

def main():
    for index in range(5):
        MyEntry.create_entry('P-21-001-' + str(index))

    MyEntry.save_entries('test')
    print('Wrote file...')

if __name__ == '__main__':
    main()
Wenn ich in der Zeile

Code: Alles auswählen

my_file.write(str(entry))
die Umwandlung nach str weg lasse, kommt die Meldung: TypeError: write() argument must be str, not MyEntry

Warum funktioniert __str__ nur bei print, nicht aber bei file.write?

Ursprünglich wollte ich my_entry_list mit json.dump speichern, hab aber dann gesehen, dass json keine Objektstrukturen speichern kann. Generell könnte ich das ganze ja auch "flach" ablegen, ganz ohne Objektruktur. Da ich aber später z.B. die Endzeit nochmal korrigieren / nachtragen möchte, erscheinen mir Objekte flexibler, als "manuelle" Änderungen in Listen / etc.

Danke und Gruß
derElch
User
Beiträge: 33
Registriert: Sonntag 25. Februar 2018, 13:14

Was spricht dagegen deine Objekte als dict abzuspeichern?
Das würde mit json funktionieren und mit __dict__ / vars das Objekt in eine json zu schreiben? siehe hier: https://stackoverflow.com/questions/615 ... cts-fields
Benutzeravatar
__blackjack__
User
Beiträge: 13003
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@mechanicalStore: Weil ist halt so. `print()` ruft intern `str()` auf. Die `write()`-Methode geht davon aus, das man die Daten vom passenden Typ übergibt.

Den Präfix `my`/`My` vergisst Du am besten gleich wieder, denn der macht keinen Sinn wenn es nicht auch ein `their` oder `our` gibt gegen den sich das abgrenzen würde.

Grundatentypen haben nichts in Namen verloren. Während der Entwicklung ändert sich das gerne mal und dann hat man entweder falsche irreführende Namen oder muss überall alle von der Änderung betroffenen Namen anpassen.

Namen sollten keine kryptischen Abkürzungen enthalten. Zum Beispiel `pr_` — da habe ich keine Ahnung wofür das stehen soll.

`FILE_PRAEFIX` sollte wohl `FILE_SUFFIX` heissen.

Man verwendet keine globalen Variablen. Klassenattribute sind in der Regel deshalb nur Konstanten. Eine dynamische Liste mit Objekten hat dort nichts zu suchen. Das passt auch gar nicht zur Aufgabe einer Klasse für *einen* Eintrag da alle Einträge zu verwalten. `save_entries()` wird damit zu einer Funktion welche die Einträge als Argument bekommt und `create_entry()` würde den erzeugten Eintrag als Ergebnis liefern statt eine globale Liste zu erweitern. In `create_entry()` kann man auch das `entry()` weglassen, denn das ist ja schon klar weil das auf dem Typ `Entry` aufgerufen wird.

`create_entry()` ist fehlerhaft weil `now()` mehrfach aufgerufen wird, zwischen diesen Aufrufen ja aber Zeit vergeht, also am Ende Datum und Uhr(zeiten) nicht zusammen passen, beispielsweise wenn der Aufruf für das Datum vor Mitternacht, mindestens einer der Aufrufe für die Uhrzeit aber nach Mitternacht passiert. Wenn man einen Zeitpunkt haben will, dann darf man dafür immer nur *einen* Aufruf machen, auch wenn man die Daten dann auf mehrere Variablen aufteilen möchte.

Die Methode ist auch nicht wirklich notwendig, weil man das auch in die `__init__()` schreiben kann.

Es fehlen Fehlerbehandlungen für den Fall das `project` ein "|" oder ein "\n" enthält oder `start` grösser als `end` ist.

Beim schreiben der Einträge fehlt ein Trenner für die Datensätze, also beispielsweise ein "\n".

Das zusammenstückeln von Zeichenketten und Werten mittels ``+`` und `str()` ist eher BASIC als Python. Dafür gibt es die `format()`-Methode auf Zeichenketten und f-Zeichenkettenliterale.

Zwischenstand:

Code: Alles auswählen

#!/usr/bin/env python3
from datetime import datetime

FILENAME_SUFFIX = ".txt"


class Entry:
    DELIMITER = "|"

    def __init__(self, project, date=None, start=None, stop=None):
        for illegal_character in [self.DELIMITER, "\n"]:
            if illegal_character in project:
                raise ValueError(
                    f"`project` contains illegal character"
                    f" ({illegal_character!r})"
                )

        if None in [date, start, stop]:
            now = datetime.now()

            if date is None:
                date = format(now, "%Y-%m-%d")

            if start is None:
                start = format(now, "%H:%M:%S")

            if stop is None:
                stop = format(now, "%H:%M:%S")

        if start > stop:
            raise ValueError(f"start ({start!r}) > stop ({stop!r})")

        self.project = project
        self.date = date
        self.start = start
        self.stop = stop

    def __repr__(self):
        return (
            f"{self.__class__.__name__}"
            f"({self.project!r}, {self.date!r}, {self.start!r}, {self.stop!r})"
        )

    def __str__(self):
        return self.DELIMITER.join(
            [self.project, self.date, self.start, self.stop]
        )

    @classmethod
    def parse(cls, text):
        return cls(*text.rstrip().split(cls.DELIMITER))


def save_entries(entries, filename):
    with open(filename + FILENAME_SUFFIX, "w", encoding="utf-8") as file:
        file.writelines(f"{entry}\n" for entry in entries)


def load_entries(filename):
    with open(filename + FILENAME_SUFFIX, "r", encoding="utf-8") as lines:
        return list(map(Entry.parse, lines))


def main():
    entries = [Entry(f"P-21-001-{index}") for index in range(5)]
    save_entries(entries, "test")
    print("Wrote file...")
    print(load_entries("test"))


if __name__ == "__main__":
    main()
Ich würde die Daten im `Entry` nicht als Zeichenkette speichern sondern als `datetime.date` und `datetime.time`-Objekte und nur zur Aus- und Eingabe in Zeichenketten wandeln beziehungsweise aus Zeichenketten parsen. Denn irgendwann wird man damit auch mal rechnen wollen, und dann wird es hässlich mit Umwandlungscode jedes mal wenn man irgendwo rechnen will/muss.

(De)serialisieren würde ich auch nicht in dem `Entry`-Objekt machen sondern in den Lade-/Speicherfunktionen. Und da dann auch weniger selber basteln, sondern etwas wie JSON(-Lines) oder CSV verwenden. Dann kann auch `project` alles enthalten was in einem Dateiformat eventuell eine besondere Bedeutung hätte. Man braucht das dann nicht mehr prüfen, und die Bibliothek zum Lesen und Schreiben des Datenformats kümmert sich um die Sonderbehandlung.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
mechanicalStore
User
Beiträge: 91
Registriert: Dienstag 29. Dezember 2009, 00:09

Hallo __blackjack__,

vielen Dank für die Hinweise, das ist sehr hilfreich. Bzgl. Fehlerabfragen, Namensgebung, usw. - das war nur ein grober Entwurf. Soll aber keine Ausrede sein, Du hast in allem Recht. Hier noch ein paar Fragen;

Ich sehe keinen Aufruf, der __repr__ benutzt?

Was bedeuten die "!r" in den f-formatierungen? Dazu habe ich nirgends was gefunden. Z.B auch nicht hier: https://docs.python.org/3/library/strin ... formatspec

Ich hätte jetzt my_entry_list nicht als "global" in dem Sinne gesehen. Wollte gerne, dass alles in einer Klasse quasi gekapselt ist. Denn diese Liste ist nicht nur zum einmaligen Erzeugen und Anzeigen gedacht. Es sollen zur Laufzeit Einträge hinzugefügt, gelöscht, geändert, gesucht, berechnet, usw. werden können. Das wollte ich alles in die Klasse packen. Das erzeugen in main() von 5 Instanzen war nur ein Test. Würdest Du also "Entries" wirklich in main() erzeugen und da halten? Und allen Funktionen, die darauf arbeiten, die Liste mitgeben?

Das Speichern in json erschließt sich mir noch nicht. derElch hat oben einen Link gepostet (danke auch Dir, derElch). Ist es so, dass mit object.__dict__ die interne Objektstruktur in json serialisiert werden könnte? Zeilenweise?
Benutzeravatar
__blackjack__
User
Beiträge: 13003
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@mechanicalStore: `Entry.__repr__()` wird von `list.__repr__()` benutzt, das wiederum von `list.__str__()` verwendet wird, das vom `print()` verwendet wird das in der letzten Zeile von `main()` steht.

Das "!r" steht direkt davor im übergeordneten Kapitel Format String Syntax und ist in der Grammatik unter dem Namen `conversion` zu finden. "r" steht dabei für `repr` ("s" für `str` (default) und "a" für `ascii`).

`my_entry_list` gibt es genau einmal und es ist von überall her über `MyEntry.my_entry_list` erreichbar, wird schon beim Import des Moduls erstellt, und es wird nicht übergeben sondern einfach ”magisch” in Funktionen/Methoden verwendet. Das ist globaler Zustand im Sinne von globalem Zustand der Böse™ ist. Man kann beispielsweise keine Tests schreiben die parallel laufen können, oder zwei solcher Sequenzen gleichzeitig haben, beispielsweise um eine zusätzliche Datei mit den Daten im Speicher abzugleichen.

Globalen Zustand vermeidet man, in dem man Werte als Argumente übergibt. Wobei ich zum Erzeugen eines Eintrags nicht die Liste irgend wo hin übergeben würde, oder zum laden. Erzeugen und hinzufügen sind separate Operationen und laden aus einer Datei sollte einfach eine neue Liste mit dem Inhalt liefern.

Speichern als JSON würde ich weniger ”magisch” machen weil `__dict__` oder `vars()` nur in eher simplen Fällen einfach so funktioniert. Zum Beispiel nicht automatisch rekursiv und die Werte müssen alle selbst direkt JSON-serialisierbar sein, was sie beispielsweise nicht sind wenn man `datetime.time`- und `datetime.date`-Objekte verwendet statt das alles als Zeichenketten im Speicher vorzuhalten. Man schreibt da in der Regel explizit Code der Objekte in JSON-serialisierbare Strukturen umwandelt und die dann mit dem `json`-Modul serialisiert, und für den umgekehrten Weg dann Code der aus solchen Strukturen wieder Objekte macht.

Wenn Du Einträge nachträglich ändern willst, wäre aber auch eine Datenbank wohl besser geeignet, denn sowohl ein CSV-ähnliches Textformat oder direkt CSV, als auch JSON können keine einzelnen Werte in der Datei verändern ohne immer komplett alle Daten zu schreiben. Und wenn man das halbwegs sicher machen will gegen Datenverlust wenn das Programm beim schreiben beendet wird, muss man auch erst in eine temporäre Datei schreiben und die dann in die Zieldatei umbenennen. Die Python-Standardbibliothek bringt `sqlite3` mit, das man stattdessen verwenden könnte. Spätestens dann würde ich auch nicht Datum, Start- und Endzeit als drei Werte speichern, sondern nur Start- und Endzeitpunkt jeweils inklusive Datum. Die beiden Werte sind zum Rechnen praktischer und lassen sich prima auf SQL's TIMESTAMP-Datentyp abbilden.

Bei relationalen Datenbanken nehme ich auch immer gerne SQLAlchemy mit ins Boot. Das macht das Leben einfacher.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
mechanicalStore
User
Beiträge: 91
Registriert: Dienstag 29. Dezember 2009, 00:09

__blackjack__ hat geschrieben: Montag 9. Mai 2022, 13:47
Man kann beispielsweise keine Tests schreiben die parallel laufen können, oder zwei solcher Sequenzen gleichzeitig haben, beispielsweise um eine zusätzliche Datei mit den Daten im Speicher abzugleichen.

Globalen Zustand vermeidet man, in dem man Werte als Argumente übergibt. Wobei ich zum Erzeugen eines Eintrags nicht die Liste irgend wo hin übergeben würde, oder zum laden. Erzeugen und hinzufügen sind separate Operationen und laden aus einer Datei sollte einfach eine neue Liste mit dem Inhalt liefern.
Vermutlich verstehe ich nicht richtig, was Du meinst. Einerseits soll es keine gobale Liste sein, andererseits würdest Du die Liste auch nicht irgendwo hin übergeben. Das (und die Tatsache mehrer möglicher Listen) impliziert, dass es nur den Weg gibt, z.B. eine Klasse EntryList zu erzeugen, in deren Instanzen jeweils eine eigene Liste liegt ?!
Die Python-Standardbibliothek bringt `sqlite3` mit, das man stattdessen verwenden könnte. Spätestens dann würde ich auch nicht Datum, Start- und Endzeit als drei Werte speichern, sondern nur Start- und Endzeitpunkt jeweils inklusive Datum. Die beiden Werte sind zum Rechnen praktischer und lassen sich prima auf SQL's TIMESTAMP-Datentyp abbilden.

Bei relationalen Datenbanken nehme ich auch immer gerne SQLAlchemy mit ins Boot. Das macht das Leben einfacher.
Sollte das Programm mal funktionieren, will ich es auf dem Firmenrechner laufen lassen. Der ist allerdings so eingeschränkt, dass sich da kein SQL-Server, etc. installieren lässt. Daher will ich das ganze mit Bordmitteln speichern können.
Benutzeravatar
__blackjack__
User
Beiträge: 13003
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@mechanicalStore: Ich würde die Liste schon übergeben aber nicht um ein Element hinzuzufügen. Also nicht ``create_entry(entries, project)`` um einen neuen Eintrag zu erzeugen und der Liste hinzuzufügen was Deine Klassenmethode ja macht, sondern die Funktion wirklich nur den Eintrag erstellen lassen. Also eher ``entries.append(create_entry(project))``. Sofern man wirklich eine Funktion oder Klassenmethode zusätzlich zur `__init__()` für so etwas braucht. Wenn `EntryList` nichts weiter tut als *eine* Liste als Attribut zu halten nur das bietet was Listen sowieso schon haben, dann braucht es dafür keine eigene Klasse.

`sqlite3` ist ein Modul in der Standardbibliothek, da braucht man nichts weiter zu installieren.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
mechanicalStore
User
Beiträge: 91
Registriert: Dienstag 29. Dezember 2009, 00:09

__blackjack__ hat geschrieben: Montag 9. Mai 2022, 20:38 Wenn `EntryList` nichts weiter tut als *eine* Liste als Attribut zu halten nur das bietet was Listen sowieso schon haben, dann braucht es dafür keine eigene Klasse.
Das ist klar. Ich wollte da alles andere mit rein packen, wie z.B. Berechnung der Gesamtzeit eines/aller Projekte in einem bestimmten Zeitraum, oder die Arbeitszeit eines Tages, Schreiben dieser Daten in ein beliebiges Ziel (z.B. csv, excel, eMail, etc). Aber generell wäre es, wenn ich Dich richtig verstehe, auch einfach besser, die Liste on the fly zu erzeugen, und das ganze functional denn objectoriented zu betrachten.
__blackjack__ hat geschrieben: Montag 9. Mai 2022, 20:38 `sqlite3` ist ein Modul in der Standardbibliothek, da braucht man nichts weiter zu installieren.
Ahja, ich dachte, das wäre nur das Binding. Da muss ich mich jetzt erst in die Syntax einlesen. Datentypen sind ja auch nur native, soweit ich das sehe, gibt es aber adapter speziell für date und datetime. Grob drüber geschaut, kann man da auch was machen mit __conform__, aber offenbar nicht für date und datetime geeignet?!
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Das es besser ‘funktional’ (nicht ganz der richtige Begriff, procedural wäre eher geeignet) wäre, ist nicht richtig. Nur eine Klasse für ein einziges Attribut ist es. In besagtes Objekt aber “alles andere” reinzupacken klingt auch erstmal problematisch. Klassen sollten möglichst nur eine Aufgabe haben.

Mit datetime kann man mit SQLite gut arbeiten. Was fehlt dir da?
mechanicalStore
User
Beiträge: 91
Registriert: Dienstag 29. Dezember 2009, 00:09

__deets__ hat geschrieben: Dienstag 10. Mai 2022, 08:37 Mit datetime kann man mit SQLite gut arbeiten. Was fehlt dir da?
Ich lese mich gerade erst ein, was das Pythonmodul betrifft. Bis jetzt ist klar, dass Variablen in den Abfragen als Tupel übergeben werden, was native Datentypen betrifft:

Aus der Doku:

Code: Alles auswählen

cur.execute("insert into lang values (?, ?)", ("C", 1972))
Unklar ist eher der Zusammenhang/Funktionsweise eines Adapters:

Aus der Doku:

Code: Alles auswählen

def adapt_datetime(ts):
    return time.mktime(ts.timetuple())

sqlite3.register_adapter(datetime.datetime, adapt_datetime)

con = sqlite3.connect(":memory:")
cur = con.cursor()

now = datetime.datetime.now()
cur.execute("select ?", (now,))
Mir fehlt so ein wenig das Verständnis, was hier von wo nach wo konvertiert wird, in welche m Format das datum dann tatsächlich in der Datenbank liegt (string?), bzw. wie die Rückumwandlung funktioniert. Auch ist mir unklar "select ?". Auf welche Tabelle bezieht sich das?
Und letztlich, was bedeutet das für meine Entry Klasse (s.O). Kann/muss ich einen Adapter für ein komplettes Objekt (also Projekt=string, date=datum, usw) bauen, bzw. lassen sich dort dann die datetime typen mit integrieren, oder würde sich der Adapter nur auf die nicht nativen Daten beziehen?
Benutzeravatar
__blackjack__
User
Beiträge: 13003
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Das SELECT bezieht sich auf gar keine Tabelle sondern einfach auf den Wert. Und in dem Code fehlt dann wohl noch, dass man sich das Ergebnis vom SELECT auch vom Cursor abholt und ausgibt, damit man sieht was das Ergebnis ist:

Code: Alles auswählen

In [29]: def adapt_datetime(ts): 
    ...:     return time.mktime(ts.timetuple()) 
    ...:  
    ...: sqlite3.register_adapter(datetime.datetime, adapt_datetime) 
    ...:  
    ...: con = sqlite3.connect(":memory:") 
    ...: cur = con.cursor() 
    ...:  
    ...: now = datetime.datetime.now() 
    ...: cur.execute("select ?", (now,))                                        
Out[29]: <sqlite3.Cursor at 0x7f8853f0e180>

In [30]: cur.fetchone()                                                         
Out[30]: (1652172420.0,)
Für `date` und `datetime` gibt es aber auch bereits Adapter, und Konverter für die Rückrichtung:

Code: Alles auswählen

In [1]: import sqlite3                                                          

In [2]: sqlite3.adapters                                                        
Out[2]: 
{(datetime.date,
  sqlite3.PrepareProtocol): <function sqlite3.dbapi2.register_adapters_and_converters.<locals>.adapt_date(val)>,
 (datetime.datetime,
  sqlite3.PrepareProtocol): <function sqlite3.dbapi2.register_adapters_and_converters.<locals>.adapt_datetime(val)>}
In [3]: sqlite3.converters                                                      
Out[3]: 
{'DATE': <function sqlite3.dbapi2.register_adapters_and_converters.<locals>.convert_date(val)>,
 'TIMESTAMP': <function sqlite3.dbapi2.register_adapters_and_converters.<locals>.convert_timestamp(val)>}
Allerdings ist das hier IMHO schon ein guter Punkt SQLAlchemy einzusetzen.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
mechanicalStore
User
Beiträge: 91
Registriert: Dienstag 29. Dezember 2009, 00:09

Wenn ich das Beispiel aus der Doku testhalber wie folgt abwandle:

Code: Alles auswählen

import sqlite3
import datetime
import time

def adapt_datetime(ts):
    return time.mktime(ts.timetuple())

sqlite3.register_adapter(datetime.datetime, adapt_datetime)

con = sqlite3.connect("testdatabase.db")
cur = con.cursor()
cur.execute('create table datetable(something text, sometime datetime)')

now = datetime.datetime.now()
print(now)

cur.execute("insert into datetable values(?, ?)", ('test', now))
con.commit()

for row in cur.execute('select * from datetable'):
    print(row)

con.close()
erhalte ich als Ausgabe:

Code: Alles auswählen

2022-05-10 11:40:23.840789
('test', 1652175623)
Offenbar geht also bei der (rück)konvertierung was schief?!
Benutzeravatar
__blackjack__
User
Beiträge: 13003
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

So funktioniert das bei mir, aber wie gesagt würde ich mich damit gar nicht beschäftigen wollen, sondern einfach SQLAlchemy verwenden.

Code: Alles auswählen

import datetime
import sqlite3

con = sqlite3.connect(":memory:", detect_types=True)
cur = con.cursor()
cur.execute("CREATE TABLE datetable (something TEXT, sometime TIMESTAMP)")

now = datetime.datetime.now()
print(now)

cur.execute("INSERT INTO datetable VALUES(?, ?)", ("test", now))
con.commit()

for row in cur.execute('SELECT * FROM datetable'):
    print(row)

con.close()
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
mechanicalStore
User
Beiträge: 91
Registriert: Dienstag 29. Dezember 2009, 00:09

__blackjack__ hat geschrieben: Dienstag 10. Mai 2022, 10:51 So funktioniert das bei mir, aber wie gesagt würde ich mich damit gar nicht beschäftigen wollen, sondern einfach SQLAlchemy verwenden.
Naja, es geht hier im Moment lediglich um 4 Einträge pro Zeile. Du hast mich ja schon von sqlite statt json überzeugt. SQLAlchemy müsste ich mich jetzt auch noch einlesen, bis ich ein brauchbares Ergebnis habe. Ich habe ja schon Probleme mit sqlite. Das würde ich ggf. später noch ändern und jetzt erst mal so weiter machen, um zu einem Ergebnis zu kommen. Ich will auch dazu keine fertige Lösung präsentiert bekommen, das wäre nicht mein Lerneffekt.
__blackjack__ hat geschrieben: Dienstag 10. Mai 2022, 10:51

Code: Alles auswählen

import datetime
import sqlite3

con = sqlite3.connect(":memory:", detect_types=True)
cur = con.cursor()
cur.execute("CREATE TABLE datetable (something TEXT, sometime TIMESTAMP)")

now = datetime.datetime.now()
print(now)

cur.execute("INSERT INTO datetable VALUES(?, ?)", ("test", now))
con.commit()

for row in cur.execute('SELECT * FROM datetable'):
    print(row)

con.close()
Danke. Dazu ein paar Fragen. Wo hätte ich denn nachlesen können, dass ich

Code: Alles auswählen

TIMESTAMP
verwenden muss, statt

Code: Alles auswählen

datetime
? Und wieso funktioniert Deine Variante ohne sqlite3.register_adapter? ist

Code: Alles auswählen

detect_types=True
die Antwort? Bzw. sind diese datentypen default registriert?
Benutzeravatar
__blackjack__
User
Beiträge: 13003
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@mechanicalStore: Im Abschnitt Default adapters and converters steht das für TIMESTAMP und DATE bereits Adapter/Konverter existieren. Da steht auch welche Werte für `detect_types` eigentlich richtig wären. Meins hat nur zufällig funktioniert. Ich benutze das so in der Regel halt auch nicht wegen SQLAlchemy. Weiss nicht ob ich das schon mal erwähnt habe. 😎

Wobei man diese beiden Typen in diesem Beitrag hier im Thema ja schon mal gesehen hat, dass es da Adapter/Konverter für gibt: viewtopic.php?p=405779#p405779

Hier mal das Beispiel mit dem SQLAlchemy-ORM:

Code: Alles auswählen

#!/usr/bin/env python3
from datetime import datetime as DateTime

from sqlalchemy import INTEGER, TEXT, TIMESTAMP, Column, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

Session = sessionmaker()
Base = declarative_base()


class Entry(Base):
    __tablename__ = "entry"

    id = Column(INTEGER, primary_key=True)
    project = Column(TEXT, nullable=False)
    start = Column(TIMESTAMP, nullable=False, default=DateTime.now)
    stop = Column(TIMESTAMP, nullable=False, default=DateTime.now)


def main():
    engine = create_engine("sqlite:///:memory:")
    Base.metadata.create_all(engine)
    session = Session(bind=engine)

    for index in range(5):
        session.add(Entry(project=f"P-21-001-{index}"))
    session.commit()

    for entry in session.query(Entry):
        print(
            f"{entry.project} on {entry.start.date()}"
            f" from {entry.start.time()} to {entry.stop.time()}"
        )


if __name__ == "__main__":
    main()
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
mechanicalStore
User
Beiträge: 91
Registriert: Dienstag 29. Dezember 2009, 00:09

__blackjack__ hat geschrieben: Mittwoch 11. Mai 2022, 00:10 ...Meins hat nur zufällig funktioniert. Ich benutze das so in der Regel halt auch nicht wegen SQLAlchemy. Weiss nicht ob ich das schon mal erwähnt habe. 😎
:mrgreen:
__blackjack__ hat geschrieben: Mittwoch 11. Mai 2022, 00:10 Wobei man diese beiden Typen in diesem Beitrag hier im Thema ja schon mal gesehen hat, dass es da Adapter/Konverter für gibt: viewtopic.php?p=405779#p405779
Ups...
__blackjack__ hat geschrieben: Mittwoch 11. Mai 2022, 00:10 Hier mal das Beispiel mit dem SQLAlchemy-ORM:

Code: Alles auswählen

#!/usr/bin/env python3
...
class Entry(Base):
    __tablename__ = "entry"

    id = Column(INTEGER, primary_key=True)
    project = Column(TEXT, nullable=False)
    start = Column(TIMESTAMP, nullable=False, default=DateTime.now)
    stop = Column(TIMESTAMP, nullable=False, default=DateTime.now)
Wenn ich das ursprüngliche Subjekt zurück denke, und wo bin ich jetzt gelandet... :mrgreen: An der Stelle schon mal vielen Dank, ich habe eine dazu Menge gelernt, und das ist ja auch das eigentliche Ziel.
Frage aber jetzt; ich habe den Abriss zu SQLAlchemy auch mal in meinem Buch nachgelesen, um Dein Beispiel besser nachvollziehen zu können. Da steht auch "...it's also possible, to use SQLAlchemy to map a table direct to a class...' , das entspricht ja auch Deiner Umsetzung. Aber warum sind das Klassenvariablen? Nach einer Abfrage will ich doch eine Liste mit Objektinstanzen haben, die ich dann weiter verarbeiten kann?! Oder ist diese Klasse nur für das Erzeugen/Abfragen der Tabelle gedacht?
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Das sind Klassenvariablen weil SA Python hier als "Domain Specific Language" (DSL) "missbraucht". Du deklarierst da etwas, das dann aber zur Laufzeit in eigentliche Klassen umgesetzt wird, und davon dann wiederum Objekte erzeugt werden.
Benutzeravatar
__blackjack__
User
Beiträge: 13003
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@mechanicalStore: Die Klassenvariablen beschreiben die gleichnamigen Attribute, die einzelne Exemplare haben. `project` ist auf der Klasse ein `InstrumentedAttribute`-Objekt und auf einem `Entry`-Objekt gibt es *auch* ein `project`-Attribut, welches dann eine Zeichenkette ist.

Code: Alles auswählen

    entry = Entry(project="test project")
    session.add(entry)
    session.commit()
    
    print(type(Entry.project))
    print(type(entry.project), entry.project)
    print(type(Entry.start))
    print(type(entry.start), entry.start)
Ausgabe:

Code: Alles auswählen

<class 'sqlalchemy.orm.attributes.InstrumentedAttribute'>
<class 'str'> test project
<class 'sqlalchemy.orm.attributes.InstrumentedAttribute'>
<class 'datetime.datetime'> 2022-05-11 11:29:47.777694
In der `Base`-Klasse steckt ein bisschen Metaklassenmagie welche die `Column`-Objekte verarbeitet/austauscht und auch ein DB-Schema über alle abgeleiteten Klassen erstellt, weshalb man dann auch mit ``Base.metadata.create_all(engine)`` die ganzen Tabellen (im Beispiel nur eine) in der Datenbank anlegen kann.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
mechanicalStore
User
Beiträge: 91
Registriert: Dienstag 29. Dezember 2009, 00:09

__blackjack__ hat geschrieben: Mittwoch 11. Mai 2022, 10:34 In der `Base`-Klasse steckt ein bisschen Metaklassenmagie welche die `Column`-Objekte verarbeitet/austauscht und auch ein DB-Schema über alle abgeleiteten Klassen erstellt, weshalb man dann auch mit ``Base.metadata.create_all(engine)`` die ganzen Tabellen (im Beispiel nur eine) in der Datenbank anlegen kann.
Das erklärt es natürlich, danke! Für Leute, die hier mitlesen, vielleicht noch folgender Link, der leicht zu finden ist, aber Leuten wir mir, die bis vor 2 Tagen noch nie von SQLAlchemy gehört haben, zusätzlich zum thread auf die Sprünge hilft. Hier kann man auch die Metadaten "sehen":

https://docs.sqlalchemy.org/en/14/orm/quickstart.html
mechanicalStore
User
Beiträge: 91
Registriert: Dienstag 29. Dezember 2009, 00:09

Schon wieder Fragen...

Code: Alles auswählen

#!/usr/bin/env python3
from datetime import datetime as DateTime

from sqlalchemy import INTEGER, TEXT, TIMESTAMP, Column, create_engine, select
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

# DATABASE_NAME = 'project_database.db'
Base = declarative_base()
Session = sessionmaker()


class Entry(Base):
    __tablename__ = "entry"

    id = Column(INTEGER, primary_key=True)
    project = Column(TEXT, nullable=False)
    start = Column(TIMESTAMP, nullable=False, default=DateTime.now)
    stop = Column(TIMESTAMP, nullable=False, default=DateTime.now)

def connect():
    return create_engine("sqlite:///:memory:")

def make_session(engine):
    return Session(bind=engine)

def generate_tables(engine):
    Base.metadata.create_all(engine)

def main():
    engine = connect()
    generate_tables(engine)
    session = make_session(engine)

    # Some testing
    for index in range(5):
        dt = DateTime(2022, 5, 12, 5 + index, 5, 14)
        dts = DateTime(2022, 5, 12, 6 + index, 5, 14)
        session.add(Entry(project=f"P-21-001", start = dt, stop = dts))
        dtx = DateTime(2022, 5, 13, 5 + index, 5, 14)
        dtsx = DateTime(2022, 5, 13, 6 + index, 5, 14)
        session.add(Entry(project=f"P-22-001", start = dtx, stop = dtsx))
    session.commit()

    sel = select(Entry).where(Entry.project == "P-22-001" and Entry.start.date() == (2022,5,12))
    elapsed_time = 0
    for entry in session.scalars(sel):
        print(
            f"{entry.project} on {entry.start.date()}"
            f" from {entry.start.time()} to {entry.stop.time()}"
        )
        # elapsed_time = elapsed_time + (entry.stop.time() - entry.start.time())
    print(elapsed_time)

if __name__ == '__main__':
    main()
Im select statement kann ich für Entry.start.date() schreiben, was ich will (ob Tupel, oder String), wird alles nicht mit einbezogen. Die Berechnung elapsed_time funktioniert nicht, es gibt da offenbar keinen Operator "-" ?!
Antworten