21
Содержание статьи¶
- Иерархия классов и
object
- Инкапсуляция и «частные» атрибуты
- Проверка соответствия типов
- О регистре букв и слове self
- Статические методы и методы класса
- Изменяемость полей класса
- Множественное наследование
- Заключение
В этом году анонсирован последний выпуск 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
?
- У него самого нет базового класса.
- У объектов этого класса не только нет атрибутов и методов — нет даже возможности их присвоить.
>>> 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
.
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
и всех его потомков.
>>> 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.
Эти два определения класса эквивалентны:
class MyClass:
def __init__(self, x):
self.x = x
class myclass:
def __init__(this, x):
this.x = x
Важно только то, что первый аргумент метода — это всегда ссылка на вызывающий его объект.
Статические методы и методы класса¶
Этим метод объекта и отличается от просто функции — при вызове ему неявно передается ссылка на какой-то объект в качестве первого аргумента. На какой именно объект? По умолчанию — на экземпляр, которому принадлежит метод. С помощью декораторов @classmethod
и @staticmethod
это поведение можно изменить.
Декоратор @classmethod
передает в метод ссылку на сам класс, а не на его конкретный экземпляр. Это дает возможность вызывать метод без создания объекта. А вот @staticmethod
, по сути, и не метод вовсе, а просто функция, никакой неявной передачи ссылок таким методам не происходит.
Посмотрим на пример. Обрати внимание: x
— поле класса, а не объектов-экземпляров.
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__
, вроде такого:
class MyClass:
def __init__(self):
self.x = 10
self.hello = "hello world"
Почему так? Дело в том, что классы сами относятся к объектам класса type
и их поля так же изменяемы, как поля всех остальных объектов.
INFO¶
Теперь ты знаешь, как проверить, что пользователь передал именно класс в твою абстрактную фабрику абстрактных фабрик или еще какую-то функцию для создания объектов произвольного класса. Совершенно верно, isinstance(c, type)
.
В момент создания объекты содержат ссылки на все поля и методы своего класса. Если кто-то случайно модифицирует поле самого класса (в стиле MyClass.x = y
), это автоматически изменит поле x
для всех уже созданных объектов этого класса.
class MyClass:
x = 10
o = MyClass()
>>> o.x
10
>>> MyClass.x = 70
>>> o.x
70
Если кто-то модифицирует поле в самом объекте, изменения в классе перестанут его затрагивать, поскольку поле потеряет связь с классом и станет ссылкой на новое значение.
>>> o.x = 80
>>> MyClass.x = 100
>>> o.x
80
Если инициализировать переменные в __init__
, это будет происходить каждый раз, когда кто-то создает новый объект, и тогда эти переменные будут действительно локальными для конкретного объекта (instance variables).
Множественное наследование¶
Получить прямой доступ к информации о предках класса можно с помощью поля __bases__
. В рабочем коде лучше так не делать, а обратиться к isinstance()
, но ради интереса можно попробовать.
>>> import collections
>>> collections.OrderedDict.__bases__
(<class 'dict'>,)
>>> dict.__bases__
(<class 'object'>,)
Ты видишь, там находится не одно значение, а кортеж (tuple), что намекает нам на возможность иметь более одного родителя. В Python на самом деле есть множественное наследование. Применять его или не применять — вопрос сложный (и в большинстве проектов решенный в пользу «не применять»). При наличии удобного синтаксиса для декораторов можно добавить новое поведение в класс или метод куда проще.
Тем не менее возможность есть и порядок поиска методов (method lookup) достаточно логичный: от первого указанного предка к последнему.
Для примера создадим два базовых класса и двух потомков, которые наследуются от них в разном порядке:
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()
— вызов отправится к первому предку в списке:
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
Заключение¶
Даже небольшие приложения будет проще поддерживать и расширять, если уделить внимание взаимоотношениям между кодом и данными. Теперь ты знаешь детали поведения объектов — надеюсь, это тебе поможет.