Теоретическая часть

Фактически, в Python любые (в том числе встроенные) типы данных определяются соответствующими классами, и все с чем мы работаем — или классы, или их экземпляры. Функция — экземпляр класса function, целое число — экземпляр класса int, и даже класс — экземпляр класса type.

Объектно-ориентированный подход в программировании базируется на нескольких принципах ООП(объектно-ориентированного программирования):

  • Абстракция — принцип, согласно которому мы создаем модели, соотносящиеся с объектами нашего домена, которые достаточно точно представляют эти объекты в нашей программе.
  • Инкапсуляция — принцип, согласно которому мы объединяем в рамках нашей модели данные и методы работы с ними. В некоторых языках программирования инкапсуляция также подразумевает сокрытие внутренней реализации наших моделей от внешнего мира, однако в Python этого нет.
  • Полиморфизм — принцип, который говорит о способности функции обрабатывать данные различных типов.
  • Наследование — принцип, согласно которому наш класс может наследовать(переиспользовать) данные и функциональность некоторого существующего типа.

Примечания:

  • Классы и их экземпляры порождают пространство имен. Фактически, в Python 4 объекта порождают пространство имен: классы, экземпляры, функции, модули.

a/b


Методы в классах

Внутри класса есть несколько различных типов методов.

Обычные методы

class A():
  def m(self):
    pass

Применяются повсеместно.

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

class A():
  @staticmethod
  def m():
    pass

Статические методы в Python — это обычные функции, помещенные в класс, в случае, если нам нет необходимости инстанциировать класс для ее использования, и(или) непосредственно экземпляр класса не используется. Например, мы можем поместить какие-то вспомогательные утилиты в специальный класс, чтобы держать их в пространстве имен этого класса.

Статические метод не могут менять состояние класса и его экземпляра.

Методы класса

class A():
  @classmethod
  def m(cls):
    pass

Методы класса принимают в качестве первого параметра не экземпляр, а сам класс.

Одна из самых распространенных областей использования методов класса — работа с переменными класса.

Абстрактные методы

from abc import ABC
from abc import abstractmethod

class MyABC(ABC):
    @abstractmethod
    def func(self):
      pass

Абстрактным называется класс, в котором есть как минимум один абстрактный метод (или свойство). Наследование от класса ABC недостаточно чтобы сделать класс абстрактным.

Абстрактный метод — метод, который в обязательном порядке должен быть переопределен в дочернем классе. При этом, мы вполне можем переиспользовать реализацию метода в абстрактном классе, например, с использованием super.

Создать экземпляр абстрактного класса нельзя.

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


Наследование

Классы, от которых наследуется данный класс, называются родительскими, или суперклассами (superclass).

Классы, унаследованные от данного называются подклассами(subclass) или дочерними.

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

Такой подход ведет к определенным проблемам, самая известная(и важная) из которых называется проблема ромбовидного наследования (diamond problem). Подробнее об этой поблеме можно прочитать здесь: Wikipedia. Ромбовидное наследование..

Подход к решению этой проблемы различается в Python2 и Python 3.

Классы в Python делятся на 2 типа: old-style classes и new-style classes.

New-style classes — классы, которые наследуются от специального встроенного класса object. Любой класс, унаследованный от new-style класса автоматически становится new-style.

Old-style classes — соответственно классы, которые object в списке своих родителей не содержат.

В Python2 классы могут быть как old- так и new-style, в зависимости от того, указали ли мы явно object в списке родителей.

В Python3 все классы являются new-style классами, даже если явно object не указывается (он будет добавлен интерпретатором).

Удобно представлять структуру классов визуально: в самом начале находится наш класс, его родители — на уровень выше и соединены с ним линией. Родители родителей — еще на уровень выше и т.д. Классы родители расположены в том же порядке как при задании класса.

Old-style classes

Для old-style классов при поиске атрибутов в класса-родителях используется поиск в глубину.

Мнемоническое правило для определения порядка обход класса:

  • У нас есть указатель, который показывает на класс в структуре классов, на котором мы находимся в текущий момент. Решение какой класс брать следующем принимается исходя из точки где мы находимся.
  • Если у нас есть несколько кандидатур-родителей, мы выбираем левый.
  • Уже просмотренные классы исключаются из опций при принятии решения.
  • В случае если кандидатур нет мы спускаемся на уровень ниже.

New-style classes

Для new-style классов описанный выше алгоритм не подходил. Проблема заключается в том, что object может обработать абсолютно любое обращение (например выдать ошибку, что такого атрибута нет), и когда мы до него дойдем, все остальные непросмотренные классы уже никогда не будут просмотрены.

Для классов нового стиля был использован другой алгоритм, который называется C3-линеаризация.

Обратите внимание на пример с предложенной статьи. Там граф зависимостей не соответствует нашим правилам визуального представления и может сбить с толку при расчете порядка обхода (там классы-родители слева направо расположены не в том порядке в котором использованы при задании подклассов).

Мнемоническое правило для определения порядка обход класса если мы не хотим выписывать все цепочки и мерджи:

  • У нас есть указатель, который показывает на класс в структуре классов, на котором мы находимся в текущий момент. Решение какой класс брать следующем принимается исходя из точки где мы находимся.
  • Если у нас есть несколько кандидатур-родителей, мы выбираем левый.
  • Уже просмотренные классы исключаются из опций при принятии решения.
  • В случае если кандидатур нет мы спускаемся на уровень ниже.
  • !!! (Отличие). Класс не может быть просмотрен, если не просмотрены все его наследними.

