사례 연구: 인터페이스 설계

[turtlechap]

이 장의 코드 예제들은 http://thinkpython.com/code/polygon.py에 있습니다.

TurtleWorld

[turtleworld]

이 책을 보조하기 위해, 저는 스왐피(Swampy)라는 패키지를 작성했습니다. http://thinkpython.com/swampy에서 스왐피를 다운로드 할 수 있습니다; 스왐피를 여러분의 시스템에 설치하려면, 그 곳에 나와있는 절차를 따르면 됩니다.

패키지는 모듈의 모음입니다; 스왐피에 있는 모듈 중 하나는 TurtleWorld인데, 화면 위에 있는 거북이를 조종해서 그림을 그리는데 필요한 함수 집합을 제공합니다.

스왐피가 여러분의 시스템에 패키지로 설치되었다면, 이런 식으로 TurtleWorld를 들여올 수 있습니다:

from swampy.TurtleWorld import *

만약 스왐피를 다운로드 했지만 패키지로 설치하지 않았다면, 여러분은 스왐피 파일들이 있는 디렉토리에서 작업하거나, 그 디렉토리를 파이썬의 검색 경로에 추가할 수 있습니다. 그런 다음에는 TurtleWorld를 이런 식으로 들여올 수 있습니다:

from TurtleWorld import *

설치 절차와 파이썬의 검색 경로를 설정하는데 필요한 세부사항은 여러분의 시스템에 따라 다릅니다. 그래서, 세부 사항을 이 곳에 포함하기 보다는, http://thinkpython.com/swampy에 여러 시스템들에 대한 최신 정보를 유지하도록 노력할 것입니다.

mypolygon.py라는 이름의 파일을 만들고, 다음과 같은 코드를 입력하세요:

from swampy.TurtleWorld import *

world = TurtleWorld()
bob = Turtle()
print bob

wait_for_user()

첫번째 줄은 swampy 패키지에 있는 TurtleWorld 모듈로부터 모든 것들을 들여옵니다.

다음 줄은 TurtleWorld를 만들어서 world에 대입하고, Turtle을 만들어서 bob에 대입합니다. bob을 인쇄하면 이런 것을 얻게 됩니다:

<TurtleWorld.Turtle instance at 0xb7bfbf4c>

이 것은 bob이 TurtleWorld 모듈에 정의된 Turtle 의 인스턴스를 가리킨다는 뜻입니다. 이 문맥에서, “인스턴스”는 집합의 원소를 뜻합니다; 이 거북이는 가능한 Turtle들 의 집합에 속하는 하나입니다.

wait_for_user는 TurtleWorld에게 사용자가 뭔가 할 때까지 기다리도록 지시합니다. 이 경우에 창을 닫는 것 외에 딱히 사용자가 할 만한 일이 없기는 합니다.

TurtleWorld는 여러 가지 거북이 조종 함수를 제공합니다: 전진과 후진을 위한 fd 와 bk, 좌회전과 우회전을 위한 lt 와 rt. 또한, 각 거북이는 올려지거나 내려질 수 있는 펜을 하나 들고 있습니다; 펜이 내려지면 거북이는 움직일 때 자취를 남깁니다. 함수 pu 와 pd 는 “펜올림(pen up)” 과 “펜내림(pen down)”을 뜻합니다.

직각을 그리려면, 이 줄들을 프로그램에 추가하세요 (bob을 만든 다음에, wait_for_user를 호출하기 전에):

fd(bob, 100)
lt(bob)
fd(bob, 100)

첫 번째 줄은 bob에게 100 걸음 전진하라고 지시합니다. 두 번째 줄은 좌회전 하도록 지시합니다.

이 프로그램을 실행하면, bob이 동쪽으로 가다 북쪽으로 가면서 두 개의 선분을 남기는 것을 볼 수 있습니다.

이제 정사각형을 그리도록 프로그램을 수정하세요. 되기 전에 다음 절로 넘어가지 마시기 바랍니다.

단순 반복

[repetition]

