There should be one — and preferably only one — obvious way to do it.
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
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... ↩
from Planet Python
via read more
No comments:
Post a Comment