Devin.KR

계산 책임을 함수로 나누기

75분 안팎

학습 목표

매개변수·반환값·출력을 구분합니다.

개념

왜 계산에 이름을 붙이는가

쪽수 합계가 화면과 보고서 두 곳에서 필요하면 같은 반복을 복사할 수 있습니다. 그러나 합계 기준을 고칠 때 두 곳 중 하나를 놓치기 쉽습니다. total_pages라는 함수에 책임을 모으면 호출부는 계산의 의도를 읽고 구현은 한 곳에서 확인합니다. 함수는 긴 코드를 접는 장식이 아니라 입력과 결과의 약속입니다. 이번에는 쪽수 합계와 완료 건수를 별도 함수로 작성합니다. 두 결과의 단위가 다르므로 변수 이름도 pages와 count로 구분합니다.

매개변수와 인자를 구별한다

def total_pages(pages_list):에서 pages_list는 매개변수입니다. total_pages([0, 10, 80]) 호출의 목록은 실제 인자입니다. 함수 정의를 읽는 것만으로 본문 계산이 실행되지는 않고 호출할 때 실행됩니다. 함수가 호출되기 전에 정의가 실행되어 있어야 합니다. 함수를 만들고 아래쪽에서 입력을 읽어 호출하면 순서를 이해하기 쉽습니다. 매개변수를 두었는데 본문에서 고정된 전역 목록을 읽으면 전달한 값이 결과에 반영되지 않습니다. 빈 목록과 다른 목록으로 각각 호출해 이 문제를 찾습니다.

return과 print는 전달 방향이 다르다

return total은 호출한 곳에 계산값을 돌려주고 함수를 종료합니다. print(total)은 표준 출력으로 글자를 보낼 뿐 그 수량을 반환하지 않습니다. return 없이 끝난 함수의 반환값은 None입니다. 계산 함수 안에서 print만 한 다음 result + 1을 하면 None과 정수의 연산으로 TypeError가 날 수 있습니다. 계산 함수는 정수를 반환하고 화면 출력은 호출부에서 합니다. 그러면 같은 결과를 출력 없이 비교하거나 다른 계산에 사용할 수 있습니다.

반환 위치와 지역 누적값

total = 0을 함수 안에서 만들면 호출할 때마다 새 누적 계산을 시작합니다. return total은 반복이 끝난 뒤에 놓습니다. return을 반복 안에 넣으면 첫 원소를 처리한 직후 함수가 끝나므로 세 건의 테스트에서 누락을 발견할 수 있습니다. 빈 목록에서도 반복 밖 return이 실행되어 0을 반환합니다. 지역 변수 total은 함수 내부의 계산 이름이므로 외부에서 그 이름을 직접 읽으려 하면 NameError가 날 수 있습니다. 바깥에서는 반환값을 받아 이름을 붙입니다.

완료 건수는 명시적인 표시를 센다

완료 표시 목록은 0과 1로 주어집니다. 1은 완료, 0은 진행 중입니다. completed_count(flags)는 flag == 1인 경우에만 count를 1 증가시킵니다. 읽은 쪽수가 크다고 완료를 추측하지 않습니다. 책의 전체 분량과 완료 기준이 제공되지 않았기 때문입니다. 브라우저 입력은 n 다음에 쪽수와 완료 표시를 공백으로 구분한 n줄입니다. 한 줄을 split으로 나누고 두 값을 int로 바꿉니다. 제목은 이 실습에 없으므로 제목의 띄어쓰기를 나누는 문제가 생기지 않습니다.

함수 전달과 입력 보존

리스트 인자를 전달하면 함수의 매개변수도 그 리스트 객체를 읽을 수 있습니다. 함수라는 경계가 자동으로 복사본을 만들지는 않습니다. 따라서 pop이나 sort 같은 변경을 하면 호출자가 보던 목록도 달라질 수 있습니다. 이번 계산 함수는 원소를 읽기만 하고 지역 합계만 갱신합니다. 호출 전후 목록을 출력해 내용과 순서가 같은지 관찰합니다. 같은 목록으로 함수를 두 번 호출해 결과가 같은지도 봅니다. 깊은 복사와 참조 공유의 자세한 사례는 자료구조 모듈에서 이어갑니다.

함수 계약을 먼저 적기

total_pages의 입력은 0 이상의 정수 쪽수 목록이고 결과는 정수 합계입니다. completed_count의 입력은 정수 0과 1의 목록이고 결과는 완료 건수입니다. 두 함수 모두 표준 입력을 읽거나 화면에 출력하지 않습니다. 계약을 먼저 적으면 함수 내부에서 해야 할 일과 호출부에서 해야 할 일이 분명해집니다. 함수 이름만 같아도 인자의 의미가 다르면 그대로 바꿔 쓸 수 없습니다. 브라우저에서는 쪽수 목록을 전달하지만 미션에서는 여러 필드를 가진 기록 목록을 전달합니다. 제공된 함수의 매개변수와 호출부를 함께 읽고 어떤 원소를 받는지 확인합니다.

호출 흐름을 한 줄씩 추적하기

