Объектно-ориентированное программирование на Python
Python – это новый язык программирования. Он был разработан в 1991 году программистом Гвидо ванн Россумом. В отличие от многих других языков, в Python все сущности являются объектами. Также язык имеет динамическую типизацию и автоматическое управление памятью. Авторы языка старались сделать его максимально удобным для разработчиков, поэтому синтаксис языка довольно минимальный. К недостаткам Python можно отнести низкую скорость работы и большое потребление памяти в отличие от компилируемых языков.
Сегодня Python занимает 3 место в рейтинге TIOBE среди самых популярных языков мира. Его используют в разных сферах: от машинного обучения и Big data до разработки игр и приложений. Благодаря простому синтаксису язык используют в процессе обучения программированию. Python позволяет сосредоточиться на изучении алгоритмов и принципов программирования, а не на изучении синтаксиса.
Принципы объектно-ориентированного программирования
Программист, использующий Python в своих проектах может пользоваться разными методологиями и парадигмами программирования. Но в основе языка лежит именно объектный подход. В виде объектов здесь представлено всё, даже простые числа – это объекты.
Принципы ООП, реализованные в Python:
- Всё, что есть в языке – это объекты, даже простые числа представлены в виде объектов. Объект имеет определённый тип, по-другому – класс, реализующий логику объекта.
- Помимо объектов, в ООП есть ещё и классы. Они обозначаются зарезервированным словом – class и представляют собой абстрактный набор методов и полей.
- Внутри каждого объекта находиться слоя логика, которая позволяет как-то общаться с этим объектом. Такой принцип называется инкапсуляция. В рамках инкапсуляции программист может ограничить доступ к логике объекта.
- Все объекты, созданные программистом, должны общаться между собой.
- Объект может включать в себе другой объект, представленный в виде поля.
- Одна из базовых концепций ООП – наследование. Оно позволяет устанавливать отношение родитель-потомок между объектами.
- Полиморфизм – вторая концепция ООП – позволяет объекту самому определять реализацию методов.
- Инкапсуляция – ограничивает доступ к данным.
Основы ООП
Чтобы успешно писать код на Python придётся разобраться с концепцией объектно-ориентированного программирования.
Классы
Классы представляют абстрактную структуру, которая содержит в себе логику для объекта. Пример класса:
Для создания класса указывается зарезервированное слово class, после которого указывается имя. В скобках можно указать родительские классы. В данном примере мы указываем класс object в качестве родителя. Python поддерживает множественное наследование.
Пример класса с наследованием:
Каждый класс имеет свойства и методы. Они задаются после «:». Пример класса со свойствами:
Непривычной особенностью языка для многих программистов является то, что свойству не нужно указывать тип. Компилятор языка сам разберётся с типизацией.
Помимо свойств, класс может иметь и методы. Они задаются как обычная функция, но с одним отличием. В качестве первого параметра методу обязательно нужно передать аргумент self. Пример класса с методом:
После «:» указывается код метода.
Объекты класса
Объект класса (экземпляр) – это конкретная реализация класса. В Python работать с экземплярами довольно просто. Пример:
classSomeClass(object):
defSomeMethod(self, x):
returnx*x
Для того, чтобы создать объект этого класса, нужно:
someObj = SomeClass()
y = someObj.SomeMethod(6)
print(y)
В результате работы программы на экран выведется число 36.
Чтобы создать объект, нужно создать переменную и присвоить ей имя класса. Далее проиллюстрирован механизм доступа к членам объекта при присваивании значения переменной y. С помощью точки можно не только вызывать методы и функции, но и обращаться к полям объекта.
В Python есть возможность создать объект с некоторым набором параметров. Пример:
class Point(object):
Def __init__(self, x, y, z):
self.coord(x, y, z)
p1 = Point(13, 14, 15)
В этом примере мы создали класс Point, который будет хранить в себе 3 координаты. Затем, создали экземпляр этого класс p1. Этот экземпляр представляет конкретную точку с координатами 13, 14 и 15. Обратиться к этим координатам можно так: p1.coord
Динамический изменяемые классы
В отличии от многих других языков, где класс – это неизменяемая структура, в Python программист может добавлять методы в класс прямо во время его работы. Для этого используется зарезервированное слово pass. Вот так создаётся динамический класс:
Подобный класс абсолютно бесполезен, пока мы не добавим в него какую-нибудь логику. Пример работы с динамическим классом:
classSomeClass(object) :
pass
def method(self, x) :
return x*x
SomeClass.square = method
someObj = SomeClass
someObj.square(5)
Результат работы программы: 25. В этом примере мы дополняем класс новым методом, который будет доступен из любых объектов.
Статические и классовые методы
Статические методы используются во многих языках программирования. Внешне они похожи на другие методы, но с одним отличием. Вызывать статические методы можно как из класса, так и из его экземпляра. Для того чтобы их создать, нужно воспользоваться специальным декоратором: @staticmethod. Пример:
classSomeClass(object) :
@staticmethod
def Hello() :
print(‘Hello’)
SomeClass.Hello() #В результате вызовется метод Hello класса SomeClass
someObj = SomeClass()
someObj.Hello() #В этой строке мы обращаемся к методу Hello, но не класса, а экземпляра
В результате работы программы на экране дважды выведется надпись Hello.
Ещё существуют классовые методы, которые выполняются в контексте класса. Чтобы создать классовый метод, нужно указать декоратор @classmethod. Важной особенностью таких методов является обязательный параметр-ссылка на класс – cls. Пример:
classSomeClass(object) :
@classmethod
def Hello(cls) :
print(‘Hello, класс<>’.format(cls.__name__))
SomeClass.Hello()
В результате на кран выведется «Hello, класс SomeClass». Программистам-новичкам не всегда бывает понятно применение классовых методов. Самый яркий пример использования – фабрика объектов.
Объекты и их методы
В Python существует довольно много специальных методов, например__init__. Эти методы введены в язык для того, чтобы управлять жизнью объекта.
Жизнь объекта
Жизненный цикл объекта включат 3 этапа — создание, работа и удаление их из памяти.Всеми этапами программист может управлять. Например, от разработчика зависит, когда объект будет проинициализирован. Для этого можно использовать метод __init__ либо __new__
Пример использования инициализатора __init__ и __new__:
classSomeClass(object):
def __new__(cls):
print(‘new’)
return super(SomeClass, cls).__new__(cls)
def __init__(self):
print(‘init’)
obj = SomeClass();
В результате работы программы выведется new init.
На первый взгляд,__new__ может показаться бесполезным, но это не так. Метод полезен, например, при реализации паттерна «Одиночка». Пример реализации:
classSingleton(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
newSingleissingle # true
Программист может не только создавать объекты, но и удалять их. Для этого существует метод-деструктор.
Пример с удалением объекта:
classSomeClass(object):
def __init__(self, name):
self.name = name
def __del__(self):
print(‘удаляется объект <> классаSomeClass’.format(self.name))
obj = SomeClass(«John»);
delobj
Результат работы программы: удаляется объект John класса SomeClass. Однако, часто использовать деструкторы – не очень хорошо. Python сам определяет ненужные объекты и удаляет их.
Объект в виде функции
Существует специальный метод __call__, который позволяет вызывать объект точно так же, как и функцию. Пример:
class Multiplier:
def __call__(self, x, y):
return x*y
multiply = Multiplier()
multiply(19, 19) # 361
multiply.__call__(19, 19) # то же самое, что и в предыдущей строке
Имитация контейнера
Многие знакомы с функцией len(). Она позволяет узнать длину списка из каких-то объектов, но это работает только для просты типов. Пример:
class Collection:
def __init__(self, list):
self.list = list
collection = Collection(list)
len(collection)#в этой строке возникнет ошибка «Object of type has no len()»
Это означает, что интерпретатор Python просто не понимает, как ему считать длину. Для решения этой проблемы, был придуман метод __len__. Пример без ошибки:
class Collection:
def __init__(self, list):
self.list = list
def __len__(self):
returnlen(self.list)
collection = Collection([1, 2, 3])
len(collection) # 3
Другие специальные методы
Помимо описанных выше, в Python определено большое количество разнообразных методов. Разработчик может изменить вид объекта при печати, определить, как конкретный объект будет преобразовываться в строку, изменить способ сравнения нескольких экземпляров и много другое. Этих методов очень много, подробно они описаны в документации языка.
Проблема с доступом к атрибутам
Объект, у которого есть несколько родительских классов, будет также иметь несколько __getattribute__ и других специальных методов. Как в таком случае компилятор обрабатывает запросы к специальным методам?
Для того чтобы разобраться с этим вопросом, рассмотрим запрос obj.field.
- В начале, компилятор вызывает специальный метод, пусть в нашем случае это будет __getattribute__(field). В качестве параметра он принимает field
- Затем, компилятор читает все пользовательские атрибуты, которые записаны в __dict__ .
- Если второй шаг не дал результата, то компилятор пытается найти атрибут в obj.__class__.__slots__
- С помощью рекурсии атрибут ищется у родительских классов в поле __dict__. Если объект имеет не одного родителя, то порядок поиска соответствующего атрибута такой же, как и порядок определения родительских классов.
- Вызывается метод __getattr__, если он определён.
- Если предыдущие шаги не дали никакого результата, то компилятор выбрасывает исключение: AttributeError, которое говорит он том, что нужного атрибута нет.
Если атрибут всё-таки был найден, то вызывается метод __get__, __set__ или __del__.
Принципы ООП в Python
Существует 3 основных концепции работы с ООП: инкапсуляция, наследование и полиморфизм. В разных языках они реализуются по-разному.
Инкапсуляция
Инкапсуляция – это механизм предоставления доступа к данным объекта. Например, любой атрибут мы можем объявить приватным (то есть не доступным для кода вне класса) с помощью нижнего подчёркивания.
classSomeClass(object) :
def _Hello(sefl) : #Перед именем метода стоить «_», то есть метод приватный
print(‘Hello’)
obj = SomeClass()
obj._Hello()#Метод на самом деле не приватный
В данном случае мы можем вызвать метод Hello у объекта, так как на самом деле он не является приватным. Ограничение доступа происходит лишь на уровне соглашения между разработчиками. Если другой программист Python увидит метод, название которого начинается с нижнего подчёркивания, то он будет знать, что этот метод приватный и вызывать его не стоит.
Существует возможность полностью ограничить доступ к методу. Делается это с помощью двух нижних подчёркиваний. Пример:
classSomeClass(object) :
def __Hello(sefl) :
print(‘Hello’)
obj = SomeClass()
obj.__Hello() #метод недоступен
Механизм инкапсуляции также включает другие специальные методы: геттеры, сеттеры и деструкторы. Они нужны для доступа к атрибутам объекта.
Пример работы со специальными методами:
classSomeClass():
def __init__(self, value):
self._value = value
defgetvalue(self): # получение значения атрибута
returnself._value
defsetvalue(self, value): # установка значения атрибута
self._value = value
defdelvalue(self): # удаление атрибута
delself._value
value = property(getvalue, setvalue, delvalue, «Свойство value»)
В данном примере показаны три специальных метода: setvalue–используется для присваивания значения свойству value, getvalue–используется для чтения и delvalue–деструктор, которые удаляет свойство.
Наследование
Механизм наследования позволят устанавливать между классами отношение родитель-потомок. В дочернем классе будут доступны все поля и методы родительского класса.
Пример одиночного наследования:
classMammal():
className = ‘Mammal’
class Dog(Mammal):
species = ‘Canis lupus’
dog = Dog()
dog.className#Поле className доступно
В данном примере родительским классом является класс – Mammal, а дочерним – Dog. При этом из объекта dogмы можем вызвать поля и методы родительского класса.
Помимо одиночного, Python поддерживает и множественное наследование. Пример:
class Horse():
isHorse = True
class Donkey():
isDonkey = True
class Mule(Horse, Donkey):
mule = Mule()
mule.isHorse #Поля доступны
mule.isDonkey
Механизм наследования необходим для разделения логики между классами и повторного её использования. Например, есть класс Person, содержащий поле Age и метод Sleep. Также есть класс Pety, в котором доступны поля из Person, но Pety содержит ещё метод Work. Суть такого разделения заключена в том, что каждый человек может спать, но не каждый может работать. Петя может и спать, и работать.
Ассоциация
Если класс имеет поля, которые тоже являются классами, то такой механизм будет называться ассоциацией. Она подразделяется на композицию и агрегацию. Нередко начинающие программисты неправильно используют ассоциацию, из-за чего может возникать утечка памяти.
Пример композиции:
classSalary:
def __init__(self,pay):
self.pay = pay
defgetTotal(self):
return (self.pay*12)
class Employee:
def __init__(self,pay,bonus):
self.pay = pay
self.bonus = bonus
self.salary = Salary(self.pay)
defannualSalary(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
defgetTotal(self):
return (self.pay * 12)
class Employee(object):
def __init__(self, pay, bonus):
self.pay = pay
self.bonus = bonus
defannualSalary(self):
return «Total: » + str(self.pay.getTotal() + self.bonus)
salary = Salary(100)
employee = Employee(salary, 10)
print(employee.annualSalary())
Полиморфизм
Последняя концепция ООП – полиморфизм. Он позволяет объекту самому определять своё поведение. В Python это сделано через виртуальные методы. Дочерний класс может переопределить поведение метода родительского класса. Такой механизм позволяет решить задачу разным путём.
classMammal:
defmove(self):
print(‘Двигается’)
class Hare(Mammal):
def move(self):
print(‘Прыгает’)
animal = Mammal()
animal.move() # Здесь вызывается метод move, класса Mammal
hare = Hare()
hare.move() # А здесь уже вызывается метод класса Hare
Как видно из примера, дочерний класс Hareпереопределил метод move, то есть добавил в него свою логику.
Чтобы получить доступ к методу базового класса, можно использовать зарезервированное слово super.
Пример с доступом к методу базового класса:
class Parent():
def __init__(self):
print(‘Parent init’)
def method1(self):
print(‘Parent method’)
class Child(Parent):
def __init__(self):
Parent.__init__(self)
def method1(self):
super(Child, self).method()
child = Child()
child.method1()
Несмотря на то, что в дочернем классе переопределён method1, вызовется всё равно метод базового класса.
Подобные способы – не единственные варианты реализации полиморфизма. В Python есть так называемая утиная типизация. Она позволяет классам иметь одинаковый интерфейс, но с разной реализацией. Под интерфейсом здесь понимается одинаковое название методов и полей.
Пример:
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
В данном примере реализованы два класса English и French. Оба они содержат метод с одинаковым названием. Также в самой программе прописан метод intro, который в качестве параметра принимает объект language. Не зависимо от типа объекта, который мы передали вintro, у переданного параметра будет вызван метод greeting. Главное, чтобы объект language содержал в себе реализацию метода с таким названием.
Множественная диспетчеризация
Благодаря виртуальным методам программист реализует одиночную диспетчеризацию, но в Python есть возможность использовать и множественную. Этот механизм позволяет выбирать функциональность исходя из количества параметров, типов и аргументов.
Множественная диспетчеризация доступна в Python благодаря мультиметодам. К сожалению, разработчики не добавили поддержку мультиметодов в язык, но существует большое количество библиотек, которые позволяют их использовать. Одна из таких библиотек – multimethods.py.
Метаклассы
Помимо обычных классов, в Python доступны и метаклассы. От обычных они отличаются тем, что в качестве метода-инициализатора (__init__) они используют другой класс.
Пример работы с метаклассами:
classMetaClass(type):
# выделение памяти для класса
def __new__(cls, name, bases, dict):
print(«Создание нового класса <>».format(name))
returntype.__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 имеет ряд особенностей, которые отличают его от других языков. Например, классы – это тоже объект. Инкапсуляция основана лишь на соглашении между разработчиками.
Руководство по магическим методам в Питоне
Это перевод 1.17 версии руководства от Rafe Kettler.
Содержание
Вступление
Что такое магические методы? Они всё в объектно-ориентированном Питоне. Это специальные методы, с помощью которых вы можете добавить в ваши классы «магию». Они всегда обрамлены двумя нижними подчеркиваниями (например, __init__ или __lt__ ). Ещё, они не так хорошо документированны, как хотелось бы. Все магические методы описаны в документации, но весьма беспорядочно и почти безо всякой организации. Поэтому, чтобы исправить то, что я воспринимаю как недостаток документации Питона, я собираюсь предоставить больше информации о магических методах, написанной на понятном языке и обильно снабжённой примерами. Надеюсь, это руководство вам понравится. Используйте его как обучающий материал, памятку или полное описание. Я просто постарался как можно понятнее описать магические методы.
Конструирование и инициализация.
Всем известен самый базовый магический метод, __init__ . С его помощью мы можем инициализировать объект. Однако, когда я пишу x = SomeClass() , __init__ не самое первое, что вызывается. На самом деле, экземпляр объекта создаёт метод __new__ , а затем аргументы передаются в инициализатор. На другом конце жизненного цикла объекта находится метод __del__ . Давайте подробнее рассмотрим эти три магических метода:
-
__new__(cls, [. )
Это первый метод, который будет вызван при инициализации объекта. Он принимает в качестве параметров класс и потом любые другие аргументы, которые будут переданы в __init__ . __new__ используется весьма редко, но иногда бывает полезен, в частности, когда класс наследуется от неизменяемого (immutable) типа, такого как кортеж (tuple) или строка. Я не намерен очень детально останавливаться на __new__ , так как он не то чтобы очень часто нужен, но этот метод очень хорошо и детально описан в документации.
Замечание от переводчика: svetlov отмечает, что здесь автор ошибается, на самом деле __del__ всегда вызывается по завершении работы интерпретатора.
Переопределение операторов на произвольных классах
Одно из больших преимуществ использования магических методов в Питоне то, что они предоставляют простой способ заставить объекты вести себя по подобию встроенных типов. Это означает, что вы можете избежать унылого, нелогичного и нестандартного поведения базовых операторов. В некоторых языках обычное явление писать как-нибудь так:
Вы, конечно, можете поступать так же и в Питоне, но это добавляет путаницы и ненужной многословности. Разные библиотеки могут по разному называть одни и те же операции, заставляя использующего их программиста совершать больше действий, чем необходимо. Используя силу магических методов, мы можем определить нужный метод ( __eq__ , в этом случае), и так точно выразить, что мы имели в виду:
Это одна из сильных сторон магических методов. Подавляющее большинство из них позволяют определить, что будут делать стандартные операторы, так что мы можем использовать операторы на своих классах так, как будто они встроенные типы.
Магические методы сравнения
В Питоне уйма магических методов, созданных для определения интуитивного сравнения между объектами используя операторы, а не неуклюжие методы. Кроме того, они предоставляют способ переопределить поведение Питона по-умолчанию для сравнения объектов (по ссылке). Вот список этих методов и что они делают:
-
__cmp__(self, other)
Самый базовый из методов сравнения. Он, в действительности, определяет поведение для всех операторов сравнения (>, ==, !=, итд.), но не всегда так, как вам это нужно (например, если эквивалентность двух экземпляров определяется по одному критерию, а то что один больше другого по какому-нибудь другому). __cmp__ должен вернуть отрицательное число, если self < other , ноль, если self == other , и положительное число в случае self > other . Но, обычно, лучше определить каждое сравнение, которое вам нужно, чем определять их всех в __cmp__ . Но __cmp__ может быть хорошим способом избежать повторений и увеличить ясность, когда все необходимые сравнения оперерируют одним критерием.
Теперь мы можем создать два Word (при помощи Word(‘foo’) и Word(‘bar’) ) и сравнить их по длине. Заметьте, что мы не определяли __eq__ и __ne__ , так как это приведёт к странному поведению (например, Word(‘foo’) == Word(‘bar’) будет расцениваться как истина). В этом нет смысла при тестировании на эквивалентность, основанную на длине, поэтому мы оставляем стандартную проверку на эквивалентность от str .
Сейчас, кажется, удачное время упомянуть, что вы не должны определять каждый из магических методов сравнения, чтобы полностью охватить все сравнения. Стандартная библиотека любезно предоставляет нам класс-декторатор в модуле functools , который и определит все сравнивающие методы, от вас достаточно определить только __eq__ и ещё один ( __gt__ , __lt__ и т.п.) Эта возможность доступна начиная с 2.7 версии Питона, но если это вас устраивает, вы сэкономите кучу времени и усилий. Для того, чтобы задействовать её, поместите @total_ordering над вашим определением класса.
Числовые магические методы
Точно так же, как вы можете определить, каким образом ваши объекты будут сравниваться операторами сравнения, вы можете определить их поведение для числовых операторов. Приготовтесь, друзья, их много. Для лучшей организации, я разбил числовые магические методы на 5 категорий: унарные операторы, обычные арифметические операторы, отражённые арифметические операторы (подробности позже), составные присваивания и преобразования типов.
Унарные операторы и функции
Унарные операторы и функции имеют только один операнд — отрицание, абсолютное значение, и так далее.
-
__pos__(self)
Определяет поведение для унарного плюса ( +some_object )
Обычные арифметические операторы
Теперь рассмотрим обычные бинарные операторы (и ещё пару функций): +, -, * и похожие. Они, по большей части, отлично сами себя описывают.
-
__add__(self, other)
Сложение.
Отражённые арифметические операторы
Помните как я сказал, что собираюсь остановиться на отражённой арифметике подробнее? Вы могли подумать, что это какая-то большая, страшная и непонятная концепция. На самом деле всё очень просто. Вот пример:
Это «обычное» сложение. Единственное, чем отличается эквивалентное отражённое выражение, это порядок слагаемых:
Таким образом, все эти магические методы делают то же самое, что и их обычные версии, за исключением выполнения операции с other в качестве первого операнда и self в качестве второго. В большинстве случаев, результат отражённой операции такой же, как её обычный эквивалент, поэтому при определении __radd__ вы можете ограничиться вызовом __add__ да и всё. Заметьте, что объект слева от оператора ( other в примере) не должен иметь обычной неотражённой версии этого метода. В нашем примере, some_object.__radd__ будет вызван только если в other не определён __add__ .
-
__radd__(self, other)
Отражённое сложение.
Составное присваивание
В Питоне широко представлены и магические методы для составного присваивания. Вы скорее всего уже знакомы с составным присваиванием, это комбинация «обычного» оператора и присваивания. Если всё ещё непонятно, вот пример:
Каждый из этих методов должен возвращать значение, которое будет присвоено переменной слева (например, для a += b , __iadd__ должен вернуть a + b , что будет присвоено a ). Вот список:
-
__iadd__(self, other)
Сложение с присваиванием.
Магические методы преобразования типов
Кроме того, в Питоне множество магических методов, предназначенных для определния поведения для встроенных функций преобразования типов, таких как float() . Вот они все:
-
__int__(self)
Преобразование типа в int.
Представление своих классов
Часто бывает полезно представление класса в виде строки. В Питоне существует несколько методов, которые вы можете определить для настройки поведения встроенных функций при представлении вашего класса.
-
__str__(self)
Определяет поведение функции str() , вызванной для экземпляра вашего класса.
Контроль доступа к атрибутам
Многие люди, пришедшие в Питон из других языков, жалуются на отсутствие настоящей инкапсуляции для классов (например, нет способа определить приватные атрибуты с публичными методами доступа). Это не совсем правда: просто многие вещи, связанные с инкапсуляцией, Питон реализует через «магию», а не явными модификаторами для методов и полей. Смотрите:
-
__getattr__(self, name)
Вы можете определить поведение для случая, когда пользователь пытается обратиться к атрибуту, который не существует (совсем или пока ещё). Это может быть полезным для перехвата и перенаправления частых опечаток, предупреждения об использовании устаревших атрибутов (вы можете всё-равно вычислить и вернуть этот атрибут, если хотите), или хитро возвращать AttributeError , когда это вам нужно. Правда, этот метод вызывается только когда пытаются получить доступ к несуществующему атрибуту, поэтому это не очень хорошее решение для инкапсуляции.
Ещё раз, мощь магических методов в Питоне невероятна, а с большой силой приходит и большая ответственность. Важно знать, как правильно использовать магические методы, ничего не ломая.
Итак, что мы узнали об управлении доступом к атрибутам? Их не нужно использовать легкомысленно. На самом деле, они имеют склонность к чрезмерной мощи и нелогичности. Причина, по которой они всё-таки существуют, в удволетворении определённого желания: Питон склонен не запрещать плохие штуки полностью, а только усложнять их использование. Свобода первостепенна, поэтому вы на самом деле можете делать всё, что хотите. Вот пример использования методов контроля доступа (заметьте, что мы используем super , так как не все классы имеют атрибут __dict__ ):
Создание произвольных последовательностей
В Питоне существует множество способов заставить ваши классы вести себя как встроенные последовательности (словари, кортежи, списки, строки и так далее). Это, безусловно, мои любимые магические методы, из-за до абсурда высокой степени контроля, которую они дают и той магии, от которой с экземплярами ваших классов вдруг начинает прекрасно работать целое множество глобальных функций. Но, до того как мы перейдём ко всяким хорошим вещам, мы должны знать о протоколах.
Протоколы
Теперь, когда речь зашла о создании собственных последовательностей в Питоне, пришло время поговорить о протоколах. Протоколы немного похожи на интерфейсы в других языках тем, что они предоставляют набор методов, которые вы должны реализовать. Однако, в Питоне протоколы абсолютно ни к чему не обязывают и не требуют обязательно реализовать какое-либо объявление. Наверное, они больше похожи на руководящие указания.
Почему мы заговорили о протоколах? Потому, что реализация произвольных контейнерных типов в Питоне влечёт за собой использование некоторых из них. Во-первых, протокол для определения неизменяемых контейнеров: чтобы создать неизменяемый контейнер, вы должны только определить __len__ и __getitem__ (продробнее о них дальше). Протокол изменяемого контейнера требует того же, что и неизменяемого контейнера, плюс __setitem__ и __delitem__ . И, наконец, если вы хотите, чтобы ваши объекты можно было перебирать итерацией, вы должны определить __iter__ , который возвращает итератор. Этот итератор должен соответствовать протоколу итератора, который требует методов __iter__ (возвращает самого себя) и next .
Магия контейнеров
Без дальнейшего промедления, вот магические методы, используемые контейнерами:
-
__len__(self)
Возвращает количество элементов в контейнере. Часть протоколов для изменяемого и неизменяемого контейнеров.
Пример
Для примера, давайте посмотрим на список, который реализует некоторые функциональные конструкции, которые вы могли встретить в других языках (Хаскеле, например).
Теперь у вас есть полезный (относительно) пример реализации своей собственной последовательности. Существуют, конечно, и куда более практичные реализации произвольных последовательностей, но большое их число уже реализовано в стандартной библиотеке (с батарейками в комплекте, да?), такие как Counter , OrderedDict , NamedTuple .
Отражение
Вы можете контролировать и отражение, использующее встроенные функции isinstance() и issubclass() , определив некоторые магические методы. Вот они:
-
__instancecheck__(self, instance)
Проверяет, является ли экземлпяр членом вашего класса ( isinstance(instance, class) , например.
Вызываемые объекты
Как вы наверное уже знаете, в Питоне функции являются объектами первого класса. Это означает, что они могут быть переданы в функции или методы так же, как любые другие объекты. Это невероятно мощная особенность.
Специальный магический метод позволяет экземплярам вашего класса вести себя так, как будто они функции, тоесть вы сможете «вызывать» их, передавать их в функции, которые принимают функции в качестве аргументов и так далее. Это другая удобная особенность, которая делает программирование на Питоне таким приятным.
-
__call__(self, [args. ])
Позволяет любому экземпляру вашего класса быть вызванным как-будто он функция. Главным образом это означает, что x() означает то же, что и x.__call__() . Заметьте, __call__ принимает произвольное число аргументов; то есть, вы можете определить __call__ так же как любую другую функцию, принимающую столько аргументов, сколько вам нужно.
Менеджеры контекста
В Питоне 2.5 было представлено новое ключевое слово вместе с новым способом повторно использовать код, ключевое слово with . Концепция менеджеров контекста не являлась новой для Питона (она была реализована раньше как часть библиотеки), но в PEP 343 достигла статуса языковой конструкции. Вы могли уже видеть выражения с with :
Менеджеры контекста позволяют выполнить какие-то действия для настройки или очистки, когда создание объекта обёрнуто в оператор with . Поведение менеджера контекста определяется двумя магическими методами:
-
__enter__(self)
Определяет, что должен сделать менеджер контекста в начале блока, созданного оператором with . Заметьте, что возвращаемое __enter__ значение и есть то значение, с которым производится работа внутри with .
Пример использования Closer с FTP-соединением (сокет, имеющий метод close):
Видите, как наша обёртка изящно управляется и с правильными и с неподходящими объектами. В этом сила менеджеров контекста и магических методов. Заметьте, что стандартная библиотека Питона включает модуль contextlib, который включает в себя contextlib.closing() — менеджер контекста, который делает приблизительно то же (без какой-либо обработки случая, когда объект не имеет метода close() ).
Абстрактные базовые классы
Построение дескрипторов
Дескрипторы это такие классы, с помощью которых можно добавить свою логику к событиям доступа (получение, изменение, удаление) к атрибутам других объектов. Дескрипторы не подразумевается использовать сами по себе; скорее, предполагается, что ими будут владеть какие-нибудь связанные с ними классы. Дескрипторы могут быть полезны для построения объектно-ориентированных баз данных или классов, чьи атрибуты зависят друг от друга. В частности, дескрипторы полезны при представлении атрибутов в нескольких системах исчисления или каких-либо вычисляемых атрибутов (как расстояние от начальной точки до представленной атрибутом точки на сетке).
Чтобы класс стал дескриптором, он должен реализовать по крайней мере один метод из __get__ , __set__ или __delete__ . Давайте рассмотрим эти магические методы:
-
__get__(self, instance, instance_class)
Определяет поведение при возвращении значения из дескриптора. instance это объект, для чьего атрибута-дескриптора вызывается метод. owner это тип (класс) объекта.
Копирование
В Питоне оператор присваивания не копирует объекты, а только добавляет ещё одну ссылку. Но для коллекций, содержащих изменяемые элементы, иногда необходимо полноценное копирование, чтобы можно было менять элементы одной последовательности, не затрагивая другую. Здесь в игру и вступает copy . К счастью, модули в Питоне не обладают разумом, поэтому мы можем не беспокоиться что они вдруг начнут бесконтрольно копировать сами себя и вскоре линуксовые роботы заполонят всю планету, но мы должны сказать Питону как правильно копировать.
-
__copy__(self)
Определяет поведение copy.copy() для экземпляра вашего класса. copy.copy() возвращает поверхностную копию вашего объекта — это означает, что хоть сам объект и создан заново, все его данные ссылаются на данные оригинального объекта. И при изменении данных нового объекта, изменения будут происходить и в оригинальном.
Использование модуля pickle на своих объектах
Pickle это модуль для сериализации структур данных Питона и он может быть невероятно полезен, когда вам нужно сохранить состояние какого-либо объекта и восстановить его позже (обычно, в целях кэширования). Кроме того, это ещё и отличный источник переживаний и путаницы.
Сериализация настолько важна, что кроме своего модуля ( pickle ) имеет и свой собственный протокол и свои магические методы. Но для начала о том, как сериализовать с помощью pickle уже существующие типы данных (спокойно пропускайте, если вы уже знаете).
Вкратце про сериализацию
Давайте погрузимся в сериализацию. Допустим, у вас есть словарь, который вы хотите сохранить и восстановить позже. Вы должны записать его содержимое в файл, тщательно убедившись, что пишете с правильным синтаксисом, потом восстановить его, или выполнив exec() , или прочитав файл. Но это в лучшем случае рискованно: если вы храните важные данные в тексте, он может быть повреждён или изменён множеством способов, с целью обрушить вашу программу или, вообще, запустить какой-нибудь опасный код на вашем компьютере. Лучше использовать pickle:
И вот, спустя несколько часов, нам снова нужен наш словарь:
Что произошло? Точно то, что и ожидалось. data как-будто всегда тут и была.
Теперь, немного об осторожности: pickle не идеален. Его файлы легко испортить случайно или преднамеренно. Pickle, может быть, безопаснее чем текстовые файлы, но он всё ещё может использоваться для запуска вредоносного кода. Кроме того, он несовместим между разными версиями Питона, поэтому если вы будете распространять свои объекты с помощью pickle, не ожидайте что все люди смогут их использовать. Тем не менее, модуль может быть мощным инструментом для кэширования и других распространённых задач с сериализацией.
Сериализация собственных объектов.
Модуль pickle не только для встроенных типов. Он может использоваться с каждым классом, реализующим его протокол. Этот протокол содержит четыре необязательных метода, позволяющих настроить то, как pickle будет с ними обращаться (есть некоторые различия для расширений на C, но это за рамками нашего руководства):
-
__getinitargs__(self)
Если вы хотите, чтобы после десериализации вашего класса был вызыван __init__ , вы можете определить __getinitargs__ , который должен вернуть кортеж аргументов, который будет отправлен в __init__ . Заметьте, что этот метод работает только с классами старого стиля.
Пример
Для примера опишем грифельную доску ( Slate ), которая запоминает что и когда было на ней записано. Впрочем, конкретно эта доска становится чистой каждый раз, когда она сериализуется: текущее значение не сохраняется.
Заключение
Цель этого руководства донести что-нибудь до каждого, кто его читает, независимо от его опыта в Питоне или объектно-ориентированном программировании. Если вы новичок в Питоне, вы получили ценные знания об основах написания функциональных, элегантных и простых для использования классов. Если вы программист среднего уровня, то вы, возможно, нашли несколько новых приятных идей и стратегий, несколько хороших способов уменьшить количество кода, написанного вами и вашими клиентами. Если же вы Питонист-эксперт, то вы обновили некоторые свои, возможно, подзабытые знания, а может и нашли парочку новых трюков. Независимо от вашего уровня, я надеюсь, что это путешествие через специальные питоновские методы было поистине магическим (не смог удержаться).
Дополнение 1: Как вызывать магические методы
Надеюсь, эта таблица избавит вас от любых вопросов о том, что за синтаксис вызова магических методов.
Дополнение 2: Изменения в Питоне 3
Опишем несколько главных случаев, когда Питон 3 отличается от 2.x в терминах его объектной модели:
Python Class Constructors: Control Your Object Instantiation
Class constructors are a fundamental part of object-oriented programming in Python. They allow you to create and properly initialize objects of a given class, making those objects ready to use. Class constructors internally trigger Python’s instantiation process, which runs through two main steps: instance creation and instance initialization.
If you want to dive deeper into how Python internally constructs objects and learn how to customize the process, then this tutorial is for you.
In this tutorial, you’ll:
- Understand Python’s internal instantiation process
- Customize object initialization using .__init__()
- Fine-tune object creation by overriding .__new__()
With this knowledge, you’ll be able to tweak the creation and initialization of objects in your custom Python classes, which will give you control over the instantiation process at a more advanced level.
To better understand the examples and concepts in this tutorial, you should be familiar with object-oriented programming and special methods in Python.
Free Bonus: Click here to get access to a free Python OOP Cheat Sheet that points you to the best tutorials, videos, and books to learn more about Object-Oriented Programming with Python.
Python’s Class Constructors and the Instantiation Process
Like many other programming languages, Python supports object-oriented programming. At the heart of Python’s object-oriented capabilities, you’ll find the class keyword, which allows you to define custom classes that can have attributes for storing data and methods for providing behaviors.
Once you have a class to work with, then you can start creating new instances or objects of that class, which is an efficient way to reuse functionality in your code.
Creating and initializing objects of a given class is a fundamental step in object-oriented programming. This step is often referred to as object construction or instantiation. The tool responsible for running this instantiation process is commonly known as a class constructor.
Getting to Know Python’s Class Constructors
In Python, to construct an object of a given class, you just need to call the class with appropriate arguments, as you would call any function:
In this example, you define SomeClass using the class keyword. This class is currently empty because it doesn’t have attributes or methods. Instead, the class’s body only contains a pass statement as a placeholder statement that does nothing.
Then you create a new instance of SomeClass by calling the class with a pair of parentheses. In this example, you don’t need to pass any argument in the call because your class doesn’t take arguments yet.
In Python, when you call a class as you did in the above example, you’re calling the class constructor, which creates, initializes, and returns a new object by triggering Python’s internal instantiation process.
A final point to note is that calling a class isn’t the same as calling an instance of a class. These are two different and unrelated topics. To make a class’s instance callable, you need to implement a .__call__() special method, which has nothing to do with Python’s instantiation process.
Understanding Python’s Instantiation Process
You trigger Python’s instantiation process whenever you call a Python class to create a new instance. This process runs through two separate steps, which you can describe as follows:
- Create a new instance of the target class
- Initialize the new instance with an appropriate initial state
To run the first step, Python classes have a special method called .__new__() , which is responsible for creating and returning a new empty object. Then another special method, .__init__() , takes the resulting object, along with the class constructor’s arguments.
The .__init__() method takes the new object as its first argument, self . Then it sets any required instance attribute to a valid state using the arguments that the class constructor passed to it.
In short, Python’s instantiation process starts with a call to the class constructor, which triggers the instance creator, .__new__() , to create a new empty object. The process continues with the instance initializer, .__init__() , which takes the constructor’s arguments to initialize the newly created object.
To explore how Python’s instantiation process works internally, consider the following example of a Point class that implements a custom version of both methods, .__new__() and .__init__() , for demonstration purposes:
Here’s a breakdown of what this code does:
Line 3 defines the Point class using the class keyword followed by the class name.
Line 4 defines the .__new__() method, which takes the class as its first argument. Note that using cls as the name of this argument is a strong convention in Python, just like using self to name the current instance is. The method also takes *args and **kwargs , which allow for passing an undefined number of initialization arguments to the underlying instance.
Line 5 prints a message when .__new__() runs the object creation step.
Line 6 creates a new Point instance by calling the parent class’s .__new__() method with cls as an argument. In this example, object is the parent class, and the call to super() gives you access to it. Then the instance is returned. This instance will be the first argument to .__init__() .
Line 8 defines .__init__() , which is responsible for the initialization step. This method takes a first argument called self , which holds a reference to the current instance. The method also takes two additional arguments, x and y . These arguments hold initial values for the instance attributes .x and .y . You need to pass suitable values for these arguments in the call to Point() , as you’ll learn in a moment.
Line 9 prints a message when .__init__() runs the object initialization step.
Lines 10 and 11 initialize .x and .y , respectively. To do this, they use the provided input arguments x and y .
Lines 13 and 14 implement the .__repr__() special method, which provides a proper string representation for your Point class.
With Point in place, you can uncover how the instantiation process works in practice. Save your code to a file called point.py and start your Python interpreter in a command-line window. Then run the following code:
Calling the Point() class constructor creates, initializes, and returns a new instance of the class. This instance is then assigned to the point variable.
In this example, the call to the constructor also lets you know the steps that Python internally runs to construct the instance. First, Python calls .__new__() and then .__init__() , resulting in a new and fully initialized instance of Point , as you confirmed at the end of the example.
To continue learning about class instantiation in Python, you can try running both steps manually:
In this example, you first call .__new__() on your Point class, passing the class itself as the first argument to the method. This call only runs the first step of the instantiation process, creating a new and empty object. Note that creating an instance this way bypasses the call to .__init__() .
Note: The code snippet above is intended to be a demonstrative example of how the instantiation process works internally. It’s not something that you would typically do in real code.
Once you have the new object, then you can initialize it by calling .__init__() with an appropriate set of arguments. After this call, your Point object is properly initialized, with all its attributes set up.
A subtle and important detail to note about .__new__() is that it can also return an instance of a class different from the class that implements the method itself. When that happens, Python doesn’t call .__init__() in the current class, because there’s no way to unambiguously know how to initialize an object of a different class.
Consider the following example, in which the .__new__() method of the B class returns an instance of the A class:
Because B.__new__() returns an instance of a different class, Python doesn’t run B.__init__() . To confirm this behavior, save the code into a file called ab_classes.py and then run the following code in an interactive Python session:
The call to the B() class constructor runs B.__new__() , which returns an instance of A instead of B . That’s why B.__init__() never runs. Note that b doesn’t have a .b_value attribute. In contrast, b does have an .a_value attribute with a value of 42 .
Now that you know the steps that Python internally takes to create instances of a given class, you’re ready to dig a little deeper into other characteristics of .__init__() , .__new__() , and the steps that they run.
Object Initialization With .__init__()
In Python, the .__init__() method is probably the most common special method that you’ll ever override in your custom classes. Almost all your classes will need a custom implementation of .__init__() . Overriding this method will allow you to initialize your objects properly.
The purpose of this initialization step is to leave your new objects in a valid state so that you can start using them right away in your code. In this section, you’ll learn the basics of writing your own .__init__() methods and how they can help you customize your classes.
Providing Custom Object Initializers
The most bare-bones implementation of .__init__() that you can write will just take care of assigning input arguments to matching instance attributes. For example, say you’re writing a Rectangle class that requires .width and .height attributes. In that case, you can do something like this:
As you learned before, .__init__() runs the second step of the object instantiation process in Python. Its first argument, self , holds the new instance that results from calling .__new__() . The rest of the arguments to .__init__() are normally used to initialize instance attributes. In the above example, you initialized the rectangle’s .width and .height using the width and height arguments to .__init__() .
It’s important to note that, without counting self , the arguments to .__init__() are the same ones that you passed in the call to the class constructor. So, in a way, the .__init__() signature defines the signature of the class constructor.
Additionally, keep in mind that .__init__() must not explicitly return anything different from None , or you’ll get a TypeError exception:
In this example, the .__init__() method attempts to return an integer number, which ends up raising a TypeError exception at run time.
The error message in the above example says that .__init__() should return None . However, you don’t need to return None explicitly, because methods and functions without an explicit return statement just return None implicitly in Python.
With the above implementation of .__init__() , you ensure that .width and .height get initialized to a valid state when you call the class constructor with appropriate arguments. That way, your rectangles will be ready for use right after the construction process finishes.
In .__init__() , you can also run any transformation over the input arguments to properly initialize the instance attributes. For example, if your users will use Rectangle directly, then you might want to validate the supplied width and height and make sure that they’re correct before initializing the corresponding attributes:
In this updated implementation of .__init__() , you make sure that the input width and height arguments are positive numbers before initializing the corresponding .width and .height attributes. If either validation fails, then you get a ValueError .
Note: A more Pythonic technique to tackle attribute validation is to turn attributes into properties. To learn more about properties, check out Python’s property(): Add Managed Attributes to Your Classes.
Now say that you’re using inheritance to create a custom class hierarchy and reuse some functionality in your code. If your subclasses provide a .__init__() method, then this method must explicitly call the base class’s .__init__() method with appropriate arguments to ensure the correct initialization of instances. To do this, you should use the built-in super() function like in the following example:
The first line in the .__init__() method of Employee calls super().__init__() with name and birth_date as arguments. This call ensures the initialization of .name and .birth_date in the parent class, Person . This technique allows you to extend the base class with new attributes and functionality.
To wrap up this section, you should know that the base implementation of .__init__() comes from the built-in object class. This implementation is automatically called when you don’t provide an explicit .__init__() method in your classes.
Building Flexible Object Initializers
You can make your objects’ initialization step flexible and versatile by tweaking the .__init__() method. To this end, one of the most popular techniques is to use optional arguments. This technique allows you to write classes in which the constructor accepts different sets of input arguments at instantiation time. Which arguments to use at a given time will depend on your specific needs and context.
As a quick example, check out the following Greeter class:
In this example, .__init__() takes a regular argument called name . It also takes an optional argument called formal , which defaults to False . Because formal has a default value, you can construct objects by relying on this value or by providing your own.
The class’s final behavior will depend on the value of formal . If this argument is False , then you’ll get an informal greeting when you call .greet() . Otherwise, you’ll get a more formal greeting.
To try Greeter out, go ahead and save the code into a greet.py file. Then open an interactive session in your working directory and run the following code:
In the first example, you create an informal_greeter object by passing a value to the name argument and relying on the default value of formal . You get an informal greeting on your screen when you call .greet() on the informal_greeter object.
In the second example, you use a name and a formal argument to instantiate Greeter . Because formal is True , the result of calling .greet() is a formal greeting.
Even though this is a toy example, it showcases how default argument values are a powerful Python feature that you can use to write flexible initializers for your classes. These initializers will allow you to instantiate your classes using different sets of arguments depending on your needs.
Okay! Now that you know the basics of .__init__() and the object initialization step, it’s time to change gears and start diving deeper into .__new__() and the object creation step.
Object Creation With .__new__()
When writing Python classes, you typically don’t need to provide your own implementation of the .__new__() special method. Most of the time, the base implementation from the built-in object class is sufficient to build an empty object of your current class.
However, there are a few interesting use cases for this method. For example, you can use .__new__() to create subclasses of immutable types, such as int , float , tuple , and str .
In the following sections, you’ll learn how to write custom implementations of .__new__() in your classes. To do this, you’ll code a few examples that’ll give you an idea of when you might need to override this method.
Providing Custom Object Creators
Typically, you’ll write a custom implementation of .__new__() only when you need to control the creation of a new instance at a low level. Now, if you need a custom implementation of this method, then you should follow a few steps:
- Create a new instance by calling super().__new__() with appropriate arguments.
- Customize the new instance according to your specific needs.
- Return the new instance to continue the instantiation process.
With these three succinct steps, you’ll be able to customize the instance creation step in the Python instantiation process. Here’s an example of how you can translate these steps into Python code:
This example provides a sort of template implementation of .__new__() . As usual, .__new__() takes the current class as an argument that’s typically called cls .
Note that you’re using *args and **kwargs to make the method more flexible and maintainable by accepting any number of arguments. You should always define .__new__() with *args and **kwargs , unless you have a good reason to follow a different pattern.
In the first line of .__new__() , you call the parent class’s .__new__() method to create a new instance and allocate memory for it. To access the parent class’s .__new__() method, you use the super() function. This chain of calls takes you up to object.__new__() , which is the base implementation of .__new__() for all Python classes.
Note: The built-in object class is the default base class of all Python classes.
The next step is to customize your newly created instance. You can do whatever you need to do to customize the instance at hand. Finally, in the third step, you need to return the new instance to continue the instantiation process with the initialization step.
It’s important to note that object.__new__() itself only accepts a single argument, the class to instantiate. If you call object.__new__() with more arguments, then you get a TypeError :
In this example, you hand over *args and **kwargs as additional arguments in the call to super().__new__() . The underlying object.__new__() accepts only the class as an argument, so you get a TypeError when you instantiate the class.
However, object.__new__() still accepts and passes over extra arguments to .__init__() if your class doesn’t override .__new__() , as in the following variation of SomeClass :
In this implementation of SomeClass , you don’t override .__new__() . The object creation is then delegated to object.__new__() , which now accepts value and passes it over to SomeClass.__init__() to finalize the instantiation. Now you can create new and fully initialized instances of SomeClass , just like some_obj in the example.
Cool! Now that you know the basics of writing your own implementations of .__new__() , you’re ready to dive into a few practical examples that feature some of the most common use cases of this method in Python programming.
Subclassing Immutable Built-in Types
To kick things off, you’ll start with a use case of .__new__() that consists of subclassing an immutable built-in type. As an example, say you need to write a Distance class as a subclass of Python’s float type. Your class will have an additional attribute to store the unit that’s used to measure the distance.
Here’s a first approach to this problem, using the .__init__() method:
When you subclass an immutable built-in data type, you get an error. Part of the problem is that the value is set during creation, and it’s too late to change it during initialization. Additionally, float.__new__() is called under the hood, and it doesn’t deal with extra arguments in the same way as object.__new__() . This is what raises the error in your example.
To work around this issue, you can initialize the object at creation time with .__new__() instead of overriding .__init__() . Here’s how you can do this in practice:
In this example, .__new__() runs the three steps that you learned in the previous section. First, the method creates a new instance of the current class, cls , by calling super().__new__() . This time, the call rolls back to float.__new__() , which creates a new instance and initializes it using value as an argument. Then the method customizes the new instance by adding a .unit attribute to it. Finally, the new instance gets returned.
Note: The Distance class in the example above doesn’t provide a proper unit conversion mechanism. This means that something like Distance(10, «km») + Distance(20, «miles») won’t attempt at converting units before adding the values. If you’re interested in converting units, then check out the Pint project on PyPI.
That’s it! Now your Distance class works as expected, allowing you to use an instance attribute for storing the unit in which you’re measuring the distance. Unlike the floating-point value stored in a given instance of Distance , the .unit attribute is mutable, so you can change its value any time you like. Finally, note how a call to the dir() function reveals that your class inherits features and methods from float .
Returning Instances of a Different Class
Returning an object of a different class is a requirement that can raise the need for a custom implementation of .__new__() . However, you should be careful because in this case, Python skips the initialization step entirely. So, you’ll have the responsibility of taking the newly created object into a valid state before using it in your code.
Check out the following example, in which the Pet class uses .__new__() to return instances of randomly selected classes:
In this example, Pet provides a .__new__() method that creates a new instance by randomly selecting a class from a list of existing classes.
Here’s how you can use this Pet class as a factory of pet objects:
Every time you instantiate Pet , you get a random object from a different class. This result is possible because there’s no restriction on the object that .__new__() can return. Using .__new__() in such a way transforms a class into a flexible and powerful factory of objects, not limited to instances of itself.
Finally, note how the .__init__() method of Pet never runs. That’s because Pet.__new__() always returns objects of a different class rather than of Pet itself.
Allowing Only a Single Instance in Your Classes
Sometimes you need to implement a class that allows the creation of a single instance only. This type of class is commonly known as a singleton class. In this situation, the .__new__() method comes in handy because it can help you restrict the number of instances that a given class can have.
Note: Most experienced Python developers would argue that you don’t need to implement the singleton design pattern in Python unless you already have a working class and need to add the pattern’s functionality on top of it.
The rest of the time, you can use a module-level constant to get the same singleton functionality without having to write a relatively complex class.
Here’s an example of coding a Singleton class with a .__new__() method that allows the creation of only one instance at a time. To do this, .__new__() checks the existence of previous instances cached on a class attribute:
The Singleton class in this example has a class attribute called ._instance that defaults to None and works as a cache. The .__new__() method checks if no previous instance exists by testing the condition cls._instance is None .
Note: In the example above, Singleton doesn’t provide an implementation of .__init__() . If you ever need a class like this with a .__init__() method, then keep in mind that this method will run every time you call the Singleton() constructor. This behavior can cause weird initialization effects and bugs.
If this condition is true, then the if code block creates a new instance of Singleton and stores it to cls._instance . Finally, the method returns the new or the existing instance to the caller.
Then you instantiate Singleton twice to try to construct two different objects, first and second . If you compare the identity of these objects with the is operator, then you’ll note that both objects are the same object. The names first and second just hold references to the same Singleton object.
Partially Emulating collections.namedtuple
As a final example of how to take advantage of .__new__() in your code, you can push your Python skills and write a factory function that partially emulates collections.namedtuple() . The namedtuple() function allows you to create subclasses of tuple with the additional feature of having named fields for accessing the items in the tuple.
The code below implements a named_tuple_factory() function that partially emulates this functionality by overriding the .__new__() method of a nested class called NamedTuple :
Here’s how this factory function works line by line:
Line 3 imports itemgetter() from the operators module. This function allows you to retrieve items using their index in the containing sequence.
Line 5 defines named_tuple_factory() . This function takes a first argument called type_name , which will hold the name of the tuple subclass that you want to create. The *fields argument allows you to pass an undefined number of field names as strings.
Line 6 defines a local variable to hold the number of named fields provided by the user.
Line 8 defines a nested class called NamedTuple , which inherits from the built-in tuple class.
Line 9 provides a .__slots__ class attribute. This attribute defines a tuple for holding instance attributes. This tuple saves memory by acting as a substitute for the instance’s dictionary, .__dict__ , which would otherwise play a similar role.
Line 11 implements .__new__() with cls as its first argument. This implementation also takes the *args argument to accept an undefined number of field values.
Lines 12 to 16 define a conditional statement that checks if the number of items to store in the final tuple differs from the number of named fields. If that’s the case, then the conditional raises a TypeError with an error message.
Line 17 sets the .__name__ attribute of the current class to the value provided by type_name .
Lines 18 and 19 define a for loop that turns every named field into a property that uses itemgetter() to return the item at the target index . The loop uses the built-in setattr() function to perform this action. Note that the built-in enumerate() function provides the appropriate index value.
Line 20 returns a new instance of the current class by calling super().__new__() as usual.
Lines 22 and 23 define a .__repr__() method for your tuple subclass.
Line 25 returns the newly created NamedTuple class.
To try your named_tuple_factory() out, fire up an interactive session in the directory containing the named_tuple.py file and run the following code:
In this code snippet, you create a new Point class by calling named_tuple_factory() . The first argument in this call represents the name that the resulting class object will use. The second and third arguments are the named fields available in the resulting class.
Then you create a Point object by calling the class constructor with appropriate values for the .x and .y fields. To access the value of each named field, you can use the dot notation. You can also use indices to retrieve the values because your class is a tuple subclass.
Because tuples are immutable data types in Python, you can’t assign new values to the point’s coordinates in place. If you try to do that, then you get an AttributeError .
Finally, calling dir() with your point instance as an argument reveals that your object inherits all the attributes and methods that regular tuples have in Python.
Conclusion
Now you know how Python class constructors allow you to instantiate classes, so you can create concrete and ready-to-use objects in your code. In Python, class constructors internally trigger the instantiation or construction process, which goes through instance creation and instance initialization. These steps are driven by the .__new__() and .__init__() special methods.
By learning about Python’s class constructors, the instantiation process, and the .__new__() and .__init__() methods, you can now manage how your custom classes construct new instances.
In this tutorial, you learned:
- How Python’s instantiation process works internally
- How your own .__init__() methods help you customize object initialization
- How overriding the .__new__() method allows for custom object creation
Now you’re ready to take advantage of this knowledge to fine-tune your class constructors and take full control over instance creation and initialization in your object-oriented programming adventure with Python.
Free Bonus: Click here to get access to a free Python OOP Cheat Sheet that points you to the best tutorials, videos, and books to learn more about Object-Oriented Programming with Python.
Watch Now This tutorial has a related video course created by the Real Python team. Watch it together with the written tutorial to deepen your understanding: Using Python Class Constructors
Get a short & sweet Python Trick delivered to your inbox every couple of days. No spam ever. Unsubscribe any time. Curated by the Real Python team.
About Leodanis Pozo Ramos
Leodanis is an industrial engineer who loves Python and software development. He's a self-taught Python developer with 6+ years of experience. He's an avid technical writer with a growing number of articles published on Real Python and other sites.
Each tutorial at Real Python is created by a team of developers so that it meets our high quality standards. The team members who worked on this tutorial are:
Master Real-World Python Skills With Unlimited Access to Real Python
Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:
Master Real-World Python Skills
With Unlimited Access to Real Python
Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:
What Do You Think?
What’s your #1 takeaway or favorite thing you learned? How are you going to put your newfound skills to use? Leave a comment below and let us know.
Commenting Tips: The most useful comments are those written with the goal of learning from or helping out other students. Get tips for asking good questions and get answers to common questions in our support portal. Looking for a real-time conversation? Visit the Real Python Community Chat or join the next “Office Hours” Live Q&A Session. Happy Pythoning!
Объектно-ориентированное программирование. Специальные методы.
Декоратор @staticmethod определяет обычную функцию (статический метод) в пространстве имён класса. У него нет обязательного параметра-ссылки self. Может быть полезно для вспомогательных функций, чтобы не мусорить пространство имён модуля. Доступ к таким методам можно получить как из экземпляра класса, так и из самого класса:
Декоратор @classmethod создаёт метод класса и требует обязательную ссылку на класс (cls). Поэтому объект класса явно передаётся через первый параметр как это с параметром self происходит для обычных методов. Также как и для self, переданный cls может отличаться от класса, в котором определён класс-метод (может быть потомок). Часто используется для создания альтернативных конструкторов.
Давайте взглянем на пример кода, в котором одновременно показаны она декоратора, это может помочь понять основные принципы:
Важно понимать, что ни classmethod ни staticmethod НЕ являются функциями от конкретного объекта класса и соответственно не принимают self. Подчеркнем еще раз их различия: — classmethod принимает cls как первый параметр, тогда как staticmethod в специальных аргументах не нуждается — classmethod может получать доступ или менять состояние класса, в то время как staticmethod нет — staticmethod в целом вообще ничего не знают про класс. Это просто функция над аргументами, объявленная внутри класса.
Специальные методы (магические) вида _ < param > _
В Python существует огромное количество специальных методов, расширяющих возможности пользовательских классов. Например, можно определить вид объекта на печати, его "официальное" строковое представление или поведение при сравнениях.
Эти методы могут эмулировать поведение встроенных классов, но при этом они необязательно существуют у самих встроенных классов. Например, у объектов int при сложении не вызывается метод add. Таким образом, их нельзя переопределить.
Давайте для примера переопределим стандартную операцию сложения. Рассмотрим класс Vector, используемый для представления радиус-векторов на координатной плоскости, и определим в нем поля-координаты: x и y. Также очень хотелось бы определить для векторов операцию +, чтобы их можно было складывать столь же удобно, как и числа или строки.
Для этого необходимо перегрузить операцию +: определить функцию, которая будет использоваться, если операция + будет вызвана для объекта класса Vector. Для этого нужно определить метод add класса Vector, у которого два параметра: неявная ссылка self на экземпляр класса, для которого она будет вызвана (это левый операнд операции +) и явная ссылка other на правый операнд:
Теперь при вызове оператора A + B Питон вызовет метод A.add(B), то есть вызовет указанный метод, где self = A, other = B.
Аналогично можно определить и оставшиеся операции. Полезной для переопределения является операция <. Она должна возвращать логическое значение True, если левый операнд меньше правого или False в противном случае (также в том случае, если объекты равны). Для переопределения этого операнда нужно определить метод lt (less than):
В этом примере оператор вернет True, если у левого операнда поле x меньше, чем у правого операнда, а также если поля x у них равны, а поле y меньше у левого операнда.
После определения оператора <, появляется возможность упорядочивать объекты, используя этот оператор. Теперь можно сортировать списки объектов при помощи метода sort() или функции sorted, при этом будет использоваться именно определенный оператор сравнения <.
Список основных перегружаемых операторов
Метод | Использование |
---|---|
Операторы сравнения | |
__lt__(self, other) | x < y |
__le__(self, other) | x <= y |
__eq__(self, other) | x == y |
__ne__(self, other) | x != y |
__gt__(self, other) | x > y |
__ge__(self, other) | x >= y |
Арифметические операторы | |
Сложение | |
__add__(self, other) | x + y |
__radd__(self, other) | y + x |
__iadd__(self, other) | x += y |
Вычитание | |
__sub__(self, other) | x — y |
__rsub__(self, other) | y — x |
__isub__(self, other) | x -= y |
Умножение | |
__mul__(self, other) | x * y |
__rmul__(self, other) | y * x |
__imul__(self, other) | x *= y |
Математическое умножение (например векторное) | |
__matmul__(self, other) | x @ y |
__rmatmul__(self, other) | y @ x |
__imatmul__(self, other) | x @= y |
Деление | |
__truediv__(self, other) | x / y |
__rtruediv__(self, other) | y / x |
__itruediv__(self, other) | x /= y |
Целочисленное деление | |
__floordiv__(self, other) | x // y |
__rfloordiv__(self, other) | y // x |
__ifloordiv__(self, other) | x //= y |
__divmod__(self, other) | divmod(x, y) |
Остаток | |
__mod__(self, other) | x % y |
__rmod__(self, other) | y % x |
__imod__(self, other) | x %= y |
Возведение в степень | |
__pow__(self, other) | x ** y |
__rpow__(self, other) | y ** x |
__ipow__(self, other) | x **= y |
Отрицание, модуль | |
__pos__(self) | +x |
__neg__(self) | -x |
__abs__(self) | abs(x) |
__len__(self) | len(x) |
Преобразование к стандартным типам | |
__int__(self) | int(x) |
__float__(self) | float(x) |
__complex__(self) | complex(x) |
__str__(self) | str(x) |
__round__(self, digits = 0) | round(x, digits) |
Блок with | |
__enter__(self) | |
__exit__(self) |
Задачи:
Задача 1:
Реализуйте свой класс Complex для комплексных чисел, аналогично встроенной реализации complex: