美文网首页
Python里的原型编程

Python里的原型编程

作者: FunFeast | 来源:发表于2017-04-08 13:00 被阅读314次

    最近在看《松本行弘的程序世界》,其中讲到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的实现方式,这里暂不讨论。

    相关文章

      网友评论

          本文标题:Python里的原型编程

          本文链接:https://www.haomeiwen.com/subject/vzjlattx.html