В данной главе мы рассмотрим основные состовляющие объектно-ориентированного программирования и изучим их реализацию в Python.
Быстрый поиск
Лирическое отступление
При разговоре про объектно-ориентированное программирование у некоторых сразу всплывает представление о каких-то специализированных языках (С++, в частности), «удостоенных» правом и некоторым функционалом для его реализации. Справедливости ради, объектное проектирование при разработке больших проектов заключается в определении некоторых общих свойств мира, объединённых в т.н. классы, на основе которых можно выделять единичные экземпляры. Исходя из таких рассуждений следует воспринимать объекты как способ управления иерархическими системами. И отсюда же следует, что на любом языке программирования можно создать объектную модель, ровно как и на любом объектно-ориентированном языке (для которого «из коробки» достаточно простым способом можно объектное проектирование поддерживать) можно обходиться без реализации объектов.
Составляющие ООП в Python
Возвращаясь к классическому определению ООП, рассмотрим три его основные составляющие — инкапсуляцию, наследование и полиморфизм.
В качестве нулевого пункта также добавим абстракцию — выделение общих деталей, необходимых для понимания иерархии системы. Примером вертикальной абстракции служат разделения объектов на классы и их экземпляры (и, отличительно для Python, метаклассы; о них мы будем говорить в будущих главах), на базовые и производные классы и так далее. примером горизонтальной абстракции выступает моделирование различных сущностей предметной области одинаковыми структурами языка.
Инкапсуляция в большинстве языков описывается как размещение в одном объекте данных и методов, которые с ними работают, а также сокрытие этих данных и методов от посторонних. В Python инкапсуляция имеет несколько другое определение. Здесь под инкапсуляцией подразумевается минимализация необходимого информационного пространства. Основным методом достижения этого часто выступает создание вложенных пространств имён. И, несомненно, концепция с объединением полей и методов под единым объектом укладывается в данное определение. Весь Python строится на принципе инкапсуляции. В частности, Duck Typing позволяет для всех «одинаковых» методов в объектах описывать их одинаковыми именами (например, метод __add__ для операции сложения называется так независимо от того, относится он к числам, строкам или другим объектам Python).
Наследование в Python не отличается определением от других языков программирования и подразумевает ровно ту же идею с созданием новых объектов на основе некоторых уже существующих с описанием их различий.
С полиморфизмом в Python ещё проще, поскольку идея обработки одним и тем же кодом объектов разного типа волшебным образом идёт с ним из коробки за счёт Duck Typing, хотя для языков статической типизации этот принцип является одним из ключевых.
Объекты, классы, экземпляры
Исходя из особенностей динамической типизации Python, а также общей «объектности» языка, непосредственно классы и их экземпляры можно воспринимать не просто как строго выделяющиеся неприкасаемые для остального пространства сущности, а как связанные вложенные пространства имён, существующие в глобальном пространстве и обладающие всеми свойствами вложенных пространств имён.
Касаемо самой объектности, в действительности, каждый объект Python является объектом в том понимании, что у него есть своё собственное вложенное пространство имён, в котором определены атрибуты — данные и методы (и, логично, каждый из этих атрибутов также является объектом).
>>> a = 100500
>>> dir(a)
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__div
mod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getat
tribute__', '__getnewargs__', '__getstate__', '__gt__', '__hash__', '__index__', '__init__', '__init_su
bclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '
__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__
', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__r
ound__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__'
, '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'as_in
teger_ratio', 'bit_count', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'is_integer'
, 'numerator', 'real', 'to_bytes']
>>> a.numerator
100500
>>> a.denominator
1
>>>
Для определения «вызываемости» атрибутов (и разделения их на поля и методы там, где это не очевидно) существует специальная функция callable
>>> callable(a.numerator)
False
>>> callable(a.to_bytes)
True
>>> a = 123456
>>> a.to_bytes(3)
b'\x01\xe2@'
>>>
Для встроенных объектов изменение из пространства имён невозможно. Однако можно создавать свои классы (делается это с помощью ключевого слова class и последующего описания полей и методов)
>>> a.numerator = 123
Traceback (most recent call last):
File "<python-input-12>", line 1, in <module>
a.numerator = 123
^^^^^^^^^^^
AttributeError: attribute 'numerator' of 'int' objects is not writable
>>> class C:
... pass
...
>>> dir(C)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__firstlineno__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__static_attributes__', '__str__', '__subclasshook__', '__weakref__']
Как видно, даже при создании пустого класса в его пространстве имён возникает немалое количество обязательных атрибутов. Большинство из них — методы.
>>> # Большинство — методы
>>> [callable(getattr(C, c)) for c in dir(C)]
[True, True, False, True, False, True, False, True, True, True, True, True, True, True, True, True, True, False, True, True, True, True, True, True, True, False, True, True, False]
* в качестве маленького отступления хочется выделить функцию getattr (методы __getattr__ или __getattribute__, срабатывающие при этом), с помощью которой можно получать объекты по их имени из указанного пространства имён. При этом это сработает не только с классом, но и с его экземпляром (опять же, потому что экземпляр также является просто пространством имён)
>>> c = C()
>>> c.__getattribute__("__sizeof__")
<built-in method __sizeof__ of C object at 0x7fd943aa0ec0>
>>> getattr(c, '__sizeof__')
<built-in method __sizeof__ of C object at 0x7fd943aa0ec0>
>>> getattr(c, '__sizeof__')()
16
>>>
Продолжением идеи работы с классом, как с пространством имён, является возможность самостоятельно заполнять его не только при определении, но и просто в процессе работы с ним (для дальнейшей работы опишем функцию, которая из всего пространства имён будет возвращать нам только созданные нами атрибуты):
>>> def ddir(obj):
... return [c for c in dir(obj) if not c.startswith('__')]
...
>>>
>>> C
<class '__main__.C'>
>>> C.qwer = 124
>>> C._asd = "AKLSNF:AS"
>>> ddir(C)
['_asd', 'qwer']
>>>
Любой класс python обладает свойством создания объектов-экземпляров (не конструирования, причину неиспользования данного термина мы рассмотрим в главе о метаклассах). Причём поскольку экземпляр класса является своеобразным вложенным пространством имён в пространство имён класса, по правилам вложенности пространств его пространство имён определяет те атрибуты, которые заданы в «глобальном» пространстве имён класса.
>>> c = C()
>>> ddir(c)
['_asd', 'qwer']
>>> C.AAA = 42
>>> c.AAA
42
Как и для любых пространств имён, при модификации атрибутов действует принцип вложенности: изменение в глобальном пространстве класса будет отображено в локальном пространстве экземпляра. Но изменение в локальном пространстве поведёт за собой создание (или модификацию, если он уже создан) локального атрибута, при этом глобальный останется неизменным.
>>> C.AAA = 100500
>>> c.AAA
100500
>>> c.AAA = 123456
>>> C.AAA
100500
>>> ddir(C)
['AAA', '_asd', 'qwer']
>>> ddir(c)
['AAA', '_asd', 'qwer']
>>> id(c.AAA)
140571119770576
>>> id(C.AAA)
140571119770256
>>> del c.AAA
>>> c.AAA
100500
>>> C.qwer, c.qwer
(124, 124)
>>> del C.qwer
>>> C.qwer
Traceback (most recent call last):
File "<python-input-55>", line 1, in <module>
C.qwer
AttributeError: type object 'C' has no attribute 'qwer'
>>> c.qwer
Traceback (most recent call last):
File "<python-input-56>", line 1, in <module>
c.qwer
AttributeError: 'C' object has no attribute 'qwer'
>>>
Однако логика создания и модификации локальных объектов нарушается для изменяемых объектов (поскольку модификация константного объекта (например, int) на деле представляет замену одного числа на другое, логичным является для локальных объектов определять свои «модифицированные» (а на деле новые) значения без затрагивания пространства класса. В случае изменяемых объектов происходит простое обращение к объекту по ссылке, изменение того же объекта, и, как следствие, отсутствие создания локальной модифицированной копии)
>>> class D:
... dct = [1, 2, 3]
...
>>> d = D()
>>> d.dct.append(100500)
>>> D.dct
[1, 2, 3, 100500]
>>>
Вернёмся к более привычному описанию классов — непосредственно при их определении.
>>> class D:
... a = 10
... b = 20
...
>>> d = D()
>>> d.a
10
>>> d.b = 2000
>>> D.b
20
>>> d.b
2000
>>>
Кроме полей мы, очевидно, моем описывать и методы класса. При этом в классе они будут определяться обычными функциями и работать без каких-либо особенностей.
>>> class D:
... a = 10
... b = 20
... def meth(*args):
... print("@@", *args)
...
>>> ddir(D)
['a', 'b', 'meth']
>>> type(D.a)
<class 'int'>
>>> type(D.meth)
<class 'function'>
>>> D.meth(1, 2,3 ,4, 6)
@@ 1 2 3 4 6
Однако при обращении к функции из экземпляра класса она отмечается как «метод», а при вызове добавляет к аргументам дополнительный нулевой параметр — сам объект-экземпляр.
>>> d = D()
>>> type(d.a)
<class 'int'>
>>> type(d.meth)
<class 'method'>
>>> callable(d.meth)
True
>>> # А теперь вызовем и получим что-то новое: добавляется нулевой параметр с самим собой
>>> d.meth(1, 2, 3)
@@ <__main__.D object at 0x7f81a4039160> 1 2 3
>>> d
<__main__.D object at 0x7f81a4039160>
>>>
>>>
>>>
Поэтому при описании методов класса следует явно определять переменную для этого объекта. Ограничений по именованию этого параметра нет, но принято называть его self:
>>> class D:
... x = 0
... def append(self, num):
... self.x += num
...
>>> d = D()
>>> d.x
0
>>> d.append(100)
>>> d.x
100
>>> D.x
0
>>> c = D()
>>> c.x
0
>>>
>>> class Num:
... num = 0
... def add(self, el):
... self.num += el
... def __str__(self):
... return f'<{self.num}>'
...
>>> n = Num()
>>> Num
<class '__main__.Num'>
>>> n
<__main__.Num object at 0x7f81a4038d70>
>>> print(Num)
<class '__main__.Num'>
>>> print(n)
<0>
>>> n.add(12)
>>> print(n)
<12>
>>>
Как мы уже говорили, поля класса до их изменения в самом объекте являются объектными полями, и становятся экземплярными лишь при «модификации». Для описания сразу экземплярных переменных используется специальный метод-_инициализатор_ __init__ (и вновь это не конструктор; конструктор уже отработал, ведь мы как минимум, подаём объект-экземпляр в качестве параметра)
>>> class D:
... def __init__(self, val):
... self.num = val
... def __str__(self):
... return f'<{self.num}>'
...
>>> ddir(D)
[]
>>> d = D(123)
>>> d.num
123
>>> print(d)
<123>
>>>
Для всех операций с объектами существуют спецметоды, реализующие эти операции. Данные методы описываются в пространстве класса и используются экземплярами. модифицировать или заменить спецметод из экземпляра невозможно: создание его произойдёт, метод сохранится в словаре экземпляра, однако вызываться кроме как вручную не будет, поскольку поиск методов по умолчанию производится, начиная с пространства класса:
>>> class A:
... def __str__(self):
... return "QKRQ"
...
>>> a = A()
>>> print(a)
QKRQ
>>> a.__str__ = lambda self: "QQ"
>>> print(a)
QKRQ
>>> a.__dict__
{'__str__': <function <lambda> at 0x7ff7e231d120>}
>>> a.__str__
<function <lambda> at 0x7ff7e231d120>
>>> a.__str__(a)
'QQ'
>>>
С помощью функций setattr и getattr также нельзя поменять методы из экземпляра.
На функции getattr хочется остановиться подробнее: Эта функция аналогична оператору . для обращения к атрибутам объекта, и для неё также существуют реализующие методы. По умолчанию этим методом является __getattribute__:
>>> d.num
123
>>> getattr(d, "num")
123
>>> d.__getattribute__('num')
123
Однако тот же самый оператор может быть реализован с помощью метода __getattr__:
>>> class D:
... count = 0
... def __getattr__(self, attr):
... self.count += 1
... return self.count
...
>>> d = D()
>>> d.wer
1
>>> d.qwerty
2
>>> d.count
2
>>>
Заметим одну особенность: при обращении к несуществующему полю производилось увеличению счётчика. При обращении к существующему count оно просто вывелось. Дело в том, что __getattr__ работает исключительно с именами, которых строго нет в пространстве имён, для всех остальных вызывается __getattribute__. Его также можно перегрузить, однако это может вызвать неконтролируемую рекурсию, поскольку при неаккуратном описании тела функции перегруженный __getattribute__ будет вызывать сам себя:
>>> class D:
... count = 0
... def __getattribute__(self, attr):
... self.count += 1
... return self.count
...
>>> d = D()
>>> d.wer
Traceback (most recent call last):
File "<python-input-74>", line 1, in <module>
d.wer
File "<python-input-72>", line 4, in __getattribute__
self.count += 1
^^^^^^^^^^
File "<python-input-72>", line 4, in __getattribute__
self.count += 1
^^^^^^^^^^
File "<python-input-72>", line 4, in __getattribute__
self.count += 1
^^^^^^^^^^
[Previous line repeated 988 more times]
RecursionError: maximum recursion depth exceeded
Спецметодов в Python, действительно, огромное количество. Кратко обсудим лишь некоторые из них:
__str__и__repr__выступают для строкового представления объекта (print) и строкового описания объекта (обращение к объекту в интерпретаторе, например) соответственно;__len__используется для определения «длины» объекта;__abs__аналогично для определения «модуля» объекта;__bool__— проверка на «пустоту объекта»;__iter__— создание итератора из объекта;- Группы методов для реализации протоколов: числового, последовательности, вызова;
__del__— метод, срабатывающий не при непосредственно удалении, а при зачистке объекта из памяти garbage collector-ом.
>>> D.__str__ = lambda x: "QQ"
>>> D.__repr__ = lambda x: "WW"
>>> e = D()
>>> e
WW
>>> print(e)
QQ
>>> class C:
... def __len__(self):
... return 42
...
>>> len(C())
42
>>> class C:
... def __abs__(self):
... return int(a > b)
...
>>> c = C()
>>> a, b = 1, 0
>>> abs(c)
1
>>> a, b = 0, 1
>>> abs(c)
0
>>> class C:
... def __bool__(self):
... return True if self.a > 0 else False
...
>>> c = C()
>>> c.a = -4
>>> if c:
... print('QQ')
... else:
... print('neQQ')
...
neQQ
>>>
>>> class C:
... def __iter__(self):
... return iter("qwerty")
...
>>> list(C())
['q', 'w', 'e', 'r', 't', 'y']
>>>
>>> class C:
... def __getitem__(self, idx):
... return ('Good!')[idx]
... def __call__(self):
... return ''.join(self[i] for i in range(1, 4))
...
>>> c = C()
>>> c[0]
'G'
>>> c[4]
'!'
>>> c()
'ood'
>>>