【问题标题】: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__ 属性引用,该属性恰好在对象构造之后被调用。

标签: python attributes python-3.x declaration


【解决方案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 AttributesInstance 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_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
    

    【讨论】:

    • 虽然你可以做Circle.xyz = 5 并更改类而不是实例...
    猜你喜欢
    • 2020-02-20
    • 2011-11-01
    • 2012-10-01
    • 2013-05-05
    • 1970-01-01
    • 1970-01-01
    • 2019-02-06
    • 1970-01-01
    相关资源
    最近更新 更多