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

75

我在学习Python,虽然我认为我已经理解了Python的整个概念和观念,但今天我遇到了一段代码,我并没有完全理解:

假设我有一个类,应该定义圆形但是缺少主体:

class Circle():
    pass

由于我没有定义任何属性,那么我该怎么做呢:

my_circle = Circle()
my_circle.radius = 12
奇怪的是Python接受了上面的语句。我不明白为什么Python没有引发“未定义名称错误”。我确实理解通过动态类型,我可以随时将变量绑定到对象,但是在Circle类中不应该存在一个名为radius的属性,以允许我这样做吗? 编辑:您的答案提供了很多有用信息!感谢大家给出的所有精彩答案!可惜我只能选择一个作为答案。

15
еҪ“дҪ еңЁ__init__дёӯеҲқе§ӢеҢ–self.radiusж—¶пјҢйҡҫйҒ“дёҚжҳҜеңЁеҒҡе®Ңе…ЁзӣёеҗҢзҡ„дәӢжғ…еҗ—пјҹ - JBernardo
@JBernardo 是的,你需要,但在这种情况下,你明确地为类Circle()定义了一个radius属性。而在我的情况中,我没有在类主体中创建任何属性。 - NlightNFotis
@NlightNFotis 不,你做的事情是一样的,因为“self”只是像其他变量一样的变量。 - JBernardo
4
@NlightNFotis,此外,Python不是Java,一种不影响你对编程思考方式的语言是不值得学习的 - Alan Perlis - JBernardo
这是被允许的,因为它很方便。想象一下拥有一个代表用户界面的对象。您可能希望动态创建小部件,并通过将它们存储在“程序窗口”类的新属性中来保留它们的引用。 - Guimoute
显示剩余4条评论
9个回答

59
一个主要原则是不存在声明这样的东西。也就是说,你从来没有声明“这个类有一个方法foo”或“这个类的实例有一个属性bar”,更不用说对存储在那里的对象类型做出陈述了。你只需定义一个方法、属性、类等,然后它就被添加了。正如JBernardo指出的那样,任何__init__方法都可以做同样的事情。将创建新属性的限制武断地局限于名称为__init__的方法并没有多大意义。有时将函数存储为__init__是有用的,但实际上并没有这个名称(例如装饰器),这样的限制会破坏这一点。
现在,这并非普遍适用。内置类型省略了这种能力作为一种优化。通过__slots__,你还可以防止用户定义的类中的这种情况。但这仅仅是一种空间优化(不需要为每个对象使用字典),而不是正确性问题。
如果你想要一个安全网,那就太遗憾了。Python没有提供这样一个功能,而且你也无法合理地添加一个,更重要的是,这将被Python程序员所抛弃,因为他们拥抱这门语言(即:几乎所有你想要一起工作的人)。测试和纪律仍然是确保正确性的最佳方法。如果可以避免,不要在__init__之外编写属性,并进行自动化测试。我很少会因此出现AttributeError或逻辑错误,而其中发生的几乎所有情况都会被测试捕获。

3
它的灵活性很好,但也带来了可读性问题,当我在维护别人的代码时,常常会忘记对象在某个特定点具有哪些属性。 - Junchen Liu

50

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

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

foo = Foo(5)

并且这段代码:

class Foo(object):
    pass

foo = Foo()
foo.bar = 5

完全等效的。实际上它们没有区别,这两种方式执行的操作完全相同。不同之处在于,第一种情况下封装了bar属性,并且清楚地表明bar属性是Foo类型对象的普通部分。而在第二种情况下,不清楚是否如此。

在第一种情况下,你无法创建一个没有bar属性的Foo对象(嗯,你可能可以,但不容易);而在第二种情况下,除非设置它,否则Foo对象将没有bar属性。

因此,虽然代码在程序上是等效的,但在不同情况下使用。


1
第二种用例是什么?它有点违反面向对象编程的原则,当然这没关系...但如果你不是在进行面向对象编程,为什么还要关心拥有一个类呢?这些不是修辞性问题,我真的很好奇! - rocksNwaves
只是因为你不是教条主义者,它并不会停止成为面向对象编程。第二种情况仍然是面向对象编程。 - Lennart Regebro

20

Python允许您在几乎任何实例(或类,说实在的)上存储任何名称的属性。可以通过像内置类型一样编写C类,或使用__slots__来阻止这种情况,后者仅允许特定名称。

