[Python-ideas] Why is design-by-contracts not widely adopted?
Steven D'Aprano
steve at pearwood.info
Sun Sep 30 10:28:45 EDT 2018
On Fri, Sep 28, 2018 at 01:49:01PM +0100, Paul Moore wrote:
> There's clearly a number of trade-offs going on here:
>
> * Conditions should be short, to avoid clutter
> * Writing helper functions that are *only* used in conditions is more
> code to test or get wrong
This is no different from any other refactoring. If you have a function
that checks its input:
def spam(arg):
if condition(arg) and other_condition(arg) or alt_condition(arg):
raise ValueError
and refactor it to a helper:
def invalid(arg):
return condition(arg) and other_condition(arg) or alt_condition(arg)
def spam(arg):
if invalid(arg):
raise ValueError
how is that a bad thing just because we call it a "precondition" instead
of calling it "error checking"?
Of course we don't necessarily want the proliferation of masses and
masses of tiny helper functions, but nor should we fear them. Helpers
should carry their own weight, and if they do, we should use them.
Whether they are used for contracts or not makes zero difference.
> * Sometimes it's just plain hard to express a verbal constraint in code
Indeed.
People seem to be arguing against some strawman of "Design By Contract
is a magic bullet that solves every imaginable problem". Of course it
doesn't. Some constraints are too hard to specify as code. Okay, then
don't do that.
DbC isn't "all or nothing". If you can't write a contract for something,
don't. You still get value from the contracts you do write.
[...]
> But given that *all* the examples I've seen of contracts have this
> issue (difficult to read expressions) I suspect the problem is
> inherent.
Are you specifically talking about *Python* examples? Or contracts in
general?
I don't know Eiffel very well, but I find this easy to read and
understand (almost as easy as Python). The trickiest thing is the
implicit "self".
put (x: ELEMENT; key: STRING) is
-- Insert x so that it will be retrievable through key.
require
count <= capacity
not key.empty
do
... Some insertion algorithm ...
ensure
has (x)
item (key) = x
count = old count + 1
end
https://www.eiffel.com/values/design-by-contract/introduction/
Here are a couple of examples from Cobra:
def fraction( numer as int, denom as int) as float
require
numer > 0
denom <> 0
body
...
def bumpState( incr as int) as int
require
incr > 0
ensure
result >= incr
.state = old.state + incr
body
.state += incr
return .state
http://cobra-language.com/trac/cobra/wiki/Contracts
If you find them difficult to read, I don't know what to say :-)
> Another thing that I haven't yet seen clearly explained. How do these
> contracts get *run*? Are they checked on every call to the function,
Yes, that's the point of them. In development they're always on. Every
time you run your dev code, it tests itself.
> even in production code?
That's up to you, but typical practice is to check pre-conditions (your
input) but not post-conditions (your output) in production.
> Is there a means to turn them off? What's the
> runtime overhead of a "turned off" contract (even if it's just an
> if-test per condition, that can still add up)?
Other languages may offer different options, but in Eiffel, contracts
checking can be set to:
no: assertions have no run-time effect.
require: monitor preconditions only, on routine entry.
ensure: preconditions on entry, postconditions on exit.
invariant: same as ensure, plus class invariant on both entry
and exit for qualified calls.
all: same as invariant, plus check instructions,
loop invariants and loop variants.
You can set the checking level globally, or class-by-class. The default
is to check only preconditions. That is, for methods to validate their
inputs. Quoting from the Eiffel docs:
When releasing the final version of a system, it is usually
appropriate to turn off assertion monitoring, or bring it down
to the ``require`` level. The exact policy depends on the
circumstances; it is a trade off between efficiency considerations,
the potential cost of mistakes, and how much the developers and
quality assurance team trust the product. When developing the
software, however, you should always assume -- to avoid loosening
your guard -- that in the end monitoring will be turned off.
https://www.eiffel.org/doc/eiffel/ET-_Design_by_Contract_%28tm%29%2C_Assertions_and_Exceptions
The intention for Python would be similar:
- we ought to be able disable contract checking globally;
- and preferrably on a case-by-case basis;
- a disabled contact ought to be like a disabled assertion,
that is, completely gone with no runtime effect at all;
- but due to the nature of Python's execution model, there will
probably be some (small) overhead at the time the function is
created, but not when the function is called.
Of course the overhead will depend on the implementation.
> And what happens if a
> contract fails - is it an exception/traceback (which is often
> unacceptable in production code such as services)?
What happens when any piece of error checking code fails and raises an
exception?
In the case of Python, a failed contract would be an exception. What you
do with the exception is up to you. The *intention* is that a failed
contract is a bug, so what you *ought to do* is fix the bug. But you
could catch it, retry the operation, restart the service, or whatever.
That's worth repeating: *contracts aren't for testing end-user input*
but for checking internal program state. A failed contract ought to be
considered a bug. Any *expected error state* (such as a missing file, or
bad user input, or a network outage) shouldn't be treated as a contract.
--
Steve
More information about the Python-ideas
mailing list