파이썬 코드 읽어보기 - json/encoder.py

json - JSON encoder and decoder

파이썬 코드 읽어보기 첫 번째 시리즈는 json입니다.

import json

개발 중 흔하게 만나던 json의 내부는 어떻게 되어 있는지 같이 확인해봅시다.

저는 cpython repository에서 코드를 확인해봤습니다. 파이썬 코드는 ./Lib/ 디렉토리 아래에서 확인할 수 있습니다.

cpython/Lib/json/ 디렉토리안의 내용입니다.

  • __init__.py
  • decoder.py
  • encoder.py
  • scanner.py
  • tool.py

본 글에서는 encoder.py를 확인해보겠습니다. 총 442줄로 구성된 파일로 생각보다 그렇게 길진 않습니다.

Pycharm을 통해 Structure를 보면

encoder-structure

V는 변수, f 메소드, C 클래스를 나타냅니다.

encoder.py 의 변수

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ESCAPE = re.compile(r'[\x00-\x1f\\"\b\f\n\r\t]')
ESCAPE_ASCII = re.compile(r'([\\"]|[^\ -~])')
HAS_UTF8 = re.compile(b'[\x80-\xff]')
ESCAPE_DCT = {
'\\': '\\\\',
'"': '\\"',
'\b': '\\b',
'\f': '\\f',
'\n': '\\n',
'\r': '\\r',
'\t': '\\t',
}
for i in range(0x20):
ESCAPE_DCT.setdefault(chr(i), '\\u{0:04x}'.format(i))
#ESCAPE_DCT.setdefault(chr(i), '\\u%04x' % (i,))

INFINITY = float('inf')

ESCAPE, ESCAPE_ASCII, HAS_UTF8 세 변수는 re.compile() 메소드의 반환 값으로 regular expression object 입니다.

1
2
>>> type(ESCAPE)
<class '_sre.SRE_Pattern'>

ESCAPE_DCT는 딕셔너리 타입의 변수로 0~31까지의 정수를 기반으로 만들어졌으며, 실제로 아래 표의 데이터를 가지고 있습니다.

Key Value
chr(index) \\u{0:04x}'.format(i)
이하 실제 값
‘\x00’ ‘\u0000’
‘\x01’ ‘\u0001’
‘\x02’ ‘\u0002’
‘\x03’ ‘\u0003’
‘\x04’ ‘\u0004’
‘\x05’ ‘\u0005’
‘\x06’ ‘\u0006’
‘\x07’ ‘\u0007’
‘\x08’ ‘\u0008’
‘\t’ ‘\t’
‘\n’ ‘\n’
‘\x0b’ ‘\u000b’
‘\x0c’ ‘\u000c’
‘\r’ ‘\r’
‘\x0e’ ‘\u000e’
‘\x0f’ ‘\u000f’
‘\x10’ ‘\u0010’
‘\x11’ ‘\u0011’
‘\x12’ ‘\u0012’
‘\x13’ ‘\u0013’
‘\x14’ ‘\u0014’
‘\x15’ ‘\u0015’
‘\x16’ ‘\u0016’
‘\x17’ ‘\u0017’
‘\x18’ ‘\u0018’
‘\x19’ ‘\u0019’
‘\x1a’ ‘\u001a’
‘\x1b’ ‘\u001b’
‘\x1c’ ‘\u001c’
‘\x1d’ ‘\u001e’
‘\x1e’ ‘\u001d’
‘\x1f’ ‘\u001f’
1
2
3
4
5
6
7
8
9
10
11
12
ESCAPE_DCT = {
'\\': '\\\\',
'"': '\\"',
'\b': '\\b',
'\f': '\\f',
'\n': '\\n',
'\r': '\\r',
'\t': '\\t',
}
for i in range(0x20):
ESCAPE_DCT.setdefault(chr(i), '\\u{0:04x}'.format(i))
#ESCAPE_DCT.setdefault(chr(i), '\\u%04x' % (i,))

