Nirvana Studio » Python :: 分享知识,传播技术

Archive for the 'Python' Category

为何 Java(以及很多其他编程语言)令人不爽

Posted by ShiningRay on 26th 七月 2006

作者:Ed Watkeys, edw@xmog.com 翻译:ShiningRay

原文地址:http://xmog.com/scrap/show/5
September 8, 2005
Updated December 27, 2005

Java 5.0其中一个最大的新特性是引入了一种迭代集合的新语法,用来替代以前繁琐的格式(如下):

for (Iterator i = c.iterator(); i.hasNext(); ) {
    String s = (String) i.next();
    ...
}

现在,多亏了这种新的优雅的迭代语法,以及泛型的引入,我们可以用下面的代码:

for (String s : c) {
    ...
}

很明显,可以少输入很多字符。但请考虑一个问题:为什么花了10年才引入了这个特性?不过我们先不考虑这个重要的问题,再看看另一种语言,Python。

Python在很多方面都是一个更加适合编程的语言。它的衍化要比Java快得多。例如,Python 2.2引入了生成器(generator)。一个生成器是一个可以产生多个值得函数,在每次调用时都会保存状态。下面是一个简单的例子:

def counter(n):
  while True:
    yield n
    n = n + 1

因为这个函数定义包含了关键词yield,所以Python就可以知道它是一个生成器。可以像下面这样使用counter生成器:

c12 = counter(12)
c12.next()
c12.next()

第一行创建了从12开始计数的生成器的实例。第二行告诉生成器运行到产生(yield)一个值为止。第三行告诉生成器继续运行直到产生了另一个值。该生成器所产生的前两个值分别是整数12和13。

这确实是一个很酷的特性:它让程序员能写出更简单的代码,而不会使生成器变得复杂和容易出错。为何Java不能学习Python呢?

我们也先将第二个问题放一边,思考一下如何用另一种语言来实现Python的生成器,这种语言就是Scheme——世界上有一些自以为是的怪人就用它。Scheme是Lisp的一种方言,它从诞生到现在已经存在了大约30年了。Lisp则已经存在了大约50年了。

下面是我可以完成的对Python中counter生成器模仿最好的Scheme实现:


对资深Lisp程序员多说一句:下面我要演示的子程序要比官方的累加器例子复杂得多,这是因为我是按照了Python生成器的语义来写的。


(define (counter n)
  (letrec ((generator
            (lambda (yield)
              (let counter ((n n))
                (call-with-current-continuation
                 (lambda (continue)
                   (set! generator (lambda (k)
                                     (set! yield k)
                                     (continue n)))
                   (yield n)))
                (counter (+ n 1))))))
    (lambda () (call-with-current-continuation
                (lambda (yield)
                  (generator yield))))))

“我靠!”你可能会有这种反应。确实太复杂了!在写这段代码的最初版本的时候,我说写这个不会太难。然后我发现了一个可能导致死循环的错误,而引发错误不是小概率事件。所以最后我认同了这点:如果只是要写一个能和Python生成器效果一样的函数,还是不要写这样的子过程的比较好。不过,这个可怕的东西在客户端代码使用起来却十分简单:

(define c12 (counter 12))
(c12)
(c12)

第一行定义了c12是子过程counter给一个参数12调用时的结果。第二行和第三行直接调用c12,没有任何参数,就和Python的例子一样,返回了12和13。不过这些都是学院派的,没有哪个疯子会在普通需求下写一个这样的子过程。

写像counter这样的子过程一般会导致手指抽痉、头脑发胀。不过,有意思的是,我们可以跳过这些来写counter,Scheme的生成器要比Python版本的更加容易使用,因为Scheme的返回的是函数,而Python生成器返回的是生成器对象,所以Python生成器需要调用next方法。

(旁白:Python生成器的设计师们本可以这样实现生成器对象:接下来的值通过c12()c12.next()来获取,不过他们并没有这样实现。)

回到Scheme上……在Scheme中写这样的生成器的复杂和容易出错看上去似乎让在Scheme中使用生成器变得不切实际,但实际上并非如此,因为Scheme包含了一个Python和Java都缺乏的特性:扩展语言语法的能力。如果你能够写出Scheme版本的counter,花不了多少功夫就可以创建一个宏(macro)使得这个特性能以一种可以被大家接受的方式使用。下面是我写的宏,可以完成这个任务:

