컬렉션의 요소를 변수에 대입하거나 함수에 전달할 때 편의성을 높여주는 기능을 살펴보자.

5.5.1 대입문과 시퀀스

여러 데이터를 하나의 변수에 묶어 담기

변수 하나에는 데이터 하나만을 대입할 수 있다. 변수 하나가 여러 개의 데이터를 가리키도록 하려면 어떻게 해야 할까? 이 장에서 배운 것처럼, 여러 데이터를 컬렉션에 담고 그 컬렉션을 변수에 대입하면 된다.

코드 5-78 여러 개의 데이터를 하나의 변수에 담기

>>> numbers = (1, 2, 3, 4, 5)

위 코드에서는 튜플로 데이터를 묶었는데, 이와 같이 여러 개의 데이터를 컬렉션을 이용해 하나의 변수에 묶어 담는 것을 패킹(packing, 싸기)이라고 부른다.

시퀀스의 데이터를 풀어 변수에 대입하기

반대로, 컬렉션을 풀어 요소를 여러 변수에 나눠 담아야 할 때도 있다. 이 작업을 수행하려면 다음과 같이 꺼내려는 요소의 개수만큼 여러 행의 코드를 작성해야 하여 불편하다.

코드 5-79 시퀀스의 데이터를 꺼내 변수에 대입하기

>>> a = numbers[0]
>>> b = numbers[1]
>>> c = numbers[2]
>>> d = numbers[3]
>>> e = numbers[4]

>>> print(a, b, c, d, e)
1 2 3 4 5

위 코드는 아래와 같이 간편하게 간추릴 수 있다.

코드 5-80 변수의 튜플을 이용해, 시퀀스의 데이터 나누어 대입하기

>>> (a, b, c, d, e) = numbers  # ❶ 대입문의 좌변과 우변이 모두 시퀀스

>>> print(a, b, c, d, e)
1 2 3 4 5

❶과 같이 대입문의 좌변과 우변이 둘 다 시퀀스이면, 우변의 각 요소가 좌변의 각 요소에 순서대로 대입된다. 이렇게 컬렉션의 요소를 여러 변수에 나눠 담는 방법을 언패킹(unpacking, 풀기)이라고 부른다.

그런데 튜플을 작성할 때는 괄호를 생략할 수 있고, 특히 대입문에서 패킹과 언패킹을 수행할 때는 괄호를 생략하는 것이 일반적이다. 코드 5-78의 패킹과 코드 5-80의 언패킹은 다음과 같이 괄호를 생략하고 작성하는 편이 간결하다.

코드 5-81 대입문에서는 튜플의 괄호를 생략하자

>>> numbers = 1, 2, 3, 4, 5  # 패킹
>>> a, b, c, d, e = numbers  # 언패킹

언패킹할 때, 즉 시퀀스의 요소를 변수 시퀀스에 나눠 대입할 때는 두 시퀀스의 길이가 일치해야 한다.

코드 5-82 대입문에서 양 변의 시퀀스 길이는 일치해야 한다

>>> a, b, c = numbers  # ❶ 대입문 양 변 시퀀스 길이 불일치 오류
ValueError: too many values to unpack (expected 3)

>>> a, b, c, d, _ = numbers  # ❷ 필요 없는 요소를 _ 변수에 대입
>>> a, _, _, d, _ = numbers  # ❸

❶처럼 길이가 일치하지 않으면 오류가 발생한다. 대입할 필요가 없는 요소는 ❷, ❸과 같이 자리를 메우는 변수에 대입하면 된다. 자리를 메우는 변수의 이름은 관례적으로 밑줄 기호 하나(_)로 한다.

남은 요소 대입받기

대입문에서 좌변의 변수 중 하나에 별 기호(*)를 붙이면, 남은 요소 전체를 리스트에 담아 대입한다.

코드 5-83 별 기호로 남은 요소를 대입받는 변수를 지정하기

>>> a = numbers[0]            # a = 1
>>> b = numbers[1]            # b = 2
>>> rest = numbers[2:]        # rest = (3, 4, 5)
>>> print(a, b, rest)
1 2 (3, 4, 5)

>>> a, b, *rest = numbers     # 1, 2를 제외한 나머지를 rest에 대입
>>> print(a, b, rest)
1 2 [3, 4, 5]

>>> *rest, c, d, e = numbers  # 3, 4, 5를 제외한 나머지를 rest에 대입
>>> print(rest)
[1, 2]

