【问题标题】:Python: standard function and context manager?Python:标准函数和上下文管理器?
【发布时间】:2016-05-22 21:53:03
【问题描述】:

在 python 中,有许多函数既可以作为标准函数,也可以作为上下文管理器。例如open() 可以被称为:

my_file=open(filename,'w')

with open(filename,'w') as my_file:

两者都给你一个my_file 对象,可以用来做你需要做的任何事情。一般来说,后者更可取,但有时人们也可能想要前者。

我已经能够弄清楚如何编写上下文管理器,方法是使用 __enter____exit__ 函数创建一个类,或者在函数上使用 @contextlib.contextmanager 装饰器和 yield 而不是 @ 987654329@。但是,当我这样做时,我不能再直接使用该函数 - 例如,使用装饰器,我得到一个 _GeneratorContextManager 对象而不是想要的结果。当然,如果我把它做成一个类,我只会得到一个生成器类的实例,我认为它本质上是一样的。

那么我该如何设计一个函数(或类),既可以作为函数工作,返回对象,也可以作为上下文管理器,返回_GeneratorContextManager 或类似的?

编辑:

例如,假设我有一个类似以下的函数(这是高度简化的):

def my_func(arg_1,arg_2):
    result=arg_1+arg_2
    return my_class(result)

因此,该函数接受许多参数,对它们进行处理,并使用这些处理的结果来初始化一个类,然后返回该类。最终结果是我有一个my_class 的实例,就像我调用open 时我会有一个file 对象一样。如果我希望能够将此函数用作上下文管理器,我可以像这样修改它:

@contextlib.contextmanager
def my_func(arg_1,arg_2):
    result=arg_1+arg_2 # This is roughly equivalent to the __enter__ function
    yield my_class(result)
    <do some other stuff here> # This is roughly equivalent to the __exit__function

作为上下文管理器调用时效果很好,但作为直接函数调用时,我不再获得my_class 的实例。也许我只是做错了什么?

编辑 2:

请注意,我确实可以完全控制 my_class,包括向其添加功能的能力。从下面接受的答案中,我可以推断出我的困难源于一个基本的误解:我认为无论我调用什么(上例中的my_func)都需要具有__exit____enter__ 函数。这是不正确的。事实上,只有函数返回(上例中的my_class)需要这些函数才能充当上下文管理器。

【问题讨论】:

  • open 是一个返回类实例的函数。无论您使用myfile = open(filename) 还是with open(filename) as myfile,它仍然是同一类的实例。什么都没有改变。
  • @zondo 是的,我正在尝试编写的函数也是如此。但是,当我将函数包装在 @contextlib.contextmanager 装饰器中并将其作为标准函数调用时,我返回的类不是我从函数中“生成”的类。只有当我将它称为上下文管理器时,我才能获得该类。我将添加一个简单的示例。
  • 在你的类中定义一个__call__ 方法。
  • @apex-meme-lord:所以我会用__call__ 方法编写上下文管理器类,它会返回我想要的实例化类?我没有明确使用__init__ 方法这一事实是否意味着当我执行my_instance=cm_class(a,b) 之类的操作时将使用__call__?我的理解是调用实例时使用__call__,我还没有实例。
  • 添加__call__ 不是您想要的。这将允许您使用x = my_func(); x(),但在这种情况下这无济于事。

标签: python contextmanager


【解决方案1】:

您将遇到的困难是,对于同时用作上下文管理器 (with foo() as x) 和常规函数 (x = foo()) 的函数,从函数返回的对象需要同时具有__enter____exit__ 方法……在一般情况下,没有一个很好的方法可以向现有对象添加方法。

一种方法可能是创建一个使用__getattr__ 将方法和属性传递给原始对象的包装类:

class ContextWrapper(object):
    def __init__(self, obj):
        self.__obj = obj

    def __enter__(self):
        return self

    def __exit__(self, *exc):
        ... handle __exit__ ...

    def __getattr__(self, attr):
        return getattr(self.__obj, attr)

但这会导致一些微妙的问题,因为它与原始函数返回的对象不完全相同(例如,isinstance 测试将失败,一些内置函数如 iter(obj)不会按预期工作,等等)。

您还可以动态子类化返回的对象,如下所示:https://stackoverflow.com/a/1445289/71522:

class ObjectWrapper(BaseClass):
    def __init__(self, obj):
        self.__class__ = type(
            obj.__class__.__name__,
            (self.__class__, obj.__class__),
            {},
        )
        self.__dict__ = obj.__dict__

    def __enter__(self):
        return self

    def __exit__(self, *exc):
        ... handle __exit__ ...

但这种方法也存在问题(如链接帖子中所述),如果没有 strong 理由,我个人不会很乐意介绍这种魔法。

我通常更喜欢添加显式 __enter____exit__ 方法,或者使用像 contextlib.closing 这样的帮助器:

with closing(my_func()) as my_obj:
    … do stuff …

【讨论】:

  • 啊,我缺少的关键是对象 returned 需要 __enter____exit__ 函数 - 不一定是对象(在这种情况下是函数)称为。因此,通过将这些函数添加到函数返回的类中,__enter__ 函数只需返回 self,它就可以工作了!
【解决方案2】:

为了清楚起见:如果您能够更改 my_class,您当然可以将 __enter__/__exit__ 描述符添加到该类。

如果您无法更改my_class(我从您的问题中推断出),这就是我所指的解决方案:

class my_class(object):

    def __init__(self, result):
        print("this works", result)

class manage_me(object):

    def __init__(self, callback):
        self.callback = callback

    def __enter__(self):
        return self

    def __exit__(self, ex_typ, ex_val, traceback):
        return True

    def __call__(self, *args, **kwargs):
        return self.callback(*args, **kwargs)


def my_func(arg_1,arg_2):
    result=arg_1+arg_2
    return my_class(result)


my_func_object = manage_me(my_func) 

my_func_object(1, 1)
with my_func_object as mf:
    mf(1, 2)

作为装饰者:

@manage_me
def my_decorated_func(arg_1, arg_2):
    result = arg_1 + arg_2
    return my_class(result)

my_decorated_func(1, 3)
with my_decorated_func as mf:
    mf(1, 4)

【讨论】:

  • 我完全可以更改 my_class。我的困惑源于我没有直接调用(或实例化)my_class - 相反,我调用了一个返回 my_class 实例的函数,并且我希望能够将该函数用作函数或上下文manager - 就像你可以使用 open() 函数一样。我没有意识到 returned 对象需要充当上下文管理器 - 我认为我正在调用的函数需要成为上下文管理器。跨度>
猜你喜欢
  • 2012-03-02
  • 2021-08-24
  • 1970-01-01
  • 1970-01-01
  • 2018-09-18
  • 2019-01-22
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多