Почему модуль не может быть диспетчером контекста (для оператора with)?

Предположим, у нас есть следующие mod.py:

def __enter__():
    print("__enter__<")

def __exit__(*exc):
    print("__exit__< {0}".format(exc))

class cls:
    def __enter__(self):
        print("cls.__enter__<")

    def __exit__(self, *exc):
        print("cls.__exit__< {0}".format(exc))

и следующее его использование:

import mod

with mod:
    pass

Я получаю сообщение об ошибке:

Traceback (most recent call last):
  File "./test.py", line 3, in <module>
    with mod:
AttributeError: __exit__

Согласно документации, инструкция with должна выполняться следующим образом (я считаю, что она не выполняется на шаге 2 и поэтому обрезает список):

  1. Выражение контекста (выражение, заданное в with_item) оценивается для получения менеджера контекста.
  2. __exit__() контекстного менеджера загружается для последующего использования.
  3. Вызывается метод __enter__() менеджера контекста.
  4. и т.д...

Насколько я понял, нет причин, по которым __exit__ не может быть найден. Есть ли что-то, что я пропустил, из-за чего модуль не может работать как менеджер контекста?


person skyking    schedule 15.11.2016    source источник
comment
Почему именно вам нужна эта возможность? Что такое вариант использования?   -  person martineau    schedule 02.12.2020


Ответы (3)


__exit__ — это специальный метод, поэтому Python ищет его по типу. Тип module не имеет такого метода, поэтому он не работает.

См. раздел Поиск специальных методов документации по модели данных Python:

Для пользовательских классов неявные вызовы специальных методов гарантированно работают правильно только в том случае, если они определены для типа объекта, а не в словаре экземпляра объекта.

Обратите внимание, что это относится ко всем специальным методам. Например, если вы добавили в модуль функцию __str__ или __repr__, она также не будет вызываться при печати модуля.

Python делает это, чтобы убедиться, что объекты типов также хешируются и представляются; если бы Python не сделал этого, то попытка поместить объект класса в словарь завершилась бы неудачей, если бы для этого класса был определен метод __hash__ (поскольку этот метод ожидал бы передачи экземпляра для self) .

person Martijn Pieters    schedule 15.11.2016

Вы не можете сделать это легко по причинам, указанным в ответе @Martijn Pieters. Однако с небольшой дополнительной работой это возможно возможно, потому что значения в sys.modules не обязательно должны быть экземплярами класса встроенного модуля, они могут быть экземплярами вашего собственного пользовательского класса со специальными методами контекстный менеджер требует.

Вот применение этого к тому, что вы хотите сделать. Учитывая следующее mod.py:

import sys

class MyModule(object):
    def __enter__(self):
        print("__enter__<")

    def __exit__(self, *exc):
        print("__exit__> {0}".format(exc))

# replace entry in sys.modules for this module with an instance of MyModule
_ref = sys.modules[__name__]
sys.modules[__name__] = MyModule()

И следующее его использование:

import mod

with mod:
    print('running within context')

Будет производить этот вывод:

__enter__<
running within context
__exit__> (None, None, None)

См. это вопрос для получения информации о том, зачем нужен _ref.

person martineau    schedule 15.11.2016
comment
в этот момент вы должны спросить себя зачем вам вообще это нужно делать? - person Martijn Pieters; 15.11.2016
comment
@Martijn: Замена модуля экземпляром пользовательского класса иногда полезна, потому что позволяет делать вещи, которые невозможно сделать с обычным объектом модуля, например, управлять доступом к атрибутам, однако я склонен согласиться, что это делается для того, чтобы модуль мог быть использовать в качестве менеджера контекста может быть немного натянуто. - person martineau; 15.11.2016
comment
Да, я знаю, что есть варианты использования, просто я не уверен, что это один из них. - person Martijn Pieters; 15.11.2016
comment
@Martijn: Просто отвечая на вопрос ОП, одновременно пытаясь избежать вынесения суждений без дополнительной информации об их мотивах. - person martineau; 15.11.2016
comment
Я далеко не такой питонист, как Мартейн, но мне это тоже кажется неправильным. Все, что он делает, это делает строку импорта немного короче, но вводит «магию», которую можно легко потерять. - person Tony Suffolk 66; 16.11.2016
comment
@Tony: Единственная причина, по которой это делается в двух строках показанного кода, заключается в том, что я не хотел жестко указывать имя модуля в коде (поскольку файл, в котором он находится, не имеет имени modulename.py в моей системе. При фактическом использовании его можно импортировать точно так же, как и любой другой: import modulename Поскольку это отвлекает людей, я могу его убрать. - person martineau; 16.11.2016
comment
У меня проблема не с программным импортом — у меня есть много собственного кода, который делает его гибким, управляемым данными и т. д. У меня проблема со вставкой класса в «sys.modules». Кажется, все, что он делает, это избавляет вас от необходимости импортировать класс и делает любой код, желающий этого, немного менее читаемым. - person Tony Suffolk 66; 16.11.2016
comment
@Tony: я действительно узнал об этой технике в одной из Python Cookbooks Алекса Мартелли. Он сказал, что возможность замены модуля экземпляром класса была преднамеренной функцией в дизайне Python (хотя не обязательно позволять использовать его в качестве менеджера контекста). Как я сказал в предыдущем комментарии, я согласен с тем, что использование его для этого может быть сомнительным, и в основном просто хотел показать, что на самом деле это можно сделать, но в основном просто чтобы больше людей знали об этой возможности. - person martineau; 16.11.2016
comment
Как насчет изменения атрибута __class__ модуля для класса, наследующего тип модуля? - person hl037_; 02.12.2020

Более мягкая версия, чем предложенная Мартино, чуть менее полемичная:

import sys

class CustomModule(sys.modules[__name__].__class__):
  """
  Custom module
  """
  def __enter__(self):
    print('enter')

  def __exit__(self, *args, **kwargs):
    print('exit')


sys.modules[__name__].__class__ = CustomModule

Вместо замены модуля (что может вызвать бесчисленное множество проблем) просто замените класс на один, унаследованный от исходного класса. Таким образом, исходный объект модуля сохраняется, нет необходимости в другой ссылке (предотвращающей сборку мусора), и он будет работать с любым пользовательским импортером. Обратите внимание на тот важный факт, что объект модуля создается и добавляется в sys.modules ДО выполнения кода модуля.

Обратите внимание, что таким образом вы можете добавить любой магический метод

person hl037_    schedule 02.12.2020
comment
Умно - мне нравится, как это позволяет избежать необходимости в дополнительной ссылке, которую требует мой ответ. - person martineau; 02.12.2020