J'ai dernièrement commit sur MSS un patch pour corriger des fuites mémoire (memory leaks), voici un récapitulatif.

Python étant ce qu'il est, il y a plusieurs manières d'utiliser une classe et ses méthodes. Et certaines sont problématiques car la gestion des ressources ne peut pas être optimale, comprenez par là qu'il y aura des fuites mémoire.

Par exemple, prenons le module MSS, et voyons les différentes possibilités qu'il nous offre :

def bound_instance_without_cm():
    """ This is bad. """
    sct = mss()
    sct.shot()


def unbound_instance_without_cm():
    """ This is really bad. """
    mss().shot()


def with_context_manager():
    """ This is the best. """
    with mss() as sct:
        sct.shot()

J'ai introduis la possibilité de libérer les ressources via la méthode close(), ce qui nous laisse une 4ème possibilité :

def bound_instance_without_cm_but_use_close():
    """ This is better. """
    sct = mss()
    try:
        sct.shot()
    finally:
        sct.close()

Chacun de ces usages était problématique, et certains le sont encore mais je ne peux pas (encore ?) y faire grand chose.
Je ne présenterais que ce que j'ai trouvé sur GNU/Linux et Windows, car sous macOS je n'avais pas de soucis particuliers.

GNU/Linux, sockets et Serveur X

Dès qu'une connexion au Serveur X est établie, il faut absolument la libérer lorsque l'on n'en a plus besoin. La paire de fonctions utilisée est XOpenDisplay/XCloseDisplay.

Voici une fonction qui permet de récupérer le nombre de sockets ouvertes pour le processus en cours :

def get_opened_socket() -> int:
    """
    GNU/Linux: a way to get the opened sockets count.
    It will be used to check X server connections are well closed.
    """

    import os
    import subprocess

    cmd = f"lsof -U | grep {os.getpid()}"
    output = subprocess.check_output(cmd, shell=True)
    return len(output.splitlines())

Pour savoir si votre code a des fuites mémoire, il suffit d'appeler cette fonction, puis votre code et enfin une nouvelle fois la fonction. Comparez les valeurs entre les 2 appels et si le résultat n'est pas égal à zéro, alors il y a fuite !

Windows, HANDLER et Device Context

Nous allons voir les couples de fonction liés aux Device Context.

Quel que soit le language de programmation, libérer la mémoire allouée est le nerf de la guerre. En C, on parle du couple malloc/free, par exemple.
Ici, il n'y en a pas qu'un seul, mais 3 : GetWindowDC/ReleaseDC, CreateCompatibleDC/DeleteDC et CreateCompatibleBitmap/DeleteObject.

En Python, il faut définir les détails de ces fonctions :

import ctypes
from ctypes.wintypes import BOOL, HBITMAP, HDC, HGDIOBJ, HWND, INT


gdi32 = ctypes.windll.gdi32
user32 = ctypes.windll.user32

user32.GetWindowDC argtypes = [HWND]
user32.GetWindowDC.restype = HDC
user32.ReleaseDC. argtypes = [HWND, HGDIOBJ]
user32.ReleaseDC.restype = INT

gdi32.CreateCompatibleDC.argtypes = [HDC]
gdi32.CreateCompatibleDC.restype = HDC
gdi32.DeleteDC.argtypes = [HDC]
gdi32.DeleteDC.restype = BOOL

gdi32.CreateCompatibleBitmap.argtypes = [HDC, INT, INT]
gdi32.restype = HBITMAP
gdi32.DeleteObject.argtypes = [HGDIOBJ]
gdi32.DeleteObject.restype = INT

À utiliser tel que :

srcdc = user32.GetWindowDC(0)
memdc = gdi32.CreateCompatibleDC(srcdc)

width, height = 600, 400
bmp = gdi32.CreateCompatibleBitmap(srcdc, width, height)

Et pour libérer les ressources :

gdi32.DeleteObject(bmp)
gdi32.DeleteDC(memdc)
user32.ReleaseDC(0, srcdc)

Maintenant que nous savons comment bien utiliser ces ressources, voici une fonction qui permet de récupérer le nombre de d'objets HANDLE alloués :

def get_handles() -> int:
    """
    Windows: a way to get the GDI handles count.
    It will be used to check the handles count is not growing, showing resource leaks.
    """

    import ctypes

    PQI = 0x400  # PROCESS_QUERY_INFORMATION
    GR_GDIOBJECTS = 0
    h = ctypes.windll.kernel32.OpenProcess(PQI, 0, PID)
    return ctypes.windll.user32.GetGuiResources(h, GR_GDIOBJECTS)

Pour savoir si votre code a des fuites mémoire, il suffit d'appeler cette fonction, puis votre code et enfin une nouvelle fois la fonction. Comparez les valeurs entre les 2 appels et si le résultat n'est pas égal à zéro, alors il y a fuite !

Conclusion

Je vous redirige vers test_leaks.py qui est très clair et montre un réel cas d'usage qui m'a permis de vérifier que j'avais bien corriger toutes les fuites mémoires. Ainsi que la documentation qui mentionne maintenant les cas d'usage problématique.

Enfin, je ne suis pas encore au point avec contextlib.ExitStack, mais j'ai crée une issue pour faire quelques tests : Use contextlib.ExitStack() to prevent resource leaks. Si j'ai bien compris, cela permettrait d'éviter les fuites mémoire même lors des cas problématiques.

Sources