Скачать ipynb
04_python_2

Python для анализа данных

1. Кортежи

Кортежи (tuples) очень похожи на списки, но являются неизменяемыми. Как мы видели, использование изменяемых объектов может приводить к неприятным сюрпризам.

Кортежи пишутся в круглых скобках. Если элементов $>1$ или 0, это не вызывает проблем. Но как записать кортеж с одним элементом? Конструкция (x) абсолютно легальна в любом месте любого выражения, и означает просто x. Чтобы избежать неоднозначности, кортеж с одним элементом x записывается в виде (x,).

In [ ]:
(1, 2, 3)
Out[ ]:
(1, 2, 3)
In [ ]:
()
Out[ ]:
()
In [ ]:
(1)
Out[ ]:
1
In [ ]:
(1,)
Out[ ]:
(1,)

Скобки ставить не обязательно, если кортеж — единственная вещь в правой части присваивания.

In [ ]:
t = 1, 2, 3
t
Out[ ]:
(1, 2, 3)

Работать с кортежами можно так же, как со списками. Нельзя только изменять их.

In [ ]:
len(t)
Out[ ]:
3
In [ ]:
t[1]
Out[ ]:
2
In [ ]:
u = 4, 5
t + u
Out[ ]:
(1, 2, 3, 4, 5)
In [ ]:
2 * u
Out[ ]:
(4, 5, 4, 5)

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

In [ ]:
x, y = 1, 2
In [ ]:
x
Out[ ]:
1
In [ ]:
y
Out[ ]:
2

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

In [ ]:
x, y = y, x
In [ ]:
x
Out[ ]:
1
In [ ]:
y
Out[ ]:
2

Это проще, чем в других языках, где приходится использовать третью переменную.

2. Множества

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

In [ ]:
s = {0, 1, 0, 5, 5, 1, 0}
s
Out[ ]:
{0, 1, 5}

Принадлежит ли элемент множеству?

In [ ]:
1 in s, 2 in s, 1 not in s
Out[ ]:
(True, False, False)

Множество можно получить из списка, или строки, или любого объекта, который можно использовать в for цикле (итерабельного).

In [ ]:
l = [0, 1, 0, 5, 5, 1, 0]
set(l)
Out[ ]:
{0, 1, 5}
In [ ]:
set('абба')
Out[ ]:
{'а', 'б'}

Как записать пустое множество? Только так.

In [ ]:
set()
Out[ ]:
set()

Дело в том, что в фигурных скобках в питоне пишутся также словари, что мы будем их обсуждать в следующем параграфе. Когда в них есть хоть один элемент, можно отличить словарь от множества. Но пустые фигурные скобки означают пустой словарь.

In [ ]:
{}
Out[ ]:
{}

Работать с множествами можно как со списками.

In [ ]:
len(s)
Out[ ]:
3
In [ ]:
for x in s:
    print(x)
0
1
5

Это генератор множества (set comprehension).

In [ ]:
{i for i in range(5)}
Out[ ]:
{0, 1, 2, 3, 4}

Объединение множеств.

In [ ]:
s2 = s | {2, 5}
s2
Out[ ]:
{0, 1, 2, 5}

Проверка того, является ли одно множество подмножеством другого.

In [ ]:
s < s2, s > s2, s <= s2, s >= s2
Out[ ]:
(True, False, True, False)

Пересечение.

In [ ]:
s2 & {1, 2, 3}
Out[ ]:
{1, 2}

Разность и симметричная разность.

In [ ]:
s2 - {1,3,5}
Out[ ]:
{0, 2}
In [ ]:
s2 ^ {1,3,5}
Out[ ]:
{0, 2, 3}

Множества (как и списки) являются изменяемыми объектами. Добавление элемента в множество и исключение из него.

In [ ]:
s2.add(4)
s2
Out[ ]:
{0, 1, 2, 4, 5}
In [ ]:
s2.remove(1)
s2
Out[ ]:
{0, 2, 4, 5}

Как и в случае +=, можно скомбинировать теоретико-множественную операцию с присваиванием.

In [ ]:
s2 |= {1, 2}
s2
Out[ ]:
{0, 1, 2, 4, 5}

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

In [ ]:
x = set([1, 4, 2, 4, 2, 1, 3, 4])
print(x)

x.add(5)  # добавление элемента
print(x)

x.pop()  # удаление элемента
print(x)

print(x.intersection(set([2, 4, 6, 8])))  # Пересечение
print(x.difference(set([2, 4, 6, 8])))  # Разность
print(x.union(set([2, 4, 6, 8])))  # Объединение
print(x.symmetric_difference(set([2, 4, 6, 8])))  # Симметрическая разность

print(x.issubset(set([2, 4, 6, 8])))  # Является ли подмножеством
print(x.issubset(set(list(range(10)))))

print(x.issuperset(set([2, 4, 6, 8])))  # Является ли надмножеством
print(x.issuperset(set([2, 4])))
{1, 2, 3, 4}
{1, 2, 3, 4, 5}
{2, 3, 4, 5}
{2, 4}
{3, 5}
{2, 3, 4, 5, 6, 8}
{3, 5, 6, 8}
False
True
False
True

Существуют также неизменяемые множества. Этот тип данных называется frozenset. Операции над такими множествами подобны обычным, только невозможно изменять их, а всего лишь добавлять и исключать элементы.

3. Словари

Словарь содержит пары ключ — значение, причем порядо значений несущественен. Это один из наиболее полезных и часто используемых типов данных в питоне.

In [ ]:
d = {'one': 1, 'two': 2, 'three': 3}
d
Out[ ]:
{'one': 1, 'two': 2, 'three': 3}

Можно узнать значение, соответствующее некоторому ключу. Словари реализованы как хэш-таблицы, так что поиск даже в больших словарях очень эффективен. В языках низкого уровня (например, C) для построения хэш-таблиц требуется использовать внешние библиотеки и писать заметное количество кода. В скриптовых языках (perl, python, php) они уже встроены в язык, и использовать их очень легко.

In [ ]:
d['two']
Out[ ]:
2
In [ ]:
d['four']
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-11-99f82dfe25e5> in <module>
----> 1 d['four']

KeyError: 'four'

Можно проверить, есть ли в словаре данный ключ.

In [ ]:
'one' in d, 'four' in d
Out[ ]:
(True, False)

Можно присваивать значения как имеющимся ключам, так и отсутствующим (они добавятся к словарю).

In [ ]:
d['one'] =- 1
d
Out[ ]:
{'one': -1, 'two': 2, 'three': 3}
In [ ]:
d['four'] = 4
d
Out[ ]:
{'one': -1, 'two': 2, 'three': 3, 'four': 4}

Длина — число ключей в словаре.

In [ ]:
len(d)
Out[ ]:
4

Можно удалить ключ из словаря.

In [ ]:
del d['two']
d
Out[ ]:
{'one': -1, 'three': 3, 'four': 4}

Метод get, если он будет вызван с отсутствующим ключом, не приводит к ошибке, а возвращает специальный объект None. Он используется всегда, когда необходимо указать, что объект отсутствует. В какой-то мере он аналогичен null в C. Если передать методу get второй аргумент — значение по умолчанию, то будет возвращаться это значение, а не None.

In [ ]:
d.get('one'), d.get('five')
Out[ ]:
(-1, None)
In [ ]:
d.get('one', 0), d.get('five', 0)
Out[ ]:
(-1, 0)

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

In [ ]:
d = {}
d
Out[ ]:
{}
In [ ]:
d['zero'] = 0
d
Out[ ]:
{'zero': 0}
In [ ]:
d['one'] = 1
d
Out[ ]:
{'zero': 0, 'one': 1}

А это генератор словаря (dictionary comprehension).

In [ ]:
d = {i: i ** 2 for i in range(5)}
d
Out[ ]:
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

Ключами могут быть любые неизменяемые объекты, например, целые числа, строки, кортежи.

In [ ]:
d = {}
d[0, 0] = 1
d[0, 1] = 0
d[1, 0] = 0
d[1, 1] = -1
d
Out[ ]:
{(0, 0): 1, (0, 1): 0, (1, 0): 0, (1, 1): -1}
In [ ]:
d[0, 0] + d[1, 1]
Out[ ]:
0

Словари, подобно спискам, можно использовать в for циклах. Перебираются имеющиеся в словаре ключи, причем в каком-то непредсказуемом порядке.

