Make comm manager (mostly) independent of InteractiveShell

This makes it possible to use comms from wrapper kernels, without instantiating the full IPython shell machinery.

The one remaining place where we need a reference to shell is to fire pre_execute and post_execute hooks (which are needed to get mpl figures right). This is a pure IPythonism, that it should be safe to ignore if shell is not set.

Ping @dsblank - you were asking about this in #6314.
It can be during traitlet initialization. Setting a traitlet triggers its `_default` method (I think in order to determine if `_changed` should fire).  So `CommManager(session=foo, kernel=k)` can trigger this call with `kernel=None`, depending on dictionary ordering.