기억해야할 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 문이 어떻게 동작하여 리스트의 각 요소를 가져오는지 그 내부 처리순서를 정리하면 다음과 같다.

  1. 리스트 객체에서 이터레이터 객체를 가져온다. (이터레이터는 객체의 첫 번째 요소를 가리킨다.)
  2. 이터레이터 안의 __next__() 메소드를 실행한다. (이터레이터를 다음 요소로 옮김)
  3. 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/

You may also like...