You are here: Home Dive Into Python 3

Difficulty level: ♦♦♢♢♢

Comprehensions

인간의 상상력이란 존재하지 않는 가상의 것을 그릴 때가 아니라, 실존하는 뭔가를 이해하려 노력할때 극대화된다.
Richard Feynman

 

Diving In

최신 프로그래밍 언어들은 저마다 하나씩 멋진 기능으로 한껏 치장한채 등장하곤 합니다. 복잡한 문제도 단 두 세 줄의 코딩으로 손쉽게 해결해내는 그런 기능 말입니다. 흠. 그런데 다룰 줄 아는 언어라곤 C 나 C++ 밖에 없으신가요? 하루 종일 회사에서 사용해야 하는 언어엔 멋진 기능이란 눈씻고 찾아봐도 없고, 문법은 복잡하고, 심지어 코딩하다 졸리기까지 한가요? 자자. 일단, 흥분을 가라앉히시고요. 아마 어쩌면 여러분이 사용중인 언어의 창시자께서는 여러분이 미쳐 생각하지 못했던, 뭔가 복잡한 문제를 해결하는데 더 많은 시간을 투자했을런지도 모릅니다. 그러니 사용하는 언어를 너무 미워하진 마세요. 아무튼요. 이번 장에서는 파이썬의 멋진 기능 가운데 하나인 comprehension에 대해 알아보겠습니다. comprehension은 우선 list comprehension, dictionary comprehension, set comprehension 이렇게 3 가지 종류로 나눌 수 있습니다. 하지만, 동작 원리는 비슷하기 때문에 한 가지의 핵심 개념만 파악하면 다른 것들도 금방 이해할 수 있습니다. comprehension 을 본격적으로 다루기에 앞서, 파이썬 3에서는 파일 시스템의 디렉토리와 파일을 어떤 방식으로 다루는지 잠깐 살펴보고 넘어갑시다.

파일과 디렉토리 다루기

파이썬 3에는 os 라는 모듈이 있습니다. os는 아시다시피 운영체제를 의미하는 Operating System 의 약자이고, 모듈 이름이 의미하듯, os 에 관련된 모듈임을 짐작하실 수 있겠죠? 운영체제에 관련된 정보를 다루는 모듈에 걸맞게 정말 어마어마한 숫자의 함수들이 이 os 모듈 아래 또아리를 틀고 있습니다. os 모듈안에 있는 함수들을 이용하면, 디렉토리나 파일부터 프로세스 그리고 환경변수에 이르기까지, 운영체제가 가지고 있는 삼라만상의 다양한 정보를 가져오고, 조작할 수 있습니다. 아시다시피, 컴퓨터 세상엔 윈도우나 리눅스를 제외하고도 수 많은 운영체제들이 있습니다. 그 많은 운영체제를 파이썬이 모두 지원할 수 있나요? 저의 대답은 파이썬은 다양한 운영체제를 지원합니다 입니다. 되도록이면 파이썬 코드를 변경하지 않아도 여러 운영체제에서 동일하게 동작할 수 있도록 최선을 다했습니다. 하지만, 때로는 운영체제의 커널 저 깊은 곳에서 서로 다르게 동작하는 것들도 있었습니다. 그런 부분까지는 파이썬도 어떻게 할 수가 없었구요. 이럴 때는 하는 수 없이 파이썬 코드가 달라져야 할 수도 있습니다. 그 점은 미리 염두에 두셨으면 좋겠네요.

현재 작업 디렉토리

이제 막 파이썬에 입문하셨다면 아마 대부분 Python Shell을 가지고 연습을 하고 계실겁니다. 제가 이제부터 책 전반에 걸쳐서 다양한 예제를 선보일텐데요, 예제들은 대략 이런 식으로 진행됩니다.

  1. examples folder 에 있는 모듈 중 하나를 import 할겁니다
  2. import 한 모듈에 있는 함수 중 하나를 호출할 거구요
  3. 함수 호출의 실행 결과를 설명합니다

만약 Python Shell이 자신이 실행되고 있는 현재 디렉토리 위치를 알 수없다면, 위에 언급한 단계중 첫 번째인 import 에서부터 ImportError라는 에러가 발생할 겁니다. 휴. 시작부터 좌절모드군요. 하지만, 잠깐만요. 입장을 바꿔서 여러분이 Python Shell이 되었다고 한번 생각해보세요. import할 때 당연히 어디에서부터 모듈을 찾아야 할지를 알아야 하지 않겠습니까? 파이썬에서는 이를 위해 the import search path 를 이용합니다. 만약 앞에서 예로 들었던 examples 이라는 폴더가 import search path 에 없다면, 그안에 있는 모듈도 찾지 못하는 것은 당연지사죠. 생각해보면 다른 언어에서도 크게 다르지 않습니다. C++ 나 Java에서 라이브러리 안에 있는 함수를 호출하려면 library path를 시스템 환경변수 또는 작업 환경 등록변수로 등록해줘야 합니다. 그것과 동일한 이치입니다. 따라서 파이썬에서는 이를 위해 아래 두 가지 옵션 가운데 하나를 해주시면 됩니다.

  1. examples 폴더를 import search path에 추가해 줍니다
  2. 먼저 콘솔에서 examples folder로 이동한 후에 python shell을 실행합니다

Python Shell은 자신이 호출되는 동시에, 호출되었던 그 디렉토리 위치를 메모리에 올려두고 필요할때마다 그 위치를 참조하여 사용합니다. 이를 현재 작업 디렉토리라고 말하는데요, 이 디렉토리는 앞에서 예로 들었던 모듈을 검색하는데도 사용합니다. 사실, Python Shell 이건, 명령행에서 파이썬 스크립트를 실행하건, 웹서버에서 파이썬 CGI 스크립트를 돌리건 간에 관계없이 현재 작업 디렉토리는 언제나 메모리에 상주하며 파이썬에 의해 필요할 때마다 사용됩니다.

파이썬에서는 현재 작업 디렉토리를 다루기 위해, 두 개의 함수를 제공합니다. 이 두 함수는 os 모듈 안에 있습니다.

>>> import os                                            
>>> print(os.getcwd())                                   
C:\Python31
>>> os.chdir('/Users/pilgrim/diveintopython3/examples')  
>>> print(os.getcwd())                                   
C:\Users\pilgrim\diveintopython3\examples
  1. os 모듈은 파이썬 설치와 함께 설치되는 내장모듈입니다. 언제 어느 위치에서건 간에 상관없이 import 할 수 있습니다.
  2. 파이썬의 현재 작업 디렉토리는 os.getcwd() 함수를 통해 조회할 수 있습니다. 만약 GUI 모드로 파이썬을 실행했다면, Python Shell 실행파일이 있는 곳이 현재 작업 디렉토리가 됩니다. MS Windows 에서는 여러분이 파이썬을 설치했던 위치가 현재 작업 디렉토리입니다. 따로 설치 디렉토리를 변경하지 않았다면, 파이썬 3.1 버전 기준으로 c:\Python31 이 될 겁니다. 만약 GUI 가 아닌, 윈도우 명령창이나 리눅스 쉘 상에서 파이썬을 실행했다면, 파이썬을 실행했던 디렉토리가 현재 작업디렉토리가 됩니다.
  3. 현재 작업디렉토리를 변경하려면, os.chdir() 함수를 호출합니다.
  4. os.chdir() 함수의 파라미터로 전달된 디렉토리 경로을 잘 보시면, 드라이브 문자도 지정하지 않았고, 역슬래쉬가 아닌 그냥 슬래쉬문자를 사용했습니다. 맞습니다. 이건 리눅스 스타일의 경로입니다. 저는 이 코드를 MS Windows 에서 작성했지만, 그냥 저런식으로 리눅스 스타일의 경로를 사용해도 파이썬이 내부에서 알아서 윈도우에 맞게 변경해줍니다. 앞에서 말씀 드렸듯이, 바로 이러한 파이썬의 특징 덕분에 한번 작성했던 파이썬 코드를 여러 OS 에서 변경없이 사용할 수 있도록 해주는 것입니다.

파일명과 디렉토리명 다루기

우리가 디렉토리에 대해 배우는 동안은 os.path 모듈에 주목해주세요. os.path 모듈은 파일명과 디렉토리명을 다루기 위한 함수를 갖고 있습니다.

>>> import os
>>> print(os.path.join('/Users/pilgrim/diveintopython3/examples/', 'humansize.py'))              
/Users/pilgrim/diveintopython3/examples/humansize.py
>>> print(os.path.join('/Users/pilgrim/diveintopython3/examples', 'humansize.py'))               
/Users/pilgrim/diveintopython3/examples\humansize.py
>>> print(os.path.expanduser('~'))                                                               
c:\Users\pilgrim
>>> print(os.path.join(os.path.expanduser('~'), 'diveintopython3', 'examples', 'humansize.py'))  
c:\Users\pilgrim\diveintopython3\examples\humansize.py
  1. os.path.join() 함수는 하나 또는 그 이상의 파일과 디렉토리 경로를 조합하는데 사용합니다.
  2. 위의 구문과 유사한 이 예제는 os.path.join() 함수가 디렉토리 경로 구분자인 '\'' 문자를 자동으로 추가해주는 것을 보여줍니다. 윈도우 머신에서 실행되었기 때문에 백슬래쉬 입니다. 만약 리눅스나 맥 PC 에서 실행하는 경우엔 백슬래쉬가 아닌 슬래쉬('/') 문자가 추가됩니다. 하지만, 슬래쉬인지 백슬래쉬인지에 대해 너무 신경 쓰지 않아도 됩니다. 파이썬이 OS 에 맞는 경로 구분자를 알아서 덧붙여 주니까요.
  3. os.path.expanduser() 함수는 현재 사용자의 홈 디렉토리를 알아낼때 사용하고 인자로 ~ 문자를 받습니다. 윈도우나 리눅스, 맥 OS 처럼 사용자의 홈 디렉토리가 생성되는 플랫홈에서 이 함수는 언제나 해당 디렉토리를 반환합니다. 반환되는 디렉토리 경로 맨 끝에 디렉토리 구분자가 붙지 않지만, os.path.join() 함수를 사용하는한 걱정하지 않아도 됩니다.
  4. 윗 라인의 예제에서 배운 함수를 총동원한 예제입니다. os.path.join() 함수의 인자는 원하는 만큼 지정할 수 있습니다. 다른 프로그래밍 언어를 쓰다보면 항상 addSlashIfNecessary() 같은 함수를 직접 만들어 사용해야 하다 보니 귀찮았는데, 이 함수를 보고 얼마나 기뻤는지 모릅니다. 파이썬에서는 그런 고민이 더 이상 필요 없어졌습니다.

os.path 모듈에는 파일과 디렉토리명을 합치는 것 뿐만 아니라 나눌 수도 있습니다.

>>> pathname = '/Users/pilgrim/diveintopython3/examples/humansize.py'
>>> os.path.split(pathname)                                        
('/Users/pilgrim/diveintopython3/examples', 'humansize.py')
>>> (dirname, filename) = os.path.split(pathname)                  
>>> dirname                                                        
'/Users/pilgrim/diveintopython3/examples'
>>> filename                                                       
'humansize.py'
>>> (shortname, extension) = os.path.splitext(filename)            
>>> shortname
'humansize'
>>> extension
'.py'
  1. split 함수는 파일의 전체 경로를 입력으로 받아 파일과 그 파일이 속해있는 디렉토리로 나눠서 반환해줍니다. 이때 반환되는 데이터의 형식은 튜플입니다.
  2. 파이썬에서는 함수가 여러 개의 값을 리턴할 때, 이를 또한 여러 개의 변수에 담을 수 있다고 했던 것 기억하시나요? os.path.split() 함수가 바로 그런 함수입니다. split 함수가 반환하는 파일과 경로명을 튜플 형태의 변수에 담을 수 있습니다. 순서 또한 그대로 지켜집니다.
  3. 첫번째 변수인 dirname 에는 os.path.split() 함수가 반환하는 튜플 중 첫번째 원소인 파일 경로가 담깁니다.
  4. 두번째 변수인 filename에는 os.path.split() 함수가 반환하는 튜플 중 두번째 원소인 파일명이 담깁니다.
  5. os.path 모듈에는 splitext() 라는 함수도 있습니다. 확장자를 가진 파일명을 입력으로 받아 확장자를 제외한 진짜(?) 파일이름과 파일 확장자를 튜플로 반환합니다.

디렉토리 조회하기

glob 모듈 또한 파이썬 표준 라이브러리 중 하나로 디렉토리 내용을 검색하는데 사용됩니다. glob 모듈 내의 함수는 와일드카드 문자 (*) 를 이용할 수 있으므로, 더욱 간편하게 디렉토리 내용을 검색할 수 있습니다.

>>> os.chdir('/Users/pilgrim/diveintopython3/')
>>> import glob
>>> glob.glob('examples/*.xml')                  
['examples\\feed-broken.xml',
 'examples\\feed-ns0.xml',
 'examples\\feed.xml']
