zoukankan      html  css  js  c++  java
  • Python内存机制简介

    1:

    变量不是盒子,应该把变量视作便利贴。变量只不过是标注,所以无法阻止为对象贴上多个标注。标注就是别名:

    >>> a = [1, 2, 3]
    >>> b = a
    >>> a.append(4)
    >>> b
    [1, 2, 3, 4]

    下面的代码中,lewis 和 charles 是别名,即两个变量绑定同一个对象。而 alex 不是 charles 的别名,因为二者绑定的是不同的对象。alex 和 charles 绑定的对象具有相同的值(== 比较的就是值),但是它们的标识不同。

    >>> charles = {'name': 'Charles L. Dodgson', 'born': 1832}
    >>> lewis = charles
    >>> lewis is charles
    True
    >>> id(charles), id(lewis)
    (140334566757552, 140334566757552)
    >>> lewis['balance'] = 950
    >>> charles
    {'born': 1832, 'balance': 950, 'name': 'Charles L. Dodgson'}
    >>> alex = {'name': 'Charles L. Dodgson', 'born': 1832, 'balance': 950}
    >>> alex == charles
    True
    >>> alex is not charles
    True

    2:

    每个变量都有标识、类型和值。对象一旦创建,它的标识绝不会变;可以把标识理解为对象在内存中的地址。

    is 运算符比较两个对象的标识;id() 函数返回对象标识的整数表示。== 运算符比较两个对象的值(对象中保存的数据)。is 运算符比 == 速度快,因为它不能重载,所以 Python 不用寻找并调用特殊方法,而是直接比较两个整数

    3:

    元组与多数 Python 集合(列表、字典、集合等)一样,保存的是对象的引用。如果引用的元素是可变的,即便元组本身不可变,元素依然可变。也就是说,元组的不可变性其实是指 tuple 数据结构的物理内容(即保存的引用)不可变,与引用的对象无关。

    而 str、bytes 和 array.array 等单一类型序列是扁平的,它们保存的不是引用,而是在连续的内存中保存数据本身(字符、字节和数字)。

    4:

    复制列表(或多数内置的可变集合)最简单的方式是使用内置的类型构造方法。例如:

    >>> l1 = [3, [55, 44], (7, 8, 9)]
    >>> l2 = list(l1)
    >>> l3 = l1
    >>> l2
    [3, [55, 44], (7, 8, 9)]
    >>> l3
    [3, [55, 44], (7, 8, 9)]
    >>> l2 is l1
    False
    >>> l3 is l1
    True

    对列表和其他可变序列来说,还能使用简洁的 l2 = l1[:]语句创建副本。

    构造方法或 [:] 做的是浅复制(即复制了最外层容器,副本中的元素是源容器中元素的引用)。如果所有元素都是不可变的,那么这样没有问题,还能节省内存。但是,如果有可变的元素,可能就会导致意想不到的问题:

    >>> l1 = [3, [66, 55, 44], (7, 8, 9)]
    >>> l2 = list(l1) 
    >>> l1.append(100) 
    >>> l1[1].remove(55) 
    >>> print('l1:', l1)
    ('l1:', [3, [66, 44], (7, 8, 9), 100])
    >>> print('l2:', l2)
    ('l2:', [3, [66, 44], (7, 8, 9)])
    >>> l2[1] += [33, 22] 
    >>> l2[2] += (10, 11) 
    >>> print('l1:', l1)
    ('l1:', [3, [66, 44, 33, 22], (7, 8, 9), 100])
    >>> print('l2:', l2)
    ('l2:', [3, [66, 44, 33, 22], (7, 8, 9, 10, 11)]) 

     

    有时我们需要的是深复制(即副本不共享内部对象的引用)。copy 模块提供的 deepcopy 和 copy 函数能为任意对象做深复制和浅复制。

    >>> class Bus:
    ...     def __init__(self, passengers=None):
    ...         if passengers is None:
    ...             self.passengers = []
    ...         else:
    ...             self.passengers = list(passengers)
    ...     def pick(self, name):
    ...         self.passengers.append(name)
    ...     def drop(self, name):
    ...         self.passengers.remove(name)
    ... 
    >>> import copy
    >>> bus1 = Bus(['Alice', 'Bill', 'Claire', 'David'])
    >>> bus2 = copy.copy(bus1)
    >>> bus3 = copy.deepcopy(bus1)
    >>> bus4 = bus1
    >>> 
    >>> print id(bus1), id(bus2), id(bus3), id(bus4)
    140334566771960 140334566290640 140334566290928 140334566771960
    
    >>> bus1.drop('Bill')
    >>> bus2.passengers
    ['Alice', 'Claire', 'David']
    
    >>> bus3.passengers
    ['Alice', 'Bill', 'Claire', 'David']
    >>> bus4.passengers
    ['Alice', 'Claire', 'David']
    >>> 
    >>> print id(bus1.passengers), id(bus2.passengers), id(bus3.passengers), id(bus4.passengers)
    140334566290568 140334566290568 140334566768792 140334566290568 

    5:

    Python 中,函数内部的形参是实参的别名。这种方案的结果是,函数可能会修改作为参数传入的可变对象,但是无法修改那些对象的标识(即不能把一个对象替换成另一个对象):

    >>> def f(a, b):
    ...     a += b
    ...     return a
    ... 
    >>> x = 1
    >>> y = 2
    >>> f(x, y)
    3
    >>> x, y
    (1, 2)
    >>> a = [1, 2]
    >>> b = [3, 4]
    >>> f(a, b)
    [1, 2, 3, 4]
    >>> a, b
    ([1, 2, 3, 4], [3, 4])
    >>> t = (10, 20)
    >>> u = (30, 40)
    >>> f(t, u)
    (10, 20, 30, 40)
    >>> t, u
    ((10, 20), (30, 40))

    对元组来说,+= 运算符创建一个新元组,

     

    6:

    应该避免使用可变的对象作为参数的默认值。比如下面的例子:

    >>> class HauntedBus:
    ...     def __init__(self, passengers=[]):
    ...         self.passengers = passengers
    ...     def pick(self, name):
    ...         self.passengers.append(name)
    ...     def drop(self, name):
    ...         self.passengers.remove(name)
    ... 
    >>> 
    >>> bus1 = HauntedBus(['Alice', 'Bill'])
    >>> bus1.pick('Charlie')
    >>> bus1.drop('Alice')
    >>> bus1.passengers
    ['Bill', 'Charlie']
    >>> 
    >>> bus2 = HauntedBus()
    >>> bus2.pick('Carrie')
    >>> bus2.passengers
    ['Carrie']
    >>> 
    >>> bus3 = HauntedBus()
    >>> bus3.passengers
    ['Carrie']
    >>> 
    >>> bus3.pick('Dave')
    >>> bus2.passengers
    ['Carrie', 'Dave']
    >>> 
    >>> bus2.passengers is bus3.passengers
    True
    >>> 
    >>> bus1.passengers
    ['Bill', 'Charlie']
    >>> HauntedBus.__init__.__defaults__
    (['Carrie', 'Dave'],)

    实例化 HauntedBus 时,如果传入乘客,会按预期运作。但是不为 HauntedBus 指定乘客的话,奇怪的事就发生了,没有指定初始乘客的 HauntedBus 实例会共享同一个乘客列表。self.passengers 变成了 passengers 参数默认值的别名。出现这个问题的根源是,默认值在定义函数时计算(通常在加载模块时),这样默认值变成了函数对象的属性。因此,如果默认值是可变对象,而且修改了它的值,那么后续的函数调用都会受到影响。

    7:

    del 语句删除名称,而不是对象。del 命令可能会导致对象被当作垃圾回收,但是仅当删除的变量保存的是对象的最后一个引用,或者无法得到对象时。 重新绑定也可能会导致对象的引用数量归零,导致对象被销毁。

    如果两个对象相互引用,当它们的引用只存在二者之间时,垃圾回收程序会判定它们都无法获取,进而把它们都销毁。

    在 CPython 中,垃圾回收使用的主要算法是引用计数。实际上,每个对象都会统计有多少引用指向自己。当引用计数归零时,对象立即就被销毁:CPython 会在对象上调用__del__ 方法(如果定义了),然后释放分配给对象的内存。

    为了演示对象生命结束时的情形,下面使用 weakref.finalize (python3)注册一个回调函数,在销毁对象时调用。

    >>> import weakref
    >>> s1 = {1, 2, 3}
    >>> s2 = s1
    >>> def bye():
    ...     print('Gone with the wind...')
    ... 
    >>> ender = weakref.finalize(s1, bye)
    >>> ender.alive
    True
    >>> 
    >>> del s1
    >>> ender.alive
    True
    >>> 
    >>> s2 = 'spam'
    Gone with the wind...
    >>> ender.alive
    False

    正是因为有引用,对象才会在内存中存在。当对象的引用数量归零后,垃圾回收程序会把对象销毁。但是,有时需要引用对象,而不让对象存在的时间超过所需时间。这经常用在缓存中。弱引用不会增加对象的引用数量。弱引用不会妨碍所指对象被当作垃圾回收。弱引用在缓存应用中很有用,

    下例展示了如何使用 weakref.ref 实例获取所指对象。如果对象存在,调用弱引用可以获取对象;否则返回 None。

    >>> import weakref
    >>> a_set = {0, 1}
    >>> wref = weakref.ref(a_set)
    >>> wref
    <weakref at 0x7fdb8cc1c368; to 'set' at 0x7fdb8cc2bd00>
    >>> 
    >>> wref()
    set([0, 1])
    >>> 
    >>> a_set = {2, 3, 4}
    >>> wref()
    set([0, 1])
    >>> 
    >>> wref() is None
    False
    >>> 
    >>> wref() is None
    True

    上例是一个控制台会话,而Python 控制台会自动把 _ 变量绑定到结果不为 None 的表达式结果上。首次调用 wref() 返回的是被引用的对象{0, 1}, {0, 1} 会绑定给 _ 变量。接下来,a_set 不再指代 {0, 1} 集合,因此集合的引用数量减少了。但是 _ 变量仍然指代它。所以再次调用 wref() 依旧返回 {0, 1}。计算这个表达式时,{0, 1} 存在,因此 wref() 不是 None。但是,随后 _ 绑定到结果值 False。现在 {0, 1} 没有强引用了。因为 {0, 1} 对象不存在了,所以 wref() 返回 None

    weakref 模块的文档(http://docs.python.org/3/library/weakref.html)指出,weakref.ref类其实是低层接口,供高级用途使用,多数程序最好使用 weakref 集合和 finalize。也就是说,应该使用 WeakKeyDictionary、WeakValueDictionary、WeakSet 和finalize(在内部使用弱引用),不要自己动手创建并处理 weakref.ref 实例。

    WeakValueDictionary 类实现的是一种可变映射,里面的值是对象的弱引用。被引用的对象在程序中的其他地方被当作垃圾回收后,对应的键会自动从 WeakValueDictionary中删除。因此,WeakValueDictionary 经常用于缓存。如下例:

    >>> class Cheese:
    ...     def __init__(self, kind):
    ...         self.kind = kind
    ...     def __repr__(self):
    ...         return 'Cheese(%r)' % self.kind
    ... 
    >>> import weakref
    >>> stock = weakref.WeakValueDictionary()
    >>> catalog = [Cheese('Red Leicester'), Cheese('Tilsit'), Cheese('Brie'), Cheese('Parmesan')]
    >>> 
    >>> for cheese in catalog:
    ...     stock[cheese.kind] = cheese
    ... 
    >>> sorted(stock.keys())
    ['Brie', 'Parmesan', 'Red Leicester', 'Tilsit']
    >>> 
    >>> del catalog
    >>> sorted(stock.keys())
    ['Parmesan']
    >>> 
    >>> del cheese
    >>> sorted(stock.keys())
    []

    删除 catalog 之后,stock 中的大多数奶酪都不见了,这是 WeakValueDictionary的预期行为。为什么不是全部呢?这是因为for循环中的变量 cheese 是全局变量,除非显式删除,否则不会消失。

    不是每个 Python 对象都可以作为弱引用的目标(或称所指对象)。基本的 list 和 dict实例不能作为所指对象,但是它们的子类可以轻松地解决这个问题:

    >>> wref_to_a_list = weakref.ref([])    
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: cannot create weak reference to 'list' object
    
    class MyList(list):
        pass
    a_list = MyList(range(10))
    # a_list可以作为弱引用的目标
    wref_to_a_list = weakref.ref(a_list) 

    8:

    如果所有 Python 对象都是不可变的,那么本章就没有存在的必要了。处理不可变的对象时,变量保存的是真正的对象还是共享对象的引用无关紧要。仅当对象可变时,对象标识才重要。

     

    CPython 中的垃圾回收主要依靠引用计数,这容易实现,但是遇到引用循环容易泄露内存,因此 CPython 2.0实现了分代垃圾回收程序,它能把引用循环中不可获取的对象销毁。

  • 相关阅读:
    如何编写gitignore文件
    【转】three.js详解之入门篇
    【转】Nginx反向代理和负载均衡
    【转】使用nvm快速搭建 Node.js 开发环境
    【转】npm包管理器那些事
    【转】用systemJS+karma+Jasmine+babel环境去编写简单的ES6工程
    个人博客 总览
    【转】vscode: Visual Studio Code 常用快捷键
    【转】Cmder--Windows下命令行利器
    APScheduler+Flask
  • 原文地址:https://www.cnblogs.com/gqtcgq/p/9240963.html
Copyright © 2011-2022 走看看