1

In my effort to subclass float and override multiple numeric operations with a wrapper, I looked at this example and tried the following:

def naturalize(*methods):
  def decorate(cls):
    for method in methods:
      method = '__' + method + '__'
      original = getattr(cls.__base__, method)
      setattr(cls, method, lambda self, *args, **kwargs: cls(original(self, *args, **kwargs)))
    return cls
  return decorate

@naturalize('add', 'sub', 'mul')
class P(float):
  pass

print('Test result:', P(.1) + .2, P(.1) - .2, P(.1) * .2)
# Test result: 0.020000000000000004 0.020000000000000004 0.020000000000000004

This didn't work: __add__, __sub__ and __mul__ were all working like __mul__. So I looked at this other example and tried:

def naturalize(*methods):
  def decorate(cls):
    def native(method):
      original = getattr(cls.__base__, method)
      return lambda self, *args, **kwargs: cls(original(self, *args, **kwargs))
    for method in methods:
      method = '__' + method + '__'
      setattr(cls, method, native(method))
    return cls
  return decorate

@naturalize('add', 'sub', 'mul')
class P(float):
  pass

print('Test result:', P(.1) + .2, P(.1) - .2, P(.1) * .2)
#Test result: 0.30000000000000004 -0.1 0.020000000000000004

Now, that did work. But I'm still not sure what exactly went wrong with my first approach. Can anyone please explain to me how exactly __add__, __sub__ and __mul__ ended up all working like __mul__?

Community
  • 1
  • 1

2 Answers2

0

In your first example, all lambdas have the same scope which means that when method was changed, it was reflected in all the lambdas. Nesting it in a function as in your second example creates a new scope per call, which isolates the lambdas from each other.

Ignacio Vazquez-Abrams
  • 776,304
  • 153
  • 1,341
  • 1,358
0

Try this debugging version of naturalize:

def naturalize(*methods):
    def decorate(cls):
        for method in methods:
            method = '__' + method + '__'
            original = getattr(cls.__base__, method)
            setattr(cls, method,
                    lambda self, *args, **kwargs:
                    (cls(original(self, *args, **kwargs)), original.__name__))
        return cls
    return decorate

You'll get output like this:

Test result: (0.020000000000000004, '__mul__') (0.020000000000000004, '__mul__') (0.020000000000000004, '__mul__')

So we see that original is always equal to __mul__ by the time the decorated method is called. Indeed, when the decorated method is called,

cls(original(self, *args, **kwargs))

is evaluated. Since original is a local variable in decorate, it retains the last value it had after the for-loop ended. So original always evaluates to __mul__.

The correct version,

def naturalize(*methods):
    def decorate(cls):
        def native(method):
            original = getattr(cls.__base__, method)
            return lambda self, *args, **kwargs: cls(original(self, *args, **kwargs))
        for method in methods:
            method = '__' + method + '__'
            setattr(cls, method, native(method))
        return cls
    return decorate

avoids this problem by defining original as a local variable in native. Now, by calling native(method) for each method, inside native original is bound once to each desired original value.

unutbu
  • 842,883
  • 184
  • 1,785
  • 1,677