CodeOnWeb
로그인

챕터 6. 클로저와 제너레이터(Closures & Generators)

클로저와 제너레이터에 대해 알아봅니다.

Park Jonghyun 2015/09/15, 20:13

내용

파이썬 3에 뛰어들기 (22)

챕터 -1. “파이썬 3로 뛰어들기”에서 달라진 점 챕터 0. 파이썬 설치하기 챕터 1. 첫 파이썬 프로그램 챕터 2. 고유 자료형 챕터 3. 컴프리헨션(Comprehensions) 챕터 4. 문자열(Strings) 챕터 5. 정규표현식(Regular Expressions) 챕터 6. 클로저와 제너레이터(Closures & Generators) 챕터 7. 클래스와 반복자(Classes & Iterators) 챕터 8. 고급 반복자(Advanced Iterators) 챕터 9. 단위 테스트(Unit Testing) 챕터 10. 리팩토링(Refactoring) 챕터 11. 파일 챕터 12. XML 챕터 13. 파이썬 객체 직렬화 챕터 14. HTTP 웹서비스 챕터 15. 사례 연구: chardet을 파이썬 3로 이식하기 챕터 16. 파이썬 라이브러리 패키징하기 부록 A. 2to3를 이용해서 코드를 파이썬 3로 이식하기 부록 B. 특수 함수 이름 부록 C. 이제 어디로 가야 할까요? 부록 D. 문제 해결

"내 글자는 좀 왔다갔다 해. 좋은 글자지만 왔다갔다 해서 문자가 이상한 장소로 가버려."
— 곰돌이 푸(Winnie-the-Pooh)

들어가 봅시다

저는 도서관 사서와 영문과 출신의 부모님 덕에, 항상 언어에 매혹되었습니다. 프로그래밍 언어 말고요. 아니 뭐, 프로그래밍 언어도 좋아하지만, 지금 말하는 건 그냥 언어입니다. 영어를 한 번 봅시다. 영어는 (적어도) 독일어, 프랑스어, 스페인어, 라틴어에서 단어를 빌려온 말하자면 좀 문어발 같은 언어입니다. 사실 빌려왔다기 보다는 약탈해왔다는 표현이 더 맞을지도 모르겠습니다. 조금 고상하게, 받아들였다고 하지요. 스타트랙의 보그(Borg)처럼요. 네, 저는 보그를 좋아해요.

우리는 보그다. 너희의 언어학적인 특성은 우리가 흡수할 것이다. 저항은 무의미하다.

이 챕터에서는 복수 명사를 표현하는 법을 배울겁니다. 또한, 다른 함수를 반환하는 함수와 고급 정규표현식 및 제너레이터에 대해서도 알아보겠습니다. 알단은 복수 명사를 만드는 법부터 봅시다. (정규표현식 챕터를 아직 읽지 않았다면, 지금이 읽어야할 때입니다. 이번 장에서는 여러분이 기본적인 정규표현식을 안다고 가정하고, 곧바로 좀 어려운 내용으로 들어갑니다.)

만약 여러분이 영어를 사용하는 나라에서 자랐거나 학교에서 영어 교육을 받았다면, 다음과 같은 기본적인 규칙에 익숙할 것입니다.

  • S, X, Z로 끝나는 단어 뒤에는 ES를 붙입니다. Bass는 bases, fax는 faxes, waltz는 waltzes가 됩니다.
  • H로 끝나는 단어에는 ES를 붙이지만, H가 묵음일 경우에는 그냥 S만 붙입니다. 묵음은 철자상으로 표기되긴 하지만 실제로 발음되지는 않는 음을 말합니다. 그래서 coach는 coaches, rash는 rashes이지만(CH와 SH 소리가 나지요), cheetah는 cheetahs가 됩니다(여기 h는 발음되지 않습니다).
  • I와 같은 발음으로 소리나는 Y로 끝나는 단어는 Y를 IES로 바꿉니다. Y가 모음과 결합해서 다른 소리를 낸다면 그냥 S만 붙입니다. 그래서 vacancy는 vacancies이지만, day는 days이지요.
  • 위의 경우에 해당하지 않는다면 그냥 S를 붙이고 틀리지 않았기를 바라면 됩니다.

