### Apache Airflow version

2.3.0

### What happened

When using a `PythonVirtualenvOperator` it is checked if `virtualenv` is installed by 
`if not shutil.which("virtualenv"):`
https://github.com/apache/airflow/blob/a1679be85aa49c0d6a7ba2c31acb519a5bcdf594/airflow/operators/python.py#L398
Actually, this expression checks if `virtualenv` is on PATH. If Airflow is installed in a virtual environment itself and `virtualenv` is not installed in the environment the check might pass but `virtualenv` cannot be used as it is not present in the environment.

### What you think should happen instead

It should be checked if `virtualenv` is actually available in the environment.

```python
if importlib.util.find_spec("virtualenv") is None:
    raise AirflowException('PythonVirtualenvOperator requires virtualenv, please install it.')
```
https://stackoverflow.com/a/14050282

### How to reproduce

_No response_

### Operating System

Ubuntu 20.04

### Versions of Apache Airflow Providers

_No response_

### Deployment

Virtualenv installation

### Deployment details

_No response_

### Anything else

_No response_

### Are you willing to submit PR?

- [X] Yes I am willing to submit a PR!

### Code of Conduct

- [X] I agree to follow this project's [Code of Conduct](https://github.com/apache/airflow/blob/main/CODE_OF_CONDUCT.md)

branch = super().execute(context)
        self.log.info("Branch callable return %s", branch)
        self.skip_all_except(context["ti"], branch)
        return branch
class ShortCircuitOperator(PythonOperator, SkipMixin):
    """
    Allows a pipeline to continue based on the result of a ``python_callable``.
    The ShortCircuitOperator is derived from the PythonOperator and evaluates the result of a
    ``python_callable``. If the returned result is False or a falsy value, the pipeline will be
    short-circuited. Downstream tasks will be marked with a state of "skipped" based on the short-circuiting
    mode configured. If the returned result is True or a truthy value, downstream tasks proceed as normal and
    an ``XCom`` of the returned result is pushed.
    The short-circuiting can be configured to either respect or ignore the ``trigger_rule`` set for
    downstream tasks. If ``ignore_downstream_trigger_rules`` is set to True, the default setting, all
    downstream tasks are skipped without considering the ``trigger_rule`` defined for tasks. However, if this
    parameter is set to False, the direct downstream tasks are skipped but the specified ``trigger_rule`` for
    other subsequent downstream tasks are respected. In this mode, the operator assumes the direct downstream
    tasks were purposely meant to be skipped but perhaps not other subsequent tasks.
    .. seealso::
        For more information on how to use this operator, take a look at the guide:
        :ref:`howto/operator:ShortCircuitOperator`
    :param ignore_downstream_trigger_rules: If set to True, all downstream tasks from this operator task will
        be skipped. This is the default behavior. If set to False, the direct, downstream task(s) will be
        skipped but the ``trigger_rule`` defined for all other downstream tasks will be respected.
    """
    def __init__(self, *, ignore_downstream_trigger_rules: bool = True, **kwargs) -> None: