简体   繁体   中英

Appending an instance of a class to a list makes a copy rather than a reference in Python

I have a Person class and a Car class. Each Person can have multiple Car objects and each Car can be shared by more than one Person . See the code below:

class Person:

    community = []

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

        Person.community.append(self)
        self.my_cars = []
        self.add_cars()

    def add_cars(self):
        c = Car.get_current_cars()
        for i in range(len(c)):
            self.my_cars.append(c[i])  # This is not making a reference to Car but creating a new copy

When a Person is created the current Car instances are added to that Person object's Car list my_car .

Note I am adding all Car objects in Car as per minimum reproducible code. In reality this is not necessary because my_car may contain a subset of the total instances and that is why I am using the append function.

When a Car instance is created I am storing each Car in a list so this can be accessed by Person like below:

class Car:

    cars = []

    def __init__(self, model):
        self.model = model
        Car.cars.append(self)


    @classmethod
    def get_current_cars(cls):
        return cls.cars

    @classmethod
    def delete(cls, name):

        for i in range(len(cls.cars)):
            if cls.cars[i].model == name:
                del cls.cars[i]
                break

The problem arises when I want to delete a Car . If this happens I would like each Person object's my_car list to be updated and this is not happening. In other words using the append function is not creating a pointer to each Car instance but rather it seems it is creating a copy.

The code below is a simple example that shows this:

if __name__ == "__main__":
    models = ["Toyota", "BMW", "Tesla", "Fiat"]

    AA = [Car(models[i]) for i in range(len(models))]

    x = Car.get_current_cars()

    A = Person("john", 0)
    B = Person("Liam", 1)

    print([A.my_cars[i].model for i in range(len(A.my_cars))])
    print([B.my_cars[i].model for i in range(len(B.my_cars))])


    Car.delete("Toyota")
    x = Car.get_current_cars()

    print([x[i].model for i in range(len(x))])


    print([A.my_cars[i].model for i in range(len(A.my_cars))])
    print([B.my_cars[i].model for i in range(len(B.my_cars))])

How can I obtain my the behavior I desire?

del deletes that variable, not the underlying object. That object is deleted when its counter of references drops to 0 and garbage collector decides to delete it.

Imagine that every object has a counter. Let's say your cars are list objects (Only for the sake of simplicity. The concept is the same for every mutable object):

car1 = [1, 2, 3]

Here the counter for object car1 points to is 1. Every time something points to that object, the counter goes up:

car2 = car1
car3 = car1

Now the counter is 3. When you delete a variable using del the reference goes down, but the object is only deleted when the counter drops to 0 and the garbage collector decides to delete that object:

del car2 

The counter is 2 now and car1 and car3 are still pointing to it, trying to alter car1 would alter car3 :

car1.append(4)
print(car3)    # output: [1, 2, 3, 4]

Conclusion: When there is more than one reference to a mutable object, all of them can alter that object, but none can actually delete the object on its own. When you collect those cars in a list, you still have a reference to those cars:

car4 = ["Toyota"]
car5 = ["Audi"]
car6 = ["Tesla"]

c = [car4, car5, car6]
my_cars = []
for x in c:
    my_cars.append(x)

Here c and my_cars both hold references to the same cars. They each point to a different list object, and each list object points to each car object on the behalf of its own. Each, increase the counter by one, thus the counter is increased by 2. You can alter cars using either of the lists:

c[0][0] = "Mercedes-Benz"
print(my_cars[0])  # output: ['Mercedes-Benz']

But as we concluded above, you cannot destroy the object using just one of the references:

del c[0]
print(my_cars)  # output is still [['Mercedes-Benz'], ['Audi'], ['Tesla']]. The object still exists.

On the other hand, when you ask c and my_cars to point to the same list, they both point to that (one) list object. Note that there is just one list object, and any changes to that object affects both variables :

c = [car4, car5, car6]
my_cars = c

del c[0]
print(my_cars) # output: [['Mercedes-Benz'], ['Tesla']]. The first object is removed.

Note that, then the object might still not be deleted if some other variables are still pointing to it:

print(car4) # output: ['Mercedes-Benz']. car4 is still pointing to that car object, thus it is not destroyed.

But the single list object both c and my_cars are pointing to does not contain it anymore.

UPDATE:

You have to rearrange the code somehow to Person.my_cars get the direct reference of Car.cars as you obviously intended. Doing so, when it Car.cars 's elements - and thus its bounds to their pointers - has been deleted with del statement, it will be done with Person.my_cars referred elements too. When you use list.append() it will create a new variable and it assigns a pointer to the same address like the original - copied - variable. del won't eliminate this new bound when you delete the original variable.

Solution 1:

So as a workaround this problem change add_cars method (at least append(), as you wrote this is just a minimal code):

def add_cars(self):
    self.my_cars = Car.get_current_cars() # you don't need any loop to copy all the cars from Car class's cars.

Solution 2:

You can delete the new bounds directly as well.

import weakref

class Person:

    community = []
    __refs__ = set()

    def __init__(self, name, id):
        self.name = name
        self.id = id
        self.__refs__.add(weakref.ref(self))

        Person.community.append(self)
        self.my_cars = []
        self.add_cars()

    def add_cars(self):
        c = Car.get_current_cars()
        for i in range(len(c)):
            self.my_cars.append(c[i])  # This is not making a reference to Car but creating a new copy

    @classmethod
    def get_instances(cls):
        for inst_ref in cls.__refs__:
            inst = inst_ref()
            if inst is not None:
                yield inst                   


class Car:

    cars = []

    def __init__(self, model):
        self.model = model
        Car.cars.append(self)

    @classmethod
    def get_current_cars(cls):
        return cls.cars

    @classmethod
    def delete(cls, name):

        for i in range(len(cls.cars)):
            if cls.cars[i].model == name:
                del cls.cars[i]
                for inst in Person.get_instances():
                    del inst.my_cars[i]                                
                break

Test:

if __name__ == "__main__":
    models = ["Toyota", "BMW", "Tesla", "Fiat"]

    AA = [Car(models[i]) for i in range(len(models))]

    x = Car.get_current_cars()

    A = Person("john", 0)
    B = Person("Liam", 1)

    print([A.my_cars[i].model for i in range(len(A.my_cars))])
    print([B.my_cars[i].model for i in range(len(B.my_cars))])


    Car.delete("Toyota")

    x = Car.get_current_cars()

    print([x[i].model for i in range(len(x))])


    print([A.my_cars[i].model for i in range(len(A.my_cars))])
    print([B.my_cars[i].model for i in range(len(B.my_cars))])  

Out:

['Toyota', 'BMW', 'Tesla', 'Fiat']
['Toyota', 'BMW', 'Tesla', 'Fiat']
['BMW', 'Tesla', 'Fiat']
['BMW', 'Tesla', 'Fiat']
['BMW', 'Tesla', 'Fiat']

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM