临时改变Python中变量的值

12 投票
5 回答
4426 浏览
提问于 2025-04-18 06:13

Python 3.4 提供了一个很方便的工具,可以暂时改变输出的地方,也就是把输出重定向到其他地方:

# From https://docs.python.org/3.4/library/contextlib.html#contextlib.redirect_stdout
with redirect_stdout(sys.stderr):
    help(pow)

这段代码并不复杂,但我不想每次都写它,特别是因为它经过了一些设计,使得它可以重复使用:

class redirect_stdout:
    def __init__(self, new_target):
        self._new_target = new_target
        # We use a list of old targets to make this CM re-entrant
        self._old_targets = []

    def __enter__(self):
        self._old_targets.append(sys.stdout)
        sys.stdout = self._new_target
        return self._new_target

    def __exit__(self, exctype, excinst, exctb):
        sys.stdout = self._old_targets.pop()

我在想有没有一种通用的方法,可以用 with 语句来暂时改变一个变量的值。sys 模块里还有两个其他的用法,分别是 sys.stderrsys.excepthook

在理想情况下,像这样的代码应该能工作:

foo = 10
with 20 as foo:
    print(foo) # 20
print (foo) # 10

我怀疑我们能否做到这一点,但也许像这样是可能的:

foo = 10
with temporary_set('foo', 20):
    print(foo) # 20
print (foo) # 10

我在 globals() 中摸索了一下,勉强让它工作了,但这并不是大家会选择使用的方法。

更新:虽然我觉得我的 "foo = 10" 的例子解释了我想做的事情,但它们并没有传达一个实际的用例。这里有两个:

  1. 重定向 stderr,和重定向 stdout 类似
  2. 暂时改变 sys.excepthook。我经常进行交互式开发,当我在 excepthook 中添加一些东西(比如用我自己的函数包装原始函数,以便使用日志模块记录异常)时,我通常希望它在某个时候被移除。这样我就不会有越来越多的函数相互包装的情况。这个问题涉及一个相关的问题。

5 个回答

1

那我们来看看什么是 closure(闭包)吧?

也就是说:

#!/usr/bin/python

def create_closure(arg):
    var = arg

    def hide_me():
        return var

    return hide_me


x = 10
clo = create_closure(x)

x *= 2
x2 = clo()
x += 1

print "x: {0}".format(x)
print "x2: {0}".format(x2)

这样会得到:

x: 21
x2: 10

因为这是一个闭包,它可以大幅扩展,用来保存很多其他的变量和状态。之后你可以选择继续使用这个闭包的形式,或者在需要的时候用它来恢复之前的状态。

3

我通常使用这个自定义的 attr_as 上下文管理器:

from contextlib import contextmanager

@contextmanager
def attr_as(obj, field:str, value) -> None:
    old_value = getattr(obj, field)
    setattr(obj, field, value)
    yield
    setattr(obj, field, old_value)

你可以用和 setattr 一样的参数来使用 attr_as

class Foo:
    def __init__(self):
        self.x = 1

foo = Foo()
with attr_as(foo, 'x', 2):
    print(foo.x)

bar = 3
with attr_as(sys.modules[__name__], 'bar', 4):
    print(bar) 

注意,如果你需要保留属性是否存在的状态,而不仅仅是它的值,这也可以做到,只需要多写几行代码:

from contextlib import contextmanager

@contextmanager
def attr_as(obj, field:str, value) -> None:
    old_exists = hasattr(obj, field)
    if old_exists:
        old_value = getattr(obj, field)
    setattr(obj, field, value)
    yield
    if old_exists:
        setattr(obj, field, old_value)
    else:
        delattr(obj, field)
3

我刚发现了另一种聪明的方法,可以使用一个叫做 unittest.mock 的工具,详细信息可以在 这里 找到。

这个方法很通用,因为你可以指定一个包含多个变量的字典:

import unittest.mock
a = b = 1
with unittest.mock.patch.dict(locals(), a=2, b=3):
  print(a, b)  # shows 2, 3
print(a, b)  # shows 1, 1

而且,即使这些变量在当前的环境中没有被提前定义,这个方法也能正常工作。

with unittest.mock.patch.dict(locals(), c=4):
  assert 'c' in locals()
assert 'c' not in locals()
11

我知道这个问题有点老了,但我遇到同样的问题时,找到了我的解决办法:

class test_context_manager():
    def __init__(self, old_object, new_object):
        self.new = new_object
        self.old = old_object
        self.old_code = eval(old_object)
    def __enter__(self):
        globals()[self.old] = self.new
    def __exit__(self, type, value, traceback):
        globals()[self.old] = self.old_code

这个方法看起来不太好,因为它大量使用了全局变量,但似乎能正常工作。

比如说:

x = 5
print(x)
with test_context_manager("x", 7):
    print(x)

print(x)

结果是:

5
7
5

或者用函数来实现:

def func1():
    print("hi")

def func2():
    print("bye")

x = 5
func1()
with test_context_manager("func1", func2):
    func1()

func1()

结果是:

hi
bye
hi
3

基于@arthaigo的回答,下面是一个更简洁的版本:

import contextlib

@contextlib.contextmanager
def temporary_assignment(object, new_value):
  old_value = eval(object)
  globals()[object] = new_value
  yield
  globals()[object] = old_value

撰写回答