4

I have a class A.

I have another class B. instances of class B should function exactly like class A, except for one caveat: I want another function available called special_method(self, args, kwargs)

So the following should work:

instance_A = classA(args, kwargs)
instance_B = classB(instance_A)
method_result = instance_B.special_method(args, kwargs)

How do I write class B to accomplish this?

Note: If I only wanted to do this for ONE class A, I could just have class B inherit class A. but I want to be able to add special_method to class C, D, E, F... etc.

user3180
  • 1,369
  • 1
  • 21
  • 38
  • Will you require the use of special methods, i.e. "dunder" methods like `__add__`, `__len__` etc? – juanpa.arrivillaga Apr 26 '21 at 05:10
  • Perhaps you want a *mixin class*, that just contains `.special_method()`. You can easily defined extended versions of your other classes that add this: `class A_extended(A, B): pass` for example. – jasonharper Apr 26 '21 at 05:16
  • @juanpa.arrivillaga I would expect instance_B to function exactly like instance_A except with the addition of a new function. All methods should work appropriately. – user3180 Apr 26 '21 at 05:17
  • You can also try method in this question https://stackoverflow.com/a/21060094/10798048 – Dhruv Agarwal Apr 26 '21 at 05:33

1 Answers1

5

So, you are describing a proxy object. Doing this for non-special methods is trivial in Python, you can use the __getattr__

In [1]: class A:
   ...:     def foo(self):
   ...:         return "A"
   ...:

In [2]: class B:
   ...:     def __init__(self, instance):
   ...:         self._instance = instance
   ...:     def special_method(self, *args, **kwargs):
   ...:         # do something special
   ...:         return 42
   ...:     def __getattr__(self, name):
   ...:         return getattr(self._instance, name)
   ...:

In [3]: a = A()

In [4]: b = B(a)

In [5]: b.foo()
Out[5]: 'A'

In [6]: b.special_method()
Out[6]: 42

However, there is one caveat here: this won't work with special methods because special methods skip this part of attribute resolution and are directly looked up on the class __dict__.

An alternative, you can simply add the method to all the classes you need. Something like:

def special_method(self, *args, **kwargs):
    # do something special
    return 42

for klass in [A, C, D, E, F]:
    klass.special_method = special_method

Of course, this would affect all instances of these classes (since you are simply dynamically adding a method to the class).

If you really need special methods, your best best would by to create a subclass, but you can do this dynamically with a simple helper function, e.g.:

def special_method(self, *args, **kwargs):
    # do something special
    return 42

_SPECIAL_MEMO = {}

def dynamic_mixin(klass, *init_args, **init_kwargs):
    if klass not in _SPECIAL_MEMO:
        child = type(f"{klass.__name__}Special", (klass,), {"special_method":special_method})
        _SPECIAL_MEMO[klass] = child
    return _SPECIAL_MEMO[klass](*init_args, **init_kwargs)

class Foo:
    def __init__(self, foo):
        self.foo = foo
    def __len__(self):
        return 88
    def bar(self):
        return self.foo*2

special_foo = dynamic_mixin(Foo, 10)

print("calling len", len(special_foo))
print("calling bar", special_foo.bar())
print("calling special method", special_foo.special_method())

The above script prints:

calling len 88
calling bar 20
calling special method 42
juanpa.arrivillaga
  • 88,713
  • 10
  • 131
  • 172