Kaydet (Commit) 0cdb8876 authored tarafından Guido van Rossum's avatar Guido van Rossum

Completed first draft.

üst 91010551
......@@ -107,7 +107,7 @@ def _test():
assert Result > arg
x = C()
x.m1(12)
x.m1(-1)
## x.m1(-1)
if __name__ == '__main__':
_test()
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()
"""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()
......@@ -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>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment