Copartition two QueryCompiler objects.

        Args:
            axis: The axis to copartition along.
            other: The other Query Compiler(s) to copartition against.
            how_to_join: How to manage joining the index object ("left", "right", etc.)
            sort: Whether or not to sort the joined index.
            force_repartition: Whether or not to force the repartitioning. By default,
                this method will skip repartitioning if it is possible. This is because
                reindexing is extremely inefficient. Because this method is used to
                `join` or `append`, it is vital that the internal indices match.

        Returns:
            A tuple (left query compiler, right query compiler list, joined index).

def copartition(self, axis, other, how_to_join, sort, force_repartition=False):
        """Copartition two QueryCompiler objects.

        Args:
            axis: The axis to copartition along.
            other: The other Query Compiler(s) to copartition against.
            how_to_join: How to manage joining the index object ("left", "right", etc.)
            sort: Whether or not to sort the joined index.
            force_repartition: Whether or not to force the repartitioning. By default,
                this method will skip repartitioning if it is possible. This is because
                reindexing is extremely inefficient. Because this method is used to
                `join` or `append`, it is vital that the internal indices match.

        Returns:
            A tuple (left query compiler, right query compiler list, joined index).
        """
        if isinstance(other, type(self)):
            other = [other]

        index_obj = (
            [o.index for o in other] if axis == 0 else [o.columns for o in other]
        )
        joined_index = self._join_index_objects(
            axis ^ 1, index_obj, how_to_join, sort=sort
        )
        # We have to set these because otherwise when we perform the functions it may
        # end up serializing this entire object.
        left_old_idx = self.index if axis == 0 else self.columns
        right_old_idxes = index_obj

        # Start with this and we'll repartition the first time, and then not again.
        reindexed_self = self.data
        reindexed_other_list = []

        def compute_reindex(old_idx):
            """Create a function based on the old index and axis.

            Args:
                old_idx: The old index/columns

            Returns:
                A function that will be run in each partition.
            """

            def reindex_partition(df):
                if axis == 0:
                    df.index = old_idx
                    new_df = df.reindex(index=joined_index)
                    new_df.index = pandas.RangeIndex(len(new_df.index))
                else:
                    df.columns = old_idx
                    new_df = df.reindex(columns=joined_index)
                    new_df.columns = pandas.RangeIndex(len(new_df.columns))
                return new_df

            return reindex_partition

        for i in range(len(other)):

            # If the indices are equal we can skip partitioning so long as we are not
            # forced to repartition. See note above about `force_repartition`.
            if i != 0 or (left_old_idx.equals(joined_index) and not force_repartition):
                reindex_left = None
            else:
                reindex_left = self._prepare_method(compute_reindex(left_old_idx))
            if right_old_idxes[i].equals(joined_index) and not force_repartition:
                reindex_right = None
            else:
                reindex_right = other[i]._prepare_method(
                    compute_reindex(right_old_idxes[i])
                )

            reindexed_self, reindexed_other = reindexed_self.copartition_datasets(
                axis, other[i].data, reindex_left, reindex_right
            )
            reindexed_other_list.append(reindexed_other)
        return reindexed_self, reindexed_other_list, joined_index