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는 약하지 않은 참조에 초점을 맞춥니다.
참조 순환
객체들 사이의 방향성 있는 연결로 이루어진 순환 부분 그래프로, 순수한 참조 카운팅 방식에서는 해당 객체들이 수거되지 못하게 합니다.
순환 고립체(CI)
외부에서 참조되는 객체가 하나도 없고, 하나 이상의 참조 순환을 포함하며, 또한 그 객체들이 여전히 사용 가능하고 손상되지 않은 상태에 있는 독립된 객체 부분 그래프입니다. 즉, 객체들은 각자의 파이널라이저에서 서로에게 접근할 수 있습니다.
순환 가비지 컬렉터(GC)
순환 고립체를 감지하여 순환 쓰레기로 바꿀 수 있는 장치입니다. 순환 쓰레기에 속한 객체는 참조가 해제되고 참조 횟수가 0으로 떨어지는 자연스러운 효과에 의해 결국 처리됩니다.
순환 쓰레기(CT)
GC에 의해 객체가 지워지기 시작한, 이전의 순환 고립체입니다. 순환 쓰레기에 속한 객체는 잠재적인 좀비입니다. Python 코드가 이 객체에 접근하면 그 증상은 이상한 AttributeError부터 크래시까지 다양할 수 있습니다.
좀비 / 손상된 객체
순환 쓰레기에 속한 객체입니다. 이 용어는 해당 객체가 안전하지 않다는 점을 강조합니다. 즉, 그 객체에서 나가는 참조가 지워졌을 수 있거나, 그 객체가 참조하는 객체 중 하나가 좀비일 수 있습니다. 따라서 임의의 코드(파이널라이저 등)가 이 객체에 접근해서는 안 됩니다.
파이널라이저
객체가 처분될 예정일 때 호출되는 함수 또는 메서드입니다. 파이널라이저는 객체에 접근하여 그 객체가 보유한 모든 자원(예를 들어 뮤텍스나 파일 디스크립터)을 해제할 수 있습니다. 한 가지 예는 __del__ 메서드입니다.
부활
파이널라이저가 CI 안의 객체에 대한 새 참조를 만드는 과정입니다. 이는 __del__ 메서드의 특이하지만 지원되는 부작용으로 일어날 수 있습니다.

영향

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

이점

이 PEP의 주된 이점은 __del__ 메서드를 가진 객체나 finally 블록을 가진 제너레이터처럼 파이널라이저를 가진 객체에 관한 것입니다. 이제 이러한 객체는 참조 순환의 일부일 때도 회수될 수 있습니다.

이 PEP는 또한 추가적인 이점을 위한 길을 닦습니다:

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

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

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

설명

참조 카운트 기반 폐기

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

그러나 객체가 이미 파이널라이즈되었다면 파이널라이저는 호출되지 않습니다. 이렇게 하여 좀비를 파이널라이즈하는 일을 방지합니다(아래 참조).

순환 고립체의 폐기

순환 고립체는 먼저 가비지 컬렉터에 의해 감지된 다음 폐기됩니다. 감지 단계는 변경되지 않으므로 여기서는 설명하지 않습니다. CI의 폐기는 전통적으로 다음 순서로 이루어집니다:

  1. CI 객체에 대한 약한 참조가 제거되고 그 콜백이 호출됩니다. 이 시점에서는 객체를 여전히 안전하게 사용할 수 있습니다.
  2. GC가 (tp_clear 함수를 사용하여) CI 내부의 알려진 모든 참조를 체계적으로 끊으면서 CI는 CT가 됩니다.
  3. 아무것도 하지 않습니다. 모든 CT 객체는 (참조 제거의 부작용으로) 2단계에서 이미 폐기되었어야 하며, 이것으로 이번 수집이 끝납니다.

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

  1. CI 객체에 대한 약한 참조가 제거되고 그 콜백이 호출됩니다. 이 시점에서는 객체를 여전히 안전하게 사용할 수 있습니다.
  2. 모든 CI 객체의 파이널라이저가 호출됩니다.
  3. CI가 여전히 고립되어 있는지 판단하기 위해 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은 타입 구조체에 유지됩니다. NULL이 아닌 tp_del을 가진 객체의 처리 방식은 변경되지 않습니다. 이러한 객체는 CI의 일부일 때 파이널라이즈되지 않고 gc.garbage에 남게 됩니다. 그러나 CPython 소스 트리에서는 (테스트 목적을 제외하면) 더 이상 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