이 절에서는 컬렉션의 요소를 변수에 대입하거나 함수에 전달할 때 편의성을 높여주는 기능을 소개한다. 이 절의 내용은 잘 모르더라도 프로그램을 만들 수 있지만, 다른 사람의 코드를 잘 읽기 위해서는 이해해 두는 편이 좋다. 조금 어려울 수도 있는데, 두 번 정도 읽어 보아도 이해가 잘 되지 않는다면 일단 생략하고 다음 장을 읽기 바란다. 파이썬 프로그래밍 경험이 더 쌓였을 때 다시 읽어보면 충분히 이해될 것이다.

5.5.1 대입문과 시퀀스

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

변수 하나에는 데이터 하나만을 대입할 수 있다. 변수 하나가 여러 개의 데이터를 가리키도록 할 수 있을까? 그냥은 안 되지만, 데이터들을 컬렉션에 담고 그 컬렉션을 변수에 대입할 수는 있다. 예를 들어, 여러 개의 수를 튜플에 담고 그 튜플을 변수에 대입하는 것이다.

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

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

이와 같이 여러 개의 데이터를 컬렉션으로 묶어 변수에 대입하는 것을 패킹(packing, 싸기)이라고 부른다.

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

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

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

>>> 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

a = numbers[0], b = numbers[1]와 같은 형태의 대입문이 다섯 번 반복되었다. 요소가 많을 수록 중복된 코드를 더 많이 사용해야 한다. 이 코드는 다음과 같이 간편하게 간추릴 수 있다.

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

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

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

❶과 같이 대입문의 좌변과 우변이 둘 다 시퀀스이면, 우변의 각 요소가 좌변의 각 요소에 순서대로 대입된다. 첫번째 변수인 a에는 numbers[0]이 대입되고, 두번째 변수인 b에는 numbers[1]이 대입되는 식이다. 이와 같이 컬렉션의 요소를 여러 개의 변수에 나누어 담는 방법을 언패킹(unpacking, 풀기)이라고 부른다.

그런데 튜플을 작성할 때는 괄호를 생략할 수 있다. 특히 대입문에서 패킹과 언패킹을 수행할 때는 괄호를 생략하는 경우가 많다. 코드 5-79의 패킹과 코드 5-81의 언패킹은 다음과 같이 괄호를 생략하고 작성하는 편이 간결하다.

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

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

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

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

>>> a, b, c = numbers        # ❶ 대입문 양 변의 시퀀스의 길이가 서로 다르면 오류가 발생한다
ValueError: too many values to unpack (expected 3)

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

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

남은 요소 대입받기

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

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

>>> 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-85 여러 행의 대입문을 하나로 줄이기

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

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

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

개념 정리

  • 패킹: 여러 개의 값을 하나의 컬렉션으로 묶어 변수에 대입하는 것 (예: collection = 1, 2, 3)
  • 언패킹: 컬렉션 속의 요소들을 여러 개의 변수에 나누어 대입하는 것 (예: a, b, c = collection)
  • 언패킹을 할 때, 대입문 좌변의 변수 하나에 별 기호(*)를 붙여 다른 변수에 대입하고 남은 나머지 요소를 대입할 수 있다.

연습문제

연습문제 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-86 여러 매개변수를 갖는 일반적인 함수의 모습

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

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

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

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

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

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

>>> 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-89 여러 개의 데이터를 시퀀스로 묶어 전달받기

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

>>> mean(1)
1.0

>>> mean(1, 2, 3, 4, 5)
3.0

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

mean()처럼 시퀀스 패킹 매개변수를 가진 함수에 시퀀스의 요소를 전달할 때도 언패킹을 할 수 있다. 다음 코드를 보자.

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

>>> numbers = 1, 2, 3, 4, 5
>>> mean(numbers[0], numbers[1], numbers[2], numbers[3], numbers[4])  # ❶

>>> mean(*numbers)  # ❷
3.0