(물론 이 규칙에 해당하지 않는 수많은 예외가 있습니다. Man/woman은 men/women이 되는데 human은 humans라고 쓰지요. Mouse는 mice이고 louse는 lice인데, house는 그냥 house입니다. Knife는 knives이고 wife도 wives이지만, lowlife는 lowlifes입니다. Sheep, deer, haiku 같이 단수와 복수가 같은 단어는 말도 마시고요.)

물론 다른 언어에서는 또 완전히 다른 문법이 있겠지요.

파이썬으로 영어 명사를 자동으로 복수화 해주는 라이브러리를 한 번 만들어 봅시다. 지금은 위의 네 가지 규칙만 고려해서 만들겁니다. 궁극적으로는 훨씬 많은 예외 처리가 필요하겠지만요.

네, 정규표현식을 또 써봅시다!

이제 우리는 단어를 가지고 놀겁니다. 적어도 영어에서 단어라 함은 문자가 나열되어 있는 것이지요. 네 가지 규칙을 이용해서 문자가 나열된 방식을 분류하고 각기 다른 방식으로 변형을 할겁니다. 정규표현식을 쓰기 딱 좋은 일 같이 들리지 않나요.

  1. 정규표현식이 바로 나왔습니다. 그런데 정규표현식 챕터에서 본 기억은 없군요. 대괄호([])는 "괄호 안의 문자 중 정확히 하나에 매치하라"는 뜻입니다. [sxz]는 "s나, x나, z" 중 하나를 매치합니다. 두 개는 안 됩니다. 그 뒤에 $는 익숙하지요. 문장의 끝을 의미합니다. 종합하면, 이 정규표현식은 noun에 담긴 문자열이 s, x, z 중 하나로 끝나는지 검사합니다.
  2. re.sub()는 정규표현식에 기반한 문자열 치환을 하는 함수입니다.

정규표현식의 치환에 대해 좀 더 알아봅시다.

  1. 문자열 Mark가 a, b, c 중 하나를 포함하고 있나요? 네. a를 포함하고 있네요.
  2. 좋습니다. 이제 a, b, c 중 하나를 찾아 그 문자를 o로 바꿔봅시다. Mark가 Mork가 되었군요.
  3. 같은 re.sub()를 rock에 적용했더니 rook이 되었습니다.
  4. 아마 caps가 oaps로 바뀔거라 생각하는 분들이 있겠지만 그렇지 않습니다. re.sub()는 첫 번째 해당되는 문자 뿐만 아니라 나머지 매치되는 문자도 모두 바꾸어 버립니다. c, a가 [abc]에 매치되므로 결과는 oops가 되지요.

이제 plural() 함수로 돌아가봅시다.

  1. 여기서는 noun 문자열의 마지막($)을 es라는 문자열로 바꾸고 있습니다. 다시 말해, 문자열의 끝에 es를 추가하는 것이지요. noun + 'es'와 같이 문자열을 서로 합쳐도 같은 결과를 얻을 수 있습니다. 하지만 저는 각 규칙을 구현하는 데 정규표현식을 쓸 겁니다. 그 이유는 곧 분명해집니다.
  2. 자세히 보세요. 새로운 표현이 또 나왔네요. 대괄호 안에 ^ 문자가 첫 번째로 나오면 "부정(negation)"이라는 특별한 의미를 가집니다. [^abc]은 "a, b, c를 제외한 아무 문자 하나"를 의미합니다. 그러니 [^aeioudgkprt]는 a, e, i, o, u, d, g, k, p, r, t가 아닌 문자 하나가 됩니다. 그리고 h가 바로 이어 나오면서 끝나는($) 부분을 찾습니다. 묵음이 아닌 H로 끝나는 단어를 찾는 것이지요.
  3. 비슷한 패턴입니다. Y로 끝나는 단어를 찾는데, Y 앞의 문자는 a, e, i, o, u가 아니어야 합니다. I와 같은 발음을 가진 Y로 끝나는 단어를 찾고 있습니다.

정규표현식의 부정(negation)에 대해 자세히 알아봅시다.

  1. vacancy는 cy로 끝나므로 이 정규표현식에 매칭됩니다. c는 a, e, i, o, u에 해당되지 않지요.
  2. boy는 oy로 끝나므로 매칭되지 않습니다. day도 마찬가지입니다. o와 a는 a, e, i, o, u 중 하나이지요.
  3. pitta는 y로 끝나지 않으므로 매칭되지 않습니다.

  1. 이 정규표현식은 vacancy를 vacancies로, agency를 agencies로 바꿔줍니다. 정확히 원하는 대로네요. boy를 바로 대입할 경우 boies로 바뀌겠지만, re.search를 먼저 실행해서 re.sub를 호출할지 말지 결정할 때 걸러지므로 문제가 없습니다.
  2. 권장하고 싶지는 않지만 re.search와 re.sub로 나누어 하던 일을 한꺼번에 합쳐서 할 수도 있다는 것을 보여드리고 싶습니다. 한 번 보시죠. 대부분은 익숙한 표현이지요? 사례 연구: 전화번호 해석하기에서 이용했던 소괄호로 묶은 그룹도 보입니다. 이렇게 그룹을 지정하면 y 바로 전의 문자가 무엇인지 기억해두고 불러올 수 있습니다. 치환될 문자열에서 이와 관련된 새로운 표현이 보이는군요. \1은 "이봐, 아까 소괄호로 묶었던 첫 번째 그룹의 내용 기억하지? 그걸 여기 대입해"라는 뜻입니다. 이 예제의 경우 y 바로 전에 c가 있으므로(그룹으로 묶어 두었기에 기억하고 있지요), vacancy의 두 번째 c 자리에 그대로 c가 들어가고 y자리에 ies가 들어갑니다. (소괄호로 묶은 그룹이 두 개 이상 있으면 \2, \3과 같은 식으로 써서 해당 위치의 그룹이 저장하고 있는 값을 읽어올 수 있습니다.)

정규표현식을 이용한 문자열 치환을 정말 강력합니다. \1과 같은 문법을 이용할 수 있다면 더 강력해집니다. 하지만 모든 연산을 하나의 정규표현식으로 축약해서 쓰면 읽기도 힘들고, 원래 의도했던 논리와 규칙을 직관적으로 표현하기도 어렵습니다. 우리는 원래 "만약 단어가 S, X, Z로 끝나면 ES를 붙인다"라는 식의 규칙을 가지고 있었지요. 이 문장을 잘 보면 두 부분으로 구성되어 있는 것을 알 수 있습니다. "만약 단어가 S, X, Z로 끝난다면", "그러면 ES를 붙여라"와 같이요. 이 두 부분을 나누어서 코딩하는 편이 보다 이해하기 쉬운 코드를 작성하는 길이겠지요.

함수의 리스트

이제 한 단계 추상적인 일을 해볼까요. 우리는 규칙을 나열하면서 이 챕터를 시작했습니다. 만약 이렇다면, 저것을 해라, 아니면 다음 규칙으로 가고. 잠시 프로그램의 일부분은 좀 복잡하게 써보겠습니다. 하지만 결과적으로는 다른 부분을 간단하게 만들어 줄겁니다.

  1. 이제 각각의 매칭 규칙은 각각의 함수로 분리되었습니다. 각 함수는 re.search() 함수의 결과를 반환합니다.
  2. 매칭이 일어났을 때 적용하는 규칙도 개별 함수로 나뉘었습니다. 조건에 맞는 복수형 단어를 만들기 위해 re.sub() 함수를 적용하는군요.
  3. 여러 규칙에 plural() 함수 하나만 쓰는 대신, 규칙의 데이터 구조를 만들었습니다. 매칭 함수와 적용 함수의 객체가 튜플로 짝을 지어 있습니다.
  4. 규칙을 데이터 구조로 만들어 규칙적으로 나누어 두었으므로, 새로운 plural() 함수는 몇 줄만 쓰면 됩니다. for 순환문을 이용하면 매칭 규칙과 적용 규칙 쌍을 한 번에 한 쌍씩 데이터 구조에서 꺼내올 수 있습니다. for 문이 처음 순환할 때 matches_rule은 match_sxz가 되고, apply_rule은 apply_sxz가 됩니다. 두 번째 순환할 때는 matches_rule이 match_h, apply_rule이 apply_h가 될 것입니다. plural() 함수는 무언가 하나는 반환하게 되어 있습니다. 마지막으로 순환할 때 match_default가 항상 True를 반환해서, 그에 짝이되는 apply_default가 적용될 수밖에 없기 때문이지요.

이런 식의 테크닉이 가능한 이유는, 함수를 포함해서 파이썬에서는 모든 것이 객체이기 때문입니다. rules 데이터 구조는 함수를 항목으로 가지고 있는데, 단순히 함수의 이름이 아닌 함수의 객체가 저장되어 있습니다. for 문에서 그 객체가 할당되면 matches_rule과 apply_rule 자체가 함수가 되어 호출할 수 있게 됩니다. for 문이 처음 순환할 때 하는 작업은, matches_sxz(noun)을 호출하고 매칭이 되는 경우 apply_sxz(noun)을 이어 호출하는 것과 동일합니다.

이런 식의 추가적인 추상화가 좀 어렵게 느껴질 수도 있는데, 함수를 죄다 펼쳐서 보면 좀 도움이 될지도 모르겠습니다. for 문을 다음과 같이 써도 같은 기능을 수행합니다.

for 문을 쓰면 plural() 함수를 간단하게 쓸 수 있습니다. 규칙을 담고 있는 rules 변수를 다른 곳에 정의하고 plural()에서는 그저 그 값을 받아와 순환하기만 하면 됩니다.

  1. 매칭 규칙을 하나 얻어옵니다.
  2. 매칭이 이루어졌나요? 그러면 적용 규칙을 부른 후에 결과를 반환합니다.
  3. 매칭되지 않았다면 다시 1번으로 돌아갑니다.

규칙을 담고 있는 rules는 어디에 어떤 방식으로든 정의될 수 있습니다. plural() 함수는 상관하지 않습니다.

그런데 이런 식으로 추상화 해서 for 문을 좀 줄여 쓰는게 꼭 필요한 일인가요? 글쎄요, 아직은 분명하지 않군요. 새로운 규칙을 추가한다고 생각해봅니다. rules 변수를 정의하지 않는 첫 번째 방법에서는 plural() 함수에 if 문을 추가해야 합니다. 추상화 된 방법을 쓰면 두 개의 함수를 추가해야 하지요. 예를 들어, match_foo()와 apply_foo()라고 합시다. 그리고 그 새로운 매칭/적용 함수의 우선순위를 다른 규칙과 비교한 후, rules의 적절한 위치에 삽입해야 하겠군요. 역시 뭐가 나은지 확실히는 모르겠네요.

하지만, 지금 본 예제는 다음 단계를 위한 주춧돌일 뿐입니다. 앞으로 계속 나가보지요.

패턴의 리스트

사실 모든 매치/적용 함수를 따로 분리해서 정의할 필요는 없습니다. 잘 보면 그 함수들은 직접 호출되지 않고, 단지 rules에 추가되어 간접적으로 호출되게 됩니다. 게다가 각 함수는 둘 중 하나의 패턴을 가지고 있습니다. 모든 매칭 함수는 re.search()를 부르고, 모든 적용 함수는 re.sub()를 호출합니다. 이 패턴을 잘 뽑아내고 버무려서 새로운 규칙을 정의하는 일을 더 쉽게 만들어 봅시다.

  1. build_match_and_apply_functions()는 다른 함수를 동적으로 생성하는 함수입니다. 패턴(pattern), 검색어(search), 단어(word)를 인자로 받아서 matches_rule() 함수를 정의하고 있습니다. matches_rule()은 그 내부에서 re.search()를 호출하는데, 이 때 build_match_and_apply_functions()의 인자로 들어온 pattern과 matches_rule 자신의 인자로 들어온 word를 넘겨줍니다. 우와. 설명이 좀 길죠.
  2. 적용 함수도 비슷한 형태를 가지고 있습니다. 적용 함수는 내부에서 re.sub()를 호출하는데, build_match_and_apply_function()의 인자로 들어온 search, replace와 apply_rule() 자신의 인자로 들어온 word를 넘겨줍니다. 이처럼 동적으로 생성되는 함수 안에서 함수 바깥에 있는 인자의 값을 사용하는 테크닉을 클로저(closure)라고 부릅니다. 실질적으로, 여러분이 생성하게 될 적용 함수 내에서 보자면 상수 두 개가 정의되어 있는 것과 마찬가지입니다. 적용 함수 자체는 하나의 인자(word)만 받아들이지만, 그 함수가 만들어질 때 이미 값이 결정되어 있는 두 가지 다른 인자(search, replace)도 같이 사용해서 함수의 전체 기능을 구성합니다.
  3. 마지막으로, build_match_and_apply_functions() 함수는 이 함수 내부에서 만들어진 두 개의 함수를 값으로 가지는 튜플을 반환합니다. 이 두 개의 함수에서 정의된 상수(matches_rule()에서는 pattern, apply_rule()에서는 search와 replace)는, build_match_and_apply_functions() 함수가 값을 반환하고 끝나더라도 계속 두 함수 내에서 존재합니다. 끝내주네요.

