对象的工厂方法 - 最佳实践?

31 投票
1 回答
25378 浏览
提问于 2025-04-17 16:34

这是一个关于在Python中如何从不同形式的数据创建类或类型实例的最佳实践的问题。是使用类方法更好,还是使用一个单独的函数更好呢?假设我有一个类,用来描述文档的大小。(注意:这只是一个例子。我想知道创建类实例的最佳方法,而不是描述文档大小的最佳方法。)

class Size(object):
    """
    Utility object used to describe the size of a document.
    """

    BYTE = 8
    KILO = 1024

    def __init__(self, bits):
        self._bits = bits

    @property
    def bits(self):
        return float(self._bits)

    @property
    def bytes(self):
        return self.bits / self.BYTE

    @property
    def kilobits(self):
        return self.bits / self.KILO

    @property
    def kilobytes(self):
        return self.bytes / self.KILO

    @property
    def megabits(self):
        return self.kilobits / self.KILO

    @property
    def megabytes(self):
        return self.kilobytes / self.KILO

我的 __init__ 方法接受一个以比特为单位的大小值(就是比特,我想保持这个方式),但假设我有一个以字节为单位的大小值,我想创建这个类的实例。使用类方法更好,还是使用一个单独的函数更好呢?

class Size(object):
    """
    Utility object used to describe the size of a document.
    """

    BYTE = 8
    KILO = 1024

    @classmethod
    def from_bytes(cls, bytes):
        bits = bytes * cls.BYTE
        return cls(bits)

或者

def create_instance_from_bytes(bytes):
    bits = bytes * Size.BYTE
    return Size(bits)

这可能看起来不是个问题,也许这两种方法都是有效的,但每次我需要实现类似的功能时,我都会考虑这个问题。很长一段时间,我更喜欢使用类方法,因为我喜欢将类和工厂方法结合在一起的组织好处。而且,使用类方法可以创建任何子类的实例,这样更符合面向对象的原则。另一方面,我的一个朋友曾经说过:“当你不确定时,就做标准库里的做法”,但我还没有在标准库中找到这样的例子。

1 个回答

36

首先,大多数时候你觉得需要像这样的东西,其实并不需要;这说明你在试图把Python当成Java来用。解决的方法是退一步,问问自己为什么需要工厂函数。

很多时候,最简单的做法就是用一个构造函数,里面可以有默认值、可选参数或者关键字参数。即使在Java中你不会这样写的情况,甚至在C++或ObjC中重载构造函数感觉不对的情况,在Python中看起来也很自然。例如,size = Size(bytes=20),或者size = Size(20, Size.BYTES),这些写法都很合理。再比如,一个Bytes(20)类,继承自Size,只不过重载了__init__,这也看起来很正常。而且这些定义起来都很简单:

def __init__(self, *, bits=None, bytes=None, kilobits=None, kilobytes=None):

或者:

BITS, BYTES, KILOBITS, KILOBYTES = 1, 8, 1024, 8192 # or object(), object(), object(), object()
def __init__(self, count, unit=Size.BITS):

不过,有时候你确实需要工厂函数。那么,这时候该怎么办呢?其实,通常有两种东西会被统称为“工厂”。

@classmethod是实现“备用构造函数”的标准写法——在标准库中有很多例子,比如itertools.chain.from_iterabledatetime.datetime.fromordinal等等。

而函数则是实现“我不在乎实际类是什么”的工厂的标准写法。比如,看看内置的open函数。你知道它在3.3版本中返回什么吗?你在乎吗?并不在乎。这就是为什么它是一个函数,而不是io.TextIOWrapper.open之类的。

你给出的例子看起来是一个完全合理的用例,明显适合“备用构造函数”的分类(如果它不适合“带额外参数的构造函数”的分类的话)。

撰写回答