Python `*args` 和 `**kwargs`:优雅处理可变参数的终极指南 & 配合 frozenset 实现通用缓存装饰器

在Python开发中,我们经常会遇到需要处理不定数量参数的场景。今天就来聊聊Python中的*args**kwargs,看看它们如何帮我们优雅地解决这类问题。

从一个实际场景说起

假设你正在开发一个数据处理框架,需要实现一个通用的函数装饰器来记录函数执行时间:

python 复制代码
import time
from functools import wraps

def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        end = time.perf_counter()
        print(f"{func.__name__} 执行耗时: {end - start:.6f} 秒")
        return result
    return wrapper

@timer
def process_data(data, threshold=0.5):
    # 模拟数据处理
    time.sleep(1)
    return [x for x in data if x > threshold]

# 使用示例
result = process_data([1, 2, 3, 0.1, 0.4])
# 输出:process_data 执行耗时: 1.003865 秒

注意到装饰器中的*args**kwargs了吗?它们让我们的装饰器可以适配任意参数的函数。

*args :处理位置参数

*args允许函数接收任意数量的位置参数,这些参数会被打包成一个元组。

python 复制代码
def sum_all(*numbers):
    return sum(numbers)

# 以下调用都是有效的
print(sum_all(1, 2))          # 3
print(sum_all(1, 2, 3, 4))    # 10

**kwargs :处理关键字参数

**kwargs则用于接收任意数量的关键字参数,这些参数会被打包成一个字典。

python 复制代码
def print_user_info(**info):
    for key, value in info.items():
        print(f"{key}: {value}")

# 可以传入任意数量的命名参数
print_user_info(name="Alice", age=30, city="Shanghai")

解包操作: *** 的另一面

除了在函数定义时使用,***还可以用于解包序列和字典:

python 复制代码
def greet(name, age, city):
    print(f"你好,{name}!你{age}岁了,来自{city}?")

# 使用*解包列表/元组
user_data = ["Bob", 25, "Beijing"]
greet(*user_data)  # 你好,Bob!你25岁了,来自Beijing?

# 使用**解包字典
user_dict = {"name": "Charlie", "age": 35, "city": "Guangzhou"}
greet(**user_dict)   # 你好,Charlie!你35岁了,来自Guangzhou?

高级应用:混合使用与顺序规则

在实际开发中,我们经常需要混合使用这些特性:

python 复制代码
def complex_function(x, y, *args, default=None, **kwargs):
    print(f"x: {x}")
    print(f"y: {y}")
    print(f"args: {args}")
    print(f"default: {default}")
    print(f"kwargs: {kwargs}")

# 调用示例
complex_function(1, 2, 3, 4, default="test", extra=True, debug=False)

这里有个重要的顺序规则:

  1. 普通位置参数
  2. *args
  3. 默认参数
  4. **kwargs

实用技巧:使用 *args**kwargs 实现通用缓存装饰器

在开发中,经常需要在不修改原函数签名的情况下添加新功能:

python 复制代码
import time
from typing import Any, Callable
from functools import wraps

class Cache:
    def __init__(self):
        self._cache = {}
    
    def cached_call(self, func: Callable[..., Any], *args, **kwargs) -> Any:
        # 使用frozenset处理kwargs,确保{a:1, b:2}和{b:2, a:1}被视为相同的调用
        key = (func.__name__, args, frozenset(kwargs.items()))

        if key not in self._cache:
            print(f"Cache miss for {func.__name__}, calculating...")
            start = time.perf_counter()
            self._cache[key] = func(*args, **kwargs)
            end = time.perf_counter()
        else:
            print(f"Cache hit for {func.__name__}, returning cached result")
        
        return self._cache[key]

# 创建缓存实例
cache = Cache()

def expensive_operation(x: int, y: int, z: int = 1) -> int:
    """模拟耗时操作"""
    time.sleep(2)  # 模拟耗时计算
    return x + y + z

def measure_time(func: Callable, *args, **kwargs) -> None:
    """测量函数执行时间"""
    start = time.perf_counter()
    result = func(*args, **kwargs)
    end = time.perf_counter()
    print(f"Result: {result}")
    print(f"Time taken: {end - start:.2f} seconds\n")
    return result

# 演示不同场景下的缓存效果
print("第一次调用(无缓存):")
measure_time(cache.cached_call, expensive_operation, 1, 2, z=3)

print("第二次调用(使用缓存):")
measure_time(cache.cached_call, expensive_operation, 1, 2, z=3)

print("不同参数顺序的调用(展示frozenset的作用):")
# 注意这里kwargs的顺序不同,但应该命中相同的缓存
result3 = cache.cached_call(expensive_operation, x=1, y=2, z=3)
result4 = cache.cached_call(expensive_operation, y=2, x=1, z=3)

输出:

txt 复制代码
第一次调用(无缓存):
Cache miss for expensive_operation, calculating...
Result: 6
Time taken: 2.01 seconds

第二次调用(使用缓存):
Cache hit for expensive_operation, returning cached result
Result: 6
Time taken: 0.00 seconds

不同参数顺序的调用(展示frozenset的作用):
Cache miss for expensive_operation, calculating...
Cache hit for expensive_operation, returning cached result

注意,在实现缓存时,我们需要一个可哈希(hashable)的键来唯一标识函数调用。但是普通的set和dict是可变的,因此不能作为字典的键。Python 的 frozenset 就是为了解决这个问题 - 它是不可变的集合类型。

关于frozenset的几个重要特点

  1. 不可变性:一旦创建就不能修改,这使它可以作为字典的键
python 复制代码
# 这是允许的
d = {frozenset([1, 2, 3]): "value"}

# 这会报错
s = set([1, 2, 3])
d = {s: "value"}  # TypeError: unhashable type: 'set'
  1. 顺序无关性
python 复制代码
# 这两个frozenset是相等的
fs1 = frozenset([1, 2, 3])
fs2 = frozenset([3, 1, 2])
print(fs1 == fs2)  # True
  1. 性能考虑
python 复制代码
# 下面这种写法更高效
key = (func.__name__, args, frozenset(kwargs.items()))

# 而不是
key = (func.__name__, args, tuple(sorted(kwargs.items())))

关于frozenset的注意事项

  1. frozenset只能包含可哈希的元素。例如,你不能创建包含列表或字典的frozenset

  2. 在我们的缓存实现中,如果函数参数包含不可哈希的类型(如列表),需要额外处理:

python 复制代码
def make_hashable(obj):
    """将对象转换为可哈希的形式"""
    if isinstance(obj, (tuple, list)):
        return tuple(make_hashable(o) for o in obj)
    elif isinstance(obj, dict):
        return frozenset((k, make_hashable(v)) for k, v in obj.items())
    elif isinstance(obj, set):
        return frozenset(make_hashable(o) for o in obj)
    return obj

# 改进的缓存键生成
key = (func.__name__, make_hashable(args), make_hashable(kwargs))

一些 *args**kwargs 的注意事项

  1. 参数名称不一定非要用argskwargs,但这是约定俗成的命名。
  2. 在函数定义中,*args必须在**kwargs之前。
  3. 在Python3 中,可以在*args之后定义强制关键字参数。

总结

*args**kwargs是Python中非常强大的特性,它们让我们能够:

  • 编写更灵活的函数和装饰器
  • 实现参数转发
  • 处理不定量的参数

掌握这些特性,可以让我们的代码更加优雅和通用。在日常开发中,合理使用这些特性可以大大提高代码的可维护性和可扩展性。

希望这篇文章对你有帮助!如果你有任何问题或见解,欢迎在评论区讨论。

相关推荐
我是Superman丶7 分钟前
【自动化】Python SeleniumUtil 工具 开启开发者模式 自动安装油猴用户脚本等
运维·python·自动化
往日情怀酿做酒 V176392963819 分钟前
Django基础之中间件
python·中间件·django
清弦墨客21 分钟前
【蓝桥杯】46195.水仙花数
python·蓝桥杯
小oo呆27 分钟前
【自然语言处理与大模型】Ollama拉取huggingface社区或modelscope社区的GGUF模型并部署
人工智能·python·自然语言处理
戴着眼镜看不清1 小时前
从腾讯云的恶意文件查杀学习下PHP的eval函数
android·python·gpt·学习·网络安全·木马·中转api
东方不败之鸭梨的测试笔记1 小时前
多进程、多线程、分布式测试支持-pytest-xdis插件
python
六千江山2 小时前
Redis bitmaps 使用
java·数据库·redis·缓存
一路狂飙的猪2 小时前
Redis性能调优:深入剖析变慢原因及应对策略
数据库·redis·缓存
数据龙傲天2 小时前
API接口性能优化:提升电商数据处理速度的关键
爬虫·python·性能优化·数据分析·api
chengchong_cc2 小时前
redis 缓存使用
前端·redis·缓存