Python 编程思想

这里收集了一些深入 python 机制的内容。


浅谈 Python 的 with 语句

with 语句是从 Python 2.5 开始引入的一种与异常处理相关的功能(2.5 版本中要通过 from __future__ import with_statement 导入后才可以使用),从 2.6 版本开始缺省可用(参考 What’s new in Python 2.6? 中 with 语句相关部分介绍)。with 语句适用于对资源进行访问的场合,确保不管使用过程中是否发生异常都会执行必要的“清理”操作,释放资源,比如文件使用后自动关闭、线程中锁的自动获取和释放等。

术语

要使用 with 语句,首先要明白上下文管理器这一概念。有了上下文管理器,with 语句才能工作。

下面是一组与上下文管理器和with 语句有关的概念。

上下文管理协议(Context Management Protocol):包含方法 __enter__()__exit__(),支持该协议的对象要实现这两个方法。

上下文管理器(Context Manager):支持上下文管理协议的对象,这种对象实现了
__enter__()__exit__() 方法。上下文管理器定义执行 with 语句时要建立的运行时上下文,负责执行 with 语句块上下文中的进入与退出操作。通常使用 with 语句调用上下文管理器,也可以通过直接调用其方法来使用。

运行时上下文(runtime context):由上下文管理器创建,通过上下文管理器的 __enter__()__exit__() 方法实现,__enter__() 方法在语句体执行之前进入运行时上下文,__exit__() 在语句体执行完后从运行时上下文退出。with 语句支持运行时上下文这一概念。

上下文表达式(Context Expression):with 语句中跟在关键字 with 之后的表达式,该表达式要返回一个上下文管理器对象。

语句体(with-body):with 语句包裹起来的代码块,在执行语句体之前会调用上下文管
理器的 __enter__() 方法,执行完语句体之后会执行 __exit__() 方法。

基本语法和工作原理

with 语句的语法格式如下:

with context_expression [as target(s)]:
with-body

这里 context_expression 要返回一个上下文管理器对象,该对象并不赋值给 as 子句中的 target(s) ,如果指定了 as 子句的话,会将上下文管理器的 __enter__() 方法的返回值赋值给 target(s)。target(s) 可以是单个变量,或者由“()”括起来的元组(不能是仅仅由“,”分隔的变量列表,必须加“()”)。

Python 对一些内建对象进行改进,加入了对上下文管理器的支持,可以用于 with 语句中,比如可以自动关闭文件、线程锁的自动获取和释放等。假设要对一个文件进行操作,使用 with 语句可以有如下代码:

with open(r'somefileName') as somefile:
for line in somefile:
print line
# ...more code

这里使用了 with 语句,不管在处理文件过程中是否发生异常,都能保证 with 语句执行完毕后已经关闭了打开的文件句柄。如果使用传统的 try/finally 范式,则要使用类似如下代码:

somefile = open(r'somefileName')
try:
for line in somefile:
print line
# ...more code
finally:
somefile.close()

比较起来,使用 with 语句可以减少编码量。已经加入对上下文管理协议支持的还有模块 threading、decimal 等。

PEP 0343 对 with 语句的实现进行了描述。with 语句的执行过程类似如下代码块:

context_manager = context_expression
exit = type(context_manager).__exit__
value = type(context_manager).__enter__(context_manager)
exc = True # True 表示正常执行,即便有异常也忽略;False 表示重新抛出异常,需要对异常进行处理
try:
try:
target = value # 如果使用了 as 子句
with-body # 执行 with-body
except:
# 执行过程中有异常发生
exc = False
# 如果 __exit__ 返回 True,则异常被忽略;如果返回 False,则重新抛出异常
# 由外层代码对异常进行处理
if not exit(context_manager, *sys.exc_info()):
raise
finally:
# 正常退出,或者通过 statement-body 中的 break/continue/return 语句退出
# 或者忽略异常退出
if exc:
exit(context_manager, None, None, None)
# 缺省返回 None,None 在布尔上下文中看做是 False
  1. 执行 context_expression,生成上下文管理器 context_manager
  2. 调用上下文管理器的 __enter__() 方法;如果使用了 as 子句,则将 __enter__() 方法的返回值赋值给 as 子句中的 target(s)
  3. 执行语句体 with-body
  4. 不管是否执行过程中是否发生了异常,执行上下文管理器的 __exit__() 方法,__exit__() 方法负责执行“清理”工作,如释放资源等。如果执行过程中没有出现异常,或者语句体中执行了语句 break/continue/return,则以 None 作为参数调用 __exit__(None, None, None) ;如果执行过程中出现异常,则使用 sys.exc_info 得到的异常信息为参数调用 __exit__(exc_type, exc_value, exc_traceback)
  5. 出现异常时,如果 __exit__(type, value, traceback) 返回 False,则会重新抛出异常,让with 之外的语句逻辑来处理异常,这也是通用做法;如果返回 True,则忽略异常,不再对异常进行处理

Python程序的执行原理

Python先把代码(.py文件)编译成字节码,交给字节码虚拟机,然后虚拟机一条一条执行字节码指令,从而完成程序的执行。

字节码在Python虚拟机程序里对应的是PyCodeObject对象。

.pyc文件是字节码在磁盘上的表现形式。

PyCodeObject对象的创建时机是模块加载的时候,即import。

Python test.py会对test.py进行编译成字节码并解释执行,但是不会生成test.pyc。

如果test.py加载了其他模块,如import util,Python会对util.py进行编译成字节码,生成util.pyc,然后对字节码解释执行。

如果想生成test.pyc,我们可以使用Python内置模块py_compile来编译。

加载模块时,如果同时存在.py和.pyc,Python会尝试使用.pyc,如果.pyc的编译时间早于.py的修改时间,则重新编译.py并更新.pyc。

Python代码的编译结果就是PyCodeObject对象。

typedef struct {
PyObject_HEAD
int co_argcount; /* 位置参数个数 */
int co_nlocals; /* 局部变量个数 */
int co_stacksize; /* 栈大小 */
int co_flags;
PyObject *co_code; /* 字节码指令序列 */
PyObject *co_consts; /* 所有常量集合 */
PyObject *co_names; /* 所有符号名称集合 */
PyObject *co_varnames; /* 局部变量名称集合 */
PyObject *co_freevars; /* 闭包用的的变量名集合 */
PyObject *co_cellvars; /* 内部嵌套函数引用的变量名集合 */
/* The rest doesn’t count for hash/cmp */
PyObject *co_filename; /* 代码所在文件名 */
PyObject *co_name; /* 模块名|函数名|类名 */
int co_firstlineno; /* 代码块在文件中的起始行号 */
PyObject *co_lnotab; /* 字节码指令和行号的对应关系 */
void *co_zombieframe; /* for optimization only (see frameobject.c) */
} PyCodeObject;

加载模块时,模块对应的PyCodeObject对象被写入.pyc文件,格式如下:

分析字节码

Python提供了内置函数compile可以编译Python代码和查看PyCodeObject对象,如下:

s = ”hello”
def func():
print s
func()

在Python交互式shell里编译代码得到PyCodeObject对象:

dir(co)已经列出co的各个域,想查看某个域直接在终端输出即可:

test.py的PyCodeObject

co.co_argcount 0
co.co_nlocals 0
co.co_names (‘s’, ’func’)
co.co_varnames (‘s’, ’func’)
co.co_consts (‘hello’, <code object func at 0x2aaeeec57110, file ”test.py”, line 3>, None)
co.co_code ’d\x00\x00Z\x00\x00d\x01\x00\x84\x00\x00Z\x01\x00e\x01\x00\x83\x00\x00\x01d\x02\x00S’

Python解释器会为函数也生成的字节码PyCodeObject对象,见上面的co_consts[1]

func的PyCodeObject

func.co_argcount 0
func.co_nlocals 0
func.co_names (‘s’,)
func.co_varnames ()
func.co_consts (None,)
func.co_code ‘t\x00\x00GHd\x00\x00S’

执行字节码

Python虚拟机的原理就是模拟可执行程序再X86机器上的运行,X86的运行时栈帧如下图:

假如test.py用C语言来实现,会是下面这个样子:

const char *s = “hello”;
void func() {
printf(“%s\n”, s);
}
int main() {
func();
return 0;
}

Python虚拟机的原理就是模拟上述行为。当发生函数调用时,创建新的栈帧,对应Python的实现就是PyFrameObject对象。

typedef struct _frame {
PyObject_VAR_HEAD
struct _frame *f_back; /* 调用者的帧 */
PyCodeObject *f_code; /* 帧对应的字节码对象 */
PyObject *f_builtins; /* 内置名字空间 */
PyObject *f_globals; /* 全局名字空间 */
PyObject *f_locals; /* 本地名字空间 */
PyObject **f_valuestack; /* 运行时栈底 */
PyObject **f_stacktop; /* 运行时栈顶 */
…….
}

那么对应Python的运行时栈就是这样子:

执行test.py的字节码时,会先创建一个栈帧,以下用f表示当前栈帧,执行过程注释如下:

test.py的符号名集合和常量集合

co.co_names (‘s’, ’func’)
co.co_consts (‘hello’, <code object func at 0x2aaeeec57110, file ”test.py”, line 3>, None)

如果你想查看当前栈帧,Python提供了sys._getframe()方法可以获取当前栈帧,你只需要在代码里加入代码如下:

def func():
import sys
frame = sys._getframe()
print frame.f_locals
print frame.f_globals
print frame.f_back.f_locals
#你可以打印frame的各个域
print s

Python 黑魔法

Python 多继承

In [1]: class A(object):
...: def foo(self):
...: print("class A")
...:
In [2]: class B(object):
...: def foo(self):
...: print("class B")
...:
In [3]: class C(A, B):
...: pass
...:
In [4]: C().foo()
class A # 例子很好懂, C继承了A和B,从左到右,发现A有foo方法,返回了

看起来都是很简单, 有次序的从底向上,从前向后找,找到就返回. 再看例子:

In [5]: class A(object):
...: def foo(self):
...: print("class A")
...:
In [6]: class B(A):
...: pass
...:
In [7]: class C(A):
...: def foo(self):
...: print("class C")
...:
In [8]: class D(B,C):
...: pass
...:
In [9]: D().foo()
class C # ? 按道理, 顺序是 D->B->A,为什么找到了C哪去了

这也就涉及了MRO(Method Resolution Order):

In [10]: D.__mro__
Out[10]: (__main__.D, __main__.B, __main__.C, __main__.A, object)

简单的理解其实就是新式类是广度优先了, D->B, 但是发现C也是继承A,就先找C,最后再去找A

列表的+和+=, append和extend

In [17]: print('ID:', id(a_list))
('ID:', 4481323592)
In [18]: a_list += [1]
In [19]: print('ID (+=):', id(a_list))
('ID (+=):', 4481323592) # 使用+= 还是在原来的列表上操作
In [20]: a_list = a_list + [2]
In [21]: print('ID (list = list + ...):', id(a_list))
('ID (list = list + ...):', 4481293056) # 简单的+其实已经改变了原有列表
In [28]: a_list = []
In [29]: id(a_list)
Out[29]: 4481326976
In [30]: a_list.append(1)
In [31]: id(a_list)
Out[31]: 4481326976 # append 是在原有列表添加
In [32]: a_list.extend([2])
In [33]: id(a_list)
Out[33]: 4481326976 # extend 也是在原有列表上添加

datetime也有布尔值

In [34]: import datetime
In [35]: print('"datetime.time(0,0,0)" (Midnight) ->', bool(datetime.time(0,0,0)))
('"datetime.time(0,0,0)" (Midnight) ->', False)
In [36]: print('"datetime.time(1,0,0)" (1 am) ->', bool(datetime.time(1,0,0)))
('"datetime.time(1,0,0)" (1 am) ->', True)

’==’ 和 is 的区别

我的理解是”is”是判断2个对象的身份, ==是判断2个对象的值

In [37]: a = 1
In [38]: b = 1
In [39]: print('a is b', bool(a is b))
('a is b', True)
In [40]: c = 999
In [41]: d = 999
In [42]: print('c is d', bool(c is d))
('c is d', False) # 原因是python的内存管理,缓存了-5 - 256的对象
In [43]: print('256 is 257-1', 256 is 257-1)
('256 is 257-1', True)
In [44]: print('257 is 258-1', 257 is 258 - 1)
('257 is 258-1', False)
In [45]: print('-5 is -6+1', -5 is -6+1)
('-5 is -6+1', True)
In [46]: print('-7 is -6-1', -7 is -6-1)
('-7 is -6-1', False)
In [47]: a = 'hello world!'
In [48]: b = 'hello world!'
In [49]: print('a is b,', a is b)
('a is b,', False) # 很明显 他们没有被缓存,这是2个字段串的对象
In [50]: print('a == b,', a == b)
('a == b,', True) # 但他们的值相同
# But, 有个特例
In [51]: a = float('nan')
In [52]: print('a is a,', a is a)
('a is a,', True)
In [53]: print('a == a,', a == a)
('a == a,', False) # 亮瞎我眼睛了~

bool其实是int的子类

In [97]: isinstance(True, int)
Out[97]: True
In [98]: True + True
Out[98]: 2
In [99]: 3 * True + True
Out[99]: 4
In [100]: 3 * True - False
Out[100]: 3
In [104]: True << 10
Out[104]: 1024

元组是不是真的不可变?

In [111]: tup = ([],)
In [112]: tup[0] += [1]
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-112-d4f292cf35de> in <module>()
----> 1 tup[0] += [1]
TypeError: 'tuple' object does not support item assignment
In [113]: tup
Out[113]: ([1],) # 我靠 又是亮瞎我眼睛,明明抛了异常 还能修改?
In [114]: tup = ([],)
In [115]: tup[0].extend([1])
In [116]: tup[0]
Out[116]: [1] # 好吧,我有点看明白了, 虽然我不能直接操作元组,但是不能阻止我操作元组中可变的子对象(list)

+= 是一个特例

In [117]: my_tup = (1,)
In [118]: my_tup += (4,)
In [119]: my_tup = my_tup + (5,)
In [120]: my_tup
Out[120]: (1, 4, 5) # ? 嗯 不是不能操作元组嘛?
In [121]: my_tup = (1,)
In [122]: print(id(my_tup))
4481317904
In [123]: my_tup += (4,)
In [124]: print(id(my_tup))
4480606864 # 操作的不是原来的元组 所以可以
In [125]: my_tup = my_tup + (5,)
In [126]: print(id(my_tup))
4474234912

