为什么`nonlocal`关键字不能将外部作用域的变量传递给调用模块?

huangapple go评论64阅读模式
英文:

Why doesn't the `nonlocal` keyword propogate the outer-scoped variable to the calling module?

问题

以下是您要翻译的内容:

所以这涉及到一系列可能不太常见的事情:

A.py

from B import call

def make_call():
    print("我正在打电话!")
    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 = "未输入"
    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: 未输入
number_list: [867-5309]

我预期"867-5309"应该是两个结果的值。

我知道在Python中,列表是通过引用传递的,但我假设nonlocal声明会沿着链路传递变量。为什么它不起作用呢?

英文:

So this involves a maybe unusual chain of things:

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]}")

What's printed is:

number: NOT ENTERED
number_list: [867-5309]

I expected "867-5309" to be the value for both results.

I know that lists are passed by reference in Python—but I assumed that the nonlocal declaration would pass the variable along down the chain.

Why doesn't it work this way?

答案1

得分: 2

如果您想查看如何调用`call`,我认为一个更简单的选择是将其替换为一个模拟对象

```python
from unittest import mock

from A import make_call


@mock.patch('A.call')
def test_make_call(fake_call):
    make_call()
    assert fake_call.call_args.kwargs['phone_number'] == '867-5309'

在这里,我们将A.call替换为unittest.mock.Mock对象。这由make_call调用,并且模拟对象记录了调用参数供以后检查。

这需要的代码要少得多。

请注意,我在这里使用了一个assert语句,但如果这是您的目标,您也可以打印出或以其他方式记录phone_number的值。


您的解决方案的主要问题是,您的patch_A_call夹具在test_make_call方法执行之前被调用一次。

所以虽然nonlocal关键字的工作方式是预期的...您从未看到结果,因为在调用make_call之前,return (number, number_list)语句已经运行了。

您在列表中看到结果是因为列表是一个"容器" -- 当调用make_call时,您将数字添加到它中,并且您可以看到结果,因为返回的列表是从您修补的call方法内部可用的相同对象。


对于我的解决方案,我们不必使用mock.patch(); 我们可以像这样重写您的夹具:

import pytest
from unittest import mock

from A import make_call


@pytest.fixture
def patch_A_call(monkeypatch):
    call_recorder = mock.Mock()
    monkeypatch.setattr("A.call", call_recorder)
    return call_recorder


def test_make_call(patch_A_call):
    make_call()
    assert patch_A_call.call_args.kwargs["phone_number"] == "867-5309"

这几乎实现了相同的目标。


<details>
<summary>英文:</summary>

If you want to see how `call` is called, I think a simpler option is to replace it with a Mock object:

from unittest import mock

from A import make_call

@mock.patch('A.call')
def test_make_call(fake_call):
make_call()
assert fake_call.call_args.kwargs['phone_number'] == '867-5309'


Here, we&#39;re replacing `A.call` with a `unittest.mock.Mock` object. This gets called by `make_call`, and the call arguments are recorded by the Mock object for later inspection.

This requires substantially less code.

Note that I&#39;m using an `assert` statement here, but you could instead print out or otherwise record the value of `phone_number` if that&#39;s your goal.

---

The primary problem with your solution is that your `patch_A_call` fixture is called once, **before** your `test_make_call` method executes.

So while the `nonlocal` keyword is working as intended...you never see the result, because that `return (number, number_list)` statement ran **before** you made the call to `make_call`.

You see the result in the list because a list is a &quot;container&quot; -- you add the number to it when calling `make_call`, and you can see the result because the returned list is the same object available from inside your patched `call` method.

---

For my solution, we don&#39;t have to use `mock.patch()`; we can rewrite your fixture like this:

import pytest
from unittest import mock

from A import make_call

@pytest.fixture
def patch_A_call(monkeypatch):
call_recorder = mock.Mock()
monkeypatch.setattr("A.call", call_recorder)
return call_recorder

def test_make_call(patch_A_call):
make_call()
assert patch_A_call.call_args.kwargs["phone_number"] == "867-5309"


This accomplishes pretty much the same thing.

</details>



huangapple
  • 本文由 发表于 2023年6月29日 08:10:42
  • 转载请务必保留本文链接:https://go.coder-hub.com/76577388.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定