为什么允许向已实例化的对象添加属性?

我正在研究 Python,尽管我认为自己已经理解了 Python 的整个概念和概念,但今天我偶然发现了一段我并不完全理解的代码:

假设我有一个类,它本应定义 Circles,但缺少一个主体:

class Circle():
pass

由于我还没有定义任何属性,我如何做到这一点:

my_circle = Circle()
my_circle.radius = 12

奇怪的是 Python 接受上述语句。我不明白为什么巨蟒不提高 undefined name error。我知道通过 动态打字我可以随时绑定变量到对象,但是 Circle类中不应该存在一个属性 radius来允许我这样做吗?

编辑 : 你的答案中有很多精彩的信息!很遗憾,我只能得到一个答案。

35278 次浏览

不,python 是这样灵活的,它不强制用户定义类上存储什么属性。

但是,在类定义上使用 __slots__属性会阻止您创建在 __slots__序列中未定义的其他属性:

>>> class Foo(object):
...     __slots__ = ()
...
>>> f = Foo()
>>> f.bar = 'spam'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Foo' object has no attribute 'bar'
>>> class Foo(object):
...     __slots__ = ('bar',)
...
>>> f = Foo()
>>> f.bar
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: bar
>>> f.bar = 'spam'

它创建 my_circleradius数据成员。

如果你要求它使用 my_circle.radius,它会抛出一个异常:

>>> print my_circle.radius # AttributeError

有趣的是,这并没有改变类,只是改变了一个实例。所以:

>>> my_circle = Circle()
>>> my_circle.radius = 5
>>> my_other_circle = Circle()
>>> print my_other_circle.radius # AttributeError

Python 允许您在实际上任何实例(或类)上存储任何名称的属性。可以用 C 语言编写类,比如内置类型,或者使用只允许特定名称的 __slots__来阻止它。

它工作的原因是大多数实例在字典中存储它们的属性。是的,一个常规的 Python 字典,就像您用 {}定义的那样。字典存储在名为 __dict__的实例属性中。事实上,有些人说“类只是字典的句法糖。”也就是说,您可以使用一个带有字典的类来做任何您能做的事情; 类只是让它变得更简单。

您已经习惯了静态语言,其中必须在编译时定义所有属性。在 Python 中,类定义是 处决,而不是编译的; 类就像任何其他对象一样是对象; 添加属性就像向 dictionary 中添加项一样简单。这就是为什么 Python 被认为是 充满活力语言。

一个主要的原则是 根本没有宣言这回事。也就是说,永远不要声明“ this class has a method foo”或“ this class 的实例 have an Attribute bar”,更不用说声明要存储在那里的对象的类型了。您只需定义一个方法、属性、类等,然后添加它。正如 JBernardo 指出的,任何 __init__方法都会做同样的事情。任意地将新属性的创建限制为名为 __init__的方法是没有意义的。有时候,将一个函数存储为实际上没有这个名称的 __init__是很有用的(例如,decorators) ,这样的限制会打破这个限制。

现在,这并不是普遍正确的。内置类型省略了这个优化功能。通过 __slots__,还可以在用户定义的类上防止这种情况发生。但是,这仅仅是一个空间优化(不需要为每个对象编写字典) ,而不是一个正确性问题。

如果你想要一个安全网,那太糟糕了。Python 不提供这种语言,而且您无法合理地添加这种语言,最重要的是,拥抱这种语言的 Python 程序员会回避这种语言(请阅读: 几乎所有您想要使用的语言)。测试和纪律,仍然有很长的路要走,以确保正确性。不要使用在 __init__ 如果可以避免的话之外创建属性的自由,并进行自动化测试。由于这样的诡计,我很少出现 AttributeError或逻辑错误,而且在发生的这些错误中,几乎所有的错误都被测试发现了。

Python 中有两种类型的属性 -Class Data AttributesInstance Data Attributes

Python 提供了动态创建 Data Attributes的灵活性。

Since an instance data attribute is related to an instance, you can also do that in __init__ method or you can do it after you have created your instance..

class Demo(object):
classAttr = 30
def __init__(self):
self.inInit = 10


demo = Demo()
demo.outInit = 20
Demo.new_class_attr = 45; # You can also create class attribute here.


print demo.classAttr  # Can access it


