为什么Python的@staticmethods与装饰类的交互性如此之差?

时间:2012-06-24 01:31:52

标签: python python-2.7 decorator memoization

最近,StackOverflow社区帮助我开发了一个相当简洁的@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完全不支持staticmethodclassmethod。例如,如果您尝试过这样的事情:

@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

我真的,真的想用functools.wraps解决这个问题,而不必带回__dict__丑陋,所以我实际上用纯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实现完美配合。

所以,我的问题是:为什么标准内置staticmethod不能正常运行,和/或为什么不functools.wraps预测这种情况并为我解决?

这是Python中的错误吗?或者在functools.wraps

覆盖内置staticmethod的注意事项是什么?就像我说的那样,它现在似乎工作正常,但我担心我的实现和内置实现之间可能存在一些隐藏的不兼容性,这可能会在以后爆炸。

感谢。

编辑以澄清:在我的应用程序中,我有一个执行昂贵查找的函数,并且经常被调用,所以我记住了它。这很简单。除此之外,我有许多表示文件的类,并且在文件系统中具有表示相同文件的多个实例通常会导致状态不一致,因此每个文件名仅强制执行一个实例非常重要。将@memoize装饰器调整到这个目的并且仍然保留它作为传统记忆器的功能基本上是微不足道的。

@memoize的三种不同用途的真实世界示例如下:

3 个答案:

答案 0 :(得分:7)

有几个想法:

  • staticmethod 的操作与类装饰器的运算符完全正交。将函数转换为静态方法只会影响属性查找期间发生的事情。类装饰器是类的编译时转换。

  • 没有"错误"在 functools.wraps 中。它所做的只是将函数属性从一个函数复制到另一个函数。

  • 按照目前的说法,您的 memoize 工具不会考虑classmethods和staticmethods的不同调用签名。这是 memoize 中的一个弱点,而不是类工具本身。

我认为您已经想象了类装饰器,静态方法,类方法和functools之类的工具,以获得某种相互整合的智能。相反,所有这些工具都非常简单,需要程序员有意识地设计他们的交互。

ISTM,潜在的问题是所述目标有点不明确:"装饰者不仅能够以一般方式装饰函数,还能装饰方法和类,即,无需预先知道什么类型的它将装饰的东西。"

memoize 的语义在每个场景中都不完全清楚。并且Python的简单组件无法以能够猜测您真正想要做的事情的方式自动编写自己。

我的建议是你从一个使用各种对象的memoize实例的列表开始。然后开始构建您当前的解决方案,让它们一次工作一个。在每一步中,您将了解您的规范与meoize实际上的匹配程度。

另一个想法是 functools.wraps 类装饰器对于这个问题并非严格必要。两者都可以手动实现。首先将工具连接起来,按照您的意愿进行操作。一旦它工作,然后看看用换行和装饰器替换步骤。在那些可能不适合的情况下,这样做会使工具强制执行。

希望这有帮助。

答案 1 :(得分:2)

装饰类用于潜在地改变类的构造。这是一种方便但与__new__不完全相同的。

# 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

上面的代码强制你的实例内的方法包装。

class Flub:
    @memoize
    @staticmethod
    def do_things():
        print 'Do some things.'
Flub.do_things()

我相信这应该是您应该使用的代码 - 请记住,如果您没有收到args,那么args [0]将转到IndexError

答案 2 :(得分:2)

问题是你的装饰者正在接受一个类(即type的一个实例)并返回一个函数。这是(几乎)编程等同于category mistake;类可能看起来像函数,因为它们可以被调用(作为构造函数),但这并不意味着返回实例的函数等同于该实例类型的类。例如,instanceof无法给出正确的结果,而且您​​的装饰类不能再被子类化(因为它不再是一个类!)

你应该做的是调整你的装饰器以检测何时在类上调用它,并在这种情况下构造一个包装类(使用class语法,或通过type 3-参数构造函数)具有所需的行为。或者记住__new__(虽然请注意__init__将在__new__的返回值上调用(如果它是适当的类型,即使它是已经存在的实例)。