리스트의 소트(sort
) 메서드를 이용하여 항목을 크기 순으로 정렬할 수 있다.
x = [4, 1, 2, 3]
x.sort()
print(x)
[1, 2, 3, 4]
기존의 리스트를 건드리지 않으면서 항목을 정렬하여 활용하고 싶으면 소티드(sorted
) 함수를 활용한다.
주의: sorted
함수는 리스트 메서드가 아니라 일반 함수이다.
따라서 메서드와 일반 함수와의 적용법 차이를 확인해야 한다.
x = [4, 1, 2, 3]
y = sorted(x)
print(f"x = {x}", f"y = {y}", sep='\n')
x = [4, 1, 2, 3] y = [1, 2, 3, 4]
sort
메서드와 sorted
함수 모두 오름차순 정렬을 기본으로 한다.
내림차순으로 정렬하려면 reverse
매개변수의 키워드 인자값을 True
로 지정하면 된다.
x = [4, 1, 2, 3]
x.sort(reverse=True)
print(x)
[4, 3, 2, 1]
x = [4, 1, 2, 3]
y = sorted(x, reverse=True)
print(y)
[4, 3, 2, 1]
정렬 기준은 기본적으로 알려진 크기이다.
x = ['dabc', 'cdabe', 'badc', 'ba', 'abc']
x.sort()
print(x)
['abc', 'ba', 'badc', 'cdabe', 'dabc']
key
옵션변수 활용¶하지만 key
옵션변수에 대한 인자를 지정하여 정렬 기준을 변경할 수 있다.
key
옵션변수의 인자로 하나의 인자를 받는 함수가 사용된다.
예를 들어, 숫자들의 절댓값을 계산하는 abs
함수는 숫자 하나를 입력받는다.
abs(-3.2)
3.2
따라서 숫자들의 절대닶의 크기를 기준으로 정렬하려면 abs
함수를 key
옵션변수의
인자로 사용하면 된다.
x = sorted([-4, 1, -2, 3], key=abs)
print(x)
[1, -2, 3, -4]
물론 절대값을 기준으로 내림차순 정렬도 가능하다.
x = sorted([-4, 1, -2, 3], key=abs, reverse=True)
print(x)
[-4, 3, -2, 1]
파이썬 특강 2부에서
살펴보았던 word_counts
변수에 저장된 단어 빈도수 목록을 살펴보자.
from collections import defaultdict, Counter
document = ["data", "science", "from", "scratch", "data", "from"]
word_counts = defaultdict(int)
for word in document:
word_counts[word] += 1
word_counts
defaultdict(int, {'data': 2, 'science': 1, 'from': 2, 'scratch': 1})
word_counts
의 항목들을 빈도수 기준 내림차순으로 정렬하고자 한다면 어떻게 해야 하는가?
가장 단순한 방법은 Counter
자료형의 most_common
메서드를 활용하면되다.
즉, 먼저 word_counts
를 Counter
자료형으로 변환시킨 후 most_common
메서드를 적용한다.
Counter(word_counts).most_common()
[('data', 2), ('from', 2), ('science', 1), ('scratch', 1)]
하지만 Counter
자료형을 사용하지 않고도 가능하다.
sorted
함수와 sort
메서드의 옵션변수인 key
의 인자로
적절한 함수를 사용하면 된다.
먼저, 정렬기준으로 사용할 함수를 살펴보자.
word_counts.items()
. word_counts.items()
dict_items([('data', 2), ('science', 1), ('from', 2), ('scratch', 1)])
sortingKey = lambda wordCount: wordCount[1]
이제 sortingKey
를 기준으로 내림차순으로 정렬하면 된다.
wc = sorted(word_counts.items(),
key=sortingKey,
reverse=True)
print(wc)
[('data', 2), ('from', 2), ('science', 1), ('scratch', 1)]
참조: wc
는 보통 워드 카운트(word count, 단어 빈도수)의 줄임말이다.
집합을 정의하기 위해 사용하는 조건제시법을 리스트, 집합, 사전(dict
)에도 적용할 수 있다.
예를 들어 0 ~ 9 까지의 정수 중에서 짝수만으로 이루어진 집합을 다음과 같이 조건제시법으로 정의할 수 있다.
{$x$ | $0 \le x < 10$, 단 $x$는 짝수}
동일한 조건으로 리스트를 생성하려면 다음과 같이 for ... in ... if ...
문을 활용한다.
형식은 다음과 같다.
[x for x in range(10) if x % 2 == 0]
for
: 파이프($|$, 일명 짝대기) 기호에 대응.x in range(10)
: '$0 \le x < 10$ 이며, $x$'는 정수를 표현.if
: '단', 즉, 조건부에 대응.x % 2 == 0
: x
를 2로 나눈 나머지가 0과 같아야 한다는 조건, 즉, 짝수 조건 표현.evensUnder10 = [x for x in range(10) if x % 2 == 0]
print(evensUnder10)
[0, 2, 4, 6, 8]
집합에 대한 조건제시법 적용은 다음과 같다.
evensUnder10_set = {x for x in range(10) if x % 2 == 0}
print(evensUnder10_set)
{0, 2, 4, 6, 8}
아래 squares
는 다음 집합에 대응한다.
{$x^2$ | $0 \le x < 10$ 이고 $x$는 정수} = {0, 1, 4, 9, 16, 25, 36, 49, 64, 81}
squares = [x * x for x in range(10)]
print(squares)
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
아래 even_squares
는 다음 집합에 대응한다.
{$x^2$ | $x \in$ evensUnder10} = {0, 4, 16, 36, 64}
even_squares = [x * x for x in evensUnder10]
print(even_squares)
[0, 4, 16, 36, 64]
사전(dict
) 자료형에 대해서도 조건제시법을 적용할 수 있다.
square_dict = {x: x * x for x in range(10)}
print(square_dict)
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81}
range(10)
의 항목들, 즉, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9.조건제시법에서 불필요한 것은 밑줄(_
)로 처리한다.
예를 들어, 동일한 항복을 반복 생성하고자 할 때 아래와 같이 할 수 있다.
참조: 밑줄은 영어로 언더스코어(underscore)라고 한다. 언더바(underbar) 표현을 사용하는 경우가 있는데 틀린 표현이다.
zeros = [0 for _ in evensUnder10]
print(zeros)
[0, 0, 0, 0, 0]
주의: 위와 같이 하면 evensUnder10
와 동일한 길이의 리스트가 생성되며,
모든 항목은 0으로 동일하다. 따라서 for ... in ...
에 사용되어야 하는 변수가
아무런 역할도 수행하지 않는다. 따라서 밑줄로 처리하는 것이다.
실제로 아래와 같이 해도 동일한 결과를 얻는다.
이유는 x
가 리스트의 항목을 생성하는데 아무런 역할도 수행하지 않기 때문이다.
zeros = [0 for x in evensUnder10]
print(zeros)
[0, 0, 0, 0, 0]
조건제시법에 여러 개의 for ... in ...
문을 사용할 수 있다.
pairs = [(x, y)
for x in range(10)
for y in range(10)]
참고: 중첩 for
반복문에 대응한다.
pairs
는 아래 집합에 대응한다.
{$(x, y)$ | $0 \le x, y < 10$ 이고 $x$, $y$ 는 정수} }
따라서 pairs
에는 총 100개의 순서쌍이 들어 있다.
첫 10개의 항목을 확인해보자.
pairs[:10]
[(0, 0), (0, 1), (0, 2), (0, 3), (0, 4), (0, 5), (0, 6), (0, 7), (0, 8), (0, 9)]
그 다음 10개를 아래와 같다.
pairs[10:20]
[(1, 0), (1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), (1, 9)]
마지막 10개를 확인해보자.
pairs[-10:]
[(9, 0), (9, 1), (9, 2), (9, 3), (9, 4), (9, 5), (9, 6), (9, 7), (9, 8), (9, 9)]
두 개의 for ... in ...
문이 어떻게 움직이는지 감잡았을 것이다.
x
를 0부터 시작하여 1씩 증가시켜 9까지 변경할 때마다
y
를 0부터 9까지 변화시킨다.다음 예제는 조금 다르게 작동한다.
x
를 0부터 시작하여 1씩 증가시켜 9까지 변경할 때마다
y
를 x
+1부터 9까지 변화시킨다.increasing_pairs = [(x, y)
for x in range(10)
for y in range(x + 1, 10)]
increasing_pairs[:9]
[(0, 1), (0, 2), (0, 3), (0, 4), (0, 5), (0, 6), (0, 7), (0, 8), (0, 9)]
increasing_pairs[9:17]
[(1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), (1, 9)]
increasing_pairs[-2:]
[(7, 9), (8, 9)]
increasing_pairs
의 길이는 9 + 8 + 7 + 6 + 5 + 4 + 3 + 2 + 1, 즉, 45이다.
len(increasing_pairs)
45
또한 만약 (x
, y
) $\in$ increasing_pairs
이면 x
가 y
보다 작다.
all(x < y for x, y in increasing_pairs)
True
assert
활용¶작성된 코드가 의도한 대로 작동하는지를 확인하는 여러 방법이 있다.
이 강좌에서는 가장 단순한 assert
테스팅 기법만 사용한다.
어서트(assert
, 주장) 이후에 나오는 문장의 참/거짓을 판단하여
참이면 그냥 넘어가고, 거짓이면 오류를 발생시킨다.
assert 1 + 1 == 2
assert 1 + 1 == 3
--------------------------------------------------------------------------- AssertionError Traceback (most recent call last) <ipython-input-32-8a19892df295> in <module> ----> 1 assert 1 + 1 == 3 AssertionError:
예외처리 기능을 활용할 수도 있다.
assert 1 + 1 == 3, "1 + 1은 2가 나와야 합니다."
--------------------------------------------------------------------------- AssertionError Traceback (most recent call last) <ipython-input-33-bc56d027d19c> in <module> ----> 1 assert 1 + 1 == 3, "1 + 1은 2가 나와야 합니다." AssertionError: 1 + 1은 2가 나와야 합니다.
assert
테스팅은 주로 함수가 제대로 구현되었는가를 테스트할 때 활용한다.
예를 들어, 숫자들의 리스트를 인자로 받아 최소값을 반환하는 함수를 아래와 같이 구현했다고 하자.
def smallest_item(xs):
return min(xs)
이제 위 구현이 제대로 작동하는지 아래처럼 테스팅할 수 있다.
assert smallest_item([10, 20, 5, 40]) == 5
assert smallest_item([1, 0, -1, 2]) == -1
함수의 인자로 제대로된 값들이 입력되는지 여부를 확인하기 위해서도 assert
가 활용될 수 있다.
def smallest_item_assert(xs):
assert xs, "빈 리스트엔 최소값이 없어요!"
return min(xs)
이제 빈 리스트([]
)를 인자로 사용하면 오류가 발생하는 대신에 바로 예외처리가 진행되어
AssertionError
가 발생하고 발생 이유를 설명한다.
smallest_item_assert([])
--------------------------------------------------------------------------- AssertionError Traceback (most recent call last) <ipython-input-37-ee97604c514b> in <module> ----> 1 smallest_item_assert([]) <ipython-input-36-f4b3e95cc1bc> in smallest_item_assert(xs) 1 def smallest_item_assert(xs): ----> 2 assert xs, "빈 리스트엔 최소값이 없어요!" 3 return min(xs) AssertionError: 빈 리스트엔 최소값이 없어요!
반면에 smallest_item
함수의 경우 빈 리스트의 최소값이 존재하지 않는다는 ValueError
가
발생한다.
smallest_item([])
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) <ipython-input-38-b3e736932245> in <module> ----> 1 smallest_item([]) <ipython-input-34-8d4a9b647158> in smallest_item(xs) 1 def smallest_item(xs): ----> 2 return min(xs) ValueError: min() arg is an empty sequence
테스팅을 몇 개를 한 결과 오류가 발생하지 않않다고 해서 구현이 제대로 되었다고 보장할 수는 없다. 테스팅은 몇 가지 경우에서 확인하니 잘 된다는 의미일 뿐이며, 모든 경우에 올바르게 작동한다는 보장은 하지 못한다.
예를 들어, 아래 코드는 두 문자열이 서로 순서가 뒤집어진 관계인지를 테스트한다.
def is_reverse(word1, word2):
if len(word1) != len(word2):
return False
i = 0
j = len(word2)-1
while j > 0:
if word1[i] != word2[j]:
return False
i = i+1
j = j-1
return True
그런데 테스팅 몇 개를 해보니 잘 구현된 것 같아 보인다.
is_reverse('pots', 'stop')
True
is_reverse('apple', 'elppa')
True
그런데 아래의 경우엔 문제가 있다.
is_reverse('apple', 'alppa')
True
이와 같이 테스팅을 통과했다고 해서 일반적으로 구현이 완전하다라고 보장할 수는 없다.
파이썬은 소위 객체 지향 프로그래밍을 지원하는 언어이다. 이 강좌에서는 객체 지행 프로그래밍의 정체를 논하지 않는다. 다만, 객체 지향 프로그래밍의 핵심 요소인 클래스(class)와 인스턴스(instance)를 어떻게 정의하고 활용하는지 예제 두 개를 이용하여 보여준다.
앞으로 클래스와 인스턴스를 수 없이 보고 사용할 것이다. 사실, 지금까지도 많은 클래스와 인스턴스를 살펴 보았다.
str
: 문자열 클래스"abc"
, "홍길동"
등list
: 리스트 클래스[1, 2, 3]
, ['ab', 1, [0, 1]]
등set
: 집합 클래스set(), {1, 2, 3}
, {'ab', 1, [0, 1]}
등dict
: 사전 클래스{"이름":"홍길동", "출신":"한양"}
등defaultdict
: 디폴트딕트 클래스defaultdict(int)
, defaultdict(list)
등Counter
: 카운터 클래스Counter([1, 2, 1, 3, 0])
등클래스는 크게 세 가지 방식으로 사용된다.
집합 자료형인 set
이 없다고 가정하고 직접 집합 자료형을 MySet
클래스를 이용하여 정의해보자.
class MySet:
# 메서드 정의:
# 모든 메서드의 첫째 매개변수는 관습적으로 "self"라 부른다.
# self의 역할은 특별하다. 여기서는 자세히 다루지 않는다.
def __init__(self, values=None):
"""이 메서드는 생성자이다. 모든 클래스의 생성자는 이 이름을 사용한다.
클래스의 인스턴스를 생성할 때 눈에 보이지 않지만 이 메서드가 호출되어 실행된다.
아래 형식으로 활용된다.
s1 = MySet() # 공집합 생성
s2 = MySet([1,2,2,3]) # 원소를 지정하면서 집합 생성"""
self.dict = {} # "self"에 입력된 MySet 클래스의 객체에 빈 사전을 기본으로 포함시킴.
# self.dict 에 원소를 추가할 것임.
if values is not None: # values에 포함된 항목을 모두 self.dict에 추가
for value in values:
self.add(value) # add 메서드 정의는 아래에 있음.
def __repr__(self):
"""MySet 자료형의 값을 화면에 출력할 때, 즉, print 함수를 실행할 때
보여주는 방식을 지정함.
print 또는 str 함수의 인자로 사용할 때 지정된 값으로 출력/변환된다.
모든 클래스에는 이 메서드가 동일한 이름으로 포함된다."""
return "MySet: " + str(self.dict.keys())
# 키에 사용된 값들만 MySet의 원소로 인정한다.
# 즉, 키값은 중요하지 않으며, 여기서는 True를 사용한다.
# 따라서 새로운 값을 원소로 추가할 때 '값:True' 형식으로 self.dict 에 추가한다.
def add(self, value):
self.dict[value] = True
# 원소 포함여부는 in 연산자를 활용한다.
def contains(self, value):
return value in self.dict
# 원소를 제거하는 메서드도 있다.
def remove(self, value):
del self.dict[value]
이닛(__init__
), 레퍼(__repr__
) 메서드처럼 두 개의 밑줄(underscores)로 감싸인 메서드를
매직 메서드(magic methods)라 부르며, 모든 파이썬 클래스에 동일한 이름으로 포함되어 있다.
기타 많은 매직 메서드가 존재하며 명시적으로 선언되지 않은 매직 메서드는 모든 클래스에서
동일하게 사용되는 기본 함수가 자동으로 지정된다. (대부분은 아무 것도 하지 않는 함수로 지정됨.)
1, 2, 3을 원소로 갖는 MySet
을 다음처럼 생성한다.
s = MySet([1,2,3])
여기서 변수 s
에 할당된 값을 MySet
클래스의 인스턴스라 부르며,
이 경우에는 특별히 MySet
자료형이라 부를 수 있다.
set
클래스의 인스턴스를 집합 자료형이라 부르는 것과 동일하다.
주의:
MySet
의 __init__
메서드가 호출된다.
그런데 __init__
메서드의 첫째 매개변수인 self
에 대한 인자는 입력하지 않으며
Values
의 인자만 사용한 것에 주의하라. self
의 용도는 아래에서 상속을 설명할 때 알아본다.이제 MySet
자료형인 s
에 원소를 추가/삭제하는 방법과 결과를 확인해보자.
print(s)
s.add(4)
print(s.contains(4))
print(s)
s.remove(3)
print(s.contains(3))
print(s)
MySet: dict_keys([1, 2, 3]) True MySet: dict_keys([1, 2, 3, 4]) False MySet: dict_keys([1, 2, 4])
웹페이지의 방문자 수를 확인하는 앱을 구현하고자 할 때 아래 도구들이 필요하다.
언급한 변수 한 개와 네 개의 도구를 포함한 일종의 도구상자를
아래 CountingClicker
클래스로 구현한다.
class CountingClicker:
"""함수의 경우처럼 문서화 문자열을 사용할 수 있다.
클래스 정보를 확인할 때 보여지는 내용을 여기에 작성한다."""
# 클래스 변수: 인스턴스 생성 횟수를 기억함.
total_count = 0
# 생성자
# 클릭수 초기값을 0으로 지정
def __init__(self, count = 0):
self.count = count
CountingClicker.total_count += 1
# 출력 메서드
def __repr__(self):
return f"CountingClicker(count={self.count})"
# 클릭 이벤트가 발생하면 클릭수를 1씩 키워주는 도구 역할 함수
def click(self, num_times = 1):
"""Click the clicker some number of times."""
self.count += num_times
# 현재 클릭수를 읽어주는 도구 역할 함수
def read(self):
return self.count
# 클릭수를 초기화하는 도구 역할 함수
def reset(self):
self.count = 0
아래 코드를 실행하면 CountingClass
의 인스턴스를 하나 생성하면 앞서 언급한 변수 한 개와 네 개의 도구를
포함한 하나의 도구상자를 얻게 되며, 도구상자의 이름은 clicker
이다.
clicker = CountingClicker()
clicker
도구상자에 포함된 도구, 즉 특정 메서드를 이용하려면 아래와 같이 실행한다.
clicker.메서드이름(인자,....)
먼저 클릭수가 0으로 초기화되어 있음을 확인하자.
이유는 아무도 클릭하지 않았기 때문이다.
실제로 clicker
도구상자를 생성할 때 호출되는 __init__
메서드의 인자가 지정되지 않아서
count
매개변수의 키워드 인자로 기본값인 0이 사용되어, self.count
변수에 할당되었다.
assert clicker.read() == 0, "클릭수는 0부터 시작해야 합니다."
self.count
와 같은 변수를 인스턴스 변수라 부른다.이제 클릭을 두 번 했다가 가정하자.
즉, click
메서드를 두 번 호출되어야 한다.
clicker.click()
clicker.click()
그러면 클릭수가 2가 되어 있어야 한다.
assert clicker.read() == 2, "두 번 클릭했으니 클릭수는 2이어야 함."
이제 클릭수를 초기화하자.
clicker.reset()
그러면 클릭수가 다시 0이 되어야 한다.
assert clicker.read() == 0, "클릭수를 초기화하면 0이 된다."
클릭수를 지정하면서 CountingClicker
의 인스턴스를 생성할 수 있다.
clicker50 = CountingClicker(50)
클릭수가 50으로 설정되었음을 확인할 수 있다.
clicker50.read()
50
지금까지 CountingClicker
의 인스턴스는 두 번 생성되었음을 아래 결과가 보여준다.
CountingClicker.total_count
2
부모 클래스로부터 모든 기능을 물려받을 수 있는 자식 클래스를 상속을 이용하여 선언할 수 있다.
예를 들어, 클릭수를 초기화할 수 없는 자식 클래스 NoResetClicker
를 아래와 같이 선언한다.
클릭수 초기화 기능을 없애기 위해서는 reset
메서드를 재정의(overriding)해야 한다.
class NoResetClicker(CountingClicker):
# 부모 클래스인 CountingClicker의 모든 메서드를 물려 받는다.
# 다만, read 함수와 reset 함수를 오버라이딩(재정의) 한다.
# 재정의하지 않는 메서드는 부모 클래스에서 정의된 그대로 물려 받는다.
# 부모 클래스의 read 함수의 반환값을 두 배해서 반환한다.
# 부모 클래스를 가리키는 super()의 용법에 주의한다.
def read(self):
return 2 * (super().read())
# reset 함수의 초기화 기능을 없앤다.
def reset(self):
pass
이제 NoResetClicker
의 인스턴스를 생성한 후 클릭수 초기화가 이루어지지 않을 확인할 수 있다.
clicker2 = NoResetClicker()
clicker2.click()
clicker2.click()
print(clicker2.read())
4
리셋이 작동하지 않습니다.
clicker2.reset()
print(clicker2.read())
4
자식 클래스의 인스턴스가 만들어져도 부모 클래스의 인스턴스 카운트가 올라간다.
CountingClicker.total_count
3
리스트, 튜플, 사전 등의 모음 자료형과 for
반복문은 서로 찰떡궁합 관계이다.
이유는 특정 명령을 지정한 횟수만큼 쉽게 반복적으로 처리할 수 있도록 만들 수 있기 때문이다.
for i in [0, 1, 2, 3, 4]:
print(i)
0 1 2 3 4
for i in (0, 1, 2, 3, 4):
print(i)
0 1 2 3 4
# range(0, 10, 2)는 0부터 2계단씩 건너 뛰며 9까지 포함.
for i in range(0, 10, 2):
print(i)
0 2 4 6 8
for i in {x : x*x for x in range(5)}.items():
print(i)
(0, 0) (1, 1) (2, 4) (3, 9) (4, 16)
리스트, 튜플, range
, 사전 자료형처럼 for ... in ...
등의 반복문에서 항목을 차례대로 읽어
활용하는 것을 가능하게 해주는 자료형을 이터러블(iterable) 자료형이다.
이터러블 자료형에는 이 외에도 더 있다. 예를 들어, 저장된 텍스트 파일의
내용을 불러와 저장한
데이터셋도
이터러블 자료형이다.
range
의 차이점¶리스트는 포함된 항목이 많아지면 그만큼 사용하는 메모리 용량도 비례해서 늘어난다. 또한 매우 많은 양의 데이터를 포함하는 리스트를 다루는 일도 그만큼 오래 걸리고 어려워진다. 예를 들어, 매우 긴 리스트를 정렬하는 데에 걸리는 시간은 리스트 길이의 제곱 정도에 비례한다. 이런 이유로 한 두 번 사용하고 버릴 대용량의 리스트를 생성하면 문제가 더욱 심각해진다.
반면에 리스트의 용량 문제를 해결할 수 있는 대안이 range
자료형이다.
range
함수는 리스트와 유사한 기능을 제공하면서도 메모리 저장과 관련된
리스트의 한계를 갖지 않는 값을 생성한다.
앞서 보았듯이, 예를 들어 range(5)
를 실행한 결과는 range
자료형이면서
[0, 1, 2, 3, 4]
와 유사한 값이다.
하지만 리스트는 이미 모든 항목을 생성한 후에 메모리에 저장되는 반면에
range(5)
는 요구되는 경우에만 항목을 하나씩 생성한다.
이와같이 필요한 만큼만 항목을 생성해주는 도구를 제너레이터(generator)라 부르며,
range
가 대표적인 제너레이터이다.
아래에서 제너레이터의 활용법을 간단한 예제를 통해 살펴본다.
range
함수¶range(5)
는 for
반복문에서 리스트 [0, 1, 2, 3, 4]
와 동일한 기능을 수행한다.
하지만 리스트와는 달리 자신을 보여달라고 하면 내부를 보여주지 않는다.
print(range(5))
range(0, 5)
즉, 0부터 4까지의 정수를 다룰 준비가 되어있다고만 알려준다.
하지만 아래와 같이 for
반복문을 이용하면 그때마다 항목을 하나씩 생성해서 보여준다.
for i in range(5):
print(i)
0 1 2 3 4
제너레이터 구현은 함수를 정의하는 방식과 거의 동일하다.
반환값을 지정하는 return
지정자 대신에 요청될 때 값을 생성하는 일드(yield
) 지정자가 사용될 뿐이다.
예를 들어, range(n)
과 동일한 기능을 수행하는 generate_range
제너레이터를 아래와 같이 정의할 수 있다.
def generate_range(n):
i = 0
while i < n:
yield i # yield 가 호출될 때마다 제너레이터의 값을 생성한다.
i += 1
아래 반복문을 실행한다고 해서 generate_range(5)
이 0부터 9까지의 정수를 한꺼번에 생성하지는 않는다.
대신에 for
반복문이 한 번씩 반복해서 실행될 때마다 차례대로 0, 1, 2, 3, 4 를 생성해서 변수 i
에 할당한다.
for i in generate_range(5):
print(f"i = {i}")
i = 0 i = 1 i = 2 i = 3 i = 4
range
, generate_range
등 처럼 모든 값을 미리 생성해서 준비해 놓는 대신에 필요할 때
필요한 항목을 생성하는 것을
소극적(lazy) 계산이라 부른다.
반면에 호출 되자마자 바로바로 실행하는 것을 적극적 계산 이라 부른다.
파이썬에서 기본적으로 적극적 계산을 사용하지만 제너레이터 경우와 같이 일부 소극적 계산을 지원한다.
제너레이터를 이용하면 무한 수열도 생성할 수 있다. 이유는 소극적 계산을 따르는 제너레이터는 항상 요구되는 만큼만 생성하고, 따라서 절대로 무한히 많은 값들을 한꺼번에 생성하지 않기 때문이다.
예를 들어, 아래 natural_numbers
함수는 모든 자연수를 생성할 준비가 된 제너레이터이다.
def natural_numbers():
"""모든 자연수 생성 가능"""
n = 1
while True:
yield n
n += 1
물론 위와 같이 무한 수열을 생성하는 제너레이터는 매우 조심스럽게 사용해야 한다. 예를 들어 아래와 같이 사용하면 무한 반복(loop)이 발생한다.
for i in natural_number():
print(i)
무한 수열을 생성하는 제너레이터는 break
명령문과 함께 적절하게 사용할 수는 있다.
for i in natural_numbers():
if i > 5:
break
print(i)
1 2 3 4 5
생성된 제너레이터는 한 번 사용하면 다시 사용할 수 없으며, 필요할 때마다 다시 제너레이터를 생성해야 한다.
예를 들어, 아래와 같이 제너레이터를 생성하여 변수에 저장하자.
iterator_once = generate_range(5)
그러고 나서 변수를 이용하여 반복문을 돌리자.
for i in iterator_once:
print(i)
0 1 2 3 4
이제 다시 동일한 변수를 이용하여 반복문을 돌리면 아무 것도 하지 않을 것이다. 이유는 지정된 제너레이터가 이미 모든 값을 생성하였기 때문이다.
for i in iterator_once:
print(i)
해결책은 해당 제너레이터를 다시 생성하는 것 뿐이다.
iterator_once = generate_range(5)
for i in iterator_once:
print(i)
0 1 2 3 4
따라서 제너레이터가 반복적으로 사용되어야 한다면 차라리 리스트로 정의해서 활용하는 것이 나을 수 있다.
리스트, 집합, 사전 자료형과는 달리 튜플을 이용한 조건제시법은 제너레이이터를 생성한다. 예를 들어, 아래 튜플 모양의 값은 20보다 작은 짝수들을 필요할 때 하나씩 생성하는 제너레이터이다.
evens_below_20 = (i for i in generate_range(20) if i % 2 == 0)
내용은 보여주지 않는다.
print(evens_below_20)
<generator object <genexpr> at 0x7f1a042c9d00>
for
반복문을 이용하여 원소들을 확인할 수 있다.
for x in evens_below_20:
print(x)
0 2 4 6 8 10 12 14 16 18
비슷한 방식으로 아래와 같이 무한 수열들을 정의할 수 있다.
naturals = natural_numbers()
evens = (x for x in naturals if x % 2 == 0)
even_squares = (x ** 2 for x in evens)
even_squares_ending_in_six = (x for x in even_squares if x % 10 == 6)
next
함수¶넥스트(next
) 함수는 제너레이터에서 아직 생성되지 않는 다음 항목을 계산해내는 함수이다.
next(even_squares_ending_in_six)
16
next(even_squares_ending_in_six)
36
next(even_squares_ending_in_six)
196
enumerate
함수¶리스트, 튜플, 제너레이터를 대상으로 반복문을 실행할 때
각 항목고 함께 해당 항목의 인덱스를 함께 사용해야 할 때가 있다.
이럴 때 이뉴머레이트(enumerate
, 열거하기) 함수가 유용하다.
names = ["Alice", "Bob", "Charlie", "Debbie"]
for i, name in enumerate(names):
print(f"{i}번 이름은 {name}입니다.")
0번 이름은 Alice입니다. 1번 이름은 Bob입니다. 2번 이름은 Charlie입니다. 3번 이름은 Debbie입니다.
아래 두 방식도 가능하지만 enumerate
함수를 사용하는 것에 비하면
별로 좋아 보이지 않는다.
for i in range(len(names)):
print(f"{i}번 이름은 {name}입니다.")
0번 이름은 Debbie입니다. 1번 이름은 Debbie입니다. 2번 이름은 Debbie입니다. 3번 이름은 Debbie입니다.
i = 0
for name in names:
print(f"{i}번 이름은 {name}입니다.")
i += 1
0번 이름은 Alice입니다. 1번 이름은 Bob입니다. 2번 이름은 Charlie입니다. 3번 이름은 Debbie입니다.
이터러블 객체와 제너레이터에 대한 보다 자세한 설명은 이터러블, 이터레이터, 제너레이트를 참고할 수 있다.
is_reverse
함수의 문제점을 찾아 수정하라.