Programas Extensíveis Através de Herança

Beginner

This tutorial is from open-source community. Access the source code

Introdução

A herança (inheritance) é uma ferramenta comumente utilizada para escrever programas extensíveis. Esta seção explora essa ideia.

Herança (Inheritance)

A herança (inheritance) é utilizada para especializar objetos existentes:

class Parent:
    ...

class Child(Parent):
    ...

A nova classe Child é chamada de classe derivada (derived class) ou subclasse (subclass). A classe Parent é conhecida como classe base (base class) ou superclasse (superclass). Parent é especificado em () após o nome da classe, class Child(Parent):.

Extensão (Extending)

Com a herança (inheritance), você está pegando uma classe existente e:

  • Adicionando novos métodos
  • Redefinindo alguns dos métodos existentes
  • Adicionando novos atributos às instâncias

No final, você está estendendo o código existente.

Exemplo (Example)

Suponha que esta é a sua classe inicial:

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    def cost(self):
        return self.shares * self.price

    def sell(self, nshares):
        self.shares -= nshares

Você pode alterar qualquer parte disso via herança (inheritance).

Adicionar um novo método (Add a new method)

class MyStock(Stock):
    def panic(self):
        self.sell(self.shares)

Exemplo de uso (Usage example).

>>> s = MyStock('GOOG', 100, 490.1)
>>> s.sell(25)
>>> s.shares
75
>>> s.panic()
>>> s.shares
0
>>>

Redefinindo um método existente (Redefining an existing method)

class MyStock(Stock):
    def cost(self):
        return 1.25 * self.shares * self.price

Exemplo de uso (Usage example).

>>> s = MyStock('GOOG', 100, 490.1)
>>> s.cost()
61262.5
>>>

O novo método substitui o antigo. Os outros métodos não são afetados. É tremendo.

Sobrescrita (Overriding)

Às vezes, uma classe estende um método existente, mas deseja usar a implementação original dentro da redefinição. Para isso, use super():

class Stock:
    ...
    def cost(self):
        return self.shares * self.price
    ...

class MyStock(Stock):
    def cost(self):
        ## Check the call to `super`
        actual_cost = super().cost()
        return 1.25 * actual_cost

Use super() para chamar a versão anterior.

Atenção: No Python 2, a sintaxe era mais verbosa.

actual_cost = super(MyStock, self).cost()

__init__ e herança (inheritance)

Se __init__ for redefinido, é essencial inicializar o pai.

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

class MyStock(Stock):
    def __init__(self, name, shares, price, factor):
        ## Check the call to `super` and `__init__`
        super().__init__(name, shares, price)
        self.factor = factor

    def cost(self):
        return self.factor * super().cost()

Você deve chamar o método __init__() no super, que é a maneira de chamar a versão anterior, como mostrado anteriormente.

Usando Herança (Inheritance)

A herança é, por vezes, usada para organizar objetos relacionados.

class Shape:
    ...

class Circle(Shape):
    ...

class Rectangle(Shape):
    ...

Pense em uma hierarquia lógica ou taxonomia. No entanto, um uso mais comum (e prático) está relacionado à criação de código reutilizável ou extensível. Por exemplo, um framework pode definir uma classe base e instruí-lo a personalizá-la.

class CustomHandler(TCPHandler):
    def handle_request(self):
        ...
        ## Custom processing

A classe base contém algum código de uso geral. Sua classe herda e personaliza partes específicas.

Relação "é um" ("is a")

A herança estabelece uma relação de tipo.

class Shape:
    ...

class Circle(Shape):
    ...

Verifique a instância do objeto.

>>> c = Circle(4.0)
>>> isinstance(c, Shape)
True
>>>

Importante: Idealmente, qualquer código que funcionasse com instâncias da classe pai também funcionará com instâncias da classe filha.

Classe base object

Se uma classe não tem pai, você às vezes vê object usado como a base.

class Shape(object):
    ...

object é o pai de todos os objetos em Python.

*Nota: tecnicamente não é obrigatório, mas você frequentemente o vê especificado como uma herança de seu uso obrigatório no Python 2. Se omitido, a classe ainda herda implicitamente de object.

Herança Múltipla

Você pode herdar de múltiplas classes, especificando-as na definição da classe.

class Mother:
    ...

class Father:
    ...

class Child(Mother, Father):
    ...

A classe Child herda características de ambos os pais. Existem alguns detalhes bastante complicados. Não faça isso a menos que saiba o que está fazendo. Algumas informações adicionais serão fornecidas na próxima seção, mas não vamos utilizar herança múltipla neste curso.

Um uso importante da herança é na escrita de código que se destina a ser estendido ou personalizado de várias maneiras - especialmente em bibliotecas ou frameworks. Para ilustrar, considere a função print_report() em seu programa report.py. Ela deve se parecer com isto:

def print_report(reportdata):
    '''
    Print a nicely formatted table from a list of (name, shares, price, change) tuples.
    '''
    headers = ('Name','Shares','Price','Change')
    print('%10s %10s %10s %10s' % headers)
    print(('-'*10 + ' ')*len(headers))
    for row in reportdata:
        print('%10s %10d %10.2f %10.2f' % row)

Quando você executar seu programa de relatório, você deve obter uma saída como esta:

>>> import report
>>> report.portfolio_report('portfolio.csv', 'prices.csv')
      Name     Shares      Price     Change
---------- ---------- ---------- ----------
        AA        100       9.22     -22.98
       IBM         50     106.28      15.18
       CAT        150      35.46     -47.98
      MSFT        200      20.89     -30.34
        GE         95      13.48     -26.89
      MSFT         50      20.89     -44.21
       IBM        100     106.28      35.84

Exercício 4.5: Um Problema de Extensibilidade

Suponha que você quisesse modificar a função print_report() para suportar uma variedade de formatos de saída diferentes, como texto simples, HTML, CSV ou XML. Para fazer isso, você poderia tentar escrever uma função gigantesca que fizesse tudo. No entanto, fazê-lo provavelmente levaria a uma bagunça não sustentável. Em vez disso, esta é uma oportunidade perfeita para usar a herança.

Para começar, concentre-se nas etapas envolvidas na criação de uma tabela. No topo da tabela, há um conjunto de cabeçalhos de tabela. Depois disso, aparecem linhas de dados da tabela. Vamos pegar essas etapas e colocá-las em sua própria classe. Crie um arquivo chamado tableformat.py e defina a seguinte classe:

## tableformat.py

class TableFormatter:
    def headings(self, headers):
        '''
        Emit the table headings.
        '''
        raise NotImplementedError()

    def row(self, rowdata):
        '''
        Emit a single row of table data.
        '''
        raise NotImplementedError()

Esta classe não faz nada, mas serve como uma espécie de especificação de design para classes adicionais que serão definidas em breve. Uma classe como esta é, por vezes, chamada de "classe base abstrata".

Modifique a função print_report() para que ela aceite um objeto TableFormatter como entrada e invoque métodos nele para produzir a saída. Por exemplo, assim:

## report.py
...

def print_report(reportdata, formatter):
    '''
    Print a nicely formatted table from a list of (name, shares, price, change) tuples.
    '''
    formatter.headings(['Name','Shares','Price','Change'])
    for name, shares, price, change in reportdata:
        rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ]
        formatter.row(rowdata)

Como você adicionou um argumento a print_report(), você também precisará modificar a função portfolio_report(). Altere-a para que ela crie um TableFormatter assim:

## report.py

import tableformat

...
def portfolio_report(portfoliofile, pricefile):
    '''
    Make a stock report given portfolio and price data files.
    '''
    ## Read data files
    portfolio = read_portfolio(portfoliofile)
    prices = read_prices(pricefile)

    ## Create the report data
    report = make_report_data(portfolio, prices)

    ## Print it out
    formatter = tableformat.TableFormatter()
    print_report(report, formatter)

Execute este novo código:

>>> ================================ RESTART ================================
>>> import report
>>> report.portfolio_report('portfolio.csv', 'prices.csv')
... crashes ...

Ele deve travar imediatamente com uma exceção NotImplementedError. Isso não é muito emocionante, mas é exatamente o que esperávamos. Continue para a próxima parte.

Exercício 4.6: Usando Herança para Produzir Saídas Diferentes

A classe TableFormatter que você definiu na parte (a) destina-se a ser estendida via herança. Na verdade, essa é a ideia principal. Para ilustrar, defina uma classe TextTableFormatter assim:

