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 언어 레퍼런스에서 정의한 “공개”의 정의를 명시적으로 채택합니다. 이를 바탕으로 하면 다음과 같습니다:
이 개념은 이미 존재하며 규범적입니다. “공개 이름”은 언어 레퍼런스에서 정의하는 용어이며, __all__를 기준으로 정의합니다. 이는 민간 전승이 아니라 명세 텍스트입니다. Python에는 공개된 것을 나타낼 방법이 없는 것이 아닙니다. 그러한 방법이 이미 있으며, 문서화되어 있습니다.
완전성은 이미 계약입니다. “__all__에는 전체 공개 API가 포함되어야 합니다”라는 표현은 모호하지 않습니다. __all__에 공개 표면의 일부만 나열하는 모듈은 다른 해석을 적용하는 것이 아니라, 레퍼런스에서 설명하는 __all__의 의미를 준수하지 않는 것입니다.
임포트된 모듈 문제는 이미 범위에 포함되어 있습니다. 레퍼런스는 이를 명시적으로 언급합니다. __all__은 부분적으로 “API에 속하지 않는 항목(모듈 내부에서 임포트하여 사용한 라이브러리 모듈 등)을 실수로 내보내는 것”을 방지하기 위해 존재합니다. __all__을 정확히 선언하고 argparse를 임포트하는 모듈은 임포트 이름을 _argparse로 변경하든 하지 않든 argparse라는 이름을 자체의 공개 API의 일부로 누출하지 않습니다.
따라서 이 격차는 의미론적인 것이 아니라 사용성에 관한 것입니다. Python은 이름이 공개된다는 의미를 이미 명세하고 있으며, 모듈이 발전할 때 그 약속을 지킬 편리한 방법은 제공하지 않으면서 __all__이 이를 완전하게 나타내야 한다고 이미 권장합니다. 정의와는 파일의 다른 부분에서 문자열 리터럴 목록을 수작업으로 유지하도록 작성자에게 요구하므로, 상당히 오류가 발생하기 쉽습니다.
이 PEP는 누락된 사용성을 제공합니다. “공개”라는 의미를 재정의하거나, 가시성에 대한 두 번째 개념을 도입하거나, __all__이 이미 의미하는 바를 변경하지 않습니다. 이름이 내보내진다는 의미를 재정의하려고 하지도 않습니다. 그러나 문서화된 계약을 실제로 준수하기에 충분할 만큼 쉽게 만들어 줍니다.
명세
두 개의 새로운 내장 함수인 public()와 private()가 추가됩니다.
이 PEP는 모듈 수준의 가시성만 다룹니다. public()와 private()는 모듈의 전역 이름 중 공개 인터페이스를 구성하는 이름을 선언하며, 모듈에 대해서만 정의되고 다른 대상에는 정의되지 않는 __all__을 유지하여 이를 수행합니다. 그 밖의 모든 스코프에서의 가시성은 명시적으로 제외됩니다. 클래스 속성과 메서드, 함수에 지역적인 이름, 중첩 스코프에 바인딩된 이름은 모두 이 제안의 영향을 받지 않습니다. Python에는 그러한 스코프에 해당하는 __all__이 없으며, 이 PEP도 그러한 것을 제안하지 않습니다. 메서드가 클래스의 공개 인터페이스에 속하는지는 오늘날과 마찬가지로 명명 규칙과 문서에 달려 있습니다.
public()
public()에는 두 가지 호출 형식이 있습니다. 데코레이터 형식(@public)이 가장 일반적인 사용법입니다.
데코레이터 형식. __module__과 __name__속성을 모두 가진 단일 위치 인자, 즉 함수나 클래스를 사용하여 호출하면 public()은 public()이 호출된 모듈의 __all__에 해당 객체의 __name__을 추가하고 객체를 변경하지 않은 채 반환합니다.
@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()은 추가하기 전에 빈 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__ = tuple(__all__)을 사용하여 고정할 수 있습니다.
근거
왜 내장 함수입니까?
모듈의 공개 인터페이스를 선언하는 것은 기본적이며 코드베이스가 커질수록 자주 요청되는 속성입니다. 논의 과정에서 많은 사용 사례가 확인되었으며, 다양한 라이브러리와 애플리케이션에서 서로 다른 접근 방식이 개발되었습니다. atpublic의 존재 기간이 10년에 이르면서, 이처럼 기본적인 것을 표현하기 위해 서드파티 의존성(또는 모든 모듈의 최상단에 import 문)을 요구하는 것은 사용을 저해하는 마찰이라는 충분한 경험이 축적되었습니다. 또한 가장 중요한 곳, 즉 표준 라이브러리 자체와 작은 단일 파일 모듈에서 특히 어색합니다.
atpublic은 오늘날 선택적 설치 단계(pip install atpublic[install])를 제공하여 이를 인정합니다. 이 단계는 인터프리터 시작 시 builtins에 public과 private를 주입하므로 import가 필요하지 않습니다. 이것이 존재한다는 사실 자체가 builtins가 이러한 유틸리티를 두기에 가장 편리한 위치라는 증거입니다.
왜 데코레이터입니까?
데코레이터는 선언을 정의가 있는 정확한 위치에 배치하며, 그것이 바로 핵심입니다.
기계적인 이점은 이름이 한 번만 나타난다는 것입니다. 동기화에서 벗어날 수 없고, 리팩터링 도구가 이를 자동으로 올바르게 이름 변경하며, 유지 관리해야 할 두 번째 목록도 없고, 같은 내용을 반복해야 할 필요도 대부분 사라집니다.
문서화 측면의 이점도 그만큼 중요합니다. @public과 @private는 독자가 이미 보고 있는 줄에 작성자의 의도를 기록합니다. “이것이 API의 일부인가?”라는 질문에 답하기 위해 파일의 다른 곳에 있는 목록까지 스크롤하거나, 그 목록을 정의와 대조 확인하거나, 앞의 밑줄이 의도적인 것인지 추측할 필요가 없습니다. 선언은 더 이상 정의에 붙은 관리 작업이 아니라 정의의 일부가 됩니다.
@private는 이를 가장 명확하게 보여 줍니다. __all__이 없는 모듈에서는 기계적으로 아무 일도 하지 않습니다. 이름을 추가하지도, 제거하지도, 동작을 변경하지도 않습니다. 그 모든 가치는 정의 지점에서 해당 이름이 의도적으로 공개되지 않았음을 말하는 데 있습니다.
함수 호출 형식의 정적 분석
이 제안에 대한 가장 강력한 반론은 함수 호출 형식과 관련되며, 이를 명시적으로 언급할 가치가 있습니다. 다음을 가정하면:
public(SEVEN=7)
SEVEN은 호출자의 프레임에 접근하는 함수에 의해 모듈의 전역 네임스페이스에 바인딩됩니다. 해당 바인딩에 관한 어떤 내용도 구문 트리에는 나타나지 않습니다. 소스 코드를 읽는 타입 검사기, 린터 또는 언어 서버는 대입문 없이 단순한 함수 호출만 보므로, 모든 사용 위치에서 SEVEN을 정의되지 않은 이름으로 보고합니다. export SEVEN = 7과 같은 소프트 키워드는 PEP 842에서 제안된 것처럼, 파일을 구문 분석하는 모든 대상이 구문을 본질적으로 볼 수 있으므로 이러한 문제가 없습니다. 이것이 PEP 842에서 제기한 DRY 반론이 아니라, 키워드 대신 내장 함수를 선택하는 데 따르는 진정한 비용입니다.
린팅 도구를 향후 수정하여 public()의 함수 호출 형식을 명시적으로 인식하도록 하면 이 문제를 쉽게 완화할 수 있습니다. 이는 소수의 도구가 한 번만 부담하면 되는 제한된 비용이지, 모든 Python 프로그래머가 지속적으로 부담해야 하는 비용이 아닙니다.
public()은 신비한 마법을 수행하는 임의의 함수가 아닙니다. 이는 작고 고정되어 명시된 시그니처를 가진 builtin이며, 모듈 네임스페이스에 미치는 효과는 호출 지점의 키워드 이름에 의해 완전히 결정됩니다. 해당 이름은 소스에 문자 그대로 존재합니다. public(SEVEN=7)이 SEVEN을 바인딩하고 "SEVEN"을 __all__에 추가한다는 것을 검사기에 가르치는 일은 이러한 도구가 쉽게 수행할 수 있는 간단한 분석입니다.
직접적인 선례가 있습니다. 정적 분석기는 단순한 할당을 넘어 __all__변이를 이미 모델링하며, 실제 코드가 이를 수행하기 때문에 __all__ += [...] 및 __all__.append(...)도 포함합니다. 또한 문법만으로 동작이 명확하지 않은 네임스페이스 생성 호출 가능 객체를 이미 특별히 처리하며, 그 예로는 namedtuple(), TypedDict, dataclass()가 있습니다. 해당 목록에 public()를 추가하는 것은 이러한 도구가 이미 수행한 작업을 확장하는 것이지, 새로운 문제 범주를 만드는 것이 아닙니다.
이 PEP가 승인되면, 도구는 언어가 제공하는 기능을 지원한다는 일반적인 이유로 그러한 지원이 신속하게 뒤따를 것으로 예상됩니다. 그동안(그리고 이전 도구 버전에서는) public()의 반환값이 특별한 지원을 전혀 요구하지 않는 완전히 명시적인 표기를 제공합니다:
SEVEN = public(SEVEN=7)
여기서 바인딩은 파이썬을 구문 분석하는 모든 도구에서 볼 수 있는 단순한 할당입니다. 이 형식은 권장 표기라기보다 전환을 돕기 위한 것이며, 오랫동안 필요하지는 않을 것입니다.
결론적으로 데코레이터를 실제로 활용할 수 없는 영역인 데이터 및 타입 별칭 사용 사례에는 새로운 문법이 전혀 필요하지 않습니다. 도구가 인식할 수 있는 함수 호출이면 새로운 전용 export키워드 없이도 충분히 동일한 역할을 수행합니다.
이것이 긴급합니까?
Guido van Rossum이 PEP 842에 관해 이 질문을 제기했으며, 여기에도 똑같이 적용됩니다:
하지만 파이썬은 35년 넘게 이 기능 없이 존재해 왔습니다. 정말 긴급합니까? “결코 하지 않는 것보다 지금 하는 것이 낫습니다. “아예 하지 않는 것이 바로 지금보다 나은 경우가 많기는 하지만.”
아닙니다. 이 PEP는 긴급하지 않으며, 긴급하다고 주장하지도 않습니다. 모듈 이름의 가시성이나 모듈이 내보내는 공개 API에 관한 어떤 사항도 긴급하지 않습니다. 그러나 이 특정 제안에 긴급성을 적용하는 것은 잘못된 판단 기준이며, 이유는 세 가지입니다.
이 기능은 새로운 것이 아닙니다. 이 PEP는 파이썬에 검증되지 않은 아이디어를 채택하라고 요구하지 않습니다. atpublic은 2016년부터 이러한 정확한 의미 체계를 구현해 왔습니다. 이 기능을 원하는 사용자는 이미 이를 사용할 수 있으므로, 질문은 “파이썬에 이 기능이 있어야 하는가?”가 아니라 “이 기능을 사용하려면 서드파티 의존성이 필요해야 하는가?”입니다. 10년에 걸친 실제 사용은 성급함과는 정반대입니다. 새 설계라면 추측해야 했을 경계 사례, 문법 및 의미 체계, 즉 모듈 수준 객체만 데코레이트할 수 있다는 점, 목록이 아닌 __all__을 어떻게 처리할지, 함수 호출 형식이 무엇을 반환해야 하는지 등이 이미 드러났고 정리되었습니다.
잘못되었을 때의 비용이 낮습니다. 긴급성 논리는 되돌릴 수 없는 변경에 가장 큰 비중을 둡니다. 문법은 영구적입니다. 소프트 키워드는 문법을 영원히 제약하고, 미래의 모든 파이썬 프로그래머에게 가르쳐야 하며, 이전 인터프리터를 지원하는 어떤 모듈에서도 사용할 수 없습니다. 런타임 결과를 수반하는 새로운 모듈 수준 변수는 경고 없이 코드의 관찰 가능한 동작을 변경합니다. 내장 함수는 이 설계 영역에서 두 가지 측면 모두 가장 비용이 낮은 선택입니다. 호출될 때까지 아무 동작도 하지 않고, 이를 무시하는 모듈에는 아무것도 변경하지 않으며, 실수로 판명되더라도 문법을 건드리지 않고 일반적인 방식으로 사용 중단할 수 있습니다.
순서가 시점보다 중요합니다. 이번 주기의 세 제안이 동일한 문제 영역을 다루며, 그중 두 가지는 새로운 문법을 요구합니다. 파이썬이 이 필요를 해결하기 위해 문법을 변경한다면, 문법 변경이 필요 없는 선택지를 검토한 후에 그 결정을 내려야 하며, 그 전에는 안 됩니다. 일단 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에는 존재하지 않습니다. 내장 함수는 인터프리터의 일부로 컴파일되므로 빠른 구현이 곧 그 구현이며, 휠 플랫폼 지원 매트릭스도, 폴백 경로도, 선택적 추가 기능도 없습니다. 더욱이 인터프리터 내부의 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__을 기준으로 정의하는 동안 “public”이라는 단어를 새로운 모듈 변수로 옮깁니다. __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가 작성되는 동안 PEP 843이 발표되었으며, 그 이후 PEP 842은 자체적인 export 문을 추가하여 이 PEP 및 PEP 843과 모두 겹치게 되었습니다. 세 가지 모두의 관계는 논의 스레드에서 정해야 합니다. Open Issues를 참조하십시오.
하위 호환성
관련 builtins에 이름을 추가해도 아무것도 가려지지 않지만, 자체 모듈 수준의 public 또는 private 이름을 정의하는 모듈에서는 그 이름이 대신 내장 이름을 가리게 됩니다. 이는 다른 내장 이름(id, type, list)의 경우와 동일하며, 잘 알려진 상황입니다.
atpublic패키지에서 public 및 private를 가져오는 코드는 명시적 가져오기가 해당 내장 이름을 가리므로 의미론이 계속 일치하는 한 변경 없이 계속 작동합니다.
이미 public 또는 private를 변수나 매개변수 이름으로 사용하는 코드는 flake8-builtins및 이에 상응하는 ruff규칙처럼 가려진 내장 이름을 표시하는 린터에서 경고를 발생시키기 시작합니다. 이는 동작 변경이 아니라 진단 변경이며, Python에 추가된 모든 내장 이름에서도 동일했습니다. 기존 코드 중 얼마나 많은 부분이 영향을 받는지는 측정되지 않았습니다.
이러한 내장 이름을 사용하는 모듈은 atpublic에 대한 의존성이나 호환성 심이 없으면 Python 3.15 및 이전 버전에서 실행되지 않습니다.
보안 영향
이 PEP에는 알려진 보안 영향이 없습니다. __all__자체와 마찬가지로 public()및 private()는 접근 제어가 아니라 문서화입니다.
가르치는 방법
public()및 private()는 다른 내장 이름과 함께 문서화되며, __all__이 소개되는 모듈 튜토리얼 절에서 참조됩니다.
가르칠 규칙은 한 문장으로 정리할 수 있습니다. 모듈 사용자가 사용하도록 의도된 이름에는 @public으로 데코레이트하고, 사용하지 않을 이름이라면 데코레이트하지 마십시오(또는 이를 명시적으로 나타내기 위해 @private로 데코레이트하십시오).
상수와 데코레이트할 수 없는 기타 이름에는 이름을 바인딩하는 동시에 공개 이름으로 표시하는 함수 호출 형식을 사용합니다:
public(SEVEN=7)
이는 대입문을 함께 작성하는 것이 아니라 대입문을 대체합니다. SEVEN = 7도 작성하면 이름이 두 번 정의되며, 이러한 내장 이름은 바로 이 반복을 제거하기 위해 존재합니다.
도입은 점진적으로 진행할 수 있습니다. 직접 작성한 __all__을 사용하는 모듈은 이를 제거하지 않고 정의에 데코레이터를 적용하기 시작할 수 있습니다. 이미 목록에 있는 이름은 두 번 추가되지 않으므로 두 스타일은 무기한 공존할 수 있습니다.
참조 구현
PyPI에서 제공되고 2016년부터 유지 관리되어 온 atpublic 패키지는 제안된 의미론을 순수 Python으로 구현합니다. 이 패키지의 source repository는 GitLab에서 호스팅됩니다.
CPython 구현은 아직 작성되지 않았습니다.
한동안 atpublic도 순수 Python 구현보다 훨씬 빠른 public()의 C 구현을 포함했습니다. 내장 기능에는 적용되지 않는 패키징상의 이유로 이를 제거했습니다. 성능에 대해서는 임포트 시 성능를 참조하십시오.
한 가지 차이점은 언급할 가치가 있습니다. atpublic 7.0.0 및 이전 버전은 위 사양과 달리 @private의 경우 __all__을 생성합니다. 이는 이 PEP를 작성하는 동안 버그로 확인되었으며, 이 글을 작성하는 시점에 사전 릴리스 중인 atpublic 8.0.0에서 수정될 예정입니다.
거부된 아이디어
데코레이터 대신 새로운 export 구문
현재 개정판의 PEP 842는 이 PEP와 동일한 범위를 다루는 export 소프트 키워드를 제안합니다. 즉, export def, export class, export NAME = value입니다. 해당 Rejected Ideas절에서는 내장 public 및 private 데코레이터를 검토하고, 이를 구문에 대한 저자의 차선의 선호 대안으로 설명하며, “이름을 중복하지 않고 단순 변수를 내보낼 쉬운 방법이 없다”는 이유로 거부합니다.
이러한 반론은 여기서 제안하는 설계에는 완전히 적용되지 않습니다. 함수 호출 형식은 데코레이터를 적용할 수 없는 경우를 위해 정확히 존재하며, 이름을 한 번만 작성합니다.
public(SEVEN=7)
이것이 전체 선언입니다. 이름 SEVEN은 모듈 전역에서 7에 바인딩되고, "SEVEN"은 __all__에 추가됩니다. 동기화해야 할 별도의 할당은 없습니다. export SEVEN = 7과 비교해 보십시오. 두 표기는 동일한 정보를 담고, 대략 동일한 키 입력이 필요하며, 둘 중 하나만 문법 변경을 요구한다는 점에서만 다릅니다.
이 반론의 본질적인 문제는 키 입력이 아니라 도구 지원입니다. 소프트 키워드는 정적 분석에 표시되지만, 호출자의 프레임을 통해 바인딩하는 함수 호출은 표시되지 않습니다. 이는 실제 비용이며, 함수 호출 형식의 정적 분석에서 이에 대한 답을 제시합니다.
일반적인 주장은 이 예를 넘어 성립합니다. 새로운 구문은 Python이 추가할 수 있는 가장 비용이 큰 요소입니다. 이를 가르쳐야 하고, 하위 버전으로 백포트할 수 없으며, 문법을 영구적으로 제약하고, 이전 버전 인터프리터에서 계속 실행되어야 하는 모든 모듈에서는 사용할 수 없습니다. 내장 기능은 이러한 비용이 전혀 없고, 이전 버전에서는 간단히 심을 수 있으며, (이 경우처럼) 10년간의 사용 경험을 바탕으로 합니다.
새로운 __export__ 변수 추가
자세한 내용은 왜 __export__가 아니라 __all__인가을 참조하십시오.
PyPI에 그대로 두기
atpublic를 PyPI에 그대로 두는 것은 현재 상태를 유지하는 선택지입니다. 이 기능을 사용하려는 사용자는 해당 라이브러리를 종속성으로 추가하고 함수를 가져오기만 하면 됩니다. (또는 pip install atpublic[install]추가 기능을 사용하여 내장 기능을 채울 수도 있습니다.)
그러나 지금 해결할 가치가 있는 문제라면, 이를 PyPI의 서드파티 패키지에 그대로 두는 것은 사용자를 충분히 지원하지 못합니다. 종속성을 포함하고 명시적으로 가져와야 한다는 점이 (작기는 하지만) 사용의 확산을 막을 만큼 충분한 장애물이 될 수 있습니다. 이를 내장 기능에 추가하면 이러한 패턴을 공식적으로 뒷받침하게 되어 채택이 확대될 것입니다.
내장 기능 대신 새로운 표준 라이브러리 모듈
이렇게 하면 서드파티 의존성 문제는 제거되지만, 명시적 임포트의 사용성 비용은 여전히 남습니다. 또한 표준 라이브러리에 추가할 뚜렷한 위치가 builtins 외에는 없습니다. 두 함수를 위해 새로운 최상위 모듈을 추가하는 비용은 아마도 그만한 가치가 없습니다. 게다가 __all__은 어떤 의미에서는 Python에 내장되어 있으므로, 이 함수들도 내장되어야 합니다.
미해결 문제
- 이 PEP와 PEP 842, PEP 843을 어떻게 조정해야 합니까? 현재 세 문서 모두 정의 위치 또는 재내보내기 선언 메커니즘을 포함하고 있으므로, 어느 하나라도 합리적으로 수용하기 전에 중복되는 부분을 해결해야 합니다.
populate_all(), 즉atpublic의 휴리스틱한 “여기에서 정의된 항목으로부터__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.