2025, Oct 15 20:16

Две легенды в Matplotlib: почему remove не работает и как исправить

Почему при нескольких легендах в Matplotlib вызовы axes.legend и add_artist создают дубликаты, remove работает странно, и как корректно удалять легенды.

Управлять несколькими легендами в Matplotlib сложнее, чем кажется. Частая ситуация — вы создаёте две легенды на одних осях, а при удалении получаете странное поведение: одна легенда пропадает, другая упорно остаётся, или универсальный цикл очистки выбрасывает исключение. Корень проблемы не в самом remove, а в том, как легенды создаются и привязываются к осям.

Воспроизведение проблемы

Пример ниже строит график с двумя легендами, но в итоге одна из них оказывается зарегистрированной дважды — из‑за этого удаление работает непредсказуемо.

import matplotlib.pyplot as plt
canvas, axes = plt.subplots()
# пример данных
xs = [1, 2, 3, 4, 5]
vals_a = [1, 4, 9, 16, 25]
vals_b = [25, 16, 9, 4, 1]
axes.plot(xs, vals_a, label='y = x^2')
axes.plot(xs, vals_b, label='y = 25 - x^2')
# временная легенда, чтобы получить объекты и подписи
axes.legend()
hnds, lbls = axes.get_legend_handles_labels()
# удаляем временную легенду
axes.get_legend().remove()
# создаём две легенды из собранных объектов и подписей
split_idx = len(hnds) // 2
legend_left = axes.legend(hnds[:split_idx], lbls[:split_idx], loc="upper left")
legend_right = axes.legend(hnds[split_idx:], lbls[split_idx:], loc="lower right")
# явно добавляем обе
axes.add_artist(legend_left)
axes.add_artist(legend_right)

Что действительно происходит

Понимание того, что делает axes.legend, объясняет странности. Вызов ax.legend создаёт легенду, прикрепляет её к осям и удаляет любую существующую на них легенду. После второго вызова в примере выше остается прикреплённой только вторая легенда; первая была автоматически убрана. Когда затем вызывается add_artist для обоих объектов легенд, первая добавляется один раз, а вторая оказывается на осях дважды. Это дублирование и приводит к тому, что один remove() будто бы ничего не делает для второй легенды, а наивный цикл очистки по дочерним элементам может получить ValueError, пытаясь удалить один и тот же объект больше одного раза.

Вызов ax.legend создаёт легенду, добавляет её к осям и по ходу заменяет любую уже существующую. Если вы затем используете add_artist, чтобы прикреплять легенды вручную, убедитесь, что каждая легенда добавлена только один раз; иначе одна и та же легенда может присутствовать несколько раз.

Как исправить

Ключевое изменение простое: не добавляйте вторую легенду повторно. Она уже прикреплена вторым вызовом axes.legend. Если каждая легенда присутствует ровно один раз, remove() корректно срабатывает для обеих.

import matplotlib.pyplot as plt
canvas, axes = plt.subplots()
xs = [1, 2, 3, 4, 5]
vals_a = [1, 4, 9, 16, 25]
vals_b = [25, 16, 9, 4, 1]
axes.plot(xs, vals_a, label='y = x^2')
axes.plot(xs, vals_b, label='y = 25 - x^2')
# создаём временную легенду, чтобы получить объекты и подписи
axes.legend()
hnds, lbls = axes.get_legend_handles_labels()
axes.get_legend().remove()
# создаём две легенды
split_idx = len(hnds) // 2
legend_ul = axes.legend(hnds[:split_idx], lbls[:split_idx], loc="upper left")
legend_lr = axes.legend(hnds[split_idx:], lbls[split_idx:], loc="lower right")
# явно добавляем только первую; вторая уже прикреплена через legend()
axes.add_artist(legend_ul)
# позже: надёжно удаляем обе легенды
legend_ul.remove()
legend_lr.remove()

Почему это важно

Управление легендами в Matplotlib — состояние‑зависимый процесс. Каждый вызов axes.legend изменяет оси, заменяя текущую легенду. Смешивание такого поведения с ручными вызовами add_artist легко приводит к дублированию объектов легенд. Дубликаты сложно заметить визуально, но они вызывают частичное удаление или исключения при очистке — например, при обходе axes.get_children() и попытке удалить один и тот же объект легенды более одного раза.

Итоги

Помните: axes.legend и создаёт, и прикрепляет легенду, одновременно убирая предыдущую. Если вам нужно несколько легенд, добавляйте только те, которых ещё нет на осях. Когда каждая легенда присутствует ровно один раз, remove() работает как задумано. Если очистка автоматизирована, следите, чтобы вы не пытались удалить один и тот же объект легенды многократно; ведение учёта добавленных экземпляров легенд поможет избежать этой ловушки.

Статья основана на вопросе на StackOverflow от BernhardWebstudio и ответе от RuthC.