setdefault()를 사용하여 키 밸류 맵핑을 했기때문에 ESCAPE_DCT 변수 선언 시 입력된 \n, \r, \t 등의 키는 값의 변화없이 첫 변수 선언 당시의 값을 그대로 유지하고 있습니다.

chr(i) 파이썬 빌트인 메소드로 입력된 정수 파라미터를 유니코드 스트링 형태로 반환합니다. 또 다른 빌트인 메소드 ord()의 반대이기도 합니다.

1
2
3
4
>>> chr(65)
'A'
>>> ord('A')
65

코드 해석과는 별개로 왜 주석 처리된 코드를 지우지 않고 유지하고 있는지 궁금하네요.

INFINITY 변수는 float(‘inf’) 를 할당받고 있습니다. 타입은 당연히 float 이네요.

1
2
>>> type(float('inf'))
<class 'float'>

다음은 메소드를 보겠습니다.

일단 아래 첫 번째 4줄의 코드는 _json 모듈로 부터 encode_basestring 메소드를 임포트 하고 있습니다. 찾을 수 없을 경우 None을 할당합니다.

1
2
3
4
try:
from _json import encode_basestring as c_encode_basestring
except ImportError:
c_encode_basestring = None

일단 _json 이 녀석부터 다시 짚고 넘어가고 싶네요.

1
2
3
4
5
6
7
8
9
10
11
12
>>> import json
>>> json
<module 'json' from '/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/json/__init__.py'>

# From mac OS
>>> import _json
>>> _json
<module '_json' from '/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/lib-dynload/_json.cpython-36m-darwin.so'>

# From linux
>>> _json
<module '_json' from '/usr/local/lib/python3.7/lib-dynload/_json.cpython-37m-x86_64-linux-gnu.so'>

json의 경우 지금 보고 있는 파일이 있는 모듈과 동일한 위치에 있습니다.

_json/lib-dynload/_json.<BUILD_NAME>.so 으로 보이는군요. cpython 빌드 후 생성되는 파일로 보이네요. 파일 경로 cpython/Modules/_json.c link

.so 확장자는 이번 기회에 처음보게 됐는데요, shared object라고 하네요.

참고용 -> stack overflow 답변 링크

1
2
3
4
5
6
7
8
9
def py_encode_basestring(s):
"""Return a JSON representation of a Python string
"""
def replace(match):
return ESCAPE_DCT[match.group(0)]
return '"' + ESCAPE.sub(replace, s) + '"'


encode_basestring = (c_encode_basestring or py_encode_basestring)

파이썬 스트링의 JSON 표현을 반환합니다. 실제 메소드의 반환값은 아래 보이는 것과 같습니다.

1
2
3
4
5
6
7
import json.encoder

>>> json.encoder.py_encode_basestring("abcdef")
'"abcdef"'

>>> json.encoder.py_encode_basestring("{'key': 'value'}")
'"{\'key\': \'value\'}"'

1
encode_basestring = (c_encode_basestring or py_encode_basestring)

cpython의 c_encode_basestring을 임포트할 수 있다면 c_encode_basestring 메소드를 사용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def py_encode_basestring_ascii(s):
"""Return an ASCII-only JSON representation of a Python string

"""
def replace(match):
s = match.group(0)
try:
return ESCAPE_DCT[s]
except KeyError:
n = ord(s)
if n < 0x10000:
return '\\u{0:04x}'.format(n)
#return '\\u%04x' % (n,)
else:
# surrogate pair
n -= 0x10000
s1 = 0xd800 | ((n >> 10) & 0x3ff)
s2 = 0xdc00 | (n & 0x3ff)
return '\\u{0:04x}\\u{1:04x}'.format(s1, s2)
return '"' + ESCAPE_ASCII.sub(replace, s) + '"'

파이썬 스트링의 JSON 표현을 반환합니다. 다만 ASCII 코드만 반환합니다.

