Capítulo 15: Conjuntos de objetos

15.1 Composição

Até agora, você vio diversos exemplos de composição. Um dos primeiros exemplos foi o uso de uma invocação de método como parte de uma expressão. Outro exemplo é a estrutura aninhada dos comandos: você pode pôr um comando if dentro de um laço while, dentro de outro comando if, e assim por diante.

Tendo visto este padrão, e tendo aprendido a respeito de listas e objetos, você não deveria ficar surpreso em aprender que você pode criar listas de objetos. Você também pode criar obejtos que contêm listas (como atritos); você pode criar listas que contêm listas; você pode criar objetos que contêm objetos; e assim por diante.

Neste capítulo e no próximo, você irá ver alguns exemplos destas combinações, usando objetos Carta como exemplo.

15.2 Objetos Carta

Se você não estiver familiarizado com jogos de cartas, agora é um bom momento para conseguir um baralho, ou então esse capítulo pode não fazer muito sentido. Há 52 cartas em um baralho, cada uma das quais pertence a um dos quatro naipes e a uma das treze posições. Os naipes são Espadas, Copas, Ouros e Paus (em ordem descendente no bridge). As posições são Ás, 2, 3, 4, 5, 6, 7, 8, 9, 10, Valete, Rainha e Rei. Dependendo do jogo, a posição do Ás pode ser maior do que a do Rei ou menor do que a do 2.

Se quisermos definir um novo objeto para representar uma carta, é óbvio que os atributos devem ser posicao e naipe. Não tão óbvio são os tipos aos quais devem pertencer os atributos. Uma possibilidade é usar strings contendo palavras como “Espada” para naipes e “Rainha” para posições. Um problema com esta implementação é que não seria fácil comparar cartas para ver qual possui o maior naipe ou posição.

Uma alternativa é usar inteiros para codificar as posições e naipes. “Codificar”, neste caso, não significa o mesmo que as pessoas normalmente pensam, que é criptografar ou traduzir para um código secreto. O que um cientista da computação quer dizer com “codificar” é “definir um mapeamento entre uma seqüência de números e os itens que eu quero representar”. Por exemplo:

  • Espadas -> 3

  • Copas -> 2

  • Ouros -> 1

  • Paus -> 0

Uma característica óbvia deste mapeamento é que os naipes são mapeados para inteiros na ordem, de modo que nós podemos comparar naipes pela comparação de inteiros. O mapeamento de posições é bastante óbvio. Cada uma das posições numéricas mapeia para o inteiro correspondente e, as cartas com figura são mapeadas conforme abaixo:

  • Valete -> 11

  • Rainha -> 12

  • Rei -> 13

O motivo pelo qual nós estamos usando notação matemática para estes mapeamentos é que eles não são parte do programa Python. Eles são parte do projeto do programa, mas eles nunca aparecem explicitamente no código. A definição de classe para o tipo Carta fica parecida com esta:

class Carta:
  def __init__(self, naipe=0, posicao=0):
    self.naipe = naipe
    self.posicao = posicao

Como sempre, nós fornecemos um método de inicialização que recebe um parâmetro opcional para cada atributo.

Para criar um objeto que representa o 3 de Paus, usa-se este comando:

tresDePaus = Carta(0, 3)

O primeiro argumento, 0, representa o naipe de Paus.

15.3 Atributos de classe e o método __str__

Para imprimir objetos Carta de uma maneira que as pessoas possam facilmente ler, nós gostaríamos de mapear os códigos inteiros para palavras. Uma forma natural de fazer isso é usar listas de strings. Nós atribuímos estas listas para atributos de classe no topo da definição de classe:

class Carta:
  listaDeNaipes = ["Paus", "Ouros", "Copas", "Espadas"]
  listaDePosicoes = ["narf", "Ás", "2", "3", "4", "5", "6", "7",
                     "8", "9", "10", "Valete", "Rainha", "Rei"]

# método init omitido

def __str__(self):
  return (self.listaDePosicoes[self.posicao] + " de " +
          self.ListaDeNaipes[self.naipe])

Um atributo de classe é definido fora de qualquer método, e ele pode ser acessado por quaisquer métodos da classe.

Dentro de __str__, nós podemos usar listaDeNaipes e listaDePosicoes para mapear os valores numéricos de naipe e posicao para strings. Por exemplo, a expressão self.listaDeNaipes[self.naipe] significa “use o atributo naipe do objeto self como um índice para o atributo de classe chamado listaDeNaipes, e selecione a string apropriada”.

O motivo para o “narf” no primeiro elemento em listaDePosicoes é preencher o lugar do 0-ésimo elemento da lista, que nunca será usado. As únicas posições válidas são de 1 a 13. Este item desperdiçado não é inteiramente necessário. Nós poderíamos ter iniciado com 0, como é normal. Porém, é menos confuso codificar 2 como 2, 3 como 3, e assim por diante.

Com os métodos que nós temos até agora, nós podemos criar e imprimir cartas:

>>> carta1 = Carta(1, 11)
>>> print (carta1)
Valete de Ouros

Atributos de classe como listaDeNaipes são compartilhados por todos os objetos Carta. A vantagem disso é que nós podemos usar qualquer objeto Carta para acessar os atributos de classe:

>>> carta2 = Carta(1, 3)
>>> print (carta2)
3 de Ouros
>>> print (carta2.listaDeNaipes[1])
Ouros

A desvantagem é que se nós modificarmos um atributo de classe, isso afetará cada instância da classe. Por exemplo, se nós decidirmos que “Valete de Ouros” deveria realmente se chamar “Valete de Baleias Rodopiantes”, nós poderíamos fazer isso:

>>> carta1.listaDeNaipes = "Baleias Rodopiantes"
>>> print (carta1)
3 de Baleias Rodopiantes

O problema é que todos os Ouros se tornam Baleias Rodopiantes:

>>> print (carta2)
3 de Baleias Rodopiantes

Normalmente, não é uma boa idéia modificar atributos de classe.

15.4 Comparando cartas

Para tipos primitivos, existem operadores condicionais (<, >, ==, etc.) que comparam valores e determinam quando um é maior que, menor que ou igual a outro. Para tipos definidos pelo usuário, nós podemos sobrescrever o comportamento dos operadores pré-definidos fornecendo um método __cmp__. Por convenção, __cmp__ recebe dois parâmetros, self e other, e retorna 1 se o primeiro objeto for maior, -1 se o segundo objeto for maior, e 0 se eles forem iguais.

Alguns tipos são totalmente ordenados, o que significa que nós podemos comparar quaisquer dois elementos e dizer qual é o maior. Por exemplo, os inteiros e os números de ponto flutuante são totalmente ordenados. Alguns conjuntos são não-ordenados, o que significa que não existe maneira significativa de dizer que um elemento é maior que o outro. Por exemplo, as frutas são não-ordenadas, e é por isso que não podemos comparar maçãs e laranjas.

O conjunto de cartas de jogo é parcialmente ordenado, o que significa que às vezes você pode comparar cartas, e às vezes não. Por exemplo, você sabe que o 3 de Paus é maior do que o 2 de Paus, e que o 3 de Ouros é maior do que o 3 de Paus. Mas qual é o melhor, o 3 de Paus ou o 2 de Ouros? Um tem uma posição maior, mas o outro tem um naipe maior.

Para tornar as cartas comparáveis, você tem que decidir o que é mais importante: posição ou naipe. Para ser honesto, a escolha é arbitrária. Por questão de escolha, nós iremos dizer que naipe é mais importante, porque um baralho de cartas novo vem ordenado com todas as cartas de Paus juntas, seguidas pelas de Ouros, e assim por diante.

Com essa decisão, nós podemos escrever __cmp__:

