今天,云朵将和大家一起学习一些 Python 的高级隐藏技巧,这些技巧不仅仅是技巧;它们是微小而有力的思维转变,可以改变你解决问题的方式。
用于__slots__减少类内存使用量
什么是__slots__
?
在 Python 中,类实例通常将其属性包含在内置字典__dict__
中。此功能虽然有助于动态分配属性,但并非没有代价——它会消耗大量内存,尤其是在实例激增时。实例越多,内存开销就越大,因为每个实例都会生成自己的__dict__
。
输入__slots__
。通过在类中定义__slots__
,你可以明确指示 Python 为固定的一组属性分配内存,从而__dict__
完全避免创建 。不再需要动态属性分配,但你获得的回报是效率 — 内存使用量的减少,这在内存密集型场景中尤为明显。因此,在扩展每个字节都很重要的应用程序时,__slots__
成为首选工具。
示例用法
__slots__
在 Python 类中使用方法如下:
class Point :
__slots__ = [ 'x' , 'y' ] # 仅允许使用 'x' 和 'y' 属性
def __init__(self, x, y):
self.x = x
self.y = y
# 创建 Point 类的实例
p1 = Point(1, 2)
p2 = Point(3, 4)
# 由于未创建 '__dict__',因此内存使用量减少
好处
- 内存效率:通过取消
__dict__
,类的实例将使用更少的内存。 -
速度:属性访问可以更快,因为 Python 不必在字典中查找属性。
- 属性的不变性:由于属性是预定义的,因此动态添加新属性更加困难(但并非不可能),这有助于维护类的完整性。
此技巧在创建大量类实例的场景中特别有用,例如在数据处理、模拟或游戏开发中。
用于functools.lru_cache优化昂贵的函数调用
什么是functools.lru_cache
?
functools.lru_cache
装饰器(非常强大的工具)可让你存储函数的输出,因此当再次使用相同参数调用该函数时,它只会返回缓存的结果,而不是重新计算数字。性能显著提升?绝对如此。尤其是当相关函数需要大量计算或涉及拖延操作(如查询数据库或类似的 I/O 任务)时。缓存一次,然后快速检索。
工作原理
将函数包在lru_cache
,Python 突然间就不仅仅是执行了,它还在记忆。每个调用、每个结果都藏在缓存中,隐藏在引擎盖下的字典中。使用相同的输入再次调用该函数,无需重新计算。Python 只需进入其缓存,检索存储的答案,然后完全跳过重新执行。内存和速度碰撞。轻松实现效率。
示例用法
这是一个如何使用的简单示例lru_cache
:
from functools import lru_cache
import time
@lru_cache(maxsize=128)
def slow_function(x):
time.sleep(2) # 模拟耗时计算
return x * x
# 第一次调用 - 需要 2 秒
print(slow_function(4)) # 输出:16
# 使用相同参数的第二次调用 - 立即返回
print(slow_function(4)) # 输出:16
关键点
- 性能改进:通过缓存结果,你可以避免冗余计算,这可以大大加快你的程序速度,特别是在递归函数或使用相同参数频繁调用的函数中。
- 可自定义的缓存大小:
maxsize
该参数控制缓存可以存储多少结果。如果缓存超出此大小,则会丢弃最近最少使用 (LRU) 的结果。 - 缓存透明度:缓存函数的行为与普通函数完全相同 - 它完全透明并且维护相同的接口。
- 用例:常见用例包括优化递归算法(如斐波那契计算)、缓存数据库查询结果或减少昂贵的 I/O 绑定操作的开销。
递归函数示例
下面是一个使用递归函数计算斐波那契数列的更实际的例子:
from functools import lru_cache
@lru_cache(maxsize=None) # 缓存大小没有限制
def fibonacci(n):
if n return n
return fibonacci(n-1) + fibonacci(n-2)
# 这将在初次运行后立即计算
print(fibonacci(100)) # 输出:354224848179261915075
如果没有lru_cache
,计算fibonacci(100)
将涉及大量冗余计算。缓存通过存储和重复使用结果来优化这一过程。
使用dataclasses的field定制和优化数据类行为
什么是dataclasses
?
Python 3.7 为我们带来了dataclasses
装饰器,以及可以神奇地为你生成繁琐方法(如__init__
、__repr__
和__eq__
)的函数。这种便利中隐藏着一个宝藏函数field()
。它不仅仅是为了可以调整、塑造和微调各个字段的行为,还可以根据你的喜好定制每个字段。即将定制和控制包装在一个看似简单的包中。
field()
工作原理
field()
函数可以在数据类中的每个字段指定附加选项,例如默认值、默认工厂,甚至可用于自定义验证或处理的元数据。
示例用法
这是一个简单的例子,演示了如何dataclasses
使用field()
:
from dataclasses import dataclass, field
from typing import List
@dataclass
class Product:
name: str
price: float = 0.0
tags: List[str] = field(default_factory=list) # 使用工厂为每个实例创建一个新列表
discount: float = field(default=0.0, metadata={"units": "percentage"}) # 使用元数据获取附加上下文
def apply_discount(self):
self.price -= self.price * (self.discount / 100)
# 创建产品
p1 = Product(name="Laptop", price=1000.0, discount=10)
p1.apply_discount()
print(p1)
# 输出: Product(name='Laptop', price=900.0, tags=[], discount=10.0)
关键点
- default_factory:使用
default_factory
,可以为每个实例创建新的可变类型(如列表或字典),避免多个实例共享同一个可变对象的常见陷阱。 - metadata:该参数允许你将自定义信息附加到字段,这对于验证、生成用户界面或在文档中提供上下文很有用。
- 定制:
field()
提供各种选项,包括是否应在生成的方法中包含字段(init
, repr
, eq
),从而对数据类的行为方式提供细粒度的控制。 - 用例:当你有需要动态或条件初始化逻辑的类时,或者当你构建定制和灵活性是关键的框架或库时,这特别有用。
元数据使用示例
metadata
可用于添加自定义验证逻辑:
from dataclasses import dataclass, field
def validate_positive(instance, attribute, value):
if value raise ValueError(f"{attribute} must be positive")
@dataclass
class InventoryItem:
name: str
quantity: int = field(default=0, metadata={"validate": validate_positive})
price: float = field(default=0.0, metadata={"validate": validate_positive})
def __post_init__(self):
for field_name, field_def in self.__dataclass_fields__.items():
validator = field_def.metadata.get("validate", None)
if validator:
validator(self, field_name, getattr(self, field_name))
# 这将由于负数量而引发ValueError
item = InventoryItem(name="Widget", quantity=-5, price=10.0)
好处
- 灵活性:可以使用
field()
和轻松集成自定义行为__post_init__
。 -
当你构建需要强大且可维护的类结构的更复杂的 Python 应用程序时,此技巧特别有用。
contextlib.suppress优雅处理异常
什么是contextlib.suppress
?
contextlib.suppress
— 上下文管理器介入,消除代码块内的特定异常。不再需要一团乱麻的try-except
结构。更干净、更精简、更优雅。这就像告诉 Python,“如果发生这种情况,请忽略它”,Python 会毫不犹豫地照做。代码保持清晰,杂乱消失。可读性?毫不妥协。
工作原理
使用 时contextlib.suppress
,你可以指定一个或多个要忽略的异常。如果在with
块中引发任何此类异常,则它们将被抑制,并且代码将继续执行。
示例用法
这是一个展示如何使用contextlib.suppress
的简单示例:
import contextlib
# 示例场景:尝试删除可能存在也可能不存在的文件
import os
filename = 'non_existent_file.txt'
# 使用 try-except 的传统方法
try :
os.remove(filename)
except FileNotFoundError:
pass # 如果文件不存在则忽略错误
# 使用 contextlib.suppress 的更简洁的方法
with contextlib.suppress(FileNotFoundError):
os.remove(filename)
关键点
- 更清晰的代码:
contextlib.suppress
帮助你避免多个嵌套try-except
块,从而产生更清晰、更易于维护的代码。 - 多个异常:你可以通过将多个异常作为参数传递来抑制它们,例如
contextlib.suppress(FileNotFoundError, PermissionError)
。 - 用例:当你预计某些异常很少见或异常不需要特殊处理(除了忽略它们)时,这尤其有用。
具有多个异常的示例
下面是一个演示如何抑制多个异常的示例:
import contextlib
# 尝试访问和删除可能存在错误的文件
filenames = ['file1.txt', 'file2.txt']
with contextlib.suppress(FileNotFoundError, PermissionError):
for filename in filenames:
os.remove(filename)
在这种情况下,FileNotFoundError
和都PermissionError
将被抑制,从而使程序能够不间断地继续运行。
好处
- 增强可读性:在可以安全忽略某些异常的情况下简化错误处理。
- 多功能性:可以用于多种场景,例如文件操作,网络请求或数据库事务。
- 改进的维护:降低在复杂的异常处理代码中引入错误的可能性。
当你想要简化代码并以简洁优雅的方式处理特定的、预期的异常时,此技巧特别有用。
使用contextlib.redirect_stdout和contextlib.redirect_stderr捕获输出
contextlib.redirect_stdout
和是什么contextlib.redirect_stderr
?
contextlib.redirect_stdout
和contextlib.redirect_stderr
— 上下文管理器擅长转移流。暂时劫持print
语句的去向,或将标准输出和错误流重新路由到文件,甚至可能是隐蔽的类似文件的对象。为什么?也许是为了捕获输出,捕获它而不用动一根手指来更改原始代码。有用吗?绝对有用。尤其是当第三方库很烦人或调试信息像坏了的水龙头一样涌出时。
工作原理
这些上下文管理器?它们让你将输出汇集到几乎任何类似文件的容器中 — 无论是简单文件、文件StringIO
,还是被称为/dev/null
输出消失的深渊。当聊天库的噪音压倒一切时,或者当你渴望为后人捕捉每一行时,将其存储在文件中以供事后分析时,这非常完美。
示例用法
下面是如何使用io.StringIO
将标准输出捕获到字符串的示例:
import contextlib
import io
# 创建一个字符串缓冲区来捕获输出
output_buffer = io.StringIO()
# 将 stdout 重定向到缓冲区
with contextlib.redirect_stdout(output_buffer):
print("This will be captured")
print("So will this")
# 检索捕获的输出
captured_output = output_buffer.getvalue()
# 显示捕获的输出
print("Captured output:", captured_output)
关键点
- 重定向输出:你可以重定向
stdout
(标准输出)和stderr
(标准错误)以捕获或抑制来自代码或第三方库的输出。 - 类似文件的对象:重定向到任何类似文件的对象,使其能够高度灵活地记录、测试或抑制输出。
- 抑制输出:要完全抑制输出,你可以将其重定向至
open(os.devnull, 'w')
。
抑制输出的示例
下面是一个如何完全抑制输出的示例:
import contextlib
import os
# 通过将输出重定向到 os.devnull 来抑制输出
with contextlib.redirect_stdout( open (os.devnull, 'w' )):
print("This will not be seen")
print("This will not be seen")
好处
- 日志记录:将输出重定向到日志文件以便更好地监控和调试。
当你需要管理大型应用程序中的输出或处理修改打印语句可能无法实现的遗留代码时,此技巧特别有用。
用来setdefault简化字典操作
什么是setdefault
?
方法setdefault
— Python 字典中一种被忽视但又十分强大的工具。它是你从未意识到的必要工具,可以简化、精简和清除杂乱。它能做什么?它会悄悄检查字典中是否存在某个键,如果不存在,它会介入并插入具有给定值的该键。但它并不止于此。然后,它会将与该键关联的值交给你,整个过程干净、高效。
工作原理
通常,在浏览字典领域时,你会插入一条if
语句,在为键分配后备值之前检查其是否存在。很乏味,对吧?输入setdefault
— 整个舞蹈浓缩为一条优雅的线条。精简、清晰、可读性更强。代码低调高效,无需冗长的检查和平衡。
示例用法
以下是如何使用的基本示例setdefault
:
# 示例场景:计算列表中项目的出现次数
items = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple']
# 使用 if-else 的传统方法
counts = {}
for item in items:
if item in counts:
counts[item] += 1
else :
counts[item] = 1
print(counts) # 输出:{'apple': 3, 'banana': 2, 'orange': 1}
# 使用 setdefault 的更简洁方法
counts = {}
for item in items:
counts.setdefault(item, 0)
counts[item] += 1
print(counts) # 输出:{'apple': 3, 'banana': 2, 'orange': 1}
关键点
- 简化:
setdefault
简化在字典中检查键和设置默认值的过程,减少了多行代码的需要。 - 初始化:当你需要在执行计数、分组或聚合等操作之前使用默认值初始化字典中的键时,它特别有用。
- 效率:
setdefault
更高效、更易读,尤其是在循环中使用或使用嵌套字典时。
嵌套字典的高级示例
setdefault
在处理嵌套字典时变得更加强大:
# 示例场景:构建嵌套字典结构
nested_dict = {}
# 使用 if-else 的传统方法
key1, key2 = 'a' , 'b'
if
key1 not in nested_dict:
nested_dict[key1] = {}
if key2 not in nested_dict[key1]:
nested_dict[key1][key2] = 0
nested_dict[key1][key2] += 1
print(nested_dict) # 输出:{'a': {'b': 1}}
# 使用 setdefault 的更简洁方法
nested_dict = {}
nested_dict.setdefault(key1, {}).setdefault(key2, 0)
nested_dict[key1][key2] += 1
print (nested_dict) # 输出:{'a': {'b': 1}}
好处
- 简洁性:减少样板代码,使你的脚本更具可读性和可维护性。
- 用例:非常适合计数、分组或创建复杂嵌套字典结构等任务。
- 清晰度:提高在频繁初始化和更新字典的情况下的清晰度。
此技巧对于经常进行数据处理、配置管理或大量使用字典的任何场景的开发人员特别有用。
itertools.groupby有效数据分组
什么是itertools.groupby
?
函数itertools.groupby
位于 Pythonitertools
模块中,是一种强大的机制,用于将可迭代对象中的顺序元素聚类,这些元素共享一个共同的线程——一个键。它不仅仅是分组;它还能对齐、分类和组织。此功能在需要数据聚合、报告生成或对自然聚类序列进行细致处理的场景中表现出色。它是一种工具,使用时只需共享一个共同的属性,即可将分散的数据转换为连贯的组。强大、精确,对任何数据管理员来说都必不可少。
工作原理
函数itertools.groupby
是什么?它将连续的元素聚集在一个可迭代对象中,每个元素共享相同的值——由指定的键函数确定。它提供给你的是一个元组的迭代器:第一个元素是键本身,第二个元素是该键统一的元素的组迭代器。它不仅仅是分组;它在序列中创建一个序列,根据一个共同的线索将数据绑定在一起,同时保持简洁。一个函数,多个层次,复杂而简单。
Tips: 可迭代对象需要按下键函数排序才能groupby
正常工作。
示例用法
以下示例展示了如何使用对数据进行分组itertools.groupby
:
import itertools
# 示例数据:表示交易的字典列表
transactions = [
{ 'date' : '2024-08-01' , 'amount' : 100 },
{ 'date' : '2024-08-01' , 'amount' : 150 },
{ 'date' : '2024-08-02' , 'amount' : 200 },
{ 'date' : '2024-08-02' , 'amount' : 50 },
{ 'date' : '2024-08-03' , 'amount' : 300 },
]
# 按日期对交易进行分组
transactions.sort(key=lambda x: x['date'])
# 首先按日期对数据进行排序
grouped_transactions = itertools.groupby(transactions, key= lambda x: x[ 'date' ])
# 处理每个组
for date, group in grouped_transactions:
print(f"Date: {date}")
for transaction in group:
rint(f" Transaction amount: {transaction['amount']}")
关键点
- 需要排序:输入数据必须按下键函数排序才能
groupby
正确分组元素。 - 内存效率:由于
groupby
返回迭代器,因此内存效率非常高。它不需要将所有数据加载到内存中,因此适合处理大型数据集。 - 自定义键功能:你可以定义自定义键功能,根据任何标准对项目进行分组,从而提供极大的灵活性。
高级示例:按派生属性分组
以下是按派生属性对项目进行分组的示例:
import itertools
# 示例数据:具有类别的产品列表
products = [
{ 'name' : 'apple' , 'category' : 'fruit' },
{ 'name' : 'banana' , 'category' : 'fruit' },
{ 'name' : 'carrot' , 'category' : 'vegetable' },
{ 'name' : 'broccoli' , 'category' : 'vegetable' },
{ 'name' : 'cherry' , 'category' : 'fruit' },
]
# 按类别对数据进行排序
products.sort(key= lambda x: x[ 'category' ])
# 按类别分组
grouped_products = itertools.groupby(products, key= lambda x: x[ 'category' ])
# 处理每个组
for category, group in grouped_products:
print(f"Category: {category}")
for product in group:
print(f" Product: {product['name']}")
好处
- 高效分组:
groupby
非常适合以内存高效的方式对数据进行分组,特别是在处理大型数据集时。 -
简单而强大:使用循环和条件简化了原本需要更冗长的代码。
此技巧对于从事数据处理、报告或任何涉及分析分组数据的场景的开发人员特别有用。