def swap_subtrees(tree_1, tree_2, ind_1, ind_2, keep_2=True):
    """
    Swaps subtrees of argument trees. Subtree position is determined by
    ``ind_1`` and ``ind_2``.

    If ``keep_2`` is False, the subtree from
    ``tree_1`` is not inserted into ``tree_2`` and only the first tree
    is returned.

    :param GpTreeIndividual tree_1: First tree.
    :param GpTreeIndividual tree_2: Second tree.
    :param int ind_1: Index of the subtree of the first subtree.
    :param int ind_2: Index of the subtree of the second tree.
    :param bool keep_2: Indicates whether the second modified tree would be returned.
    :return (GpTreeIndividual, GpTreeIndividual) or (GpTreeIndividual,):
        Returns both trees with swapped subtrees or only the first tree with
        a subtree inserted from ``tree_2`` (according to ``keep_2``).
    """

    # update node heights
    root_height_1 = tree_1.primitives[ind_1].depth
    root_height_2 = tree_2.primitives[ind_2].depth
    height_diff = root_height_1 - root_height_2

    def move_node(prim, diff):
        prim.height = prim.depth + diff
        return prim

    # subtree indices
    ind_begin_1, _ = tree_1.subtree(ind_1)
    ind_begin_2, _ = tree_2.subtree(ind_2)

    ind_end_1 = ind_1 + 1
    ind_end_2 = ind_2 + 1

    # insert into tree_1 - copy subtree from tree_2
    subtree_2 = [move_node(prim, height_diff)
                 for prim in tree_2.primitives[ind_begin_2:ind_end_2]]

    # insert into tree_2
    if keep_2:
        subtree_1 = (move_node(prim, -height_diff)
                     for prim in tree_1.primitives[ind_begin_1:ind_end_1])

        tree_2.primitives[ind_begin_2:ind_end_2] = subtree_1
        tree_2.max_height = max(prim.depth + 1 for prim in tree_2.primitives)  # update height

    # insert into tree_1 - insert subtree
    tree_1.primitives[ind_begin_1: ind_end_1] = subtree_2
    tree_1.max_height = max(prim.depth + 1 for prim in tree_1.primitives)  # update height

    return tree_1, tree_2 if keep_2 else tree_1,