Classes New-Style

Classes New-Style

Author: Michael Fötsch (tradução de Walter Rodrigo de Sá Cruz e revisão por Rodrigo Cacilhas)
Date: Wed Apr 18 11:48:20 2007

Tradução do artigo em http://www.geocities.com/foetsch/python/new_style_classes.htm

Porque Classes New-Style?

Classes new-style são parte de um esforço de unificar os tipos built-in e as classes definidas pelos usuários na linguagem de programação Python. Classes new-style estão aí desde o Python 2.2 (não são tão novas assim), então, é hora de tirar vantagem das novas possibilidades.

Uma classe new-style é derivada, direta ou indiretamente, de um tipo built-in (Algo que não era possível antes do Python 2.2.). Os tipos built-in incluem tipos como:

  • int
  • list
  • tuple
  • dict
  • str
  • e outros

A classe base para classes todas as classes new-style é chamada object. Todos os exemplos a seguir são new-style classes:

class NewStyleUserDefinedClass(object):
    pass


class DerivedFromBuiltInType(list):
    pass


class IndirectlyDerivedFromType(DerivedFromBuiltInType):
    pass

Classes new-style oferecem:

  • Propriedades: atributos que são definidos por métodos get/set
  • Métodos estáticos e métodos de classe
  • O novo gancho __getattribute__, que, ao contrário do __getattr__, é chamado para cada acesso de atributo, não apenas quando o atributo não pode ser encontrado na instância.
  • Descritores: Um protocolo para definir o comportamento de acesso de atributos entre objetos
  • Sobrescrita do construtor __new__
  • Metaclasses (não discutidas aqui)

Tentarei ser breve, porém dando a você informação o suficiente para que você use essas características da linguagem. No lugar de longas descrições, exemplos práticos. Uma vez seu interesse seja despertado, você pode verificar a seção Referências para materiais mais detalhados sobre esse tópico.

Propriedades

Uma propriedade é um atributo que é definida por métodos get/set. O conceito é simples e bem conhecido em outras linguagens. Uma propriedade é definida assim:

class ClassWithProperty(object):
    ...
    TheProperty = property(fget=<the get method>,
                           fset=<the set method>,
                           fdel=<the set method>,
                           doc=<the docstring>)

A assinatura do descritor da propriedade é property(fget=None, fset=None, fdel=None, doc=None). Se nenhum dos métodos é especificado, uma exceção do tipo AttributeError é lançada quando é tentada a respectiva operação. Por exemplo, para definir uma propriedade como somente leitura, você deve especificar o fget mas não o fset. (Propriedades somente de escrita são possíveis, embora um método regular consiga o mesmo resultado de uma forma menos complexa.)

Aqui tem um exemplo mais completo:

class ClassWithProperty(object):
    def __SetTheProperty(self, value):
        print "Setting the property"
        self.__m_the_property = value

    def __GetTheProperty(self):
        print "Getting the property"
        return self.__m_the_property

    def __DelTheProperty(self):
        print "Deleting the property"
        del self.__m_the_property

    TheProperty = property(fget=__GetTheProperty,
                           fset=__SetTheProperty,
                           fdel=__DelTheProperty,
                           doc="The property description.")

    def __GetReadOnlyProperty(self):
        return "This is a calculated value."

    ReadOnlyProperty = property(fget=__GetReadOnlyProperty)

A propriedade é usada assim:

>>> c = ClassWithProperty()
>>> c.TheProperty = 10
Setting the property
>>> print c.TheProperty
Getting the property
10
>>> del c.TheProperty
Deleting the property
>>> # The property itself is still there after deleting
>>> c.TheProperty = 5
Setting the property
>>> c.TheProperty.__doc__
'The property description.'
>>> print c.ReadOnlyProperty
This is a calculated value.
>>> c.ReadOnlyProperty = 100
Traceback (most recent call last):
  File "<interactive input>", line 1, in ?
AttributeError: can't set attribute

Nota: Não esqueça de derivar sua classe de object, se não as propriedades não funcionarão.

Métodos Estáticos

Não há muito o que explicar sobre métodos estáticos; eles se comportam como no C++. Um método estático é definido usando o descritor staticmethod:

class MyClass(object):
    def SomeMethod(x):
        print x
    SomeMethod = staticmethod(SomeMethod)


>>> MyClass.SomeMethod(15)
15
>>> obj = MyClass()
>>> obj.SomeMethod(15)
15

