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.
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:
and then override the method _f in a subclass:
then d.f(10) will call the method of class C and return 11 instead of 20.
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:
Perhaps it would even be nice to be able to say that certain pre or postconditions should be run even when __debug__ == 0
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?
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).