Skip to content
1 change: 1 addition & 0 deletions Include/internal/pycore_global_objects_fini_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Include/internal/pycore_global_strings.h
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(decoder)
STRUCT_FOR_ID(default)
STRUCT_FOR_ID(defaultaction)
STRUCT_FOR_ID(defaults)
STRUCT_FOR_ID(delete)
STRUCT_FOR_ID(depth)
STRUCT_FOR_ID(desired_access)
Expand Down
1 change: 1 addition & 0 deletions Include/internal/pycore_runtime_init_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Include/internal/pycore_unicodeobject_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

72 changes: 68 additions & 4 deletions Lib/operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@
'delitem', 'eq', 'floordiv', 'ge', 'getitem', 'gt', 'iadd', 'iand',
'iconcat', 'ifloordiv', 'ilshift', 'imatmul', 'imod', 'imul',
'index', 'indexOf', 'inv', 'invert', 'ior', 'ipow', 'irshift',
'is_', 'is_none', 'is_not', 'is_not_none', 'isub', 'itemgetter', 'itruediv',
'ixor', 'le', 'length_hint', 'lshift', 'lt', 'matmul', 'methodcaller', 'mod',
'mul', 'ne', 'neg', 'not_', 'or_', 'pos', 'pow', 'rshift',
'setitem', 'sub', 'truediv', 'truth', 'xor']
'is_', 'is_none', 'is_not', 'is_not_none', 'isub', 'itemgetter',
'itemtuplegetter', 'itruediv', 'ixor', 'le', 'length_hint', 'lshift', 'lt',
'matmul', 'methodcaller', 'mod', 'mul', 'ne', 'neg', 'not_', 'or_', 'pos',
'pow', 'rshift', 'setitem', 'sub', 'truediv', 'truth', 'xor']

from builtins import abs as _abs

Expand Down Expand Up @@ -307,6 +307,70 @@ def __repr__(self):
def __reduce__(self):
return self.__class__, self._items

class itemtuplegetter:
"""
Return a callable object that fetches the given items from its operand in a tuple.

If defaults is given, when called on an object where i-th `items` is not present,
the corresponding defaults is returned instead. If the defaults iterable is
shorter than subscripts iterable, the remaining subscripts have no defaults.
If the defaults iterable is longer than subscripts iterable, extra defaults are
ignored.

The returned callable has two read-only properties:
operator.itemtulegetter.items: a tuple containing items to fetch
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

itemtuPle
& one below

operator.itemtulegetter.defaults: a tuple containing provided defaults

For example,
After f = itemtuplegetter([0, 2], defaults=(-1, -2)), f([1, 2]) evaluates to (1, -2).
After g = itemtuplegetter([0, 2], defaults=(-1)), f([1, 2]) resutls in an IndexError.
After h = itemtuplegetter([0], defaults=(-1, -2)), f([1, 2]) evaluates to (1,).
After i = itemtuplegetter([1, 0], defaults=(-1, -2)), f([1, 2]) evaluates to (2, 1).
"""
__slots__ = ('_items', '_defaults')

def __init__(self, items, /, defaults = None):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No spaces around =: defaults=None

self._items = tuple(items)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if not isinstance(items, tuple):
    items = tuple(items)
self._items = items

self._defaults = () if defaults is None else tuple(defaults)
Copy link

@dg-pb dg-pb Nov 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will not cut it. E.g. if one sources itertools.repeat(None), this goes to infinite loop.

if defaults is None:
    defaults = ()
elif isinstance(defaults, (tuple, list)):
    defaults = defaults[:len(items)]
else:
    defaults = itl.islice(defaults, len(items))
if not isinstance(defaults, tuple):
    defaults = tuple(defaults)
self._defaults = defaults

Might need alternative to islice not use itertools if it is not a dependency. And maybe some simplification to above conditionals possible.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, probably need a test for infinite defaults iterator.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I had a version with a custom islice I forgot to update it. Thanks.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will not cut it. E.g. if one sources itertools.repeat(None), this goes to infinite loop.

...
elif isinstance(defaults, (tuple, list)):
    defaults = defaults[:len(items)]
else:
    defaults = itl.islice(defaults, len(items))
...

Might need alternative to islice not use itertools if it is not a dependency. And maybe some simplification to above conditionals possible.

So you would expect extra defaults to get cut-off at creation time, even for sequences? That changes things in c implementation. Just to be clear, you'd expect itemtuplegetter([], defaults=(1, 2, 3)).defaults == (), right?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.

Behaviour of sequences need to be consistent with iterators. Otherwise it is a mess...


@property
def items(self):
return self._items

@property
def defaults(self):
return self._defaults

def __call__(self, obj, /):
if not (defaults := self._defaults):
return tuple(obj[item] for item in self._items)
else:
result = []
append = result.append
items = iter(self._items)
for default in defaults:
try:
item = next(items)
except StopIteration:
return tuple(result)
try:
append(obj[item])
except (IndexError, KeyError):
append(default)

for item in items:
append(obj[item])
return tuple(result)

def __repr__(self):
return '%s.%s(%s, defaults=%s)' % (self.__class__.__module__,
self.__class__.__name__,
repr(self._items),
repr(self._defaults))

def __reduce__(self):
return self.__class__, (self._items, self._defaults)

class methodcaller:
"""
Return a callable object that calls the given method on its operand.
Expand Down
178 changes: 178 additions & 0 deletions Lib/test/test_operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,158 @@ class T(tuple):
self.assertEqual(operator.itemgetter(0)(['a', 'b', 'c']), 'a')
self.assertEqual(operator.itemgetter(0)(range(100, 200)), 100)

def test_itemtuplegetter(self):
operator = self.module
self.assertRaises(TypeError, operator.itemtuplegetter, 2)
self.assertRaises(TypeError, operator.itemtuplegetter, 2, 2)
self.assertRaises(TypeError, operator.itemtuplegetter, 2, 2,)
self.assertRaises(TypeError, operator.itemtuplegetter, (2,), 2)
a = 'ABCDE'
f = operator.itemtuplegetter((2,))
self.assertEqual(f(a), ('C',))
self.assertRaises(TypeError, f)
self.assertRaises(TypeError, f, a, 3)
self.assertRaises(TypeError, f, a, size=3)
f = operator.itemtuplegetter((10,))
self.assertRaises(IndexError, f, a)
f = operator.itemtuplegetter((10,), defaults=(0,))
self.assertEqual(f(a), (0,))


class C(object):
def __getitem__(self, name):
raise SyntaxError
self.assertRaises(SyntaxError, operator.itemtuplegetter((42,)), C())

f = operator.itemtuplegetter(('name',))
self.assertRaises(TypeError, f, a)
self.assertRaises(TypeError, operator.itemtuplegetter)

d = dict(key='val')
f = operator.itemtuplegetter(('key',))
self.assertEqual(f(d), ('val',))
f = operator.itemtuplegetter(('nonkey',))
self.assertRaises(KeyError, f, d)

# example used in the docs
inventory = [('apple', 3), ('banana', 2), ('pear', 5), ('orange', 1)]
sorter = operator.itemtuplegetter((1, 0))
self.assertEqual(list(map(sorter, inventory)),
[(3, 'apple'), (2, 'banana'), (5, 'pear'), (1, 'orange')])
self.assertEqual(sorted(inventory, key=sorter),
[('orange', 1), ('banana', 2), ('apple', 3), ('pear', 5)])

# multiple gets
data = list(map(str, range(20)))
self.assertEqual(operator.itemtuplegetter((2,10,5))(data), ('2', '10', '5'))
self.assertRaises(TypeError, operator.itemtuplegetter((2, 'x', 5)), data)

# interesting indices
t = tuple('abcde')
self.assertEqual(operator.itemtuplegetter((-1,))(t), ('e',))
self.assertEqual(operator.itemtuplegetter((slice(2, 4),))(t), (('c', 'd'),))

# defaults
inventory = dict(inventory)
seq = range(10)

kwd_default = operator.itemtuplegetter(("banana", "mango"), defaults=(0, -1))
pos_default = operator.itemtuplegetter(("banana", "mango"), (0, -1))
# positional default
self.assertEqual(kwd_default(inventory), (2, -1))
self.assertEqual(pos_default(inventory), (2, -1))

# defaults is None by default, and None is treated as empty tuple
f1 = operator.itemtuplegetter(())
f2 = operator.itemtuplegetter((), None)
f3 = operator.itemtuplegetter((), ())

self.assertEqual(repr(f1), repr(f2))
self.assertEqual(f1.defaults, f2.defaults)

self.assertEqual(repr(f1), repr(f3))
self.assertEqual(f1.defaults, f3.defaults)

# default shorter than items and items missing
self.assertRaises(KeyError,
operator.itemtuplegetter(("banana", "mango"), defaults=(0,)),
inventory)
self.assertRaises(IndexError,
operator.itemtuplegetter((9, 10), defaults=(0,)),
seq)
# default shorter than items, but missing item has default
self.assertEqual(operator.itemtuplegetter(("mango", "banana"), defaults=(0,))(inventory),
(0, 2))
# default longer than items and items missing
self.assertEqual(operator.itemtuplegetter(("banana", "mango"), defaults=(0, -1, 0))(inventory),
(2, -1))
self.assertEqual(operator.itemtuplegetter((9, 10), defaults=(0, -1, 0))(seq),
(9, -1))
# default shorter than items and items present
self.assertEqual(operator.itemtuplegetter(("banana", "pear"), defaults=(0, -1, 0))(inventory),
(2, 5))
self.assertEqual(operator.itemtuplegetter((8, 9), defaults=(0, -1, 0))(seq),
(8, 9))

# iterable variants
seq = range(10)
base = operator.itemtuplegetter((10, 12, 5), defaults=(-1, -2))

## iterators
self.assertEqual(operator.itemtuplegetter(iter([10, 12, 5]), defaults=iter((-1, -2)))(seq),
base(seq))
## lists
self.assertEqual(operator.itemtuplegetter([10, 12, 5], defaults=[-1, -2])(seq),
base(seq))
## tuple subclass
class T(tuple):
'Tuple subclass'
pass
self.assertEqual(operator.itemtuplegetter(T((10, 12, 5)), defaults=T((-1, -2)))(seq),
base(seq))
## custom iterable
self.assertEqual(operator.itemtuplegetter(Seq1([10, 12, 5]), defaults=Seq2([-1, -2]))(seq),
base(seq))
## generator + range
g = (i for i in [10, 12, 5])
self.assertEqual(operator.itemtuplegetter(g, defaults=range(-1, -3, -1))(seq),
base(seq))
## validate base
self.assertEqual(base(seq), (-1, -2, 5))

## str
self.assertEqual(operator.itemtuplegetter([10, 12, 5], defaults="abc")(seq),
("a", "b", 5))

# interesting init
EMPTY_TUPLE = ()
self.assertEqual(operator.itemtuplegetter(EMPTY_TUPLE)(seq), EMPTY_TUPLE)
self.assertEqual(operator.itemtuplegetter(EMPTY_TUPLE, EMPTY_TUPLE)(seq), EMPTY_TUPLE)
self.assertEqual(operator.itemtuplegetter(EMPTY_TUPLE, EMPTY_TUPLE)(EMPTY_TUPLE), EMPTY_TUPLE)
self.assertEqual(operator.itemtuplegetter(iter(EMPTY_TUPLE), EMPTY_TUPLE)(EMPTY_TUPLE), EMPTY_TUPLE)
self.assertEqual(operator.itemtuplegetter(iter(EMPTY_TUPLE), None)(EMPTY_TUPLE), EMPTY_TUPLE)

# attributes
itg = operator.itemtuplegetter((1, 2, 3))
self.assertEqual(itg.items, (1, 2, 3))
self.assertEqual(itg.defaults, ())
itg = operator.itemtuplegetter((1, 2, 3), defaults=None)
self.assertEqual(itg.items, (1, 2, 3))
self.assertEqual(itg.defaults, ())
itg = operator.itemtuplegetter(iter([1]), defaults=range(2))
self.assertEqual(itg.items, (1,))
self.assertEqual(itg.defaults, (0, 1))

self.assertRaises(AttributeError, setattr, itg, "items", 2)
self.assertRaises(AttributeError, setattr, itg, "defaults", 2)

with self.assertRaises(AttributeError):
itg.items = 2
with self.assertRaises(AttributeError):
itg.items += (2,)
with self.assertRaises(AttributeError):
itg.defaults += (2,)

def test_methodcaller(self):
operator = self.module
self.assertRaises(TypeError, operator.methodcaller)
Expand Down Expand Up @@ -637,6 +789,13 @@ def test_itemgetter_signature(self):
sig = inspect.signature(operator.itemgetter(2, 3, 5))
self.assertEqual(str(sig), '(obj, /)')

def test_itemtuplegetter_signature(self):
operator = self.module
sig = inspect.signature(operator.itemtuplegetter)
self.assertEqual(str(sig), '(items, /, defaults=None)')
sig = inspect.signature(operator.itemtuplegetter((2, 3, 5)))
self.assertEqual(str(sig), '(obj, /)')

def test_methodcaller_signature(self):
operator = self.module
sig = inspect.signature(operator.methodcaller)
Expand Down Expand Up @@ -703,6 +862,25 @@ def test_itemgetter(self):
self.assertEqual(repr(f2), repr(f))
self.assertEqual(f2(a), f(a))

def test_itemtuplegetter(self):
itemtuplegetter = self.module.itemtuplegetter
a = 'ABCDE'
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
with self.subTest(proto=proto):
f = itemtuplegetter((1, 3))
f2 = self.copy(f, proto)
self.assertEqual(repr(f2), repr(f))
self.assertEqual(f2(a), f(a))
self.assertEqual(f2.defaults, f.defaults)
self.assertEqual(f2.items, f.items)
# multiple gets
f = itemtuplegetter((1, 3), defaults=iter(range(3)))
f2 = self.copy(f, proto)
self.assertEqual(repr(f2), repr(f))
self.assertEqual(f2(a), f(a))
self.assertEqual(f2.defaults, f.defaults)
self.assertEqual(f2.items, f.items)

def test_methodcaller(self):
methodcaller = self.module.methodcaller
class A:
Expand Down
Loading
Loading