>>> a, *rest, e = numbers     # 1, 5를 제외한 나머지를 rest에 대입
>>> print(rest)
[2, 3, 4]

여러 행의 대입문을 하나로 줄이기

대입문의 양 변에 요소 튜플과 데이터의 튜플을 나열하면 여러 행의 대입문을 한 행으로 줄일 수 있다.

코드 5-84 여러 행의 대입문을 하나로 줄이기

>>> x = 10                # ❶ 여러 행으로 대입
>>> y = -20
>>> z = 0

>>> x, y, z = 10, -20, 0  # ❷ 한 행으로 대입

하지만 여러 개의 변수를 정의할 때 ❷ 방식보다는 각 변수에 대입되는 데이터를 쉽게 알아볼 수 있는 ❶ 방식을 사용하는 편이 일반적으로 좋다.

연습문제

연습문제 5-18 변수의 데이터 서로 교환하기

김파이 씨는 변수 x와 변수 y의 데이터를 서로 교환하여 출력하는 프로그램을 작성했다.

# 변수에 대입되어 있는 데이터
x = 10
y = -20

# 두 변수의 데이터를 서로 교환하기 (이 부분을 수정하시오)
x = y  # 변수 x에 변수 y의 데이터를 대입한다.
y = x  # 변수 y에 변수 x의 데이터를 대입한다.

# 바뀐 결과 출력
print(x)  # -20이 출력된다 (-20이 출력되어야 한다)
print(y)  # -20이 출력된다 (10이 출력되어야 한다)

그런데 프로그램을 실행해보니 두 변수의 데이터가 둘 다 -20이 되어 버렸다. 김파이 씨의 프로그램이 의도대로 동작하지 않은 이유는 무엇인가? 그 이유를 찾고 프로그램을 올바르게 수정해 보아라.

5.5.2 함수의 매개변수와 시퀀스 패킹·언패킹

시퀀스를 함수 매개변수에 전달하기

함수를 정의할 때, 인자를 전달받을 매개변수 목록을 정의하는 것을 기억할 것이다. 세 개의 매개변수를 갖는 다음 함수를 생각해 보자.

코드 5-85 여러 매개변수를 갖는 일반적인 함수의 모습

>>> def date_to_string(y, m, d):
...     """년(y), 월(m), 일(d)을 입력받아,
...        'y년 m월 d일' 형태의 문자열을 반환한다."""
...     return str(y) + '년 ' + str(m) + '월 ' + str(d) + '일'
... 

날짜 데이터를 (1917, 9, 4)와 같이 시퀀스로 정의해 두었다면, 이 함수에 어떻게 전달해야 할까? 가장 기본적인 방법은 시퀀스에서 값을 하나씩 꺼내 전달하는 것이다.

코드 5-86 시퀀스의 요소를 꺼내 함수에 전달하기

>>> date = (1917, 9, 4)
>>> date_to_string(date[0], date[1], date[2])
'1917년 9월 4일'

이 때, 언패킹을 이용하면 더 편리하게 함수에 값을 전달할 수 있다. 시퀀스의 요소를 하나씩 꺼낼 필요 없이, 시퀀스 앞에 별 기호를 붙이면 시퀀스를 풀어 전달할 수 있다. 시퀀스의 요소는 순서대로 함수의 매개변수에 대입된다.

코드 5-87 시퀀스를 풀어 함수 매개변수에 전달하기

>>> date_to_string(*date)  # ❶
'1917년 9월 4일'

>>> date_to_string(*[2001, 12, 31])  # ❷
'2001년 1월 31일'

>>> date_to_string(2017, *(9, 17))  # ❸
'2017년 9월 17일'

함수를 호출할 때 ❶과 같이 함수에 전달할 시퀀스 앞에 별 기호를 붙이면 시퀀스의 요소를 풀어 전달한다. ❷처럼 변수에 대입하지 않은 시퀀스에도 적용이 가능하며, ❸과 같이 일부 매개변수에 다른 값을 전달한 뒤 남은 매개변수에 부분적으로 적용하는 것도 가능하다.

함수에서 임의의 개수의 데이터를 전달받기

시퀀스를 풀어 매개변수에 할당하는 것과 반대로, 여러 개의 데이터를 시퀀스로 묶어 하나의 매개변수에 전달받을 수 있다. 여러 개의 데이터를 콤마로 구분해 전달받는 print() 함수가 이 기법을 활용하는 한 예다.

함수에서 데이터를 시퀀스로 묶어 전달받으려면 함수에 시퀀스를 전달받을 매개변수를 정의해 두어야 한다. 매개변수 목록을 정의할 때 시퀀스 패킹 매개변수로 사용할 변수의 이름 앞에 별 기호를 붙여 두면 된다. 시퀀스 패킹 매개변수의 이름은 자유롭게 지을 수 있는데, 인자들(arguments)을 의미하는 args라는 이름이 자주 사용된다. numbers 와 같은 좀 더 구체적인 이름을 사용하는 것도 좋다.

코드 5-88 여러 개의 데이터를 시퀀스로 묶어 전달받기

>>> def 산술평균(*args):  # ❶
...     """여러 개의 수를 전달받아 산술평균을 계산한다."""
...     if len(args) == 0:
...         return None
...     return sum(args) / len(args)
... 

>>> 산술평균()
None

>>> 산술평균(1)
1.0

>>> 산술평균(1, 2, 3, 4, 5)
3.0

❶의 *args와 같이 별 기호를 붙여 시퀀스 패킹 매개변수를 정의할 수 있다. 이제 산술평균() 함수를 호출할 때 여러 개의 인자를 전달하면 모두 튜플로 묶여 args 매개변수에 대입된다.

그런데 시퀀스 패킹 매개변수를 가진 함수에 numbers = 1, 2, 3, 4, 5와 같이 시퀀스로 묶어 놓은 데이터를 전달하려면 어떻게 해야 할까? 이 함수에는 시퀀스의 요소를 꺼내 나열하여 전달해야 한다. 요소들을 numbers에서 직접 꺼내려면 불편하지만, 앞에서 배운 방법으로 시퀀스에 별 기호를 붙이면 언패킹이 수행되어 시퀀스의 요소를 간편히 전달할 수 있다.

코드 5-89 데이터를 묶어 전달받는 함수에 시퀀스를 풀어 전달하기

>>> numbers = 1, 2, 3, 4, 5
>>> 산술평균(*numbers)
3.0

필수 매개변수와 나머지 매개변수 정의하기

시퀀스 패킹 매개변수는 다른 매개변수와 함께 정의할 수 있다. 이 떄, 다른 매개변수들은 인자를 반드시 전달해야 하는 필수 매개변수가 되고, 시퀀스 패킹 매개변수는 나머지 인자를 전달하고 남은 인자를 시퀀스로 묶어 전달받는 나머지 매개변수가 된다.

코드 5-90 필수 매개변수와 나머지 매개변수 정의하기

>>> def 가격계산(할인율, *구매가_목록):
...     """구매가 목록을 합산하고 할인율을 반영해 가격을 계산한다."""
...     return (1 - 할인율) * sum(구매가_목록)
... 

>>> 가격계산(0.25, 100)
75.0

>>> 가격계산(0.25, 100, 200, 300, 400, 200)
900.0

위 코드의 가격계산() 함수에서 할인율 매개변수는 필수 매개변수다. 그리고 그 외의 여러 개의 데이터를 구매가_목록 매개변수가 추가로 전달받을 수 있다.

이상에서 알아본 것처럼, 시퀀스 패킹과 언패킹을 활용하면 함수가 여러 개의 데이터를 유연하게 전달받도록 할 수 있다.

연습문제

연습문제 5-19 산술평균 2

산술평균을 구하는 함수 mean2()를 정의하라. 이 함수는 여러 개의 수를 각각 매개변수로 입력받아 산술평균을 계산해 반환한다. 이 함수를 정의한 뒤 다음과 같이 테스트해 보아라.

>>> mean2(10, 20, 30, 40)
25.0

>>> mean2(*[10, 20, 30, 40])
25.0

힌트: 정해지지 않은 여러 개의 매개변수를 전달받기 위해 패킹을 활용하라.

5.5.3 함수의 매개변수와 매핑 패킹·언패킹

매핑을 함수 매개변수에 전달하기

함수를 호출할 때, 매핑에 담긴 데이터도 풀어 매개변수에 분배할 수 있다. 코드 5-85에서 정의한 date_to_string() 함수에 사전으로 정의된 날짜 {'y': 1917, 'm': 9, 'd': 4}를 전달하는 방법을 생각해 보자. 먼저, 시퀀스의 데이터를 풀어 전달할 때와 마찬가지로 하나씩 직접 꺼내 전달하는 방법이 있다.

코드 5-91 매핑의 요소를 꺼내 함수에 전달하기

