2025, Oct 18 23:17
Исправляем баг со срезом: пары в Python без совпадающих ролей
Почему генерация пар в Python ломается: из‑за среза с индекса 2 игнорируется первая роль. Разбираем причину, показываем исправление и рабочий код. Пример кода.
Случайное составление пар кажется пустяком, пока одно ограничение не переворачивает результат. В нашей задаче нужно собрать пары, которых раньше не было, и при этом участники не должны иметь одинаковую роль. Реализация почти достигает цели, но из‑за тонкой ошибки со срезом в пары просачиваются люди с одинаковыми ролями.
Как воспроизвести проблему
Ниже приведена реализация, которая читает уже встречавшиеся пары из CSV, перемешивает список участников и формирует новые пары, отсекая дубликаты, исключения и пересечения по ролям. Подвох в том, как из каждого кортежа извлекаются роли.
import random
import pandas as p
# Список всех участников и их ролей
roster = [
("Samantha Reyes", "Innovation", "Product Owner"),
("Ethan McAllister", "Data Scientist"),
("Priya Deshmukh", "Data Architect", "SMT"),
("Marcus Liu", "Stream 3"),
("Elena Petrova", "SMT", "Stream 3"),
]
# Загрузка ранее сгенерированных пар из CSV‑файла
def read_seen_duos(csv_path):
df = p.read_csv(csv_path)
duo_cache = set()
for _, row in df.iterrows():
duo = (row['name1'], row['name2'])
duo_rev = (row['name2'], row['name1'])
duo_cache.add(duo)
duo_cache.add(duo_rev)
return duo_cache
# Добавление новых пар в CSV‑файл
def append_duos_to_csv(csv_path, duos):
df = p.DataFrame(duos, columns=['name1', 'name2'])
df.to_csv(csv_path, index=False, mode='a', header=False)
# Путь к файлу с ранее сгенерированными парами
pairs_csv_path = 'prev_pairs2.csv'
# Инициализация кэша уже встречавшихся пар
prior_duos = read_seen_duos(pairs_csv_path)
# Исключения для логики формирования пар
banned_duos = []
def build_fresh_duos_with_trace(roster, prior_duos, banned_duos):
banned_set = set(banned_duos) | set((a[1], a[0]) for a in banned_duos)
max_tries = 10000
attempts = 0
while attempts < max_tries:
random.shuffle(roster)
duos = []
engaged = set()
leftovers = []
for i in range(0, len(roster) - 1, 2):
member1, member2 = roster[i], roster[i + 1]
tags1 = set(member1[2:]) if len(member1) > 2 else set()
tags2 = set(member2[2:]) if len(member2) > 2 else set()
duo_key = (member1[0], member2[0])
duo_key_rev = (member2[0], member1[0])
if duo_key in banned_set or duo_key_rev in banned_set:
print(f"Skipping pair due to exclusion: {member1[0]} - {member2[0]}")
continue
if duo_key in prior_duos or duo_key_rev in prior_duos:
print(f"Skipping pair due to seen pair: {member1[0]} - {member2[0]}")
continue
if tags1 & tags2:
print(f"Skipping pair due to role overlap: {member1[0]} ({tags1}) - {member2[0]} ({tags2})")
continue
if member1[0] in engaged or member2[0] in engaged:
print(f"Skipping pair due to duplicate usage: {member1[0]} - {member2[0]}")
continue
duos.append((member1, member2))
engaged.update([member1[0], member2[0]])
leftovers = [person for person in roster if person[0] not in engaged]
if not leftovers:
duo_keys = set((d[0][0], d[1][0]) for d in duos)
if not any((k in prior_duos or (k[1], k[0]) in prior_duos) for k in duo_keys):
prior_duos.update(duo_keys)
return duos
attempts += 1
print("Unable to generate unique pairs with the given restrictions.")
print(f"Skipped individuals: {[person[0] for person in leftovers]}")
raise ValueError("Unable to generate unique pairs with the given restrictions.")
# Генерация пар
result_duos = build_fresh_duos_with_trace(roster, prior_duos, banned_duos)
# Вывод пар
for d in result_duos:
print(f"{d[0][0]} - {d[1][0]}")
print("-----")
print(f"Total pairs: {len(result_duos)}")
# Сохранение
append_duos_to_csv(pairs_csv_path, [(d[0][0], d[1][0]) for d in result_duos])
В чём реальная проблема
Кортежи в списке участников устроены так: под индексом 0 хранится имя, а под индексами 1 и далее — роли. Поскольку в Python индексация начинается с нуля, срез с индекса 2 означает «начиная с третьего элемента». Иначе говоря, первая роль с индексом 1 незаметно исключается. Когда проверка пересечений опирается на такие множества ролей, двое людей, совпадающих только по первой роли, проходят фильтр и попадают в одну пару — именно это вы и наблюдали.
Исправление
Формируйте множества ролей, начиная с индекса 1, чтобы учесть все роли. Дополнительная проверка длины не нужна: срез с 1 у одноэлементного кортежа вернёт пустой кортеж, который превратится в пустое множество.
def build_fresh_duos_with_trace(roster, prior_duos, banned_duos):
banned_set = set(banned_duos) | set((a[1], a[0]) for a in banned_duos)
max_tries = 10000
attempts = 0
while attempts < max_tries:
random.shuffle(roster)
duos = []
engaged = set()
leftovers = []
for i in range(0, len(roster) - 1, 2):
member1, member2 = roster[i], roster[i + 1]
# Исправление: учитываем все роли, начиная с индекса 1
tags1 = set(member1[1:])
tags2 = set(member2[1:])
duo_key = (member1[0], member2[0])
duo_key_rev = (member2[0], member1[0])
if duo_key in banned_set or duo_key_rev in banned_set:
print(f"Skipping pair due to exclusion: {member1[0]} - {member2[0]}")
continue
if duo_key in prior_duos or duo_key_rev in prior_duos:
print(f"Skipping pair due to seen pair: {member1[0]} - {member2[0]}")
continue
if tags1 & tags2:
print(f"Skipping pair due to role overlap: {member1[0]} ({tags1}) - {member2[0]} ({tags2})")
continue
if member1[0] in engaged or member2[0] in engaged:
print(f"Skipping pair due to duplicate usage: {member1[0]} - {member2[0]}")
continue
duos.append((member1, member2))
engaged.update([member1[0], member2[0]])
leftovers = [person for person in roster if person[0] not in engaged]
if not leftovers:
duo_keys = set((d[0][0], d[1][0]) for d in duos)
if not any((k in prior_duos or (k[1], k[0]) in prior_duos) for k in duo_keys):
prior_duos.update(duo_keys)
return duos
attempts += 1
print("Unable to generate unique pairs with the given restrictions.")
print(f"Skipped individuals: {[person[0] for person in leftovers]}")
raise ValueError("Unable to generate unique pairs with the given restrictions.")
Почему это важно
Предположения о структуре данных быстро просачиваются в управляющую логику. В данном случае уникальность и проверка пересечений опираются на корректное понимание структуры кортежа. Ошибка на один индекс в срезе легко ускользает при ревью, но прямо подрывает инвариант подбора и засоряет историю неверными парами. Исправление среза возвращает задуманный смысл без изменения стратегии формирования пар и поведения ввода/вывода.
Итоги
Когда роли начинаются со второго элемента, всегда делайте срез с индекса 1, чтобы собрать полный набор атрибутов для фильтрации. В этом контексте set(person[1:]) достаточно и устойчив к кортежам переменной длины. С таким подходом проверка пересечений работает как задумано, и генератор выдаёт только новые пары с разными ролями.