The topic of this post is similar magic enablers - "context managers", defined in PEP-343. I will also demonstrate one idiosyncratic context manager example.
To begin with, it is important to note that Python reasonably suggests that when a developer modifies the behavior (i.e. the semantics) of something, it is still done somewhat in line with the original syntax. The syntax therefore implies a certain direction in which a particular behavior could be shifted.
For instance, it would be rather awkward if you override the dot operator on some class in such way that it throws an exception upon attribute access:
class Awkward:It is a possible but very unusual way of interpreting the meaning of a "dot", which is originally a lookup of an instance attribute.
def __getattr__(self, n):
raise Exception(n)
Awkward().foo # throws Exception("foo")
Having this in mind we proceed to the context managers. They originate from the typical resource-accessing syntactical pattern:
r = allocate_resource(...)Such code is encountered so often, that it indeed was a good idea to wrap it into a simpler syntactical primitive. Context manager in Python is an object whose responsibility is to deallocate the resource when it comes out of its scope (or, context). The developer should only be concerned with allocating a resource and using it:
try:
r.use()
finally:
r.deallocate()
with allocated_resource(...) as r:In simple terms, the above translates to:
r.use(...)
ctx_mgr = ResourceAllocator(...)I note a few obvious things first:
r = ctx_mgr.__enter__()
try:
r.use()
finally:
ctx_mgr.__exit__()
- Context manager is any instance that supports __enter__ and __exit__ methods (aka context manager protocol).
- A specific ResourceAllocator must be defined for a particular kind of resource. The syntactical simplification does not come for free.
- Context managers are one-time objects, which are created and disposed of as wrappers around the resource instances they protect.
lock = threading.Lock()which is identical to
with lock:
# do something while the lock is acquired
lock = threading.Lock()Finally, I proceed to an example of my own.
lock.acquire()
try:
# do something while the lock is acquired
finally:
lock.release()
See, I tend to write a lot of self-tests and I love Python for forcing me to. And some of the tests require that you check for a failure. Long ago I used to write code like this:
try:which made my test code very noisy. I have even posted a suggestion that a syntactical primitive is introduced to the language just for that. It was rejected (duh !).
test_specific_failure_condition()
except SpecificError, e:
assert str(e) == "error message"
else:
assert False, "should have thrown SpecificError"
And then I wrote a simple "expected" context manager which makes exactly the same thing for me every day now:
with expected(SpecificError("error message")):See how much noise has been eliminated ? How much clearer the test code becomes ? It is not a particularly "resource-protecting" kind of thing, but still in line with the original syntax, just like I said above.
test_specific_failure_condition()
The "expected" context manager source code is available here, please feel free to use it if you like.
To be continued...
4 comments:
Thanks! expected context manager is a good idea, will use it
For testing exceptions, it's better to use the assertRaises method of TestCase. And in general, it's a good idea to use the unittest module for unit testing.
This was a nice conversational journey from programming situation to practical convenience feature in Python. These kinds of articles are very helpful for people like me who are coming from other languages to Python.
I have one question. This article is quite old and was marked "to be continued..." So, was it?
(Admittedly, I have a related bad habit on my own blog.)
Ahem... I'm sorry if it comes as a disappointment, but "to be continued" was a general closing line, not a promise. I will gladly post something else Python-related as soon as anything comes to mind.
Post a Comment