## tableformat.py
...
class TextTableFormatter(TableFormatter):
    '''
    Emit a table in plain-text format
    '''
    def headings(self, headers):
        for h in headers:
            print(f'{h:>10s}', end=' ')
        print()
        print(('-'*10 + ' ')*len(headers))

    def row(self, rowdata):
        for d in rowdata:
            print(f'{d:>10s}', end=' ')
        print()

Modifique a função portfolio_report() assim e experimente:

## report.py
...
def portfolio_report(portfoliofile, pricefile):
    '''
    Make a stock report given portfolio and price data files.
    '''
    ## Read data files
    portfolio = read_portfolio(portfoliofile)
    prices = read_prices(pricefile)

    ## Create the report data
    report = make_report_data(portfolio, prices)

    ## Print it out
    formatter = tableformat.TextTableFormatter()
    print_report(report, formatter)

Isso deve produzir a mesma saída de antes:

>>> ================================ RESTART ================================
>>> import report
>>> report.portfolio_report('portfolio.csv', 'prices.csv')
      Name     Shares      Price     Change
---------- ---------- ---------- ----------
        AA        100       9.22     -22.98
       IBM         50     106.28      15.18
       CAT        150      35.46     -47.98
      MSFT        200      20.89     -30.34
        GE         95      13.48     -26.89
      MSFT         50      20.89     -44.21
       IBM        100     106.28      35.84
>>>

No entanto, vamos mudar a saída para outra coisa. Defina uma nova classe CSVTableFormatter que produz saída em formato CSV:

## tableformat.py
...
class CSVTableFormatter(TableFormatter):
    '''
    Output portfolio data in CSV format.
    '''
    def headings(self, headers):
        print(','.join(headers))

    def row(self, rowdata):
        print(','.join(rowdata))

Modifique seu programa principal da seguinte forma:

def portfolio_report(portfoliofile, pricefile):
    '''
    Make a stock report given portfolio and price data files.
    '''
    ## Read data files
    portfolio = read_portfolio(portfoliofile)
    prices = read_prices(pricefile)

    ## Create the report data
    report = make_report_data(portfolio, prices)

    ## Print it out
    formatter = tableformat.CSVTableFormatter()
    print_report(report, formatter)

Você deve ver agora a saída CSV assim:

>>> ================================ RESTART ================================
>>> import report
>>> report.portfolio_report('portfolio.csv', 'prices.csv')
Name,Shares,Price,Change
AA,100,9.22,-22.98
IBM,50,106.28,15.18
CAT,150,35.46,-47.98
MSFT,200,20.89,-30.34
GE,95,13.48,-26.89
MSFT,50,20.89,-44.21
IBM,100,106.28,35.84

Usando uma ideia semelhante, defina uma classe HTMLTableFormatter que produz uma tabela com a seguinte saída:

<tr><th>Name</th><th>Shares</th><th>Price</th><th>Change</th></tr>
<tr><td>AA</td><td>100</td><td>9.22</td><td>-22.98</td></tr>
<tr><td>IBM</td><td>50</td><td>106.28</td><td>15.18</td></tr>
<tr><td>CAT</td><td>150</td><td>35.46</td><td>-47.98</td></tr>
<tr><td>MSFT</td><td>200</td><td>20.89</td><td>-30.34</td></tr>
<tr><td>GE</td><td>95</td><td>13.48</td><td>-26.89</td></tr>
<tr><td>MSFT</td><td>50</td><td>20.89</td><td>-44.21</td></tr>
<tr><td>IBM</td><td>100</td><td>106.28</td><td>35.84</td></tr>

Teste seu código modificando o programa principal para criar um objeto HTMLTableFormatter em vez de um objeto CSVTableFormatter.

Exercício 4.7: Polimorfismo em Ação

Uma característica importante da programação orientada a objetos é que você pode conectar um objeto em um programa e ele funcionará sem ter que alterar nenhum dos códigos existentes. Por exemplo, se você escreveu um programa que esperava usar um objeto TableFormatter, ele funcionaria, independentemente do tipo de TableFormatter que você realmente fornecesse. Esse comportamento é, por vezes, referido como "polimorfismo".

Um problema potencial é descobrir como permitir que um usuário escolha o formatador que deseja. O uso direto dos nomes das classes, como TextTableFormatter, é frequentemente irritante. Assim, você pode considerar alguma abordagem simplificada. Talvez você incorpore uma instrução if- no código assim:

