Questions or a demo for your school?Student? Sit a test
BrainBit Infotech TeachingStart free

Python notes · Topic 16 of 22 · 5 min read

Python comprehensions, generators and decorators

Pythonic Python notes: list, dict and set comprehensions, map, filter, zip and enumerate, sorting with key, iterators, generators with yield and decorators.

List comprehensions: build a list in one line

squares = []
for n in range(1, 6):
    squares.append(n * n)

squares = [n * n for n in range(1, 6)]       # the same, in one line

The pattern is [expression for item in iterable if condition]:

marks = [45, 82, 91, 30, 67]
passed = [m for m in marks if m >= 33]            # [45, 82, 91, 67]
names = [s.title() for s in ["asha", "ravi"]]     # ['Asha', 'Ravi']
labels = ["Pass" if m >= 33 else "Fail" for m in marks]

Note where the if goes: a filter goes at the end; a choice between two values goes at the start.

Dict and set comprehensions

names = ["Asha", "Ravi", "Meena"]
lengths = {n: len(n) for n in names}         # {'Asha': 4, 'Ravi': 4, 'Meena': 5}
prices = {"pen": 10, "bag": 450}
with_gst = {k: v * 1.18 for k, v in prices.items()}
initials = {n[0] for n in names}             # a set: {'A', 'R', 'M'}

table = [[r * c for c in range(1, 4)] for r in range(1, 4)]   # nested: a 3x3 grid

Keep comprehensions short. If one does not fit comfortably on a line, a normal loop is clearer.

map, filter, zip, enumerate, any and all

FunctionWhat it doesExample
map(f, items)apply f to every itemlist(map(int, ["1", "2"])) gives [1, 2]
filter(f, items)keep items where f is truelist(filter(str.isdigit, ["1", "a"])) gives ['1']
zip(a, b)pair items uplist(zip(["A", "B"], [90, 80]))
enumerate(items, 1)number the itemsfor i, x in enumerate(items, 1)
any(items)is at least one true?any(m < 33 for m in marks)
all(items)are all true?all(m >= 33 for m in marks)
names = ["Asha", "Ravi"]
marks = [91, 78]
for name, m in zip(names, marks):
    print(f"{name}: {m}")

A common use of map: read several numbers on one line.

a, b, c = map(int, input("Three numbers: ").split())

sorted(), min() and max() with a key

The key function tells Python *what to compare*:

students = [("Asha", 91), ("Ravi", 78), ("Meena", 85)]
by_marks = sorted(students, key=lambda s: s[1], reverse=True)
topper = max(students, key=lambda s: s[1])
shortest = min(["Ranchi", "Gaya", "Patna"], key=len)    # 'Gaya'

Iterators: how a for loop really works

A for loop calls iter() on what you give it, then next() again and again until there are no items left:

it = iter(["a", "b"])
print(next(it))     # a
print(next(it))     # b
next(it)            # StopIteration: nothing left

Generators: functions that yield values one at a time

A generator function uses yield instead of return. Each yield hands back one value and pauses the function until the next value is asked for.

def countdown(n):
    while n > 0:
        yield n
        n -= 1

for x in countdown(3):
    print(x)          # 3, 2, 1

A generator expression looks like a list comprehension with round brackets:

total = sum(n * n for n in range(1_000_000))

Generators save memory

A list holds all its values at once; a generator makes them one at a time. To read a huge log file, a generator handles one line at a time instead of loading millions of lines into memory:

def errors(path):
    with open(path, encoding="utf-8") as f:
        for line in f:
            if "ERROR" in line:
                yield line.strip()

for e in errors("server.log"):
    print(e)

Decorators: wrap a function to add a feature

A decorator takes a function and returns a new function that does something extra before or after calling the original. You apply it with @.

import time

def timer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        print(f"{func.__name__} took {time.time() - start:.3f} s")
        return result
    return wrapper

@timer                       # same as: slow_sum = timer(slow_sum)
def slow_sum(n):
    return sum(range(n))

slow_sum(10_000_000)

Real apps use decorators for login checks (@login_required in Flask and Django), caching (@functools.lru_cache) and logging.

Common mistakes

  • Putting a filter if before the for in a comprehension.
  • Expecting map() or filter() to give a list: wrap them in list() to see the values.
  • Looping over a generator twice: it is used up after the first loop.
  • Forgetting to return result in a decorator's wrapper, so the decorated function returns None.

Key points

  • [expr for x in items if cond] builds a list; {k: v for ...} and {x for ...} build dicts and sets.
  • zip, enumerate, any and all replace many hand-written loops.
  • key= tells sorted, min and max what to compare.
  • Generators yield values one at a time; decorators wrap functions with @.

Practice questions

1. Write a comprehension for the even numbers from 1 to 20.

[n for n in range(1, 21) if n % 2 == 0]

2. Make a dictionary of each number from 1 to 5 and its cube.

{n: n ** 3 for n in range(1, 6)}

3. What does list(zip([1, 2, 3], "ab")) give?

[(1, 'a'), (2, 'b')]. zip stops at the shorter one.

4. Write a generator that yields the first n even numbers.
def evens(n):
    for i in range(n):
        yield 2 * i
5. What does @timer above a function definition mean?

It replaces the function with timer(function), the wrapped version that also measures the time taken.