ООП Python Классы Создавать классы в Python class SomeClass(object): # поля и методы класса SomeClass Классы-родители перечисляются в скобках через запятую: class SomeClass(ParentClass1, ParentClass2, …): # поля и методы класса SomeClass Свойства классов устанавливаются с помощью простого присваивания: class SomeClass(object): attr1 = 42 attr2 = "Hello, World" Методы объявляются как простые функции: class SomeClass(object): def method1(self, x): # код метода Обратите внимание на первый аргумент – self – общепринятое имя для ссылки на объект, в контексте которого вызывается метод. Этот параметр обязателен и отличает метод класса от обычной функции. Все пользовательские атрибуты сохраняются в атрибуте __dict__, который является словарем. Экземпляры классов Инстанцировать класс в Python тоже очень просто: class SomeClass(object): attr1 = 42 def method1(self, x): return 2*x obj = SomeClass() obj.method1(6) # 12 obj.attr1 # 42 Можно создавать разные инстансы одного класса с заранее заданными параметрами с помощью инициализатора (специальный метод __init__). Для примера возьмем класс Point (точка пространства), объекты которого должны иметь определенные координаты: class Point(object): def __init__(self, x, y, z): self.coord = (x, y, z) p = Point(13, 14, 15) p.coord # (13, 14, 15) Подробнее о других специальных методах жизненного цикла объектов поговорим чуть ниже. Динамическое изменение Можно обойтись даже без определения атрибутов и методов: class SomeClass(object): pass Кажется, этот класс совершенно бесполезен? Отнюдь. Классы в Python могут динамически изменяться после определения: class SomeClass(object): pass def squareMethod(self, x): return x*x SomeClass.square = squareMethod obj = SomeClass() obj.square(5) # 25 Статические и классовые методы Для создания статических методов в Python предназначен декоратор @staticmethod. У них нет обязательных параметров-ссылок вроде self. Доступ к таким методам можно получить как из экземпляра класса, так и из самого класса: class SomeClass(object): @staticmethod def hello(): print("Hello, world") SomeClass.hello() # Hello, world obj = SomeClass() obj.hello() # Hello, world Еще есть так называемые методы классов. Они аналогичны методам экземпляров, но выполняются не в контексте объекта, а в контексте самого класса (классы – это тоже объекты). Такие методы создаются с помощью декоратора @classmethod и требуют обязательную ссылку на класс (cls). class SomeClass(object): @classmethod def hello(cls): print('Hello, класс {}'.format(cls.__name__)) SomeClass.hello() # Hello, класс SomeClass Статические и классовые методы доступны без инстанцирования. Специальные методы Жизненный цикл объекта С инициализатором объектов __init__ вы уже знакомы. Кроме него есть еще и метод __new__, который непосредственно создает новый экземпляр класса. Первым параметром он принимает ссылку на сам класс: class SomeClass(object): def __new__(cls): print("new") return super(SomeClass, cls).__new__(cls) def __init__(self): print("init") obj = SomeClass(); # new # init Это обсуждение на stackoverflow поможет лучше разобраться с инстанцированием классов. Метод __new__ может быть очень полезен для решения ряда задач, например, создания иммутабельных объектов или реализации паттерна Синглтон: class Singleton(object): obj = None # единственный экземпляр класса def __new__(cls, *args, **kwargs): if cls.obj is None: cls.obj = object.__new__(cls, *args, **kwargs) return cls.obj single = Singleton() single.attr = 42 newSingle = Singleton() newSingle.attr # 42 newSingle is single # true В Python вы можете поучаствовать не только в создании объекта, но и в его удалении. Специально для этого предназначен метод-деструктор __del__. class SomeClass(object): def __init__(self, name): self.name = name def __del__(self): print('удаляется объект {} класса SomeClass'.format(self.name)) obj = SomeClass("John"); del obj # удаляется объект John класса SomeClass На практике деструктор используется редко, в основном для тех ресурсов, которые требуют явного освобождения памяти при удалении объекта. Не следует совершать в нем сложные вычисления. Объект как функция Объект класса может имитировать стандартную функцию, то есть при желании его можно "вызвать" с параметрами. За эту возможность отвечает специальный метод __call__: class Multiplier: def __call__(self, x, y): return x*y multiply = Multiplier() multiply(19, 19) # 361 # то же самое multiply.__call__(19, 19) # 361 Имитация контейнеров Вы знакомы с функцией len(), которая умеет вычислять длину списков значений? list = ['hello', 'world'] len(list) # 2 Но для объектов вашего пользовательского класса это не пройдет: class Collection: def __init__(self, list): self.list = list collection = Collection(list) len(collection) Этот код выдаст ошибку object of type 'Collection' has no len(). Интерпретатор просто не понимает, как ему посчитать длину collection. Решить эту проблему поможет специальный метод __len__: class Collection: def __init__(self, list): self.list = list def __len__(self): return len(self.list) collection = Collection([1, 2, 3]) len(collection) # 3 Можно работать с объектом как с коллекцией значений, определив для него интерфейс классического списка с помощью специальных методов: ● ● ● ● __getItem__ – реализация синтаксиса obj[key], получение значения по ключу; __setItem__ – установка значения для ключа; __delItem__ – удаление значения; __contains__ – проверка наличия значения. Имитация числовых типов Ваши объекты могут участвовать в математических операциях, если у них определены специальные методы. Например, __mul__ позволяет умножать объект на число по определенной программистом логике: class SomeClass: def __init__(self, value): self.value = value def __mul__(self, number): return self.value*number obj = SomeClass(42) print(obj * 100) # 4200 Другие специальные методы В Python существует огромное количество специальных методов, расширяющих возможности пользовательских классов. Например, можно определить вид объекта на печати, его "официальное" строковое представление или поведение при сравнениях. Узнать о них подробнее вы можете в официальной документации языка. Эти методы могут эмулировать поведение встроенных классов, но при этом они необязательно существуют у самих встроенных классов. Например, у объектов int при сложении не вызывается метод __add__. Таким образом, их нельзя переопределить. Принципы ООП на Python Рассмотрим "большую тройку" объектно-ориентированной концепции: инкапсуляцию, полиморфизм и наследование. Инкапсуляция Все объекты в Python инкапсулируют внутри себя данные и методы работы с ними, предоставляя публичные интерфейсы для взаимодействия. Атрибут может быть объявлен приватным (внутренним) с помощью нижнего подчеркивания перед именем, но настоящего скрытия на самом деле не происходит – все на уровне соглашений. class SomeClass: def _private(self): print("Это внутренний метод объекта") obj = SomeClass() obj._private() # это внутренний метод объекта Если поставить перед именем атрибута два подчеркивания, к нему нельзя будет обратиться напрямую. Но все равно остается обходной путь: class SomeClass(): def __init__(self): self.__param = 42 # защищенный атрибут obj = SomeClass() obj.__param # AttributeError: 'SomeClass' object has no attribute '__param' obj._SomeClass__param # 42 Специальные свойства и методы класса, некоторые из которых вам уже знакомы, имеют двойные подчеркивания до и после имени. Кроме прямого доступа к атрибутам (obj.attrName), могут быть использованы специальные методы доступа (геттеры, сеттеры и деструкторы): class SomeClass(): def __init__(self, value): self._value = value def getvalue(self): # получение значения атрибута return self._value def setvalue(self, value): # установка значения атрибута self._value = value def delvalue(self): # удаление атрибута del self._value value = property(getvalue, setvalue, delvalue, "Свойство value") obj = SomeClass(42) print(obj.value) obj.value = 43 Такой подход очень удобен, если получение или установка значения атрибута требует сложной логики. Вместо того чтобы вручную создавать геттеры и сеттеры для каждого атрибута, можно перегрузить встроенные методы __getattr__, __setattr__ и __delattr__. Например, так можно перехватить обращение к свойствам и методам, которых в объекте не существует: class SomeClass(): attr1 = 42 def __getattr__(self, attr): return attr.upper() obj = SomeClass() obj.attr1 # 42    obj.attr2 # ATTR2 __getattribute__ перехватывает все обращения (в том числе и к существующим атрибутам): class SomeClass(): attr1 = 42 def __getattribute__(self, attr): return attr.upper() obj = SomeClass() obj.attr1 # ATTR1 obj.attr2 # ATTR2 Таким образом, можно организовать динамический доступ к методам и свойствам объекта, как действуют, например, RPC-системы. Наследование Язык программирования Python реализует как стандартное одиночное наследование: class Mammal(): className = 'Mammal' class Dog(Mammal): species = 'Canis lupus' dog = Dog() dog.className # Mammal так и множественное: class Horse(): isHorse = True class Donkey(): isDonkey = True class Mule(Horse, Donkey): mule = Mule() mule.isHorse # True mule.isDonkey # True Используя множественное наследования можно создавать классы-миксины (примеси), представляющие собой определенную особенность поведения. Такой микси можно "примешать" к любому классу. Ассоциация Кроме наследования, существует и другой способ организации межклассового взаимодействия – ассоциация (агрегация или композиция), при которой один класс является полем другого. Пример композиции: class Salary: def __init__(self,pay): self.pay = pay def getTotal(self): return (self.pay*12) class Employee: def __init__(self,pay,bonus): self.pay = pay self.bonus = bonus self.salary = Salary(self.pay) def annualSalary(self): return "Total: " + str(self.salary.getTotal() + self.bonus) employee = Employee(100,10) print(employee.annualSalary()) Пример агрегации: class Salary(object): def __init__(self, pay): self.pay = pay def getTotal(self): return (self.pay * 12) class Employee(object): def __init__(self, pay, bonus): self.pay = pay self.bonus = bonus def annualSalary(self): return "Total: " + str(self.pay.getTotal() + self.bonus) salary = Salary(100) employee = Employee(salary, 10) print(employee.annualSalary()) Ассоциированные объекты могут циклически ссылаться друг на друга, что ломает стандартный механизм сборки мусора. Избежать подобных проблем при ассоциации помогают слабые ссылки (модуль weakref). Полиморфизм Концепция полиморфизма – важная часть ООП на Python. Все методы в языке изначально виртуальные. Это значит, что дочерние классы могут их переопределять и решать одну и ту же задачу разными путями, а конкретная реализация будет выбрана только во время исполнения программы. Такие классы называют полиморфными. class Mammal: def move(self): print('Двигается') class Hare(Mammal): def move(self): print('Прыгает') animal = Mammal() animal.move() # Двигается hare = Hare() hare.move() # Прыгает Впрочем, можно получить и доступ к методам класса-предка либо по прямому обращению, либо с помощью функции super: class Parent(): def __init__(self): print('Parent init') def method(self): print('Parent method') class Child(Parent): def __init__(self): Parent.__init__(self) def method(self): super(Child, self).method() child = Child() # Parent init child. method() # Parent method Одинаковый интерфейс с разной реализацией могут иметь и классы, не связанные родственными узами. В следующем примере код может одинаково удобно работать с классами English и French, так как они обладают одинаковым интерфейсом: class English: def greeting(self): print ("Hello") class French: def greeting(self): print ("Bonjour") def intro(language): language.greeting() john = English() gerard = French() intro(john) # Hello intro(gerard) # Bonjour Это возможно благодаря утиной типизации. Множественная диспетчеризация Виртуальные методы обеспечивают одиночную диспетчеризацию, при которой выбор конкретного метода зависит от объекта, в контексте которого произошел вызов. Мультиметоды позволяют выбирать нужную функциональность в зависимости от количества, типов или значений аргументов. Программирование на Python не поддерживает мультиметоды из коробки, поэтому для их реализации необходимо подключать сторонние Python библиотеки, например, multimethods.py. Порядок разрешения доступа к атрибутам Складывается достаточно интересная картина: у одного объекта может быть несколько родительских классов, а также специальные методы вроде __getattribute__, которые перехватывают запросы к атрибутам. Каким же образом интерпретатор разрешает сложные запросы к свойствам и методам? Рассмотрим последовательность поиска на примере запроса obj.field: 1. Вызов obj.__getattribute__('field'), если он определен. При установке или удалении атрибута проверяется соответственно наличие __setattr__ или __delattr__. 2. Поиск в obj.__dict__ (пользовательские атрибуты). 3. Поиск в object.__class__.__slots__. 4. Рекурсивный поиск в поле __dict__ всех родительских классов. Если класс имеет несколько предков, порядок проверки соответствует порядку их перечисления в определении. 5. Если определен метод __getattr__, то происходит вызов obj.__getattr__('field') 6. Выбрасывается исключение несуществующего атрибута – AttributeError. Наконец, когда атрибут нашелся, проверяется наличие метода __get__ (при установке – __set__, при удалении – __delete__). Все эти проверки совершаются только для пользовательских атрибутов. Метаклассы Метаклассы – это классы, инстансы которых тоже являются классами. class MetaClass(type): # выделение памяти для класса def __new__(cls, name, bases, dict): print("Создание нового класса {}".format(name)) return type.__new__(cls, name, bases, dict) # инициализация класса def __init__(cls, name, bases, dict): print("Инициализация нового класса {}".format(name)) return super(MetaClass, cls).__init__(name, bases, dict) # порождение класса на основе метакласса SomeClass = MetaClass("SomeClass", (), {}) # обычное наследование class Child(SomeClass): def __init__(self, param): print(param) # получение экземпляра класса obj = Child("Hello") Узнайте больше о метаклассах в Python из нашего материала. ООП на Python Подведем краткий итог всему вышесказанному и выделим основные особенности реализации ООП на Python: ● ● ● ● ● ● ● ● ● Классы в Python – это тоже объекты. Допустимо динамическое изменение и добавление атрибутов классов. Жизненным циклом объекта можно управлять. Многие операторы могут быть перезагружены. Многие методы встроенных объектов можно эмулировать. Для скрытия внутренних данных используются синтаксические соглашения. Поддерживается наследование. Полиморфизм обеспечивается виртуальностью всех методов. Доступно метапрограммирование.