[python] 인수

Hyeseong·2020년 12월 16일
1

TIL

목록 보기
3/13

인수

인수(또는 인자)는 함수의 이름에 할당되나, 인수는 변수 범위 보다는 객체 참조와 더 관련되어 있어요. 또한 파이썬은 함수에 인수가 전달되는 방식에 유연성을 주기 위해 키워드 인수, 기본값, 임의 인수 수집기(collector)와 추출기(extractor) 같은 번외의 도구들을 제공하는데, 이에 대해 예제와 함께 볼게요.

인수 - 전달 방식의 기본

  • 인수는 객체를 자동으로 지역변수 이름에 할당함으로써 전달됨
  • 함수 내에서 인수 이름에 할당하는 것은 호출자에게 영향을 주지 않는다
  • 함수 안에서 가변 객체 인수를 변경하는 것은 호출자에게 영향을 줄 수 있다
  • 불변 인수는 실질적으로 '값에 의해' 전달된다.
  • 가변 인수는 실질적으로 '포인터에 의해' 전달된다.

인수와 공유 참조

def f(a):
    a = 99
b = 88
print(b)

88

def changer(a,b):   # 인수에는 객체 참주가 할당됨
    a = 2           # 지역 이름의 값만 변경
    b[0] = 'spam'

x = 1
l = [1,2]           # 호출자
changer(x,l)        # 불변 객체와 가변 객체와 가변 객체 전달 
print(x)
print(l)                # x는 변경되지 않았지만 l은 변경됨
1
['spam', 2]
  • a는 함수 범위에서 지역 변수 이름이므로 첫 번째 할당은 호출자에게 아무런 영향을 미치지 않는다. 다만 이것은 지역 변수 a를 완전히 다른 객체를 참조 하도록 변경하는 것으로, 호출자의 범위에 있는 이름 x의 연결을 변경하는 것이 아니다.

a는 함수 범위에서 지역 변수 이름이므로 첫 번째 할당은 호출자에게 아무런 영향을 미치지 않는다. 다만 이것은 지역 변수 a를 완전히 다른 객체를 참조 하도록 변경하는 것으로, 호출자의 범위에 있는 이름 x의 연결을 변경하는 것이 아니다.

  • b 또한 지역 변수 이름이지만, 이는 가변 객체를 전달한다. 두 번째 할당은 직접 객체를 변경하기 때문에 b[0]에 할당한 결과는 함수가 반환한 후에도 L의 값에 영향을 준다
X = 1
a = X
a = 2
print(X)
# 할당은 호출자에게 아무런 영향을 주지 않아요

1

L = [1,2]
b = L
b[0] = 'spam'
print(L)
# 여기서 할당은 객체를 직접 변경하기 때문에 호출시 전달된 변수에 영향을 줌

['spam', 2]

가변객체와 불변객체

  • 가변객체 : 객체에 할당된 값을 수정할 수 있다.
  • 불변객체 : 객체에 할당된 값을 수정할 수 없다.
객체종류데이터 타입
가변(mutable)list, set, dict
불변(immutable)int, float, bool, tuple, string, unicode

가변 인수 변경 피하기

가변 인수를 직접 변경하는 것이 버그는 아니에요. 파이썬에서 인수를 전달하는 방식이에요.(많이 사용중)
인수는 보통 우리가 일반적으로 원하는 방식인 참조로 함수에 전달됨.

즉, 우리가 프로그램 내의 큰 객체들을 중간에 여러 사본을 만들지 않고 전달할 수 있으며, 우리가 원하는 대로 객체를 쉽게 업데이트할 수 있다는 것을 의미함.

그러나 만약 함수 내에서 우리가 전달한 객체에 영향을 줄 만한 직접 변경을 원하지 않는다면, 가변객체(list, set, dict)를 명시적 사본으로 만들어 버릴수 있어요. 함수 인수의 경우 우리는 호출 시에 항상 list.copy, 또는 빈 슬라이스와 같은 도구들을 이용하여 리스트를 복사 할 수 있다.

# 복사기법 1 
def changer(a,b):
    a = 2
    b[0] = 'spam'

