Python `*args` 和 `**kwargs`:優雅處理可變參數的終極指南 & 配合 frozenset 實現通用緩存器
在Python開發中,我們經常會遇到需要處理不定數量參數的場景。今天就來聊聊Python中的*args和**kwargs,看看它們如何幫我們優雅地解決這類問題。
從一個實際場景說起
假設你正在開發一個數據處理框架,需要實現一個通用的函數裝飾器來記錄函數執行時間:
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允許函數接收任意數量的位置參數,這些參數會被打包成一個元組。
def sum_all(*numbers):
return sum(numbers)
# 以下調用都是有效的
print(sum_all(1, 2)) # 3
print(sum_all(1, 2, 3, 4)) # 10
**kwargs :處理關鍵字參數
**kwargs則用于接收任意數量的關鍵字參數,這些參數會被打包成一個字典。
def print_user_info(**info):
for key, value in info.items():
print(f"{key}: {value}")
# 可以傳入任意數量的命名參數
print_user_info(name="Alice", age=30, city="Shanghai")
解包操作: * 和 ** 的另一面
除了在函數定義時使用,*和**還可以用于解包序列和字典:
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?
高級應用:混合使用與順序規則
在實際開發中,我們經常需要混合使用這些特性:
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)
這里有個重要的順序規則:
- 普通位置參數
- *args
- 默認參數
- **kwargs
實用技巧:使用 *args 和 **kwargs 實現通用緩存裝飾器
在開發中,經常需要在不修改原函數簽名的情況下添加新功能:
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)
輸出:
第一次調用(無緩存):
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的幾個重要特點
- 不可變性:一旦創建就不能修改,這使它可以作為字典的鍵
# 這是允許的
d = {frozenset([1, 2, 3]): "value"}
# 這會報錯
s = set([1, 2, 3])
d = {s: "value"} # TypeError: unhashable type: 'set'
- 順序無關性:
# 這兩個frozenset是相等的
fs1 = frozenset([1, 2, 3])
fs2 = frozenset([3, 1, 2])
print(fs1 == fs2) # True
- 性能考慮:
# 下面這種寫法更高效
key = (func.__name__, args, frozenset(kwargs.items()))
# 而不是
key = (func.__name__, args, tuple(sorted(kwargs.items())))
關于frozenset的注意事項
- frozenset只能包含可哈希的元素。例如,你不能創建包含列表或字典的frozenset。
- 在我們的緩存實現中,如果函數參數包含不可哈希的類型(如列表),需要額外處理:
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 的注意事項
- 參數名稱不一定非要用args和kwargs,但這是約定俗成的命名。
- 在函數定義中,*args必須在**kwargs之前。
- 在Python3 中,可以在*args之后定義強制關鍵字參數。
總結
*args和**kwargs是Python中非常強大的特性,它們讓我們能夠:
- 編寫更靈活的函數和裝飾器
- 實現參數轉發
- 處理不定量的參數
掌握這些特性,可以讓我們的代碼更加優雅和通用。在日常開發中,合理使用這些特性可以大大提高代碼的可維護性和可擴展性。