메아리 저널

파이썬을 두루 이해하는 데 도움 되는(?) 퀴즈: 답변 (1편)

파이썬을 두루 이해하는 데 도움 되는(?) 퀴즈(퍼키의 열고 보는 세상)를 보고 한 번 풀어 본답시고 풀어 봤습니다. 맞는지는 저도 보장 못 합니다. -_-;

너무 많아서 일단 첫 열 문제만 풀어서 1편으로 나눕니다.

1번

역설적이게도 decimal.Decimal, int/long, str, float 순서.

float는 mantissa의 길이가 고정된 부동 소수점 값이라서 뒤로 밀립니다. int/long과 str 중에서는, int/long은 한 바이트의 7비트 이상(8비트는 아닌 걸로 기억하고 있음)을 활용하지만 str은 고작해야 log236 ≈ 5.2비트 밖에 못 쓰니까 int/long이 더 많은 정보를 담겠지요. decimal.Decimal은 float과 마찬가지로 mantissa의 길이가 (context에 따라) 고정되어 있지만, 대신 지수부를 마음대로 늘릴 수 있기 때문에 가장 큰 수를 저장할 수 있습니다.

참고로 int/long은 보통 10만자리를 넘으면 repr하기도 벅찹니다. 2진법으로 저장된 숫자를 10진법으로 변환하는 데 10만회의 나눗셈이 필요하거든요. FFT를 사용해서 변환하는 빠른 방법도 존재하긴 합니다만 파이썬에 이런 걸 기대하긴 무리인 듯.

2번

set과 dict.

일반적으로 변하지 않는 값(immutable)만이 해시가 가능하며, 따라서 dict나 set 등의 키값으로 쓰일 수 있습니다. 물론 멀쩡하게 생긴 클래스에 __hash__ 메소드 집어 넣고 뻥을 치는 건 가능하긴 합니다만 이 클래스의 값을 바꿔 버리면 사전의 동작이 희한하게 변해 버릴테니 좋은 행동은 아니겠죠.

덤으로 만약 set을 해시 가능하게 만들려면 frozenset이라는 별도의 형을 써야 합니다.

3번

(ㄱ). 그 다음은 (ㄷ) 아니면 (ㄴ)인데 둘의 차이는 별로 크지 않아서 생략.

(ㄱ)은 range를 써서 임시 리스트를 하나 만들 뿐만 아니라 제너레이터가 아닌 리스트 해석을 써서 또 다른 리스트를 만들고 있습니다. 1000 크기의 리스트 두 개가 생기니 가장 많은 메모리를 먹을 수 밖에요.

4번

가장 쉬운 방법은 __x 메소드가 어느 클래스에 있는지 확인해서, 그 이름을 통해 바로 접근(예를 들면 _SuperClass__x)하는 것이겠습니다.

만약 어느 클래스에 있는지 잘 모르겠다면, 그리고 object로부터 상속받은 (보통 new-style이라 불리는) 클래스라면, __mro__ 속성을 사용해서 다음과 같이 참고해도 되겠습니다. 이 속성은 메소드를 검색하기 위해 뒤져 봐야 하는 클래스 객체들의 목록을 순서대로 나열한 것입니다.

def searchprivate(obj, method):
    assert method.startswith('__')
    for cls in type(obj).__mro__:
        try: return getattr(obj, '_' + cls.__name__ + method)
        except AttributeError: pass
    return None

…더 이상 생각나는 게 없어서 쥐쥐.

5번

당장 생각하기에는 (ㄴ)이 가장 가능성이 없는 얘기 같습니다. 확신은 못 하겠고…

pymalloc의 작동 원리는 사실 간단합니다. 파이썬 객체들은 동적으로 할당되는 포인터들만 빼면 그다지 크지도 않은데 malloc의 부하(대부분의 malloc 구현은 아무리 작은 메모리라도 최소한 8바이트 내지 16바이트 정도의 공간을 내부 관리용으로 사용합니다.)를 참고 쓰기는 그렇죠. 그래서 같은 크기의 객체를 담을 공간을 적절히 많이 할당해 놓은 뒤, 필요할 때마다 그 메모리에 대한 포인터를 반환하는 식으로 할당을 처리하는 것입니다. 진실(?)을 말하자면, 이런 할당 정책은 pymalloc을 비롯한 많은 메모리 관리자들이 쓰고 있을 뿐만 아니라 몇몇 malloc 구현이 작은 메모리에 한해서 이렇게 하고 있기도 합니다. -_-;

따라서 한 객체만 생성해도 다른 비어 있는 공간(보통 arena라 부름)이 없으면 새로 큰 공간을 할당하게 되므로 (ㄱ)는 당연히 성립하고, 한 객체가 메모리를 해제한다 하더라도 arena에 다른 것들이 차 있으면 전체적으로는 메모리 해제가 불가능하니 (ㄷ)도 성립하겠습니다. (ㄹ)도 일말의 가능성은 있지만 그 특성상 한 객체만 생성해도 새로 큰 공간을 할당해야 할 때는 큰 비용이 필요할테니 (이래도 amortized cost는 일정합니다만) 일단 맞다고 칩시다.

(ㄴ)는 메모리가 샌다는 표현을 하고 있는데, 메모리가 새는 것은 메모리 관리자보다는 garbage collector의 책임이기 때문에 pymalloc의 문제라고 보기는 힘들 것 같습니다. 버그가 없다면 pymalloc은 적어도 아예 필요 없는 메모리를 해제 안 한 채 버리지는 않아야 합니다.

6번

당장 생각하기에는 (ㄹ)일 가능성이 큽니다. 다만 제가 파이썬 소스 코드를 잘 안 봐서 (ㄷ)일 수도 있습니다.

