如何利用饰器实现 Python 函数重载

目录
  • 装饰器实现Python 函数重载
    • 一、为什么 Python 中没有函数重载?
    • 二、在 Python 中实现函数重载
    • 三、把函数封装起来
    • 四、构建虚拟的命名空间
    • 五、使用装饰器作为钩子
    • 六、从命名空间中找到正确的函数
    • 七、实现函数的调用
    • 八、运用函数重载
    • 九、总结

装饰器实现Python 函数重载

函数重载指的是有多个同名的函数,但是它们的签名或实现却不同。当调用一个重载函数 fn 时,程序会检验传递给函数的实参/形参,并据此而调用相应的实现。

int area(int length, int breadth) {
  return length * breadth;
}

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

在以上例子中(用 c++ 编写),函数 area 被重载了两个实现。第一个函数接收两个参数(都是整数),表示矩形的长度和宽度,并返回矩形的面积。另一个函数只接收一个整型参数,表示圆的半径。

当我们像 area(7) 这样调用函数 area 时,它会调用第二个函数,而 area(3,4) 则会调用第一个函数。

一、为什么 Python 中没有函数重载?

Python 不支持函数重载。当我们定义了多个同名的函数时,后面的函数总是会覆盖前面的函数,因此,在一个命名空间中,每个函数名仅会有一个登记项(entry)。

注意:这里说 Python 不支持函数重载,指的是在不用语法糖的情况下。使用 functools 库的 singledispatch 装饰器,Python 也可以实现函数重载。原文作者在文末的注释中专门提到了这一点。

通过调用 locals() 和 globals() 函数,我们可以看到 Python 的命名空间中有什么,它们分别返回局部和全局命名空间。

def area(radius):
  return 3.14 * radius ** 2

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

在定义一个函数后,接着调用 locals() 函数,我们会看到它返回了一个字典,包含了定义在局部命名空间中的所有变量。字典的键是变量的名称,值是该变量的引用/值。

当程序在运行时,若遇到另一个同名函数,它就会更新局部命名空间中的登记项,从而消除两个函数共存的可能性。因此 Python 不支持函数重载。这是在创造语言时做出的设计决策,但这并不妨碍我们实现它,所以,让我们来重载一些函数吧。

二、在 Python 中实现函数重载

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

  • 维护一个虚拟的命名空间,在其中管理函数定义
  • 根据每次传递的参数,设法调用适当的函数

为了简单起见,我们在实现函数重载时,通过不同的参数数量来区分同名函数。

三、把函数封装起来

我们创建了一个名为Function的类,它可以封装任何函数,并通过重写的__call__方法来调用该函数,还提供了一个名为key的方法,该方法返回一个元组,使该函数在整个代码库中是唯一的。

from inspect import getfullargspec

class Function(object):
  """Function类是对标准的Python函数的封装"""
  def __init__(self, fn):
    self.fn = fn

  def __call__(self, *args, **kwargs):
    """当像函数一样被调用时,它就会调用被封装的函数,并返回该函数的返回值"""
    return self.fn(*args, **kwargs)

  def key(self, args=None):
    """返回一个key,能唯一标识出一个函数(即便是被重载的)"""
    # 如果不指定args,则从函数的定义中提取参数
    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__方法会调用被封装的函数,并返回计算的值(这没有啥特别的)。这使得Function的实例可以像函数一样被调用,并且它的行为与被封装的函数完全一样。

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

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

在上面的例子中,函数area被封装在Function中,并被实例化成func。key() 返回一个元组,其第一个元素是模块名__main__,第二个是类<class 'function'>,第三个是函数名area,而第四个则是该函数接收的参数数量,即 2。

这个示例还显示出,我们可以像调用普通的 area函数一样,去调用实例 func,当传入参数 3 和 4时,得到的结果是 12,这正是调用 area(3,4) 时会得到的结果。当我们接下来运用装饰器时,这种行为将会派上用场。

四、构建虚拟的命名空间

我们要创建一个虚拟的命名空间,用于存储在定义阶段收集的所有函数。

