파이썬 코드 읽어보기 - json/encoder.py
json - JSON encoder and decoder
- 출처 : https://docs.python.org/3/library/json.html
- 코드 : https://github.com/python/cpython/blob/3.7/Lib/json/encoder.py
파이썬 코드 읽어보기 첫 번째 시리즈는 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를 보면
V
는 변수, f
메소드, C
클래스를 나타냅니다.
encoder.py
의 변수1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17ESCAPE = 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 | ESCAPE_DCT = { |
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 | try: |
일단 _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 | def py_encode_basestring(s): |
파이썬 스트링의 JSON 표현을 반환합니다. 실제 메소드의 반환값은 아래 보이는 것과 같습니다.1
2
3
4
5
6
7import 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 | def py_encode_basestring_ascii(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 | if n < 0x10000: |
u0000~uffff
에 해당하는 문자열의 경우 위에 보이는 것과 같은 형태로 반환합니다.
코드를 읽다보니 u0000~uffff
라는 범위는 어떤 기준으로 만들어진 것일까 라는 의문이 들었습니다.
- [Plane]https://en.wikipedia.org/wiki/Plane_(Unicode)
- [BMP]https://en.wikipedia.org/wiki/Plane_(Unicode)#Basic_Multilingual_Plane
위 링크를 참조하여 간단히 설명하면, Plane
은 65,536개의 연속된 코드포인트라고 할 수 있습니다. 총 17개의 plane
이 존재하며, u0000~uffff
은 십진수로 변환시 0~65,535이기에 첫 번째 Plane
이라고 할 수 있습니다.
BMP
는 Basic Multilingual Plane
의 약자이며, 첫 번째 plane
을 의미합니다. 첫 번째 plane
인 BMP
는 현대 언어의 거의 모든 문자와 기호를 포함하고 있다고 합니다. CJK의 문자와 기호가 BMP
의 많은 부분을 차지하고 있다고 합니다.
다음으로 else
절을 보겠습니다.
1 | else: |
surrogate pair
가 무엇인지 부터 알아야겠네요. 아래 두 글을 참조하여 알아보겠습니다.
1 | from stackoverflow |
1 | from Wikipedia |
네, 잘 알아보았습니다. 역시 Wikipedia
의 Example을 보니 좀 이해가 가네요.
다음으로 JSONEncoder
클래스를 보겠습니다.
위 클래스의 메소드를 보겠습니다. 참고로 가독성을 위해 메소드의 파라미터는 생략했습니다.
__init__()
default()
encode()
iterencode()
_make_iterencode()
생성자, 퍼블릭 메소드 셋, 프라이빗 메소드 하나를 가지고 있습니다.
__init__()
: 생성자의 경우 특별한 로직 없이 입력받은 파라미터를 인스턴스 변수로 지정합니다. 다만 몇몇 변수의 경우 입력되지 않은 경우 기존에 있는 변수 및 메소드를 사용합니다.
default()
: 어떤 값이 입력되어도 TypeError
를 일으키도록 돼있습니다.1
2
3def default(self, o):
raise TypeError(f'Object of type {o.__class__.__name__} '
f'is not JSON serializable')
1 | Specializing JSON object encoding:: |
위와 같이 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 | 'a') > encoder.encode( |
파라미터가 str
가 아니라면 인스턴스 메소드 iterencode()
의 반환값을 chunks
에 할당하고, chunks
가 list
또는 tuple
이 아닌경우 chunks
를 list
로 변환시켜 return ''.join(chunks)
으로 본 메소드는 마무리 됩니다.
iterencode(self, o, _one_shot=False)
: 입력된 오브젝트를 인코드한 후 yield
합니다.
1 | For example:: |
코드를 확인해 보니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 | def _iterencode(o, _current_indent_level) |
다음 로직은 markers
의 값에 따라 결정됩니다. markers
는 iterencode()
메소드 안에서 check_circular의 값에 따라 결정됩니다.
1 | def iterencode(self, o, _one_shot=False): |
check_circular
의 역할은 다음과 같습니다.
1 | If check_circular is true, then lists, dicts, and custom encoded |
인코딩중 일어날 수 있는 infinite recursion(무한 재귀)
를 막기위한 파라미터로 true
인 경우 circular references
여부를 검사합니다.
JSONEncoder
생성자의 check_circular=True
와 같은 기본 값을 가지고 있습니다. 특별히 False
로 지정하지 않는 한 순환 참조와 관련한 검사가 실행됩니다.
1 | def _iterencode(o, _current_indent_level) |
다시 _iterencode()
코드를 보겠습니다. 특별한 변경이 없다면 markers
는 빈 딕셔너리를 값으로 가지고 있습니다. 빌트인 메소드 id()
를 통해 입력받은 오브젝트의 식별값을 makers
의 key
, 오브젝트를 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
표현식에 익숙치 않아서 그런지 좀 찾아봤습니다.
- https://docs.python.org/3/whatsnew/3.3.html
- New syntax features:
- New
yield from
expression for generator delegation.
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
가 이어집니다.