Объектная модель в Python 3

Python

Сегодня мы сфокусируемся на объектной модели. Синтаксис создания классов и объектов в Python достаточно очевиден, а вот детали поведения — не всегда. Для опытных пользователей эта информация общеизвестна, но у тех, кто переходит от написания коротких скриптов и использования чужих классов к созданию своих приложений, порой вызывает сложности.

Иерархия классов и [crayon-67079dc4e9539197637657-i/] в Python 3

Начнем с самого простого случая — с классов, у которых нет явно указанного предка.

В Python 3 у любого пользовательского класса есть как минимум один базовый класс. В корне иерархии классов находится встроенный класс object — предок всех классов.

В учебных материалах и коде часто можно видеть такую конструкцию:

В Python 3 она избыточна, поскольку object — базовый класс по умолчанию. Можно смело писать так:

Популярность явного синтаксиса в коде на Python 3 связана с существовавшей долгое время необходимостью поддерживать обе ветки.

В Python 2.7 синтаксис MyClass(object) был нужен, чтобы отличать «новые» классы от режима совместимости с доисторическими версиями. В Python 3 никакого режима совместимости с наследием старых версий просто не существует, поэтому наконец можно вернуться к более короткому старому синтаксису.

Что особенного в классе object?

  1. У него самого нет базового класса.
  2. У объектов этого класса не только нет атрибутов и методов — нет даже возможности их присвоить.

«Техническая» причина этому — отсутствие у object поля __dict__, в котором хранятся все поля и методы класса.

Инкапсуляция и «частные» атрибуты

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

Во всех остальных случаях можно поменять любые атрибуты объекта и никаких ошибок это не вызовет — ошибки возникнут потом, когда в другом месте кода кто-то обратится к модифицированным полям.

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

Казалось бы, инкапсуляция — один из трех фундаментальных принципов ООП, наравне с наследованием и полиморфизмом. Главное правило инкапсуляции — не давать пользователю объекта вносить в него не предусмотренные автором изменения. Однако даже без доступа к исходному коду достаточно упертый пользователь найдет способ модифицировать что угодно. Дух этого правила в другом: однозначно дать понять пользователю, где заканчивается стабильный публичный интерфейс и начинаются детали реализации, которые автор может поменять в любой момент.

Для этого в Python есть один встроенный механизм. Те поля и методы, которые не входят в публичный интерфейс, называют с подчеркиванием перед именем: _foo, _bar. Никакого влияния на работу кода это не оказывает, это просто просьба не использовать такие поля бездумно.

Для создания частных (private) атрибутов применяются два подчеркивания ( __foo, __bar). Такие поля будут видны изнутри объекта под своими исходными именами, но вне объекта к ним применяется name mangling — переименования в стиле _MyClass__my_attribute:

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

Переименование не применяется к полям и методам с подчеркиванием с двух сторон вроде __init__. По соглашению такие имена дают «магическим методам», на которых построены все внутренние интерфейсы стандартной библиотеки Python: к примеру, o = MyClass() — это эквивалент o = MyClass.__new__(MyClass). Такие методы, очевидно, должны быть доступны извне объекта под исходными именами.

Сэкономить время на создание публичного интерфейса к частным полям можно с помощью встроенного декоратора @property. Он создает поле, которое выглядит как переменная только для чтения — попытка присвоить значение вызовет исключение AttributeError. Для примера создадим класс с логическим значением _boolean_property, которое можно поменять только методом set_property, отклоняющим значения всех типов, кроме bool.

Функцию set_property можно было завернуть в декоратор @my_property.setter, в этом случае присваивание o.my_property = False работало бы, а o.my_property = 9 вызывало ValueError.

Проверка соответствия типов

В предыдущем примере мы явно проверяем, что тип значения — bool, с помощью type(value) == bool. В случае с bool это оправданно, поскольку никакой другой тип не может его заменить в логических выражениях, а наследоваться от него Python нам не даст.

В общем случае проверять type() — плохая идея, поскольку не учитывает ни возможность наследования, ни возможность создать свои классы с совместимым интерфейсом.

Для проверки совместимости с неким базовым классом нужно использовать isinstance(object, class). Эта проверка истинна для класса class и всех его потомков.

Такая проверка оправданна в функциях, которые работают с объектами заранее известных классов из твоих же модулей.

А вот в библиотеке для широкого круга пользователей даже isinstance() может быть слишком жестким ограничением. Почти все встроенные возможности Python основаны на соглашениях о «магических методах», включая арифметические операторы, итерацию, контексты ( with ... as ...) и многое другое. Сторонний разработчик вполне может создать свой контейнер, который можно использовать наравне со встроенными list и dict, или, к примеру, запускать и завершать работу демонов через интерфейс контекста.

