Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 471 – os.scandir() 함수 – 더 좋고 더 빠른 디렉터리 이터레이터

Author:
Ben Hoyt <benhoyt at gmail.com>
BDFL-Delegate:
Victor Stinner <vstinner at python.org>
Status:
Final
Type:
Standards Track
Created:
30-May-2014
Python-Version:
3.5
Post-History:
27-Jun-2014, 08-Jul-2014, 14-Jul-2014

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

초록

이 PEP는 새로운 디렉터리 순회 함수인 os.scandir()를 표준 라이브러리에 포함할 것을 제안합니다. 이 새 함수는 유용한 기능을 추가하며, 대부분의 경우 os.stat() 호출을 피함으로써 os.walk()의 속도를 (플랫폼과 파일 시스템에 따라) 2-20배 향상합니다.

근거

Python의 내장 os.walk()는 필요 이상으로 상당히 느립니다. 각 디렉터리에 대해 os.listdir()를 호출하는 것 외에도, 각 파일에 대해 stat() 시스템 호출이나 GetFileAttributes()를 실행하여 해당 항목이 디렉터리인지 아닌지를 판별하기 때문입니다.

하지만 하부 시스템 호출인 Windows의 FindFirstFile / FindNextFile과 POSIX 시스템의 readdir는 반환된 파일이 디렉터리인지 아닌지를 이미 알려 주므로, 추가 시스템 호출이 필요하지 않습니다. 게다가 Windows 시스템 호출은 파일 크기와 마지막 수정 시각 등 디렉터리 항목에 대한 stat_result 객체의 모든 정보를 반환합니다.

요컨대, os.walk() 같은 트리 함수에 필요한 시스템 호출 횟수를 약 2N에서 N으로 줄일 수 있습니다. 여기서 N은 트리에 있는 파일과 디렉터리의 총 개수입니다. (그리고 디렉터리 트리는 보통 깊이보다 너비가 더 넓기 때문에, 실제로는 이보다 훨씬 나은 경우가 많습니다.)

실제로 이러한 추가 시스템 호출을 모두 제거하면 os.walk()가 Windows에서 약 8-9배, POSIX 시스템에서 약 2-3배 빨라집니다. 즉, 미세 최적화 수준의 이야기가 아닙니다. benchmarks here에서 더 많은 벤치마크를 확인하십시오.

이와 어느 정도 관련하여, 많은 사람이 (Python Issue 11406 참고) 파일 이름을 하나의 큰 리스트로 반환하는 대신 순회하면서 하나씩 내놓는 버전의 os.listdir()도 원하고 있습니다. 이렇게 하면 매우 큰 디렉터리를 순회할 때 메모리 효율이 향상됩니다.

따라서 직접 호출할 수 있는 scandir() 이터레이터 함수를 제공할 뿐만 아니라, Python의 기존 os.walk() 함수의 속도도 크게 높일 수 있습니다.

구현

이 제안의 구현은 Ben Hoyt(초기 버전)와 Tim Golden(C 확장 모듈에 많은 도움을 줌)이 작성했습니다. 구현은 GitHub의 benhoyt/scandir에 있습니다. (구현이 이 PEP의 갱신 사항보다 약간 뒤처질 수 있습니다.)

이 모듈은 실제로 사용되고 테스트되었으므로(이 PEP의 “Use in the wild” 절 참고), 개념 증명 이상의 것임에 유의하십시오. 하지만 베타 소프트웨어로 표시되어 있으며, 충분히 실전 검증되지는 않았습니다. 표준 라이브러리에 들어가기 전에 posixmodule.c로의 통합뿐만 아니라 약간의 정리와 더 철저한 테스트가 필요할 것입니다.

제안 세부 사항

os.scandir()

구체적으로, 이 PEP는 표준 라이브러리의 os 모듈에 선택적 문자열 하나를 인자로 받는 단일 함수 scandir를 추가할 것을 제안합니다:

scandir(path='.') -> generator of DirEntry objects

