IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Vous êtes nouveau sur Developpez.com ? Créez votre compte ou connectez-vous afin de pouvoir participer !

Vous devez avoir un compte Developpez.com et être connecté pour pouvoir participer aux discussions.

Vous n'avez pas encore de compte Developpez.com ? Créez-en un en quelques instants, c'est entièrement gratuit !

Si vous disposez déjà d'un compte et qu'il est bien activé, connectez-vous à l'aide du formulaire ci-dessous.

Identifiez-vous
Identifiant
Mot de passe
Mot de passe oublié ?
Créer un compte

L'inscription est gratuite et ne vous prendra que quelques instants !

Je m'inscris !

Mathématiques et Python - Apprendre à créer des générateurs aléatoires en Python
Un tutoriel de Denis Hulo

Le , par User

18PARTAGES

15  0 
Bonjour,

Je vous présente un nouvel article :

Tutoriel pour apprendre à réaliser des simulations aléatoires en Python
Le module random met en œuvre des générateurs de nombres pseudo-aléatoires pour différentes distributions (loi uniforme, loi normale, etc.).

Après avoir présenté quelques fonctions importantes de ce module, notre objectif sera de les utiliser pour réaliser différentes simulations (jeu de pile ou face, simulation de variable aléatoire, etc.).

Pour conclure, nous proposerons de créer notre propre générateur de nombres pseudo-aléatoires et de le tester à l'aide d'une méthode de Monte-Carlo.

Bonne lecture
Vous avez lu gratuitement 2 321 articles depuis plus d'un an.
Soutenez le club developpez.com en souscrivant un abonnement pour que nous puissions continuer à vous proposer des publications.

Une erreur dans cette actualité ? Signalez-nous-la !

Avatar de wiztricks
Expert éminent sénior https://www.developpez.com
Le 02/06/2025 à 17:30
Salut,

Bel effort... mais 3 remarques sur la dernière partie (le PRNG).
D'abord un petit bug, vous intialisez vos paramètres par défaut via:
Code : Sélectionner tout
        def __init__(self, m=2147483648, a=1103515245, c=12345, s=int(time.time())):
L'intention semble être d'initialiser, par défaut, X0 avec le retour de time.time à l'instant de l'appel.
Dit autrement, si j'écris:
Code : Sélectionner tout
1
2
>>> u = Random()
>>> v = Random()
la suite de nombres aléatoires générée par u et v devrait être différente.
Hélas, elle sera ici identique.
La valeur par défaut sera calculée lorsque l'interpréteur définit la fonction (et non lorsqu'elle sera appelée).

La qualité du générateur dépend des valeurs de m, a et c comme mentionné dans l'article de wikipédia sur ce type de générateurs.

Il est préférable d'utilisée des valeurs "bien construites" et déjà testées.. et si on ne teste pas la différence d'aléas entre plusieurs valeurs, on ne proposera peut être pas de les changer n'importe comment.

Du coup, on teste le code et non le "degré d'aléatoire" du générateur.
A partir du moment, où on doit choisir des valeurs déjà testées (m, a, c,...) pour un tel générateur, on testera que le code et non le générateur (ce qui est beaucoup plus compliqué...).

Enfin, utiliser un générateur à la place d'une classe serait plus concis et donnerait (compte tenu des remarques précédentes):
Code : Sélectionner tout
1
2
3
4
5
6
7
8
def random(s=None):
    m=2147483648
    a=1103515245
    c=12345
    s = s or int(time.time())
    while True:
        s = (a * s + c) % m
        yield s/m

- W
0  0 
Avatar de User
Rédacteur/Modérateur https://www.developpez.com
Le 02/06/2025 à 19:47
Bonjour,

Citation Envoyé par wiztricks
...
la suite de nombres aléatoires générée par u et v devrait être différente.
Hélas, elle sera ici identique.
La valeur par défaut sera calculée lorsque l'interpréteur définit la fonction (et non lorsqu'elle sera appelée).
En effet, ça m'a surpris. Je vais corriger avec un code de ce genre inspiré du vôtre :

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
    def __init__(self, m=2147483648, a=1103515245, c=12345, s=None):
        # m : module ; a : multiplicateur; c : incrément; s : valeur initiale ou graine (seed)
        # Si les arguments de la fonction sont omis, on passe les mêmes valeurs que pour le générateur d'UNIX

        # mise à jour des attributs avec les valeurs passées en argument
        self.m = m
        self.a = a

        # self.x = s or int(time.time())
        self.x = s if s else int(time.time())
pour être un peu plus lisible, merci à vous.

------------------------------------------------

J'ai aussi une autre méthode init_rand() pour réinitialiser le générateur :

Code : Sélectionner tout
1
2
3
4
5
    def init_rand(self, s=None):
        # méthode permettant d'initialiser le générateur de nombres aléatoires

        # mise à jour de x avec la valeur initiale ou la graine s
        self.x = s if s else int(time.time())
Citation Envoyé par wiztricks
La qualité du générateur dépend des valeurs de m, a et c comme mentionné dans l'article de wikipédia sur ce type de générateurs.

Il est préférable d'utilisée des valeurs "bien construites" et déjà testées.. et si on ne teste pas la différence d'aléas entre plusieurs valeurs, on ne proposera peut être pas de les changer n'importe comment.
Oui, je laisse cette possibilité. J'ai pensé que ça donnerait plus de souplesse pour changer les paramètres, mais je précise bien :

Dans notre cas, on va choisir de créer un générateur congruentiel linéaire, car il est facile à mettre en œuvre et généralement de bonne qualité sous réserve bien sûr de choisir les bons paramètres.
...
...on connaît les critères sur les nombres a, c et m qui vont permettre d’obtenir une période maximale (égale à m) :
https://fr.wikipedia.org/wiki/G%C3%A...incr%C3%A9ment

et plus loin je précise aussi :

Les valeurs par défaut de m, a et c sont également celles utilisées par le générateur d'UNIX : 231, 1103515245 et 12345.


avec d'autres liens :

https://fr.wikipedia.org/wiki/G%C3%A...9aire#Exemples
https://en.wikipedia.org/wiki/Linear..._in_common_use

---------------------

Pour la partie test du générateur, comme je le mentionne, je suis aussi conscient des limites de la méthode. j'essaierai de vous répondre prochainement plus en détail sur ce point.

Citation Envoyé par wiztricks
Enfin, utiliser un générateur à la place d'une classe serait plus concis et donnerait (compte tenu des remarques précédentes):
J'ai choisi d'utiliser une classe car comme je le propose elle permet de créer simplement d'autre méthodes basées sur le générateur rand() comme rand_int(lower,upper) (un peu comme pour le module random) :

La classe Random contient également une méthode init_rand() pour initialiser le générateur de nombres aléatoires une fois l'objet créé, et une méthode rand_int(lower,upper) permettant de générer un entier au hasard. Libre à chacun d'ajouter d'autres méthodes à cette classe.
D'autre part personnellement je trouve le code plus lisible.

En tous cas merci pour vos remarques.
0  0 
Avatar de papajoker
Expert confirmé https://www.developpez.com
Le 02/06/2025 à 21:51
bonjour

De, sans intéret à plus ...

Puisque c'est une classe , un petit moyen de gagner une nano seconde et quelques octets
Code : Sélectionner tout
__slots__ = ("x", "a", "c", "m")
Puisque c'est une classe, (pourquoi pas ...) j'ai testé :
Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
... 
    def __enter__(self): 
        return self.rand 
 
    def __exit__(self, exception_type, exception_value, exception_traceback): 
        pass 
 
with Random() as rand: 
    print(rand()) 
    print(rand())
On pousse le bouchon
Code : Sélectionner tout
1
2
3
4
5
6
    def __call__(self): 
        return self.rand() 
 
rand = Random() 
print(rand()) 
print(rand())
Puisque c'est une classe (et que le mot "générateur" est dans le titre), en faire un véritable en langue python
Code : Sélectionner tout
1
2
3
4
5
    def __iter__(self): 
        return self 
 
    def __next__(self): 
        return self.rand()
Puisque l'on parle de "test" et que maintenant j'ai un "vrai" générateur, j'ai (quand même) testé si c'est bien si aléaloire que cela (on peut rêver avec ces valeurs)
Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
MAX = 100_000 
tests = [-1] * MAX 
 
for i, value in enumerate(Random()): 
    if i >= MAX: 
        break 
    if value in tests: 
        raise Exception("BAD Random, at", i, value) 
    tests[i] = value 
    if i % 5_000 == 0:     # je n'aime pas dormir devant une console vide 
        print(i, value)
Même pas vu si mon raise fonctionne

Code : Sélectionner tout
1
2
3
for i, value in enumerate(Random(m=3648, a=245, c=45)): 
... 
Exception: ('BAD Random, at', 576, 0.15158991228070176)
yes
0  0 
Avatar de User
Rédacteur/Modérateur https://www.developpez.com
Le 03/06/2025 à 11:40
Bonjour,

Ah oui, merci

Comme on parle de réaliser des simulations l'aspect "sécurité" est moins primordial mais pour finir j'ai quand même choisi de définir les paramètres du générateur en dur dans le code :

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Random():
    # classe permettant de définir le générateur congruentiel linéaire (LCG)
    
    def __init__(self, s=None):
        # s : valeur initiale ou graine (seed)

        # self.x = s or int(time.time())
        self.x = s if s else int(time.time())
        
    def rand(self):
        # méthode permettant de générer un nombre aléatoire entre 0 et 1

        # paramètres du générateur :  m : module ; a : multiplicateur; c : incrément
        m=2147483648; a=1103515245; c=12345
        
        # formule de récurrence pour le LCG : Xn+1 = (aXn + c) mod m
        self.x = (a * self.x + c) % m

        # renvoie la nouvelle valeur pour x divisé par le module m
        return self.x/m
Cdlt
0  0 
Avatar de Sve@r
Expert éminent sénior https://www.developpez.com
Le 03/06/2025 à 19:47
Bonjour
Citation Envoyé par User  Voir le message
En effet, ça m'a surpris.

C'est parce que, contrairement à ce que laisse sous-entendre l'intuition, un paramètre par défaut est créé non pas à l'appel de la fonction mais à la définition de la fonction.

Démonstration
Code python : Sélectionner tout
1
2
3
4
5
6
7
8
class toto: 
	def __init__(self): print("Création toto") 
  
def fct(param=toto()): pass 
  
print("Début") 
fct() 
print("Fin")
Si le paramètre était créé à l'appel de la fonction, on aurait "Début" puis "Création toto" puis "Fin". Mais dans les faits, on a "Création toto" puis "Début" puis "Fin" montrant que l'objet est créé au maximum avant le premier print. Rajouter d'autres print avant et après la définition de la fonction te montrera que cela se passe bel et bien à la définition de la fonction.

Dans la plupart des cas, ce comportement (qui est fait pour optimiser la vitesse) ne pose pas de souci. Les soucis n'arrivent que quand le paramètre par défaut est soit un objet mutable (ie une liste) soit un évènement calculé (ton cas).

D'où la solution pour créer le comportement désiré : positionner l'argument par défaut à une valeur particulière immuable (ie None) puis tester cette valeur dans la fonction.
0  0 
Avatar de papajoker
Expert confirmé https://www.developpez.com
Le 03/06/2025 à 21:26
Citation Envoyé par User Voir le message
quand même choisi de définir les paramètres du générateur en dur dans le code :
Je serais plus pour un compromis.

Code : Sélectionner tout
1
2
3
4
5
6
7
8
class Random:
    MODULE = 2147483648
    MULTIPLICATEUR = 1103515245
    INCREMENT = 12345

    def rand(self):
        self.m, self.a, self.c = self.MODULE, self.MULTIPLICATEUR, self.INCREMENT
        # ou bien sûr, plus logique : utiliser directement ces constantes au lieu de créer des variables m, a et c