mean() 함수에 numbers 튜플의 요소를 모두 전달하려면 시퀀스의 요소를 모두 나열해야 하지만, ❷ 조금전 배운 시퀀스 언패킹을 활용하면 간편하다.

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

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

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

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

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

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

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

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

개념 정리

  • 함수를 호출할 때, 시퀀스 데이터를 *[1, 2, 3]과 같이 별 기호 하나를 붙여 넘겨줄 수 있다. 그러면 시퀀스의 각 요소가 언패킹되어 함수의 매개변수에 제각각 대입된다.
  • 함수를 정의할 때, 시퀀스 형태의 패킹 매개변수를 정의할 수 있다. 별 기호 하나를 붙여 *args와 같이 정의한다. 이 매개변수는 함수 호출시 다른 매개변수에 대입되지 못하고 남은 인자들을 튜플로 묶어 대입받는다. 정해지지 않은 여러 개의 인자를 전달받는 함수를 정의할 때 활용할 수 있다.

연습문제

연습문제 5-19 가장 큰 차이

함수 gap()을 정의하라. 이 함수는 여러 개의 수를 전달받아, 인자 중 가장 큰 수와 가장 작은 수의 차이를 반환한다. 이 함수를 정의한 뒤 다음과 같이 테스트해 보아라.

>>> gap(100)
0

>>> gap(10, 20, 30, 40)
30

>>> ages = [19, 16, 24, 19, 23]
>>> gap(*ages)
8

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

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

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

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

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

>>> 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-93 매핑을 풀어 함수 매개변수에 전달하기

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

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

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

>>> 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-95 값을 전달받을 매개변수 지정하기

>>> 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-96 매개변수 목록에 정의되지 않은 데이터를 매핑으로 묶어 전달받기

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

>>> date_to_string(1917, 10, 26)       # ❹
'1917년 10월 26일 None시'

**kwargs와 같이 별 기호를 두 개 붙여 매핑 패킹 매개변수를 정의할 수 있다. 매핑으로 묶은 데이터를 전달받았으면, ❷ 키로 값을 꺼내 사용할 수 있다. 함수를 호출할 때는 ❸ h 매개변수에 인자를 지정하여 호출할 수도 있고, ❹ 생략하여 호출할 수도 있다. ❹에서 ‘None시’라는 텍스트가 덧붙어 출력되었는데, 코드를 ‘선택적으로 실행’하는 방법(6장)을 학습하면 이런 문제를 해결할 수 있다.

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

개념 정리

  • 함수를 호출할 때, 매핑 데이터를 **{date: 1917, month: 10, day: 26}과 같이 별 기호 두 개를 붙여 넘겨줄 수 있다. 그러면 매핑의 각 키-값 쌍을 언패킹하여 키와 이름이 같은 함수의 매개변수를 찾아 값을 대입한다.
  • 함수를 정의할 때, 매핑 형태의 패킹 매개변수를 정의할 수 있다. 별 기호 두 개를 붙여 **kwargs와 같이 정의한다. 함수의 매개변수 목록에 정의되지 않은 이름으로 인자가 대입되는 경우, 패킹 매개변수에 사전으로 묶여 전달된다. 이 인자는 함수 본문에서 kwargs.get('year')와 같이 선택적으로 꺼내 사용할 수 있다.

연습문제

연습문제 5-20 문자열 연결

여러 개의 문자열을 연결해 반환하는 함수 concatenate()를 정의하라. 이 함수는 seperator라는 이름으로 구분자 문자열을 전달받을 수 있는데, 문자열을 연결할 때 구분자를 각 문자열 사이에 끼워넣어 반환한다. 예를 들면 다음과 같이 실행되어야 한다.

>>> concatenate('가난하다고', '해서', '외로움을', '모르겠는가', seperator='/')
가난하다고/해서/외로움을/모르겠는가

>>> concatenate(*'월화수목금토일', seperator=' - ')
'월 - 화 - 수 - 목 - 금 - 토 - 일'

힌트: 5.2절에서 소개한 join() 메서드를 활용하자.