def __cmp__(self, other):
  # verificar os naipes
  if self.naipe > other.naipe: return 1
  if self.naipe < other.naipe: return -1
  # as cartas têm o mesmo naipe... verificar as posições
  if self.posicao > other.posicao: return 1
  if self.posicao < other.posicao> return -1
  # as posições são iguais... é um empate
  return 0

Nesta ordenação, Ases são menores do que 2.

Como um exercício, modifique ``__cmp__``, de modo que os Ases sejam maiores do que os Reis.

15.5 Baralhos

Agora que nós temos objetos para representar Cartas, o próximo passo lógico é definir uma classe para representar um Baralho. É claro que um baralho é formado por cartas; portanto, cada objeto Baralho irá conter uma lista de cartas como um atributo.

A seguir, damos uma definição para a classe Baralho. O método de inicialização cria o atributo cartas e gera o conjunto padrão de 52 cartas:

classe Baralho
  def __init__(self):
    self.cartas = []
    for naipe in range(4):
      for posicao in range(1, 14):
        self.cartas.append(Carta(naipe, posicao))

A maneira mais fácil de popular o baralho é com um laço aninhado. O laço externo enumera os naipes de 0 até 3. O laço interno enumera as posições de 1 até 13. Como o laço externo repete quatro vezes e o laço interno 13 vezes, o número total de vezes que o corpo é executado é 52 (13 vezes quatro). Cada iteração cria uma nova instância de Carta com o naipe e posição atuais e a inclui na lista cartas.

O método append trabalha sobre listas mas não, obviamente, sobre tuplas.

15.6 Imprimindo o baralho

Como sempre, quando nós definimos um novo tipo de objeto, nós gostaríamos de ter um método para imprimir o conteúdo de um objeto. Para imprimir um Baralho, nós percorremos a lista e imprimimos cada Carta:

class Baralho:
  ...
  def imprimirBaralho(self):
    for carta in self.cartas:
      print (carta)

Aqui, e a partir daqui, as reticências (…) indicam que nós omitimos os outros métodos da classe.

Como uma alternativa a imprimirBaralho, nós poderíamos escrever um método __str__ para a classe Baralho. A vantagem de __str__ é que ela é mais flexível. Em vez de apenas imprimir o conteúdo de um objeto, ela gera uma representação em string que outras partes do programa podem manipular antes de imprimir ou armazenar para uso posterior.

Abaixo, uma versão de __str__ que devolve uma representação em string de um Baralho. Para adicionar um pouco de estilo, ela distribui as cartas em uma cascata, na qual cada carta é indentada um espaço a mais do que a carta anterior:

class Baralho:
  ...
  def __str__(self):
    s = ""
    for i in range(len(self.cartas)):
      s = s + " "*i + str(self.cartas[i]) + "\n"
    return s

Este exemplo demonstra diversas características. Primeiro, em vez de percorrer self.cartas e atribuir cada carta a uma variável, nós estamos usando i como uma variável de laço e um índice para a lista de cartas.

Segundo, nós estamos usando o operador de multiplicação de strings para indentar cada carta com um espaço adicional com relação à anterior. A expressão " "*i produz um número de espaços igual ao valor atual de i.

Terceiro, em vez de chamar a função print para imprimir as cartas, nós usamos a função str. Passar um objeto como um argumento para str equivale a invocar o método __str__ sobre o objeto.

Finalmente, nós estamos usando a variável s como um acumulador. Inicialmente, s é a string vazia. A cada repetição do laço, uma nova string é gerada e concatenada com o valor antigo de s para obter um novo valor. Quando o laço termina, s contém a representação em string completa do Baralho, que se parece com:

>>> baralho = Baralho()
>>> print (Baralho)
Ás de Paus
 2 de Paus
  3 de Paus
   4 de Paus
    5 de Paus
     6 de Paus
      7 de Paus
       8 de Paus
        9 de Paus
         10 de Paus
          Valete de Paus
           Rainha de Paus
            Rei de Paus
             Ás de Ouros

