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,而不是返回已修改的列表。 map 和 filter 这样的函数不是列表方法。另一方面,许多 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())