<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 More on data structures
Iterable vs. Iterators
Lists are examples of iterable data structures, which means that you can iterate over the actual objects in these data structures.
Step1:  generators return their contents 'lazily'. This leaves a minimal memory footprint, at the cost of making the generator nonreusable.
Step2:  'range' is something like a generator, but with special properties because of its intended use case (in 'for' loops or similar structures.
Step3:  From the docs (https
Step4:  zips produced iterators from pairs
Step5:  More on Dicts
The dict data structure shows up all over Python.
Step6:  from assignment
Step7:  from iterator
Step8:  In function definitions
Step9:  In function calls
Step10:  This allows, for instance, matplotlibs plot function to accept a huge range of different plotting options, or few to none at all.
<ASSISTANT_TASK:>
Python Code:
# iterating over a list by object
x = ['bob', 'sue', 'mary']
for name in x: 
    print(name.upper() + ' WAS HERE')
# alternatively, you could iterate over position
for i in range(len(x)):
    print(x[i].upper() + ' WAS HERE')
dir(x)  # ignore the __ methods for now
Explanation: More on data structures
Iterable vs. Iterators
Lists are examples of iterable data structures, which means that you can iterate over the actual objects in these data structures.
End of explanation
y = (x*x for x in [1, 2, 3])
type(y)
dir(y)
y.send??
y[5]
next(y)
y.send(1)
next(y)  # run this cell twice - what happens?
Explanation: generators return their contents 'lazily'. This leaves a minimal memory footprint, at the cost of making the generator nonreusable.
End of explanation
z = range(10, 5, -1)
dir(range)
# let's filter that list a little
[x for x in dir(range) if not x.startswith('_')]
z.start
len(z)  # __ function - overloaded operator
Explanation: 'range' is something like a generator, but with special properties because of its intended use case (in 'for' loops or similar structures.
End of explanation
for i in z:
    print(i)
Explanation: From the docs (https://docs.python.org/3/library/stdtypes.html#typesseq-range): The advantage of the range type over a regular list or tuple is that a range object will always take the same (small) amount of memory, no matter the size of the range it represents (as it only stores the start, stop and step values, calculating individual items and subranges as needed).
Range objects implement the collections.abc.Sequence ABC, and provide features such as containment tests, element index lookup, slicing and support for negative indices (see Sequence Types — list, tuple, range):
End of explanation
GPA = zip(['bob', 'sue', 'mary'], [2.3, 4.0, 3.7])
type(GPA)
dir(GPA)
next(GPA)
next(GPA)[1]
Explanation: zips produced iterators from pairs:
End of explanation
dict?
Explanation: More on Dicts
The dict data structure shows up all over Python.
End of explanation
GPA_2 = dict(bob=2.0, sue=3.4, mary=4.0)
Explanation: from assignment:
End of explanation
names = ['bob', 'mary', 'sue', 'lisa']
gpas = [3.2, 4.0, 3.1, 2.8]
GPA_3 = dict(zip(names, gpas))
GPA_3
Explanation: from iterator:
End of explanation
# explicitly named arguments are also positional
# Anything after * in a function is a positional argument - tuple
# Anything after ** is a named argument 
# the latter are unpacked as dicts
def arg_explainer(x, y, *args, **kwargs):
    print('-'*30)
    print('x is %d, even though you didn\'t specify it, because of its position.' % x)
    print('same with y, which is %d.' %y)
    if args:
        print('-'*30)
        print('type(*args) = %s' % type(args))
        print('these are the *args arguments: ')
        for arg in args:
            print(arg)
    else:
        print('-'*30)
        print('no *args today!')
    if kwargs:
        print('-'*30)
        print('type(**kwargs) == %s' % type(kwargs))
        for key in kwargs:
            print(key, kwargs[key])
    else:
        print('-'*30)
        print('no **kwargs today!')
        print('-'*30)
arg_explainer(2, 4, 3, 7, 8, 9, 10, plot=True, sharey=True, rotate=False)
Explanation: In function definitions:
End of explanation
my_kwargs = {'plot': False, 'sharey': True}
arg_explainer(1, 2, **my_kwargs)
Explanation: In function calls:
End of explanation
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
?plt.plot
x = np.linspace(-5, 5, 100)
y1 = np.sin(x)
y2 = np.cos(x)
plt.plot(x, y1)  # all of these arguments are *args
plt.plot(x, y2, color='red', label='just on the cosine, for no reason at all')  # starting w/ color, **kwargs
plt.legend(loc='center');
Explanation: This allows, for instance, matplotlibs plot function to accept a huge range of different plotting options, or few to none at all.
End of explanation
<END_TASK>