Python 设计模式 : using class attributes to store data vs. 局部函数变量

标签 python design-patterns

我经常发现自己遇到了同样的问题。一个常见的模式是我创建一个执行某些操作的类。例如。加载数据、转换/清理数据、保存数据。那么问题就出现了如何传递/保存中间数据。看看以下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_datatransform没有理由公开(“公开”我的意思是:没有用下划线标记为实现细节)。如果您用下划线标记它们,则它们不再是契约(Contract)的一部分,并且您对测试它们感到不舒服。为什么?因为尽管可能会出现测试失败,但实现可能会在不破坏类契约的情况下发生变化。这是一个残酷的困境:要么是你的类(class) 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 ,现在很容易测试类的行为。

    我们总结一下这段代码的优点:
  • 确保 Action 的顺序(如 DataManipulator2 )
  • run方法尽可能干净(如 DataManipulator2 )
  • 代码是模块化的:您可以创建一个新的 Transformer ,或新的 DataSample (例如从数据库加载并保存到 csv 文件)
  • 代码是可测试的:每个方法都是公开的(在 Python 意义上),但 API 仍然很简单。

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

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

    相关文章:

    python - 我能在python中找到哪些参数是 "pre-assigned"吗?

    java - 什么是 DataVO 模式?

    c# - 为现有类创建接口(interface)?

    python - 如何将json作为json而不是PUT方法推送到 Elasticsearch

    python - 将 pandas 间隔数据框减少为 12 个月 x 24 小时聚合值的表

    python - 如何使用 re 获取具有相似格式的多个变量?

    java - 理解单一职责原则

    wpf - 将使用 backgroundworker 的方法替换为 async/tpl (.NET 4.0)

    java - 使用 Java 构建插件系统的最佳方法

    python - 两个相同的 AutomationID 使用 xpath python 和 appium