메아리 저널

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

이전 글에 이어집니다. 뒷부분 열 문제인데 너무 많군요. 몇 문제는 시간이 없어서 풀다 만 것도 있으니 상상력으로 채워 보아요.

11번

가장 손쉽고 빠른 방법은 파이썬 2.5에 추가된 ctypes 모듈을 쓰는 것입니다.

import ctypes
ctypes.CFUNCTYPE(int)(0x12345678)()

물론 이러면 윈도에서는 WindowsError가 대신 나겠지만, 코드를 조금 꼬아 놓는다면 (예를 들어서 CreateThreadEx 따위에 이상한 함수 포인터를 걸어 놓는다거나) 큰 문제는 되지 않습니다. 근데 왜 WindowsError가 나냐고요? 윈도의 예외 처리(SEH) API는 세그폴트 난 것도 예외로 처리해서 아래로 내려 보내거든요. MS C++에서는 이 방법으로 세그폴트 추적도 가능합니다. (…)

조금 더 그럴듯한 방법은 파이썬의 내부 구조를 사용하는 것입니다. 그 중 가장 쓸만한 것이 코드 객체인데, 일단 코드 객체를 생성할 방법만 존재하면 파이썬 클라이언트를 죽이기 어렵지 않기 때문에 특히 인클봇-_- 같이 파이썬 코드를 외부에서 실행할 수 있는 프로그램에서는 주의해야 합니다.

import types
types.FunctionType(types.CodeType(0,1,0,0,'d\0S',(0,),(),(),'','',0,''),{})()

위 코드는 바이트코드가 사용할 스택의 크기를 일부러 작게 잡아 줘서 세그폴트를 내는 예입니다. 파이썬 바이트코드는 스택 기계를 기반으로 하는데, 코드 객체에는 이 기계에 있는 최대 스택의 크기를 지정하는 항목이 있습니다. 위에서는 적어도 한 칸의 스택 공간(LOAD_CONST 때문에)이 필요한 바이트코드를 스택 공간이 없이 실행해서 프로그램이 망해 버립니다. 그 외에 잘못된 바이트코드를 쓰는 방법도 가능하겠습니다.

마지막 방법은 스택을 강제로 오버플로우 나게 만드는 것입니다. 이것도 어렵진 않군요.

import sys
sys.setrecursionlimit(99999999)
(lambda f=(lambda f:f(f)):f(f))()

파이썬은 내부적으로 재귀 호출의 한도를 두고 있기 때문에 일부러 이 과정을 따라하려면 먼저 재귀 호출 한도를 높여야 합니다. 파이썬의 함수 호출은 시스템 스택을 소모하면서 일어나기 때문에 재귀가 깊으면 시스템 스택을 모두 써 버릴 수도 있거든요. 스택리스 파이썬과 같은 다른 구현체는 이런 문제가 없습니다.

번외편으로 모로 가도 파이썬만 죽이면 된다는 신념을 실행하는 다음 코드도 참고하면 좋습니다. 윈도에서 돌아가는지 모르겠습니다.

import os, signal
os.kill(os.getpid(), signal.SIGSEGV) # 세그폴트 난 척 하기

12번

(ㄹ), (ㅁ), (ㅅ)이며, 해당하는 메소드는 각각 __ne__, __le__, __or__입니다. (ㄱ)와 (ㅂ)도 __nonzero__ 메소드를 통해 동작을 바꿀 수 있긴 하지만 완전히 다른 연산자로 동작하게 할 수는 없습니다.

13번

당장 생각할 수 있는 방법은 __getattribute__를 오버라이딩해서 자식 클래스가 반환하는 묶인(bounded) 메소드를 덮어 씌우는 것입니다. 자식은 __getattribute__를 다시 오버라이딩하지 않는 한 이 동작을 바꿀 수 없습니다.

import types
class Base(object):
    def __getattribute__(self, name):
        if name == 'method':
            return types.MethodType(getattr(Base, name), self, Base)
        raise AttributeError
    def method(self, value=0):
        return 42 + value

메타클래스를 사용하는 방법도 생각해 봤는데 생각대로 잘 되지 않아서 일단 쥐쥐.

14번

(ㄹ)입니다. 특히 __del__ 메소드가 있는 클래스끼리 순환 참조가 일어날 경우 그렇습니다.

