关于python:编写一个Class,当调用list(c)和dict(c)时返回不同的值

Write a Class which returns different values when called as list(c) and dict(c)

我试图实现一个自定义类,当调用list(c)dict(c)时,该类返回不同的值。但是,我的印象是,list(c)dict(c)都在发动机罩下使用c.__iter__()?如果是这样的话,我怎样才能有不同的行为称为list(c)dict(c)?我知道这是可能的,因为python字典和pandas数据帧有不同的混合体。

例如:

1
2
3
4
5
6
7
8
class Foo:
    def __init__(self):
        self._keys = ['a', 'b', 'd', 'd', 'e']
        self._data = [10, 20, 30, 40, 50]

    def __iter__(self):
        for key, value in zip(self._keys, self._data):
            yield key, value

打电话给dict(c)我得到了我想要的:

1
2
3
>>> f = Foo()
>>> dict(f)
{'a': 10, 'b': 20, 'd': 40, 'e': 50}

但是,我不能让list(c)打印出一个键(或值)列表,而是同时得到两个键:

1
2
3
>>> f = Foo()
>>> list(f)
[('a', 10), ('b', 20), ('d', 30), ('d', 40), ('e', 50)]

字典的等效代码要干净得多:

1
2
3
4
5
>>> f = {'a': 10, 'b': 20, 'c': 30, 'd': 40, 'e': 50}
>>> dict(f)
{'a': 10, 'b': 20, 'c': 30, 'd': 40, 'e': 50}
>>> list(f)
['a', 'b', 'c', 'd', 'e']


显然,__iter__只能返回密钥,否则list(f)将无法工作。

python文档说明了dict构造函数的以下内容:

If a positional argument is given and it is a mapping object, a dictionary is created with the same key-value pairs as the mapping object.

现在,问题是什么"映射"对于dict构造函数足够?DataFrame不继承任何映射类,也不针对抽象基类注册。事实证明,我们只需要支持keys方法:如果传递给dict构造函数的对象有一个名为keys的方法,则调用该方法是为了提供键[cpython source]的不可重复项。对于每个键,通过索引获取值。

也就是说,dict构造函数执行以下逻辑等效操作:

1
2
3
4
5
if hasattr(source, 'keys'):
    for k in source.keys():
        self[k] = source[k]
else:
    self.update(iter(source))

用这个我们得到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Foo:
    def __init__(self):
        self._keys = ['a', 'b', 'd', 'd', 'e']
        self._data = [10, 20, 30, 40, 50]

    def __iter__(self):
        return iter(self.keys)

    def __getitem__(self, key):
        idx = self._keys.index(key)
        return self._data[idx]

    def keys(self):
        return self._keys

测试:

1
2
3
4
5
6
>>> f = Foo()
>>> list(f)
['a', 'b', 'd', 'd', 'e']

>>> dict(f)
{'d': 30, 'e': 50, 'a': 10, 'b': 20}

(从上面的代码中可以看到,不需要实际继承任何东西)

但是,并不能保证所有映射构造函数的行为都是相同的——其他一些可能称为items——因此,最兼容的方法是实现collections.abc.Mapping所需的所有方法并从中继承。也就是说,这就足够了

1
2
3
4
5
6
7
8
9
10
11
class Foo(collections.abc.Mapping):
    ...
    def __getitem__(self, key):
        idx = self._keys.index(key)
        return self._data[idx]

    def __iter__(self):
        return iter(self._keys)

    def __len__(self):
        return len(self._keys)


@米吉尔森的评论是正确的,这可以通过继承collections.abc.Mapping类来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Foo(collections.abc.Mapping):
    def __init__(self):
        self._keys = ['a', 'b', 'd', 'd', 'e']
        self._data = [10, 20, 30, 40, 50]

    def __iter__(self):
        for key in self._keys:
            yield key

    def __getitem__(self, value):
        return self._data[self._keys.index(value)]

    def __len__(self):
        return len(self._keys)
1
2
3
4
5
6
>>> f = Foo()
>>> list(f)
['a', 'b', 'd', 'd', 'e']

>>> dict(f)
{'a': 10, 'b': 20, 'd': 30, 'e': 50}