Python 함수 기본값으로 가변 객체 사용 시 주의점: 방심하면 생기는 복잡한 버그들!

Python에서 함수의 기본값으로 가변 객체(예: 리스트, 딕셔너리, 집합)를 사용할 때는 특별히 주의해야 합니다. 기본값은 함수 정의 시점에 한 번만 평가되기 때문에, 가변 객체를 기본값으로 사용하면 모든 함수 호출에서 동일한 객체를 공유하게 됩니다.

이 동작은 의도치 않은 동작을 유발할 수 있으며, 특히 코드가 복잡해질수록 디버깅이 어려운 버그로 이어질 수 있습니다. 이번 글에서는 이를 방지하기 위한 방법과 실제 코드에서 자주 발생하는 사례를 살펴봅니다.


1. 기본값으로 가변 객체를 사용했을 때 생기는 문제

Python에서 함수 기본값으로 리스트 같은 가변 객체를 설정하면, 함수가 호출될 때마다 새로운 객체가 생성되지 않습니다. 대신, 동일한 객체가 모든 호출에서 재사용됩니다. 이를 이해하기 위해 아래 코드를 살펴봅시다.

def append_to_list(value, my_list=[]):
    my_list.append(value)
    return my_list

# 호출
print(append_to_list(1))  # [1]
print(append_to_list(2))  # [1, 2]
print(append_to_list(3))  # [1, 2, 3]

결과

[1]
[1, 2]
[1, 2, 3]

설명

  • my_list는 기본값으로 빈 리스트([])를 갖습니다.
  • 하지만 함수 호출마다 새로운 리스트가 생성되지 않고, 기존 리스트가 재사용됩니다.
  • 이로 인해 호출마다 값이 기존 리스트에 추가되어, 예상치 못한 결과를 초래합니다.

2. 이런 문제가 발생하는 이유: Python 기본값 동작 원리

Python에서 함수의 기본값은 함수 정의 시점에 한 번만 평가됩니다. 즉, 가변 객체를 기본값으로 설정하면, 그 객체는 모든 함수 호출에서 공유됩니다.

비유로 이해하기

기본값으로 가변 객체를 설정하면 새로운 물건을 받는 게 아니라, 항상 같은 창고에서 물건을 꺼내 쓰는 것과 같습니다. 만약 창고에 물건이 추가되면, 다음 호출에서도 그 변경된 상태가 유지됩니다.


3. 해결 방법: 기본값으로 None 사용하기

가변 객체를 기본값으로 설정하고 싶다면, 기본값에 None을 사용하고, 함수 내부에서 객체를 생성하는 방법이 가장 안전합니다.

def append_to_list(value, my_list=None):
    if my_list is None:
        my_list = []  # 새로운 리스트 생성
    my_list.append(value)
    return my_list

# 호출
print(append_to_list(1))  # [1]
print(append_to_list(2))  # [2]
print(append_to_list(3))  # [3]

결과

[1]
[2]
[3]

설명

  • 기본값으로 None을 설정하고, 함수 내부에서 리스트를 생성합니다.
  • 각 함수 호출마다 새로운 객체가 생성되기 때문에, 독립된 값을 반환합니다.
  • 이런 방식은 가변 객체를 안전하게 기본값으로 사용할 수 있는 가장 널리 쓰이는 패턴입니다.

4. 실전 예제: 가변 객체 기본값으로 생긴 버그와 해결 방법

아래 코드는 기본값으로 딕셔너리를 사용하는 경우에 발생하는 문제를 보여줍니다.

def add_to_dict(key, value, my_dict={}):
    my_dict[key] = value
    return my_dict

# 호출
print(add_to_dict("a", 1))  # {'a': 1}
print(add_to_dict("b", 2))  # {'a': 1, 'b': 2}

결과

{'a': 1}
{'a': 1, 'b': 2}

문제 설명

  • my_dict가 모든 호출에서 동일한 객체를 공유하기 때문에, 함수 호출이 독립적이지 않습니다.
  • 두 번째 호출에서 딕셔너리가 초기화되지 않고, 이전 상태를 유지합니다.

해결 방법: None 사용

def add_to_dict(key, value, my_dict=None):
    if my_dict is None:
        my_dict = {}  # 새로운 딕셔너리 생성
    my_dict[key] = value
    return my_dict

# 호출
print(add_to_dict("a", 1))  # {'a': 1}
print(add_to_dict("b", 2))  # {'b': 2}

결과

{'a': 1}
{'b': 2}

5. 실제 프로젝트에서 주의해야 할 사례

사례 1: API 요청에서 데이터 누적 문제

API 요청에 필요한 파라미터를 딕셔너리로 기본값으로 설정하면, 여러 요청 간 데이터가 누적되어 의도하지 않은 요청을 보내게 될 수 있습니다.

잘못된 코드

def api_request(params={}):
    params["token"] = "secure_token"
    return params

# 호출
print(api_request({"user": "Alice"}))  # {'user': 'Alice', 'token': 'secure_token'}
print(api_request({"user": "Bob"}))    # {'user': 'Bob', 'token': 'secure_token'}

해결 코드

def api_request(params=None):
    if params is None:
        params = {}
    params["token"] = "secure_token"
    return params

# 호출
print(api_request({"user": "Alice"}))  # {'user': 'Alice', 'token': 'secure_token'}
print(api_request({"user": "Bob"}))    # {'user': 'Bob', 'token': 'secure_token'}

사례 2: 데이터 분석에서 리스트 기본값 문제

데이터를 필터링하여 결과를 리스트로 저장할 때, 기본값으로 리스트를 사용하면 값이 누적될 수 있습니다.

잘못된 코드

def filter_data(data, results=[]):
    for value in data:
        if value > 10:
            results.append(value)
    return results

# 호출
print(filter_data([5, 15, 25]))  # [15, 25]
print(filter_data([30, 40]))     # [15, 25, 30, 40]

해결 코드

def filter_data(data, results=None):
    if results is None:
        results = []
    for value in data:
        if value > 10:
            results.append(value)
    return results

# 호출
print(filter_data([5, 15, 25]))  # [15, 25]
print(filter_data([30, 40]))     # [30, 40]

6. 요약: 가변 객체 기본값 사용 시 반드시 기억할 점

  1. 가변 객체 기본값의 위험:
    • 함수 호출마다 동일한 객체를 공유하여, 의도하지 않은 결과를 초래할 수 있습니다.
  2. 안전한 패턴:
    • 기본값으로 None을 사용하고, 함수 내부에서 가변 객체를 초기화합니다.
  3. 실전에서 발생할 수 있는 버그:
    • 리스트, 딕셔너리, 집합 같은 가변 객체의 기본값을 사용할 때 예상치 못한 동작이 발생할 수 있습니다.

Python에서 함수 기본값으로 가변 객체를 사용할 때 발생할 수 있는 문제는 처음에는 잘 보이지 않을 수 있습니다. 하지만 코드가 복잡해지면 치명적인 버그로 이어질 수 있으니, 항상 None 패턴을 사용해 안전하게 코드를 작성하세요!


Python functools.reduce: 복잡한 집계를 간단하게 만드는 매직

Python의 functools.reduce는 리스트와 같은 반복 가능한 객체에서 연속적인 집계 작업을 수행할 때 유용합니다. 이름 그대로, 데이터를 "줄이는(reduce)" 과정에서 사용됩니다. 복잡한 계산이나 집계를 간단한 코드로 구현할 수 있지만, 잘 사용하지 않으면 읽기 어려운 코드가 될 수 있어요.

이 글에서는 reduce의 기본 개념을 설명하고, 특히 실제 코드에서 유용하게 적용할 수 있는 예제를 통해 이를 활용하는 방법을 소개하겠습니다. 데이터 집계, 딥러닝 모델 학습 중 데이터 전처리 등에서 유용하게 사용할 수 있는 사례를 중심으로 작성했습니다.


functools.reduce란?

reduce는 Python의 functools 모듈에 포함된 함수로, 리스트나 튜플과 같은 반복 가능한 객체의 요소를 두 개씩 묶어 연산을 수행하며 데이터를 집계하는 데 사용됩니다. reduce를 사용하면 데이터를 축약하는 과정을 간결하고 효율적으로 표현할 수 있습니다.


1. reduce 기본 사용법

reduce는 세 가지 요소를 필요로 합니다:

  1. 함수: 두 개의 인자를 받아 처리하는 함수
  2. 반복 가능한 객체: 리스트나 튜플 등
  3. 초기값 (선택사항): 집계를 시작할 초기값

기본적인 예제로 리스트의 합을 구하는 코드를 작성해보겠습니다.

from functools import reduce

# 리스트 합 계산
numbers = [1, 2, 3, 4, 5]
result = reduce(lambda x, y: x + y, numbers)

print("합계:", result)

결과

합계: 15

설명

  • lambda x, y: x + y: 두 값을 더하는 익명 함수.
  • reduce는 처음 두 값을 더하고, 결과를 다음 요소와 다시 더하는 과정을 반복합니다.
  • 최종적으로 리스트의 모든 값이 더해져 결과가 반환됩니다.

2. reduce로 복잡한 집계를 단순화하기

reduce는 리스트의 합이나 곱뿐 아니라, 복잡한 집계 작업도 간단히 구현할 수 있습니다. 예를 들어, 최댓값을 구하는 코드입니다.

from functools import reduce

# 최댓값 구하기
numbers = [7, 3, 5, 8, 2, 9]
max_value = reduce(lambda x, y: x if x > y else y, numbers)

print("최댓값:", max_value)

결과

최댓값: 9

설명

  • lambda x, y: x if x > y else y: 두 값을 비교해 더 큰 값을 반환하는 함수.
  • reduce는 리스트의 처음 두 값을 비교하고, 더 큰 값을 다음 요소와 다시 비교하며 최종적으로 최댓값을 반환합니다.

3. 딥러닝 데이터 전처리에 적용: 레이어 크기 계산

딥러닝 모델을 설계할 때, 네트워크의 전체 파라미터 수를 계산해야 할 때가 있습니다. 각 레이어의 크기를 리스트로 저장한 뒤, 이를 reduce를 사용해 곱해보겠습니다.

from functools import reduce

# 각 레이어의 크기
layer_sizes = [784, 128, 64, 10]  # 입력층(784), 히든층(128, 64), 출력층(10)

# 전체 파라미터 수 계산
total_params = reduce(lambda x, y: x * y, layer_sizes)

print("전체 파라미터 수:", total_params)

결과

전체 파라미터 수: 64225280

설명

  • lambda x, y: x * y: 두 값을 곱하는 익명 함수.
  • reduce는 각 레이어의 크기를 차례로 곱해 최종적으로 네트워크의 총 파라미터 수를 계산합니다.
  • 복잡한 계산식도 단 한 줄로 간단히 표현할 수 있습니다.

4. 여러 딕셔너리 합치기

reduce는 리스트에 있는 여러 딕셔너리를 하나로 병합할 때도 유용합니다. 딕셔너리를 단순히 합칠 뿐 아니라 중복된 키를 처리하거나 값을 누적시키는 작업도 가능합니다.

from functools import reduce

# 여러 딕셔너리
dicts = [
    {"a": 1, "b": 2},
    {"b": 3, "c": 4},
    {"c": 5, "d": 6}
]

# 딕셔너리 병합
merged_dict = reduce(lambda x, y: {**x, **y}, dicts)

print("병합된 딕셔너리:", merged_dict)

결과

병합된 딕셔너리: {'a': 1, 'b': 3, 'c': 5, 'd': 6}

설명

  • {x, y}: 딕셔너리 xy를 병합. 중복된 키가 있으면 y의 값으로 덮어씁니다.
  • reduce를 사용하면 여러 딕셔너리를 효율적으로 합칠 수 있습니다.

5. 텍스트 데이터 처리: 문자열 합치기

문자열 리스트를 연결할 때 reduce를 사용하면 효율적이고 깔끔한 코드를 작성할 수 있습니다.

from functools import reduce

# 텍스트 데이터
words = ["Deep", "Learning", "is", "awesome!"]

# 문자열 연결
sentence = reduce(lambda x, y: x + " " + y, words)

print("문장:", sentence)

결과

문장: Deep Learning is awesome!

설명

  • lambda x, y: x + " " + y: 두 문자열을 공백으로 연결하는 함수.
  • reduce는 리스트의 각 문자열을 순서대로 연결하여 최종 문장을 만듭니다.

reduce가 주는 이점

  1. 코드 단순화: 반복문 없이도 데이터를 집계하는 복잡한 과정을 간단히 구현할 수 있습니다.
  2. 유연성: 수치 데이터, 문자열, 딕셔너리 등 다양한 데이터 유형에 적용 가능합니다.
  3. 효율성: 데이터를 한 번만 순회하며 작업을 처리하므로 빠르고 간결한 코드 작성이 가능합니다.

요약: functools.reduce로 집계를 간단히!

Python의 functools.reduce는 반복 가능한 데이터를 단일 값으로 집계하는 작업을 간단하게 만듭니다. 데이터 분석, 딥러닝 전처리, 문자열 처리 등 다양한 작업에서 효율적으로 활용할 수 있습니다.

reduce는 특히 다음과 같은 경우 유용합니다:

  1. 리스트의 합, 곱, 최댓값/최솟값 등 수치 집계 작업.
  2. 딕셔너리를 병합하거나 누적 처리해야 할 때.
  3. 텍스트 데이터를 연결하거나 변환할 때.
  4. 딥러닝 모델 설계 시 레이어 크기 계산.

썸네일 멘트

""


Python 문자열 포매팅의 끝판왕: f-strings의 고급 활용법

Python에서 데이터를 출력하거나 문자열을 조합할 때 문자열 포매팅은 필수적입니다. 그중에서도 f-strings(포매팅 문자열 리터럴)는 Python 3.6부터 도입된 기능으로, 더 간단하고 직관적이며 강력한 포매팅 방식입니다. 특히 데이터를 다루는 프로그래밍 작업에서, f-strings는 단순한 데이터 출력 이상의 역할을 합니다.

이번 글에서는 f-strings기본 문법부터 고급 활용법, 그리고 실제 코딩 상황에서의 유용한 예제까지 다뤄보겠습니다.


1. 기본 사용법: f-strings란 무엇인가?

f-strings는 문자열 앞에 f를 붙여서 사용하며, 중괄호 {} 안에 변수나 표현식을 직접 삽입할 수 있는 포매팅 방식입니다.

기본 예제

name = "Alice"
age = 30

print(f"My name is {name} and I am {age} years old.")

출력 결과

My name is Alice and I am 30 years old.

설명

  • {name}: 변수 name의 값을 문자열에 삽입합니다.
  • {age}: 변수 age도 동일하게 처리됩니다.

이 기본 문법을 알면 코드가 간결해지고, 가독성이 높아집니다. 하지만 f-strings는 단순한 포매팅을 넘어, 더 강력한 기능을 제공합니다.


2. 수학 계산과 표현식 삽입

f-strings를 사용하면 문자열 내에서 수학 계산이나 표현식을 직접 사용할 수 있습니다. 이런 방식은 계산 결과를 출력하거나 데이터를 가공할 때 유용합니다.

예제: 수학 계산

x = 5
y = 3

print(f"{x} * {y} = {x * y}")

출력 결과

5 * 3 = 15

딥러닝 모델 예제: 학습률 계산

learning_rate = 0.01
epochs = 10
total_steps = epochs * 100

print(f"Learning Rate: {learning_rate}, Total Steps: {total_steps}")

출력 결과

Learning Rate: 0.01, Total Steps: 1000

설명

  • {x * y}: 중괄호 안에서 직접 계산이 가능합니다.
  • 딥러닝과 같은 상황에서 변수 간의 연산 결과를 즉시 출력할 수 있어 유용합니다.

3. 값의 포맷 지정하기: 소수점 제한 및 정렬

f-strings를 사용하면 숫자의 소수점 자리수 제한, 정렬, 채우기 문자 지정 등 다양한 값 포맷을 간편하게 지정할 수 있습니다.

예제: 소수점 제한

accuracy = 0.987654321
print(f"Accuracy: {accuracy:.2f}")  # 소수점 둘째 자리까지 출력

출력 결과

Accuracy: 0.99

예제: 정렬과 채우기

for i in range(1, 4):
    print(f"Epoch {i:02}: Loss = {0.1234 * i:.4f}")

출력 결과

Epoch 01: Loss = 0.1234
Epoch 02: Loss = 0.2468
Epoch 03: Loss = 0.3702

설명

  • {accuracy:.2f}: 소수점 둘째 자리까지 반올림하여 출력합니다.
  • {i:02}: 숫자를 두 자리로 맞추며, 비어 있는 자리는 0으로 채웁니다.
  • {0.1234 * i:.4f}: 계산 후 소수점 넷째 자리까지 출력합니다.

4. 딕셔너리와 f-strings

딕셔너리 데이터를 출력할 때 f-strings중괄호 내부에서 키를 직접 접근할 수 있어 유용합니다.

예제: 딕셔너리 데이터 출력

model_config = {
    "learning_rate": 0.001,
    "batch_size": 32,
    "optimizer": "Adam"
}

print(f"Learning Rate: {model_config['learning_rate']}")
print(f"Batch Size: {model_config['batch_size']}")
print(f"Optimizer: {model_config['optimizer']}")

출력 결과

Learning Rate: 0.001
Batch Size: 32
Optimizer: Adam

설명

딕셔너리 데이터를 출력할 때, f-strings를 사용하면 키를 직접 참조하여 값에 접근할 수 있으므로 더 간결하고 직관적입니다.


5. 다중 줄 문자열 포매팅

f-strings는 여러 줄에 걸쳐 데이터를 출력할 때도 유용합니다. 이는 복잡한 데이터 구조나 설정값을 출력할 때 특히 편리합니다.

예제: 모델 정보 출력

model_name = "ResNet50"
params = 23500000
accuracy = 0.935

model_summary = f"""
Model Summary:
  - Name: {model_name}
  - Parameters: {params:,}  # 23,500,000 형식으로 출력
  - Accuracy: {accuracy:.2%}  # 퍼센트 형식으로 출력
"""
print(model_summary)

출력 결과

Model Summary:
  - Name: ResNet50
  - Parameters: 23,500,000
  - Accuracy: 93.50%

설명

  • {params:,}: 숫자를 쉼표로 구분하여 가독성을 높입니다.
  • {accuracy:.2%}: 값을 퍼센트 형식으로 변환합니다.
  • 여러 줄 문자열(""")과 함께 사용하면 복잡한 데이터를 보기 좋게 정리할 수 있습니다.

6. 날짜와 시간 포매팅

데이터 처리에서 날짜와 시간을 출력할 때도 f-strings는 매우 편리합니다.

예제: 현재 날짜와 시간 출력

from datetime import datetime

now = datetime.now()
print(f"Current Date: {now:%Y-%m-%d}")
print(f"Current Time: {now:%H:%M:%S}")

출력 결과

Current Date: 2024-11-06
Current Time: 14:35:22

설명

  • {now:%Y-%m-%d}: 날짜를 YYYY-MM-DD 형식으로 출력합니다.
  • {now:%H:%M:%S}: 시간을 HH:MM:SS 형식으로 출력합니다.
  • f-strings로 날짜와 시간을 가독성 있게 표현할 수 있습니다.

7. 디버깅에 활용하기: 변수명과 값을 함께 출력

Python 3.8부터 = 문법을 사용하여 변수명과 값을 동시에 출력할 수 있습니다. 이는 디버깅 시 매우 유용합니다.

예제: 디버깅

learning_rate = 0.01
batch_size = 32

print(f"{learning_rate=}, {batch_size=}")

출력 결과

learning_rate=0.01, batch_size=32

설명

  • {변수명=} 형태로 사용하면 변수명과 값을 함께 출력할 수 있습니다.
  • 디버깅 과정에서 변수 상태를 직관적으로 확인할 수 있습니다.

요약: f-strings로 더 간결하고 강력한 문자열 포매팅

Python의 f-strings는 단순한 문자열 포매팅을 넘어, 데이터 가공, 디버깅, 출력 형식 지정 등 다양한 작업에서 강력한 도구로 활용됩니다. 특히 다음과 같은 상황에서 유용합니다:

  1. 수학 계산과 데이터 출력: 연산 결과를 직관적으로 출력할 수 있습니다.
  2. 딕셔너리 데이터 처리: 키를 직접 참조하여 가독성을 높일 수 있습니다.
  3. 날짜와 시간 포매팅: 가독성 높은 날짜와 시간 표현이 가능합니다.
  4. 디버깅: 변수명과 값을 한 번에 출력하여 문제를 쉽게 파악할 수 있습니다.

실제 코딩 상황에서 f-strings를 적극 활용해 효율적인 코드를 작성해보세요!


setdefault로 안전한 딕셔너리 관리하기: 코드를 간결하게 만드는 꿀팁

딕셔너리는 Python에서 가장 유용한 자료구조 중 하나입니다. 하지만 데이터를 삽입할 때 키(key)가 이미 있는지 확인하고 값(value)을 추가해야 하는 번거로운 작업이 자주 발생합니다. 이럴 때 setdefault 메서드를 사용하면 코드가 훨씬 간결해지고 안전해집니다.

이번 글에서는 setdefault의 기본 사용법과 실제 코드에서 딕셔너리를 효율적으로 관리하는 방법을 살펴보겠습니다. 딥러닝 모델 설정이나 데이터 전처리 같은 실제 개발 상황에서 쓸만한 예제를 중심으로 설명합니다.


setdefault란 무엇일까?

setdefault는 딕셔너리에서 키를 확인한 후:

  1. 키가 이미 있다면 해당 키의 값을 반환합니다.
  2. 키가 없다면 기본값(default value)을 해당 키에 설정한 후 반환합니다.

이 메서드는 키가 존재하지 않을 경우 기본값을 추가하면서도, 중복 삽입을 방지할 수 있어 편리합니다.

기본 문법

dictionary.setdefault(key, default_value)
  • key: 딕셔너리에 추가하거나 확인할 키.
  • default_value: 키가 없을 경우 설정할 기본값 (생략 가능, 기본값은 None).

setdefault 기본 예제: 안전하게 값 삽입하기

먼저 setdefault의 기본 사용법을 확인해봅시다.

# 딕셔너리 생성
data = {"name": "Alice", "age": 30}

# 키가 이미 있는 경우
print(data.setdefault("name", "Bob"))  # Alice

# 키가 없는 경우 기본값 추가
print(data.setdefault("location", "New York"))  # New York

# 결과 확인
print(data)

결과

Alice
New York
{'name': 'Alice', 'age': 30, 'location': 'New York'}

설명

  • name 키는 이미 존재하므로 기존 값 Alice를 반환합니다.
  • location 키는 없었으므로 기본값 New York을 추가하고 반환합니다.
  • 이처럼 setdefault를 사용하면 키가 이미 존재하는지 확인하고 조건문을 작성할 필요가 없어 코드가 간결해집니다.

1. 딥러닝 모델 설정에서 setdefault 사용하기

딥러닝 모델을 구성할 때, 여러 하이퍼파라미터설정값을 관리해야 합니다. 이때 키가 없으면 기본값을 설정해야 하는 상황에서 setdefault를 사용하면 코드가 깔끔해집니다.

def configure_model(config):
    # 하이퍼파라미터 기본값 설정
    config.setdefault("learning_rate", 0.001)
    config.setdefault("batch_size", 32)
    config.setdefault("optimizer", "adam")
    config.setdefault("dropout_rate", 0.5)

    # 설정 출력
    for key, value in config.items():
        print(f"{key}: {value}")

# 사용자 입력값
user_config = {"batch_size": 64}

# 모델 설정
configure_model(user_config)

결과

batch_size: 64
learning_rate: 0.001
optimizer: adam
dropout_rate: 0.5

설명

  • setdefault를 사용하여 사용자가 설정하지 않은 키에 기본값을 추가합니다.
  • 이미 설정된 값은 덮어쓰지 않고 유지되므로 안전하게 기본값을 지정할 수 있습니다.
  • 하이퍼파라미터가 많을수록 setdefault는 조건문을 줄이고 코드 가독성을 높여줍니다.

2. 데이터 전처리에서 setdefault로 카테고리 분류하기 (카테고리 없는 데이터 포함)

setdefault는 키가 없을 때 기본값을 설정하기 때문에, 카테고리 없는 데이터도 안전하게 처리할 수 있습니다. 아래 예제는 데이터 항목 중 일부가 category 키를 가지지 않는 경우를 포함해 어떻게 처리할 수 있는지 보여줍니다.

# 데이터 분류 함수
def categorize_data(data):
    categories = {}
    for item in data:
        # 카테고리가 없는 경우 "Uncategorized"로 설정
        category = item.get("category", "Uncategorized")
        # 카테고리 키가 없으면 빈 리스트로 초기화
        categories.setdefault(category, []).append(item["value"])
    return categories

# 데이터 예시
data = [
    {"category": "A", "value": 10},
    {"category": "B", "value": 20},
    {"value": 30},  # 카테고리 없음
    {"category": "C", "value": 40},
    {"value": 50},  # 카테고리 없음
]

# 데이터 분류
result = categorize_data(data)
print(result)

결과

{'A': [10], 'B': [20], 'Uncategorized': [30, 50], 'C': [40]}

설명

  • 각 데이터 항목의 category 값을 키로, value 값을 리스트에 추가합니다.
  • 키가 없으면 setdefault가 자동으로 빈 리스트를 설정합니다.
  • 조건문 없이도 효율적이고 안전하게 데이터를 분류할 수 있습니다.

3. 로그 데이터에서 setdefault로 발생 횟수 기록하기

로그 데이터를 분석할 때, 특정 이벤트의 발생 횟수를 기록하는 작업이 필요할 수 있습니다. 이때 setdefault를 사용하면 코드가 훨씬 단순해집니다.

def count_events(logs):
    event_counts = {}
    for event in logs:
        # 이벤트 키가 없으면 초기값 0 설정 후 증가
        event_counts[event] = event_counts.setdefault(event, 0) + 1
    return event_counts

# 로그 데이터 예시
logs = ["login", "view", "click", "login", "click", "click", "logout"]

# 이벤트 발생 횟수 계산
result = count_events(logs)
print(result)

결과

{'login': 2, 'view': 1, 'click': 3, 'logout': 1}

설명

  • setdefault(event, 0)는 이벤트 키가 없으면 기본값 0을 추가합니다.
  • 이후 값을 1씩 증가시켜 이벤트 발생 횟수를 기록합니다.
  • 이 방식은 조건문 없이도 중복된 키를 안전하게 처리할 수 있어, 코드가 간결하고 효율적입니다.

4. 중첩 딕셔너리 관리에서 setdefault 활용하기

중첩된 데이터를 관리할 때도 setdefault는 매우 유용합니다. 예를 들어, 특정 키와 하위 키를 가지는 딕셔너리를 관리할 때 중복 체크 없이 쉽게 값 추가가 가능합니다.

# 데이터 삽입 함수
def add_to_nested_dict(data, key1, key2, value):
    data.setdefault(key1, {}).setdefault(key2, []).append(value)

# 초기 딕셔너리
nested_data = {}

# 데이터 추가
add_to_nested_dict(nested_data, "user1", "actions", "login")
add_to_nested_dict(nested_data, "user1", "actions", "click")
add_to_nested_dict(nested_data, "user2", "actions", "view")

# 결과 확인
print(nested_data)

결과

{'user1': {'actions': ['login', 'click']}, 'user2': {'actions': ['view']}}

설명

  • 첫 번째 setdefault(key1, {})는 최상위 키가 없으면 빈 딕셔너리를 추가합니다.
  • 두 번째 setdefault(key2, [])는 하위 키가 없으면 빈 리스트를 추가합니다.
  • 이렇게 중첩된 딕셔너리를 처리할 때, 조건문 없이도 안전하고 효율적으로 값을 추가할 수 있습니다.

요약: 왜 setdefault를 써야 할까?

setdefault는 Python 딕셔너리를 다룰 때 키가 존재하는지 확인하는 반복적인 조건문을 줄여주며, 값을 안전하게 추가할 수 있는 강력한 도구입니다. 특히 다음 상황에서 유용합니다:

  1. 딕셔너리에 기본값을 설정하면서 값을 추가해야 할 때.
  2. 데이터 분류, 그룹화, 또는 발생 횟수 집계가 필요할 때.
  3. 중첩된 딕셔너리를 처리할 때.

setdefault를 활용하면 조건문으로 키를 확인하는 번거로움을 덜고, 더 간결하고 읽기 쉬운 코드를 작성할 수 있습니다. 이제 딕셔너리 작업에서 setdefault를 활용해보세요!


Python의 Counter 클래스 완벽 가이드: 데이터 카운팅부터 분석까지

Python의 collections 모듈에 포함된 Counter 클래스는 데이터를 카운팅하는 데 매우 유용한 도구입니다. 반복적인 데이터에서 빈도를 계산하거나, 가장 많이 등장한 요소를 파악해야 할 때 직관적이고 빠르게 사용할 수 있습니다. 특히, 데이터 분석, 로그 처리, 텍스트 분석 등 다양한 상황에서 활용할 수 있습니다.

이번 글에서는 Counter의 기본 사용법부터, 실제 코드에서 활용할 수 있는 유용한 예제를 중심으로 Counter 클래스를 완벽히 이해할 수 있도록 도와드리겠습니다.


1. Counter 클래스란?

Counter는 Python의 collections 모듈에 포함된 클래스 중 하나로, 반복 가능한(iterable) 객체의 요소 개수를 손쉽게 셀 수 있도록 설계되었습니다. 기본적인 딕셔너리 형태로 동작하며, 각 요소를 키(key)로, 그 요소의 개수를 값(value)으로 저장합니다.


2. 기본 사용법: 데이터 카운팅하기

Counter의 기본적인 사용법은 리스트나 문자열에서 각 요소의 등장 횟수를 카운팅하는 것입니다.

from collections import Counter

# 리스트 카운팅
fruits = ["apple", "banana", "apple", "orange", "banana", "apple"]
fruit_counter = Counter(fruits)

print(fruit_counter)
print(fruit_counter["apple"])  # 특정 요소의 개수 확인

결과

Counter({'apple': 3, 'banana': 2, 'orange': 1})
3

설명

  • Counter(fruits)는 리스트의 요소를 카운팅하여 각 요소와 빈도를 저장합니다.
  • fruit_counter["apple"]"apple"의 등장 횟수를 반환합니다.
  • 이 방식으로 데이터의 빈도를 직관적으로 파악할 수 있습니다.

3. 텍스트 분석에 활용하기

Counter는 텍스트 분석에서 자주 사용됩니다. 예를 들어, 텍스트에서 각 단어의 빈도를 계산해 가장 많이 등장한 단어를 찾는 데 사용할 수 있습니다.

from collections import Counter

# 텍스트 데이터
text = "Python is amazing and Python is fun. Python is powerful."

# 단어 분리 후 카운팅
words = text.lower().split()
word_counter = Counter(words)

print(word_counter)
print(word_counter.most_common(3))  # 상위 3개 단어 출력

결과

Counter({'python': 3, 'is': 3, 'amazing': 1, 'and': 1, 'fun.': 1, 'powerful.': 1})
[('python', 3), ('is', 3), ('amazing', 1)]

설명

  • text.lower().split()로 텍스트를 소문자로 변환하고 단어를 분리합니다.
  • Counter(words)를 사용해 각 단어의 빈도를 계산합니다.
  • most_common(3) 메서드는 가장 많이 등장한 3개의 단어와 그 빈도를 반환합니다.

4. 로그 데이터 분석에 활용하기

로그 파일에서 특정 이벤트나 에러 코드의 빈도를 확인할 때도 Counter를 사용할 수 있습니다. 아래는 웹 서버 로그에서 요청 URL의 빈도를 계산하는 예제입니다.

from collections import Counter

# 샘플 로그 데이터
logs = [
    "/home",
    "/about",
    "/home",
    "/products",
    "/home",
    "/products",
    "/contact"
]

log_counter = Counter(logs)
print(log_counter)
print(log_counter.most_common(1))  # 가장 많이 요청된 URL

결과

Counter({'/home': 3, '/products': 2, '/about': 1, '/contact': 1})
[('/home', 3)]

설명

  • Counter(logs)는 각 URL의 요청 빈도를 계산합니다.
  • most_common(1)을 사용해 가장 많이 요청된 URL을 찾을 수 있습니다.
  • 이 방식으로 로그 데이터를 효율적으로 분석하고, 트래픽이 많은 페이지를 파악할 수 있습니다.

5. 데이터 비교 및 업데이트

Counter두 데이터 간의 차이나 합집합을 계산하거나, 기존 카운팅 데이터에 새로운 데이터를 추가하는 작업을 간단히 처리할 수 있습니다.

데이터 합치기

from collections import Counter

counter1 = Counter({"apple": 3, "banana": 2})
counter2 = Counter({"apple": 1, "orange": 4})

# 데이터 합치기
combined = counter1 + counter2
print(combined)

결과

Counter({'apple': 4, 'orange': 4, 'banana': 2})

데이터 차이 계산

# 데이터 차이 계산
difference = counter1 - counter2
print(difference)

결과

Counter({'apple': 2, 'banana': 2})

설명

  • counter1 + counter2는 각 요소의 빈도를 더한 결과를 반환합니다.
  • counter1 - counter2counter1에서 counter2의 요소 개수를 뺀 결과를 반환합니다.
  • 이를 통해 두 데이터 세트 간의 차이나 중복 요소를 빠르게 계산할 수 있습니다.

6. 데이터 필터링: 특정 조건의 데이터 추출

Counter는 데이터를 카운팅한 뒤 특정 조건에 맞는 데이터만 필터링하는 데 유용합니다.

from collections import Counter

# 샘플 데이터
ages = [23, 23, 25, 30, 30, 30, 35, 35, 40]

# 카운팅
age_counter = Counter(ages)

# 2번 이상 등장한 나이만 필터링
filtered = {age: count for age, count in age_counter.items() if count > 1}
print(filtered)

결과

{23: 2, 30: 3, 35: 2}

설명

  • Counter(ages)는 각 나이의 빈도를 계산합니다.
  • 딕셔너리 컴프리헨션을 사용해 빈도가 2 이상인 나이만 필터링합니다.
  • 이를 통해 특정 조건을 만족하는 데이터를 추출할 수 있습니다.

요약: Python Counter의 장점과 활용법

Python의 Counter 클래스는 다음과 같은 상황에서 강력한 도구로 활용될 수 있습니다:

  1. 데이터 빈도 분석: 리스트, 문자열, 로그 파일 등에서 요소의 빈도를 빠르게 계산할 수 있습니다.
  2. 텍스트 분석: 단어 빈도를 계산하여 가장 많이 사용된 단어나 패턴을 파악할 수 있습니다.
  3. 로그 분석: 웹 서버 로그, 시스템 로그 등에서 특정 요청이나 에러 코드의 빈도를 분석할 수 있습니다.
  4. 데이터 비교: 두 데이터 세트 간의 차이와 합집합을 간단히 계산할 수 있습니다.
  5. 조건부 데이터 필터링: 특정 조건에 맞는 데이터를 손쉽게 추출할 수 있습니다.

Python의 Counter를 활용하면 데이터 처리와 분석이 더욱 간단하고 효율적으로 변합니다. 이제 Counter와 함께 더 빠르고 직관적인 코드를 작성해보세요!


Python ChainMap으로 중첩 딕셔너리 깔끔하게 다루기: 딕셔너리 합치기의 종결자

파이썬에서 딕셔너리는 매우 자주 사용되는 데이터 구조입니다. 하지만, 딕셔너리가 중첩되거나 여러 딕셔너리를 한꺼번에 다뤄야 할 경우, 코드가 복잡해지고 비효율적이 될 수 있습니다. 이럴 때, collections 모듈의 ChainMap은 이런 문제를 해결할 수 있는 강력한 도구가 됩니다.

ChainMap은 여러 딕셔너리를 하나의 맵처럼 처리할 수 있도록 해줍니다. 이 글에서는 ChainMap의 개념과 실제 코딩 상황에서 어떻게 유용하게 사용할 수 있는지 예제를 통해 알아보겠습니다.


1. ChainMap이란 무엇인가?

ChainMap은 Python의 collections 모듈에 포함된 클래스입니다. 여러 딕셔너리를 체인처럼 연결하여 하나의 딕셔너리처럼 동작하게 합니다. 연결된 딕셔너리에서 데이터를 검색할 때, 첫 번째 딕셔너리부터 차례대로 검색하여 값을 반환합니다.

주요 특징

  • 연결된 딕셔너리를 수정하지 않고 논리적으로 합칠 수 있습니다.
  • 데이터가 여러 계층(레이어)로 구성된 상황에서 매우 유용합니다.
  • 검색 순서ChainMap에 딕셔너리를 추가한 순서를 따릅니다.

2. 기본 사용법: 여러 딕셔너리 연결하기

먼저 ChainMap의 기본 사용법을 살펴보겠습니다. 두 개 이상의 딕셔너리를 연결하여 하나의 맵처럼 사용하는 간단한 예제입니다.

from collections import ChainMap

# 두 개의 딕셔너리
defaults = {"theme": "light", "show_line_numbers": True, "font_size": 12}
user_settings = {"theme": "dark", "font_size": 14}

# ChainMap으로 연결
config = ChainMap(user_settings, defaults)

# 값 검색
print(config["theme"])  # dark (user_settings 우선)
print(config["show_line_numbers"])  # True (defaults에서 가져옴)

출력 결과

dark
True

설명

  • ChainMapuser_settingsdefaults를 연결합니다.
  • 키를 검색할 때, user_settings에서 먼저 찾고, 없으면 defaults에서 찾습니다.
  • 이렇게 하면 기본 설정값과 사용자 설정값을 효율적으로 병합할 수 있습니다.

3. 실제 코드 상황: 설정값 처리하기

애플리케이션에서 기본 설정값을 정의하고, 사용자 설정값으로 덮어쓰기 해야 하는 상황은 매우 흔합니다. ChainMap을 사용하면 이를 간단하게 처리할 수 있습니다.

from collections import ChainMap

def get_final_settings(defaults, user_overrides):
    # ChainMap으로 기본값과 사용자 값을 합침
    return ChainMap(user_overrides, defaults)

# 기본 설정값
default_config = {"theme": "light", "language": "English", "autosave": True}

# 사용자 지정값
user_config = {"theme": "dark", "autosave": False}

# 최종 설정값
final_config = get_final_settings(default_config, user_config)

print(final_config["theme"])  # dark
print(final_config["language"])  # English
print(final_config["autosave"])  # False

출력 결과

dark
English
False

설명

  • get_final_settings 함수는 ChainMap을 사용해 사용자 설정값(user_config)이 기본 설정값(default_config)을 덮어쓰도록 만듭니다.
  • 이 방식은 중첩된 설정값을 다룰 때, 코드를 깔끔하고 간결하게 유지할 수 있습니다.

4. 중첩 딕셔너리에서 데이터 검색하기

중첩된 딕셔너리에서 특정 값을 검색해야 하는 상황에서도 ChainMap은 유용합니다. 아래는 중첩된 환경 변수 맵에서 값을 검색하는 예제입니다.

from collections import ChainMap

# 환경 변수 설정
global_env = {"PATH": "/usr/bin", "USER": "admin"}
local_env = {"USER": "local_user", "EDITOR": "vim"}
runtime_env = {"DEBUG": "True", "USER": "runtime_user"}

# ChainMap으로 연결
env = ChainMap(runtime_env, local_env, global_env)

# 환경 변수 검색
print(env["USER"])  # runtime_user (runtime_env 우선)
print(env["PATH"])  # /usr/bin (global_env에서 가져옴)
print(env["DEBUG"])  # True

출력 결과

runtime_user
/usr/bin
True

설명

  • runtime_env, local_env, global_env을 연결하여 환경 변수 값을 우선순위에 따라 검색합니다.
  • ChainMap은 키를 상위 딕셔너리부터 차례로 검색하며, 최상위 딕셔너리(runtime_env)의 값을 우선합니다.

5. ChainMap에서 데이터 수정하기

ChainMap은 연결된 딕셔너리의 첫 번째 맵에 데이터를 수정합니다. 이를 활용하면 특정 딕셔너리의 값을 효율적으로 업데이트할 수 있습니다.

from collections import ChainMap

# 기본값과 사용자 설정
defaults = {"theme": "light", "font_size": 12}
user_settings = {"theme": "dark"}

# ChainMap 생성
config = ChainMap(user_settings, defaults)

# 값 수정
config["font_size"] = 14  # user_settings에 추가됨
print(config["font_size"])  # 14
print(user_settings)  # {"theme": "dark", "font_size": 14}

출력 결과

14
{'theme': 'dark', 'font_size': 14}

설명

  • config["font_size"] = 14user_settings에 값을 추가하거나 수정합니다.
  • ChainMap을 통해 상위 딕셔너리의 데이터를 쉽게 변경할 수 있습니다.

6. ChainMap으로 다중 소스 데이터 병합하기

여러 데이터 소스를 병합해야 할 때도 ChainMap은 유용합니다. 예를 들어, 여러 데이터베이스에서 정보를 가져와 합칠 수 있습니다.

from collections import ChainMap

# 데이터 소스
db1 = {"id": 1, "name": "Alice"}
db2 = {"email": "alice@example.com"}
db3 = {"phone": "123-456-7890"}

# ChainMap으로 병합
merged_data = ChainMap(db1, db2, db3)

print(merged_data["name"])  # Alice
print(merged_data["email"])  # alice@example.com
print(merged_data["phone"])  # 123-456-7890

출력 결과

Alice
alice@example.com
123-456-7890

설명

  • db1, db2, db3의 데이터를 병합하여 하나의 데이터 소스처럼 사용할 수 있습니다.
  • 기존 딕셔너리를 수정하지 않으므로 데이터의 원본을 안전하게 유지할 수 있습니다.

7. 딕셔너리 우선순위 변경하기

ChainMap은 연결된 딕셔너리의 순서를 바꿔 우선순위를 변경할 수 있습니다. 이를 활용해 동적으로 데이터 흐름을 조정할 수 있습니다.

from collections import ChainMap

# 설정값
defaults = {"theme": "light", "autosave": True}
user_settings = {"theme": "dark"}
runtime_settings = {"autosave": False}

# ChainMap 생성
config = ChainMap(runtime_settings, user_settings, defaults)

# 동적으로 순서 변경
config = config.new_child({"theme": "blue"})
print(config["theme"])  # blue

출력 결과

blue

설명

  • config.new_child()를 사용해 새로운 딕셔너리를 추가하고 우선순위를 변경합니다.
  • ChainMap의 동적인 순서 변경은 상황에 따라 데이터 흐름을 유연하게 조정할 수 있도록 도와줍니다.

요약: ChainMap으로 딕셔너리를 효율적으로 다루는 법

Python의 ChainMap은 중첩 딕셔너리를 간단하고 효율적으로 관리할 수 있도록 도와줍니다. 특히 다음과 같은 상황에서 유용합니다:

  1. 여러 데이터 소스를 병합할 때.
  2. 기본값과 사용자 설정값을 관리할 때.
  3. 환경 변수나 설정 계층을 처리할 때.
  4. 데이터 우선순위를 동적으로 조정할 때.

Python ** 문법으로 복잡한 코드를 간단하게: 딕셔너리 언패킹과 키워드 인자 사용법

Python에서 ** 문법은 딕셔너리를 언패킹하거나 가변 키워드 인자를 받아, 함수 호출을 더 유연하게 하고 코드 작성을 훨씬 간단하게 만들어줍니다. 특히, 반복적인 코드나 설정값이 많은 함수에서 불필요한 부분을 줄여, 코드가 읽기 쉽고 유지보수하기 쉬워집니다.


1. **kwargs로 가변 키워드 인자 받기

함수에 많은 인자를 전달해야 하거나 인자의 수가 변동적일 때, **kwargs를 사용하면 필요한 인자만 딕셔너리 형태로 받아 쉽게 처리할 수 있습니다. 여러 개의 키워드 인자를 사용하는 경우 **kwargs가 없으면 함수 호출이 복잡해질 수 있습니다.

def user_info(name, age, **kwargs):
    print(f"Name: {name}, Age: {age}")
    for key, value in kwargs.items():
        print(f"{key.capitalize()}: {value}")

# 사용 예시
user_info("Alice", 30, location="New York", job="Engineer", hobby="Reading")

설명

위 예제에서 **kwargs를 사용하여 임의의 키워드 인자를 받아 처리할 수 있습니다. user_info 함수에 location, job, hobby와 같은 인자를 추가해도 함수 정의를 바꿀 필요가 없습니다. 예전 방식으로 하려면 인자마다 함수 정의를 변경해야 했겠지만, **kwargs를 통해 더 깔끔하고 유연하게 코드를 작성할 수 있습니다.


2. 딕셔너리 언패킹으로 함수 호출을 간단히

기존 방식에서는 딕셔너리의 각 값을 하나씩 인자로 전달해야 했습니다. 그러나 **를 사용하면 딕셔너리를 바로 언패킹하여, 각 키를 인자명으로, 값을 인자 값으로 함수에 전달할 수 있습니다. 이로써 불필요한 반복을 줄이고, 코드 가독성을 높일 수 있습니다.

def display_order(item, price, quantity):
    print(f"Item: {item}, Price: {price}, Quantity: {quantity}")

order = {
    "item": "Laptop",
    "price": 1200,
    "quantity": 1
}

# 기존 방식
# display_order(order["item"], order["price"], order["quantity"])

# 언패킹 사용
display_order(**order)

설명

display_order(**order)는 딕셔너리 order의 키-값 쌍을 자동으로 언패킹하여 함수 인자로 전달합니다. 이전 방식에서는 각각의 키를 지정하여 값을 하나하나 전달해야 했지만, **를 사용하면 더 간단하고 직관적인 코드가 됩니다.


3. 기본값 설정이 많은 함수에서 **로 코드 단순화하기

기본값이 많은 함수에서 **를 사용하면, 코드가 짧아지고 관리하기 쉬워집니다. 기본 설정값을 가진 딕셔너리와 **kwargs를 함께 사용하여 특정 설정값만 쉽게 오버라이드할 수 있습니다.

def setup_environment(**config):
    # 기본 설정값
    default_config = {
        "resolution": "1080p",
        "color": "blue",
        "fullscreen": True
    }
    # 전달된 값만으로 기본값을 덮어씁니다.
    final_config = {**default_config, **config}

    for key, value in final_config.items():
        print(f"{key.capitalize()}: {value}")

# 사용 예시 - 일부 설정값만 오버라이드
setup_environment(resolution="4K", color="green")

설명

이 예제에서 setup_environment 함수는 default_config 딕셔너리와 config병합하여 최종 설정을 자동으로 조정합니다. 예전 방식대로라면 기본값을 개별적으로 지정하거나 모든 설정을 한꺼번에 전달해야 했지만, ** 문법을 통해 필요한 부분만 선택적으로 변경할 수 있어 코드가 더 유연해집니다.


4. 딕셔너리 병합하기

Python 3.9 이상에서는 ** 문법을 사용해 여러 딕셔너리를 간단히 병합할 수 있습니다. 이전 방식에서는 update() 메서드나 반복문을 사용해야 했던 불편함이 있었지만, **를 활용하면 훨씬 간결해집니다.

default_settings = {"theme": "dark", "font": "Arial"}
user_settings = {"font": "Helvetica", "fontsize": 12}

# 여러 딕셔너리를 한 번에 병합
settings = {**default_settings, **user_settings}

print(settings)

설명

{**default_settings, **user_settings}를 사용하여 두 딕셔너리를 손쉽게 병합할 수 있습니다. 중복된 키가 있을 경우, user_settings의 값이 우선 적용됩니다. 이 방법은 코드 작성과 유지보수가 간편해지며, 특히 설정값을 다룰 때 유용합니다.


5. 클래스 인스턴스 생성 시 유연하게 인자 전달하기

클래스를 사용할 때 ** 문법을 통해 유연하게 인자를 전달하면, 인스턴스 생성 과정에서 불필요한 코드를 줄일 수 있습니다. 특히 클래스 속성이 많을 때 편리합니다.

class Product:
    def __init__(self, **attributes):
        for key, value in attributes.items():
            setattr(self, key, value)

# 딕셔너리로 속성 값을 한 번에 전달
product_info = {"name": "Laptop", "price": 1200, "stock": 5}
product = Product(**product_info)

print(product.__dict__)

설명

위 코드에서 Product 클래스의 __init__ 메서드는 **attributes를 사용해 여러 속성을 한꺼번에 추가합니다. 이렇게 하면 클래스의 속성이 많을 때도 인스턴스를 간단하게 생성할 수 있습니다. 기존 방식대로라면 인스턴스를 생성할 때 속성을 일일이 지정해줘야 했지만, **를 사용하면 코드가 훨씬 간결해집니다.


요약: Python에서 ** 문법으로 코드 간소화하기

Python의 ** 문법은 다음과 같은 방식으로 복잡한 코드를 간소화합니다.

  1. 함수에서 가변 키워드 인자를 받아, 불필요한 함수 정의 수정 없이 다양한 인자를 처리할 수 있음.
  2. 딕셔너리의 키-값 쌍을 언패킹하여 함수에 간편히 전달할 수 있음.
  3. 기본 설정값을 딕셔너리로 관리하고, 필요한 부분만 오버라이드할 수 있어 유연한 코드 작성 가능.
  4. 여러 딕셔너리를 손쉽게 병합하여 관리할 수 있음.
  5. 클래스 인스턴스를 생성할 때 동적으로 속성 추가가 가능해짐.

이처럼 ** 문법은 코드의 가독성과 유지보수성을 크게 개선하므로, Python을 더 효율적으로 사용하고 싶다면 이 문법을 익혀두세요!

+ Recent posts