(accidently posted this as a comment to the previous post)

Perhaps like this:

import types

class Meta(type):
....def __init__(cls, name, bases, dict):
........super(Meta, cls).__init__(name, bases, dict)
........for fun in dict.values():
............if isinstance(fun, types.FunctionType):
................print "Adding method " + fun.__name__

class Foo(object):
....__metaclass__ = Meta
....def bar(self): pass

# --> Adding method bar

Not as nice as the Ruby version, but then Ruby seems to explicitely provide the method_added hook.


Gravatar That works, although it's not exactly the same... all the methods are created first, then the messages are displayed after that. Not sure if there are non-trivial situations where this matters, though.

However, it doesn't work when you add a method to the class after it has been defined (which is common in Ruby, but less common in Python). I suppose that case can be caught by the __setattr__ hack.


Gravatar I think julien's solution is probably the best that can be done in Python. The metaclass trick doesn't work because python has to read in the class before it can check the __metaclass__ variable to see what the metaclass is. By this point, all the methods have been defined, so the metaclass can only find out about them through introspection after the fact.

It's hard to imagine scenarios where the Python version couldn't do the same thing as the Ruby version. The only thing I can think of is a situation where you need to know the order in which the methods were defined. The python dictionary will scramble the order, so that is not recoverable.

The data models in django get around this by having the fields (whose order needs to be preserved) do some accounting in the background as you declare them. Not too far really from the decorator hack you mention.


Gravatar You might also find class advisors that are used in Zope 3 interesting. James Henstridge recently wrote a very nice exposition on those:

http://blogs.gnome.org/view/jame...sh/2005/09/08/ 0


Gravatar I think that you can also use sys.settrace to detect function definitions as soon as they're executed. Details are left as an excercise for the reader. Phillip J. Eby has implemented something very much like this in PEAK:
http://dirtsimple.org/2004/11/us...-22-and- 23.html


Gravatar [comment moved here from #822, HN]

Here's a solution that will approximate the behavior you want.

When the Python interpreter evaluates a method in a class definition, the class object itself doesn't actually exist yet, as it appears to in Ruby.

Evaluating a class definition results in a 3-tuple: the class name, a tuple of its base classes, and a dictionary of name->attribute pairs. That 3-tuple is passed to the __new__ method of the metaclass, which is the built-in 'type' object by default.

With a custom metaclass, you can alter the makeup of the class object, including trapping method definitions, like this:

class Meta(type):
def __new__(cls, name, bases, dct):
for k,v in dct.iteritems() :
if type(v) == FunctionType:
print "Adding method %s" % k
return type.__new__(cls, name, bases, dct)

def __setattr__(cls, k, v):
if type(v) == FunctionType:
print "Adding method %s" % k
return type.__setattr__(cls, k, v)

class Foo(object):
__metaclass__ = Meta

def __init__(self, foo):
self.foo = foo

def bar(self):
pass

x = Baz("foo")
Baz.bazooey = lambda: True
print Baz.bazooey

--

This will print:

Adding method bar
Adding method __init__
Adding method bazooey

You need the meta __setattr__ function to capture new method definitions that occur after the class has been defined.

Hope this helps!

-Andy Gross


Gravatar The reason this doesn't work exactly as done in the Ruby example is that in Python the functions are all defined and created before the class. They are not added to the class, the class is initialized with a dictionary of all its methods and other attributes, which are all defined before hand.


It would probably be best to define a new 'magic' method in a metaclass like this(sorry, this will be in multiple posts, because of size limit):

import types


class MetaMethodChanger(type):

....def __new__(cls, name, bases, class_dict):
........changer = class_dict.get('__new_methods__')
........if isinstance(changer, types.FunctionType):
............class_dict['__new_methods__'] = staticmethod(changer)
............for name, obj in class_dict.items():
................if isinstance(obj, types.FunctionType):
....................new_name, new_meth = changer(obj, name, obj.func_defaults)
....................del class_dict[name]
....................class_dict[new_name] = new_meth
........return type.__new__(cls, name, bases, class_dict)

....def __setattr__(cls, name, obj):
........changer = cls.__dict__.get('__new_methods__')
........if isinstance(changer, types.FunctionType):
............if isinstance(obj, types.FunctionType):
................name, obj = changer(obj, name, obj.func_defaults)
........type.__setattr__(cls, name, obj)


Then you can define a mix-in class:


class MethodChanger(object):

....__metaclass__ = MetaMethodChanger

....def __new_methods__(func, name, defaults):
........if not isinstance(func, types.FunctionType):
............raise TypeError
........if not isinstance(name, types.StringType):
............raise TypeError
........if defaults is not None:
............defaults = tuple(defaults)
............if not len(defaults) <= func.func_code.co_argcount:
................raise TypeError
........func.func_name = name
........func.func_defaults = defaults
........return name, func


and finaly:


class YourWish(MethodChanger):

....def __new_methods__(func, name, defaults):
........print name
........return MethodChanger.__new_methods__(func, name, defaults)

....def foo(self):pass

....def bar(self, x, y, z, a=1, b='SPAM', c=None):pass


or even:

class Example(MethodChanger):

....def __new_methods__(func, name, defaults):
........argcount = func.func_code.co_argcount
........argnames = func.func_code.co_varnames[:argcount]
........if defaults is None:
............print 'nn == method %s == nnarguments:' % name
............for arg in argnames:
................print arg
........else:
............defaultcount = len(defaults)
............without_defaults = argnames[:defaultcount]
............with_defaults = argnames[defaultcount + 1:]
............print 'nn == method %s == nnarguments:' % name
............for arg in without_defaults:
................print arg
............for arg, default_value in zip(with_defaults, defaults):
................print arg, '=', default_value
........return MethodChanger.__new_methods__(func, name, defaults)

....def foo(self):pass

....def bar(self, x, y, z, a=1, b='SPAM', c=None):pass


Sorry, two bugs in MetaMethodChanger; updated version:

class MetaMethodChanger(type):

....def __new__(cls, class_name, bases, class_dict):
........changer = class_dict.get('__new_methods__')
........if isinstance(changer, types.FunctionType):
............class_dict['__new_methods__'] = staticmethod(changer)
............for name, obj in class_dict.items():
................if isinstance(obj, types.FunctionType):
....................new_name, new_meth = changer(obj, name, obj.func_defaults)
....................del class_dict[name]
....................class_dict[new_name] = new_meth
........return type.__new__(cls, class_name, bases, class_dict)

....def __setattr__(cls, name, obj):
........changer = getattr(cls, '__new_methods__', None)
........if isinstance(changer, types.FunctionType):
............if isinstance(obj, types.FunctionType):
................name, obj = changer(obj, name, obj.func_defaults)
........type.__setattr__(cls, name, obj)