<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Make My Python Code Faster
John Parejko, Lia Corrales, Phil Marshall, Andrew Hearin and Your Name Here>
This notebook demonstrates some ways to make your python code go faster.
Step 1
Step1:  Inline Timing
Use %timeit in the notebook, and other commands in functions... Need examples of these!
Step2:  Profiling with cProfile
Step3:  OK - so all the time is being taken by the function "square," as expected.
We need to re-write with the lines separated into functions - which is a better way to code anyway.
Step4:  Much better - you can see the cumulative time spent in each function.
Another useful tool is the line_profiler, from rkern on GitHub.
Step5:  We could also run the line_profiler from the command line... 
Which means the square function needs writing out to a file...
Can we do this from this notebook?
Cythonization
This is something of a last resort
Step6:  Let's write the same exact function in cython syntax
Step8:  What's happening here is that in the pure python code, at each step of these tight nested loops python is doing a bunch of type-checking on i, j and x. All that cdef declaration does is to tell the cython compiler to declare these variables as c-ints, so that the code will not do this type-checking anymore. 
Even if the above pattern is the only one you ever learn in cython, it comes up so, so often that it's worth taking the time to pick up. 
Parallelization
Multiprocessing
John's example
Step9:  The processes print their output words at semi-random times - in general, you have to be careful with what you do when dealing with a simple pool of processors.
If we make our function return a word, rather than just print it, then we can collect the outputs and display them in the correct order.
<ASSISTANT_TASK:>
Python Code:
import numpy as np
x = np.random.randn(1000)
Explanation: Make My Python Code Faster
John Parejko, Lia Corrales, Phil Marshall, Andrew Hearin and Your Name Here>
This notebook demonstrates some ways to make your python code go faster.
Step 1: Profile and improve your code
Because how can you optimize something if you haven't first evaluated it?
Step 2: Parallelize your code
Because you probably own more than one CPU.
Profiling
End of explanation
%timeit np.power(x,2)
%timeit x**2
Explanation: Inline Timing
Use %timeit in the notebook, and other commands in functions... Need examples of these!
End of explanation
import cProfile
import pstats
def square(x):
    for k in range(1000):
        sq = np.power(x,2)
        sq = x**2
        sq = x*x
    return 
log = 'square.profile'
cProfile.run('square(x)',filename=log)
stats = pstats.Stats(log)
stats.strip_dirs()
stats.sort_stats('cumtime').print_stats(20)
Explanation: Profiling with cProfile
End of explanation
def bettersquare(x):
    
    def powersquare(x):
        return np.power(x,2)
    def justsquare(x):
        return x**2
    def selfmultiply(x):
        return x*x
    
    for k in range(1000):
        sq = powersquare(x)
        sq = justsquare(x)
        sq = selfmultiply(x)
    
    return 
log = 'bettersquare.profile'
cProfile.run('bettersquare(x)',filename=log)
stats = pstats.Stats(log)
stats.strip_dirs()
stats.sort_stats('cumtime').print_stats(20)
Explanation: OK - so all the time is being taken by the function "square," as expected.
We need to re-write with the lines separated into functions - which is a better way to code anyway.
End of explanation
!pip install --upgrade line_profiler
Explanation: Much better - you can see the cumulative time spent in each function.
Another useful tool is the line_profiler, from rkern on GitHub.
End of explanation
def my_expensive_loop(n):
    x = 0
    for i in range(int(n)):
        for j in range(int(n)):
            x += i + j
%timeit my_expensive_loop(1000)
Explanation: We could also run the line_profiler from the command line... 
Which means the square function needs writing out to a file...
Can we do this from this notebook?
Cythonization
This is something of a last resort: don't go to cython unless you know it's going to help.
Cython allows us to replace simple lines of math with the equivalent lines of C, while still coding in python.
On the command line, 
cython -a file.pyx
makes file.c, but also file.html. The html file shows you the lines that were unwrapped into C. 
Can we demo this process from this notebook? Hmm.
Compiling cython with IPython Notebook magic functions
Here's a simple example of a double-for loop that cython speeds up tremendously, and a %magic trick for compiling cython within a Notebook. First, our simple slow pure python function:
End of explanation
%load_ext cython
%%cython 
def my_cythonized_loop(int n):
    cdef int i, j, x
    x = 0
    for i in range(int(n)):
        for j in range(int(n)):
            x += i + j
%timeit my_cythonized_loop(1000)
Explanation: Let's write the same exact function in cython syntax:
End of explanation
The multiprocessing joke.
from __future__ import print_function
import multiprocessing
def print_function(word):
    print(word, end=' ')
def tell_the_joke():
    print()
    print('Why did the parallel chicken cross the road?')
    answer = 'To get to the other side.'
    print()
    # Summon a pool to handle some number of processes.
    # Think of N as the number of processors you have?
    
    N = 2
    
    pool = multiprocessing.Pool(processes=N)
    # Prepare a list of function inputs: 
    args = answer.split()
    # Pass the function, and its arguments, to the pool: 
    pool.map(print_function, args)
    
    # Tell the pool members to finish their work.
    pool.close()
    
    # "Ask the pool to report that they are done.
    pool.join()
    print()
    print()
    
    return
tell_the_joke()
Explanation: What's happening here is that in the pure python code, at each step of these tight nested loops python is doing a bunch of type-checking on i, j and x. All that cdef declaration does is to tell the cython compiler to declare these variables as c-ints, so that the code will not do this type-checking anymore. 
Even if the above pattern is the only one you ever learn in cython, it comes up so, so often that it's worth taking the time to pick up. 
Parallelization
Multiprocessing
John's example:
End of explanation
def new_function(word):
    return word+' '
def tell_the_joke_better():
    print()
    print('Why did the parallel chicken cross the road?')
    answer = 'To get to the other side.'
    print()
    # Summon a pool to handle some number of processes.
    # Leave N = blank to have multiprocessing guess!
    # Or measure it yourself:
    
    N = multiprocessing.cpu_count()
    
    pool = multiprocessing.Pool(processes=N)
    # Prepare a list of function inputs: 
    args = answer.split()
    # Pass the function, and its arguments, to the pool: 
    punchline = pool.map(new_function, args)
        
    # Tell the pool members to finish their work.
    pool.close()
    
    # "Ask the pool to report that they are done.
    pool.join()
    # Use the outputs of the function, which are accessible via the map() method:
    print(punchline)
    print()
    print()
    
    return
tell_the_joke_better()
Explanation: The processes print their output words at semi-random times - in general, you have to be careful with what you do when dealing with a simple pool of processors.
If we make our function return a word, rather than just print it, then we can collect the outputs and display them in the correct order.
End of explanation
<END_TASK>