Структура даних для збереження табличних даних у пам'яті?


80

Мій сценарій такий: у мене є таблиця даних (кілька полів, менше ста рядків), яку я широко використовую в своїй програмі. Мені також потрібні ці дані, щоб бути постійними, тому я зберігаю їх як CSV і завантажую під час запуску. Я вирішив не використовувати базу даних, оскільки кожен варіант (навіть SQLite) є надмірним для моєї скромної вимоги (також - я хотів би мати можливість редагувати значення в автономному режимі простим способом, і нічого не простіше, ніж блокнот).

Припустимо, мої дані виглядають наступним чином (у файлі вони розділені комами без заголовків, це лише ілюстрація):

 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

Примітки:

  1. Рядок може бути "реальним" значенням, записаним у файл, або просто автоматично згенерованим значенням, яке представляє номер рядка. У будь-якому випадку це існує в пам'яті.
  2. Імена унікальні.

Що я роблю з даними:

  1. Шукати рядок на основі ідентифікатора (ітерація) або імені (прямий доступ).
  2. Відображати таблицю в різних замовленнях на основі кількох полів: мені потрібно сортувати, наприклад, за пріоритетом, а потім за роком, або за роком, а потім за пріоритетом тощо.
  3. Мені потрібно підрахувати екземпляри на основі наборів параметрів, наприклад, скільки рядків мають рік між 1997 і 2002 роками, або скільки рядків у 1998 році і пріоритет> 2 тощо.

Я знаю це "крики" щодо SQL ...

Я намагаюся зрозуміти, який найкращий вибір для структури даних. Ось кілька варіантів, які я бачу:

Список списків рядків:

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

Список списків стовпців (очевидно, буде API для add_row тощо):

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

Словник списків стовпців (для заміщення рядкових ключів можна створити константи):

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] 

Словник із клавішами, що є кортежами (Рядок, Поле):

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

І я впевнений, що є й інші способи ... Однак кожен із способів має недоліки, коли справа стосується моїх вимог (складне впорядкування та підрахунок).

Який рекомендований підхід?

РЕДАГУВАТИ:

Щоб пояснити, продуктивність для мене не є основною проблемою. Оскільки таблиця настільки мала, я вважаю, що майже кожна операція буде знаходитися в діапазоні мілісекунд, що не турбує мою програму.


3
Не потрібно читати відповіді. Ваше питання дало відповідь на моє запитання! Велике спасибі за таке чудове запитання :)
Gajendra Bagali

Відповіді:


80

Наявність "таблиці" в пам'яті, яка потребує пошуку, сортування та довільного агрегування, дійсно вимагає SQL. Ви сказали, що пробували SQLite, але чи розумієте ви, що SQLite може використовувати базу даних, що зберігається лише в пам'яті?

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

Тоді ви можете створювати / скидати / запитувати / оновлювати таблиці в пам’яті з усіма функціональними можливостями SQLite і не залишаючи файлів, коли закінчите. А станом на Python 2.5, sqlite3він знаходиться у стандартній бібліотеці, тож це насправді не є "надмірним" IMO.

Ось зразок того, як можна створити та заповнити базу даних:

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

Якщо ви дійсно віддаєте перевагу не використовувати SQL, вам слід скористатися списком словників:

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

Потім тестування дає:

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

Особисто мені більше подобається версія SQLite, оскільки вона краще зберігає ваші типи (без додаткового коду перетворення в Python) і легко зростає відповідно до майбутніх вимог. Але знову ж таки, мені цілком приємно користуватися SQL, тому YMMV.


2
Який рекомендований підхід для заповнення бази даних у цьому прикладі?
Рої Адлер,

4
Я відредагував відповідь, включивши зразок коду для заповнення бази даних із файлу CSV.
Рік Коупленд,

1
-1: overkill, список словників, мабуть, корисніший для цього додатка.
S.Lott

11
Списки словників роблять запити, сортування та агрегування набагато детальнішими, навіть із розумінням списку та сортуванням (..., key =). Отже, я б сказав, що sqlite in-memory тут ідеально підходить. Кожному своє ...
Рік Коупленд,

2
До відповіді я також додав опцію списку словників.
Рік Коупленд,

35

Дуже старе питання, яке я знаю, але ...

