Эмпирические правила, когда использовать перегрузку операторов в Python

Из того, что я помню из своего класса C ++, профессор сказал, что перегрузка операторов - это круто, но поскольку требуется относительно много размышлений и кода, чтобы охватить все конечные случаи (например, при перегрузке + вы, вероятно, также захотите перегрузить ++ и +=, а также убедитесь, что вы обрабатываете конечные случаи, такие как добавление объекта к себе и т. д.), вы должны учитывать это только в тех случаях, когда эта функция будет иметь большое влияние на ваш код, например, перегрузка операторов для класса матрицы в математическом приложении. .

То же самое относится и к питону? Вы бы порекомендовали переопределить поведение оператора в Python? И какие эмпирические правила вы можете мне дать?


person olamundo    schedule 12.10.2009    source источник


Ответы (4)


Перегрузка оператора в основном полезна, когда вы создаете новый класс, который попадает в существующий «абстрактный базовый класс» (ABC) - действительно, многие из ABC в стандартном библиотечном модуле collections полагаются на наличие определенных специальных методов (и специальных методов, имена которых начинаются и заканчиваются двойным подчеркиванием AKA" dunders " , точно так же, как вы выполняете перегрузку операторов в Python). Это дает хорошее руководство по запуску.

Например, класс Container должен переопределить специальный метод __contains__, т. Е. Оператор проверки членства item in container (например, if item in container: - не путайте с оператором for, for item in container:, который полагается на _7 _! - ). Точно так же Hashable должен переопределять __hash__, Sized должен переопределять __len__, Sequence или Mapping должен переопределять __getitem__ и так далее. (Более того, ABC могут предоставить вашему классу функции микширования - например, как Sequence, так и Mapping могут предоставить __contains__ на основе предоставленного вами переопределения __getitem__ и, таким образом, автоматически сделать ваш класс Container).

Помимо collections, вы захотите переопределить специальные методы (т.е. предусмотреть перегрузку оператора) в основном, если ваш новый класс «является числом». Существуют и другие особые случаи, но не поддавайтесь искушению перегрузить операторы «просто для прохлады», без семантической связи с «нормальными» значениями, как потоки C ++ для << и >> и строк Python (в Python 2.*, к счастью, не в 3.* any подробнее ;-) сделать для % - когда такие операторы больше не означают «сдвиг битов» или «остаток от деления», вы просто вносите путаницу. Стандартная библиотека языка может сойти с рук (хотя и не должна ;-), но если ваша библиотека не получит такого же широкого распространения, как стандартная библиотека языка, путаница повредит! -)

person Alex Martelli    schedule 12.10.2009
comment
Кстати, для тех, кто отчаялся при мысли об отсутствии% для форматирования строк: хотя документация Python 3 описывает% как устаревший, он все еще задокументирован, и, судя по недавним обсуждениям, нет никаких шансов, что эта функция действительно исчезнет до Python 4 в python-dev. Это оставляет достаточно времени, чтобы изучить и полюбить новый метод строкового формата, уже доступный в версии 2.6. - person Ned Deily; 12.10.2009

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

Только операторы перегрузки, если это естественное, ожидаемое действие и не имеет побочных эффектов.

Поэтому, если вы создаете новый класс RomanNumeral, имеет смысл перегрузить сложение и вычитание и т. Д. Но не перегружайте его, если это не является естественным: нет смысла определять сложение и вычитание для объекта Car или Vehicle.

Еще одно практическое правило: не перегружайте ==. Из-за этого очень сложно (хотя и возможно) проверить, являются ли два объекта одинаковыми. Я допустил эту ошибку и долго расплачивался за нее.

Что касается того, когда перегружать +=, ++ и т. Д., Я бы сказал: перегружайте только дополнительные операторы, если у вас есть большой спрос на эту функциональность. Легче иметь один способ сделать что-то, чем пять. Конечно, это означает, что иногда вам придется писать x = x + 1 вместо x += 1, но можно и больше кода, если он будет понятнее.

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

РЕДАКТИРОВАТЬ: Я хотел добавить пояснительную записку о перегрузке ==, потому что кажется, что разные комментаторы неправильно это понимают, и это меня поймало. Да, is существует, но это другая операция. Скажем, у меня есть объект x, который либо из моего настраиваемого класса, либо является целым числом. Я хочу проверить, является ли x числом 500. Но если вы установите x = 500, а затем протестируете x is 500, вы получите False из-за того, как Python кэширует числа. С 50 он вернет True. Но вы не можете использовать is, потому что вы можете захотеть, чтобы x == 500 возвращал True, если x является экземпляром вашего класса. Сбивает с толку? Определенно. Но это то, что вам нужно знать, чтобы успешно перегрузить операторы.

person Peter    schedule 12.10.2009
comment
Перегрузка ++ особо не применима, поскольку в Python нет оператора ++. - person Chris Lutz; 12.10.2009
comment
конечно, я изменю пример на Python. (хотя я хотел объяснить общий принцип). - person Peter; 12.10.2009
comment
Разве вы не можете проверить, совпадают ли два объекта с помощью if a is b: ..., даже если == перегружен? Или я неправильно понимаю, о чем вы говорите? - person sth; 12.10.2009
comment
Нет ничего плохого в перегрузке == / __eq__, на самом деле это, вероятно, один из наиболее перегруженных методов. Python имеет is для проверки идентичности объекта. - person Jochen Ritzel; 12.10.2009
comment
ну только вроде. например, в большинстве реализаций Python x = 1<newline>x is 1 будет истинным, а x = 500<newline> x is 500 - нет. это такие вещи, которые очень запутываются, очень быстро - person Peter; 12.10.2009
comment
вместо этого вы можете просто использовать object .__ eq __ (a, b). но имеет смысл перегрузить == чем-то, что в любом случае означает равенство для вашего класса - person John La Rooy; 12.10.2009
comment
на самом деле использование __eq__ аналогично использованию ==. - person Peter; 12.10.2009
comment
x is x должен возвращать False для всех чисел, но в cpython этого не происходит, потому что компилятор оптимизирует некоторые константы. Но это не часть языка. Код, который проверяет что-то, что должен всегда возвращать False, просто плох для начала, так что не вините в этом Python. - person Jochen Ritzel; 12.10.2009
comment
ну, пока вы не пытаетесь запустить код, который должен работать, но не из-за простых проблем с реализацией. - person Peter; 12.10.2009
comment
Обратите внимание, что в обновленном тексте вы перепутали ==, что сбивает с толку. х = 500; х == 500; всегда должно быть правдой :). - person James Antill; 12.10.2009
comment
@Peter, вы говорите не перегружать +=' unless absolutely necessary, and instead rely on writing x = x + 1` Но если вы перегрузили +, не будет += вернуться к перегруженному плюсу и сделать x = x + 1, заставляя += работать должным образом? Этот ответ, кажется, предполагает, что переопределить «+ =» в Python? (метод __iadd __ ()) - person Kyle Heuton; 01.03.2015

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

Я только что нашел Lumberjack, который использует этот синтаксис в реальном коде.



class pipely(object):
    def __init__(self, *args, **kw):
        self._args = args
        self.__dict__.update(kw)

    def __ror__(self, other):
        return ( self.map(x) for x in other if self.filter(x) )

    def map(self, x):
        return x

    def filter(self, x):
        return True

class sieve(pipely):
    def filter(self, x):
        n = self._args[0]
        return x==n or x%n

class strify(pipely):
    def map(self, x):
        return str(x)

class startswith(pipely):
    def filter(self, x):
        n=str(self._args[0])
        if x.startswith(n):
            return x

print"*"*80
for i in xrange(2,100) | sieve(2) | sieve(3) | sieve(5) | sieve(7) | strify() | startswith(5):
    print i

print"*"*80
for i in xrange(2,100) | sieve(2) | sieve(3) | sieve(5) | sieve(7) | pipely(map=str) | startswith(5):
    print i

print"*"*80
for i in xrange(2,100) | sieve(2) | sieve(3) | sieve(5) | sieve(7) | pipely(map=str) | pipely(filter=lambda x: x.startswith('5')):
    print i

person John La Rooy    schedule 12.10.2009
comment
+1, потому что это очень интересно, хотя я не знаю, одобряю ли я использование этого в реальном коде. - person Chris Lutz; 12.10.2009
comment
:) Я админ, я не использовал его в реальном коде, но это удобный способ связать генераторы вместе. Вы можете сделать что-то подобное с сопрограммами, но синтаксис становится больше похожим на `sieve (2, sieve (3, sieve (5, sieve (7))))`, который мне не нравится больше - person John La Rooy; 12.10.2009

Перегрузка в Python в целом «безопаснее», чем в C ++ - например, оператор присваивания не может быть перегружен, а += имеет разумную реализацию по умолчанию.

Однако в некотором смысле перегрузка в Python все еще так же «сломана», как и в C ++. Программисты должны сдерживать желание «повторно использовать» оператор для несвязанных целей, таких как повторное использование битовых сдвигов в C ++ для выполнения форматирования и синтаксического анализа строк. Не перегружайте оператор семантикой, отличной от вашей реализации, только для того, чтобы получить более красивый синтаксис.

Современный стиль Python категорически не поощряет «мошенническую» перегрузку, но многие аспекты языка и стандартной библиотеки сохраняют плохо названные операторы для обратной совместимости. Например:

  • %: модуль и форматирование строки
  • +: сложение и конкатенация последовательностей
  • *: умножение и повторение последовательности

Итак, практическое правило? Если ваша реализация оператора удивит людей, не делайте этого.

person John Millikin    schedule 12.10.2009
comment
Я думаю, что двойное использование + и * заслуживает - оба использования, по крайней мере, делают одну и ту же концептуальную вещь, даже если они делают это по-разному. - person Chris Lutz; 12.10.2009
comment
Они совсем не такие. (1+2)==(2+1), но ("a"+"b")!=("b"+"a"). (1+2-1)==2, но ("a"+"b"-"a") ерунда. Такие же проблемы существуют и с умножением. - person John Millikin; 12.10.2009
comment
Я не хотел сказать, что они такие же, но я сказал об этом плохо. Я имел в виду, что концептуально они выполняют аналогичные действия. Я думаю, что большинство людей сказали бы, что объединение двух строк и сложение двух чисел как аддитивные операции, и что умножение чисел и повторение строки несколько раз являются мультипликативными операциями. - person Chris Lutz; 12.10.2009
comment
единственный раз, когда я столкнулся * с перегрузкой в ​​Python, это было удивительно и неоднозначно (кто-то спрашивал, почему умножение матриц 500x500 было быстрее в Python, чем в Java - ответ был, что оператор * выполнял умножение массивов, а не умножение матриц) - person Pete Kirkham; 20.12.2009
comment
@JohnMillikin Что такого странного в этом поведении? В матричной / тензорной математике это очень общепринятое явление .. И вы не собираетесь говорить мне, что для матричного класса вам придется использовать функции? Это ужасно, когда выражение на бумаге выглядит так: (A + B) * (C + D) - person paul23; 14.12.2011