允许在Python属性中使用特殊字符的原因

7
我有些偶然地发现,您可以使用setattr为对象设置“非法”属性。所谓“非法”,是指无法使用传统的 . 操作符引用中的__getattr__接口检索名称的属性。它们只能通过 getattr 方法检索。

对我来说,这似乎相当惊人,我想知道是否有原因,或者是否只是被忽视了等等。由于存在用于检索属性的运算符,以及 setattribute 接口的标准实现,我期望它只允许实际上可以正常检索的属性名称。如果您有一些奇怪的原因想要具有无效名称的属性,则必须为其自己实现接口。

这种行为让我感到惊讶,难道只有我一个人吗?

class Foo:
    "stores attrs"

foo = Foo()
setattr(foo, "bar.baz", "this can't be reached")
dir(foo)

这个方法返回的结果既奇怪又有点误导性:[...'__weakref__', 'bar.baz']

如果我想以“标准”方式访问foo.bar.baz,我是无法实现的。不能检索到它是合理的,但能够设置它是令人惊讶的。

foo.bar.baz
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Foo' object has no attribute 'bar'

假设你必须使用setattr设置变量,那么就默认你将通过getattr来引用它吗?因为在运行时,这并不总是正确的,尤其是在Python的交互式解释器、反射等方面。这仍然似乎非常奇怪,为什么默认情况下会允许这种情况。

编辑:以下是setattr默认实现的(非常粗略)示例:

import re

class Safe:
    "stores attrs"

    def __setattr__(self, attr, value):
        if not re.match(r"^\w[\w\d\-]+$", attr):
            raise AttributeError("Invalid characters in attribute name")
        else:
            super().__setattr__(attr, value)

这将不允许我在属性名称中使用无效字符。显然,super() 不能用于基础 Object 类,但这只是一个例子。


1
你正在使用哪个版本的Python?你也可以使用foo.__dict__['bar.baz']... - Charles D Pantoga
Python 3.5.0。感谢提供另一种方法。那种方法有什么优点超过getattr吗?还是只是个人偏好? - Keozon
@Keozon -- 推荐使用getattr。它适用于没有__dict__的对象,例如。这样的对象很少见(因为通常不鼓励创建它们),但它们确实存在,并且对某些目的非常有用。 - mgilson
2个回答

5
我认为你关于属性“必须”是“标识符”的假设是不正确的。正如你所指出的,Python对象支持任意属性(不仅限于标识符),因为对于大多数对象,属性存储在实例的__dict__中(它是一个dict,因此支持任意字符串键)。然而,为了拥有属性访问运算符,可以通过这种方式进行访问的名称集合需要受到限制,以便生成可解析的语法。
“如果你必须使用setattr设置变量,是否简单地认为你将通过getattr引用它?”
不是的。我不认为会有这种假设。我认为的假设是,如果您正在使用“。”运算符引用属性,则知道这些属性是什么。如果您能够知道这些属性是什么,那么可能可以控制它们的名称。如果您可以控制它们的名称,则可以将它们命名为解析器知道如何处理的内容;-)。

1
dict中的键不一定都是字符串,如果需要的话,您可以在同一个字典中混合使用不同类型的键。 - Mark Ransom
@MarkRansom -- 没错。我并不是想暗示 dict 只能保存字符串。我只是想说,如果它是一个字符串,那么 dict 可以保存它,并且 setattr 确实可以防止非字符串被设置为属性。 - mgilson
@mgilson 谢谢。你对我的假设是正确的,也许你对原因也是正确的。我所困扰的是,在我看来,使Python成为一种伟大语言的东西是每个运算符都是通过某些接口实现的。由于这种观点,我倾向于认为运算符是利用接口的最优雅或“标准”方式...因此,接口的“标准”实现应符合运算符的限制。我需要考虑一下这个问题。 - Keozon

2
我看到语言特性是实现方式的意外副作用。
有几个问题表明这个特性是一个副作用。
首先,来自"Python之禅":
应该只有一种——最好只有一种——显而易见的方法去做。
对我来说,访问属性的明显方式是使用 "." 操作符。因此,我认为与操作符不兼容的名称是非法的,因为它们需要“黑科技”来使用。
其次,尽管我们可以在实例的__dict__中有整数键(如Mark Ransom所指出的),但我不认为int是一个有效的属性名称。尤其是它破坏了对象的行为:
>>> a.__dict__[12] = 42
>>> dir(a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unorderable types: int() < str()

第三点,Python文档关于.操作符和getattr()内置函数等价的陈述并非完全正确。它们的区别在于生成的字节码不同。前者会编译为LOAD_ATTR字节码,而后者则编译为CALL_FUNCTION

>>> dis.dis(lambda x: x.a)
  1           0 LOAD_FAST                0 (x)
              3 LOAD_ATTR                0 (a)
              6 RETURN_VALUE
>>> dis.dis(lambda x: getattr(x, 'a'))
  1           0 LOAD_GLOBAL              0 (getattr)
              3 LOAD_FAST                0 (x)
              6 LOAD_CONST               1 ('a')
              9 CALL_FUNCTION            2 (2 positional, 0 keyword pair)
         12 RETURN_VALUE

同样适用于setattr()内置函数。因此,我认为内置函数是一种解决方法,旨在方便动态属性访问(该内置函数在Python 0.9.1中不存在)。
最后,以下代码(声明__slots__属性)会失败:
>>> class A(object):
...     __slots__ = ['a.b']
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __slots__ must be identifiers

这表明属性名称应该是标识符。然而,由于我找不到允许的属性名称的正式语法,我也认为@mgilson提出的观点是有道理的。

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