1 of 62

ООП

2 of 62

Классы и объекты в Python

Класс – это способ описания сущности, определяющий состояние и поведение, зависящее от этого состояния, а также правила для взаимодействия с данной сущностью (интерфейс).

  • Класс можно рассматривать как набор данных (полей, атрибутов, членов класса) и функций для работы с ними (методов).

  • С точки зрения структуры программы, класс является сложным типом данных.

3 of 62

Классы и объекты в Python

Объект (экземпляр) – это отдельный представитель класса, имеющий конкретное состояние и поведение, полностью определяемое классом.

Говоря простым языком, объект имеет конкретные значения атрибутов и методы, работающие с этими значениями на основе правил, заданных в классе.

4 of 62

Классы и объекты в Python

Класс – это описание еще не созданного объекта. Своеобразный шаблон или чертеж, по которому будет строиться экземпляр класса

Объект - то, что инициализировано по описанию из класса

4

class MyClass:

def __init__(self, param: int):

self.param = param

def increment(self):

return self.param + 1

myclass = MyClass(123)

myclass.increment()

5 of 62

ООП

ООП - это идея/способ/парадигма/стиль как можно писать код.

Идею ООП можно описать так:

  • Суть лежит в, собственно, объектах и классах.
  • Важная часть: объединение данных и кода с ними работающего,
  • Объекты в программе отражают сущности реального мира и их взаимодействие друг с другом

Соответственно, чтобы эффективно использовать ООП следует хорошо выделять эти объекты и их взаимодействия.

6 of 62

Зачем оно надо

Представьте себе, что вы проектируете автомобиль.

  1. Вы знаете, что автомобиль должен содержать двигатель, подвеску, две передних фары, 4 колеса, и т.д.
  2. Ещё вы знаете, что ваш автомобиль должен иметь возможность набирать и сбавлять скорость, совершать поворот и двигаться задним ходом.
  3. Вы точно знаете, как взаимодействует двигатель и колёса, согласно каким законам движется распредвал и коленвал, а также как устроены дифференциалы.

Вы уверены в своих знаниях и начинаете проектирование.

7 of 62

Зачем оно надо

Можно увидеть, что:

  • Данные или структура в вашем объекте
  • Функциональности или что может делать ваш объект[ы]
  • [Как объекты взаимодействуют между собой]

Получаете вашу “архитектуру”.

Концепции ООП позволяют элегантно решить проблему написания такой “архитектуры”

8 of 62

Принципы ООП. Полиморфизм

Полиморфизм - исполнение разного кода одинаковыми вызовами

Примеры полиморфизма:

  • Перегрузка методов
  • Приведение типов
  • Определение и использование интерфейсов

В питоне полиморфизм везде из-за утиной типизации

8

9 of 62

Принципы ООП. Инкапсуляция

Инкапсуляция - объединение набора данных в один компонент и предоставление единого интерфейса для обращения к данным и их изменения.

Мы предполагаем, что пользователь не будет менять некоторые поля и умышленно скрываем их.

В 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("Плохой пароль")

10 of 62

Принципы ООП. Инкапсуляция

В python нет механизмов сокрытия данных. Все атрибуты классов можно прочитать и изменить. Но это не отменяет инкапсуляцию.

10

class Hidden:

__hidden = 10

hidden_obj = Hidden()

print(hidden_obj.__hidden) # AttributeError

print(hidden_obj._Hidden__hidden) # OK

11 of 62

Принципы ООП. Наследование

Наследование - передача всех свойств класса наследнику (субклассу)

Наследование довольно часто усложняет код, заставляет строить сложные иерархии объектов. Чаще всего наследованию предпочитают композицию.

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

12 of 62

Принципы ООП. Абстракция

Абстра́кция в объектно-ориентированном программировании — это использование только тех характеристик объекта, которые с достаточной точностью представляют его в данной системе.

Основная идея состоит в том, чтобы представить объект минимальным набором полей и методов и при этом с достаточной точностью для решаемой задачи.

По мне так это понятие присуще в общем программированию.

13 of 62

Вернемся ближе к Python

13

14 of 62

Закон протекающих абстракций

Все нетривиальные абстракции до определенного предела протекают.

Этот закон утверждает, что абстракции, которые обычно используются в IT для упрощения работы со сложными системами, в определённых ситуациях дают «протечку», пропуская наверх элементы лежащих в их основе систем, из-за чего абстракция начинает вести себя непредсказуемо.

14

15 of 62

Дандеры

Дандер - double underscore, способ именования переменных в питоне. В питоне нет private атрибутов, поэтому все работает на уровне договоренностей

_var - скрытая переменная, состояние которой должен менять только сам объект

var_ - когда мы не хотим перезаписывать builtin функцию (напр. list, class)

__var - скрытый атрибут, который не перейдет в наследника класса

__var__ - специальные магические методы, например __str__, __add__ и т.д.

15

16 of 62

Магические методы

__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 of 62

Магические методы

17

class A:

def __new__(cls) -> A:

return super().__new__(cls)

def __init__(self) -> None:

pass

A()

18 of 62

Жизненный цикл объекта

18

__new__

__init__

__del__

19 of 62

Атрибуты класса и экземпляра в 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

20 of 62

Статические методы

Статические методы - методы класса, которые не принимают аргумент self и могут вызываться без создания объекта класса

20

class C:

@staticmethod

def f(arg1, arg2, ...): ...

def regular_function():

...

class C:

method = staticmethod(regular_function)

21 of 62

Метод класса

Статический метод, которому первым аргументом передают класс объекта и ожидается, что вернется новый объект этого класса. По сути, это фабрика на уровне языка

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'))

22 of 62

Магические методы

__repr__ vs __str__

__repr__ - нужен, чтобы дать недвусмысленную информацию об объекте

__str__ - нужен, чтобы дать читаемую информацию об объекте

Если __str__ не реализован, то __repr__ == __str__

Хороший __repr__ позволяет сделать так:

22

eval(repr(x)) == x

23 of 62

Магические методы

__del__ - вызывается перед удалением объекта, должен вызывать методы __del__() базовых классов.

Гарантий того, что интерпретатор вызовет __del__() для всех существующих объектов в момент вызова нет.

Важно: del x не вызывает напрямую x.__del__(), а лишь уменьшает счетчик ссылок на него на 1 x.__del__() будет вызван, когда счетчик ссылок на объект станет равным нулю.

23

24 of 62

Магические методы

24

import sys

a = 'hello hello'

b = a

print(sys.getrefcount(b)) # 3

del a

print(sys.getrefcount(b)) # 2

25 of 62

Магические методы

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('!')

26 of 62

Магические методы

__call__ - определяет, что будет происходить, если мы вызовем объект как функцию.

26

class Product:

def __call__(self, a, b):

print(a * b)

ans = Product()

ans(10, 20) # 200

27 of 62

Встроенные атрибуты классов

>>> 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 of 62

Множественное наследование

28

Проблема ромбовидного наследования

29 of 62

Множественное наследование

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

30 of 62

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

31 of 62

Когда 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

32 of 62

Про self

Когда люди приходят из других ООП языков (типа C#/Java), то их пугает self.

Есть причины, по которым self существует:

  • Возможность использовать декораторы
  • Независимость функций и классов
  • Это архитектурная особенность языка, которая позволяет реализовать множество важных для языка фичей
  • Огромный простор для кастомизации логики работы с классами и экземплярами

32

33 of 62

Про super()

super(type, object_or_type)

Функция super(), возвращает объект-посредник, который делегирует вызовы метода родительскому или родственному классу, указанного type типа. Это полезно для доступа к унаследованным методам, которые были переопределены в классе.

  • type - необязательно, тип, от которого начинается поиск объекта-посредника
  • object_or_type - необязательно, тип или объект, определяет порядок разрешения метода для поиска

33

34 of 62

Перегрузка методов и 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}

35 of 62

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

36 of 62

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

37 of 62

Множественное наследование в реальности

В реальности используется, но вроде как избегается, заменяется примесями и композицией

38 of 62

Композиция

Не влезая в дебри различий между простой ассоциацией, агрегированием и композицией, давайте "на пальцах" определим:

Композиция — это когда один объект предоставляет другому свою функциональность частично или полностью.

Это когда один класс включает в себя другой класс в качестве одного из полей. Ассоциация описывается словом «имеет». Автомобиль имеет двигатель.

39 of 62

Mixins (примеси)