它能够工作的原因是大多数实例将其属性存储在字典中。是的,就像您用{}定义的普通Python字典一样。该字典存储在实例属性__dict__中。事实上,有些人说“类只是字典的语法糖”。也就是说,您可以用字典做所有类能做到的事情;类只是使其更加容易。

您可能习惯了静态语言,在那里必须在编译时定义所有属性。在Python中,类定义是执行而不是编译的;类就像任何其他对象一样;添加属性与向字典添加项一样简单。这就是为什么Python被认为是一种动态语言的原因。


你好,那么你的意思是在Python中,类的目的不是将数据和行为捆绑在一起(面向对象编程),而只是通过添加一些易于阅读的语法来标识给定字典的目的吗? - rocksNwaves
1
您可以使用它们来捆绑数据和行为,语法鼓励这样做,并且它基本上像面向对象编程一样工作(尽管不是强 OOP 版本 --- 在 Python 中封装相当薄弱,因为没有私有属性)。但是,在类语法的下面,类基本上是带有一些额外(非常有用)行为的字典。如果您正在编写编译器,您可能会在定义期间使用字典(哈希)来跟踪类的成员; Python 只是在运行时执行这个操作。 - kindall

19
不,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'

7
你不需要这样做。Python和Java是不同的,你不应该试图用Python写Java代码。如果你想要那样做,那就直接写Java代码吧。 - Gareth Latty
7
分享编程的乐趣,不设安全网 :) - ypercubeᵀᴹ
3
Java的类型安全是一种幻觉。它感觉上更安全、更可靠,让你相信代码更加可信,但实际上并非如此。蛋糕是个谎言。 - Lennart Regebro
3
Python有一种理念,被描述为“我们都是成年人”,意思是如果你想要打破常规并直接更改foo._variable,比如出于调试或避免一些错误,那么你可以这样做。但是如果它导致了问题,你就不能向foo的作者抱怨。 - Thomas K
3
__slots__ 是一种节省内存的优化方式,不应将其用作锁定类的方法。 - Ethan Furman
显示剩余7条评论

7
它创建了一个 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

5
虽然你可以执行 Circle.xyz = 5 来更改类而不是实例... - Joran Beasley

4

在Python中有两种属性 - 类数据属性实例数据属性

Python允许您动态地创建数据属性,这使得代码更加灵活。

由于实例数据属性与实例相关,因此您可以在 __init__ 方法中创建它,也可以在创建实例后再创建它。

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__内部创建的实例属性将被设置为所有实例使用,而如果在外部创建,则可以为不同的实例创建不同的实例属性。
这与Java不同,Java中每个类的实例都具有相同的实例变量。
注意:虽然您可以通过实例访问类属性,但无法删除它。另外,如果您尝试通过实例修改类属性,实际上会创建一个实例属性来覆盖类属性。

4
不,你也不需要声明类属性。你需要定义它们。这些定义是可执行语句,和普通语句一样,只不过不是操作某个函数的作用域,而是操作类的属性。而且类属性也不是固定不变的:添加、替换和删除类属性都很容易。 - user395760
我仍然不明白为什么您在开头区分类属性和实例属性。两者都是明确定义的,在任何情况下都是在运行时定义,这些定义和重新定义可以随时发生。 - user395760

2
补充Conchylicultor的回答,Python 3.10 添加了一个新参数到dataclassslots参数将在类中创建__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

2

如何防止新属性的创建?

使用类

为了控制新属性的创建,您可以覆盖__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` 的子类。

1
正如delnan所说,您可以使用__slots__属性来实现此行为。但是,它是一种节省内存空间和访问类型的方法,并不排除它也是禁用动态属性的一种/主要方式。
禁用动态属性是一个合理的做法,即使仅仅是为了防止由于拼写错误而导致的微妙错误。"测试和纪律"是好的,但依赖自动验证也并非错误 - 也不一定不符合Pythonic。
此外,自2016年以来,attrs库已经达到16版(显然比原始问题和答案晚得多),创建具有slots的封闭类从未如此简单。
>>> 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'

1
另一种禁用动态属性的机制,它不使用插槽,因此不会破坏继承:from pystrict import strict \n @strict \n class Circle: ... - Erik Aronesty

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接