Fluent Python2 【Chapter19_QA】
1. vars()函数的功能是什么?举例说明其用法
vars()
函数返回对象的 __dict__
属性,如果没有 __dict__
属性,它会引发一个 TypeError
。这个函数返回对象的命名空间(namespace)的字典,其中包含对象的所有属性和对应的值。
以下是 vars()
函数的用法示例:
class Person: def __init__(self, name, age): self.name = name self.age = age person1 = Person("Alice", 30) print(vars(person1)) # 输出 {'name': 'Alice', 'age': 30}
在这个示例中,vars()
返回了 person1
对象的属性字典,其中包含了 name
和 age
属性及其对应的值。
vars(math)
vars(math)
这行代码尝试返回 math
模块的命名空间的字典,其中包含了 math
模块的所有属性和对应的值。然而,由于 math
模块没有 __dict__
属性,因此调用 vars(math)
会引发 TypeError
异常。因此,不能使用 vars()
函数直接查看模块的属性。
2. 并发和并行的概念、和区别理解、举例说明
-
并发(Concurrency):
- 并发指的是系统同时处理多个任务的能力,这些任务可以在重叠的时间段内执行,但并不一定同时执行。换句话说,并发是指在一段时间内,有多个任务都在运行,但在任意时刻只有一个任务处于运行状态。任务之间通过时间片轮转或事件驱动等方式进行切换,实现看似同时执行的效果。
- 并发通常用于处理大量的独立任务,例如同时处理多个客户端请求、多个用户的交互等场景。
- 在 Python 中,使用多线程、协程等技术可以实现并发编程。
-
并行(Parallelism):
- 并行指的是系统同时执行多个任务,这些任务实际上在同一时刻运行,每个任务使用不同的处理单元(例如不同的 CPU 核心)进行处理。
- 并行通常用于加速计算密集型任务,例如图像处理、科学计算等。
- 在 Python 中,可以使用多进程模块来实现并行计算。
区别:
- 并发关注的是任务的调度和管理,即使是在单个处理器上,也可以实现并发,但是任务是交替执行的。
- 并行关注的是同时执行多个任务,需要多个处理器或者多核处理器来支持。
举例说明:
- 并发:一个操作系统可以同时运行多个应用程序,每个应用程序都在执行自己的任务,但在同一时刻只有一个应用程序处于活动状态。
- 并行:一个系统上有多个 CPU 核心,每个核心都在执行不同的任务,例如一个 CPU 核心处理图像处理,另一个核心处理数据分析。
3. 如下代码中的flush=True是什么意思?
def spin(msg:str, done: synchronize.Event) -> None: for char in itertools.cycle(r'\|/-'): status = f'\r{char} {msg}' print(status, end='', flush=True) if done.wait(0.3): break blanks = ' ' * len(status) print(f'\r{blanks}\r', end='')
在 Python 中,flush=True
是在调用 print()
函数时传递的一个参数,用于控制是否立即将文本输出到标准输出流(通常是终端)。
当 flush=True
时,print()
函数会立即将其输出刷新到标准输出流,而不是等待缓冲区满或者遇到换行符才刷新。这意味着,即使在输出文本末尾没有换行符的情况下,也会立即将文本显示出来。
在上述代码中,flush=True
的作用是确保每次打印状态信息时都会立即显示到终端,而不会等待缓冲区满或者等到循环结束后才显示。这对于实时显示状态信息非常有用,例如在循环中更新进度条或者状态信息时。
如下print函数的源码,flush参数在其中有说明
def print(self, *args, sep=' ', end='\n', file=None): # known special case of print """ print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=False) Prints the values to a stream, or to sys.stdout by default. Optional keyword arguments: file: a file-like object (stream); defaults to the current sys.stdout. sep: string inserted between values, default a space. end: string appended after the last value, default a newline. flush: whether to forcibly flush the stream. """
在 Python 中,print()
函数有一个可选的参数 flush
,用于指定是否强制刷新输出流。当 flush=True
时,表示在打印完内容后立即将输出缓冲区中的内容刷新到输出设备中,即使缓冲区并没有填满。而当 flush=False
时,打印的内容会先存储在输出缓冲区中,直到缓冲区满了、换行符出现或者程序结束时才会被刷新到输出设备中。
下面是使用 flush=True
和 flush=False
的具体示例:
# 使用 flush=True with open('output.txt', 'w') as f: print('Hello, world!', file=f, flush=True) # 立即将内容刷新到文件中 # 使用 flush=False with open('output.txt', 'w') as f: print('Hello, world!', file=f, flush=False) # 先将内容存储在缓冲区中,不会立即刷新到文件中
在第一个示例中,由于 flush=True
,所以 Hello, world!
字符串会立即被写入到文件中。而在第二个示例中,由于 flush=False
,所以 Hello, world!
字符串会先存储在输出缓冲区中,不会立即写入到文件中。
再看另一个例子:
import time def print_with_flush(msg: str, flush: bool = False): print(msg, flush=flush) # 使用 flush=True print_with_flush("Printing with flush=True", flush=True) time.sleep(2) # 等待2秒,模拟一些其他操作 # 使用 flush=False print_with_flush("Printing with flush=False", flush=False) time.sleep(2) # 等待2秒,模拟一些其他操作 print("Done")
在这个示例中,我们定义了一个函数 print_with_flush()
,它接受一个消息字符串和一个布尔值参数 flush
,用于控制是否刷新输出。然后我们分别调用了这个函数两次,一次使用 flush=True
,一次使用 flush=False
。
当 flush=True
时,在消息打印之后立即刷新输出。这意味着消息会立即显示在控制台上。而当 flush=False
时,消息会先被写入缓冲区,直到遇到换行符或者缓冲区满了才会被输出。在这个示例中,我们故意在每次打印消息后等待了2秒,以模拟其他操作,观察两种情况下的输出行为差异。
4. 猴子补丁的理解
猴子补丁(Monkey Patching)是指在运行时动态修改类或模块的行为的技术。通俗来说,就是在程序运行过程中,通过修改现有的代码或添加新的代码来改变已经定义好的类或模块的行为。这种技术通常用于临时性地解决问题或者调整现有代码的行为,而无需修改原始源代码。
假设我们使用了一个第三方库,但发现其中某个函数的功能不符合我们的需求,我们可以通过猴子补丁来临时性地修改该函数的实现,以适应我们的需求,而不必直接修改第三方库的源代码。
下面是一个更加具体的例子,我们使用猴子补丁来修改 Python 内置的 math
模块中的 sin
函数,使其返回 0:
import math # 定义一个新的函数来替代原来的 sin 函数 def new_sin(x): return 0 # 将新函数绑定到 math 模块的 sin 函数名上,实现猴子补丁 math.sin = new_sin # 现在,当我们调用 math 模块的 sin 函数时,将会执行我们定义的新函数 result = math.sin(3.14) print(result) # 输出:0
在这个例子中,我们通过猴子补丁技术修改了 Python 内置模块 math
中的 sin
函数,使其返回了我们定义的固定值 0。
5. python中职程(worker)的理解
"职程(worker)"这个术语通常用于描述并发编程中的一种工作模式。在并发编程中,"worker"通常指的是执行特定任务的工作单元或执行者。
在异步编程中,"worker"常常是指执行异步任务的实体,它们可以是协程、线程或进程。每个worker通常负责执行特定的任务,比如处理网络请求、计算任务、IO操作等。在多任务并发执行时,可以有多个worker同时运行,从而实现并发处理。
例如,在一个Web服务器应用中,可以有多个worker同时处理来自客户端的HTTP请求。每个worker负责接收客户端请求、处理请求、生成响应,并将响应发送回客户端。通过多个worker并发执行,可以提高服务器的吞吐量和并发处理能力。
总之,"worker"是并发编程中的一种执行单元,用于执行特定的任务,通过多个worker的并发执行,可以提高系统的性能和吞吐量。
在Python中,"职程(worker)"通常指的是执行并发任务的工作单元或执行者。通俗地说,职程就是负责执行特定任务的个体,它可以是线程、进程或者协程。
举个例子,假设你有一个需要处理大量数据的任务,比如对大型文件进行处理或者执行复杂的计算。你可以创建多个职程来并行执行这些任务,每个职程负责处理其中的一部分数据。通过这种方式,你可以更快地完成任务,提高系统的效率和性能。
引入职程的概念有几个重要原因:
- 并发执行: 使用职程可以让多个任务并行执行,从而充分利用多核处理器或者异步执行的优势,提高系统的响应速度和处理能力。
- 任务隔离: 每个职程通常运行在独立的上下文中,可以避免任务之间的相互干扰和影响,提高系统的稳定性和可靠性。
- 资源管理: 职程可以独立管理资源,比如内存、文件、网络连接等,有效地控制资源的分配和利用,避免资源竞争和浪费。
- 模块化设计: 使用职程可以将复杂任务拆分成多个小任务,每个职程负责执行其中的一部分,使得系统结构更加清晰和模块化,易于维护和扩展。
总之,引入职程的概念可以帮助我们更好地进行并发编程,实现任务的并行执行,提高系统的性能和效率,同时保证系统的稳定性和可靠性。
下面是一个简单的示例,演示了如何使用Python的多进程模块multiprocessing
创建职程(worker),并发地执行任务。
import multiprocessing import time # 定义一个任务函数,用于模拟耗时操作 def worker_task(task_id): print(f"Task {task_id} started") time.sleep(2) # 模拟耗时操作 print(f"Task {task_id} completed") if __name__ == "__main__": # 创建多个职程,每个职程执行一个任务 num_workers = 4 with multiprocessing.Pool(processes=num_workers) as pool: # 使用职程池并行执行任务 pool.map(worker_task, range(num_workers)) print("All tasks completed")
在这个例子中,我们定义了一个worker_task
函数,它接受一个任务编号作为参数,并模拟了一个耗时的操作。然后,我们使用multiprocessing.Pool
创建了一个具有多个职程的职程池,每个职程负责执行一个任务。最后,我们通过pool.map
方法将任务分发给职程池,并行地执行任务。
运行这个代码,你会看到四个任务被同时启动,并且在大约两秒后依次完成,最后输出"All tasks completed"表示所有任务执行完毕。
6. KISS原则的理解
KISS原则全称为“Keep It Simple, Stupid”,即“保持简单,愚蠢”。这是一项常见的软件开发原则,旨在指导开发人员设计简单、直观且易于理解的系统。
通俗来说,KISS原则就是要尽量保持事情简单化,不要过度复杂化。它鼓励开发人员在设计和实现软件时避免不必要的复杂性和过度设计,而是尽量选择简单直接的方案。
举例来说,假设一个团队正在开发一个新的用户管理系统。按照KISS原则,他们应该尽量避免引入过多的功能和复杂的设计。例如,他们可以设计一个简单直接的用户注册页面,只包含必要的字段,而不是设计一个复杂的多步骤注册流程。这样不仅可以降低开发和维护的成本,还可以提高用户体验。
总的来说,KISS原则是在软件开发过程中指导开发人员避免过度设计和复杂性的一项重要原则,以简单和直观为目标,从而提高系统的可理解性、可维护性和可扩展性。
7. assert的关键字的理解
assert
是 Python 中的一个关键字,用于在代码中进行断言检查,通常用于测试代码的正确性。它的基本语法是:
assert expression
其中 expression
是一个条件表达式,如果该表达式的结果为 False
,则 assert
语句会抛出一个 AssertionError
异常,表明断言失败。
assert
语句主要用于以下几个方面:
-
代码测试: 在开发阶段,可以使用
assert
语句来检查代码的正确性。通过在代码中插入assert
语句来确保程序的运行状态符合预期。 -
前置条件检查: 在函数或方法的开头,可以使用
assert
语句检查输入参数的合法性或者前置条件是否满足。如果不满足条件,则立即引发异常,避免程序继续执行下去导致更严重的错误。 -
调试: 可以在
assert
语句中输出一些调试信息,帮助定位问题。比如可以在assert
语句中添加一些表达式或者变量,当断言失败时,会输出这些信息,帮助开发人员快速定位问题所在。
下面是一些示例:
# 简单的断言检查 x = 10 assert x == 10 # 带有表达式的断言检查 assert x > 0, "x 应该是一个正数" # 前置条件检查 def divide(x, y): assert y != 0, "除数不能为零" return x / y # 调试信息 debug_mode = False assert debug_mode == True, "调试模式应该开启"
需要注意的是,在生产环境中,assert
语句默认是被禁用的(-O
选项),因为它们可能会影响代码的性能。因此,在编写重要的前置条件检查时,建议使用其他方式进行检查,比如使用条件语句或者异常处理。
8. 如何理解如下代码的supervisor函数 []
import itertools import time from threading import Thread, Event # 导入 itertools 模块,用于生成一个循环迭代器 # 导入 time 模块,用于暂停程序执行 # 导入 threading 模块,用于创建和管理线程 def spin(msg: str, done: Event): # 定义一个函数 spin,它接受两个参数: # msg: 需要打印的信息 # done: 一个 Event 对象,用于通知线程停止 # 该函数在控制台打印一个旋转的动画,直到收到 done 的事件通知 for char in itertools.cycle(r'\|/-'): # 使用 itertools.cycle 生成一个循环迭代器,它重复生成字符 '|', '-', '/', '\\' status = f'\r{char} {msg}' # 构建一个包含动画字符和消息的字符串,并使用 \r 确保字符串从行首开始打印 print(status, end='', flush=True) # 打印字符串,并设置 end='' 以避免换行,flush=True 以立即刷新输出 if done.wait(0.1): # 理解要点1 # 使用 done 的事件对象等待最多 0.1 秒 break # 如果 done 的事件被设置,则退出循环 blanks = ' ' * len(status) # 计算需要打印的空格数量 print(f'\r{blanks}\r', end='') # 打印与之前动画相同长度的空格,并使用 \r 确保字符串从行首开始打印 def slow() -> int: # 定义一个函数 slow,它不返回任何值(void) time.sleep(5) # 使用 time.sleep(5) 暂停程序执行 5 秒 return 42 def supervisor() -> int: # 定义一个函数 supervisor,它返回一个整数 done = Event() # 创建一个 Event 对象,用于通知线程停止 spinner = Thread(target=spin, args=('thinking!', done)) # 创建一个线程,其目标函数为 spin,参数为 ('thinking!', done) print(f'spinner object: {spinner}') # 打印线程对象的信息 spinner.start() # 启动线程 result = slow() # 调用 slow 函数,并将其返回值赋给 result done.set() # 理解要点2 # 设置 done 的事件,通知线程停止 spinner.join() # 等待 spinner 线程完成 return result def main() -> None: # 定义一个函数 main,它不返回任何值(void) result = supervisor() # 调用 supervisor 函数,并将其返回值赋给 result print(f'Answer: {result}') # 打印 result 的值 if __name__ == '__main__': # 如果当前模块是主模块(直接运行的模块),则执行 main 函数 main()
如果 done 的事件被设置,则退出循环 这个要如何理解?
理解要点1:
在 spin
函数中,done
事件对象被用来作为线程停止的信号。done.wait(0.1)
这一行代码的意思是,函数会等待最多 0.1 秒,看 done
事件是否被设置。
如果 done
事件在等待时间内被设置了(即 done.set()
方法被调用),done.wait(0.1)
方法会立即返回 True
,并且循环会提前退出。
如果 done
事件在等待时间内没有被设置,循环会继续执行,打印下一个动画字符。
换句话说,if done.wait(0.1):
这一行代码检查 done
事件是否被设置,如果设置了,循环就会停止,否则循环会继续执行。
这里是代码中的相关部分的解释:
if done.wait(0.1): break
done.wait(0.1)
:调用done
事件的wait
方法,等待最多 0.1 秒。如果在这段时间内done
事件被设置了,wait
方法会立即返回True
。if done.wait(0.1):
:检查wait
方法是否返回True
。如果返回True
,说明done
事件在等待时间内被设置了,循环应该停止。break
:如果done
事件被设置了,使用break
语句退出循环。
理解要点2:
done.set() 这行代码相当于告诉 spin
函数停止打印动画,因为它已经完成了慢速操作。
9. 追问:supervisor
函数的作用
def supervisor() -> int: done = Event() spinner = Thread(target=spin, args=('thinking!', done)) print(f'spinner object: {spinner}') spinner.start() result = slow() done.set() spinner.join() return result
这个函数的作用是:
-
创建一个事件对象:
done = Event()
,这个事件对象用来作为信号,告诉spin
函数停止打印动画。 -
创建一个线程对象:
spinner = Thread(target=spin, args=('thinking!', done))
,这里创建了一个线程,它的目标函数是spin
,参数是('thinking!', done)
。这个线程会在后台运行spin
函数。 -
打印线程对象的信息:
print(f'spinner object: {spinner}')
,这行代码用来打印线程对象的引用,通常是为了调试或监控。 -
启动线程:
spinner.start()
,这行代码让spinner
线程开始执行。 -
执行一个慢速操作:
result = slow()
,这里调用slow
函数,这个函数会暂停程序 5 秒。 -
设置事件对象:
done.set()
,这行代码告诉spin
函数停止打印动画,因为它已经完成了慢速操作。 -
等待线程完成:
spinner.join()
,这行代码让主线程等待spinner
线程完成,以确保spin
函数能够正确地停止打印动画。 -
返回慢速操作的结果:
return result
,这行代码返回slow
函数的结果,也就是42
。
细节注意点:
spinner.start()
启动了spin
函数的线程,但不会暂停supervisor()
函数。result = slow()
调用了slow
函数,这个函数会暂停 5 秒。- 在
slow
函数暂停的 5 秒内,spin
线程在后台执行,打印旋转的动画。 - 当
slow
函数执行完毕,它返回一个结果,supervisor()
函数继续执行。
10. 追问: 详细解释 print(f'\r{blanks}\r', end='')
这行代码是 spin
函数中的一部分,它的作用是在控制台打印一个空白字符串,并确保打印后的输出会立即覆盖之前的输出,即所谓的“清屏”效果。
具体来说,这行代码的作用如下:
-
print(f'\r{blanks}\r', end='')
:这里使用了 Python 的格式化字符串(f-string)和print
函数。 -
f'\r{blanks}\r'
:这是一个格式化字符串,其中:\r
是一个回车符,它告诉操作系统将光标移动到行首。{blanks}
是一个变量,它包含一个空白字符串,这个空白字符串的长度与之前的动画字符串status
相同。\r
是一个回车符,它告诉操作系统将光标移动到行首。
-
end=''
:这个参数告诉print
函数不要在输出后添加一个换行符(\n
)。这样,新的输出会直接覆盖之前的输出,而不是在新的一行开始。 -
flush=True
:这个参数告诉print
函数立即将输出发送到控制台,而不是等到缓冲区满了再发送。
综上所述,这行代码的作用是在控制台打印一个空白字符串,并将光标移动到行首,从而实现清屏的效果。这样,当 spin
函数再次打印动画时,它会从行首开始,而不是在之前的输出后面添加新的输出。
11. 追问:为什么需要两个\r, 一个不就够了吗?
确实,理论上只需要一个 \r
回车符就足够将光标移动到行首。但是,由于 print
函数默认会在输出后添加一个换行符 \n
,所以一个 \r
通常是不够的。
如果只有一个 \r
,那么在打印完动画字符后,光标会移动到行首,但紧接着会因为 \n
的存在而自动移动到下一行。
因此,为了确保在打印完动画字符后光标能够回到原来的位置,并且不会自动换行,需要使用两个 \r
回车符。
第一个 \r
将光标移动到行首,第二个 \r
再次将光标移动到行首,从而覆盖之前的输出,并保持在原来的位置。
总结来说,两个 \r
回车符的目的是确保在打印完动画字符后,光标能够回到原来的位置,并且不会自动换行。
12. 如下代码中,在slow函数中为什么把time.sleep(3)替换成await asyncio.sleep(3)后,之前的spin函数就可以执行并打印旋转指针了?
是不是前者是阻塞、后者非阻塞?能这样理解吗?
import asyncio import itertools import time from primes_my import is_prime async def spin(msg: str) -> None: for char in itertools.cycle('\|/-'): status = f'\r{char} {msg}' print(status, flush=True, end='') try: await asyncio.sleep(0.1) except asyncio.CancelledError: break blanks = ' ' * len(status) print(f'\r{blanks}\r', end='') async def slow() -> int: await asyncio.sleep(3) # time.sleep(3)
# is_prime(5_000_111_000_222_021) return 42 async def supervisor() -> int: spinner = asyncio.create_task(spin('thinking!')) print(f'spinner object: {spinner}') result = await slow() spinner.cancel() return result def main() -> None: result = asyncio.run(supervisor()) print(f'Answer: {result}') if __name__ == '__main__': main()
在您提供的代码中,当您将 time.sleep(3)
替换为 await asyncio.sleep(3)
后,之前的 spin
函数确实可以执行并打印旋转指针了。
这是因为 await asyncio.sleep(3)
是非阻塞的,而 time.sleep(3)
是阻塞的。
下面是对代码的逐行解释,特别是关于 slow
函数中的 await asyncio.sleep(3)
和 spin
函数的执行:
在 slow
函数中,使用 await asyncio.sleep(3)
会导致函数暂停执行,但不会阻塞整个程序。
它会释放控制权给其他等待的异步任务,包括 spin
任务。因此,当 slow
函数暂停时,spin
任务仍然可以继续执行,并打印旋转指针。
相比之下,如果使用 time.sleep(3)
,它会阻塞整个程序,包括 spin
任务。
这意味着 spin
任务在 slow
函数暂停时也会暂停,直到 slow
函数执行完毕。 (一点灵感例子:自己不拉尿要憋着,同时也要让别人不准尿)
所以,将 time.sleep(3)
替换为 await asyncio.sleep(3)
后,spin
任务就可以在 slow
函数暂停时继续执行,打印旋转指针。
这是因为 await asyncio.sleep(3)
是非阻塞的,而 time.sleep(3)
是阻塞的。
13. 追问:它会释放控制权给其他等待的异步任务?这个释放控制权有什么法则吗?还是由谁授权来决定的?请详细说明
在Python的异步编程中,当一个异步函数(通常以 async def
定义)中的 await
表达式被评估时,它会释放当前任务的执行权,允许其他等待的异步任务有机会执行。
这个过程是由Python的异步事件循环(asyncio event loop)管理的。
异步事件循环是Python异步编程的核心,它负责协调多个异步任务的执行。
当一个任务使用 await
表达式时,它会暂停执行,并将控制权返回给事件循环。事件循环会检查是否有其他等待的任务可以执行,如果有,它会选择下一个可以执行的任务并继续执行。这个过程是自动的,不需要开发者手动干预。
在上述提供的代码中,当 slow
函数中的 await asyncio.sleep(3)
被评估时,它会暂停 slow
函数的执行,并将控制权返回给事件循环。事件循环会检查是否有其他等待的任务,比如 spin
任务。
如果 spin
任务尚未完成,事件循环会选择 spin
任务并继续执行。这样,spin
任务就可以在 slow
函数暂停时继续执行,打印旋转指针。
总结来说,异步事件循环决定了哪个任务可以获得执行权。当一个任务使用 await
表达式时,它会释放执行权,事件循环会根据任务队列的状态选择下一个可以执行的任务。这个过程是自动的,不需要开发者手动干预。
14. 追问:time.sleep(3)替换为is_prime(5_000_111_000_222_021)后spin函数依然并不能执行,如何理解?
当您将 time.sleep(3)
替换为 is_prime(5_000_111_000_222_021)
后,spin
函数确实不能执行。这是因为 is_prime(5_000_111_000_222_021)
是一个同步函数调用,它会阻塞当前线程,直到 is_prime
函数执行完毕。
由于这个函数调用是在 slow
函数中,它会阻塞 slow
函数的执行,从而也阻塞了 spin
函数的执行。
在异步编程中,当一个异步函数(使用 async def
定义)中的 await
表达式被评估时,它会暂停当前任务的执行,并将控制权返回给事件循环。
然而,这个机制仅适用于异步函数中的 await
表达式。在同步函数中,没有类似的机制来暂停执行并将控制权返回给事件循环。
因此,当 slow
函数中的 is_prime(5_000_111_000_222_021)
调用阻塞时,spin
函数无法执行,因为 slow
函数无法将控制权返回给事件循环。
要解决这个问题,您需要确保 slow
函数中的操作是异步的。例如,您可以将 is_prime
函数替换为一个异步版本的函数,或者使用 asyncio.run_in_executor
函数来在后台执行一个阻塞操作,而不阻塞事件循环。
15. 线程命中缓存是什么意思?缓存命中率又是什么意思?
16. python中序列化和反序列化的函数:"pickle.dump(), pickle.load()" 的用法理解
17. Python的GIL的全面理解