Welcome, guest | Sign In | My Account | Store | Cart

An eiffel like method, with preconditions and postconditions, in python2.2

Python, 63 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
"""
Implemetation of eiffel like methods (methods with preconditions and postconditions).

eiffelmethod is a new descriptor that implements eiffel like methods. It accepts a method and 
optional pre and post conditions. fI the pre or post conditions are not given it searchs methodName_pre
and methodName_post.
"""

import types 

class eiffelmethod(object):
    def __init__(self, method, pre=None, post = None):
        self._method = method
        self._pre = pre
        self._post = post
    def __get__(self, inst, type=None):
    	result = EiffelMethodWraper(inst,self._method,self._pre, self._post) 
    	setattr(inst, self._method.__name__,result)
        return result

class EiffelMethodWraper:
    def __init__(self, inst, method, pre, post):
        self._inst = inst
        self._method = method
        if not pre:
        	pre = getattr(inst,method.__name__+"_pre",None)
        	if pre: pre = pre.im_func
        self._pre = pre
        if not post:
        	post = getattr(inst,method.__name__+"_post",None)
        	if post: post = post.im_func
        self._post = post
   
    def __call__(self, *args, **kargs):
        if self._pre:
        	apply(self._pre,(self._inst,)+args, kargs)
        result = apply(self._method,(self._inst,)+args, kargs)
        if self._post:
			apply(self._post,(self._inst,result)+args, kargs)
        return result

def _test():
    class C:
        def f(self, arg):
            return arg+1
        def f_pre(self, arg):
            assert arg>0
        def f_post(self, result, arg):
            assert result>arg
        f = eiffelmethod(f,f_pre,f_post)
    c = C()
    c.f(1)
    try:
    	c.f(-1)
    except AssertionError:
    	pass
    else:
    	raise "c.f(-1) bad implemented"
    print "OK"


if __name__=='__main__':
    _test()

A.M. Kuchling introduces descriptors in What's New in Python 2.2. He proposed a new descriptor for eiffel type methods, but he doesn't give an implementation. This implementation is based on GvR Eiffel MetaClass.

4 comments

Gregor Rayman 18 years, 11 months ago  # | flag

Inheritance? The problem with this implementation is that it does not consider inheritance.

When I override the method f in a subclass, the pre_f and post_f methods will not be called anymore. When I choose a different name for the original and the eiffelized method like here:

class C:
        def _f(self, arg):
            return arg+1
        def _f_pre(self, arg):
            assert arg>0
        def _f_post(self, result, arg):
            assert result>arg
        f = eiffelmethod(f,f_pre,f_post)

and then override the method _f in a subclass:

class D:
        def _f(self, arg):
            return arg * 2

d = D()

then d.f(10) will call the method of class C and return 11 instead of 20.

Bob Ippolito 18 years, 10 months ago  # | flag

Inheritance works. When you override a method in a subclass you're changing what it does. If it does something else, it should have different pre and post conditions.

You're free to eiffelize your new method.

The implementation detail I do disagree with is that if you don't specify pre/post explicitly it will getattr the class instance to find it. In that case you can run into funky behavior when subclassing if you take advantage of this lazy feature. For example, if class A method f is eiffelized with a precondition and postcondition, class B(A) method f is eiffelized with just a precondition then the eiffelization of class B(A) method f will find the postcondition for class A method f and assume that it still applies.

I'd probably do it differently myself.. I'd allow either str or callable arguments, have a pre/post condition chain (with each returning self so you could chain them up in one line for convenience), and also allow you to specify what it throws.. This way you could have chains of pre/postconditions:

class NumberIsNotOdd(Exception):
    pass
def even_to_odd(even):
    return even + 1
even_to_odd = eiffelize(odd)
even_to_odd.preCondition('int(even)').preCondition(lambda even:not (even & 1))
even_to_odd.postCondition(lambda rval:rval & 1, NumberIsNotOdd)

Perhaps it would even be nice to be able to say that certain pre or postconditions should be run even when __debug__ == 0

Gregor Rayman 18 years, 10 months ago  # | flag

Why eiffelize? If I override a method in a subclass, I am changig its implementation, I am neither changig its semantics nor its contract. So, perhaps I could add further pre- and post-condition, but I cannot get out of the contract the superclass subscribed to. (Liskov's substitution principle)

Anyway, if overriding the method should ivalidate the contract specified in the superclass, why to "eiffelize" it at all? Why not simply put the pre- and post-condition in the mehod itself? Why eiffelmethod and not simply assert?

a 11 years, 7 months ago  # | flag

apply() is deprecated. apply(self._pre,(self._inst,)+args, kargs) should be changed to self._pre(self._inst, *args, **kargs). apply(self._method,(self._inst,)+args, kargs) should be changed to self._method(self._inst, *args, **kargs). apply(self._post,(self._inst,result)+args, kargs) should be changed to self._post(self._inst,result, *args, **kargs).