Python JSON 객체에서 중첩된 키 접근 시 KeyError 처리 방법

PythonBeginner
지금 연습하기

소개

Python 의 다재다능함은 널리 사용되는 데이터 교환 형식인 JSON 데이터 작업 능력으로 확장됩니다. 하지만 중첩된 JSON 객체를 다룰 때, 데이터 처리 워크플로우를 방해할 수 있는 악명 높은 KeyError 를 만날 수 있습니다. 이 튜토리얼은 KeyError 를 효과적으로 처리하고 Python 코드가 복잡한 JSON 구조를 원활하게 탐색할 수 있도록 하는 전략을 안내합니다.

Python 에서 JSON 객체 생성 및 이해

JSON (JavaScript Object Notation) 은 사람이 읽고 쓰기 쉽고, 기계가 파싱하기 쉬운 가벼운 데이터 교환 형식입니다. Python 에서 JSON 객체는 중괄호 {}로 묶인 키 - 값 쌍인 딕셔너리로 표현됩니다.

간단한 Python 파일을 생성하고 기본적인 JSON 객체를 정의하는 것으로 시작해 보겠습니다.

  1. WebIDE (VS Code) 를 열고 왼쪽 탐색 패널에서 "New File" 아이콘을 클릭하여 새 파일을 만듭니다.

  2. 파일 이름을 json_basics.py로 지정하고 다음 코드를 추가합니다.

## Define a simple JSON object as a Python dictionary
person = {
    "name": "John Doe",
    "age": 30,
    "email": "john.doe@example.com"
}

## Access and print values from the dictionary
print("Person details:")
print(f"Name: {person['name']}")
print(f"Age: {person['age']}")
print(f"Email: {person['email']}")
  1. Ctrl+S를 누르거나 메뉴에서 "File" > "Save"를 선택하여 파일을 저장합니다.

  2. 터미널을 열어 (메뉴에서: "Terminal" > "New Terminal") 다음을 입력하여 스크립트를 실행합니다.

python3 json_basics.py

다음과 같은 출력을 볼 수 있습니다.

Person details:
Name: John Doe
Age: 30
Email: john.doe@example.com

이제 더 복잡한 중첩된 JSON 객체를 만들어 보겠습니다. json_basics.py 파일을 다음 코드로 업데이트합니다.

## Define a nested JSON object
user_data = {
    "person": {
        "name": "John Doe",
        "age": 30,
        "address": {
            "street": "123 Main St",
            "city": "Anytown",
            "state": "CA",
            "zip": "12345"
        }
    },
    "hobbies": ["reading", "hiking", "photography"]
}

## Access and print values from the nested dictionary
print("\nUser Data:")
print(f"Name: {user_data['person']['name']}")
print(f"Age: {user_data['person']['age']}")
print(f"Street: {user_data['person']['address']['street']}")
print(f"City: {user_data['person']['address']['city']}")
print(f"First hobby: {user_data['hobbies'][0]}")

파일을 저장하고 다시 실행합니다. 다음을 볼 수 있습니다.

Person details:
Name: John Doe
Age: 30
Email: john.doe@example.com

User Data:
Name: John Doe
Age: 30
Street: 123 Main St
City: Anytown
First hobby: reading

이것은 JSON 객체에서 중첩된 값에 접근하는 방법을 보여줍니다. 다음 단계에서는 존재하지 않는 키에 접근하려고 할 때 어떤 일이 발생하는지, 그리고 그 상황을 처리하는 방법을 살펴보겠습니다.

Try-Except 를 사용하여 KeyError 발생 및 처리

딕셔너리에 존재하지 않는 키에 접근하려고 하면 Python 은 KeyError를 발생시킵니다. 이는 중첩된 JSON 객체로 작업할 때, 특히 데이터 구조가 일관되지 않거나 불완전할 때 흔히 발생하는 문제입니다.

이 문제를 탐구하기 위해 새 파일을 만들어 보겠습니다.

  1. WebIDE 에서 key_error_handling.py라는 새 파일을 만듭니다.

  2. KeyError를 보여주기 위해 다음 코드를 추가합니다.

## Define a nested JSON object with incomplete data
user_data = {
    "person": {
        "name": "John Doe",
        "age": 30,
        "address": {
            "street": "123 Main St",
            "city": "Anytown",
            "state": "CA"
            ## Note: zip code is missing
        }
    },
    "hobbies": ["reading", "hiking", "photography"]
}

## This will cause a KeyError
print("Trying to access a non-existent key:")
try:
    zip_code = user_data["person"]["address"]["zip"]
    print(f"Zip code: {zip_code}")
except KeyError as e:
    print(f"KeyError encountered: {e}")
    print("The key 'zip' does not exist in the address dictionary.")
  1. 파일을 저장하고 터미널을 열어 다음을 입력하여 실행합니다.
python3 key_error_handling.py

다음과 유사한 출력을 볼 수 있습니다.

Trying to access a non-existent key:
KeyError encountered: 'zip'
The key 'zip' does not exist in the address dictionary.

이것은 try-except 블록을 사용하여 KeyError를 처리하는 기본적인 방법을 보여줍니다. 이제 중첩된 딕셔너리에서 여러 잠재적인 키 오류를 처리하도록 예제를 확장해 보겠습니다.

## Add this code to your key_error_handling.py file

print("\nHandling multiple potential KeyErrors:")

## Function to safely access nested dictionary values
def safe_get_nested_value(data, keys_list):
    """
    Safely access nested dictionary values using try-except.

    Args:
        data: The dictionary to navigate
        keys_list: A list of keys to access in sequence

    Returns:
        The value if found, otherwise a message about the missing key
    """
    current = data
    try:
        for key in keys_list:
            current = current[key]
        return current
    except KeyError as e:
        return f"Unable to access key: {e}"

## Test the function with various paths
paths_to_test = [
    ["person", "name"],                     ## Should work
    ["person", "address", "zip"],           ## Should fail
    ["person", "contact", "phone"],         ## Should fail
    ["hobbies", 0]                          ## Should work
]

for path in paths_to_test:
    result = safe_get_nested_value(user_data, path)
    print(f"Path {path}: {result}")
  1. 파일을 저장하고 다시 실행합니다. 다음과 유사한 출력을 볼 수 있습니다.
Trying to access a non-existent key:
KeyError encountered: 'zip'
The key 'zip' does not exist in the address dictionary.

Handling multiple potential KeyErrors:
Path ['person', 'name']: John Doe
Path ['person', 'address', 'zip']: Unable to access key: 'zip'
Path ['person', 'contact', 'phone']: Unable to access key: 'contact'
Path ['hobbies', 0]: reading

이것은 JSON 객체에서 중첩된 키에 접근할 때 잠재적인 KeyError 예외를 처리하기 위해 try-except 를 사용하는 방법을 보여줍니다. 함수는 키를 찾을 수 없을 때 적절한 메시지를 반환하며, 이는 처리되지 않은 예외로 인해 프로그램이 충돌하는 것보다 훨씬 좋습니다.

다음 단계에서는 dict.get() 메서드를 사용하여 훨씬 더 우아한 접근 방식을 탐구해 보겠습니다.

안전한 접근을 위한 dict.get() 메서드 사용

dict.get() 메서드는 KeyError를 발생시키지 않고 딕셔너리 값에 접근하는 더 우아한 방법을 제공합니다. 이 메서드를 사용하면 키가 존재하지 않는 경우 반환할 기본값을 지정할 수 있습니다.

이 접근 방식을 탐구하기 위해 새 파일을 만들어 보겠습니다.

  1. WebIDE 에서 dict_get_method.py라는 새 파일을 만듭니다.

  2. 다음 코드를 추가합니다.

## Define a nested JSON object with incomplete data
user_data = {
    "person": {
        "name": "John Doe",
        "age": 30,
        "address": {
            "street": "123 Main St",
            "city": "Anytown",
            "state": "CA"
            ## Note: zip code is missing
        }
    },
    "hobbies": ["reading", "hiking", "photography"]
}

## Using dict.get() for safer access
print("Using dict.get() method:")
zip_code = user_data["person"]["address"].get("zip", "Not provided")
print(f"Zip code: {zip_code}")

## This approach still has a problem with deeper nesting
print("\nProblem with deeper nesting:")
try:
    ## This works for 'person' key that exists
    contact = user_data.get("person", {}).get("contact", {}).get("phone", "Not available")
    print(f"Contact phone: {contact}")

    ## But this will still raise KeyError if any middle key doesn't exist
    non_existent = user_data["non_existent_key"]["some_key"]
    print(f"This won't print due to KeyError: {non_existent}")
except KeyError as e:
    print(f"KeyError encountered: {e}")
  1. 파일을 저장하고 터미널을 열어 다음을 입력하여 실행합니다.
python3 dict_get_method.py

다음과 유사한 출력을 볼 수 있습니다.

Using dict.get() method:
Zip code: Not provided

Problem with deeper nesting:
Contact phone: Not available
KeyError encountered: 'non_existent_key'

이제 중첩된 딕셔너리 구조를 안전하게 탐색할 수 있는 더 강력한 솔루션을 구현해 보겠습니다.

## Add this code to your dict_get_method.py file

print("\nSafer nested dictionary navigation:")

def deep_get(dictionary, keys, default=None):
    """
    Safely access nested dictionary values using dict.get().

    Args:
        dictionary: The dictionary to navigate
        keys: A list of keys to access in sequence
        default: The default value to return if any key is missing

    Returns:
        The value if the complete path exists, otherwise the default value
    """
    result = dictionary
    for key in keys:
        if isinstance(result, dict):
            result = result.get(key, default)
            if result == default:
                return default
        else:
            return default
    return result

## Test our improved function
test_paths = [
    ["person", "name"],                     ## Should work
    ["person", "address", "zip"],           ## Should return default
    ["person", "contact", "phone"],         ## Should return default
    ["non_existent_key", "some_key"],       ## Should return default
    ["hobbies", 0]                          ## Should work with list index
]

for path in test_paths:
    value = deep_get(user_data, path, "Not available")
    path_str = "->".join([str(k) for k in path])
    print(f"Path {path_str}: {value}")

## Practical example: formatting user information safely
print("\nFormatted user information:")
name = deep_get(user_data, ["person", "name"], "Unknown")
city = deep_get(user_data, ["person", "address", "city"], "Unknown")
state = deep_get(user_data, ["person", "address", "state"], "Unknown")
zip_code = deep_get(user_data, ["person", "address", "zip"], "Unknown")
primary_hobby = deep_get(user_data, ["hobbies", 0], "None")

print(f"User {name} lives in {city}, {state} {zip_code}")
print(f"Primary hobby: {primary_hobby}")
  1. 파일을 저장하고 다시 실행합니다. 다음과 유사한 출력을 볼 수 있습니다.
Using dict.get() method:
Zip code: Not provided

Problem with deeper nesting:
Contact phone: Not available
KeyError encountered: 'non_existent_key'

Safer nested dictionary navigation:
Path person->name: John Doe
Path person->address->zip: Not available
Path person->contact->phone: Not available
Path non_existent_key->some_key: Not available
Path hobbies->0: reading

Formatted user information:
User John Doe lives in Anytown, CA Unknown
Primary hobby: reading

우리가 만든 deep_get() 함수는 KeyError 예외를 발생시키지 않고 딕셔너리에서 중첩된 값에 접근하는 강력한 방법을 제공합니다. 이 접근 방식은 구조가 일관되지 않거나 불완전할 수 있는 외부 소스의 JSON 데이터를 사용할 때 특히 유용합니다.

중첩된 JSON 처리를 위한 고급 기술

중첩된 JSON 객체에서 KeyError를 처리하는 기본적인 접근 방식을 살펴봤으므로, 코드를 더욱 강력하고 유지 관리 가능하게 만들 수 있는 몇 가지 고급 기술을 살펴보겠습니다.

  1. WebIDE 에서 advanced_techniques.py라는 새 파일을 만듭니다.

  2. 여러 고급 기술을 구현하기 위해 다음 코드를 추가합니다.

## Exploring advanced techniques for handling nested JSON objects
import json
from functools import reduce
import operator

## Sample JSON data with various nested structures
json_str = """
{
    "user": {
        "id": 12345,
        "name": "Jane Smith",
        "profile": {
            "bio": "Software developer with 5 years of experience",
            "social_media": {
                "twitter": "@janesmith",
                "linkedin": "jane-smith"
            }
        },
        "skills": ["Python", "JavaScript", "SQL"],
        "employment": {
            "current": {
                "company": "Tech Solutions Inc.",
                "position": "Senior Developer"
            },
            "previous": [
                {
                    "company": "WebDev Co",
                    "position": "Junior Developer",
                    "duration": "2 years"
                }
            ]
        }
    }
}
"""

## Parse the JSON string into a Python dictionary
data = json.loads(json_str)
print("Loaded JSON data structure:")
print(json.dumps(data, indent=2))  ## Pretty-print the JSON data

print("\n----- Technique 1: Using a path string with split -----")
def get_by_path(data, path_string, default=None, separator='.'):
    """
    Access a nested value using a dot-separated path string.

    Example:
        get_by_path(data, "user.profile.social_media.twitter")
    """
    keys = path_string.split(separator)

    ## Start with the root data
    current = data

    ## Try to traverse the path
    for key in keys:
        ## Handle array indices in the path (e.g., "employment.previous.0.company")
        if key.isdigit() and isinstance(current, list):
            index = int(key)
            if 0 <= index < len(current):
                current = current[index]
            else:
                return default
        elif isinstance(current, dict) and key in current:
            current = current[key]
        else:
            return default

    return current

## Test the function
paths_to_check = [
    "user.name",
    "user.profile.social_media.twitter",
    "user.skills.1",
    "user.employment.current.position",
    "user.employment.previous.0.company",
    "user.contact.email",  ## This path doesn't exist
]

for path in paths_to_check:
    value = get_by_path(data, path, "Not available")
    print(f"{path}: {value}")

print("\n----- Technique 2: Using functools.reduce -----")
def get_by_path_reduce(data, path_list, default=None):
    """
    Access a nested value using reduce and operator.getitem.
    This approach is more concise but less flexible with error handling.
    """
    try:
        return reduce(operator.getitem, path_list, data)
    except (KeyError, IndexError, TypeError):
        return default

## Test the reduce-based function
path_lists = [
    ["user", "name"],
    ["user", "profile", "social_media", "twitter"],
    ["user", "skills", 1],
    ["user", "employment", "current", "position"],
    ["user", "employment", "previous", 0, "company"],
    ["user", "contact", "email"],  ## This path doesn't exist
]

for path in path_lists:
    value = get_by_path_reduce(data, path, "Not available")
    path_str = "->".join([str(p) for p in path])
    print(f"{path_str}: {value}")

print("\n----- Technique 3: Class-based approach -----")
class SafeDict:
    """
    A wrapper class for dictionaries that provides safe access to nested keys.
    """
    def __init__(self, data):
        self.data = data

    def get(self, *keys, default=None):
        """
        Access nested keys safely, returning default if any key is missing.
        """
        current = self.data
        for key in keys:
            if isinstance(current, dict) and key in current:
                current = current[key]
            elif isinstance(current, list) and isinstance(key, int) and 0 <= key < len(current):
                current = current[key]
            else:
                return default
        return current

    def __str__(self):
        return str(self.data)

## Create a SafeDict instance
safe_data = SafeDict(data)

## Test the class-based approach
print(f"User name: {safe_data.get('user', 'name', default='Unknown')}")
print(f"Twitter handle: {safe_data.get('user', 'profile', 'social_media', 'twitter', default='None')}")
print(f"Second skill: {safe_data.get('user', 'skills', 1, default='None')}")
print(f"Current position: {safe_data.get('user', 'employment', 'current', 'position', default='None')}")
print(f"Previous company: {safe_data.get('user', 'employment', 'previous', 0, 'company', default='None')}")
print(f"Email (missing): {safe_data.get('user', 'contact', 'email', default='Not provided')}")
  1. 파일을 저장하고 터미널을 열어 다음을 입력하여 실행합니다.
