Скачать ipynb
03_python_1

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

1. Язык Python

При программировании на питоне первые полезные результаты начинают получаться минут через 5 после начала работы над проектом. Не надо изобретать никаких велосипедов: всё, что может понадобиться, доступно или в стандартной библиотеке, или в одном из многочисленных дополнительных пакетов. Программы получаются элегантные и лаконичные, их легко и приятно писать и читать. Имеется широкий выбор высококачественных инструментов программиста: интегрированные среды разработки, отладчики и средства тестирования и т.д. Питон прекрасно приспособлен для написания больших и сложных программных комплексов. И всё это совершенно не зависит от железа и операционной системы. Конечно, если не вызывать низкоуровневых системно-зависимыех библиотек.

Главный минус — питон, мягко говоря, не славится потрясающим быстродействием. Писать программы сложных вычислений на питоне, конечно, глупо. Но есть ряд способов обойти это ограничение. Если программа занимается регулярной обработкой больших массивов чисел с плавающей точкой, то использование numpy радикально повышает её быстродействие. Пакет numpy и его расширение scipy фактически делают matlab ненужным — дают ту же функциональность с более приятным языком программирование. Есть ряд пакетов для построения высококачественных графиков, например, matplotlib. Другое средство повышения быстродействия — cython. Пишется программа, выглядящая почти как питонская, но со вставленными статичестими декларациями типов. cython транслирует её в исходный код на C, что часто даёт быстродействие, сравнимое с вручную написанным C. Программа на cython-е может свободно вызывать функции из библиотек на C; её можно использовать из обычного питона. Ну и наконец можно написать критически важные для быстродействия системы в целом вычисления на другом языке (C например), и вызывать эти внешние программы из питона. Питон при этом выполняет роль клея — реализует логику высокого уровня, системно-независимый GUI и т.д.

Питон — сильно типизированный язык с динамической типизацией. Всё, с чем работает программа — объекты; каждый из них принадлежит определённому типу. Если программа пытается выполнить какую-то операцию над объектом такого типа, который не поддерживает эту операцию, произойдёт ошибка времени выполнения. Описаний переменных в питоне нет. Одна и та же переменная может в разные моменты иметь значения — объекты разных типов. Ошибка в типах может проявиться много времени спустя после того, как программа сдана в эксплуатацию, особенно если она происходит в редко используемом участке кода. Язык допускает ситуацию, когда мы сначала присвоим какой-то переменной целое число, потом строку, потом ещё что-то, но это плохой стиль. Переменная заводится для одного конкретного использования, и естественно, чтобы её значения в любой момент подходили для этого использования и имели одинаковый тип. В исходный текст на питоне 3.5 можно включить (необязательные) аннотации типов переменных и функций, и прогнать её через программу статической проверки типов.

Питонячий стиль кода

Обязательно прочитайте инструкцию по стилю кода: http://pep8.ru/doc/pep8/

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

  • Отступы составляют ровно 4 пробела, табуляции не используются. К слову, в Питоне нет ключевых слов по типу { и } в C и begin и end в Паскале. Блоки кода разделяются пробелами.
  • Все переменные должны иметь понятные названия и состоять только из строчных букв. Например, вместо того, чтобы назвать выборку как X, лучше назвать ее sample. В качестве разделителей используйте подчеркивания. В редких случаях можно и отступать от этого правила, если обозначения понятны из решаемой задачи.
  • Вокруг всех знаков арифметических операций, присваивания и пр. обязательны пробелы с двух сторон. Исключение — запись вида a=b в аргументах функции. Примеры будут далее.
  • Разделяйте логические куски программы пустыми строками. Врядли вы сможете написать код строк на 10-15, в который нельзя вставить ни одну пустую строку, разделяющую код на логические части. ПЕРЕД всеми логическими кусками кода должен быть комментарий к этому куску кода.
  • Все функции (кроме самых маленьких) должны содержать подробную документацию, написанную по правилам оформления документаций.
  • Если комментарий дается на строке с кодом, то оформить его стоит так:

