클래스와 메쏘드

이 장의 예제는 http://thinkpython.com/code/Time2.py 에 있습니다.

객체 지향적인 기능들

파이썬은 객체 지향적(object-oriented) 프로그래밍 언어인데, 객체 지향적 프로그래밍을 지원하는 기능들을 제공한다는 뜻입니다.

객체 지향적 프로그래밍을 정의하는 것은 쉽지 않습니다만, 몇 가지 특징은 이미 보았습니다:

  • 프로그램은 객체 정의와 함수 정의로 구성되고, 대부분의 계산은 객체에 대한 연산으로 표현됩니다.
  • 각 객체 정의는 실 세계의 어떤 객체나 개념에 대응되고, 그 객체에 작용하는 함수는 실 세계의 객체들이 상호작용하는 방법에 대응합니다.

예를 들어, [time] 장에서 정의된 Time 클래스는 사람들이 하루 중 시간을 기록하는 방식에 대응하고, 우리가 정의한 함수들은 사람들이 시간들을 갖고 하는 것들에 대응합니다. 비슷하게, Point 와 Rectangle 클래스는 점과 직사각형의 수학적 개념에 대응합니다.

지금까지, 우리는 파이썬이 객체 지향형 프로그래밍을 지원하기 위해 제공하는 기능들의 장점을 활용하지 않았습니다. 이 기능들이 반드시 필요한 것은 아닙니다; 그들 대부분은 우리가 이미 해온 것들의 대안적인 문법을 제공합니다. 그러나 많은 경우에, 이 대안은 더 간결하고 프로그램의 구조를 더 정확하게 전달합니다.

예를 들어, Time 프로그램에서, 클래스 정의와 그 뒤를 따르는 함수 정의 사이에는 명확한 연결이 없습니다. 살펴보면, 모든 함수들이 적어도 하나의 Time 객체를 인자로 받아들인다는 것이 명백해집니다.

이 관찰이 메쏘드(method) 의 동기가 됩니다; 메쏘드는 특정 클래스와 결합된 함수입니다. 우리는 문자열, 리스트, 딕셔너리, 튜플들의 메쏘드를 보아왔습니다. 이 창에서, 우리는 사용자 정의 형들의 메쏘드를 정의합니다.

메쏘드는 의미적으로 함수와 동일합니다만, 두 가지 문법적인 차이가 있습니다:

  • 클래스와 메쏘드간의 관계를 구체적으로 만들기 위해 메쏘드는 클래스 정의 안에서 정의됩니다.
  • 메쏘드를 호출하는 문법은 함수를 호출하는 문법과 다릅니다.

다음 몇 개의 절에서, 앞의 두 장에서 나온 함수들을 메쏘드로 바꾸게 됩니다. 이 변환은 아주 기계적입니다; 여러분은 단지 몇 단계의 절차를 따르는 것 만으로 할 수 있습니다. 여러분이 한가지 형태를 다른 것으로 바꾸는데 익숙해지면, 여러분은 하려고 하는 것에 가장 적합한 형태를 선택할 수 있게 될 것입니다.

객체 인쇄하기

[time] 장에서, Time 클래스를 정의했고, 연습 [ex.printtime]에서, print_time 라는 함수를 작성했습니다:

class Time(object):
    """Represents the time of day."""

def print_time(time):
    print '%.2d:%.2d:%.2d' % (time.hour, time.minute, time.second)

이 함수를 호출하려면, Time 객체를 인자로 전달해야만 합니다:

>>> start = Time()
>>> start.hour = 9
>>> start.minute = 45
>>> start.second = 00
>>> print_time(start)
09:45:00

print_time 를 메쏘드로 만들기 위해, 우리가 해야 하는 것은 함수 정의를 클래스 정의 안으로 옮기는 것뿐입니다. 들여쓰기의 변화를 주목하세요.

class Time(object):
    def print_time(time):
        print '%.2d:%.2d:%.2d' % (time.hour, time.minute, time.second)

이제 print_time 를 호출하는 두 가지 방법이 있습니다. 첫 번째 (덜 일반적인) 방법은 함수 문법을 사용하는 것입니다:

>>> Time.print_time(start)
09:45:00

이와 같은 점 표기법(dot notation)에서, Time은 클래스의 이름이고 print_time 은 메쏘드의 이름입니다. start 는 인자로 전달됩니다.

두 번째 (더 간결한) 방법은 메쏘드 문법을 사용하는 것입니다:

>>> start.print_time()
09:45:00

이와 같은 점 표기법(dot notation)에서, print_time 은 (앞과 마찬가지로) 메쏘드의 이름이고, start 는 메쏘드가 적용되는 객체인데, 주체(subject)라고 부릅니다. 메쏘드 호출의 주체는 메쏘드가 작용하는 대상입니다.

메쏘드 안에서 주체는 첫 번째 매개변수에 대입되는데, 이 경우에는 start가 time에 대입됩니다.

관례적으로, 메쏘드의 첫 번째 매개변수를 self라고 부르기 때문에, print_time 을 이런 식으로 작성하는 것이 더 일반적입니다:

class Time(object):
    def print_time(self):
        print '%.2d:%.2d:%.2d' % (self.hour, self.minute, self.second)

이런 관례는 은근한 암시 때문입니다:

  • 함수 호출 문법, print_time(start),은 함수가 능동적인 주체임을 제안합니다. 이런 식으로 말합니다, “어이 print_time! 여기 당신이 인쇄할 객체가 있소이다.”
  • 객체 지향형 프로그래밍에서는, 객체가 능동적인 주체입니다. start.print_time() 와 같은 메쏘드 호출은 “어이 start! 자신을 인쇄해주세요.” 라고 말합니다.

이런 관점의 변화는 더 예의 바를 수 있습니다만, 쓸모가 있는지는 확실하지 않습니다. 지금까지 본 예에서는, 그리 쓸모 있지는 않습니다. 하지만, 때때로 책임을 함수에서 객체로 옮기는 것은 더 융통성 있는 함수를 작성하는 것이 가능하도록 하고, 유지하고 재사용하기 쉽게 만듭니다.

[연습 17.1.] [convert]

([prototype] 절의) time_to_int 를 메쏘드로 다시 작성하세요. 아마도 int_to_time 를 메쏘드로 다시 작성하는 것은 적절치 못할 겁니다; 어떤 객체를 대상으로 호출해야 할까요?

또 하나의 예

여기 ([increment] 절의) increment 의 메쏘드로 다시 작성한 버전이 있습니다:

# inside class Time:

    def increment(self, seconds):
        seconds += self.time_to_int()
        return int_to_time(seconds)

이 버전은 연습 [convert] 에서처럼 time_to_int 가 메쏘드로 작성되었다고 가정하고 있습니다. 또 수정자가 아니라 순수 함수라는 점에도 주목해주세요.

이런 식으로 increment를 호출합니다:

>>> start.print_time()
09:45:00
>>> end = start.increment(1337)
>>> end.print_time()
10:07:17

주체, start, 는 첫 번째 매개변수, self’에 대입됩니다. 인자, 1337,는 두 번째 매개변수, seconds, 에 대입됩니다.

이 메커니즘은 혼란스러울 수 있는데, 특히 여러분이 오류를 만들었을 때 그렇습니다. 예를 들어, increment를 두 개의 인자로 호출하면 이렇게 됩니다:

>>> end = start.increment(1337, 460)
TypeError: increment() takes exactly 2 arguments (3 given)

괄호에는 오직 두 개의 인자만이 있기 때문에, 오류 메시지가 처음에는 혼란스럽습니다. 그러나 주체 또한 인자로 취급되기 때문에, 모두 합하면 세 개입니다.

더 복잡한 예

(연습 [isafter]의) is_after 는 약간 더 복잡한데, 두 개의 Time 객체를 매개변수로 받아들이기 때문입니다. 이 경우에 첫 번째 매개변수를 self로 두 번째 매개변수를 other로 부르는 것이 관례입니다:

# inside class Time:

    def is_after(self, other):
        return self.time_to_int() > other.time_to_int()

이 메쏘드를 사용하려면, 하나의 객체에 대해 호출하면서 다른 하나를 인자로 전달해야 합니다:

>>> end.is_after(start)
True

이 문법의 멋진 점 하나는 거의 영어처럼 읽힌다는 것입니다: “end 가 start 뒤에 오나요?(end is after start?)”

init 메쏘드

init 메쏘드(“초기화(initialization)”의 줄임 말)는 객체가 인스턴스화될 때 호출되는 특수 메쏘드(special method)입니다. 완전한 이름은 __init__ 입니다(두 개의 밑줄 뒤에 init가 오고, 그 뒤로 밑줄 두 개가 옵니다). Time 클래스의 init 메쏘드는 이렇게 될 수 있습니다:

# inside class Time:

    def __init__(self, hour=0, minute=0, second=0):
        self.hour = hour
        self.minute = minute
        self.second = second

__init__ 의 매개변수들이 애트리뷰트들과 같은 이름을 갖는 것은 흔합니다. 문장

self.hour = hour

은 매개변수 hour의 값을 self의 애트리뷰트에 저장합니다.

매개변수는 선택적이라서, 인자 없이 Time을 호출하면 내정 값을 얻습니다.

>>> time = Time()
>>> time.print_time()
00:00:00

인자 하나를 제공하면, hour를 번복(override)합니다:

>>> time = Time (9)
>>> time.print_time()
09:00:00

인자 두 개를 제공하면, hour 와 minute를 번복합니다.

>>> time = Time(9, 45)
>>> time.print_time()
09:45:00

그리고 만약 세 개의 인자를 제공하면, 세 개의 내정 값 모두를 번복합니다.

[연습 17.2.]

x 와 y를 선택적인 매개변수로 받아서 해당 애트리뷰트에 대입하도록 Point 클래스의 init 메쏘드를 작성하세요.

__str__ 메쏘드

__init__ 처럼 __str__ 도 특수 메쏘드인데 객체의 문자열 표현을 돌려주기로 되어있습니다.

예를 들어, 여기 Time 객체의 str 메쏘드가 있습니다:

# inside class Time:

    def __str__(self):
        return '%.2d:%.2d:%.2d' % (self.hour, self.minute, self.second)

여러분이 객체를 print 할 때, 파이썬은 str 메쏘드를 호출합니다:

>>> time = Time(9, 45)
>>> print time
09:45:00

새 클래스를 작성할 때, 저는 거의 항상 객체 인스턴스화를 쉽게 만드는 __init__와 디버깅에 쓸모 있는 __str__ 를 작성하는 것으로 시작합니다.

[연습 17.3.]

Point 클래스의 str 메쏘드를 작성하세요. Point 객체를 만들고 인쇄하세요.

연산자 오버로딩

[operator.overloading]

다른 특수 메쏘드들을 정의함으로써, 사용자 정의 형들에 대한 연산자들의 동작을 지정할 수 있습니다. 예를 들어, Time 클래스에 __add__ 라는 메쏘드를 정의하면, Time 객체들에 + 연산자를 사용할 수 있게 됩니다.

정의는 이런 식입니다:

# inside class Time:

    def __add__(self, other):
        seconds = self.time_to_int() + other.time_to_int()
        return int_to_time(seconds)

그리고 이렇게 사용할 수 있습니다.

>>> start = Time(9, 45)
>>> duration = Time(1, 35)
>>> print start + duration
11:20:00

Time 객체들에 + 연산자를 적용하면, 파이썬은 __add__ 를 호출합니다. 결과를 인쇄할 때, 파이썬은 __str__를 호출합니다. 무대 뒤에서는 많은 일들이 벌어지고 있습니다!

사용자 정의형과 사용할 수 있도록 연산자의 동작을 바꾸는 것을 연산자 오버로딩(operator overloading)이라고 합니다. __add__ 처럼 , 파이썬의 모든 연산자들마다 대응하는 특수 메쏘드들이 있습니다. 더 자세한 내용은 http://docs.python.org/2/reference/datamodel.html#specialnames를 보세요.

[연습 17.4.]

Point 클래스의 add 메쏘드를 작성하세요.

형 기반 디스패치

앞 절에서 우리는 두 개의 Time 객체를 더했습니다. 하지만, 여러분은 정수를 Time 객체에 더하고 싶기도 할 것입니다. 다음은 other의 형을 검사한 다음, add_time 이나 increment를 호출하는 버전의 __add__ 입니다:

# inside class Time:

    def __add__(self, other):
        if isinstance(other, Time):
            return self.add_time(other)
        else:
            return self.increment(other)

    def add_time(self, other):
        seconds = self.time_to_int() + other.time_to_int()
        return int_to_time(seconds)

    def increment(self, seconds):
        seconds += self.time_to_int()
        return int_to_time(seconds)

내장 함수 isinstance는 값과 클래스 객체를 받아서 값이 그 클래스의 인스턴스면 True를 돌려줍니다.

만약 other가 Time 객체면, __add__add_time 를 호출합니다. 그렇지 않으면 매개변수가 정수라고 가정하고 increment를 호출합니다. 이 연산을 형 기반 디스패치(type-based dispatch)라고 부르는데, 인자의 형에 따라 다른 메쏘드로 계산을 디스패치하기 때문입니다.

여기 서로 다른 형에 + 연산자를 쓰는 예가 있습니다:

>>> start = Time(9, 45)
>>> duration = Time(1, 35)
>>> print start + duration
11:20:00
>>> print start + 1337
10:07:17

불행하게도, 이 덧셈의 구현은 교환적(commutative)이지 않습니다. 만약 정수가 첫 번째 피 연산자면, 이렇게 됩니다:

>>> print 1337 + start
TypeError: unsupported operand type(s) for +: 'int' and 'instance'

문제는, Time 객체에 정수를 더하라고 요청하는 대신, 파이썬은 정수에 Time 객체를 더하라고 요청하고 있고, 정수는 어떻게 하는지 모른다는 것입니다. 그러나 이 문제를 위한 영리한 해법이 있습니다: 특수 메쏘드 __radd__ 인데 “오른쪽 덧셈(right-side add)”을 뜻합니다. 이 메쏘드는 + 연산자의 오른쪽에 Time 객체가 등장할 때 호출됩니다. 이렇게 정의합니다:

# inside class Time:

    def __radd__(self, other):
        return self.__add__(other)

이런 식으로 사용합니다:

>>> print 1337 + start
10:07:17

[연습 17.5.]

Point 객체나 튜플과 함께 쓸 수 있는 Point 의 add 메쏘드를 작성하세요:

  • 만약 두 번째 피 연산자가 Point 이면, 메쏘드는 \(x\) 좌표가 피 연산자들의 \(x\) 좌표들의 합이고, \(y\) 좌표도 마찬가지인 새 Point 를 돌려줘야 합니다.
  • 만약 두 번째 피 연산자가 튜플이면, 이 메쏘드는 튜플의 첫 번째 요소를 \(x\) 좌표에, 두번째 요소를 \(y\) 좌표에 더한 결과를 갖는 새 Point 를 돌려줘야 합니다.

다형성

형 기반 디스패치는 필요할 경우 유용합니다만, (다행히도) 늘 필요하지는 않습니다. 종종 여러 형의 인자에도 올바르게 동작하는 함수들을 작성함으로써 회피할 수 있습니다.

우리가 문자열을 위해 작성한 많은 함수들은 실제로 모든 종류의 시퀀스에 대해서 동작합니다. 예를 들어, [histogram] 절에서 단어에 등장하는 각 글자들의 빈도를 세려고 histogram을 사용했습니다.

def histogram(s):
    d = dict()
    for c in s:
        if c not in d:
            d[c] = 1
        else:
            d[c] = d[c]+1
    return d

이 함수는 s의 요소가 해싱가능(hashable)해서 d의 키로 사용할 수 있는 한, 리스트, 튜플은 물론이고 딕셔너리에서도 동작합니다.

>>> t = ['spam', 'egg', 'spam', 'spam', 'bacon', 'spam']
>>> histogram(t)
{'bacon': 1, 'egg': 1, 'spam': 4}

여러 가지 형에 대해 동작하는 함수들을 다형적(polymorphic)이라고 합니다. 다형성(polymorphism)은 코드 재사용을 촉진합니다. 예를 들어, 시퀀스의 요소들을 더하는 내장 함수 sum은 시퀀스의 요소들이 덧셈을 지원하는 한 동작합니다.

Time 객체는 add 메쏘드를 제공하기 때문에, sum 에 사용될 수 있습니다:

>>> t1 = Time(7, 43)
>>> t2 = Time(7, 41)
>>> t3 = Time(7, 37)
>>> total = sum([t1, t2, t3])
>>> print total
23:01:00

일반적으로, 함수내의 모든 연산들이 주어진 형에 적용될 수 있다면, 그 함수는 그 형에 대해 동작합니다.

가장 좋은 다형성은 의도하지 않은 상태에서 일어나는 것인데, 여러분이 이미 작성한 함수가 여러분이 전혀 계획하지 않은 형에 대해서도 적용될 수 있음을 발견하는 경우입니다.

디버깅

프로그램이 실행되는 동안 아무 때건 객체에 애트리뷰트를 추가할 수 있습니다만, 만약 여러분이 형 이론(type theory)에 까다롭다면, 같은 형의 객체가 다른 애트리뷰트 세트를 갖는 다는 것은 미덥지 않은 관행입니다. 보통 init 메쏘드에서 모든 애트리뷰트들을 초기화하는 것이 좋습니다.

