Repository: pytorch
Issue: Nice way to override string representation of tensor subclasses
Labels: triaged,enhancement,tensor subclass

### 🐛 Describe the bug

It's natural for tensor subclass writers to want to control how a subclass is printed. The pythonic way to do this is to override `__repr__()` or `__str__()`:

```python
class WrapperTensor(torch.Tensor):
    @staticmethod
    def __new__(cls, e):
        r = torch.Tensor._make_wrapper_subclass(cls, e.shape, dtype=e.dtype, requires_grad=False)
        r.elem = e
        return r

    __torch_function__ = torch._C._disabled_torch_function_impl

    def __repr__(self):
        return f'WrapperTensor({self.elem})'
```

While this seems okay on the surface, fully overriding `__repr__()` or `__str__()` leads to subtle violations of printing expectations. Specifically, additional properties that may be associated with a tensor subclass instance are no longer printed. Examples:
* Parameter-ness - is the instance a parameter? Its printed representation should indicate this.
* Dual-ness: is the instance a dual created with `torch.autograd.forward_ad.make_dual()`? Likely it'd be useful to include the tangent in the printed representation.
* etc.

Generally, these properties are composable with subclass-ness and each other, and there is an open set of them. Forcing the subclass writer to handle them explicitly in their `__repr__()` isn't a great solution.

```python
# Custom __repr__() has to know about properties the instance could have to print
# correctly (!)
def __repr__(self):
    # Handle printing tangent if present.
    primal, tangent = torch.autograd.forward_ad.unpack_dual(self)
    repr = f"WrapperTensor({primal}"
    repr += ")" if tangent is None else f", tangent={tangent})"

    # Handle printing parameter-ness.
    if isinstance(self, nn.Parameter):
        repr = f"Parameter({repr})"

    # Handle any other properties that should be in the printed representation.
    ...
```

What's the right way to give subclass writers control over subclass representations while not making them responsible to explicitly handling printing for this open set of properties?

One partial solution to this is to provide a good base `Tensor.__repr__()` implementation that minimizes the need to customize the representation. It should include the subclass name and handle the above tensor properties.

Note that the current `Tensor.__repr__()` includes information that may or not be desired in a subclass representation:
* Tensor contents
* Autograd information (if present)
* Metadata (e.g. device, dtype if they are not the defaults)

Should we provide a granular way to select / override these in the subclass representation? Possible approach [here](https://github.com/pytorch/pytorch/pull/73459/files#r817907751) that hacks in new kwargs to `Tensor.__repr__()`.

```python
class Tensor:
    ...
    def __repr__(self, *, tensor_contents=None):
        # Use provided tensor_contents if present, otherwise build them up in the standard way.
        ...

...

class Subclass(Tensor):
    ...
    def __repr__(self):
        # Get all the base representation (including handling of parameter-ness, dual-ness,
        # whatever) but swap the tensor contents portion out.
        return super().__repr__(tensor_contents="custom stuff")
```

This might be a bad idea! Alternatives welcome :)

### Versions

main