PyPy와 CPython의 차이점

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

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

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

PyPy가 사용하거나 구현하는 가비지 컬렉터는 참조 카운팅에 기반하지 않으므로, 객체에 더 이상 도달할 수 없게 되어도 객체가 즉시 해제되지 않습니다. 이로 인한 가장 뚜렷한 영향은 파일(그리고 소켓 등)이 스코프를 벗어나도 곧바로 닫히지 않는다는 점입니다. 쓰기용으로 열린 파일의 경우 데이터가 한동안 출력 버퍼에 남아 있을 수 있으며, 이 때문에 디스크상의 파일이 비어 있거나 잘린 것처럼 보일 수 있습니다. 게다가 운영체제가 허용하는 동시에 열 수 있는 파일 수의 한도에 도달할 수도 있습니다.

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도 마찬가지입니다). 이는 또한 약한 참조가 예상보다 조금 더 오래 살아 있을 수 있다는 뜻이기도 합니다. 이 때문에 (weakref.proxy()가 반환하는) “약한 프록시”의 유용성이 다소 떨어집니다. PyPy에서는 약한 프록시가 조금 더 오래 살아 있는 것처럼 보이다가 갑자기 실제로 소멸하며, 그다음 접근할 때 ReferenceError를 발생시킵니다. 약한 프록시를 사용하는 코드는 약한 프록시를 사용하는 모든 곳에서 이러한 ReferenceError를 빠짐없이 잡아야 합니다. (더 좋은 방법은 weakref.proxy()를 아예 사용하지 않고 weakref.ref()를 사용하는 것입니다.)

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

callback이 주어졌고 None이 아니며 반환된 weakref 객체가 아직 살아 있다면, 객체가 파이널라이즈(finalize)되기 직전에 callback이 호출됩니다.

