Python设计模式:使用类属性存储数据与局部函数变量

tvg*_*iek 9 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()
Run Code Online (Sandbox Code Playgroud)

现在,对于编写测试,我发现DataManipulator2更好,因为可以更轻松地独立测试功能。同时,我也喜欢DataManipulator的干净运行功能。什么是最pythonic的方式?

jfe*_*ard 8

与其他答案中所说的不同,我认为这不是个人品味的问题。

正如你所写的,DataManipulator2乍一看,似乎更容易测试。(但正如@AliFaizan 所说,对需要数据库连接的函数进行单元测试并不容易。)而且测试似乎更容易,因为它是无状态的。无状态类并不自动更容易测试,但更容易理解:对于一个输入,你总是得到相同的输出。

但这还不是唯一的一点:withDataManipulator2中的动作顺序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()
Run Code Online (Sandbox Code Playgroud)

这还不错,而且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))
Run Code Online (Sandbox Code Playgroud)

还有一点:文件名。尝试更喜欢文件类对象而不是文件名作为参数,因为您可以使用io模块测试您的代码:

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))
Run Code Online (Sandbox Code Playgroud)

使用模拟对象,现在可以很容易地测试类的行为。

我们总结一下这段代码的优点:

  • 确保动作的顺序(如DataManipulator2)
  • 该run方法应该是干净的(如DataManipulator2)
  • 代码是模块化的:您可以创建新的Transformer或新的DataSample(例如从数据库加载并保存到 csv 文件)
  • 代码是可测试的:每个方法都是公开的(在 Python 意义上),但 API 仍然很简单。

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


Art*_*cek 3

最Pythonic的方式是什么?

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 Code Online (Sandbox Code Playgroud)

在我的对象生命周期中的任何时候,它们都保存一致类型的初始化数据。run()这使得它们可测试、不易出错并且更可能在不同的实现中(不仅仅是)有用。

由于列出的所有好处,我认为值得在管道的每个结构化步骤(DataCleaner 等)编写一个类,因为随着代码的增长,它会更容易维护。