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

Python 개선 제안 한국어 번역

PEP 442 – 안전한 객체 종료화

Author:
Antoine Pitrou <solipsis at pitrou.net>
BDFL-Delegate:
Benjamin Peterson <benjamin at python.org>
Status:
Final
Type:
Standards Track
Created:
18-May-2013
Python-Version:
3.4
Post-History:
18-May-2013
Resolution:
Python-Dev message

Table of Contents

번역·라이선스 안내

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

초록

이 PEP는 객체 종료화의 현재 한계를 다루는 것을 제안합니다. 목표는 객체 그래프 내에서의 위치와 무관하게, 모든 객체에 대해 종료자를 정의하고 실행할 수 있도록 하는 것입니다.

이 PEP는 Python 코드의 어떠한 변경도 요구하지 않습니다. 기존 종료자를 가진 객체는 자동으로 이점을 얻게 됩니다.

정의

참조
한 객체에서 다른 객체로 향하는 방향성 링크입니다. 참조의 대상은, 원본이 그 자체로 살아 있고 참조가 지워지지 않는 한, 참조에 의해 계속 살아 있게 됩니다.
약한 참조
한 객체에서 다른 객체로 향하는 방향성 링크이지만, 대상을 계속 살아 있게 하지 않습니다. 이 PEP는 비약한(non-weak) 참조에 초점을 둡니다.
참조 순환
객체들 사이의 방향성 링크로 이루어진 순환 부분 그래프로서, 순수한 참조 카운팅 방식에서는 해당 객체들이 수집되지 못하게 합니다.
순환 고립체(CI)
외부에서 참조되는 객체가 하나도 없고, 하나 이상의 참조 순환을 포함하며, 또한 그 안의 객체들이 여전히 사용 가능하고 손상되지 않은 상태여서 각자의 파이널라이저에서 서로 접근할 수 있는 독립적인 객체 서브그래프입니다.
순환 가비지 컬렉터(GC)
순환 고립체(cyclic isolate)를 탐지하여 순환 쓰레기(cyclic trash)로 전환할 수 있는 장치입니다. 순환 쓰레기 안의 객체들은 참조가 정리되고 참조 카운트가 0으로 떨어지는 자연스러운 효과에 의해 결국 폐기됩니다.
순환 쓰레기(CT)
GC에 의해 객체들이 정리되기 시작한, 이전의 순환 고립체입니다. 순환 쓰레기 안의 객체들은 잠재적인 좀비이며, Python 코드에서 접근될 경우 그 증상은 이상한 AttributeError부터 충돌까지 다양할 수 있습니다.
좀비 / 손상된 객체
순환 쓰레기의 일부인 객체입니다. 이 용어는 해당 객체가 안전하지 않음을 강조합니다: 이 객체의 나가는 참조가 이미 정리되었을 수도 있고, 이 객체가 참조하는 객체 중 하나가 좀비일 수도 있습니다. 따라서 (파이널라이저 등) 임의의 코드에서 접근해서는 안 됩니다.
파이널라이저
객체가 폐기되려 할 때 호출되는 함수 또는 메서드입니다. 파이널라이저는 해당 객체에 접근하여 그 객체가 보유한 자원(예: 뮤텍스나 파일 디스크립터)을 해제할 수 있습니다. 예로는 __del__ 메서드가 있습니다.
부활
종료자(finalizer)가 CI에서 객체에 대한 새 참조를 생성하는 과정입니다. 이는 __del__ 메서드의 특이하지만 지원되는 부작용으로 발생할 수 있습니다.

영향

이 PEP는 CPython 고유의 구현 세부 사항을 다루고 있지만, 종료 처리(finalization) 의미론의 변경은 Python 생태계 전반에 영향을 미칠 것으로 예상됩니다. 특히, 이 PEP는 “__del__ 메서드를 가진 객체는 참조 순환의 일부가 되어서는 안 된다”는 기존 지침을 무효화합니다.

이점

이 PEP의 주요 이점은 __del__ 메서드를 가진 객체나 finally 블록을 가진 제너레이터와 같이 종료자를 가진 객체에 관한 것입니다. 이러한 객체는 이제 참조 순환의 일부일 때도 회수될 수 있습니다.

이 PEP는 또한 추가적인 이점을 위한 길을 마련합니다:

  • 모듈 종료 절차는 더 이상 전역 변수를 None으로 설정할 필요가 없을 수 있습니다. 이는 잘 알려진 종류의 성가신 문제들을 해결할 수 있습니다.

이 PEP는 다음의 의미론은 변경하지 않습니다:

  • 참조 순환에 갇힌 약한 참조.
  • 사용자 정의 tp_dealloc 함수를 가진 C 확장 타입.

설명

참조 카운트 기반 폐기

일반적인 참조 카운트 기반 해제에서는 객체가 할당 해제되기 직전에 그 객체의 파이널라이저가 호출됩니다. 파이널라이저가 객체를 부활시키면 할당 해제는 중단됩니다.

하지만 객체가 이미 파이널라이즈된 경우에는 파이널라이저가 호출되지 않습니다. 이는 좀비를 파이널라이즈하지 못하도록 방지합니다(아래 참조).

순환 고립체의 해제

순환 고립체는 먼저 가비지 컬렉터에 의해 감지된 후 해제됩니다. 감지 단계는 변경되지 않으며 여기서는 설명하지 않습니다. CI의 해제는 전통적으로 다음 순서로 동작합니다:

  1. CI 객체에 대한 약한 참조가 정리되고, 그 콜백이 호출됩니다. 이 시점에서는 객체를 여전히 안전하게 사용할 수 있습니다.
  2. GC가 그 안의 모든 알려진 참조를 체계적으로 끊음(tp_clear 함수 사용)에 따라 CI는 CT가 됩니다.
  3. 아무것도 없습니다. 모든 CT 객체는 2단계에서(참조를 정리한 부수 효과로서) 이미 해제되었어야 하며, 이 수집은 완료됩니다.

이 PEP는 CI 해제를 다음 순서(새 단계는 굵게 표시)로 바꿀 것을 제안합니다:

  1. CI 객체에 대한 약한 참조가 정리되고, 그 콜백이 호출됩니다. 이 시점에서는 객체를 여전히 안전하게 사용할 수 있습니다.
  2. CI의 모든 객체에 대한 파이널라이저가 호출됩니다.
  3. CI가 여전히 고립되어 있는지 판단하기 위해 다시 순회합니다. CI 내의 객체 중 적어도 하나가 CI 외부에서 도달 가능한 것으로 판단되면, 이 수집은 중단되고 CI 전체가 부활합니다. 그렇지 않으면 진행합니다.
  4. GC가 tp_clear 함수를 사용하여 CI 내부의 알려진 모든 참조를 체계적으로 끊으면서, CI는 CT가 됩니다.
  5. 없음. 모든 CT 객체는 4단계에서 (참조를 지우는 것의 부수 효과로) 처리되었어야 하며, 이 수집은 완료됩니다.

Note

GC는 위의 2단계 이후 CI를 다시 계산하지 않으므로, 전체 서브그래프가 여전히 고립되어 있는지 확인하기 위해 3단계가 필요합니다.

C 수준 변경 사항

타입 객체는 __del__ 메서드가 매핑되는(그리고 반대로도 매핑되는) 새로운 tp_finalize 슬롯을 가지게 됩니다. 제너레이터는 tp_del 대신 이 슬롯을 사용하도록 수정됩니다. tp_finalize 함수는 유효하고 살아있는 PyObject를 유일한 인자로 하여 호출되는 일반적인 C 함수입니다. 이는 호출자가 처리할 것이므로, 객체의 참조 카운트를 조작할 필요가 없습니다. 그러나 호출자에게 반환하기 전에 원래의 예외 상태가 복원되도록 보장해야 합니다.

호환성을 위해, tp_del은 타입 구조체에 유지됩니다. non-NULL tp_del을 가진 객체의 처리 방식은 변경되지 않습니다: CI의 일부일 경우, 이들은 종료화되지 않고 gc.garbage에 남게 됩니다. 하지만 CPython 소스 트리에서는 (테스트 목적을 제외하고) non-NULL tp_del이 더 이상 나타나지 않습니다.

특히 사용자 정의 할당 해제자에서 tp_finalize를 더 쉽게 호출할 수 있도록 두 개의 새로운 C API 함수가 제공됩니다.

