4

最近,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绝对不支持staticmethods 或classmethods。例如,如果你尝试过这样的事情:

@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

4

3 回答 3

7

给你几个想法:

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

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

  • 正如目前所写的那样,您的memoize工具没有考虑到 classmethods 和 staticmethods 的不同调用签名。这是memoize的弱点,而不是课堂工具本身。

我想你已经想象过类装饰器、静态方法、类方法和 functools 等工具具有某种相互集成的智能。相反,所有这些工具都非常简单,需要程序员有意识地设计它们的交互。

ISTM 认为,根本问题是既定目标在某种程度上没有明确说明:“装饰器不仅能够以一般方式装饰函数,还能够装饰方法和类,即,无需预先知道它将装饰什么类型的东西。 "

尚不完全清楚memoize在每种情况下的语义是什么。Python 的简单组件无法以能够猜出您真正想要做什么的方式自动组合自己。

我的建议是,您首先列出一个使用各种对象的 memoize 示例列表。然后开始构建您当前的解决方案,让它们一次一个地工作。在每个步骤中,您将了解您的规范与 meoize 实际所做的不匹配的地方。

另一个想法是functools.wraps类装饰器对于这个问题并不是绝对必要的。两者都可以手动实现。从连接你的工具开始做你想做的事。一旦它开始工作,然后看看用包装和装饰器替换步骤。这比在可能不适合的情况下试图强迫工具满足你的意愿要好。

希望这可以帮助。

于 2012-06-24T01:58:16.927 回答
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

于 2012-06-24T02:08:34.363 回答
2

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

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

于 2012-06-25T00:57:10.600 回答