Classes e Objetos - Indo um pouco mais fundo

Frações

Todos nós já tivemos de trabalhar com frações quando éramos mais jovens. Ou talvez você cozinha bastante e tem que lidar com medições dos ingredientes. De qualquer jeito, as frações são algo que estamos familiarizados. Neste capítulo vamos desenvolver uma classe para representar uma fração incluindo alguns dos métodos mais comuns que nós gostaríamos que as frações fossem capazes de fazer.

Uma fração é mais comumente vista como dois inteiros, um sobre o outro, com uma linha separando os dois. O número na parte superior é chamado de numerador e o número na parte inferior é chamado de denominador. Às vezes, as pessoas usam uma barra para a linha e, outras vezes, eles usam uma linha reta. O fato é que isso realmente não importa, desde que você saiba qual é o numerador e qual é o denominador.

Para projetar a nossa classe, nós simplesmente precisamos usar a análise acima para perceber que o estado de um objeto fração pode ser completamente descrito por dois números inteiros que o representam. Podemos começar pela implementação da classe Fraction e o método __init__ que permitirá que o usuário forneça o numerador e o denominador da fração a ser criada.

Observe que o método __str__ fornece um visual típico de fração usando uma barra entre o numerador e o denominador. A figura abaixo mostra o estado de myfraction. Nós também incluímos alguns métodos simples de acesso, getNum e getDen, que retornam os valores do estado da fração.

../_images/fractionpic1.png

Objetos são Mutáveis

Nós podemos mudar o estado de um objeto fazendo uma atribuição a uma das variáveis da instância. Por exemplo, pode-se alterar o numerador da fracção através da atribuição de um novo valor para self.num. Da mesma forma, nós poderíamos fazer a mesma coisa para self.den.

Um lugar onde este tipo de modificação faz sentido é quando simplificamos uma fração. Uma fração completamente simplificada significa que o numerador e o denominador não compartilham fatores comuns. Por exemplo, 12 / 16 é uma fracção, mas não está completamente simplificada pois é possível dividir 12 e 16 por 2, ou seja, 2 é um divisor comum. Se dividirmos o numerador eo denominador por um divisor comum, temos uma fração equivalente. Se dividirmos pelo máximo divisor comum, vamos obter a fração completamente simplificada. Neste caso 4 seria o máximo divisor comum e a fração simplificada seria 3/4.

Há um método iterativo muito elegante para calcular o maior divisor comum de dois inteiros. Tente executar a função para vários exemplos diferentes.

Agora que temos uma função para encontrar o máximo divisor comum, podemos usá-la para implementar um método fração chamado simplify. Vamos pedir à fração que ela se “simplifique completamente”.

O método simplify vai passar o numerador e o denominador para a função gcd para encontrar o máximo divisor comum. A instância então se modificará, dividindo o seu num e o seu den por esse valor.

Há duas coisas importantes a serem observadas nessa implementação. Em primeiro lugar, a função gcd não é um método da classe. Ele não pertence a Fraction. Em vez disso, é uma função que é usado por Fraction para auxiliar em uma tarefa que precisa ser realizada. Esse tipo de função é muitas vezes chamado de uma função auxiliar. Em segundo lugar, o método simplify não retorna nada. Sua função é modificar o objeto em si. Este tipo de método é conhecido como um método modificador (mutator) porque ele realiza modificações ou mutações do estado interno do objecto.

Igualdade

O significado da palavra mesmo parece perfeitamente claro até que você pense um pouco mais e então perceba que há mais do que você imaginava.

Por exemplo, se você diz, Chris e eu temos o mesmo carro, você quer dizer que seu carro e o dele são da mesma marca e modelo, mas que eles são dois carros diferentes. se você diz, Chris e eu temos a mesma mãe, você quer dizer que sua mãe e seu são a mesma pessoa.

Quando você fala sobre os objetos, há uma ambigüidade similar. Por exemplo, se dois Fractionss são os mesmos, isso significa que eles contêm os mesmos dados (o mesmo numerador e o mesmo denominador), ou que são, na verdade, o mesmo objeto?

Nós já vimos o operador is no capítulo sobre listas, onde nós falou sobre apelidos (aliases). Ela nos permite descobrir se duas referências se referem ao mesmo objeto.

Embora myfraction e yourfraction tenham o mesmo numerodor e denominador, eles não são o mesmo objeto.

../_images/fractionpic2.png

Se atribuirmos myfraction a ourfraction, então as duas variáveis são apelidos do mesmo objeto.

../_images/fractionpic3.png

Este tipo de igualdade é chamado de igualdade rasa porque só compara as referências, e não o conteúdo dos objectos. Usando o operador == para verificar a igualdade entre dois objetos definidos pelo usuário retornará o resultado da igualdade rasa. Em outras palavras, os objetos Fraction são iguais (==), se eles são o mesmo objeto.

Obviamente podemos definir a igualdade para significar que as fracções têm o mesmo numerador e o mesmo denominador. Por exemplo, aqui é uma função booleana que realiza essa verificação.

def sameFraction(f1,f2):
    return (f1.getNum() == f2.getNum()) and (f1.getDen() == f2.getDen())

Esse tipo de igualdade é chamada de igualdade profunda pois compara valores “profundos” no objeto, não apenas a referência ao objeto.

Obviamente, se duas variáveis se referem ao mesmo objeto, elas tem igualdade rasa e profunda.

Tenha cuidado com ==

“Quando eu uso uma palavra”, disse Humpty Dumpty, em um tom bastante desdenhoso, “ela significa apenas o que eu quiser que ela signifique — nem mais nem menos.” Alice no País das Maravilhas

