2

I understand I can create a tuple by these two methods:

tuple([1, 2, 3])
(1, 2, 3)

I'm trying to understand what exactly is happening here:

tuple( (x, x*x) for x in [1, 2, 3] )

the tuple constructor seems to want a list, but it doesn't seem like the following would produce a list

(x, x*x) for x in [1, 2, 3]

Also I'm wondering why this doesn't work:

( (x, x*x) for x in [1, 2, 3] )

With dictionaries it seems I can use either:

my_dict = dict()
my_dict = {}

but now I'm wondering if, like tuples, there's a difference. Is ():tuple() a different :: relationship than {}:dict()?

Remy J
  • 709
  • 1
  • 7
  • 18
Maria Scott
  • 21
  • 1
  • 2
  • 2
    check https://stackoverflow.com/questions/16940293/why-is-there-no-tuple-comprehension-in-python for some info – buran Dec 22 '18 at 15:27
  • 2
    You got it wrong, Parenthesis have **nothing** to do with tuples. It's the **commas** that create tuples. `1,2,3` is the same as `(1,2,3)`. the outer parenthesis are just for grouping. The only case where you need parenthesis is the empty tuple `()`. The `tuple` constructor takes any iterable not just lists. – Bakuriu Dec 22 '18 at 15:29
  • 1
    And you are feeding a generator expression which is iterable to the tuple() constructor. if you `print( ((x, x*x) for x in [1, 2, 3]) )` - you'll get somiething along ` at 0x7f847eccc0f8>` - putting this as iterable into `tuple(iterable)`creates the tuple by iterating the generator – Patrick Artner Dec 22 '18 at 15:32
  • 1
    There's no tuple comprehension, ((x, x*x) for x in [1, 2, 3]) this code is a generator comprehension. generator is a iterable object, i.e. range(5) is a generator. – Lester_wu Dec 22 '18 at 15:35
  • Just to add to @Bakuriu comment. In a empty tuple the parentheses are the essential elements however this is not the case for one-element tuples and multiple-element tuples which can be defined like `1,` and `1,2,3`. – Remy J Dec 22 '18 at 15:43

1 Answers1

6

Let's break down what's happening here. You're not wrong that the tuple constructor seems to want a list, but it would be more precise to say that the tuple constructor seems to want a list-like object. (to be specific, any iterable works)

This is a philosophy called as Duck Typing.

The saying goes as follows:

If it walks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.

So then, a list works

a = [1, 2, 3, 2]
tuple(a) #Output: (1, 2, 3, 2)

but so does a different iterable, such as a set

tuple(set(a)) #Output: (1, 2, 3)

So, tuple does not care whether the object it receives is a list, just that it should be able to iterate and get values from the object.

Now, The second part of the magic comes from something called list comprehension/generator expressions. They are iterables you can create which make it easy to write 1 liners that create a list or a generator expression respectively. More on the generator later, for now, it is sufficient to see how a list comprehension works.

A simple example of list comprehension

[a for a in range(4)] #Output: [0, 1, 2, 3]
[a*a for a in range(4)] #output: [0, 1, 4, 9]

We see that they produce lists. So, can we feed them to a tuple constructor? Why not!

tuple([a for a in range(4)]) #Output: (0, 1, 2, 3)
tuple([a*a for a in range(4)]) #output: (0, 1, 4, 9)

Now, what about using the same expression but wrapping it in curved brackets instead?

(a for a in range(4)) #Output: <generator object <genexpr> at 0x000000FA4FDBE728>

You just created a generator expression

They are essentially memory efficient on-demand iterables. (to be jargon specific, they have a yield and next , and only yield values as needed). Let's see it in action.

my_generator = (a for a in range(4)) #generator created. 
next(my_generator) #Outputs 0
next(my_generator) #Outputs 1
next(my_generator) #outputs 2
next(my_generator) #outputs 3
next(my_generator) #Raises StopIteration Error. The generator is exhausted.

We can see that we receive the same values as with our list comprehension. So, does a tuple accept something like a generator? Well, duck typing to the rescue! Absolutely!

tuple((a for a in range(4))) #Output: (0, 1, 2, 3)

Do i need the redundant parenthesis? Nope!

tuple(a for a in range(4)) #Output: (0, 1, 2, 3)
tuple(a*a for a in range(4)) #Output: (0, 1, 4, 9)

Now, what does this produce? (x, x*x) for x in [1, 2, 3]

Well, it is an expression, but let's get an idea of how it would look in a list comprehension instead

[(x, x*x) for x in [1, 2, 3]] #Output: [(1, 1), (2, 4), (3, 9)]

Ah, its a list of tuples? Can a generator do the same?

my_generator = ((x, x*x) for x in [1, 2, 3]) #<generator object <genexpr> at 0x000000FA4FD2DCA8>
next(my_generator) #Output: (1, 1)
next(my_generator) #Output: (2, 4)
next(my_generator) #Output: (3, 9)
next(my_generator) #Raises StopIteration

Yep, looks good. So its a generator, but its an iterable. Behaves like a duck anyways, doesn't it? So, the tuple constructor should work just fine!

tuple((x, x*x) for x in [1, 2, 3]) #Output: ((1, 1), (2, 4), (3, 9))

So, that wraps up everything. The parenthesis do not imply a tuple all the time, () are not reserved for tuples. We see here that they can be used for generator expressions as well! Similarly, the {} do not have to be tied to dictionaries always, something similar to list comprehension actually exists for dictionaries too! (Known as dict comprehension)

I highly recommend going through the links for a more thorough explanation of the individual pieces that are working together here. Hope this helps!

Paritosh Singh
  • 6,034
  • 2
  • 14
  • 33
  • 1
    One of the best answers I ever read on Stack Overflow, thanks for taking time writing this. – Mossab Sep 20 '22 at 18:18