>>> date = {'y': 1917, 'm': 9, 'd': 4}
>>> date_to_string(date['y'], date['m'], date['d'])
'1917년 9월 4일'

그런데 이 예에서 매핑의 키(h, m, s)와 함수의 매개변수(h, m, s)가 이름이 서로 같다. 이럴 때 데이터를 풀어 이름이 같은 짝에 전달되도록 할 수 있다. 시퀀스를 풀때 별 기호(*)를 사용하는 것과 유사하게, 매핑은 별 기호 두 개(**)를 표기하여 언패킹을 할 수 있다.

코드 5-92 매핑을 풀어 함수 매개변수에 전달하기

>>> date_to_string(**date)  ❶
'1917년 9월 4일'

❶과 같이 함수에 전달하는 매핑에 별 기호를 두 개 붙이면 매핑을 풀어 매핑의 키와 함수 매개변수의 이름을 서로 짝지어 값을 전달한다. 만일 사전의 키와 함수의 매개변수가 이름이 서로 대응하지 않으면 오류가 발생한다.

코드 5-93 키와 매개변수 이름이 일치하지 않으면 오류가 발생한다

>>> date_to_string(**{'century': 20})
TypeError: date_to_string() got an unexpected keyword argument 'century'

함수에서 다양한 이름의 데이터를 전달받기

매핑을 풀어 함수의 매개변수에 할당하는 것과 반대로, 함수에 전달된 데이터를 하나의 매핑으로 묶어 전달받을 수도 있다.

함수를 호출할 때 date_to_string(y=1917, m=10, d=26)과 같이 값을 전달할 매개변수를 지정할 수 있다.(3.5 절 참고) 단, 값을 전달받을 매개변수가 함수에 정의되어 있어야 한다. 그렇지 않으면 오류가 발생한다.

코드 5-94 값을 전달받을 매개변수 지정하기

>>> date_to_string(y=1917, m=10, d=26)
'1917년 10월 26일'

>>> date_to_string(y=1917, m=10, d=26, h=3)
TypeError: date_to_string() got an unexpected keyword argument 'h'

함수에 정의되지 않은 이름을 향해 전달된 데이터도 전달받고 싶다면? 매핑으로 묶어 전달받으면 된다. 묶은 데이터를 대입할 매개변수를 미리 정의해두어야 하는데, 매핑 패킹 매개변수로 사용할 변수의 이름 앞에 별 기호 두 개(**)를 붙여 표시하면 된다. 이 변수의 이름은 자유롭게 지을 수 있지만 키 인자들(keyword arguments)이라는 의미의 kwargs라는 이름이 자주 사용된다. options 와 같은 좀 더 구체적인 이름도 좋다.

코드 5-95 매개변수 목록에 정의되지 않은 데이터를 매핑으로 묶어 전달받기

>>> def date_to_string(y, m, d, **kwargs):  # ❶
...     """날짜를 입력받아 문자열로 반환한다."""
...     date_string = str(y) + '년 ' + str(m) + '월 ' + str(d) + '일'
...     
...     # 시(h) 매개변수가 전달된 경우 문자열에 h시를 덧붙인다
...     date_string += kwargs.get('h') + '시'  # ❷
...     
...     return date_string
... 
>>> date_to_string(1917, 10, 26, h=2)
'1917년 10월 26일 2시'

❶의 **kwargs와 같이 별 기호를 두 개 붙여 매핑 패킹 매개변수를 정의할 수 있다. 매핑으로 묶은 데이터를 전달받았으면, ❷와 같이 키로 값을 꺼내 사용할 수 있다.

매개변수를 매핑으로 묶어 전달받는 방법은 함수에서 처리해야 할 매개변수의 종류가 너무 많아 매개변수 목록에 다 정의하기 힘들거나 필수 매개변수와 구분되는 선택적 매개변수를 정의할 때 자주 활용된다.

연습문제

연습문제 5-20 산술평균 3

산술평균을 구하는 함수 mean3()을 정의하라. 이 함수는 여러 개의 수를 각각 매개변수로 입력받아 산술평균을 계산해 반환한다. 그리고 이 함수를 호출할 때, round 매개변수에 반올림 자리수를 추가로 전달할 수 있다. 이 함수의 실행 결과는 다음과 같다.

>>> mean3(10, 20, 30, 40)
25.0

>>> mean3(99, 83, 45)
75.66666666666667

>>> mean3(99, 83, 45, round=2)
75.67
  • 힌트: round 매개변수를 추가로 전달받기 위해 패킹을 활용하라.