Some people say that Python does not support function overloading?

As we all know, Python is a dynamic language. The so-called dynamic language means that the types of variables are dynamic. During program operation, the types of variables can be changed at will. Because Python variables have no fixed types, and function overloading depends on variable types, overloading is to define multiple functions with the same name, but the parameter types of these functions with the same name are different, When different types of parameters are passed in, the corresponding functions are executed.

Python variables have no type, so Python syntax itself does not support function overloading. Therefore, it is correct to say that Python does not support function overloading. However, what this article wants to say is that the flexibility of Python dynamic language can realize multiple functions of a function without function overloading. However, if Python really supports function overloading, it can be implemented. Specifically, there are two schemes.

Scheme 1: pseudo overloading

The advantage of Java overloading is that you can see which variable types the function supports from the form of the function, but Python is not very readable because the variable has no fixed type. For example, the following function fun actually supports two parameters, one is all character strings, the other is all integers, but do not look at the code, In fact, I can't see it:

def fun(x, y):
    if isinstance(x, str) and isinstance(y, str):
        print(f"str {x =}, {y = } ")
    elif isinstance(x, int) and isinstance(y, int):
        print(f"int {x = }, {y = }")
fun("hello", "world")
fun(1,2)

Operation results

str x ='hello', y = 'world' 
int x = 1, y = 2

Fortunately, python has type hints. With the help of Python's standard library typing, we can also write overloaded Code:

import typing


class A:
    @typing.overload
    def fun(self, x: str, y: str) -> None:
        pass

    @typing.overload
    def fun(self, x: int, y: int) -> None:
        pass

    def fun(self, x, y) -> None:
        if isinstance(x, str) and isinstance(y, str):
            print(f"str {x =}, {y = } ")
        elif isinstance(x, int) and isinstance(y, int):
            print(f"int {x = }, {y = }")


if __name__ == "__main__":
    a = A()
    a.fun("hello", "world")
    a.fun(1, 2)

Operation results:

str x ='hello', y = 'world' 
int x = 1, y = 2

In this way, the readability is improved, but this is a formal overload. The last function without decorator really plays a role. The first two functions with decorator only exist for better readability and have no actual role. They can be deleted without affecting the operation of the program.

To implement true function overloading like Java, see scheme 2.

Scheme 2: realize real overloading with the help of metaclass

Metaclass is a relatively advanced feature of Python. If you give complete code at the beginning, you may not understand it. Here, the implementation process is shown step by step.

Everything in Python is an object. For example, 1 is an instance of int and int is an instance of type:

In [7]: a = 5

In [8]: type(a)
Out[8]: int

In [9]: type(int)
Out[9]: type

In [10]:
In [11]: type??
Init signature: type(self, /, *args, **kwargs)
Docstring:
type(object_or_name, bases, dict)
type(object) -> the object's type
type(name, bases, dict) -> a new type
Type:           type
Subclasses:     ABCMeta, EnumMeta, _TemplateMetaclass, _ABC, MetaHasDescriptors, NamedTupleMeta, _TypedDictMeta, LexerMeta, StyleMeta, _NormalizerMeta, ...

As can be seen from the above, type(object) returns the type of object, and type(name, bases, dict) will generate a new type, that is, type(name, bases, dict) will generate a class:

In [17]: A = type('A',(),{})

In [18]: a = A()

In [19]: type(a)
Out[19]: __main__.A
In [20]: type(A)
Out[20]: type

The above code is equivalent to:

In [21]: class A:
    ...:     pass
    ...:

In [22]: a = A()

In [23]: type(a)
Out[23]: __main__.A

In [24]: type(A)
Out[24]: type

Understand this, even if we do not use the class keyword, we can create a class, such as the following make_ The functions of A() and A() are the same:

class A:
    a = 1
    b = "hello"
    def fun(self):
        return "Class A"

def make_A():
    name = 'A'
    bases = ()
    a = 1
    b = "hello"

    def fun():
        return "Class A"

    namespace = {'a':a,'b':b,'fun': fun}

    return type(name,bases,namespace)

if __name__ == '__main__':
    a = A()
    print(a.b)
    print(a.fun())
    print("==="*5)
    b = make_A()
    print(b.b)
    print(b.fun())

Execution results:

hello
Class A
===============
hello
Class A

Please note the make above_ There is a namespace in function a, which is a dictionary that stores the member variables and member functions of the class. When we define multiple functions with the same name in a class, the last one will overwrite all the previous ones. This is the characteristic of the dictionary. If the same key is assigned multiple times, only the last one will be retained. Therefore, Python class does not support function overloading.

