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

时间:2019-04-16 10:38:38

标签: 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的干净运行功能。最Python的方式是什么?

3 个答案:

答案 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所说,对需要数据库连接的函数进行单元测试并不是那么容易。)由于无状态,它似乎更易于测试。无状态类并不是自动更容易测试,但更容易理解:对于一个输入,您总是得到相同的输出。

但这不是唯一的要点:使用DataManipulator2run中的动作顺序不会错,因为每个函数都会将一些数据传递给下一个,而下一个可以没有这些数据就不要继续。对于静态(强烈)类型的语言,这将更加明显,因为您甚至无法编译错误的run函数。

相反,DataManipulator不容易测试,有状态,并且不能确保动作的顺序。这就是方法DataManipulator.run如此干净的原因。这是太干净的事件,因为其实现隐藏了一些非常重要的东西:函数调用是有序的。

因此,我的回答:相对于DataManipulator2实现,更喜欢DataManipulator实现。

但是DataManipulator2完美吗?是的,没有。对于快速而肮脏的实现,这就是要走的路。但是,让我们尝试进一步。

您需要将功能run公开,但是load_datasave_datatransform没有理由公开(“公开”一词的意思是:未标记(带下划线的实施细节)。如果用下划线标记它们,则它们不再是合同的一部分,并且您不愿意对其进行测试。为什么?因为尽管可能存在测试失败,但实现可能会在不违反类合同的情况下进行更改。这是一个残酷的难题:您的类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文件中)
  • 代码是可测试的:每个方法都是公共的(在Python意义上),但API仍然很简单。

当然,这确实是(旧式)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 classesFrozen=False,采用第一种方法。如果它是service样式类(将其视为代码的其他部分的REST api),则采用第二种方法。