본문 바로가기
IT 개발 및 프로그래밍/파이썬(Python)

파이썬 문법 자주 묻는 질문 BEST 10가지 총정리

by 노마드데이터랩 2025. 3. 4.
반응형

1. 주요 질문 및 이슈

일반적인 Python 개발자들이 문법과 관련하여 가장 많이 궁금해하는 상위 10가지 질문과 이슈는 다음과 같습니다.

  1. 기본 인수로 가변 객체를 사용할 때 발생하는 문제
  2. 변수의 유효 범위(Scope)와 global/nonlocal 키워드
  3. 동일성 연산자 is와 동등성 연산자 ==의 차이
  4. 함수 정의에서의 *args와 **kwargs 사용법
  5. 얕은 복사와 깊은 복사의 차이
  6. 파이썬의 인자 전달 방식 (Call by Reference vs Call by Value)
  7. 리스트(List)와 튜플(Tuple)의 차이
  8. 파이썬 OOP에서 self 키워드와 클래스 변수 vs 인스턴스 변수
  9. 데코레이터(Decorator)의 개념과 사용 방법
  10. 제너레이터(Generator)와 yield 키워드의 동작 원리

2. 질문 빈도 및 중요도

위 10가지 문법 질문에 대한 등장 빈도중요도를 정리하면 다음과 같습니다.

질문/이슈빈도중요도

