关于python:多个构造函数

Multiple constructors: the Pythonic way?

本问题已经有最佳答案,请猛点这里访问。

我有一个保存数据的容器类。创建容器时,有不同的方法来传递数据。

  • 传递包含数据的文件
  • 通过参数直接传递数据
  • 不要传递数据;只需创建一个空容器
  • 在爪哇,我将创建三个构造函数。如果可以在python中使用,它的外观如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Container:

        def __init__(self):
            self.timestamp = 0
            self.data = []
            self.metadata = {}

        def __init__(self, file):
            f = file.open()
            self.timestamp = f.get_timestamp()
            self.data = f.get_data()
            self.metadata = f.get_metadata()

        def __init__(self, timestamp, data, metadata):
            self.timestamp = timestamp
            self.data = data
            self.metadata = metadata

    在python中,我看到了三个明显的解决方案,但没有一个是漂亮的:

    A:使用关键字参数:

    1
    2
    3
    4
    5
    6
    7
    def __init__(self, **kwargs):
        if 'file' in kwargs:
            ...
        elif 'timestamp' in kwargs and 'data' in kwargs and 'metadata' in kwargs:
            ...
        else:
            ... create empty container

    B:使用默认参数:

    1
    2
    3
    4
    5
    6
    7
    def __init__(self, file=None, timestamp=None, data=None, metadata=None):
        if file:
            ...
        elif timestamp and data and metadata:
            ...
        else:
            ... create empty container

    C:只提供构造函数来创建空容器。提供用不同来源的数据填充容器的方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    def __init__(self):
        self.timestamp = 0
        self.data = []
        self.metadata = {}

    def add_data_from_file(file):
        ...

    def add_data(timestamp, data, metadata):
        ...

    解决方案A和B基本相同。我不喜欢做if/else,特别是因为我必须检查是否提供了此方法所需的所有参数。如果用第四种方法扩展代码来添加数据,A比B更灵活。

    解决方案C似乎是最好的,但用户必须知道他需要哪种方法。例如:如果他不知道args是什么,他就不能做c = Container(args)

    最能解决Python问题的方法是什么?


    Python中不能有多个同名的方法。与Java不同,不支持函数重载。

    使用默认参数或**kwargs*args参数。

    可以使用@staticmethod@classmethod修饰器生成静态方法或类方法,以返回类的实例或添加其他构造函数。

    我建议你这样做:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class F:

        def __init__(self, timestamp=0, data=None, metadata=None):
            self.timestamp = timestamp
            self.data = list() if data is None else data
            self.metadata = dict() if metadata is None else metadata

        @classmethod
        def from_file(cls, path):
           _file = cls.get_file(path)
           timestamp = _file.get_timestamp()
           data = _file.get_data()
           metadata = _file.get_metadata()      
           return cls(timestamp, data, metadata)

        @classmethod
        def from_metadata(cls, timestamp, data, metadata):
            return cls(timestamp, data, metadata)

        @staticmethod
        def get_file(path):
            # ...
            pass

    ? Never have mutable types as defaults in python. ?
    See here.


    不能有多个构造函数,但可以有多个适当命名的工厂方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class Document(object):

        def __init__(self, whatever args you need):
           """Do not invoke directly. Use from_NNN methods."""
            # Implementation is likely a mix of A and B approaches.

        @classmethod
        def from_string(cls, string):
            # Do any necessary preparations, use the `string`
            return cls(...)

        @classmethod
        def from_json_file(cls, file_object):
            # Read and interpret the file as you want
            return cls(...)

        @classmethod
        def from_docx_file(cls, file_object):
            # Read and interpret the file as you want, differently.
            return cls(...)

        # etc.

    但是,您不能轻易地阻止用户直接使用构造函数。(如果这很关键,作为开发过程中的安全预防措施,您可以分析构造函数中的调用堆栈,并检查调用是否来自预期的方法之一。)


    大多数pythonic都将是python标准库已经做的事情。核心开发人员RaymondHettinger(collections人)对此进行了讨论,并给出了如何编写类的一般指导原则。

    使用单独的类级函数初始化实例,比如dict.fromkeys()不是类初始值设定项,但仍然返回dict的实例。这使您能够灵活地处理所需的参数,而不必随着需求的变化而更改方法签名。


    我不确定我是否理解正确,但这不管用吗?

    1
    2
    3
    4
    5
    6
    7
    def __init__(self, file=None, timestamp=0, data=[], metadata={}):
        if file:
            ...
        else:
            self.timestamp = timestamp
            self.data = data
            self.metadata = metadata

    或者你可以这样做:

    1
    2
    3
    4
    5
    6
    7
    8
    def __init__(self, file=None, timestamp=0, data=[], metadata={}):
        if file:
            # Implement get_data to return all the stuff as a tuple
            timestamp, data, metadata = f.get_data()

        self.timestamp = timestamp
        self.data = data
        self.metadata = metadata

    感谢Jon Kiparsky的建议,有一种更好的方法可以避免关于datametadata的全球声明,因此这是一种新的方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    def __init__(self, file=None, timestamp=None, data=None, metadata=None):
        if file:
            # Implement get_data to return all the stuff as a tuple
            with open(file) as f:
                timestamp, data, metadata = f.get_data()

        self.timestamp = timestamp or 0
        self.data = data or []
        self.metadata = metadata or {}


    此代码的系统目标是什么?从我的观点来看,您的关键短语是but the user has to know which method he requires.,您希望您的用户对您的代码有什么体验?这将驱动接口设计。

    现在,转向可维护性:哪个解决方案最容易阅读和维护?再次,我觉得解决方案C是劣质的。对于大多数和我一起工作的团队来说,解决方案B比A更可取:虽然两者都很容易分解成小代码块进行治疗,但阅读和理解起来要容易一些。


    如果您使用的是python 3.4+,则可以使用functools.singledispatch装饰器来完成此操作(需要@zeropiraeus为其答案编写的methoddispatch装饰器的一些额外帮助):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class Container:

        @methoddispatch
        def __init__(self):
            self.timestamp = 0
            self.data = []
            self.metadata = {}

        @__init__.register(File)
        def __init__(self, file):
            f = file.open()
            self.timestamp = f.get_timestamp()
            self.data = f.get_data()
            self.metadata = f.get_metadata()

        @__init__.register(Timestamp)
        def __init__(self, timestamp, data, metadata):
            self.timestamp = timestamp
            self.data = data
            self.metadata = metadata


    最简单的方法是确保任何可选参数都有默认值。因此,包括您知道需要的所有参数,并为它们分配适当的默认值。

    1
    2
    def __init__(self, timestamp=None, data=[], metadata={}):
        timestamp = time.now()

    需要记住的一件重要的事情是,任何必需的参数都不应该有默认值,因为您希望在不包括这些参数的情况下引发错误。

    您可以使用参数列表末尾的*args**kwargs接受更多可选参数。

    1
    2
    3
    def __init__(self, timestamp=None, data=[], metadata={}, *args, **kwards):
        if 'something' in kwargs:
            # do something