python3 advanced_techniques.py

JSON 객체에서 중첩된 값에 안전하게 접근하는 다양한 방법을 보여주는 출력을 볼 수 있습니다. 각 기술에는 고유한 장점이 있습니다.

  • Path string with split: 경로가 문자열로 정의된 경우 (예: 구성 파일) 사용하기 쉽습니다.
  • Reduce with operator.getitem: 더 간결한 접근 방식으로, 함수형 프로그래밍에 유용합니다.
  • Class-based approach: 코드를 더 깨끗하고 유지 관리 가능하게 만드는 재사용 가능한 래퍼를 제공합니다.

이제 이러한 기술을 사용하여 더 복잡한 JSON 데이터 구조를 처리하는 실용적인 응용 프로그램을 만들어 보겠습니다.

## Create a new file called practical_example.py
  1. practical_example.py라는 새 파일을 만들고 다음 코드를 추가합니다.
import json

## Sample JSON data representing a customer order system
json_str = """
{
    "orders": [
        {
            "order_id": "ORD-001",
            "customer": {
                "id": "CUST-101",
                "name": "Alice Johnson",
                "contact": {
                    "email": "alice@example.com",
                    "phone": "555-1234"
                }
            },
            "items": [
                {
                    "product_id": "PROD-A1",
                    "name": "Wireless Headphones",
                    "price": 79.99,
                    "quantity": 1
                },
                {
                    "product_id": "PROD-B2",
                    "name": "Smartphone Case",
                    "price": 19.99,
                    "quantity": 2
                }
            ],
            "shipping_address": {
                "street": "123 Maple Ave",
                "city": "Springfield",
                "state": "IL",
                "zip": "62704"
            },
            "payment": {
                "method": "credit_card",
                "status": "completed"
            }
        },
        {
            "order_id": "ORD-002",
            "customer": {
                "id": "CUST-102",
                "name": "Bob Smith",
                "contact": {
                    "email": "bob@example.com"
                    // phone missing
                }
            },
            "items": [
                {
                    "product_id": "PROD-C3",
                    "name": "Bluetooth Speaker",
                    "price": 49.99,
                    "quantity": 1
                }
            ],
            "shipping_address": {
                "street": "456 Oak St",
                "city": "Rivertown",
                "state": "CA"
                // zip missing
            }
            // payment information missing
        }
    ]
}
"""

## Parse the JSON data
try:
    data = json.loads(json_str)
except json.JSONDecodeError as e:
    print(f"Invalid JSON: {e}")
    exit(1)

## Import our SafeDict class from the previous example
class SafeDict:
    def __init__(self, data):
        self.data = data

    def get(self, *keys, default=None):
        current = self.data
        for key in keys:
            if isinstance(current, dict) and key in current:
                current = current[key]
            elif isinstance(current, list) and isinstance(key, int) and 0 <= key < len(current):
                current = current[key]
            else:
                return default
        return current

    def __str__(self):
        return str(self.data)

## Create a SafeDict instance
safe_data = SafeDict(data)

print("Processing order information safely...")

## Process each order
for i in range(10):  ## Try to process up to 10 orders
    ## Use SafeDict to avoid KeyError
    order = safe_data.get('orders', i)
    if order is None:
        print(f"No order found at index {i}")
        break

    ## Create a SafeDict for this specific order
    order_dict = SafeDict(order)

    ## Safely extract order information
    order_id = order_dict.get('order_id', default='Unknown')
    customer_name = order_dict.get('customer', 'name', default='Unknown Customer')
    customer_email = order_dict.get('customer', 'contact', 'email', default='No email provided')
    customer_phone = order_dict.get('customer', 'contact', 'phone', default='No phone provided')

    ## Process shipping information
    shipping = order_dict.get('shipping_address', default={})
    shipping_dict = SafeDict(shipping)
    shipping_address = f"{shipping_dict.get('street', default='')}, " \
                       f"{shipping_dict.get('city', default='')}, " \
                       f"{shipping_dict.get('state', default='')} " \
                       f"{shipping_dict.get('zip', default='')}"

    ## Process payment information
    payment_status = order_dict.get('payment', 'status', default='Unknown')

    ## Calculate order total
    items = order_dict.get('items', default=[])
    order_total = 0
    for item in items:
        item_dict = SafeDict(item)
        price = item_dict.get('price', default=0)
        quantity = item_dict.get('quantity', default=0)
        order_total += price * quantity

    ## Print order summary
    print(f"\nOrder ID: {order_id}")
    print(f"Customer: {customer_name}")
    print(f"Contact: {customer_email} | {customer_phone}")
    print(f"Shipping Address: {shipping_address}")
    print(f"Payment Status: {payment_status}")
    print(f"Order Total: ${order_total:.2f}")
    print(f"Items: {len(items)}")

    ## Print item details
    for j, item in enumerate(items):
        item_dict = SafeDict(item)
        name = item_dict.get('name', default='Unknown Product')
        price = item_dict.get('price', default=0)
        quantity = item_dict.get('quantity', default=0)
        print(f"  {j+1}. {name} (${price:.2f} × {quantity}) = ${price*quantity:.2f}")
  1. 파일을 저장하고 실행합니다.
python3 practical_example.py

누락되거나 불완전한 데이터를 적절하게 처리하면서 복잡한 JSON 데이터 구조를 안전하게 처리하는 방법을 보여주는 출력을 볼 수 있습니다. 이는 구조가 항상 예상과 일치하지 않을 수 있는 외부 소스의 데이터를 처리할 때 특히 중요합니다.

실용적인 예제는 다음을 보여줍니다.

  • 중첩된 JSON 구조를 안전하게 탐색합니다.
  • 누락된 데이터를 적절한 기본값으로 처리합니다.
  • JSON 내에서 객체 컬렉션을 처리합니다.
  • 중첩된 정보를 추출하고 형식을 지정합니다.

이러한 기술은 KeyError 예외로 인해 충돌하지 않고 실제 JSON 데이터를 처리할 수 있는 더 강력한 애플리케이션을 구축하는 데 도움이 됩니다.

요약

이 튜토리얼에서는 Python JSON 객체에서 중첩된 키에 접근할 때 KeyError를 처리하기 위한 효과적인 전략을 배웠습니다. 우리는 다음과 같은 여러 가지 접근 방식을 탐구했습니다.

  1. 기본 try-except 블록 - KeyError 예외를 포착하고 처리하는 기본적인 방법
  2. dict.get() 메서드 - 기본값을 지정할 수 있는 더 깔끔한 접근 방식
  3. 사용자 정의 도우미 함수 - 중첩된 구조를 안전하게 탐색하기 위한 재사용 가능한 함수 생성
  4. 고급 기술 - 경로 문자열, reduce 를 사용한 함수형 프로그래밍, 클래스 기반 래퍼 포함

이러한 기술을 적용하여 JSON 객체에서 누락되거나 불완전한 데이터를 우아하게 처리하고 KeyError 예외로 인해 애플리케이션이 충돌하는 것을 방지하는 더 강력한 코드를 작성할 수 있습니다.

다음 핵심 사항을 기억하십시오.

  • JSON 데이터를 사용할 때는 항상 누락된 키의 가능성을 고려하십시오.
  • 애플리케이션에 적합한 적절한 기본값을 사용하십시오.
  • 중첩된 데이터 구조 작업을 단순화하기 위해 재사용 가능한 유틸리티를 만듭니다.
  • 특정 사용 사례 및 코딩 스타일에 가장 적합한 접근 방식을 선택하십시오.

이러한 기술은 외부 소스, API 또는 사용자 입력에서 JSON 데이터를 사용하는 모든 Python 개발자에게 필수적이며, 실제 데이터를 자신 있게 처리할 수 있는 더욱 탄력적인 애플리케이션을 구축할 수 있도록 합니다.