Python:静态变量装饰器

4 投票
8 回答
9679 浏览
提问于 2025-04-15 15:18

我想创建一个像下面这样的装饰器,但我想不出一个可行的实现方案。我开始觉得这可能不太可能,但还是想先问问你们。

我知道在Python中有很多其他方法可以创建静态变量,但我觉得那些方法都不好看。如果可以的话,我真的想使用下面的语法。

@static(x=0)
def f():
    x += 1
    print x

f() #prints 1
f() #prints 2

我不在乎static的实现是长还是复杂,只要它能像上面那样工作就行。

我创建了这个版本,但它只允许使用<function>.<varname>的语法,这样在函数和变量名较长时就会变得很麻烦。

def static(**assignments):
    def decorate(func):
        for var, val in assignments.items():
            setattr(func, var, val)
        return func
    return decorate

我想过各种方法,但都没能成功,以下是一些我尝试过的:

  1. 把f(被装饰的函数)改成一个可调用的类,然后以某种方式在self中透明地存储静态变量。
  2. 在装饰器内部修改f()的全局变量,并以某种方式将'global x'语句插入到f的代码中。
  3. 把f改成一个生成器,手动绑定变量,然后直接执行f的代码。

8 个回答

8

这里有一个非常简单的解决方案,它的工作方式就像普通的Python静态变量一样。

def static(**kwargs):
  def wrap(f):
    for key, value in kwargs.items():
      setattr(f, key, value)
    return f
  return wrap

使用示例:

@static(a=0)
def foo(x):
  foo.a += 1
  return x+foo.a

foo(1)  # => 2
foo(2)  # => 4
foo(14) # => 17

这个方法更接近于Python中处理静态变量的常规方式。

def foo(x):
  foo.a += 1
  return x+foo.a
foo.a = 10
9

当你的装饰器拿到函数对象 f 时,这个函数已经被编译过了。具体来说,它在编译时就知道 x 是局部变量(因为它是用 += 赋值的)。在 2.* 版本中,你可以通过在 f 开头加上 exec '' 来打破这种优化,但这样会严重影响性能;而在 2.* 版本中,你无法打破这种优化。简单来说,如果你想用你想要的语法,就得重新编译 f(如果你知道源代码在运行时会可用,可以恢复源代码,或者更难的是,通过字节码黑客来实现)并对源代码进行某种修改——一旦决定这样做,最简单的方法就是把 xf 的整个主体中改成 f.x

就我个人而言,如果我发现自己在拼命想要改变某种语言(或其他技术)来满足我的需求,我会意识到我可能在用错语言(或其他技术)。如果这些需求非常重要,那就必须换技术;如果这些需求不是那么重要,那就放弃它们。

无论如何,我都不想把语言扭曲得太远,偏离它明显的设计意图。即使我想出了某种黑科技、脆弱的解决方案,肯定也无法维护。在这种情况下,Python 的设计意图非常明确:在函数内部重新绑定的变量是该函数的局部变量,除非明确指定为全局变量——就这么简单。所以,你试图让在函数内部重新绑定的变量有完全不同于“局部变量”的含义,正是这种不必要的斗争。

编辑:如果你愿意放弃坚持使用局部变量作为你的“静态变量”,那么你就不再与 Python 对抗,而是“顺应”语言的设计(尽管 globalnonlocal 的设计确实有点问题,不过那是另一个话题;-)。所以,例如:

class _StaticStuff(object):
  _static_stack = []
  def push(self, d):
    self._static_stack.append(d)
  def pop(self):
    self._static_stack.pop()
  def __getattr__(self, n):
    return self._static_stack[-1][n]
  def __setattr__(self, n, v):
    self._static_stack[-1][n] = v
import __builtin__
__builtin__.static = _StaticStuff()

def with_static(**variables):
  def dowrap(f):
    def wrapper(*a, **k):
      static.push(variables)
      try: return f(*a, **k)
      finally: static.pop()
    return wrapper
  return dowrap

@with_static(x=0)
def f():
    static.x += 1
    print static.x

f()
f()

这就像你想要的那样工作,先打印 1 然后打印 2。(我使用 __builtin__ 是为了让使用 with_static 装饰任何模块中的函数变得简单。)你可以有几种不同的实现,但任何好的实现的关键点是“静态变量”将是合格的名称,而不是局部变量——这明确表明它们不是局部变量,顺应语言的设计等等。(类似的内置容器,以及基于它们的合格名称,应该在 Python 的设计中使用,而不是 globalnonlocal 的设计缺陷,以指示其他类型的变量,这些变量不是局部的,因此不应该使用局部变量……唉,你可以自己实现一个 globvar 特殊容器,和上面的 static 一样,甚至不需要装饰,尽管我不太确定这对于 nonlocal 的情况是否完全可行[也许加上一些装饰和一点黑魔法……;=])。

编辑:有评论指出,给一个返回闭包的函数装饰器时,代码是无法工作的(而不是装饰闭包本身)。没错:当然,你必须装饰使用 static 的具体函数(根据函数 static 变量的定义,只有一个!),而不是一个随机的函数,这个函数实际上并没有使用 static,而只是恰好与那个使用的函数在某种词法上有联系。例如:

def f():
  @with_static(x=0)
  def g():
    static.x += 1
    print static.x
  return g

x = f()
x()
x()

这样工作,而把装饰器移到 f 而不是 g 就不行(也不可能)。

如果实际需求不是关于静态变量(只在单个函数内可见和使用),而是某种混合的东西,可以在某些特定的函数组合中使用,那么需要非常精确地指定(而且无疑需要非常不同的实现,具体取决于实际的规格是什么)——理想情况下,这需要在一个新的、单独的 StackOverflow 问题中进行,因为这个问题(特别是关于静态的)以及对此特定问题的回答,已经足够大了。

7

这里有一个看起来可以用的装饰器。需要注意的是,由于无法从外部设置局部变量,所以在函数的最后需要返回 locals()(我编程经验不多,如果有其他方法我也不知道)。

class Static(object):
    def __init__(self, **kwargs):
        self.kwargs = kwargs

    def __call__(self, f):
        def wrapped_f():
            try:
                new_kwargs = {}
                for key in self.kwargs:
                    i = getattr(f, key)
                    new_kwargs[key] = i
                self.kwargs = new_kwargs
            except:
                pass
            for key, value in f(**self.kwargs).items():
                setattr(f, key, value)
        return wrapped_f

@Static(x=0, y=5, z='...')
def f(x, y, z):
    x += 1
    y += 5
    print x, y, z
    return locals()

输出结果将是:

>>> f()
1 10 ...
>>> f()
2 15 ...
>>> f()
3 20 ...

编辑:

我在 http://code.activestate.com/recipes/410698/ 找到了一些东西,决定尝试把它加到这个里面。现在它可以不需要返回了。

再次编辑:改动了一下,让它快了几秒。

编辑3:改成了函数而不是类。

def static(**kwargs):
    def wrap_f(function):
        def probeFunc(frame, event, arg):
            if event == 'call':
                frame.f_locals.update(kwargs)
                frame.f_globals.update(kwargs)
            elif event == 'return':
                for key in kwargs:
                    kwargs[key] = frame.f_locals[key]
                sys.settrace(None)
            return probeFunc
        def traced():
            sys.settrace(probeFunc)
            function()
        return traced
    return wrap_f

测试结果:

@static(x=1)
def f():
    x += 1

global_x = 1
def test_non_static():
    global global_x
    global_x += 1


print 'Timeit static function: %s' % timeit.timeit(f)
print 'Timeit global variable: %s' % timeit.timeit(test_non_static)

输出:

Timeit static function: 5.10412869535
Timeit global variable: 0.242917510783

使用 settrace 会让速度明显变慢。

撰写回答