listdir와 마찬가지로 scandir는 주어진 path에 있는 파일들의 이름을 얻기 위해 운영 체제의 디렉터리 순회 시스템 호출을 호출하지만, 두 가지 점에서 listdir와 다릅니다:

  • 단순한 파일 이름 문자열을 반환하는 대신, 파일 이름 문자열을 담고 운영 체제가 반환했을 수 있는 추가 데이터에 접근할 수 있는 간단한 메서드를 제공하는 가벼운 DirEntry 객체를 반환합니다.
  • 리스트 대신 제너레이터를 반환하므로, scandir는 전체 리스트를 즉시 반환하는 대신 진정한 이터레이터로 동작합니다.

scandir()는 path에 있는 각 파일과 하위 디렉터리마다 DirEntry 객체를 하나씩 내놓습니다. listdir와 마찬가지로 '.'와 '..' 의사 디렉터리는 건너뛰며, 항목들은 시스템에 따라 달라지는 순서로 나옵니다. 각 DirEntry 객체에는 다음 속성과 메서드가 있습니다:

  • name: scandir path 인자에 대한 상대적인 항목의 파일 이름입니다(os.listdir의 반환값에 대응합니다)
  • path: 항목의 전체 경로 이름(반드시 절대 경로일 필요는 없음)으로, os.path.join(scandir_path, entry.name)과 동등합니다
  • inode(): 항목의 inode 번호를 반환합니다. 결과는 DirEntry 객체에 캐시되므로, 최신 정보를 가져오려면 os.stat(entry.path, follow_symlinks=False).st_ino를 사용하십시오. Unix에서는 시스템 호출이 필요하지 않습니다.
  • is_dir(*, follow_symlinks=True): pathlib.Path.is_dir()와 비슷하지만 반환값이 DirEntry 객체에 캐시됩니다. 대부분의 경우 시스템 호출이 필요하지 않으며, follow_symlinks가 False이면 심볼릭 링크를 따라가지 않습니다.
  • is_file(*, follow_symlinks=True): pathlib.Path.is_file()과 비슷하지만 반환값이 DirEntry 객체에 캐시됩니다. 대부분의 경우 시스템 호출이 필요하지 않으며, follow_symlinks가 False이면 심볼릭 링크를 따라가지 않습니다.
  • is_symlink(): pathlib.Path.is_symlink()와 비슷하지만 반환값이 DirEntry 객체에 캐시됩니다. 대부분의 경우 시스템 호출이 필요하지 않습니다.
  • stat(*, follow_symlinks=True): os.stat()과 비슷하지만 반환값이 DirEntry 객체에 캐시됩니다. Windows에서는 (심볼릭 링크를 제외하면) 시스템 호출이 필요하지 않으며, follow_symlinks가 False이면 (os.lstat()처럼) 심볼릭 링크를 따라가지 않습니다.

모든 메서드는 경우에 따라 시스템 호출을 수행할 수 있으므로 OSError를 발생시킬 수 있습니다. 자세한 내용은 “예외 처리에 관한 참고 사항” 절을 참조하십시오.

DirEntry의 속성과 메서드 이름은 일관성을 위해 가능한 한 새로운 pathlib 모듈의 이름과 같도록 정했습니다. 기능상의 유일한 차이는 DirEntry 메서드가 처음 호출된 후 그 값을 항목 객체에 캐시한다는 점입니다.

os 모듈의 다른 함수들처럼 scandir()는 path 매개변수로 bytes 객체와 str 객체를 모두 받으며, DirEntry.name과 DirEntry.path 속성을 path와 같은 타입으로 반환합니다. 그러나 str 타입을 사용할 것을 강력히 권장합니다. 그래야 유니코드 파일 이름을 여러 플랫폼에서 지원할 수 있기 때문입니다. (Windows에서 bytes 파일 이름은 Python 3.3부터 폐지 예정(deprecated)되었습니다.)

os.walk()

이 제안의 일환으로 os.walk()도 listdir()와 os.path.isdir() 대신 scandir()를 사용하도록 수정됩니다. 이렇게 하면 os.walk()의 속도가 (앞에서 언급했듯이 시스템에 따라 2~20배) 매우 크게 향상됩니다.

예시

먼저, DirEntry.name 속성과 DirEntry.is_dir() 메서드의 사용법을 보여 주는 아주 간단한 scandir() 예제입니다:

def subdirs(path):
    """Yield directory names not starting with '.' under given path."""
    for entry in os.scandir(path):
        if not entry.name.startswith('.') and entry.is_dir():
            yield entry.name

이 subdirs() 함수는 Windows와 POSIX 시스템 모두에서, 특히 중간 크기 이상의 디렉터리에서 os.listdir()와 os.path.isdir()를 사용할 때보다 scandir를 사용할 때 훨씬 빠릅니다.

또는 디렉터리 트리에 있는 파일의 전체 크기를 구하는 예제로, DirEntry.stat() 메서드와 DirEntry.path 속성의 사용법을 보여 줍니다:

def get_tree_size(path):
    """Return total size of files in given path and subdirs."""
    total = 0
    for entry in os.scandir(path):
        if entry.is_dir(follow_symlinks=False):
            total += get_tree_size(entry.path)
        else:
            total += entry.stat(follow_symlinks=False).st_size
    return total

이 예제는 is_dir()에 follow_symlinks 매개변수를 사용하는 방법도 보여 줍니다. 이와 같은 재귀 함수에서는 아마 링크를 따라가지 않는 편이 좋을 것입니다. (이와 같은 재귀 함수에서 링크를 제대로 따라가려면, 심볼릭 링크를 따라갔을 때 재귀 루프가 생기는 경우를 특별히 처리해야 합니다.)

get_tree_size()는 추가 stat 호출이 필요 없기 때문에 Windows에서는 속도가 크게 향상되지만, POSIX 시스템에서는 디렉터리 이터레이션 함수가 크기 정보를 반환하지 않으므로 이 함수가 얻는 이점이 없다는 점에 유의하십시오.

캐싱에 관한 참고 사항

DirEntry 객체는 비교적 단순합니다. name과 path 속성은 당연히 항상 캐시되고, is_X와 stat 메서드는 (Windows에서는 FindNextFile을 통해 즉시, POSIX 시스템에서는 처음 사용할 때 stat 시스템 호출을 통해) 값을 캐시하며 시스템에서 다시 가져오지 않습니다.

이러한 이유로 DirEntry 객체는 이터레이션 후에 사용하고 버리도록 의도되었으며, 수명이 긴 자료 구조에 저장해 두고 메서드를 거듭 호출하도록 의도된 것이 아닙니다.

개발자가 (예를 들어 파일 크기 변화를 감시하기 위해) “새로 고침” 동작을 원한다면, 간단히 pathlib.Path 객체를 사용하거나, 호출할 때마다 운영 체제에서 새 데이터를 가져오는 일반 os.stat() 또는 os.path.getsize() 함수를 호출하면 됩니다.

예외 처리에 관한 참고 사항

DirEntry.is_X()와 DirEntry.stat()은 속성이나 프로퍼티가 아니라 명시적으로 메서드인데, 이는 이들이 (흔히 비용이 적게 들기는 하지만) 비용이 적은 연산이 아닐 수 있으며 시스템 호출을 할 수도 있다는 점을 분명히 하기 위해서입니다. 따라서 이 메서드들은 OSError를 발생시킬 수 있습니다.

예를 들어, POSIX 기반 시스템에서 DirEntry.stat()은 항상 시스템 호출을 하며, 그러한 시스템에서 readdir()가 d_type을 지원하지 않거나 DT_UNKNOWN 값을 가진 d_type을 반환하면 DirEntry.is_X() 메서드가 stat() 시스템 호출을 합니다. 이런 일은 특정 조건에서나 특정 파일 시스템에서 일어날 수 있습니다.

대개는 이것이 문제가 되지 않습니다. 예를 들어, 표준 라이브러리에 정의된 os.walk()는 listdir() 호출 주변의 오류만 잡습니다.

또한 DirEntry.is_X 메서드의 예외 발생 동작은 pathlib의 동작과 일치하므로(pathlib은 권한 문제나 기타 치명적인 오류의 경우에만 OSError를 발생시키고, 경로가 존재하지 않거나 깨진 심볼릭 링크이면 False를 반환합니다) is_X() 호출 주변에서 오류를 잡을 필요가 없는 경우가 많습니다.

그러나 사용자에게 세밀한 오류 처리가 필요한 경우에는 모든 메서드 호출 주변에서 OSError를 잡아 적절히 처리하는 것이 바람직할 수 있습니다.

예를 들어, 아래는 위에서 보인 get_tree_size() 예제에 세밀한 오류 처리를 추가한 버전입니다:

def get_tree_size(path):
    """Return total size of files in path and subdirs. If
    is_dir() or stat() fails, print an error message to stderr
    and assume zero size (for example, file has been deleted).
    """
    total = 0
    for entry in os.scandir(path):
        try:
            is_dir = entry.is_dir(follow_symlinks=False)
        except OSError as error:
            print('Error calling is_dir():', error, file=sys.stderr)
            continue
        if is_dir:
            total += get_tree_size(entry.path)
        else:
            try:
                total += entry.stat(follow_symlinks=False).st_size
            except OSError as error:
                print('Error calling stat():', error, file=sys.stderr)
    return total

지지

GitHub의 scandir 모듈은 상당히 많이 포크되고 사용되었지만(이 PEP의 “실제 사용 사례” 참조), python-dev와 python-ideas 메일링 리스트에서도 코어 개발자와 그 밖의 사람들이 scandir와 같은 함수를 직접 지지한 경우가 꽤 많았습니다. 몇 가지 예를 들면 다음과 같습니다:

  • python-dev: 2014년 6월의 이 python-dev 스레드에서 scandir와 PEP 471에 대해 상당수의 +1이 나왔고 반대 의견은 극소수였습니다
  • Alyssa Coghlan, 파이썬 핵심 개발자: “현지 Red Hat 릴리스 엔지니어링 팀이 dirent 구조체에 이미 있는 정보를 얻으려고 네트워크에 마운트된 디렉터리 트리의 모든 파일에 stat을 해야 하는 것에 불만을 표한 적이 있습니다. 그러니 os.scandir가 그 정보를 제공하기만 한다면 저는 확실히 +1입니다.” [source1]
  • 코어 Python 개발자인 Tim Golden은 scandir의 C 확장 모듈을 리팩터링하고 크게 개선하는 데 시간을 들였을 만큼 scandir를 지지합니다. [source2]
  • Christian Heimes, 파이썬 핵심 개발자: “yielddir() 같은 것에 +1입니다” [source3] 그리고 “정말 그렇습니다! 이 기능이 3.4에 들어가서 저희 코드베이스에서 제가 만든 임시방편 코드를 제거할 수 있으면 좋겠습니다.” [source4]
  • Gregory P. Smith, 코어 Python 개발자: “오늘 밤 3.4beta1이 나오므로 이것은 3.4에 들어가지 못할 것이고, 따라서 3.5로 미루겠습니다. 위에서 설명한 제안된 설계가 정말 마음에 듭니다.” [source5]
  • Guido van Rossum, Python 3.5에 scandir를 추가할 가능성에 대해(3.4에는 너무 늦었으므로): “scandir()를 (os에든 pathlib에든) 추가하는 것도 마찬가지로 이미 때를 놓쳤습니다. 얼마든지 실험해 보시고 3.5에서 검토할 수 있도록 준비해 주십시오. 하지만 3.4에는 추가하고 싶지 않습니다.” [source6]

