动态语言中的设计模式

今儿迁主机的时候,顺便把旧文档分类整理了一下。发现自己在04年还写过这么一篇…… 居然还投给了《程序员》…… 那时候真是太有闲情逸致了,我操……

读了读,除了“检视”这种港台腔外。发现我那会儿就有把简单事写复杂的天赋…… 恩,以下愿者上钩……

——时光倒流的分割线———

动态语言中的设计模式

Grady Booch在为《Design Patterns》一书所写的序言中谈到,“软件领域中的设计模式为开发人员提供了一种使用专家设计经验的有效途径”。 从设计模式出现的那一天起,这众多的模式就成为面向对象软件开发过程中一笔宝贵的财富,而对设计模式的总结工作也从来没有停止过。先是GoF的《Design Patterns》中23种经典模式的编目和命名,而后广大开发人员在此基础上不断扩充。这种种工作都为人们积累了重要的设计经验。

然而尽管设计模式本身应该与具体语言实现无关,但设计模式是从实践中总结而来的,因此在成文过程中它又不可避免的与特定的语言相关联。目前由于在工业中C++、Java等静态语言占据主导地位,所以人们总结出来的设计模式通常也是基于这些语言的。

如今计算机硬件仍然依照摩尔定律所提出的趋势飞速发展,软件的复杂度也越来越高,相对而言软件开发效率却一直没有质的飞跃。因此执行效率相对低下但开发效率较高的动态语言愈发受到人们的关注。但是想在动态语言中使用目前总结出的大量设计模式却存在一定的困难,因为静态语言与动态语言的语义差异很大,如果只是照搬目前这些设计模式并不能充分发挥动态语言的语言特性,有时候把某些静态语言的模式运用到动态语言上甚至是作茧自缚、画蛇添足了。

Python作为一种完全面向对象的动态语言,因为其简洁、易用、灵活的特点已经越来越受到开发人员的青睐。以下行文中就将用Python实践几种简单的模式,以此来逐步展示Python作为动态语言所具有的语言特性对于设计模式实现的影响。

1.    运用动态类型特性实现灵活性更高的Abstract Factory

不妨让我们从《Design Patterns》中所提到的第一种模式Abstract Factory开始。

实际上同样是面向对象的语言,把C++的代码翻译成Python的代码并不难。下面Python代码就是由《Design Patterns》中提供的用于创建迷宫相关产品的抽象工厂类MazeFactory翻译得到:


class MazeFactory:
  def makemaze(self):
    return Maze()
  def makewall(self):
    return Wall()
  def makeroom(self, n):
    return Room(n)
  def makedoor(self, r1, r2):
    return Door(r1, r2)

这个类就可以用于创建Maze的基本产品了,同时我们可以继承这个类已得到其他系列产品的工厂,比如魔法迷宫EnchantedMaze的抽象工厂:


class EnchantedMazeFactory(MazeFactory):
  def makeroom(self, n):
    return EnchantedRoom (n)
  def makedoor(self, r1, r2):
    return EnchantedDoor (r1, r2)

这样我们就不再需要将有关Maze风格的代码硬编码到具体Maze的创建过程中,而当创建不同风格的Maze时只需要替换不同的Factory就行了。
不过这么简单的翻译C++代码很无趣,而且也不能充分体现Python作为动态语言为编程人员带来的便利。下面让我们来看一看Python语言中的动态特性能为我们做些什么。

略加察看后我们就会发现这样实现的MazeFactory具有一个不足之处:很难向MazeFactory里添加新种类的产品。当需要支持新类型的产品时(比如一个陷阱产品Trap以及EnchantedTrap),我们需要修改MazeFactory以及它的所有子类,这是一件很枯燥的事情。而且我们还不得不改变MazeFactory的接口,这更是我们不愿意看到的。

对于这个问题GoF的书中向我们介绍了一个比一般Abstract Factory更灵活的解决办法,即只实现一个统一的make函数,通过传递给make不同的参数来确定要创建的产品的类型。

然而对于C++这样的静态语言,实现这种解决方法很困难。因为为了确定make返回值的类型,我们不得不将所有的产品继承自一个公共基类。但是Abstract Factory考虑的只是产品的风格系列,而同一系列不同类型的产品间逻辑上可能不存在明确的公共基类——比如Maze和Wall。而且即使使用公共基类也会有导致大量的向下强制类型转换出现。这些都是我们编程时希望避免的。

此时动态语言中动态类型特性的优势充分体现出来。动态类型允许一个变量在运行时刻绑定到不同类型的对象上,也就是说所有变量——包括函数的参数和返回值——都可以是任意类型的。因此我们不必要求Maze、Room等不同类型产品具有公共基类,也能很容易就能实现这种拥有统一make函数的工厂:


class Maze:…
class Wall:…
class Room:…

…

class MazeFactory(object):(注1)
  def make(self, typename, *args):
    if typename == 'maze': return Maze()
    elif typename == 'wall': return Wall()
    elif typename == 'room': return Room(args[0])
    else typename == 'door': return Door(args[0], args[1])

class EnchantedMazeFactory(MazeFactory):
  def make(self, typename, *args):
    if typename == 'room': return EnchantedRoom (args[0])
    elif typename == 'door': return EnchantedDoor (args[0], args[1])
    else: return super(EnchantedMazeFactory, self).make(typename, args)

而创建Maze的代码可能是:


mf = EnchantedMazeFactory ()
mz = mf.make('maze')
r1 = mf.make('room', 1)
mz.addroom(r1)

make函数第三个参数*args表示把从第三个开始以后所有的参数接受为一个list,产品的构造函数从这个list中取出相应的参数用于构造对象。super是内置函数,用于返回一个变量的基类对象。

这样利用Python动态类型的特性(注2),我们用很小的代价实现了一个具有更多灵活性的MazeFactory了。现在当我们要支持新的产品类型时,我们只需要添加标示新产品的参数,而保持了MazeFactory接口的稳定。

不过估计你看到这一堆if、else会感到很不爽(其实就是个switch),我也是。更好的做法是编制一个产品的字典,利用这个字典来索引要产生类型,后面的部分我们将具体介绍这个方法。

动态类型是动态语言的一个基本特性,它能简化类层次、去除不必要的强制类型转换,提高语言表达能力以及自由性。实际上有很多模式都因为静态类型检查的限制造成了不必要的类层次和强制类型转换。比如一个用于遍历包含许多不同类型元素容器的Iterator,就可能要求所有的元素都继承自同一基类以便实现getCurrentItem,而实际上这些不同类型的元素逻辑上并不需要一个公共基类。利用动态类型特性可以使这种模式的实现更加简单灵活。

2.    动态语言不需要一些静态语言中的设计模式

前面提到我们需要编制一个产品的字典,利用这个字典索引要产生类型,这样我们就可以通过类似index['room']的代码来创建产品了。同时我还希望赋予这个MazeFactory动态配置产品类型的能力。用什么模式?Prototype?没错,这的确是运用Prototype的地方,让我们来实现它:


import copy
class MazeFactory:
  def __init__(self):
    self.index = {'maze': Maze(),
      'wall': Wall(),
      'room': Room(),
      'door': Door()}
  def make(self, typename,):
    return copy.deepcopy(self.index[typename])
  def registtype(self, typename, instance):
    self.index[typename] = instance
  def unregisttype(self, typename):
    del self.index[typename]

copy是Python的一个内部模块,其中包含deepcopy函数用于深拷贝对象。registtype、unregisttype函数则用来动态增删产品类型。
似模似样,还不错。不过Python中有没有更好的做法呢?

首先检视一下Prototype的初衷。因为C++并不提供类一级的对象——也就是说类本身不是对象,这就给生成产品以及动态控制产品类型造成了一定的困难,Proto Type模式的出现就是为了解决这个问题。然而像Smalltalk一样,对于Python来说类本身(也包括函数,模块)也是一个对象——type类的对象,可以对对象进行的每样操作都可以使用到类上。利用这点便利,我们不使用Prototype模式就可以实现这个MazeFactory:


class MazeFactory:
  def __init__(self):
    self.index = {'maze': Maze,
      'wall': Wall,
      'room': Room,
      'door': Door}
  def make(self, typename, *args):
    return apply(self.index[typename], args)
  def registtype(self, typename, type):
    self.index[typename] = type
  def unregisttype(self, typename):
    del self.index[typename]

apply是一个内置函数,它接受两个参数一个函数、一个参数list,它的作用是把args作为参数传给类构造函数运行。

现在当我们要添加新的产品类型时,在字典中添加新的索引值对即可。比如我要在MazeFactory中添加一个新产品Trap,只需要在index里加入:'trap': Trap。而动态添加类型时也只需要传入相应的类即可:mf.registtype('trap', Trap)

可以看到我们已经得到了一个静态、动态都比较容易管理产品类型很灵活的工厂。

设计模式中一部分模式的作用就是让程序更灵活,拥有更多的动态特性。而动态语言可能在语言一级就已经支持了这些特性,所以这部分模式也就不需要了。Prototype是一个例子,类似的还有Template Method。实际上因为上面提到的动态类型特性,任何函数都可以接受任意类型的变量做参数,所以Template Method模式也就没有任何价值了。

3.    运行时刻修改类结构来实现Singleton

Singleton是一个非常有用的模式(注3),比如前面的MazeFactory就很有可能需要做成一个单件。让我们来尝试实现这个模式。首先还是按照C++的方法来实现一下:


class Singleton(object):
  instance = None
  def __new__(cls):
    if cls.instance is None:
      cls.instance = object.__new__(cls)
    return cls.instance

类的__new__方法是一个特定的函数,它在对象创建前被执行返回一个cls类的对象。instance则负责保存单件对象。

依循C++的思路,这个程序完全正确,但是看起来缺少一点感觉。让我们查看一下Singleton对象创建的过程,第一次构造对象时创建一个新的对象,以后构造时则不再创建而只是返回第一次创建的结果。

可以看到这是一个典型的运行时刻修改类结构的例子。Python来做自是手到擒来:


class Singleton(object):
  def __new__(cls):
    cls.instance = object.__new__(cls)
    cls.__new__ = cls._new
    return cls.instance
  def _new(type, cls):
    return cls.instance
    _new = classmethod(_new)(注4

classmethod函数的作用是把一个成员方法变成一个类方法。

整个类的代码很简单,第一次进行构造时创建对象,同时用_new函数替换__new__函数,而以后__new__(实际上就是_new)只是返回第一次创建的对象。

能够在运行时刻修改程序结构是动态语言区别于静态语言的一个重要特点,Python中它主要表现在可以动态添加、删除、修改对象(包括类和函数)的方法和变量。这个特性可以用来帮助实现Singleton、Decorator、Strategy等一些要求运行时刻动态改变对象职能的模式。

4.    用Functional Programming简化Command模式

最后要谈到的一点就是Functional Programming。在设计模式里谈到FP可能不很切合。如果我们注意一下会发现GoF《Design Patterns》的副标题是“Elements of Reusable Object-Oriented Software”(可复用面向对象软件的基础)。实际上我们日常谈到的设计模式主要是针对OO Programming来说——因为工业中主要应用的是面向对象编程语言。而FP是区别于OOP的另一种编程风格。

但是Python作为一种面向对象的动态语言,它对FP和OOP的编程都提供了良好的支持。在OO的设计模式中运用一些Functional Programming的思想也会收获一定的好处。下面以Command模式为例讲述这一点。

因为有时候请求操作的对象不知道被请求操作对象的任何信息(打开文档的按钮对象不会知道任何文档对象的信息)。Command模式用以解决这个问题,根本目的在于把被调用操作的对象与实现该操作的对象解耦。而Command模式的实现方式是“把函数层面的任务提升到了类的层面”(注5)。因为FP中函数本身就是第一类对象,所以我们不必再像C++中那样运用继承组合来迂回地实现这一点。FP实现Command模式是非常简单的。


class Button:
  def click(self):pass

这样我们就已经有了一个Button类。看起来什么都没有?别急,先让我们实现一个用于打开文档的Button看看:


opendoc = Button()
opendoc.click = doc.open

现在当opendoc这个按钮被点击时,仅仅需要执行opendoc.click,就等价调用了doc.open这个函数了。这段代码看似简单,但观察下会发现Button和Document已经被很好地解耦,只是FP的实现中间没有那么多迂回,而思路、效果与Command模式是一样的。

再来看看MacroCommand。MacroCommand的目的在于一次执行一系列的Command——也就是函数。有了FP的办法,我们也不必再用Composite模式搞得很复杂,而是利用匿名函数lambda关键字和内置FP函数map实现为:

marcocommand = lambda commands: map(lambda command: command(), commands)

map依次把commands中每个的command传给第一个参数lambda command: command(),而这个参数本身也是一个函数,用来执行传入的command。现在如果我们要实现一个Find & Replace的Button,我们只要这么写:


findreplace = Button()
findreplace.click = lambda: marcocommand([doc.find, doc.replace])

当然Function Programming本身是很复杂的,仅仅在设计模式中一些简单的运用不能充分体现FP的思维方法和能力。但FP作为区别于OOP的编程风格,将其适当用在设计模式中,的确可以优雅简单地实现一些原本复杂的模式。而不同编程风格的尝试和融合也有利于拓宽我们的思路。

结语

通过Python的模式实践,我们可以看到动态语言的种种特性对各种设计模式的实现方式的影响,它们在简化了部分模式实现的同时更带来了较大的灵活性。当然天下没有免费的午餐,动态语言在语言一级提供更大灵活性是以执行效率的下降为代价的,个中的利弊还要广大开发人员去权衡。

本文只是浮光掠影地进行了一些简单的讲述,其中不免有许多疏漏。要想更好地掌握动态语言中设计模式的应用,必须要对各种设计模式以及动态语言的特性有着清晰的了解,并在实践中不断尝试才能做到。

注1: Python中继承自object的class表示一个type,否则是一个classobj。一些特定的操作必须是type类型才能执行,比如super函数要求传入一个type类型的参数、__new__函数也只有type类型才支持。如果需要用到这样操作,则必须让你的class继承自object。

注2: 值得注意的是Python实际上是一种强类型动态类型的语言,而不是弱类型的语言。关于强弱类型的具体解释可以参考这篇文章《Typing: Strong vs. Weak, Static vs. Dynamic》By Aahz。

注3: 实际上由于Singleton模式不能很好地适用于多线程环境,多线程环境中使用更多的可能是Singleton的变种Double-checked locking模式。不过理解了Python中Singleton实现,DCL也是大同小异,所以这里仍然用基本的Singleton模式来解说。

注4: Python2.4中提供新的语法Decorators for Functions and Methods来使类似操作更容易的实现。具体的语法和相关说明见PEP 318

注5: 语自《Agile Software Development》By Robert Martin

2004-11-25左右成稿

python的垃圾回收机制

因为要和别人讲理,所以把这部分代码看了看。

大概说一下我的理解。python广义上的垃圾回收是用两种互补的方式实现的:首先对于每一个对象,如果它的引用计数减到0,那么它的__del__会先被调用然后被回收;python里还有一个gcmodule,这个模块虽然叫gc,但其实只是处理循环引用的问题。注释中明确地说了这个gcmodule中不会发现引用计数为0的对象。这个模块中有分代的机制。

比较java/C#和python的垃圾收集(也许是内存管理)大概有两方面区别。python和java/C#第一个区别是垃圾回收的时机。python是一旦一个对象的引用计数减为0就把这个对象回收,这里实际上是延续了C/C++的思维方式(考虑new/delete)。而java/C#都是在内存紧张才会执行整个空间上的垃圾回收。这里python看上去的好处是__del__的调用时机很明确,但是如果深入观察就会发现其实不是这样,因为你没办法控制引用计数,一旦发生循环引用你依然没办法控制__del__调用的时机(根据源码__del__还是有可能根本不被调用)。我是很怀疑python在这里把可以一起做的事情拆散了,因为完全可以在gcmodule处理循环引用的时候一并处理引用计数为零的情况。

另外的区别就是对堆的使用方式。java在创建对象时用的是很简单的方法就是从前向后不断分配――很想栈所以很快,这样在垃圾收集时,显然必要地,会对回收后还在使用的内存进行压缩。而python(至少是CPython)是继承了C的malloc,我对malloc的机制不怎么熟悉,印象中应该是空闲块list的方式吧,可能不对。加之前面回收时机的问题,这就使得python很需要一个高效的pool。

以上两种综合的机制(立即回收+malloc vs. 必要时回收+像栈一样使用,实际上有点像C/C++ vs. java了)哪个效率更高肯定是根具体实现有关的,要看具体的数据结构和算法(回收算法,pool的实现)。不过如果是java/C#对比python,那么java/C#作为大厂商们的推崇,胜出应该必然的结果。

学习了一下Python的实现

最近在网上看到这个《Python源码剖析》系列,写得很好,赞一下顺便帮着宣传http://blog.donews.com/lemur/,呵呵

顺着看下来,发现Python的实现的确有够糟糕。又引用计数来维护对象进行垃圾回收,每当一个对象的引用计数减到零时就进行回收――我怎么也觉得这是十年以前的技术了。

另外比如对于int和string类型(都是经常被大量创建和销毁的对象)使用了两种不同的缓存机制来加速。但是对于int来说,只有一定区间内(官方版本里是[-5,100)之中)的整数才能被缓存,而区间之外的整数来说都会被创建多个不同的副本。而对于string则是虽然被缓存再利用但是缓存之前字符串一定要被创建一次――也就是说这个机制只能帮助只是帮助比较操作(估计是没说清楚:-p)。本能上觉得应该有更好的做法,但也未必,因为我实际上也就看过这一种实现。

再有的发现就是一个PyPy项目http://codespeak.net/pypy/dist/pypy/doc/news.html,用Python来实现Python。从用Lisp实现Lisp开始,我就没有搞清楚这里的逻辑关系,抽空要把这个项目看一看。

Python AOP概览

Lightweight Python AOP
http://www.cs.tut.fi/~ask/aspects/aspects.html
足够简单,因此也很像个玩具或者随意的练习。
喜欢这个方案中把函数作为第一类对象的做法,wrap_around函数会动态complie一段代码来创建新函数来替代原有函数,并在新函数中调用方面(aspect)的代码和原函数。
缺点是当需要多个方面时它采用嵌套的方法来进行织入(weave),也就是说如果想在织入后从织入链中去掉一个方面会非常困难。而且把原函数改名保存的做法虽然直接看起来却不够优雅――如果类中恰好有和修改后的名称同名的函数怎么办?而且把改名后的函数(也包括其他一些需要的属性,比如方面调用时的堆栈)保存在函数所属类中,也就是说虽然在这个方案中把函数作为第一类对象来看待,但并不能把一般函数作为连接点(join point)。而对实例(instance)的织入会影响该类的所有实例。

Pythius.aop
http://cvs.sourceforge.net/viewcvs.py/pythius/pythius/pythius/aop.py?rev=1.36&content-type=text/vnd.viewcvs-markup
用metaclass实现的Aop,如果对应到Java上Aop的实现可以说是非常相似了――或者说很像是实现了Python版的动态代理。pythius.aop.Metaclass(这个名字起的可够糟糕……)作为元类(metaclass)其实例化的类会修改__getattr__、__setattr__的实现在其中拦截全部调用――包括函数、变量等等。它提供的例子是这样的:
>>> class Square:
        __metaclass__ = pythius.aop.Metaclass    # This is constant!

        # This changes!  Note that we are referring to an *instance*
        # of an Aspect, not the Logger class itself.
        _aspect = my_logger
        …
这里织入的声明包含在类声明中,于是类和方面间是有很强的耦合,实际上这是完全没有必要的。完全可以把这个织入的声明抽象出来
>>> SquareWithLogger = pythius.aop.Metaclass(&aposSquareWithLogger&apos, (Square,), {&apos_aspect&apos:my_logger})
这会产生一个很像Decorator的子类,值得注意的是如果用动态代理来实现Aop那么动态代理其实不如叫作动态装饰器。这个实现方法支持方面extend来合并多个方面,不必非要进行嵌套织入,理论上也可以修改织入链――但没有提供对应的方法。
问题在于不支持对一般函数的织入。不支持实例的织入。Metaclass对类的改动太大,有些直觉上是不必要(比如已经通过修改__getattr__实现拦截为何还要从类中删去连接点的声明),可能导致一些依靠反射程序无法正常运行。另外类中具体的函数连接点实际上是被定义在方面中的,这个耦合很糟糕。

Aspects
http://www.logilab.org/projects/aspects/
定义了一个抽象方面AbstractAspect在其中保存原函数和方面――用户可以扩展这个方面,然后定义一个创建函数的函数在被创建的函数中依次调用方面和原函数。这个和Lightweight Python AOP的思路很像,但是因为用抽象方面把信息抽象一致地保存所以就不需要动态compile出新函数――相比之下Lightweight Python AOP通过一个递增的数字拼接处字符串来记录原函数和方面的做法就太过粗糙了。支持实例的织入。而且这个方法的思路本质上是可以支持一般函数的织入的,可惜在构造AbstractAspect时默认传入的参数是成员函数――但是可以很类似实现出支持一般函数的织入的抽象方面和调用函数。
另外,Weaver作为一个Singleton实现其不仅提供织入的功能同时还保存了被织入函数和方面的信息(比如防止一个方面被织入同一函数两次――不过我觉得这个假设毫无道理),这个Weaver很像是个容器。好处是虽然也采用了嵌套织入的方式但根据Weaver中的信息可以实现方面的拆卸――但我认为应该有依赖性更小的方式。
这个方案可以说十分注意不影响连接点,比如新函数替换连接点函数时保持__doc__仍然可用,值得借鉴。

PyContainer
http://pycontainer.sourceforge.net/
实现了Interceptor,还算不上Aop。另外实现的方式很奇特(如果不是奇怪),利用container的优势返回包装过的实例,这个实例重写了__getattr__方法当调用存在拦截器的函数时就会返回这个实例自身……因为这个实例同时实现了__call__方法其中依次调用拦截器和原函数。整个项目还是一个很粗糙的版本而且已经一年多没更新过了。

AOPTutorial–Doing AOP With TransWarp (Under Construction)
http://www.zope.org/Members/pje/Wikis/TransWarp/AOPTutorial
不知道这个该不该算成一个Aop的方案,因为看这篇文档感觉主要是在讲Mix-in。其中的Aspect像一个可以动态添加的基类。有趣的概念是如果Aspect包括一个类体系那么它每次混入的结果都会产生一个新的类体系。具体看文档吧,一两句话说不清楚。另外没看到代码不太清楚是怎么实现的,因为实在懒得下一个Zope了。

理想中的Python AOP
   方面和类不应该存在任何耦合
   支持函数、属性作为连接点
   支持把各种函数都作为连接点,包括function, unbound method,  instancemethod, class method, static method
   支持before, after, around, throw
   织入后除了连接点之外不会对程序的其他部分产生影响,比如dir()、__doc__应该还能正常工作
   支持运行时刻连接点上方面的添加和卸载
   不依赖容器