>>> os.chdir('examples/')                        
>>> glob.glob('*test*.py')                       
['alphameticstest.py',
 'pluraltest1.py',
 'pluraltest2.py',
 'pluraltest3.py',
 'pluraltest4.py',
 'pluraltest5.py',
 'pluraltest6.py',
 'romantest1.py',
 'romantest10.py',
 'romantest2.py',
 'romantest3.py',
 'romantest4.py',
 'romantest5.py',
 'romantest6.py',
 'romantest7.py',
 'romantest8.py',
 'romantest9.py']
  1. glob 모듈은 와일드카드 문자를 입력으로 받아 이에 해당하는 모든 파일과 디렉토리 경로를 반환합니다. 이 예제에서는 examples 폴더 내에 있는 xml 로 끝나는 모든 파일들을 조회합니다.
  2. 현재 작업 디렉토리를 examples 폴더로 변경합니다. os.chdir() 함수의 인자로는 절대 경로 뿐 아니라, 상대 경로를 줘도 됩니다.
  3. 인자를 지정할때 여러 개의 와일드카드 문자를 사용해도 됩니다. 이 예제는 현재 작업 디렉토리에서 py 확장자를 가진 파일들중 test 라는 문자열이 포함된 파일의 리스트를 반환합니다.

파일 속성 조회하기

현대 파일 시스템은 각 파일을 저장시 파일과 관련된 메타 정보 또한 함께 저장합니다. 메타정보의 예로는 파일 생성일, 최종 수정일, 크기 등이 있습니다. 파이썬에서 이 메타정보를 조화하는 법은 API 하나만 알면 됩니다. 파일을 열고 닫을 필요도 없고, 파일 명만 인자로 넘겨주면 됩니다.

>>> import os
>>> print(os.getcwd())                 
c:\Users\pilgrim\diveintopython3\examples
>>> metadata = os.stat('feed.xml')     
>>> metadata.st_mtime                  
1247520344.9537716
>>> import time                        
>>> time.localtime(metadata.st_mtime)  
time.struct_time(tm_year=2009, tm_mon=7, tm_mday=13, tm_hour=17,
  tm_min=25, tm_sec=44, tm_wday=0, tm_yday=194, tm_isdst=1)
  1. 현재 작업 디렉토리는 examples 폴더입니다.
  2. example 폴더안에 있는 feed.xml 파일을 대상으로 os.stat() 함수를 호출합니다. 이 함수는 feed.xml 파일의 메타 정보를 담은 객체 하나를 반환합니다.
  3. st_mtime 은 파일의 수정시각을 가지고 있습니다. 하지만, 보시다시피 결과값이 눈에 쉽게 들어오지는 않네요. (사실, 결과로 반환되는 저 숫자는 1970 년 1월 1일부터 현재까지의 시간을 초 단위로 변경한 값입니다. 계산해 보세요. 진짜예요.)
  4. 위의 결과 값을 좀더 알아보기 쉽게 하려면 time 모듈을 사용해야 합니다. 이 모듈 또한 파이썬 표준 라이브러리이고요. 이 모듈 안에는 여러 종류의 시간 포맷을 문자열로 변경해준다거나, 원하는 타임 존에 맞춰 알아보기 쉽도록 변경해주는 함수들이 잔뜩 들어 있습니다.
  5. time 모듈 안에 있는 localtime() 함수는 3번에서 얻어진 결과값 (1970년 1월 1일부터 현재까지 지난 시각)을 연도, 월, 일, 시, 분, 초 등을 가진 객체에 담아 반환합니다. 이 파일은 2009년 1월 13일 5:25분 경에 수정되었네요.
# 앞에 예제에서 이어집니다.
>>> metadata.st_size                              
3070
>>> import humansize
>>> humansize.approximate_size(metadata.st_size)  
'3.0 KiB'
  1. os.stat() 함수는 파일의 크기도 반환하는데, 이는 st_size 프로퍼티에 담겨있습니다. feed.xml 파일의 크기는 3070 바이트 입니다
  2. st_size 프로퍼티를 approximate_size() 함수에 인자로 넘겨서 좀 더 읽기 쉬운 포맷으로 변경할 수도 있습니다.

파일 절대 경로 구하기

