我经常会遇到同样的问题。一个常见的模式是我创建一个执行某些操作的类。例如。加载数据,转换/清除数据,保存数据。然后出现问题,如何传递/保存中间数据。查看以下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的方式是什么?
答案 0 :(得分:3)
最蟒蛇的方式是什么?
Python支持多种范例。您的第二种形式更接近功能性,第一种形式更势在必行。严格来说,这是一个优先事项,没有上下文。
但是,我有第三个建议,因为我喜欢何时可以避免对象不是有状态的。这很容易测试,并且可以避免在错误的run()
复杂故障方法中遇到各种问题(例如,加载前进行转换,调用两次转换,不进行转换保存等)。
class DataTransformer:
@classmethod
def from_csv(cls, some_file):
'''Because I don't like __init__ to do logic, it's harmful for testability,
but at the same time this is needed data for proper initialization
'''
return cls(read_csv_as_string(some_file))
def __init__(self, raw_data):
''' Feel free to init with bogus test data '''
self.raw_data = raw_data
def transform(self):
''' Returning the data instead of a ContentSaver is a less coupled design (suppose you add more exporters)'''
return self.raw_data[:10]
class ContentSaver:
'''Having a different class makes sense now the data is transformed:
it's a different type of data, from a logical standpoint.'''
def __init__(self, some_content):
self.content = some_content
def save_data(self):
store_data_to_database(self.content)
def run():
'''Note this code part isn't easily testable, so it's better if possible mistakes are made fewer.'''
transformer = DataTransformer.from_csv('some_file')
writer = ContentSaver(transformer.transform())
# Possible further uses of transformer and writer without care of order
writer.save_data()
在对象生命周期的任何时候,它们都保存一致类型的初始化数据。这样一来,它们就可以进行测试,更不易出错,并且更有可能在不同的实现中有用(不仅仅是run()
)。
由于列出的所有好处,我想在管道的每个结构化步骤(DataCleaner等)中编写一个类是值得的,因为随着代码的增长,它将更易于维护。
答案 1 :(得分:3)
与其他答案中所说的不同,我认为这不是个人喜好。
正如您所写,DataManipulator2
乍看起来似乎更容易测试。 (但是正如@AliFaizan所说,对需要数据库连接的函数进行单元测试并不是那么容易。)由于无状态,它似乎更易于测试。无状态类并不是自动更容易测试,但更容易理解:对于一个输入,您总是得到相同的输出。
但这不是唯一的要点:使用DataManipulator2
,run
中的动作顺序不会错,因为每个函数都会将一些数据传递给下一个,而下一个可以没有这些数据就不要继续。对于静态(强烈)类型的语言,这将更加明显,因为您甚至无法编译错误的run
函数。
相反,DataManipulator
不容易测试,有状态,并且不能确保动作的顺序。这就是方法DataManipulator.run
如此干净的原因。这是太干净的事件,因为其实现隐藏了一些非常重要的东西:函数调用是有序的。
因此,我的回答:相对于DataManipulator2
实现,更喜欢DataManipulator
实现。
但是DataManipulator2
完美吗?是的,没有。对于快速而肮脏的实现,这就是要走的路。但是,让我们尝试进一步。
您需要将功能run
公开,但是load_data
,save_data
和transform
没有理由公开(“公开”一词的意思是:未标记(带下划线的实施细节)。如果用下划线标记它们,则它们不再是合同的一部分,并且您不愿意对其进行测试。为什么?因为尽管可能存在测试失败,但实现可能会在不违反类合同的情况下进行更改。这是一个残酷的难题:您的类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
开始变得很复杂,一门课就是一个很好的解决方案。
答案 2 :(得分:0)
我不会讲功能式或命令式。如果语言为您提供了使您的生活更轻松的功能,请使用它,而不管其背后的哲学如何。
您说您发现DataManipulator2
更容易测试。我不同意。例如,在函数save_data
中,您将传递data
作为DataManipulator2
中的输入。在DataManipulator
中,您必须将其用作fixture
。看看两个最著名的python测试库pytest和unittest,以探索编写测试的不同风格。
现在,我看到您需要考虑两件事。首先让您自己使用。您提到您发现DataManipulator
更干净。它表明这种方式对您和您的团队来说更自然。无论我说过DataManipulator2
多少次都变得更加轻松和整洁,这将由您决定如何修改,维护和向他人解释代码。因此,采用最适合您的方法。您应该考虑的第二个重要方面是代码与数据的耦合程度(我认为任何方法都不对)。
在第一种方法中,无论何时执行任何操作,它都会*更改状态(并非总是如此。您的函数可以在self.result
上执行操作,并提供输出而无需更改状态)。您可以像正在编辑启用自动保存功能的文件一样查看它。唯一的区别是您无法撤消(至少不能使用ctr / cmd + z撤消)。在第二个选项中,您或您的班级用户将决定他们是否要保存。可能需要做更多工作,但对于类的创建者和用户来说都是自由的。
结论:定义班级的目的,职责和代码的总体结构。如果是面向数据的类,例如数据python 3.7 data classes和Frozen=False
,采用第一种方法。如果它是service
样式类(将其视为代码的其他部分的REST api),则采用第二种方法。