我经常发现自己遇到了同样的问题。一个常见的模式是我创建一个执行某些操作的类。例如。加载数据,转换/清除数据,保存数据。然后出现了问题,即如何传递/保存中间数据。查看以下2个选项:
import read_csv_as_string, store_data_to_database
class DataManipulator:
''' Intermediate data states are saved in self.results'''
def __init__(self):
self.results = None
def load_data(self):
'''do stuff to load data, set self.results'''
self.results = read_csv_as_string('some_file.csv')
def transform(self):
''' transforms data, eg get first 10 chars'''
transformed = self.results[:10]
self.results = transformed
def save_data(self):
''' stores string to database'''
store_data_to_database(self.results)
def run(self):
self.load_data()
self.transform()
self.save_data()
DataManipulator().run()
class DataManipulator2:
''' Intermediate data states are not saved but passed along'''
def load_data(self):
''' do stuff to load data, return results'''
return read_csv_as_string('some_file.csv')
def transform(self, results):
''' transforms data, eg get first 10 chars'''
return results[:10]
def save_data(self, data):
''' stores string to database'''
store_data_to_database(data)
def run(self):
results = self.load_data()
trasformed_results = self.transform(results)
self.save_data(trasformed_results)
DataManipulator2().run()
现在,对于编写测试,我发现DataManipulator2更好,因为可以更轻松地独立测试功能。同时,我也喜欢DataManipulator的干净运行功能。什么是最 python 的方式?
最佳答案
与其他答案中所说的不同,我认为这不是个人喜好。
正如您所写,乍一看DataManipulator2
似乎更容易测试。 (但是正如@AliFaizan所说,对需要数据库连接的功能进行单元测试并不容易。)而且由于它是无状态,因此似乎更易于测试。无状态类并不是自动更容易测试,但更容易理解:对于一个输入,您总是会得到相同的输出。
但这不是唯一的要点:使用DataManipulator2
,在run
中的操作顺序不会错,因为每个函数都会将一些数据传递给下一个,而下一个不能没有这些数据就继续进行。对于静态(强类型)语言,这将更加明显,因为您甚至无法编译错误的run
函数。
相反,DataManipulator
不容易测试,有状态且不能确保操作顺序。这就是为什么DataManipulator.run
方法如此干净的原因。它的事件太干净了,因为它的实现隐藏了一些非常重要的东西:函数调用是有序的。
因此,我的答案是:比DataManipulator2
实现更喜欢DataManipulator
实现。
但是DataManipulator2
完美吗?是的,没有。对于快速而肮脏的实现,这是要走的路。但是,让我们尝试进一步。
您需要将函数run
公开,但是load_data
,save_data
和transform
没有理由公开(通过“public”我的意思是:未标记为实现细节,但带有下划线)。如果用下划线标记它们,它们将不再是契约(Contract)的一部分,并且您不愿意对其进行测试。为什么?因为尽管可能存在测试失败,但实现可能会在不违反类约定的情况下进行更改。这是一个残酷的难题:您的类DataManipulator2
具有正确的API还是无法完全测试。
不过,这些功能应该是可测试的,但应作为另一类API的一部分。考虑一下三层架构:
load_data
和save_data
在数据层transform
在业务层中。 run
调用位于表示层让我们尝试实现这一点:
class DataManipulator3:
def __init__(self, data_store, transformer):
self._data_store = data_store
self._transformer = transformer
def run(self):
results = self._data_store.load()
trasformed_results = self._transformer.transform(results)
self._data_store.save(transformed_results)
class DataStore:
def load(self):
''' do stuff to load data, return results'''
return read_csv_as_string('some_file.csv')
def save(self, data):
''' stores string to database'''
store_data_to_database(data)
class Transformer:
def transform(self, results):
''' transforms data, eg get first 10 chars'''
return results[:10]
DataManipulator3(DataStore(), Transformer()).run()
这还不错,而且
Transformer
很容易测试。但:DataStore
并不方便:要读取的文件也埋在了代码和数据库中。 DataManipulator
应该能够对多个数据样本运行Transformer
。 因此,另一个解决这些问题的版本:
class DataManipulator4:
def __init__(self, transformer):
self._transformer = transformer
def run(self, data_sample):
data = data_sample.load()
results = self._transformer.transform(data)
self.data_sample.save(results)
class DataSample:
def __init__(self, filename, connection)
self._filename = filename
self._connection = connection
def load(self):
''' do stuff to load data, return results'''
return read_csv_as_string(self._filename)
def save(self, data):
''' stores string to database'''
store_data_to_database(self._connection, data)
with get_db_connection() as conn:
DataManipulator4(Transformer()).run(DataSample('some_file.csv', conn))
还有一点:文件名。尝试使用类似文件的对象而不是文件名作为参数,因为您可以使用
io
module测试代码:class DataSample2:
def __init__(self, file, connection)
self._file = file
self._connection = connection
...
dm = DataManipulator4(Transformer())
with get_db_connection() as conn, open('some_file.csv') as f:
dm.run(DataSample2(f, conn))
使用mock objects,现在可以非常轻松地测试类的行为。
让我们总结一下此代码的优点:
DataManipulator2
一样)run
方法非常干净(就像DataManipulator2
中一样)Transformer
或一个新的DataSample
(从数据库加载并保存到例如csv文件中)当然,这确实是(旧样式)Java风格的。在python中,您可以简单地传递函数
transform
而不是Transformer
类的实例。但是,一旦您的transform
开始变得很复杂,则一个类便是一个很好的解决方案。关于Python设计模式: using class attributes to store data vs.局部函数变量,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/55706215/