Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
297 changes: 297 additions & 0 deletions chapter1/pythonic_code.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@

## **1. 컴프리헨션 (Comprehension) 활용법**

컴프리헨션은 반복문을 사용하지 않고도 List, Dict, Set과 같은 자료구조를 매우 간결하게 생성할 수 있는 기능이다.
for 루프를 한 줄로 압축한 형태로, 가독성과 성능 면에서 이점을 가진다.

### 리스트 컴프리헨션 (List Comprehension)
- **리스트 컴프리헨션 (List Comprehension)** 가장 일반적으로 사용되는 컴프리헨션으로, 기존 리스트를 기반으로 새로운 리스트를 생성한다.
- **기본 구문:** `[표현식 for 항목 in 반복 가능한 객체 if 조건]`
- **예시:** 0부터 9까지의 숫자 중 짝수만 제곱하여 새로운 리스트 만들기
```python
# 기본 for문
squares = []
for i in range(10):
if i % 2 == 0:
squares.append(i * i)
print(squares) # [0, 4, 16, 36, 64]

# 리스트 컴프리헨션 활용
squares_comp = [i * i for i in range(10) if i % 2 == 0]
print(squares_comp) # [0, 4, 16, 36, 64]
```
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2중, 3중 중첩된 리스트 컴프리헨션을 작성해본 경험이 있나요? 어느 시점부터 일반 for문으로 바꾸는 것이 더 나을까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

명확한 기준이 있지는 않아서 팀 규칙에 따라 작성하면 될것 같습니다.
개인적으로는 (2중 컴프리핸션 + If문) 부터 for문으로 바꾸는 편입니다.


### 딕셔너리 컴프리헨션 (Dictionary Comprehension)
- **딕셔너리 컴프리헨션 (Dictionary Comprehension)** 리스트 컴프리헨션과 유사하게 한 줄로 딕셔너리를 생성할 수 있다.
- **기본 구문:** `{키_표현식: 값_표현식 for 항목 in 반복 가능한 객체 if 조건}`
- **예시:** 단어 리스트를 사용하여 {단어: 단어 길이} 형태의 딕셔너리 만들기

```python
words = ['apple', 'banana', 'cherry']
word_lengths = {word: len(word) for word in words}
print(word_lengths) # {'apple': 5, 'banana': 6, 'cherry': 6}
```

### 셋 컴프리헨션 (Set Comprehension)
- **셋 컴프리헨션 (Set Comprehension)** 중복을 허용하지 않는 셋(Set)을 생성할 때 사용한다.
- **기본 구문:** `{표현식 for 항목 in 반복 가능한 객체 if 조건}`
- **예시:** 리스트에 있는 숫자들 중 고유한 셋 만들기
```python
numbers = [1, 2, 2, 3, 4, 5, 5]
odd_squares = {x for x in numbers if x % 2 != 0}
print(odd_squares) # {1, 2, 3, 4, 5}
```

### 속도 차이?

일반적인 상황에서 List comprehension 의 동작이 유의미하게 빠르다.
아래 예제를 통해 간단하게 확인이 가능하다.
> 정확히 설명하자면 인터프리터 모드인 경우에 유의미하게 빠르다.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

자료에서 "인터프리터 모드에서 유의미하게 빠르다"고 했는데, 컴파일된 코드에서는 차이가 없을까요? 그리고 왜 컴프리헨션이 더 빠른지 내부 동작 원리를 설명할 수 있나요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cpython 과 같이 컴파일된 코드에선 차이가 없는걸로 알고 있습니다.

리스트를 생성하는 코드에서 list.append() 바이트 코드 레벨에서 성능 차이가 발생하는걸로 알고 있습니다.


```python
import timeit

# 테스트할 코드
setup_code = "numbers = range(10000000)"
for_loop_code = """
squared_numbers = []
for x in numbers:
squared_numbers.append(x**2)
"""
comprehension_code = "squared_numbers = [x**2 for x in numbers]"

# 실행 시간 측정
for_loop_time = timeit.timeit(stmt=for_loop_code, setup=setup_code, number=1)
comprehension_time = timeit.timeit(stmt=comprehension_code, setup=setup_code, number=1)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1억 개의 숫자를 처리해야 한다면 리스트 컴프리헨션이 최선일까요? 제너레이터 표현식 (x**2 for x in range(100000000))과 비교했을 때 어떤 차이가 있을까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 케이스라면 제너레이터를 쓰는게 맞을것 같습니다.
리스트 컴프리헨션은 모두 메모리에 올려서 한번에 처리해 메모리 이슈가 생길것 같습니다.


print(f"For loop: {for_loop_time:.6f} seconds")
print(f"List comprehension: {comprehension_time:.6f} seconds")
```

```bash
For loop: 0.271319 seconds
List comprehension: 0.245245 seconds
```

---

## **2. 언패킹(Unpacking)과 제너레이터(Generator)**

