多个构造函数:Pythonic 方式?

Multiple constructors: the Pythonic way?

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

  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

在 Python 中,我看到了三个明显的解决方案,但其中 none 个很漂亮:

A: 使用关键字参数:

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: 使用默认参数:

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

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

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)

什么是最 Pythonic 解决方案?

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

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

您可以使用 @staticmethod@classmethod 装饰器将静态方法或 class 方法创建为 return 您的 class 实例,或添加其他构造函数。

我建议你这样做:

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.

此代码的系统目标是什么?从我的角度来看,您的关键短语是 but the user has to know which method he requires. 您希望您的用户对您的代码有什么样的体验?这应该会推动界面设计。

现在,转向可维护性:哪个解决方案最容易阅读和维护?再次,我觉得解决方案C是次等的。对于与我合作过的大多数团队来说,解决方案 B 优于 A:它更容易阅读和理解,尽管两者都很容易分解成小代码块进行处理。

我不确定我是否理解正确,但这行不通吗?

def __init__(self, file=None, timestamp=0, data=[], metadata={}):
    if file:
        ...
    else:
        self.timestamp = timestamp
        self.data = data
        self.metadata = metadata

或者你甚至可以这样做:

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 上进行全局声明,所以这是新方法:

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 {}

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

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

要记住的重要一点是,任何必需的参数都应该 而不是 具有默认值,因为如果不包括它们,您希望引发错误。

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

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

大多数 Pythonic 都是 Python 标准库已经做的。核心开发人员 Raymond Hettinger(collections 人)gave a talk on this,以及如何编写 classes.

的一般指南

使用单独的 class 级函数来初始化实例,例如 dict.fromkeys() 不是 class 初始化程序但仍然 returns [=12] 的实例=].这使您可以灵活地处理所需的参数,而无需随着需求的变化而更改方法签名。

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

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.

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

如果您使用的是 Python 3.4+,则可以使用 functools.singledispatch decorator to do this (with a little extra help from the methoddispatch decorator that @ZeroPiraeus wrote for his answer):

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