You are here: Home Dive Into Python 3

Difficulty level: ♦♦♦♢♢

Strings

I’m telling you this ’cause you’re one of my friends.
My alphabet starts where your alphabet ends!
— Dr. Seuss, On Beyond Zebra!

 

약간 지루하지만, 반드시 짚고 넘어가야 할 사실들

많은 사람들이 무심코 지나치는 사실이지만, 사실 문자 체계란 놀랄 정도로 복잡한 것 중 하나입니다. 알파벳을 예로 들어봅시다. Bougainville이라는 나라가 있습니다. 세상에서 가장 적은 수의 알파벳을 가지고 있는 나라이고요. Rotokas alphabet 이라는 알파벳을 가지고 있는데, 단지 12개의 문자, 즉 A, E, G, I, K, O, P, R, S, T, U, V 만을 가지고 있습니다. 한편, 이와는 반대로, 중국어나 일본어, 한국어 같은 언어는 수 천여개의, 알파벳에 해당하는 문자들을 가지고 있습니다. 영어는 26개의 문자  — 대소문자를 별도로 구별한다면 52개 — 와 !@#$%& 같은 약간의 특수문자들도 가지고 있습니다.

여러분은 “텍스트”라고 하면 컴퓨터 스크린 위 문자나 기호를 떠올릴지도 모르겠습니다. 컴퓨터는 사실 문자나 기호를 직접 다루지 못합니다. 대신 비트나 바이트를 다룰 수 있지요. 여러분이 컴퓨터 스크린 위에서 보는 텍스트란 사실 특정 문자열 인코딩에 의해 저장된 비트나 바이트인 셈입니다. 결국 문자열 인코딩이란 여러분이 스크린을 통해 보는 어떤 텍스트 기호와 실제로 그 내부에 저장된 비트간의 연결고리인 셈 입니다. 이런 문자열 인코딩의 종류는 아주 다양합니다. 러시안어나 중국어, 영어와 같이 특정 언어 만 지원하는 것에서부터, 여러 가지의 언어들을 한꺼번에 처리할 수 있는 것들도 있습니다.

문자열 인코딩의 세계는 사실 좀 전에 말씀드린 것보다 조금 더 복잡합니다. 어떤 문자들은 서로 다른 여러 개의 인코딩을 사용하더라도 화면상으론 동일하게 표현되지만, 실제 각각의 인코딩 내부는 서로 다른 바이트 배열을 사용하고 있기도 합니다. 문자열 인코딩은 암호를 푸는 열쇠정도로 생각하셔도 됩니다. 직장상사가 여러분에게 바이트 배열을 휙하고 던져주고 나서, “자. 잘들으세요. 이건 파일이구요, 요건 웹 페이지입니다. 아. 참! 그리고 이거 전부 다 텍스트 데이타예요.”이렇게 말하고 쌩하니 가버렸다고 생각해봅시다. 황당한 기분에 던져진 바이트 배열을 들여다 보지만, 손에 쥔 것은 0과 1로 이루어진 바이트 더미일 뿐입니다. 이걸 텍스트로 올바르게 해석해내기 위해서는 캐릭터 인코딩이 뭘로 되있었는지 물어봤어야 했습니다. 지금이라도 물어보러 가야겠군요. 하지만, 만약 상사가 잘못된 정보를 알려주었거나, 아예 모르쇠로 일관하는 경우엔 십중팔구 여러분은 오후 내내 이게 뭔지 골머리싸고 끙끙대다가 두 손 두 발 다들고, 포장마차에서 소주잔이나 기울이며 신세한탄을 할 수 밖에 없을 겁니다.

따옴표가 나와야 할 자리에 요상하게 생긴 물음표가 나오는 웹페이지를 본 적이 있나요? 아니면 웩켁쵁 처럼 희한한 문자로 가득한 웹페이지는요? 이는 모두 웹 페이지 작성자가 문자열 인코딩 설정을 제대로 해주지 않아서 생기는 일입니다. 아무리 날래고 쌩쌩한 웹 브라우저라도 이런 대책없는 페이지를 만나게 되면, 그야말로 어찌할바를 모르다가 종국에는 이런 알 수 없는 요상한 기호들을 쏟아내게 되는 거죠.

세상 대부분의 주요언어들은 컴퓨터에서 적절히 처리되기 위한 고유한 문자열 인코딩 방식을 가지고 있습니다. 모든 나라의 문자가 서로 다르고, 이를 한꺼번에 처리한다는 것은 컴퓨터에겐 힘들고 값비싼 작업이었으므로, 각 나라의 문자열 인코딩방식은 그 나라의 언어에 최적화되어 있었습니다. 제가 여기서 말하는 그나라 언어에 최적화 되있다는 말은 서로 다른 인코딩 방식이라도 0–255 범위 내에 있는 같은 숫자를 사용할 수 있고, 같은 숫자지만 표시하는 문자는 언어별로 다르다는 겁니다. 가령 , 예를 들어, ASCII 라는 인코딩 방식에 대해 들어본 적이 있을겁니다. 0 부터 127 사이에 값에 영어 알파벳을 매칭해놓은 인코딩 방식으로, 여기서 65라는 숫자는 “A”이고, 97은 소문자 “a” 입니다. 영어는 아주 작은 수의 알파벳 집합을 가지고 있을 뿐이므로, 128개로 모든 문자를 표현할 수 있습니다. 128개는 2의 7승이므로, 한 바이트 (2의 8승)로 알파벳을 모두 표현할 수 있는 셈입니다.

프랑스어나 스페인어, 독일어 같은 동유럽권의 언어는 영어보다 많은 수의 알파벳을 가지고 있습니다. 좀더 정확히 말하면, 이들 언어에는 알파벳 위에 물결 표시와 같은 (틸다라고 부른답니다) 것을 얹어놓은 듯한, 가령 ñ 와 같은 문자도 있습니다. 이 언어들을 위한 대표적인 인코딩방식이 CP-1252 입니다. “windows-1252” 이라고도 불리는데 Microsoft Windows에서 많이 사용되었기 때문입니다. CP-1252 의 0부터 127까지의 값은 ASCII 코드의 값과 동일합니다. 그리고 128 부터 255 까지의 범위의 값에는 앞에서 예를 들었던 ñ 같은 문자들 할당해두었습니다. 예를 들어, ñ는 241입니다. 영어에 사용되는 ascii 방식보다는 데이터가 많이 필요하지만, 그래도 여전히 한 바이트 (0-255)만 있으면 모든 문자를 표현할 수 있습니다.

한편 중국어, 일본어, 한국어 같은 언어들이 있습니다. 이 언어들은 영어의 알파벳에 해당하는 문자가 수천개에 달하므로 한 바이트 만으로는 해결할 수 없습니다. 따라서 이런 나라의 언어들을 위해서는 한 바이트가 더 필요하여 총 두 바이트를 사용해야 합니다(0–65535). 제가 앞서 드린 말씀 중에 한 바이트 인코딩에서는 각 언어에 최적화 되어 있다고 한 것 기억나시나요? 같은 숫자를 사용하지만, 표현되는 문자는 언어별로 다르다는 거요. 이 사실은 두 바이트 문자열 인코딩에서도 그대로 적용됩니다. 표현해야 하는 문자의 숫자가 증가한 것 밖에는 차이나는 것이 없습니다.

사실 이런 문제는 여러분이 직접 “텍스트”를 입력하고 출력하는 한 문제될 것은 없었습니다. “평범한 텍스트 (plain text)”라는 개념은 존재하지도 않았습니다. 프로그램 소스코드는 ASCII 코드로 되어있고, 워드프로세서를 사용하는 사람들도 평범한 텍스트건, 워드프로세서에서 정의한 기호이던 간에, 동일한 소프트웨어만 사용한다면, 입력하고 출력하는데 아무런 문제가 없었던 것입니다.

하지만 이제, 워드프로세서 앞에서 문서나 작성하던 시대는 지났고, 이메일이나 웹 서핑같은 일이 일상이 되어버린 글로벌 네트워크의 시대가 되었습니다. 엄청난 양의 “평범한 텍스트 (plain text)”들이 전세계를 휘저으며 돌아다니고 있습니다. 여러분의 PC에서 작성된 이메일이 어떤 컴퓨터를 거쳐 또 다른 컴퓨터에게로 전송됩니다. 반면 컴퓨터는 글을 읽을 수 없습니다. 단지 숫자만을 처리하지요. 앞에서 이야기한 것처럼 같은 숫자라도 맥락에 따라 (어떤 문자열 인코딩을 사용했느냐에 따라) 다른 의미를 가질 수 있습니다. 명심하세요. 보낸 사람이 타이핑했던 대로 받는 사람이 읽기 위해서는, 컴퓨터가 다루는 숫자를 사람이 읽을 수 있는 글자로 변환할 수 있는 어떤 변환 키 같은 것이 필요합니다. 이 키가 없는 당신은 화면에 온통 이상하게 뭉그러지고, 찌그러진 기호더미 앞에서 한숨을 쉴 수 밖에 없습니다.

한편으로, 여러 종류의 텍스트를 한 군데 모아놓고 처리해야 하는 상황에 대해 생각해봅시다. 예를 들어, 여러분들이 받은 이메일을 어떤 데이터베이스에 저장해두는 상황이 있을 수 있겠죠? 텍스트만 저장해두면 될까요? 아니죠. 텍스트를 적절하게 화면에 표시하기 위해서는 어떤 문자열 인코딩을 사용했는지도 함께 저장해야 합니다. 흠. 얼핏봐도 쉽지 않을 거 같군요. 이메일이 있는 데이터베이스를 검색해야 하는 상황은 또 어떤가요? 이 때는 해당 검색 키워드에 맞는 문자열 인코딩으로 그때 그때 변경해줘야 제대로 된 검색 결과를 얻을 수 있을 것 같군요. 음. 역시 만만치 않을 거 같네요.

여러분이 작성중인 문서에 영어와 한국어를 동시에 사용해야 하는 경우도 생각해봅시다. (힌트입니다: 실제로 이런 워드프로세스 어플리케이션들은 특정 문자열 인코딩 모드에서 다른 문자열 인코딩 모드로 변경할 때 마다 escape code 를 사용합니다. 여러분 PC의 인코딩 모드가 러시안 koi8-r에 있는 경우 숫자 241은 Я 를 의미합니다만, Mac Greek 모드로 변경되는 순간 이 숫자는 ώ를 의미합니다.) 당연히 이 문서도 특정 텍스트로 검색할 수 있어야 합니다.

짜증이 쓰나미처럼 밀려오시나요? 자, 자. 일단 좀 진정하시고요, 여기까지 읽으신 분들은 미리 잠시 머리를 식혀두시기 바랍니다. 왜냐면 이제까지 여러분이 알고 있던 문자열에 대한 지식은 모두 잘못된 것이라는 사실에 분노게이지가 하늘을 찌를 수도 있으니까요. 아무튼 중요한건, plain text 따위는 이제부턴 존재하지 않는다는 겁니다. 오늘부터 이렇게 딱 정한거예요. 자 이제 본격적으로 들어갑니다. 심호흡하시고, 안전벨트 매세요.

Unicode

Enter Unicode.

유니코드 (Unicode)란 지구상의 모든 언어를 표현하기 위해 고안된 문자 인코딩 시스템입니다. 지구상의 모든 기호와 문자들을 4 바이트 범위내의 숫자로 표현합니다. 각 숫자는 각각 고유한 문자를 표현하므로 절대 겹치는 일은 없습니다만, 만약 특정 문자가 여러 개의 언어에서 사용되는 경우엔 (영어 알파벳을 공통으로 사용하는 나라가 생각보다 많습니다.) 해당하는 유니코드 숫자는 동일한 숫자를 사용합니다. 아무튼 여기서 중요한 내용은 특정 유니코드 숫자는 고유한 문자 하나를 의미한다는 것 입니다. 예를 들어 유니코드의 숫자 U+0041 는 언제나 'A' 라는 문자를 의미하는 거죠.

자, 이제 우리의 문제가 완벽하게 해결된 것 같지 않나요? 이제 한 문서에 여러 종류의 문자로 입력할 수도 있고, 더 이상 인코딩 모드를 변경시키면서 이 모드에서 저 모드로 번거롭게 왔다 갔다 하지 않아도 되겠네요. 하지만 혹시 그런 생각 안드세요? "뭐? 4 바이트나 사용한다고 ?!! 이거 너무 낭비가 심하잖아 !!" 특히 영어권에서 온 성질급한 사람들은 더할겁니다. 고작 24개 밖에 안되는 알파벳을 사용하는데 4 바이트라니요. 4 바이트면 자그마치 40억개가 넘는 가짓수를 표현할 수 있는 크기입니다. 사실 영어권의 사람들은 한 바이트만 있어도 충분하죠. 한 바이트로도 256 가짓수의 서로 다른 문자를 표현할 수 있으니까요. 심지어 어마어마한 종류의 문자를 가지고 있는 중국어도 두 바이트만 있으면 거뜬히 표현할 수 있거든요.

아무튼 이렇게 한 문자를 4바이트로 표현하는 유니코드 인코딩 방식을 UTF-32 방식이라고 합니다. 뒤에 붙은 숫자 32는 4바이트가 32비트이기 때문에 그렇게 정한 것입니다. UTF-32는 가장 직관적인 인코딩 방식입니다. 세계 어느나라의 문자도 4 바이트에 해당하는 숫자로 고유하게 맵핑되기 때문에, 특정 유니코드 문자를 검색해야하는 경우 그 숫자만큼 이동하면 됩니다. 따라서 어떤 문자건 간에 정해진 시간 내에 찾아낼 수 있습니다. 하지만 그 대신, 앞에 언급한대로, 32 비트라는 커다란 공간을 할당해야 하는 맹점도 함께 가지고 있습니다.

유니코드 안에 어마어마한 양의 문자들이 있긴 하지만, 사람들은 곧 유니코드의 뒷 쪽에 배치되있는 특수기호나 문자는 거의 사용할 일이 없다는 사실을 깨닫게 됩니다. 그리고 나서 UTF-16 이라는 새로운 인코딩 표준이 짜잔하고 등장하게 됩니다. 짐작하시듯 이 기술은 16 비트, 즉 2 바이트의 숫자만 사용하는 것으로, 표현할 수 있는 범위는 0 - 65535 까지 입니다. 이 범위를 넘어서는 문자를 표현해야 하는 경우엔 조금 지저분한 꼼수를 사용하긴 합니다만, 어쨋건 표현이 가능합니다. UTF-16이 UTF-32에 비해 갖는 이점은 명확합니다. UTF-32에 비해 할당해야 할 바이트 크기를 반으로 줄일 수 있고, 여전히 검색성능도 나쁘지 않습니다. (검색해야 하는 문자가 0 부터 65535 범위내라면 말이죠.)

그러나, UTF-32에도 UTF-16에도 뭔가 뒷맛이 개운치 못한 문제가 있었으니, 그것은 바로 바이트를 저장하는 방식인 Byte Order가 컴퓨터마다 다르다는 것이었습니다. 가령 예를 들면 UTF-16 문자 U+4E2D의 경우 컴퓨터 OS 가 Big-endian을 사용하느냐, Little-endian을 사용하느냐에 따라 4E 2D 로 저장될 수도 있고, 2D 4E 로 저장될 수도 있습니다. (역자주. 인텔 CPU 기반의 윈도우와 리눅스 PC는 리틀 엔디안을 사용하고, RISC 기반의 UNIX 시스템이나 네트워크 시스템은 빅 엔디안을 사용합니다.) UTF-32의 경우엔 사이즈가 커지니만큼 상황이 더 안좋고요. 만약 남들과 문서를 주고받을 일이 없다면 상관 없겠지만, 엔디안 방식이 다른 어딘가에 있는 PC로 전송해야 하는 경우엔 내가 어떤 인코딩 방식을 사용했는지를 알려주어야 받는 쪽에서 올바르게 해석할 수 있을 겁니다.

이런 문제를 해결하기 위해 UTF-32 (4바이트) 나 UTF-16 (2바이트) 같이 멀티 바이트를 사용하는 인코딩 시스템에서는 “Byte Order Mark,” 라고 하는 특수한 종류의 문자를 만들어 사용합니다. 그리고 이 Byte Order Mark를 전송하는 모든 문서 앞 부분에 포함시켜서 문서에 사용된 Byte Order를 표시합니다. UTF-16 시스템의 Byte Order Mark는 U+FEFF 입니다. 만약 여러분이 어딘가로부터 받은 UTF-16 문서가 FF FE 로 시작한다면 동일한 바이트 순서를 사용하는 것이고, FE FF 로 시작한다면 바이트 순서가 바뀌었다는 의미입니다.

하지만 UTF-16 이 이상적인 것은 아닙니다. 특히 ASCII 문자를 많이 다루어야 하는 상황이라면 말이죠. 중국에 있는 어느 회사의 웹사이트를 생각해봅시다. 만약 이 웹사이트가 영자신문과 같이 대부분의 정보는 영어로 이루어져 있고, 가끔씩 중국어가 나오는 상황이라면 어떨까요? 음. 대부분의 영어정보는 ASCII 문자로 표현 할 수 있고, 중국어의 경우라도 두 바이트로 너끈히 해결이 가능하겠군요. 검색도 정해진 시간에 가능할거구요. 하지만 두 바이트 범위를 넘어서는 문자에 대해서는 어떻게 해야할까요. 별도로 참조테이블을 운영하지 않는 이상 이 문제는 속시원히 해결되지 않을겁니다.

실망하지 마세요. 여기, 해결책이 있습니다.

UTF-8

UTF-8 은 유니코드를 위한 가변길이(variable-length) 인코딩 시스템입니다. 즉, 고정된 크기의 바이트를 이용하는게 아니라, 문자마다 바이트 길이가 달라질 수 있다는 거죠. ASCII 코드 (A-Z, &c.) 의 경우 문자마다 한 바이트만 사용합니다. 사실 UTF-8 인코딩에서 등장하는 1부터 127까지의 바이트 코드와 ASCII 에서 1부터 127 까지의 바이트코드는 동일합니다. 즉 ASCII 코드에서 등장하는 128 종류의 문자는 UTF-8 에서도 동일한 바이트 코드를 갖는다는 거죠. ñ 나 ö 같은 확장 라틴어는 두 바이트를 사용하고, 중국어의 경우는 세 바이트를 사용합니다. 잘 사용되지 않는 “astral plane (아스트랄 평면)” 같은 경우엔 4 바이트를 사용합니다. 물론 UTF-8 에도 장점과 단점이 공존합니다. 단점: 각 문자가 각기 다른 바이트 크기일 수 있기 때문에 N 번째의 문자를 검색하는 속도가 O(N)에 가깝습니다. 즉, 어떤 단어 안에서 특정 문자를 검색해야 할때, 그 단어가 길면 길수록 검색하는 시간이 길어집니다. 또한 가변길이의 인코딩 시스템이기 때문에 인코딩이나 디코딩을 할때 비트 오퍼레이션을 사용해야 합니다. 장점: ASCII 코드같이 광범위하게 사용되는 문자를 인코딩해야 할때 탁월한 성능을 보여줍니다. 확장 라틴어의 경우엔 UTF-16과 성능이 비슷하고 (두 바이트를 쓰니까요), 중국어의 경우엔 UTF-32 보다 성능이 좋습니다. 그리고 비트 오퍼레이션을 해야 하는 특성상, 멀티 바이트 인코딩 시스템 (UTF-16, UTF-32) 의 경우에서 발생할 수 있는, 바이트 순서로 인해 발생되는 문제도 미연에 방지할 수 있습니다. 즉, UTF-8로 인코딩된 문서의 경우 어느 컴퓨터에 갖다놓더라도 동일한 바이트 순서를 갖게 됩니다.

Diving In

파이썬 3에서 모든 스트링은 유니코드 문자열입니다. UTF-8 이나 CP-1251 등으로 인코딩 되어있지 않은 그냥 유니코드일 뿐입니다. UTF-8 같은 인코딩 시스템은 문자열을 바이트배열로 바꿔주는 방법중의 하나라는 사실을 잊지마세요. 만약 어떤 스트링을 특정 인코딩 시스템을 사용하여 해당 바이트 배열로 바꾸고 싶다면, Python 3 를 이용할 수 있습니다. 반대로 특정 바이트 배열을 스트링으로 바꾸고 싶다면 또 역시 Python 3를 이용할 수 있습니다. 바이트 배열은 문자열이 아닙니다. 바이트 배열은 문자 그대로 바이트 배열일 뿐입니다. 문자열은 추상화 단계의 개념이고, 스트링은 이러한 문자열들의 집합입니다.

>>> s = '深入 Python'    
>>> len(s)               
9
>>> s[0]                 
'深'
>>> s + ' 3'             
'深入 Python 3'
  1. 파이썬에서 스트링을 생성하려면 간단히 따옴표로 묶어주면 됩니다. 파이썬 문자열은 홑 따옴표(')나 또는 이중 따옴표(")를 사용하여 정의할 수 있습니다.
  2. 파이썬 내장함수 len() 은 문자열의 길이를 리턴해줍니다. 즉, 문자의 갯수를 반환합니다. 이 함수는 리스트(list)나 튜플(tuple), 셋(set), 딕셔너리(dictionary) 에도 사용할 수 있습니다. 스트링은 문자열의 튜플로 생각할 수 있습니다.
  3. 마치 리스트안에서 개별적인 아이템을 꺼내듯, index 표기법을 이용하면 개별 문자를 추출할 수 있습니다.
  4. 리스트에서와 같이, + 연산자를 이용하여 string을 다른 string과 concatenate 할 수도 있습니다.

Formatting Strings

humansize.py 코드를 봅시다.:

[download humansize.py]

SUFFIXES = {1000: ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],         
            1024: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']}

def approximate_size(size, a_kilobyte_is_1024_bytes=True):
    '''Convert a file size to human-readable form.                          

    Keyword arguments:
    size -- file size in bytes
    a_kilobyte_is_1024_bytes -- if True (default), use multiples of 1024
                                if False, use multiples of 1000

    Returns: string

    '''                                                                     
    if size < 0:
        raise ValueError('number must be non-negative')                     

    multiple = 1024 if a_kilobyte_is_1024_bytes else 1000
    for suffix in SUFFIXES[multiple]:
        size /= multiple
        if size < multiple:
            return '{0:.1f} {1}'.format(size, suffix)                       

    raise ValueError('number too large')
  1. 'KB', 'MB', 'GB' 이는 모두 각각 string 입니다.
  2. Function docstring이라고 부르는 것인데, 이것 역시 아무튼 string 입니다. 여러 줄에 걸쳐서 작성할 수 있습니다. 시작할때 따옴표 세개를 연속으로 사용하면 됩니다.
  3. Function docstring을 끝낼때도 역시 따옴표 세개를 연속으로 사용합니다.
  4. 예외 메시지에도 string이 사용됩니다.
  5. 아...이건..string 같긴한데...쩝. 이건 대체 뭘까요?
파이썬 3에서는 특정 값을 string 내부에 포맷팅할 수 있습니다. 복잡한 예도 많지만, 일단 기본적으로 placeholder 하나를 사용하여 특정 값 하나를 string 에 넣어주는 예를 봅시다.
>>> username = 'mark'
>>> password = 'PapayaWhip'                             
>>> "{0}'s password is {1}".format(username, password)  
"mark's password is PapayaWhip"
  1. 실제로 제가 사용하는 패스워드는 아니예요.
  2. 여기서 많은 일이 벌어졌네요. 먼저 이 라인은 string literal 에 대한 메소드 호출입니다. string은 객체이고 객체는 메소드를 가지고 있습니다. 두번째, 이 메소드의 결과는 다시 string이 됩니다. 마지막으로, {0}{1}replacement fields 라고 부르는 것인데, format()메소드 내부에 인자로 전달되는 것들로 치환됩니다.

Compound Field Names

정수를 치환해주는 가장 간단한 예제를 보았습니다. format() 메소드의 인자 리스트에 인덱스를 사용하여 정수를 치환해주는 방식이었는데, 좀더 설명드리면 {0} 는 첫번째 인자를 치환하고 (예에서는 username 이었습니다), {1}는 두번째 인자 (예에서는 password 이었습니다) 를 치환하는 형식입니다. 물론 인자의 갯수만큼,그리고 원하는 만큼 리스트 인덱스를 더 넣을 수도 있습니다. 그러나 replacement field 에는 더 강력한 기능들이 있습니다.

>>> import humansize
>>> si_suffixes = humansize.SUFFIXES[1000]      
>>> si_suffixes
['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
>>> '1000{0[0]} = 1{0[1]}'.format(si_suffixes)  
'1000KB = 1MB'
  1. humansize 모듈 안에 있는 함수를 호출하는 대신, 모듈 내부에 정의된 자료구조인 “SI” (1000의 제곱)만 불러 들였습니다.(powers-of-1000).
  2. 얼핏 보기엔 복잡하지만, 그렇지 않습니다. {0}format() 메소드에 넘겨진 첫번째 인자인 si_suffixes를 의미합니다. 하지만 si_suffixes 는 리스트입니다. 따라서 {0[0]}format() 메소드에 넘겨진 첫번째 인자인 해당리스트의 첫번째 아이템인 'KB'이 되는거죠. 한편, {0[1]} 는 같은 리스트의 두번째 아이템인 'MB'이구요. curly brace 바깥에 있는 모든 것들, 가령 숫자 1000, 등호표시(=), 그리고 빈칸은 변화가 없습니다. 따라서, 출력되는 string은 '1000KB = 1MB'이 됩니다.

이 예제가 보여주듯이 format specifiers 는 파이썬 문법을 통하면 거의 모든 파이썬 자료구조의 특성과 각 개별 아이템들에 접근할 수 있습니다. 이를 가르켜 compound field names 이라고 하고, 다음과 같은 compound field names 들이“가능합니다”:

그럼 실제로 어떻게 사용가능한지 예제를 통해 알아봅시다:

>>> import humansize
>>> import sys
>>> '1MB = 1000{0.modules[humansize].SUFFIXES[1000][0]}'.format(sys)
'1MB = 1000KB'

이제 코드에 대한 설명을 보시죠

Format Specifiers

잠깐만요. 아직 끝난게 아니예요. humansize.py를 다시 한번 의심스런 눈초리로 바라봅시다:

if size < multiple:
    return '{0:.1f} {1}'.format(size, suffix)

{1}format() 메소드의 두번째 파라미터인 suffix 로 대체됩니다. 그런데 {0:.1f}은 뭐죠? 일단 두 부분으로 나눠서 생각할 수 있겠네요. {0}은 첫번째 파라미터로 대체 되는건 알겠죠? 그런데 :.1f 는 아리송하군요. 이건 format specifier라고 부르는 것인데요, 대체된 변수가 디스플레이될 때 어떤 방식으로 되야 하는지를 정의하는 구문입니다.

Format specifiers는 대체되는 텍스트를 다양한 방식으로 표현할 수 있도록 해줍니다. C 언어의 printf() 함수와 같이 자릿수를 추가한다던지, 글자 사이에 빈칸을 넣거나, 심지어 10진수를 16진수로 변환할 수도 있습니다.

replacement field 내에서 콜론은(:) format specifier가 시작하는 부분을 의미합니다. Format specifier “.1” 은 “소숫점 1의 자리에서 반올림 하라”는 의미입니다. ( 소숫점 뒤의 한 자리만 나타내라는 것이죠). Format specifier “f” 는 “fixed-point number” 라는 뜻이고요. 따라서, 이를 주어진 크기의 숫자 698.24접미어 'GB'에 적용하면, formatted string은 '698.2 GB'이 됩니다.

>>> '{0:.1f} {1}'.format(698.24, 'GB')
'698.2 GB'

format specifier에 대해 더 자세한 내용은 파이썬 공식 문서인 Format Specification Mini-Language를 참고하시기 바랍니다.

Other Common String Methods

string에는 Formatting 말고도 다른 유용한 기능들이 많이 있습니다.

>>> s = '''Finished files are the re-  
... sult of years of scientif-
... ic study combined with the
... experience of years.'''
>>> s.splitlines()                     
['Finished files are the re-',
 'sult of years of scientif-',
 'ic study combined with the',
 'experience of years.']
>>> print(s.lower())                   
finished files are the re-
sult of years of scientif-
ic study combined with the
experience of years.
>>> s.lower().count('f')               
6
  1. 파이썬 interactive shell 에서도 multiline string을 입력할 수 있습니다. multiline string을 입력할 때는 우선 따옴표 세개를 연달아 써주면 됩니다. ENTER 키를 누르면 다음 행으로 이동해 입력을 계속할 수 있습니다. 입력을 마칠 때는 시작할 때와 같이 따옴표 세개를 연달아 써주면 됩니다. 그리고 다시 ENTER 키를 누르면 이 커맨드가 실행되는 것입니다. (이 경우엔 해당 multiline string을 변수 s에 할당하게 됩니다).
  2. splitlines() 메소드는 multiline string 하나를 파라미터로 받아서 string의 list를 반환합니다. list 내 아이템은 각 라인의 string을 의미합니다. 다만, multiline string을 입력할 때 라인 끝에서 입력했던 ENTER 키에 해당하는 carriage return 값은 포함되지 않음에 주의하세요.
  3. lower() 메소드는 전체 string을 소문자로 변환시킵니다. (같은 이치로, upper() 메소드는 string을 대문자로 변환합니다.)
  4. count()메소드는 입력으로 전달된 substring이 포함된 갯수를 반환합니다. 네. 맞습니다. 문장 내에서 “f”는 정확히 6번 나옵니다!

다른 예도 봅시다. 마치 키-값의 쌍처럼 (key-value pair) 보이는, 가령 key1=value1&key2=value2 과 같은 포맷의 string이 있다고 생각해 봅시다. 이걸 적절히 쪼개서 {key1: value1, key2: value2}의 형태를 갖는 dictionary 로 만들고 싶습니다. 어떻게 하면 될까요?

>>> query = 'user=pilgrim&database=master&password=PapayaWhip'
>>> a_list = query.split('&')                                        
>>> a_list
['user=pilgrim', 'database=master', 'password=PapayaWhip']
>>> a_list_of_lists = [v.split('=', 1) for v in a_list if '=' in v]  
>>> a_list_of_lists
[['user', 'pilgrim'], ['database', 'master'], ['password', 'PapayaWhip']]
>>> a_dict = dict(a_list_of_lists)                                   
>>> a_dict
{'password': 'PapayaWhip', 'user': 'pilgrim', 'database': 'master'}
  1. split() 메소드는 구분자(delimiter)를 인자로서 반드시 제공해야 합니다. 제공된 구분자를 이용해 string을 쪼개어 리스트에 담아 반환합니다. 이 예에서 사용된 구분자는 ampersand 문자입니다만, 다른 어떤 문자도 구분자로 사용할 수 있습니다.
  2. 이제 string이 원소인 리스트를 갖게 되었습니다. 이 string의 형식은 key, = 표시, 그리고 뒤를 따르는 value 입니다. 전체 리스트를 순회하면서 각 아이템에 대해 key 와 value를 추출하기 위해서 list comprehension 와 string 메소드중 하나인 split 메소드를 사용하고 있습니다. split() 메소드의 두번째 파라미터는 필수가 아닙니다만, 사용되면 첫 번째 파라미터인 구분자를 이용하여 몇 번이나 분할하고 싶은지를 표현합니다. 1 은 “한 번만 분할” 하라는 뜻이므로, 여기서 사용된 split 메소드는 = 표시를 기준으로 하여 string을 나누게 되고, key 와 value 이렇게 두 개의 string 아이템을 가진 리스트를 반환하게 됩니다. (만약 'key=value=foo'.split('=') 과 같이 되있었다면, ['key', 'value', 'foo'] 와 같이 아이템이 3개인 리스트를 얻게 됩니다만, 'key=value=foo'.split('=',1)['key', 'value=foo']을 반환합니다. 이와 같이 분할되는 string안에 구분자를 포함시키는 것도 가능합니다)
  3. 마지막으로, dict() 함수를 이용하여 리스트의 리스트를 딕셔너리 자료형으로 변경합니다.

이번에 다뤘던 예제는 URL에 나오는 query 파라미터를 파싱하는 것과 유사하지만, 실제 URL 을 파싱하는 일은 훨씬 더 복잡합니다. 만약 URL query 파라미터들을 다뤄야 한다면 urllib.parse.parse_qs() 함수를 사용하는 편이 훨씬 낫습니다.

Slicing A String

string을 정의한후, 그 일부를 취해서 새로운 string으로 만드는것도 가능합니다.이를 일컬어 string을 slicing 한다고 말합니다. string을 slicing 하는 것은 list를 slicing 하는 것과 동작 방식이 똑같습니다. string이 character의 집합인, 일종의 리스트라는 사실을 잊지마세요.

>>> a_string = 'My alphabet starts where your alphabet ends.'
>>> a_string[3:11]           
'alphabet'
>>> a_string[3:-3]           
'alphabet starts where your alphabet en'
>>> a_string[0:2]            
'My'
>>> a_string[:18]            
'My alphabet starts'
>>> a_string[18:]            
' where your alphabet ends.'
  1. string의 일부분인 “slice”를 얻으려면 두 개의 인덱스인 시작 인덱스와 종료 인덱스를 지정해주면 됩니다. 이로 인해 반환되는 값은 string의 시작 인덱스부터 종료인덱스까지의 부분입니다.
  2. 리스트의 경우와 유사하게 마이너스 값을 인덱스로 주는 것도 가능합니다.
  3. string의 시작 인덱스는 0입니다. 따라서 a_string[0:2] 은 string의 처음부터 두번째까지의 slice를 반환합니다. 이때 시작되는 지점은 a_string[0] 이고, 종료되는 지점은 a_string[2]의 바로 앞 지점입니다.
  4. 만약 시작 지점을 의미하는 좌측의 인덱스가 0인 경우엔 생략도 가능합니다. 따라서 a_string[:18]a_string[0:18] 과 동일한 의미를 갖습니다.
  5. 만약 종료 지점을 의미하는 우측의 인덱스를 전체 string 크기와 같게 하려면 이를 생략해도 됩니다. 따라서 a_string[18:]a_string[18:44] 과 동일한 의미를 갖습니다. 예에서의 a_string[:18] 는 string의 앞 부분 18 글자을 잘라 반환하고, a_string[18:] 은 앞 부분의 18 글자를 제외한 뒷부분 전체를 반환합니다. 정리하면, a_string[:n] 은 앞 부분 n 만큼의 글자를, 그리고 a_string[n:] 은 글자의 전체 길이와 관계없이 나머지를 반환합니다.

Strings vs. Bytes

string 은 유니코드 문자의 연속된 집합으로 변경이 불가능한 (immutable) 속성을 가지고 있습니다. bytes object가 string과 다른점은 유니코드 문자가 아닌 0부터 255 사이의 숫자로 되어있다는 것입니다. 변경이 불가능한 속성을 가지고 있는 것은 같습니다.

>>> by = b'abcd\x65'  
>>> by
b'abcde'
>>> type(by)          
<class 'bytes'>
>>> len(by)           
5
>>> by += b'\xff'     
>>> by
b'abcde\xff'
>>> len(by)           
6
>>> by[0]             
97
>>> by[0] = 102       
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'bytes' object does not support item assignment
  1. bytes object를 정의하기 위해서는 b'' 라고 하는 “byte literal” 신택스를 사용합니다. byte literal 내에는 ASCII 문자 또는 0 부터 255 사이의 숫자를 16진수로 인코딩한 숫자인 \x00 부터 \xff 사이의 값을 넣을 수 있습니다.
  2. bytes object 의 타입은 bytes 입니다.
  3. bytes object 의 길이를 구하기 위해서는 내장함수인 len() 함수를 사용하면 됩니다. string 이나 list의 경우에서와 마찬가지 방식이지요.
  4. bytes objects 를 연결하기 위해서는 + 연산자를 사용하면 됩니다. 결과로 새로운 bytes object가 반환됩니다. 이 또한 string이나 list에서 사용하던 방식과 동일합니다.
  5. 5 바이트 크기의 bytes object 와 1 바이트 크기의 bytes object 를 연결하면 6 바이트 크기의 bytes object가 반환됩니다.
  6. list나 string에서 사용하던 방식과 마찬가지로 bytes object 내의 개별 바이트에 접근하기 위해 index를 사용할 수 있습니다. string에서 index로 접근한 요소를 string으로 얻어낼 수 있었듯이, bytes object에서 index를 잉요해 얻어낸 요소는 0 부터 255 사이의 정수값입니다.
  7. bytes object 는 일단 생성되고 나면 변경이 불가능합니다. 이 객체 내의 개별 요소를 바꿀 수 없습니다. 그렇게 하려면 두 가지 방법이 있습니다. 첫째로, string slicing 에서 사용한 방식을 그대로 사용하여 byte object를 분할한 다음 concatenation operator (+=) 를 이용하거나, 두번째로 bytes object를 개별요소에 대한 변경이 가능한 bytearray object 로 바꿔주면 됩니다.
>>> by = b'abcd\x65'
>>> barr = bytearray(by)  
>>> barr
bytearray(b'abcde')
>>> len(barr)             
5
>>> barr[0] = 102         
>>> barr
bytearray(b'fbcde')
  1. bytes object 를 bytearray object로 변경하려면 내장함수인 bytearray() 함수를 이용하면 됩니다.
  2. bytes object에서 사용하는 메소드나 오퍼레이션은 그대로 bytearray object에도 사용이 가능합니다.
  3. 유일한 차이점은 bytearray object로는 index를 이용하여 개별 바이트에 대한 변경이 가능하다는 것입니다. 다만 변경시키는 값은 0 부터 255 사이의 정수값이어야 합니다.

하지만, byte 와 string 은 절대로 섞지 마세요.

>>> by = b'd'
>>> s = 'abcde'
>>> by + s                       
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't concat bytes to str
>>> s.count(by)                  
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't convert 'bytes' object to str implicitly
>>> s.count(by.decode('ascii'))  
1
  1. byte 와 string은 절대 합칠 수 없습니다. 서로 다른 타입임을 명심하세요.
  2. string 안에서 byte object의 갯수를 셀 수는 없습니다. string은 string 이고, byte 는 byte 니까요. 다시 한번 강조하지만, 서로 다른 타입입니다. string 은 유니코드 문자의 연속임을 잊지마세요. 만약 어떤 byte를 특정 문자열 인코딩 방식으로 변환했을때, 이에 해당하는 string을 찾고 싶었던 거라면, 다른 방식으로 좀더 명시적으로 표현해줘야 합니다. Python 3에선 string 과 byte 간에 묵시적인 형변환은 절대 허용되지 않습니다.
  3. 이 라인에서는 by라는 byte object를 ascii 방식으로 디코딩한 후에 반환되는 string과 동일한 string의 갯수를 s 라는 string object 내에서 찾으라고 명시적으로 표현하고 있네요.

byte와 string은 상당히 밀접합니다. bytes object 에는 decode() 라는 메소드가 있는데, 메소드 파라미터로 인코딩을 의미하는 문자열을 받아, 이를 적용한 후 결과에 해당하는 string을 반환합니다. 반대로 string은 encode() 라는 메소드가 있어서, 마찬가지로 인코딩을 의미하는 문자열을 인자로 받아 이를 적용한 결과에 해당하는 bytes object를 반환하죠. 앞의 예제 3번을 보면, 디코딩하는 과정이 쉽게 이해가 될겁니다. ASCII 방식으로 인코딩된 일련의 바이트를 문자열로 이뤄진 string으로 변환하고 있죠. 예제에서는 byte object 가 ascii 코드인 영어로 작성되어 있기 때문에 ASCII 방식의 디코딩을 사용했지만, 만약 byte object가 다른 언어를 사용했다면, 설령 그 언어가 유니코드가 아닐지라도, 해당 인코딩 방식을 지정해주기만 하면 예제에서와 같이 byte object 와 string 간의 인코딩과 디코딩을 실행할 수 있습니다.

>>> a_string = '深入 Python'         
>>> len(a_string)
9
>>> by = a_string.encode('utf-8')    
>>> by
b'\xe6\xb7\xb1\xe5\x85\xa5 Python'
>>> len(by)
13
>>> by = a_string.encode('gb18030')  
>>> by
b'\xc9\xee\xc8\xeb Python'
>>> len(by)
11
>>> by = a_string.encode('big5')     
>>> by
b'\xb2`\xa4J Python'
>>> len(by)
11
>>> roundtrip = by.decode('big5')    
>>> roundtrip
'深入 Python'
>>> a_string == roundtrip
True
  1. 이건 string 이고 9개의 character로 되어 있습니다.
  2. 이건 bytes object 이고, 길이는 13 바이트입니다. a_stringUTF-8 형식으로 인코딩한 결과입니다.
  3. 이건 bytes object 이고, 길이는 11 바이트입니다. a_string 변수를 GB18030이라는 인코딩 포맷으로 인코딩한 결과입니다.
  4. 이건 bytes object이고, 길이는 11 바이트 입니다. a_string 변수를 Big5라는 인코딩 포맷으로 인코딩한 결과입니다. 결과로 나오는 바이트가 전혀 다르다는 것을 볼 수 있습니다.
  5. 이건 string이고, 9개의 character로 이루어져 있습니다. by라는 byte object 를 Big5 인코딩 포맷으로 디코딩한 결과입니다. 원래의 string 값과 동일하다는 것을 알 수 있습니다.

Postscript: Character Encoding Of Python Source Code

Python 3 는 여러분의 소스코드, 즉 .py 파일이 UTF-8 으로 인코딩 되있다고 가정합니다.

Python 2 에서는 .py 파일에 대한 디폴트 인코딩이 ASCII입니다. 이에 반해 Python 3 에서의 , 디폴트 인코딩은 UTF-8입니다.

만약 UTF-8이 아닌, 다른 인코딩 방식을 소스코드내에 적용하고 싶다면,. 소스코드 맨 첫라인에 인코딩 선언문을 명시하면 됩니다.아래 선언문은 해당 파이썬 소스코드의 인코딩을 windows-1252로 지정합니다:

# -*- coding: windows-1252 -*-

만약 첫라인에 hash-bang 커맨드를 입력하는 경우엔, 두번째 라인에 명시해도 됩니다. 아래 처럼요.

#!/usr/bin/python3
# -*- coding: windows-1252 -*-

더 자세한 내용은 PEP 263: Defining Python Source Code Encodings 표준 문서를 참조하시기 바랍니다.

Further Reading

파이썬에서 유니코드 사용법에 대해서는 아래 링크를 참조하세요.

유니코드에 대해 좀 더 알고 싶다면 아래 링크를 참조하세요:

다른 방식으로 문자열을 인코딩하는 방법을 알고 싶다면:

string에 대한 전반적인 내용과 스트링 포맷팅에 대해 궁금하다면:

© 2001–11 Mark Pilgrim