Полиморфизм

Полиморфизм — способность функции обрабатывать данные разных типов.

Существует два типа полиморфизма:

  • Параметрический полиморфизм — случай, когда наши методы позволяют обрабатывать значения разных типов однообразно. Это вносит существенную гибкость в язык программирования. В Python реализован параметрический полиморфизм.
  • Ad-hoc полиморфизм — осуществляетс посредством перегрузки методов, а в некоторых языках — посредством приведения типов. В Python перегрузить метод — достаточно хлопотное занятие, ввиду того что создание функции с таким же именем и другим набором параметров просто создаст новую функцию, а старая исчезнет. Такого полиморфизма в Python нет.

Классический пример полиморфизма:

print(1 + 1.0)  # 2.0
print(1 + 1)  # 2

В обоих случаях один и тот же объект 1 типа int вызывает один и тот же метод __add__, которому передаются параметры разных типов: в первом случае float, во втором int, и этот метод возвращает разные значения в зависимости от типа.

Не стоит путать корректный пример выше с другим:

print([1] + [1])  # [1, 1]
print('1' + '1')  # '11'

В данном случае у разных объектов разных типов вызываются разные(!!) методы, которые просто имеют одинаковое имя, им передаются разные параметры и мы получаем разный результат. Это полиморфизмом не является.


Сокрытие переменных

Классически в языках программирования существуют 3 уровня доступа к их внутренним объектам:

  • public — элементы, доступные как изнутри так и снаружи классов.
  • protected — элементы, доступные только внутри класса и его наследников
  • private — элементы, доступные только внутри класса.

В Python таких механизмов нет, однако есть синтаксис, позволяющий эмулировать такое поведение.

Protected атрибуты

Для того чтобы сделать атрибут protected, необходимо перед его именем поставить _. В таком случае, на уровне джентльменского соглашения считается, что переменная предназначена для использования внутри класса и его наследников, и не стоит трогать эти атрибуты и методы извне класса. Однако, в некоторых ситуациях, например при написании unit тестов, это вполне допустимо.

Private атрибуты

Чтобы сделать атрибут доступным только внутри класса, необходимо перед его именем поставить __. В таком случае будет применен специальный механизм, который называется name mangling — искажение имен. При этом, внутри класса можно использовать атрибут или метод по его имени, а снаружи, а также у наследников, для получения доступа к этому атрибуту придется писать уже измененное имя, которое будет иметь вид _ClassName__var_name.


Code Snippets


Работа с классами


# Создание простейшего класса.
class MyClass:
  pass

inst = MyClass()  # создание экземпляра класса

print(type(inst), inst)
print(type(MyClass), MyClass)
<class '__main__.MyClass'> <__main__.MyClass object at 0x7f043422d690>
<class 'type'> <class '__main__.MyClass'>

# В Python все объекты являются экземплярами какого-то класса.
a = list((1, 2, 3))
print(a, type(a))
[1, 2, 3] <class 'list'>

# Конструктор класса - метод, который вызывается после создания для каждого
# экземпляра класса.
class MyClass:
  def __init__(self, color):
    print("init", color)
    self.color = color

inst1 = MyClass('red')
inst2 = MyClass('black')

print(inst1.color, inst2.color)

init red
init black
red black

# Первый аргумент может иметь любое имя, однако традиционное имя для него - 
# self. Не стоит уходить от этого именования без очень веских причин.
class MyClass:
  def __init__(pokemon):
    print("init")
    pokemon.a = 5

inst = MyClass()
print(inst.a)

init
5

# Методы класса, в том числе конструктор, как и обычные функции могут принимать
# параметры.
class MyClass:
  def __init__(self, color):
    print("init")
    self.color = color

inst1 = MyClass('red')
inst2 = MyClass('black')

print(inst1.color, inst2.color)
init
init
red black

# Все тоже самое что и в функциях. Любые типы параметров. Методы - это обычные
# функции, просто они определяются внутри класса и первым аргументом получают
# вызвавший их экземпляр. 
class MyClass:
  def __init__(self, var, c=0, *args, e=1, **kwargs):
    self.var = var
    self.a = 1
    self.c = c
    self.args = args

inst = MyClass(6)
print(inst.var, inst.c, inst.args)
6 0 ()

# Атрибуты экземпляра, как и обычные переменные, создаются при присвоении. 
# При этом не обязательно, чтобы это происходила внутри конструктора или 
# какого-либо метода. Это можно сделать и извне, хоть это и является плохой
# практикой. 
class My:
  pass

inst1 = My()
inst2 = My()

inst1.b = [123]

print(inst1.b)
print(inst2.b)  # а здесь этого атрибута не будет

# Атрибуты экземпляра, как и обычные переменные, создаются при присвоении. 
# При этом не обязательно, чтобы это происходила внутри конструктора или 
# какого-либо метода. Это можно сделать и извне, хоть это и является плохой
# практикой. 
class My:
  pass

inst1 = My()
inst2 = My()

inst1.b = [123]

