ISBN: 5-17-014188-2

Text
                    1


Валерий Рубанцев Железопитон Python: Питон наш! Все права защищены. Никакая часть этой книги не может быть воспроизведена в любой форме без письменного разрешения правообладателей. Автор книги не несёт ответственности за возможный вред от использования информации, составляющей содержание книги и приложений. Copyright 2026 Валерий Рубанцев Лилия Рубанцева 2
От автора 3
Железопитон – это не русифицированный IronPython, которому по плечу работа в среде Microsoft Visual Studio, а обычный Питон, но который крепок – как железобетон. То есть здесь мы наблюдаем простой каламбурчик. Книга Железопитон. Python: Питон наш! завершает трилогию об основах языка программирования Питон. Первая книга называется Питонарий. Python: Первое знакомство, вторая - Питоний. Python: лучший друг. В этой книге мы изучим работу с: o o o o o o o o o o o файлами, строками, множествами, списками, словарями, двоичными/бинарными деревьями, функциями eval и exec, простыми дробями, классами и объектами, исключениями и классом Counter. Сухую теорию мы усластим большим количеством занимательных и познавательных примеров, которые пойдут всем на пользу знаний. Для первого, но тоже крепкого знакомства с языком программирования Питон этих знаний вполне достаточно, чтобы успешно решать разнообразные задачи. Книга адресуется, прежде всего: • Школьникам с 10-летнего возраста. • Родителям для совместных занятий по программированию с детьми. • Учителям информатики. • Всем начинающим программистам любого возраста, не имеющим предварительного опыта в программировании. Автор 4
Условные обозначения, принятые в книге: Дополнение Указание Замечание Задание для самостоятельного решения Исходный код: while True: f = float(input('Введите температуру F > ')) # температура в градусах Цельсия: c = round((f - 32) / 1.8, 2) print('Температура С =', c) print() Заголовок проекта: Проект … Исходные коды всех проектов находятся в папке _Projects 5
Оглавление 6
Железопитон .......................................................... 2 Python: Питон наш! .................................................... 2 От автора ............................................................... 3 Оглавление ............................................................. 6 Файлы .................................................................. 11 Словарный запас ...................................................................................................... 13 Проект Загружаем словарь ................................................................................... 15 Открытие и закрытие файла ................................................................................ 17 Чтение из файла ....................................................................................................... 25 Запись в файл ........................................................................................................... 31 Фракционирование, или Слова мелкого и крупного помола .................... 34 Дополнительные операции с файлами ........................................................... 40 Обобщение ................................................................................................................. 43 Строки .................................................................. 44 Объявление строковых переменных ................................................................. 45 Определение/создание строковых переменных .......................................... 46 Доступ к символам строки ................................................................................... 49 Сравнение строк на равенство и неравенство .................................................. 56 Срезы строк ................................................................................................................ 59 Свойства строк ........................................................................................................... 63 Проект Рекурсивные перевороты строк............................................................ 77 Проект Эйлер - Задача 4 ....................................................................................... 79 Проект Слова Фибоначчи ....................................................................................... 82 Проект АЛЬТернативный язык ............................................................................ 85 Проект Занимательная тарабарщина ................................................................. 87 Регулярные выражения ........................................................................................... 93 Проект Пора по парам findall .............................................................................. 93 Проект Фракционирование словаря match..................................................... 100 Проект Поиск по шаблону .................................................................................. 102 Проект Извлечение чисел из текста ................................................................ 103 Проект Проверка форматов ............................................................................... 105 Проект Замена слов .............................................................................................. 106 7
Проект Разбиение текста на слова ................................................................... 107 Проект Именованные группы ............................................................................ 108 Проект Жадный vs нежадный поиск ................................................................ 108 Проект Флаги........................................................................................................... 109 Проект Слова-матрёшки ....................................................................................... 111 Проект Регексные палиндромы .........................................................................113 Проект Регексные анаграммы ............................................................................114 Проект Регексные чайнворды .............................................................................116 Множества (Sets) ................................................... 119 Операции над множествами ............................................................................... 122 Отношения между множествами ....................................................................... 123 Диаграммы Венна .................................................................................................. 125 Множества в Питоне ..............................................................................................131 Проект Оперируем множествами ..................................................................... 133 Проект Разнобуквицы ........................................................................................... 136 Проект Числобус .................................................................................................... 139 Проект УЖ3 = ПИТОН .......................................................................................... 145 Проект LeetCode 136. Single Number 2 ........................................................... 150 Проект Кто читал обе книги? ..............................................................................151 Проект Сложные числа ........................................................................................ 153 Проект Шесть рукопожатий ................................................................................ 154 Проект Решето Эратосфена ................................................................................ 156 Проект Проверяем судоку .................................................................................. 159 Проект Анаграммы ................................................................................................ 160 Проект Покрытие множества............................................................................. 162 Проект Проверяем магический квадрат ......................................................... 165 Проект Разбиение на непересекающиеся множества ................................ 166 Проект Парадокс дней рождения ..................................................................... 168 Списки (Lists) ....................................................... 171 Объявление и определение списков............................................................... 172 Генераторы списков .............................................................................................. 177 Свойства списков ....................................................................................................181 Срезы списков ......................................................................................................... 195 Списки списков ....................................................................................................... 197 8
Изменяемые и неизменяемые объекты .......................................................... 199 Значимые и ссылочные типы............................................................................ 201 Проект Делится - не делится? 2 ...................................................................... 207 Проект Самые согласные.....................................................................................211 Проект TwoSum2Loops .......................................................................................... 213 Проект Каркас слов ............................................................................................... 215 Проект Гласность ................................................................................................. 223 Проект Слово в слове 2 ...................................................................................... 227 Проект Палиндромная программа ................................................................... 231 Проект Эйлер - Задача 4a .................................................................................. 233 Проект Детский лепет ........................................................................................ 236 Проект Пора по парам ......................................................................................... 241 Словари (Dictionaries) .............................................. 246 Объявление и определение/создание словарей ....................................... 247 Генераторы словарей........................................................................................... 253 Копирование словарей ........................................................................................ 254 Доступ к элементам словаря............................................................................. 257 Свойства словарей ................................................................................................. 261 Проект TwoSum ...................................................................................................... 264 Проект АЛЬТруизм ............................................................................................... 266 Занимательная логопедия ................................................................................. 268 Проект Логопеф.................................................................................................... 270 Тыблоки и яквы ..................................................................................................... 273 Проект Тыблоки и яквы...................................................................................... 274 Проект Аналитическая статистика .................................................................. 278 Проект Аналитическая статистика 2 .............................................................. 286 Проект Фракционирование словаря 2............................................................. 293 Проект Самое длинное число .......................................................................... 296 Проект LeetCode 1207. Unique Number of Occurrences ............................ 299 Проект Мемоизация. Числа Фибоначчи и пути на сетке ........................... 301 Проект Шифр Цезаря и Виженера.................................................................... 305 Задания для самостоятельного решения ...................................................... 308 Класс Counter ....................................................... 309 Проект Разнобуквицы 2 ........................................................................................ 313 9
Проект Повторяемость ........................................................................................ 315 Проект Повторяемость 2 ..................................................................................... 316 Проект LeetCode 1207. Unique Number of Occurrences 2 ......................... 318 Классы и объекты .................................................. 320 Определение класса ............................................................................................ 322 Методы класса и статические методы .......................................................... 332 Режимы доступа к атрибутам класса .............................................................. 334 Принципы ООП .................................................................................................... 344 Проект Головоломка «Переправа через реку» ........................................... 359 Проект Игра «Быки и коровы» ......................................................................... 364 Обобщение .............................................................................................................. 367 Двоичные/бинарные деревья ...................................... 369 Проект LeetCode 100. Same Tree ..................................................................... 378 Проект LeetCode 101. Symmetric Tree ............................................................. 381 Проект LeetCode 102. Binary Tree Level Order Traversal ......................... 383 Проект LeetCode 1028. Recover a Tree From Preorder Traversal ........... 386 Проект Кодирование Хаффмана ...................................................................... 392 Проект Калькулятор через дерево разбора ................................................... 396 Исключения ......................................................... 401 Функции eval и exec ................................................ 406 Проект 9 цифр ....................................................................................................... 409 Модуль fractions – Простые дроби ............................... 416 Проект Простые дроби ........................................................................................ 417 Проект Математика с простыми дробями .................................................... 423 Проект Задача 1933 года из учебника математики для 6 класса .......... 424 Вопросы и задачи на сообразительность ......................... 426 Ответы ............................................................... 432 Литература ........................................................ 433 10
Файлы 11
Храните данные в файлах! Если они у вас, конечно, есть. Программист Жорж Милославский Очень часто нужно не просто напечатать результаты работы программы, но и сохранить их на диске для последующего использования. Файл – это данные, которые хранятся на диске. Форматы файлов могут быть различными. Питон умеет работать с текстовыми и бинарными файлами. Текстовые файлы должны быть записаны в форматах txt, py, csv и rtf. Бинарные файлы могут иметь любые форматы - png, jpg, mp3, mp4 и другие. Любые действия с файлами начинаются с открытия файла, а заканчиваются его закрытием. После открытия файл его содержимое можно считывать или сохранять новую информацию в файле. Указание В видеокурсе From 0 to 1 Learn Python Programming - Easy as Pie, в части 012 A File is like a barrel файл уподобляется бочке. Подумайте над этой аналогией! 12
Поскольку теорию гораздо проще понять на примерах, то мы потренируемся на текстовых файлах, которые нам ещё пригодятся в будущих проектах Словарный запас Словарный запас за язык не тянет. Компьютерная поговорка В любых словесных играх преимущество имеет тот игрок, чей словарный запас больше. Это и понятно – есть из чего выбирать! С другой стороны, всех слов не знает никто, и мы в наших проектах будем придерживаться этой позиции. За основу мы примем Толковый словарь русского языка Ожегова и Шведовой. 13
В папке с программами вы найдёте ещё два дополнительных словаря. Попробуйте воспользоваться ими, если вам придётся разгадывать трудные, редкие слова. В словаре makarov_frc.txt буква Ё везде заменена буквой Е, имейте это в виду. В русской головоломной литературе традиционно используются имена существительные, нарицательные, в единственном числе (или во множественном, если единственное число отсутствует, - штаны, ножницы, шахматы), в именительном падеже. Специальные термины, ненормативная лексика и тому подобные слова, как правило, не употребляются. В некоторых словесных играх и головоломках вместо отдельных слов можно брать какие-либо фразы, пословицы, названия книг, фильмов и так далее. А в кроссвордах, например, допускаются и слова из названий или фраз, которые не являются существительными. Также широко употребляются имена собственные. Мы по возможности будем ограничивать свой познавательный пыл существительными из словаря Ожегова и Шведовой. 14
Проект Загружаем словарь Сначала и прежде всего нам нужно научиться загружать словарь в программу. Для хранения слов годится список. В функции main вызываем функцию load_words с названием файла со словами: # Основная программа def main(): print(' Загружаем словарь') print() load_lex() main() Вполне разумно список слов назвать words. Функция open открывает заданный файл, а метод readlines считывает все строки из файла и возвращает список строк: # -*- coding: Windows-1251 -*- # путь к файлу: path = 'OSH-W97.txt' # массив слов: global words # ВОЗВРАЩАЕТ СПИСОК СЛОВ ИЗ ФАЙЛА def load_words(): with open(path, 'r') as file: Однако после каждого слова в списке остаются символы переноса строки из файла. Избавляемся от них с помощью строкового метода rstrip: 15
return [line.strip() for line in file.readlines()] # ЗАГРУЖАЕМ СЛОВАРЬ def load_lex(): global words # список слов: words = load_words() Исключительно для контроля печатаем список и число слов в списке: # число слов: count = 0 for w in words: count += 1 # печатаем слово: print(f' {w}') print(f' Всего слов: {count}') print(f' Всего слов: {len(words)}') print() Запускаем программу и убеждаемся, что список слов составлен верно. Загружаем словарь АБАЖУР АБАЗИНЕЦ АБАЗИНКА АББАТ . . . ЯШМА ЯЩЕР ЯЩЕРИЦА ЯЩИК ЯЩУР Всего слов: 27414 Всего слов: 27414 16
Открытие и закрытие файла 17
Систематизируем файловые знания! Чтобы открыть файл, нужно знать имя файла, если он находится в том же каталоге, что и программа, или полный путь к файлу в противном случае. Встроенная функция open открывает указанный файл и возвращает поток чтения-записи - объект типа _io.TextIOWrapper. Присвоим его переменной file: # словарь: fileName = 'OSH-W97.txt' file = open(fileName) print(file) print(type(file)) fileName = 'C:/OSH-W97.txt' file = open(fileName) print(file) Путь к файлу может быть относительным (в данном случае это просто имя файла), если он указан по отношению к папке с программой или абсолютным, когда указывается полный путь к файлу, который состоит из имени диска и списка всех вложенных папок. Если файл открыт для чтения, то: - все папки, указанные в строке полного пути, должны быть созданы заранее! - файл с указанным именем должен существовать на диске, иначе возникнет ошибка. Если файл открыт для записи, но не существует, то будет создан новый файл. Если файл существует, то информация будет записана в указанный файл. 18
Вы можете сразу распечатать содержимое файла: print(*file) <_io.TextIOWrapper name='OSH-W97.txt' mode='r' encoding='cp1251'> <class '_io.TextIOWrapper'> АБАЖУР АБАЗИНЕЦ АБАЗИНКА АББАТ ... Если распечатать файл построчно, то легко заметить, что между строками записан знак переноса: for s in file: print(s) АБАЖУР АБАЗИНЕЦ АБАЗИНКА АББАТ При печати знак переноса легко убрать, присвоив параметру end значение пустой строки: for s in file: print(s, end='') Либо отрезать от строки последний символ с помощью среза: 19
for s in file: print(s[:-1]) Если файл не существует, то возникнет ошибка. Если вы не уверены, что файл существует, то предварительно проверьте это с помощью метода exists: fileName = 'C:/OSH-W97.txt' if os.path.exists('fileName'): file = open(fileName) print(*file) else: print('Файл не существует!') Открытый файл обязательно должен быть закрыт. Для этого нужно вызвать метод close открытого файла. Свойство closed возвращает True, если указанный файл закрыт: # словарь: fileName = 'OSH-W97.txt' # открываем файл: file = open(fileName) print(f'{fileName} открыт') # закрываем файл: file.close() if file.closed: print(f'{fileName} закрыт') Чаще операции открытия и закрытия файла заменяют одной инструкцией with. После ключевого слова with нужно вызвать функцию open, затем записать оператор as и имя файловой переменной. Инструкция заканчивается двоеточием. with open(fileName) as file: print(f'{fileName} открыт') print(*file) 20
if file.closed: print(f'{fileName} закрыт') Файл закроется автоматически, когда закончатся операции с файлом. OSH-W97.txt открыт АБАЖУР АБАЗИНЕЦ АБАЗИНКА АББАТ . . . ЯЩЕРИЦА ЯЩИК ЯЩУР OSH-W97.txt закрыт По умолчанию в функции open действует режим открытия файла mode='r', что означает только для чтения: # словарь: fileName = 'OSH-W97.txt' file = open(fileName) print(file) Свойство mode возвращает текущий режим доступа к файлу: fileName = 'OSH-W97.txt' file = open(fileName) print(f'Режим доступа: {file.mode}') Свойство name возвращает имя открытого файла: 21
fileName = 'OSH-W97.txt' file = open(fileName) print(f'Имя файла: {file.name}') Метод tell возвращает текущую позицию считывания из файла: .tell() -> int print(file.tell()) print(file.readline()) print(file.tell()) print(file.readline()) print(file.tell()) print(file.readline()) print(file.tell()) print(file.tell()) print(file.read(10)) print(file.tell()) Для строк позиция считывания напоминает индексацию символов в строке. Метод seek устанавливает текущую позицию считывания из файла (всегда от начала файла) и возвращает её: .seek(position : int) -> int fileName = 'OSH-W97.txt' file = open(fileName) print(file.seek(1)) for s in file: print(s[:-1]) 22
print(file.seek(1)) print(file.readline()) print(file.seek(0)) print(file.readline()) В таблице указаны все режимы открытия файла: Режим Действие mode 'r' 'rb' 'w' 'wb' 'r+' 'rb+' 'w+' 'wb+' 'a' 'a+' 'ab' 'ab+' Только для чтения. Файл текстовый. Только для чтения. Файл бинарный. Только для записи. Существующий файл будет перезаписан. Если указанный файл не существует, будет создан новый. Файл текстовый. То же самое для бинарного файла. Для чтения и записи. Файл текстовый. Для чтения и записи. Файл бинарный. Как 'r+' Как 'rb+' Добавит новые данные к существующему файлу. Если указанный файл не существует, будет создан новый. Файл текстовый. Как 'а' Добавит новые данные к существующему файлу. Если указанный файл не существует, будет создан новый. Файл бинарный. Как 'аb+' Расшифровка режимов: r w a b → → → → read/чтение write/запись append/добавление binary/бинарный, двоичный 23
В общем виде функцию open и метод close можно представить так: f = open(file_name/full_path, access_mode='r') -> stream IO .close() -> None 24
Чтение из файла 25
Для удобства и на будущее давайте напишем функцию load для открытия файла и манипуляций с ним: def main(): # словарь: fileName = 'OSH-W97.txt' # загружаем словарь: load(fileName) main() Файловый метод read считывает текстовый файл как строку: # ЗАГРУЖАЕМ СЛОВАРЬ def load(fn): with open(fn) as file: spisok = file.read() print(type(spisok)) print(spisok) print() При этом сохраняются и знаки переноса строк. Метод read имеет параметр, значение которого равно числу символов для считывания. По умолчанию будет считан весь файл. Но давайте считаем дюжину символов из файла: def load(fn): with open(fn) as file: spisok = file.read(12) print(spisok) print(len(spisok)) print() Легко посчитать, что в строке spisok содержится 11 печатных символов, а длина строки равна 12 символам. Один непечатный символ – это символ переноса строки после слова АБАЖУР. 26
Чтобы избавиться от лишних символов, мы можем, например, создать список из отдельных слов длинной строки: def load(fn): with open(fn) as file: spisok = file.read(12) print(spisok) print(len(spisok)) print() spisok = spisok.split() print(type(spisok)) print(spisok) print(len(spisok)) print() Метод readline без параметров считывает первую строку файла: def load2(fn): with open(fn) as file: spisok = file.readline() print(spisok) print(type(spisok)) print(len(spisok)) print() Символ переноса также включается в строку. Его легко убрать, применив метод rstrip к строке: spisok = spisok.rstrip() print(spisok) print(type(spisok)) print(len(spisok)) print() В методе readline можно указать максимальное число в строке для считывания. Если мы считаем только 4 символа первой строки, то получим слово 27
АБАЖ, которое многим известно по фильму Королевство кривых зеркал. Если его прочитать наоборот, то получится слово ЖАБА. with open(fn) as file: spisok = file.readline(4) print(spisok) print(type(spisok)) print(len(spisok)) print() Метод readlines возвращает список всех строк из файла: def load3(fn): with open(fn) as file: spisok = file.readlines() print(spisok) print(type(spisok)) print(len(spisok)) print() Символ переноса также включён в строки. 28
['АБАЖУР\n', 'АБАЗИНЕЦ\n', 'АБАЗИНКА\n', 'АББАТ\n', 'АББАТИСА\n', 'АББАТСТВО\n', 'АББРЕВИАТУРА\n', 'АБЕРРАЦИЯ\n', 'АБЗАЦ\n', 'АБИССИНЕЦ\n', 'АБИССИНКА\n', 'АБИТУРИЕНТ\n', 'АБИТУРИЕНТКА\n', 'АБОНЕМЕНТ\n', . . . 'ЯХОНТ\n', 'ЯХТА\n', 'ЯХТСМЕН\n', 'ЯХТСМЕНКА\n', 'ЯЧЕИСТОСТЬ\n', 'ЯЧЕЙКА\n', 'ЯЧЁНОК\n', 'ЯЧЕСТВО\n', 'ЯЧЕЯ\n', 'ЯЧМЕНЬ\n', 'ЯШМА\n', 'ЯЩЕР\n', 'ЯЩЕРИЦА\n', 'ЯЩИК\n', 'ЯЩУР\n'] <class 'list'> 27414 Избавляемся от лишних символов: spisok = [s.rstrip() for s in spisok] print(spisok) Теперь все слова без «примесей». ['АБАЖУР', 'АБАЗИНЕЦ', 'АБАЗИНКА', 'АББАТ', 'АББАТИСА', 'АББАТСТВО', 'АББРЕВИАТУРА', 'АБЕРРАЦИЯ', 'АБЗАЦ', 'АБИССИНЕЦ', 'АБИССИНКА', 'АБИТУРИЕНТ', 'АБИТУРИЕНТКА', 'АБОНЕМЕНТ', 'АБОНЕНТ', 'АБОНЕНТКА', 'АБОРДАЖ', . . . 'ЯТРЫШНИК', 'ЯТРЫШНИКОВЫЕ', 'ЯТЬ', 'ЯХОНТ', 'ЯХТА', 'ЯХТСМЕН', 'ЯХТСМЕНКА', 'ЯЧЕИСТОСТЬ', 'ЯЧЕЙКА', 'ЯЧЁНОК', 'ЯЧЕСТВО', 'ЯЧЕЯ', 'ЯЧМЕНЬ', 'ЯШМА', 'ЯЩЕР', 'ЯЩЕРИЦА', 'ЯЩИК', 'ЯЩУР'] Чтение бинарного файла выполняется так же, как текстового, но обязательно нужно указать режим чтения 'rb': # ЧИТАЕТ ИЗ БИНАРНОГО ФАЙЛА def load4(fn): with open(fn, 'rb') as file: for b in file: print(b) 29
def main(): # картинка: fileName = 'тюльпаны.jpg' # загружаем картинку: load4(fileName) main() Вы можете взять любой бинарный файл, например картинку в формате .jpg. В бинарных файлах записана и служебная информация, поэтому изменить его не так-то просто. b'\xff\xd8\xff\xe1"\xa5Exif\x00\x00II*\x00\x08\x00\x00\x00\x11\x00\x00\x0 1\x03\x00\x01\x00\x00\x00\xb0\x04\x00\x00\x01\x01\x03\x00\x01\x00\x00\x00 \x84\x03\x00\x00\x02\x01\x03\x00\x03\x00\x00\x00\xda\x00\x00\x00\x06\x01\ x03\x00\x01\x00\x00\x00\x02\x00\x00\x00\x0e\x01\x02\x00 30
\x00\x00\x00\xe0\x00\x00\x00\x0f\x01\x02\x00\x05\x00\x00\x00\x00\x01\x00\ x00\x10\x01\x02\x00\t\x00\x00\x00\x05\x01\x00\x00\x12\x01\x03\x00\x01\x00 \x00\x00\x01\x00\x00\x00\x15\x01\x03\x00\x01\x00\x00\x00\x03\x00\x00\x00\ x1a\x01\x05\x00\x01\x00\x00\x00\x0e\x01\x00\x00\x1b\x01\x05\x00\x01\x00\x 00\x00\x16\x01\x00\x00(\x01\x03\x00\x01\x00\x00\x00\x02\x00\x00\x001\x01\ x02\x00\x1c\x00\x00\x00\x1e\x01\x00\x002\x01\x02\x00\x14\x00\x00\x00:\x01 \x00\x00\x13\x02\x03\x00\x01\x00\x00\x00\x02\x00\x00\x00\xa5\xc4\x07\x00\ x1c\x00\x00\x00N\x01\x00\x00i\x87\x04\x00\x01\x00\x00\x00l\x01\x00\x00p\x 03\x00\x00\x08\x00\x08\x00\x08\x00 \x00SONY\x00DSC-F828\x00\x80\xfc\n' b"\x00\x10'\x00\x00\x80\xfc\n" b'\x00\x10\'\x00\x00Adobe Photoshop CS5 Windows\x002010:07:18 11:39:24\x00PrintIM\x000250\x00\x00\x02\x00\x02\x00\x01\x00\x00\x00\x01\x 01\x00\x00\x00\x00\x00\x00\x1f\x00\x9a\x82\x05\x00\x01\x00\x00\x00\xe6\x0 2\x00\x00\x9d\x82\x05\x00\x01\x00\ Запись в файл Метод write записывает строку в указанный файл, открытый для записи. Если такой файл не существует он будет создан. Символ переноса "\n" между строками автоматически не добавляется, поэтому в файле окажется одна длинная строка. Если нужно сохранить отдельные строки, то добавляйте символ переноса самостоятельно: def load(fn): with open(fn) as file: spisok = file.readlines() return [s.rstrip() for s in spisok] # СОХРАНЯЕМ СПИСОК В ФАЙЛЕ def save(spisok : list[str]) -> None: with open('tmp.txt', 'w') as f: for s in spisok: f.write(f"{s}\n") def main(): 31
# словарь: fileName = 'OSH-W97.txt' # загружаем словарь: spisok = load(fileName) # выбираем трёхбуквенные слова: str3 = [] for s in spisok: if len(s) == 3: str3.append(s) print(len(str3)) save(str3) main() Метод write возвращает число записанных символов в файл. Метод writelines записывает в файл заданную последовательность строк. Например, это может быть список строк. Знак переноса строки автоматически не добавляется, поэтому нам нужно позаботиться об этом самим: def save2(spisok : list[str]) -> None: with open('tmp.txt', 'w') as f: f.writelines(spisok) def main(): # словарь: fileName = 'OSH-W97.txt' # загружаем словарь: spisok = load(fileName) # выбираем трёхбуквенные слова: str3 = [] for s in spisok: if len(s) == 3: str3.append(f"{s}\n") save2(str3) Функция print имеет параметр file, которому можно передать поток вывода, и тогда она запишет данные в файл, открытый для записи. Если вы 32
хотите только выбрать и сохранить в файле некоторые слова из файла, то это легко сделать одновременно со считыванием файла: def load_save(fn_in, fn_out): with open(fn_in, 'r') as f_in: with open(fn_out, 'w') as f_out: for s in f_in: if len(s) == 3 + 1: print(s, file=f_out, end='') def main(): # словарь: fileName = 'OSH-W97.txt' load_save(fileName, 'load_save.txt') Бинарный файл таким способом записать нельзя. Методу read можно передать число байтов для считывания. Длину любого файла легко узнать в файловом менеджере. 33
Но если вам точная длина неизвестна, то просто укажите любое положительное значение. Всё равно будет считан весь файл. Данные из буфера можно записать в файл методом write того файла, который открыт для записи в бинарный файл: def load_save2(fn_in, fn_out): buffer_size = 158401 with open(fn_in, 'rb') as f_in: buffer = f_in.read(buffer_size) with open(fn_out, 'wb') as f_out: while len(buffer): f_out.write(buffer) buffer = f_in.read(buffer_size) def main(): load_save2('тюльпаны.jpg', 'load_save.jpg') main() В результате этих действий вы получите в файле load_save.jpg - точную копию исходного бинарного файла. Для осмысленного изменения бинарного файла нужно учитывать его формат, что гораздо сложнее, чем просто переписать файл без изменений. Фракционирование, или Слова мелкого и крупного помола Мели, Емеля, Твоя неделя! Крылатое выражение В нашем списке OSH-W97.txt все слова, как им и полагается, расположены в алфавитном порядке. Пока мы рассматриваем каждое слово в отдельности, 34
нам безразлично, какой в нашем списке порядок или беспорядок. Но довольно часто требуется и такой список, в котором слова располагаются по длине (по числу букв): сначала двухбуквенные, затем трёхбуквенные и так далее. Например, при игре в Виселицу, при составлении списка анаграмм, метаграмм, при разгадывании кроссвордов число букв в словах известно заранее, поэтому бывает очень полезно иметь список, в котором слова расположены не только по алфавиту, но и по длине. У нас пока имеется алфавитный список слов, который хранится в файле OSH-W97.txt. Мы можем непосредственно из него загрузить слова нужной нам длины, но тогда придётся просматривать весь файл (всякий раз для слов той или иной длины), что не очень хорошо. Будет правильнее, если мы на его основе создадим такой список, в котором слова отсортированы не только по алфавиту, но и по длине. В итоге получится вот такой полезный списочек: АД АЗ АР АС ГО ЁЖ . . . АЗУ АКР АКТ АУЛ . . . СВЕТОЧУВСТВИТЕЛЬНОСТЬ ЕСТЕСТВОИСПЫТАТЕЛЬНИЦА НЕУДОВЛЕТВОРИТЕЛЬНОСТЬ ПЕРЕОСВИДЕТЕЛЬСТВОВАНИЕ ЧЕЛОВЕКОНЕНАВИСТНИЧЕСТВО 35
Мы загружаем словарь в список, фракционируем список и распечатываем его на экране: # ЗАГРУЖАЕМ СЛОВАРЬ def load(fn): with open(fn) as file: spisok = file.readlines() # убираем символ переноса строки: return [s.rstrip('\n') for s in spisok] def main(): # словарь: #fileName = 'makarov_frc.txt' fileName = 'OSH-W97.txt' # загружаем словарь: spisok = load(fileName) # фракционируем словарь: frac(spisok) # распечатываем фракционированный словарь: for s in spisok: print(s) print(len(spisok)) print() main() Список умеет сам себя сортировать – по алфавиту, но не по «росту». Для сортировки слов по длине нужна функция, которая объясняет нашему списочному словарю, как это делается: # СОРТИРОВЩИК СЛОВ ПО ДЛИНЕ def compare(a, b): return len(a) - len(b) 36
В функции compare мы просто сравниваем слова по длине (числу букв). Если разница положительная, то первое слов длиннее второго. Если отрицательная – всё наоборот. Если нулевая – слова равны по длине. Функция frac получает список слов и применяет к нему метод sort с ключом. Здесь нам понадобится модуль functools: # Фракционирование словаря import functools Метод cmp_to_key из этого модуля получает функцию для сравнения двух значений и возвращает функцию, которая используется как ключ: # ФРАКЦИОНИРУЕМ СЛОВАРЬ def frac(spisok): spisok.sort(key=functools.cmp_to_key(compare)) В итоге применения сортировки по ключу мы получаем список, отсортированный по длине слов, но при этом сохраняется алфавитный порядок слов в каждой фракции. А в награду за наш ударный труд мы узнаём, что самое длинное слово у Ожегова – ЧЕЛОВЕКОНЕНАВИСТНИЧЕСТВО, в котором 24 буквы. АД АЗ АР АС . . . ЕСТЕСТВОИСПЫТАТЕЛЬНИЦА НЕУДОВЛЕТВОРИТЕЛЬНОСТЬ ПЕРЕОСВИДЕТЕЛЬСТВОВАНИЕ ЧЕЛОВЕКОНЕНАВИСТНИЧЕСТВО 27414 37
Самые длинные английские слова короче нашего: COUNTERREVOLUTIONARIES INTERDENOMINATIONALISM NONREPRESENTATIONALISM Справедливости ради следует отметить, что самым длинным английским словом считается FLOCCINAUCINIHILIPILIFICATION, которое состоит из 29 букв. Это событие было зафиксировано в 1992 году Книгой Рекордов Гиннесса. Впервые оно было использовано в 1741 году, и до сих пор иногда употребляется в бюрократических кругах. Как известно, немцы - большие любители соединять разные слова в одно, зачастую непомерной длины. Считается, что самое длинное немецкое слово – это RINDFLEISCHETIKETTIERUNGSÜBERWACHUNGSAUFGABENÜBERTRAGUNGSGESETZ , в котором 63 буквы. В своё время немецкие же химики навязали свои традиции словообразования для названий органических химических соединений. Если вы знаете химию (а ято её знаю), то много раз и с благодарностью вспоминали эту занятную немецкую шутку (а у них все шутки такие). Ясное дело, такие словечки не во всякий словарь засунешь, но вот ДИХЛОРДИФЕНИЛТРИХЛОРМЕТИЛМЕТАН там своё место нашёл. Формула дихлордифенилтрихлорметилметана (ДДТ) 38
К слову ЧЕЛОВЕКОНЕНАВИСТНИЧЕСТВО я отыскал ещё одно, такой же длины – ВЫСОКОПРЕВОСХОДИТЕЛЬСТВО. Если вы найдёте в Интернете ещё более длинные слова, не верьте глазам своим! Чтобы не фракционировать словарь, когда он понадобится, мы сохраняем его на диске: # сохраняем фракционированный список в файле: with open('OSH-W97_frc.txt', 'w') as f: for s in spisok: f.write(f"{s}\n") 39
Дополнительные операции с файлами 40
Функция rename из модуля os переименовывает существующий файл на диске: import os def load(fn): with open(fn) as file: spisok = file.readlines() return [s.rstrip() for s in spisok] # СОХРАНЯЕМ СПИСОК В ФАЙЛЕ def save(fn : str, spisok : list[str]) -> None: with open(fn, 'w') as f: for s in spisok: f.write(f"{s}\n") def main(): # словарь: fileName = 'OSH-W97.txt' # загружаем словарь: spisok = load(fileName) # выбираем трёхбуквенные слова: str3 = [] for s in spisok: if len(s) == 3: str3.append(s) # имя файла: fn = 'tmp.txt' # сохраняем новый список в файле: save(fn, str3) # переименовываем файл: os.rename(fn, 'tmp2.txt') main() Если файл с новым именем уже существует, возникнет ошибка. Метод remove удаляет указанный файл в корзину: 41
# имя файла: fn = 'tmp.txt' # сохраняем новый список в файле: save(fn, str3) # переименовываем файл: os.rename(fn, 'tmp2.txt') # удаляем новый файл: os.remove('tmp2.txt') Если файл не существует, возникнет ошибка. Метод mkdir создаёт новую папку в папке с проектами, если указано только имя папки, или в любом месте диска, если указан полный путь: os.mkdir('НОВАЯ ПАПКА' Если папка с новым именем уже существует, возникнет ошибка. Метод chdir устанавливает новую рабочую папку: os.chdir('НОВАЯ ПАПКА') Метод getcwd возвращает путь к текущей рабочей папке: print(os.getcwd()) Метод rmdir удаляет указанную папку: 42
os.rmdir('НОВАЯ ПАПКА/tmp') Если в папке есть файлы, возникнет ошибка. Обобщение Очень часто нужно сохранить результаты работы программы в файле на диске, а затем загружать его обратно. Для этого в языке Питон имеются специальные методы для работы с файлами – read, readline, readlines, write. 43
Строки 44
Разговоры изобретены для того, чтобы мешать людям думать. Агата Кристи Всякое слово где-нибудь да оказывается подходящим. Квинтилиан Людям привычнее общаться с помощью слов, а не чисел, поэтому для вывода сообщений и другой текстовой информации почти в каждой программе используются строки. Строка – это упорядоченная неизменяемая коллекция (последовательность) символов в кодировке Юникода UTF-8. При каждом изменении строки создаётся новая строка. Объявление строковых переменных Чтобы объявить строковую переменную, напишите имя переменной, поставьте двоеточие и укажите строковый тип str: имя_переменной : str После объявления переменной её нельзя использовать до инициализации: s : str print(type(s)) print(s) Среда разработки подсказывает, что строковая переменная не определена: 45
Как вы видите, неопределённые строковые переменные нельзя использовать в программе. Определение/создание строковых переменных Строковые переменные определяются/создаются подобно числовым переменным, но в правой части инструкции присваивания нужно записать строковый литерал. Строковые литералы – это неименованные строковые константы. Вы уже знаете, что строковый литерал – это последовательность символов в одинарных или двойных кавычках. Одинарные или двойные кавычки равноправны, так что вы можете использовать те кавычки, которые вам больше нравятся. Если внутри строкового литерала также есть слова в кавычках, то внешние и внутренние кавычки должны различаться: s1 = '' s2 = "" s3 = 'Строка' print(s3) s4 = 'Это тоже строка' print(s4) print() s5 = 'Роман "Война и мир"' print(s5) s6 = "Роман 'Война и мир'" print(s6) Новую строковую переменную можно определить/создать из уже определённой ранее строковой переменной: 46
s7 = s4 print(s4) print(s7) print() Вы также знаете, что строку можно создать из кортежа символов с помощью строкового метода join: k = tuple('Это тоже строка') print(k) s = ''.join(k) print(s) print() ('Э', 'т', 'о', ' ', 'т', 'о', 'ж', 'е', ' ', 'с', 'т', 'р', 'о', 'к', 'а') Это тоже строка Строка может содержать до 2**63 - 1 элементов-символов, в том числе и ни одного: s1 = '' s2 = "" s3 = str() Есть несколько простых способов, как определить, пустая ли заданная строка или содержит символы: s1 = '' s2 = '12' print(s1 == '') print(s2 != '') print(bool(s1)) print(bool(s2)) print(not s1) print(not s2) 47
print(len(s1) == 0) print(len(s2) > 0) Второй способ определения/создания строковых переменных – использование функции str, которая создаёт и возвращает строку, - заданную целым числом: s = str(2022) print(s) print() - или числовым выражением: s = str(2022 + 1) print(s) print() - или вещественным числом/выражением: s = str(3.1415926) print(s) print() - или логическим литералом, логической переменной, логическим выражением: s = str(2 > 1) print(s) print() 48
Доступ к символам строки Поскольку строка это упорядоченная коллекция, то можно получить элемент/символ по его индексу (индексация, как обычно начинается с нуля): 0123456789012345678901234567890 s = '1 2 3 4 5 вышел зайчик погулять' print(s[0], s[18]) Зная это, мы легко можем перебрать символы строки в цикле for: s = '1 2 3 4 5 вышел зайчик погулять' for i in range(len(s)): print(s[i]) Питон поддерживает индексацию элементов от конца к началу строки. При этом самый последний символ строки имеет индекс -1, а первый – (длина строки + 1): s = 'зайчик' for i in range(1, len(s) + 1): print(s[-i]) Извлечение из строки отдельного символа, находящегося в заданной позиции, называется операцией индексации. Строка – это неизменяемая последовательность символов, поэтому изменить значение символа в строке по его индексу невозможно. Среда разработки сообщит, что для строк не определена операция изменения значения символа: s = 'пальчик' print(s) s[0] = 'м' 49
Если вы запустите программу, то получите сообщение об ошибке. Метод removeprefix возвращает строку, из начала которой он удалил заданную подстроку: s = 'пальчик' print(s) s = s.removeprefix('п') print(s) print() Метод removesuffix возвращает строку, из конца которой он удалил заданную подстроку: s = 'СТОЛБ' print(s) s = s.removesuffix('Б') print(s) С помощью этих методов мы можем заменить одну или несколько букв в начале или в конце строки: s = 'пальчик' print(s) s = 'м' + s.removeprefix('п') print(s) s = 'ПАРОВОЗ' print(s) s = s.removesuffix('ВОЗ') + 'ХОД' print(s) 50
Метод replace возвращает строку, в которой старая подстрока заменена новой: s = 'КОРОНА' print(s) s = s.replace('Н', 'В') print(s) print() s = 'КОРОНА' print(s) s = s.replace('К', 'В') print(s) print() С этими словами связана давняя история. В одной из российских газет напечатали заметку о короновании нового императора. Там была такая фраза: Голову императора украшала ворона. Ну, мы-то знаем, что это была не ворона, а корона. Естественно, редактора газеты строго наказали и обязали перепечатать заметку. На следующий день ошибка была исправлена: Голову императора украшала корова. Слова с одинаковым числом букв (одинаковой длины), различающиеся только одной буквой (иногда несколькими), называются метаграммами. Важно отметить, что метод replace заменит все старые подстроки новыми? s = 'ОБОРОНОСПОСОБНОСТЬ' print(s) s = s.replace('О', 'А') print(s) Третий, необязательный параметр метода replace задаёт максимальное число замен: 51
s = 'ОБОРОНОСПОСОБНОСТЬ' print(s) s = s.replace('О', 'А', 4) print(s) В Питоне нет отдельного типа данных для одного символа. Например, в языке Си-шарп для этого имеется тип char. Из этого следует, что даже единственный символ в кавычках, например 's', это строка. С другой стороны, каждый символ имеет код, то есть числовое значение. Функция ord возвращает код символа односимвольной строки: # Функция ord print('Функция ord') s = 'Ы' print(f'Код символа {s} = {ord(s)}') s = 'Ё' print(f'Код символа {s} = {ord(s)}') print() Если в строке больше одного символа, то программа выдаст ошибку: s = 'ЁЖ' print(f'Код символа {s} = {ord(s)}') Функция chr возвращает односимвольную строку с заданным кодом. С помощью этой функции мы легко распечатаем заглавные буквы латинского алфавита: # Функция chr print('Функция chr') for i in range(65, 91): print(f'Символ с кодом {i} --> {chr(i)}') print() 52
Функция chr Символ с кодом Символ с кодом Символ с кодом Символ с кодом Символ с кодом Символ с кодом Символ с кодом Символ с кодом Символ с кодом Символ с кодом Символ с кодом Символ с кодом Символ с кодом 65 66 67 68 69 70 71 72 73 74 75 76 77 --> --> --> --> --> --> --> --> --> --> --> --> --> A B C D E F G H I J K L M Символ Символ Символ Символ Символ Символ Символ Символ Символ Символ Символ Символ Символ с с с с с с с с с с с с с кодом кодом кодом кодом кодом кодом кодом кодом кодом кодом кодом кодом кодом 78 79 80 81 82 83 84 85 86 87 88 89 90 --> --> --> --> --> --> --> --> --> --> --> --> --> N O P Q R S T U V W X Y Z Диапазон кодов символов = 0..0x110000-1, или 0.. 1114111. Большие числа вызывают ошибку. С помощью этих функций можно узнать, какой символ следует после заданного: print(chr(ord('A') + 1)) Среди символов Юникода есть немало забавных! Вы можете найти их, например, на сайте: https://unicode-table.com/ru/ Символы разбиты по темам, так что вы легко подберёте нужные. 53
Чтобы использовать символы по их кодам, нужна небольшая функция: def make_uchr(code: str): return chr(int(code.lstrip("U+").zfill(8), 16)) print(make_uchr('U+1F43F')) print(make_uchr('U+1F42E')) print(make_uchr('U+1F42D')) print(make_uchr('U+1F42B')) 54
🐿 🐮 🐭 🐫 Если вы хотите увидеть большие символы, то настройте шрифт той среды разработки, которой вы пользуетесь. Метод upper переводит все символы строки в верхний регистр (ПРОПИСНЫЕ буквы). Метод lower переводит все символы строки в нижний (строчные буквы): while True: s = input('Введите слово > ') print(f'Строка в верхнем регистре - {s.upper()}') print(f'Строка в нижнем регистре - {s.lower()}') print() Введите слово > Питон Строка в верхнем регистре - ПИТОН Строка в нижнем регистре - питон Естественно, эти преобразования имеют смысл только для букв, все остальные символы этими методами не изменяются. Метод isupper возвращает True, если все символы строки записаны в ВЕРХНЕМ регистре. Метод islower возвращает True, если все символы строки записаны в нижнем регистре. Небуквенные символы игнорируются. 55
Но если строка состоит только из небуквенных символов, то оба метода вернут False: while True: s = input('Введите слово > ') if s.isupper(): print('Строка в верхнем регистре') elif s.islower(): print('Строка в нижнем регистре') else: print('В строке буквы в разных регистрах!') print() Сравнение строк на равенство и неравенство Две строки можно сравнивать между собой. При этом символы с одинаковыми индексами сравниваются по значению, начиная с первого. Строки равны, если они полностью совпадают: 56
s1 = 'РЫБА' s2 = 'РЫБА' print(s1) print(s2) print(s1 == s2) print() s1 = 'РЫБА' s2 = 'рыба' print(s1) print(s2) print(s1 == s2) print() s1 = 'РЫБА' s2 = 'РЫБАК' print(s1) print(s2) print(s1 == s2) print() Строки можно сравнивать не только на равенство, но и на неравенство. Строки не равны, если они различаются хотя бы одним символом в соответствующей позиции или длиной: s1 = 'РЫБА' s2 = 'РЫБА' print(s1) print(s2) print(s1 != s2) print() s1 = 'РЫБА' s2 = 'рыба' print(s1) print(s2) print(s1 != s2) print() s1 = 'РЫБА' 57
s2 = 'РЫБАК' print(s1) print(s2) print(s1 != s2) Для строк также определены такие же операции сравнения, как и для чисел: < > <= >=. При этом сравниваются коды символов в соответствующих позициях, начиная с первой. Если они совпадают, то сравниваются следующие символы. Если в какой-либо позиции символы не совпадают, то больше то слово, символ которого в этой позиции больше. Если все символы строк совпали, но одна строка длиннее другой, то больше длинная строка. s1 = 'РЫБА' s2 = 'РЫБА' print(s1) print(s2) print(s1 >= s2) print(s1 > s2) print() s1 = 'РЫБА' s2 = 'рыба' print(s1) print(s2) print(s1 < s2) print() s1 = 'РЫБА' s2 = 'РЫБАК' print(s1) print(s2) print(s1 > s2) print() ПРОПИСНЫЕ и строчные буквы различаются. Строчные буквы больше прописных. 58
Поскольку строки это не числа, то нужно определиться, как их сравнивать. Принято считать, что из двух строк меньше та, которая занимает (или занимала бы) место в словаре выше. Поэтому • пустая строка всегда меньше строки, имеющей символы; • если строки имеют равную длину, то поочерёдно сравниваются символы двух строк. Если в какой-либо позиции символы не совпали, то меньше та строка, код символа которой меньше: "ab" "ba" "ab" меньше "ac" Если строки имеют одинаковую длину и все символы совпали, то строки равны: "a" = "a" "bc" = "bс" "abc" = "abс" "abcd" = "abсd" • если строки имеют разную длину и все символы более короткой строки совпали с соответствующими символами более длинной строки, то меньше первая строка: "abc" меньше "abсd" Срезы строк Один элемент строки можно получить по его индексу – от начала или с конца: s = 'СТРОКА' print(s) print(s[0]) print(s[-1]) Номера индексов, которые отсчитываются с конца строки, отрицательные. 59
В общем случае можно получить несколько последовательных символов строки. Эта операция называется срезом. В этом случае в квадратных скобках нужно указать через двоеточие начальный индекс, конечный индекс и шаг. Давайте получим подстроку из первых четырёх символов строки s. Нулевой индекс можно не указывать: s = 'ПОРТАЛ' print(s) print(s[0 : 4]) print(s[ : 4]) print() Обратите внимание, что конечный индекс в срезе на 1 меньше указанного. Если начальный индекс ненулевой, то его указывают обязательно: s = 'ПОРТАЛ' print(s) print(s[1 : 4]) Конечный индекс можно не указывать, если срез берётся до конца кортежа: s = 'ПОРТАЛ' print(s) print(s[3 : ]) Срез с шагом также понятен: s = 'ЛЕПЕСТОК' print(s) print(s[1 : 8 : 2]) 60
Если начальный индекс равен 0, а конечный – индекс последнего элемента кортежа, то их можно не указывать: s = 'ТЕЛЕСКОП' print(s) print(s[ : : 2]) Если шаг отрицательный, то элементы индексируются сзади: s = 'СКЕЛЕТ' print(s) print(s[ : : -1]) Если границы среза не указаны, то мы получим слово, записанное задом наперёд. s = 'СКЕЛЕТ' print(s) print(s[ : : -2]) Символы строки можно индексировать сзади, но тут нужна сноровка: s = 'СКЕЛЕТ' print(s) print(s[ -6 : -2]) s = 'СКЕЛЕТ' print(s) print(s[ -1 : -6 : -1]) 61
Для строк определена операция сложения, или конкатенации. В результате этой операции создаётся новая строка из всех строк-слагаемых: сначала первая строка, затем вторая, и так далее: s1 = 'косят' s2 = 'зайцы' s3 = 'траву' s = s1 + s2 + s3 print(s) Пробелы между строками автоматически не вставляются. Как и в математике, сложение одинаковых строк можно заменить умножением, или повторением: s1 = 'Рассказать тебе сказку про белого бычка?\n' s = s1 * 4 print(s) Мы даже можем создавать выражения из строк и операторов сложения и умножения: s1 = 'горе ' s2 = 'не ' s3 = 'беда ' s4 = 'да ' s = s1 + s2 + s3 + s4 * 3 print(s) 62
Свойства строк Функция len возвращает длину строки (число символов в строке): # функция len s = '' print(len(s)) s = '2026' print(len(s)) print() Метод count возвращает число заданных символов в строке: # метод count: s = '2022' print(s.count('0')) print(s.count('1')) print(s.count('2')) Метод index возвращает индекс первого элемента строки с заданной подстрокой. Если в метод передаётся только подстрока, то поиск подстроки ведётся по всей строке. Если указывается начальный индекс, то поиск ведётся от него и до конца строки. Если задаётся и конечный индекс, то поиск подстроки ведётся в заданном диапазоне индексов символов в строке: # метод index: s = '2022' print(s.index('0')) #print(s.index('1')) print(s.index('2')) print(s.index('2', 3)) print(s.index('2', 2, 3)) print() Если заданной подстроки в строке нет, то программа выдаст ошибку. 63
Метод find действует так же, как и метод index, но более безопасный: если заданной подстроки в строке нет, то он возвращает отрицательный индекс -1: # метод find: print('метод find:') s = '2022' print(s.find('0')) print(s.find('1')) print(s.find('2')) print(s.find('2', 3)) print(s.find('2', 2, 3)) Метод rfind действует так же, как и метод find, но возвращает индекс последнего вхождения заданной подстроки в строке или -1, если подcтроки в строке нет: # метод rfind: print('метод rfind:') s = '2022' print(s.rfind('0')) print(s.rfind('1')) print(s.rfind('2')) print(s.rfind('2', 3)) print(s.rfind('2', 2, 3)) Оператор in возвращает True, если заданная подстрока есть в строке, в противном случае – False. С помощью этого оператора мы можем безопасно искать подстроки: # оператор in: s = 'КОРТЕЖ' if 'КОРТ' in s: print(f'Подстрока в слове есть') else: print(f'Подстроки в слове нет') print() if 'ЁЖ' in s: 64
print(f'Подстрока в слове есть') else: print(f'Подстроки в слове нет') print() Метод startswith возвращает True, если строка начинается с заданной подстроки: # метод startswith: print('метод startswith') s = 'КОРТЕЖ' s_start = 'КОРТ' if s.startswith(s_start): print(f'Слово {s} начинается с подстроки {s_start}') else: print(f'Слово {s} не начинается с подстроки {s_start}') print() s_start = 'КОТ' if s.startswith(s_start): print(f'Слово {s} начинается с подстроки {s_start}') else: print(f'Слово {s} не начинается с подстроки {s_start}') print() Дополнительно можно задать начальный и конечный индексы для поиска подстроки. Так можно найти подстроку внутри строки: s_start = 'ОРТ' print(s_start ) if s.startswith(s_start, 1, -1): print(f'Слово {s} начинается с подстроки {s_start}') else: print(f'Слово {s} не начинается с подстроки {s_start}') print() 65
Вместо строки для поиска можно передать в метод startswith кортеж из строк. Тогда этот метод будет искать строки, которые начинаются с любой строки в кортеже: s_start = ('КК', 'ОРТ', 'КОРТ') print(s_start ) if s.startswith(s_start): print(f'Слово {s} начинается с подстроки {s_start}') else: print(f'Слово {s} не начинается с подстроки {s_start}') print() s_start = ('КК', 'ОРТ', 'КОРТТ') print(s_start ) if s.startswith(s_start): print(f'Слово {s} начинается с подстроки {s_start}') else: print(f'Слово {s} не начинается с подстроки {s_start}') print() Метод endswith действует, как метод startswith, но для конца строки: # метод endswith: print('метод endswith') s = 'КОНСТРУКТОР' print(s) s_end = ('ОР', 'ТОР', 'ОРТ') print(s_end) if s.endswith(s_end): print(f'Слово {s} заканчивается подстрокой {s_end}') else: print(f'Слово {s} не заканчивается подстрокой {s_end}') print() 66
Оператор цикла for-in для строк Для строк, как и для кортежей, существует цикл for-in Этот цикл последовательно перебирает все символы строки от первого до последнего, то есть переменная цикла последовательно принимает значения символов строки: string = 'ТЕЛЕСКОП' for s in string: print(s, end = '_') print() В цикле for-in, как и в других циклах, можно прерывать итерации с помощью операторов break и continue. Простые операции над строками Строка – это неизменяемая коллекция, поэтому её невозможно отсортировать. При сортировке создаётся новая строка. Вы можете заменить этой строкой прежнюю строку или создать новую: s = 'АБРАКАДАБРА' print(s) ss = sorted(s) s = sorted(s) print(type(ss)) print(ss) print(s) Функция sorted возвращает список символов. 67
Строковый метод join возвращает строку, составленную из любого числа строк. Так мы можем легко получить строку из списка отсортированных символов: s = 'АБРАКАДАБРА' print(s) s = ''.join(sorted(s)) print(s) Поскольку метод join строковый, то сначала нужно записать строку, которая будет вставляться между отдельными строками, а затем вызвать метод join: s = 'АБРАКАДАБРА' print(s) s = '<>'.join(sorted(s)) print(s) Добавим перенос строк к методу join и получим простой способ записывать строчки колонкой: s = ''.join(f'Число = {n}\n' for n in range(10)) print(s) Число Число Число Число Число Число Число Число Число Число = = = = = = = = = = 0 1 2 3 4 5 6 7 8 9 68
Метод rstrip удаляет конечные пробелы из строки (справа): s = 'КОСМОС ' print(s) s = s.rstrip() print(s) Пробелы при печати не видны, поэтому заменим их знаками подчёркивания. Но тогда в метод rstrip нужно передать символ для удаления: s = 'КОСМОС___' print(s) s = s.rstrip('_') print(s) Можно удалить и любое число конечных символов строки: s = 'ПАРАД' print(s) s = s.rstrip('Д') print(s) print() s = 'ПАРАД' print(s) s = s.rstrip('АД') print(s) Метод lstrip удаляет ведущие пробелы или заданные символы из строки (слева): s = '___КОСМОС' print(s) s = s.lstrip('_') print(s) 69
print() s = 'БАРАК' print(s) s = s.lstrip('Б') print(s) print() s = 'БАРАК' print(s) s = s.lstrip('БА') print(s) print() Метод strip удаляет ведущие и конечные пробелы или заданные символы из строки (слева и справа): s = '___КОСМОС***' print(s) s = s.strip('_*') print(s) print() s = 'КУЛАК' print(s) s = s.strip('К') print(s) print() s = 'КАРЕТА' print(s) s = s.strip('КАТА') print(s) print() Метод split разбивает строку на подстроки по пробелам, если методу передать пробел как разделитель или вообще ничего не передавать. Метод возвращает список строк: 70
s1 = '1 2 3 4 5 вышел зайчик погулять' s = s1.split() print(s) Второй, необязательный параметр метода устанавливает максимальное число подстрок, остальная часть строки на подстроки не разбивается. По умолчанию значение этого параметра равно -1, то есть вся строка разбивается на подстроки: s1 = '1 2 3 4 5 вышел зайчик погулять' s = s1.split(' ', 5) print(s) Разделителем может быть любая строка/символ: s1 =' 1, 2, 3, 4, 5, вышел, зайчик, погулять' s = s1.split(',') print(s) Известная вам функция str конвертирует числовые и логические литералы или переменные в строки. Функции int, float, bool выполняют обратные операции: print(str(12)) print(str(12.11)) print(str(True)) print() print(int('12')) print(float('12.11')) print(bool('True')) print(bool('2022')) Функции tuple, list и set конвертируют строку в кортеж, список и множество, соответственно: 71
s = 'абракадабра' print(tuple(s)) print(list(s)) print(set(s)) К наиболее часто применяемым операциям над строками можно отнести: • взятие индекса (индексация) []. Эту операцию мы уже рассмотрели. Она возвращает символ, находящийся в заданной позиции строки. • операция присваивания = вам также известна. После её выполнения строковая переменная получает значение строкового выражения, находящегося в правой части оператора присваивания. • операции сравнения == и !=. Они позволяют установить, равны ли две строки или нет. • операция объединения (сложения, конкатенации) строк: + - оператор сцепления строк. В результате получается одна длинная строка, состоящая из более коротких строк. Пробелы между отдельными строками автоматически не добавляются! В качестве операндов можно использовать строковые и символьные литералы, символьные и строковые переменные и именованные константы. Другие строковые методы Метод isnumeric возвращает True, если все символы в строке обозначают числа, и False, если хотя бы один символ не числовой: s1 = '2026' print(s1.isnumeric()) 72
s2 = '2026 год' print(s2.isnumeric()) Поэтому символы Юникода, обозначающие дроби, римские и древнегреческие числа, вернут True: s5 = '\u2154' print(s5) print(s5.isnumeric()) s5 = '\u2155' print(s5) print(s5.isnumeric()) s5 = '\u2167' print(s5) print(s5.isnumeric()) s5 = '\u2188' print(s5) print(s5.isnumeric()) Метод isdigit более строгий – он возвращает True, если все символы - арабские цифры, и False, если хотя бы один символ не цифра: s1 = '2026' print(s1.isdigit()) s2 = '2026 год' print(s2.isdigit()) s3 = '12.1' print(s3.isdigit()) s4 = '\u2154' print(s4.isdigit()) Метод isdecimal возвращает True, если все символы – десятичные цифры, и False, если хотя бы один символ не цифра: s1 = '2026' print(s1.isdecimal()) s2 = '2026 год' 73
print(s2.isdecimal()) s3 = '12.1' print(s3.isdecimal()) s4 = '1e4' print(s4.isdecimal()) Метод isalnum возвращает True, если все символы в строке – буквы или цифры, и False, если хотя бы один символ не цифра и не буква: s1 = '2026' print(s1.isalnum()) s2 = '2026 год' print(s2.isalnum()) s3 = '2026год' print(s3.isalnum()) Пробел, знаки препинания и прочие символы не считаются буквами и цифрами. Метод isalpha возвращает True, если все символы в строке – буквы, и False, если хотя бы один символ не буква: s1 = '2026' print(s1.isalpha()) s2 = '2026 год' print(s2.isalpha()) s3 = 'год' print(s3.isalpha()) Пробел, знаки препинания, цифры и прочие символы не считаются буквами. Метод isascii возвращает True, если все символы в строке – это символы ASCII, и False, если хотя бы один символ не принадлежит ASCII: s1 = 'aBc2026' print(s1.isascii()) 74
s2 = 'год' print(s2.isascii()) s3 = '!"#$%&' print(s3.isascii()) Пустая строка также вернёт True. Символы ASCII имеют коды в диапазоне [0x0000, 0x007F), поэтому мы легко распечатаем их: for i in range(0x0000, 0x007F): print(chr(i), end=' ') ! " # $ % & ' ( ) * + , - . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ? @ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _ ` a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~ Первый печатный символ – это пробел с кодом 32 = 0х20, остальные символы вы можете посмотреть в таблице: 75
Метод isidentifier возвращает True, если строка – допустимый идентификатор, и False в противном случае: s1 = '2026' print(s1.isidentifier()) s2 = '_2026' print(s2.isidentifier()) s3 = 'get_sum' print(s3.isidentifier()) s4 = 'решаем_задачу' print(s4.isidentifier()) Метод ljust выравнивает строку слева заданными символами до указанной длины, а метод rjust – справа: s = 'CTPOKA' print(s.ljust(12, '.')) print(s.rjust(12, '.')) Символ '\0' называется нуль-символом. Его код равен нулю. Обратная косая черта используется для обозначения непечатных символов, поскольку их нельзя ввести с клавиатуры. Кроме нулевого символа, при форматировании строк часто применяют символы перевода на новую строку '\n' и возврата каретки '\r'. 76
Несмотря на то что в записи участвуют 2 знака, каждая из них обозначает 1 символ! Например, строка "\n\r" при печати в консольном окне переводит вывод информации в начало следующей строки. Проект Рекурсивные перевороты строк Пишем рекурсивную функцию reverse_str: # ПЕРЕВОРАЧИВАЕМ СТРОКУ def reverse_str(s : str, id : int = 0): # основной случай: if id >= len(s): return '' # рекурсия: else: res = reverse_str(s, id + 1) res += s[id] return res s = 'КАТОК' print(s) revs = reverse_str(s) print(revs) print() Если перевёрнутая строка совпадает с исходной, то мы нашли слово-палиндром, которое читается одинаково слева направо и справа налево: s = 'ШАЛАШ' print(s) revs = reverse_str(s) print(revs) print() 77
Функция reverse_str возвращает строку, записанную задом наперёд. Она получает последовательность символов (строку) и номер (индекс) очередного элемента в последовательности (символа). Сначала это первый элемент/символ (с нулевым индексом). Так как этот элемент/символ принадлежит последовательности, то основной случай не наступает, и функция рекурсивно вызывает себя со следующим индексом. И так до тех пор, пока индекс не выйдет за границы последовательности/строки. Тогда наступит основной случай, и рекурсивные вызовы прекратятся. Все функции начнут возвращать свои значения, то есть символы строки, начиная с пустого (его вернёт вызов с основным случаем). К нему добавится последний, затем предпоследний. И так далее до самого первого. Так как символы добавляются к пустой строке от конечного к начальному, то в строке res мы получим обращённую строку. Если вы хотите понаблюдать за этим процессом, добавьте функцию print в функцию reverse_str: # ПЕРЕВОРАЧИВАЕМ СТРОКУ def reverse_str(s : str, id : int = 0): # основной случай: if id >= len(s): return '' # рекурсия: else: res = reverse_str(s, id + 1) res += s[id] print(res) return res Эти же операции можно выполнить и без рекурсивных премудростей: # ПЕРЕВОРАЧИВАЕМ СТРОКУ НЕРЕКУРСИВНО def reverse_str2(s : str) -> str: res = '' for i in range(len(s) - 1, - 1, -1): res += s[i] return res 78
s = 'КАТОК' print(s) revs = reverse_str2(s) print(revs) print() s = 'ШАЛАШ' print(s) revs = reverse_str2(s) print(revs) print() Но и эту функцию можно заменить простым срезом, который самостоятельно вывернет строку: return s[::-1] Если слово одинаково читается в прямом и в обратном направлении, то оно называется палиндромом. Проект Эйлер - Задача 4 Палиндромы бывают не только словесные, но и числовые. Очень часто задачи с палиндромами предлагаются на различных олимпиадах по программированию. Проект Эйлер также не обошёл своим вниманием числовые палиндромы: Задача 4 (Problem 4) была опубликована 16 ноября 2001 года. Задача любопытная, но не очень сложная. Условие задачи на английском языке: A palindromic number reads the same both ways. The largest palindrome made from the product of two 2-digit numbers is 9009 = 91 99. 79
Find the largest palindrome made from the product of two 3-digit numbers. То есть нам предлагают: найти наибольший палиндром среди произведений двух трёхзначных чисел. Обозначим первое трёхзначное число буквой i, а второе - буквой j. Чтобы не находить каждый палиндром дважды, будем считать, что j >= i. Тогда значениями переменной i могут быть числа от 100 (наименьшее трёхзначное число) до 999 (наибольшее трёхзначное число). Переменная j может иметь значения от i до 999. Во вложенных циклах мы находим произведения всех трёхзначных чисел, выделяем из них палиндромы и запоминаем самый большой из них. Функция main вызывает функцию solve, которая и решает задачу: def main(): print() print("Решаем задачу 4 из Проекта Эйлер") print() solve() main() Самое сложное действие в функции solve – это определение «палиндромности» очередного произведения: # РЕШАЕМ ЗАДАЧУ def solve(): nVar = 0 max = 0 maxi = maxj = 0 for i in range(100, 999 + 1): for j in range(i, 999 + 1): mul = i * j smul = str(mul) 80
if isPalindrome(smul): print(f'{i} * {j} = {smul}') if mul > max: max = mul maxi = i maxj = j nVar += 1 print(f'Всего палиндромов = {nVar}') print() print(f'Наибольший палиндром = {maxi} * {maxj} = {max}') print() Наиболее простой способ для этого – преобразовать число в строку функцией str, а затем проверить эту строку, что мы уже умеем делать: def isPalindrome(word : str) -> bool: return word == word[::-1] Вот и вся задача! Решаем задачу 4 из Проекта Эйлер 101 * 101 = 10201 101 * 111 = 11211 101 * 121 = 12221 101 * 131 = 13231 . . . 916 * 968 = 886688 924 * 932 = 861168 924 * 962 = 888888 Всего палиндромов = 1239 Наибольший палиндром = 913 * 993 = 906609 Цифровой вариант решения см. в Списках. 81
Проект Слова Фибоначчи Если числа Фибоначчи известны всем и каждому (вам-то, по крайней мере, они хорошо известны!), то о словах Фибоначчи не слышал и сам Фибоначчи. Вы можете прочитать о них в книге Jewels of Stringology (Maxime Crochemore, Wojciech Rytter), на страницах 13-14. А мы начнём нашу программу с функции main, в которой вводим желаемое число слов Фибоначчи, которые хотим увидеть (или даже прочитать!): def main(): # бесконечный цикл ввода данных # пока пользователь не закроет программу # или не введет 0: while True: num = int(input("Сколько слов Фибоначчи напечатать? # если число равно 0, # то программу закрываем: > ")) 82
if num == 0: break # печатаем слова Фибоначчи: FibonacciWords(num) print() main() «Вычисление» слов Фибоначчи мало чем отличается от аналогичного процесса для чисел. Мы полагаем, что: • • • нулевое слово Фибоначчи – пустая строка первое слово Фибоначчи – одна буква b второе слово Фибоначчи – одна буква а # Fibonacci Words # ИЩЕМ СЛОВА ФИБОНАЧЧИ def FibonacciWords(num : int): f = '' f1 = "b" f2 = "a" print(f'0-е слово Фибоначчи = {f}') print(f'1-е слово Фибоначчи = {f1}') Все последующие слова составляются из двух предыдущих (конкатенация строк): for i in range(2, num + 1): print(f'{i}-е слово Фибоначчи = {f2}') f = f2 f2 = f2 + f1 f1 = f 83
Как и числа Фибоначчи, одноимённые слова очень быстро увеличиваются в длине, поэтому мы посмотрим только на первую десятку слов Фибоначчи. Сколько слов Фибоначчи напечатать? > 10 0-е слово Фибоначчи = 1-е слово Фибоначчи = b 2-е слово Фибоначчи = a 3-е слово Фибоначчи = ab 4-е слово Фибоначчи = aba 5-е слово Фибоначчи = abaab 6-е слово Фибоначчи = abaababa 7-е слово Фибоначчи = abaababaabaab 8-е слово Фибоначчи = abaababaabaababaababa 9-е слово Фибоначчи = abaababaabaababaababaabaababaabaab 10-е слово Фибоначчи = abaababaabaababaababaabaababaabaababaababaabaababaababa Слова Фибоначчи, начиная с десятого, уже можно использовать в качестве текстов для современных песен… 84
Проект АЛЬТернативный язык Писатель-юморист Семён Альтов знаменит не только своими смешными рассказами, но и особенностями их чтения: он глухие звуки произносит как звонкие, что очень смешно. Например, канарейка у него стала ганарейгой, а парикмахер – баригмакером. Сейчас мы напишем программу, которая умеет «читать» поальтовски. Самое сложное в ней – подобрать парные согласные. Я предлагаю поместить их в две строки: # Альтов # ПРОГРАММА ДЛЯ ПЕРЕВОДА ТЕКСТА # НА АЛЬТОВСКИЙ ЯЗЫК lit1 = "ПФКШСТХ" lit2 = "БВГЖЗДК" В строку lit1 мы собрали глухие согласные (вы можете подредактировать этот набор, если не согласны с моим выбором), а в строку lit2 – звонкие. В функции main мы вводим «нормальные» слова, а функция Altov переводит их на альтовский язык: def main(): print("Альтов") # бесконечный цикл ввода данных: while True: text = input("Ваш текст > ") # переводим буквы в верхний регистр: text = text.upper() print(text) # альтовский текст: 85
print("Альтовский текст > altov = Altov(text) # печатаем перевод: print(altov) print() ", end='') main() Сделать это очень просто. Мы просматриваем строку text буква за буквой и, если найдём в ней глухую букву, то заменяем её звонкой. Остальные буквы оставляем без изменений: # ПЕРЕВОДИМ ТЕКСТ def Altov(text : str) -> str: # альтовский текст: res = '' # кодируем - заменяем буквы парными: for ch in text: # глухая буква: id = lit1.find(ch) if id > -1: # заменяем звонкой: res += lit2[id] else: res += ch return res Поскольку строки lit1 и lit2 мы составили так, что парные согласные имеют одинаковые индексы, то перевод слов на альтовский язык получился очень простым. Запускаем программу и вводим слова, в которых много глухих согласных, а наш переводчик тут же выдаёт эти слова по-альтовски. Альтов Ваш текст > КАНАРЕЙКА Альтовский текст > канарейка ГАНАРЕЙГА 86
Ваш текст > ПАРИКМАХЕР Альтовский текст > парикмахер Ваш текст > ПОЛКОВНИК Альтовский текст > полковник Ваш текст > КАТАСТРОФА Альтовский текст > катастрофа БАРИГМАКЕР БОЛГОВНИГ ГАДАЗДРОВА Змежно болучилозь! Проект Занимательная тарабарщина Учитель стал показывать Филипку буквы, а Филипок их уже знал и немножко читать умел. - Ну-ка, сложи свое имя. Филипок сказал: - Хве-и - хви, ле-и - ли, пе-ок - пок. Лев Толстой, Филипок Английские школьники придумали настоящий тарабарский язык, с которым мы сейчас и познакомимся. Но нам, конечно, придётся учесть особенности нашего родного языка. Этот тарабарский язык называется обратным сленгом, или pig Latin, и отличается он от нормального языка только тем, что все слова коверкаются по определенным правилам. Правила тарабарского языка: 1. Все согласные каждого слова до первой гласной переносятся в его конец. 87
2. Если слово начинается с гласной, то в конец переносятся все буквы до второй гласной. Если в слове только одна гласная, то оно не изменяется. 3. Если слово начинается с нескольких гласных, то все они считаются за одну букву. 4. Если после переноса букв последняя буква - согласная, то к слову добавляется АЙ, если гласная – Й. Как видите, правил очень немного, и они совсем простые, но, чтобы свободно говорить на тарабарском наречии русского языка, вам потребуется тренировка. А мы сначала рассмотрим несколько примеров тарабарских преобразований русских слов, а затем напишем программу-переводчик с русского языка на тарабарский. По ходу дела вы хорошенько усвоите все нехитрые правила тарабарской грамматики. Правило 1: ПАРУС → АРУСПАЙ; СЛОВО → ОВОСЛАЙ. Правило 2: ИКС → ИКСАЙ; ОТПАД → АДОТПАЙ. Правило 3: ОАЗИС → ИСОАЗАЙ; АОРТА → ААОРТАЙ. Дополнительно в программе следует учесть «особые» слова, состоящие из одной буквы: В → ВАЙ, И → ИЙ. В функции main мы набираем любой текст, а затем отправляемся в функцию createTarabar – и получаем на экране полную тарабарщину, в которой трудно узнать нашу родную речь! def main(): print() print("Тарабарская грамота") print() # бесконечный цикл ввода данных while True: # исходный текст: text = input("Ваш текст > ") # переводим буквы в верхний регистр: text = text.upper() # тарабарский текст: tarabar = createTarabar(text) 88
# печатаем перевод: print(tarabar) print() main() Белеет парус одинокий ЕЛЕЕТБАЙ АРУСПАЙ ИНОКИЙОДАЙ В тумане моря голубом!.. ВАЙ УМАНЕТАЙ ОРЯМАЙ ОЛУБОМГАЙ!.. Что ищет он в стране далёкой? ОЧТАЙ ЕТИЩАЙ ОНАЙ ВАЙ АНЕСТРАЙ АЛЁКОЙДАЙ? Что кинул он в краю родном?.. ОЧТАЙ ИНУЛКАЙ ОНАЙ ВАЙ АЮКРАЙ ОДНОМРАЙ?.. Знаменитый Парус на тарабарском языке! Как видите, шифровка получается достаточно забавной для того, чтобы немедля приступить к написанию кода. Из грамматических правил тарабарского языка следует, что нам предстоит устанавливать принадлежность каждого символа текста к русским буквам 89
вообще и к гласным и согласным – в частности. Поэтому мы заведем три строковых константы для всех этих букв: # ПРОГРАММА ДЛЯ ПЕРЕВОДА ТЕКСТА # НА ТАРАБАРСКИЙ ЯЗЫК rus = "АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ" glas = "АЕЁИОУЫЭЮЯ" soglas = "БВГДЖЗЙКЛМНПРСТФХЦЧШЩЪЬ" Обратите внимание: мы используем только ПРОПИСНЫЕ буквы, хотя в тексте обязательно встретятся и строчные. Подумайте, как перевести текст на тарабарский язык, не искажая исходный регистр букв! Поскольку правила тарабарского языка распространяются только на отдельные слова, то мы должны из входного текста, который может включать не только слова русского языка, но и знаки препинания, цифры и другие символы, последовательно выделять слова, переводить их на тарабарский язык и добавлять к переводу. Сделать это совсем нетрудно, если учесть, что слова содержат только буквы из строковой константы rus. Как только нам встретится другой символ, мы заканчиваем составление слова и передаём его функции convert для перевода. Все остальные символы текста мы добавляем к строке tarabar без изменений: # ПЕРЕВОДИМ ТЕКСТ def createTarabar(text: str): # тарабарский текст: tarabar = "" # кодируем --> res = "" for ch in text: # если это не буква --> # слово закончилось: if ch not in rus: 90
# если мы уже составили слово, # то добавляем его к переводу: if len(res) > 0: tarabar += convert(res) # добавляем символ: tarabar += ch # начинаем новое слово: res = "" else: # буква --> # добавляем ее к слову: res += ch return tarabar + convert(res) Каждое русское слово исходного текста мы, согласно правилам, переводим на тарабарский язык: # ПЕРЕВОДИМ СЛОВО НА ТАРАБАРСКИЙ ЯЗЫК def convert(s : str) -> str: if len(s) == 0: return "" # считаем начальные согласные: i = 0 s1 = "" s2 = "" while i < len(s) and isSoglas(s[i]): s1 += s[i] i += 1 # если слово начинается с согласных, # переносим их в конец слова: if i > 0: s2 = s[i : ] #print(s2 + s1 + "АЙ") #return s2 + s1 + "АЙ" # считаем начальные гласные: i = 0 s1 = "" while i < len(s) and isGlas(s[i]): s1 += s[i] i += 1 91
# есть ли в слове другие гласные: flg = False n = 0 for j in range(i, len(s)): if isGlas(s[j]): flg = True n = j break # если # то и if not if второй гласной нет, переносов нет: flg: isGlas(s[len(s) - 1]): return s + "Й" else: return s + "АЙ" # вторая s1 = s[0 s2 = s[n s = s2 + return s гласная стоит на j-том месте в слове: : n] : ] s1 + "АЙ" Эта функция просто реализует алгоритм перевода по правилам тарабарского языка. Для удобства обе проверки - принадлежности заданной буквы к гласным или к согласным - оформлены в виде отдельных функций: # ПРОВЕРЯЕМ: ГЛАСНАЯ? def isGlas(ch : str) -> bool: return ch in glas # ПРОВЕРЯЕМ: СОГЛАСНАЯ? def isSoglas(ch : str) -> bool: return ch in soglas Русско-тарабарский переводчик готов к употреблению. Плоды его усилий вы уже видели в начале проекта, но вы можете оттарабарить и другие замечательные произведения русской мысли. 92
Наш тарабарский язык не всегда благозвучный. Облагородьте правила перевода так, чтобы тарабарская речь стала плавной и мелодичной! Регулярные выражения Питон имеет замечательный модуль re (сокращение от regular expressions), который легко подключается к программе. Регулярное выражение это строка-шаблон для поиска подстрок в заданном тексте. Давайте рассмотрим несколько полезных примеров использования этого модуля. Проект Пора по парам findall В июньском номере журнала Квантик за 2015 год предлагалось решить такую задачу. Её условие сразу адресует нас к названию одной из лучших команд КВН Пора по парам. Впрочем, это каламбурное сочетание слов можно встретить и на театральных афишах, и в объявлениях. Я вспомнил только одно слово - геенна. В словаре Ожегова оно присутствует, но известно ли оно «любознательным школьникам», для которых и предназначается эта задача? Терзаемый сомнениями, я решил поискать и другие слова, но уже компьютерным способом. 93
Действительно пора! Понятно, что программа для решения этой задачи несложная, поэтому её можно написать за несколько минут. В функции main мы выбираем файл со списком слов и вызываем функцию ааbb: def main(): # Квантик 2015 06 27 1 print('Пора по парам') 94
# словарь: fileName = 'makarov_frc.txt' #fileName = "OSH-W97_frc.txt" # ищем слова: aabb(fileName) fileName = 'EnDictionary_frc.txt' # ищем слова: #aabb(fileName) main() В функции aabb мы загружаем все слова в список spisok: # Квантик 2015 06 27 1 import re # РЕШАЕМ ЗАДАЧУ def aabb(fn): # нашли слов: nWords = 0 with open(fn) as file: spisok = file.readlines() # убираем символ переноса строки: spisok = [s.rstrip('\n') for s in spisok] А затем проверяем все слова в цикле for: for wrd in spisok: Метод findall класса регулярных выражений re возвращает список всех совпадений с заданным условием. Для нашего случая годится простое выражение: '(.)\1' Или немного более сложное: '(\w)\1' 95
Они срабатывают, если в очередном слове wrd имеются две одинаковые смежные буквы: for wrd in spisok: match = re.findall(r'(.)\1', wrd) if len(match) > 1: print(match, wrd) Но так мы найдём слова, которые содержат не менее двух пар одинаковых букв, которые не обязательно следуют друг за другом. Пора по парам ['Е', 'Н'] ГЕЕННА ['Л', 'Т'] БАЛЛОТТЕ ['К', 'С'] БЕККРОСС ['О', 'С'] ЗООМАССА ['С', 'И'] СИССИТИИ ['Н', 'Т'] БЕННЕТТИТ ['П', 'Т'] ГРУППЕТТО ['Л', 'Т'] МИЛЛИВАТТ ['М', 'И'] ОММАТИДИИ ['Д', 'Н'] ПОДДАННАЯ ['Д', 'Н'] ПОДДАННЫЙ ['Д', 'Н'] ПОДДОННИК ['Л', 'Т'] АЛЛЕГРЕТТО . . . Слова, да не те! Среди найденных слов безусловно имеются и те, которые нам нужны. Например, «моё» слово геенна, а также слова вакуумметр и массообмен, которые также подходят под условие задачи. Иногда и такие слова вам могут пригодиться. 96
Всего в списке 100 слов, которые можно просмотреть и вручную, однако несколько дополнительных строк в программе избавят нас и от этой необходимости. Запишем в строковую переменную letters найденные парные буквы. Например, для тестового слова геенна это буквы ЕЕНН: if len(match) > 1: print(match, wrd) # пары одинаковых букв: letters = "" for i in range(len(match)): letters += match[i] + match[i] print(letters) Пора по парам ['Е', 'Н'] ГЕЕННА ЕЕНН ['Л', 'Т'] БАЛЛОТТЕ ЛЛТТ ['К', 'С'] БЕККРОСС ККСС ['О', 'С'] ЗООМАССА ООСС . . . Если между парами одинаковых букв имеются другие буквы, то этот метод слово проигнорирует. Если же в проверяемом слове пары одинаковых букв стоят рядом, то мы легко обнаружим эту «группировку»: for wrd in spisok: #print (wrd) match = re.findall(r'(.)\1', wrd) #match = re.findall(r'(\w)\1{2,}', wrd) #ooo #match = re.findall(r'(\w)\1', wrd) #match = re.findall(r'\w{2}', wrd) # пара любых букв #print(match) if len(match) > 1: 97
#print(match, wrd) # пары одинаковых букв: letters = "" for i in range(len(match)): letters += match[i] + match[i] #print(letters[4:]) if len(match) >= 2 and letters[0:4] in wrd: #if len(match) == 2 and wrd.find(letters[0:4]) != -1: print(wrd) nWords += 1 В некоторых словах содержатся 3 пары букв: ВОЕННООБЯЗАННЫЙ Метод findall найдёт их все, поэтому мы должны учесть и этот случай. В строке letters окажутся буквы ННООНН. Мы ищем в слове ВОЕННООБЯЗАННЫЙ буквы ННОО, но нужно также проверить и ООНН, что мы и сделали в программе. Однако в русском языке не нашлось ни одного слова с тремя парами одинаковых букв, которые располагались бы по второй схеме (кроме слова телегаммааппарат, о котором речь пойдёт дальше). Всего мне удалось обнаружить 13 слов, удовлетворяющих условию задачи. Пора по парам ГЕЕННА ВАКУУММЕТР МАССООБМЕН ВАКУУММЕТРИЯ МЕТАЛЛООПТИКА ВОЕННООБЯЗАННАЯ ВОЕННООБЯЗАННЫЙ КРИСТАЛЛООПТИКА МЕТАЛЛООБРАБОТКА ПАРААГГЛЮТИНАЦИЯ ТЕЛЕГАММААППАРАТ МИЦЕЛЛООБРАЗОВАНИЕ КРИСТАЛЛООБРАЗОВАНИЕ Всего найдено слов 13 98
Какое именно из этих слов имел в виду автор задачи, мне неизвестно, но наиболее «детское» из этих слов – военнообязанный. Если внимательно приглядеться к найденным словам, то можно найти единственное слово, в котором идут подряд 3 пары одинаковых букв. Это причудливое слово телегаммааппарат, которое начинается со слова телега, так что не каждый прочитает его правильно с первого раза. Всегда интересно покопаться в иноязычных словарях – на предмет сравнения их с могучим русским. В английском языке нашлось всего 3 однокоренных слова с тремя смежными парами букв и ещё 12 слов, в которых пары располагаются по второй схеме. Возможно, нас больше порадовали бы эстонские и финские слооваарии, но я таковых не имею под рукой… По прошествии времени мы уже можем заглянуть в ответ на эту задачу, который приведён в номере 10, на странице 28. Списка слов из словаря Зализняка у меня нет, но, в общем и целом, мы справились с задачей. А вот какое слово должны были найти детишки, так и осталось тайной. 99
В словесных играх принято искать имена существительные, в единственном числе, в именительном падеже. Если же это не так, то нужно указывать, что таких ограничений нет. Поэтому все эти аллее и идиллии – это пример нечестной игры. Учите русский язык! Проект Фракционирование словаря match С помощью метода match можно выбрать из списка слова заданной длины: # Фракционируем словарь import re # ИЩЕМ СЛОВА def find(fn): # нашли слов: nWords = 0 with open(fn) as file: spisok = file.readlines() # убираем символ переноса строки: spisok = [s.rstrip('\n') for s in spisok] for wrd in spisok: #print(wrd) match = re.match(r'\b\w{2}\b', wrd) if match: print(wrd) nWords += 1 print('Всего найдено слов', nWords) print() def main(): 100
print('Фракционируем словарь') # словарь: fileName = 'makarov_frc.txt' #fileName = "OSH-W97_frc.txt" # ищем слова: find(fileName) #fileName = 'EnDictionary_frc.txt' # ищем слова: #find(fileName, len_word) main() Фракционируем словарь АВ АД АЗ АР АС . . . ЯД ЯК ЯЛ ЯМ ЯР Всего найдено слов 46 101
Проект Поиск по шаблону 102
Несколько полезных шаблонов: import re # ЗАГРУЖАЕМ СЛОВАРЬ def load(fn): with open(fn) as file: spisok = file.readlines() # убираем символ переноса строки: return [s.rstrip('\n') for s in spisok] def main(): fn = "OSH-W97_frc.txt" words = load(fn) # Слова, начинающиеся на «а» или «А» pattern_a = re.compile(r'^[аА]', re.UNICODE) starts_a = [w for w in words if pattern_a.match(w)] print(f" Начинаются на А/а: {starts_a}") # Слова длиннее 6 букв (только буквы, без цифр) pattern_long = re.compile(r'^[а-яёА-ЯЁa-zA-Z]{13,}$', re.UNICODE) long_words = [w for w in words if pattern_long.match(w)] print(f" Длиннее 12 букв: {long_words}") # Слова, содержащие цифры pattern_digit = re.compile(r'\d') with_digits = [w for w in words if pattern_digit.search(w)] print(f" Содержат цифры: {with_digits}") # Слова, состоящие ТОЛЬКО из букв верхнего регистра pattern_upper = re.compile(r'^[А-ЯЁA-Z]+$') all_upper = [w for w in words if pattern_upper.match(w)] print(f" Только заглавные: {all_upper}") main() Проект Извлечение чисел из текста Чрезвычайно полезные числовые шаблоны: 103
import re text_numbers = """ Температура: -12.5°C, давление 1013.25 гПа. Скорость света: 299_792_458 м/с. Координаты: 55.7558° N, 37.6176° E Цена: 1 234,99 руб. Скидка 15%. Версия: 3.11.4 IPv4: 192.168.0.1 """ # Целые числа integers = re.findall(r'\b\d+\b', text_numbers) print(f" Целые числа: {integers}") # Числа с плавающей точкой (включая отрицательные) floats = re.findall(r'-?\d+[.,]\d+', text_numbers) print(f" Вещественные числа: {floats}") # Числа с единицей измерения with_units = re.findall(r'(-?\d+(?:[.,]\d+)?)\s*(°C|гПа|м/с|руб|%)', text_numbers) print(f" Числа с единицами: {with_units}") # Числа через подчёркивание (Python-стиль) underscore_nums = re.findall(r'\d[\d_]+\d', text_numbers) print(f" С подчёркиванием: {underscore_nums}") # Только целые числа → преобразование в int int_values = [int(n) for n in re.findall(r'\b\d+\b', text_numbers)] print(f" → как int: {int_values}") # Географические координаты coords = re.findall(r'(\d+\.\d+)°\s*([NS EW])', text_numbers) print(f" Координаты: {coords}") Целые числа: ['12', '5', '1013', '25', '55', '7558', '37', '6176', '1', '234', '99', '15', '3', '11', '4', '192', '168', '0', '1'] Вещественные числа: ['-12.5', '1013.25', '55.7558', '37.6176', '234,99', '3.11', '192.168', '0.1'] Числа с единицами: [('-12.5', '°C'), ('1013.25', 'гПа'), ('458', 'м/с'), ('234,99', 'руб'), ('15', '%')] С подчёркиванием: ['1013', '299_792_458', '7558', '6176', '234', '192', '168'] 104
→ как int: [12, 5, 1013, 25, 55, 7558, 37, 6176, 1, 234, 99, 15, 3, 11, 4, 192, 168, 0, 1] Координаты: [('55.7558', 'N'), ('37.6176', 'E')] Проект Проверка форматов Довольно часто нужно проверить правильность записи в каком-либо формате: import re samples = { "email": ["user@example.com", "bad-email", "a.b+c@mail.ru", "@nodomain"], "phone": ["+7 (495) 123-45-67", "8-800-555-35-35", "123", "+1 650 253 0000"], "date": ["2024-01-31", "31.12.2023", "2024/13/01", "01-13-2024"], "ip": ["192.168.0.1", "255.255.255.255", "256.0.0.1", "10.0.0"], "hex": ["#FF5733", "#abc", "#GGGGGG", "ff5733"], } patterns = { "email": re.compile(r'^[\w.+\-]+@[\w\-]+\.[a-z]{2,}$', re.IGNORECASE), "phone": re.compile(r'^[+8][\d\s()\-]{9,}$'), "date": re.compile(r'^(\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])' r'|\d{2}\.\d{2}\.\d{4})$'), "ip": re.compile(r'^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}' r'(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$'), "hex": re.compile(r'^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$'), } for kind, values in samples.items(): print(f"\n {kind.upper()}:") for v in values: ok = "✓" if patterns[kind].match(v) else "✗" print(f" {ok} {v!r}") EMAIL: ✓ 'user@example.com' ✗ 'bad-email' IP: ✓ ✓ '192.168.0.1' '255.255.255.255' 105
✓ ✗ 'a.b+c@mail.ru' '@nodomain' ✗ ✗ PHONE: ✓ '+7 (495) 123-45-67' ✓ '8-800-555-35-35' ✗ '123' ✓ '+1 650 253 0000' HEX: ✓ ✓ ✗ ✗ '256.0.0.1' '10.0.0' '#FF5733' '#abc' '#GGGGGG' 'ff5733' DATE: ✓ '2024-01-31' ✓ '31.12.2023' ✗ '2024/13/01' ✗ '01-13-2024' Проект Замена слов Нужные в хозяйстве шаблоны: import re text_replace = "Цена: 1000р., скидка 200р., итого 800р." # Добавить пробел перед «р.» result = re.sub(r'(\d+)р\.', r'\1 руб.', text_replace) print(f" Исходный: {text_replace}") print(f" После sub: {result}") # Маскировка email text_email = "Пишите на admin@site.ru или support@company.com" masked = re.sub(r'([\w.]+)@([\w.]+)', r'\1@***', text_email) print(f" Email mask: {masked}") # Нормализация пробелов messy = "Слово1 слово2\t\tслово3\n\nслово4" clean = re.sub(r'\s+', ' ', messy).strip() print(f" Пробелы: {clean!r}") # Замена с функцией: умножить все числа на 2 text_mul = "a=3, b=7, c=12" 106
def double(m): return str(int(m.group()) * 2) result_mul = re.sub(r'\d+', double, text_mul) print(f" ×2 числа: {text_mul!r} → {result_mul!r}") Исходный: После sub: Email mask: Пробелы: ×2 числа: Цена: 1000р., скидка 200р., итого 800р. Цена: 1000 руб., скидка 200 руб., итого 800 руб. Пишите на admin@*** или support@*** 'Слово1 слово2 слово3 слово4' 'a=3, b=7, c=12' → 'a=6, b=14, c=24' Проект Разбиение текста на слова Необходимые шаблоны при работе с текстами: import re csv_line = 'Иван, Петров; 42 | Москва' tokens = re.split(r'[\s,;|]+', csv_line.strip()) print(f» CSV-строка → {tokens}») sentences = «Первое предложение. Второе! Третье? Четвёртое…» parts = re.split(r'(?<=[.!?])\s+', sentences) print(f» Предложения → {parts}») CSV-строка → ['Иван', 'Петров', '42', 'Москва'] Предложения → ['Первое предложение.', 'Второе!', 'Третье?', 'Четвёртое…'] 107
Проект Именованные группы Шаблоны, чтобы разложить информацию по полочкам: import re log_line = '2024-03-15 14:32:07 ERROR [auth] Неверный пароль для user42' pattern_log = re.compile( r'(?P<date>\d{4}-\d{2}-\d{2})\s+' r'(?P<time>\d{2}:\d{2}:\d{2})\s+' r'(?P<level>\w+)\s+' r'\[(?P<module>\w+)\]\s+' r'(?P<message>.+)' ) m = pattern_log.match(log_line) if m: print(f» Дата: {m.group('date')}») print(f» Время: {m.group('time')}») print(f» Уровень: {m.group('level')}») print(f» Модуль: {m.group('module')}») print(f» Сообщение:{m.group('message')}») print(f» Словарь: {m.groupdict()}») Дата: 2024-03-15 Время: 14:32:07 Уровень: ERROR Модуль: auth Сообщение:Неверный пароль для user42 Словарь: {'date': '2024-03-15', 'time': ’14:32:07', 'level': 'ERROR', 'module': 'auth', 'message': 'Неверный пароль для user42'} Проект Жадный vs нежадный поиск Шаблоны для Интернетовской информации: 108
import re html = '<b>жирный</b> и <i>курсив</i>' greedy = re.findall(r'<.+>', html) non_greedy = re.findall(r'<.+?>', html) print(f" print(f" print(f" HTML: Жадный: Нежадный: # жадный: весь фрагмент # нежадный: каждый тег отдельно {html!r}") {greedy}") {non_greedy}") # Текст между тегами content = re.findall(r'<\w+>(.*?)</\w+>', html) print(f" Содержимое: {content}") HTML: Жадный: Нежадный: Содержимое: '<b>жирный</b> и <i>курсив</i>' ['<b>жирный</b> и <i>курсив</i>'] ['<b>', '</b>', '<i>', '</i>'] ['жирный', 'курсив'] Проект Флаги Туда же: import re multiline_text = """ошибка: файл не найден Ошибка: нет прав доступа ОШИБКА: диск заполнен""" # re.IGNORECASE — регистронезависимый поиск errors = re.findall(r'^ошибка:.+', multiline_text, re.IGNORECASE | re.MULTILINE) print(f" IGNORECASE+MULTILINE: {errors}") # re.VERBOSE — читаемое регулярное выражение с комментариями pattern_verbose = re.compile(r""" (\d{1,3}) # первый октет 109
\. # точка (\d{1,3}) # второй октет \. # точка (\d{1,3}) # третий октет \. # точка (\d{1,3}) # четвёртый октет """, re.VERBOSE) ip_match = pattern_verbose.match("192.168.1.100") print(f" VERBOSE IP-октеты: {ip_match.groups() if ip_match else None}") IGNORECASE+MULTILINE: ['ошибка: файл не найден', 'Ошибка: нет прав доступа', 'ОШИБКА: диск заполнен'] VERBOSE IP-октеты: ('192', '168', '1', '100') 110
Проект Слова-матрёшки 111
Слова-матрёшки – это известные на слова в словах, которые мы ищем теперь с помощью регулярных выражений: re.search ищет подстроки в более длинных строка, а re.sub подсвечивает короткое слово скобками: import re # ЗАГРУЖАЕМ СЛОВАРЬ def load(fn): with open(fn) as file: spisok = file.readlines() # убираем символ переноса строки: return [s.rstrip(‚\n‘) for s in spisok] def main(): fn = „OSH-W97_frc.txt“ words = load(fn) short = [w for w in words if 3 <= len(w) <= 4] long = [w for w in words if len(w) > 4] matroshki = [] for needle in short: pat = re.compile(needle, re.IGNORECASE) for haystack in long: if needle != haystack and pat.search(haystack): matroshki.append((needle, haystack)) for needle, haystack in matroshki: # Подсветим найденное слово в «контейнере» через re.sub highlighted = re.sub( f‘({needle})‘, r‘[\1]‘, haystack, flags=re.IGNORECASE ) print(f“ «{needle}» в «{highlighted}»“) main() 112
Программа находит огромное число матрёшек, поэтому только самая маленькая матрёшка: «АЗУ» в «К[АЗУ]С» «АЗУ» в «М[АЗУ]Т» «АЗУ» в «Р[АЗУ]М» «АЗУ» в «Л[АЗУ]РЬ» «АЗУ» в «П[АЗУ]ХА» «АЗУ» в «ГЛ[АЗУ]РЬ» . . . «ЯМКА» в «Л[ЯМКА]» «ЯНКИ» в «ДРОБ[ЯНКИ]» «ЯРКА» в «ДО[ЯРКА]» «ЯРКА» в «ПУЛ[ЯРКА]» «ЯРКА» в «СОЛ[ЯРКА]» «ЯРКА» в «МАДЬ[ЯРКА]» «ЯРУС» в «СТЕКЛ[ЯРУС]» «ЯХТА» в «ШЛ[ЯХТА]» «ЯЩЕР» в «[ЯЩЕР]ИЦА» Проект Регексные палиндромы Регулярные выражения не годятся для поиска длинных палиндромов. Им под силу только трёхбуквенные: import re # ЗАГРУЖАЕМ СЛОВАРЬ def load(fn): with open(fn) as file: spisok = file.readlines() # убираем символ переноса строки: return [s.rstrip('\n') for s in spisok] def main(): fn = "OSH-W97_frc.txt" 113
words = load(fn) # Для коротких палиндромов (3 буквы) можно написать regex напрямую: # слово вида «ABA» — первая и третья буква одинаковы pat_aba = re.compile(r'\b([а-яё])([а-яё])\1\b', re.IGNORECASE | re.UNICODE) aba_words = pat_aba.findall(' '.join(words)) print([m[0] + m[1] + m[0] for m in dict.fromkeys(aba_words)]) main() ['БОБ', 'ДЕД', 'КОК', 'МИМ', 'ОКО', 'ОНО', 'ПОП', 'ПУП', 'ТАТ', 'ТУТ', 'ШИШ'] Проект Регексные анаграммы Поиск анаграмм суров, поэтому разумно ограничивайте длину слов для проверки: import re from itertools import combinations # ЗАГРУЖАЕМ СЛОВАРЬ def load(fn): with open(fn) as file: spisok = file.readlines() # убираем символ переноса строки: return [s.rstrip('\n') for s in spisok if len(s) <= 7] def regex_anagram_check(word_a: str, word_b: str) -> bool: if len(word_a) != len(word_b): return False # Для каждой уникальной буквы проверяем совпадение частоты for ch in set(word_a): count = word_a.count(ch) # Паттерн: строка содержит ровно count вхождений ch pat = re.compile( rf'^(?:[^{ch}]*{re.escape(ch)}){{{count}}}[^{ch}]*$' ) 114
if not pat.match(word_b): return False return True def main(): fn = "OSH-W97_frc.txt" words = load(fn) # Найдём анаграммы в нашем списке anagram_pairs = [] for w1, w2 in combinations(words, 2): if sorted(w1.lower()) == sorted(w2.lower()) and w1 != w2: anagram_pairs.append((w1, w2)) print(" Найденные анаграммы:") if anagram_pairs: for a, b in anagram_pairs: ok = regex_anagram_check(a.lower(), b.lower()) check = "✓ regex" if ok else "✗" print(f" {a} ↔ {b} ({check})") main() Найденные анаграммы: ЛЯ ↔ ЯЛ (✓ regex) СУ ↔ УС (✓ regex) АКР ↔ РАК (✓ regex) БАР ↔ БРА (✓ regex) БАР ↔ РАБ (✓ regex) БРА ↔ РАБ (✓ regex) БУК ↔ КУБ (✓ regex) ВЕС ↔ СЕВ (✓ regex) ВОЗ ↔ ЗОВ (✓ regex) . . . ТЩАНИЕ ↔ ЩЕТИНА (✓ regex) УМНИЦА ↔ ЦУНАМИ (✓ regex) ФАКТОР ↔ ФОРТКА (✓ regex) ЧАСТИК ↔ ЧИСТКА (✓ regex) ЧЕПРАК ↔ ЧЕРПАК (✓ regex) 115
Проект Регексные чайнворды 116
Чайнворды – это цепочки слов, которые цепляются друг за друга. Они хорошо известны любителям поиграть в города. Но в этой игре слова цепляются только одиночными начальными и конечными буквами, а в чайнвордах слова могут цепляться любым число букв. Для проекта нам нужен небольшой список слов, иначе цепочка получится очень длинной. Для примера годится список некоторых столиц. import re # ЗАГРУЖАЕМ СЛОВАРЬ def load(fn): with open(fn) as file: spisok = file.readlines() # убираем символ переноса строки: return [s.rstrip('\n') for s in spisok if len(s) <= 7] def find_chains(words: list[str], min_len: int = 2) -> list[list[str]]: """Ищет все цепочки заданной минимальной длины.""" words_lower = [(w, w.lower()) for w in words] chains = [] def dfs(chain: list[str], used: set[int]): last = chain[-1].lower() # Последние N букв текущего слова extended = False for suffix_len in range(min(4, len(last) - 1), 0, -1): suffix = last[-suffix_len:] pat = re.compile(rf'^{re.escape(suffix)}', re.IGNORECASE) for i, (w, wl) in enumerate(words_lower): if i not in used and pat.match(wl) and wl != last: dfs(chain + [w], used | {i}) extended = True if not extended and len(chain) >= min_len: chains.append(chain[:]) for i, (w, _) in enumerate(words_lower): dfs([w], {i}) # Убираем дубликаты и оставляем длинные seen = set() unique = [] for c in sorted(chains, key=len, reverse=True): key = tuple(x.lower() for x in c) 117
if key not in seen: seen.add(key) unique.append(c) return unique[:10] def main(): fn = "города2.txt" words = load(fn) chains = find_chains(words, min_len=2) for chain in chains: # выделяем «стыки» через regex: parts = [] for i, word in enumerate(chain): if i < len(chain) - 1: next_word = chain[i + 1].lower() # общие концы: for slen in range(min(4, len(word) - 1), 0, -1): if next_word.startswith(word[-slen:].lower()): marked = word[:-slen] + f"[{word[-slen:]}]" parts.append(marked) break else: parts.append(word) else: parts.append(word) arrow = " → ".join(parts) print(f" {arrow} (длина: {len(chain)})") main() В итоге мы нашли десяток самых длинных чайнвордных цепочек: МАНИ[ЛА] → ЛАГО[С] → СЕУ[Л] → ЛИМ[А] → АН[КАРА] → КАРА[ЧИ] → ЧИКАГО (длина: 7) МАНИ[ЛА] → ЛАГО[С] → СЕУ[Л] → ЛИМ[А] → АН[КАРА] → КАРАЧ[И] → ИБАДАН (длина: 7) ЛАГО[С] → СЕУ[Л] → ЛИ[МА] → МАНИЛ[А] → АН[КАРА] → КАРА[ЧИ] → ЧИКАГО (длина: 7) ЛАГО[С] → СЕУ[Л] → ЛИ[МА] → МАНИЛ[А] → АН[КАРА] → КАРАЧ[И] → ИБАДАН (длина: 7) БАГДА[Д] → ДЕ[ЛИ] → ЛИ[МА] → МАНИ[ЛА] → ЛАГО[С] → СЕУ[Л] → ЛОНДОН (длина: 7) БАГДА[Д] → ДЕ[ЛИ] → ЛИ[МА] → МАНИ[ЛА] → ЛАГО[С] → СЕУ[Л] → ЛАХОР (длина: 7) БАГДА[Д] → ДЕ[ЛИ] → ЛИ[МА] → МАНИЛ[А] → АН[КАРА] → КАРА[ЧИ] → ЧИКАГО (длина: 7) БАГДА[Д] → ДЕ[ЛИ] → ЛИ[МА] → МАНИЛ[А] → АН[КАРА] → КАРАЧ[И] → ИБАДАН (длина: 7) ДЕ[ЛИ] → ЛИ[МА] → МАНИ[ЛА] → ЛАГО[С] → СЕУ[Л] → ЛОНДОН (длина: 6) ДЕ[ЛИ] → ЛИ[МА] → МАНИ[ЛА] → ЛАГО[С] → СЕУ[Л] → ЛАХОР (длина: 6) 118
Множества (Sets) 119
Множество – это неупорядоченный набор, коллекция уникальных (неповторяющихся) объектов (элементов) разных типов. Все элементы множества различны. По-английски множество называют set, по-немецки – Menge. Иногда в комбинаторике рассматривают и множества с повторениями. Удобно представлять множество в виде мешка, который может быть пустым, а может быть доверху набит, например, книгами или другими подарками – фруктами, разноцветными шариками, спинерами, мобильниками и другими гаджетами… В отличие от списка в множестве (то есть в нашем мешке) все подарки должны быть разными. Если мы положим в мешок два экземпляра книги Незнайка на Луне, то останется только один. В список мы можем поместить сколько угодно одинаковых подарков. Другое отличие множества от списка состоит в том, что его элементы не имеют строго заведенного порядка. Например, если в списке мы поставили книгу Незнайка на Луне в первую ячейку, а книгу Винни-Пух и все-все-все во вторую, то они всегда будут стоять именно в этом порядке. Возвращаемся к нашему мешку (множеству) – в нём книги лежат вперемешку, и мы не можем знать, где какая книга находится. Вам может показаться, что множество менее удобно, чем список, но есть немало задач, когда множество значительно удобнее и полезнее. Действительно, вы пошли в магазин и решили купить книгу, но вам кажется, что она у вас уже есть. В этом случае достаточно спросить: «Мешок, у тебя есть такая книга?», и затем действовать, исходя из его ответа. В случае списка вам пришлось бы просматривать все полки (или только часть, если вы, к примеру, расставили книги по алфавиту, автору или темам), чтобы ответить на этот житейский вопрос. 120
Теорию множеств (Set theory) создал в 19-ом веке немецкий математик Георг Фердинанд Людвиг Филипп Кантор (Georg Ferdinand Ludwig Philipp Cantor, 1845-1918). Интересно, что он родился в России, мать у него была русская, а отец – датчанин. Сейчас теория множеств - это одна из важнейших областей математики. Множество может быть конечным (finite) и бесконечным (infinite) – в зависимости от числа элементов в нём. В том числе множество может не иметь ни одного элемента (пустое множество, null set, empty set). Примеры конечных множеств: подарки в нашем мешке, ученики в классе, цвета в радуге, буквы в алфавите, имена людей, марки автомобилей, дни недели, названия месяцев. В математике рассматриваются множества чисел: натуральных, целых, рациональных, иррациональных, комплексных. Всё это бесконечные множества. Пустое множество составляют, например, летающие крокодилы. Множества принято обозначать заглавными латинскими буквами, написанными жирным курсивом, а элементы множества – строчными буквами и нежирным курсивом. Для пустого множества выделен особый символ – перечеркнутый кружок: A=∅ Или пустые скобки: A={} Множества могут быть заданы двумя способами: - Перечислением элементов конечного (и не очень большого!) множества: 121
A = { 2, 3, 5, 7 } - Указанием общего свойства (или свойств), которое имеют элементы множества: A = { e | e – простое число ⋀ e < 10 } Вместо прямой черты используют двоеточие. Элементы множества записывают в фигурных скобках. Если элемент e принадлежит множеству A, то записывают: e∈A А если не принадлежит – e∉A Операции над множествами Пусть первое множество M = {Red, Orange}, второе N = {Orange, Yellow}, а результат выполнения операции – множество S. ⋃ (или +) - оператор объединения множеств. В результате операции получается новое множество (сумма, union), которое содержит по одному разу все элементы исходных множеств: M ⋃ N → S = { Red, Orange, Yellow } ⋂ (или *) - оператор пересечения множеств. В результате операции получается новое множество (произведение, intersection), которое содержит элементы, принадлежащие одновременно обоим множествам: M ⋂ N → S = { Orange } или M N → S = { Orange } 122
- (или \) оператор разности множеств (относительное дополнение, difference). В результате операции получается новое множество, которое содержит элементы первого множества, не принадлежащие второму множеству: M - N → S = { Red } ∆ оператор симметричной разности множеств. В результате операции получается новое множество, которое содержит элементы, содержащиеся либо в первом множества, либо во втором, но не одновременно в них обоих: M ∆ N → S = { Red, Yellow } Эта операция соответствует логической операции XOR. Отношения между множествами = оператор эквивалентности. Два множества равны, если они содержат одни и те же элементы. Если множество M = {Red, Orange}, а множество N = {Orange, Red}, то M = N. ⊈ (или ≠) оператор неэквивалентности. Множества не равны, если они отличаются хотя бы одним элементом. Если M = {Red, Orange}, второе N = {Orange, Yellow}, то M≠ N. ⊆ (или ≤) оператор подмножества. Если все элементы первого множества принадлежат и второму множеству, то первое множество называют (нестрогим) подмножеством второго множества. Если множество M = {Red, Orange}, а множество N = {Orange, Red, Yellow}, то M ≤ N. 123
Если число элементов в первом множестве меньше, чем во втором, то говорят о строгом подмножестве. В нашем примере это так, поэтому мы можем записать отношение множеств так: M < N. Или так: M ⊂ N. Пустое подмножество является подмножеством любого другого непустого множества. Эта особенность пустого множества приводит к забавным результатам. Например, в книге Парадоксы теории множеств доказывается, что все летающие крокодилы участвуют в олимпиадах. Действительно, множество летающих крокодилов пустое (как известно, крокодилы летают только в анекдотах), поэтому такие крокодилы вполне могут входить в любую олимпийскую сборную (в российской команде эту роль исполняет Чебурашка – тоже неведомая зверушка). ⊇ (или ≥) оператор включающего множества (надмножества). Если все элементы второго множества принадлежат и первому множеству, то первое множество называют (нестрогим) надмножеством второго множества. Если множество M = {Red, Orange, Yellow}, а множество N = {Orange, Redто M ≥ N. 124
Два множества называются непересекающимися (disjoint sets), если они не содержат ни одного общего элемента (все элементы в них разные). Некоторые свойства множеств: • Пересечение множества с пустым множеством равно пустому множеству. • Объединение множества с пустым множеством равно исходному множеству. • Пересечение или объединение множества с самим собой равно исходному множеству. • От перемены мест множеств в операциях пересечения и объединения результирующее множество не изменяется. Диаграммы Венна Для наглядного представления логических связей (отношений) между множествами часто используют диаграммы Венна (Venn diagrams), в которых множества изображают кругами, эллипсами или замкнутыми кривыми, а элементы множества – точками. Графические методы использовались в науке с давних времен. Ещё в 13-ом веке Раймонд Луллий употреблял в своих работах подобные круги. Имеются они и в работах немецкого математика Лейбница. Чешский математик Бернард Больцано вместо кругов рисовал прямоугольники. В книге Алгебра логики круги применял немецкий математик Эрнест Шрёдер. Распространению метода особенно способствовал Леонард Эйлер, который в Письмах к немецкой принцессе (1768) с их помощью пояснял решение силлогизмов (логических рассуждений) Аристотеля. Английский логик Джон Венн (John Venn, 1843—1923) обобщил этот графический метод в книге Символическая логика (1881). При внешнем сходстве кругов Эйлера и диаграмм Венна они отличаются областями применения. Первые использовались в традиционной силлогистике, вторые – в математической логике. В некоторых случаях их 125
объединяют под общим названием круги (диаграммы) Эйлера-Венна. В нашем случае был бы более уместен именно этот термин, но мы всё-таки остановимся на более коротком варианте. Объявим несколько множеств, элементы которых натуральные числа: однозначные M ={1,2,3,4,5} N = {3,5,6,7} K=M L = {2,3} P = {1,2,3,4,5,6,7} Представим множества в виде кругов, внутри которых находятся элементы множеств. А вот так с помощью диаграмм Венна можно изобразить операции над множествами. Объединение множеств S=M+N 126
Из диаграммы следует, что сумма множеств S содержит все элементы множеств M и N по одному разу. Пересечение множеств S = MN Пересечение множеств S содержит общие элементы множеств M и N по одному разу. Разность множеств S=M–N Из диаграмммы видно, что разность множеств М-N равна множеству М, из которого удалена общая часть (пересечение) множеств МN, то есть 127
М-N = M – MN Если множество N является подмножеством М, то множество М-N называют дополнением (complement) к множеству N. Симметричная разность множеств S=M∆N Принадлежность элемента множеству 5 ∈ М, 6 ∉ М 128
Эквивалентность множеств М=К Неэквивалентность множеств М≠К Подмножество L<М 129
Надмножество P>М 130
Множества в Питоне 131
Множества могут быть только конечными, в том числе и пустыми. Элементы множества неизменяемы (immutаblе), хотя сами множества можно изменять. Чтобы создать пустое множество без элементов, используют пару фигурных скобок или функцию set: # пустые множества: set1 = {} set2 = set() Можно сразу перечислить элементы множества в фигурных скобках: set3 = {1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'} print(set3) {1, 2, 3, 4, 5, 'зайчик', 'погулять', 'вышел'} А в функцию set можно передать список: lst = [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] set4 = set(lst) print(set4) {1, 2, 3, 4, 5, 'зайчик', 'погулять', 'вышел'} Кортеж: tup = (1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять') set7 = set(tup) print(set7) 132
{1, 2, 3, 4, 5, 'зайчик', 'погулять', 'вышел'} Или функцию range: set5 = set(range(10)) print(set5) {0, 1, 2, 3, 4, 5, 6, 7, 8, 9} Поскольку строка – это коллекция символов, то из неё также можно создать множество: set6 = set('КОЛОКОЛ') print(set6) {'О', 'Л', 'К'} Обратите внимание, что порядок букв нарушен, а повторяющиеся буквы исчезли. Проект Оперируем множествами Самый простой способ для печати всех элементов множества – передать его функции print: def print_set(): set1 = {1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'} print(set1) 133
print_set() {'зайчик', 1, 2, 3, 4, 5, 'погулять', 'вышел'} При печати мы получим множество элементов, заключённое в фигурные скобки. Для печати отдельных элементов, поставьте звёздочку перед именем множества: def print_set(): set1 = {1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'} print(set1) print(*set1) Можно и самостоятельно распечатать элементы множества в цикле for-in: # ПЕЧАТАЕМ ЭЛЕМЕНТЫ МНОЖЕСТВА def print_set(): set1 = {1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'} print(set1) print(*set1) for e in set1: print(e, end=' ') print() 1 2 3 4 5 погулять вышел зайчик Функция len возвращает число элементов в множестве: print(f'Число элементов в множестве = {len(set1)}') 134
Число элементов в множестве = 8 135
Проект Разнобуквицы 136
В каком слове можно найти все русские буквы? Загадка Давайте воспользуемся особенностью множеств хранить только неповторяющиеся элементы для того, чтобы отыскать слова, которые состоят из разных букв. Такие слова называются разнобуквицами. Например, ШКОЛА, УЧЕНИК, УЧЕНИЦА – это именно такие слова, а вот слово КЛАСС не годится – в нём не одна, а две буквы С. Можно внимательно просмотреть словарь и выписать нужные слова, но это занятие очень долгое и утомительное. Оно больше подходит компьютеру вот мы и составим для него небольшую программу, а уж он пусть просматривает словарь! Мы последовательно берём из списка по одному слову и проверякем, подходит оно нам или нет. Если бы не было множеств, нам пришлось бы проверять каждое слово так. Сравниваем первую букву слова со всеми последующими, до самого конца слова. Если она совпадёт с какой-либо буквой, значит, слово не подходит. Например, букву К в слове КЛАСС мы сравниваем с буквами Л, А, С, С и совпадений не обнаруживаем. Если первая буква не совпала ни с одной другой, переходим к следующей букве. Легко убедиться, что буквы Л и С также выдерживают проверку, а вот предпоследняя буква С совпадает с последней, поэтому слово КЛАСС – не разнобуквица. Рассуждая аналогично, мы придем к выводу, что ни одна буква в слове ШКОЛА не повторяется. Но с множеством организовать проверку слов гораздо проще: def main(): # словарь: fileName = 'OSH-W97_frc.txt' # загружаем словарь: spisok = load(fileName) # ищем разнобуквицы: raznobuk(spisok) 137
main() После запуска программы мы считываем все слова из заданного файла в переменную spisok : # Разнобуквицы # ЗАГРУЖАЕМ СЛОВАРЬ def load(fn): with open(fn) as file: spisok = file.readlines() # убираем символ переноса строки: spisok = [s.rstrip('\n') for s in spisok] return spisok А затем передаём список слов функцию raznobuk. Здесь из букв каждого слова word из списка мы создаём множество razn. При этом все кратные буквы исчезнут. Например, от слова ЛИЛИЯ останется только ЛИЯ. Но разнобуквицы при переходе в множество не изменятся, поэтому слово РОЗА таковым и останется. А это значит, что если в множестве столько же букв, сколько их было в исходном слове, то мы нашли разнобуквицу. Ну, а если меньше – обычное слово. # ИЩЕМ РАЗНОБУКВИЦЫ def raznobuk(spisok): for word in spisok: # заполняем множество буквами: razn = set(word) if len(razn) == len(word): # разнобуквица # печатаем: print(word) print() Запускаем программу – и тут же получаем полный отчёт о разнобуквицах русского языка. 138
ЭПИКУРЕЙСТВО ЯЙЦЕВИДНОСТЬ БАРЫШНИЧЕСТВО ГРУШЕВИДНОСТЬ ДРУЖЕЛЮБНОСТЬ НЕВЫРАЗИМОСТЬ НЕУДАЧЛИВОСТЬ ОПРЫСКИВАТЕЛЬ ПРИВЫКАЕМОСТЬ УПРАВЛЯЕМОСТЬ ДЕСЯТИРУБЛЁВКА ЗВУКОСНИМАТЕЛЬ РАЗГИЛЬДЯЙСТВО ЧЕТЫРЁХУГОЛЬНИК Как видите, самое длинное слово в русском языке, в котором буквы не повторяются – это ЧЕТЫРЁХУГОЛЬНИК. В нём 15 букв и все разные! Слово простое и всем известное, но легко ли было бы найти его без помощи компьютера? Если ослабить контроль и считать разнобуквицами любые формы слов, то можно нарастить к этому рекордному слову ещё две буквы – ЧЕТЫРЁХУГОЛЬНИКАМ. Проект Числобус В шестом номере журнала Квант за 1970 год была напечатана такая задача: 139
Здесь, кроме делимого, делителя и частного, даны и промежуточные произведения. Они облегчают ручное решение головоломки, но нам они не нужны, так что мы будем решать сокращённый вариант задачи: ABCD : CD = BCD Как вы видите, от всей задачи осталась только первая строка, что, конечно, ничуть не изменило её сути. Функция main программы нужна нам исключительно для украшательства окна программы и вызова функции solve: def main(): print() print('Квант') print() solve() print() if __name__ == '__main__': main() В функции solve, мы тут же обзаводимся множествами, которые хранят все возможные значения для букв A, B, C, D числобуса и для переменных соответствующих циклов: # Журнал Квант 1970 №6, с.54 # РЕШАЕМ ЗАДАЧУ def solve(): ar = set(range(1, br = set(range(1, cr = set(range(1, dr = set(range(0, 9+1)) 9+1)) 9+1)) 9+1)) 140
Здесь мы учли, что буквы A, B и С не могут заменять нуль, поскольку с них начинаются числа. Сила и новизна рассматриваемого способа решения числобусов заключена в множестве digits: # множество использованных цифр: digits = set() Сразу после создания множество пустое, то есть не содержит ни одной цифры. Это и понятно: мы ещё не приступили к решению задачи. В первом, внешнем цикле переменная а последовательно получает значения из множества ar: 1, 2, … , 9. Так как использованная цифра не должна повторяться, то мы помещаем её в множество digits: for a in ar: digits.add(a) Теперь в нём находится цифра (однозначное число), которой мы заменили букву А в числобусе. Буква В получает свои значения точно так же, но во вложенном цикле: for b in br - digits: digits.add(b) Но если мы будем брать значения для переменной цикла b из множества br, то рано или поздно попадётся цифра, которая уже использована нами для буквы А. Чтобы этого избежать, мы удаляем из множества br цифру для буквы А. В результате выполнения операции: digits.add(b) 141
само множество br не изменится, а в цикле будет использована его копия за вычетом использованной ранее цифры. Так как в новом, укороченном множестве заведомо нет использованной цифры, то нам нет нужды каждый раз сверять значение переменной b со значением переменной а. Код становится простым и изящным! Но у нас осталось ещё 2 цикла – действуем в них аналогично: for c in cr - digits: digits.add(c) for d in dr - digits: В самом последнем цикле, естественно, добавлять цифру в множество digits не нужно, так как она повториться уже никак не сможет. В данной задаче всего 4 буквы и столько же циклов. Но если бы их было больше, то мы легко добавили бы и другие циклы. Теперь мы получили значения для всех букв числобуса и должны проверить, выполняется ли условие задачи, то есть равна ли левая часть равенства правой: left = a * 1000 + b * 100 + c * 10 + d right = (b * 100 + c * 10 + d) * (c * 10 + d) Если это так, то мы печатаем решение на экране: if (left == right): print(f'ABCD : CD = BCD {left} : {b * 100 + c * 10 + d} = {c * 10 + d}') Если бы мы наверняка знали, что решение единственное, то дальнейший перебор нам бы не понадобился. Но поскольку сомнения остаются, то мы доводим перебор до конца. 142
Следующие действия нужно выполнить и в том случае, если значения букв найдены, но равенства и братства не получилось: digits.remove(c) digits.remove(b) digits.remove(a) print() В конце каждого цикла мы удаляем из множества digits значение соответствующей переменной, иначе в нём быстро окажутся все однозначные числа. Несмотря на наше усердное старание, мы нашли единственное решение задачи. Множество множеством, а решение-то единственное! Полный код программы: # Журнал Квант 1970 №6, с.54 # РЕШАЕМ ЗАДАЧУ def solve(): ar = set(range(1, br = set(range(1, cr = set(range(1, dr = set(range(0, 9+1)) 9+1)) 9+1)) 9+1)) # множество использованных цифр: digits = set() for a in ar: digits.add(a) for b in br - digits: digits.add(b) for c in cr - digits: digits.add(c) 143
for d in dr - digits: left = a * 1000 + b * 100 + c * 10 + d right = (b * 100 + c * 10 + d) * (c * 10 + d) if (left == right): print(f'ABCD : CD = BCD {left} : {b * 100 + c * 10 + d} = {c * 10 + d}') digits.remove(c) digits.remove(b) digits.remove(a) print() def main(): print() print('Квант') print() solve() print() if __name__ == '__main__': main() 144
Проект УЖ3 = ПИТОН 145
Смотрим ролик на Ютубе: Самый сложный ребус этого года. 90% решавших не смогли решить: https://www.youtube.com/watch?v=G0-9nxEzj_A Нужно решить числобус: УЖ3 = ПИТОН В ролике наглядно показано, что пятизначные кубы дают только числа из последовательности 22..46. Давайте создадим такую же последоваетльность nums: # РЕШАЕМ ЗАДАЧУ def solve() -> None: # создаём последовательность чисел 22..46: nums = [n for n in range(22, 46 + 1)] print(nums) def main(): solve() 146
main() [22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46] Поскольку перебор чисел небольшой, то мы пропускаем отбрасывание заведомо негодных двузначных чисел. Оставшиеся числа возводятся в куб. Мы делаем то же самое: cubes = tuple(map(lambda a: a * a * a, nums)) print(cubes) [22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46] (10648, 12167, 13824, 15625, 17576, 19683, 21952, 24389, 27000, 29791, 32768, 35937, 39304, 42875, 46656, 50653, 54872, 59319, 64000, 68921, 74088, 79507, 85184, 91125, 97336) 147
Но дальше нам нужно отобрать такие пары двузначных чисел и их кубов, в которых цифры не повторяются, поэтому лучше сразу создать 7-значные числа из первых двух цифр и из пяти цифр их кубов: nums7 = tuple(map(lambda a, b: a * 100000 + b * b * b, nums, nums)) print(nums7) (2210648, 2312167, 2413824, 2515625, 2617576, 2719683, 2821952, 2924389, 3027000, 3129791, 3232768, 3335937, 3439304, 3542875, 3646656, 3750653, 3854872, 3959319, 4064000, 4168921, 4274088, 4379507, 4485184, 4591125, 4697336) Так как число это не последовательность цифр, то лучше сразу преобразовать семизначные числа в строки: nums7 = tuple(map(lambda a, b: str(a * 100000 + b * b * b), nums, nums)) print(nums7) 148
('2210648', '2312167', '2413824', '2515625', '2617576', '2719683', '2821952', '2924389', '3027000', '3129791', '3232768', '3335937', '3439304', '3542875', '3646656', '3750653', '3854872', '3959319', '4064000', '4168921', '4274088', '4379507', '4485184', '4591125', '4697336') То, что автор ролика делал вручную, мы поручим циклу: # просматриваем все строки с # семизначными числами: for t in nums7: Из символов строки создаём множество, в котором останутся только несовпадающие цифры семизначного числа: # множество разных цифр: razn = set(t) Если в множестве остались все 7 цифр, значит, они разные, и мы решили задачу: # если все цифры разные, # печатаем ответ: if len(razn) == 7: print(f'Ответ: {t[:2]} ^ 3 = {t[2:]}') print() Совсем необязательно все задачи решать на компьютере, для тренировок в программировании это не повредит. 149
Проект LeetCode 136. Single Number 2 Given a non-empty array of integers nums, every element appears twice except for one. Find that single one. You must implement a solution with a linear runtime complexity and use only constant extra space. Example 1: Input: nums = [2,2,1] Output: 1 Example 2: Input: nums = [4,1,2,1,2] Output: 4 Example 3: Input: nums = [1] Output: 1 Constraints: 1 <= nums.length <= 3 * 104 -3 * 104 <= nums[i] <= 3 * 104 Each element in the array appears twice except for one element which appears only once. Дан непустой массив целых чисел, в котором все элементы встречаются дважды, кроме одного. Нужно найти это единственное число. Задача должна решаться за линейное время. Эту задачу мы уже решили с помощью логической операции XOR, но есть интересное решение с множеством. Если в списке имеются повторяющиеся числа, то в множестве останется только по одному числу: Список: [a,b,c,b,c] Множество:[a,b,c] 150
Найдём удвоенную сумму чисел в множестве: a + a + b + b + c + c Легко видеть, что каждый элемент массива входит в сумму дважды, то есть единственный элемент мы посчитаем дважды. Если мы теперь вычтем из этой суммы сумму всех элементов исходного массива, то все парные числа обнулятся, а из двух единственных чисел останется одно. Это и есть искомое число: a + (a + b + b + c + c) – (a + b + b + c + c) → a # РЕШАЕМ ЗАДАЧУ def solve(lst: list[int]) -> int: res = 2 * sum(set(lst)) - sum(lst) return res def main(): lst = [2, 2, 1] # lst = [4,1,2,1,2] # lst = [1] res = solve(lst) print(f'Единственное число: {res}') if __name__ == '__main__': main() Проект Кто читал обе книги? Классическая задача из учебников по дискретной математике: В классе 30 учеников. 20 читали «Робинзона», 15 — «Гулливера». Все читали хотя бы одну книгу. Сколько читали обе?" Мы несколько, то есть слегка внесём разнообразие в условие задачи: 151
import random random.seed(42) all_students = set(range(1, 31)) robinson = set(random.sample(list(all_students), 20)) gulliver = all_students - (all_students - set(random.sample(list(all_students), 15))) # все читали хотя бы одну gulliver = gulliver | (all_students - robinson - gulliver) # подгоняем размер while len(gulliver) > 15: gulliver -= {random.choice(list(gulliver - (all_students - robinson)))} while len(gulliver) < 15: gulliver |= {random.choice(list(all_students - gulliver))} both = either only_r only_g robinson & = robinson = robinson = gulliver print(f" print(f" print(f" print(f" print(f" print(f" print() print(f" print(f" Читали Читали Читали Читали Только Только gulliver | gulliver - gulliver - robinson «Робинзона»: «Гулливера»: обе книги: хотя бы одну: «Робинзона»: «Гулливера»: # # # # пересечение объединение только «Робинзон» только «Гулливер» {len(robinson)} чел.") {len(gulliver)} чел.") {len(both)} чел. ← пересечение ∩") {len(either)} чел. ← объединение ∪") {len(only_r)} чел. ← разность A∖B") {len(only_g)} чел. ← разность B∖A") Формула включений-исключений: |A|+|B|-|A∩B| = {len(either)}") Проверка: 20 + 15 - {len(both)} = {20 + 15 - len(both)} ✓") Здесь мы встречаемся с пересечением, объединением и разностью множеств, что весьма полезно и кстати. А ответ на задачу такой: Читали Читали Читали Читали Только Только «Робинзона»: «Гулливера»: обе книги: хотя бы одну: «Робинзона»: «Гулливера»: 20 чел. 15 чел. 5 чел. 30 чел. 15 чел. 10 чел. ← ← ← ← пересечение ∩ объединение ∪ разность A∖B разность B∖A Формула включений-исключений: |A|+|B|-|A∩B| = 30 152
Проверка: 20 + 15 - 5 = 30 ✓ Проект Сложные числа Пусть у нас есть N чисел. Нам нужно найти числа, которые делятся на 2, 3 или 5, но не делятся ни на 7, ни на 11. Для начала создадим множества чисел, которые делятся на предложенные числа: N = 100 nums = set(range(1, N + 1)) div2 div3 div5 div7 div11 = = = = = {n {n {n {n {n for for for for for n n n n n in in in in in nums nums nums nums nums if if if if if n n n n n % % % % % 2 3 5 7 11 == == == == == 0} 0} 0} 0} 0} Находим объединение множеств div2, div3, div5 и вычитаем из них множества div7 и div11: # Делятся на 2, 3 или 5 — но не на 7 и не на 11 result = (div2 | div3 | div5) - div7 - div11 result_sorted = sorted(result) Задача решена: print(f" Делятся на 2: {len(div2)} чисел") print(f" Делятся на 3: {len(div3)} чисел") print(f" Делятся на 5: {len(div5)} чисел") print(f" Делятся на 2,3 или 5:{len(div2|div3|div5)} чисел (с повторами: {len(div2)+len(div3)+len(div5)})") print(f" Из них кратны 7: {len((div2|div3|div5) & div7)} чисел (исключаем)") print(f" Из них кратны 11: {len((div2|div3|div5) & div11)} чисел (исключаем)") 153
print(f" print(f" Итог: {len(result)} чисел") Первые 20: {result_sorted[:20]}") Делятся на 2: 50 чисел Делятся на 3: 33 чисел Делятся на 5: 20 чисел Делятся на 2,3 или 5:74 чисел (с повторами: 103) Из них кратны 7: 10 чисел (исключаем) Из них кратны 11: 7 чисел (исключаем) Итог: 57 чисел Первые 20: [2, 3, 4, 5, 6, 8, 9, 10, 12, 15, 16, 18, 20, 24, 25, 26, 27, 30, 32, 34] Проект Шесть рукопожатий Нам нужно найти кратчайшую цепочку знакомств в заданном множестве людей, то есть найти знакомых для заданной пары человек. Поскольку в задаче речь идёт о кратчайшей цепочке, то мы воспользуемся алгоритмом BFS (Breadth-First Search), то есть поиском в ширину. Всех знакомых мы упаковали в словарь: # Граф знакомств (словарь: человек → множество знакомых) acquaintances = { "Аня": {"Борис", "Вера", "Гена"}, "Борис": {"Аня", "Дима", "Елена"}, "Вера": {"Аня", "Жора"}, "Гена": {"Аня", "Зина"}, "Дима": {"Борис", "Ира"}, "Елена": {"Борис", "Коля"}, "Жора": {"Вера", "Лёша"}, "Зина": {"Гена", "Миша"}, "Ира": {"Дима", "Надя"}, "Коля": {"Елена", "Оля"}, "Лёша": {"Жора", "Петя"}, "Миша": {"Зина", "Рита"}, "Надя": {"Ира"}, 154
} "Оля": "Петя": "Рита": {"Коля"}, {"Лёша"}, {"Миша"}, Для проверки работы программы составляем произвольные пары и в функции handshakes ищем кратчайший путь от знакомых к знакомым так, чтобы связать заданную пару людей: def handshakes(graph: dict, start: str, end: str) -> list[str] | None: """BFS — поиск кратчайшего пути через множества посещённых вершин.""" visited = {start} # очередь путей queue = [[start]] while queue: path = queue.pop(0) node = path[-1] if node == end: return path for neighbour in graph.get(node, set()): if neighbour not in visited: visited.add(neighbour) queue.append(path + [neighbour]) return None pairs = [("Аня", "Рита"), ("Аня", "Петя"), ("Борис", "Надя")] for a, b in pairs: path = handshakes(acquaintances, a, b) if path: chain = " → ".join(path) print(f" {a} ↔ {b}: {len(path)-1} рукопожатия {chain}") else: print(f" {a} ↔ {b}: не связаны") В итоге все быстро подружились: Аня ↔ Рита: 4 рукопожатия Аня → Гена → Зина → Миша → Рита Аня ↔ Петя: 4 рукопожатия Аня → Вера → Жора → Лёша → Петя Борис ↔ Надя: 3 рукопожатия Борис → Дима → Ира → Надя 155
Проект Решето Эратосфена 156
Эратосфен пропускал натуральные числа через сито так, что через него проходили только простые числа. На самом деле Эратосфен схитрил: он не просеивал числа, а вычёркивал их в таблице, что и для нас дюже хорошо! def sieve(n: int) -> set[int]: candidates = set(range(2, n + 1)) for p in range(2, int(n**0.5) + 1): if p in candidates: # Вычёркиваем все кратные p (кроме самого p) candidates -= set(range(p * p, n + 1, p)) return candidates for limit in [50, 100, 1000]: primes = sieve(limit) print(f" До {limit:4d}: {len(primes):3d} простых чисел", end="") if limit <= 50: print(f" → {sorted(primes)}") else: top5 = sorted(primes)[-5:] print(f" (последние 5: {top5})") Раззудив плечо, мы найдём простые числа-близнецы, которые находятся неподалёку друг от друга (ближе только простые числа 2 и 3): # Простые-близнецы (разница = 2) primes_1000 = sieve(1000) twins = {(p, p+2) for p in primes_1000 if (p+2) in primes_1000} print(f"\n Простые-близнецы до 1000: {len(twins)} пар") print(f" Первые 8: {sorted(twins)[:8]}") Просеивание идёт быстро, и вот они – простые числа: До 50: 15 простых чисел 41, 43, 47] До 100: 25 простых чисел До 1000: 168 простых чисел → [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, (последние 5: [73, 79, 83, 89, 97]) (последние 5: [971, 977, 983, 991, 997]) 157
Простые-близнецы до 1000: 35 пар Первые 8: [(3, 5), (5, 7), (11, 13), (17, 19), (29, 31), (41, 43), (59, 61), (71, 73)] 158
Проект Проверяем судоку У нас есть уже решённое судоку, и мы проверяем решение, учитывая, что в каждой горизонтали, вертикали и в каждом квадрате 3 х 3 клетки должно быть ровно 9 разных цифр: VALID = set(range(1, 10)) def check_sudoku(grid: list[list[int]]) -> tuple[bool, list[str]]: errors = [] # Строки for i, row in enumerate(grid): if set(row) != VALID: errors.append(f"строка {i+1}: {row}") # Столбцы for j in range(9): col = [grid[i][j] for i in range(9)] if set(col) != VALID: errors.append(f"столбец {j+1}: {col}") # Блоки 3×3 for bi in range(3): for bj in range(3): block = [grid[bi*3+r][bj*3+c] for r in range(3) for c in range(3)] if set(block) != VALID: errors.append(f"блок ({bi+1},{bj+1}): {block}") return (len(errors) == 0), errors # Правильное решение correct = [ [5,3,4, 6,7,8, 9,1,2], [6,7,2, 1,9,5, 3,4,8], [1,9,8, 3,4,2, 5,6,7], [8,5,9, 7,6,1, 4,2,3], [4,2,6, 8,5,3, 7,9,1], [7,1,3, 9,2,4, 8,5,6], [9,6,1, 5,3,7, 2,8,4], [2,8,7, 4,1,9, 6,3,5], [3,4,5, 2,8,6, 1,7,9], ] # Неправильное (меняем две цифры) wrong = [row[:] for row in correct] wrong[0][0], wrong[0][1] = wrong[0][1], wrong[0][0] # меняем в строке 159
wrong[4][4] = 0 # убираем цифру for label, grid in [("Правильное", correct), ("Неправильное", wrong)]: ok, errs = check_sudoku(grid) status = "✓ ВЕРНО" if ok else f"✗ ОШИБКИ ({len(errs)})" print(f" {label}: {status}") for e in errs[:3]: print(f" → {e}") Результаты проверки: Правильное: ✓ ВЕРНО Неправильное: ✗ ОШИБКИ (5) → строка 5: [4, 2, 6, 8, 0, 3, 7, 9, 1] → столбец 1: [3, 6, 1, 8, 4, 7, 9, 2, 3] → столбец 2: [5, 7, 9, 5, 2, 1, 6, 8, 4] Проект Анаграммы Анаграммы – это слова, которые состоят из одних и тех же букв в равных количествах. # ЗАГРУЖАЕМ СЛОВАРЬ def load(fn): with open(fn) as file: spisok = file.readlines() # убираем символ переноса строки: spisok = [s.rstrip('\n') for s in spisok] return spisok from collections import defaultdict def anagram_key(word: str) -> frozenset: """Ключ анаграммы — frozenset пар (буква, число_вхождений).""" freq = defaultdict(int) for ch in word: freq[ch] += 1 return frozenset(freq.items()) 160
groups = defaultdict(list) # словарь: fn = 'OSH-W97_frc.txt' wordlist = load(fn) for word in wordlist: groups[anagram_key(word)].append(word) anagram_groups = {k: v for k, v in groups.items() if len(v) > 1} for key, grp in sorted(anagram_groups.items(), key=lambda x: -len(x[1])): letters = "".join(sorted(set(ch for ch, _ in key))) print(f" {{{letters}}} → {grp}") Самые обильные блоки анаграмм: {АВОРТ} → ['АВТОР', 'ВТОРА', 'ОТВАР', 'РВОТА', 'ТАВРО', 'ТОВАР'] {ОРСТ} → ['РОСТ', 'СОРТ', 'ТОРС', 'ТРОС'] {АБОРТ} → ['АБОРТ', 'ОБРАТ', 'ТАБОР', 'ТОРБА'] {АЕКСТ} → ['АСКЕТ', 'СЕКТА', 'СЕТКА', 'ТЕСАК'] {КЛНОУ} → ['КЛОУН', 'КОЛУН', 'КУЛОН', 'УКЛОН'] {ИКОПРТ} → ['ПОРТИК', 'ПОРТКИ', 'ПРИТОК', 'ТРОПИК'] 161
Проект Покрытие множества 162
У нас есть 9 радиостанций. Нам нужно выбрать минимум радиостанций, чтобы они покрыли все регионы: # Какие регионы покрывает каждая радиостанция stations = { "Радио-1": {"Москва", "Тверь", "Ярославль"}, "Радио-2": {"Тверь", "Псков", "Новгород"}, "Радио-3": {"Ярославль", "Кострома", "Иваново"}, "Радио-4": {"Псков", "Смоленск", "Брянск"}, "Радио-5": {"Новгород", "Вологда", "Архангельск"}, "Радио-6": {"Кострома", "Нижний Новгород", "Владимир"}, "Радио-7": {"Иваново", "Владимир", "Рязань"}, "Радио-8": {"Брянск", "Орёл", "Курск"}, "Радио-9": {"Смоленск", "Калуга"}, } all_regions = set().union(*stations.values()) print(f" Всего регионов: {len(all_regions)}") print(f" Радиостанций: {len(stations)}") print() # Жадный алгоритм: на каждом шаге выбираем станцию, # покрывающую максимум ещё не охваченных регионов uncovered = set(all_regions) chosen = [] remaining = dict(stations) while uncovered: best_station = max(remaining, key=lambda s: len(remaining[s] & uncovered)) covered_now = remaining[best_station] & uncovered chosen.append((best_station, covered_now)) uncovered -= covered_now del remaining[best_station] print(f" Жадное решение ({len(chosen)} станций):") for station, covered in chosen: print(f" {station:<12} покрывает: {sorted(covered)}") print(f"\n Все регионы охвачены: " f"{'✓' if not uncovered else '✗ осталось: '+str(uncovered)}") Следуя науке, мы обошлись великолепной семёркой радиостанций: Всего регионов: 17 163
Радиостанций: Жадное решение Радио-1 Радио-4 Радио-5 Радио-6 Радио-7 Радио-8 Радио-9 9 (7 станций): покрывает: ['Москва', 'Тверь', 'Ярославль'] покрывает: ['Брянск', 'Псков', 'Смоленск'] покрывает: ['Архангельск', 'Вологда', 'Новгород'] покрывает: ['Владимир', 'Кострома', 'Нижний Новгород'] покрывает: ['Иваново', 'Рязань'] покрывает: ['Курск', 'Орёл'] покрывает: ['Калуга'] Все регионы охвачены: ✓ 164
Проект Проверяем магический квадрат Эта задача сродни проверке судоку, но в магическом квадрате должны быть равны суммы чисел в горизонталях, вертикалях и в двух диагоналях: def check_magic(sq: list[list[int]]) -> tuple[bool, int | None, set]: n = len(sq) nums = {sq[i][j] for i in range(n) for j in range(n)} # Все числа от 1 до n² — проверка через множество if nums != set(range(1, n*n + 1)): return False, None, nums sums = set() # строки for row in sq: sums.add(sum(row)) # столбцы for j in range(n): sums.add(sum(sq[i][j] for i in range(n))) # главная диагональ sums.add(sum(sq[i][i] for i in range(n))) # побочная диагональ sums.add(sum(sq[i][n-1-i] for i in range(n))) magic_sum = n * (n*n + 1) // 2 return (sums == {magic_sum}), magic_sum, sums squares = { "3×3 (Lo Shu)": [ [2,7,6], [9,5,1], [4,3,8], ], "4×4 (Дюрер)": [ [16, 3, 2,13], [ 5,10,11, 8], [ 9, 6, 7,12], [ 4,15,14, 1], ], "3×3 (неверный)": [ [1,2,3], [4,5,6], [7,8,9], ], 165
} for name, sq in squares.items(): ok, magic_sum, sums = check_magic(sq) status = f"✓ S = {magic_sum}" if ok else f"✗ print(f" {name:<20} {status}") for row in sq: print(f" {row}") print() суммы = {sums}" 3×3 (Lo Shu) [2, 7, 6] [9, 5, 1] [4, 3, 8] ✓ S = 15 4×4 (Дюрер) [16, 3, 2, 13] [5, 10, 11, 8] [9, 6, 7, 12] [4, 15, 14, 1] ✓ S = 34 3×3 (неверный) [1, 2, 3] [4, 5, 6] [7, 8, 9] ✗ суммы = {6, 12, 15, 18, 24} Проект Разбиение на непересекающиеся множества Нам нужно определить, кто с кем в одной команде, если известны пары знакомых спортсменов. from collections import defaultdict class UnionFind: def __init__(self, items): self.parent = {x: x for x in items} self.rank = {x: 0 for x in items} def find(self, x): 166
if self.parent[x] != x: self.parent[x] = self.find(self.parent[x]) return self.parent[x] def union(self, x, y): rx, ry = self.find(x), self.find(y) if rx == ry: return if self.rank[rx] < self.rank[ry]: rx, ry = ry, rx self.parent[ry] = rx if self.rank[rx] == self.rank[ry]: self.rank[rx] += 1 def components(self) -> list[set]: groups = defaultdict(set) for x in self.parent: groups[self.find(x)].add(x) return list(groups.values()) people = list("АБВГДЕЖЗИК") pairs = [("А","Б"), ("Б","В"), ("Г","Д"), ("Е","Ж"), ("Ж","З"), ("З","И"), ("К","А")] uf = UnionFind(people) for a, b in pairs: uf.union(a, b) components = uf.components() print(f" Связей задано: {len(pairs)}") print(f" Компонент (групп): {len(components)}") for i, comp in enumerate(sorted(components, key=lambda s: -len(s)), 1): print(f" Группа {i}: {sorted(comp)}") # Проверка принадлежности к одной группе через множества print() checks = [("А","В"), ("А","Г"), ("Е","И")] for a, b in checks: same = uf.find(a) == uf.find(b) print(f" {a} и {b} в одной группе: {'✓ да' if same else '✗ нет'}") Связей задано: 7 Компонент (групп): 3 Группа 1: ['А', 'Б', 'В', 'К'] Группа 2: ['Е', 'Ж', 'З', 'И'] Группа 3: ['Г', 'Д'] А и В в одной группе: ✓ да А и Г в одной группе: ✗ нет Е и И в одной группе: ✓ да 167
Проект Парадокс дней рождения 168
Матьимачехи утверждают, что в группе из N человек вероятность совпадения хотя бы двух дней рождения больше 50% уже при N = 23, а при 57 человеках эта вероятность превышает 99 %. В этом проекте мы проверим это утверждение с помощью случайных экспериментов с множествами. import random def birthday_probability(group_size: int, trials: int = 10_000) -> float: """Оценка вероятности через симуляцию. Совпадение есть, если множество дней рождения меньше, чем число людей.""" hits = sum( 1 for _ in range(trials) if len({random.randint(1, 365) for _ in range(group_size)}) < group_size ) return hits / trials print(f" print(f" {'N':>4} {'Теория':>8} {'Симуляция':>10} {'-'*4} {'-'*8} {'-'*10} {'-'*20}") Вывод") import math def theoretical(n: int) -> float: """Точная формула через произведение.""" prob_no_match = 1.0 for k in range(n): prob_no_match *= (365 - k) / 365 return 1 - prob_no_match for n in [10, 15, 20, 23, 30, 40, 50, 70]: theory = theoretical(n) sim = birthday_probability(n, trials=20_000) flag = "← перевал 50%!" if abs(theory - 0.5) < 0.05 else "" print(f" {n:>4} {theory:>7.1%} {sim:>9.1%} {flag}") N ---10 15 20 Теория -------11.7% 25.3% 41.1% Симуляция ---------11.8% 25.2% 41.5% Вывод -------------------- 169
23 30 40 50 70 50.7% 70.6% 89.1% 97.0% 99.9% 50.8% 70.8% 88.9% 97.2% 100.0% ← перевал 50%! Обычным людям трудно в это поверить, ведь в году 365 дней, поэтому для такого совпадения нужна гораздо большая группа людей. Но парадокс раскрывается просто: мы сравниваем не одного человека со всеми, а все возможные пары людей друг с другом. Люди часто путают две разные задачи: - Какова вероятность, что кто-то в группе родился в мой день рождения?» — это сравнение одного человека с остальными. Для группы из 23 человек такая вероятность невелика (примерно 6 %). - Какова вероятность, что любые двое в группе родились в один день? — тут мы смотрим на все пары. А их намного больше, чем кажется. В группе из 23 человек можно составить 23×22/2=253 пары. То есть у нас не 23 «шанса», а 253 сравнения, и это резко повышает вероятность совпадения. 170
Списки (Lists) 171
Список – это изменяемая коллекция (последовательность) упорядоченных элементов разных типов, которые могут повторяться. На самом деле списки в Питоне – это динамические массивы. Они хранят не объекты разных типов, а адреса этих объектов в памяти, то есть все элементы списка имеют одинаковый тип. Ссылка – это переменная, которая хранит адрес объекта в памяти компьютера. Объявление и определение списков Чтобы объявить списочную переменную, напишите имя переменной, поставьте двоеточие и укажите тип list: имя_переменной : list После объявления переменной её нельзя использовать до инициализации. Пустой список можно определить так: имя_переменной = [] То есть после знака присваивания просто записать пару квадратных скобок, и тогда переменная получит правильный тип: lst = [] print(type(lst)) print(len(lst)) print() 172
Обычно список инициализируют сразу при определении. Тогда в квадратных скобках через запятую нужно перечислить элементы списка. Элементы списка могут иметь разный тип: lst = [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] print(*lst) print(f'Длина списка = {len(lst)}') Элементами списка могут быть выражения и переменные: a = 2022 b = '2022' lst = [2 * 2, a, b] print(lst) print(*lst) Второй способ создания списков – использование функции list, которая создаёт и возвращает список. Функция без параметров создаёт пустой список: lst = list() print(len(lst)) Этой функции можно передать итерируемый объект, то есть последовательность - упорядоченный объект - или множество – неупорядоченный объект. Последовательностями являются строки, кортежи и списки. Вся строка разбивается на отдельные символы, поэтому мы легко сможем получить все буквы заданного слова: # строка: lst = list('1 2 3 4 5 вышел зайчик погулять') print(lst) print(*lst) print(len(lst)) 173
['1', ' ', '2', ' ', '3', ' ', '4', ' ', '5', ' ', 'в', 'ы', 'ш', 'е', 'л', ' ', 'з', 'а', 'й', 'ч', 'и', 'к', ' ', 'п', 'о', 'г', 'у', 'л', 'я', 'т', 'ь'] 1 2 3 4 5 в ы ш е л з а й ч и к п о г у л я т ь 31 # кортеж: t = (1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять') lst = list(t) print(lst) print(*lst) print(len(lst)) [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] 1 2 3 4 5 вышел зайчик погулять 8 # список: lst1 = [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] lst = list(lst1) print(lst) print(*lst) print(len(lst)) # множество: set = {1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'} lst = list(set) print(lst) print(*lst) print(len(lst)) Обратите внимание, что в множестве элементы не упорядочены. Функция range возвращает последовательность чисел, поэтому её также можно использовать при создании списков: 174
lst = list(range(10)) print(lst) print(*lst) print(len(lst)) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 0 1 2 3 4 5 6 7 8 9 10 Метод copy возвращает копию списка: .copy() -> list lst = [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] print(lst) lst2 = lst.copy() print(lst2) [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] Обратите внимание, что, присвоив переменную lst переменной lst2, вы получите 2 переменные, которые указывают на один и тот же список, поэтому изменение элемента в одной переменной вызовет изменение и в другой: lst = [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] print(lst) lst2 = lst print(lst2) lst[0] = 'один' 175
print(lst) print(lst2) print() print() [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] ['один', 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] ['один', 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] При копировании списка переменные будут указывать на разные списки, поэтому изменения в одном списке не затронут другой список: lst = [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] print(lst) lst2 = lst.copy() print(lst2) lst[0] = 'один' print(lst) print(lst2) [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] ['один', 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] 176
Генераторы списков 177
Пусть нам нужен список факториалов. Мы можем добавлять факториалы в обычном цикле for, но удобнее использовать для этого генератор списков. Функция factorial из модуля math избавляет нас от необходимости вычислять факториалы самостоятельно: from math import factorial В правой части инструкции присваивания пишем в квадратных скобках значение факториала числа n, а затем пишем цикл for для переменной цикла n: def main(): while True: num = int(input('Введите число > ')) # генерируем список факториалов: facts = [factorial(n) for n in range(num)] В цикле for переменная n последовательно получает значения 0..num-1. Текущее значение переменной в данном случае – это номер факториала в последовательности. Для каждого номера n вычисляется значение factorial(n). Обратите внимание, что facts это не список факториалов, а генератор списка. Генератор должен быть вызван, чтобы он выдал элементы списка: # печатаем список: id = 0 for f in facts: print(f'{id} --> {f}') id += 1 print() Генератор списка называют также списковым включением. 178
Всё верно. Введите число > 11 0 --> 1 1 --> 1 2 --> 2 3 --> 6 4 --> 24 5 --> 120 6 --> 720 7 --> 5040 8 --> 40320 9 --> 362880 10 --> 3628800 В общем виде генератор списков можно представить так: имя_списка = [выражение for элемент in коллекция] Квадратные скобки указывают границы генератора списка. Выражение включает элемент списка, над которым нужно выполнить какие-либо операции. В простейшем случае это может быть просто элемент списка. Коллекцией могут быть функция range, список, строка. Генератору списка соответствует цикл for: имя_списка = [] for элемент in коллекция: имя_списка.append(выражение) В программировании на Питоне обычно используют генераторы, а не циклы, но вы можете выбрать тот способ, который вам больше нравится. 179
В генератор списка можно включить и некоторое условие, которое «фильтрует» элементы словаря. Например, мы хотим выбрать из списка слов только двухбуквенные слова: # ЗАГРУЖАЕМ СЛОВАРЬ def load(fn): with open(fn) as file: spisok = file.readlines() # убираем символ переноса строки: return [s.rstrip() for s in spisok] def main(): # словарь: fileName = 'OSH-W97.txt' # загружаем словарь: spisok = load(fileName) spisok = [s for s in spisok if len(s) == 2] print(spisok) main() ['АД', 'АЗ', 'АР', 'АС', 'ГО', 'ДО', 'ЁЖ', 'ИЛ', 'ЛЯ', 'МИ', 'ОМ', 'ОР', 'ПА', 'РЕ', 'РЭ', 'СИ', 'СУ', 'УЖ', 'УМ', 'УС', 'ФА', 'ЩИ', 'ЮГ', 'ЮЗ', 'ЮР', 'ЮС', 'ЯД', 'ЯК', 'ЯЛ', 'ЯМ', 'ЯР'] В общем виде генератор списка с условием можно представить так: имя_списка = [выражение for элемент in коллекция if условие] Ему соответствует цикл for: имя_списка = [] for элемент in коллекция: if условие: 180
имя_списка.append(выражение) Вложенные циклы позволяют создавать списки кортежей: list_t = [(a, b) for a in [2, 3, 4, 5] for b in [6, 7, 8, 9]] for t in list_t: print(t) (2, (2, (2, (2, (3, (3, (3, (3, 6) 7) 8) 9) 6) 7) 8) 9) (4, (4, (4, (4, (5, (5, (5, (5, 6) 7) 8) 9) 6) 7) 8) 9) Естественно, можно обойтись без кортежей, а использовать элементы двух списков, например, для вычисления всех произведений в таблице умножения: tabl = [a * b for a in [2, 3, 4, 5, 6, 7, 8, 9] for b in [2, 3, 4, 5, 6, 7, 8, 9]] for r in tabl: print(r, end=' ') print() 4 6 8 10 12 14 16 18 6 9 12 15 18 21 24 27 8 12 16 20 24 28 32 36 10 15 20 25 30 35 40 45 12 18 24 30 36 42 48 54 14 21 28 35 42 49 56 63 16 24 32 40 48 56 64 72 18 27 36 45 54 63 72 81 Свойства списков Метод append добавляет новый элемент в конец списка: .append(object) -> None 181
lst = [1,2,3,4,5] print(lst) lst.append(6) print(lst) Метод insert добавляет новый элемент в указанное место списка: .insert(index, object) -> None lst = [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] print(lst, len(lst)) lst.insert(1, 'один') lst.insert(6, 'пять') print(lst, len(lst)) [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] 8 [1, 'один', 2, 3, 4, 5, 'пять', 'вышел', 'зайчик', 'погулять'] 10 Метод sort сортирует список по возрастанию значений элементов - на месте: . sort() -> None import random lst = [random.randint(1, 99) for _ in range(10)] print(lst) lst.sort() print(lst) 182
Но если параметру reverse присвоить значение True, то элементы будут отсортированы в обратном порядке: lst = [random.randint(1, 99) for _ in range(10)] print(lst) lst.sort(reverse=True) print(lst) Параметру key можно присвоить функцию, которая применяется ко всем элементам списка и определяет порядок сортировки. Например, мы можем отсортировать элементы списка по возрастанию последних цифр чисел: lst = [random.randint(1, 99) for _ in range(10)] print(lst) lst.sort(key=lambda n : n % 10) print(lst) Чтобы сохранить исходный список после сортировки, используйте функцию sorted, которая возвращает новый, отсортированный список: lst = [random.randint(1, 99) for _ in range(10)] print(lst) lst2 = sorted(lst) print(lst2) print(lst) Метод reverse обращает порядок элементов списка на месте: .reverse() -> None lst = [random.randint(1, 99) for _ in range(10)] print(lst) lst.reverse() print(lst) 183
Списки можно объединить в один список с помощью оператора +. В новом списке сначала идут элементы первого списка, а затем второго. Порядок элементов при этом не нарушается: lst = [random.randint(1, 99) for _ in range(10)] print(lst) lst2 = sorted(lst) print(lst2) lst3 = lst + lst2 print(lst3) [40, 64, 79, 69, 99, 21, 83, 7, 26, 21] [7, 21, 21, 26, 40, 64, 69, 79, 83, 99] [40, 64, 79, 69, 99, 21, 83, 7, 26, 21, 7, 21, 21, 26, 40, 64, 69, 79, 83, 99] Метод extend добавляет заданную последовательность к списку. Порядок элементов не нарушается, но новый список не создаётся: print() lst = [random.randint(1, 99) for _ in range(10)] print(lst) lst2 = sorted(lst) print(lst2) lst.extend(lst2) print(lst) [13, 61, 76, 72, 20, 6, 89, 26, 85, 37] [6, 13, 20, 26, 37, 61, 72, 76, 85, 89] [13, 61, 76, 72, 20, 6, 89, 26, 85, 37, 6, 13, 20, 26, 37, 61, 72, 76, 85, 89] Если умножить список на натуральное число n, то можно получить новый список, в котором элементы исходного списка повторяются n раз: print() lst = [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] 184
print(lst) lst2 = lst * 2 print(lst2) [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять', 1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] Метод pop удаляет элемент по указанному индексу и возвращает его: pop(index) -> object print() lst = [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] print(lst, len(lst)) e = lst.pop(5) print(e) print(lst, len(lst)) [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] 8 вышел [1, 2, 3, 4, 5, 'зайчик', 'погулять'] 7 Если индекс не указан, то удаляется последний элемент списка: print() lst = [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] print(lst, len(lst)) e = lst.pop() print(e) print(lst, len(lst)) [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] 8 погулять 185
[1, 2, 3, 4, 5, 'вышел', 'зайчик'] 7 Метод remove удаляет указанный элемент (первый, если таких элементов несколько): .remove (element) -> object print() lst = [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] print(lst, len(lst)) lst.remove('зайчик') print(lst, len(lst)) [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] 8 [1, 2, 3, 4, 5, 'вышел', 'погулять'] 7 Если элемент отсутствует, возникнет ошибка. Инструкция del удаляет указанный элемент списка: print() lst = [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] print(lst) del lst[6] print(lst) [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] [1, 2, 3, 4, 5, 'вышел', 'погулять'] Так можно удалить несколько элементов списка: 186
print() lst = [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] print(lst) del lst[5 : ] print(lst) [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] [1, 2, 3, 4, 5] print() lst = [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] print(lst) del lst[ : : 2] print(lst) [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] [2, 4, 'вышел', 'погулять'] Метод clear удаляет все элементы списка: .clear() -> None print() lst = [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] print(lst, len(lst)) lst.clear() print(lst, len(lst)) [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] 8 [] 0 187
Функция len возвращает число элементов в списке: print() lst = [random.randint(1, 99) for _ in range(10)] print(lst, len(lst)) lst2 = sorted(lst) print(lst2, len(lst2)) lst.extend(lst2) print(lst, len(lst)) [83, 8, 12, 8, 45, 92, 26, 14, 62, 6] 10 [6, 8, 8, 12, 14, 26, 45, 62, 83, 92] 10 [83, 8, 12, 8, 45, 92, 26, 14, 62, 6, 6, 8, 8, 12, 14, 26, 45, 62, 83, 92] 20 Метод index возвращает индекс первого элемента списка с указанным значением: .index(значение) -> int print() lst = [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] print(lst, len(lst)) id = lst.index('зайчик') print(id) [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] 8 6 Дополнительно можно задать индекс start, с которого начнётся поиск элемента: .index(значение, start) -> int 188
print() lst = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 5] print(lst, len(lst)) id = lst.index(4, 5) print(id) [1, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 5] 15 6 По умолчанию поиск ведётся до конца списка, но его можно ограничить, присвоив параметру stop нужное значение: .index(значение, start, stop) -> int Если заданного элемента в списке нет, возникнет ошибка: lst = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 5] print(lst, len(lst)) id = lst.index(7) print(id) В таких случаях нужно предварительно проверять существование элемента в списке оператором in: print() lst = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 5] print(lst, len(lst)) if 7 in lst: id = lst.index(7) print(id) Метод count возвращает число элементов списка с указанным значением: 189
.count(значение) -> int print() lst = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 5] print(lst) n = lst.count(3) print(n) [1, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 5] 3 Если элемент отсутствует метод вернёт 0. Функция max возвращает наибольший элемент списка: print() lst = [random.randint(1, 99) for _ in range(10)] print(lst) e = max(lst) print(e, lst) [8, 85, 88, 88, 39, 10, 1, 21, 74, 44] 88 [8, 85, 88, 88, 39, 10, 1, 21, 74, 44] Нужно учитывать, что элементы должны быть таких типов, чтобы их можно было сравнивать между собой. Например, нельзя сравнить числа и строки. В таких случаях мы можем присвоить параметру key функцию, которая применяется ко всем элементам последовательности: print() lst = [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] print(lst) 190
e = max(lst, key = lambda e: str(e)) print(e, lst) [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] погулять [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] Функция min действует аналогично, но возвращает наименьший элемент списка: print() lst = [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] print(lst) e = min(lst, key = lambda e: str(e)) print(e, lst) [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] 1 [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] Список – это упорядоченная последовательность элементов, поэтому мы можем получить элемент по его индексу/номеру в списке: print() lst = [random.randint(1, 99) for _ in range(10)] print(lst, len(lst)) for i in range(len(lst)): print(lst[i], end=' ') print() [5, 4, 59, 63, 27, 92, 88, 29, 86, 85] 10 5 4 59 63 27 92 88 29 86 85 191
lst = [random.randint(1, 99) for _ in range(10)] print(lst, len(lst)) for i in range(1, len(lst), 2): print(lst[i], end=' ') print() [96, 3, 79, 35, 69, 75, 54, 31, 67, 70] 10 3 35 75 31 70 Все элементы списка можно перебрать в цикле for по индексу: print() lst = [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] print(lst, len(lst)) for i in range(len(lst)): print(f'{i} -> {lst[i]}') print() [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] 8 0 -> 1 1 -> 2 2 -> 3 3 -> 4 4 -> 5 5 -> вышел 6 -> зайчик 7 -> погулять Или в цикле for-in непосредственно по значению: print() lst = [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] print(lst, len(lst)) for e in lst: 192
print(e) print() [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] 8 1 2 3 4 5 вышел зайчик погулять Важное отличие: в цикле for можно изменять значения элементов, а в цикле for-in нет: print() lst = [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] print(lst, len(lst)) for i in range(len(lst)): print(f'{i} -> {lst[i]}') lst[i] = str(lst[i]) + '!' print() print(lst) ['1!', '2!', '3!', '4!', '5!', 'вышел!', 'зайчик!', 'погулять!'] Список – это изменяемая последовательность элементов, поэтому мы можем присвоить любому элементу по его индексу/номеру в списке новое значение: print() lst = [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] print(lst) 193
lst[6] = 'кролик' print(lst) [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] [1, 2, 3, 4, 5, 'вышел', 'кролик', 'погулять'] С помощью среза можно одновременно изменить значения нескольких элементов списка: print() lst = [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] print(lst) lst[5 : ] = ('Винни', 'любит', 'мёд') print(lst) [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] [1, 2, 3, 4, 5, 'Винни', 'любит', 'мёд'] Если значений на замену окажется меньше, то список будет урезан. Если значений на замену окажется больше, то список будет расширен. Значения могут быть представлены не только кортежем, но и списком. Этим способом можно удалить несколько элементов списка: print() lst = [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] print(lst) lst[ : 5] = [] print(lst) [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] ['вышел', 'зайчик', 'погулять'] 194
Срезы списков 195
Срезы списков выполняются так же, как срезы строк, поэтому мы только обобщим знания: lst[:] lst[start:] lst[start:stop] lst[:stop] → → → → все все все все элементы элементы от start до конца списка элементы с индексами start..stop-1 элементы от начала до stop-1 lst = [0,1,2,3,4,5,6,7,8,9] res = lst[:] print(res) res = lst[1:] print(res) res = lst[1:7] print(res) res = lst[:7] print(res) → последний элемент списка → последние n элементов списка → все элементы списка, кроме n последних lst[-1] lst[-n:] lst[:-n] res = lst[-1] print(res) res = lst[-3:] print(res) res = lst[:-3] print(res) lst[::-1] lst[n::-1] lst[:-n:-1] lst[-n::-1] → → → → все элементы в обратном порядке первые n+1 элементов списка в обратном порядке последние n-1 элементов списка в обратном порядке все элементы, кроме n-1 последних, в обратном порядке 196
Срез возвращает новый список! Индексация элементов списка в прямом и в обратном направлении. P 0 -6 y 1 -5 t 2 -4 h 3 -3 o 4 -2 n 5 -1 Вместо срезов можно использовать объект slice: res = lst[slice(None, None)] print(res) res = lst[slice(1, None)] print(res) res = lst[slice(1, 7)] res = lst[1:7] print(res) res = lst[slice(None, 7)] print(res) Параметры в срезе разделяются двоеточиями, а в slice – запятыми. В срезе отсутствующие параметры просто пропускают, а в slice нужно указывать значение None: lst[start:stop:step] lst[slice(start,stop,step)] Списки списков В Питоне нет двумерных массивов, поэтому их заменяют списками списков, которые можно легко создать в генераторе вложенных списков: 197
rows = 3 cols = 5 matrix = [[0] * cols for _ in range(rows)] for row in matrix: print(row) Доступ к элементам двумерного массива выполняется так: matrix[1][4] = 'X' for row in matrix: print(row) В первых квадратных скобках после имени списка списков указывается индекс строки, а во вторых квадратных скобках – индекс столбца, или элемента в этой строке. Если у нас есть несколько списков, то мы можем создать новый список списков перечислением списков через запятую в квадратных скобках: import random print() lst1 = [random.randint(1, 99) for _ in range(10)] print(lst1) lst2 = [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] print(lst2) res = [lst1, lst2] print(res) [72, 64, 61, 72, 68, 75, 38, 79, 29, 94] [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] [[72, 64, 61, 72, 68, 75, 38, 79, 29, 94], [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять']] 198
Изменяемые и неизменяемые объекты Объекты в Питоне можно разделить на неизменяемые (immutable) и изменяемые (mutable). Неизменяемые объекты – это экземпляры следующих классов: • • • • • • • int float bool str tuple complex None Изменяемые объекты – это экземпляры следующих классов: • list • set • dict Давайте создадим целочисленную переменную n и проверим её адрес в памяти компьютера. Или неизменяемый объект любого другого типа. def main(): # целочисленная переменная: n = 11 # её адрес в памяти: addr = id(n) print(addr) 199
Затем изменим значение переменной и снова проверим её адрес. Запускаем программу и видим, что при изменении значения адрес переменной изменился: # изменяем значение переменной: n = 12 # её адрес в памяти: addr = id(n) print(addr) if __name__ == '__main__': main() Это значит, что мы не изменили значение существующей переменной, а определили новую переменную с тем же именем. При каждом изменении значения неизменяемого объекта создаётся новый объект с тем же именем, но с другим значением. Если значение неизменяемого объекта не изменяется, то новая переменная не создаётся: # целочисленная переменная: n = 11 # её адрес в памяти: addr = id(n) print(addr) # изменяем значение переменной: n = 11 # её адрес в памяти: addr = id(n) print(addr) Список – это изменяемый объект, поэтому создаём список и печатаем его адрес. Затем изменяем значение любого элемента списка и снова печатаем 200
его адрес. Легко видеть, что при изменении списка его адрес в памяти сохраняется, то есть новая переменная не создаётся: # создаём список: lst = [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] print(lst) # его адрес в памяти: addr = id(lst) print(addr) # изменяем значение элемента списка: lst[6] = 'КРОЛИК' print(lst) # его адрес в памяти: addr = id(lst) print(addr) print() Значимые и ссылочные типы В Питоне, как и во многих других языках программирования, все типы данных делятся на значимые (в Питоне их называют атомарными) и ссылочные. К значимым типам относятся числа и строки. К ссылочным – кортежи, списки, словари, множества, функции, классы. Переменные разных типов отличаются поведением. Все переменные одного типа сравниваются по значению. Оператор is возвращает True, если объект сравнивается сам с собой. При этом имена объекта могут быть разные. Если сравнивается объект сам с собой, то адреса должны совпадать. Если мы создадим 2 целочисленные переменные с одинаковым значением, то получим 1 объект с разными именами. Понятно, что при сравнении и значения и адреса переменных совпадут: num1 = 1 num2 = 1 print(id(num1)) print(id(num2)) 201
print(num1 == num2) print(num1 is num2) print() Если мы изменим значение, например, второй переменной, то будет создан новый объект с новым значением 2. Теперь различаются не только значения переменных num1 и num2, но также их адреса в памяти: num2 = 2 print(id(num1)) print(id(num2)) print(num1 == num2) print(num1 is num2) print() Теперь давайте создадим 2 одинаковых списка. Они имеют одинаковое значение, но разные адреса в памяти, поэтому это 2 разные переменные с одинаковыми значениями: lst1 = [1, 2] lst2 = [1, 2] print(id(lst1)) print(id(lst2)) print(lst1 == lst2) print(lst1 is lst2) print() Итак, переменные хранят адреса объектов в памяти. Разные переменные хранятся по разным адресам. Если адреса переменных совпадают, то это одна и та же переменная с разными адресами. Если мы присвоим переменной num3 значение переменной num2, то она будет указывать на тот же объект (литерал 1) в памяти. Таким образом, все 3 переменные указывают на один и тот же объект в памяти: 202
num1 = 1 num2 = 1 num3 = num2 print(id(num1)) print(id(num2)) print(id(num3)) print(num1 == num2) print(num1 is num2) print(num2 == num3) print(num2 is num3) Если мы присвоим переменной lst3 значение переменной lst2, то она получит адрес объекта, на который указывает переменная lst2. Переменные lst2 и lst3 – это одна и та же переменная с разными именами: lst1 = [1, 2] lst2 = [1, 2] lst3 = lst2 print(id(lst1)) print(id(lst2)) print(id(lst3)) print(lst1 == lst2) print(lst1 is lst2) print(lst2 == lst3) print(lst2 is lst3) При изменении числовых переменных создаются новые переменные с теми же именами, поэтому числовые переменные будут указывать на другие адреса в памяти. Если мы изменим значение переменной lst2, то её адрес в памяти не изменится, поэтому значение переменной lst3, которая указывает на тот же адрес, также изменится: lst2 = [1, 2] lst3 = lst2 print(lst2) print(lst3) 203
print(id(lst2)) print(id(lst3)) print(lst2 == lst3) print(lst2 is lst3) lst2 += [3] print(lst2) print(lst3) print(id(lst2)) print(id(lst3)) print(lst2 == lst3) print(lst2 is lst3) print() Так как при изменении значения списка новая переменная не создаётся, то все переменные с одинаковыми адресами будут ссылаться на новое значение в памяти. Пусть у нас есть функция add, которая возвращает сумму двух чисел: def add(num, dob): num += dob print(id(num)) return num Мы передаём в функции add два числа num = 1 и dob = 2, а функция возвращает результат – число 3. Функция принимает от переменной num ссылку на число 1. Внутри функции значение переменной num изменяется, поэтому в функции создаётся новая переменная num с новым значением. После завершения работы функции add все её локальные переменные уничтожаются, поэтому в программе остаётся только старая переменная num, значение которой не изменилось, поэтому не изменилась и ссылка на число 1: num = 1 dob = 2 print(num) print(id(num)) res = add(num, dob) 204
print(res) print(num) print(id(num)) Точно так же ведут себя все неизменяемые объекты. Изменяемые объекты ведут себя иначе, поскольку при изменении их значений новые переменные не создаются. Пусть теперь у нас есть функция-генератор sum_lst, которая принимает список чисел и возвращает их сумму. Затем эта функция добавляет по 10 к каждому элементу списка и возвращает новую сумму. Параллельно мы печатаем элементы списка и адрес переменной lst: def sum_lst(lst: list[int]): print(id(lst)) yield sum(lst) for i in range(len(lst)): lst[i] += 10 print(lst) print(id(lst)) yield sum(lst) Создаём список из пяти чисел и отсылаем его в функцию-генератор sum_lst. Не забываем печатать элементы списка и адрес переменной lst. Хорошо видно, что адрес переменной lst не изменился, то есть переменная lst везде указывает на список чисел. Сам список в памяти изменяется, но адрес сохраняется, поэтому значения элемента списка изменились в функции-генераторе: lst = [1,2,3,4,5] print(lst) print(id(lst)) res_lst = sum_lst(lst) print(next(res_lst)) print(next(res_lst)) print(lst) print(id(lst)) 205
Точно так же ведут себя и другие изменяемые объекты. Из этого следует, что при работе с изменяемыми объектами нужна осторожность, поскольку их значения могут быть изменены в любом месте кода. По этой причине не следует объявлять параметры функции со значениями по умолчанию для изменяемых объектов. 206
Проект Делится - не делится? 2 207
Мы уже решали эту задачу, но без списка. А со списком многие задачи решать гораздо проще. Легко заметить, что произведение симметричных относительно середины ряда делителей равно заданному числу. Например, для числа 64 мы получили такой ряд делителей: 1 2 4 8 16 32 64 Проверяем утверждение: 1 х 64 = 2 x 32 = 4 x 16 = 8 x 8 = 64 Всё верно! Единственное «неудобство» причиняют восьмёрки – они дважды входят в произведение, поэтому нам следует подумать, как оставить только одну из них. Теперь легко заметить, что нам достаточно найти делители заданного числа в диапазоне 1..√num. Вторую половину делителей мы найдём, поделив заданное число на очередной делитель. Для хранения делителей заведём список res: import math # ФУНКЦИЯ СО СПИСКОМ def solveList(num): # список для записи делителей res = [] for i in range(1, round(math.sqrt(num)) + 1): if num % i == 0: res.append(i) # не допускаем повторов делителей if i * i != num: res.append(num // i) print('Число', num, 'делится на:') print(*res, sep=' ', end='') 208
print() while True: num = int(input("Введите натуральное число > if num == 0: break solveList(num) print("") ")) Для первого делителя – единицы – мы найдём парный делитель, равный заданному числу num, для второго парный делитель равен (num // второй делитель). То есть делители мы напечатаем не по порядку. Неупорядоченные делители Чтобы выправить ситуацию, мы до печати результатов отсортируем список res с помощью метода sort списочного класса, а затем напечатаем делители строго по ранжиру: import math # ОПТИМИЗИРОВАННАЯ ФУНКЦИЯ СО СПИСКОМ def solveList(num): # список для записи делителей res = [] for i in range(1, round(math.sqrt(num)) + 1): if num % i == 0: res.append(i) # не допускаем повторов делителей if i * i != num: res.append(num // i) 209
print('Число', num, 'делится на:') res.sort() print(*res, sep=' ', end='') print() while True: num = int(input("Введите натуральное число > if num == 0: break ")) solveList(num) print("") Теперь даже для огромных чисел мы мгновенно выписываем все делители. Введите натуральное число > 777777777 Число 777777777 делится на: 1 3 7 9 21 37 63 111 259 333 777 2331 333667 1001001 2335669 3003003 7007007 12345679 21021021 37037037 86419753 111111111 259259259 777777777 Для последовательного получения делителей какого-либо числа можно воспользоваться простым генератором: def divisorGenerator(num): for i in range(1, num // 2 + 1): if num % i == 0: yield i yield num for i in divisorGenerator(num): print(i, end=' ') 210
Проект Самые согласные Любителям словесных головоломок известно слово ВСПЛЕСК, в котором из 7 букв только 1 гласная. В русском языке нет существительных, состоящих только из согласных, поэтому слово ВСПЛЕСК – рекордсмен в этой номинации. А в этом проекте мы поищем и другие самые согласные слова! Словарь Макарова более богат редкими словечками, поэтому мы используем его: def main(): # словарь: fileName = 'makarov_frc.txt'#'OSH-W97_frc.txt' solve(fileName) main() Все гласные помещаем в строку VOWELS: # гласные: VOWELS = 'АЕЁИОУЫЭЮЯ' Последовательно получаем из файла все слова: # РЕШАЕМ ЗАДАЧУ def solve(fn): # проверяем все слова из файла: file = open(fn) for word in file: # слово без переноса: word = word.rstrip() Составляем список гласных в каждом слове: 211
# список гласных в слове word: cons = [item for item in word if item not in VOWELS] Если в слове word все 1 гласная, то список согласных букв cons должен быть короче на 1 элемент: # должна исчезнуть 1 гласная: if len(word) - len(cons) == 1: print(word, len(word)) # закрываем файл: file.close() Слова с мягким знаком могут вызвать вопросы, поэтому мы добавили к слову ВСПЛЕСК только редкостное слово ВЗПЛЕСК. А вот 6-буквенных самых согласных слов оказалось очень много. БЛИСТР БРАНЛЬ ВЗБРОС ВЗВЕСЬ ВЗВИЗГ ВЗГЛЯД ВЗЪЕЗД ВЛАСТЬ ВПРЫСК ВСКРИК ВСХЛИП ВСХРАП ГВЕЛЬФ ГВОЗДЬ ГОРСТЬ 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 ГРОЗДЬ ГРУЗДЬ ГРУСТЬ ЗЛОСТЬ КЛЕЙМС КРЕМЛЬ КСЕНДЗ ЛЬСТЕЦ МАНГЛЬ МОНСТР ПЕРСТЬ ПЛЕКТР ПОЛСТЬ ПРОБСТ СЕЛЬДЬ 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 СКАЛЬД СКАЛЬП СКВАЙР СКОРБЬ СЛАСТЬ СМЕРТЬ СНАСТЬ СПЕКТР СПРИНТ СТЕРНЬ СТИЛЬБ СТРЕСС СТРИНГ СФИНКС ТВЕРДЬ 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 ТРОЛЛЬ 6 ТРОСТЬ 6 ФАЛЬШЬ 6 ФИЛЬТР 6 ФЛАНДР 6 ФЬЕЛЬД 6 ШЕРСТЬ 6 ШПЛИНТ 6 ШПРИНГ 6 БРАНДЛЬ 7 ВЗБЛЕСК 7 ВСПЛЕСК 7 СТРАСТЬ 7 СТРЕНДЬ 7 212
Проект TwoSum2Loops Любителям решать программистские задачи, вероятно, известен сайт https://leetcode.com/problemset/all/. Традиционно самая первая задача очень лёгкая. Её решила почти половина участников. Вот её условие: Задача 1. Two Sum: Given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target. You may assume that each input would have exactly one solution, and you may not use the same element twice. You can return the answer in any order. Вольный перевод: Дан массив целых чисел nums и целочисленную сумма. Нужно, вернуть индексы таких двух чисел так, чтобы их сумма равнялась заданной. Нельзя использовать один и тот же элемент массива дважды. Задача имеет ровно одно решение., Вы можете вернуть ответ в любом порядке. Для проверки решения даны 3 набора данных: nums = [2, 7, 11, 15] target = 9 nums = [3, 2, 4] target = 6 213
nums = [3, 3] target = 6 Самый простой алгоритм решения: • последовательно берём первое число от первого до предпоследнего в наборе • последовательно берём второе число от следующего после первого и до последнего в наборе. Дойдя до конца набора, возвращаемся в 1 • находим сумму двух чисел • если она совпадает с заданной, то возвращаем индексы найденных чисел • иначе возвращаемся в 2 Поскольку наборы чисел нам известны, то мы перебираем их во вложенных циклах for. Переводим наш незатейливый алгоритм на питоний язык – и задача решена: # РЕШАЕМ ЗАДАЧУ def twoSum(nums: list[int], target: int) -> for i in range(len(nums)): for j in range(i + 1, len(nums)): if nums[i] + nums[j] == target: return [i, j] list[int]: nums = [2, 7, 11, 15] target = 9 res = twoSum(nums, target) print(res) nums = [3, 2, 4] target = 6 res = twoSum(nums, target) print(res) nums = [3, 3] target = 6 214
res = twoSum(nums, target) print(res) Проект Каркас слов Предлагаю вам развлечься словесной игрой Каркас, которую придумал программист А.Битман. Правила её очень просты. Из списка «согласных», к которым здесь причисляются буквы БВГДЖЗКЛМНПРСТФХЦЧШЩ, произвольно выбирают несколько, обычно от двух до четырёх. Например, я предпочёл собственные инициалы – РВД. Они образуют каркас слов, которые предстоит найти игрокам (для нас задача упрощается – отыскать в словаре). Побеждает тот, у кого улов слов окажется больше. Действительно, согласные гораздо «полезнее» гласных. В этом легко убедиться, если из какой-нибудь известной фразы последовательно выбросить сначала согласные, а затем гласные. У меня, например, получилось так: оа аае аиое ое. оаи уаю оё и ео. Влг впдт в Кспйск мр. Лшд кшт вс сн. Если вы столь же догадливы, как и я, то по первой «шараде» ни за что не догадаетесь, что это крылатое выражение Волга впадает в Каспийское море. Лошади кушают овёс и сено из рассказа Учитель словесности Антона Павловича Чехова. Вторая «закавыка» разгадывается без особого труда. Слова обязательно должны содержать все буквы каркаса (у нас это РВД), причем ни кратность букв, ни порядок их следования в слове специально не 215
оговариваются. Важно, чтобы в них не было других согласных. А все гласные и буквы Й, Ь и Ъ могут присутствовать в любом количестве. Для нашей игры я составил вот такой списочек. Каркас слов Ваши согласные > ['Р', 'В', 'Д'] РВД 1. ВРЕД 2. ДВОР 3. ВЕДРО 4. ВЁДРО 5. ВЫДРА 6. ДВЕРЬ 7. ДРЕВО 8. ДРОВА 9. ДЕВЕРЬ 10. ДЕРЕВО 11. ВОДОРОД 12. ДОВЕРИЕ 13. РЯДОВОЙ 14. ЮРОДИВАЯ 15. ЮРОДИВЫЙ Всего найдено слов 15 Совершенно очевидно, что эта игра исключительно на знание слов и быстроту их извлечения из памяти. А такие игры лучше и правильнее поручать компьютеру. Обратимся к правилам игры и найдём условие: Из списка «согласных» играющиеся произвольно выбирают несколько букв, обычно от двух до четырёх. Вопрос «как выбирают?» мы решаем в функции main, где любой интеллектуал, могущий отличить гласные от согласных, набирает с клавиатуры дорогие ему буквы. Не будем от него требовать ввода букв в ВЕРХНЕМ 216
регистре, об этом мы позаботимся сами. После проверки ввода программа переходит в функцию karkas, которая и выдаёт исчерпывающий ответ. # согласные: SOGL = "БВГДЖЗКЛМНПРСТФХЦЧШЩ" # ЗАГРУЖАЕМ СЛОВАРЬ def load(fn): with open(fn) as file: spisok = file.readlines() # убираем символ переноса строки: return [s.rstrip('\n') for s in spisok] В строковую константу SOGL мы упаковали все согласные - чтобы соблюсти правила игры. def main(): # словарь: fileName = 'OSH-W97_frc.txt' # загружаем словарь: spisok = load(fileName) print() print("Каркас слов") print() while True: sogl = input("Ваши согласные > ") # переводим буквы в верхний регистр: sogl = list(sogl.upper()) print(sogl) # проверяем правильность ввода: for ch in sogl: if not ch in SOGL: print("Буква " + ch + " не согласная!") break else: print() # ищем каркасные слова: 217
karkas(spisok, sogl) main() Обязательно следует проверить ввод игрока: мы последовательно берём по одной букве ch из тех, что выбрал игрок, и проверяем, имеется ли она среди полного списка согласных SOGL. При первом же нарушении правил мы отсылаем игрока в начало цикла while. В первом случае ввод правильный, во втором и третьем – ошибочный: Функция karkas получает список всех слов и список согласных букв игрока: # ИЩЕМ В СЛОВАРЕ КАРКАСНЫЕ СЛОВА def karkas(spisok : list[str], sogl : list[str]) -> None: # нашли слов: nWords = 0 Ищем подходящие каркасные слова: # ищем в списке подходящие слова --> # для всех слов из словаря: for word in spisok: 218
# оставшиеся согласные буквы: ost = sogl.copy() flg = True for ch in word: # в слове должны быть только согласные: if ch in SOGL and not ch in sogl: # слово не годится: flg = False break if ch in ost: ost.remove(ch) # печатаем найденное слово: if flg and len(ost) == 0: nWords += 1 print(f'{nWords}. {word}') print(f'Всего найдено слов {nWords}') По одному извлекаем слова из списка spisok в строковую переменную word и начинаем проверять каждую букву ch слова word. Если это не согласная, то мы её пропускаем, поскольку такие буквы могут присутствовать без ограничений. Если же буква оказалась согласной (сработало условие ch in SOGL, но отсутствует в списке sogl, что и проверяет второе условие - not ch in sogl, то эта согласная – не та согласная, которую загадал игрок. По правилам игры, такое слово мы должны пропустить и перейти к следующему. Для этого создаём из списка согласных игрока sogl новый список ost: # оставшиеся согласные буквы: ost = sogl.copy() Сначала в нём окажутся все три буквы РВД. Если в слове word мы обнаружим букву из списка sogl, то её удаляем: if ch in ost: ost.remove(ch) 219
К примеру, если мы проверяем слово ВОДОРОД, то первая буква В изгонит эту букву из списка, и в нём останутся только две буквы – РД. Проследите эту мысль дальше, и вы убедитесь, что если в слове имеются все три буквы РВД, то в списке ost не останется ни одной буквы. Тривиальная проверка len(ost) == 0 укажет нам, соблюдён ли и этот пункт правил. Других хитростей в поиске каркасных слов нет – вы можете играть с кем угодно, не опасаясь поражения. Каркас слов Ваши согласные > ккстр ['К', 'К', 'С', 'Т', 'Р'] 1. СКРУТКА 2. КАСТОРКА 3. КРАСОТКА 4. РАСКАТКА 5. ЭКСТРАКТ 6. КРАТКОСТЬ 7. КУРСИСТКА 8. РАСКРУТКА 9. АККУРАТИСТ 10. КАРАТИСТКА 11. КОРОТКОСТЬ 12. КРИТИКЕССА 13. КАРАТЕИСТКА 14. КАРЬЕРИСТКА 15. КОСОРУКОСТЬ 16. АККУРАТИСТКА 17. АРИСТОКРАТКА 18. КАРИКАТУРИСТ 19. ТРАКТОРИСТКА 20. КОРРЕКТОРСКАЯ 21. КАРИКАТУРИСТКА Всего найдено слов 21 Встречается и такая разновидность игры, которую можно назвать Строгий каркас. Здесь дозволяется использовать каждую заданную букву только по 220
одному разу в каждом слове. Замените одну строчку – и играйте по строгим правилам: #if ch in SOGL and not ch in sogl: if ch in SOGL and not ch in ost: Например, в книге [ГЕ02] предлагается найти слова с буквами КНТ. Наш ответ: легко! Каркас слов Ваши согласные > ['К', 'Н', 'Т'] КНТ 1. КАНТ 2. КНУТ 3. ТАНК 4. КАНАТ 5. НАКАТ 6. НАТЁК 7. НИТКА 8. НЫТИК 9. ТЕНЁК 10. ТКАНЬ 11. АНКЕТА 12. АНЮТКА 13. НОКАУТ 14. ТАЙНИК 15. ТКАНЬЁ 16. ТУНИКА 17. УТЁНОК 18. ЕКТЕНИЯ 19. ЕКТЕНЬЯ 20. КАТАНИЕ 21. КАТАНЬЕ 22. ТАКАНЬЕ 23. ТИКАНЬЕ 24. ТУКАНЬЕ 25. ТЫКАНЬЕ 26. ТЮКАНЬЕ 221
Всего найдено слов 26 У автора книги [ГЕ02] отсутствуют многие слова из нашего списка, зато присутствуют ТОНИК, ТОНИКА и ОКТАН, которых нет в нашем словаре. 222
Проект Гласность 223
Речь пойдёт о словесной игре Сергея Федина [ФС06] в противовес Каркасу. Здесь всё то же самое, только наоборот: место согласных занимают гласные. Нетрудно посчитать, что таких букв в русском алфавите меньше, а вот слов почти с каждой буквой можно найти очень много. Именно поэтому в оригинальном варианте игры разрешается использовать единственную букву. Мы это ограничение обойдём. Если кто-то желает слепо следовать правилам, пусть задаёт одну букву. Нам потребуется совсем немного перестройки, чтобы превратить Каркас в полноценную Гласность: # гласные: GLAS = "АЕЁИОУЫЭЮЯ" # ЗАГРУЖАЕМ СЛОВАРЬ def load(fn): with open(fn) as file: spisok = file.readlines() # убираем символ переноса строки: return [s.rstrip('\n') for s in spisok] # ИЩЕМ В СЛОВАРЕ "ГЛАСНЫЕ" СЛОВА def glasnost(spisok : list[str], glas : list[str]) -> None: # нашли слов: nWords = 0 #print(sogl) # ищем в списке подходящие слова --> # для всех слов из словаря: for word in spisok: #print(word) # оставшиеся согласные буквы: ost = glas.copy() flg = True for ch in word: # в слове должны быть только согласные: #if ch in SOGL and not ch in sogl: if ch in GLAS and not ch in ost: # слово не годится: 224
flg = False break #if ch in ost: # ost.remove(ch) # печатаем найденное слово: #if flg and len(ost) == 0: if flg: nWords += 1 print(f'{nWords}. {word}') print(f'Всего найдено слов {nWords}') print() def main(): # словарь: fileName = 'OSH-W97_frc.txt' # загружаем словарь: spisok = load(fileName) print() print("Гласность") print() while True: glas = input("Ваши гласные > ") # переводим буквы в верхний регистр: glas = list(glas.upper()) print(glas) # проверяем правильность ввода: for ch in glas: if not ch in GLAS: print("Буква " + ch + " не гласная!") break else: print() # ищем 'гласные' слова: glasnost(spisok, glas) main() 225
С такой программой мы осилим любого врага! Гласность Ваши гласные > ['А'] 1. 2. 3. 4. А АД АЗ АР АС . . . 1004. 1005. 1006. 1007. АБРАКАДАБРА ТРАНСПАРАНТ КАРАКАЛПАЧКА ТРАНСПЛАНТАТ Всего найдено слов 1007 А статистика, которая знает всё, утверждает, что больше всего одинаковых гласных мы найдём в словах: АБРАКАДАБРА – КАРАКАЛПАЧКА 5 А ПЕРЕШЕЕК – ЗЕМЛЕДЕЛЕЦ – ПЕРЕСЕЛЕНЕЦ - … 4 Е ИНФИНИТИВ – СИФИЛИТИК – ПРИМИТИВИЗМ 4 И ОБОРОНОСПОСОБНОСТЬ 6 О БУРУНДУК – ФУРУНКУЛ 3 У ССЫЛЬНЫЙ 2 Ы МЭР – ЭЛЬФ - … 1 Э ТЮТЮН 2 Ю МЯМЛЯ – СТРЯПНЯ - … 2 Я 226
Проект Слово в слове 2 Некоторые слова затаились в более длинных. Например, в слове СЛОВО мы легко найдем короткое слово ЛОВ. А в слове ФАСОЛЬ – сразу несколько: ФА, АС, СОЛЬ. Если слово целиком можно составить из отдельных слов, то оно называется шарадой. Примером шарады является слово ФАСОЛЬ = ФА + СОЛЬ. Как и в случае с кортежами, мы легко найдём все шарады, которые состоят из двух слов: # ЗАГРУЖАЕМ СЛОВАРЬ def load(fn): with open(fn) as file: spisok = file.readlines() # убираем символ переноса строки: return [s.rstrip('\n') for s in spisok] # ИЩЕМ СЛОВА def slova(spisok): for s in spisok: # в слове должно быть чётное число букв: lens = len(s) if lens % 2 != 0: continue # первое слово из первой половины букв слова s: s1 = s[: lens // 2] # второе слово из второй половины букв слова s: s2 = s[lens // 2 : ] # если такие слова есть в списке, # значит, мы нашли шараду: if s1 in spisok and s2 in spisok: print(f'{s1} + {s2} = {s}') 227
def main(): # словарь: fileName = 'makarov_frc.txt' # загружаем словарь: spisok = load(fileName) slova(spisok) main() . . . КОЛО + КОЛО + КОМИ + КОПА + КОРА + КОРТ + КОРТ + КРЕП + КРОН + КРОТ + . . . КОЛА МАЗЬ ТЕНТ ТЕЛЬ БЛИК ИЗОЛ ОЧКИ ОСТЬ ГЛАС ОСТЬ = = = = = = = = = = КОЛОКОЛА КОЛОМАЗЬ КОМИТЕНТ КОПАТЕЛЬ КОРАБЛИК КОРТИЗОЛ КОРТОЧКИ КРЕПОСТЬ КРОНГЛАС КРОТОСТЬ Найти заданное слово в других, более длинных словах и того проще. Совершенно очевидно, что нужно поочерёдно брать слова из списка и искать в них более короткое слово. Оператор in и покажет нам все слова, в которых прячется заданное слово. После этих недолгих размышлений функцию поиска мы напишем за пару минут: # ЗАГРУЖАЕМ СЛОВАРЬ def load(fn): with open(fn) as file: spisok = file.readlines() # убираем символ переноса строки: return [s.rstrip('\n') for s in spisok] # ИЩЕМ СЛОВО В СЛОВАХ 228
def slova2(spisok, w): for s in spisok: if w in s: print(f'{w} <-- {s}') def main(): # словарь: fileName = 'makarov_frc.txt' # загружаем словарь: spisok = load(fileName) #slova(spisok) while True: w = input('Короткое слово: ') slova2(spisok, w) main() . . . РОЗА <-РОЗА <-РОЗА <-РОЗА <-РОЗА <-РОЗА <-РОЗА <-РОЗА <-РОЗА <-РОЗА <-РОЗА <-РОЗА <-РОЗА <-РОЗА <-РОЗА <-РОЗА <-РОЗА <-- ПТЕРОЗАВР ПАЛЬМАРОЗА ЦЕЛУРОЗАВР КОНТРУГРОЗА ПРОЗАУРОПОД ПРОТОРОЗАВР САХАРОЗАВОД АЛЬДОТЕТРОЗА ВТОРОЗАКОНИЕ ПРОЗАИЧНОСТЬ ЭЛЕКТРОЗАОВД ЖИРОЗАМЕНИТЕЛЬ МАСЛОСЫРОЗАВОД САХАРОЗАВОДЧИК ПРОЗАСЕДАВШИЙСЯ САХАРОЗАВОДЧИЦА СТАРОЗАВЕТНОСТЬ Нас также могут заинтересовать слова, которые начинаются с более короткого слова. И тут нам на помощь приходит метод startswith: 229
# ЗАГРУЖАЕМ СЛОВАРЬ def load(fn): with open(fn) as file: spisok = file.readlines() # убираем символ переноса строки: return [s.rstrip('\n') for s in spisok] # ИЩЕМ СЛОВА, КОТОРЫЕ НАЧИНАЮТСЯ # С ЗАДАННОГО СЛОВА def slova_beg(spisok, w): for s in spisok: if s.startswith(w): print(f'{w} <-- {s}') def main(): # словарь: fileName = 'makarov_frc.txt' # загружаем словарь: spisok = load(fileName) #slova(spisok) while True: w = input('Короткое слово: ') #slova2(spisok, w) slova_beg(spisok, w) print() main() Короткое слово: РОГ РОГ <-- РОГ РОГ <-- РОГА . . . РОГ <-- РОГУЛЬКА РОГ <-- РОГОНОСЕЦ РОГ <-- РОГОХВОСТ РОГ <-- РОГУЛЬНИК . . . 230
Проект Палиндромная программа Слова, фразы, числа и вообще любые последовательности символов, которые читаются одинаково слева направо и наоборот, называются палиндромами. Мы поставим перед собой простую задачу – отыскать слова, которые не изменяются при чтении наоборот. Вы наверняка знаете немало таких слов. Например, РОТОР, ШАЛАШ, КАБАК. Чтобы найти все такие слова, достаточно внимательно просмотреть словарь русского языка, но мы делегируем это занятие компьютеру. Задача очень простая, но над алгоритмом программы необходимо немного подумать. Сравним обычное слово и палиндром: ПИРАТ ПОТОП Переписываем слова справа налево и сравниваем между собой каждую пару. Для первого слова получаем ПИРАТ → ТАРИП, для второго ПОТОП → ПОТОП. Очевидно, что слово тогда и только тогда является палиндромом, когда слова в паре совпадают. Отсюда вытекает способ опознания палиндромов: # ИЩЕМ ПАЛИНДРОМЫ В СЛОВАРЕ def palindromes(spisok : list[str]): nWords = 0 for word in spisok: rev = word[::-1] if rev == word: nWords += 1 print(f'{nWords}. {word}') Как обычно, в функции main загружаем список слов и передаём его в функцию palindromes: 231
# ЗАГРУЖАЕМ СЛОВАРЬ def load(fn): with open(fn) as file: spisok = file.readlines() # убираем символ переноса строки: return [s.rstrip('\n') for s in spisok] def main(): # словарь: fileName = 'OSH-W97_frc.txt' # загружаем словарь: spisok = load(fileName) # ищем палиндромы: palindromes(spisok) main() Итак, программа написана, запускаем её! Результаты мы получаем практически мгновенно. 1. БОБ 2. ДЕД 3. КОК 4. МИМ 5. ОКО 6. ОНО 7. ПОП 8. ПУП 9. ТАТ 10. ТУТ 11. ШИШ 27. РОТАТОР 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. ДОВОД ДОХОД ЗАКАЗ КАБАК КАЗАК КОЛОК КОМОК МАДАМ НАГАН ПОТОП РАДАР РОТОР ТОПОТ ШАБАШ ШАЛАШ 232
Проект Эйлер - Задача 4a Обычно на всякого рода олимпиадах и конкурсах в программах следует использовать только такие возможности, которые имеются практически во всех языках программирования. За функцию str мы поручиться не можем, поэтому нам нужно либо написать такую функцию для нашего проекта, либо решить задачу вообще без строк, что, безусловно, правильнее, коль мы имеем дело с числами. Условие задачи смотрите в Строках. Главная функция в нашей программе - это isPalindrome, которая сравнивает симметричные относительно середины слова word символы. При этом символы извлекаются из списка цифр. Из числа типа int так просто нельзя извлечь нужную цифру. Это значит, что нам нужно превратить заданное число в список digits: def isPalindrome(num : int) -> bool: digits = getDigits(num) lend = len(digits) flg = True # число-палиндром? for i in range(lend // 2): d1 = digits[lend - i - 1] d2 = digits[i] flg = d1 == d2 # не палиндром: if not flg: break return flg В функции getDigits мы последовательно выделяем из числа его цифры, начиная с разряда единиц, то есть двигаемся справа налево. Каждую цифру 233
вставляем или добавляем в начало списка, чтобы сохранить правильный порядок цифр: def getDigits(num : int) -> list[int]: digits = [] while num > 0: #digits.insert(0, num % 10) digits = [num % 10] + digits num //= 10 return digits Для палиндромов порядок цифр в списке не имеет значения, поэтому можно добавлять новые цифры в список с помощью метода append. Остальная часть программы не вызывает вопросов: # РЕШАЕМ ЗАДАЧУ def solve(): nVar = 0 max = 0 maxi = maxj = 0 for i in range(100, 999 + 1): for j in range(i, 999 + 1): mul = i * j if isPalindrome(mul): print(f'{i} * {j} = {mul}') if mul > max: max = mul maxi = i maxj = j nVar += 1 234
print(f'Всего палиндромов = {nVar}') print() print(f'Наибольший палиндром = {maxi} * {maxj} = {max}') print() def main(): print() print("Решаем задачу 4 из Проекта Эйлер") print() solve() main() Проверка показывает, что программа работает безукоризненно. Решаем задачу 4 из Проекта Эйлер 101 * 101 * 101 * . . . 902 * 902 * 913 * 916 * 924 * 924 * 101 = 10201 111 = 11211 121 = 12221 909 914 993 968 932 962 = = = = = = 819918 824428 906609 886688 861168 888888 Всего палиндромов = 1239 Наибольший палиндром = 913 * 993 = 906609 235
Проект Детский лепет Малые детки причудливо лепечут слова, состоящие из двух одинаковых половинок. Начинают они с первого человеческого слова МАМА, а затем из их уст появятся: ПАПА, ДЯДЯ, ТЯТЯ, БАБА, ЛЯЛЯ, КАКА, КОКО, БОБО, ВАВА, ПИПИ, СИСИ. Повзрослев, они пролепечут и более сложные слова: КАНКАН и ВАРВАР. Мы уже занимались поиском таких слов в проекте Кортежная тавтология. Программа со списком даже проще кортежной: # ЗАГРУЖАЕМ СЛОВАРЬ def load(fn): with open(fn) as file: spisok = file.readlines() # убираем символ переноса строки: return [s.rstrip('\n') for s in spisok] # ИЩЕМ СЛОВА def lepet(spisok): for s in spisok: # пропускаем длинные слова: if len(s) > 3: 236
break # удвоенный слово из букв слова s: s2 = s * 2 # если такое слово есть в списке, # значит, мы нашли тавтологию: if s2 in spisok: print(s, s2) def main(): # словарь: fileName = 'makarov_frc.txt' # загружаем словарь: spisok = load(fileName) lepet(spisok) print() main() Если половины слов не обязательно должны быть осмысленными словами, то лепетальных слов мы найдём больше: # ЗАГРУЖАЕМ СЛОВАРЬ def load(fn): with open(fn) as file: spisok = file.readlines() # убираем символ переноса строки: return [s.rstrip('\n') for s in spisok] # ИЩЕМ СЛОВА def lepet2(spisok): for s in spisok: # в слове должно быть чётное число букв: lens = len(s) if lens % 2 != 0: continue # слово из первой половины букв слова s: s1 = s[: lens // 2] 237
# слово из второй половины букв слова s: s2 = s[lens // 2 : ] # если половины совпадают, # значит, мы нашли слово: if s1 == s2: print(f'{s1} + {s2} = {s}') def main(): # словарь: fileName = 'makarov_frc.txt' # загружаем словарь: spisok = load(fileName) #lepet(spisok) print() lepet2(spisok) main() БА БЕ ГА ДЯ МА НЯ ПА ТЯ ФИ ФУ ЦА ЦЕ ЧА + + + + + + + + + + + + + БА БЕ ГА ДЯ МА НЯ ПА ТЯ ФИ ФУ ЦА ЦЕ ЧА = = = = = = = = = = = = = БАБА БЕБЕ ГАГА ДЯДЯ МАМА НЯНЯ ПАПА ТЯТЯ ФИФИ ФУФУ ЦАЦА ЦЕЦЕ ЧАЧА БЕР + БЕР = ВАР + ВАР = КАН + КАН = КВА + КВА = КУС + КУС = ПУР + ПУР = ТАМ + ТАМ = ТАР + ТАР = БУЛЬ + БУЛЬ БЮЛЬ + БЮЛЬ МАТА + МАТА БЕРБЕР ВАРВАР КАНКАН КВАКВА КУСКУС ПУРПУР ТАМТАМ ТАРТАР = БУЛЬБУЛЬ = БЮЛЬБЮЛЬ = МАТАМАТА Ещё более простой способ отыскания лепетальных слов такой. 238
Ищем двудольные слова Итак, нам нужно последовательно сравнивать буквы из первой половины слова с буквами из его второй половины (слово должно состоять из чётного числа букв): # ЗАГРУЖАЕМ СЛОВАРЬ def load(fn): with open(fn) as file: spisok = file.readlines() # убираем символ переноса строки: return [s.rstrip('\n') for s in spisok] # ИЩЕМ СЛОВА def lepet3(spisok): for s in spisok: # в слове должно быть чётное число букв: lens = len(s) if lens % 2 != 0: continue # длинные слова не проверяем: 239
if lens > 8: break flg = True for i in range(lens // 2): ch1 = s[lens // 2 + i] ch2 = s[i] flg = ch1 == ch2 if not flg: break # печатаем найденное слово: if flg: print(s) def main(): # словарь: fileName = 'makarov_frc.txt' #fileName = 'EnDictionary.txt' # загружаем словарь: spisok = load(fileName) #lepet(spisok) print() lepet3(spisok) main() Ограничение длины слов для поиска условием len > 8 основано на жизненном опыте и знании русского языка. В других языковых культурах вполне могут встретиться и более длинные слова, так что вы можете это условие удалить из кода в случае необходимости. Но вот и в английском языке я также не нашёл длинных слов. Для поиска английских мам подправьте код: fileName = 'EnDictionary.txt' 240
# длинные слова не проверяем: #if lens > 8: # break Проект Пора по парам Условие задачи смотрите в Строках. В функции main загружаем список слов и вызываем функцию solve: # ЗАГРУЖАЕМ СЛОВАРЬ def load(fn): with open(fn) as file: spisok = file.readlines() # убираем символ переноса строки: return [s.rstrip('\n') for s in spisok] def main(): # словарь: fileName = 'makarov_frc.txt' #fileName = 'EnDictionary_frc.txt' # загружаем словарь: spisok = load(fileName) solve(spisok, 2) print() main() Обратите внимание, что мы передаём этой функции число пар смежных букв, которые она должна найти в словах: 241
# ИЩЕМ СЛОВА def solve(spisok: list[str], np : int) -> None: # нашли слов: nWords = 0 Понятно, что это число не может быть меньше единицы. Тогда при значении параметра np, равном 1, функция напечатает слова с одной парой двойных букв (или более). При значении 2 – слова с двумя парами букв (или более). И наконец, при значении, большем 2, – слова, в которых не менее трёх пар двойных букв. Легко посчитать, что в словах, которые содержат np пар двойных букв, должно быть не меньше 2 * np букв. Если это не так, то такие слова мы сразу же, без проверки пропускаем: # ИЩЕМ СЛОВА def solve(spisok: list[str], np : int) -> None: if (np < 1): return # нашли слов: nWords = 0 # просматриваем все слова: for word in spisok: # длина слова: len_word = len(word) # пропускаем короткие слова: if (len_word < 2 * np): continue Все остальные слова мы проверяем так. Начинаем с самой первой буквы слова, а заканчиваем буквой, которая занимает позицию 2 * np от конца слова: for i in range(len_word - 2 * np + 1): 242
По условию задачи, первая проверяемая буква должна совпадать со второй. Если же они различаются, то дальше проверять слово смысла нет, и мы переходим к проверке следующего слова: # совпадает первая пара букв? if (word[i] != word[i + 1]): continue Аналогично мы проверяем (если нужно) вторую и третью пару букв, которые должны следовать сразу друг за другом: # совпадает вторая пара букв? if ((np >= 2) and (word[i + 2] != word[i + 3])): continue # совпадает третья пара букв? if ((np > 2) and (word[i + 4] != word[i + 5])): continue Если все пары букв совпали, мы печатаем слово на экране: nWords += 1 print(f'{nWords}. {word}') Если мы ищем, например, слова с одной парой букв, то слова с двумя парами двойных букв будут напечатаны дважды. Нам этого не нужно, поэтому после печати очередного слова мы сразу же переходим к следующему слову в списке: break В конце функции solve печатаем число найденных слов: 243
print(f'Всего найдено слов: {nWords}') print() Действительно, нужных нам букв в слове должно быть 2 * np штук, поэтому, если мы начнём проверку с последующих букв слова, то нам их заведомо не хватит: Начальные буквы для проверки Таким образом, начиная с первой буквы слова (её индекс в слове равен 0), мы проверяем буквы с такими индексами: 0-1 1-2 3-4 Если в слове больше, чем 2 * np букв, а нужных нам пар букв там так и не оказалось, то мы переходим ко второй букве (её индекс в слове равен 1) и снова выполняем все описанные выше проверки. Когда позиция первой проверяемой буквы превысит значение len - 2 * np, проверку слова необходимо закончить. Давайте для начала найдём слова с одной парой двойных букв. Таких слов огромное множество! 244
. . . 4471. 4472. 4473. 4474. 4475. 4476. 4477. 4478. 4479. ЭЛЕКТРОЭНЦЕФАЛОГРАММА ДВАДЦАТИЧЕТЫРЕХГРАННИК КАРБОКСИМЕТИЛЦЕЛЛЮЛОЗА КРИСТАЛЛОЛЮМИНЕСЦЕНЦИЯ МУЛЬТИПРОГРАММИРОВАНИЕ НЕДИСЦИПЛИНИРОВАННОСТЬ ЭЛЕКТРООТРИЦАТЕЛЬНОСТЬ ФОНОЭЛЕКТРОКАРДИОГРАММА ЭЛЕКТРОАППАРАТОСТРОЕНИЕ Всего найдено слов: 4479 Всего мне удалось обнаружить 13 слов, удовлетворяющих условию задач. 1. ГЕЕННА 2. ВАКУУММЕТР 3. МАССООБМЕН 4. ВАКУУММЕТРИЯ 5. МЕТАЛЛООПТИКА 6. ВОЕННООБЯЗАННАЯ 7. ВОЕННООБЯЗАННЫЙ 8. КРИСТАЛЛООПТИКА 9. МЕТАЛЛООБРАБОТКА 10. ПАРААГГЛЮТИНАЦИЯ 11. ТЕЛЕГАММААППАРАТ 12. МИЦЕЛЛООБРАЗОВАНИЕ 13. КРИСТАЛЛООБРАЗОВАНИЕ Всего найдено слов: 13 245
Словари (Dictionaries) 246
Словарь (dictionary) представляет собой сложную структуру данных, неупорядоченную изменяемую коллекцию объектов разных типов, обеспечивающую доступ к элементам по ключу. Главное свойство словарей — быстрый поиск на основе ключей. Можно также свободно добавлять и удалять элементы, подобно тому, как это делается в списках, но без накладных расходов производительности, связанных с необходимостью смещения последующих элементов в памяти. Словари называют также ассоциативными списками, потому что мы сами устанавливаем связи (ассоциации) между ключами и значениями. В списках, например, элементы получают индексы независимо от нашего желания. Объявление и определение/создание словарей Чтобы объявить словарную переменную, напишите имя переменной, поставьте двоеточие и укажите тип dict: имя_переменной : dict Нетрудно догадаться, что название типа dict – это сокращение от английского слова dictionary. После объявления переменной её нельзя использовать до инициализации. Пустой словарь можно определить так: имя_переменной = {} То есть после знака присваивания просто записать пару фигурных скобок. И тогда переменная получит правильный тип: slovar = {} print(type(slovar)) 247
Обычно словарь инициализируют сразу при определении. В проекте АЛЬТруизм нам потребуется словарь соответствия глухих и звонких согласных русского языка. Мы можем считать, что глухие согласные – это ключи, а звонкие – значения. Чтобы инициализировать словарь, нужно в фигурных скобках перечислить через запятую пары ключ – значение. Ключ и значение в каждой паре отделяются двоеточием: # создаём словарь: dchars = {'П' : 'Б', 'Ф' : 'В', 'К' : 'Г', 'Ш' : 'Ж', 'С' : 'З', 'Т' : 'Д', 'Х' : 'К'} Метод keys возвращает список всех ключей в словаре, а метод values – список всех значений: print(*dchars.keys()) print(*dchars.values()) Важно помнить, что ключи в словаре не могут повторяться, но разным ключам могут соответствовать одинаковые значения. Метод items возвращает список кортежей с парами ключ – значение: print(*dchars.items()) ('П', 'Б') ('Ф', 'В') ('К', 'Г') ('Ш', 'Ж') ('С', 'З') ('Т', 'Д') ('Х', 'К') 248
Новый словарь можно создать из другого словаря. Метод copy возвращает копию словаря: d = dchars.copy() print(*d.items()) ('П', 'Б') ('Ф', 'В') ('К', 'Г') ('Ш', 'Ж') ('С', 'З') ('Т', 'Д') ('Х', 'К') Для ключей-строк существует второй способ определения словарей. Функция dict без аргументов создаёт пустой словарь. Его можно сразу инициализировать, если передать в эту функцию элементы словаря, перечислив их через запятую. Между ключом и значением нужно поставить знак присваивания. Значение может быть любым, но ключ должен быть строкой, но без кавычек – кавычки интерпретатор расставит самостоятельно: d = dict(П='Б', Ф='В', К='Г', Ш='Ж', С='З', Т='Д', Х='К') print(d) ('П', 'Б') ('Ф', 'В') ('К', 'Г') ('Ш', 'Ж') ('С', 'З') ('Т', 'Д') ('Х', 'К') В функцию dict можно передать и список списков. Вложенные списки разделяются запятыми. Каждый вложенный список – это элемент словаря – ключ и значение, которые разделяются запятыми: lst = [['П', 'Б'], ['Ф','В'], ['К','Г'], ['Ш','Ж'], ['С','З'], ['Т','Д'], ['Х','К']] d = dict(lst) print(d) ('П', 'Б') ('Ф', 'В') ('К', 'Г') ('Ш', 'Ж') ('С', 'З') ('Т', 'Д') ('Х', 'К') 249
Ключи в этом случае могут быть любого типа. Третий способ создания словаря – передать в метод fromkeys список ключей: lst = ['П', 'Ф', 'К', 'Ш', 'С', 'Т', 'Х'] d = dict.fromkeys(lst) print(d) Но всем ключам будет присвоено значение None: {'П': None, 'Ф': None, 'К': None, 'Ш': None, 'С': None, 'Т': None, 'Х': None} Вы можете присвоить ключам любое значение, если передадите в метод fromkeys значение вторым аргументом: lst = ['П', 'Ф', 'К', 'Ш', 'С', 'Т', 'Х'] d = dict.fromkeys (lst, 'БВГЖЗДК') print(d) Все ключи получат одно и то же значение, но потом значения ключей можно изменить. {'П': 'БВГЖЗДК', 'Ф': 'БВГЖЗДК', 'К': 'БВГЖЗДК', 'Ш': 'БВГЖЗДК', 'С': 'БВГЖЗДК', 'Т': 'БВГЖЗДК', 'Х': 'БВГЖЗДК'} Ключом могут быть только строковые и числовые литералы, а значения могут быть любого типа. Словарь можно создать из двух списков: keys = [1,2,3,4,5] values = ['раз','два','три','четыре','пять'] dict2 = dict(zip(keys, values)) 250
print(dict2) print() {1: 'раз', 2: 'два', 3: 'три', 4: 'четыре', 5: 'пять'} С тем же успехом можно взять два кортежа: keys = (1,2,3,4,5) values = ('раз','два','три','четыре','пять') dict2 = dict(zip(keys, values)) print(dict2) А множества могут дать курьёзные словари: keys = {1,2,3,4,5} values = {'раз','два','три','четыре','пять'} dict2 = dict(zip(keys, values)) print(dict2) {1: 'два', 2: 'четыре', 3: 'раз', 4: 'пять', 5: 'три'} Хороший словарь можно создать из списка кортежей с парами ключ - значение: items = [(1,'два'), (2,'четыре'), (3,'раз'), (4,'пять'), (5,'три')] dict2 = dict(items) print(dict2) print() {1: 'два', 2: 'четыре', 3: 'раз', 4: 'пять', 5: 'три'} 251
Если ключи – целые числа, то можно создать словарь из списка: values = ['раз','два','три','четыре','пять'] dict2 = { i : values[i] for i in range(len(values) ) } print(dict2) {0: 'раз', 1: 'два', 2: 'три', 3: 'четыре', 4: 'пять'} Если взять пару списков, то можно выбирать любые ключи: keys = [1,2,3,4,5] values = ['раз','два','три','четыре','пять'] dict2 = { keys[i] : values[i] for i in range(len(values) ) } print(dict2) {1: 'раз', 2: 'два', 3: 'три', 4: 'четыре', 5: 'пять'} values = ['раз','два','три','четыре','пять'] dict2 = { values[i] : 0 for i in range(len(values) ) } print(dict2) {'раз': 0, 'два': 0, 'три': 0, 'четыре': 0, 'пять': 0} Если значения - последовательные числа, то можно обойтись словарём значений: values = ['раз','два','три','четыре','пять'] dict2 = { values[i] : i + 1 for i in range(len(values) ) } print(dict2) 252
{'раз': 1, 'два': 2, 'три': 3, 'четыре': 4, 'пять': 5} Генераторы словарей Если нам нужно заполнить словарь результатами вычислений, то мы можем использовать генератор словаря. Пусть у нас есть функция fibo, которая вычисляет заданное число n-ое число Фибоначчи и возвращает его: # ВОЗВРАЩАЕТ n-ое ЧИСЛО ФИБОНАЧЧИ def fibo(n): a, b = 0, 1 for _ in range(1, n): a, b = b, a + b return b Мы хотим создать словарь, в котором ключ – это номер числа Фибоначчи, а значение – само число. В правой части инструкции присваивания записываем в фигурных скобках имя ключа – k и через двоеточие – его значение fibo(k). Затем мы пишем цикл for для переменной цикла k: # генерируем словарь с числами Фибоначчи: fibo_dict = { k : fibo(k) for k in range(1, 16) } В цикле for переменная k последовательно получает значения 1..15. Но текущее значение переменной k – это ключ в словаре. Для каждого ключа вычисляется значение fibo(k). После окончания цикла for словарь заполнен элементами ключ (k) : значение (fibo(k)). Убеждаемся в этом: # печатаем словарь: for k,v in fibo_dict.items(): 253
print(f'{k} --> {v}') print() Всё верно. 1 --> 1 2 --> 1 3 --> 2 4 --> 3 5 --> 5 6 --> 8 7 --> 13 8 --> 21 9 --> 34 10 --> 55 11 --> 89 12 --> 144 13 --> 233 14 --> 377 15 --> 610 Копирование словарей Метод copy создаёт поверхностную копию словаря, что вполне годится для простых словарей: dchars = {'П': 'Б', 'Ф': 'В', 'К': 'Г', 'Ш': 'Ж', 'С': 'З', 'Т': 'Д', 'Х': 'К'} dchars2 = dchars.copy() print(dchars) print(dchars2) dchars['П'] = 'ББ' 254
print(dchars) print(dchars2) {'П': {'П': {'П': {'П': 'Б', 'Ф': 'В', 'К': 'Г', 'Ш': 'Ж', 'С': 'З', 'Т': 'Д', 'Х': 'К'} 'Б', 'Ф': 'В', 'К': 'Г', 'Ш': 'Ж', 'С': 'З', 'Т': 'Д', 'Х': 'К'} 'ББ', 'Ф': 'В', 'К': 'Г', 'Ш': 'Ж', 'С': 'З', 'Т': 'Д', 'Х': 'К'} 'Б', 'Ф': 'В', 'К': 'Г', 'Ш': 'Ж', 'С': 'З', 'Т': 'Д', 'Х': 'К'} Такой же результат мы получим с помощью функции copy из одноимённого модуля: import copy dchars = {'П': 'Б', 'Ф': 'В', 'К': 'Г', 'Ш': 'Ж', 'С': 'З', 'Т': 'Д', 'Х': 'К'} dchars2 = copy.copy(dchars) print(dchars) print(dchars2) dchars['П'] = 'ББ' print(dchars) print(dchars2) print() Глубокая копия сделает, естественно, то же самое: dchars = {'П': 'Б', 'Ф': 'В', 'К': 'Г', 'Ш': 'Ж', 'С': 'З', 'Т': 'Д', 'Х': 'К'} dchars2 = copy.deepcopy(dchars) print(dchars) 255
print(dchars2) dchars['П'] = 'ББ' print(dchars) print(dchars2) Разница между поверхностной и глубокой копиями появится, если значениями словаря служат изменяемые объекты, например списки. В этом случае значения словаря хранят ссылки на списки, поэтому при изменении списка изменятся и значения в словаре: lst = [1,2,3,4,5] dchars = {'П': lst, 'Ф': lst, 'К': lst} dchars2 = dchars.copy() print(dchars) print(dchars2) #dchars['П'][0] = 6 lst[0] = 6 print(dchars) print(dchars2) {'П': {'П': {'П': {'П': [1, [1, [6, [6, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5], 5], 5], 5], 'Ф': 'Ф': 'Ф': 'Ф': [1, [1, [6, [6, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5], 5], 5], 5], 'К': 'К': 'К': 'К': [1, [1, [6, [6, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5]} 5]} 5]} 5]} При глубоком копировании в новом словаре создаются ссылки на новые списки, поэтому изменения в исходном списке не затрагивают значения в глубокой копии: lst = [1,2,3,4,5] dchars = {'П': lst, 'Ф': lst, 'К': lst} dchars2 = copy.deepcopy(dchars) print(dchars) 256
print(dchars2) #dchars['П'][0] = 6 lst[0] = 6 print(dchars) print(dchars2) {'П': {'П': {'П': {'П': [1, [1, [6, [1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5], 5], 5], 5], 'Ф': 'Ф': 'Ф': 'Ф': [1, [1, [6, [1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5], 5], 5], 5], 'К': 'К': 'К': 'К': [1, [1, [6, [1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5]} 5]} 5]} 5]} Доступ к элементам словаря Словарь это неупорядоченная коллекция, поэтому извлечь элемент по индексу невозможно. То же самое мы видели на примере множеств. А для извлечения элементов по индексу существуют упорядоченные коллекции – кортежи и списки. Чтобы получить значение элемента словаря, нужно после имени словаря в квадратных скобках записать ключ. Мы создали словарь dchars и хотим получить значение, соответствующее ключу - строке 'C'. Это значение – строка 'З': dchars = {'П': 'Ф': 'К': 'Ш': 'С': 'Т': 'Х': 'Б', 'В', 'Г', 'Ж', 'З', 'Д', 'К'} ch = dchars['C'] print(ch) Точно так же вы получите значение по любому другому ключу. 257
Но если ключ не существует, то программа завершится с ошибкой. Оператор in проверяет наличие в словаре заданного ключа: if 'С' in dchars: print(dchars['С']) else: print('Такого ключа нет!') print() if 'Ж' in dchars: print(dchars['Ж']) else: print('Такого ключа нет!') При сомнениях и колебаниях используйте безопасный метод get, который возвращает значение по заданному ключу или None, если ключ отсутствует: ch = dchars.get('Ш') print(ch) ch = dchars.get('Ё') print(ch) Вместо значения по умолчанию None, вы можете указать любое другое во втором аргументе: ch = dchars.get('Ё', 'Такого ключа нет!') print(ch) Метод setdefault возвращает значение по заданному ключу, если он существует. Если заданного ключа в словаре нет, то в словарь добавляется новый элемент с заданным ключом и значением None: ch = dchars.setdefault('Ш') print(ch) print(*dchars.items()) 258
print() ch = dchars.setdefault('Ё') print(ch) print(*dchars.items()) Ж ('П', 'Б') ('Ф', 'В') ('К', 'Г') ('Ш', 'Ж') ('С', 'З') ('Т', 'Д') ('Х', 'К') None ('П', 'Б') ('Ф', 'В') ('К', 'Г') ('Ш', 'Ж') ('С', 'З') ('Т', 'Д') ('Х', 'К') ('Ё', None) Этот метод можно использовать для добавления элементов в словарь, но более простой и понятный способ добавления элемента в словарь – присвоить новому ключу какое-либо значение: dchars['Ы'] = 'ЫШШО ОДИН ЁЖ' print(dchars) {'П': 'Б', 'Ф': 'В', 'К': 'Г', 'Ш': 'Ж', 'С': 'З', 'Т': 'Д', 'Х': 'К', 'Ё': None, 'Ы': 'ЫШШО ОДИН ЁЖ'} Метод pop удаляет пару ключ-значение с указанным ключом и возвращает удалённое значение: ch = dchars.pop('Ы') print(ch) print(*dchars.items()) ЫШШО ОДИН ЁЖ ('П', 'Б') ('Ф', 'В') ('К', 'Г') ('Ш', 'Ж') ('С', 'З') ('Т', 'Д') ('Х', 'К') ('Ё', None) 259
Если заданный ключ отсутствует, возникает ошибка, поэтому указывайте значение None вторым аргументом: ch = dchars.pop('Ё1', None) print(ch) print(*dchars.items()) В этом случае словарь не изменится, но ошибки не будет. None ('П', 'Б') ('Ф', 'В') ('К', 'Г') ('Ш', 'Ж') ('С', 'З') ('Т', 'Д') ('Х', 'К') ('Ё', None) Метод update обновляет значение существующего ключа или создаёт новый элемент с заданным ключом и значением: dchars.update(Ы='ДВА ЕЖА') dchars.update(Э='ЭНИКИ-БЕНИКИ') print(dchars) print() Ключ должен иметь строковый тип. Инструкция del также удаляет указанный элемент словаря: del dchars['Э'] print(dchars) print() Если заданный ключ отсутствует, возникает ошибка. Метод popitem возвращает кортеж из ключа и значения последнего добавленного элемента в словаре и удаляет его: 260
t = dchars.popitem() print(t) print(dchars) {'П': 'Б', 'Ф': 'В', 'К': 'Г', 'Ш': 'Ж', 'С': 'З', 'Т': 'Д', 'Х': 'К', 'Ё': None, 'Ы': 'ДВА ЕЖА'} ('Ы', 'ДВА ЕЖА') {'П': 'Б', 'Ф': 'В', 'К': 'Г', 'Ш': 'Ж', 'С': 'З', 'Т': 'Д', 'Х': 'К', 'Ё': None} Попытка удалить элемент из пустого словаря вызовет ошибку. Чтобы изменить значение существующего ключа, присвойте ему новое значение: dchars['П'] = 'БЭБЭ' print(*dchars.items()) ('П', 'БЭБЭ') ('Ф', 'В') ('К', 'Г') ('Ш', 'Ж') ('С', 'З') ('Т', 'Д') ('Х', 'К') ('Ё', None) Свойства словарей Функция len возвращает длину словаря, то есть число элементов в нём. # создаём словарь: dchars = {'П': 'Б', 'Ф': 'В', 'К': 'Г', 'Ш': 'Ж', 'С': 'З', 'Т': 'Д', 'Х': 'К'} 261
print(f'Длина словаря dchars = {len(dchars)}') print() Оператор in проверяет, есть ли указанный ключ в словаре: print('П' in dchars) print('ПП' in dchars) print('ПП' not in dchars) Метод clear удаляет все элементы из словаря: dchars.clear() print(dchars) print(len(dchars)) print(dchars == {}) print(bool(dchars)) В цикле for мы можем распечатать все ключи заданного словаря: dchars = {'П': 'Ф': 'К': 'Ш': 'С': 'Т': 'Х': 'Б', 'В', 'Г', 'Ж', 'З', 'Д', 'К'} for k in dchars: print(k, end=' ') По ключам мы легко найдём и соответствующие значения: for k in dchars: print(f'{k} --> {dchars[k]}') 262
Ф К Ш С Т Х --> --> --> --> --> --> В Г Ж З Д К Все ключи можно получить от метода keys: for k in dchars.keys(): print(k, end =' ') print() Все значения мы получим от метода values: for v in dchars.values(): print(v, end =' ') print() Но гораздо полезнее разом получить и ключи, и значения от метода items: for k,v in dchars.items(): print(k, v) П Ф К Ш С Т Х Б В Г Ж З Д К 263
Проект TwoSum Задача 1. Two Sum с сайта leetcode.com: Given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target. You may assume that each input would have exactly one solution, and you may not use the same element twice. You can return the answer in any order. Дан массив целых чисел nums и целочисленная сумма. Нужно, вернуть индексы таких двух чисел так, чтоб сумма соответствующих чисел равнялась заданной. Нельзя использовать один и тот же элемент массива дважды. Задача имеет ровно одно решение., Вы можете вернуть ответ в любом порядке. В функции main мы последовательно передаём в функцию twoSum список чисел и заданную сумму, а получаем из неё список индексов: def main(): nums = [2, 7, 11, 15] target = 9 nums = [3, 2, 4] target = 6 nums = [3, 3] target = 6 res = twoSum(nums, target) print(res) main() Самое простое решение – искать заданную сумму в двух вложенных циклах for, но со словарём решение изящнее. В одном цикле for мы последовательно получаем все числа из списка nums: 264
def twoSum(nums: list[int], target: int) -> list[int]: dict = {} for i in range(len(nums)): Находим второе число num, которое дополняет первое число nums[i] до заданной суммы target: num = target - nums[i] if num in dict: Если такое число как ключ имеется в словаре, то мы возвращаем значение этого ключа, то есть индекс числа в словаре, а также индекс второго числа – i: if dict.get(num) != None: return [dict[num], i] Если такого ключа в словаре не оказалось, то мы записываем туда число (ключ) и его индекс (значение): dict[nums[i]] = i Запускаем программу и получает ответы для всех наборов чисел: [0, 1] [1, 2] [0, 1] 265
Проект АЛЬТруизм В проекте АЛЬТернативный язык вы уже познакомились с языком юмориста Альтова. Всем, кому бонравилозь говоридь бо-альдовзги, просто необходим полный Зловарь альдовзгого языга. Мы как АЛЬТруисты займёмся созданием такого словаря в новом проекте. АЛЬТруист – это человек, который любит творчество Семёна Альтова и помогает людям любить его творчество. Словарь для перевода человеческого языка на альтовский мы уже создали: # создаём словарь: dchars = {'П': 'Б', 'Ф': 'В', 'К': 'Г', 'Ш': 'Ж', 'С': 'З', 'Т': 'Д', 'Х': 'К'} Теперь нам нужно загрузить список слов с диска – и можно вызывать функцию altov: # ЗАГРУЖАЕМ СЛОВАРЬ def load(fn): with open(fn) as file: spisok = file.readlines() # убираем символ переноса строки: return [s.rstrip('\n') for s in spisok] def main(): # словарь: fileName = 'makarov_frc.txt' 266
fileName = 'OSH-W97_frc.txt' # загружаем словарь: spisok = load(fileName) # переводим слова на альтовский язык: altov(spisok) С помощью словаря перевод это броздо жудга для нас: def altov(spisok : list[str]) -> None: # число изменений: n_min = 3 for s in spisok: if len(s) > 8: break res = '' n = 0 for ch in s: if ch in dchars: res += dchars[ch] n += 1 else: res += ch if n >= n_min: print(f'{s} --> {res}') print() В функции altov мы последовательно извлекаем из списка слова, избегая очень длинных. В цикле for получаем все буквы из каждого слова. Если очередная буква – это ключ в словаре dchars, то мы добавляем к строковой переменной res значение, соответствующее ключу. В противном случае добавляем букву без изменений. Не всегда альтовские слова смешные, поэтому в переменной n_min мы задаём минимальное число изменённых букв. После замены не менее n_min букв мы печатаем альтовское слово. Словарь готов! АНАРХИСТ АНТРЕКОТ АПОСТРОФ АПТЕКАРЬ --> --> --> --> АНАРКИЗД АНДРЕГОД АБОЗДРОВ АБДЕГАРЬ ШТАТСКИЙ ШТЕМПЕЛЬ ШТЕПСЕЛЬ ШТИБЛЕТА --> --> --> --> ЖДАДЗГИЙ ЖДЕМБЕЛЬ ЖДЕБЗЕЛЬ ЖДИБЛЕДА 267
АРЕСТАНТ АРТИСТКА АРФИСТКА АРХИВИСТ АСЕПТИКА АСКЕТИЗМ АСПИРАНТ АССОНАНС --> --> --> --> --> --> --> --> АРЕЗДАНД АРДИЗДГА АРВИЗДГА АРКИВИЗД АЗЕБДИГА АЗГЕДИЗМ АЗБИРАНД АЗЗОНАНЗ ШТОПАНЬЕ ШТРАФБАТ ШТРАФНИК ШТУКАТУР ШТУКОВКА ШТЫКОВКА ШУМНОСТЬ ЩУПЛОСТЬ ЭВКАЛИПТ ЭГОИСТКА ЭКЗОТИКА --> --> --> --> --> --> --> --> --> --> --> ЖДОБАНЬЕ ЖДРАВБАД ЖДРАВНИГ ЖДУГАДУР ЖДУГОВГА ЖДЫГОВГА ЖУМНОЗДЬ ЩУБЛОЗДЬ ЭВГАЛИБД ЭГОИЗДГА ЭГЗОДИГА Самые-пресамые альтовские слова содержат 5-6 замен. Многие из них очень змежные. СЕКСТЕТ --> ЗЕГЗДЕД СКЕПСИС --> ЗГЕБЗИЗ СКЕПТИК --> ЗГЕБДИГ СТАТИСТ --> ЗДАДИЗД СУФФИКС --> ЗУВВИГЗ ТАКСИСТ --> ДАГЗИЗД АТТЕСТАТ --> АДДЕЗДАД БИФШТЕКС --> БИВЖДЕГЗ КАСКЕТКА --> ГАЗГЕДГА КОНСПЕКТ --> ГОНЗБЕГД КОНТЕКСТ --> ГОНДЕГЗД КОСТЯШКА --> ГОЗДЯЖГА Занимательная логопедия -Февочка, скажи «ыба» - Селёдка! Логопедический диалог из комедии По семейным обстоятельствам 268
Некоторые люди неверно произносят отдельные буквы (точнее, звуки, обозначаемые этими буквами), а один логопеф не выговаривал даже половину букв русского алфавита, что ничуть не мешало ему учить других людей правильно говорить. Иногда это приводило к непониманию, например, названия улиц Кировская и Киевская в его исполнении звучали совершенно одинаково. В других случаях смысл его речей был понятен, но смешон. А «смешон» - это как раз то, что нам нужно! Логопеф) везде заменял букву Д буквой Ф (конечно, он произносил звуки, а не буквы, но у нас будут как раз буквы): вместо девочка он говорил февочка, вместо будет – буфет, ну и так фалее. Настоящий киношный логопед 269
Проект Логопеф Перевод слов на логопефический язык ничем не отличается от перевода на язык альтовский. При создании словаря мы только должны учесть, что буква Д в тексте может быть и строчной, и ПРОПИСНОЙ: # создаём словарь: dchars = {'Д': 'Ф', 'д': 'ф'} В остальной части программы нужно изменить только имя функции и комментарий: def logopef(spisok : list[str]) -> None: # число изменений: n_min = 2 for s in spisok: if len(s) > 8: break res = '' n = 0 for ch in s: if ch in dchars: res += dchars[ch] n += 1 else: res += ch if n >= n_min: print(f'{s} --> {res}') print() # ЗАГРУЖАЕМ СЛОВАРЬ def load(fn): with open(fn) as file: spisok = file.readlines() # убираем символ переноса строки: return [s.rstrip('\n') for s in spisok] 270
def main(): # словарь: fileName = 'makarov_frc.txt' fileName = 'OSH-W97_frc.txt' # загружаем словарь: spisok = load(fileName) # переводим слова на логопефский язык: logopef(spisok) main() Получилось фовольно сфорово. . . . ДАРМОЕД ДИАДЕМА ДОВОДКА ДОГАДКА ДОДЕЛКА ДОМОСЕД ДУРАНДА ДЫМОХОД ИНДИВИД ЛЕДОХОД . . . --> --> --> --> --> --> --> --> --> --> ФАРМОЕФ ФИАФЕМА ФОВОФКА ФОГАФКА ФОФЕЛКА ФОМОСЕФ ФУРАНФА ФЫМОХОФ ИНФИВИФ ЛЕФОХОФ Слова с тремя фефефектами речи. АДДЕНДА --> АФФЕНФА ДЕЙДВУД --> ФЕЙФВУФ АДДЕНДУМ --> АФФЕНФУМ ДИВИДЕНД --> ФИВИФЕНФ Для пущего смеху лучше подобрать текст, в котором много букв Д. Например, в Интернете можно найти «однобуквицу» про Деда Данилыча, которая вполне годится для наших экспериментов. 271
Длинною дорогой, двигался дед Данилыч домой. Дивная дубрава душила дебрями духоту, давая деду дышать. "Доброго дня, доброй дороги"-дрожали дружно деревья. "Длиннее дорога драгоценней дружба"-доносил до дубов Данилыч. Дома деда дожидалась Дарья Дмитриевна. Данилыч дарил Дарье Дмитревне душистые диадемы. Дарья Дмитриевна давала дорогому другу дольку дыни. Данилыч доедал дыню, далее дремал, давя диван. Тексты, все слова которых начинаются на одну и ту же букву, называются тавтограммами. Самая известная тавтограмма: Четыре чёрненьких чумазеньких чертёнка чертили чёрными чернилами чертёж - чрезвычайно чисто. Для красоты печати пишем пару новых функций: def load2(fn): with open(fn) as file: spisok = file.readlines() return spisok def logopef2(spisok : list[str]) -> None: for s in spisok: res = '' for ch in s: if ch in dchars: res += dchars[ch] else: res += ch print(res) print() def main(): fileName = 'Дед Данилыч.txt' spisok = load2(fileName) print(*spisok) spisok = load(fileName) 272
logopef2(spisok) main() Длинною дорогой, двигался дед Данилыч домой. Дивная дубрава душила дебрями духоту, давая деду дышать. "Доброго дня, доброй дороги"-дрожали дружно деревья. "Длиннее дорога драгоценней дружба"-доносил до дубов Данилыч. Дома деда дожидалась Дарья Дмитриевна. Данилыч дарил Дарье Дмитревне душистые диадемы. Дарья Дмитриевна давала дорогому другу дольку дыни. Данилыч доедал дыню, далее дремал, давя диван. Флинною форогой, фвигался феф Фанилыч фомой. Фивная фубрава фушила фебрями фухоту, фавая фефу фышать. "Фоброго фня, фоброй фороги"-фрожали фружно феревья. "Флиннее форога фрагоценней фружба"-фоносил фо фубов Фанилыч. Фома фефа фожифалась Фарья Фмитриевна. Фанилыч фарил Фарье Фмитревне фушистые фиафемы. Фарья Фмитриевна фавала форогому фругу фольку фыни. Фанилыч фоефал фыню, фалее фремал, фавя фиван. Тыблоки и яквы Компьютерное тыблоко Алексей Иванович Пантелеев в рассказе Буква "ты" учил маленькую девочку читать и писать. Иринушка, так звали девочку, быстро осваивала алфавит, пока они не дошли до последней буквы – Я, которую Иринушка упорно называла буквой ТЫ. Например, предложение Якову дали яблоко она читала так: Тыкову дали тыблоко. И так продолжалось до тех пор, пока Пантелеев не сказал Иринушке, что буква называется не Я, а ТЫ. Эта уловка 273
помогла, и она стала читать все слова правильно. Но мы поищем в русском языке именно тыблоки. Проект Тыблоки и яквы Опять всё дело в простом словаре: # создаём словарь: dchars = {'Я': 'ТЫ', 'я': 'ты'} Функцию tybloko легко получаем от логопефа: # ЗАГРУЖАЕМ СЛОВАРЬ def load(fn): with open(fn) as file: spisok = file.readlines() # убираем символ переноса строки: return [s.rstrip('\n') for s in spisok] def tybloko(spisok : list[str]) -> None: # число изменений: n_min = 2 for s in spisok: if len(s) > 8: break res = '' n = 0 for ch in s: if ch in dchars: res += dchars[ch] n += 1 else: res += ch if n >= n_min: print(f'{s} --> {res}') print() 274
def main(): # словарь: fileName = 'makarov_frc.txt' fileName = 'OSH-W97_frc.txt' # загружаем словарь: spisok = load(fileName) # переводим слова на тыблочный язык: tybloko(spisok) main() Единственное слово с тремя ТЫ. ЯСНОВИДЯЩАЯ → ТЫСНОВИДТЫЩАТЫ Иногда тыблоки получаются обычными словами: ЯК – ТЫК ЯЛ – ТЫЛ СОЯ – СОТЫ БАНЯ – БАНТЫ БУРЯ – БУРТЫ СВАЯ – СВАТЫ УНИЯ – УНИТЫ Но попадаются и очень смешные тыблоки: МЯЧ – МТЫЧ МАЯК – МАТЫК ТУЯ – ТУТЫ ПЕНЯ – ПЕНТЫ ЯЗЬ – ТЫЗЬ ПЛЯЖ – ПЛТЫЖ БЯЗЬ – БТЫЗЬ ЯВОР – ТЫВОР БЯКА – БТЫКА ЯДРО – ТЫДРО ДОЯР – ДОТЫР ЯЗЫК – ТЫЗЫК 275
ДУЛЯ – ДУЛТЫ ЯЙЦО – ТЫЙЦО ДЫНЯ – ДЫНТЫ ЯЛИК – ТЫЛИК ЗАРЯ – ЗАРТЫ ЯНКИ – ТЫНКИ ЗАЯЦ – ЗАТЫЦ ЯСЛИ – ТЫСЛИ ЗМЕЯ – ЗМЕТЫ ЯХТА – ТЫХТА ЗЮЗЯ – ЗЮЗТЫ ЯШМА – ТЫШМА КЛЯП – КЛТЫП ЯЩЕР – ТЫЩЕР КРЯЖ – КРТЫЖ ЯЩИК – ТЫЩИК Можно перевести на тыблочный язык стихотворения и песни: # создаём словарь: dchars = {'Я': 'ТЫ', 'я': 'ты'} # ЗАГРУЖАЕМ СЛОВАРЬ def load(fn): with open(fn) as file: spisok = file.readlines() # убираем символ переноса строки: return [s.rstrip('\n') for s in spisok] def tybloko2(spisok : list[str]) -> None: for s in spisok: res = '' for ch in s: if ch in dchars: res += dchars[ch] else: res += ch print(res) print() def load2(fn): with open(fn) as file: spisok = file.readlines() return spisok 276
def main(): fileName = 'Парус.txt' #fileName = 'Яблочко.txt' spisok = load2(fileName) print(*spisok) spisok = load(fileName) tybloko2(spisok) main() Но в русских словах буква Я встречается нечасто, так что особого успеха здесь не ждите. Яблочко ТЫблочко Эх, яблочко, Да куда котишься? Ко мне в рот попадёшь — Да не воротишься! Эх, тыблочко, Да куда котишьсты? Ко мне в рот попадёшь — Да не воротишьсты! Эх, яблочко, да скороспелое! И петь, и плясать мы умелые! Эх, тыблочко, да скороспелое! И петь, и плтысать мы умелые! Эх, яблочко, созрело вкусное! Не надо сидеть с миной грустною! Эх, тыблочко, созрело вкусное! Не надо сидеть с миной грустною! Парус Парус Белеет парус одинокой В тумане моря голубом!.. Что ищет он в стране далекой? Что кинул он в краю родном?.. Белеет парус одинокой В тумане морты голубом!.. Что ищет он в стране далекой? Что кинул он в краю родном?.. Играют волны - ветер свищет, И мачта гнется и скрыпит... Увы! он счастия не ищет И не от счастия бежит! Играют волны - ветер свищет, И мачта гнетсты и скрыпит... Увы! он счаститы не ищет И не от счаститы бежит! 277
Под ним струя светлей лазури, Над ним луч солнца золотой... А он, мятежный, просит бури, Как будто в бурях есть покой! Под ним струты светлей лазури, Над ним луч солнца золотой... А он, мтытежный, просит бури, Как будто в буртых есть покой! Проект Аналитическая статистика Статистика знает всё Ильф и Петров Американская мафиозно-аналитическая комедия Прежде чем анализировать, нужно собрать необходимую информацию. В этом проекте мы будем разбирать литературные тексты, но чисто программистским способом, то есть простым подсчётом букв и слов, что иногда позволяет правильно оценить пристрастия автора к тем или иным словам, буквам и звукам. 278
Поскольку нас интересуют, в первую очередь, литературные тексты на русском языке, то объявим строковую константу RUSLETTERS со всеми буквами отечественного алфавита и переменную letters для их хранения: # все русские буквы: RUSLETTERS = "АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ" Для проверки работоспособности и функциональности нашей программы вполне разумно начать статистические выкладки со словаря русского языка. Для наших лингвистических мероприятий нам нужны 3 словаря: # словари: words = {} starts = {} letters = {} Словарь words хранит слова, starts – число слов, начинающихся с той или иной буквы, а letters – отдельные символы. Как обычно, начинаем с загрузки списка слов: # ЗАГРУЖАЕМ СЛОВАРЬ def load(fn): with open(fn) as file: spisok = file.readlines() # убираем символ переноса строки: 279
return [s.rstrip('\n') for s in spisok] def main(): # словарь: fileName = 'makarov_frc.txt' fileName = 'OSH-W97_frc.txt' # загружаем словарь: spisok = load(fileName) # считаем слова: statistic(spisok) main() Если в качестве источника информации вы используете текст, то в нём могут попадаться и такие «слова», которые начинаются с «непозволительных» символов. Избавиться от них вам поможет строковая константа BADCHARS, в которую следует записать все символы-изгои: # посторонние начальные символы: BADCHARS = "-0123456789" Обработку списка слов мы проводим с помощью словаря dict. В цикле for перебираем список слов и пропускаем те слова, которые не могут быть словами, поскольку начинаются «не с той» буквы. Одновременно мы ведём учёт и контроль числа повторяющихся слов. Если в словаре такие слова могут присутствовать только вследствие ошибки, то в литературном тексте многие слова наверняка повторяются: # СЧИТАЕМ СЛОВА И БУКВЫ def statistic(spisok : list[str]) -> None: # словари: words = {} starts = {} letters = {} for s in spisok: 280
# пропускаем "плохие" слова: if s[0] in BADCHARS: continue # считаем слова: if s not in words.keys(): words[s] = 1 else: words[s] += 1 # считаем слова с начальной буквой: if s[0] not in starts.keys(): starts[s[0]] = 1 else: starts[s[0]] += 1 for ch in s: if ch not in letters.keys(): letters[ch] = 1 else: letters[ch] += 1 # печатаем статистику: for k in words: print(f'{k} : {words[k]}') print() for k in starts: print(f'{k} : {starts[k]}') print() for k in letters: print(f'{k} : {letters[k]}') print() print(f'Всего слов: {len(words)}') print(f'Всего букв: {sum(letters.values())}') print() Как и следовало ожидать, все слова в словаре Ожегова и Шведовой присутствуют в единственном числе. ДОСТОПРИМЕЧАТЕЛЬНОСТЬ : 1 КОНКУРЕНТОСПОСОБНОСТЬ : 1 281
НЕДОБРОКАЧЕСТВЕННОСТЬ : 1 ОБЩЕУПОТРЕБИТЕЛЬНОСТЬ : 1 ПОКРОВИТЕЛЬСТВЕННОСТЬ : 1 ПРОТИВОЕСТЕСТВЕННОСТЬ : 1 РАСПРОСТРАНИТЕЛЬНОСТЬ : 1 СВЕТОЧУВСТВИТЕЛЬНОСТЬ : 1 ЕСТЕСТВОИСПЫТАТЕЛЬНИЦА : 1 НЕУДОВЛЕТВОРИТЕЛЬНОСТЬ : 1 ПЕРЕОСВИДЕТЕЛЬСТВОВАНИЕ : 1 ЧЕЛОВЕКОНЕНАВИСТНИЧЕСТВО : 1 Более интересно узнать, сколько слов (только существительных!) в русском языке начинается с буквы А или Э. А Г Д Ё И Л М О П Р С У Ф Щ Ю : : : : : : : : : : : : : : : 797 910 1079 9 679 726 1346 1618 4102 1467 2815 722 535 70 53 Я Б В Е Ж З Й К Н Т Х Ц Ч Ш Э : : : : : : : : : : : : : : : 133 1376 1331 99 248 900 9 2207 1262 1090 378 182 421 474 376 Всем любителям словесных игр известно, что русские слова предпочитают начинаться с буквы П. А вот в середине слов буква П далеко не на первом мечте, а лидирует здесь буква О. А Д З Р : : : : 22639 5855 4032 14637 Ф Щ Ю К : : : : 1287 1046 601 12020 282
С Г О Ё Ж И Л Я М П Е Э У : : : : : : : : : : : : : 13962 3362 23642 1023 1788 18180 10161 3115 5508 7406 18828 463 5159 Т Х Б Й Ч Н Ы В Ш Ь Ц Ъ : : : : : : : : : : : : 16306 1508 4056 1078 2956 16731 1812 8633 1850 6729 2757 78 Легко видеть, что слова и буквы поступают в словари в порядке живой очереди, а нам удобнее анализировать слова и буквы по частоте. Импортируем класс Counter из модуля collections: from collections import Counter def statistic2(spisok : list[str]) -> None: # словари: words = {} starts = {} letters = {} for s in spisok: # пропускаем "плохие" слова: if s[0] in BADCHARS: continue # считаем слова: if s not in words: words[s] = 1 else: words[s] += 1 283
# считаем слова с начальной буквой: if s[0] not in starts: starts[s[0]] = 1 else: starts[s[0]] += 1 for ch in s: if ch not in letters: letters[ch] = 1 else: letters[ch] += 1 # печатаем статистику: for k in words: print(f'{k} : {words[k]}') print() for k in starts: print(f'{k} : {starts[k]}') print() Создаём экземпляр класса Counter из словаря. Метод most_common возвращает элементы словаря, отсортированные по значениям от больших к меньшим. Метод reverse сортирует элементы словаря от меньших к большим. Печатаем элементы словаря, отсортированные по значениям: c = Counter(starts) cc = c.most_common() cc.reverse() for t in cc: print(*t) print() for k in letters: print(f'{k} : {letters[k]}') print() c = Counter(letters) cc = c.most_common() cc.reverse() for t in cc: print(*t) 284
print() print(f'Всего слов: {len(words)}') print(f'Всего букв: {sum(letters.values())}') print() Запускаем программу и получаем легкоусваиваемую статистику: - число слов, начинающихся с … Й Ё Ю Щ Е Я Ц Ж Э Х Ч Ш Ф И У 9 9 53 70 99 133 182 248 376 378 421 474 535 679 722 Л А З Г Д Т Н В М Б Р О К С П 726 797 900 910 1079 1090 1262 1331 1346 1376 1467 1618 2207 2815 4102 У М Д Ь П В Л К С Р 5159 5508 5855 6729 7406 8633 10161 12020 13962 14637 - общее число букв в словах. Ъ Э Ю Ё Щ Й Ф Х Ж Ы 78 463 601 1023 1046 1078 1287 1508 1788 1812 285
Ш Ц Ч Я Г З Б 1850 2757 2956 3115 3362 4032 4056 Т Н И Е А О 16306 16731 18180 18828 22639 23642 Проект Аналитическая статистика 2 Давайте обратимся к реальному литературному тексту. В своё время в Советском Союзе были очень популярны романы Анн и Сержа Голон про Анжелику, а также их экранизации с французской красавицей Мишель Мерсье в главной роли. Я предлагаю «проанализировать» один из наиболее удачных романов - Анжелика и король. Обложка книги и «афиша» фильма 286
Я нашёл в Интернете текст этого романа и отсортировал слова описанным выше способом. Общее число слов в романе – 88314, но разных – только 18141, то есть большинство слов повторяется. В этом проекте я ограничился только первой главой, так что цифры получились изрядно скромнее: Русская статистика французского романа Начало романа в интернетовской интерпретации выглядит так: В эту ночь Анжелика никак не могла уснуть. Трепетные видения волнующих событий грядущего дня виделись ей. У нее было состояние ребенка в канун Рождества. Дважды она вскакивала и зажигала свечу, испытывая желание рассмотреть оба костюма, лежавшие на стульях возле кровати, — один для предстоящей завтра королевской охоты, другой — для торжества, последующего за ней. Наряд для охоты явно нравился ей. Она дала точные указания портному, чтобы придать жемчужно-серому бархатному жакету мужской покрой, облегающий нежные округлости ее фигуры. Огромная шляпа из белого фетра с каскадами страусовых перьев была подобна снежной лавине. Нашу первую задачу – загрузить текст и перевести его в верхний регистр – мы решаем в функции load: def main(): # текст: fileName = 'Анжелика.txt' # загружаем текст: text = load(fileName) print(text) main() 287
Это вы умеете делать: # ЗАГРУЖАЕМ СЛОВАРЬ def load(fn): with open(fn) as file: spisok = file.readlines() # убираем символ переноса строки: return [s.rstrip().upper() for s in spisok] В итоге мы получаем список строк со всеми знаками препинания. [' В ЭТУ НОЧЬ АНЖЕЛИКА НИКАК НЕ МОГЛА УСНУТЬ. ТРЕПЕТНЫЕ ВИДЕНИЯ ВОЛНУЮЩИХ СОБЫТИЙ ГРЯДУЩЕГО ДНЯ ВИДЕЛИСЬ ЕЙ. У НЕЕ БЫЛО СОСТОЯНИЕ РЕБЕНКА В КАНУН РОЖДЕСТВА.', ' ДВАЖДЫ ОНА ВСКАКИВАЛА И ЗАЖИГАЛА СВЕЧУ, ИСПЫТЫВАЯ ЖЕЛАНИЕ РАССМОТРЕТЬ ОБА КОСТЮМА, ЛЕЖАВШИЕ НА СТУЛЬЯХ ВОЗЛЕ КРОВАТИ, — ОДИН ДЛЯ ПРЕДСТОЯЩЕЙ ЗАВТРА КОРОЛЕВСКОЙ ОХОТЫ, ДРУГОЙ — ДЛЯ ТОРЖЕСТВА, ПОСЛЕДУЮЩЕГО ЗА НЕЙ.', ' НАРЯД ДЛЯ ОХОТЫ ЯВНО НРАВИЛСЯ ЕЙ. ОНА ДАЛА ТОЧНЫЕ УКАЗАНИЯ ПОРТНОМУ, ЧТОБЫ ПРИДАТЬ ЖЕМЧУЖНО-СЕРОМУ БАРХАТНОМУ ЖАКЕТУ МУЖСКОЙ ПОКРОЙ, ОБЛЕГАЮЩИЙ НЕЖНЫЕ ОКРУГЛОСТИ ЕЕ ФИГУРЫ. ... Из всего этого орфографического и синтаксического изобилия мы должны оставить только пробелы и буквы русского языка: # выделяем слова: spisok = [] for s in text: #print('s =',s) w = '' for ch in s: if ch == ' ' or ch in RUSLETTERS: w += ch В переменной w мы получаем из списка строки без лишних символов. Метод split разбивает строку на отдельные слова по пробелам. Если слово не пустое, то мы добавляем его в список слов: 288
w = w.split() print('w.split =', w) if w: spisok += w print(spisok) Теперь у нас есть список всех слов из первой главы романа в порядке поступления. w.split = ['В', 'ЭТУ', 'НОЧЬ', 'АНЖЕЛИКА', 'НИКАК', 'НЕ', 'МОГЛА', 'УСНУТЬ', 'ТРЕПЕТНЫЕ', 'ВИДЕНИЯ', 'ВОЛНУЮЩИХ', 'СОБЫТИЙ', 'ГРЯДУЩЕГО', 'ДНЯ', 'ВИДЕЛИСЬ', 'ЕЙ', 'У', 'НЕЕ', 'БЫЛО', 'СОСТОЯНИЕ', 'РЕБЕНКА', 'В', 'КАНУН', 'РОЖДЕСТВА'] . . . Пришло время потрудиться нашей функции statistics, и мы узнаём, что большинство слов встречается в тексте одноразово, а наиболее часто - сама Анжелика, местоимения и предлоги. ПРОКЛЯТЫЙ 1 ЗАХОДА 1 ДОБЕРУСЬ 1 ЭКИПАЖА 1 РВОТЫ 1 ЛЕЖУ 1 ОТСУТСТВИЕ 1 ОБЪЯСНИЛ 1 ЗНАЕТ 1 ПОЛДЕНЬ 1 ОТСЧИТАЛИ 1 ЧАСЫ 1 ВИНА 1 . . . ОН 33 ВЫ 39 АНЖЕЛИКА 42 КАК 47 С 48 ЕЕ 59 Я 63 ЧТО 65 НА 73 НЕ 89 ОНА 92 В 125 И 137 Всего 7 слов начинаются с буквы Ю, а вот с буквы П начинается больше всего слов. Ю Ц Ш Ф Х Л 7 9 26 37 38 63 Ч З Е Т Б Д 125 136 149 181 204 210 289
Я Ж Г Э А У Р 69 69 80 82 96 116 124 И М К О С Н В П 215 222 262 362 394 419 502 506 Во всех словах твёрдый знак встречается всего 6 раз. А буква О опять лидирует. Ъ Ф Ц Э Щ Ю Ш Х Й Ч Ж Г З Б Ы Я 6 53 68 84 101 165 219 231 273 329 359 368 395 444 493 555 Ь У Д П М К Р В Л С Т И Н А Е О 557 720 723 750 754 870 1033 1088 1240 1274 1458 1560 1570 2176 2206 2623 Легко заметить, что одной буквы в списке не хватает. И нетрудно догадаться, что это буква Ё, которую обычно не печатают. Функция statistics вам известна: # СЧИТАЕМ СЛОВА И БУКВЫ def statistic(spisok : list[str]) -> None: # словари: words = {} starts = {} letters = {} for s in spisok: # считаем слова: if s not in words: 290
words[s] = 1 else: words[s] += 1 # считаем слова с начальной буквой: if s[0] not in starts: starts[s[0]] = 1 else: starts[s[0]] += 1 for ch in s: if ch not in letters: letters[ch] = 1 else: letters[ch] += 1 # печатаем статистику: #for k in words: # print(f'{k} : {words[k]}') #print() c = Counter(words) cc = c.most_common() cc.reverse() for t in cc: print(*t) print() #for k in starts: # print(f'{k} : {starts[k]}') #print() c = Counter(starts) cc = c.most_common() cc.reverse() for t in cc: print(*t) print() #for k in letters: # print(f'{k} : {letters[k]}') #print() c = Counter(letters) cc = c.most_common() cc.reverse() 291
for t in cc: print(*t) print() print(f'Всего слов: {len(spisok)}') print(f'Всего разных слов: {len(words)}') print(f'Всего букв: {sum(letters.values())}') print() Методы get могут заменить условные операторы if-else, чтобы сократить код, но вряд ли он станет более понятным: for s in spisok: # считаем слова: words[s] = words.get(s, 0) + 1 #if s not in words: # words[s] = 1 #else: # words[s] += 1 # считаем слова с начальной буквой: starts[s[0]] = starts.get(s[0], 0) + 1 #if s[0] not in starts: # starts[s[0]] = 1 #else: # starts[s[0]] += 1 for ch in s: letters[ch] = letters.get(ch, 0) + 1 #if ch not in letters: # letters[ch] = 1 #else: # letters[ch] += 1 . . . Иногда полезнее получить словарь, отсортированный не по частоте слов, а по алфавиту. А 30 АБСОЛЮТНАЯ 1 АГА 1 ЮБКА 1 ЮБКЕ 1 ЮБКОЙ 1 292
АД 1 АЛТАРЬ 1 АЛЬКОВА 1 АМАЗОНКИ 1 АНГЕЛ 1 АНЖЕЛИКА 42 АНЖЕЛИКЕ 5 АНЖЕЛИКИ 3 . . . ЮБКУ 1 ЮВЕЛИР 1 ЮВЕЛИРА 1 ЮНЫЙ 1 Я 63 ЯВНО 1 ЯРДЫ 1 ЯРКОАЛЫЙ 1 ЯРОСТИ 1 ЯРОСТЬ 1 ЯСНО 1 Для этого нужно отсортировать элементы словаря: swords = sorted(words.items()) for t in swords: print(*t) print() Или так: for k in sorted(words): print(k, words[k]) print() Проект Фракционирование словаря 2 В проекте Фракционирование словаря мы фракционировали список слов в списке. В этом проекте мы используем для этого словарь. В функции main мы загружаем список слов и отправляем его в функцию frac, откуда получаем фракционированный словарь: def main(): # словарь: fileName = 'OSH-W97.txt' 293
# загружаем словарь: spisok = load(fileName) # фракционируем словарь: spisok = frac(spisok) # распечатываем фракционированный словарь: for s in spisok: print(s) print(len(spisok)) print() # сохраняем фракционированный список в файле: with open('OSH-W97_frc_2.txt', 'w') as f: for s in spisok: f.write(f"{s}\n") main() def load(fn): with open(fn) as file: spisok = file.readlines() # убираем символ переноса строки: return [s.rstrip('\n') for s in spisok] Для сортировки элементов словаря по значению нам нужен оператор itemgetter из модуля operator: # Фракционирование словаря с помощью словаря from operator import itemgetter В функции frac мы генерируем словарь с ключом – слово из словаря, значение – длина этого слова: # ФРАКЦИОНИРУЕМ СЛОВАРЬ def frac(spisok): spisok = { w : len(w) for w in spisok } 294
В функцию sorted мы передаём все значения из словаря, то есть длины слов. Параметру key передаём итератор для значений, то есть для длин слов в словаре. Обратите внимание, что оператор itemgetter получает 1 для сортировки словаря по значению; для сортировки словаря по ключу оператору itemgetter получит 0. Функция sorted возвращает список, который мы конвертируем в словарь, хотя в этом проекте это необязательно: spisok = dict(sorted(spisok.items(), key=lambda item: item[1])) Отсортированный по значению словарь возвращаем в функцию main: return spisok Для проверки распечатываем фракционированный словарь: # распечатываем фракционированный словарь: for s in spisok: print(s) print(len(spisok)) print() Фракционирование списка слов прошло успешно. АД АЗ АР АС ГО ДО ЁЖ ИЛ ЛЯ МИ ОМ НЕДОБРОКАЧЕСТВЕННОСТЬ ОБЩЕУПОТРЕБИТЕЛЬНОСТЬ ПОКРОВИТЕЛЬСТВЕННОСТЬ ПРОТИВОЕСТЕСТВЕННОСТЬ РАСПРОСТРАНИТЕЛЬНОСТЬ СВЕТОЧУВСТВИТЕЛЬНОСТЬ ЕСТЕСТВОИСПЫТАТЕЛЬНИЦА НЕУДОВЛЕТВОРИТЕЛЬНОСТЬ ПЕРЕОСВИДЕТЕЛЬСТВОВАНИЕ ЧЕЛОВЕКОНЕНАВИСТНИЧЕСТВО 27414 295
. . . По умолчанию функция sorted возвращает список, отсортированный по возрастанию значений. Но если параметру reverse задать значение True, то список будет отсортирован в обратном порядке: spisok = dict(sorted(spisok.items(), key=itemgetter(1), reverse=True)) ЧЕЛОВЕКОНЕНАВИСТНИЧЕСТВО ПЕРЕОСВИДЕТЕЛЬСТВОВАНИЕ ЕСТЕСТВОИСПЫТАТЕЛЬНИЦА НЕУДОВЛЕТВОРИТЕЛЬНОСТЬ ВЗАИМООБУСЛОВЛЕННОСТЬ ДЕЛОПРОИЗВОДИТЕЛЬНИЦА ДОСТОПРИМЕЧАТЕЛЬНОСТЬ КОНКУРЕНТОСПОСОБНОСТЬ НЕДОБРОКАЧЕСТВЕННОСТЬ ОБЩЕУПОТРЕБИТЕЛЬНОСТЬ ПОКРОВИТЕЛЬСТВЕННОСТЬ ПРОТИВОЕСТЕСТВЕННОСТЬ РАСПРОСТРАНИТЕЛЬНОСТЬ . . . УМ УС ФА ЩИ ЮГ ЮЗ ЮР ЮС ЯД ЯК ЯЛ ЯМ ЯР 27414 Оператор itemgetter можно с успехом заменить лямбда-выражением: spisok = dict(sorted(spisok.items(), key=lambda item: item[1]) Проект Самое длинное число Этот проект чисто тренировочный, поэтому не ищите в нём глубокого смысла. Итак, мы генерируем словарь со случайными числами: 296
from random import randrange def solve(): # генерируем словарь со случайными числами: dict = {k: randrange(1000000) for k in range(1, 16)} # печатаем словарь: for k,v in dict.items(): print(f'{k} --> {v}') print() И находим в нём самое длинное число: res = max(dict.values(), key=lambda n: len(str(n))) print(res) def main(): solve() main() Этот поиск был бы более уместен со словами, но там он был бы гораздо проще – достаточно указать ключ по длине слов: key=len. С числами так просто не получится, потому что функция len применима только к строкам. Но функция str быстро превратит число в строку – и задача решена. 1 --> 415484 2 --> 674801 3 --> 904138 4 --> 216014 5 --> 322170 6 --> 250551 7 --> 203605 8 --> 867506 9 --> 66900 10 --> 239550 297
11 12 13 14 15 --> --> --> --> --> 448398 406492 239627 395153 211121 415484 Давайте решим более числовую задачу – найти в списке случайных чисел такое число, у которого сумма цифр наибольшая. Здесь нам понадобится функция get_all_digits, которая возвращает список всех цифр заданного числа: # ВОЗВРАЩАЕТ СПИСОК ЦИФР ЗАДАННОГО ЧИСЛА def get_all_digits(n: int | None = None) -> list[int]: if n == None or n == 0: return [0] # убираем минус: n = abs(n) digs : list[int] = [] while n > 0: digs = [n % 10] + digs n //= 10 return digs Переделываем ключ под наши нужды: res = max(dict.values(), key=lambda n: sum(get_all_digits(n))) И находим самое тяжёлое на цифры число: res = max(dict.values(), key=lambda n: sum(get_all_digits(n))) print(res, sum(get_all_digits(res))) 1 --> 991181 298
2 --> 852255 3 --> 638981 4 --> 371703 5 --> 959822 6 --> 648898 7 --> 57876 8 --> 224344 9 --> 688389 10 --> 399116 11 --> 223515 12 --> 147134 13 --> 243767 14 --> 688910 15 --> 420970 648898 43 Задавая параметру key разные функции, вы сможете найти в словаре значения с заданными свойствами. Но только самое первое значение, если их несколько. Проект LeetCode 1207. Unique Number of Occurrences Задача 1207. Unique Number of Occurrences: Given an array of integers arr, return true if the number of occurrences of each value in the array is unique, or false otherwise. Example 1: Input: arr = [1,2,2,1,1,3] Output: true Explanation: The value 1 has 3 occurrences, 2 has 2 and 3 has 1. No two values have the same number of occurrences. Example 2: Input: arr = [1,2] Output: false 299
Example 3: Input: arr = [-3,0,1,-3,1,1,1,-3,10,0] Output: true Constraints: 1 <= arr.length <= 1000 -1000 <= arr[i] <= 1000 Дан массив целых чисел. Если все числа в массиве повторяются неодинаковое число раз, то функция должна вернуть True. В противном случае функция должна вернуть False. Число повторений мы легко найдём с помощью словаря: # РЕШАЕМ ЗАДАЧУ def solve(lst: list[int]) -> bool: dict = {} for num in lst: dict[num] = dict.get(num, 0) + 1 nums = dict.values() def main(): #llst = [1,2,2,1,1,3] #lst = [1,2] lst = [-3,0,1,-3,1,1,1,-3,10,0] res = solve(lst) print(res) if __name__ == '__main__': main() Легко видеть, что длина множества будет короче, чем длина списка, если в списке числа повторяются. На этом наблюдении решение задачи заканчивается: return len(nums) == len(set(nums)) 300
Проект Мемоизация. Числа Фибоначчи и пути на сетке 301
Мемоизация - это способ оптимизации программы: результаты вычислений сохраняют, или запоминают в кэше, чтобы при повторном запросе с теми же входными данными не пересчитывать их заново, а сразу вернуть сохранённый результат. Этот способ обычно применяют, когда функция медленная, а вызовы с одинаковыми аргументами встречаются часто - как в рекурсивных алгоритмах. Числа Фибоначчи определяются так: • • • 𝐹(0) = 0 𝐹(1) = 1 𝐹(𝑛) = 𝐹(𝑛 − 1) + 𝐹(𝑛 − 2)при 𝑛 > 1 Рекурсивная реализация напрямую следует этой формуле, поэтому одни и те же значения вычисляются многократно. Например, чтобы посчитать 𝐹(5), функция 𝐹(3)будет вызвана дважды, 𝐹(2) — трижды и так далее. Количество вызовов растёт экспоненциально: сложность рекурсии — 𝑂(2𝑛 ). Мемоизация устраняет повторные вычисления: каждое значение вычисляется ровно один раз и сохраняется для дальнейшего использования. В итоге сложность уменьшается до 𝑂(𝑛). Функция fib использует уже вычисленные значения чисел Фибоначчи из словаря cache_fib: import time # Фибоначчи с кэшем cache_fib: dict[int, int] = {} def fib(n: int) -> int: if n in cache_fib: return cache_fib[n] if n <= 1: return n result = fib(n - 1) + fib(n - 2) cache_fib[n] = result return result 302
print(" Числа Фибоначчи F(0)..F(15):") fib_list = [fib(i) for i in range(16)] print(f" {fib_list}") print(f" F(50) = {fib(50)} (кэш содержит {len(cache_fib)} значений)") С аналогичной хитростью вычисляются пути в прямоугольной сетке: # Пути на сетке M×N (только вправо и вниз) cache_paths: dict[tuple, int] = {} def paths(m: int, n: int) -> int: """Сколько путей из (0,0) в (m,n)?""" if (m, n) in cache_paths: return cache_paths[(m, n)] if m == 0 or n == 0: return 1 result = paths(m - 1, n) + paths(m, n - 1) cache_paths[(m, n)] = result return result print(f"\n Пути на сетке (только вправо и вниз):") for size in [(2,2), (3,3), (4,4), (10,10)]: m, n = size p = paths(m, n) print(f" {m}×{n}: {p:>10}") # Сравнение скорости: с кэшем и без def fib_slow(n: int) -> int: return n if n <= 1 else fib_slow(n-1) + fib_slow(n-2) n_test = 32 t0 = time.perf_counter(); _ = fib_slow(n_test); t_slow = time.perf_counter() t0 t0 = time.perf_counter(); _ = fib(n_test); t_fast = time.perf_counter() t0 print(f"\n Скорость F({n_test}): без кэша {t_slow*1000:.1f} мс, " f"с кэшем {t_fast*1000:.3f} мс, " f"ускорение ×{t_slow/max(t_fast,1e-9):.0f}") А вот и результаты применения мемоизации подоспели: Числа Фибоначчи F(0)..F(15): [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610] 303
F(50) = 12586269025 (кэш содержит 49 значений) Пути на сетке (только вправо и вниз): 2×2: 6 3×3: 20 4×4: 70 10×10: 184756 Скорость F(32): без кэша 595.5 мс, с кэшем 0.005 мс, ускорение ×126696 Вот более упругий для ума вариант алгоритма нахождения чисел Фибоначчи: def fib_memo(n, cache=None): if cache is None: cache = {} if n in cache: return cache[n] if n <= 1: result = n else: result = fib_memo(n - 1, cache) + fib_memo(n - 2, cache) cache[n] = result return result def main(): print(" Числа Фибоначчи F(0)..F(15):") fib_list = [fib_memo(i) for i in range(16)] print(f" {fib_list}") print(f" F(50) = {fib_memo(50)}") main() Результаты мы получаем мгновенно! Числа Фибоначчи F(0)..F(15): [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610] F(50) = 12586269025 304
Декоратор @lru_cache автоматически запоминает результаты вызовов функции. Параметр maxsize=None означает, что кэш может расти без ограничений. from functools import lru_cache @lru_cache(maxsize=None) def fib_cached(n): if n <= 1: return n return fib_cached(n - 1) + fib_cached(n - 2) def main(): print(" Числа Фибоначчи F(0)..F(15):") fib_list = [fib_cached(i) for i in range(16)] print(f" {fib_list}") print(f" F(50) = {fib_cached(50)}") main() Так мы можем задекорировать любую рекурсивную функцию, которая занимается вычислениями типа чисел Фибоначчи. Проект Шифр Цезаря и Виженера Говорят, что Цезарь придумал этот шифр для передачи команд во время баталий. Он очень простой: каждая буква сообщения заменяется другой буквой, которая находится на несколько букв правее. Это называется сдвигом. Буквы в конце алфавита сдвигаются на начало. Например, при сдвиге на 3 позиции мы получим такие замены: A → D B → E C → F 305
… X → A Y → B Z → C Нас больше интересует русский алфавит: А→Г Б→Д … Э→А Ю→Б Я→В Пишем функцию caesar со словарём: RU = "абвгдеёжзийклмнопрстуфхцчшщъыьэюя" def caesar(text: str, shift: int) -> str: """Шифр Цезаря: словарь символ → сдвинутый символ.""" table: dict[str, str] = {} for i, ch in enumerate(RU): table[ch] = RU[(i + shift) % len(RU)] table[ch.upper()] = RU[(i + shift) % len(RU)].upper() return "".join(table.get(c, c) for c in text) msg shift enc dec = = = = print(f" print(f" print(f" print(f" "Атакуем на рассвете" 5 caesar(msg, shift) caesar(enc, -shift) Цезарь (сдвиг {shift}):") Исходный: {msg!r}") Зашифрован:{enc!r}") Расшифрован:{dec!r}") И вот так мы сдвинули Цезаря на 5 позиций: 306
Цезарь (сдвиг 5): Исходный: 'Атакуем на рассвете' Зашифрован:'Ечепшйс те хеццжйчй' Расшифрован:'Атакуем на рассвете' Шифр Виженера гораздо интереснее и сложнее. Вместо одного фиксированного сдвига используется ключевое слово. Каждая буква ключа задаёт свой сдвиг, и сдвиги циклически повторяются по длине сообщения. Пусть ключ: LEMON Сообщение: ATTACKATDAWN Сдвигаем каждую букву сообщения на величину, соответствующую букве ключа (A=0, B=1, …, Z=25). Ключ повторяется: L E M O N L E M O N L E. A (0) + L (11) = 11 → L T (19) + E (4) = 23 → X T (19) + M (12) = 31 % 26 = 5 → F ... В результате получаем слово LXFOPVEFRNHR. Передаём в функцию vigenere сообщение и слово-ключ: def vigenere(text: str, key: str, decrypt: bool = False) -> str: """Шифр Виженера: ключевое слово задаёт сдвиги.""" key = key.lower() ki = 0 result = [] for ch in text: lo = ch.lower() if lo in RU: shift = RU.index(key[ki % len(key)]) shift = -shift if decrypt else shift new = RU[(RU.index(lo) + shift) % len(RU)] result.append(new.upper() if ch.isupper() else new) ki += 1 else: result.append(ch) 307
return "".join(result) key = "ключ" enc_v = vigenere(msg, key) dec_v = vigenere(enc_v, key, decrypt=True) print(f"\n Виженер (ключ='{key}'):") print(f" Исходный: {msg!r}") print(f" Зашифрован:{enc_v!r}") print(f" Расшифрован:{dec_v!r}") Процесс шифрования мы разобрали, и в награду получаем зашифрованное сообщение, которое без ключа трудно расшифровать: Виженер (ключ='ключ'): Исходный: 'Атакуем на рассвете' Зашифрован:'Кюювюрк ек ьюиьнгйп' Расшифрован:'Атакуем на рассвете' Задания для самостоятельного решения Тыблоки и яквы Можно заменять не только букву Я словом ТЫ, но и другие буквы какимилибо буквосочетаниями. Дополните программу новой функцией, чтобы она искала такие слова. АЛЬТруизм Напишите программу, которая переводила бы на альтовский язык связный текст. 308
Класс Counter 309
Класс Counter предназначен для подсчёта разных элементов в итерируемом объекте. Он находится в модуле collections: from collections import Counter Создаём список из латинских букв с повторами: from random import randint from collections import Counter def main(): ab = 'ABCDEFGHIJKLMNOPQRST' # создаём список: lst = [ab[randint(0, 7)] for _ in range(16)] print(lst) Создаём и инициализируем счётчик этим списком: # создаём счётчик: c = Counter(lst) Распечатываем счётчик: print(c) main() Легко заметить, что счётчик возвращает словарь, в котором ключ – это имя элемента, а значение – число повторов в списке. ['G', 'G', 'G', 'H', 'E', 'B', 'B', 'B', 'D', 'D', 'E', 'A', 'F', 'D', 'D', 'F'] Counter({'D': 4, 'G': 3, 'B': 3, 'E': 2, 'F': 2, 'H': 1, 'A': 1}) 310
Метод items возвращает список кортежей – (ключ=имя элемента, значение=число повторов). Метод keys возвращает список ключей – имён элементов. Метод values возвращает список значений – число повторов: print(c.items()) print(c.keys()) print(c.values()) dict_items([('A', 2), ('C', 3), ('F', 2), ('B', 2), ('D', 3), ('E', 2), ('G', 1), ('H', 1)]) dict_keys(['A', 'C', 'F', 'B', 'D', 'E', 'G', 'H']) dict_values([2, 3, 2, 2, 3, 2, 1, 1]) По ключу мы можем получить значение, а также изменить его: print(c['A']) c['A'] += 1 print(c) 0 Counter({'E': 6, 'F': 4, 'D': 2, 'G': 2, 'B': 1, 'H': 1, 'A': 1}) Метод most_common без параметров возвращает список всех элементов словаря по убыванию значений. В этот метод можно передать число элементов словаря, которые он вернёт: # сортируем счётчик по числу повторов: mc = c.most_common() print(mc) # указываем число элементов: print(c.most_common(1)) [('H', 5), ('E', 3), ('C', 2), ('B', 2), ('D', 2), ('A', 2), ('F', 1)] 311
[('H', 5)] Отсортировать элементы словаря по возрастанию или убыванию значений можно и с помощью функции sorted: print() sc = sorted(c.items(), key=lambda i: i[1]) print(sc) sc = sorted(c.items(), key=lambda i: i[1], reverse=True) print(sc) [('B', 1), ('E', 1), ('H', 2), ('D', 2), ('C', 3), ('A', 4), ('F', 4)] [('A', 4), ('F', 4), ('C', 3), ('H', 2), ('D', 2), ('B', 1), ('E', 1)] Эта функция легко отсортирует элементы словаря и по ключу: print() sc = sorted(c.items(), key=lambda i: i[0]) print(sc) sc = sorted(c.items(), key=lambda i: i[0], reverse=True) print(sc) [('A', 1), ('C', 4), ('D', 1), ('E', 4), ('F', 2), ('G', 4), ('H', 1)] [('H', 1), ('G', 4), ('F', 2), ('E', 4), ('D', 1), ('C', 4), ('A', 1)] Сначала можно создать пустой счётчик, а затем добавлять в него строки методом update: print() c = Counter() print(c) c.update('ПРИВЕТ') print(c) 312
c.update('МИР') print(c) Counter() Counter({'П': 1, 'Р': 1, 'И': 1, 'В': 1, 'Е': 1, 'Т': 1}) Counter({'Р': 2, 'И': 2, 'П': 1, 'В': 1, 'Е': 1, 'Т': 1, 'М': 1}) Проект Разнобуквицы 2 Счётчик поможет нам найти все разнобуквицы русского языка, то есть слова, в которых буквы не повторяются. Для каждого слова из списка слов создаём счётчик: # Разнобуквицы from collections import Counter # ИЩЕМ РАЗНОБУКВИЦЫ def raznobuk(fn): file = open(fn) for word in file: word = word.rstrip() #print(word) c = Counter(word) Нас интересует первый элемент счётчика, который содержит наибольшее значение, то есть число повторов самой повторяемой буквы: # первый элемент с наибольшим значением: lst = c.most_common(1) 313
Это элемент списка, который представляет собой кортеж. Первый (нулевой – это ключ) элемент кортежа – это максимальное число повторов: # наибольшее значение: n = lst[0][1] #print(n) Если максимальное число повторов равно 1, то все буквы в заданном слове не повторяются, то есть мы нашли разнобуквицу: # разнобуквица: if n == 1: print(word, len(word)) # закрываем файл: file.close() def main(): # словарь: fileName = 'OSH-W97_frc.txt' # ищем разнобуквицы: raznobuk(fileName) main() Запускаем программу и получаем длинный список разнобуквиц. Самые длинные разнобуквицы находятся в самом конце списка. ТРЁХДЮЙМОВКА 12 УПАДНИЧЕСТВО 12 ФЕХТОВАЛЬЩИК 12 ЦАРЕУБИЙСТВО 12 ШКУРНИЧЕСТВО 12 ЭПИКУРЕЙСТВО 12 ЯЙЦЕВИДНОСТЬ 12 БАРЫШНИЧЕСТВО 13 ГРУШЕВИДНОСТЬ 13 НЕВЫРАЗИМОСТЬ 13 НЕУДАЧЛИВОСТЬ 13 ОПРЫСКИВАТЕЛЬ 13 ПРИВЫКАЕМОСТЬ 13 УПРАВЛЯЕМОСТЬ 13 ДЕСЯТИРУБЛЁВКА 14 ЗВУКОСНИМАТЕЛЬ 14 РАЗГИЛЬДЯЙСТВО 14 ЧЕТЫРЁХУГОЛЬНИК 15 314
ДРУЖЕЛЮБНОСТЬ 13 Проект Повторяемость А теперь давайте найдём слова с наибольшими повторениями любой буквы: from collections import Counter # РЕШАЕМ ЗАДАЧУ def solve(fn): file = open(fn) for word in file: word = word.rstrip() c = Counter(word) # первый элемент с наибольшим значением: lst = c.most_common(1) # наибольшее значение: n = lst[0][1] Для этого достаточно изменить условие отбора слов: if n >= 6: print(word, len(word), lst[0][0], lst[0][1]) # закрываем файл: file.close() def main(): # словарь: fileName = 'OSH-W97_frc.txt' solve(fileName) main() 315
На первом месте оказалось слово ОБОРОНОСПОСОБНОСТЬ. Оно состоит из 18 букв, 7 из которых буквы О. На втором месте причудливое слово ПЕРЕОБРЕМЕНЕНИЕ, на третьем – более привычное слово ПЕРЕРАСПРЕДЕЛЕНИЕ. ПЕРЕОБРЕМЕНЕНИЕ 15 Е 6 ПЕРЕРАСПРЕДЕЛЕНИЕ 17 Е 6 ОБОРОНОСПОСОБНОСТЬ 18 О 7 Слов, в которых какая-либо буква повторяется 5 раз, очень много. Почти всегда это гласные. И всего 2 слова с согласными: КУКЛУКСКЛАНОВКА 15 К 5 НЕЗАКОННОРОЖДЁННЫЙ 18 Н 5 Проект Повторяемость 2 А вот слов, в которых все буквы повторяются очень мало (Рис. 1). БАБА 4 ГАГА 4 ДЯДЯ 4 МАМА 4 НЯНЯ 4 ПАПА 4 ТЯТЯ 4 ЦАЦА 4 ЦЕЦЕ 4 БЕРБЕР 6 ВАРВАР 6 КАНКАН 6 КРЕКЕР 6 ПУРПУР 6 СОСКОК 6 ТАМТАМ 6 ТАРТАР 6 КОЛОКОЛ 7 РАСКРАСКА 9 ТРАМТАРАМ 9 В этом проекте мы найдём все такие слова: from collections import Counter 316
# РЕШАЕМ ЗАДАЧУ def solve(fn): file = open(fn) for word in file: word = word.rstrip() c = Counter(word) Нам нужно отсортировать список значений, чтобы вначале стояли самые маленькие значения: lst = sorted(c.values()) # мин. значение: n = lst[0] Если самое маленькое значение больше 1, значит, все буквы повторяются: if n > 1: print(word, len(word)) # закрываем файл: file.close() def main(): # словарь: fileName = 'OSH-W97_frc.txt' solve(fileName) main() Если самое маленькое значение больше 1, значит, все буквы повторяются: 317
Проект LeetCode 1207. Unique Number of Occurrences 2 Задача 1207. Unique Number of Occurrences: Given an array of integers arr, return true if the number of occurrences of each value in the array is unique, or false otherwise. Example 1: Input: arr = [1,2,2,1,1,3] Output: true Explanation: The value 1 has 3 occurrences, 2 has 2 and 3 has 1. No two values have the same number of occurrences. Example 2: Input: arr = [1,2] Output: false Example 3: Input: arr = [-3,0,1,-3,1,1,1,-3,10,0] Output: true Constraints: 1 <= arr.length <= 1000 -1000 <= arr[i] <= 1000 Дан массив целых чисел. Если все числа в массиве повторяются неодинаковое число раз, то функция должна вернуть True. В противном случае - False. Эту задачу мы уже решали при помощи словаря. Экземпляры класса Counter также используют словарь для подсчёта элементов, но скрытно от нас. from collections import Counter # РЕШАЕМ ЗАДАЧУ def solve(lst: list[int]) -> bool: 318
Создаём и инициализируем счётчик элементов заданном списке: # создаём счётчик: c = Counter(lst) Нас интересуют повторы, а не элементы: # получаем значения: nums = c.values() Это действие вам должно быть известно и понятно: return len(nums) == len(set(nums)) def main(): #llst = [1,2,2,1,1,3] lst = [1,2] #lst = [-3,0,1,-3,1,1,1,-3,10,0] res = solve(lst) print(res) main() 319
Классы и объекты 320
Объектно-ориентированная парадигма пришла на смену процедурной, когда программы стали очень большими и сложными (например, код современных компьютерных игр достигает миллиона строк!). Более того, очень важно сгруппировать данные и методы для их обработки в некий единый «модуль», который в ООП называется классом. В процедурных языках данные и подпрограммы (процедуры и функции) существуют независимо друг от друга, что не очень удобно. Класс можно образно представить как описание некоторого реального или абстрактного объекта, который может быть создан по этому описанию. Поэтому объекты называют также экземплярами класса. Поскольку компьютерные программы отражают окружающий нас мир, то вполне разумно использовать его свойства в программировании. Люди, животные, предметы неживой природы – всё это объекты, обладающие какими-либо свойствами и поведением. Свойства объектов в программе можно представить данными разных типов. Например, каждый человек имеет рост, вес, возраст, температуру тела, образование, адрес, национальность, семейное положение, имя, фамилию и так далее. Поведение человека – это действия, которые от может совершать: думать, говорить, питаться, размножаться, передвигаться и, наконец, программировать на языке Питон. Поведение неживых объектов определяется физическими, химическими и прочими законами природы, а также воздействием на них других объектов – как живых, так и неживых. Объекты могут и не иметь материального воплощения. Например, мы можем создать класс Math с методами для различных математических вычислений, но мы не сможем «нарисовать» его образ. Если мы захотим создать калькулятор, то он, конечно, будет пользоваться методами класса Math, но 321
при этом у него появятся кнопки, память и прочие овеществлённые методы этого класса. Легко заметить, что свойства и поведение объектов реального мира обычно гораздо сложнее, чем их компьютерные модели, но все особенности настоящих объектов учесть невозможно, а зачастую и не нужно. При разработке программ мы выбираем только существенные, с нашей точки зрения, особенности объектов, а все остальные игнорируем. Этот процесс называется абстрагированием, а объект в программе можно назвать абстракцией реального объекта. Определение класса В самом простом виде определение класса сходно с определением функции, но есть и различия. Вместо ключевого слова def используют ключевое слово class. Имя класса начинается с ПРОПИСНОЙ буквы. Если имя состоит из нескольких слов, то каждое слово начинается с ПРОПИСНОЙ буквы. Круглые скобки после имени класса могут отсутствовать. Внутри класса вы можете писать любые инструкции, как и в функции: # ОПРЕДЕЛЕНИЕ КЛАССА class PrintMessage: print('Привет, Мир!') print('Привет, Россия!') Само по себе определение класса не выполняется при запуске программы. Чтобы запустить инструкции внутри класса, нужно «вызвать» его по имени: def main(): PrintMessage if __name__ == '__main__': main() 322
И тогда выполнятся все инструкции внутри определения класса. Внутри класса можно определять переменные – атрибуты класса, которые также называют полями: # ОПРЕДЕЛЕНИЕ КЛАССА class PrintMessage: # атрибуты класса/поля: message1 = 'Привет, Мир!' message2 = 'Привет, Россия!' print(message1) print(message2) Все атрибуты нашего класса находятся внутри пространства имён PrintMessage. Они видны/доступны только внутри этого пространства имён, поэтому мы не можем в функции main использовать атрибуты класса. Чтобы получить доступ к атрибутам класса, нужно сначала написать имя класса, поставить точку, а затем указать атрибут: print(PrintMessage.message1) print(PrintMessage.message2) Когда вы поставите точку после имени класса, среда разработки покажет на экране все доступные атрибуты. 323
Все атрибуты класса хранятся в словаре __dict__, поэтому вы можете среди служебных/встроенных атрибутов увидеть свои атрибуты класса: print(PrintMessage.__dict__) {'__module__': '__main__', '__firstlineno__': 2, 'message1': 'Привет, Мир!', 'message2': 'Привет, Россия!', '__static_attributes__': (), '__dict__': <attribute '__dict__' of 'PrintMessage' objects>, '__weakref__': <attribute '__weakref__' of 'PrintMessage' objects>, '__doc__': None} Вы можете изменить значение атрибута так же, как обычной переменной: PrintMessage.message1 = 'Привет, все-все-все!' print(PrintMessage.message1) Если вы свяжете 2 переменные с классом, то получите 2 ссылки на класс, поэтому все изменения в любой переменной отразятся в обеих переменных: pm1 = PrintMessage pm2 = PrintMessage print(type(pm1)) print(type(pm2)) print(id(pm1)) print(id(pm2)) pm1.message1 = 'Привет, все-все-все!' print(pm1.message1) print(pm1.message2) print(pm2.message1) print(pm2.message2) Вряд ли такие переменные нужны в программе, поэтому обычно создают экземпляры класса, или объекты. Для этого в правой части оператора 324
присваивания нужно записать имя класса и круглые скобки – как при вызове функции: pm1 = PrintMessage() pm2 = PrintMessage() print(type(pm1)) print(type(pm2)) print(id(pm1)) print(id(pm2)) pm1.message1 = 'Привет, все-все-все!' print(pm1.message1) print(pm1.message2) print(pm2.message1) print(pm2.message2) Теперь переменные pm1 и pm2 ссылаются на разные объекты, которые имеют собственные атрибуты, независимые от атрибутов другого экземпляра того же класса. Обратите внимание, что переменные pm1 и pm2 получили ссылки на атрибуты класса, но собственных атрибутов они не имеют: pm1 = PrintMessage() pm2 = PrintMessage() print(PrintMessage.__dict__) print(pm1.__dict__) print(pm2.__dict__) {'__module__': '__main__', '__firstlineno__': 2, 'message1': 'Привет, всевсе-все!', 'message2': 'Привет, Россия!', '__static_attributes__': (), '__dict__': <attribute '__dict__' of 'PrintMessage' objects>, '__weakref__': <attribute '__weakref__' of 'PrintMessage' objects>, '__doc__': None} {} {} 325
При изменении значения атрибута в пространстве имён класса изменятся и значения этих атрибутов и в экземплярах класса: pm1 = PrintMessage() pm2 = PrintMessage() PrintMessage.message1 = 'Привет, все-все-все!' print(PrintMessage.message1) print(pm1.message1) print(pm2.message1) Однако каждый экземпляр класса создаёт собственное пространство имён. Если мы изменим значение атрибута объекта, то атрибуты класса и другого объекта не изменятся: pm1 = PrintMessage() pm2 = PrintMessage() pm1.message1 = 'Привет, все-все-все!' print(PrintMessage.message1) print(pm1.message1) print(pm2.message1) print() Это значит, что в экземпляре класса создаётся новый атрибут, не зависящий от атрибута класса. Классы редко содержат только атрибуты-переменные. Поведение объекта определяется функциями, или методами. Методы определяются так же, как обычные функции, но всегда их первый параметр называется self – это ссылка на тот объект, который вызвал метод. Если поля у экземпляров класса могут быть разными, то методы одинаковы у всех экземпляров класса. Давайте напишем метод print_message, который печатает строки message1 и message2: # ОПРЕДЕЛЕНИЕ КЛАССА class PrintMessage: 326
# атрибуты класса/поля: message1 = 'Привет, Мир!' message2 = 'Привет, Россия!' # функция/метод: def print_message(self): print(self.message1) print(self.message2) В функции main создаём 2 экземпляра класса PrintMessage и в каждом вызываем метод print_message. Так как поля у экземпляров класса одинаковые, то они напечатают одно и то же сообщение: def main(): pm1 = PrintMessage() pm1.print_message() pm2 = PrintMessage() pm2.print_message() main() Гораздо полезнее добавить в метод параметр, чтобы из экземпляра класса посылать нужные сообщения: # функция/метод: def print_message(self, message: str) -> None: print(message) pm1 = PrintMessage() pm1.print_message('Привет из первого объекта') pm2 = PrintMessage() pm2.print_message('Привет из второго объекта') Поля класса доступны во всех экземплярах класса, но объектам часто нужны собственные поля. Их можно создавать в методах. Например, метод 327
set_message получает строку-сообщение и запоминает его в поле объекта message. Метод print_message печатает это сообщение: class PrintMessage: # функции/методы: def set_message(self, message: str) -> None: self.message = message def print_message(self) -> None: print(self.message) Теперь объекты pm1 и pm2 имеют поля message, которые принадлежат только этим объектам. На это указывает параметр self, который ссылается на тот объект, которому принадлежит метод. И хотя методы у всех объектов одинаковые, но поля message у них разные, поэтому одинаковые методы напечатают разные сообщения: pm1 = PrintMessage() pm1.set_message('Привет из первого объекта') pm1.print_message() pm2 = PrintMessage() pm2.set_message('Привет из второго объекта') pm2.print_message() print(pm1.__dict__) print(pm2.__dict__) Привет из первого объекта Привет из второго объекта {'message': 'Привет из первого объекта'} {'message': 'Привет из второго объекта'} Обратите внимание, что мы сначала вызываем метод set_message, чтобы создать поле message, а затем метод print_message, который использует это поле. В противном случае метод print_message не найдёт поле message, что приведёт к ошибке при выполнении программы. 328
Некоторые поля нужны уже при создании объекта, поэтому определяем метод __init__, которому передаём значение поля: # инициализируем объект: def __init__(self, message: str): self.message = message Метод __init__ вызывается автоматически после создания объекта, но это метод базового класса object, которому по умолчанию наследуют все пользовательские классы. Естественно, метод __init__ ничего не знает о нашем классе и его атрибутах, поэтому, если мы хотим изменить поведение метода __init__ базового класса, его нужно переопределить, что мы и сделали. Теперь поля message определяются сразу после создания объекта, когда будет автоматически вызван метод объекта __init__: pm1 = PrintMessage('Привет из первого объекта') pm1.print_message() pm2 = PrintMessage('Привет из второго объекта') pm2.print_message() print(pm1.__dict__) print(pm2.__dict__) Привет из первого объекта Привет из второго объекта {'message': 'Привет из первого объекта'} {'message': 'Привет из второго объекта'} Имена методов __dict__ и __init__ начинаются и заканчиваются двумя подчёркиваниями. Такие методы называются магическими. Они имеют особое 329
назначение. Вы не должны объявлять собственные методы с двумя начальными и конечными подчёркиваниями! Если вы хотите знать, какие поля получит объект, вы можете в начале определения класса перечислить поля класса, которые потом заменят поля объекта: # ОПРЕДЕЛЕНИЕ КЛАССА class PrintMessage: # атрибуты класса/поля: message: str # инициализируем объект: def __init__(self, message: str): self.message = message def print_message(self) -> None: print(self.message) pm1 = PrintMessage('Привет из первого объекта') pm1.print_message() pm2 = PrintMessage('Привет из второго объекта') pm2.print_message() PrintMessage.message = 'Привет из класса' pm1.print_message() pm2.print_message() Привет Привет Привет Привет из из из из первого второго первого второго объекта объекта объекта объекта На всякий случай можно присвоить параметру message значение по умолчанию: 330
# инициализируем объект: def __init__(self, message: str = 'НЕ ЗАБУДЬ ПЕРЕДАТЬ ПРИВЕТ!'): self.message = message Если вы забудете передать в метод __init__ аргумент, то функция print_message напечатает сообщение по умолчанию: pm1 = PrintMessage() pm1.print_message() pm2 = PrintMessage('Привет из второго объекта') pm2.print_message() НЕ ЗАБУДЬ ПЕРЕДАТЬ ПРИВЕТ! Привет из второго объекта 331
Методы класса и статические методы При решении математических задач мы использовали функции для проверки заданного числа на квадратичность и кубичность. Также часто нужны значения констант е и 𝞹. Если вы усиленно решаете задачи, то вполне разумно создать класс Math и собрать в нём все нужные константы и функции. Совершенно очевидно, что нам не нужно создавать несколько экземпляров этого класса, поскольку они всё равно будут действовать одинаково. В этом случае мы можем определить в классе поля и методы класса. С полями класса вы уже знакомы, а методы класса определяются, как методы объекта, но первый параметр метода должен называться не self, а cls (сокращение от class), а также перед заголовком метода нужно вставить строку @classmethod, которая называется декоратором. Параметр cls ссылается на класс, а не на объект, поэтому не нужно создавать объект, чтобы вызывать методы класса. При желании вы можете создать экземпляр класса Math и вызывать методы класса. class Math: # ПРОВЕРЯЕТ ЧИСЛО НА КВАДРАТИЧНОСТЬ @classmethod def isQuadrat(cls, n: float) -> bool: return round(n ** (1 / 2)) ** 2 == n # ПРОВЕРЯЕТ ЧИСЛО НА КУБИЧНОСТЬ @classmethod def isCube(cls, n: float) -> bool: return round(n ** (1 / 3)) ** 3 == n def main(): print("Math.isQuadrat()") for i in range(100): if Math.isQuadrat(i): print(f'Число {i} полный квадрат') if Math.isCube(i): print(f'Число {i} полный куб') 332
print() if __name__ == '__main__': main() Math.isQuadrat() Число 0 полный квадрат Число 0 полный куб Число 1 полный квадрат Число 1 полный куб Число 4 полный квадрат Число 8 полный куб Число 9 полный квадрат Число 16 полный квадрат Число 25 полный квадрат Число 27 полный куб Число 36 полный квадрат Число 49 полный квадрат Число 64 полный квадрат Число 64 полный куб Число 81 полный квадрат Если функция, описанная в определении класса, не использует атрибуты класса, то её можно превратить в статический метод при помощи декоратора @staticmethod: # ПРОВЕРЯЕТ ЧИСЛО НА КВАДРАТИЧНОСТЬ @staticmethod def is_quadrat(n: float) -> bool: return round(n ** (1 / 2)) ** 2 == n # ПРОВЕРЯЕТ ЧИСЛО НА КУБИЧНОСТЬ @staticmethod def is_cube(n: float) -> bool: return round(n ** (1 / 3)) ** 3 == n 333
Поскольку таким методам не нужны ссылки на класс или объект, то параметры self и cls отсутствуют. Вызываются статические методы так же, как методы класса: for i in range(100): if Math.is_quadrat(i): print(f'Число {i} полный квадрат') if Math.is_cube(i): print(f'Число {i} полный куб') Число Число Число Число Число Число Число Число Число Число Число Число Число Число Число 0 полный квадрат 0 полный куб 1 полный квадрат 1 полный куб 4 полный квадрат 8 полный куб 9 полный квадрат 16 полный квадрат 25 полный квадрат 27 полный куб 36 полный квадрат 49 полный квадрат 64 полный квадрат 64 полный куб 81 полный квадрат Режимы доступа к атрибутам класса Давайте добавим к нашему математическому классу константы E и PI, а также 2 метода класса, которые возвращают их значения: class Math: E = 2.718281828459 PI = 3.141592653589793 @classmethod def get_e(cls): 334
return Math.E @classmethod def get_pi(cls): return Math.PI В функции main печатаем значения констант: def main(): print(Math.get_e()) print(Math.get_pi()) print() print(Math.E) print(Math.PI) print() if __name__ == '__main__': main() Как вы видите, можно из любого места кода получить доступ не только к методам класса, но и к атрибутам класса. Поэтому можно легко изменить значения атрибутов класса в любом месте кода, и это отразится на экземплярах класса: print(Math.E, Math.PI) Math.E, Math.PI = 3, 3 print(Math.E, Math.PI) print() m1 = Math() print(m1.E, m1.PI) Таким образом, по умолчанию атрибуты класса доступны в любом месте кода. Такой режим доступа называется общим, публичным или public. 335
Защищённый режим protected ограничивает доступ к атрибутам класса внутри класса и во всех дочерних классах. Для этого имена атрибутов класса должны начинаться с подчёркивания: class Math: # атрибуты/поля класса: _E = 2.718281828459 _PI = 3.141592653589793 Защищённые атрибуты класса доступны только через методы класса (или объекта): @classmethod def get_e(cls): return Math._E @classmethod def get_pi(cls): return Math._PI Но, если мы попробуем, непосредственно обратиться к защищённым атрибутам класса или изменить их значения, то мы не встретим препятствий: def main(): print(Math.get_e()) print(Math.get_pi()) print() print(Math._E, Math._PI) print() Math._E, Math._PI = 3, 3 print(Math._E, Math._PI) print() m1 = Math() m1._E = 4 m1._PI = 4 print(m1._E, m1._PI) print() 336
if __name__ == '__main__': main() На самом деле защищённые атрибуты в Питоне ничем не отличаются от открытых (публичных) атрибутов. Их поведение можно сравнить с поведением констант. Константы в Питоне – это обычные переменные, поэтому их значения можно изменять. Чтобы предостеречь программиста от недозволенных действий, принято имена констант писать прописными буквами. Точно так же и с защищёнными атрибутами. Подчёркивание не защищает атрибут от свободного доступа и изменения, а только предупреждает программиста не делать этого. Для реальной защиты атрибутов класса служат закрытые (private) атрибуты. Их идентификаторы начинаются с двух подчёркиваний. Доступ к ним ограничен пространством имён класса, то есть они доступны только внутри описания класса: class Math: # атрибуты/поля класса: __E = 2.718281828459 __PI = 3.141592653589793 @classmethod def get_e(cls): return Math.__E @classmethod def get_pi(cls): return Math.__PI Методы класса работают: def main(): print(Math.get_e()) print(Math.get_pi()) print() 337
Казалось бы, обратиться непосредственно к закрытым атрибутам класса уже не удастся, но это не так: Math.__E, Math.__PI = 3, 3 print(Math.__E, Math.__PI) print() Эти примеры показывают, что подчёркивания носят рекомендательный характер и не защищают атрибуты от свободного доступа и изменения. Мы как сознательные программисты понимаем свою ответственность за несанкционированный доступ к атрибутам класса, поэтому давайте получим разрешение на доступ. Пусть у нас есть закрытая переменная __num: class Math: # атрибуты/поля класса: __E = 2.718281828459 __PI = 3.141592653589793 __num = 12 Её значение можно получить или изменить только в методах класса (или экземпляра класса). Пишем 2 метода класса. Метод get_num возвращает значение атрибута __num, а метод set_num присваивает ему новое значение: @classmethod def get_num(cls): return Math.__num @classmethod def set_num(cls, value): Math.__num = value Проверяем, как работают эти методы. Они работаю отлично: 338
print(Math.get_num()) Math.set_num(3) print(Math.get_num()) print() Метод get_num просто возвращает значение атрибута, так что он очень простой, а вот в методе set_num мы можем проверить новое значение и отклонить его, если оно не выдерживает проверки. Например, атрибут __num должен принимать только положительные значения: @classmethod def set_num(cls, value): if value > 0: Math.__num = value Отрицательные значения, передаваемые в метод set_num не изменят текущего значения атрибута __num: print(Math.get_num()) Math.set_num(3) print(Math.get_num()) print() Math.set_num(-3) print(Math.get_num()) print() Итак, два метода класса обеспечивают неприкосновенность атрибутов. Но чаще приходится иметь дело с методами объекта, поэтому давайте создадим новый класс с закрытым атрибутом __num и с двумя методами объекта, которые допускают нас к этому объекту: class MyClass: # атрибуты/поля класса: __num = 12 def get_num(self): 339
return self.__num def set_num(self, value): if value > 0: self.__num = value В функции main создаём экземпляр нашего класса и вызываем оба метода: m = MyClass() print(m.get_num()) m.set_num(7) print(m.get_num()) print() print() Метод, который возвращает значение атрибута, называют геттером. Имя такого метода принято начинать со слова get - получить. Метод, который изменяет значение атрибута, называется сеттером. Имя такого метода принято начинать со слова set - установить. Эти методы обычно работают в паре, но это необязательно. Геттер и сеттер работают хорошо, но 2 разных метода для одного атрибута использовать не очень удобно, поэтому часто создают свойство. Свойство – это экземпляр класса property. Для создания свойства нужно передать этому классу 2 функции – геттер и сеттер: # свойство: num = property(fget=get_num, fset=set_num) Внешне свойство напоминает атрибут, то есть мы можем изменять и получать значение атрибута через свойство так, как будто мы имеем дело непосредственно с атрибутом: # создаём экземпляр класса: m = MyClass() print(m.num) 340
m.num = 7 print(m.num) На самом деле свойство всякий раз вызывает геттер или сеттер для соответствующего атрибута. В этом легко убедиться, если записать в геттер и сеттер функцию print: def get_num(self): print('геттер') return self.__num def set_num(self, value): print('сеттер') if value > 0: self.__num = value Для кода выше мы получим такую информацию: # создаём экземпляр класса: m = MyClass() print(m.num) m.num = 7 print(m.num) геттер 12 сеттер геттер 7 Когда свойство возвращает значение атрибута: print(m.num) 341
вызывается геттер. Когда свойство изменяет значение атрибута: m.num = 7 вызывается сеттер. Мы обращаемся со свойствами так же, как с атрибутами, но при этом можем контролировать действия пользователя или программы через геттеры и сеттеры. Обычно геттер нужен всегда, поэтому давайте создадим свойство только с геттером: num = property(fget=get_num) Тогда вне класса мы сможем получить значение атрибута, но не сможем его изменить, так как мы не передали свойству сеттер. Это защищает атрибут от внешних изменений, если это необходимо. С помощью метода setter вы можете потом добавить к свойству и сеттер: num = property(fget=get_num) num = num.setter(set_num) Вы даже можете сначала создать свойство без геттера и сеттера, а затем добавить их: num = property() num = num.getter(get_num) num = num.setter(set_num) 342
Теперь у нас есть свойство с двумя методами – геттером и сеттером. У вас, вероятно, появилась мысль, что это перебор. Мысль верная, потому что мы можем сразу объявить геттер свойством. Это делается с помощью декоратора @property: @property def num(self): print('геттер') return self.__num Так мы получаем свойство только для чтения. Если вы не хотите извне изменять значение атрибута, то такого свойства уже вполне достаточно. Но можно добавить и сеттер. Для этого помечаем метод декоратором @num.setter, где num – это имя свойства: @num.setter def num(self, value): print('сеттер') if value > 0: self.__num = value Мы получили точно такое же свойство для чтения и записи, как и раньше, но более коротким и понятным способом! 343
Принципы ООП Объекты взаимодействуют друг с другом и с окружающей средой, при этом изменяются как их свойства, так и поведение. Поскольку всякий объект происходит от другого объекта, то он имеет изначально сходные с ним свойства и поведение, но на протяжении жизни они изменяются. Также мы знаем, что дети даже в одной семье отличаются друг от друга. Принципы объектно-ориентированного программирования (ООП) вытекают из свойств реального мира, а потому помогают правильно передавать их в программах. Таким образом, мы можем считать ООП более «реалистичным», чем полностью «абстрактное» процедурное программирование. Как Земля покоится на трёх слонах, так и ООП прочно стоит на трёх принципах – инкапсуляция, наследование и полиморфизм. Они не очень легки для понимания, но на первых порах вы можете конструировать свои объекты с помощью классов, написанных достаточно просто. Инкапсуляция Главное преимущество объектно-ориентированного программирования перед процедурным состоит в том, что классы включают в себя не только данные (атрибуты, поля), но и функции, оперирующие этими данными (методы). Данные, хранящиеся в открытых атрибутах, легко могут быть изменены, потому что к ним имеется прямой доступ из любого места кода. Значит, случайно или намеренно пользователь (или даже сам программист) может придать атрибутам опасные для работы программы значения (например, укажет, что игроку -100 лет). Более того, принято считать, что пользователь вообще не должен знать, какие данные и какого типа имеются в классе. Это не только повышает надёжность программирования, но и скрывает от посторонних глаз детали реализации 344
класса. Для пользователя класс должен быть неким подобием чёрного ящика, состояние которого (данные) можно изменять только через его методы. Таким образом, принцип инкапсуляции (Encapsulation) заключается в том, что класс объединяет данные и методы для работы с ними, причём пользователь не имеет возможности напрямую изменять данные, а должен вызывать соответствующие методы класса. Как реализовать принцип инкапсуляции в программах на Питоне, мы уже подробно разобрали. Наследование Наследование (Inheritance) можно признать наиболее полезным принципом ООП, которым вы будете пользоваться всегда и везде. Действительно, любой класс в программе на Питоне наследует базовому классу object. Как и любой наследник, новый класс (класспотомок) получает от родительского класса (класса-предка), наследство в виде его атрибутов и методов. Впрочем, класс object настолько бедный, что взять у него почти нечего, поэтому мы создадим свой собственный класс. Пусть нам нужен класс, который умеет оперировать координатами пикселей на экране. Вполне естественно назвать его Point2D. Он, по крайней мере, должен иметь два закрытых атрибута для хранения координат точки, метод-инициализатор и 4 метода – для чтения и записи координат: # КЛАСС ТОЧКИ НА ПЛОСКОСТИ class Point2D(object): 345
# координаты точки: __x = 0 __y = 0 # инициализируем объект: def __init__(self, x=1920//2, y=1080//2): self.set_x(x) self.set_y(y) # МЕТОДЫ # ВОВРАЩАЕМ КООРДИНАТУ Х def get_x(self): return self.__x # ИЗМЕНЯЕМ КООРДИНАТУ Х def set_x(self, value): if value < 0 or value > 1920: self.__x = 0 else: self.__x = value # ВОВРАЩАЕМ КООРДИНАТУ Y def get_y(self): return self.__y # ИЗМЕНЯЕМ КООРДИНАТУ Y def set_y(self, value): if value < 0 or value > 1080: self.__y = 0 else: self.__y = value Здесь я явно указал, что наш класс наследует классу object: class Point2D(object): Это делать необязательно, поскольку интерпретатор и без нас это знает: 346
class Point2D(): Метод __init__ часто называют конструктором объекта, но это неверно. Объект создаёт метод __new__, а метод __init__ создаёт и инициализирует поля объекта. Класс может иметь только один метод __init__, поэтому в нём следует инициализировать атрибуты класса. Если атрибуты класса могут иметь значения по умолчанию, то их можно сразу указать в методе __init__, чтобы не усложнять создание нового объекта. Если метод __init__ встречает имя атрибута, то он создаёт его, поэтому необязательно указывать атрибуты в начале определения класса: # координаты точки: # __x = 0 # __y = 0 # инициализируем объект: def __init__(self, x=1920 // 2, y=1080 // 2): self.set_x(x) self.set_y(y) С другой стороны, если есть список атрибутов с их типами и назначением, то программу легче понять. Дополнительно можно написать открытый метод to_str, который возвращает строку с координатами точки: def to_str(self): s = f'Координаты точки: x = {self.get_x()} y = {self.get_y()}' return s В функции main мы создаём экземпляр нашего класса и распечатываем текущие координаты точки: 347
def main(): p = Point2D() print(p.to_str()) if __name__ == '__main__': main() Запускаем программу – в консольном окне напечатаны текущие координаты точки. Создадим в функции main второй экземпляр класса Point2D, вызвав на этот раз метод __init__ с параметрами: def main(): p = Point2D() print() print(p.to_str()) print() p2 = Point2D(-10, 222) print(p2.to_str()) print() Мы намеренно задали горизонтальной координате отрицательное значение, чтобы проверить бдительность нашего класса. Метод __init__ не присваивает слепо атрибутам нового объекта заданные пользователем значения, а вызывает для этого методы set_x и set_y, которые контролируют действия пользователя: мы предполагаем, что на экране нет отрицательных координат, поэтому заменяем отрицательные значения нулём. Вы видите, что неверное значение было исправлено. 348
Атрибуты под контролем Присвоим атрибутам объекта новые значения: p2.set_x(12) p2.set_y(2000) print(p2.to_str()) Если первое значение задано верно, то второе опять выходит за рамки дозволенного. Впрочем, методы всё равно не позволяют задавать неверные значения. Строгий контроль Через две точки можно провести прямую. Давайте создадим новый класс, взяв за основу класс Point2D: # КЛАСС ПРЯМОЙ ЛИНИИ НА ПЛОСКОСТИ class Line2D(Point2D): pass Класс Point2D, которому наследует новый класс, называется базовым, а новый класс-наследник – производным. Базовый класс должен быть указан после идентификатора производного класса в круглых скобках. В общем виде определение производного класса можно представить так: 349
class ИмяПроизводногоКласса(ИмяБазовогоКласса): . . . Тело класса Единственное исключение из этого правила – класс object, который не обязательно указывать как базовый. В функции main мы можем создать экземпляр нового класса, изменить его атрибуты и распечатать их значения на экране: # создаём линию: ln = Line2D() print(ln.to_str()) ln.set_x(11) ln.set_y(22) print(ln.to_str()) print() Объект нового класса При этом в теле класса Line2D нет ни одной строчки кода! Атрибуты и методы он получил от базового класса Point2D. Но нам дополнительно нужны координаты ещё двух точек, которые потребуют введения двух атрибутов и переопределения метода to_str: # КЛАСС ПРЯМОЙ ЛИНИИ НА ПЛОСКОСТИ class Line2D(Point2D): # координаты второй точки: __x2 = 0 __y2 = 0 350
Обратите внимание, что в методе __init__ мы указываем координаты новых точек. Также обязательно нужно вызвать метод __init__ базового класса, чтобы проинициализировать атрибуты, доставшиеся нам от него: # инициализируем объект: def __init__(self, x2=0, y2=0): super().__init__() self.set_x2(x2) self.set_y2(y2) # МЕТОДЫ # ВОВРАЩАЕМ КООРДИНАТУ Х def get_x2(self): return self.__x2 # ИЗМЕНЯЕМ КООРДИНАТУ Х def set_x2(self, value): if value < 0 or value > 1920: self.__x2 = 0 else: self.__x2 = value # ВОВРАЩАЕМ КООРДИНАТУ Y def get_y2(self): return self.__y2 # ИЗМЕНЯЕМ КООРДИНАТУ Y def set_y2(self, value): if value < 0 or value > 1080: self.__y2 = 0 else: self.__y2 = value # ВОЗВРАЩАЕМ СТРОКУ С КООРДИНАТАМИ ПРЯМОЙ def to_str(self): s = f'Координаты прямой: x = {self.get_x()} y = {self.get_y()}' s += f' x2 = {self.get_x2()} y2 = {self.get_y2()}' return s 351
Естественно, мы могли бы создать класс линий по-другому, но как пример наш класс вполне показательный. В функции main мы создаём экземпляр класса Line2D, присваиваем значения координатам точек и печатаем их на экране: # создаём линию: ln = Line2D() print(ln.to_str()) ln.set_x(11) ln.set_y(22) ln.set_x2(111) ln.set_y2(222) print(ln.to_str()) Координаты прямой: x = 960 y = 540 x2 = 0 y2 = 0 Координаты прямой: x = 11 y = 22 x2 = 111 y2 = 222 Сначала координаты линии получают значения по умолчанию, а затем пользовательские. Координаты первой точки получат значения по умолчанию. Если мы хотим создать линию с четырьмя определёнными координатами, то нужно изменить функцию __init__: # инициализируем объект: def __init__(self, x1=1920 // 3, y1=1080 // 3, x2=0, y2=0): super().__init__(x1, y1) self.set_x2(x2) self.set_y2(y2) Теперь мы можем создать вторую линию просто и изящно: 352
# создаём вторую линию: ln = Line2D(1, 2, 3, 4) print(ln.to_str()) Координаты прямой: x = 1 y = 2 x2 = 3 y2 = 4 353
Полиморфизм Суть полиморфизма (Polymorphism) состоит в том, что классы-потомки могут иметь методы с тем же самым названием, что и в родительском классе, но при этом выполнять действия, специфические для каждого конкретного класса. Начните новый проект и определите базовый класс: # Polymorphism class Animal: def voice(self): print("Голос!") Пусть это будет некое исходное животное, от которого произошли все остальные. Мы также полагаем, что оно могло подавать голос, хотя некоторые потомки потеряли его в процессе эволюции. В функции main мы создаём экземпляр праживотного, которое тут же подаёт голос: def main(): animal = Animal() animal.voice() 354
main() В принципе, нужно было бы создать хотя бы пару животных, но, как мы знаем из программы Андрея Малахова, некоторые особи плодятся почкованием, так что нам вполне хватит и одной твари. Затем в результате мутаций и кроссинговера хромосом на свет появляются рыбы: class Fish(Animal): def voice(self): print("Буль-буль...") Птицы: class Cuckoo(Animal): def voice(self): print("Ку-ку!") Собаки: class Dog(Animal): def voice(self): print("Гав-гав!") Кошки: class Cat(Animal): def voice(self): print("Мяу-мяу!") И ослы: 355
class Donkey(Animal): def voice(self): print("Иа-иа!") Дальше эволюция совершила скачкообразный рывок и породила бэби: class Baby(Animal): def voice(self): print("Ням-ням!") Повзрослевшее бэби превратилось в чилдрена: class Child(Baby): def voice(self): print("Дай!!!") А чилдрен – в мэна: class Man(Child): def voice(self): print("Да пошёл ты!") Ну и последнее слово, конечно, должно остаться за женщиной: class Woman(Man): def voice(self): print("Добился своего!..") На этом эволюция остановилась, поскольку достигла совершенства. Как видите, наши животные отличаются друг от друга только тем, что умеют говорить всякие фразы. Для понимания сути полиморфизма этого вполне достаточно. Методы класса-потомка переопределяют методы 356
родительского класса с теми же названиями. В других языках программирования для этого используется модификатор override, а Питон обходится без него. Впрочем, среда разработки подсказывает, что метод voice класса Woman переопределяет метод voice родительского класса Man. Непосредственно особей всех классов мы создаём в функции main: def main(): animal = Animal() animal.voice() fish = Fish() fish.voice() cuckoo = Cuckoo() cuckoo.voice() dog = Dog() dog.voice() cat = Cat() cat.voice() donkey = Donkey() donkey.voice() baby = Baby() baby.voice() child = Child() child.voice() man = Man() man.voice() 357
woman = Woman() woman.voice() main() Запускаем программу – и каждый объект (и в то же время субъект) говорит на своём языке. Голос! Буль-буль... Ку-ку! Гав-гав! Мяу-мяу! Иа-иа! Ням-ням! Дай!!! Да пошёл ты! Добился своего!.. Таким образом, все производные классы обходятся одним и тем же методом voice, чтобы произносить разные фразы. Когда речь идёт о нескольких классах, порождённых от одного родительского, возможность переопределять метод класса-предка очень полезна. Впрочем, в любом переопределённом методе доступен и метод базового класса. Для этого нужно вызвать метод базового класса: class Woman(Man): def voice(self): super().voice() print("Вау!") Давайте укрепим и уплотним наши «классические» знания на программировании двух классических занимательных задач, в которых использование классов выглядит естественно и помогает познакомиться с объектно-ориентированным программированием. 358
Обе программы показывают, что класс объединяет данные и действия: - в первой задаче объект описывает состояние головоломки и умеет проверять его корректность; - во второй задаче объект представляет игру, хранит секретное число и предоставляет метод для проверки догадок. Эти примеры знакомят с конструкторами, методами, состоянием объекта, инкапсуляцией и взаимодействием объектов. Проект Головоломка «Переправа через реку» Условие задачи: Крестьянин (farmer) должен перевезти через реку волка (wolf), козу (goat) и капусту (cabbage). В лодке помещается только крестьянин и один объект. При этом нельзя оставлять: - волка с козой; - козу с капустой. Класс State описывает текущее состояние перевозок: class State: def __init__(self, farmer, wolf, goat, cabbage): self.farmer = farmer self.wolf = wolf self.goat = goat self.cabbage = cabbage Сначала все объекты находятся на левом берегу реки: start = State(0, 0, 0, 0) 359
Метод is_valid проверяет допустимость состояния согласно условию задачи: def is_valid(self): # Волк съест козу if self.wolf == self.goat and self.farmer != self.wolf: return False # Коза съест капусту if self.goat == self.cabbage and self.farmer != self.goat: return False return True Задача решена, если все объекты успешно переправились: def is_goal(self): return (self.farmer == self.wolf == self.goat == self.cabbage == 1) Вспомогательные методы для копирования текущего состояния и его печати на экране: def copy(self): return State(self.farmer, self.wolf, self.goat, self.cabbage) def __str__(self): names = ["Левый", "Правый"] return (f"Ф:{names[self.farmer]} " f"В:{names[self.wolf]} " f"К:{names[self.goat]} " f"Кап:{names[self.cabbage]}") 360
Непосредственно задачу решает функция dfs: answer = dfs(start, set(), [start]) Она принимает текущее состояние: def dfs(state, visited, path): Задача решена: if state.is_goal(): return path key = (state.farmer, state.wolf, state.goat, state.cabbage) if key in visited: return None visited.add(key) Список объектов: items = ["wolf", "goat", "cabbage", None] Перебираем объекты, чтобы получить допустимые состояния: for item in items: new = state.copy() new.farmer ^= 1 if item == "wolf" and new.wolf != state.farmer: continue 361
if item == "goat" and new.goat != state.farmer: continue if item == "cabbage" and new.cabbage != state.farmer: continue if item == "wolf": new.wolf ^= 1 elif item == "goat": new.goat ^= 1 elif item == "cabbage": new.cabbage ^= 1 Если новое состояние допустимо, то ищем следующее состояние – пока задача не будет решена: if new.is_valid(): result = dfs(new, visited, path + [new]) if result: return result return None Когда задача будет решена, мы напечатаем решение: start = State(0, 0, 0, 0) answer = dfs(start, set(), [start]) for step, st in enumerate(answer): print(step, st) И вот оно – решение: 0 1 2 3 4 Ф:Левый В:Левый К:Левый Кап:Левый Ф:Правый В:Левый К:Правый Кап:Левый Ф:Левый В:Левый К:Правый Кап:Левый Ф:Правый В:Правый К:Правый Кап:Левый Ф:Левый В:Правый К:Левый Кап:Левый 362
5 Ф:Правый В:Правый К:Левый Кап:Правый 6 Ф:Левый В:Правый К:Левый Кап:Правый 7 Ф:Правый В:Правый К:Правый Кап:Правый 0. Все объекты находятся на левом берегу реки. 1. Крестьянин перевозит козу на правый берег. На левом берегу остаётся волк с капустой. 2. Крестьянин возвращается на левый берег. На правом берегу остаётся коза. 3. Крестьянин перевозит волка на правый берег. 4. И увозит козу на левый берег. На правом берегу остаётся волк. 5. Крестьянин перевозит капусту на правый берег. На правом берегу остаётся волк с капустой. 6. Крестьянин возвращается на левый берег. 7. Крестьянин перевозит козу на правый берег. Теперь на все переправились на правый берег! По ходу решения головоломки мы изучили: • • • • • класс; методы; создание копии объекта; рекурсию; поиск в глубину. 363
Проект Игра «Быки и коровы» 364
Известная игра Быки и коровы! Компьютер загадывает четырёхзначное число из неповторяющихся цифр. Нужно за минимальное число попыток отгадать это число. Попытка заключается в том, что отгадчик называет своё четырёхзначное число из неповторяющихся цифр. Компьютер, отвечает сколько быков и коров содержит его попытка. Бык – цифра правильная и стоит на своём месте. Корова - цифра правильная, но стоит не на своём месте. Поскольку речь идёт об игре, то вполне разумно описать её классом Game. В начале игры мы создаём экземпляр этого класса: # создаём игру: game = Game() print("Игра 'Быки и коровы'") После чего игрок вводит с клавиатуры свои попытки - четырёхзначные числа из неповторяющихся цифр while True: s = input("Введите число: ") # число должно быть четырёхзначным: if len(s) != 4: continue # цифры не повторяются: if len(set(s)) != 4: continue Компьютер проверяет ход и сообщает игроку, сколько быков и коров тот получил: # проверяем ход: bulls, cows = game.check(s) # печатаем результат хода: print(f"Быков: {bulls} Коров: {cows}") 365
4 быка – это чистая победа: # если 4 быка, то число угадано: if bulls == 4: print("Угадали!") print("Попыток:", game.count) На этом игра закончена: break Самое интересное в этой программе – это, безусловно, класс игры. Импортируем модуль random для загадывания случайных чисел: import random При создании игрового объекта сразу загадываем секретное четырёхзначное число, которое состоит из разных цифр и не начинается с нуля: class Game: def __init__(self): digits = list("0123456789") random.shuffle(digits) if digits[0] == '0': digits[0], digits[1] = digits[1], digits[0] self.secret = "".join(digits[:4]) self.count = 0 Метод check проверяет ход игрока, добавляет ход, считает быков и коров и возвращает результат в основную программу: 366
def check(self, guess): self.count += 1 bulls = 0 cows = 0 for i in range(4): if guess[i] == self.secret[i]: bulls += 1 elif guess[i] in self.secret: cows += 1 return bulls, cows В таком виде это, конечно, не игра, а головоломка, поскольку в игре должны участвовать по крайней мере 2 игрока. Ходы игроков должны чередоваться, что делает программу более сложной. Но наша программа учебная, поэтому её вполне достаточно, чтобы проникнуться принципом инкапсуляции: • секретное число хранится внутри объекта; • игрок взаимодействует только через метод check(); • объект сам ведет счёт попыток. Обобщение Принцип инкапсуляции заключается в том, что класс объединяет в единое целое данные и методы для их обработки, а также скрывает от пользователя детали реализации самого класса. Принцип наследования позволяет создавать производные классы от базовых. Новые классы могут использовать члены базового класса, переопределять их, вводить новые атрибуты, свойства и методы, изменяя поведение объектов классов-наследников. 367
Принцип полиморфизма продолжает и развивает принцип наследования, разрешая в производных классах переопределять методы родительского класса. Несмотря на все преимущества объектно-ориентированного программирования перед процедурным, не нужно всегда и во всём следовать принципам ООП, иначе простую программу вы будете писать в несколько раз дольше, а работать она будет заметно медленнее, чем «процедурная». Если вы пишете простую программу и она нужна вам только на один раз, не создавайте объекты! Программа наверняка будет работать медленнее, а вы потратите в 3 раза больше времени на описание класса. Если программа большая, или вы планируете её долго использовать, делая новые версии, или хотите поделиться с другими программистами, то старайтесь каждый объект оформлять в виде класса. Такой подход облегчит написание программы, её отладку, модификацию и сопровождение. Объявляйте закрытые атрибуты и методы, если они не должны использоваться вне класса или объекта. Не оставляйте аргументы без присмотра, это - проявление бесшабашности, которое в программировании не приветствуется. 368
Двоичные/бинарные деревья 369
Двоичное/бинарное дерево (bynary tree) – это сложная, древовидная структура данных. Она очень эффективна, когда речь идёт о добавлении и удалении новых элементов и о поиске элементов с заданными свойствами. Элементы двоичного дерева называются узлами. В отличие от настоящих деревьев бинарное дерево растёт вниз. Самый верхний узел называется корнем дерева (корневой узел). Все узлы имеют 1 или 2 дочерних узла или вообще не имеют продолжения. Дочерние узлы разделяют на левый и правый узел, или потомок. Типичное бинарное дерево представлено на рисунке. На нулевом уровне находится корень. На первом уровне 1–2 его потомка. На втором уровне – 4 потомка предыдущих потомков, и так далее. Число уровней дерева называют также высотой дерева. Каждый узел дерева хранит какое-то значение. На рисунке⬆ узлы просто пронумерованы для наглядности. В реальном двоичном дереве место номеров занимают числа или значения других типов. В бинарном дереве значения узлов могут быть произвольными. Структура данных, которая используется для быстрого поиска значений, называется бинарным деревом поиска (bynary search tree, BST). В таком дереве левый потомок должен быть меньше правого потомка и родительского узла. 370
Для наших экспериментов мы воспользуемся готовой библиотекой Binarytree, которую нужно импортировать в наши проекты: from binarytree import build При запуске программы возникнет ошибка. Удалите из файла __init__.py этого модуля проблемные строки. Подробную информацию об этой библиотеке вы можете найти здесь: https://github.com/joowani/binarytree Всё дерево состоит из отдельных узлов, которые описываются классом Node: # КЛАСС УЗЛА ДЕРЕВА class Node: # инициализируем узел: def __init__(self, value=None, left=None, right=None): # значение: self.value = self.val = value # левый потомок: self.left = left # правый потомок: self.right = right Новый узел при инициализации получает только значение value, потомков у него может и не быть. В функции main мы создаём корень дерева со значением 0. Сейчас дерево состоит только из корня: def main(): # создаём корень дерева: root = Node(0) 371
if __name__ == '__main__': main() К корню мы можем добавить дочерние узлы с произвольными значениями: def main(): # создаём корень дерева: root = Node(0) # создаём левый дочерний узел: root.left = Node(1) # создаём правый дочерний узел: root.right = Node(2) Аналогично мы можем добавить к потомкам новых потомков: # создаём следующий уровень узлов: root.left.left = Node(3) root.left.right = Node(4) root.right.left = Node(5) root.right.right = Node(6) print(root) Если у нас есть список значений всех узлов, то функция build из библиотеки binarytree сама построит точно такое же дерево: from binarytree import build values = [0,1,2,3,4,5,6] tree = build(values) print(tree) 372
Это способ построения деревьев более удобный, если мы сразу имеем список значений для всех вершин. При этом мы можем получить произвольное дерево, которое нарушает договорённости для бинарного дерева поиска, поэтому оно не годится для быстрого поиска значений в дереве. Чтобы добавить новый узел в дерево (в том числе и корень), нам нужен метод insert, который принимает значение для нового узла: # вставляем новый узел def insert(self, value): Если корень не существует, мы создаём его: if self.value is None: self.value = value В противном случае мы добавляем узел к существующему родительскому узлу. Если значение для нового узла меньше родительского, то это должен быть левый узел. В противном случае – правый. Но вполне возможно, что родительский узел уже имеет левого потомка, и тогда мы попытаемся добавить новый узел к нему. Если же левого потомка ещё нет, мы добавляем его к родительскому узлу: else: # новый узел меньше родительского: if value < self.value: # левого потомка нет: if self.left is None: # создаём левого потомка: self.left = Node(value) # левый потомок уже есть: else: self.left.insert(value) Правые потомки создаются аналогично: 373
# новый узел не меньше родительского: else: # правого потомка нет: if self.right is None: # создаём правого потомка: self.right = Node(value) # правый потомок уже есть: else: self.right.insert(value) Мы создали прекрасное бинарное дерево, вручную добавляя узлы. Легко видеть, что это не бинарное дерево поиска, потому что левые потомки больше родителей. Давайте для этих же значений создадим бинарное дерево поиска: tree = Node() tree.insert(0) tree.insert(1) tree.insert(2) tree.insert(3) tree.insert(4) tree.insert(5) tree.insert(6) Но тут возникает проблема с визуализацией дерева, без чего трудно представить, как выглядит полученное дерево. Поскольку бинарные деревья — это сложная структура данных, то их непросто напечатать на экране. Вот самая простая функция для этого: 374
# ПЕЧАТАЕМ ДЕРЕВО def print_tree(node, indent=0, distance=3): # indent - отступ от левого края # distance - расстояние между уровнями # дерево построено: if not node: return # отступ от левого края для текущего уровня: indent += distance # печатаем правый узел: print_tree(node.right, indent) for _ in range(distance, indent): print(end=" ") print(f'{node.value}<') # печатаем левый узел: print_tree(node.left, indent) print_tree(tree) Она рекурсивная, поэтому вам нужно напрячься, чтобы понять, как она работает. Рисунок поможет вам разобраться в этом хитроумном процессе. Обратите внимание, что наше дерево растёт не сверху вниз, как это принято в теории деревьев, а слева направо, так что вам нужно наклонить голову к левому плечу, чтобы приблизить теорию к практике. Людям с богатым воображением или в меру ленивым этого делать, конечно, не нужно. В любом из этих случаев видно, что наше кучное дерево выродилось в голый ствол, без ветвей и листьев. По сути, мы получили тот же самый список значений, 375
который был у нас раньше. Такие деревья невезения могут случаться, если данные изначально упорядочены (например, мы строим дерево по отсортированному списку). Если значения узлов более или менее случайны, то дерево будет ветвиться. Например, мы можем взять список из 16 последовательных целых чисел и перемешать его: from random import shuffle # упорядоченный список значений: values = [n for n in range(16)] # перемешиваем его: shuffle(values) # печатаем: print(values) Или вы можете заполнить список случайными числами в некотором диапазоне: from random import randint # список случайных значений: values = [randint(0, 20) for _ in range(16)] Если мы имеем готовый список значений, то удобно сразу создать всё дерево целиком. Для этого пишем метод insert_values, который просто вызывает метод insert для каждого значения из списка: # вставляем новые узлы: def insert_values(self, values): for value in values: self.insert(value) 376
Теперь вы можете легко и непринуждённо выращивать деревья, которые демонстрируют свою сложную структуру. # создаём пустое дерево: tree = Node() # добавляем все узлы: tree.insert_values(values) # печатаем дерево: print_tree(tree) print() 377
Проект LeetCode 100. Same Tree Чтобы не отягощать память сложной теорией, мы будем изучать двоичные деревья, решая реальные задачи. Задача с сайта LeetCode называется 100. Same Tree: Given the roots of two binary trees p and q, write a function to check if they are the same or not. Two binary trees are considered the same if they are structurally identical, and the nodes have the same value. Example 1: Input: p = [1,2,3], q = [1,2,3] Output: true Example 2: Input: p = [1,2], q = [1,null,2] Output: false 378
Example 3: Input: p = [1,2,1], q = [1,1,2] Output: false Constraints: • • The number of nodes in both trees is in the range [0, 100]. -104 <= Node.val <= 104 В задаче даны пары примитивных двоичных деревьев, и нужно определить, это одно и то же дерево или нет. Двоичные деревья равны, если они имеют одну и ту же структуру и значения узлов. Импортируем функцию build из библиотеки binarytree, которая умеет строить деревья по значениям узлов: from binarytree import build В функции main мы передаём в функцию solve списки значений для всех узлов двоичного дерева: def main(): values1 = [1, 2, 3] values2 = [1, 2, 3] print(solve(values1, values2)) print() values1 = [1, 2] values2 = [1, None, 2] print(solve(values1, values2)) 379
print() values1 = [1, 2, 1] values2 = [1, 1, 2] print(solve(values1, values2)) print() if __name__ == '__main__': main() В функции solve мы создаём и печатаем деревья по спискам вершин: # РЕШАЕМ ЗАДАЧУ def solve(val1: list[int], val2: list[int]) -> bool: tree1 = build(val1) print(tree1) tree2 = build(val2) print(tree2) А метод equals сравнивает 2 дерева на равенство: return tree1.equals(tree2) Запускаем программу – все контрольные примеры решены верно. 380
Проект LeetCode 101. Symmetric Tree Given the root of a binary tree, check whether it is a mirror of itself (i.e., symmetric around its center). Example 1: Input: root = [1,2,2,3,4,4,3] Output: true Example 2: 381
Input: root = [1,2,2,null,3,null,3] Output: false Constraints: • • The number of nodes in the tree is in the range [1, 1000]. -100 <= Node.val <= 100 Follow up: Could you solve it both recursively and iteratively? У нас есть двоичное дерево, и мы должны проверить, симметричное ли оно. Двоичное дерево симметрично, если левое поддерево симметрично правому поддереву. Корень дерева можно пропустить. Дальше мы находим в обоих примерах поддеревья 2 – 2, которые симметричны. Спускаемся ниже. В первом дереве поддеревья симметричны: 1 / \ 2 2 / \ / \ 3 4 4 3 3 4 4 3 → 3 4 4 3 Во втором – не симметричны: 1 / \ 2 2 \ \ 3 3 None 3 None 3 → 3 None 3 None Каждое двоичное дерево имеет свойство is_symmetric, которое проверяет, симметрично ли дерево, и возвращает True, если это так. Вот и всё решение: from binarytree import build 382
# РЕШАЕМ ЗАДАЧУ def solve(val: list[int]) -> bool: tree = build(val) print(tree) res = tree.is_symmetric return res def main(): values = [1,2,2,3,4,4,3] print(solve(values)) print() values = [1,2,2,None,3,None,3] print(solve(values)) print() if __name__ == '__main__': main() Проект LeetCode 102. Binary Tree Level Order Traversal Given the root of a binary tree, return the level order traversal of its nodes' values. (i.e., from left to right, level by level). Example 1: 383
Input: root = [3,9,20,null,null,15,7] Output: [[3],[9,20],[15,7]] Example 2: Input: root = [1] Output: [[1]] Example 3: Input: root = [] Output: [] Constraints: • • The number of nodes in the tree is in the range [0, 2000]. -1000 <= Node.val <= 1000 Для этого проекта в библиотеке binarytree для нас нет готового решения, поэтому мы используем функцию build из этой библиотеки только для печати дерева на экране: def main(): values = [3,9,20,None,None,15,7] tree = build(values) 384
print(tree) print(solve(values)) print() values = [1] tree = build(values) print(tree) print(solve(values)) print() values = [] tree = build(values) print(tree) print(solve(values)) print() if __name__ == '__main__': main() Функция solve построена на простом наблюдении: число узлов на каждом уровне – это последовательные степени двойки: 1 – 2 – 4 – 8 - … Осталось правильно рассчитать начало значений start для каждого уровня: from binarytree import build # РЕШАЕМ ЗАДАЧУ def solve(val: list[int]): # список списков: res = [] # индекс начального узла на уровне: start = 0 # число узлов на уровне: num_node = 1 # кока в списке есть достаточно узлов: while num_node <= len(val): # список значений узлов для текущего уровня: level = [] # по всему уровню: for i in range(start, start + num_node): # если узел имеет значение, 385
# добавляем его в список значений для текущего уров-ня: if val[i] != None: level += [val[i]] # добавляем список значений узлов для текущего уровня # к списку списков: res += [level] # переходим к следующему уровню: start += num_node num_node *= 2 return res Контрольные примеры взяты успешно. Проект LeetCode 1028. Recover a Tree From Preorder Traversal We run a preorder depth-first search (DFS) on the root of a binary tree. At each node in this traversal, we output D dashes (where D is the depth of this node), then we output the value of this node. If the depth of a node is D, the depth of its immediate child is D + 1. The depth of the root node is 0. If a node has only one child, that child is guaranteed to be the left child. Given the output traversal of this traversal, recover the tree and return its root. Example 1: 386
Input: traversal = "1-2--3--4-5--6--7" Output: [1,2,5,3,4,6,7] Example 2: Input: traversal = "1-2--3---4-5--6---7" Output: [1,2,5,3,null,6,null,4,null,7] Example 3: 387
Input: traversal = "1-401--349---90--88" Output: [1,401,null,349,88,90] Constraints: • • The number of nodes in the original tree is in the range [1, 1000]. 1 <= Node.val <= 109 Дана строка traversal, в которой записаны значения в узлах. Первое значение – это корень дерева. Перед остальными значениями стоят тире. Число тире показывает уровень, на котором находятся значения. По заданной строке нужно правильно записать значения, чтобы по ним можно было бы построить дерево. В функции main мы отправляем строку с заданием в функцию solve, откуда получаем список значений, по которым создаём дерево и печатаем его на экране: def main(): traversal = "1-2--3--4-5--6--7" # [1,2,5,3,4,6,7] #traversal = "1-2--3---4-5--6---7" # [1,2,5,3,None,6,None,4,None,7] #traversal = "1-401--349---90--88" # [1,401,None,349,88,90] values = solve(traversal) #tree = build2(values) 388
#print(tree.values) #print(tree) print() Заданная строка traversal очень неудобна для анализа дерева, поэтому в функции solve мы разбиваем её на части по тире: # РЕШАЕМ ЗАДАЧУ def solve(traversal: str): lst = traversal.split('-') print(lst) print() В итоге мы получаем список с числами и пустыми строками. Причём число пустых строк на 1 меньше, чем было тире. ['1', '2', '', '3', '', '4', '5', '', '6', '', '7'] Теперь нам нужно создать 2 списка – первый список содержит все значения узлов, а второй – их уровень: # корень: level = 0 nodes = [int(lst[level])] levels= [0] #print(nodes, levels) # по всему списку lst1: for i in range(1, len(lst)): # пропускаем пробелы: if lst[i] == '': continue #print(lst1[i]) # число пустых строк перед числом: n_empty = 1 id = i - 1 389
while lst[id] == '': n_empty += 1 id -= 1 #print(lst1[i], n_empty) nodes += [int(lst[i])] levels += [n_empty] print(nodes) print(levels) print() Теперь мы знаем какие значения находятся на каждом уровне дерева. В первом примере мы имеем полное бинарное дерево, поэтому воссоздать его очень просто. Мы проходимся по списку levels и ищем узлы на заданном уровне level. Найдя узел, добавляем его в список values. Если на каком-то уровне не окажется ни одного узла, значит, построение дерева закончено: # строим дерево: level = 0 values = [nodes[level]] flg = True while flg: # следующий уровень: flg = False level += 1 for id in range(len(levels)): if levels[id] == level: values += [nodes[id]] flg = True print(values) return values Во втором примере некоторые узлы отсутствуют, поэтому список values должен быть таким: values = [1, 2, 5, 3, None, 6, None, 4, None, 7] 390
Он совпадает со строкой на сайте[1,2,5,3,null,6,null,4,null,7], но константы null нужно заменить. Также необходимо импортировать и использовать для построения дерева функцию build2: from binarytree import build2 tree = build2(values) Она построит правильное дерево. → Для третьего примера список значений должен быть таким: values = [1, 401, None, 349, 88, 90] И тогда функция build2 также построит правильное дерево. Таким образом, наша задача – по исходной строке правильно заполнить список значений values. Для полного бинарного дерева мы это уже умеем делать. 391
Если мы посмотрим, как записаны значения по уровням в полном двоичном дереве (Пример 1), то заметим, что значения на втором уровне идут парами. Это и понятно: каждый более высокий узел в двоичном дереве имеет ровно пару веток. Во втором примере на втором и третьем уровне только по одной ветке, поэтому их нужно дополнить константой None: [1,2,5,3,None,6,None,4,None,7] [1, 401, None, 349, 88, 90] Проект Кодирование Хаффмана Кодирование Хаффмана используют для сжатия текста. Частые буквы заменяют короткими кодами, редкие - длинными. Бинарное дерево строится так, что путь от корня к листу - это код символа (влево — 0, вправо - 1). import heapq from collections import Counter, defaultdict 392
class HuffmanNode: def __init__(self, char=None, freq=0, left=None, right=None): self.char = char # символ (только в листьях) self.freq = freq # частота self.left = left self.right = right def __lt__(self, other): # Для мин-кучи: сравниваем по частоте return self.freq < other.freq def build_huffman_tree(text): """Строит дерево Хаффмана из текста.""" if not text: return None # Считаем частоты freq_map = Counter(text) # Создаём кучу из листьев heap = [HuffmanNode(ch, fr) for ch, fr in freq_map.items()] heapq.heapify(heap) # Строим дерево: объединяем два узла с минимальной частотой while len(heap) > 1: left = heapq.heappop(heap) right = heapq.heappop(heap) merged = HuffmanNode(freq=left.freq + right.freq, left=left, right=right) heapq.heappush(heap, merged) return heap[0] # корень дерева def build_codes(root, prefix="", code_map=None): """Обход дерева в глубину, собираем коды.""" if code_map is None: code_map = {} if root is None: return code_map # Если лист — запоминаем код if root.char is not None: code_map[root.char] = prefix or "0" # если текст из 1 символа 393
return code_map build_codes(root.left, prefix + "0", code_map) build_codes(root.right, prefix + "1", code_map) return code_map def huffman_encode(text): """Кодирует текст и возвращает дерево, коды и битовую строку.""" if not text: return None, {}, "" root = build_huffman_tree(text) codes = build_codes(root) encoded = "".join(codes[ch] for ch in text) return root, codes, encoded def print_tree(node, indent=0, branch=""): """Красивый вывод дерева.""" if node is None: return if node.char is not None: print(" " * indent + f"{branch}→ '{node.char}' ({node.freq})") else: print(" " * indent + f"{branch}● [{node.freq}]") print_tree(node.left, indent + 1, "0") print_tree(node.right, indent + 1, "1") # === Демонстрация === if __name__ == "__main__": text = "abracadabra" root, codes, encoded = huffman_encode(text) print(f"Исходный текст: '{text}'") print(f"Длина текста: {len(text) * 8} бит (в ASCII)") print() print("Коды Хаффмана:") for ch, code in sorted(codes.items()): print(f" '{ch}' → {code}") print() print("Дерево Хаффмана:") print_tree(root) 394
print() print(f"Закодировано: {encoded}") print(f"Длина кода: {len(encoded)} бит") print(f"Сжатие: {len(encoded) / (len(text) * 8) * 100:.1f}% от исходного размера") Результат работы программы: Исходный текст: 'abracadabra' Длина текста: 88 бит (в ASCII) Коды Хаффмана: 'a' → 0 'b' → 111 'c' → 101 'd' → 100 'r' → 110 Дерево Хаффмана: ● [11] 0→ 'a' (5) 1● [6] 0● [2] 0→ 'd' (1) 1→ 'c' (1) 1● [4] 0→ 'r' (2) 1→ 'b' (2) Закодировано: 01111100101010001111100 Длина кода: 23 бит Сжатие: 26.1% от исходного размера 395
Проект Калькулятор через дерево разбора Строка с арифметическим выражением 3 + 5 * 2 превращается в бинарное дерево, где листья - числа, а узлы - операции. Обход дерева снизу вверх даёт результат с правильным приоритетом операций. class ExprNode: """Узел дерева арифметического выражения.""" def __init__(self, value, left=None, right=None): self.value = value # число или оператор '+', '-', '*', '/' self.left = left self.right = right def evaluate(self): """Рекурсивно вычисляет значение поддерева.""" if isinstance(self.value, (int, float)): return self.value left_val = self.left.evaluate() right_val = self.right.evaluate() match self.value: case '+': return left_val + right_val case '-': return left_val - right_val case '*': return left_val * right_val case '/': return left_val / right_val if right_val != 0 else float('inf') case _: raise ValueError(f"Неизвестная операция: {self.value}") def to_infix(self): """Возвращает выражение в скобках (для наглядности).""" if isinstance(self.value, (int, float)): return str(self.value) return f"({self.left.to_infix()} {self.value} {self.right.to_infix()})" class Parser: """Простой рекурсивный парсер арифметических выражений.""" 396
def __init__(self, text): self.tokens = text.replace('(', ' ( ').replace(')', ' ) ').split() self.pos = 0 def current(self): return self.tokens[self.pos] if self.pos < len(self.tokens) else None def consume(self): tok = self.current() self.pos += 1 return tok def parse(self): """Запускает парсинг и возвращает корень дерева.""" return self.parse_expression() def parse_expression(self): """expression = term { ('+' | '-') term }""" node = self.parse_term() while self.current() in ('+', '-'): op = self.consume() right = self.parse_term() node = ExprNode(op, node, right) return node def parse_term(self): """term = factor { ('*' | '/') factor }""" node = self.parse_factor() while self.current() in ('*', '/'): op = self.consume() right = self.parse_factor() node = ExprNode(op, node, right) return node def parse_factor(self): """factor = number | '(' expression ')'""" tok = self.current() if tok == '(': self.consume() # '(' node = self.parse_expression() self.consume() # ')' return node 397
# Число self.consume() return ExprNode(float(tok) if '.' in tok else int(tok)) def print_tree(node, indent=0, branch=""): """Визуализация дерева разбора.""" if node is None: return pad = " " * indent if isinstance(node.value, (int, float)): print(f"{pad}{branch}📄 {node.value}") else: print(f"{pad}{branch}🔧 '{node.value}'") print_tree(node.left, indent + 1, "├─L─ ") print_tree(node.right, indent + 1, "└─R─ ") # === Демонстрация === if __name__ == "__main__": expressions = [ "3 + 5 * 2", "(3 + 5) * 2", "10 - 2 - 3", "8 / 4 / 2", "2 * 3 + 4 * 5" ] for expr in expressions: print("=" * 50) print(f"Выражение: {expr}") parser = Parser(expr) tree = parser.parse() print(f"Дерево разбора:") print_tree(tree) print(f"Скобочная запись: {tree.to_infix()}") print(f"Результат: {tree.evaluate()}") print() Калькулятор работает отлично: 398
Выражение: 3 + 5 * 2 Дерево разбора: 🔧 '+' ├─L─ 📄 3 └─R─ 🔧 '*' ├─L─ 📄 5 └─R─ 📄 2 Скобочная запись: (3 + (5 * 2)) Результат: 13 Выражение: (3 + 5) * 2 Дерево разбора: 🔧 '*' ├─L─ 🔧 '+' ├─L─ 📄 3 └─R─ 📄 5 └─R─ 📄 2 Скобочная запись: ((3 + 5) * 2) Результат: 16 Выражение: 10 – 2 – 3 Дерево разбора: 🔧 '-' ├─L─ 🔧 '-' ├─L─ 📄 10 └─R─ 📄 2 └─R─ 📄 3 Скобочная запись: ((10 – 2) – 3) Результат: 5 Выражение: 8 / 4 / 2 Дерево разбора: 🔧 '/' ├─L─ 🔧 '/' ├─L─ 📄 8 └─R─ 📄 4 └─R─ 📄 2 Скобочная запись: ((8 / 4) / 2) Результат: 1.0 399
Выражение: 2 * 3 + 4 * 5 Дерево разбора: 🔧 '+' ├─L─ 🔧 '*' ├─L─ 📄 2 └─R─ 📄 3 └─R─ 🔧 '*' ├─L─ 📄 4 └─R─ 📄 5 Скобочная запись: ((2 * 3) + (4 * 5)) Результат: 26 400
Исключения 401
Пусть у нас есть функция add, которая возвращает строку – 2 числа и их сумму: def add(a, b): return f’{a} + {b} = {a + b}’ Если мы передадим в функцию 2 числа, то получим правильный ответ: def main(): a = 1 b = 2 res = add(a, b) print(res) print() if __name__ == ‘__main__’: main() Если мы передадим в функцию не число, а строку, то программа завершится с ошибкой: a = 1 b = ‘2’ res = add(a, b) print(res) В таких случаях опасный код помещаем в секцию try: 402
def add2(a, b): try: return f’{a} + {b} = {a + b}’ Если код не вызовет ошибку, то функция вернёт строку с числами и суммой. В противном случае выполнится код в секции except. Функция вернёт сообщение и значение None: except: print('ВЫ ДОЛЖНЫ ПЕРЕДАТЬ 2 ЧИСЛА!') return None a = 1 b = ‘2’ res = add2(a, b) print(res) Программа продолжит свою работу, потому что мы сами обработали ошибку. Подобных простых ошибок легко избежать с аннотацией типов для параметров функции: def add3(a: int, b: int) -> str: return f’{a} + {b} = {a + b}’ Среда разработки подчеркнёт ошибочный аргумент, а мышка подскажет, в чём заключается ошибка. A = 1 b = ‘2’ res = add3(a, b) print(res) 403
Тут, правда, нужно быть внимательным, потому что аннотация типов не спасёт вас от ошибки, если вы всё-таки запустите программу. В некоторых случаях бывает полезно использовать громоздкую конструкцию из ветвей try – except – else – finally: def add4(a, b): try: res = f'{a} + {b} = {a + b}' except: print('ВЫ ДОЛЖНЫ ПЕРЕДАТЬ 2 ЧИСЛА!') else: return res finally: print('Это конец!') Секция finally выполняется всегда, поэтому вы можете использовать её, например, для закрытия файла независимо от того, произошла ли ошибка при работе с файлом или нет. В нашем примере функция add4 просто напечатает сообщение. Но сначала выполнится секция try. Здесь код может вызвать исключение, если обнаружатся существенные ошибки. Например, один из аргументов имеет недопустимый тип. В этом случае выполнится секция except, и функция напечатает сообщение ВЫ ДОЛЖНЫ ПЕРЕДАТЬ 2 ЧИСЛА! Если секция except не имеет оператора return, то функция вернёт значение None (но перед этим выполнится секция finally): a = 1 b = ‘2’ res = add4(a, b) print(res) 404
Если код в секции try не вызвал ошибку, то секция except не выполнится, а выполнится код в секции else, и функция вернёт строку с решением арифметического примера: a = 1 b = 2 res = add4(a, b) print(res) 405
Функции eval и exec 406
eval это встроенная функция, поэтому её можно вызывать без дополнительного импорта. Эта функция получает строку, вычисляет выражение в ней и возвращает результат вычислений. Еval – это сокращение слова evaluate – оценивать. Пусть у нас есть строка со списком и мы хотим извлечь из неё список. Передаём строку в функцию eval и получаем от неё список в готовом виде: lst = «[1,2,3,4,5, 'вышел', 'зайчик', 'погулять']» print(lst, type(lst)) lst = eval(lst) print(lst, type(lst)) [1,2,3,4,5, 'вышел', 'зайчик', 'погулять'] <class 'str'> [1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять'] <class 'list'> Строку, в которой перечисляются элементы разных типов, функция eval превращает в кортеж, причём числа сразу становятся числами, что очень удобно: lst = «1,2,3,4,5, 'вышел', 'зайчик', 'погулять'» print(lst, type(lst)) lst = eval(lst) print(lst, type(lst)) print(type(lst[0]), type(lst[5])) 1,2,3,4,5, 'вышел', 'зайчик', 'погулять' <class 'str'> (1, 2, 3, 4, 5, 'вышел', 'зайчик', 'погулять') <class 'tuple'> <class 'int'> <class 'str'> 407
exec также встроенная функция. Она получает строку, компилирует код и выполняет его. Еxec – это сокращение слова execute – выполнять. Например, мы можем определить новую функцию при вызове функции exec. Пусть это будет функция summa, которая возвращает сумму элементов итерируемого объекта: lst = [1,2,3,4,5] exec('def summa(iter): return sum(iter)') print(f'Сумма = {summa(lst)}') Поскольку функция summa будет определена только при выполнении программы, среда разработки ничего не знает о ней. Запускаем программу и получаем правильный ответ от уже определённой функции summa. Сумма = 15 С помощью функции exec мы можем определять и новые переменные: exec(‘def summa(iter): return sum(iter)’) exec (‘lst = [1,2,3,4,5]’) print(f’Сумма = {summa(lst)}’) 408
Проект 9 цифр Многим известна задача: расставить между цифрами 1..9 знаки +- *, чтобы в результате вычислений получилась сотня. Мы облегчим себе задачу, если расставим знаки между всеми цифрами, то есть не будем делить цифры на группы, в которых несколько подряд идущих цифр. Знаки + и – можно ставить перед первым однозначным числом, а знак умножения нельзя. Для решения задачи нам потребуются 2 глобальные переменные и функция solve: # заданные цифры: digits = ['1','2','3','4','5','6','7','8','9'] # словарь для сохранения результатов: dic = {} def main(): # решаем задачу: solve(9, []) # сортируем словарь: res = dict(sorted(dic.items())) # распечатываем словарь: for k in res: print(k, res[k]) main() Рекурсивная функция solve генерирует все комбинации из трёх арифметических знаков и отправляет их в функцию calculate: def solve(n, l): if len(l) == n: print(l) #calculate(l) else: solve(n, l + [‘-‘]) solve(n, l + [‘+’]) solve(n, l + [‘*’]) 409
[‘-‘, [‘-‘, [‘-‘, . . . [‘*’, [‘*’, [‘*’, [‘*’, ‘-‘, ‘-‘, ‘-‘, ‘-‘, ‘-‘, ‘-‘, ‘-‘, ‘-‘] ‘-‘, ‘-‘, ‘-‘, ‘-‘, ‘-‘, ‘-‘, ‘-‘, ‘+’] ‘-‘, ‘-‘, ‘-‘, ‘-‘, ‘-‘, ‘-‘, ‘-‘, ‘*’] ‘*’, ‘*’, ‘*’, ‘*’, ‘*’, ‘*’, ‘*’, ‘*’, ‘*’, ‘*’, ‘*’, ‘*’, ‘*’, ‘*’, ‘*’, ‘*’, ‘*’, ‘*’, ‘*’, ‘*’, ‘*’, ‘*’, ‘*’, ‘*’, ‘+’, ‘*’, ‘*’, ‘*’, ‘*’] ‘-‘] ‘+’] ‘*’] В функции calculate мы собираем строку из арифметических знаков и цифр: def calculate(l): global digits, dict # строка для формирования # арифметического выражения: s = ‚‘ # собираем строку: for i in range(9): # первым знаком не может быть * if i == 0 and l[i] == ‚*‘: return s += l[i] + digits[i] Вычисляем полученное арифметическое выражение: # вычисляем выражение: res = eval(s) И сохраняем результаты в словаре: # оставляем только суммы 0..100: if 0 < res < 101: # записываем результат и выражение в словарь: if res in dic: dic[res] += [s] else: dic[res] = [s] 410
Число 100 можно получить многими способами: 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. -1-2+3*4*5+6*7-8+9 -1+2+3+4*5*6-7-8-9 -1+2*3*4+5*6+7*8-9 -1*2-3-4-5+6*7+8*9 -1*2-3-4+5*6+7+8*9 -1*2-3+4*5+6+7+8*9 -1*2+3+4+5*6-7+8*9 -1*2+3+4+5*6+7*8+9 -1*2+3*4+5+6+7+8*9 +1-2+3*4*5-6+7*8-9 +1-2+3*4*5+6*7+8-9 +1-2*3-4-5+6*7+8*9 +1-2*3-4+5*6+7+8*9 +1-2*3+4*5+6+7+8*9 +1+2-3*4-5+6*7+8*9 +1+2-3*4+5*6+7+8*9 +1+2+3-4*5+6*7+8*9 +1+2+3+4+5+6+7+8*9 +1+2*3+4*5-6+7+8*9 +1*2*3-4*5+6*7+8*9 +1*2*3+4+5+6+7+8*9 +1*2*3*4+5+6-7+8*9 +1*2*3*4+5+6+7*8+9 При вычислении арифметических выражений сначала выполняется умножение, а затем сложение и вычитание! Не всем числам так здорово повезло! Например, число 83 допускает только десяток способов, но все натуральные числа до 100 включительно вполне можно получить таким способом. Как видите, вполне можно ограничиться двумя арифметическими операциями – сложением и умножением. 411
Менее известна задача, в которой цифры располагаются в обратном порядке: digits = [‚9‘,‘8‘,‘7‘,‘6‘,‘5‘,‘5‘,‘3‘,‘2‘,‘1‘] Сотню здесь можно получить также многими способами: 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. -9-8-7+6*5*4+3+2-1 -9+8+7*6+5*4*3-2+1 -9+8*7-6+5*4*3-2+1 -9+8*7+6*5+4*3*2-1 +9-8+7*6+5*4*3-2-1 +9+8*7+6+5+4*3*2*1 +9+8*7+6*5+4+3-2*1 +9*8-7+6+5+4*3*2*1 +9*8-7+6*5+4+3-2*1 +9*8+7-6+5*4+3*2+1 +9*8+7+6+5+4+3+2+1 +9*8+7+6+5+4+3*2*1 +9*8+7+6+5+4*3-2*1 +9*8+7+6+5*4-3-2*1 +9*8+7+6+5*4-3*2+1 +9*8+7+6*5-4-3-2*1 +9*8+7+6*5-4-3*2+1 +9*8+7+6*5-4*3+2+1 +9*8+7*6-5-4-3-2*1 +9*8+7*6-5-4-3*2+1 +9*8+7*6-5-4*3+2+1 +9*8+7*6-5*4+3+2+1 +9*8+7*6-5*4+3*2*1 И при такой расстановке цифр можно получить все натуральные числа до 100 включительно. Если добавить операцию деления, то мы получим ещё 2 варианта для сотни: 412
1. -1+2+3+4*5*6*7/8-9 2. +1+2*3*4*5/6+7+8*9 Также появятся новые варианты и для всех других чисел до сотни. При обратной расстановке цифр новых вариантов добавилось гораздо больше: 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. -9*8-7+6*5*4*3/2-1 +9+8*7+6+5+4*3*2/1 +9+8*7+6+5*4*3/2-1 +9+8*7+6*5+4+3-2/1 +9+8*7+6*5+4*3/2-1 +9*8-7+6+5+4*3*2/1 +9*8-7+6+5*4*3/2-1 +9*8-7+6*5+4+3-2/1 +9*8-7+6*5+4*3/2-1 +9*8+7+6+5+4+3*2/1 +9*8+7+6+5+4*3-2/1 +9*8+7+6+5*4-3-2/1 +9*8+7+6*5-4-3-2/1 +9*8+7+6*5*4/3/2+1 +9*8+7*6-5-4-3-2/1 +9*8+7*6-5*4+3*2/1 +9*8*7/6+5+4+3*2+1 +9*8*7/6+5+4*3-2+1 +9*8*7/6+5*4-3-2+1 +9*8*7/6*5/4-3-2*1 +9*8*7/6*5/4-3-2/1 +9*8*7/6*5/4-3*2+1 Все остальные числа до сотни также обзавелись новыми вариантами. Вычисления немного усложнились: def solve(n, l): if len(l) == n: #print(l) calculate(l) 413
else: solve(n, solve(n, solve(n, solve(n, l l l l + + + + ['-']) ['+']) ['*']) ['/']) def calculate(l): global digits, dict # строка для формирования # арифметического выражения: s = '' # собираем строку: for i in range(9): # первым знаком не может быть * if i == 0 and (l[i] == '*' or l[i] == '/'): return s += l[i] + digits[i] # оставляем только варианты с делением: if not '/' in s: return # вычисляем выражение: res = eval(s) # оставляем только целые числа: if res % 1 != 0: return res = int(res) # оставляем только суммы 0..100 if 0 < res < 101: # записываем результат и выражение в словарь: if res in dic: dic[res] += [s] else: dic[res] = [s] Найдите варианты расстановки также с арифметическими знаками // и ** 414
Давайте оставим только семёрки: digits = [‚7‘,‘7‘,‘7‘,‘7‘,‘7‘,‘7‘,‘7‘,‘7‘,‘7‘] Число 100 нам получить не удастся даже с целочисленным делением, поэтому используем вещественное деление. А получить нужно половину сотни: 1. 2. 3. 4. 5. 6. 7. 8. +7+7+7+7+7+7+7+7/7 +7+7+7+7+7+7+7/7+7 +7+7+7+7+7+7/7+7+7 +7+7+7+7+7/7+7+7+7 +7+7+7+7/7+7+7+7+7 +7+7+7/7+7+7+7+7+7 +7+7/7+7+7+7+7+7+7 +7/7+7+7+7+7+7+7+7 Легко заметить, что, по сути, вариант единственный. Немного не дотянуло до сотни число 98: 1. 2. 3. 4. 5. 6. 7. 8. +7+7+7+7+7+7+7+7*7 +7+7+7+7+7+7+7*7+7 +7+7+7+7+7+7*7+7+7 +7+7+7+7+7*7+7+7+7 +7+7+7+7*7+7+7+7+7 +7+7+7*7+7+7+7+7+7 +7+7*7+7+7+7+7+7+7 +7*7+7+7+7+7+7+7+7 Эти примеры показывают, что одинаковые цифры лучше не использовать из-за бедности вариантов расстановки арифметических знаков. 415
Модуль fractions – Простые дроби 416
Для работы с простыми дробями в Питоне имеется модуль fractions. Все программы, в которых используются вычисления с простыми дробями, должны начинаться с импорта этого модуля: import fractions as fr Проект Простые дроби Обыкновенная дробь состоит из числителя и знаменателя, которые обычно записываются так: числитель ----------знаменатель По-английски числитель называется numerator, а знаменатель – denominator. Самый простой способ получить дробь – создать экземпляр класса Fraction. Каждая дробь имеет свойство numerator, которое возвращает числитель, и свойство denominator, которое возвращает знаменатель. По умолчанию числитель равен нулю, а знаменатель – единице: import fractions as fr dr = fr.Fraction() print(f'Числитель = {dr.numerator}') print(f'Знаменатель = {dr.denominator}') print(f'Дробь = {dr}') Целое число – это дробь, у которой знаменатель равен единице. Поскольку по умолчанию все экземпляры класса Fraction получают такой знаменатель, то целые числа как дроби можно создать так: dr = fr.Fraction(12) print(f'Числитель = {dr.numerator}') 417
print(f'Знаменатель = {dr.denominator}') print(f'Дробь = {dr}') Если знаменатель отличается от единицы, то его следует передать в конструктор класса Fraction: dr = fr.Fraction(12, 5) print(f'Числитель = {dr.numerator}') print(f'Знаменатель = {dr.denominator}') print(f'Дробь = {dr}') Если числитель делится нацело на знаменатель, то мы получим после сокращения целое число: dr = fr.Fraction(12, 4) print(f'Числитель = {dr.numerator}') print(f'Знаменатель = {dr.denominator}') print(f'Дробь = {dr}') Новую дробь можно создать из готовой дроби. Если второй аргумент - целое число, то на него умножается знаменатель, то есть дробь делится на число: dr = fr.Fraction(15, 4) print(f'Числитель = {dr.numerator}') print(f'Знаменатель = {dr.denominator}') print(f'Дробь = {dr}') print() dr = fr.Fraction(dr, 2) print(f'Числитель = {dr.numerator}') print(f'Знаменатель = {dr.denominator}') print(f'Дробь = {dr}') Если создать дробь из двух дробей, то первая дробь будет разделена на вторую: 418
dr = fr.Fraction(15, 4) print(f'Числитель = {dr.numerator}') print(f'Знаменатель = {dr.denominator}') print(f'Дробь = {dr}') print() dr2 = fr.Fraction(17, 5) print(f'Числитель = {dr2.numerator}') print(f'Знаменатель = {dr2.denominator}') print(f'Дробь = {dr2}') print() dr = fr.Fraction(dr, dr2) print(f'Числитель = {dr.numerator}') print(f'Знаменатель = {dr.denominator}') print(f'Дробь = {dr}') Дробь можно создать из вещественного числа. Однако вещественные числа неточные, поэтому дроби получаются ужасные: dr = fr.Fraction(1.2) print(f'Числитель = {dr.numerator}') print(f'Знаменатель = {dr.denominator}') print(f'Дробь = {dr}') Числитель = 5404319552844595 Знаменатель = 4503599627370496 Дробь = 5404319552844595/4503599627370496 И даже метод from_float не спасает ситуацию: dr = fr.Fraction.from_float(1.2) print(f'Числитель = {dr.numerator}') print(f'Знаменатель = {dr.denominator}') print(f'Дробь = {dr}') 419
Числитель = 5404319552844595 Знаменатель = 4503599627370496 Дробь = 5404319552844595/4503599627370496 Вместо вещественных чисел лучше использовать числа типа Decimal: import decimal f = decimal.Decimal('1.2') dr = fr.Fraction(f) print(f'Числитель = {dr.numerator}') print(f'Знаменатель = {dr.denominator}') print(f'Дробь = {dr}') Такой же результат даёт метод from_decimal: f = decimal.Decimal('1.2') dr = fr.Fraction.from_decimal(f) print(f'Числитель = {dr.numerator}') print(f'Знаменатель = {dr.denominator}') print(f'Дробь = {dr}') Также с вещественными limit_denominator: числами можно использовать метод dr = fr.Fraction.from_float (1.2).limit_denominator() print(f'Числитель = {dr.numerator}') print(f'Знаменатель = {dr.denominator}') print(f'Дробь = {dr}') Ему можно передавать максимальное значение числителя, чтобы получить приближённое значение вещественного числа. Самое старое более или менее точное значение числа пи было получено около 4 тыс. лет назад: 25/8 = 3,125. Давайте проверим, как с этим справится Питон: 420
import math dr = fr.Fraction.from_float (math.pi).limit_denominator(10) print(f'Числитель = {dr.numerator}') print(f'Знаменатель = {dr.denominator}') print(f'Дробь = {dr} --> {dr.numerator/dr.denominator}') Питон нашёл более точное приближение. Числитель = 22 Знаменатель = 7 Дробь = 22/7 --> 3.142857142857143 Именно такую дробь предложил Архимед для приближённых вычислений. Если мы ограничим знаменатель сотней, то получим ещё более точное приближение: dr = fr.Fraction.from_float (math.pi).limit_denominator(100) print(f'Числитель = {dr.numerator}') print(f'Знаменатель = {dr.denominator}') print(f'Дробь = {dr} --> {dr.numerator/dr.denominator}') Числитель = 311 Знаменатель = 99 Дробь = 311/99 --> 3.1414141414141414 Идя дальше по этому пути, мы получим очень хорошее приближение: dr = fr.Fraction.from_float (math.pi).limit_denominator(1000) Числитель = 355 421
Знаменатель = 113 Дробь = 355/113 --> 3.1415929203539825 Гораздо более точное, чем в древнем египетском папирусе Ахмеса, – 256/81 = 3.1604938271604937. И даже более поздние приближения - 92/29 ≈ 3,1724 и корень квадратный из 10 ≈ 3,1622 – не столь точны. Астроном Клавдий Птолемей предложил другое значение, но тоже не очень точное – 377/120 = 3.14166666666. Фибоначчи предложил свой вариант, который оказался даже хуже – 864/275 = 3.1418181818181816. В 480-х годах китайский математик Цзу Чунчжи нашёл «наше» приближение - 355/113. Википедия подытожила наши изыскания: А наш последний пи-рекорд такой: 422
dr = fr.Fraction.from_float (math.pi).limit_denominator(100000) Числитель = 312689 Знаменатель = 99532 Дробь = 312689/99532 --> 3.1415926536189365 Проект Математика с простыми дробями Над простыми дробями можно выполнять все арифметические операции: import fractions as fr dr1 = fr.Fraction(1, 3) print(f'Числитель = {dr1.numerator}') print(f'Знаменатель = {dr1.denominator}') print(f'Дробь = {dr1}') print() dr2 = fr.Fraction(2, 7) print(f'Числитель = {dr2.numerator}') print(f'Знаменатель = {dr2.denominator}') print(f'Дробь = {dr2}') print() res = dr1 + dr2 print(f'{dr1} + res = dr1 - dr2 print(f'{dr1} res = dr1 * dr2 print(f'{dr1} * res = dr1 / dr2 print(f'{dr1} / res = dr1 % dr2 print(f'{dr1} % {dr2} = {res}') {dr2} = {res}') {dr2} = {res}') {dr2} = {res}') {dr2} = {res}') 423
При возведении в степень не всегда получаются простые дроби: res = dr1 ** 2 print(f'{dr1} ** 2 = {res}') res = dr1 ** dr2 print(f'{dr1} ** {dr2} = {res}') Проект Задача 1933 года из учебника математики для 6 класса Старая задача из Ютуба: https://www.youtube.com/watch?v=06jG8WlAl7c Спустя 5 минут просмотра мы узнаем, что значение выражения равно 10. 424
Когда я был маленьким, я тоже учился в школе и вычислял дроби. Ничего сложного в этом занятии нет, но нудно. Поэтому мы решим задачу на Питоне, что столь же полезно, как и вычисление смешанных дробей. Смешанные дроби состоят из целой и дробной частей. Для удобства перевода смешанных дробей в питоньи дроби пишем вспомогательную функцию d: import fractions as fr # ВОЗВРАЩАЕТ ДРОБЬ # с - целая часть # b - числитель # a - знаменатель def d(c=0, a=1, b=1): return c + fr.Fraction(a, b) Аккуратно переписываем условие задачи и получаем правильный ответ: # вычисляем выражение: res = d(24,3,4) - (d(12,5,9) - d(4,7,8) + d(1,5,12)) - (d(13,15,16) - d(10,5,9)) - d(2,13,48) print(f'Ответ: {res}') Питон умеет считать дроби, а мы умеем программировать на Питоне! 425
Вопросы и задачи на сообразительность 426
Задача из комбинаторной геометрии. Разрежьте (или раскрасьте) квадрат с дыркой на 4 равные части вдоль сторон сетки 5 х 5 клеток. При этом решения, полученные с помощью поворотов и отражений, не считаются различными, поэтому на рисунке ниже показано одно решение задачи, а не два! Всего существует 7 принципиально различных способов разрезания квадрата на 4 равные части вдоль линий сетки. Один из них вам уже известен. Найдите остальные шесть! Так как квадрат состоит из 25 клеток, а разбить его требуется на 4 равные части, то на каждую из них приходится по 6 клеток. Одна клетка – в центре квадрата – в разбиении не участвует. Гексамино – это фигурки, составленные из шести равных квадратиков, имеющих общие стороны. Гексамино делятся на двусторонние, которые при переворачивании изменяются, и на односторонние – они при переворачивании сохраняют свою форму. Односторонних гексамино насчитывается 10 штук, а двусторонних – 25. Если считать гексамино, которые получаются при переворотах и отражениях, разными, то всего имеется 10 + 25*2 = 60 разных гексамино. Односторонние гексамино на рисунке окрашены в красный цвет, а двусторонние- в зелёный. 427
428
Так как решение головоломки на разрезание предполагает, что равные части образуют связные области, то её условие можно перефразировать так: Составьте квадрат размером 5 х 5 клетки с дыркой в центре из 4 одинаковых фигурок гексамино. 429
Для решения задачи вы можете использовать бумагу в клеточку: Или спички: 430
431
Ответы Квадрат 5 х 5 клетки с дыркой в центре можно разрезать на 4 равные части вдоль сторон сетки семью различными способами: Первый вариант разрезания был дан в условии задачи, поэтому здесь не приводится. 432
Литература 433
[Python] Рубанцев Валерий Питонарий. Python: Первое знакомство 2026, - 726 с. 434
[Python] Рубанцев Валерий Питоний. Python: Лучший друг 2026, - 614 с. 435
[C#] Рубанцев Валерий Программирование на языке C#. Начальный уровень 2014, - 622 с. 436
[C#] Рубанцев Валерий Программирование на языке Си-шарп. Базовый уровень 2013, - 860 с. 437
[Котлин] Рубанцев Валерий Привет, Котлин! 2018, - 336 с. 438
[PascalABC.NET] Рубанцев Валерий Программирование на языке PascalABC.NET. Начальный уровень. Часть 1 2023, - 540 с. 439
[PascalABC.NET] Рубанцев Валерий Программирование на языке PascalABC.NET. Начальный уровень. Часть 2 2023, - 540 с. 440
[КА86] [КА96] Б.А. Кордемский, А.А.Ахадов Удивительный мир чисел (МАТЕМАТИЧЕСКИЕ ГОЛОВОЛОМКИ И ЗАДАЧИ ДЛЯ ЛЮБОЗНАТЕЛЬНЫХ) М.: Просвещение, 1986. – 144 с. М.: Просвещение, 1996. – 159 с 441
[Нагибин88] Нагибин Ф.Ф., Канин Е.С. Математическая шкатулка М.: Просвещение, 1988. – 160 с. 442
[ГЕ02] Гик Е.Я. 1000 интеллектуальных игр Астрель, АСТ, 2002. – 272 с. ISBN 5-17-014188-2, 5-271-04107-7 [ГМ72] Гарднер Мартин Математические досуги М.: Мир, 1972. – 495 с. 443
[ЗД07] Златопольский Д.М. Сборник задач по программированию БХВ-Петербург, 2007. – 240 с. ISBN: 5-9775-0017-3 [GJE06] James Edward Gray II Best of Ruby Quiz The Pragmatic Programmers LLC, 2006. – 285 c. ISBN 0-9766940-7-7 444
[KS98] Kreher D.L., Stinson D.R. Combinatorial Algorithms: Generation, Enumeration, and Search CRC, 1998.- 344 c. ISBN: 084933988X [PC03] Skiena Steven S., Revilla Miguel A. Programming Challenges: The Programming Contest Training Manual Springer, 2003. - 368 с. ISBN 0387001638 445
[SI10] Stewart Ian Professor Stewart's Hoard of Mathematical Treasures Basic Books, 2010. – 352 с. ISBN 0465017754, 1846682924 [КК07] Красиков И. В., Красикова И. Е. Алгоритмы Эксмо, 2007.- 256 с. ISBN: 978-5-699-21047-3 Серия: Просто как дважды два 446
[КП63] Кобринский Н., Пекелис В. Быстрее мысли Молодая гвардия, 1963. – 472 с. 2-е изд. 447