为什么Python没有内置混合的getattr和__getitem__?

12 投票
3 回答
4369 浏览
提问于 2025-04-16 21:47

我有一些方法可以接受字典或其他对象,以及要从这些对象中获取的“字段”名称。如果这个对象是字典,那么这个方法就会用 __getitem__ 来获取指定的键;如果不是字典,它就会用 getattr 来获取指定的属性。这种做法在网页模板语言中非常常见。例如,在一个 Chameleon 模板中,你可能会看到:

<p tal:content="foo.keyname">Stuff goes here</p>

如果你传入一个像 {'keyname':'bar'} 这样的字典给 foo,那么 foo.keyname 就会获取 'keyname' 这个键的值,也就是 'bar'。如果 foo 是一个类的实例,比如:

class Foo(object):
    keyname = 'baz'

那么 foo.keyname 就会从 keyname 这个属性中获取值。Chameleon 本身在 chameleon.py26 模块中是这样实现这个功能的:

def lookup_attr(obj, key):
    try:
        return getattr(obj, key)
    except AttributeError as exc:
        try:
            get = obj.__getitem__
        except AttributeError:
            raise exc
        try:
            return get(key)
        except KeyError:
            raise exc

我在 我自己的包 中是这样实现的:

try:
    value = obj[attribute]
except (KeyError, TypeError):
    value = getattr(obj, attribute)

问题是,这种模式非常常见。我在很多模块中都见过类似的方法。那么,为什么在语言的核心部分,或者至少在某个核心模块中没有类似的东西呢?如果没有的话,有没有一种明确的方法来写出这种功能?

3 个回答

5

你可以很简单地写一个自己的 dict 子类,让它自然地以这种方式工作。我称这种简单的实现为“属性堆”,代码大概是这样的:

class Pile(dict):
    # raise AttributeError for missing key here to fulfill API
    def __getattr__(self, key):
        if key in self:
            return self[key]
        else:
            raise AttributeError(key)
    def __setattr__(self, key, value):
        self[key] = value

不过,如果你需要处理传给你的字典或者带有很多属性的对象,而不是从一开始就控制这个对象,这种方法就没什么帮助了。

在你的情况下,我可能会使用一种和你现在的方式非常相似的方法,只不过把它拆分成一个函数,这样就不用每次都重复写了。

6

在Python的标准库中,最接近的东西是namedtuple(),你可以在这里查看详细信息:http://docs.python.org/dev/library/collections.html#collections.namedtuple

Foo = namedtuple('Foo', ['key', 'attribute'])
foo = Foo(5, attribute=13)
print foo[1]
print foo.key

或者你可以很简单地定义自己的类型,这种类型实际上会存储到字典中,但看起来又像是在设置和获取属性:

class MyDict(dict):
    def __getattr__(self, attr):
        return self[attr]
    def __setattr__(self, attr, value):
        self[attr] = value

d = MyDict()

d.a = 3
d[3] = 'a'
print(d['a']) # 3
print(d[3]) # 'a'
print(d['b']) # Returns a keyerror

不过不要写 d.3,因为那是语法错误。当然,还有更复杂的方法可以创建这样的混合存储类型,可以在网上搜索很多例子。

至于如何同时检查这两者,Chameleon的方法看起来很全面。至于“为什么标准库里没有办法同时做到这两点”,原因是模糊性是个坏事。没错,我们在Python中有鸭子类型和其他各种伪装,类实际上也就是字典,但在某些时候,我们希望从像字典或列表这样的容器中获得不同的功能,而不是从类中获得,因为类有它的方法解析顺序、重写等特性。

18

我大概半心半意地读了你的问题,写了下面的内容,然后又重新读了一遍你的问题,才发现我回答的是一个稍微不同的问题。不过我觉得下面的内容其实还是能提供一些答案。如果你觉得不合适,那就假装你问的是这个更一般的问题,我觉得它包含了你的问题作为一个子问题:

“为什么Python没有提供任何内置的方法来把属性和项目当作可互换的东西?”


我对这个问题想了不少,觉得答案其实很简单。当你创建一个容器类型时,区分属性项目是非常重要的。一个设计得比较好的容器类型通常会有一些属性——通常是方法——来帮助它优雅地管理内容。比如说,字典(dict)有itemsvalueskeysiterkeys等等。这些属性都是用.来访问的。而项目则是用[]来访问的。所以它们之间不会冲突。

如果你用.来访问项目,会发生什么呢?这就会出现命名空间重叠的问题。那你现在该怎么处理冲突呢?如果你继承了一个字典并给它这个功能,要么你就不能像items那样使用键,要么你得创建某种命名空间层级。第一种选择会造成一个繁琐的规则,难以遵循和执行。第二种选择则会增加复杂性,而且并不能完全解决冲突问题,因为你仍然需要一个替代接口来指定你想要的是items这个项目还是items这个属性。

对于某些非常简单的类型,这种情况是可以接受的。这可能就是为什么标准库里有namedtuple的原因。例如。(但要注意,namedtuple也会面临这些问题,这可能就是它被实现为工厂函数的原因(防止继承),并使用像_asdict这样的奇怪的私有方法名。)

创建一个没有(公共)属性的object子类并使用setattr是非常简单的。重写__getitem____setitem____delitem__来调用__getattribute____setattr____delattr__,让项目访问变成getattr()setattr()等的语法糖也是相对简单的。(不过这有点问题,因为这样会产生一些意想不到的行为。)

但是对于任何想要扩展和继承的成熟容器类来说,添加新的、有用的属性,使用__getattr__ + __getitem__的混合方式,坦白说,会是个巨大的麻烦。

撰写回答