1. Python函数参数的黑魔法
第一次写Python函数时,你可能觉得参数传递就是简单的值传递。但当我调试一个线上bug时,发现函数内部修改了列表参数后,外部的原始列表竟然也跟着变了——这才意识到参数传递的水比想象中深得多。
Python的参数传递本质上是对象引用传递。举个例子,当你传递一个列表给函数时,实际上传递的是这个列表的引用(相当于把自家钥匙给了别人)。这就解释了为什么函数内修改可变对象会影响外部:
def surprise_meet(items): items.append("surprise!") shopping_list = ["牛奶"] surprise_meet(shopping_list) print(shopping_list) # 输出:['牛奶', 'surprise!']参数类型可以分为三大门派:
- 位置参数:最基础的传参方式,按顺序一对一匹配
- 关键字参数:像点菜一样指明参数名,顺序不重要
- 默认参数:给参数设定默认值,像备胎随时待命
但真正容易踩坑的是默认参数的陷阱。有次我定义了一个带默认空列表的函数,结果不同调用之间竟然共享了同一个列表:
def buggy_func(value, items=[]): items.append(value) return items print(buggy_func(1)) # [1] print(buggy_func(2)) # [预期是[2],实际输出[1,2]]这是因为默认参数在函数定义时就被创建了,而不是每次调用时新建。正确的做法是用None作为默认值:
def safe_func(value, items=None): items = items or [] items.append(value) return items2. 可变参数的灵活运用
接手老项目时,我见过一个函数接收十几个参数,调用时代码长得像裹脚布。后来用*args和**kwargs改造后,代码顿时清爽得像刚做完大扫除。
*可变位置参数(args)就像收纳盒,把所有多余的位置参数打包成元组。比如实现一个加法器:
def super_add(*numbers): return sum(numbers) print(super_add(1,2,3,4)) # 输出10**可变关键字参数(kwargs)则把多余的关键字参数存成字典。这在配置初始化时特别有用:
def init_config(**options): default = {"color": "red", "size": 10} return {**default, **options} print(init_config(color="blue")) # 输出{'color': 'blue', 'size': 10}实际项目中,我常用这种技巧处理API参数。比如封装requests调用时:
def call_api(url, **params): response = requests.get(url, params=params) return response.json()但要注意一个坑:普通参数必须放在可变参数前面,否则Python会懵圈。就像做汉堡,得先放面包再放馅料。
3. 作用域迷宫的生存指南
刚开始学Python时,我以为在函数里能访问所有变量。直到遇到UnboundLocalError,才明白作用域就像俄罗斯套娃,每层都有自己的规则。
Python的作用域遵循LEGB规则:
- Local:函数内部
- Enclosing:嵌套函数的上一层
- Global:模块全局
- Built-in:内置命名空间
有一次我写递归函数时踩了个典型坑:
count = 0 def recursion(): count += 1 # 报错!UnboundLocalError if count < 5: recursion()这是因为在函数内对count赋值时,Python会把它当作局部变量。解决方法是用global声明:
def safe_recursion(): global count count += 1 if count < 5: safe_recursion()更优雅的做法是使用闭包。比如实现计数器工厂:
def counter_factory(): count = 0 def counter(): nonlocal count count += 1 return count return counter my_counter = counter_factory() print(my_counter()) # 1 print(my_counter()) # 2闭包就像带记忆的函数,特别适合需要保持状态的场景,比如游戏中的角色属性管理。
4. 函数式编程的三把利剑
以前我写代码总喜欢用for循环处理列表,直到发现map/filter/reduce这组神器,代码量直接减半。
map()像流水线,把函数应用到每个元素上。比如批量处理字符串:
names = ["alice", "bob", "charlie"] upper_names = list(map(str.upper, names)) # 输出:['ALICE', 'BOB', 'CHARLIE']filter()是质检员,只放行符合条件的元素。比如筛选偶数:
numbers = [1,2,3,4,5] evens = list(filter(lambda x: x%2==0, numbers)) # 输出:[2,4]reduce()像榨汁机,把序列压缩成单个值。需要从functools导入:
from functools import reduce product = reduce(lambda x,y: x*y, [1,2,3,4]) # 输出:24但要注意,Python3中这些函数返回的都是迭代器,想看到结果需要用list()转换。我在项目中最常用的是列表推导式替代方案,可读性更好:
# 等价于map的例子 upper_names = [name.upper() for name in names] # 等价于filter的例子 evens = [n for n in numbers if n%2==0]5. 装饰器的魔法世界
第一次看到@符号时,我以为是什么特殊语法。后来发现装饰器就像给函数穿衣服,可以随意添加功能而不修改原函数。
最简单的装饰器模板长这样:
def logger(func): def wrapper(*args, **kwargs): print(f"调用函数:{func.__name__}") return func(*args, **kwargs) return wrapper @logger def say_hello(name): print(f"Hello, {name}!") say_hello("World") # 输出: # 调用函数:say_hello # Hello, World!实际项目中,我常用装饰器做这些事情:
- 性能计时
- 权限校验
- 缓存结果
- 异常捕获重试
比如实现一个简易缓存:
def cache(func): _cache = {} def wrapper(*args): if args not in _cache: _cache[args] = func(*args) return _cache[args] return wrapper @cache def fibonacci(n): if n < 2: return n return fibonacci(n-1) + fibonacci(n-2)装饰器有个小缺点:会掩盖原函数的元信息。解决方法是用functools.wraps:
from functools import wraps def proper_logger(func): @wraps(func) def wrapper(*args, **kwargs): print(f"调用函数:{func.__name__}") return func(*args, **kwargs) return wrapper6. 类型提示的防弹衣
以前我觉得Python的动态类型很自由,直到在线上环境遇到类型错误,才明白类型提示就像给代码穿防弹衣。
基本类型提示语法:
def greet(name: str) -> str: return f"Hello, {name}"复杂类型需要从typing模块导入:
from typing import List, Dict, Optional def process_data( items: List[int], config: Dict[str, float], limit: Optional[int] = None ) -> float: ...我在项目中强制使用mypy做静态检查后,运行时类型错误减少了70%。配置方法是在项目根目录放一个mypy.ini:
[mypy] python_version = 3.8 warn_return_any = True disallow_untyped_defs = True类型提示的进阶技巧:
- 使用NewType创建语义化类型
- 用Protocol定义接口
- 用Literal限定特定值
- 用TypedDict定义字典结构
比如定义用户类型:
from typing import NewType UserId = NewType('UserId', int) def get_user(user_id: UserId) -> str: ...7. 函数优化的实战技巧
接手过一个运行缓慢的脚本,通过函数级优化将执行时间从2小时缩短到10分钟。关键技巧包括:
记忆化缓存:对于纯函数,可以缓存结果避免重复计算:
from functools import lru_cache @lru_cache(maxsize=128) def expensive_calc(n): print(f"计算 {n}...") return n * n print(expensive_calc(4)) # 会打印 print(expensive_calc(4)) # 不会打印,直接返回缓存生成器惰性求值:处理大数据集时,用yield替代return:
def read_large_file(file): with open(file) as f: for line in f: yield line.strip() # 内存友好,逐行处理 for line in read_large_file("huge.log"): process(line)局部变量加速:在循环中将全局变量转为局部变量:
# 慢速版 import math def slow_calc(): return [math.sqrt(i) for i in range(10000)] # 快速版 def fast_calc(): sqrt = math.sqrt return [sqrt(i) for i in range(10000)]最后分享一个真实案例:用functools.partial固定部分参数,创建专用函数:
from functools import partial # 原始函数 def power(base, exponent): return base ** exponent # 创建平方函数 square = partial(power, exponent=2) print(square(5)) # 25