Capitulo 16: Herança

16.1 Herança

Uma das características mais marcantes das linguagens orientadas a objetos é a herança. Herança é a habilidade de definir uma nova classe que é uma versão modificada de uma classe existente.

A principal vantagem dessa característica é que você pode adicionar novos métodos a uma classe sem ter que modificar a classe existente. Chama-se “herança” porque a nova classe herda todos os métodos da classe existente. Ampliando a metáfora, podemos dizer que a classe existente é às vezes chamada de classe mãe (parent). A nova classe pode ser chamada de classe filha ou, simplesmente, “subclasse”.

A herança é uma característica poderosa. Alguns programas que seriam complicados sem herança podem ser escritos de forma simples e concisa graças a ela. E a herança também pode facilitar o reuso do código, uma vez que você pode adaptar o comportamento de classes existentes sem ter que modificá-las. Em alguns casos, a estrutura da herança reflete a natureza real do problema, tornando o programa mais fácil de entender.

Por outro lado, a herança pode tornar um programa seja difícil de ler. Quando um método é invocado, às vezes não está claro onde procurar sua definição. A parte relevante do código pode ser espalhada em vários módulos. E, também, muitas das coisas que podem ser feitas utilizando herança também podem ser feitas de forma igualmente elegante (ou até mais) sem ela. Se a estrutura natural do problema não se presta a utilizar herança, esse estilo de programação pode trazer mais problemas que vantagens.

Nesse capítulo, vamos demonstrar o uso de herança como parte de um programa que joga uma variante de Mico. Um dos nossos objetivos é escrever um código que possa ser reutilizado para implementar outros jogos de cartas.

16.2 Uma mão de cartas

Para quase todos os jogos de baralho, é preciso representar uma mão de cartas. Uma mão de cartas é similar a um maço de baralho. Porque ambos são formados por uma série de cartas e ambos requerem operações, como, adicionar e remover cartas. Fora isso, a habilidade de embaralhar a mão e o baralho também são úteis.

Mas, ao mesmo tempo, a mão é também diferente do baralho. Dependendo do jogo que está sendo jogado, precisamos realizar algumas operações nas mãos de cartas que não fazem sentido para o baralho inteiro. Por exemplo, no pôquer, podemos classificar uma mão (trinca, flush, etc.) ou compará-la com outra mão. No jogo de bridge, podemos querer computar a quantidade de pontos que há numa mão, a fim de fazer um lance.

Essa situação sugere o uso de herança. Se Mao é uma subclasse de Baralho, terá todos os métodos de Baralho, e novos métodos podem ser adicionados.

Na definição de classe, o nome da classe pai aparece entre parênteses:

class Mao(Baralho):
  pass

Esse comando indica que a nova classe Mao herda da classe existente Baralho.

O construtor de Mao inicializa os atributos da mão, que são nome e cartas. A string nome identifica essa mão, provavelmente pelo nome do jogador que está segurando as cartas. O nome é um parâmetro opcional com a string vazia como valor default. cartas é a lista de cartas da mão, inicializada com uma lista vazia

class Mao(Baralho):
  def __init__(self, nome=""):
    self.cartas = []
    self.nome = nome

Em praticamente todos os jogos de cartas, é necessario adicionar e remover cartas do baralho. Remover cartas já está resolvido, uma vez que Mao herda removerCarta de Baralho. Mas precisamos escrever adicionarCarta:

class Mao(Baralho):
  #...
  def adicionarCarta(self,carta):
    self.cartas.append(carta)

De novo, a elipse indica que omitimos outros métodos. O método de listas append adiciona a nova carta no final da lista de cartas.

16.3 Dando as cartas

Agora que temos uma classe Mao, queremos distribuir cartas de Baralho para mãos de cartas. Não é imediatamente óbvio se esse método deve ir na classe Mao ou na classe Baralho, mas como ele opera num único baralho e (possivelmente) em várias mãos de cartas, é mais natural colocá-lo em Baralho.

O método distribuir deve ser bem geral, já que diferentes jogos terão diferentes requerimentos. Podemos querer distribuir o baralho inteiro de uma vez só ou adicionar uma carta a cada mão.

distribuir recebe dois argumentos, uma lista (ou tupla) de mãos e o numero total de cartas a serem dadas. Se não houver cartas suficientes no baralho, o método dá todas as cartas e pára:

class Baralho:
  #...
  def distribuir(self, maos, nCartas=999):
    nMaos = len(maos)
    for i in range(nCartas):
      if self.estahVazia(): break    # interromper se acabaram as cartas
      carta = self.pegarCarta()      # pegar a carta do topo
      mao = maos[i % nMaos]       # quem deve receber agora?
      mao.adicionarCarta(carta)   # adicionar a carta à mao

O segundo parâmetro, nCartas, é opcional; o default é um número grande, o que na prática significa que todas as cartas do baralho serão dadas se este parâmetro for omitido.

A variável do laço i vai de 0 a nCartas-1. A cada volta do laço, uma carta é removida do baralho, usando o método de lista pop, que remove e retorna o último item na lista.

O operador módulo (%) permite dar cartas em ao redor da mesa (uma carta de cada vez para cada mão). Quando i é igual ao numero de mãos na lista, a expressão i % nMaos volta para o começo da lista (índice 0).

16.4 Exibindo a mao

Para exibir o conteúdo de uma mão, podemos tirar vantagem dos métodos exibirBaralho e __str__ herdados de Baralho. Por exemplo:

>>> baralho = Baralho()
>>> baralho.embaralhar()
>>> mao = Mao("fabio")
>>> baralho.distribuir([mao], 5)
>>> print (mao)
Mão fabio contém
2 de espadas
 3 de espadas
  4 de espadas
   Ás de copas
    9 de paus

Nao é lá uma grande mão, mas tem potencial para um straight flush.

Embora seja conveniente herdar os métodos existentes, há outras informacoes num objeto Mao que podemos querer incluir quando ao exibí-lo. Para fazer isso, podemos fornecer um método __str__ para a classe Mao que sobrescreva o da classe Baralho:

class Mao(Baralho)
  #...
  def __str__(self):
    s = "Mao " + self.nome
    if self.estahVazia():
      return s + " está vazia\n"
    else:
      return s + " contém\n" + Baralho.__str__(self)

Inicialmente, s é uma string que identifica a mão. Se a mão estiver vazia, o programa acrescenta as palavras está vazia e retorna o resultado.

Se não, o programa acrescenta a palavra contém e a representação de string do Baralho, computada pela invocação do método __str__ na classe Baralho em self.

Pode parecer estranho enviar self, que se refere à Mao corrente, para um método Baralho, mas isso só até voce se lembrar que um Mao é um tipo de Baralho. Objetos Mao podem fazer tudo que os objetos Baralho fazem, entao, é permitido passar uma instância de Mao para um método Baralho.

Em geral, sempre é permitido usar uma instância de uma subclasse no lugar de uma instância de uma classe mãe.

16.5 A classe JogoDeCartas

A classe JogoDeCartas toma conta de algumas tarefas básicas comuns a todos os jogos, como, criar o baralho e embaralhá-lo:

class JogoDeCartas:
  def __init__(self):
    self.baralho = Baralho()
    self.baralho.embaralhar()

Este é o primeiro dos casos que vimos até agora em que o método de inicialização realiza uma computação significativa, para além de inicializar atributos.

Para implementar jogos específicos, podemos herdar de JogoDeCartas e adicionar caracteristicas para o novo jogo. Como exemplo, vamos escrever uma simulação de Mico.

O objetivo do jogo é livrar-se das cartas que estiverem na mão. Para fazer isso, é preciso combinar cartas formando pares ou casais que tenham a mesma cor e o mesmo número ou figura. Por exemplo, o 4 de paus casa com o 4 de espadas porque os dois naipes são pretos. O Valete de copas combina com o Valete de ouros porque ambos são vermelhos.

Antes de mais nada, a Dama de paus é removida do baralho, para que a Dama de espadas fique sem par. A Dama de espadas então faz o papel do mico. As 51 cartas que sobram são distribuidas aos jogadores em ao redor da mesa (uma carta de cada vez para cada mão). Depois que as cartas foram dadas, os jogadores devem fazer todos os casais possíveis que tiverem na mão, e em seguida descartá-los na mesa.

Quando ninguém mais tiver nenhum par para descartar, o jogo começa. Na sua vez de jogar, o jogador pega uma carta (sem olhar) do vizinho mais proximo à esquerda, que ainda tiver cartas. Se a carta escolhida casar com uma carta que ele tem na mão, ele descarta esse par. Quando todos os casais possíveis tiverem sido feitos, o jogador que tiver sobrado com a Dama de espadas na mão perde o jogo.

Em nossa simulação computacional do jogo, o computador joga todas as mãos. Infelizmente, algumas nuances do jogo presencial se perdem. Num jogo presencial, o jogador que está com o mico na mão pode usar uns truques para induzir o vizinho a pegar a carta, por exemplo, segurando-a mais alto que as outras, ou mais baixo, ou se esforçando para que ela não fique em destaque. Já o computador simplesmente pega a carta do vizinho aleatoriamente…

