python3函数重载_9.20 利用函数注解实现方法重载

问题

你已经学过怎样使用函数参数注解,那么你可能会想利用它来实现基于类型的方法重载。但是你不确定应该怎样去实现(或者到底行得通不)。

解决方案

本小节的技术是基于一个简单的技术,那就是Python允许参数注解,代码可以像下面这样写:

class Spam:

def bar(self, x:int, y:int):

print("Bar 1:", x, y)

def bar(self, s:str, n:int = 0):

print("Bar 2:", s, n)

s = Spam()

s.bar(2, 3) # Prints Bar 1: 2 3

s.bar("hello") # Prints Bar 2: hello 0

下面是我们第一步的尝试,使用到了一个元类和描述器:

# multiple.py

import inspect

import types

class MultiMethod:

"""

Represents a single multimethod.

"""

def __init__(self, name):

self._methods = {}

self.__name__ = name

def register(self, meth):

"""

Register a new method as a multimethod

"""

sig = inspect.signature(meth)

# Build a type signature from the method"s annotations

types = []

for name, parm in sig.parameters.items():

if name == "self":

continue

if parm.annotation is inspect.Parameter.empty:

raise TypeError(

"Argument {} must be annotated with a type".format(name)

)

if not isinstance(parm.annotation, type):

raise TypeError(

"Argument {} annotation must be a type".format(name)

)

if parm.default is not inspect.Parameter.empty:

self._methods[tuple(types)] = meth

types.append(parm.annotation)

self._methods[tuple(types)] = meth

def __call__(self, *args):

"""

Call a method based on type signature of the arguments

"""

types = tuple(type(arg) for arg in args[1:])

meth = self._methods.get(types, None)

if meth:

return meth(*args)

else:

raise TypeError("No matching method for types {}".format(types))

def __get__(self, instance, cls):

"""

Descriptor method needed to make calls work in a class

"""

if instance is not None:

return types.MethodType(self, instance)

else:

return self

class MultiDict(dict):

"""

Special dictionary to build multimethods in a metaclass

"""

def __setitem__(self, key, value):

if key in self:

# If key already exists, it must be a multimethod or callable

current_value = self[key]

if isinstance(current_value, MultiMethod):

current_value.register(value)

else:

mvalue = MultiMethod(key)

mvalue.register(current_value)

mvalue.register(value)

super().__setitem__(key, mvalue)

else:

super().__setitem__(key, value)

class MultipleMeta(type):

"""

Metaclass that allows multiple dispatch of methods

"""

def __new__(cls, clsname, bases, clsdict):

return type.__new__(cls, clsname, bases, dict(clsdict))

@classmethod

def __prepare__(cls, clsname, bases):

return MultiDict()

为了使用这个类,你可以像下面这样写:

class Spam(metaclass=MultipleMeta):

def bar(self, x:int, y:int):

print("Bar 1:", x, y)

def bar(self, s:str, n:int = 0):

print("Bar 2:", s, n)

# Example: overloaded __init__

import time

class Date(metaclass=MultipleMeta):

def __init__(self, year: int, month:int, day:int):

self.year = year

self.month = month

self.day = day

def __init__(self):

t = time.localtime()

self.__init__(t.tm_year, t.tm_mon, t.tm_mday)

下面是一个交互示例来验证它能正确的工作:

>>> s = Spam()

>>> s.bar(2, 3)

Bar 1: 2 3

>>> s.bar("hello")

Bar 2: hello 0

>>> s.bar("hello", 5)

Bar 2: hello 5

>>> s.bar(2, "hello")

Traceback (most recent call last):

File "", line 1, in

File "multiple.py", line 42, in __call__

raise TypeError("No matching method for types {}".format(types))

TypeError: No matching method for types (, )

>>> # Overloaded __init__

>>> d = Date(2012, 12, 21)

>>> # Get today"s date

>>> e = Date()

>>> e.year

2012

>>> e.month

12

>>> e.day

3

>>>

讨论

坦白来讲,相对于通常的代码而已本节使用到了很多的魔法代码。但是,它却能让我们深入理解元类和描述器的底层工作原理,并能加深对这些概念的印象。因此,就算你并不会立即去应用本节的技术,它的一些底层思想却会影响到其它涉及到元类、描述器和函数注解的编程技术。

本节的实现中的主要思路其实是很简单的。MutipleMeta 元类使用它的 __prepare__() 方法来提供一个作为 MultiDict 实例的自定义字典。这个跟普通字典不一样的是,MultiDict 会在元素被设置的时候检查是否已经存在,如果存在的话,重复的元素会在 MultiMethod实例中合并。

MultiMethod 实例通过构建从类型签名到函数的映射来收集方法。在这个构建过程中,函数注解被用来收集这些签名然后构建这个映射。这个过程在 MultiMethod.register() 方法中实现。这种映射的一个关键特点是对于多个方法,所有参数类型都必须要指定,否则就会报错。

为了让 MultiMethod 实例模拟一个调用,它的 __call__() 方法被实现了。这个方法从所有排除 slef 的参数中构建一个类型元组,在内部map中查找这个方法,然后调用相应的方法。为了能让 MultiMethod 实例在类定义时正确操作,__get__() 是必须得实现的。它被用来构建正确的绑定方法。比如:

>>> b = s.bar

>>> b

>

>>> b.__self__

>>> b.__func__

>>> b(2, 3)

Bar 1: 2 3

>>> b("hello")

Bar 2: hello 0

>>>

不过本节的实现还有一些限制,其中一个是它不能使用关键字参数。例如:

>>> s.bar(x=2, y=3)

Traceback (most recent call last):

File "", line 1, in

TypeError: __call__() got an unexpected keyword argument "y"

>>> s.bar(s="hello")

Traceback (most recent call last):

File "", line 1, in

TypeError: __call__() got an unexpected keyword argument "s"

>>>

也许有其他的方法能添加这种支持,但是它需要一个完全不同的方法映射方式。问题在于关键字参数的出现是没有顺序的。当它跟位置参数混合使用时,那你的参数就会变得比较混乱了,这时候你不得不在 __call__() 方法中先去做个排序。

同样对于继承也是有限制的,例如,类似下面这种代码就不能正常工作:

class A:

pass

class B(A):

pass

class C:

pass

class Spam(metaclass=MultipleMeta):

def foo(self, x:A):

print("Foo 1:", x)

def foo(self, x:C):

print("Foo 2:", x)

原因是因为 x:A 注解不能成功匹配子类实例(比如B的实例),如下:

>>> s = Spam()

>>> a = A()

>>> s.foo(a)

Foo 1: <__main__.a object at>

>>> c = C()

>>> s.foo(c)

Foo 2: <__main__.c object at>

>>> b = B()

>>> s.foo(b)

Traceback (most recent call last):

File "", line 1, in

File "multiple.py", line 44, in __call__

raise TypeError("No matching method for types {}".format(types))

TypeError: No matching method for types (,)

>>>

作为使用元类和注解的一种替代方案,可以通过描述器来实现类似的效果。例如:

import types

class multimethod:

def __init__(self, func):

self._methods = {}

self.__name__ = func.__name__

self._default = func

def match(self, *types):

def register(func):

ndefaults = len(func.__defaults__) if func.__defaults__ else 0

for n in range(ndefaults+1):

self._methods[types[:len(types) - n]] = func

return self

return register

def __call__(self, *args):

types = tuple(type(arg) for arg in args[1:])

meth = self._methods.get(types, None)

if meth:

return meth(*args)

else:

return self._default(*args)

def __get__(self, instance, cls):

if instance is not None:

return types.MethodType(self, instance)

else:

return self

为了使用描述器版本,你需要像下面这样写:

class Spam:

@multimethod

def bar(self, *args):

# Default method called if no match

raise TypeError("No matching method for bar")

@bar.match(int, int)

def bar(self, x, y):

print("Bar 1:", x, y)

@bar.match(str, int)

def bar(self, s, n = 0):

print("Bar 2:", s, n)

描述器方案同样也有前面提到的限制(不支持关键字参数和继承)。

所有事物都是平等的,有好有坏,也许最好的办法就是在普通代码中避免使用方法重载。不过有些特殊情况下还是有意义的,比如基于模式匹配的方法重载程序中。举个例子,8.21小节中的访问者模式可以修改为一个使用方法重载的类。但是,除了这个以外,通常不应该使用方法重载(就简单的使用不同名称的方法就行了)。

在Python社区对于实现方法重载的讨论已经由来已久。对于引发这个争论的原因,可以参考下Guido van Rossum的这篇博客:Five-Minute Multimethods in Python

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值