이런 걸 쓸 수도 있습니다(TurtleWorld를 만들고 사용자를 기다리는 코드는 생략합니다):

fd(bob, 100)
lt(bob)

fd(bob, 100)
lt(bob)

fd(bob, 100)
lt(bob)

fd(bob, 100)

for문장으로 같은 일을 더 간결하게 할 수 있습니다. 이 예제를 mypolygon.py에 넣고 다시 실행하세요:

for i in range(4):
    print 'Hello!'

이런 걸 볼 수 있습니다:

Hello!
Hello!
Hello!
Hello!

이 것은 for 문장의 가장 단순한 예입니다; 나중에 더 보게 될 것입니다. 하지만 정사각형 그리기 프로그램을 다시 쓰는 데는 충분합니다. 할 수 있을 때까지 다음으로 넘어가지 마세요.

여기에 정사각형을 그리는 for 문장이 있습니다:

for i in range(4):
    fd(bob, 100)
    lt(bob)

for 문장의 문법은 함수 정의와 비슷합니다. 콜론으로 끝나는 헤더와 들여쓰기 한 바디가 있습니다. 바디가 포함할 수 있는 문장에 개수 제한은 없습니다.

for 문장은 때로 루프라고 불리는데, 실행 흐름이 바디를 통과한 다음 처음으로 돌아가기 때문입니다. 이 경우는 바디를 네 번 실행합니다.

이 버전은 앞에서 나온 정사각형 그리기 코드와는 조금 다른데, 마지막 변을 그린 후에 회전을 한 번 더 합니다. 추가된 회전은 약간의 시간을 더 소모합니다. 하지만 루프의 매 반복마다 같은 일을 한다면 코드가 단순해집니다. 이 버전은 거북이가 출발 때의 위치와 방향으로 되돌아오게 하는 효과가 있습니다.

연습

다음에 나오는 것은 TurtleWorld를 사용한 몇 가지 연습입니다. 재미를 위한 것이지만 요점도 있습니다. 푸는 동안에 요점이 무엇인지 생각해보세요.

다음 절에는 연습의 답이 있습니다. 마치기 전에는 보지 마시기 바랍니다(적어도 시도는 해 보세요).

  1. 거북이 t를 매개변수로 받아들이는 함수 square를 작성하세요. 거북이를 사용해서 정사각형을 그려야 합니다.

    bob을 square에 인자로 전달하는 함수 호출을 작성한 다음, 프로그램을 다시 실행하세요.

  2. length 라는 매개변수를 square에 추가하세요. 변의 길이가 length가 되도록 바디를 수정하고, 두 번째 인자를 제공하도록 함수 호출을 수정하세요. 프로그램을 다시 실행하세요. 여러 가지 length 값으로 프로그램을 실험해 보세요.

  3. 함수 lt 와 rt 는 기본적으로 90도 회전하지만, 각도를 두 번째 인자로 전달할 수 있습니다. 예를 들어, lt(bob, 45) 는 bob 을 45도 좌회전 시킵니다.

    square 를 복사해서 이름을 polygon으로 바꾸세요. n 이라는 매개변수를 추가하고 바디를 정n각형을 그리도록 수정하세요. 힌트: 정n각형의 외각은 \(360/n\) 도 입니다.

  4. 거북이 t, 반지름 r를 매개변수로 받아서 원과 유사한 도형을 그리는 함수 circle을 작성하세요. polygon에 적절한 변의 길이와 면의 수를 전달해야 합니다. r을 바꿔가면서 함수를 실험해보세요.

    힌트: 원의 둘레를 파악하고 length * n = 원의 둘레가 성립하도록 하세요.

    또 하나의 힌트: 만약 bob이 너무 느리면, bob.delay를 바꿔서 속도를 올릴 수 있습니다. 이 값은 이동 간에 삽입되는 시간으로 초가 단위입니다. bob.delay = 0.01 정도면 움직일 겁니다.

  5. 원호를 지정하는 매개변수 angle을 추가하여 circle을 더 일반화한 함수 arc를 작성하세요. angle은 디그리를 단위로 사용하기 때문에, angle=360 이면 arc는 완전한 원을 그려야 합니다.

캡슐화

첫 번째 연습은 정사각형 그리기 코드를 함수 정의에 넣은 다음, 거북이를 매개변수로 전달하면서 함수를 호출하라고 요구합니다. 여기 답이 있습니다:

def square(t):
    for i in range(4):
        fd(t, 100)
        lt(t)

square(bob)

가장 안쪽에 있는 문장 fd 와 lt는 두 번 들여쓰기 되어 있는데, 함수 정의의 내부에 있는 for 루프의 내부에 있음을 표시하기 위함입니다. 다음 줄 square(bob)은 왼쪽 끝에 맞춰졌기 때문에, for 루프와 함수 정의를 끝냅니다.

함수 안에서 t 가 bob과 같은 거북이를 가리키기 때문에, lt(t)는 lt(bob)와 같은 효과를 줍니다. 그러면 왜 매개변수를 bob이라 하지 않을까요? 착상은 t가 bob뿐만 아니라 어떤 거북이든 될 수 있어서, 두 번째 거북이를 만들어서 square에 인자로 전달할 수 있다는 것입니다:

ray = Turtle()
square(ray)

코드 갓을 함수로 감싸는 것을 캡슐화라 부릅니다. 캡슐화의 한가지 혜택은 코드에 이름을 부여해서 일종의 문서화로 기능하게 하는 것입니다. 다른 장점은 코드를 재사용할 때 바디를 복사해서 붙이는 것 보다, 함수를 두 번 호출하는 것이 더 간결하다는 것입니다.

일반화

다음 단계는 length 매개변수를 square에 추가하는 것입니다. 여기 답이 있습니다:

def square(t, length):
    for i in range(4):
        fd(t, length)
        lt(t)

square(bob, 100)

함수에 매개변수를 추가하는 것을 일반화라고 부르는데, 함수를 더 범용으로 만들기 때문입니다: 이전 버전에서 정사각형은 항상 같은 크기입니다; 이 버전에서는 어떤 크기도 될 수 있습니다.

다음 단계 또한 일반화입니다. 정사각형을 그리는 대신에, polygon은 임의의 개수의 면을 갖는 정다각형을 그립니다. 여기 답이 있습니다:

def polygon(t, n, length):
    angle = 360.0 / n
    for i in range(n):
        fd(t, length)
        lt(t, angle)

polygon(bob, 7, 70)

이 코드는 변의 길이가 70인 정7각형을 그립니다. 서너 개 이상의 숫자 인자가 있으면, 역할과 순서를 잊기 쉽습니다. 인자 목록에 매개변수의 이름을 포함시키는 것은, 문법상 올바를 뿐만 아니라 때로 도움이 됩니다:

polygon(bob, n=7, length=70)

이 것들을 키워드 인자라고 부르는데, 매개변수 이름을 “키워드”로 포함하기 때문입니다(while 이나 def같은 파이썬 예약어와 혼동하지 않길 바랍니다).

이 문법은 프로그램을 좀 더 읽기 쉽게 만듭니다. 인자와 매개변수가 어떤 식으로 동작하는지 상기시키기도 합니다: 함수를 호출할 때, 인자는 매개변수에 대입됩니다.

인터페이스 설계

다음 단계는 매개변수로 반지름 r을 받아들이는 circle을 작성하는 것입니다. 여기 정50각형을 그리는 polygon을 쓰는 간단한 답이 있습니다: The next step is to write circle, which takes a radius,

def circle(t, r):
    circumference = 2 * math.pi * r
    n = 50
    length = circumference / n
    polygon(t, n, length)

첫 줄은 식 \(2 \pi r\) 를 사용하여, 반지름 r인 원의 원주의 길이(circumference)를 계산합니다. math.pi를 사용하기 때문에, math를 들여와야 합니다. 관습적으로, import 문장은 스크립트의 처음에 둡니다.

