Программирование и научные вычисления на языке Python/§12

Мы уже знаем как хранить информацию в виде самых различных объектов: чисел, строк, списков, кортежей, массивов и файлов. Но оказывается, что в Python есть еще один прекрасный тип объекта — словарь, который оказывается очень гибким способом для хранения не просто каких-то значений, а уже каким-то образом связанных данных различных типов.

Список, как мы знаем, индексируется с помощью целых чисел, начиная с нуля. Однако, можно заметить, что эта нумерация зачастую может быть несколько побочной, и мы бы хотели «индексировать» какую-то информацию словами. Грубо говоря, списки, индексируемые словами, и являются словарями.

## Делаем словари

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



```
temps = [13, 15.4, 17.5]
```


Но для того, чтобы запомнить принадлежность к городам, нам нужно соотнести индексы списка и города. Как здесь действовать, мы и думать не будем, поскольку есть словари:



```
temps = {'Oslo': 13, 'London': 15.4, 'Paris': 17.5}
# или
temps = dict(Oslo=13, London=15.4, Paris=17.5)
```


Вдруг мы узнали температуру в Мадриде, и ничего не стоит ее добавить:



```
temps['Madrid'] = 26.0
```


Посмотрим, что получилось:



```
>>> print temps
{'Oslo': 13, 'London': 15.4, 'Paris': 17.5, 'Madrid': 26.0}
```



## Операции со словарями

Строковые «индексы» в словарях называются ключами, *keys*. И для того, чтобы пройти циклом по словарю мы можем писать `for key in d` и сопоставлять ему значение, записываемое в форме `d[key]`. Применим эту технику к нашему словарю для температур:



```
>>> for city in temps:
... print 'The temperature in %s is %g' % (city, temps[city])
...
The temperature in Paris is 17.5
The temperature in Oslo is 13
The temperature in London is 15.4
The temperature in Madrid is 26
```


Мы можем проверить, есть ли в нашей базе данных в виде словаря данные о каком-то городе:



```
>>> if 'Berlin' in temps:
... print 'Berlin:', temps['Berlin']
... else:
... print 'No temperature data for Berlin'
...
No temperature data for Berlin
```


Запись вида `key in d` представляет собой обычное булево выражение:



```
>>> 'Oslo' in temps
True
```


И ключи, и значения можно достать из словаря в виде списков:



```
>>> temps.keys()
['Paris', 'Oslo', 'London', 'Madrid']
>>> temps.values()
[17.5, 13, 15.4, 26.0]
```


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



```
>>> for city in sorted(temps):
... print city
...
London
Madrid
Oslo
Paris
```


Пара ключ-значения может быть удалена с помощью `del d[key]`:



```
>>> del temps['Oslo']
>>> temps
{'Paris': 17.5, 'London': 15.4, 'Madrid': 26.0}
>>> len(temps)
3
```


Когда нам нужно скопировать словарь:



```
>>> temps_copy = temps.copy()
>>> del temps_copy['Paris'] # это не затронет temps
>>> temps_copy
{'London': 15.4, 'Madrid': 26.0}
>>> temps
{'Paris': 17.5, 'London': 15.4, 'Madrid': 26.0}
```


Если же две переменные ссылаются на один словарь, и мы меняем одну из них, то это скажется и на другой:



```
>>> t1 = temps
>>> t1['Stockholm'] = 10.0 # поменяли t1
>>> temps # temps также изменилось
{'Stockholm': 10.0, 'Paris': 17.5, 'London': 15.4, 'Madrid': 26.0}
```


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



## Пример: Полиномы

Вообще говоря, ключами в словарях могут выступать не только строки, а любые неизменяемые объекты. То есть это могут быть int, float, complex, str и tuple. И с помощью ключей в виде int объектов, это оказывается удобным, например, как будет показано далее для представления многочленов. Рассмотрим полином:


Данные о полиноме могут быть эффективно представлены в виде словаря, где ключами служат степени *x*, а значениями — коэффициенты при этих степенях. То есть:



```
p = {0: -1, 2: 1, 7: 3}
```


Словари освобождают нас и память компьютера от ненужных, неизвестных нам членов других степеней. Для списков нам бы пришлось обозначить все предполагаемые степени. Используя такую запись словарей, мы можем придумать функцию, которая возвращает значение для заданного в такой форме полинома и численного значения *x*:



```
def poly1(data, x):
sum = 0.0
for power in data:
sum += data[power]*x**power
return sum
# или более емкая запись с использованием sum:
def poly1(data, x):
return sum([data[p]*x**p for p in data])
```



## Чтение из файла

В упражнении к первому уроку у нас была таблица о плотностях разных веществ, пусть она теперь записана в виде dat-файла, данные из которого мы хотим занести словарь:



air 0.0012 gasoline 0.67 ice 0.9 pure water 1.0 seawater 1.025 human body 1.03 limestone 2.6 granite 2.7 iron 7.8 silver 10.5 mercury 13.6 gold 18.9 platinium 21.4 Earth mean 5.52 Earth core 13 Moon 3.3 Sun mean 1.4 Sun core 160 proton 2.8E+14


Ключами, естественно, у нас будут названия веществ, а значениями их плотности. Для переноса информации из файла в словарь, мы построчно прочитаем файл densities.dat, разобьем строки на слова, и конвертируем каждое второе слово в число:



```
def read_densities(filename):
infile = open(filename, 'r')
densities = {}
for line in infile:
words = line.split()
density = float(words[-1])
if len(words[:-1]) == 2: #для объектов, состоящих из более чем одного слова (Sun core)
substance = words[0] + ' ' + words[1]
else:
substance = words[0]
densities[substance] = density
infile.close()
return densities
densities = read_densities('densities.dat')
print(densities)
```

## Вложенные словари

Представим, у нас есть файл данных, представленный в виде таблицы:



A B C D 1 11.7 0.035 2017 99.1 2 9.2 0.037 2019 101.2 3 12.2 no no 105.2 4 10.1 0.031 no 102.1 5 9.1 0.033 2009 103.3 6 8.7 0.036 2015 101.9


Слово no здесь означает, что данных нет. Для словарей, что мы использовали раньше здесь уже возникают сложности, поскольку здесь каждому «словесному индексу» соответствует не одно число, а несколько. Поэтому оказывается естественной концепция создания вложенных словарей, в которых обращение к какому-то элементу, скажем Сi, происходит в форме `data['C'][i]`. Кроме того, мы хотим по ходу действия не просто перенести данные в словарь, а и рассчитать среднее значение для каждого столбца.

Алгоритм для создания такого табличного словаря в начале рассмотрим на более простых примерах:



```
>>> d = {'key1': {'key1': 2, 'key2': 3}, 'key2': 7}
```


И тут же исследуем его. Можно воспринимать вложенные словари просто как то, что значением, соответствующим ключу может быть практически любой тип объектов, в том числе и словарь. То есть, как в списках, воспринимаем запись `d['key1']` как уже самостоятельный объект со своей нумерацией:



```
>>> d['key1'] # это словарь
{'key2': 3, 'key1': 2}
>>> type(d['key1'])
<type 'dict'> # точно словарь
>>> d['key1']['key1']
2
>>> d['key1']['key2']
3
>>> d['key2']['key1'] # а здесь это не пройдет
...
TypeError: unsubscriptable object
>>> type(d['key2'])
<type 'int'> # потому что это просто целое
```


Теперь, когда мы ознакомились с концепцией, можно приступать к делу:



```
infile = open('table.dat', 'r')
lines = infile.readlines()
infile.close()
data = {}
first_line = lines[0]
properties = first_line.split()
for p in properties:
data[p] = {}
for line in lines[1:]:
words = line.split()
i = int(words[0])
values = words[1:]
for p, v in zip(properties, values):
if v != 'no':
data[p][i] = float(v)
for p in data:
values = data[p].values()
data[p]['mean'] = sum(values)/len(values)
for p in sorted(data):
print 'Mean value of property %s = %g' % (p, data[p]['mean'])
```


На выходе имеем рассказ о средних значениях:


`Mean value of property A = 9.875`


Mean value of property B = 0.0279167

Mean value of property C = 1678.42

Mean value of property D = 98.0417


Если бы мы распечатали сам словарь, то узнали, что он выглядит примерно так:



```
{'A': {1: 11.7, 2: 9.2, 3: 12.2, 4: 10.1, 5: 9.1, 6: 8.7, 'mean': 10.1667},
'B': {1: 0.035, 2: 0.037, 4: 0.031, 5: 0.033, 6: 0.036, 'mean': 0.0344},
'C': {1: 2017, 2: 2019, 5: 2009, 6: 2015, 'mean': 2015},
'D': {1: 99.1, 2: 101.2, 3: 105.2, 4: 102.1, 5: 103.3, 6: 101.9, 'mean': 102.133}}
```


