Maison  >  Article  >  développement back-end  >  Accélérez `shutil.copytree` !

Accélérez `shutil.copytree` !

WBOY
WBOYoriginal
2024-08-28 18:32:06478parcourir

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 destinations.
  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 grande.

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. Note de référence :

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.

la 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 avec moins de frais généraux en utilisant des E/S asynchrones.

J'écris ces codes :

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()

Sortie :

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


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

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn