【问题标题】:Class initialization in exposed method using *args and **kwargs vs. using make function使用 *args 和 **kwargs 与使用 make 函数的公开方法中的类初始化
【发布时间】:2018-03-28 11:36:33
【问题描述】:

我正在研究一种方法,该方法通过查看一个或多个文件的某些内容来收集三个数量(称为ABC)的(统计)信息。整个事情或多或少等同于以下代码:

def get_statistics(self, file, some_option = True):
    A_list = list()
    B_list = list()
    C_list = list()
    with open(file, 'r') as f:
        for line in f:
            A, B, C = self._analyze(line, some_option)
            A_list.append(A)
            B_list.append(B)
            C_list.append(C)
    return A_list, B_list, C_list

这很容易理解并且工作正常(除了你可能想指出重复的appending 效率不高,但这不是问题的重点)但有产生 huge 列出了A_listB_listC_list。事实上,当循环多个文件时,列表可能会变得如此之大,以至于我需要担心它们是否适合内存而不会引起问题。

通常,这种方法的用户不会对完整的数据集感兴趣,但只需要一些统计信息。也许用户只想要数量的均值和方差,或者有兴趣绘制直方图以可视化数据。好吧,很简单,让我们修改方法的签名并允许提供自定义类来收集感兴趣的数据。我只会通过调用它们的append() 方法与这些类进行交互,用户可以决定该方法的作用。例如,它可以将所有传入的值相加并计算它们的数量以在需要时计算平均值。我修改后的方法可能如下所示:

def get_statistics(self, file, some_option = True, A_cls = list, B_cls = list, C_cls = list):
    A_store = A_cls()
    B_store = B_cls()
    C_store = C_cls()
    ...
    return A_store, B_store, C_store

这种方法让用户可以灵活地决定应该存储多少潜在的巨大数据集。如果A_clsB_clsC_cls的参数被省略,我的第一个方法的功能就恢复了。现在考虑想要绘制直方图的用户的示例,将需要将初始化参数传递给这些类,例如 bin 大小或 bin 数量。好的,让我们添加一种将初始参数传递给这些类的方法:

def get_statistics(self, file, some_option = True, A_cls = list, A_args = [], A_kwargs = {}, B_cls = list, B_args = [], B_kwargs = {}, C_cls = list, C_args = [], C_kwargs = {}):
    A_store = A_cls(*A_args, **A_kwargs)
    B_store = B_cls(*B_args, **B_kwargs)
    C_store = C_cls(*C_args, **C_kwargs)
    ...
    return A_store, B_store, C_store

哇,突然间,我用 11 个参数加上 self 使我最初易于阅读的函数变得臃肿不堪!诚然,它们中的大多数都有合理的默认值,但这对于编码和文档来说仍然是一场噩梦。

那么我怎样才能让我的界面保持整洁呢?一种选择是省略传递初始化参数的可能性,并且只有五个参数,我认为这是可以接受的。然后,用户需要使用闭包来恢复此功能:

def make_A_helper(A_cls, *args, **kwargs):
    def make_A():
        return A_cls(*args, **kwargs)
    return make_A
A_helper = make_A_helper(A_cls, *some_args, **some_other_kwargs)

A_helper 传递给我的方法然后会潜入some_argssome_other_kwargs,这是用户可能已经定义的,但是每次编码都显得麻烦且烦人。我觉得用户自然会问,为什么他不能直接将some_argssome_other_kwargs 传递给我的方法。那么处理这种情况的好方法是什么?

TL; DR: Python 提供了一个机会,让用户通过将类作为参数传递给函数来影响一段代码的行为,并将这些类的内部视为一个黑盒子。在不传递大量 *args**kwargs 的情况下,使用此功能实现功能的最佳方法是什么?让用户编写 make 函数作为类生成器是否可以接受?有没有更好的方法,比如提供某种register() 方法来让应该使用它们的代码知道这些类?