기본 인수의 가변 객체 문제 자주 높음
변수 스코프와 global 키워드 자주 높음
is vs == 차이 자주 중간
*args/**kwargs 사용법 자주 중간
얕은 복사 vs 깊은 복사 가끔 중간
인자 전달 방식 (참조 vs 값) 자주 높음
리스트 vs 튜플 차이 자주 낮음
self 및 클래스 변수 vs 인스턴스 변수 자주 높음
데코레이터 사용 가끔 중간
제너레이터와 yield 사용 가끔 중간

(빈도: 자주=다수의 개발자가 자주 언급, 가끔=때때로 언급, 드물게=거의 언급되지 않음. 중요도: 높음=프로그램 동작에 큰 영향, 중간=개념 이해에 중요, 낮음=알면 좋지만 치명적이지 않음.)

3. 세부 분석 및 배경 설명

이제 각 질문마다 왜 이러한 의문이 자주 등장하는지개발자들이 어려워하는 이유를 기술적/교육적 관점에서 분석해보겠습니다.

3.1 기본 인수로 가변 객체를 사용할 때 발생하는 문제

질문 배경: 함수의 기본 파라미터로 리스트나 딕셔너리 같은 가변(mutable) 객체를 지정하면, 함수를 여러 번 호출할 때 예상과 달리 그 기본 객체가 재사용되어 값이 누적되는 현상이 발생합니다. 초보 개발자들은 함수 호출마다 새로운 객체가 생성될 것이라고 기대하지만, Python에서는 함수 정의 시점에 단 한 번만 기본값을 생성하기 때문입니다​. 예를 들어 아래와 같은 함수를 생각해봅시다.

def foo(bar=[]):  # 기본값으로 빈 리스트 [] 사용
    bar.append("baz")
    return bar

print(foo())  # 첫 호출
print(foo())  # 두 번째 호출​

첫 호출에서는 ["baz"]가 출력되지만, 두 번째 호출 결과는 ["baz", "baz"]가 되어 많은 초심자들을 놀라게 합니다. 이는 bar의 기본 리스트가 모든 호출에서 공유되기 때문입니다. 개발자들은 함수 호출 시 기본값이 매번 새로운 객체로 초기화될 것이라 오해하기 쉽지만, 실제로는 함수 정의 시에 한 번만 평가되므로 이후 호출에서는 그 객체를 계속 사용하게 됩니다​. 숫자, 문자열, 튜플처럼 불변(immutable) 객체를 기본값으로 사용하면 이런 문제가 없지만, 리스트나 딕셔너리같이 가변 객체는 변경이 누적되어 예기치 않은 동작을 일으킬 수 있습니다​. 이 특징은 Python 문법의 미묘한 부분이라 초보자들이 자주 질문하게 됩니다.

어려워하는 이유: 대부분의 언어에서는 기본 인수를 이런 방식으로 동작시키지 않거나, 함수가 호출될 때마다 기본 표현식을 새로 평가합니다. Python의 설계 철학상 함수 기본값을 정적 객체로 취급하는 점은 처음 접하는 이에게 다소 비직관적입니다. 이로 인해 "왜 함수 기본 리스트가 계속 누적되나요?"와 같은 질문이 자주 발생합니다. 이러한 동작을 모르면 함수가 상태를 은폐하여 유지하기 때문에 버그를 찾기 어려워 중요도도 높습니다. 교육적으로는 이 개념을 이해시키기 위해 Python의 함수 객체 생성 시점을 설명하고, 권장되는 회피 방법(예: None 사용)을 함께 가르치는 경우가 많습니다.

3.2 변수의 유효 범위와 global/nonlocal 키워드

질문 배경: Python에서 변수의 스코프(scope) 규칙은 LEGB(Local-Enclosed-Global-Builtins)에 따라 정해집니다. 함수 내부에서 변수를 할당하면 자동으로 지역 변수로 간주되어, 바깥에 같은 이름의 변수가 있더라도 가려지게 됩니다. 예를 들어 아래 코드를 봅시다.

x = 10
def foo():
    print(x)   # 여기서 x는?
    x += 1     # x에 할당 수행
foo()

직관적으로는 foo() 안의 print(x)가 전역 변수 x(값 10)을 출력할 것 같지만, 실제로는 UnboundLocalError가 발생합니다​. 이는 foo 함수 본문에서 x += 1과 같이 할당문이 등장함으로써 Python이 x를 지역 변수로 인지했기 때문입니다. 이 경우 함수 시작 시점에 지역 변수 x가 아직 초기화되지 않았으므로 참조 에러가 나는 것입니다​. Python의 규칙상, 함수 내부에서 값을 할당하는 변수는 그 함수의 지역 변수로 간주되며, 동일한 이름의 전역 변수가 있어도 가려지게(shadowing) 됩니다​.

이러한 스코프 규칙은 처음 Python을 접한 개발자에겐 다소 놀랍기 때문에 “함수 안에서 전역 변수를 참조하려면 어떻게 해야 하나요?” 같은 질문이 자주 나옵니다. 특히 변수 선언이 필요한 C 계열 언어와 달리 Python은 암묵적으로 처리하므로 혼란이 생길 수 있습니다.

어려워하는 이유: 많은 초보자들은 함수 내부에서 단순히 전역 변수를 읽고 수정할 수 있다고 생각하지만, Python은 암묵적으로 지역 변수를 결정하는 룰이 있습니다​. 이러한 동작은 의도하지 않은 버그를 유발할 수 있어 중요합니다. 기술적으로는 global 키워드를 통해 함수 내부에서 전역 변수를 사용할 수 있고​, 중첩 함수 내에서는 nonlocal 키워드로 외부 함수의 변수에 접근할 수 있습니다​. 하지만 이를 모르는 경우 갑작스런 에러에 당황하게 되고, 스코프 개념 자체를 다시 학습해야 합니다. 교육적으로, 지역 변수와 전역 변수의 구분 및 global/nonlocal 키워드의 용도를 이해시키는 것이 중요합니다.

3.3 is와 == 연산자의 차이

질문 배경: Python에는 두 가지 비교 연산자가 있습니다. ==은 **값의 동등성(equality)**을 비교하고, is는 객체의 동일성(identity), 즉 같은 객체인지(메모리 주소가 같은지)를 비교합니다​. 처음 Python을 배울 때는 이 둘의 차이가 불분명하게 느껴지는데, 특히 작은 숫자나 짧은 문자열의 경우 Python이 내부적으로 객체를 재사용(interning)하기 때문에 x == y와 x is y가 모두 True로 나와 혼동을 줍니다. 예를 들어:

a = 5
b = 5
print(a == b, a is b)        # True True (우연히도 둘 다 True)
x = [1, 2, 3]
y = [1, 2, 3]
print(x == y, x is y)        # True False

첫 번째 경우에는 5라는 작은 정수가 인터닝되어 a is b가 True를 줬지만, 두 번째 리스트의 경우 값은 같으나 별개의 객체이므로 x is y는 False가 됩니다​. 이처럼 is와 ==의 용도가 다르다는 점을 이해하지 못하면 "왜 동일한 내용인데 is 결과가 다를까요?"와 같은 질문이 생깁니다.

어려워하는 이유: 다른 언어를 쓰던 개발자들은 ==만으로 모든 비교를 해왔기에 is의 존재가 낯설 수 있습니다. 특히 Java나 C#의 ==는 참조 비교일 때도 있어 혼동되는데, Python에서는 개념을 분리해놓았기 때문입니다. is를 잘못 사용하면 논리 오류가 생길 수 있으므로 중요도는 중간 정도입니다. 예를 들어 문자열 비교에 is를 썼을 때 우연히 통과되다가 어떤 길이 이상의 문자열에서는 False가 되어버리는 등 잠복 버그가 발생할 수 있습니다. 따라서 개발자들은 언제 is를 쓰고 언제 ==를 써야 하는지 혼란스러워하며 질문하게 됩니다. 기술적으로는 동일성 비교는 주로 Singleton 객체(예: None)에만 사용하고, 일반적인 값 비교에는 ==를 써야 한다는 점을 이해해야 합니다. 이 개념을 확실히 짚고 넘어가야 코드의 의도가 명확해지므로 교육적으로 중요합니다.

3.4 *args와 **kwargs의 사용법

질문 배경: 함수 정의에서 *args와 **kwargs 문법은 가변인자를 처리하기 위한 Python만의 방식입니다. 처음 이를 본 개발자는 *와 **의 의미를 몰라서 "함수 정의의 *args는 무엇인가요?" 같은 질문을 하게 됩니다. *args는 함수에 임의 개수의 위치 인자를 튜플 형태로 전달하고자 할 때 사용하며, **kwargs는 임의 개수의 키워드 인자를 딕셔너리로 전달받을 때 사용합니다​. 즉, 함수 정의에 *args가 있으면 호출 시 하나의 인자 대신 여러 인자를 콤마로 나열해 넘길 수 있고, 함수 내부에서는 이를 튜플 args로 처리합니다. 마찬가지로 **kwargs가 있으면 key=value 형태의 인자를 임의로 많이 받아서 함수 내부에서 딕셔너리 kwargs로 사용할 수 있게 됩니다.

예를 들어 아래 함수들을 살펴보겠습니다.

def print_everything(*args):
    for count, thing in enumerate(args):
        print(f"{count}. {thing}")

def table_things(**kwargs):
    for name, value in kwargs.items():
        print(f"{name} = {value}")

print_everything('apple', 'banana', 'cabbage')
# 출력:
# 0. apple
# 1. banana
# 2. cabbage

table_things(apple='fruit', cabbage='vegetable')
# 출력:
# apple = fruit
# cabbage = vegetable

위와 같이 *args를 사용하면 매개변수를 몇 개 받을지 모를 때 유용하며​, **kwargs는 함수에서 미리 정의하지 않은 이름의 키워드 인자들도 처리할 수 있게 해줍니다​. 이 문법을 쓰면 함수의 유연성이 높아져, 하나의 함수가 다양한 형태의 호출에 대응할 수 있습니다.

어려워하는 이유: 가변인자 문법은 초보자에게는 다소 특이하게 보입니다. 특히 *과 **의 역할 차이를 헷갈려 하며, 언제 어떤 것을 써야 하는지 묻곤 합니다. 교육적으로는 *args는 튜플로, **kwargs는 딕셔너리로 인자를 받는다는 것을 실습을 통해 익히게 하는 것이 효과적입니다. 또한 함수 정의뿐 아니라 함수 호출 시에도 *와 **를 사용하여 시퀀스를 언패킹(unpacking)하거나 딕셔너리를 언패킹해 인자로 넘길 수 있다는 점까지 알게 되면 이 문법을 완전히 이해하게 됩니다. 이 개념 자체는 코드 이해와 재사용성 측면에서 중요하지만, 몰라도 함수 정의를 평범하게 쓰면 되므로 중요도는 중간으로 볼 수 있습니다.

3.5 얕은 복사와 깊은 복사의 차이

질문 배경: **얕은 복사(shallow copy)**와 **깊은 복사(deep copy)**는 자료구조를 복사할 때 중요한 개념입니다. Python에서 객체를 복사할 때 표면적인 1차원 구조만 복사하는 얕은 복사를 하면, 내부에 포함된 가변 객체들은 원본과 참조를 공유하게 됩니다​. 반면 깊은 복사는 중첩된 객체들까지 모두 재귀적으로 새로운 객체로 복제하므로 원본과 완전히 독립적인 복사본을 만듭니다​.

개발자들은 리스트나 딕셔너리 등을 복사한 뒤, 한쪽을 변경했더니 다른 쪽에도 변화가 생겨 "복사를 제대로 한 게 맞나요?"라는 의문을 가집니다. 예를 들어 2차원 리스트를 복사한 뒤 내부 리스트의 값을 수정하면 원본에 영향이 갈 수 있습니다. 이는 얕은 복사로 인해 내부 리스트 객체를 공유하고 있기 때문입니다. 이러한 현상을 접하면 얕은/깊은 복사의 차이를 질문하게 됩니다.

어려워하는 이유: 표면적으로는 복사본을 만들었으니 원본과 분리되어야 한다고 생각하기 쉽지만, Python의 객체 모델은 객체의 참조(reference) 개념을 기반으로 하기 때문에 얕은 복사는 사실상 객체 참조의 복사에 불과하지 않습니다​. 따라서 중첩 구조를 가진 데이터에서 이 참조 공유를 간과하면 버그가 생길 수 있습니다. 기술적으로는 copy 모듈의 copy() 함수를 쓰거나 슬라이싱(list[:])으로 리스트를 복사하면 얕은 복사가 이루어지고, copy.deepcopy()를 사용하면 깊은 복사가 이루어집니다​. 어느 상황에서 어떤 복사가 필요한지 판단해야 하기 때문에 중요도는 중간 정도입니다. 교육적으로, 메모리 그림을 통해 얕은 복사가 어떻게 원본과 일부 객체를 공유하는지 보여주면 이해시키는 데 도움이 됩니다. 또한 불변 객체만 있다면 얕은 복사로도 문제가 없고, 가변 객체가 중첩된 경우 깊은 복사가 필요함을 사례를 통해 설명합니다.

3.6 파이썬의 인자 전달 방식 (Call by Reference vs Call by Value)

질문 배경: Python의 함수 인자 전달방식을 두고 흔히 "참조에 의한 호출(call by reference)인가요, 값에 의한 호출(call by value)인가요?"라는 질문이 많이 제기됩니다. 정답은 전형적인 둘 중 하나가 아니라, Python은 “객체 참조에 의한 전달(call by object reference)” 방식을 사용한다는 것입니다​. 이는 함수를 호출할 때 인자의 **객체 참조(포인터와 유사한 값)**를 함수에 복사해서 넘긴다는 뜻입니다. 결과적으로 함수 내부에서 불변(immutable) 객체를 변경하려 하면 새 객체를 만들어 재할당할 뿐 호출자 측엔 영향이 없지만 (마치 값 복사처럼 동작), 가변(mutable) 객체를 변경하면 원본 객체가 변경되어 호출자에도 그 영향이 나타납니다 (참조에 의한 호출처럼 동작)​.

예를 들어, 정수나 문자열(불변 객체)은 함수 안에서 바꾸려고 해도 외부에 영향이 없지만 리스트나 딕셔너리(가변 객체)는 함수 안에서 내용을 수정하면 바깥 변수도 변경된 내용을 보게 됩니다. 이 혼합된 성질 때문에 많은 개발자가 혼동을 느낍니다.

어려워하는 이유: 전통적인 용어로 보면 Python은 값에 의한 전달도, 참조에 의한 전달도 아닌 것처럼 보여 헷갈립니다. 사실 함수 인자로 전달된 변수는 함수 안에서 객체의 참조값을 복사한 현상태이며, 함수 안에서 그 참조로 객체를 조작하면 원본 객체를 수정하는 셈이 됩니다​. 하지만 함수 안에서 해당 변수명을 다른 객체로 재할당하면 원본과의 연결이 끊어질 뿐이죠. 이러한 동작을 제대로 이해하지 못하면 "왜 함수 안에서 리스트를 고쳤는데 밖에서도 바뀌죠?" 혹은 "정수를 넘겼는데 왜 안 바뀌죠?" 같은 질문이 나옵니다. 중요도는 중간이지만, 특히 mutable 객체를 함수에서 다룰 때 side effect를 예측하기 위해 꼭 알아야 하는 개념입니다. 교육적으로는, 함수를 호출할 때 발생하는 객체 ID 변화 유무를 출력해보거나, 함수 내부에서의 재할당 vs. 내부 변경의 차이를 보여주는 실험을 통해 깨닫게 하는 것이 효과적입니다.

3.7 리스트와 튜플의 차이

질문 배경: Python의 기본 자료형 중 **리스트(list)**와 **튜플(tuple)**은 모두 순차 자료형(sequence)으로 비슷한 점이 많지만, **가변성(mutable 여부)**에서 큰 차이가 있습니다. 리스트는 가변 객체로 원소를 추가/삭제하거나 값을 변경할 수 있지만, 튜플은 불변 객체라 한 번 정의하면 그 내용을 변경할 수 없습니다​. 초보자들은 둘 다 값을 나열하는 자료구조이기에 "리스트와 튜플은 뭐가 다른가요? 언제 각각을 써야 하나요?"를 자주 묻습니다.

리스트는 append, remove 등 원소를 조작하는 여러 메서드를 지원하지만 튜플은 그런 메서드가 없으며, 리스트는 []로 정의하고 튜플은 ()로 정의한다는 문법적 차이도 있습니다. 또한 튜플은 Python에서 해시 가능한(immutable) 객체이기 때문에 딕셔너리의 키로 사용할 수 있지만, 리스트는 해시 불가능하여 키로 쓸 수 없다는 차이도 있습니다​.

어려워하는 이유: 자료구조 선택의 문제이기도 해서, 둘 중 무엇을 언제 쓰는지 감이 안 오는 경우가 많습니다. 일반적인 권장사항은 변경 가능한 나열이 필요하면 리스트, 한 번 정하면 바뀌지 않는 값들의 집합이나 레코드 용도로는 튜플을 쓰라는 것입니다. 예를 들어 좌표 (x, y)는 튜플로 표현하고, 학생 명단은 리스트로 표현하는 식입니다. 성능적으로 튜플이 리스트보다 약간 더 빠르고 메모리 효율적이지만​, 현대 Python에서는 큰 차이가 없어서 주로 의도 전달 차원에서 선택합니다. 중요도는 낮은 편이지만 코드를 Pythonic하게 작성하기 위해 스타일 가이드 등에서 이 구분을 강조하기도 합니다. 교육 시에는 리스트를 사용해보고, 동일한 작업을 튜플로 하려다 오류가 나는 예시 등을 통해 튜플은 불변임을 인지시키고 그 장점을 설명합니다 (예: 실수로 데이터가 변경되는 것을 막아주거나 딕셔너리 키로 활용 가능 등).

3.8 self 키워드와 클래스 변수 vs 인스턴스 변수

질문 배경: Python의 객체 지향(OOP) 문법에서 가장 먼저 부딪치는 난관 중 하나가 바로 self 키워드입니다. 다른 언어에서는 인스턴스 메서드 정의 시 self에 해당하는 것을 명시적으로 적지 않지만, Python은 인스턴스 메서드의 첫 번째 인자로 항상 인스턴스 자기 자신(self)을 받아야 합니다. 그래서 초보자들은 "self를 왜 써야 하나요? 자동으로 안 되나요?"라는 질문을 많이 합니다. 이것은 Python의 철학인 "명시적인 것이 암시적인 것보다 낫다(Explicit is better than implicit)"에 부합하는 설계로, 숨겨진 this 포인터 대신 self를 통해 인스턴스를 명확히 드러내는 것입니다​. 즉, self는 특별한 키워드가 아니라 관례적으로 쓰이는 변수명일 뿐이며, 해당 메서드를 호출한 객체 자신을 가리킵니다.

또 하나 자주 혼동하는 개념은 클래스 변수(class variable)와 인스턴스 변수(instance variable)의 차이입니다. Python에서는 클래스 정의 안에 바로 대입한 변수는 클래스 변수로서 모든 인스턴스가 공유하고, self.변수로 할당하는 경우 인스턴스 별로 독립적인 변수가 됩니다. 이를 잘 모르면, 예를 들어 클래스 변수로 리스트를 하나 만들어 놓고 인스턴스마다 다르게 사용할 것이라 생각했다가 모든 인스턴스가 그 리스트를 공유해서 예기치 않은 동작이 일어납니다. 실제로 상속 관계에서도 클래스 변수는 상위 클래스에서 하위 클래스로 속성 탐색(MRO)에 따라 전파되므로, 어떤 클래스의 클래스 변수를 변경하면 그 클래스를 상속한 다른 클래스의 클래스 변수 값도 변할 수 있습니다​. 이런 작동 때문에 "파이썬에서 클래스 변수와 인스턴스 변수는 어떻게 다른가요?"라는 질문도 빈번합니다.

어려워하는 이유: Python의 OOP는 다른 언어와 미묘하게 다릅니다. self를 명시하는 문법은 처음엔 번거로워 보여도, Python이 암묵적인 일은 하지 않는다는 철학을 반영한 것입니다​. 초보자들은 이를 이해하기 전까지는 자꾸 self를 빠뜨려 에러를 만나곤 합니다. 또한 클래스 변수와 인스턴스 변수의 구분을 못 하면 하나의 변수가 여러 인스턴스 간에 공유되는 부작용을 초래합니다. 이는 종종 버그로 이어지므로 중요도가 높습니다. 예를 들어 어떤 클래스의 클래스 변수 x를 변경했더니, 그 클래스를 상속한 다른 클래스의 x도 같이 바뀌었다는 상황은 클래스 변수의 동작 원리를 모르면 충격적으로 다가옵니다​. 교육적으로, self를 사용하는 예제와 그렇지 않은 예제 (예: 클래스 메서드나 정적 메서드)들을 비교해보고, 클래스 변수와 인스턴스 변수를 선언/사용하는 코드를 직접 실행해 보게 하면 개념 이해에 도움이 됩니다.

3.9 데코레이터(Decorator)의 개념과 사용 방법

질문 배경: 데코레이터는 Python의 강력한 문법 기능 중 하나로, 함수나 클래스를 래핑(wrapping)하여 부가적인 기능을 첨가하는 역할을 합니다. 문법적으로는 함수 정의 앞에 @데코레이터이름을 붙이는 것으로 사용되는데, 처음 보는 사람에겐 상당히 난해해 보입니다. 예컨대 Flask나 Django 같은 프레임워크 코드에서 @app.route(...)나 @login_required 같은 구문을 접하고 “저건 어떻게 동작하는 거지?”라는 질문을 많이 하게 됩니다.

데코레이터의 개념을 풀어서 설명하면: 데코레이터는 함수를 인자로 받아 새로운 함수를 반환하는 함수입니다​. 평소에 반복적으로 추가해야 하는 기능(예를 들어 함수 실행 시간 측정, 로그인 확인 등)을 함수에 직접 코드 쓰지 않고 데코레이터로 만들어 두고 @ 문법으로 적용하면, 해당 함수의 앞뒤로 데코레이터의 로직이 자동으로 실행되게 됩니다. @decorator를 함수 정의 앞에 붙이는 것은 사실 func = decorator(func)와 동일한 효과라고 보면 됩니다​.

어려워하는 이유: 데코레이터는 개념적으로 **고차 함수(함수를 인자로 받고 함수를 반환)**와 클로저(내부 함수가 외부 변수에 접근) 개념을 이해해야만 완전히 납득이 됩니다. 이러한 함수형 프로그래밍 패턴에 익숙하지 않은 개발자들은 처음에 매우 혼란스러워합니다. 하지만 일단 이해하고 나면 코드 재사용과 반복 제거에 큰 도움을 주기 때문에 중요도는 중간 이상입니다. 기술적 관점에서, 데코레이터는 결국 함수 객체를 조작하는 문법 설탕(syntax sugar)이므로, 이를 풀어서 쓰는 방법부터 먼저 익히면 좋습니다​. 교육적으로는 간단한 데코레이터를 직접 만들어보게 하여 (@ 없이 함수로 wrapping 했다가 나중에 @ 문법으로 전환) 동작을 확인시킵니다. 그러면 개발자들은 "아, 함수 정의 위에 @로 처리하는 것이 이런 식으로 동작하는구나" 하고 감을 잡게 됩니다.

3.10 제너레이터와 yield 키워드의 동작 원리

질문 배경: **제너레이터(generator)**는 Python에서 이터레이터(iterable)를 생성하는 특별한 함수로, 이 함수를 실행하면 한 번에 하나의 값만 산출하고 상태를 유지한 채 일시중지되는 특징이 있습니다. 이를 가능하게 하는 키워드가 **yield**입니다. 많은 개발자들이 처음 yield를 접하면 함수 안에 return 대신 여러 번 쓰이는 yield의 동작을 이해하지 못해 "yield가 도대체 뭔가요? 어떻게 작동하죠?"라는 질문을 합니다.

yield가 쓰인 함수를 호출하면 함수 본문을 바로 실행하지 않고 제너레이터 객체를 반환합니다. 이 제너레이터는 __iter__와 __next__ 메서드를 가진 이터레이터로서, next()를 호출하거나 for 루프로 순회할 때마다 함수 본문이 이전 실행 지점부터 이어서 실행됩니다. yield를 만나면 함수는 현재 상태를 보존한 채 호출자에게 값을 산출(yield)하고, 다시 호출될 때 그 지점 바로 다음부터 실행을 재개합니다​. 이러한 작동 방식 때문에 제너레이터는 **게으른 평가(lazy evaluation)**나 스트림 처리 등에 유용하며, 무한 수열도 메모리 걱정 없이 만들어낼 수 있습니다.

어려워하는 이유: 제너레이터는 일반 함수와 달리 한 번에 하나의 값만 반환하고 멈춰있다가(next 호출 전까지) 다시 이어서 실행된다는 점에서 **코루틴(coroutine)**에 가까운 동작을 합니다. 이 개념은 전통적인 절차적 프로그래밍만 해온 사람들에게는 어렵게 느껴집니다. 또한 yield from 같은 추가 문법까지 들어가면 더 복잡해지죠. 하지만 제너레이터를 이해하면 효율적인 반복문 구현이 가능하기 때문에 중요도는 중간 정도입니다. 개발자들은 주로 "한 함수에서 여러 값을 순차적으로 어떻게 리턴하지?" 혹은 "yield를 쓰면 무엇이 좋은가요?" 등을 궁금해합니다. 교육적으로는 간단한 예제를 통해, 함수가 yield에서 멈추고 재개되는 모습을 출력으로 확인시키면 이해가 빠릅니다. 예를 들어 함수 안에서 print를 사용해 어떤 순서로 실행되는지 보여주면 "yield가 실행 지점을 보존하고 있다는 것"을 체감할 수 있습니다​. 이를 통해 제너레이터의 동작 원리를 파악하면, 큰 데이터 처리나 커스터마이즈된 이터레이터 작성 등에 활용할 수 있게 됩니다.

4. 추천 해결 방법

이제 위에 소개된 각 질문별로 문제를 해결하거나 개념을 명확히 이해하기 위한 방법을 제시하겠습니다. 각 항목마다 예제 코드를 포함하여 설명합니다.

4.1 기본 인수로 가변 객체를 사용할 때의 해결 방법

해결책: 함수 정의 시 가변 객체를 직접 기본값으로 두지 않는 것이 가장 좋습니다. 대신 기본값으로 None을 두고, 함수 내부에서 None일 경우 새로운 객체를 생성하도록 처리합니다​. 이렇게 하면 호출 때마다 새로운 리스트나 딕셔너리가 만들어져 앞서 말한 누적 문제가 해결됩니다. 다음은 잘못된 예와 개선된 예입니다.

# 나쁜 예: 기본값으로 빈 리스트 사용 (문제 발생)
def append_to_list(val, lst=[]):
    lst.append(val)
    return lst

print(append_to_list(1))  # [1]
print(append_to_list(2))  # [1, 2] (기대와 달리 이전 값 누적)

# 좋은 예: None을 기본값으로 사용
def append_to_list_fixed(val, lst=None):
    if lst is None:            # 매 호출마다 새로운 리스트 생성
        lst = []
    lst.append(val)
    return lst

print(append_to_list_fixed(1))  # [1]
print(append_to_list_fixed(2))  # [2] (각 호출 결과 독립적)

이처럼 기본 인수를 None으로 두고 내부에서 초기화하는 패턴은 Python에서 널리 사용되는 관용구입니다. 만약 기본값으로 놓고 싶었던 불변 객체라면 (None, 숫자, 문자열, 튜플 등) 그대로 사용해도 안전합니다​. 문제는 가변 객체일 때뿐이므로 이를 주의하면 됩니다.

4.2 변수 스코프 이슈 해결 (global/nonlocal의 사용)

해결책: 함수 내부에서 전역 변수를 읽고 쓰려면 global 키워드를 사용해 해당 이름이 전역임을 명시해야 합니다. 또한 중첩 함수 내부에서 바깥 함수의 변수에 접근하려면 nonlocal 키워드를 사용합니다. 아래 예시를 통해 차이를 살펴보겠습니다.

x = 10
def use_global():
    global x        # x가 전역 변수임을 선언
    print(x)        # 전역 x 출력 (10)
    x += 5          # 전역 x 값을 변경

use_global()
print(x)            # 전역 x가 변경되었음 (15)

def outer():
    y = 5
    def inner():
        nonlocal y  # 바깥 함수의 y를 사용한다고 선언
        y += 1      # 바깥 함수의 y를 변경
    inner()
    return y

print(outer())      # 6 (inner 실행으로 outer의 y가 변경됨)

위와 같이 global을 선언하면 함수 내부에서 전역 변수를 직접 수정할 수 있고​, nonlocal을 선언하면 중첩 함수에서 enclosing(바로 바깥) 함수의 변수를 수정할 수 있습니다​. 반대로, 아무 선언 없이 함수 내부에서 전역변수와 같은 이름으로 할당을 하면 별도의 지역 변수가 생성되는 것이므로 주의해야 합니다.

일반적으로 전역 변수의 사용은 최소화하는 것이 좋습니다. 함수의 리턴 값을 사용하거나, 객체의 속성을 이용하는 방식으로 변경하는 편이 바람직합니다. 부득이 전역 상태를 변경해야 한다면 이처럼 global을 쓰되, 코드 가독성을 위해 해당 함수의 역할을 명확히 해두는 것이 좋습니다.

4.3 is vs ==: 올바른 비교 연산자 사용법

해결책: 값을 비교할 때는 항상 ==을 사용하고, 객체의 동일성을 확인해야 할 때만 is를 사용하는 것이 원칙입니다​. 특히 None과의 비교는 Python 스타일 가이드(PEP 8)에서도 is None 또는 is not None으로 하라고 명시하고 있습니다. 몇 가지 사례를 통해 올바른 사용을 정리해보면:

# 동등성 비교 (==) 사용 예시
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b)   # True (내용이 같으므로 동등)
print(a is b)   # False (서로 다른 객체)

# 동일성 비교 (is) 사용 예시
x = None
print(x is None)      # True (None 객체 자체를 비교)
y = float('nan')
print(y == None)      # False (잘못된 사용, None 비교는 is로 해야 함)
print(y is None)      # False (y는 None이 아님)

위 예에서 리스트 a와 b는 값은 같지만 a is b는 False입니다. 반면 None처럼 싱글턴 객체동일 객체임을 보장해야 하는 상황에서는 is를 사용합니다. 예를 들어 어떤 클래스에서 싱글턴 패턴을 구현했다면 인스턴스 비교에 is를 쓸 수 있겠죠.

또한, == 연산자는 사용자 정의 객체에서 오버라이드되어 동작할 수 있으므로 (예를 들어 numpy의 배열 == 연산 등) 의도치 않은 부작용이 있을 수 있지만, is는 그런 오버라이드를 무시하고 **객체의 정체(identity)**만 비교합니다​. 따라서 요약하면:

  • 일반적인 값 비교: == 사용 (특히 숫자, 문자열, 리스트 등 내용이 중요한 경우)
  • 특별한 객체 자체 비교: is 사용 (None 여부, 혹은 두 변수 참조가 같은 객체인지 확인 등)

이 규칙을 따르면 대부분의 상황에서 올바르게 연산자를 사용할 수 있습니다.

4.4 *args와 **kwargs 사용 예시와 팁

해결책: *args와 **kwargs는 함수 정의와 호출 양쪽에서 모두 활용될 수 있습니다. 정의 시에는 위에서 설명한대로 가변 인자를 받는 용도로 사용하고, 호출 시에는 시퀀스나 사전을 풀어서 인자로 전달하는 용도로 사용합니다. 몇 가지 팁과 함께 예제를 다시 살펴보겠습니다.

def fun_with_args(*args, **kwargs):
    # args는 튜플, kwargs는 딕셔너리로 받음
    print(f"args: {args}")
    print(f"kwargs: {kwargs}")

# 호출 측면에서의 *와 ** 사용
numbers = [1, 2, 3]
options = {"sep": " -> ", "end": " !!!\n"}
print(*numbers, **options)  
# 출력: 1 -> 2 -> 3 !!!    (*numbers는 1,2,3을 개별 인자로 전달, **options는 sep와 end 키워드를 전달)

# 함수에 가변인자 전달
fun_with_args(10, 20, key1="foo", key2="bar")
# 출력:
# args: (10, 20)
# kwargs: {'key1': 'foo', 'key2': 'bar'}

위 예시에서 print(*numbers, **options)는 리스트 numbers를 언패킹하여 print(1, 2, 3, sep=" -> ", end=" !!!\n")와 동일하게 호출합니다. 또 fun_with_args 함수는 위치 인자 2개와 키워드 인자 2개를 받아 args 튜플과 kwargs 딕셔너리에 담아 출력하고 있습니다.

이처럼 **정의할 때의 *args/**kwargs**와 호출할 때의 */** 개념을 모두 알아두면 편리합니다. 가령, 한 함수를 감싸서 그대로 다른 함수에 전달만 하고 싶다면:

