关于编码样式:在Python中使用静态方法-最佳实践

Using static methods in python - best practice

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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)

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

1
2
3
4
5
6
7
8
9
10
11
12
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号中查找实体数据是否更有意义?

1
2
3
4
5
6
7
8
9
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种不同的方法来查找实体数据,因此我希望在某种程度上保持这种逻辑的独立性。有道理?


When and how are static methods suppose to be used in python?

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

即使是油嘴滑舌,但也不像无用的答案:当它们使代码更可读时。

首先,让我们绕道去文档:

Static methods in Python are similar to those found in Java or C++. Also see classmethod() for a variant that is useful for creating alternate class constructors.

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

嗯,不。

在爪哇中,没有函数,只是方法,所以最终创建的只是伪静态的捆绑类。在Python中做同样的事情的方法就是只使用自由函数。

这很明显。但是,要想让一个适当的类将函数插入到一个适当的类中,这是很好的Java样式,这样你就可以避免编写那些伪类,而同样的事情又是坏的Python风格,使用自由函数,这就不那么明显了。

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

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

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

当然,当需要访问类时,可以使用@classmethod,但文档暗示,实例替换构造函数并不是这样的范例。虽然您经常可以通过显式引用类(特别是当您没有太多的子类时)来模拟@classmethod@staticmethod,但您不应该这样做。

最后,回答你的具体问题:

如果客户机需要按ID查找数据的唯一原因是构造一个Entity,这听起来像是一个不应该公开的实现细节,而且它还使客户机代码更加复杂。只需使用构造函数。如果您不想修改您的__init__(并且您认为有充分的理由不想修改),可以使用@classmethod作为备用构造函数:Entity.from_id(id_number, db_connection)

另一方面,如果这种查找在其他与Entity构造无关的情况下对客户本身有用,那么这似乎与Entity类(或者至少与同一模块中的其他内容无关)无关。所以,让它成为一个自由函数。


链接问题的答案明确表示:

A @classmethod is the idiomatic way to do an"alternate constructor"—there are examples all over the stdlib—itertools.chain.from_iterable, datetime.datetime.fromordinal, etc.

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

另一种方法是使用默认的构造函数参数,比如:

1
2
3
4
5
6
7
8
9
10
11
12
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相当模糊。


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

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

使用诸如find这样的动词也会非常混乱,因为它没有给出返回值的任何指示——当找到数据时,函数应该做什么?(是的,我意识到str有一个find方法,不是叫index_of更好吗?但是,还有一个index它让我想起了经典的:

find x

我总是想一个名字对一个(a)对系统一无所知,和(b)对系统其他部分一无所知的人意味着什么,而不是说我总是成功的!


下面是@staticmethod的一个不错的用例。

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

当我在游戏中掷骰子时,我需要说…获取基本角色统计,然后将任何库存和效果统计添加到这个大净值数字中。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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)

这会使stat组合调用像这样工作

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

{'strength': 4, 'speed': 0, 'intellect': 2, 'tenacity': 5}

< /块引用>< /块引用>