19

什么时候以及如何在 python 中使用静态方法?我们已经建立了使用类方法作为工厂方法来创建对象的实例,应该尽可能避免。换句话说,使用类方法作为替代构造函数并不是最佳实践(请参阅Python 对象的工厂方法 - 最佳实践)。

假设我有一个类用于表示数据库中的一些实体数据。想象一下,数据是一个dict包含字段名称和字段值的对象,其中一个字段是使数据唯一的 ID 号。

class Entity(object):
    def __init__(self, data, db_connection):
        self._data = data
        self._db_connection

在这里,我的__init__方法采用实体数据dict对象。假设我只有一个 ID 号,我想创建一个Entity实例。首先我需要找到其余的数据,然后创建我的Entity对象的一个​​实例。根据我之前的问题,我们确定应该尽可能避免使用类方法作为工厂方法。

class Entity(object):

    @classmethod
    def from_id(cls, id_number, db_connection):
        filters = [['id', 'is', id_number]]
        data = db_connection.find(filters)
        return cls(data, db_connection)

    def __init__(self, data, db_connection):
        self._data = data
        self._db_connection


# Create entity
entity = Entity.from_id(id_number, db_connection)

上面是一个例子,说明如果有替代方案,不该做什么或至少不该做什么。现在我想知道是否编辑我的类方法,使其更像是一种实用方法而更少的工厂方法是一个有效的解决方案。换句话说,以下示例是否符合使用静态方法的最佳实践。

class Entity(object):

    @staticmethod
    def data_from_id(id_number, db_connection):
        filters = [['id', 'is', id_number]]
        data = db_connection.find(filters)
        return data


# Create entity
data = Entity.data_from_id(id_number, db_connection)
entity = Entity(data)

还是使用独立函数从 ID 号中查找实体数据更有意义。

def find_data_from_id(id_number, db_connection):
    filters = [['id', 'is', id_number]]
    data = db_connection.find(filters)
    return data


# Create entity.
data = find_data_from_id(id_number, db_connection)
entity = Entity(data, db_connection)

注意:我不想改变我的__init__方法。以前人们建议让我的__init__方法看起来像这样__init__(self, data=None, id_number=None),但可能有 101 种不同的方法来查找实体数据,所以我更愿意在某种程度上保持逻辑分离。说得通?

4

4 回答 4

30

什么时候以及如何在 python 中使用静态方法?

油嘴滑舌的答案是:不经常。

甚至说白了但不是很无用的答案是:当它们使您的代码更具可读性时。


首先,让我们绕道文档

Python 中的静态方法类似于 Java 或 C++ 中的静态方法。另请参阅classmethod()可用于创建替代类构造函数的变体。

所以,当你需要 C++ 中的静态方法时,你需要 Python 中的静态方法,对吧?

嗯,不。

在 Java 中,没有函数,只有方法,因此您最终创建的伪类只是静态方法的捆绑包。在 Python 中做同样事情的方法就是使用自由函数。

这很明显。然而,最好的 Java 风格是尽可能努力地寻找一个合适的类来插入一个函数,这样你就可以避免编写那些伪类,而做同样的事情是不好的 Python 风格——再次,使用自由函数——而且这个不太明显。

C++ 没有与 Java 相同的限制,但许多 C++ 风格无论如何都非常相似。(另一方面,如果你是一个“现代 C++”程序员,内化了“自由函数是类接口的一部分”的成语,那么你对“静态方法在哪里有用”的直觉对于 Python 来说可能是相当不错的。)


但是,如果您是从第一原则而不是从另一种语言来解决这个问题的,那么有一种更简单的看待事物的方法:

A@staticmethod基本上只是一个全局函数。如果您有一个函数,如果它拼写为,foo_module.bar()由于某种原因会更易读,请将其设为. 如果没有,不要。这就是它的全部。唯一的问题是建立对惯用 Python 程序员更易读的内容的直觉。foo_module.BazClass.bar()@staticmethod

当然,@classmethod当您需要访问类而不是实例时使用 a ——正如文档所暗示的那样,备用构造函数是这种情况的范例。尽管您通常可以通过显式引用类来模拟 a @classmethod@staticmethod尤其是当您没有太多子类化时),但您不应该这样做。


最后,解决您的具体问题:

如果客户端需要通过 ID 查找数据的唯一原因是构造一个Entity,这听起来像是您不应该公开的实现细节,而且它也会使客户端代码更加复杂。只需使用构造函数。如果你不想修改你的__init__(你是对的,你可能不希望有充分的理由),使用 a@classmethod作为替代构造函数:Entity.from_id(id_number, db_connection).

另一方面,如果该查找在其他与Entity构造无关的情况下对客户来说本质上是有用的,那么这似乎与类无关Entity(或者至少与同一模块中的其他任何东西无关) )。所以,只要让它成为一个免费的功能。

于 2013-02-22T19:42:24.100 回答
11

链接问题的答案具体说明了这一点:

@classmethod 是执行“替代构造函数”的惯用方式——stdlib 中到处都有示例——itertools.chain.from_iterable、datetime.datetime.fromordinal 等。

所以我不知道你是怎么想到使用类方法本质上是不好的。我实际上喜欢在您的特定情况下使用类方法的想法,因为它使遵循代码和使用 api 变得容易。

另一种方法是使用默认构造函数参数,如下所示:

class Entity(object):
    def __init__(self, id, db_connection, data=None):
        self.id = id
        self.db_connection = db_connection
        if data is None:
            self.data = self.from_id(id, db_connection)
        else:
            self.data = data

    def from_id(cls, id_number, db_connection):
        filters = [['id', 'is', id_number]]
        return db_connection.find(filters)

但是,我更喜欢您最初编写的 classmethod 版本。特别是因为data它相当模棱两可。

于 2013-02-22T06:08:28.847 回答
9

你的第一个例子对我来说最有意义:Entity.from_id非常简洁明了。

它避免了data在接下来的两个示例中的使用,它没有描述返回的内容;用于data构造Entity. 如果您想具体说明data用于构造的Entity方法,那么您可以将您的方法命名为类似Entity.with_data_for_id或等效的函数entity_with_data_for_id

使用诸如这样的动词find也可能会让人很困惑,因为它没有给出任何返回值的指示——当它找到数据时函数应该做什么?(是的,我意识到str有一个find方法;不是更好地命名index_of吗?但是还有index……)它让我想起了经典:

找到 x

我总是试着去想一个名字对那些(a)不了解系统,以及(b)了解系统其他部分的人来说意味着什么——并不是说我总是成功的!

于 2013-02-22T06:08:18.767 回答
1

这是@staticmethod 的一个不错的用例。

我一直在做一个游戏作为一个副项目。该游戏的一部分包括基于统计数据的掷骰子,以及拾取影响角色统计数据的物品和效果的可能性(无论好坏)。

当我在游戏中掷骰子时,我基本上需要说……获取基本角色统计数据,然后将任何库存和效果统计数据添加到这个庞大的网状图形中。

您不能在不指示程序如何操作的情况下获取这些抽象对象并添加它们。我也没有在类级别或实例级别做任何事情。我不想在某个全局模块中定义函数。最后一个最佳选择是使用静态方法将统计数据加在一起。这样才最有意义。

class Stats:

    attribs = ['strength', 'speed', 'intellect', 'tenacity']

    def __init__(self,
                 strength=0,
                 speed=0,
                 intellect=0,
                 tenacity=0
                 ):
        self.strength = int(strength)
        self.speed = int(speed)
        self.intellect = int(intellect)
        self.tenacity = int(tenacity)

    # combine adds stats objects together and returns a single stats object
    @staticmethod
    def combine(*args: 'Stats'):
        assert all(isinstance(arg, Stats) for arg in args)
        return_stats = Stats()
        for stat in Stats.attribs:
            for _ in args:
                setattr(return_stats, stat,
                        getattr(return_stats, stat) + getattr(_, stat))
        return (return_stats)

这将使统计组合调用像这样工作

a = Stats(strength=3, intellect=3)
b = Stats(strength=1, intellect=-1)
c = Stats(tenacity=5)
print(Stats.combine(a, b, c).__dict__)

{'力量':4,'速度':0,'智力':2,'坚韧':5}

于 2018-03-14T03:31:23.657 回答