Design by Contract for Python

Carl Banks imbosol at aerojockey.com
Wed May 14 18:21:24 EDT 2003


Terence Way wrote:
> The major problem is support for 'old' variables.  Post-conditions
> should be able to compare changed values with saved values.
> I see no elegant way to do this with Daniel's approach.  To do
> this with functions should require something ick, like so:
> 
>     def foo(self, a, b):
>          blah, blah, blah
> 
>     def foo_pre(self, a, b):
>          assert a > 5
> 
>     def foo_save(self, a, b):
>         return [copy.copy(b)]
> 
>     def foo_post(self, old, ret, a, b):
>         assert b == old[0] + 1
> 
> 
> I think a compromise approach might work: for a function foo,
> simply check if the functions _foo_pre and _foo_post are defined
> and have matching arguments ( _foo_post would have to take
> two additional arguments after the self argument).  If so, these
> functions are executed as well...  ANDed with the documented
> checks.

Ick.  It would work, and in fact, I've written a simpleminded package
that encodes complicated relationships in the names of functions.  But
I don't like it, and it turns out to be unnecessary.

Metaclasses offer a neat, cool way of grouping related functions like
preconditions and postconditions.  I'll give you a bare bones example;
it should work as both a regular function and a method.  (BTW, in this
example, the precondtion returns a tuple of old-values to pass into
the post-condition, rather than having a separate save function.
Also, it passes ret and old after the given args, so that you don't
have to do anything weird with self.)


    class ContractMetaclass(type):
        def __new__(metaclass,name,bases,clsdict):
            if bases == (object,):
                return type.__new__(metaclass,name,bases,clsdict)
            elif bases == (Contract,):
                pre = clsdict['pre']
                body = clsdict['body']
                post = clsdict['post']
                def contract_function(*args):
                    saves = pre(*args)
                    if saves is None:
                        saves = ()
                    ret = body(*args)
                    post(*(args+(ret,)+saves))
                    return ret
                return contract_function
            else:
                raise TypeError

    class Contract(object):
        __metaclass__ = ContractMetaclass


Then, you can create a function with pre- and post-conditions like
this:

    class incitem0(Contract):
        def pre(s):
            return (s[0],)
        def body(s):
            s[0] += 1
            return s[0]
        def post(s,ret,old_s0):
            assert ret == s[0] == old_s0+1


Note that the functions pre, body, and post don't take self
parameters: this particular metaclass returns an object that invokes
the three functions without a self parameter.  (Metaclasses can do
that.)

The only minor problems I can see with this are an extra indentation
level, and getting used to defining a function with a class statement.

The good thing is no icky name encoding, and it forces you to keep
related functions together.


> This solves several problems with *my* stuff: the largest being
> access to closure variables (inaccessible by dynamically
> generated checking functions, but accessible by the _foo_*
> methods).

This method should likewise give you access to variables in the
surrounding scope.

My example is only bare bones: it can be improved mightily.  Perhaps a
very suave version could examine the bytecode of the postcondition and
automatically save "old" values.


-- 
CARL BANKS




More information about the Python-list mailing list