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

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, 풀기) 또는 구조분해(destructuring)라고 부른다.

그런데 튜플을 작성할 때는 괄호를 생략할 수 있고, 특히 대입문에서 패킹과 언패킹을 수행할 때는 괄호를 생략하는 프로그래머들이 많다. 그래서 코드 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     # 나머지를 rest에 대입
>>> print(a, b, rest)
1 2 [3, 4, 5]

>>> *rest, c, d, e = numbers
>>> print(rest)
[1, 2]

>>> a, *rest, e = numbers
>>> 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

코드 5-88에서 정의한 산술평균() 함수는 앞에 별 기호가 붙은 args 매개변수를 갖고 있다. 이 함수를 호출할 때 입력한 여러 개의 데이터는 모두 튜플로 묶여 args 매개변수에 대입된다.

여러 개의 데이터를 시퀀스로 묶어 전달받는 함수에는 시퀀스를 직접 전달할 수 없다. 여러 개의 데이터를 묶어 전달받기는 편해졌지만, 반대로 시퀀스에 담긴 데이터를 전달하려면 풀어 전달해야하는 불편이 생긴 것이다. 하지만 함수를 호출할 때 전달할 시퀀스에 별 기호를 붙이면 언패킹이 수행되어 시퀀스를 간편히 전달할 수 있다.

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

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

데이터를 묶어 전달받는 함수에 필수로 입력받아야 하는 다른 매개변수가 있다면 함께 정의할 수 있다. 이 때 필수 매개변수는 나머지 데이터보다 왼쪽에 정의하는 것이 일반적이다.

코드 5-90 필수 매개변수 정의하기

>>> def 가격계산(할인율, *구매가_목록):
...     """구매가 목록을 합산하고 할인율을 반영해 가격을 계산한다."""
...     return (1 - 할인율) * sum(구매가_목록)
... 
>>> 가격계산(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시를 덧붙인다
...     if 'h' in kwargs:
...         date_string += ' ' + str(kwargs['h']) + '시'
...     
...     return date_string
... 
>>> date_to_string(1917, 10, 26, h=2)
'1917년 10월 26일 2시'

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

위 코드와 같이 데이터를 매핑(kwargs)으로 묶어 전달받을 때는 함수에서 전달받은 키가 존재하는지 확인(키 in 매핑) 후 꺼내(매핑[키]) 사용하면 된다.

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

연습문제

연습문제 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 매개변수를 추가로 전달받기 위해 패킹을 활용하라.