Шаг 1: представление записей

shag 1 predstavlenie zapisej «Программирование на Python»: краткий очерк

Коль скоро мы собрались сохранять записи в базе данных, на самом первом этапе нам необходимо решить, как будут выглядеть эти записи. В языке 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

подпись: >>> for rec in people:
print(field(rec, ‘age’))
подпись: # вывести возраст всех людей
# в базе данных

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

‘Smith’

>  >> 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’] # извлечь имя Боба

‘Bob Smith’

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

Jones

или напрямую, организовав обход значений словаря:

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

Оцените статью
Секреты программирования
Добавить комментарий