Datenstruktur zur Pflege von Tabellendaten im Speicher?

80

Mein Szenario lautet wie folgt: Ich habe eine Datentabelle (eine Handvoll Felder, weniger als hundert Zeilen), die ich in meinem Programm häufig verwende. Ich brauche diese Daten auch, um dauerhaft zu sein, also speichere ich sie als CSV und lade sie beim Start. Ich entscheide mich, keine Datenbank zu verwenden, da jede Option (auch SQLite) ein Overkill für meine bescheidene Anforderung ist (auch - ich möchte die Werte auf einfache Weise offline bearbeiten können und nichts ist einfacher als der Editor).

Angenommen, meine Daten sehen wie folgt aus (in der Datei ist das Komma ohne Titel getrennt, dies ist nur eine Illustration):

 Row  | Name     | Year   | Priority
------------------------------------
 1    | Cat      | 1998   | 1
 2    | Fish     | 1998   | 2
 3    | Dog      | 1999   | 1 
 4    | Aardvark | 2000   | 1
 5    | Wallaby  | 2000   | 1
 6    | Zebra    | 2001   | 3

Anmerkungen:

  1. Zeile kann ein "realer" Wert sein, der in die Datei geschrieben wird, oder nur ein automatisch generierter Wert, der die Zeilennummer darstellt. So oder so existiert es im Speicher.
  2. Namen sind einzigartig.

Dinge, die ich mit den Daten mache:

  1. Suchen Sie eine Zeile basierend auf ID (Iteration) oder Name (direkter Zugriff).
  2. Zeigen Sie die Tabelle in verschiedenen Reihenfolgen an, basierend auf mehreren Feldern: Ich muss sie z. B. nach Priorität und dann nach Jahr oder nach Jahr und dann nach Priorität usw. sortieren.
  3. Ich muss Instanzen basierend auf Parametersätzen zählen, z. B. wie viele Zeilen ihr Jahr zwischen 1997 und 2002 haben oder wie viele Zeilen 1998 und Priorität> 2 usw. sind.

Ich weiß, dass dies nach SQL "schreit" ...

Ich versuche herauszufinden, was die beste Wahl für die Datenstruktur ist. Es folgen verschiedene Möglichkeiten, die ich sehe:

Liste der Zeilenlisten:

a = []
a.append( [1, "Cat", 1998, 1] )
a.append( [2, "Fish", 1998, 2] )
a.append( [3, "Dog", 1999, 1] )
...

Liste der Spaltenlisten (es wird offensichtlich eine API für add_row usw. geben):

a = []
a.append( [1, 2, 3, 4, 5, 6] )
a.append( ["Cat", "Fish", "Dog", "Aardvark", "Wallaby", "Zebra"] )
a.append( [1998, 1998, 1999, 2000, 2000, 2001] )
a.append( [1, 2, 1, 1, 1, 3] )

Wörterbuch der Spaltenlisten (Konstanten können erstellt werden, um die Zeichenfolgenschlüssel zu ersetzen):

a = {}
a['ID'] = [1, 2, 3, 4, 5, 6]
a['Name'] = ["Cat", "Fish", "Dog", "Aardvark", "Wallaby", "Zebra"] 
a['Year'] = [1998, 1998, 1999, 2000, 2000, 2001] 
a['Priority'] = [1, 2, 1, 1, 1, 3] 

Wörterbuch mit Schlüsseln als Tupel von (Zeile, Feld):

Create constants to avoid string searching
NAME=1
YEAR=2
PRIORITY=3

a={}
a[(1, NAME)] = "Cat"
a[(1, YEAR)] = 1998
a[(1, PRIORITY)] = 1
a[(2, NAME)] = "Fish"
a[(2, YEAR)] = 1998
a[(2, PRIORITY)] = 2
...

Und ich bin mir sicher, dass es andere Wege gibt ... Jeder Weg hat jedoch Nachteile, wenn es um meine Anforderungen geht (komplexe Bestellung und Zählung).

Was ist der empfohlene Ansatz?

BEARBEITEN:

Zur Verdeutlichung ist die Leistung für mich kein großes Problem. Da die Tabelle so klein ist, glaube ich, dass fast jede Operation im Bereich von Millisekunden liegen wird, was für meine Anwendung kein Problem darstellt.

