С выражениями присваивания в 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

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


person Antti Haapala    schedule 17.07.2018    source источник


Ответы (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 теперь привязан к диспетчеру контекста, а не к новому фиктивному объекту.

person Antti Haapala    schedule 17.07.2018