Python里的原型编程

最近在看《松本行弘的程序世界》,其中讲到Ruby和Javascript里的原型编程,觉得非常灵活。我记得之前看Python教程的时候也有提到过类似的机制,无奈当时不求甚解,现在只好补补课。

在原型编程中,对象的成员变量和方法都可以动态修改,通过复制已有的对象来实现代码的重用。这样不需要事先定义好的类就可以实现面向对象编程。要实现原型编程,首要条件就是对象的成员变量和方法可以动态修改。Python提供了setattr()和delattr()两个函数来动态修改对象的属性,也就是成员变量和方法。先看下面一段代码:

class Foo(object):
    def do(self):
        print("done")

o = Foo()
try:
    print(o.bar)
except:
    print("no attribute bar found")

setattr(o, "bar", 1)
try:
    print(o.bar)
except:
    print("no attribute bar found")

delattr(o, "bar")
try:
    print(o.bar)
except:
    print("no attribute bar found")

第一次print(o.bar)的时候,因为Foo类型的对象没有这个属性,所以会抛出异常;第二次打印的时候,已经通过setattr()添加了bar属性,所以可以成功;之后又用delattr()删除了属性,所以第三次打印又是异常。事实上,还有更简单的写法:

o.bar = 1   # 等价于setattr(o, "bar", 1)
del o.bar   # 等价与delattr(o, "bar")

在Python里,每个自定义类的对象都有一个__dict__成员变量,这个字典里记录了这个对象的动态属性(在类定义之外添加的属性)。对于Foo类型的对象o,打印出来就是这样:

>>> o = Foo()
>>> o.bar = 1
>>> o.__dict__
{'bar': 1}

动态添加的bar在__dict__中,而类中定义的do不在。__dict__在对象外部是可读可写的,相当于一个public类型的成员变量。通过修改对象的__dict__,就可以修改对象的属性。所以前面的代码还有第三种写法:

o.__dict__["bar"] = 1   # 等价于setattr(o, "bar", 1)
del o.__dict__["bar"]   # 等价与delattr(o, "bar")

Python的内建类型,比如int、str或者object是没有这个成员变量的,因此这些类型的对象是不能动态添加属性的。既然可以对象的属性可以动态增加,那么把一个函数赋值给o的一个成员变量,是不是就成了一个方法了呢?

def func():
    print("hello")

o.say = func
o.say()

看起来好像问题已经解决了,不过其实没有那么简单。在类的成员方法中,第一个参数都是self(比如前面的do方法),通过它可以访问对象的成员变量,相当于C++的this。但是这里的func()函数并没有self参数,因此也没有办法引用成员变量。我们试试给func()添加一个self参数:

def func(self):
    print("hello")

再把它设置为o的属性:

>>> o.say = func
>>> o.say()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: func() takes exactly 1 argument (0 given)

这次直接抛出异常,告诉我们func()函数缺少一个参数。而对于正常的成员方法,调用的时候是不需要显式的传递self参数的,这说明func()函数距离真正成为成员函数还差了那么一丢丢。用type()函数查看一下类型,可以发现do和say原来是不同的类型:

>>> type(o.do)
<type 'instancemethod'>
>>> type(o.say)
<type 'function'>

看起来只要把func转换instancemethod类型就可以了。Python的types包中提供了MethodType来完成这种转换[1]

>>> from types import MethodType
>>> o.say = MethodType(func, o, Foo)
>>> o.say()
hello

到这里我们的目的已经达到了。不过我还想补充一点儿关于开放类的内容。《松本行弘的程序世界》第6章中讲解了Ruby的开放类。在Ruby中,已经声明的类可以在代码中动态的修改,可以添加和删除方法,或者给方法起别名。这种方法很有用,比如RoR里就通过AcitveSupport对Ruby的基本类型进行了扩展,所以可以像下面这样写代码:

2.weeks.ago

Python中其实也有类似的机制。在Python里,一切皆对象。所谓的类,也不过是一种特殊类型的对象罢了。先看看上面的Foo类,其实也有__dict__属性:

>>> Foo.__dict__
dict_proxy({'__dict__': <attribute '__dict__' of 'Foo' objects>, '__module__': 'test', '__weakref__': <attribute '__weakref__' of 'Foo' objects>, '__doc__': None})

不过这个__dict__的类型有点不一样,不是字典,而是dict_proxy,这个我们暂时不讨论,只要知道这里的__dict__是不能修改的就好了。

>>> Foo.__dict__['a'] = 1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'dictproxy' object does not support item assignment

虽然这种方式不能用了,但是setattr()和Foo.bar = 1这两种写法还是可以的,因此类的属性也是可以动态增加的。动态增加Foo类的属性,是否会影响已经生成的Foo类对象呢?

>>> o = Foo()
>>> Foo.bar = 1
>>> o.bar
1
>>> Foo.bar = 2
>>> o.bar
2

答案是肯定的。反过来,修改对象o的属性,是否会影响到Foo呢?接着前面的代码继续执行:

>>> o.bar = 3
>>> Foo.bar
2
>>> Foo.bar = 4
>>> o.bar

修改对象o的bar属性不会影响类Foo的bar属性,并且当o的bar属性被修改之后,再次修改Foo.bar,就不会对o.bar产生影响了。这有点类似与copy-on-write机制。在没有修改o.bar的时候,对象o其实是共享了Foo类的bar属性,因此修改Foo.bar,o.bar也随之改变。一旦修改了o.bar之后,就创建了一个bar属性的副本,再修改Foo.bar就不会影响o.bar了。其实只要把整个过程中对象o的__dict__属性打印出来就很清楚了:

>>> Foo.bar = 2
>>> o.__dict__
{}
>>> o.bar = 3
>>> o.__dict__
{'bar': 3}
>>> Foo.bar = 4
>>> o.__dict__
{'bar': 3}

给Foo类增加bar属性的时候,o.__dict__是空。这是打印o.bar,由于o本身没有bar属性,就会找到Foo类的bar属性。而对o.bar进行了修改之后,o.__dict__里就多了一个key为“bar”的项目,也就是给o本身增加了一个bar属性,此后再打印o.bar,访问的就是这个属性,而不是Foo.bar了。

前面给类添加了成员变量,要添加方法也是一样的,先用types.MethodType()将函数转换成instancemethod类型,然后复制给某个属性。给类添加方法时,MethodType()的第二个参数可以填None,不指定任何对象。

总结一下,Python还是很灵活的,不过美中不足的是,前面讲的东西仅仅适用于自定义类型。对于Python的内建类型,比如int、str和object,不论是类型本身还是对象,都是不能动态修改的。所以Python也没有办法像前面RoR那么灵活的去编程。另外,这种灵活性其实是双刃剑,滥用的话会让代码变得很难读,因为读者找不到某些类或者对象的属性都是在哪里添加的,也就很难理解代码。


  1. 这个方法是在https://segmentfault.com/q/1010000004087006看到的,里面还提供另一种使用partial的实现方式,这里暂不讨论。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341

推荐阅读更多精彩内容

  • 1.元类 1.1.1类也是对象 在大多数编程语言中,类就是一组用来描述如何生成一个对象的代码段。在Python中这...
    TENG书阅读 1,240评论 0 3
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,566评论 18 139
  • 前言 人生苦多,快来 Kotlin ,快速学习Kotlin! 什么是Kotlin? Kotlin 是种静态类型编程...
    任半生嚣狂阅读 26,125评论 9 118
  • 我是一个全职妈妈,带了两个娃,并拒绝老人们的帮助,因为,我不希望娃成为智能玩具,也不希望娃被惯的人嫌狗弃,所以,宁...
    青杏路涂阅读 122评论 4 7
  • 今天,和我家两个小侄女分享思维导图这个工具。 她们俩的性格特质很明显,一个是I,一个是S。 我分享完之后让她们画思...
    邓男神Sweety阅读 500评论 0 0