2

For instance, let's say we have a Matrix class that implements matrices.

We want to to be able to use the '+' operator to add 2 matrices, or to add 1 matrix with 1 number (which is actually going to be defined as adding the number to each element of the matrix).

In C++, we could have done (skipping details with '...'):

class Matrix
{
    ...

    Matrix operator + (Matrix A, Matrix B)
    {
        ... // process in some way
    }

    Matrix operator + (Matrix A, double x)
    {
        ... // process in some other way
    }
}

But in Python as I understand, multiple definitions override the previous ones.

class Matrix:

    ...

    def __add__(A, B):
        ... # process in some way

    def __add__(A, x):
        ... # process in some other way

This does NOT work: only the last method definition is active.

So the first solution I could think of was to define only one method and parse arguments (according to their types, number, or keywords).

For example, with type-checking we could do something like (let's call it technique #1):

class Matrix:
    
    ...

    def __add__(A, X):
        if isinstance(X, Matrix)
            ... # process in some way
        elif isinstance(X, (int, float, complex))
            ... # process in some other way
        else:
            raise TypeError("unsupported type for right operand")

But I have often read that type-checking is not 'pythonic', so what else?

Moreover, in this case, there are always two arguments, but more generally what if we want to be able to process different numbers of arguments ?

To clear things up, let's say we have a method named 'mymethod' and we want to be able to call, say:

mymethod(type1_arg1)
mymethod(type2_arg1)
mymethod(type3_arg1, type4_arg2)

Let's also assume that:

  • each versions process the same object so it must be one unique method,
  • each processing is different and that arguments types matter.

---------------------------------------------------------- EDIT ----------------------------------------------------------

Thanks everyone for your interest in this topic.

A good alternative to type-checking was suggested below by @thegreatemu (let's call it technique #2). It uses the duck-typing paradigm and exception processing. But as far as I know it only works when the number of arguments in the same in all cases.

From what I understood from answers here and on the linked topic, when we want to process different numbers of arguments, we can use keyword arguments. Here is an example (let's call it technique #3).

class Duration:
    """Durations in H:M:S format."""

    def __init__(hours, mins=None, secs=None):
        """Works with decimal hours or H:M:S format."""
        
        if mins and secs: # direct H:M:S
            ... # checks
            self.hours = hours
            self.mins = mins
            self.secs = secs

        else: # decimal
           ... # checks
           ... # convert decimal hours to H:M:S

Or we can also use variable length argument *args (or **kwargs). Here is the alternate version with *args and length checking (let's call it technique #4).

class Duration:
    """Durations in H:M:S format."""

    def __init__(*args): # direct H:M:S
        """Works with decimal hours or H:M:S format."""
        
        if len(args) == 3 # direct H:M:S format
            ... # checks
            self.hours = args[0]
            self.mins = args[1]
            self.secs = args[2]

        elif len(args) == 1: # decimal
           ... # checks
           ... # convert decimal hours to H:M:S
           
        else:
            raise TypeError("expected 1 or 3 arguments")

What is best between techniques #3 and #4 in this case? Matter of taste maybe.

But as I vaguely mentioned in the original post, decorators may also be used. In particular, the linked topic mentions the overloading module that provides an API similar to C++ overloading with @overload decorator. See PEP3124. It certainly looks handy (and that would be technique #5)!

lostdatum
  • 23
  • 6
  • 1
    You do the last part using default arguments. `def mymethod(self, arg1, arg2=defaultValue):` – Barmar Oct 21 '20 at 16:50
  • 1
    I use `isinstance` and actually prefer it because it makes it explicit that there is different functionality based on what type of object is being passed in. In regard to your second question, use either default values or pass in `*args` or `**kwargs`. – Chrispresso Oct 21 '20 at 16:52
  • 1
    "using decorators" is pretty vague as a description of a solution. It is basically (almost exactly) equivalent as saying "use functions". In any case, no, Python doesnt have method overloading. – juanpa.arrivillaga Oct 21 '20 at 17:46
  • @juanpa.arrivillaga Indeed, I came across different techniques perusing tutorials, but I have added what seems to be the most user-friendly to my question. – lostdatum Oct 22 '20 at 13:38

2 Answers2

1

The alternative to type checking in python is "duck typing." The origin of the term "duck typing" is the phrase "if it looks like a duck and walks like a duck and quacks like a duck, it's probably a duck."

So if I have a Vector class, representing a 2D Euclidian vector:

class Vector(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        return Vector(self.x+other.x, self.y+other.y)

Now if I have a Coordinate object that also has x and y attributes, I can add it to my Vector and it will behave at least somewhat as expected. If I try to add a Vector to a float, I'll get an AttributeError, which in this case is what I want, since adding a scalar to a vector doesn't make sense mathematically. But let's say I want to do something like numpy's broadcasting, so adding a scalar to a vector adds it to both x and y. Then the __add__ method would become:

    def __add__(self, other):
        try:
            x, y = self.x + other.x, self.y + other.y
        except AttributeError:
            x, y = self.x + other, self.y + other
        return Vector(x, y)

Now I can accept any object that quacks like a Vector (i.e., any object that has x and y attributes) or ints, floats, or any object that can be sensibly added to the attributes, so it will "just work" if someone tries to add a Decimal.

In a real implementation, you should do more exception catching to generate clear error messages, and should also implement the __radd__ method, etc.

thegreatemu
  • 495
  • 2
  • 11
  • Well thanks that is interesting. Though to me it feels a bit hacky to use exceptions like this. Is it common and recommended practice in Python? Are there any benefits over type checking in this kind of scenario? – lostdatum Oct 21 '20 at 23:13
  • 1
    I come from C++ and thought the exception approach in python was odd too, but it seems to be the most "pythonic" way to approach things. You could equivalently do `if hasattr(other, 'x')`, etc. The advantage over type-checking is being able to handle classes you didn't think about. Your overloaded case wouldn't work with `Decimal`s, but the duck-typed method does – thegreatemu Oct 21 '20 at 23:55
  • Even though it does not really provide a solution for cases in which we want different numbers of arguments, this is still the most helpful answer so I'll flag it as accepted. – lostdatum Oct 22 '20 at 13:21
0

no, if you declare a method more than once in the same scope the last definition is what's used. even if you have a different number of arguments and the same function definition, only the last one will be used.

this is determined by the function name.

Mike Lyons
  • 22,575
  • 2
  • 16
  • 19