在Python中使用静态方法 - 最佳实践
在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 个回答
你第一个例子对我来说最有意义:Entity.from_id
这个名字简洁明了。
它避免了在接下来的两个例子中使用 data
,因为这个词并没有清楚地描述返回的内容;data
是用来构建一个 Entity
的。如果你想更具体地说明这个 data
是用来构建 Entity
的话,你可以把方法命名为 Entity.with_data_for_id
或者类似的 entity_with_data_for_id
。
使用像 find
这样的动词也可能让人困惑,因为它并没有说明返回的值是什么——当找到数据后这个函数应该做什么呢?(是的,我知道 str
有一个 find
方法;难道不应该叫 index_of
吗?但还有 index
呢……)这让我想起了经典的例子:
我总是试着考虑一个名字对那些(a)对系统没有了解的人,以及(b)对系统其他部分有了解的人会传达什么——当然,我并不是总能成功!
这个回答专门提到了一点:
@classmethod 是一种常见的写法,用来做“备用构造函数”——在标准库里有很多例子,比如 itertools.chain.from_iterable、datetime.datetime.fromordinal 等等。
所以我不明白你为什么会觉得使用 classmethod 本身就是个坏主意。我其实很喜欢在你这种情况下使用 classmethod,因为这样可以让代码更容易理解,使用起来也方便。
另一种方法是使用默认的构造函数参数,像这样:
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
这个名字有点模糊。
在Python中,静态方法应该什么时候用,怎么用呢?
简单来说:用得不多。
再简单一点的说法是:当它能让你的代码更易读时,就可以用。
首先,我们先看看官方文档:
Python中的静态方法和Java或C++中的类似。还有
classmethod()
,它是一个变种,适合用来创建备用的类构造函数。
所以,如果在C++中需要静态方法,那在Python中也需要静态方法,对吧?
其实不是。
在Java中,只有方法,没有函数,所以你会创建一些伪类,里面全是静态方法。而在Python中,做同样的事情只需要用普通函数就可以了。
这点很明显。不过,在Java中,尽量找一个合适的类来放函数是个好习惯,这样可以避免写那些伪类;而在Python中,做同样的事情就不太好——还是用普通函数吧,这一点就不那么明显了。
C++没有Java那样的限制,但很多C++的风格还是挺相似的。不过,如果你是个“现代C++”程序员,已经习惯了“普通函数是类接口的一部分”这种说法,那么你对“静态方法在哪儿有用”这个问题的直觉可能对Python也挺有帮助。
但是如果你是从基础知识出发,而不是从其他语言过来的话,有一个更简单的看法:
@staticmethod
基本上就是一个全局函数。如果你有一个函数foo_module.bar()
,如果把它写成foo_module.BazClass.bar()
会让人更容易理解,那就把它做成@staticmethod
。如果没有,那就别做。其实就这么简单。唯一的问题是,你需要培养出对什么更易读的直觉,尤其是对Python程序员来说。
当然,当你需要访问类而不是实例时,就用@classmethod
——备用构造函数就是一个典型的例子,文档中也提到了。虽然你通常可以通过显式引用类来模拟@classmethod
,尤其是在没有太多子类的情况下,但最好还是不要这么做。
最后,针对你的具体问题:
如果客户只需要通过ID查找数据来构造一个Entity
,那听起来像是一个不应该暴露的实现细节,而且这样会让客户的代码变得更复杂。直接用构造函数就行。如果你不想修改你的__init__
(而且你说得对,确实有很多好理由不想修改),可以用@classmethod
作为备用构造函数:Entity.from_id(id_number, db_connection)
。
另一方面,如果这个查找在其他情况下对客户是有用的,而和Entity
的构造没有关系,那这就和Entity
类没什么关系(或者说关系不大)。所以,直接把它做成一个普通函数就好。