C.W.K.
Stream
Lesson 04 of 07 · published

변수와 할당 — 이름, 값, 그리고 정체성

~22 min · 변수, 할당, 정체성, 동등성, mutability, 참조, id, 튜플-언패킹

Level 0호기심
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete

할당은 *바인딩* 이지 복사가 아니야

Lesson 1 에서 지나가듯 말한 거 — *이름은 라벨, 타입은 값에 살아*. 이 lesson 이 그 mental model 을 *load-bearing* 으로 바꿔. 한 번 내재화하면, 커리어 절반의 버그가 증발해. 잘못 이해하면 — 몇 달 단위로 mutability 사고에 계속 물려.

x = 42 라고 쓰면, Python 이 x 라는 *상자* 를 만들어서 42 를 그 안에 넣는 게 아니야. Python 이 *값이 42 인 정수 객체* 를 만들고, 이름 x 가 그 객체를 가리키게 해. 이름과 객체는 *다른 것* — 그리고 *같은 객체* 를 *여러 이름* 이 가리킬 수 있어.

Mutable vs immutable — 언어 전체를 가로지르는 분리선

모든 Python 값은 *mutable* (내용 in-place 변경 가능) 또는 *immutable* (불가능) 둘 중 하나. 분리는 타입으로 고정:

  • Immutable — int, float, str, bool, tuple, frozenset, bytes
  • Mutable — list, dict, set, bytearray, 그리고 명시적으로 frozen 안 한 모든 클래스 인스턴스

왜 중요한가 — 두 이름이 *같은 mutable 객체* 를 가리키면, 한 이름으로 변경한 게 다른 이름으로 보여. 객체는 *하나뿐*. 두 이름이 *같은 immutable 객체* 를 가리키면, 변경 자체가 불가 — 변경처럼 보이는 연산 (예 — int 의 x += 1) 은 사실 *새 객체 만들고 이름 다시 가리키게* 함.

경고: Python 초보자의 가장 흔한 단일 버그 — a = [1, 2, 3]; b = a; b.append(4). 이제 a[1, 2, 3, 4] 야. 왜냐면 ab 는 *두 개의 list* 였던 적이 없어. 항상 *이름 두 개를 가진 list 하나* 였어. 분리된 list 가 필요하면 명시적으로 — b = a.copy() (또는 b = list(a), 또는 b = a[:]).

== vs is — 동등성 vs 정체성

비슷해 보이고 *완전히 다른 의미* 의 두 연산자:

  • ==동등성 검사 — *"두 값이 같다고 비교되나?"* 내부적으로 __eq__ 호출, 클래스가 override 가능.
  • is정체성 검사 — *"두 이름이 메모리의 *문자 그대로 같은 객체* 를 가리키나?"* override 불가.

경험칙 — 값 비교는 ==, *singleton* 비교 (None, True, False) 는 is. PEP 8 가 codify 한 것 — if x is None:, *절대* if x == None: 안 함. 결과는 같지만, is None 이 idiom.

id() — 객체 식별자

Python built-in id(obj) 가 객체의 lifetime 동안 유일한 정수 반환. CPython 은 메모리 주소로 구현. a is bid(a) == id(b) 와 동치.

production 코드에선 id() 거의 안 써. 근데 *학습 중* 이해 도구로는 무가치. *"내 연산 후에 이게 여전히 같은 객체인가?"* — 전후로 id() 출력해.

다중 할당과 튜플 언패킹

Python 은 한 statement 에서 여러 이름 할당을 허용. 오른쪽이 *튜플로 먼저* 평가되고, 왼쪽 이름들로 *언패킹*. 가장 idiomatic Python 패턴 일부의 기반.

대표 — 변수 스왑. 대부분 언어에선 ab 스왑이 임시 변수 필요. Python 에선 — a, b = b, a. 오른쪽이 튜플 (b, a) 가 되고, 다시 a, b 로 언패킹. 원자적, 임시 변수 없음.

자기참조: cwkPippa 의 백엔드는 튜플 언패킹을 어디서나 써 — session_id, message_id = await create_pair(), brain, model = pick_brain_for_task(task), x, y, w, h = avatar_box. 영어처럼 읽혀 — *"이 두 가지에 이름 붙여, 값은 여기."* 그 readability 가 Pythonic 의 보상.

Augmented 할당 — mutable 의 깜짝 효과

x += 1 이 단일 연산자로 보이는데, 의미는 왼쪽이 mutable 인지에 달려있어. immutable 타입 (int, str, tuple) 에선 x += 1x = x + 1 과 *정확히 동치* — Python 이 새 객체 만들고 이름 다시 가리킴.

mutable 타입 (list, dict, set) 에선 x += y 가 정의돼있으면 __iadd__ 호출, x 를 *in-place 변경*. list += other 는 in-place append, list = list + other 는 새 list 생성. 비슷해 보여, 같지 않아.

*"default mutable 인자"* 함정 (preview)

이건 위 모든 걸 합쳐서 Python 의 가장 유명한 초보자 함정. Flow 트랙에서 제대로 다뤄, 지금은 *모양만* 봐:

def foo(items=[]): 라고 쓰고 인자 없이 세 번 호출하면, 세 호출이 *같은* list 를 공유한다는 걸 발견. default 인자는 함수 *정의* 시점에 *한 번* 평가, 호출마다 X. mutable default 가 호출 사이에 살아남아.

해결 — def foo(items=None): 그리고 안에서 if items is None: items = []. Flow 트랙이 *왜* 를 설명. 지금은 그 *냄새* 만 알아봐.

요약

이름이 객체를 가리켜. 어떤 객체는 mutable, 어떤 건 아니. 두 이름이 같은 mutable 객체를 가리킬 수 있어. == 는 값 비교, is 는 정체성 비교. 이 네 문장이 자명하게 느껴지는 시점부터, Python 의 나머지 동작이 더 이상 안 놀라워.

Pythonic Way: null 체크는 is None / is not None, *절대* == None 안 함. throw-away 변수 도입 전에 튜플 언패킹부터 손이 가는 게 맞아. *"안전 위해"* list copy 할 때, *진짜로 copy 가 필요한지* 아니면 *새 이름이 필요한 건지* 정직해야 — copy 가 싸지만 공짜는 아니고, 불필요한 copy 는 진짜 의도를 가려.

Code

이름은 객체를 가리킴, 상자가 아님·python
>>> x = 42
>>> y = x        # y 가 이제 x 와 *같은* 객체를 가리킴
>>> id(x) == id(y)
True
>>> x = 100      # x 가 새 객체로 다시 가리킴; y 는 여전히 42 가리킴
>>> y
42
>>> id(x) == id(y)
False
정체성 vs 동등성 — == vs is·python
>>> a = [1, 2, 3]
>>> b = [1, 2, 3]    # 같은 내용의 *다른* list
>>> a == b
True               # 값이 같음
>>> a is b
False              # 메모리상 다른 객체

>>> c = a            # c 가 a 와 *같은* 객체를 가리킴
>>> a is c
True               # 같은 객체

>>> # None 체크의 PEP 8 idiom:
>>> x = None
>>> if x is None: ...    # 맞음
>>> if x == None: ...    # 동작은 하지만 idiomatic 아님
Mutable aliasing — 가장 흔한 초보자 버그·python
>>> a = [1, 2, 3]
>>> b = a            # b 가 copy 받는 게 아님 — *같은* list 받음
>>> b.append(4)      # b 통해 변경
>>> a                # a 도 보임 — 처음부터 list 는 하나뿐
[1, 2, 3, 4]

>>> # 진짜 copy:
>>> b = a.copy()     # shallow copy — 새 list, 같은 element
>>> # 또는 b = list(a) 또는 b = a[:]

>>> # 중첩 구조 (list of lists) 면 deep copy 필요:
>>> import copy
>>> b = copy.deepcopy(a)
튜플 언패킹과 swap idiom·python
>>> # Pythonic swap — 임시 변수 X.
>>> a, b = 1, 2
>>> a, b = b, a
>>> a, b
(2, 1)

>>> # 함수에서 언패킹:
>>> def get_pair():
...     return "hello", 42
>>> name, count = get_pair()

>>> # Star 언패킹 — 나머지가 list 로:
>>> first, *middle, last = [1, 2, 3, 4, 5]
>>> first, middle, last
(1, [2, 3, 4], 5)
Augmented 할당 — int 와 list 가 다르게 동작·python
>>> # Immutable: x += 1 은 새 int 생성.
>>> x = 10
>>> id(x)
4327683600
>>> x += 1
>>> id(x)
4327683632       # 다른 객체

>>> # Mutable: list += other 는 in-place 변경.
>>> a = [1, 2, 3]
>>> id(a)
4385720128
>>> a += [4, 5]
>>> id(a)
4385720128       # 같은 객체 — a 가 in-place 변경됨

>>> # 비교: a = a + [4, 5] 는 새 list 만들고 a 를 다시 가리키게 했을 거야.
>>> # 비슷해 보여, 미묘하게 달라.
Default mutable 인자 함정 (preview)·python
# 이렇게 하지 마:
def add_item(item, items=[]):
    items.append(item)
    return items

print(add_item('a'))    # ['a']
print(add_item('b'))    # ['a', 'b']  — 깜짝!
print(add_item('c'))    # ['a', 'b', 'c']  — 셋이 한 list 공유

# 이렇게 해:
def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

print(add_item('a'))    # ['a']
print(add_item('b'))    # ['b']  — 호출마다 fresh list

External links

Exercise

REPL 에서 a = [1, 2, 3]b = a 만들기. b4 append, 그 다음 a 출력. 깜짝 결과 — 돌리기 *전에* 예측해보고 확인. 그 다음 b = a.copy() 로 반복. id(a)id(b) 로 *진짜로 뭐가 바뀌었나* 봐. 끝나는 시점에 — 아무한테나 (또는 아무한테도 아니게) *왜 첫 버전이 깜짝이고 두 번째가 안 깜짝인지* 소리 내서 설명할 수 있어야 해.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.