今天小编给大家分享一下Python怎么定义多个构造器方法重载和泛方法的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。
什么是“泛方法”?
由多个方法组成的方法,这些方法为不同的类型实现相同的操作。
举个栗子
现在有个需求,需要你通过以下几种方式创建一个自定义的日期类(
CustomDate
):时间戳
年、月、日(包含三个整数的元组)
ISO 格式的字符串
-
类Datetime
一般实现
from datetime import date, datetime
class CustomDate:
def __init__(self, arg):
if isinstance(arg, (int, float)):
self.__date = date.fromtimestamp(arg)
elif isinstance(arg, tuple) and len(arg) == 3 and all(map(lambda x: isinstance(x, int), arg):
self.__date = date(*arg)
elif isinstance(arg, str):
self.__date = date.fromisoformat(arg)
elif isinstance(arg, datetime):
self.__date = datetime.date()
else:
raise TypeError("could not create instance from " + type(arg).__name__)
@property
def date():
return self.__date
注:这里暂不讨论传入的日期/时间戳合不合法,仅仅只对类型做大致判断。
有没有更好的方式?
我们可以将不同的构建方式拆分为多个方法,并利用
functools
中的 singledispatchmethod
装饰器来根据传入的参数类型决定调用哪个方法。from datetime import date, datetime
from functools import singledispatchmethod
class CustomDate:
@singledispatchmethod
def __init__(self, arg):
raise TypeError("could not create instance from " + type(arg).__name__)
@__init__.register(int)
@__init__.register(float)
def __from_timestamp(self, arg):
self.__date = date.fromtimestamp(arg)
@__init__.register(tuple)
def __from_tuple(self, arg):
if len(arg) == 3 and all(map(lambda x: isinstance(x, int), arg)):
self.__date = date(*arg)
else:
raise ValueError("could not create instance from a malformed tuple")
@__init__.register(str)
def __from_isoformat(self, arg):
self.__date = date.fromisoformat(arg)
@__init__.register(datetime)
def __from_datetime(self, arg):
self.__date = arg.date()
@property
def date(self):
return self.__date
这样一来,我们便能将每种参数类型的初始化独立成一个个的方法了。
缺点
#1 单分派
在调用期间应该使用哪个方法实现由分派算法决定。如果该算法只基于单个参数的类型来决定使用哪个方法实现,则称其为单分派。
singledispatchmethod
就是就是单分派的。也就是说,只有第一个参数会作为考量。这在实际业务中是远远不足的。#2 不支持 typing
然而,如上,对元组中元素类型判断还是需要我们用
if
/else
实现。也就是说,我们不能使用 typing.Tuple[int, int, int]
。作为一种折中的方案,或许我们可以定义一个
ThreeIntTuple
类来对其进行限定,将这些判断从 CustomDate
类中隔离开来。这里仅提供一个思路让大家参考,我就不实现了(因为我们有更好的方式 xD)。
替代方案:multimethod 库
这个库不是标准库之一,需要通过 pip 安装:
pip install multimethod
优势
multimethod
采用的是多分派算法,能更好地满足更复杂的场景。此外,该库对 typing
中的类型也有不错的支持。更更好的实践方式
回到上面的问题,我们可以这么改进:
-
使用
方法来替代multimethod
;singledispatchmethod
-
使用
来替代Tuple[int, int, int]
,不再需要手动校验元组的长度和元素类型了;tuple
from datetime import date, datetime
from typing import Tuple, Union
from multimethod import multimethod
class CustomDate:
@multimethod
def __init__(self, arg):
raise TypeError("could not create instance from " + type(arg).__name__)
@__init__.register
def __from_timestamp(self, arg: Union[int, float]):
self.__date = date.fromtimestamp(arg)
@__init__.register
def __from_tuple(self, arg: Tuple[int, int, int]):
self.__date = date(*arg)
@__init__.register
def __from_isoformat(self, arg: str):
self.__date = date.fromisoformat(arg)
@__init__.register
def __from_datetime(self, arg: datetime):
self.__date = arg.date()
@property
def date(self):
return self.__date
究极好的实践方式(真正的方法重载)
在此之前,先问大家一个简单的问题(这跟我们之后的内容有很大的联系):
class A:
def a(self):
print(1)
def a(self):
print(2)
A().a()
以上这段代码会输出什么?还是会抛出错误?
输出
2
。在 Python 中,如果定义了重名的方法,最后一个方法是会覆盖掉之前的方法的。
但你或许不知,我们可以通过元类(metaclass)来改变这一行为:
class MetaA(type):
class __prepare__(dict):
def __init__(*args):
pass
def __setitem__(self, key, value):
if self.get('a'): # Line 7
super().__setitem__('b', value) # Line 8
else:
super().__setitem__(key, value)
class A(metaclass=MetaA):
def a(self):
print(1)
def a(self):
print(2)
A().a() # => 1
A().b() # => 2 # Line 22
在第 7 和第 8 行,我们将重名的
a
方法改名为 b
,并在第 22 行成功地调用它了。
multimethod
的维护者们很好地运用了这一点,对重名的方法进行了处理,以达到一种“特殊的效果”。回到正题,我们可以做出如下改进:
-
将
设置为multimethod.multidata
类的元类;CustomDate
-
将所有方法命名为
。__init__
from datetime import date, datetime
from typing import Tuple, Union
from multimethod import multimeta
class CustomDate(metaclass=multimeta):
def __init__(self, arg: Union[int, float]):
self.__date = date.fromtimestamp(arg)
def __init__(self, arg: Tuple[int, int, int]):
self.__date = date(*arg)
def __init__(self, arg: str):
self.__date = date.fromisoformat(arg)
def __init__(self, arg: datetime):
self.__date = arg.date()
def __init__(self, arg):
raise TypeError("could not create instance from " + type(arg).__name__)
@property
def date(self):
return self.__date
从效果上来看,这完全和静态语言的方法重载一模一样!