del demo.classAttr         # Cannot do this.. Should delete only through class


demo.classAttr = 67  # creates an instance attribute for this instance.
del demo.classAttr   # Now OK.
print Demo.classAttr

因此,您可以看到我们已经创建了两个实例属性,一个在 __init__内部,另一个在创建实例之后在 __init__外部。

But a difference is that, the instance attribute created inside __init__ will be set for all the instances, while if created outside, you can have different instance attributes for different isntances..

这与 Java 不同,Java 中类的每个实例都有相同的实例变量集。

  • 注意:-虽然可以通过实例访问 class 属性,但不能删除它。 Also, if you try to modify a class attribute through an instance, you actually create an instance attribute which shadows the class attribute..

只是为了澄清这里讨论中的一些误解。这个代码:

class Foo(object):
def __init__(self, bar):
self.bar = bar


foo = Foo(5)

还有这个代码:

class Foo(object):
pass


foo = Foo()
foo.bar = 5

完全相同。真的没有区别。完全一样。不同之处在于,在第一种情况下,它是封装的,很明显 bar 属性是 Foo 类型对象的正常部分。在第二种情况下,情况是否如此尚不清楚。

在第一种情况下,你不能创建一个没有 bar 属性的 Foo 对象(好吧,你可能可以,但不容易) ,在第二种情况下,除非你设置了 bar 属性,否则 Foo 对象将不会有 bar 属性。

因此,尽管代码在编程上是等价的,但它在不同的情况下使用。

正如 delnan 所说,您可以通过 __slots__属性获得此行为。但事实上,这是一种节省内存空间和访问类型的方法,并不排除这样一个事实,即它(也)是一个/禁用动态属性的手段。

如果只是为了防止拼写错误导致的细微错误,那么禁用动态属性是一种合理的做法。“测试和纪律”很好,但依赖自动化验证当然也不是错误的——而且也不一定非 Python 化。

而且,自从 attrs库在2016年达到了16版(显然比最初的问题和答案晚了很多) ,创建一个带插槽的封闭类从来没有这么容易过。

>>> import attr
...
... @attr.s(slots=True)
... class Circle:
...   radius = attr.ib()
...
... f = Circle(radius=2)
... f.color = 'red'
AttributeError: 'Circle' object has no attribute 'color'

如何防止新属性的创建?

利用课堂

若要控制新属性的创建,可以重写 __setattr__方法。它将在每次调用 my_obj.x = 123时被调用。

参见 文件:

class A:
def __init__(self):
# Call object.__setattr__ to bypass the attribute checking
super().__setattr__('x', 123)


def __setattr__(self, name, value):
# Cannot create new attributes
if not hasattr(self, name):
raise AttributeError('Cannot set new attributes')
# Can update existing attributes
super().__setattr__(name, value)


a = A()
a.x = 123  # Allowed
a.y = 456  # raise AttributeError

请注意,如果用户直接调用 object.__setattr__(a, 'attr_name', attr_value),仍然可以绕过检查。

使用数据类

使用 dataclasses,您可以禁止使用 frozen=True创建新属性。它还将防止更新现有属性。

@dataclasses.dataclass(frozen=True)
class A:
x: int




a = A(x=123)
a.y = 123  # Raise FrozenInstanceError
a.x = 123  # Raise FrozenInstanceError

注意: dataclasses.FrozenInstanceError是 AttributeError 的子类

为了添加到 Conchylicultor 的回答,Python 3.10向 dataclass添加了一个新参数。

slots参数将在类中创建 __slots__属性,防止在 __init__之外创建新属性,但是允许分配到现有属性。

如果为 slots=True分配未定义的属性将引发 AttributeError

下面是 slotsfrozen的一个例子:

from dataclasses import dataclass


@dataclass
class Data:
x:float=0
y:float=0


@dataclass(frozen=True)
class DataFrozen:
x:float=0
y:float=0


@dataclass(slots=True)
class DataSlots:
x:float=0
y:float=0


p = Data(1,2)
p.x = 5 # ok
p.z = 8 # ok


p = DataFrozen(1,2)
p.x = 5 # FrozenInstanceError
p.z = 8 # FrozenInstanceError


p = DataSlots(1,2)
p.x = 5 # ok
p.z = 8 # AttributeError