草庐IT

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

coder 2023-08-19 原文

我经常发现自己遇到了同样的问题。一个常见的模式是我创建一个执行某些操作的类。例如。加载数据、转换/清理数据、保存数据。那么问题就出现了如何传递/保存中间数据。看看以下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 设计模式 : using class attributes to store data vs. 局部函数变量的更多相关文章

    1. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

      关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

    2. ruby-on-rails - Rails - 子类化模型的设计模式是什么? - 2

      我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co

    3. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

      我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

    4. ruby - 如何在续集中重新加载表模式? - 2

      鉴于我有以下迁移:Sequel.migrationdoupdoalter_table:usersdoadd_column:is_admin,:default=>falseend#SequelrunsaDESCRIBEtablestatement,whenthemodelisloaded.#Atthispoint,itdoesnotknowthatusershaveais_adminflag.#Soitfails.@user=User.find(:email=>"admin@fancy-startup.example")@user.is_admin=true@user.save!ende

    5. ruby-on-rails - 如何使用 instance_variable_set 正确设置实例变量? - 2

      我正在查看instance_variable_set的文档并看到给出的示例代码是这样做的:obj.instance_variable_set(:@instnc_var,"valuefortheinstancevariable")然后允许您在类的任何实例方法中以@instnc_var的形式访问该变量。我想知道为什么在@instnc_var之前需要一个冒号:。冒号有什么作用? 最佳答案 我的第一直觉是告诉你不要使用instance_variable_set除非你真的知道你用它做什么。它本质上是一种元编程工具或绕过实例变量可见性的黑客攻击

    6. ruby - 通过 ruby​​ 进程共享变量 - 2

      我正在编写一个gem,我必须在其中fork两个启动两个webrick服务器的进程。我想通过基类的类方法启动这个服务器,因为应该只有这两个服务器在运行,而不是多个。在运行时,我想调用这两个服务器上的一些方法来更改变量。我的问题是,我无法通过基类的类方法访问fork的实例变量。此外,我不能在我的基类中使用线程,因为在幕后我正在使用另一个不是线程安全的库。所以我必须将每个服务器派生到它自己的进程。我用类变量试过了,比如@@server。但是当我试图通过基类访问这个变量时,它是nil。我读到在Ruby中不可能在分支之间共享类变量,对吗?那么,还有其他解决办法吗?我考虑过使用单例,但我不确定这是

    7. ruby - 在没有 sass 引擎的情况下使用 sass 颜色函数 - 2

      我想在一个没有Sass引擎的类中使用Sass颜色函数。我已经在项目中使用了sassgem,所以我认为搭载会像以下一样简单:classRectangleincludeSass::Script::FunctionsdefcolorSass::Script::Color.new([0x82,0x39,0x06])enddefrender#hamlengineexecutedwithcontextofself#sothatwithintemlateicouldcall#%stop{offset:'0%',stop:{color:lighten(color)}}endend更新:参见上面的#re

    8. ruby-on-rails - 使用 rails 4 设计而不更新用户 - 2

      我将应用程序升级到Rails4,一切正常。我可以登录并转到我的编辑页面。也更新了观点。使用标准View时,用户会更新。但是当我添加例如字段:name时,它​​不会在表单中更新。使用devise3.1.1和gem'protected_attributes'我需要在设备或数据库上运行某种更新命令吗?我也搜索过这个地方,找到了许多不同的解决方案,但没有一个会更新我的用户字段。我没有添加任何自定义字段。 最佳答案 如果您想允许额外的参数,您可以在ApplicationController中使用beforefilter,因为Rails4将参数

    9. ruby-on-rails - 如何在我的 Rails 应用程序 View 中打印 ruby​​ 变量的内容? - 2

      我是一个Rails初学者,但我想从我的RailsView(html.haml文件)中查看Ruby变量的内容。我试图在ruby​​中打印出变量(认为它会在终端中出现),但没有得到任何结果。有什么建议吗?我知道Rails调试器,但更喜欢使用inspect来打印我的变量。 最佳答案 您可以在View中使用puts方法将信息输出到服务器控制台。您应该能够在View中的任何位置使用Haml执行以下操作:-puts@my_variable.inspect 关于ruby-on-rails-如何在我的R

    10. ruby-on-rails - 在 ruby​​ 中使用 gsub 函数替换单词 - 2

      我正在尝试用ruby​​中的gsub函数替换字符串中的某些单词,但有时效果很好,在某些情况下会出现此错误?这种格式有什么问题吗NoMethodError(undefinedmethod`gsub!'fornil:NilClass):模型.rbclassTest"replacethisID1",WAY=>"replacethisID2andID3",DELTA=>"replacethisID4"}end另一个模型.rbclassCheck 最佳答案 啊,我找到了!gsub!是一个非常奇怪的方法。首先,它替换了字符串,所以它实际上修改了

    随机推荐