为何`nonlocal`关键字无法将外部作用域变量传递至调用模块?
问题分析与解答
问题场景
我们有以下三段代码:
A.py
from B import call def make_call(): print("I'm making a call!") call(phone_number="867-5309")
B.py
def call(phone_number): pass
test_A.py
import pytest from A import make_call @pytest.fixture def patch_A_call(monkeypatch): number = "NOT ENTERED" number_list = [] def call(phone_number): nonlocal number number = phone_number number_list.append(phone_number) monkeypatch.setattr("A.call", call) return (number, number_list) def test_make_call(patch_A_call): make_call() print(f"number: {patch_A_call[0]}") print(f"number_list: {patch_A_call[1]}")
运行测试后得到的实际输出:
number: NOT ENTERED number_list: ['867-5309']
原本期望两者的值都是"867-5309",但字符串变量未更新,列表却正常变化,这背后的原因如下:
核心原因
1. 不可变类型与可变类型的本质差异
- 字符串是不可变类型:当执行
number = phone_number时,并不是修改原来的"NOT ENTERED"字符串对象,而是让fixture作用域里的number变量重新指向了一个新的字符串对象"867-5309"。但fixture返回的元组中第一个元素,是最初那个"NOT ENTERED"对象的引用,这个引用不会因为后续变量的重新指向而改变。 - 列表是可变类型:
number_list.append(phone_number)是直接修改了列表对象本身的内容,fixture返回的元组中第二个元素始终指向这个列表对象,因此能看到更新后的结果。
2. fixture返回时机的影响
fixture中的return (number, number_list)语句在patch函数定义完成、monkeypatch设置完毕后就已经执行,此时number的值还是"NOT ENTERED"。后续测试函数触发patch后的call函数时,nonlocal number确实能让内部函数修改fixture作用域里的number变量,但这种修改只是变量的重新绑定,不会改变之前返回的元组中已存在的引用指向。
解决方法
如果想让字符串变量的更新能被测试函数感知,可以用可变容器包裹它:
方法1:用列表包裹字符串
修改fixture代码:
@pytest.fixture def patch_A_call(monkeypatch): number = ["NOT ENTERED"] # 用列表包裹字符串 number_list = [] def call(phone_number): number[0] = phone_number # 修改列表内部元素,而非重新绑定变量 number_list.append(phone_number) monkeypatch.setattr("A.call", call) return (number, number_list)
测试函数中访问更新后的值:
def test_make_call(patch_A_call): make_call() print(f"number: {patch_A_call[0][0]}") print(f"number_list: {patch_A_call[1]}")
方法2:用自定义类存储值
class ValueHolder: def __init__(self, value): self.value = value @pytest.fixture def patch_A_call(monkeypatch): number = ValueHolder("NOT ENTERED") number_list = [] def call(phone_number): number.value = phone_number # 修改类实例的属性 number_list.append(phone_number) monkeypatch.setattr("A.call", call) return (number, number_list)
测试函数中访问更新后的值:
def test_make_call(patch_A_call): make_call() print(f"number: {patch_A_call[0].value}") print(f"number_list: {patch_A_call[1]}")
内容的提问来源于stack exchange,提问作者AmagicalFishy
相关产品推荐
相关产品推荐

