Ruby 的 `tap` 习惯用法在 Python 中的实现

52 阅读3分钟

Ruby 中有一个非常实用的 tap 习惯用法,它允许你创建一个对象,对它进行一些操作,然后返回它。下面是一个例子,其中我们使用了一个列表,但我的实际代码会更复杂一些:

def foo
  [].tap do |a|
    b = 1 + 2
    # ... and some more processing, maybe some logging, etc.
    a << b
  end
end

>> foo
=> [1]

在 Rails 中有一个类似的方法叫做 returning,所以你可以这样写:

def foo
  returning([]) do |a|
    b = 1 + 2
    # ... and some more processing, maybe some logging, etc.
    a << b
  end
end

无论你对对象进行了多少处理,都很明显它是函数的返回值。

在 Python 中,我必须这样写:

def foo():
  a = []
  b = 1 + 2
  # ... and some more processing, maybe some logging, etc.
  a.append(b)
  return a

我想知道有没有办法将这个 Ruby 习惯用法移植到 Python 中。我的第一个想法是使用 with 语句,但 return with 不是一个有效的语法。

解决方案

方法 1

简短的回答是:Ruby 鼓励方法链式调用,而 Python 不鼓励。 我猜正确的问题应该是:Ruby 的 tap 有什么用? 我我对 Ruby 了解得不多,但通过谷歌搜索,我印象很深刻,tap 在概念上可以用作方法链式调用。 在 Ruby 中,SomeObject.doThis().doThat().andAnotherThing() 这种风格是相当惯用的。例如,它奠定了 fluent 接口的基础。Ruby 的 tap 是这种情况的一个特例,在这里你可以动态地定义 doThis()。 我为什么要解释这一切?因为这告诉我们为什么 tap 在 Python 中没有很好的支持。Python 一般不使用调用链式调用,当然也有一些例外。 例如,Python 列表方法通常返回 None,而不是返回已修改的列表。 mapfilter 这样的函数不是列表方法。另一方面,许多 Ruby 数组方法确实会返回已修改的数组。 除了某些情况(如一些 ORM),Python 代码不使用 fluent 接口。 最终,这是惯用 Ruby 和惯用 Python 之间的区别。如果你从一种语言转向另一种语言,你需要进行调整。

方法 2

你可以在 Python 中按如下方式实现它:

def tap(x, f):
    f(x)
    return x

Usage:
>>> tap([], lambda x: x.append(1))
[1]

然而,它在 Python 2.x 中不会有太多用处,因为它在 Ruby 中有用,因为 Python 中的 lambda 函数非常有限。例如,你不能内联调用 print,因为它是一个关键字,所以你不能用它来内联调试代码。你可以在 Python 3.x 中做到这一点,尽管它不如 Ruby 语法那么简洁。

>>> tap(2, lambda x: print(x)) + 3
2
5

方法 3

如果你非常想要这个,你可以创建一个上下文管理器:

class Tap(object):
    def __enter__(self, obj):
        return obj

    def __exit__(*args):
        pass

你可以像这样使用它:

def foo():
    with Tap([]) as a:
        a.append(1)
        return a

没有办法绕过 return 语句,而且 with 实际上并没有做任何事情。但是你确实在开头就有 Tap,它可以让你了解函数的内容。它比使用 lambda 要好,因为你不用局限于表达式,并且可以在 with 语句中包含几乎任何你想要的内容。 总体而言,我想说,如果你真的想要 tap,那就坚持使用 Ruby,而如果你需要用 Python 编程,那就用 Python 来写 Python,而不是 Ruby。当我有时间学习 Ruby 时,我打算写 Ruby 代码。

方法 4

我有了一个想法,可以使用函数装饰器来实现,但是由于 Python 中表达式和语句的区别,这最终仍然需要在最后 return。 在我的经验中,Ruby 语法很少使用,而且远不如 Python 的显式方法可读。如果 Python 具有隐式返回或将多个语句包装成单个表达式的某种方法,那么就可以做到这一点 - 但它既没有这些东西也故意没有。 这里是我的 - 有点没用的 - 装饰器方法,仅供参考:

class Tapper(object):
    def __init__(self, initial):
        self.initial = initial
    def __call__(self, func):
        func(self.initial)
        return self.initial

def tap(initial):
    return Tapper(initial)

if __name__ == "__main__":
    def tapping_example():
        @tap([])
        def tapping(t):
            t.append(1)
            t.append(2)
        return tapping

    print repr(tapping_example())