Indexing¶
x[...] desugars to __getitem__/__setitem__, but Lucid changes
what the desugaring means for multiple indices, removes a legacy iteration
fallback, and covers how indexed and destructuring access relate.
Comma-separated indexing passes multiple arguments¶
Python parses x[1, 2, 3] as a single tuple index. Lucid treats
comma-separated indexing as multiple index arguments.
x[1,
2, 3] and x[(1, 2, 3)] compile to the identical call,
x.__getitem__((1, 2, 3)), so __getitem__ has no way to tell which one
the caller wrote — not "must guess," since by the time it runs there is
nothing left to guess from. NumPy hits this wall directly: arr[i, j, k]
(three-axis indexing) and arr[(i, j, k)] (looking up one tuple-valued
key) are the same call, so NumPy simply cannot support both; indexing by an
actual tuple object as a single key needs a workaround like wrapping it in
another tuple. Lucid has no tuple to reach for either, so it keeps the two
meanings separate by construction: multiple arguments are comma-separated,
and one combined argument is built explicitly, with the immutable list
marker (see No tuple or namedtuple type):
Assignment follows the same rule:
grid[row, col] = value # grid.__setitem__(row, col, value)
table[![row, col]] = value # table.__setitem__(![row, col], value)
An explicit combined index remains available by constructing one directly,
as grid[![row, col]] does above. Single indexing passes one index the
same way:
No __getitem__ iteration fallback¶
Python has an old sequence-iteration fallback: if an object has __getitem__
but no __iter__, iteration may call __getitem__ with successive integers
until indexing fails.
class Pages:
def __getitem__(self, index):
if index >= 3:
raise IndexError
return "page " + str(index)
pages = Pages()
for page in pages:
print(page)
Pages never says it is iterable, Python prints page 0,
page 1, and page 2. The for loop silently tries pages[0],
pages[1], pages[2], and stops when pages[3] raises
IndexError.
The fallback means Python cannot even define "iterable" as "has
__iter__": Pages is iterable by that test's own behavior — for
accepts it — yet Python's own ABC disagrees with its own for loop:
from collections.abc import Iterable
isinstance(pages, Iterable) # False
iter(pages) # succeeds anyway
Iterable, and that
check is finally trustworthy.
No __delitem__¶
Python desugars del d[key] to d.__delitem__(key) — a third
indexing dunder, reached through a statement instead of a call, for an
operation every mapping or sequence type already needs to expose some
other way: dict.pop and set.remove exist because "did it work"
and "what was removed" matter more often than bare deletion says. The
same dunder handles a sequence slice too — del lst[i:j] calls
lst.__delitem__(slice(i, j)) — so a single removal, a slice removal,
and a mapping removal all route through one overloaded method reached
only through statement syntax. Lucid removes __delitem__ and every
del x[...] form with it. Removing a key or an element is an ordinary
method call, and removing a range is a third, dispatch-resolved case
of the same method rather than a separate operation:
del d[key] # not part of Lucid
d.pop(key) # removes key, returns its value
del lst[i] # not part of Lucid
lst.pop(i) # removes the element at i, returns it
del lst[i:j] # not part of Lucid
lst.pop(i, j) # removes the range [i, j), returns it as a list
lst.pop(i) and lst.pop(i, j) are two dispatch definitions, not
one method with an optional second parameter — they return different
types (T versus list[T]), the same reason No @overload replaces @overload with dispatch rather than a
default argument: a caller who wrote pop(i, j) should get a
list[T] back without narrowing a union it never needed.
pop is not the whole story: it addresses by key or index, the same
handle del already needed, so it is the direct replacement wherever
del applied. Removing by value instead — no index in hand, or none to
have, the way a set has none at all — is a different lookup and keeps its
own method, remove:
lst.remove(value) # removes the first occurrence of value
s.remove(item) # the only way to take a specific item out of a set
x[...] and x[...] = value
actually desugar to, __getitem__ and __setitem__; nothing reached
through the syntax goes further than what the syntax itself spells out.
Unpacking¶
Unpacking's assignment-target semantics and its precedence relative to binary operators are both covered together in No tuple or namedtuple type.