py_encode_basestring_ascii() 안에 있는 replace() 메소드를 보겠습니다.

처음에 소개한 ESCAPE_DCT에서 match.group(0)을 키로 조회합니다. ESCAPE_DCT 객체는 0~31까지의 수로 만들어진 \x00 ~ \x1f 를 키로 가지고 있습니다. 그렇기에 이외의 키로 조회하면 KeyError를 일으켜 except 구문으로 넘어가게됩니다. ord(s)를 통해 n은 0이상의 정수를 할당받습니다.

1
2
3
4
5
6
7
8
if n < 0x10000:

>>> 0x10000
65536
>>> '\\u{0:04x}'.format(65535)
'\\uffff' # OK
>>> '\\u{0:04x}'.format(0)
'\\u0000' # OK

u0000~uffff에 해당하는 문자열의 경우 위에 보이는 것과 같은 형태로 반환합니다.

코드를 읽다보니 u0000~uffff라는 범위는 어떤 기준으로 만들어진 것일까 라는 의문이 들었습니다.

위 링크를 참조하여 간단히 설명하면, Plane은 65,536개의 연속된 코드포인트라고 할 수 있습니다. 총 17개의 plane이 존재하며, u0000~uffff은 십진수로 변환시 0~65,535이기에 첫 번째 Plane이라고 할 수 있습니다.

BMPBasic Multilingual Plane의 약자이며, 첫 번째 plane을 의미합니다. 첫 번째 planeBMP는 현대 언어의 거의 모든 문자와 기호를 포함하고 있다고 합니다. CJK의 문자와 기호가 BMP의 많은 부분을 차지하고 있다고 합니다.

다음으로 else 절을 보겠습니다.

1
2
3
4
5
6
else:
# surrogate pair
n -= 0x10000
s1 = 0xd800 | ((n >> 10) & 0x3ff)
s2 = 0xdc00 | (n & 0x3ff)
return '\\u{0:04x}\\u{1:04x}'.format(s1, s2)

surrogate pair가 무엇인지 부터 알아야겠네요. 아래 두 글을 참조하여 알아보겠습니다.

1
2
3
4
5
6
7
8
9
from stackoverflow 

The term "surrogate pair" refers to a means of encoding Unicode characters with high code-points in the UTF-16 encoding scheme.

In the Unicode character encoding, characters are mapped to values between 0x0 and 0x10FFFF.

Internally, Java uses the UTF-16 encoding scheme to store strings of Unicode text. In UTF-16, 16-bit (two-byte) code units are used. Since 16 bits can only contain the range of characters from 0x0 to 0xFFFF, some additional complexity is used to store values above this range (0x10000 to 0x10FFFF). This is done using pairs of code units known as surrogates.

The surrogate code units are in two ranges known as "high surrogates" and "low surrogates", depending on whether they are allowed at the start or end of the two-code-unit sequence.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from Wikipedia

Code points from the other planes (called Supplementary Planes) are encoded as two 16-bit code units called a surrogate pair, by the following scheme:

Examples
To encode U+10437 (𐐷) to UTF-16:

- Subtract 0x10000 from the code point, leaving 0x0437.
- For the high surrogate, shift right by 10 (divide by 0x400), then add 0xD800, resulting in 0x0001 + 0xD800 = 0xD801.
- For the low surrogate, take the low 10 bits (remainder of dividing by 0x400), then add 0xDC00, resulting in 0x0037 + 0xDC00 = 0xDC37.

To decode U+10437 (𐐷) from UTF-16:
- Take the high surrogate (0xD801) and subtract 0xD800, then multiply by 0x400, resulting in 0x0001 × 0x400 = 0x0400.
- Take the low surrogate (0xDC37) and subtract 0xDC00, resulting in 0x37.
- Add these two results together (0x0437), and finally add 0x10000 to get the final decoded UTF-32 code point, 0x10437.

네, 잘 알아보았습니다. 역시 Wikipedia의 Example을 보니 좀 이해가 가네요.

