
On Tue, Jan 26, 2016 at 10:34:55AM +1100, Steven D'Aprano wrote:
On Wed, Jan 20, 2016 at 05:04:21PM -0800, Guido van Rossum wrote:
On Wed, Jan 20, 2016 at 4:10 PM, Steven D'Aprano <steve@pearwood.info> wrote: [...]
(I'm saving my energy for Eiffel-like require/ensure blocks *wink*).
Now you're making me curious.
Okay, just to satisfy your curiosity, and not as a concrete proposal at this time, here is a sketch of the sort of thing Eiffel uses for Design By Contract.
Each function or method has an (optional, but recommended) pre-condition and post-condition. Using a hybrid Eiffel/Python syntax, here is a toy example:
class Lunch: def __init__(self, arg): self.meat = self.spam(arg)
def spam(self, n:int=5): """Set the lunch meat to n servings of spam.""" require: # Assert the pre-conditions of the method. assert n >= 1 ensure: # Assert the post-conditions of the method. assert self.meat.startswith('Spam') if ' ' in self.meat: assert ' spam' in self.meat # main body of the method, as usual serves = ['spam']*n serves[0] = serves.title() self.meat = ' '.join(serves)
The require block runs before the body of the method, and the ensure block runs after the body, but before the method returns to the caller. If either fail their assertions, the method fails and raises an exception.
Benefits:
- The pre- and post-conditions make up (part of) the method's contract, which is part of the executable documentation of the method. Documentation tools can extract the ensure and require sections as present them as part of the API docs.
- The compiler can turn the contract checking on or off as needed, with the ensure/require sections handled independently.
- Testing pre- and post-conditions is logically separate from the method's implementation. This allows the implementation to vary while keeping the contract the same.
- But at the same time, the contract is right there with the method, not seperated in some potentially distant part of the code base.
One thing I immediately thought of was using decorators. def requires(*conditions): def decorator(func): # TODO: Do some hackery such that the signature of wrapper # matches the signature of `func`. def wrapper(*args, **kwargs): for condition in conditions assert eval(condition, {}, locals()) return func(*args, **kwargs) return wrapper return decorator def ensure(*conditions): def decorator(func): def wrapper(*args, **kwargs): try: return func(*args, **kwargs) finally: for condition in conditions: assert eval(condition, {}, locals()) return decorator Maybe do some checking for the optimization-level flag, and replace the decorator function with `return func` instead of another wrapper? The `ensure` part isn't quite to my liking yet, but I think that the `ensure` should have no need to access internal variables of the function, but only the externally visible state. (This somewhat mimics what I'm trying to fiddle around with in my own time: writing a decorator that does run-time checking of argument and return types of functions.)