Итак, что происходит в программе. В первых трех строчках мы открыли файл для чтения, прочитали и закрыли. Далее создали пустой словарь, в который мы будем заносить данные, выделили первую строку (с индексом 0), разбили ее на слова, которые и есть наши A, B, C, D и сформировали первый уровень словаря `data`.

Далее срезом исключая рассмотренную строку, циклом проходимся по остальным строкам таблицы. Разбиваем каждую строку на слова. Каждое слово с индексом 0 это заголовок строки, оставшиеся слова — числа и слова 'no'. Для того, чтобы записать только известные данные в числовом формате, используется zip-проход, который и создает второй уровень вложенного словаря.

Следующий цикл находит среднее значение, а последний сортирует и выводит данные.

## Сравнение стоимости акций

Мы хотим сравнить эволюцию стоимости акций трех гигантов компьютерной индустрии: Microsoft, Sun Microsystems и Google. Соответствующие данные вы можете найти на finance.yahoo.com введя в строке поиска вверху слева имя компании и после этого выбрав слева вкладку Historical Prices. На этой странице мы можем выбрать временной интервал, для которого мы хотим просмотреть историю. Пусть для Microsoft и Sun это будут 1 января 1988, а для Google 1 января 2005 года. Выбираем "Monthly" (помесячно) и нажимаем "Get prices". Эти данные теперь мы можем загрузить на свой компьютер в виде табличного csv-файла и обозвать в стиле `stockprices_name_of_company.csv`.

Теперь можете посмотреть как они выглядят в вашем табличном процессоре. Там вы обнаружите что-то вроде:



Date,Open,High,Low,Close,Volume,Adj Close 2008-06-02,12.91,13.06,10.76,10.88,16945700,10.88 2008-05-01,15.50,16.37,12.37,12.95,26140700,12.95 2008-04-01,15.78,16.23,14.62,15.66,10330100,15.66 2008-03-03,16.35,17.38,15.41,15.53,12238800,15.53 2008-02-01,17.47,18.03,16.06,16.40,12147900,16.40 2008-01-02,17.98,18.14,14.20,17.50,15156100,17.50 2007-12-03,20.61,21.55,17.96,18.13,9869900,18.13 2007-11-01,5.65,21.60,5.10,20.78,17081500,20.78 ...


Итак, содержание файла весьма простое. Первая строка содержит названия столбцов, первый из которых обозначает дату, а остальные различные измерения стоимости акций, среди которых мы будем следить за изменением последнего Adjusted Closing Price — скорректированной цены закрытия.

Сравнивать числа неинтересно, мы построим графики. Задача разбивается на две: прочитать файл, считав из него данные и построить по ним зависимости. Начнем с первой проблемы, для этого напишем читающую функцию:



```
def read_file(filename):
infile = open(filename, 'r')
infile.readline() # читаем заголовки столбцов
dates = []; prices = 
for line in infile:
columns = line.split(',') # разделяем по запятой
date = columns[0]
date = date[:-3] # пропускаем день месяца (три последних цифры)
price = columns[-1] # нам нужен только последний столбец
dates.append(date)
prices.append(float(price)) # не забываем конвертировать
infile.close()
dates.reverse() # возвращаем порядок: от более старых к новым
prices.reverse() # и соответственно цены
return dates, prices
```


Для того, чтобы прочитать файл достаточно вызвать функцию:



```
dates_Google,prices_Google = read_file('stockprices_Google.csv')
```


Вместо того, чтобы работать с разными переменными, удобно ввести словари с именами компаний в качестве ключей. Например, в двух словарях:



```
dates = {}; prices = {}
d, p = read_file('stockprices_Sun.csv')
dates['Sun'] = d; prices['Sun'] = p
d, p = read_file('stockprices_Microsoft.csv')
dates['MS'] = d; prices['MS'] = p
d, p = read_file('stockprices_Google.csv')
dates['Google'] = d; prices['Google'] = p
```


Мы также можем собрать словари дат и цен в один словарь данных:



```
data = {'prices': prices, 'dates': dates}
```


Словарь `data` вложенный, поэтому и обращения к нему имеют соответствующую форму, например, цены на акции Microsoft мы получим с помощью `data['prices']['MS']`.

