---
# System prepended metadata

title: Python Stuff

---

# Python Stuff

J'ai parfois utilisé poe.com et mistral hein ;)

Un notebook python entierement en javascript : https://jupyter.4x.re/lab/index.html

[TOC]




## Annotations de type : 

Pour les annotations de types vers soit même : 

Auto-référence (https://realpython.com/python-type-self/)

```python
class Stack:
    def __init__(self) -> None:
        self.items: list[Any] = []

    def push(self, item: Any) -> Self:
        self.items.append(item)
        return self
```

Voir la doc https://peps.python.org/pep-0673/


#### Mypy (ou autre)

Avec extension :  https://marketplace.visualstudio.com/items?itemName=ms-python.mypy-type-checker

J'utilise ce fichier mypy.ini 

```properties
[mypy]
check_untyped_defs = True
disallow_any_generics = True
disallow_untyped_calls = True
disallow_untyped_defs = True
ignore_missing_imports = True
strict_optional = False
explicit_package_bases=True
```

Voir ici pour le linting en python : https://code.visualstudio.com/docs/python/linting

Et ici : https://realpython.com/python-type-checking/

### Gestion de projet

Un truc assez sympa aussi est d'utiliser des choses comme black et des pre-commit hooks. Sur un projet j'utilise ça par exemple : 

```sh
#!/bin/bash
autoflake --in-place --remove-unused-variables -r --remove-all-unused-imports .
mypy --non-interactive --install-types
pre-commit run --all-files
```

+ autoflake enleve les imports supperflus
+ mypy fait la vérif statique
+ [pre-commit](https://github.com/pre-commit/pre-commit-hooks) fait une vérification et clean les fichiers. Voir ici : https://github.com/pre-commit/pre-commit-hooks

Voila mon fichier de pre-commit `.pre-commit-config.yaml`: 

```yaml
repos:
    - repo: https://github.com/pre-commit/pre-commit-hooks
      rev: v4.4.0
      hooks:
        - id: check-added-large-files
        - id: check-case-conflict
        - id: check-docstring-first
        - id: detect-private-key
        - id: fix-byte-order-marker
    - repo: https://github.com/psf/black
      rev: 23.7.0
      hooks:
        - id: black
```


### Décorateur: 

Voir ici : https://realpython.com/primer-on-python-decorators/

En Python, un décorateur est une fonction qui prend une autre fonction en entrée et renvoie une nouvelle fonction qui encapsule la fonction d'origine. Les décorateurs sont souvent utilisés pour ajouter des fonctionnalités ou modifier le comportement d'une fonction existante, sans avoir à modifier son code source. En Java, une annotation est une métadonnée qui est attachée à une déclaration de code (par exemple, une méthode, une classe ou un champ), et qui peut être utilisée pour fournir des informations supplémentaires sur cette déclaration.

Les décorateurs et les annotations sont des concepts différents, mais ils peuvent être utilisés de manière similaire pour ajouter des fonctionnalités ou modifier le comportement du code.

Pour écrire votre propre décorateur en Python, vous pouvez suivre les étapes suivantes :

1. Définissez une fonction qui prend une autre fonction en entrée et renvoie une nouvelle fonction.
2. Dans la nouvelle fonction, ajoutez le code qui encapsule la fonction d'origine et ajoute les fonctionnalités ou modifie le comportement souhaité.
3. Renvoyez la nouvelle fonction.

Voici un exemple simple de décorateur qui ajoute une fonctionnalité de journalisation à une fonction existante : 

```python
def log_function_call(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__} with arguments {args}, {kwargs}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned {result}")
        return result
    return wrapper

@log_function_call
def add_numbers(a, b):
    return a + b
```
Dans cet exemple, le décorateur `log_function_call` est appliqué à la fonction `add_numbers` en utilisant la syntaxe `@log_function_call`. Lorsque la fonction `add_numbers` est appelée, le décorateur est exécuté et renvoie une nouvelle fonction qui encapsule la fonction d'origine et ajoute les messages de journalisation.


#### @staticmethod

En Python, le décorateur `@staticmethod` est utilisé pour définir une méthode statique dans une classe. Une méthode statique est une méthode qui ne requiert pas d'instance de la classe pour être appelée, et qui ne reçoit pas l'objet `self` en premier argument.

Le décorateur `@staticmethod` fonctionne en ajoutant une méthode à la classe, et en spécifiant que cette méthode est une méthode statique. Il est appliqué à une fonction en utilisant la syntaxe suivante :
```python
class MyClass:
    @staticmethod
    def my_static_method():
        # code de la méthode statique
```
 Dans cet exemple, le décorateur `@staticmethod` est appliqué à la fonction `my_static_method` pour en faire une méthode statique de la classe `MyClass`. La méthode statique peut ensuite être appelée en utilisant la syntaxe suivante :
```python
MyClass.my_static_method()
```
Le décorateur `@staticmethod` fonctionne en modifiant le dictionnaire de la classe pour ajouter la fonction décorée en tant que méthode statique. Il est défini dans le module `functools` de la bibliothèque standard de Python, et est implémenté en utilisant le mot-clé `def` pour définir une fonction qui prend une fonction en entrée et renvoie une fonction statique.

Voici un exemple de code qui montre comment le décorateur `@staticmethod` est implémenté en utilisant une fonction Python :
```python
def staticmethod(func):
    """
    Convert the function to a static method.

    A static method does not receive an implicit first argument.
    To declare a static method, use this idiom:

        class C:
            @staticmethod
            def f(arg1, arg2, ...): ...

    It can be called either on the class (e.g. C.f()) or on an instance
    (e.g. C().f()).  The instance is ignored except for its class.

    Static methods in Python are similar to those found in Java or C++.
    For a more advanced concept, see the classmethod builtin.
    """
    func.__self__ = None
    return func
```
Dans cet exemple, la fonction `staticmethod` prend une fonction en entrée et modifie son dictionnaire `__dict__` pour ajouter l'attribut `__self__`, qui est défini sur `None`. Cette modification indique que la fonction est une méthode statique, et que l'argument `self` n'est pas requis. La fonction décorée est ensuite renvoyée, et peut être utilisée comme une méthode statique de la classe.


# /!\ Le module functools (décorateur de cache, et autres)

Super pratique parce que pas mal de décorateurs utile : 
+ pour le cache (@lru ...)
+ pour la gestion d'ordre (@totalordering)

Voir ici : https://docs.python.org/3/library/functools.html

Exemple : 

```python 
@cache
def factorial(n):
    return n * factorial(n-1) if n else 1

>>> factorial(10)      # no previously cached result, makes 11 recursive calls
3628800
>>> factorial(5)       # just looks up cached value result
120
>>> factorial(12)      # makes two new recursive calls, the other 10 are cached
479001600
```


# Les collections

### Iterable 

Voir https://docs.python.org/3/library/collections.abc.html

En Python, le type `Iterable` est défini dans le module `collections.abc` (abstrait base class) de la bibliothèque standard. Il s'agit d'une classe abstraite qui définit l'interface pour les objets qui peuvent être parcourus (itérés) en utilisant une boucle `for`.

La classe `Iterable` définit une seule méthode abstraite, `__iter__()`, qui doit être implémentée par les classes qui souhaitent être considérées comme itérables. Cette méthode doit renvoyer un objet itérateur, qui est un objet qui fournit une méthode `__next__()` pour obtenir la valeur suivante de l'objet itéré, et qui lève une exception `StopIteration` lorsqu'il n'y a plus de valeurs à itérer.

Voici un exemple de classe Python qui implémente l'interface `Iterable` en définissant une méthode `__iter__()` qui renvoie un objet itérateur :
```python
from collections.abc import Iterable

class MyIterable:
    def __init__(self, values):
        self.values = values
        self.index = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.index < len(self.values):
            result = self.values[self.index]
            self.index += 1
            return result
        else:
            raise StopIteration
```
Dans cet exemple, la classe `MyIterable` définit une méthode `__iter__()` qui renvoie l'objet lui-même, et une méthode `__next__()` qui renvoie la valeur suivante de l'objet itéré, ou lève une exception `StopIteration` si toutes les valeurs ont été itérées. La classe `MyIterable` peut ensuite être utilisée dans une boucle `for`, comme suit :
```python
my_iterable = MyIterable([1, 2, 3])
for value in my_iterable:
    print(value)
```
Le code ci-dessus imprime les valeurs 1, 2 et 3, qui sont les valeurs itérées de l'objet `my_iterable` .



# /!\ Les context managers : 

Voir : https://realpython.com/python-with-statement/

Super utiles aussi, on peut écrire les siens : 

```python 
with expression as target_var:
    do_something(target_var)
```

Il faut que les objets soit closable (ca ressemble au try with ressources en java mais c'est mieux fouttu)

Voir ici : https://realpython.com/python-with-statement/#creating-custom-context-managers

```java 
>>> class HelloContextManager:
...     def __enter__(self):
...         print("Entering the context...")
...         return "Hello, World!"
...     def __exit__(self, exc_type, exc_value, exc_tb):
...         print("Leaving the context...")
...         print(exc_type, exc_value, exc_tb, sep="\n")
...

>>> with HelloContextManager() as hello:
...     print(hello)
...
Entering the context...
Hello, World!
Leaving the context...
None
None
None
```


# Les classes

Voir mon cours. Les objets en "__" sont masqués (mangling)

```python
class MyClass:
    def __init__(self, value):
        self.__value = value

    def get_value(self):
        return self.__value

    def set_value(self, value):
        self.__value = value

my_instance = MyClass(5)
print(my_instance.__value) # affiche une erreur
```

## Propriétés 

En Python, les propriétés (properties) sont utilisées pour encapsuler l'accès aux attributs d'une classe et fournir une interface cohérente pour la lecture et l'écriture de ces attributs. Les propriétés permettent de définir des méthodes getter et setter pour les attributs d'une classe, tout en conservant une syntaxe simple pour l'accès à ces attributs.

Voici un exemple simple de classe Python qui utilise des propriétés pour encapsuler l'accès à l'attribut `value` :
```python
class MyClass:
    def __init__(self, value=0):
        self._value = value

    @property
    def value(self):
        """Get the value."""
        return self._value

    @value.setter
    def value(self, value):
        """Set the value."""
        if not isinstance(value, int):
            raise ValueError("Value must be an integer.")
        self._value = value
```
Dans cet exemple, la classe `MyClass` définit un attribut `_value` qui est initialisé avec une valeur par défaut de 0. La classe définit ensuite une propriété nommée `value` qui est utilisée pour encapsuler l'accès à l'attribut `_value`. La propriété `value` est définie en utilisant le décorateur `@property`, et les méthodes getter et setter sont définies en utilisant les décorateurs `@value.getter` et `@value.setter`, respectivement.

La méthode getter est définie pour renvoyer la valeur de l'attribut `_value`, et la méthode setter est définie pour vérifier si la valeur est un entier et mettre à jour l'attribut `_value` en conséquence.

Les propriétés respectent l'encapsulation en fournissant une interface cohérente pour l'accès aux attributs d'une classe, tout en cachant les détails d'implémentation. Les utilisateurs de la classe peuvent accéder à l'attribut `value` en utilisant une syntaxe simple, sans avoir à connaître les détails d'implémentation de la classe. Par exemple, l'attribut `value` peut être lu ou écrit comme suit :
```python
my_instance = MyClass(5)
print(my_instance.value) # affiche 5
my_instance.value = 10
print(my_instance.value) # affiche 10
```
Dans cet exemple, l'utilisateur de la classe n'a pas besoin de connaître l'existence de l'attribut `_value` pour accéder à la valeur encapsulée par la propriété `value`. Cela permet de modifier l'implémentation de la classe sans affecter le code des utilisateurs, ce qui est un principe clé de l'encapsulation.

En outre, les propriétés peuvent être utilisées pour fournir une logique supplémentaire pour la lecture et l'écriture des attributs, sans avoir à modifier le code des utilisateurs. Par exemple, la méthode setter peut être utilisée pour valider les valeurs entrantes, ou pour effectuer des calculs ou des transformations avant de mettre à jour l'attribut. De même, la méthode getter peut être utilisée pour renvoyer une valeur calculée à partir de plusieurs attributs, ou pour renvoyer une valeur mise en cache plutôt que de calculer à chaque fois.

## Dataclasses

Les `dataclasses` sont une fonctionnalité de Python introduite dans la version 3.7 qui fournissent une syntaxe simple pour définir des classes qui contiennent principalement des attributs. Les `dataclasses` fournissent automatiquement des méthodes spéciales telles que `__init__`, `__repr__`, et `__eq__`, ainsi que des méthodes de classe telles que `from_dict` et `to_dict`, ce qui rend leur utilisation particulièrement pratique pour définir des structures de données simples.

Voici un exemple simple de classe `dataclass` qui définit une structure de données pour représenter un point dans un espace à deux dimensions :
```python
from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float
```
Dans cet exemple, la décoration `@dataclass` est utilisée pour définir une classe `Point` qui contient deux attributs, `x` et `y`, qui sont tous deux de type `float`. Les `dataclasses` fournissent automatiquement une méthode `__init__` qui initialise les attributs à partir des arguments passés au constructeur, ainsi qu'une méthode `__repr__` qui renvoie une représentation lisible de l'objet.

Les `dataclasses` fournissent également des méthodes de classe pour convertir facilement les instances de `dataclass` en dictionnaires et vice versa. Par exemple, la méthode de classe `from_dict` peut être utilisée pour créer une instance de `Point` à partir d'un dictionnaire, comme suit :
```python
point_dict = {'x': 1.0, 'y': 2.0}
point = Point.from_dict(point_dict)
print(point) # affiche Point(x=1.0, y=2.0)
```
De même, la méthode `to_dict` peut être utilisée pour convertir une instance de `Point` en un dictionnaire, comme suit :
```python
point = Point(x=1.0, y=2.0)
point_dict = point.to_dict()
print(point_dict) # affiche {'x': 1.0, 'y': 2.0}
```
Les `dataclasses` fournissent également des fonctionnalités avancées telles que l'héritage et la définition de méthodes personnalisées, ce qui les rend très flexibles pour définir des structures de données complexes. En outre, les `dataclasses` peuvent être utilisées avec des outils tels que `pydantic` pour effectuer une validation de données supplémentaire.

#### Annexe (comment fonctionne @dataclass)

Le décorateur `@dataclass` est implémenté en utilisant une fonction qui prend une classe comme argument et renvoie une nouvelle classe qui contient les fonctionnalités automatiques fournies par `@dataclass`. La fonction qui implémente `@dataclass` est définie dans le module `dataclasses` de la bibliothèque standard de Python.

Lorsque `@dataclass` est appliquée à une classe, la fonction qui implémente `@dataclass` crée une nouvelle classe qui hérite de la classe d'origine et ajoute les méthodes spéciales et les méthodes de classe aux nouvelles classes. La nouvelle classe est ensuite renvoyée et utilisée à la place de la classe d'origine.

Voici un exemple simple qui montre comment un décorateur de classe peut être implémenté en Python :
```python
def my_decorator(cls):
    class MyDecoratedClass:
        def __init__(self, arg1, arg2):
            self.arg1 = arg1
            self.arg2 = arg2

        def my_method(self):
            print(f"arg1: {self.arg1}, arg2: {self.arg2}")

    return MyDecoratedClass

@my_decorator
class MyClass:
    pass
```
Dans cet exemple, le décorateur `my_decorator` est défini comme une fonction qui prend une classe en argument et renvoie une nouvelle classe qui contient une méthode `my_method`. Lorsque `@my_decorator` est appliqué à la classe `MyClass`, la fonction `my_decorator` est appelée avec `MyClass` comme argument, et la nouvelle classe `MyDecoratedClass` est renvoyée et utilisée à la place de `MyClass`.

Lorsque `MyClass` est instanciée, la nouvelle classe `MyDecoratedClass` est utilisée pour créer l'objet, et la méthode `my_method` est disponible sur l'objet, comme suit :
```python
my_instance = MyClass("hello", "world")
my_instance.my_method() # affiche "arg1: hello, arg2: world"
```
Le décorateur `@dataclass` fonctionne de manière similaire, mais il ajoute automatiquement des méthodes spéciales et des méthodes de classe à la classe, en fonction des annotations de type des variables de classe.

Voici un exemple de code qui montre comment `@dataclass` pourrait être implémenté en utilisant un décorateur de classe :
```python
from functools import wraps
from typing import Any

def dataclass(cls):
    @wraps(cls)
    class DataClass:
        def __init__(self, **kwargs):
            for name, value in kwargs.items():
                setattr(self, name, value)

        def __repr__(self):
            args = ", ".join([f"{name}={repr(getattr(self, name))}"
                              for name in cls.__annotations__])
            return f"{cls.__name__}({args})"

        def __eq__(self, other):
            if not isinstance(other, cls):
                return False
            return all([getattr(self, name) == getattr(other, name)
                       for name in cls.__annotations__])

        @classmethod
        def from_dict(cls, d):
            return cls(**d)

    return DataClass

@dataclass
class Point:
    x: float
    y: float

p1 = Point(x=1.0, y=2.0)
p2 = Point.from_dict({"x": 1.0, "y": 2.0})
print(p1) # affiche "Point(x=1.0, y=2.0)"
print(p1 == p2) # affiche "True"
```
Dans cet exemple, la fonction `dataclass` est définie comme un décorateur de classe qui prend une classe en argument et renvoie une nouvelle classe qui contient les méthodes spéciales et les méthodes de classe fournies par `@dataclass`. La nouvelle classe est ensuite renvoyée et utilisée à la place de la classe d'origine.

Lorsque `@dataclass` est appliqué à la classe `Point`, la fonction `dataclass` est appelée avec `Point` comme argument, et la nouvelle classe `DataClass` est renvoyée et utilisée à la place de `Point`. La nouvelle classe `DataClass` contient les méthodes spéciales `__init__`, `__repr__`, et `__eq__`, ainsi que la méthode de classe `from_dict`. Lorsque `Point` est instanciée, la nouvelle classe `DataClass` est utilisée pour créer l'objet, et les méthodes spéciales et la méthode de classe sont disponibles sur l'objet.

# Coroutines

https://docs.python.org/3/library/asyncio-task.html

# Considérations systèmes

## Allocation mémoire

Python est la spécification du langage. La gestion de la mémoire peut varier d'une implémentation à une autre.

Pour ce qui concerne CPython, les variables sont allouées sur la mémoire heap (tas) et non sur la mémoire stack (pile).

La mémoire heap est un espace de mémoire partagé utilisé pour stocker des objets de grande taille ou persistants, tels que les objets de grande taille ou les objets qui doivent être conservés pour une utilisation ultérieure. La mémoire heap est généralement plus lente à accéder que la mémoire stack, mais elle peut être plus flexible en ce qui concerne la taille des objets qu'elle peut stocker.

La mémoire stack, en revanche, est utilisée pour stocker les variables locales et les informations d'exécution, telles que les retours d'appel de fonction. La mémoire stack est plus rapide à accéder que la mémoire heap, car les données y sont stockées de manière ordonnée et structurée. Cependant, la mémoire stack a une taille limitée et peut déborder si des données sont empilées excessivement.

Les variables sont considérées comme des objets et sont gérées dynamiquement, ce qui signifie que leur taille peut varier au cours de l'exécution du programme. Cela implique que les variables doivent être stockées sur la mémoire heap, qui peut s'adapter à des variations de taille d'objet. En utilisant la mémoire heap pour gérer les variables, Python peut garantir la flexibilité nécessaire pour stocker des objets de taille variable, mais cela peut également avoir un impact sur les performances en termes de vitesse d'accès à la mémoire. Cependant, en général, ce n'est pas un problème majeur car la vitesse d'exécution du programme en Python est souvent limitée par d'autres facteurs tels que la complexité algorithmique.


## Bytecode et Paquet dis

Python utilise du bytecode pour accélérer l'exécution des programmes. Bien que Python soit un langage interprété, cela ne signifie pas que le code source soit interprété ligne par ligne à chaque exécution. Au lieu de cela, le code source Python est compilé en bytecode, qui est un format intermédiaire entre le code source et la machine.

Lorsqu'un programme Python est exécuté, l'interpréteur Python charge le bytecode en mémoire et l'exécute, ce qui est beaucoup plus rapide que l'interprétation du code source à chaque exécution. De plus, le bytecode peut être enregistré sur le disque dur et chargé en mémoire à chaque exécution ultérieure, ce qui accélère encore plus l'exécution du programme.

En utilisant le bytecode, Python peut tirer parti des avantages de la compilation (comme la vérification de la syntaxe et la génération de code plus efficace) tout en conservant la flexibilité de l'interprétation. Cela fait de Python un langage de programmation souple et facile à utiliser pour les développeurs.

Le paquet dis (disassembler) est un module intégré en Python qui permet de décompiler et d'afficher le bytecode Python généré à partir d'une fonction Python. Il peut être utile pour comprendre comment fonctionne le bytecode Python et comment les instructions Python sont traduites en opérations machine sous-jacentes.

Pour utiliser le module dis, vous devez simplement importer le module et appeler la fonction dis.dis en fournissant la fonction Python que vous souhaitez décompiler en argument. Voici un exemple simple :

```python
import dis
def f(a,b) :
    while (a < b) :
        a = a /2
dis.dis(f)
```

Ce qui vous donnera un aperçu du bytecode généré pour la fonction f :

```asm
  4           0 LOAD_FAST                0 (a)
              2 LOAD_FAST                1 (b)
              4 COMPARE_OP               0 (<)
              6 POP_JUMP_IF_FALSE       12 (to 24)

  5     >>    8 LOAD_FAST                0 (a)
             10 LOAD_CONST               1 (2)
             12 BINARY_TRUE_DIVIDE
             14 STORE_FAST               0 (a)

  4          16 LOAD_FAST                0 (a)
             18 LOAD_FAST                1 (b)
             20 COMPARE_OP               0 (<)
             22 POP_JUMP_IF_TRUE         4 (to 8)

  6     >>   24 LOAD_FAST                0 (a)
             26 RETURN_VALUE
```

Ce code est un exemple de bytecode Python généré par la fonction f dont l'utilité est discutable.

+ `LOAD_FAST` charge une variable locale dans la pile d'opérandes. Les variables a et b sont chargées en utilisant les opérations LOAD_FAST 0 (a) et `LOAD_FAST 1 (b)`, respectivement.
+ `COMPARE_OP` effectue une opération de comparaison entre les deux derniers éléments de la pile d'opérandes et pousse un résultat booléen sur la pile. Dans ce cas, COMPARE_OP 0 (<) effectue une comparaison de type "inférieur à" entre les valeurs de a et b.
+ `POP_JUMP_IF_FALSE` et `POP_JUMP_IF_TRUE` sont des opérations de contrôle de flux qui consomment le résultat booléen sur la pile d'opérandes et sautent à l'étiquette spécifiée si la valeur est fausse ou vraie, respectivement. Par exemple, `POP_JUMP_IF_FALSE 14 (to 28)` saute à l'étiquette `28` si le résultat de la comparaison est faux.
+ `LOAD_CONST` charge une valeur constante dans la pile d'opérandes.
+ `BINARY_TRUE_DIVIDE` effectue une division flottante entre les deux derniers éléments de la pile d'opérandes et pousse le résultat sur la pile.
+ `STORE_FAST` enregistre une valeur dans une variable locale.
+ `RETURN_VALUE` renvoie la valeur en haut de la pile d'opérandes comme résultat de la fonction.

L'utilisation de dis peut être utile pour comprendre comment le code Python est interprété par l'interpréteur Python et peut également être utile pour optimiser les performances du code en identifiant les opérations coûteuses ou inefficaces. Cependant, il est généralement conseillé d'utiliser dis uniquement pour des fins de débogage et de développement, car le bytecode généré peut changer entre les versions de Python et ne doit pas être considéré comme une interface stable pour l'interpréteur Python.

On peut précompiler un programme Python en utilisant l'outil py_compile. Cet outil génère un fichier bytecode Python qui peut être exécuté plus rapidement que le code source original en utilisant l'interpréteur Python. Par exemple :

```python
import py_compile
py_compile.compile("mon_programme.py")
```

Le fichier bytecode généré aura l'extension .pyc et peut être exécuté à l'aide de la commande python suivie du nom du fichier .pyc :

```
python mon_programme.pyc
```

Il est important de noter que le code précompilé n'est pas protégé contre l'inspection ou la modification. Il peut donc être décompilé facilement pour récupérer le code source original.


## Compilation sous forme d'exécutable Windows, Mac ou Linux

Vous pouvez créer un fichier exécutable à partir d'un programme Python en utilisant des outils tels que cx_Freeze, PyInstaller ou py2exe.

Ces outils convertissent votre code Python en un fichier exécutable qui peut être distribué sur d'autres ordinateurs sans nécessiter l'installation de Python ou des bibliothèques tierces nécessaires à l'exécution de votre programme.

Voici un exemple de code pour créer un fichier exécutable à l'aide de cx_Freeze:

```python
pip install cx_Freeze
cxfreeze myscript.py --target-dir dist
```

Le fichier exécutable généré se trouvera dans le répertoire dist, mais sera dépendant du répertoire dist.

Il est possible de générer un seul fichier exécutable qui inclut toutes les dépendances nécessaires à l'exécution de votre programme Python. Cela peut être utile pour la distribution de votre programme, car vous n'avez plus à vous soucier de la gestion des dépendances et de la compatibilité des versions.

Par exemple, avec PyInstaller, vous pouvez utiliser la commande suivante pour générer un fichier exécutable unique :

```python
pyinstaller --onefile myscript.py
```

Le fichier exécutable unique sera généré dans le répertoire dist et peut être distribué sans les dépendances supplémentaires. Notez cependant que la taille de ce fichier exécutable unique peut être plus importante, car elle inclut toutes les dépendances nécessaires à l'exécution du programme.