def portfolio_report(portfoliofile, pricefile, fmt='txt'):
    '''
    Make a stock report given portfolio and price data files.
    '''
    ## Read data files
    portfolio = read_portfolio(portfoliofile)
    prices = read_prices(pricefile)

    ## Create the report data
    report = make_report_data(portfolio, prices)

    ## Print it out
    if fmt == 'txt':
        formatter = tableformat.TextTableFormatter()
    elif fmt == 'csv':
        formatter = tableformat.CSVTableFormatter()
    elif fmt == 'html':
        formatter = tableformat.HTMLTableFormatter()
    else:
        raise RuntimeError(f'Unknown format {fmt}')
    print_report(report, formatter)

Neste código, o usuário especifica um nome simplificado como 'txt' ou 'csv' para escolher um formato. No entanto, colocar uma grande instrução if- na função portfolio_report() dessa forma é a melhor ideia? Pode ser melhor mover esse código para uma função de uso geral em outro lugar.

No arquivo tableformat.py, adicione uma função create_formatter(name) que permita a um usuário criar um formatador, dado um nome de saída como 'txt', 'csv' ou 'html'. Modifique portfolio_report() para que fique assim:

def portfolio_report(portfoliofile, pricefile, fmt='txt'):
    '''
    Make a stock report given portfolio and price data files.
    '''
    ## Read data files
    portfolio = read_portfolio(portfoliofile)
    prices = read_prices(pricefile)

    ## Create the report data
    report = make_report_data(portfolio, prices)

    ## Print it out
    formatter = tableformat.create_formatter(fmt)
    print_report(report, formatter)

Tente chamar a função com diferentes formatos para garantir que ela esteja funcionando.

Exercício 4.8: Juntando tudo

Modifique o programa report.py para que a função portfolio_report() aceite um argumento opcional que especifica o formato de saída. Por exemplo:

>>> report.portfolio_report('portfolio.csv', 'prices.csv', 'txt')
      Name     Shares      Price     Change
---------- ---------- ---------- ----------
        AA        100       9.22     -22.98
       IBM         50     106.28      15.18
       CAT        150      35.46     -47.98
      MSFT        200      20.89     -30.34
        GE         95      13.48     -26.89
      MSFT         50      20.89     -44.21
       IBM        100     106.28      35.84
>>>

Modifique o programa principal para que um formato possa ser fornecido na linha de comando:

$ python3 report.py portfolio.csv prices.csv csv
Name,Shares,Price,Change
AA,100,9.22,-22.98
IBM,50,106.28,15.18
CAT,150,35.46,-47.98
MSFT,200,20.89,-30.34
GE,95,13.48,-26.89
MSFT,50,20.89,-44.21
IBM,100,106.28,35.84
$

Discussão

Escrever código extensível é um dos usos mais comuns de herança em bibliotecas e frameworks. Por exemplo, um framework pode instruí-lo a definir seu próprio objeto que herda de uma classe base fornecida. Em seguida, você é instruído a preencher vários métodos que implementam vários pedaços de funcionalidade.

Outro conceito um tanto mais profundo é a ideia de "possuir suas abstrações". Nos exercícios, definimos nossa própria classe para formatar uma tabela. Você pode olhar para seu código e dizer a si mesmo "Eu deveria apenas usar uma biblioteca de formatação ou algo que outra pessoa já fez em vez disso!" Não, você deve usar AMBOS, sua classe e uma biblioteca. Usar sua própria classe promove o acoplamento fraco e é mais flexível. Contanto que seu aplicativo use a interface de programação da sua classe, você pode alterar a implementação interna para funcionar da maneira que desejar. Você pode escrever código totalmente personalizado. Você pode usar um pacote de terceiros. Você troca um pacote de terceiros por um pacote diferente quando encontra um melhor. Não importa - nenhum dos códigos do seu aplicativo será interrompido, desde que você preserve a interface. Essa é uma ideia poderosa e é uma das razões pelas quais você pode considerar a herança para algo como isso.

Dito isso, projetar programas orientados a objetos pode ser extremamente difícil. Para mais informações, você provavelmente deve procurar livros sobre o tópico de padrões de projeto (embora entender o que aconteceu neste exercício o levará bastante longe em termos de uso de objetos de uma forma praticamente útil).

Resumo

Parabéns! Você concluiu o laboratório de Herança. Você pode praticar mais laboratórios no LabEx para aprimorar suas habilidades.