Struktura danych do przechowywania danych tabelarycznych w pamięci?

Mój scenariusz wygląda następująco: mam tabelę danych (garść pól, mniej niż 100 wierszy), które intensywnie używam w moim programie. Potrzebuję również tych danych, aby były trwałe, więc zapisuję je jako plik CSV i ładuję przy starcie. Zdecydowałem się nie korzystać z bazy danych, ponieważ każda opcja (nawet SQLite) jest przesadą dla moich skromnych wymagań (również - chciałbym mieć możliwość edycji wartości offline w prosty sposób, a nic nie jest prostsze niż notatnik).

Załóżmy, że moje dane wyglądają następująco (w plik oddzielony przecinkami bez tytułów, to tylko ilustracja):

 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

Uwagi:

  1. wiersz może być "rzeczywistą" wartością zapisaną do pliku lub po prostu automatycznie wygenerowaną wartością reprezentującą numer wiersza. Tak czy siak istnieje w pamięci.
  2. nazwy są unikalne.

Rzeczy, które robię z danymi:

  1. wyszukuje wiersz na podstawie ID (iteracji) lub name (direct access).
  2. wyświetlanie tabeli w różnych kolejnościach na podstawie wielu pole: muszę to posortować np. według priorytetu, a następnie roku, lub roku, a następnie Priorytetu, itp.
  3. muszę policzyć instancje na podstawie zestawów parametrów, np. ile wierszy ma swój rok między 1997 a 2002, lub ile wierszy jest w 1998 i Priorytet > 2, itd.

Wiem, że to "płacze" dla SQL...

Staram się ustalić, jaki jest najlepszy wybór dla struktury danych. Oto kilka opcji, które widzę:

Lista wierszy:

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

Lista kolumn listy (oczywiście będzie API dla add_row itp.):

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

Słownik list kolumn (stałe mogą być tworzone w celu zastąpienia kluczy łańcuchowych):

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] 

Słownik z klawiszami będącymi krotkami (wiersz, pole):

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
...
I jestem pewien, że są inne sposoby... Jednak każdy sposób ma wady, jeśli chodzi o moje wymagania (złożone zamawianie i liczenie).

Jakie jest zalecane podejście?

EDIT:

Dla wyjaśnienia, wydajność nie jest głównym problem dla mnie. Ponieważ tabela jest tak mała, wierzę, że prawie każda operacja będzie w zakresie milisekund, co nie jest problemem dla mojej aplikacji.

Author: Brian Tompsett - 汤莱恩, 2009-06-24

6 answers

Posiadanie" tabeli " w pamięci, która wymaga wyszukiwania, sortowania i arbitralnej agregacji, naprawdę wymaga użycia SQL. Mówiłeś, że próbowałeś SQLite, ale czy zdajesz sobie sprawę, że SQLite może używać bazy danych tylko w pamięci?

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

Następnie możesz utworzyć tabele / drop/query/update w pamięci z całą funkcjonalnością SQLite i bez plików po zakończeniu. A od Pythona 2.5, sqlite3 jest w bibliotece standardowej, więc to nie jest tak naprawdę" przesada " IMO.

Oto próbka Jak jeden może tworzyć i wypełniać bazę danych:

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

Jeśli naprawdę wolisz nie używać SQL, powinieneś prawdopodobnie użyć listy słowników:

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

Testowanie daje:

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

Osobiście podoba mi się wersja SQLite lepiej, ponieważ zachowuje swoje typy lepiej (bez dodatkowego kodu konwersji w Pythonie) i łatwo rośnie, aby dostosować się do przyszłych wymagań. Ale z drugiej strony, jestem całkiem zadowolony z SQL, więc YMMV.

 71
Author: Rick Copeland,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-06-24 14:56:48

Bardzo stare pytanie wiem ale...

Ramka danych pandy wydaje się być idealną opcją tutaj.

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

Z blurb

Rozmiar dwuwymiarowy-zmienne, potencjalnie niejednorodne dane tabelaryczne struktura z oznaczonymi osiami(wiersze i kolumny). Operacje arytmetyczne wyrównaj etykiety wierszy i kolumn. Można uznać za dict-like pojemnik na serie obiektów. Podstawowa struktura danych pand

Http://pandas.pydata.org/

 28
Author: user5061,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-05-08 23:18:39

