多个构造函数:Pythonic 方式? [复制]

Posted

技术标签:

【中文标题】多个构造函数:Pythonic 方式? [复制]【英文标题】:Multiple constructors: the Pythonic way? [duplicate] 【发布时间】:2017-11-29 15:12:46 【问题描述】:

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

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

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

什么是最 Pythonic 的解决方案?

【问题讨论】:

相关:***.com/questions/7113032/…. 还有其他选择。 ***.com/questions/19305296/… 另外,我总是试图让我的代码满足我的需求,而不是围绕它们编写来让我的代码更纯粹。 虽然这里的所有答案都集中在提供解决方案上,但 Jörg W Mittag provides a very nice explanation 关于为什么函数重载在动态语言中没有意义。 【参考方案1】:

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。

【讨论】:

@classmethod 会更干净;方法很好。 请永远,永远,永远,将可变类型作为python中的默认值。这是初学者需要在 python 中学习的第一个(也是少数几个)奇怪的边缘案例之一。尝试做x = F(); x.data.append(5); y = F(); print y.data。你有一个惊喜。惯用的方法是默认为None,并在条件或三元运算符内分配给self.dataself.metadata Johannes,如果我错了其他人可以纠正我(仍然是 Python 新手),但我认为这是因为继承。假设一个新类G 继承了类F。使用@classmethod,调用G.from_file 会给出G 的实例。使用@staticmethod,类名被硬编码到方法中,因此G.from_file 将给出F 的实例,除非G 覆盖该方法。 @Mark 然后,如果有人用一个空字典调用构造函数,他们打算与其他东西共享,它会被一个新的空字典替换吗?这可能会导致一些令人讨厌的令人头疼的事情:my_dict = ; f = F(metadata=my_dict); my_dict[1] = 2; f.metadata => 。这里,f.metadata 当然应该是1: 2 @Mark 您的评论引发了另一个问题:***.com/questions/44784276/…【参考方案2】:

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

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.

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

【讨论】:

【参考方案3】:

大多数 Pythonic 将是 Python 标准库已经具备的功能。核心开发人员 Raymond Hettinger(collections 家伙)gave a talk on this,以及如何编写类的一般指南。

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

【讨论】:

【参考方案4】:

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

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

【讨论】:

【参考方案5】:

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

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 

【讨论】:

这里有一个微妙的错误。由于参数列表是在首次创建函数时评估的,因此数据和元数据的列表和字典将是有效的全局变量。不过基本上是合理的,除了那个问题。 所以最好使用关键字参数? 那个,或者你可以使用None作为默认值,然后self.data = data or []【参考方案6】:

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

【讨论】:

【参考方案7】:

最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

【讨论】:

永远不要使用可变类型作为默认值——它可能会引入一些非常微妙的错误,这些错误很难找到。总是建议做类似func( data = None ): data = data or []的事情;多一行,少一个错误 :-) python-guide-pt-br.readthedocs.io/en/latest/writing/gotchas

以上是关于多个构造函数:Pythonic 方式? [复制]的主要内容,如果未能解决你的问题,请参考以下文章

C++——构造函数析构函数以及复制构造函数

c++ 复制构造函数和析构函数

5 规则(用于构造函数和析构函数)过时了吗?

确保函数的输入参数是 int/str 的 Pythonic 方式? [复制]

链表:如何实现析构函数、复制构造函数和复制赋值运算符?

对象做函数参数和函数返回值时,调用复制构造函数,构造函数,析构函数的情况