PyPy와 CPython의 차이점

이 페이지는 PyPy Python 인터프리터와 CPython 사이의 몇 가지 차이점과 비호환성을 문서화합니다. 이러한 차이점 중 일부는 “의도된 것”인데, CPython의 동작이 버그인 경우가 있다고 생각하며 그 버그를 그대로 따라 하고 싶지 않기 때문입니다.

여기에 나열되지 않은 차이점은 PyPy의 버그로 간주되어야 합니다.

가비지 컬렉션 전략과 관련된 차이점

PyPy가 사용하거나 구현하는 가비지 컬렉터는 참조 계수(reference counting) 방식에 기반하지 않으므로, 객체는 더 이상 도달할 수 없게 되어도 즉시 해제되지 않습니다. 이로 인한 가장 명백한 영향은 파일(및 소켓 등)이 스코프를 벗어나도 즉시 닫히지 않는다는 것입니다. 쓰기용으로 열린 파일의 경우, 데이터가 한동안 출력 버퍼에 남아있을 수 있어 디스크 상의 파일이 비어 있거나 잘린 것처럼 보일 수 있습니다. 게다가, 동시에 열려 있는 파일 수에 대한 운영체제(OS)의 제한에 도달할 수도 있습니다.

PyPy2에서 프로그램의 파일이 제대로 닫히지 않는 경우를 디버깅한다면, -X track-resources 명령줄 옵션을 사용할 수 있습니다. Python3(CPython과 PyPy 모두)에서는 -Walways 명령줄 옵션을 사용하십시오. 두 경우 모두, 프로그램 끝에 gc.collect() 호출을 추가해야 할 수 있습니다. 그러면 가비지 컬렉터가 닫는 모든 파일과 소켓에 대해 ResourceWarning이 발생합니다. PyPy에서는 프로그램의 어느 부분이 파일을 명시적으로 닫지 않는지 쉽게 알 수 있도록, 경고에 파일이나 소켓이 생성된 위치의 스택 트레이스가 항상 포함됩니다.

CPython과의 이 차이를 고치는 것은 참조 카운팅 방식의 가비지 컬렉션을 강제하지 않고서는 본질적으로 불가능합니다. CPython에서 얻는 효과는 언어 설계 결정이 아니라 구현의 부작용으로 명확히 설명되어 왔습니다: 이에 의존하는 프로그램은 기본적으로 잘못된 것입니다. Jython이나 IronPython(또는 Java나 .NET으로의 다른 Python 포트)에 채택될 가능성이 전혀 없다는 점을 고려하면, 언어 명세에서 CPython의 동작을 강제하려는 것은 지나치게 강한 제약일 것입니다.

OS의 한계에 위험할 정도로 가까워졌을 때 전체 GC를 강제로 실행한다는 소박한 발상조차도 경우에 따라 매우 나쁜 결과를 낳을 수 있습니다. 프로그램이 열린 파일을 심하게 누수시킨다면 이 방법은 작동은 하겠지만, n번째로 누수되는 파일마다 전체 GC 사이클을 강제로 실행하게 됩니다. n의 값은 상수이지만 프로그램은 임의의 양의 메모리를 사용할 수 있으므로, 전체 GC 사이클이 임의로 길어질 수 있습니다. 최종 결과는 PyPy가 자신의 실행 시간 중 임의로 큰 비율을 GC에 소비하게 된다는 것입니다 — 즉, 실제 실행 속도가 10%도 100%도 1000%도 아니라, 사실상 임의의 배수로 느려집니다.

저희가 아는 한, 이 문제에는 프로그램을 수정하는 것보다 더 나은 해결책이 없습니다. 이것이 서드파티 코드에서 발생한다면, 이는 작성자를 찾아가 문제를 설명해야 한다는 뜻입니다: CPython 기반이 아닌 Python 구현에서 실행하려면 열려 있는 파일을 닫아야 합니다.


몇 가지 더 기술적인 세부 사항을 살펴보겠습니다. 이 문제는 __del__ 메서드가 호출되는 정확한 시점에 영향을 미치는데, PyPy(그리고 Jython이나 IronPython도 마찬가지로)에서는 이 시점이 신뢰할 수 없고 적시에 이루어지지도 않습니다. 이는 또한 weak references가 예상보다 조금 더 오래 살아있을 수 있음을 의미합니다. 이 때문에 (weakref.proxy()가 반환하는) “weak proxies”는 다소 쓸모가 줄어듭니다: PyPy에서는 이들이 조금 더 오래 살아있는 것처럼 보이다가, 갑자기 실제로 죽어버려서 다음 접근 시 ReferenceError를 발생시킵니다. weak proxies를 사용하는 모든 코드는 이를 사용하는 모든 곳에서 이러한 ReferenceError를 주의 깊게 처리해야 합니다. (또는 더 나은 방법으로, weakref.proxy()를 아예 사용하지 말고 weakref.ref()를 사용하십시오.)

