Aula 2. Estruturas de controle, Funções e Classes

1. Estruturas de Controle

Estruturas de controle alteram a sequência em que instruções são executadas em um programa. Aqui estudaremos as estruturas condicionais e estruturas de repetição.

Estruturas Condicionais

Estruturas condicionais permitem alterar o fluxo de execução de um programa através da verificação de condições, que podem ser codificadas em nosso programa por meio de expressões lógicas (aquelas cuja avaliação resulta em valores do tipo booleano: True ou False). Podemos construir estruturas condicionais com diferentes níveis de complexidade, dependendo do conjunto de condições. Vejamos como construí-las

A construção if

A construção if é a maneira mais simples de escrevermos um bloco de código $A$ que somente deve ser executado caso uma determinada condição seja satisfeita (ou seja, avaliada como True). Caso contrário, o código $A$ é simplesmente ignorado

In [1]:
print("Antes da condicional: sempre executa")

cond = True

if cond:
    print("Código A: só executa caso a condição seja verdadeira")

print("Após a condicional: sempre executa")
Antes da condicional: sempre executa
Código A: só executa caso a condição seja verdadeira
Após a condicional: sempre executa

A construção if else

Um if pode ser associado a um else, contendo um bloco de código que é executado caso a condição do if não seja satisfeita (ou seja, avaliada como False). Assim como no exemplo anterior, o código $A$ é executado caso a condição seja verdadeira. A diferença é que, no caso de a condição ser falsa, um código alternativo $B$ será executado

In [2]:
print("Antes da condicional: sempre executa")

cond = True

if cond:
    print("Código A: só executa caso a condição seja verdadeira")
else:
    print("Código B: só executa caso a condição seja falsa")

print("Após a condicional: sempre executa")
Antes da condicional: sempre executa
Código A: só executa caso a condição seja verdadeira
Após a condicional: sempre executa

As duas construções tratadas acima se baseiam na verificação de apenas uma condição, que dependendo de como for avaliada (True ou False), desencadeia um dentre dois possíveis comportamentos. Mas e se desejarmos construir uma estrutura condicional que permita mais do que apenas dois "caminhos" diferentes? Neste caso precisamos verificar mais do que uma única condição. Para isso precisamos usar estruturas condicionais aninhadas

Condicionais aninhadas

A primeira forma de incluir mais mais do que uma condição a ser testada na estrutura condicional é por aninhamento. Como resultado, teremos uma estrutura hierárquica, em que a execução de uma segunda condição depende do resultado de uma primeira condição. Não há limites para o número de níveis hierárquicos em uma estrutura condicional, apesar de que muitos níveis hierárquicos podem tornar o código mais difícil de entender.

Cuidado! Em Python é fundamental respeitar o nível de indentação dos blocos de código em cada nível hierárquico!

In [3]:
print("Antes da condicional: sempre executa")

cond1 = True
cond2 = True

if cond1:
    print("Código A: só executa caso a condição 1 seja verdadeira (não importa a condição 2)")
else:
    if cond2:
        print("Código B: só executa caso a condição 2 seja verdadeira (condição 1 foi falsa)")
    else:
        print("Código C: só executa caso a condição 2 seja falsa (condição 1 foi falsa)")

print("Após a condicional: sempre executa")
Antes da condicional: sempre executa
Código A: só executa caso a condição 1 seja verdadeira (não importa a condição 2)
Após a condicional: sempre executa

A construção if elif else

Uma sequência de vários if e else aninhados pode inserir muitos níveis de indentação no código, tornando-o confuso. Para evitar este problema, usamos a construção podemos inserir novas condições a serem verificadas usando a palavra elif. Pense no elif como uma fusão entre um else e um if. O else no final captura qualquer caso em que nenhuma das condições anteriores foram satisfeitas.

O código das condicionais aninhadas na célula acima pode ser reescrito usando um elif. Note que agora toda a estrutura condicional está no mesmo nível de indentação!

In [4]:
print("Antes da condicional: sempre executa")

cond1 = True
cond2 = True

if cond1:
    print("Código A: só executa caso a condição 1 seja verdadeira (não importa a condição 2)")
elif cond2:
    print("Código B: só executa caso a condição 2 seja verdadeira (condição 1 foi falsa)")
else:
    print("Código C: só executa caso a condição 2 seja falsa (condição 1 foi falsa)")

print("Após a condicional: sempre executa")
Antes da condicional: sempre executa
Código A: só executa caso a condição 1 seja verdadeira (não importa a condição 2)
Após a condicional: sempre executa

Estruturas de Repetição

Estruturas de repetição nos permitem realizar tarefas repetitivas, potencializando o nível de automação em nossos programas. Loops (em português "laços de repetição") são estruturas que garantem que determinada parte do código seja repetida várias vezes. Existem dois tipos fundamentais de loops: (i) for loops e (ii) while loops

Loops do tipo "for"

Loops do tipo "for" permitem percorrer um objeto iterável (como uma lista, uma tupla ou uma range) sequencialmente. Tipicamente, usamos loops do tipo "for" quando já sabemos, a priori, o número de iterações. Sua estrutura é composta por:

  • uma variável de iteração (normalmente chamada i ou j, mas poderia receber qualquer outro nome);
  • a lista ou objeto iterável.

Percorrendo uma lista

In [5]:
lista = ['a','b','c','d','e','f']

for i in lista:
    print(f"Nova iteração: elemento {i}")
Nova iteração: elemento a
Nova iteração: elemento b
Nova iteração: elemento c
Nova iteração: elemento d
Nova iteração: elemento e
Nova iteração: elemento f

Percorrendo uma range

In [6]:
for i in range(1,11):
    print(f"Nova iteração: elemento {i}")
Nova iteração: elemento 1
Nova iteração: elemento 2
Nova iteração: elemento 3
Nova iteração: elemento 4
Nova iteração: elemento 5
Nova iteração: elemento 6
Nova iteração: elemento 7
Nova iteração: elemento 8
Nova iteração: elemento 9
Nova iteração: elemento 10

Percorrendo elementos em um dicionário.

In [7]:
dicio = { 'k1':'val1', 'k2':'val2', 'k3':'val3','k4':'val4', 'k5':'val5' }

for k,v in dicio.items():
    print(f"Nova iteração: chave {k} --> valor {v}")
Nova iteração: chave k1 --> valor val1
Nova iteração: chave k2 --> valor val2
Nova iteração: chave k3 --> valor val3
Nova iteração: chave k4 --> valor val4
Nova iteração: chave k5 --> valor val5

Obs 1. O método items fornece uma lista de 2-tuplas, cada uma contendo uma chave e seu valor, respectivamente.

Dica. Sequence unpacking

Quando escrevemos k,v no lugar da variável de iteração, estamos associando cada chave a uma variável de nome k e cada valor a uma variável de nome v. Esta forma de associar as variáveis se chama sequence unpacking. Ela funciona tanto para objetos iteráveis, em geral (listas, tuplas, ranges). No entanto, é necessário que o número de variáveis seja igual ao número de elementos dentro do objeto

In [8]:
var1, var2 = ( 'a', 'b' )

print(f"var1 recebe {var1}")
print(f"var2 recebe {var2}")
var1 recebe a
var2 recebe b

Dica. Enumerating

Podemos também usar a função enumerate para obter o índice de cada elemento na lista, além de seu valor. A cada iteração, o índice do elemento e o valor são retornados como uma 2-tupla

In [9]:
lista = ['a','b','c','d','e','f']

for i,el in enumerate(lista):
    print(f"Nova iteração (índice {i}): elemento {el}")
Nova iteração (índice 0): elemento a
Nova iteração (índice 1): elemento b
Nova iteração (índice 2): elemento c
Nova iteração (índice 3): elemento d
Nova iteração (índice 4): elemento e
Nova iteração (índice 5): elemento f

Loops do tipo "while"

Loops do tipo while permitem continuar um loop enquanto determinada condição é satisfeita. São mais adequados nos casos em que não conseguimos determinar, a priori, o número total de iterações a serem realizadas.

Em loops do tipo "while", normalmente usamos uma variável de controle, como uma variável contadora (normalmente chamada cntr). Esta variável de controle deve ser atualizada a cada iteração!

In [10]:
cntr = 0

while cntr < 10:
    print(f"Nova iteração. Variável contadora tem valor {cntr}")
    cntr = cntr + 1