由于只有一个命名空间/注册表,我们创建了一个单例类,并把函数保存在字典中。该字典的键不是函数名,而是我们从 key 函数中得到的元组,该元组包含的元素能唯一标识出一个函数。

通过这样,我们就能在注册表中保存所有的函数,即使它们有相同的名称(但不同的参数),从而实现函数重载。

class Namespace(object):
  """Namespace是一个单例类,负责保存所有的函数"""
  __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):
    """在虚拟的命名空间中注册函数,并返回Function类的可调用实例"""
    func = Function(fn)
    self.function_map[func.key()] = fn
    return func

Namespace类有一个register方法,该方法将函数 fn 作为参数,为其创建一个唯一的键,并将函数存储在字典中,最后返回封装了 fn 的Function的实例。这意味着 register 函数的返回值也是可调用的,并且(到目前为止)它的行为与被封装的函数 fn 完全相同。

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

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

五、使用装饰器作为钩子

既然已经定义了一个能够注册函数的虚拟命名空间,那么,我们还需要一个钩子来在函数定义期间调用它。在这里,我们会使用 Python 装饰器。

在 Python 中,装饰器用于封装一个函数,并允许我们在不修改该函数的结构的情况下,向其添加新功能。装饰器把被装饰的函数 fn 作为参数,并返回一个新的函数,用于实际的调用。新的函数会接收原始函数的 args 和 kwargs,并返回最终的值。

以下是一个装饰器的示例,演示了如何给函数添加计时功能。

import time

def my_decorator(fn):
  """这是一个自定义的函数,可以装饰任何函数,并打印其执行过程的耗时"""
  def wrapper_function(*args, **kwargs):
    start_time = time.time()
    # 调用被装饰的函数,并获取其返回值
    value = fn(*args, **kwargs)
    print("the function execution took:", time.time() - start_time, "seconds")
    # 返回被装饰的函数的调用结果
    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,并在标准输出上打印出执行 area 所需的时间。

每当解释器遇到一个函数定义时,就会调用装饰器函数 my_decorator(用它封装被装饰的函数,并将封装后的函数存储在 Python 的局部或全局命名空间中),对于我们来说,它是在虚拟命名空间中注册函数的理想钩子。

因此,我们创建了名为overload的装饰器,它能在虚拟命名空间中注册函数,并返回一个可调用对象。

def overload(fn):
  """用于封装函数,并返回Function类的一个可调用对象"""
  return Namespace.get_instance().register(fn)

overload装饰器借助命名空间的 .register() 函数,返回 Function 的一个实例。现在,无论何时调用函数(被 overload 装饰的),它都会调用由 .register() 函数所返回的函数——Function 的一个实例,其 call 方法会在调用期间使用指定的 args 和 kwargs 执行。

现在剩下的就是在 Function 类中实现__call__方法,使得它能根据调用期间传入的参数而调用相应的函数。

六、从命名空间中找到正确的函数

想要区别出不同的函数,除了通常的模块、类和函数名以外,还可以依据函数的参数数量,因此,我们在虚拟的命名空间中定义了一个 get 方法,它会从 Python 的命名空间中读取待区分的函数以及实参,最后依据参数的不同,返回出正确的函数。我们没有更改 Python 的默认行为,因此在原生的命名空间中,同名的函数只有一个。

这个 get 函数决定了会调用函数的哪个实现(如果重载了的话)。找到正确的函数的过程非常简单——先使用 key 方法,它利用函数和参数来创建出唯一的键(正如注册时所做的那样),接着查找这个键是否存在于函数注册表中;如果存在,则获取其映射的实现。

def get(self, fn, *args):
  """从虚拟命名空间中返回匹配到的函数,如果没找到匹配,则返回None"""
  func = Function(fn)
  return self.function_map.get(func.key(args=args))

get 函数创建了 Function 类的一个实例,这样就可以复用类的 key 函数来获得一个唯一的键,而不用再写创建键的逻辑。然后,这个键将用于从函数注册表中获取正确的函数。

七、实现函数的调用

前面说过,每次调用被 overload 装饰的函数时,都会调用 Function 类中的__call__方法。我们需要让__call__方法从命名空间的 get 函数中,获取出正确的函数,并调用之。

__call__方法的实现如下:

def __call__(self, *args, **kwargs):
  """重写能让类的实例变可调用对象的__call__方法"""
  # 依据参数,从虚拟命名空间中获取将要调用的函数
  fn = Namespace.get_instance().get(self.fn, *args)
  if not fn:
    raise Exception("no matching function found.")
  # 调用被封装的函数,并返回调用的结果
  return fn(*args, **kwargs)

该方法从虚拟命名空间中获取正确的函数,如果没有找到任何函数,它就抛出一个 Exception,如果找到了,就会调用该函数,并返回调用的结果。

八、运用函数重载

准备好所有代码后,我们定义了两个名为 area 的函数:一个计算矩形的面积,另一个计算圆的面积。下面定义了两个函数,并使用overload装饰器进行装饰。

@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 的重载。

注:从 Python 3.4 开始,Python 的 functools.singledispatch 支持函数重载。从 Python 3.8 开始,functools.singledispatchmethod 支持重载类和实例方法。感谢 Harry Percival 的指正。

九、总结

Python 不支持函数重载,但是通过使用它的基本结构,我们捣鼓了一个解决方案。

我们使用装饰器和虚拟的命名空间来重载函数,并使用参数的数量作为区别函数的因素。我们还可以根据参数的类型(在装饰器中定义)来区别函数——即重载那些参数数量相同但参数类型不同的函数。

重载能做到什么程度,这仅仅受限于getfullargspec函数和我们的想象。使用前文的思路,你可能会实现出一个更整洁、更干净、更高效的方法,所以,请尝试实现一下吧。

正文到此结束。以下附上完整的代码:

# 模块:overload.py
from inspect import getfullargspec

class Function(object):
  """Function is a wrap over standard python function
  An instance of this Function class is also callable
  just like the python function that it wrapped.
  When the instance is "called" like a function it fetches
  the function to be invoked from the virtual namespace and then
  invokes the same.
  """
  def __init__(self, fn):
    self.fn = fn

  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)

  def key(self, args=None):
    """Returns the key that will uniquely identifies
    a function (even when it is overloaded).
    """
    if args is None:
      args = getfullargspec(self.fn).args
    return tuple([
      self.fn.__module__,
      self.fn.__class__,
      self.fn.__name__,
      len(args or []),
    ])

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 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)
    specs = getfullargspec(fn)
    self.function_map[func.key()] = fn
    return func

  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))

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)
最后,演示代码如下:

from overload import overload

@overload
def area(length, breadth):
  return length * breadth

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

@overload
def area(length, breadth, height):
  return 2 * (length * breadth + breadth * height + height * length)

@overload
def volume(length, breadth, height):
  return length * breadth * height

@overload
def area(length, breadth, height):
  return length + breadth + height

@overload
def area():
  return 0

print(f"area of cuboid with dimension (4, 3, 6) is: {area(4, 3, 6)}")
print(f"area of rectangle with dimension (7, 2) is: {area(7, 2)}")
print(f"area of circle with radius 7 is: {area(7)}")
print(f"area of nothing is: {area()}")
print(f"volume of cuboid with dimension (4, 3, 6) is: {volume(4, 3, 6)}")

