web-dev-qa-db-fra.com

Structure de données pour conserver les données tabulaires en mémoire?

Mon scénario est le suivant: j’ai une table de données (une poignée de champs, moins d’une centaine de lignes) que j’utilise beaucoup dans mon programme. J'ai également besoin que ces données soient persistantes, je les enregistre donc au format CSV et je les charge au démarrage. Je choisis de ne pas utiliser de base de données, car chaque option (même SQLite) est excessive pour mon humble exigence (je voudrais aussi pouvoir modifier les valeurs hors ligne de manière simple, et rien n'est plus simple que le bloc-notes).

Supposons que mes données se présentent comme suit (dans le fichier, elles sont séparées par une virgule sans titre, ceci n’est qu’une 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

Remarques:

  1. La ligne peut être une valeur "réelle" écrite dans le fichier ou simplement une valeur générée automatiquement qui représente le numéro de la ligne. De toute façon, il existe en mémoire.
  2. Les noms sont uniques.

Choses que je fais avec les données:

  1. Recherchez une ligne en fonction de son ID (itération) ou de son nom (accès direct).
  2. Affichez la table dans différents ordres en fonction de plusieurs champs: je dois la trier, par exemple. par priorité puis année, année ou année et ensuite priorité, etc.
  3. Je dois compter les instances en fonction de jeux de paramètres, par exemple. combien de lignes ont leur année entre 1997 et 2002, ou combien de lignes sont en 1998 et de priorité> 2, etc.

Je sais que "pleure" pour SQL ...

J'essaie de comprendre quel est le meilleur choix pour la structure de données. Voici plusieurs choix que je vois:

Liste des listes de lignes:

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

Liste des listes de colonnes (il y aura évidemment une API pour add_row, etc.):

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] )

Dictionnaire des listes de colonnes (des constantes peuvent être créées pour remplacer les clés de chaîne):

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] 

Dictionnaire avec les clés étant des tuples de (Row, Field):

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
...

Et je suis sûr qu'il y a d'autres moyens ... Cependant, chaque solution présente des inconvénients en ce qui concerne mes exigences (commande et comptage complexes).

Quelle est l'approche recommandée?

MODIFIER:

Pour clarifier, la performance n'est pas un problème majeur pour moi. Comme la table est si petite, je pense que presque toutes les opérations seront dans la gamme des millisecondes, ce qui n’est pas une préoccupation pour mon application.

71
Roee Adler

Avoir une "table" en mémoire qui nécessite des recherches, un tri et une agrégation arbitraire appelle vraiment SQL. Vous avez dit avoir essayé SQLite, mais avez-vous réalisé que SQLite peut utiliser une base de données en mémoire uniquement?

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

Vous pouvez ensuite créer/supprimer/interroger/mettre à jour des tables en mémoire avec toutes les fonctionnalités de SQLite et aucun fichier restant lorsque vous avez terminé. Et à compter de Python 2.5, sqlite3 est dans la bibliothèque standard, donc ce n'est pas vraiment "overkill" IMO.

Voici un exemple de création et de remplissage de la base de données:

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()

Si vous préférez vraiment ne pas utiliser SQL, vous devriez probablement utiliser une liste de dictionnaires:

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

Les tests donnent alors:

>>> 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

Personnellement, j'aime mieux la version SQLite, car elle préserve mieux vos types (sans code de conversion supplémentaire en Python) et évolue facilement pour répondre aux exigences futures. Mais là encore, je suis assez à l'aise avec SQL, donc YMMV.

76
Rick Copeland

Une très vieille question que je connais mais ...

A pandas DataFrame semble être l'option idéale ici.

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

Du texte de présentation

Structure de données tabulaire bidimensionnelle, potentiellement hétérogène et modifiable en taille, avec axes étiquetés (lignes et colonnes). Les opérations arithmétiques s'alignent sur les étiquettes des lignes et des colonnes. Peut être considéré comme un conteneur de type dict pour les objets Series. La structure de données primaire pandas

http://pandas.pydata.org/

30
user5061

Personnellement, j'utiliserais la liste des listes de lignes. Étant donné que les données de chaque ligne sont toujours dans le même ordre, vous pouvez facilement trier par n'importe quelle colonne en accédant simplement à cet élément dans chacune des listes. Vous pouvez également compter facilement en fonction d'une colonne particulière de chaque liste et effectuer également des recherches. C'est fondamentalement aussi proche que cela arrive à un tableau 2-d.

En réalité, le seul inconvénient est que vous devez savoir dans quel ordre se trouvent les données. Si vous modifiez cet ordre, vous devrez modifier vos routines de recherche/tri pour les faire correspondre.

Une autre chose que vous pouvez faire est d’avoir une liste de dictionnaires.

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

Cela éviterait d'avoir à connaître l'ordre des paramètres, vous pouvez donc consulter chaque champ "année" de la liste.

17
AlbertoPL

Avoir une classe Table dont les lignes sont une liste d'objets dict ou mieux

Dans la table, n’ajoutez pas directement de lignes, mais utilisez une méthode qui met à jour quelques cartes de référence, par exemple. pour name si vous n’ajoutez pas de lignes dans l’ordre ou id ne sont pas consécutives, vous pouvez aussi avoir idMap, par exemple.

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'})
6
Anurag Uniyal

Premièrement, étant donné que vous avez un scénario de récupération de données complexe, êtes-vous sûr que même SQLite est excessif?

Vous finirez par avoir une implémentation lente ad hoc, spécifiée de manière informelle, lente à la moitié de SQLite, paraphrasant Dixième règle de Greenspun .

Cela dit, vous avez parfaitement raison de dire que le choix d’une structure de données unique aura une incidence sur la recherche, le tri ou le comptage. Ainsi, si les performances sont primordiales et que vos données sont constantes, vous pouvez envisager d’utiliser plusieurs structures à des fins différentes.

Surtout, mesurez quelles opérations seront les plus communes et décidez quelle structure finira par coûter moins cher.

5
Vinko Vrsalovic

Personnellement, j'ai écrit une bibliothèque pour à peu près tout récemment, elle s'appelle BD_XML

car sa raison d’existence la plus fondamentale est de servir de moyen d’échange de données entre des fichiers XML et des bases de données SQL.

Il est écrit en espagnol (si cela compte dans un langage de programmation) mais c'est très simple.

from BD_XML import Tabla

Il définit un objet appelé Tabla (Table), il peut être créé avec un nom permettant d'identifier un objet de connexion pré-créé d'une interface de base de données compatible pep-246.

Table = Tabla('Animals') 

Ensuite, vous devez ajouter des colonnes avec la méthode agregar_columna (Add_column), avec qui peut prendre différents arguments Word clés:

  • campo (champ): le nom du champ

  • tipo (type): le type de données stockées, par exemple 'varchar' et 'double' ou le nom de python si vous n'êtes pas intéressé par l'exportation à une base de données ce dernier.

  • defecto (par défaut): définit une valeur par défaut pour la colonne s'il n'y en a pas lorsque vous ajoutez une ligne

  • il y a d'autres 3 mais ne sont là que pour les bases de données et pas réellement fonctionnel

comme:

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')

Ensuite, vous ajoutez les lignes avec l'opérateur + = (ou + si vous voulez créer une copie avec une ligne supplémentaire)

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

Ensuite, vous pouvez générer du XML et l'écrire dans un fichier avec exportar_XML (Export_XML) et escribir_XML (Write_XML):

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

Et réimportez-le ensuite avec importar_XML (Import_XML) avec le nom du fichier et indiquez que vous utilisez un fichier et non un littéral de chaîne:

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

Avancée

Ce sont des façons dont vous pouvez utiliser un objet Tabla de manière SQL.

#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)

ces exemples supposent une colonne nommée 'id' mais peuvent être remplacés par width row.pos pour votre exemple.

if row.pos == 2:

Le fichier peut être téléchargé à partir de:

https://bitbucket.org/WolfangT/librerias

2
Wolfang Torres