Introdução

Na primeira parte desse curso tratamos de tipos básicos como int e float, e na segunda parte introduzimos listas e strings, que são estruturas mais complexas, capazes de reunir sequências de dados. Nesse terceira parte vamos explorar conceitos de programação orientada à objetos para tratar de tipos de dados ainda mais complexos. Dentre os tópicos que serão cobertos estão:

  • Princípios de programação orientada a objetos
    • Classes e objetos
    • Atributos e métodos
  • Tipos abstratos de dados
    • Filas
    • Pilhas
  • Algoritmos de busca e ordenação;
    • Busca sequencial e binária
    • Algoritmos elementares de ordenação
    • Noção de complexidade

Programação orientada a objetos

Objetivo

Introduzir conceitos de orientação a objetos. Ao final dessa aula você deve saber o que é uma classe e a diferença para um objeto da classe, o que são atributos e métodos. Você saberá também como definir um classe em Python, e utilizar o método __init__ (construtor) e __str__.

Tópicos

Programação orientada a objetos

A capacidade de representar e abstrair conceitos é fundamental para o nosso pensamento, tornando possível a generalização de coisas e a construção de conceitos cada vez mais complexos.

Na evolução das linguagens de programação, a necessidade de se representar e manipular informações complexas resultou no conceito de classes e objetos, onde classes servem como abstrações (representação) e objetos seriam instâncias de classes que mantém e permitem a manipulação da informação.

Classes e Objetos em Python

Em Python, toda informação que usamos é representada na forma de um objeto. Assim, o número 6 é um objeto da classe int, o número 3.14 é um objeto da classe float, e assim por diante.

>>> type(6)  # type mostra o tipo do objeto passado como argumento
<class 'int'>
>>> id(6)    # mostra o local na memória (endereço) onde o objeto está armazenado
4297370848
>>> i = 6
>>> j = 6
>>> id(i)    # note que i e j correspondem ao mesmo objeto 6
4297370848   # pois estão no mesmo lugar na memória
>>> id(j)
4297370848
>>>

Em computação, criar um objeto significa criar uma instância de uma classe. Linguagens orientadas a objetos permitem a definição de novas classes.

Uma classe é uma abstração de alguma “coisa”, que possui um estado e comportamento. Um estado é definido por um conjunto de variáveis chamadas de atributos. Esses estados podem ser alterados por meio de “ações” sobre o objeto, que definem seu comportamento. Essas ações são funções chamadas de “métodos”.

Por exemplo, para representar um carro podemos definir:

- uma classe Carro:
- com os atributos: ano, modelo, cor, e vel
- e os métodos:
    - acelera(vel): acelera até vel
    - pare(): vel = 0

Definição de classes e criação de objetos

Uma classe mínima em Python pode ser definida como:

>>> class Carro:
>>>    pass

>>> fusca = Carro()
>>> brasilia = Carro()

Essa classe não possui atributos nem métodos, mas já nos permite criar objetos, ou seja, instâncias da classe Carro.

Atributos

Para criar atributos, basta fazer atribuições usando nome_do_objeto.nome_do_atributo e, a seguir, usar os atributos como variáveis. Exemplo:

(exemplo1_carros)



Assim como listas, tome muito cuidado com referências ao mesmo objeto. Crie outros atributos e outros carros e verifique o que acontece.

O método especial __init__ (construtor)

Como sabemos que todos os carros que queremos representar possuem as propriedades “cor”, “ano”, e “modelo”, podemos deixar essas propriedades como atributos da classe e inicializá-las quando um objeto é instanciado (criado). Para isso, vamos utilizar o método especial __init__, conhecido como construtor da classe.

A nova definição da classe ficaria assim:

(exemplo02_carros)



Mas o que é esse self?

Todo método de uma classe recebe como primeiro parâmetro uma referência à instância que chama o método, permitindo assim que o objeto acesse os seus próprios atributos e métodos. Por convenção, chamamos esse primeiro parâmetro de self, mas qualquer outro nome poderia ser utilizado.

O método especial __init__ é chamado automaticamente após a criação de um objeto, e no caso de um Carro, na nova instância são criados os atributos ‘modelo’, ‘ano’ e ‘cor’ com os valores dados como argumentos do construtor (no caso Carro(“Brasilia”, 1968, “amarela”)).

Assim, no corpo de qualquer método, um atributo pode ser criado, acessado ou modificado usando self.nome_do_atributo.

Outros métodos

Para exemplificar a criação de outros métodos e entender melhor o papel do self vamos criar os métodos imprima, acelere e pare. Por ser um exemplo mais elaborado, vamos também colocar os testes dentro da função main.

(exemplo03_carros)



Leia esse código com atenção e confira a sua execução.

Na função main, pri e seg são instâncias de Carro. Para chamar o método acelere de Carro, usamos a notação:

`Carro.acelere(pri, 40)`

O método acelere recebe dois parâmetros, self e v. Assim, quando o método é executado, self é o objeto pri e v é o número 40. Observe que na chamada:

`Carro.pare(pri)`

Apenas a instância (self) é passada ao método pare, pois esse método recebe apenas 1 argumento.

Essa notação explicita a passagem dos objetos como primeiro parâmetro em cada método, mas é redundante visto que todo objeto sabe a que classe ele pertence. Uma notação mais comum e enxuta é chamar os métodos usando a notação com . de forma semelhante aos atributos e, como o primeiro argumento é sempre ele mesmo, ele pode ser evitado. Assim a chamada:

`Carro.acelere.(pri, 40)`

pode ser escrita como:

`pri.acelere(40)`

O exemplo abaixa mostra o mesmo código anterior, mas usando a notação mais enxuta.

(exemplo04_carros)



Observe que tanto o método acelere quanto pare chamam o método imprima usando self.imprima() para exibir o estado da instância.

O método especial __str__

O método imprima é muito útil para qualquer tipo de objeto mas não é a forma mais comum utilizando objetos. Em muitos casos, é desejável simplesmente utilizar a função print para imprimir uma “representação textual” do objeto.

Na verdade, quando executamos o comando print(6), como o 6 é um objeto da classe int, a função print converte o inteiro 6 no string 6 antes de ser impresso, algo como print( str(6) ). Lembre-se que, dentre as funções para conversão de tipos (lembra do int em int(input(“Digite um número: ”))?), a função str converte o inteiro 6 para o string 6.

Para exibir então o conteúdo de um objeto usando print, devemos definir o método especial __str__, e devolver um string com a representação textual do objeto. A função str também chama e retorna o valor desse método (se existir).

O código abaixo é basicamente o mesmo que o anterior, mas substituimos o método imprima pelo método especial __str__. No corpo desse método, ao invés de chamar a função print, carrega-se um string apropriado em s, que é retornado pelo método para a função print ou str. Veja que nos métodos acelere e pare, basta chamar print(self).

(exercício05_carros)



Exercício 17.1

Escreva uma classe Complexo que permita representar números complexos e ao menos os métodos:

  • __init__ (esse precisa estar presente em todas as classes)
  • soma
  • multiplicação
  • __str__, que retorna um string na forma a+bj (ou a-bj), onde a e b são as partes real e imaginária do número complexo.

OBS: o Python tem uma classe complex para manipular números complexos.

(exercício_17_1_tentativa)