Notas para contenedor de docker:
Comando de docker para ejecución de la nota de forma local:
nota: cambiar <ruta a mi directorio>
por la ruta de directorio que se desea mapear a /datos
dentro del contenedor de docker.
docker run --rm -v <ruta a mi directorio>:/datos --name jupyterlab_numerical -p 8888:8888 -d palmoreck/jupyterlab_numerical:1.1.0
password para jupyterlab: qwerty
Detener el contenedor de docker:
docker stop jupyterlab_numerical
Documentación de la imagen de docker palmoreck/jupyterlab_numerical:1.1.0
en liga.
Esta nota utiliza métodos vistos en 1.5.Integracion_numerica
Instalamos las herramientas que nos ayudarán al perfilamiento:
In [1]:
%pip install -q --user line_profiler
In [2]:
%pip install -q --user memory_profiler
In [3]:
%pip install -q --user psutil
In [4]:
%pip install -q --user guppy3
La siguiente celda reiniciará el kernel de IPython para cargar los paquetes instalados en la celda anterior. Dar Ok en el mensaje que salga y continuar con el contenido del notebook.
In [5]:
import IPython
app = IPython.Application.instance()
app.kernel.do_shutdown(True)
Out[5]:
In [1]:
import math
from scipy.integrate import quad
En esta nota revisamos algunas herramientas de Python para perfilamiento de código: uso de cpu y memoria.
Medición de tiempos con:
Módulo time de Python.
%time de comandos de magic <- esta herramienta es sólo para medir tiempos de un statement y sólo la coloco para referencia pero no se usará en la nota.
/usr/bin/time) de Unix
.
%timeit de comandos de magic.
Perfilamiento:
De CPU con: line_profiler, CProfile que es built-in
en la standard-library de Python.
De memoria con: memory_profiler y heapy.
El primer acercamiento que usamos en la nota para perfilar nuestro código es identificar qué es lento, otras mediciones son la cantidad de RAM, el I/O en disco o network.
In [2]:
import time
Ejemplo de implementación de regla compuesta de rectángulo: usando math
Utilizar la regla compuesta del rectángulo para aproximar la integral $\int_0^1e^{-x^2}dx$ con $10^6$ subintervalos.
In [3]:
f=lambda x: math.exp(-x**2) #using math library
In [4]:
def Rcf(f,a,b,n): #Rcf: rectángulo compuesto para f
"""
Compute numerical approximation using rectangle or mid-point method in
an interval.
Nodes are generated via formula: x_i = a+(i+1/2)h_hat for i=0,1,...,n-1 and h_hat=(b-a)/n
Args:
f (lambda expression): lambda expression of integrand
a (int): left point of interval
b (int): right point of interval
n (int): number of subintervals
Returns:
Rcf (float)
"""
h_hat=(b-a)/n
nodes=[a+(i+1/2)*h_hat for i in range(0,n)]
sum_res=0
for node in nodes:
sum_res=sum_res+f(node)
return h_hat*sum_res
In [5]:
n=10**6
In [6]:
start_time = time.time()
aprox=Rcf(f,0,1,n)
end_time = time.time()
In [7]:
secs = end_time-start_time
print("Rcf tomó",secs,"segundos" )
Obs: recuérdese que hay que evaluar que se esté resolviendo correctamente el problema. En este caso el error relativo nos ayuda
In [8]:
def err_relativo(aprox, obj):
return math.fabs(aprox-obj)/math.fabs(obj) #obsérvese el uso de la librería math
In [9]:
obj, err = quad(f, 0, 1)
err_relativo(aprox,obj)
Out[9]:
Comentarios:
Tómese en cuenta que al medir tiempos de ejecución, siempre hay variación en la medición. Tal variación es normal.
Considérese que la máquina en la que se están corriendo las pruebas puede estar realizando otras tareas mientras se ejecuta el código, por ejemplo acceso a la red, al disco o a la RAM. Por ello, son factores que pueden causar variación en el tiempo de ejecución del programa.
Si se van a realizar reportes de tiempos, es importante indicar las características de la máquina en la que se están haciendo las pruebas, p.ej: Dell E6420 con un procesador Intel Core I7-2720QM (2.20 GHz, 6 MB cache, Quad Core) y 8 GB de RAM en un Ubuntu $13.10$.
Para la línea de comando /usr/bin/time
primero escribimos el siguiente archivo en la ruta donde se encuentra este notebook con la línea de comando magic %file
In [10]:
%%file Rcf.py
import math
def Rcf(f,a,b,n): #Rcf: rectángulo compuesto para f
"""
Compute numerical approximation using rectangle or mid-point method in
an interval.
Nodes are generated via formula: x_i = a+(i+1/2)h_hat for i=0,1,...,n-1 and h_hat=(b-a)/n
Args:
f (lambda expression): lambda expression of integrand
a (int): left point of interval
b (int): right point of interval
n (int): number of subintervals
Returns:
Rcf (float)
"""
h_hat=(b-a)/n
nodes=[a+(i+1/2)*h_hat for i in range(0,n)]
sum_res=0
for node in nodes:
sum_res=sum_res+f(node)
return h_hat*sum_res
if __name__=="__main__": #añadimos este bloque para ejecución de la función Rcf
n=10**6
f=lambda x: math.exp(-x**2)
print("aproximación: {:0.6e}".format(Rcf(f,0,1,n)))
Lo siguiente es necesario si no tienen instalado el comando /usr/bin/time
:
In [11]:
%%bash
sudo apt-get install time
In [12]:
%%bash
/usr/bin/time -p python3 Rcf.py #la p es de portabilidad,
#ver: http://manpages.ubuntu.com/manpages/xenial/man1/time.1.html
#para mayor información
Comentarios:
real
que mide el wall clock o elapsed time.user
que mide la cantidad de tiempo de tu ejecución que la CPU gastó para funciones que no están relacionadas con el kernel* del sistema.sys
que mide la cantidad de tiempo de tu ejecución que la CPU gastó en funciones a nivel de kernel del sistema.*Ver kernel operating system) para definición del kernel de una máquina.
Obs: Una función relacionada con el kernel del sistema es el alojamiento de memoria al crear una variable. Otras son las instrucciones relacionadas con el I/O como leer de la memoria, disco o network.
/usr/bin/time
es que no es específico de Python./usr/bin/time
puede ser una medida útil.Nota: Si se suma user
con sys
se tiene una idea de cuánto tiempo se gastó en la CPU y la diferencia entre este resultado y real
da una idea de cuánto tiempo se gastó para I/O o también puede dar una idea de la cantidad de tiempo que se ocupó el sistema en correr otras tareas.
verbose
para obtener más información:
In [13]:
%%bash
/usr/bin/time --verbose python3 Rcf.py
y una explicación (breve) del output se puede encontrar aquí. Para el caso de Major (requiring I/O)
nos interesa que sea $0$ pues indica que el sistema operativo tiene que cargar páginas de datos del disco pues tales datos ya no residen en RAM (por alguna razón).
El módulo de timeit
es otra forma de medir el tiempo de ejecución en la CPU.
Nota: el módulo de timeit
desabilita temporalmente el garbage collector* de Python (esto es, no habrá desalojamiento en memoria de objetos de Python que no se utilicen). Si el garbage collector es invocado en tus operaciones para un ejemplo del mundo real, esto puede ser una razón de posibles diferencias que obtengas en las mediciones de tiempo.
In [14]:
%timeit?
In [15]:
%timeit -n 5 -r 10 Rcf(f,0,1,n)
para este caso se está ejecutando la función Rcf
en un loop de tamaño $5$, se están promediando los tiempos de las $5$ ejecuciones y calculando su desviación estándar y al repetir esto $10$ veces se está reportando el mejor resultado. $ms$ es milisecond, $\mu s$ es microsecond y $ns$ es nanosecond.
Comentarios:
timeit
se recomienda usar para secciones de código pequeñas. Para secciones más grandes típicamente modificar el valor de $n$ (ejecutar el código n veces en un loop) resulta en mediciones distintas.
Ejecuta timeit
varias ocasiones para asegurarse que se obtienen tiempos similares. Si observas una gran variación en las mediciones de tiempo entre distintas repeticiones de timeit
, realiza más repeticiones hasta tener un resultado estable.
cProfile
es una herramienta built-in en la standard library para perfilamiento. Se utiliza con la implementación CPython
de Python
(ver liga para explicación de implementaciones de Python) para medir el tiempo de ejecución de cada función en el programa.
Se ejecuta desde la línea de comandos o con un comando de magic. La flag -s
indica que se ordene el resultado por el tiempo acumulado dentro de cada función.
El output siguiente de cProfile
muestra:
El tiempo total de ejecución, el cual incluye el tiempo del bloque de código que estamos midiendo y el overhead al usar cProfile
. Por esta razón se tiene un mayor tiempo de ejecución que con las mediciones de tiempo anteriores.
La columna ncalls
que como el nombre indica, muestra el número de veces que se llamó a cada función. En este caso las funciones lambda
y math.exp
son las que se llaman un mayor número de veces: $n=10^6$ veces. La columnatottime
muestra el tiempo que tardaron estas funciones en ejecutarse (sin llamar a otras funciones).
La columna percall
es el cociente entre tottime
y ncalls
.
La columna cumtime
contiene el tiempo gastado en la función y en las demás que llama. Por ejemplo la función Rcf
llama a listcomp
por lo que es natural que Rcf
esté más arriba en el output ordenado de cProfile
. Esto también ocurre con lambda
y math.exp
pues la primera llama a la segunda.
La columna de percall
es un cociente entre la columna cumtime
y el llamado a primitivas.
La última columna indica información de la función y la línea en la que se encuentra dentro del código. Por ejemplo la línea $1$ de módulo es el llamado a la función __main__
. La línea $2$ es el llamado a la función Rcf
. Por lo que es prácticamente negligible el llamado a __main__
.
In [16]:
%%bash
python3 -m cProfile -s cumulative Rcf.py
Nota: Recordar que el output de CProfile
con la flag -s cumulative
está ordenando por el gasto en tiempo de las funciones que son llamadas en el bloque de código analizado. No está ordenando por parent functions. Para tener un output en el que se tenga qué funciones llaman a qué otras se puede utilizar lo siguiente:
In [17]:
%%bash
python3 -m cProfile -o profile.stats Rcf.py
In [18]:
import pstats
In [19]:
p = pstats.Stats("profile.stats")
p.sort_stats("cumulative")
Out[19]:
In [20]:
p.print_stats()
Out[20]:
In [21]:
p.print_callers()
Out[21]:
y podemos también tener la información de a qué funciones llamó cada función
In [22]:
p.print_callees()
Out[22]:
El comando de magic es %prun
:
In [23]:
%prun -s cumulative Rcf(f,0,1,n)
line_profiler
trabaja perfilando el código de forma individual funciones línea por línea. La idea sería perfilar primero con CProfile
al programa para identificar aquellas funciones que gastan un mayor tiempo de ejecución y posteriormente perfilarlas con line_profiler
.
Comentario: una buena práctica es guardar las diferentes versiones de tu código cuando vas modificándolo para tener un registro de tus cambios.
Puede ejecutarse desde la línea de comandos o cargarse en IPython con el comando magic load_ext
:
In [24]:
%load_ext line_profiler
In [25]:
%lprun?
En el siguiente output:
%Time
contiene el porcentaje de tiempo gastado. En el caso que se perfila, la líneasum_res=sum_res+f(node)
es en la que más porcentaje del tiempo se gasta. Seguida de la línea del for
y de la línea donde se hace uso de list comprehension para crear a los nodos de integración numérica.
In [26]:
%lprun -f Rcf Rcf(f,0,1,n)
Con la evidencia generada con line_profiler
¿podríamos escribir una función que fuera más rápida?
Lo primero que podemos hacer es utilizar un generator en lugar de una lista:
In [27]:
def Rcf2(f,a,b,n):
"""
Compute numerical approximation using rectangle or mid-point method in
an interval.
Nodes are generated via formula: x_i = a+(b-a)/n*i for i=0,1,...,n-1
Args:
f (lambda expression): lambda expression of integrand
a (int): left point of interval
b (int): right point of interval
n (int): number of subintervals
Returns:
Rcf2 (float)
"""
h_hat=(b-a)/n
nodes=(a+(i+1/2)*h_hat for i in range(0,n))
sum_res=0
for node in nodes:
sum_res=sum_res+f(node)
return h_hat*sum_res
medir con %timeit
:
In [28]:
%timeit -n 5 -r 10 Rcf2(f,0,1,n)
In [42]:
aprox=Rcf2(f,0,1,n)
revisar que está correcta esta nueva implementación:
In [43]:
err_relativo(aprox,obj)
Out[43]:
perfilarla con line_profiler
:
In [44]:
%lprun -f Rcf2 Rcf2(f,0,1,n)
y observar que la línea en la que se creaba la lista ahora es despreciable el porcentaje de tiempo que se gasta en ella.
Podemos hacer una implementación que se encargue del gasto del tiempo en la línea del for
:
In [31]:
def Rcf3(f,a,b,n):
"""
Compute numerical approximation using rectangle or mid-point method in
an interval.
Nodes are generated via formula: x_i = a+(b-a)/n*i for i=0,1,...,n-1
Args:
f (lambda expression): lambda expression of integrand
a (int): left point of interval
b (int): right point of interval
n (int): number of subintervals
Returns:
Rcf3 (float)
"""
h_hat=(b-a)/n
nodes=(a+(i+1/2)*h_hat for i in range(0,n))
suma_res = sum((f(node) for node in nodes))
return h_hat*suma_res
medir con %timeit
:
In [32]:
%timeit -n 5 -r 10 Rcf3(f,0,1,n)
revisar que está correcta esta nueva implementación:
In [45]:
aprox=Rcf3(f,0,1,n)
In [46]:
err_relativo(aprox,obj)
Out[46]:
perfilarla con line_profiler
:
In [47]:
%lprun -f Rcf3 Rcf3(f,0,1,n)
y se tiene la mayoría del porcentaje de tiempo ahora en una sola línea.
Recuérdese que el resultado de Cprofile
indicó que se llama a la función lambda
y math.exp
$n=10^6$ veces. Una implementación de la regla del rectángulo con menor número de llamadas a funciones (y por tanto menor tiempo) sería:
In [35]:
def Rcf4(a,b,n):
"""
Compute numerical approximation using rectangle or mid-point method in
an interval.
Nodes are generated via formula: x_i = a+(b-a)/n*i for i=0,1,...,n-1
Args:
f (lambda expression): lambda expression of integrand
a (int): left point of interval
b (int): right point of interval
n (int): number of subintervals
Returns:
Rcf4 (float)
"""
h_hat=(b-a)/n
nodes=(a+(i+1/2)*h_hat for i in range(0,n))
suma_res = sum(((math.exp(-node**2) for node in nodes)))
return h_hat*suma_res
In [36]:
%lprun -f Rcf4 Rcf4(0,1,n)
In [37]:
%timeit -n 5 -r 10 Rcf4(0,1,n)
In [49]:
aprox=Rcf4(0,1,n)
In [50]:
err_relativo(aprox,obj)
Out[50]:
Si bien esta implementación es la más rápida hasta este punto no es tan flexible pues está calculando la regla del rectángulo para una función definida dentro de la misma función. Si quisiéramos calcular la regla para otra función se tendría que directamente modificar la función Rcf
lo cual no es flexible. Aunque Rcf4
es más rápida preferimos Rcf3
por su flexibilidad y menor uso de recursos (que se verá con el memory_profiler
más adelante).
In [57]:
def Rcf5(a,b,n):
"""
Compute numerical approximation using rectangle or mid-point method in
an interval.
Nodes are generated via formula: x_i = a+(b-a)/n*i for i=0,1,...,n-1
Args:
f (lambda expression): lambda expression of integrand
a (int): left point of interval
b (int): right point of interval
n (int): number of subintervals
Returns:
Rcf4 (float)
"""
h_hat=(b-a)/n
f_nodes=(math.exp(-(a+(i+1/2)*h_hat)**2) for i in range(0,n))
suma_res = sum(f_nodes)
return h_hat*suma_res
In [58]:
%lprun -f Rcf5 Rcf5(0,1,n)
In [59]:
%timeit -n 5 -r 10 Rcf5(0,1,n)
In [60]:
aprox=Rcf5(0,1,n)
In [61]:
err_relativo(aprox,obj)
Out[61]:
Obsérvese que en una línea se están construyendo nodos y transformando con math.exp
en Rcf5
. Aunque esta implementación es la más rápida hasta ahora, no se sugiere usarla pues le falta flexibilidad como Rcf4
y no es recomendable en una línea construir datos y transformarlos. Combinar operaciones en una sola línea resulta en código difícil de leer. Es mejor separar en dos funciones estas dos tareas por si falla una sepamos cuál falló y por qué falló.
Ejemplo de ejecución de line_profiler desde la línea de comandos:
In [62]:
%%file Rcf4.py
import math
@profile #esta línea es necesaria para indicar que la siguiente función
#desea perfilarse con line_profiler
def Rcf4(a,b,n):
"""
Compute numerical approximation using rectangle or mid-point method in
an interval.
Nodes are generated via formula: x_i = a+(b-a)/n*i for i=0,1,...,n
Mid point is calculated via formula: x_{i-1}+(x_i-x_{i-1})/2 for i=1,...,n-1 to avoid rounding errors
Args:
f (lambda expression): lambda expression of integrand
a (int): left point of interval
b (int): right point of interval
n (int): number of subintervals
Returns:
Rcf4 (float)
"""
h_hat=(b-a)/n
nodes=(a+(i+1/2)*h_hat for i in range(0,n))
suma_res = sum(((math.exp(-node**2) for node in nodes)))
return h_hat*suma_res
if __name__ == "__main__":
n=10**6
print("aproximación: {:0.6e}".format(Rcf4(0,1,n)))
In [63]:
%%bash
$HOME/.local/bin/kernprof -l -v Rcf4.py
Observese en el output de CProfile
siguiente para la función Rcf4
que las líneas con mayor gasto en el tiempo total son:
nodes=(a+(i+1/2)*h_hat for i in range(0,n))
suma_res = sum(((math.exp(-node**2) for node in nodes)))
In [64]:
import math
def Rcf4(a,b,n):
"""
Compute numerical approximation using rectangle or mid-point method in
an interval.
Nodes are generated via formula: x_i = a+(b-a)/n*i for i=0,1,...,n
Mid point is calculated via formula: x_{i-1}+(x_i-x_{i-1})/2 for i=1,...,n-1 to avoid rounding errors
Args:
f (lambda expression): lambda expression of integrand
a (int): left point of interval
b (int): right point of interval
n (int): number of subintervals
Returns:
Rcf4 (float)
"""
h_hat=(b-a)/n
nodes=(a+(i+1/2)*h_hat for i in range(0,n))
suma_res = sum(((math.exp(-node**2) for node in nodes)))
return h_hat*suma_res
In [65]:
%prun -s cumulative Rcf4(0,1,n)
Al realizar análisis del uso de memoria de tu código podemos responder preguntas como:
¿Es posible utilizar menos RAM al reescribir mi función para que trabaje más eficientemente?
¿Podemos usar más RAM para aprovechar mejor el uso del caché?
Es equivalente a %timeit
en el sentido que realiza una serie de repeticiones para obtener un resultado estable del bloque de código analizado.
In [66]:
%load_ext memory_profiler
In [67]:
%memit?
Primero medimos cuánto RAM está utilizando el proceso del notebook:
In [68]:
%memit #how much RAM this process is consuming
Y podemos realizar mediciones para cada una de las implementaciones de la regla del rectángulo:
In [74]:
%memit -c Rcf(f,0,1,n)
In [75]:
%memit -c Rcf2(f,0,1,n)
In [76]:
%memit -c Rcf3(f,0,1,10**5)
In [77]:
%memit -c Rcf4(0,1,10**5)
In [78]:
%memit -c Rcf5(0,1,10**5)
El uso de generators
nos ayuda a disminuir la cantidad de memoria RAM usada por nuestro proceso.
Para medición de memoria línea por línea utilizamos memory_profiler
. Se ejecuta más lento que line_profiler
(entre $10$ y $100$ veces más lento!) y mejora su velocidad de ejecución al instalar el paquete psutil
.
Con línea de comandos se ejecuta como sigue:
In [49]:
%%file Rcf_memory_profiler.py
import math
@profile #esta línea es necesaria para indicar que la siguiente función
#desea perfilarse con memory_profiler
def Rcf(f,a,b,n): #Rcf: rectángulo compuesto para f
"""
Compute numerical approximation using rectangle or mid-point method in
an interval.
Nodes are generated via formula: x_i = a+(i+1/2)h_hat for i=0,1,...,n and h_hat=(b-a)/n
Args:
f (lambda expression): lambda expression of integrand
a (int): left point of interval
b (int): right point of interval
n (int): number of subintervals
Returns:
Rcf (float)
"""
h_hat=(b-a)/n
nodes=[a+(i+1/2)*h_hat for i in range(0,n)]
sum_res=0
for node in nodes:
sum_res=sum_res+f(node)
return h_hat*sum_res
if __name__=="__main__": #añadimos este bloque para ejecución de la función Rcf
n=10**6
f=lambda x: math.exp(-x**2)
print("aproximación: {:0.6e}".format(Rcf(f,0,1,n)))
En el output siguiente se observa que la línea que más incrementa la cantidad de RAM alojada para el proceso que contiene la ejecución de la función Rcf
es la creación de la lista de nodos nodes=[a+(i+1/2)*h_hat for i in range(0,n)]
. Cuidado: el valor de la columna Increment
para esta línea no necesariamente indica que la lista nodes
ocupa en memoria $512 MB$'s, sólo que para la alocación de la lista el proceso creció en $512 MB$'s
Nota: en el output aparece $MiB$ que son mebibytes. Aunque no se cumple que un mebibyte sea igual a un megabyte, se toma en este comentario como megabytes pues la diferencia entre estas unidades es sutil.
In [50]:
%%bash
python3 -m memory_profiler Rcf_memory_profiler.py
Como ya se había notado, los generators ahorran memoria:
In [51]:
%%file Rcf3_memory_profiler.py
import math
@profile #esta línea es necesaria para indicar que la siguiente función
#desea perfilarse con memory_profiler
def Rcf3(f,a,b,n):
"""
Compute numerical approximation using rectangle or mid-point method in
an interval.
Nodes are generated via formula: x_i = a+(b-a)/n*i for i=0,1,...,n
Args:
f (lambda expression): lambda expression of integrand
a (int): left point of interval
b (int): right point of interval
n (int): number of subintervals
Returns:
Rcf3 (float)
"""
h_hat=(b-a)/n
nodes=(a+(i+1/2)*h_hat for i in range(0,n))
suma_res = sum((f(node) for node in nodes))
return h_hat*suma_res
if __name__=="__main__": #añadimos este bloque para ejecución de la función Rcf3
n=10**6
f=lambda x: math.exp(-x**2)
print("aproximación: {:0.6e}".format(Rcf3(f,0,1,n)))
En el output siguiente el proceso que involucra la ejecución de la función Rcf3
no incrementa el uso de memoria RAM por el uso de generators:
In [52]:
%%bash
python3 -m memory_profiler Rcf3_memory_profiler.py
In [53]:
import math
from guppy import hpy
def Rcf(f,a,b,n): #Rcf: rectángulo compuesto para f
"""
Compute numerical approximation using rectangle or mid-point method in
an interval.
Nodes are generated via formula: x_i = a+(i+1/2)h_hat for i=0,1,...,n and h_hat=(b-a)/n
Args:
f (lambda expression): lambda expression of integrand
a (int): left point of interval
b (int): right point of interval
n (int): number of subintervals
Returns:
Rcf (float)
"""
hp=hpy()
h_hat=(b-a)/n
h=hp.heap()
print("beginning of Rcf")
print(h)
nodes=[a+(i+1/2)*h_hat for i in range(0,n)]
h=hp.heap()
print("After creating list")
print(h)
sum_res=0
for node in nodes:
sum_res=sum_res+f(node)
h=hp.heap()
print("After loop")
print(h)
return h_hat*sum_res
In [54]:
Rcf(f,0,1,n)
Out[54]:
In [55]:
import math
from guppy import hpy
def Rcf3(f,a,b,n):
"""
Compute numerical approximation using rectangle or mid-point method in
an interval.
Nodes are generated via formula: x_i = a+(b-a)/n*i for i=0,1,...,n
Args:
f (lambda expression): lambda expression of integrand
a (int): left point of interval
b (int): right point of interval
n (int): number of subintervals
Returns:
Rcf3 (float)
"""
hp=hpy()
h_hat=(b-a)/n
h=hp.heap()
print("beginning of Rcf3")
print(h)
nodes=(a+(i+1/2)*h_hat for i in range(0,n))
h=hp.heap()
print("After creating generator")
print(h)
suma_res = sum((f(node) for node in nodes))
h=hp.heap()
print("After loop")
print(h)
return h_hat*suma_res
In [56]:
Rcf3(f,0,1,n)
Out[56]:
Como ya se había revisado el uso de generators ayuda a disminuir el consumo de memoria, manteniendo la eficiencia.
Ejercicios
Referencias
Otras referencias para heapy:
Ver SnakeViz para visualización del output de CProfile
.