2장 - 함수
함수는 큰 프로그램을 작고 단순한 조각으로 나눌 수 있게 해준다.
함수를 사용하면 가독성이 높아지고 코드를 더 이해하기 쉬워진다.
재사용 + 리팩토링까지 가능
파이썬에서 제공하는 함수들에는 다양한 부가 기능이 있다.
이러한 부가기능들은 함수의 목적을 분명하게 하고, 불필요한 요소 제거, 호출자의 의도를 보여주며, 찾기 어려운 버그를 줄여준다.
None을 반환하기보다는 예외를 일으키자
None이 의미를 가져서 return값이달라지면
0, []등 모두 false가 뜨고 None 또한 if 문에서 모두 if문 동작이 가능하므로 구별할수없게된다. 따라서 None보다 예외를 일으키자
파이썬 프로그래머들은 유틸리티 함수를 작성할 때 반환 값 None에 특별한 의미를 부여하는 경향이 있다.
1 | # Example 1 |
그런데 분자가 0이 되어버리면 반환값도 0이 되어버린다.
즉 빈 문자열, 빈 리스트, 0이 모두 암시적으로 False로 인식 되어버린다.
오류인지 알아내려고 None 대신 실수로 False에 해당하는 값을 검사할 수 있다.(???)
1 | x, y = 0, 5 #x, y = 5,0 만 zerodivisionError로서 None받아서 not result의도함. |
위에 코드처럼 0/5도 0을 반환하면서 false취급되므로 Invalid inputs이 결과값이 되어버린다.
위에 예시는 None에 특별한 의미가 있을 떄 파이썬 코드에서 흔히 하는 실수다.
바로 이 상황이 함수에서 None을 반환하면 오류가 일어나기 쉬운 이유다.
다음 두가지 방법을 통해 이런 오류상황을 줄일수있다
첫 번쨰 방법은 반환 값을 두개로 나눠서 튜플에 담는 것이다.
처음 파라미터에서 작업을 성공했는지 알려주고, 두번째 파라미터에서 계산된 실제 결과를 반환한다.
1
2
3
4
5def divide(a, b):
try:
return True, a / b
except ZeroDivisionError:
return False, None이렇게 하면 분자가 0인 경우 True에 0값이 리턴된다.
이 함수를 호출하는 쪽에서 튜플을 풀어야한다.!!!
1
2
3
4x, y = 5, 0
success, result = divide(x, y)
if not success:
print('Invalid inputs')문제는 호출자가 (파이썬에서 사용하지 않을 변수에 붙이는 관례인 밎줄(_) 변수 이름을 사용해서) 튜플의 첫 번째 파라미터의 첫 번째 부분을 쉽게 무시할 수 있다는 점이다
얼핏 보기에는 괜찮아보이지만, 결과는 그냥 None을 반환하는 것만큼이나 나쁘다.
1
2
3
4
5
6
7
8
9
10
11
12x, y = 5, 0
_, result = divide(x, y)
if not result:
print('Invalid inputs') # This is right
x, y = 0, 5
_, result = divide(x, y)
if not result:
print('Invalid inputs')
# This is wrong, 결국 문제 또 발생 result가0이므로
Invalid inputs두 번째 방법은 절대로 None을 반환하지 않는 것이다
대신 호출하는 쪽에 예외를 일으켜서 호출하는 쪽에서 그 예외를 처리하게 하는 것이다.
여기서는 호출하는 쪽에 입력값이 잘못됏음을 알리려고 ZeroDivisionError를 ValueError로 변경하였다.
1
2
3
4
5
6def divide(a, b):
try:
return a / b
except ZeroDivisionError as e:
#오류메세지와, 에러명 변경
raise ValueError('Invalid inputs') from e이제 호출하는 쪽에서는 잘못된 입력에 대한 예외를 처리해야 한다.
(이런 동작에 대해서는 “모든 함수, 클래스, 모듈에 docstring을 작성하자” 참고)
호출하는 쪽에서 더는 함수의 반환값을 조건식으로 검사할 필요가 없다. 함수가 예외를 일으키지 않는다면 반환 값은 문제가 없다. 예외를 처리하는 코드 또한 깔끔해진다.
1
2
3
4
5
6
7x, y = 5, 2
try:
result = divide(x, y)
except ValueError: #divide함수에서 ZeroDivisionError발생시 ValueError로 뜸
print('Invalid inputs')
else:
print('Result is %.1f' % result)반드시 특별한 상황을 알릴 떄 None값을 반환하는 대신 예외를 읽으키자, 문서화 되어있다면 호출하는 코드에서 예외를 적절하게 처리할 것이다
클로저가 변수 스코프와 상호 작용하는 방법을 알자(!!!)
클로저 자세히
클로저 자세히2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 def say_words(msg):
def say_sentence():
return "와 msg변수 살아있다: %s" %(msg,)
return say_sentence #즉 내장함수가 반환됨, 지역변수 msg도 살아있음.. 이게 클로저
a = say_words("출출하다")
print(dir(a)) #dir(a) : 객체(a)가 자체적으로 가지고 있는 변수나 함수를 보여준다. 가장 큰 단위의 조사라고 보면 된다.
print(type(a.__closure__)) #dir()이라는 내장함수가 변수나 함수를 보여준다고 했으니까, 변수인지 함수인지 타입을 찍어보자.
print(a.__closure__) #cell이 뭔가 찾아보니 여러 범위(scope)에서 참조할 수 있는 값을 저장할 수 있는 객체라고 한다., <cell~~>하나밖에없으므로 [0]불러보자
print(dir(a.__closure__[0])) #마지막에 cell_contents를 숨기고 있었다.
print(a.__closure__[0].cell_contents) #클로져인 출출하다가 출력됨
>>>
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
<class 'tuple'>
(<cell at 0x10b763890: str object at 0x10b766210>,)
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'cell_contents']
출출하다클로저의 목적: 전역변수 사용 빈도를 줄이기 위함
즉, 전통적인 프로그래밍에서 한 변수를 특정 함수의 종료 여부와 관계없이 여러 함수에서 사용하려면 보통 변수를 전역변수로 선언한다. 그러나 전역변수를 무작정 생성하다 보면 간섭현상(side effect)가 발생하기 쉽다.전역 변수를 사용하면 프로그래밍 자체는 쉬워지나, 사용 빈도수가 높아지면 간섭현상이 생기면서 프로그램에 문제가 생겼을 때 디버깅 하기가 어려워진다. 함수 클로저를 만들기 위해선 내가 함수 클로저로 만들고 싶은 함수를 다른 함수로 감싸면 된다. 이때 함수 클로저를 감싸주는 함수의 반환형은 함수 클로저가 된다
전역변수, 지역변수
- 전역변수: 함수 밖, 전역 이름공간에 정의된 변수
- 지역변수: 함수 안, 지역 이름공간에 정의된 변수
- 지역변수는 그 변수가 정의된 함수 안에서만 읽을 수 있다.
- 전역변수는 프로그램 어디서든 읽을 수 있다. 단, 함수 안에서 전역변수에 새로운 값을 대입할 수는 없다.
햇갈리지말아야할것
1
2
3
4
5
6
7
8
9
10
11
12
13 #cat함수의 스코프 차이로 발생하는 것, cat2 스코프에서는 변수가 정의되어버림 (값변경x)
def cat():
rhdiddl = True
def cat2():
rhdiddl = False
return cat2(), rhdiddl
a= cat()
print(a)
(None, True)
#함수안에 for, if 문이 끝나서 변수 변경은 해당함수 스코프 안에있으므로 변수 값 변경으로 처리됨.
숫자 리스트를 정렬할 때 특정 그룹의 숫자들이 먼저 오도록 우선순위를 매기려고 한다.
이런 패턴은 사용자 인터페이스를 표현하거나, 다른 것보다 중요한 메세지나 예외 이벤트를 먼저 보여줘야 할 떄 유용하다.
이렇게 만드는 일반적인 방법은 리스트의 sort 메서드에 헬퍼 함수를 key 인수로 넘기는 것이다. 헬퍼의 반환 값은 리스트에 있는 각 아이템을 정렬하는 값으로 사용된다. 헬퍼는 주어진 아이템이 중요한 그룹에 있는지 확인하고 그에 따라 정렬 키를 다르게 할 수 있다.
1 | def sort_priority(values, group): |
1 | numbers = [8, 3, 1, 2, 5, 4, 7, 6] |
함수가 예상대로 동작한 이유는 세 가지다.
- 파이썬은 클로저(closure)를 지원하다. 클로저란 자신이 정의된 범위(스코프)에 있던 변수를 참조하는 함수다. 바로 이 점 덕분에 helper 함수가 sort_priority의 group인수에 접근할 수 있다.(함수안에 함수에서 안에있는 함수가 변수를 쓸수있다는이야기)(지역변수는 모두 스코프되는듯???)
- 함수는 파이썬에서 일급 객체(first-class object)다. 이 말은 함수를 직접 참조하고, 변수에 할당하고, 다른 함수의 인수로 전달하고, 표현식과 if 문 등에서 비교할 수 있다는 의미다. 따라서 sort 메서드에서 클로저 함수를 key인수로 받을 수 있다
- 파이썬에서 튜플을 비교하는 특정한 규칙이 있다. 먼저 인덱스 0으로 아이템을 비교하고 그 다음으로 인덱스 1, 다음은 인덱스 2와 같이 진행한다. helper 클로저의 반환 값이 정렬 순서를 분리된 두 그룹으로 나뉘게 한 건이 규칙 떄문이다. (여기서 말하는 인덱스는 튜플 안에 왼쪽부터 인덱스 0라고 부르는듯.)
함수에서 우선순위가 높은 아이템을 발견했는지 여부를 반환해서 사용자 인터페이스 코드가 그에 따라 동작하게 하면 좋을 것이다.
이런 동작을 추가하는 일은 쉬워 보인다. 이미 각 숫자가 어느 그룹에 포함되어 있는지 판별하는 클로저 함수가있다.
우선순위가 높은 아이템을 발견했을때 플래그(아래 예시에서 found객체)를 뒤집는 데도 클로저(현재 자신이 정의된 범위(스코프)에 있던 변수를 참조하는 해서 변경역할하는 함수임)를 사용하는 건 어떨까? 그러면 함수는 클로저가 수정한 플래그 값을 반환할 수 있다.
1 | def sort_priority2(numbers, group): |
사용해보자
1 | found = sort_priority2(numbers, group) |
왜 found값이 false가 나와버렸을까?
표현식에서 변수를 참조할 때 파이썬 인터프리터는 참조를 해결하려고 다음과 같은 수서로 유효 범위(scope)를 탐색한다.
- 현재 함수의 스코프
- (현재 스코프를 담고있는 다른 함수 같은) 감싸고 있는 스코프
- 코드를 포함하고 있는 모듈의 스코프(전역 스코프라고도함)
- (len이나 str 같은 함수를 담고 있는) 내장 스코프
이 주 어느 스코프에도 참조한 이름으로 된 변수가 정의되어 있지 않으면 NameError 예외가 일어난다
변수에 값을 할당할 때는 다른 방식으로 동작한다. 변수가 이미 현재 스코프에 정의되어 있다면 새로운 값을 얻는다
__파이썬은 변수가 현재 스코프에 존재하지 않으면 변수 정의로 취급한다. __
새로 정의되는 변수의 스코프는 그 할당을 포함하고 있는 함수가 된다.
(즉, 새로 정의된 변수는 정의되었던 스코프밖을 나가지못함, 지금은 found=True가 helper함수 밖을 나가지 못함 )
즉, sort_priority2함수의 반환 값이 잘못된이유는 found변수는 helper 클로저에서 True로 할당되도록 설계하려고 햇으나, 실제로 클로저 할당은 sort_priority2에서 일어나는 할당이 아닌 helper함수에서 일어나는 새 변수 정의가 되어버렸기때문이다.
이런 점은 초보자들을 놀라게한다. (‘스코프 버그’라고함)
하지만 이 동작은 함수의 지역 변수가 자신을 포함하는 모듈을 오염시키는 문제를 막아준다. 그렇지 않았다면 함수 안에서 일어나는 모든 할당이 전역 모듈 스코프에 쓰레기를 넣는 결과로 이어졌을 것이다.
그렇게 되면 불필요한 할당에 그치지 않고 결과로 만들어지는 전역 변수들의 상호 작용으로 알기 힘든 버그가 생긴다.
데이터 얻어오기
파이썬 3에는 클로저에서 >>> 데이터를 얻어오는 특별한 문법이 있다. nonlocal문은 특정 변수 이름에 할당할 때 __스코프 탐색이 일어나야 함을 나타낸다. __(이렇게 하면 탐색후 할당)
유일한 제약은 nonlocal이 (전역 변수 오염 피하려고) 모듈 수준 스코프까지는 탐색할 수 없다는 점이다.
다음은 nonlocal을 사용하여 같은 함수를 다시 정의한 예다.
(nonlocal문을 사용하여 클로저를 감싸는 스코프의 변수를 수정할 수있음을 알린다.)
1 | def sort_priority3(numbers, group): |
nonlocal 문은 클로저에서 데이터를 다른 스코프에 할당하는 시점을 알아보기 쉽게 해준다.
nonlocal문은 변수 할당이 모듈 스코프에 직접 들어가게 하는 global문을 보안한다(모듈 스코프는안됨.)
하지만 전역 변수의 안티패턴(anti-pattern)과 마찬가지로 간단한 함수 이외에는 nonlocal을 사용하지 않도록 주의해야 한다. nonlocal의 부작용은 알아내기가 상당히 어렵다.
특히 nonlocal문과 관련 변수에 대한 할당이 멀리 떨어진 긴 함수에서는 이해하기가 더욱 어렵다
nonlocal을 사용할 때 복잡해지기 시작하면 헬퍼 클래스로 상태를 감싸는 방법을 이용하는 게 낫다.
이제 nonlocal을 사용할 때와 같은 결과를 얻는 클래스를 정의해 보자
코드는 약간 더 길지만 이해하기는 훨씬 쉽다(“인터페이스가 간단하면 클래스 대신 함수를 받자”에서 __call__ 이라는 특별한 메서를 자세히 설명한다.)
1 | class Sorter(object): |
파이썬2의 스코프
nonlocal지원안함.. pass
found = [False]로 줘서 수정가능한 helper함수안에 found[0] = Ture를 하면 탐색이 일어나므로 변경은 가능….
list 본체 정렬
- reverse : 리스트를 거꾸로 뒤집는다. desc 정렬이 아님
1 | 1, 10, 5, 7, 6] a = [ |
- sort : 정렬, 기본값은 오름차순 정렬, reverse옵션 True는 내림차순 정렬
1 | 1, 10, 5, 7, 6] a = [ |
- sort의 key 옵션, key 옵션에 지정된 함수의 결과에따라 정렬, 아래는 원소의 길이
1 | '나는 파이썬을 잘하고 싶다' m = |
list 정렬된 결과 반환
- 정렬된 결과를 반환하는 함수는 본체는 변형하지 않습니다.
- sorted : 순서대로 정렬, 정렬된 리스트를 반환
1 | >>> x = [1 ,11, 2, 3] |
- reversed : 거꾸로 뒤집기, iterable한 객체를 반환, 확인을 위해서는 list로 한번 더 변형 필요
1 | >>> x = [1 ,11, 2, 3] |
리스트를 반환하는 대신 제너레이터를 고려하자(B16)
일련의 결과를 생성하는 함수에서 택할 가장 간단한 방법은 아이템의 리스트를 반환하는 것이다.
예를 들어 문자열에 있는 모든 단어의 인덱스를 출력하고 싶다(띄어쓰기를 기준으로 나누면됨)
다음 코드에서 append 메서드로 리스트에 결과들을 누적하고 함수가 끝날 때 해당 리스트를 반환한다.
1 | def index_words(text): |
샘플 입력이 몇 개뿐일 때는 함수가 기대한 대로 동작한다.
1 | address = 'Four score and seven years ago our fathers brought forth on this continent a new nation, conceived in liberty, and dedicated to the proposition that all men are created equal.' |
하지만 index_words 함수에는 두 가지 문제가 있다.
첫 번째 문제는 코드가 약간 복잡하고 깔끔하지 않다는 것이다. 새로운 결과가 나올 때마다 append 메서드를 호출해야 한다. 메서드 호출(result.append)이 많아서 리스트에 가하는 값(index+1)이 덜 중요해 보인다. 결과 리스트를 생성하는 데 한 줄이 필요하고, 그 값을 반환하는 데도 한 줄이 필요하다. 함수 몸체에 문자가 130개 가량(공백 제외)있지만 그중에서 중요한 문자는 약 75개다
이 함수를 작성하는 더 좋은 방법은 제너레이터(generator)를 사용하는것이다
제너레이터는 yield 표현식을 사용하는 함수다.
__제너레이터 함수는 호출되면 실제로 실행하지 않고 바로 이터레이터(iterator)를 반환한다. __
내장 함수 next를 호출할 때마다 이터레이터는 제너레이터가 다음 yield 표현식으로 진행하게 한다.
제너레이터에서 yield에 전달한 값을 이터레이터86가 호출하는 쪽으로 반환한다.
동일결과를 내는 제너레이터 함수
1 | def index_words_iter(text): |
제너레이터는 자신이 리턴할 모든 값을 메모리에 저장하지 않기 때문에 조금 전 일반 함수의 결과와 같이 한번에 리스트로 보이지 않는 것입니다. 제너레이터는 한 번 호출될때마다 하나의 값만을 전달(yield)합니다. 즉, 위의 #1까지는 아직 아무런 계산을 하지 않고 누군가가 다음 값에 대해서 물어보기를 기다리고 있는 상태입니다.
결과 리스트와 연동하는 부분이 사라져서 훨씬 이해하기 쉽다. 결과는 리스트가 아닌 yield 표현식으로 전달된다.
제너레이터 호출로 반환되는 이터레이터를 내장 함수 list에 전달하면 손쉽게 리스트로 변환할 수 있다.(B9 참조)
1 | myresult=list(index_words_iter('rhdiddlsms 최고다 rhddd')) |
두번째 문제는
반환하기 전에 모든 결과를 리스트에 저장해야한다는 점이다. 입력이 매우 많다면 프로그램 실행 중에 메모리가 고갈되어 동작을 멈추는 원인이 된다. 반면 제너레이터로 작성한 버전은 다양한 길이의 입력에도 쉽게 이용가능
다른 예시로 또 차이를 보자
아래 함수는 파일에서 입력을 한 번에 한 줄씩 읽어서 한 번에 한 단어씩 출력을 내어주는 제너레이터다. 이 함수가 동작할 때 사용하는 메모리는 입력 한 줄의 최대 길이까지다.
1 | def index_file(handle): |
islice는 이터레이너 객체가 반환하는 제너레이터를 처음(0), 마지막(3)으로 슬라이스침
이터레이터는 반복가능한 객체를 말한다(next()가 가능한 객체)
제너레이터(반복가능한 객체를 만들어주는 행위)
yield(제너레이터에서의 return과 동일한 역할을 수행,, )
이와 같이 제너레이터를 정의할 때 알아둬야 할 단 하나는 반환되는 이터레이터에 상태가 있고 재사용할 수 없다는 사실을 호출하는 쪽에서 알아야 한다는 점이다(B17 참조)
정리
- 제너레이터에서 반환한 이터레이터는 제너레이터 함수의 본문에 있는 yield 표현식에 전달된 값들의 집합이다.
- 제너레이터는 모든 입력과 출력을 메모리에 저장하지 않으므로 입력값의 양을 알기 어려울 때도 연속된 출력을 만들 수 있다.
인수를 순회할 때는 방어적으로 하자(B17)
파라미터로 객체의 리스트를 받는 함수에서 리스트를 여러 번 순회해야 할 때가 종종있다.
예를 들어 미국 텍사스주의 여행자 수를 분석하자
데이터 집합은 각 도시의 방문자수라고 하자
각 도시에서 전체 여행자 중 몇 퍼센트를 받아들이는지 알고 싶을 것이다
이런 작업을 하려면 정규화 함수가 필요하다. 정규화 함수에서는 입력을 합산해서 연도별 총 여행자 수를 구한다.
그러고 나서 각 도시의 방문자 수를 전체 방문자 수로 나누어 각 도시가 전체에서 차지하는 비중을 알아낸다.
1 | def normalize(numbers): |
방문 리스트를 확대하려면 모든 도시가 들어 있는 파일에서 데이터를 읽어야한다.
모든 데이터의 리스트를 메모리에 넣는건 무리..
나중에 같은 함수를 재사용하여 더 큰 데이터 세트인 전 세계의 여행자 수를 계산할 수 있기 때문에 리스트보다 제너레이터로구현한다
1 | def read_visits(data_path): |
1 | it = read_visits('my_numbers.txt') |
위 함수를 해보면 안된다. 그 이유는 이터레이터가 결과를 한 번만 생성하기 때문이다
이미 StopIteration 예외를 일으킨 이터레이터나 제너레이터를 순회하면 어떤 결과도 얻을수없다. 아래 참조하자
1 | it = read_visits('my_numbers.txt') |
이를 해결하려면 입력 이터레이터를 명시적으로 소진하고 전체 콘텐츠의 복사본을 리스트에 저장해야 한다.
(없어지는 값을 일단 복사)
1 | def normalize_copy(numbers): |
이 방법의 문제점은 입력받은 이터레이터 콘텐츠의 복사본이 클 수도 있다는 점이다
이런 이터레이터를 복사하면 프로그램의 메모리가 고갈되어 동작을 멈출 수도있다.
이런 문제를 피하는 한가지 방법은 호출될 때마다 새 이터레이터를 반환하는 함수를 받게 만드는 것이다
1 | def normalize_func(get_iter): |
normalize_func
을 사용하려면 제너레이터를 호출해서 매번 새 이터레이터를 생성하는 람다 표현식을 넘겨주면 된다.
(위에 나왔던방식은 이터레이터가 한번만 소모될수있었으므로 안됬으니까 이렇게 함수안에서 호출을 두번 새롭게 해주면된다.)
1 | percentages = normalize_func(lambda: read_visits(path)) |
코드가 잘 동작하긴 하지만, 이렇게 람다 함수를 넘겨주는 방법은 세련되지 못하다. 같은 결과를 얻는 더 좋은 방법은 이터레이터 프로토콜을 구현한 새 컨테이너 클래스를 제공하는 것이다.
이터레이터 프로토콜은 파이썬의 for 루프와 관련 표현식이 컨테이너 타입의 콘텐츠를 탐색하는 방법을 나타낸다
파이썬은 for x in foo
같은 문장을 만나면 실제로는 iter(foo)를 호출한다. 그러면 내장 함수 iter는 특별한 메서드인 foo.__iter__ 를 호출한다. __iter__ 메서드는 (__next__ 라는 특별한 메서드를 구현하는) 이터레이터 객체를 반환해야 한다. 마지막으로 for 루프는 이터레이터를 모두 소진할 때까지 (그래서 StopIteration 예외가 발생할 때까지) 이터레이터 객체에 내장 함수 next
를 계속 호출한다. 자세히
1 | #__iter__ 써보자 |
복잡해 보이지만 사실 클래스의 __iter__ 메서드를 제너레이터로 구현하면 이렇게 동작을 만들 수 있다.
다음은 여행자 데이터를 담은 파일을 읽는 이터러블(iterable:순회가능)컨테이너 클래스다
1 | class ReadVisits(object): #object는 필요없지안나??? |
새로 정의한 컨테이너 타입은 원래의 함수에 수정을 가하지 않고 넘겨도 제대로 동작한다.
1 | visits = ReadVisits("rhdiddl.txt") |
normalize를 쓸수있는 이유는 normalize의 sum메서드가 새 이터레이터 객체를 할당하려고 ReadVisits.__iter__ 를 호출하기때문이다. (새 이터레이터 만듬)
또한 숫자를 정규화하는 for 루프도 두 번째 이터레이터 객체를 할당할때 __iter__ 를 호출한다. 따라서 두 이터레이터는 독립적으로 동작하므로 각각의 순회 과정에서 모든 입력 데이터 값을 얻을 수 있다. 이 방법의 유일한 단점은 입력 데이터를 여러번 읽는다는 점이다
이제 ReadVisits와 같은 컨테이너의 작동방법을 안다.
파라미터가 단순한 이터레이터가 아님을 보장하는 함수를 작성할 차례다.
프로토콜에 따르면 내장 함수 iter에 이터레이터를 넘기면 이터레이터 자체가 반환된다. 반면 iter에 컨테이너 타입을 넘기면 매번 새 이터레이터 객체가 반환된다. 따라서 이 동작으로 입력값을 테스트해서 이터레이터면 TypeError를 일으켜 거부하게 만들면 된다.
1 | def normalize_defensive(numbers): |
normalize_defensive는 normalize_copy처럼 입력 이터레이터 전체를 복사하고 싶지 않지만, 입력 데이터를 여러번 순회해야 할 때, 사용하면 좋다. 이 함수는 list와 ReadVisits를 입력으로 받으면 입력이 컨테이너이므로 기대한 대로 동작한다. 이터레이터 프로토콜을 따르면 어떤 컨테이너 타입에 대해서도 제대로 동작할 것이다.
1 | visits = [15, 35, 80] |
하지만 입력이 이터러블이어도 컨테이너가 아니면 예외를 일으킨다
이 함수를 사용할때 그냥 이터레이터가 들어오면 순환하지 못하므로 그것을 방지하기위해 if절로 구별함
1 | it = iter(visits) |
정리
- 입력 인수를 여러번 순회하는 함수를 작성할 때 주의하자. 입력인수가 이터레이터라면 이상하게 동작(한번만 호출되고 사라지므로)해서 값 잃어버림
- 파이썬의 이터레이터 프로토콜은 컨테이너와 이터레이터가 내장함수 iter, next와 for 루프 및 관련 표현식과 상호작용하는 방법을 정의한다
- __iter__ 메서드를 제너레이터로 구현하면 자신만의 이터러블 컨테이너 타입을 쉽게 정의할 수있다
- 어떤 값에 iter를 두번 호출했을때 같은 결과(id,위치까지같고, 변한게없음)가 나오고 내장 함수 next로 전진시킬 수 있다면 그 값은 컨테이너가 아닌 이터레이터다.
가변 위치 인수로 깔끔하게 보이게 하자(B18)
선택적인 위치 인수(이런 파라미터의 이름을 보통 *arg라 한다. ‘star args’라고도함)를 받게 만들면 함수 호출을 더 명확하게 할 수 있고 보기에 방해가 되는 요소를 없앨 수 있다.
예를 들어 디버그 정보 몇 개를 로그남기자. 인수의 개수가 고정되어 있다면 메시지와 값 리스트를 받는 함수를 구현
1 | def log(message, values): |
로그로 남길 값이 없을 때 빈 리스트를 넘겨야 한다는 것은 불편함.
두 번째 인수를 아예 남겨주면 좋을 것이다. 파이썬에서의 *기호를 마지막 위치 파라미터 이름 앞에 붙이면된다.
로그 메세지(log함수의 message인수)를 의미하는 첫 번째 파라미터는 필수지만, 다음의 나오는 위치 인수는 몇개든 선택적이다.
1 | def log(message, *values): |
가변 개수의 위치 인수를 받는 방법에는 두 가지 문제가 있다
첫번째는 가변 인수가 함수에 전달되기에 앞서 항상 튜플로 변환된다는 점이다.(즉 값이 일단 다 풀려있어야함.이 튜플을 예시에서는 for문이 도니까,)
이는 함수를 호출하는 쪽에서 제너레이터에 *연상자를 쓰면 제너레이터가 모두 소진될 떄까지 순회됨을 의미한다. 결과로 만들어지는 튜플은 제너레이터로부터 생성된 모든 값을 담으므로 메모리를 많이 차지해서 프로그램 망가짐.
1 | def my_generator(): |
*arg를 받는 함수는 인수 리스트에 있는 입력의 수가 적당히 적다는 사실을 아는 상황에서 가장 좋은 방법이다. 이런 함수는 많은 리터럴이나 변수 이름을 한꺼번에 넘기는 함수 호출에 이상적이다. 개발자들을 편하게하고 가독성 높임
두 번째는 추후에 호출 코드를 모두 변경하지 않고서는 새 위치 인수를 추가할 수 없다는 것이다. 인수 리스트의 앞쪽에 위치 인수를 추가하면 기존의 호출 코드가 수정 없이는 이상하게 동작함(사용법까지 바꿔야함)
1 | #앞에 sequence인수추가 |
이 코드의 문제는 두 번째 호출이 sequence인수를 받지 못했기때문에 7을 message파라미터로 사용해버렸다. 이런 버그는 예외를 발생시키지 않으면 찾기 매우 어렵다. 완전히 없애려면 *arg를 받는 함수를 확장할 때 키워드 전용(*krewg)인수를 사용해야한다(B21참조)
정리
- def문에서 *arg사용시 가변 개수의 위치 인수를 받을수있다
- 제너레이터와 *연산자를 같이 사용시 메모리 부족으로 프로그램망가짐가능
- *args를 받는 함수에 새 위치 파라미터를 추가하면 찾기 어려운 버그생성가능
키워드 인수로 선택적인 동작을 제공하자 (B19)
대부분의 프로그래밍 언어처럼 파이썬에서도 함수를 호출시 인수를 위치로 전달할 수 있다.
1 | def remainder(number, divisor): |
파이썬 함수의 위치 인수를 모두 키워드로 전달도 가능하다. 이때 인수의 이름을 함수 호출의 광호 안에 있는 할당문에서 사용한다.
1 | remainder(20, 7) |
위치 인수는 반드시 키워드 인수 앞에서 먼저 지정되어야한다
1 | remainder(number=20,2) #말도안되는짓 |
키워드 인수의 유연성은 세 가지 중요한 이점이 있다.
첫번째는 코드를 보는 사람이 함수 호출을 명확하게 이해할수있다
두번째는 함수를 정의할 때 기본값을 설정할수있다.(덕분에 다들 기본값써서 간결하게 호출가능)
아래와 같이 period값을 호출시 주어지지않아도 함수는 동작할수있다.
1 | def flow_rate(weight_diff, time_diff, period=1): |
동적 기본 인수를 지정하려면 (B20참조)
세번째는 기존의 호출 코드와 호환성을 유지하면서도 함수의 파라미터를 확장할 수 있는 강력한 수단이 된다는 점이다.(*args와 큰 차이)
이러면 반대로 인수들이 깔끔해보이지 않는데 더 좋은 방법으로 키워드 전용 인수를 활용하자(B21참조)
정리
- 함수의 인수를 위치나 키워드로 지정가능
- 위치 인수만으로는 이해하기 어려울때 키워드쓰면 목적명확
- 키워드 인수에 기본값을 지정하면 호출이 쉬워짐
- 선택적인 키워드 인수는 항상 위치가 아닌 키워드로 넘겨야 한다
동적 기본 인수를 지정하려면 None과 docstring을 이용하자(B20)(???)
키워드 인수의 기본값으로 비정적타입을 사용해야 할 때가 있다.
예를 들어 이벤트 발생시각까지 포함해 로깅 메세지를 출력한다고하자
1 | from time import sleep |
위 코드에 문제는 datetime.now는 함수를 정의할 때 딱 한번만 실행되므로 타임스탬프가 동일하게 출력된다. 기본 인수의 값은 모듈이 로드될 때 한 번만 평가되며 프로그램 시작시 일어난다. 모듈이 로드된후 그때 딱 한번만 평가되고 기본 인수인 datetime.now를 다시 평가하지 않는다.
파이썬에서 우리가 기대한 결과가 나오게 하려면 기본값을 None으로 설정하고 docstring(문서화 문자열)으로 실제 동작을 문서화하는 게 관례다 (B49참조)
코드에서 인수 값으로 None이 나타나면 알맞는 기본값을 할당하면 된다
1 | from time import sleep |
기본 인수 값으로 None을 사용하는 방법은 인수가 수정가능(mutable)할 때 특히 중요하다.
예를 들어 JSON데이터로 인코드된 값을 로드한다고 해보자. 데이터 디코딩이 실패하면 기본값으로 빈 딕셔너리를 반환하려고 한다.
1 | import json |
위의 코드에는 datetime.now 예제와 같은 문제가 있다. 기본 인수 값은 (모듈이 로드될 때) 딱 한번만 평가되므로, 기본값으로 설정한 딕셔너리를 모든 decode 호출에서 공유한다. 이 문제가 나타는 오류를 보자
1 | foo = decode('bad data') |
아마 각각 단일 키와 값을 담은 서로 다른 딕셔너리 두 개를 예상했을 것이다.
하지만 하나를 수정하면 다른 하나도 수정되는 것처럼 보인다. 이런 문제의 원인은 foo와 bar 둘다 기본 파라미터와 같다는 점이다. 즉, 이 둘은 같은 딕셔너리 객체이다.
1 | assert foo is bar |
키워드 인수의 기본값을 None으로 설정하고 함수의 docstring에 동작을 문서화해서 이 문제를 고친다.
1 | def decode(data, default=None): |
위에 코드를 보면 알수있는것은 default=None이 모듈 로딩 때 객체 하나의 객체가됨. 그래서 decode(data, 1)이렇게 입력하면 1을 default=1이 그때 메모리에 할당하고, decode(data)를 하면 default=None으로 함수호출됨(메모리에 없음).
1 | foo = decode('bad data') |
정리
- 기본 인수는 모듈 로드 시점에 함수 정의 과정에서 딱 한번만 평가된다. 따라서 {},[]같은 동적 값에는 이상하게 작동가능
- 값이 동적인 키워드 인수에는 기본값으로 None을 사용하자. 그러고 나서 함수의 docstring에 실제 기본 동작을 문서화하자
키워드 전용 인수로 명료성을 강요하자 (B21)
키워드로 인수를 넘기는 방법은 파이썬 함수의 강력한 기능이다 (B19 참조)
이 덕분에 쓰임새가 분명한 코드를 작성가능
예를 들어 어떤 숫자를 다른 숫자를 나눈다고 해보자. 하지만 특별한 경우를 매우 주의해야한다. 때로는 ZeroDivisionError예외를 무시하고 무한대 값을 반환하고 싶을 수 있다. 어떨 때는 OverflowError예외를 무시하고 0을 반환하고 싶을수있다.
1 | def safe_division(number, divisor, ignore_overflow, |
위의 함수 사용은 함수 호출은 나눗셈에서 일어나는 float오버플로우를 무시하고 0을 반환한다.
1 | result = safe_division(1.0, 10**500, True, False) |
각각의 예시를 실행하면 assert가 뜨지않는다
문제는 예외 무시 동작을 제어하는 두 불 인수의 위치를 혼동하기 쉽다. 따라서 키워드 인수를 사용하자.(가독성도 높아짐)
1 | def safe_division_b(number, divisor, ignore_overflow=False, |
1 | safe_division_b(1.0, 10**500, ignore_overflow=True) |
위처럼 위치상관없이, 선택적인 동작이라서 함수를 호출하는 쪽에 키워드 인수로 의도를 명확하게 드러내라고 강요할 방법이 없다는점이 문제이다. 여전히 위치 인수로도 사용이 가능하기때문이다.
이처럼 복잡한 함수를 작성할 때는 호출하는 쪽에서 의도를 명확히 드러내는 인수를 넘기도록 요구하는 것이 좋다.
파이썬3에서는 키워드 전용 인수로 함수를 정의해서 의도를 명확히 드러내도록 요구할수있다.(키워드 전용 인수는 키워드로만 넘길 뿐, 위치로는 절대 넘길 수 없다)
다음은 키워드 전용 인수로 safe_division_c 함수를 다시 정의한 버전이다. 인수 리스트에 있는 *
기호는 위치 인수의 끝과 키워드 전용 인수의 시작을 가르킨다.
1 | def safe_division_c(number, divisor, *, |
이제 키워드 인수가 아닌 위치 인수를 사용하는 함수 호출은 동작못한다
1 | safe_division_c(1.0, 10**500, ignore_overflow=True) |
파이썬 2의 키워드 전용 인수
인수 리스트에 **
연산자를 사용해 올바르지 않은 함수 호출을 할 때 TypeError를 일으키는 방법으로 같은 동작을 만들 수 있다. 가변 개수의 위치 인수 대신에 키어드 인수를 몇 개든 받을수있다는 점만 빼면 **
연산자는 *
연산자와 비슷하다(B 18 참조 )
1 | def print_args(*args, **kwargs): |
파이썬2에서는 safe_division이 **kwargs를 받게 만들어서 키워드 전용 인수를 받게 한다. 그런 다음 pop메서드로 kwargs딕셔너리에서 원하는 키워드 인수를 꺼낸다. 키가 없을 때의 기본값은 pop메서드의 두번째 인수로 지정한다. 마지막으로 kwargs에 더는 남아 있는 키워드가 없을을 환이하여 호출하는 쪽에서 올바르지 않는 인수를 넘기지 않게 한다.
1 | def safe_division_d(number, divisor, **kwargs): |
이렇 구성하면 키워드 인수를 위로 넘기려 하면 파이썬 3와 마찬가지로 제대로 동작하지 않습니다.
정리
- 키워드 인수는 함수 호출의 의도를 더 명확하게 해준다
- 불 플래그를 여러 개 받는 함수처럼 햇갈리기 휘운 함수는 키워드 전용 인수를 사용하자
- 파이썬 3는 함수의 키워드 전용 인수 문법을 명시적으로 지원한다
- 파이썬 2에선 **kwargs를 사용하고 TypeError에외를 직접 일으키는 방법으로 함수의 키워드 전용 인수를 흉내 낼수있다