def wrapper(*args, **kwargs):
    # 받은 모든 인자를 그대로 다른 함수에 전달
    return other_function(*args, **kwargs)

처럼 작성할 수 있습니다. 이 경우 wrapper는 어떤 인자를 받아도 other_function에 그대로 넘겨주므로 재사용성이 높아지겠죠. 또한 함수 정의 시엔 *args 뒤에 일반 인자를 또 적을 순 없지만, 키워드 전용 인자를 정의하려면 *args 뒤에 명시적으로 키워드 인자를 적거나, Python 3.8+부터는 매개변수 리스트에 /와 * 기호를 사용해 위치 전용/키워드 전용 인자를 구분할 수 있다는 점도 고급 팁입니다.

4.5 얕은 복사 vs 깊은 복사: 제대로 복사하기

해결책: 객체를 복사할 때 내부에 중첩된 가변 객체가 없다면 얕은 복사로 충분하지만, 중첩 구조를 갖고 있다면 깊은 복사를 해야 원본과 독립적인 객체를 얻을 수 있습니다. Python에서는 내장 copy 모듈을 제공하며, copy.copy()는 얕은 복사를, copy.deepcopy()는 깊은 복사를 수행합니다. 사용 예시는 다음과 같습니다.

import copy

original = [[1, 2], [3, 4]]
shallow_copy = copy.copy(original)
deep_copy = copy.deepcopy(original)

shallow_copy[0][0] = 99
print("Original after shallow_copy modification:", original)
# Original after shallow_copy modification: [[99, 2], [3, 4]]

deep_copy[1][1] = 88
print("Original after deep_copy modification:", original)
# Original after deep_copy modification: [[99, 2], [3, 4]]

위 코드에서 얕은 복사본 shallow_copy의 [0][0] 원소를 변경했더니 original에도 영향이 갔습니다. 반면 깊은 복사본 deep_copy를 변경한 것은 original에 전혀 영향을 주지 않았습니다. 이처럼 얕은 복사는 중첩 객체의 참조를 공유하므로 한 쪽 변경이 원본을 변경할 수 있고​, 깊은 복사는 모든 구성요소를 새로 만들어 복사하므로 완전히 별개의 객체가 됩니다​.

복사 방법 요약:

  • 얕은 복사: 객체의 최상위 레벨만 복사. (예: copy.copy(), 리스트의 list(...) 생성이나 [:] 슬라이싱 등)
  • 깊은 복사: 내부에 포함된 객체들까지 재귀적으로 모두 복사. (예: copy.deepcopy())

만약 객체가 1차원 리스트처럼 중첩이 없다면 얕은 복사로도 충분합니다. 그러나 복합 객체라면 언제 얕은 복사로 인해 참조 공유가 발생할지 염두에 두고 있어야 합니다. 또한, 불변 객체만 포함한 구조라면 얕은 복사도 안전합니다 (불변 객체는 공유되어도 내용 변경이 불가능하므로). 상황에 맞게 적절한 복사 전략을 선택하세요.

4.6 Python의 인자 전달 방식 이해 및 활용

해결책: Python이 함수 인자를 객체 참조로 전달한다는 것을 이해하면, 원하는 동작을 얻기 위해 어떻게 코드를 작성해야 할지 감이 잡힙니다. 요점을 정리하면:

  • 함수 내부에서 뮤터블 객체를 변경하면 호출자 쪽 원본도 변경된다.
  • 함수 내부에서 변수에 재할당(assign) 하면 그 변수는 지역 변수로 취급되어 호출자에 영향을 주지 않는다.

이를 확인하는 간단한 예를 보겠습니다.

def append_element(lst):
    lst.append(100)   # 전달받은 리스트에 원소 추가

def replace_list(lst):
    lst = [0, 1, 2]   # 새로운 리스트로 변수 재할당 (지역 변수 변경)

my_list = [5, 6]
append_element(my_list)
print("after append_element:", my_list)   # after append_element: [5, 6, 100]

replace_list(my_list)
print("after replace_list:", my_list)     # after replace_list: [5, 6, 100]

위에서 append_element는 리스트 lst 자체를 수정하기 때문에 함수 호출 후에도 원본 my_list에 그 변경이 반영됩니다. 반면 replace_list는 함수 내부에서 lst를 새로운 리스트로 바꿨지만, 이는 지역 변수 lst에만 영향을 주었을 뿐 원본 my_list에는 아무 영향이 없습니다.

