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

Python 개선 제안 한국어 번역

PEP 203 – 복합 대입

Author:
Thomas Wouters <thomas at python.org>
Status:
Final
Type:
Standards Track
Created:
13-Jul-2000
Python-Version:
2.0
Post-History:
14-Aug-2000

Table of Contents

번역·라이선스 안내

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

소개

이 PEP는 Python 2.0을 위한 복합 대입 제안을 설명합니다. 이 PEP는 Python 2.0에서 도입될 예정인 이 기능의 진행 상태와 담당자를 추적합니다. 이 PEP에는 기능에 대한 설명과 기능을 지원하는 데 필요한 변경 사항의 개요가 담겨 있습니다. 이 PEP는 메일링 리스트 포럼 [1]에서 있었던 논의를 요약하고, 필요한 곳에는 추가 정보를 볼 수 있는 URL을 제공합니다. 확정적인 역사 기록은 이 파일의 CVS 리비전 이력에 담겨 있습니다.

제안하는 의미론

Python에 복합 대입을 추가하는 제안된 패치는 다음과 같은 새 연산자를 도입합니다:

+= -= *= /= %= **= <<= >>= &= ^= |=

이 연산자들은 일반 이항 형태와 같은 연산을 구현합니다. 다만 왼쪽 객체가 지원하면 연산을 제자리에서 수행하고, 왼쪽은 한 번만 평가한다는 점이 다릅니다.

이 연산자들은 본래 수행하려는 이항 연산 외에 일반적인 로드 및 저장 연산도 모두 수행한다는 점에서 진정으로 복합 대입으로 동작합니다. 따라서 다음 표현식이 주어지면:

x += y

객체 x를 로드한 다음 여기에 y를 더하고, 그 결과 객체를 원래 자리에 다시 저장합니다. 두 인자에 대해 수행되는 정확한 동작은 x의 타입에 따라, 그리고 경우에 따라서는 y의 타입에 따라 달라집니다.

Python 복합 대입의 기본 아이디어는 이항 연산의 결과를 왼쪽 피연산자에 저장하는 흔한 관행을 더 쉽게 작성하는 방법일 뿐만 아니라, 해당 왼쪽 피연산자가 수정된 자신의 복사본을 만드는 대신 자기 자신에 대해 연산해야 한다는 것을 알 수 있게 하는 방법이기도 하다는 것입니다.

이를 가능하게 하기 위해 Python 클래스와 C 확장 타입에 여러 개의 새로운 훅이 추가되며, 이 훅은 해당 객체가 복합 대입 연산의 좌변으로 사용될 때 호출됩니다. 클래스나 타입이 제자리 훅을 구현하지 않으면 해당 이항 연산의 일반 훅을 사용합니다.

따라서 인스턴스 객체 x가 주어지면 다음 표현식은:

x += y

__add__의 제자리 변형인 x.__iadd__(y)를 호출하려고 시도합니다. __iadd__가 없으면 x.__add__(y)를 시도하고, __add__도 없으면 마지막으로 y.__radd__(x)를 시도합니다. __iadd__에는 오른쪽 변형이 없습니다. 그런 변형이 있으려면 y가 x를 제자리에서 수정하는 방법을 알아야 하는데, 이는 아무리 좋게 말해도 안전하지 않기 때문입니다. __iadd__ 훅은 __add__와 비슷하게 동작하여, 변수 x에 대입할 연산 결과(self일 수도 있습니다)를 반환해야 합니다.

C 확장 타입에서 훅은 PyNumberMethods와 PySequenceMethods 구조체의 멤버입니다. 이 메서드들을 사용하거나 Python 인스턴스 객체와 C 타입을 섞어 쓸 때 최대한 예상 밖의 일이 일어나지 않도록 몇 가지 특별한 의미론이 적용됩니다.

