我经常发现自己遇到了同样的问题。一个常见的模式是我创建一个执行某些操作的类。例如。加载数据、转换/清理数据、保存数据。那么问题就出现了如何传递/保存中间数据。看看以下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的clean run功能。最pythonic的方式是什么?
最佳答案
与其他答案中所说的不同,我认为这不是个人品味的问题。
正如您所写,DataManipulator2
乍一看,似乎更容易测试。 (但正如@AliFaizan 所说,对需要数据库连接的函数进行单元测试并不容易。)而且测试似乎更容易,因为它是 无国籍 .无状态类并不自动更容易测试,但更容易理解:对于一个输入,你总是得到相同的输出。
但这不是唯一的一点:使用 DataManipulator2
, run
中的 Action 顺序不会错的,因为每个函数都会传递一些数据给下一个,没有这个数据下一个就无法进行。对于静态(和强)类型语言,这会更明显,因为您甚至无法编译错误的 run
功能。
相反,DataManipulator
不容易测试,有状态并且不能确保操作的顺序。这就是方法 DataManipulator.run
的原因太干净了。它的事件太干净了,因为它的实现隐藏了一些非常重要的东西:函数调用是有序的。
因此,我的回答是:更喜欢 DataManipulator2
实现到 DataManipulator
执行。
但是是DataManipulator2
完美的?是和否。对于快速而肮脏的实现,这就是要走的路。但让我们尝试走得更远。
您需要功能 run
公开,但 load_data
, save_data
和 transform
没有理由公开(“公开”我的意思是:没有用下划线标记为实现细节)。如果您用下划线标记它们,则它们不再是契约(Contract)的一部分,并且您对测试它们感到不舒服。为什么?因为尽管可能会出现测试失败,但实现可能会在不破坏类契约的情况下发生变化。这是一个残酷的困境:要么是你的类(class) 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
的实例类(class)。但是只要您的transform
开始变得复杂,一个类是一个很好的解决方案。
关于Python 设计模式 : using class attributes to store data vs. 局部函数变量,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55706215/