IT이야기

기본 인코딩이 ASCII인데 Python이 유니코드 문자를 인쇄하는 이유는?

cyworld 2022. 3. 24. 21:52
반응형

기본 인코딩이 ASCII인데 Python이 유니코드 문자를 인쇄하는 이유는?

Python 2.6 쉘에서:

>>> import sys
>>> print sys.getdefaultencoding()
ascii
>>> print u'\xe9'
é
>>> 

"e" 문자는 ASCII의 일부가 아니며 인코딩을 지정하지 않았기 때문에 나는 인쇄문 뒤에 횡설수설이나 에러가 있을 것으로 기대했다.기본 인코딩인 ASCII가 무슨 뜻인지 이해가 안 가는 것 같아.

편집

나는 답안 섹션으로 편집을 옮기고 제안된 대로 수락했다.

다양한 답변의 단편적인 부분들 덕분에, 나는 우리가 설명을 붙일 수 있다고 생각한다.

유니코드 문자열 u'\xe9'을 인쇄하려고 시도함으로써 Python은 암시적으로 현재 sys.stdout.encoding에 저장된 인코딩 체계를 사용하여 해당 문자열을 인코딩하려고 시도한다.Python은 실제로 그것이 시작된 환경에서 이 설정을 선택한다.환경에서 적절한 인코딩을 찾을 수 없는 경우, 그 후에만 기본값인 ASCII로 되돌아간다.

예를 들어, 나는 UTF-8로 인코딩하는 bash shell을 사용한다. 여기서 Python을 시작하면 bash shell은 다음 설정을 사용한다.

$ python

>>> import sys
>>> print sys.stdout.encoding
UTF-8

잠시 파이톤 셸에서 벗어나 가짜 인코딩으로 바시의 환경을 설정해 봅시다.

$ export LC_CTYPE=klingon
# we should get some error message here, just ignore it.

그런 다음 파이썬 셸을 다시 시작하고 그것이 정말로 그것의 기본 아스키 인코딩으로 돌아가는지 확인하십시오.

$ python

>>> import sys
>>> print sys.stdout.encoding
ANSI_X3.4-1968

빙고!

이제 아스키 이외의 유니코드 문자를 출력하려고 하면 좋은 오류 메시지가 표시됨

>>> print u'\xe9'
UnicodeEncodeError: 'ascii' codec can't encode character u'\xe9' 
in position 0: ordinal not in range(128)

파이톤에서 나와서 배쉬껍질을 버리자.

이제 Python이 문자열을 출력한 후에 어떤 일이 발생하는지 관찰해 봅시다.이를 위해 먼저 그래픽 단자(Gnome Terminal) 내에서 bash shell을 시작하고(나는 Gnome Terminal을 사용함) ISO-8859-1 aka latin-1로 출력을 디코딩하도록 단자를 설정하십시오(그래픽 단자에는 일반적으로 드롭다운 메뉴 중 하나에 문자 인코딩을 설정할 수 있는 옵션이 있음).이것은 실제 셸 환경의 인코딩을 변경하지 않으며, 웹 브라우저와 같이 단말기 자체가 주어진 출력을 디코딩하는 방식만 바꾼다는 점에 유의하십시오.그러므로 당신은 셸의 환경에서 독립적으로 터미널의 인코딩을 변경할 수 있다.그런 다음 셸에서 Python을 시작하여 sys.stdout.encoding이 셸 환경의 인코딩(UTF-8 for me):

$ python

>>> import sys

>>> print sys.stdout.encoding
UTF-8

>>> print '\xe9' # (1)
é
>>> print u'\xe9' # (2)
é
>>> print u'\xe9'.encode('latin-1') # (3)
é
>>>

(1) python은 있는 그대로 바이너리 문자열을 출력하고, 단말기는 이를 수신하여 라틴-1 문자표와 그 값을 일치시키려고 한다.라틴-1, 0xe9 또는 233에서는 "e"라는 문자를 생성하므로 터미널이 이를 표시한다.

(2) python은 현재 sys.stdout.encoding에 설정된 모든 체계를 사용하여 유니코드 문자열을 암시적으로 인코딩하려고 시도하며, 이 경우 "UTF-8"이다. UTF-8 인코딩 후 결과 2진수 문자열은 '\xc3\xa9'이다(나중의 설명 참조).Terminal은 스트림을 이와 같이 수신하고, 라틴-1을 이용하여 0xc3a9를 디코딩하려고 하지만, 라틴-1은 0에서 255로 바뀌기 때문에 스트림을 한 번에 1바이트만 디코딩한다.0xc3a9는 길이가 2바이트로, 따라서 라틴-1 디코더는 0xc3(195), 0xa9(169)로 해석하고, 2개의 문자를 산출한다: WANG와 ©.

(3) python은 라틴-1 체계로 유니코드 코드 포인트 u'\xe9'(233)를 암호화한다.라틴-1 코드 포인트 범위는 0-255이며, 해당 범위 내에서 유니코드와 정확히 동일한 문자를 가리킨다.따라서 해당 범위의 유니코드 코드 포인트는 라틴-1로 인코딩할 때 동일한 값을 산출한다.그래서 라틴-1로 인코딩된 u'\xe9'(233)도 이진 문자열 '\xe9'을 산출한다.Terminal은 그 값을 받아 라틴-1 문자 지도에서 일치시키려 한다.케이스(1)와 마찬가지로 「e」를 산출하고, 그것이 디스플레이 된다.

이제 드롭다운 메뉴에서 터미널의 인코딩 설정을 UTF-8로 변경해 봅시다(예: 웹 브라우저의 인코딩 설정을 변경).Python을 중지하거나 셸을 다시 시작할 필요가 없다.터미널의 인코딩은 이제 파이썬의 인코딩과 일치한다.다시 인쇄해 봅시다.

>>> print '\xe9' # (4)

>>> print u'\xe9' # (5)
é
>>> print u'\xe9'.encode('latin-1') # (6)

>>>

(4) python은 있는 그대로 이진 문자열을 출력한다.터미널은 UTF-8로 그 스트림을 디코딩하려고 시도하지만 UTF-8은 0xe9 값을 이해하지 못해(나중의 설명 참조) 유니코드 코드 포인트로 변환할 수 없다.코드 포인트를 찾을 수 없고 문자가 인쇄되지 않음.

(5) python은 sys.stdout.encoding에 있는 것으로 유니코드 문자열을 암시적으로 인코딩하려고 시도한다.아직 "UTF-8"이야결과 이진 문자열은 '\xc3\xa9'이다.Terminal은 스트림을 수신하고 UTF-8을 사용하여 0xc3a9의 디코딩을 시도한다.유니코드 문자 지도에서 기호 "e"를 가리키는 코드 값 0xe9(233)를 산출한다.터미널에 "e"가 표시됨.

(6) python encodes unicode string with latin-1, it yields a binary string with the same value '\xe9'. Again, for the terminal this is pretty much the same as case (4).

Conclusions: - Python outputs non-unicode strings as raw data, without considering its default encoding. The terminal just happens to display them if its current encoding matches the data. - Python outputs Unicode strings after encoding them using the scheme specified in sys.stdout.encoding. - Python gets that setting from the shell's environment. - the terminal displays output according to its own encoding settings. - the terminal's encoding is independant from the shell's.


More details on unicode, UTF-8 and latin-1:

Unicode is basically a table of characters where some keys (code points) have been conventionally assigned to point to some symbols. e.g. by convention it's been decided that key 0xe9 (233) is the value pointing to the symbol 'é'. ASCII and Unicode use the same code points from 0 to 127, as do latin-1 and Unicode from 0 to 255. That is, 0x41 points to 'A' in ASCII, latin-1 and Unicode, 0xc8 points to 'Ü' in latin-1 and Unicode, 0xe9 points to 'é' in latin-1 and Unicode.

When working with electronic devices, Unicode code points need an efficient way to be represented electronically. That's what encoding schemes are about. Various Unicode encoding schemes exist (utf7, UTF-8, UTF-16, UTF-32). The most intuitive and straight forward encoding approach would be to simply use a code point's value in the Unicode map as its value for its electronic form, but Unicode currently has over a million code points, which means that some of them require 3 bytes to be expressed. To work efficiently with text, a 1 to 1 mapping would be rather impractical, since it would require that all code points be stored in exactly the same amount of space, with a minimum of 3 bytes per character, regardless of their actual need.

Most encoding schemes have shortcomings regarding space requirement, the most economic ones don't cover all unicode code points, for example ascii only covers the first 128, while latin-1 covers the first 256. Others that try to be more comprehensive end up also being wasteful, since they require more bytes than necessary, even for common "cheap" characters. UTF-16 for instance, uses a minimum of 2 bytes per character, including those in the ascii range ('B' which is 65, still requires 2 bytes of storage in UTF-16). UTF-32 is even more wasteful as it stores all characters in 4 bytes.

UTF-8 happens to have cleverly resolved the dilemma, with a scheme able to store code points with a variable amount of byte spaces. As part of its encoding strategy, UTF-8 laces code points with flag bits that indicate (presumably to decoders) their space requirements and their boundaries.

UTF-8 encoding of unicode code points in the ascii range (0-127):

0xxx xxxx  (in binary)
  • the x's show the actual space reserved to "store" the code point during encoding
  • The leading 0 is a flag that indicates to the UTF-8 decoder that this code point will only require 1 byte.
  • upon encoding, UTF-8 doesn't change the value of code points in that specific range (i.e. 65 encoded in UTF-8 is also 65). Considering that Unicode and ASCII are also compatible in the same range, it incidentally makes UTF-8 and ASCII also compatible in that range.

e.g. Unicode code point for 'B' is '0x42' or 0100 0010 in binary (as we said, it's the same in ASCII). After encoding in UTF-8 it becomes:

0xxx xxxx  <-- UTF-8 encoding for Unicode code points 0 to 127
*100 0010  <-- Unicode code point 0x42
0100 0010  <-- UTF-8 encoded (exactly the same)

UTF-8 encoding of Unicode code points above 127 (non-ascii):

110x xxxx 10xx xxxx            <-- (from 128 to 2047)
1110 xxxx 10xx xxxx 10xx xxxx  <-- (from 2048 to 65535)
  • the leading bits '110' indicate to the UTF-8 decoder the beginning of a code point encoded in 2 bytes, whereas '1110' indicates 3 bytes, 11110 would indicate 4 bytes and so forth.
  • the inner '10' flag bits are used to signal the beginning of an inner byte.
  • again, the x's mark the space where the Unicode code point value is stored after encoding.

e.g. 'é' Unicode code point is 0xe9 (233).

1110 1001    <-- 0xe9

When UTF-8 encodes this value, it determines that the value is larger than 127 and less than 2048, therefore should be encoded in 2 bytes:

110x xxxx 10xx xxxx   <-- UTF-8 encoding for Unicode 128-2047
***0 0011 **10 1001   <-- 0xe9
1100 0011 1010 1001   <-- 'é' after UTF-8 encoding
C    3    A    9

The 0xe9 Unicode code points after UTF-8 encoding becomes 0xc3a9. Which is exactly how the terminal receives it. If your terminal is set to decode strings using latin-1 (one of the non-unicode legacy encodings), you'll see é, because it just so happens that 0xc3 in latin-1 points to à and 0xa9 to ©.

When Unicode characters are printed to stdout, sys.stdout.encoding is used. A non-Unicode character is assumed to be in sys.stdout.encoding and is just sent to the terminal. On my system (Python 2):

>>> import unicodedata as ud
>>> import sys
>>> sys.stdout.encoding
'cp437'
>>> ud.name(u'\xe9') # U+00E9 Unicode codepoint
'LATIN SMALL LETTER E WITH ACUTE'
>>> ud.name('\xe9'.decode('cp437')) 
'GREEK CAPITAL LETTER THETA'
>>> '\xe9'.decode('cp437') # byte E9 decoded using code page 437 is U+0398.
u'\u0398'
>>> ud.name(u'\u0398')
'GREEK CAPITAL LETTER THETA'
>>> print u'\xe9' # Unicode is encoded to CP437 correctly
é
>>> print '\xe9'  # Byte is just sent to terminal and assumed to be CP437.
Θ

sys.getdefaultencoding() is only used when Python doesn't have another option.

Note that Python 3.6 or later ignores encodings on Windows and uses Unicode APIs to write Unicode to the terminal. No UnicodeEncodeError warnings and the correct character is displayed if the font supports it. Even if the font doesn't support it the characters can still be cut-n-pasted from the terminal to an application with a supporting font and it will be correct. Upgrade!

The Python REPL tries to pick up what encoding to use from your environment. If it finds something sane then it all Just Works. It's when it can't figure out what's going on that it bugs out.

>>> print sys.stdout.encoding
UTF-8

You have specified an encoding by entering an explicit Unicode string. Compare the results of not using the u prefix.

>>> import sys
>>> sys.getdefaultencoding()
'ascii'
>>> '\xe9'
'\xe9'
>>> u'\xe9'
u'\xe9'
>>> print u'\xe9'
é
>>> print '\xe9'

>>> 

In the case of \xe9 then Python assumes your default encoding (Ascii), thus printing ... something blank.

It works for me:

import sys
stdin, stdout = sys.stdin, sys.stdout
reload(sys)
sys.stdin, sys.stdout = stdin, stdout
sys.setdefaultencoding('utf-8')

As per Python default/implicit string encodings and conversions :

  • When printing unicode, it's encoded with <file>.encoding.
    • when the encoding is not set, the unicode is implicitly converted to str (since the codec for that is sys.getdefaultencoding(), i.e. ascii, any national characters would cause a UnicodeEncodeError)
    • for standard streams, the encoding is inferred from environment. It's typically set fot tty streams (from the terminal's locale settings), but is likely to not be set for pipes
      • so a print u'\xe9' is likely to succeed when the output is to a terminal, and fail if it's redirected. A solution is to encode() the string with the desired encoding before printing.
  • When printing str, the bytes are sent to the stream as is. What glyphs the terminal shows will depend on its locale settings.

ReferenceURL : https://stackoverflow.com/questions/2596714/why-does-python-print-unicode-characters-when-the-default-encoding-is-ascii

반응형