Source

aprendacompy / edicao_1.1 / capitulo_14.rst

Full commit

Capítulo 14: Classes e métodos

14.1 Características da orientação a objetos

Python é uma linguagem de programação orientada a objetos, o que significa que ela tem características que suportam a programação orientada a objetos.

Não é fácil definir programação orientada a objetos, mas já vimos algumas de suas características:

  • Programas são construídos sobre definições de objetos e funções, e a maioria das operações é expressa sobre objetos.
  • Cada definição de objeto corresponde a algum objeto ou conceito do mundo real, e as funções que operam com aqueles objetos correspondem à maneira como os objetos do mundo real interagem.

Por exemplo, a classe Horario, definida no capítulo 13, corresponde à maneira como as pessoas denotam as horas do dia, e as funções que definimos correspondem às coisas que a pessoas fazem com uma determinada hora. Do mesmo modo, as classes Ponto e Retangulo correspondem aos conceitos matemáticos de um ponto e de um retângulo, respectivamente.

Até aqui, não tiramos vantagem dos recursos do Python que suportam a programação orientada a objetos. De maneira estrita, estas características não são necessárias. Na maioria das vezes, elas fornecem uma sintaxe alternativa para as coisas que já fizemos, mas em muitos casos a alternativa é mais concisa e faz mais sentido na estrutura do programa.

Por exemplo, no programa Horario, não existe uma conexão óbvia entre a definição da classe e a definição da função que segue. Podemos observar que toda função toma pelo menos um objeto Horaro na lista de parâmetros.

Esta observação é a motivação por trás dos métodos. Já temos visto alguns métodos, tais como keys e values, que foram invocados em dicionários. Cada método é associado a classe e é invocado em instâncias daquela classe.

Métodos são simplesmente como funções, com duas diferenças:

  • Métodos são definidos dentro da definição de uma classe, o que torna explícita a relação entre a classe e o método.
  • A sintaxe para a chamada de um método é diferente da sintaxe para a chamada de uma função.

Nas próximas seções, vamos pegar as funções dos dois capítulos anteriores e transformá-las em métodos. Esta transformação é puramente mecânica: você pode consegui-la simplesmente seguindo uma sequência de passos. Se você se sentir confortável convertendo de uma forma para a outra, você estará apto para escolher a melhor forma para qualquer coisa que você estiver fazendo.

14.2 Método imprimirHorario

No capítulo 13, definimos uma classe chamada Horario e você escreveu uma função chamada imprimirHorario (no primeiro exercício), que deve ter ficado mais ou menos assim:

class Horario:
  pass

def imprimirHorario(horario)
  print str(horario.horas) + ':' + \
    str(horario.minutos) + ':' + \
    str(horario.segundos)

Para chamar esta função, passamos um objeto Horario como um parâmetro:

>>> horaCorrente = Horario()
>>> horaCorrente.horas = 9
>>> horaCorrente.minutos = 14
>>> horaCorrente.segundos = 30
>>> imprimirHorario(horaCorrente)

Para fazer de imprimirHorario um método, tudo o que temos a fazer é mover a definição da função para dentro da definição da classe. Note a mudança na endentação:

class Horario:
  def imprimirHorario(horario):
    print str(horario.horas) + ':' + \
      str(horario.minutos) + ':' + \
      str(horario.segundos)

Agora podemos chamar imprimirHorario usando a natação de ponto:

>>> horaCorrente.imprimirHorario()

Como já vimos, o objeto no qual o método é invocado aparece antes do ponto e o nome do método aparece depois do ponto.

O objeto no qual o método é invocado é atribuído ao primeiro parâmetro, então, neste caso, horaCorrente é atribuído ao parâmetro horario.

Por convenção, o primeiro parâmetro de um método é chamado self. A razão para isto é um pouco confusa, mas é baseada numa metáfora útil.

A sintaxe para uma chamada de função, imprimirHorario(horaCorrente), sugere que a função é um agente ativo. Diz algo como, 'Ei, exibeHora! Aqui está um objeto para você exibir.'.

Na programação orientada a objetos, os objetos são agentes ativos. Uma chamado do tipo horaCorrente.imprimirHorario() diz 'Ei, horaCorrente! Por favor exiba-se a si mesmo!'.

Esta mudança de perspectiva pode ser mais polida e bela, mas não demonstra a utilidade que existe no uso de métodos ao invés de funções. Nos exemplos que vimos até aqui, poder ser que não haja tanta utilidade em usar métodos. Mas às vezes, deslocar a responsabilidade das funções para cima dos objetos torna possível escrever funções mais versáteis, e torna mais fácil manter e reutilizar o código.

14.3 Um outro exemplo

