8

I have a class that need to make some magic with every operator, like __add__, __sub__ and so on.

Instead of creating each function in the class, I have a metaclass which defines every operator in the operator module.

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)

The approach works fine, but I think It would be better if I generate the replacement operator only when needed.

Lookup of operators should be on the metaclass because x + 1 is done as type(x).__add__(x,1) instead of x.__add__(x,1), but it doesn't get caught by __getattr__ nor __getattribute__ methods.

That doesn't work:

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

Also, the resulting "function" must be a method bound to the instance used.

Any ideas on how can I intercept this lookup? I don't know if it's clear what I want to do.


For those questioning why do I need to this kind of thing, check the full code here. That's a tool to generate functions (just for fun) that could work as replacement for lambdas.

Example:

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

Just for the record, I don't want to know another way to do the same thing (I won't declare every single operator on the class... that will be boring and the approach I have works pretty fine :). I want to know how to intercept attribute lookup from an operator.

JBernardo
  • 32,262
  • 10
  • 90
  • 115
  • "I have a class that need to make some magic with every operator" - Why? It sounds like you are barking up a very complicated tree... – Lennart Regebro Dec 26 '11 at 21:16
  • @LennartRegebro I'm writing a function generator using the operators on some object. `f = FuncBuilder(); g = f ** 2 + 1; g(10) == 101`. It's not something very useful (lots of function calls), but is somewhat fun to use :D – JBernardo Dec 26 '11 at 21:58
  • @LennartRegebro I posted the full code. – JBernardo Dec 26 '11 at 22:08
  • OK, so you have created a way to make lambdas. :-) As long as you do it for fun, it's all OK. :-) – Lennart Regebro Dec 27 '11 at 05:00

4 Answers4

7

Some black magic let's you achieve your goal:

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!

Output

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

Now you can use your second code sample literally by inheriting from my OperatorHackiness base class. You even get an additional benefit: __getattr__ will only be called once per instance and operator and there is no additional layer of recursion involved for the caching. We hereby circumvent the problem of method calls being slow compared to method lookup (as Paul Hankin noticed correctly).

NOTE: The loop to add the operator methods is only executed once in your whole program, so the preparation takes constant overhead in the range of milliseconds.

AMC
  • 2,642
  • 7
  • 13
  • 35
Niklas B.
  • 92,950
  • 18
  • 194
  • 224
  • Well, looks like your `for` loop is adding all the operators to the class (look at my code, I also do that). I want to **not** have them :). BTW, i think it's already a improvement. – JBernardo Dec 29 '11 at 16:28
  • @JBernardo: Look again. It works entirely different to your code. What is added are not the created operator functions but only shallow wrappers around a `__getattr__` call. This is necessary, because, as you said, you cannot intercept those method calls using a custom `__getattr__` function. As the loop is only executed _once in your whole program_ and the number of operators is finite, it takes constant overhead in the range of milliseconds. Basically, this is a hack to allow you to use `__getattr__` to intercept operators like any other methods (which is exactly what you requested). – Niklas B. Dec 29 '11 at 16:33
  • I understand your code (also you should add these comments to the answer), but what you do is: `x + y -> x.__add__ -> x.__getattr__('__add__')`. It's a interesting idea, but seems like not having the operators is impossible somehow. – JBernardo Dec 29 '11 at 18:34
  • It's more like `x + y -> x.__add__ -> cached_func = x.__getattr__('__add__')`. The second time it's `x + y -> cached_func` directly. You're right in so far as it is not possible to intercept addition without having an `__add__` method at all (why should it be?). This should be the closest you can possibly get to a solution of your problem. – Niklas B. Dec 29 '11 at 18:40
  • I'm accepting your answer because is the closest I can get from what I want, but I'll wait a little more to give the bounty. Thanks – JBernardo Dec 29 '11 at 19:07
  • Okay, fair enough. Maybe someone comes up with a more elegant solution :) – Niklas B. Dec 29 '11 at 19:36
  • @EthanFurman: I'm a Ruby coder, so I got to love metaclasses :) – Niklas B. Jan 01 '12 at 15:29
  • Just as a point of clarification, you aren't actually using metaclasses in the Python sense. – Ethan Furman Jan 02 '12 at 07:03
  • @EthanFurman: I know, it's more a simulation of singleton/eigen/metaclasses in Ruby. – Niklas B. Jan 02 '12 at 12:57
  • @NiklasB.Just a small question - Why would you create the instance temporary class? It will work perfectly without it, and considering the fact his methods are going to use instance variables and not pre-set variables then they will work just fine in any other instance of the same class. – Bharel Jan 27 '18 at 15:10
3

The issue at hand is that Python looks up __xxx__ methods on the object's class, not on the object itself -- and if it is not found, it does not fall back to __getattr__ nor __getattribute__.

The only way to intercept such calls is to have a method already there. It can be a stub function, as in Niklas Baumstark's answer, or it can be the full-fledged replacement function; either way, however, there must be something already there or you will not be able to intercept such calls.

If you are reading closely, you will have noticed that your requirement for having the final method be bound to the instance is not a possible solution -- you can do it, but Python will never call it as Python is looking at the class of the instance, not the instance, for __xxx__ methods. Niklas Baumstark's solution of making a unique temp class for each instance is as close as you can get to that requirement.

Ethan Furman
  • 63,992
  • 20
  • 159
  • 237
1

It looks like you are making things too complicated. You can define a mixin class and inherit from it. This is both simpler than using metaclasses and will run faster than using __getattr__.

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

Then every class you want to have these operators, inherit from OperatorMixin.

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

Generating the operator methods when they're needed isn't a good idea: __getattr__ is slow compared to regular method lookup, and since the methods are stored once (on the mixin class), it saves almost nothing.

  • Yeah, there are at least 10 operators (plus inplace and reversed forms) and I don't want to write them by hand and calling the exact same function (changing the operator) for each of them. – JBernardo Dec 26 '11 at 17:04
  • My idea now is to *only* create the `func` or `rfunc` when the operator gets called. – JBernardo Dec 26 '11 at 17:05
  • What will creating the functions lazily give you? –  Dec 26 '11 at 17:09
  • That's because the class is a function generator. It's designed to be used little times and the resulting object is the one to be called as many times as the user wants. – JBernardo Dec 26 '11 at 17:12
  • In that case I'd make a class full of functions like "make_adder" etc. Don't override anything. Then subclass from that baseclass for every specific class you need. – Lennart Regebro Dec 26 '11 at 21:17
  • That would take away the fun of using operators ;) – JBernardo Dec 26 '11 at 22:10
  • `__getattr__` isn't even called for `__xxx__` methods. So, yeah, lousy performance. ;) – Ethan Furman Jan 02 '12 at 07:25
0

If you want to achieve your goal without metaclasses, you can append the following to your code:

def get_magic_wrapper(name):
    
    def wrapper(self, *a, **kw):
        print('Wrapping')
        res = getattr(self._data, name)(*a, **kw)
        return res
    return wrapper

_magic_methods = ['__str__', '__len__', '__repr__']
for _mm in _magic_methods:
    setattr(ShowMeList, _mm, get_magic_wrapper(_mm))

It reroutes the methods in _magic_methods to the self._data object, by adding these attributes to the class iteratively. To check if it works:

>>> l = ShowMeList(range(8))
>>> len(l)
Wrapping
8
>>> l
Wrapping 
[0, 1, 2, 3, 4, 5, 6, 7]
>>> print(l)
Wrapping
[0, 1, 2, 3, 4, 5, 6, 7]

drmaettu
  • 71
  • 4