Fluent Python2 【Chapter20_QA】

1. 方法阻塞的概念理解

在 Python 中,方法阻塞指的是当调用某个方法时,当前线程会被暂时挂起,直到该方法执行完成或者达到某个条件才会继续执行后续代码。在阻塞状态下,程序会等待某个操作完成,期间无法执行其他任务。

通俗地说,就像你在餐厅排队点餐一样。当你排到自己的位置时,你会向服务员点菜,然后等待厨师烹饪完成,才能继续前进,这段时间你不能做其他事情,就像是被“阻塞”了一样。

以下是阻塞和非阻塞的代码示例:

阻塞示例:

import time

def slow_task():
    print("开始执行慢速任务...")
    time.sleep(3)  # 模拟耗时操作,阻塞了3秒钟
    print("慢速任务执行完成!")

print("开始执行程序...")
slow_task()
print("程序执行完毕!")

在上面的代码中,调用 slow_task() 方法会阻塞当前线程 3 秒钟,直到任务执行完成后才会继续执行后续代码。

非阻塞示例:

import threading
import time

def slow_task():
    print("开始执行慢速任务...")
    time.sleep(3)  # 模拟耗时操作,阻塞了3秒钟
    print("慢速任务执行完成!")

print("开始执行程序...")
t = threading.Thread(target=slow_task)
t.start()  # 启动一个新的线程执行慢速任务,不会阻塞主线程
print("程序继续执行,不受慢速任务阻塞影响!")

在这个示例中,我们通过创建一个新的线程来执行耗时的慢速任务 slow_task(),主线程可以继续执行后续代码,不会被任务阻塞。

 

2. python中print()函数的flush=True, False(默认)的理解

原理:

        print() 函数会把内容放到内存中, 内存中的内容并不一定能够及时刷新显示到屏幕中(应该是要满足某个条件,这个条件现在还不清楚)。 使用flush=True之后,会在print结束之后,不管你有没有达到条件,立即将内存中的东西显示到屏幕上,清空缓存。 

使用场景:

1.尤其是在while循环中,要想每进行一次while循环体,在屏幕上更新打印的内容就得使用flush = True的参数。

2. 打开一个文件, 向其写入字符串, 在关闭文件f.close()之前, 打开文件是看不到写入的字符的。 要想在关闭之前实时的看到写入的字符串,应该用flush = True. 

参考:python print(xx, flush = True) 全网最清晰的解释

 

 3. 我们常说的服务器,其实是指电脑,但是python开发中经常提到的服务器,应该不是电脑硬件这块的含义把?我理解是不是类似软件更多点,比如nginx, http.server类似这些

您的理解是正确的。在计算机科学和网络技术中,服务器通常指的是一个硬件设备或软件服务,用于提供数据、文件、网络资源或服务。在Python开发中,当我们提到服务器时,我们通常指的是软件服务器,这些服务器软件运行在物理或虚拟的服务器硬件上。

例如,Nginx是一个高性能的HTTP和反向代理服务器,它广泛用于Web服务器、负载均衡器、邮件代理服务器和通用TCP/UDP代理服务器。而http.server是Python标准库中提供的一个简单的HTTP服务器,用于测试或简单的Web服务。

在Python开发中,我们经常使用服务器软件来托管我们的应用程序,例如:

  • Web服务器:如Nginx、Apache、Gunicorn等,它们处理HTTP请求,并返回Web页面或API响应。
  • 应用服务器:如Django的WSGI服务器、Flask的Werkzeug服务器,它们运行Python应用程序,处理业务逻辑。
  • 数据库服务器:如MySQL、PostgreSQL、MongoDB等,它们存储和检索数据。
  • 消息队列服务器:如RabbitMQ、Kafka等,它们用于异步消息传递。
  • 文件服务器:如FTP服务器、Samba服务器等,它们提供文件共享服务。

这些服务器软件可以是独立的应用程序,也可以是集成在更大的软件栈中,如使用Docker、Kubernetes等容器化技术或微服务架构。

因此,在Python开发中,我们所说的服务器通常指的是这些软件服务器,而不是指物理硬件设备。

 

4. 如何理解对于如下代码<3>解释的这段话 [flags_threadpoool.py]

实例化ThreadPoolExecutor, 作为上下文管理器。executor.__exit__方法将调用executor.shutdown(wait=True), 在所有线程都执行完毕前阻塞线程。???

from concurrent import futures

from flags import save_flag, get_flag, main  # <1>

def download_one(cc: str):  # <2>
    image = get_flag(cc)
    save_flag(image, f'{cc}.gif')
    print(cc, end=' ', flush=True)
    return cc

def download_many(cc_list: list[str]) -> int:
    with futures.ThreadPoolExecutor() as executor:       # <3>  如何理解?
        res = executor.map(download_one, sorted(cc_list))  # <4>

    return len(list(res))                                  # <5>

if __name__ == '__main__':
    main(download_many)  # <6>

让我来解释一下这句话的意思:

with futures.ThreadPoolExecutor() as executor:

ThreadPoolExecutor是 Python 的concurrent.futures模块中的一个类,用于创建一个线程池,以便执行并发任务。

当你使用with语句创建一个ThreadPoolExecutor对象时,它实际上是一个上下文管理器。这意味着它实现了__enter____exit__方法。

__enter__方法中,它会创建并返回一个ThreadPoolExecutor实例。这就是为什么在with语句中,你可以使用executor变量来表示这个线程池实例。

关键是在__exit__方法中,它会做一些清理工作。具体来说,它会调用executor.shutdown(wait=True)

executor.shutdown(wait=True)的作用是:

  1. 阻止新的任务被提交到线程池
  2. 等待所有当前正在执行的任务完成
  3. 终止所有空闲线程