print(inst1.b)
print(inst2.b)  # а здесь этого атрибута не будет
[123]
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-3-acefce63bc45> in <module>
     12 
     13 print(inst1.b)
---> 14 print(inst2.b)  # а здесь этого атрибута не будет

AttributeError: 'My' object has no attribute 'b'

# Экземпляры класса после создания являются независиммыми друг от друга
# пространствами имен. Кроме того, атрибуты инстансов для класса никак не видны.
class MyClass:
  def __init__(self):
    self.a = [1]

inst = MyClass()
new_inst = MyClass()

print(inst.a) 
print()

inst.a.append(4)
print(inst.a)
print(new_inst.a)
print()

inst.b = 5
print(dir(inst))
print(dir(new_inst))
print(dir(MyClass))
[1]

[1, 4]
[1]

['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'a', 'b']
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'a']
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']

# Создание обычных методов. Просто задаем функцию внутри класса, и первым 
# аргументом ставим ей self, в который будет передан вызвавщий метод инстанс.

class MyClass:
  def myf(self, a):
    return a + 1

inst = MyClass()
print(inst.myf(1))
2

# Еще один пример. Метод меняет заданные при инициализации значения.
class MyClass:
  def __init__(self, base):
    self.base = base
    
  def myf(self, a):
    self.base = a

inst = MyClass(5)
new_inst = MyClass(3)

print(inst.myf(1), new_inst.myf(2))
print(inst.base, new_inst.base)
None None
1 2

Типы атрибутов в классе


# Создадим различные типы переменных.
class MyClass():
  # Часто такие переменные используются как константы класса, поэтому 
  # ее имя в upper case.
  CLASS_VAR = []  # Переменная, определенная внутри класса - переменная класса

  def func(self, param=None):
    local_var = 1  # обычная локальная переменная в методе
    self.inst_var = 2  # переменная инстанса. 
      
inst = MyClass()
print(MyClass.CLASS_VAR)
inst.func()
print(inst.inst_var)
print(inst.local_var)  # очевидно, эту переменную мы получить не сможем

[]
2
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-7-6d537fbce2a2> in <module>
     13 inst.func()
     14 print(inst.inst_var)
---> 15 print(inst.local_var)  # очевидно, эту переменную мы получить не сможем

AttributeError: 'MyClass' object has no attribute 'local_var'

class MyClass():
  ANY_CLASS_VAR = [0, 0]

  def __init__(self):
    # Экземпляр вполне может получить значение переменной класса
    print(self.ANY_CLASS_VAR)     

  def func(self):
    # А вот с присвоением у нас возникнут объективные сложности.
    # Присвоение создает переменную в пространстве имен экземпляра.
    self.ANY_CLASS_VAR = 10 
      
inst = MyClass()
inst.func()
print(MyClass.ANY_CLASS_VAR, inst.ANY_CLASS_VAR)
[0, 0]
[0, 0] 10

# Избежать данной проблемы можно например таким образом. 
# Или использовать методы класса(он них чуть дальше).
class MyClass():
  ANY_CLASS_VAR = [0, 0]

  def func(self):
    MyClass.ANY_CLASS_VAR = 10 
      
inst = MyClass()
inst.func()
print(MyClass.ANY_CLASS_VAR, inst.ANY_CLASS_VAR)
10 10

# Кроме того, переменные класса могут очевидно меняться и извне класса.
class MyClass():
  ANY_CLASS_VAR = [0, 0]
      
MyClass.ANY_CLASS_VAR[0] = 1
print(MyClass.ANY_CLASS_VAR)
[1, 0]

Наследование


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

# Такой синтаксис будет обязателен в Python2 при определении new style классов,
# а в Python3 object в списке родителей можно опустить.
class A(object):
  def __init__(self):
    print("A")

  a = 10  # переменная класса А

  def myf(self):
    print('myf')

class B(A):
  def __init__(self):
    print(self.a)  # наследник получает доступ ко всем атрибутам родителя
    self.myf()  # и его методам


b = B()
10
myf

# При множественном наследовании со сложной структурой мы сталкиваемся с
# вопросом, атрибут какого родителя нужно использовать. 
# Данная проблема называется проблемой ромбовидного наследования.
class A(object):
  var = 'A'

class B(A):
  pass

class C(A):
  var = 'C'

class MyClass(B, C):
  pass

inst = MyClass()
print(inst.var)
C

# Посмотреть порядок, в котором будут просматриваться родители, можно в
# специальном атрибуте __mro__ класса(mthod resolution order).
# MRO для класса вычисляется при определении класса, а также пересчитывается
# в случае, если у класса в процессе выполнения программы меняются родительские
# классы (это очень, очень плохая практика, не стоит так делать без очень веских
# оснований).
class A():
  var = 'A'

class B(A):
  pass

class C(A):
  var = 'D'

class MyClass(B, C):
  pass

print(MyClass.__mro__)
(<class '__main__.MyClass'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)

# Функции issubclass и type.
class A: 
  pass
class B(A): 
  pass

inst = B()
print(type(inst))  # позволяет узнать тип объекта
print(issubclass(B, A))  # проверяет, является ли класс подклассом заданного
<class '__main__.B'>
True

Методы в классах


# Задание статического метода. Может быть вызван как из инстанса так и без
# инстанциирования класса.
class MyClass:
  @staticmethod
  def static_method(a=0, *, c=3):
    print("static")