Roee Adler
quelle
3
Keine Notwendigkeit, Antworten zu lesen. Ihre Frage gab eine Antwort auf meine Frage! Vielen Dank für so eine tolle Frage :)
Gajendra Bagali

Antworten:

80

Eine "Tabelle" im Speicher zu haben, die Nachschlagen, Sortieren und willkürliche Aggregation erfordert, erfordert wirklich SQL. Sie sagten, Sie hätten SQLite ausprobiert, aber haben Sie erkannt, dass SQLite eine Nur-Speicher-Datenbank verwenden kann?

connection = sqlite3.connect(':memory:')

Anschließend können Sie Tabellen im Speicher mit allen Funktionen von SQLite erstellen / löschen / abfragen / aktualisieren, ohne dass Dateien übrig bleiben, wenn Sie fertig sind. Und ab Python 2.5sqlite3 befindet es sich in der Standardbibliothek, so dass es IMO nicht wirklich "übertrieben" ist.

Hier ist ein Beispiel, wie man die Datenbank erstellen und füllen kann:

import csv
import sqlite3

db = sqlite3.connect(':memory:')

def init_db(cur):
    cur.execute('''CREATE TABLE foo (
        Row INTEGER,
        Name TEXT,
        Year INTEGER,
        Priority INTEGER)''')

def populate_db(cur, csv_fp):
    rdr = csv.reader(csv_fp)
    cur.executemany('''
        INSERT INTO foo (Row, Name, Year, Priority)
        VALUES (?,?,?,?)''', rdr)

cur = db.cursor()
init_db(cur)
populate_db(cur, open('my_csv_input_file.csv'))
db.commit()

Wenn Sie SQL wirklich nicht verwenden möchten, sollten Sie wahrscheinlich eine Liste von Wörterbüchern verwenden:

lod = [ ] # "list of dicts"

def populate_lod(lod, csv_fp):
    rdr = csv.DictReader(csv_fp, ['Row', 'Name', 'Year', 'Priority'])
    lod.extend(rdr)

def query_lod(lod, filter=None, sort_keys=None):
    if filter is not None:
        lod = (r for r in lod if filter(r))
    if sort_keys is not None:
        lod = sorted(lod, key=lambda r:[r[k] for k in sort_keys])
    else:
        lod = list(lod)
    return lod

def lookup_lod(lod, **kw):
    for row in lod:
        for k,v in kw.iteritems():
            if row[k] != str(v): break
        else:
            return row
    return None

Testen ergibt dann:

>>> lod = []
>>> populate_lod(lod, csv_fp)
>>> 
>>> pprint(lookup_lod(lod, Row=1))
{'Name': 'Cat', 'Priority': '1', 'Row': '1', 'Year': '1998'}
>>> pprint(lookup_lod(lod, Name='Aardvark'))
{'Name': 'Aardvark', 'Priority': '1', 'Row': '4', 'Year': '2000'}
>>> pprint(query_lod(lod, sort_keys=('Priority', 'Year')))
[{'Name': 'Cat', 'Priority': '1', 'Row': '1', 'Year': '1998'},
 {'Name': 'Dog', 'Priority': '1', 'Row': '3', 'Year': '1999'},
 {'Name': 'Aardvark', 'Priority': '1', 'Row': '4', 'Year': '2000'},
 {'Name': 'Wallaby', 'Priority': '1', 'Row': '5', 'Year': '2000'},
 {'Name': 'Fish', 'Priority': '2', 'Row': '2', 'Year': '1998'},
 {'Name': 'Zebra', 'Priority': '3', 'Row': '6', 'Year': '2001'}]
>>> pprint(query_lod(lod, sort_keys=('Year', 'Priority')))
[{'Name': 'Cat', 'Priority': '1', 'Row': '1', 'Year': '1998'},
 {'Name': 'Fish', 'Priority': '2', 'Row': '2', 'Year': '1998'},
 {'Name': 'Dog', 'Priority': '1', 'Row': '3', 'Year': '1999'},
 {'Name': 'Aardvark', 'Priority': '1', 'Row': '4', 'Year': '2000'},
 {'Name': 'Wallaby', 'Priority': '1', 'Row': '5', 'Year': '2000'},
 {'Name': 'Zebra', 'Priority': '3', 'Row': '6', 'Year': '2001'}]
>>> print len(query_lod(lod, lambda r:1997 <= int(r['Year']) <= 2002))
6
>>> print len(query_lod(lod, lambda r:int(r['Year'])==1998 and int(r['Priority']) > 2))
0

Persönlich mag ich die SQLite-Version besser, da sie Ihre Typen besser bewahrt (ohne zusätzlichen Konvertierungscode in Python) und leicht wächst, um zukünftigen Anforderungen gerecht zu werden. Aber andererseits bin ich mit SQL ziemlich vertraut, also YMMV.

Rick Copeland
quelle
2
Was ist der empfohlene Ansatz zum Auffüllen der Datenbank in diesem Beispiel?
Roee Adler
4
Ich habe die Antwort so bearbeitet, dass sie Beispielcode zum Auffüllen der Datenbank aus einer CSV-Datei enthält.
Rick Copeland
1
-1: Overkill, eine Liste von Wörterbüchern ist für diese Anwendung wahrscheinlich nützlicher.
S.Lott
11
Listen mit Wörterbüchern machen die Abfragen, Sortierung und Aggregation viel ausführlicher, selbst bei Listenverständnissen und beim Sortieren (..., key =). Ich würde also sagen, dass SQLite in-Memory hier genau das Richtige ist. Jedem sein eigenes ...
Rick Copeland
2
Ich habe der Antwort auch die Option Liste der Wörterbücher hinzugefügt.
Rick Copeland
35

Eine sehr alte Frage, die ich kenne, aber ...

Ein Pandas DataFrame scheint hier die ideale Option zu sein.

http://pandas.pydata.org/pandas-docs/version/0.13.1/generated/pandas.DataFrame.html

Aus dem Klappentext

Zweidimensionale größenveränderliche, möglicherweise heterogene tabellarische Datenstruktur mit beschrifteten Achsen (Zeilen und Spalten). Arithmetische Operationen werden sowohl auf Zeilen- als auch auf Spaltenbeschriftungen ausgerichtet. Kann als diktatartiger Container für Serienobjekte betrachtet werden. Die primäre Pandas-Datenstruktur

http://pandas.pydata.org/

user5061
quelle
20

Ich persönlich würde die Liste der Zeilenlisten verwenden. Da die Daten für jede Zeile immer in derselben Reihenfolge vorliegen, können Sie einfach nach jeder der Spalten sortieren, indem Sie einfach auf dieses Element in jeder der Listen zugreifen. Sie können auch einfach anhand einer bestimmten Spalte in jeder Liste zählen und auch suchen. Es ist im Grunde so nah wie an einem 2D-Array.

Der einzige Nachteil hierbei ist, dass Sie wissen müssen, in welcher Reihenfolge sich die Daten befinden. Wenn Sie diese Reihenfolge ändern, müssen Sie Ihre Such- / Sortierroutinen entsprechend ändern.

Sie können auch eine Liste mit Wörterbüchern erstellen.

rows = []
rows.append({"ID":"1", "name":"Cat", "year":"1998", "priority":"1"})

Auf diese Weise müssen Sie die Reihenfolge der Parameter nicht kennen, sodass Sie jedes Feld "Jahr" in der Liste durchsehen können.

AlbertoPL
quelle
7
+1: Wörterbuchlisten funktionieren WIRKLICH gut und sind mit dem Lesen und Schreiben von JSON- oder CSV-Dateien kompatibel.
S.Lott
7

Haben Sie eine Tabellenklasse, deren Zeilen eine Liste von diktierten oder besseren Zeilenobjekten sind

Fügen Sie in der Tabelle keine Zeilen direkt hinzu, sondern verfügen Sie über eine Methode, mit der einige Lookup-Maps aktualisiert werden, z. B. nach Namen. Wenn Sie keine Zeilen in der angegebenen Reihenfolge hinzufügen oder die ID nicht fortlaufend ist, können Sie auch idMap verwenden, z

class Table(object):
    def __init__(self):
        self.rows =  []# list of row objects, we assume if order of id
        self.nameMap = {} # for faster direct lookup for row by name

    def addRow(self, row):
        self.rows.append(row)
        self.nameMap[row['name']] = row

    def getRow(self, name):
        return self.nameMap[name]


table = Table()
table.addRow({'ID':1,'name':'a'})
Anurag Uniyal
quelle
6

Sind Sie angesichts des komplexen Datenabrufs sicher, dass auch SQLite zu viel des Guten ist?

Sie werden eine ad hoc, informell spezifizierte, fehlerbehaftete, langsame Implementierung der Hälfte von SQLite erhalten, die die zehnte Regel von Greenspun umschreibt .

Sie haben jedoch Recht, wenn Sie sagen, dass die Auswahl einer einzelnen Datenstruktur sich auf eine oder mehrere Such-, Sortier- oder Zählvorgänge auswirkt. Wenn also die Leistung an erster Stelle steht und Ihre Daten konstant sind, können Sie in Betracht ziehen, mehr als eine Struktur für verschiedene Zwecke zu verwenden.

Messen Sie vor allem, welche Vorgänge häufiger auftreten, und entscheiden Sie, welche Struktur weniger kostet.

Vinko Vrsalovic
quelle
3

Ich persönlich habe eine Bibliothek geschrieben, die vor kurzem BD_XML heißt

Der grundlegendste Existenzgrund besteht darin, Daten zwischen XML-Dateien und SQL-Datenbanken hin und her zu senden.

Es ist in Spanisch geschrieben (wenn das in einer Programmiersprache wichtig ist), aber es ist sehr einfach.

from BD_XML import Tabla

Es definiert ein Objekt namens Tabla (Tabelle) und kann mit einem Namen zur Identifizierung eines vorab erstellten Verbindungsobjekts einer pep-246-kompatiblen Datenbankschnittstelle erstellt werden.

Table = Tabla('Animals') 

Dann müssen Sie Spalten mit der agregar_columnaMethode (add_column) hinzufügen, mit der verschiedene Schlüsselwortargumente verwendet werden können:

  • campo (Feld): Der Name des Feldes

  • tipo (Typ): Der Typ der gespeicherten Daten kann beispielsweise 'varchar' und 'double' oder der Name von Python-Objekten sein, wenn Sie nicht daran interessiert sind, letztere in eine Datenbank zu exportieren.

  • defecto (Standard): Legen Sie einen Standardwert für die Spalte fest, wenn beim Hinzufügen einer Zeile keiner vorhanden ist

  • Es gibt andere 3, aber nur für Datenbanktöne und nicht wirklich funktionsfähig

mögen:

Table.agregar_columna(campo='Name', tipo='str')
Table.agregar_columna(campo='Year', tipo='date')
#declaring it date, time, datetime or timestamp is important for being able to store it as a time object and not only as a number, But you can always put it as a int if you don't care for dates
Table.agregar_columna(campo='Priority', tipo='int')

Anschließend fügen Sie die Zeilen mit dem Operator + = hinzu (oder +, wenn Sie eine Kopie mit einer zusätzlichen Zeile erstellen möchten).

Table += ('Cat', date(1998,1,1), 1)
Table += {'Year':date(1998,1,1), 'Priority':2, Name:'Fish'}
#…
#The condition for adding is that is a container accessible with either the column name or the position of the column in the table

Anschließend können Sie XML generieren und mit exportar_XML(export_XML) und escribir_XML(write_XML) in eine Datei schreiben :

file = os.path.abspath(os.path.join(os.path.dirname(__file__), 'Animals.xml'))
Table.exportar_xml()
Table.escribir_xml(file)

Importieren Sie es dann mit importar_XML(import_XML) mit dem Dateinamen und dem Hinweis, dass Sie eine Datei und kein Zeichenfolgenliteral verwenden, zurück:

Table.importar_xml(file, tipo='archivo')
#archivo means file

Fortgeschrittene

Auf diese Weise können Sie ein Tabla-Objekt auf SQL-Weise verwenden.

#UPDATE <Table> SET Name = CONCAT(Name,' ',Priority), Priority = NULL WHERE id = 2
for row in Table:
    if row['id'] == 2:
        row['Name'] += ' ' + row['Priority']
        row['Priority'] = None
print(Table)

#DELETE FROM <Table> WHERE MOD(id,2) = 0 LIMIT 1
n = 0
nmax = 1
for row in Table:
    if row['id'] % 2 == 0:
        del Table[row]
        n += 1
        if n >= nmax: break
print(Table)

In diesen Beispielen wird eine Spalte mit dem Namen 'id' angenommen, die jedoch für Ihr Beispiel durch width row.pos ersetzt werden kann.

if row.pos == 2:

Die Datei kann heruntergeladen werden von:

https://bitbucket.org/WolfangT/librerias

Wolfang Torres
quelle