x <augop> y의 일반적인 경우(또는 PyNumber_InPlace API 함수를 사용하는 비슷한 경우)에 연산 대상이 되는 주된 객체는 x입니다. 이는 x와 y를 협력하는 관계로 볼 수 있는 일반 이항 연산과 다릅니다. 이항 연산과 달리 제자리 연산에서는 피연산자를 서로 바꿀 수 없기 때문입니다. 하지만 제자리 수정이 지원되지 않으면 제자리 연산은 일반 이항 연산으로 대체되며, 그 결과 다음과 같은 규칙이 생깁니다:

  • 왼쪽 객체(x)가 인스턴스 객체이고 __coerce__ 메서드가 있으면, y를 인자로 하여 그 함수를 호출합니다. 강제 변환이 성공하고 그 결과로 얻은 왼쪽 객체가 x와 다른 객체이면, 제자리 연산으로 처리하는 것을 멈추고 강제 변환된 x와 y를 인자로 하여 일반 이항 연산에 해당하는 함수를 호출합니다. 연산 결과는 그 함수가 반환하는 값입니다.

    강제 변환이 x에 대해 다른 객체를 내놓지 않거나 x가 __coerce__ 메서드를 정의하지 않았고, x에 이 연산에 맞는 __ihook__ 메서드가 있으면, y를 인자로 하여 그 메서드를 호출하며 연산 결과는 그 메서드가 반환하는 값입니다.

  • 그렇지 않고 왼쪽 객체가 인스턴스 객체는 아니지만 그 타입이 이 연산의 제자리 함수를 정의한다면, x와 y를 인자로 하여 그 함수를 호출하며 연산 결과는 그 함수가 반환하는 값입니다.

    이 경우 x나 y 어느 쪽에도 강제 변환(coercion)이 수행되지 않으며, C 타입이 두 번째 인자로 인스턴스 객체를 받는 것도 완전히 유효합니다. 이는 일반적인 이항 연산에서는 일어날 수 없는 일입니다.

  • 그렇지 않으면 인자 강제 변환을 포함하여 일반적인 (제자리가 아닌) 이항 연산과 정확히 동일하게 처리하십시오. 요컨대, 두 인자 중 하나라도 인스턴스 객체이면 __coerce__, __hook__, __rhook__을 통해 연산을 해결하십시오. 그렇지 않으면 두 객체 모두 C 타입이며, 이들은 강제 변환된 후 적절한 함수에 전달됩니다.
  • 연산을 처리할 방법을 찾을 수 없으면 해당 연산에 특화된 오류 메시지와 함께 TypeError를 발생시키십시오.
  • 시퀀스에 대해 특별한 의미를 갖는 +와 *의 경우를 고려하기 위한 몇 가지 특수 처리가 존재합니다. 시퀀스 연결인 +의 경우, C 타입이 sq_concat이나 sq_inplace_concat을 정의하면 어떠한 강제 변환도 수행되지 않습니다. 시퀀스 반복인 *의 경우, sq_inplace_repeat나 sq_repeat를 호출하기 전에 y가 C 정수로 변환됩니다. 이는 y가 인스턴스인 경우에도 수행되지만, x가 인스턴스인 경우에는 수행되지 않습니다.

제자리 함수는 항상 새 참조를 반환해야 합니다. 이 참조는 연산이 실제로 제자리에서 수행된 경우에는 기존 x 객체에 대한 것이고, 그렇지 않은 경우에는 새 객체에 대한 것입니다.

근거

Python에 이 기능을 추가하는 주된 이유는 두 가지로, 표현의 단순성과 제자리(in-place) 연산의 지원입니다. 최종 결과는 문법의 단순성과 표현의 단순성 사이의 절충이며, 대부분의 새 기능과 마찬가지로 복합 대입은 이전에 불가능했던 것을 새로 추가하지 않습니다. 단지 이러한 작업을 더 쉽게 할 수 있게 해 줄 뿐입니다.

복합 대입을 추가하면 Python의 문법은 더 복잡해집니다. 단일 대입 연산 대신 이제 열두 가지 대입 연산이 있으며, 그중 열한 가지는 이항 연산도 수행합니다. 그러나 이 열한 가지 새로운 대입 형태는 대입과 이항 연산의 결합으로 쉽게 이해할 수 있으며, 이해하는 데 큰 개념적 도약이 필요하지 않습니다. 게다가 복합 대입을 갖춘 언어들은 이것이 인기 있고 많이 사용되는 기능임을 보여 주었습니다. 다음 형태의 표현식은:

<x> = <x> <operator> <y>

이러한 언어에서 추가 문법을 둘 가치가 있을 만큼 충분히 흔하며, Python에도 그러한 표현식이 그보다 현저히 적지 않습니다. 사실은 오히려 그 반대인데, Python에서는 이항 연산자로 리스트를 연결할 수도 있고, 이는 상당히 자주 이루어지는 일이기 때문입니다. 위 표현식을 다음과 같이 작성하는 것은:

<x> <operator>= <y>

가독성이 더 높고 오류가 발생할 가능성도 더 낮습니다. 변경되는 것이 <x> 자체이며, <x>가 <x>와 거의, 그러나 완전히는 아니게, 전혀 다른 무언가로 대체되는 것이 아니라는 점이 읽는 사람에게 즉시 분명하기 때문입니다.

새로운 제자리 연산은 행렬 계산을 비롯하여 큰 객체가 필요한 여러 응용 프로그램에 특히 유용합니다. 사용 가능한 프로그램 메모리를 효율적으로 다루기 위해, 그러한 패키지들은 현재의 이항 연산을 무작정 사용할 수 없습니다. 이러한 연산은 항상 새 객체를 생성하므로, 기존의 (큰) 객체에 항목 하나를 추가하려면 객체 전체를 복사하고(이로 인해 응용 프로그램의 메모리가 부족해질 수 있습니다), 그 항목 하나를 추가한 다음, 참조 횟수에 따라 원래 객체를 삭제하게 될 수도 있습니다.

이 문제를 우회하기 위해 현재 이러한 패키지는 객체를 제자리에서 수정하는 메서드나 함수를 사용해야 하며, 이는 복합 대입 표현식보다 확실히 가독성이 떨어집니다. 애초에 일부 연산은 제한된 이항 연산자 집합으로 표현할 수 없으므로 복합 대입이 이러한 패키지의 모든 문제를 해결하지는 못하지만, 하나의 시작은 됩니다. PEP 211은 새 연산자 추가를 검토하고 있습니다.

새 메서드

제안된 구현은 Python 클래스가 복합 대입 연산을 오버로드하기 위해 구현할 수 있는 다음 11가지 훅을 추가합니다:

__iadd__
__isub__
__imul__
__idiv__
__imod__
__ipow__
__ilshift__
__irshift__
__iand__
__ixor__
__ior__

__iadd__의 i는 in-place(제자리)를 의미합니다.

C 확장 타입의 경우, 다음 구조체 멤버가 추가됩니다.

PyNumberMethods에:

binaryfunc nb_inplace_add;
binaryfunc nb_inplace_subtract;
binaryfunc nb_inplace_multiply;
binaryfunc nb_inplace_divide;
binaryfunc nb_inplace_remainder;
binaryfunc nb_inplace_power;
binaryfunc nb_inplace_lshift;
binaryfunc nb_inplace_rshift;
binaryfunc nb_inplace_and;
binaryfunc nb_inplace_xor;
binaryfunc nb_inplace_or;

PySequenceMethods에 추가되는 항목:

binaryfunc sq_inplace_concat;
intargfunc sq_inplace_repeat;

바이너리 호환성을 유지하기 위해, tp_flags TypeObject 멤버를 사용하여 해당 TypeObject가 이 슬롯들을 위한 공간을 할당했는지 판별합니다. 바이너리 호환성을 완전히 깨는 변경이 이루어지기 전까지는(2.0 이전에 이루어질 수도, 그렇지 않을 수도 있습니다) 새 구조체 멤버 중 하나를 사용하려는 코드는 먼저 PyType_HasFeature() 매크로로 해당 멤버를 사용할 수 있는지 확인해야 합니다.:

if (PyType_HasFeature(x->ob_type, Py_TPFLAGS_HAVE_INPLACE_OPS) &&
    x->ob_type->tp_as_number && x->ob_type->tp_as_number->nb_inplace_add) {
        /* ... */

이 확인은 메서드 슬롯이 NULL 값인지 검사하기보다도 먼저 수행해야 합니다! 이 매크로는 슬롯을 사용할 수 있는지만 검사할 뿐, 슬롯이 메서드로 채워져 있는지는 검사하지 않습니다.

구현

복합 대입의 현재 구현 [2]은 이미 다룬 메서드와 슬롯 외에 13개의 새 바이트코드와 13개의 새 API 함수를 추가합니다.

이 API 함수들은 단순히 현재의 이항 연산 API 함수들의 제자리(in-place) 버전입니다.:

PyNumber_InPlaceAdd(PyObject *o1, PyObject *o2);
PyNumber_InPlaceSubtract(PyObject *o1, PyObject *o2);
PyNumber_InPlaceMultiply(PyObject *o1, PyObject *o2);
PyNumber_InPlaceDivide(PyObject *o1, PyObject *o2);
PyNumber_InPlaceRemainder(PyObject *o1, PyObject *o2);
PyNumber_InPlacePower(PyObject *o1, PyObject *o2);
PyNumber_InPlaceLshift(PyObject *o1, PyObject *o2);
PyNumber_InPlaceRshift(PyObject *o1, PyObject *o2);
PyNumber_InPlaceAnd(PyObject *o1, PyObject *o2);
PyNumber_InPlaceXor(PyObject *o1, PyObject *o2);
PyNumber_InPlaceOr(PyObject *o1, PyObject *o2);
PySequence_InPlaceConcat(PyObject *o1, PyObject *o2);
PySequence_InPlaceRepeat(PyObject *o, int count);

이 함수들은 Python 클래스 훅(두 객체 중 하나라도 Python 클래스 인스턴스인 경우)을 호출하거나, C 타입의 숫자 메서드 또는 시퀀스 메서드를 호출합니다.

새 바이트코드는 다음과 같습니다.:

INPLACE_ADD
INPLACE_SUBTRACT
INPLACE_MULTIPLY
INPLACE_DIVIDE
INPLACE_REMAINDER
INPLACE_POWER
INPLACE_LEFTSHIFT
INPLACE_RIGHTSHIFT
INPLACE_AND
INPLACE_XOR
INPLACE_OR
ROT_FOUR
DUP_TOPX

INPLACE_* 바이트코드는 BINARY_* 바이트코드와 대응되지만, InPlace API 함수를 호출하는 방식으로 구현된다는 점이 다릅니다. 나머지 두 바이트코드는 유틸리티 바이트코드입니다. ROT_FOUR는 ROT_THREE와 같이 동작하지만, 스택 맨 위의 네 항목을 회전시킨다는 점이 다릅니다.

DUP_TOPX는 인자 하나를 받는 바이트코드로, 이 인자는 한 블록으로 복제할 항목의 개수이며 1 이상 5 이하의 정수여야 합니다. 다음과 같은 스택이 있다고 합시다(리스트의 오른쪽이 스택의 맨 위입니다).:

[1, 2, 3, 4, 5]

DUP_TOPX 3은 맨 위의 세 항목을 복제하며, 그 결과 스택은 다음과 같이 됩니다.:

[1, 2, 3, 4, 5, 3, 4, 5]

인자가 1인 DUP_TOPX는 DUP_TOP과 같습니다. 5라는 제한은 순전히 구현상의 제한입니다. 복합 대입의 구현에는 인자가 2와 3인 DUP_TOPX만 필요하며, 상당수의 DUP_TOP과 ROT_*를 대가로 치르면 이 새 옵코드 없이도 구현할 수 있습니다.

미해결 문제

PyNumber_InPlace API는 일반 PyNumber API의 부분 집합일 뿐이며, 복합 대입 문법을 지원하는 데 필요한 함수만 포함됩니다. 다른 제자리 API 함수가 필요하다면 나중에 추가할 수 있습니다.

DUP_TOPX 바이트코드는 편의를 위한 바이트코드이며, 실제로 꼭 필요한 것은 아닙니다. 이 바이트코드를 둘 가치가 있는지 검토해야 합니다. 현재로서는 이 바이트코드를 달리 사용할 곳이 없어 보입니다.

References