作者: Jim Wang 公众号: 巴博萨船长

Python 是动态的编程语言

在自己有关Python的第一篇文章中就介绍了Python的特性与优势,在文章中也介绍了其是一种动态的编程语言。在这里我再简单在当前文章中介绍一下,如下:

能够在运行时修改自身程序结构的语言,就属于动态语言。那怎样才算是“运行时修改自身程序结构”捏?比如下面这几个例子都 算:在运行时给某个类增加成员函数及成员变量;在运行时改变某个类的父类;在运行时创建出某个函数.。 Python是可以实现动态类的创建类(在任意代码位置,符合正确的书写格式),或者给类增加删除属性。因为类也是对象,你可以在运行时动态的创建它们,就像其他任何对象一样。比如,你可以在函数中创建类,使用class关键字即可。” 既然是一种动态的高级语言,那么就意味着,其是在运行时就能改变其结构的编程语言。这在调试代码时,可以动态实时地改变变量,重写函数等,比起传统的C++与广泛运用的Java具有很强的优势。所叙述的结构包括:函数、变量与对象。已有的函数或方法可以被删除或是其他结构上的变化。

任务描述

最近在工作中遇到了一个问题:现有一Wxpython的Widget类,即继承TextCtrl的自定义类,该类有一个实例方法,在这暂且就称作method_x吧,整个项目都在使用该实例方法,虽然其内在调用关系十分复杂,这也是我一直诟病我们项目代码的地方,但其一直表现良好。我的需求是:在不改变这个类的,也不影响其他代码的的情况下,重写或者重新定义这个方法。这些只并且仅对特定实例有效。

首先我能想到的就是继承该类,进而得到一个子类,在子类里面仅Override覆盖该实例方法。这应该是最正常的方法,或者大部分人能想到的方法。可是我又不希望这样做,这样在后期维护的该类的时候还要为子类针对一下改变做出调整。而且在文章中还要import一下。那么有没有别的方法呢?考虑到Python是动态语言,动态调整函数和变量都是自己应用过的,那么这样的动态语言,能不能动态地修改(重写)一个类的实例方法呢?抱着这样的想法,我查阅了一些网页,成功实现了这样的想法,也在完成该任务,解决该问题的过程中更深层的理解了Python这种语言的动态特性。

最终我归纳出三种方法,可以实现类的实例方法的动态重写,在此我针对每种方法列出范例代码,并给出运行结果,并对这三种方法进行比较说明,本人并非专家,有见解不足之处,也希望各位高手留言赐教。

方法一

使用函数的_get()__内置方法,也是一个描述符,其本质是一个新式类, 在这个新式类中, 至少实现了___get()_\, __set()__, _delete()__中的一个, 这也被称为描述符协议,其中__get()_\:在调用一个属性时被触发。 __get__(self, instance, owner) 定义当描述器的值被取得的时候的行为, instance 是拥有者对象的一个实例。 owner 是拥有者类本身。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env python
#-*- coding: utf-8 -*-
class Test:
def func1(self):
print "I am old func1"

def new_func1(self):
print "I am new func1"

objTest1 = Test()
print id(new_func1)
objTest2 = Test()
objTest1.func1()
objTest2.func1()

# 仅对该实例重写这个方法
print new_func1.__get__(objTest1, Test)
objTest1.func1 = new_func1.__get__(objTest1, Test)
objTest1.func1()
objTest2.func1()

代码输出如下:

1
2
3
4
5
6
7
C:\\>python test.py
55131384
I am old func1
Iam old func1
<bound method Test.new_func1 of <__main__.Test instance at 0x0000000002F60C88>>
I am new func1
I am old func1

那么现在我们来尝试来理解一下这个方法的原理,上文说到__get()__是一个描述符,或称描述器,描述器分为两种,一共是资料描述器(data-descriptor), 一种是非资料描述器(non-data descriptor)。我们在这里使用的是非资料描述器, __get__(self, instance, owner) ,该方法的第二个参数是可选的(非必需)。 上面代码在使用该描述器的时候,就是将函数new_func1()绑定在类Test的实例objTest1上,而等号前面的是objTest1.func1 意味这,要将新绑定的实例方法覆盖到func1上,进而在再次调用实例方法func1()的时候,其实是在调用new_func1()。 该方法很有好处,可以理解为”几乎”是实现了一个方法的覆盖,你可以在新定义的方法中使用关键字self。 但是不能使用关键字super,即不能访问类的父类。

方法二

使用Python的偏函数,Python的偏函数,与数学意义上的偏函数不同。其偏函数的思想可以理解为绑定了一部分参数的函数,其作用就是减少传入函数,使函数更短,更简洁。在这儿使用偏函数重写实例方法,目的就在与保证第一个参数self的传入。也就是partial()的第二个参数。把new_func1()的第一个参数固定了下来。保证重写的实例方法能访问self关键字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from functools import partial

class Test:
def __init__(self):
self.m = "message"
def func1(self):
print "I am old func1"

def new_func1(self):
print self.m
print "I am new func1"

objTest1 = Test()

print id(new_func1)

objTest2 = Test()

objTest1.func1()

objTest2.func1()

# 仅对该实例重写这个方法
#print new_func1.__get__(objTest1, Test)
#objTest1.func1 = new_func1.__get__(objTest1, Test)

print id(objTest1.func1)
objTest1.func1 = partial(new_func1, objTest1)
print id(objTest1.func1)

objTest1.func1()

objTest2.func1()

输出结果为:

1
2
3
4
5
6
7
8
9
C:\\>python test.py
52313336
I am old func1
I am old func1
47500528
47692840
message
I am new func1
I am old func1

该种方法也简单有效,但是也要从functool中引入partial。也算是一种有效的解决方案。但是其也不能访问关键字super。输出的结果也显示,这样的改变只对特定实例有效。我个人觉得这也是一个简单容易理解的方法,相比较方法一而言,其原理理解起来也并不困难。也不需要更深层次的Python理论知识。

方法三

使用types包中的MethodType 函数。这个方法理解起来就相对更为简单。MethodType可以将一个函数绑定在特定的实例,或者类上。而且该类或者实例,可以没有方法,如果有相同名字的方法,就视为重写。如果绑定在实例上,则完全覆盖其实例的方法。如果绑定在类上,则对所有实例有效。两个绑定方法的区别就在与函数的第二个参数上。 MethodType把方法绑定在类实例上时,每个实例有自己单独的指向区域,互不干扰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import types
#from functools import partial

class Test:
def __init__(self):
self.m = "message"
def func1(self):
print "I am old func1"

def new_func1(self):
print self.m
print "I am new func1"

objTest1 = Test()

print id(new_func1)

objTest2 = Test()

objTest1.func1()

objTest2.func1()

print id(objTest1.func1)
# 仅对该实例重写这个方法
#print new_func1.__get__(objTest1, Test)
#objTest1.func1 = new_func1.__get__(objTest1, Test)
#objTest1.func1 = partial(new_func1, objTest1)

objTest1.func1 = types.MethodType(new_func1, objTest1)

print id(objTest1.func1)

objTest1.func1()

objTest2.func1()

输出结果为:

1
2
3
4
5
6
7
8
9
C:\\>python test.py
60046584
I am old func1
I am old func1
55233776
55233776
message
I am new func1
I am old func1

从上面的输出结果来看,时候该方法重写实例方法的时候,实例方法的id也没有改变,这是完全意义上方法重写,使用该方法重写类的实例方法,可以访问类的所有关键字。该方法绑定类方法时有一个很有趣区别,因为并不是当前任务需要,这些内容,我们以后在别的文章中进行讨论。

小结

三种方法以三个不同方向,三种不同的Python背景知识诠释了如何动态的重写(或绑定)一个实例的方法。在分析利弊与结合自己的使用需求,我个人最终选择了方法三, 每种方法都有自己的优缺点,比如方法一就不需要引入别的Python模块。但是至于那个方法是最优的解决方法,那应该是仁者见仁智者见智。这样的经历也是在自己在解决问题的过程中常常经历的。不断地思考探究,也能扎实自己的理论知识,向大拿高手学习的过程中,也为自己在以后遇见问题是,扩展了思考方向。让自己不断拷问自己,同样的需求是不是还有别的解决方案。然后探究,学习,尝试,掌握。 上述内容实自己总结归纳的,总有不足之处,望见谅并指正。

参考目录:

Stackoverflow: https://stackoverflow.com/questions/394770/override-a-method-at-instance-level

CSDN Blog: [https://blog.csdn.net/yuanyangsdo/article/details/60776612](


版权声明:
文章首发于 Jim Wang's blog , 转载文章请务必以超链接形式标明文章出处,作者信息及本版权声明。