<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Today we went through some basic tools to inspect Python scripts for errors and performance bottlenecks.
Debugging
Python DeBugger (PDB)
The standard Python tool for interactive debugging is pdb, the Python debugger. 
This debugger lets the user step through the code line by line in order to see what might be causing a more difficult error.
The IPython-enhanced version of this is ipdb, the IPython debugger.
Using in the command line
We went through this excellent pdb tutorial
An even more convenient version is pdbpp
Using PDB in IPython or Jupyter
First, we import a function to insert breakpoints
Step1:  Define a simple function, insert a breakpoint
Step2:  (Uncomment it to run with debugger)
In IPython, perhaps the most convenient interface to debugging is the %debug magic command. If you call it after hitting an exception, it will automatically open an interactive debugging prompt at the point of the exception. The ipdb prompt lets you explore the current state of the stack, explore the available variables, and even run Python commands!
Example
Step3:  If you'd like the debugger to launch automatically whenever an exception is raised, you can use the %pdb magic function to turn on this automatic behavior
Step4:  Note that because this operation is so fast, %timeit automatically does a large number of repetitions. For slower commands, %timeit will automatically adjust and perform fewer repetitions.
Profiling
Once you have your code working, it can be useful to dig into its efficiency a bit. Sometimes it's useful to check the execution time of a given command or set of commands; other times it's useful to dig into a multiline process and determine where the bottleneck lies in some complicated series of operations. 
You can either use the built in profiler
Step5:  We can now load a line_profiler extension (after we install line_profiler package using pip or conda).
Step6:  And call the function using the special profiler magic
Step7:  Memory profiling
In the similar manner, we can even do memory profiling (once the memory_profiler package is installed).
Step8:  Disassembling and inspecting Python code
Tracing CPython code execution
Step9:  Let's disassemble it with the dis module
Step10:  What's BINARY_ADD?
We crack open CPython's source code and take a look inside Python/ceval.c
Step11:  Summary
CPython slowdown 1
<ASSISTANT_TASK:>
Python Code:
from IPython.core.debugger import set_trace
Explanation: Today we went through some basic tools to inspect Python scripts for errors and performance bottlenecks.
Debugging
Python DeBugger (PDB)
The standard Python tool for interactive debugging is pdb, the Python debugger. 
This debugger lets the user step through the code line by line in order to see what might be causing a more difficult error.
The IPython-enhanced version of this is ipdb, the IPython debugger.
Using in the command line
We went through this excellent pdb tutorial
An even more convenient version is pdbpp
Using PDB in IPython or Jupyter
First, we import a function to insert breakpoints:
End of explanation
def fun(foo, bar):
    set_trace()
    return foo + bar
# fun(1, 2)
Explanation: Define a simple function, insert a breakpoint:
End of explanation
def func1(a, b):
    return a / b
def func2(x):
    a = x
    b = x - 1
    return func1(a, b)
func2(1)
# %debug
Explanation: (Uncomment it to run with debugger)
In IPython, perhaps the most convenient interface to debugging is the %debug magic command. If you call it after hitting an exception, it will automatically open an interactive debugging prompt at the point of the exception. The ipdb prompt lets you explore the current state of the stack, explore the available variables, and even run Python commands!
Example: 2 functions, one of which calls another
End of explanation
%timeit sum(range(100))
Explanation: If you'd like the debugger to launch automatically whenever an exception is raised, you can use the %pdb magic function to turn on this automatic behavior:
python
%pdb on
...some code to debug...
%pdb off
Timing and Profiling
Simple timing
In IPython and Jupyter you can use special magics to measure how much time a snippet of code takes to run.
End of explanation
def sum_of_lists(N):
    total = 0
    for i in range(5):
        L = [j ^ (j >> i) for j in range(N)]
        total += sum(L)
    return total
Explanation: Note that because this operation is so fast, %timeit automatically does a large number of repetitions. For slower commands, %timeit will automatically adjust and perform fewer repetitions.
Profiling
Once you have your code working, it can be useful to dig into its efficiency a bit. Sometimes it's useful to check the execution time of a given command or set of commands; other times it's useful to dig into a multiline process and determine where the bottleneck lies in some complicated series of operations. 
You can either use the built in profiler:
```python
import cProfile
cp = cProfile.Profile()
cp.enable()
some code to profile
cp.disable()
cp.print_stats()
```
Line-by-line profiling
Let's define a function with a big number of for-loop iterations.
End of explanation
%load_ext line_profiler
Explanation: We can now load a line_profiler extension (after we install line_profiler package using pip or conda).
End of explanation
%lprun -f sum_of_lists sum_of_lists(5000)
Explanation: And call the function using the special profiler magic:
End of explanation
%load_ext memory_profiler
%memit sum_of_lists(100000)
Explanation: Memory profiling
In the similar manner, we can even do memory profiling (once the memory_profiler package is installed).
End of explanation
def foo(a, b):
    return a + b
Explanation: Disassembling and inspecting Python code
Tracing CPython code execution
End of explanation
import dis
dis.dis(foo)
Explanation: Let's disassemble it with the dis module:
End of explanation
%timeit foo(1, 2)
%timeit foo('a', 'b')
Explanation: What's BINARY_ADD?
We crack open CPython's source code and take a look inside Python/ceval.c:
c
/* Python/ceval.c */
TARGET(BINARY_ADD) {
    PyObject *right = POP();
    PyObject *left = TOP();
    PyObject *sum;
    /* NOTE(haypo): Please don't try to micro-optimize int+int on
       CPython using bytecode, it is simply worthless.
       See http://bugs.python.org/issue21955 and
       http://bugs.python.org/issue10044 for the discussion. In short,
       no patch shown any impact on a realistic benchmark, only a minor
       speedup on microbenchmarks. */
    if (PyUnicode_CheckExact(left) &amp;&amp;
            PyUnicode_CheckExact(right)) {
        sum = unicode_concatenate(left, right, f, next_instr);
        /* unicode_concatenate consumed the ref to left */
    }
    else {
        sum = PyNumber_Add(left, right); // &lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;
        Py_DECREF(left);
    }
    Py_DECREF(right);
    SET_TOP(sum);
    if (sum == NULL)
        goto error;
    DISPATCH();
}
What's PyNumber_Add(left, right)?
c
/* Objects/abstract.c */
PyObject *
PyNumber_Add(PyObject *v, PyObject *w)
{
    PyObject *result = binary_op1(v, w, NB_SLOT(nb_add)); // &lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;
    if (result == Py_NotImplemented) {
        PySequenceMethods *m = v-&gt;ob_type-&gt;tp_as_sequence;
        Py_DECREF(result);
        if (m &amp;&amp; m-&gt;sq_concat) {
            return (*m-&gt;sq_concat)(v, w);
        }
        result = binop_type_error(v, w, "+");
    }
    return result;
}
What's binary_op1()?
```c
static PyObject *
binary_op1(PyObject v, PyObject w, const int op_slot)
{
    PyObject *x;
    binaryfunc slotv = NULL;
    binaryfunc slotw = NULL;
if (v-&gt;ob_type-&gt;tp_as_number != NULL)
    slotv = NB_BINOP(v-&gt;ob_type-&gt;tp_as_number, op_slot);
if (w-&gt;ob_type != v-&gt;ob_type &amp;&amp;
    w-&gt;ob_type-&gt;tp_as_number != NULL) {
    slotw = NB_BINOP(w-&gt;ob_type-&gt;tp_as_number, op_slot); // &lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;
    if (slotw == slotv)
        slotw = NULL;
}
if (slotv) {
    if (slotw &amp;&amp; PyType_IsSubtype(w-&gt;ob_type, v-&gt;ob_type)) {
        x = slotw(v, w);
        if (x != Py_NotImplemented)
            return x;
        Py_DECREF(x); /* can't do it */
        slotw = NULL;
    }
    x = slotv(v, w);
    if (x != Py_NotImplemented)
        return x;
    Py_DECREF(x); /* can't do it */
}
if (slotw) {
    x = slotw(v, w);
    if (x != Py_NotImplemented)
        return x;
    Py_DECREF(x); /* can't do it */
}
Py_RETURN_NOTIMPLEMENTED;
}
```
What's NB_BINOP()?
c
#define NB_BINOP(nb_methods, slot) \
        (*(binaryfunc*)(&amp; ((char*)nb_methods)[slot]))
Cut to the chase: where's the addition function for two ints (longs)?
```c
/ Objects/longobject.c /
static PyObject *
long_add(PyLongObject a, PyLongObject b)
{
    PyLongObject *z;
CHECK_BINOP(a, b);
if (Py_ABS(Py_SIZE(a)) &lt;= 1 &amp;&amp; Py_ABS(Py_SIZE(b)) &lt;= 1) {
    return PyLong_FromLong(MEDIUM_VALUE(a) + MEDIUM_VALUE(b)); // &lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;
}
if (Py_SIZE(a) &lt; 0) {
    if (Py_SIZE(b) &lt; 0) {
        z = x_add(a, b);
        if (z != NULL) {
            /* x_add received at least one multiple-digit int,
               and thus z must be a multiple-digit int.
               That also means z is not an element of
               small_ints, so negating it in-place is safe. */
            assert(Py_REFCNT(z) == 1);
            Py_SIZE(z) = -(Py_SIZE(z));
        }
    }
    else
        z = x_sub(b, a);
}
else {
    if (Py_SIZE(b) &lt; 0)
        z = x_sub(a, b);
    else
        z = x_add(a, b);
}
return (PyObject *)z;
}
```
What's MEDIUM_VALUE()?
c
/* Objects/longobject.c */
#define MEDIUM_VALUE(x) (assert(-1 &lt;= Py_SIZE(x) &amp;&amp; Py_SIZE(x) &lt;= 1),  \
     Py_SIZE(x) &lt; 0 ? -(sdigit)(x)-&gt;ob_digit[0] :  \
         (Py_SIZE(x) == 0 ? (sdigit)0 :  \
          (sdigit)(x)-&gt;ob_digit[0]))
Why so much code for such a simple operation?
C-level is interpreting bytecodes (BINARY_ADD in this case).
Polymorphism -- code can handle foo('a', 'b') or any types that support +.
Works for user-defined types, too, with an __add__ or __radd__ magic method.
Error checking everywhere...
For adding ints, does overflow checking and conversions, etc.
All these features mean a lot of code at the C level.
What is the performance?
End of explanation
HTML(html)
Explanation: Summary
CPython slowdown 1: interpreted bytecode execution
Fetching CPython bytecode ops, managing the stack machine, all ops in ceval.c.
Extensive error checking and handling.
CPython slowdown 2: dynamic type resolution
Type introspection, dynamic dispatch on every operation / method call, supporting generic operations.
And extensive error checking and handling, up-and-down the call stack.
References
https://github.com/kwmsmith/scipy-2017-cython-tutorial
https://jakevdp.github.io/PythonDataScienceHandbook/01.06-errors-and-debugging.html
https://jakevdp.github.io/PythonDataScienceHandbook/01.07-timing-and-profiling.html
https://toucantoco.com/en/tech-blog/tech/python-performance-optimization
http://mortada.net/easily-profile-python-code-in-jupyter.html
End of explanation
<END_TASK>