С помощью выражений присваивания в 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: "Должен быть один - и желательно только один - очевидный способ сделать это".?

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

Другие вопросы по тегам