Search code examples
pythonoopoverloadingclass-methodinstance-methods

How to define one method as both instance method and class method (sharing the same name), each with different arguments?


In essence, I'm trying to accomplish the below but where bar and baz have the same handle (e.g. just bar) rather than being two differently-named functions.

Definitions
import numpy as np
foo = np.add # For example; real function will be different in every instance

class MyClass(object):
    def __init__(self, arg1, arg2):
        self.arg1 = arg1
        self.arg2 = arg2

    def bar(self):
        return foo(self.arg1, self.arg2)

    @classmethod
    def baz(cls, arg1, arg2):
        return foo(arg1, arg2)
Example Usage
a, b = 1, 2
mine = MyClass(a, b)
print(mine.bar())
>>> 3
x, y = 3, 4
print(MyClass.baz(x, y))
>>> 7

I'm trying to do this for intelligibility's sake. The real function names are long with many underscores, and making two slightly differently-named functions (e.g. prepending one function name with a _) for every function to which I want to do this is only going to confuse an already complicated situation.

The function will mostly be used internally, but I'd like the ability to call the function in a static context with ad hoc parameters that might not necessarily match the instance variables of a given object of MyClass (in fact, I would only call it this way if they didn't match). I'm using @classmethod rather than @staticmethod because the real functions use some internal class variables.

I've already tried simply implementing the above with bar for both function names, but as expected, the instance method has been overridden by the class method.

  • I suppose one awkward solution would be to define bar as:

    def bar(self, arg1=None, arg2=None):
        if arg1 is None:
            arg1 = self.arg1
        if arg2 is None:
            arg2 = self.arg2
        return foo(arg1, arg2)
    

    and then, if I wanted to call it statically elsewhere, I would need to use:

    # Assuming that a specific object of MyClass does not exist yet
    useless = "not", "used"
    mine = MyClass(*useless)
    
    x, y = 6, 9
    print(mine.bar(x, y))
    

    I don't really like this because it requires me to call bar from a specific object of MyClass. This is technically not an issue if I already have an instance of MyClass somewhere, but I want a class method for a reason: bar in this context doesn't depend on any instance variables (though it does on class variables).

  • Another potential solution would be to make a separate class for this function (and others):

    class Functions(object):
        @classmethod
        def bar(cls, arg1, arg2):
            return foo(arg1, arg2)
    

    and to call it within MyClass as Functions.bar(self.arg1, self.arg2) and externally as Functions.bar(arg1, arg2)

    I'm not super keen on this either since I'm putting bar and similar functions in MyClass for a reason: they're relevant to MyClass conceptually.

I saw some answers to similar SO posts that use Descriptors, but I was hoping there might be a more elegant solution.

Any wise Python wizards out here have advice?


Solution

  • You can let self take a default argument as well, so that you can distinguish between mine.bar() and MyClass.bar(). The price is that the other two arguments must be keyword arguments.

    class MyClass:
    
        _sentinel = object()
    
        def bar(self=None, *, arg1=_sentinel, arg2=_sentinel):
            if self is not None:
                if arg1 is _sentinel:
                    arg1 = self.arg1
                if arg2 is _sentinel:
                    arg2 = self.arg2
            else:
                if arg1 is _sentinel:
                    raise ValueError("Missing required arg1")
                if arg2 is _sentinel:
                    raise ValueError("Missing required arg2")
    
            return foo(arg1, arg2)
    
    
    mine.bar()  # self is mine, arg1 is _sentinel, arg2 is _sentinel
    
    MyClass.bar(arg1=3, arg2=4)