이 예제가 너무 헷갈린다면(분명히 그럴거에요. 이게 좀 이상하거든요) 실제 사용하는 예를 보면서 이해해봅시다.

  1. 우리의 복수화 "규칙"은 이제 함수가 아니라 세 개의 문자열을 항목으로 하는 튜플의 튜플로 정의되었습니다. 각 그룹의 첫 번째 문자열은 re.search()에 넘겨주는 매칭 규칙(pattern)을 담고 있는 정규표현식입니다. 각 그룹의 두 번째, 세 번째 문자열은 re.sub()를 호출할 때 넘겨주는 search와 replace 표현으로, 매칭이 성공했을 때 실제로 변환 규칙을 적용해서 명사를 복수형으로 바꾸는 역할을 합니다.
  2. 마지막에 약간 변화가 있네요. 이전 예제에서 match_default() 함수는 무조건 True를 반환해서, 아무 조건도 만족하지 못한 경우 그냥 주어진 문자열 마지막에 s를 붙이도록 했었죠. 이 예제의 2번 줄은 기능적으로 똑같은 일을 합니다. 마지막 정규표현식은 단어의 끝($)이 있는지 검색합니다. 물론 모든 문자열은 끝이 있습니다. 빈 문자열도요. 그러니 이 표현식은 항상 매칭되겠지요. 그래서 이 표현은 항상 참을 반환하는 match_default() 함수와 같은 목적을 가지고 있습니다. 이전의 모든 규칙에 해당하지 않는다면 주어진 문자열의 마지악에 s를 붙이는 것이죠.
  3. 마지막 줄은 거의 마법 같네요. patterns에 들어 있는 문자열 튜플을 이용해서 함수의 리스트를 만들어 냅니다. 어떻게요? 문자열 그룹을 build_match_and_apply_function() 함수에 전달해서요. 다시 말해, 세 개의 문자열로 구성된 각 튜플 항목을 build_match_and_apply_function() 함수의 인자(pattern, search, replace)로 전달하면, build_match_and_apply_function()는 그에 해당하는 두 함수를 항목으로 가지는 튜플을 반환합니다. 따라서 rules는 이전 예제의 각 함수들이 하는 일을 동일하게 수행할 수 있습니다. 튜플은 두 개의 함수 쌍을 항목으로 가지고, 그 튜플 네 개가 모여 하나의 리스트를 이룹니다. 각 튜플의 첫 번째 함수는 re.search()를 호출하는 매칭 함수이고, 두 번째 함수는 re.sub()를 호출하는 적용 함수입니다.

지금까지의 설명을 따라올 수 있었다면 plural() 함수를 이해할 수 있습니다.

  1. 여기서 rules라는 리스트는 바로 전에 본 그 rules가 맞습니다. plural() 함수가 전혀 변하지 않았다는 점에 어쩌면 놀라실 수도 있겠습니다. 완전히 일반적이죠. 규칙을 담고 있는 함수의 리스트를 읽어서 거기 담긴 함수를 차례대로 호출할 뿐입니다. 규칙이 어떻게 정의되어 있는지는 아무 상관이 없습니다. 이전 예제에서는 그 규칙이 이름을 가진 별개의 함수로 존재했죠. 이제 그 함수들은 일련의 문자열 리스트를 build_match_and_apply_functions() 함수에 넘겨줌으로써 동적으로 만들어집니다. 그렇거나 말거나 plural() 함수의 입장에서는 상관없습니다. 그냥 하던 일만 그대로 하면 되거든요.

패턴 파일

지금까지 중복되는 모든 코드를 합치고 추상화 하면서 복수화 규칙을 문자열의 리스트로 정의하였습니다. 그 다음 단계는 이 문자열을 별개의 파일로 저장해서 코드와 상관없이 유지, 보수될 수 있게 만드는 것입니다.

먼저, 여러분이 원하는 규칙을 담고 있는 텍스트 파일 하나를 만들겠습니다. 뭐 대단한 데이터 구조는 아니고요, 그저 공백을 이용해서 세 열(column)로 나뉘어져 있는 문자열 몇 개 입니다. 이 파일을 plural4-rules.txt라고 이름 짓겠습니다.

이제 이 규칙 파일을 사용하는 법을 알아봅시다.

  1. build_match_and_apply_functions() 함수는 바뀌지 않았습니다. 여전히 두 개의 함수를 동적으로 생성하기 위해 함수 밖의 변수를 이용하는 클로저를 사용하고 있습니다.
  2. 파이썬에 내장된 open() 함수는 파일 하나를 연 뒤에 파일 객체를 반환합니다. 여기서는 pattern_file이라는 이름으로 반환하네요. 이 경우, 우리가 여는 파일은 명사 복수화 규칙을 담고 있는 문자열을 포함하고 있습니다. with 구문은 컨텍스트(context)라는 것을 만듭니다. with 블록이 끝나면 파이썬은 알아서 파일을 닫습니다. 블록 안에서 예외가 발생하더라도 마찬가지입니다. 이 블록과 파일 객체에 대한 사항은 파일 챕터에서 자세히 알아볼겁니다.
  3. 파일 객체에 for 문을 쓰면 파일을 한 번에 한 줄씩 읽어서 그 내용을 line 변수에 저장합니다. 파일을 읽어오는 부분도 파일 챕터에서 자세하게 다루겠습니다. 지금 관심있는 건 그게 아니라서요.
  4. 각 줄은 원래 공백(탭이나 빈 칸)으로 나뉘어 진 세 개의 변수였습니다. 그 변수를 나누어 주기 위해 문자열이 가지고 있는 split() 함수를 이용합니다. split() 함수의 첫 번째 인자로 None을 주었는데, "아무 공백(탭, 빈 칸 아무거나 상관없습니다)이나 만나면 거기서 나누라"는 의미입니다. 두 번째 인자는 3인데, "공백을 만나서 문장 나누는 일을 최대 세 번 반복하라"는 의미입니다. 만약 공백이 세 번을 초과해서 나오는 경우, 세 번째 이후의 공백은 나눠지지 않고 그대로 보존됩니다. 예를 들어, 첫 번째 줄인 [sxz]$ $ es은 ['[sxz]$', '$', 'es']로 나누어지게 됩니다. 그래서 pattern에는 '[sxz]$]가, search에는 '$'가, replace에는 'es'가 들어가게 되겠지요. 한 줄에 참 많은 기능이 들어가 있네요.
  5. 마지막으로, pattern, search, replace를 build_match_and_apply_functions() 함수에 전달합니다. 그러면 두 함수를 항목으로 가지는 튜플이 반환되겠지요. 이 튜플을 rules라는 리스트에 추가(append())합니다. 결국 rules는 매칭/적용 함수 쌍의 리스트를 저장하게 될 것이고, 이는 바로 plural() 함수에 넘겨줄 수 있겠지요.

여기서 개선된 점은, 복수화하는 규칙을 외부 파일로 완전히 분리시켜서 코드와 상관없이 유지 가능하게 된 것입니다. 코드는 코드고, 데이터는 데이터이미, 삶은 편해집니다.

제너레이터(Generators)

rules 파일을 해석해서 처리하는 범용 plural() 함수를 작성했다는 것이 대단하게 느껴지지 않나요? 규칙을 받아서 매칭되는지 확인하고, 매칭될 경우만 적절한 변형을 가합니다. 아니면 다음 규칙으로 넘어가고요. 그것이 plural() 함수가 하는 전부이고, 또 해야만 하는 전부이지요. 전체 파일이 필요하시면 여기서 받을 수 있습니다.