다음으로 JSONEncoder 클래스를 보겠습니다.

위 클래스의 메소드를 보겠습니다. 참고로 가독성을 위해 메소드의 파라미터는 생략했습니다.

  • __init__()
  • default()
  • encode()
  • iterencode()
  • _make_iterencode()

생성자, 퍼블릭 메소드 셋, 프라이빗 메소드 하나를 가지고 있습니다.

__init__() : 생성자의 경우 특별한 로직 없이 입력받은 파라미터를 인스턴스 변수로 지정합니다. 다만 몇몇 변수의 경우 입력되지 않은 경우 기존에 있는 변수 및 메소드를 사용합니다.

default() : 어떤 값이 입력되어도 TypeError를 일으키도록 돼있습니다.

1
2
3
def default(self, o):
raise TypeError(f'Object of type {o.__class__.__name__} '
f'is not JSON serializable')

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Specializing JSON object encoding::

>>> import json
>>> def encode_complex(obj):
... if isinstance(obj, complex):
... return [obj.real, obj.imag]
... raise TypeError(f'Object of type {obj.__class__.__name__} '
... f'is not JSON serializable')
...
>>> json.dumps(2 + 1j, default=encode_complex)
'[2.0, 1.0]'
>>> json.JSONEncoder(default=encode_complex).encode(2 + 1j)
'[2.0, 1.0]'
>>> ''.join(json.JSONEncoder(default=encode_complex).iterencode(2 + 1j))
'[2.0, 1.0]'

위와 같이 JSONEncoder를 통해 인스턴스를 만들때 default 파라미터에 자신이 만든 encode용 메소드를 지정할 수 있습니다.

encode() : 파이썬 자료구조의 오브젝트를 JSON형태의 스트링으로 반환합니다.

1
2
3
>>> from json.encoder import JSONEncoder
>>> JSONEncoder().encode({"foo": ["bar", "baz"]})
'{"foo": ["bar", "baz"]}'

조금 더 자세히 해당 메소드를 보겠습니다.

첫 번째로 입력받은 파라미터가 str 인 경우

isinstance()로 파라미터의 타입을 알아냅니다. str이 맞다면 인스턴스 변수 중 ensure_ascii의 값에 따라 encode_basestring_ascii() 또는 encode_basestring() 의 결과값을 반환하며 메소드가 끝이 납니다.

1
2
>>> encoder.encode('a')
'"a"'

파라미터가 str 가 아니라면 인스턴스 메소드 iterencode()의 반환값을 chunks에 할당하고, chunkslist 또는 tuple이 아닌경우 chunkslist로 변환시켜 return ''.join(chunks)으로 본 메소드는 마무리 됩니다.

iterencode(self, o, _one_shot=False) : 입력된 오브젝트를 인코드한 후 yield 합니다.

1
2
3
4
For example::

for chunk in JSONEncoder().iterencode(bigobject):
mysocket.write(chunk)

