【问题标题】:How can we write a `__getitem__` method which accepts any iterable as input, and chains the items together? [closed]我们如何编写一个 `__getitem__` 方法来接受任何可迭代作为输入,并将这些项目链接在一起? [关闭]
【发布时间】:2022-08-24 05:38:03
【问题描述】:

我们如何将cookiejar[(1, 2, 3)] 变成cookiejar[1][2][3]

期望的行为是什么?

以下两段代码(LEFT CODERIGHT CODE)在调用__getitem__ 时应该做同样的事情

+----------------------+--------------------------+
|      LEFT CODE       |        RIGHT CODE        |
+----------------------+--------------------------+
| cjar   = CookieJar() | cjar     = CookieJar()   |
| result = cjar[index] | indices  = [1, 2, 3]     |
|                      | indices  = iter(index)   |
|                      | index    = next(it)      |
|                      | result = cjar[index][it] |
+----------------------+--------------------------+

更多示例如下所示。左侧列中的代码应该表现出与右侧列中的代码相同的外部行为。

+----------------------------+-------------------------------+
|  cookie_jar[1][2][3]       |  cookie_jar[(1, 2, 3)]        |
+----------------------------+-------------------------------+
|  cookie_jar[x][y]          |  cookie_jar[(x, y)]           |
+----------------------------+-------------------------------+
|  cookie_jar[99]            |  cookie_jar[(99,)]            |
+----------------------------+-------------------------------+
|  cookie_jar[99]            |  cookie_jar[[[[99]]]          |
+----------------------------+-------------------------------+
|  cookie_jar[1][2][3]       |  cookie_jar[1, 2][3]          |
+----------------------------+-------------------------------+
|  cookie_jar[1][2][3]       |  cookie_jar[[1, [2]], [3]]    |
+----------------------------+-------------------------------+
|  cookie_jar[1][2][3]       |  cookie_jar[1, 2, 3]          |
+----------------------------+-------------------------------+
|  cookie_jar[3][11][19]     |  cookie_jar[3:20:8]           |
+----------------------------+-------------------------------+
|  cookie_jar[3][11][19]     |  cookie_jar[range(3, 20, 8)]  |
+----------------------------+-------------------------------+

单个键/索引与键或索引容器之间有什么区别?

如果您尝试将table[\"hello world\"] 转换为table[\'h\'][\'e\'][\'l\'][\'l\'][\'o\']... [\'l\'][\'d\'],您可以轻松创建无限循环。

以下代码永远不会停止运行:

def go_to_leaf(root):
    while hasattr(root, \'__iter__\'):
        root = iter(root)
        root = next(root)

# BEGIN INFINITE LOOP!
left_most_leaf = go_to_leaf(\"hello world\")

应该改用这样的东西:

def is_leaf(cls, knode):
    \"\"\"
        returns true if the input is a valid key (index)
        into the container.

        returns false if the input is a container of keys
        or is an invalid key  
    \"\"\"
    if hasattr(knode, \"__iter__\"):
        return str(knode) == \"\".join(str(elem) for elem in knode)
    else: # not iterable
        return True

如果您有一个 3 维数字表,那么 x-y 坐标是在单个元组或列表中还是单独使用都没有关系。

element = table[2][7][3]
element = table[2, 7, 3]
element = table[(2, 7, 3)]
  • 一个函数不知道它的结果将如何被使用。所以cookiejar[1] 不能返回不同的结果,这取决于它是否会被[2] 进一步索引。
  • @Barmar 我们不需要知道root[1] 是否会被进一步索引或不被进一步索引。在树应用程序中root[1] 返回树中根节点的子节点。对于root[1][2],起初我们有一个根节点的子节点,但最终我们有一个根节点的孙子节点。我们写root[1]然后退出或写root[1][2]都没有关系。同样,如果您有一个数字矩阵 mat[3] 返回第 3 行。如果您想要第 3 行和第 8 列中的值,那么我们有 mat[3][8] 行对象不知道它是否会被进一步索引或它最终用户想要整行。
  • 对不起,我误解了你在问什么。问题太长了,大部分都没看。
  • @Barmar 这是一个很长的问题,是的。我希望我更简洁。我建议阅读除代码块之外的所有内容。如果您仍然感兴趣,请返回并阅读代码块。
  • 所以你基本上是在问如何将cookiejar[(1, 2, 3)] 变成cookiejar[1][2][3]?后者是索引多个维度的正常方法,您希望能够使用可迭代来代替。

标签: python python-3.x indexing containers operator-overloading


【解决方案1】:

基本理念

与其制作一个单独的容器类型,不如制作一个看法用于容器。语义是:

  • 视图实例跟踪一些可迭代对象(可能是其他可迭代对象的元素)。为简单起见,我们不会检查它是否是正确的容器类型或延迟评估。

  • 当视图使用不可迭代类型的值进行索引时,它会使用该值索引到容器中。

  • 当视图使用可迭代类型的值进行索引时,它会为该值中的每个元素重复索引。

  • 如果索引的结果是可迭代的,则结果是围绕该可迭代的视图。否则,结果就是值本身。

它可以很简单地实现:

class View:
    def __init__(self, data):
        self._data = data

    def __getitem__(self, indices):
        result = self._data
        # We can't easily distinguish a `TypeError` due to `indices`
        # being a non-iterable, from a `TypeError` due to reaching a 
        # leaf in the data prematurely. So we explicitly check first.
        try:
            iter(indices)
        except TypeError:
            result = result[indices]
        else:
            for i in indices:
                result = result[i]
        # Now decide whether to wrap the result
        try:
            iter(result)
        except TypeError:
            return result
        else:
            return View(result)

作为重构,我们可以使用__new__ 而不是__init__,这样如果参数不可迭代,则返回不变。这可以防止显式创建坏视图,还可以简化__getitem__ 逻辑:

class View:
    def __new__(cls, data):
        try:
            iter(data)
            result = object.__new__(cls)
            result._data = data
        except TypeError:
            result = data
        return result

    def __getitem__(self, indices):
        result = self._data
        try:
            iter(indices)
        except TypeError:
            result = result[indices]
        else:
            for i in indices:
                result = result[i]
        return View(result)

特别案例

与规范相比,这个结果有两个问题:

  1. slice 对象实际上是不可迭代的。我们想解释myview[3:20:8],就好像它实际上是用它描述的值索引的一样范围, 按顺序。幸运的是,将slice 转换为具有相同startstopstep 的对应range 对象很简单。

    但是,如果 startstop 未指定,我们需要抱怨,否则语义没有任何意义;我们必须记住,范围不接受None 作为步长值(slices 将其视为等同于1)。最后,我们必须接受负值不会从最后开始索引,因为再次解释所有极端情况应该发生的事情太困难了。

  2. 字符串(可能还有其他类型)可迭代,并且元素本身是非空字符串 - 因此它们可以被任意多次索引。我们需要对它们进行特殊处理,以便它们作为叶节点工作。

    我们需要辅助逻辑来处理字符串,就好像它们不可迭代一样。它也应该适用于构造(因为否则我们可以从字符串中创建一个完全无用的View 实例)。我们不希望该逻辑处理切片,因为View(slice(0)) 应该给我们原始的slice,而不是range

    通过一些重构,我们得到:

    def _make_range(a_slice):
        start, stop, step = a_slice.start, a_slice.stop, a_slice.step
        if start is None or stop is None:
            raise ValueError('start and stop must be defined to convert to range')
        return range(start, stop, 1 if step is None else step)
    
    def _non_string_iterable(obj):
        try:
            iter(data)
            return not isinstance(obj, str)
        except TypeError:
            return False
    
    class View:
        def __new__(cls, data):
            if _non_string_iterable(data):
                result = object.__new__(cls)
                result._data = data
                return result
            return data
    
        def __getitem__(self, indices):
            result = self._data
            if isinstance(indices, slice):
                indices = _make_range(indices)
            if _non_string_iterable(indices):
                for i in indices:
                    result = result[i]
            else:
                result = result[indices]
            return View(result)
    

【讨论】:

  • 您的定义 def __getitem__(self, indices) 强制 __getitem__ 只接受一个参数。我们希望__getitem__ 是可变的。在对 3 维表的函数调用中,我们可以写成 table[1, 68, 2] 我建议写成 def __getitem__(self, *indices) 从那时起我们可以写成 table[1, 68, 2]。如果我们按照你原来的方式做事,那么我们必须写table[(1, 68, 2)]table[[1, 68, 2]] 之类的东西。也就是说,我们可以在__getitem__ 中输入一个元组或一个列表,但不能超过一个参数。理想情况下,__getitem__ 可以接受任意数量的参数。
  • try: iter(index) 不是检查索引是否为叶节点的好方法。正如您所指出的,字符串可以无限索引。例如,next(iter(next(iter("A")))) == "A"。此外,如果 index 是 1 个或多个字符的字符串,则以下是无限循环:while hasattr(index, "__iter__"): index = next(iter(index))。字符串不是唯一这样做的类。有一个bytes 类,其children 的children 的children 总是可迭代的。在我最初的问题中,我解释说叶节点的测试是str(knode) == "".join(str(elem) for elem in knode)
  • 我们应该测试str(parent) == "".join(str(child) for child in parent) 而不是测试hasattr(index, "__iter__") 或测试isinstance(index, str)
  • “你的定义def __getitem__(self, indices) 强制__getitem__ 只接受一个参数。我们希望__getitem__ 是可变的。”不,这不是__getitem__ 的工作方式。如果您拨打x[3, 4, 5] 之类的电话,则将通过__getitem__一个元组在这些值中,而不是单独的参数 - 它与 x[(3, 4, 5)] 相同。 __getitem__ 在被 Python 内部调用时只会收到一个参数。
  • try: iter(index) 不是检查索引是否为叶节点的好方法。”这就是我在下一步中构建包装器的原因。 bytes 不像在 3.x 中那样工作 - 索引到 bytes 会给出一个整数。我看到了您的测试逻辑,您可以轻松地将其换成_non_string_iterable
【解决方案2】:

结合collapse()dig() 的Python 版本,以及特殊的slice 处理,重现您输入的示例表:

from more_itertools import collapse  # or implement this yourself
from unittest.mock import MagicMock


def dig(collection, *keys):
    """Dig into nested subscriptable objects, e.g. dict and list, i.e JSON."""
    curr = collection
    for k in keys:
        if curr is None:
            break

        if not hasattr(curr, '__getitem__') or isinstance(curr, str):
            raise TypeError(f'cannot dig into {type(curr)}')

        try:
            curr = curr[k]
        except (KeyError, IndexError):
            curr = None

    return curr


def what_you_wanted(collection, *keys):  # If I understood you correctly
    slic = keys[0] if len(keys) == 1 and isinstance(keys[0], slice) else None
    dig_keys = range(slic.stop)[slic] if slic else collapse(keys)
    return dig(collection, *dig_keys)


def test_getitem_with(*keys):
    mock = MagicMock()
    mock.__getitem__.returns = mock
    what_you_wanted(mock, *keys)
    print(mock.mock_calls)


test_getitem_with((1, 2, 3))
test_getitem_with(('x', 'y'))
test_getitem_with((99,))
test_getitem_with([[[99]]])
test_getitem_with((1, 2), 3)
test_getitem_with(([1, [2]], [3]))
test_getitem_with(1, 2, 3)
test_getitem_with(slice(3, 20, 8))
test_getitem_with(range(3, 20, 8))

印刷:

[call.__getitem__(1),
 call.__getitem__().__getitem__(2),
 call.__getitem__().__getitem__().__getitem__(3)]
[call.__getitem__('x'), call.__getitem__().__getitem__('y')]
[call.__getitem__(99)]
[call.__getitem__(99)]
[call.__getitem__(1),
 call.__getitem__().__getitem__(2),
 call.__getitem__().__getitem__().__getitem__(3)]
[call.__getitem__(1),
 call.__getitem__().__getitem__(2),
 call.__getitem__().__getitem__().__getitem__(3)]
[call.__getitem__(1),
 call.__getitem__().__getitem__(2),
 call.__getitem__().__getitem__().__getitem__(3)]
[call.__getitem__(3),
 call.__getitem__().__getitem__(11),
 call.__getitem__().__getitem__().__getitem__(19)]
[call.__getitem__(3),
 call.__getitem__().__getitem__(11),
 call.__getitem__().__getitem__().__getitem__(19)]

为了完成,可以使用what_you_wanted() 定义一个实现__getitem__() 的集合对象(或视图)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-09-18
    • 1970-01-01
    • 1970-01-01
    • 2011-04-11
    • 2015-06-03
    • 1970-01-01
    • 2023-01-02
    • 1970-01-01
    相关资源
    最近更新 更多