두 열 입력 예제에서는 먼저 쪽수와 완료 표시를 각각 목록에 모읍니다. 입력이 0 0, 10 1, 80 1이면 pages는 [0, 10, 80], flags는 [0, 1, 1]입니다. total_pages(pages)가 90을 반환한 다음 completed_count(flags)가 2를 반환하고 호출부의 print가 90 2 한 줄을 출력합니다. 함수의 정의 순서가 출력 순서를 정하는 것은 아닙니다. 실제 호출 표현식이 실행되는 순서를 따라가야 합니다. 다른 목록을 전달하면 같은 본문으로 새로운 결과를 계산하므로 데이터마다 함수를 다시 정의할 필요가 없습니다.

연속 호출로 숨은 상태 찾기

total_pages([1, 2])를 두 번 실행하면 각각 3이어야 합니다. 첫 호출에서 만든 지역 total은 다음 호출의 시작 합계가 되지 않습니다. 전역 변수에 결과를 계속 더하는 구현은 두 번째 호출이 6이 되는 등 호출 순서에 의존할 수 있습니다. 함수가 입력 목록에서 원소를 꺼내 버리면 두 번째 호출은 빈 목록을 받을 수도 있습니다. 한 번의 성공만으로 입력 보존을 판단하지 말고 같은 인자를 두 번 전달해 보세요. 서로 다른 목록도 번갈아 전달하면 매개변수를 무시하고 고정 목록을 읽는 오류를 구별할 수 있습니다.

반환 계약을 시험하는 방법

합계 함수는 빈 목록에서 0을 돌려주어야 하므로 반복이 한 번도 실행되지 않는 흐름을 따로 추적합니다. return이 반복 본문에만 있으면 빈 경우 None이 되어 후속 계산에 사용할 수 없습니다. 완료 함수는 [0, 1, 0]에서 1, [1, 1]에서 2를 반환해야 합니다. 쪽수와 완료 여부를 섞지 않도록 [0] 쪽수와 [1] 완료 표시를 함께 시험합니다. 0쪽 기록도 완료 표시가 1이면 완료 한 건입니다. 실제 단위와 반환 자료형을 함께 확인하고 호출부에서만 출력해야 다른 코드에서도 결과를 재사용할 수 있습니다.

작은 책임을 조합하기

함수를 나누는 기준은 줄 수보다 책임입니다. 쪽수 합계와 완료 건수는 모두 반복을 쓰지만 더하는 대상과 판단 조건이 다릅니다. 호출부는 두 반환값을 받아 화면의 한 줄로 조합합니다. 이때 완료 건수 함수를 쪽수 합계 함수 안에서 호출할 이유는 없습니다. 서로 독립된 계산을 유지하면 합계 오류와 완료 조건 오류를 각각 확인할 수 있습니다. 함수 안에서 입력 검증까지 새로 추가하기 전에 이미 제공된 입력부가 보장하는 범위를 읽으세요. 이번 연습에서는 정상 인자를 계산하는 책임에 집중합니다.

따라하기

함수 정의와 호출

코드를 실행해 반환값을 확인합니다.

def total_pages(values):
    total = 0
    for value in values:
        total += value
    return total

def completed_count(flags):
    count = 0
    for flag in flags:
        if flag == 1:
            count += 1
    return count
print(total_pages([0, 10, 80]))
print(completed_count([0, 1, 0]))

실행 결과

90
1

출력과 반환 비교

코드를 실행해 반환값을 확인합니다.

def show_total(values):
    print(sum(values))
result = show_total([10, 80])
print(result)

실행 결과

90
None

연속 호출과 입력 보존

코드를 실행해 반환값을 확인합니다.

def total_pages(values):
    total = 0
    for value in values:
        total += value
    return total

def completed_count(flags):
    count = 0
    for flag in flags:
        if flag == 1:
            count += 1
    return count
values = [0, 10, 80]
print(total_pages(values), total_pages([]))
print(total_pages(values), values)

실행 결과

90 0
90 [0, 10, 80]

두 열 입력

3 뒤에 쪽수·완료 표시 두 값을 공백으로 구분한 세 줄을 입력합니다.

def total_pages(values):
    total = 0
    for value in values:
        total += value
    return total

def completed_count(flags):
    count = 0
    for flag in flags:
        if flag == 1:
            count += 1
    return count
n = int(input())
pages = []
flags = []
for index in range(n):
    page, flag = input().split()
    pages.append(int(page))
    flags.append(int(flag))
print(total_pages(pages), completed_count(flags))

실행 결과

90 2

확인 문제

실습

첫 줄 n 다음 n줄에 0 이상 정수 쪽수와 완료 표시 0 또는 1이 공백으로 주어집니다. total_pages와 completed_count를 완성하고 쪽수 합계와 완료 건수를 공백으로 구분해 한 줄로 출력합니다. 두 함수는 입력을 바꾸지 않고 정수를 반환합니다.

모범 답안
def total_pages(values):
    total = 0
    for value in values:
        total += value
    return total

def completed_count(flags):
    count = 0
    for flag in flags:
        if flag == 1:
            count += 1
    return count
n = int(input())
pages = []
flags = []
for index in range(n):
    page, flag = input().split()
    pages.append(int(page))
    flags.append(int(flag))
print(total_pages(pages), completed_count(flags))

더 읽기

면접 질문

  • 매개변수와 인자의 차이는 무엇인가요?
  • return과 print의 차이 및 계산 함수에서 반환을 쓰는 이유를 설명해 주세요.
  • 같은 리스트로 함수를 두 번 호출할 때 입력 보존을 어떻게 확인하나요?