x = 1
l = [1,2]
changer(x, l[:]) # 빈 리스트를 통해서 원본이 변경되지 않은 변수 ㅣ 
print(x)
print(l)
1
[1, 2]
# 복사기법 2
def changer(a,b):
    b = b[:]    # 입력된 리스트를 복사함으로써 호출자에 영향을 주지 않음
    a = 2
    b[0] = 'spam' # 우리가 만든 사본만 변경함
    return a,b
x = 1
l = [1,2]
print(changer(x, l))
print(x)
print(l) # 원볹 변경 X
(2, ['spam', 2])
1
[1, 2]

이 두가지 복사 기법은 함수가 객체를 변경하는 것을 막지 않아요.(변경해요)
호출자에게 영향을 주지 않을뿐.

정말로 객체의 변경을 안하고 싶다면, 함수 안에서 변경 시도시 Error를 발생하도록

객체를 불변 객체(int, float, bool, tuple, string, unicode)로 변경하면 되요.

예. 튜플은 변경 시도가 있을때 예외를 발생시켜요.

l = [1,2]
changer(x, (l,)) # 방법 하나
changer(x, tuple(l)) # 방법 둘
TypeError                                 Traceback (most recent call last)
<ipython-input-20-94be31d7f784> in <module>
      1 l = [1,2]
      2 # changer(x, (l,)) # 방법 하나
----> 3 changer(x, tuple(l)) # 방법 둘

<ipython-input-15-50f855409761> in changer(a, b)
      2     b = b[:]    # 입력된 리스트를 복사함으로써 호출자에 영향을 주지 않음
      3     a = 2
----> 4     b[0] = 'spam' # 우리가 만든 사본만 변경함
      5     return a,b
      6 x = 1
TypeError: 'tuple' object does not support item assignment

위 방법은 함수가 전달된 객체를 절대로!! 변경하지 못하도록 하기 때문에 함수가 본래 가져야 할 제약보다 더한 조치이므로 극단적이에요.

일반적으로 이 방법은 피해야해요. (다른 어떤 함수 호출이 생겨 인수를 언제 변경하게 될지 알 수 없기 때문.)

명심! 함수는 자신에게 전달된 list, dictionary,set 같은 가변 객체를 업데이트 할 수 있다는거!

만약 예기치 못한 방식으로 객체가 변경된다면 이에 대한 원인이 호출 받는 함수에 있는지 검사하고, 필요하다면 객체를 전달할 때 사본을 만들어야 한다는 것!

출력 인자와 다수의 결과값 흉내 내기

return은 어떤 종류의 객체라도 되돌려 줄 수 있기 때문에 다수의 값을 튜플 또는 다른 컬렉션 타입으로 묶어서 반환 할 수 있어요.

튜플을 반환하거나 , 호출자의 기존 인수 이름에 결과를 재할당 하는 방식으로 일부 언어에서 행하는 '참조에 의한 호출'을 흉내 낼 수 있어요.

def multiple(x,y):
    x = 2 # 지역 이름만 변경
    y = [3,4]
    return x, y # 다수의 새로운 값을 튜플로 반환
x = 1
l = [1,2]
x, l = multiple(x, l) # 결과를 호출자의 이름에 재할당
x,l
(2, [3, 4])

여기 이 코드는 두 개의 값을 반환하는 것으로 보이지만, 실제로는 하나의 값만 반환한다.(튜플 값 하나)

호출이 반환된 후에는 반환된 튜플의 구성 요소들을 unpacking 하기 위해 튜플을 할당 할 수 이썽요.

인수 매칭의 기본

  • 왼쪽부터 오른쪽으로 매칭
  • 인수 이름으로 매칭
  • 함수 호출 시 전달 되지 않는 선택적인 인수를 위한 값을 지정
    • 함수 호출 시 함수 정의보다 더 적은 수의 인수를 전달할 경우를 위해 함수는 이름 = 값 구문을 사용하여 스스로의 인수의 기본값을 명시 할 수 있다.
  • 가변 인수 모으기 : 임의의 많은 수의 위치 또는 키워드 인수 모으기
    • 함수는 하나 또는 두 개의 * 별이 붙은 문자가 있는 특별 인수를 사용하여 임의 개수의 추가적인 인수를 모을 수 있어요.
  • 가변 인수 언패킹: 임의의 많은 위치 또는 키워드 인수 전달하기
    • 또한, 호출자는 별을 이용하여 인수 컬렉션을 개별 인수들로 언패킹 할 수 있어요. 이것은 함수 헤더의 과는 정반대로 헤더에서 *는 임의의 개수의 인수를 모으는 것의 의미하지만 호출에서는 임의의 개수의 인수를 언패킹하고 그들 각각의 개별 값으로 전달하는 것을 의미해요.
  • 키워드 전용 인수: 반드시 이름으로 전달되어야만 하는 인수
    • 함수는 키워드 인수와 함께 위치가 아닌 이름으로 전달되어야 하는 인수를 명시 할 수 있어요. 이러한 인수들은 보통 실제 인수와 함께 추가로 설정 옵션을 정의해야 할 때 사용되요.

함수 인수 매칭 형식

구문위치해석
func(값)호출자일반인수: 위치에 의한 매칭
func(이름=값)호출자키워드 인수: 이름에 의한 매칭
func(*반복 객체)호출자반복객체안의 모든 객체를 개별 위치적 인수로 전달
func(**딕셔너리)호출자딕셔너리의 모든 키/값 쌍을 개별 키워드 인수로 전달
def func(이름)함수일반 인수: 위치나 이름으로 전달된 값을 매칭시킴
def func(이름=값)함수호출시 값이 전달X 기본 인수 값 사용
def func(*이름)함수나머지 위치 인수들을 하나의 튜플로 매칭하고 수집
def func(**이름)함수나머지 키워드 인수들을 하나의 딕셔너리로 매칭하고 수집
def func(*기타,이름)함수호출 시 키워드로만 전달되어야 하는 인수
def func(*기타, 이름=값)함수호출 시 키워드로만 전달되어야 하는 인수

불편한 것

인수 매칭 모드를 사용한다면 아래 순서 규치에 따라 사용해야해요.

  • 함수 호출에서 인수는 위치 인수, 그다음은 키워드 인수의 조함(이름=값)과 *반복객체 형식, 딕셔너리형식이라는 순서를 준수해야해요.

  • 함수 정의시에도 함수 호출과 동일한 순석로 규칙을 따라야해요.

함수 호출과 정의 양쪽 모두에 **args 형식이 존재한다면, 가장 마지막 위치에 두세요.만약 이 순서대로 인수를 배치하지 않으면, 인수의 조합이 모호해저 파이썬에서 구문 에러를 발생시켜요.

  1. 위치에 따라 키워드가 아닌 인수들을 할당함
  2. 매칭되는 이름에 따라 키워드 인수들을 할당.
  3. *이름 튜플에 나머지 키워드가 아닌 인수드릉ㄹ 할당
  4. **이름 딕셔너리에 나머지 키워드 인수들을 할당
  5. 함수 정의시 할당되지 않은 인수들에게는 기본값을 할당.

키워드와 기본값 예제

파이썬은 인수를 왼쪽에서 오른쪽 방향으로 매칭시켜요.

def f(a,b,c): print(a,b,c)
f(1,2,3)

키워드

여기서 더 명확하게 무엇이 어디로 갈지에 대해 설명해 줄수 있어요.

f(c=3, b=2, a=1)

시밎어 하나의 호출에 위치 인수와 키워드 인수를 조합하여 사용할 수 도 있어요. 이경우 키워드 인수가 이름에 의해 매칭되기 전에 먼저 모든 위치 인수가 배열의 왼쪽에서 오른쪽으로 매칭되어야 해요.

f(1, c=3, b=1)
# 1 2 3

특히 큰 프로그램에서라면 키워드는 호출에 사용된 데이터에 대한 라벨 역할을 해요.

func(name='이민우', age=100, job='dev')

기본값

함수 호출시에 값이 전달되지 않으면 함수가 실행되기 전에 그 인수에 기본 값을 할당해요.

def f(a, b=2, c=3): print(a,b,c)

# a에 전달값은 필수!! b와 c는 선택사항
def f(a, b=2, c=3): return (a,b,c)

print(f(1))
print(f(a=2))

아래는 기본값은 무시되고 전달된 값을 사용했네요

f(1,4)
# 1 4 3
f(1,4,5)
1,4,5

기본값이 무시되고 호출되었어요.

f(1,c=6)
# 1 2 6

키워드 인수와 기본값 조함하기

앞서 했던 내용들에 대한 종합 예제입니다.

def func(spam, eggs, toast=0, ham=0): # 처음 2개는 필수
    return (spam, eggs, toast, ham)

print(func(1,2)) 
print(func(1, ham=1, eggs=0))
print(func(spam=1, eggs=0))
print(func(toast=1, eggs=2, spam=3))
print(func(1,2,3,4))

결과

(1, 2, 0, 0)
(1, 0, 0, 1)
(1, 0, 0, 0)
(3, 2, 1, 0)
(1, 2, 3, 4)

함수 정의: 인수 수집

첫째, 함수 정의에서 매칭되지 않는 위치 인수를 하나의 튜플에 모으게 되요.

def f(*args): return args

print(type(f()))
print(f(1))
print(f(1,2,3,4))
<class 'tuple'>
(1,)
(1, 2, 3, 4)

이 함수가 호출될 때, 모든 위치 인수드릉ㄹ 하나의 새로운 튜플에 모으고 그 튜플에 가변 길이의 args를 할당해요. 이것은 일반 튜플 객체라서 인덱스가 뷰여되고 for루프를 사용할 수도 있어요.

**의 특징도 비슷한데요. 이건 키워드 인수에 대해서만 동작해요.
키워드 인수를 새로운 딕셔너리로 모으고 일반 딕셔너리 도구를 이용하여 처리할 수 있게 되요.

아래는 일반 인수, *, ** 를 결합하여 유연한 호출 방식을 구현했어요.

def f(a, *pargs, **kargs): return a, pargs, kargs

print(f(1,2,3,x=1, y=2))
(1, (2, 3), {'x': 1, 'y': 2})

사실 위와 같은 코드는 드물어요.

호출: 인수 unpacking

함수 호출에서의 * 구문은 <--> 함수수 정의와 정반대!!

def f(a,b,c,d): return a,b,c,d

args = (1,2)
args += 3,4
f(*args)
(1, 2, 3, 4)

위와 유사하게 함수 호출에서 ** 구문은 키/값들의 쌍들로 구성된 딕셔너리를 개별 키워드 인수로 언패킹함

args = {'a':1,'b':2,'c':3, }
args['d'] =4
f(**args)
(1, 2, 3, 4)
print(f(*(1,2), **{'d':4, 'c':3}))
print(f(1,*(2,3), **{'d':4} ) )
print(f(1, c=3, *(2,), **{'d':4} )  )
print(f(1, *(2,3), d=4)  )
print(f(1, *(2,), c=3, **{'d':4})  )
(1, 2, 3, 4)
(1, 2, 3, 4)
(1, 2, 3, 4)
(1, 2, 3, 4)
(1, 2, 3, 4)

함수의 일반화 적용

일부 프로그램은 사전에 함수의 이름이나 인수에 대해 모른 채 일반적인 방식으로 임의의 함수를 호출할 필요가 있다. 실제로, 특별한 가변 인수 'varargs'호출 구문의 진짜 힘은 스크립트를 작성하기 전에 함수 호출에 얼마나 많은 인수가 필요한지에 대해 알 필요가 없다는데 있다. 예를들어 로직 if 로직을 사용하여 일반적으로 여러 함수와 인수 리스트로부터 어느 함수와 인수를 사용할지 선택할 수 있으며, 그리고 이들 중 아무라도 호출할 수 있다.

if sometest:
	action, args = func1, (1,) # 이 경우, 하나의 인수로 func1 호출
else:
	action, args = func2, (1,2,3) # 여기에서는 세 개의 인수로 func2 호출
...등등...
action(*args)			# 일반적으로 처리

이 예제는 * 형식, 함수가 어떤 변수로도 참조 또는 호출 될 수 있는 객체라는 사실을 이용한다. 좀 더 일반적으로는 이 가변 인수(varargs) 호출 구문은 인수 목록을 예측할 수 없을 때 유용하다. 예를 들어 만약 사용자가 임의의 함수를 사용자 인터페이스를 통해 선택 한다면, 여러분은 스크립트 작성할 때 함수 호출을 하드코딩할 수 없을 것이다. 이 문제를 해결하기 위해 간단히 시퀀스 연산을 이용하여 인수 리스트를 만들고, 인수들을 언패킹하기 위해 별표 인수 구문으로 해당 함수를 호출할 수 있다.

def tracer(func, *pargs, **kargs):
    print('callings:', func.__name__)
    return func(*pargs, **kargs)

def func(a,b,c,d):
    return a + b + c + d
print(tracer(func, 1,2,c=3,d=4))

이 코드는 모든 함수에 첨부되어 있는 내장된 __name__ 속성을 사용하며, 별표 인수를 사용하여 추적 대상 함수의 인수를 수집하고 언패킹한다. 다른 말로 이 코드가 실행되면 tracer는 인수들을 가로챈 후 가변 인수 호출 구문을 이용하여 전달한다.

calling: func
10

키워드 전용 인수

함수 정의시 헤어 부분에 순서 규칙을 일반화하여 키워드 전용 인수를 명시할 수 있게 해준다. 키워드 전용 인수란 키워드로만 전달되어야 하는 인수로, 위치 인수로는 절대로 값이 채워질 수 없다. 만약 함수가 어떤 개수의 인수라도 처리하고 선택적인 설정 옵션을 수용하도록 만들기를 원한다면, 이방식이 유용하다.

키워드 전용 인수들은 인수 리스트에서 *args 뒤에 등장하는 지정된 인수로 작성된다. 모든 이러한 인수들은 호출에서 키워드 구문을 사용하여 전달되어야만한다.
그중 한 예로 다음에서는 a는 이름 또는 위치로 전달될 수 있으며, b는 나머지 위치 인수 모두를 모으고 c는 키워드로만 전달되어야 한다.

def kwonly(a, *b, c):
	print(a,b,c)
    
kwonly(1, 2, c=3)
kwonly(a=1, c=3)
kwonly(1, 2, 3)
1 (2,) 3
1 () 3
kwonly() missing 1 required keyword-only argument: 'c'

또 인수 리스트에 문자만을 사용하여 함수가 가변 인수 리스트를 받을 수는 없지만, 뒤에 따라나오는 모든 인수들이 키워드로 전달될 것을 기대하고 있음을 나타낼 수 있다. 다음 함수에서 a는 위치나 이름으로 전달되겟지만 b와 c는 키워드로 전달되어야 하며 나머지 위치 인수는 허용되지 않는다.

def kwonly(a, *,b, c):
	print(a,b,c)
    
kwonly(1, b=2, c=3) # 결과 1,2,3
kwonly(a=1,b=2, c=3) # 결과 1,2,3
kwonly(1, 2, 3) # TypeError kwonly() takes 1 positional argument but 3 were given
kwonly(1) # TypeError kwonly() missing 2 required keyword-only arguments: 'b' and 'c'

순서 규칙

마지막으로, 키워드 전용 인수는 별표 두 개가 아닌 별표 한 개 다음에 있어야 한다는 것에 주의!이름이 있는 인수는 임의 키워드 형식인 **args 다음에 나올 수 없으며, ** 는 인수 리스트에서 단독으로 사용될 수 없다. 이러한 시도는 구문 에러를 일으킨다.

def kwonly(a, **pargs, b, c):
# SyntaxError: invalid syntax
def kwonly(a, **, b, c):
# syntaxError: invalid syntax

함수 헤더에서 키워드 전용 인수는 **args 앞과 args 다음에 작성되어야 한다는 의미이다. args 앞에 인수 이름이 등장한다면, 이것은 아마도 키워드 전용 인수가 아니라 기본 위치 인수일 것이다.

def f(a, *b, **d, c=6): print(a,b,c,d) # 키워드 전용은 ** 앞에!
## syntaxerror: invalid syntax

def f(a, *b, c=6, **d): print(a,b,c,d) # 헤더 안에서 인수 수집

f(1,2,3,x=4, y=5) # 기본 값 사용
# 1 (2,3) 6 {'y':5, 'x':4}

f(1, 2, 3, x=4, y=5, c=7) # 기본값 무시 
# 1 (2,3) 7 {'y':5, 'x':4}

f(1, 2, 3, c=7, x=4, y=5)
# 1 (2,3) 7, {'y':5, 'x':4}

def f
def f(a, c=6, *b, **d): print(a,b,c,d)
f(1,2,3,x=4)
1 (3,) 2 {'x': 4}

실제로 유사한 순서 규칙이 함수 호출에서도 유효함. 키워드 전용 인수가 전달될 경우, 이들은 **args 형식 전에 나타나야 한다. 하지만 키워드 전용 인수는 *args앞이나 뒤에 등장할 수 있고, **args에 포함 될 수도 있다.

def f(a, *b, c=6, **d): print(a, b, c, d) # *와 **사이에 키워드 전용 인수 
f(1, *(2,3), **dict(x=4, y=5)) # 호출시 인수를 언패킹
# 1 (2, 3) 6 {'x': 4, 'y': 5}

print(f(1, *(2,3), **dict(x=4, y=5), c=7)) # **args 앞에 키워드 인수!
                                    # 3.5부터는 인수 순서 규칙의완화로 지원
print(f(1, *(2,3),c=7, **dict(x=4, y=5), )) # 기본값 무시

print(f(1,c=7, *(2,3), **dict(x=4, y=5), )) # *앞이나 뒤에 위치

print(f(1, *(2,3), **dict(x=4, y=5, c=999,z=22), )) # ** 안에 키워드 전용 인수 사용
(1, (2, 3), 6, {'x': 4, 'y': 5})
(1, (2, 3), 7, {'x': 4, 'y': 5})
(1, (2, 3), 7, {'x': 4, 'y': 5})
(1, (2, 3), 7, {'x': 4, 'y': 5})
(1, (2, 3), 999, {'x': 4, 'y': 5, 'z': 22})

Quiz

  1. 다음 코드의 결과는 무엇이고, 그 이유는 뭘까요?
def func(a, b=4, c=5): print(a,b,c)

func(1,2)
# 1 2 5

이유: 1과 2는 위치에 따라 a와 b에 전달되고 c는 호출에서 생략되어 기본값인 5가 출력됨

  1. 다음 코드의 결과는 무엇이고, 그 이유는 뭘까요?
def func(a, b, c=5): print(a,b,c)

func(1, c=3, b=2)
# 1 2 3

이유: 1 2 3이 출력되는데 1은 a에 전달되고, b와 c는 이름으로 2와 3이 전달된다.

  1. 다음 코드의 결과는 무엇이고, 그 이유는 뭘까요?
def func(a, b, c=5): return (a,b,c)

print(func(1, c=3, b=2))
# (1, 2, 3)
for i in func(1, c=3, b=2):
    print(i)
# 1
# 2
# 3
 

이유: 1 (2, 3)이 출력되며, 1은 a에 전달되고 *pargs는 남은 위치 인수를 새로운 튜플 객체로 수집함. 나머지 인치 인수를 모은 튜플을 for문을 이용하여 단계적으로 처리 가능하다.

  1. 다음 코드의 결과는 무엇이고, 그 이유는 뭘까요?
def func(a, **kargs): print(a,kargs)

func(a=1, c=3, b=2)
# 1 {'c': 3, 'b': 2}

이유: 1 {'b':2, 'c':3}을 출력하며, 1은 a에 이름으로 전달되고, **kargs는 나머지 키워드 인수를 하나의 딕셔너리로 모음. for문을 이용하여 나머지 키워드 인수를 모은 딕셔너리를 단계별로 처리할수도 있어요.

  1. 다음 코드의 결과는 무엇이고, 그 이유는 뭘까요?
def func(a, b, c=3, d=4): print(a,b,c,d)

func(1, *(5,6))
# 1 5 6 4

이유: 1 5 6 4 출력. 1은 a에 위치로 매칭, 5,6은 b와 c에 *name 위치로 매칭된다. 그리고 d는 값이 전달되지 않았으므로 기본값인 4를 가진다.

  1. 다음 코드의 결과는 무엇이고, 그 이유는 뭘까요?
def func(a, b, c):
    a = 2
    b[0] = 'x'
    c['a']='y'
l=1; m=[1]; n={'a':0}
func(l,m,n)
l,m,n
# (1, ['x'], {'a': 'y'})

이유: (1, ['x'], {'a', 'y'})를 출력한다. 함수에서 첫 번째 할당은 호출자에게 영향을 주지 않지만 다음 두개 할당은 전달된 가변 객채를 직접 변경해서 호출자에게 영향을 준다.

profile
어제보다 오늘 그리고 오늘 보다 내일...

0개의 댓글