Você deve realmente considerar a criação de um método estático sempre que o método não faz uso substancial da instância (self).

Métodos de Classe

Um método de classe é similar ao método estático no fato que não tem o argumento self. Porém, ele recebe uma classe como seu primeiro argumento. Por convenção, esse argumento é chamado cls. Um método de classe é definido usando o descritor classmethod:

class MyClass(object):
    def SomeMethod(cls, x):
        print cls, x
    SomeMethod = classmethod(SomeMethod)

class DerivedClass(MyClass):
    pass



>>> MyClass.SomeMethod(15)
__main__.MyClass 15
>>> obj = MyClass()
>>> obj.SomeMethod(15)
__main__.MyClass 15
>>> DerivedClass.SomeMethod(150)
__main__.DerivedClass 15

Na última chamada, você pode ver que é somente a classe envolvida em fazer a chamada de método que define o valor do argumento cls. Isso acontece a despeito do fato que o método foi definido em uma classe diferente.

Descritores

Já vimos três tipos diferentes de descritores:

  • property
  • staticmethod
  • classmethod

Mas o que é exatamente um descritor?

Em geral, um descritor é um atributo de objeto com «comportamento de ligação». É um objeto cujo acesso de atributos foi sobrescrito por métodos no protocolo de descritor. Esses métodos são __get__, __set__, e __delete__. Se algum desses métodos está definido para um objeto, dizemos que ele é um descritor. [1]

Ao executar a atribuição x.m = y, m pode ser um objeto que define um método __set__. Nesse caso, esse método é chamado para executar a atribuição.

O exemplo a seguir mostra como definir cada um dos três métodos do protocolo do descritor:

class MyDescriptor(object):
    def __get__(self, obj, type=None):
        print "get", self, obj, type
        return "The value"

    def __set__(self, obj, value):
        print "set", self, obj, val
        return None

    def __delete__(self, obj):
        print "delete", self, obj
        return None

class SomeClass(object):
    m = MyDescriptor()

Nota: Ambas as classes devem ser derivadas de object.

Agora nós podemos começar a usar o descritor:

>>> x = SomeClass()
>>> print x.m
get <__main__.MyDescriptor object at 0x12345678> <__main__.SomeClass object at 0x23456789> <class '__main__.SomeClass'>
The value
>>> x.m = 1000
set <__main__.MyDescriptor object at 0x12345678> <__main__.SomeClass object at 0x23456789> 1000
>>> del x.m
delete <__main__.MyDescriptor object at 0x12345678><__main__.SomeClass object at 0x23456789>

Aqui está como o método descritor é chamado:

  • Na escrita de um atributo, o método __setattr__ invoca o método __set__ do descritor.
  • Na leitura de um atributo, o método __getattribute__ invoca o método __get__ do descritor.
  • Na remoção de um atributo, o método __delattr__ invoca o método __delete__ do descritor.

Algumas precauções:

  • O descritor e a classe que o utiliza devem ser classes new-style.
  • Ao sobrescrever __setattr__, __getattribute__, e __delattr__, assegure-se de invocar o método herdado (isso é, estender esses métodos, não sobrescrevê-los). Se não, o mecanismo dos descritores irá parar de funcionar.

Nota: No seu HOWTO [2], Raymond Hettinger tem algumas coisas a dizer sobre a diferença entre descitores de dados e descritores que não são de dados:

Se um objeto define __get__ e __set__, ele é considerado como um descritor de dados. Descritores que somente definem o método __get__ são chamados descritores que não são de dados(eles são usados tipicamente para métodos, mas outros usos são possíveis).

Descritores de dados e descritores que não são de dados diferem em como as sobrescritas são calculadas com respeito as entradas no dicionário da instância. Se um dicionário de instância tem uma entrada com o mesmo nome do descritor de dados, o descritor de dados recebe a precedência. Se um dicionário de instância tem uma entrada com o mesmo nome com um descritor que não é de dados, e entrada no dicionário recebe a precedência.

Para criar um descritor somente leitura, defina os métodos __get__ e __set__ com o __set__ lançando uma exceção AttributeError quando chamado. Definir o método __set__ com um lançamento de exceção é suficiante para torná-lo um descritor de dados.

Porém, eu não consegui adicionar uma entrada no dicionário de tal forma que o __get__ de um descritor que não seja de dados não seja chamado. Talvez eu tenha tentado as coisas erradas, talvez eu tenha entedido algo errado. Enquanto isso, vou apenas seguir o conselho de Raymond sobre definir ambos __get__ e __set__ por descritores somente de leitura.