换句话说,当你离开with语句块时,__exit__方法会确保所有任务都已经执行完毕,并且所有线程都已经被正确终止和清理。这可以避免资源泄漏和其他潜在的问题

所以,这句话的意思是:当你使用with futures.ThreadPoolExecutor() as executor:语句时,你实例化了一个ThreadPoolExecutor作为上下文管理器。

当你离开with块时,上下文管理器的__exit__方法会调用executor.shutdown(wait=True)来阻塞线程,直到所有任务都执行完毕,然后再终止所有线程。(阻塞就是为了等待其他线程执行完毕,阻塞的理解参考问题1)

这种机制确保了线程池在使用完毕后得到了正确的清理和终止,避免了资源泄漏和其他潜在问题。

 

或者也可以这样理解:

当使用 with 语句与 ThreadPoolExecutor 上下文管理器一起工作时,executor.__enter__() 方法被调用来创建和管理线程池。在 executor.__exit__() 方法被调用之前,所有的任务都应该已经提交给线程池,并且正在执行。

executor.__exit__() 方法调用 executor.shutdown(wait=True) 的目的是为了等待所有提交的任务执行完成。这是为了确保所有任务都得到正确处理,并且线程池中的线程在执行完所有任务后能够安全地关闭。

如果不等待所有任务完成就关闭线程池,可能会导致某些任务没有得到处理,或者资源没有正确释放。

因此,executor.shutdown(wait=True) 需要阻塞线程,直到所有任务都执行完毕。这是因为 shutdown 方法需要确保所有任务都已经完成,这通常需要等待线程执行完毕。

如果您不需要等待所有任务完成,您可以使用 shutdown(wait=False) 方法。这将立即关闭线程池,而不等待任何正在执行的任务。但是,这种做法可能会导致资源泄露或任务失败,因此在大多数情况下,建议使用 wait=True

 

5. try/except/else结构中,else什么时候执行,什么时候不执行

在Python中,try/except/else 结构是一种异常处理机制,用于捕获并处理程序执行中可能出现的异常。

else 语句的执行时机

  • else 语句:在 try 块中的代码没有抛出任何异常的情况下,else 块中的代码将会执行。
  • else 语句:在 try 块中的代码抛出异常并被 except 块捕获之后,else 块中的代码不会执行。
  • else 语句:在 try 块中的代码抛出异常但未被任何 except 块捕获时,else 块中的代码也不会执行。

通俗解释

想象一下,你正在尝试打开一个文件,但是你不知道这个文件是否存在。你可能会这样做:

  1. 尝试打开文件。
  2. 如果文件不存在,抛出一个异常。
  3. 如果文件存在,执行一些操作。
try:
    file = open('example.txt', 'r')
    content = file.read()
    print(content)
except FileNotFoundError:
    print('File not found.')
else:
    print('File was found and read.')

 

6. 为什么如下代码中,await coro就可以得到result了?不需要coro.result()么?如何理解逐个as_complete()的作用,能处理什么问题,输入输出又是什么?

import asyncio
import socket
from collections.abc import Iterable, AsyncIterator
from typing import NamedTuple, Optional


class Result(NamedTuple):  # <1>
    domain: str
    found: bool


OptionalLoop = Optional[asyncio.AbstractEventLoop]  # <2>


async def probe(domain: str, loop: OptionalLoop = None) -> Result:  # <3>
    if loop is None:
        loop = asyncio.get_running_loop()
    try:
        await loop.getaddrinfo(domain, None)
    except socket.gaierror:
        return Result(domain, False)
    return Result(domain, True)


async def multi_probe(domains: Iterable[str]) -> AsyncIterator[Result]:  # <4>
    loop = asyncio.get_running_loop()
    coros = [probe(domain, loop) for domain in domains]  # <5>
    for coro in asyncio.as_completed(coros):  # <6>
        result = await coro  # <7>
        yield result  # <8>

在如上的代码中,await coro可以直接获取result的原因是因为coro本身就是一个awaitable对象,即一个协程对象。当我们使用await语句时,Python会自动获取协程对象的结果,无需显式调用coro.result()

至于asyncio.as_completed()的作用,它是一个用于并发执行多个协程任务,并获取它们结果的高级工具。让我们逐步分析一下这段代码:

  1. coros = [probe(domain, loop) for domain in domains]创建了一个协程对象列表,每个协程对象代表一个probe()任务。
  2. asyncio.as_completed(coros)会并发地运行所有coros中的协程任务,并返回一个AsyncIterator对象。
  3. for coro in asyncio.as_completed(coros):循环中,每次迭代都会获取一个已经完成的协程任务coro
  4. 使用await coro可以直接获取该协程任务的结果, 无需调用coro.result()
  5. 将获取到的结果yield出来,供multi_probe()函数的调用者使用。

总的来说,asyncio.as_completed()的作用是并发地运行多个协程任务,并在它们完成时一一返回结果,而不需要等待所有任务都完成。这在处理多个独立的I/O操作时非常有用,可以充分利用异步编程的优势,提高效率。

举个例子,如果我们需要并行探测100个域名是否可访问,使用asyncio.as_completed()可以让所有探测任务同时进行,而不是一个一个地等待。这样,即使某些域名探测时间较长,也不会影响整体的效率。

输入输出方面:

  • 输入是一个可迭代对象domains,包含了需要探测的域名列表。
  • 输出是一个AsyncIterator对象,每次迭代都会产出一个Result对象,包含探测结果(域名和是否可访问)。

通过asyncio.as_completed()的高级功能,我们可以方便地实现并发探测,充分利用异步编程的优势,提高整体效率。

 

7. 追问:什么时候需要用到.result()方法呢?请举例说明,之前在其他代码看到过有用.result()获取协程执行的结果

 

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