내부적으로는, GC가 관리하는 객체가 종료화되었음을 나타내기 위해 GC 헤더에 비트 하나가 예약되어 있습니다. 이는 객체를 두 번 종료화하는 것을 방지하는 데 도움이 됩니다(특히, GC에 의해 손상된 이후에 CT 객체를 종료화하는 것을 방지합니다).

Note

GC가 활성화되지 않은 객체도 tp_finalize 슬롯을 가질 수 있습니다. 이러한 객체의 tp_finalize 함수는 할당 해제자에서만 호출될 수 있으므로 추가 비트가 필요하지 않습니다: 따라서 부활한 경우를 제외하고는 두 번 호출될 수 없습니다.

논의

예측 가능성

이 방식을 따르면, 객체의 종료자는 이후에 부활하더라도 항상 정확히 한 번만 호출됩니다.

CI 객체의 경우, 종료자가 호출되는 순서(위의 2단계)는 정의되어 있지 않습니다.

안전성

제안된 변경이 왜 안전한지 설명하는 것이 중요합니다. 논의해야 할 두 가지 측면이 있습니다:

  • 종료자가 (종료 중인 객체를 포함하여) 좀비 객체에 접근할 수 있습니까?
  • 파이널라이저가 객체 그래프를 변경하여 CI에 영향을 미치면 어떻게 됩니까?

첫 번째 문제부터 논의해 보겠습니다. 가능한 경우를 두 가지 범주로 나누겠습니다:

  • 파이널라이즈되는 객체가 CI의 일부인 경우: 구조상 CI 파이널라이저는 참조 절단이 이루어지기 전에 호출되므로 CI 내의 객체 중 아직 좀비인 것은 없습니다. 따라서 파이널라이저는 존재하지 않는 좀비 객체에 접근할 수 없습니다.
  • 파이널라이즈되는 객체가 CI/CT의 일부가 아닌 경우: 정의상 CI/CT 내 객체는 CI/CT 외부로부터 가리키는 참조를 전혀 갖지 않습니다. 따라서 파이널라이저는 어떤 좀비 객체에도 도달할 수 없습니다(즉, 파이널라이즈되는 객체 자체가 좀비 객체로부터 참조되고 있었다 하더라도 마찬가지입니다).

이제 두 번째 문제를 살펴보겠습니다. 잠재적인 경우는 세 가지입니다:

  • 파이널라이저가 CI 객체에 대한 기존 참조를 지웁니다. 이 경우 GC가 끊으려 하기 전에 해당 CI 객체가 이미 정리되었을 수 있으나, 이는 문제없습니다(GC는 단지 이 가능성을 인지하고 있기만 하면 됩니다).
  • 파이널라이저가 CI 객체에 대한 새 참조를 만듭니다. 이는 오직 CI 객체의 파이널라이저에서만 일어날 수 있습니다(그 이유는 위를 참조하십시오). 따라서 이 새 참조는 모든 CI 파이널라이저가 호출된 후(위의 3단계) GC에 의해 감지되며, 어떤 객체도 끊기지 않은 채로 수집이 중단됩니다.
  • 파이널라이저가 비CI 객체에 대한 참조를 지우거나 만듭니다. 구조상 이는 문제가 되지 않습니다.

구현

구현은 http://hg.python.org/features/finalize/ 저장소의 finalize 브랜치에서 확인할 수 있습니다.

검증

일반적인 Python 테스트 스위트를 실행하는 것 외에도, 이 구현은 참조 순환, 객체 부활, 레거시 tp_del 슬롯을 포함한 다양한 종료화 가능성에 대한 테스트 케이스를 추가합니다.

이 구현은 또한 다음 테스트 스위트에서 회귀가 발생하지 않는지 검증되었습니다.

참고 자료

참조 순환 수집과 약한 참조 콜백에 대한 참고 사항: http://hg.python.org/cpython/file/4e687d53b645/Modules/gc_weakref.txt

제너레이터 메모리 누수: http://bugs.python.org/issue17468

객체가 GC에 의해 수집될 수 있는지 스스로 결정하도록 허용: http://bugs.python.org/issue9141

GC 기반 모듈 종료 절차 http://bugs.python.org/issue812369