我经常发现自己遇到了同样的问题。一个常见的模式是我创建一个执行某些操作的类。例如。加载数据,转换/清除数据,保存数据。然后出现了问题,即如何传递/保存中间数据。查看以下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_datasave_datatransform没有理由公开(通过“public”我的意思是:未标记为实现细节,但带有下划线)。如果用下划线标记它们,它们将不再是契约(Contract)的一部分,并且您不愿意对其进行测试。为什么?因为尽管可能存在测试失败,但实现可能会在不违反类约定的情况下进行更改。这是一个残酷的难题:您的类DataManipulator2具有正确的API还是无法完全测试。

不过,这些功能应该是可测试的,但应作为另一类API的一部分。考虑一下三层架构:

  • load_datasave_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文件中)
  • 代码是可测试的:每个方法都是公共(public)的(在Python意义上),但API仍然很简单。

  • 当然,这确实是(旧样式)Java风格的。在python中,您可以简单地传递函数transform而不是Transformer类的实例。但是,一旦您的transform开始变得很复杂,则一个类便是一个很好的解决方案。

    关于Python设计模式: using class attributes to store data vs.局部函数变量,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/55706215/

    10-12 18:47