Чему научитесь
- Переводить символ в число и обратно
- Считать номер буквы в алфавите и получать букву по номеру
- Сдвигать буквы по кольцу алфавита без выхода за его край
- Шифровать и расшифровывать текст одной и той же формулой
Как устроено занятие
Сначала разбор с примерами и короткими проверками понимания — они нужны, чтобы поймать непонятое сразу, а не через три темы.
Дальше 15 задач лестницей: разминка, основа, со звёздочкой. Занятие засчитывается, когда решено 10 — остальные не пропадают и учитываются отдельно.
После занятия — вторая часть, ещё 15 задач на те же приёмы в новых сюжетах.
Сколько это займёт
Примерно час-полтора вместе с задачами. Сроков нет: можно закрыть вкладку и вернуться когда удобно — прогресс сохранится.
У каждого символа есть номер
Компьютер не хранит буквы. Он хранит числа, а буквы — это договорённость о том, какое число что означает.
Узнать номер символа и получить символ по номеру умеют две функции:
print(ord("a")) # 97
print(chr(97)) # a
Важны не сами числа, а то, что буквы идут подряд:
| символы | коды |
|---|---|
a … z |
97 … 122 |
A … Z |
65 … 90 |
0 … 9 |
48 … 57 |
Раз подряд — значит с ними можно считать. Сравнение "a" <= ch <= "z" из занятия 13 работало именно поэтому: под ним сравнивались коды.
Запоминать 97 и 65 не нужно и даже вредно: пишите ord("a"), и код останется понятным. Числа в тексте программы через неделю ничего не скажут ни вам, ни проверяющему.
Буква как число
Из «коды идут подряд» следует главный приём занятия — перевод буквы в её номер в алфавите и обратно:
i = ord(ch) - ord("a") # номер буквы, от 0 до 25
ch = chr(ord("a") + i) # буква по номеру
Вычитание убирает «смещение» алфавита: где бы он ни начинался в таблице, после вычитания a превращается в 0, b в 1, z в 25.
Дальше с номером можно делать что угодно — складывать, сравнивать, брать остаток, — а потом вернуть обратно в букву.
Разница регистров
Строчные и заглавные буквы отличаются на одну и ту же величину:
print(ord("a") - ord("A")) # 32 — и так для любой пары
Поэтому перевод регистра — это просто арифметика: chr(ord(ch) - 32) превращает строчную в заглавную. Метод upper делает то же самое и им, конечно, удобнее пользоваться, — но полезно понимать, что за ним стоит.
Проверка: номер буквы
Какое выражение даёт номер буквы ch в алфавите, считая от нуля?
Сдвиг по кольцу
Шифр Цезаря: каждая буква заменяется на ту, что стоит в алфавите на позиций дальше. При буква a превращается в d.
Вопрос только в том, что делать в конце алфавита. Ответ: алфавит замыкается в кольцо, после z снова идёт a. А кольцо в программировании — это всегда остаток от деления:
result = ""
for ch in s:
i = ord(ch) - ord("a") # номер буквы
i = (i + k) % 26 # сдвинули по кольцу
result += chr(ord("a") + i) # вернули букву
print(result)
Три шага: в номер, сдвинуть, обратно в букву. Обычно их пишут одной строкой:
result += chr((ord(ch) - ord("a") + k) % 26 + ord("a"))
Без % 26 программа не упадёт — она выдаст мусор. Сдвинутая за z буква превратится в {, | и прочие знаки, которые идут в таблице следом. Это ошибка, которую видно только по выводу.
Если большое, остаток берут заранее: k = k % 26. Двадцать шесть полных оборотов ничего не меняют.
Расшифровка и отрицательный остаток
Чтобы расшифровать, надо сдвинуть назад — то есть на -k. И тут возникает вопрос: что даст остаток от отрицательного числа?
В Python — то, что нужно:
print(-1 % 26) # 25, а не -1
Остаток в Python всегда неотрицателен, если делитель положителен. Поэтому расшифровка — это ровно тот же код, только со знаком минус:
result += chr((ord(ch) - ord("a") - k) % 26 + ord("a"))
Отдельную функцию для расшифровки писать не нужно. Более того, расшифровать можно и шифрованием: сдвиг на 26 - k даёт тот же результат, что сдвиг на -k.
Это не мелочь языка, а полезное свойство: во многих языках остаток от отрицательного числа отрицателен, и там пришлось бы писать ((i - k) % 26 + 26) % 26. Если будете переходить на C++ — вспомните этот абзац.
Проверка: сдвиг через край
Букву y сдвинули на 4 позиции вперёд по кольцу алфавита.
Какой номер получился, если считать от нуля (a — это 0)? Введите целое число.
Что делать с остальными символами
В настоящем тексте есть пробелы, цифры и знаки препинания. Сдвигать их нельзя — иначе пробел уедет в непечатаемый символ, а текст станет нечитаемым.
Поэтому шифрование почти всегда выглядит так: проверили, буква ли, — и только тогда сдвинули.
for ch in s:
if "a" <= ch <= "z":
result += chr((ord(ch) - ord("a") + k) % 26 + ord("a"))
elif "A" <= ch <= "Z":
result += chr((ord(ch) - ord("A") + k) % 26 + ord("A"))
else:
result += ch # всё прочее — без изменений
Две ветки для букв нужны потому, что у строчных и заглавных разное начало отсчёта: одни считаются от a, другие от A. Перепутать их — самая обидная ошибка темы: программа работает, вывод выглядит правдоподобно, а буквы не те.
Обратите внимание на последнюю ветку. Символ, который не трогаем, всё равно надо добавить в результат — иначе он просто пропадёт.
Три ошибки этого занятия
Забытый % 26. Сдвиг уезжает за z в посторонние знаки. Признак: в ответе появились символы вроде {, |, }. Программа при этом не падает.
Забытый ord("a") при возврате. Получился номер вместо буквы — и chr выдал управляющий символ из начала таблицы. Признак: вывод пустой или состоит из странных знаков.
Перепутанные a и A. Для заглавной буквы вычли код строчной. Признак: буквы сдвинулись, но не туда, и в ответе смешались регистры.
Как проверять себя
- буква
zи сдвиг 1 — самая короткая проверка кольца; - сдвиг 0 и сдвиг 26 — оба должны вернуть строку без изменений;
- строка с пробелом и цифрой — не уехали ли они;
- зашифровать и сразу расшифровать — должно получиться исходное.
Практика: пятнадцать задач
Лестница прежняя: пять разминочных, семь основных, три со звёздочкой. Зачёт при десяти решённых.
Первые пять — про сами ord и chr, дальше начинаются сдвиги. Формула кольца везде одна и та же, меняется только размер кольца: у букв двадцать шесть, у цифр десять.
Шифры в задачах ненастоящие — Цезаря вскрывают перебором двадцати шести вариантов за секунду. Они здесь не ради секретности, а потому что это самый наглядный способ научиться считать буквами.
Шифр, который портит конец алфавита
Задача: «сдвиньте каждую букву строки на позиций по кольцу алфавита».
Ученик написал:
s = input()
k = int(input())
result = ""
for ch in s:
result += chr(ord(ch) + k)
print(result)
На строке abc при программа отвечает верно — bcd. А на строке xyz при том же выдаёт yz{. Объясните, что произошло, почему ошибка не проявилась на первом примере, и как её исправить.