код [ровно 2 пробела] # [ровно 1 пробел] комментарий

  • Если комментарий дается на отдельной строке, то он должен иметь тот же отступ, что и строка кода под ним. Скорее всего перед таким комментарием можно вставить пустую строку.
  • Не нужно комментировать очевидное.
  • Не нужно писать весь код в одной ячейке ноутбука!!!
  • Не стоит создавать вермишель ячеек — несколько подряд идущих ячеек с 1-3 строками кода, если в этом нет необходимости. Прим., в данном ноутбуке такая вермишель осмысленна :)
  • Если есть возможность, при выводе десятичных чисел следует оставлять разумное число знаков после запятой.

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

2. Типы объектов

Любой объект имеет тип. Если вкратце, то в python есть следующие основные типы данных:

  1. Числовые: int(целое число), float(число с плавающей точкой), complex(комплексное число), bool(логические: true, false).
  2. Последовательности: str(строки), list(списки, что-то вроде массивов), tuple(кортежи или неизменяемые списки).
  3. Прочее: dict(словарь), set(множество), файл и None(аналог NULL).

Списки, множества и словари являются изменяемыми объектами, а все остальные неизменяемыми.

Пример неизменяемости объекта. Мы создаем объект типа int. Id x и y указывают на один и тот же объект:

In [1]:
x = 10
y=x
id(x) == id(y)
Out[1]:
True
In [2]:
id(y) == id(10)
Out[2]:
True
In [3]:
x = x + 1
print(id(x) == id(y))
False
In [4]:
id(x) == id(10)
Out[4]:
False

Объект переменной x изменился. Объект 10 не может изменяться. Неизменяемые объекты не допускают изменений после создания.

А если объект изменяемый:

In [5]:
m = list([1, 2, 3])
n = m
id(m) == id(n)
Out[5]:
True
In [6]:
m.pop() #Удаление элемента из объекта списка изменяет сам объект
print(id(m) == id(n))
True

Переменные m и n будут указывать на один и тот же объект списка после изменения. В объекте списка теперь лежит [1,2].

3. Числа

Арифметические операции имеют ожидаемые приоритеты. При необходимости используются скобки.

In [7]:
1 + 2 * 3
Out[7]:
7
In [8]:
(1 + 2) * 3
Out[8]:
9

Возведение целого числа в целую степень даёт целое число, если показатель степени $\geqslant 0$, и число с плавающей точкой, если он $<0$. Так что тип результата невозможно определить статически, если значение переменной n неизвестно.

In [9]:
n = 3
2 ** n
Out[9]:
8
In [10]:
n = -3
2 ** n
Out[10]:
0.125

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

In [11]:
n + 1.0
Out[11]:
-2.0

Деление целых чисел всегда даёт результат с плавающей точкой, даже если они делятся нацело. Операторы // и % дают целое частное и остаток.

In [12]:
7 / 4
Out[12]:
1.75
In [13]:
7 // 4
Out[13]:
1
In [14]:
7 % 4
Out[14]:
3
In [15]:
4 / 2
Out[15]:
2.0

Если Вы попытаетесь использовать переменную, которой не присвоено никакого значения, то получите сообщение об ошибке.

In [16]:
x
Out[16]:
11

Операция x+=1 означает x=x+1, аналогично для других операций. В питоне строго различаются операторы (например, присваивание) и выражения, так что таких операций, как ++ в C, нет. Хотя вызов функции в выражении может приводить к побочным эффектам.

In [17]:
x = 1
x += 1
print(x)
2
In [18]:
x *= 2
print(x)
4

Удаление объекта

In [19]:
del x
x
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-19-1beca76b84e9> in <module>
      1 del x
----> 2 x

NameError: name 'x' is not defined
In [20]:
x = 4

Любопытная особенность питона: можно использовать привычные из математики сравнения вроде $x<y<z$, которые в других языках пришлось бы записывать как x<y and y<z.

In [21]:
1 < 2 <= 2
Out[21]:
True
In [22]:
1 < 2 < 2
Out[22]:
False

Логические выражения можно комбинировать с помощью and и or, причем эти операции имеют более низкий приоритет, чем сравнения. Если результат уже ясен из первого операнда, второй операнд не вычисляется. А вот так выглядит оператор if.

In [23]:
if 1 < 2 and x < 3:
    print('T')
else:
    print('F')
F
In [24]:
if 1 < 2 or x < 3:
    print('T')
else:
    print('F')
T

После строчки, заканчивающейся :, можно писать последовательность операторов с одинаковым отступом (больше, чем у строчки if). Никакого признака конца такой группы операторов не нужно. Первая строчка после else:, имеющая тот же уровень отступа, что и if и else: — это следующий оператор после if.

Также в питоне есть конструкция match/case. Оператор match принимает выражение subject и сравнивает его значение с последовательными шаблонами, заданными как один или несколько блоков case. Рассмотрим пример с функцией факториала:

In [25]:
def factorial(n):
    if n == 0 or n == 1:
        return 1
    else:
        return n * factorial(n-1)

factorial(5)    
Out[25]:
120

Есть и условные выражения:

In [26]:
(0 if x < 0 else 1) + 1
Out[26]:
2

Обычно в начале пишется основное выражение, оно защищается условием в if, а после else пишется исключительный случай.

В питоне немного встроенных функций. Большинство надо импортировать. Элементарные функции импортируют из модуля math, а также из модуля numpy, который мы будем в дальнейшем пользоваться чаще всего. Модуль numpy позволяет питону работать быстрее, поэтому является предпочтительней (о ней будет описано в другом разделе). Рассмотрим несколько примеров использования библиотеки math:

In [27]:
from math import sin, pi
In [28]:
pi
Out[28]:
3.141592653589793
In [29]:
round(pi, 2)
Out[29]:
3.14
In [30]:
print('{:.2f}'.format(pi))
3.14
In [31]:
round(sin(pi / 6), 2)
Out[31]:
0.5
In [32]:
type(2)
Out[32]:
int
In [33]:
type(int)
Out[33]:
type
In [34]:
type(type)
Out[34]:
type
In [35]:
type(2.1)
Out[35]:
float
In [36]:
type(True)
Out[36]:
bool
In [37]:
type(None)
Out[37]:
NoneType

Имена типов по совместительству являются функциями, преобразующими в этот тип объекты других типов если такое преобразование имеет смысл.

In [38]:
float(2)
Out[38]:
2.0
In [39]:
int(2.0)
Out[39]:
2
In [40]:
int(2.9)
Out[40]:
2
In [41]:
int(-2.9)
Out[41]:
-2

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

4. Строки

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

In [42]:
s = 'Какая-нибудь строка \u00F6 \u03B1 \u2230 \u342A'
print(s)
Какая-нибудь строка ö α ∰ 㐪
In [43]:
'Эта строка может содержать " внутри'
Out[43]:
'Эта строка может содержать " внутри'
In [44]:
"Эта строка может содержать ' внутри"
Out[44]:
"Эта строка может содержать ' внутри"
In [45]:
s = 'Эта содержит и \', и \"'
print(s)
Эта содержит и ', и "
In [46]:
s = """Строка,
занимающая
несколько
строчек
"""
print(s)
Строка,
занимающая
несколько
строчек

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

In [47]:
s = 'Такие ' 'строки ' 'слипаются'
print(s)
Такие строки слипаются
In [48]:
print('Такие\n'
      'строки\n'
      'слипаются')
Такие
строки
слипаются

В питоне нет специального типа char, его роль играют строки длины 1. Функция ord возвращает (юникодный) номер символа, а обратная ей функция chr возвращает символ (строку длины 1).

In [49]:
n = ord('а')
n
Out[49]:
1072
In [50]:
chr(n)
Out[50]:
'а'

Функция len возвращает длину строки. Она применима не только к строкам, но и к спискам, словарям и многим другим типам, про объекты которых разумно спрашивать, какая у них длина.

In [51]:
s = '0123456789'
len(s)
Out[51]:
10

Символы в строке индексируются с 0. Отрицательные индексы используются для счёта с конца: s[-1] — последний символ в строке, и т.д.

In [52]:
s[0]
Out[52]:
'0'
In [53]:
s[3]
Out[53]:
'3'
In [54]:
s[-1]
Out[54]:
'9'
In [55]:
s[-2]
Out[55]:
'8'

Можно выделить подстроку, указав диапазон индексов. Подстрока включает символ, соответствующий началу диапазона, но не включает соответствующий концу. Удобно представлять себе, что индексы соответствуют положениям между символами строки. Тогда подстрока s[n:m] будет расположена между индексами n и m.