O Python tem um poderoso recurso que permite que um designer de uma classe decida o que uma operação como == ou < deve significar. (Acabamos de mostrar como podemos controlar a forma como os nossos objetos são convertidos em strings, ou seja, já temos um começo!) Nós vamos ver mais detalhes mais tarde. Mas às vezes os implementadores usam igualdade rasas, e outras igualdade profunda, como mostrado neste pequeno experimento:

p = Point(4, 2)
s = Point(4, 2)
print("== on Points returns", p == s)  # by default, == does a shallow equality test here

a = [2,3]
b = [2,3]
print("== on lists returns",  a == b)  # by default, == does a deep equality test on lists

Que resulta na saída:

== on Points returns False
== on lists returns True

Assim, podemos concluir que, mesmo que duas listas (ou tuplas, etc.) sejam objetos distintos, com diferentes endereços de memória, em um caso o operador relacional == testa por igualdade profunda, enquanto que no caso de pontos faz um teste raso.

Métodos aritméticos

Vamos concluir este capítulo, acrescentando mais alguns métodos para a nossa classe Fraction. Em particular, vamos implementar aritmética. Para começar, considere o que significa adicionar duas frações. Lembre-se que você só pode adicionar frações se elas tiverem o mesmo denominador. A maneira mais fácil de encontrar um denominador comum é multiplicar os dois denominadores. Tudo o que fazemos para o denominador precisa ser feito para o numerador. Isso nos dá a seguinte equação para soma de frações

a/b + c/d = (ad + cb)/bd

Nosso método add vai receber um Fraction como um parâmetro. Ele irá retornar um novo Fraction que representa a soma. Nós vamos usar a equação mostrada acima para calcular o novo numerador e o novo denominador. Uma vez que esta equação não vai resultar em uma fração simplificada, vamos usar uma técnica semelhante a que foi usada no método simplify para encontrar o máximo divisor comum e, em seguida, dividir cada parte da nova fração.

def add(self,otherfraction):

    newnum = self.num*otherfraction.den + self.den*otherfraction.num
    newden = self.den * otherfraction.den

    common = gcd(newnum,newden)

    return Fraction(newnum//common,newden//common)

Experimente o método de adição e então modifique as frações e tente de novo.

Uma modificação final para este método irá ser bastante útil. Em vez de invocar o método add, podemos usar o operador de adição “+”. Isso exige que implementemos outro método especial, desta vez chamado __add__. Os detalhes do método são os mesmos.

def __add__(self,otherfraction):

    newnum = self.num*otherfraction.den + self.den*otherfraction.num
    newden = self.den * otherfraction.den

    common = gcd(newnum,newden)

    return Fraction(newnum//common,newden//common)

Porém, agora podemos fazer somas da maneira que estamos acostumados a fazer com outros dados numéricos.

f1 = Fraction(1,2)
f2 = Fraction(1,4)

f3 = f1 + f2    # calls the __add__ method of f1
print(f3)

operador + versus método __add__

Acontece que a adição é um método que existe para inteiros também. 4+5 pode ser escrito como (4).__add__(5). Estamos pedindo para o 4 invocar o seu método add, passando 5 como o outro valor.

Glossário

cópia profunda

Para copiar o conteúdo de um objeto, bem como quaisquer objetos incorporados, e dos objetos embutidos neles, e assim por diante; implementado pela função deepcopy do módulo copy.

cópia rasa

Para copiar o conteúdo de um objeto, incluindo quaisquer referências a objetos embutidos; implementada pela função copy no módulo copy.

igualdade profunda

Igualdade de valores, ou duas referências que apontam para os objetos que têm o mesmo valor.

igualdade rasa

Igualdade de referências, ou duas referências que apontam para o mesmo objeto.

Exercícios

  1. Inclua o método area à classe Rectangle que retorna a área de uma instância:

    r = Rectangle(Point(0, 0), 10, 5)
    test(r.area(), 50)
    
  2. Escreva um método perimeter na classe Rectangle para que possamos encontrar o perímetro de uma instância de Rectangle:

    r = Rectangle(Point(0, 0), 10, 5)
    test(r.perimeter(), 30)
    
  3. Escreva um método transpose na classe Rectangle que troca o largura e altura de uma instância de Rectangle:

    r = Rectangle(Point(100, 50), 10, 5)
    test(r.width, 10)
    test(r.height, 5)
    r.flip()
    test(r.width, 5)
    test(r.height, 10)
    
  4. Escreva um novo método na classe Rectangle para testar se um Point está dentro de um Rectangle. Para esse exercício, assuma que o retângulo está em (0, 0) com largura 10 e altura 5 tem limites superiores abertos na largura e na altura, ou seja, ele se estica na direção x de [0 a 10), onde o 0 está dentro mas o 10 está fora, e de [0 a 5] na direção y. Assim o ponto (10, 2) está fora. Esses testes devem passar:

    r = Rectangle(Point(0, 0), 10, 5)
    test(r.contains(Point(0, 0)), True)
    test(r.contains(Point(3, 3)), True)
    test(r.contains(Point(3, 7)), False)
    test(r.contains(Point(3, 5)), False)
    test(r.contains(Point(3, 4.99999)), True)
    test(r.contains(Point(-3, -3)), False)
    
  5. Em jogos, nós frequentemente colocamos uma moldura retangular ao redor de objetos do jogo. Assim fica mais fácil realizar a detecção de colisão entre objetos, como bombas e naves espaciais, verificando se há alguma região de sobreposição entre os retângulos.

    Escreva uma função para determinar se dois retângulos se colidem. Dica: esse exercício pode ser muito difícil! Pense bem sobre todos os casos possíveis antes de programar.

You have attempted of activities on this page