n 이 원을 근사할 때 사용할 선분의 개수이기 때문에, length는 각 선분의 길이 입니다. 그래서 polygon은 반지를 r인 원을 근사한 정50각형을 그립니다.

이 답의 한가지 한계는 n이 상수라는 것인데, 아주 큰 원에서는 선분이 너무 길고, 작은 원에서는 아주 짧은 선분을 그리기 위해 시간을 낭비한다는 뜻입니다. 한가지 해결책은 n을 매개변수로 받도록 이 함수를 일반화하는 것입니다. 이 방법은 (circle를 호출하는) 사용자에게 더 많은 제어 권을 제공하지만, 인터페이스를 덜 명료하게 만듭니다.

인터페이스는 함수가 어떻게 사용되는지에 대한 요약입니다: 매개변수는 뭔가요? 함수가 하는 일은 뭔가요? 반환 값은 뭔가요? 인터페이스가 “더 할 나위 없이 간단하다 as simple as possible, but not simpler. (Einstein)”면 “명료하다”고 말한다.

이 연습에서 r 은 그려질 원을 지정하기 때문에 인터페이스에 포함시킵니다. n 은 원이 어떻게 그려져야 하는지에 대한 세부사항과 관련되기 때문에 덜 적합합니다.

인터페이스를 혼란스럽게 만드는 것 보다는, circumference에 따라 적절한 n의 값을 선택하는 것이 더 좋습니다:

def circle(t, r):
    circumference = 2 * math.pi * r
    n = int(circumference / 3) + 1
    length = circumference / n
    polygon(t, n, length)

이제 선분의 개수는 (대략) circumference/3 이고, 각 선분의 길이는 (대략) 3이 되는데, 보기 좋은 원이 될 만큼 짧고, 효율적일 만큼 길며, 모든 크기의 원에 적합합니다.

리팩터링

[refactoring]

circle을 작성할 때, 정다각형이 원의 괜찮은 근사이기 때문에 polygon을 재사용할 수 있었습니다. 하지만 arc는 그리 협조적이지 않습니다; 호를 그리는데 polygon 이나 circle을 사용할 수가 없습니다.

한가지 방법은 polygon 의 사본에서 출발해서 arc로 바꾸는 것입니다. 결과는 이런 식입니다:

def arc(t, r, angle):
    arc_length = 2 * math.pi * r * angle / 360
    n = int(arc_length / 3) + 1
    step_length = arc_length / n
    step_angle = float(angle) / n

    for i in range(n):
        fd(t, step_length)
        lt(t, step_angle)

이 함수의 후반부는 polygon처럼 보입니다만, 인터페이스를 변경하지 않고 polygon을 재사용할 수 없습니다. polygon 을 일반화해서 세 번째 인자로 각을 받아들이도록 할 수 있습니다만, polygon 은 더 이상 적절한 이름이 아닙니다! 대신에, 더 일반적인 함수를 polyline이라고 부릅시다:

def polyline(t, n, length, angle):
    for i in range(n):
        fd(t, length)
        lt(t, angle)

이제 polygon 과 arc 를 polyline을 사용하도록 다시 쓸 수 있습니다:

def polygon(t, n, length):
    angle = 360.0 / n
    polyline(t, n, length, angle)

def arc(t, r, angle):
    arc_length = 2 * math.pi * r * angle / 360
    n = int(arc_length / 3) + 1
    step_length = arc_length / n
    step_angle = float(angle) / n
    polyline(t, n, step_length, step_angle)

마지막으로, circle 이 arc를 사용하도록 다시 쓸 수 있습니다:

def circle(t, r):
    arc(t, r, 360)

이런 과정—함수 인터페이스를 개선하고 코드 재사용을 촉진하도록 프로그램을 재배치하는—을 리팩터링이라 부릅니다. 이 경우에, arc 와 polygon에 유사한 코드가 있는 것을 파악하고, polyline으로 “분리”해냈습니다.

우리가 미리 계획했다면, polyline를 먼저 쓰고 리팩터링을 피했을 것입니다만, 종종 프로젝트의 초반에는 모든 인터페이스를 설계할 수 있을 정도로 충분히 알지 못합니다. 일단 코딩을 시작하면, 문제를 더 잘 이해하게 됩니다. 때로 리팩터링은 여러분이 뭔가를 배웠다는 신호입니다.

개발 계획

개발 계획은 프로그램을 쓰는 절차입니다. 이 사례 연구에서 우리가 사용한 절차는 “캡슐화와 일반화”입니다. 이 절차의 단계들은:

  1. 함수 정의가 없는 작은 프로그램을 쓰는 것으로 시작한다.
  2. 일단 프로그램이 작동하면, 함수로 캡슐화하고 이름을 부여한다.
  3. 적절한 매개변수를 추가하여 함수를 일반화한다.
  4. 일련의 작동하는 함수들을 얻을 때까지 1–3 단계를 반복한다. 재입력(과 재디버깅)을 피하기 위해 작동하는 코드를 복사해서 붙여 넣으세요.
  5. 리팩터링으로 프로그램을 개선할 수 있는 기회를 살피세요. 예를 들어, 여러 곳에 비슷한 코드들이 있다면, 적당한 일반적인 함수로 분리해 내는 것을 고려하세요.

이 절차는 몇 가지 결점을 갖고 있습니다만—후에 대안들을 보게 됩니다—, 프로그램을 어떻게 함수로 나누어야 하는지 미리 알지 못할 때 유용할 수 있습니다. 이런 접근법은 진행 하면서 점진적으로 설계하도록 합니다.

주석문자열

[docstring]

주석문자열 docstring은 인터페이스를 설명하기 위해 함수의 첫머리에 두는 문자열입니다(“doc”은 “문서화 documentation”의 줄임 말입니다). 여기 예가 있습니다:

def polyline(t, n, length, angle):
    """길이 length 와 사이 각 angle (디그리)로 n 개의
    선분을 그린다. t 는 거북이다.
    """
    for i in range(n):
        fd(t, length)
        lt(t, angle)

이 주석문자열은 삼중따옴표로 둘러싸인 문자열인데, 삼중따옴표가 여러 줄로 구성된 문자열을 허용하기 때문에 복수 행 문자열로 알려져 있기도 합니다.

간결하지만, 이 함수를 사용하는 사람이 필요로 할 핵심 정보들을 갖고 있습니다. 함수가 무엇을 하는지에 대해 (어떻게 하는지에 관한 세부사항은 언급하지 않고) 간략하게 설명합니다. 각 매개변수가 함수에 동작에 어떤 영향을 주고, 어떤 형이 제공되어야 하는지 (만약 명백하지 않다면) 설명합니다.

이런 종류의 설명을 쓰는 것은 인터페이스 설계의 중요한 부분입니다. 잘 설계된 인터페이스는 설명하기 간단해야 합니다; 함수를 설명하기 힘들다면, 인터페이스에 개선의 여지가 있다는 신호일 수 있습니다.

디버깅

인터페이스는 함수와 호출자간의 계약과 같습니다. 호출자는 어떤 매개변수를 제공하는데 동의하고, 함수는 어떤 일을 하는데 동의합니다.

예를 들어, polyline 은 4개의 인자를 요구합니다: t 는 거북이여야 합니다; n 은 선분의 갯수라서 정수가 돼야 합니다; length 는 양수여야 합니다; angle 은 숫자여야하고, 디그리로 해석됩니다.

함수가 실행되기 전에 만족되어야 하기 때문에, 이 요구사항들을 사전조건이라고 부릅니다. 반대로, 함수 끝에서의 조건들을 사후조건이러고 부릅니다. 사후조건은 함수의 (선들을 그리는 것 같은) 의도된 효과와 (거북이를 움직이거나 세계에 다른 변화를 만드는 것 따위의) 부작용을 포함합니다.

