Python3 06 – Conjuntos (Set)

dezembro 20th, 2017 by Rudson Alves Leave a reply »
Este artigo é a parte 6 de 10 na série Python3

Conjuntos são um tipo especial de dados do Python com características bem específicas, principalmente para serem empregados como chaves em dicionários e bancos de dados.

1. Conjuntos (set)

Conjunto é uma estrutura de dados mutável, composto de uma coletânea de objetos hashable desordenados e com elementos únicos, ou seja, sem duplicatas. Conjuntos ainda suportam operações matemáticas de união, interseção, diferença e diferença simétrica.

Objetos hashable são todos aqueles que possuem o método __hash__(), o qual retorna um valor inteiro constante, ou seja, o mesmo valor por toda a vida do objeto. Objetos com o mesmo hash() são considerados idênticos quando empregados como índice. Isto ficará mais claro quando for tratado sobre a estrutura de dados dicionário. Entretanto, esta característica é indispensável para que conjuntos possam serem empregados como chaves em dicionários, como será mostrado no próximo texto1.

Um conjunto pode ser criado usando chaves ou pela função set(), passando como elementos qualquer conjunto de objetos ou mesmo um iterável. Veja os comandos a seguir:

>>> cesta = {'laranja', 'goiaba', 'banana', 'maça', 'abacate', 'banana', 'goiaba'}
>>> cesta
{'banana', 'abacate', 'goiaba', 'laranja', 'maça'}
>>> a = set('São Paulo')
>>> a
{'o', 'l', 'P', 'a', 'u', 'ã', 'S', ' '}
>>> b = set('Espírito Santo')
>>> b
{'p', 'i', 'í', 't', 'r', 's', 'o', 'E', 'a', 'n', 'S', ' '}
>>> c = {1, 2.14, 'três', 5j, (1,0,0)}
>>> c
{1, 2.14, (1, 0, 0), 5j, 'três'}

No primeiro conjunto, cesta, são adicionadas diversas frutas, mas as repetidas são excluídas do conjunto. Em seguida, são criados os dois conjuntos a e b, com as letas das strings ‘São Paulo’ e ‘Espírito Santo’. Observe que a ordem dos elementos dos conjuntos não é a ordem de entrada, ou seja, não são ordenados. Por fim, o conjunto c utiliza uma diversidade de tipos de objetos imutáveis, como inteiros, complexo, string, um float e uma tupla. Todos estes imutáveis são hashables e, portanto, retornam um código hash único para elementos diferentes.

>>> for i in c:
...   out = 'hash(' + str(i) + ')'
...   print('{0:>15} = {1}'.format(out, hash(i)))
... 
        hash(1) = 1
     hash(2.14) = 322818021289917442
hash((1, 0, 0)) = 2528505496374819208
       hash(5j) = 5000015
     hash(três) = -5298805650081748838

Embora conjuntos possam ser empregados em laços de repetição, ou seja, sejam iteráveis, eles também não possuem o método __getitem__() por não serem ordenados, como em listas e tuplas, e por isto não podem ser indexados. Uma tentativa de indexar um elemento de um conjunto gera um erro TypeError:

>>> c[1]
Traceback (most recent call last):
  File "", line 1, in 
TypeError: 'set' object does not support indexing

1.1. Conjuntos são Mutáveis, frozenset não

Conjuntos são objetos mutáveis e podem ter elementos adicionados e removidos através dos métodos add(), pop() e remove().

>>> c.add(5)
>>> c.pop()
1
>>> c.pop()
2.14
>>> c.remove('três')
>>> c
{5, (1, 0, 0), 5j}

Explico melhor estes métodos mais adiante. Existe um tipo de conjunto que é imutável e também pode ser usado como elemento em um conjunto.

>>> f = frozenset({2.8, 9j, 'elf'})
>>> f
frozenset({'elf', 2.8, 9j})
>>> hash(f)
-8561833039905925542
>>> c.add(f)
>>> c
{5, (1, 0, 0), 5j, frozenset({'elf', 2.8, 9j})}

FrozenSet são conjuntos imutáveis.

1.2. Operadores em Conjuntos

Conjuntos suportam um grupo específico de operadores específicos além dos operadores convencionais de comparação e operação, mas com comportamentos um pouco distintos.

Para os exemplos a seguir, considere os conteúdos dos conjuntos a e b como sendo o conjunto de letras nas strings ‘laranja’ e ‘marajo’, respectivamente:

>>> a = set('laranja')
>>> b = set('marajo')
>>> a
{'l', 'j', 'r', 'n', 'a'}
>>> b
{'j', 'r', 'o', 'm', 'a'}

Segue a lista de operadores:

  • __and__ – mesmo que o operador &. Retorna a interseção dos conjuntos, ou seja, os elementos comuns entre os dois conjuntos,
    >>> a.__and__(b)
    {'a', 'j', 'r'}
    >>> a & b
    {'a', 'j', 'r'}
    
  • __or__ – mesmo que o operador |. Retorna a união dos elementos contidos nos dois conjuntos,
    >>> a.__or__(b)
    {'l', 'j', 'r', 'o', 'n', 'm', 'a'}
    >>> a | b
    {'l', 'j', 'r', 'o', 'n', 'm', 'a'}
    
  • __xor__ – o mesmo que o operador ^. Retorna a diferença simétrica entre os dois conjuntos, ou seja, os elementos do conjunto a que não estão contidos no conjunto b e os elementos de b que não estão contidos em a,
    >>> a ^ b
    {'l', 'o', 'n', 'm'}
    >>> a.__xor__(b)
    {'l', 'o', 'n', 'm'}
    
  • __sub__ – operador subtração, -. A operação a - b retorna os elementos de a, removidos os elementos semelhantes em b, ou seja apenas os elementos ‘n’ e ‘l’. O contrário, b - a retornará os elementos ‘o’ e ‘m’. Veja no exemplo abaixo:
    >>> a - b
    {'n', 'l'}
    >>> b - a
    {'o', 'm'}
    

    Observe que a operação (a - b) | (b - a) é a definição de a ^ b:

    >>> (a - b) | (b - a) == (a ^ b)
    True
    
  • __iand__, __isub__, __ixor__, __ior__ – são implementações para os operadores &=, -=, ^= e |=, respectivamente.
  • Tab 03: Operadores Lógicos Compostos

    Operador Descrição Uso Equivalência
    &= interseção igual a &= b a = a & b
    -= diferença igual a -= b a = a – b
    ^= dif. sim. igual a ^= b a = a ^ b
    |= união igual a |= b a = a | b
  • __rand__, __rsub__, __rxor__, __ror__ – este ‘r’ corresponde a ‘right’. Essencialmente, implica em trocar os conjuntos de posição nas operações. Por exemplo a.__rxor__(b) = b ^ a, ao invés de a ^ b. Com exceção ao operador de subtração, todos os demais retornaram o mesmo resultado.
    >>> a.__rsub__(b)
    {'o', 'm'}
    >>> a.__rand__(b)
    {'a', 'j', 'r'}
    >>> a & b
    {'a', 'j', 'r'}
    >>> a & b == b & a
    True
    >>> a - b == b - a
    False
    
  • __eq__, __ne__, __ge__, __gt__, __le__, __lt__ – Operadores condicionais estão definidos para conjuntos, mas operam de forma diferenciada ao visto em lista e tuplas. Como conjuntos são desordenados, os operadores de igualdade e diferença comparam os conjuntos elemento a elemento, independente da ordem que os seus elementos possam ser apresentados.
    >>> a = set('abacaxi')
    >>> b = set('xibac')
    >>> a == b
    True
    >>> a.add('f')
    >>> a == b
    False
    >>> a != b
    True
    

    Os operadores de ‘>‘ e ‘<‘ correspondem aos operadores ‘contém’ e ‘está contido’ em conjuntos.

    >>> a
    {'b', 'c', 'a', 'i', 'f', 'x'}
    >>> b
    {'b', 'c', 'a', 'i', 'x'}
    >>> a > b
    True
    >>> b < a
    True
    >>> a < b
    False
    >>> b > a
    False
    

    Como b esta contido em a, as duas primeiras comparações (a > b e b < a) retornam verdadeiras, já as duas seguintes retornam falso.

Como feito até o momento, métodos específicos a construção e atributos da classe set (conjuntos) não foram abordados, deixando este debate para adiante.

1.3. Métodos de Conjuntos

Segue os métodos dos conjuntos.

  • add(Valor) – adiciona o elemento Valor ao conjunto.
    >>> a.add(5)
    >>> a
    {5, 'l', 'j', 'r', 'a', 'n'}
    
  • clear() – apaga todo o conteúdo do conjunto.
  • copy() – cria uma cópia do conteúdo do conjunto. Como conjuntos são mutáveis, um método para fazer a sua cópia é essencial para evitar problemas como o ilustrado com as listas, no exemplo de matriz no texto anterior. Veja o exemplo:
    >>> c = a.copy()
    >>> d = c
    >>> c
    {'a', 5, 'j', 'l', 'n', 'r'}
    >>> a
    {5, 'l', 'j', 'r', 'a', 'n'}
    >>> d
    {'a', 5, 'j', 'l', 'n', 'r'}
    >>> c.clear()
    >>> d
    set()
    >>> a
    {5, 'l', 'j', 'r', 'a', 'n'}
    

    Na primeira linha, c recebe uma cópia de a. Já d apenas aponta para o objeto de c. Ao apagar c, d também é apagado, mas o mesmo não ocorre a a, pois são objetos distintos na memória.

  • difference(set), intersection(set), union(set) e symmetric_difference(set) – estes métodos retornam o mesmo que os operadores -, &, | e ^, respectivamente.
    >>> a.difference(b)
    {'l', 'n', 5}
    >>> a.intersection(b)
    {'j', 'a', 'r'}
    >>> a.union(b)
    {5, 'l', 'j', 'r', 'm', 'o', 'a', 'n'}
    >>> a.symmetric_difference(b)
    {'m', 'o', 5, 'l', 'n'}
    
  • difference_update(set), intersection_update(set), update(set) e symmetric_difference_update(set) – estes métodos retornam o mesmo que os operadores compostos -=, &=, |= e ^=, respectivamente. Observe que o método union_update(set) não existe, é apenas update(set).