In [56]:
s[1:3]
Out[56]:
'12'
In [57]:
s[:3]
Out[57]:
'012'
In [58]:
s[3:]
Out[58]:
'3456789'
In [59]:
s[:-1]
Out[59]:
'012345678'
In [60]:
s[3:-2]
Out[60]:
'34567'
In [61]:
s[::2]
Out[61]:
'02468'

Если не указано начало диапазона, подразумевается от начала строки; если не указан его конец — до конца строки.

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

In [62]:
s = 'abc'; t = 'def'
s + t
Out[62]:
'abcdef'
In [63]:
s * 3
Out[63]:
'abcabcabc'

Операция in проверяет, содержится ли символ (или подстрока) в строке.

In [64]:
'a' in s
Out[64]:
True
In [65]:
'd' in s
Out[65]:
False
In [66]:
'ab' in s
Out[66]:
True
In [67]:
'b' not in s
Out[67]:
False

У объектов типа строка есть большое количество методов. Метод lstrip удаляет все whitespace-символы (пробел, tab, newline) в начале строки; rstrip — в конце; а strip — с обеих сторон. Им можно передать необязательный аргумент — символы, которые нужно удалять.

In [68]:
'   строка   '.lstrip()
Out[68]:
'строка   '
In [69]:
'   строка   '.rstrip()
Out[69]:
'   строка'
In [70]:
'   строка   '.strip()
Out[70]:
'строка'

lower и upper переводят все буквы в маленькие и заглавные.

In [71]:
'СтРоКа'.lower()
Out[71]:
'строка'
In [72]:
'СтРоКа'.upper()
Out[72]:
'СТРОКА'

Проверки: буквы (маленькие и заглавные), цифры, пробелы.

In [73]:
'АбВг'.isalpha()
Out[73]:
True
In [74]:
'абвг'.islower()
Out[74]:
True
In [75]:
'АБВГ'.isupper()
Out[75]:
True
In [76]:
'0123'.isdigit()
Out[76]:
True
In [77]:
' \t\n'.isspace()
Out[77]:
True

Строки имеют тип str.

In [78]:
type(s)
Out[78]:
str
In [79]:
s = str(123)
s
Out[79]:
'123'
In [80]:
n = int(s)
n
Out[80]:
123
In [81]:
int('123x')
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-81-1183a716c046> in <module>
----> 1 int('123x')

ValueError: invalid literal for int() with base 10: '123x'
In [82]:
x = float('123.456E-7')
x
Out[82]:
1.23456e-05

Метод format особенно полезен для вывода.

In [83]:
'str: {}  int: {}  float: {}'.format(s, n, x)
Out[83]:
'str: 123  int: 123  float: 1.23456e-05'

Или можно использовать f-строки, которые имеют такой же функционал, но позволяет питону выигрывать в скорости, в отличие от метода format.

In [84]:
f'str: {s} int: {n} float: {x}'
Out[84]:
'str: 123 int: 123 float: 1.23456e-05'

Ширина поля 5 (больше, если не влезет); десятичный, шестнадцатиричный и двоичный форматы.

In [85]:
print('''{:5d}
{:5x}
{:5b}'''.format(n, n, n))
  123
   7b
1111011

Ширина поля 10 (больше, если не влезет); после десятичной точки 5 цифр; формат с фиксированной точкой или экспоненциальный.

In [86]:
print('''{:10.5f}
{:10.5e}
{:10.5f}
{:10.5e}'''.format(x, x, 1 / x, 1 / x))
   0.00001
1.23456e-05
81000.51840
8.10005e+04

Современным аналогом format является f-string или f-строки.

"F-строки" обеспечивают краткий, читаемый способ включения значения выражений Python внутри строк. В исходном коде Python форматированная строка или по другому f-string — это буквальная строка с префиксом 'f' или 'F', которая содержит выражения внутри фигурных скобок {}. Выражения заменяются их значениями. Использование f-строк помогает выйграть во времени и имеет много дополнительных полезных функций. Ранее уже приводился пример с f-строкой. Рассмотрим некоторые дополнительные "фишки", которые дают нам f-строки.