【问题讨论】:

  • 是什么阻止您使用三个简单的阅读器函数从文件中提取 A、B 和 C?您的代码中几乎没有类的情况,因为没有要存储的状态,真的。
  • 好点是我故意遗漏了所有这些细节。拆分 A、B 和 C 的提取会导致计算时间增加 3 倍,因为当您计算另一个时,其中两个是免费的。此外,self._analyze() 确实 大量使用内部状态。更改类的属性基本上会改变您查看数据的方式。我明白你的意思,但我强烈支持拥有一个类(因为存在内部状态)和拥有一个方法(因为 A、B 和 C 是强耦合的)。
  • 希望这不是一种读取 csv 文件的方式。 ) 您的设计可能是一个读取器类/函数,它将行流作为输入(它比读取内部文件更干净)并产生 A、B 和 C 以供进一步处理/累积。通过这种方式,我看到 self._analyze() 变为 Reader(incoming_stream).iterate()yield_parsed_values(incoming_stream) 取决于使用了多少内部状态。这限制了解析例程的责任并且更容易测试。您可以稍后将其集成到更大的类中。
  • 不是 CSV,我可以向你保证。 :-) 实际上数据以某种二进制格式存储,其中的大块放入numpy 数组并处理以获得A、B 和C。据我所知,本质上是Reader(incoming_stream).iterate() 方法。

标签: python interface


【解决方案1】:

这里最简单的答案是让用户传递对象而不是类,即:

class Something():
    def get_statistics(self, file, some_option=True, acollector=None, bcollector=None, ccollector=None):
        if acollector is None:
            acollector = list()
        if bcollector is None:
            bcollector = list()
        if ccollector is None:
            ccollector = list()

        with open(file, 'r') as f:
            for line in f:
                A, B, C = self._analyze(line, some_option)
                acollector.append(A)
                bcollector.append(B)
                ccollector.append(C)
        return acollector, bcollector, ccollector

另一种解决方案是提供一个迭代器,让用户使用A, B, C 做任何事情

def iterfile(self, file, some_option=True):
    with open(file, 'r') as f:
        for line in f:
            yield self._analyze(line, some_option)

然后用户可以自己收集信息:

something = Something()
for a, b, c in something.iterfile("a/file.ext"):
    do_something_with(a, b, c)

这里的好处是内存使用不再是你的问题,而是用户的xD

FWIW 您可以通过基于iterfile() 重新实现get_statistics() 来组合两者,从而保持兼容性:

    def get_statistics(self, file, some_option=True, acollector=None, bcollector=None, ccollector=None):
        if acollector is None:
            acollector = list()
        if bcollector is None:
            bcollector = list()
        if ccollector is None:
            ccollector = list()
        for A, B, C in self.iterfile(file, some_option):        
            acollector.append(A)
            bcollector.append(B)
            ccollector.append(C)
        return acollector, bcollector, ccollector

【讨论】:

  • 在某种意义上,传递一个对象而不是一个类与传递一个make函数是一样的:用户需要在调用我的方法之前指定所有*args**kwargs。但我同意传递一个实例会产生更简洁的代码。迭代器方法当然很好,但对我来说有一个缺点,即每秒有很多次迭代,所以我会强制用户让他们的消费者快速而不是显着减慢执行速度。
  • 迭代器可以通过允许用户 1/ 避免 python for 循环(无法优化)并使用任何更高级别和更优化的构造来加快执行速度(map, filter,列表理解等),2/通过忽略他们不关心的值来进一步优化,3/通过不一定存储所有值等来优化内存使用......实际上你的 for 循环/追加解决方案很远从最佳的 wrt/ 空间和时间。
  • 您的观察是正确的,但正如我的问题和 cmets 中提到的,在实际代码中没有这样的循环,而是一个 numpy 数组和一些数字运算。我不认为列表推导比 C 优化的 numpy 向量操作更快,但我可能在这一点上弄错了。关于您提到的第 1 点和第 2 点,您当然是对的。
  • 当用户不需要完整数据集时,第 3 点仍然有效。而且我只能评论你实际发布的代码——我们不知道你在哪里使用 numpy 数组,也不知道是为了什么。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-03-26
  • 1970-01-01
  • 1970-01-01
  • 2020-12-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多