RPython 언어

정의

RPython은 정적 분석이 가능한 Python의 제한된 부분 집합입니다. 언어에 추가된 기능도 있고 의외로 작동하는 것들도 있지만, 다음은 고려해야 할 제한 사항을 대략적으로 정리한 목록입니다. 작업을 진행하다 보면 특수한 경우에만 적용되는 제한 사항을 수없이 마주치게 된다는 점에 유의하십시오. 정확한 정의는 “RPython은 우리 변환 툴체인이 받아들일 수 있는 모든 것”입니다 :)

흐름 제약 사항

변수

각 제어 흐름 지점에서 변수는 Object restrictions에 설명된 대로 최대 한 가지 타입의 값만 담아야 합니다. 예를 들어, 같은 변수가 문자열과 int를 모두 담도록 제어 경로를 합치는 일은 피해야 합니다. None(기본적으로 널 포인터 역할을 합니다)은 래핑된 객체, 클래스 인스턴스, 리스트, 딕셔너리, 문자열 등 다른 여러 타입과 섞어 쓸 수 있지만, int, float, 튜플과는 섞어 쓸 수 없습니다.

상수

모든 모듈 전역 변수는 상수로 간주됩니다. 이 바인딩은 런타임에 변경해서는 안 됩니다. 또한 전역(즉, 미리 만들어진) 리스트와 딕셔너리는 불변이어야 합니다. 예를 들어 전역 리스트를 수정하면 일관되지 않은 결과가 나옵니다. 하지만 전역 인스턴스에는 이러한 제약이 없으므로, 변경 가능한 전역 상태가 필요하다면 미리 만들어진 싱글턴 인스턴스의 속성에 저장하십시오.

제어 구조

모두 허용되며, for 루프는 내장 타입으로 제한되고, 제너레이터는 매우 제한적입니다.

range

range와 xrange는 동일합니다. range가 항상 배열을 생성하는 것은 아니며, 결과가 수정되는 경우에만 배열을 생성합니다. 어디에서나 사용할 수 있으며 완전히 구현되어 있습니다. CPython과 비교했을 때 눈에 보이는 유일한 차이점은 xrange의 start, stop, step 필드에 접근할 수 없다는 점입니다.

정의

클래스나 함수를 런타임에 정의하는 것은 허용되지 않습니다.

제너레이터

제너레이터는 지원되지만, 지원되는 정확한 범위는 매우 제한적입니다. 하나의 제어 지점에서 서로 다른 두 제너레이터를 병합할 수는 없습니다.

예외

완전히 지원됩니다. 내장 연산이 발생시키는 예외에 대한 제약 사항은 아래의 Exception rules를 참조하십시오.

객체 제약 사항

우리는 사용하고 있습니다

정수, 부동소수점 수, 불리언

동작합니다.

문자열

많은 문자열 메서드가 지원되지만 전부 지원되지는 않으며, 지원되는 메서드라도 모든 인자를 받는 것은 아닙니다. 인덱스는 음수일 수 있습니다. 인덱스가 음수가 아닌 경우, 변환기가 인덱스가 음수가 아님을 증명할 수 있으면 약간 더 효율적인 코드를 얻을 수 있습니다. 문자열을 슬라이싱할 때는 슬라이스의 시작 인덱스와 끝 인덱스가 음수가 아님을 증명해야 합니다. 어디에서도 str에서 유니코드로의 암시적 캐스팅은 일어나지 않습니다. 포맷 문자열을 변환 시점에 알 수 있다면 % 연산자를 사용한 간단한 문자열 포매팅이 동작합니다. 지원되는 포맷 지정자는 %s, %d, %x, %o, %f뿐이며, %r도 지원되지만 사용자 정의 인스턴스에만 사용할 수 있습니다. 변환 플래그, 정밀도, 길이 등의 수식자는 지원되지 않습니다. 또한 포매팅할 때 유니코드와 문자열을 섞어 쓰는 것은 금지되어 있습니다.

튜플

가변 길이 튜플은 없습니다(튜플의 길이는 변수가 될 수 없습니다). 튜플은 값의 쌍이나 n-튜플을 저장하거나 반환하는 데 사용하십시오. 요소 타입과 길이의 조합마다 서로 섞어 쓸 수 없는 별개의 타입이 됩니다.

결과의 길이를 정적으로 알 수 없기 때문에 리스트를 튜플로 변환하는 일반적인 방법은 없습니다. (물론 lst에 항목이 3개 있다는 것을 알고 있다면 t = (lst[0], lst[1], lst[2])와 같이 작성할 수 있습니다.)

리스트

리스트는 할당된 배열로 사용됩니다. 리스트는 초과 할당(over-allocate)되므로 list.append()는 상당히 빠릅니다. 그러나 크기가 고정된 리스트를 사용하면 코드가 더 효율적입니다. 어노테이터는 리스트 컴프리헨션을 사용하는 경우에도 대부분 리스트가 고정 크기인지 알아낼 수 있습니다. 음수 인덱스나 범위를 벗어난 인덱스는 다음과 같이 가장 일반적인 연산에서만 허용됩니다:

  • 인덱싱: 양수 인덱스와 음수 인덱스를 모두 사용할 수 있습니다. 인덱스 검사는 IndexError 예외 절로 요청한 경우에만 수행됩니다.
  • 슬라이싱: 슬라이스의 시작 위치는 범위 안에 있어야 합니다. 끝은 범위 안에 있을 필요가 없지만, 시작보다 작아서는 안 됩니다. [:-1]이라는 특수한 경우를 제외하면 음수 인덱스는 모두 허용되지 않습니다. 스텝은 지정할 수 없습니다. 슬라이스 삭제에도 같은 규칙이 적용됩니다.
  • 슬라이스 할당: len(sublist) == y - x인 경우에만 lst[x:y] = sublist를 지원합니다. 즉, 슬라이스 할당으로는 리스트의 전체 길이를 바꿀 수 없으며 항목을 교체하는 것만 가능합니다.
  • 기타 연산자: +, +=, in, *, *=, ==, !=는 예상대로 동작합니다.
  • 메서드: append, index, insert, extend, reverse, pop. pop()에 사용되는 인덱스는 위의 인덱싱과 같은 규칙을 따릅니다. insert()에 사용되는 인덱스는 범위 안에 있어야 하며 음수여서는 안 됩니다.

딕셔너리(dict)

해시 가능한 경우에 한해 단일 키 타입만 사용하는 딕셔너리. 사용자 정의 해시 함수와 사용자 정의 동등성 비교는 적용되지 않습니다. 사용자 정의 해시 함수가 필요하면 rpython.rlib.objectmodel.r_dict를 사용하십시오.

집합

RPython에서는 집합(set)이 직접 지원되지 않습니다. 대신 일반 dict를 사용하고 값을 None으로 채워야 합니다. 그런 dict의 값은 공간을 차지하지 않습니다.

리스트 컴프리헨션

할당 및 초기화가 완료된 배열을 생성하는 데 사용할 수 있습니다.

함수

  • 함수 선언에는 기본값과 *args를 사용할 수 있지만, **keywords는 사용할 수 없습니다.
  • 함수 호출은 알려진 함수나 변수에 담긴 함수, 또는 메서드를 대상으로 할 수 있습니다. 위치 인자와 키워드 인자로 호출할 수 있으며, *args 인자도 전달할 수 있습니다(이 인자는 튜플이어야 합니다).
  • 위에서 설명한 것처럼, 튜플은 변수 길이를 가지지 않습니다. 인자 개수가 동적으로 달라지는 방식으로 함수를 호출해야 한다면, 일반 리스트 하나를 인자로 받도록 함수 자체를 리팩터링하십시오.
  • 동적 디스패치에서는 호출될 수 있는 모든 함수의 시그니처가 같거나, 적어도 “충분히 호환”되어야 합니다. 이는 주로 메서드 호출에 해당하며, 메서드가 오버라이드되었거나 여러 클래스에서 어떤 식으로든 서로 다르게 정의된 경우가 이에 속합니다. 흔하지는 않지만 함수 객체를 명시적으로 조작하는 경우에도 해당합니다. 정확한 호환성 규칙은 설명하기가 꽤 복잡합니다(다만 이 규칙을 어기면 알 수 없는 크래시가 아니라 rtyper가 내는 명시적인 오류를 보게 될 것입니다.)

