6장. 객체 지향 자이썬

이번 장에서는 객체지향 프로그래밍의 기본적인 것들을 다룰 것이다. 우선 코드를 객체지향적으로 작성하는 이유를 알아보고, 기초적인 문법을 모두 파악한 다음, 심도 있는 예제를 살펴볼 것이다.

객체지향 프로그래밍이란 데이터와 기능을 하나로 묶어서 프로그래밍하는 기법을 말한다. 자이썬에서는 클래스 정의를 통해 이런 데이터와 기능을 묶어주는 틀을 정의할 수 있다. 이러한 첫 클래스가 만들어지면, 그 클래스의 인스턴스를 만들 수 있고, 그 인스턴스는 자기 자신에게 종속된 데이터(멤버 변수)를 포함한다. 또한 그 데이터에 기반한 특정 기능을 수행할 수 있는 메소드(멤버 함수)들도 포함한다. 이런 객체지향 프로그래밍 기법은 간결하면서 유지보수하기 쉬운 코드를 작성하는 데에 도움이 된다.

자이썬 2.5 판부터는 C로 만들어진 파이썬과 Java로 만들어진 자이썬 사이에 문법적인 차이가 거의 없다. 따라서, 비록 여기서는 자이썬만을 다루기는 하지만, 자이썬에서 돌아가는 코드들은 C 언어로 만든 파이썬에서도 똑같이 돌아간다고 보면 된다. 개요는 이 정도로 하고, 기초적인 문법에는 어떤 것들이 있는지 자세히 살펴보도록 하자.

기본 구문

클래스Class를 작성하는 것은 간단하다. 클래스는, 본질적으로 ‘상태state‘를 관리하면서, 상태를 조작하기 위한 몇 가지 함수를 외부에 노출시킨다. 그러한 함수를 객체지향의 용어로는 ‘메소드method‘라고 부른다. 먼저 Car 클래스로 시작해보자. 스스로의 위치를 나타내는 이차원 좌표를 관리하는 개체를 만드는 것을 목표로 하겠다. 그것에게 방향을 바꾸어서 전진하라고 할 수도 있고, 현재 위치가 어디인지 물어볼 수도 있도록 하고자 한다. 다음과 같은 코드를 ‘car.py’라는 이름의 파일로 저장하자.

예제 6-1.

class Car(object):

    NORTH = 0
    EAST = 1
    SOUTH = 2
    WEST = 3

    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y
        self.direction = self.NORTH

    def turn_right(self):
        self.direction += 1
        self.direction = self.direction % 4

    def turn_left(self):
        self.direction -= 1
        self.direction = self.direction % 4

    def move(self, distance):
        if self.direction == self.NORTH:
            self.y += distance
        elif self.direction == self.SOUTH:
            self.y -= distance
        elif self.direction == self.EAST:
            self.x += distance
        else:
            self.x -= distance

    def position(self):
        return (self.x, self.y)

클래스 정의에 대하여 상세하게 살펴보는 것은 나중으로 미루고, 우선은 자동차를 어떻게 생성하고, 이동시키고, 위치를 물어보는지부터 살펴보도록 하자.

예제 6-2.

from car import Car

def test_car():
    c = Car()
    c.turn_right()
    c.move(5)
    assert (5, 0) ==  c.position()

    c.turn_left()
    c.move(3)
    assert (5, 3) == c.position()

자이썬에는 ‘호출할 수 있는 것callable‘들이 있다. 호출할 수 있는 것 중 한 가지는 함수이며, 다른 한 가지로 클래스가 있다. 그러므로 클래스를 바라보는 한 가지 관점은, 그저 실체instance를 생성해낼 수 있는 특별한 부류의 함수라고 생각하는 것이다.

자동차의 실체를 일단 생성하면, 우리는 Car 클래스에 붙어있는 함수를 불러내는 것만으로 그 차의 위치를 관리할 수 있게 된다. 우리의 테스트 코드의 관점에서 살펴보면, 자동차의 위치를 일일이 관리할 필요가 없을 뿐만 아니라 자동차가 나아가는 방향도 챙길 필요가 없다. 우리는 그저 가라고만 하면, 차가 알아서 제 할 일을 한다. 여기서 정확히 어떤 일이 일어나는지 알아보기 위하여 구문을 더 자세히 살펴보도록 하자.

car.py의 1행에서, Car 개체는 최상위의 ‘object’ 클래스의 하위 클래스임을 정의하였다. 많은 객체 지향 언어들과 마찬가지로, 자이썬은 다른 모든 개체의 기초가 되는 ‘root’ 개체를 갖는다. 이 ‘object’ 클래스는 모든 클래스에서 재사용할 수 있는 기본적인 동작을 정의한다.

자이썬에서는 신·구의 두 가지 형태가 공존한다. 예전 방식에서는 ‘object’라고 칠 필요가 없었는데, 그런 식으로 작성된 자이썬 코드가 가끔 보이기는 하겠지만, 그리 좋은 방법은 아니다. 여러분의 기본 클래스들에 대하여 단지 ‘object’의 하위 클래스로 해두기만 하면 삶이 더욱 편해질 것이다.

3행에서 6행은 어떠한 자동차가 지향하는 방향에 대한 클래스 속성을 선언한다. 이것들은 클래스 속성이므로, Car 개체의 모든 개체 인스턴스 간에 공유가 가능하다. 클래스 속성은 개체 인스턴스를 생성하지 않더라도 참조될 수 있다.

지금부터가 좋은 것들이다.

8~11행은 개체 초기화initializer 메소드를 정의한다. 이 메소드는 개체가 생성되고 메모리가 할당된 직후에 호출된다. 다른 언어에서의 생성자constructor에 해당하는 것으로, 자이썬에는 생성 이후에 초기화를 한다. 자이썬에서의 유효한 메소드 이름은 C 언어 같은 것들에서와 유사하다. 일반적으로, 메소드 이름은 문자로 시작하며, 숫자를 꼭 쓰고 싶다면 그 뒷부분에 쓰도록 하되, 공백이 들어가서는 안된다.

자이썬 클래스는 특별한 “마법” 메소드도 갖추고 있다.이러한 메소드는 모두 밑줄 두 개로 시작하여 밑줄 두 개로 끝난다. 이 메소드들은 언어에서 예약되어 있으며 특별한 의미를 갖는다. 그러니까 여러분이 ‘Car()’를 통해 생성자를 호출하면 자이썬 런타임은 자동적으로 초기화 메소드 ‘__init__’을 호출한다. 클래스를 커스터마이즈할 수 있도록 해주는 또 다른 예약된 메소드 이름들에 대해서도 나중에 다룰 것이다.

