1

Imagine you have the following code:

class A:
    pass

NewA = ... # copy A
NewA.__init__ = decorator(A.__init__) # but don't change A's init function, just NewA's

I am looking for a way to change some of the attributes/methods in the cloned class and the rest I want them to be similar to the base class object (preferebly even through MappingProxyType so that when A changes the unchanged logic of NewA reflects the changes as well).

I came across this ancient thread, where there some suggetions which don't fully work:

  1. Repurposing inheritance class NewA(A): pass which doesn't exactly result in what I am looking for
  2. Dynamically generating a new class using type and somehow having an eye out for the tons of cases that might happen (having mutable attributes/descriptors/calls to globals ...)
  3. Using copy.deepcopy which is totally wrong (since class object's internal data representation is a MappingProxyType which we cannot copy/deepcopy)

Is there a way to still achive this without manually handling every corner case, especially considering the fact that the base class we intend to copy could be anything (with metaclasses and custom init_subclass parents, and a mixture of attributes mutable and what not, and potentially with __slots__)?

7
  • No, there is no generic way to clone anything in the way you want. You can copy a mapping proxy, something like types.MappingProxy(dict(mapping_proxy)) Commented Feb 16, 2023 at 9:26
  • Also, I don't see why you'd need to keep an eye out for descriptors/mutable attributes. I don't know what you mean by "calls to globals". But you can always just deepcopy the .__dict__ attribute (convert it to a dict first, then deep copy, then pass to type). Commented Feb 16, 2023 at 9:28
  • If a method in the base class calls globals() it refers to the globals available in the module it was implemented. I'm not sure whether regenerating a class with type rebinds the methods or not, but in case it does we might need to manually update the locals/globals for each newly generated method. Commented Feb 16, 2023 at 9:31
  • 1
    How does option 1 with inheritance not meet your requirements? Commented Feb 16, 2023 at 9:34
  • 1
    Generating a class with type doesn't "rebind" anything. You are explicitly providing the methods as the class namespace (and inherited methods by providing the bases). Commented Feb 16, 2023 at 9:41

1 Answer 1

1

Here is a humble attempt to get you started. I've tested it out with a class with slots and it seems to work. I am not very sure about that aspect of it though.

import types
import copy

def clone_class(klass):

    def _exec_body(ns):
        # don't add in slots descriptors, it will fail!
        ns_no_slots = {
            k:v for k,v in vars(klass).items()
            if not isinstance(v, types.MemberDescriptorType)
        }
        ns |= copy.deepcopy(ns_no_slots)
        return ns

    return types.new_class(
        name=klass.__name__,
        bases=klass.__bases__,
        kwds={"metaclass": type(klass)},
        exec_body=_exec_body,
    )

Now, this seems to work with classes that have __slots__. The one thing that might trip things up is if the metaclass has slots (which must be empty). But that would be really weird.

Here is a test script:

import types
import copy

def clone_class(klass):
    def _exec_body(ns):
        ns_no_slots = {
            k:v for k,v in vars(klass).items()
            if not isinstance(v, types.MemberDescriptorType)
        }
        ns |= copy.deepcopy(ns_no_slots)
        return ns
    return types.new_class(
        name=klass.__name__,
        bases=klass.__bases__,
        kwds={"metaclass": type(klass)},
        exec_body=_exec_body,
    )


class Meta(type):
    def meta_magic(cls):
        print("magical!")

class Foo(metaclass=Meta):
    __slots__ = ('x','y')
    @property
    def size(self):
        return 42

class Bar(Foo):
    state = []
    __slots__ = ('z',)
    def __init__(self, x=1, y=2, z=3):
        self.x = x
        self.y = y
        self.z = z
    @property
    def closed(self):
        return False

BarClone = clone_class(Bar)

bar = BarClone()
BarClone.state.append('foo')
print(BarClone.state, Bar.state)
BarClone.meta_magic()

This prints:

['foo'] []
magical!
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.