앞에서 glob.glob() 함수로 파일을 검색하는 예를 보았었죠? 첫번째 예에서는 'examples\feed.xml' 를 인자로, 두번째 예에서는 *test*.py 를 인자로 주었고, 인자에 해당 하는 검색 결과값인 파일의 상대 경로가 담긴 리스트가 반환됬습니다. 사실 결과값으로 반환된 파일들과 동일한 경로에서 작업을 한다는 조건하에서는, 얻어진 결과를 이용해 파일을 여는 등의 추가적인 작업이 가능합니다만, example 폴더가 아닌 다른 곳에서 Python이 실행됬다면, 결과값은 무용지물입니다. 이런 경우에는 상대경로가 아닌 절대 경로가 필요합니다. os.path.realpath() 함수가 바로 이럴 때 필요한 함수입니다.

>>> import os
>>> print(os.getcwd())
c:\Users\pilgrim\diveintopython3\examples
>>> print(os.path.realpath('feed.xml'))
c:\Users\pilgrim\diveintopython3\examples\feed.xml

List 컴프리헨션

리스트 컴프리헨션(list comprehension)이란 특정 리스트의 각각의 원소에 어떤 함수를 적용한 후 그 결과를 받아 새로운 리스트로 만들 수 있는 아주 간편한 방법입니다.

>>> a_list = [1, 9, 8, 4]
>>> [elem * 2 for elem in a_list]           
[2, 18, 16, 8]
>>> a_list                                  
[1, 9, 8, 4]
>>> a_list = [elem * 2 for elem in a_list]  
>>> a_list
[2, 18, 16, 8]
  1. 리스트 컴프리헨션은 오른쪽에서 왼쪽으로 읽으면 이해가 쉽습니다. a_list 는 어떤 함수를 적용하려는 대상이 되는 리스트입니다. 파이썬이 이 리스트 컴프리헨션을 처리하는 방식은 a_list 내부를 순회하면서 각각의 원소를 elem 이라는 임시변수에 저장합니다. 그리고 저장된 임시변수를 대상으로 elem * 2 라는 함수를 적용후 그 결과를 반환되는 리스트에 계속하여 추가(append)합니다.
  2. 리스트 컴프리헨션은 결과값으로 새로운 리스트를 반환합니다. 원래 리스트는 변경하지 않아요.
  3. 결과값을 사용하려면 변수에 저장해 두는 것이 좋습니다. 이 실행 예에서는 기존 a_list 의 값이 리스트 컴프리헨션에 의해 반환된 값으로 변경되었습니다.

리스트 컴프리헨션에서는 어떠한 파이썬 표현식도 쓸 수 있습니다. 따라서 다음에 나오는 예제에서처럼 os 표준 라이브러리 내에 있는 파일과 디렉토리를 다루는 함수들도 리스트 컴프리헨션 안에서 사용할 수 있습니다.

>>> import os, glob
>>> glob.glob('*.xml')                                 
['feed-broken.xml', 'feed-ns0.xml', 'feed.xml']
>>> [os.path.realpath(f) for f in glob.glob('*.xml')]  
['c:\\Users\\pilgrim\\diveintopython3\\examples\\feed-broken.xml',
 'c:\\Users\\pilgrim\\diveintopython3\\examples\\feed-ns0.xml',
 'c:\\Users\\pilgrim\\diveintopython3\\examples\\feed.xml']
  1. 현재 작업 디렉토리에 있는 파일중 확장자가 .xml인 파일들을 리스트 형태로 가져옵니다.
  2. 확장자가 .xml 인 파일들의 목록을 가져와서, 이를 파일 전체경로로 변환한 다음 그 결과값을 리스트 형태로 반환합니다.

리스트 컴프리헨션의 다른 예로, 원래 리스트에서 원하는 아이템만 뽑아내 (filter) 새로운 리스트를 구성할 때도 사용하면 좋습니다.

>>> import os, glob
>>> [f for f in glob.glob('*.py') if os.stat(f).st_size > 6000]  
['pluraltest6.py',
 'romantest10.py',
 'romantest6.py',
 'romantest7.py',
 'romantest8.py',
 'romantest9.py']
  1. 원하는 아이템을 뽑아낼 때는 리스트 컴프리헨션 뒤에 if 문을 사용합니다. 이 if 문이 리스트 각각의 아이템에 적용되어 주어진 조건에 맞는지를 판단하고, 참인 경우에만 반환 결과 리스트에 그 값이 포함됩니다. 이 예에서는 .py 라는 확장자를 가진 모든 파일에 대해 그 크기가 6000 바이트가 넘는 경우에만 결과값에 반영됩니다.

