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__)?

Vahid Zee
  • 11
  • 1
  • 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))` – juanpa.arrivillaga Feb 16 '23 at 09: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`). – juanpa.arrivillaga Feb 16 '23 at 09: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. – Vahid Zee Feb 16 '23 at 09:31
  • 1
    How does option 1 with inheritance not meet your requirements? – matszwecja Feb 16 '23 at 09: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). – juanpa.arrivillaga Feb 16 '23 at 09:41
  • so start by `namespace_copy = copy.deepcopy(dict(Foo.__dict__))`. Then you can just use `Foo.__bases__` and `Foo.__name__`. I'm still not sure about `__slots__` though... Also, you should probably use `types.new_class`, which correctly handles metaclasses – juanpa.arrivillaga Feb 16 '23 at 09:46
  • Still wondering why you don't want to simply use inheritance there - seens to get all your requisites covered, but for you stating that inheritance won't work for you. – jsbueno Apr 12 '23 at 19:02

1 Answers1

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!
juanpa.arrivillaga
  • 88,713
  • 10
  • 131
  • 172