<jupyter_start><jupyter_text>Pythonic Thinking---> Author: <NAME>  > Email: <EMAIL>  > Year: 2019  > Version: 1.0---Python is a high-level interepreted programming language. It provides the simplicity needed for quick development, while retaining the power needed for dedicated software development. One of the core tenets of Python is the emphasis on readability, *i.e.* the ability to implement complex functionality with simple and elegant syntax.In order to get the most out of using Python, it is essntial to know how to code in a Pythonic way. This notebook will introduce some useful objects and operators that are central to how Python works.If you are new to Jupyter notebooks note that cells are executed by pressing SHIFT+ENTER (&x21E7;+ &x23ce;). See the Jupyter documentation for more details.---There is plenty to cover in this tutorial but first let's take a deep breath and take in *The Zen of Python.*<jupyter_code>import this<jupyter_output><empty_output><jupyter_text>Contents---1. [Set-Up](1-Set-Up)1. [A Quick Recap](2-A-Quick-Recap)1. [Lambda Functions](3-Lambda-Functions)1. [Unpacking](4-Unpacking)    1. [Basic Unpacking](Basic-Unpacking)    1. [Positional Expansion](Positional-Expansion)    1. [Unpacking with Functions](Unpacking-with-Functions)1. [Dictionaries](5-Dictionaries)    1. [First Example](First-Example)    1. [Dictionary Unpacking](Dictionary-Unpacking)    1. [Keyword Arguments](Keyword-Arguments)    1. [Tips and Tricks](Tips-and-Tricks)1. [List Comprehension](6-List-Comprehension)    1. [Loops](Loops)    1. [List Comprehension](List-Comprehension)    1. [Absolute Magnitude Example](Absolute-Magnitude-Example)    1. [Redshift Example](Redshift-Example)    1. [Conditions](Conditions)    1. [Nested Lists](Nested-Lists)1. [Iterators & Generators](7-Iterators-&-Generators)    1. [Iterators](Iterators)    1. [Built-In Functions](Built-In-Functions)    1. [Generators](Generators)    1. [Generator Expressions](Generator-Expressions)1. [Exercises](8-Exercises)  1 Set-Up---The following cell contains some set-up commands. Be sure to execute this cell before continuing.<jupyter_code># Notebook Set-Up Commands

import math

def print_error(error):
    """ Print Error
    
    Function to print exceptions in red.
    
    Parameters
    ----------
    error : string
        Error message
    
    """
    print('\033[1;31m{}\033[1;m'.format(error))<jupyter_output><empty_output><jupyter_text>2 A Quick Recap---Before getting into the specifics of Pythonic coding, let's take a second to refresh the basics. In order to understand the topics covered in this notebook you will need to know the following: Basic Python object types<jupyter_code># Basic Python objects

myint = 1
print('Object is of type:', type(myint))

myfloat = 1.0
print('Object is of type:', type(myfloat))

mybool = True
print('Object is of type:', type(mybool))

mystring = 'hello'
print('Object is of type:', type(mystring))

mylist = [1, 2, 3]
print('Object is of type:', type(mylist))

mytuple = (1, 2, 3)
print('Object is of type:', type(mytuple))

myset = set([1, 1, 2])
print('Object is of type:', type(myset))<jupyter_output><empty_output><jupyter_text>Standard operators<jupyter_code># Standard operators

print('Addition: 1 + 2 = ', 1 + 2)
print('Subtraction: 1 - 2 = ', 1 - 2)
print('Multiplication: 4 * 2 = ', 4 * 2)
print('Division: 4 / 2 = ', 4 / 2)
print('Floor Division: 4 // 2 = ', 4 // 2)
print('Exponentiation: 4 ** 2 = ', 4 ** 2)<jupyter_output><empty_output><jupyter_text>Simple logic operations<jupyter_code># Simple logic operations

print('True and True = ', True and True)
print('True and False = ', True and False)
print('True or True = ', True or True)
print('True or False = ', True or False)<jupyter_output><empty_output><jupyter_text>Functions<jupyter_code># Functions

def say_hello():
    print('Hello world!')
    
def show_value(value):
    print('My value is {}.'.format(value))
    
say_hello()
show_value(5)<jupyter_output><empty_output><jupyter_text>3 Lambda Functions---To get started, let's take second to remember how functions work in Python. Functions are defined with the keyword `def` followed by the function name, the function arguments in `()` and a colon `:` to end the definition. On the following lines the function behaviour is coded and if the function returns an object this is provided to the keyword `return`.Let's look at an example of a function that calculates the square of an input value.<jupyter_code># Function to calculate the square of the input value
def square_func(x):
    
    return x ** 2

square_func(3)<jupyter_output><empty_output><jupyter_text>Python provides a useful shorthand for writing simple functions of this type using the keyword `lambda`.Let's look at an example to perform the same squaring function.<jupyter_code># Lambda function to calculate the square of the input value
square_lambda = lambda x: x ** 2

square_lambda(3)<jupyter_output><empty_output><jupyter_text>The syntax is `lambda` followed by the function arguments, a colon to end the definition, and finally the function behaviour is automatically returned. Like all Python functions, lambda functions are objects (we will come back to this idea) and since they are not named, they need to be assigned to an object in order to be used.Here is another example using multiple arguments.<jupyter_code># Lambda function to calculate the sum of two input values
add = lambda x, y: x + y

add(1, 2)<jupyter_output><empty_output><jupyter_text>While lambda functions are extremely useful, best practices dictate that they should be used sparingly.    4 Unpacking---A fundamental first step on your journey of Pythonic coding is understanding how handle array-like objects (*i.e.* lists and tuples). Basic UnpackingAs you are no doubt aware lists and tuples can be indexed by passing an element number inside `[]`...<jupyter_code># Create a tuple
mytuple = (1, 2, 3)

# Get index 1 from the tuple
print('mytuple[1] =', mytuple[1])<jupyter_output><empty_output><jupyter_text>... however, it is also possible to *unpack* these objects.<jupyter_code># Unpack a tuple
val1, val2, val3 = (1, 2, 3)

print('val1 = {} ; val2 = {} ; val3 = {}'.format(val1, val2, val3))<jupyter_output><empty_output><jupyter_text>Notice that when three objects are assigned to the tuple each value is unpacked automatically, but if we attempt to unpack three values into two objects we raise an exception.<jupyter_code># Try to unpack values into two objects
try:
    val1, val2 = (1, 2, 3)
except Exception as error:
    print_error(error)<jupyter_output><empty_output><jupyter_text>Positional ExpansionWe can bypass the problem using the *splat* (`*`) operator. In addition to be used as the standard operator for multiplication, `*` also has special unpacking features.<jupyter_code># Print mytuple
print('mytuple =', mytuple)

# Print mytuple after unpacking
print('unpacked mytuple =', *mytuple)<jupyter_output><empty_output><jupyter_text>Using this operator we can now unpack our three-element tuple into just two objects.<jupyter_code># Unpack a tuple
val1, *val2 = mytuple

print('val1 = {} ; val2 = {}'.format(val1, val2))<jupyter_output><empty_output><jupyter_text>We can see that the first value was unpacked and the remaining values were packed into a list.> **Puzzle 1:** Guess the value of `val2` in the following example.<jupyter_code># Create a list
mylist = [7, 2, 8, 2, 6]

# Unpack the list
val1, *val2, val3 = mylist

# Uncomment to see the answer
# print(val2)<jupyter_output><empty_output><jupyter_text>We can also recursively unpack array-like objects.> **Puzzle 2:** Guess the value of `b` in the following cell.<jupyter_code># Create a list of tuples
list_of_tuples = [(1, 2, 3), (4, 5, 6), (7, 8, 9)]

# Unpack the list
*A, B = list_of_tuples

# Unpack one of the tuples
*a, b = A[1]

# Uncomment to see the answer
# print('b =', b)<jupyter_output><empty_output><jupyter_text>> Further Reading> - Understanding the Asterisk of Python  Unpacking with FunctionsUnpacking can be particularly useful for passing arguments to functions. *e.g.* if you want to pass a single object to a function that expects multiple arguments.<jupyter_code># Define a function that takes multiple arguments
def myfunc(a, b, c):
    
    return a + b - c

# Set a tuple of values
myvalues = (4, 5, 3)

# Unpack the values into the function
print('The result of myfunc is {}.'.format(myfunc(*myvalues)))<jupyter_output><empty_output><jupyter_text>Alternatively, you could pass multiple values to a function that does not know how many arguments to expect.<jupyter_code># Define a function that has an unknown number of arguments
def count_agrs(*args):
        
    return len(args)

# Pass multiple values to the function
print('count_agrs received {} arguments.'.format(count_agrs(1, 2, 3)))<jupyter_output><empty_output><jupyter_text>5 Dictionaries---Python provides many different ways of storing multiple values in a single object (*e.g.* lists, tuples, sets, *etc*). One of the most useful, and certainly one of the most important, are *dictionaries*. Dictionaries are designated with `{}` and are comprised of two main components: *keys* and *values*. The benefit that dictionaries provide with respect to simpler objects like lists is the ability to label values. This makes it a lot easier to store a large number of values in a single object without losing track of what they are. First ExampleLet's look at a concrete example. Imagine we want to keep track of the colours associated to the Teenage Mutant Ninja Turtles.We could start by defining a unique object for each turtle.<jupyter_code># Unique objects for turtle colours
Leonardo = 'blue'
Raphael = 'red'
Donatello = 'purple'
Michelangelo = 'orange'

print('Leonardo wears {}.'.format(Leonardo))<jupyter_output><empty_output><jupyter_text>It might be nicer instead to define a single object.<jupyter_code># List of turtle colours
turtles = ['blue', 'red', 'purple', 'orange']

print('Raphael wears {}.'.format(turtles[1]))<jupyter_output><empty_output><jupyter_text>This, however, assume you will remember the order and names of the turtles. A dictionary makes it possible to retain both the simplicty of a list, but the details of the single objects.To define a dictionary object we need to specify a series of keys followed by a colon (`:`) and then the corresponding values, all comma separated and within `{}`.<jupyter_code># Dictionary of turtle names and colours
turtles = {'Leo': 'blue', 'Raph': 'red', 'Donny': 'purple', 'Mickey': 'orange'}<jupyter_output><empty_output><jupyter_text>We can look at the pieces that make up the dictionary by looking at the `keys`, `values` and `items`.<jupyter_code># Show dictionary components
print('dict:', turtles)
print('keys:', turtles.keys())
print('values:', turtles.values())
print('items:', turtles.items())<jupyter_output><empty_output><jupyter_text>Now we can use the keys (*i.e.* the turtles' names) to look up the values (*i.e.* the corresponding colours). *e.g.* To look up what colour Donatello wears we simply need to pass the key `'Donny'` to the dictionary `turtles` as follows.```pythonturtles['Donny']```<jupyter_code># Look up dictionary value|
print('Donatello wears {}.'.format(turtles['Donny']))<jupyter_output><empty_output><jupyter_text>> Note that dictionary look up in Python is highly optimised, but it only works one way (*i.e.* keys &rarr; values).It is possible to add entries to a dictionary by specifying a new key and assigning a corresponding value.<jupyter_code># Add dictionary entry
turtles['Splinter'] = 'wine'
print(turtles)<jupyter_output><empty_output><jupyter_text>It is also possible to modify existing entry values by takinging an existing key and assigning a new value.<jupyter_code># Modify dictionary value
turtles['Splinter'] = 'yellow'
print(turtles)<jupyter_output><empty_output><jupyter_text>You will raise an exception if you try to index a dictionary or look up a key for a entry that doesn't exist.<jupyter_code>try:
    turtles['Rocksteady']
except Exception as error:
    print_error('KeyError: {}'.format(error))<jupyter_output><empty_output><jupyter_text>Dictionary UnpackingSimilarly to array-like objects, dictionaries can be unpacked.<jupyter_code># Create a dictionary
mydict = {'a' : 1, 'b' : 2, 'c' : 3}

# Unpack the dictionary
val1, val2, val3 = mydict

print('val1 = {} ; val2 = {} ; val3 = {}'.format(val1, val2, val3))<jupyter_output><empty_output><jupyter_text>Note that by default unpacking is performed over the keys. To unpack the values you have to be more explicit.<jupyter_code># Unpack the dictionary values
val1, val2, val3 = mydict.values()

print('val1 = {} ; val2 = {} ; val3 = {}'.format(val1, val2, val3))<jupyter_output><empty_output><jupyter_text>> **Puzzle 3:** What values will you get when you unpack the dictionary items?<jupyter_code># Unpack the dictionary values
val1, val2, val3 = mydict.items()

# Uncomment to see the answer
# print('val1 = {} ; val2 = {} ; val3 = {}'.format(val1, val2, val3))<jupyter_output><empty_output><jupyter_text>Keyword ArgumentsIn addition to standard position arguments, Python functions accept *keyword arguments* (or simply *kwagrs*), *i.e.* a dictionary of keys and values. kwargs can be used to define default values in functions.<jupyter_code># Define a function with defaults
def myfunc(a=1, b=2, c=2):
    
    return a + b * 3

print('myfunc =', myfunc())<jupyter_output><empty_output><jupyter_text>They can also be used to pass arguments in any given order.<jupyter_code>print('myfunc =', myfunc(c=5, a=3, b=7))<jupyter_output><empty_output><jupyter_text>There is also a special keyword expansion operator `**` thay behaves similarly to the splat operator `*` for use in functions.<jupyter_code># Create dictionary of values
mydict = {'a': 3, 'b': 4, 'c': 5}

# Unpack the dictionary kwargs into the function
print('myfunc =', myfunc(**mydict))<jupyter_output><empty_output><jupyter_text>Finally, you can define functions for which an idefinite number of kwargs can be passed.<jupyter_code># Define a function with optional kwargs
def subtract(a, b, **kwargs):
    
    res = a - b
    
    if 'return_abs' in kwargs and kwargs['return_abs']:
        res = abs(res)
        
    if 'add_value' in kwargs:
        res += kwargs['add_value']

    return res
            
print('1 - 2 = {}'.format(subtract(1, 2)))
print('|1 - 2| = {}'.format(subtract(1, 2, return_abs=True)))
print('1 - 2 + 3 = {}'.format(subtract(1, 2, add_value=3, dummy_arg=None)))<jupyter_output><empty_output><jupyter_text>Tips and TricksThere are plenty of useful tricks that make dictionary handling a lot easier.For example, you can convert a list of tuples to a dictionay.<jupyter_code># Tuples of film names and release dates
film1 = ('alien', '1979')
film2 = ('the shining', '1980')
film3 = ('the evil dead', '1981')
film4 = ('blade runner', '1982')

# List of films
films = [film1, film2, film3, film4]
print('List: ', films)
print()

# Dictionary of films
films = dict(films)
print('Dict: ', films)
print()
print('Alien came out in {}.'.format(films['alien']))<jupyter_output><empty_output><jupyter_text>You can concatinate two dictionaries in a single line.<jupyter_code># Make another dictionary
more_films = dict([('WarGames', '1983'), ('Amadeus', '1984')])

# Concatinate the dictionaries
films_80s = {**films, **more_films}
print('Concatination: ', films_80s)<jupyter_output><empty_output><jupyter_text>You can clear the contents of an existing dictionary using the `clear` method.<jupyter_code># Clear the films dictionary
films.clear()
print('Clear: ', films)<jupyter_output><empty_output><jupyter_text>Finally, you can add, look up and remove dicttionary entries using the `update`, `get` and `pop` methods respectively.<jupyter_code># Add a value to the dictionary
films.update({'Back to the Future': '1985'})
print('Update:', films)
print()

# Get the value for the key
print('Get: Back to the Future came out in {}.'.format(films.get('Back to the Future')))
print()

# Pop the entry out of the dictionary
films.pop('Back to the Future')
print('Pop: ', films)<jupyter_output><empty_output><jupyter_text>> Note that the `get` method will not raise an error if the key is not found, instead it will simply return `None`. > Further Reading  > - Python dicts  6 List Comprehension---This section introduces the concept of *list comprehension*, a tool that you will likely have seen implemented but perhaps not understood. The most sensible way to begin understanding this concept is by looking at basic loops. LoopsLoops are a core component of any programming language and Python is certainly no different.<jupyter_code># Simple loop
for i in (1, 2, 3):
    print('i =', i)<jupyter_output><empty_output><jupyter_text>Here we can see the use of a basic `for` statement using the variable `i`, which iterates through the values in a tuple.That being said, Python does offer an extremely powerful way to generate lists from loops using a process called list comprehension. In lower level lanaguages (*e.g.* C, Fortran, *etc.*) arrays are usually built by declaring an empty object of a given size and type and then iteratively filling this array. It is possible to do something similar in Python, in fact, this is often done by people who come to Python from one of these languages.<jupyter_code># Create a list of size 3
my_int_list = [None, None, None]

# Run a loop
for i in (1, 2, 3):
    # Fill the list values
    my_int_list[i - 1] = i
    
print('my_int_list =', my_int_list)<jupyter_output><empty_output><jupyter_text>While this works, it goes against most of what Python aims to achieve. Using Python list properties we can improve a little bit.<jupyter_code># Create an empty list
my_int_list = []

# Run a loop
for i in range(1, 4):
    # Add list values
    my_int_list.append(i)
    
print('my_int_list =', my_int_list)<jupyter_output><empty_output><jupyter_text>> Note that `range` is a built-in function that produces a list-like object of values within a given range.This approach may be appropriate in certain situations, particularly if we plan to *break* the loop when a condition is met. For this particular example, however, there is a much easier way to generate the desired object. List ComprehensionTo perform list comprehension we need the following structure: `[final_expression for_loop_and_conditions]`. For example, to generate the same list object as the previous cell we can do this:<jupyter_code># Create an object through list comprehension
my_int_list = [i for i in range(1, 4)]

print('my_int_list =', my_int_list)<jupyter_output><empty_output><jupyter_text>In this example the first `i` is the final expression we want (*i.e.* simply the value), this is followed by a for loop that is identical to that in the loop example.Using list comprehension it is possible to create a list object in one line! This may seem trivial, but it is actually a very useful and highly optimised tool in Python. Absolute Magnitude ExampleImagine you need to calculate the absolute magnitude $$M = m - 5\log_{10}(d_{\textrm{pc}}) + 5$$of various stars:|Star|$m_v$|$d_{\textrm{pc}}$||----|-----|-----------------||Alpha Centauri|-0.3|1.3||Canopus|-0.72|30.1||Rigel|0.14|276.1||Deneb|1.26|490.8|One way would be be to store the information about the stars in a dictionary (as we just learned about them)<jupyter_code># Dictionary of star properties
stars = dict([('alpha centauri', {'mv': -0.3, 'dpc': 1.3}), 
              ('canopus', {'mv': -0.72, 'dpc': 30.1}), 
              ('rigel', {'mv': 0.14, 'dpc': 276.1}), 
              ('deneb', {'mv': 1.26, 'dpc': 490.8})])

print('The apparent magnitude of Rigel is {}.'.format(stars['rigel']['mv']))<jupyter_output><empty_output><jupyter_text>and to loop through the stars.<jupyter_code># Create an empty list
abs_mags = []

# Loop through the stars
for star in stars.values():
    # Calculate absolute magnitude
    mag = round(star['mv'] - 5 * math.log10(star['dpc']) + 5, 2)
    # Add value to list
    abs_mags.append(mag)
    
print('Absolute magnitude values =', abs_mags)<jupyter_output><empty_output><jupyter_text>A more Pythonic approach, however, would be to define a reusable function and generate the final object via list comprehension.<jupyter_code># Define a function to calculate absolute magnitude
def abs_mag(mv, dpc):
    
    return round(mv - 5 * math.log10(dpc) + 5, 2)

# Produce a list of absolute magnitude values
abs_mags = [abs_mag(star['mv'], star['dpc']) for star in stars.values()]

print('Absolute magnitude values =', abs_mags)<jupyter_output><empty_output><jupyter_text>This way, we not only have a function that can be used for future calculations, but we also generate our object in one line. Redshift ExampleLet's look at another example. Imagine you want to calculate the redshift of an object given the emitted and observed wavelenths.$$z = \frac{\lambda_{\textrm{obs}}-\lambda_{\textrm{emit}}}{\lambda_{\textrm{emit}}}$$|$\lambda_{\textrm{emit}}$|$\lambda_{\textrm{obs}}$||-------------------------|------------------------||450|679||348|956||579|264|We can approach this in a very similar way to the previous example, except in this case let's try without using a dictionary.<jupyter_code># Create lists of wavelength values
lambda_emit = [450., 348., 579.]
lambda_obs = [679., 956., 264.]

# Define a function to calculate redshift
def redshift(emit, obs):
    
    return round((obs - emit) / emit, 3)

# Generate redshift values via list comprehension
z = [redshift(e, o) for e, o in zip(lambda_emit, lambda_obs)]

print('Redshifts =', z)<jupyter_output><empty_output><jupyter_text>> **Puzzle 4:** Note the use of the `zip` function. Any guess for what it does?<jupyter_code># Uncomment to see the answer
# print('The result of zip is:', list(zip(lambda_emit, lambda_obs)))<jupyter_output><empty_output><jupyter_text>ConditionsIt is possible to embed a complex series of conditions (*e.g.* `if` statements) into list comprehension.<jupyter_code># A list of names
names = ['paul', 'jessica', 'leto', 'gunrey', 'duncan', 'piter']

# A new list of names satisfying various conditions
new_names = [name for name in names if 'e' in name and name[0] < 'k']

print('Names containing the letter "e" and starting with a letter lower than "k":', new_names)<jupyter_output><empty_output><jupyter_text>Nested ListsIt is also possible to flatten nested lists using unpacking and conditions.<jupyter_code># Nested list
cats = [['lion-o', 'cheetara', 'tygra', 'panthro'], ['wilykit', 'wilycat'], ['snarf'], ['mumm-ra']]

# Flatten list with condition
cats = [cat for group in cats for cat in group if cat!='mumm-ra']

print('Thundercats:', cats)<jupyter_output><empty_output><jupyter_text>> Further Reading  > - List Comprehension in Python  7 Iterators & Generators---Some very useful tools in Python, particularly when it comes to memory management, are iterators and generators. IteratorsThe `iter` function can be used to convert objects to iterators, such as lists...<jupyter_code># Create list iterator
myiter = iter([1, 2, 3, 4])

print('myiter =', myiter)<jupyter_output><empty_output><jupyter_text>... or tuples.<jupyter_code># Create tuple iterator
myiter = iter((1, 2, 3, 4))

print('myiter =', myiter)<jupyter_output><empty_output><jupyter_text>> **Puzzle 5:** How would you go about converting an iterator into a list?<jupyter_code># Uncomment and edit the following lines appropriately
# Convert iterator into a list
# mylist = iter((1, 2, 3, 4))
# 
# print('mylist =', mylist)<jupyter_output><empty_output><jupyter_text>It is not possible to index an iterator.<jupyter_code># Try to index iterator
try:
    myiter[0]
except Exception as error:
    print_error(error)<jupyter_output><empty_output><jupyter_text>Instead, iterator values are accessed one at a time using the `next` function.<jupyter_code>print('next(myiter) =', next(myiter))<jupyter_output><empty_output><jupyter_text>> Try re-running the previous cell multiple times to see what happens. Built-In FunctionsMost built-in functions that work on lists (*e.g.* `sum`, `min`, `max`, *etc.*) will also work directly on iterators.<jupyter_code># Create a list
mylist = [1, 2, 3, 4, 5]

# Create an iterator
myiter = iter(mylist)

print('sum(mylist) =', sum(mylist))
print('sum(myiter) =', sum(myiter))<jupyter_output><empty_output><jupyter_text>But note that, unlike a list, once an interator has been fully run it remains empty.<jupyter_code>print('sum(mylist) =', sum(mylist))
print('sum(myiter) =', sum(myiter))<jupyter_output><empty_output><jupyter_text>In addtion, many built-in function return iterator-like objects by default (*e.g.* `zip`).<jupyter_code># Create zip object
myzip = zip([1, 2], [3, 4])

print('Next:', next(myzip))
print('Next:', next(myzip))<jupyter_output><empty_output><jupyter_text>GeneratorsGenerators can be thought of as functions that behave like iterators. Imagine you want a function that can provide the square of a list of values. One approach would be to write a function that takes all of the inputs and returns a list of the corresponding squared values.<jupyter_code># Function to calculate square of input values
def square(values):
    
    return [value ** 2 for value in values]

print('Squared values =', square([1, 2, 3, 4, 5]))<jupyter_output><empty_output><jupyter_text>So, while this certainly works, we are storing all of the outputs in memory. In some situations we may only wish to calculate the output value when it's needed. To do so we can define a *generator* using a `yield` statement.<jupyter_code># Define a generator function to calculate square of input values
def square(values):
    
    for value in values:
        
        yield value ** 2
        
# Create a generator object
mygen = square([1, 2, 3, 4, 5])

# Run the generator
print('next(mygen) =', next(mygen))
print('next(mygen) =', next(mygen))
print('next(mygen) =', next(mygen))
print('next(mygen) =', next(mygen))
print('next(mygen) =', next(mygen))<jupyter_output><empty_output><jupyter_text>In this case the function is only evaluated when the `next` function is called.An important feature of generators is that mutiple `yield` statements can be included in a single function.<jupyter_code># Define a generator function with multiple yields
def peasant():
    
    yield "Ready to work."
    yield "Job's done."
    yield "Alright."
    yield "You're the king? Well, I didn't vote for you."

# Create a generator object
p = peasant()

# Run the generator
print(next(p))
print(next(p))
print(next(p))
print(next(p))<jupyter_output><empty_output><jupyter_text>Generator ExpressionsSimilarly to list comprehension, it is possible to produce generator objects in one line using `()`.<jupyter_code># Define a generator expression to square values from 1 to 5
mygen = (value ** 2 for value in range(1, 6))

print('next(mygen) =', next(mygen))
print('next(mygen) =', next(mygen))
print('next(mygen) =', next(mygen))
print('next(mygen) =', next(mygen))
print('next(mygen) =', next(mygen))
# Calculate the sum of cubes using a generator expression
soc = sum((value ** 3 for value in range(10)))

print('The sum of cubes from 0 to 9 is {}.'.format(soc))<jupyter_output><empty_output><jupyter_text>> Further Reading> - Python Practice Book: Iterators  8 Exercises---1. Create a list of your favourite superheros and another list of their secret identities.    1. Convert your two lists into a dictionary. (Can you do it in one line?)    1. Remove one of your heroes and add a villain to your dictionary.    1. Add a character that has multiple identities to your dictionary. (What kind of object should this be?)    1. Demonstrate that you can look up one of your character's identities.    | Superhero   | Identity                    ||:-----------:|:---------------------------:|| Iron Man    | <NAME>                  || The Thing   | <NAME>                   || Storm       | <NAME>                || Spider-Man  | <NAME>, <NAME> |<jupyter_code># Add your solution here<jupyter_output><empty_output><jupyter_text>2. Use Hubble's law ($v=H_{0}\,D$) to calculate the distance (in Mpc) of the galaxies in the following table.|Galaxy|Velocity (km/s)||------|---------------||NGC 123|1320||NGC 2342|5690||NGC 4442|8200|Remember that $H_0 \approx  70$ km/s/Mpc.<jupyter_code># Add your solution here<jupyter_output><empty_output><jupyter_text>3. Flatten the following list using list comprehension.```pythonmylist = [[[1, 2], [3, 4, 5]], [[6], [7, 8]]]```<jupyter_code># Add your solution here<jupyter_output><empty_output><jupyter_text>4. Write a generator function that can be used to calculate the Fibonacci sequence.<jupyter_code># Add your solution here<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.11739523875817903, 'fraction_numerical': 0.01597521926771544, 'mean_word_length': 4.984794834409498, 'pattern_counts': {'":': 1, '<': 265, '<?xml version=': 0, '>': 285, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '64', 'dir': 'jupyter-structured-clean-dedup', 'id': '530661', 'license': 'MIT', 'n_tokens_mistral': 9200, 'n_tokens_neox': 8743, 'n_words': 3359, 'path': 'Pythonic.ipynb', 'repo_name': 'Euclid-Python/python_and_oop', 'size': '28780'}