In [ ]:
d = {'one': 1, 'two': 2, 'three': 3}
for x in d:
    print(x, '  ', d[x])
one    1
two    2
three    3

Метод keys возвращает список ключей, метод values — список соответствующих значений (в том же порядке), а метод items — список пар (ключ, значение). Точнее говоря, это не списки, а некоторые объекты, которые можно использовать в for циклах или превратить в списки функцией list. Если хочется написать цикл по упорядоченному списку ключей, то можно использовать sorted(d.keys)).

In [ ]:
d.keys(), d.values(), d.items()
Out[ ]:
(dict_keys(['one', 'two', 'three']),
 dict_values([1, 2, 3]),
 dict_items([('one', 1), ('two', 2), ('three', 3)]))
In [ ]:
for x in sorted(d.keys()):
    print(x, '  ', d[x])
one    1
three    3
two    2
In [ ]:
for x, y in d.items():
    print(x, '  ', y)
one    1
two    2
three    3
In [ ]:
del x, y

Что есть истина? И что есть ложь? Подойдём к этому философскому вопросу экспериментально.

In [ ]:
bool(False), bool(True)
Out[ ]:
(False, True)
In [ ]:
bool(None)
Out[ ]:
False
In [ ]:
bool(0), bool(123)
Out[ ]:
(False, True)
In [ ]:
bool(''), bool(' ')
Out[ ]:
(False, True)
In [ ]:
bool([]), bool([0])
Out[ ]:
(False, True)
In [ ]:
bool(set()), bool({0})
Out[ ]:
(False, True)
In [ ]:
bool({}), bool({0: 0})
Out[ ]:
(False, True)
In [ ]:
bool(0.0)
Out[ ]:
True

На выражения, стоящие в булевых позициях (после if, elif и while), неявно напускается функция bool. Некоторые объекты интерпретируются как False: число 0, пустая строка, пустой список, пустое множество, пустой словарь, None и некоторые другие. Все остальные объекты интерпретируются как True. В операторах if или while очень часто используется список, словарь или что-нибудь подобное, что означает делай что-то если этот список (словарь и т.д.) не пуст.

Заметим, что число с плавающей точкой 0.0 тоже интерпретируется как False. Это использовать категорически не рекомендуется: вычисления с плавающей точкой всегда приближённые, и неизвестно, получите Вы 0.0 или 1.234E-12. Лучше напишите if abs(x)<epsilon:.

4. Функции

Это простейшая в мире функция. Она не имеет параметров, ничего не делает и ничего не возвращает. Оператор pass означает "ничего не делай"; он используется там, где синтаксически необходим оператор, а делать ничего не нужно, например, после if или elif, после def и т.д..

In [ ]:
def f():
    pass
In [ ]:
f
Out[ ]:
<function __main__.f()>
In [ ]:
pass
In [ ]:
type(f)
Out[ ]:
function
In [ ]:
r = f()
print(r)
None

Эта функция более полезна: она имеет параметр и что-то возвращает.

In [ ]:
def f(x):
    return x + 1
In [ ]:
f(1), f(1.0)
Out[ ]:
(2, 2.0)
In [ ]:
f('abc')
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-42-eb74912c9d38> in <module>
----> 1 f('abc')

<ipython-input-40-b2c2e2352aee> in f(x)
      1 def f(x):
----> 2     return x + 1

TypeError: must be str, not int

Если у функции много параметров, то возникает желание вызывать её попроще в наиболее часто встречающихся случаях. Для этого в операторе def можно задать значения некоторых параметров по умолчанию, которые должны размещаться в конце списка параметров. При вызове необходимо указать все обязательные параметры, у которых нет значений по умолчанию, а необязательные можно и не указывать. Если при вызове указывать параметры в виде имя=значение, то это можно делать в любом порядке. Это гораздо удобнее, чем вспоминать, является данный параметр восьмым или девятым при вызове какой-нибудь сложной функции. Обратите внимание, что в конструкции имя=значение не ставятся пробелы между символом =.

In [ ]:
def f(x, a=0, b='b'):
    print(x, '  ', a, '  ', b)
In [ ]:
f(1.0)
1.0    0    b
In [ ]:
f(1.0, 1)
1.0    1    b
In [ ]:
f(1.0, b='a')
1.0    0    a
In [ ]:
f(1.0, b='a', a=2)
1.0    2    a
In [ ]:
f(a=2, x=2.0)
2.0    2    b

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

In [ ]:
a = 1
In [ ]:
def f():
    a = 2
    return a
In [ ]:
f()
Out[ ]:
2
In [ ]:
a
Out[ ]:
1

Если в функции нужно использовать какие-нибудь глобальные переменные, их нужно описать как global.

In [ ]:
def f():
    global a
    a = 2
    return a
In [ ]:
f()
Out[ ]:
2
In [ ]:
a
Out[ ]:
2

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

Если функции передаётся в качестве аргумента какой-нибудь изменяемый объект, и функция его изменяет, то это изменение будет видно снаружи после этого вызова. Мы уже обсуждали эту ситуацию, когда две переменные указывают на один и тот же изменяемый объект объект. В данном случае такими переменнями являются глобальная переменная и параметр функции

In [ ]:
def f(x, l):
    l.append(x)
    return l
In [ ]:
l = [1, 2, 3]
f(0, l)
Out[ ]:
[1, 2, 3, 0]
In [ ]:
l
Out[ ]:
[1, 2, 3, 0]

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

In [ ]:
def f(x, l=[]):
    l.append(x)
    return l
In [ ]:
f(0)
Out[ ]:
[0]
In [ ]:
f(1)
Out[ ]:
[0, 1]
In [ ]:
f(2)
Out[ ]:
[0, 1, 2]

Чтобы избежать таких сюрпризов, в качестве значений по умолчанию лучше использовать только неизменяемые объекты.

In [ ]:
def f(x, l=None):
    if l is None:
        l = []
    l.append(x)
    return l
In [ ]:
f(0)
Out[ ]:
[0]
In [ ]:
f(1)
Out[ ]:
[1]
In [ ]:
f(2, [0, 1])
Out[ ]:
[0, 1, 2]

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

In [ ]:
def f(x, *l):
    print(x, '  ', l)
In [ ]:
f(0)
0    ()
In [ ]:
f(0, 1)
0    (1,)
In [ ]:
f(0, 1, 2)
0    (1, 2)
In [ ]:
f(0, 1, 2, 3)
0    (1, 2, 3)

Звёздочку можно использовать и при вызове функции. Можно заранее построить список (или кортеж) аргументов, а потом вызвать функцию с этими аргументами.

In [ ]:
l=[1, 2]
c=('a', 'b')
f(*l, 0, *c)
1    (2, 0, 'a', 'b')

Такую распаковку из списков и кортежей можно использовать не только при вызове функции, но и при построении списка или кортежа.

In [ ]:
(*l, 0, *c)
Out[ ]:
(1, 2, 0, 'a', 'b')
In [ ]:
[*l, 0, *c]
Out[ ]:
[1, 2, 0, 'a', 'b']
In [ ]:
[*l, 3]
Out[ ]:
[1, 2, 3]

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

In [ ]:
def f(x, y, **d):
    print(x, '  ', y, '  ', d)
In [ ]:
f(0, 1, foo=2, bar=3)
0    1    {'foo': 2, 'bar': 3}

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

In [ ]:
d={'foo': 2, 'bar': 3}
f(0, 1, **d)
0    1    {'foo': 2, 'bar': 3}
In [ ]:
d['x'] = 0
d['y'] = 1
f(**d)
0    1    {'foo': 2, 'bar': 3}

Вот любопытный способ построить словарь с ключами-строками.

In [ ]:
def f(**d):
    return d
In [ ]:
f(x=0, y=1, z=2)
Out[ ]:
{'x': 0, 'y': 1, 'z': 2}

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

In [ ]:
d={0: 'a', 1: 'b'}
{**d, 2: 'c'}
Out[ ]:
{0: 'a', 1: 'b', 2: 'c'}

Вот простой способ объединить два словаря.

In [ ]:
d1 = {0: 'a', 1: 'b'}
d2 = {2: 'c', 3: 'd'}
{**d1, **d2}
Out[ ]:
{0: 'a', 1: 'b', 2: 'c', 3: 'd'}