In [87]:
greetings = "hello"
greetings
Out[87]:
'hello'
In [88]:
f"{greetings:>10}"
Out[88]:
'     hello'
In [89]:
f"{greetings:<10}"
Out[89]:
'hello     '
In [90]:
f"{greetings:^11}"
Out[90]:
'   hello   '
In [91]:
# выравнивание чисел
a = "1"
b = "21"
c = "321"
d = "4321"
print("\n".join((f"{a:>10}", f"{b:>10}", f"{c:>10}", f"{d:>10}")))
         1
        21
       321
      4321

Также можно указывать количество разрядов после запятой:

In [92]:
x = 3.14159265
print(f'pi = {x:.2f}')
pi = 3.14

5. Списки

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

In [93]:
l = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
l
Out[93]:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
In [94]:
len(l)
Out[94]:
10
In [95]:
l[0]
Out[95]:
0
In [96]:
l[3]
Out[96]:
3
In [97]:
l[10]
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-97-f97e037cfd13> in <module>
----> 1 l[10]

IndexError: list index out of range
In [98]:
l[-1]
Out[98]:
9
In [99]:
l[-2]
Out[99]:
8
In [100]:
l[1:3]
Out[100]:
[1, 2]

Обратите внимание, что l[:3]+l[3:]==l.

In [101]:
l[:3]
Out[101]:
[0, 1, 2]
In [102]:
l[3:]
Out[102]:
[3, 4, 5, 6, 7, 8, 9]
In [103]:
l[3:3]
Out[103]:
[]
In [104]:
l[3:-2]
Out[104]:
[3, 4, 5, 6, 7]
In [105]:
l[:-2]
Out[105]:
[0, 1, 2, 3, 4, 5, 6, 7]

Списки являются изменяемыми объектами. Это сделано для эффективности. В списке может быть 1000000 элементов. Создавать его копию каждый раз, когда мы изменили один элемент, слишком дорого.

In [106]:
l[3] = 'три'
l
Out[106]:
[0, 1, 2, 'три', 4, 5, 6, 7, 8, 9]

Можно заменить какой-нибудь подсписок на новый список (в том числе другой длины).

In [107]:
l[3:3] = [0]
l
Out[107]:
[0, 1, 2, 0, 'три', 4, 5, 6, 7, 8, 9]
In [108]:
l[3:3] = [10, 11, 12]
l
Out[108]:
[0, 1, 2, 10, 11, 12, 0, 'три', 4, 5, 6, 7, 8, 9]
In [109]:
l[5:7] = [0, 0, 0, 0]
l
Out[109]:
[0, 1, 2, 10, 11, 0, 0, 0, 0, 'три', 4, 5, 6, 7, 8, 9]
In [110]:
l[3:] = []
l
Out[110]:
[0, 1, 2]
In [111]:
l[len(l):] = [3, 4]
l
Out[111]:
[0, 1, 2, 3, 4]

Некоторые из этих операций могут быть записаны в другой форме.

In [112]:
l = [0, 1, 2, 3, 4, 5, 6, 7]
del l[3]
l
Out[112]:
[0, 1, 2, 4, 5, 6, 7]
In [113]:
del l[3:5]
l
Out[113]:
[0, 1, 2, 6, 7]
In [114]:
l.insert(3, 0)
l
Out[114]:
[0, 1, 2, 0, 6, 7]
In [115]:
l.append(8)
l
Out[115]:
[0, 1, 2, 0, 6, 7, 8]
In [116]:
l.extend([9, 10, 11])
l
Out[116]:
[0, 1, 2, 0, 6, 7, 8, 9, 10, 11]
In [117]:
l.append([9, 10, 11])
l
Out[117]:
[0, 1, 2, 0, 6, 7, 8, 9, 10, 11, [9, 10, 11]]

Элементы списка могут быть разных типов.

In [118]:
l = [0, [1, 2, 3], 'abc']
l[1][1] = 'x'
l
Out[118]:
[0, [1, 'x', 3], 'abc']

Когда мы пишем m=l, мы присваиваем переменной m ссылку на тот же объект, на который ссылается l. Поэтому, изменив этот объект (список) через l, мы увидим эти изменения и через m — ведь список всего один.

In [119]:
l = [0, 1, 2, 3, 4, 5]
m = l
l[3] = 'три'
m
Out[119]:
[0, 1, 2, 'три', 4, 5]

Операция is проверяет, являются ли m и l одним и тем же объектом.

In [120]:
m is l
Out[120]:
True

Если мы хотим видоизменять m и l независимо, нужно присвоить переменной m не список l, а его копию. Тогда это будут два различных списка, просто в начальный момент они состоят из одних и тех же элементов. Для этого в питоне есть идиома: l[:] - это подсписок списка l от начала до конца, а подсписок всегда копируется.

In [121]:
m = l[:]

Теперь m и l - два независимых объекта, имеющих равные значения.

In [122]:
m is l
Out[122]:
False
In [123]:
m == l
Out[123]:
True

Их можно менять независимо.

In [124]:
l[3] = 0
l
Out[124]:
[0, 1, 2, 0, 4, 5]
In [125]:
m
Out[125]:
[0, 1, 2, 'три', 4, 5]

Как и для строк, сложение списков означает конкатенацию, а умножение на целое число - повторение списка несколько раз. Операция in проверяет, содержится ли элемент в списке.

In [126]:
[0, 1, 2] + [3, 4, 5]
Out[126]:
[0, 1, 2, 3, 4, 5]
In [127]:
2 * [0, 1, 2]
Out[127]:
[0, 1, 2, 0, 1, 2]
In [128]:
l = [0, 1, 2]
l += [3, 4, 5]
l
Out[128]:
[0, 1, 2, 3, 4, 5]
In [129]:
2 in l
Out[129]:
True

Простейший вид цикла в питоне — это цикл по элементам списка.

In [130]:
for x in l:
    print(x)
0
1
2
3
4
5

Можно использовать цикл while. В этом примере он выполняется, пока список l не пуст. Этот цикл гораздо менее эффективен, чем предыдущий - в нём на каждом шаге меняется список l. Он тут приведён не для того, чтобы ему подражали, а просто чтобы показать синтаксис цикла while.

In [131]:
while l:
    print(l[0])
    l = l[1:]
0
1
2
3
4
5
In [132]:
l
Out[132]:
[]

Очень часто используются циклы по диапазонам целых чисел.

In [133]:
for i in range(4):
    print(i)
0
1
2
3

Функция range(n) возвращает диапазон целых чисел от 0 до $n-1$ (всего $n$ штук) в виде специального объекта range, который можно использовать в for цикле. Можно превратить этот объект в список функцией list. Но этого делать не нужно, если только такой список не нужен для проведения каких-нибудь списковых операций. Число n может быть равно 1000000. Зачем занимать память под длинный список, если он не нужен? Для написания цикла достаточно короткого объекта range, который хранит только пределы.

In [134]:
r = range(4)
r
Out[134]:
range(0, 4)
In [135]:
list(r)
Out[135]:
[0, 1, 2, 3]

Функции range можно передать первый параметр - нижний предел.

In [136]:
for i in range(2, 4):
    print(i)
2
3
In [137]:
r = range(2, 4)
r
Out[137]:
range(2, 4)
In [138]:
list(r)
Out[138]:
[2, 3]

Можно также передать шаг третьим аргументом

In [139]:
for i in range(1, 10, 2):
    print(i)
1
3
5
7
9
In [140]:
r = range(1, 10, 2)
r
Out[140]:
range(1, 10, 2)
In [141]:
list(r)
Out[141]:
[1, 3, 5, 7, 9]

Функция list превращает строку в список символов.

In [142]:
l = list('абвгд')
l
Out[142]:
['а', 'б', 'в', 'г', 'д']

Как написать цикл, если в его теле нужно использовать и номера элементов списка, и сами эти элементы? Первая идея, которая приходит в голову по аналогии с C — это использовать range.

In [143]:
for i in range(len(l)):
    print(i, '  ', l[i])
0    а
1    б
2    в
3    г
4    д

Можно поступить наоборот — устроить цикл по элементам списка, а индексы вычислять.

In [144]:
i = 0
for x in l:
    print(i, '  ', x)
    i += 1
0    а
1    б
2    в
3    г
4    д

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

In [145]:
for i, x in enumerate(l):
    print(i, '  ', x)
0    а
1    б
2    в
3    г
4    д

Про такие пары мы поговорим в следующем параграфе.

Довольно часто удобно использовать цикл while True:, то есть пока рак на горе не свистнет, а выход (или несколько выходов) из него устраивать в нужном месте (или местах) при помощи break.

In [146]:
while True:
    print(l[-1])
    l = l[:-1]
    if l == []:
        break
д
г
в
б
а

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

Можно строить список поэлементно.

In [147]:
l = []
for i in range(10):
    l.append(i ** 2)
l
Out[147]:
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Но более компактно и элегантно такой список можно создать при помощи генератора списка (list comprehension). К тому же это эффективнее — размер списка известен заранее, и не нужно много раз увеличивать его. Опытные питон-программисты используют генераторы списков везде, где это возможно (и разумно).

In [148]:
[i ** 2 for i in range(10)]
Out[148]:
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
In [149]:
[[i, j] for i in range(3) for j in range(2)]
Out[149]:
[[0, 0], [0, 1], [1, 0], [1, 1], [2, 0], [2, 1]]

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

In [150]:
[i ** 2 for i in range(10) if i != 5]
Out[150]:
[0, 1, 4, 9, 16, 36, 49, 64, 81]

Создадим список случайных целых чисел.

In [151]:
from random import randint
In [152]:
l = [randint(0, 9) for i in range(10)]
l
Out[152]:
[7, 9, 7, 4, 7, 7, 8, 2, 6, 2]

Функция sorted возвращает отсортированную копию списка. Метод sort сортирует список на месте. Им можно передать дополнительный параметр — функцию, указывающую, как сравнивать элементы.

In [153]:
sorted(l)
Out[153]:
[2, 2, 4, 6, 7, 7, 7, 7, 8, 9]
In [154]:
l
Out[154]:
[7, 9, 7, 4, 7, 7, 8, 2, 6, 2]
In [155]:
l.sort()
l
Out[155]:
[2, 2, 4, 6, 7, 7, 7, 7, 8, 9]

Аналогично, функция reversed возвращает обращённый список. Точнее говоря, некий объект, который можно использовать в for цикле или превратить в список функцией list. Метод reverse обращает список на месте.

In [156]:
list(reversed(l))
Out[156]:
[9, 8, 7, 7, 7, 7, 6, 4, 2, 2]
In [157]:
l
Out[157]:
[2, 2, 4, 6, 7, 7, 7, 7, 8, 9]
In [158]:
l.reverse()
l
Out[158]:
[9, 8, 7, 7, 7, 7, 6, 4, 2, 2]

Метод split расщепляет строку в список подстрок. По умолчанию расщепление производится по пустым промежуткам, то есть последовательностям пробелов и символов tab и newline. Но можно передать ему дополнительный аргумент — подстроку-разделитель.

In [159]:
s = 'abc \t def \n ghi'
l = s.split()
l
Out[159]:
['abc', 'def', 'ghi']

Чтобы напечатать элементы списка через запятую или какой-нибудь другой символ (или строку), очень полезен метод join. Он создаёт строку из всех элементов списка, разделяя их строкой-разделителем. Запрограммировать это в виде цикла было бы существенно длиннее, и такую программу было бы сложнее читать.

In [160]:
s = ', '.join(l)
s
Out[160]:
'abc, def, ghi'

Также существует очень полезный метод deepcopy, который рекурсивно создает копию каждого элемента объекта и не копирует ссылки.

In [161]:
d = [1, 5]
d
Out[161]:
[1, 5]
In [162]:
import copy

d2= copy.deepcopy(d)
d2
Out[162]:
[1, 5]

6. Как объекты передаются в функции

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

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

In [163]:
def updateList(list1):
    list1 += [10]
n = [5, 6]
print(id(n))
140402773574016
In [164]:
updateList(n)
print(n)
[5, 6, 10]
In [165]:
print(id(n))
140402773574016

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

Давайте посмотрим на другой пример:

In [166]:
def updateNumber(n):
    print(id(n))
    n += 10
b = 5
print(id(b))    
9793216
In [167]:
updateNumber(b) 
9793216
In [168]:
print(b)  
5

В примере выше в функцию передается один и тот же объект, но значение переменных не меняется даже если их id одинаковые. Так работает передача по значению. Что же здесь происходит? Когда функция вызывает значение, передается только значение переменной, а не сам объект. Таким образом, переменная, ссылающаяся на объект, не изменяется, а сам объект изменяется, но только в пределах области видимости функции. Следовательно, изменение не видно «снаружи».


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