Final Review: Problem Sets 1

  1. Given the following Python code:

     from functools import wraps
    
     def function_logger(func):
         counter = 0
    
         @wraps(func)
         def wrapper(*args, **kwargs):
             nonlocal counter
    
             counter += 1
             result = func(*args, **kwargs)
    
             print(f"The function {func.__name__} has run {counter} times")
    
             return result
         return wrapper
    
    
     @function_logger
     def count_generator():
         i = 0
         while True:
             yield i
             i += 1
    
    
     @function_logger
     def do_nothing() -> None:
         pass
    
    
     gen = count_generator()
     for _ in range(5):
         print(next(gen))
    
     for _ in range(5):
         do_nothing()
    

    a. How many times will the output The function count_generator has run n times be printed?

    b. How many times will the output The function do_nothing has run n times be printed?

     

  2. Write a coroutine function that yields Fibonacci numbers indefinitely, but allows resetting its state via send(). The coroutine will receive any integer value as a sentinel for resetting its state.

     fib_gen = fibonacci_generator()
    
     # Generate some numbers
     print(next(fib_gen))  # 0
     print(next(fib_gen))  # 1
     print(next(fib_gen))  # 1
     print(next(fib_gen))  # 2
     print(next(fib_gen))  # 3
    
     # Reset the sequence
     print(fib_gen.send(0))  # 0
    
     # Start over
     print(next(fib_gen))  # 1
     print(next(fib_gen))  # 1
    

     

  3. What will be the output of the following code?

    def decorator(func):
        return lambda: (x for x in range(5))
    
    @decorator
    def gen():
        for x in range(3):
            yield x
    
    print(sum(gen()))
    
    

     

  4. Determine the output of this nested generator comprehension:

    print(sum(x for x in (y for y in range(3))))
    

     

  5. Is there a syntax error in this code? If not, determine the output:

    gen = ((x * y,) for x in range(2) for y in range(2))
    print(list(gen))
    

     

  6. Predict the output of the following code involving multiple decorators:

     def dec1(func):
         return lambda x: func(x) * 10
    
    
     def dec2(func):
         return lambda x: -func(x)
    
    
     def dec3(func):
         return lambda x: func(x) + 5
    
    
     @dec1
     @dec2
     @dec3
     def number(n):
         return n
    
    
     print(number(5))
    

     

  7. Trace the following nested generator expression. What is the output of the print statements?

    gen = ((x, y, z) for x in 'abc' for y in '123' for z in [True, False])
    print(next(gen))
    print(next(gen))
    print(next(gen))
    

     

  8. What is the output of the following code? If we want the output to be the opposite, what do we have to change about the code?

    data = {
        "name": "General Kenobi",
        "lines": {
            "hello there": 1,
            "this is where the fun begins": 0,
        }
    }
    new_data = data.copy()
    new_data["lines"]["hello there"] = 2
    print(data == new_data)
    

     

  9. What is the output?

    d1 = {'a': 1, 'b': 2}
    d2 = {'b': 3, 'c': 4}
    
    d2 = {**d1, **d2}
    print(d2)
    

     

  10. Write a function create_memo that accepts a function reference func as an argument. create_memo should:

    • Cache the result of func for each unique input.
    • Return the cached result if the same input is provided again, without recalculating the result.

    Example Usage:

    def expensive_calculation(x):
        print(f"Calculating for {x}...")
        return x * x
    
    calc = create_memo(expensive_calculation)
    print(calc(5))  # Output: Calculating for 5... 25
    print(calc(5))  # Output: 25 (cached)
    print(calc(7))  # Output: Calculating for 7... 49
    print(calc(5))  # Output: 25 (cached)
    print(calc(7))  # Output: 49 (cached)