Slots de Atributos

Uma novidade interessante que eu descobri no documento de Guido van Rossum sobre classes new-style [3] são os slots Eis como eles funcionam:

class X(object):
    __slots__ = ["m", "n"]



>>> x = X()
>>> x.m = 10
>>> x.n = 10
>>> x.k = 3
Traceback (most recent call last):
  File "<interactive input>", line 1, in ?
AttributeError: 'X' object has no attribute 'k'

__slots__ reserva espaço para as variáveis listadas diretamente na instância. Classes que definem slots não tem um dicionário de instância (__dict__). Se você tenta atribuir algo para um atributo que não exista __slots__, você recebe um erro. Isso pode ser bem útil para classes semelhantes a estruturas, pois previne problemas com nomes de atributos escritos incorretamente.

O propósito principal parece ser classes que derivam de tipos built-in. Por exemplo, um dicionário derivado pode ter apenas alguns slots para todos os atributos adicionais que ele precisa. Não é necessário ser criado um segundo __dict__ para esses atributos, o que economiza espaço.

Fique alerta: um slots em uma classe derivada esconde um slots com o mesmo nome na classe base.

O construtor __new__

Se você é como eu, então você provavelmente sempre pensou o método __init__ como o equivalente Python do que é chamado de um construtor em C++. Isso porém, não é tudo.

Quando uma instância de uma classe é criada, Python primeiro executa o método __new__ da classe. __new__ é um método estático que é chamado com a classe como primeiro argumento. __new__ retorna uma nova instância da classe.

O método __init__ é chamado logo em seguida para inicializar a instância. Em algumas situações (pensa em pickle/unpicle), nenhuma inicialização é executada. Também, tipos imutáveis como um int ou str são completamente construídos pelo método __new__; seus métodos __init__ não fazem nada. Dessa forma, é impossível tripudear a imutabilidade chamando o método __init__ explicitamente depois da construção.

Super Chamada Cooperativa

Há uma nova forma do Python manipular resolução de métodos em conexão com a herança múltipla. Pessoalmente, eu tento evitar a herança múltipla sempre que possível, então não vou detalhar isso aqui. Porém, o que eu aprendi é que quando alguém mais deriva multiplamente de minhas próprias classes, seria legal se minhas classes executassem o que é chamado de «super chamada cooperativa». Para encurtar, ao invés do antigo //&lt;classe-base&gt;.&lt;método-herdado&gt;//""(self)"", poderíamos usar //super(&lt;a-própria-classe&gt;, self).&lt;método-herdado&gt;//().

class BaseClass:
    def Method(self):
        pass

class DerivedClass:
    def Method(self):
        super(DerivedClass, self).Method()

Parece ser bem fácil.

Conclusão

Vimos como classes new-style podem ser usadas para:

  • Definir propriedades
  • Definir métodos estáticos e de classe
  • Definir descritores
  • Atribuir slots de atributos
  • Sobrescrever o construtor __new__

Eu não mencionei metaclasses. Eu mesmo não tenho usado metaclasses, então é melhor perguntar aos especialistas. Há uma série de links para artigos sobre metaclasses na página de classes new-style no python.org [4].

Eu espero que esse artigo tenha sido útil. Para quaisquer questões, sugestões, ou comentários, sinta-se livre para me mandar um e-mail.

Referências

[1]Unificando tipos e classes no Python 2.2, por Guido Van Rossum http://www.python.org/2.2.3/descrintro.html
[2]Guia How-To para descritores, por Raymond Hettinger http://users.rcn.com/python/download/Descriptor.htm
[3]Classes New-style Classes no python.org. http://www.python.org/doc/newstyle.html
[4]Orientação a Objetos no Python depois do 2.2, por Michael Hudson http://starship.python.net/crew/mwh/hacks/oop-after-python22-1.txt - Algum tipo de slide ASCII, muito conciso

O autor era anteriormente um programador apenas de C++ que amaldiçoou Python por anos por não dispor de nada como a palavra-chave property do Delphi (o autor nunca almodiçoaria C++ por não ter propriedades).

# % if c.boo_box: CD High School Musical Kit High School Musical Brasil CD O Melhor do Musical JM CD High School Musical 3: Ano da Formatura CD High School Musical: the Concert - CD+DVD CD High School Musical - Edi&#231;&#227;o Especial- Duplo