多个构造函数:Pythonic方式?

时间:2017-06-26 17:40:52

标签: python constructor initialization initializer idiomatic

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

  1. 传递包含数据的文件
  2. 直接通过参数传递数据
  3. 不要传递数据;只需创建一个空容器
  4. 在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中,我看到了三个明显的解决方案,但它们都不是很漂亮:

    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似乎是最好的,但用户必须知道他需要哪种方法。例如:如果他不知道c = Container(args)是什么,他就不能args

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

7 个答案:

答案 0 :(得分:77)

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

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

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

我建议你这样做:

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
  

⚠在python中永远不会有可变类型作为默认值。 ⚠   请参阅here

答案 1 :(得分:26)

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

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.

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

答案 2 :(得分:16)

大多数Pythonic都是Python标准库已经做的。核心开发人员Raymond Hettinger(collections家伙)gave a talk on this,以及如何编写课程的一般指导原则。

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

答案 3 :(得分:5)

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

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

答案 4 :(得分:4)

我不确定我是否理解正确但不会做这项工作?

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

答案 5 :(得分:3)

如果您使用的是Python 3.4+,则可以使用functools.singledispatch装饰器执行此操作(methoddispatch@ZeroPiraeushis 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 装饰器提供一些额外帮助):

val newDF = df.withColumn("CID", col("CID").cast("string"))
.withColumn("STD", col("STD").cast("string"))

newDF.printSchema()

答案 6 :(得分:0)

最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