만약 객체에 특정 애트리뷰트가 있는지 의심스럽다면, 내장 함수 hasattr 를 사용할 수 있습니다([hasattr] 절을 보세요).

객체의 애트리뷰트에 접근하는 다른 방법은 특수 애트리뷰트 __dict__를 통하는 것인데, 애트리뷰트 이름(문자열)을 값에 대응시키는 딕셔너리입니다:

>>> p = Point(3, 4)
>>> print p.__dict__
{'y': 4, 'x': 3}

디버깅의 목적에서, 이 함수를 가까이 두면 유용합니다:

def print_attributes(obj):
    for attr in obj.__dict__:
        print attr, getattr(obj, attr)

print_attributes 는 객체의 딕셔너리에 있는 항목들을 탐색해서, 각 애트리뷰트들의 이름과 대응하는 값들을 인쇄합니다.

내장 함수 getattr은 객체와 애트리뷰트 이름(문자열)을 받아서 애트리뷰트의 값을 돌려줍니다.

인터페이스와 구현

객체 지향적 설계의 목표 중 하나는 소프트웨어를 더 유지하기 좋게, 즉 시스템의 다른 부분이 바뀔 때 프로그램이 계속 동작하게 할 수 있도록, 만드는 것과 새 요구조건에 맞춰 프로그램을 수정하는 것입니다.

이 목표를 달성할 수 있도록 돕는 한가지 디자인 원리는 인터페이스를 구현과 분리하는 것입니다. 객체의 경우, 이 것은 클래스가 제공하는 메쏘드들이 애트리뷰트들이 표현되는 방법에 의존하지 않아야 한다는 뜻입니다.

예를 들어, 이 장에서 하루 중의 시간을 나타내는 클래스를 개발했습니다. 이 클래스가 제공하는 메쏘드들은 time_to_int, is_after, add_time 를 포함합니다.

우리는 이 메쏘드들을 여러 가지 방법으로 구현할 수 있습니다. 구현의 세부사항은 우리가 시간을 어떻게 표현하는가에 달려있습니다. 이 장에서, Time 객체의 애트리뷰트는 hour, minute, second 입니다.

대안으로, 이 애트리뷰트들을 자정 이후의 초를 표현하는 하나의 정수로 바꿀 수 있습니다. 이 구현은, is_after 와 같은, 어떤 함수들을 작성하기 쉽게 만들지만 또 어떤 함수들은 더 어렵게 만듭니다.

새 클래스를 배포한 후에 더 좋은 구현을 발견할 수 있습니다. 만약 프로그램의 다른 부분이 여러분의 클래스를 사용하고 있다면, 인터페이스를 변경하는 것은 시간이 걸리고 오류를 일으키기 쉽습니다.

그러나 여러분이 인터페이스를 주의 깊게 설계했다면, 인터페이스를 바꾸지 않고 구현을 바꿀 수 있는데, 프로그램의 다른 부분들은 바꿀 필요가 없다는 뜻입니다.

인터페이스를 구현과 분리하는 것은 애트리뷰트들을 감춰야 한다는 뜻입니다. 프로그램의 다른 부분들(클래스 정의의 바깥)에 있는 코드는 객체의 상태를 읽고 바꾸는데 메쏘드를 사용해야만 합니다. 애트리뷰트에 직접 접근하지 말아야 합니다. 이 원리를 정보 은닉(information hiding) 이라고 합니다; http://en.wikipedia.org/wiki/Information_hiding를 보세요.

[연습 17.6.]

이 장의 코드를 다운로드 하세요(http://thinkpython.com/code/Time2.py). Time의 애트리뷰트들을 자정 이후의 초를 나타내는 하나의 정수로 바꾸세요. 그런 다음 메쏘드들(과 함수 int_to_time)을 새 구현에 맞게 수정하세요. main 에 있는 테스트 코드를 수정할 필요가 없어야 합니다. 작업을 마쳤을 때, 출력은 전과 동일해야 합니다. 답: http://thinkpython.com/code/Time2_soln.py.

용어

객체 지향 언어 object-oriented language:
사용자 정의 클래스와 메쏘드 문법과 같이, 객체 지향 프로그래밍을 촉진하는 기능들을 제공하는 언어.
객체 지향 프로그래밍 object-oriented programming:
데이터와 그를 조작하는 연산들이 클래스와 메쏘드로 조직화되는 프로그래밍 스타일.
메쏘드 method:
클래스의 내부에서 정의되고 그 클래스의 인스턴스로 호출되는 함수.
주체 subject:
메쏘드가 호출되는 대상 객체.
연산자 오버로딩 operator overloading:
사용자 정의 형과 함께 동작하도록 + 와 같은 연산자의 동작을 바꾸는 것.
형 기반 디스패치 type-based dispatch:
피 연산자의 형을 검사하고 그에 따라 다른 함수를 호출하는 프로그래밍 패턴.
다형적 polymorphic:
하나 이상의 형에 사용되는 함수에 대한 표현.
정보 은닉 information hiding:
객체에 의해 제공되는 인터페이스는 구현, 특히 애트리뷰트들의 표현,에 독립적이어야 한다는 원리.

연습

[연습 17.7.]

이 연습은 파이썬에서 가장 흔하고 찾기 힘든 오류들 중 하나에 관한 교훈입니다. 다음과 같은 메쏘드들을 갖는 Kangaroo 라는 이름의 클래스 정의를 작성하세요:

  1. pouch_contents 라는 이름의 애트리뷰트를 빈 리스트로 초기화하는 __init__ 메쏘드.
  2. 임의의 형의 객체를 받아서 pouch_contents 에 추가하는 put_in_pouch 메쏘드.
  3. Kangaroo 객체와 주머니(pouch)의 내용물들의 문자열 표현을 돌려주는 __str__ 메쏘드.

두 개의 Kangaroo 객체를 만들어서, 변수 kanga 와 roo 에 대입하고, roo 를 kanga 의 주머니 내용물에 추가하는 방법으로 여러분의 코드를 검사하세요.

http://thinkpython.com/code/BadKangaroo.py를 다운로드 하세요. 이 파일은 앞 문제에 대한 답을 갖고 있지만, 하나의 크고 고약한 버그도 있습니다. 버그를 찾아서 고치세요.

막히면 http://thinkpython.com/code/GoodKangaroo.py를 다운로드 할 수 있는데, 문제를 설명하고 답을 제시합니다.

[연습 17.8.]

Visual 은 3-D 그래픽을 제공하는 파이썬 모듈입니다. 파이썬 설치에 항상 포함되는 것은 아니기 때문에, 여러분의 소프트웨어 저장소나, 거기에 없는 경우, vpython.org에서 설치해야 할 수 있습니다.

다음 예는 가로, 세로, 높이가 256 단위인 3-D 공간을 만들고, “중심(center)”이 점 \((128,128,128)\) 이 되도록 설정합니다. 그런 다음 파란 구를 그립니다.

from visual import *

scene.range = (256, 256, 256)
scene.center = (128, 128, 128)

color = (0.1, 0.1, 0.9)          # mostly blue
sphere(pos=scene.center, radius=128, color=color)

color 는 RGB 튜플입니다; 즉 요소들은 0.0 과 1.0 사이의 적-녹-청(Red-Green-Blue) 수준입니다(http://en.wikipedia.org/wiki/RGB_color_model를 보세요).

이 코드를 실행하면, 검은 배경에 파란 구가 있는 창을 보게 됩니다. 가운데 버튼을 위 아래로 끌면 확대 축소할 수 있습니다. 오른쪽 버튼을 끌어서 장면을 회전시킬 수도 있습니다만, 단지 구 하나뿐인 세상에서 차이를 말하기는 어렵습니다.

다음과 같은 순환은 구로 만들어진 정육면체를 만듭니다:

t = range(0, 256, 51)
for x in t:
    for y in t:
        for z in t:
            pos = x, y, z
            sphere(pos=pos, radius=10, color=color)
  1. 이 코드를 스크립트에 넣고 작동을 확인하세요.
  2. 정육면체의 각 구가 RGB 공간에서의 위치에 해당하는 색깔을 갖도록 프로그램을 수정하세요. 좌표는 범위 0–255 에 있는 반면, RGB 튜플은 범위 0.0–1.0 에 있다는 점을 주목하세요.
  3. http://thinkpython.com/code/color_list.py를 다운로드하고, 함수 read_colors를 사용해서, 여러분의 시스템에서 사용 가능한 색상들의 이름과 RGB 값들의 리스트를 만드세요.

여러분은 제 답을 http://thinkpython.com/code/color_space.py에서 볼 수 있습니다.