You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何`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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 10:52:57