documentation for weakref callbacks의 세부 사항에 주목하십시오:

callback이 제공되고 None이 아니면, 그리고 반환된 weakref 객체가 여전히 살아 있다면, 객체가 소멸되려고 할 때 callback이 호출됩니다.

CPython의 참조 카운트(refcount) 의미론으로 인해, weakref가 그것이 가리키는 객체보다 바로 직전 또는 직후에 소멸하는 경우가 있습니다(일반적으로 순환 참조가 있는 경우입니다). 바로 직후에 소멸하는 경우라면, 콜백이 호출됩니다. PyPy에서의 유사한 경우, 객체와 weakref 둘 다 동시에 죽은 것으로 간주되어 콜백은 호출되지 않습니다. (이슈 #2030)

새로운 차이점: CPython 3.4 이전에는, x에 대한 약한 참조(weakref)가 x.__del__() 메서드가 호출되기 전에 항상 해제되었습니다. CPython 3.4부터는 상황이 더 모호해졌습니다. 흔히 x.__del__()이 실행되는 동안 약한 참조가 여전히 살아있지만, 항상 그런 것은 아닙니다(예: 참조 순환의 경우는 그렇지 않습니다). PyPy3에서는 3.4 이전의 더 일관된 동작을 유지했습니다. 순환 참조가 있는지 여부에 따라 실제로 다르게 동작하도록 만들 수는 없습니다.


GC의 차이로 인한 몇 가지 추가적인 영향이 있습니다. 가장 눈에 띄는 점은, 객체에 __del__이 있는 경우 PyPy에서는 __del__이 두 번 이상 호출되지 않는다는 것입니다. 하지만 CPython은 객체가 부활했다가 다시 죽으면 동일한 __del__을 여러 번 호출합니다(적어도 오래된 CPython에서는 확실히 그렇습니다; 최신 CPython은 소멸자를 한 번만 호출하려고 시도하지만, 반례도 있습니다). __del__메서드는, CPython에서와 마찬가지로, 서로를 가리키는 객체들에 있는 경우 “올바른” 순서로 호출됩니다. 하지만 CPython과 달리, 서로를 참조하는 객체들의 죽은 순환(cycle)이 있는 경우에도 __del__메서드는 어쨌든 호출됩니다; CPython은 대신 이들을 gc 모듈의 garbage 리스트에 넣습니다. 더 자세한 정보는 블로그 [1] [2]에서 확인할 수 있습니다.

이 차이는 경우에 따라 간접적으로 나타날 수 있다는 점에 유의합니다. 예를 들어, 중간에 멈춰 있는 제너레이터는 — 다시 말하지만 — CPython보다 PyPy에서 더 나중에 가비지 컬렉션됩니다. 정지된 지점의 yield 키워드가 try: 또는 with: 블록으로 둘러싸여 있는 경우 이 차이를 확인할 수 있습니다. 이는 예를 들어 issue 736에서 나타납니다.

기본 GC(minimark라고 함)를 사용하면, 내장 함수 id()는 CPython에서와 같이 동작합니다. 다른 GC를 사용하면 실제 주소가 아닌 숫자를 반환하며(객체가 여러 번 이동할 수 있기 때문입니다), 이를 많이 호출하면 성능 문제로 이어질 수 있습니다.

객체들의 긴 연쇄가 있고, 각 객체가 다음 객체에 대한 참조를 가지며, 각각 __del__을 가지고 있다면, PyPy의 가비지 컬렉터 성능이 나빠진다는 점에 유의해야 합니다. 밝은 면으로는, 대부분의 다른 경우에는 벤치마크에 따르면 PyPy의 가비지 컬렉터가 CPython의 것보다 훨씬 더 나은 성능을 보입니다.

또 다른 차이점은 기존 클래스에 __del__을 추가해도 호출되지 않는다는 것입니다.:

>>>> class A(object):
....     pass
....
>>>> A.__del__ = lambda self: None
__main__:1: RuntimeWarning: a __del__ method added to an existing type will not be called

더 모호한 경우도 있습니다: 구식(old-style) 클래스에서 인스턴스에 __del__을 붙이는 경우에도 마찬가지입니다(CPython에서도 신식(new-style) 클래스에서는 이렇게 동작하지 않습니다). PyPy에서는 RuntimeWarning이 발생합니다. 이런 경우를 해결하려면 처음부터 클래스에 __del__ 메서드가 있도록 하면 됩니다(pass만 포함하고 있어도 되며, 나중에 재정의하거나 오버라이드해도 잘 동작합니다).

CPython은 프로그램이 종료될 때 자동으로 gc.collect()를 수행하려 시도하지만, PyPy는 그렇지 않습니다. (CPython과 PyPy 모두에서, 모든 객체가 소멸하기 전에 여러 번의 gc.collect()가 필요한 경우를 설계하는 것이 가능합니다. 이는 결국 CPython의 방식이 어차피 “대부분의 경우”에만 작동하게 만듭니다.)

누락된 sys.getrefcount

위의 다른 전략 때문에, sys.getrefcount()은 신뢰할 수 없는 숫자를 반환합니다. 따라서 PyPy는 이를 구현하지 않으며, 사용하려고 하면 AttributeError: module 'sys' has no attribute 'getrefcount'이 발생합니다. 최신 버전의 CPython도 sys.getrefcount()의 의미를 변경했다는 점에 유의하십시오.

내장 타입의 서브클래스

공식적으로, CPython에는 내장 타입의 서브클래스에서 오버라이드된 메서드가 정확히 언제 암묵적으로 호출되는지에 대한 규칙이 전혀 없습니다. 근사적으로, 이러한 메서드는 동일한 객체의 다른 내장 메서드에 의해 호출되지 않습니다. 예를 들어, dict의 서브클래스에서 오버라이드된 __getitem__()은 내장 get()메서드에 의해 호출되지 않습니다.

위 내용은 CPython과 PyPy 모두에서 사실입니다. 내장 함수나 메서드가 self가 아닌 다른 객체의 오버라이드된 메서드를 호출하는지 여부에서 차이가 발생할 수 있습니다. PyPy에서는 CPython이라면 호출하지 않을 경우에도 종종 호출됩니다. 두 가지 예:

class D(dict):
    def __getitem__(self, key):
        if key == 'print':
            return print
        return "%r from D" % (key,)

class A(object):
    pass

a = A()
a.__dict__ = D()
a.foo = "a's own foo"
print(a.foo)
# CPython => a's own foo
# PyPy => 'foo' from D

print('==========')

glob = D(foo="base item")
loc = {}
exec("print(foo)", glob, loc)
# CPython => base item, and never looks up "print" in D
# PyPy => 'foo' from D, and looks up "print" in D

이미 딕셔너리 키로 사용 중인 객체의 클래스 변경하기

다음 코드 스니펫을 살펴보겠습니다:

class X(object):
    pass

def __evil_eq__(self, other):
    print 'hello world'
    return False

def evil(y):
    d = {X(): 1}
    X.__eq__ = __evil_eq__
    d[y] # might trigger a call to __eq__?

CPython에서는 __evil_eq__가 호출될 수도 있지만, 이를 안정적으로 호출하는 테스트를 작성할 방법은 없습니다. 이는 y is not x이고 hash(y) == hash(x)인 경우에 발생하며, 여기서 hash(x)x가 딕셔너리에 삽입될 때 계산됩니다. 우연히 조건이 만족되면 __evil_eq__가 호출됩니다.

PyPy는 기본 __hash__, __eq__, __cmp__를 오버라이드하지 않는 사용자 정의 클래스의 인스턴스를 키로 사용하는 딕셔너리를 최적화하기 위해 특별한 전략을 사용합니다: 이 전략을 사용할 때 __eq____cmp__는 결코 호출되지 않고, 대신 조회는 동일성(identity)에 의해 수행되므로, 위의 경우 __eq__가 호출되지 않음이 보장됩니다.

다른 모든 경우(예를 들어 y에 커스텀 __hash____eq__가 있는 경우)에는 동작이 CPython과 완전히 동일하다는 점에 유의하십시오.

무시된 예외

많은 예외적인(corner case) 상황에서 CPython은 예외를 조용히 삼킬 수 있습니다. 대부분의 경우가 매우 드물긴 하지만, 이런 일이 발생하는 정확한 경우의 목록은 상당히 깁니다. 가장 잘 알려진 곳은 사용자 정의 리치 비교 메서드(__eq__ 같은), 딕셔너리 조회, isinstance()와 같은 일부 내장 함수 호출입니다.

이 동작이 설계상 명확히 의도된 것이고 그렇게 문서화되어 있지 않은 한(예를 들어 hasattr()의 경우처럼), 대부분의 경우 PyPy는 대신 예외가 전파되도록 둡니다.

기본형 값의 객체 동일성, isid

기본형(primitive) 값의 객체 동일성(identity)은 래퍼(wrapper)의 동일성이 아니라 값 동등성(value equality)으로 작동합니다. 즉, 임의의 정수 x에 대해 x + 1 is x + 1은 항상 참입니다. 이 규칙은 다음 유형들에 적용됩니다:

  • int
  • float
  • long
  • complex
  • str (비어 있거나 단일 문자로 된 문자열만 해당)
  • unicode (빈 문자열 또는 단일 문자 문자열만 해당)
  • tuple(빈 튜플만 해당)
  • frozenset(빈 frozenset만 해당)
  • 언바운드 메서드객체 (Python 2 전용)

이 변경은 id에도 일부 변경이 필요합니다. id는 다음 조건을 만족합니다: x is y <=> id(x) == id(y). 따라서 위 타입들의 id는 인자로부터 계산된 값을 반환하므로 임의로 클 수 있으며, sys.maxsize(또는 PyPy2에서는 sys.maxint)보다 커질 수 있습니다.

길이가 2 이상인 문자열은 동일하지 않아도 같을 수 있다는 점에 유의하십시오. 마찬가지로, x가 튜플을 담고 있고 x == (2,)이더라도 x is (2,)가 반드시 참인 것은 아닙니다. 고유성 규칙은 위에서 설명한 특정 경우에만 적용됩니다. str, unicode, tuplefrozenset규칙은 PyPy 5.4에서 추가되었습니다. 그 이전에는 x"?"()와 같더라도 if x is "?"if x is ()같은 테스트가 실패할 수 있었습니다. PyPy 5.4에서 추가된 새로운 동작은 CPython의 동작에 더 가깝습니다. CPython은 정확히 빈 튜플/frozenset과 (일반적으로는 그렇지만 항상은 아니게) 길이가 1 이하인 문자열 및 유니코드를 캐시합니다.

부동소수점 수에서는 float의 “비트 패턴(bit pattern)”당 객체가 정확히 하나만 “있다”는 점에 유의하십시오. 따라서 float('nan') is float('nan')는 PyPy에서는 참이지만, 두 개의 서로 다른 객체이기 때문에 CPython에서는 그렇지 않습니다. 반면 0.0 is -0.0은 비트 패턴이 다르므로 항상 거짓입니다. 평소와 같이 float('nan') == float('nan')은 항상 거짓입니다. 컨테이너에서 사용될 때(예: 리스트 항목이나 집합의 원소로 사용될 때), 실제로 사용되는 동등성 규칙은 “if x is y or x == y”입니다(CPython과 PyPy 모두에서 동일합니다). 그 결과, PyPy에서는 모든 nans이 동일하기 때문에, CPython과 달리 집합에 여러 개를 넣을 수 없습니다. (이슈 #1974를 참고하십시오). 또 다른 결과로, cmp(float('nan'), float('nan')) == 0입니다. 이는 cmp가 인자가 동일한지 여부를 먼저 is로 확인하기 때문입니다(이 cmp 호출이 반환할 적절한 값이 없는데, cmp가 float에 전순서(total order)가 존재하는 척하지만 NaN에는 이것이 옳지 않기 때문입니다).

인스턴스 딕셔너리에서의 키 순서

PyPy에서 인스턴스 딕셔너리가 항상 삽입 순서대로 정렬되는 것은 아닙니다. 구체적으로, 클래스의 __init__이 속성을 서로 다른 순서로 추가하는 경우 이런 일이 발생할 수 있습니다. 다음은 예시입니다:

>>>> class A:
....     def __init__(self, a, b, reorder=False):
....         if not reorder:
....             self.a = a
....             self.b = b
....         else:
....             self.b = b
....             self.a = a
....
>>>> print(A(1, 2).__dict__)
{'a': 1, 'b': 2}
>>>> print(A(1, 2, reorder=True).__dict__)
{'a': 1, 'b': 2}

CPython은 마지막 줄에 {'b': 2, 'a': 1}을 출력합니다.

배경: CPython 3.6에서 딕셔너리는 삽입 순서대로 정렬되었습니다. 이는 인스턴스 딕셔너리에도 해당합니다. PyPy에서는 일반 딕셔너리가 2015년부터 정렬되어 있습니다. 인스턴스 딕셔너리의 경우, 이는 항상 가능한 것은 아닙니다. 이는 PyPy 성능의 초석 중 하나인 Self 스타일 맵/히든 클래스를 사용해 표현되기 때문입니다.

확장 모듈에서 허용되는 ABI 태그

CPython은 abi3 ABI 태그가 있는 모듈로 제한된 C-API를 지원하며, ABI나 플랫폼 태그가 없는 확장 모듈도 임포트합니다. 이는 (Python3.9, x86_64, linux에서) _imp.extension_suffix() 호출을 비교해보면 확인할 수 있습니다:

python _imp.extension_suffixes() 노트
cpython3.9 [“.cpython-39-x86_64-linux-gnu.so”, 일반 [1]
“.abi3.so”, 제한된 C-API
“.so”] 확장자 없음
pypy3.9 [‘.pypy39-pp73-x86_64-linux-gnu.so’] 일반 [1]

각주

[1](1, 2) 일반 확장은 <python-tag>-<abi-tag>-<platform-tag>를 사용합니다

CMake는 2023년 초로 예정된 릴리스 3.26에서 PyPy3.9에 대한 올바른 접미사 지원을 할 예정입니다

C-API 차이점

외부 C-API는 PyPy에서 내부 cpyext 모듈로 재구현되었습니다. 문서화된 C-API의 대부분을 지원하지만, 때때로 내부 C 추상화가 CPython에서 새어 나와 (아마도 의도치 않게) 남용되기도 합니다. 예를 들어, 튜플이 내부적으로 사용된 이후에는 PyTupleObject에 대한 할당이 지원되지 않으며, 이는 다른 C-API 함수 호출에 의한 사용이라도 마찬가지입니다. CPython에서는 참조 카운트(refcount)가 1인 한 이 작업이 성공합니다. PyPy에서는 항상 SystemError('PyTuple_SetItem called on tuple after  use of tuple") 예외가 발생합니다(검색 엔진을 위해 여기에 명시적으로 기재합니다).

또 다른 유사한 문제는 PyType_Ready를 호출한 후 tp_as_* 구조체 중 하나에 새 함수 포인터를 할당하는 것입니다. 예를 들어, CPython에서 PyType_Ready를 호출한 후 tp_as_number.nb_int를 다른 함수로 재정의하면, x.__int__()는 (클래스 __dict__ 조회를 통해) 이전 함수를 호출하고, int(x)는 (슬롯 조회를 통해) 새 함수를 호출하게 됩니다. PyPy에서는 이전 함수가 아니라 항상 __new__ 함수를 호출하며, 이 특이한 동작은 안타깝게도 NumPy를 완전히 지원하기 위해 필요합니다.

cpyext 레이어는 복잡성을 추가하고느립니다. 가능하면 cffi 또는 HPy를 사용하십시오.

성능 차이

CPython은 반복적인 문자열 연결이 이차 시간이 되지 않도록 하는 최적화 기능을 갖추고 있습니다. 예를 들어, 이런 종류의 코드는 O(n) 시간에 실행됩니다.:

s = ''
for string in mylist:
    s += string

PyPy에서 이 코드는 항상 이차 복잡도를 가집니다. 또한 CPython 최적화는 취약하여 코드에 약간의 변화만 있어도 깨질 수 있다는 점을 유의하십시오. 따라서 어차피 코드를 다음과 같이 교체해야 합니다:

parts = []
for string in mylist:
    parts.append(string)
s = "".join(parts)

Python 3.11의 바이트코드 차이점

PyPy 3.11과 CPython 3.11 사이에는 바이트코드차이가 있습니다:

  • RESUME opcode가 없습니다.

    CPython 3.11은 모든 함수, 클래스 본문, 모듈의 시작 부분에 RESUME 옵코드를 삽입합니다. 이 옵코드에는 줄 번호 0이 할당되며, 트레이싱과 프로파일링을 위해 적응형 특수화 인터프리터가 사용합니다. PyPy는 RESUME을 내보내지 않습니다.

    눈에 보이는 효과:

    • co.co_lines() / co_positions(): CPython에서는 첫 번째 항목이 0번째 줄(RESUME)을 가리키지만, PyPy의 첫 번째 항목은 첫 실제 문장입니다.
    • 간단한 함수에 대해 dis.get_instructions()는 PyPy에서 하나 적은 명령어를 반환합니다.
  • 통합된 BINARY_OP와 연산자별 오피코드

    CPython 3.11은 타입별로 특화된 이진 연산 opcode 계열(BINARY_ADD, BINARY_SUBTRACT 등과 이에 대응하는 INPLACE_* 계열)을, 연산자를 인자로 받는 단일 BINARY_OP 명령어로 대체했습니다. PyPy는 연산자별 opcode를 원래대로 유지합니다.

  • 통합된 CALL과(와) CALL_FUNCTION / CALL_METHOD의 비교

    CPython 3.11은 CALL_FUNCTION, CALL_FUNCTION_KW, CALL_FUNCTION_EX, CALL_METHOD를 통합된 CALL 명령어(PUSH_NULL 또는 PRECALL이 앞에 옴)로 대체했습니다. PyPy는 이전 동작을 유지합니다. PRECALL은 스페셜라이저(specialiser)가 호출 지점을 재작성하는 데 사용된다는 점에 유의하십시오.

    가시적 효과: 옵코드 이름으로 분기하거나 PRECALL을 확인하는 코드는 PyPy에서 이를 찾을 수 없습니다.

  • LOAD_METHOD가 여전히 존재합니다

    CPython 3.11은 LOAD_METHOD를 제거하고 메서드 로딩을 LOAD_ATTR로 통합했습니다(이 명령은 런타임에 디스크립터를 검사하고 스택에 플래그를 남깁니다). PyPy는 메서드 호출에 대해 여전히 LOAD_METHOD를 방출하며, 이어서 CALL_METHOD를 사용합니다.

  • 인라인 캐시 없음

    CPython 3.11은 적응형 특수화 인터프리터를 지원하기 위해 인라인 캐시 항목을 바이트코드 스트림에 직접 내장합니다(CACHE 의사 명령어). 이는 co_code를 상당히 부풀립니다. PyPy는 적응형 특수화기가 없으며 인라인 캐시가 다른 곳에 저장되므로, 코드 객체가 더 작습니다. len(co_code)log(len(co_code))를 스택 깊이의 대용으로 사용하는 모든 테스트는 서로 다른 값을 보게 됩니다.

기타

  • 해시 무작위화(-R)는 is ignored in PyPy에서 무시됩니다. 3.4 이전 버전의 CPython에서는 little point를 가집니다. CPython >= 3.4와 PyPy3 모두 무작위화된 SipHash 알고리즘을 구현하며 -R을 무시합니다.

  • 타입 객체에는 문자열이 아닌 키를 저장할 수 없습니다. 예를 들어:

    class A(object):
        locals()[42] = 3
    

    작동하지 않습니다.

  • sys.setrecursionlimit(n)은 사용 가능한 스택 공간을 n * 768바이트로 설정하여 제한을 근사적으로만 설정합니다. 리눅스에서는 컴파일러 설정에 따라 기본값인 768KB로 약 1400번의 호출까지 충분합니다.

  • 딕셔너리(dictionary) 구현이 다르기 때문에, __hash____eq__가 호출되는 정확한 횟수도 다릅니다. CPython도 특별한 보장을 제공하지 않으므로, 이에 의존하지 마십시오.

  • __builtins__이름은 항상 __builtin__모듈을 참조하며, CPython에서 때때로 그런 것처럼 딕셔너리를 참조하는 경우는 결코 없습니다. __builtins__에 값을 할당해도 아무런 효과가 없습니다. (RestrictedPython과 같은 도구의 사용 사례에 대해서는 issue #2653를 참고하세요.)

  • 일부 내장 타입의 내부 매직 메서드를 유효하지 않은 인자로 직접 호출하면 결과가 약간 다를 수 있습니다. 예를 들어, [].__add__(None)(2).__add__(None)은 PyPy에서 둘 다 NotImplemented를 반환합니다. CPython에서는 후자만 그렇고, 전자는 TypeError를 발생시킵니다. (물론 []+None2+None은 어디서나 둘 다 TypeError를 발생시킵니다.) 이 차이는 PyPy에 없는 내부 C 레벨 슬롯 때문에 나타나는 구현 세부사항입니다.

  • CPython에서 [].__add__method-wrapper이고, list.__add__slot wrapper이며, list.extend는 (내장) method 객체입니다. PyPy에서는 이것들이 모두 일반적인 메서드 또는 함수 객체입니다(PyPy2에서는 언바운드 메서드 객체). 이는 내장 타입을 검사하는 일부 도구를 가끔 혼란스럽게 할 수 있습니다. 예를 들어, 표준 라이브러리 inspect 모듈에는 언바운드 메서드 객체에 대해서는 True를 반환하지만 method-wrapper나 slot wrapper에 대해서는 False를 반환하는 ismethod() 함수가 있습니다. PyPy에서는 그 차이를 구별할 수 없습니다. 따라서 PyPy2에서는 ismethod([].__add__) == ismethod(list.extend) == True이고, PyPy3에서는 isfunction(list.extend) == True입니다. CPython에서는 이 모든 것이 False입니다.

  • CPython에서 내장 타입들은 다양한 방식으로 구현될 수 있는 속성을 가지고 있습니다. 그 방식에 따라, 읽기 전용(또는 삭제 불가능한) 속성에 쓰기(또는 삭제)를 시도하면 TypeErrorAttributeError가 발생합니다. PyPy는 여기서 완전한 일관성과 완전한 호환성 사이에서 어느 정도 중간 지점을 찾으려고 합니다. 이는 몇 가지 코너 케이스에서는 del (lambda:None).__closure__처럼 동일한 예외가 발생하지 않는다는 것을 의미합니다.

  • 순수 Python에서 class A(object): def f(self): pass를 작성하고 f()를 오버라이드하지 않는 서브클래스 B가 있다면, B.f(x)는 여전히 xB의 인스턴스인지 확인합니다. CPython에서는 C로 작성된 타입이 다른 규칙을 사용합니다. A가 C로 작성되었다면, A의 모든 인스턴스는 B.f(x)에 의해 허용됩니다(그리고 실제로 이 경우 B.f is A.f입니다). CPython에서는 동작하지만 PyPy에서는 동작하지 않을 수 있는 코드의 예로는 datetime.datetime.strftime(datetime.date.today(), ...)가 있습니다(여기서 datetime.datedatetime.datetime의 상위 클래스입니다). 어쨌든 제대로 된 해결책은 애초에 일반적인 메서드 호출을 사용하는 것이라고 할 수 있습니다: datetime.date.today().strftime(...)

  • gc모듈의 일부 함수와 속성은 약간 다르게 동작합니다: 예를 들어, gc.enablegc.disable은 지원되지만, “GC를 활성화하고 비활성화하는 것”은 PyPy에서 CPython과 다른 의미를 가집니다. 이 함수들은 실제로 메이저 컬렉션과 파이널라이저의 실행을 활성화하고 비활성화합니다.

  • PyPy는 대화형 모드에서 시작할 때 과거 #pypy IRC 주제 중 무작위로 한 줄을 출력합니다. 릴리스 버전에서는 이 동작이 억제되지만, 환경 변수 PYPY_IRC_TOPIC을 설정하면 다시 활성화됩니다. 다운스트림 패키지 제공자가 이 기능을 완전히 비활성화하는 경우도 있다는 점에 유의하십시오.

  • PyPy의 readline 모듈은 처음부터 새로 작성되었습니다: GNU의 readline이 아닙니다. 대체로 호환되며, 멀티라인 지원이 추가되었습니다(multiline_input() 참고). 반면, parse_and_bind() 호출은 무시됩니다(이슈 #2072).

  • sys.getsizeof()는 항상 TypeError를 발생시킵니다(그리고 객체는 __sizeof__ 메서드를 가지지 않습니다). 이는 이 함수를 사용하는 메모리 프로파일러가 PyPy에서는 실제와 맞지 않는 결과를 낼 가능성이 높기 때문입니다. 충분한 작업을 들이면 sys.getsizeof()가 어떤 숫자를 반환하게 만드는 것도 가능하겠지만, 그 숫자가 객체가 사용하는 메모리량을 나타낼 수도 있고 그렇지 않을 수도 있습니다. 시스템의 나머지 부분과 분리해서 객체 하나가 얼마나 사용하는지 묻는 것 자체가 사실 큰 의미가 없습니다. 예를 들어, 인스턴스는 맵(map)을 가지는데, 이 맵은 흔히 여러 인스턴스에 걸쳐 공유됩니다. 이 경우 sys.getsizeof()의 구현은 아마 맵을 무시할 것이지만, 고유한 맵을 가진 인스턴스가 많은 경우에는 맵의 오버헤드가 중요할 수 있습니다. 반대로, 서로 다른 객체라 하더라도 값이 같은 문자열은 내부 문자열 데이터를 공유할 수 있습니다—유니코드 문자열과 그것을 UTF-8로 인코딩한 bytes 버전조차 공유됩니다—또는 빈 컨테이너는 비어 있는 한 내부의 일부를 공유할 수 있습니다. 더 이상하게도, 일부 리스트는 읽는 시점에 객체를 생성합니다. range(10**6)의 메모리 크기를 모든 항목 크기의 합으로 추정하려고 하면, 그 연산 자체가 애초에 존재하지 않았던 백만 개의 정수 객체를 생성하게 됩니다. 이러한 우려 중 일부는 CPython에도 존재하지만, 정도가 덜할 뿐입니다. 이러한 이유로 우리는 sys.getsizeof()를(__sizeof__도 마찬가지로) 명시적으로 구현하지 않습니다.

  • timeit모듈은 PyPy에서 다르게 동작합니다: 최솟값은 종종 오해의 소지가 있기 때문에, 최솟값 대신 평균 시간과 표준 편차를 출력합니다.

  • sysconfigdistutils.sysconfigget_config_vars 메서드는 완전하지 않습니다. POSIX 플랫폼에서 CPython은 인터프리터를 빌드하는 데 사용된 Makefile에서 설정 변수를 가져옵니다. PyPy는 컴파일 중에 값을 박아 넣어야 하지만, 아직 그렇게 하지 않습니다.

  • CPython의 sys.settrace()foryield from 줄 끝에서 StopIteration에 대해 exception을 보고할 때도 있고, 그렇지 않을 때도 있습니다. 문제는 이것이 명확히 정의되지 않은 경우들의 부분집합에서 발생한다는 점입니다. PyPy는 이를 모방하려 시도하지만, 정확한 경우의 집합이 완전히 같지는 않습니다.

  • "%d" % x"%x" % x 및 이와 유사한 구문들에서, x가 특수 메서드 __str__, __hex__, __oct__를 오버라이드하는 long의 서브클래스 인스턴스인 경우: PyPy는 특수 메서드를 호출하지 않지만 CPython은 호출합니다—단 int가 아니라 long의 서브클래스인 경우에만 그렇습니다. CPython의 동작은 정말 지저분합니다: 예를 들어 %x의 경우 __hex__()를 호출하는데, 이는 -0x123L과 같은 문자열을 반환하도록 되어 있습니다; 그런 다음 0x와 마지막 L이 제거되고 나머지는 유지됩니다. __hex__()에서 예상치 못한 문자열을 반환하면 예외가 발생합니다(또는 CPython 2.7.13 이전 버전에서는 충돌이 발생합니다).

  • PyPy에서는 **kwargs로 전달된 딕셔너리는 dict()dict.update()에서도 문자열 키만 포함할 수 있습니다. CPython 2.7은 이 두 경우에(우리가 아는 한 오직 이 경우에만) 문자열이 아닌 키를 허용합니다. 예를 들어, 다음 코드는 CPython 3.x뿐만 아니라 모든 PyPy에서도 TypeError를 발생시킵니다: dict(**{1: 2}). (dict(**d1)dict(d1)과 동등하다는 점에 유의하십시오.)

  • __class__에 대한 할당은 CPython 2.5에서 작동하는 경우로 제한됩니다. CPython 2.6과 2.7에서는 좀 더 많은 경우에 작동하지만, 이는 지금까지 PyPy에서 지원되지 않습니다. (필요하다면 지원될 수 있지만, 그렇게 되면 PyPy에서 CPython 2.6/2.7보다 많은 경우에 작동할 가능성이 높습니다.) PyPy 3에서는 힙 타입(heaptype)과 비힙 타입 간의 __class__ 속성 할당입니다. CPython은 모듈 서브타입에 대해서는 이를 허용하지만, 예를 들어 intfloat 서브타입에 대해서는 허용하지 않습니다. 현재 PyPy는 비힙 타입 서브타입에 대한 __class__ 속성 할당을 전혀 지원하지 않습니다.

  • PyPy에서 모듈과 클래스 딕셔너리는 속성을 삭제하는 일이 드물다는 가정 하에 최적화됩니다. 이 때문에, 예를 들어 foo가 함수 bar를 포함하는 모듈(또는 클래스)인 경우 del foo.bar는 CPython보다 상당히 느립니다.

  • CPython에서 다양한 내장 함수들은 위치 인자만 받고 키워드 인자는 받지 않습니다. 이는 오랫동안 이어져 온 역사적 세부사항으로 볼 수 있습니다: 새로운 함수들은 키워드 인자를 받아들이는 경향이 있고, 오래된 함수도 종종 이를 지원하도록 수정됩니다. PyPy에서는 대부분의 내장 함수가 키워드 인자를 받습니다(help()는 인자 이름을 보여줍니다). 하지만 CPython이 나중에 이를 받아들이기 시작하면 PyPy의 향후 버전에서 인자 이름을 변경해야 할 수도 있으니 이에 너무 의존하지 마십시오.

  • PyPy3: distutilsVS%0.f0COMNTOOLS (일반적으로 VS140COMNTOOLS) 환경 변수가 가리키는 디렉터리에서 VsDevCmd.bat을 찾을 수 있도록 개선되었습니다. CPython은 그 값보다 위쪽 어딘가에서 vcvarsall.bat을 찾습니다.

  • SyntaxError는 실패 원인에 대한 세부 정보를 더 열심히 제공하려고 하므로, 오류 메시지가 CPython과 동일하지 않습니다

  • PyPy에서 딕셔너리와 집합은 순서가 있습니다. CPython 3.6 미만에서는 그렇지 않으며, CPython 3.6 이상에서는 딕셔너리(집합은 제외)에 순서가 있습니다.

  • PyPy2는 단독으로 남은 .pyc 파일, 즉 .py 파일을 삭제한 후에도 남아 있는 .pyc 파일을 로드하지 않습니다. PyPy3는 대신 CPython처럼 동작합니다. PyPy2에서 이 차이를 기꺼이 수정할 수도 있습니다: 현재 버전은 PyPy를 개발하는 동안 우리를 너무 자주 괴롭혔던 CPython의 이 세부 사항에 대한 우리의 짜증을 반영한 것입니다. (정말 필요하다면 PyPy를 번역(translation)할 때 --lonepycfile 플래그를 전달하는 것만큼 쉽습니다.)

  • PyPy3는 sys.path를 재정의하기 위한 _pth file protocol을 구현하지 않습니다.

확장 모듈

우리가 지원하는 확장 모듈 목록입니다:

  • 내장 모듈로 지원됩니다 (pypy/module/에):

    __builtin__ __pypy__ _ast _codecs _collections _continuation _ffi _hashlib _io _locale _lsprof _md5 _minimal_curses _multiprocessing _random _rawffi _sha _socket _sre _ssl _warnings _weakref _winreg array binascii bz2 cStringIO cmath cpyext crypt errno exceptions fcntl gc imp itertools marshal math mmap operator parser posix pyexpat select signal struct symbol sys termios thread time token unicodedata zipimport zlib

    Windows에서 번역(translation)될 때, 일부 Unix 전용 모듈은 건너뛰어지고, 대신 다음 모듈이 빌드됩니다:

    _winreg

  • 순수 Python(경우에 따라 cffi를 사용하여)으로 다시 작성되어 지원됩니다: lib_pypy/ 디렉터리를 참고하십시오. 이 방식으로 지원하는 모듈의 예로는 ctypes, cPickle, cmath, dbm, datetime 등이 있습니다. 이 중 일부 모듈은 그곳과 위 목록 양쪽 모두에 있다는 점에 유의하십시오. 기본적으로는 내장 모듈이 사용됩니다(다만 변환 시점에 비활성화할 수 있습니다).

위에서도, lib_pypy/에서도 언급되지 않은 확장 모듈(즉, 표준 CPython에서 C로 작성된 모듈)은 PyPy에서 사용할 수 없습니다.