python引用
1. 在python种通常我们不说把对象分配给变量,而是把变量分配给对象,也就是说,我们不说将对象赋值给变量,而认为我们将变量标识赋给对象。
>>> a = [1,2,3,4]
>>> b = a
>>> a is b
True
>>> id(a), id(b)
(140391868520256, 140391868520256)
>>>
可以注意到,变量a和b指向同一个对象,它们所指向的对象在内存中的位置是相同的。当人们复制列表或字典时,就复制了对象列表的引用同,如果改变引用的值,则修改了原始的值。
2. 对引用式变量来说,说把变量分配给对象更合理,反过来说就有问题。因为,对象在赋值之前就创建了。
>>> class a:
... def __init__(self):
... pass
...
>>> a
<class '__main__.a'>
>>> b = a()
>>> b
<__main__.a object at 0x7faf875f0880>
>>>
Python中的赋值语句,应该始终先读右边。对象在右边创建或获取,在此之后左边的变量才会绑定到对象上,这就像为对象贴上标注。
3. 循环引用
>>> a = [1,2,3]
>>> b = [4,5,6]
>>> a.append(b)
>>> b.append(a)
>>> a
[1, 2, 3, [4, 5, 6, [...]]]
>>> b
[4, 5, 6, [1, 2, 3, [...]]]
>>>
向a中添加了b,又向b中添加了a,在打印的时,python会试图将其打印出来。由于引用循环,对象可能直接或间接的引用了本身,所以循环中的每个对象的引用计数都不是0。
4. 引用的问题
>>> t = (1,2,[3,4])
>>> t[2] += [5,6]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> t
(1, 2, [3, 4, 5, 6])
>>>
t[2]被改动了,但是也有异常抛出。让我们看看字节码
>>> dis.dis('t[2] += [5,6]')
1 0 LOAD_NAME 0 (t)
2 LOAD_CONST 0 (2)
4 DUP_TOP_TWO
6 BINARY_SUBSCR
8 LOAD_CONST 1 (5)
10 LOAD_CONST 2 (6)
12 BUILD_LIST 2
14 INPLACE_ADD
16 ROT_THREE
18 STORE_SUBSCR
20 LOAD_CONST 3 (None)
22 RETURN_VALUE
>>>
- LOAD_NAME(namei):将与 co_names[namei] 相关联的值推入栈顶。
- LOAD_CONST(consti):将 co_consts[consti] 推入栈顶
- DUP_TOP_TWO:复制堆栈顶部的两个引用,使它们保持相同的顺序
- BINARY_SUBSCR:实现 TOS = TOS1[TOS],(TOS(Top Of Stack,栈的顶端))。
- BUILD_LIST(count):创建一个使用了来自栈的 count 个项的列表,并将结果列表推入栈顶。
- INPLACE_ADD:就地实现 TOS = TOS1 + TOS
- ROT_THREE:将第二个和第三个堆栈项向上提升一个位置,顶项移动到位置三
- STORE_SUBSCR:实现 TOS1[TOS] = TOS2
- RETURN_VALUE:返回 TOS 到函数的调用者。
可以看到,代码的执行过程:
- 将t[2]的值存入TOS(Top Of Stack,栈的顶端)。
- 计算TOS+=[5,6]。这一步能够完成,是因为TOS指向的是一个可变对象(也就是列表)。
- t[2]=TOS赋值。这一步失败,是因为s是不可变的元组。
5. is 与 ==
- ==运算符比较两个对象的值(对象中保存的数据),而is比较对象的标识。
- is运算符比==速度快,因为它不能重载,所以Python不用寻找并调用特殊方法,而是直接比较两个整数ID。而a==b是语法糖,等同于a.__eq__(b)。继承自object的__eq__方法,用于比较两个对象的ID,结果与is一样。但是多数内置类型使用更有意义的方式覆盖了__eq__方法,会考虑对象属性的值。
6. 深复制与浅复制
浅复制:python默认做浅复制,例如:复制列表(或多数内置的可变集合)最简单的方式是使用内置的类型构造方法。python的copy模块提供的copy函数能对任意对象做浅复制。。
>>> a = [1,2,3,4] >>> b = list(a) >>> b == a True >>> b is a False >>> >>> import copy >>> a = [1,2,[3,4]] >>> b = copy.copy(a) >>> a == b True >>> a is b False >>> a[2][0] = 5 >>> a == b True >>> a [1, 2, [5, 4]] >>> b [1, 2, [5, 4]] >>>
可以看到list(a)创建a的副本。副本与源列表相等。但是二者指代不同的对象。对列表和其他可变序列来说,还能使用简洁的l2=l1[:]语句创建副本。然而,构造方法或[:]做的是浅复制(即复制了最外层容器,副本中的元素是源容器中元素的引用)。
深复制:深复制(即副本不共享内部对象的引用),python的copy模块提供的deepcopy函数能对任意对象做深复制。
>>> import copy >>> a = [1,2,[3,4]] >>> b = copy.deepcopy(a) >>> a == b True >>> a is b False >>> a[2][0] = 5 >>> a [1, 2, [5, 4]] >>> b [1, 2, [3, 4]] >>>
7. 不要使用可变类型作为参数的默认值
class box:
def __init__(self, goods=[]):
self.goods = goods
def get_good(self):
self.goods.pop(0)
def post_good(self, good):
self.goods.append(good)
b = box()
c = box()
b.post_good('apple')
print(c.goods)
结果
None
1 1.5 3 5 6 6.5 7 ['apple']
Process finished with exit code 0
注意到c中列表值也同b一样改变了。这种问题很难发现。如示例所示,实例化box时,如果传入物品,会按预期运作。但是不为box指定goods的话,奇怪的事就发生了,这是因为self.goods变成了goods参数默认值的别名。出现这个问题的根源是,默认值在定义函数时计算(通常在加载模块时),因此默认值变成了函数对象的属性。因此,如果默认值是可变对象,而且修改了它的值,那么后续的函数调用都会受到影响。上例改为此可避免以上问题的发生。
class box:
def __init__(self, goods=None):
if goods is None:
goods = []
self.goods = goods
def get_good(self):
self.goods.pop(0)
def post_good(self, good):
self.goods.append(good)
python引用计数
原理
- 在C/C++语言中,程序员负责动态分配和回收堆heap当中的内存。在C里,通过函数malloc()和free() 来完成。在C++里是操作 new 和 delete 来实现相同的功能。每个由malloc()分配的内存块,最终都要由free()退回到可用内存池里面去。而调用 free() 的时机非常重要,如果一个内存块忘了free() 则会导致内存泄漏,这块内存在程序结束前将无法重新使用。这叫做 内存泄漏 。而如果对同一内存块 free() 了以后,另外一个指针再次访问,则再次使用malloc()复用这块内存会导致冲突。这叫做野指针 。
- 内存泄露往往发生在一些并不常见的代码流程上面。比如一个函数申请了内存以后,做了些计算,然后释放内存块。现在一些对函数的修改可能增加对计算的测试并检测错误条件,然后过早的从函数返回了。这很容易忘记在退出前释放内存,特别是后期修改的代码。这种内存泄漏,一旦引入,通常很长时间都难以检测到,错误退出被调用的频度较低,而现代电脑又有非常巨大的虚拟内存,所以泄漏仅在长期运行或频繁调用泄漏函数时才会变得明显。因此,有必要避免内存泄漏,通过代码规范会策略来最小化此类错误。
- Python通过malloc()和 free()包含大量的内存分配和释放,同样需要避免内存泄漏和野指针。他选择的方法就是引用计数。其原理比较简单:每个对象都包含一个计数器,计数器的增减与对象引用的增减直接相关,当引用计数为0时,表示对象已经没有存在的意义了,对象就可以删除了。
- 另一个叫法是 自动垃圾回收 。(有时引用计数也被看作是垃圾回收策略,于是这里的"自动"用以区分两者)。自动垃圾回收的优点是用户不需要明确的调用free()。(另一个优点是改善速度或内存使用,然而这并不难)。缺点是对C,没有可移植的自动垃圾回收器,而引用计数则可以可移植的实现(只要malloc()和free()函数是可用的,这也是C标准担保的)。也许以后有一天会出现可移植的自动垃圾回收器,但在此前我们必须与引用计数一起工作。
- Python使用传统的引用计数实现,也提供了循环监测器,用以检测引用循环。这使得应用无需担心直接或间接的创建了循环引用,这是引用计数垃圾收集的一个弱点。引用循环是对象(可能直接)的引用了本身,所以循环中的每个对象的引用计数都不是0。典型的引用计数实现无法回收处于引用循环中的对象,或者被循环所引用的对象,哪怕没有循环以外的引用了。循环检测器能够检测垃圾回收循环并能回收它们。 gc 模块提供了一种运行该检测器的方式(collect()函数),以及多个配置接口和在运行时禁用该检测器的功能。
代码分析
typedef struct _typeobject PyTypeObject;
/* Define pointers to support a doubly-linked list of all live heap objects. */
#define _PyObject_HEAD_EXTRA \
struct _object *_ob_next; \
struct _object *_ob_prev;
/* Nothing is actually declared to be a PyObject, but every pointer to
* a Python object can be cast to a PyObject*. This is inheritance built
* by hand. Similarly every pointer to a variable-size Python object can,
* in addition, be cast to PyVarObject*.
*/
typedef struct _object {
_PyObject_HEAD_EXTRA
Py_ssize_t ob_refcnt;
PyTypeObject *ob_type;
} PyObject;
typedef struct {
PyObject ob_base;
Py_ssize_t ob_size; /* Number of items in variable part */
} PyVarObject;
PyObject 有两个成员的结构体,引用计数、对象类型:ob_refcnt,ob_type;还有一个指针域,用于生成双向循环链表。Py_ssize_t 是一个所占字节数与 size_t 相同的有符号的整数类型(C99中没有定义ssize_t这种类型,某些编译器比如gcc扩展有该类型)。
由以上代码可知:
- 在引用计数中,最重要的是ob_refcnt,它记录了该对象被引用的次数。若此对象的引用计数器变为0,则代表此对象已经被废弃,python内存管理将自动回收该内存。
每个对象维护一个ob_ref减1,用来记录该对象当前被引用的次数 - 每当新的引用指向该对象时,它的引用计数ob_refcnt加1
- 每当该对象的引用失效时ob_refcnt减1
- 一旦对象的引用计数为0,该对象可以被回收,对象占用的内存空间将被释放
- 它的缺点是需要额外的空间维护计数,这个问题是其次的
- 最主要的问题是它不能解决对象的循环引用
在python中可使用sys模块获取ob_refcnt的值
>>> import sys
>>> a = [1,2,3]
>>> b = a
>>> c = a
>>> sys.getrefcount(a)
4
>>>
当使用某个引用计数作为参数,传递给getrefcount()时,参数实际上创建了一个临时的引用,因此,getrefcount()所得到的的结果,会比期望多1.
Comments | NOTHING