shan

python 函数重载

2020-02-21

原文:Overload Functions in Python

原文代码位置:代码demo

本文为 overload function in python 的翻译文章。

函数重载就是可以存在多个具有相同名称但签名/实现不同的函数。当调用重载函数 fn 时,运行时首先判断传递给函数调用的参数,并以此判断来调用相应的实现。本文通过创建用户维护的虚拟命名空间和装饰器,从而在python 中实现了函数的重载。

1
2
3
4
5
6
7
int area(int length, int breadth) {
return length * breadth;
}

float area(int radius) {
return 3.14 * radius * radius;
}

在上面的例子中(C++代码),函数 area 通过两个实现进行重载的;一个实现接收两个参数(两个int)代表一个矩形的长和宽,然后返回面积;另一个函数接收一个圆的半径,返回圆的面积。当我们像 area(7) 这样调用 area 函数时,它引用第二个方法,当调用 area(3,4) 时,它引用第一个方法。

Why no Function Overloading in Python?

Python 不支持函数的重载。当我们用相同的名字定义多个函数时,后面的函数会覆盖前面的函数,在命名空间中,每个函数名称总是只有一个条目。我们可以通过调用 locals()globals() 函数来获取局部和全局的命名空间。

1
2
3
4
5
6
7
8
9
def area(radius):
return 3.14 * radius ** 2

>>> locals()
{
...
'area': <function area at 0x10476a440>,
...
}

定义一个函数之后,我们通过调用函数 locals() 可以得到局部的命名空间,它会返回一个字典,其中包含了所有定义在局部命名空间的变量。字典的键值是变量的名称,值为变量的引用。当运行时遇到具有相同名称的另一个函数时,它会更新局部命名空间中的条目,从而消除了两个函数共存的可能性。因此Python不支持函数的重载。这是在创建语言时做出的设计决定,但这并不能阻止我们实现它,因此让我们重载一些函数。

Implementing Function Overloading in Python

我们知道 Python 是如何管理命名空间的,如果我们想要实现函数的重载,我们需要:

  • 维护一个虚拟的命名空间中管理函数的定义
  • 找到一种方法,根据传递给它的参数来调用适当的函数

为了让事情变得简单,我们会实现函数的重载,其中函数拥有相同的名称,然后通过接收的参数的数量来进行区分。

Wrapping the function

我们创建一个名叫 Function 的类,它装饰其他任意的函数,使它通过一个被重写的 __call__ 方法进行调用,同时暴露一个 key 方法,它返回一个元组,使得此函数在整个代码库中是唯一的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from inspect import getfullargspec

class Function(object):
"""Function is a wrap over standard python function.
"""
def __init__(self, fn):
self.fn = fn

def __call__(self, *args, **kwargs):
"""when invoked like a function it internally invokes
the wrapped function and returns the returned value.
"""
return self.fn(*args, **kwargs)

def key(self, args=None):
"""Returns the key that will uniquely identify
a function (even when it is overloaded).
"""
# if args not specified, extract the arguments from the
# function definition
if args is None:
args = getfullargspec(self.fn).args

return tuple([
self.fn.__module__,
self.fn.__class__,
self.fn.__name__,
len(args or []),
])

在上面的代码片段中,key 方法返回一个元组,该元组唯一标识代码库中的函数并且保持:

  • 函数的模块
  • 该函数所属的类
  • 函数的名称
  • 函数接收参数的数量

重写的 __call__ 方法调用被装饰的函数并返回计算后的值。这使得实例可以像函数一样被调用,并且其行为与装饰后的函数完全一样。

1
2
3
4
5
6
7
8
def area(l, b):
return l * b

>>> func = Function(area)
>>> func.key()
('__main__', <class 'function'>, 'area', 2)
>>> func(3, 4)
12

在上面的例子中,area 函数被包装在实例化的 Functionfunc 中。key() 方法返回一个元组,它的第一个元素为模块的名称 __main__, 第二个元素是类 Function,第三个元素是函数的名称 area,第四个元素是函数 area 接收的参数个数,上面例子中是 2

例子中同样表明了,我们可以直接调用实例 func,就像调用 area 函数一样,传入参数 34 然后得到返回值12 ,这个结果我们同样可以调用 area(3, 4) 得到。当我们利用装饰器时,这种行为会在以后的阶段派上用场。

Building the virtual Namespace

我们在这里创建的虚拟命名空间将会存储在定义阶段收集到的所有函数。由于只有一个命名空间/注册表,所以我们创建了一个单例类,将类中的函数保存在字典中,其键不仅是函数名,而是我们从 key 函数中获得的元组,其中元组在整个代码库中包含唯一标识函数中的元素。这样,即使函数具有相同的名称(但参数不同),我们也可以将它们保留在注册表中,从而有助于函数重载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Namespace(object):
"""Namespace is the singleton class that is responsible
for holding all the functions.
"""
__instance = None

def __init__(self):
if self.__instance is None:
self.function_map = dict()
Namespace.__instance = self
else:
raise Exception("cannot instantiate a virtual Namespace again")

@staticmethod
def get_instance():
if Namespace.__instance is None:
Namespace()
return Namespace.__instance

def register(self, fn):
"""registers the function in the virtual namespace and returns
an instance of callable Function that wraps the
function fn.
"""
func = Function(fn)
self.function_map[func.key()] = fn
return func

Namespace 有一个 register 方法,它接收函数 fn 作为参数,为此函数创建一个特殊的键值,在字典中存储并返回 Function 实例化对象装饰的 fn。这意味着从函数 register 返回的值同样是可调用的并且其行为与被装饰的函数 fn 相同。

1
2
3
4
5
6
7
def area(l, b):
return l * b

>>> namespace = Namespace.get_instance()
>>> func = namespace.register(area)
>>> func(3, 4)
12

Using decorators as a hook

现在我们已经定义了一个虚拟的命名空间,它能够注册函数,我们需要一个钩子在函数定义期间被调用。我们可以使用装饰器来实现它。在 Python 中,一个装饰器装饰一个函数并且允许我们在一个已有的函数中添加新的功能,而不改变现有的结构。一个装饰器接受被装饰的函数 fn 作为参数,返回另一个被调用的函数。这个函数在被调用过程中接收 argskwargs 并且返回结果。

下面展示了一个关于计时器函数装饰器的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import time


def my_decorator(fn):
"""my_decorator is a custom decorator that wraps any function
and prints on stdout the time for execution.
"""
def wrapper_function(*args, **kwargs):
start_time = time.time()

# invoking the wrapped function and getting the return value.
value = fn(*args, **kwargs)
print("the function execution took:", time.time() - start_time, "seconds")

# returning the value got after invoking the wrapped function
return value

return wrapper_function


@my_decorator
def area(l, b):
return l * b


>>> area(3, 4)
the function execution took: 9.5367431640625e-07 seconds
12

在上面的例子中,我们定义了一个名字是 my_decorator 的装饰器,它装饰了函数 area 并且在 stdout 里打印了它执行所花费的时间。

每当解释器遇到函数定义时,装饰器函数 my_decorator 都会被调用(因此它装饰需要被装饰的函数并将这个新包装的函数存储在 Python 局部或全局命名空间中)。 它对我们来说是一个理想的钩子,用来在虚拟命名空间中注册函数。因此我们创建我们的名为 overload 的装饰器,它在虚拟命名空间中注册函数并且返回一个被调用的可调用对象。

1
2
3
4
5
def overload(fn):
"""overload is the decorator that wraps the function
and returns a callable object of type Function.
"""
return Namespace.get_instance().register(fn)

overload 装饰器返回一个 Function 实例,作为命名空间函数 .register() 的返回。现在无论何时调用函数,它都是调用由 .register() 函数返回的函数 —— 一个 Function 类的实例,并且 __call__ 方法将在调用期间通过指定的参数 argskwargs 执行。现在剩下的是在 Function 类中实现 __call__ 方法,以便在调用期间在给定参数的情况下调用适当的函数。

Finding the right function from the namespace

除通常的类和变量外,消除歧义的范围是函数接受的参数数量,因此我们在虚拟命名空间中定义了一个名为 get 的方法,该方法接受来自 python 命名空间的函数(将是同名函数的最后一个定义 —— 因为我们没有更改 Python 命名空间的默认行为)以及调用期间传递的参数(我们消除歧义的部分)并且返回已消除歧义的函数进行调用。

这个 get 函数的作用是决定要调用哪个函数的实现。获取合适函数的过程非常简单 —— 从函数和参数中使用 key 函数创建唯一键,看看它是否存在于函数注册表中;如果存在,则提取针对它存储的实现。

1
2
3
4
5
6
7
def get(self, fn, *args):
"""get returns the matching function from the virtual namespace.

return None if it did not fund any matching function.
"""
func = Function(fn)
return self.function_map.get(func.key(args=args))

函数 get 只是创建了 Function 的实例,以便它可以使用 key 函数来获取唯一键而不是复制逻辑。然后,该键用于从函数的注册表中获取适当的函数。

Invoking the function

如上所述,每次调用由 overload 装饰器修饰的函数时,都会调用 Function 类中的 __call__ 方法。我们使用此函数通过命名空间的 get 函数获取适当的函数,并调用重载函数所需要的实现。 __call__ 方法的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
def __call__(self, *args, **kwargs):
"""Overriding the __call__ function which makes the
instance callable.
"""
# fetching the function to be invoked from the virtual namespace
# through the arguments.
fn = Namespace.get_instance().get(self.fn, *args)
if not fn:
raise Exception("no matching function found.")

# invoking the wrapped function and returning the value.
return fn(*args, **kwargs)

该方法从虚拟命名空间获取适当的函数,如果未找到任何函数,则引发 Exception,如果找到了,则调用该函数并返回值。

Function overloading in action

一旦所有代码都完成了,我们将定义两个名为 area 的函数:一个函数计算矩形的面积,另一个函数计算圆的面积。这两个函数都在下面定义,并用overload 装饰器装饰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@overload
def area(l, b):
return l * b

@overload
def area(r):
import math
return math.pi * r ** 2


>>> area(3, 4)
12
>>> area(7)
153.93804002589985

当我们用一个参数调用 area 时,它返回一个圆的面积;当我们传递两个参数时,它调用了计算矩形面积的函数,从而使函数 area 重载。

Conclusion

Python 不支持函数重载,但是通过使用通用语言结构,我们得到了它的解决方案。我们使用装饰器和用户维护的命名空间重载函数,并使用参数数量作为消除歧义的因素。我们还可以使用参数的数据类型(在装饰器中定义)来消除歧义 —— 这允许具有相同数量的参数但具有不同类型的函数进行重载。重载的粒度仅受函数 getfullargspec 和我们的想象力的限制。

使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

扫描二维码,分享此文章