기억해야할 Python 기능 정리(2)
이번 글에서는 다음의 기능들을 살펴본다.
- __doc__ 속성과 help 함수
- 이터레이터(iterator)
- 제너레이터(generator)
- enumerate()
- 조건식의 참/거짓 판단과 단축 평가
- range() - 수열의 생성
- 리스트 내장
- zip() 과 map() 함수
__doc__ 속성과 help 함수
보통 모듈에서 제공하는 함수의 리스트 등을 확인하고 싶을 때 dir(os)와 같이 dir 을 사용한다.
하지만, 이는 리스트 만을 보여줄 뿐 구체적인 사용법은 생각이 나지 않아 혼동이 된다. 이 때는 help(os)와 같이 해당 모듈에 대한 상세한 설명을 읽거나 특정 함수에 대한 설명 만을 따로 떼어 보고 싶을 때는 help(os.wait)와 같이 확인하면 된다.
이렇게 help 를 통해 볼 수 있는 document 내용은 개발자가 미리 작성해둔 것을 보여주는 것이다.
document 를 작성하는 방법은 두 가지가 있는데 하나는 객체의__doc__ 속성을 이용하는 것이고, 하나는 함수 객체 등을 정의할 때 """(쌍따옴표 3개)를 이용하여 기록하는 방법이다.
__doc__ 속성은 모든 객체의 부모인 object 에 포함된 기본적인 속성으로 주로 객체에 대한 설명을 적는데 사용된다. 즉, Python 의 모든 객체는 object 로부터 상속되므로 객체 모두가 __doc__ 속성에 설명을 기록할 수 있다는 말이다.
>>> def func(a,b): ... return a + b ... >>> func.__doc__ >>> func.__doc__ = "This is func's document" >>> func.__doc__ "This is func's document" >>> help(func)
>>> def func1(a,b): ... """This is func1's document""" ... return a + b ... >>> func1.__doc__ "This is func1's document" >>> help(func1)
이터레이터(Iterator)
리스트, 튜플, 문자열같은 순회 가능한 객체들에는 이터레이터(Iterator)라는 특별한 객체를 포함하고 있다. 이터레이터는 각 요소들을 순차적으로 접근하기 편하도록 만들어둔 객체이다.
아래 for 문을 보자. for 문은 어떤 방식으로 리스트의 각 요소를 가져올까?
이를 고민해보면 객체에서 이터레이터의 역할을 이해할 수 있다. 물론 리스트 대신 어떠한 형태의 순회 가능한 객체를 사용할 때도 마찬가지이다.
>>> for i in [1,2,3,4]: ... print(i) ... 1 2 3 4
for 문이 어떻게 동작하여 리스트의 각 요소를 가져오는지 그 내부 처리순서를 정리하면 다음과 같다.
- 리스트 객체에서 이터레이터 객체를 가져온다. (이터레이터는 객체의 첫 번째 요소를 가리킨다.)
- 이터레이터 안의 __next__() 메소드를 실행한다. (이터레이터를 다음 요소로 옮김)
- for 구문은 StopIteration 예외를 만날 때까지 반복적으로 __next__() 를 수행한다.
위의 과정을 명시적으로 for 문 대신 직접 해볼 수도 있다.
다만 it.__next__() 와 같이 직접 이터레이터의 내부 메소드를 호출하면 에러가 발생한다. next() 라는 built-in 함수가 준비되어 있으므로 이를 사용한다.
마지막에 리스트의 끝에 도달했는데도 계속 next()를 호출하면 StopIteration 을 만났다는 에러가 리턴되는 것을 확인할 수 있다.
>>> it = iter([1,2,3,4]) >>> it <listiterator object at 0x7fb04038ea90> >>> type(it) <type 'listiterator'> >>> it.__next__() Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'listiterator' object has no attribute '__next__' >>> help(next) >>> next(it) 1 >>> next(it) 2 >>> next(it) 3 >>> next(it) 4 >>> next(it) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
제너레이터(Generator)
함수라는 것은 필요할 때 호출되어 스택에 생성된 후 역할이 끝나고 리턴하면 스택에서 사라진다. Python 에서 함수도 객체이긴 하지만 원리는 동일하게 동작한다. 즉, 스택 메모리를 이용하여 함수 객체가 생성된 후 함수가 리턴되면 스택에서 사라진다.
하지만, 함수를 정의할 때 return 대신 yield 를 사용할 수가 있는데, 이렇게 되면 함수를 완전히 끝내는 대신 함수의 리턴 값만 호출한 곳에 돌려준 후 함수는 실행되던 그대로 살려둔다. 호출한 곳에서는 이전 상태를 유지하고 있는 함수를 다시 호출할 수 있다. 함수는 실행되면 제너레이터 객체를 리턴한다.
함수의 상태가 그대로 유지되는 특성때문에 이터레이터 객체와 같은 특성을 가지므로 상당히 유용하게 쓰인다.
>>> def func(): ... d = "abc" ... for i in d: ... yield i ... >>> func <function func at 0x7fb04003ec08> >>> func() <generator object func at 0x7fb04003c8c0> >>> it = iter(func()) >>> type(it) <type 'generator'> >>> next(it) 'a' >>> next(it) 'b' >>> next(it) 'c' >>> next(it) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
아래 예제를 보면 제너레이터를 사용하는 방식으로 사용 시 데이터를 미리 만들어두는 방식보다 메모리를 상당히 절약할 수 있다는 것을 알 수 있다.
() 를 이용하여 제터레이터를 만들 수 있고, 역으로 제너레이터를 이용하여 리스트를 만들 수도 있다는 것을 팁으로 알아두자.
>>> l = [1,2,3,4,5,6,7,8,9,10] >>> sum(l) 55 >>> g = (i for i in range(11)) >>> g <generator object <genexpr> at 0x7fb04003c370> >>> type(g) <type 'generator'> >>> sum(g) 55 >>> list(g) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
enumerate()
enumerate()는 Python 이 제공하는 내장 함수로, 순회가 가능한 객체에서 인덱스 값과 요소의 값을 동시에 반환해주는 함수이다. 이터레이터는 순회할 때 각 요소의 값만 반환하지만 enumerate()를 사용하면 인덱스까지 함께 반환하게 할 수 있다.
아래의 간단한 예를 보면 차이를 확연하게 알 수 있을 것이다.
>>> for i in [1,2,3]: ... print(i) ... 1 2 3 >>> for idx, val in enumerate([1,2,3]): ... print(idx, val) ... (0, 1) (1, 2) (2, 3)
조건시의 참/거짓 판단과 단축 평가
어떤 조건식이 참이냐 거짓이냐를 어떻게 판단할까? 조건식에는 정수도 올 수 있고, 실수도 올 수 있고, 문자열같은 객체도 올 수 있다. 어떤 타입의 객체가 조건식에 오건 항상 0 값, 아무 것도 없는 상태의 값이 False 가 된다.
정수는 0, 실수는 0.0, 리스트는 [], 튜플은 (), 사전은 {}, 문자열은 '' 등은 모두 False 라고 판별이 될 것이다.
Python 에서 아무 것도 없음을 나타내는 None 도 False 로 판별이 된다.
>>> bool(0) False >>> bool(0.0) False >>> bool([]) False >>> bool(()) False >>> bool({}) False >>> bool(None) False
2 개 이상의 논리식을 판별할 때 and, &, or, | 연산자를 사용하는데, 이러한 연산자가 사용되면 판별할 때 기본적으로 왼쪽에서 오른쪽으로 판별을 수행한다.
즉, 0 & [] 와 같다면 0 먼저 판별하고 [] 를 판별한다.
0 & [] 일 경우에는 굳이 [] 에 대한 판별을 하지 않아도 왼쪽의 0 에 대한 판별만으로도 전체 조건식의 결과를 알 수 있다. 물론 결과는 False 이다. 이렇게 왼쪽에서 순차적으로 조건식을 판별하는 과정에서 일부만 판별해도 전체 조건식의 결과를 알 수 있을 때 더 이상의 판별의 중단하는 것을 단축 평가라고 한다.
and 와 or 의 경우는 Python 에서 단축 평가로 동작하는 것을 보장하므로 & 나 | 보다는 and / or 를 사용할 것을 권장한다.
단축 평가로 동작하게 되면 판별 시간을 단축하여 성능 향상이 될 뿐 아니라, 논리식에 존재하는 런타임 에러 발생을 try~catch 구문이 아니라 논리식 만으로 사전에 차단할 수도 있다.
아래의 예제에서 & 를 사용했을 때는 ZeroDivisionError 에러가 발생하는 반면, and 를 사용했을 때는 왼쪽의 논리식만 수행되어 에러가 발생하지 않음을 알 수 있다.
>>> a = 0 >>> if a & 10/a: ... print("a is 0") ... else: ... print("pass without error") ... Traceback (most recent call last): File "<stdin>", line 1, in <module> ZeroDivisionError: integer division or modulo by zero >>> a = 0 >>> if a and 10/a: ... print("a is 0") ... else: ... print("pass without error") ... pass without error
range() - 수열의 생성
range(['시작값'], '종료값'[, '증가값'])
range() 는 정수의 수열을 생성하여 리스트 객체로 만들어주는 함수이다. 리스트를 생성해주니 for 문과 아주 잘 사용될 수 있고 이터레이터 또한 사용가능하다.
>>> r = range(10) >>> type(r) <type 'list'> >>> r [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] >>> for i in range(3): ... print(i) ... 0 1 2 >>> for idx, val in enumerate(range(3)): ... print(idx,val) ... (0, 0) (1, 1) (2, 2)
리스트 내장
기존의 리스트 객체에서 조합이나 필터링 등 추가적으로 연산을 수행하여 새로운 나만의 리스트를 만들고자 할 경우 '리스트 내장' 기능이 매우 유용하다.
<표현식> for <아이템> in <시퀀스 타입 객체> (if <조건식)
>>> l = [1,2,3,4,5] >>> [ i for i in l ] [1, 2, 3, 4, 5] >>> [ i*2 for i in l ] [2, 4, 6, 8, 10] >>> [ i for i in l if i % 2 == 0 ] [2, 4]
>>> d = {1:'dplee', 2:'cloudrain21', 3:'inwonns'} >>> [ len(v) for v in d.values() ] [5, 11, 7] >>> [ v.upper() for v in d.values() ] ['DPLEE', 'CLOUDRAIN21', 'INWONNS']
zip() 과 map() 함수
zip() 은 2 개 이상의 시퀀스 형 또는 이터레이터형 객체의 각 요소를 서로 쌍으로 묶어서 튜플 형태로 만들어주는 함수이다. 대상 객체 중에서 요소가 작은 쪽 기준 갯수로 결합된다.
>>> a = [1,2,3] >>> b = ['a','b','c'] >>> zip(a,b) [(1, 'a'), (2, 'b'), (3, 'c')] >>> for i in zip(a,b): ... print(i) ... (1, 'a') (2, 'b') (3, 'c') >>> c = ['x','y'] >>> zip(a,c) [(1, 'x'), (2, 'y')]
zip() 으로 결합된 결과를 다시 분리할 수도 있다. 이 때 zip() 함수의 인자 앞에 *를 붙인다. 인자는 zip()으로 결합된 객체나 이터레이터이어야 한다.
>>> ret = zip(a,b) >>> x,y = zip(*ret) >>> x (1, 2, 3) >>> y ('a', 'b', 'c')
3 개 이상의 객체도 결합 가능하다.
>>> zip(a,b,c) [(1, 'a', 'x'), (2, 'b', 'y')]
map(<함수 이름>, 이터레이션이 가능한 객체, ...)
map 함수는 순회 가능한 객체의 모든 요소를 순회하면서 원하는대로 변형하고 싶을 때 사용한다. map()함수는 객체를 순회하면서 각 요소를 첫 인자인 함수에 전달하고, 함수의 수행 결과를 이터레이터 객체로 생성해서 반환한다.
map() 함수의 결과는 리스트이다.
일반 함수 대신 람다 함수를 map 의 인자로도 사용할 수 있다.
>>> l = [1,2,3] >>> def func(x): ... return x * x ... >>> map(func, l) [1, 4, 9] >>> map(lambda x: x*x, l) [1, 4, 9] >>> type( map(func, l) ) <type 'list'> >>> r = map(func, l) >>> r [1, 4, 9]
References
Python 3.2 Programming (신호철 등)
Python - 제너레이터
https://docs.python.org/3/library/