Что такое перегрузка операторов
Перейти к содержимому

Что такое перегрузка операторов

Перегрузка операторов

Python 3 логотип

Перегрузка операторов — один из способов реализации полиморфизма, когда мы можем задать свою реализацию какого-либо метода в своём классе.

Например, у нас есть два класса:

 В данном примере класс B наследует класс A, но переопределяет метод go, поэтому он имеет мало общего с аналогичным методом класса A.

Однако в python имеются методы, которые, как правило, не вызываются напрямую, а вызываются встроенными функциями или операторами.

Например, метод __init__ перегружает конструктор класса. Конструктор — создание экземпляра класса.

  

__new__(cls[, . ]) — управляет созданием экземпляра. В качестве обязательного аргумента принимает класс (не путать с экземпляром). Должен возвращать экземпляр класса для его последующей его передачи методу __init__.

__init__(self[, . ]) - как уже было сказано выше, конструктор.

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

__repr__(self) - вызывается встроенной функцией repr; возвращает "сырые" данные, использующиеся для внутреннего представления в python.

__str__(self) - вызывается функциями str, print и format. Возвращает строковое представление объекта.

__bytes__(self) - вызывается функцией bytes при преобразовании к байтам.

__format__(self, format_spec) - используется функцией format (а также методом format у строк).

__le__(self, other) - x ≤ y вызывает x.__le__(y).

__eq__(self, other) - x == y вызывает x.__eq__(y).

__ne__(self, other) - x != y вызывает x.__ne__(y)

__gt__(self, other) - x > y вызывает x.__gt__(y).

__ge__(self, other) - x ≥ y вызывает x.__ge__(y).

__hash__(self) - получение хэш-суммы объекта, например, для добавления в словарь.

__bool__(self) - вызывается при проверке истинности. Если этот метод не определён, вызывается метод __len__ (объекты, имеющие ненулевую длину, считаются истинными).

__getattr__(self, name) - вызывается, когда атрибут экземпляра класса не найден в обычных местах (например, у экземпляра нет метода с таким названием).

__setattr__(self, name, value) - назначение атрибута.

__delattr__(self, name) - удаление атрибута (del obj.name).

__call__(self[, args. ]) - вызов экземпляра класса как функции.

__len__(self) - длина объекта.

__getitem__(self, key) - доступ по индексу (или ключу).

__setitem__(self, key, value) - назначение элемента по индексу.

__delitem__(self, key) - удаление элемента по индексу.

__iter__(self) - возвращает итератор для контейнера.

__reversed__(self) - итератор из элементов, следующих в обратном порядке.

__contains__(self, item) - проверка на принадлежность элемента контейнеру (item in self).

Перегрузка арифметических операторов

__add__(self, other) - сложение. x + y вызывает x.__add__(y).

__sub__(self, other) - вычитание (x - y).

__mul__(self, other) - умножение (x * y).

__truediv__(self, other) - деление (x / y).

__floordiv__(self, other) - целочисленное деление (x // y).

__mod__(self, other) - остаток от деления (x % y).

__divmod__(self, other) - частное и остаток (divmod(x, y)).

__pow__(self, other[, modulo]) - возведение в степень (x ** y, pow(x, y[, modulo])).

__lshift__(self, other) - битовый сдвиг влево (x

__rshift__(self, other) - битовый сдвиг вправо (x >> y).

__and__(self, other) - битовое И (x & y).

__xor__(self, other) - битовое ИСКЛЮЧАЮЩЕЕ ИЛИ (x ^ y).

__or__(self, other) - битовое ИЛИ (x | y).

__radd__(self, other),

__rsub__(self, other),

__rmul__(self, other),

__rtruediv__(self, other),

__rfloordiv__(self, other),

__rmod__(self, other),

__rdivmod__(self, other),

__rpow__(self, other),

__rlshift__(self, other),

__rrshift__(self, other),

__rand__(self, other),

__rxor__(self, other),

__ror__(self, other) - делают то же самое, что и арифметические операторы, перечисленные выше, но для аргументов, находящихся справа, и только в случае, если для левого операнда не определён соответствующий метод.

Например, операция x + y будет сначала пытаться вызвать x.__add__(y), и только в том случае, если это не получилось, будет пытаться вызвать y.__radd__(x). Аналогично для остальных методов.

__iadd__(self, other) - +=.

__isub__(self, other) - -=.

__imul__(self, other) - *=.

__itruediv__(self, other) - /=.

__ifloordiv__(self, other) - //=.

__imod__(self, other) - %=.

__ipow__(self, other[, modulo]) - **=.

__ilshift__(self, other) -

__irshift__(self, other) - >>=.

__iand__(self, other) - &=.

__ixor__(self, other) - ^=.

__ior__(self, other) - |=.

__neg__(self) - унарный -.

__pos__(self) - унарный +.

__abs__(self) - модуль (abs()).

__invert__(self) - инверсия (~).

__complex__(self) - приведение к complex.

__int__(self) - приведение к int.

__float__(self) - приведение к float.

__round__(self[, n]) - округление.

__enter__(self), __exit__(self, exc_type, exc_value, traceback) - реализация менеджеров контекста.

Рассмотрим некоторые из этих методов на примере двухмерного вектора, для которого переопределим некоторые методы:

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

Для вставки кода на Python в комментарий заключайте его в теги

  • Модуль csv - чтение и запись CSV файлов
  • Создаём сайт на Django, используя хорошие практики. Часть 1: создаём проект
  • Онлайн-обучение Python: сравнение популярных программ
  • Книги о Python
  • GUI (графический интерфейс пользователя)
  • Курсы Python
  • Модули
  • Новости мира Python
  • NumPy
  • Обработка данных
  • Основы программирования
  • Примеры программ
  • Типы данных в Python
  • Видео
  • Python для Web
  • Работа для Python-программистов
  • Сделай свой вклад в развитие сайта!
  • Самоучитель Python
  • Карта сайта
  • Отзывы на книги по Python
  • Реклама на сайте

Что такое перегрузка операторов

В статье про С++ мы упоминали перегрузку операторов. Это мощный и гибкий инструмент, который может оказаться опасным и непредсказуемым в неумелых руках. Настало время разобраться.

�� Опытным программистам: мы намеренно упростим детали для понимания сути. Ну сорян.

На примере сложения

Во всех языках есть оператор «плюс» — обычно он умеет складывать числа и соединять строки:

‘два’ + ‘два’ = ‘двадва’

‘четы’ + ‘ре’ = ‘четыре’

Допустим, мы пишем софт для интернет-магазина, и у нас есть там класс объектов «заказ». Напомним, что класс — это как бы чертёж, по которому создаются объекты. А объект — это такая коробка с данными и функциями, которыми мы можем управлять как единым целым. Подробнее об этом — в статьях про объекты и классы.

В объекте типа «заказ» лежит куча всего:

  • массив с содержимым корзины,
  • дата и время, когда сформирован заказ,
  • метод «очистить корзину»,
  • место для промокода,
  • метод «применить промокод»,
  • метод «проверить наличие товаров по складу»
  • идентификатор пользователя,
  • что-нибудь ещё интересное.

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

  • Если идентификатор постоянный, значит, мы однозначно пользователя узнали (его адрес, номер кредитки и т. д.).
  • Если идентификатор временный, значит, мы не знаем, что за пользователь — просто храним его корзину, пока он не оформит заказ. Это может быть новый человек или старый, но ещё не залогинившийся. В любом случае мы должны хранить его данные.

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

Обе части выражения — это объекты класса «Заказ». А наш язык программирования не знает, что значит «сложить два объекта класса „Заказ“». Он не знает:

  • Что с чем складывать? Число товаров? Суммы? Номера товаров? Номера телефонов? Ведь язык не понимает, что за объект перед ним. Для него это просто коробка с данными, он может с ними делать что хочешь.
  • Что возвращать? Число? Строку? Объект? Список заказов?
  • Может быть, нужно сравнить два заказа и по каким-то критериям определить самый актуальный?
  • Или нужно объединить две корзины в одну?
  • А что тогда делать с повторяющимися товарами? Заменить? Добавить в количество? Проигнорировать?

Вроде бы простая операция — а столько вопросов. Вот этому всему мы можем обучить оператор «+», и это будет перегрузка оператора.

�� Короче

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

В случае с нашим примером мы можем сказать, что если складываются два заказа, делай следующее:

  1. Найди, какой из этих заказов постоянный.
  2. Переложи из временного в постоянный все уникальные товары.
  3. Если есть неуникальные товары (например, и в том, и в другом заказе была одна и та же позиция), склей их и поставь максимальное количество. Например, если в постоянном заказе стояло 3 штуки одного артикула, а во временном этого же артикула 9 штук, то поставь в постоянный 9 штук.
  4. Временному заказу поставь статус «Склеено».
  5. Залогируй время склейки заказов.
  6. Верни постоянный заказ с обновлёнными данными.

Довольно много действий для одного плюса, не находите?

Что хорошего в перегрузке

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

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

Чем опасна перегрузка операторов

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

Это, с одной стороны, элегантно. А с другой, создаёт проблемы в отладке.

  1. Представьте, что после вас какой-то программист переделал структуру класса «Заказ», и теперь там по-другому работает массив с товарными позициями. Раньше у товаров были числовые идентификаторы типа integer (целые числа), а новый программист переделал их на строки.
  2. Ваш язык в неявном виде поддерживает сравнение чисел со строками и наоборот. Он производит какие-то свои преобразования и позволяет сравнить число со строкой. В 99,9% случаев это не сломает вашу программу, и даже перегруженный оператор будет работать.
  3. Но в 0,1% случаев сравнение случится некорректно, и никто не будет понимать, в чём дело. Где-то под капотом перегруженный оператор некорректно склеивает списки покупок, у пользователя вываливаются какие-то «левые» товары, которых он не заказывал. Он не глядя их оплачивает и потом катает жалобу на ваш магазин.

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

И что?

Перегрузка операторов — это полезно, но сложно.

Если программист не понимает полностью механизма работы перегрузок, лучше не перегружать.

Если понимает — он молодец и может учить стандартные инструменты нестандартному поведению.

Не перегружайтесь, берегите себя.

Получите ИТ-профессию

В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.

Лекция N. Перегрузка операторов

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

  1. Вместо get(i) удобнее использовать [i].
  2. a.plus(b) или plus(a, b) удобнее заменить на a + b.
  3. Вместо p.get()->print() удобнее использовать p->print().
  4. Обобщённое программирование.

В С++ существуют следующие операторы:

  • +a -a
  • + - / * %
  • += -= /= *= %=
  • && || !
  • = == !=
  • & | ~
  • a-> *a &a
  • >> . ::
  • ()?():()
  • [] ()
  • a++ ++a a-- --a
  • > = , [], ().
  • Для операторов && и || применяется логика "ленивых" вычислений. При перегрузке этих операторов она теряется.
  • Оператор -> должен обязательно возвращать указатель.
  • Операторов () может быть сколько угодно, с различными сигнатурами.
  • Оператор , в качестве результата возвращает тип и значение самого правого выражения. Перегружается он очень редко. Пример использования:

Пример перегрузки оператора бинарного сложения

Оператор бинарного сложения можно перегрузить снаружи класса следующим образом:

BigNum operator+ (BigNum const &a, BigNum const &b)

А так будет выглядеть определение внутри:

BigNum BigNum::operator+ (BigNum const &b) const < . >//первым аргументом в данном случае является *this //метод не меняет значение, поэтому const 

Пример перегрузки оператора +=

Определение оператора снаружи класса выглядит следующим образом:

BigNum & operator+= (BigNum &a, BigNum const &b) < . return a; >

А определение внутри класса такое:

BigNum & BigNum::operator+= (BigNum const &b) const < . return *this; >

В частности, можно передавать операнды различных типов (например, для перегрузки BigNum+int).

Другие примеры

Определение унарного минуса снаружи класса:

BigNum operator- (BigNum const &a)

Он же - внутри класса:

BigNum BigNum::operator- () const

Далее показана разница между определением методов ++a и a++:

BigNum & operator++ (BigNum &a); BigNum & operator++ (BigNum &a, size_t); //во втором случае используется фиктивный параметр, //который нужен только для разделения префиксного и постфиксного инкрементов 
BigNum BigNum::operator[] (size_t i); BigNum BigNum::operator() (. ); . BigNum b; b(17, 34);

Примеры грамотного использования перегрузки операторов

Array

struct Array < int operator[] (size_t i) const < assert (ireturn data_[i]; > int & operator[] (size_t i) < return data_[i]; > void swap (Array &a) < std::swap(a.data_, data_); std::swap(a.size_, size_); >//swap меняет значения местами Array & operator= (Array const &a) < if (this != &a) Array(a).swap(*this); return *this; > >;

Можно подключить или отключить assert, это зависит от параметров компиляции. При выходе за границы массива тогда возникнет ошибка.

Для использования ostream и istream достаточно подключить

#include ;

т.к. iostream весьма и весьма велик.

Array a; std::cout operator (std::ostream & os, Array const &a); //этот и следующий операторы могут быть переопределены только снаружи класса std::istream &operator>> (std::ostream & is, Array &a); //при чтении объект изменяется (std::cin >> a) 

Замечание: swap(Array(a)) не сработает, потому что swap принимает константную ссылку, а временные объекты нельзя передавать по константной ссылке.

BigNum

Оператор += лучше определять внутри класса, а оператор + — через += снаружи, как показано в примере:

struct BigNum < BigNum & operator+= (BigNum &b) < . >. > BigNum operator+ (BigNum a, BigNum const &b) < return a+=b; > //+, определённый снаружи, может быть вызван от аргументов, //которые может быть приведены к типу BigNum a(10); //должен быть BigNum(int) a+20; //работает и внутри, и снаружи 20+a; //работает только снаружи bool operator== (BigNum const &a, BigNum const &b) < . >bool operator!= (BigNum const &a, BigNum const &b) < return !(a==b); >

Умные указатели

В данном примере рассматривается перегрузка операторов p-> и *p.

struct smart_pstr < string *p_; . string * operator-> () const < return p_; > string & operator* () const < return *p_; > >; //предположим, что мы переопределим оператор ниже: operator== (smart_pstr, smart_pstr) . smart_pstr p; string *q; p == q; //произойдёт неявное преобразование типа, //создастся smart_pstr(q), а потом он будет удалён //для того, чтобы этого избежать, нужно конструктор сделать explicit //(т.е. запретить неявное преобразование типов) 

Перегрузка операторов в Python

Перегрузка операторов в Python – это возможность с помощью специальных методов в классах переопределять различные операторы языка. Имена таких методов включают двойное подчеркивание спереди и сзади.

Под операторами в данном контексте понимаются не только знаки +, -, *, /, обеспечивающие операции сложения, вычитания и др., но также специфика синтаксиса языка, обеспечивающая операции создания объекта, вызова объекта как функции, обращение к элементу объекта по индексу, вывод объекта и другое.

Мы уже использовали ряд методов перегрузки операторов. Это

  • __init__() – конструктор объектов класса, вызывается при создании объектов Конструктор класса и его вызов
  • __del__() – деструктор объектов класса, вызывается при удалении объектов
  • __str__() – преобразование объекта к строковому представлению, вызывается, когда объект передается функциям print() и str()
  • __add__() – метод перегрузки оператора сложения, вызывается, когда объект участвует в операции сложения будучи операндом с левой стороны __add__ - метод перегрузки оператора сложения
  • __setattr__() – вызывается, когда атрибуту объекта выполняется присваивание __setattr__ - метод перегрузки оператора присваивания

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

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

Возможность перегрузки операторов обеспечивает схожесть пользовательского класса со встроенными классами Python. Ведь все встроенные типы данных Питона – это классы. В результате все объекты могут иметь одинаковые интерфейсы. Так если ваш класс предполагает обращение к элементу объекта по индексу, например a[0] , то это можно обеспечить.

Пусть будет класс-агрегат B , содержащий в списке объекты класса A :

class A: def __init__(self, arg): self.arg = arg def __str__(self): return str(self.arg) class B: def __init__(self, *args): self.aList = [] for i in args: self.aList.append(A(i)) group = B(5, 10, 'abc') 

Чтобы получить элемент списка, несомненно, мы можем обратиться по индексу к полю aList :

print(group.aList[1])

Однако куда интереснее извлекать элемент по индексу из самого объекта, а не из его поля:

class B: def __init__(self, *args): self.aList = [] for i in args: self.aList.append(A(i)) def __getitem__(self, i): return self.aList[i] group = B(5, 10, 'abc') print(group.aList[1]) # выведет 10 print(group[0]) # 5 print(group[2]) # abc

Это делает объекты класса B похожими на объекты встроенных в Python классов-последовательностей (списков, строк, кортежей). Здесь метод __getitem__ перегружает операцию извлечения элемента по индексу. Другими словами, этот метод вызывается, когда к объекту применяется операция извлечения элемента: объект[индекс] .

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

a = A() a() a(3, 4)

Метод __call__ автоматически вызывается, когда к объекту обращаются как к функции. Например, здесь во второй строке произойдет вызов метода __call__() некогоКласса :

объект = некийКласс() объект([возможные аргументы])
class Changeable: def __init__(self, color): self.color = color def __call__(self, newcolor): self.color = newcolor def __str__(self): return "%s" % self.color canvas = Changeable("green") frame = Changeable("blue") canvas("red") frame("yellow") print(canvas, frame)

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

В Python кроме метода __str__ есть схожий с ним по поведению, но более "низкоуровневый" метод __repr__ . Оба метода должны возвращать строку.

Если в классе есть только метод __str__() , то при обращении к объекту в интерпретаторе без функции print() , он не будет вызываться:

>>> class A: . def __str__(self): . return "This is object of A" . >>> a = A() >>> print(a) This is object of A >>> a >>> str(a) 'This is object of A' >>> repr(a) ''

Метод __str__() вызывается при попытке преобразования объекта в строку с помощью встроенной функции str() . А функция print() так устроена, что сама вызывает str() для своих аргументов.

В Python есть встроенная функция repr() , которая также как str() преобразует объект в строку. Но "сырую" строку. Что это значит, попробуем понять с помощью примера:

>>> a = '3 + 2' >>> b = repr(a) >>> a '3 + 2' >>> b "'3 + 2'" >>> eval(a) 5 >>> eval(b) '3 + 2' >>> c = "Hello\nWorld" >>> d = repr(c) >>> c 'Hello\nWorld' >>> d "'Hello\\nWorld'" >>> print(c) Hello World >>> print(d) 'Hello\nWorld'

Функция eval() преобразует переданную строку в программный код, который тут же выполняется. Функция print() выполняет переход на новую строку, если встречает символ \n . Функция repr() выполняет действия, направленные на своего рода защиту строки от интерпретации, оставляет ее "сырой", т. е. в исходном виде. Еще раз:

>>> c = "Hello\nWorld" >>> c # аналог print(repr(c)) 'Hello\nWorld' >>> print(c) # аналог print(str(c)) Hello World

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

>>> class A: . def __repr__(self): . return "It's obj of A" . >>> a = A() >>> a It's obj of A >>> repr(a) "It's obj of A" >>> str(a) "It's obj of A" >>> print(a) It's obj of A

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

Практическая работа

Напишите класс Snow по следующему описанию.

В конструкторе класса инициируется поле, содержащее количество снежинок, выраженное целым числом.

Класс включает методы перегрузки арифметических операторов: __add__ – сложение, __sub__ – вычитание, __mul__ – умножение, __truediv__ – деление. В классе код этих методов должен выполнять увеличение или уменьшение количества снежинок на число n или в n раз. Метод __truediv__ перегружает обычное / , а не целочисленное // деление. Однако пусть в методе происходит округление значения до целого числа.

Класс включает метод make_snow , который помимо self принимает число снежинок в ряду и возвращает строку вида "*****\n*****\n*****…" , где количество снежинок между '\n' равно переданному аргументу, а количество рядов вычисляется, исходя из общего количества снежинок.

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

Курс с примерами решений практических работ:
pdf-версия

X Скрыть Наверх

Объектно-ориентированное программирование на Python

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *