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

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

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

问题

以下是您要翻译的内容:

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

A.py

  1. from B import call
  2. def make_call():
  3. print("我正在打电话!")
  4. call(phone_number="867-5309")

B.py

  1. def call(phone_number):
  2. pass

test_A.py

  1. import pytest
  2. from A import make_call
  3. @pytest.fixture
  4. def patch_A_call(monkeypatch):
  5. number = "未输入"
  6. number_list = []
  7. def call(phone_number):
  8. nonlocal number
  9. number = phone_number
  10. number_list.append(phone_number)
  11. monkeypatch.setattr("A.call", call)
  12. return (number, number_list)
  13. def test_make_call(patch_A_call):
  14. make_call()
  15. print(f"number: {patch_A_call[0]}")
  16. print(f"number_list: {patch_A_call[1]}")

打印出的内容是:

  1. number: 未输入
  2. number_list: [867-5309]

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

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

英文:

So this involves a maybe unusual chain of things:

A.py

  1. from B import call
  2. def make_call():
  3. print("I'm making a call!")
  4. call(phone_number="867-5309")

B.py

  1. def call(phone_number):
  2. pass

test_A.py

  1. import pytest
  2. from A import make_call
  3. @pytest.fixture
  4. def patch_A_call(monkeypatch):
  5. number = "NOT ENTERED"
  6. number_list = []
  7. def call(phone_number):
  8. nonlocal number
  9. number = phone_number
  10. number_list.append(phone_number)
  11. monkeypatch.setattr("A.call", call)
  12. return (number, number_list)
  13. def test_make_call(patch_A_call):
  14. make_call()
  15. print(f"number: {patch_A_call[0]}")
  16. print(f"number_list: {patch_A_call[1]}")

What's printed is:

  1. number: NOT ENTERED
  2. 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

  1. 如果您想查看如何调用`call`我认为一个更简单的选择是将其替换为一个模拟对象
  2. ```python
  3. from unittest import mock
  4. from A import make_call
  5. @mock.patch('A.call')
  6. def test_make_call(fake_call):
  7. make_call()
  8. 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(); 我们可以像这样重写您的夹具:

  1. import pytest
  2. from unittest import mock
  3. from A import make_call
  4. @pytest.fixture
  5. def patch_A_call(monkeypatch):
  6. call_recorder = mock.Mock()
  7. monkeypatch.setattr("A.call", call_recorder)
  8. return call_recorder
  9. def test_make_call(patch_A_call):
  10. make_call()
  11. assert patch_A_call.call_args.kwargs["phone_number"] == "867-5309"

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

  1. <details>
  2. <summary>英文:</summary>
  3. 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'

  1. 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.
  2. This requires substantially less code.
  3. 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.
  4. ---
  5. The primary problem with your solution is that your `patch_A_call` fixture is called once, **before** your `test_make_call` method executes.
  6. 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`.
  7. 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.
  8. ---
  9. 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"

  1. This accomplishes pretty much the same thing.
  2. </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:

确定