### 언패킹 (Unpacking)
- **언패킹 (Unpacking)** 언패킹은 튜플이나 리스트 같은 시퀀스 자료형의 요소들을 여러 변수에 한 번에 할당하는 기능이다.
- **예시:**
```python
# 기본 언패킹
a, b, c = (1, 2, 3)
print(a, b, c) # 1 2 3

# *를 이용한 언패킹 (나머지 요소들을 리스트로 받기)
head, *body, tail = [10, 20, 30, 40, 50]
print(head) # 10
print(body) # [20, 30, 40]
print(tail) # 50
```

### 이터레이터 (Iterator)

이터레이터는 파이썬에서 **반복 가능한 객체(iterable)**를 순회할 때 사용되는 객체이다.
이터레이터는 `__iter__()`와 `__next__()` 메서드를 구현한 객체로, 한 번에 하나씩 요소를 반환하며 순회 상태를 기억한다.

#### 이터레이터의 특징
- **지연 평가(Lazy Evaluation)**: 필요할 때만 값을 생성하므로 메모리 효율적이다
- **상태 유지**: 현재 위치를 기억하고 다음 호출 시 그 다음 요소를 반환한다

#### 이터레이터 vs 이터러블
- **이터러블(Iterable)**: `__iter__()` 메서드를 가진 객체 (리스트, 튜플, 문자열 등)
- **이터레이터(Iterator)**: `__iter__()`와 `__next__()` 메서드를 모두 가진 객체
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iter()와 next()를 구현해야 하는 이유는 뭘까요? 그냥 리스트를 반환하면 안 되는 특별한 상황이 있을까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

리스트로 반환할 경우 리스트 전체를 메모리에 올려야해서 Lazy하게 처리하기 위해 사용합니다.


#### 기본 사용법
```python
# 리스트는 이터러블이지만 이터레이터는 아니다
my_list = [1, 2, 3, 4, 5]
print(hasattr(my_list, '__iter__')) # True
print(hasattr(my_list, '__next__')) # False

# iter() 함수로 이터레이터 생성
my_iterator = iter(my_list)
print(hasattr(my_iterator, '__next__')) # True

# next() 함수로 요소 하나씩 가져오기
print(next(my_iterator)) # 1
print(next(my_iterator)) # 2
print(next(my_iterator)) # 3
```

#### 커스텀 이터레이터 클래스
```python
class CountDown:
def __init__(self, start):
self.current = start

def __iter__(self):
return self

def __next__(self):
if self.current <= 0:
raise StopIteration
else:
self.current -= 1
return self.current + 1

# 사용 예시
counter = CountDown(5)
for num in counter:
print(num) # 5, 4, 3, 2, 1

# 또는 next() 함수로 직접 사용
counter2 = CountDown(3)
print(next(counter2)) # 3
print(next(counter2)) # 2
print(next(counter2)) # 1
```

#### 제너레이터와의 관계
제너레이터는 이터레이터의 한 종류로, `yield` 키워드를 사용하여 더 간단하게 이터레이터를 만들 수 있다:
```python
def countdown_generator(start):
yield start
yield start+1
yield start+2

# 제너레이터는 자동으로 이터레이터가 된다
gen = countdown_generator(1)
print(next(gen)) # 1
print(next(gen)) # 2
print(next(gen)) # 3
```

### 제너레이터 (Generator)
- **제너레이터 (Generator)** 제너레이터는 모든 값을 메모리에 올리지 않고, 필요할 때마다 값을 하나씩 생성하여 반환하는 **이터레이터(iterator)**를 만드는 기능이다.
- 대용량 데이터를 처리할 때 메모리 효율성을 극대화할 수 있다. 함수 내에서 `yield` 키워드를 사용한다.
- **특징:**
- `yield`를 통해 값을 하나씩 반환하고, 함수의 상태는 그대로 유지된다.
- 다음에 `next()` 함수로 호출되면 이전에 멈췄던 지점부터 다시 실행된다.
- **예시:** 1부터 n까지의 숫자를 생성하는 제너레이터
```python
def number_generator(n):
print("Generator starts")
for i in range(1, n + 1):
yield i
print(f"{i} yielded")

my_gen = number_generator(3)
print(next(my_gen)) # Generator starts, 1 출력
print(next(my_gen)) # 1 yielded, 2 출력
print(next(my_gen)) # 2 yielded, 3 출력
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

예제의 my_gen을 한번 순회한 후 다시 사용하려면 어떻게 해야 할까요? 제너레이터가 exhausted되는 특성이 실제 코딩에서 어떤 버그를 유발할 수 있을까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제너레이터는 재사용(reset)이 불가능한것으로 알고 있어서 다시 사용하려면 재할당이 필요할것 같습니다.
데이터 검증 같은 호출에서 exhausted 되어 예상치 못한 버그를 일으킬 수 있습니다.

```


---

## **3. 매직 메서드 (Magic Methods)**