코드를 확인해 보니

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
if (_one_shot and c_make_encoder is not None
and self.indent is None):
_iterencode = c_make_encoder(
markers, self.default, _encoder, self.indent,
self.key_separator, self.item_separator, self.sort_keys,
self.skipkeys, self.allow_nan)
else:
_iterencode = _make_iterencode(
markers, self.default, _encoder, self.indent, floatstr,
self.key_separator, self.item_separator, self.sort_keys,
self.skipkeys, _one_shot)
return _iterencode(o, 0)
`

iterencode()는 파라미터를 입력받아 _make_iterencode() 메소드를 부르고 있습니다.

encoder.py 의 인코딩하는 주요 로직은 _make_iterencode()에 있었습니다. 프라이빗 메소드라서 안보고 넘어가려고 했는데 볼 수 밖에 없겠군요.

_make_iterencode() 는 내부에 세개의 프라이빗 메소드를 가지고 있습니다.

각각의 역할은 다음과 같습니다.

  • _iterencode() : 로직이 처음으로 시작되는 메소드입니다. 입력된 오브젝트의 타입에 따라 적절한 메소드를 실행시킵니다.
  • _iterencode_list() : 오브젝트가 리스트 혹은 튜플 타입
  • _iterencode_dict() : 오브젝트가 딕셔너리 타입

def _iterencode(o, _current_indent_level): 를 좀 더 자세히 보겠습니다.

입력된 오브젝트의 타입에 따라 적절한 메소드를 실행합니다. [str, None, Bool, int, float, list, tuple, dict] 이외의 타입은 else 절로 분기처리됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def _iterencode(o, _current_indent_level)

...

else:
if markers is not None:
markerid = id(o)
if markerid in markers:
raise ValueError("Circular reference detected")
markers[markerid] = o
o = _default(o)
yield from _iterencode(o, _current_indent_level)
if markers is not None:
del markers[markerid]

다음 로직은 markers 의 값에 따라 결정됩니다. markersiterencode() 메소드 안에서 check_circular의 값에 따라 결정됩니다.

1
2
3
4
5
def iterencode(self, o, _one_shot=False):
if self.check_circular:
markers = {}
else:
markers = None

check_circular의 역할은 다음과 같습니다.

1
2
3
4
If check_circular is true, then lists, dicts, and custom encoded
objects will be checked for circular references during encoding to
prevent an infinite recursion (which would cause an OverflowError).
Otherwise, no such check takes place.

인코딩중 일어날 수 있는 infinite recursion(무한 재귀) 를 막기위한 파라미터로 true인 경우 circular references 여부를 검사합니다.

JSONEncoder 생성자의 check_circular=True 와 같은 기본 값을 가지고 있습니다. 특별히 False로 지정하지 않는 한 순환 참조와 관련한 검사가 실행됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def _iterencode(o, _current_indent_level)

...

else:
if markers is not None:
markerid = id(o)
if markerid in markers:
raise ValueError("Circular reference detected")
markers[markerid] = o
o = _default(o)
yield from _iterencode(o, _current_indent_level)
if markers is not None:
del markers[markerid]

다시 _iterencode() 코드를 보겠습니다. 특별한 변경이 없다면 markers는 빈 딕셔너리를 값으로 가지고 있습니다. 빌트인 메소드 id() 를 통해 입력받은 오브젝트의 식별값을 makerskey, 오브젝트를 value로 할당합니다.

다음으로 _default() 메소드로 오브젝트 직렬화 가능한 객체로 변환시킵니다. 만약 직렬화 가능하지 않다면 TypeError를 일으킵니다.

json - python3 official document에서 default() 메소드에 관한 설명을 가져왔습니다.

1
returns a serializable object for o if possible, otherwise it should call the superclass implementation (to raise TypeError).

직렬화 된 객체를 다시 본 메소드(_iterencode())의 첫 번째 파라미터로 넣어 메소드를 실행합니다. yield from 예약 키워드를 통해 본 메소드의 반환 값을 yield 합니다. 그 후 markers 에서 makerid 키를 제거합니다.

(yield 번역을 어떻게 해야할지 잘 모르겠네요…)

yield from 표현식에 익숙치 않아서 그런지 좀 찾아봤습니다.

generator delegation 제너레이터 위임이라… 정확한 내용은 문서를 좀 더 읽어보겠습니다. 추가로 기존에 있던 yeild 와 어떤 차이가 있는지도 알아두고 싶네요.

일단 3.3에 추가된 기능이군요.

1
PEP 380 adds the 'yield from' expression, allowing a generator to delegate part of its operations to another generator. This allows a section of code containing yield to be factored out and placed in another generator. Additionally, the subgenerator is allowed to return with a value, and the value is made available to the delegating generator.

해석은 여러분들 각자에게 맡기겠습니다.

이것으로 json/encoder.py 읽어보기가 끝났습니다. 다음으로는 decoder.py 가 이어집니다.