1

Lists are a mutable data type. If I have sublists inside a 2d list that have multiple references to the same object, the fact that changing any one makes changes in others suggests a question: am I able to extract information about linked sublists?

To explain, let's consider a 2d list:

import random
lst = [[0] for i in range(9)]  # create list of 9 sublists with value inside
lst
[[0], [0], [0], [0], [0], [0], [0], [0], [0], [0]] .

Each sublist pointing to the same object 0, but independently. If any given sublist's contents change, the other sublists contents will remain stable. The fact that all IDs are different proves this point:

[id(i) for i in lst]
[139963231930640, 139963235043920, 139963236442592, 139963222312992, 139963783242688, 139963234936784, 139963233636256, 139963233634176, 139963233635056] .

Now let's create a random sample of objects:

indcs = random.sample(range(9), 9)    

and then modify the sublists using the following function:

def mutables(a):
    for i in range(len(a) - 1):
        lst[a[i+1]] = lst[a[i]]
        lst[a[i]][0] += 1

mutables(indcs[:3])
mutables(indcs[3:6])
mutables(indcs[6:])

lst
[[2], [2], [2], [2], [2], [2], [2], [2], [2]] .

The list now contains 3 groups of 3 sublists, each element of a group pointing to the same object. There are only 3IDsx3 in lst:

[id(i) for i in lst]
[139963231930640, 139963235043920, 139963236442592, 139963235043920, 139963236442592, 139963231930640, 139963235043920, 139963231930640, 139963236442592] .

If we change, e.g., the third element of a list, it changes each element of a group:

lst[2][0] += 1
lst
[[2], [2], [3], [2], [3], [2], [2], [2], [3]] .

Here, the sublists (2,4,8) are changed. The 2nd(0,5,7) and 3rd(1,3,6) groups behave the same way:

lst[0][0] += 1
lst
[[3], [2], [3], [2], [3], [3], [2], [3], [3]]
lst[1][0] += 1
lst
[[3], [3], [3], [3], [3], [3], [3], [3], [3]] .

The change of an integer values inside sublists did not change IDs:

[id(i) for i in lst]
    [139963231930640, 139963235043920, 139963236442592, 139963235043920, 139963236442592, 139963231930640, 139963235043920, 139963231930640, 139963236442592],

but adding 1 every time a new group member arrives provides useful information on the group size.

The crux of my question is, while the information about link between the sublists is retained, how to extract the indices of each group ((2,4,8),(0,5,7),(1,3,6)) without needing to refer to indcs and how it was originally grouped: [:3:6:]? I could change the 1st sublist value, track changes in others and pop them in a loop, but this information should be implicitly available on the basis of which an element in a group mutates. Is there any way to collect it? E.g., I can get id(name) of a sublist and try to find linked sublists by one of the methods in described here or here, but these options return values, not their names.

Yerbol Sapar
  • 31
  • 1
  • 8
  • It's a little unclear amidst your stream of thought here, but it seems like you want to know which of the list items are pointing to the same object? "Bound by mutation" is a weird term, nevermind "mutually bound by mutation". It is also unclear how you want to represent the indices - it seems harder to track which of the underlying objects the list is pointing to are the same. It might help to know *why* you want to do this? – Nathaniel Ford Jun 29 '22 at 19:20
  • Every slice is a new list. The only shared data are the `[0]` lists that you created in the first line. See https://stackoverflow.com/questions/240178/list-of-lists-changes-reflected-across-sublists-unexpectedly – Barmar Jun 29 '22 at 19:28
  • It seems to me that you're trying to create behaviour that is very similar to what `pandas.DataFrame` allows you to do by indexing a dataframe with a series of boolean values, or what `numpy` allows with masks. Why create a complicated and hard to understand (nevermind relatively slow) solution to a problem that has already been solved very efficiently in very commonly used packages? – Grismar Jun 30 '22 at 04:45
  • @Grismar You are right, seems that numpy with masks is applicable, but the problem is my lists are multi-dimensional with different length of elements. – Yerbol Sapar Jun 30 '22 at 12:44
  • @NathanielFord Thank you very much for your valuable editing that made the question much clearer. The reason, why i need this is extracting graph connected components avoiding time consuming analysis, i.e., `components = [G.subgraph(c).copy() for c in networks.connected_components(G)]`. I thought, during graph forming stage, I could use the function like `mutables()` to establish links among some nodes without explicit creation of extra variables. – Yerbol Sapar Jun 30 '22 at 16:09
  • @YerbolSapar I think it's unlikely you'll save much effort. List elements simply point to an object, so a copy of the element is just a copy of the reference to the object. This isn't going to be sped up particularly. If you're doing a deep copy of objects, that might be different, but it's not clear you are. If you ARE doing a deep copy of objects then simply keeping a registry of them might help. – Nathaniel Ford Jun 30 '22 at 16:26
  • 1
    It seems you're asking a question with the intent to make something not just simpler, but also faster, while any working solution would actually create more overhead and, although shorter, wouldn't necessarily be simpler from a user's perspective. It appears that you're looking for reusable masks or structured compound indices - but constructing these will add complexity and using them will likely cause overhead due to the additional indirection. Wouldn't it be clearer to just create a list-based class that has the behaviour you need on the index, if you still see a case for it? – Grismar Jun 30 '22 at 20:49
  • @Grismar Yes, it is true, I rather regarded Python mutability as an opportunity than consequence from the Python data structure to make the code faster. Now I am in process of rethinking my approach. Thank you for your advice and attention – Yerbol Sapar Jul 01 '22 at 10:33

1 Answers1

1

I think you have some confusion about where the mutability of lists that you're dealing with lays.

>>> class Obj:
...   name: str = None
...   def __init__(self, name):
...     self.name = name
...
>>> o = Obj("Athena")
>>> o.name
'Athena'
>>> a = Obj("Athena")
>>> b = Obj("Bacchus")
>>> c = Obj("Clotho")
>>>
>>> la = [a, b]
>>> lb = [a, b]
>>> ls = [la, lb]
>>> for sublist in ls:
...   for item in sublist:
...     print(f"{item.name}")
...
Athena
Bacchus
Athena
Bacchus
>>> ls[0][0] = c
>>> for sublist in ls:
...   for item in sublist:
...     print(f"{item.name}")
...
Clotho
Bacchus
Athena
Bacchus

This demonstrates that if you create the sublists individually, even if the objects that are the elements of that list are the same, the sublists are separate mutable objects, and changing one does not mean changing the others.

Consider, instead:

>>> la = [a, b]
>>> ls = [la, la]
>>> for sublist in ls:
...   for item in sublist:
...     print(f"{item.name}")
...
Athena
Bacchus
Athena
Bacchus
>>> ls[0][0] = c
>>> for sublist in ls:
...   for item in sublist:
...     print(f"{item.name}")
...
Clotho
Bacchus
Clotho
Bacchus

Here we see that if we use the same sublist, la, when we change one sublist we change another sublist. To take this a step further:

>>> lb = [a, c]
>>> ls[1] = lb
>>> for sublist in ls:
...   for item in sublist:
...     print(f"{item.name}")
...
Athena
Bacchus
Athena
Clotho
>>> ls[0][0] = d
>>> for sublist in ls:
...   for item in sublist:
...     print(f"{item.name}")
...
Demosthenes
Bacchus
Athena
Clotho

Here, we've pointed the second element of the outer list to a new list. Now when we change an element of the first sublist, it does not change the second sublist. This makes sense, because these are different lists.

The upshot here is that each element of a list points to a single object. Multiple elements can point to the same object. So, for your outer list, you can have multiple elements point to the same object, a sublist, and for any such sublist changes to that sublist will be visible in any other element of the outer list that points to the same sublist object. But only so long as that is the case.

And this is where your reasoning in your question doesn't quite make sense:

>>> lb = [a, b]
>>> ls = [la, lb, la]
>>> for sublistA in ls:
...   for sublistB in ls:
...     print("Equal") if sublistA is sublistB else print("Not equal")
...
Equal
Not equal
Equal
Not equal
Equal
Not equal
Equal
Not equal
Equal

Here we see that although la and lb are equivalent, they are not the same object. And really, all you're doing in your complicated example is shuffling around where you add the sublist object to your outer list. Something akin to:

>>> ls = [la, lb, lb, la]
>>> ls[0][0] = c
>>> for sublist in ls:
...   for item in sublist:
...      print(f"{item.name}")
...
Clotho
Bacchus
Athena
Bacchus
Athena
Bacchus
Clotho
Bacchus

This has nothing to do with the 'groups' you're putting them in, it's just that each element in the outer list is referencing a particular list object. That is where the 'implicit tracking' is occurring, and you can get at that information by using a comparison as I did. Because a list is not hashable, it's difficult to do a reverse lookup dictionary without creating a wrapper for the list, and it's expensive to do the object comparison every time (and not the difference between is and == in comparing two objects, especially lists!). Apart from those two options - keeping a separate variable that tracks the indices of each unique object (in this case, each sublist that has itself multiple elements) - there isn't a lot you can do that would be avoiding the work you're trying to avoid.

Nathaniel Ford
  • 20,545
  • 20
  • 91
  • 102
  • Thank you for your answer, it contributes to understanding of the subject what Python mutable is. You are right that 'implicit tracking is occurring when referencing a particular list object'. About the groups purpose, the question implies the possibility of exploiting the Python mutability for control over the groups of objects by keeping the same ID for particular sublists, i.e., `mutables()` is supposed to assign a group to the same ID. To add more clarity on that I have added the roles of IDs to a question. – Yerbol Sapar Jul 01 '22 at 05:57