2.9. Дополнительные типы данных и прочие замечания

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

2.9.1. int

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

Процессоры компьютеров умеют работать с целыми числами только в пределах определенного диапазона значений. На современных процессорах максимальное значение, с которым процессор может работать напрямую, без разных ухищрений — это \(2^{64}-1\), т.е. \(18\,446\,744\,073\,709\,551\,615\). (На самом деле с точки зрения процессора есть несколько разных типов данных, различающихся этими максимальными значениями.)

Если вам надо работать с еще бóльшими числами, то как правило приходится работать отдельно с каждой цифрой, писать сложение «в столбик» (а не напрямую просить процессор сложить два числа), и т.д. Это то, что называется «длинная арифметика»; прямо сейчас вам в этом разбираться не надо.

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

Но есть одна проблема: сложить два небольших числа — это одна инструкция процессора, а если надо использовать длинную арифметику, то надо совершать намного больше действий. Поэтому пока вы работаете с не очень большими числами (грубо говоря, не больше того же \(2^{64}\), хотя поскольку еще бывают отрицательные числа, то скорее до примерно \(\pm2^{63}\), а в 32-битных версиях питона может быть до \(2^{32}\) или примерно \(\pm2^{31}\)), то все операции будут относительно быстрыми. Но как только ваши числа станут длиннее, операции с ними в питоне будут выполняться заметно медленнее.

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

(А еще хуже на других языках типа C++ или паскаля. Там автоматического перехода к длинной арифметике не произойдет, и если результат какой-то операции выйдет за пределы, поддерживаемые соответствующим типом данных, то произойдет собственно переполнение, и результат операции получится, скорее всего, совсем не тот, который вы ожидали.)

2.9.2. bool

Логический тип данных, или bool — это тот самый тип данных, который вы пишете в разных условиях: в if, в while и т.п. Например, вы можете написать if a > 0, а можно написать и так:

x = a > 0
if x:
    ...

Тут вы сравниваете a с нулем, но не сразу используете результат сравнения в if, а сначала сохраняете результат сравнения в переменную x, и дальше уже в if считываете результат сравнения из x и в зависимости от значения x или идете в if, или нет. Вот эта переменная x как раз и будет иметь тип данных bool, или, как говорят, логический тип.

Естественно, такое сравнение может иметь только два результата: или истина (a больше нуля), или ложно (a не больше нуля). Соответственно, тип bool имеет лишь два значения, они обозначаются True (истина) и False (ложь). Их можно использовать и напрямую:

x = True

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

Естественно, тут можно использовать и and, or и not; на самом деле, and, or и not — это просто операторы, работающие с логическим типом данных — логические операторы (аналогично тому, как есть арифметические операторы +, - и т.п., и они работают с числовым типом данных). Соответственно, вы можете писать

x = (a > 0 or c == 0) and d < e
y = not x
if y and q < w:
    ...

и т.д.

Скорее всего, вы уже раньше местами применяли так называемые «переменные-флажки», в которые помечали, выполняется ли какое-нибудь условие. Например, если вам надо проверить, есть ли в данном массиве число ноль, вы могли писать примерно так:

flag = 0
for i in range(n):
    if a[i] == 0:
        flag = 1
if flag == 1:
    print("yes")
else:
    print("no")

Вот правильнее и логичнее тут использовать логический тип данных:

flag = False
for i in range(n):
    if a[i] == 0:
        flag = True
if flag:
    print("yes")
else:
    print("no")

Еще дополнительно напомню-подчеркну, что логический тип данных — это как раз то, что напрямую получается при разных проверках (сравнениях и т.п.), и что можно напрямую использовать в if. Например, в примере выше не надо писать if flag == True:, достаточно просто if flag: — переменная flag имеет логический тип, поэтому ее прекрасно можно (и нужно) использовать в if напрямую.

Аналогично, возвращаясь к самому первому примеру в этом разделе, не надо писать

if a > 0:
    x = True
else:
    x = False

Правильно писать так:

x = a > 0

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

Более частый пример про то же самое — у вас может быть функция, которая что-то проверяет; в качестве элементарного примера пусть вам нужна функция, которая проверяет, что число четное. Вы можете захотеть написать так:

def is_even(x):
    z = x % 2
    if z == 0:
        return True
    else:
        return False

Но не надо так писать! Пишите проще:

def is_even(x):
    z = x % 2
    return z == 0

(ну или, конечно, сразу return x % 2 == 0).

Ведь результат сравнения z == 0 — это сразу или True, или False, как вам и надо, поэтому лишний if писать незачем.

Примечание

На самом деле, в if вы можете использовать напрямую не только логический тип данных. Например, даже если у вас переменная a хранит целое число, вы можете написать if a: — в питоне это будет обозначать «если a не равно нулю». Но вот так делать я вам очень не советую, потому что проверка целых чисел — это на самом деле не очень естественная операция. Действительно, пусть a равно 42. Тогда запись if a: читается как «если 42». Так 42 — это истинно или ложно? Видите, что вопрос звучит в принципе странно? Вы можете спросить «если 42 больше 0» или что-то подобное, но вопрос «если 42» большого смысла не имеет.

А для логических переменных такой проблемы нет; наоборот, они используются в if напрямую и очень естественно. Если у вас x равно например True, то запись if x обозначает «если истина», что очень логично: истинное утверждение же истинно, такой проблемы как с 42 нет, наоборот, скорее тут получается тавтология.

Единственный случай, когда имеет смысл писать не-bool переменные напрямую в if — когда эти переменные имеют еще и очень понятный bool-смысл, если сравнение с нулем отвечает не просто на вопрос «равна ли переменная нулю», а имеет и какой-то более понятный и естественный смысл. Например, если у вас в переменной a хранится количество каких-то объектов, то проверку if a можно понимать как «если эти объекты вообще есть» (действительно, если a==0, то объектов нет, иначе они есть), поэтому такая проверка имеет смысл.

Пример — задача про нули, которую обсуждали выше. Вы можете написать так:

count = 0
for i in range(n):
    if a[i] == 0:
        count += 1
if count:
    print("yes")
else:
    print("no")

Тут проверка if count очень понятна: «если мы нашли хотя бы один ноль».

(В данном конкретном случае с bool-переменной лучше, потому что вам это количество само по себе не нужно. Но если вам это количество потом куда-то надо будет еще использовать, или если количество вы не сами считаете, а сразу откуда-то получаете, то напрямую проверять количество в if вполне можно.)

А вот проверка на четность — это пример, когда так писать не надо. Проверка

if z % 2:

обозначает вовсе не то, что вы можете подумать: она обозначает не «если z делится на 2», а «если z не делится на 2» (т.е. «если остаток не ноль»). Тут очень легко ошибиться и запутаться, поэтому не используйте такое неявное сравнение с нулем, когда нет однозначной и очевидной bool-трактовки.

И да, конечно, все сказанное в этом примечании относится к тому, как стоит писать программу, а не к тому, что конкретно питон вам разрешает. Питон спокойно вам разрешит писать if z % 2:, но это не значит, что так делать надо.

2.9.3. Кортежи, они же tuple

Кортеж, он же tuple — это почти то же самое, что и массив, только его нельзя никак изменять. Вы один раз записываете значение, дальше можете по нему итерироваться, копировать и т.д., но никакие операции изменения вам не будут доступны, максимум вы можете создать новый кортеж. Кортеж создается так же, как и массив, только вместо квадратных скобок используются круглые:

a = (1, 10, 100)
print(a[1])  # выведет 10

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

2.9.4. Массивы и цикл for

В теме про циклы мы обсуждали, что элементы массива можно обойти циклом for i in range(len(a)). Но если вам нужны только значения, а индексы элементов не нужны, то можно просто писать for i in a — теперь переменная i будет последовательно принимать все значения, которые хранятся в a. Например, так можно массив вывести на экран:

for i in a:
    print(i)

Аналогично можно работать с строками (перебирать все символы) и с кортежами.

2.9.5. Словари

Вы уже знаете массивы — в них элементы индексируются последовательными целыми числами, начиная с нуля. Есть очень похожая на первый взгляд конструкция — ассоциативные массивы, в питоне они называются «словари». В первом приближении это массив, в котором элементы могут адресоваться примерно чем угодно. Нам будет в первую очередь интересна возможность в качестве индексов словарей использовать произвольные числа (не обязательно подряд), а также строки.

Пишется это так:

d = {}  # так создается пустой словарь. В нем нет ни одного элемента
d[3] = 10  # теперь в словаре только один элемент, но его индекс — 3
d[17] = 137  # теперь два элемента, с индексами 3 и 17
d["abc"] = 42  # а теперь три элемента, с индексами 3, 17, и "abc"

# к элементам словаря обращаетесь как к элементам массива:
print(d[3] + d[17])
d["abc"] = d["abc"] + 1

# конечно, в квадратных скобках можно использовать любые выражения:
print(d[4 - 1])
print(d["ab" + "c"])
s = input()
d[s] = 10  # индексом будет введенная строка

# конечно, значениями элементов словаря может быть что угодно
d[10] = "qwe"  # строка
d["abc"] = [1, 2, 3]  # массив
d["qwe"] = {}  # даже другой словарь, и т.д.

# а вот так можно создать словарь с заранее заданным содержимым:
pairs = {
    # через двоеточие задаем индекс и значение
    "(": ")",
    "[": "]",
    "{": "}"
}
print(pairs["("])  # выведет )

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

Когда работают со словарями, часто вместо слова «индекс» (массива) говорят «ключ» (словаря). Например, «записать значение 10 в словарь d по ключу 3» ­— это значит d[3] = 10.

Примечание

Помимо чисел и строк, конечно, в качестве индексов можно использовать другие типы данных, но не все. А именно, в качестве индексов можно использовать только типы, значения которых невозможно изменить. В частности, массивы или другие словари в качестве индексов использовать нельзя, а вот кортежи (tuple) и bool’ы можно.

Основная операция при работе с массивом — это обход массива, обычно через for i in range(len(a)). Со словарями так просто не получится, потому что элементы словаря не занумерованы по порядку. Тут есть два способа:

for key in d:
    ....  # переменная key переберет все ключи словаря
    ....  # дальше что-то делаете с d[key]

или сразу можно перебирать пары (ключ, значение):

for key, value in d.items():
    ...

Удалить элемент из словаря можно командой del, например, del d[3]. Проверить, если ли какой-то ключ в словаре — проверкой if 3 in d.

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