Следующее, что мы должны сделать — нормировать данные, чтобы их было легко сравнивать. Идея в том, что Sun и Microsoft начинают с единичной ценой, а Google стартует позже, с лучшей на тот месяц из цен Sun и Microsoft. Нормировка для Sun и Microsoft тогда осуществляется делением на начальную стоимость акций:



```
norm_price = prices['Sun'][0]
prices['Sun'] = [p/norm_price for p in prices['Sun']]
norm_price = prices['MS'][0]
prices['MS'] = [p/norm_price for p in prices['MS']]
```


Нормировка для Google, как мы решили, требует от нас узнать цены для Sun и Microsoft для января 2005 года. Поскольку данные о датах и ценах расположены в списках, имеющих один порядок следования, мы можем найти индекс для '2005-01' в списке дат и использовать его для поиска соответствующей. Тогда нормировка:



```
jan05_MS = prices['MS'][dates['MS'].index('2005-01')]
jan05_Sun = prices['Sun'][dates['Sun'].index('2005-01')]
norm_price = prices['Google'][0]/max(jan05_MS, jan05_Sun)
prices['Google'] = [p/norm_price for p in prices['Google']]
```


И теперь наша цель построить график, показывающий как изменялись цены акций гигантов во времени. Проблема в том, что это самое время у нас представлено в виде строк, как, например, '2005-01'. Решить ее несложно, просто создав нужное количество точек *x*:



```
x = {}
x['Sun'] = range(len(prices['Sun']))
x['MS'] = range(len(prices['MS']))
```


Для Google, координата, с которой стартует график, другая, и это нужно учесть. Определяем индекс нужного месяца и ведем отсчет уже с него:



```
jan05 = dates['Sun'].index('2005-01')
x['Google'] = range(jan05, jan05 + len(prices['Google']), 1)
```


Последний шаг — построить три зависимости:

```
import matplotlib.pyplot as plt
plt.plot(x['MS'], prices['MS'], 'g-')
plt.plot(x['Sun'], prices['Sun'], 'y-')
plt.plot(x['Google'], prices['Google'], 'r-')
plt.legend(['Microsoft', 'Sun', 'Google'], loc=0)
plt.grid()
plt.show()
```


Для более простого восприятия и оценки, приведем текст программы целиком.



```
def read_file(filename):
infile = open(filename, 'r')
infile.readline() # читаем заголовки столбцов
dates = []; prices = 
for line in infile:
columns = line.split(',') # разделяем по запятой
date = columns[0]
date = date[:-3] # пропускаем день месяца (три последних цифры)
price = columns[-1] # нам нужен только последний столбец
dates.append(date)
prices.append(float(price)) # не забываем конвертировать
infile.close()
dates.reverse() # возвращаем порядок: от более старых к новым
prices.reverse() # и соответственно цены
return dates, prices
dates = {}; prices = {}
d, p = read_file('stockprices_Sun.csv')
dates['Sun'] = d; prices['Sun'] = p
d, p = read_file('stockprices_Microsoft.csv')
dates['MS'] = d; prices['MS'] = p
d, p = read_file('stockprices_Google.csv')
dates['Google'] = d; prices['Google'] = p
data = {'prices': prices, 'dates': dates}
# нормировка цен:
norm_price = prices['Sun'][0]
prices['Sun'] = [p/norm_price for p in prices['Sun']]
norm_price = prices['MS'][0]
prices['MS'] = [p/norm_price for p in prices['MS']]
jan05_MS = prices['MS'][dates['MS'].index('2005-01')]
jan05_Sun = prices['Sun'][dates['Sun'].index('2005-01')]
norm_price = prices['Google'][0]/max(jan05_MS, jan05_Sun)
prices['Google'] = [p/norm_price for p in prices['Google']]
# обозначаем "x" точки для построения графиков
x = {}
x['Sun'] = range(len(prices['Sun']))
x['MS'] = range(len(prices['MS']))
# для Google мы должны начать с января 2005:
jan05 = dates['Sun'].index('2005-01')
x['Google'] = range(jan05, jan05 + len(prices['Google']), 1)
import matplotlib.pyplot as plt
plt.plot(x['MS'], prices['MS'], 'g-')
plt.plot(x['Sun'], prices['Sun'], 'y-')
plt.plot(x['Google'], prices['Google'], 'r-')
plt.legend(['Microsoft', 'Sun', 'Google'], loc=0)
plt.grid()
plt.show()
```