Yee.
Yee.

Reputation: 90

__self__ of a new class method still refers to the previous class?

What I want to do seems weird, in short, I am trying to build a new class and copy instance/static/class methods from another class without doing inheritance (i.e., I only want a class with only a few methods). As I looked it up on the web, it seems people were suggesting what I did in illustration (New Class), but as I tried to test it, the class method declared in the new class seems to still point to the previous class (Old Class).

This is what I did:


class OldClass(object):
    current_count = 1

    def __init__(self):
        pass

    @classmethod
    def method_class(cls):
        cls.current_count += 1

    def method_normal(self):
        return 2


# new class
class NewClass(object):
    method_class_copy = OldClass.method_class
    method_normal_copy = OldClass.method_normal

Checking __self__ gives the following:

class_self = getattr(NewClass.method_class_copy, '__self__', None)
print(class_self)

it returns:

<class '__main__.OldClass'>

but checking the instance method after being instantiated:

instance_self = getattr(NewClass().method_normal_copy, '__self__', None)
print(instance_self)

as you can see, it still refers to the OldClass, and if I run the class method NewClass.method_class_copy(), the OldClass.current_count will increase its value the way I defined.

it returns:

<__main__.NewClass object at 0x000001D8CC1AEA88>

Thanks

Upvotes: 2

Views: 81

Answers (2)

MisterMiyagi
MisterMiyagi

Reputation: 52139

This is an effect of the descriptor protocol and how classmethod uses it: A classmethod is bound to its class by being looked up on it. Loosely speaking, the lookup some_cls.some_classmethod returns the method with the cls parameter pre-filled to some_class.

Since "normal" methods are bound to their instance by being looked up on the instance, looking them up on the class does not bind them already.

>>> class Foo:
...    def some_normalmethod(self): ...
...    @classmethod
...    def some_classmethod(cls): ...
...
>>> Foo.some_classmethod
<bound method Foo.some_classmethod of <class '__main__.Foo'>>
>>> Foo.some_normalmethod
<function __main__.Foo.some_normalmethod(self)>

Thus, "copying" a classmethod by fetching it from its class does not work as expected (it is bound to the class already), whereas "copying" a normal method does work (it is not bound to an instance).


In order to "copy" a classmethod, extract its underlying function and create a new classmethod from it:

>>> bound_cm = Foo.some_classmethod  # bound classmethod
>>> base_cm = bound_cm.__func__      # function underlying classmethod
>>> class Bar:
...     some_classmethod = classmethod(base_cm)  # new classmethod of same function
...
>>> Bar.some_classmethod
<bound method Foo.some_classmethod of <class '__main__.Bar'>>

Note that this creates a "copy" of the classmethod, not the underlying function. Certain metadata, e.g. the name Foo.some_classmethod, still points to its origin.

If the original classmethod object is desired, circumventing the descriptor protocol gives direct access without binding the method.

>>> Foo.__dict__['some_classmethod']
<classmethod at 0x10b6d9d00>
>>> class Qux:
...     some_classmethod = Foo.__dict__['some_classmethod']
...
>>> Qux.some_classmethod
<bound method Foo.some_classmethod of <class '__main__.Qux'>>

Upvotes: 2

Alex P
Alex P

Reputation: 1155

You can do what you want by defining the methods as class methods, only after you build the second class.

class OldClass(object):
    current_count = 1

    def __init__(self):
        pass

    def method_class(cls):
        cls.current_count += 1
    
    def method_normal(self):
        return 2


# new class
class NewClass(object):
    method_class_copy = OldClass.method_class
    method_normal_copy = OldClass.method_normal


OldClass.method_class = classmethod(OldClass.method_class)
NewClass.method_class_copy = classmethod(NewClass.method_class_copy)

If you test this with

class_self = getattr(OldClass.method_class, '__self__', None)
print(class_self)
instance_self = getattr(OldClass().method_normal, '__self__', None)
print(instance_self)

print()

class_self = getattr(NewClass.method_class_copy, '__self__', None)
print(class_self)
instance_self = getattr(NewClass().method_normal_copy, '__self__', None)
print(instance_self)

you get

<class '__main__.OldClass'>
<__main__.OldClass object at 0x7f1adc174ac0>

<class '__main__.NewClass'>
<__main__.NewClass object at 0x7f1adc138c70>

which is what you want.

Edit: You can also do this dynamically (use @classmethod in OldClass), by first making all class methods of the OldClass static with staticmethod(), then creating the NewClass and then calling classmethod().

Upvotes: 0

Related Questions