Nova iteração. Variável contadora tem valor 0
Nova iteração. Variável contadora tem valor 1
Nova iteração. Variável contadora tem valor 2
Nova iteração. Variável contadora tem valor 3
Nova iteração. Variável contadora tem valor 4
Nova iteração. Variável contadora tem valor 5
Nova iteração. Variável contadora tem valor 6
Nova iteração. Variável contadora tem valor 7
Nova iteração. Variável contadora tem valor 8
Nova iteração. Variável contadora tem valor 9

Podemos também percorrer uma lista com um loop "while", embora neste caso o "for" seja mais adequado. Neste caso, a condição é que entramos em um novo loop sempre que o índice (i) for menor que o comprimento da lista (len(lista)). O índice i começa em 0 e é incrementado a cada loop

In [11]:
lista = ['a','b','c','d','e','f']
i = 0

while i < len(lista):
    print(f"Nova iteração: Elemento {lista[i]} (índice {i})")
    i = i + 1
Nova iteração: Elemento a (índice 0)
Nova iteração: Elemento b (índice 1)
Nova iteração: Elemento c (índice 2)
Nova iteração: Elemento d (índice 3)
Nova iteração: Elemento e (índice 4)
Nova iteração: Elemento f (índice 5)

Cuidado! Loops do tipo "while" podem originar loops infinitos, caso a condição do loop seja atualizada. Isso normalmente ocorre quando o programador se esquece de atualizar ou mesmo incluir a variável de controle.

In [12]:
# Atenção: esta célula gera um loop infinito. Para finalizá-lo, clique no botão "pause", na barra de ferramentas
cond=True

while cond:
    print("Nova iteração")
Nova iteração
Nova iteração
Nova iteração
Nova iteração
Nova iteração
Nova iteração
Nova iteração
Nova iteração
Nova iteração
Nova iteração
Nova iteração
Nova iteração
Nova iteração
Nova iteração
Nova iteração
Nova iteração
Nova iteração
Nova iteração
Nova iteração
Nova iteração
Nova iteração
...

2. Funções

Funções são construções que encapsulam um determinado comportamento que se espera executar múltiplas vezes durante a execução de um programa. São rotinas, que podem ter sido definidas pelo próprio programador ou por outros programadores, cujos detalhes da implementação são abstraídos para o usuário. O usuário portanto só precisa saber o que precisa saber como operar a função: que tipos de coisas deve fornecer como entrada (input) e o que deve esperar receber como saída (output)

Funções facilitam sua vida por dois motivos principais.

  1. Permitem ao programador abstrair computações, sem precisar se preocupar a todo momento sobre os mínimos detalhes de como elas são de fato realizadas. Imagine se você tivesse que se preocupar com os detalhes sobre como um texto é imprimido na tela de seu computador toda vez que você precisasse desta funcionalidade... Felizmente a função print permite que esta rotina seja abstraída para você, o que torna seu trabalho muito mais fluido!

  2. Permitem compartilhar e reutilizar código. Se você construir uma função que possa ajudar outras pessoas também, por que não compartilhar? Isso acontece bastante na comunidade de programadores, e os ajuda a não ficar "reinventando a roda" quando precisam de alguma funcionalidade que já foi implementada por alguém.

Para começar a entender como trabalhar com funções em Python, precisamos conhecer seus três componentes principais: (i) nome, (ii) parâmetros e (iii) corpo.

Dar um nome às funções é uma forma simples de mantermos uma referência a elas. Embora possamos nomear funções conforme nossa vontade, é recomendável escolhermos nomes que nos digam algo sobre seu funcionamento. É fácil lembrar que a função print, por exemplo, serve para imprimir algo na tela.

Os parâmetros fornecem um meio para "afinarmos" o comportamento de uma função para nossas necessidades. Os dados de entrada (inputs) são também passados para as funções através de parâmetros. No jargão da programação, nos referimos aos valores mapeados para cada um dos parâmetros como argumentos. Pense em um parâmetro como um placeholder para um valor, enquanto o argumento é o valor em si, passado para dentro da função através de um parâmetro.

Por fim, no corpo da função especificamos todas as etapas que devem ser realizadas por ela. Estas etapas incluem o processamento dos dados de entrada (inputs) e a construção do resultado que ela produzirá como saída (output). Lembre-se: no fim das contas, a ideia é que as computações descritas no corpo da função sejam abstraídas para o usuário da função.

Definindo funções

Para definir (criar) uma nova função precisamos obedecer à seguinte sintaxe:

  • a palavra def indica que uma nova função está sendo definida;
  • após def, deve ser escrito o nome da função;
  • após o nome da função, entre parênteses, os parâmetros são separados por vírgulas;
  • Os dois pontos : após os parênteses indica que o corpo da função vem a seguir, no bloco de código abaixo. Ele contém todas as instruções (ou algoritmo) que determinam como a função se comporta;
  • No fim do corpo da função, o valor de saída (output) é indicado após a palavra return.

Cuidado! O bloco de código deve ser escrito com uma indentação, que é o distanciamento em relação à margem esquerda da célula

def nome_funcao(par1, par2):
    instrucao_1
    instrucao_2
    instrucao_3
    return resultado

Para demonstrar como definir funções em Python, vamos começar com alguns exemplos bastante simples. As funções a seguir apenas imprimem mensagens no console.

Podemos declarar funções sem parâmetros

In [13]:
def foo():
    print("Função `foo` foi executada")
    print("Como não há parâmetros, o comportamento da função sempre será o mesmo")
In [14]:
foo()
Função `foo` foi executada
Como não há parâmetros, o comportamento da função sempre será o mesmo

com apenas um parâmetro

In [15]:
def bar(par1):
    print("Função `bar` foi executada")
    print(f"Parâmetro par1 recebe {par1} como valor (argumento)")
In [16]:
bar('Santos')
Função `bar` foi executada
Parâmetro par1 recebe Santos como valor (argumento)

ou com múltiplos parâmetros

In [17]:
def baz(par1, par2):
    print("Função `baz` foi executada")
    print(f"Parâmetro par1 recebe argumento {par1}")
    print(f"Parâmetro par2 recebe argumento {par2}")
In [18]:
baz('Santos','Dumont')
Função `baz` foi executada
Parâmetro par1 recebe argumento Santos
Parâmetro par2 recebe argumento Dumont

Podemos especificar um valor default para os parâmetros, que será usado caso o usuário não forneça outro

In [19]:
def baz2(par1, par2="Barney"):
    print("Função `baz2` foi executada")
    print(f"Parâmetro par1 recebe argumento {par1}")
    print(f"Parâmetro par2 recebe argumento {par2}")
In [20]:
baz2('Santos')
Função `baz2` foi executada
Parâmetro par1 recebe argumento Santos
Parâmetro par2 recebe argumento Barney
In [21]:
baz2('Santos', 'Dumont')
Função `baz2` foi executada
Parâmetro par1 recebe argumento Santos
Parâmetro par2 recebe argumento Dumont

Podemos também passar os argumentos fora de ordem, desde que especifiquemos a quais parâmetros eles devem ser mapeados

In [22]:
baz2(par2="Dumont", par1="Santos")
Função `baz2` foi executada
Parâmetro par1 recebe argumento Santos
Parâmetro par2 recebe argumento Dumont

Retornos de funções

As funções que vimos acima apenas imprimem mensagens no console. Mas normalmente esperamos que funções retornem os resultados de suas computações como valores, que podem ser associados a variáveis e utilizados em computações subsequentes. Vamos ver como retornar resultados de funções usando a keyword return

Vamos construir uma função adicao que soma dois números e retorna o resultado

In [23]:
def adicao(n1,n2):
    res = n1 + n2
    return res
In [24]:
adicao(3,7)
Out[24]:
10

Da mesma forma, podemos construir funções que fazem subtração, multiplicação e divisão

In [25]:
def subtracao(n1,n2):
    res = n1 - n2
    return res

def multiplicacao(n1,n2):
    return n1 * n2

def divisao(n1,n2):
    return n1/n2

Cada uma dessas funções realiza uma determinada operação matemática com dois números n1 e n2 e retorna o resultado

In [26]:
a,b,c,d = 3,5,7,10

Vamos agora fazer as seguintes operações, usando apenas as funções que declaramos acima. Primeiramente usaremos variáveis para armazenar os valores resultantes de cada operação:

  1. u recebe o resultado da adição de a e b
  2. v recebe o resultado da subtração entre d e c
  3. w recebe o resultado da multiplicação de u e v
  4. x recebe o resultado da divisão de w por 2
In [27]:
u = adicao(a,b)
v = subtracao(d,c)
w = multiplicacao(u,v)
x = divisao(w,2)

Vamos verificar o valor final (em x)

In [28]:
x
Out[28]:
12.0

Poderíamos também realizar todas estas etapas em apenas uma expressão, evitando usar variáveis intermediárias. Por definição, funções mais "internas", que são passadas como argumentos para outras, executam primeiro. Seus valores retornados são passados como argumentos para funções mais "externas".

In [29]:
divisao( multiplicacao( adicao(a,b), subtracao(d,c) ), 2)
Out[29]:
12.0

No exemplo acima, as funções adicao e subtracao são as primeiras a serem avaliadas. Seus resultados são usados pela função multiplicacao que, por sua vez, tem seu resultado passado para a função divisão, que é a última a ser executada

Funções Anônimas, ou Lambda

Funções lambda são consideradas funções anônimas, por não serem necessariamente associadas a um nome. São constituídas por uma única expressão. Sua construção é simples:

lambda x p1,p2,p3: expressão
  • A keyword lambda indica a criação de uma função lambda;
  • O conjunto de parâmetros (p1, p2, p3), cada qual separado do próximo por uma vírgula, é inserido após a keyword lambda. São encerrados por dois-pontos(:);
  • Após os dois-pontos (:), escrevemos uma expressão que irá compor o corpo da função. O valor retornado é o resultado da expressão, e portanto não usamos a keyword return

A grande vantagem é que podemos, em apenas uma linha, construir e executar pequenas funções, sem o passo intermediário de armazená-las em memória. Este comportamento fornece grande agilidade em alguns casos, como por exemplo quando precisamos fornecer funções como argumentos para outras funções. Como veremos nas próximas aulas, é comum usarmos este recurso quando queremos aplicar uma função a um conjunto de dados (mais sobre isso nas próximas aulas)

Vamos construir uma função lambda com um único parâmetro x, cuja funcionalidade é exponenciar x à potência 2

In [30]:
lambda x: x**2 
Out[30]:
<function __main__.<lambda>(x)>

Agora vamos, em uma linha, construir três variações destas funções e executá-las imediatamente. Perceba que as funções não permanecem em memória, e "desaparecem" assim que são executadas

In [31]:
(lambda x: x**2)(2)
Out[31]:
4
In [32]:
(lambda x: x**3)(2)
Out[32]:
8
In [33]:
(lambda x: x**4)(2)
Out[33]:
16

Se quisermos, podemos também guardar uma função lambda em memória, bastando para isso associá-la a uma variável

In [34]:
exp2 = lambda x: x**2
exp3 = lambda x: x**3
exp4 = lambda x: x**4
In [35]:
print( exp2(2) )
print( exp3(2) )
print( exp4(2) )
4
8
16

Funções lambda não necessariamente precisam ter apenas um parâmetro! Vamos adicionar o parâmetro p, que indica a potência da exponenciação

In [36]:
exp = lambda x,p: x**p
In [37]:
print( exp(2,2) )
print( exp(2,3) )
print( exp(2,4) )
4
8
16

Inclusive, podemos fornecer argumentos por default

In [38]:
exp = lambda x,p=2: x**p
In [39]:
print( exp(2) ) # por default, a potência é 2
print( exp(2,2) )
print( exp(2,3) )
print( exp(2,4) )
4
4
8
16

3. Classes

O conceito de classe vem do paradigma de programação orientada a objetos. Como o nome diz, este paradigma se baseia na existência de objetos, elementos que são criados durante a execução do programa, que armazenam dados, possuem comportamentos pré-definidos e mantêm estado. Os resultados da execução do programa são obtidos através de várias interações entre objetos distintos.

Classes permitem ao programador definir como cada tipo de objeto deve armazenar dados, bem como quais procedimentos é capaz de realizar. A ideia é que objetos são criados (ou instanciados) com base em classes definidas pelo programador. Elas especificam as instruções sobre como construir os objetos de determinado tipo através de atributos e métodos:

Atributos definem os dados que pertencem a objetos de determinado tipo. Embora os atributos sejam os mesmos para todos os objetos do mesmo tipo, os valores de cada atributo são diferentes para cada objeto individual (também referido como instância).

Métodos definem os comportamentos de objetos de determinado tipo. Podem fornecer informação sobre o objeto (métodos "getter"), ou atualizar seus atributos (métodos "setter").

Construindo objetos

Objetos são instanciados segundo o padrão especificado da classe. Um método especial, chamado construtor (em python, seu nome é __init__), é o primeiro a ser executado durante a instanciação, e guarda as instruções para a construção do objeto. Nele são definidos os valores para cada um dos atributos.

Vamos definir uma nova classe, chamada Vehicle, que especifica objetos que representam veículos. O construtor __init__ é o único método que deve ser necessariamente implementado na definição de qualquer classe. Por enquanto a classe Vehicle possui apenas quatro atributos (type, model, manufacturer e seat_capacity), e não possui métodos

In [40]:
class Vehicle:
    
    def __init__(self, type, model, manufacturer, seat_capacity):
        self.type = type
        self.model = model
        self.manufacturer = manufacturer
        self.seat_capacity = seat_capacity

Obs. a keyword self deve sempre ser o primeiro parâmetro de qualquer método definido dentro de uma classe. Ela se refere ao objeto sendo instanciado, e precisa ser usada sempre que queremos resgatar ou atribuir valores aos atributos da instância.

Agora vamos criar dois veículos: um avião e um carro, ambos instâncias de Vehicle. Os objetos serão atribuídos às variáveis myPlane e myCar, respectivamente, para que possam ser referenciados posteriormente. Como as instâncias são diferentes umas das outras, passamos para o construtor de cada uma os valores que devem ser estabelecidos como atributos, na forma de argumentos.

In [41]:
myPlane = Vehicle('plane', "A320", "Airbus", 180)
myCar = Vehicle('car', model="Uno", manufacturer="Fiat", seat_capacity=5)

Usamos a notação de ponto (.) para acessar atributos e métodos de objetos. Podemos consultar os valores dos atributos

In [42]:
print( myPlane.type )
print( myPlane.manufacturer )
print( myPlane.model )
print( myPlane.seat_capacity )
plane
Airbus
A320
180
In [43]:
print( myCar.type )
print( myCar.manufacturer )
print( myCar.model )
print( myCar.seat_capacity )
car
Fiat
Uno
5

ou alterar diretamente os valores dos atributos (embora esta abordagem não seja recomendada)

In [44]:
myPlane.model = 'a321'
In [45]:
myPlane.model
Out[45]:
'a321'

Consultando dados do objeto com métodos "getter"

Em programação orientada a objetos, uma boa prática é prevenir o acesso direto a atributos do objeto, de forma a manter o encapsulamento de dados. Assim, os atributos do objeto tornam-se privados, sendo apenas visíveis pelo próprio objeto. Uma vantagem direta desta abordagem é que assim evitamos que nosso código modifique os atributos inadvertidamente. Outra vantagem é que adicionamos uma camada de abstração ao objeto: não importa muito como a informação é representada internamente ao objeto, podemos processá-la antes de externalizá-la.

Vamos então adicionar dois métodos getter à nossa classe Vehicle

In [46]:
class Vehicle:
    
    def __init__(self, type, model, manufacturer, seat_capacity):
        self.type = type
        self.model = model
        self.manufacturer = manufacturer
        self.seat_capacity = seat_capacity
        
    # Getter methods
    
    def getModel(self):
        return f"{self.manufacturer} {self.model}"
        
    def getSeatCapacity(self):
        return self.seat_capacity

O método getModel retorna o modelo do veículo, após o nome do fabricante. O método getNumPassengers simplesmente retorna o numero de passageiros que o veículo comporta. Note que decidimos não externalizar o tipo (atributo type) do veículo

In [47]:
myPlane = Vehicle('plane', "A320", "Airbus", 180)
myCar = Vehicle('car', model="Uno", manufacturer="Fiat", seat_capacity=5)
In [48]:
print( myPlane.getModel() )
print( f"Cabem {myPlane.getSeatCapacity()} passageiros no {myPlane.getModel()}" )
Airbus A320
Cabem 180 passageiros no Airbus A320
In [49]:
print( myCar.getModel() )
print( f"Cabem {myCar.getSeatCapacity()} passageiros no {myCar.getModel()}" )
Fiat Uno
Cabem 5 passageiros no Fiat Uno

Atualizando dados do objeto com métodos "setter"

Frequentemente precisamos atualizar valores de atributos de objetos. Vamos adicionar à classe Vehicle a funcionalidade de embarcar e desembarcar passageiros.

Para isso, faremos algumas modificações:

  1. Adicionamos um atributo n_passengers_onboard, que armazena o número de passageiros a bordo. Este atributo é inicializado com o valor zero. Ou seja, no momento em que o objeto é criado não há nenhum passageiro embarcado;

  2. Adicionamos mais dois métodos "getter": getNumPassengersOnboard retorna o número de passageiros que se encontram a bordo do veículo; e getNumSeatsAvailable retorna o número de assentos ainda disponíveis;

  3. Adicionamos três métodos "setter": removeAllPassengers remove todos os passageiros a bordo; embarkPassengers adiciona $n$ passageiros ao veículo, desde que haja espaço; removePassengers remove $n$ passageiros do veículo.

In [50]:
class Vehicle:
    
    def __init__(self, type, model, manufacturer, seat_capacity):
        self.type = type
        self.model = model
        self.manufacturer = manufacturer
        self.seat_capacity = seat_capacity
        
        self.n_passengers_onboard = 0
        
    # Getter methods
    
    def getModel(self):
        return f"{self.manufacturer} {self.model}"
        
    def getSeatCapacity(self):
        return self.seat_capacity
    
    def getNumPassengersOnboard(self):
        return self.n_passengers_onboard
    
    def getNumSeatsAvailable(self):
        return self.seat_capacity - self.n_passengers_onboard
    
    # Setter methods
    
    def removeAllPassengers(self):
        self.n_passengers_onboard = 0
        
    def embarkPassengers(self, n_passengers_to_embark):
        
        available_seats = self.getNumSeatsAvailable()
        
        if available_seats >= n_passengers_to_embark:
            self.n_passengers_onboard += n_passengers_to_embark
            return f"Mais {n_passengers_to_embark} passageiros embarcados com sucesso!"
        
        else:
            return f"Impossível embarcar mais {n_passengers_to_embark} passageiros! Apenas {available_seats} assentos disponíveis."
        
    def removePassengers(self, n_passengers_to_remove):
        
        if n_passengers_to_remove <= self.n_passengers_onboard:
            self.n_passengers_onboard -= n_passengers_to_remove
            return f"Desembarcaram {n_passengers_to_remove} passageiros"
        
        elif self.n_passengers_onboard > 0:
            n_passengers_remaining = self.n_passengers_onboard
            self.removeAllPassengers()
            return f"Havia apenas {n_passengers_remaining} embarcados! Todos desembarcaram."
        
        else:
            return "Nenhum passageiro a bordo"

Vamos então recriar o avião e o carro, embarcar e desembarcar passageiros

In [51]:
myPlane = Vehicle('plane', "A320", "Airbus", 180)
myCar = Vehicle('car', "Uno", "Fiat", 5)

No avião:

In [52]:
print( myPlane.embarkPassengers(50) )
print( f"{myPlane.getNumPassengersOnboard()} passageiros a bordo" )
print( f"{myPlane.getNumSeatsAvailable()} assentos disponíveis" )
Mais 50 passageiros embarcados com sucesso!
50 passageiros a bordo
130 assentos disponíveis
In [53]:
print(myPlane.removePassengers(5))
print( f"{myPlane.getNumPassengersOnboard()} passageiros a bordo" )
print( f"{myPlane.getNumSeatsAvailable()} assentos disponíveis" )
Desembarcaram 5 passageiros
45 passageiros a bordo
135 assentos disponíveis

No carro:

In [54]:
print( myCar.embarkPassengers(5) )
print( f"{myCar.getNumPassengersOnboard()} passageiros a bordo" )
print( f"{myCar.getNumSeatsAvailable()} assentos disponíveis" )
Mais 5 passageiros embarcados com sucesso!
5 passageiros a bordo
0 assentos disponíveis
In [55]:
print(myCar.removePassengers(5))
print( f"{myCar.getNumPassengersOnboard()} passageiros a bordo" )
print( f"{myCar.getNumSeatsAvailable()} assentos disponíveis" )
Desembarcaram 5 passageiros
0 passageiros a bordo
5 assentos disponíveis

