31
loading...
This website collects cookies to deliver better user experience
def add(a: int, b: int) -> int:
return a + b
logger = logging.getLogger(__name__)
def add(a: int, b: int) -> int:
result = return a + b
logger.debug("adding %s and %s gives %s", a, b, result)
return result
add
method:logger = logging.getLogger(__name__)
def add(a: int, b: int) -> int:
t_start = time.perf_counter()
result = return a + b
t_end = time.perf_counter()
logger.debug("adding %s and %s gives %s", a, b, result)
took = t_end - t_start
logger.debug("took %s seconds", took)
return result
stdout
testing is probably not much of an issue at this stage, but if we're storing logs remotely our tests are at risk of becoming slow, fragile, and we're likely spamming logs with useless messages every time we run our tests. It's a slippery slope towards spaghetti code - there must be a better way!# service.py
from typing import Protocol
class AddServiceProtocol(Protocol):
"Represents functionality of adding two numbers."
def add(self, a: int, b: int) -> int:
...
int
values and we return an int
value representing their sum. Given the protocol, the concrete implementation might look as follows:# service.py
class AddService:
"Implements AddServiceProtocol."
def add(self, a: int, b: int) -> int:
return a + b
mypy
will be able to reason about protocols and their implementations based on method signatures alone. Think - abstract base classes light. Or, think - pythonic duck-typing augmented with static verification tooling.add
method of the main implementation let's create a separate implementation that will satisfy the service protocol while also wrapping the service itself:# service.py
class LoggingAddService:
"""
Implements AddServiceProtocol. Wraps AddService and adds basic logging.
"""
def __init__(self, service: AddServiceProtocol, logger: Logger) -> None:
self._inner = service
self._logger = logger
def add(self, a: int, b: int) -> int:
result = self._inner.add(a, b)
self._logger.debug("[add] adding %s and %s gives %s", a, b, result)
return result
LoggingAddService
with a reference to an instance of a class that fulfills the AddServiceProtocol
contract and an instance of a logging.Logger
. When called, the add
methodLoggingAddService
runs the add
method on the _inner
class, while also logging the details of the call using the reference to the _logger
.AddServiceProtocol
. Since we can, let's create another middleware, one that records how long it takes to add numbers:# service.py
class TimingAddService:
"""
Implements AddServiceProtocol. Wraps AddService and adds timing of method calls.
"""
def __init__(self, service: AddServiceProtocol, logger: Logger) -> None:
self._inner = service
self._logger = logger
def add(self, a: int, b: int) -> int:
start = time.perf_counter()
result = self._inner.add(a, b)
end = time.perf_counter()
elapsed = end - start
self._logger.debug(f"[add] took {elapsed:0.8f} seconds")
return result
# main.py
import typer
from logger import std_out_logger
from service import AddService, AddServiceProtocol, LoggingAddService, TimingAddService
def main(a: int, b: int, debug: bool = False, timing: bool = False) -> None:
"""
Adding 'a' to 'b' made easy!
"""
service: AddServiceProtocol = AddService()
if timing:
service = TimingAddService(service=service, logger=std_out_logger("timing"))
if debug:
service = LoggingAddService(service=service, logger=std_out_logger("logging"))
print(service.add(a, b))
if __name__ == "__main__":
typer.run(main)
main
function is where we wire the parts of our application together. The individual components don't need to know about each other otherwise - all they care about is the contract represented by the protocol and whatever additional dependencies they require to do their thing. The various middlewares are layered on top of the core service based on the values of debug
and timing
flags. The main function becomes the only place where we use the conditionals that toggle the timing and logging features - just imagine what our code would look like if these had to be colocated with our business logic!A working example of the protocol-based implementation can be found on GitHub.
This post was originally posted on my blog.
Cover photo by Ryan Quintal.