Коль скоро мы собрались сохранять записи в базе данных, на самом первом этапе нам необходимо решить, как будут выглядеть эти записи. В языке Python имеется масса способов представления информации о людях. Зачастую для этих целей вполне достаточно бывает использовать объекты встроенных типов, такие как списки и словари, особенно если изначально не требуется предусматривать обработку сохраняемых данных.
Списки
Списки, например, позволяют сохранять информацию о людях упорядоченным способом. Запустите интерпретатор Python в интерактивном режиме и введите следующие две инструкции:
>>> bob = [‘Bob Smith’, 42, 30000, ‘software’]
> >> sue = [‘Sue Jones’, 45, 40000, ‘hardware’]
Мы только что создали две простые записи, представляющие информацию о Бобе (Bob) и Сью (Sue) (мои извинения, если вас действительно зовут Боб или Сью[I]). Каждая запись является списком с четырьмя элементами: имя, возраст, оклад и должность. Чтобы получить доступ к этим элементам, достаточно просто использовать операцию индексирования. Результат в примере ниже заключен в круглые скобки потому, что он является кортежем из двух результатов:
> >> bob[0], sue[2] # получить имя и оклад
(‘Bob Smith’, 40000)
В таком представлении записи легко обрабатывать — достаточно просто использовать операции над списками. Например, можно получить фамилию человека, разбив поле с именем по пробельному символу и отобрав последнюю часть, точно так же можно повысить оклад, изменив соответствующий список:
> >> bob[0].split()[-1] # получить фамилию Боба
‘Smith’
> >> sue[2] *= 1.25 # повысить оклад Сью на 25%
>>> sue
[‘Sue Jones’, 45, 50000.0, ‘hardware’]
Выражение, извлекающее фамилию в этом примере, обрабатывается интерпретатором слева направо: сначала извлекается строка с именем и фамилией Боба, затем строка разбивается по пробелам и преобразуется в список подстрок, а операция индексирования возвращает фамилию (разбейте это выражение на несколько операций, чтобы увидеть, как это происходит).
Первые замечания
Поскольку это первый пример программного кода в книге, необходимо сделать несколько практических замечаний:
• Этот программный код можно ввести в среде IDLE с графическим интерфейсом; после ввода команды python в командной строке (или той же команды с указанием полного пути к ней, если она не находится в системном списке путей поиска выполняемых файлов) и так далее.
• Символы >>> characters — это приглашение к вводу интерпретатора Python (эти символы не нужно вводить).
• Информационные строки, которые интерпретатор Python выводит при запуске, я опустил ради экономии места.
• Все примеры из этой книги я запускал под управлением Python 3.1; результаты работы примеров во всех версиях линейки 3.X должны быть одинаковыми (разумеется, исключая непредвиденные случаи внесения существенных изменений в Python).
• Большая часть примеров в этой книге, за исключением некоторых из них, демонстрирующих приемы системного программирования и интеграции с программным кодом на языке C, выполнялись в ОС Windows 7. Однако, благодаря переносимости Python, не имеет значения, в какой операционной системе будут опробоваться примеры, если иное не указано явно.
Если прежде вам не доводилось выполнять программный код на языке Python подобным способом, тогда обращайтесь за справочной информацией к вводным материалам, таким как книга «Изучаем Python». Далее в этой главе я сделаю несколько замечаний, касающихся запуска программного кода, хранящегося в файлах сценариев.
База данных в виде списка
К настоящему моменту мы всего создали всего лишь две переменных, но не базу данных. Чтобы объединить информацию о Бобе и Сью, мы могли бы просто включить ее в другой список:
> >> people = [bob, sue] # ссылки в списке списков
> >> for person in people:
print(person)
[‘Bob Smith’, 42, 30000, ‘software’]
[‘Sue Jones’, 45, 50000.0, ‘hardware’]
Теперь нашу базу данных представляет список people. Мы можем извлекать из него отдельные записи в соответствии с их позициями в списке и обрабатывать их в цикле:
> >> people[1][0]
‘Sue Jones’
> >> for person in people: print(person[0].split()[-1]) # вывести фамилию
person[2] *= 1.20 # увеличить оклад на 20%
Smith
Jones
> >> for person in people: print(person[2]) # проверить новый размер оклада
36000.0
60000.0
Теперь, когда у нас имеется список, мы можем организовать выборку значений полей из записей с помощью более мощных инструментов выполнения итераций, присутствующих в языке Python, таких как генераторы списков, функция map и выражения-генераторы:
> >> pays = [person[2] for person in people] # выбрать все оклады
> >> pays
[36000.0, 60000.0]
> >> pays = map((lambda x: x[2]), people) # то же самое (в версии 3.X
> >> list(pays) # функция map возвращает генератор)
[36000.0, 60000.0]
> >> sum(person[2] for person in people) # выражение—генератор,
> 6000.0 # sum — встроенная функция
Для добавления новых записей в базу данных вполне достаточно использовать обычные операции над списками, такие как append и extend:
> >> people.append([‘Tom’, 50, 0, None])
> >> len(people)
3
> >> people[-1][0]
‘Tom’
Списки неплохо подходят для реализации нашей базы данных, и их возможностей вполне достаточно для некоторых программ, но они страдают рядом существенных недостатков. Во-первых, информация о Бобе и Сью в настоящий момент хранится в виде объектов в оперативной памяти, и она будет утеряна сразу после завершения работы интерпретатора Python. Во-вторых, всякий раз, когда потребуется извлечь фамилию человека или повысить ему оклад, нам придется повторно вводить программный код, который мы только что видели. Это может вызвать определенные проблемы, если когда-нибудь поменяется алгоритм выполнения этих операций, — нам может потребоваться изменить наш программный код во многих местах. Мы вернемся к этим проблемам через несколько минут.
Обращение к полям по именам
Самый, пожалуй, существенный недостаток при использовании списков заключается в необходимости запоминать позиции полей: как иначе можно утверждать, что программный код, обращающийся к элементу записи с таинственным индексом 2, извлекает размер оклада? С точки зрения очевидности программного кода было бы лучше, если бы можно было присвоить каждому полю осмысленное имя.
Мы могли бы связать имена с позициями полей в записи, используя встроенную функцию range, которая генерирует набор последовательных целых чисел при использовании в контексте итераций (таких как операция присваивания последовательности ниже):
>>> NAME, AGE, PAY = range(3) # 0, 1 и 2
>>> bob = [‘Bob Smith’, 42, 10000]
>>> bob[NAME]
‘Bob Smith’
>>> PAY, bob[PAY]
(2, 10000)
Это решает проблему читаемости программного кода: три имени переменных, состоящих из заглавных символов, по сути, превратились в имена полей. Однако такое решение делает программный код зависящим от инструкции присваивания позиций именам полей, — мы должны помнить о необходимости обновлять ее при любом изменении структуры записи. Поскольку имена полей и записи никак не связаны между собой, они могут перестать соответствовать друг другу, и тогда возникнет необходимость вмешательства в программный код.
Кроме того, так как имена полей являются независимыми переменными, между записью в виде списка и именами полей отсутствует обратная связь. Имея одну только запись в виде списка, например, нельзя реализовать форматированный вывод значений полей с их именами. В случае с предыдущей записью без дополнительных ухищрений невозможно получить имя AGE из значения 42: вызов bob.index(42) вернет 1, значение переменной AGE, но не само имя AGE.
Можно было бы попробовать представлять записи в виде списков кортежей, где кортежи хранят не только значения полей, но их имена. Но еще лучше было бы использовать списки списков, что позволило бы изменять поля (кортежи относятся к категории неизменяемых объектов). Ниже демонстрируется воплощение этой идеи на примере простых записей:
> >> bob = [[‘name’, ‘Bob Smith’], [‘age’, 42], [‘pay’, 10000]]
> >> sue = [[‘name’, ‘Sue Jones’], [‘age’, 45], [‘pay’, 20000]]
> >> people = [bob, sue]
Однако на самом деле этот прием не решает проблему, потому что для извлечения полей все равно необходимо использовать числовые индексы:
> >> for person in people: print(person[0][1], person[2][1]) # имя, оклад
Bob Smith 10000
Sue Jones 20000
> >> [person[0][1] for person in people] # выборка имен
[‘Bob Smith’, ‘Sue Jones’]
> >> for person in people:
print(person[0][1].split()[-1]) # получить фамилию person[2][1] *= 1.10 # повысить оклад на 10%
Smith
Jones
> >> for person in people: print(person[2])
[‘pay’, 11000.0]
[‘pay’, 22000.0]
Все, чего мы добились, — добавили еще один уровень индексирования. Для достижения желаемого мы могли бы просматривать имена полей в цикле, отыскивая необходимые (в следующем цикле используется операция присваивания кортежа для распаковывания пар имя/значение):
> >> for person in people:
for (name, value) in person: if name == ‘name’: print(value) # поиск требуемого поля
Bob Smith
Sue Jones
Еще лучше было бы реализовать функцию, выполняющую всю работу за нас:
>>> def field(record, label):
for (fname, fvalue) in record:
if fname == label: return fvalue
>>> field(bob, ‘name’) ‘Bob Smith’
>>> field(sue, ‘pay’)
22000.0
42
45
Если двигаться дальше по этому пути, в конечном итоге можно получить набор функций, осуществляющих интерфейс к записям, отображающих имена полей в их значения. Однако если прежде вам приходилось программировать на языке Python, вы наверняка знаете, что существует более простой способ реализации такого рода ассоциаций, и вы уже наверняка догадались, в какую сторону мы направимся в следующем разделе.
Словари
Реализация записей на основе списков, как было показано в предыдущем разделе, вполне работоспособна, хотя и за счет некоторой потери производительности из-за необходимости выполнять поиск полей по именам (если вас волнуют потери, измеряемые миллисекундами). Однако если вы уже имеете некоторое знакомство с языком Python, вы должны знать, что существуют более эффективные и более удобные способы связывания имен полей с их значениями. Встроенные объекты словарей выглядят естественно:
> >> bob = {‘name’: ‘Bob Smith’, ‘age’: 42, ‘pay’: 30000, ‘job’: ‘dev’}
> >> sue = {‘name’: ‘Sue Jones’, ‘age’: 45, ‘pay’: 40000, ‘job’: ‘hdw’}
Теперь bob и sue — это объекты, автоматически отображающие имена полей в их значения, и их использование делает программный код более простым и понятным. Нам не требуется запоминать, что означают числовые индексы, и мы даем интерпретатору возможность использовать его высокоэффективный алгоритм индексации словарей, чтобы отыскивать значения полей, ассоциированные с их именами:
> >> bob[‘name’], sue[‘pay’] # в отличие от bob[0], sue[2]
(‘Bob Smith’, 40000)
>>> bob[‘name’].split()[-1]
‘Smith’
>>> sue[‘pay’] *= 1.10
>>> sue[‘pay’]
44000.0
Поскольку теперь при обращении к полям используются символические имена, программный код выглядит более осмысленным для всех, кто будет читать его (включая и вас).
Другие способы создания словарей
Словари являются настолько удобными объектами при программировании на языке Python, что было предусмотрено еще несколько способов их создания, отличающихся от традиционного синтаксиса литералов, продемонстрированного выше, — например, вызовом конструктора с именованными аргументами, при этом все ключи будут строками:
>>> bob = dict(name=’Bob Smith’, age=42, pay=30000, job=’dev’)
>>> sue = dict(name=’Sue Jones’, age=45, pay=40000, job=’hdw’)
>>> bob
{‘pay’: 30000, ‘job’: ‘dev’, ‘age’: 42, ‘name’: ‘Bob Smith’}
>>> sue
{‘pay’: 40000, ‘job’: ‘hdw’, ‘age’: 45, ‘name’: ‘Sue Jones’}
заполнением словаря поле за полем (напомню, что для ключей словаря не предусматривается какой-то определенный порядок следования):
>>> sue = {}
>>> sue[‘name’] = ‘Sue Jones’
>>> sue[‘age’] = 45
>>> sue[‘pay’] = 40000
>>> sue[‘job’] = ‘hdw’
>>> sue
{‘job’: ‘hdw’, ‘pay’: 40000, ‘age’: 45, ‘name’: ‘Sue Jones’}
объединением двух списков, содержащих имена и значения:
>>> names = [‘name’, ‘age’, ‘pay’, ‘job’]
>>> values = [‘Sue Jones’, 45, 40000, ‘hdw’]
>>> list(zip(names, values))
[(‘name’, ‘Sue Jones’), (‘age’, 45), (‘pay’, 40000), (‘job’, ‘hdw’)]
>>> sue = dict(zip(names, values))
>>> sue
{‘job’: ‘hdw’, ‘pay’: 40000, ‘age’: 45, ‘name’: ‘Sue Jones’}
Словари можно даже создавать из последовательностей ключей и необязательного начального значения для всех ключей (этот способ удобно использовать для инициализации пустых словарей):
>>> fields = (‘name’, ‘age’, ‘job’, ‘pay’)
>>> record = dict.fromkeys(fields, ‘?’)
>>> record
{‘job’: ‘?’, ‘pay’: ‘?’, ‘age’: ‘?’, ‘name’: ‘?’}
Списки словарей
Независимо от способа создания словарей, нам все еще необходимо собрать словари-записи в базу данных. Здесь также можно использовать список, при условии, что нам не требуется обеспечить доступ по ключу на верхнем уровне:
>>> bob
{‘pay’: 30000, ‘job’: ‘dev’, ‘age’: 42, ‘name’: ‘Bob Smith’}
>>> sue
{‘job’: ‘hdw’, ‘pay’: 40000, ‘age’: 45, ‘name’: ‘Sue Jones’}
>>> people = [bob, sue] # ссылки в списке
>>> for person in people: print(person[‘name’], person[‘pay’], sep=’, ‘) # все имена, оклады
Bob Smith, 30000
Sue Jones, 40000
>>> for person in people: if person[‘name’] == ‘Sue Jones’: # оклад Сью
print(person[‘pay’])
40000
Здесь точно так же используются инструменты итераций, но вместо таинственных числовых индексов используются ключи (в терминах баз данных генератор списков и функция map в следующем примере возвращают проекцию базы данных по полю «name»):
>>> names = [person[‘name’] for person in people] # выбирает имена
>>> names
[‘Bob Smith’, ‘Sue Jones’]
>>> list(map((lambda x: x[‘name’]), people)) # то же самое
[‘Bob Smith’, ‘Sue Jones’]
>>> sum(person[‘pay’] for person in people) # сумма всех окладов 70000
Интересно, что такие инструменты, как генераторы списков и выражения-генераторы, способны по своему удобству приблизиться к запросам в языке SQL, с тем отличием, что они манипулируют объектами в памяти:
>>> [rec[‘name’] for rec in people if rec[‘age’] >= 45] # SQL—подобный
[‘Sue Jones’] # запрос
>>> [(rec[‘age’] ** 2 if rec[‘age’] >= 45 else rec[‘age’]) for rec in people] [42, 2025]
> >> G = (rec[‘name’] for rec in people if rec[‘age’] >= 45)
> >> next(G)
‘Sue Jones’
> >> G = ((rec[‘age’] ** 2 if rec[‘age’] >= 45 else rec[‘age’]) for rec in people)
> >> G.__next__()
42
А так как словари являются обычными объектами, к этим записям можно также обращаться с использованием привычного синтаксиса:
> >> for person in people:
print(person[‘name’].split()[-1]) # фамилия
person[‘pay’] *= 1.10 # повышение на 10%
Smith
Jones
> >> for person in people: print(person[‘pay’])
33000.0
44000.0
Вложенные структуры
В предыдущих примерах мы могли бы при желании избежать необходимости писать дополнительный программный код, извлекающий фамилии, еще больше структурировав наши записи. Поскольку в языке все объекты составных типов данных могут вкладываться друг в друга сколь угодно глубоко, мы легко можем конструировать чрезвычайно сложные структуры данных, используя простой синтаксис объектов, а Python сам позаботится о создании компонентов, связывании структур в памяти и освобождении памяти позднее. Это одно из значительных преимуществ таких языков сценариев, как Python.
В следующем примере демонстрируется более структурированная запись, содержащая вложенный словарь, список и кортеж внутри другого словаря:
> >> bob2 = {‘name’: {‘first’: ‘Bob’, ‘last’: ‘Smith’},
‘age’: 42,
‘job’: [‘software’, ‘writing’],
‘pay’: (40000, 50000)}
Эта запись содержит вложенные структуры, поэтому для доступа к более низкому уровню мы просто будем использовать двойные индексы:
> >> bob2[‘name’] # полное имя Боба
{‘last’: ‘Smith’, ‘first’: ‘Bob’}
> >> bob2[‘name’][‘last’]
> >> bob2[‘pay’][1] # верхний предел оклада Боба
50000
Поле name здесь — это еще один словарь, поэтому вместо того чтобы разбивать строку для извлечения фамилии, мы просто используем операцию индексирования. Кроме того, сотрудники могут занимать несколько должностей, а также иметь верхний и нижний предел оклада. Фактически в подобных ситуациях Python превращается в своеобразный язык запросов — мы можем извлекать и изменять вложенные значения с применением обычных операций над объектами:
> >> for job in bob2[‘job’]: print(job) # все должности, занимаемые Бобом software
writing
> > bob2[‘job’][-1] # последняя должность Боба
‘writing’
>>> bob2[‘job’].append(‘janitor’) # Боб получает новую должность
>>> bob2
{‘job’: [‘software’, ‘writing’, ‘janitor’], ‘pay’: (40000, 50000), ‘age’: 42, ‘name’: {‘last’: ‘Smith’, ‘first’: ‘Bob’}}
В расширении вложенного списка с помощью метода append нет ничего необычного, потому что в действительности он является независимым объектом. Такие вложенные конструкции могут пригодиться в более сложных приложениях. Однако, чтобы не усложнять примеры, мы сохраним прежнюю, плоскую структуру записей.
Словари словарей
И еще один поворот в реализации нашей базы данных с информацией о людях: мы можем расширить область применения словарей, задействовав еще один словарь для представления самой базы данных. То есть мы можем создать словарь словарей — внешний словарь будет играть роль базы данных, а вложенные словари — роль записей. В отличие от простого списка записей, база данных, представленная в виде словаря, позволит нам сохранять и извлекать записи с помощью символических ключей:
>>> bob = dict(name=’Bob Smith’, age=42, pay=30000, job=’dev’)
>>> sue = dict(name=’Sue Jones’, age=45, pay=40000, job=’hdw’)
>>> bob
{‘pay’: 30000, ‘job’: ‘dev’, ‘age’: 42, ‘name’: ‘Bob Smith’}
> >> db = {}
> >> db[‘bob’] = bob # ссылки на словари в словаре
> >> db[‘sue’] = sue
>>>
> >> db[‘bob’][‘name’] # извлечь имя Боба
> >> db[‘sue’][‘pay’] = 50000 # изменить оклад Сью
> >> db[‘sue’][‘pay’] # извлечь оклад Сью
50000
Обратите внимание, что такая организация позволяет нам обращаться к записям непосредственно, без необходимости выполнять поиск в цикле — мы получаем непосредственный доступ к записи с информацией о Бобе за счет использования ключа bob. Это действительно словарь словарей, хотя это и не заметно, если не вывести всю базу данных сразу (для подобных целей удобно использовать модуль pprint форматированного вывода):
>>> db
{‘bob’: {‘pay’: 30000, ‘job’: ‘dev’, ‘age’: 42, ‘name’: ‘Bob Smith’}, ‘sue’: {‘pay’: 50000, ‘job’: ‘hdw’, ‘age’: 45, ‘name’: ‘Sue Jones’}}
> >> import pprint
> >> pprint.pprint(db)
{‘bob’: {‘age’: 42, ‘job’: ‘dev’, ‘name’: ‘Bob Smith’, ‘pay’: 30000},
‘sue’: {‘age’: 45, ‘job’: ‘hdw’, ‘name’: ‘Sue Jones’, ‘pay’: 50000}}
Если же возникнет необходимость последовательно обойти все записи в базе данных, можно воспользоваться итераторами словарей. В последних версиях Python реализован итератор словаря, который на каждой итерации в цикле for воспроизводит по одному ключу (для совместимости с более ранними версиями в циклах for можно также вместо простого имени db использовать явный вызов метода db.keys, но, так как в Python 3 метод keys возвращает генератор, конечный результат будет тот же самый):
> >> for key in db: print(key, ‘=>’, db[key][‘name’])
bob => Bob Smith sue => Sue Jones
> >> for key in db: print(key, ‘=>’, db[key][‘pay’])
bob => 30000
sue => 50000
В процессе обхода доступ к отдельным записям можно получать с использованием операции индексирования по ключу:
>>> for key in db:
print(db[key][‘name’].split()[-1]) db[key][‘pay’] *= 1.10
Smith
или напрямую, организовав обход значений словаря:
>>> for record in db.values(): print(record[‘pay’])
33000.0
55000.0
>>> x = [db[key][‘name’] for key in db]
>>> x
[‘Bob Smith’, ‘Sue Jones’]
>>> x = [rec[‘name’] for rec in db.values()]
>>> x
[‘Bob Smith’, ‘Sue Jones’]
А чтобы добавить новую запись, достаточно просто выполнить операцию присваивания по новому ключу. В конце концов — это всего лишь словарь:
>>> db[‘tom’] = dict(name=’Tom’, age=50, job=None, pay=0)
>>>
>>> db[‘tom’]
{‘pay’: 0, ‘job’: None, ‘age’: 50, ‘name’: ‘Tom’}
>>> db[‘tom’][‘name’]
‘Tom’
>>> list(db.keys())
[‘bob’, ‘sue’, ‘tom’]
>>> len(db)
3
# >> [rec[‘age’] for rec in db.values()]
[42, 45, 50]
# >> [rec[‘name’] for rec in db.values() if rec[‘age’] >= 45] # SQL—подобный [‘Sue Jones’, ‘Tom’] # запрос
Наша база данных по-прежнему является объектом, хранящимся в оперативной памяти. Но, как оказывается, такой формат словаря словарей в точности соответствует формату, который используется системой сохранения объектов в файлах, — модулем shelve (с точки зрения грамматики английского языка этот модуль должен был бы называться shelf, но в Python термин shelve, обозначающий сохранение объектов, одновременно служит и названием соответствующего ему модуля). О том, как это делается, вы узнаете в следующем разделе.
Использованная литература:
Марк Лутц — Программирование на Python, 4-е издание, I том, 2011