Динамическая перегрузка операторов на классах dict в Python - PullRequest
3 голосов
/ 23 апреля 2010

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

import operator

class IshyNum:
    def __init__(self, n):
        self.num=n
        self.buildArith()

    def arithmetic(self, other, o):
        return o(self.num, other)

    def buildArith(self):
        map(lambda o: setattr(self, "__%s__"%o,lambda f: self.arithmetic(f, getattr(operator, o))), ["add", "sub", "mul", "div"])

if __name__=="__main__":
    number=IshyNum(5)
    print number+5
    print number/2
    print number*3
    print number-3

Но если я изменю класс для наследования из словаря (class IshyNum(dict):), это не сработает. Мне нужно явно def __add__(self, other) или что-то еще, чтобы это работало. Почему?

Ответы [ 4 ]

4 голосов
/ 23 апреля 2010

Ответ находится в двух типах классов, которые есть в Python.

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

class Foo:
   def __init__(self, num):
      self.num = num
      def _fn(other):
         return self.num + other.num
      self.__add__ = _fn

и получите правильный ответ:

>>> f = Foo(2)
>>> g = Foo(1)
>>> f + g
3

Но, подкласс dict означает, что вы генерируете класс нового стиля. И семантика перегрузки операторов различна:

class Foo (object):
   def __init__(self, num):
      self.num = num
      def _fn(other):
         return self.num + other.num
      self.__add__ = _fn
>>> f = Foo(2)
>>> g = Foo(1)
>>> f + g
Traceback ...
TypeError: unsupported operand type(s) for +: 'Foo' and 'Foo'

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

class _MetaFoo(type):
    def __init__(cls, name, bases, args):
        def _fn(self, other):
            return self.num + other.num
        cls.__add__ = _fn

class Foo(object):
    __metaclass__ = _MetaFoo
    def __init__(self, num):
        self.num = num

>>> f = Foo(2)
>>> g = Foo(1)
>>> f+g
3

Кроме того, семантическая разница означает, что в самом первом случае я мог определить свой локальный метод добавления с одним аргументом (self, который он использует, захватывается из окружающей области, в которой он определен), но с новым стилем классы, Python ожидает явной передачи обоих значений, поэтому внутренняя функция имеет два аргумента.

Как упомянул предыдущий комментатор, лучше избегать классов старого стиля, если это возможно, и придерживаться классов нового стиля (классы старого стиля удалены в Python 3+). К сожалению, классы старого стиля сработали для вас в этом случае, когда классы нового стиля потребуют больше кода.


Редактировать:

Вы также можете сделать это больше, чем пытались изначально, установив метод для класса вместо instance :

class Foo(object):
    def __init__(self, num):
        self.num = num
setattr(Foo, '__add__', (lambda self, other: self.num + other.num))
>>> f = Foo(2)
>>> g = Foo(1)
>>> f+g
3

Боюсь, я иногда думаю в метаклассах, где более простые решения были бы лучше:)

3 голосов
/ 23 апреля 2010

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

Вы, вероятно, хотите использовать метакласс, а не странную вещь, которую вы здесь делаете.

Вот учебник метакласса: http://www.voidspace.org.uk/python/articles/metaclasses.shtml

2 голосов
/ 23 апреля 2010

Для классов нового стиля Python не проверяет экземпляр для метода __add__ при выполнении сложения, вместо этого он проверяет класс. Проблема заключается в том, что вы привязываете метод __add__ (и все остальные) к экземпляру как связанный метод, а не к классу как несвязанный метод. (Это верно и для других специальных методов, вы можете прикрепить их только к классу, а не к экземпляру). Таким образом, вы, вероятно, захотите использовать метакласс для достижения этой функциональности (хотя я думаю, что это очень неловко, поскольку гораздо проще прочитать эти методы явно). Во всяком случае, вот пример с метаклассами:

import operator

class OperatorMeta(type):
    def __new__(mcs, name, bases, attrs):
        for opname in ["add", "sub", "mul", "div"]:
            op = getattr(operator, opname)
            attrs["__%s__" % opname] = mcs._arithmetic_func_factory(op)
        return type.__new__(mcs, name, bases, attrs)

    @staticmethod
    def _arithmetic_func_factory(op):
        def func(self, other):
            return op(self.num, other)
        return func

class IshyNum(dict):
    __metaclass__ = OperatorMeta

    def __init__(self, n):
        dict.__init__(self)
        self.num=n

if __name__=="__main__":
    number=IshyNum(5)
    print number+5
    print number/2
    print number*3
    print number-3
2 голосов
/ 23 апреля 2010

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

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

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

    • Если вы хотите автоматизировать процесс выполнения одной и той же вещи для нескольких методов (вероятно, это не стоит в этомcase), вы можете сделать это сразу после блока определения класса.

      • Не используйте map для этого.map для составления списка;использовать его для побочных эффектов глупо.Просто используйте обычный цикл for.
  • Если вы хотите использовать состав для автоматической ссылки множества методов на один и тот же атрибут при использовании композиции, используйте __getattr__ и перенаправить на методы этого атрибута.

  • Не наследовать dict.Нет ничего особенного в том, чтобы наследовать встроенные типы.Оказывается, это больше сбивает с толку, чем стоит, и вам не придется многократно использовать.

    • Если ваш код выше чем-то близок к материалу в вашем посте, вы действительно не хочу наследовать dict.Если это не так, попробуйте опубликовать ваш реальный вариант использования.

Вот что вы действительно хотели знать:

  • Когда вы наследуетеdict, вы делаете новый класс .IshyNum является классом старого стиля, потому что он не наследует object (или один из его подклассов).

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

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