为什么Python的@staticmethods与装饰类交互如此糟糕?(Why do Python's @staticmethods interact so poorly with decorated classes?)
最近,StackOverflow社区帮助我开发了一个相当简洁的
@memoize
装饰器,它不仅能够以通用的方式修饰函数,还能修饰方法和类,也就是说,不需要预先知道它将装饰什么类型的东西。我碰到的一个问题是,如果你用
@memoize
修饰了一个类,然后试图用@memoize
来修饰它的一个方法,@staticmethod
这将无法按预期工作,也就是说,你将无法调用ClassName.thestaticmethod()
。 我想到的原始解决方案如下所示:def memoize(obj): """General-purpose cache for classes, methods, and functions.""" cache = obj.cache = {} def memoizer(*args, **kwargs): """Do cache lookups and populate the cache in the case of misses.""" key = args[0] if len(args) is 1 else args if key not in cache: cache[key] = obj(*args, **kwargs) return cache[key] # Make the memoizer func masquerade as the object we are memoizing. # This makes class attributes and static methods behave as expected. for k, v in obj.__dict__.items(): memoizer.__dict__[k] = v.__func__ if type(v) is staticmethod else v return memoizer
但后来我了解了
functools.wraps
,它旨在使装饰器函数以更简洁更完整的方式伪装成装饰函数,而且我确实采用了这种方式:def memoize(obj): """General-purpose cache for class instantiations, methods, and functions.""" cache = obj.cache = {} @functools.wraps(obj) def memoizer(*args, **kwargs): """Do cache lookups and populate the cache in the case of misses.""" key = args[0] if len(args) is 1 else args if key not in cache: cache[key] = obj(*args, **kwargs) return cache[key] return memoizer
虽然这看起来很不错,但
functools.wraps
绝对不支持staticmethod
或classmethod
。 例如,如果你尝试了这样的事情:@memoize class Flub: def __init__(self, foo): """It is an error to have more than one instance per foo.""" self.foo = foo @staticmethod def do_for_all(): """Have some effect on all instances of Flub.""" for flub in Flub.cache.values(): print flub.foo Flub('alpha') is Flub('alpha') #=> True Flub('beta') is Flub('beta') #=> True Flub.do_for_all() #=> 'alpha' # 'beta'
这将与我列出的
@memoize
的第一个实现一起工作,但会引发TypeError: 'staticmethod' object is not callable
与第二个一起TypeError: 'staticmethod' object is not callable
。我真的很想用
functools.wraps
解决这个问题,而不必带回__dict__
ugliness,所以我实际上用纯Python重新实现了我自己的staticmethod
,看起来像这样:class staticmethod(object): """Make @staticmethods play nice with @memoize.""" def __init__(self, func): self.func = func def __call__(self, *args, **kwargs): """Provide the expected behavior inside memoized classes.""" return self.func(*args, **kwargs) def __get__(self, obj, objtype=None): """Re-implement the standard behavior for non-memoized classes.""" return self.func
据我所知,这与我在上面列出的第二个
@memoize
实现完美@memoize
。所以,我的问题是:为什么标准内置
staticmethod
不能正确运行,以及/或者为什么functools.wraps
没有预测到这种情况并为我解决?这是Python中的错误吗? 或者在
functools.wraps
?重写内置
staticmethod
哪些注意事项? 就像我说的那样,现在看起来工作得很好,但是恐怕我的实现和内置实现之间可能存在一些隐藏的不兼容性,这可能会在以后爆炸。谢谢。
编辑澄清:在我的应用程序中,我有一个功能,执行昂贵的查找,并频繁调用,所以我记忆它。 这非常简单。 除此之外,我有许多表示文件的类,并且在文件系统中有多个表示同一文件的实例通常会导致状态不一致,因此每个文件名仅强制一个实例很重要。 将
@memoize
装饰器调整为此目的并且仍然保留其作为传统记忆器的功能本质上是微不足道的。
@memoize
三种不同用途的真实世界示例如下:Recently, the StackOverflow community helped me develop a fairly concise
@memoize
decorator that is able to decorate not only functions but also methods and classes in a general way, ie, without having any foreknowledge of what type of thing it will be decorating.One of the problems that I ran into is that if you decorated a class with
@memoize
, and then tried to decorate one of it's methods with@staticmethod
, this would not work as expected, ie, you would not be able to callClassName.thestaticmethod()
at all. The original solution that I came up with looked like this:def memoize(obj): """General-purpose cache for classes, methods, and functions.""" cache = obj.cache = {} def memoizer(*args, **kwargs): """Do cache lookups and populate the cache in the case of misses.""" key = args[0] if len(args) is 1 else args if key not in cache: cache[key] = obj(*args, **kwargs) return cache[key] # Make the memoizer func masquerade as the object we are memoizing. # This makes class attributes and static methods behave as expected. for k, v in obj.__dict__.items(): memoizer.__dict__[k] = v.__func__ if type(v) is staticmethod else v return memoizer
But then I learned about
functools.wraps
, which is intended to make the decorator function masquerade as the decorated function in a much cleaner and more complete way, and indeed I adopted it like this:def memoize(obj): """General-purpose cache for class instantiations, methods, and functions.""" cache = obj.cache = {} @functools.wraps(obj) def memoizer(*args, **kwargs): """Do cache lookups and populate the cache in the case of misses.""" key = args[0] if len(args) is 1 else args if key not in cache: cache[key] = obj(*args, **kwargs) return cache[key] return memoizer
Although this looks very nice,
functools.wraps
provides absolutely no support for eitherstaticmethod
s orclassmethod
s. Eg, if you tried something like this:@memoize class Flub: def __init__(self, foo): """It is an error to have more than one instance per foo.""" self.foo = foo @staticmethod def do_for_all(): """Have some effect on all instances of Flub.""" for flub in Flub.cache.values(): print flub.foo Flub('alpha') is Flub('alpha') #=> True Flub('beta') is Flub('beta') #=> True Flub.do_for_all() #=> 'alpha' # 'beta'
This would work with the first implementation of
@memoize
I listed, but would raiseTypeError: 'staticmethod' object is not callable
with the second.I really, really wanted to solve this just using
functools.wraps
without having to bring back that__dict__
ugliness, so I actually reimplemented my ownstaticmethod
in pure Python, which looked like this:class staticmethod(object): """Make @staticmethods play nice with @memoize.""" def __init__(self, func): self.func = func def __call__(self, *args, **kwargs): """Provide the expected behavior inside memoized classes.""" return self.func(*args, **kwargs) def __get__(self, obj, objtype=None): """Re-implement the standard behavior for non-memoized classes.""" return self.func
And this, as far as I can tell, works perfectly alongside the second
@memoize
implementation that I list above.So, my question is: Why doesn't the standard builtin
staticmethod
behave properly on it's own, and/or why doesn'tfunctools.wraps
anticipate this situation and solve it for me?Is this a bug in Python? Or in
functools.wraps
?What are the caveats of overriding the builtin
staticmethod
? Like I say, it seems to be working fine now, but I'm afraid that there might be some hidden incompatibility between my implementation and the builtin implementation, which might blow up later on.Thanks.
Edit to clarify: In my application, I have a function that does an expensive lookup, and gets called frequently, so I memoized it. That is quite straightforward. In addition to that, I have a number of classes that represent files, and having multiple instances representing the same file in the filesystem will generally result in inconsistent state, so it's important to enforce only one instance per filename. It's essentially trivial to adapt the
@memoize
decorator to this purpose and still retain it's functionality as a traditional memoizer.Real world examples of the three different uses of
@memoize
are here:
原文:https://stackoverflow.com/questions/11174362