매직 메서드(또는 특별 메서드, Special Methods)는 파이썬 클래스 내에서 `__` (더블 언더스코어)로 시작하고 끝나는 특별한 이름의 메서드이다.
이 메서드들은 `+`, `len()`, `print()`와 같은 파이썬의 내장 연산이나 함수가 해당 객체에 사용될 때 자동으로 호출된다.

- `__init__(self, *args, **kwargs)`
- 클래스의 **생성자(constructor)** 메소드이다.
- `__str__(self)`
- `print()` 함수나 `str()` 내장 함수를 사용하여 객체를 **문자열로 변환**할 때 호출된다.
- 주로 사용자가 보기 쉬운, **비공식적인** 문자열 표현을 반환하는 데 사용된다.
- `__repr__(self)`
- `repr()` 내장 함수를 호출할 때나, 인터프리터에서 객체를 그냥 출력할 때 호출된다.
- 주로 개발자가 디버깅 목적으로 사용하는, 객체를 **명확하게 식별**할 수 있는 **공식적인** 문자열 표현을 반환하는 데 사용된다. `eval(repr(obj)) == obj`가 성립하는 것을 목표로 한다.
- `__eq__(self, other)`
- `==` 연산자를 사용하여 두 객체의 **내용이 같은지** 비교할 때 호출된다.
- 이 메서드를 구현하지 않으면, 기본적으로 두 객체의 메모리 주소를 비교한다.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

__eq__만 구현하고 __hash__를 구현하지 않으면 어떤 문제가 생길까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해시 불가능해서 에러가 발생합니다.
Hashable Contract 로 두 객체가 같다면, 두 객체의 해시값도 반드시 같아야 한다 때문에 그렇군요..?
처음 알았네요!

- **예시:**
```python
class Person:
def __init__(self, name, age):
self.name = name
self.age = age

def __str__(self):
return f"{self.name}, {self.age}살"

def __repr__(self):
return f"Person('{self.name}', {self.age})"

def __eq__(self, other):
return self.name == other.name and self.age == other.age

p1 = Person("Alice", 30)
p2 = Person("Alice", 30)
p3 = Person("Bob", 25)

print(p1) # __str__ 호출 -> Alice, 30살
print(str(p1)) # __str__ 호출 -> Alice, 30살
print(repr(p1)) # __repr__ 호출 -> Person('Alice', 30)
print(p1 == p2) # __eq__ 호출 -> True
print(p1 == p3) # __eq__ 호출 -> False
```


---

## **4. 데코레이터 (Decorator)**

데코레이터는 기존 함수의 코드를 수정하지 않으면서, 함수에 **새로운 기능을 추가하거나 수정**할 때 사용하는 강력한 디자인 패턴이다.

함수를 인자로 받아서, 새로운 기능이 추가된 또 다른 함수를 반환하는 형태로 동작한다.
- 주로 로깅, 실행 시간 측정, 접근 제어 등 AOP(관점 지향 프로그래밍)에 유용하게 사용된다.
- **기본 구조:**
```python
def decorator_function(original_function):
def wrapper_function(*args, **kwargs):
# --- 함수 실행 전 처리 ---
print(f"{original_function.__name__} 함수 실행 전입니다.")

result = original_function(*args, **kwargs) # 원래 함수 실행

# --- 함수 실행 후 처리 ---
print(f"{original_function.__name__} 함수 실행 후입니다.")
return result
return wrapper_function
```
- **사용법 (`@` 구문):** 기능을 추가하고 싶은 함수 위에 `@데코레이터_이름`을 붙여준다
- **예시:** 함수의 실행 시간을 측정하는 데코레이터
```python
import time

def measure_time(func):
def wrapper(*args, **kwargs):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

예제의 wrapper 함수는 원본 함수의 메타데이터(이름, docstring 등)를 잃어버려요. @functools.wraps(func)를 사용하지 않으면 어떤 문제가 생길까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사용하지 않을 경우 디버깅하는데 있어서 원본 함수가 보이지 않고 Typehint 등이 사라질 수 있습니다

start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"'{func.__name__}' 실행 시간: {end_time - start_time:.4f}초")
return result
return wrapper

@measure_time
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여러 데코레이터를 겹쳐서 사용할 때(@decorator1 @decorator2 @func) 실행 순서는 어떻게 될까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

func = decorator1(decorator2(func))와 동일합니다.

def slow_function(delay):
print(f"{delay}초 대기 시작...")
time.sleep(delay)
print("대기 완료!")
return "완료"

slow_function(2)

```

**실행 결과:**
```
2초 대기 시작...
대기 완료!
'slow_function' 실행 시간: 2.0021초
```

## 출처

- https://docs.python.org/ko/3.13/tutorial/controlflow.html#unpacking-argument-lists
- https://docs.python.org/ko/3.13/reference/datamodel.html#special-method-names
- https://wikidocs.net/134909
- https://wikidocs.net/192021
- https://docs.python.org/ko/3.13/tutorial/datastructures.html#list-comprehensions
- https://docs.python.org/ko/3.13/tutorial/classes.html#iterators
- https://docs.python.org/ko/3.13/tutorial/classes.html#generators