ООП
Классы и объекты в Python
Класс – это способ описания сущности, определяющий состояние и поведение, зависящее от этого состояния, а также правила для взаимодействия с данной сущностью (интерфейс).
Классы и объекты в Python
Объект (экземпляр) – это отдельный представитель класса, имеющий конкретное состояние и поведение, полностью определяемое классом.
Говоря простым языком, объект имеет конкретные значения атрибутов и методы, работающие с этими значениями на основе правил, заданных в классе.
Классы и объекты в Python
Класс – это описание еще не созданного объекта. Своеобразный шаблон или чертеж, по которому будет строиться экземпляр класса
Объект - то, что инициализировано по описанию из класса
4
class MyClass:
def __init__(self, param: int):
self.param = param
def increment(self):
return self.param + 1
myclass = MyClass(123)
myclass.increment()
ООП
ООП - это идея/способ/парадигма/стиль как можно писать код.
Идею ООП можно описать так:
Соответственно, чтобы эффективно использовать ООП следует хорошо выделять эти объекты и их взаимодействия.
Зачем оно надо
Представьте себе, что вы проектируете автомобиль.
Вы уверены в своих знаниях и начинаете проектирование.
Зачем оно надо
Можно увидеть, что:
Получаете вашу “архитектуру”.
Концепции ООП позволяют элегантно решить проблему написания такой “архитектуры”
Принципы ООП. Полиморфизм
Полиморфизм - исполнение разного кода одинаковыми вызовами
Примеры полиморфизма:
В питоне полиморфизм везде из-за утиной типизации
8
Принципы ООП. Инкапсуляция
Инкапсуляция - объединение набора данных в один компонент и предоставление единого интерфейса для обращения к данным и их изменения.
Мы предполагаем, что пользователь не будет менять некоторые поля и умышленно скрываем их.
В python это можно сделать при помощи нижнего подчеркивания.
9
class PasswordKeeper:
def __init__(self) -> None:
self._password = None
def check_password(self):
...
def set_password(self, new_pass):
if self.check_password(new_pass):
self._password = new_pass
else:
raise ValueError("Плохой пароль")
Принципы ООП. Инкапсуляция
В python нет механизмов сокрытия данных. Все атрибуты классов можно прочитать и изменить. Но это не отменяет инкапсуляцию.
10
class Hidden:
__hidden = 10
hidden_obj = Hidden()
print(hidden_obj.__hidden) # AttributeError
print(hidden_obj._Hidden__hidden) # OK
Принципы ООП. Наследование
Наследование - передача всех свойств класса наследнику (субклассу)
Наследование довольно часто усложняет код, заставляет строить сложные иерархии объектов. Чаще всего наследованию предпочитают композицию.
11
class A:
var = 1
def print(self):
print(self.var)
class B(A):
def increment(self):
self.var += 1
b = B()
b.increment()
b.print() # 2
Принципы ООП. Абстракция
Абстра́кция в объектно-ориентированном программировании — это использование только тех характеристик объекта, которые с достаточной точностью представляют его в данной системе.
Основная идея состоит в том, чтобы представить объект минимальным набором полей и методов и при этом с достаточной точностью для решаемой задачи.
По мне так это понятие присуще в общем программированию.
Вернемся ближе к Python
13
Закон протекающих абстракций
Все нетривиальные абстракции до определенного предела протекают.
Этот закон утверждает, что абстракции, которые обычно используются в IT для упрощения работы со сложными системами, в определённых ситуациях дают «протечку», пропуская наверх элементы лежащих в их основе систем, из-за чего абстракция начинает вести себя непредсказуемо.
14
Дандеры
Дандер - double underscore, способ именования переменных в питоне. В питоне нет private атрибутов, поэтому все работает на уровне договоренностей
_var - скрытая переменная, состояние которой должен менять только сам объект
var_ - когда мы не хотим перезаписывать builtin функцию (напр. list, class)
__var - скрытый атрибут, который не перейдет в наследника класса
__var__ - специальные магические методы, например __str__, __add__ и т.д.
15
Магические методы
__new__ - метод, который вызывается при создании объекта
__init__ - метод, который вызывается сразу после new для инициализации объекта
16
class A:
def __init__(self) -> None:
return 1
A() # TypeError __init__() should return None, not 'int'
class A:
def __new__(cls) -> A:
return 1
def __init__(self) -> None:
return 1
A() # 1
Магические методы
17
class A:
def __new__(cls) -> A:
return super().__new__(cls)
def __init__(self) -> None:
pass
A()
Жизненный цикл объекта
18
__new__
__init__
__del__
Атрибуты класса и экземпляра в Python
class Phone:
# Статические поля (переменные класса), обращаться через Phone.var
default_color = 'Grey'
default_model = 'C385'
var = 42
def __init__(self, color, model):
# Динамические поля (переменные объекта), обращаться через инстанс
self.color = color
self.model = model
19
Статические методы
Статические методы - методы класса, которые не принимают аргумент self и могут вызываться без создания объекта класса
20
class C:
@staticmethod
def f(arg1, arg2, ...): ...
def regular_function():
...
class C:
method = staticmethod(regular_function)
Метод класса
Статический метод, которому первым аргументом передают класс объекта и ожидается, что вернется новый объект этого класса. По сути, это фабрика на уровне языка
21
class Car:
@classmethod
def create(cls, brand):
if brand == 'Audi':
return AudiCar()
return cls()
class AudiCar: pass
print(Car.create('Audi'))
print(Car.create('Porsche'))
Магические методы
__repr__ vs __str__
__repr__ - нужен, чтобы дать недвусмысленную информацию об объекте
__str__ - нужен, чтобы дать читаемую информацию об объекте
Если __str__ не реализован, то __repr__ == __str__
Хороший __repr__ позволяет сделать так:
22
eval(repr(x)) == x
Магические методы
__del__ - вызывается перед удалением объекта, должен вызывать методы __del__() базовых классов.
Гарантий того, что интерпретатор вызовет __del__() для всех существующих объектов в момент вызова нет.
Важно: del x не вызывает напрямую x.__del__(), а лишь уменьшает счетчик ссылок на него на 1 x.__del__() будет вызван, когда счетчик ссылок на объект станет равным нулю.
23
Магические методы
24
import sys
a = 'hello hello'
b = a
print(sys.getrefcount(b)) # 3
del a
print(sys.getrefcount(b)) # 2
Магические методы
25
>>> <__main__.A object at 0x00000288A6BD19D0>
>>> del
>>> !
class A:
def __del__(self):
print('del')
a = A()
b = a
del a
print(b)
del b
print('!')
Магические методы
__call__ - определяет, что будет происходить, если мы вызовем объект как функцию.
26
class Product:
def __call__(self, a, b):
print(a * b)
ans = Product()
ans(10, 20) # 200
Встроенные атрибуты классов
>>> class Noop:
... """I do nothing at all."""
...
>>> Noop.__doc__
'I do nothing at all.'
>>> Noop.__name__
'Noop'
>>> Noop.__module__
'__main__'
>>> Noop.__bases__
(<class 'object'>,)
>>> noop = Noop()
>>> noop.__class__
<class '__main__.Noop'>
>>> noop.__dict__ # словарь атрибутов объекта
{}
27
Множественное наследование
28
Проблема ромбовидного наследования
Множественное наследование
29
class A:
def method(self):
print('A')
class B:
def method(self):
print('B')
class C(A, B):
pass
class D(B, A):
pass
Method resolution order
В Python 2.2 были представлены new-style classes. В них произошло довольно много изменений (например, связали понятия класса и типа), но нас интересует, что поменяли механизм наследования (уже в 2.3). Под капотом используется C3 линеаризация
30
class A: x = 'a'
class B(A): pass
class C(A): x ='c'
class D(B, C): pass
print(D.x)
print(D.__mro__)
Legacy: D - B - A - C - A
New-style: D - B - C - A - object
Когда MRO не справляется
31
class X: pass
class Y: pass
class A(X, Y): pass
class B(Y, X): pass
class C(A, B): pass
Object
X
Y
A
B
C
Про self
Когда люди приходят из других ООП языков (типа C#/Java), то их пугает self.
Есть причины, по которым self существует:
32
Про super()
super(type, object_or_type)
Функция super(), возвращает объект-посредник, который делегирует вызовы метода родительскому или родственному классу, указанного type типа. Это полезно для доступа к унаследованным методам, которые были переопределены в классе.
33
Перегрузка методов и super()
>>> class Counter:
... all_counters = []
...
... def __init__(self, initial=0):
... self.__class__.all_counters.append(self)
... self.value = initial
...
>>> class OtherCounter(Counter):
... def __init__(self, initial=0):
... self.initial = initial
... super().__init__(initial)
...
>>> oc = OtherCounter()
>>> vars(oc)
{'initial': 0, 'value': 0}
isinstance
Предикат isinstance принимает объект и класс и проверяет, что объект является экземпляром класса:
class A: pass
class B(A): pass
isinstance(B(), A) # -> True
В качестве второго аргумента можно также передать кортеж классов:
>>> class C:
... pass
>>> isinstance(B(), (A, C))
True
>>> isinstance(B(), A) or isinstance(B(), C)
True
35
issubclass
Предикат issubclass принимает два класса и проверяет, что первый класс является потомком второго:
class A: pass
class B(A): pass
issubclass(B, A) # -> True
Аналогично isinstance второй аргумент может быть кортежем классов:
>>> class C:
... pass
...
>>> issubclass(B, (A, C))
True
>>> issubclass(B, A) or issubclass(B, C)
True
36
Множественное наследование в реальности
В реальности используется, но вроде как избегается, заменяется примесями и композицией
Композиция
Не влезая в дебри различий между простой ассоциацией, агрегированием и композицией, давайте "на пальцах" определим:
Композиция — это когда один объект предоставляет другому свою функциональность частично или полностью.
Это когда один класс включает в себя другой класс в качестве одного из полей. Ассоциация описывается словом «имеет». Автомобиль имеет двигатель.
Mixins (примеси)
Это особый вид множественного наследования, который мы можем использовать в двух случаях:
Mixin похож на интерфейс, только методы в нем не абстрактные
39
Mixins (примеси)
40
class AddMixin:
def __add__(self, right):
return self.__class__(self.value + right)
class Value:
def __init__(self, value) -> None:
super().__init__()
self.value = value
def __str__(self):
return str(self.value)
class AddableValue(Value, AddMixin):
pass
print(AddableValue(5) + 3)
SOLID и другие хорошие вещи
Можно поискать в интернете, а также почитать конец главы 12 в Fluent Python
Дополнительные магические методы о которых стоит знать
42
__dict__
__dict__ - словарь всех доступных полей объекта. Для классов __dict__ будет не словарем, а mappingproxy (read-only словарь), благодаря которому интерпретатор гарантирует, что названия переменных и методов - это строки, и может проводить оптимизации на основе этого.
В остальных объектах мы можем хранить не-строковые названия атрибутов
43
car = Car()
car.__dict__['a'] = 1
car.__dict__[1] = 1
car.__dict__[(1,2)] = 1
print(
car.a,
car.__dict__[1],
car.__dict__[(1, 2)]
)
__getattr__(self, name)
Вы можете определить поведение для случая, когда пользователь пытается обратиться к атрибуту, который не существует (совсем или пока ещё).
Это может быть полезным для:
44
__setattr__(self, name, value)
Этот метод позволяет вам определить поведение для присвоения значения атрибуту, независимо от того существует атрибут или нет.
То есть, вы можете определить любые правила для любых изменений значения атрибутов. Впрочем, вы должны быть осторожны с тем, как использовать __setattr__
def __setattr__(self, name, value):
self.name = value
# это рекурсия, так как всякий раз, когда любому атрибуту присваивается значение,вызывается __setattr__()
def __setattr__(self, name, value):
self.__dict__[name] = value # присваивание в словарь переменных класса
# дальше определение произвольного поведения
45
__getattribute__(self, name)
Этот метод позволяет вам определить поведение для каждого случая доступа к атрибутам (а не только к несуществующим, как __getattr__(self, name)).
Он страдает от таких же проблем с бесконечной рекурсией, как и его коллеги (на этот раз вы можете вызывать __getattribute__ у базового класса, чтобы их предотвратить).
46
__getattr__ vs __getattribute__
Мы можем поменять логику того, как питон обращается к полям класса через эти два метода
__getattribute__ - вызывается всегда, когда мы обращаемся к какому-то полю (мы не знаем, существует оно или нет)
__getattr__ - вызывается, когда __getattribute__ кидает AttributeError
__setattr__ и __delattr__ работают аналогично __getattribute__, то есть вызываются всегда
47
Слоты
Мы можем определить поле __slots__, в котором укажем к каким полям класса можем обращаться.
Если верить доке, то это позволяет оптимизировать код (и по памяти и по времени)
48
Дескрипторы
49
Дескрипторы
Дескриптор - объект, который реализует методы __get__, __set__, __delete__ (хотя бы один из трех)
Это позволяет контролировать, что будет лежать в атрибуте класса и динамически менять содержимое
50
Property
В мире ООП существует хорошая практика - осуществлять доступ ко всем полям класса через геттеры и сеттеры, а сами поля должны быть защищенными. В питоне это реализовано через проперти.
Проперти работают на основе дескрипторов
51
Property
52
class Temperature:
def __init__(self, temperature=0):
self.temperature = temperature
@property
def temperature(self):
return self._temperature
@temperature.setter
def temperature(self, value):
self._temperature = value
human = Temperature(37)
print(human.temperature)
Геттеры и сеттеры
class PosDataModel:
def __init__(self):
self._params = []
@property
def params(self):
return self._params
@params.setter
def params(self, new_params):
assert all(map(lambda p: p > 0, new_params))
self._params = new_params
@params.deleter
def params(self):
del self._params
53
model = PosDataModel()
model.params = [0.1, 0.5, 0.4]
print(model.params) # [0.1, 0.5, 0.4]
del model.params
Декораторы классов
Работают абсолютно также, как и для функций.
def singleton(cls):
instance = None
@functools.wraps(cls)
def inner(*args, **kwargs):
nonlocal instance
if instance is None:
instance = cls(*args, **kwargs)
return instance
return inner
@singleton
class Noop:
"I do nothing at all."
print(id(Noop())) # 4383371952
print(id(Noop())) # 4383371952
54
Полезные ссылки
Руководство по магическим методам в Питоне
How can I create a directly-executable cross-platform GUI app using Python?
И все остальные из прошлой презентации
Python OOP Tutorial 4: Inheritance - Creating Subclasses
Главы 11-12 из книги Лучано Рамальо - Fluent Python
Композиция или наследование: как выбрать?
55
Extra
56
Небольшой пример про области видимости
>>> a = 1
>>> class cls:
... a = a
...
>>> def fcn():
... a = a
...
>>> t = cls()
>>> print(t.a)
1
>>> fcn()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in fcn
UnboundLocalError: local variable 'a' referenced before assignment
>>>
Что выведет программа?
class A:
__x = 1
def f(self):
return "f from A"
def g(self):
return "g from A"
class B:
__x = 2
def f(self):
return "f from B"
def g(self):
return "g from B"
class C(A, B):
f = B.f
c = C()
print(c.f(), c.g(), c._A__x, c._B__x)
58
И так можно?
class A:
def f(self):
print('A', end="")
class B:
def f(self):
print('B', end="")
class C(A):
def f(self):
print("C", end="")
super().f()
self.__class__.__bases__ = (B,)
x = C()
_ = x.f(), x.f()
59
Что выведет код?
class B:
def __init__(self) :
print ('В', end='')
super().__init__() # super(B, self).__init__()
class C:
def __init__(self):
print('C', end='')
super().__init__() # super(C, self).__init__()
# (D, B, C, object)
class D(B, C):
def __init__(self):
print('D', end='')
B.__init__(self) # super(D, self).__init__() # super().__init__()
C.__init__(self)
d = D()
60
Что выведет код?
class MyList(list):
def __getitem__(self, index):
return super().__getitem__(index) * 2
m = MyList([1, 2])
print([m[i] + x for i, x in enumerate(m)])
61
Проблема ромбовидного наследования
В каком порядке реализуется порядок вызовов в такой иерархии?
class A:
def ping(self):
print('ping:', self)
class B(A):
def pong(self):
print('pong:', self)
class C(A):
def pong(self):
print('PONG:', self)
class D(B, C):
def ping(self):
super().ping()
print('post-ping:', self)
def pingpong(self):
self.ping()
super().ping()
self.pong()
super().pong()
super(B, self).pong()
C.pong(self)