Search code examples
pythongloballocalsymbol-table

Why can't I iterate over locals() and within the iteration use returned item as a key?


I have a module named Promotions with some functions, and I have a variable that stores a list of all the functions containing "_promo" in the name.

promos = [
    locals()[name] for name in locals() # Iterate over each name in the dictionary returned by locals()
    if name.endswith("_promo") # Select only names that end with the _promo suffix.
    ] 

When I import Promotions in another place and ask to retrieve promos, I get a KeyError.

However, if I do the same thing but replace locals() with globals(), I don't get a KeyError.

Does anyone know why?

edit: Is it because the second time I call locals() (in locals()[name]) I am no longer in the same scope?


Solution

  • Is it because the second time I call locals() (in locals()[name]) I am no longer in the same scope?

    That's exactly the case. List comprehension have it's own scope just like a function, but the iterator over locals() is created in the outer scope.

    import inspect
    
    class LoudIterable:
        def __iter__(self):
            print(inspect.currentframe())
            return iter([1, 2])
    
    x = [print(inspect.currentframe()) for i in LoudIterable()]
    
    # <frame at 0x0000021795BFD4B8, file '', line 5, code __iter__>
    # <frame at 0x0000021795CF8AF8, file '', line 8, code <listcomp>>
    # <frame at 0x0000021795CF8AF8, file '', line 8, code <listcomp>>
    

    You'll see that the each iteration has the same frame, but the __iter__ was called in an another frame.

    And that makes sense when you think about generators.

    non_iterable = 2
    x = (i for i in non_iterable)
    

    The iter is called on the iterable in an eager way, even though we haven't even started the iteration, you'll see the error straight away: TypeError: 'int' object is not iterable

    Anyway, the easy fix is this:

    promos = [v for k, v in locals().items() if k.endswith("_promo")]