Python 类中定义多个构造器方法重载与泛方法

 更新时间:2023年03月22日 10:46:54   作者:披着狼皮的羊_  
这篇文章主要为大家介绍了Python 类中定义多个构造器方法重载与泛方法示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

引言

在 Python 开发中,参数校验这件事一直让我很苦恼;校验嘛,就得写很多与业务不相关的 if/else,特别麻烦;不校验嘛,又担心会有哪些意料之外的 bug,而且在很多情况下报的错误也和具体问题有些偏差,难以定位问题。

归功于 Python 动态语言的特性,这个问题好像没有很好的解决方式。通过历代 Python 版本的发布你也可以看到,官方一直都在完善类型注解(比如最近的 Python 3.11 新增的 Self 类型),似乎想通过某种方式解决此类问题。

不管怎样,就目前来看,泛方法(generic method)或许能很大程度上解决这个问题。

什么是“泛方法”?

由多个方法组成的方法,这些方法为不同的类型实现相同的操作。

举个栗子 🌰

现在有个需求,需要你通过以下几种方式创建一个自定义的日期类(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

从效果上来看,这完全和静态语言的方法重载一模一样!

总结

使用泛方法能让代码更加简洁、灵活。希望上述内容能对你有帮助。

参考资料

以上就是Python 类中定义多个构造器方法重载与泛方法的详细内容,更多关于Python 类多个构造器方法的资料请关注脚本之家其它相关文章!

相关文章

  • Numpy中stack(),hstack(),vstack()函数用法介绍及实例

    Numpy中stack(),hstack(),vstack()函数用法介绍及实例

    这篇文章主要介绍了Numpy中stack(),hstack(),vstack()函数用法介绍及实例,具有一定借鉴价值,需要的朋友可以参考下
    2018-01-01
  • 使用Python实现Wake On Lan远程开机功能

    使用Python实现Wake On Lan远程开机功能

    这篇文章主要介绍了使用Python实现Wake On Lan远程开机功能,文中给大家补充介绍了python通过wakeonlan唤醒内网电脑开机,非常不错,感兴趣的朋友跟随小编一起学习吧
    2020-01-01
  • Python机器学习利用随机森林对特征重要性计算评估

    Python机器学习利用随机森林对特征重要性计算评估

    本文是对随机森林如何用在特征选择上做一个简单的介绍。随机森林非常简单,易于实现,计算开销也很小,更令人惊奇的是它在分类和回归上表现出了十分惊人的性能
    2021-10-10
  • Python实战爬虫之女友欲买文胸不知何色更美

    Python实战爬虫之女友欲买文胸不知何色更美

    实践来源于理论,做爬虫前肯定要先了解相关的规则和原理,网络爬虫又称为网页蜘蛛,网络机器人,更经常的称为网页追逐者,是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本。一句话概括就是网上信息搬运工。本篇文章带你深入了解,需要的朋友可以参考下
    2021-09-09
  • python使用py2neo查询Neo4j的节点、关系及路径

    python使用py2neo查询Neo4j的节点、关系及路径

    本文介绍了使用Py2neo的NodeMatcher和RelationshipMatcher查询图中的节点和关系,以及通过执行Cypher语句的查询方式。感兴趣的小伙伴请看下文
    2021-08-08
  • python3 动态模块导入与全局变量使用实例

    python3 动态模块导入与全局变量使用实例

    今天小编就为大家分享一篇python3 动态模块导入与全局变量使用实例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2019-12-12
  • pycharm右键没有run,run不了问题的解决

    pycharm右键没有run,run不了问题的解决

    这篇文章主要介绍了pycharm右键没有run,run不了问题的解决方案,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-08-08
  • django admin实现动态多选框表单的示例代码

    django admin实现动态多选框表单的示例代码

    借助django-admin,可以快速得到CRUD界面,但若需要创建多选标签字段时,需要对表单进行调整,本文通过示例代码给大家介绍django admin多选框表单的实现方法,感兴趣的朋友跟随小编一起看看吧
    2021-05-05
  • Python学习笔记之函数的参数和返回值的使用

    Python学习笔记之函数的参数和返回值的使用

    这篇文章主要介绍了Python学习笔记之函数的参数和返回值的使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-11-11
  • 在keras中model.fit_generator()和model.fit()的区别说明

    在keras中model.fit_generator()和model.fit()的区别说明

    这篇文章主要介绍了在keras中model.fit_generator()和model.fit()的区别说明,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-06-06

最新评论