(define-syntax define-generator
  (syntax-rules ()
    ((define-generator (NAME ARG ...) YIELD-PROC E1 E2 ...)
     (define (NAME ARG ...)
       (letrec ((generator
                 (lambda (yield)
                   (let ((YIELD-PROC
                          (lambda v
                            (call-with-current-continuation
                             (lambda (continue)
                               (set! generator (lambda (k)
                                                 (set! yield k)
                                                 (apply continue v)))
                               (apply yield v))))))
                     (let NAME ((ARG ARG) ...)
                       E1 E2 ...)))))
         (lambda () (call-with-current-continuation
                     (lambda (yield)
                       (generator yield)))))))))

一旦有了这个宏,counter生成器的Scheme版本就可以这样定义了:

(define-generator (counter n) yield
  (counter (+ 1 (yield n))))

还不错吧?这个版本唯一让我烦的地方是必须指定yield函数的名称。不过它还是给予程序员一些灵活性,可以根据代码的上下文来给函数起一个最有意义的名称。(其实,资深的Lisp程序员应该知道这个“特性”可以使用一些非hygenic宏来修正,不过这里我们还是坚持标准R5RS Scheme)。

如果你比较一下第一版和第二版的counter,你可能会注意到我在新的define-generator版本中作了一些小手脚:yield函数返回了它产生的值,因此它可以用于对counter的递归调用中。而Python的生成器就不能这样用。

那么为什么Java不能变得更像Python?答案是——其实Java和Python很像:Python的用户也等了将近10年才可以用上生成器。而我在玩了几天之后花了几个小时就在Scheme中加入了对生成器的支持。不过还是有人会说生成器以及最近的其他一些Python特性,如列表包容,都使得Python变得更加容易编写——我当然完全同意这个观点——但是,从根本上来说,Java和Python在这一点上是一样的——都不能修改语言本身。

Java、Python和几乎所有其他非Lisp语言都是让你任由语言设计者摆布。你要等他们实现你需要的语言特性,可能只有列表中的前几个。而且等他们弄个一个新东西给你搔搔痒,谁能确定你喜欢这种结果?

那么为何花了10年才在Java中有了增强迭代语法?这是因为在Java和很多其他编程语言一样,语法是一个大问题。一般用户都不会修改语言的语言。因为这很难完成,只有少数人学习了这种技术才能去做。当需要修改语法时,表达力和清晰的优先级都没有保持向后兼容重要。

在Scheme中,加入语法相对还比较简单,同时还可以根据特定问题的基础来完成,所以无须担心是有要给出一个普遍适用的理想解决方案。这种能根据问题构建语言的能力要胜过对使用很多括号的语言的担心。

链接

Posted in Java, Lisp, Python, Scheme | 7 Comments »

Python 不是 Java

Posted by ShiningRay on 3rd 四月 2006

作者:Phillip J. Eby.

翻译:ShiningRay @ NirvanaStudio

原文地址:http://dirtsimple.org/2004/12/python-is-not-java.html


我最近正在看一个基于wxPython的GUI应用程序,大概45.5KLOC的样子,但我没有计算它用到的库的大小(如Twisted)。代码是由那些对Python相对生疏的Java的开发者写的,所以程序有很严重的性能问题(如三十秒的启动时间)。我在检查代码的时候发现他们写了很多对Java有意义但是对Python却很恐怖的东西。并不是因为“Python比Java慢”,而是因为在Python中有更方便的方法去完成同样的目标,甚至在Java中不可能的事情。

所以,可悲的事就是这些可怜人事倍功半,产生了很多很多不需要写的代码,从而比相应合乎Python习惯的写法慢得多得多。我们来看一些例子:

  • 在Java中一个静态的方法(static)不能翻译成一个Python的类方法(classmethod)。哦,当然,多多少少他最终产生类似的效果,但类方法的目的实际上是做了一些通常在Java中不可能的事(如继承一个非默认的构造函数)。Java静态方法的习惯翻译通常是一个模块级函数,而不是一个类方法或静态方法(staticmethod)。(同时静态封闭(final)字段应该翻译成模块级常量。)

    这并不是一个性能上的问题,但是一个Python程序员要用像这些类似Java习惯的代码的话,可能就会被在该输入Foo.someFunction时却要输入Foo.Foo.someMethod这种情况给惹毛了。但是请注意:调用一个类方法将会比调用一个静态方法和函数要多一部分额外的内存。

    啊,那些Foo.Bar.Baz也不是省油的。在Java中,这些点分割的名称是由编译器去查找的,所以运行时根本无所谓你有多少点。在Python中,每次运行时都要查找,所以每个点都要计算在内。(Python中一定要记住这点,“平铺比嵌套好”,尽管比起性能,他和“可读性”和“简单就是美”更靠近。)

  • 要用switch语句?Python翻译将是一个哈希表,不是一堆if-then语句。用一堆if-then在Java中也不是switch语句,如果有字符串参与了呢?他其实是一个哈希表。CPython字典实现用了性能最佳—在我们宇宙中目前所知道的—的哈希表的实现之一。你自己所写的代码也不会比这个再好了,除非你是Guido、Tim Peters和Raymond Hettinger的“私生子”——还是遗传增强了的。
  • XML不是答案。它也不是一个问题。要在正则表达式上解释Jamie Zawinski,“一些人,当遇到一个问题的时候,就想‘我知道,我要用XML’那这个时候,他们就有两个问题了。”

    和Java比这个一个不同的情况,因为比起Java代码,XML是轻巧而且有弹性的。但比起Python的代码来,XML就是一个船锚,一个绊脚石。在Python中,XML是用来做交换,而不是你的核心功能,因为你不需要这么做。在Java中,XML可能是你的大救星因为他让你实现了特定领域的语言并“不通过编码”提高了你的应用程序的适应性。在Java中,避免编码是一个很大的优势,因为编码意味着重新编译。但在Python中,更常见的是,写代码比写XML更方便简单。同时Python处理代码要远远比处理XML快。(不仅仅是这个,你必须书XML处理代码,同时Python自身就已经为你准备好了。)

    如果你是一个Java程序员,对于你是否要在你的Python核心应用中使用XML作为一部分,不要相信你的本能。如果你不是因为信息交互的原因去实现一个已经存在的XML标准或是建立某种导入、导出格式或者建立某种XML编辑器或处理工具,那么就不要这么做。一次也别。甚至连想都不要想。现在,扔掉那个XML模式把你的手解放吧!如果你的应用程序或者平台要被Python开发者使用,他们只会感谢你不要在他们的工作量中添加使用XML的负担。

    (这里唯一的例外是如果你的受众的的确确,确确实实需要XML,出于某种奇怪的理由。像,他们拒绝学习Python并只对你使用了XML而付钱给你,或者你打算给他们一个编辑XML的GUI,同时这个写XML的GUI呢是另一个人写的,同时你得到免费使用的权利。还有一些很少见的架构上的原因需要用到XML。相信我,他们不会出现在你的程序中。如果有疑问,对一个资深的Python开发员解释你的用例。或者,如果你脸皮厚的话,试试向一个Lisp程序解释你的程序为什么要用XML!)

  • Getter和setter是坏蛋。坏蛋,魔鬼!Python对象不是Java Bean。不要写什么getter和setter,然后还把它们包装在“属性”里面。它直到你能证明你需要比一个简单访问复杂一点的功能时才有意义,否则,不要写getter和setter。它们是CPU时间的浪费,更要紧的是,它们还是程序员宝贵时间的极大浪费。不仅仅对于写代码和测试的人,对于那些要阅读和理解它们的人也是。

    在Java中,你必须使用getter和setter因为公共字段不允许你以后改变想法再去使用getter和setter。在Python中,这样做很傻,因为你可以以一个普通特性开始并可以在任何时间改变你的想法,而不用影响到这个类的任何客户。所以不要写getter和setter。

  • 代码重复在Java中常常是一个不得不要的魔鬼,你必须经常一遍一遍写同一个方法而只有一点点的变化(通常是因为静态类型约束)。在Python中这样做是没有必要的也是不值得的(除了极少数一些特定的场合需要内联一些要求性能的函数)。如果你发现自己一遍一遍在写同样的代码而且变化很少,你就需要去学一下闭包。他们并不是真的很可怕。

    这就是你要做的。你写了一个包含了函数的函数。这里内部的函数就是你要一遍遍写的函数的模版,但是在里面加入了针对不同情况的函数要使用变量。外部的函数需要刚刚提高的那种变量作为参数,并且将内部的函数作为结果返回。然后,每次你要写另一种略微不同的函数的时候,你只要调用这个外部的函数,并且把返回值赋给你要让“重复”函数出现的名字。现在,如果你需要改变这个工作方式,你只要改变一个地方:这个模版。 

在我所看过的应用程序/平台中,只有一个很微不足道的程序使用了这个技术之后可以去掉数百行重复代码。事实上,自从开发者使用了特别的样板文件来为这平台开发插件,这会节省很多很多第三方开发人员的代码,同时也使那些程序员要学习的东西简化了。

这只是Java->Python思维方式转变的冰山一角而已,现在我可以让他转变成正确的而不用钻研这个程序的细节。本质上,如果你曾经用过一段时间Java,而且对Python比较陌生,不要太相信自己的本能。你的本能已经为Java调节,而不是Python。向后退一步,最重要的,不要写这么多代码了。

要这样做,让自己觉得更加需要Python。假装好像Python是可以做任何你想做的魔棒,却让你无须动一个手指。问一下,“Python是怎样解决我的问题的?”还有“Python语言的哪个特点和我的问题最相似?”你绝对会惊讶于你需要的东西其实已经有了某种固定形式。事实上,这种现象实在是太普遍了,甚至在很有经验的Python程序员中也会出现,以至于Python社区中给这种现象起了个名字。我们称之为“GUIDO的时间机器”(GUIDO是美语中太空飞行工程师的意思),因为有时候看上去得到我们所需要的东西好像只有他知道的一种方法,但当我们自己知道了就不一样了。

所以,如果你不能感到你在使用Python时至少比用Java要多出10倍的生产力,!(同时如果你还怀念你的Java IDE,考虑一下这种可能性:因为你写的Python程序比他所需要的要复杂得多)


附录:(翻译自此篇文章的评论) 

确实,哈希表==字典。举个最简单的例子,从Python

标准库中检出“pickle”和“copy”模块,这两个模块会从字典中查找类型并调用相应的函数。另一个有些诡异的例子是范型函数,我已经在最近的Blog中写了一下。

关于闭包的例子,我这里给出一个很笨的例子。假设你要写很多这样的函数: 

def addOne(x): return x+1
def addTwo(x): return x+2

然后你可以这样写:

def makeAdder(addend):
… def add_it(x): return x+addend
… return add_it

并且这样使用: 

 

addOne = makeAdder(1)
addTwo = makeAdder(2)

这样就可以等同于原来的定义了。 

 

相关资料:http://www.razorvine.net/python/PythonForJavaProgrammers

Posted in Java, Python | 1 Comment »

简介延续“Continuation”

Posted by ShiningRay on 3rd 四月 2006

作者: Denys Duchier

翻译: Shining Ray @ Nirvana Studio

写本文旨在回应comp.lang.python新闻组中关于延续的讨论。


对于call/cc(call with current continuation)的情结和关于他的操作解释粗糙的细节内容,至今一直掩盖了延续的简洁和优雅。在本文中,我想用两个方式来纠正这个问题:

  • 首先用一个简单且直观的方式展示延续的概念。
  • 第二通过提供_可运行的_Python代码,来描述如何使用延续而不用call/cc来实现搜索引擎。

我将展示:

  1. 一个针对命题公式确定性检查工具。
  2. 一个基本的带回溯和裁剪的prolog引擎。

我希望这能帮助那些对于这个论题有些迷惑的人搞清这个问题。我还想指出上面讲到的两个程序是我在今天晚上花了几个小时写的,他们应该可以给你关于下面将要讲述的技术的能力的一些衡量尺度。


1. 简化延续

习惯上一般来说,一个函数返回一个值是这样的:

def foo(x):
        return x+1

这隐含了它的值返回的地方。而延续的想法是通过添加一个延续参数来明确要返回的地方。函数不“返回”值,而是通过把值作为一个参数传递给延续“继续”处理值。在基于延续的程序中,以上函数foo变成了:

def foo(x,c):
    c(x+1)

从这个角度看,函数从不使用返回“return”。取代的是它“继续”。正因为这个原因,延续有时候会被描述为“带参数的goto”。

上面描述的想法是。更精确地说,它是CPS(Continuation Passing Style,延续传送风格)的一个初步代码转换。基本的想法是给每个函数添加一个额外的“延续”参数,并进一步转化函数体以便不用返回他的值,而是把值传送给这个额外的延续参数

在foo函数的例子中已经大体描述了这个概念。然而,更确切的,要注意CPS转换同时展开了所有的非lambda算子的嵌套表达式(换句话说,就是他显式地连接了所有子表达式的计算)。让我们看一个例子:

def baz(x,y):
        return 2*x+y

在延续传送的观点中,甚至像*+之类的基本操作符都要带一个额外的延续参数。我们通过以下定义进行模拟:

def add(x,y,c): c(x+y)
def mul(x,y,c): c(x*y)

现在,CPS可以把上面的baz函数转换成:

def baz(x,y,c):
        mul(2,x,lambda v,y=y,c=c: add(v,y,c))

