多个构造函数:Pythonic方式?

Joh*_*nes 57 python constructor initialization idiomatic initializer

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

  1. 传递包含数据的文件
  2. 通过参数直接传递数据
  3. 不要传递数据; 只需创建一个空容器

在Java中,我将创建三个构造函数.以下是在Python中可能出现的情况:

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
Run Code Online (Sandbox Code Playgroud)

在Python中,我看到了三个明显的解决方案,但它们都不是很漂亮:

:使用关键字参数:

def __init__(self, **kwargs):
    if 'file' in kwargs:
        ...
    elif 'timestamp' in kwargs and 'data' in kwargs and 'metadata' in kwargs:
        ...
    else:
        ... create empty container
Run Code Online (Sandbox Code Playgroud)

B:使用默认参数:

def __init__(self, file=None, timestamp=None, data=None, metadata=None):
    if file:
        ...
    elif timestamp and data and metadata:
        ...
    else:
        ... create empty container
Run Code Online (Sandbox Code Playgroud)

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

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

def add_data_from_file(file):
    ...

def add_data(timestamp, data, metadata):
    ...
Run Code Online (Sandbox Code Playgroud)

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

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

什么是最恐怖的解决方案?

gle*_*oux 76

您不能拥有多个具有相同名称的方法Python.函数重载 - 与in不同Java- 不受支持.

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

您可以使用@staticmethodor @classmethoddecorator 创建静态方法或类方法以返回类的实例,或添加其他构造函数.

我建议你这样做:

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
Run Code Online (Sandbox Code Playgroud)

⚠在python中永远不要将可变类型作为默认值.⚠见这里.

  • 请永远不要在python中将可变类型作为默认值.这是初学者需要在python中学习的第一个(也是少数)奇怪的边缘情况之一.尝试做`x = F(); x.data.append(5); y = F(); 打印y.data`.你是一个惊喜.习惯的方式是默认为"None",并在条件或三元运算符中分配给`self.data`和`self.metadata`. (18认同)
  • 约翰内斯,其他人可以纠正我,如果我错了(仍然是Python的新手),但我认为这是因为继承.假设一个新类"G"继承了类`F`.使用`@ classmethod`,调用`G.from_file`给出一个'G`的实例.使用`@ staticmethod`,类名被硬编码到方法中,因此`G.from_file`将给出一个'F`的实例,除非`G`覆盖该方法. (3认同)
  • @Mark然后如果有人用空的dict调用构造函数,他们打算与其他东西共享,它会被一个新的空字典替换掉?这可能导致一些讨厌的头脑风暴:`my_dict = {}; f = F(metadata = my_dict); my_dict [1] = 2; f.metadata => {}`.在这里,`f.metadata`当然应该是`{1:2}`. (2认同)
  • @Mark您的评论启发了另一个问题:/sf/ask/3134899351/ (2认同)

900*_*000 25

您不能拥有多个构造函数,但可以使用多个适当命名的工厂方法.

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.
Run Code Online (Sandbox Code Playgroud)

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


Ary*_*thy 15

大多数Pythonic都是Python标准库已经做的.核心开发人员Raymond Hettinger(该collections人)就此进行了讨论,并提供了如何编写课程的一般指导原则.

使用单独的类级函数来初始化实例,比如dict.fromkeys()不是类初始值设定项但仍返回实例dict.这使您可以灵活地使用所需的参数,而无需在需求更改时更改方法签名.