Fluent Python2 【Chapter6_QA】
1. “弱引用”和“强引用”的理解
强引用和弱引用是 Python 中管理对象生命周期的两种方式。以下是它们的概念、通俗解释以及区别:
强引用:
概念: 强引用是一种常规的引用方式,当对象被一个或多个强引用引用时,它将保持存在,直到所有强引用都不再引用它时才会被垃圾回收。
通俗解释: 就像你手中拿着一个物体,只要你不松手,物体就不会丢失。
区别: 强引用会阻止对象被垃圾回收,只有当所有强引用都消失时,对象才会被销毁。
弱引用:
概念: 弱引用是对对象的一种非强制性引用方式,不会阻止对象被垃圾回收。当对象只被弱引用引用时,如果没有其他强引用指向该对象,那么对象可能被垃圾回收。
通俗解释: 就像你指着一个东西,但是你并不用力握住,这样当其他人不再用力握住它时,它就会掉落。
区别: 弱引用不会阻止对象被垃圾回收,即使只有弱引用引用着对象,对象也可能被回收。
当对象只有强引用时,即使没有任何操作,对象也不会被销毁,只有在所有强引用都被删除时,对象才会被垃圾回收。
而当对象只有弱引用时,即使没有其他强引用指向对象,对象也可能被销毁,只要没有其他强引用引用该对象。
强引用的例子:
class MyClass: pass obj = MyClass() # 创建对象 ref = obj # 创建对对象的强引用 【可以用便利贴的例子去理解】 del obj # 删除对对象的引用 print(ref) # 强引用仍然存在,对象不会被销毁
弱引用的例子:
import weakref class MyClass: pass obj = MyClass() # 创建对象 weak_ref = weakref.ref(obj) # 创建对对象的弱引用 del obj # 删除对对象的引用 print(weak_ref()) # 因为没有其他强引用了,对象可能被销毁,输出为 None
在弱引用的例子中,即使没有其他强引用引用对象,对象也可能被销毁。这是因为弱引用不会阻止对象被垃圾回收。
弱引用不会增加被引用对象的引用计数,因此如果一个对象只被弱引用所引用,一旦最后一个强引用被删除,它就会被垃圾回收。
2. 一个经典的陷阱.
为什么bus3.passenger的返回结果和bus2.passenger一样?但是bus的id不一样,而bus2/3.passenger的id却又一样?
class HauntedBus: def __init__(self, passengers=[]): # 问题就出在这,focus on detail # self.passengers = passengers def pick(self, name): self.passengers.append(name) def drop(self, name): self.passengers.remove(name) # bus1 = HauntedBus(['Alice', 'Bill']) # print(bus1.passengers) # bus1.pick('Charlie') # print(bus1.passengers) bus2 = HauntedBus() bus2.pick('Carrie') print(bus2.passengers) # ['Carrie'] bus3 = HauntedBus() print(bus3.passengers) # ['Carrie'] print('id(bus2): ', id(bus2)) print('id(bus3): ', id(bus3)) print('id(bus2.passengers): ', id(bus2.passengers)) print('id(bus3.passengers): ', id(bus3.passengers)) print(id(bus2) == id(bus3)) #output
['Carrie']
['Carrie']
id(bus2): 1967351218128
id(bus3): 1967351217840
id(bus2.passengers): 1967347989632
id(bus3.passengers): 1967347989632
False
这个问题涉及到 Python 默认参数的一个常见陷阱。在 HauntedBus
类中,初始化 passengers
参数时,默认值是一个空列表 []
。这个默认列表是在类定义时创建的,并且被所有实例共享。
当你创建 bus2
和 bus3
时,它们都使用了相同的默认列表对象作为初始乘客列表。因此,当你修改其中一个实例的乘客列表时,另一个实例的乘客列表也会受到影响,因为它们实际上引用的是同一个列表对象。
这解释了为什么 bus2.passengers
和 bus3.passengers
的 ID 是相同的,因为它们引用的是同一个列表对象。而当你输出 id(bus2)
和 id(bus3)
时,你得到的是两个不同的对象,因为它们是不同的实例。
为了避免这个问题,你可以将默认参数设置为 None
,然后在初始化时检查并创建一个新的列表,例如:
class HauntedBus: def __init__(self, passengers=None): # 修改 if passengers is None: self.passengers = [] else: self.passengers = passengers def pick(self, name): self.passengers.append(name) def drop(self, name): self.passengers.remove(name) # bus1 = HauntedBus(['Alice', 'Bill']) # print(bus1.passengers) # bus1.pick('Charlie') # print(bus1.passengers) bus2 = HauntedBus() bus2.pick('Carrie') print(bus2.passengers) # ['Carrie'] bus3 = HauntedBus() print(bus3.passengers) # ['Carrie'] print('id(bus2): ', id(bus2)) print('id(bus3): ', id(bus3)) print('id(bus2.passengers): ', id(bus2.passengers)) print('id(bus3.passengers): ', id(bus3.passengers)) print(id(bus2) == id(bus3)) #output ['Carrie'] [] id(bus2): 2573413072848 id(bus3): 2573413072608 id(bus2.passengers): 2573410237568 id(bus3.passengers): 2573413080960 False
3. __doc__如何理解?
简单理解:就是能保留模块、类、函数的注释"""xxx"""部分信息的一个属性。
__doc__
并不是一个方法,而是 Python 中的一个特殊属性,用于存储对象的文档字符串。它可以被用于类、函数、模块等多种对象。
概念和作用
在 Python 中,我们可以为类、函数、模块等对象编写文档字符串,用于描述它们的功能、用法、参数等信息。这些文档字符串会被存储在对象的 __doc__
属性中。Python 解释器和一些工具可以访问这些文档字符串,用于自动生成文档或提供代码补全和智能提示等功能。
通俗解释
__doc__
可以被看作是一个对象的"说明书"或"使用手册"。就像我们买东西时会附带一份使用说明一样,Python 对象也可以通过 __doc__
属性来记录它们的介绍、用途、使用方式等信息。这些信息不仅可以帮助代码的作者更好地理解和维护代码,也可以方便其他人阅读和使用这些代码。
使用举例
1)函数的文档字符串
def greet(name):
"""
Greets the person with the given name.
Args:
name (str): The name of the person to greet.
Returns:
str: The greeting message.
"""
return f"Hello, {name}!"
print(greet.__doc__)
输出:
Greets the person with the given name.
Args:
name (str): The name of the person to greet.
Returns:
str: The greeting message.
2)类的文档字符串
class MyClass:
"""
A simple class to demonstrate the usage of __doc__.
This class has a single attribute and a method.
"""
def __init__(self, value):
"""
Initialize the MyClass instance.
Args:
value (int): The initial value of the attribute.
"""
self.value = value
def double(self):
"""
Double the value of the attribute.
Returns:
int: The doubled value.
"""
self.value *= 2
return self.value
print(MyClass.__doc__)
输出:
A simple class to demonstrate the usage of __doc__.
This class has a single attribute and a method.
3)模块的文档字符串
在 Python 模块的开头,我们通常会添加一个文档字符串来描述模块的功能和用途。可以通过模块的 __doc__
属性访问这个文档字符串。
在上面的示例中,我们分别为函数、类和模块添加了文档字符串,并通过 __doc__
属性访问和打印它们。这些文档字符串不仅可以提高代码的可读性和可维护性,也为自动生成文档提供了便利。
4. "字典键共享内存优化措施"的概念理解
"字典键共享内存优化措施"是 Python 在处理字典时采用的一种内存优化技术,旨在减少内存使用并提高性能。
概念:
在 Python 中,字典使用哈希表实现,键必须是不可变对象,因为它们的哈希值不会改变。字典键共享内存优化措施的基本思想是,相同的不可变对象(如数字、字符串等)在内存中只存储一份,多个字典可以共享这些不可变对象作为键,从而节省内存。
作用:
字典键共享内存优化措施的主要作用是:
- 节省内存: 通过共享相同的不可变对象,避免重复存储相同的数据,从而减少内存使用。
- 提高性能: 由于不需要重复创建和存储相同的不可变对象,可以减少内存分配和复制操作,从而提高性能。
通俗解释:
想象一下,你有两个字典,分别用于记录学生的成绩和联系信息。两个字典中可能会有一些相同的键(例如学生姓名)。如果没有共享内存的优化措施,每个字典都需要单独存储相同的学生姓名。
但是,由于字典键共享内存优化,Python 只需要在内存中存储一份学生姓名,两个字典可以共享这个内存地址作为键,从而节省了内存空间。
举例说明:
# 创建两个字典
scores = {'Alice': 90, 'Bob': 85, 'Charlie': 92}
contacts = {'Alice': '123456789', 'Bob': '987654321'}
# 检查相同键的内存地址
print(id(scores['Alice']) == id(contacts['Alice'])) # 输出: True
# 创建一个新的字符串对象
new_str = 'Alice'
print(id(new_str) == id(scores['Alice'])) # 输出: False
在这个示例中:
- 我们创建了两个字典
scores
和contacts
,它们都使用字符串作为键。 - 我们检查
scores['Alice']
和contacts['Alice']
的内存地址是否相同,结果为True
。这说明两个字典共享了相同的字符串对象'Alice'
作为键。 - 我们创建了一个新的字符串对象
'Alice'
,发现它的内存地址与字典键的内存地址不同。这是因为字典键共享已存在的不可变对象,而不是重新创建。
通过这种内存共享优化,Python 可以减少字典占用的内存空间,提高程序的效率。但需要注意,这种优化仅适用于不可变对象,可变对象(如列表)作为字典键时不会受益于此优化。
5. 继问题15中,现在有个新的问题,对于scores['Alice'] 、contacts['Alice']两个key的地址相同,在内存中共享,
但是同样的内存地址如何映射到不同的值呢?比如scores['Alice']映射的值是90,contacts['Alice']映射的值是'123456789'。
虽然字典的键共享内存,但它们映射的值仍然是独立存储的。
在 Python 中,字典实际上由两部分组成:
一个存储键的数据结构(通常是哈希表)和一个存储值的数据结构(通常是列表)。当你执行 dict[key] = value
操作时,Python 会:
- 计算
key
的哈希值。 - 使用这个哈希值在键的数据结构中查找或插入
key
。如果key
已存在,Python 会共享现有的不可变对象。 - 将
value
存储在值的数据结构中,并将其索引与key
在键的数据结构中的位置相关联。
这意味着,虽然 scores['Alice']
和 contacts['Alice']
共享了同一个键 'Alice'
在内存中的位置,但它们分别映射到了不同的值存储区域。
用图示可能会更容易理解:
键的数据结构 值的数据结构
+-----------+ +-----------+
| 'Alice' --+---+ | 90 |
+-----------+ | +-----------+
| | '123456789'|
| +-----------+
+----------+
|
v
scores['Alice'] --> 90
contacts['Alice'] --> '123456789'
下面是共享同一个键"Alice"是如何分别映射到不同的value上,这种关键机制的详细介绍
好的,我来详细解释一下这个映射机制:
每个字典在内部都维护两个数据结构:
- 散列表(哈希表):存储字典中所有键的散列值(哈希值)和它们在 entries 数组中对应的索引。
- entries 数组:存储实际的键值对。
当你执行 d[key] = value
操作时,Python 会:
- 计算
key
的哈希值。 - 在散列表中查找这个哈希值对应的索引。如果找到,说明这个键已经存在,Python 只需要更新 entries 数组中相应索引位置的值即可。
- 如果没找到,说明这个键不存在,Python 会:
- 将这个新的键插入散列表,并记录它在 entries 数组中的插入位置索引。
- 在 entries 数组中分配一个新的空位,存储新的键值对。
现在来看具体的例子:
scores = {'Alice': 90}
contacts = {'Alice': '123456789'}
在内存中,可能是这样的:
散列表:
哈希值1 --> 索引0
entries 数组:
索引0: ('Alice', 90)
索引1: ('Alice', '123456789')
可以看到,虽然两个字典中的键 'Alice'
在散列表中只存储了一次,但
是它们在 entries 数组中对应了不同的索引位置,分别存储了不同的值 90
和 '123456789'
。
当我们执行:
print(scores['Alice']) # 输出 90 print(contacts['Alice']) # 输出 '123456789'
Python 会:
- 计算
'Alice'
的哈希值。 - 在两个字典的散列表中查找这个哈希值对应的索引。
- 根据这个索引,在各自的 entries 数组中获取相应的值。
所以,虽然共享了相同的键,但由于 entries 数组中存储的是不同的值,因此可以将同一个键映射到不同的值。
这种设计使得在大量重复键的情况下,可以节省内存并提高性能。
6. 如何理解线程安全、不安全的队列。
进程和线程是操作系统中非常重要的概念。它们是用于管理和执行多任务的基本单位。
在并发编程中,队列被广泛使用作为线程或进程之间通信和数据传递的媒介。根据队列的使用环境,队列可以分为线程安全和线程不安全两种类型。
线程不安全的队列:
- 概念:线程不安全的队列指的是在多线程环境下,如果没有适当的同步机制,就可能会出现数据争用问题,导致数据损坏或程序异常。
- 通俗解释:比如有一个容器,多个人同时往里面放东西或者取东西,如果没有合理的规则约束,就很容易导致混乱。
- 用途:线程不安全的队列一般用于单线程环境中,或者已经采用了其他同步机制保证线程安全的多线程环境中。
- 举例:Java中的ArrayList、LinkedList等集合类都是线程不安全的。
线程安全的队列:
- 概念:线程安全的队列在并发环境下能够保证数据的完整性和正确性,不会出现数据争用问题。
- 通俗解释:对于上面的例子,线程安全的队列相当于在容器外加了一个看门人,规范了每个人放取东西的顺序和方式。
- 用途:线程安全的队列主要用于多线程并发编程中,确保多个线程之间对队列的操作是安全的。
- 举例:Java中的并发包concurrent中的BlockingQueue接口的实现类都是线程安全的,如ArrayBlockingQueue、LinkedBlockingQueue等。
Python中也有线程安全和线程不安全的队列实现,下面用Python代码分别举例说明:
线程不安全队列示例:
import threading
import queue
# 创建一个普通的队列,线程不安全
unsafeQueue = queue.Queue()
# 生产者线程
def producer():
for i in range(10):
unsafeQueue.put(f"Element {i}")
print(f"Producer added: Element {i}")
# 消费者线程
def consumer():
while True:
try:
element = unsafeQueue.get(block=False)
print(f"Consumer got: {element}")
except queue.Empty:
break
# 启动线程
producer_thread = threading.Thread(target=producer)
consumer_thread = threading.Thread(target=consumer)
producer_thread.start()
consumer_thread.start()
producer_thread.join()
consumer_thread.join()
在这个例子中,我们使用Python的queue.Queue
创建了一个线程不安全的队列。生产者线程向队列中添加元素,消费者线程从队列中取出元素。由于缺乏同步机制,可能会出现数据竞争问题。
线程安全队列示例:
import threading
import queue
# 创建一个线程安全的队列
safeQueue = queue.Queue()
# 生产者线程
def producer():
for i in range(10):
safeQueue.put(f"Element {i}")
print(f"Producer added: Element {i}")
# 消费者线程
def consumer():
while True:
element = safeQueue.get()
print(f"Consumer got: {element}")
safeQueue.task_done()
# 启动线程
producer_thread = threading.Thread(target=producer)
consumer_thread = threading.Thread(target=consumer)
producer_thread.start()
consumer_thread.start()
producer_thread.join()
safeQueue.join()
在这个例子中,我们使用Python的queue.Queue
创建了一个线程安全的队列。生产者线程向队列中添加元素,消费者线程从队列中取出元素。
queue.Queue
内部使用了锁机制,保证了线程安全。消费者线程在取出元素后调用task_done()
方法,表示该元素已被处理,join()
方法会阻塞直到队列中所有元素都被处理完毕。
总的来说,Python的queue.Queue
是线程安全的,适用于多线程环境。
如果需要在单线程环境中使用,可以直接使用Python的列表。在多线程环境下,使用queue.Queue
可以避免数据竞争问题,提供了更好的线程管理和控制功能。
7. 如何理解循环引用,以及如下的代码。
a = [10, 20]
b = [a, 30]
a.append(b)
print(a) # [10, 20, [[...], 30]]
这个结果是由于Python中列表是可变对象,当你将一个列表赋值给另一个列表中的元素时,实际上是将该列表的引用赋值给了新列表。
让我们一步步分析这段代码:
a = [10, 20] # a = [10, 20] b = [a, 30] # b = [[10, 20], 30] a.append(b) # a = [10, 20, [[10, 20], 30]] print(a) # 输出: [10, 20, [[10, 20], 30]]
a = [10, 20]
创建了一个包含两个元素的列表a
。b = [a, 30]
创建了一个新列表b
。由于列表a
是可变对象,所以b
中的第一个元素实际上是对a
的引用,而不是创建了一个新的副本。a.append(b)
将列表b
作为一个新的元素添加到列表a
中。这样a
就变成了[10, 20, [a, 30]]
。注意这里a
中的第三个元素是一个新的列表,其第一个元素又是对a
本身的引用,形成了一个循环引用。- 输出
a
时, 得到的结果是[10, 20, [[10, 20], 30]]
。
这个结果可能看起来有点令人困惑,因为它包含了一个循环引用。当你打印一个包含自身引用的列表时,Python会用[...]
来表示这个循环引用,以避免无限递归。
如果你想创建一个新的列表副本,而不是引用,可以使用列表的切片操作或者list()
函数:
a = [10, 20]
b = [list(a), 30] # 使用 list() 创建副本
a.append(b)
print(a) # 输出: [10, 20, [[10, 20], 30]]
或者:
a = [10, 20]
b = [a[:], 30] # 使用切片创建副本
a.append(b)
print(a) # 输出: [10, 20, [[10, 20], 30]]
这样, b
中的第一个元素就是a
的一个副本,而不是对a
的引用。这种情况下,a
和b
中的列表就是相互独立的,不会形成循环引用。
8. 如何理解当有需要兼容元组或字典的API时,不适合使用python数据类。
当需要兼容元组或字典的API时,使用 Python 数据类可能不是最佳选择,因为数据类通常用于定义具有固定字段的结构化数据类型,
而元组和字典是动态可变的数据结构,其字段和值可以在运行时动态添加或修改。
以下是一个案例以及相应的解释:
from dataclasses import dataclass
@dataclass
class Person:
name: str
age: int
# 使用数据类定义一个 Person 类型的结构
person = Person(name='Alice', age=30)
# 需求变更:需要从 JSON 数据中解析 Person 对象
json_data = '{"name": "Bob", "age": 25}'
# 使用数据类的 from_dict() 方法从字典中创建 Person 对象
person_from_dict = Person.from_dict(json_data)
# 需求变更:需要将 Person 对象转换为元组
person_tuple = tuple(person)
在这个例子中,虽然我们可以使用数据类的 from_dict()
方法从字典中创建一个 Person
对象,但是当我们尝试将 Person
对象转换为元组时,会遇到困难。
数据类生成的 __repr__
方法默认返回的是类似于 Person(name='Alice', age=30)
的字符串,而不是一个元组,因此不能直接将其转换为元组。
因此,当需要与需要动态字段或动态键值对的 API 兼容时,使用 Python 数据类可能不是最佳选择。相反,应该考虑使用普通的类、字典或元组,因为它们更适合于动态添加或修改字段或键值对。
20. python中对象不可达是什么意思
在Python中,"不可达对象"(unreachable object)是指没有任何变量或其他对象引用它的对象。这些不可达对象会被Python的垃圾回收机制自动回收和销毁,以释放它们所占用的内存空间。
更具体地说,Python使用了一种基于引用计数的垃圾回收机制。每个对象都有一个关联的引用计数,指示有多少个其他对象引用了该对象。
当一个对象的引用计数降为0时,Python解释器就认为它已经"不可达",即没有任何方式可以访问该对象。这种情况下,该对象所占用的内存空间就会被回收。
例如:
a = [1, 2, 3] # 列表对象的引用计数为1 b = a # 引用计数增加为2 del a # 引用计数减少为1 del b # 引用计数减少为0, 该列表对象变为不可达
在上面的例子中,最后一步del b
将列表对象的引用计数减少为0,这时该列表对象就变成了不可达对象,随后会被垃圾回收器自动回收内存。
需要注意的是,如果存在循环引用,即两个对象互相引用形成了环路,那么它们的引用计数永远不会降为0,因此不会被自动回收。这种情况下需要使用Python的cycle detector来手动检测并打破循环引用。
总之,"不可达对象"是指那些没有任何变量或其他对象引用它的对象,这些对象的内存空间将被自动回收以节省资源。理解这个概念有助于编写高效且无内存泄漏的Python代码。
9.关于引用计数,如何理解如下代码
import weakref
s1 = {1, 2, 3}
s2 = s1
def bye():
print('...like tears in the rain.')
ender = weakref.finalize(s1, bye)
print(ender.alive) # True
del s1
print(ender.alive) # True
s2 = 'spam'
print(ender.alive)
# ...like tears in the rain.
# False
如下逐行来解释上面这段代码:
import weakref
这行导入了Python的weakref
模块,它提供了创建弱引用的功能。
s1 = {1, 2, 3}
s2 = s1
这两行创建了一个集合s1
包含元素1
、2
和3
,然后将s1
赋值给s2
。此时,s1
和s2
都引用同一个集合对象。
def bye():
print('...like tears in the rain.')
这行定义了一个函数bye
,它在被调用时会打印字符串'...like tears in the rain.'
。
ender = weakref.finalize(s1, bye)
这行创建了一个finalize
对象,它是一种特殊的弱引用。finalize
对象关联了s1
对象和bye
函数。当s1
对象被垃圾回收时,bye
函数会被自动调用。
print(ender.alive)
这行打印ender
的alive
属性,它表示ender
所引用的对象(也就是s1
)是否还存活。由于s1
目前还有s2
的强引用,所以输出为True
。
del s1
print(ender.alive)
这两行首先删除了s1
变量,但由于s2
仍然引用着同一个集合对象,所以ender.alive
输出仍为True
。
s2 = 'spam' print(ender.alive)
这两行将s2
重新赋值为字符串'spam'
,这意味着之前的集合对象没有任何强引用了。因此, ender.alive
输出为False
。同时,由于集合对象被销毁,bye
函数会被自动调用, 输出'...like tears in the rain.'
。
总结一下,这段代码展示了如何使用weakref.finalize
来关联一个对象和一个回调函数。当该对象被垃圾回收时,关联的回调函数会被自动调用。这在某些情况下很有用,比如需要在对象被销毁时执行一些清理操作。
10. 为什么同样的如下代码,在我的电脑上print(t1 == t2) 返回的是True, 在流畅的python2这本书里返回的是False
t1 = (1, 2, 3) t2 = (1, 2, 3) print(t2 is t1) # True print(t1 == t2) # True print(id(t1), id(t2))
在Python中,对于小的不可变对象(如整数、短字符串和短元组),解释器会在内存中重用已创建的对象,而不是每次都新创建一个对象。这种优化技术被称为"intern机制"。
当您执行t1 = (1, 2, 3)
和t2 = (1, 2, 3)
时,由于(1, 2, 3)
是一个短小的元组,Python解释器会重用内存中已经存在的(1, 2, 3)
对象,因此t1
和t2
指向了内存中同一个对象。这就是为什么t1 is t2
返回True
。
但是,在《流畅的Python》这本书中,作者可能使用的是不同版本的Python解释器或者不同的环境设置,intern机制的行为可能会有所不同。在某些情况下,即使是短小的元组,Python也可能会为它们创建新的对象,而不是重用已有对象。
因此,您在自己的电脑上得到的结果与书中的结果不同,可能是由于不同的Python版本、环境设置或其他因素导致的。不过,无论t1 is t2
的结果如何,t1 == t2
都应该返回True
,因为它们的值相等。
总的来说,小对象重用这种优化行为在不同的Python版本和环境中可能会有所不同,但不影响代码的正确性。如果您想保证获得一致的行为,可以避免依赖这种优化,而是显式地创建新对象。