2

Consider the following class hierarchy:

       A
       |
 _____________
 |     |     |
Foo   Bar   Baz

The class A defines a __call__ method, which Foo, Bar and Baz implement. The classes also define various other methods I'm interested in. All three classes come from a third party library, so I do not want to change their implementation. However, in some cases at runtime, I would like to alter the implementation of Foo's, Bar's and Baz' __call__ method. Right now, I did this by defining my own classes, which solely override the __call__ method:

Foo   Bar   Baz
 |     |     |
Foo1  Bar1  Baz1

Then, depending on the situation, I instantiate the desired class to a common variable. I'm not satisfied with this solution, as it has the following drawbacks:

  • The additions I make to __call__ are not necessarily uniquely determined by the class. Bar1 and Baz1 might share the implementation of __call__ (but Bar and Baz differ in other aspects). So, as far as I understand, I need to repeat code.

  • At runtime, instantiating the desired class requires m × n case distinctions (where m is the number of classes at the level of Foo, Bar, and Baz and n is the number of classes at the level of Foo1, Bar1, and Baz1). This will grow rapidly as m or n increase. Ideally, I would like the number of case distinction to be m + n.

I already had a look at https://python-patterns.guide/gang-of-four/composition-over-inheritance/#solution-3-the-decorator-pattern, at Cannot overwrite implementation for __call__ and at __call__ method of type class. However, these solutions do not fit perfectly to my case, as (1) I'm overriding a dunder method (2) of third party classes and (3) still would like to have access to the other class methods of Foo, Bar, and Baz.

Is there an elegant why to achieve what I am looking for?

Edit: The change of __call__ at runtime should only affect the instance object, not the class or all objects of that class at once.

Edit 2 (requested by @enzo):

# -*- coding: utf-8 -*-
from abc import ABCMeta, abstractmethod


class A(metaclass=ABCMeta):
    @abstractmethod
    def __call__(self, x, y):
        """Docstring A"""
    
    @abstractmethod
    def method1(self, a, b):
        """Docstring method1"""

class Foo(A):
    def __call__(self, x, y):
        return x + y # indeed, no dependence on `self`
    
    def method1(self, a, b):
        # but this might depend on `self`
        pass

class Bar(A):
    def __call__(self, x, y):
        return x * y # indeed, no dependence on `self`
    
    def method1(self, a, b):
        # but this might depend on `self`
        pass

    def method2(self, c, d):
        # but this might depend on `self`
        pass

class Baz(A):
    def __call__(self, x, y):
        return x ** y # indeed, no dependence on `self`
    
    def method1(self, a, b):
        # but this might depend on `self`
        pass

    def method2(self, c, d):
        # but this might depend on `self`
        pass
user3389669
  • 799
  • 6
  • 20

2 Answers2

1

I don't know if that's what you meant, but based on your sample code you could do something like this:

# Create a decorator to keep the reference to the original __call__'s
class CustomA(A):
    def __init__(self, obj):
        self.obj = obj
        
    def __call__(self, x, y):
        return self.obj(x, y)
        
    def method1(self, a, b):
        return self.obj.method1(a, b)
        
# Create a decorator that calls the original __call__
class MultiplyA(CustomA):
    def __init__(self, obj):
        super().__init__(obj)
        
    def __call__(self, x, y):
        result = super().__call__(x, y)
        return result * 10
        
# Create a decorator that ignores the original __call__
class DivideA(CustomA):
    def __init__(self, obj):
        super().__init__(obj)
        
    def __call__(self, x, y):
        # You can still access the original object' attributes here
        super().method1(x, y)
        return x / y

Usage:

foo = Foo()
print(foo(1, 2))
# Outputs 3

foo = MultiplyA(foo)
print(foo(1, 2))
# Outputs 30

bar = Bar()
print(bar(2, 3))
# Outputs 6

bar = DivideA(bar)
print(bar(10, 5))
# Outputs 2.0
enzo
  • 9,861
  • 3
  • 15
  • 38
  • From what I understand, I need to implement in `CustomA` the forwarding to `__call__`, `method1` (and other methods I did not show here), so I may focus in the other decorators on just altering the `__call__`s? – user3389669 Jul 31 '21 at 16:54
0

you can inject a custom call into it:

class A():
    def __call__(self, *args, **kwargs):
        print("In A.__call__")

    def showCall(self):
        print(self.__call__)

class foo(A):
    pass

class bar(A):
    pass

class baz(A):
    pass

def my_custom_func():
    print("In my custom func")

f = foo()
f()

b = bar()
b()

f.showCall()
b.showCall()

print("##setting call##")
f.__call__ = my_custom_func
print("##running f calls##")
f.showCall()
f()

print("##running b calls##")
b.showCall()
b()

While this does solve your injection problem other things might come into play where you still need the original __call__ method. In which case you will need to save them before setting them.

testfile
  • 2,145
  • 1
  • 12
  • 31
  • It seems that in this solution you require to alter the definition of `A`. Is that correct? – user3389669 Jul 30 '21 at 09:05
  • No. A stays as is. All you're doing is changing what function gets called on the instance of Foo or Bar. The underlying class stays the same – testfile Jul 30 '21 at 09:08
  • When I execute your code with python3, all 4 calls of `showCall()` result in `In A.__call__`. None of them prints `In my custom func`. – user3389669 Jul 30 '21 at 09:36