EduBrick
классная работа

Символы, коды, шифры

Программирование на Python: от нуля до олимпиад

0/18решено 0 из 18до зачёта осталось 10

Чему научитесь

  • Переводить символ в число и обратно
  • Считать номер буквы в алфавите и получать букву по номеру
  • Сдвигать буквы по кольцу алфавита без выхода за его край
  • Шифровать и расшифровывать текст одной и той же формулой

Как устроено занятие

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

Дальше 15 задач лестницей: разминка, основа, со звёздочкой. Занятие засчитывается, когда решено 10 — остальные не пропадают и учитываются отдельно.

После занятия — вторая часть, ещё 15 задач на те же приёмы в новых сюжетах.

Сколько это займёт

Примерно час-полтора вместе с задачами. Сроков нет: можно закрыть вкладку и вернуться когда удобно — прогресс сохранится.

теория

У каждого символа есть номер

Компьютер не хранит буквы. Он хранит числа, а буквы — это договорённость о том, какое число что означает.

Узнать номер символа и получить символ по номеру умеют две функции:

print(ord("a"))      # 97
print(chr(97))       # a

Важны не сами числа, а то, что буквы идут подряд:

символы коды
az 97 … 122
AZ 65 … 90
09 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 в алфавите, считая от нуля?

Войдите, чтобы ответить.
теория

Сдвиг по кольцу

Шифр Цезаря: каждая буква заменяется на ту, что стоит в алфавите на kk позиций дальше. При k=3k = 3 буква 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 буква превратится в {, | и прочие знаки, которые идут в таблице следом. Это ошибка, которую видно только по выводу.

Если kk большое, остаток берут заранее: 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, дальше начинаются сдвиги. Формула кольца везде одна и та же, меняется только размер кольца: у букв двадцать шесть, у цифр десять.

Шифры в задачах ненастоящие — Цезаря вскрывают перебором двадцати шести вариантов за секунду. Они здесь не ради секретности, а потому что это самый наглядный способ научиться считать буквами.

лестница задач
развёрнутый ответ

Шифр, который портит конец алфавита

Задача: «сдвиньте каждую букву строки на kk позиций по кольцу алфавита».

Ученик написал:

s = input()
k = int(input())
result = ""

for ch in s:
    result += chr(ord(ch) + k)

print(result)

На строке abc при k=1k = 1 программа отвечает верно — bcd. А на строке xyz при том же kk выдаёт yz{. Объясните, что произошло, почему ошибка не проявилась на первом примере, и как её исправить.

Войдите, чтобы ответить.