Python是一种面向对象的语言。通常可以在类的__init__方法中定义了如何创建新对象。下面是一个简单的类,可以实现两个实例变量存储的功能:
class MyClass:
def __init__(self, attr1, attr2):
self.attr1 = attr1
self.attr2 = attr2
def get_variables(self):
return self.attr1, self.attr2
my_object = MyClass("value1", "value2")
my_object.get_variables() # -> ("value1", "value2")
创建对象的语法为() 。在这种情况下,__init__ 方法接受两个参数,这些参数被存储为实例变量。创建对象后,可以调用在对象上使用此数据的方法。
然而,大多数对象要复杂得多。通常,对象需要存储的数据不是直接可用的,需要从其他输入中派生。而我们希望能够从不同类型的输入创建相同的对象。
我在许多 Python 代码库中看到相同的错误,所有这些逻辑都堆叠到__init__ 方法中。虽然可能会在某个辅助的_initialize() 方法中隐藏__init__ 中的一些操作,但结果是一样的:Python 对象的创建逻辑变得难以理解。
让我们看一个例子。我们有一个表示某个配置集合的对象,通常用于从文件中加载该配置。代码如下:
class Configuration:
def __init__(self, filepath):
self.filepath = filepath
self._initialize()
def _initialize(self):
self._parse_config_file()
self._precompute_stuff()
def _parse_config_file(self):
# 解析self.filepath中的文件,并将数据存储在一些变量self.<attr>中
...
def _precompute_stuff(self):
# 使用在self._parse_config_file中定义的变量计算和设置新的实例变量
...
这些问题通常在开发的后期阶段显现出来。许多 Python 开发人员试图通过使情况变得更糟来解决问题。例如:
为什么说所有这些都是糟糕的解决方案?主要是因为它们都是对问题2)的补丁,同时它们都会使问题1)变得更糟。如果你发现自己在处理初始化逻辑并使用上述策略之一,考虑退后一步,采用另一种方法。
为了解决问题1),我尝试将几乎每个类都视为数据类(dataclass)或 NamedTuple(即使不直接使用这些原语)。这意味着我们应该将对象视为仅仅是相关数据的集合。类定义了数据字段的名称和类型,并可选择实现操作该数据的方法。__init__方法不应该做任何其他事情,只需将这些数据分配给相应的实例变量。许多其他语言都为此概念提供了内置构造:结构体(struct)。
为什么优先考虑这种方法而不是普通的Python对象?
为了说明这一点,让我们看一下Configuration类的另一种实现方式:
class Configuration:
def __init__(self, attr1, attr2):
self.attr1 = attr1
self.attr2 = attr2
@classmethod
def from_file(cls, filepath):
parsed_data = cls._parse_config_file(filepath)
computed_data = cls._precompute_stuff(parsed_data)
return cls(
attr1=parsed_data,
attr2=computed_data,
)
@classmethod
def _parse_config_file(cls, filepath):
# 解析filepath中的文件并返回数据
...
@classmethod
def _precompute_stuff(cls, data):
# 使用从配置文件解析的数据计算新数据
...
在这里,__init__方法尽可能简洁。很明显,Configuration需要存储两个属性。如何获取这两个属性的数据不是__init__方法关心的问题。
我们现在不再将文件路径传递给构造函数,而是将其实现为一个类方法的from_file工厂方法。我们还将解析和计算方法转换为类方法,这些方法现在接受输入并返回结果。这些方法返回的数据传递给构造函数,并返回生成的对象。
这种方法的优点是:
更容易理解和推理状态。在实例化后,立即清楚对象上定义了哪些实例属性。
更容易测试。我们的构造函数是纯函数,可以独立调用,不依赖于对象中已存在的状态。
更易于扩展。我们可以轻松地实现其他工厂方法,以替代方式创建Configuration对象,例如从字典中。
更易于保持一致性。在大多数类中遵循这种方法比不断重新发明复杂的自定义初始化逻辑要容易得多。
您还可以考虑将创建代码完全与类本身分离,例如将逻辑移动到函数或工厂类中。
最好将类的__init__ 方法保持简单,并将类视为结构体。将对象构建逻辑移动到工厂方法或构建器中。这将使您的代码更易读、更易测试,并且更易于将来扩展,并编写更健壮的 Python 代码。
网站声明:如果转载,请联系本站管理员。否则一切后果自行承担。
添加我为好友,拉您入交流群!
请使用微信扫一扫!