-
Python 에서 JSON 사용하기Python 2023. 12. 10. 22:30
Python 에서 JSON 사용하기
얼마 전 Python 에서 JSON 을 다뤄야 하는 문제가 있었는데 문서는 안 읽고 매번 똑같은 커맨드 사용했더니 문제를 못 풀었어요.
현타 오고 바보가 된 것 같아서 문서를 쭉 읽어보기로 해요.fp 를 다루는 dump, load 는 비슷하니, 더 자주 쓸 것 같은 dumps, loads 로 알아볼게요.
json.dumps()
def dumps(obj, *, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, cls=None, indent=None, separators=None, default=None, sort_keys=False, **kw)
변환표
- 파이썬 -> JSON 변환이 어떻게 되는지 알아봐요.
파이썬 JSON dict 오브젝트(object) list, tuple 배열(array) str 문자열(string) int, float, int와 float에서 파생된 열거형 숫자(number) True true False false None null skipkeys=False
True
설정 시 기본형(str, int, float, bool, None)이 아닌 딕셔너리 키는 TypeError 를 발생시키지 않고 건너뛰어요.- 딕셔너리 키는 immutable 한 값이어야 하는데 그러면 위의 기본형을 제외하고는 tuple 밖에 생각이 안나네요.
- dict key 값에 tuple 이 있을 때 사용하면 될 것 같아요.
data = {"name": "kingsubin", "city": "busan", ('tuple_key', ): "soob"} json.dumps(data) # TypeError: keys must be str, int, float, bool or None, not tuple json.dumps(data, skipkeys=True) # {"name": "kingsubin", "city": "busan"}
ensure_ascii=True
False
설정 시 비 아스키 문자들을 그대로 출력해 줘요.
data = {"이름": "수빈", "city": "부산", "pc": "mac"} json.dumps(data) # {"\uc774\ub984": "\uc218\ube48", "city": "\ubd80\uc0b0", "pc": "mac"} json.dumps(data, ensure_ascii=False) # {"이름": "수빈", "city": "부산", "pc": "mac"}
check_circular=True
- 순환 참조를 검사할지 여부예요.
- 이걸 False 로 설정하고 쓸 일이 있을지 잘 모르겠어요.
data = {} data["self"] = data json.dumps(data) # ValueError: Circular reference detected json.dumps(data, check_circular=False) # RecursionError: maximum recursion depth exceeded while encoding a JSON object
allow_nan=True
- JSON 범위를 벗어난 float 값(nan, inf, -inf)을 직렬화하면 ValueError 를 발생시켜요.
- JSON https://www.json.org/json-en.html 을 보면 NaN, Infinity, -Infinity 는 없는데 JavaScript equivalents (
NaN
,Infinity
,-Infinity
) 를 사용한다고 해요. - https://github.com/python/cpython/blob/8b58d12f6ccc654389976661d193361da932d96a/Lib/json/encoder.py#L35 INFINITY 를 정의해놓고 쓰고 있어요.
- nan, inf, -inf 를 막고 싶을 때 쓰려나 싶은데 자주 쓰진 않을 것 같아요.
data = {"nan": math.nan, "inf": float("inf"), "-inf": float("-inf")} json.dumps(data) # {"nan": NaN, "inf": Infinity, "-inf": -Infinity} json.dumps(data, allow_nan=False) # ValueError: Out of range float values are not JSON compliant
cls=None
- type[JSONEncoder] 을 받아요.
- 기본값으로는
json.JSONEncoder
를 사용하는데 특정한 객체를 JSON 으로 변환하는 방법을 정의하고 싶을 때 사용해요. - 많이 쓰이면서 serializable 하지 않은 set, datetime 같은 타입을 위해 커스텀인코더를 만들어 두고 쓰면 좋을 것 같은 생각이 들어요.
data = { "name": "kingsubin", "cities": ["seoul", "busan"], "nickname": "soob", "timestamp": datetime.now(), } class KingsubinEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, set): return list(obj) elif isinstance(obj, datetime): return obj.isoformat() return super().default(obj) json.dumps(data, cls=KingsubinEncoder) # {"name": "kingsubin", "cities": ["seoul", "busan"], "nickname": "soob", "timestamp": "2023-12-08T23:39:09.835149"}
indent=None
- 음수나 ""는 줄 넘김만 삽입하고, 양의 정수를 사용하면 레벨당 그만큼의 스페이스를 들여 쓰기 해요.
- 새로운 라인을 어떻게 만드는지 궁금하면 코드를 쭉 타고 봐야 할 것 같아요.
- https://github.com/python/cpython/blob/8b58d12f6ccc654389976661d193361da932d96a/Lib/json/encoder.py#L260C5-L260C21
data = {"name": "kingsubin", "city": "busan", "nickname": "soob"} json.dumps(data) # {"name": "kingsubin", "city": "busan", "nickname": "soob"} json.dumps(data, indent=0) """ { "name": "kingsubin", "city": "busan", "nickname": "soob" } """ json.dumps(data, indent="umm") """ { umm"name": "kingsubin", umm"city": "busan", umm"nickname": "soob" } """
separators=None
- tuple[str, str] 타입을 받아요. (item_separator, key_separator)
- 기본값은
(', ', ': ')
이어서, 각 separator 뒤에 한 칸씩 공백이 있어요. - 공백을 줄이고 싶으면
(',', ':')
이렇게 뒤에 공백을 제거하면 되겠어요.
data = {"name": "kingsubin", "city": "busan", "nickname": "soob"} json.dumps(data) # {"name": "kingsubin", "city": "busan", "nickname": "soob"} json.dumps(data, separators=(",", ":")) # {"name":"kingsubin","city":"busan","nickname":"soob"} json.dumps(data, separators=("item", "key")) # {"name"key"kingsubin"item"city"key"busan"item"nickname"key"soob"}
default=None
- 직렬화할 수 없는 객체에 대해 호출되는 함수예요.
- 예시로 많이 쓰이는 직렬화 할 수 없는 datetime 객체를 다뤄봐요.
data = { "name": "kingsubin", "city": "busan", "nickname": "soob", "timestamp": datetime.now(), } def serialize_datetime(object): if isinstance(object, datetime): return object.isoformat() return None json.dumps(data) # TypeError: Object of type datetime is not JSON serializable json.dumps(data, default=serialize_datetime) # {"name": "kingsubin", "city": "busan", "nickname": "soob", "timestamp": "2023-12-08T23:29:59.819621"}
sort_keys=False
True
설정 시 JSON 객체의 키를sorted()
를 사용해서 정렬해요.- key 에 str, int 가 섞여 있으면 정렬하지 못해요.
data = {"name": "kingsubin", "city": "busan", "nickname": "soob"} json.dumps(data) # {"name": "kingsubin", "city": "busan", "nickname": "soob"} json.dumps(data, sort_keys=True) # {"city": "busan", "name": "kingsubin", "nickname": "soob"} data = {3: "kingsubin", 1: "busan", "nickname": "soob"} json.dumps(data, sort_keys=True) # TypeError: '<' not supported between instances of 'str' and 'int'
json.loads()
def loads(s, *, cls=None, object_hook=None, parse_float=None, parse_int=None, parse_constant=None, object_pairs_hook=None, **kw)
변환표
- JSON -> Python 변환이 어떻게 되는지 알아봐요.
JSON 파이썬 오브젝트(object) dict 배열(array) list 문자열(string) str 숫자 (정수) int 숫자 (실수) float true True false False null None cls=None
- json.dumps() 에서 설명한 cls 와 같아요.
object_hook=None
- dict 대신 다른 객체를 만들 때 호출되는 함수를 지정하는 옵션이에요.
- custom decoders 를 구현하는 데 사용할 수 있어요.
- 예시로 json_string 에 datetime isoformat 이 존재하면 datetime 으로 바꿔주는 custom_hook 을 만들어봐요.
json_string = '{"name": "kingsubin", "cities": ["seoul", "busan"], "nickname": "soob", "timestamp": "2023-12-08T23:54:18.839402"}' def custom_hook(dct): for key, value in dct.items(): if isinstance(value, str): try: dct[key] = datetime.fromisoformat(value) except ValueError: pass return dct json.loads(json_string, object_hook=custom_object_hook) # {'name': 'kingsubin', 'cities': ['seoul', 'busan'], 'nickname': 'soob', 'timestamp': datetime.datetime(2023, 12, 8, 23, 54, 18, 839402)}
parse_float=None
- 디코딩될 모든 JSON float 에 대해서 호출돼요.
json_string = '{"name": "subin", "age": 30, "city": "busan", weight: 64.123}' def custom_float_parser(weight): return rount(float(weight), 2) json.loads(json-string, parse_float=custom_float_parser) # {'name': 'subin', 'age': 30, 'city': 'busan', 'weight': 64.12}
parse_int=None
- 디코딩 될 모든 JSON int 에 대해서 호출돼요.
- parse_float 과 같이 쓰면 될 것 같은데 적절히 쓰일만한 예시 생각이 안 나요.
parse_constant=None
- 디코딩 될
'-Infinity'
,'Infinity'
,'NaN'
에 대해서 호출돼요. - 자주 쓰이진 않을 것 같아요.
json_string = '{"inf": Infinity, "neginf": -Infinity, "nan": NaN}' def custom_constant_parser(x): if x == "Infinity": return "hello" elif x == "-Infinity": return "hello2" elif x == "NaN": return "hello3" return x json.loads(json_string) # {'inf': inf, 'neginf': -inf, 'nan': nan} json.loads(json_string, parse_constant=custom_constant_parser) # {'inf': 'hello', 'neginf': 'hello2', 'nan': 'hello3'}
object_pairs_hook=None
- object_hook 과 유사하지만, dict 대신 (key, value) 이터레이터를 받아요.
- object_hook 이 정의되어 있으면 object_pairs_hook 이 우선순위를 가져요.
json_string = '{"NAME": "subin", "City": "busan"}' def custom_pairs_hook(pairs): result = {} for key, value in pairs: result[key.lower()] = value return result json.loads(json_string) # {'NAME': 'subin', 'Age': 30, 'City': 'Busan'} json.loads(json_string, object_pairs_hook=custom_pairs_hook) # {'name': 'subin', 'age': 30, 'city': 'Busan'}
한 번 봤으니 이제는 Python 으로 JSON 을 다룰 때 뚝딱뚝딱 거릴 수 있겠어요.
날로 먹으려 하지 말고 문서를 보도록 해요...
그럼 안녕
참조:
- https://json.org/
- https://docs.python.org/ko/3.12/library/json.html#
- https://github.com/python/cpython/blob/3.12/Lib/json/__init__.py
'Python' 카테고리의 다른 글
pyenv 설정하기 (0) 2023.10.25