Сегодня мы сфокусируемся на объектной модели. Синтаксис создания классов и объектов в Python достаточно очевиден, а вот детали поведения — не всегда. Для опытных пользователей эта информация общеизвестна, но у тех, кто переходит от написания коротких скриптов и использования чужих классов к созданию своих приложений, порой вызывает сложности.
Иерархия классов и object
в Python 3
Начнем с самого простого случая — с классов, у которых нет явно указанного предка.
В Python 3 у любого пользовательского класса есть как минимум один базовый класс. В корне иерархии классов находится встроенный класс object
— предок всех классов.
В учебных материалах и коде часто можно видеть такую конструкцию:
1 2 |
class MyClass(object): pass |
В Python 3 она избыточна, поскольку object
— базовый класс по умолчанию. Можно смело писать так:
1 2 |
class MyClass: pass |
Популярность явного синтаксиса в коде на Python 3 связана с существовавшей долгое время необходимостью поддерживать обе ветки.
В Python 2.7 синтаксис MyClass(object)
был нужен, чтобы отличать «новые» классы от режима совместимости с доисторическими версиями. В Python 3 никакого режима совместимости с наследием старых версий просто не существует, поэтому наконец можно вернуться к более короткому старому синтаксису.
Что особенного в классе object
?
- У него самого нет базового класса.
- У объектов этого класса не только нет атрибутов и методов — нет даже возможности их присвоить.
12345>>> o = object()>>> o.my_attribute = NoneTraceback (most recent call last):File "<stdin>", line 1, in <module>AttributeError: 'object' object has no attribute 'my_attribute'
«Техническая» причина этому — отсутствие у object
поля __dict__
, в котором хранятся все поля и методы класса.
Инкапсуляция и «частные» атрибуты
Попытка изменить значение поля у объекта класса object
— это единственный случай, когда нечаянное или намеренное изменение атрибута уже созданного объекта завершится с ошибкой.
Во всех остальных случаях можно поменять любые атрибуты объекта и никаких ошибок это не вызовет — ошибки возникнут потом, когда в другом месте кода кто-то обратится к модифицированным полям.
1 2 3 4 5 6 7 8 9 10 |
>>> 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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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 |
Как видишь, это все еще достаточно мягкое ограничение доступа — просто настойчивая просьба не использовать частные поля в обход публичного интерфейса объекта.
Переименование не применяется к полям и методам с подчеркиванием с двух сторон вроде
__init__
. По соглашению такие имена дают «магическим методам», на которых построены все внутренние интерфейсы стандартной библиотеки Python: к примеру,o = MyClass()
— это эквивалентo = MyClass.__new__(MyClass)
. Такие методы, очевидно, должны быть доступны извне объекта под исходными именами.
Сэкономить время на создание публичного интерфейса к частным полям можно с помощью встроенного декоратора @property
. Он создает поле, которое выглядит как переменная только для чтения — попытка присвоить значение вызовет исключение AttributeError
. Для примера создадим класс с логическим значением _boolean_property
, которое можно поменять только методом set_property
, отклоняющим значения всех типов, кроме bool
.
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 27 28 29 |
class MyClass: def __init__(self): self._boolean_property = True @property def my_property(self): return self._boolean_property def set_property(self, value): if type(value) == bool: self._boolean_property = value else: raise ValueError("Property value must be a bool") >>> o = MyClass() >>> o.my_property True >>> o.my_property = False Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: cant set attribute >>> o.set_property(9) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 11, in set_property ValueError: Property value must be a bool >>> o.set_property(False) >>> o.my_property False |
Функцию 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
и всех его потомков.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
>>> class Foo: ... pass ... >>> class Bar(Foo): ... pass ... >>> x = Foo() >>> y = Bar() >>> isinstance(x, Foo) True >>> isinstance(y, Foo) True >>> type(y) == Foo False |
Такая проверка оправданна в функциях, которые работают с объектами заранее известных классов из твоих же модулей.
А вот в библиотеке для широкого круга пользователей даже isinstance()
может быть слишком жестким ограничением. Почти все встроенные возможности Python основаны на соглашениях о «магических методах», включая арифметические операторы, итерацию, контексты (with ... as ...
) и многое другое. Сторонний разработчик вполне может создать свой контейнер, который можно использовать наравне со встроенными list
и dict
, или, к примеру, запускать и завершать работу демонов через интерфейс контекста.
Поэтому, если твой код делает предположения только об интерфейсе объектов, но не о деталях их поведения, лучше всего не проверять их тип, а отлавливать возникшие при вызове метода исключения и выдавать ошибки в стиле Object is not iterable.
О регистре букв и слове self
На телефонном собеседовании в одной крупной компании меня однажды спросили, какое ключевое слово обязательно использовать как первый аргумент метода в Python. Я сначала даже не понял смысл вопроса — такого обязательного ключевого слова не существует. Дело в том, что многие соглашения в Python так сильны, что их соблюдают, даже если это не обязательно.
В стандартной библиотеке Python и сообществе его пользователей повелось, что названия классов начинаются с заглавной буквы, а ссылку на сам объект именуют self
. Однако ни то, ни другое не является элементом синтаксиса языка, в отличие от this
в Java.
Эти два определения класса эквивалентны:
1 2 3 |
class MyClass: def __init__(self, x): self.x = x |
1 2 3 |
class myclass: def __init__(this, x): this.x = x |
Важно только то, что первый аргумент метода — это всегда ссылка на вызывающий его объект.
Статические методы и методы класса
Этим метод объекта и отличается от просто функции — при вызове ему неявно передается ссылка на какой-то объект в качестве первого аргумента. На какой именно объект? По умолчанию — на экземпляр, которому принадлежит метод. С помощью декораторов @classmethod
и @staticmethod
это поведение можно изменить.
Декоратор @classmethod
передает в метод ссылку на сам класс, а не на его конкретный экземпляр. Это дает возможность вызывать метод без создания объекта. А вот @staticmethod
, по сути, и не метод вовсе, а просто функция, никакой неявной передачи ссылок таким методам не происходит.
Посмотрим на пример. Обрати внимание: x
— поле класса, а не объектов-экземпляров.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class MyClass: x = 10 @staticmethod def foo(): print(x) @classmethod def bar(self): print(self.x) def quux(self): print(self.x) >>> o = MyClass() >>> o.x = 30 >>> o.foo() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 5, in foo NameError: name 'x' is not defined >>> o.bar() 10 >>> o.quux() 30 |
С помощью o.x = 30
мы меняем значение поля x
в объекте o
. Как видишь, метод класса bar
выводит исходное значение, потому что self
для него — это сам класс MyClass
. Для quux
переменная self
содержит ссылку именно на экземпляр o
, поэтому метод выводит поле самого объекта.
Метод foo
вовсе не работает, если во внешней области видимости нет переменной x
. Если есть, то выводит ее. С помощью @staticmethod
можно создать только такие методы, которым доступ к полям класса не нужен вообще, хотя зачем это может понадобиться в языке с модулями и пространствами имен — вопрос сложный.
Изменяемость полей класса
В прошлом примере мы ввели поле класса x
. В большинстве случаев вся инициализация переменных делается в методе __init__
, вроде такого:
1 2 3 4 |
class MyClass: def __init__(self): self.x = 10 self.hello = "hello world" |
Почему так? Дело в том, что классы сами относятся к объектам класса type
и их поля так же изменяемы, как поля всех остальных объектов.
Теперь ты знаешь, как проверить, что пользователь передал именно класс в твою абстрактную фабрику абстрактных фабрик или еще какую-то функцию для создания объектов произвольного класса. Совершенно верно,
isinstance(c, type)
.
В момент создания объекты содержат ссылки на все поля и методы своего класса. Если кто-то случайно модифицирует поле самого класса (в стиле MyClass.x = y
), это автоматически изменит поле x
для всех уже созданных объектов этого класса.
1 2 3 4 5 6 7 8 9 10 |
class MyClass: x = 10 o = MyClass() >>> o.x 10 >>> MyClass.x = 70 >>> o.x 70 |
Если кто-то модифицирует поле в самом объекте, изменения в классе перестанут его затрагивать, поскольку поле потеряет связь с классом и станет ссылкой на новое значение.
1 2 3 4 |
>>> o.x = 80 >>> MyClass.x = 100 >>> o.x 80 |
Если инициализировать переменные в __init__
, это будет происходить каждый раз, когда кто-то создает новый объект, и тогда эти переменные будут действительно локальными для конкретного объекта (instance variables).
Множественное наследование
Получить прямой доступ к информации о предках класса можно с помощью поля __bases__
. В рабочем коде лучше так не делать, а обратиться к isinstance()
, но ради интереса можно попробовать.
1 2 3 4 5 6 |
>>> import collections >>> collections.OrderedDict.__bases__ (<class 'dict'>,) >>> dict.__bases__ (<class 'object'>,) |
Ты видишь, там находится не одно значение, а кортеж (tuple), что намекает нам на возможность иметь более одного родителя. В Python на самом деле есть множественное наследование. Применять его или не применять — вопрос сложный (и в большинстве проектов решенный в пользу «не применять»). При наличии удобного синтаксиса для декораторов можно добавить новое поведение в класс или метод куда проще.
Тем не менее возможность есть и порядок поиска методов (method lookup) достаточно логичный: от первого указанного предка к последнему.
Для примера создадим два базовых класса и двух потомков, которые наследуются от них в разном порядке:
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 |
class Foo: def hello(self): print("hello world") class Bar: def hello(self): print("hi world") class Baz(Foo, Bar): pass class Quux(Bar, Foo): pass x = Baz() y = Quux() >>> x.hello() hello world >>> y.hello() hi world >> Baz.__bases__ (<class '__main__.Foo'>, <class '__main__.Bar'>) >>> Quux.__bases__ (<class '__main__.Bar'>, <class '__main__.Foo'>) |
То же самое относится к вызовам методов предка через super()
— вызов отправится к первому предку в списке:
1 2 3 4 5 6 7 8 9 10 |
class Xyzzy(Foo, Bar): def hello(self): super().hello() print("This is what my parent does") x = Xyzzy() >>> x.hello() hello world This is what my parent does |
РЕКОМЕНДУЕМ:
Как написать базу данных на Python
Заключение
Даже небольшие приложения будет проще поддерживать и расширять, если уделить внимание взаимоотношениям между кодом и данными. Теперь ты знаешь детали поведения объектов — надеюсь, это тебе поможет.