Herança

Um conceito muito importante na programação orientada a objetos é a herança (inheritance). Subclasses podem "herdar" comportamentos e atributos de sua superclasse, ou classe-pai.

Vamos criar duas novas subclasses, Airplane e Car, que são veículos mais "específicos". Elas extendem a superclasse Vehicle, que é mais "genérica". Alguns pontos importantes:

Construtores

As subclasses têm seus próprios métodos construtores (__init__), que não usam o argumento type. Isso porque o tipo da classe Airplane sempre será 'plane', enquanto o tipo da classe Car sempre será 'car'. Este atributo, no entanto, é passado para o construtor da superclasse (super().__init__). Além disso, a classe Airplane espera um novo argumento airline, que guarda o nome da companhia aérea e, portanto, é um atributo exclusivo de aeronaves

Novos métodos e atributos

Subclasses podem ter métodos e atributos que não existiam em sua superclasse. No nosso exemplo, a classe Airplane possui o método takeoff, enquanto a classe Car possui o método drive. Não faria sentido um método chamado takeoff na classe Vehicle, pois nem todos os veículos podem decolar. O atributo airline, também exclusivo da classe Airplane, também não faria sentido para outros veículos como carros ou barcos.

Substituição (overriding)

Um subclasse pode substituir o comportamento herdado de sua superclasse, através de um mecanismo chamado overriding (substituição, em português). Em Python, isso pode ser feito simplesmente reimplementando o método da superclasse. Na classe Car, por exemplo, o método embarkPassengers foi substituído, sendo adicionado o parâmetro name_driver, que deve receber o nome do motorista. Este comportamento portanto ocorre apenas em carros, e não substitui o comportamento do mesmo método na classe Airplane nem na classe Vehicle.

In [56]:
class Airplane(Vehicle):
    def __init__(self, model, manufacturer, airline, seat_capacity):
        super().__init__(type='plane',model=model,manufacturer=manufacturer,seat_capacity=seat_capacity)
        self.airline = airline
        
    def takeoff(self):
        print(f"O {self.getModel()} da {self.airline} está decolando com {self.getNumPassengersOnboard()} passageiros")
        
        
        
        
        
class Car(Vehicle):
    def __init__(self, model, manufacturer, seat_capacity):
        super().__init__(type='car',model=model,manufacturer=manufacturer,seat_capacity=seat_capacity)
        
    def embarkPassengers(self, n_passengers_to_embark, name_driver):

            available_seats = self.getNumSeatsAvailable()
            self.name_driver = name_driver

            if available_seats >= n_passengers_to_embark:
                self.n_passengers_onboard += n_passengers_to_embark
                return f"Mais {n_passengers_to_embark} passageiros embarcados com sucesso!"

            else:
                return f"Impossível embarcar mais {n_passengers_to_embark} passageiros! Apenas {available_seats} assentos disponíveis."

        
    def drive(self):
        if self.getNumPassengersOnboard()>0:
            print(f"O {self.getModel()}, conduzido por {self.name_driver}, está na estrada com {self.getNumPassengersOnboard()} passageiros")

        else:
            print("Um carro não pode operar sem motorista!")

Vamos agora testar as subclasses

In [57]:
latamPlane = Airplane('A320','Airbus', "Latam", 180)

print(latamPlane.embarkPassengers(130))
latamPlane.takeoff()
Mais 130 passageiros embarcados com sucesso!
O Airbus A320 da Latam está decolando com 130 passageiros
In [58]:
azulPlane = Airplane('ERJ190','Embraer', "Azul", 100)

print(azulPlane.embarkPassengers(70))
azulPlane.takeoff()
Mais 70 passageiros embarcados com sucesso!
O Embraer ERJ190 da Azul está decolando com 70 passageiros
In [59]:
myCar = Car('Uno','Fiat', 5)

print(myCar.embarkPassengers(3, name_driver="Sérgio"))
myCar.drive()
Mais 3 passageiros embarcados com sucesso!
O Fiat Uno, conduzido por Sérgio, está na estrada com 3 passageiros