def test(a):
    return a

%timeit test(1)

"""
Explanation: Overhead of Functions and Coroutines
Plain function calls
End of explanation
"""


numbers = list(range(1000))

%timeit sum(numbers)

"""
Explanation: So we get 10M function calls per second (a few hundred CPU clock cycles). This is about 100 times slower than what can be achieved in low level languages.
CPython does use the C stack for function calls and spins up a new interpreter loop for each call. However, it uses a custom frame object for the actual Python content.
Plain list
Using this for comparison with generators (reading the numbers from a precomputed list should be faster...).
End of explanation
"""


def generator_func():
    i = 0
    while i < 1000:
        i += 1
        yield i

%timeit sum(generator_func())

"""
Explanation: Generator
End of explanation
"""


def generator_wrapper(inner_iterator):
    yield from inner_iterator

%timeit sum(generator_wrapper(generator_wrapper(generator_func())))

"""
Explanation: So while this is ten times slower, we can still manage about 10M iterations per second (similar to the number of function calls we can manage).
Under the hood CPython keeps the frame object and reuses it when entering the function again, but a new C stack frame is pushed each time.
What about nesting generators?
End of explanation
"""


def test_func(i):
    return i + 1;

def generator_calling_func():
    i = 0
    while i < 1000:
        i = test_func(i)
        yield i
        
%timeit sum(generator_calling_func())

"""
Explanation: So while nesting added some overhead, the effect is not dramatic (we still manage about five million iterations per second).
Plain function calls in a generator
End of explanation
"""


import asyncio
loop = asyncio.get_event_loop()

async def counter():
    sum = 0
    i = 0
    while i < 1000:
        i = await get_next(i)
        sum += i
    return sum

async def get_next(i):
    return i + 1
    
%timeit loop.run_until_complete(counter())

"""
Explanation: asyncio overhead
The cost of switching to another thread is hard to estimate, but a rough estimate might be $30 \mu s$. If entering a generator takes $100ns$ then this should be enough for 300 such enterings in the same amount of time.
End of explanation
"""