이 PEP 자체에 대한 지지(메타 지지?)는 python-dev에서 Alyssa (Nick) Coghlan이 표명했습니다: “3.5를 위해 이 모든 것을 검토하고 구체적인 os.scandir API를 제안하는 PEP가 있다면 좋을 것입니다.” [source7]

실제 사용 사례

지금까지 scandir 구현은 분명히 유용하지만 “베타”로 명확히 표시되어 있었기 때문에, 실제로 얼마나 사용되고 있는지는 확실하지 않습니다. Ben Hoyt는 이를 사용하는 사람들로부터 여러 건의 보고를 받았습니다. 예를 들면 다음과 같습니다:

  • Chris F: “꽤 큰 디렉터리들을 처리하고 있는데, getdents를 수정해야 할지도 모른다고 반쯤 예상하고 있었습니다. 그 수고를 덜어 주셔서 감사합니다.” [개인 이메일을 통해]
  • bschollnick: “이 코드의 구성 요소로 Scandir를 사용하고 있어서 이 사실을 알려 드리고 싶었습니다. scandir가 os.listdir에 비해 성능을 획기적으로 개선한 좋은 사례가 여기 있습니다.” [source8]
  • Avram L: “작업 중인 프로젝트에 쓰려고 scandir를 테스트하고 있습니다. 꽤 견고해 보여서, 우선 훌륭한 작업이라고 말씀드리고 싶습니다!” [개인 이메일을 통해]
  • Matt Z: “scandir를 사용해 네트워크 디렉터리의 내용을 15초도 안 걸려 덤프했습니다. 루트 디렉터리 13개, 전체 구조에 파일 60,000개였습니다. 이것은 똑같은 작업을 하는 데 15~20분이 걸리던, 스프레드시트에 포함된 오래된 VBA 코드를 대체할 것입니다.” [개인 이메일을 통해]

다른 사람들이 이를 위한 PyPI 패키지를 요청했으며(requested a PyPI package), 그 패키지는 만들어졌습니다. PyPI package를 참조하십시오.

GitHub 통계가 큰 의미가 있는 것은 아니지만, scandir에는 워처, 이슈, 포크 등이 꽤 있습니다. 2014년 7월 7일 기준 통계 요약은 다음과 같습니다:

  • 워처: 17
  • 스타: 57
  • 포크: 20
  • 이슈: 열림 4개, 닫힘 26개

또한 이 PEP는 os.walk()의 속도를 크게 높이므로, 수천 명의 개발자와 스크립트, 그리고 많은 프로덕션 코드가 혜택을 받을 것입니다. 예를 들어 GitHub에서 os.walk의 사용 횟수(194,000)는 os.mkdir의 사용 횟수(230,000)와 거의 비슷합니다.

기각된 아이디어

명명

이 함수의 이름으로 실질적으로 경쟁한 다른 후보는 iterdir()뿐이었습니다. 그러나 Python의 iterX() 함수들(주로 Python 2에 있음)은 대개 이터레이터가 아닌 대응 함수를 단순히 이터레이터로 바꾼 것입니다. 예를 들어 dict.iterkeys()는 dict.keys()의 이터레이터 버전일 뿐이며, 반환되는 객체는 동일합니다. 그러나 scandir()의 경우에는 반환값이 상당히 다른 객체(DirEntry 객체 대 파일 이름 문자열)이므로, 이 차이를 이름에도 반영하는 것이 바람직할 것입니다. 그래서 이름이 scandir()입니다.

python-dev의 관련 논의를 참조하십시오.

와일드카드 지원

Windows의 FindFirstFile/FindNextFile은 *.jpg 같은 “와일드카드”를 전달하는 기능을 지원합니다. 그래서 처음에는 사람들(이 PEP의 작성자 포함)이 사용자가 이를 전달할 수 있도록 scandir 함수에 windows_wildcard 키워드 인자를 넣는 것이 좋겠다고 생각했습니다.

그러나 더 생각하고 논의한 끝에, 크로스 플랫폼으로 만들 수 없다면 (pattern 키워드 인자 같은 방식으로) 이는 좋지 않은 아이디어라는 결론을 내렸습니다. 처음에는 이것이 꽤 쉬워 보입니다. Windows에서는 OS의 와일드카드 지원을 그대로 사용하고, POSIX 기반 시스템에서는 나중에 fnmatch나 re 같은 것을 사용하면 됩니다.

안타깝게도 정확한 Windows 와일드카드 매칭 규칙은 Microsoft가 어디에도 제대로 문서화하지 않았고, 규칙 자체도 상당히 특이합니다(이 블로그 글을 참조하십시오). 따라서 fnmatch나 정규식으로 이를 흉내 내기는 매우 어렵습니다.

그래서 Windows 와일드카드 지원은 좋지 않은 아이디어라는 데 합의가 이루어졌습니다. 크로스 플랫폼으로 구현할 방법이 있다면 나중에 추가할 수는 있겠지만, 초기 버전에는 넣지 않습니다.

자세한 내용은 이 2012년 11월 python-ideas 스레드와 PEP 471에 관한 이 2014년 6월 python-dev 스레드를 읽어 보십시오.

기본적으로 심볼릭 링크를 따라가지 않는 메서드

DirEntry 메서드가 심볼릭 링크를 따라가야 하는지를 두고(is_X() 메서드에 follow_symlinks 매개변수가 없던 때) python-dev에서 많은 논쟁이 있었습니다(이 스레드의 메시지를 참조하십시오).

처음에는 따라가지 않았지만(이 PEP의 이전 버전과 scandir.py 모듈을 참조하십시오), Victor Stinner가 python-dev에서 기본적으로 심볼릭 링크를 따라가는 편이 더 낫다는 꽤 설득력 있는 주장을 폈습니다. 그 이유는 다음과 같습니다.

  • 대개는 링크를 따라가는 것이 원하는 동작입니다(표준 라이브러리에서 os.listdir()와 os.path.isdir()를 사용하는 함수는 92%의 경우 심볼릭 링크를 따라갑니다).
  • 비슷한 함수인 os.path.isdir()와 pathlib.Path.is_dir()가 세운 선례가 그러하므로, 다르게 동작하면 혼란스러울 것입니다.
  • 링크를 따라가지 않는 방식에서는 링크를 따라가고 싶을 때 if (entry.is_symlink() and os.path.isdir(entry.path)) or entry.is_dir()와 같이 작성해야 하는데, 이는 번거롭습니다.

