LANG·I Язык Глава 12 из 65
Остров кроликов и лис
Глава-симуляция: строим остров, где трава кормит кроликов, а кролики — лис, и смотрим, как из тысяч мелких решений складываются волны популяций. По дороге — классы и объекты, которые в Норвегии придумали как раз для таких моделей.
Язык
Опирается на: 11 · Отчёт комиссии 08 · Словарь и телеграф
Что вы унесёте из главы
- описывать вещи из мира классами: состояние — в атрибутах, поведение — в методах
- строить семейства классов наследованием и собирать из объектов большую модель
- понимать, когда класс нужен, а когда хватит функции, словаря или dataclass
Прошлая глава кончилась на том, что наш код стал надёжным: он проверяет ввод, ловит исключения и сам себя тестирует. Но всё, что он до сих пор обрабатывал, — числа и строки. Мир, который хочется описать программой, состоит из вещей. У банковского счёта есть остаток, и снять больше остатка нельзя. У персонажа игры — здоровье и место на карте. У зайца — голод, возраст и нора. Таких вещей тысячи, у каждой своё состояние, и каждая ведёт себя по своим правилам.
В этой главе мы построим остров: на нём растёт трава, траву едят кролики, кроликов едят лисы. Каждый зверь живёт сам по себе: бродит, тратит силы, ест, если повезёт, приносит потомство и умирает от голода, если не повезёт. Вот что получится в конце главы. Запустите.
Зелёное — трава, жёлтое — земля, где траву съели и она ещё не отросла. Серые точки — кролики, рыжие — лисы. В программе нет ни одной строчки о популяции в целом: никто не командует «через сто дней пусть кроликов станет много». Каждый зверь решает только за себя. А волны складываются сами: кроликов становится много, лисы отъедаются и плодятся, кроликов становится мало, лисы голодают — и всё начинается заново. Следите за числами в углах: лисы всё время отстают от кроликов на несколько десятков дней.
Сейчас в этой программе незнакомо почти всё: Island, Rabbit, Fox, точки после имён. К концу главы вы напишете остров сами, строчку за строчкой, и сможете менять в нём что угодно: сделать лис прожорливее, траву медленнее, наслать на остров засуху.
Остров из словарей
Начнём с того, что уже умеем. Зверь — это несколько значений под одним именем: где стоит, сколько у него сил. Для этого есть словарь из главы 8, а для того, что зверь делает, — функции из главы 5.
Работает. Но в последней строке вывода у кролика из-за опечатки в ключе молча появилась вторая «энергия». Трейсбека не будет: словарь не знает, какие ключи у кролика правильные, а какие нет. Так же молча можно положить в energy строку или отдать лису функции, которая ждёт кролика.
Хуже другое. Функция step знает обо всех зверях сразу: на каждый вид — своя ветка if. Добавим ежа — придётся дописать ветку в step, потом в функцию, которая рисует зверей, потом в ту, что их считает. Данные зверя лежат в словаре, а его поведение размазано по функциям, и каждая функция обязана помнить про всех. На двух видах это терпимо, на двадцати — уже нет.
Хочется сложить в одно место всё, что относится к кролику: из чего он состоит и что умеет. Сказать один раз: «вот что такое кролик» — и потом делать кроликов сколько угодно. Такой способ придумали шестьдесят с лишним лет назад, и как раз для нашей задачи — моделирования.
Осло, 1962. Язык для моделей
Классы из Simula, прямо или через посредников, взяли языки, на которых сегодня пишут почти всё, — C++, Java, C#, Python. Бьёрн Страуструп, автор C++, и Джеймс Гослинг, автор Java, называли Simula одним из главных источников своих идей. Наш остров — задача того же рода, что у Нюгора: много участников, у каждого своё состояние и свои правила. Возьмём его инструмент.
Чертёж кролика
Опишем кролика так, как описывали участников модели в Simula: чертежом.
Слово class начинает описание нового вида объектов — класса. Имена классов принято писать с большой буквы: Rabbit, Fox, Island. Внутри — функция со странным именем __init__, по два подчёркивания с каждой стороны. Сами мы её не вызываем: её вызывает Python, когда делает нового кролика.
Строка bunny = Rabbit(3, 5) вызывает класс как функцию. Python создаёт пустой объект, передаёт его в __init__ первым аргументом — под именем self, «сам», — а за ним 3 и 5. Функция __init__ вешает на новый объект три имени: self.x, self.y, self.energy. Имена, привязанные к объекту, называют атрибутами, и достают их через точку: bunny.energy. Объект, сделанный по классу, называют экземпляром этого класса: bunny и fluffy — два экземпляра Rabbit, и атрибуты у каждого свои.
Последняя строка печатает <class '__main__.Rabbit'>. У Python появился новый тип — наш собственный, наравне с int и str. Слово __main__ означает «из главной программы»: класс описан в ней самой.
Что кролик умеет
Пока наш кролик — тот же словарь, только с точками вместо квадратных скобок. Всё меняется, когда в класс кладут ещё и поведение.
Функции, описанные внутри класса, называют методами. Вызов bunny.hop(1, 0) Python читает так: найди у класса кролика bunny функцию hop и вызови её, подставив bunny первым аргументом. То есть это в точности
Поэтому у каждого метода первый параметр — self: в него попадает тот объект, что стоит перед точкой. Сам по себе self — обычное имя, его можно было бы назвать как угодно, но все пишут self — пишите и вы. Нажмите «Шаги» в ячейке выше и дойдите до строки self.energy -= 1: в кадре метода hop имя self указывает на кролика, у которого метод вызвали, — на bunny. А в кадре метода eat тот же self указывает уже на fluffy.
self и как та же строка выглядит в записи через класс.Так же устроены и все объекты, с которыми вы работали до сих пор.
int, str, list — классы, такие же, как наш Rabbit, только написанные на языке C внутри самого Python. Метод append из главы 6 — функция класса list, и self у неё — список перед точкой. Число 255 — экземпляр класса int, и у него есть метод, который считает, сколько двоичных цифр нужно для его записи, — восемь. Всё, что в Python можно положить в переменную, — объект какого-то класса.
Каким кролик выглядит
Попробуем напечатать кролика.
Python не знает, что в кролике интересно, и печатает имя класса и адрес объекта в памяти — шестнадцатеричное число, которое у вас будет другим. Научим кролика себя показывать.
Методы с двумя подчёркиваниями с каждой стороны — специальные; программисты зовут их «дандер», от double underscore. Напрямую их обычно не вызывают: Python вызывает их сам, когда с объектом нужно что-то сделать. print и str() просят у объекта __str__ — текст для человека. Список, отладчик и функция repr() из главы 7 просят __repr__ — представление для программиста. Хороший __repr__ похож на код, который создаёт такой же объект. Если написать только __repr__, print возьмёт его.
Специальных методов десятки, и через них ваши объекты встраиваются в язык. len(x) вызывает x.__len__(), a + b — a.__add__(b), a < b — a.__lt__(b). Определите у класса __add__ — и его экземпляры можно будет складывать плюсом, как числа. Так устроены дроби, даты, векторы; вектор вы напишете сами в задаче в конце главы.
Общее на всех
У всех кроликов есть общее: клетка травы даёт каждому одинаково — скажем, 4 единицы сил. Хранить четвёрку в каждом кролике незачем. Её можно записать в самом классе, без self.
Когда кролик ищет у себя self.FOOD, Python сначала смотрит в атрибуты самого объекта, не находит и идёт в класс. Поменяли число в классе — поменялось для всех кроликов сразу, даже уже родившихся. Такие атрибуты класса удобны для настроек модели, и мы будем ими пользоваться.
Ещё один подвох в духе главы 2. Кролики живут в норах, у каждой норы свой список жильцов. Прежде чем запускать, сделайте ставку.
Пушок живёт в северной норе, Снежок — в южной. Что напечатает print(north.rabbits)?
Обоих. Строка rabbits = [] стоит в теле класса и выполняется один раз — когда Python читает описание класса. Список получился один, и лежит он в классе. У объектов north и south своего атрибута rabbits нет, поэтому self.rabbits находит список класса, а append меняет этот единственный список. Нор две, а список жильцов один на весь остров.
Это та же ловушка, что в главе 6: два ярлыка на одном изменяемом объекте. Лекарство — создавать список в __init__: строка self.rabbits = [] выполняется при каждом создании норы и даёт каждой норе свой новый список. Правило такое: изменяемые значения (списки, словари, множества) заводят в __init__, а в теле класса держат только общее и неизменяемое, вроде чисел-настроек.
Два кролика или один
Когда два кролика равны?
Кролики a и b стоят в одной клетке, у них одинаковые силы — и всё же a == b ложно. Для собственных классов Python по умолчанию считает равным только сам объект себе: == работает как is. Для зверей это правильно: два кролика в одной клетке — всё-таки два кролика. А c = a ничего не копирует, оно вешает на кролика второй ярлык, как в главе 2. Лиса поймала c — и погиб a, потому что это один и тот же кролик. Пройдите программу кнопкой «Шаги»: от a и c стрелки идут в одну точку.
Бывают объекты, которые естественно сравнивать по содержимому: клетка острова, точка, вектор, дата. Для них пишут __eq__.
Теперь две клетки с одинаковыми координатами равны, и in находит клетку в списке: он сравнивает элементы через ==. А последняя строка падает с TypeError: unhashable type: 'Cell'. Это та же история, что с ключами словаря в главе 8: множество раскладывает элементы по ящикам, а у объекта, равенство которого определили вы, Python не знает, как выбрать ящик, чтобы равные клетки попадали в один. Для этого нужен ещё метод __hash__; как он устроен и почему такие объекты лучше не менять, расскажет глава 16.
Забор вокруг травы
Кролик ест траву. Где она хранится? Пусть остров помнит для каждой клетки, через сколько дней трава отрастёт: 0 — уже выросла, 10 — только что съедена. Вот луг.
Таблица _wait начинается с подчёркивания. Для Python это обычное имя, но для программистов — договорённость: «это внутреннее, снаружи не трогать». Кролик не полезет в таблицу и не будет вычитать из неё единицы. Он спросит луг: meadow.has_grass(x, y) — и скажет: meadow.eat(x, y). Всё, что луг умеет, он делает сам, через свои методы.
Строгости окупаются: теперь луг можно переделать, не трогая кроликов. Если завтра мы решим хранить только съеденные клетки, в словаре «клетка → сколько ждать», поменяется внутренность Meadow, а кролики, которые знают только has_grass и eat, ничего не заметят. Когда объект прячет своё устройство за методами и сам следит за своими правилами, это называют инкапсуляцией.
Замков Python не ставит: meadow._wait прочитать можно. Подчёркивание — забор по пояс, через который перешагнёт любой, кто очень захочет. Зато все видят, где забор стоит. Тот же принцип работает в банке: остаток счёта меняют только методы «положить» и «снять», и они проверяют, что сумма положительна и денег хватает. Такой счёт с проверками и исключениями из главы 11 вы напишете в задаче.
Лиса: наследование
Пора заселить лис. Лиса во многом как кролик: стоит в клетке, бродит, тратит силы, умирает, когда силы кончаются. Отличается она едой и, может быть, ещё парой привычек. Можно скопировать класс Rabbit, переименовать и поправить. Но тогда ошибка, найденная в одной копии, останется в другой, а улучшение придётся вносить дважды. Лучше вынести общее в класс «зверь» и сказать, что кролик и лиса — звери со своими особенностями.
Запись class Rabbit(Animal) значит: кролик — это зверь, у него есть всё, что есть у Animal, плюс то, что мы допишем. Это наследование: Animal — родительский, или базовый, класс, Rabbit и Fox — его подклассы. У кролика пока нет ничего своего — слово pass значит «ничего не делать». Но он уже умеет всё: ходить, жить, показываться в repr. В Simula наследование записывали приставкой: Animal class Fox — «класс Fox с приставкой Animal», а сами подклассы были одной из главных новинок Simula 67.
Лиса заменяет два метода родителя своими. Когда в подклассе есть метод с тем же именем, побеждает он, а родительский можно позвать через super(): super().__init__(…) — «сделай то, что делает при рождении любой зверь», super().step() — «походи, как любой зверь». Метод __repr__ написан один раз, в Animal, но печатает имя класса самого объекта: type(self).__name__ у лисы — "Fox". А isinstance отвечает, принадлежит ли объект классу или его потомкам: лиса — экземпляр Fox и Animal, но не Rabbit.
Наследование вы уже писали, не зная этого слова. В прошлой главе строка class AlignmentError(ValueError) объявила исключение-разновидность ValueError, поэтому except ValueError его и ловил. Все исключения Python выстроены в такое же дерево: ZeroDivisionError — частный случай ArithmeticError, тот — Exception.
Чей метод вызвать, Python выясняет поиском по цепочке: сначала в самом объекте, потом в его классе, потом в родителе, потом в родителе родителя — до класса object, общего предка всех классов Python. Побеждает первое найденное.
FOOD и найти имя, которого нет нигде.Остров целиком
Все детали готовы, можно собирать остров. Остров не зверь и не трава, он их содержит: у острова есть луг и есть список зверей. «Лиса — это зверь» — наследование; «у острова есть луг» — композиция объектов. Большие программы собирают из объектов, как машину из узлов: двигатель не наследуют от автомобиля, его ставят внутрь.
Вот весь остров. Это самая длинная программа курса до сих пор, но в ней нет ничего, чего вы не видели в этой главе. Прочитайте её сверху вниз и запустите.
Зверь каждый день тратит единицу сил, делает шаг в случайную соседнюю клетку, если она не в море, и с небольшой вероятностью рожает. Детёныш получает половину сил матери, а вид его определяет строка type(self)(…): «вызови класс того, кто рожает». Поэтому метод give_birth написан один раз на всех: у кролика родится кролик, у лисы — лиса. Кролик сверх этого ест траву под собой, лиса — кролика, если он стоит в её клетке. Числа FOOD и BIRTH — атрибуты классов, свои у каждого вида: self.BIRTH в методе Animal.step найдёт у кролика 0,04, а у лисы 0,05.
В Island.step остров перемешивает зверей, чтобы никто не ходил всегда первым, и каждому говорит одно и то же: animal.step(self). Остров не знает, кто перед ним, и не спрашивает: в этом цикле нет ни одного if про вид зверя. Кролик на эти слова пойдёт есть траву, лиса — охотиться. Одна и та же строка кода делает разное в зависимости от того, какой объект её получил. Это называют полиморфизмом, «многообразием форм». Сравните с функцией step из раздела «Остров из словарей»: там цепочка if по видам росла бы с каждым новым зверем, а здесь новый зверь приносит своё поведение с собой.
Ниже — схема классов этой программы. Прямоугольник — класс: имя, атрибуты, методы. Стрелка с треугольником — «это» (наследование: лиса — это зверь), линия с ромбом — «есть» (композиция: у острова есть луг). Допишите в ячейку class Hedgehog(Animal): pass, и на схеме появится ёж.
Таскать сто с лишним строк из ячейки в ячейку незачем, поэтому мы сложили эти классы без изменений в модуль cs.island песочницы курса и добавили туда функцию draw, которая рисует остров кадр за кадром. Теперь вернитесь к первой ячейке главы: в ней не осталось ни одного незнакомого слова.
Волны
На анимации волну легко проглядеть, на графике она видна сразу. Проживём на острове тысячу дней и запишем, сколько каждый день кроликов и лис. Справа — те же числа, но по осям отложены сами популяции: каждая точка — один день, кролики по горизонтали, лисы по вертикали.
Слева видны четыре-пять волн, каждая длиной двести с лишним дней, и пик лис всякий раз запаздывает за пиком кроликов. Справа волны складываются в петли: популяции ходят по кругу против часовой стрелки — много кроликов и мало лис, много тех и других, мало кроликов и много лис, мало тех и других. Запустите ячейку несколько раз: каждый остров проживёт свою историю, а форма у историй одна.
Лисы плодятся, когда кроликов много, но с опозданием: чтобы стать многочисленными, им нужно время. Когда лис наконец много, кроликов на всех уже не хватает: лисы выели их сами. Тогда лисы голодают, и, пока их мало, кролики снова размножаются. Запаздывание хищника за жертвой и делает из погони качели.
Те же качели в 1920-х годах вывели из уравнений Альфред Лотка и Вито Вольтерра. В их модели вместо отдельных зверей — численности, непрерывные величины: скорость роста зайцев пропорциональна их числу, минус встречи с волками; скорость роста волков пропорциональна встречам, минус голодная смерть. Решения этих уравнений ходят по замкнутым петлям — точно как наш правый график, только без случайного дрожания. Как вывести модель и почему петли замкнуты, рассказывает стенд «Волки и зайцы» в главе о дифференциальных уравнениях учебника математики. Мы пришли к той же картине с другой стороны: ни одного уравнения, только правила для каждого зверя.
Ниже — тот же остров, только живёт он прямо в браузере, и опыты на нём ставятся быстро. Что, если лисы станут прожорливее? Если трава будет отрастать вдвое дольше? Есть ли настройки, при которых погибают все?
cs.island. Слева — остров, справа — популяции по дням и петля «кролики — лисы». Ползунки меняют атрибуты классов на ходу.Опыты показывают, что качели хрупкие. Слишком ловкие лисы съедают всех кроликов и вымирают следом. Слишком медленные вымирают сразу, и кролики упираются в траву. На маленьком острове вымирание случается чаще, чем на большом: когда лис остаётся пять, достаточно невезения на несколько дней. Экологи знают это и без симуляций: так исчезают виды на реальных островах.
Засуха: всё, что умеет ходить
В начале главы была обещана засуха: несколько недель солнце каждый день выжигает часть травы. Засуха не зверь, но остров многого от жителей и не требует: каждому говорит step и спрашивает is_alive. Если засуха умеет то же самое, остров примет и её.
Класс Drought ни от кого не унаследован, и остров о нём ничего не знает. Но у засухи есть step и is_alive, и этого достаточно: остров сорок дней вызывает её вместе со зверями, а потом она «умирает» и исчезает из списка. Траву она жжёт через тот же метод eat, которым пользуются кролики, — забор вокруг луга устоял. Кролики голодают первыми, лисы — следом. Бывает, что лисы не переживают засуху совсем, и тогда кролики, оправившись, плодятся без врагов до самого потолка травы. Запустите несколько раз.
В Python так устроено всё: объект судят по тому, что он умеет, а не по классу. Если нечто ходит как утка и крякает как утка, с ним можно обращаться как с уткой. Этот принцип так и называют — утиная типизация. Цикл for работает с любым объектом, который умеет выдавать элементы по одному, len — с любым, у которого есть __len__, а наш остров — с любым, у кого есть step и is_alive.
Наш остров устроен по рецепту Кэя. Звери и засуха — маленькие самостоятельные вычислители со своим состоянием, остров посылает каждому сообщение step, и каждый отвечает по-своему. Кто именно ответит, Python решает в самый момент вызова. Много лет спустя Кэй перечислил, что для него составляет суть объектов: сообщения, сокрытие состояния внутри объекта и решение «кто ответит» как можно позже. Всё это вы только что видели в работе.
Когда класс не нужен
Освоив классы, легко начать делать класс из всего. На конференции PyCon в 2012 году Джек Дидерих прочёл доклад с названием «Перестаньте писать классы». Одна из его мыслей: если у класса два метода и один из них __init__, скорее всего, вы хотели написать функцию. Класс оправдан, когда у вещи есть состояние, которое меняется, правила, за которыми надо следить, и поведение, которое меняется вместе с видом вещи, — как у наших зверей. Если ничего этого нет, есть инструменты попроще.
- Функция — когда есть действие, но нет состояния между вызовами. Перевести градусы в радианы — функция, а не класс «Конвертер».
- Кортеж или словарь — когда несколько значений нужно сложить вместе и передать дальше: строка каталога землетрясений из главы 6, запись из JSON в главе 8.
- Dataclass — когда данные хочется назвать и сравнивать, но писать
__init__,__repr__и__eq__лень.
Строка @dataclass над классом — декоратор: функция, которая получает класс и дописывает ему __init__, __repr__ и __eq__ по списку полей. Модуль dataclasses появился в Python 3.7 в 2018 году; предложил его Эрик Смит — тот же, кто придумал f-строки из главы 2. Наша клетка из раздела про равенство уложилась в четыре строки.
Задачи
Четыре задачи: от одного класса до семейства классов. Тесты создают ваши объекты, вызывают методы и проверяют состояние после каждого шага — в том числе то, что два объекта не делят один список.
Напишите класс Rabbit. Кролик создаётся по имени: Rabbit("Пушок"); у него атрибуты name, energy (сначала 10) и hops (сколько раз прыгнул, сначала 0). Методы:
eat(food)прибавляет к силамfood;hop()— прыжок: стоит одну единицу сил и увеличиваетhops. Если сил уже 0, кролик не прыгает, ничего не меняется. Метод возвращаетTrue, если прыжок был, иFalse, если нет;is_alive()— истинно, пока силы больше нуля;__repr__возвращает строку видаRabbit('Пушок', energy=10).
В __init__ повесьте на self три атрибута: self.name = name, self.energy = 10, self.hops = 0.
В hop сначала проверьте силы: if self.energy == 0: return False. Для __repr__ пригодится !r в f-строке: f"{self.name!r}" возьмёт имя в кавычки, как это делает repr.
Проверка в начале hop следит, чтобы силы не ушли в минус. Пока кролика меняют только через методы, нарушить это правило снаружи нельзя — так работает инкапсуляция.
Напишите класс Vector — вектор на плоскости, как смещение зверя за ход. Vector(3, 4) хранит атрибуты x и y. Научите векторы языку:
v + wиv - wдают новый вектор, самиvиwне меняются;v * 3и3 * v— вектор, растянутый в три раза;v == wистинно, когда координаты совпадают; сравнение с чем-то, что не вектор, —False, а не ошибка;v.length()— длина: $\sqrt{x^2 + y^2}$;repr(Vector(3, 4))— строкаVector(3, 4).
Специальные методы: __add__, __sub__, __mul__, __eq__, __repr__. Каждый арифметический возвращает новый Vector(…).
v * 3 вызывает v.__mul__(3). А 3 * v сначала спрашивает число: int.__mul__(3, v). Число не умеет умножаться на вектор, и тогда Python пробует «правое» умножение вектора: v.__rmul__(3). Напишите __rmul__, который делает то же, что __mul__.
В __eq__ сначала проверьте isinstance(other, Vector): у кортежа (3, 4) нет атрибута x, и без проверки сравнение упадёт.
math.hypot(x, y) считает $\sqrt{x^2+y^2}$ аккуратнее, чем формула напрямую: не переполняется на огромных числах. Векторы, сложение которых пишется плюсом, — пример того, как специальные методы делают ваш класс частью языка; так же устроены fractions.Fraction, datetime и массивы NumPy.
Напишите класс Account. Account("Ада") — счёт с нулевым остатком, Account("Ада", 100) — со ста. Атрибуты: owner, balance и history — список операций в виде кортежей ("deposit", 50) и ("withdraw", 30). Методы:
deposit(amount)кладёт деньги. Сумма должна быть больше нуля, иначе —ValueError;withdraw(amount)снимает. Сумма больше нуля и не больше остатка, иначе —ValueError;transfer(other, amount)переводит деньги на счётother: у себя снимает, другому кладёт.
После неудачной операции остаток и история должны остаться прежними. И у каждого счёта история своя.
В заготовке уже сидит ловушка из раздела про норы. Найдите её: создайте два счёта и положите деньги на один.
Проверяйте сумму до того, как что-то менять: if amount <= 0: raise ValueError("сумма должна быть больше нуля"). Тогда при ошибке ничего не успеет измениться.
В transfer сначала self.withdraw(amount): если денег не хватает, он бросит исключение, и до other.deposit дело не дойдёт.
Список history = [] в теле класса был один на все счета: операции Ады попадали в историю Грейс. Проверка «сначала убедись, потом меняй» делает каждую операцию неделимой: она либо проходит целиком, либо не меняет ничего. В реальных банках то же свойство обеспечивают транзакции баз данных — о них глава 46.
Постройте семейство классов. Базовый класс Shape с методами area() и perimeter(), которые бросают NotImplementedError: у фигуры вообще площади не бывает, она бывает у конкретной фигуры. И метод describe(), написанный один раз, в Shape: он возвращает строку вида "Circle: площадь 3.14" — имя класса и площадь с двумя знаками после точки.
Подклассы: Circle(r), Rect(w, h) и Square(side). Квадрат — это прямоугольник, поэтому Square наследуется от Rect и не переписывает ни area, ни perimeter. Ещё напишите функцию largest(shapes): фигура с наибольшей площадью (для пустого списка — None).
describe не знает, какая перед ним фигура, — и не должен: type(self).__name__ даст имя класса, а self.area() вызовет площадь нужного подкласса. Формат двух знаков — f"{x:.2f}".
Квадрату достаточно одного метода: def __init__(self, side): super().__init__(side, side). Всё остальное он получит от прямоугольника.
Для largest вспомните max с ключом из главы 10: max(shapes, key=lambda s: s.area()).
Метод describe и функция largest написаны один раз и работают с любыми фигурами, даже с теми, которых ещё нет: добавьте Triangle со своими area и perimeter, и больше ничего менять не придётся: полиморфизм работает на вас. А квадрат, который ничего не переписывает, показывает, чем хорошо наследование: формулы прямоугольника достались ему даром.
Куда дальше
Наш остров — 48 на 30 клеток, на нём пара сотен зверей. Экологи моделируют леса и океаны, где особей миллионы. Увеличим остров и населим его гуще. Модель та же, мы только засечём, сколько длится один день.
Зверей каждый раз вдвое больше, а день длится почти вчетверо дольше. На шестнадцати тысячах кроликов день идёт почти секунду, а тысяча дней — больше десяти минут. Сто тысяч кроликов этот остров не потянет вовсе: судя по росту, один день занял бы с полминуты. Где-то в нашем аккуратном коде прячется что-то, что растёт быстрее числа зверей. Где именно — и как предсказать время работы программы заранее, ещё до запуска, по её тексту? Похожий вопрос в 2021 году задал себе игрок, которого шесть минут загрузки одной игры довели до того, что он полез в её машинный код. С его расследования начнётся следующая глава — и вторая часть курса.