사전조건은 호출자의 책임입니다. 만약 호출자가 (잘 문서화된!) 사전조건을 어기로, 함수가 제대로 동작하지 않았다면, 버그는 함수가 아니라 호출자에게 있는 것입니다.

용어

인스턴스 instance:
집합의 원소. 이 장에서 TurtleWorld 는 TurtleWorld들 집합의 한 원소다.
루프 loop:
반복적으로 실행할 수 있는 프로그램의 한 부분.
캡슐화 encapsulation:
일련의 문장들을 함수 정의로 바꾸는 과정.
일반화 generalization:
(숫자처럼) 필요이상으로 구체적인 것을 (변수나 매개변수처럼) 적당히 일반적인 것으로 바꾸는 과정.
키워드 인자 keyword argument:
매개변수의 이름을 “키워드”로 포함하는 인자.
인터페이스 interface:
함수를 사용하는 법에 대한 기술로, 이름, 인자와 반환 값에 대한 기술을 포함한다.
리팩터링 refactoring:
함수 인터페이스와 코드의 다른 품질을 개선하기 위해 작동하는 프로그램을 수정하는 과정.
개발 계획 development plan:
프로그램을 쓰는 절차.
주석문자열 docstring:
함수의 인터페이스를 문서화하기 위해 함수 정의에 등장하는 문자열.
사전조건 precondition:
함수가 시작하기 전에 호출자가 만족시켜야 하는 요구사항.
사후조건 postcondition:
종료하기 전에 함수가 만족시켜야 하는 요구사항.

연습

[연습 4.1.]

이 장의 코드를 http://thinkpython.com/code/polygon.py에서 다운로드하세요.

  1. polygon, arc, circle 에 적절한 주석문자열을 써 넣으세요.
  2. circle(bob, radius)를 실행하는 동안의 프로그램 상태를 보여주는 스택 다이어그램을 그리세요. 계산을 손으로 하거나 코드에 print 문장을 추가할 수 있습니다.
  3. [refactoring] 절 에 나오는 arc 가 아주 정확하지는 않은데, 원의 선형 근사가 항상 진짜 원의 바깥에 놓이기 때문입니다. 결과적으로, 거북이는 올바른 종착점에서 몇 단위 벗어나게 됩니다. 제 답은 이 오류의 효과를 줄이는 방법을 보여줍니다. 코드를 읽고 여러분에게도 말이 되는지 보세요. 도표를 그려본다면, 어떻게 동작하는지 보일 겁니다.

flowers

[fig.flowers]

[연습 4.2.]

그림 [fig.flowers]와 같은 꽃을 그리는, 적절하게 일반적인 함수들을 작성하세요.

답: http://thinkpython.com/code/flower.py, http://thinkpython.com/code/polygon.py 도 필요합니다.

pies

[fig.pies]

[연습 4.3.]

그림 [fig.pies]와 같은 모양을 그리는, 적절하게 일반적인 함수들을 작성하세요.

답: http://thinkpython.com/code/pie.py.

[연습 4.4.]

알파벳의 글자들은 수직선, 수평선과 몇 가지 곡선들과 같은 기본 요소들 약간으로 그릴 수 있습니다. 최소한의 기본 요소들로 그릴 수 있는 글꼴을 디자인하고, 알파벳의 글자들을 그리는 함수를 작성하세요.

각 글자마다 하나의 함수를 작성해야 하고, 이름은 draw_a, draw_b, 등등, 함수를 letters.py하는 파일에 저장하세요. 여러분의 코드를 테스트하는데 도움이 되는 “거북 타자기”를 http://thinkpython.com/code/typewriter.py에서 다운로드 할 수 있습니다.

답: http://thinkpython.com/code/letters.py, http://thinkpython.com/code/polygon.py도 필요합니다.

[연습 4.5.]

http://en.wikipedia.org/wiki/Spiral에서 나선에 관한 글을 읽으세요; 그런 다음 아르키메데스 나선(Archimedian spiral) (또는 다른 종류 아무거나)을 그리는 프로그램을 작성하세요. 답: http://thinkpython.com/code/spiral.py.