Если один и тот же ключ встречается несколько раз, следующее значение затирает предыдущее.

In [ ]:
d2 = {1: 'B', 2: 'C'}
{**d1, 3: 'D', **d2, 3: 'd'}
Out[ ]:
{0: 'a', 1: 'B', 3: 'd', 2: 'C'}

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

In [ ]:
def f(x, y, *l, **d):
    print(x, '  ', y, '  ', l, '  ', d)
In [ ]:
f(0, 1, 2, 3, foo=4, bar=5)
0    1    (2, 3)    {'foo': 4, 'bar': 5}

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

In [ ]:
def f0(x):
    return x + 2
In [ ]:
def f1(x):
    return 2 * x
In [ ]:
l = [f0, f1]
l
Out[ ]:
[<function __main__.f0(x)>, <function __main__.f1(x)>]
In [ ]:
x = 2.0
n = 1
l[n](x)
Out[ ]:
4.0

Если Вы пишете функцию не для того, чтобы один раз её вызвать и навсегда забыть, то нужна документация, объясняющая, что эта функция делает. Для этого сразу после строчки def пишется строка. Она называется док-строкой, и сохраняется при трансляции исходного текста на питоне в байт-код, в отличие от комментариев, которые при этом отбрасываются. Обычно эта строка заключается в тройные кавычки и занимает несколько строчек. Док-строка доступна как атрибут __doc__ функции, и используется функцией help. Вот пример культурно написанной функции, вычисляющей $n$-е число Фибоначчи.

Для проверки типов аргументов, переданных функции, удобно использовать оператор assert. Если условие в нём истинно, всё в порядке, и он ничего не делает; если же оно ложно, выдаётся сообщение об ошибке.

In [ ]:
def fib(n):
    '''вычисляет n-е число Фибоначчи'''
    
    assert type(n) is int and n>0
    
    if n <= 2:
        return 1
    
    x, y = 1, 1
    for i in range(n - 2):
        x, y = y, x + y
    
    return y
In [ ]:
fib.__doc__
Out[ ]:
'вычисляет n-е число Фибоначчи'
In [ ]:
help(fib)
Help on function fib in module __main__:

fib(n)
    вычисляет n-е число Фибоначчи

В jupyter-ноутбуке к документации можно обращаться более удобным способом

In [ ]:
fib?
In [ ]:
[fib(n) for n in range(1, 10)]
Out[ ]:
[1, 1, 2, 3, 5, 8, 13, 21, 34]
In [ ]:
fib(-1)
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-92-673c4d72cb80> in <module>
----> 1 fib(-1)

<ipython-input-87-0acae02994d6> in fib(n)
      2     '''вычисляет n-е число Фибоначчи'''
      3 
----> 4     assert type(n) is int and n>0
      5 
      6     if n <= 2:

AssertionError: 
In [ ]:
fib(2.0)
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-93-1676211dbb8d> in <module>
----> 1 fib(2.0)

<ipython-input-87-0acae02994d6> in fib(n)
      2     '''вычисляет n-е число Фибоначчи'''
      3 
----> 4     assert type(n) is int and n>0
      5 
      6     if n <= 2:

AssertionError: 

5. Некоторые полезные функции

Функция zip скрещивает два массива одной длины

In [ ]:
x = zip(range(5), range(0, 10, 2))
print(list(x))
[(0, 0), (1, 2), (2, 4), (3, 6), (4, 8)]

Функция map применяет функию к каждому элементу массива

In [ ]:
x = map(fib, range(1, 10))
print(list(x))
[1, 1, 2, 3, 5, 8, 13, 21, 34]

Функция sorted выполняет сортировку данныз

In [ ]:
x = list(zip([7, 3, 4, 4, 5, 3, 9], ['a', 'n', 'n', 'a', 'k', 'n', 'a']))
# сначала сортировка по букве по алфавиту, потом сортировка по убыванию по числу 
x = sorted(x, key=lambda element: (element[1], -element[0]))
print(list(x))
[(9, 'a'), (7, 'a'), (4, 'a'), (5, 'k'), (4, 'n'), (3, 'n'), (3, 'n')]

При подготовке использованы материалы https://inp.nsk.su/~grozin/python/