流畅的python学习笔记(五):面向对象惯用法(1:对象引用、可变性和垃圾回收 )

  • 本章的主题是对象与对象名称之间的区别。名称不是对象,而是单独的东西。
  • 本章先以一个比喻说明 Python 的变量:变量是标注,而不是盒子。如果你不知道引用式变量是什么,可以像这样对别人解释别名。
  • 本章讨论对象标识、值和别名等概念。随后,本章会揭露元组的一个神奇特性:元组是不可变的,但是其中的值可以改变,之后就引申到浅复制和深复制。接下来的话题是引用和函数参数:可变的参数默认值导致的问题,以及如何安全地处理函数的调用者传入的可变参数。
  • 本章最后一节讨论垃圾回收、del 命令,以及如何使用弱引用“记住”对象,而无需对象本身存在。
  • 本章的内容有点儿枯燥,但是这些话题却是解决 Python 程序中很多不易察觉的 bug 的关键。
  • 首先,我们要抛弃变量是存储数据的盒子这一错误观念。
1. 变量不是盒子
  • Python 变量类似于 Java 中的引用式变量,因此最好把它们理解为附加在对象上的标注。
  • 如下示例所示的交互式控制台中,无法使用“变量是盒子”做解释。图说明了在 Python 中为什么不能使用盒子比喻,而便利贴则指出了变量的正确工作方式。
'''
	变量 a 和 b 引用同一个列表,而不是那个列表的副本
'''
if __name__ == '__main__':
    a = [1, 2, 3]
    b = a
    a.append(4)
    print(b)
    # [1, 2, 3, 4]

  • 如果把变量想象为盒子,那么无法解释 Python 中的赋值;应该把变量视作便利贴,这样示例中的行为就好解释了。
  • 对引用式变量来说,说把变量分配给对象更合理,反过来说就有问题。毕竟,对象在赋值之前就创建了。示例 证明赋值语句的右边先执行:
'''
	创建对象之后才会把变量分配给对象
'''
class Gizmo:
    def __init__(self):
        print('Gizmo id: %d' % id(self))


if __name__ == '__main__':
    x = Gizmo()
    # Gizmo id: 2915825302496
    try:
        y = Gizmo() * 10
        # Gizmo id: 2915825505280
    except TypeError as e:
        print(e)
        # unsupported operand type(s) for *: 'Gizmo' and 'int'

    print(dir())
    # ['Gizmo', '__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'x']
  • 示例说明在乘法运算中使用 Gizmo 实例会抛出异常,在尝试求积之前其实会创建一个新的 Gizmo 实例。不会创建变量 y,因为在对赋值语句的右边进行求值时抛出了异常。
  • 为了理解 Python 中的赋值语句,应该始终先读右边。对象在右边创建或获取,在此之后左边的变量才会绑定到对象上,这就像为对象贴上标注。忘掉盒子吧!
  • 因为变量只不过是标注,所以无法阻止为对象贴上多个标注。贴的多个标注,就是别名。参见下一节。
2. 标识、相等性和别名
  • Lewis Carroll 是 Charles Lutwidge Dodgson 教授的笔名。Carroll 先生指的就是 Dodgson 教授,二者是同一个人。示例用 Python 表达了这个概念:
'''
	charles 和 lewis 指代同一个对象
'''
if __name__ == '__main__':
    charles = {
   'name': 'Charles L. Dodgson', 'born': 1832}
    lewis = charles
    print(lewis is charles)
    # True
    print(id(charles), id(lewis))
    # 2186528121728 2186528121728
    lewis['balance'] = 950
    print(charles)
    # {'name': 'Charles L. Dodgson', 'born': 1832, 'balance': 950}
  • lewis 是 charles 的别名,is 运算符和 id 函数确认了这一点。向 lewis 中添加一个元素相当于向 charles 中添加一个元素。
  • 然而,假如有冒充者(姑且叫他 Alexander Pedachenko 博士)生于 1832年,声称他是 Charles L. Dodgson。这个冒充者的证件可能一样,但是Pedachenko 博士不是 Dodgson 教授。这种情况如下所示:
	alex = {
   'name': 'Charles L. Dodgson', 'born': 1832, 'balance': 950}
    print(alex == charles)
    # True
    print(alex is charles)
    # False

  • alex 指代的对象与赋值给 charles 的对象内容一样。比较两个对象,结果相等,这是因为 dict 类的 __eq__方法就是这样实现的。但它们是不同的对象。这是 Python 说明标识不同的方式:a is not b
  • lewis 和 charles 是别名,即两个变量绑定同一个对象。而 alex 不是 charles 的别名,因为二者绑定的是不同的对象。alex 和 charles 绑定的对象具有相同的值(== 比较的就是值),但是它们的标识不同。
  • 每个变量都有标识、类型和值。对象一旦创建,它的标识绝不会变;你可以把标识理解为对象在内存中的地址。is 运算符比较两个对象的标识;id() 函数返回对象标识的整数表示。
  • 其实,实际编程中很少使用 id() 函数。标识最常使用 is 运算符检查,而不是直接比较 ID。接下来讨论 is 和 == 的异同。
2.1 在 == 和 is 之间选择
  • == 运算符比较两个对象的值(对象中保存的数据),而 is 比较对象的标识。

  • 通常,我们关注的是值,而不是标识,因此 Python 代码中 == 出现的频率比 is 高。

  • 然而,在变量和单例值之间比较时,应该使用 is。目前,最常使用 is检查变量绑定的值是不是 None。下面是推荐的写法:

    x is None
    
  • 否定的正确写法是:

    x is not None
    
  • is 运算符比 == 速度快,因为它不能重载,所以 Python 不用寻找并调用特殊方法,而是直接比较两个整数 ID。而a == b是语法糖,等同于a.__eq__(b)。继承自 object 的__eq__方法比较两个对象的ID,结果与 is 一样。但是多数内置类型使用更有意义的方式覆盖了__eq__方法,会考虑对象属性的值。相等性测试可能涉及大量处理工作,例如,比较大型集合或嵌套层级深的结构时。

  • 在结束对标识和相等性的讨论之前,我们来看看著名的不可变类型 tuple(元组),它没有你想象的那么一成不变。

2.2 元组的相对不可变性
  • 元组与多数 Python 集合(列表、字典、集,等等)一样,保存的是对象的引用。 如果引用的元素是可变的,即便元组本身不可变,元素依然可变。也就是说,元组的不可变性其实是指 tuple 数据结构的物理内容(即保存的引用)不可变,与引用的对象无关。
  • 元组的值会随着引用的可变对象的变化而变。元组中不可变的是元素的标识。如下示例:
if __name__ == '__main__':
    t1 = (1, 2, [30, 40])
    t2 = (1, 2, [30, 40])
    print(t1 == t2)
    # True
    print(t1[-1])
    # [30, 40]
    print(id(t1[-1]))
    # 1907465332864
    t1[-1].append(99)
    print(id(t1[-1]))
    # 1907465332864
    print(t1 == t2
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值