There should be one — and preferably only one — obvious way to do it.
Tim Peters, “The Zen of Python”
Moshe wrote a blog post a couple of days
ago which neatly constructs a wonderful
little coding example from a scene in a movie. And, as we know from the Zen of
Python quote, there should only be one obvious way to do something in Python.
So my initial reaction to his post was of course to do it differently — to
replace an __init__
method with the new @dataclasses.dataclass
decorator.
But as I thought about the code example more, I realized there are a number of things beyond just dataclasses that make the difference between “toy”, example-quality Python, and what you’d do in a modern, professional, production codebase today.
So let’s do everything the second, not-obvious way!
There’s more than one way to do it
Larry Wall, “The Other Zen of Python”
Getting started: the __future__
is now
We will want to use type annotations. But, the Guard
and his friend are very
self-referential, and will have lots of annotations that reference things that
come later in the file. So we’ll want to take advantage of a future feature of
Python, which is to say, Postponed Evaluation of
Annotations. In addition to the
benefit of slightly improving our import time, it’ll let us use the nice type
annotation syntax without any ugly quoting, even when we need to make forward
references.
So, to begin:
1 |
|
Doors: safe sets of constants
Next, let’s tackle the concept of “doors”. We don’t need to gold-plate this
with a full blown Door
class with instances and methods - doors don’t have
any behavior or state in this example, and we don’t need to add it. But, we
still wouldn’t want anyone using using this library to mix up a door or
accidentally plunge to their doom by accidentally passing "certian death"
when they meant certain
. So a Door
clearly needs a type of its own, which
is to say, an Enum:
1 2 3 4 5 |
|
Questions: describing type interfaces
Next up, what is a “question”? Guards expect a very specific sort of value as
their question
argument and we if we’re using type annotations, we should
specify what it is. We want a Question
type that defines arguments for
each part of the universe of knowledge that these guards understand. This
includes who they are themselves, who the set of both guards are, and what the
doors are.
We can specify it like so:
1 2 3 4 5 6 7 |
|
The most flexible way to define a type of thing you can call using mypy
and
typing
is to define a Protocol
with a __call__
method and nothing
else1. We could also describe this type as Question = Callable[[Guard,
Sequence[Guard], Door], bool]
instead, but as you may be able to infer, that
doesn’t let you easily specify names of arguments, or keyword-only or
positional-only arguments, or required default values. So
Protocol
-with-__call__
it is.
At this point, we also get to consider; does the questioner need the ability to
change the collection of doors they’re passed? Probably not; they’re just
asking questions, not giving commands. So they should receive an immutable
version, which means we need to import Sequence
from the typing
module and
not List
, and use that for both guards
and doors
argument types.
Guards and questions: annotating existing logic with types
Next up, what does Guard
look like now? Aside from adding some type
annotations — and using our shiny new Door
and Question
types — it looks
substantially similar to Moshe’s version:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Similarly, the question that we want to ask looks quite similar, with the addition of:
- type annotations for both the “outer” and the “inner” question, and
- using
Door.castle
for our comparison rather than the string"castle"
- replacing
List
withSequence
, as discussed above, since the guards in this puzzle also have no power to change their environment, only to answer questions. - using the
[var] = value
syntax for destructuring bind, rather than the more subtlevar, = value
form
1 2 3 4 5 6 7 8 9 |
|
Eliminating global state: building the guard post
Next up, how shall we initialize this collection of guards? Setting a couple of global variables is never good style, so let’s encapsulate this within a function:
1 2 3 4 5 6 7 |
|
Defining the main point
And finally, how shall we actually have this execute? First, let’s put this in
a function, so that it can be called by things other than running the script
directly; for example, if we want to use
entry_points
to expose this as a script. Then, let's put it in a "__main__"
block, and
not just execute it at module scope.
Secondly, rather than inspecting the output of each one at a time, let’s use
the all
function to
express that the interesting thing is that all of the guards will answer the
question in the affirmative:
1 2 3 4 5 6 |
|
Appendix: the full code
To sum up, here’s the full version:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
|
Acknowledgments
I’d like to thank Moshe Zadka for the post that inspired this, as well as Nelson Elhage, Jonathan Lange, Ben Bangert and Alex Gaynor for giving feedback on drafts of this post.
-
I will hopefully have more to say about
typing.Protocol
in another post soon; it’s the real hero of the Mypy saga, but more on that later... ↩