传递参数给__enter__方法

60

刚学习 with 语句,特别是从这篇文章中了解的:http://effbot.org/zone/python-with-statement.htm

问题是,我能否向 __enter__ 传递参数?

我有以下代码:

class clippy_runner:
    def __enter__(self):
        self.engine = ExcelConnection(filename = "clippytest\Test.xlsx")
        self.db = SQLConnection(param_dict = DATASOURCES[STAGE_RELATIONAL])

        self.engine.connect()
        self.db.connect()

        return self

我想将文件名和参数字典作为参数传递给__enter__。这是否可行?

7个回答

60

是的,你可以通过添加一些代码来达到这种效果。


    #!/usr/bin/env python

    class Clippy_Runner( dict ):
        def __init__( self ):
            pass
        def __call__( self, **kwargs ):
            self.update( kwargs )
            return self
        def __enter__( self ):
            return self
        def __exit__( self, exc_type, exc_val, exc_tb ):
            self.clear()

    clippy_runner = Clippy_Runner()

    print clippy_runner.get('verbose')     # Outputs None
    with clippy_runner(verbose=True):
        print clippy_runner.get('verbose') # Outputs True
    print clippy_runner.get('verbose')     # Outputs None

7
这似乎是正确答案,因为您不需要在with语句中创建变量,而可以使用已经创建的对象(如锁定)并将变量传递给with语句。非常好的回答! - rubmz
2
非常好的答案!这应该被接受,因为它可以在for循环中使用,例如,每次迭代都不需要实例化一个新对象。 - Maor Refaeli

51
不行。你不能这样做。你需要将参数传递给__init__()函数。
class ClippyRunner:
    def __init__(self, *args):
        # save args as attributes 
        self._args = args
    
    def __enter__(self):
        # Do something with args
        print(self._args)


with ClippyRunner(args) as something:
    # work with "something"
    pass

7
我有点糊涂。因为您在 __init__ 中仅使用 pass,您是否暗示 __init__ 中传递给 args__enter__ 函数中可用? - Hovis Biddle
1
Hovis:传递给init的参数可以保存,然后在enter方法中使用。 def __init__(self, filename, param_dict): self.filename = filename self.param_dict = param_dict def __enter__(self):self.filename ... - spazm
2
为什么这个答案被接受了?我似乎不是唯一一个认为这是最差的答案的人。“不,你不能,你要传参数给__init__..”这只是不正确的简单说法,通过init传参确实是其中一种方法,但绝不是唯一的方法,并且也不总是最合适的方式。如果您想像OP所希望的那样在with .. as ..行中将args传递给上下文管理器,那么绝对可以这样做,这通常是最合适的方式,具体取决于应用程序。(请参见所有其他答案) - user9413641

21

被接受的答案(我认为是不正确的)表明你不能这样做,而应该使用以下方法;

class Comedian:
    def __init__(self, *jokes):
        self.jokes = jokes
    def __enter__(self):
        jokes = self.jokes
        #say some funny jokes
        return self

虽然通常情况下您可能会这样做,但这并不总是最佳解决方案,甚至不是一个解决方案,而且绝对不是唯一的解决方案!

我假设您想要做的是与以下类似的事情;

funny_object = Comedian()
with funny_object('this is a joke') as humor:
    humor.say_something_funny()

如果情况确实如此,并且事情并不比这更复杂,那么您只需要执行以下操作:

class Comedian:
    def __enter__(self):
        jokes = self.jokes
        #say some funny jokes
        return self
    def __call__(self, *jokes):
        self.jokes = jokes
        return self  # EDIT as pointed out by @MarkLoyman

那样您仍然可以用任何想要的参数初始化对象,并像通常一样对对象执行任何其他操作,但当您将对象用作上下文管理器时,您首先调用它的 call 函数并为上下文管理器设置一些参数。

这里重要的是要准确理解Python中上下文管理器的工作原理。

在Python中,上下文管理器是定义了一个 enter 方法的任何对象。当您执行以下操作时,将自动调用此方法;

with object as alias:
    alias.do_stuff()
    ..

注意对象后面没有一对“()”,这是一个隐式函数调用,并且它不需要任何参数。

你可能已经从enter传递参数的想法中获得了灵感;

with open(filename) as file:
    "do stuff with file..

但这与覆盖 enter 不同,因为 "open" 不是一个对象,而是一个函数。

一个好的练习是打开交互式Python控制台并输入 "open" + [ENTER]

>>> open
<built-in function open>

"

“open”不是上下文管理对象,而是函数。它根本没有enter方法,而是按以下方式定义的;

"
@contextmanager
def open(..):
    ...

你可以以相同的方式定义自己的上下文管理器函数,甚至可以覆盖“open”的定义。

不过,在我看来,如果您需要创建一个对象,然后稍后使用它作为具有参数的上下文管理器(..我所做的),最好的方法是给对象添加一个返回定义了enter方法的临时对象的方法。示例如下:

class Comedian:
    def context(audience):
        class Roaster:
            context = audience
            def __enter__(self):
                audience = self.__class__.context
                # a comedian needs to know his/her audience.
        return Roaster(audience)

funny_thing = Comedian()
with funny_thing.context('young people') as roaster:
    roaster.roast('old people')

该示例中调用链的顺序是:Comedian.__init__() -> Comedian.context(args) -> Roaster.__enter__()。我觉得这个答案在众多答案中缺失,所以我添加了它。 编辑:根据 @MarkLoyman 的指示,将Comedian.__call__中的"return self"添加进去。

2
这是对Python基本组件的精彩解释,将它们结合在一起,使所有内容都变得清晰明了。 - Dan Nissenbaum
1
我认为你在 def __call__(self, ...): 中缺少了一个 return self - Mark Loyman
@MarkLoyman,你说得对,谢谢指出。我已经修改了它。 - user9413641
太棒了,正是我在寻找的东西。我有一个高级的记录器,我想要记录正在被记录的过程的各个阶段,通过在记录器中设置自定义字段,然后在该阶段结束时将其删除。这样可以整洁地完成,并提供一个良好的概览。 - undefined

7

4
我同意这个观点。我有一些只在特定环境下相关的设置。将它们传递给__init__是愚蠢的。 - Muposat

3

你只需要通过类构造函数将值传递给__init__吗?


0

你可以在实例中保存状态:(顺便说一句,我不建议这样做,因为它会导致代码混乱)

class Thing:

    def __init__(self):
        self.name = 'original'

    def __call__(self, name):
        self._original_name = self.name
        self.name = name
        return self

    def __enter__(self):
        pass

    def __exit__(self, exc_type, exc_value, traceback):
        self.name = self._original_name

这是测试:
instance = Thing()
assert instance.name == 'original'
with instance('new name'):
    assert instance.name == 'new name'

assert instance.name == 'original'

0
我认为使用contextlib.contextmanager(原生包)是一个好主意。
更多细节如下。

一个简单的例子

from contextlib import contextmanager


class Person:
    def __init__(self, name):
        self.name = name

    def say_something(self, msg):
        print(f'{self.name}: {msg}')

    @staticmethod
    @contextmanager
    def enter(name,  # <-- members of construct
              para_1, options: dict  # <-- Other parameter that you wanted.
              ):
        with Person(name) as instance_person:
            try:
                print(para_1)
                print(options)
                yield instance_person
            finally:
                ...

    def __enter__(self):
        print(self.name)
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print('__exit__')


with Person.enter('Carson', para_1=1, options=dict(key='item_1')) as carson:
    carson.say_something('age=28')
    print('inside')
print('outside')

输出

Carson
1
{'key': 'item_1'}
Carson: age=28
inside
__exit__
outside

你的例子

from typing import Union
from contextlib import contextmanager


def main():
    with ClippyRunner.enter(filename="clippytest/Test.xlsx",
                            param_dict='DATASOURCES[STAGE_RELATIONAL]') as clippy_runner:
        clippy_runner.do_something()


class ConnectBase:
    def connect(self):
        print(f'{type(self).__name__} connect')

    def disconnect(self):
        print(f'{type(self).__name__} disconnect')


class ExcelConnection(ConnectBase):
    def __init__(self, filename):
        self.filename = filename


class SQLConnection(ConnectBase):
    def __init__(self, param_dict):
        self.param_dict = param_dict


class ClippyRunner:
    def __init__(self, engine: Union[ExcelConnection], db: Union[SQLConnection]):
        self.engine = engine
        self.db = db

    def do_something(self):
        print('do something...')

    @staticmethod
    @contextmanager
    def enter(filename, param_dict):
        with ClippyRunner(ExcelConnection(filename),
                          SQLConnection(param_dict)) as cr:
            try:
                cr.engine.connect()
                cr.db.connect()
                yield cr
            except:
                cr.release()  # disconnect
            finally:
                ...

    def __enter__(self):
        return self

    def release(self):
        self.engine.disconnect()
        self.db.disconnect()

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.release()


if __name__ == '__main__':
    main()

输出

ExcelConnection connect
SQLConnection connect
do something...
ExcelConnection disconnect
SQLConnection disconnect

关于contextmanager

contextmanager主要有以下三个作用:

  1. 在代码块之前运行一些代码。
  2. 在代码块之后运行一些代码。
  3. 可选地,抑制在代码块中引发的异常。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接