Por que os métodos __init__ da superclasse não são invocados automaticamente?


155

Por que os designers do Python decidiram que os __init__()métodos das subclasses não chamam automaticamente os __init__()métodos de suas superclasses, como em outras línguas? O idioma pitonico e recomendado é realmente o seguinte?

class Superclass(object):
    def __init__(self):
        print 'Do something'

class Subclass(Superclass):
    def __init__(self):
        super(Subclass, self).__init__()
        print 'Do something else'

2
Você pode escrever um decorador para herdar o __init__método e até mesmo procurar automaticamente subclasses e decorá-las.
osa

2
@osa, parece realmente uma boa ideia. gostaria de descrever um pouco mais sobre a parte do decorador?
precisa saber é o seguinte

1
@osa sim, descreva mais!
Charlie Parker

Respostas:


163

A distinção crucial entre o Python __init__e os construtores de outras linguagens é que não__init__ é um construtor: é um inicializador (o construtor real (se houver, mas veja mais adiante ;-) é e funciona completamente diferente de novo). Embora a construção de todas as superclasses (e, sem dúvida, fazê-lo "antes" você continue construindo para baixo) é obviamente parte de dizer que você está construindo a instância de uma subclasse, isso claramente não é o caso para inicializar__new__, já que existem muitos casos de uso em que a inicialização das superclasses precisa ser ignorada, alterada, controlada - ocorrendo, se é que existe, "no meio" da inicialização da subclasse e assim por diante.

Basicamente, a delegação de superclasse do inicializador não é automática no Python pelas mesmas razões que essa delegação também não é automática para outros métodos - e observe que esses "outros idiomas" não fazem delegação de superclasse automática para nenhum método. outro método também ... apenas para o construtor (e, se aplicável, o destruidor), que, como mencionei, não é o que é o Python __init__. (O comportamento de __new__também é bastante peculiar, embora realmente não esteja diretamente relacionado à sua pergunta, pois __new__é um construtor tão peculiar que na verdade não precisa necessariamente construir nada - poderia perfeitamente retornar uma instância existente, ou mesmo uma não instância. ... claramente o Python oferece muitomais controle da mecânica do que as "outras línguas" que você tem em mente, o que também inclui não ter delegação automática em __new__si! -).


7
Promovido porque, para mim, o benefício real é a capacidade que você menciona de chamar _ init () _ da superclasse em qualquer ponto da inicialização da subclasse (ou não).
Kindall

53
-1 para " __init__não é um construtor ... o construtor real ... é __new__". Como você observa, __new__não se comporta como um construtor de outras línguas. __init__é de fato muito semelhante (é chamado durante a criação de um novo objeto, após a alocação desse objeto, para definir variáveis ​​de membro no novo objeto) e quase sempre é o lugar para implementar a funcionalidade que em outros idiomas você colocaria um construtor. Então, basta chamá-lo de construtor!
31511 Ben

8
Também acho que essa é uma afirmação bastante absurda: " construir todas as superclasses ... obviamente faz parte de dizer que você está construindo uma instância de subclasse, que claramente não é o caso de inicializar ". Não há nada nas palavras construção / inicialização que torne isso "óbvio" para qualquer pessoa. E __new__também não invoca automaticamente a superclasse __new__. Portanto, sua afirmação de que a principal distinção é que a construção envolve necessariamente a construção de superclasses, enquanto a inicialização não é inconsistente com a afirmação de que __new__é o construtor.
31511 Ben

36
De fato, dos documentos python para __init__em docs.python.org/reference/datamodel.html#basic-customization : "Como uma restrição especial para construtores , nenhum valor pode ser retornado; isso fará com que um TypeError seja gerado em tempo de execução "(ênfase minha). Então, é oficial, __init__é um construtor.
Ben

3
Na terminologia Python / Java, __init__é chamado de construtor. Esse construtor é uma função de inicialização chamada após o objeto ter sido completamente construído e inicializado em um estado padrão, incluindo seu tipo de tempo de execução final. Não é equivalente aos construtores C ++, chamados em um objeto alocado, estaticamente digitado, com um estado indefinido. Também são diferentes de __new__, então realmente temos pelo menos quatro tipos diferentes de funções de alocação / construção / inicialização. Os idiomas usam terminologia mista, e a parte importante é o comportamento, e não a terminologia.
Elazar 13/01

36

Fico um pouco envergonhado quando as pessoas papagaiam o "Zen of Python", como se isso fosse uma justificativa para qualquer coisa. É uma filosofia de design; decisões de design específicas sempre podem ser explicadas em termos mais específicos - e devem ser, ou então o "Zen of Python" se torna uma desculpa para fazer qualquer coisa.

O motivo é simples: você não constrói necessariamente uma classe derivada de maneira semelhante à maneira como constrói a classe base. Você pode ter mais parâmetros, menos, eles podem estar em uma ordem diferente ou não estar relacionados.

class myFile(object):
    def __init__(self, filename, mode):
        self.f = open(filename, mode)
class readFile(myFile):
    def __init__(self, filename):
        super(readFile, self).__init__(filename, "r")
class tempFile(myFile):
    def __init__(self, mode):
        super(tempFile, self).__init__("/tmp/file", mode)
class wordsFile(myFile):
    def __init__(self, language):
        super(wordsFile, self).__init__("/usr/share/dict/%s" % language, "r")

Isso se aplica a todos os métodos derivados, não apenas __init__.


6
Este exemplo oferece algo especial? linguagens estáticas pode fazer isso também
jean

18

Java e C ++ exigem que um construtor de classe base seja chamado devido ao layout da memória.

Se você tem uma classe BaseClasscom um membro field1e cria uma nova classe SubClassque adiciona um membro field2, uma instância de SubClasscontém espaço para field1e field2. Você precisa de um construtor de BaseClasspreenchimento field1, a menos que exija que todas as classes herdadas repitam BaseClassa inicialização de seus próprios construtores. E se field1for privado, as classes herdadas não poderão inicializar field1.

Python não é Java ou C ++. Todas as instâncias de todas as classes definidas pelo usuário têm a mesma 'forma'. Eles são basicamente apenas dicionários nos quais os atributos podem ser inseridos. Antes de qualquer inicialização, todas as instâncias de todas as classes definidas pelo usuário são quase exatamente iguais ; são apenas lugares para armazenar atributos que ainda não estão sendo armazenados.

Portanto, faz sentido que uma subclasse Python não chame seu construtor de classe base. Apenas poderia adicionar os próprios atributos, se quisesse. Não há espaço reservado para um determinado número de campos para cada classe na hierarquia e não há diferença entre um atributo adicionado pelo código de um BaseClassmétodo e um atributo adicionado pelo código de um SubClassmétodo.

Se, como é comum, SubClassrealmente quer ter todos BaseClassos invariantes configurados antes de fazer sua própria personalização, sim, você pode simplesmente ligar BaseClass.__init__()(ou usar super, mas isso é complicado e, às vezes, tem seus próprios problemas). Mas você não precisa. E você pode fazer isso antes, depois ou com argumentos diferentes. Inferno, se você quisesse, poderia chamar o BaseClass.__init__de outro método inteiramente do que __init__; talvez você tenha alguma coisa bizarra de inicialização lenta.

O Python consegue essa flexibilidade, mantendo as coisas simples. Você inicializa objetos escrevendo um __init__método que define atributos self. É isso aí. Ele se comporta exatamente como um método, porque é exatamente um método. Não há outras regras estranhas e não intuitivas sobre o que deve ser feito primeiro, ou o que acontecerá automaticamente se você não fizer outras coisas. O único objetivo que ele precisa servir é ser um gancho para executar durante a inicialização do objeto para definir valores de atributos iniciais, e é exatamente isso. Se você quiser fazer outra coisa, escreva explicitamente isso no seu código.


2
Quanto ao C ++, isso não tem nada a ver com o "layout da memória". O mesmo modelo de inicialização que o Python oferece poderia ter sido implementado em C ++. A única razão pela qual a construção / destruição é a maneira como eles são em C ++ é devido à decisão do projeto de fornecer recursos confiáveis ​​e bem comportados para o gerenciamento de recursos (RAII), que também poderiam ser gerados automaticamente (o que significa menos erros humanos e de código) pelos compiladores, uma vez que as regras para eles (sua ordem de chamada) são rigorosamente definidas. Não tenho certeza, mas o Java provavelmente seguiu essa abordagem como outra linguagem semelhante ao POLA C.
Alexander Shukaev 12/11/2018

10

"Explícito é melhor que implícito." É o mesmo raciocínio que indica que devemos escrever explicitamente 'self'.

Acho que, no final, é um benefício - você pode recitar todas as regras que Java tem em relação à chamada de construtores de superclasses?


8
Eu concordo com você em sua maior parte, mas a regra do Java é realmente muito simples: o construtor no-arg é chamado, a menos que você solicite especificamente um diferente.
Laurence Gonsalves

1
@ Laurence - O que acontece quando a classe pai não define um construtor no-arg? O que acontece quando o construtor no-arg é protegido ou privado?
Mike Axiak 23/09/10

8
Exatamente a mesma coisa que aconteceria se você tentasse chamá-lo explicitamente.
Laurence Gonsalves

8

Frequentemente, a subclasse possui parâmetros extras que não podem ser passados ​​para a superclasse.


7

No momento, temos uma página bastante longa descrevendo a ordem de resolução do método em caso de herança múltipla: http://www.python.org/download/releases/2.3/mro/

Se os construtores fossem chamados automaticamente, você precisaria de outra página de pelo menos o mesmo comprimento, explicando a ordem do ocorrido. Isso seria um inferno ...


Esta foi a resposta certa. O Python precisaria definir a semântica da passagem de argumentos entre subclasse e superclasse. Pena que esta resposta não seja votada. Talvez se mostrasse o problema com alguns exemplos?
Gary Weiss

5

Para evitar confusão, é útil saber que você pode chamar o __init__()método base_class se o child_class não tiver uma __init__()classe.

Exemplo:

class parent:
  def __init__(self, a=1, b=0):
    self.a = a
    self.b = b

class child(parent):
  def me(self):
    pass

p = child(5, 4)
q = child(7)
z= child()

print p.a # prints 5
print q.b # prints 0
print z.a # prints 1

De fato, o MRO em python procurará __init__()na classe pai quando não puder encontrá-lo na classe filhos. Você precisa chamar o construtor da classe pai diretamente se você já possui um __init__()método na classe filho.

Por exemplo, o código a seguir retornará um erro: pai da classe: def init (self, a = 1, b = 0): self.a = a self.b = b

    class child(parent):
      def __init__(self):
        pass
      def me(self):
        pass

    p = child(5, 4) # Error: constructor gets one argument 3 is provided.
    q = child(7)  # Error: constructor gets one argument 2 is provided.

    z= child()
    print z.a # Error: No attribute named as a can be found.

3

Talvez __init__seja o método que a subclasse precisa substituir. Às vezes, as subclasses precisam da função do pai para executar antes de adicionar código específico da classe, e outras vezes precisam configurar variáveis ​​de instância antes de chamar a função do pai. Como o Python não tem como saber quando seria mais apropriado chamar essas funções, isso não deve ser adivinhado.

Se esses não o influenciarem, considere que __init__é apenas mais uma função. Se a função em questão fosse dostuffalternativa, você ainda desejaria que o Python chamasse automaticamente a função correspondente na classe pai?


2

Eu acredito que a consideração muito importante aqui é que, com uma chamada automática para super.__init__(), você proíbe, por design, quando esse método de inicialização é chamado e com quais argumentos. evitar a chamada automática e exigir que o programador faça explicitamente essa chamada exige muita flexibilidade.

afinal, apenas porque a classe B é derivada da classe A não significa que A.__init__()pode ou deve ser chamado com os mesmos argumentos que B.__init__(). Tornar a chamada explícita significa que um programador pode ter, por exemplo, definir B.__init__()parâmetros completamente diferentes, fazer alguns cálculos com esses dados, chamar A.__init__()argumentos conforme apropriado para esse método e depois executar algum pós-processamento. esse tipo de flexibilidade seria difícil de obter se A.__init__()fosse chamado B.__init__()implicitamente, antes da B.__init__()execução ou logo após.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.