Оператор за прехващане на метаклас

Имам клас, който трябва да направи малко магия с всеки оператор, като __add__, __sub__ и т.н.

Вместо да създавам всяка функция в класа, имам метаклас, който дефинира всеки оператор в операторния модул.

import operator
class MetaFuncBuilder(type):
    def __init__(self, *args, **kw):
        super().__init__(*args, **kw)
        attr = '__{0}{1}__'
        for op in (x for x in dir(operator) if not x.startswith('__')):
            oper = getattr(operator, op)

            # ... I have my magic replacement functions here
            # `func` for `__operators__` and `__ioperators__`
            # and `rfunc` for `__roperators__`

            setattr(self, attr.format('', op), func)
            setattr(self, attr.format('r', op), rfunc)

Подходът работи добре, но мисля, че би било по-добре, ако генерирам заместващия оператор само когато е необходимо.

Търсенето на оператори трябва да е в метакласа, защото x + 1 се извършва като type(x).__add__(x,1) вместо x.__add__(x,1), но не се улавя от __getattr__ или __getattribute__ методи.

Това не работи:

class Meta(type):
     def __getattr__(self, name):
          if name in ['__add__', '__sub__', '__mul__', ...]:
               func = lambda:... #generate magic function
               return func

Освен това получената "функция" трябва да бъде метод, обвързан с използвания екземпляр.

Някакви идеи как мога да прихвана това търсене? Не знам дали е ясно какво искам да правя.


За тези, които се питат защо ми трябват подобни неща, проверете пълния код тук. Това е инструмент за генериране на функции (само за забавление), които могат да работят като заместител на lambdas.

Пример:

>>> f = FuncBuilder()
>>> g = f ** 2
>>> g(10)
100
>>> g
<var [('pow', 2)]>

Само за протокола, не искам да знам друг начин да направя същото (няма да декларирам всеки отделен оператор в класа... това ще бъде скучно и подходът, който имам, работи доста добре :). Искам да знам как да прихвана търсене на атрибут от оператор.


person JBernardo    schedule 26.12.2011    source източник
comment
Имам клас, който трябва да направи малко магия с всеки оператор - Защо? Звучи сякаш лаете много сложно дърво...   -  person Lennart Regebro    schedule 27.12.2011
comment
@LennartRegebro Пиша генератор на функции, използвайки операторите на някакъв обект. f = FuncBuilder(); g = f ** 2 + 1; g(10) == 101. Не е нещо много полезно (много извиквания на функции), но е донякъде забавно за използване :D   -  person JBernardo    schedule 27.12.2011
comment
@LennartRegebro Публикувах пълния код.   -  person JBernardo    schedule 27.12.2011
comment
Добре, значи сте създали начин да правите ламбда. :-) Докато го правиш за удоволствие, всичко е наред. :-)   -  person Lennart Regebro    schedule 27.12.2011


Отговори (3)


Малко черна магия ви позволява да постигнете целта си:

operators = ["add", "mul"]

class OperatorHackiness(object):
  """
  Use this base class if you want your object
  to intercept __add__, __iadd__, __radd__, __mul__ etc.
  using __getattr__.
  __getattr__ will called at most _once_ during the
  lifetime of the object, as the result is cached!
  """

  def __init__(self):
    # create a instance-local base class which we can
    # manipulate to our needs
    self.__class__ = self.meta = type('tmp', (self.__class__,), {})


# add operator methods dynamically, because we are damn lazy.
# This loop is however only called once in the whole program
# (when the module is loaded)
def create_operator(name):
  def dynamic_operator(self, *args):
    # call getattr to allow interception
    # by user
    func = self.__getattr__(name)
    # save the result in the temporary
    # base class to avoid calling getattr twice
    setattr(self.meta, name, func)
    # use provided function to calculate result
    return func(self, *args)
  return dynamic_operator

for op in operators:
  for name in ["__%s__" % op, "__r%s__" % op, "__i%s__" % op]:
    setattr(OperatorHackiness, name, create_operator(name))


# Example user class
class Test(OperatorHackiness):
  def __init__(self, x):
    super(Test, self).__init__()
    self.x = x

  def __getattr__(self, attr):
    print "__getattr__(%s)" % attr
    if attr == "__add__":
      return lambda a, b: a.x + b.x
    elif attr == "__iadd__":
      def iadd(self, other):
        self.x += other.x
        return self
      return iadd
    elif attr == "__mul__":
      return lambda a, b: a.x * b.x
    else:
      raise AttributeError

## Some test code:

a = Test(3)
b = Test(4)

# let's test addition
print(a + b) # this first call to __add__ will trigger
            # a __getattr__ call
print(a + b) # this second call will not!

# same for multiplication
print(a * b)
print(a * b)

# inplace addition (getattr is also only called once)
a += b
a += b
print(a.x) # yay!

Изход

__getattr__(__add__)
7
7
__getattr__(__mul__)
12
12
__getattr__(__iadd__)
11

Сега можете да използвате втория си примерен код буквално, като наследите от моя базов клас OperatorHackiness. Получавате дори допълнително предимство: __getattr__ ще бъде извикан само веднъж за екземпляр и оператор и няма включен допълнителен слой рекурсия за кеширането. По този начин заобикаляме проблема с извикванията на метод, които са бавни в сравнение с търсенето на метод (както Пол Ханкин забеляза правилно).

ЗАБЕЛЕЖКА: Цикълът за добавяне на методите на оператора се изпълнява само веднъж в цялата ви програма, така че подготовката отнема постоянни разходи в диапазона от милисекунди.

person Niklas B.    schedule 29.12.2011
comment
Е, изглежда, че вашият for цикъл добавя всички оператори към класа (вижте моя код, аз също правя това). Искам да ги няма :). Между другото, мисля, че вече е подобрение. - person JBernardo; 29.12.2011
comment
@JBernardo: Погледни отново. Работи напълно различно от вашия код. Това, което се добавя, не са създадените операторни функции, а само плитки обвивки около __getattr__ повикване. Това е необходимо, тъй като, както казахте, не можете да прихванете тези извиквания на метод с помощта на персонализирана __getattr__ функция. Тъй като цикълът се изпълнява само веднъж в цялата ви програма и броят на операторите е краен, той отнема постоянни допълнителни разходи в диапазона от милисекунди. По принцип това е хак, който ви позволява да използвате __getattr__ за прихващане на оператори като всеки друг метод (което е точно това, което поискахте). - person Niklas B.; 29.12.2011
comment
Разбирам вашия код (също така трябва да добавите тези коментари към отговора), но това, което правите е: x + y -> x.__add__ -> x.__getattr__('__add__'). Това е интересна идея, но изглежда, че липсата на оператори е някак невъзможно. - person JBernardo; 29.12.2011
comment
По-скоро е x + y -> x.__add__ -> cached_func = x.__getattr__('__add__'). Вторият път е x + y -> cached_func директно. Прав си, доколкото не е възможно да се прихване събирането, без изобщо да има __add__ метод (защо трябва да бъде?). Това трябва да е възможно най-близкото решение на проблема ви. - person Niklas B.; 29.12.2011
comment
Приемам отговора ви, защото е най-близкото, което мога да получа от това, което искам, но ще изчакам още малко, за да дам наградата. Благодаря - person JBernardo; 29.12.2011
comment
Добре, честно. Може някой да измисли по-елегантно решение :) - person Niklas B.; 29.12.2011
comment
@EthanFurman: Аз съм програмист на Ruby, така че трябва да обичам метакласовете :) - person Niklas B.; 01.01.2012
comment
Само като точка за пояснение, вие всъщност не използвате метакласове в смисъла на Python. - person Ethan Furman; 02.01.2012
comment
@EthanFurman: Знам, това е по-скоро симулация на singleton/eigen/metaclass в Ruby. - person Niklas B.; 02.01.2012
comment
@NiklasB. Само малък въпрос - Защо бихте създали временния клас на екземпляра? Той ще работи перфектно без него и като се има предвид факта, че неговите методи ще използват променливи на екземпляр, а не предварително зададени променливи, тогава те ще работят добре във всеки друг екземпляр от същия клас. - person Bharel; 27.01.2018

Въпросът е, че Python търси __xxx__ методи в класа на обекта, а не в самия обект -- и ако не бъде намерен, не се връща към __getattr__ или __getattribute__.

Единственият начин да прихванете такива обаждания е да имате метод, който вече е там. Това може да бъде функция за заглушаване, както в отговора на Niklas Baumstark, или може да бъде пълноценна заместваща функция; така или иначе, обаче, трябва вече да има нещо или няма да можете да прихванете такива повиквания.

Ако четете внимателно, ще забележите, че вашето изискване за обвързване на крайния метод към екземпляра не е възможно решение - можете да го направите, но Python никога няма да го извика, тъй като Python разглежда класа на екземпляр, а не екземплярът, за __xxx__ методи. Решението на Niklas Baumstark за създаване на уникален временен клас за всеки екземпляр е възможно най-близо до това изискване.

person Ethan Furman    schedule 02.01.2012

Изглежда, че правите нещата твърде сложни. Можете да дефинирате миксин клас и да наследите от него. Това е едновременно по-просто от използването на метакласове и ще работи по-бързо от използването на __getattr__.

class OperatorMixin(object):
    def __add__(self, other):
        return func(self, other)
    def __radd__(self, other):
        return rfunc(self, other)
    ... other operators defined too

Тогава всеки клас, който искате да имате тези оператори, наследете от OperatorMixin.

class Expression(OperatorMixin):
    ... the regular methods for your class

Генерирането на операторните методи, когато са необходими, не е добра идея: __getattr__ е бавно в сравнение с редовното търсене на метод и тъй като методите се съхраняват веднъж (в класа mixin), не спестява почти нищо.

person Community    schedule 26.12.2011
comment
Да, има поне 10 оператора (плюс inplace и обърнати форми) и не искам да ги пиша на ръка и да извиквам една и съща функция (смяна на оператора) за всеки от тях. - person JBernardo; 26.12.2011
comment
Идеята ми сега е да само създавам func или rfunc, когато операторът бъде извикан. - person JBernardo; 26.12.2011
comment
Какво ще ви даде мързеливото създаване на функциите? - person ; 26.12.2011
comment
Това е така, защото класът е генератор на функции. Той е проектиран да се използва малко пъти и полученият обект е този, който трябва да бъде извикан толкова пъти, колкото потребителят иска. - person JBernardo; 26.12.2011
comment
В такъв случай бих направил клас, пълен с функции като make_adder и т.н. Не отменяйте нищо. След това подклас от този базов клас за всеки конкретен клас, от който се нуждаете. - person Lennart Regebro; 27.12.2011
comment
Това би премахнало удоволствието от използването на оператори ;) - person JBernardo; 27.12.2011
comment
__getattr__ дори не се извиква за __xxx__ методи. Така че, да, лошо представяне. ;) - person Ethan Furman; 02.01.2012