심볼릭 링크를 따라가지 않는 버전이 오류를 일으키기 쉽다는 것을 보여 주는 실례로, 이 PEP의 작성자는 scandir.py에서 scandir.walk()를 처음 구현할 때 바로 이 검사를 잘못 작성하여 버그를 낸 적이 있습니다(여기의 Issue #4를 참조하십시오).

결국 메서드가 심볼릭 링크를 따라가야 한다는 데 완전한 합의가 이루어지지는 않았지만, 가장 깊이 관여한 참가자들 사이에서는 기본적인 합의가 있었습니다. 또한 이 PEP의 작성자는 위의 근거가 기본적으로 심볼릭 링크를 따라가도록 하기에 충분히 강력하다고 생각합니다.

게다가 다른 동작을 원한다면 해당 메서드를 follow_symlinks=False로 호출하면 간단히 해결됩니다.

프로퍼티로서의 DirEntry 속성

어떤 면에서는 DirEntry의 is_X()와 stat()을 메서드 대신 프로퍼티로 만들어, 비용이 매우 적거나 없다는 점을 나타내는 편이 더 좋을 수도 있습니다. 그러나 실제로는 꼭 그렇지 않습니다. stat()은 Windows에서는 OS 호출이 필요 없지만 POSIX 기반 시스템에서는 OS 호출이 필요하기 때문입니다. is_dir() 등의 메서드조차도 (특정 파일 시스템에서) dirent.d_type 값이 DT_UNKNOWN이면 POSIX 기반 시스템에서 OS 호출을 수행할 수 있습니다.

또한 사람들은 entry.is_dir 속성 접근이 내부적으로 시스템 호출을 하는 경우에도 OSError가 아니라 오직 AttributeError만 발생시킬 것이라고 기대할 것입니다. 그러면 호출하는 코드는 단순한 속성 접근처럼 보이는 부분을 try/except로 감싸야 하므로, 이것들을 메서드로 만드는 편이 훨씬 낫습니다.

이 PEP 작성자가 이 주장을 펼치고 코어 개발자의 동의를 얻은 2013년 5월의 이 python-dev 스레드를 참조하십시오.

DirEntry 필드가 “정적” 속성만 가진 객체인 방안

2014년 7월의 python-dev 메시지에서 Paul Moore는 “OS 기능을 감싸는 얇은 래퍼”인 해결책을 제안했습니다. 이 해결책에서 DirEntry 객체는 name, path, is_X라는 정적 속성만 가지며, st_X 속성은 Windows에서만 존재합니다. 이 더 단순한 저수준 함수를 고수준 함수의 구성 요소로 사용하자는 것이 그 발상이었습니다.

처음에는 이런 식으로 단순화하는 것이 좋다는 데 대체로 의견이 모였습니다. 하지만 이 접근 방식에는 두 가지 문제가 있었습니다. 첫째, 이 방식은 is_dir 및 유사한 속성이 POSIX에서 항상 존재한다고 가정하는데, 실제로는 그렇지 않습니다(d_type이 없거나 DT_UNKNOWN인 경우). 둘째, 실제로는 훨씬 사용하기 어려운 API입니다. POSIX에서는 is_dir 속성조차 항상 존재하지는 않으므로, hasattr()로 검사한 다음 존재하지 않으면 os.stat()을 호출해야 하기 때문입니다.

이 선택지가 왜 이상적이지 않은 해결책인지 자세히 설명한 이 PEP 작성자의 2014년 7월 python-dev 답신과, 이어서 동의를 표한 Paul Moore의 답장을 참고하십시오.

DirEntry 필드가 정적이며 ensure_lstat 옵션을 두는 방안

겉보기에 더 단순하고 매력적인 또 다른 선택지는 Alyssa Coghlan이 2014년 6월의 python-dev 메시지에서 제안했습니다. DirEntry.is_X와 DirEntry.lstat_result를 프로퍼티로 만들고, scandir() 호출 시 새 인자 ensure_lstat=True가 지정된 경우에만 반복 시점에 DirEntry.lstat_result를 채우자는 것입니다.

이 방식은 필요할 때 scandir()에서 stat 결과를 쉽게 얻을 수 있다는 점에서 앞의 방식보다 확실히 장점이 있습니다. 하지만 반복 중에 stat()이 호출되어(따라서 OSError가 발생할 수 있어) 다소 보기 흉한 수작업 반복 루프가 필요해지므로, 세밀한 오류 처리가 지저분해진다는 심각한 단점이 있습니다.:

it = os.scandir(path)
while True:
    try:
        entry = next(it)
    except OSError as error:
        handle_error(path, error)
    except StopIteration:
        break

아니면 scandir()가 onerror 인자, 즉 반복 중에 stat() 오류가 발생할 때 호출할 함수를 받아야 한다는 뜻이 됩니다. 이 PEP의 작성자가 보기에 이는 DirEntry.stat() 호출을 try/except로 감싸는 것만큼 직접적이지도, 파이썬답지도 않습니다.

또 다른 단점은 os.scandir()가 코드를 더 빠르게 만들기 위해 작성되었다는 점입니다. POSIX에서 항상 os.lstat()을 호출하면 아무런 속도 향상도 얻을 수 없습니다. 대부분의 경우 완전한 stat_result 객체는 필요하지 않습니다. is_X() 메서드로 충분하며 이 정보는 이미 알려져 있습니다.

이를 요약하고 결국 원래의 PEP 471 제안이 왜 “올바른 것”이라고 생각하는지 자세히 설명한, 토론에 대한 Ben Hoyt의 2014년 7월 답신을 참고하십시오.

반환값이 (name, stat_result) 2-튜플인 방안

처음에 이 PEP의 작성자는 이 개념을 (name, stat_result) 2-튜플을 산출하는 iterdir_stat()이라는 함수로 제안했습니다. 이 방식은 새로운 타입을 도입하지 않는다는 장점이 확실히 있습니다. 하지만 POSIX 기반 시스템에서는 stat_result가 부분적으로만 채워지므로(대부분의 필드가 None으로 설정되는 등 여러 특이점이 있음) 사실상 전혀 stat_result 객체라고 할 수 없으며, os.stat()과 다르다는 점을 철저히 문서화해야 합니다.

또한 Python은 속성과 메서드를 갖춘 제대로 된 객체를 잘 지원하므로, 2-튜플보다 더 합리적이고 단순한 API를 만들 수 있습니다. 또한 운영 체제에 기능이 추가되어 이를 DirEntry에 포함하고자 할 때를 대비해, DirEntry 객체를 더 확장 가능하고 미래에도 대응할 수 있게 만들어 줍니다.

다음의 이전 논의도 참고하십시오:

반환값이 오버로드된 stat_result 객체인 방안

논의된 또 다른 대안은 반환값을 name과 path 속성을 가진 오버로드된 stat_result 객체로 만드는 것이었습니다. 그러나 이것이 이상한(그리고 억지스러운!) 종류의 오버로딩이라는 점은 차치하더라도, 이 방법에는 앞서 언급한 것과 같은 문제가 있습니다 – POSIX 시스템에서 readdir()은 stat_result 정보 대부분을 가져오지 않으며, st_mode 값(의 일부)만 가져옵니다.

pathlib.Path 객체를 반환값으로 사용하는 방안

Antoine Pitrou의 새로운 표준 라이브러리 pathlib 모듈이 있으므로, 처음에는 scandir()가 pathlib.Path 인스턴스를 반환하는 것이 훌륭한 생각처럼 보입니다. 그러나 pathlib.Path의 is_X()와 stat() 함수는 명시적으로 캐시되지 않는 반면, scandir는 (많은 경우) 원래의 디렉터리 순회 시스템 호출에서 얻은 값을 반환하기 때문에 설계상 이를 캐시해야 합니다.

그리고 scandir가 반환하는 pathlib.Path 인스턴스는 stat 값을 캐시하는데 일반 pathlib.Path 객체는 명시적으로 캐시하지 않는다면, 이는 적잖이 혼란스러울 것입니다.

Guido van Rossum은 scandir의 맥락에서 pathlib.Path가 stat을 캐시하는 것을 여기에서 명시적으로 거부했으며, 이 때문에 pathlib.Path 객체는 scandir 반환값으로 적합하지 않은 선택이 되었습니다.

가능한 개선 사항

scandir에 적용할 수 있는 개선 사항은 많지만, 이 PEP의 저자가 염두에 두고 있는 몇 가지를 간단히 나열하면 다음과 같습니다:

  • Py_BEGIN_ALLOW_THREADS 블록마다 readdir / FindNextFile을 예컨대 50번씩 호출하여 C 확장 모듈 안에 더 오래 머무르게 하면 scandir의 속도를 더 높일 수 있을 것이며, 그 결과 다소 더 빨라질 수 있습니다. 이 방법은 아직 테스트되지 않았지만, Antoine Pitrou가 Issue 11406에서 제안한 것입니다. [source9]
  • scandir는 반복마다 발생하는 메모리 할당 비용을 피하기 위해 프리 리스트를 사용할 수 있습니다 – 10개, 어쩌면 1개짜리의 짧은 프리 리스트만으로도 도움이 될 수 있습니다. Victor Stinner가 python-dev thread on June 27에서 제안했습니다.

이전 논의