L'aléatoire est trop logique

Le problème exposé dans ce sujet a été résolu.

Bonjour,

À la base j'ai ce code :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# -*- coding : utf-8 -*-

from random import randint

class Terrain:

    def __init__(self, largeur, hauteur):

        self.largeur = largeur
        self.hauteur = hauteur

        self.terrain = [[0] * self.largeur] * self.hauteur

    def display(self):

        for ligne in self.terrain:
            print(ligne)

    def place_mines(self, nombre=10):

        for y in range(self.hauteur):

            for x in range(self.largeur):

                self.terrain[y][x] = 9 if randint(0, 9) == 9 else 0

t = Terrain(6, 6)
t.display()
print("----------------")
t.place_mines()
t.display()

J'aimerais obtenir quelque chose comme ça, où les 9 sont disposés aléatoirement à travers le Terrain, qu'il puisse y en avoir plusieurs par lignes/colones:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
[0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0]
----------------
[9, 0, 0, 9, 0, 0]
[0, 0, 0, 0, 9, 0]
[9, 0, 0, 9, 0, 0]
[9, 0, 0, 0, 0, 9]
[0, 9, 0, 0, 0, 0]
[0, 0, 9, 0, 0, 0]

Mais à la place j'obtiens toujours les 9 sur une même colonne, ou pas de 9du tout (ce qui est quand même assez improbable au vu du nombre de case):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
[0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0]
----------------
[9, 0, 0, 0, 0, 0]
[9, 0, 0, 0, 0, 0]
[9, 0, 0, 0, 0, 0]
[9, 0, 0, 0, 0, 0]
[9, 0, 0, 0, 0, 0]
[9, 0, 0, 0, 0, 0]

Ai-je mal compris un fonctionnement du module random ? De ce je pense comprendre, je ne suis pas censé obtenir le résultat que j'ai, donc il y a probablement une erreur dans mon raisonnement mais où (telle est la question :p ) ?

Merci d'avance :)

+0 -0

Salut.

Le problème vient de la façon dont tu initialises l'attribut terrain :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
>>> from pprint import pprint
>>> array = [[0] * 10] * 10
>>> pprint(array)
[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]
>>> array[4][5] = 1
>>> pprint(array)
[[0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 1, 0, 0, 0, 0]]

En gros, ton terrain est une liste de 6 références sur la même liste.

Correction rapide :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
>>> array = [[0] * 10 for _ in range(10)]
>>> array[4][5] = 1
>>> pprint(array)
[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]
+3 -0

C'est un piège connu et une de mes questions vaches favorites en entretien d'embauche.

En gros: tout est passé par référence. Tout le temps. Donc quand tu fais [plop] * 10, tu ne dupliques pas l'objet plop (y'a pas de copie implicite) mais juste la référence contenue par la liste [plop].

1
2
3
4
5
6
7
>>> plop = [1, 2, 3]
>>> ma_liste = [plop] * 10
>>> ma_liste[1] is ma_liste[5]
True
>>> ma_liste = [0] * 10
>>> ma_liste[1] is ma_liste[5]
True
+3 -0

Au contraire le dernier exemple est juste génial et exprime vraiment bien le passage par référence (que j'avais visiblement mal compris). Que ce soit des immuables ou des muables, le résultat est le même: tous les éléments de la liste vont pointer vers le même objet (ce qui n'est pas facile à voir pour les immuables).

À l'inverse, ton exemple dépend justement de l'implémentation car ce que tu mets à gauche ne sera pas une copie de la référence. Le problème c'est que ça ne se voit pas avec des immuables (il y a sûrement une histoire d'optimisation derrière), mais ça se voit direct avec une liste:

1
2
lst = [[1, 2] for _ in range(10)]
print(lst[0] is lst[5])
+1 -0

En fait c'est exactement ce que j'avais en tête : quel que soit plop (muable, immuable, constante entière ou whatever), [plop] * x sera toujours une liste de x références sur plop. D'où ces 2 exemples.

Que certaines implémentations produisent le même resultat avec des entiers et une autre syntaxe n'entre pas en considération, ici l'exemple sert simplement à illustrer comment fonctionne cet opérateur et le caractère systématique de ce phénomène.

Du coup je ne vois pas tellement en quoi mon exemple n'est pas valable…

+1 -0

Bien sûr, lorsque l'on a affaire à une liste d'immutables, il est moins immédiat de faire apparaître qu'il s'agit de multiples références sur le même objet. Ici nohar l'a bien montré, mais ça aurait justement pu avoir une autre raison, ce qu'il me parait important de souligner.

J'ai simplement dit que l'interprétation du test ma_liste[1] is ma_liste[5] peut prêter à confusion, et du coup l'exemple est nettement moins parlant. Je me suis peut-être mal exprimé: l'exemple en soi est peut-être "juste", c'est sa démonstration qui me gêne (bon, vu qu'il n'y a pas de démonstration sans ambiguïté possible, je pense qu'on peut dire que l'exemple pose problème).

D'ailleurs, en relisant, le premier exemple me gêne également, parce que la construction explicite d'une liste en comprehension donne exactement le même résultat :

1
2
3
4
>>> plop = [1,2,3]
>>> l = [plop for _ in range(10)]
>>> l[1] is l[5]
True

Du coup, le premier exemple est finalement encore pire que le second, parce que ce n'est pas implementation defined ici.

+1 -0
1
2
3
4
>>> plop = [1,2,3]
>>> l = [plop for _ in range(10)]
>>> l[1] is l[5]
True

Du coup, le premier exemple est finalement encore pire que le second, parce que ce n'est pas implementation defined ici.

yoch

Wow wow wow, ne nous emballons pas, je n'ai jamais cherché à mettre les deux syntaxes (opérateur * et liste en intension) en comparaison, mais je vois où tu veux en venir. Voici donc mon explication revue et corrigée pour mieux illustrer le problème.

L'expression x = [[1, 2, 3]] * 3 va d'abord évaluer les opérandes avant de calculer la multiplication, donc elle est strictement équivalente à ceci :

1
2
tmp = [[1, 2, 3]]
x = tmp * 3

Quant à l'expression tmp = [[1, 2, 3]], qui est une liste explicite, celle-ci est strictement équivalente à :

1
2
tmp_ = [1, 2, 3]
tmp = [tmp_]

Ce qui veut dire que x = [[1, 2, 3]] * 3, en supprimant un intermédiaire, est parfaitement équivalent à :

1
2
tmp = [1, 2, 3]
x = [tmp] * 3

Soit une liste contenant trois références sur l'objet que j'ai nommé tmp, et c'est d'ailleurs parfaitement équivalent à :

1
2
tmp = [1, 2, 3]
[tmp for _ in range(3)]

Qui revient à l'exemple du dernier post de yoch.

Par contre, l'expression x = [[1, 2, 3] for _ in range (3)] n'est pas évaluée dans le même ordre. L'expression à gauche du for n'est pas évaluée avant que la liste en intension soit créée, mais elle l'est à chaque tour de boucle du for, ce qui veut dire que la boucle est construite grosso-modo comme ceci :

1
2
3
4
tmp = []
for _ in range(3):
   tmp.append([1, 2, 3])
x = tmp

Ce qui signifie que les éléments de x sont construits indépendamment au fur et à mesure qu'ils sont ajoutés à la liste.

+2 -0

Wow wow wow, ne nous emballons pas, je n'ai jamais cherché à mettre les deux syntaxes (opérateur * et liste en intension) en comparaison, mais je vois où tu veux en venir.

nohar

Ah, bah ça a été exactement mon sentiment, peut-être ceci explique cela.

En tout cas merci et bravo pour l'explication très détaillée de la question. :)

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte