2006-09-13

Metaclasses: singleton

Brincando na aula de PRJ no ISTCC-P, surgiu o assunto singleton (a página em inglês é muuuuito mais completa), que é quando queremos que uma determinada classe tenha somente uma instância, e esta instância seja retornada na tentativa de se criar novas conexões.

Complicado? Que nada! Vou dar o mesmo exemplo que o professor deu: imagine que seu programa tenha uma conexão com um banco de dados. Cada vez que uma função do programa tente criar uma conexão, esta receberá a conexão que já foi criada.

Simples! Em Java isso é feito «bloqueando» o construtor da classe e usando um método para intermediar esta requisição:

class Conexao {
//Atributos
...
private static Conexao inst = null;

//Metodos
private Conexao() {
// O construtor é privado!
...
}

public static Conexao nova() {
// Este método fará as vezes do construtor
if (inst == null)
inst = new Conexao();
return inst;
}
...
}


Então, para criar uma conexão, não será usado new, mas o método nova():

Conexao conn = Conexao.nova();


Funciona. =)

Mas o grande problema aqui é que singleton não é transparente. É preciso estar atento e não usar new, que é a forma natural de se obter uma instância de classe. =P

Outras linguagens já têm uma abordagem diferente, possibilitando a criação de singleton transparente. As duas que comentarei aqui são Python e Ruby.

Ruby



Como Ruby não é meu forte, vou falar desta linguagem primeiro. É extremamente simples fazer singleton em Ruby:

requires 'singleton'

class Conexao
include Singleton
...
end


Bastou um include! Isto é incrivelmente simples, prático, mas tem um pequeno problema pra quem está tentando entender: como é que funciona?

Fica aí a dúvida pros rubyistas explicarem nos comentários. =)

Singleton em Python usando herança



Já Python é outra história. Como sou programador Python (La Batalema Pitonisto, lembram?) posso usar melhor esta linguagem para mostrar como as coisas funcionam.

Uma forma de fazer singleton em Python é usando herança. Assim podemos criar uma classe Singleton que implemente a funcionalidade (feature) desejada e herdar as demais classes desta.

Bem, em Python o construtor é __init__(), mas há um método que é chamado antes do construtor, que é __new__(), e funciona de forma muito parecida com new de Perl. Ele cria a nova instância e a retorna.

Na verdade, em Python quando fazemos:

a = X(b, c)


Por trás o que está acontecendo é:

a = X.__new__(X, b, c)
a.__init__(b, c)


Então podemos sobrescrever este método para termos singleton. A idéia é que, se já houver uma instância, o __new__() retorne esta instância em vez de uma nova:

class Singleton(object):

__inst = None

def __new__(cls, *args, **kw):
if cls.__inst is None:
cls.__inst = object.__new__(cls)
return cls.__inst

def __copy__(self): #(trecho novo de código!)
return self

def __deepcopy__(self, memo=None):
return self


Então temos o atributo privado de classe __inst que contém nada (None) na criação da classe, mas depois será uma referência à instância única.

O método __new__() está preparado para receber argumentos genéricos (*args, **kw), mas não fará nada com eles – é problema do __init__(). A função do __new__() é verificar se já existe a instância única, se não existe, cria, depois retorna ela.

Podemos usar a herança desta forma:

class Conexao(Singleton):
...


Ou seja, funciona exatamente como em Ruby.

Agora vamos à crítica!

O grande problema aqui é justamente a sobrescrita um método geralmente esquecido...

Isso pode gerar uma série de problemas quando surge a herança múltipla ou quando o método é novamente sobrescrito – ou foi também sobrescrito por outra classe pai.

Para contornar estes problemas uma boa saída é o uso de metaclasses.

Singleton em Python usando metaclasse



Em Python classes são objetos! São instâncias de type. Metaclasses são classes filhas de type, portanto suas instâncias também são classes.

Podemos criar uma metaclasse que tenha procedimentos que precedam o construtor e o chamem só se for preciso.

Assim as classes instância da metaclasse podem ser filhas de outras classes e possuir até herança múltipla sem se preocupar com sobrescrita de métodos – pois todo tratamento é realizado num escopo ainda mais protegido.

Numa metaclasse o método que precede o construtor das classes instância é – por motivos óbvios para programadores Python – __call__().

Vamos criar então a metaclasse! Vou chamar de unique em vez de singleton (Por quê? Porque eu gosto, ora pois!):

class unique(type):

def __init__(cls, name, base, dict):
super(unique, cls).__init__(name, base, dict)
cls.__inst = None
cls.__copy__ = lambda self: self #(nova linha!)
cls.__deepcopy__ = lambda self, memo=None: self #(nova linha!)

def __call__(cls, *args, **kw):
if cls.__inst is None:
cls.__inst = super(unique, cls).__call__(*args, **kw)
return cls.__inst


Esta metaclasse é bela! Vamos dissecá-la:

O construtor chama seu super (construtor da classe pai, type) e depois, a única coisa que faz de diferente é criar um atributo privado de classe __inst. Só que este atributo é «mais privado» do que os normais. =)

Aqui o escopo para o atributo privado não é a classe, mas unique (a metaclasse). Podem imaginar o nível de proteção disso? É como em C++, só que funciona. =)

Bem, o método __call__() faz exatamente o mesmo que __new__() no exemplo que usa herança, só que de forma mais eficiente. A implementação disso poderia ser:

class Conexao(object):

__metaclass__ = unique

...


Simples! E sem os problemas que poderiam vir com o uso de herança.

Mas aí vem um espírito de porco qualquer e pergunta:

Peraí! E se eu quiser usar também outra metaclasse, como autoprop?

Ahá! Não é tão complicado assim!

class Conexao(object):

__metaclass__ = type("__metaclass__", (autoprop, unique), {})

...


Complicou? É realmente... o uso de type não é tão intuitivo... =P

Mas há uma forma mais clara (aliás, eu prefiro) de fazer isso!

class Conexao(object):

class __metaclass__(autoprop, unique):
pass

...


Acho que é só isso! Comentários – ou melhor, complementos – por favor!

[]'s

Leitura recomendada: