pythonpython-decoratorsclass-method

What's @classmethod do outside of a class in Python?


In the below code, if the @classmethod annotation is present, the inner def new() is allowed to stand in for the target's __new__() -- but the class is passed twice. If @classmethod is removed then we get an error like "". What is @classmethod doing here and is there a way to do without it? (My motivation is clarity: code I don't understand seems like an accident waiting to happen.)

"""Namedtuple annotation.

Creates a namedtuple out of a class, based on the signature of that class's
__init__ function. Defaults are respected. After namedtuple's initializer is
run, the original __init__ is run as well, allowing one to assign synthetic
parameters and internal book-keeping variables.

The class must not have varargs or keyword args.
"""     
import collections
import inspect


def namedtuple(cls):
    argspec = inspect.getargspec(cls.__init__)
    assert argspec.varargs is None
    assert argspec.keywords is None 
    non_self_args = argspec.args[1:]

    # Now we can create the new class definition, based on a namedtuple.
    bases = (collections.namedtuple(cls.__name__, non_self_args), cls)
    namespace = {'__doc__': cls.__doc__}
    newcls = type(cls.__name__, bases, namespace)

    # Here we set up the new class's __new__, which hands off to namedtuple's
    # after setting defaults.
    @classmethod
    def new(*args, **kwargs):
        kls, _kls_again = args[:2]              # The class is passed twice...?
        # Resolve default assignments with this utility from inspect.
        values = inspect.getcallargs(cls.__init__, None, *args[2:], **kwargs)
        values = [values[_] for _ in non_self_args]
        obj = super(newcls, kls).__new__(kls, *values)
        cls.__init__(obj, *values)              # Allow initialization to occur
        return obj
    # The @classmethod annotation is necessary because otherwise we get an
    # error like "unbound method new takes a class instance".

    newcls.__new__ = new

    return newcls

Solution

  • __new__ is treated as a static method, not a classmethod, and is looked up directly on the class when invoked by Python. Python passes in the class object as a first argument, explicitly. See the documentation:

    __new__() is a static method (special-cased so you need not declare it as such) that takes the class of which an instance was requested as its first argument

    By making it a classmethod, the method is bound to the class and the class is passed in automatically in addition to the explicit class, which is why you get it twice. A classmethod object is a descriptor (as are staticmethod, function and property objects) and it is the lookup on a class or instance that triggers the binding behaviour.

    You should not use classmethod. Python makes it a static method when creating the class, so if you were to use

    def new(*args, **kwargs):
        # ...
    namespace = {'__doc__': cls.__doc__, '__new__': new}
    newcls = type(cls.__name__, bases, namespace)
    

    rather than set it on the class after the fact, then omitting the @classmethod decorator is enough:

    def new(*args, **kwargs):
        kls = args[0]
        # Resolve default assignments with this utility from inspect.
        values = inspect.getcallargs(cls.__init__, None, *args[1:], **kwargs)
        values = [values[_] for _ in non_self_args]
        obj = super(newcls, kls).__new__(kls, *values)
        cls.__init__(obj, *values)              # Allow initialization to occur
        return obj
    

    Alternatively, make it a staticmethod manually:

    @staticmethod
    def new(*args, **kwargs):
        # ...
    
    newcls.__new__ = new
    

    Take into account that a namedtuple-produced class is immutable, so if the shadowed class __init__ method tries to set arguments by the same names as the __init__ arguments, you'll get an AttributeError exception.