Структура данных для хранения табличных данных в памяти? - PullRequest
69 голосов
/ 24 июня 2009

Мой сценарий таков: у меня есть таблица данных (несколько полей, меньше ста строк), которую я широко использую в своей программе. Мне также нужно, чтобы эти данные были постоянными, поэтому я сохраняю их как файл 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] 

Словарь с ключами, являющимися кортежами (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
...

И я уверен, что есть и другие способы ... Однако у каждого способа есть свои недостатки, когда речь идет о моих требованиях (сложный порядок и подсчет).

Какой рекомендуемый подход?

EDIT:

Для пояснения, производительность не является для меня серьезной проблемой. Поскольку таблица настолько мала, я считаю, что почти каждая операция будет выполняться в диапазоне миллисекунд, что не имеет значения для моего приложения.

Ответы [ 6 ]

75 голосов
/ 24 июня 2009

Наличие «таблицы» в памяти, которая требует поиска, сортировки и произвольного агрегирования, действительно требует 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.

30 голосов
/ 09 мая 2014

Очень старый вопрос, который я знаю, но ...

Кажется, идеальным вариантом здесь является DataFrame для панд.*

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

http://pandas.pydata.org/

16 голосов
/ 24 июня 2009

Я бы лично использовал список списков строк. Поскольку данные для каждой строки всегда находятся в одном и том же порядке, вы можете легко сортировать по любому из столбцов, просто обращаясь к этому элементу в каждом из списков. Вы также можете легко рассчитывать на основе определенного столбца в каждом списке, а также производить поиск. Это в основном так же близко, как и к двумерному массиву.

Действительно, единственным недостатком здесь является то, что вы должны знать, в каком порядке находятся данные, и если вы измените этот порядок, вам придется изменить процедуры поиска / сортировки, чтобы они соответствовали.

Еще одна вещь, которую вы можете сделать, это иметь список словарей.

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

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

6 голосов
/ 24 июня 2009

Иметь класс Table, строки которого являются списком объектов типа 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'})
5 голосов
/ 24 июня 2009

Во-первых, учитывая, что у вас сложный сценарий извлечения данных, вы уверены, что даже SQLite является избыточным?

В конечном итоге вы получите специальную, неформально заданную, с ошибками и медленную реализацию половины SQLite, перефразируя Десятое правило Гринспуна .

При этом вы совершенно правы, говоря, что выбор одной структуры данных повлияет на один или несколько этапов поиска, сортировки или подсчета, поэтому, если производительность имеет первостепенное значение, а ваши данные постоянны, вы можете рассмотреть возможность использования более чем одной структуры разные цели.

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

2 голосов
/ 09 июня 2014

Лично я написал библиотеку для довольно недавнего времени, она называется 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

Advanced

Это способы, которыми вы можете использовать объект 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)

этот пример предполагает столбец с именем 'id' но можно заменить шириной row.pos для вашего примера.

if row.pos == 2:

Файл можно скачать с:

https://bitbucket.org/WolfangT/librerias

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...