Added example solutions to several chapters. Feel free to create a pull request with your answers. Also for the chapters that have no solutions yet :)
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
# Extend the `track` function to monitor execution time.
|
||||
import functools
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def track(function=None, label=None):
|
||||
# Trick to add an optional argument to our decorator
|
||||
if label and not function:
|
||||
return functools.partial(track, label=label)
|
||||
|
||||
print(f'initializing {label}')
|
||||
|
||||
@functools.wraps(function)
|
||||
def _track(*args, **kwargs):
|
||||
print(f'calling {label}')
|
||||
|
||||
start = datetime.now()
|
||||
result = function(*args, **kwargs)
|
||||
end = datetime.now()
|
||||
|
||||
print(f'called {label} in {end - start}')
|
||||
|
||||
return result
|
||||
|
||||
return _track
|
||||
|
||||
|
||||
@track(label='outer')
|
||||
@track(label='inner')
|
||||
def func():
|
||||
print('func')
|
||||
|
||||
|
||||
@track(label='Slow function')
|
||||
def slower_func():
|
||||
print('slower_func')
|
||||
time.sleep(0.5)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
func()
|
||||
slower_func()
|
||||
@@ -0,0 +1,61 @@
|
||||
# Extend the `track` function with min/max/average execution time and call
|
||||
# count.
|
||||
import functools
|
||||
import random
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
def track(function=None, label=None):
|
||||
# Trick to add an optional argument to our decorator
|
||||
if label and not function:
|
||||
return functools.partial(track, label=label)
|
||||
|
||||
execution_times = dict(
|
||||
min=timedelta.max,
|
||||
max=timedelta.min,
|
||||
total=timedelta(),
|
||||
count=0,
|
||||
)
|
||||
|
||||
print(f'initializing {label}')
|
||||
|
||||
@functools.wraps(function)
|
||||
def _track(*args, **kwargs):
|
||||
print(f'calling {label}')
|
||||
|
||||
start = datetime.now()
|
||||
result = function(*args, **kwargs)
|
||||
end = datetime.now()
|
||||
|
||||
duration = end - start
|
||||
execution_times['min'] = min(execution_times['min'], duration)
|
||||
execution_times['max'] = max(execution_times['max'], duration)
|
||||
execution_times['total'] += duration
|
||||
execution_times['count'] += 1
|
||||
|
||||
print(f'called {label} in {duration}')
|
||||
return result
|
||||
|
||||
def print_stats():
|
||||
print(f'{label} stats:')
|
||||
print(f' min: {execution_times["min"]}')
|
||||
print(f' max: {execution_times["max"]}')
|
||||
print(f' total: {execution_times["total"]}')
|
||||
print(f' avg: {execution_times["total"] / execution_times["count"]}')
|
||||
|
||||
_track.print_stats = print_stats
|
||||
|
||||
return _track
|
||||
|
||||
|
||||
@track(label='random sleep')
|
||||
def random_sleep():
|
||||
time.sleep(random.random())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
for i in range(10):
|
||||
random_sleep()
|
||||
|
||||
random_sleep.print_stats()
|
||||
@@ -0,0 +1,46 @@
|
||||
# Modify the memoization function to function with unhashable types.
|
||||
|
||||
import functools
|
||||
|
||||
|
||||
cache = dict()
|
||||
|
||||
def memoize(function):
|
||||
def safe_hash(args):
|
||||
'''
|
||||
In the case of unhashable types use the `repr()` to be hashable.
|
||||
'''
|
||||
try:
|
||||
return hash(args)
|
||||
except TypeError:
|
||||
return repr(args)
|
||||
|
||||
@functools.wraps(function)
|
||||
def _memoize(*args):
|
||||
# If the cache is not available, call the function
|
||||
# Note that all args need to be hashable
|
||||
# key = function, safe_hash(args)
|
||||
key = function, args
|
||||
if key not in cache:
|
||||
cache[key] = function(*args)
|
||||
return cache[key]
|
||||
|
||||
return _memoize
|
||||
|
||||
|
||||
@memoize
|
||||
def printer(*args):
|
||||
print(args)
|
||||
|
||||
|
||||
def main():
|
||||
# Should work as expected
|
||||
printer('a', 'b', 'c')
|
||||
|
||||
# Would have issues with the original memoize function because the
|
||||
# parameters are unhashable
|
||||
printer(dict(a=1, b=2, c=3))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,49 @@
|
||||
# Modify the memoization function to have a cache per function instead of a
|
||||
# global one.
|
||||
|
||||
import functools
|
||||
|
||||
|
||||
def memoize(function):
|
||||
# Store the cache as attribute of the function so we can
|
||||
# apply the decorator to multiple functions without
|
||||
# sharing the cache.
|
||||
function.cache = dict()
|
||||
|
||||
def safe_hash(args):
|
||||
'''
|
||||
In the case of unhashable types use the `repr()` to be hashable.
|
||||
'''
|
||||
try:
|
||||
return hash(args)
|
||||
except TypeError:
|
||||
return repr(args)
|
||||
|
||||
@functools.wraps(function)
|
||||
def _memoize(*args):
|
||||
# If the cache is not available, call the function
|
||||
# Note that all args need to be hashable
|
||||
key = safe_hash(args)
|
||||
if key not in function.cache:
|
||||
function.cache[key] = function(*args)
|
||||
return function.cache[key]
|
||||
|
||||
return _memoize
|
||||
|
||||
|
||||
@memoize
|
||||
def printer(*args):
|
||||
print(args)
|
||||
|
||||
|
||||
def main():
|
||||
# Should work as expected
|
||||
printer('a', 'b', 'c')
|
||||
|
||||
# Would have issues with the original memoize function because the
|
||||
# parameters are unhashable
|
||||
printer(dict(a=1, b=2, c=3))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,62 @@
|
||||
# Create a version of `functools.cached_property` that can be recalculated
|
||||
# as needed.
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class _NotFound:
|
||||
pass
|
||||
|
||||
|
||||
class CachedProperty:
|
||||
# Note that this is a very basic version of `functools.cached_property`. If
|
||||
# you wish to use this in production I suggest looking at the original
|
||||
# `cached_property` decorator and implement the locking and conflict
|
||||
# handling as well.
|
||||
|
||||
def __init__(self, func):
|
||||
self.func = func
|
||||
|
||||
def clear(self):
|
||||
self.cache.pop(self.attrname, None)
|
||||
|
||||
def __set_name__(self, owner, name):
|
||||
if not hasattr(owner, '_cache'):
|
||||
owner._cache = dict()
|
||||
# Add a clear method to the owner class
|
||||
setattr(owner, f'clear_{name}', self.clear)
|
||||
|
||||
self.cache = owner._cache
|
||||
self.attrname = name
|
||||
|
||||
def __get__(self, instance, owner=None):
|
||||
if instance is None:
|
||||
return self
|
||||
|
||||
key = self.attrname
|
||||
if key not in self.cache:
|
||||
self.cache[key] = self.func(instance)
|
||||
|
||||
return self.cache[key]
|
||||
|
||||
|
||||
class SomeClass:
|
||||
|
||||
@CachedProperty
|
||||
def current_time(self):
|
||||
return datetime.now()
|
||||
|
||||
|
||||
def main():
|
||||
some_class = SomeClass()
|
||||
a = some_class.current_time
|
||||
b = some_class.current_time
|
||||
assert a == b
|
||||
# Clear the cache. Even though your editor might complain, this method
|
||||
# exists. Can you think of a better API to make the cache clearable?
|
||||
some_class.clear_current_time()
|
||||
c = some_class.current_time
|
||||
assert a != c
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,107 @@
|
||||
# Create a single-dispatch decorator that considers all or a configurable
|
||||
# number of arguments instead of only the first one.
|
||||
import functools
|
||||
import inspect
|
||||
import typing
|
||||
|
||||
|
||||
def fancysingledispatch(*disabled_args, **disabled_kwargs):
|
||||
'''
|
||||
A single-dispatch decorator that considers all or a configurable
|
||||
number of arguments instead of only the first one.
|
||||
|
||||
Args:
|
||||
disabled_args: A list of argument names to ignore.
|
||||
disabled_kwargs: A list of keyword argument names to ignore.
|
||||
|
||||
'''
|
||||
registry = dict()
|
||||
disabled_args = set(disabled_args)
|
||||
for key, value in disabled_kwargs.items():
|
||||
if value:
|
||||
disabled_args.add(key)
|
||||
|
||||
def register(function):
|
||||
key_parts = []
|
||||
for key, type_ in typing.get_type_hints(function).items():
|
||||
if key == 'return':
|
||||
# Ignore the return type
|
||||
continue
|
||||
|
||||
if key in disabled_args:
|
||||
key_parts.append(None)
|
||||
else:
|
||||
key_parts.append(type_)
|
||||
|
||||
registry[tuple(key_parts)] = function
|
||||
return function
|
||||
|
||||
def dispatch(function):
|
||||
signature = inspect.signature(function)
|
||||
|
||||
@functools.wraps(function)
|
||||
def _dispatch(*args, **kwargs):
|
||||
bound = signature.bind(*args, **kwargs)
|
||||
bound.apply_defaults()
|
||||
|
||||
key_parts = []
|
||||
for key, value in bound.arguments.items():
|
||||
if key in disabled_args:
|
||||
key_parts.append(None)
|
||||
else:
|
||||
key_parts.append(type(value))
|
||||
|
||||
key = tuple(key_parts)
|
||||
if key in registry:
|
||||
return registry[key](*args, **kwargs)
|
||||
else:
|
||||
raise TypeError(f'No matching function for {key}')
|
||||
|
||||
_dispatch.register = register
|
||||
register(function)
|
||||
return _dispatch
|
||||
|
||||
return dispatch
|
||||
|
||||
|
||||
@fancysingledispatch(last_name=True)
|
||||
def hello(first_name: str, last_name: str, age: None = None) -> str:
|
||||
return f'Hello {first_name} {last_name}'
|
||||
|
||||
|
||||
# Since this function only differs in the last_name argument, it will
|
||||
# override the previous one. The original `hello` function will never get
|
||||
# called again.
|
||||
@hello.register
|
||||
def first_name_only(
|
||||
first_name: str,
|
||||
last_name: None = None,
|
||||
age: None = None,
|
||||
) -> str:
|
||||
return f'Hello {first_name}'
|
||||
|
||||
|
||||
@hello.register
|
||||
def name_age(first_name: str, last_name: str, age: int) -> str:
|
||||
# Reuse the function above
|
||||
return hello(first_name, last_name) + f', you are {age} years old'
|
||||
|
||||
|
||||
@hello.register
|
||||
def name_age_days(first_name: str, last_name: str, age: float) -> str:
|
||||
days = int((age % 1) * 365)
|
||||
age = int(age)
|
||||
return hello(
|
||||
first_name,
|
||||
last_name
|
||||
) + f', you are {age} years and {days} days old'
|
||||
|
||||
|
||||
def main():
|
||||
print(hello('Rick', 'van Hattem'))
|
||||
print(hello('Rick', 'van Hattem', age=30))
|
||||
print(hello('Rick', 'van Hattem', age=30.5))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,120 @@
|
||||
# Enhance the `type_check` decorator to include additional checks such as
|
||||
# requiring a number to be greater than or less than a given value.
|
||||
import abc
|
||||
import functools
|
||||
import inspect
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
# Note: the exercise erroneously mentions `type_check` instead of
|
||||
# `enforce_type_hints`. For clarity the function was renamed in the text of
|
||||
# the book, but it seems I forgot about the exercise.
|
||||
|
||||
|
||||
class Constraint(abc.ABC):
|
||||
def __call__(self, name, value):
|
||||
return False
|
||||
|
||||
def to_string(self, name, value, constraint):
|
||||
return f'{name}={value!r} must be {constraint}'
|
||||
|
||||
def __str__(self):
|
||||
return self.to_string()
|
||||
|
||||
|
||||
class Gt(Constraint):
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
|
||||
def __call__(self, name, value):
|
||||
if not value > self.value:
|
||||
raise ValueError(self.to_string(name, value))
|
||||
|
||||
def to_string(self, name='x', value='x', constraint='x > y'):
|
||||
return super().to_string(name, value, f'greater than {self.value}')
|
||||
|
||||
|
||||
class Between(Constraint):
|
||||
def __init__(self, min_value, max_value):
|
||||
self.min_value = min_value
|
||||
self.max_value = max_value
|
||||
|
||||
def __call__(self, name, value):
|
||||
if not self.min_value < value < self.max_value:
|
||||
raise ValueError(self.to_string(name, value))
|
||||
|
||||
def to_string(self, name='x', value='x', constraint='x < y < z'):
|
||||
return super().to_string(
|
||||
name,
|
||||
value,
|
||||
f'between {self.min_value} and {self.max_value}',
|
||||
)
|
||||
|
||||
|
||||
def enforce_type_hints(**constraint_kwargs):
|
||||
def _enforce_type_hints(function):
|
||||
# Construct the signature from the function which contains
|
||||
# the type annotations
|
||||
signature = inspect.signature(function)
|
||||
|
||||
@functools.wraps(function)
|
||||
def __enforce_type_hints(*args, **kwargs):
|
||||
# Bind the arguments and apply the default values
|
||||
bound = signature.bind(*args, **kwargs)
|
||||
bound.apply_defaults()
|
||||
|
||||
for key, value in bound.arguments.items():
|
||||
param = signature.parameters[key]
|
||||
# The annotation should be a callable
|
||||
# type/function so we can cast as validation
|
||||
if param.annotation:
|
||||
try:
|
||||
bound.arguments[key] = param.annotation(value)
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
f'{key} must be {param.annotation.__name__}'
|
||||
)
|
||||
|
||||
if key in constraint_kwargs:
|
||||
constraint_kwargs[key](key, value)
|
||||
|
||||
return function(*bound.args, **bound.kwargs)
|
||||
|
||||
return __enforce_type_hints
|
||||
|
||||
return _enforce_type_hints
|
||||
|
||||
|
||||
@enforce_type_hints(bacon=Gt(0), eggs=Between(1, 4))
|
||||
def sandwich(bacon: float, eggs: int):
|
||||
print(f'bacon: {bacon!r}, eggs: {eggs!r}')
|
||||
|
||||
|
||||
def test_sandwich():
|
||||
sandwich(1, 2)
|
||||
sandwich(5, 3)
|
||||
|
||||
try:
|
||||
sandwich(1, 0)
|
||||
except ValueError as e:
|
||||
assert str(e) == 'eggs=0 must be between 1 and 4'
|
||||
else:
|
||||
assert False
|
||||
|
||||
try:
|
||||
sandwich(1, 5)
|
||||
except ValueError as e:
|
||||
assert str(e) == 'eggs=5 must be between 1 and 4'
|
||||
else:
|
||||
assert False
|
||||
|
||||
try:
|
||||
sandwich(0, 5)
|
||||
except ValueError as e:
|
||||
assert str(e) == 'bacon=0 must be greater than 0'
|
||||
else:
|
||||
assert False
|
||||
|
||||
if __name__ == '__main__':
|
||||
pytest.main(['-vv'])
|
||||
Reference in New Issue
Block a user