자. 지금까지 본 예제들은 숫자를 곱하거나, 단일 함수를 호출하는 등 비교적 간단한 것들이었습니다. 좀 더 복잡한 예제들도 보죠.

>>> import os, glob
>>> [(os.stat(f).st_size, os.path.realpath(f)) for f in glob.glob('*.xml')]            
[(3074, 'c:\\Users\\pilgrim\\diveintopython3\\examples\\feed-broken.xml'),
 (3386, 'c:\\Users\\pilgrim\\diveintopython3\\examples\\feed-ns0.xml'),
 (3070, 'c:\\Users\\pilgrim\\diveintopython3\\examples\\feed.xml')]
>>> import humansize
>>> [(humansize.approximate_size(os.stat(f).st_size), f) for f in glob.glob('*.xml')]  
[('3.0 KiB', 'feed-broken.xml'),
 ('3.3 KiB', 'feed-ns0.xml'),
 ('3.0 KiB', 'feed.xml')]
  1. 이 리스트 컴프리헨션은 현재 작업 디렉토리에 있는 .xml 확장자 파일을 모두 검색해서, 이 파일의 크기(os.stat() 호출)와 절대 경로(os.path.realpath() 호출) 의 튜플로 만든 후 리스트로 반환합니다.
  2. 이 예제는 앞의 예제와 비슷한데, 반환되는 파일의 크기에 approximate_size() 함수를 적용하고 있습니다.

Dictionary 컴프리헨션

dictionary 컴프리헨션은 list 컴프리헨션과 거의 흡사합니다. 차이점은 실행 후 결과로 리스트 대신 딕셔너리를 반환하는 것 뿐입니다.

>>> import os, glob
>>> metadata = [(f, os.stat(f)) for f in glob.glob('*test*.py')]    
>>> metadata[0]                                                     
('alphameticstest.py', nt.stat_result(st_mode=33206, st_ino=0, st_dev=0,
 st_nlink=0, st_uid=0, st_gid=0, st_size=2509, st_atime=1247520344,
 st_mtime=1247520344, st_ctime=1247520344))
>>> metadata_dict = {f:os.stat(f) for f in glob.glob('*test*.py')}  
>>> type(metadata_dict)                                             
<class 'dict'>
>>> list(metadata_dict.keys())                                      
['romantest8.py', 'pluraltest1.py', 'pluraltest2.py', 'pluraltest5.py',
 'pluraltest6.py', 'romantest7.py', 'romantest10.py', 'romantest4.py',
 'romantest9.py', 'pluraltest3.py', 'romantest1.py', 'romantest2.py',
 'romantest3.py', 'romantest5.py', 'romantest6.py', 'alphameticstest.py',
 'pluraltest4.py']
>>> metadata_dict['alphameticstest.py'].st_size                     
2509
  1. 이건 그냥 리스트 컴프리헨션입니다. 파일명에 test라는 문자열이 들어있는 모든 python 파일 (.py) 을 찾아서 파일명과 메타데이타 (os.stat() 함수로 메타 데이타를 추출했습니다) 의 튜플을 만든후 이를 리스트로 반환합니다. metadata 라는 변수에 이 리스트를 저장합니다.
  2. 리스트 내 각각의 아이템은 튜플입니다.
  3. 자. 바로 이것이 딕셔너리 컴프리헨션입니다. 리스트 컴프리헨션과의 차이를 알아차리셨나요? 첫번째 차이는, 리스트 컴프리헨션은 [] (square bracket) 로 둘러싸여 있는데 반해, 딕셔너리 컴프리헨션은 {} (curly brace) 로 감쌉니다. 두번째 차이는, 리스트 컴프리헨션에서는 각각의 아이템에 대해 하나의 표현식을 사용하지만, 딕셔너리 컴프리헨션에서는 : (colon) 을 기준으로 두개의 표현식, 즉 하나는 key 로 사용하기 위한 표현식과 다른 하나는 value 로 사용하기 위한 표현식을 사용합니다. 이 예에서는 f 가 key, os.stat(f) 가 value 입니다.
  4. 타입을 조회해보면 딕셔너리입니다.
  5. 결과로 반환된 딕셔너리의 key 는 glob.glob('*test*.py') 호출을 통해 반환된 파일이름이네요.
  6. 결과로 반환된 딕셔너리의 value는 os.stat() 함수를 통해 반환된 값입니다. 이 예제와 같이, 파일이름을 key로 해당파일의 메타 정보를 조회할 수 있는 자료구조를 딕셔너리 컴프리헨션을 사용하여 손쉽게 생성할 수 있음을 알 수 있습니다.

리스트 컴프리헨션에서와 같이 딕셔너리 컴프리헨션에서도 if 절을 추가하여 결과값에 필터를 적용할 수 있습니다.

>>> import os, glob, humansize
>>> metadata_dict = {f:os.stat(f) for f in glob.glob('*')}                                  
>>> humansize_dict = {os.path.splitext(f)[0]:humansize.approximate_size(meta.st_size) \     
...                   for f, meta in metadata_dict.items() if meta.st_size > 6000}          
>>> list(humansize_dict.keys())                                                             
['romantest9', 'romantest8', 'romantest7', 'romantest6', 'romantest10', 'pluraltest6']
>>> humansize_dict['romantest9']                                                            
'6.5 KiB'
  1. 현재 작업 디렉토리에 있는 파일들의 이름과 메타정보를 기반으로 딕셔너리를 만드는 딕셔너리 컴프리헨션입니다. glob.glob('*') 을 통해 현재 작업 디렉토리의 모든 파일을 가져왔고, os.stat(f) 를 통해 각 파일의 메타 정보를 가져왔습니다. 결과로 생성되는 딕셔너리의 key는 파일명이고, value는 파일의 메타정보 입니다.
  2. 앞에서 생성된 metadata_dict 딕셔너리 아이템 가운데, 그 파일의 크기가 6000 바이트 보다 큰 (if meta.st_size > 6000) 파일에 대해서만 확장자를 제외한 파일명(os.path.splitext(f)[0])과 대략적인 파일크기를 뽑아내는 딕셔너리 컴프리헨션입니다.
  3. 6000 바이트 보다 크기가 큰 파일은 6개이군요.
  4. romanteset9 이라는 key에 해당하는 value는 6.5 KiB 라는 string입니다. approximate_size() 라는 함수에 의해 반환되고 있습니다.

딕셔너리 컴프리헨션을 이용한 재밌는 것들

알고 있으면 유용한 테크닉 하나를 소개합니다. 딕셔너리 내의 key 와 value 를 바꿔치기 해야 할 때, 다음과 같이 딕셔너리 컴프리헨션을 사용할 수 있습니다.

>>> a_dict = {'a': 1, 'b': 2, 'c': 3}
>>> {value:key for key, value in a_dict.items()}
{1: 'a', 2: 'b', 3: 'c'}

물론 이 테크닉은 원래 딕셔너리 내 value 값을 key 값으로 바꾸는 것이므로, value 값이 예제에서와 같이 숫자이거나, 또는 string 이나 tuple 처럼 immutable 해야합니다. 만약 딕셔너리내의 value가 list 와 같이 mutable 한 타입인 경우, 다음 코드와 같이 무시무시한 에러를 내며 실패합니다.

>>> a_dict = {'a': [1, 2, 3], 'b': 4, 'c': 5}
>>> {value:key for key, value in a_dict.items()}
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in <dictcomp>
TypeError: unhashable type: 'list'

Set 컴프리헨션

set도 자체 컴프리헨션 문법이 있습니다만, 아주 간단합니다. 딕셔너리 컴프리헨션과 똑같고 유일하게 다른점은 key:value 쌍 대신에 value 만 있습니다.

>>> a_set = set(range(10))
>>> a_set
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
>>> {x ** 2 for x in a_set}           
{0, 1, 4, 81, 64, 9, 16, 49, 25, 36}
>>> {x for x in a_set if x % 2 == 0}  
{0, 8, 2, 4, 6}
>>> {2**x for x in range(10)}         
{32, 1, 2, 4, 8, 64, 128, 256, 16, 512}
  1. set 컴프리헨션은 set 을 입력으로 받을 수 있습니다. 이 set 컴프리헨션 예제는 0부터 9까지의 숫자를 가진 set에서 각 원소의 제곱을 구하고 있습니다.
  2. set 컴프리헨션도 다른 컴프리헨션과 마찬가지로 if 절을 사용해 결과를 필터링할 수 있습니다.
  3. 꼭 입력이 set일 필요는 없습니다. 어떤 시퀀스 (예에서는 list) 라도 가능합니다.

더 공부할 내용

© 2001–11 Mark Pilgrim