返回介绍

建议64:利用操作符重载实现中缀语法

发布于 2024-01-30 22:19:09 字数 3543 浏览 0 评论 0 收藏 0

可能你跟我一样,学完各种对象协议后就跃跃欲试。当年我初学Python的时候,原本最熟悉的编程语言是C++,所以学会操作符重载以后,就拿来炫技了。

>>> class endl(object):pass
... 
>>> class Cout(object):
...  def __lshift__(self, obj):
...      if obj is endl:
...          print
...          return
...      print obj,
...      return self
... 
>>> cout = Cout()
>>> cout << 1 << 2 << endl
1 2

如果你像我当年那样初学Python,你就会明白这种模拟C++的流输出让我感觉有多炫!但是现在我知道这是一种对特性的滥用,不应提倡。不过我在这里重提旧事的原因是想要引出一个非常棒的利用操作符重载实现更优雅的代码的例子。

熟悉Shell脚本编程朋友应该都非常熟悉“|”这个符号,它表示管道,用以连接两个应用程序的输入输出。比如按字母表反序遍历当前目录的文件与子目录,可以如下使用:

$ ls | sort -r
test
releases
prj
intern
doc
common
branches
art

管道的处理非常清晰,因为它是中缀语法。而我们常用的Python是前缀语法的,比如类似的Python代码应该是sort(ls(),reverse=True),明显没有那么清晰,特别是在极限情况下。

sum(select(where(take_while(fib(), lambda x: x < 1000000) lambda x: x % 2), lambda x: x * x))

像这样的代码,一堆sum、select、where混在一起,一眼看过去已经头大如斗了。管道符号在Python中,也是或符号,那么有没有可能利用它来简化代码呢?这个想法后来由Julien Palard开发了一个pipe库,达成了所愿。这个pipe库的核心代码只有几行,就是重载了__ror__()方法。

class Pipe:
  def __init__(self, function):
     self.function = function
  def __ror__(self, other):
     return self.function(other)
  def __call__(self, *args, **kwargs):
     return Pipe(lambda x: self.function(x, *args, **kwargs))

这个Pipe类可以当成函数的decorator来使用。比如在列表中筛选数据,可使用如下实现:

@Pipe
def where(iterable, predicate):
  return (x for x in iterable if (predicate(x)))

pipe库内置了一堆这样的处理函数,上文所述的sum、select、where等函数尽在其中,所以马上就可以拿来改造之前的代码。

fib() | take_while(lambda x: x < 1000000) \
   | where(lambda x: x % 2) \
   | select(lambda x: x * x) \
   | sum()

看,现在是不是一眼就可以看出代码的意义了呢?就是找出小于1000000的斐波那契数,并计算其中的偶数的平方之和。通过这个例子,可以看出中缀语法在这种流式数据处理上的确是非常有优势的。

pipe已经发布到pypi,可以使用pip install pip命令安装。安装完成以后可以在Python Shell中尝试一下。

>>> from pipe import *  
>>> [1, 2, 3, 4, 5] | where(lambda x: x % 2) | tail(2) | select(lambda x: x * x) 
  | add  
34  

此外,pipe是惰性求值的,所以我们完全可以弄一个无穷生成器而不用担心内存被用完。比如:

>>> def fib():
...   a, b = 0, 1
...   while True:
...    yield a
...    a, b = b, a + b

然后来做一个题目:计算小于4?000?000的斐波那契数中的偶数之和。

>>> euler2 = fib() | where(lambda x: x % 2 == 0) | take_while(lambda x: x < 4000000) | add
>>> assert euler2 == 4613732

可以看到代码非常易读,就像读自然语言一样。除了处理数值很方便,用它来处理文本也一样简单。看看这个需求:读取文件,统计文件中每个单词出现的次数,然后按照次数从高到低对单词排序。

from __future__ import print_function
from re import split
from pipe import *
with open('test_descriptor.py') as f:
  print(f.read()
      | Pipe(lambda x:split('/W+', x))
      | Pipe(lambda x:(i for i in x if i.strip()))
      | groupby(lambda x:x)
      | select(lambda x:(x[0], (x[1] | count)))
      | sort(key=lambda x:x[1], reverse=True)
      )

看看,非常简单吧?在我这里运行的结果如下:

[('self', 13), ('foo', 9), ('item', 9), ('_data', 8), ('print', 7), ('def', 5), 
  ('return', 5), ('Jeff', 4), ('i', 4), ('in', 4), ('jeff', 4), ('ken', 4), 
  ('obj', 4), ('val', 4), ('class', 3), ('lai', 3), ('pan', 3), ('tmp', 3), 
  ('Foo', 2), ('ItemDescriptor', 2), ('Wrapper', 2), ('__iter__', 2), ('for', 2), 
  ('if', 2), ('next', 2), ('object', 2), ('0', 1), ('1', 1), ('30', 1), ('8', 1),
  ('None', 1), ('__class__', 1), ('__future__', 1), ('__get__', 1), ('__init__', 1), 
  ('__set__', 1), ('bin', 1), ('coding', 1), ('env', 1), ('f', 1), ('from', 1),
   ('import', 1), ('instance', 1), ('isinstance', 1), ('len', 1), ('list', 1), 
  ('print_function', 1), ('python', 1), ('type', 1), ('usr', 1), ('utf', 1)]

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文