O que todo mundo deveria saber antes de entrar na parte prática
Operação entre conjuntos não é tão simples quanto parece quando você começa a lidar com isso no dia a dia. A maioria dos tutores ensina união, interseção, diferença e diferença simétrica como se fossem conceitos isolados, mas na prática eles se sobrepõem e causam confusão especialmente quando os dados não estão limpos ou quando os conjuntos são muito grandes. Eu já vi gente gastar horas debugando um problema que era só uma confusão entre diferença e diferença simétrica, então vale a pena entender o que cada coisa faz de verdade.
Como funciona operação entre conjuntos na prática
Vamos começar pelo básico mesmo, mas com a parte que poucos explicam direito. União (|) pega todos os elementos de pelo menos um dos conjuntos. Interseção (&) pega só o que está em ambos. Diferença (-) pega o que está no primeiro mas não no segundo. Diferença simétrica (^) pega o que está em um ou em outro, mas não nos dois. Parece óbvio até você tentar aplicar isso em um projeto real e se deparar com resultados inesperados. O problema é que esses conjuntos em Python, por exemplo, são implementados como hash sets. Isso significa que os elementos precisam ser hashable. Strings, números e tuplas funcionam sem dor de cabeça. Listas, dicionários e conjuntos dentro de conjuntos dão erro porque não são imutáveis. Já me deparei com isso várias vezes e a solução é sempre a mesma: converter para tuple antes de incluir no conjunto.
Aqui vai algo que eu aprendi na marra e acho que vale a pena registrar: a ordem dos operandos importa em diferença, mas não em união e interseção. A - B é diferente de B - A. União e interseção são comutativas, o que facilita na hora de escrever código mais robusto. Diferença simétrica também é comutativa. Se você estiver fazendo operações encadeadas, fique atento a isso. Outro ponto que ninguém menciona com frequência é performance. Operações entre conjuntos em Python são O(n + m) no pior caso, onde n e m são os tamanhos dos conjuntos. Isso é muito mais rápido do que fazer verificações manualmente com loops aninhados, que seria O(n * m). Para conjuntos pequenos, a diferença é irrisória. Para conjuntos com milhares ou milhões de elementos, a diferença pode ser de segundos para minutos.
Um caso específico que eu enfrentei recentemente envolveu a interseção de dois conjuntos grandes de IDs de usuários — algo em torno de 500 mil elementos em cada um. A operação levou cerca de 0,3 segundos no heap, o que é aceitável. Mas quando tentei fazer o mesmo usando uma lista e um loop com in, levou cerca de 45 segundos. A lição é simples: use conjuntos para operações de pertenência e comparação.
Pegadinhas que causam problemas sérios
A primeira pegadinha é achar que conjuntos eliminam duplicatas automaticamente em qualquer situação. Eles eliminam duplicatas dentro do próprio conjunto, mas se você tiver dois conjuntos com elementos duplicados entre si, a união ainda vai mostrar cada elemento apenas uma vez. É a definição matemática de conjunto, mas nem todo mundo lembra disso na hora de codificar. A segunda pegadinha envolve subconjuntos e superconjuntos. O método issubset() e o operador <= verificam se todos os elementos de um conjunto estão contidos em outro. O importante é que um conjunto é considerado subconjunto de si mesmo. Se você precisa verificar subconjunto próprio, tem que usar < em vez de
=. Eu já vi gente esquecer disso e um teste passar quando não deveria.
👉 Clique no botão abaixo para saber mais sobre o assunto!
A terceira pegadinha, e talvez a mais perigosa, é a mutação. Alguns métodos modificam o conjunto original em vez de criar um novo. difference_update() modifica o conjunto, enquanto difference() retorna um novo conjunto. intersection_update() e symmetric_difference_update() seguem a mesma lógica. Se você não prestar atenção a isso, pode acabar destruindo dados que precisava manter. Um erro que cometi uma vez e demorei para perceber foi chamar difference_update() achando que criava uma cópia, quando na verdade ele modificava o conjunto original e eu tinha perdido dados que não dava mais para recuperar.
Quando operação entre conjuntos não é a melhor ferramenta
Conjuntos não são bons quando você precisa preservar a ordem dos elementos ou contar repetições. Se a ordem importa, use listas ou tuples. Se você precisa saber quantas vezes um elemento aparece, use collections.Counter. Conjuntos também são ruins para dados que não são hashable, e nesse caso a alternativa é converter tudo para algo hashable primeiro, como eu disse acima, ou usar dicionários com chaves sendo os elementos. Outro cenário onde conjuntos falham é quando os dados são extremamente grandes e a memória é um problema. Um conjunto de milhões de strings grandes consome bastante RAM porque cada string é um objeto separado com overhead. Nesse caso, considere usar estruturas mais compactas como arrays numéricos ou até mesmo processar os dados em lotes. Eu tive um projeto onde um conjunto de 2 milhões de URLs consumia cerca de 800 MB de RAM. A solução foi particionar os dados e processar em batches de 100 mil elementos, o que reduziu o uso de memória para algo em torno de 50 MB por vez.
Exemplo real de como aplicar
Pegue dois arquivos CSV com listas de emails. Um contém emails de clientes ativos, o outro de clientes inativos. Quer saber quais emails aparecem em ambos os listas. Você poderia abrir os dois arquivos, ler linha por linha, e fazer comparações manuais. Ou poderia ler os emails para dois conjuntos e aplicar interseção. A diferença em tempo de execução e legibilidade é enorme. Aqui vai um código direto, sem enfeite: leia os dois arquivos, coloque cada coluna de email em um set, aplique a interseção. Pronto. Se precisar dos que estão em um mas não no outro, use diferença. Se quiser os que estão em um ou no outro mas não nos dois, use diferença simétrica. A escolha da operação depende do que você quer descobrir, não do que parece mais interessante.
Se quiser testar isso localmente, Python já vem com suporte nativo a conjuntos. Não precisa instalar nada. O módulo set faz tudo que você precisa. Para manipulação mais avançada, em projetos com dados pesados, numpy.setops1d oferece funções semelhantes com suporte a arrays numéricos, mas para a maioria dos casos o set nativo é suficiente e mais fácil de usar.