클래스와 함수

[time]

이 장에 나오는 코드 예는 http://thinkpython.com/code/Time1.py에 있습니다.

Time

[time.object]

사용자 정의 형의 다른 예로, 하루 중의 시간을 기록하는 Time 이라는 클래스를 정의하겠습니다. 클래스 정의는 이렇습니다:

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

    attributes: hour, minute, second
    """

새 Time 객체를 만들고 애트리뷰트에 시,분,초를 대입할 수 있습니다:

time = Time()
time.hour = 11
time.minute = 59
time.second = 30

Time 객체의 상태도는 그림 [fig.time]과 같습니다.

[연습 16.1.] [ex.printtime]

Time 객체를 받아서 시:분:초 형태로 인쇄하는 함수 print_time를 작성하세요. 힌트: 포맷 시퀀스 '%.2d' 는 정수를 인쇄하는데, 최소한 두 개의 숫자를 사용하고, 필요하다면 앞에 0 을 붙입니다.

[연습 16.2.] [isafter]

두 개의 Time 객체, t1 과 t2,를 받아서, t1 이 시간순으로 t2 뒤에 오면 True를 그렇지 않으면 False를 돌려주는 논리함수 is_after 를 작성하세요. 도전: if 문장을 사용하지 마세요.

time

[fig.time]

순수 함수

다음에 올 몇개의 절에서, 시간 값들을 더하는 두 개의 함수를 작성합니다. 그들은 두 가지 종류의 함수를 예시합니다: 순수 함수(pure function)와 비순수 함수(modifier). 그들은 또한 제가 프로토타입과 패치(prototype and patch)라고 부르는 개발 계획(development plan)을 예시하기도 하는데, 간단한 프로토타입으로 시작해서 점진적으로 복잡성을 처리하는 방식으로 복잡한 문제를 다루는 방법입니다.

여기 add_time의 간단한 프로토타입이 있습니다:

def add_time(t1, t2):
    sum = Time()
    sum.hour = t1.hour + t2.hour
    sum.minute = t1.minute + t2.minute
    sum.second = t1.second + t2.second
    return sum

이 함수는 새 Time 객체를 만들고, 애트리뷰트들을 초기화한 후, 새 객체의 참조를 돌려줍니다. 이 것은 순수 함수(pure function) 라고 불리는데, 인자로 전달된 어떤 객체들도 수정하지 않고, 값을 돌려주는 것 외에 값을 출력하거나 사용자의 입력을 받는 것과 같은 부수 효과가 없기 때문입니다.

이 함수를 검사하기위해, 두 개의 Time 객체를 만들 것입니다: start 는 영화, Monty Python and the Holy Grail 같은, 시작 시간을 갖고있고, duration은 영화 상영시간 (1시간 35분)을 갖고 있습니다.

add_time 은 영화가 언제 끝날지를 계산합니다.

>>> start = Time()
>>> start.hour = 9
>>> start.minute = 45
>>> start.second =  0

>>> duration = Time()
>>> duration.hour = 1
>>> duration.minute = 35
>>> duration.second = 0

>>> done = add_time(start, duration)
>>> print_time(done)
10:80:00

결과 10:80:00 는 여러분이 기대하던 것이 아닐겁니다. 문제는 이 함수가 초나 분의 합이 60을 넘는 경우를 다루지 않는 것입니다. 그런 경우에, 남는 초를 분으로, 남는 분을 시로 “올려줘야(carry)” 합니다.

여기에 개선된 버전이 있습니다:

def add_time(t1, t2):
    sum = Time()
    sum.hour = t1.hour + t2.hour
    sum.minute = t1.minute + t2.minute
    sum.second = t1.second + t2.second

    if sum.second >= 60:
        sum.second -= 60
        sum.minute += 1

    if sum.minute >= 60:
        sum.minute -= 60
        sum.hour += 1

    return sum

이 함수가 옳기는 하지만, 점점 커지기 시작합니다. 뒤에서 더 짧은 대안을 보게됩니다.

수정자

[increment]

때로 함수가 인자로 전달된 객체들을 수정하는 것이 편리하기도 합니다. 그런 경우에, 변화는 호출자에게 드러납니다. 이런 식으로 동작하는 함수들을 수정자(modifier)라고 합니다.

increment, 주어진 초를 Time 객체에 더합니다,는 수정자로 자연스럽게 작성할 수 있습니다. 여기 대략적인 초안이 있습니다:

def increment(time, seconds):
    time.second += seconds

    if time.second >= 60:
        time.second -= 60
        time.minute += 1

    if time.minute >= 60:
        time.minute -= 60
        time.hour += 1

첫째줄은 기본적인 연산을 수행합니다; 나머지는 앞에서 본 특별한 경우들을 다룹니다.

이 함수는 올바릅니까? 만약 seconds가 60보다 많이 크면 어떻게 될까요?

그런 경우에, 한번만 올려주는 것으로 충분치 않습니다; time.second 가 60보다 작아질때까지 계속해야만 합니다. 한가지 해법은 if 문을 while 문으로 바꾸는 것입니다. 이 방법은 함수를 옳기는 하지만 아주 효율적이지는 않게 만듭니다.

[연습 16.3.]

순환을 포함하지 않는 increment 의 올바른 버전을 작성하세요.

수정자도 할 수 있는 모든 것은 순수 함수로도 할 수 있습니다. 사실, 어떤 프로그래밍 언어는 순수 함수만을 허락합니다. 순수 함수를 사용하는 프로그램이 수정자를 사용하는 프로그램 보다 개발이 빠르고 오류를 덜 유발한다는 약간의 증거가 있습니다. 그러나 수정자는 때때로 편리하고, 함수형 프로그램(functional program)은 덜 효율적이되 되는 경향이 있습니다.

일반적으로, 합리적인 때는 언제나 순수 함수를 작성하고 명확한 장점이 있는 경우에만 수정자에 의지하도록 권합니다. 이런 접근법을 함수형 프로그래밍 방식(functional programming style)이라고 부를 수 있습니다.

[연습 16.4.]

매개변수를 수정하는 대신 새 Time 객체를 만들어서 돌려주는 “순수” 버전의 increment를 작성하세요.

프로토타이핑 대 계획

[prototype]

제가 시연하고 있는 개발 계획을 “프로토타입과 패치(prototype and patch)”라고 부릅니다. 각 함수마다, 기본적인 계산을 수행하는 프로토타입을 작성한 후, 오류를 패치해가면서 검사했습니다.

특히 여러분에게 아직 문제에 대한 깊은 이해가 없을 때, 이런 접근법은 효과적일 수 있습니다. 그러나 점진적인 수정은 필요이상으로 —많은 특별한 경우를 다루기 때문에— 복잡하고 —모든 오류를 찾았는지 알기 어렵기 때문에— 믿을 수 없는 코드를 만들 수 있습니다.

대안은 계획된 개발(planned development)인데, 문제에 대한 높은 수준의 통찰은 프로그래밍을 아주 쉽게 만들 수 있습니다. 이 경우에, 통찰은 Time 객체가 사실 60진법의 세자리 숫자라는 것입니다(http://en.wikipedia.org/wiki/Sexagesimal를 보세요.)! second 애트리뷰트는 “1의 자리”이고, minute 애트리뷰트는 “60의 자리”이고, hour 애트리뷰트는 “360의 자리”입니다.

add_time 와 increment를 작성할 때, 결과적으로 60진법의 덧샘을 했는데, 한 자리에서 다음으로 올림을 해야만했던 이유입니다.

이 관찰은 전체 문제에 대한 다른 접근법을 제안합니다—Time 객체를 정수로 변환한 후 컴퓨터가 정수 사칙연산을 어떻게 하는지 알고있다는 사실의 장점을 취할 수 있습니다.

여기 Time 은 정수로 바꾸는 함수가 있습니다:

def time_to_int(time):
    minutes = time.hour * 60 + time.minute
    seconds = minutes * 60 + time.second
    return seconds

그리고 여기에는 정수를 Time 으로 바꾸는 함수가 있습니다(divmod가 첫번째 인자를 두번째 인자로 나누고 몫과 나머지를 튜플로 돌려준다는 것을 기억하세요).

def int_to_time(seconds):
    time = Time()
    minutes, time.second = divmod(seconds, 60)
    time.hour, time.minute = divmod(minutes, 60)
    return time

이 함수들이 올바르다는 것을 스스로에게 이해시키기 위해 약간의 숙고와 실험이 필요할 수도 있습니다. 실험하는 한가지 방법은 여러 x 값에 대해 time_to_int(int_to_time(x)) == x 인지 검사하는 것입니다. 이 것은 일관성(consistency) 검사의 한 예입니다.

일단 이 함수들이 올바르다고 이해하게되면, add_time 을 다시 작성하는데 사용할 수 있습니다:

def add_time(t1, t2):
    seconds = time_to_int(t1) + time_to_int(t2)
    return int_to_time(seconds)

이 버전은 원래 것보다 더 짧고, 검증하기 쉽습니다.

[연습 16.5.]

time_to_intint_to_time를 사용해서 increment를 다시 작성하세요.

어떤 면에서 60진법을 10진법으로 바꾸고 다시 되돌리는 것은 단지 시간을 다루는 것보다 어렵습니다. 진법 변환은 더 추상적입니다; 우리의 직관은 시간 값을 다루는 데 더 적합합니다.

그러나 만약 우리가 시간을 60진법 숫자로 다루는 통찰을 갖고 변환 함수들 (time_to_intint_to_time)을 작성하는데 투자한다면, 더 짧고 읽고 디버깅하기 쉽고 더 신뢰성있는 프로그램을 얻게됩니다.

나중에 기능을 더하는 것도 더 쉽습니다. 예를 들어, 두 개의 Time들을 빼서 둘 간의 기간을 구한다고 가정해보세요. 단순한 접근법은 빌려오기를 사용해 빼기를 구현하는 것일겁니다. 변환 함수를 사용하는 것은 더 쉽고 옳을 가능성이 더 높습니다.

얄궂게도, 때로 문제를 더 어렵게 (또는 더 일반적으로) 만드는 것이 문제를 더 쉽게 만듭니다 (특별한 경우가 더 적어지고 오류가 발생할 기회가 줄기 때문입니다).

디버깅

Time 객체는 minute 와 second의 값이 0 and 60 사이(0은 포함하고 60은 제외합니다)이고 hour가 양수일 때 규칙에 맞습니다(well-formed). hour 와 minute 는 반드시 정수값이어야 하지만, second 는 소수점 이하의 값을 갖도록 허락할 수 있습니다.

이런 종류의 요구사항들을 불변 조건(invariant)라고 부르는데, 항상 참이어야 하기 때문입니다. 달리 말하면, 참이 아닐 경우 뭔가 잘못된 것입니다.

여러분의 불변 조건을 확인하는 코드들 작성하는 것은 오류를 감지하고 원인을 찾는데 도움이 됩니다. 예를 들어, Time 객체를 받아서 불변 조건을 어기면 False 를 돌려주는 valid_time 과 같은 함수가 있을 수 있습니다:

def valid_time(time):
    if time.hour < 0 or time.minute < 0 or time.second < 0:
        return False
    if time.minute >= 60 or time.second >= 60:
        return False
    return True

그러면 각 함수의 처음에서 올바른지 확실히 하기 위해 인자들을 검사할 수 있습니다:

def add_time(t1, t2):
    if not valid_time(t1) or not valid_time(t2):
        raise ValueError, 'invalid Time object in add_time'
    seconds = time_to_int(t1) + time_to_int(t2)
    return int_to_time(seconds)

또는 assert 문장을 사용할 수 있는데, 주어진 불변 조건을 확인하고 잘못되었으면 예외를 일으킵니다:

def add_time(t1, t2):
    assert valid_time(t1) and valid_time(t2)
    seconds = time_to_int(t1) + time_to_int(t2)
    return int_to_time(seconds)

assert 문은 일반적인 조건들을 다루는 코드를 오류를 검사하는 코드와 구분시켜주기 때문에 쓸모있습니다.

용어

프로토타입과 패치 prototype and patch:
프로그램의 대략적인 초안 작성, 검사, 발견되는 오류의 수정을 수반하는 개발 계획.
게획된 개발 planned development:
문제에 대한 높은 수준의 통찰과 점진적 개발이나 프로토타입 개발보다 많은 계획을 수반하는 개발 계획.
순수 함수 pure function:
인자로 받은 어떤 객체도 수정하지 않는 함수. 대부분의 순수 함수는 결과가 있다.
수정자 modifier:
인자로 받은 하나나 그 이상의 객체들을 수정하는 함수. 대부분의 수정자들은 결과가 없다.
함수형 프로그래밍 방식 functional programming style:
대부분의 함수들이 순수 함수인 프로그램 설계 방식.
불변 조건 invariant:
프로그램이 실행되는 동안 항상 참이 유지되어야 하는 조건.

연습

이 장의 코드 예는 http://thinkpython.com/code/Time1.py에 있습니다; 이 연습의 답은 http://thinkpython.com/code/Time1_soln.py에 있습니다.

[연습 16.6.]

Time 객체와 숫자를 받아서 Time 과 숫자의 곱을 포함하는 새 Time 객체를 돌려주는 함수 mul_time 를 작성하세요.

그런 다음 경주에서 주행시간을 나타내는 Time 객체와 거리를 나타내는 숫자를 받아서 평균 패이스(마일당 시간)를 나타내는 Time 객체를 돌려주는 함수를 작성하는데 mul_time를 사용하세요.

[연습 16.7.]

datetime 모듈은 이 장에 나오는 Date 와 Time 객체와 유사한 date 와 time 객체를 제공하는데, 더 풍부한 메쏘드와 연산들을 제공합니다. http://docs.python.org/2/library/datetime.html에서 설명서를 읽으세요.

  1. datetime 모듈을 써서, 현재 날짜를 얻어서 요일을 인쇄하는 프로그램을 작성하세요.
  2. 입력으로 생일을 받아서 사용자의 나이와 다음 생일까지 남은 일, 시, 분, 초를 인쇄하는 프로그램을 작성하세요.
  3. 다른 날에 태어난 두 사람이 있을 때, 한 사람의 나이가 다른 사람의 두배인 날이 있습니다. 이 날이 그들의 더블 데이(Double Day)입니다. 두 개의 생일을 받아서 더블 데이를 계산하는 프로그램을 작성하세요.
  4. 조금 더 도전적으로, 한 사람의 나이가 다른 사람의 \(n\) 배가 되는 날을 계산하는, 더 일반화된 버전을 작성하세요.