Search code examples
pythonmetaprogramming

Copying a class object in Python


Suppose I have a complicated class with lots of methods. In particular it is callable via a __call__ member. I would like to have an object from the exact same class, but not callable and not with __call__ at all in fact, because some introspection tool would detect it and this is not wanted.

The obvious solution is to make a new class, copying the code from the original one, and removing __call__. But is there a way to do this programmatically?

I went from:

import copy

class A:
    def __call__(self):
       print(self, "CALLED")

B = copy.deepcopy(A) # for some reason it is not a real copy...
del B.__call__ # seems to work, but also removes `__call__` from A !!!

a=A()
a() # TypeError: 'A' is not callable

# this was the more surprising to me...

to:

class B(A):
    def __getattribute__(self, attr):
        if attr == "__call__":
            raise AttributeError(attr)
        return super().__getattribute__(attr)

b = B()
b.__call__ # raises attribute error, nice
b() # still calls A.__call__... WTF!

passing through many attempts.

Is there a way? How to get a true copy of a class object (not an instance) in Python?

I checked existing Q&A, but none seem to address this particular issue. I also thought about extracting source code, and removing the __call__ method from the string and having it re-evaluated.


Solution

  • You can use type() to dynamically create a new class without the specified method:

    def remove_method_from_class(cls, new_class_name: str, method_name: str):
        attrs = {k: v for k, v in cls.__dict__.items() if k != method_name}
        return type(new_class_name, cls.__bases__, attrs)
    
    class A:
        def __call__(self):
            print(self, "CALLED")
        
        def other_method(self):
            print("Other method")
    
    B = remove_method_from_class(A, "B", "__call__")
    
    a = A()
    a()  # This should work
    
    b = B()
    # b()  # This raises a TypeError because B is not callable
    print(hasattr(b, '__call__'))  # Prints False
    print(hasattr(a, '__call__'))  # Prints True
    b.other_method()  # This should still work