만약 함수 내부에서 원본 객체를 변경하지 않도록 하려면, 불변 객체를 사용하거나 가변 객체를 복사해서 전달하면 됩니다. 예를 들어 리스트를 함수에 넘길 때 func(list(original))처럼 아예 복사본을 넘겨주면 함수 안에서 어떤 조작을 하든 원본은 안전합니다. 또는 함수 내부에서 원본을 건드리지 않고 새로운 객체를 만들어 반환하는 방식을 택할 수도 있죠.

결국 핵심은, Python 함수는 인자로 받은 객체에 대한 레퍼런스를 가지고 작업한다는 것입니다. 이 사실을 이해하면, 필요에 따라 원본을 보존하거나 수정하는 코딩 전략을 세울 수 있습니다. 특별히 함수의 부작용(side effect)을 최소화하려는 함수형 프로그래밍 관점에서는, 가변 객체를 함수에 전달할 때 주의하여 설계해야 합니다.

4.7 리스트 vs 튜플: 언제 무엇을 사용해야 하나

해결책: 리스트와 튜플의 가장 큰 차이는 변경 가능성입니다. 따라서 데이터를 변경해야 한다면 리스트를, 한 번 정해지면 변경할 필요가 없는 데이터라면 튜플을 사용하는 것이 원칙입니다. 몇 가지 지침과 예를 들어보겠습니다.

  • 리스트를 사용할 때: 목록의 내용이 수시로 바뀌거나, 동적으로 원소를 추가/삭제해야 하는 경우.
  • 튜플을 사용할 때: 값의 집합이 논리적으로 한 번 정해지면 바뀌지 않거나, 각 위치의 값에 특별한 의미가 있는 레코드의 경우. 또한 튜플은 불변이라 딕셔너리의 키나 집합의 원소로 사용할 수 있습니다.
# 리스트 예시 - 변경이 필요한 경우
fruits = ["apple", "banana"]
fruits.append("cherry")      # 리스트에 원소 추가
fruits[0] = "avocado"        # 첫 원소 변경
print(fruits)               # ['avocado', 'banana', 'cherry']

# 튜플 예시 - 변경이 불필요한 고정 데이터
person = ("John Doe", 30, "New York")
# person[1] = 31             # 오류! 튜플은 항목 변경 불가
print(person)               # ('John Doe', 30, 'New York')

위에서 fruits 리스트는 .append()나 인덱스를 통한 수정이 가능하지만, person 튜플은 그런 작업을 허용하지 않습니다. 만약 튜플을 수정하려고 하면 AttributeError나 TypeError가 발생할 것입니다.

튜플을 사용하면 데이터가 불변임을 보장할 수 있기 때문에, 실수로 값이 변경되는 것을 막아주는 장점이 있습니다. 또한 앞서 언급했듯 튜플은 **해시(hash)**가 가능하여 딕셔너리 키로 쓸 수 있지만 리스트는 불가능합니다​. 예를 들어 좌표를 키로 갖는 딕셔너리를 만들 때 튜플을 키로 사용할 수 있습니다.

location_data = {}
coords = (37.5665, 126.9780)  # 서울의 위도, 경도 (튜플 사용)
location_data[coords] = "Seoul"
print(location_data[ (37.5665, 126.9780) ])  # "Seoul"

이렇듯 자료의 성격에 따라 적합한 자료형을 선택하는 것이 좋습니다. 만약 어떤 시퀀스가 수정될 일이 없다고 판단되면 튜플로 만들어 의도를 명확히 하는 편이 유지보수에 도움이 됩니다.

4.8 self와 클래스 변수/인스턴스 변수 제대로 사용하기

해결책 (self 사용): 인스턴스 메서드 정의 시 첫 번째 매개변수는 반드시 self로 받아야 하며, 이를 통해 객체 자신의 속성에 접근하거나 다른 메서드를 호출합니다. self를 누락하면 해당 함수는 인스턴스 메서드가 아니라 일반 함수로 취급되므로 클래스 내부에 정의해도 호출 시 인자를 제대로 받지 못해 에러가 납니다. 또한 self라는 이름은 강제되지 않지만 PEP 8 스타일 가이드에 따라 반드시 self를 사용할 것을 권장합니다​. 아래는 올바른 self 사용 예시입니다.

class Person:
    def __init__(self, name):
        self.name = name      # 인스턴스 변수 설정에 self 사용

    def greet(self):
        print(f"Hello, my name is {self.name}")  # 인스턴스 속성 접근에 self 사용

p = Person("Alice")
p.greet()  # "Hello, my name is Alice"

위에서 __init__과 greet 메서드 모두 self를 첫 인자로 받고, 인스턴스 변수 self.name에 접근하고 있습니다. 이렇게 함으로써 각 Person 객체는 자기만의 name 속성을 가지게 됩니다.

해결책 (클래스 변수 vs 인스턴스 변수): 클래스 변수를 잘못 사용하여 모든 인스턴스가 하나의 객체를 공유하는 문제는, 인스턴스별로 가져야 할 데이터는 무조건 self를 통해 인스턴스 변수로 만들어야 해결됩니다. 클래스 변수는 정말 모든 인스턴스가 공유해야 하는 상수값이나 설정값 등에만 사용하십시오. 다음 예제로 차이를 확인합니다.

class MyClass:
    shared_list = []           # 클래스 변수 (모든 인스턴스가 공유)
    def __init__(self, value):
        self.value = value     # 인스턴스 변수 (각 인스턴스별로 별도)

obj1 = MyClass(1)
obj2 = MyClass(2)

obj1.shared_list.append(99)
print(obj2.shared_list)  # [99] - obj1과 obj2가 shared_list를 공유

obj1.value = 100
print(obj2.value)        # 2 - obj2의 value는 독립적

위 코드에서 shared_list는 클래스 변수로 정의되었기 때문에 obj1.shared_list.append(99)를 하면 obj2.shared_list에서도 그 변화가 보입니다. 반면 value는 각 인스턴스의 속성이므로 한쪽을 변경해도 다른 쪽에 영향이 없습니다.

만약 shared_list가 인스턴스마다 달라야 했다면, 클래스 변수로 두지 말고 __init__에서 self.shared_list = []로 초기화했어야 합니다. 클래스 변수를 읽는 것은 문제없지만 (예: 모든 인스턴스가 동일해야 하는 설정값 등), 클래스 변수를 수정할 때는 해당 클래스의 모든 인스턴스와 subclasses에 영향을 준다는 점을 명심해야 합니다​.

정리하면:

  • 인스턴스 변수: self.변수명 = 값 형태로 정의. 개별 객체의 상태를 저장. 인스턴스마다 따로 존재.
  • 클래스 변수: 클래스 정의 바로 아래에 변수명 = 값 형태로 정의. 해당 클래스와 그 하위 클래스의 모든 인스턴스가 공유.

올바르게 사용하면 클래스 변수는 유용하지만, 애매하면 인스턴스 변수로 만드는 편이 안전합니다. 또한 파이썬에서는 클래스 변수와 인스턴스 변수 이름이 충돌할 수 있으므로 (인스턴스 네임스페이스에 없으면 클래스 변수 참조) 가급적 같은 이름을 사용하지 않도록 합니다.

4.9 데코레이터 사용법 예제

해결책: 데코레이터의 동작을 이해하고 사용하는 가장 좋은 방법은 직접 간단한 데코레이터를 만들어보는 것입니다. 아래에 문자열을 대문자로 변환하는 데코레이터를 예로 보여드립니다.

def uppercase_decorator(func):
    def wrapper():
        result = func()            # 기존 함수 실행
        return result.upper()      # 결과를 대문자로 변환하여 반환
    return wrapper

@uppercase_decorator
def greet():
    return "Hello, world!"

print(greet())  # HELLO, WORLD!

여기서 uppercase_decorator는 함수를 인자로 받아 wrapper라는 내부 함수를 반환합니다. greet 함수 위에 @uppercase_decorator를 붙였기 때문에, greet()를 호출하면 실제로는 데코레이터가 반환한 wrapper 함수가 실행되어 원본 greet의 결과를 가로채 대문자로 변환 후 출력합니다. 이 과정은 다음과 같이 풀어 쓸 수도 있습니다 (데코레이터 적용을 풀어서 설명):

def greet():
    return "Hello, world!"
greet = uppercase_decorator(greet)  # 데코레이터 수동 적용
print(greet())  # HELLO, WORLD!

두 방식 모두 동일한 결과를 냅니다​.

이런 데코레이터 패턴을 활용하면 로그 출력, 권한 체크, 성능 측정 등 반복되는 부가 기능을 깔끔하게 분리할 수 있습니다. 몇 가지 실전 팁을 추가로 소개하면:

  • 인자가 있는 함수에 데코레이터를 적용하려면, 데코레이터 내부 wrapper를 정의할 때 *args, **kwargs를 사용하여 임의의 인자를 받아 원본 함수에 그대로 전달하면 됩니다.
  • 데코레이터가 자체 인자를 받아 동작을 바꿔야 할 경우 (예: @retry(times=3) 같은 것), 데코레이터를 한 번 더 감싼 데코레이터 팩토리를 만들어야 합니다. 이는 다소 복잡한 주제이므로 필요할 때 학습하면 됩니다.
  • 표준 라이브러리의 functools.wraps를 이용하면 데코레이터가 적용된 함수의 __name__이나 __doc__ 같은 메타데이터를 원본 것처럼 유지할 수 있습니다.

데코레이터는 처음엔 복잡해 보이지만, **“함수를 인자로 받아 함수를 리턴하는 객체”**임을 기억하면 됩니다​. 이 개념에 익숙해지면, Pythonic한 코드 구조를 만드는 강력한 도구로 활용할 수 있습니다.

4.10 제너레이터와 yield 사용 예제

해결책: 제너레이터의 동작 원리를 이해하기 위해서는 yield가 함수 실행을 일시 중단하고, 값 반환 후 다시 호출될 때 그 다음 줄부터 재개한다는 것을 직접 보는 것이 가장 좋습니다. 아래 예를 통해 yield의 동작을 단계별로 살펴보겠습니다.

def simple_generator():
    print("Start")
    yield 1            # 첫 번째 산출
    print("Continue")
    yield 2            # 두 번째 산출
    print("End")

gen = simple_generator()
print(next(gen))  # Start \n 1
print(next(gen))  # Continue \n 2
try:
    print(next(gen))
except StopIteration:
    print("Generator finished.")
# 출력:
# Start
# 1
# Continue
# 2
# End
# Generator finished.

위 코드에서 simple_generator()를 호출하면 함수 내용이 실행되지 않고 제너레이터 객체 gen이 얻어집니다. next(gen)을 처음 호출하면 함수 본문이 실행되다가 yield 1을 만나 **"Start"를 출력한 뒤 1을 산출(yield)**하고 일시 중단됩니다. 두 번째 next(gen) 호출 시 바로 이전 중단 지점 다음 줄부터 (print("Continue")) 실행하여 "Continue"를 출력하고 yield 2를 만나 2를 산출합니다. 다시 중단되었다가 next를 세 번째 호출하면 남은 코드를 실행하고 더 이상 yield가 없으므로 "End"를 출력한 뒤 함수가 종료되고 StopIteration 예외가 발생합니다. 이 흐름을 통해 yield 키워드가 어떻게 함수의 상태를 유지한 채 실행을 중단했다 재개하는지 명확히 볼 수 있습니다​.

일반 함수라면 한 번 return하면 끝이지만, 제너레이터는 yield를 여러 번 사용하여 여러 값을 차례로 산출할 수 있다는 점이 핵심입니다. 이런 특징으로 인해 제너레이터는 메모리 효율적인 이터레이터를 구현하는 데 많이 쓰입니다. 예를 들어 아주 큰 범위의 숫자를 생성해야 할 때 리스트에 모두 넣기보다, 제너레이터로 하나씩 필요할 때 생성하면 메모리 소비를 줄일 수 있습니다.

def count_up_to(n):
    i = 1
    while i <= n:
        yield i
        i += 1

for num in count_up_to(5):
    print(num, end=" ")
# 출력: 1 2 3 4 5

위 count_up_to 제너레이터는 1부터 n까지의 숫자를 순차적으로 발생시킵니다. for 루프가 제너레이터를 자동으로 이터레이션하며 StopIteration이 나오면 종료합니다. 제너레이터를 잘 활용하면 무한 수열도 표현할 수 있고 (itertools.count() 등), 데이터 스트림을 처리할 수도 있습니다.

마지막으로, yield from 문법은 제너레이터 내부에서 또 다른 이터러블(또는 제너레이터)을 위임하여 값들을 연속해서 내보낼 때 사용합니다. 이는 고급 주제이므로 필요할 때 학습하면 되고, 기본적으로는 yield의 동작 원리만 잘 이해하면 대부분의 제너레이터 패턴을 구현할 수 있습니다.

5. 최근 Python 문법 트렌드

최근 1~2년간 Python에는 몇 가지 주목할 만한 새로운 문법 기능트렌드가 등장했습니다. Python 개발자 커뮤니티에서 화제가 되었던 문법적 변화들을 정리하면 다음과 같습니다.

  • 할당 표현식(Assignment Expressions) – 일명 바다코끼리 연산자 :=의 도입. Python 3.8에서 추가된 이 새로운 연산자는 표현식 안에서 변수를 할당할 수 있게 해줍니다​. 예를 들어, 이전에는 조건문 전에 값을 계산하고 변수에 담았다가 조건에서 그 변수를 사용하는 패턴이 흔했는데, 이제는 if (n := len(some_list)) > 10:처럼 조건문 안에서 바로 some_list의 길이를 n에 저장하며 사용할 수 있습니다. 이 기능은 while 루프에서 입력을 받으면서 바로 검사한다거나 하는 경우에 유용하며, 코드의 중복을 줄이고 가독성을 개선하는 최근 트렌드로 자리잡았습니다.
  • 구조적 패턴 매칭(Structural Pattern Matching) – Python 3.10에서 도입된 match-case 문은 데이터 구조의 형태에 따라 분기 처리를 할 수 있는 강력한 기능입니다​. 간단히 말해, 다른 언어의 switch문을 일반화한 형태로 볼 수 있는데, 리스트나 딕셔너리 같은 구조까지 패턴으로 매칭하여 분기할 수 있다는 점이 혁신적입니다. 예를 들어 match point: 구문을 사용해 (x, y) 형태의 튜플을 매칭하거나, {"type": "info", "msg": msg} 같은 딕셔너리 패턴도 매칭할 수 있습니다. 이를 통해 복잡한 데이터 언패킹과 검사 로직을 간결하게 표현할 수 있으며, Python 3.10 이후 개발자들 사이에서 적극 활용되는 추세입니다. 패턴 매칭은 **데이터 클래스(dataclass)**나 타이핑과 결합하여 유용하게 쓰이고 있으며, Python이 점점 보다 선언적인 스타일을 지원하게 되었음을 보여주는 사례입니다.
  • 타입 힌트의 발전과 정적 타입 체크 – Python 3.5에 도입된 형 힌트(type hint) 시스템이 최근 몇 년간 크게 발전하였습니다. 특히 Python 3.9~3.11 사이에 타입 힌트 문법이 개선되어, list[int]처럼 내장 컬렉션에 대한 제너릭 표기를 지원하고, int | str처럼 Union 타입을 파이프 연산자로 간단히 표시할 수 있게 되었으며​, 3.11에서는 Self 타입이나 3.12의 PEP 695 (타입 매개변수 문법) 등으로 제네릭 사용이 더욱 편리해졌습니다​. 이와 함께 mypyPyright 같은 정적 분석기가 널리 쓰이며, 대규모 프로젝트에서 타입 체크를 통한 버그 예방이 트렌드가 되고 있습니다. 즉, Python이 동적 타입 언어이지만 선택적으로 정적 타입 체크를 도입하여 코드의 안정성과 문서화를 향상시키는 방향으로 나아가고 있습니다. 많은 라이브러리들도 함수에 타입 힌트를 부여하여 사용자에게 인터페이스를 명확히 설명하는 추세입니다.
  • 그 외 문법 및 편의성 개선 – 최근 버전들은 사용자 친화적인 작은 개선들도 다수 포함하고 있습니다. 예를 들어 향상된 에러 메시지는 Python 3.10부터 IndentationError나 NameError 등에 대해 "혹시 ...를 의도한 건가요?" 같은 제안을 제공합니다. 3.11에서는 Exception Groupexcept* 문법이 도입되어 동시 발생한 예외를 처리할 수 있게 되었고, 3.12에서는 f-문자열의 제한이 제거(모든 식을 지원)되는 등​자잘한 문법 개선이 이어졌습니다. 이러한 변화들은 Python이 신규 사용자에게 더욱 친절하고, 숙련자에게는 편의성을 주는 방향으로 발전하고 있음을 보여줍니다.

요약하면, Python은 꾸준히 문법을 발전시키고 있습니다. 새로 추가된 := 연산자나 패턴 매칭은 코딩 스타일에 변화를 주었고, 타입 힌트의 대중화는 Python 코드를 작성하고 이해하는 방식을 조금씩 바꾸고 있습니다. 최신 문법 트렌드를 따라가는 것은 더 나은 Pythonic 코드를 작성하는 데 도움이 될 것이며, 앞으로도 Python의 변화에 유연하게 적응하는 것이 중요합니다.

6. 참고문헌

  1. Python 공식 문서 – Programming FAQ: 함수 기본값의 동작, 변수 스코프 등 Python 언어의 자주 묻는 문법 사항들을 정리한 FAQ​​. Python Docs (https://docs.python.org/3/faq/programming.html)
  2. Python 공식 문서 – What’s New in Python 3.10: PEP 634 구조적 패턴 매칭 등 Python 3.10의 새로운 문법 기능을 소개​. Python Docs (https://docs.python.org/3/whatsnew/3.10.html)
  3. Python 공식 문서 – What’s New in Python 3.12: PEP 695 등 Python 3.12의 새로운 문법 (타입 파라미터 구문 개선, f-string 향상 등) 소개​. Python Docs (https://docs.python.org/3/whatsnew/3.12.html)
  4. Toptal 기술 블로그 – “Top 10 Most Common Python Code Mistakes”: Python 개발자가 자주 저지르는 실수 목록으로, 가변 기본 인수나 클래스 변수 오용 등의 사례를 다룸​. (https://www.toptal.com/python/top-10-mistakes-that-python-programmers-make)
  5. Simplilearn – “Python Interview Questions & Answers”: 얕은 복사 vs 깊은 복사 등의 개념을 문답 형식으로 설명​​
    . (https://www.simplilearn.com/tutorials/python-tutorial/python-interview-questions)
  6. Velog 블로그 – “Python: call-by-reference or call-by-value?”: Python의 인자 전달 방식을 한국어로 설명한 글로, mutable/immutable에 따른 동작 차이를 정리​. (https://velog.io/@bandi12/Python-call-by-reference-or-call-by-value)
  7. **Stack Overflow Q&A – *“Use of args and kwargs”: *args와 **kwargs의 쓰임을 묻는 질문에 대한 최고 답변으로, 예제 코드와 함께 사용 방법을 설명​​
    . (https://stackoverflow.com/questions/3394835/use-of-args-and-kwargs)
  8. Stack Overflow Q&A – “‘is’ vs ‘==’ in Python”: is와 ==의 차이를 설명한 답변으로, 두 연산자의 용도 차이를 예시와 함께 명확히 서술​. (https://stackoverflow.com/questions/132988/is-vs-in-python)
  9. Stack Overflow Q&A – “Python decorator explained”: 데코레이터의 개념을 간단히 설명하고 @ 문법과 일반 함수 할당을 비교한 답변​. (https://stackoverflow.com/questions/12046883/python-decorator-can-someone-please-explain-this)
  10. Stack Overflow Q&A – “What does the yield keyword do?”: yield의 작동 원리를 자세히 풀어낸 유명한 답변으로, 제너레이터가 함수의 상태를 유지하며 실행을 중단하는 과정을 서술​. (https://stackoverflow.com/questions/231767/what-does-the-yield-keyword-do-in-python)
반응형

댓글