Pre/Postconditions with decorators

R

Rittersporn

My humble attempt to model pre/postconditions with
decorators :) It's also my first experiment
with decorators.
If you have any ideas or thoughts on how to
improve the code snippet, I'll be happy to
learn more.

Enjoy :)

def condition(pretext,posttext=""):
precode=compile(pretext or "True","","eval")
postcode=compile(posttext or "True","","eval")

# function -> decorated(function)
def decorate_condition(function):
# FIXME: Does not work with wrapped functions
argcount=function.func_code.co_argcount
var=function.func_code.co_varnames[0:argcount]

# arguments -> closure(assertion)
def evaluate_condition(*args,**kargs):
# FIXME: check if "var" always contains ordered list of arguments
# map arguments and
args_seq=[(argname,args[pos]) for pos,argname in enumerate(var)]
# key-arguments to value
kargs_seq=[(k,v) for k,v in kargs.itervalues()]
environment=args_seq+kargs_seq

# precondition
assert eval(precode,{},dict(environment)),pretext
tmp=function(*args,**kargs)
environment2=environment+[('result',tmp)]

# postcondition
assert eval(postcode,{},dict(environment2)),posttext
return tmp

return evaluate_condition

return decorate_condition

@condition("number>0 and number<2","result>=0")
def sqrt(number):
import math
return math.sqrt(number)

@condition("list(seq) is not None","sum(seq)==result")
def my_sum(seq):
tmp=0
for element in seq:
tmp+=element
return tmp

print sqrt(1.2)
print my_sum([1,2,3])
 
S

Stephen Thorne

@condition("number>0 and number<2","result>=0")
def sqrt(number):
import math
return math.sqrt(number)

@condition("list(seq) is not None","sum(seq)==result")
def my_sum(seq):
tmp=0
for element in seq:
tmp+=element
return tmp

print sqrt(1.2)
print my_sum([1,2,3])

I think it would be nicer to have the pre and post conditions being compilable.

@condition((list(seq) is not None for seq in args), (sum(seq)==result
for ((seq,), result) in (args, result))

or something silly like that.

Personally, I'd prefer this:
@precondition(list(seq) is not None for seq in args)
@postcondition(sum(seq)==result for ((seq,), result) in (args, result))

(this is, of course, using the framehack lambda replacement presented
in a thread about a week ago).

Stephen.


Stephen.
 
R

rittersporn

Hi Stephen
I have not read anything about the
"framehack lambda replacement" yet,
but I do compile the pre- and
postconditions. Syntax erros e.g.
will be raised if the module
is compiled. Although I must admit
that your code snippets look more like
compiled code ;-)

Hi Robert
thanks for the link to the Ian Bicking blog.

Hi George,
it would be nice to see how you have tackled
the task.
Maybe we will have a checker
module in Python one day... ;-)

Well, I have attached my latest attempt
to model pre/postconditions (without "framehack
lambda replacement") which does wrap the
original function with a class which delegates
attribute access. Now I can split my
"condition" into pre- and postcondition
and the "tracer" prints the original
function name.
I have also fixed a bug
with keyword arguments.
Major difference compared to
other examples is probably
only that I can refer to function
arguments by name:

class Delegate(object):
def __init__(self,function):
self.function=function
def __getattr__(self,key):
return getattr(self.function,key)


def condition(pretext,posttext=""):
precode=compile(pretext or "True","","eval")
postcode=compile(posttext or "True","","eval")

# function -> decorated(function)
def decorate_condition(function):
argcount=function.func_code.co_argcount
var=function.func_code.co_varnames[0:argcount]
class EvalCond(Delegate):
def __call__(self,*args,**kargs):
# FIXME: check if "var" always contains
ordered list of arguments
# map arguments and
args_seq=[(argname,args[pos]) for
pos,argname in \
enumerate(var) if (argname not
in kargs)]
# key-arguments to value
kargs_seq=[(k,v) for k,v in
kargs.iteritems()]
environment=args_seq+kargs_seq

# precondition
assert
eval(precode,{},dict(environment)),pretext
tmp=function(*args,**kargs)

environment2=environment+[('result',tmp)]

# postcondition
assert
eval(postcode,{},dict(environment2)),posttext
return tmp
return EvalCond(function)
return decorate_condition

def trace(function):
class Trace(Delegate):
def __call__(self,*args,**kargs):
print "enter function %s with " % \
self.function.func_name,args,kargs
result=self.function(*args,**kargs)
print "leave function %s with " % \
self.function.func_name,args,kargs
return result
return Trace(function)

def precondition(prgtext):
return condition(prgtext)

def postcondition(prgtext):
return condition("",prgtext)


@precondition("number>0 and number<2")
@postcondition("result>=0")
def sqrt(number):
import math
return math.sqrt(number)

@trace
@precondition("len(seq)>0 is not None and str(more)")
@postcondition("sum(seq)==result")
def my_sum(seq,more):
tmp=0
for element in seq:
tmp+=element
return tmp

print sqrt(1.2)
print my_sum([1,2,3],more="more")
 
R

rittersporn

Thank you very much. It is really a very elegant piece of code :)

Eiffel (language) has both type checking and design by contract.
Python lacks both.
Your module tackles type checking, I
tried to execute arbitrary code before
and after function execution to realize
runtime assertions.

I wonder if this should be separated in a Python
"checker"-module.
Both are part of the interface "contract".

Ciao
 
S

Skip Montanaro

Actually, Python is strongly typed. It's just dynamically instead of
statically typed.

Skip
 
S

Stephen Thorne

I posted my module on http://rafb.net/paste/results/voZYTG78.html and its unit test on
http://rafb.net/paste/results/MYxMQW95.html. Any feedback will be appreciated.

Okay, nice.

Unresolved Problems:
1) How do you handle duck types, i.e. a method that accepts StringIO,
cStringIO or any other object that has a .readlines(), .seek() and
..read() method?
2) How do you turn off the type checking for production code?

Otherwise I quite like it. Please publish it somewhere.

Stephen.
 
P

Paul Rubin

Stephen Thorne said:
Unresolved Problems:
1) How do you handle duck types, i.e. a method that accepts StringIO,
cStringIO or any other object that has a .readlines(), .seek() and
.read() method?

That should really be done through having those classes inherit a
file-operations mixin, or else through interfaces (which might get
added to Python).
2) How do you turn off the type checking for production code?

It should be left on. Leaving it in for development and turning it
off for production is like wearing a parachute during ground training
and taking it off once you're in the air.
 
S

Stephen Thorne

That should really be done through having those classes inherit a
file-operations mixin, or else through interfaces (which might get
added to Python).
Working under the assumption we cannot change the existing standard library.
It should be left on. Leaving it in for development and turning it
off for production is like wearing a parachute during ground training
and taking it off once you're in the air.
So we can't use this for a case where we have an extremely large list
then ;). Its an O(n) operation to just call a function that takes a
list of a specific type.

Stephen.
 
P

Paul Rubin

Stephen Thorne said:
So we can't use this for a case where we have an extremely large list
then ;). Its an O(n) operation to just call a function that takes a
list of a specific type.

The condition should be enforced at the time whenever list itself is
mutated.
 
R

rittersporn

Thank you very much for reminding me. I have been sloppy.
It should have said static type checking.

ciao
 
G

George Sakkis

Stephen Thorne said:
Unresolved Problems:
1) How do you handle duck types, i.e. a method that accepts StringIO,
cStringIO or any other object that has a .readlines(), .seek() and
.read() method?

I think the 'proper' way of checking duck types (among other things) is by defining appropriate
protocols; check pyProtocols (http://peak.telecommunity.com/PyProtocols.html) for details.
2) How do you turn off the type checking for production code?

Currently I don't, but that's easy to do if desired by checking __debug__ in the decorator and
return the passed function if it is false:

def params(**checkedArgs):
if not __debug__:
return lambda function: function
# else return a typechecked proxy of function
(...)

Otherwise I quite like it. Please publish it somewhere.

Thanks. I'll post an entry on Vaults of Parnassus as soon as I publish it.

George
 
A

Andrew Dalke

Paul said:
[Type checking] should be left on. Leaving it in for development
and turning it off for production is like wearing a parachute
during ground training and taking it off once you're in the air.

Why isn't it like practicing the trapeze with a net but
going without a net when performing for a big circus?

Why isn't it like using training wheels when learning to
ride bicycle but getting rid of them once you've got the
idea down?

Or like using a map when you get to a new city but gradually
stop as you know the place?

Fighting analogies with analogies :)

Andrew
(e-mail address removed)
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,744
Messages
2,569,484
Members
44,903
Latest member
orderPeak8CBDGummies

Latest Threads

Top