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 参数时,默认值是一个空列表 []。这个默认列表是在类定义时创建的,并且被所有实例共享。

当你创建 bus2bus3 时,它们都使用了相同的默认列表对象作为初始乘客列表。因此,当你修改其中一个实例的乘客列表时,另一个实例的乘客列表也会受到影响,因为它们实际上引用的是同一个列表对象。

这解释了为什么 bus2.passengersbus3.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 中,字典使用哈希表实现,键必须是不可变对象,因为它们的哈希值不会改变。字典键共享内存优化措施的基本思想是,相同的不可变对象(如数字、字符串等)在内存中只存储一份,多个字典可以共享这些不可变对象作为键,从而节省内存。

作用:

字典键共享内存优化措施的主要作用是:

  1. 节省内存: 通过共享相同的不可变对象,避免重复存储相同的数据,从而减少内存使用。
  2. 提高性能: 由于不需要重复创建和存储相同的不可变对象,可以减少内存分配和复制操作,从而提高性能。

通俗解释:

想象一下,你有两个字典,分别用于记录学生的成绩和联系信息。两个字典中可能会有一些相同的键(例如学生姓名)。如果没有共享内存的优化措施,每个字典都需要单独存储相同的学生姓名。

但是,由于字典键共享内存优化,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

在这个示例中:

  1. 我们创建了两个字典 scores 和 contacts,它们都使用字符串作为键。
  2. 我们检查 scores['Alice'] 和 contacts['Alice'] 的内存地址是否相同,结果为 True。这说明两个字典共享了相同的字符串对象 'Alice' 作为键。
  3. 我们创建了一个新的字符串对象 'Alice',发现它的内存地址与字典键的内存地址不同。这是因为字典键共享已存在的不可变对象,而不是重新创建。

通过这种内存共享优化,Python 可以减少字典占用的内存空间,提高程序的效率。但需要注意,这种优化仅适用于不可变对象,可变对象(如列表)作为字典键时不会受益于此优化。

 

5. 继问题15中,现在有个新的问题,对于scores['Alice'] 、contacts['Alice']两个key的地址相同,在内存中共享,

但是同样的内存地址如何映射到不同的值呢?比如scores['Alice']映射的值是90,contacts['Alice']映射的值是'123456789'。

虽然字典的键共享内存,但它们映射的值仍然是独立存储的。

在 Python 中,字典实际上由两部分组成:

          一个存储键的数据结构(通常是哈希表)和一个存储值的数据结构(通常是列表)。当你执行 dict[key] = value 操作时,Python 会:

  1. 计算 key 的哈希值。
  2. 使用这个哈希值在键的数据结构中查找或插入 key。如果 key 已存在,Python 会共享现有的不可变对象。
  3. 将 value 存储在值的数据结构中,并将其索引与 key 在键的数据结构中的位置相关联。

这意味着,虽然 scores['Alice'] 和 contacts['Alice'] 共享了同一个键 'Alice' 在内存中的位置,但它们分别映射到了不同的值存储区域。

用图示可能会更容易理解: 

           键的数据结构            值的数据结构
         +-----------+           +-----------+
         | 'Alice' --+---+       |     90    |
         +-----------+   |       +-----------+
                          |       | '123456789'|
                          |       +-----------+
                          +----------+
                                     |
                                     v
                        scores['Alice'] --> 90
                        contacts['Alice'] --> '123456789'

下面是共享同一个键"Alice"是如何分别映射到不同的value上,这种关键机制的详细介绍

好的,我来详细解释一下这个映射机制:

每个字典在内部都维护两个数据结构:

  1. 散列表(哈希表):存储字典中所有键的散列值(哈希值)和它们在 entries 数组中对应的索引。
  2. entries 数组:存储实际的键值对。

当你执行 d[key] = value 操作时,Python 会:

  1. 计算 key 的哈希值。
  2. 在散列表中查找这个哈希值对应的索引。如果找到,说明这个键已经存在,Python 只需要更新 entries 数组中相应索引位置的值即可。
  3. 如果没找到,说明这个键不存在,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 会:

  1. 计算 'Alice' 的哈希值。
  2. 在两个字典的散列表中查找这个哈希值对应的索引。
  3. 根据这个索引,在各自的 entries 数组中获取相应的值。

所以,虽然共享了相同的键,但由于 entries 数组中存储的是不同的值,因此可以将同一个键映射到不同的值。

这种设计使得在大量重复键的情况下,可以节省内存并提高性能。

 

6. 如何理解线程安全、不安全的队列。

进程和线程是操作系统中非常重要的概念。它们是用于管理和执行多任务的基本单位。

在并发编程中,队列被广泛使用作为线程或进程之间通信和数据传递的媒介。根据队列的使用环境,队列可以分为线程安全和线程不安全两种类型。

线程不安全的队列:

  1. 概念:线程不安全的队列指的是在多线程环境下,如果没有适当的同步机制,就可能会出现数据争用问题,导致数据损坏或程序异常。
  2. 通俗解释:比如有一个容器,多个人同时往里面放东西或者取东西,如果没有合理的规则约束,就很容易导致混乱。
  3. 用途:线程不安全的队列一般用于单线程环境中,或者已经采用了其他同步机制保证线程安全的多线程环境中。
  4. 举例:Java中的ArrayList、LinkedList等集合类都是线程不安全的。

线程安全的队列:

  1. 概念:线程安全的队列在并发环境下能够保证数据的完整性和正确性,不会出现数据争用问题。
  2. 通俗解释:对于上面的例子,线程安全的队列相当于在容器外加了一个看门人,规范了每个人放取东西的顺序和方式。
  3. 用途:线程安全的队列主要用于多线程并发编程中,确保多个线程之间对队列的操作是安全的。
  4. 举例: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]]
  1. a = [10, 20]创建了一个包含两个元素的列表a
  2. b = [a, 30]创建了一个新列表b。由于列表a是可变对象,所以b中的第一个元素实际上是对a的引用,而不是创建了一个新的副本。
  3. a.append(b)将列表b作为一个新的元素添加到列表a中。这样a就变成了[10, 20, [a, 30]]。注意这里a中的第三个元素是一个新的列表,其第一个元素又是对a本身的引用,形成了一个循环引用。
  4. 输出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的引用。这种情况下,ab中的列表就是相互独立的,不会形成循环引用。

 

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包含元素123,然后将s1赋值给s2。此时,s1s2都引用同一个集合对象。

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)

这行打印enderalive属性,它表示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)对象,因此t1t2指向了内存中同一个对象。这就是为什么t1 is t2返回True

但是,在《流畅的Python》这本书中,作者可能使用的是不同版本的Python解释器或者不同的环境设置,intern机制的行为可能会有所不同。在某些情况下,即使是短小的元组,Python也可能会为它们创建新的对象,而不是重用已有对象。

因此,您在自己的电脑上得到的结果与书中的结果不同,可能是由于不同的Python版本、环境设置或其他因素导致的。不过,无论t1 is t2的结果如何,t1 == t2都应该返回True,因为它们的值相等。

总的来说,小对象重用这种优化行为在不同的Python版本和环境中可能会有所不同,但不影响代码的正确性。如果您想保证获得一致的行为,可以避免依赖这种优化,而是显式地创建新对象。

 

 










posted @ 2024-04-11 15:14  AlphaGeek  阅读(16)  评论(0)    收藏  举报