AND
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.
iter("HELLO") returns an iterator
"HELLO" is an iterable, but it is not an iterator.
When next() is called on an iterator, the iterator returns the next item. It keeps doing so until it raises a StopIteration error.
def for_loop(iterable, func):
iterator = iter(iterable)
while True:
try:
thing = next(iterator)
except StopIteration:
break
else:
func(thing)
Generators
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
next
on a generator with nothing left to yield will throw a
StopIteration
error
StopIteration
error gets thrown
Generator Expressions
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
An Example
Some Number Theory!
A number is called abundant if the sum of all of its proper divisors exceeds the number.
Examples:
Non-Examples:
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
Introduction to Decorators
What's a Decorator??
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?
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