내장 함수

여러 내장 함수를 사용할 수 있습니다. 정확한 목록은 rpython/annotator/builtin.py에서 확인할 수 있습니다(def builtin_xxx()를 참고하십시오). 다만 일부 내장 함수는 지원하는 범위가 제한적일 수 있습니다.

int, float, str, ord, chr… 등은 간단한 변환 함수로 사용할 수 있습니다. int, float, str… 등은 isinstance 안에서만 타입으로서 특별한 의미를 갖는다는 점에 유의하십시오.

클래스

  • 메서드와 기타 클래스 속성은 시작 후에 변경되지 않습니다.
  • 단일 상속은 완전히 지원됩니다
  • 클래스 M의 전체 내용을 복사하려면 클래스 본문에서 rpython.rlib.objectmodel.import_from_mixin(M)을 사용하십시오. 이 기능으로 믹스인(mixin)을 구현할 수 있습니다. 함수와 정적 메서드는 복제됩니다(그 외의 클래스 속성은 수정 없이 그대로 복사됩니다).
  • 클래스 역시 일급 객체입니다

객체

일반 규칙이 적용됩니다. 지원되는 특수 메서드는 __init__, __del__, __len__, __getitem__, __setitem__, __getslice__, __setslice__, __iter__뿐입니다. 슬라이싱을 처리하려면 __getslice__와 __setslice__를 사용해야 합니다. 슬라이싱에 __getitem__과 __setitem__을 사용하는 것은 지원되지 않습니다. 또한 __getslice__를 사용하더라도 슬라이싱에 음수 인덱스를 사용하는 것은 여전히 지원되지 않습니다.

소멸자 __del__에는 단순한 연산만 포함되어야 한다는 점에 유의하십시오. 더 복잡한 소멸자가 필요하다면 대신 rpython.rlib.rgc.FinalizerQueue를 사용하는 것을 고려하십시오.

이러한 레이아웃 덕분에 신경 써야 할 타입의 수가 상당히 제한됩니다.

정수 타입

정수 타입을 구현하는 과정에서, 현재 CPython에서 정수가 상당히 유동적인 상태라는 문제에 부딪혔습니다. Python 2.4부터는 오버플로가 발생하면 정수가 long으로 바뀝니다. 이와 달리, 저희에게는 기본적으로 머신 크기의 랩어라운드(wrap-around) 산술을 수행하면서도, 명시적으로 필요할 때는 오버플로를 검사할 수 있는 방법이 필요합니다. 게다가 변환 전과 후에 일관된 동작이 필요합니다.

부호 있는 산술 연산에는 일반 정수를 사용합니다. 즉, 변환 전에는 오버플로가 발생하면 long이 되고, 변환 후에는 아무런 경고 없이 값이 순환(wrap-around)됩니다. 더 세밀한 제어가 필요할 때는 다음 헬퍼들을 사용합니다(rpython/rlib/rarithmetic.py에 있습니다):

ovfcheck()

이 특수 함수는 z = ovfcheck(x+y)와 같이 단일 산술 연산 하나만을 인자로 넘겨 사용해야 합니다. 이 함수는 주어진 연산을 오버플로 검사 모드로 수행하도록 의도되었습니다.

런타임에 Python에서는 ovfcheck() 함수 자체가 결과를 검사하고, 결과가 long이면 OverflowError를 발생시킵니다. 그러나 코드 생성기는 ovfcheck()를 힌트로 사용합니다. 즉, ovfcheck(x+y) 표현식 전체를 C의 오버플로 검사 덧셈 하나로 대체합니다.

intmask()

이 함수는 랩어라운드(wrap-around) 산술 연산에 사용됩니다. 이 함수는 인자의 하위 비트를 반환하며, C의 “signed long int”에 들어가지 않는 부분은 모두 마스킹하여 제거합니다. 이 함수의 목적은 Python에서 이전 연산의 결과로 생긴 Python long을 다시 Python int로 변환하는 것입니다. 코드 생성기는 어차피 기본적으로 항상 랩어라운드 방식의 부호 있는 산술 연산을 수행하므로 intmask()를 완전히 무시합니다. (현재 우리에게는 C의 “int”와 “long int” 구분에 해당하는 것이 없으며, 모든 곳에서 “long int”를 가정합니다.)

