博客链接:http://inarrater.com/2016/07/10/pythonadvance5/
7. Function
作为Callable部分第一个别提到,但是最后来分析它。原因其实很简单,我们已经发现前面的很多内容,包括bound method也好,static method也好,甚至operators,本质上都是function。
那么,如何去探究一个Python的Function的属性呢,阅读过前面内容的读者应该很明白了,通过一些简单的代码加上print就可以看到不少东西了。
我们先用dirf来看下一个function对象身上有什么。
def foo(a, b = 10):
print a + b
print dir(foo)
输出结果:
['__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__doc__', '__format__', '__get__', '__getattribute__', '__globals__', '__hash__', '__init__', '__module__', '__name__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'func_closure', 'func_code', 'func_defaults', 'func_dict', 'func_doc', 'func_globals', 'func_name']
不管双下划线开头的部分属性,我们只看对外开放的部分,先来看一看func_name属性吧。
print foo #<function foo at 0x0235A2B0>
foo.func_name = 'abc'
print foo <function abc at 0x0235A2B0>
func_name看上只是用来记录信息的一个名称而已,修改它并不会影响函数对象的调用。
我们再来看下func_defaults属性,看名称它是和默认值相关。
foo(1) #11
print foo.func_defaults #(10,)
foo.func_defaults = (100, )
foo(1) #101
依然是把输出的结果放在代码行的后面以注释的形式给出,我们看到函数对象的func_defaults属性是一个元组,依次列出了所有默认参数。我们可以通过改变这个属性来改变已经被定义了的函数的默认参数。
为了可以看到func_closture的内容,我们构建一个闭包:
def bar(n):
def f(x):
return x + n
return f
f = bar(1)
g = bar("abc")
print f(2) # 3
print g("def") # defabc
print foo.func_closure # None
print f.func_closure # (<cell at 0x029655F0: int object at 0x029378E0>,)
print g.func_closure # (<cell at 0x02A31130: str object at 0x02972AE8>,)
f和g是两个闭包对象,可以看到foo这个函数对象身上的func_closure属性为None,f和g身上分别是两个cell对象,这部分内容和闭包中讲的部分就契合在了一起。
func_code属性我们也来看一下
def bar(a, b):
print a * b
print bar.func_code
print dir(bar.func_code)
foo(6, 2)
foo.func_code = bar.func_code
foo(6, 2)
输出的结果由于比较长,写在了下面:
<code object bar at 02516C80, file "C:\Users\David-PC\Desktop\Advanced Course on Python 2016\a014.py", line 27>
['__class__', '__cmp__', '__delattr__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_stacksize', 'co_varnames']
8
12
我们可以看到,func_code是一个code object,它有的属性有很多,基本以co_开头,如果有兴趣,可以自己一点点去把他们print出来看,也可以发现很多有趣的东西,这里暂时不进行展开。例子的另外一部分展示了func_code是可以被替换的。
扩展:关于code对象,可以阅读Exploring Python Code Objects,了解怎样通过compile方法来生成code object,以及code类的一些属性。
属性名称 | 描述 |
---|---|
func_name | 函数名称 |
func_defaults | 函数的默认值列表 |
func_code | 函数体的code对象 |
func_globals | 函数的全局命名空间 |
func_closure | 函数的cell对象 |
关于function中比较重要的几个属性,以表格的形式总结出来。
属性名称 | 描述 |
---|---|
func_name | 函数名称 |
func_defaults | 函数的默认值列表 |
func_code | 函数体的code对象 |
func_globals | 函数的全局命名空间 |
func_closure | 函数的cell对象 |
看了function的这些属性,对于之前讨论过的hotfix是不是有了更深的了解呢?只需要替换一个函数对象的func_code,func_defaults,func_closure等属性,这个函数的行为就可以被改变了,结合上bound method和unbound method的动态生成的特性,对Python语言的动态性的原理的理解是否有更深入了一步呢?
8. Classes
关于类,其实有很多可以讨论的内容,从生命周期的角度来看,一个C++类的对象包含如下四个生命周期:
- 内存分配,malloc
- 调用构造函数初始化对象,A::A()
- 调用析构函数清理对象,A::~A()
- 释放内存,free
不同的编译器在内存分配或者释放的时候具体使用的函数可能不同,但这四个步骤是都有的,而且在C++中,1和4两个步骤是隐式的,即开发者通常不需要去关心(当然也有可以去操作的方法),它们分别隐含在了构造函数和析构函数当中。
对于Python的对象来说,其生命周期包含如下三个部分:
- 内存分配,
__new__
方法; - 调用初始化(initializer),
__init__
方法; - 调用终结器(finalizer),
__del__
方法;
对于自定义的类,上述的过程可以通过重载对应的方法来实现对于其过程的控制。可以看到,这里并没有内存释放的过程,也就是说开发者无法主动控制对象所占用过的内存的释放,这一部分是方便开发者不需要进行内存的管理,另外也利于Python语言本身进行对象缓存池的设计与实现。这部分内容在bound method的部分已经看到了缓存池在Python的应用,更多的讨论放在内存管理的部分来进行。
思考:Python中如何实现单例模式?是否可以通过重载
__new__
方法,在每次分配内存的时候返回同一个对象来实现呢?
答案是否定的,因为Python对于生命周期的控制决定了在__new__
方法被调用,内存分配完毕之后会主动调用__init__
方法,这样虽然分配的是同一个对象,但是多次__init__
方法的调用可能会导致对象的属性被修改,可能会引发意料之外的bug,比如已经被修改过属性又被__init__
方法改变等。
说了这些之后,对于Class我们返回Callable的主题,Python中的Class也是一个Callable的对象,调用一个类的结果很简单,就是获得一个类的实例化对象,我们来看一个简单的例子。
class Foo(object):
pass
print Foo # <class '__main__.Foo'>
print Foo.__call__ # <method-wrapper '__call__' of type object at 0x029D33E0>
print Foo() # <__main__.Foo object at 0x02AAEED0>
def func():
pass
print func.__call__ # <method-wrapper '__call__' of function object at 0x029C77F0>
类Foo是一个class对象,它是可以访问到__call__
属性的,它是一个id为0x029D33E0的type对象的'call'方法的method-wrapper,如果打印print id(Foo)
的话,你可以发现它的十六进制结果就是0x029D33E0。这容易理解,而对于method-wrapper,我把它理解为一个方法的封装。查了一些资料,但是没有找到官方的精准答案,这里我猜测对象的创建、函数的执行等过程,在Python中可能是一种C的实现,因此在Python层给出的是一个wrapper,可以让他们的行为像一个Python的函数一样。当然这是我的猜测,如果有知道准确答案的朋友欢迎指导。
思考:Class作为一个Callable的类型,对于我们开发中有什么好处吗?
想象一下工厂方法在C++中的实现,通常需要通过一个函数来封装一个对象的创建过程,而在Python中,我们可以把对象类型存储在一个字典或者列表中,通过映射关系,可以直接生成对象。某种程度上说,这也是一种“反射”机制。具体的代码由于比较简单,此处就不列举了。
总结:关于Callable的部分已经聊得差不多了,从我们分析的过程看,我们通常使用dir和print在加上一些分析能力就可以看出Python语言设计上的一些原理和思路,一切皆对象的理念被应用的淋漓尽致,而为了实现其动态特性,Python语言做了很多特殊的设计和方法。
2016年7月10日于杭州家中