Python、__slots__和“attribute is read-only”

26

我想在Python中创建一个带有几个属性的对象,并且希望保护自己不会意外使用错误的属性名称。代码如下:

class MyClass( object ) :
    m = None # my attribute
    __slots__ = ( "m" ) # ensure that object has no _m etc

a = MyClass() # create one
a.m = "?"  # here is a PROBLEM

但是在运行这个简单的代码后,我得到了一个非常奇怪的错误:

Traceback (most recent call last):
  File "test.py", line 8, in <module>
    a.m = "?"
AttributeError: 'test' object attribute 'm' is read-only

有没有聪明的程序员能抽出一点时间并启迪我关于“只读”错误的问题?


1
这似乎是一个不必要的用例。你为什么要这样做?为什么不使用属性? - S.Lott
我想避免因为打错属性名而出现错误,例如如果我写成 object.my_prperty = 1 而不是 object.my_property = 1(少了一个“o”),Python 不会显示任何错误,但我的程序将出现逻辑错误。因此,我想将属性限制为预定义的集合,这样只有它们才能被访问。如何使用属性来解决这个问题? - grigoryvp
9
通常在Python编程中,我们接受作业中可能出现的拼写错误,并编写良好的测试来捕获表面和更语义化的错误。我谦虚地建议,如果你想写Python代码,应该考虑按照Python的方式进行,而不是费力地对抗这门语言。我想补充一点,过去9年里我的Python代码中的拼写错误可能只浪费了我大约20分钟的时间。 - llimllib
1
你也可以尝试使用Python IDE。我通常使用IDLE,其中(在shell中而非.py文件中)你可以输入a=MyClass(),然后输入a.,它会显示所有属性。我也用过PyDev,这是适用于Eclipse的Python插件,它甚至可以在.py文件中实现该功能。市场上有很多其他IDE可供选择。请在Google或本网站上搜索“Python IDE”,相信你一定会找到喜欢的一个。 - Tyler
错误应显示为 AttributeError: 'MyClass'对象属性'm'是只读的 - Mr_and_Mrs_D
5个回答

49
当你使用__slots__声明实例变量时,Python会创建一个与名字相同的类变量描述符对象。在你的情况下,这个描述符对象被你定义在下一行的类变量m所覆盖。
  m = None # my attribute

以下是您需要做的事情:不要定义一个名为m的类变量,在__init__方法中初始化实例变量m

class MyClass(object):
  __slots__ = ("m",)
  def __init__(self):
    self.m = None

a = MyClass()
a.m = "?"

顺便提一下,单元素元组需要在元素后加上逗号。你的代码两种写法都可以工作,因为__slots__接受一个字符串或一个可迭代/序列的字符串。一般来说,要定义包含元素1的元组,请使用(1,)1,而不是(1)


你知道为什么如果我这样写: slots = [ "m" ] m = 5Python会覆盖描述符对象,并且不使用描述符对象的set()方法来设置值吗? - grigoryvp
7
由于在此上下文中,m是一个类变量而不是实例变量。为了使用描述符的set()方法,你应该将其赋值给一个对象的m,而不是类。如果你想要初始化对象的实例变量,在__init__方法中进行,就像我在代码片段中所做的那样。 - Ayman Hourieh
文档中的关键信息(顺便提一下,已经移动到这里)是“通过为每个变量名创建描述符,在类级别上实现__slots__。因此,类属性不能用于为由__slots__定义的实例变量设置默认值;否则,类属性将覆盖描述符分配”。所以发生的情况是(无论定义m__slots__的顺序如何?)当尝试赋值时,set方法不再映射到任何东西 - 也没有get,但它会回退到MyClass.m。 - Mr_and_Mrs_D

11
您完全误用了__slots__,这会阻止实例创建__dict__。只有在遇到许多小对象导致内存问题时,才有意义,因为去掉__dict__可以减少占用空间。这是一种极端的优化,在99.9%的情况下不需要。
如果您需要所描述的安全性,则Python确实不是正确的语言。最好使用严格的语言,如Java(而不是尝试在Python中编写Java)。
如果您无法自行弄清楚为什么类属性会导致代码出现问题,那么也许您应该三思而后行,不要引入此类语言黑科技。可能更明智的做法是首先熟悉该语言。
仅供完整性,这是slots文档链接

4
完整性加一。如多处提到的那样,__slots__并不是一个坏主意,但需要谨慎使用。它不仅是一个安全检查,而且增加了许多复杂性,需要注意如何使用对象。 - David Berger

7

__slots__ 作用于实例变量,而你拥有的是类变量。你应该这样做:

class MyClass( object ) :
  __slots__ = ( "m", )
  def __init__(self):
    self.m = None

a = MyClass()
a.m = "?"       # No error

6
考虑一下这个问题。
class SuperSafe( object ):
    allowed= ( "this", "that" )
    def __init__( self ):
        self.this= None
        self.that= None
    def __setattr__( self, attr, value ):
        if attr not in self.allowed:
            raise Exception( "No such attribute: %s" % (attr,) )
        super( SuperSafe, self ).__setattr__( attr, value )

更好的方法是使用单元测试来进行这种检查。这会增加一定的运行时开销。

3
但是这段代码比 slots 更长吗?为什么要手动检查语言可以自动执行的内容。 - grigoryvp
3
为避免出现“非常奇怪的错误”信息并避免花费大量时间调试__slots__内部的神秘内容,我更喜欢明显且简单的解决方案,不需要秘密知识来进行调试。 - S.Lott
4
无法添加新的实例变量是 slots 的副作用,而不是它的目的(即性能优化)。 - Ravi
@Ravi - 我认为那就是问题的核心。 - Ben Blank

0
class MyClass( object ) :
    m = None # my attribute

这里的m是类属性,而不是实例属性。你需要在__init__中使用self将其与你的实例连接起来。

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