r_uint

해시 테이블 조작과 같은 몇몇 경우에는 머신 크기의 부호 없는 산술 연산이 필요합니다. 이러한 경우를 위해 r_uint 클래스가 있습니다. 이 클래스는 워드 크기의 부호 없는 정수에 대한 순수 Python 구현이며, 오버플로가 발생하면 별도의 알림 없이 값이 순환합니다. (“워드 크기”와 “머신 크기”는 같은 의미로 사용되며, C에서 “unsigned long”을 사용할 때 얻는 네이티브 크기를 뜻합니다.) 이 클래스의 목적은 (위에서 설명한 헬퍼 함수와 달리) 일관된 타입 지정에 있습니다. Python과 어노테이터 모두 프로그램 안에서 r_uint 인스턴스를 전파하며, 이들 사이의 모든 연산을 부호 없는 연산으로 해석합니다. 코드 생성기는 r_uint 인스턴스를 특수하게 처리하여 적절한 저수준 타입과 연산을 사용합니다. 연산에서 (부호 있는) 정수와 r_uint를 섞어 사용하면 r_uint가 생성되며, 이는 결과가 부호 없는 값이라는 뜻입니다. r_uint에서 다시 부호 있는 정수로 변환하려면 intmask()를 사용하십시오.

타입 강제 및 검사

RPython은 함수 인자에 RPython 수준의 타입을 강제하는 헬퍼 데코레이터를 제공합니다. enforceargs()라는 이 데코레이터는 함수의 인자와 일치해야 하는 타입들을 매개변수로 받습니다.

enforceargs()로 데코레이트된 함수는 임포트 시점에 함수 시그니처가 분석되고 RPython 수준의 타입이 추론됩니다(RPython에서 수행되는 변환의 특성에 대한 자세한 내용은 Annotation pass documentation을 참조하십시오). RPython이 지원하지 않는 타입을 만나면 TypeError가 발생합니다.

enforceargs()는 기본적으로 함수가 호출될 때마다 매개변수 타입에 대한 타입 검사도 수행합니다. 이 동작을 비활성화하려면 데코레이터에 typecheck=False 매개변수를 전달하면 됩니다.

예외 규칙

간단한 경우에는 기본적으로 예외가 생성되지 않습니다.:

#!/usr/bin/python

    lst = [1,2,3,4,5]
    item = lst[i]    # this code is not checked for out-of-bound access

    try:
        item = lst[i]
    except IndexError:
        # complain

예외 핸들러가 없는 코드는 예외를 발생시키지 않습니다(정확히 말하면 변환된 후에 그렇습니다. 물론 CPython 위에서 실행하면 예외가 발생할 수 있습니다). 예외 핸들러를 제공하면 오류 검사를 요청하는 것입니다. 예외 처리기를 제공하지 않으면 해당 연산이 실패할 수 없다고 시스템에 보증하는 것입니다. 이 규칙은 함수 호출에는 적용되지 않습니다. 호출되는 모든 함수는 어떤 예외든 발생시킬 수 있다고 가정합니다.

예를 들어:

x = 5.1
x = x + 1.2       # not checked for float overflow
try:
    x = x + 1.2
except OverflowError:
    # float result too big

하지만:

z = some_function(x, y)    # can raise any exception
try:
    z = some_other_function(x, y)
except IndexError:
    # only catches explicitly-raised IndexErrors in some_other_function()
    # other exceptions can be raised, too, and will not be caught here.

위에서 설명한 ovfcheck() 함수도 같은 규칙을 따릅니다. 오버플로가 발생하면 이 함수는 OverflowError를 명시적으로 발생시키며, 이 예외는 어디에서든 잡을 수 있습니다.

명시적으로 발생시키거나 다시 발생시킨 예외는 항상 생성됩니다.

PyPy는 CPython 위에서 디버깅할 수 있습니다

PyPy는 표준 CPython에서 실행할 수 있다는 장점이 있습니다. 즉, 모든 예외 처리를 활성화한 상태로 PyPy 전체를 실행할 수 있으므로, 암묵적으로 전제한 단언(assertion)을 지키지 못한 경우를 잡아낼 수 있습니다.