【问题标题】:"Lazy" property that gets initialized automatically at __init__在 __init__ 自动初始化的“惰性”属性
【发布时间】:2017-02-21 11:54:47
【问题描述】:

我想使用类似于通常的 lazy property 装饰器的东西,但是由于 TensorFlow 的工作方式和我使用它的方式,我需要在 __init__ 上自动初始化所​​有惰性属性(TensorFlow 部分是不是问题的一部分,但请参阅here 了解我的意思)。我所说的“初始化”是指调用getattr 来运行一次属性方法并缓存结果。

以下工作已经完成:

import functools

def graph_property(getter):
    property_name = getter.__name__
    attribute = '_cache_' + property_name

    @property
    @functools.wraps(getter)
    def decorated(self):
        if not hasattr(self, attribute):
            setattr(self, attribute, getter(self))
            self._graph.append(property_name) # for illustration
            print('Initializing ' + property_name)
        return getattr(self, attribute)

    return decorated


class Test:
    def __init__(self):
        self._graph = []
        self.inputs    # DON'T LIKE TO DO THIS
        self.do_stuff  # AND THIS

    @graph_property
    def inputs(self):
        return 42.0

    @graph_property
    def do_stuff(self):
        return self.inputs + 1.0


if __name__ == '__main__':
    t = Test()
    print(t._graph)

但是,最好摆脱在__init__ 中手动调用self.inputself.do_stuff ——这很快就会变得乏味。

我正在考虑多种方法来“记住”列表中某处的 graph_propertys 属性,但我认为所有都必须失败,因为在应用装饰器时,它还不知道该类(更不用说self)。

我可以想象的一种工作方式是为返回的decorated 对象提供一些标记属性,并为Test 编写一个元类,它查看所有方法,收集带有此标记的方法,并以某种方式为它们创建一个初始化程序.我未能实现这一点,因为我对元类非常不熟悉并且property 描述符不允许我添加属性。

所描述的方法是否可行(如果可行,如何实现)?或者有没有更简单的方法(没有手动开销和同样漂亮的语法)我只是没有看到它?

【问题讨论】:

    标签: python python-3.x python-decorators


    【解决方案1】:

    您可以添加一个简单的 mixin 并定义 property 的子类,然后在 mixin 的 __init__ 方法中执行与此自定义属性相关的所有初始化。通过这种方式,您可以选择希望它们在哪个类中初始化以及何时不希望它们初始化。

    import functools
    
    
    class lazy_property(property):
        """
        This class will help us in identifying our lazy properties, so that we
        don't confuse them with normal properties. 
        """
        pass
    
    def graph_property(getter):
        property_name = getter.__name__
        attribute = '_cache_' + property_name
    
        @lazy_property
        @functools.wraps(getter)
        def decorated(self):
            if not hasattr(self, attribute):
                setattr(self, attribute, getter(self))
                self._graph.append(property_name)  # for illustration
                print('Initializing ' + property_name)
            return getattr(self, attribute)
    
        return decorated
    
    class InitializeLazyPropertiesMixin:
        """
        This mixin does all of the work of initializing lazy properties
        """
        def __init__(self):
            cls = type(self)
            fields = (k for k in dir(cls) if isinstance(getattr(cls, k), lazy_property))
            for field in fields:
                getattr(self, field)
    
    
    class Test(InitializeLazyPropertiesMixin):
        def __init__(self):
            self._graph = []
            # Whenever you're inheriting from this mixin make sure to call
            # super `__init__` method.
            super().__init__()
    
        @graph_property
        def inputs(self):
            return 42.0
    
        @graph_property
        def do_stuff(self):
            return self.inputs + 1.0
    
    class Test1:
        """
        Just another class that doesn't require initializing any of the lazy properties
        """
        def __init__(self):
            self._graph = []
    
        @graph_property
        def inputs(self):
            return 42.0
    
        @graph_property
        def do_stuff(self):
            return self.inputs + 1.0
    

    演示输出:

    >>> t = Test()
    Initializing inputs
    Initializing do_stuff
    >>> print(t._graph)
    ['inputs', 'do_stuff']
    >>> t = Test1()
    >>> print(t._graph)
    []
    >>> t.inputs
    Initializing inputs
    42.0
    >>> t._graph
    ['inputs']
    

    【讨论】:

    • 啊,这个子类化技巧很漂亮!无论如何,我有一个通用的基类,所以这非常适合。
    • 我突然想到 vars(type(self)).items() 不会包含在超类中定义的变量 - 这只是检查声明的类本身的描述符,而不是它的祖先。
    • @jsbueno 不错,更新为使用dir() 其中goes recursively 获取属性。
    【解决方案2】:

    由于您可以完全控制您的属性和类层次结构,因此只需标记要初始化的属性,并在基类 __init__ 方法中编写代码,该方法将调用所有这些属性。

    因此,首先,在您的装饰器中,在您的 graph_property 装饰器上设置一个变量,以便它标记要初始化的方法。 由于property 对象与函数不同,不能分配任意属性,解决方法是将 Python 的本机属性包装在用户定义的类中:

    class MarcableProperty(property):
        pass
    
    def graph_property(getter):
        property_name = getter.__name__
        attribute = '_cache_' + property_name
    
        @MarcableProperty
        @functools.wraps(getter)
        def decorated(self):
            ...
    
        decorated._graph_initialize = True
        return decorated
    

    然后,在所有其他类的 Base 或 mixin 类上,执行以下操作:

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for cls_member_name in dir(self.__class__): 
             # "dir" is good because it automatically looks
             # at the superclasses as well
             cls_member = getattr(self.__class__, cls_member_name)
             if getattr(cls_member, "_graph_initialize", False):
                  # Fetch property, initializing its value:
                  getattr(self, cls_member_name)
    

    应该就是这样。

    【讨论】:

    • 这正是我最初的想法,但在decorated 上设置变量会导致“AttributeError: 'property' object has no attribute '_graph_initialize'”。
    • 这只是意味着 Python 的 property 使用 __slots__ - 只需使用可以简单的自定义属性:class MyProperty(property): pass
    • (我已经用这个例子更新了答案)实际上,你现在甚至需要标记,只需检查类属性是否是 MarcableProperty 的实例 - 就像它在 @Ashwini 的答案中一样)
    • 是的,我认识到这会起作用,但是是多余的。还是谢谢!
    • 是的,property implements 称为tp_members 插槽,相当于__slots__
    猜你喜欢
    • 2021-11-21
    • 2012-11-25
    • 2018-05-15
    • 2015-11-29
    • 1970-01-01
    • 2011-05-30
    • 1970-01-01
    • 1970-01-01
    • 2013-08-15
    相关资源
    最近更新 更多