如何在Python中将变量放入栈/上下文中
简单来说,我想在栈上放一个变量,这个变量可以被栈上所有后续的调用访问,直到这个块结束。在Java中,我会用一个静态的线程本地变量和一些辅助方法来解决这个问题,这样就可以在方法中访问它。
举个典型的例子:你收到一个请求,然后打开一个数据库连接。在请求完成之前,你希望所有的代码都能使用这个数据库连接。完成请求并关闭后,你再关闭这个数据库连接。
我需要这个的原因是为了生成报告。每个报告由多个部分组成,每个部分可能依赖于不同的计算,有时不同的部分又会依赖于相同的计算。因为我不想重复进行耗时的计算,所以我需要缓存这些计算结果。我的想法是给方法加上一个缓存装饰器。这个缓存会根据方法名、模块和参数生成一个ID,然后查看这个ID是否已经在栈变量中计算过,如果没有,就执行这个方法。
我会通过展示我当前的实现来进一步说明。我想做的是简化那些实现计算的代码。
首先,我有一个中央缓存访问对象,我称之为MathContext:
class MathContext(object):
def __init__(self, fn):
self.fn = fn
self.cache = dict()
def get(self, calc_config):
id = create_id(calc_config)
if id not in self.cache:
self.cache[id] = calc_config.exec(self)
return self.cache[id]
fn参数是与创建上下文相关的文件名,从这个文件中可以读取数据进行计算。
接下来是Calculation类:
class CalcBase(object):
def exec(self, math_context):
raise NotImplementedError
这里有一个简单的斐波那契数列的例子。实际上这些方法并不是递归的,而是处理大量数据,但这个例子可以展示你如何依赖其他计算:
class Fibonacci(CalcBase):
def __init__(self, n): self.n = n
def exec(self, math_context):
if self.n < 2: return 1
a = math_context.get(Fibonacci(self.n-1))
b = math_context.get(Fibonacci(self.n-2))
return a+b
我希望斐波那契数列的方法只是一个被装饰的方法:
@cache
def fib(n):
if n<2: return 1
return fib(n-1)+fib(n-2)
在math_context超出作用域时,它的所有缓存值也会消失。我希望装饰器也有同样的效果。也就是说,在某个时刻,所有通过@cache缓存的内容都会被解除引用,以便进行垃圾回收。
相关问题:
4 个回答
“你收到一个请求,然后打开一个数据库连接……最后关闭这个数据库连接。”
这就是对象的作用。你可以创建一个连接对象,把它传递给其他对象,等用完了再关闭它。全局变量不太合适。只需把这个值作为参数传递给其他正在工作的对象就可以了。
“每个报告由多个部分组成,每个部分可能依赖于不同的计算,有时候不同的部分又会部分依赖于相同的计算……我需要缓存这些计算结果。”
这也是对象的作用。你可以创建一个字典,里面存放有用的计算结果,然后在报告的各个部分之间传递这个字典。
你不需要去搞什么“栈变量”、“静态线程局部变量”之类的东西。只需把普通的变量作为参数传递给普通的方法就行了。这样你会开心很多。
class MemoizedCalculation( object ):
pass
class Fibonacci( MemoizedCalculation ):
def __init__( self ):
self.cache= { 0: 1, 1: 1 }
def __call__( self, arg ):
if arg not in self.cache:
self.cache[arg]= self(arg-1) + self(arg-2)
return self.cache[arg]
class MathContext( object ):
def __init__( self ):
self.fibonacci = Fibonacci()
你可以这样使用它
>>> mc= MathContext()
>>> mc.fibonacci( 4 )
5
你可以定义任意数量的计算,并把它们都放进一个单一的容器对象里。
如果你愿意,可以把 MathContext 做成一个正式的上下文管理器,这样就可以和with语句一起使用。只需在 MathContext 中添加这两个方法。
def __enter__( self ):
print "Initialize"
return self
def __exit__( self, type_, value, traceback ):
print "Release"
然后你可以这样做。
with MathContext() as mc:
print mc.fibonacci( 4 )
在with语句结束时,可以确保调用了__exit__方法。
这个问题看起来其实是两个问题。
- a) 共享数据库连接
- b) 缓存/记忆化
b) 你们自己已经回答了。
a) 我不太明白你为什么需要把它放在栈上?你可以这样做:
- 你可以使用一个类,把连接作为它的属性。
- 你可以给所有的函数加上装饰器,让它们从一个中心位置获取连接。
- 每个函数可以明确地使用一个全局连接的方法。
- 你可以创建一个连接并在各处传递,或者创建一个上下文对象,把上下文传递过去,连接可以是上下文的一部分。
等等等等。
我做了一个可能正好符合你需求的东西。它可以同时用作装饰器和上下文管理器:
from __future__ import with_statement
try:
import cPickle as pickle
except ImportError:
import pickle
class cached(object):
"""Decorator/context manager for caching function call results.
All results are cached in one dictionary that is shared by all cached
functions.
To use this as a decorator:
@cached
def function(...):
...
The results returned by a decorated function are not cleared from the
cache until decorated_function.clear_my_cache() or cached.clear_cache()
is called
To use this as a context manager:
with cached(function) as function:
...
function(...)
...
The function's return values will be cleared from the cache when the
with block ends
To clear all cached results, call the cached.clear_cache() class method
"""
_CACHE = {}
def __init__(self, fn):
self._fn = fn
def __call__(self, *args, **kwds):
key = self._cache_key(*args, **kwds)
function_cache = self._CACHE.setdefault(self._fn, {})
try:
return function_cache[key]
except KeyError:
function_cache[key] = result = self._fn(*args, **kwds)
return result
def clear_my_cache(self):
"""Clear the cache for a decorated function
"""
try:
del self._CACHE[self._fn]
except KeyError:
pass # no cached results
def __enter__(self):
return self
def __exit__(self, type, value, traceback):
self.clear_my_cache()
def _cache_key(self, *args, **kwds):
"""Create a cache key for the given positional and keyword
arguments. pickle.dumps() is used because there could be
unhashable objects in the arguments, but passing them to
pickle.dumps() will result in a string, which is always hashable.
I used this to make the cached class as generic as possible. Depending
on your requirements, other key generating techniques may be more
efficient
"""
return pickle.dumps((args, sorted(kwds.items())), pickle.HIGHEST_PROTOCOL)
@classmethod
def clear_cache(cls):
"""Clear everything from all functions from the cache
"""
cls._CACHE = {}
if __name__ == '__main__':
# used as decorator
@cached
def fibonacci(n):
print "calculating fibonacci(%d)" % n
if n == 0:
return 0
if n == 1:
return 1
return fibonacci(n - 1) + fibonacci(n - 2)
for n in xrange(10):
print 'fibonacci(%d) = %d' % (n, fibonacci(n))
def lucas(n):
print "calculating lucas(%d)" % n
if n == 0:
return 2
if n == 1:
return 1
return lucas(n - 1) + lucas(n - 2)
# used as context manager
with cached(lucas) as lucas:
for i in xrange(10):
print 'lucas(%d) = %d' % (i, lucas(i))
for n in xrange(9, -1, -1):
print 'fibonacci(%d) = %d' % (n, fibonacci(n))
cached.clear_cache()
for n in xrange(9, -1, -1):
print 'fibonacci(%d) = %d' % (n, fibonacci(n))