Vamos converter incrementar (da Seção 13.3) em um método. Para poupar espaço, deixaremos de fora métodos definidos previamente, mas você deve mantê-los em sua versão:

class Horario:
  # Métodos definidos previamente devem estar aqui

  def incrementar(self, segundos):
    self.segundos = segundos + self.segundos

    while self.segundos >= 60:
      self.segundos = self.segundos - 60
      self.minutos = self.minutos + 1

    while self.minutos >= 60:
      self.minutos = self.minutos - 60
      self.horas = self.horas + 1

A transformação é puramente manual e mecânica: movemos a definição do método para dentro da definição da classe e mudamos o nome do primeiro parâmetro.

Agora podemos chamar incrementar como um método:

horaCorrente.incrementar(500)

De novo, o objeto no qual o método é chamado é atribuído ao primeiro parâmetro, self. O segundo parâmetro, segundos recebe o valor 500.

Como exercício, converta 'converterParaSegundos' (da Seção 13.5) para um método na classe Horario.

14.4 Um exemplo mais complicado

A função vemDepois (feita por você no segundo exercício do Capítulo 13) é um pouco mais complicada, já que ela trabalha com dois objetos do tipo Horario, e não apenas um. Quando isto ocorre, nós chamaos apenas o primeiro parâmetro de self, o outro continua o mesmo:

class Horario:
  # Métodos definidos previamente devem ficar aqui

  def vemDepois(self, h2):
    if self.horas > h2.horas:
      return True

    if self.horas < h2.horas:
      return False

    if self.minutos > h2.minutos:
      return True

    if self.minutos < h2.minutos:
      return False

    if self.segundos > h2.segundos:
      return True

    return False

Nós invocamos este método através da notação do ponto em um objeto, passando o outro objeto Horario por parâmetro:

if horaDeAcabar.vemDepois(horaAtual):
  print "Ainda não acabou"

Você pode ler isso facilmente em linguagem humana: "Se a hora de acabar vem depois da hora atual, então...".

14.5 Argumentos opcionais

Vimos algumas funções do Python que recebiam um número variável de argumentos. Por exemplo, string.find pode receber dois, três ou quatro argumentos.

Também podemos escrever nossas funções com argumentos opcionais. Por exemplo, podemos melhorar nossa função find para agir de forma semelhante à função string.find.

Esta é a versão original, que criamos na seção 7.7:

def find(palavra, ch):
  indice = 0
  while indice < len(str):
    if palavra[indice] == ch:
      return indice
    indice = indice + 1
  return -1

Esta é a nova versão, melhorada:

def find(palavra, ch, inicio = 0):
  indice = inicio
  while indice < len(str):
    if palavra[indice] == ch:
      return indice
    indice = indice + 1
  return -1

O terceiro parâmetro, chamado inicio, é opcional por possuir um valor padrão 0. Se invocarmos a nossa função find com apenas dois parâmetros, o valor padrão 0 será atribuído à variável inicio, fazendo com que a busca inicie do começo da string informada:

>>> find("banana", "n")
2

Se passarmos o terceiro parâmetro, o valor padrão (0) será sobrescrito pelo valor informado:

>>> find("banana", "n", 3)
4
>>> find("banana", "n", 5)
-1
>>> find("banana", "n", 4)
4

Observe de onde temos começado e como a função se desempenha.

Como exercício, adicione um quarto parâmetro à função find chamado fim. Este parâmetro deverá indicar onde a busca termina.

Atenção: Este exercício é um pouco complicado. O valor padrão de fim deve ser len(palavra), mas isso não funciona. Os valores padrão são atribuídos quando a função é declarada, não quando ela é chamada. Quando declaramos find, palavra ainda não existe, então você não consegue encontrar o seu tamanho.

14.6 O método de inicialização

O método de inicialização é um método especial que é invocado quando um objeto é criado. O nome deste método é __init__ (dois caracteres de underscore, seguidos por init, e mais dois caracteres de underscore). Um método de inicialização para a classe Horario seria semelhante ao seguinte:

class Horario:
  def __init__(self, horas = 0, minutos = 0, segundos = 0):
    self.horas = horas
    self.minutos = minutos
    self.segundos = segundos

Não há conflitos entre o atributo self.horas e o parâmetro horas. A notação do ponto especifica a qual variável estamos nos referindo.

Quando chamamos o construtor de Horario, os argumentos informados são passados para o método __init__:

>>> horaAtual = Horario(9, 14, 30)
>>> horaAtual.imprimirHorario()
9:14:30

Como os parâmetros são opcionais, nós podemos omití-los:

>>> horaAtual = Horario()
>>> horaAtual.imprimirHorario()
0:0:0

Ou passarmos apenas o primeiro argumento:

>>> horaAtual = Horario(9)
>>> horaAtual.imprimirHorario()
9:0:0

Ou os dois primeiros...

>>> horaAtual = Horario(9, 14)
>>> horaAtual.imprimirHorario()
9:14:0

Por fim, podemos fazer atribuições a um subconjunto de parâmetros informando-os explicitamente:

>>> horaAtual = Horario(segundos = 30, horas = 9)
>>> horaAtual.imprimirHorario()
9:0:30

14.7 Redefinindo a classe Ponto

Vamos reescrever a classe Ponto da seção 12.1 em um estilo mais orientado a objetos:

class Ponto:
  def __init__(self, x = 0, y = 0):
    self.x = x
    self.y = y

  def __str__(self):
    return '(' + str(self.x) + ', ' + str(self.y) + ')'

O método de inicialização possui os parâmetros opcionais x e y. O valor padrão para ambos é 0.

O método seguinte, chamado __str__, retorna uma string com a representação matemática de um Ponto. Se uma classe implementa o método __str__, este sobrescreve o comportamento padrão da função str provida pelo Python.

>>> p = Ponto(3, 4)
>>> str(p)
'(3, 4)'

Quando imprimimos um objeto Ponto, o Python chama implicitamente o método __str__ no objeto, então ao definirmos __str__, mudamos também o comportamento do print:

>>> p = Ponto(3, 4)
>>> print p
(3, 4)

Quando escrevemos uma nova classe, quase sempre começamos com o método __init__, pois este facilita a instanciação de objetos, e o método __str__, que quase sempre é muito útil para depuração.

14.8 Sobrecarga de operadores

Algumas linguagens de programação permitem que mudemos o comportamento de operadores comuns (+, -, *, etc.) quando eles são aplicados a tipos definidos pelo usuário. Este recurso é chamado de sobrecarga de operadores. É algo extremamente vantajoso principalmente quando estamos definindo novos tipos matemáticos.

Por exemplo, para sobrescrever o operadores de soma +, devemos implementar um método chamado __add__:

class Ponto:
  # Métodos definidos previamente devem estar aqui!

  def __add__(self, other):
    return Ponto(self.x + other.x, self.y + other.y)

Como já vimos anteriormente, o primeiro parâmetro é o objeto no qual o método foi invocado. O segundo parâmetro é chamado other por convenção, para distingui-lo de self. Para somar dois objetos do tipo Ponto, nós criamos e retornamos um novo Ponto que contém a soma das coordenadas x e a soma das coordenadas y.

Agora, quando aplicamos o operador de soma (+) para objetos do tipo Ponto, o Python invoca o método __add__:

>>> p1 = Ponto(3, 4)
>>> p2 = Ponto(5, 7)
>>> p3 = p1 + p2
>>> print p3
(8, 11)

A expressão p1 + p2 é equivalente à expressão p1.__add__(p2), mas é muito mais elegante.

Como exercício, crie o método __sub__ na classe Ponto. Este método representa o operador de subtração. Após criar o método, teste-o.

Há algumas formas de substituir o comportamento do operador de multiplicação: implementando um método chamado __mul__, ou __rmul__, ou ambos os métodos.

Se o membro da esquerda da multiplicação é um Ponto, o método __mul__ é invocado, que assumo que o outro membro da multiplicação também é um Ponto. Este método calcula o produto dos pontos, definido de acordo com as regras da álgebra linear:

def __mul__(self, other):
  return self.x * other.x + self.y * other.y

Se o membro da esquerda da multiplicação é um tipo primitivo e o membro da direita é um Ponto, o método __rmul__ é invocado. Este método calcula a multiplicação escalar:

def __rmul__(self, other):
  return Ponto(other * self.x, other * self.y)

O resultado é um novo Ponto onde as coordenadas são múltiplos das coordenadas originais. Se o parâmetro other pertencer a um tipo que não pode ser multiplicado por um número de ponto flutuante (float), o método __rmul__ produzirá um erro.

O exemplo a seguir demonstra os dois tipos de multiplicação:

>>> p1 = Ponto(3, 4)
>>> p2 = Ponto(5, 7)
>>> print p1 * p2
43
>>> print 2 * p2
(10, 14)

O que acontece se tentarmos executar a expressão p2 * 2? Como o primeiro operador é um Ponto, o método __mul__ será invocado, o programa tentará acessar a coordenada x do objeto other, mas esta operação falhará por que um inteiro não tem atributos:

>>> print p2 * 2
AttributeError: 'int' object has no attribute 'x'

Infelizmente, a mensagem de erro é um pouco sombria. Este exemplo apresentou uma das dificuldades da programação orientada a objetos. Às vezes é difícil até para descobrirmos o que está acontecendo no código executado.

Se você deseja um exemplo mais completo de sobrecarga de operadores, dê uma conferida no Apêndice B.

14.9 Polimorfismo

Muitos dos métodos que escrevemos tratam apenas um tipo específico de dados para cada parâmetro. Quando você cria um novo objeto, você cria métodos que trabalham com estes tipos.

Mas existem algumas operações onde você vai querer usar vários tipos, como as operações aritméticas da seção anterior. Se vários tipos dão suporte a um mesmo conjunto de operações, você pode escrever funções que trabalham com qualquer um desses tipos.

Por exemplo, a função multiplicaSoma (que é muito comum na álgebra linear) recebe três argumentos: ela multiplica os dois primeiros argumentos e soma o terceiro ao resultado da multiplicação. Podemos escrever isso em Python da seguinte forma:

def multiplicaSoma(x, y, z):
  return x * y + z

Esta função funcionará com qualquer valores de x e y que possam ser multiplicados entre si e com qualquer valor de z que possa ser somado ao produto.

Podemos chamar a função com valores numéricos:

>>> multiplicaSoma(3, 2, 1)
7

Ou com objetos do tipo Ponto:

>>> p1 = Ponto(3, 4)
>>> p2 = Ponto(5, 7)
>>> print multiplicaSoma(2, p1, p2)
(11, 15)
>>> print multiplicaSoma(p1, p2, 1)
44

No primeiro caso, o Ponto é multiplicado de forma escalar e somado a outro Ponto. No segundo caso, o produtos dos pontos gera um valor numérico, então o terceiro parâmetro também tem que ser um valor numérico.

Uma função como esta que pode tratar argumentos de diferentes tipos é chamada de função polimórfica.

Como outro exemplo, considere o método normalInvertida, que imprime uma lista duas vezes: do início ao fim e do fim ao início:

def normalInvertida(normal):
  import copy
  invertida = copy.copy(normal)
  invertida.reverse()
  print str(normal) + str(invertida)

Dado o fato que o método reverse é um modificador, criamos uma cópia da lista antes de invertê-la. Desta forma, o método não modifica a lista passada por parâmetro.

Segue um exemplo que usa normalInvertida em uma lista:

>>> minhaLista = [1, 2, 3, 4]
>>> normalInvertida(minhaLista)
[1, 2, 3, 4][4, 3, 2, 1]

Como projetamos a função pensando em usá-la com listas, não há surpresas em ver que ela funciona normalmente com listas. Ficaríamos surpresos ao descobrirmos que a função funciona também com objetos do tipo Ponto.

Para determinarmos se uma função pode ser aplicada a um tipo qualquer, nós verificamos a regra fundamental do polimorfismo:

Se todas as operações internas da função podem ser aplicados ao tipo, a função pode ser aplicada ao tipo.

As operações na função normalInvertida são copy, reverse e print.

A função copy funciona em qualquer objeto, e nós já definimos o método __str__ que permite que o print funcione sobre o objeto Ponto. Então precisamos escrever apenas o método reverse na nossa classe Ponto:

class Ponto:

# Métodos definidos previamente devem estar aqui!

def reverse(self):
self.x, self.y = self.y, self.x

Agora podemos passar um Ponto para a função normalInvertida:

>>> p = Ponto(3, 4)
>>> normalInvertida(p)
(3, 4)(4, 3)

A melhor parte do polimorfismo é a parte sem intenção, onde você descobre que uma função que você já escreveu pode ser aplicada a um tipo sem que você tenha planejado isso.

14.10 Glossário

função polimórfica
Uma função que pode operar com mais de um tipo. Se todas as operações de uma função pode ser aplicadas a um certo tipo, então a função pode ser aplicada a este tipo.
linguagem orientada a objetos
Uma linguagem que provê características tais como classes definidas pelo usuário e herança, que facilitam a programação orientada a objetos.
método
Uma função que é definida dentro de uma definição de classe e é chamada em instâncias desta classe.
método de inicialização (tambem chamado de construtor)
Um método especial que é invocado automaticamente quando um novo objeto é criado e que inicializa os atributos deste objeto.
multiplicação escalar
Operação definida na álgebra linear que multiplica cada uma das coordenadas de um ponto por um valor numérico.
override
Substituir uma definição já pronta. Exemplos incluem substituir um parâmetro padrão por um argumento particular e substituir um método padrão, fornecendo um novo método com o mesmo nome.
produto de pontos
Operação definida na álgebra linear que multiplica dois pontos (com coordenadas (x,y)) e retorna um valor numérico.
programação orientada a objetos
Um estilo de programação na qual os dados e as operações que os manipulam estão organizados em classes e métodos.
sobrecarga de operador
Estender a funcionalidade dos operadores nativos (+, -, *, >, <, etc.) de forma que eles funcionem também com tipos definidos pelo usuário.