Thrastylon Thrastylon - 6 months ago 15
Python Question

How can one use a class method in a class attribute definition

All is in the title. I'd like to create a class method and a class attribute, both constructed only once, when the class is created, using the first in the second's definition.

With my best try, I just get a

TypeError: 'classmethod' object is not callable
.

Here is my code :

import numpy as np

class Foo( object ) :

@classmethod
def bar( cls, x ) :
return x+1

bar_vect = np.vectorize( bar )

Foo.bar_vect( np.array([ 1, 2, 3 ]) )

>> TypeError: 'classmethod' object is not callable


EDIT 1 :



'classmethod' object is not callable is a problem raising the same error, but with a lot of workarounds. My question is meant to go straight to the point and have a clear idea of how to use
@classmethod
without a scope giving access to
cls
.

Another try that I made was the following :

import numpy as np

class Foo( object ) :

@classmethod
def bar( cls, x ) :
return x+1

bar_vect = np.vectorize( bar )

>> NameError: name 'Foo' is not defined

Answer

@classmethods are implemented as a special object that gets processed using the descriptor protocol when looked up on the class; inside the definition, as a raw name (unqualified), it's a special classmethod object, not a normal function and it's not bound to the class properly. If you check the pure Python definition of classmethod, you'll note it's just a normal object that implements __init__ (for construction) and __get__ (for descriptor lookup), but not __call__, meaning that if you have the raw classmethod object, it's not actually a callable at all.

The trick is to qualify the reference so the "magic" happens to bind it to the class, and move the qualified reference outside the class definition (so Foo is a defined name and can be referenced for binding) changing:

class Foo(object):
    ... rest of class ...
    bar_vect = np.vectorize(bar)  # Indented and unqualified, BAD

to:

class Foo(object):
    ... rest of class ...
# Must qualify both bar_vect and bar, since no longer in class definition
Foo.bar_vect = np.vectorize(Foo.bar)  # Dedented, so Foo is defined for referencing, GOOD

Note that since you're using a classmethod, I suspect you may eventually be interested in subclassing and overriding bar. As written, you'd need to explicitly redefine bar_vect after defining each subclass, or it would use the inherited bar_vect, based on Foo.bar, even if the subclass defines its own bar classmethod. Explicitly redefining bar_vect each time is an option, but the other approach is to use metaclasses to implicitly define bar_vect when a class redefines bar:

class BarVectorized(type):
    def __new__(cls, name, bases, namespace, **kwargs):
        namespace = dict(namespace)
        newcls = type.__new__(cls, name, bases, namespace)
        if 'bar' in namespace:
            # Bar was (re-)defined for this class; make new vectorized wrapper
            newcls.bar_vect = np.vectorize(newcls.bar)
        return newcls

class Foo(object):
    __metaclass__ = BarVectorized
    @classmethod
    def bar(cls, x): return x + 1

class Foo2(Foo):
    @classmethod
    def bar(cls, x): return x + 2  # New bar means new bar_vect

class Foo3(Foo2):
    pass  # No new bar means bar_vect inherited without modification from Foo2

>>> Foo.bar_vect([1,2,3])
array([2, 3, 4])
>>> Foo2.bar_vect([1,2,3])
array([3, 4, 5])
>>> Foo3.bar_vect([1,2,3])
array([3, 4, 5])
>>> Foo.bar_vect is Foo2.bar_vect   # New vectorize wrapper for new bar
False
>>> Foo2.bar_vect is Foo3.bar_vect  # vectorize wrapper reused for unchanged bar
True

No need to define bar_vect explicitly at all, and bar_vect seamlessly uses the most local classes' definition of bar available at class definition time, so unless bar is redefined after class definition, it always works, and it works as efficiently as possible. To make it use bar live, you'd need to resort to more extreme measures that perform dynamic lookup and (barring a cache) reconstruction of the np.vectorize object on each use, which is suboptimal to say the least.