python - Múltiplos construtores: o caminho pitonico?




constructor initialization (5)

Esta questão já tem uma resposta aqui:

Eu tenho uma classe de contêiner que contém dados. Quando o contêiner é criado, existem métodos diferentes para passar dados.

  1. Passar um arquivo que contém os dados
  2. Passar os dados diretamente via argumentos
  3. Não passe dados; basta criar um contêiner vazio

Em Java, eu criaria três construtores. Veja como ficaria se fosse possível em Python:

class Container:

    def __init__(self):
        self.timestamp = 0
        self.data = []
        self.metadata = {}

    def __init__(self, file):
        f = file.open()
        self.timestamp = f.get_timestamp()
        self.data = f.get_data()
        self.metadata = f.get_metadata()

    def __init__(self, timestamp, data, metadata):
        self.timestamp = timestamp
        self.data = data
        self.metadata = metadata

Em Python, vejo três soluções óbvias, mas nenhuma delas é bonita:

A : Usando argumentos de palavras-chave:

def __init__(self, **kwargs):
    if 'file' in kwargs:
        ...
    elif 'timestamp' in kwargs and 'data' in kwargs and 'metadata' in kwargs:
        ...
    else:
        ... create empty container

B : Usando argumentos padrão:

def __init__(self, file=None, timestamp=None, data=None, metadata=None):
    if file:
        ...
    elif timestamp and data and metadata:
        ...
    else:
        ... create empty container

C : Somente forneça o construtor para criar contêineres vazios. Forneça métodos para preencher contêineres com dados de diferentes origens.

def __init__(self):
    self.timestamp = 0
    self.data = []
    self.metadata = {}

def add_data_from_file(file):
    ...

def add_data(timestamp, data, metadata):
    ...

As soluções A e B são basicamente as mesmas. Eu não gosto de fazer o if / else, especialmente desde que eu tenho que verificar se todos os argumentos necessários para este método foram fornecidos. A é um pouco mais flexível que B se o código precisar ser estendido por um quarto método para adicionar dados.

A solução C parece ser a mais legal, mas o usuário precisa saber qual método ele requer. Por exemplo: ele não pode fazer c = Container(args) se ele não souber o que é args .

Qual é a solução mais Pythonica?


A maioria dos Pythonic seria o que a biblioteca padrão do Python já faz. O desenvolvedor principal Raymond Hettinger (o cara das collections ) deu uma palestra sobre isso , além de orientações gerais sobre como escrever classes.

Use funções de nível de classe separadas para inicializar instâncias, como como dict.fromkeys() não é o inicializador de classe, mas ainda retorna uma instância de dict . Isso permite que você seja flexível em relação aos argumentos de que precisa sem alterar as assinaturas de método à medida que os requisitos mudam.


A maneira mais pythonic é garantir que quaisquer argumentos opcionais tenham valores padrão. Portanto, inclua todos os argumentos que você sabe que precisa e atribua a eles os padrões apropriados.

def __init__(self, timestamp=None, data=[], metadata={}):
    timestamp = time.now()

Uma coisa importante a lembrar é que quaisquer argumentos necessários não devem ter padrões, pois você quer que um erro seja levantado se eles não estiverem incluídos.

Você pode aceitar ainda mais argumentos opcionais usando *args e **kwargs no final de sua lista de argumentos.

def __init__(self, timestamp=None, data=[], metadata={}, *args, **kwards):
    if 'something' in kwargs:
        # do something

Quais são os objetivos do sistema para este código? Do meu ponto de vista, sua frase crítica é, but the user has to know which method he requires. Que experiência você deseja que seus usuários tenham com seu código? Isso deve impulsionar o design da interface.

Agora, vá para manutenção: qual solução é mais fácil de ler e manter? Mais uma vez, sinto que a solução C é inferior. Para a maioria das equipes com as quais trabalhei, a solução B é preferível a A: é um pouco mais fácil de ler e entender, embora ambas se dividam em pequenos blocos de código para tratamento.


Se você está no Python 3.4+, pode usar o decorador functools.singledispatch para fazer isso (com uma pequena ajuda extra do decorador @ZeroPiraeus que @ZeroPiraeus escreveu para sua resposta ):

class Container:

    @methoddispatch
    def __init__(self):
        self.timestamp = 0
        self.data = []
        self.metadata = {}

    @__init__.register(File)
    def __init__(self, file):
        f = file.open()
        self.timestamp = f.get_timestamp()
        self.data = f.get_data()
        self.metadata = f.get_metadata()

    @__init__.register(Timestamp)
    def __init__(self, timestamp, data, metadata):
        self.timestamp = timestamp
        self.data = data
        self.metadata = metadata

Você não pode ter vários métodos com o mesmo nome no Python . A sobrecarga de funções - ao contrário de Java - não é suportada.

Use os parâmetros padrão ou os argumentos **kwargs e *args .

Você pode criar métodos estáticos ou métodos de classe com o @staticmethod ou o @classmethod decorator para retornar uma instância de sua classe ou para incluir outros construtores.

Eu aconselho você a fazer:

class F:

    def __init__(self, timestamp=0, data=None, metadata=None):
        self.timestamp = timestamp
        self.data = list() if data is None else data
        self.metadata = dict() if metadata is None else metadata

    @classmethod
    def from_file(cls, path):
       _file = cls.get_file(path)
       timestamp = _file.get_timestamp()
       data = _file.get_data()
       metadata = _file.get_metadata()       
       return cls(timestamp, data, metadata)

    @classmethod
    def from_metadata(cls, timestamp, data, metadata):
        return cls(timestamp, data, metadata)

    @staticmethod
    def get_file(path):
        # ...
        pass

⚠ Nunca tenha tipos mutáveis ​​como padrões em python. ⚠ Veja here .





idiomatic