a = MyClass() 
a.static_method()
MyClass.static_method()
static
static

# Создание классового метода. Вызывается как из инстанса так и через класс.
class MyClass:
  logging_level = 'debug'
  
  @classmethod
  def class_method(cls, level):
    cls.logging_level = level

  def f():
    if logging_level > 'debug':
      logging.debug("blablabla")
  
print(MyClass.logging_level)

MyClass.class_method('info')
print(MyClass.logging_level)

a = MyClass()
a.class_method('warning')
print(MyClass.logging_level)
debug
info
warning

# Наследование класса от ABC еще не делает его абстрактным. Мы можем создать его
# экземпляр.
from abc import ABC

class MyABC(ABC):
    a = 10

inst = MyABC()
print(inst.a)
10

# И обычные методы, заданные в классе не делают его абстрактным.
from abc import ABC

class MyABC(ABC):
    a = 10
    
    def func(self): 
      return MyABC.a

inst = MyABC()
print(inst.a)
print(inst.func())
10
10

# Здесь мы добавили абстрактный метод. Мы не можем инстанциировать класс.
from abc import ABC, abstractmethod

class MyABC(ABC):
    a = 10

    @abstractmethod
    def func(self, arg=10):
        arg *=10
        print("abstract")

inst = MyABC()

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-18-66bce488a220> in <module>
     10         print("abstract")
     11 
---> 12 inst = MyABC()
     13 

TypeError: Can't instantiate abstract class MyABC with abstract methods func

# Мы не можем просто создать наследника абстрактного класса, мы обязаны
# создать реализацию абстрактных методов.
from abc import ABC, abstractmethod

class MyABC(ABC):
    a = 10

    @abstractmethod
    def func(self):
        print("abstract")

class NewClass(MyABC):
  pass

inst = NewClass()
print(inst.a)

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-15-ca249880b409> in <module>()
     13   pass
     14 
---> 15 inst = NewClass()
     16 print(inst.a)

TypeError: Can't instantiate abstract class NewClass with abstract methods func

# Так все работает.
from abc import ABC, abstractmethod

class MyABC(ABC):
    a = 10

    @abstractmethod
    def func(self): pass

class NewClass(MyABC):
  def func(self):
    print("newclass")

inst = NewClass()
inst.func()
newclass
None

super

Часто мы хотим переиспользовать код родительских классов, а не использовать их без изменений. В этом нам поможет функция super.

super(type, obj_to_find) — принимает два аргумента. Тип объекта, по MRO которого будем мы будем проходить, второй — объект, тип которого мы будем искать в этой последовательности. Возвращает прокси-объект типа, следующего за искомым.


# Часто в obj_to_find передается self, тогда у нас будет возвращен первый
# родитель нашего класса (В MRO находится типа объекта = сам класс, и 
# возвращается следующий).
class A(object):
  def __init__(self): 
    print('a')

class C(A):
  def __init__(self): 
    print('c')

class D(C):
  def __init__(self):
    super(D, self).__init__()
    print('d')

d = D()
c
d

# Часто реализуют целые цепочки вызовов через супер, каждый новый класс просто
# уточняет функционал предыдущего.
class A(object):
  def __init__(self):
    print('a')

class B(A):
    def __init__(self):
      super(B, self).__init__()
      print('b')
  
class C(A):
  def __init__(self):
    super(C, self).__init__()
    print('с')

class D(C, B):
  def __init__(self):
    super(D, self).__init__()
    print('d')

  ind = {}

class tracking():
  # updated_by = db.DateTimeField(###)
  ###
  ind = {'updated_by'}
  

# Full Text Search
# obj_type obj_id field_name field_value


class D(C, B, tracking):
  def __init__(self):
    super().__init__()
    print('d')

  ind = {}

d = D()

# MIXIN

D C B tracking indexed


[a for a in iterable]
a
b
с
d

# Super будет также прекрасно работать с абстрактными классами.
from abc import ABC, abstractmethod

class MyABC(ABC):
  @abstractmethod
  def func(self):
    print("abstract")

class NewClass(MyABC):
  def func(self): 
    super(NewClass, self).func()

    
inst = NewClass()
inst.func()
abstract

Работа с атрибутами класса

У нас есть несколько функций, которые позволяют работать с атрибутами класса:

  • getattr(obj, name[, default]) — получает атрибут с именем name объекта obj, в случае его отсутствия — возвращает default.
  • setattr(obj, name, value) — присваивает атрибуту с именем name объекта obj значение value.
  • delattr(obj, name) — удаляет атрибут с именем name объекта obj.
  • hasattr(obj, name) — проверяет, есть ли у объекта obj атрибут с именем name.

# Получаем и устанавливаем значение атрибута.
class A():
  a = 42

attr = getattr(A, 'a')
print(attr)
setattr(A, 'a', 'new_value')
print(A.a)
42
new_value

# Удобство методов заключается в том, что мы можем не иметь переменные,
# соответствующие нужным атрибутам. А здесь нам достаточно имен.
a = 10

class A():
  a = None
  
print(dir(A))

clear_attrs = ['a', 'b', 'c']
for attr in clear_attrs:
  if hasattr(A, attr):
    delattr(A, attr)

