为什么Python没有内置混合的getattr和__getitem__?
我有一些方法可以接受字典或其他对象,以及要从这些对象中获取的“字段”名称。如果这个对象是字典,那么这个方法就会用 __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 个回答
你可以很简单地写一个自己的 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
不过,如果你需要处理传给你的字典或者带有很多属性的对象,而不是从一开始就控制这个对象,这种方法就没什么帮助了。
在你的情况下,我可能会使用一种和你现在的方式非常相似的方法,只不过把它拆分成一个函数,这样就不用每次都重复写了。
在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中有鸭子类型和其他各种伪装,类实际上也就是字典,但在某些时候,我们希望从像字典或列表这样的容器中获得不同的功能,而不是从类中获得,因为类有它的方法解析顺序、重写等特性。
我大概半心半意地读了你的问题,写了下面的内容,然后又重新读了一遍你的问题,才发现我回答的是一个稍微不同的问题。不过我觉得下面的内容其实还是能提供一些答案。如果你觉得不合适,那就假装你问的是这个更一般的问题,我觉得它包含了你的问题作为一个子问题:
“为什么Python没有提供任何内置的方法来把属性和项目当作可互换的东西?”
我对这个问题想了不少,觉得答案其实很简单。当你创建一个容器类型时,区分属性和项目是非常重要的。一个设计得比较好的容器类型通常会有一些属性——通常是方法——来帮助它优雅地管理内容。比如说,字典(dict)有items
、values
、keys
、iterkeys
等等。这些属性都是用.
来访问的。而项目则是用[]
来访问的。所以它们之间不会冲突。
如果你用.
来访问项目,会发生什么呢?这就会出现命名空间重叠的问题。那你现在该怎么处理冲突呢?如果你继承了一个字典并给它这个功能,要么你就不能像items
那样使用键,要么你得创建某种命名空间层级。第一种选择会造成一个繁琐的规则,难以遵循和执行。第二种选择则会增加复杂性,而且并不能完全解决冲突问题,因为你仍然需要一个替代接口来指定你想要的是items
这个项目还是items
这个属性。
对于某些非常简单的类型,这种情况是可以接受的。这可能就是为什么标准库里有namedtuple
的原因。例如。(但要注意,namedtuple
也会面临这些问题,这可能就是它被实现为工厂函数的原因(防止继承),并使用像_asdict
这样的奇怪的私有方法名。)
创建一个没有(公共)属性的object
子类并使用setattr
是非常简单的。重写__getitem__
、__setitem__
和__delitem__
来调用__getattribute__
、__setattr__
和__delattr__
,让项目访问变成getattr()
、setattr()
等的语法糖也是相对简单的。(不过这有点问题,因为这样会产生一些意想不到的行为。)
但是对于任何想要扩展和继承的成熟容器类来说,添加新的、有用的属性,使用__getattr__ + __getitem__
的混合方式,坦白说,会是个巨大的麻烦。