【问题标题】:How to ensure functions are run in specific order?如何确保函数按特定顺序运行?
【发布时间】:2019-03-28 00:04:30
【问题描述】:

我对 python 设计模式比较陌生,但我想确保我的代码尽可能易于理解和灵活。这是一个例子:

data=query_data(sql)

transformed_data=transform_data(data, arg1, arg2)

train, test, validate = train_test_validate(transformed_data, frac_test, frac_validate)

model = fit_model(train,test, loss, learning_rate)

predictions, f1 = model.predict(validate)

每个函数都必须按此顺序运行。在这个特定的例子中,顺序应该非常明显。但在更复杂的模块中,可能存在分支路径,如果没有大量文档,可能会不清楚函数应该以哪个顺序运行。

只需将模块包装在另一个按顺序应用函数的函数中就足够简单了,但这似乎会产生一个不必要的复杂函数,其中包含许多违反单一职责的参数。

我还尝试将每个函数包装在一个类中,该类返回具有适当分支方法的序列中的下一个类。这样做的好处是代码的顺序隐含在模块的设计中,方法只能在适当的时候调用。这导致许多类,大多数只有一种没有分支的方法。我被警告说这是一种糟糕的设计模式。

是否有最好的方法来确保代码以特定的顺序运行并且该顺序在代码设计中是显而易见的?

【问题讨论】:

  • 顺序总是从上到下运行,任何函数调用都需要时间(除非你使用线程等)。
  • 上课似乎是最好的方法。创建一个类并在__init__ 函数中,为相关对象调用所有其他函数。然后在类中创建可能需要的其他函数。
  • 添加一个带有示例用法的模块级文档字符串?如果函数在不涉及全局状态的情况下正确解耦,那么如果它们无序调用它们应该不是问题,只要它们对每个阶段都有有效的输入(无论它们是派生的)。
  • @Recessive 所以在这种情况下,我会有一个类模型,其中包含函数 query_data、transform_data、train_test_validate、fit_model,然后可能会进行预测。如果我在 init 方法中调用所有这些,那不会给我留下一个有效的函数调用,其中包含大量参数和可能的许多输出?否则,将它们全部分组在一个类中似乎是有道理的,但似乎并不意味着对方法进行排序。感谢您的帮助!
  • @ShadowRanger 我认为这是一个好主意,除了暗示代码结构中的顺序。我只是担心在一个令人困惑的过程中很难追踪你是否有一个有效的输入。对于一个更具体的例子,我正在考虑一个特定的包,在一个类中你可以创建一个密集或稀疏矩阵。稀疏矩阵是一种模型类型的无效输入,但对另一种模型类型有效(我找不到记录并浪费了一个小时弄清楚的事实)。我想确保我的代码的用户在结构上意识到这个路径没有意义。

标签: python function class design-patterns


【解决方案1】:

您已经在使用通用方法来处理函数调用顺序:将一个令牌从一个函数传递给另一个:

token1 = func1(token0)
token2 = func2(token1)
token3 = func2(token2)
...

通常,标记也是有用的结果。在你的例子中:如果你没有data 给它,你不会打电话给transform_datadatatoken0。因此,transform_data 将在 query_data 之后被调用。

但我认为你处理的真正问题是不同的:你担心用户给下一个函数提供了错误的输入,并且该函数可能会接受它并返回错误的结果:

我只是担心在一个混乱的过程中很难追踪你是否有一个有效的输入......稀疏矩阵是一种模型类型的无效输入,但对另一种模型类型有效 (cmets)

在静态类型的语言中,它(通常)不会发生,因为如果你在预期一只猫的时候给一只兔子,程序将无法编译。在 python 中,用鸭子打字,这并不容易。让我们看看如何执行此操作:

  • 一个很好的文档
  • 检查输入数据
  • 对函数进行分组
  • 锁定一些参数

良好的文档

这是迄今为止最好的解决方案。在这里,每个人都是成年人。不要太担心。

