Python类设置器 - 每次创建新实例时都会重新分配类属性吗?

Posted

技术标签:

【中文标题】Python类设置器 - 每次创建新实例时都会重新分配类属性吗?【英文标题】:Python class setter - are class attributes reassigned every time a new instance is created? 【发布时间】:2021-07-17 08:46:57 【问题描述】:

我正在尝试创建一个具有由所有实例共享的基本属性的类,该类在初始化实例属性时用作模板。理想情况下,它将是最终的/不可变的,但这是 Python,所以我只是不打算覆盖它。

class Foo:
    class_template_attr = some_resource_intensive_operation()
    
    def __init__(self, *args, **kwargs):
        self.instance_attr = method_using_class_attribute()
            
    def method_using_class_attribute():
        # determine instance_specific_extras
        return self.class_template_attr + instance_specific_extras
        

我想避免在每次实例化新的Foo 时重新生成class_template_attr,主要是因为它是一个资源密集型操作,但也因为它应该对每个Foo 都相同,所以它应该' t被再生。高高在上,看低了,一直无法确定创建新实例时是否重新计算类属性。

如果它们被重新计算,我会假设我可以定义一个 setter 来检查 class_template_attr 是否存在,然后创建它或传递该属性(如果它已经存在)。我目前正在使用元类来执行此操作,但我想知道这是否是最 Pythonic 的事情(或者我是否一开始就做对了)。

class MetaFoo(type):
    def __init__(cls, *args, **kwargs):
        super.__init__(*args, **kwargs)
        cls.class_template_attr = some_resource_intensive_operation()

    @property
    def class_template_attr(cls):
        return cls.__class_template_attr

    @class_template_attr.setter
    def class_template_attr(cls, value):
        if cls.__class_template_attr is None:
            cls.__class_template_attr = value
        else:
            pass

def bar(input):
    value = some_operation
    return value


class Foo(metaclass=MetaFoo):
    def __init__(self, object_specific_input):
        self.instance_attr = bar(object_specific_input)

    @property
    def instance_attr(self):
        return self.__instance_attr

    @instance_attr.setter
    def instance_attr(self, value):
        self.__instance_attr = value

注意:我意识到实例属性的 getter/setter 在这里是多余的,但我将它们用于特定目的,我不会去为了 MRE 的缘故。

【问题讨论】:

【参考方案1】:

我想知道为什么你认为如果你没有在类和实例中获得最基本的属性,你就可以成功编写元类。

事实上,你所描述的你所需要的是 Python 中对象的基本行为:你的属性是在类体执行时(通常在模块导入时)计算一次,并且在所有实例中都可用。

此外,如果在一个实例中有代码执行如下操作:self.class_template_attr = "new value",则只会在该实例中创建一个新属性,这将隐藏类属性 - 但仅针对该实例。原始值将保持所有其他实例的默认可见值。

现在,关于您的元类:您尝试在元类本身上创建一个属性,并将其级联到类 - 这肯定会有点过头了。

您的实现也存在一些问题 - 但从非错误开始:虽然“@property”可以在元类上工作并且可以作为类对象的属性使用,但它可以工作,但你赢了'在任何文档中都找不到这样的例子:因为它们不打算以这种方式使用。它们之所以起作用,是因为 Python 在其规则上非常一致,然后,作为元类的对象和实例的类也不例外,描述符协议是“使property 工作的原因”:将使用元类上的描述符用于类的属性检索。但是,元类上的相同描述符不会用于从该类的实例中检索属性。

在实现上还有一些其他的问题,但压倒性的冗余是最糟糕的部分。我要添加一个旁注,不要过多地依赖“私有”属性的双下划线前缀。有时它可能很有用,但它肯定比具有适当publicprivateprotected 修饰符的语言要少得多——它们更用于防止使用多重继承和复杂类层次结构中的名称冲突mixins,否则迟早会妨碍你。

Python 中的属性检索

关于制作myinstance.myattr

    Python 将首先检查属性是否存在于 myinstance 的类中,如果存在,则检查它是否是描述符(属性值的类应该具有 __get__ 方法。property 实现了该方法)。如果是,则调用它,并返回结果值; Python 将检查实例本身的属性。通常,作为实例的__dict__ 上的条目。如果是,则返回该值。 Python 在实例的类中查找属性,不管它是描述符。 如果未找到,它将检查该类是否具有 __getattr__ 方法 - 将使用预期的属性名称调用该方法,并返回一个值或引发 AttributeError。 如果到目前为止没有找到任何东西,则搜索失败,并引发 AttributeError。

请注意,对于上面的步骤 (1)、(3) 和 (4),搜索的是类,而不是它自己的元类 - 相反,搜索是通过继承链进行的:在每个步骤中,超类(在它们在myinstance.__class__.__mro__) 中显示的顺序被搜索以查找该属性。

property 用于只读类属性

如果设置一个普通的类属性还不够,并且您想防止实例意外地用self.myattr = "foo" 覆盖实例值,可以在类主体上创建一个只读属性。无需求助于元类。

请注意,这仍然不会阻止属性在类本身中被覆盖(使用self.__class__.myattr = "new value" - 如果您想阻止,那么您需要一个元类并自定义它__setattr__ 方法。这就是 Python 中“同意的成年人”的概念:如果一个人一直写 self.__class__.myattr = "new value" ,他们非常担心更改属性,而不管课程作者的意图如何,并愿意为此承担责任)。

无论如何,例如不可变的属性,并且,作为额外的,你得到你的昂贵值延迟计算:它将在第一次实际需要它时计算,而不是在模块导入时计算:有一个权衡:您在应用启动时获得敏捷性,并在实际第一次使用该计算时支付费用:

class Foo:
    @property
    def my_attribute(self):
         if not hasattr(self, "_my_attribute"):
              self._my_attribute = some_resource_intensive_operation()
         return self._my_attribute

现在 - 这就是我们所需要的。没有“setter”:第一次读取值时,计算该值并将其存储在“秘密”私有位置中。如果没有设置器,尝试在实例中设置其值时会出现属性错误。不需要元类。

【讨论】:

感谢您抽出宝贵时间回复! 如果我理解正确,类属性是在定义/导入类时计算的。即使不是,将属性定义为属性并在分配之前使用hasattr 检查它的存在会更简单/pythonic。尽管元类示例有效,但它非常不符合 Python 风格,而且还有些矫枉过正。 是的。 (此文本达到最小评论长度)【参考方案2】:

我意识到我可以轻松地测试我自己的问题,如下所示:

import random, string

def get_random_string():
    return ''.join(random.choices(string.ascii_uppercase + string.digits, k=6))

random.seed(10)
class TestClassAttributeInstantiation:
    class_attr = get_random_string()
    def __init__(self):
        self.inst_attr = get_random_string()

然后,在解释器中:

>>> TestClassAttributeInstantiation.class_attr
'OLPFVV'
>>> foo = TestClassAttributeInstantiation()
>>> foo.class_attr
'OLPFVV'
>>> foo.inst_attr
'QENIGY'
>>> bar = TestClassAttributeInstantiation()
>>> bar.class_attr
'OLPFVV'
>>> bar.inst_attr
'ZBWPJH'

如您所见,class_attr 是在定义类而不是实例化时设置的。这一点很明显,所有实例化都有一个具有相同值的class_attr,并且对类的引用也具有该值。

【讨论】:

以上是关于Python类设置器 - 每次创建新实例时都会重新分配类属性吗?的主要内容,如果未能解决你的问题,请参考以下文章

Python 类为每次执行的@property 提供不同的输出

每次代码更改时都会重新生成 DataBindingInfo.java

长寿命的多播 Observable,每次订阅时都会重新订阅其源

表单的新实例覆盖现有表单变量声明

谷歌浏览器怎样设置每次点击连接时都会弹出一个新窗口

我想创建 chrome 扩展,每次 youtube 播放视频时都会弹出