Это особый вид множественного наследования, который мы можем использовать в двух случаях:

  • Хотим расширить возможности класса, не изменяя его
  • Хотим предоставить фичу сразу нескольким классам

Mixin похож на интерфейс, только методы в нем не абстрактные

39

40 of 62

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)

41 of 62

SOLID и другие хорошие вещи

Можно поискать в интернете, а также почитать конец главы 12 в Fluent Python

  • Принцип единственной ответственности (Single Responsibility Principle)
    • Существует лишь одна причина, приводящая к изменению класса.
  • Принцип открытости/закрытости (Open-closed Principle)
    • Программные сущности должны быть открыты для расширения, но закрыты для модификации.
  • Принцип подстановки Барбары Лисков (Liskov Substitution Principle)
  • Принцип разделения интерфейса (Interface Segregation Principle)
    • Нельзя заставлять клиента реализовать интерфейс, которым он не пользуется.
  • Принцип инверсии зависимостей (Dependency Inversion Principle)
    • Высокоуровневые модули не должны зависеть от низкоуровневых. Оба вида модулей должны зависеть от абстракций.

42 of 62

Дополнительные магические методы о которых стоит знать

42

43 of 62

__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)]

)

44 of 62

__getattr__(self, name)

Вы можете определить поведение для случая, когда пользователь пытается обратиться к атрибуту, который не существует (совсем или пока ещё).

Это может быть полезным для:

  • перехвата и перенаправления частых опечаток,
  • предупреждения об использовании устаревших атрибутов
  • хитро возвращать AttributeError, когда это вам нужно.

44

45 of 62

__setattr__(self, name, value)

Этот метод позволяет вам определить поведение для присвоения значения атрибуту, независимо от того существует атрибут или нет.

То есть, вы можете определить любые правила для любых изменений значения атрибутов. Впрочем, вы должны быть осторожны с тем, как использовать __setattr__

def __setattr__(self, name, value):

self.name = value

# это рекурсия, так как всякий раз, когда любому атрибуту присваивается значение,вызывается __setattr__()

def __setattr__(self, name, value):

self.__dict__[name] = value # присваивание в словарь переменных класса

# дальше определение произвольного поведения

45

46 of 62

__getattribute__(self, name)

Этот метод позволяет вам определить поведение для каждого случая доступа к атрибутам (а не только к несуществующим, как __getattr__(self, name)).

Он страдает от таких же проблем с бесконечной рекурсией, как и его коллеги (на этот раз вы можете вызывать __getattribute__ у базового класса, чтобы их предотвратить).

46

47 of 62

__getattr__ vs __getattribute__

Мы можем поменять логику того, как питон обращается к полям класса через эти два метода

__getattribute__ - вызывается всегда, когда мы обращаемся к какому-то полю (мы не знаем, существует оно или нет)

__getattr__ - вызывается, когда __getattribute__ кидает AttributeError

__setattr__ и __delattr__ работают аналогично __getattribute__, то есть вызываются всегда

47

48 of 62

Слоты

Мы можем определить поле __slots__, в котором укажем к каким полям класса можем обращаться.

Если верить доке, то это позволяет оптимизировать код (и по памяти и по времени)

48

49 of 62

Дескрипторы

49

50 of 62

Дескрипторы

Дескриптор - объект, который реализует методы __get__, __set__, __delete__ (хотя бы один из трех)

Это позволяет контролировать, что будет лежать в атрибуте класса и динамически менять содержимое

50

51 of 62

Property

В мире ООП существует хорошая практика - осуществлять доступ ко всем полям класса через геттеры и сеттеры, а сами поля должны быть защищенными. В питоне это реализовано через проперти.

Проперти работают на основе дескрипторов

51

52 of 62

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)

53 of 62

Геттеры и сеттеры

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

54 of 62

Декораторы классов

Работают абсолютно также, как и для функций.

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

55 of 62

Полезные ссылки

55

56 of 62

Extra

56

57 of 62

Небольшой пример про области видимости

>>> 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

>>>

58 of 62

Что выведет программа?

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

59 of 62

И так можно?

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

60 of 62

Что выведет код?

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

61 of 62

Что выведет код?

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

62 of 62

Проблема ромбовидного наследования

В каком порядке реализуется порядок вызовов в такой иерархии?

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)