您可以使用 Python typing abilities 获取人类信息。

检查输入数据

这是Defensive programming 的变体。如果需要,包装你不拥有的函数:

def wrap_transform_data(data, arg1, arg2):
    if not valid(data): # find a way to check this
        raise Exception("data is not valid")
    return transform_data(data, arg1, arg2)

使用异常并避免 try... except 阻塞,因为快速失败在这里更好。

有时这还不够。想象一下你有一个方阵:你如何检查它是否在下一个函数调用之前被转置?

对功能进行分组

只需将模块包装在另一个按顺序应用函数的函数中就足够简单了,但这似乎会产生一个不必要的复杂函数,其中包含许多违反单一职责的参数。

你会得到类似的东西:

def full_process(sql, arg1, arg2, frac_test, frac_validate, loss, learning_rate):
    data=query_data(sql)
    transformed_data=transform_data(data, arg1, arg2)
    train, test, validate = train_test_validate(transformed_data, frac_test, frac_validate)
    model = fit_model(train,test, loss, learning_rate)
    predictions, f1 = model.predict(validate)
    return ...

正如您所说,full_process 方法有很多混合参数。在 Python 中处理这个问题的常用方法是使用默认值:

def full_process(sql, arg1=1, arg2=2, frac_test=0.7, frac_validate=0.5, loss=0.2, learning_rate=0.1):
    data=query_data(sql)
    transformed_data=transform_data(data, arg1, arg2)
    train, test, validate = train_test_validate(transformed_data, frac_test, frac_validate)
    model = fit_model(train,test, loss, learning_rate)
    predictions, f1 = model.predict(validate)
    return ...

它变得可读,除非你每次都有不同的参数:

full_process(sql, frac_validate=0.9)

如果transpose 函数是其中之一,这将解决上面的转置矩阵示例。

注意:只对没有分支的部分进行分组。不要写这样的东西:

def full_process(sql, , case1, case2, case3, arg1=1, arg2=2, frac_test=0.7, frac_validate=0.5, loss=0.2, learning_rate=0.1):
    data=query_data(sql)
    if case1:
        transformed_data=transform_data(data, arg1, arg2)
        train, test, validate = train_test_validate(transformed_data, frac_test, frac_validate)
        if case2:
            ...
        else:
            ...
    else:
        transformed_data=transform_data2(data, arg1, arg2)
        train, test, validate = train_test_validate(transformed_data, frac_test, frac_validate)
        if case2:
            ...
        else:
            ...

    return ...

很难阅读和维护,这可能导致组合爆炸!

锁定部分参数

我将此添加为记录,但我认为这不是一个好的做法,除非在某些特定情况下。这类似于您对类的尝试,但使用了函数。

我关注这两行:

data=get_matrix()
transformed_data=transform_data(data, arg1, arg2)

如果要保证用户调用transform_data,返回值为query_data,可以返回一个函数:

def wrapped_query_data(sql):
    def ret(arg1, arg2) # you might use functools.partial 
        transform_data(data, arg1, arg2)
    return ret

现在的代码是:

data_transformer = wrapped_query_data(sql)
train_test_validate = data_transformer(arg1, arg2) # no way the user can twist data here

显然,如果您尝试概括这一点,您将需要一个包装器,并限制分支的可能性。

结论

是否有最好的方法来确保代码以特定的顺序运行并且该顺序在代码设计中是显而易见的?

请记住,Python 培养每个程序员都负责任。仅在需要时使用强制执行函数调用顺序的方法。

【讨论】:

  • 哇,这比我想象的要全面得多。谢谢,这正是我所需要的。
猜你喜欢
  • 2017-07-08
  • 1970-01-01
  • 2015-12-18
  • 1970-01-01
  • 1970-01-01
  • 2019-07-30
  • 2021-05-07
  • 2014-07-13
  • 2018-08-21
相关资源
最近更新 更多