到此这篇关于如何利用饰器实现 Python 函数重载的文章就介绍到这了,更多相关装饰器实现Python 函数重载内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 在Python中实现函数重载的示例代码

    假设你有一个函数connect,它有一个参数address,这个参数可能是一个字符串,也可能是一个元组.例如: connect('123.45.32.18:8080') connect(('123.45.32.18', 8080)) 你想在代码里面兼容这两种写法,于是你可能会这样写代码: def connect(address): if isinstance(address, str): ip, port = address.split(':') elif isinstance(address,

  • 理解python中装饰器的作用

    装饰器的作用就是用一个新函数封装旧函数(是旧函数代码不变的情况下增加功能)然后会返回一个新函数,新函数就叫做装饰器,一般为了简化装饰器会用语法糖@新函数来简化 例子: 这是一段代码,但功能太少,要对这个进行增强,但又不能改变代码. def hello(): return "hello world!" 现在我们的需求是要增强hello()函数的功能,希望给返回加上HTML标签,比如<i>hello world</i>,但要求我们不得改变hello()函数原来的定义

  • python中有函数重载吗

    python中没有函数重载 为了考虑为什么python不提供函数重载,首先我们要研究为什么需要提供函数重载. 函数重载主要是为了解决两个问题: 可变参数类型. 可变参数个数. 另外,一个基本的设计原则是,仅仅当两个函数除了参数类型和参数个数不同以外,其功能是完全相同的,此时才使用函数重载,如果两个函数的功能其实不同,那么不应当使用重载,而应当使用一个名字不同的函数. 那么对于情况 1 ,函数功能相同,但是参数类型不同,python 如何处理? 答案是根本不需要处理,因为 python 可以接受任

  • 详解Python装饰器 给你的咖啡加点料

    一.函数回顾 1.在python中函数是一等公民,函数也是对象.我们可以把函数赋予变量. def make_cofe(type): print('获得一杯 : {}'.format(type)) ​ get_cofe = make_cofe get_cofe('咖啡') ​ ####输出##### 获得一杯 : 咖啡 这个例子中,我们把函数make_cofe 赋予了变量 get_cofe,这样之后你调用 get_cofe,就相当于是调用函数 make_cofe(). 2.把函数当作参数,传入另一

  • 如何正确理解python装饰器

    一.闭包 要想了解装饰器,首先要了解一个概念,闭包.什么是闭包,一句话说就是,在函数中再嵌套一个函数,并且引用外部函数的变量,这就是一个闭包了.光说没有概念,直接上一个例子. def outer(x): def inner(y): return x + y return inner print(outer(6)(5)) ----------------------------- >>>11 如代码所示,在outer函数内,又定义了一个inner函数,并且inner函数又引用了外部函数ou

  • Python Pytest装饰器@pytest.mark.parametrize详解

    Pytest中装饰器@pytest.mark.parametrize('参数名',list)可以实现测试用例参数化,类似DDT 如:@pytest.mark.parametrize('请求方式,接口地址,传参,预期结果',[('get','www.baidu.com','{"page":1}','{"code":0,"msg":"成功"})',('post','www.baidu.com','{"page"

  • python 装饰器的使用与要点

    一.装饰器使用场景 经常用于有切面需求的场景,比如:插入日志.性能测试.事务处理.缓存.权限校验等场景.装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用. 概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能. 二.为什么需要装饰器 1.先来看一个简单例子: def foo(): print('i am foo') 2.增加需求 现在有一个新的需求,希望可以记录下函数的执行日志,于是在代码中添加日志代码: def foo(): print

  • python装饰器原理源码示例分析

    目录 前言 一.什么是装饰器 二.为什么要用装饰器 三.简单的装饰器 四.装饰器的语法糖 五.装饰器传参 六.带参数的装饰器 七.类装饰器 八.带参数的类装饰器 九.装饰器的顺序 前言 最近有人问我装饰器是什么,我就跟他说,其实就是装饰器就是类似于女孩子的发卡.你喜欢的一个女孩子,她可以有很多个发卡,而当她戴上不同的发卡,她的头顶上就是装饰了不同的发卡.但是你喜欢的女孩子还是你喜欢的女孩子.如果还觉得不理解的话,装饰器就是咱们的手机壳,你尽管套上了手机壳,但并不影响你的手机功能,可你的手机还是该

  • 如何利用饰器实现 Python 函数重载

    目录 装饰器实现Python 函数重载 一.为什么 Python 中没有函数重载? 二.在 Python 中实现函数重载 三.把函数封装起来 四.构建虚拟的命名空间 五.使用装饰器作为钩子 六.从命名空间中找到正确的函数 七.实现函数的调用 八.运用函数重载 九.总结 装饰器实现Python 函数重载 函数重载指的是有多个同名的函数,但是它们的签名或实现却不同.当调用一个重载函数 fn 时,程序会检验传递给函数的实参/形参,并据此而调用相应的实现. int area(int length, in

  • 详解利用装饰器扩展Python计时器

    目录 介绍 理解 Python 中的装饰器 创建 Python 定时器装饰器 使用 Python 定时器装饰器 Python 计时器代码 其他 Python 定时器函数 使用替代 Python 计时器函数 估计运行时间timeit 使用 Profiler 查找代码中的Bottlenecks 总结 介绍 在本文中,云朵君将和大家一起了解装饰器的工作原理,如何将我们之前定义的定时器类 Timer 扩展为装饰器,以及如何简化计时功能.最后对 Python 定时器系列文章做个小结. 这是我们手把手教你实

  • 详解Python 函数如何重载?

    什么是函数重载?简单的理解,支持多个同名函数的定义,只是参数的个数或者类型不同,在调用的时候,解释器会根据参数的个数或者类型,调用相应的函数. 重载这个特性在很多语言中都有实现,比如 C++.Java 等,而 Python 并不支持.这篇文章呢,通过一些小技巧,可以让 Python 支持类似的功能. 参数个数不同的情形 先看看这种情况下 C++ 是怎么实现重载的 #include <iostream> using namespace std; int func(int a) { cout &l

  • Python函数装饰器的使用教程

    典型的函数装饰器 以下示例定义了一个装饰器,输出函数的运行时间: 函数装饰器和闭包紧密结合,入参func代表被装饰函数,通过自由变量绑定后,调用函数并返回结果. 使用clock装饰器: import time from clockdeco import clock @clock def snooze(seconds): time.sleep(seconds) @clock def factorial(n): return 1 if n < 2 else n*factorial(n-1) if _

  • Python 函数装饰器应用教程

    目录 一.什么是函数装饰器 二.函数装饰器的执行时机 三.变量作用域 四.闭包 五.保留函数的元数据 七.使用lru_cache缓存函数执行结果 八.使用singledispatch实现泛型函数 九.通过参数控制函数装饰器的行为 一.什么是函数装饰器 1.函数装饰器是Python提供的一种增强函数功能的标记函数: 2.装饰器是可调用的函数对象,其参数是另一个函数(被装饰的函数): 我们可以使用修饰器来封装某个函数,从而让程序在执行这个函数之前与执行完这个函数之后,分别运行某些代码.这意味着,调用

  • 详解利用上下文管理器扩展Python计时器

    目录 一个 Python 定时器上下文管理器 了解 Python 中的上下文管理器 理解并使用 contextlib 创建 Python 计时器上下文管理器 使用 Python 定时器上下文管理器 写在最后 上文中,我们一起学习了手把手教你实现一个 Python 计时器.本文中,云朵君将和大家一起了解什么是上下文管理器 和 Python 的 with 语句,以及如何完成自定义.然后扩展 Timer 以便它也可以用作上下文管理器.最后,使用 Timer 作为上下文管理器如何简化我们自己的代码. 上

  • Python利用装饰器实现类似于flask路由

    目录 1.例子1 2.python 利用装饰器实现类似于flask路由 1.例子1 def f1(): print(1111) def f2(): print(2222) if __name__ == '__main__': print(33) 打印结果: 33 在例子1中,f1() 与f2() 都没有被调用,只执行了print(33) f1与f2,是没有被调用的,但是如果f1 和 f2 上面有注解,就会被调用执行. 2.python 利用装饰器实现类似于flask路由 注释类 Grass #

  • python函数装饰器用法实例详解

    本文实例讲述了python函数装饰器用法.分享给大家供大家参考.具体如下: 装饰器经常被用于有切面需求的场景,较为经典的有插入日志.性能测试.事务处理等.装饰器是解决这类问题的绝佳设计, 有了装饰器,我们就可以抽离出大量函数中与函数功能本身无关的雷同代码并继续重用.概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能. #! coding=utf-8 import time def timeit(func): def wrapper(a): start = time.clock() func

随机推荐