替换属性以获得性能提升

时间:2011-09-12 12:59:23

标签: python properties

场合

this question类似,我想替换一个属性。与那个问题不同,我不想在子类中覆盖它。我想在init和属性本身中替换它以提高效率,这样它就不必调用每次调用属性时计算值的函数。

我有一个在其上有属性的类。构造函数可以获取属性的值。如果传递了值,我想用值替换属性(不只是设置属性)。这是因为属性本​​身会计算该值,这是一项昂贵的操作。类似地,我想用属性计算的值替换属性,以便将来对属性的调用不必重新计算:

class MyClass(object):
    def __init__(self, someVar=None):
        if someVar is not None: self.someVar = someVar

    @property
    def someVar(self):
        self.someVar = calc_some_var()
        return self.someVar

问题

上面的代码不起作用,因为执行self.someVar =不会替换someVar函数。它试图调用属性的setter,它没有定义。

潜在解决方案

我知道我可以用稍微不同的方式实现同​​样的目的:

class MyClass(object):
    def __init__(self, someVar=None):
        self._someVar = someVar

    @property
    def someVar(self):
        if self._someVar is None:
            self._someVar = calc_some_var()
        return self._someVar

这将略微降低效率,因为每次调用属性时都必须检查None。该应用程序对性能至关重要,因此这可能也可能不够好。

问题

有没有办法替换类实例上的属性?如果我能够做到这一点会有多高效(即避免无检查和函数调用)?

4 个答案:

答案 0 :(得分:15)

您正在寻找的是Denis Otkidach出色的CachedAttribute:

class CachedAttribute(object):    
    '''Computes attribute value and caches it in the instance.
    From the Python Cookbook (Denis Otkidach)
    This decorator allows you to create a property which can be computed once and
    accessed many times. Sort of like memoization.
    '''
    def __init__(self, method, name=None):
        # record the unbound-method and the name
        self.method = method
        self.name = name or method.__name__
        self.__doc__ = method.__doc__
    def __get__(self, inst, cls):
        # self: <__main__.cache object at 0xb781340c>
        # inst: <__main__.Foo object at 0xb781348c>
        # cls: <class '__main__.Foo'>       
        if inst is None:
            # instance attribute accessed on class, return self
            # You get here if you write `Foo.bar`
            return self
        # compute, cache and return the instance's attribute value
        result = self.method(inst)
        # setattr redefines the instance's attribute so this doesn't get called again
        setattr(inst, self.name, result)
        return result

可以像这样使用:

def demo_cache():
    class Foo(object):
        @CachedAttribute
        def bar(self):
            print 'Calculating self.bar'  
            return 42
    foo=Foo()
    print(foo.bar)
    # Calculating self.bar
    # 42

请注意,后续访问foo.bar不会调用getter函数。 (Calculating self.bar未打印。)

    print(foo.bar)    
    # 42
    foo.bar=1
    print(foo.bar)
    # 1

foo.bar删除foo.__dict__会重新公开Foo中定义的属性。 因此,再次调用foo.bar会重新计算该值。

    del foo.bar
    print(foo.bar)
    # Calculating self.bar
    # 42

demo_cache()

装饰器已在Python Cookbook发布,也可在ActiveState上找到。

这是有效的,因为尽管属性存在于类__dict__中,但在计算之后,在实例的__dict__中创建了同名属性。 Python的属性查找规则优先于实例__dict__中的属性,因此类中的属性会被有效覆盖。

答案 1 :(得分:2)

当然,您可以在类实例的私有字典中设置属性,该属性在调用属性函数foo之前优先(在静态字典A.__dict__中)

class A:
    def __init__(self):
        self._foo = 5
        self.__dict__['foo'] = 10

    @property
    def foo(self):
        return self._foo

assert A().foo == 10

如果您想再次重置以使用该属性,只需del self.__dict__['foo']

答案 2 :(得分:1)

class MaskingProperty():
    def __init__(self, fget=None, name=None, doc=None):
        self.fget = fget
        if fget is not None:
            self.name = fget.__name__
        self.__doc__ = doc or fget.__doc__
    def __call__(self, func):
        self.fget = func
        self.name = func.__name__
        if not self.__doc__:
            self.__doc__ = func.__doc__
        return self
    def __get__(self, instance, cls):
        if instance is None:
            return self         
        if self.fget is None:
            raise AttributeError("seriously confused attribute <%s.%s>" % (cls, self.name))
        result = self.fget(instance)
        setattr(instance, self.name, result)
        return result

这与Denis Otkidach的CachedAttribute基本相同,但稍微强一些,因为它允许:

@MaskingProperty
def spam(self):
    ...

@MaskingProperty()     # notice the parens!  ;)
def spam(self):
    ...

答案 3 :(得分:0)

您可以通过将功能的__code__对象替换为另一个功能中的__code__对象来change what code a function has

这是我创建的装饰器函数,专门为您执行此操作。随时根据自己的喜好对其进行修改。不过要记住的一件大事是,两个函数都需要具有相同数量的“自由变量”才能像这样交换。可以使用非本地强制将其轻松完成(如下所示)。

NULL = object()
def makeProperty(variable = None, default = NULL, defaultVariable = None):
    """Crates a property using the decorated function as the getter.
    The docstring of the decorated function becomes the docstring for the property.

    variable (str) - The name of the variable in 'self' to use for the property
        - If None: uses the name of 'function' prefixed by an underscore

    default (any) - What value to initialize 'variable' in 'self' as if it does not yet exist
        - If NULL: Checks for a kwarg in 'function' that matches 'defaultVariable'

    defaultVariable (str) - The name of a kwarg in 'function' to use for 'default'
        - If None: Uses "default"
        Note: this must be a kwarg, not an arg with a default; this means it must appear after *
    ___________________________________________________________

    Example Use:
        class Test():
            @makeProperty()
            def x(self, value, *, default = 0):
                '''Lorem ipsum'''
                return f"The value is {value}"

        test = Test()
        print(test.x) #The value is 0
        test.x = 1
        print(test.x) #The value is 1

    Equivalent Use:
        @makeProperty(defaultVariable = "someKwarg")
        def x(self, value, *, someKwarg = 0):

    Equivalent Use:
        @makeProperty(default = 0)
        def x(self, value):
    ___________________________________________________________
    """
    def decorator(function):
        _variable = variable or f"_{function.__name__}"

        if (default is not NULL):
            _default = default
        elif (function.__kwdefaults__ is not None):
            _default = function.__kwdefaults__.get(defaultVariable or "default")
        else:
            _default = None

        def fget(self):
            nonlocal fget_runOnce, fget, fset, _default #Both functions must have the same number of 'free variables' to replace __code__
            return getattr(self, _variable)

        def fget_runOnce(self):
            if (not hasattr(self, _variable)):
                fset(self, _default)

            fget_runOnce.__code__ = fget.__code__
            return getattr(self, _variable)

        def fset(self, value):
            setattr(self, _variable, function(self, value))

        def fdel(self):
            delattr(self, _variable)

        return property(fget_runOnce, fset, fdel, function.__doc__)
    return decorator