Now we need to keep multiple functions with the same name, so we need to rewrite the dictionary. When the same key is assigned multiple times, keep these values (functions) in a list. Write a class for the specific method, inherit dict, and then write the code as follows:

class OverloadDict(dict):
    def __setitem__(self, key, value):
        assert isinstance(key, str), "keys must be str"

        prior_val = self.get(key, _MISSING)
        overloaded = getattr(value, "__overload__", False)

        if prior_val is _MISSING:
            insert_val = OverloadList([value]) if overloaded else value
            super().__setitem__(key, insert_val)
        elif isinstance(prior_val, OverloadList):
            if not overloaded:
                raise ValueError(self._errmsg(key))
            prior_val.append(value)
        else:
            if overloaded:
                raise ValueError(self._errmsg(key))
            super().__setitem__(key, value)

    @staticmethod
    def _errmsg(key):
        return f"must mark all overloads with @overload: {key}"

A key point of the above code is that if there is an overload flag, it will be placed in the list prior_ In Val:

elif isinstance(prior_val, OverloadList):
    if not overloaded:
        raise ValueError(self._errmsg(key))
    prior_val.append(value)

Overload list is a list, which is defined as follows:

class OverloadList(list):
    pass

Write another decorator to identify whether a function should be overloaded:

def overload(f):
    f.__overload__ = True
    return 

Then let's test the overload dict to see its effect:

print("OVERLOAD DICT USAGE")
d = OverloadDict()

@overload
def f(self):
    pass

d["a"] = 1
d["a"] = 2
d["b"] = 3
d["f"] = f
d["f"] = f
print(d)

Operation results:

OVERLOAD DICT USAGE
{'a': 2, 'b': 3, 'f': [<function overload_dict_usage.<locals>.f at 0x7fdec70090d0>, <function overload_dict_usage.<locals>.f at 0x7fdec70090d0>]}

OverloadDict solves the problem of how to save duplicate functions, that is, to put them in a list. There is another problem that has not been solved, that is, how to take the correct function from the list to execute when calling?

It must be based on the parameter type passed in by the function. How to implement it? With the help of Python's type prompt and introspection module inspect, of course, with the help of Python's metaclass:

class OverloadMeta(type):
    @classmethod
    def __prepare__(mcs, name, bases):
        return OverloadDict()

    def __new__(mcs, name, bases, namespace, **kwargs):
        overload_namespace = {
            key: Overload(val) if isinstance(val, OverloadList) else val
            for key, val in namespace.items()
        }
        return super().__new__(mcs, name, bases, overload_namespace, **kwargs)

There is an Overload class, which is used to map the signature and definition of the function. It will be called when we use a.f__ get__ Method to obtain the corresponding function. It is defined as follows:

class Overload:
    def __set_name__(self, owner, name):
        self.owner = owner
        self.name = name

    def __init__(self, overload_list):
        if not isinstance(overload_list, OverloadList):
            raise TypeError("must use OverloadList")
        if not overload_list:
            raise ValueError("empty overload list")
        self.overload_list = overload_list
        self.signatures = [inspect.signature(f) for f in overload_list]

    def __repr__(self):
        return f"{self.__class__.__qualname__}({self.overload_list!r})"

    def __get__(self, instance, _owner=None):
        if instance is None:
            return self
        # don't use owner == type(instance)
        # we want self.owner, which is the class from which get is being called
        return BoundOverloadDispatcher(
            instance, self.owner, self.name, self.overload_list, self.signatures
        )

    def extend(self, other):
        if not isinstance(other, Overload):
            raise TypeError
        self.overload_list.extend(other.overload_list)
        self.signatures.extend(other.signatures)

__ get__ The returned is a BoundOverloadDispatcher class, which binds the parameter type to the corresponding function and will be called only when the function is called__ call__ Return the most matching function to call:

class BoundOverloadDispatcher:
    def __init__(self, instance, owner_cls, name, overload_list, signatures):
        self.instance = instance
        self.owner_cls = owner_cls
        self.name = name
        self.overload_list = overload_list
        self.signatures = signatures

    def best_match(self, *args, **kwargs):
        for f, sig in zip(self.overload_list, self.signatures):
            try:
                bound_args = sig.bind(self.instance, *args, **kwargs)
            except TypeError:
                pass  # missing/extra/unexpected args or kwargs
            else:
                bound_args.apply_defaults()
                # just for demonstration, use the first one that matches
                if _signature_matches(sig, bound_args):
                    return f

        raise NoMatchingOverload()

    def __call__(self, *args, **kwargs):
        try:
            f = self.best_match(*args, **kwargs)
        except NoMatchingOverload:
            pass
        else:
            return f(self.instance, *args, **kwargs)

        # no matching overload in owner class, check next in line
        super_instance = super(self.owner_cls, self.instance)
        super_call = getattr(super_instance, self.name, _MISSING)
        if super_call is not _MISSING:
            return super_call(*args, **kwargs)
        else:
            raise NoMatchingOverload()
            
def _type_hint_matches(obj, hint):
    # only works with concrete types, not things like Optional
    return hint is inspect.Parameter.empty or isinstance(obj, hint)


def _signature_matches(sig: inspect.Signature, bound_args: inspect.BoundArguments):
    # doesn't handle type hints on *args or **kwargs
    for name, arg in bound_args.arguments.items():
        param = sig.parameters[name]
        hint = param.annotation
        if not _type_hint_matches(arg, hint):
            return False
    return True

It's almost here. We can assemble the above code to make Python realize real overloading:

import inspect

class NoMatchingOverload(Exception):
    pass

_MISSING = object()

class A(metaclass=OverloadMeta):
    @overload
    def f(self, x: int):
        print("A.f int overload", self, x)

    @overload
    def f(self, x: str):
        print("A.f str overload", self, x)

    @overload
    def f(self, x, y):
        print("A.f two arg overload", self, x, y)


class B(A):
    def normal_method(self):
        print("B.f normal method")

    @overload
    def f(self, x, y, z):
        print("B.f three arg overload", self, x, y, z)

    # works with inheritance too!


class C(B):
    @overload
    def f(self, x, y, z, t):
        print("C.f four arg overload", self, x, y, z, t)


def overloaded_class_example():
    print("OVERLOADED CLASS EXAMPLE")

    a = A()
    print(f"{a=}")
    print(f"{type(a)=}")
    print(f"{type(A)=}")
    print(f"{A.f=}")

    a.f(0)
    a.f("hello")
    # a.f(None) # Error, no matching overload
    a.f(1, True)
    print(f"{A.f=}")
    print(f"{a.f=}")

    b = B()
    print(f"{b=}")
    print(f"{type(b)=}")
    print(f"{type(B)=}")
    print(f"{B.f=}")
    b.f(0)
    b.f("hello")
    b.f(1, True)
    b.f(1, True, "hello")
    # b.f(None)  # no matching overload
    b.normal_method()

    c = C()
    c.f(1)
    c.f(1, 2, 3)
    c.f(1, 2, 3, 4)
    # c.f(None) # no matching overload


def main():
    overloaded_class_example()


if __name__ == "__main__":
    main()

The operation results are as follows:

OVERLOADED CLASS EXAMPLE
a=<__main__.A object at 0x7fbabe67d8e0>
type(a)=<class '__main__.A'>
type(A)=<class '__main__.OverloadMeta'>
A.f=Overload([<function A.f at 0x7fbabe679280>, <function A.f at 0x7fbabe679310>, <function A.f at 0x7fbabe6793a0>])
A.f int overload <__main__.A object at 0x7fbabe67d8e0> 0
A.f str overload <__main__.A object at 0x7fbabe67d8e0> hello
A.f two arg overload <__main__.A object at 0x7fbabe67d8e0> 1 True
A.f=Overload([<function A.f at 0x7fbabe679280>, <function A.f at 0x7fbabe679310>, <function A.f at 0x7fbabe6793a0>])
a.f=<__main__.BoundOverloadDispatcher object at 0x7fbabe67d910>
b=<__main__.B object at 0x7fbabe67d910>
type(b)=<class '__main__.B'>
type(B)=<class '__main__.OverloadMeta'>
B.f=Overload([<function B.f at 0x7fbabe6794c0>])
A.f int overload <__main__.B object at 0x7fbabe67d910> 0
A.f str overload <__main__.B object at 0x7fbabe67d910> hello
A.f two arg overload <__main__.B object at 0x7fbabe67d910> 1 True
B.f three arg overload <__main__.B object at 0x7fbabe67d910> 1 True hello
B.f normal method
A.f int overload <__main__.C object at 0x7fbabe67d9a0> 1
B.f three arg overload <__main__.C object at 0x7fbabe67d9a0> 1 2 3
C.f four arg overload <__main__.C object at 0x7fbabe67d9a0> 1 2 3 4

Code is longer, put together is not conducive to reading and understanding, but all the code is displayed in the text, if you do not want to assemble yourself, you want to complete the code that can run on one key, you can pay attention to the official account "Python seven", the dialog box returns "heavy load" to get the complete code of Python overload.

Added by james2010 on Thu, 04 Nov 2021 16:07:08 +0200