Поэтому, если твой код делает предположения только об интерфейсе объектов, но не о деталях их поведения, лучше всего не проверять их тип, а отлавливать возникшие при вызове метода исключения и выдавать ошибки в стиле Object is not iterable.

О регистре букв и слове self

На телефонном собеседовании в одной крупной компании меня однажды спросили, какое ключевое слово обязательно использовать как первый аргумент метода в Python. Я сначала даже не понял смысл вопроса — такого обязательного ключевого слова не существует. Дело в том, что многие соглашения в Python так сильны, что их соблюдают, даже если это не обязательно.

В стандартной библиотеке Python и сообществе его пользователей повелось, что названия классов начинаются с заглавной буквы, а ссылку на сам объект именуют self. Однако ни то, ни другое не является элементом синтаксиса языка, в отличие от this в Java.

Эти два определения класса эквивалентны:

Важно только то, что первый аргумент метода — это всегда ссылка на вызывающий его объект.

Статические методы и методы класса

Этим метод объекта и отличается от просто функции — при вызове ему неявно передается ссылка на какой-то объект в качестве первого аргумента. На какой именно объект? По умолчанию — на экземпляр, которому принадлежит метод. С помощью декораторов @classmethod и @staticmethod это поведение можно изменить.

Декоратор @classmethod передает в метод ссылку на сам класс, а не на его конкретный экземпляр. Это дает возможность вызывать метод без создания объекта. А вот @staticmethod, по сути, и не метод вовсе, а просто функция, никакой неявной передачи ссылок таким методам не происходит.

Посмотрим на пример. Обрати внимание: x — поле класса, а не объектов-экземпляров.

С помощью o.x = 30 мы меняем значение поля x в объекте o. Как видишь, метод класса bar выводит исходное значение, потому что self для него — это сам класс MyClass. Для quux переменная self содержит ссылку именно на экземпляр o, поэтому метод выводит поле самого объекта.

Метод foo вовсе не работает, если во внешней области видимости нет переменной x. Если есть, то выводит ее. С помощью @staticmethod можно создать только такие методы, которым доступ к полям класса не нужен вообще, хотя зачем это может понадобиться в языке с модулями и пространствами имен — вопрос сложный.

Изменяемость полей класса

В прошлом примере мы ввели поле класса x. В большинстве случаев вся инициализация переменных делается в методе __init__, вроде такого:

Почему так? Дело в том, что классы сами относятся к объектам класса type и их поля так же изменяемы, как поля всех остальных объектов.

Теперь ты знаешь, как проверить, что пользователь передал именно класс в твою абстрактную фабрику абстрактных фабрик или еще какую-то функцию для создания объектов произвольного класса. Совершенно верно, isinstance(c, type).

В момент создания объекты содержат ссылки на все поля и методы своего класса. Если кто-то случайно модифицирует поле самого класса (в стиле MyClass.x = y), это автоматически изменит поле x для всех уже созданных объектов этого класса.

Если кто-то модифицирует поле в самом объекте, изменения в классе перестанут его затрагивать, поскольку поле потеряет связь с классом и станет ссылкой на новое значение.

Если инициализировать переменные в __init__, это будет происходить каждый раз, когда кто-то создает новый объект, и тогда эти переменные будут действительно локальными для конкретного объекта (instance variables).

Множественное наследование

Получить прямой доступ к информации о предках класса можно с помощью поля __bases__. В рабочем коде лучше так не делать, а обратиться к isinstance(), но ради интереса можно попробовать.

Ты видишь, там находится не одно значение, а кортеж (tuple), что намекает нам на возможность иметь более одного родителя. В Python на самом деле есть множественное наследование. Применять его или не применять — вопрос сложный (и в большинстве проектов решенный в пользу «не применять»). При наличии удобного синтаксиса для декораторов можно добавить новое поведение в класс или метод куда проще.

Тем не менее возможность есть и порядок поиска методов (method lookup) достаточно логичный: от первого указанного предка к последнему.

Для примера создадим два базовых класса и двух потомков, которые наследуются от них в разном порядке:

То же самое относится к вызовам методов предка через super() — вызов отправится к первому предку в списке:

РЕКОМЕНДУЕМ:
Как написать базу данных на Python

Заключение

Даже небольшие приложения будет проще поддерживать и расширять, если уделить внимание взаимоотношениям между кодом и данными. Теперь ты знаешь детали поведения объектов — надеюсь, это тебе поможет.

Понравилась статья? Поделиться с друзьями:
Добавить комментарий