>>> c = a.copy()
>>> c.difference_update(b)
>>> c == a - b
True
>>> c = a.copy()          
>>> c.intersection_update(b)
>>> c == a & b
True
>>> c = a.copy()
>>> c.update(b)
>>> c == a | b
True
>>> c = a.copy()
>>> c.symmetric_difference_update(b)
>>> c == a ^ b
True
  • pop() – remove um elemento arbitrário do conjunto.
    >>> c
    {'m', 'o', 5, 'l', 'n'}
    >>> c.pop()
    'm'
    >>> c.pop()
    'o'
    
  • remove(Valor) – remove o elemento Valor do conjunto. Se o elemento não existir, retorna o erro KeyError.
    >>> c
    {5, 'l', 'n'}
    >>> c.remove(5)
    >>> c.remove('n')
    >>> c.remove(3)
    Traceback (most recent call last):
      File "", line 1, in 
    KeyError: 3
    
  • discard(Valor) – remove o elemento Valor do conjunto. A única diferença com o método remove(Valor) é que o discard(Valor) não retorna erro caso o elemento não exista.
    >>> c = a | b
    >>> c
    {5, 'l', 'j', 'r', 'm', 'o', 'a', 'n'}
    >>> c.discard('a')
    >>> c
    {5, 'l', 'j', 'r', 'm', 'o', 'n'}
    >>> c.discard(3)
    >>> 
    
  • isdisjoint(set) – retorna verdadeiro se os dois conjuntos possuem interseção nula.
    >>> c
    {5, 'l', 'j', 'r', 'm', 'o', 'n'}
    >>> c -= b
    >>> c
    {5, 'l', 'n'}
    >>> b
    {'j', 'r', 'm', 'o', 'a'}
    >>> c.isdisjoint(b)
    True
    >>> a.isdisjoint(b)
    False
    
  • issubset(set) – retorna verdadeiro se o conjunto for um subconjunto do outro.
    >>> c
    {5, 'l', 'n'}
    >>> a
    {5, 'l', 'j', 'r', 'a', 'n'}
    >>> c.issubset(a)
    True
    >>> a.issubset(c)
    False
    
  • issuperset(set) – retorna verdadeiro se o conjunto argumento (set) for um subconjunto do conjunto. É o contrário do issubset(set).
    >>> c.issuperset(a)
    False
    >>> a.issuperset(c)
    True
    
  • 1.3. Set Comprehensions

    Como em listas, conjuntos também podem ser gerados usando um set comprehensions, o qual funciona com uma expressão em um laço de repetição fechado entre chaves. As sintaxes suportadas são as mesmas de listas, apenas substituindo os colchetes por chaves:

    { <expressão> for <item> in <iterável> }
    { <expressão> for <item> in <iterável> if <condição> }

    Seguem alguns exemplos:

    >>> a = {x for x in 'abracadabra' if x not in 'abc'}
    >>> b = {x for x in 'abracadabra' if x > 'c'}
    >>> a
    {'r', 'd'}
    >>> a == b
    True
    

    Duas formas diferentes de construir o mesmo conjunto das letras de ‘abracadabra’ maiores que c.

    2. Considerações

    A grande aplicação para conjuntos é o seu emprego como chaves em outras estruturas de dados como dicionários, apresentado próximo texto, ou chaves de bancos de dados, além de aplicações matemáticas como conjuntos. Embora não o tenha utilizado muito, é aconselhável ter consciência de suas possibilidades em momento de programação.

    O próximo texto será sobre dicionários, uma estrutura de dados composta, mas que pode empregar qualquer tipo de imutável como índice, além de inteiros. A lista de índices de um dicionário, como veremos, é um conjunto.

    1. Existem alguns detalhes sobre objetos hashables que não consegui compreender ainda. Se tiver uma oportunidade mais adiante, pretendo olhar com mais atenção estes códigos hash.
    Advertisement

    Deixe uma resposta