Python overload decorator

I know that this is not Pythonic for writing functions that take care of the type of arguments, but there are times when it is simply impossible to ignore types because they are handled differently.

Having empty isinstance checks in your function is just ugly; Is there a function decorator available that allows you to overload functions? Something like that:

 @overload(str) def func(val): print('This is a string') @overload(int) def func(val): print('This is an int') 

Update:

Here are some comments that I left on David Zaslavsky:

With a few modifications [s], this will fit my purpose well. Another limitation that I noticed in your implementation, since you use func.__name__ as a dictionary key, you tend to name conflicts between modules, which is not always desirable. [Continued]

[continued] For example, if I have one module that overloads func and another completely unrelated module that also overloads func , these overloads will collide because the dispatch dict function is global. This dict must be local to the module, somehow. And not only that, it should also support some kind of β€œinheritance”. [Continued]

[cont.] By "inheritance" I mean the following: let's say I have a first module with some overloads. Then there are two more modules that are not related to each other, but each import is first ; both of these modules add new overloads to the existing ones that they just imported. These two modules should be able to use the overloads in first , but the new ones they just added should not collide with each other between the modules. (This is actually quite difficult to do right, now that I think about it.)

Some of these problems could be resolved by slightly changing the syntax of the decorator:

first.py

 @overload(str, str) def concatenate(a, b): return a + b @concatenate.overload(int, int) def concatenate(a, b): return str(a) + str(b) 

second.py

 from first import concatenate @concatenate.overload(float, str) def concatenate(a, b): return str(a) + b 
+6
source share
2 answers

Quick answer: PyPI has an overload package that implements this more reliably than what I describe below, although using slightly different syntax. He announced that he only works with Python 3, but it seems that only minor modifications will be required to work with Python 2 (if any, I have not tried).


Long answer:. In languages ​​where you can overload functions, the name of the function (literally or effectively) is supplemented by information about its type signature, both when defining a function and when it is called. When the compiler or interpreter looks at the definition of a function, then it uses both the declared name and parameter types to allow access to this function. Thus, the logical way to implement overloading in Python is to implement a shell that uses both the declared name and parameter types to solve this function.

Here's a simple implementation:

 from collections import defaultdict def determine_types(args, kwargs): return tuple([type(a) for a in args]), \ tuple([(k, type(v)) for k,v in kwargs.iteritems()]) function_table = defaultdict(dict) def overload(arg_types=(), kwarg_types=()): def wrap(func): named_func = function_table[func.__name__] named_func[arg_types, kwarg_types] = func def call_function_by_signature(*args, **kwargs): return named_func[determine_types(args, kwargs)](*args, **kwargs) return call_function_by_signature return wrap 

overload should be called with two optional arguments, a tuple representing the types of all positional arguments and tuples of the tuples representing matching name types of all the keyword arguments. Here is a usage example:

 >>> @overload((str, int)) ... def f(a, b): ... return a * b >>> @overload((int, int)) ... def f(a, b): ... return a + b >>> print f('a', 2) aa >>> print f(4, 2) 6 >>> @overload((str,), (('foo', int), ('bar', float))) ... def g(a, foo, bar): ... return foo*a + str(bar) >>> @overload((str,), (('foo', float), ('bar', float))) ... def g(a, foo, bar): ... return a + str(foo*bar) >>> print g('a', foo=7, bar=4.4) aaaaaaa4.4 >>> print g('b', foo=7., bar=4.4) b30.8 

Disadvantages of this include

  • In fact, he does not verify that the function on which the decorator is applied is even compatible with the arguments provided to the decorator. You can write

     @overload((str, int)) def h(): return 0 

    and you will get an error when calling the function.

  • It does not gracefully handle the case where there is no overloaded version corresponding to the types of arguments passed (this would help to increase a more descriptive error)

  • It distinguishes between named and positional arguments, so something like

     g('a', 7, bar=4.4) 

    does not work.

  • There are many nested parentheses in use, as in the definitions for g .
  • As mentioned in the comments, this does not apply to functions with the same name in different modules.

All of this could be corrected with enough grunts, I think. In particular, the name collision problem is easily resolved by storing the distribution table as an attribute of the function returned from the decorator. But, as I said, this is just a simple example that demonstrates the basics of how to do this.

+4
source

This does not give a direct answer to your question, but if you really want to have something that behaves like an overloaded function for different types and (quite rightly) do not want to use isinstance, then I would suggest something like:

 def func(int_val=None, str_val=None): if sum(x != None for x in (int_val, str_val)) != 1: #raise exception - exactly one value should be passed in if int_val is not None: print('This is an int') if str_val is not None: print('This is a string') 

When used, the intent is obvious, and does not even require that different options have different types:

 func(int_val=3) func(str_val="squirrel") 
0
source

Source: https://habr.com/ru/post/904593/


All Articles