如何以符合人体工程学的方式迭代具有不同签名的函数

How to ergonomically iterate over functions with different signatures

我有一个包含基因的 "genotype"。这些基因代表什么并不重要,它们只是任意对象,都可以引用为 "gene objects"。

我需要通过几种方法来突变这个基因,但是并不是所有的函数签名都匹配。给定一个起始基因,一个新基因被创建,随机机会 select 这些方法之一(或没有方法)进行突变。

例如,我有 duplicate(gene)replace(gene, othergene)insert(gene, othergene)delete(gene)othermutation(gene, genotype)。所有这些 return 一个基因列表(即使该列表只包含一个元素,或零个元素)以保持函数签名之间的同质性。

我想将情况概括为这些变异函数的列表以及相关的使用概率。我已经有了通过二进制搜索和累积分布 selecting 这些基因的方法,我生成了一个 R 并可以根据 R 的四舍五入二进制索引检索正确的函数。这大致允许我执行以下操作:

def mutate(genotype, mutation_list, cumulative_probabilities)
    mutated_genotype = []
    for gene in genotype:
        r = random.random()
        mutation = mutation_list(cumulative_probabilities(r))
        mutated_genotype.extend(mutation(gene))
    return mutated_genotype

理想情况下,我不需要知道第五行的突变,我只需要在某处有一个突变列表和相关概率。如您所见,需要第二个参数的 replace(gene, othergene) 会发生什么情况?或者 othermutation(gene, genotype) 需要一个不同但又单独的参数?

为了解决这个问题,我想出了几个办法。首先,我可以将所有函数签名同化为完全相同。我的意思是即使 duplicate(gene) 不需要 othergene 我仍然会把它放在函数定义中,它只是不会使用它或者它会用它做一些微不足道的事情。但是这个解决方案的缺点是,每次我需要添加一个带有新参数的新函数时,我都需要更改所有函数签名,这在某种程度上违反了所有函数的 SRP,并且会是烦人的处理。但我可以将它包装为一个参数对象,我在每次迭代中设置所需的参数。如果需要一个带有新参数类型的新函数,我可以简单地将该参数添加到 "mutation parameter" 对象,并将该对象传递给每个函数,并且每个函数只会得到它需要的东西,例如:

for gene in genotype:
    #note other gene is created from some other function or is a generator itself
    mutation_parameter = MutationParameter(gene, genotype, othergene)
    r = random.random()
    mutation = mutation_list(cumulative_probabilities(r))
    mutated_genotype.extend(mutation(mutation_parameter))

这里的坚持者是 MutationParameter 的成员不一定彼此相关,我们必须事先知道突变特征是什么,你将无法做到添加新签名,需要更新多段代码。

另一种我可以处理这个问题的方法是我可以使函数参数通用,但这样我将被迫添加一个额外的数据结构来处理将数据拉入函数签名(以便所有函数都采用*args**kwargs),这可能意味着为每个签名定制函数并降低性能,因为需要线性化或将累积概率与 hashtable/dictionary 相关联。

我可以通过制作仿函数来处理这些函数,仿函数在调用 'function' 本身中存储一些参数数据(例如 "other gene" 就像一个随机生成基因的生成器) .这将需要我为每个函数创建一个新的 class,以便为每个需要唯一参数的函数处理这种情况。即使那样,我也无法将当前的基因列表 genotype 放入 othermutation 中,而无需在函数本身执行期间创建调用时的函数列表(因此并非所有仿函数无法作为 mutate.

中的 mutation_list 传入

如果我想有一个很好的方法来处理通用 mutation_list 并且我可以通过在两种类型的突变列表中。

我还可以通过使 othergene 参数成为所有实例化都具有的仿函数的静态 function/variable 来做类似的事情。

所以总结三种方法:

  1. 通过参数对象传递所有参数,函数选择他们需要的参数;

  2. 使用 *args 可以有效地做同样的事情,但是您还需要为每个签名关联一个函数来提取数据;

  3. 或使用仿函数(带或不带静态变量)与另一个单独传入的变异列表一起保存数据,用于在 mutation 之前无法确定额外参数的变异函数被称为。

我想避免让我的变异函数关心潜在的变异是什么,这样它就可以尽可能通用。

一个合理的策略是用另一个提供但不使用另一个参数的函数包装一个参数的函数:

def dup2(gene, othergene):
    return duplicate(gene)

def del2(gene, othergene):
    return delete(gene)

这将使两个较短的签名匹配其他函数:replace(gene, othergene), insert(gene, othergene), othermutation(gene, genotype)

另一种策略是使用try/except来测试可能性:

try:
    return func(gene, othergene)     # try two-arguments
except TypeError:
    return func(gene)                # try one-argument

我看到的问题是:你怎么知道为每个函数调用提供什么参数,因为它们没有相同的签名?我认为有一个很好的方法可以避免提供不需要的参数,并以易于理解的方式构建程序:使用 classes 而不是简单的函数。 类 提供了一种方法来检查工作函数需要多少参数和什么类型的参数。您可以将此代码分解为基数 class。例如:

class MutatorBase:
   def __init__(self, needs_othergene=False, needs_genotype=False):
        self.needs_othergene = needs_othergene
        self.needs_genotype = needs_genotype

   def f(self, gene, othergene=None, genotype=None):
        """This is the function that does the actual calculations"""
        raise NotImplementedError

现在你可以创建一堆派生的 classes,你可以在其中实现实际的功能 f。在构造函数中,您设置变量 needs_othergeneneeds_genotype。示例:

class Duplicator(MutatorBase):
    def __init__(self):
        super().__init__()

    def f(self, gene):
        return duplicate(gene)

class Replacer(MutatorBase):
    def __init__(self):
        super().__init__(needs_othergene=True)

    def f(self, gene, othergene=g):
        return replace(gene, g)

在你调用其中一个之前,假设它被命名为 replacer.f,你检查变量以查看需要什么样的参数。您适当地设置了函数调用。您不是将概率与一组函数相关联,而是将概率与一组 classes 相关联。 Python 它们同样简单。

我将回答我自己的问题,因为我发现当前的答案并不令人满意(并且几乎与我建议的答案完全相同),但我发现此解决方案最 "ergonomic"我的具体情况(最通用等,结构良好且可扩展)。

我的第一个建议,

1: pass all parameters in via parameter object, functions pick and choose which parameters they need

我发现它对我来说是最好的,甚至可能在 Python 之外并进入非动态语言。这基本上是这样的:

class MutationParameters:
    def __init__(self, param1, param2, param3 ...):
        self.param1 = param1
        self.param2 = param2
        self.param3 = param3
        ...

def mutate(genes, mutator_generator, otherparams...):
    # paraminit...
    new_genes = []
    mutation_params = MutationParameters(param1, param2, param3...)
    for gene in genes:
        # runtime param init ...
        mutation_params.paramn = paramn #runtime param
        new_genes = next(mutator_generator)(mutation_params)
        new_genes.extend(new_genes)
    return StackGenotype(new_genes)

是否需要新参数?更新 MutationParameters,更新 mutate 一次,就是这样,不需要更改所有其他函数的参数(因此为什么使用参数并忽略一些不是一个好主意,类似于仿函数)。添加新功能?只需要一个变量,然后在其中寻找您需要的参数。这也适用于 C++。

我相信,除非有人发布更好的答案,否则这是解决我的问题的最佳方案,我会把它标记为答案,直到出现更好的答案。