How to add a classmethod in Python dynamically
Asked Answered
I

4

16

I'm using Python 3. I know about the @classmethod decorator. Also, I know that classmethods can be called from instances.

class HappyClass(object):
    @classmethod
    def say_hello():
        print('hello')
HappyClass.say_hello() # hello
HappyClass().say_hello() # hello

However, I don't seem to be able to create class methods dynamically AND let them be called from instances. Let's say I want something like

class SadClass(object):
    def __init__(self, *args, **kwargs):
        # create a class method say_dynamic

SadClass.say_dynamic() # prints "dynamic!"
SadClass().say_dynamic() # prints "dynamic!"

I've played with cls.__dict__ (which produces exceptions), and with setattr(cls, 'say_dynamic', blahblah) (which only makes the thingie callable from the class and not the instance).

If you ask me why, I wanted to make a lazy class property. But it cannot be called from instances.

@classmethod
def search_url(cls):
    if  hasattr(cls, '_search_url'):
        setattr(cls, '_search_url', reverse('%s-search' % cls._meta.model_name))
    return cls._search_url

Maybe because the property hasn't been called from the class yet...

In summary, I want to add a lazy, class method that can be called from the instance... Can this be achieved in an elegant (nottoomanylines) way?

Any thoughts?

How I achieved it

Sorry, my examples were very bad ones :\

Anyway, in the end I did it like this...

@classmethod
def search_url(cls):
    if not hasattr(cls, '_search_url'):
        setattr(cls, '_search_url', reverse('%s-search' % cls._meta.model_name))
    return cls._search_url

And the setattr does work, but I had made a mistake when testing it...

Illomened answered 2/7, 2014 at 18:50 Comment(1)
Why the setattr with a constant 2nd argument? You could as well write cls._search_url = reverse(...).Bronze
I
-1

How I achieved it:

@classmethod
def search_url(cls):
    if not hasattr(cls, '_search_url'):
        setattr(cls, '_search_url', reverse('%s-search' % cls._meta.model_name))
    return cls._search_url
Illomened answered 22/3, 2017 at 21:35 Comment(0)
B
18

You can add a function to a class at any point, a practice known as monkey-patching:

class SadClass:
    pass

@classmethod
def say_dynamic(cls):
    print('hello')
SadClass.say_dynamic = say_dynamic

>>> SadClass.say_dynamic()
hello
>>> SadClass().say_dynamic()
hello

Note that you are using the classmethod decorator, but your function accepts no arguments, which indicates that it's designed to be a static method. Did you mean to use staticmethod instead?

Bronze answered 2/7, 2014 at 19:10 Comment(2)
Is it okay if you use @classmethod decorator outside of class? The code works, but I worry if is this a really pythonic way to solve the problem.Bucolic
@AlexeiMarinichenko It's ok to use @classmethod outside of a class if you plan to attach the resulting function object to a class. Another matter is whether you need such monkey-patching in the first place. (You almost never do.)Bronze
A
11

If you want to create class methods, do not create them in the __init__ function as it is then recreated for each instance creation. However, following works:

class SadClass(object):
    pass

def say_dynamic(cls):
    print("dynamic")

SadClass.say_dynamic = classmethod(say_dynamic)
# or 
setattr(SadClass, 'say_dynamic', classmethod(say_dynamic))

SadClass.say_dynamic() # prints "dynamic!"
SadClass().say_dynamic() # prints "dynamic!"

Of course, in the __init__ method the self argument is an instance, and not the class: to put the method in the class there, you can hack something like

class SadClass(object):
    def __init__(self, *args, **kwargs):
        @classmethod
        def say_dynamic(cls):
            print("dynamic!")

        setattr(self.__class__, 'say_dynamic', say_dynamic)

But it will again reset the method for each instance creation, possibly needlessly. And notice that your code most probably fails because you are calling the SadClass.say_dynamic() before any instances are created, and thus before the class method is injected.

Also, notice that a classmethod gets the implicit class argument cls; if you do want your function to be called without any arguments, use the staticmethod decorator.

Amnesia answered 2/7, 2014 at 19:8 Comment(2)
self should be cls in the first part of your example.Keratoid
You're right, writing it inside init would make kind of put extra work unnecessarily. My example was bad, but I actually did need the cls argument in real life. Sorry about that >.<Illomened
S
0

As a side note, you can just use an instance attribute to hold a function:

>>> class Test:
...    pass
... 
>>> t=Test()
>>> t.monkey_patch=lambda s: print(s)
>>> t.monkey_patch('Hello from the monkey patch')
Hello from the monkey patch
Scriven answered 2/7, 2014 at 20:13 Comment(0)
I
-1

How I achieved it:

@classmethod
def search_url(cls):
    if not hasattr(cls, '_search_url'):
        setattr(cls, '_search_url', reverse('%s-search' % cls._meta.model_name))
    return cls._search_url
Illomened answered 22/3, 2017 at 21:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.