<SYSTEM_TASK:>
Solve the following problem using Python, implementing the functions described below, one line at a time
<END_TASK>
<USER_TASK:>
Description:
 def apply_unitary(unitary_value: Any, args: ApplyUnitaryArgs, default: TDefault = RaiseTypeErrorIfNotProvided ) -> Union[np.ndarray, TDefault]: """High performance left-multiplication of a unitary effect onto a tensor. If `unitary_value` defines an `_apply_unitary_` method, that method will be used to apply `unitary_value`'s unitary effect to the target tensor. Otherwise, if `unitary_value` defines a `_unitary_` method, its unitary matrix will be retrieved and applied using a generic method. Otherwise the application fails, and either an exception is raised or the specified default value is returned. Args: unitary_value: The value with a unitary effect to apply to the target. args: A mutable `cirq.ApplyUnitaryArgs` object describing the target tensor, available workspace, and axes to operate on. The attributes of this object will be mutated as part of computing the result. default: What should be returned if `unitary_value` doesn't have a unitary effect. If not specified, a TypeError is raised instead of returning a default value. Returns: If the receiving object is not able to apply its unitary effect, the specified default value is returned (or a TypeError is raised). If this occurs, then `target_tensor` should not have been mutated. If the receiving object was able to work inline, directly mutating target_tensor it will return target_tensor. The caller is responsible for checking if the result is target_tensor. If the receiving object wrote its output over available_buffer, the result will be available_buffer. The caller is responsible for checking if the result is available_buffer (and e.g. swapping the buffer for the target tensor before the next call). The receiving object may also write its output over a new buffer that it created, in which case that new array is returned. Raises: TypeError: `unitary_value` doesn't have a unitary effect and `default` wasn't specified. """


    # Check if the specialized method is present.
    func = getattr(unitary_value, '_apply_unitary_', None)
    if func is not None:
        result = func(args)
        if result is not NotImplemented and result is not None:
            return result

    # Fallback to using the object's _unitary_ matrix.
    matrix = unitary(unitary_value, None)
    if matrix is not None:
        # Special case for single-qubit operations.
        if matrix.shape == (2, 2):
            zero = args.subspace_index(0)
            one = args.subspace_index(1)
            return linalg.apply_matrix_to_slices(args.target_tensor,
                                                 matrix,
                                                 [zero, one],
                                                 out=args.available_buffer)

        # Fallback to np.einsum for the general case.
        return linalg.targeted_left_multiply(
            matrix.astype(args.target_tensor.dtype).reshape(
                (2,) * (2 * len(args.axes))),
            args.target_tensor,
            args.axes,
            out=args.available_buffer)

    # Don't know how to apply. Fallback to specified default behavior.
    if default is not RaiseTypeErrorIfNotProvided:
        return default
    raise TypeError(
        "object of type '{}' has no _apply_unitary_ or _unitary_ methods "
        "(or they returned None or NotImplemented).".format(
            type(unitary_value)))