I was trying to set a type attribute to a class using the Python built-in setattr. I've declared __new__ and __init__ methods in the type to see what their parameters would be and surprisingly they're not being bound to receive the class instance. I've read the Python docs on setattr and descriptors and I've performed some tests in the interpreter and I haven't found a way to bind the type's __new__ or __init__ methods to the class instance.
This is the code fragment I've been toying with:
T = type("T", (object,), {"__new__": lambda cls: print(f"T.__new__: {cls}") or object.__new__(cls), "__init__": lambda self: print(f"T.__init__: {self}")})
T()
# T.__new__: <class '__main__.T'>
# T.__init__: <__main__.T object at 0x7f970b667c10>
# <__main__.T object at 0x7f970b667c10>
class A:
pass
setattr(A, "T", T)
A.T()
# T.__new__: <class '__main__.T'>
# T.__init__: <__main__.T object at 0x7f970b6675e0>
# <__main__.T object at 0x7f970b6675e0>
A().T()
# T.__new__: <class '__main__.T'>
# T.__init__: <__main__.T object at 0x7f970b667ee0>
# <__main__.T object at 0x7f970b667ee0>
Essentially I want to know how can I make T receive the instance of A in the __new__ or __init__ methods. I believe that I didn't fully understand how setattr actually works and I'm misusing it or the way to accomplish this behaviour is not related to setattr at all.
PS.: Declaring T as a regular class changes nothing and declaring T::__get__ changes nothing as well.
The
setattrfunction is just a way to programatically do what you could do with a normal assignment. Your callsetattr(A, "T", T)is exactly the same as doingA.T = T. It doesn't help you achieve what you seem to want, which is for the classTto have binding behavior when looked up inA(or maybe in anAinstance).While you could make a metaclass that makes the
Ttype you declare as an attribute of theAclass a descriptor, a much simpler approach is probably to write a method ofAthat returns an instance of theTclass without actually being the class itself.Now you can do
A().make_T()and you'll get aTinstance that was passed theAinstance as an argument to its__init__method. If you want to, you can even renamemake_TtoT, and it will mostly work like you intended with your nested classes. It's not quite the same, since you can't useA.Tas a class in other contexts, likeisinstancechecks. Using a name likemake_Tis a little bit clearer that it's a factory method, not a class itself.If you really do need to put the class
Tinside ofA, here's the metaclass approach:That metaclass is a lot more complicated and subtle than code really should be if you want to be able to read and maintain it. It creates a subclass of
Tfor eachAinstance you look the original class up on. That might be very confusing, in some situations (like the lastisinstancecheck in the example code)!Here are some of the subtleties: We need to be selective about which of
__init__and__new__we create because if we unconditionally create both, we'll get errors ifTdoesn't define them both as takingobjas a positional argument. Using naive binding (the way methods do), you'd end up with a potentially infinite number of classes, since you'd create a new subclass for every lookup (a.Twould be a different class each time). To avoid that, I cache the subclasses usingsetattr(bringing us back full circle!).I'd strongly recommend against using this kind of design for any serious code. This class architecture really stinks of trying to force Python into a design that would fit more naturally in some other programming language, where inner classes are a normal thing. It's almost certainly unnecessary to design the classes this way, there's likely to be a slightly modified design that is much more natural to Python's class model. Do yourself (and anyone who ever needs to read your code in the future) a huge favor and figure out what that better design is, rather than using a metaclass monstrosity that will be very tricky to understand or modify.