On Nov 19, 2019, at 08:04, Random832
wrote: On Tue, Nov 19, 2019, at 07:03, Paul Moore wrote: That sounds reasonable, with one proviso. I would *strongly* object to calling context managers that conform to the new expectations "well behaved", and by contrast implying that those that don't are somehow "misbehaving". File objects have been considered as perfectly acceptable context managers since the first introduction of context managers (so have locks, and zipfile objects, which might also fall foul of the new requirements). Suddenly deeming them as "misbehaving" is unreasonable.
The problem is that if this model is perfectly okay, then *there's no reason for __enter__ to exist at all*. Why doesn't *every* context manager just do *everything* in __init__? I think it's clear that something was lost between the design and the implementation.
Forget about context managers for a second. A class can bind attributes in __new__ and return a fully initialized object. If that’s perfectly ok, why doesn’t every class do everything in __new__, in which case there’s no reason for __init__ to exist at all? But in fact, it’s usually a good idea to bind your mutable attributes and attributes that you expect subclasses to override in __init__. This signals your intentions better, and makes it easier to use your class with a range of optional utilities. But it’s not mandatory, and sometimes there are good reasons to violate it. And yet, despite the split being entirely up to the class writers and there being no hard rules about it, it’s still useful. So, why can’t much the same be true for context managers? It’s usually a good idea to do resource acquisition in __enter__, and also to have __init__ never raise. This signals your intentions better, and makes it easier to use your cm with a range of optional utilities, but it’s not mandatory, and sometimes there are good reasons not to. A language could certainly live without the distinction, but it could also live without two-phase initialization. (C++ merges all of __new__, __init__, and __enter__ into one constructor, and __del__ and __exit__ into one destructor, and “resource acquisition is initialization” would work perfectly if not for half the stdlib and 80% of the third-party ecosystem being inherited without wrappers from C, and therefore not exception safe…). But that doesn’t mean a language can’t benefit from the distinction. For example, notice that Python doesn’t have C++‘s complicated member destructor rules or ObjC’s different kinds of attributes to manage ARC, and yet we can still get away with having resources with dynamic lifetimes (tied to an owning object rather than a lexical scope). That works because resources can easily be used manually, rather than every resource being a context manager and only usable that way; otherwise we’d need language or library support for managing your attribute context. It isn’t perfect (you can’t screw up RAII in C++ if you only use RAII objects; you can easily screw up cleanup in Python even with objects that have cm support), but it mostly works. Arguably we’re getting half the benefit of an RAII system with only a quarter of the costs. (And part of the cost we’re skipping may be that it’s nearly impossible to add non-refcounting GC to an implementation of C++ or ObjC, but pretty easy for Python.) I can imagine other designs that might have the same benefit and still not require __enter__. For example, make ExitStack syntactic and then eliminate the cm machinery: scope: f1 = open(fn1) defer f1.close() f2 = open(fn2) defer f2.close() More verbose, but simpler, and it means you don’t need to write anything to make an object manageable; the name “close” is just a convention rather than machinery. (For cleanup that’s not a single expression, you’d have to factor it out into a function or method—but that’s at worst equivalent to writing the __exit__ method today, and now you’d only need that for complicated resource managers rather than for all of them.) Or just go back to Python 2.2 and mandate deterministic destruction (and if that means Jython can’t be simple and efficient, so be it) and then build from there. Now all you need is a way to create scopes without manually defining and calling nested functions and you’re done. But if we’re really going to rethink resource management from scratch, I don’t think we’re talking about Python anymore anyway.