PEP 844 – public과 private 내장 함수
- Author:
- Barry Warsaw <barry at python.org>
- Discussions-To:
- Discourse thread
- Status:
- Draft
- Type:
- Standards Track
- Created:
- 05-Aug-2026
- Python-Version:
- 3.16
- Post-History:
- 11-Aug-2026
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 PEP는 두 개의 새로운 내장 함수 public()과 private()을 추가할 것을 제안합니다. 이 함수들은 모듈의 __all__을 해당 모듈에서 실제로 공개로 정의된 이름들과 동기화된 상태로 유지하여 모듈의 공개 인터페이스를 문서화합니다. 두 함수 모두 클래스 정의와 함수 정의에 데코레이터(@public과 @private)로 사용됩니다. 따라서 이름의 가시성은 그 이름이 정의되는 지점에서 정확히 한 번만 선언됩니다. public()은 상수처럼 데코레이터를 적용할 수 없는 이름을 위한 함수 호출 형태도 추가로 제공합니다.
예를 들면 다음과 같습니다:
# spam.py
@public
class Public:
...
@private
class Private:
...
public(SEVEN=7)
>>> import spam
>>> spam.__all__
['Public', 'SEVEN']
제안하는 의미 체계는 서드파티 atpublic 패키지의 의미 체계와 같습니다. 이 패키지는 2016년부터 이 기능을 제공해 왔습니다.
이 PEP는 PEP 842 및 PEP 843을 보완하는 문서입니다. Relationship to PEP 842 and PEP 843을 참조하십시오.
동기
모듈 전역 변수 __all__은 현재 Python이 모듈의 공개 이름을 선언하기 위해 정의한 메커니즘입니다. 하지만 __all__에는 잘 알려진 문제가 있습니다. 대개 별도의 리스트로 정의되는데, 이 리스트는 그 안에 이름이 담긴 객체들과 멀리 떨어져 있는 경우가 많습니다. 파일의 한 지점에서 정의된 객체가 다른 곳, 보통 파일 맨 위에 있는 __all__ 리스트에 문자열 리터럴로 반복됩니다.
둘을 동기화된 상태로 유지해 주는 장치가 없기 때문에 다음과 같은 문제가 생깁니다:
- 이름이 모듈에는 추가되지만
__all__에는 추가되지 않습니다. - 이름이 모듈에서는 제거되거나 변경되었지만
__all__에는 반영되지 않아,from spam import *가AttributeError를 일으킵니다. __all__에서 이름을 잘못 입력하거나 리스트 항목을 구분하는 쉼표를 빠뜨리기 쉽습니다.- 불일치는 한 방향으로만 감지할 수 있습니다. 린터는
__all__에 있지만 모듈에 객체로 존재하지 않는 이름에 플래그를 지정할 수 있습니다. 하지만__all__에서 누락된 공개 이름에는 어떤 도구도 플래그를 지정할 수 없습니다. 소스 어디에도 그 이름이 공개될 의도였다는 내용이 없기 때문입니다. - 코드를 읽는 사람은 “이 이름은 공개인가?”라는 질문에 답하려면 파일의 다른 부분(또는 다른 화면)으로 스크롤해야 합니다.
비공개 이름 앞에 밑줄을 붙이는 관례는 이와 관련은 있지만 다른 문제를 다룹니다. PEP 842는 접두사만으로는 충분한 해답이 되지 않는 이유를 자세히 설명합니다.
여기서 제안하는 패턴, 즉 데코레이터로 정의 지점에서 가시성을 선언하는 방식은 새롭거나 검증되지 않은 것이 아닙니다. PyPI의 atpublic 패키지는 10년 동안 이 패턴을 구현해 왔으며, 이미 여러 프로젝트가 이 패키지에 의존하고 있습니다. 이 PEP가 주장하는 것은 이 패턴이 서드파티 의존성 없이도 쓸 수 있어야 할 만큼 충분히 흔하고 유용하다는 점입니다. 따라서 atpublic의 public()과 private() 함수를 내장 함수에 추가할 것을 제안합니다.
__all__은 이미 공개 API를 정의합니다
Python에는 모듈의 어떤 이름이 공개이고 어떤 이름이 비공개인지 표현할 방법이 없으며, __all__은 from spam import *를 규율하는 관례에 불과하다는 말이 종종 나옵니다. 하지만 언어 레퍼런스는 명시적으로 다음과 같이 말합니다:
모듈이 정의하는 공개 이름은 모듈의 네임스페이스에서__all__이라는 이름의 변수를 확인하여 결정됩니다. 이 변수가 정의되어 있다면, 해당 모듈이 정의하거나 임포트한 이름들로 이루어진 문자열 시퀀스여야 합니다. […]__all__에 주어진 이름들은 모두 공개로 간주되며 반드시 존재해야 합니다.__all__이 정의되지 않은 경우, 공개 이름 집합에는 모듈의 네임스페이스에 있는 이름 중 밑줄 문자('_')로 시작하지 않는 모든 이름이 포함됩니다.__all__은 공개 API 전체를 포함해야 합니다. 이는 API의 일부가 아닌 항목(모듈 안에서 임포트하여 사용한 라이브러리 모듈 등)을 실수로 내보내지 않도록 하기 위한 것입니다.
이 PEP는 Python 언어 레퍼런스에 있는 “공개”의 정의를 명시적으로 채택합니다. 이에 따라 다음과 같은 결론이 나옵니다:
이 개념은 이미 존재하며 규범적입니다. “공개 이름(public name)”은 언어 레퍼런스가 정의하는 용어이며, 레퍼런스는 이를 __all__을 기준으로 정의합니다. 이것은 민간 전승이 아니라 명세 텍스트입니다. Python에 무엇이 공개인지 말할 방법이 없는 것이 아닙니다. 방법이 있고, 문서화되어 있습니다.
완전성은 이미 계약입니다. “__all__은 공개 API 전체를 포함해야 합니다”라는 문장은 모호하지 않습니다. __all__이 공개 표면의 일부만 나열하는 모듈은 어떤 대안적 해석을 따르고 있는 것이 아닙니다 – 레퍼런스가 말하는 __all__의 의미에 부합하지 않는 것입니다.
임포트된 모듈 문제는 이미 다루어지고 있습니다. 레퍼런스는 이를 명시적으로 언급합니다: __all__이 존재하는 이유 중 하나는 “API의 일부가 아닌 항목(모듈 내에서 임포트되어 사용된 라이브러리 모듈 등)을 실수로 내보내는 것”을 피하기 위해서입니다. __all__을 정확하게 선언하고 argparse를 임포트하는 모듈은, 임포트 이름을 _argparse로 바꾸든 바꾸지 않든, argparse라는 이름을 자신의 공개 API 일부로 누출하지 않습니다.
그렇다면 부족한 것은 의미론이 아니라 사용 편의성입니다. Python은 이름이 공개라는 것이 무엇을 의미하는지 이미 명시하고 있고, __all__이 이를 빠짐없이 나타내도록 이미 권장하고 있지만, 모듈이 발전하는 동안 그 약속을 지킬 편리한 방법은 제공하지 않습니다. 작성자에게 문자열 리터럴 목록을 정의와는 떨어진 파일의 다른 부분에서 손으로 유지하도록 요구하며, 이는 오류가 발생하기 상당히 쉽습니다.
이 PEP는 부족한 사용 편의성을 제공합니다. 이 PEP는 “공개”가 무엇을 의미하는지 재정의하거나, 두 번째 가시성 개념을 도입하거나, __all__이 이미 의미하는 바를 변경하지 않습니다. 이름이 내보내진다는 것이 무엇을 의미하는지 재정의하려 하지도 않습니다. 다만 문서화된 계약을 실제로 지킬 수 있을 만큼 쉽게 만듭니다.
명세
두 개의 새로운 내장 함수 public()과 private()이 추가됩니다.
이 PEP는 모듈 수준의 가시성만 다룹니다. public()과 private()은 모듈의 전역 이름 중 어떤 것이 공개 인터페이스를 구성하는지 선언하며, 모듈에 대해서만 정의되고 그 외에는 정의되지 않는 __all__을 유지하는 방식으로 이를 수행합니다. 다른 스코프에서의 가시성은 명시적으로 제외됩니다. 클래스 속성과 메서드, 함수의 지역 이름, 중첩 스코프에서 바인딩된 이름은 모두 이 제안의 영향을 받지 않습니다. Python에는 그러한 스코프를 위한 __all__ 대응물이 없으며, 이 PEP도 그런 것을 제안하지 않습니다. 메서드가 클래스의 공개 인터페이스에 속하는지는 지금과 마찬가지로 명명 규칙과 문서화의 문제로 남습니다.
public()
public()에는 두 가지 호출 형식이 있습니다. 데코레이터 형식(@public)이 가장 일반적인 사용법입니다.
데코레이터 형식. __module__과 __name__ 속성을 모두 가진 위치 인자 하나로 – 즉 함수나 클래스로 – 호출되면, public()은 그 객체의 __name__을 public()이 호출된 모듈의 __all__에 추가하고 객체를 변경 없이 반환합니다:
@public
def foo():
...
@public
class Bar:
...
# __all__ == ['foo', 'Bar']
괄호 없는 데코레이터를 사용한다는 점에 유의하십시오. Python의 의미에 따라, 데코레이터가 장식하는 객체가 데코레이터 함수의 첫 번째 인자로 암묵적으로 전달됩니다.
함수 호출 형식. 상수, 인스턴스, 별칭처럼 데코레이트할 수 없는 이름은 키워드 인자로 public()을 호출하여 선언합니다. 각 키워드는 호출한 모듈의 전역 네임스페이스에 그 값을 바인딩하고 동시에 그 이름을 __all__에 추가합니다:
public(SEVEN=7)
public(a_bar=Bar())
public(ONE=1, TWO=2)
키워드 인자가 하나이면 그 값이 반환되고, 여러 개이면 값들의 튜플이 순서대로 반환됩니다:
a, b, c = public(a=3, b=2, c=1)
d = public(d=9)
모든 경우에, public()은 자신이 호출된 모듈의 __all__만 수정합니다. 다른 모듈의 __all__은 결코 영향을 받지 않습니다.
모듈이 아직 __all__을 정의하지 않았다면, public()은 이름을 추가하기 전에 __all__을 빈 list로 생성합니다. __all__이 존재하지만 리스트가 아니면 ValueError가 발생합니다. 기존 __all__에 이미 들어 있는 문자열은 리스트에 그대로 남습니다. 추가 동작은 멱등적이므로, 이미 __all__에 있는 이름은 두 번 추가되지 않습니다.
private()
private()는 (@private 형태로만 사용되며) public()의 데코레이터 형태와 짝을 이루는 반대 기능입니다. 이 데코레이터는 어떤 이름이 모듈의 공개 인터페이스에 속하지 않음을 문서화하고, 그 이름이 __all__에 나타나지 않도록 보장하며, 이미 들어 있다면 제거합니다. 데코레이트된 객체는 변경 없이 그대로 반환됩니다:
@private
def helper():
...
public()과 달리 private()는 절대 __all__을 생성하지 않습니다. 모듈이 __all__을 정의하지 않으면 @private는 모듈 네임스페이스에 아무런 영향도 주지 않으며, 순전히 정의 지점에서 작성자의 의도를 문서화하는 역할만 합니다. __all__이 존재한다면 리스트여야 하며, 그렇지 않으면 ValueError가 발생합니다. 데코레이트된 객체의 이름이 그 안에 있으면 제거됩니다.
@private는 의도적으로 빈 __all__을 생성하지 않습니다. 그렇게 하면 from spam import *의 의미가 조용히 바뀌기 때문입니다. __all__이 없으면 와일드카드 임포트는 밑줄로 시작하지 않는 모든 이름을 바인딩하지만, __all__ = []이면 아무것도 바인딩하지 않습니다. 문서화가 목적인 데코레이터가 그런 효과를 내서는 안 됩니다.
따라서 @private만으로는 이름이 from spam import *에서 제외되지 않습니다. 이름을 제외하는 것은 @public의 역할입니다. 모듈의 어떤 이름이든 공개로 표시되는 순간 __all__이 존재하게 되고, 공개로 표시되지 않은 모든 것은 자동으로 제외됩니다. @private는 작성자의 의도를 기록하고, @public은 그 의도를 관찰 가능하게 만듭니다.
Note
private()는 함수 호출 형태를 지원하지 않습니다. atpublic 패키지 사용자들이 이에 대한 타당한 사용 사례를 찾아내거나 요청한 적이 없기 때문입니다. 자세한 논의는 Open Issues를 참조하십시오.
제약 사항
@public과 @private는 모듈 전역 변수 __all__을 동기화된 상태로 유지하기 위해 존재하므로, 모듈 수준 객체만 선언할 수 있습니다. __all__은 클래스 내용이 아니라 모듈 내용을 문서화하므로, 클래스 본문 안의 메서드를 데코레이트하는 것은 지원되지 않습니다.
두 함수 모두 자신이 호출된 스코프를 검사하지 않으므로, 이러한 오용은 현재 진단되지 않습니다. 메서드에 적용된 데코레이터는 메서드 이름을 이를 둘러싼 모듈의 __all__에 추가하며, 클래스 본문에서 사용된 함수 호출 형태는 키워드를 클래스 본문이 아닌 모듈 전역에 바인딩합니다. 어느 결과도 작성자가 의도한 바일 가능성은 낮습니다. 이러한 경우에 대신 예외를 발생시켜야 하는지는 Open Issues에서 다룰 문제입니다.
이 함수들이 __all__에 이름을 추가하려면 __all__이 가변이어야 하므로, __all__을 직접 할당하는 모듈은 리스트를 할당해야 합니다. 불변 __all__을 원하는 모듈은 마지막 선언 후에 __all__ = tuple(__all__)로 이를 고정할 수 있습니다.
근거
왜 내장 함수로 제공합니까?
모듈의 공개 인터페이스를 선언하는 일은 기본적인 속성이며, 특히 코드베이스가 커질수록 자주 요청되는 속성입니다. 논의 과정에서 많은 사용 사례가 확인되었으며, 여러 라이브러리와 애플리케이션에서 저마다 다른 접근 방식이 개발되었습니다. atpublic이 존재해 온 10년 동안 충분한 경험이 쌓였고, 그 결과 이처럼 기본적인 것을 표현하는 데 서드파티 의존성(또는 모든 모듈 상단의 임포트)을 요구하는 것은 사용을 가로막는 마찰이라는 점이 분명해졌습니다. 또한 이는 가장 중요한 바로 그 지점, 즉 표준 라이브러리 자체와 작은 단일 파일 모듈에서 특히 불편합니다.
atpublic은 현재 인터프리터 시작 시 public과 private을 builtins에 주입하여 임포트가 필요 없게 하는 선택적 설치 단계(pip install atpublic[install])를 제공함으로써 이 점을 인정하고 있습니다. 이런 것이 존재한다는 사실 자체가 이러한 유틸리티를 두기에 가장 편리한 위치가 내장이라는 증거입니다.
왜 데코레이터입니까?
데코레이터는 선언을 정의가 있는 바로 그 자리에 두며, 이것이 핵심의 전부입니다.
기계적인 이점은 이름이 단 한 번만 나타난다는 것입니다. 선언과 정의가 서로 어긋날 수 없고, 리팩터링 도구가 별도의 수고 없이 이름을 올바르게 바꾸어 주며, 관리해야 할 두 번째 목록이 없고, 같은 내용을 반복해야 할 필요도 대부분 사라집니다.
문서화 측면의 이점도 그에 못지않게 중요합니다. @public과 @private은 독자가 이미 보고 있는 줄에 작성자의 의도를 기록합니다. “이것이 API의 일부인가?”라는 질문에 답하기 위해 파일의 다른 곳에 있는 목록으로 스크롤할 필요도, 그 목록을 정의들과 대조할 필요도, 앞에 붙은 밑줄이 의도적인 것인지 추측할 필요도 없습니다. 선언은 더 이상 정의에 덧붙은 장부 관리가 아니라 정의의 일부가 됩니다.
@private이 이를 가장 분명하게 보여 줍니다. __all__이 없는 모듈에서 이 데코레이터는 기계적으로 아무 일도 하지 않습니다. 이름을 추가하지도, 제거하지도 않으며, 동작을 바꾸지도 않습니다. 이 데코레이터의 가치는 오로지 정의 지점에서 해당 이름이 의도적으로 공개되지 않는다는 것을 밝히는 데 있습니다.
함수 호출 형태의 정적 분석
이 제안에 대한 가장 강력한 반론은 함수 호출 형태에 관한 것이며, 이를 명시적으로 짚어 둘 가치가 있습니다. 다음과 같은 코드가 있다고 합시다:
public(SEVEN=7)
SEVEN은 호출자의 프레임에 접근하는 함수에 의해 모듈의 전역 변수에 바인딩됩니다. 그 바인딩에 관한 어떤 것도 문법 트리에 드러나지 않습니다. 소스를 읽는 타입 검사기, 린터, 언어 서버는 대입 없이 단순한 함수 호출만 보게 되므로, 모든 사용 지점에서 SEVEN을 정의되지 않은 것으로 보고할 것입니다. PEP 842가 제안하는 export SEVEN = 7 같은 소프트 키워드에는 그런 문제가 없습니다. 문법은 그 구조상 파일을 파싱하는 모든 것에 보이기 때문입니다. PEP 842에서 제기된 DRY 반론이 아니라 바로 이것이 키워드 대신 내장 함수를 선택하는 데 따르는 실제 비용입니다.
이 문제는 향후 린팅 도구가 public()의 함수 호출 형태를 명시적으로 인식하도록 수정되면 쉽게 완화될 수 있습니다. 이는 모든 Python 프로그래머가 지속적으로 치르는 비용이 아니라, 소수의 도구가 한 번만 치르는 한정된 비용입니다.
public()은 알 수 없는 마법을 부리는 임의의 함수가 아닙니다. 이는 작고 고정되어 있으며 명세된 시그니처를 가진 내장 함수이고, 모듈 네임스페이스에 미치는 효과는 호출 지점의 키워드 이름에 의해 완전히 결정되며, 그 이름들은 소스에 문자 그대로 존재합니다. public(SEVEN=7)이 SEVEN을 바인딩하고 "SEVEN"을 __all__에 추가한다는 것을 검사기에 가르치는 일은 이러한 도구들이 쉽게 수행할 수 있는 간단한 분석입니다.
직접적인 선례가 있습니다. 실제 코드가 바로 그렇게 하기 때문에, 정적 분석기는 이미 __all__ += [...]나 __all__.append(...)처럼 단순 할당을 넘어서는 __all__ 변경까지 모델링하고 있습니다. 정적 분석기는 이미 namedtuple(), TypedDict, dataclass()처럼 문법만으로는 동작이 드러나지 않는, 네임스페이스를 만드는 호출 가능 객체를 특별하게 처리하고 있습니다. 그 목록에 public()를 추가하는 것은 이 도구들이 이미 해 온 작업에 조금 더하는 것일 뿐, 새로운 종류의 문제가 아닙니다.
이 PEP가 채택되면, 도구는 언어가 제공하는 것을 지원한다는 평범한 이유로 그러한 지원이 빠르게 뒤따를 것으로 예상됩니다. 그동안에는 (그리고 이전 버전의 도구에서는) public()의 반환값을 사용하여 특별한 지원이 전혀 필요 없는, 완전히 명시적인 표기를 쓸 수 있습니다:
SEVEN = public(SEVEN=7)
여기서 바인딩은 평범한 대입이므로, Python을 파싱하는 모든 도구에 보입니다. 이 형식은 권장 표기가 아니라 과도기를 위한 보조 수단이며, 오래 필요하지는 않을 것입니다.
결론적으로, 데코레이터를 실제로 활용할 수 없는 곳인 데이터와 타입 별칭 사용 사례에는 새로운 문법이 전혀 필요하지 않습니다. 도구가 인식할 수 있는 함수 호출이면 새로운 전용 export 키워드 없이도 똑같이 제 역할을 합니다.
이것이 급한 일입니까?
Guido van Rossum이 PEP 842에 대해 이 질문을 제기했으며, 이 질문은 여기에도 똑같이 적용됩니다:
하지만 Python은 이 기능 없이 35년 넘게 존재해 왔습니다 – 정말 시급한 일입니까? 파이썬의 선(Zen of Python)을 기억하십시오. 거기에는 “지금 하는 것이 결코 하지 않는 것보다 낫습니다. 하지만 결코 하지 않는 것이 지금 당장 하는 것보다 나은 경우가 많습니다.”라고 쓰여 있습니다.
아닙니다. 이 PEP는 급하지 않으며, 급하다고 주장하지도 않습니다. 모듈 이름의 가시성이나 모듈이 내보내는 공개 API에 관한 어떤 것도 급하지 않습니다. 하지만 세 가지 이유로, 이 제안에는 긴급성이 적절한 판단 기준이 아닙니다.
이 기능은 새롭지 않습니다. 이 PEP는 Python에 검증되지 않은 아이디어를 채택하라고 요구하지 않습니다. atpublic은 2016년부터 바로 이 의미를 구현해 왔습니다. 이를 원하는 사용자는 이미 사용하고 있으므로, 질문은 “Python에 이것이 있어야 하는가?”가 아니라 “이것을 쓰는 데 서드파티 의존성이라는 비용을 치러야 하는가?”입니다. 10년에 걸친 실제 운용 경험은 서두름과는 정반대입니다. 그 사용 경험을 통해, 새로운 설계라면 추측해야 했을 경계 사례, 문법, 의미 체계가 이미 드러나고 정리되었습니다. 모듈 수준 객체에만 데코레이터를 적용할 수 있다는 점, 리스트가 아닌 __all__을 어떻게 처리할지, 그리고 함수 호출 형식이 무엇을 반환해야 하는지가 그것입니다.
틀렸을 때의 비용이 낮습니다. 긴급성 인자는 되돌릴 수 없는 변경에 대해 가장 큰 무게를 가집니다. 문법은 영구적입니다. 소프트 키워드는 문법을 영원히 제약하고, 미래의 모든 Python 프로그래머에게 가르쳐야 하며, 더 오래된 인터프리터를 지원하는 모듈에서는 사용할 수 없습니다. 런타임에 영향을 미치는 새로운 모듈 수준 변수는 경고 없이 코드의 관찰 가능한 동작을 바꿉니다. 내장 함수는 두 가지 측면 모두에서 이 설계 공간에서 가장 비용이 적게 드는 선택지입니다. 호출되기 전까지는 아무 작용도 하지 않고, 이를 무시하는 모듈에는 아무것도 바꾸지 않으며, 실수로 판명되더라도 문법을 건드리지 않고 일반적인 방식으로 폐지할 수 있습니다.
시기보다 순서가 더 중요합니다. 이번 주기에는 세 가지 제안이 같은 문제 영역을 다루고 있으며, 그중 두 가지는 새로운 문법을 요구합니다. Python이 이 필요를 해결하기 위해 문법을 변경하려 한다면, 그 결정은 문법 변경이 필요 없는 선택지를 검토하기 전이 아니라 검토한 후에 내려져야 합니다. 일단 export 키워드가 생기면, 같은 영역을 다루는 내장 함수는 더 나은 해답이었는지와 관계없이 불필요해지므로 결코 추가되지 않을 것입니다. 이러한 비대칭성이 바로 이 PEP를 나중이 아니라 지금 검토해야 하는 이유입니다. 이 기능이 시급해서가 아니라, 비용이 큰 대안이 도입되고 나면 비용이 적은 대안은 더 이상 선택할 수 없게 되기 때문입니다.
임포트 시점 성능
몇 년 전, PEP 842와 PEP 843 중 어느 것도 존재하지 않던 때에 이 아이디어를 코어 개발자들에게 비공식적으로 제시했을 때, 제기된 반대는 설계에 대한 것이 아니라 유용성에 견준 비용에 대한 것이었습니다. 데코레이터는 임포트 시점에 데코레이트된 이름마다 한 번씩 실행되며, CPython의 시작 시간은 면밀하게 주시되는 수치입니다. 이는 정당한 우려이며 직접적인 답변을 받을 만합니다.
호출당 작업은 작고 한정되어 있습니다. 데코레이터 형태의 public()은 데코레이터가 적용된 객체의 __name__을 읽고, 그 객체를 정의한 모듈의 전역 변수를 가져오며, 필요하면 __all__을 빈 리스트로 만든 다음 문자열 하나를 추가합니다. 복잡한 인트로스펙션도, 모듈 크기에 비례하는 할당이나 작업도, I/O도 없습니다. 상수 계수가 얼마로 나타나든 그 값은 모듈 크기에 따라 커지지 않습니다.
비용은 선택적이며 공개 API에 비례합니다. public()을 호출하지 않는 모듈은 아무 비용도 치르지 않습니다. 이는 참여 여부와 관계없이 모든 모듈에 영향을 미치는 모듈 속성 접근 변경과 다른 점입니다. 이 함수를 호출하는 모듈은 공개 이름마다 한 번씩 비용을 치르며, 모듈의 공개 표면은 일반적으로 그 모듈이 정의하는 이름 가운데 작은 일부에 불과합니다.
문법이라고 비용이 없는 것도 아닙니다. 대안이 실제로 무엇을 절약하는지 정확히 짚어 볼 필요가 있습니다. PEP 842의 export 문은 이름이 전역 변수에 있는지 확인하고, __export__가 없으면 만들고, list.append를 호출하도록 명세되어 있습니다 – 같은 연산을 함수 호출 대신 바이트코드로 표현한 것입니다. 절약되는 것은 함수 호출 디스패치이지 그 밑에서 이루어지는 작업이 아닙니다. 실제로 차이가 있기는 하지만, 이미 작은 상수에 곱해지는 상수 계수 수준의 차이일 뿐이며 본질적인 차이는 아닙니다.
C 구현은 실현 가능하며 빠릅니다. 이 점에서는 내장 함수가 서드파티 패키지보다 확실히 유리한 위치에 있습니다. atpublic은 한동안 public()의 C 구현을 제공했으며, 이 구현은 순수 Python 버전보다 상당히 빨랐습니다. 결국 이것은 제거되었는데, 동작하지 않아서가 아니라 서드파티 패키지에서 컴파일된 확장 모듈을 요구하는 것이 이렇게 작은 라이브러리에는 상당한 패키징 및 설치 부담이었기 때문입니다 – 그 부담은 오로지 순수 Python 대체 구현을 피하기 위해 짊어진 것이었습니다.
CPython에는 이러한 트레이드오프가 존재하지 않습니다. 내장은 인터프리터의 일부로 컴파일되므로, 빠른 구현이 곧 유일한 구현이 되며, wheel 플랫폼 지원 매트릭스도, 대체 경로도, 선택적 엑스트라도 필요 없습니다. 더욱이 인터프리터 내부의 C 구현은 어떤 서드파티 구현보다도 적은 작업만 수행할 수 있습니다. 데코레이터 형태는 순수 Python 구현에 필요한 __module__과 sys.modules 조회 대신 호출한 프레임의 전역 변수에 직접 접근할 수 있으며, 함수 호출 형태에는 Python 수준의 스택 검사가 필요하지 않습니다.
따라서 이 인자는 원래의 반론과 어느 정도 반대 방향입니다. 성능 우려는 public()을 빠르게 만들 수 없는 PyPI에 남겨 둘 이유가 아니라, 빠르게 만들 수 있는 내장 함수에 넣어야 할 이유입니다.
Note
이 절에서는 비용이 수용 가능하다고 주장할 뿐이며, 아직 이를 입증하지는 않습니다. 데코레이터를 적용한 표준 라이브러리와 인위적으로 만든 최악의 경우 모두에 대해 CPython 시작 벤치마크로 측정한 결과를 참조 구현과 함께 제시해야 합니다. Open Issues를 참조하십시오.
PEP 842 및 PEP 843과의 관계
요약하면, PEP 842는 현재 개정판에서 export 키워드와 새 모듈 전역 변수 __export__를 추가할 것을 제안합니다. PEP 843은 from ... export ... 형식을 추가할 것을 제안합니다.
하나가 아닌 두 가지 문제
모듈 가시성에 관한 논의는 서로 분리할 수 있는 두 가지 문제를 다룹니다:
- 관리 문제. 이름이 공개인지 비공개인지는 그 이름이 가리키는 객체를 정의하는 곳과 다른 곳에서 선언되므로, 선언이 구현과 점점 어긋나게 됩니다. 이는 선언을 어디에 추가하는가에 관한 문제입니다.
- 런타임에 미치는 영향.
__all__은 공개 API를 선언하지만, 그 선언이 실제로 적용되는 곳은from spam import *뿐입니다. 이것은 속성 접근,dir(),help(), 자동 완성에 아무런 영향을 주지 않으므로, 비공개로 유지하려는 이름도 이러한 방식의 모듈 인트로스펙션에서는 공개 이름과 구별되지 않습니다. 이는 선언이 무엇을 하는가에 관한 문제입니다.
이 PEP는 첫 번째 문제만 다룹니다. 이 PEP는 두 문제 중 첫 번째 문제가 더 시급하고 더 폭넓게 적용된다는 입장을 취합니다. 또한 첫 번째 문제는 새로운 문법이나 새로운 변수 없이 해결할 수 있으며, 이를 해결하더라도 두 번째 문제에 대한 특정한 답에 Python을 커밋하지 않는다는 입장을 취합니다.
왜 __export__가 아니라 __all__인가
PEP 842는 새로운 __export__ 변수를 제안합니다. 이 PEP는 __all__을 계속 사용할 것을 제안합니다.
PEP 842는 __all__이 부적절한 이유 두 가지를 제시합니다. 첫째는 __all__이 모듈과 어긋나 동기화가 깨진다는 것입니다. 이는 사실이며, 바로 atpublic과 이 PEP가 해결하는 문제입니다. 그러나 파일에서 똑같이 멀리 떨어진 곳에 두는 새로운 문자열 리터럴 리스트는 이 문제를 직접 해결하지 못합니다. PEP 842의 개정 이력도 스스로 이 점을 인정하며, 원래의 __export__ 전용 설계에 대한 Guido van Rossum의 말을 인용합니다:
하지만 사용 편의성은__all__과 비슷하며, 그 편의성은 나쁩니다. 리스트에 무언가를 추가하는 것(또는 제거하는 것!)을 잊기가 너무 쉽고, 내보내는 대상의 정의와 완전히 다른 파일 위치에서 내보내기 정보를 갱신해야 하는 것은 주의를 흩뜨립니다.클래스와 함수만 신경 쓴다면, 더 사용하기 편한 접근 방식은
@export데코레이터일 것입니다. 데이터나 타입 별칭을 내보내는 것까지 신경 쓴다면, 저는export라는 이름의 소프트 키워드(또는 더 나은 기본값을 위해private)를 추가하는 해법을 찾는 편을 훨씬 선호합니다.
어긋남은 __all__의 속성이 아니라 멀리 떨어진 곳에서 선언하는 방식의 속성입니다. 손으로 관리하는 변수는 모두 이 속성을 가지며, 정의 위치에서 관리하는 변수는 어느 것도 이 속성을 갖지 않습니다.
그 인용문의 앞부분은 이 PEP가 기반으로 삼는 인자입니다. 뒷부분은 클래스와 함수에 대한 사용하기 편한 해답으로 데코레이터를 지목합니다. 남은 질문, 즉 데코레이트할 대상이 없는 데이터와 타입 별칭을 어떻게 처리할 것인가는 함수 호출 형태의 정적 분석에서 다룹니다.
두 번째 이유는 실제로는 __all__이 공개 이름을 항상 빠짐없이 담고 있지는 않다는 것입니다. 모듈은 와일드카드 임포트를 하는 네임스페이스를 오염시키지 않으려고 공개 타입 별칭을 의도적으로 __all__에서 제외할 수 있으며, 그 결과 모듈의 공개 API가 __all__에 나열된 것의 상위 집합이 될 수 있습니다.
이는 기존 코드에 대한 정확한 관찰입니다. 하지만 처음 보이는 것보다 약한 인자입니다. 명세를 다르게 읽는 방식을 설명하는 것이 아니라 명세에서 벗어난 것을 설명하기 때문입니다. __all__은 이미 공개 API를 정의합니다에서 설명하듯이, 언어 레퍼런스는 이미 __all__이 “전체 공개 API를 포함해야 한다”고 명시합니다. 공개 이름을 __all__에서 빼 두는 모듈은 __all__이 공개 API보다 좁은 무언가를 의미한다고 주장하는 것이 아니라, import *에 대한 통제권을 얻는 대가로 명세 준수를 포기하는 것입니다.
그 맞교환이 드러내는 것은 실제 결함이지만, PEP 842가 진단하는 것과는 다른 결함입니다: __all__이 두 가지 역할을 겸한다는 것입니다. 그것은 무엇이 공개인지에 대한 선언인 동시에 와일드카드 임포트를 제어하는 수단이며, 이 두 목적이 충돌할 때 작성자들은 선언을 희생합니다. 실질적인 강제력을 가진 것은 와일드카드 동작뿐이기 때문입니다.
__export__를 도입해도 두 역할이 뒤섞인 이 문제는 바로잡히지 않습니다. 그렇게 하면 __all__은 여전히 두 역할을 모두 맡고, 첫 번째 선언과 동기화 상태를 유지해야 하는 두 번째 선언이 추가되며, “공개”라는 단어는 새 모듈 변수로 옮겨 가는 반면, 언어 레퍼런스는 계속해서 __all__을 기준으로 그 단어를 정의합니다. __export__를 정확하게 관리할 만큼 성실한 모듈이라면 __all__도 정확하게 관리할 만큼 성실했을 것입니다. 어긋나는 모듈은 양쪽 모두에서 어긋날 것입니다.
이 PEP는 내보내지 않은 이름에 대한 경고가 바람직한지에 대해서는 어떠한 입장도 취하지 않습니다. 이 PEP는 단지 기록 관리의 문제가 런타임 의미론의 문제와 분리될 수 있다는 점을 지적하고, 그중 전자에 답할 뿐입니다. public()은 리스트를 채웁니다. 나중에 Python이 어떤 리스트에 런타임 효과를 부여하기로 결정한다면, public()은 대신 그 리스트를 채우거나 둘 다 채울 수 있습니다. 여기에 있는 어떤 내용도 PEP 842의 가능성을 닫지 않습니다.
PEP 843이 좋은 동반자인 이유
이 PEP는 재내보내기에 대한 DRY 문제를 해결하지 않으며, 깔끔하게 해결할 수도 없습니다. 비공개 하위 모듈에서 이름을 가져오는 “허브 모듈”은 현재 각 이름을 세 번씩 써야 합니다:
from ._core import Widget
public(Widget=Widget)
Widget은 임포트하기 위해 한 번, 내보내기 위해 두 번 더 등장합니다. 이는 DRY를 크게 위반하는 것입니다! __all__을 손으로 관리하면 이름을 두 번만 쓰면 되므로, 특히 재내보내기에 대해서는 public()이 단순히 도움이 되지 않는 정도가 아니라 오히려 퇴보입니다.
여기에는 데코레이트할 대상이 없으므로 @public 데코레이터 형태를 사용할 수 없으며, public() 함수 호출 형태를 쓰려면 바인딩 이름을 명시적으로 지정해야 합니다. 이것이 바로 PEP 843이 지적하는 공백이며, PEP 843의 from ._core export Widget 표기는 어떤 데코레이터도 할 수 없는 방식으로 이 공백을 메웁니다.
따라서 두 제안은 문제를 깔끔하게 나누어 맡으며, 뛰어난 시너지를 냅니다:
public()과private()은 모듈이 정의하는 이름을 처리합니다.from <module> export <name>은 모듈이 그대로 전달하는 이름을 처리합니다.
둘 다 __all__을 채웁니다. 어느 쪽도 다른 쪽을 필요로 하지 않으며, 어느 쪽도 그 결과를 위해 새로운 런타임 의미 체계를 요구하지 않습니다.
Note
PEP 843은 이 PEP의 초안이 작성되던 중에 공개되었으며, 그 뒤로 PEP 842에도 이 PEP와 PEP 843 모두와 겹치는 자체 export 문이 추가되었습니다. 세 제안 사이의 관계는 토론 스레드에서 정리되어야 합니다. Open Issues를 참조하십시오.
하위 호환성
builtins에 이름을 추가해도 기존의 어떤 이름도 가리지 않지만, 모듈 수준에서 자체적으로 public 또는 private 이름을 정의하는 모듈에서는 반대로 그 이름이 내장 함수를 가리게 됩니다. 이는 다른 모든 내장 함수(id, type, list)와 같은 상황이며, 잘 알려진 사항입니다.
atpublic 패키지에서 public과 private을 임포트하는 코드는 명시적 임포트가 내장 함수를 가리므로 (의미 체계가 계속 일치하는 한) 변경 없이 계속 동작합니다.
이미 public 또는 private를 변수나 매개변수 이름으로 사용하는 코드는 flake8-builtins 및 이에 상응하는 ruff 규칙처럼 가려진 내장 이름에 플래그를 지정하는 린터에 걸리기 시작할 것입니다. 이는 동작상의 변화가 아니라 진단상의 변화이며, Python에 추가된 모든 내장 함수에서도 마찬가지였습니다. 이 변경이 기존 코드에 얼마나 영향을 미치는지는 측정되지 않았습니다.
이 내장 함수를 사용하는 모듈은 atpublic에 대한 의존성이나 호환성 심(shim) 없이는 Python 3.15 이하에서 실행되지 않습니다.
보안 영향
이 PEP에는 알려진 보안 영향이 없습니다. __all__ 자체와 마찬가지로, public()과 private()은 접근 제어 수단이 아니라 문서화 수단입니다.
교육 방법
public()과 private()은 다른 내장 함수와 함께 문서화되고, 튜토리얼에서 __all__을 소개하는 모듈 절에서 참조될 것입니다.
가르칠 규칙은 한 문장입니다: 모듈 사용자가 사용하도록 의도된 이름이라면 @public으로 데코레이트하고, 그렇지 않다면 데코레이트하지 마십시오(또는 이를 명시적으로 밝히려면 @private으로 데코레이트하십시오).
데코레이트할 수 없는 상수와 기타 이름에는 함수 호출 형태를 사용하며, 이 형태는 이름을 바인딩하는 동시에 공개 이름으로 표시합니다:
public(SEVEN=7)
이 형태는 대입문과 함께 쓰는 것이 아니라 대입문을 대체합니다. SEVEN = 7도 함께 작성하면 이름을 두 번 정의하게 되며, 이는 바로 이 내장 함수들이 없애려는 반복입니다.
도입은 점진적으로 할 수 있습니다. 직접 작성한 __all__이 있는 모듈은 이를 제거하지 않고도 정의에 데코레이터를 붙이기 시작할 수 있습니다. 이미 나열된 이름은 두 번 추가되지 않으므로, 두 방식은 무기한 공존할 수 있습니다.
참조 구현
PyPI에서 제공되며 2016년부터 유지 관리되어 온 atpublic 패키지는 제안된 의미 체계를 순수 Python으로 구현합니다. 이 패키지의 소스 저장소는 GitLab에 호스팅되어 있습니다.
CPython 구현은 아직 작성되지 않았습니다.
한동안 atpublic에는 public()의 C 구현도 포함되어 있었으며, 이는 순수 Python 구현보다 상당히 빨랐습니다. 이 구현은 패키징상의 이유로 제거되었는데, 그 이유는 내장 함수에는 해당하지 않습니다. 임포트 시점 성능를 참고하십시오.
한 가지 차이점은 짚어 둘 만합니다. atpublic 7.0.0 및 그 이전 버전은 위의 명세와 달리 @private의 경우에도 __all__을 생성합니다. 이는 이 PEP를 작성하는 과정에서 버그로 확인되었으며, 이 글을 쓰는 시점에 사전 릴리스 단계에 있는 atpublic 8.0.0에서 수정될 예정입니다.
기각된 아이디어
데코레이터 대신 새로운 export 문법
PEP 842는 현재 개정판에서 이 PEP와 같은 영역을 다루는 export 소프트 키워드를 제안합니다 – export def, export class, export NAME = value. 그 PEP의 거부된 아이디어 절은 내장 public 및 private 데코레이터를 검토하고, 이를 작성자가 문법 다음으로 선호하는 대안이라고 설명한 뒤, “이름을 중복하지 않고 단순 변수를 내보낼 쉬운 방법이 없다”는 이유로 거부합니다.
그 반론은 여기에서 제안하는 설계에는 온전히 적용되지 않습니다. 함수 호출 형식은 바로 데코레이터를 적용할 수 없는 경우를 위해 존재하며, 이름을 정확히 한 번만 씁니다:
public(SEVEN=7)
이것이 선언의 전부입니다. 이름 SEVEN은 모듈 전역에서 7에 바인딩되고, "SEVEN"이 __all__에 추가됩니다. 동기화 상태를 유지해야 할 별도의 대입문이 없습니다. export SEVEN = 7과 비교해 보십시오: 두 표기는 같은 정보를 담고, 입력하는 키 수도 거의 같으며, 둘 중 하나만 문법 변경을 요구한다는 점만 다릅니다.
이 반론의 실질적인 핵심은 키 입력 수가 아니라 도구 지원입니다: 소프트 키워드는 정적 분석에서 보이지만, 호출자의 프레임을 통해 이름을 바인딩하는 함수 호출은 보이지 않습니다. 이는 실제로 존재하는 비용이며, 이에 대해서는 함수 호출 형태의 정적 분석에서 답합니다.
일반적인 논거로서 이 인자는 이 예시를 넘어서도 성립합니다. 새로운 문법은 Python이 추가할 수 있는 것 중 가장 비용이 큰 것입니다. 새 문법은 가르쳐야 하고, 백포트할 수 없으며, 문법을 영구적으로 제약하고, 여전히 이전 인터프리터에서 실행되어야 하는 모든 모듈에서는 사용할 수 없습니다. 내장 함수에는 그러한 비용이 전혀 들지 않고, 이전 버전에서는 간단한 shim으로 쉽게 대체할 수 있으며, (이 경우처럼) 10년에 걸친 사용 경험이 뒷받침합니다.
새로운 __export__ 변수 추가
왜 __export__가 아니라 __all__인가을 참고하십시오.
PyPI에 그대로 두기
atpublic을 PyPI에 그대로 두는 것은 현상 유지 방안입니다. 이 기능을 선택적으로 사용하고자 하는 사용자는 해당 라이브러리를 의존성으로 추가하고 함수를 임포트하기만 하면 됩니다(또는 pip install atpublic[install] 엑스트라를 사용하여 내장에 채워 넣을 수도 있습니다).
그러나 이것이 정말로 지금 해결할 가치가 있는 문제라면, 이를 PyPI의 서드파티 패키지로 남겨 두는 것은 사용자에게 충분한 도움이 되지 않습니다. 의존성을 추가하고 명시적으로 임포트해야 한다는 점은 (작기는 하지만) 널리 사용되는 것을 막기에 충분한 걸림돌이 될 수 있습니다. 이를 내장에 추가하면 이 패턴을 공식적으로 지지하게 되므로 채택이 더 넓어질 것입니다.
내장 함수 대신 새로운 표준 라이브러리 모듈
이렇게 하면 서드파티 의존성 문제는 해결되지만, 명시적으로 임포트해야 하는 사용성 비용은 여전히 남습니다. 또한 표준 라이브러리에서 내장 이외에는 이를 추가할 명확한 위치가 없습니다. 함수 두 개를 위해 새 최상위 모듈을 만드는 비용을 감수할 가치는 없을 것입니다. 게다가 __all__은 어떤 의미에서 Python에 내장되어 있으므로, 이 함수들도 내장되어야 합니다.
미해결 문제
- 이 PEP와 PEP 842, PEP 843을 어떻게 조정해야 합니까? 현재 세 PEP 모두 정의 위치 선언 또는 재내보내기 선언 메커니즘을 포함하고 있으므로, 어느 하나라도 합당하게 수락되려면 먼저 이 중복을 해소해야 합니다.
atpublic이 제공하는 “여기서 정의된 것으로부터__all__을 추론하는” 휴리스틱 함수인populate_all()도 포함해야 합니까? 이 문제는 당분간 보류합니다. 휴리스틱은 두 가지 명시적 선언보다 내장 함수로 만들 근거를 제시하기가 더 어려우며, 모듈 가시성의 사용 편의성을 개선하는 데에도 덜 필수적입니다.- 대칭성을 위해
private()도 함수 호출 형식을 지원해야 합니까?atpublic은 그러한 형식을 제공하지 않으며, 그 필요성이 입증되거나 요청된 적도 없습니다. public()과private()가 모듈 스코프 밖에서 호출된 경우를 진단해야 합니까? 현재는 둘 다 자신을 호출한 스코프를 검사하지 않으므로, 메서드에@public을 적용하면 해당 메서드의 이름이 모듈의__all__에 조용히 추가됩니다. 예외를 발생시키면 더 친절하겠지만, 호출할 때마다 스코프를 검사하는 비용이 들며 이는 임포트 시점 성능와 관련이 있습니다.- 표준 라이브러리 자체도 이 데코레이터들을 채택해야 합니까? 그렇다면 어떤 일정으로 채택해야 합니까? 이 질문은 임포트 시점 성능와 얽혀 있으므로, 시작 시간 측정 결과를 확보한 뒤에 결정해야 합니다. 또한 모든 새로운 기능(예를 들어 지연 임포트)과 마찬가지로, Python의 정책은 일반적으로 새 기능을 수용하기 위해 표준 라이브러리를 일괄적으로 갱신하지 않는 것입니다. 이 새 함수들은 가장 큰 이점을 얻을 수 있는 모듈에서, 또는 모듈을 대대적으로 다시 작성할 때 기회가 닿는 대로 활용할 수 있습니다.
- C 구현에 대한 임포트 시간 벤치마크는 아직 수행되지 않았습니다.
감사의 글
이 제안의 계기가 된 PEP 842와 PEP 843을 작성한 Peter Bierma와 Neil Girdhar, 그리고 지난 10년간 atpublic에 기여한 기여자와 사용자에게 감사드립니다.
변경 이력
미정
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.