아니 그런데 잠깐. 뭔가 좀 다른데, 이건 또 뭔가요? 다른 예제를 통해 한 번 살펴볼까요.

  1. make_counter에 있는 yield 키워드는 이 함수가 일반적인 함수가 아니라는 것을 뜻합니다. 이런 함수는 한 번에 하나의 값을 생성하는 특별한 종류입니다. 일시정지했다가 다시 돌릴 수 있는 함수라고 생각할 수도 있습니다(영상을 볼 때 일시정지/재생을 반복하는 경우를 생각할 수도 있습니다). 이 함수를 호출하면 제너레이터(generator)라는 것이 반환됩니다. 이 예제의 제너레이터는 하나씩 증가하는 x의 값을 생성할 수 있죠.
  2. make_counter 제너레이터의 개체를 하나 생성하려면 그냥 다른 함수처럼 호출하면 됩니다. 이 호출이 함수의 코드를 실제로 실행하지는 않는다는 점을 눈여겨 보세요. make_counter의 첫 번째 줄에 있는 print() 함수가 실행되지 않는 것으로 확인할 수 있지요.
  3. make_counter() 함수는 제너레이터 객체를 반환합니다.
  4. next() 함수는 제너레이터를 인자로 받아 그 제너레이터의 다음 값을 반환합니다. counter 제너레이터에 next()를 처음 호출하면 첫 번째 yield 문을 만날 때까지 make_counter() 함수의 코드를 실행하고 멈춘 후에 yield 뒤에 있는 값을 반환합니다. 이 경우 그 값은 2가 됩니다. 제너레이터를 만들 때 make_counter(2)라고 선언했기 때문이죠.
  5. 같은 제너레이터를 가지고 next()를 계속 호출하면 이전에 멈췄던 곳에서 코드 실행을 재개한 뒤 그 다음 yield 문을 만날 때까지 진행합니다. yield를 만나면 모든 변수와 실행 상태 등이 저장되고 다음 번 next()가 호출될 때 그대로 복구됩니다. 그래서 실행되기를 기다리고 있던 print() 함수가 호출되어 'incrementing x'를 출력합니다. 이어서 x = x + 1가 실행되겠지요. 다음 갈 곳은 while 루프의 처음인데, 가자마자 yield x를 만났습니다. 그러면 다시 모든 상태를 저장하고 x의 현재 값(3)을 반환합니다.
  6. 두 번째로 next(counter)를 호출하면 같은 일을 반복하고 이제 4가 된 x를 반환합니다.

make_counter는 무한 루프를 돌게 되어 있으므로 이론상 여러분은 이 일을 영원히 반복할 수 있습니다. x를 계속 증가시키면서 반환하겠지요. 하지만 제너레이터를 좀 더 생산적으로 쓰는 예제를 보는 게 좋겠습니다.

피보나치 제너레이터(A Fibonacci Generator)

  1. 피보나치 수열은 각 숫자가 바로 이전 두 개의 숫자를 더해서 만들어지는 수열입니다. 0과 1로 시작해서 처음에는 느리게 증가하지만 좀 지나면 빨라집니다. 수열을 시작하기 위해서는 변수 두 개가 필요합니다. a는 0에서, b는 1에서 시작합니다.
  2. a는 수열에서 현재 숫자를 표현합니다. 반환합시다.
  3. b는 수열에서 그 다음 숫자이므로 b는 a에 대입합니다. 다음 차례를 위해 (a + b)의 값도 b에 대입해 둡니다. 이 과정이 한 줄에 쓰여진 걸 보세요. 만약 a가 3이고 b가 5라면, a, b = b, a + b를 실행하면 a에는 5가(b의 이전 값이죠), b에는 8이(이전 a, b 값의 합이죠) 할당됩니다.

이제 피보나치 수열을 반환하는 함수를 가지게 되었습니다. 물론 재귀호출(recursion)을 이용할 수도 있지만 이 쪽이 읽기는 쉽네요. 물론 for 문을 써서 작성해도 상관없습니다.

  1. fib()와 같은 제너레이터를 for 루프에서 바로 쓸 수도 있습니다. for 문을 자동으로 next() 함수를 호출해서 fib() 제너레이터의 yield 값을 받아온 뒤에 인덱스 변수인 n에 할당합니다.
  2. for 루프를 돌 때마다 n에는 yield 문으로부터 새로운 값이 전달되므로, 여러분이 해야 할 일은 그저 그 값을 출력하는 것 뿐입니다. fib()가 끝나면(a가 max보다 커지는 경우겠죠) for 문도 자연스럽게 끝납니다.
  3. 아주 유용한 표현입니다. 제너레이터를 list() 함수에 던져주면 (for 문과 같이) 제너레이터 전체를 돌면서 모든 값을 차례대로 반환해줍니다.

명사 복수화 규칙 제너레이터

이제 본론으로 돌아가서 좀 바뀐 plural() 함수가 어떻게 작동하는지 살펴보겠습니다.

  1. 특별한 건 없습니다. 규칙을 담은 파일이 공백으로 구분 된 세 개의 열(column)로 구성되어 있으므로, 각 값을 pattern, search, replace에 할당하기 위해 line.split(None, 3)을 호출핼습니다.
  2. 그리고 yield 문이 나왔습니다. 무엇을 반환하나요? build_match_and_apply_functions() 함수를 통해 동적으로 생성된, 이미 우리의 친구가 되어 버려 익숙한 두 개의 함수지요. 다시 말해, rules()는 우리가 요청할 때마다 매칭/적용 함수 쌍을 하나씩 반환하는 제너레이터입니다.
  3. rules()는 제너레이터이므로, for 문에서 바로 사용할 수 있습니다. for 문을 처음 돌 때 rules()를 호출하면, 규칙이 담긴 파일을 열고 첫 줄을 읽은 후 그 정보를 이용해서 매칭/적용 함수를 동적으로 생성합니다. 그리고 그렇게 만들어진 두 함수를 yield 문을 통해 반환하고 작동을 멈추지요. for 문을 두 번째로 돌게 되면 rules()의 멈췄던 부분에서 코드 실행을 재개합니다. 처음 하는 일은 이미 열려 있는 파일의 두 번째 줄을 읽어 함수를 만드는 데 필요한 정보를 얻는 것이지요. 마찬가지로 그 규칙에 맞는 다른 매칭/적용 함수를 동적으로 생성해서 반환하고 다시 멈춥니다.

yield 문을 사용하지 않고 함수를 작성했을 때와 비교해 뭐가 더 좋은거죠? 초기 시작 시간입니다. yield를 사용하지 않은 경우, 파일에 있는 모든 패턴을 다 읽어서 가능한 모든 함수쌍을 미리 만들어 둡니다. plural() 함수를 호출 하기도 전에 말이지요. 제너레이터를 쓰면 꼭 필요하지 않은 작업은 좀 뒤에 처리할 수 있습니다. 첫 번째 규칙만 먼저 읽어와서 함수를 만들어 시도해보고, 만약 그 규칙이 잘 맞았다면 파일의 나머지 부분을 읽거나 다른 함수를 만들지 않아도 됩니다.

잃는 것은 무엇이 있을까요. 성능이죠! plural() 함수를 호출할 때마다 rules() 제너레이터는 일을 처음부터 시작해야 합니다. 패턴 파일을 다시 열고 첫 줄부터 한 줄 한 줄 읽어나가는 일을 반복하지요.

그러면 두 가지 방법의 장점만 취할 수 있다면 어떨까요. 처음 시작할 때 드는 비용이 적으면서도(모든 코드를 처음부터 다 실행하지는 않으면서) 최대의 성능을 이끌어 내는거죠(같은 함수를 매번 새로 생성하지 않게). 아, 물론 규칙은 여전히 파일에 분리해서 두고요(코드는 코드고 데이터는 데이터니까요). 그냥 같은 줄을 두 번 읽지만 않게 하는 겁니다.

이렇게 하려면 반복자(iterator)를 만들어야 합니다. 하지만 그 전에 파이썬의 클래스에 대해 좀 알 필요가 있어요.

더 읽을거리

PEP 255: Simple Generators
Understanding Python’s “with” statement
Closures in Python
Fibonacci numbers
English Irregular Plural Nouns


이 강의는 영어로 된 원문을 기초로 작성되었으며, Creative Commons Attribution Share-Alike 라이센스하에 자유로운 변경, 배포가 가능합니다

5271 읽음
이전 챕터 5. 정규표현식(Regular Expressions)
다음 챕터 7. 클래스와 반복자(Classes & Iterators)

저자

토론이 없습니다

Please log in to leave a comment

16.5.11618.20190612.allo
문제 보고 · 사용조건 · 개인정보보호
래블업 주식회사 · 대한민국 서울 테헤란로 145 · 대표: 신정규 · 사업자번호: 864-88-00080 · +82 70-8200-2587

거절 확인

닫기
좋아요 책갈피 토론

아래 주소를 복사하세요