Содержание статьи
Конечно, Python 2 не исчез из реальности, как только питоночасы пробили 00:00, но он уже стал темным прошлым, а не суровым настоящим. Начинающие могут смело знакомиться с Python 3 без оглядки на 2.7, что сильно упрощает жизнь.
Популярность Python привела к тому, что многие люди изучают его неформально: по чужим примерам и статьям в блогах. В таком методе нет ничего плохого, но есть риск упустить важные особенности поведения.
Сегодня мы сфокусируемся на объектной модели. Синтаксис создания классов и объектов в Python достаточно очевиден, а вот детали поведения — не всегда. Для опытных пользователей эта информация общеизвестна, но у тех, кто переходит от написания коротких скриптов и использования чужих классов к созданию своих приложений, порой вызывает сложности.
Иерархия классов и object
Начнем с самого простого случая — с классов, у которых нет явно указанного предка.
В Python 3 у любого пользовательского класса есть как минимум один базовый класс. В корне иерархии классов находится встроенный класс object
— предок всех классов.
В учебных материалах и коде часто можно видеть такую конструкцию:
class MyClass(object):
pass
В Python 3 она избыточна, поскольку object
— базовый класс по умолчанию. Можно смело писать так:
class MyClass:
pass
Популярность явного синтаксиса в коде на Python 3 связана с существовавшей долгое время необходимостью поддерживать обе ветки.
В Python 2.7 синтаксис MyClass(object)
был нужен, чтобы отличать «новые» классы от режима совместимости с доисторическими версиями. В Python 3 никакого режима совместимости с наследием старых версий просто не существует, поэтому наконец можно вернуться к более короткому старому синтаксису.
Что особенного в классе object
?
- У него самого нет базового класса.
- У объектов этого класса не только нет атрибутов и методов — нет даже возможности их присвоить.
>>> o = object()
>>> o.my_attribute = None
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'object' object has no attribute 'my_attribute'
«Техническая» причина этому — отсутствие у object
поля __dict__
, в котором хранятся все поля и методы класса.
Инкапсуляция и «частные» атрибуты
Попытка изменить значение поля у объекта класса object
— это единственный случай, когда нечаянное или намеренное изменение атрибута уже созданного объекта завершится с ошибкой.
Во всех остальных случаях можно поменять любые атрибуты объекта и никаких ошибок это не вызовет — ошибки возникнут потом, когда в другом месте кода кто-то обратится к модифицированным полям.
>>> class MyClass:
... def my_method(self):
... print("I'm a method")
...
>>> o = MyClass()
>>> o.my_method = None
>>> o.my_method()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'NoneType' object is not callable
Можно также добавить в уже созданный объект произвольные поля — класс объекта определяет, какие поля и методы у него будут сразу после создания, но никак не ограничивает, какие поля у него могут быть во время выполнения.
Казалось бы, инкапсуляция — один из трех фундаментальных принципов ООП, наравне с наследованием и полиморфизмом. Главное правило инкапсуляции — не давать пользователю объекта вносить в него не предусмотренные автором изменения. Однако даже без доступа к исходному коду достаточно упертый пользователь найдет способ модифицировать что угодно. Дух этого правила в другом: однозначно дать понять пользователю, где заканчивается стабильный публичный интерфейс и начинаются детали реализации, которые автор может поменять в любой момент.
Для этого в Python есть один встроенный механизм. Те поля и методы, которые не входят в публичный интерфейс, называют с подчеркиванием перед именем: _foo
, _bar
. Никакого влияния на работу кода это не оказывает, это просто просьба не использовать такие поля бездумно.
Для создания частных (private) атрибутов применяются два подчеркивания (__foo
, __bar
). Такие поля будут видны изнутри объекта под своими исходными именами, но вне объекта к ним применяется name mangling — переименования в стиле _MyClass__my_attribute
:
class MyClass:
x = 0
_x = 1
__x = 2
def print_x(self):
print(self.__x)
>>> o = MyClass()
>>> o.print_x()
2
>>> o.x
0
>>> o._x
1
>>> o.__x
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'MyClass' object has no attribute '__x'
>>> o._MyClass__x
2
Как видишь, это все еще достаточно мягкое ограничение доступа — просто настойчивая просьба не использовать частные поля в обход публичного интерфейса объекта.
INFO
Переименование не применяется к полям и методам с подчеркиванием с двух сторон вроде __init__
. По соглашению такие имена дают «магическим методам», на которых построены все внутренние интерфейсы стандартной библиотеки Python: к примеру, o = MyClass()
— это эквивалент o = MyClass.__new__(MyClass)
. Такие методы, очевидно, должны быть доступны извне объекта под исходными именами.
Сэкономить время на создание публичного интерфейса к частным полям можно с помощью встроенного декоратора @property
. Он создает поле, которое выглядит как переменная только для чтения — попытка присвоить значение вызовет исключение AttributeError
. Для примера создадим класс с логическим значением _boolean_property
, которое можно поменять только методом set_property
, отклоняющим значения всех типов, кроме bool
.
Продолжение доступно только участникам
Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вариант 2. Открой один материал
Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»