16.6 Classe MaoDeMico

Uma mão para jogar Mico requer algumas habilidades para alem das habilidades gerais de uma Mao. Vamos definir uma nova classe, MaoDeMico, que herda de Mao e provê um método adicional chamado descartarCasais:

class MaoDeMico(Mao):
  def descartarCasais(self):
    conta = 0
    cartasIniciais = self.cartas[:]
    for carta in cartasIniciais:
      casal = Carta(3 - carta.naipe, carta.valor)
      if casal in self.cartas:
        self.cartas.remove(carta)
        self.cartas.remove(casal)
        print ("Mao %s: %s casais %s" % (self.nome,carta,casal))
        conta = conta + 1
    return conta

Começamos fazendo uma cópia da lista de cartas, para poder percorrer a cópia enquanto removemos cartas do original. Uma vez que self.cartas é modificada no laço, não queremos usá-la para controlar o percurso. Python pode ficar bem confuso se estiver percorrendo uma lista que está mudando!

Para cada carta na mão, verificamos qual é a carta que faz par com ela e vamos procurá-la. O par da carta tem o mesmo valor (número ou figura) e naipe da mesma cor. A expressão 3 - carta.naipe transforma um paus (naipe 0) numa espadas (naipe 3) e um ouros (naipe 1) numa copas (naipe 2). Você deve analisar a fórmula até se convencer de que as operações opostas também funcionam. Se o par da carta tambem estiver na mão, ambas as cartas são removidas.

O exemplo a seguir demonstra como usar descartarCasais:

>>> jogo = JogoDeCartas()
>>> mao = MaoDeMico("fabio")
>>> jogo.baralho.distribuir([mao], 13)
>>> print (mao)
mão fabio contém
Ás de espadas
 2 de ouros
  7 de espadas
   8 de paus
    6 de copas
     8 de espadas
      7 de paus
       Rainha de paus
        7 de ouros
         5 de paus
          Valete de ouros
           10 de ouros
            10 de copas

>>> mao.descartarCasais()
Mão fabio: 7 de espadas faz par com 7 de paus
Mão fabio: 8 de espadas faz par com 8 de paus
Mão fabio: 10 de ouros faz par com 10 de copas
>>> print (mao)
Mão fabio contém
Ás de espadas
 2 de ouros
  6 de copas
   Rainha de paus
    7 de ouros
     5 de paus
      Valete de ouros

Observe que não existe um método __init__ para a classe MaoDeMico. Ele é herdado de Mao.

16.7 Classe Mico

Agora podemos focar nossa atenção no jogo em si. Mico é uma subclasse de JogoDeCartas com um novo método chamado jogar que recebe uma lista de jogadores como argumento.

Já que __init__ é herdado de JogoDeCartas, um novo objeto Mico contém um novo baralho embaralhado:

class Mico(JogoDeCartas):
  def jogar(self, nomes):
    # remover a Dama de paus
    self.baralho.removerCarta(Carta(0,12))

    # fazer uma mão para cada jogador
    self.maos = []
    for nome in nomes :
      self.maos.append(MaoDeMico(nome))

    # distribuir as cartas
    self.baralho.distribuir(self.maos)
    print ("---------- As cartas foram dadas")
    self.exibirMaos()

    # remover casais iniciais
    casais = self.removerTodosOsCasais()
    print ("---------- Os pares foram descartados, o jogo começa")
    self.exibirMaos()

    # jogar até que 25 casais se formem
    vez = 0
    numMaos = len(self.maos)
    while casais < 25:
      casais = casais + self.jogarVez(vez)
      vez = (vez + 1) % numMaos

    print ("---------- Fim do jogo")
    self.exibirMaos()

Algumas etapas do jogo foram separadas em métodos. removerTodosOsCasais percorre a lista de mãos e invoca descartarCasais em cada uma:

class Mico(JogoDeCartas):
  #...
  def removerTodosOsCasais(self):
    conta = 0
    for mao in self.maos:
      conta = conta + mao.descartarCasais()
    return conta

   Como exercício, escreva ``exibirMaos`` que percorre ``self.maos`` e exibe cada mão.

conta é uma acumulador que soma o número de pares em cada mão e retorna o total.

Quando o número total de pares alcança 25, 50 cartas foram removidas das mãos, o que significa que sobrou só uma carta e o jogo chegou ao fim.

A variável vez mantém controle sobre de quem é a vez de jogar. Começa em 0 e incrementa de um em um; quando atinge numMaos, o operador módulo faz ela retornar para 0.

O método jogarVez recebe um argumento que indica de quem é a vez de jogar. O valor de retorno é o número de pares feitos durante essa rodada:

class Mico(JogoDeCartas):
  #...
  def jogarVez(self, i):
    if self.maos[i].estahVazia():
      return 0
    vizinho = self.buscarVizinho(i)
    novaCarta = self.maos[vizinho].pegarCarta()
    self.maos[i].adicionarCarta(novaCarta)
    print ("Mao", self.maos[i].nome, "pegou", novaCarta)
    conta = self.maos[i].descartarCasais()
    self.maos[i].embaralhar()
    return conta

Se a mão de um jogador estiver vazia, ele está fora do jogo, então, ele não faz nada e retorna 0.

Do contrário, uma jogada consiste em achar o primeiro jogador à esquerda que tenha cartas, pegar uma carta dele, e tentar fazer pares. Antes de retornar, as cartas na mão são embaralhadas, para que a escolha do próximo jogador seja aleatória.

O método buscarVizinho começa com o jogador imediatamente à esquerda e continua ao redor da mesa até encontrar um jogador que ainda tenha cartas:

class Mico(JogoDeCartas):
  #...
  def buscarVizinho(self, i):
    numMaos = len(self.maos)
    for next in range(1,numMaos):
      vizinho = (i + next) % numMaos
      if not self.maos[vizinho].estahVazia():
        return vizinho

Se buscarVizinho alguma vez circulasse pela mesa sem encontrar cartas, retornaria None e causaria um erro em outra parte do programa. Felizmente, podemos provar que isso nunca vai acontecer (desde que o fim do jogo seja detectado corretamente).

Não mencionamos o método exibirBaralhos. Esse você mesmo pode escrever.

A saída a seguir é produto de uma forma reduzida do jogo, onde apenas as 15 cartas mais altas do baralho (do 10 para cima) foram dadas, para três jogadores. Com esse baralho reduzido, a jogada pára depois que 7 combinações foram feitas, ao invés de 25:

>>> import cartas
>>> jogo = cartas.Mico()
>>> jogo.jogar(["Alice","Jair","Clara"])
---------- As cartas foram dadas
Mão Alice contém
Rei de copas
 Valete de paus
  Rainha de espadas
   Rei de espadas
    10 de ouros

Mão Jair contém
Rainha de copas
 Valete de espadas
  Valete de copas
   Rei de ouros
    Rainha de ouros

Mão Clara contém
Valete of ouros
 Rei de paus
  10 de espadas
   10 de copas
    10 de paus

Mão Jair: Dama de copas faz par com Dama de ouros
Mão Clara: 10 de espadas faz par com 10 de paus
---------- Os pares foram descartados, o jogo começa
Mão Alice contém
Rei de copas
 Valete de paus
  Rainha de espadas
   Rei de espadas
    10 de ouros

Mão Jair contém
Valete de espadas
 Valete de copas
  Rei de ouros

Mão Clara contém
Valete de ouros
 Rei de paus
  10 de copas

Mão Alice pegou o Rei de ouros
Mão Alice: Rei de copas faz par com Rei de ouros
Mão Jair pegou 10 de copas
Mão Clara pegou Valete de paus
Mão Alice pegou Valete de copas
Mão Jair pegou Valete de ouros
Mão Clara pegou Dama de espadas
Mão Alice pegou Valete de ouros
Mão Alice: Valete de copas faz par com Valete de ouros
Mão Jair pegou Rei de paus
Mão Clara pegou Rei de espadas
Mão Alice pegou 10 de copas
Mão Alice: 10 de ouros faz par com 10 de copas
Mão Jair pegou Dama de espadas
Mão Clara pegou Valete de espadas
Mão Clara: Valete de paus faz par com Valete de espadas
Mão Jair pegou Rei de espadas
Mão Jeff: Rei de paus faz par com Rei de espadas
---------- Fim do jogo
Mão Alice está vazia

Mão Jair contém
Rainha de espadas

Mão Clara está vazia

Então, o Jair perdeu.

16.8 Glossário

herança (inheritance)

Habilidade de definir uma nova classe que é a versão modificada de uma classe definida anteriormente.

classe mãe (parent class)

A classe de quem a classe filha herda.

classe filho (child class)

Um nova classe criada herdando de uma classe existente; também chamada de “subclasse”.