ENH: add a method for piping self instead of the function return value
#### Is your feature request related to a problem?

Pandas has the `.pipe` method to pipe a function result to the next step in a ETL process. However, this method is awkward when calling functions where you want to pipe self and not the function output.

There are many situations where piping self is wanted:
* validation
* logging
* charting
* saving to file / database etc.
* inplace operations on a DataFrame/Series, e.g. changing index or columns

and probably other needs as well.

To use the above functionality in pipes in pandas now, you need to explicitly return the input dataframe even though that isolated doesn't make sense. So, instead of the natural validation function:

```python
def validate(df: pd.DataFrame) -> None:
    ... # validate something
```

You currently usually do:

```python
def validate(df: pd.DataFrame) -> pd.DataFrame:
    ... # validate something
    return df
```

Returning a df indicates that the function returns somethings modified, even though that is not the case here, so this makes thing less clear in the piped functions.

Also, reusing external functions that don't return anything is more difficult in pandas currently, because you have to wrap them before using them.

Lastly, using lambdas in `.pipe` to work around the problem is problematic, because lambdas don't have a return type, so e.g.

```python
new_df = (df.pipe(manipulate_1)
            .pipe(lambda x: x.plot.bar() and x)
            .pipe(manipulate_2)
)
```

loses the type information in the second step.

#### Describe the solution you'd like

I propose adding a `.do` method that is implemented like

```python
def do(self: T, func: Union[Callable, Tuple[Callable, str]], *args, **kwargs) -> T:
    self.pipe(func, *args, **kwargs)
    return self
```

This means `.pipe` will be for when you want to pipe the function result and `.do` is for when you want to pipe self. Notice the different signature compared to `.pipe`.

This method is much more natural to use for all the steps listed above + it always understands the return type.

#### API breaking implications

This is a new method, so no breaking implications.

#### Describe alternatives you've considered

Alternatives - in addition to always add a `return df` inside functions that naturally would return None - may be using lambdas:

```python
new_df = (df.pipe(manipulate_1)
            .pipe(lambda x: validate(x); x)  # notice the semi-colon
            .pipe(manipulate(2)
)
```

or to make a seperate user-created `.do` function:

```python
def do(obj: T, func, *args, **kwars) -> T:
    func(obj, *args, **kwargs)
    return obj

new_df = (df.pipe(manipulate_1)
            .pipe(do, validate)
            .pipe(manipulate_2)
)
```