¿Cómo evitar tener datos de clase compartidos entre instancias?

146

Lo que quiero es este comportamiento:

class a:
    list = []

x = a()
y = a()

x.list.append(1)
y.list.append(2)
x.list.append(3)
y.list.append(4)

print(x.list) # prints [1, 3]
print(y.list) # prints [2, 4]

Por supuesto, lo que realmente sucede cuando imprimo es:

print(x.list) # prints [1, 2, 3, 4]
print(y.list) # prints [1, 2, 3, 4]

Claramente están compartiendo los datos en clase a. ¿Cómo obtengo instancias separadas para lograr el comportamiento que deseo?

8steve8
fuente
14
Por favor, no lo use listcomo nombre de atributo. listes una función integrada para construir una nueva lista. Deberías escribir clases de nombres con mayúscula.
Marc Tudurí

Respuestas:

147

Tu quieres esto:

class a:
    def __init__(self):
        self.list = []

Declarar las variables dentro de la declaración de clase los convierte en miembros de "clase" y no miembros de instancia. Declararlos dentro del __init__método asegura que se cree una nueva instancia de los miembros junto con cada nueva instancia del objeto, que es el comportamiento que está buscando.

abyx
fuente
55
Una aclaración adicional: si fuera a reasignar la propiedad de la lista en una de las instancias, no afectaría a las otras. Entonces, si hiciste algo así x.list = [], podrías cambiarlo y no afectar a ningún otro. El problema que enfrenta es que x.listy y.listson la misma lista, por lo que cuando llama anexar en uno, afecta al otro.
Matt Moriarity
Pero, ¿por qué sucede esto solo para la lista? Cuando declaró un número entero o una cadena fuera del init , ¿no se compartió entre los objetos? ¿Alguien puede compartir algún enlace de documento a este concepto?
Amal Ts
1
@AmalTs Parece que no entiendes cómo funciona la asignación en Python. Vea este video o esta publicación SO . El comportamiento que ve es causado por el hecho de que está mutando listas pero volviendo a vincular referencias a ints y strings.
Андрей Беньковский
44
@AmalTs Nota: se considera una mala práctica utilizar los atributos de clase como valores predeterminados "vagos" para los atributos de instancia. Incluso si los atributos son de un tipo inmutable, es mejor asignarlos dentro __init__.
Андрей Беньковский
21

La respuesta aceptada funciona, pero un poco más de explicación no hace daño.

Los atributos de clase no se convierten en atributos de instancia cuando se crea una instancia. Se convierten en atributos de instancia cuando se les asigna un valor.

En el código original no se asigna ningún valor al listatributo después de la instanciación; entonces sigue siendo un atributo de clase. La definición de la lista interna __init__funciona porque __init__se llama después de la creación de instancias. Alternativamente, este código también produciría la salida deseada:

>>> class a:
    list = []

>>> y = a()
>>> x = a()
>>> x.list = []
>>> y.list = []
>>> x.list.append(1)
>>> y.list.append(2)
>>> x.list.append(3)
>>> y.list.append(4)
>>> print(x.list)
[1, 3]
>>> print(y.list)
[2, 4]

Sin embargo, el escenario confuso en la pregunta nunca sucederá con objetos inmutables como números y cadenas, porque su valor no se puede cambiar sin asignación. Por ejemplo, un código similar al original con el tipo de atributo de cadena funciona sin ningún problema:

>>> class a:
    string = ''


>>> x = a()
>>> y = a()
>>> x.string += 'x'
>>> y.string += 'y'
>>> x.string
'x'
>>> y.string
'y'

Para resumir: los atributos de clase se convierten en atributos de instancia si y solo si se les asigna un valor después de la instanciación, esté __init__o no en el método . Esto es bueno porque de esta manera puede tener atributos estáticos si nunca asigna un valor a un atributo después de la creación de instancias.

jurgenreza
fuente
11

Declaró "lista" como una "propiedad de nivel de clase" y no como "propiedad de nivel de instancia". Para que las propiedades tengan un alcance en el nivel de instancia, debe inicializarlas haciendo referencia al parámetro "self" en el __init__método (o en otro lugar, según la situación).

No tiene que inicializar estrictamente las propiedades de la instancia en el __init__método, pero facilita la comprensión.

jldupont
fuente
11

Aunque la respuesta aceptada es acertada, me gustaría agregar una pequeña descripción.

Hagamos un pequeño ejercicio.

En primer lugar, defina una clase de la siguiente manera:

class A:
    temp = 'Skyharbor'

    def __init__(self, x):
        self.x = x

    def change(self, y):
        self.temp = y

entonces que tenemos aqui?

  • Tenemos una clase muy simple que tiene un atributo tempque es una cadena
  • Un __init__método que estableceself.x
  • Un método de cambio establece self.temp

Bastante sencillo hasta ahora, ¿sí? Ahora comencemos a jugar con esta clase. Inicialicemos esta clase primero:

a = A('Tesseract')

Ahora haga lo siguiente:

>>> print(a.temp)
Skyharbor
>>> print(A.temp)
Skyharbor

Bueno, a.tempfuncionó como se esperaba, pero ¿cómo demonios A.tempfuncionó? Bueno, funcionó porque temp es un atributo de clase. Todo en Python es un objeto. Aquí A es también un objeto de clase type. Por lo tanto, el atributo temp es un atributo mantenido por la Aclase y si cambia el valor de temp a través de A(y no a través de una instancia de a), el valor cambiado se reflejará en toda la instancia de Aclase. Avancemos y hagamos eso:

>>> A.temp = 'Monuments'
>>> print(A.temp)
Monuments
>>> print(a.temp)
Monuments

¿Interesante no? Y tenga en cuenta que id(a.temp)y id(A.temp)siguen siendo los mismos .

Cualquier objeto de Python recibe automáticamente un __dict__atributo, que contiene su lista de atributos. Investiguemos qué contiene este diccionario para nuestros objetos de ejemplo:

>>> print(A.__dict__)
{
    'change': <function change at 0x7f5e26fee6e0>,
    '__module__': '__main__',
    '__init__': <function __init__ at 0x7f5e26fee668>,
    'temp': 'Monuments',
    '__doc__': None
}
>>> print(a.__dict__)
{x: 'Tesseract'}

Tenga en cuenta que el tempatributo se enumera entre Alos atributos de la clase mientras que xse enumera para la instancia.

Entonces, ¿cómo es que obtenemos un valor definido de a.tempsi ni siquiera está listado para la instancia a? Bueno, esa es la magia del __getattribute__()método. En Python, la sintaxis punteada invoca automáticamente este método, por lo que cuando escribimos a.temp, Python se ejecuta a.__getattribute__('temp'). Ese método realiza la acción de búsqueda de atributos, es decir, encuentra el valor del atributo buscando en diferentes lugares.

La implementación estándar de __getattribute__()búsquedas primero el diccionario interno ( dict ) de un objeto, luego el tipo del objeto en sí. En este caso se a.__getattribute__('temp')ejecuta primero a.__dict__['temp']y luegoa.__class__.__dict__['temp']

Bien, ahora usemos nuestro changemétodo:

>>> a.change('Intervals')
>>> print(a.temp)
Intervals
>>> print(A.temp)
Monuments

Bueno, ahora que lo hemos usado self, print(a.temp)nos da un valor diferente de print(A.temp).

Ahora, si comparamos id(a.temp)y id(A.temp), serán diferentes.

Swapnil
fuente
3

Sí, debe declarar en el "constructor" si desea que la lista se convierta en una propiedad de objeto y no en una propiedad de clase.

osanchezmon
fuente
3

Así que casi todas las respuestas aquí parecen perder un punto en particular. Las variables de clase nunca se convierten en variables de instancia, como lo demuestra el siguiente código. Al utilizar una metaclase para interceptar la asignación de variables a nivel de clase, podemos ver que cuando se reasigna a.myattr, no se llama al método mágico de asignación de campo en la clase. Esto se debe a que la asignación crea una nueva variable de instancia . Este comportamiento no tiene absolutamente nada que ver con la variable de clase, como lo demuestra la segunda clase que no tiene variables de clase y, sin embargo, todavía permite la asignación de campo.

class mymeta(type):
    def __init__(cls, name, bases, d):
        pass

    def __setattr__(cls, attr, value):
        print("setting " + attr)
        super(mymeta, cls).__setattr__(attr, value)

class myclass(object):
    __metaclass__ = mymeta
    myattr = []

a = myclass()
a.myattr = []           #NOTHING IS PRINTED
myclass.myattr = [5]    #change is printed here
b = myclass()
print(b.myattr)         #pass through lookup on the base class

class expando(object):
    pass

a = expando()
a.random = 5            #no class variable required
print(a.random)         #but it still works

EN CORTO Las variables de clase no tienen NADA que ver con las variables de instancia.

Más claramente , simplemente están dentro del alcance de las búsquedas en instancias. Las variables de clase son, de hecho , variables de instancia en el propio objeto de clase. También puede tener variables de metaclases si lo desea, porque las metaclases en sí mismas también son objetos. Todo es un objeto, ya sea que se use para crear otros objetos o no, así que no se enrede en la semántica del uso de la clase de palabras en otros idiomas. En python, una clase es realmente solo un objeto que se usa para determinar cómo crear otros objetos y cuáles serán sus comportamientos. Las metaclases son clases que crean clases, solo para ilustrar más este punto.

TheQabalist
fuente
0

Para proteger su variable compartida por otra instancia, debe crear una nueva variable de instancia cada vez que cree una instancia. Cuando declara una variable dentro de una clase, es variable de clase y es compartida por todas las instancias. Si desea hacerlo, por ejemplo, debe usar el método init para reinicializar la variable como se refiere a la instancia

Desde Python Objects and Class por Programiz.com :

__init__()función. Esta función especial se llama cada vez que se instancia un nuevo objeto de esa clase.

Este tipo de función también se llama constructores en la Programación Orientada a Objetos (OOP). Normalmente lo usamos para inicializar todas las variables.

Por ejemplo:

class example:
    list=[] #This is class variable shared by all instance
    def __init__(self):
        self.list = [] #This is instance variable referred to specific instance
Projesh Bhoumik
fuente