Ja osobiście użyłbym listy wierszy. Ponieważ dane dla każdego wiersza są zawsze w tej samej kolejności, można łatwo sortować według dowolnej kolumny, po prostu uzyskując dostęp do tego elementu w każdej z list. Możesz również łatwo liczyć na podstawie określonej kolumny na każdej liście, a także wyszukiwać. Jest w zasadzie tak blisko, jak do tablicy 2-D.

Naprawdę jedyną wadą jest to, że musisz wiedzieć, w jakiej kolejności są dane, a jeśli zmienisz tę kolejność, będziesz musisz zmienić procedury wyszukiwania / sortowania, aby pasowały.

Kolejną rzeczą, którą możesz zrobić, to mieć listę słowników.

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

Pozwoli to uniknąć konieczności znajomości kolejności parametrów, dzięki czemu można przejrzeć każde pole " rok " na liście.

 13
Author: AlbertoPL,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-06-24 13:03:31

Mają klasę tabeli, której wiersze są listą obiektów dict lub better row

W tabeli nie dodajemy bezpośrednio wierszy, ale mamy metodę, która aktualizuje kilka map np. dla nazwy jeśli nie dodajesz wierszy w kolejności lub id nie są kolejne, możesz również mieć idMap np.

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
Author: Anurag Uniyal,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-06-24 13:12:43

Po pierwsze, biorąc pod uwagę, że masz złożony scenariusz pobierania danych, jesteś pewien, że nawet SQLite jest przesadą?

Będziesz miał Ad hoc, nieformalnie określoną, pozbawioną błędów, powolną implementację połowy SQLite, parafrazując dziesiątą regułę Greenspuna .

To powiedziawszy, masz rację mówiąc, że wybór pojedynczej struktury danych wpłynie na jedno lub więcej przeszukiwania, sortowania lub liczenia, więc jeśli wydajność jest najważniejsza, a dane są stałe, możesz rozważyć posiadające więcej niż jedną strukturę do różnych celów.

Przede wszystkim zmierz, które operacje będą bardziej powszechne i zdecyduj, która struktura będzie kosztować mniej.

 5
Author: Vinko Vrsalovic,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-06-24 13:00:06

Osobiscie napisalem lib dla calkowicie tego, ze calkiem niedawno nazywa sie BD_XML

Jako jego najbardziej fundamentalny powód istnienia służy jako sposób przesyłania danych tam iz powrotem między plikami XML i bazami danych SQL.

Jest napisany po hiszpańsku (jeśli ma to znaczenie w języku programowania), ale jest bardzo prosty.

from BD_XML import Tabla

Definiuje obiekt o nazwie Tabla( Table), można go wytworzyć z nazwą identyfikującą wstępnie utworzony obiekt połączenia Pep-246 kompatybilny interfejs bazy danych.

Table = Tabla('Animals') 

Następnie należy dodać kolumny za pomocą metody agregar_columna (add_column), która może przyjmować różne argumenty słowa kluczowego:

  • campo (pole): Nazwa pola

  • tipo (type): Typ przechowywanych danych, może to być "varchar" i "double" lub nazwa obiektów Pythona, jeśli nie jesteś zainteresowany eksportem do bazy danych.

  • defecto (default): Ustaw domyślną wartość dla kolumny jeśli nie ma żadnego po dodaniu wiersza

  • Istnieją inne 3, ale są tam tylko dla tings bazy danych i nie faktycznie funkcjonalne

Jak:

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

Następnie dodajesz wiersze za pomocą operatora + = (lub+, jeśli chcesz utworzyć kopię z dodatkowym wierszem)

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

Następnie możesz wygenerować XML i zapisać go do pliku z exportar_XML (export_XML) i escribir_XML (write_xml):

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

A następnie zaimportować go z powrotem za pomocą importar_XML (import_XML) z nazwą pliku i wskazanie, że używasz pliku, a nie literalnego łańcucha znaków:

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

Zaawansowane

W ten sposób można używać obiektu Tabla w sposób 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)

Te przykłady zakładają kolumnę o nazwie "id" ale można zastąpić wiersz szerokości.pos dla Twojego przykładu.

if row.pos == 2:

Plik można pobrać ze strony:

Https://bitbucket.org/WolfangT/librerias

 2
Author: Wolfang Torres,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-04-04 17:24:04