换句话说,2*x的计算现在使用了一个延续来接收结果v并使用它来计算 v+y并最终把这个结果传给总的延续c

当在这个上下文中理解了call/cc之后,它就不再神秘了。它只是一个特殊的手段,能让我们的双手去接触那个不可见的由CPS引入的额外的延续参数,并让我们像程序中其他函数值那样使用它。
。思考call/cc(f),其中f需要接受当前延续作为参数。其实说到底,call/cc(f)可以通过CPS转换成call_cc(f,c),而c就是CPS所引进的延续参数同时call_cc可以按照以下方式定义:

def call_cc(f,c):
        f(c,c)

即,f的普通参数和由CPS引入的额外的延续参数都是当前的延续c

还有一些细节,不过以上部分是基础。

CPS转换是很多针对函数式语言的编译器的基础。它的缺点是它引入了很多lambda算子(即闭包),同时,必要的是,编译器要尽可能多得把他们优化出去。在这方面,Scheme的Rabbit编译器的Steele,T的Orbit编译器的Kelsey etal. 和SML/NJ编译器的Apple有着很多的研究。有一个优点是,如果lambda表达式是你唯一的控制结构而且你把他们优化到了极限,那么你同时也优化了所有的控制结构。

然而应该注意的是,也正如很多人已经注意到的,由于编译器的工作恰是常常要把CPS引入的东西删除,所以有些人对将CPS转换作为编译过程基础的价值提出了质疑。

2. 将延续作为一个普通的编程技术

你可能已经注意到一些人对于不可思议难懂的延续的应用极其狂热。但事实上还是有着很多“非不可思议”的延续的应用,他们也不要求call/cc的存在。你可以在Python中书写延续传递程序,或者在任何支持闭包的部分形式和有自动垃圾收集的语言中写。

我最了解的应用程序是关注于“搜索”。它和正在进行的关于迭代子的讨论有很大的关系。很多年以前,我从我以前的导师Drew McDermott那里学习了这个技术,我会在下面讲述它。然而,我得指出,和Tim的特性记述相反,生成器(generator,在Drew的观点上)不需要以“类栈方式”进行表现;尽管很少有提出不这样用的 :-)

这个概念是通过传递两个延续来进行搜索:

  1. 一个成功延续,用于进一步进行搜索
  2. 一个失败延续,用于回溯到第一个更早的选择点

用Python来表达它,常常是以下面的形式出现:

class Foo:
  def search(self,info,yes,no):
    if self.check(info):
      return yes(info,no)
    else:
      return no()

“info”是在搜索中传送的一些信息。“yes”是成功延续,“no”是失败延续。“yes”以当前的“info”状态和当前的失败延续作为参数。而“no”则没有参数。

如果self.check(info)为真,则Foo对象满足了搜索条件。

一个有两个Foo“one”和“two”属性的类Baz。定义一个Baz对象来满足搜索条件,只要“one”属性满足它或者“two”属性满足(换句话说就是一个Baz对象是一种析取式)。我们通过调用“one”属性的搜索方法来表达,同时还给他传递一个尝试“two”属性的失败延续。

class Baz:
  def __init__(self,foo1,foo2):
    self.one = foo1
    self.two = foo2
  def search(self,info,yes,no):
    return self.one.search(
      info,yes,
      lambda self=self,info=info,yes=yes,no=no: \
      self.two.search(info,yes,no))

很明显,在上面的内容中,由于Python缺乏对于闭包的真正支持,使得要按照函数式语言中简洁和优雅的写法书写有些困难。

3. 检查命题公式的确定新

命题逻辑的公式如下:

         ((p|q) & (p->r) & (q->r)) -> r

它表示

        if     p or q
           and p implies r
           and q implies r
        then
           r

如果 p 或 q
    且 p 蕴含 r
    且 q 蕴含 r
那么
    r

p,q,r 是可以被赋以真值得命题变量。你可以证明不管你给p,q,r赋什么值,上面这个公式总是为真的。看一个更加简单的公式会更清楚,如(p | !p)也就是“p或非p”。这样一个公式是所谓的“确定的”:它总是为真,无论你如何解释他的变量。

下面的Python程序实现了针对命题公式的确定性检验工具,它使用了前面描述的延续传送风格。这个程序仅仅只是要描述一个例子而已。对于这种人物有更加有效的方法。然而,我相信它可以很好地表达关于使用延续传送实现搜索的一个通用的想法。

两个程序(确定性检查工具和前面提到的prolog引擎)都可以在下面的URL中获得:

Posted in Python, 函数式编程 | No Comments »