"Si un ouvrier veut bien faire son travail, il doit d'abord affûter ses outils." - Confucius, "Les Entretiens de Confucius. Lu Linggong"
Page de garde > La programmation > Accélérez `shutil.copytree` !

Accélérez `shutil.copytree` !

Publié le 2024-11-04
Parcourir:173

Speed up `shutil.copytree` !

Discutez de l’accélération deshutil.copytree

Écrivez ici

Il s'agit d'une discussion sur , voir : https://discuss.python.org/t/speed-up-shutil-copytree/62078. Si vous avez des idées, envoyez-moi s'il vous plaît !

Arrière-plan

shutil est un module très utile en Python. Vous pouvez le trouver sur github : https://github.com/python/cpython/blob/master/Lib/shutil.py

shutil.copytree est une fonction qui copie un dossier dans un autre dossier.

Dans cette fonction, il appelle la fonction _copytree pour copier.

Que fait _copytree ?

  1. Ignorer les fichiers/répertoires spécifiés.
  2. Création de répertoires de destination.
  3. Copie de fichiers ou de répertoires tout en gérant des liens symboliques.
  4. Collecter et éventuellement signaler les erreurs rencontrées (par exemple, des problèmes d'autorisation).
  5. Réplication des métadonnées du répertoire source vers le répertoire de destination.

Problèmes

La vitesse de

_copytree n'est pas très rapide lorsque le nombre de fichiers est important ou que la taille du fichier est importante.

Testez ici :

import os
import shutil

os.mkdir('test')
os.mkdir('test/source')

def bench_mark(func, *args):
    import time
    start = time.time()
    func(*args)
    end = time.time()
    print(f'{func.__name__} takes {end - start} seconds')
    return end - start

# write in 3000 files
def write_in_5000_files():
    for i in range(5000):
        with open(f'test/source/{i}.txt', 'w') as f:
            f.write('Hello World'   os.urandom(24).hex())
            f.close()

bench_mark(write_in_5000_files)

def copy():
    shutil.copytree('test/source', 'test/destination')

bench_mark(copy)

Le résultat est :

write_in_5000_files prend 4,084963083267212 secondes
la copie prend 27,12768316268921 secondes

Ce que j'ai fait

Multithreading

J'utilise le multithread pour accélérer le processus de copie. Et je renomme la fonction _copytree_single_threaded et ajoute une nouvelle fonction _copytree_multithreaded. Voici le copytree_multithreaded :

def _copytree_multithreaded(src, dst, symlinks=False, ignore=None, copy_function=shutil.copy2,
                            ignore_dangling_symlinks=False, dirs_exist_ok=False, max_workers=4):
    """Recursively copy a directory tree using multiple threads."""
    sys.audit("shutil.copytree", src, dst)

    # get the entries to copy
    entries = list(os.scandir(src))

    # make the pool
    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        # submit the tasks
        futures = [
            executor.submit(_copytree_single_threaded, entries=[entry], src=src, dst=dst,
                            symlinks=symlinks, ignore=ignore, copy_function=copy_function,
                            ignore_dangling_symlinks=ignore_dangling_symlinks,
                            dirs_exist_ok=dirs_exist_ok)
            for entry in entries
        ]

        # wait for the tasks
        for future in as_completed(futures):
            try:
                future.result()
            except Exception as e:
                print(f"Failed to copy: {e}")
                raise

J'ajoute un jugement pour choisir d'utiliser ou non le multithread.

if len(entries) >= 100 or sum(os.path.getsize(entry.path) for entry in entries) >= 100*1024*1024:
        # multithreaded version
        return _copytree_multithreaded(src, dst, symlinks=symlinks, ignore=ignore,
                                        copy_function=copy_function,
                                        ignore_dangling_symlinks=ignore_dangling_symlinks,
                                        dirs_exist_ok=dirs_exist_ok)

else:
    # single threaded version
    return _copytree_single_threaded(entries=entries, src=src, dst=dst,
                                        symlinks=symlinks, ignore=ignore,
                                        copy_function=copy_function,
                                        ignore_dangling_symlinks=ignore_dangling_symlinks,
                                        dirs_exist_ok=dirs_exist_ok)

Test

J'écris 50 000 fichiers dans le dossier source. Repère de nivellement:

def bench_mark(func, *args):
    import time
    start = time.perf_counter()
    func(*args)
    end = time.perf_counter()
    print(f"{func.__name__} costs {end - start}s")

Écrivez :

import os
os.mkdir("Test")
os.mkdir("Test/source")

# write in 50000 files
def write_in_file():
    for i in range(50000):
         with open(f"Test/source/{i}.txt", 'w') as f:
             f.write(f"{i}")
             f.close()

Deux comparaison :

def copy1():
    import shutil
    shutil.copytree('test/source', 'test/destination1')

def copy2():
    import my_shutil
    my_shutil.copytree('test/source', 'test/destination2')

  • "my_shutil" est ma version modifiée de Shutil.

copie1 coûte 173,04780609999943s
copy2 coûte 155,81321870000102s

copy2 est beaucoup plus rapide que copy1. Vous pouvez courir plusieurs fois.

Avantages et inconvénients

L'utilisation du multithread peut accélérer le processus de copie. Mais cela augmentera l'utilisation de la mémoire. Mais nous n'avons pas besoin de réécrire le multithread dans le code.

Asynchrone

Merci à "Barry Scott". Je vais suivre sa suggestion :

Vous pourriez obtenir la même amélioration pour moins de frais généraux en utilisant les E/S asynchrones.

J'écris ce code :

import os
import shutil
import asyncio
from concurrent.futures import ThreadPoolExecutor
import time


# create directory
def create_target_directory(dst):
    os.makedirs(dst, exist_ok=True)

# copy 1 file
async def copy_file_async(src, dst):
    loop = asyncio.get_event_loop()
    await loop.run_in_executor(None, shutil.copy2, src, dst)

# copy directory
async def copy_directory_async(src, dst, symlinks=False, ignore=None, dirs_exist_ok=False):
    entries = os.scandir(src)
    create_target_directory(dst)

    tasks = []
    for entry in entries:
        src_path = entry.path
        dst_path = os.path.join(dst, entry.name)

        if entry.is_dir(follow_symlinks=not symlinks):
            tasks.append(copy_directory_async(src_path, dst_path, symlinks, ignore, dirs_exist_ok))
        else:
            tasks.append(copy_file_async(src_path, dst_path))

    await asyncio.gather(*tasks)
# choose copy method
def choose_copy_method(entries, src, dst, **kwargs):
    if len(entries) >= 100 or sum(os.path.getsize(entry.path) for entry in entries) >= 100 * 1024 * 1024:
        # async version
        asyncio.run(copy_directory_async(src, dst, **kwargs))
    else:
        # single thread version
        shutil.copytree(src, dst, **kwargs)
# test function
def bench_mark(func, *args):
    start = time.perf_counter()
    func(*args)
    end = time.perf_counter()
    print(f"{func.__name__} costs {end - start:.2f}s")

# write in 50000 files
def write_in_50000_files():
    for i in range(50000):
        with open(f"Test/source/{i}.txt", 'w') as f:
            f.write(f"{i}")

def main():
    os.makedirs('Test/source', exist_ok=True)
    write_in_50000_files()

    # 单线程复制
    def copy1():
        shutil.copytree('Test/source', 'Test/destination1')

    def copy2():
        shutil.copytree('Test/source', 'Test/destination2')

    # async
    def copy3():
        entries = list(os.scandir('Test/source'))
        choose_copy_method(entries, 'Test/source', 'Test/destination3')

    bench_mark(copy1)
    bench_mark(copy2)
    bench_mark(copy3)

    shutil.rmtree('Test')

if __name__ == "__main__":
    main()

Sortir:

la copie 1 coûte 187,21 s
copy2 coûte 244,33 s
copy3 coûte 111,27 s


Vous pouvez voir que la version asynchrone est plus rapide que la version à thread unique. Mais la version monothread est plus rapide que la version multithread. (Peut-être que mon environnement de test n'est pas très bon, vous pouvez essayer de m'envoyer votre résultat en réponse)

Merci Barry Scott !

Avantages et inconvénients

Async est un bon choix. Mais aucune solution n’est parfaite. Si vous rencontrez un problème, vous pouvez m'envoyer une réponse.

Fin

C'est la première fois que j'écris une discussion sur python.org. S'il y a un problème, faites-le-moi savoir. Merci.

Mon Github : https://github.com/mengqinyuan
Mon Dev.to : https://dev.to/mengqinyuan

Déclaration de sortie Cet article est reproduit sur : https://dev.to/mengqinyuan/add-multithreading-to-shutil--2lm1?1 En cas de violation, veuillez contacter [email protected] pour le supprimer.
Dernier tutoriel Plus>

Clause de non-responsabilité: Toutes les ressources fournies proviennent en partie d'Internet. En cas de violation de vos droits d'auteur ou d'autres droits et intérêts, veuillez expliquer les raisons détaillées et fournir une preuve du droit d'auteur ou des droits et intérêts, puis l'envoyer à l'adresse e-mail : [email protected]. Nous nous en occuperons pour vous dans les plus brefs délais.

Copyright© 2022 湘ICP备2022001581号-3