Welcome, guest | Sign In | My Account | Store | Cart

This descriptor can be used to decorate methods, similar to the built-ins classmethod and staticmethod. It enables the caller to call methods on either the class or an instance, and the first argument passed to the method will be the class or the instance respectively.

This differs from classmethods, which always passes the class, and staticmethods, which don't pass either.

Like all descriptors, you can only use this in new-style classes.

Python, 32 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import functools

class dualmethod(object):
    """Descriptor implementing dualmethods (combination class/instance method).

    Returns a method which takes either an instance or a class as the first
    argument. When called on an instance, the instance is passed as the first
    argument. When called as a class, the class itself is passed instead.

    >>> class Example(object):
    ...     @dualmethod
    ...     def method(this):
    ...         if type(this) is type:
    ...             print "I am the class '%s'." % this.__name__
    ...         else:
    ...             print "I am an instance of the class '%s'." % this.__class__.__name__
    ...
    >>> Example.method()
    I am the class 'Example'.
    >>> Example().method()
    I am an instance of the class 'Example'.

    """
    def __init__(self, func):
        self.func = func
    def __get__(self, obj, cls=None):
        if cls is None:  cls = type(obj)
        if obj is None:  obj = cls
        @functools.wraps(self.func)
        def newfunc(*args, **kwargs):
            return self.func(obj, *args, **kwargs)
        return newfunc

Use-case: if you have a class without an __init__ or __new__ method, then this may be of use to you. It enables the caller to call methods on either the class, or an instance, and the first argument passed to the method will be the class or the instance respectively.

This may be useful if you have a class with shared state that doesn't require instantiating:

class Example(object):  # must be a new-style class
    SHARED = 'something'
    @dualmethod
    def method(this, arg):
        return this.SHARED + " " + arg

You can use this class without instantiating:

Example.method('else')  # returns 'something else'

But if you want to customise the behaviour, you can instantiate it and modify the attribute at the instance, without effecting the shared state:

x = Example()  # create an instance
x.SHARED = 'nothing'  # make an instance attribute overriding the class attribute
x.method('else')  # returns 'nothing else'
Example.method('else')  # still returns 'something else'

This differs from class methods, which always passes the class.

Known issues:

Beware that doctests in dualmethods may not be run by doctest.testmod(). This appears to be a limitation of doctest, that it doesn't work with descriptors other than the builtins property, classmethod and staticmethod. See http://bugs.python.org/issue4037

As a work-around, you can run the tests by hand by calling doctest.run_docstring_examples on each dualmethod object.

1 comment

Steven D'Aprano (author) 11 years, 9 months ago  # | flag

The original version I posted made any docstring of the method disappear. I have now fixed that.