Programming by Contract for Python, by Terence Way
This commit is contained in:
parent
dec7e745af
commit
613e70e3b8
|
@ -0,0 +1,418 @@
|
||||||
|
PEP: 316
|
||||||
|
Title: Programming by Contract for Python
|
||||||
|
Version: $Revision$
|
||||||
|
Last-Modified: $Date$
|
||||||
|
Author: Terence Way <terry@wayforward.net>
|
||||||
|
Status: Draft
|
||||||
|
Type: Standards Track
|
||||||
|
Content-Type: text/x-rst
|
||||||
|
Created: 02-May-2003
|
||||||
|
Python-Version:
|
||||||
|
Post-History:
|
||||||
|
|
||||||
|
|
||||||
|
Abstract
|
||||||
|
========
|
||||||
|
|
||||||
|
This submission describes programming by contract for Python.
|
||||||
|
Eiffel's Design By Contract(tm) is perhaps the most popular use of
|
||||||
|
programming contracts [#dbc]_.
|
||||||
|
|
||||||
|
Programming contracts extends the language to include invariant
|
||||||
|
expressions for classes and modules, and pre- and post-condition
|
||||||
|
expressions for functions and methods.
|
||||||
|
|
||||||
|
These expressions (contracts) are similar to assertions: they must
|
||||||
|
be true or the program is stopped, and run-time checking of the
|
||||||
|
contracts is typically only enabled while debugging. Contracts
|
||||||
|
are higher-level than straight assertions and are typically
|
||||||
|
included in documentation.
|
||||||
|
|
||||||
|
|
||||||
|
Motivation
|
||||||
|
==========
|
||||||
|
|
||||||
|
Python already has assertions, why add extra stuff to the language to
|
||||||
|
support something like contracts? The two best reasons are 1) better,
|
||||||
|
more accurate documentation, and 2) easier testing.
|
||||||
|
|
||||||
|
Complex modules and classes never seem to be documented quite right.
|
||||||
|
The documentation provided may be enough to convince a programmer to
|
||||||
|
use a particular module or class over another, but the programmer
|
||||||
|
almost always has to read the source code when the real debugging
|
||||||
|
starts.
|
||||||
|
|
||||||
|
Contracts extend the excellent example provided by the ``doctest``
|
||||||
|
module [#doctest]_. Documentation is readable by programmers, yet has
|
||||||
|
executable tests embedded in it.
|
||||||
|
|
||||||
|
Testing code with contracts is easier too. Comprehensive contracts
|
||||||
|
are equivalent to unit tests [#xp]_. Tests exercise the full range of
|
||||||
|
pre-conditions, and fail if the post-conditions are triggered.
|
||||||
|
Theoretically, a correctly specified function can be tested completely
|
||||||
|
randomly.
|
||||||
|
|
||||||
|
So why add this to the language? Why not have several different
|
||||||
|
implementations, or let programmers implement their own assertions?
|
||||||
|
The answer is the behavior of pre-conditions under inheritance.
|
||||||
|
|
||||||
|
If Alice produces a class library protected by her own assertions
|
||||||
|
package, Bob cannot derive classes from Alice's library and weaken the
|
||||||
|
pre-conditions, unless both have agreed on an assertions system that
|
||||||
|
supports weakening pre-conditions. The natural place to find this
|
||||||
|
assertions system is in the language's run-time library.
|
||||||
|
|
||||||
|
|
||||||
|
Specification
|
||||||
|
=============
|
||||||
|
|
||||||
|
The docstring of any module or class can include invariant contracts
|
||||||
|
marked off with a line that starts with the keyword ``inv`` followed
|
||||||
|
by a colon (:). Whitespace at the start of the line and around the
|
||||||
|
colon is ignored. The colon is either immediately followed by a
|
||||||
|
single expression on the same line, or by a series of expressions on
|
||||||
|
following lines indented past the ``inv`` keyword. The normal Python
|
||||||
|
rules about implicit and explicit line continuations are followed
|
||||||
|
here. Any number of invariant contracts can be in a docstring.
|
||||||
|
|
||||||
|
Some examples::
|
||||||
|
|
||||||
|
# state enumeration
|
||||||
|
START, CONNECTING, CONNECTED, CLOSING, CLOSED = range(5)
|
||||||
|
|
||||||
|
class conn:
|
||||||
|
"""A network connection
|
||||||
|
|
||||||
|
inv: self.state in [START, CLOSED, # closed states
|
||||||
|
CONNECTING, CLOSING, # transition states
|
||||||
|
CONNECTED]
|
||||||
|
|
||||||
|
inv: 0 <= self.seqno < 256
|
||||||
|
"""
|
||||||
|
|
||||||
|
class circbuf:
|
||||||
|
"""A circular buffer.
|
||||||
|
|
||||||
|
inv:
|
||||||
|
# there can be from 0 to max items on the buffer
|
||||||
|
0 <= self.len <= len(self.buf)
|
||||||
|
|
||||||
|
# g is a valid index into buf
|
||||||
|
0 <= self.g < len(self.buf)
|
||||||
|
|
||||||
|
# p is also a valid index into buf
|
||||||
|
0 <= self.p < len(self.buf)
|
||||||
|
|
||||||
|
# there are len items between get and put
|
||||||
|
(self.p - self.g) % len(self.buf) == \
|
||||||
|
self.len % len(self.buf)
|
||||||
|
"""
|
||||||
|
|
||||||
|
Module invariants must be true after the module is loaded, and at the
|
||||||
|
entry and exit of every public function within the module.
|
||||||
|
|
||||||
|
Class invariants must be true after the ``__init__`` function returns,
|
||||||
|
at the entry of the ``__del__`` function, and at the entry and exit of
|
||||||
|
every other public method of the class. Class invariants must use the
|
||||||
|
self variable to access instance variables.
|
||||||
|
|
||||||
|
A method or function is public if its name doesn't start with an
|
||||||
|
underscore (_), unless it starts and ends with '__' (two underscores).
|
||||||
|
|
||||||
|
The docstring of any function or method can have pre-conditions
|
||||||
|
documented with the keyword ``pre`` following the same rules above.
|
||||||
|
Post-conditions are documented with the keyword ``post`` optionally
|
||||||
|
followed by a list of variables. The variables are in the same scope
|
||||||
|
as the body of the function or method. This list declares the
|
||||||
|
variables that the function/method is allowed to modify.
|
||||||
|
|
||||||
|
An example::
|
||||||
|
|
||||||
|
class circbuf:
|
||||||
|
def __init__(self, leng):
|
||||||
|
"""Construct an empty circular buffer.
|
||||||
|
|
||||||
|
pre: leng > 0
|
||||||
|
post[self]:
|
||||||
|
self.is_empty()
|
||||||
|
len(self.buf) == leng
|
||||||
|
"""
|
||||||
|
|
||||||
|
Expressions in pre- and post-conditions are defined in the module
|
||||||
|
namespace -- they have access to nearly all the variables that the
|
||||||
|
function can access, except closure variables.
|
||||||
|
|
||||||
|
The contract expressions in post-conditions have access to two
|
||||||
|
additional variables: ``__old__`` which is filled with shallow copies
|
||||||
|
of values declared in the variable list immediately following the post
|
||||||
|
keyword, and ``__return__`` which is bound to the return value of the
|
||||||
|
function or method.
|
||||||
|
|
||||||
|
An example::
|
||||||
|
|
||||||
|
class circbuf:
|
||||||
|
def get(self):
|
||||||
|
"""Pull an entry from a non-empty circular buffer.
|
||||||
|
|
||||||
|
pre: not self.is_empty()
|
||||||
|
post[self.g, self.len]:
|
||||||
|
__return__ == self.buf[__old__.self.g]
|
||||||
|
self.len == __old__.self.len - 1
|
||||||
|
"""
|
||||||
|
|
||||||
|
All contract expressions have access to some additional convenience
|
||||||
|
functions. To make evaluating the truth of sequences easier, two
|
||||||
|
functions ``forall`` and ``exists`` are defined as::
|
||||||
|
|
||||||
|
def forall(a, fn = bool):
|
||||||
|
"""Return True only if all elements in a are true.
|
||||||
|
|
||||||
|
>>> forall([])
|
||||||
|
1
|
||||||
|
>>> even = lambda x: x % 2 == 0
|
||||||
|
>>> forall([2, 4, 6, 8], even)
|
||||||
|
1
|
||||||
|
>>> forall('this is a test'.split(), lambda x: len(x) == 4)
|
||||||
|
0
|
||||||
|
"""
|
||||||
|
|
||||||
|
def exists(a, fn = bool):
|
||||||
|
"""Returns True if there is at least one true value in a.
|
||||||
|
|
||||||
|
>>> exists([])
|
||||||
|
0
|
||||||
|
>>> exists('this is a test'.split(), lambda x: len(x) == 4)
|
||||||
|
1
|
||||||
|
"""
|
||||||
|
|
||||||
|
An example::
|
||||||
|
|
||||||
|
def sort(a):
|
||||||
|
"""Sort a list.
|
||||||
|
|
||||||
|
pre: isinstance(a, type(list))
|
||||||
|
post[a]:
|
||||||
|
# array size is unchanged
|
||||||
|
len(a) == len(__old__.a)
|
||||||
|
|
||||||
|
# array is ordered
|
||||||
|
forall([a[i] >= a[i-1] for i in range(1, len(a))])
|
||||||
|
|
||||||
|
# all the old elements are still in the array
|
||||||
|
forall(__old__.a, lambda e: __old__.a.count(e) == a.count(e))
|
||||||
|
"""
|
||||||
|
|
||||||
|
To make evaluating conditions easier, the function ``implies`` is
|
||||||
|
defined. With two arguments, this is similar to the logical implies
|
||||||
|
(=>) operator. With three arguments, this is similar to C's
|
||||||
|
conditional expression (x?a:b). This is defined as::
|
||||||
|
|
||||||
|
implies(False, a) => True
|
||||||
|
implies(True, a) => a
|
||||||
|
implies(False, a, b) => b
|
||||||
|
implies(True, a, b) => a
|
||||||
|
|
||||||
|
On entry to a function, the function's pre-conditions are checked. An
|
||||||
|
assertion error is raised if any pre-condition is false. If the
|
||||||
|
function is public, then the class or module's invariants are also
|
||||||
|
checked. Copies of variables declared in the post are saved, the
|
||||||
|
function is called, and if the function exits without raising an
|
||||||
|
exception, the post-conditions are checked.
|
||||||
|
|
||||||
|
|
||||||
|
Exceptions
|
||||||
|
----------
|
||||||
|
|
||||||
|
Class/module invariants are checked even if a function or method exits
|
||||||
|
by signalling an exception (post-conditions are not).
|
||||||
|
|
||||||
|
All failed contracts raise exceptions which are subclasses of the
|
||||||
|
``ContractViolationError`` exception, which is in turn a subclass of the
|
||||||
|
``AssertionError`` exception. Failed pre-conditions raise a
|
||||||
|
``PreconditionViolationError`` exception. Failed post-conditions raise
|
||||||
|
a ``PostconditionViolationError`` exception, and failed invariants raise
|
||||||
|
a ``InvariantViolationError`` exception.
|
||||||
|
|
||||||
|
The class hierarchy::
|
||||||
|
|
||||||
|
AssertionError
|
||||||
|
ContractViolationError
|
||||||
|
PreconditionViolationError
|
||||||
|
PostconditionViolationError
|
||||||
|
InvariantViolationError
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
try:
|
||||||
|
some_func()
|
||||||
|
except contract.PreconditionViolationError:
|
||||||
|
# failed pre-condition, ok
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
Inheritance
|
||||||
|
-----------
|
||||||
|
|
||||||
|
A class's invariants include all the invariants for all super-classes
|
||||||
|
(class invariants are ANDed with super-class invariants). These
|
||||||
|
invariants are checked in method-resolution order.
|
||||||
|
|
||||||
|
A method's post-conditions also include all overridden post-conditions
|
||||||
|
(method post-conditions are ANDed with all overridden method
|
||||||
|
post-conditions).
|
||||||
|
|
||||||
|
A method's pre-conditions can be ignored if an overridden method's
|
||||||
|
pre-conditions are met (method pre-conditions are ORed with all
|
||||||
|
overridden method pre-conditions). This prevents derived classes from
|
||||||
|
breaking assumptions made by clients that only know the base method's
|
||||||
|
pre-conditions.
|
||||||
|
|
||||||
|
A somewhat contrived example::
|
||||||
|
|
||||||
|
class SimpleMailClient:
|
||||||
|
def send(self, msg, dest):
|
||||||
|
"""Sends a message to a destination:
|
||||||
|
|
||||||
|
pre: self.is_open() # we must have an open connection
|
||||||
|
"""
|
||||||
|
|
||||||
|
def recv(self):
|
||||||
|
"""Gets the next unread mail message.
|
||||||
|
|
||||||
|
Returns None if no message is available.
|
||||||
|
|
||||||
|
pre: self.is_open() # we must have an open connection
|
||||||
|
post: __return__ == None or isinstance(__return__, Message)
|
||||||
|
"""
|
||||||
|
|
||||||
|
class ComplexMailClient(SimpleMailClient):
|
||||||
|
def send(self, msg, dest):
|
||||||
|
"""Sends a message to a destination.
|
||||||
|
|
||||||
|
The message is sent immediately if currently connected.
|
||||||
|
Otherwise, the message is queued locally until a
|
||||||
|
connection is made.
|
||||||
|
|
||||||
|
pre: True # weakens the pre-condition from SimpleMailClient
|
||||||
|
"""
|
||||||
|
|
||||||
|
def recv(self):
|
||||||
|
"""Gets the next unread mail message.
|
||||||
|
|
||||||
|
Waits until a message is available.
|
||||||
|
|
||||||
|
pre: True # can always be called
|
||||||
|
post: isinstance(__return__, Message)
|
||||||
|
"""
|
||||||
|
|
||||||
|
Because pre-conditions are ORed, a ``ComplexMailClient`` can replace a
|
||||||
|
``SimpleMailClient`` with no fear of breaking existing code.
|
||||||
|
|
||||||
|
|
||||||
|
Rationale
|
||||||
|
=========
|
||||||
|
|
||||||
|
Except for the following differences, programming-by-contract for
|
||||||
|
Python mirrors the Eiffel DBC specification [#oosc]_.
|
||||||
|
|
||||||
|
Embedding contracts in docstrings is patterned after the doctest
|
||||||
|
module. It removes the need for extra syntax, ensures that programs
|
||||||
|
with contracts are backwards-compatible, and no further work is
|
||||||
|
necessary to have the contracts included in the docs.
|
||||||
|
|
||||||
|
The keywords ``pre``, ``post``, and ``inv`` were chosen instead of the
|
||||||
|
Eiffel-style ``REQUIRE``, ``ENSURE``, and ``INVARIANT`` because
|
||||||
|
they're shorter, more in line with mathematical notation, and for a
|
||||||
|
more subtle reason: the word 'require' implies caller
|
||||||
|
responsibilities, while 'ensure' implies provider guarantees. Yet
|
||||||
|
pre-conditions can fail through no fault of the caller when using
|
||||||
|
multiple inheritance, and post-conditions can fail through no fault of
|
||||||
|
the function when using multiple threads.
|
||||||
|
|
||||||
|
Loop invariants as used in Eiffel are unsupported. They're a pain to
|
||||||
|
implement, and not part of the documentation anyway.
|
||||||
|
|
||||||
|
The variable names ``__old__`` and ``__return__`` were picked to avoid
|
||||||
|
conflicts with the ``return`` keyword and to stay consistent with
|
||||||
|
Python naming conventions: they're public and provided by the Python
|
||||||
|
implementation.
|
||||||
|
|
||||||
|
Having variable declarations after a post keyword describes exactly
|
||||||
|
what the function or method is allowed to modify. This removes the
|
||||||
|
need for the ``NoChange`` syntax in Eiffel, and makes the
|
||||||
|
implementation of ``__old__`` much easier. It also is more in line
|
||||||
|
with Z schemas [#z]_, which are divided into two parts: declaring what
|
||||||
|
changes followed by limiting the changes.
|
||||||
|
|
||||||
|
Shallow copies of variables for the ``__old__`` value prevent an
|
||||||
|
implementation of contract programming from slowing down a system too
|
||||||
|
much. If a function changes values that wouldn't be caught by a
|
||||||
|
shallow copy, it can declare the changes like so::
|
||||||
|
|
||||||
|
post[self, self.obj, self.obj.p]
|
||||||
|
|
||||||
|
The ``forall``, ``exists``, and ``implies`` functions were added after
|
||||||
|
spending some time documenting existing functions with contracts.
|
||||||
|
These capture a majority of common specification idioms. It might
|
||||||
|
seem that defining ``implies`` as a function might not work (the
|
||||||
|
arguments are evaluated whether needed or not, in contrast with other
|
||||||
|
boolean operators), but it works for contracts since there should be
|
||||||
|
no side-effects for any expression in a contract.
|
||||||
|
|
||||||
|
|
||||||
|
Reference Implementation
|
||||||
|
========================
|
||||||
|
|
||||||
|
A reference implementation is available [#imp]_. It replaces existing
|
||||||
|
functions with new functions that do contract checking, by directly
|
||||||
|
changing the class' or module's namespace.
|
||||||
|
|
||||||
|
Other implementations exist that either hack ``__getattr__`` [#dbc4p]_
|
||||||
|
or use ``__metaclass__`` [#pydbc]_.
|
||||||
|
|
||||||
|
|
||||||
|
References
|
||||||
|
==========
|
||||||
|
|
||||||
|
.. [#dbc] Design By Contract is a registered trademark of Eiffel
|
||||||
|
Software Inc.
|
||||||
|
(http://archive.eiffel.com/doc/manuals/technology/contract/)
|
||||||
|
|
||||||
|
.. [#oosc] Object-oriented Software Construction, Bertrand Meyer,
|
||||||
|
ISBN 0-13-629031-0
|
||||||
|
|
||||||
|
.. [#doctest] http://www.python.org/doc/current/lib/module-doctest.html
|
||||||
|
doctest -- Test docstrings represent reality
|
||||||
|
|
||||||
|
.. [#dbc4p] Design by Contract for Python, R. Plosch
|
||||||
|
*IEEE Proceedings of the Joint Asia Pacific Software Engineering
|
||||||
|
Conference (APSEC97/ICSC97), Hong Kong, December 2-5, 1997*
|
||||||
|
(http://www.swe.uni-linz.ac.at/publications/abstract/TR-SE-97.24.html)
|
||||||
|
|
||||||
|
.. [#pydbc] PyDBC -- Design by Contract for Python 2.2+,
|
||||||
|
Daniel Arbuckle
|
||||||
|
(http://www.nongnu.org/pydbc/)
|
||||||
|
|
||||||
|
.. [#imp] Implementation described in this document.
|
||||||
|
(http://www.wayforward.net/pycontract/)
|
||||||
|
|
||||||
|
.. [#xp] Extreme Programming Explained, Kent Beck,
|
||||||
|
ISBN 0-201-61641-6
|
||||||
|
|
||||||
|
.. [#z] The Z Notation, Second Edition, J.M. Spivey
|
||||||
|
ISBN 0-13-978529-9
|
||||||
|
|
||||||
|
|
||||||
|
Copyright
|
||||||
|
=========
|
||||||
|
|
||||||
|
This document has been placed in the public domain.
|
||||||
|
|
||||||
|
|
||||||
|
..
|
||||||
|
Local Variables:
|
||||||
|
mode: indented-text
|
||||||
|
indent-tabs-mode: nil
|
||||||
|
sentence-end-double-space: t
|
||||||
|
fill-column: 70
|
||||||
|
End:
|
Loading…
Reference in New Issue