De plus, le nom des constantes est plus parlant.
Un dev python doit comprendre que se sont des constantes donc pas de raison d'y toucher. Et on laisse une possibilité...

-----------------------------
EDIT
-----------------------------

Puisque j'ai la possibilité de modifier, autre test complémentaire, ici je ne cherche plus l'unicité mais l'uniformité sur un "grand" nombre de tirages.

J'ai écrit ce test sur uniquement Random.MODULE (et j'ai laissé le code pour le transformer en itérateur)
ps: sous réserve d'un matheux (car je ne le suis pas), ma logique est peut-être fausse ?
Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
count = 0
PAS_BON = 40    # normalement un pourcentage
for n in range(1000):
    MAX = 1000
    tests = [0] * 10

    for i, value in enumerate(Random()):
        if i >= MAX:
            break
        val = int((value * 10) // 1)
        tests[val] += 1
    v = max(tests) - min(tests)
    if v > PAS_BON:
        count += 1
        print(f"  {Random.MODULE:<14} {str(tests):60} {v}", "*" * 5)
    Random.MODULE = random.randint(5_000_000, 2_147_483_648) # OUI random.randint() !
print(count, "/", n + 1, "au dessus de ", PAS_BON)
résultat, j'ai à peu près toujours environ +10% avec un trop grand écart. Et généralement + de 1% pour écart de 50 max
Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
  1822770248     [112, 92, 97, 128, 95, 110, 78, 73, 109, 106]                55 *****
  994122736      [113, 82, 146, 75, 104, 104, 93, 94, 124, 65]                81 *****
  1104348338     [107, 100, 127, 97, 88, 108, 64, 111, 103, 95]               63 *****
  165707073      [38, 78, 129, 117, 169, 52, 78, 104, 91, 144]                131 *****
...

121 / 1000 au dessus de  40
15 / 1000 au dessus de  50
Je n'ai pas la moindre idée que nous pouvons donner à PAS_BON A partir de quand peut-on dire que nous avons un mauvais aléatoire ??? (si ma logique n'est pas trop mauvaise...)
0  0 
Avatar de User
Rédacteur/Modérateur https://www.developpez.com
Le 04/06/2025 à 7:48
Bonjour,

Citation Envoyé par Sve@r  Voir le message
Bonjour

C'est parce que, contrairement à ce que laisse sous-entendre l'intuition, un paramètre par défaut est créé non pas à l'appel de la fonction mais à la définition de la fonction.

Démonstration
Code python : Sélectionner tout
1
2
3
4
5
6
7
8
class toto: 
	def __init__(self): print("Création toto") 
  
def fct(param=toto()): pass 
  
print("Début") 
fct() 
print("Fin")
Si le paramètre était créé à l'appel de la fonction, on aurait "Début" puis "Création toto" puis "Fin". Mais dans les faits, on a "Création toto" puis "Début" puis "Fin" montrant que l'objet est créé au maximum avant le premier print. Rajouter d'autres print avant et après la définition de la fonction te montrera que cela se passe bel et bien à la définition de la fonction.

Dans la plupart des cas, ce comportement (qui est fait pour optimiser la vitesse) ne pose pas de souci. Les soucis n'arrivent que quand le paramètre par défaut est soit un objet mutable (ie une liste) soit un évènement calculé (ton cas).

D'où la solution pour créer le comportement désiré : positionner l'argument par défaut à une valeur particulière immuable (ie None) puis tester cette valeur dans la fonction.

Merci pour ces précisions éclairantes, l'intuition peut en effet nous induire en erreur. J'ai fait un raccourci dans ma tête sans vraiment analyser le problème.

@papajoker

Je pense que je vais laisser comme ça pour les variables pour me baser sur la formule de récurrence.

Merci pour tes propositions.
0  0