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:
Rick van Hattem
2022-09-05 00:04:00 +02:00
parent 82cf71ed1c
commit 500a31afac
34 changed files with 742 additions and 0 deletions
@@ -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()
+107
View File
@@ -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()
+120
View File
@@ -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'])