С выражениями присваивания в Python 3.8, почему мы должны использовать 'as' в 'with'?

Теперь, когда PEP 572 принято, Python 3.8 предназначен для использования выражений присваивания, поэтому мы можем использовать выражение присваивания в with, т.е.

with (f := open('file.txt')):
    for l in f:
        print(f)

вместо

with open('file.txt') as f:
    for l in f:
        print(f)

и все будет работать как раньше.

Как используется ключевое слово as с оператором with в Python 3.8? Разве это не против дзен Python: "Должен быть один - и, предпочтительно, только один - очевидный способ сделать это"?


Когда функция была первоначально предложена, не было четко указано, должно ли выражение присваивания заключаться в скобки в with и что

with f := open('file.txt'):
    for l in f:
        print(f)

может работать. Однако в Python 3.8a0

with f := open('file.txt'):
    for l in f:
        print(f)

вызовет

  File "<stdin>", line 1
    with f := open('file.txt'):
           ^
SyntaxError: invalid syntax

но выражение в скобках работает.

Ответ 1

TL; DR: поведение не одинаково для обеих конструкций, хотя между этими двумя примерами не будет заметных различий.

Вы почти никогда не должны использовать := в выражении with, и иногда это очень неправильно. В случае сомнений всегда используйте with ... as ..., когда вам нужен управляемый объект в блоке with.


В with context_manager as managed managed связан с возвращаемым значением context_manager.__enter__(), тогда как в with (managed := context_manager), managed связан с самим context_manager, а возвращаемое значение вызова метода __enter__() отбрасывается. Поведение почти идентично для открытых файлов, потому что их метод __enter__ возвращает self.

Первый отрывок примерно аналогичен

_mgr = (f := open('file.txt')) # 'f' is assigned here, even if '__enter__' fails
_mgr.__enter__()               # the return value is discarded

exc = True
try:
    try:
        BLOCK
    except:
        # The exceptional case is handled here
        exc = False
        if not _mgr.__exit__(*sys.exc_info()):
            raise
        # The exception is swallowed if exit() returns true
finally:
    # The normal and non-local-goto cases are handled here
    if exc:
        _mgr.__exit__(None, None, None)

тогда как форма as будет

_mgr = open('file.txt')   # 
_value = _mgr.__enter__() # the return value is kept

exc = True
try:
    try:
        f = _value        # here f is bound to the return value of __enter__
                          # and therefore only when __enter__ succeeded
        BLOCK
    except:
        # The exceptional case is handled here
        exc = False
        if not _mgr.__exit__(*sys.exc_info()):
            raise
        # The exception is swallowed if exit() returns true
finally:
    # The normal and non-local-goto cases are handled here
    if exc:
        _mgr.__exit__(None, None, None)

т.е. with (f := open(...)) установит f в возвращаемое значение open, тогда как with open(...) as f связывает f с возвращаемым значением неявного вызова метода __enter__().

Теперь, в случае файлов и потоков, file.__enter__() вернет self, если это удастся, поэтому поведение для этих двух подходов почти одинаково - единственное отличие состоит в том, что __enter__ выдает исключение.

Тот факт, что выражения присваивания часто работают вместо as, является обманчивым, поскольку существует много классов, в которых _mgr.__enter__() возвращает объект, отличный от self. В этом случае выражение присваивания работает иначе: вместо управляемого объекта назначается менеджер контекста. Например, unittest.mock.patch - менеджер контекста, который будет возвращать фиктивный объект. Документация для этого имеет следующий пример:

>>> thing = object()
>>> with patch('__main__.thing', new_callable=NonCallableMock) as mock_thing:
...     assert thing is mock_thing
...     thing()
...
Traceback (most recent call last):
  ...
TypeError: 'NonCallableMock' object is not callable

Теперь, если бы он был написан для использования выражения присваивания, поведение было бы другим:

>>> thing = object()
>>> with (mock_thing := patch('__main__.thing', new_callable=NonCallableMock)):
...     assert thing is mock_thing
...     thing()
...
Traceback (most recent call last):
  ...
AssertionError
>>> thing
<object object at 0x7f4aeb1ab1a0>
>>> mock_thing
<unittest.mock._patch object at 0x7f4ae910eeb8>

mock_thing теперь связан с менеджером контекста, а не с новым фиктивным объектом.