2

As far as I know, comprehensions in Python only work with lists, dictionaries, tuples and sets. There is nothing on comprehension in the Python data model.

The syntax for tuples is quite interesting

>>> tuple(i for i in range(3))
(0, 1, 2)

Following the same pattern, I'd like to write a comprehension for my custom class.

>>> MySequence(i for i in range(3))
MySequence(0, 1, 2)
>>> MyMapping{str(i): i for i in range(3)}
MyMapping({'0': 0, '1': 1, '2': 2})

How can I achieve that?

edd313
  • 1,109
  • 7
  • 20
  • 3
    The first one is a generator expression, and that syntax is not directly related to `tuple`. It just so happens that the `tuple` "constructor" knows how to handle iterables, namely generator objects – DeepSpace Mar 09 '22 at 14:50
  • 1
    Usually, a generator expression is in parentheses, like `(i for i in range(3))`, and it's this expression that's passed to `tuple`: `tuple( (i for i in range(3)) )`. When the generator expression is the *only* argument to a function (*any* function, not just `tuple`) the parentheses can be dropped, producing `tuple(i for i in range(3))`. – chepner Mar 09 '22 at 14:52

2 Answers2

3

You are mixing up two things that are not related: class instantiation and (generator-)comprehensions.

tuple(i for i in range(3))

is equivalent to

tuple((i for i in range(3)))

which is equivalent to

generator = (i for i in range(3))
tuple(generator)

The generator-comprehension is evaluated before tuple.__init__ (or __new__) is called. In general, all arguments in Python are evaluated before being passed to a callable.

Any class can accept an iterable (such as generators) for instantiation if you code __init__ accordingly, e.g.

class AcceptIterable:
    def __init__(self, it):
        for x in it:
            print(x)

Demo:

>>> a = AcceptIterable(i for i in range(3))
0
1
2
>>> a = AcceptIterable([i for i in range(3)])
0
1
2
>>> a = AcceptIterable(range(3))
0
1
2
>>> a = AcceptIterable([0, 1, 2])
0
1
2
timgeb
  • 76,762
  • 20
  • 123
  • 145
  • 1
    It's a generator expression, not generator-comprehension. A comprehension is one of the 3 displays that uses generator-expression syntax to define lists, dicts, and sets. – chepner Mar 09 '22 at 14:54
  • 1
    Nah, if `[x for x in y]` is a list comprehension, I'll call `(x for x in y)` a generator-comprehension and you can't stop me. :) – timgeb Mar 09 '22 at 14:56
  • No, but I can mock you for making up names for things that already have names :) – chepner Mar 09 '22 at 14:57
  • @chepner You're technically correct, but colloquially [generator-comprehension](https://stackoverflow.com/questions/364802/how-exactly-does-a-generator-comprehension-work) seems to be well understood. – timgeb Mar 09 '22 at 14:58
  • But seriously, it helps to find documentation for things if you know the correct term to look for. [Section 6.2](https://docs.python.org/3/reference/expressions.html#atoms) of the Python language docs defines both [displays](https://docs.python.org/3/reference/expressions.html#displays-for-lists-sets-and-dictionaries) and [generator expressions](https://docs.python.org/3/reference/expressions.html#generator-expressions). – chepner Mar 09 '22 at 15:04
  • 1
    (The key differences between the two are that 1) a comprehension is a syntactic component of a list, dict, or set display, while a generator expression is a first-class expression itself, and 2) both comprehensions and generator expressions share the `comp_for` syntactic construct, but comprehensions allow assignment expressions where generator expressions do not. Compare `[(x:=y+1) for y in ...]` and the syntactically incorrect `((x:=y+1) for y in ...)`.) – chepner Mar 09 '22 at 15:08
1

I managed to achieve this by subclassing and extending the __init__ method. Meet AbsList, which inherits from list and takes the absolute value of every number when initialising.

class AbsList(list):
    def __init__(self, li):
        super().__init__(abs(l) for l in li)
    def __repr__(self):
        return f"{self.__class__.__name__}({super().__repr__()})"
>>> AbsList(-i for i in range(-3))
AbsList([0, 1, 2])

AbsDict is quite similar.

class AbsDict(dict):
    def __init__(self, di):
        super().__init__({key: abs(value) for key, value in di.items()})
    def __repr__(self):
        return f"{self.__class__.__name__}({super().__repr__()})"
>>> AbsDict({str(i): -i for i in range(3)})
AbsDict({'0': 0, '1': 1, '2': 2})

I would have liked to remove the () brackets but I couldn't manage.

edd313
  • 1,109
  • 7
  • 20