Mainly for modularity. As it is now, with the classes separate, you can write a program that uses PNMImage (for instance, image-trans or image-resize) and link it with just a small part of Panda3D. If PNMImage were the same thing as Texture, then such a program would have to link with pretty much all of Panda3D, including all of the graphics context and rendering code, even though it’s never going to do any rendering.
This is the function of classes, to separate different concepts from each other. To take an extreme example, we could technically combine all of the different classes in Panda into one enormous class definition that did everything. But then it wouldn’t be very object-oriented any more, would it?
Though this is technically possible, it would be difficult. One of the properties of PNMImage is that all images are laid out (almost) the same way in memory, regardless of their properties. This makes it very easy to write a class like PNMPainter, which can paint equally well on grayscale, grayscale/alpha, 24-bit, 32-bit, or 64-bit images. However, the memory in a Texture is laid out differently according to its texture properties, to match the format the graphics hardware expects, for fast upload to the graphics hardware. Thus, to make a PNMPainter paint directly onto a Texture, there would have to be an interface layer that does the conversion to the appropriate format. But that’s exactly what a PNMImage is, isn’t it?
I suppose you could have a class that did this automatically, without the need to explicitly call Texture.load() from your PNMImage. But that doesn’t sound that useful, and it wouldn’t run any faster than if you made the call yourself. It would just hide the underlying operation from you.