옛날 파이썬은 (ㄱ)에 해당하는 순환 참조도 제대로 처리하지 못 했습니다. 하지만 시간이 지나면서 정확한 순환 참조 체크 알고리즘이 구현되었고, 이제 대부분의 경우 파이썬은 정확히 garbage collection을 수행합니다. 하지만 만약 __del__ 메소드가 있는 클래스끼리 순환 참조가 일어나면, 파이썬은 순환 참조를 끊으려 하지만 어느 __del__을 먼저 수행해야 할 지 결정할 수 없기 때문에 암시적으로 참조를 끊지 않습니다. 대신 이 정보는 gc.garbage(아마도)에 저장되며, 프로그램은 명시적으로 순환 참조를 끊기 위해 이 리스트를 사용할 수 있습니다.

15번

  • x.__iadd__를 수행하는 도중에 import를 시도하다 실패했을 경우.
  • x.__iadd__가 없고, x.__add__를 수행하는 도중에 import를 시도하다 실패했을 경우.
  • x.__iadd__x.__add__를 얻기 위해 x.__getattr__을 수행하던 도중에 import를 시도하다 실패했을 경우.
  • x나 y가 coercion을 사용하고 (근데 이런 자료형은 별로 흔치 않습니다만) x.__coerce__를 수행하던 도중에 import를 시도하다 실패했을 경우.
  • 위에 언급한 모든 메소드들 중 하나가 import는 안 하고 일부러 raise ImportError를 냈을 경우. (-_-;)
  • ImportError가 아닌 다른 이유(보통은 NameError겠지만)로 예외가 발생한 뒤, 사용자 정의 sys.excepthook에서 이 예외를 출력하려다가 ImportError를 발생시킨 경우.
  • 대화형 인터프리터에서, sys.ps1이나 sys.ps2가 적절한 클래스로 설정되어 있고 어떠한 이유로 ImportError를 낼 경우.
  • 파이썬 디버거나 쓰레드와 같이 외부 요인으로 인한 경우.

…물론 위에 열거한 것들 중 실제로 발생할 만한 것은 사실 별로 없습니다.

16번

쥐쥐. orz

17번

  • x가 반복자를 가지고 있으며, 그 반복자가 바로 종료하지 않는 경우. 가장 가능성이 높습니다.
  • list가 리스트 생성자가 아닌 다른 함수로 바뀌어서 거짓이 아닌 값을 반환하는 경우. 두 번째로 가능성이 높으며 보통 버그의 온상이 됩니다.
  • list가 리스트 생성자가 아닌 다른 함수로 바뀌어서 그 안에서 프로그램을 종료해 버리는 경우.
  • x가 반복자를 가지고 있으며, 그 반복자가 수행되는 도중에 프로그램이 종료된 경우.
  • x의 반복자를 얻으려 __iter__ 메소드를 수행하던 도중에 프로그램이 종료된 경우.

18번

  • 당연하지만, 함수 x 안에서 TypeError를 발생시켰을 경우.
  • 함수 x에 딸린 데코레이터가 있고, 데코레이터가 반환한 함수(x처럼 보이긴 하지만)가 TypeError를 발생시켰을 경우.
  • x를 선언한 이래 x가 뭔가로 다시 덮어 씌워져서 호출 불가능한 값이 되었을 경우. 함수 이름이 x이면 그럴 법도 하죠.

…세 개 말고는 잘 모르겠습니다.

19번

  • __import__가 선언되어 있고, 그 안에서 AttributeError를 발생시켰을 경우.
  • __import__가 선언되어 있지만 listdir이 존재할 리가 없는 잘못된 모듈을 반환하였을 경우. (예를 들어 os.py라는 모듈이 있을 때 무작정 불러 올 경우)

마지막 한 가지는 잘 모르겠는데, 아마 파이썬의 내부 설정에 따라 달라질 수 있는 경우가 있을 것 같습니다. -.-

20번

  • x가 사실 int가 아니라 뭔가 다른 클래스라서 repr하면 '0'이 나오는데 __nonzero__는 True를 반환하는 경우.
  • 같은 상황이지만, x.__nonzero__가 수행되는 도중에 출력이 일어날 경우. (이 경우 이 메소드는 False를 반환해야 합니다)
  • print x를 실행한 시점에 Yay!가 이미 찍혀 있지만 버퍼가 아직 flush가 안 되어 있어서 화면에 보이지 않을 경우. (터미널이 이상하다거나 sys.stdout이 바뀌어 있다거나 bufsize가 바뀌었거나 등등) 이 경우 뒤에 뭔가 flush를 할 수 밖에 없게 만드는 코드가 더 있어야 겠지요.
  • 사실 숫자 0이 아닌 대문자 O일 지도 모릅니다. 이럴 때는 터미널 글꼴을 바꾸면 만사 오케이.

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


(rev 1d46270eb038)