diff options
author | Guido van Rossum <guido@python.org> | 1997-08-26 00:08:51 (GMT) |
---|---|---|
committer | Guido van Rossum <guido@python.org> | 1997-08-26 00:08:51 (GMT) |
commit | 0cdb88767647cc4a684cdb61b9fd9aa9a89d75cb (patch) | |
tree | d159aaa8a69f2e94ca083277d6782a43b6c61fa7 /Demo/metaclasses | |
parent | 910105515ebe340fafe357f8fe1b898787a292a0 (diff) | |
download | cpython-0cdb88767647cc4a684cdb61b9fd9aa9a89d75cb.zip cpython-0cdb88767647cc4a684cdb61b9fd9aa9a89d75cb.tar.gz cpython-0cdb88767647cc4a684cdb61b9fd9aa9a89d75cb.tar.bz2 |
Completed first draft.
Diffstat (limited to 'Demo/metaclasses')
-rw-r--r-- | Demo/metaclasses/Eiffel.py | 2 | ||||
-rw-r--r-- | Demo/metaclasses/Simple.py | 45 | ||||
-rw-r--r-- | Demo/metaclasses/Synch.py | 256 | ||||
-rw-r--r-- | Demo/metaclasses/index.html | 182 |
4 files changed, 467 insertions, 18 deletions
diff --git a/Demo/metaclasses/Eiffel.py b/Demo/metaclasses/Eiffel.py index e3efa7f..38ee6bc 100644 --- a/Demo/metaclasses/Eiffel.py +++ b/Demo/metaclasses/Eiffel.py @@ -107,7 +107,7 @@ def _test(): assert Result > arg x = C() x.m1(12) - x.m1(-1) +## x.m1(-1) if __name__ == '__main__': _test() diff --git a/Demo/metaclasses/Simple.py b/Demo/metaclasses/Simple.py new file mode 100644 index 0000000..03ed259 --- /dev/null +++ b/Demo/metaclasses/Simple.py @@ -0,0 +1,45 @@ +import types + +class Tracing: + def __init__(self, name, bases, namespace): + """Create a new class.""" + self.__name__ = name + self.__bases__ = bases + self.__namespace__ = namespace + def __call__(self): + """Create a new instance.""" + return Instance(self) + +class Instance: + def __init__(self, klass): + self.__klass__ = klass + def __getattr__(self, name): + try: + value = self.__klass__.__namespace__[name] + except KeyError: + raise AttributeError, name + if type(value) is not types.FunctionType: + return value + return BoundMethod(value, self) + +class BoundMethod: + def __init__(self, function, instance): + self.function = function + self.instance = instance + def __call__(self, *args): + print "calling", self.function, "for", self.instance, "with", args + return apply(self.function, (self.instance,) + args) + +Trace = Tracing('Trace', (), {}) + +class MyTracedClass(Trace): + def method1(self, a): + self.a = a + def method2(self): + return self.a + +aninstance = MyTracedClass() + +aninstance.method1(10) + +print aninstance.method2() diff --git a/Demo/metaclasses/Synch.py b/Demo/metaclasses/Synch.py new file mode 100644 index 0000000..1fb9160 --- /dev/null +++ b/Demo/metaclasses/Synch.py @@ -0,0 +1,256 @@ +"""Synchronization metaclass. + +This metaclass makes it possible to declare synchronized methods. + +""" + +import thread + +# First we need to define a reentrant lock. +# This is generally useful and should probably be in a standard Python +# library module. For now, we in-line it. + +class Lock: + + """Reentrant lock. + + This is a mutex-like object which can be acquired by the same + thread more than once. It keeps a reference count of the number + of times it has been acquired by the same thread. Each acquire() + call must be matched by a release() call and only the last + release() call actually releases the lock for acquisition by + another thread. + + The implementation uses two locks internally: + + __mutex is a short term lock used to protect the instance variables + __wait is the lock for which other threads wait + + A thread intending to acquire both locks should acquire __wait + first. + + The implementation uses two other instance variables, protected by + locking __mutex: + + __tid is the thread ID of the thread that currently has the lock + __count is the number of times the current thread has acquired it + + When the lock is released, __tid is None and __count is zero. + + """ + + def __init__(self): + """Constructor. Initialize all instance variables.""" + self.__mutex = thread.allocate_lock() + self.__wait = thread.allocate_lock() + self.__tid = None + self.__count = 0 + + def acquire(self, flag=1): + """Acquire the lock. + + If the optional flag argument is false, returns immediately + when it cannot acquire the __wait lock without blocking (it + may still block for a little while in order to acquire the + __mutex lock). + + The return value is only relevant when the flag argument is + false; it is 1 if the lock is acquired, 0 if not. + + """ + self.__mutex.acquire() + try: + if self.__tid == thread.get_ident(): + self.__count = self.__count + 1 + return 1 + finally: + self.__mutex.release() + locked = self.__wait.acquire(flag) + if not flag and not locked: + return 0 + try: + self.__mutex.acquire() + assert self.__tid == None + assert self.__count == 0 + self.__tid = thread.get_ident() + self.__count = 1 + return 1 + finally: + self.__mutex.release() + + def release(self): + """Release the lock. + + If this thread doesn't currently have the lock, an assertion + error is raised. + + Only allow another thread to acquire the lock when the count + reaches zero after decrementing it. + + """ + self.__mutex.acquire() + try: + assert self.__tid == thread.get_ident() + assert self.__count > 0 + self.__count = self.__count - 1 + if self.__count == 0: + self.__tid = None + self.__wait.release() + finally: + self.__mutex.release() + + +def _testLock(): + + done = [] + + def f2(lock, done=done): + lock.acquire() + print "f2 running in thread %d\n" % thread.get_ident(), + lock.release() + done.append(1) + + def f1(lock, f2=f2, done=done): + lock.acquire() + print "f1 running in thread %d\n" % thread.get_ident(), + try: + f2(lock) + finally: + lock.release() + done.append(1) + + lock = Lock() + lock.acquire() + f1(lock) # Adds 2 to done + lock.release() + + lock.acquire() + + thread.start_new_thread(f1, (lock,)) # Adds 2 + thread.start_new_thread(f1, (lock, f1)) # Adds 3 + thread.start_new_thread(f2, (lock,)) # Adds 1 + thread.start_new_thread(f2, (lock,)) # Adds 1 + + lock.release() + import time + while len(done) < 9: + print len(done) + time.sleep(0.001) + print len(done) + + +# Now, the Locking metaclass is a piece of cake. +# As an example feature, methods whose name begins with exactly one +# underscore are not synchronized. + +from Meta import MetaClass, MetaHelper, MetaMethodWrapper + +class LockingMethodWrapper(MetaMethodWrapper): + def __call__(self, *args, **kw): + if self.__name__[:1] == '_' and self.__name__[1:] != '_': + return apply(self.func, (self.inst,) + args, kw) + self.inst.__lock__.acquire() + try: + return apply(self.func, (self.inst,) + args, kw) + finally: + self.inst.__lock__.release() + +class LockingHelper(MetaHelper): + __methodwrapper__ = LockingMethodWrapper + def __helperinit__(self, formalclass): + MetaHelper.__helperinit__(self, formalclass) + self.__lock__ = Lock() + +class LockingMetaClass(MetaClass): + __helper__ = LockingHelper + +Locking = LockingMetaClass('Locking', (), {}) + +def _test(): + # For kicks, take away the Locking base class and see it die + class Buffer(Locking): + def __init__(self, initialsize): + assert initialsize > 0 + self.size = initialsize + self.buffer = [None]*self.size + self.first = self.last = 0 + def put(self, item): + # Do we need to grow the buffer? + if (self.last+1) % self.size != self.first: + # Insert the new item + self.buffer[self.last] = item + self.last = (self.last+1) % self.size + return + # Double the buffer size + # First normalize it so that first==0 and last==size-1 + print "buffer =", self.buffer + print "first = %d, last = %d, size = %d" % ( + self.first, self.last, self.size) + if self.first <= self.last: + temp = self.buffer[self.first:self.last] + else: + temp = self.buffer[self.first:] + self.buffer[:self.last] + print "temp =", temp + self.buffer = temp + [None]*(self.size+1) + self.first = 0 + self.last = self.size-1 + self.size = self.size*2 + print "Buffer size doubled to", self.size + print "new buffer =", self.buffer + print "first = %d, last = %d, size = %d" % ( + self.first, self.last, self.size) + self.put(item) # Recursive call to test the locking + def get(self): + # Is the buffer empty? + if self.first == self.last: + raise EOFError # Avoid defining a new exception + item = self.buffer[self.first] + self.first = (self.first+1) % self.size + return item + + def producer(buffer, wait, n=1000): + import time + i = 0 + while i < n: + print "put", i + buffer.put(i) + i = i+1 + print "Producer: done producing", n, "items" + wait.release() + + def consumer(buffer, wait, n=1000): + import time + i = 0 + tout = 0.001 + while i < n: + try: + x = buffer.get() + if x != i: + raise AssertionError, \ + "get() returned %s, expected %s" % (x, i) + print "got", i + i = i+1 + tout = 0.001 + except EOFError: + time.sleep(tout) + tout = tout*2 + print "Consumer: done consuming", n, "items" + wait.release() + + pwait = thread.allocate_lock() + pwait.acquire() + cwait = thread.allocate_lock() + cwait.acquire() + buffer = Buffer(1) + n = 1000 + thread.start_new_thread(consumer, (buffer, cwait, n)) + thread.start_new_thread(producer, (buffer, pwait, n)) + pwait.acquire() + print "Producer done" + cwait.acquire() + print "All done" + print "buffer size ==", len(buffer.buffer) + +if __name__ == '__main__': + _testLock() + _test() diff --git a/Demo/metaclasses/index.html b/Demo/metaclasses/index.html index 378ceb3..269dc69 100644 --- a/Demo/metaclasses/index.html +++ b/Demo/metaclasses/index.html @@ -6,9 +6,9 @@ <BODY BGCOLOR="FFFFFF"> -<H1>Metaprogramming in Python 1.5</H1> +<H1>Metaprogramming in Python 1.5 (DRAFT)</H1> -<H4>XXX Don't link to this page! It is very much a work in progress.</H4> +<H4>XXX This is very much a work in progress.</H4> <P>While Python 1.5 is only out as a <A HREF="http://grail.cnri.reston.va.us/python/1.5a3/">restricted alpha @@ -267,7 +267,7 @@ class Instance: value = self.__klass__.__namespace__[name] except KeyError: raise AttributeError, name - if type(value) is not types.FuncType: + if type(value) is not types.FunctionType: return value return BoundMethod(value, self) @@ -276,20 +276,150 @@ class BoundMethod: self.function = function self.instance = instance def __call__(self, *args): - print "calling", self.function, "for", instance, "with", args + print "calling", self.function, "for", self.instance, "with", args return apply(self.function, (self.instance,) + args) -<HR> -Confused already? +Trace = Tracing('Trace', (), {}) + +class MyTracedClass(Trace): + def method1(self, a): + self.a = a + def method2(self): + return self.a + +aninstance = MyTracedClass() + +aninstance.method1(10) + +print "the answer is %d" % aninstance.method2() +</PRE> + +Confused already? The intention is to read this from top down. The +Tracing class is the metaclass we're defining. Its structure is +really simple. + +<P> + +<UL> + +<LI>The __init__ method is invoked when a new Tracing instance is +created, e.g. the definition of class MyTracedClass later in the +example. It simply saves the class name, base classes and namespace +as instance variables.<P> + +<LI>The __call__ method is invoked when a Tracing instance is called, +e.g. the creation of aninstance later in the example. It returns an +instance of the class Instance, which is defined next.<P> + +</UL> + +<P>The class Instance is the class used for all instances of classes +built using the Tracing metaclass, e.g. aninstance. It has two +methods: + +<P> + +<UL> + +<LI>The __init__ method is invoked from the Tracing.__call__ method +above to initialize a new instance. It saves the class reference as +an instance variable. It uses a funny name because the user's +instance variables (e.g. self.a later in the example) live in the same +namespace.<P> + +<LI>The __getattr__ method is invoked whenever the user code +references an attribute of the instance that is not an instance +variable (nor a class variable; but except for __init__ and +__getattr__ there are no class variables). It will be called, for +example, when aninstance.method1 is referenced in the example, with +self set to aninstance and name set to the string "method1".<P> + +</UL> + +<P>The __getattr__ method looks the name up in the __namespace__ +dictionary. If it isn't found, it raises an AttributeError exception. +(In a more realistic example, it would first have to look through the +base classes as well.) If it is found, there are two possibilities: +it's either a function or it isn't. If it's not a function, it is +assumed to be a class variable, and its value is returned. If it's a +function, we have to ``wrap'' it in instance of yet another helper +class, BoundMethod. + +<P>The BoundMethod class is needed to implement a familiar feature: +when a method is defined, it has an initial argument, self, which is +automatically bound to the relevant instance when it is called. For +example, aninstance.method1(10) is equivalent to method1(aninstance, +10). In the example if this call, first a temporary BoundMethod +instance is created with the following constructor call: temp = +BoundMethod(method1, aninstance); then this instance is called as +temp(10). After the call, the temporary instance is discarded. +<P> + +<UL> -<P>XXX More text is needed here. For now, have a look at some very -preliminary examples that I coded up to teach myself how to use this -feature: +<LI>The __init__ method is invoked for the constructor call +BoundMethod(method1, aninstance). It simply saves away its +arguments.<P> +<LI>The __call__ method is invoked when the bound method instance is +called, as in temp(10). It needs to call method1(aninstance, 10). +However, even though self.function is now method1 and self.instance is +aninstance, it can't call self.function(self.instance, args) directly, +because it should work regardless of the number of arguments passed. +(For simplicity, support for keyword arguments has been omitted.)<P> +</UL> -<H2>Real-life Examples</H2> +<P>In order to be able to support arbitrary argument lists, the +__call__ method first constructs a new argument tuple. Conveniently, +because of the notation *args in __call__'s own argument list, the +arguments to __call__ (except for self) are placed in the tuple args. +To construct the desired argument list, we concatenate a singleton +tuple containing the instance with the args tuple: (self.instance,) + +args. (Note the trailing comma used to construct the singleton +tuple.) In our example, the resulting argument tuple is (aninstance, +10). + +<P>The intrinsic function apply() takes a function and an argument +tuple and calls the function for it. In our example, we are calling +apply(method1, (aninstance, 10)) which is equivalent to calling +method(aninstance, 10). + +<P>From here on, things should come together quite easily. The output +of the example code is something like this: + +<PRE> +calling <function method1 at ae8d8> for <Instance instance at 95ab0> with (10,) +calling <function method2 at ae900> for <Instance instance at 95ab0> with () +the answer is 10 +</PRE> + +<P>That was about the shortest meaningful example that I could come up +with. A real tracing metaclass (for example, <A +HREF="#Trace">Trace.py</A> discussed below) needs to be more +complicated in two dimensions. + +<P>First, it needs to support more advanced Python features such as +class variables, inheritance, __init__ methods, and keyword arguments. + +<P>Second, it needs to provide a more flexible way to handle the +actual tracing information; perhaps it should be possible to write +your own tracing function that gets called, perhaps it should be +possible to enable and disable tracing on a per-class or per-instance +basis, and perhaps a filter so that only interesting calls are traced; +it should also be able to trace the return value of the call (or the +exception it raised if an error occurs). Even the Trace.py example +doesn't support all these features yet. + +<P> + +<HR> + +<H1>Real-life Examples</H1> + +<P>Have a look at some very preliminary examples that I coded up to +teach myself how to use metaprogramming: <DL> @@ -313,13 +443,13 @@ and ``Color.red + 1'' raise a TypeError exception. <P> -<DT><A HREF="Trace.py">Trace.py</A> +<DT><A NAME=Trace></A><A HREF="Trace.py">Trace.py</A> -<DD>The resulting classes work much like standard classes, but by -setting a special class or instance attribute __trace_output__ to -point to a file, all calls to the class's methods are traced. It was -a bit of a struggle to get this right. This should probably redone -using the generic metaclass below. +<DD>The resulting classes work much like standard +classes, but by setting a special class or instance attribute +__trace_output__ to point to a file, all calls to the class's methods +are traced. It was a bit of a struggle to get this right. This +should probably redone using the generic metaclass below. <P> @@ -338,13 +468,31 @@ hooks tough; we provide a similar hook _getattr_ instead. <P> <DT><A HREF="Eiffel.py">Eiffel.py</A> - +ppp <DD>Uses the above generic metaclass to implement Eiffel style pre-conditions and post-conditions. <P> + +<DT><A HREF="Synch.py">Synch.py</A> + +<DD>Uses the above generic metaclass to implement synchronized +methods. + +<P> + </DL> +<P>A pattern seems to be emerging: almost all these uses of +metaclasses (except for Enum, which is probably more cute than useful) +mostly work by placing wrappers around method calls. An obvious +problem with that is that it's not easy to combine the features of +different metaclasses, while this would actually be quite useful: for +example, I wouldn't mind getting a trace from the test run of the +Synch module, and it would be interesting to add preconditions to it +as well. This needs more research. Perhaps a metaclass could be +provided that allows stackable wrappers... + </BODY> </HTML> |