import os
print(dir(os))
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'a']
['CLD_CONTINUED', 'CLD_DUMPED', 'CLD_EXITED', 'CLD_TRAPPED', 'DirEntry', 'EX_CANTCREAT', 'EX_CONFIG', 'EX_DATAERR', 'EX_IOERR', 'EX_NOHOST', 'EX_NOINPUT', 'EX_NOPERM', 'EX_NOUSER', 'EX_OK', 'EX_OSERR', 'EX_OSFILE', 'EX_PROTOCOL', 'EX_SOFTWARE', 'EX_TEMPFAIL', 'EX_UNAVAILABLE', 'EX_USAGE', 'F_LOCK', 'F_OK', 'F_TEST', 'F_TLOCK', 'F_ULOCK', 'GRND_NONBLOCK', 'GRND_RANDOM', 'MutableMapping', 'NGROUPS_MAX', 'O_ACCMODE', 'O_APPEND', 'O_ASYNC', 'O_CLOEXEC', 'O_CREAT', 'O_DIRECT', 'O_DIRECTORY', 'O_DSYNC', 'O_EXCL', 'O_LARGEFILE', 'O_NDELAY', 'O_NOATIME', 'O_NOCTTY', 'O_NOFOLLOW', 'O_NONBLOCK', 'O_PATH', 'O_RDONLY', 'O_RDWR', 'O_RSYNC', 'O_SYNC', 'O_TMPFILE', 'O_TRUNC', 'O_WRONLY', 'POSIX_FADV_DONTNEED', 'POSIX_FADV_NOREUSE', 'POSIX_FADV_NORMAL', 'POSIX_FADV_RANDOM', 'POSIX_FADV_SEQUENTIAL', 'POSIX_FADV_WILLNEED', 'PRIO_PGRP', 'PRIO_PROCESS', 'PRIO_USER', 'P_ALL', 'P_NOWAIT', 'P_NOWAITO', 'P_PGID', 'P_PID', 'P_WAIT', 'PathLike', 'RTLD_DEEPBIND', 'RTLD_GLOBAL', 'RTLD_LAZY', 'RTLD_LOCAL', 'RTLD_NODELETE', 'RTLD_NOLOAD', 'RTLD_NOW', 'RWF_DSYNC', 'RWF_HIPRI', 'RWF_NOWAIT', 'RWF_SYNC', 'R_OK', 'SCHED_BATCH', 'SCHED_FIFO', 'SCHED_IDLE', 'SCHED_OTHER', 'SCHED_RESET_ON_FORK', 'SCHED_RR', 'SEEK_CUR', 'SEEK_DATA', 'SEEK_END', 'SEEK_HOLE', 'SEEK_SET', 'ST_APPEND', 'ST_MANDLOCK', 'ST_NOATIME', 'ST_NODEV', 'ST_NODIRATIME', 'ST_NOEXEC', 'ST_NOSUID', 'ST_RDONLY', 'ST_RELATIME', 'ST_SYNCHRONOUS', 'ST_WRITE', 'TMP_MAX', 'WCONTINUED', 'WCOREDUMP', 'WEXITED', 'WEXITSTATUS', 'WIFCONTINUED', 'WIFEXITED', 'WIFSIGNALED', 'WIFSTOPPED', 'WNOHANG', 'WNOWAIT', 'WSTOPPED', 'WSTOPSIG', 'WTERMSIG', 'WUNTRACED', 'W_OK', 'XATTR_CREATE', 'XATTR_REPLACE', 'XATTR_SIZE_MAX', 'X_OK', '_Environ', '__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', '_check_methods', '_execvpe', '_exists', '_exit', '_fspath', '_fwalk', '_get_exports_list', '_putenv', '_spawnvef', '_unsetenv', '_wrap_close', 'abc', 'abort', 'access', 'altsep', 'chdir', 'chmod', 'chown', 'chroot', 'close', 'closerange', 'confstr', 'confstr_names', 'cpu_count', 'ctermid', 'curdir', 'defpath', 'device_encoding', 'devnull', 'dup', 'dup2', 'environ', 'environb', 'error', 'execl', 'execle', 'execlp', 'execlpe', 'execv', 'execve', 'execvp', 'execvpe', 'extsep', 'fchdir', 'fchmod', 'fchown', 'fdatasync', 'fdopen', 'fork', 'forkpty', 'fpathconf', 'fsdecode', 'fsencode', 'fspath', 'fstat', 'fstatvfs', 'fsync', 'ftruncate', 'fwalk', 'get_blocking', 'get_exec_path', 'get_inheritable', 'get_terminal_size', 'getcwd', 'getcwdb', 'getegid', 'getenv', 'getenvb', 'geteuid', 'getgid', 'getgrouplist', 'getgroups', 'getloadavg', 'getlogin', 'getpgid', 'getpgrp', 'getpid', 'getppid', 'getpriority', 'getrandom', 'getresgid', 'getresuid', 'getsid', 'getuid', 'getxattr', 'initgroups', 'isatty', 'kill', 'killpg', 'lchown', 'linesep', 'link', 'listdir', 'listxattr', 'lockf', 'lseek', 'lstat', 'major', 'makedev', 'makedirs', 'minor', 'mkdir', 'mkfifo', 'mknod', 'name', 'nice', 'open', 'openpty', 'pardir', 'path', 'pathconf', 'pathconf_names', 'pathsep', 'pipe', 'pipe2', 'popen', 'posix_fadvise', 'posix_fallocate', 'pread', 'preadv', 'putenv', 'pwrite', 'pwritev', 'read', 'readlink', 'readv', 'register_at_fork', 'remove', 'removedirs', 'removexattr', 'rename', 'renames', 'replace', 'rmdir', 'scandir', 'sched_get_priority_max', 'sched_get_priority_min', 'sched_getaffinity', 'sched_getparam', 'sched_getscheduler', 'sched_param', 'sched_rr_get_interval', 'sched_setaffinity', 'sched_setparam', 'sched_setscheduler', 'sched_yield', 'sendfile', 'sep', 'set_blocking', 'set_inheritable', 'setegid', 'seteuid', 'setgid', 'setgroups', 'setpgid', 'setpgrp', 'setpriority', 'setregid', 'setresgid', 'setresuid', 'setreuid', 'setsid', 'setuid', 'setxattr', 'spawnl', 'spawnle', 'spawnlp', 'spawnlpe', 'spawnv', 'spawnve', 'spawnvp', 'spawnvpe', 'st', 'stat', 'stat_result', 'statvfs', 'statvfs_result', 'strerror', 'supports_bytes_environ', 'supports_dir_fd', 'supports_effective_ids', 'supports_fd', 'supports_follow_symlinks', 'symlink', 'sync', 'sys', 'sysconf', 'sysconf_names', 'system', 'tcgetpgrp', 'tcsetpgrp', 'terminal_size', 'times', 'times_result', 'truncate', 'ttyname', 'umask', 'uname', 'uname_result', 'unlink', 'unsetenv', 'urandom', 'utime', 'wait', 'wait3', 'wait4', 'waitid', 'waitid_result', 'waitpid', 'walk', 'write', 'writev']

