我们如何编写一个接受任何可迭代项作为输入并将项链接在一起的`__getitem__`方法



如何将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

如果你有一个三维数字表,那么x-y坐标是在一个元组或列表中,还是单独使用都无关紧要。

element = table[2][7][3]
element = table[2, 7, 3]
element = table[(2, 7, 3)]

基本思想

与其创建单独的容器类型,不如为容器创建视图。语义是:

  • 视图实例跟踪一些可迭代的(可能是其他可迭代的元素(。为了简单起见,我们不必检查它是一个正确的容器类型还是经过延迟评估的。

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

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

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

它可以很简单地实现:

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)

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__()的集合对象(或视图(。

相关内容

最新更新