'How does one cast a super class instance as a child class instance in Python OOP?

For example,

# low level file Foo.py
class Foo:
   def __init__(...):
      # a class that is difficult to construct, as it is connected to configuration
      # files and schemas

      # it is also useful to import and use early prior to the interpretation of other 
      # classes since it is a configured entity
# resource file resource.py
cfg = load_config('config.json')
foo = Foo.from_config(cfg)
# enum file TheFoos.py
from resource import foo

class TheFoos:

    A = 'a'
    B = 'b'

    _ctx = foo.ctx
# descendant class with useful methods in UpgradedFoo.py
from Foo import Foo

class UpgradedFoo(Foo):

    def do_fabble(self, ...):
        pass

Finally, the crux of the question:

# interface for end user interface.py
from resources import foo
from UpgradedFoo import UpgradedFoo

upgraded_foo = UpgradedFoo(foo)  # the pythonic pseudo code of the C++ way

And in a large variety of downstream files:

from deep.internal.interface import upgraded_foo

upgraded_foo.do_fabble(...)

And a large variety of legacy files:

from deep.internal.TheFoos import TheFoos

TheFoos.do_something_configured_just_once_in_new_code()

So winding back around to the impasse:

upgraded_foo = UpgradedFoo(foo) # the c++-ish way

# but what is the python way?

But what is the way this is done in python?

I have tried: from typing import cast; cast(UpgradedFoo, foo), but that did not work. I have also tried researching the question, but this seems to be a sticking point between the experts and the noobs, and there is no real good answer that actually confronts and addresses the issue directly.

The idea is to get this functioning so there are no circular imports and code parameters are all based on a single configuration without inducing tremendous change propagation in the code.

Please note that while the above is not some sort of ideal architecture, it is along the path to an ideal architecture having integrated old work from many authors with new work.


Motivation aside, the need here is to upcast the base class instance to the inherited class instance.


Note on the "C++ way" (all details):

Suppose a base class and a derived class exist, and that the "data" held by the base class is identical to the derived class -- there is no "extra" sort of data in the derived class relative to the base class.

Then a pointer can be generated and cast to the derived class for derived class functionality. The pointer only "cares" about the instance data and where the binary functions are. The actual bundle of functions associated by the compiler with that data can be upgraded to derived class context w/ the cast operation.

#include<iostream>

class A {
public:
  int foo;
  A(int x): foo(x){};
};

class B : public A {
  public: int bar() {return this-> foo * 2;};
};

int main() {
  A a = A(1);

  A* astar = &a;
  B* bstar = (B*) &a;
  B b = *bstar;

  // symbolic cast
  B b2 = *(B*)&a;
  

  std::cout << astar << std::endl;
  std::cout << bstar << std::endl;
  std::cout << bstar->bar() << std::endl;
  std::cout << b.bar() << std::endl;
  std::cout << b2.bar() << std::endl;
}
// output:
0x#####
0x#####
2
2
2


Solution 1:[1]

So I suppose this is a philosophical choice in python, but it looks like a push model of change propagation is deployed, and there is no way out.

If there is a base class, and a descendant class pulls that base class content into it:

class Foo:
   ...

class DeepFoo(Foo):
   ...

Then if DeepFoo is to be wrapped around an initialized Foo in some context, the DeepFoo Must know how to initialize itself from a Foo by reversing Foo's data dictionary to Foo's input contract -- or lose the root class constructor:

class DeepFoo(Foo):
   def __init__(self, foo: Foo):  # overriding the root constructor
       self.__dict__ = deepcopy(foo.__dict__)

Further, if a DeepFoo.from_foo(...) is possible and defined, the Foo cannot inform the DeepFoo about its own changes -- the DeepFoo is responsible for discovering and keeping up with the Foo in order to have the data necessary for all inherited Foo functions to function properly when DeepFoo is wrapped around a pre-existing Foo.

Although python is a dynamic language, dynamic casting is not available, and one cannot simply "own" the data contents of another class without actually going through the construction process.

That means that, when this point is reached in code and it is desirable to split a class for whatever reason -- such as a difficult reverse engineering position -- into a base and descendant, it may be necessary to shift to a nested, dormant wrapper model that, while similar to the first copy method, at least is what it claims to be:

class Wrapper:
    def __init__(self, item: Any):
        self.__item = item
    
        def __getattr__(self, item: str) -> None:
            return eval(f"self.item.{item}")

class DeepFoo(Wrapper):
    def __init__(self, foo: Foo):
        self.super().__init__(foo)
    
    def do_fabble(self, args):
        # use self.item here!

And, in this scenario there is a downside in that the IDE intellisense is now disconnected such that the Foo methods DeepFoo has access to are private (or dormant) for the purposes of intellisense, but they are there during dynamic evaluation. However, the wrapper here can serve as a placeholder that is not misleading as functions are used to replace the DeepFoo.do_fabble in downstream code.


A third alternative that I thought of which minimizes any "hackery" and keystrokes is to keep the DeepFoo name around, but make it contain only @staticmethods

# So

class DeepFoo(Foo):
   def do_fabble(self, ...):
      pass


# becomes
class DeepFoo:
   @staticmethod
   def do_fabble(self: Foo, ...):
       ... 

And all downstream changes to dependencies on DeepFoo still propagate, but are relatively casual and only propagate once.


The last alternative got me past all import issues, got my legacy code tethered to my new configuration file, and prevented a large refactor without generating a lot of labor or code that is difficult to understand.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1