4.3 Herança

Um mecanismo fundamental em sistemas orientados a objetos modernos é herança: uma maneira de derivar classes novas a partir da definição de classes existentes, denominadas neste contexto classes-base. As classes derivadas possuem acesso transparente aos atributos e métodos das classes base, e podem redefinir estes conforme conveniente.

Herança é uma forma simples de promover reuso através de uma generalização: desenvolve-se uma classe-base com funcionalidade genérica, aplicável em diversas situações, e definem-se subclasses concretas, que atendam a situações específicas.

Classes Python suportam herança simples e herança múltipla. Os exemplos até agora evitaram o uso de herança, mas nesta seção é possível apresentar a sintaxe geral para definição de uma classe:

    class nome-classe(base1, base2, ..., basen):
        atributo-1 = valor-1
        .
        .
        atributo-n = valor-n

        def nome-método-1(self, arg1, arg2, ..., argn):
            # bloco de código do método
        .
        .
        def nome-método-n(self, arg1, arg2, ..., argn):
            # bloco de código do método

Como pode ser observado acima, classes base são especificadas entre parênteses após o nome da classe sendo definida. Na sua forma mais simples:

    class Foo:
        a = 1
        def cheese(self):
            print "cheese"

        def foo(self):
            print "foo"

    class Bar(Foo):
        def bar(self):
            print "bar"

        def foo(self):              # método redefinido 
            print "foo de bar!"

uma instância da classe Bar tem acesso aos métodos cheese(), bar() e foo(), este último sendo redefinido localmente:

    >>> b = Bar()
    >>> b.cheese()
    cheese
    >>> b.foo()         # saída demonstra método redefinido
    foo de bar!         # em Bar
    >>> b.bar()
    foo
    >>> print b.a       # acesso transparente ao atributo
    1                   # definido em Foo

enquanto uma instância da classe Foo tem acesso apenas às funções definidas nela, foo() e cheese:

    >>> f = Foo()
    >>> f.foo()
    foo

4.3.0.1 Invocando métodos de classes-base

Para acessar os métodos de uma classe-base, usamos uma construção diferente para invocá-los, que permite especificar qual classe armazena o método sendo chamado. Seguindo o exemplo, vamos novamente a redefinir o método Bar.foo():

    class Bar(Foo):
        # ...
        def foo(self):
            Foo.foo(self)
            print "foo de bar!"

Nesta versão, o método foo() inclui uma chamada ao método Foo.foo(), que conforme indicado pelo seu nome, é uma referência direta ao método da classe base. Ao instanciar um objeto desta classe:

    >>> b = Bar()
    >>> b.foo()
    foo
    foo de bar!

pode-se observar que são executados ambos os métodos especificados. Este padrão, aqui demonstrado de forma muito simples, pode ser utilizado em situações mais elaboradas; seu uso mais freqüente é para invocar, a partir de um construtor de uma classe, o construtor das suas classes-base.

4.3.0.2 Funções Úteis

Há duas funções particularmente úteis para estudar uma hierarquia de classes e instâncias:

4.3.0.3 Atributos de classe versus atributos de instância

Uma particularidade em Python, que deriva da forma transparente como variáveis são acessadas, é a distinção entre atributos definidos em uma classe, e atributos definidos em uma instância desta classe. Observe o código a seguir:

    class Foo:
        a = 1

A classe acima define uma variável a com o valor 1. Ao instanciar esta classe,

    >>> f = Foo()
    >>> print f.a
    1

observa-se que a variável parece estar definida na instância. Esta observação convida a algumas indagações:

As respostas para estas perguntas são todas relacionadas a um mecanismo central em Python, que é o protocolo getattr. Este protocolo dita como atributos são transparentemente localizados em uma hierarquia de classes e suas instâncias, e segue a seguinte receita:

  1. Ao acessar um atributo de uma instância (por meio de uma variável qualquer ou self) o interpretador tenta localizar o atributo no estado da instância.
  2. Caso não seja localizado, busca-se o atributo na classe da instância em questão. Por sinal, este passo é o que permite que métodos de uma classe sejam acessíveis a partir de suas instâncias.
  3. Caso não seja localizado, busca-se o atributo entre as classes base definidas para a classe da instância.
  4. Ao atribuir uma variável em uma instância, este atributo é sempre definido no estado local da instância.

Uma vez compreendido este mecanismo, é possível elucidar respostas para as questões acima. No exemplo, a variável a está definida na classe Foo, e pelo ponto 2 acima descrito, é acessível como se fosse definida pela própria instância. Ao atribuir um valor novo a f.a, estamos definindo uma nova variável a no estado local da variável f, o que não tem nenhum impacto sobre a variável a definida em Foo, nem sobre novas instâncias criadas a partir desta.

Se o descrito acima parece confuso, não se preocupe; o mecanismo normalmente funciona exatamente da maneira que se esperaria de uma linguagem orientada a objetos. Existe apenas uma situação `perigosa', que ocorre quando usamos atributos de classe com valores mutáveis, como listas e dicionários.

    class Foo:
        a = [1,2]

Nesta situação, quando criamos uma instância a partir de Foo, a variável a pode ser alterada por meio desta instância. Como não foi realizada atribuição, a regra 4 descrita acima não se aplica:

    >>> f = Foo()
    >>> f.a.append(3)
    >>> g = Foo()
    >>> print g.a
    [1, 2, 3]

e a variável da classe é de fato modificada. Esta particularidade é freqüentemente fonte de bugs difíceis de localizar, e por este motivo se recomenda fortemente que não se utilize variáveis de tipos mutáveis em classes.


Subsecções