为啥允许向已经实例化的对象添加属性?
Posted
技术标签:
【中文标题】为啥允许向已经实例化的对象添加属性?【英文标题】:Why is adding attributes to an already instantiated object allowed?为什么允许向已经实例化的对象添加属性? 【发布时间】:2012-09-16 03:33:20 【问题描述】:我正在学习python,虽然我认为我了解了Python的整个概念和概念,但今天我偶然发现了一段我没有完全理解的代码:
假设我有一个应该定义 Circles 但缺少主体的类:
class Circle():
pass
由于我没有定义任何属性,我该怎么做:
my_circle = Circle()
my_circle.radius = 12
奇怪的是 Python 接受了上述声明。我不明白为什么 Python 不提出undefined name error
。我确实明白,通过动态类型,我只是在需要时将变量绑定到对象,但Circle
类中不应该存在属性radius
来允许我这样做吗?
编辑:你的答案中有很多精彩的信息! 感谢大家提供的所有精彩答案!很遗憾,我只能将其中一个标记为答案。
【问题讨论】:
当您在__init__
初始化self.radius
时,您做的不是完全相同吗?
@JBernardo 是的,但是在这种情况下,您正在为类 Circle()
显式定义 radius
属性。就我而言,我没有在类主体中创建任何属性。
@NlightNFotis 不,您正在做同样的事情,因为 self
只是一个变量,就像任何其他变量一样。
@NlightNFotis 另外,Python is not Java 和一种不会影响您对编程的看法的语言不值得了解 - [Alan Perlis](en.wikiquote.org/wiki/Alan_Perlis)
@NlightNFotis 不,你不是。您定义一个函数,该函数分配给它的第一个参数的一个属性。碰巧这个函数被一个类的__init__
属性引用,该属性恰好在对象构造之后被调用。
【参考方案1】:
如何防止创建新属性?
使用类
要控制新属性的创建,您可以覆盖__setattr__
方法。每次调用my_obj.x = 123
时都会调用它。
见documentation:
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 的子类
【讨论】:
【参考方案2】:正如 delnan 所说,您可以使用 __slots__
属性获得此行为。但它是一种节省内存空间和访问类型的方法这一事实并没有放弃它(也)是禁用动态属性的手段这一事实。
禁用动态属性是一个合理的做法,如果只是为了防止由于拼写错误导致的细微错误。 “测试和纪律”很好,但依赖自动验证当然也没有错——也不一定是不符合标准的。
此外,自从 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'
【讨论】:
另一种禁用不使用槽的动态属性的机制,因此不会破坏继承:from pystrict import strict \n @strict \n class Circle: ...
【参考方案3】:
Python 允许您在几乎任何实例(或类)上存储任何名称的属性。可以通过在 C 中编写类(如内置类型)或使用仅允许某些名称的 __slots__
来阻止这种情况。
它起作用的原因是大多数实例将它们的属性存储在字典中。是的,就像您使用 定义的常规 Python 字典一样。字典存储在名为
__dict__
的实例属性中。事实上,有人说“类只是字典的语法糖”。也就是说,你可以用字典做你能做的一切;类只是让事情变得更容易。
您习惯于必须在编译时定义所有属性的静态语言。在 Python 中,类定义是执行,而不是编译;类和其他对象一样是对象;添加属性就像在字典中添加项目一样简单。这就是为什么 Python 被认为是一种动态语言。
【讨论】:
嗨,你是说在 Python 中,类的目的不是捆绑数据和行为 (OOP),而只是通过修饰来确定给定字典的用途有一些人类可读的语法? 您可以使用它们来捆绑数据和行为,并且语法鼓励这样做,并且它基本上就像您期望的 OOP 一样工作(尽管不是 OOP 的强大版本——Python 中的封装非常弱,因为没有私有属性)。但是,在类语法之下,类基本上是字典,顶部有一些额外的(非常有用的)行为。如果您正在编写编译器,您可能会使用字典(哈希)在定义期间跟踪类的成员; Python 只是在运行时执行此操作。 感谢您的澄清!【参考方案4】:一个主要原则是没有声明这样的东西。也就是说,你永远不会声明“这个类有一个方法 foo”或“这个类的实例有一个属性 bar”,更不用说声明要存储在那里的对象的类型了。您只需定义一个方法、属性、类等,它就会被添加。正如 JBernardo 指出的那样,任何__init__
方法都做同样的事情。将新属性的创建任意限制为名称为__init__
的方法没有多大意义。有时将函数存储为__init__
,但实际上并没有该名称(例如装饰器),这样的限制会破坏这一点。
现在,这并非普遍适用。内置类型省略了此功能作为优化。通过__slots__
,您还可以在用户定义的类上防止这种情况。但这只是空间优化(不需要为每个对象提供字典),而不是正确性。
如果你想要一个安全网,那就太糟糕了。 Python 不提供,而且你不能合理地添加一个,最重要的是,拥抱该语言的 Python 程序员会避开它(阅读:几乎所有你想与之合作的人)。测试和纪律,对于确保正确性还有很长的路要走。不要随意在__init__
以外的地方补属性如果可以避免,做自动化测试。由于这样的诡计,我很少遇到AttributeError
或逻辑错误,而在这些发生的情况中,几乎所有都被测试捕获了。
【讨论】:
它的方式很好,但灵活性,也引发了可读性问题,在维护其他人的代码时,我经常忘记对象在某个点具有什么属性。【参考方案5】:只是为了澄清这里讨论中的一些误解。这段代码:
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 对象(嗯,您可能可以,但不容易),在第二种情况下,除非您设置 Foo 对象没有 bar 属性它。
因此,尽管代码在编程上是等效的,但它用于不同的情况。
【讨论】:
第二种的用例是什么?它有点破坏 OOP,这当然很好......但是如果你不编程 OOP,你为什么还要关心上课呢?这些不是反问,我真的很好奇! 它不会因为你不教条就停止成为 OOP。第二种情况还是OOP。【参考方案6】:Python 中有两种类型的属性 - Class Data Attributes
和 Instance Data Attributes
。
Python 让您可以灵活地动态创建Data Attributes
。
由于实例数据属性与实例相关,您也可以在__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 中每个类的实例都有相同的实例变量集..
注意: - 虽然您可以通过实例访问类属性,但不能删除它。 此外,如果您尝试通过实例修改类属性,您实际上会创建一个隐藏类属性的实例属性..【讨论】:
不,你也没有声明类属性。你定义它们。这些定义是可执行语句并且非常普通,它们不是操纵某些函数的范围,而是操纵类的属性。并且类属性也不是一成不变的:添加、替换和删除类属性是微不足道的。 我还是不明白你为什么一开始就区分类和实例属性。两者都是在运行时显式定义的,在这两种情况下,这些定义和重新定义都可以随时发生。【参考方案7】:不,python 就是这样灵活的,它不强制您可以在用户定义的类中存储哪些属性。
但是有一个技巧,在类定义上使用__slots__
attribute 会阻止您创建未在__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'
【讨论】:
@NlightNFotis 你没有。 Python 不是 Java,您不应该尝试用 Python 编写 Java。如果您想这样做,请编写 Java。 分享没有安全网的编程乐趣:) @NlightNFotis:Java 的类型安全是一种错觉。它感觉更安全,更安全,你可以更信任代码,但你真的不能。蛋糕是个谎言。 @NlightNFotis:Python 的哲学被描述为“我们在这里都是同意的成年人”。这意味着如果您想打破惯例并直接更改foo._variable
- 也许是为了调试,或者为了避免 foo 中的一些错误 - 您可以。但是如果它破坏了某些东西,你就不能向 foo 的作者抱怨。
__slots__
是为了节省内存;它不应该被用作锁定课程的一种方式。【参考方案8】:
它创建my_circle
的radius
数据成员。
如果你要求它提供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
【讨论】:
虽然你可以做Circle.xyz = 5
并更改类而不是实例...以上是关于为啥允许向已经实例化的对象添加属性?的主要内容,如果未能解决你的问题,请参考以下文章