Python class setter - 每次创建新实例时都会重新分配 class 属性吗?

Python class setter - are class attributes reassigned every time a new instance is created?

我正在尝试创建一个 class,它具有所有实例共享的基本属性,在初始化实例属性时用作模板。理想情况下,它将是 final/immutable,但这是 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 所以它不应该被重新生成。我看了高低,一直没能确定class属性是否在创建新实例时重新计算

如果重新计算它们,我假设我可以定义一个 setter 来检查 class_template_attr 是否存在,然后创建它或传递属性(如果它已经存在)。我目前正在使用 metaclass 来执行此操作,但我想知道这是否是最 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 而深入。

我想知道你为什么认为如果你没有掌握classes和实例中最基本的属性,你就可以成功编写metaclasses。

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

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

现在,关于您的元class:您尝试在元class 本身上创建一个属性 并将其级联到classes - 那远射肯定是矫枉过正。

您的实施也存在一些问题 - 但以 non-error 开头:而“@属性”可以在 metaclass 上运行并可用作class object 上的 属性 可以工作,但您不会在任何文档中找到这方面的示例:由于事实上它们不应该以这种方式使用.它们起作用是因为 Python 的规则非常一致,然后,classes,作为 objects 和 metaclass 的实例,对描述符协议也不例外这就是“是什么让 property 工作”:metaclass 上的描述符将用于 class 上的属性检索。但是,metaclass 上的相同描述符不会 用于从 class.

的实例中检索属性

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

Python

中的属性检索

关于myinstance.myattr:

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

请注意,对于上面的步骤 (1)、(3) 和 (4),搜索的是 class,而不是它自己的元 class - 相反,搜索遵循继承链:在这些步骤的每一步中,都会搜索 superclasses(按照它们在 myinstance.__class__.__mro__ 中显示的顺序)搜索属性。

对 read-only class 属性使用 property

如果设置普通 class 属性还不够,并且您希望防止实例意外地用 self.myattr = "foo" 覆盖实例值,可以创建只读 属性在 class body 上。无需求助于 metaclasses.

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

无论如何,例如不可变属性,而且,作为额外的,你会得到延迟计算的昂贵值:它将在第一次实际需要时计算,而不是在模块导入时计算:这是一个交易关闭:您在应用程序启动时获得敏捷性,并在实际首次使用时支付计算成本:

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”:第一次读取值时,计算值并存储在“秘密”私有位置。在没有 setter 的情况下,尝试在实例中设置其值时会出现属性错误。不需要元classes。

我意识到我可以很容易地测试我自己的问题如下:

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 时设置的,而不是在实例化时设置的。这是显而易见的,因为所有实例化都有一个具有相同值的 class_attr 并且对 class 的引用也具有该值。