流畅的python--第五章/第六章
数据类构建器
一个简单的类,表示地理位置的经纬度。
使用nametuple
构建Coordinate
类。namedtuple
是一个工厂方法,使用指定的名称和字段构建tuple
的子类。
典型的具名元组
collections.namedtuple
是一个工厂函数,用于构建增强的tuple
子类,具有字段名称、类名和提供有用的信息的__repr__
方法。
namedtuple
构建的类可在任何需要元组的地方使用。
示例1:定义一个具名元组,存储一个城市的信息
除了从tuple
继承,具名元组还有几个额外的属性和方法。
示例2:构建一个具名元组,为字段指定默认值
@dataclass
示例:都柏林核心模式
实际使用中通常需要更多的字段。根据都柏林核心(Dublin Core
)模式,使用 @dataclass
构建一个
更复杂的类。都柏林核心模式是一小组术语,可用于描述数字资源(视频、图
像、网页等),也可用于描述物理资源,例如图书、CD 和艺术品
等对象。
关键字模式
import typing
class City(typing.NamedTuple):
continent: str
name: str
country: str
cities = [
City('Asia', 'Tokyo', 'JP'),
City('Asia', 'Delhi', 'IN'),
City('North America', 'Mexico City', 'MX'),
City('North America', 'New York', 'US'),
City('South America', 'São Paulo', 'BR'),
]
通过函数返回列表中位于亚洲的城市。
对象引用、可变性和垃圾回收
变量不是盒子
通过变量b
可以看出,如果认为b
是盒子,存储盒子a
的副本,那么结果就对不上了。更合理的理解是把变量视作便利贴。
因此,b=a
语句不是把a
盒子中的内容复制到b
盒子中,而是在标注为a
对象上再贴一个标注b
。
同一性、相等性和别名
在==
和is
之间的选择
==
运算符比较两个对象的值(对象存储的数据),而is
比较对象的标识。
最常使用is检查变量绑定的值是不是None。
x is None
否定的写法是
x is not None
元组的相对不可变性
元组与多数python容器(列表、字典、集合)一样,存储的是对象的引用。如果引用的项是可变的,即便元组本身不可变,项依然可以更改。也就是说
,元组的不可变性其实是指tuple数据结构的物理内容(即存储的引用)不可变,与引用的对象无关。
🚩相比之下,
str
、bytes
和array.array
等扁平序列存储的不是引用,而是在连续的内存中存储内容的本身(字符、字节序列和数值)。
复制对象时,相等性和同一性之间的区别有更深层的影响。副本与源对
象相等,但是 ID 不同。
默认做浅拷贝
复制列表(或多数内置的可变容器)最简单的方式是使用内置的类型构造函数。例如:
对列表和其他可变序列来说,还可以使用简洁的 l2 = l1[:]
语句创建副本。
然而,构造函数或 [:]
做的是浅拷贝(即复制最外层容器,副本中的
项是源容器中项的引用)。如果所有项都是不可变的,那么这种行为没
有问题,而且还能节省内存。但是,如果有可变的项,可能就会导致意
想不到的问题。
🚩浅拷贝的副本,可变对象跟着变化,而对象本身的变化及不可变的对象发生改变,则副本不发生变化。
为任意对象做浅拷贝和深拷贝
有时需要深拷贝(即副本不共享内部对象的引用),copy
模块提供的copy
和deepcopy
函数分别对任意对象做浅拷贝和深拷贝。
示例1:校车乘客在途中有上有下
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)
copy
和deepcopy
产生的不同效果.
🚩浅拷贝对于可变对象,浅拷贝的副本也会跟着变化,而深拷贝引用另一个列表,因此不会变
示例2:循环引用:b引用a,又把b追加到a中;deepcopy会想办法复制a
🚩深拷贝有时可能太深了。例如对象可能会引用不该复制的外部资源或单列。
函数的参数是引用时
Python
唯一支持的参数传递模式是共享传参(call by sharing)
。多数面
向对象语言采用这一模式,包括 JavaScript
、Ruby
和 Java
(Java
的引用
类型是这样,原始类型按值传参)。共享传参指函数的形参获得实参引
用的副本。也就是说,函数内部的形参是实参的别名。这种模式的结果是,函数可能会修改作为参数传入的可变对象,但是无
法修改那些对象的标识(也就是说,不能把一个对象彻底替换成另一个
对象)
示例1:函数可能会修改接收到的任何可变对象
不要使用可变类型作为参数的默认值
可选参数可以有默认值,这是 Python 函数定义的一个很棒的特性,这
样我们的 API 在演进的同时能保证向后兼容。然而,应该避免使用可变的对象作为参数的默认值。
示例2:可变默认值的危险性
class HauntedBus:
"""一个受幽灵乘客折磨的校车模型"""
# 如果没有传入 passengers 参数,则绑定默认的列表对象(一开始是空列表)
def __init__(self, passengers=[]): #
self.passengers = passengers
# 这个赋值语句把 self.passengers 变成 passengers 的别名。没有
# 提供 passengers 参数时,passengers 是默认列表的别名
def pick(self, name):
self.passengers.append(name)
# 在 self.passengers 上调用 .remove() 和 .append() 方法,修改
# 的其实是默认列表,它是函数对象的一个属性。
def drop(self, name):
self.passengers.remove(name)
问题在于,没有指定初始乘客的 HauntedBus 实例共享同一个乘客列表。
实例化 HauntedBus
时,如果
传入乘客,则一切正常。但是,不为 HauntedBus
指定乘客的话,奇怪
的事就会发生,这是因为 self.passengers
变成了 passengers
参数
默认值的别名。出现这个问题的根源是,默认值在定义函数时求解(通
常在加载模块时),因此默认值变成了函数对象的属性。所以,如果默
认值是可变对象,而且修改了它的值,那么后续的函数调用都会受到影
响。
防御可变参数
如果你定义的函数接收可变参数,那就应该谨慎考虑调用方是否期望修
改传入的参数。例如,如果函数接收一个字典,而且在处理的过程中要修改它,那么这
个副作用要不要体现到函数外部?具体问题具体分析。这其实需要函数
的编写者和调用方达成共识。
del
和垃圾回收
对象绝不会自行销毁;然而,对象不可达时,可能会被当作垃圾回
收。
首先,你可能觉得奇怪,del
不是函数而是语句,写作 del x
而不是
del(x)
。
其次,del
语句删除引用,而不是对象。del
可能导致对象被当作垃圾
回收,但是仅当删除的变量保存的是对象的最后一个引用时。重新绑定
也可能导致对象的引用数量归零,致使对象被销毁。
示例:没有指向对象的引用时,监控对象生命结束时的情形
上面的示例明确指出del
不删除对象,但是执行del
操作后可能会导致对象不可达,从而使得对象被删除。
python对不可变类型的操作
示例1:使用一个元组构建一个元组,得到的其实是同一个元组
str
、bytes
和 frozenset
实例也有这种行为。
示例2:字符串字面量可能会创建共享的对象
共享字符串字面量是一种优化措施,称为驻留(interning)。CPython
还会在小的整数上使用这个优化措施,防止重复创建“热门”数值,例如
0、1、-1
等。注意,CPython
不会驻留所有字符串和整数,驻留的条件
是实现细节,而且没有文档说明。