이 문제를 풀려면 파이썬의 기본 자료형들이 어떻게 구성되는지를 알아야 합니다. Fixnum만 특수한 처리를 하는 괴악한 방법을 쓰는 루비와는 달리, 파이썬은 모든 객체를 힙에 올려 버리되 너무 많이 쓰여서 닳아 없어질 것만 같은 객체들만 따로 캐싱을 해 두는 정책을 씁니다. 가장 흔한 예로 None 같은 내부 데이터형이나, -5부터 100까지(컴파일 설정에 따라 다를 수 있음)의 int 값들, 1바이트 문자열들 등이 포함됩니다.

따라서 (ㄱ)는 별도의 할당 없이 캐시된 객체를 반환하는 것으로 끝나고, (ㄴ)의 경우에도 몇몇 객체들은 캐시된 객체를 반환하게 되므로 답이 될 수 없습니다. (ㄷ)와 (ㄹ)에는 그러한 캐시 정책이 없습니다만, 제 생각에는 (ㄷ)도 작은 숫자(보통 int형으로 표현 가능할만한)에 대해서는 메모리를 덜 할당하는 정책을 쓰지 않을까 싶어서 (ㄹ)일 거라고 생각합니다.

7번

TabError입니다. 이게 만에 하나 나타난다면 파이썬 컴파일러가 아니라 파서가 틀린 겁니다. 각각의 예외에 대해 나타날 수 있는 가능성을 제시해 본다면:

  • NameError는 x나 y가 존재하지 않는 변수일 때 발생합니다.
  • OverflowError는 x나 y가 float이고, 그 결과가 너무 클 때 발생합니다. 다만 이 코드는 부동 소숫점 하드웨어 예외를 어떻게 설정했느냐에 따라서 그냥 inf를 반환할 수도 있습니다.
  • MemoryError는 x나 y가 모두 int/long이고, 그 결과가 너무 클 때 발생합니다. 하지만 그렇게 자주 볼 수 있는 건 아니고, 보통은 쓰래싱(thrashing)이 일어나다가 malloc이 뻗거나 하는 상황에만 나오는 것 같습니다.
  • TypeError는 x나 y 중 하나 이상이 수치형이 아닐 경우에 발생합니다.
  • RuntimeError는 언제 발생할 지 감은 못 잡겠습니다만, 많은 수의 예외가 RuntimeError로부터 상속받으니 뭐 발생할 수도 있겠죠 뭐. (무책임)
  • ZeroDivisionError는 x가 0이고 y가 음수이거나 복소수일 때 발생합니다.

TabError는 SyntaxError로부터 상속받아서, 코드의 들여쓰기에 탭이나 공백이 잘못 섞여 있을 때 나는 예외입니다. exec는 물론 이 예외를 낼 수 있습니다만 코드가 고정되어 있는 이상 그런 예외는 날 수가 없겠죠.

8번

warnings입니다. 다른 것들은 이리 저리 쓸 데가 있습니다:

  • sys.path는 모듈을 검색할 때 사용할 경로들의 목록입니다.
  • zipfile은 만약 sys.path에 zip 파일이 들어 있을 경우 그 안에 있는 모듈을 읽기 위해 사용합니다. py2exe 같은 것들이 이 기능을 자주 쓰죠.
  • imp는 모듈을 들이는 내부 과정을 구현한 모듈입니다.
  • marshal은 모듈을 들이고 코드 객체를 읽어 들이는데 사용합니다. pickle과 비슷한 역할입니다만 오로지(?) 파이썬의 내부 바이너리 포맷을 읽을 때만 쓰지요. 실제로 두 모듈은 지원하는 자료형들도 살짝 살짝 다릅니다.
  • sys.modules는 모듈을 읽기 전에 이미 그 모듈을 들여왔는지 확인하기 위해서 사용되는 사전입니다.
  • __future__(엄밀하게는 from __future__ import 문)는 여지껏 파이썬에서 모듈을 읽는 데 사용되진 않았습니다만, 비교적 최근에 상대/절대 모듈 들여오기를 위한 확장이 추가되었기 때문에 답은 아닙니다.

9번

가장 그럴듯한 것은 (ㄹ)입니다. 구현에 따라서는 (ㄴ)도 답이 될 수 있습니다.

a += a를 수행하면 자료형이 변해 버리는 (ㄱ)나 너무 길어서 어차피 재할당이 필요한 (ㄷ)는 답이 될 수 없고, (ㅁ)는 자료형이 변하지는 않지만 아쉽게도 파이썬에는 int.__iadd__ 메소드가 없기 때문에 새로운 객체가 할당되긴 마찬가지입니다.

(ㄹ)의 경우 a += a를 수행하면 a는 2가 되는데, 이 객체는 6번 답에서 설명했듯이 캐시되기 때문에 새로운 할당이 일어나지 않습니다. (ㄴ)는 보통 아닐 가능성이 높지만, 만약 파이썬 구현에서 리스트를 만들 때 처음으로 할당되는 크기가 최소 4라거나 하면 (STL 같은 일부 자료형 구현체들이 이런 정책을 사용합니다. 8인 경우도 어쩌다 봤습니다.) 재할당이 일어나지 않을 수도 있기 때문에 일단 가능성은 열어 놓았습니다.

10번

못 풀고 쥐쥐쳤습니다. -_- 누가 좀 풀어줘요…

전체 글 목록은 다음과 같다.

10번은 아마도 파이썬의 GIL(전역 인터프리터 락)과 관련된 문제였을 거라고 생각한다. (2010-04-03)

이 글은 본래 http://mearie.org/journal/2008/02/inside-python-quiz에 썼던 것을 옮겨 온 것입니다.


(rev 1d46270eb038)