generators and decorators
ITERATORS
AND
GENERATORS
Objectives
- Define Iterator and Iterable
- Understand the iter() and next() methods
- Build our own for loop
- Define what generators are and how they can be used
- Compare generator functions and generator expressions
- Use generators to pause execution of expensive functions
- Define what decorators are and how they can be used
- Create decorators to enhance the behavior of a function
- Explain what the wraps function is, and why it's used when writing decorators
- Create decorators that accept arguments
Iterators? Iterables??
Iterator - an object that can be iterated upon. An object which returns data, one element at a time when next() is called on it
Iterable - An object which will return an Iterator when iter() is called on it.
Ok But what does that really mean
iter("HELLO") returns an iterator
"HELLO" is an iterable, but it is not an iterator.
NEXT
When next() is called on an iterator, the iterator returns the next item. It keeps doing so until it raises a StopIteration error.
CUSTOM FOR LOOP
def for_loop(iterable, func):
iterator = iter(iterable)
while True:
try:
thing = next(iterator)
except StopIteration:
break
else:
func(thing)
Generators
- Generators are iterators
- Generators can be created with generator functions
- Generator functions use the yield keyword
- Generators can be created with generator expressions
Functions vs Generator Functions
Functions | Generator Functions |
---|---|
uses
return
|
uses
yield
|
returns once | can yield multiple times |
When invoked, returns the return value | When invoked, returns a generator |
Our First Generator
def count_up_to(max):
count = 1
while count <= max:
yield count
count += 1
Exhausting a Generator
- Calling
next
on a generator with nothing left to yield will throw aStopIteration
error - When we loop over a generator, the loop will stop before the
StopIteration
error gets thrown
Generator Expressions
- You can also create generators from generator expressions
- Generator expressions look a lot like list comprehensions
- Generator expressions use () instead of []
Another Example
def sum_of_nums():
total = 0
num = 1
while True:
total += num
yield total
num += 1
s = sum_of_nums() # another generator!
CAN'T STOP WON'T STOP
(this generator)
Why Generators?
Lazy Evaluation
- Also called calculation on demand
- Only compute values as needed
- Can help improve performance of your code
An Example
Some Number Theory!
A number is called abundant if the sum of all of its proper divisors exceeds the number.
Examples:
- 12 (1 + 2 + 3 + 4 + 6 > 12)
- 18 (1 + 2 + 3 + 6 + 9 > 18)
- 20 (1 + 2 + 4 + 5 + 10 > 20)
Non-Examples:
- 4 (1 + 2 < 4)
- 6 (1 + 2 + 3 = 6)
- 15 (1 + 3 + 5 < 15)
Generating Abundant Numbers
def is_abundant(n):
total = 0
for d in range(1,n):
if n % d == 0:
total += d
return total > n
is_abundant(12) # True
is_abundant(4) # False
Lists vs. Generators
def list_first_abundants(n):
abundant_nums = []
num = 1
while len(abundant_nums) < n:
if is_abundant(num):
abundant_nums.append(num)
num += 1
return abundant_nums
def gen_first_abundants(n):
count = 0
num = 1
while count < n:
if is_abundant(num):
yield num
count += 1
num +=1
Recap
- Generators are iterators
- Generators can be created with generator functions using the yield keyword
- Generators can be created with generator expressions
- Generators may or may not have terminating conditions
- Generators can provide memory savings
- Generators calculate values as they are needed
YOUR TURN
Introduction to Decorators
What's a Decorator??
- Decorators are functions
- Decorators wrap other functions and enhance their behavior
- Decorators are examples of higher order functions
- Decorators have their own syntax, using "@" (syntactic sugar)
Decorators as Functions
def be_polite(fn):
def wrapper():
print("What a pleasure to meet you!")
fn()
print("Have a great day!")
return wrapper
def greet():
print("My name is Colt.")
greet = be_polite(greet)
# we are decorating our function
# with politeness!
Decorator Syntax
def be_polite(fn):
def wrapper():
print("What a pleasure to meet you!")
fn()
print("Have a great day!")
return wrapper
@be_polite
def greet():
print("My name is Matt.")
# we don't need to set
# greet = be_polite(greet)
Functions with Different Signatures
def shout(fn):
def wrapper(name):
return fn(name).upper()
return wrapper
@shout
def greet(name):
return f"Hi, I'm {name}."
@shout
def order(main, side):
return f"Hi, I'd like the {main}, with a side of {side}, please."
Decorator Pattern
def my_decorator(fn):
def wrapper(*args, **kwargs):
# do some stuff with fn(*args, **kwargs)
pass
return wrapper
Preserving Metadata
def log_function_data(fn):
def wrapper(*args, **kwargs):
print(f"you are about to call {fn.__name__}")
print(f"Here's the documentation: {fn.__doc__}")
return fn(*args, **kwargs)
return wrapper
@log_function_data
def add(x,y):
'''Adds two numbers together.'''
return x + y;
Decorator Pattern
from functools import wraps
# wraps preserves a function's metadata
# when it is decorated
def my_decorator(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
# do some stuff with fn(*args, **kwargs)
pass
return wrapper
Using Decorators
Why Use Decorators?
- Removing code duplication across functions
- More easily perform function analytics/logging
- Exit out of a function early if certain conditions aren't met
Decorators Example
from functools import wraps
from time import time
def speed_test(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
t1 = time()
result = fn(*args, **kwargs)
t2 = time()
print(f"Time Elapsed: {t2 - t1} seconds.")
return result
return wrapper
Another Example
from functools import wraps
def ensure_no_kwargs(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
if kwargs:
return "No keyword arguments allowed!"
return fn(*args)
return wrapper
Decorators with Arguments
@ensure_first_arg_is("burrito")
def fav_foods(*foods):
print(foods)
fav_foods("burrito", "ice cream")
# ('burrito', 'ice cream')
fav_foods("ice cream", "burrito")
# 'Invalid! First argument must be burrito'
@ensure_first_arg_is(10)
def add_to_ten(num1, num2):
return num1 + num2
add_to_ten(10, 12) # 12
add_to_ten(1, 2)
# 'Invalid! First argument must be 10'
How can we write this decorator?
Decorators with Arguments
def ensure_first_arg_is(val):
def inner(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
if args and args[0] != val:
return f"Invalid! First argument must be {val}"
return fn(*args, **kwargs)
return wrapper
return inner
Recap
- Decorators are functions that enhance other functions
- Decorators use "@" as syntactic sugar
- In general, the functions that decorators return accept an unlimited number of positional and keyword arguments
- To preserve information about the decorated function, use wraps
- To write a decorator that accepts an argument, use another level of function nesting
- Decorators are useful for minimizing code duplication, analyzing functions, returning early from a function, and more!
YOUR TURN
Generators and Decorators
By colt
Generators and Decorators
- 4,363