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.