¿Es una buena práctica declarar variables de instancia como Ninguna en una clase en Python?

68

Considere la siguiente clase:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

Mis compañeros de trabajo tienden a definirlo así:

class Person:
    name = None
    age = None

    def __init__(self, name, age):
        self.name = name
        self.age = age

La razón principal de esto es que su editor de elección muestra las propiedades para el autocompletado.

Personalmente, no me gusta el último, porque no tiene sentido que una clase tenga esas propiedades establecidas None.

¿Cuál sería una mejor práctica y por qué razones?

Remco Haszing
fuente
63
¿Nunca dejes que tu IDE dicte qué código escribes?
Martijn Pieters
14
Por cierto: usar un IDE de Python adecuado (por ejemplo, PyCharm), establecer los atributos en el __init__ya proporciona autocompletado, etc. Además, el uso Noneevita que el IDE infiera un mejor tipo para el atributo, por lo que es mejor usar un valor predeterminado sensible en su lugar (cuando posible).
Bakuriu
Si es solo para autocompletar, puede usar sugerencias de tipo, y las cadenas de documentos adicionales también serán una ventaja.
guiones
3
"Nunca dejes que tu IDE dicte qué código escribes" es un tema debatido. A partir de Python 3.6, hay anotaciones en línea y un typingmódulo, que le permiten proporcionar pistas al IDE y la interfaz, si ese tipo de cosas le hacen cosquillas ...
cz
1
Estas tareas a nivel de clase no tienen efecto en el resto del código. No tienen ningún efecto sobre self. Incluso si estuvieran asignados self.nameo self.ageno __init__, no aparecerían en la instancia self, solo aparecerían en la clase Person.
jolvi

Respuestas:

70

Llamo a esta última mala práctica bajo la regla "esto no hace lo que crees que hace".

La posición de su compañero de trabajo puede reescribirse como: "Voy a crear un montón de variables cuasi-globales estáticas de clase a las que nunca se accede, pero que ocupan espacio en las tablas de espacios de nombres de varias clases ( __dict__), solo para hacer que mi IDE funcione alguna cosa."

StarWeaver
fuente
44
Un poco como docstrings entonces ;-) Por supuesto, Python tiene un modo práctico para despojarlos -OO, para aquellos pocos que lo necesitan.
Steve Jessop
15
El espacio ocupado es, con mucho, la razón menos importante por la que apesta esta convención. Es una docena de bytes por nombre (por proceso). Siento que he perdido el tiempo solo leyendo la parte de tu respuesta correspondiente, así de importante es el costo del espacio.
8
@delnan Estoy de acuerdo en que el tamaño de la memoria de las entradas no tiene sentido, estaba más pensando en el espacio lógico / mental ocupado, haciendo una depuración introspectiva y eso requiere más lectura y clasificación, supongo. I ~ LL
StarWeaver
3
En realidad, si fueras tan paranoico con respecto al espacio, notarías que esto ahorra espacio y desperdicia tiempo. Los miembros no inicializados no pueden usar el valor de la clase y, por lo tanto, no hay un montón de copias del nombre de la variable asignada a None (una para cada instancia). Entonces, el costo es de unos pocos bytes en la clase y el ahorro es de unos pocos bytes por instancia. Pero cada búsqueda fallida de nombres de variables cuesta un poco de tiempo.
Jon Jay Obermark
2
Si estuviera preocupado por los 8 bytes, no estaría usando Python.
cz
26

1. Haz tu código fácil de entender

El código se lee con mucha más frecuencia que lo escrito. Facilite la tarea de su mantenedor de código (también puede ser usted mismo el próximo año).

No conozco ninguna regla estricta, pero prefiero que cualquier estado de instancia futuro se declare claramente. Chocar con un ya AttributeErrores bastante malo. No ver claramente el ciclo de vida de un atributo de instancia es peor. La cantidad de gimnasia mental requerida para restaurar las posibles secuencias de llamadas que conducen al atributo asignado puede convertirse fácilmente en algo no trivial y provocar errores.

Por lo tanto, generalmente no solo defino todo en el constructor, sino que también me esfuerzo por mantener el número de atributos mutables al mínimo.

2. No mezcle miembros de nivel de clase y nivel de instancia

Cualquier cosa que defina dentro de la classdeclaración pertenece a la clase y es compartida por todas las instancias de la clase. Por ejemplo, cuando define una función dentro de una clase, se convierte en un método que es el mismo para todas las instancias. Lo mismo se aplica a los miembros de datos. Esto es totalmente diferente a los atributos de instancia que usualmente define __init__.

Los miembros de datos de nivel de clase son más útiles como constantes:

class Missile(object):
  MAX_SPEED = 100  # all missiles accelerate up to this speed
  ACCELERATION = 5  # rate of acceleration per game frame

  def move(self):
    self.speed += self.ACCELERATION
    if self.speed > self.MAX_SPEED:
      self.speed = self.MAX_SPEED
    # ...
9000
fuente
2
Sí, pero mezclar miembros de nivel de clase y de instancia es más o menos lo que def hace. Crea funciones que consideramos como atributos de los objetos, pero que en realidad son miembros de la clase. Lo mismo ocurre con la propiedad y su tipo. Dar la ilusión de que el trabajo pertenece a los objetos cuando está realmente mediado por la clase es una forma tradicional de no volverse loco. ¿Cómo puede ser tan malo, si está bien para Python?
Jon Jay Obermark
Bueno, el orden de resolución de métodos en Python (también aplicable a los miembros de datos) no es muy simple. Lo que no se encuentra a nivel de instancia, se buscará a nivel de clase, luego entre las clases base, etc. De hecho, puede sombrear un miembro de nivel de clase (datos o método) asignando un miembro de nivel de instancia con el mismo nombre. Pero los métodos a nivel de instancia están vinculados a la instancia selfy no necesitan selfser pasados, mientras que los métodos a nivel de clase no están vinculados y son funciones simples como se ve en el defmomento, y aceptan una instancia como primer argumento. Entonces estas son cosas diferentes.
9000
Creo que una AttributeErrores una buena señal de que hay un error. De lo contrario, se tragaría el Ninguno y obtendría resultados sin sentido. Especialmente importante en el caso en cuestión, donde los atributos se definen en el __init__, por lo que un atributo faltante (pero existente a nivel de clase) solo podría ser causado por una herencia defectuosa.
Davidmh
@Davidmh: ¡Un error detectado siempre es mejor que un error no detectado, de hecho! Diría que si tiene que hacer un atributo Noney este valor no tiene sentido en el momento de la construcción de la instancia, tiene un problema en su arquitectura y tiene que repensar el ciclo de vida del valor del atributo o su valor inicial. Tenga en cuenta que al definir sus atributos temprano, puede detectar dicho problema incluso antes de escribir el resto de la clase, y mucho menos ejecutar el código.
9000
¡Divertido! misiles! de todos modos, estoy bastante seguro de que está bien hacer vars de nivel de clase y mezclarlos ... siempre que el nivel de clase contenga valores predeterminados, etc.
Erik Aronesty
18

Personalmente defino los miembros en el método __ init __ (). Nunca pensé en definirlos en la parte de la clase. Pero lo que siempre hago: inicio a todos los miembros en el método __ init__, incluso aquellos que no son necesarios en el método __ init__.

Ejemplo:

class Person:
    def __init__(self, name, age):
        self._name = name
        self._age = age
        self._selected = None

   def setSelected(self, value):
        self._selected = value

Creo que es importante definir a todos los miembros en un solo lugar. Hace que el código sea más legible. Ya sea dentro de __ init __ () o afuera, no es tan importante. Pero es importante que un equipo se comprometa con más o menos el mismo estilo de codificación.

Ah, y puede notar que alguna vez agrego el prefijo "_" a las variables miembro.

este mismo
fuente
13
Debe establecer todos los valores en el constructor. Si el valor se estableció en la propia clase, se compartiría entre las instancias, lo cual está bien para Ninguno, pero no para la mayoría de los valores. Así que no cambies nada al respecto. ;)
Remco Haszing
44
Los valores predeterminados para los parámetros y la capacidad de tomar argumentos de posición y palabras clave arbitrarios lo hacen mucho menos doloroso de lo que cabría esperar. (Y en realidad es posible delegar la construcción a otros métodos o incluso a funciones independientes, por lo que si realmente necesita un despacho múltiple, también puede hacerlo).
Sean Vieira
66
@Benedict Python no tiene control de acceso. El guión bajo principal es la convención aceptada para detalles de implementación. Ver PEP 8 .
Doval
3
@Doval Hay una razón extraordinariamente buena para prefijar los nombres de los atributos con _: ¡Para indicar que es privado! (¿Por qué tantas personas en este hilo confunden o confunden Python con otros idiomas?)
1
Ah, entonces eres una de esas personas de propiedades o funciones para la interacción con todos los expuestos ... Perdón por entender mal. Ese estilo siempre me parece excesivo, pero tiene seguidores.
Jon Jay Obermark
11

Esta es una mala práctica. No necesita esos valores, desordenan el código y pueden causar errores.

Considerar:

>>> class WithNone:
...   x = None
...   y = None
...   def __init__(self, x, y):
...     self.x = x
...     self.y = y
... 
>>> class InitOnly:
...   def __init__(self, x, y):
...     self.x = x
...     self.y = y
... 
>>> wn = WithNone(1,2)
>>> wn.x
1
>>> WithNone.x #Note that it returns none, no error
>>> io = InitOnly(1,2)
>>> InitOnly.x
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: class InitOnly has no attribute 'x'
Daenyth
fuente
Sería difícil llamar a eso 'causar un error'. Es ambiguo lo que quiere decir al solicitar 'x' aquí, pero es muy posible que desee el valor inicial más probable.
Jon Jay Obermark
Debería haber explicado que puede causar un error si se usa incorrectamente.
Daenyth
El mayor riesgo aún se está inicializando en un objeto. Ninguno es realmente seguro. El único error que causará es tragar excepciones realmente tontas.
Jon Jay Obermark
1
No causar errores es un problema si los errores hubieran evitado problemas más serios.
Erik Aronesty
0

Iré con "un poco como docstrings, entonces" y declararé esto inofensivo, siempre y cuando sea siempreNone , o un rango estrecho de otros valores, todos inmutables.

Apesta a atavismo y apego excesivo a lenguajes estáticamente escritos. Y no sirve como código. Pero tiene un propósito menor que queda, en la documentación.

Documenta cuáles son los nombres esperados, por lo que si combino el código con alguien y uno de nosotros tiene 'nombre de usuario' y el otro nombre de usuario, hay una pista para los humanos de que nos hemos separado y no estamos usando las mismas variables.

Forzar la inicialización completa como política logra lo mismo de una manera más pitónica, pero si hay un código real en el __init__, esto proporciona un lugar más claro para documentar las variables en uso.

Obviamente, el GRAN problema aquí es que tienta a la gente a inicializar con valores distintos a None, que pueden ser malos:

class X:
    v = {}
x = X()
x.v[1] = 2

deja un rastro global y no crea una instancia para x.

Pero eso es más una peculiaridad en Python en general que en esta práctica, y ya deberíamos estar paranoicos al respecto.

Jon Jay Obermark
fuente