Теоретическая часть
Фактически, в 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:
- https://www.python.org/dev/peps/pep-0234/ — PEP.
- https://docs.python.org/dev/library/stdtypes.html#iterator-types — официальная документация 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, определения этих классов можно включить в определение класса головастика с _
, что с большей вероятностью исключит использование хвоста головастика у каких-то других животных, при этом в наследниках головастика(различных видов лягушек), мы сможем переиспользовать и даже уточнять поведение хвоста через наследование.
Подпишитесь на рассылку