Capítulo 15: Classes e objetos

A esta altura você já sabe como usar funções para organizar código e tipos integrados para organizar dados. O próximo passo é aprender “programação orientada a objeto”, que usa tipos definidos pelos programadores para organizar tanto o código quanto os dados. A programação orientada a objeto é um tópico abrangente; será preciso passar por alguns capítulos para abordar o tema.

Os exemplos de código deste capítulo estão disponíveis em http://thinkpython2.com/code/Point1.py; as soluções para os exercícios estão disponíveis em http://thinkpython2.com/code/Point1_soln.py.

15.1 - Tipos definidos pelos programadores

Já usamos muitos tipos integrados do Python; agora vamos definir um tipo próprio. Como exemplo, criaremos um tipo chamado Point, que representa um ponto no espaço bidimensional.

Na notação matemática, os pontos muitas vezes são escritos entre parênteses, com uma vírgula separando as coordenadas. Por exemplo, (0,0) representa a origem e (x, y) representa o ponto que está x unidades à direita e y unidades acima da origem.

Há várias formas para representar pontos no Python:

Criar um tipo é mais complicado que outras opções, mas tem vantagens que logo ficarão evidentes.

Um tipo definido pelo programador também é chamado de classe. Uma definição de classe pode ser assim:

class Point:
    """Represents a point in 2-D space."""

O cabeçalho indica que a nova classe se chama Point. O corpo é uma docstring que explica para que a classe serve. Você pode definir variáveis e métodos dentro de uma definição de classe, mas voltaremos a isso depois.

Definir uma classe denominada Point cria um objeto de classe:

>>> Point
<class '__main__.Point'>

Como Point é definido no nível superior, seu “nome completo” é __main__.Point.

O objeto de classe é como uma fábrica para criar objetos. Para criar um Point, você chama Point como se fosse uma função:

>>> blank = Point()
>>> blank
<__main__.Point object at 0xb7e9d3ac>

O valor de retorno é uma referência a um objeto Point, ao qual atribuímos blank.

Criar um objeto chama-se instanciação, e o objeto é uma instância da classe.

Quando você exibe uma instância, o Python diz a que classe ela pertence e onde está armazenada na memória (o prefixo o 0x significa que o número seguinte está em formato hexadecimal).

Cada objeto é uma instância de alguma classe, então “objeto” e “instância” são intercambiáveis. Porém, neste capítulo uso “instância” para indicar que estou falando sobre um tipo definido pelo programador.

15.2 - Atributos

Você pode atribuir valores a uma instância usando a notação de ponto:

>>> blank.x = 3.0
>>> blank.y = 4.0

Essa sintaxe é semelhante à usada para selecionar uma variável de um módulo, como math.pi ou string.whitespace. Nesse caso, entretanto, estamos atribuindo valores a elementos nomeados de um objeto. Esses elementos chamam-se atributos.

Em inglês, quando é um substantivo, a palavra “AT-trib-ute” é pronunciada com ênfase na primeira sílaba, ao contrário de “a-TRIB-ute”, que é um verbo.

O diagrama seguinte mostra o resultado dessas atribuições. Um diagrama de estado que mostra um objeto e seus atributos chama-se diagrama de objeto; veja a Figura 15.1.

Figura 15.1 – Diagrama de um objeto Point.
Figura 15.1 – Diagrama de um objeto Point.

A variável blank refere-se a um objeto Point, que contém dois atributos. Cada atributo refere-se a um número de ponto flutuante.

Você pode ler o valor de um atributo usando a mesma sintaxe:

>>> blank.y
4.0
>>> x = blank.x
>>> x
3.0

A expressão blank.x significa “Vá ao objeto a que blank se refere e pegue o valor de x”. No exemplo, atribuímos este valor a uma variável x. Não há nenhum conflito entre a variável x e o atributo x.

Você pode usar a notação de ponto como parte de qualquer expressão. Por exemplo:

>>> '(%g, %g)' % (blank.x, blank.y)
'(3.0, 4.0)'
>>> distance = math.sqrt(blank.x ** 2 + blank.y ** 2)
>>> distance
5.0

Você pode passar uma instância como argumento da forma habitual. Por exemplo:

def print_point(p):
    print('(%g, %g)' % (p.x, p.y))

print_point toma um ponto como argumento e o exibe em notação matemática. Para invocá-lo, você pode passar blank como argumento:

>>> print_point(blank)
(3.0, 4.0)

Dentro da função, p é um alias para blank, então, se a função altera p, blank também muda.

Como exercício, escreva uma função chamada distance_between_points, que toma dois pontos como argumentos e retorna a distância entre eles.

15.3 - Retângulos

Às vezes, é óbvio quais deveriam ser os atributos de um objeto, mas outras é preciso decidir entre as possibilidades. Por exemplo, vamos supor que você esteja criando uma classe para representar retângulos. Que atributos usaria para especificar a posição e o tamanho de um retângulo? Você pode ignorar ângulo; para manter as coisas simples, suponha que o retângulo seja vertical ou horizontal.

Há duas possibilidades, no mínimo:

Nesse ponto é difícil dizer qual opção é melhor, então implementaremos a primeira, como exemplo.

Aqui está a definição de classe:

class Rectangle:
    """Represents a rectangle.
    attributes: width, height, corner.
    """

A docstring lista os atributos: width e height são números; corner é um objeto Point que especifica o canto inferior esquerdo.

Para representar um retângulo, você tem que instanciar um objeto Rectangle e atribuir valores aos atributos:

box = Rectangle()
box.width = 100.0
box.height = 200.0
box.corner = Point()
box.corner.x = 0.0
box.corner.y = 0.0

A expressão box.corner.x significa “Vá ao objeto ao qual box se refere e pegue o atributo denominado corner; então vá a este objeto e pegue o atributo denominado x”.

A Figura 15.2 mostra o estado deste objeto. Um objeto que é um atributo de outro objeto é integrado.

A Figura 10.1 mostra o diagrama de estado para cheeses, numbers e empty.

Figura 15.2 – Diagrama de um objeto Rectangle.
Figura 15.2 – Diagrama de um objeto Rectangle.

15.4 - Instâncias como valores de retorno

As funções podem retornar instâncias. Por exemplo, find_center recebe um Rectangle como argumento e devolve um Point, que contém as coordenadas do centro do retângulo:

def find_center(rect):
    p = Point()
    p.x = rect.corner.x + rect.width/2
    p.y = rect.corner.y + rect.height/2
    return p

Aqui está um exemplo que passa box como um argumento para find_center e atribui o ponto resultante à variável center:

>>> center = find_center(box)
>>> print_point(center)
(50, 100)

15.5 - Objetos são mutáveis

Você pode alterar o estado de um objeto fazendo uma atribuição a um dos seus atributos. Por exemplo, para mudar o tamanho de um retângulo sem mudar sua posição, você pode alterar os valores de width e height:

box.width = box.width + 50
box.height = box.height + 100

Você também pode escrever funções que alteram objetos. Por exemplo, grow_rectangle recebe um objeto Rectangle e dois números, dwidth e dheight, e adiciona os números à largura e altura do retângulo:

def grow_rectangle(rect, dwidth, dheight):
    rect.width += dwidth
    rect.height += dheight

Eis um exemplo que demonstra o efeito:

>>> box.width, box.height
(150.0, 300.0)
>>> grow_rectangle(box, 50, 100)
>>> box.width, box.height
(200.0, 400.0)

Dentro da função, rect é um alias de box, então quando a função altera rect, box aponta para o objeto alterado.

Como exercício, escreva uma função chamada move_rectangle que toma um Rectangle e dois números chamados dx e dy. Ela deve alterar a posição do retângulo, adicionando dx à coordenada x de corner e adicionando dy à coordenada y de corner.

15.6 - Cópia

Alias podem tornar um programa difícil de ler porque as alterações em um lugar podem ter efeitos inesperados em outro lugar. É difícil monitorar todas as variáveis que podem referir-se a um dado objeto.

Em vez de usar alias, copiar o objeto pode ser uma alternativa. O módulo copy contém uma função chamada copy que pode duplicar qualquer objeto:

>>> p1 = Point()
>>> p1.x = 3.0
>>> p1.y = 4.0
>>> import copy
>>> p2 = copy.copy(p1)

p1 e p2 contêm os mesmos dados, mas não são o mesmo Point:

>>> print_point(p1)
(3, 4)
>>> print_point(p2)
(3, 4)
>>> p1 is p2
False
>>> p1 == p2
False

O operador is indica que p1 e p2 não são o mesmo objeto, que é o que esperamos. Porém, você poderia ter esperado que == fosse apresentado como True, porque esses pontos contêm os mesmos dados. Nesse caso, pode ficar desapontado ao saber que, para instâncias, o comportamento padrão do operador == é o mesmo que o do operador is; ele verifica a identidade dos objetos, não a sua equivalência. Isso acontece porque, para tipos definidos pelo programador, o Python não sabe o que deve ser considerado equivalente. Pelo menos, ainda não.

Se você usar copy.copy para duplicar um retângulo, descobrirá que ele copia o objeto Rectangle, mas não o Point embutido nele:

>>> box2 = copy.copy(box)
>>> box2 is box
False
>>> box2.corner is box.corner
True

A Figura 15.3 mostra como fica o diagrama de objeto. Esta operação chama-se cópia superficial porque copia o objeto e qualquer referência que contenha, mas não os objetos integrados.

Figura 15.3 – Diagrama: dois objetos Rectangle compartilhando o mesmo Point.
Figura 15.3 – Diagrama: dois objetos Rectangle compartilhando o mesmo Point.

Para a maior parte das aplicações, não é isso que você quer. Nesse exemplo, invocar grow_rectangle em um dos Rectangles não afetaria o outro, mas invocar move_rectangle em qualquer um deles afetaria a ambos! Esse comportamento é confuso e propenso a erros.

Felizmente, o módulo copy oferece um método chamado deepcopy que copia não só o objeto, mas também os objetos aos quais ele se refere, e os objetos aos quais estes se referem, e assim por diante. Você não se surpreenderá ao descobrir que esta operação se chama cópia profunda.

>>> box3 = copy.deepcopy(box)
>>> box3 is box
False
>>> box3.corner is box.corner
False
box3 e box são objetos completamente separados.

Como exercício, escreva uma versão de move_rectangle que cria e retorne um novo retângulo em vez de alterar o antigo.

15.7 - Depuração

Ao começar a trabalhar com objetos, provavelmente você encontrará algumas novas exceções. Se tentar acessar um atributo que não existe, recebe um AttributeError:

>>> p = Point()
>>> p.x = 3
>>> p.y = 4
>>> p.z
AttributeError: Point instance has no attribute 'z'

Se não estiver certo sobre o tipo que um objeto é, pode perguntar:

>>> type(p)
<class '__main__.Point'>

Você também pode usar isinstance para verificar se um objeto é uma instância de uma classe:

>>> isinstance(p, Point)
True

Caso não tenha certeza se um objeto tem determinado atributo, você pode usar a função integrada hasattr:

>>> hasattr(p, 'x')
True
>>> hasattr(p, 'z')
False

O primeiro argumento pode ser qualquer objeto; o segundo argumento é uma string com o nome do atributo.

Você também pode usar uma instrução try para ver se o objeto tem os atributos de que precisa:

try:
    x = p.x
except AttributeError:
    x = 0

Essa abordagem pode facilitar a escrita de funções que atuam com tipos diferentes; você verá mais informações sobre isso em “Polimorfismo”, na página 248.

15.8 - Glossário

classe
Tipo definido pelo programador. Uma definição de classe cria um objeto de classe.
objeto de classe
Objeto que contém a informação sobre um tipo definido pelo programador. O objeto de classe pode ser usado para criar instâncias do tipo.
instância
Objeto que pertence a uma classe.
instanciar
Criar um objeto.
atributo
Um dos valores denominados associados a um objeto.
objeto integrado
Objeto que é armazenado como um atributo de outro objeto.
cópia superficial
Copiar o conteúdo de um objeto, inclusive qualquer referência a objetos integrados; implementada pela função copy no módulo copy.
cópia profunda
Copiar o conteúdo de um objeto, bem como qualquer objeto integrado, e qualquer objeto integrado a estes, e assim por diante; implementado pela função deepcopy no módulo copy.
diagrama de objeto
Diagrama que mostra objetos, seus atributos e os valores dos atributos.

15.9 - Exercícios

Exercício 15.1

  1. Escreva uma definição para uma classe denominada Circle, com os atributos center e radius, onde center é um objeto Point e radius é um número.

  2. Instancie um objeto Circle, que represente um círculo com o centro em 150, 100 e raio 75.

  3. Escreva uma função denominada point_in_circle, que tome um Circle e um Point e retorne True, se o ponto estiver dentro ou no limite do círculo.

  4. Escreva uma função chamada rect_in_circle, que tome um Circle e um Rectangle e retorne True, se o retângulo estiver totalmente dentro ou no limite do círculo.

  5. Escreva uma função denominada rect_circle_overlap, que tome um Circle e um Rectangle e retorne True, se algum dos cantos do retângulo cair dentro do círculo. Ou, em uma versão mais desafiadora, retorne True se alguma parte do retângulo cair dentro do círculo.

Solução: http://thinkpython2.com/code/Circle.py.

Exercício 15.2

  1. Escreva uma função chamada draw_rect que receba um objeto Turtle e um Rectangle e use o Turtle para desenhar o retângulo. Veja no Capítulo 4 os exemplos de uso de objetos Turtle.

  2. Escreva uma função chamada draw_circle, que tome um Turtle e um Circle e desenhe o círculo.