LANG·I Язык Глава 2 из 65

Имена и значения

Глава-представление: восемь фокусов с переменными. Перед каждым вы делаете ставку, потом запускаете код и узнаёте, где был спрятан трюк. А в конце — история, где та же арифметика перестаёт быть игрой: в 1991 году из-за неточно записанной десятой доли секунды противоракетный комплекс потерял цель.

С нуля 45 минут Python Программирование

Опирается на: 01 · Первая программа и первая ошибка

Что вы унесёте из главы

  • запоминать значения под именами и понимать, что имя — ярлык на объекте, а не коробка
  • отличать int, float, str и bool, превращать одно в другое и спрашивать данные у человека через input()
  • не сравнивать дроби через == и считать деньги и время в целых числах

Глава 1 закончилась шарманкой: наши программы каждый раз играют одну и ту же мелодию. Они не помнят, что посчитали строкой выше, и ничего не могут спросить у человека. Чтобы запомнить число, ему нужно дать имя. Звучит просто, но у имён в Python есть свои секреты, поэтому глава устроена как представление фокусника. Перед каждым фокусом вы делаете ставку на то, что напечатает программа, потом запускаете её, и мы показываем, где был спрятан трюк.

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

Фокус первый. x = x + 1

Математик, увидев строчку x = x + 1, скажет, что так не бывает: никакое число не равно самому себе плюс единица. Вот программа, в которой эта строчка есть. Не запускайте её сразу — сначала сделайте ставку.

Что напечатает программа?

Двенадцать. Знак = в Python — не равенство, а приказ: «посчитай то, что справа, и назови результат именем слева». Вторая строка берёт нынешнее x (пять), прибавляет единицу и вешает имя x на шестёрку. Третья делает то же с умножением: $6 \cdot 2 = 12$.

Запустите и проверьте ставку. Весь трюк в знаке равенства. В математике $x = x + 1$ — утверждение, и оно ложно. В Python x = x + 1 — команда, и порядок у неё строгий: сначала вычисляется правая часть со старым значением x, потом имя слева привязывается к результату. Такая команда называется присваиванием, а имя, которое можно привязывать к разным значениям, — переменной.

Теперь программа может помнить. Посчитаем, сколько секунд в сутках, не держа промежуточные числа в голове:

Имя может состоять из букв, цифр и знака подчёркивания, но не может начинаться с цифры. Большие и маленькие буквы различаются: Day и day — разные имена. Python разрешает даже русские имена (скорость = 5 работает), но принято писать латиницей, маленькими буквами и разделять слова подчёркиванием: seconds_per_day. Хорошее имя говорит, что за ним стоит. Через месяц seconds_per_day вы поймёте сразу, а над s придётся думать.

И не путайте имя с текстом: print(seconds_per_day) печатает число, на которое указывает имя, а print("seconds_per_day") — сами эти пятнадцать символов.

Фокус второй. Куда делась десятка

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

Что напечатает программа?

11 10. Строка b = a выполнилась один раз, когда a было десятью, и больше эти два имени ничто не связывает. Это разовая команда: «пусть b указывает туда же, куда сейчас указывает a».

Есть две картинки того, что такое переменная. В школе часто рисуют коробку: a — ящик, в нём лежит 10; b = a кладёт в ящик b копию десятки. Python устроен иначе. Число 10 живёт в памяти само по себе, как вещь на полке, — это объект. Имя a — ярлык на верёвочке, привязанный к этому объекту. Команда b = a ничего не копирует: она привязывает к той же десятке второй ярлык. А a = a + 1 создаёт новый объект 11 и перевешивает на него ярлык a. Десятка остаётся на месте, на ней висит b.

Схема ниже показывает это по шагам, и картинки в ней не нарисованы заранее: код отправляется на сервер, выполняется по одной строке, и после каждой строки записывается, какое имя к какому объекту привязано.

Слева — программа, справа — память: ярлыки-имена и объекты, на которые они указывают. Шагайте стрелками. Вкладки сверху — другие фокусы главы. Кнопка «Свой код» даёт вписать любую программу: она выполнится на сервере, и схема построится по её выполнению.

У каждого объекта есть номер — его выдаёт функция id(). Если у двух имён одинаковый номер, они указывают на один и тот же объект. Короче проверить это оператором is: a is b значит «a и b — один объект».

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

Бис: обмен

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

Что напечатает программа?

«кофе кофе». После a = b оба ярлыка висят на строке "кофе", а на строке "чай" не осталось ни одного. До объекта без ярлыков программе больше не добраться: ни одно имя на него не указывает. Когда следующая строка выполняет b = a, ярлык b перевешивается туда, куда указывает a, — на кофе, и чай потерян.

Откройте во вкладках схемы фокус «Обмен» и найдите момент, когда чай остаётся без ярлыка. Классическое лекарство — третье имя, которое подержит чай, пока ярлыки перевешивают: tmp = a, a = b, b = tmp. В Python есть способ короче. Справа от знака = можно написать несколько значений через запятую, слева — столько же имён:

Работает то же правило присваивания: сначала вычисляется правая часть, пара «объект, на который сейчас указывает b, и объект, на который указывает a», и только потом ярлыки перевешиваются. Пока вычисляется правая часть, ни один ярлык ещё не сдвинулся, и терять нечего. Вкладка «Обмен в одну строку» показывает это по шагам.

Фокус третий. Зёрна на шахматной доске

Есть старая легенда: изобретатель шахмат попросил у правителя в награду пшеницы — одно зерно на первую клетку доски, два на вторую, четыре на третью, и так на каждую клетку вдвое больше, чем на предыдущую. Всего на доске окажется $2^{64} - 1$ зёрен (почему именно столько, рассказано в главе о последовательностях «Царицы наук»). Попросим Python посчитать.

Что напечатает Python?

Все цифры: 18446744073709551615. Целые числа в Python не имеют верхнего предела, они растут, сколько хватит памяти.

Ставка «ошибка» или «перекрутится» вовсе не глупая. В большинстве языков целое число занимает в памяти фиксированное место, обычно 32 или 64 двоичных разряда, и самое большое число, которое помещается в 64 разряда, — ровно $2^{64} - 1$, награда из легенды до последнего зерна. Ещё одно зерно, и такой 64-битный счётчик обнулится, как механический одометр после 999 999 километров. Почему так и чем это кончалось на практике, расскажет глава 28. Python в таком случае берёт ещё памяти: попробуйте вписать в ячейку 2 ** 1000 или 10 ** 100.

Чтобы почувствовать это число, представьте, что кто-то считает зёрна по одному в секунду, без сна и выходных.

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

Присмотритесь к выводу: у grains нет точки, а у years есть, да ещё дробная часть. Это значения разных типов. Целое число — тип int (от integer), дробное — float (от floating point, «плавающая точка»). Узнать тип любого значения можно функцией type():

Деление / всегда даёт float, даже если делится нацело: 6 / 2 — это 3.0, а не 3. Целое частное даёт //. А у float, в отличие от int, предел есть, и не один. Число 2.0 ** 64 Python показал приблизительно: 1.8446744073709552e+19 значит $1{,}8446744073709552 \cdot 10^{19}$, и последние цифры потеряны. Дробное число хранит только около шестнадцати значащих цифр. А 2.0 ** 1024 уже вызовет OverflowError: самый большой float — около $1{,}8 \cdot 10^{308}$.

Фокус четвёртый. 0,1 + 0,2

Этот фокус знает каждый программист, потому что каждый когда-то на него попался.

Двойной знак == спрашивает «равны ли?». Что ответит Python?

False. Чему равна сумма, покажет следующая ячейка.

0.30000000000000004. А десять раз по одной десятой — 0.9999999999999999, чуть меньше единицы. Python не сломан: так считают почти все языки программирования и почти все процессоры. Разгадка — в том, как машина записывает дроби.

Мы записываем дроби в десятичной системе: $0{,}1$ — это одна десятая. Машина хранит числа в двоичной, и дробь там — сумма половинок, четвертушек, восьмушек и так далее. Одна четверть записывается в двоичной системе точно: 0.01. А одна десятая — нет: её двоичная запись бесконечная, 0.000110011001100…, с вечно повторяющимся хвостом 0011. Точно так же в десятичной системе бесконечна одна третья: $0{,}333\ldots$ Почему у одних дробей хвост кончается, а у других повторяется вечно, объясняет глава о дробях «Царицы наук». Бесконечный хвост в память не помещается, и машина его отрезает: float хранит 53 двоичные значащие цифры. Поэтому вместо 0.1 в памяти лежит ближайшее к одной десятой двоичное число. Рассмотрите его под лупой.

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

Точное значение, которое хранится вместо 0.1, можно увидеть и в Python. Модуль decimal умеет показывать float без округления:

Обычно Python печатает дробь самой короткой записью, которая превращается обратно в то же число. Поэтому print(0.1) показывает 0.1 и создаёт иллюзию точности. Но 0.1 хранится как число чуть больше одной десятой, 0.2 — тоже чуть больше, их сумма — ещё больше, и она оказывается другим двоичным числом, чем то, что хранится вместо 0.3. Разница — в семнадцатом знаке после запятой, а == сравнивает числа до последнего двоичного разряда.

Дроби не сравнивают через ==. Сравнивают с допуском — «равны с точностью до миллиардной»: abs(x - y) < 1e-9. В модуле math для этого есть готовая функция math.isclose(x, y). А деньги и время, где на счету каждая сотая и каждая доля секунды, лучше хранить в целых числах: тиыны вместо тенге, миллисекунды вместо секунд.

Ещё два фокуса с округлением

round(2.5) даёт 2, а round(3.5) — 4: Python округляет половинки к ближайшему чётному числу. Такое правило иногда называют банковским: с ним ошибки округления в длинных суммах не копятся в одну сторону. А round(2.675, 2) даёт 2.67, хотя по школьным правилам должно быть 2.68: в памяти 2.675 хранится как 2.67499999999999982236431605997495353221893310546875 — проверьте лупой выше. Устройство float — знак, порядок, мантисса — подробно разобрано в главе 28.

Фокус пятый. Дважды два — двадцать два

Что напечатает первая строка программы?

22. Двойки в кавычках — это строки, тип str. Плюс склеивает строки, звёздочка с числом повторяет строку: "2" * 3 — это "222".

Один и тот же знак делает разные вещи в зависимости от типа. 2 + 2 — сложение чисел, "2" + "2" — склейка текстов. А "2" + 2, как мы видели в главе 1, — ошибка TypeError: Python не берётся угадывать, что вы имели в виду. Функция len() считает в строке символы, и пробел — тоже символ: в «Ада Лавлейс» их одиннадцать.

Ни число, ни строка

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

Сравнение даёт логическое значение, тип bool: True (истина) или False (ложь). В следующей главе такие значения будут решать, по какой дорожке пойдёт программа. А True + True равно двум по историческим причинам: отдельный тип bool появился в Python только в версии 2.3 (2003), до того истину и ложь записывали единицей и нулём, и ради старых программ True до сих пор ведёт себя как 1.

Фокус шестой. Машина читает мысли

Классический фокус: «задумайте число, проделайте с ним несколько действий, скажите результат — и я назову задуманное». Запустите программу: она спросит вас прямо под кодом. Но сначала ставка.

Вы задумали 7 и честно ответили 76. Что будет?

Ошибка. Функция input() всегда возвращает строку, даже если вы набрали одни цифры. Получив "76", программа пытается вычесть шесть из текста, и Python отказывается.

Функция input() печатает вопрос, ждёт, пока человек наберёт ответ и нажмёт Enter, и возвращает набранное — строку. Машине неоткуда знать, что «76» вы имели в виду как число: с тем же успехом вы могли ответить «семьдесят шесть» или «не скажу». Превратить строку в число — ваша забота. Для этого есть функция int():

Теперь машина «читает мысли». Разоблачение — школьная алгебра: если задумано $n$, вы назвали $(5n + 3) \cdot 2 = 10n + 6$. Вычесть шесть, разделить на десять — и вот $n$. Попробуйте ответить что-нибудь странное: «76.5», «семьдесят шесть», пустую строку. Каждый раз Python ответит ValueError: тип подходящий, строка, но из такого значения целого не сделать.

Превращать умеют четыре функции, по одной на тип: int(), float(), str() и bool(). Ведут они себя местами не так, как подсказывает интуиция. Проверьте свою: сначала угадайте, что получится в клетке, потом нажмите на неё.

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

Три вещи из таможни стоит запомнить. int() не округляет, а отбрасывает дробную часть: int(3.99) — это 3, а int(-3.99) — минус 3. Строку с точкой int() не понимает: int("3.5") — ошибка, сначала нужен float(). И bool() считает истиной всё непустое: bool("0") — True, потому что строка из одного символа «0» не пустая. Ложь — только ноль, пустая строка и ещё несколько «пустых» значений; к ним мы вернёмся в главе 3.

Фокус седьмой. Строка, которая считает сама

Что напечатает третья строка программы — та, что без буквы f?

Фигурные скобки останутся как есть. Для Python это обычная строка, и {name} в ней — шесть символов, не больше. Подстановка работает только в строке с буквой f перед кавычкой.

Строка с буквой f перед кавычкой — f-строка (от formatted, «отформатированная»). В фигурных скобках внутри неё можно написать любое выражение: имя, арифметику, вызов функции. Python вычислит его и подставит результат в текст. Это гораздо удобнее, чем склеивать куски плюсами и следить, чтобы не пропали пробелы и все числа превратились в строки. f-строки появились в Python 3.6 (2016) по предложению Эрика Смита и с тех пор стали обычным способом собрать текст.

После двоеточия в скобках можно сказать, как показать значение:

:, разбивает число на тройки цифр, :.2f оставляет два знака после точки (округляя), :.1% показывает долю в процентах. Последняя строка — подарок для отладки: знак = в конце скобок печатает и само выражение, и его значение.

Фокус восьмой. Список с двумя именами

Вот обещанный фокус, на котором ломаются коробки. В нём есть то, чего мы ещё не проходили: список, о нём будет глава 6. Пока достаточно двух подсказок. Квадратные скобки создают список — несколько значений под одним именем. А crew.append("Грейс") дописывает в конец списка ещё одно значение.

Мы дописали Грейс в crew. Что лежит в team?

['Ада', 'Грейс']. Список один, ярлыков на нём два. Изменили список через одно имя — изменение видно через любое.

Если бы переменные были коробками, crew = team положило бы в crew копию списка, и Грейс попала бы только в копию. Ярлыки объясняют, что произошло: crew = team повесило второй ярлык на тот же список, а append меняет этот список, нового объекта не создавая. Откройте на схеме вкладку «Список»: стрелки от обоих имён идут в одну точку.

С числами и строками такого не бывает: их нельзя изменить, только заменить другими. Список меняться умеет, и без картинки с ярлыками в нём не разобраться. Эта ловушка — одна из самых частых ошибок в программах на Python. В главе 6 мы научимся делать независимые копии, а в главе 12 увидим, что так же устроены все объекты, которые вы будете создавать сами.

Дахран, 25 февраля 1991 года

Через год Счётная палата США (GAO) выпустила отчёт о расследовании. Вот что в нём написано, если перевести на язык этой главы.

Часы компьютера считали время в десятых долях секунды — целым числом тиков: 1, 2, 3… Чтобы получить секунды, программа умножала число тиков на одну десятую. А регистры компьютера, его рабочие ячейки, были 24-битными, и одна десятая хранилась в них двоичной дробью с 23 знаками после запятой: 0.00011001100110011001100. Бесконечный хвост 1100… отрезали, как на нашей лупе, только гораздо раньше, чем у float. Каждый тик часы недосчитывали примерно 0,000000095 секунды. Посчитаем, во что это вырастает.

Во второй строке int() отбрасывает дробную часть, так же как 24-битный регистр отрезал хвост. За сто часов часы отстали на ноль целых тридцать четыре сотых секунды. За это время «Скад», летящий со скоростью около 1,7 километра в секунду (в отчёте — около пяти скоростей звука), смещается больше чем на полкилометра.

Радар не ведёт цель непрерывно. Заметив её, он рассчитывает, где она окажется в следующий момент, и смотрит только в это место — в «окно» по дальности. Расчёт опирается на скорость цели и время последнего обнаружения. Если время неверное, окно оказывается не там, и чем дольше система работает без перезагрузки, тем дальше оно уходит.

Часы батареи. Ползунок — сколько часов система работает без перезагрузки. Время ошибки посчитано так же, как в ячейке выше; сдвиг окна — по таблице из отчёта GAO. Ширину окна отчёт не называет: мы оценили её по израильским данным (сдвиг в 55 метров после 8 часов — это 20 % окна).

Одно отставание часов промаха ещё не объясняет; это заметил специалист по вычислительной математике Роберт Скил, разбиравший отчёт для журнала SIAM News. Для прицела нужно не абсолютное время, а разность двух моментов: когда цель была здесь и когда там. Если оба момента посчитаны с одинаковой ошибкой, при вычитании ошибки почти взаимно уничтожаются. Но программу, написанную на ассемблере лет за двадцать до войны, несколько раз дорабатывали под быстрые баллистические ракеты, для которых система изначально не предназначалась, и в одну из доработок добавили более точный пересчёт тиков в секунды. Но вставили его не во все места, где он был нужен. Одно время получалось точным, другое — с отрезанным хвостом, и их разность больше не гасила ошибку.

Хронология из отчёта читается тяжело. 11 февраля армия получила от израильтян данные: после восьми часов непрерывной работы окно радара сдвигается на 20 %. Средство было простое: перезагружать систему каждые несколько часов. Перезагрузка занимает минуту-полторы и обнуляет часы. 21 февраля батареям разослали сообщение, что «очень долгая» работа может сдвинуть окно, но не уточнили, сколько это — «очень долго». Батарея «Альфа» к вечеру 25 февраля работала без перерыва больше ста часов. Исправленная программа прибыла в Дахран на следующий день, 26 февраля.

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

Десять тиков, сложенных по одной десятой, дают 0.9999999999999999. Те же десять тиков, посчитанные целым числом и поделённые один раз, — ровно 1.0.

Ваша очередь

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

Программа спрашивает, сколько вам полных лет, и печатает, сколько это секунд, если в каждом году 365 дней. Последней строкой вывода должно быть одно число, без пробелов внутри, например для 15 лет — 473040000.

Заготовка пока считает дни, и считает неправильно. Запустите её и ответьте «2». Почему вместо 730 она напечатала длинную строку из двоек?

input() возвращает строку, а строка, умноженная на число, повторяется. Превратите ответ в целое число, а потом домножьте дни на часы, минуты и секунды.

Заготовка повторяла строку "2" 365 раз. Обернём ввод в int() и досчитаем до секунд:

Пятнадцать лет — 473 миллиона секунд, а миллиард секунд исполняется примерно в 31 год и 8 месяцев.

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

С каждого: 275.00

Для счёта 1000, чаевых 10 % и четырёх человек: $1000 + 100 = 1100$, делим на четверых — 275.

Сумма может быть дробной — какая функция превращает «1234.5» в число? А процент и число людей — целые.

Чаевые — это bill * tip / 100. Итог делится на people.

Два знака после точки даёт f-строка: f"{x:.2f}".

Формат :.2f заодно округляет: 1000 тенге на троих без чаевых — 333.33. Куда делся последний тиын, решайте с друзьями сами.

Программа читает два слова, по одному в строке, и печатает их в обратном порядке через пробел. Но без обходных путей: поменять местами значения переменных a и b, не заводя третьей переменной, и только потом напечатать a и b — две строки с input() в начале и строка print(a, b) в конце программы должны остаться как есть.

Вспомните бис второго фокуса: что случилось, когда мы написали a = b, а потом b = a?

Справа от = можно написать несколько значений через запятую, слева — несколько имён.

Правая часть b, a вычисляется целиком до того, как перевешивается хоть один ярлык, поэтому ни одно значение не теряется. В языках без такого присваивания нужна третья переменная: tmp = a; a = b; b = tmp.

Кассовая программа получает цену в тенге с тиынами — строку вроде 19.99 — и должна напечатать цену в тиынах, целым числом: 1999. Заготовка выглядит правильной, но на некоторых ценах ошибается на тиын. Найдите такую цену и почините программу. Последняя строка вывода — одно целое число.

Запустите заготовку с ценой 19.99. Потом напечатайте float(price) * 100 без int().

19.99 хранится чуть меньше, чем 19,99, поэтому после умножения получается 1998.9999999999998. А int() не округляет — он отбрасывает дробную часть.

round() без второго аргумента округляет до ближайшего целого и возвращает int. Ошибка дробного числа здесь меньше триллионной доли тиына, и округление её убирает. Ещё надёжнее вообще не пускать цену через float: разрезать строку по точке и собрать тиыны из двух целых. Это вы сможете сделать после главы 7.

Куда дальше

Наши программы научились запоминать и спрашивать, но остались прямыми, как рельсы без единой стрелки: идут сверху вниз, строка за строкой, и каждый раз одним путём. Фокусник не может сказать «если вы задумали больше пятидесяти, умножьте на два, а если меньше — на три». Счёт в кафе не может предупредить, что людей ноль и делить не на кого. Чтобы программа выбирала, что делать, ей нужны развилки: «если — то — иначе». На развилках держится любая игра и любая проверка ввода, а внутри процессора — вообще вся логика. Об этом глава 3, в ней вы напишете свою текстовую игру.