2025-05-21
多线程、文件操作(multi-thread.py)
- with关键字:
- 代码更清晰简洁,避免手动写
try...finally
。 - 简化资源管理的操作,确保资源在使用后能够被正常释放(即便是代码块发生异常,资源也会被释放),
- 可应用的场景比较多,比如:多线程、文件操作、数据库操作、锁操作等等;
- 工作原理:依赖对象的
__enter__()
和__exit__()
方法:- 在进入with代码块时,enter方法被调用,返回资源对象;
- 在离开with代码块时,exit方法被调用,释放资源对象;
- 举例:
# 写入文件 with open(filename, 'wb') as file: for chunk in response.iter_content(chunk_size=8192): file.write(chunk)
# 创建线程池,最大线程数为5 with ThreadPoolExecutor(max_workers=5) as executor: # 提交所有下载任务 futures = [executor.submit(download_image, url) for url in IMAGE_URLS] # 获取所有任务的结果 results = [future.result() for future in futures]
- 代码更清晰简洁,避免手动写
列表推导式(List Comprehension)
- 说明
# expression:对每个元素执行的操作,如item * 2 # item:迭代变量,表示可迭代对象中的每个元素 # iterable:可迭代对象,如列表、元组、字符串等 [expression for item in iterable]
- 示例
# 传统写法 square = [] for x in [1,2,3,4] squares.append(x**2) print(squares) # 输出:[1,4,9,16] # 列表推导式 squares = [x**2 for x in [1,2,3,4]] print(squares) # 输出:[1,4,9,16]
f-string,格式化字符串字面量(Formatted String Literal)
- 说明
Python 3.6及以上版本引入的语法糖,支持嵌入表达式,包括变量、函数、数学运算、格式化、对齐填充等; - 示例
name = "Zhangsan" age = 30 height = 187.237 print(f"Name:{name},Age:{30+1},Height:{height:.2f}")
流式下载文件,并存储到本地
- 代码
def download_image(url): """下载单个图片的函数""" try: # 获取图片文件名 filename = f'downloads/image_{url.split("=")[-1]}.jpg' # 发送HTTP请求 response = requests.get(url, stream=True) response.raise_for_status() # 检查请求是否成功 # 写入文件 with open(filename, 'wb') as file: for chunk in response.iter_content(chunk_size=8192): file.write(chunk) print(f"下载完成: {filename}") return filename except Exception as e: print(f"下载失败 {url}: {e}") return None
- 解析
url.split("=")[-1]
:根据字符串=
分割成列表,取最后一个元素request.get(url, stream=True)
:代表流式获取文件,降低内存消耗,尤其是读取大文件时response.raise_for_status
:安全检查机制,检查http响应的状态码,如果是200~299,则会继续执行,否则会抛出异常for chunk in response.iter_content(chunk_size=8192)
:在流式读取文件时,以8192字节(8k)的数据库大小进行读取下载
多线程
- 代码
# 创建线程池,最大线程数为5 with ThreadPoolExecutor(max_workers=5) as executor: # 提交所有下载任务 futures = [executor.submit(download_image, url) for url in IMAGE_URLS] # 获取所有任务的结果 results = [future.result() for future in futures]
- 解释
with
关键字:确保线程池在使用完后被正确关闭,避免代码块发生异常时线程池未正确关闭;ThreadPoolExecutor(max_workers=5) as executor
:创建最大线程数为5的线程池futures = [executor.submit(download_image, url) for url in IMAGE_URLS]
:列表推导式,生成5个Future对象future.result()
:线程的执行是executor.submit()
方法触发的,future.result()
方法只是阻塞在这里等待结果返回
线程安全
- 代码
def thread_safety_demo():
"""线程安全问题演示"""
print("\n线程安全问题演示...")
class Counter:
def __init__(self):
self.value = 0
self.lock = threading.Lock() # 创建锁对象
def increment_unsafe(self):
"""非线程安全的自增方法"""
self.value += 1
def increment_safe(self):
"""线程安全的自增方法"""
with self.lock: # 使用锁保护共享资源
self.value += 1
def worker(counter, method):
for _ in range(1000000):
method()
# 测试非线程安全的计数器
unsafe_counter = Counter()
threads = [
threading.Thread(target=worker, args=(unsafe_counter, unsafe_counter.increment_unsafe))
for _ in range(10)
]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"非线程安全计数器预期结果: 100000,实际结果: {unsafe_counter.value}")
# 测试线程安全的计数器
safe_counter = Counter()
threads = [
threading.Thread(target=worker, args=(safe_counter, safe_counter.increment_safe))
for _ in range(10)
]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"线程安全计数器预期结果: 100000,实际结果: {safe_counter.value}")
if __name__ == "__main__":
thread_safety_demo()
运行结果:
线程安全问题演示...
非线程安全计数器预期结果: 100000,实际结果: 5833995
线程安全计数器预期结果: 100000,实际结果: 10000000
- 分析
- 定义一个方法
thread_safety_demo()
,用来演示线程安全问题 - 在方法内创建一个对象:
Counter
:该对象内包含 - 构造函数:
__init__(self)
,包含两个实例变量:value
和lock
value
:用来计数的变量lock
:创建锁对象
- 线程不安全的方法:
increment_unsafe(self)
- 线程安全的方法:
increment_safe(self):
- 线程内需要执行的任务:
worker(counter,method)
counter
:代表一个对象,这里要传入的是Counter的一个实例,因为我们要用不同的实例去运行相同的循环+1的方法,看结果值的不同来演示线程安全问题method
:代表真正要执行的任务
- 分别用10个线程来执行线程不安全的任务:
increment_unsafe
和线程安全的任务:increment_safe
- 注意这里用的不是线程池,而是使用的普通创建线程的方式【不推荐】,主要区别在于:无法控制最大线程数量,无法线程复用
- 此外,上述demo循环了100万次,如果是1万次的时候,效果不明显,有可能结果相同。
- 定义一个方法
文档字符串(docstring)
- 代码
def calculate_average(numbers):
"""计算列表中所有数字的平均值。
参数:
numbers (list): 包含数字的列表
返回:
float: 平均值
"""
if not numbers:
return 0
return sum(numbers) / len(numbers)
- 说明
- 方便阅读:用于说明其功能、参数以及返回值等信息。
- 字符串与注释的差异:三个双引号创建的是字符串对象,而注释是用 # 或者 ‘’'(三个单引号)来实现的(在非 docstring 场景下)。
- 格式化方面:在多行字符串里,换行符、缩进等格式都会被保留,所以要留意代码的排版。
- 原始字符串的使用:如果需要创建原始字符串(忽略转义字符),可以在引号前加上 r,例如 r"““C:\path\to\file””"。