Панда DataFrame, здається, тут ідеальний варіант.

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

З розмитості

Двовимірна змінна розміру, потенційно неоднорідна таблична структура даних із позначеними осями (рядки та стовпці). Арифметичні операції вирівнюються як на мітках рядків, так і на стовпцях. Можна розглядати як контейнер, схожий на дикт, для об’єктів серії. Первинна структура даних про панд

http://pandas.pydata.org/


20

Я особисто скористався списком списків рядків. Оскільки дані для кожного рядка завжди мають однаковий порядок, ви можете легко сортувати за будь-яким із стовпців, просто отримавши доступ до цього елемента в кожному зі списків. Ви також можете легко підрахувати на основі певного стовпця в кожному списку, а також здійснити пошук. В основному він наближається до 2-х масивів.

Дійсно, єдиним недоліком тут є те, що ви повинні знати, в якому порядку знаходяться дані, і якщо ви зміните це впорядкування, вам доведеться змінити свої процедури пошуку / сортування на відповідність.

Ще одне, що ви можете зробити, - це скласти список словників.

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

Це дозволить уникнути необхідності знати порядок параметрів, тому ви можете переглянути кожне поле "рік" у списку.


7
+1: Списки словників ДІЙСНО працюють і сумісні з читанням та записом файлів JSON або CSV.
S.Lott

7

Майте клас Таблиця, рядки якого є списком об'єктів dict або кращих рядків

У таблиці не додайте рядки безпосередньо, але використовуйте метод, який оновлює декілька пошукових карт, наприклад, для імені, якщо ви не додаєте рядки в порядку або id не є послідовними, ви можете також мати idMap, наприклад

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

По-перше, зважаючи на те, що у вас складний сценарій пошуку даних, ви впевнені, що навіть SQLite є надмірним?

У підсумку ви отримаєте спеціальну, неофіційно вказану помилку, повільну реалізацію половини SQLite, перефразовуючи Десяте правило Грінспуна .

Тим не менш, ви дуже праві, стверджуючи, що вибір однієї структури даних вплине на один або кілька пошуків, сортування або підрахунку, тому, якщо продуктивність має першочергове значення, а ваші дані постійні, ви можете подумати про наявність декількох структур для різних цілей.

Перш за все, виміряйте, які операції будуть більш поширеними, і вирішіть, яка структура в кінцевому підсумку буде коштувати дешевше.


3

Я особисто писав бібліотеку майже зовсім недавно, вона зовсім недавно називається BD_XML

оскільки його найбільш фундаментальна причина існування полягає у тому, щоб служити способом надсилання даних між файлами XML та базами даних SQL.

Він написаний іспанською (якщо це важливо мовою програмування), але дуже просто.

from BD_XML import Tabla

Він визначає об'єкт під назвою Tabla (Таблиця), його можна створити з іменем для ідентифікації попередньо створеного об'єкта підключення інтерфейсу бази даних, сумісного з pep-246.

Table = Tabla('Animals') 

Потім вам потрібно додати стовпці за допомогою agregar_columnaметоду (add_column), причому може приймати різні аргументи ключового слова:

  • campo (поле): назва поля

  • tipo (тип): тип даних, що зберігаються, може бути такими, як 'varchar' та 'double' або ім'я об'єктів python, якщо ви не зацікавлені в експорті до останньої бази даних.

  • defecto (за замовчуванням): встановіть значення за замовчуванням для стовпця, якщо його немає під час додавання рядка

  • є ще 3, але вони є лише для відтінків бази даних, а насправді не функціональні

подібно до:

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

Потім ви додаєте рядки за допомогою оператора + = (або +, якщо ви хочете створити копію з додатковим рядком)

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

Тоді ви можете сформувати XML і записати його у файл за допомогою exportar_XML(export_XML) та escribir_XML(write_XML):

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

А потім імпортуйте його назад за допомогою importar_XML(import_XML) із назвою файлу та зазначенням того, що ви використовуєте файл, а не літеральний рядок:

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

Розширений

Це способи використання об’єкта Tabla у спосіб 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)

в цих прикладах передбачається стовпець з ідентифікатором, але його можна замінити шириною row.pos для вашого прикладу.

if row.pos == 2:

Файл можна завантажити з:

https://bitbucket.org/WolfangT/librerias

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.