1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7

# теория · шаг 1 из 7

Числа, имена и присваивание: чем Python отличается от твоего языка

Коротко

Урок для того, кто уже писал на другом языке и хочет знать, где Python ведёт себя иначе. Целые числа не переполняются: 2 ** 100 считается точно. Деление / всегда даёт дробное число, а целочисленное деление // округляет частное вниз, к минус бесконечности, а остаток % согласован с ним: -7 // 2 равно -4, а не -3, как в C и Java. Переменная - это имя, привязанное к значению, а не ячейка с типом. a, b = b, a меняет значения местами, потому что правая часть вычисляется целиком до присваивания. and и or ленивые и возвращают один из операндов. Блоки задаются отступом.

Если ты уже пишешь на Python и знаешь, чему равно -7 // 2, листай до «Теперь сам» в конце.

Как думаешь, что напечатает эта строка

print(7 / 2, 7 // 2, -7 // 2, -7 % 2, 2 ** 100)

Прежде чем читать дальше, ответь себе. Если ты пишешь на C, Java или Go, честный ответ звучит примерно так: «3, потом 3, потом -3, потом -1, а последнее переполнится». В Python верно только второе число. Все программы курса написаны на нём, поэтому разницу стоит выучить один раз здесь, а не ловить по одной в каждом разделе.

На что это похоже

Переменная в C похожа на коробку с надписью «целое число»: в неё кладут значение, и коробка определяет, что в ней может лежать. Переменная в Python - это наклейка с именем. Значение живёт само по себе, а наклейку можно переклеить на другое значение, в том числе другого типа. Две наклейки на одном значении - это одно значение с двумя именами, а не две копии. Для чисел и строк это почти незаметно, для списков это главный источник сюрпризов следующего урока.

Механизм: что делает каждая операция

delenie.py
print(7 / 2, 7 // 2, 7 % 2)
print(-7 // 2, -7 % 2)
print(7 // -2, 7 % -2)
print(2 ** 100)
print(0.1 + 0.2, 0.1 + 0.2 == 0.3)
↳  выводpython3 delenie.py
3.5 3 1
-4 1
-4 -1
1267650600228229401496703205376
0.30000000000000004 False

Строка за строкой:

запись что делает чем отличается
/ обычное деление, результат всегда дробный в C 7 / 2 даёт 3
// частное, округлённое ВНИЗ в C и Java округление к нулю
% остаток, знак у него как у делителя в C знак как у делимого
** степень: 2 ** 10 равно 1024 ^ - не степень: 2 ^ 10 даёт 8 (это побитовая операция)
целые любой длины, без переполнения цена операции растёт с числом цифр
дробные 64-битные числа с плавающей точкой как double в C, со всеми его неточностями

Неточность видна в последней строке вывода. Дробь хранится по степеням двойки, а 0.1 так - бесконечная дробь, как 1/3 в десятичной: её приходится округлить, и сумма двух округлённых чисел отличается от округлённого 0.3 в последнем знаке.

// и % связаны: для любых целых a и b (кроме нуля) верно a == (a // b) * b + a % b. Если частное округлено вниз, остаток обязан быть того же знака, что делитель, - иначе равенство не сойдётся. Пример: 7 / -2 равно -3.5, вниз будет -4, и тогда 7 - (-4) · (-2) = 7 - 8 = -1 - остаток отрицательный, как делитель. Что такое остаток с точки зрения математики и почему он такой удобный, разберёт глава «Математика и логика для курса».

Правило

/ всегда даёт дробное, // округляет частное к минус бесконечности, % берёт знак делителя, и a == (a // b) * b + a % b верно всегда. Целые не переполняются. Переменная - имя, а не ячейка: присваивание привязывает имя к значению, а правая часть вычисляется целиком ДО того, как что-то присвоено.

Разбор: обмен и шаг Фибоначчи в одну строку

В прошлой главе обмен двумя присваиваниями a = b; b = a испортил данные. В Python есть запись a, b = b, a, и она работает. Причина в порядке: сначала правая часть вычисляется полностью и превращается в пару значений (9, 4), и только потом имена a и b получают по элементу пары. Пока правая часть считается, ни одно имя не тронуто.

Та же запись спасает шаг чисел Фибоначчи, где каждое следующее - сумма двух предыдущих:

fib.py
a, b = 0, 1
for _ in range(6):
    a, b = b, a + b
print(a, b)
a, b = 0, 1
for _ in range(6):
    a = b
    b = a + b
print(a, b)
↳  выводpython3 fib.py
8 13
32 64

Первый цикл даёт верную пару. Во втором b = a + b видит уже новое a, то есть складывает b с самим собой, и вместо Фибоначчи получаются степени двойки. range(6) выдаёт числа от 0 до 5, то есть цикл делает шесть кругов (подробно о range - в следующем уроке). Имя _ в for - соглашение: «переменная цикла мне не нужна».

Разбор: ленивые and и or, сравнение цепочкой

logika.py
a = []
i = 0
print(i < len(a) and a[i] > 0)
print(0 or "пусто", [] or [7], 3 and 5)
x = None
print(x is None, 0 <= 5 < 10, 1 < 3 > 2)
↳  выводpython3 logika.py
False
пусто [7] 5
True True True

Первая строка вывода показывает самое полезное свойство: and не вычисляет правую часть, если левая уже ложна. len(a) - длина списка, здесь 0, так что i < len(a) ложно; a[0] упало бы с ошибкой, но до него дело не дошло. На этом держится условие сортировки вставками while i >= 0 and a[i] > klyuch - сначала проверка границы, потом обращение. Поменяй их местами, и при i = -1 Python молча возьмёт ПОСЛЕДНИЙ элемент списка (отрицательные номера считаются с конца - о них следующий урок).

Вторая строка: or и and возвращают не True/False, а один из операндов. Пустой список, ноль, пустая строка и None считаются ложью. Третья: None - значение «ничего», его сравнивают через is; 0 <= 5 < 10 - сравнение цепочкой, оно значит 0 <= 5 and 5 < 10. Поэтому 1 < 3 > 2 истинно, хотя читается странно.

Что ломается без этого

Программа, перенесённая с C, считает середину отрезка или номер ячейки через // с отрицательным числом и получает соседнюю ячейку: -7 // 2 даёт -4. Проверка 0.1 + 0.2 == 0.3 ложна, и цикл «пока сумма не станет равна единице» с шагом 0.1 не остановится никогда - дробные сравнивают с допуском, а деньги считают в целых копейках. x == None обычно работает, но для объектов со своим сравнением может соврать; x is None - нет. И i++ в Python нет вовсе: это ошибка синтаксиса, а вот ++i молча разбирается как «плюс плюс i» и не меняет ничего. Счётчик увеличивают записью i += 1.

Зачем это в работе

На собеседовании задачу часто решают на Python, а сама задача пришла из книги на C или Java. Хеш по модулю, середина отрезка, номер дня недели «три дня назад» - во всех этих местах // и % с отрицательным числом дают в Python другой ответ, чем в оригинале. Зато здесь (день - 3) % 7 всегда попадает в 0..6, и проверка «если меньше нуля, прибавь 7», без которой в C не обойтись, не нужна. Целые без переполнения означают, что произведение двух больших чисел не нужно проверять на выход за предел, - но и что оно стоит дороже с каждым разрядом.

Теперь сам

Что напечатает строка print(-1 // 3, -1 % 3, 10 // -3, 3 < 5 < 4)?

Ответ. -1 2 -4 False. -1 / 3 - это -0.33, округление вниз даёт -1. Остаток подбирается так, чтобы -1 == -1 * 3 + 2, - то есть 2, со знаком делителя. 10 / -3 - это -3.33, вниз будет -4. Цепочка 3 < 5 < 4 значит 3 < 5 and 5 < 4, а второе ложно.

Главное

В Python // округляет частное к минус бесконечности, а % согласован с ним, целые не переполняются, а переменная - это имя, привязанное к значению. Самое частое место ошибки при переносе кода из другого языка - отрицательное число в делении, и оно не падает, а молча даёт соседний ответ.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

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