开发者

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

目录
  • 引言
  • 什么是“泛方法”?
    • 举个栗子
    • 一般实现
    • 有没有更好的方式?
    • 缺点
      • #1 单分派
      • #2 不支持 typing
  • 替代方案:multimethod 库
    • 优势
      • 更更好的实践方式
        • 究极好的实践方式(真正的方法重载)
        • 总结

          引言

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

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

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

          什么是“泛方法”?

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

          举个栗子

          开发者_JAVA开发

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

          注:这里暂不讨论传入的日期/时间戳合不合法,仅仅只对类型做大致判断。

          有没有更好的方式?

          我们www.devze.com可以将不同的构建方式拆分为多个方法,并利用 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(*ar编程g)
                  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(sjavascriptelf, arg: dateFVuLeMhDntime):
                  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

          javascript

          在 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
          

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

          总结

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

          参考资料

          • peps.python.org/pep-0443/
          • github.com/coady/multi…
          • realpython.com/python-mult…

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

          0

          上一篇:

          下一篇:

          精彩评论

          暂无评论...
          验证码 换一张
          取 消

          最新开发

          开发排行榜