Сокрытие переменных


# Зададим скрытый атрибут. Его могут использовать как класс так и его наследники.
class MyClass:
  _hidden_var = "hidden"
  def func(self):
    print(self._hidden_var)

class NewClass(MyClass):
  def func(self):
    print(self._hidden_var)

inst = MyClass()
inst.func()

new_inst = NewClass()
NewClass().func()
hidden
hidden
hidden

# Еще пример и пасхалочка...3 часа ночи 8 марта... я хочу спать а у меня 7 серверов на максималках работают уже 12 часов.

import random
import time

class WithHidden:
  def __init__(self, seed):
    self.seed = seed

  @staticmethod
  def _myhidden(seed, num):
    # my hidden stuff
    random.seed(seed)
    print('new seed')
  
  def randomize(self):
    return(self._myhidden(self.seed, time.time()))

a = WithHidden(123456)
a.randomize()
  
  
new seed

# Атрибуты, эмулирующие private начинаются с __.
# Они могут использоваться с заданным именем только в классе где заданы.
# Во всех остальных местах используется mangled имя _Class__attribute.
class MyClass:
  __mangled_var = "mangled"
  def func(self):
    print(self.__mangled_var)

class NewClass(MyClass):
  def func(self):
    print(self._MyClass__mangled_var)

inst = MyClass()
inst.func()

new_inst = NewClass()
NewClass().func()

mangled
mangled

Задача для закрепления

Создайте класс, в котором у экземпляра есть переменная с именем b. Создайте 2 экземпляра класса. Присвойте переменной экземпляров разные значения(списки) и выведите на экран.


Создайте модель из жизни. Это может быть бронирование комнаты в отеле, покупка билета в транспортной компании, или простая РПГ. Создайте несколько объектов классов, которые описывают ситуацию Объекты должны содержать как атрибуты так и методы класса для симуляции различных действий. Программа должна инстанцировать объекты и эмулировать какую-либо ситуацию — вызывать методы, взаимодействие объектов и т.д.


Magic methods


Теоретическая часть

Магические методы в Python, который является объектно-ориентированным языком, являются одной из важнейших концепций.

Это специальные методы, которые будут вызываться неявно в тех или иных ситуациях. Они всегда начинаются и заканчиваются символом двойного подчеркивания __.

Рассмотрим несколько категорий магических методов:

  • Методы, реализующие жизненный цикл объекта. (__init____del____new__).
  • Методы, реализующие сравнение объектов. (__cmp____eq____ne____lt__ и др.).
  • Методы, реализующие стандартные операторы языка, например +- и т.д. Они работают не только для чисел, а для абсолютно любых объектов. (__add____div____pow__ и др.).
  • Методы, реализующие составные присваивания, например +=-=. (__iadd____imul____idiv__ и др.).
  • Методы, неявно вызывающиеся при передачи объекта во встроенные функции. (__sizeof____dir____hash__ и др.).
  • Методы, осуществляющие контроль доступа к атрибутам класса. (__getattr____setattr____delattr____getattribute__).
  • Методы, реализующие определенные типы объектов: контекстный менеджер, итерируемый объект.

Это далеко не полный перечень существующих магических методов. Вообще, описание магических методов достаточно сильно разбросано по документации. Неплохая попытка их систематизировать находится здесь(исходники): https://github.com/RafeKettler/magicmethods. А собранное в файл это творение можно найти здесь:
https://rszalski.github.io/magicmethods/.


Реализация контекстного менеджера

Для реализации контекстного менеджера(создания класса, который может работать как контекстный менеджер), необходима реализация двух magic методов:

  • __enter__ — выполняется при входе в контекстный менеджер. Объект, возвращенный из метода __enter__ будет присвоен переменной, стоящей после as в блоке with.
  • __exit__ — выполняется при выходе из контекстного менеджера. Выполняться он будет в любом случае, если выход произошел из-за исключения, тип, значение и traceback соответствующего исключения будут переданы в соответствующие переменные.
class CM:
    def __enter__(self):
      pass

    def __exit__(self, exc_type, exc_val, exc_tb):
      pass

Кроме того, возможна реализация контекстного менеджера с помощью contextlib, которая с помощью декоратора contextmanager может создать контекстный менеджер из функции-генератора.


Реализация итерируемых объектов и итератора

Реализация протокола итерирования в Python имеет некоторые отличия от паттерна, описанного в банде четырех.

Протокол итерирования подразумевает наличие двух объектов: итерируемого объекта и итератора.

Итерируемый объект — объект, который содержит коллекцию, по которой мы будем итерироваться.

Для реализации итерируемого объекта необходимо реализовать magic метод:

  • __iter__ — этот метод должен возвращать объект итератора.

Итератор — объект, который осуществляет обход коллекции итерируемого объекта. Для реализации итератора в Python необходимо реализовать следующие методы:

  • __next__ — метод, задача которого вернуть следующий объект из коллекции, а в случае если обход завершен, возбудить исключение StopIteration.
  • __iter__ — метод, который является отличным от классической реализации итератора. Необходимо, чтобы он возвращал сам себя, т.е. self. Тем не менее в большинстве реализаций Python он является не обязательным, и почти все будет работать без него.

Обоснование существования метода __iter__ непосредственно в итераторе обусловлено требованием, чтобы итератор можно было передать в цикл for или использовать с оператором in. Т.е. мы сначала создаем итератор через метод iter, возможно даже ходим по нему, а потом передаем в for. Насколько это критичный функционал, судить Вам.

Подробнее про протокол итерирования в Python:


Code Snippets


Базовые примеры использования Magic методов


# Давайте посмотрим что есть внутри самого обычного типа данных. 
dir(int)

#p.s очень длинные выводы в примеры кода не вставляю ибо читать будет невозможно. Пробуйте код вводить в PyCharm

# Самый простой пример реализации magic метода. 
# Пускай наш класс нормально работает с командой принт.

class MyClass: 
  def __repr__(self):
    return "MyClass()"
  
  def __str__(self):
    return "MyClass example object."

inst = MyClass()
print(inst)  # как видно, при работе print просто вызывается функция str
print(str(inst))
print(repr(inst))

MyClass example object.
MyClass example object.
MyClass()

Фактически, str и repr оба преобразуют объект в строку.

На уровне джентльменского соглашения, считается, что str используется для человеко-читаемого представления объекта, а repr — для представления объекта, который может воссоздать сам объект.


# Тем не менее, если мы реализуем класс только с __repr__, этот метод будет
# использоваться во всех случаях.

class MyClass: 
  def __repr__(self):
    return "MyClass()"

inst = MyClass()
print(inst)
print(str(inst))
print(repr(inst))
MyClass()
MyClass()
MyClass()

# Класс, который хранит определенное значение и может прибавлять к нему любой
# объект с помощью оператора +=. Объекты преобразуются в строки, поэтому
# добавить мы можем что угодно
class A:
  def __init__(self, value=''):
    self._value = str(value)
    
  def __iadd__(self, other):
    self._value += str(other)
    # Так как iadd это +=, то после сложения происходит присвоение объекта. 
    # В нашем случае мы храним значение внутри объекта, и можем вернуть self. 
    return self  
  
  def __str__(self):
    return self._value

inst = A("mystr")
inst += 10000
print(inst)

def f():
  pass

inst += f
print('После добавления функции:', inst)
mystr10000
После добавления функции: mystr10000<function f at 0x7f8df1926f70>

# Тем не менее складывать такие объекты обычным сложением мы не сможем.
class A:
  def __init__(self, value):
    self._value = str(value)
    
  def __iadd__(self, other):
    self._value += str(other)
    # Так как iadd это +=, то после сложения происходит присвоение объекта. 
    # В нашем случае мы храним значение внутри объекта, и можем вернуть self. 
    return self  
  
  def __str__(self):
    return self._value

my_sum = A('1') + A('2')

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-7-d0a2bc936e8b> in <module>()
     13     return self._value
     14 
---> 15 my_sum = A('1') + A('2')

TypeError: unsupported operand type(s) for +: 'A' and 'A'

# Добавим поддержку сложения.
class A:
  def __init__(self, value):
    self.value = str(value)

  def __add__(self, other):
    self.value += str(other)
    return self
  
  def __iadd__(self, other):
    self.value += str(other)
    return self

  def __str__(self):
    return self.value

inst = A("mystr")
inst = inst + '_new_str'
print(inst, type(inst))

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-9-746a8c16a47b> in <module>()
     17 inst = A("mystr")
     18 # inst = inst + '_new_str'
---> 19 inst = '_new_str' + inst
     20 print(inst, type(inst))

TypeError: can only concatenate str (not "A") to str

# Нюанс прошлого примера - для работы, наш класс должен стоять левым операндом
# при сложении. Сейчас сделаем класс который может стоять только справа.
class A:
  def __init__(self, value):
    self.value = str(value)

  def __radd__(self, other):
    self.value += str(other)
    return self
  
  def __iadd__(self, other):
    self.value += str(other)
    return self

  def __str__(self):
    return self.value

inst = A("aa")
inst = '_new_str' + inst
print(inst, type(inst))
aa_new_str <class '__main__.A'>

# Мы можем точно также переопределять поведение стандартных объектов.
# Здесь мы создадим наследника int, с весьма специфическим умножением.
class MyInt(int): 
  def __mul__(self, other):
    return MyInt(   int(self)*2   )

a = MyInt(5)
print(a, type(a))
print(a*6, type(a*6))
5 <class '__main__.MyInt'>
10 <class '__main__.MyInt'>

"ПОЛИМОРФИЗМ"
a = 1
a += 1 # 2
a += 1.0 # 3.0

1 + 1  # 2
1 + 1.0 # 2.0

инт.__add__(инт)
инт.__add__(флоат)

NameError                                 Traceback (most recent call last)
<ipython-input-21-9d57aa8a6819> in <module>
      7 1 + 1.0 # 2.0
      8 
----> 9 инт.__add__(инт)
     10 инт.__add__(флоат)

NameError: name 'инт' is not defined

[20]

0 сек.

"НЕ полиморфизм!!!!!!!!!!"
1+1=2
'1'+'1'='11'
[1]+[1]=[1,1]
инт.__add__(инт)
стр.__add__(стр)
лист.__add__(лист)

  File "<ipython-input-20-90ad8228dbfc>", line 2
    1+1=2
    ^
SyntaxError: cannot assign to operator

Контекстный менеджер


# Реализация простейшего контекстного менеджера.
class CM():
  def __init__(self):
    print('init')

  def __enter__(self):
    print('in cm')
    return 'cm_instance'
  
  # не будем обрабатывать исключения, поэтому просто свернем параметры в exit
  def __exit__(self, *args):  
    print(args)

with CM() as obj:
  print('obj:', obj)
  print('inside cm')
init
in cm
obj: cm_instance
inside cm
(None, None, None)

# Реализация простейшего контекстного менеджера.
class CM():
  def __init__(self):
    print('init')

  def __enter__(self):
    print('in cm')
    return 'cm_instance'
  
  # не будем обрабатывать исключения, поэтому просто свернем параметры в exit
  def __exit__(self, *args):  
    print(args)

with CM() as obj:
  raise Exception  # обратите внимание что блок exit выполнился

init
in cm
(<class 'Exception'>, Exception(), <traceback object at 0x7ff22530c550>)
---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
<ipython-input-17-cec083719bc2> in <module>()
     13 
     14 with CM() as obj:
---> 15   raise Exception  # обратите внимание что блок exit выполнился

Exception: 

Протокол итерирования


class Iterable:
  def __init__(self, collection=None):
    if collection is None:
      self._collection = [1, 2, 3, 4]
    else:
      self._collection = collection
  
  def __iter__(self):
    return Iterator(self._collection)

class Iterator:

  def __init__(self, collection):
    print('init')
    self.collection = collection[::2]
    self.cursor = 0
    
  def __iter__(self):
    return self
  
  def __next__(self):  # Здесь очень хочется сделать генератор, но так нельзя
    if self.cursor >= len(self.collection):
      raise StopIteration
    element = self.collection[self.cursor]
    self.cursor +=1
    return element

for element in Iterable():
  print(element)
init
1
3

def f(arg):
  pass

# for el in iterable:
#   f(el) 


iterator = iter(iterable)
while True:
  try:
    el = next(iterator)
    f(el)
  except StopIteration:
    break
NameError    
                             Traceback (most recent call last)
<ipython-input-22-065185b244c8> in <module>
      6 
      7 
----> 8 iterator = iter(iterable)
      9 while True:
     10   try:

NameError: name 'iterable' is not defined

Композиция. Агрегация


Теоретическая часть

Кроме уже известного нам наследования, существуют также и другие способы организации классов в нашей программе.

Однако, классы могут быть организованы так, что просто будут использовать функционал других классов. Такие отношения называются композицией и агрегацией.

Агрегация

Агрегация — отношение, которое подразумевает что один класс может использовать функционал другого. Например:

class Student:
  pass

class Teacher:
  pass

class Group:
  def __init__(self, teacher, *students):
    self.teacher = teacher
    self.students = list(students)

Group(Teacher(), *(Student() for _ in range(15)))

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

Композиция

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

class _Tail:
  pass

class _Head:
  pass

class Tadpole:
  def __init__:
    self.tail = _Tail()
    self.head = _Head()

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

Кроме того, в Python, определения этих классов можно включить в определение класса головастика с _, что с большей вероятностью исключит использование хвоста головастика у каких-то других животных, при этом в наследниках головастика(различных видов лягушек), мы сможем переиспользовать и даже уточнять поведение хвоста через наследование.

Подпишитесь на рассылку

Если это было вам полезно — вы можете сказать нам спасибо!