CPython의 참조 카운트(refcount) 의미론 때문에 weakref가 자신이 가리키는 객체보다 바로 직전이나 직후에 소멸하는 경우가 있습니다(대개 순환 참조가 있을 때입니다). weakref가 마침 객체 바로 직후에 소멸하면 콜백이 호출됩니다. PyPy에서 이와 비슷한 상황이 발생하면 객체와 weakref가 동시에 소멸한 것으로 간주되므로 콜백이 호출되지 않습니다. (이슈 #2030)

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


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

이 차이가 경우에 따라서는 간접적으로 드러날 수 있다는 점에 유의하십시오. 예를 들어, 도중에 멈춘 채로 남겨진 제너레이터는 — 이번에도 — CPython보다 PyPy에서 더 늦게 가비지 컬렉션됩니다. 제너레이터가 멈춰 있는 yield 키워드 자체가 try: 또는 with: 블록 안에 들어 있다면 그 차이를 확인할 수 있습니다. 이러한 현상은 예를 들어 issue 736에서 나타납니다.

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

객체들이 긴 사슬을 이루고, 각 객체가 다음 객체에 대한 참조를 가지며, 각 객체에 __del__이 있다면 PyPy의 GC는 성능이 나빠진다는 점에 유의하십시오. 다행히 그 밖의 대부분의 경우에는 PyPy의 GC가 CPython의 GC보다 훨씬 더 좋은 성능을 낸다는 것이 벤치마크를 통해 확인되었습니다.

또 다른 차이점은 기존 클래스에 __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과 완전히 같다는 점에 유의하십시오.

무시된 예외

CPython은 여러 코너 케이스에서 예외를 아무 말 없이 삼켜 버릴 수 있습니다. 대부분은 매우 드문 경우이지만, 이런 일이 일어나는 경우를 정확히 나열하면 목록이 꽤 깁니다. 가장 잘 알려진 경우는 사용자 정의 풍부한 비교(rich comparison) 메서드(__eq__ 등), 딕셔너리 조회, isinstance() 같은 일부 내장 함수 호출입니다.

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

원시 값의 객체 동일성, is와 id

기본형 값의 객체 동일성은 래퍼의 동일성이 아니라 값의 동등성에 따라 결정됩니다. 즉, 임의의 정수 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 이상인 문자열은 동일(identical)하지 않으면서도 같을(equal) 수 있다는 점에 유의하십시오. 마찬가지로, x에 튜플이 들어 있고 x == (2,)이더라도 x is (2,)가 반드시 참인 것은 아닙니다. 이러한 유일성 규칙은 위에서 설명한 특정 경우에만 적용됩니다. str, unicode, tuple, frozenset 규칙은 PyPy 5.4에서 추가되었습니다. 그 이전에는 x가 "?" 또는 ()와 같더라도 if x is "?" 또는 if x is () 같은 테스트가 실패할 수 있었습니다. PyPy 5.4에서 추가된 새 동작은 CPython의 동작에 더 가깝습니다. CPython은 정확히 빈 tuple/frozenset, 그리고 (항상은 아니지만 일반적으로) 길이가 1 이하인 문자열과 유니코드를 캐시합니다.

부동소수점 수의 경우 “비트 패턴” 하나당 객체가 하나만 “is”(존재)한다는 점에 유의하십시오. 따라서 float('nan') is float('nan')은 PyPy에서는 참이지만, CPython에서는 두 객체가 서로 다른 객체이므로 참이 아닙니다. 반면 0.0 is -0.0은 비트 패턴이 다르므로 항상 False입니다. 늘 그렇듯이 float('nan') == float('nan')은 항상 False입니다. 컨테이너 안에서(예를 들어 리스트 항목이나 집합 원소로) 사용될 때 적용되는 정확한 동등성 규칙은 “if x is y or x == y”입니다(CPython과 PyPy 모두 동일합니다). 그 결과 PyPy에서는 모든 nans가 동일하므로, CPython과 달리 집합에 여러 개를 넣을 수 없습니다. (이슈 #1974). 또 다른 결과로 cmp(float('nan'), float('nan')) == 0이 됩니다. cmp는 인자들이 동일한지를 먼저 is로 확인하기 때문입니다(이 cmp 호출이 반환할 적절한 값은 없습니다. cmp는 부동소수점 수에 전순서(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 태그나 플랫폼 태그가 없는 확장 모듈도 임포트합니다. 이는 _imp.extension_suffix() 호출 결과를 비교해 보면 알 수 있습니다(Python3.9, x86_64, linux 기준):

python _imp.extension_suffixes() 참고
cpython3.9 [“.cpython-39-x86_64-linux-gnu.so”, 일반 [1]
“.abi3.so”, 제한된 C-API
“.so”] 베어(bare) 확장
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의 대부분을 지원하지만, 때때로 CPython에서는 내부 C 추상화가 밖으로 새어 나와 오용되기도 하며, 이는 어쩌면 모르는 사이에 일어나기도 합니다. 예를 들어, 튜플 인스턴스가 내부적으로 사용된 후에는 다른 C-API 함수 호출에 의한 경우라도 PyTupleObject에 대한 할당이 지원되지 않습니다. 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 옵코드가 없습니다.

    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은 타입별 이항 옵코드 계열(BINARY_ADD, BINARY_SUBTRACT 등과 이에 대응하는 INPLACE_* 옵코드)을 연산자를 인자로 받는 단일 BINARY_OP 명령어로 대체했습니다. PyPy는 연산자별로 나뉜 기존 옵코드를 그대로 유지합니다.

  • 통합 CALL과 CALL_FUNCTION / CALL_METHOD 비교

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

    드러나는 영향: 옵코드 이름으로 분기하거나 PRECALL을 확인하는 코드는 PyPy에서 이를 찾지 못합니다.

  • LOAD_METHOD가 여전히 존재합니다

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

  • 인라인 캐시 없음

    CPython 3.11은 적응형 특수화 인터프리터를 지원하기 위해 인라인 캐시 항목(CACHE 의사 명령어)을 바이트코드 스트림에 직접 삽입합니다. 이 항목들 때문에 co_code가 상당히 커집니다. PyPy에는 적응형 특수화 기능이 없고 인라인 캐시도 다른 곳에 저장되므로, 코드 객체가 더 작습니다. len(co_code) 또는 log(len(co_code))를 스택 깊이의 프록시로 사용하는 테스트는 모두 다른 값을 보게 됩니다.

기타

  • 해시 무작위화(-R)는 PyPy에서 무시됩니다(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 바이트로 설정하는 방식이므로 한도를 대략적으로만 설정합니다. Linux에서는 컴파일러 설정에 따라 다르지만, 기본값인 768KB는 약 1400번의 호출에 충분합니다.

  • 딕셔너리 구현이 다르기 때문에 __hash__와 __eq__가 호출되는 정확한 횟수도 다릅니다. CPython 역시 이에 대해 특별히 보장하지 않으므로 이 동작에 의존하지 마십시오.

  • __builtins__ 이름은 항상 __builtin__ 모듈을 참조하며, CPython에서 때때로 그렇듯이 딕셔너리를 참조하는 일은 없습니다. __builtins__에 값을 할당해도 아무런 효과가 없습니다. (RestrictedPython과 같은 도구를 사용하는 경우에 대해서는 issue #2653을 참조하십시오.)

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

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

  • CPython에서 내장 타입들은 여러 가지 방식으로 구현될 수 있는 속성을 가지고 있습니다. 구현 방식에 따라, 읽기 전용(또는 삭제 불가능한) 속성에 값을 쓰거나 그 속성을 삭제하려고 하면 TypeError가 발생하기도 하고 AttributeError가 발생하기도 합니다. PyPy는 이 부분에서 완전한 일관성과 완전한 호환성 사이의 절충점을 찾으려고 합니다. 즉, del (lambda:None).__closure__와 같은 몇몇 코너 케이스에서는 같은 예외가 발생하지 않습니다.

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

  • gc 모듈의 일부 함수와 속성은 약간 다르게 동작합니다. 예를 들어 gc.enable과 gc.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()가 숫자를 반환하도록 만들 수는 있겠지만, 그 숫자가 객체가 실제로 사용하는 메모리 양을 나타낼 수도 있고 그렇지 않을 수도 있습니다. 시스템의 나머지 부분과 떼어 놓고 하나의 객체가 메모리를 얼마나 사용하는지 묻는 것 자체가 사실 별로 의미가 없습니다. 예를 들어 인스턴스에는 맵이 있는데, 맵은 여러 인스턴스 사이에서 공유되는 경우가 많습니다. 이 경우 sys.getsizeof() 구현은 아마 맵을 무시하겠지만, 고유한 맵을 가진 인스턴스가 많다면 경우에 따라 맵의 오버헤드가 중요해집니다. 반대로, 값이 같은 문자열은 서로 다른 객체이더라도 내부 문자열 데이터를 공유할 수 있으며—유니코드 문자열과 그것을 utf8로 인코딩한 bytes 버전조차 데이터를 공유합니다—빈 컨테이너는 비어 있는 동안 내부 구조의 일부를 공유할 수도 있습니다. 더 이상한 점은, 어떤 리스트는 읽는 순간에 객체를 생성한다는 것입니다. range(10**6)의 메모리 크기를 모든 항목 크기의 합으로 추정하려고 하면, 그 작업 자체가 애초에 존재하지도 않았던 정수 객체 백만 개를 생성하게 됩니다. 이러한 문제 중 일부는 CPython에도 존재하지만, 그 정도가 덜할 뿐이라는 점에 유의하십시오. 이러한 이유로 PyPy는 sys.getsizeof()를 (__sizeof__도) 의도적으로 구현하지 않습니다.

  • timeit 모듈은 PyPy에서 다르게 동작합니다. 최솟값은 오해를 불러일으키는 경우가 많으므로, 최솟값 대신 평균 시간과 표준 편차를 출력합니다.

  • sysconfig와 distutils.sysconfig의 get_config_vars 메서드는 완전하지 않습니다. POSIX 플랫폼에서 CPython은 인터프리터를 빌드하는 데 사용된 Makefile에서 설정 변수를 가져옵니다. PyPy는 컴파일할 때 이 값들을 미리 넣어 두어야 하지만, 아직은 그렇게 하지 않습니다.

  • CPython의 sys.settrace()는 for나 yield from 줄의 끝에서 StopIteration에 대한 exception을 보고할 때도 있고 보고하지 않을 때도 있습니다. 문제는 이 동작이 명확하게 정의되지 않은 일부 경우에만 발생한다는 점입니다. PyPy는 이를 흉내 내려고 하지만, 정확히 어떤 경우에 발생하는지는 CPython과 완전히 같지 않습니다.

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

  • PyPy에서는 **kwargs로 전달되는 딕셔너리에 문자열 키만 사용할 수 있으며, dict()와 dict.update()도 예외가 아닙니다. CPython 2.7은 이 두 경우에 문자열이 아닌 키를 허용합니다(저희가 아는 한 이 두 경우뿐입니다). 예를 들어 다음 코드는 모든 PyPy뿐 아니라 CPython 3.x에서도 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은 모듈 하위 타입에 대해서는 이를 허용하지만, 예를 들어 int나 float 하위 타입에 대해서는 허용하지 않습니다. 현재 PyPy는 비힙 타입의 어떤 하위 타입에 대해서도 __class__ 속성 할당을 지원하지 않습니다.

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

  • CPython의 여러 내장 함수는 위치 인자만 받고 키워드 인자는 받지 않습니다. 이는 오래전부터 이어져 온 역사적 세부 사항으로 볼 수 있습니다. 새로운 함수는 대체로 키워드 인자를 받으며, 오래된 함수도 가끔 키워드 인자를 받도록 수정됩니다. PyPy에서는 대부분의 내장 함수가 키워드 인자를 받습니다(help()로 인자 이름을 확인할 수 있습니다). 하지만 여기에 너무 의존하지는 마십시오. CPython도 키워드 인자를 받기 시작하면 향후 PyPy 버전에서 인자 이름을 바꿔야 할 수도 있기 때문입니다.

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

  • SyntaxError는 실패 원인을 더 자세히 알려 주려고 하므로, 오류 메시지가 CPython과 같지 않습니다

  • PyPy에서는 딕셔너리와 집합의 순서가 유지됩니다. CPython < 3.6에서는 순서가 유지되지 않으며, CPython >= 3.6에서는 딕셔너리의 순서는 유지되지만 집합의 순서는 유지되지 않습니다.

  • PyPy2는 짝 없는 .pyc 파일, 즉 .py 파일을 삭제한 뒤에도 남아 있는 .pyc 파일을 불러오지 않습니다. 반면 PyPy3은 CPython과 같이 동작합니다. PyPy2의 이 차이를 고치는 것도 기꺼이 고려할 수 있습니다. 현재 버전은 PyPy를 개발하는 동안 너무 자주 저희를 곤란하게 했던 CPython의 이 세부 동작에 대한 저희의 불만을 반영한 것입니다. (꼭 필요하다면 PyPy를 변환할 때 --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에서 변환할 때는 Unix 전용 모듈 몇 개를 건너뛰고, 그 대신 다음 모듈을 빌드합니다:

    _winreg

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

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