Want to use some more advanced features of Python in coming examples
Can now explain them in terms of what we've seen in previous lessons
import time
def elapsed(since):
return time.time() - since
def mock_time():
return 200
def test_elapsed():
time.time = mock_time
assert elapsed(50) == 150
But this changes time.time for everything
Want a reliable way to restore the original
If a function is just an object
We can make an object that looks like a function
class Adder:
def __init__(self, value):
self.value = value
def __call__(self, arg):
return arg + self.value
add_3 = Adder(3)
result = add_3(8)
print(f"add_3(8): {result}")
add_3(8): 11
class Fake:
def __init__(self, func=None, value=None):
self.calls = []
self.func = func
self.value = value
def __call__(self, *args, **kwargs):
self.calls.append([args, kwargs])
if self.func is not None:
return self.func(*args, **kwargs)
return self.value
def fakeit(name, func=None, value=None):
assert name in globals()
fake = Fake(func, value)
globals()[name] = fake
return fake
def adder(a, b):
return a + b
def test_with_real_function():
assert adder(2, 3) == 5
def test_with_fixed_return_value():
fakeit("adder", value=99)
assert adder(2, 3) == 99
def test_fake_records_calls():
fake = fakeit("adder", value=99)
assert adder(2, 3) == 99
assert adder(3, 4) == 99
assert adder.calls == [[(2, 3), {}], [(3, 4), {}]]
def test_fake_calculates_result():
fakeit("adder", func=lambda left, right: 10 * left + right)
assert adder(2, 3) == 23
A protocol specifies how programs can tell Python to do specific things at specific moments
__init__ to build objects
__call__ to emulate function call
Define __enter__ and __exit__ to create a context manager
that a with statement can use
with C(…args…) as name:
…do things…
C's constructor to create an object.__enter__ method and assign the result to name.with block.Call name.__exit__() when the block finishes.
__enter__ doesn't need extra arguments
Python calls __exit__ with three values for error handling
class ContextFake(Fake):
def __init__(self, name, func=None, value=None):
super().__init__(func, value)
self.name = name
self.original = None
def __enter__(self):
assert self.name in globals()
self.original = globals()[self.name]
globals()[self.name] = self
return self
def __exit__(self, exc_type, exc_value, exc_traceback):
globals()[self.name] = self.original
def original(value):
print(f"original: {value}")
def logging(value):
print("before call")
original(value)
print("after call")
original = logging
original("example")
before call
before call
before call
def original(value):
print(f"original: {value}")
def logging(func):
def _inner(value):
print("before call")
func(value)
print("after call")
return _inner
original = logging(original)
original("example")
before call
original: example
after call
def original(value):
print(f"original: {value}")
def logging(func, label):
def _inner(value):
print(f"++ {label}")
func(value)
print(f"-- {label}")
return _inner
original = logging(original, "call")
original("example")
++ call
original: example
-- call
def wrap(func):
def _inner(*args):
print("before call")
func(*args)
print("after call")
return _inner
@wrap
def original(message):
print(f"original: {message}")
original("example")
before call
original: example
after call
def wrap(label): # function returning a decorator
def _decorate(func): # the decorator Python will apply
def _inner(*args): # the wrapped function
print(f"++ {label}") # 'label' is visible because
func(*args) # …it's captured in the closure
print(f"-- {label}") # …of '_decorate'
return _inner
return _decorate
@wrap("wrapping") # call 'wrap' to get a decorator
def original(message): # decorator applied here
print(f"original: {message}")
original("example")
++ wrapping
original: example
-- wrapping
A decorator must take exactly one argument, so how do we pass other parameters to the decorator itself?
Simple-to-learn answer would have been to treat function being decorated
like self in method definition and call
def decorator(func, label):
def _inner(arg):
print(f"entering {label}")
func(arg)
return _inner
@decorator("message")
def double(x): # equivalent to
return 2 * x # double = decorator(double, "message")
Python calls thing.__iter__ at the start of a for loop
to get an iterator
Calls iterator.__next__ repeatedly to get loop items
Stops when the iterator raises StopIteration
(Almost) always create a separate object so that we can run nested loops on the same target
class BetterIterator:
def __init__(self, text):
self._text = text[:]
def __iter__(self):
return BetterCursor(self._text)
class BetterCursor:
def __init__(self, text):
self._text = text
self._row = 0
self._col = -1
def __next__(self):
self._advance()
if self._row == len(self._text):
raise StopIteration
return self._text[self._row][self._col]
# ...6 lines not shown...
def test_naive_buffer_nested_loop():
buffer = BetterIterator(["a", "b"])
result = ""
for _ in buffer:
for inner in buffer:
result += inner
assert result == "abab"