В этом году анонсирован последний выпуск Python 2.7, после чего Python Software Foundation перестанет поддерживать ветку 2.7. Множество популярных библиотек и фреймворков тоже прекращают официальную поддержку Python 2, а целый ряд дистрибутивов Linux уже не включают его в набор пакетов по умолчанию.

Конечно, 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?

  1. У него самого нет базового класса.
  2. У объектов этого класса не только нет атрибутов и методов — нет даже возможности их присвоить.
>>> 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»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.


1 комментарий

  1. Аватар

    Alexey Fedorov

    25.02.2020 в 20:50

    Простите, но о чем эта статья? Все описанные пункты прекрасно работают в python2.

Оставить мнение