如何使用默认的函数参数来避免创建一个类(附代码)

54 阅读3分钟

最近我在写一些Python代码,以打印出Prometheus的指标,说明我们是否能登录到IMAP服务器。作为一个端到端的测试,这可能会因为各种各样的原因而失败;我们可能无法连接到IMAP服务器,在TLS会话协商中遇到TLS错误,服务器的TLS证书无法验证,可能存在IMAP协议问题,或者服务器可能拒绝我们的登录尝试。如果我们失败了,我们想知道为什么,以便进行诊断(特别是,在这个测试中,某些类型的失败比其他的更重要)。在Prometheus的世界里,这通常是通过为每一个可能失败的不同事情发出一个单独的指标来实现的。

在我的代码中,这些指标都是由一个单一的函数准备的,在不同的地方被调用。它看起来像这样:

def logingauges(host, ok, ...):
  [...]

def logincheck(host, user, pw):
  try:
    c = ssl.create_default_context()
    m = imaplib.IMAP4_SSL(host=host, ssl_context=c)
  except ssl.CertificateError:
    return logingauges(host, 0, ...)
  except [...]
  [...]

  try:
    r = m.login(user, pw)
    [...]
  except imaplib.IMAP4.error:
    return logingauges(host, 0, ...)
  except [...]

  # success, finally.
  return logingauges(host, 1, ...)

当我刚开始写这段代码的时候,我只区分了几个不同的失败原因,所以我把这些原因的状态直接作为额外的参数传给了logingauges() 。随着失败原因的增加,这变得既不方便又令人讨厌,部分原因是增加一个新的失败原因需要通过所有现有的调用logingauges() ,给每个人增加一个新参数。

所以我放弃了。我把所有的失败原因变成了默认为0的关键字参数:

def logingauges(host, ok,
                connerr=0, loginerr=0, certerr=0,
                sslerr=0, imaperr=0):
  [...]

现在要在失败时调用logingauges() ,我只需要为特定的失败提供一个参数:

  return logingauges(host, 0, sslerr=1)

添加一个新的失败原因变得更加本地化;我只需要在logingauges() ,添加一个新的测量指标,加上一个新的关键字参数,然后从正确的地方调用它。

这让我觉得是个黑客。正确的方法可能是创建一个类,将所有这些状态信息作为实例的属性来保存,在logincheck() 的开头创建一个实例,适当地操作这些属性,完成后返回这个实例。这个类甚至可以有一个to_gauges() 函数,从它的当前值生成所有的实际度量。

(在Python 3.7中,我会使用一个数据类,但这必须在Ubuntu 18.04和Python 3.6.7上运行,所以它必须是一个无聊的老类。)

然而,我不仅已经有了使用默认函数参数的版本,而且基于类的版本还需要为一个小程序中基本上是简单的情况增加一堆代码和官僚主义。我喜欢用正确的方式做事,但我不确定我是否喜欢这样。就目前而言,默认函数参数的方法是令人愉快的最小化和低开销。

(或者说,在现在的 Python 中,这被认为是对缺省函数参数的适当使用。带有缺省值的参数经常被用来为实例属性设置缺省初始值,这也是我在这里做的。基于类的方法的一个版本实际上看起来是一样的;我不是调用一个函数,而是返回一个刚刚创建的IMAPStatus类的实例。)

(这只是有点类似于使用默认的函数参数来将几个API合并在一起。 在这里,如果说有多个API,每个故障原因都有一个,那就太夸张了)。