우리의 초기화 메소드에서는, 차의 처음 위치를 2차원 평면 상의 (0, 0)으로 하며, 방향은 북쪽을 가리키도록 설정한다. 개체를 초기화할 때, 그 위치값은 명시적으로 전달하지 않아도 된다. 함수 서명signature은 자이썬의 기본 인자 리스트 기능을 사용하므로 우리는 명시적으로 초기 위치 (0,0)를 설정할 필요가 없다. 메소드의 디폴트 인자는 4 장에서 다룬 디폴트 함수 인자와 똑같이 동작한다. 메소드가 생성될 때 자이썬은 디폴트 값을 바인드하는데, 아무 것도 전달되지 않으면, 서명된 값이 사용된다. 또한 ‘self’라는 인자도 있다. 이것은 Car 개체의 현재의 개체에 대한 참조이다. 여러분이 다른 C 스타일의 언어에 익숙하다면, ‘this’ 참조를 호출해보았을 것이다.

클래스 정의는 객체의 인스턴스를 생성함에 유념하라. 개체가 만들어지면, 그것은 자체적으로 관리하는 내부 변수의 고유한 집합을 갖는다. 여러분의 개체는 클래스 내부 메소드 뿐만 아니라 이러한 것들에도 필연적으로 접근하게 될 것이다. 자이썬은 현재의 개체를 모든 인스턴스 메소드에 첫번째 인자로 전달한다.

다른 객체 지향 언어를 써보았다면, 아마도 ‘this’ 변수에 익숙할 것이다. C++이나 자바와는 달리, 자이썬은 접근할 수 있는 변수들의 이름 공간으로 참조를 마법처럼 소개하지는 않지만, 이는 명시적인 처리를 통하여 명료함을 얻고자하는 자이썬의 철학에 따른 것이다.

초기 x, y 위치를 지정하고자 한다면, 단지 개체에 있는 이름 ‘x’와 ‘y’에 값들을 할당하면 된다. x와 y의 값을 self에 바인딩함으로써 위치 값을 어느 코드에서나, 즉 그 개체의 다른 메소드에서도 접근할 수 있게 된다. 한 가지 세부사항으로, 자이썬에서는 여러분이 기술적으로 인자에 대한 이름을 지을 수 있다. 첫 인자를 ‘self’ 대신에 ‘this’라고 불러도 무방하지만, ‘self’라고 쓰는 것이 커뮤니티의 표준이다. 시인성과 스타일에 대한 커뮤니티의 표준은 자이썬의 강점 가운데 하나이다.

13행부터 19행은 차를 다른 방향으로 돌리는 두 개의 메소드를 선언한다. Car 개체를 호출하는 쪽에서 방향을 직접적으로 조작하지 않음에 주목하라. 우리는 그저 차가 방향을 틀기를 요청하고, 그러면 차는 스스로의 ‘direction’ 상태를 변경한다. 자이썬에서는, 밑줄 두 개를 써서 은닉private 속성을 지정할 수 있으며, self.direction을 self.__direction과 같이 바꿀 수 있다. 개체가 초기화되면, 여러분의 메소드는 밑줄 두 개를 사용하는 은닉 속성에 대한 접근을 계속하지만, 밖에서 호출하는 쪽에서는 그러한 내부의 속성에 대해서는 쉽사리 접근하지 못한다. 외부의 호출자에게는 속성 이름은 ‘obj._Car__direction’으로 보여질 것이다. 실용적으로는 은닉 속성을 사용하는 것을 권장하지 않는데, 이는 어떻게 코드를 작성하는 것이 안전한지 알 수 없기 때문이다. 다른 프로그래머를 위해 어떤 속성이 은닉된 것으로 간주된다는 힌트를 제공하고 싶다면 밑줄 하나를 사용할 수 있다.

21에서 29행은 우리가 차를 전진시킬 때 어디로 가야하는지를 정의한다. 내부의 direction 변수는 차에게 x와 y 값을 어떻게 조작해야할 지를 알려준다. Car 개체를 호출한 쪽에서는 차가 어느 방향을 향하고 있는지 정확히 알아야할 필요가 없다. 호출자는 단지 그 개체에게 방향을 바꾸고 전진하라고 말하기만 하면 된다. 그 메시지를 어떻게 다룰 것인지에 대한 세부적인 사항은 추상적인 방법으로 다루어진다.

수십 줄 정도의 코드에 대해서는 그리 나쁘지 않다.

이렇게 내부적인 세부사항을 숨기는 개념을 캡슐화encapsulation라고 한다. 이것은 객체 지향 프로그래밍의 핵심적인 개념이다. 이 단순한 예제를 통해서도 알 수 있듯이, 그것은 여러분의 코드를 구조화하여 코드의 사용자에게 단순화된 인터페이스를 제공할 수 있도록 해준다.

단순화된 인터페이스를 갖는다는 것은, 우리는 방향전환과 이동을 하기 위한 함수 호출의 이면에서 일어나는 모든 종류의 행위를 처리하지만, 호출하는 쪽에서는 모든 세부사항을 잊고 차를 관리하는 대신 사용하는 데에 집중할 수 있다는 뜻이다.

메소드의 본성은 바뀌지 않지만, 호출자는 그 어느 것에 대해서도 주의를 기울일 필요가 없어진다.

이제, 디스크를 이용하여 차의 상태를 저장하거나 불러낼 수 있도록 클래스 정의를 확장해보도록 하자. 여기서 목표는 클래스에 대한 기존의 인터페이스를 깨뜨리지 않으면서 그것을 추가하는 것이다.

첫번째로, pickle 모듈을 끌어내자. 피클을 사용하여 자이썬 개체를 바이트 문자열로 변환하였다가 나중에 개체로 되돌릴 수 있다.

pickle 가져오기

개체의 상태를 불러오고 저장하는 두 가지 새로운 메소드를 추가한다.

예제 6-3.

def save(self, filename):
    state = (self.direction, self.x, self.y)
    pickle.dump(state, open(filename,'wb'))

def load(self, filename):
    state = pickle.load(open(filename,'rb'))
    (self.direction, self.x, self.y) = state

turn과 move 메소드 끝부분에 save() 메소드 호출을 간단히 추가하면, 개체는 자동으로 모든 관련 내부 값을 디스크에 저장한다.

여기에 약간의 문제가 있는데, 우리는 각각의 자동차에 대한 다른 파일들이 필요하다. load와 save 메소드는 명시적인 파일 이름 인자들을 가지고 있지만 우리의 개체들 자체는 이름의 개념이 전혀없다. 개체에 바인딩된 이름을 항상 갖도록 초기화메소드를 수정하자. __init__을 변경하여 name 인자를 받아들이도록 한다.

예제 6-4.

def __init__(self, name, x=0, y=0):
    self.name = name
    self.x = x
    self.y = y
    self.direction = self.NORTH

Car 개체를 사용하는 사람들은 그것이 디스크에 저장된다는 것조차 알 필요가 없는데, 이는 car 개체가 보이지 않는 곳에서 그런 일을 처리하기 때문이다.

예제 6-5.

def turn_right(self):
    self.direction += 1
    self.direction = self.direction % 4
    self.save(self.name)

def turn_left(self):
    self.direction -= 1
    self.direction = self.direction % 4
    self.save(self.name)

def move(self, distance):
    if self.direction == self.NORTH:
        self.y += distance
    elif self.direction == self.SOUTH:
        self.y -= distance
    elif self.direction == self.EAST:
        self.x += distance
    else:
        self.x -= distance
    self.save(self.name)

이제, turn이나 move 메소드를 호출할 때 car는 자동적으로 디스크에 저장된다. 이전에 저장한 피클 파일에서 자동차 개체의 상태를 재구성하려면, 단순히 load() 메소드를 호출하여 자동차의 문자열 이름으로 전달할 수 있다.

객체 속성 조회

주의를 기울여왔다면, NORTH, SOUTH, EAST, 그리고 WEST 변수가 어떻게 self에 바인드되었는지 궁금할 수도 있을 것이다. 개체를 초기화하는 동안에 그것들을 self 변수에 할당한 적이 없는데, 대체 move()를 호출할 때 무슨 일이 벌어진 것일까? 자이썬은 어떻게 네 변수의 값을 구하는 것일까?

자이썬이 이름 조회를 어떻게 해결하는지 설명하기에 적당한 때가 온 것 같다.

방향의 이름은 실제로는 car 클래스에 바인드된다. 자이썬에서는 여러분이 어떠한 개체의 이름에 접근하려고 할 때에 약간의 마법을 부려서, ‘self’에 바인드된 것을 먼저 찾는다. 만약에 self에서 그 이름에 해당하는 속성을 찾을 수 없으면, 클래스 정의에 대한 객체 그래프로 넘어간다. 방향 속성 NORTH, SOUTH, EAST, WEST는 클래스 정의에 바인드되었으므로, 이름 찾기는 성공하며 클래스 속성의 값을 얻을 수 있다.

다음의 짤막한 예제가 이것을 명확히 하는 데 도움이 될 것이다.

예제 6-6.

>>> class Foobar(object):
...     def __init__(self):
...         self.somevar = 42
...     class_attr = 99
...
>>>
>>> obj = Foobar()
>>> obj.somevar
42
>>> obj.class_attr
99
>>> obj.not_there
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Foobar' object has no attribute 'not_there'
>>>

여기서 주요한 차이점은 어느 값에 바인드하느냐이다. self에 바인드하는 값들은 단일 개체에 대해서만 사용가능하다. 클래스 정의에 바인드하는 값들은 클래스의 모든 인스턴스에 대하여 사용가능하다. 모든 인스턴스 사이에서 클래스 속성을 공유하는 것은 크리티컬한 차이점인데, 이는 클래스 속성에 변경을 가하면 모든 인스턴스에 영향을 미치기 때문이다. 이는 주의를 기울이지 않으면 변수 값에 예기치 않은 변경이 일어날 수 있으므로 의도하지 않은 부작용을 일으킬 수 있다.

예제 6-7.

>>> other = Foobar()
>>> other.somevar
42
>>> other.class_attr
99
>>> # obj와 other는 각각 다른 값의 somevar를 가질 수 있다.
>>> obj.somevar = 77
>>> obj.somevar
77
>>> other.somevar
42
>>> # other.class_attr에 할당하면, 다른 것의 인스턴스 속성이 class_attr을 호출하게 만든다.
>>> other.class_attr = 66
>>> other.class_attr
66
>>> # 그리고, 다른 개체를 위하여 class_attribute class_attr을 변경하지 않는다.
>>> obj.class_attr
99
>>> # other.__class__.class_attr를 바라봄으로써 여전히 클래스 속성에서 얻을 수 있다.
>>> other.__class__.class_attr
99
>>> # 그리고 인스턴스 속성 other.class_attr을 제거하면,
>>> other.class_attr은 클래스 속성을 참조하도록 되돌아간다.
>>> del other.class_attr

>>> other.class_attr
99

>>> # 그러나 class_attribute가 변경가능할 경우, 변경을 하게 되면, 모든 인스턴스에 대하여 변경을 하게 된다
>>> Foobar.class_list = []

>>> obj.class_list
[]

>>> other.class_list
[]

>>> obj.class_list.append(1)

>>> obj.class_list
[1]

>>> other.class_list
[1]

우리는 자이썬의 개체 시스템이 실제로 얼마나 투명한지 강조하는 것을 중요시한다. 개체 속성은 단지 일반 자이썬 사전에 저장된다. 직접 __dict__ 속성​을 보고 사전에 접근할 수 있다.

예제 6-8.

>>> obj = Foobar()
>>> obj.__dict__
{'somevar': 42}

클래스의 메소드(이 경우, 우리의 초기화 메소드initializer ) 또는 class 속성 ‘class_attr’ 에 대한 어떠한 참조가 없는 것을 주목하라. __dict__는 개체의 지역 변수와 메소드를 보여준다. 우리는 상속을 짧게 다룰것이며, 서브클래싱subclassing을 통하여 클래스들을 특정짓는 예제에서 속성과 메소드가 보여지는 법을 보게 될 것이다. 클래스의 모든 속성을 검사하는 데에 동일한 트릭을 적용할 수 있다. 클래스 정의의 __dict__ 속성을 들여다보면 클래스 정의 및 여러분의 클래스 정의에 붙어있는 모든 메소드를 찾을 수 있을 것이다.

예제 6-9.

>>> Foobar.__dict__
{'__module__': '__main__',
    'class_attr': 99,
    '__dict__': <attribute '__dict__' of 'Foobar' objects>,
    '__init__': <function __init__ at 1>}

이러한 투명성은 클로져와 새로운 함수를 바인딩하는 동적 프로그래밍 기법을 통하여 잘 활용할 수 있다. 이에 대하여는 함수를 동적으로 생성하는 것을 살펴볼 때에 다시 설명할 것이며 마지막으로 메타프로그래밍에 대하여 짧게 소개할 것이다.

상속과 오버로딩

자동차 예제에서, 우리는 root 개체 유형으로부터 하위 클래스를 만들었다. 마찬가지로 여러분의 클래스를 subclass하여 개체의 행위를 특별하게 만들 수 있다. 여러 다른 클래스들 사이에 공통적인 습성을 갖고 있다는 것을 깨닫게 된다면 바로 그런 것을 바라게 될 것이다. 개체를 가지고, 여러분은 하나의 클래스를 작성하고, 부모 클래스에 이미 존재하는 기능 및 속성에 대하여 자동적으로 접근을 얻기 위하여 상속을 사용하여 재사용할 수 있다. 여러분의 ‘기본’ 개체는 근원이 되는 ‘object’ 클래스로부터 속성을 물려받을 것이지만, 어떠한 subsequent 하위 클래스든지 여러분의 클래스로부터 상속할 것이다.

이것이 어떻게 작동하는지 알아보기 위하여 동물 클래스를 사용하는 간단한 예제를 살펴보도록 하자. 다음 코드와 같이 ‘animals.py’를 정의하자.

예제 6-10.

class Animal(object):
    def sound(self):
        return "I don't make any sounds"
class Goat(Animal):
    def sound(self):
        return "Bleeattt!"
class Rabbit(Animal):
    def jump(self):
        return "hippity hop hippity hop"
class Jackalope(Goat, Rabbit):
    pass

이제 자이썬 인터프리터로 해당 모듈을 탐험할 수 있을 것이다.

예제 6-11.

>>> from animals import *
>>> animal = Animal()
>>> goat = Goat()
>>> rabbit = Rabbit()
>>> jack = Jackalope()
>>> animal.sound()
"I don't make any sounds"
>>> animal.jump()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Animal' object has no attribute 'jump'
>>> rabbit.sound()
"I don't make any sounds"
>>> rabbit.jump()
'hippity hop hippity hop'
>>> goat.sound()
'Bleeattt!'
>>> goat.jump()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Goat' object has no attribute 'jump'
>>> jack.jump()
'hippity hop hippity hop'
>>> jack.sound()
'Bleeattt!'

상속은 매우 단순한 개념으로, 새로운 클래스를 선언할 때, 재사용하고자 하는 부모 클래스를 지정하는 것이다. 그렇게 해서 새로 만든 클래스는 자동적으로 슈퍼 클래스의 메소드와 속성에 접근할 수 있게 된다. 위의 예제에서, 염소Goat 개체는 jump 메소드를 갖고 있지 않고 상위 클래스인 Animal에도 jump 메소드가 없기 때문에, jump() 메소드의 호출은 실패한다. rabbit에 대하여 sound 메소드를 호출하면 슈퍼 클래스의 sound 메소드가 실제로 호출된다.

이와 같이, 로컬 개체 인스턴스 내에서 속성을 찾을 수 없으면, 상속 트리를 거슬러 올라가 슈퍼 클래스에서 찾게 된다는 것이 바로 핵심적인 아이디어이다. 토끼사슴Jackalope은 두 개의 슈퍼 클래스에서 메소드를 찾을 수 있기 때문에, 토끼와 염소 양쪽의 메소드에 접근할 수 있게 된 것에 유의하기 바란다.

하나의 상위 클래스로부터 상속받는 단일 상속에서, 속성 또는 메소드를 어디에서 찾아야 해결하기 위한 규칙은 매우 간단하다. 사용하는 개체가 일치하는 속성를 가지고 있지 않다면, Jython은 부모 클래스에서 찾는다.

Rabbit 클래스는 Animal 클래스의 한 형태이다. isinstance 함수를 사용하면 Jython 런타임은 프로그램의 수행 결과로서 그러한 사실을 알려준다.

예제 6-12.

>>> isinstance(bunny, Rabbit)
True
>>> isinstance(bunny, Animal)
True
>>> isinstance(bunny, Goat)
False

클래스를 만들 때, 특정 기능을 재작성overriding하기보다는, 상위 클래스에서 제공하는 기능을 확장하는 것이 나은 경우가 많을 것이다. 그럴때는 super() 함수를 사용하면 된다. 아래와 같이 Rabbit 클래스를 특화시켜보자.

예제 6-13.

class EasterBunny(Rabbit):
    def sound(self):
        orig = super(EasterBunny, self).sound()
        return "%s - but I have eggs!" % orig

이 토끼가 말하게 하려면, 기본 Rabbit 클래스로부터 원래의 sound() 메소드를 확장하도록 한다. super() 함수를 호출하면 상위 클래스의 sound 메소드 구현에 접근할 수 있다. 이 예제는, EasterBunny 클래스가 기본 Rabbit 클래스의 sound() 메소드를 재사용하고 있고, 확장하고 있기 때문에 유용하다.

예제 6-14.

>>> bunny = EasterBunny()
>>> bunny.sound()
"I don't make any sounds - but I have eggs!"

그리 나쁘진 않다. 이런 예들에서 우리는 단지 상속된 메소드들이 호출될 수 있다는 것을 설명했지만, self로 바인딩되는 속성과 정확히 같은 것을 할 수 있다.

다중 상속은 문제가 좀 복잡하다. 자이썬은 속성 조회를 해결하기 위해 “왼쪽 우선, 깊이 우선” 검색을 사용한다. 간단히 말하자면, 만약 상속 다이어그램을 그린다고 하면, Jython은 왼쪽에서 오른쪽으로 아래에서 위로 가면서 속성을 검색하는 그래프의 왼쪽 측면을 내려다볼 것이다. 어떤 상위 클래스가 2개 이상의 하위 클래스로 상속되는 경우에는, 상위 클래스는 하위 클래스에서 모든 속성 검색이 모두 이루어진 이후에 조회된다.

밑줄 메소드

평범한 클래스를 사용한 추상화는 놀라운 것이기는 하지만, 만약 여러분의 코드가 언어의 구문에 자연스럽게 녹아든다면 더욱 좋을 것이다. 자이썬에서는 시작과 끝에 ‘_’ 기호가 붙은 여러 가지 밑줄 메소드를 제공하여, 여러분의 개체의 습성을 오버로드할 수 있도록 해준다. 이것은 개체가 언어 자체와 보다 긴밀하게 통합된다는 것을 의미한다. __init__ 메소드는 이미 본 적이 있을 것이다.

밑줄 메소드를 가지고 논리적이고 수학 연산에 대한 개체의 행동을 제공할 수 있다.심지어 객체가 더 많은 목록, 집합 또는 사전과 같은 표준적으로 내장된 유형처럼 행동하게 만들 수 있다. 이런 경우의 가장 간단한 예를 보기 위해 SimpleObject에 유니코드 확장을 추가하는 예를 먼저 살펴보자. 그런 다음에 맞춤형 컨테이너 클래스 만들기로 넘어가도록 하겠다.

예제 6-15.

from __future__ import with_statement
from contextlib import closing
with closing(open('simplefile','w')) as fout:
    fout.writelines(["blah"])
with closing(open('simplefile','r')) as fin:
    print fin.readlines()

위의 코드는 그저 파일을 열고, 텍스트를 조금 기록한 다음, 내용물을 읽어낸다. 그리 대단할 것은 없다. 자이썬에서는 대부분의 개체가 피클pickle 모듈을 사용하여 문자열로 직렬화된다. 피클 모듈은 살아있는 자이썬 개체를 바이트의 흐름stream으로 변환하며, 이를 디스크에 저장하였다가 개체로 되살려낼 수 있다. 동작하는 버전은 아래와 같다.

예제 6-16.

from __future__ import with_statement
from contextlib import closing
from pickle import dumps, loads

def write_object(fout, obj):
    data = dumps(obj)
    fout.write("%020d" % len(data))
    fout.write(data)

def read_object(fin):
    length = int(fin.read(20))
    obj = loads(fin.read(length))
    return obj

class Simple(object):
    def __init__(self, value):
        self.value = value
    def __unicode__(self):
        return "Simple[%s]" % self.value

with closing(open('simplefile','wb')) as fout:
    for i in range(10):
        obj = Simple(i)
        write_object(fout, obj)

print "Loading objects from disk!"
print '=' * 20

with closing(open('simplefile','rb')) as fin:
    for i in range(10):
        print read_object(fin)

이것은 다음과 같은 출력을 만들어낼 것이다.

예제 6-17.

Loading objects from disk!
====================
Simple[0]
Simple[1]
Simple[2]
Simple[3]
Simple[4]
Simple[5]
Simple[6]
Simple[7]
Simple[8]
Simple[9]

이제 우리는 재미있는 것을 하고 있다. 여기서 정확히 무슨 일이 일어나는지 살펴 보자.

먼저, Simple 개체가 멋지게 렌더링되었음을 알게 될 것이다. Simple 개체는 __unicode__ 메소드를 사용하여 스스로를 렌더할 수 있다. 각괄호와 헥스 코드를 사용하던 이전의 개체 렌더링에 비하여 명백히 진보를 이룬 것이다.

write_object 함수는 매우 간단하다, 그냥 피클 모듈을 사용하여 우리의 객체들을 문자열들로 변환하고있고, 문자열의 길이를 계산하며, 길이와 실제 디스크로 직렬화 되는 개체를 쓰고 있다.

괜찮기는 하지만, 읽는 측면은 좀 투박하다. 우리는 언제 읽기가 중단되는지 알지 못한다. 반복 프로토콜을 사용하면 이 문제를 해결할 수 있다. 필자가 자이썬의 개체를 즐겨 사용하는 이유이기도 하다.

규약

자이썬에는 ‘덕 타이핑duck typing‘이라는 것이 있는데, “오리처럼 꽥꽥거리며 걷고 겉모습도 오리처럼 생긴 것이 있다면, 그것은 바로 오리”라는 말로 설명하곤 한다. 이것은 C#이나 자바와 같이 형식적인 인터페이스 정의를 가진, 보다 엄격한 언어와 비교할 때 극명한 차이를 보이는 부분이다. 덕 타이핑의 장점 중 하나는 자이썬은 객체 규약protocol의 개념을 가지고 있다는 것이다.

메소드를 올바로 구현하였다면, 자이썬은 여러분의 개체를 특정한 유형의 ‘것’으로 인식하게 된다.

반복자iterator는 목록처럼 생긴 것으로, 다음 번에 오는 개체를 읽을 수 있도록 해준다. 반복자 규약의 구현은 직관적이다. 그저 next() 메소드와 __iter__ 메소드를 구현하면 준비가 된 것이다. 실제 예제를 보자.

예제 6-18.

class PickleStream(object):
    """
    이 스트림은 스트림 개체를 날것의 파일 스트림으로 떨어뜨리는 데에 사용할 수 있다.
    """
    def __init__(self, file):
        self.file = file

    def write(self, obj):
        data = dumps(obj)
        length = len(data)
        self.file.write("%020d" % length)
        self.file.write(data)

    def __iter__(self):
        return self

    def next(self):
        data = self.file.read(20)
        if len(data) == 0:
            raise StopIteration
        length = int(data)
        return loads(self.file.read(length))

    def close(self):
        self.file.close()

이 클래스는 간단한 파일 개체를 감싸서, 날것의 자이썬 개체에 보내어 파일에 기록한다든지, 스트림이 그저 개체의 목록인 것처럼 개체를 읽을 수 있도록 해준다. 이로써 읽고 쓰는 것이 훨씬 간단해진다.

예제 6-19.

with closing(PickleStream(open('simplefile','wb'))) as stream:
    for i in range(10):
        obj = Simple(i)
        stream.write(obj)

with closing(PickleStream(open('simplefile','rb'))) as stream:
    for obj in stream:
        print obj

직렬화에 대한 세부사항을 추상화한 PickleStream을 통하여 우리는 디스크에 기록하는 것에 대해서는 ‘잊어버릴’ 수 있다. 우리는 write() 메소드를 호출하였을 때에 개체가 올바로 일을 하는지만 신경쓰면 된다.

반복자 프로토콜은, 심지어 이 예제와 함께도, 훨씬 더 진보적인 용도로 사용할 수도 있지만 그것의 유용성에 대하여 분명히 해둘 필요가 있다. read() 메소드를 가지고 읽기를 구현할 수 있으므로, 스트림은 그저 루핑에 사용할 수 있는 대상으로 여기면 이해하기 쉽다.

한 걸음 뒤로 물러나 다른 밑줄 메소드를 살펴보도록 하자. 밑줄 메소드의 가장 일반적인 용도 두 가지는 프록시를 구현하는 것과 컨테이너와 유사한 자신만의 클래스를 구현하는 것이다. 프록시는 많은 프로그래밍 문제에서 매우 유용하다. 호출자와 피호출자 간의 중개자로서 프록시를 사용할 수 있다. 프록시 클래스는 추가적인 기능을, 호출자에 투명한 방식으로 추가할 수 있다. 자이썬에서, 메소드나 속성이 존재하지 않는 것 같아보인다면, 속성 조회를 구현하기 위하여 __getattr__ 메소드를 사용할 수 있다.

예제 6-20.

class SimpleProxy(object):
    def  __init__(self, parent):
        self._parent = parent

    def __getattr__(self, key):
        return getattr(self._parent, key)

이것은 가장 단순하고 (그다지 쓸모는 없는) 프록시이다. SimpleProxy에 없는 속성에 대하여 조회하게 되면 자동적으로 __getattr__ 메소드를 호출하여, 조회를 부모 개체에 위임하게 된다. 참고로, 이러한 동작은 밑줄을 붙이지 않은 속성에 해당하는 것이다. 좀 더 명확하게 하기 위해 간단한 예를 살펴 보자.

예제 6-21.

>>> class TownCrier(object):
...     def __init__(self, parent):
...         self._parent = parent
...     def __getattr__(self, key):
...         print "Accessing : [%s]" % key
...         return getattr(self._parent, key)
...
>>> class Calc(object):
...     def add(self, x, y):
...         return x + y
...     def sub(self, x, y):
...         return x – y
...
>>> calc = Calc()
>>> crier = TownCrier(calc)
>>> crier.add(5,6)
Accessing : [add]
11
>>> crier.sub(3,6)
Accessing : [sub]
-3

여기서 우리는 TownCrier 클래스가 메소드가 호출될 때마다 Calculator 개체에 통제를 위임하되, 디버그 메시지를 추가하고 있음을 알 수 있다. 자바와 같은 언어에서는 프록시를 생성하기 위해 특정 인터페이스를 구현해야만 하는 - 이미 프록시가 있더라도 그렇게 해야 한다 - 것과 달리 자이썬에서는 자유롭게 프록시를 만들 수 있다. 일반적인 조회 방식을 사용하는 속성 조회가 실패하면 __getattr__ 메소드가 자동으로 호출된다. 프록시는 위임delegation 패턴을 통하여 새로운 기능을 집어넣을 수 있도록 해준다. 여기서 좋은 점은 위임받는 쪽의 구현에 대하여 알 필요 없이 새로운 기능을 추가할 수 있다는 점이다. 상속을 통해서는 이러한 것을 구현할 수 없을 것이다.

밑줄 메소드의 또 다른 용법으로는, 자신만의 컨테이너 클래스를 구현하는 것을 들 수 있다. 작은 사전 비슷한 클래스를 구현하는 것을 살펴보도록 하자. 일반적인 사전처럼 동작하되, 모든 읽기 접근을 파일에 기록하는 클래스가 있다고 하자. 사전의 기본적인 기능들, 즉 키/값 쌍에 대한 get, set 및 delete, 키가 존재하는지에 대한 검사, 사전의 레코드 갯수 세기와 같은 것들이 필요하다. 그러한 기능을 모두 얻기 위해서는 다음과 같은 메소드들을 구현해야 한다.

예제 6-22.

__getitem__(self, key)
__setitem__(self, key, value)
__delitem__(self, key)
__contains__(self, item)
__len__(self)

메소드의 이름만 보아도 어떤 것인지 쉽게 짐작할 수 있을 것이다. __gettiem__, __setitem__ 및 __delitem__은 모두 사전에서의 키/값 쌍을 조작한다. 손쉬운 방법으로, 일반적인 목록 개체 위에 쌓아올려서 구현할 수 있다. ‘foo.py’ 파일을 다음과 같이 작성하자.

예제 6-23.

class  SimpleDict(object):
    def __init__(self):
        self._items = []

    def __getitem__(self, key):
       # 키를 억지 검색하여 값을 반환
        for k, v in self._items:
            if k == key:
                return v
        raise LookupError, "can't find key: [%s]" % key

    def __setitem__(self, key, value):
        # 키가 존재하는 경우 억지 검색 및 치환을 수행한다.
        # 그렇지 않은 경우에는, 새로운 키/값 쌍을 추가한다.
        for i, (k , v) in enumerate(self._items):
            if k == key:
                self._items[i][1] = v
                return
        self._items.append((key, value))

    def __delitem__(self, key):
        # 억지 검색 및 삭제
        for i, (k , v) in enumerate(self._items):
            if k == key:
                del self._items[i]
                return
        raise LookupError, "Can't find [%s] to delete" % key

앞서 나열한 구현이 엉성하기는 하지만, 이를 통하여 사용법의 기본 패턴을 살펴볼 수 있을 것이다. 일단 세 개의 메소드를 구현하면, 사전 형태의 속성 접근을 할 수 있게 된다.

예제 6-24.

>>> from foo import *
>>> x = SimpleDict()
>>> x[0] = 5
>>> x[15]= 32
>>> print x[0]
5
>>> print x[15]
32

그 외에 key의 존재여부와 사전의 크기를 얻기 위하여, __contains__와 __len__ 메소드를 작성한다.

예제 6-25.

def __contains__(self, key):
    return key in [k for (k, v) in self._items]

def __len__(self):
    return len(self._items)

이렇게 구현한 사전은 이제 표준적인 사전과 거의 동일하게 동작할 것이며, items(), keys() 및 values()와 같은 몇몇 ‘일반적’ 메소드가 없기는 하지만, 대괄호를 사용하여 SimpleDict에 접근하는 것은 사전과 마찬가지로 동작할 것이다. 이 구현은 의도적으로 단순하게 만들었으므로, 우리의 아이템들을 텍스트 파일, 데이터베이스 뒷단backend 또는 기타 저장 창고에 저장할 수 있었음을 쉽게 이해할 것이다. 그것들과 상호 작용하는 것은 오로지 사전 인터페이스이기 때문에, 호출자는 이러한 변화를 눈치챌 수 없을 것이다.

디폴트 인자

메소드 signature에서의 디폴트 값을 사용하는 것은, 모든 자이썬 프로그래머를 난감하게 만든다.

예제 6-26.

>>> class Tricky(object):
...     def mutate(self, x=[]):
...         x.append(1)
...         return x
...
>>> obj = Tricky()
>>> obj.mutate()
[1]
>>> obj.mutate()
[1, 1]
>>> obj.mutate()
[1, 1, 1]

여기서 일어나는 것은 인스턴스 메소드 ‘mutate’가 하나의 개체라는 것이다. 이 메소드 개체는 속성 내의 ‘x’에 대한 디폴트 값을 메소드 개체 내부에 저장한다. 더욱 복잡하게도, 메소드 객체는 클래스 정의에 바인딩된다. 따라서 목록을 변경하면, 그 메소드 자체의 속성 값을 실제로 변경하게 된다. 개체 인스턴스 각각은 동일한 클래스 정의와 동일한 메소드를 가리킨다. 디폴트 인자는 모든 인스턴스에서 변경될 것이다!

메소드의 실행시간 바인딩

자이썬의 한 가지 흥미로운 특징은, 인스턴스 메소드는 실제로는 단지 클래스 정의에 걸려있는 속성이라는 것이다. 함수는 ‘호출할 수 있다’는 점을 제외하면 여느 다른 변수와 같은 속성에 불과하다.

심지어는 인스턴스 메소드들을 생성하기 위하여 실행시간에 new 모듈을 사용하여 함수를 생성하고 클래스 정의에 바인드하는 것도 가능하다. 아무 것도 갖지 않는 클래스를 정의해놓고, 실행시간에 클래스 정의에 메소드를 바인드하는 것이 실제로 가능하다. 예제를 통하여 확인해보자.

예제 6-27.

>>> def some_func(self, x, y):
...     print "I'm in object: %s" % self
...     return x * y
...
>>> import new
>>> class Foo(object): pass
...
>>> f = Foo()
>>> f
<__main__.Foo object at 0x1>
>>> Foo.mymethod = new.instancemethod(some_func, f, Foo)
>>> f.mymethod(6,3)
I'm in object: <__main__.Foo object at 0x1>
18

mymethod 메소드를 호출하면, 동일한 속성 조회 메커니즘이 호출된다. 자이썬은 그 이름을 ‘self’ 개체에서 찾는다. 아무 것도 찾을 수 없으면, 클래스 정의로 이동한다. 거기에서 이름을 찾으면 instancemethod 개체가 반환된다. 함수는 두 개의 인수로 호출되며, 우리는 최종적인 결과를 볼 수 있게 된다.

특수한 함수인 new.instancemethod는 some_func이 호출되면 마법을 부려서, 자이썬 런타임이 자동으로 개체 인스턴스를 첫번째 인자로 전달하게끔 해준다. 그것이 우리가 앞에서 보았던 self 속성이다. 이러한 방식으로 개체에 묶여있는bound 함수를 ‘바운드 메소드’라고 부른다. 이러한 바인딩 기능이 없으면, 개체 인스턴스는 첫번째 인자로서 전달되지 않는다. 이 경우에는, 메소드를 “언바운드 메소드”라고 부른다.

자이썬이 가진 이러한 종류의 동적인 경향은 매우 강력하다. 프로그램 실행 시간에 함수를 생성하여, 그러한 함수를 개체에 바인드하는 코드를 작성할 수 있다. 이러한 모든 것이 가능한 이유는, 자이썬에서 클래스와 함수는 “일급 개체”이기 때문이다. 클래스 정의 자체는 다른 개체들과 마찬가지의 실제 개체이다. 클래스를 조작하는 것은 어느 다른 객체를 조작하는 것만 큼이나 간단하다.

이러한 기법의 실용적인 예는 코드를 생성하는 도구를 구축하는 것에서 찾아볼 수 있다. 함수와 메소드를 정적으로 생성하는 대신에, 여러분의 개체가 실행시간에 갖는 기능에 의하여 메소드가 ‘자라나도록’ 할 수 있다. 이것이 대부분의 파이썬 데이터베이스 도구가 하는 일이다. 데이터베이스에서 개체를 표현하는 클래스를 정의하고, 도구모음은 여러분의 개체를 조사하여 클래스의 지속성을 향상시킨다. 실행시간에 새로운 메소드를 생성하는 것과 같은 동적 프로그래밍 기법을 사용하는 것은, 클래스에 대한 사후처리의 가능성을 열어준다.

속성 접근을 캐시하기

계산 집약적인 메소드를 필요로 하지만, 시간이 지남에 따른 결과의 차이는 크지 않다고 가정하자. 매번 계산을 수행하지 않도록 결과를 캐시할 수 있다면 좋지 않을까? 4 장에서 다룬 장식자 패턴을 활용하여 계산 결과를 우리 개체의 새로운 속성으로 작성할 수 있다.

다음은 느린 계산을 수행하는 메소드를 갖는 클래스이다. slow_compute() 메소드는 딱히 하는 일이 없이 잠자면서 1초를 소모한다. caching 장식자로 메소드를 감쌈으로써, 메소드를 호출할 때마다 1초씩 기다리지 않도록 하려고 한다.

예제 6-28.

import time
class Foobar(object):
    def slow_compute(self, *args, **kwargs):
        time.sleep(1)
        return args, kwargs, 42

장식자 함수를 사용하여 값을 캐시해보도록 하자. 우리의 전략은, 인자 목록을 갖는 X라는 이름의 함수에 대하여 고유한 이름을 생성하고 그 이름에 대한 최종적인 계산 결과값을 저장하는 것이다. 캐시된 값에 대하여 사람이 읽을 수 있는 이름을 붙이되, 첫번째로 전달된 함수 이름과 함께 원래의 함수 이름을 재사용하려고 한다. 코드를 살펴보자!

예제 6-29.

import hashlib
def cache(func):
    """
    이 장식자는 캐시된 값을 저장하기 위하여 인스턴스 메소드의
    첫번째 호출 이후에_cache_functionName_HEXDIGEST 속성을
    추가할 것이다.
    """
    # 함수의 이름을 얻음
    func_name = func.func_name
    # 이름이 없거나 이름을 가진 인자에 대한 고유값을 계산
    arghash = hashlib.sha1(str(args) + str(kwargs)).hexdigest()
    cache_name = '_cache_%s_%s' % (func_name, arghash)

    def inner(self, *args, **kwargs):
        if hasattr(self, cache_name):
            # 캐시된 값이 있을 경우에는 그것을 사용
            print "Fetching cached value from : %s" % cache_name
            return getattr(self, cache_name)
        result = func(self, *args, **kwargs)
        setattr(self, cache_name, result)
        return result
    return inner

이 코드에는 오직 두 가지 새로운 트릭이 있다.

  1. 함수의 인자를 고유한 단일 문자열로 변환하기 위하여 hashlib 모듈을 사용한다.
  2. 인스턴스 개체 상의 캐시된 값을 조작하기 위하여 getattr, hasattr, 및 setattr를 사용한다.

세 함수 getattr, setattr, 그리고 hasattr는 개체에 있는 속성을 기호가 아닌 문자열 이름을 사용하여 얻고, 설정하고, 테스트할 수 있도록 해준다. 즉, foo.bar에 접근하는 것은 getattr(foo, ‘bar’)를 호출하는 것과 동등하다. 앞의 예에서는, 속성 함수를 느린 계산 함수의 결과를 Foobar의 인스턴스의 속성에 바인드하는 데에 사용하였다.

다음 번에는 장식된 메소드가 호출되어, hasattr 테스트가 캐시된 값을 찾아서 사전에 계산된 값을 반환한다.

자, slow 메소드를 캐시하려면 메소드 선언의 윗 줄에 @cache 행을 던져놓으면 된다.

예제 6-30.

@cache
def slow_compute(self, *args, **kwargs):
    time.sleep(1)
    return args, kwargs, 42

환상적이다! 우리는 이 cache 장식자를 원한다면 어느 메소드에나 재사용할 수 있게 되었다. 캐시를 매 N번 호출마다 무효화하고자 한다고 가정하자. currying의 실용적인 사용은 원래의 캐싱 코드에 사소한 수정을 가하는 것이다. 목표는 동일하다. 메소드에서 계산된 결과를 개체의 속성으로서 저장할 것이다. 속성의 이름은 실제의 함수 이름을 기초로 하여 정해지며, 메소드에 들어오는 인자를 사용하여 계산된 해시 문자열을 써서 이어붙이게 된다.

예제 코드에서, 함수 이름은 변수명 ‘func_name’으로, 인자 해시 값은 ‘arghash’로 저장한다.

그 두 변수는 카운터 속성의 이름을 계산하는 데 사용된다. 계수기가 N에 도달하면 미리 계산된 값을 지움으로써 다시 계산이 이루어질 수 있도록 한다.

예제 6-31.

import hashlib
def cache(loop_iter):
    def function_closure(func):
        func_name = func.func_name

        def closure(self, loop_iter, *args, **kwargs):
            arghash = hashlib.sha1(str(args) + str(kwargs)).hexdigest()
            cache_name = '_cache_%s_%s' % (func_name, arghash)
            counter_name = '_counter_%s_%s' % (func_name, arghash)

            if hasattr(self, cache_name):
                # 캐시된 값이 있을 경우 그것을 사용
                print "Fetching cached value from : %s" % cache_name
                loop_iter -= 1
                setattr(self, counter_name, loop_iter)
                result = getattr(self, cache_name)

                if loop_iter == 0:
                    delattr(self, counter_name)
                    delattr(self, cache_name)
                    print "Cleared cached value"
                return result

            result = func(self, *args, **kwargs)
            setattr(self, cache_name, result)
            setattr(self, counter_name, loop_iter)
            return result

        return closure

    return function_closure

이제 우리는 캐시된 값에 대한 자동적인 무효화를 포함하여, 느린 메소드에 @cache를 자유롭게 사용할 수 있다.

다음과 같이 사용하면 된다.

예제 6-32.

@cache(10)
def slow_compute(self, *args, **kwargs):
    # TODO: stuff goes here...
    pass

요약

자, 여러분의 상상력을 발휘해보기 바란다. 상당한 부분을 재빨리 훑어보았다.

이제 우리는 (__dict__ 속성을 사용하여) 개체의 속성을 조회할 수 있으며, (isinstance 함수를 사용하여) 개체가 특정 클래스 계층 구조에 속해 있는지 확인할 수 있고, currying을 사용하여 다른 함수로부터 함수를 만들어내고 심지어 그것을 임의의 이름에 바인드할 수도 있게 되었다.

환상적이다. 이제 우리에게는 우리 클래스의 속성을 기반으로 복잡한 메소드를 생성하는 데 필요한 모든 기본적인 재료를 갖추었다. 간단한 연락처를 갖는 단순화된 주소록 애플리케이션을 상상해보라.

예제 6-33.

class Contact(object):
    first_name = str
    last_name = str
    date_of_birth = datetime.Date

우리가 데이터베이스에 저장하고 적재하는 방법을 알고 있다고 가정할 경우, 우리는 load() 및 save() 메소드를 자동으로 생성하고 우리의 Contact 클래스에 바인드하는 함수 생성 기법을 사용할 수 있다. 어떤 속성을 데이터베이스에 저장할 필요가 있는지 판단하기 위해서 인트로스펙션을 사용할 수 있다. 심지어는 Contact 클래스에서 특별한 메소드를 길러서 모든 클래스 속성에 대하여 반복을 수행하고 마법과 같이 ‘searchby_first_name’ 및 ‘searchby_last_name’ 메소드를 길러낼 수 있다.

자이썬의 유연한 객체 시스템은, __dict__와 같이 단순히 사전에서의 정보를 조회하는 방법으로 인트로스펙션을 할 수 있는 깊은 능력을 가진 코드를 작성할 수 있도록 해준다. 장식자를 사용하여 클래스의 일부를 재작성할 수 있으며 심지어 실행시간에 새로운 인스턴스 메소드를 생성할 수도 있다. 그 자체를 효과적으로 다시 쓸 수 있도록 이러한 기법들을 혼용하여 코드를 작성할 수 있다. 이 기술은 ‘메타프로그래밍’라고 한다. 이 기술은 매우 강력하다. 우리는 극히 최소한의 코드를 작성할 수 있으며, 전문화된 기능 모두를 생성하는 코드를 작성할 수 있다. 우리의 연락처의 경우에, 그것은 그 자체를 데이터베이스에 저장하고, 적재하고, 제거하는 방법을 ‘마법’과 같이 알고 있다.

이는 장고와 SQLAlchemy의 데이터베이스 매퍼가 동작하는 방식과 정확히 일치한다. 그것들은 데이터베이스와 대화하는 프로그램 일부를 재작성한다. 현실 세계 설정에서 이러한 기술 중 일부를 적용하는 방법을 보려면 해당 라이브러리에 대한 소스 코드를 열어보기를 강력히 권장한다.