E assim por diante. Mesmo que o resultado apareça em 52 linhas, é uma string longa que contém newlines.

15.7 Embaralhando

Se um baralho estiver perfeitamente embaralhado, então cada carta tem a mesma probabilidade de aparecer em qualquer lugar no baralho, e qualquer localização no baralho tem a mesma probabilidade de conter qualquer carta.

Para embaralhar as cartas, nós usaremos a função randrange do módulo random. Com dois argumentos inteiros, a e b, randrange escolhe um inteiro aleatório no intervalo a <= x < b. Como o limite superior é estritamente menor que b, nós podemos usar o comprimento de uma lista como o segundo parâmetro, e nós garantimos que o índice sempre será válido. Por exemplo, esta expressão escolhe o índice de uma carta aleatória em um baralho:

random.randrange(0, len(self.cartas))

Uma maneira fácil de embaralhar as cartas é percorrer a lista e trocar cada carta por outra escolhida aleatoriamente. É possível que a carta seja trocada por ela mesma, mas isso não é problema. Na verdade, se nós excluíssemos essa possibilidade, a ordem das cartas não seria totalmente aleatória:

class Baralho:
  ...
  def embaralhar(self):
    import random
    nCartas = len(self.cartas)
    for i in range(nCartas):
      j = random.randrange(i, nCartas)
      self.cartas[i], self.cartas[j] = self.cartas[j], self.cartas[i]

Em vez de assumir que existem 52 cartas no baralho, nós obtivemos o comprimento real da lista e o guardamos na variável nCartas.

Para cada carta no baralho, nós escolhemos uma carta aleatória dentre as cartas que ainda não foram embaralhadas. Então, nós trocamos a carta atual (i) pela carta selecionada (j). Para trocar as cartas, nós usamos uma atribuição de tupla, como visto na Seção 9.2:

self.cartas[i], self.cartas[j] = self.cartas[j], self.cartas[i]

Como exercício, reescreva esta linha de código sem usar uma atribuição de seqüência.

15.8 Removendo e distribuindo cartas

Outro método que pode ser útil para a classe Baralho é removerCarta. Ele recebe uma carta como parâmetro, remove-a do baralho e retorna verdadeiro (1), se a carta estava no baralho e falso (0), caso contrário:

class Baralho:
  ...
  def removerCarta(self, carta):
    if carta in self.cartas:
      self.cartas.remove(carta)
      return 1
    else
      return 0

O operador in retorna verdadeiro se o primeiro operando estiver contido no segundo, que deve ser uma lista ou uma tupla. Se o primeiro operando for um objeto, Python usa o método __cmp__ do objeto para determinar igualdade com os itens da lista. Como o método __cmp__ da classe Carta verifica por igualdade profunda, o método removerCarta também testa por igualdade profunda.

Para distribuir as cartas, nós iremos remover e devolver a carta do topo. O método de lista pop fornece uma maneira conveniente de fazer isso:

class Baralho:
  ...
  def distribuirCarta(self):
    return self.cards.pop()

Na verdade, pop remove a última carta da lista. Portanto, nós estamos realmente distribuindo as cartas do fim para o início do baralho.

Uma última operação que nós poderíamos querer é a função booleana estahVazio, que retorna verdadeiro se o baralho não contém cartas:

class Baralho:
  ...
  def estahVazio(self):
    return (len(self.cartas) == 0)

15.9 Glossário

codificar (encode)

Representar um conjunto de valores usando outro conjunto de valores, construindo um mapeamento entre eles.

atributo de classe (class atribute)

Uma variável que é definida dentro de uma definição de classe, mas fora de qualquer método. Atributos de classe podem ser acessados a partir de qualquer método da classe e são compartilhados por todas as instâncias da classe.

acumulador (accumulator)

Uma variável usada em um laço para acumular uma série de valores, para, por exemplo, concatená-los em uma string ou somá-los a uma soma em andamento.