异常处理加 else

In [150]: try:
.....: print('third element:', a_list[2])
.....: except IndexError:
.....: print('raised IndexError')
.....: else:
.....: print('no error in try-block') # 只有在try里面没有异常的时候才会执行else里面的表达式
.....:
raised IndexError # 抛异常了 没完全完成
In [153]: i = 0
In [154]: while i < 2:
.....: print(i)
.....: i += 1
.....: else:
.....: print('in else')
.....:
0
1
in else # while也支持哦~
In [155]: i = 0
In [156]: while i < 2:
.....: print(i)
.....: i += 1
.....: break
.....: else:
.....: print('completed while-loop')
.....:
0 # 被break了 没有完全执行完 就不执行else里面的了
In [158]: for i in range(2):
.....: print(i)
.....: else:
.....: print('completed for-loop')
.....:
0
1
completed for-loop
In [159]: for i in range(2):
.....: print(i)
.....: break
.....: else:
.....: print('completed for-loop')
.....:
0 # 也是因为break了

深拷贝与浅拷贝

对象赋值

Python中,对象的赋值,拷贝(深/浅拷贝)之间是有差异的,如果使用的时候不注意,就可能产生意外的结果。

直接看一段代码

will = ["Will", 28, ["Python", "C#", "JavaScript"]]
wilber = will
print id(will)
print will
print [id(ele) for ele in will]
print id(wilber)
print wilber
print [id(ele) for ele in wilber]
will[0] = "Wilber"
will[2].append("CSS")
print id(will)
print will
print [id(ele) for ele in will]
print id(wilber)
print wilber
print [id(ele) for ele in wilber]

tip1

下面来分析一下这段代码:

  • 首先,创建了一个名为will的变量,这个变量指向一个list对象,从第一张图中可以看到所有对象的地址(每次运行,结果可能不同)
  • 然后,通过will变量对wilber变量进行赋值,那么wilber变量将指向will变量对应的对象(内存地址),也就是说”wilber is will”,”wilber[i] is will[i]”
  • 可以理解为,Python中,对象的赋值都是进行对象引用(内存地址)传递

第三张图中,由于will和wilber指向同一个对象,所以对will的任何修改都会体现在wilber上
这里需要注意的一点是,str是不可变类型,所以当修改的时候会替换旧的对象,产生一个新的地址39758496

浅拷贝

下面就来看看浅拷贝的结果:

import copy
will = ["Will", 28, ["Python", "C#", "JavaScript"]]
wilber = copy.copy(will)
print id(will)
print will
print [id(ele) for ele in will]
print id(wilber)
print wilber
print [id(ele) for ele in wilber]
will[0] = "Wilber"
will[2].append("CSS")
print id(will)
print will
print [id(ele) for ele in will]
print id(wilber)
print wilber
print [id(ele) for ele in wilber]

代码结果为:

分析一下这段代码:

  • 首先,依然使用一个will变量,指向一个list类型的对象
  • 然后,通过copy模块里面的浅拷贝函数copy(),对will指向的对象进行浅拷贝,然后浅拷贝生成的新对象赋值给wilber变量
    • 浅拷贝会创建一个新的对象,这个例子中”wilber is not will”
    • 但是,对于对象中的元素,浅拷贝就只会使用原始元素的引用(内存地址),也就是说”wilber[i] is will[i]”
  • 当对will进行修改的时候
    • 由于list的第一个元素是不可变类型,所以will对应的list的第一个元素会使用一个新的对象39758496
    • 但是list的第三个元素是一个可变类型,修改操作不会产生新的对象,所以will的修改结果会相应的反应到wilber上

总结一下,当我们使用下面的操作的时候,会产生浅拷贝的效果:

  • 使用切片[:]操作
  • 使用工厂函数(如list/dir/set)
  • 使用copy模块中的copy()函数

深拷贝

最后来看看深拷贝:

import copy
will = ["Will", 28, ["Python", "C#", "JavaScript"]]
wilber = copy.deepcopy(will)
print id(will)
print will
print [id(ele) for ele in will]
print id(wilber)
print wilber
print [id(ele) for ele in wilber]
will[0] = "Wilber"
will[2].append("CSS")
print id(will)
print will
print [id(ele) for ele in will]
print id(wilber)
print wilber
print [id(ele) for ele in wilber]

代码的结果为:

分析一下这段代码:

  • 首先,同样使用一个will变量,指向一个list类型的对象
  • 然后,通过copy模块里面的深拷贝函数deepcopy(),对will指向的对象进行深拷贝,然后深拷贝生成的新对象赋值给wilber变量
    • 跟浅拷贝类似,深拷贝也会创建一个新的对象,这个例子中”wilber is not will”
    • 但是,对于对象中的元素,深拷贝都会重新生成一份(有特殊情况,下面会说明),而不是简单的使用原始元素的引用(内存地址)
    • 例子中will的第三个元素指向39737304,而wilber的第三个元素是一个全新的对象39773088,也就是说,”wilber[2] is not will[2]”
  • 当对will进行修改的时候
    • 由于list的第一个元素是不可变类型,所以will对应的list的第一个元素会使用一个新的对象39758496
    • 但是list的第三个元素是一个可变类型,修改操作不会产生新的对象,但是由于”wilber[2] is not will[2]”,所以will的修改不会影响wilber

拷贝的特殊情况

其实,对于拷贝有一些特殊情况:

  • 对于非容器类型(如数字、字符串、和其他’原子’类型的对象)没有拷贝这一说
    • 也就是说,对于这些类型,”obj is copy.copy(obj)” 、”obj is copy.deepcopy(obj)”
  • 如果元组变量只包含原子类型对象,则不能深拷贝,看下面的例子

总结

本文介绍了对象的赋值和拷贝,以及它们之间的差异:

  • Python中对象的赋值都是进行对象引用(内存地址)传递
  • 使用copy.copy(),可以进行对象的浅拷贝,它复制了对象,但对于对象中的元素,依然使用原始的引用.
  • 如果需要复制一个容器对象,以及它里面的所有元素(包含元素的子元素),可以使用copy.deepcopy()进行深拷贝
  • 对于非容器类型(如数字、字符串、和其他’原子’类型的对象)没有被拷贝一说
  • 如果元组变量只包含原子类型对象,则不能深拷贝,看下面的例子

Python 中 self 的含义

刚开始学习Python的类写法的时候觉得很是麻烦,为什么定义时需要而调用时又不需要,为什么不能内部简化从而减少我们敲击键盘的次数?

self代表类的实例,而非类。

实例来说明

class Test:
def prt(self):
print(self)
print(self.__class__)
t = Test()
t.prt()

执行结果如下

<__main__.Test object at 0x000000000284E080>
<class '__main__.Test'>

从上面的例子中可以很明显的看出,self代表的是类的实例。而self.class则指向类。

self不必非写成self

有很多童鞋是先学习别的语言然后学习Python的,所以总觉得self怪怪的,想写成this,可以吗?

当然可以,还是把上面的代码改写一下。

class Test:
def prt(this):
print(this)
print(this.__class__)
t = Test()
t.prt()

改成this后,运行结果完全一样。

当然,最好还是尊重约定俗成的习惯,使用self。

self可以不写吗

在Python的解释器内部,当我们调用t.prt()时,实际上Python解释成Test.prt(t),也就是说把self替换成类的实例。

有兴趣的童鞋可以把上面的t.prt()一行改写一下,运行后的实际结果完全相同。

实际上已经部分说明了self在定义时不可以省略,如果非要试一下,那么请看下面:

class Test:
def prt():
print(self)
t = Test()
t.prt()

运行时提醒错误如下:prt在定义时没有参数,但是我们运行时强行传了一个参数。
由于上面解释过了t.prt()等同于Test.prt(t),所以程序提醒我们多传了一个参数t。

Traceback (most recent call last):
File "h.py", line 6, in <module>
t.prt()
TypeError: prt() takes 0 positional arguments but 1 was given

当然,如果我们的定义和调用时均不传类实例是可以的,这就是类方法。

class Test:
def prt():
print(__class__)
Test.prt()

运行结果如下

<class '__main__.Test'>

在继承时,传入的是哪个实例,就是那个传入的实例,而不是指定义了self的类的实例。
先看代码

class Parent:
def pprt(self):
print(self)
class Child(Parent):
def cprt(self):
print(self)
c = Child()
c.cprt()
c.pprt()
p = Parent()
p.pprt()

运行结果如下

<__main__.Child object at 0x0000000002A47080>
<__main__.Child object at 0x0000000002A47080>
<__main__.Parent object at 0x0000000002A47240>

解释:

运行c.cprt()时应该没有理解问题,指的是Child类的实例。

但是在运行c.pprt()时,等同于Child.pprt(c),所以self指的依然是Child类的实例,由于self中没有定义pprt()方法,所以沿着继承树往上找,发现在父类Parent中定义了pprt()方法,所以就会成功调用。

在描述符类中,self指的是描述符类的实例

不太容易理解,先看实例:

class Desc:
def __get__(self, ins, cls):
print('self in Desc: %s ' % self )
print(self, ins, cls)
class Test:
x = Desc()
def prt(self):
print('self in Test: %s' % self)
t = Test()
t.prt()
t.x

运行结果如下:

self in Test: <__main__.Test object at 0x0000000002A570B8>
self in Desc: <__main__.Desc object at 0x000000000283E208>
<__main__.Desc object at 0x000000000283E208> <__main__.Test object at 0x0000000002A570B8> <class '__main__.Test'>

大部分童鞋开始有疑问了,为什么在Desc类中定义的self不是应该是调用它的实例t吗?怎么变成了Desc类的实例了呢?

注意:此处需要睁大眼睛看清楚了,这里调用的是t.x,也就是说是Test类的实例t的属性x,由于实例t中并没有定义属性x,所以找到了类属性x,而该属性是描述符属性,为Desc类的实例而已,所以此处并没有顶用Test的任何方法。

那么我们如果直接通过类来调用属性x也可以得到相同的结果。

下面是把t.x改为Test.x运行的结果。

self in Test: <__main__.Test object at 0x00000000022570B8>
self in Desc: <__main__.Desc object at 0x000000000223E208>
<__main__.Desc object at 0x000000000223E208> None <class '__main__.Test'>

题外话:由于在很多时候描述符类中仍然需要知道调用该描述符的实例是谁,所以在描述符类中存在第二个参数ins,用来表示调用它的类实例,所以t.x时可以看到第三行中的运行结果中第二项为。而采用Test.x进行调用时,由于没有实例,所以返回None。

总结

  • self在定义时需要定义,但是在调用时会自动传入。
  • self的名字并不是规定死的,但是最好还是按照约定是用self
  • self总是指调用时的类的实例。

Python的内存管理

以Python语言为例子,说明一门动态类型的、面向对象的语言的内存管理方式。

对象的内存使用

赋值语句是语言最常见的功能了。但即使是最简单的赋值语句,也可以很有内涵。Python的赋值语句就很值得研究。

a = 1

整数1为一个对象。而a是一个引用。利用赋值语句,引用a指向对象1。Python是动态类型的语言(参考动态类型),对象与引用分离。Python像使用“筷子”那样,通过引用来接触和翻动真正的食物——对象。

为了探索对象在内存的存储,我们可以求助于Python的内置函数id()。它用于返回对象的身份(identity)。其实,这里所谓的身份,就是该对象的内存地址。

a = 1
print(id(a))
print(hex(id(a)))

在我的计算机上,它们返回的是:

11246696
’0xab9c68′

分别为内存地址的十进制和十六进制表示。

在Python中,整数和短小的字符,Python都会缓存这些对象,以便重复使用。当我们创建多个等于1的引用时,实际上是让所有这些引用指向同一个对象。

a = 1
b = 1
print(id(a))
print(id(b))

上面程序返回

11246696
11246696

可见a和b实际上是指向同一个对象的两个引用。

为了检验两个引用指向同一个对象,我们可以用is关键字。is用于判断两个引用所指的对象是否相同。

# True
a = 1
b = 1
print(a is b)

# True
a = "good"
b = "good"
print(a is b)

# False
a = "very good morning"
b = "very good morning"
print(a is b)

# False
a = []
b = []
print(a is b)

上面的注释为相应的运行结果。可以看到,由于Python缓存了整数和短字符串,因此每个对象只存有一份。比如,所有整数1的引用都指向同一对象。即使使用赋值语句,也只是创造了新的引用,而不是对象本身。长的字符串和其它对象可以有多个相同的对象,可以使用赋值语句创建出新的对象。

在Python中,每个对象都有存有指向该对象的引用总数,即引用计数(reference count)。

我们可以使用sys包中的getrefcount(),来查看某个对象的引用计数。需要注意的是,当使用某个引用作为参数,传递给getrefcount()时,参数实际上创建了一个临时的引用。因此,getrefcount()所得到的结果,会比期望的多1。

from sys import getrefcount
a = [1, 2, 3]
print(getrefcount(a))
b = a
print(getrefcount(b))

由于上述原因,两个getrefcount将返回2和3,而不是期望的1和2。

Python的一个容器对象(container),比如表、词典等,可以包含多个对象。实际上,容器对象中包含的并不是元素对象本身,是指向各个元素对象的引用。

我们也可以自定义一个对象,并引用其它对象:

class from_obj(object):
def __init__(self, to_obj):
self.to_obj = to_obj
b = [1,2,3]
a = from_obj(b)
print(id(a.to_obj))
print(id(b))

可以看到,a引用了对象b。

对象引用对象,是Python最基本的构成方式。即使是a = 1这一赋值方式,实际上是让词典的一个键值”a”的元素引用整数对象1。该词典对象用于记录所有的全局引用。该词典引用了整数对象1。我们可以通过内置函数globals()来查看该词典。

当一个对象A被另一个对象B引用时,A的引用计数将增加1。

from sys import getrefcount
a = [1, 2, 3]
print(getrefcount(a))
b = [a, a]
print(getrefcount(a))

由于对象b引用了两次a,a的引用计数增加了2。

容器对象的引用可能构成很复杂的拓扑结构。我们可以用objgraph包来绘制其引用关系,比如

x = [1, 2, 3]
y = [x, dict(key1=x)]
z = [y, (x, y)]
import objgraph
objgraph.show_refs([z], filename='ref_topo.png')

objgraph是Python的一个第三方包。安装之前需要安装xdot。

sudo apt-get install xdot
sudo pip install objgraph

两个对象可能相互引用,从而构成所谓的引用环(reference cycle)。

a = []
b = [a]
a.append(b)

即使是一个对象,只需要自己引用自己,也能构成引用环。

a = []
a.append(a)
print(getrefcount(a))

引用环会给垃圾回收机制带来很大的麻烦,我将在后面详细叙述这一点。

某个对象的引用计数可能减少。比如,可以使用del关键字删除某个引用:

from sys import getrefcount
a = [1, 2, 3]
b = a
print(getrefcount(b))
del a
print(getrefcount(b))

del也可以用于删除容器元素中的元素,比如:

a = [1,2,3]
del a[0]
print(a)

如果某个引用指向对象A,当这个引用被重新定向到某个其他对象B时,对象A的引用计数减少:

from sys import getrefcount
a = [1, 2, 3]
b = a
print(getrefcount(b))
a = 1
print(getrefcount(b))

垃圾回收

吃太多,总会变胖,Python也是这样。当Python中的对象越来越多,它们将占据越来越大的内存。不过你不用太担心Python的体形,它会乖巧的在适当的时候“减肥”,启动垃圾回收(garbage collection),将没用的对象清除。在许多语言中都有垃圾回收机制,比如Java和Ruby。尽管最终目的都是塑造苗条的提醒,但不同语言的减肥方案有很大的差异。

从基本原理上,当Python的某个对象的引用计数降为0时,说明没有任何引用指向该对象,该对象就成为要被回收的垃圾了。比如某个新建对象,它被分配给某个引用,对象的引用计数变为1。如果引用被删除,对象的引用计数为0,那么该对象就可以被垃圾回收。比如下面的表:

a = [1, 2, 3]
del a

del a后,已经没有任何引用指向之前建立的[1, 2, 3]这个表。用户不可能通过任何方式接触或者动用这个对象。这个对象如果继续待在内存里,就成了不健康的脂肪。当垃圾回收启动时,Python扫描到这个引用计数为0的对象,就将它所占据的内存清空。

然而,减肥是个昂贵而费力的事情。垃圾回收时,Python不能进行其它的任务。频繁的垃圾回收将大大降低Python的工作效率。如果内存中的对象不多,就没有必要总启动垃圾回收。所以,Python只会在特定条件下,自动启动垃圾回收。当Python运行时,会记录其中分配对象(object allocation)和取消分配对象(object deallocation)的次数。当两者的差值高于某个阈值时,垃圾回收才会启动。

我们可以通过gc模块的get_threshold()方法,查看该阈值:

import gc
print(gc.get_threshold())

返回(700, 10, 10),后面的两个10是与分代回收相关的阈值,后面可以看到。700即是垃圾回收启动的阈值。可以通过gc中的set_threshold()方法重新设置。

我们也可以手动启动垃圾回收,即使用gc.collect()。

Python同时采用了分代(generation)回收的策略。这一策略的基本假设是,存活时间越久的对象,越不可能在后面的程序中变成垃圾。我们的程序往往会产生大量的对象,许多对象很快产生和消失,但也有一些对象长期被使用。出于信任和效率,对于这样一些“长寿”对象,我们相信它们的用处,所以减少在垃圾回收中扫描它们的频率。

小家伙要多检查

Python将所有的对象分为0,1,2三代。所有的新建对象都是0代对象。当某一代对象经历过垃圾回收,依然存活,那么它就被归入下一代对象。垃圾回收启动时,一定会扫描所有的0代对象。如果0代经过一定次数垃圾回收,那么就启动对0代和1代的扫描清理。当1代也经历了一定次数的垃圾回收后,那么会启动对0,1,2,即对所有对象进行扫描。

这两个次数即上面get_threshold()返回的(700, 10, 10)返回的两个10。也就是说,每10次0代垃圾回收,会配合1次1代的垃圾回收;而每10次1代的垃圾回收,才会有1次的2代垃圾回收。

同样可以用set_threshold()来调整,比如对2代对象进行更频繁的扫描。

import gc
gc.set_threshold(700, 10, 5)

引用环的存在会给上面的垃圾回收机制带来很大的困难。这些引用环可能构成无法使用,但引用计数不为0的一些对象。

a = []
b = [a]
a.append(b)
del a
del b

上面我们先创建了两个表对象,并引用对方,构成一个引用环。删除了a,b引用之后,这两个对象不可能再从程序中调用,就没有什么用处了。但是由于引用环的存在,这两个对象的引用计数都没有降到0,不会被垃圾回收。

为了回收这样的引用环,Python复制每个对象的引用计数,可以记为gc_ref。假设,每个对象i,该计数为gc_ref_i。Python会遍历所有的对象i。对于每个对象i引用的对象j,将相应的gc_ref_j减1。

在结束遍历后,gc_ref不为0的对象,和这些对象引用的对象,以及继续更下游引用的对象,需要被保留。而其它的对象则被垃圾回收。

总结

Python作为一种动态类型的语言,其对象和引用分离。这与曾经的面向过程语言有很大的区别。为了有效的释放内存,Python内置了垃圾回收的支持。Python采取了一种相对简单的垃圾回收机制,即引用计数,并因此需要解决孤立引用环的问题。Python与其它语言既有共通性,又有特别的地方。对该内存管理机制的理解,是提高Python性能的重要一步。

字典推导(Dictionary comprehensions)和集合推导(Set comprehensions)

大多数的Python程序员都知道且使用过列表推导(list comprehensions)。如果你对list comprehensions概念不是很熟悉——一个list comprehension就是一个更简短、简洁的创建一个list的方法。

>>> some_list = [1, 2, 3, 4, 5]
>>> another_list = [ x + 1 for x in some_list ]
>>> another_list
[2, 3, 4, 5, 6]

自从python 3.1 (甚至是Python 2.7)起,我们可以用同样的语法来创建集合和字典表:

>>> # Set Comprehensions
>>> some_list = [1, 2, 3, 4, 5, 2, 5, 1, 4, 8]
>>> even_set = { x for x in some_list if x % 2 == 0 }
>>> even_set
set([8, 2, 4])
>>> # Dict Comprehensions
>>> d = { x: x % 2 == 0 for x in range(1, 11) }
>>> d
{1: False, 2: True, 3: False, 4: True, 5: False, 6: True, 7: False, 8: True, 9: False, 10: True}

在第一个例子里,我们以some_list为基础,创建了一个具有不重复元素的集合,而且集合里只包含偶数。而在字典表的例子里,我们创建了一个key是不重复的1到10之间的整数,value是布尔型,用来指示key是否是偶数。

这里另外一个值得注意的事情是集合的字面量表示法。我们可以简单的用这种方法创建一个集合:

>>> my_set = {1, 2, 1, 2, 3, 4}
>>> my_set
set([1, 2, 3, 4])

而不需要使用内置函数set()。

Python调用系统命令或者脚本

使用 os.system() 调用系统命令 , 程序中无法获得到输出和返回值

>>> import os
>>> os.system('ls -l /proc/cpuinfo')
>>> os.system("ls -l /proc/cpuinfo")
-r--r--r-- 1 root root 0 329 16:53 /proc/cpuinfo
0

使用 os.popen() 调用系统命令, 程序中可以获得命令输出,但是不能得到执行的返回值

>>> out = os.popen("ls -l /proc/cpuinfo")
>>> print out.read()
-r--r--r-- 1 root root 0 329 16:59 /proc/cpuinfo

使用 commands.getstatusoutput() 调用系统命令, 程序中可以获得命令输出和执行的返回值

>>> import commands
>>> commands.getstatusoutput('ls /bin/ls')
(0, '/bin/ls')

Python 捕获用户 Ctrl+C ,Ctrl+D 事件

有些时候,需要在程序中捕获用户键盘事件,比如ctrl+c退出,这样可以更好的安全退出程序

try:
do_some_func()
except KeyboardInterrupt:
print "User Press Ctrl+C,Exit"
except EOFError:
print "User Press Ctrl+D,Exit"

Python 读写文件

一次性读入文件到列表,速度较快,适用文件比较小的情况下

track_file = "track_stock.conf"
fd = open(track_file)
content_list = fd.readlines()
fd.close()
for line in content_list:
print line

逐行读入,速度较慢,适用没有足够内存读取整个文件(文件太大)

fd = open(file_path)
fd.seek(0)
title = fd.readline()
keyword = fd.readline()
uuid = fd.readline()
fd.close()

写文件 write 与 writelines 的区别

  • Fd.write(str) : 把str写到文件中,write()并不会在str后加上一个换行符
  • Fd.writelines(content) : 把content的内容全部写到文件中,原样写入,不会在每行后面加上任何东西

装饰器

装饰器使一个函数或方法包装在另一个函数里头,可以在被包装的函数添加一些额外的功能,比如日志,还可以对参数、返回结果进行修改。装饰器有点类似Java中的AOP。下面这个例子是打印被装饰的函数里面的参数的装饰器,

>>> def print_args(function):
>>>     def wrapper(*args, **kwargs):
>>>         print 'Arguments:', args, kwargs
>>>         return function(*args, **kwargs)
>>>     return wrapper

>>> @print_args
>>> def write(text):
>>>     print text

>>> write('foo')
Arguments: ('foo',) {}
foo

@是语法糖,它等价于:

>>> write = print_args(write)
>>> write('foo')
arguments: ('foo',) {}
foo
捧个钱场?