[Python-Dev] cpython: Rename contextlib.ignored() to contextlib.ignore(). (original) (raw)
Nick Coghlan ncoghlan at gmail.com
Thu Oct 17 17:26:16 CEST 2013
- Previous message: [Python-Dev] cpython: Rename contextlib.ignored() to contextlib.ignore().
- Next message: [Python-Dev] cpython: Rename contextlib.ignored() to contextlib.ignore().
- Messages sorted by: [ date ] [ thread ] [ subject ] [ author ]
On 17 October 2013 01:24, Barry Warsaw <barry at python.org> wrote:
On Oct 16, 2013, at 08:31 AM, Eric Snow wrote:
When a module's maintainer makes a decision on a relatively insignificant addition to the module, I'd expect little resistance or even comment (the original commit was months ago). That's why I'm surprised by the reaction to this change. It just seems like the whole thing is being blown way out of proportion to the detriment of other interesting problems. Sure, things could have been done differently. But in this case it's not that big a deal. Any project as big and diverse as Python needs a hierarchical structure of trust and responsibility. I see it roughly as core dev < module maintainer < release manager < bdfl delegate < bdfl. However, it's imperative to remain vigilantly transparent so that everyone understands the rationale and motivation behind a change, even if they disagree with it. Trust is extended upwards when this transparency is extended downloads. "'Cause I said so" only works at the top of the chain. ;)
Right, and that's why I never opted out of the thread entirely, despite being seriously tempted at times :)
It's also caused me to reflect on a few things over the past few days, including why the bar for contextlib is (at this point in time) comparatively lower than the one for functools, collections and itertools. I was also asked a very good question off-list as to why TransformDict escalated to needing a PEP, while I still believe a couple of issues is adequate for keeping this minor addition to contextlib (for the record: the patch Zero posted to http://bugs.python.org/issue19266 has been committed, so the pattern is now called "suppress").
An interesting lightning talk at BrisPy last night also included a key point: the speaker spent a lot of time creating custom internal tools for process engineers to use as an alternative to getting expensive Matlab licenses, and his experience was that object-oriented programming was just beyond what most process engineers could cope with in terms of writing their own code.
If you look at the history of various constructs in Python, it takes years for idioms and patterns to build up around them, even after the core syntax additions is made to the language. Once those idioms are in place, though, you get to a point where you can first teach people how to use them effectively and only later teach them how to build their own. Because the patterns have been enshrined in things with names, you also don't need to be able to recognise them on sight - you have something to look up the first you set, rather than having to interpret a particular shape of code as indicating a particular idiom.
This pattern recurs again and again:
- you learn how to call functions before you learn how to write them
- you learn how to instantiate and use classes before you learn how to write new ones
- you learn how to import modules before you learn how to write your own
- you learn how to iterate before you learn how to write iterators
- you learn how to apply decorators before you learn how to build them
- you learn how to invoke context managers before you learn how to write your own
- you often don't even have to learn how to use descriptors per se, as it's often hidden in learning to use decorators. Learning to create your own then lets you realise how much of what you thought you knew about classes is actually just based on the way a few standard descriptors work.
- you also often don't even have to learn how to use metaclasses, as it's often hidden behind class inheritance (e.g. from abc.ABC or enum.Enum, or from a base class in an ORM or web framework). Learning to create your own metaclasses, on the other hand, can be completely mindbending.
Over time, this progressively lowers the barrier to entry for Python programming, as the intent is to enable users to do more without necessarily needing to learn (much) more, as well as allowing code authors to more clearly communicate their intent to code readers by using named patterns rather than leaving them implicit in the code structure.
Exception handling, however, is a notable exception(!) to that general pattern. Appropriately handling exceptions is actually harder than raising them:
raise Exception
vs:
try:
<code that may raise an exception>
except Exception:
<do something about it>
The introduction of context managers several years ago basically provides an opportunity to change that over time: by standardising various common try/except idioms, it becomes feasible to postpone the explanations of the underlying details.
Compared to the extensive toolkits we already have for functional abstractions, iterator based abstractions and data containers, the general purpose context manipulation idioms are still quite weak. contextlib.closing was part of the initial toolkit in Python 2.5, while contextlib.nested was later removed as fundamentally broken in the presence of resources that acquire their resource in init (like files).
contextlib.contextmanager is a tool for writing context managers not using them, as is contextlib.ContextDecorator.
contextlib.ExitStack is similarly on the "power tool" side of the fence, being the context management equivalent of the iter() and next() builtins (letting you cleanly manipulate the individual steps of a with statement, just as those two builtins let you manipulate the individual steps of a for loop).
By contrast, suppress() and redirect_stdout() are the first general purpose context managers added to contextlib since its incarnation in Python 2.5 (although there have been many various domain specific context manager additions elsewhere in the standard library).
It's worth unpacking the "simple" six line definition of contextlib.suppress, to see how many Python concepts it actually requires you to understand:
@contextmanager
def suppress(*exceptions):
try:
yield
except exceptions:
pass
By my count, it's at least:
- defining functions
- applying decorators
- accepting arbitrary positional arguments
- generators
- implementing context managers with generators
- exception handling
- using dynamic tuples for exception handling
- the interaction between generators, exception handling and context management
This function is short because of the information density provided by the various constructs it uses, but it's far from being simple.
We then turn to the motivating example for the addition, taking an eight (or five) line construct and turning it into a four (or even two!) line construct:
First, the eight line version:
try:
os.remove("somefile.tmp")
except FileNotFoundError:
pass
try:
os.remove("someotherfile.tmp")
except FileNotFoundError:
pass
And the five line version (which only applies in this particular example because the two commands are almost identical, and carries the cost of conveying less information in the traceback purely through line position):
for name in ("somefile.tmp", "someotherfile.tmp"):
try:
os.remove(name)
except FileNotFoundError:
pass
Now, the four line version:
with suppress(FileNotFoundError):
os.remove("somefile.tmp")
with suppress(FileNotFoundError):
os.remove("someotherfile.tmp")
And even a two line version:
with suppress(FileNotFoundError): os.remove("somefile.tmp")
with suppress(FileNotFoundError): os.remove("someotherfile.tmp")
With the try/except version, it's much harder to tell at a glance what the code is doing, because significant parts of the intent (the specific exception being caught and the "pass" keyword that indicates it is being suppressed) are hidden down and to the right.
Now, the other point raised in the thread is that there's a more general idiom that's potentially of interest, which is to keep track of whether or not an exception was thrown. I actually agree with that, and it's something I now plan to explore in contextlib2 and then (if I decide I actually like the construct) a PEP for Python 3.5. Specifically, it would be a replacement for the existing search loop idiom that was based on a context manager in order to generalise to more than one level of loop nesting. However, the potential ramifications of offering such a feature are vastly greater than those of the far more limited contextlib.suppress context manager (which only covers one very specific exception handling pattern), so it would need to go through the PEP process.
it would be something along the lines of the following (note: I'm not proposing this for 3.4. There's no way I'd propose it or anything like it for the standard library without trialling it in contextlib2 first, and letting it bake there for at least few months. It may even make sense to tie the failed/missing idiom to suppress() as was suggested in this thread rather than to the new construct):
# Arbitrarily nested search loop
with exit_label() as found:
for i in range(x):
for j in range(y):
if matches(i, j):
found.exit((i, j))
if found:
print(found.value)
# Delayed exception handling
with exit_label(OSError) as failed:
os.remove("somefile.tmp")
# Do other things...
if failed:
print(failed.exc)
with exit_label(ValueError) as missing:
location = data.find(substr)
if missing:
# Implementation sketch
def exit_label(exception):
return _ExitLabel(exception)
class _ExitLabel:
def __init__(exception):
if not exception:
class ExitToLabel(Exception):
pass
exception = ExitToLabel
self._exception = exception
self._sentinel = sentinel = object()
self._value = sentinel
self._exc = sentinel
self._entered = False
def __enter__(self):
if self._entered:
raise RuntimeError("Cannot reuse exit label")
self._entered = True
return self
def __exit__(self, exc_type, exc_value, exc_tb):
self._exc = exc_value
if isinstance(exc_value, self._exceptions):
traceback.clear_frames(exc_value.__traceback__)
return True # Suppress the exception
return False # Propagate the exception
def __bool__(self):
return self._exc and self._exc is not self._sentinel
def exit(self, value=None):
self._value = value
raise self._exception
@property
def value(self):
if self._value is self._sentinel:
raise RuntimeError("Label value not yet set")
return self._value
@property
def exc(self):
if self._exc is self._exc:
raise RuntimeError("Label exception result not yet set")
return self._exc
Regards, Nick.
-- Nick Coghlan | ncoghlan at gmail.com | Brisbane, Australia
- Previous message: [Python-Dev] cpython: Rename contextlib.ignored() to contextlib.ignore().
- Next message: [Python-Dev] cpython: Rename contextlib.ignored() to contextlib.ignore().
- Messages sorted by: [ date ] [ thread ] [ subject ] [ author ]