Lists can have any number of elements of different types, but are memory-intensive. But they have the advantage of being mutable.
Tuples can also have any number of elements of different types, but are immutable (you cannot pop or remove elements from them). They are useful when you have a given number of known elements and you only want to iterate on them. They are faster than lists. So, they are more effective than lists when you need to iterate on previously known elements in the fastest and most efficient (from a memory perspective) way.
Sets can be used when you need a data structure that behaves like mathematical sets, and when you need to deduplicate elements from a list or tuple. They implement mathematical set operations like intersection and difference between 2 or more sets.
The yield statement is analogous to a return statement. The difference is that it returns a generator (an analog to an iterator - but one where you can iterate over once, because it is “consumed” during the loop). It can be used to avoid e.g. the memory overhead of a list. E.g.:
def memory_hungry_list_implementation():
my_list = [ ]
for index in enumerate(1000000):
my_list.append(index + 13)
return my_list
for element in memory_hungry_list_implementation():
print(element)
def memory_efficient_generator_implementation():
for index in enumerate(1000000):
yield index + 13
for element in memory_efficient_generator_implementation():
print(element)