Python / 객체(Object)와 변수(Variable)

Python에서 객체는 값과 동작을 가진 데이터이고, 변수는 그 객체를 참조하도록 연결된 이름으로 이해하면 좋습니다. x = 100은 이름 x를 정수 객체 100에 연결하는 대입이며, 이후 다른 값을 대입하면 x가 가리키는 객체가 바뀝니다. 대입 자체가 객체를 복사하는 것은 아니므로 여러 이름이 같은 리스트를 공유할 수 있습니다.

이름과 객체, 자료형의 관계

Python 데이터 모델에서 객체에는 정체성(identity), 자료형(type), 값(value)이 있습니다. 자료형은 객체가 지원하는 연산과 성질을 결정합니다. 정체성은 객체가 살아 있는 동안 유지되며, 값의 변경 가능 여부는 자료형에 따라 다릅니다. 변수 이름에 고정된 자료형의 상자를 배정한다고 생각하면 재대입과 공유를 설명하기 어렵습니다.

아래 예제는 Python 3와 표준 라이브러리만 사용하며 Python 3.12.14에서 출력을 확인했습니다. UTF-8로 objects_demo.py를 저장하고 터미널에서 python objects_demo.py로 실행합니다. 환경에 따라 실행기는 python3일 수 있습니다. 이후 예제도 파일 내용을 교체하여 독립적으로 실행합니다.

value = 100
print(value, type(value).__name__)
value = "hello"
print(value, type(value).__name__)
100 int
hello str

이름 value가 처음에는 int 객체를, 다음에는 str 객체를 가리킵니다. 정수 100 자체의 자료형을 문자열로 바꾼 것이 아닙니다. 대입문은 오른쪽 표현식을 평가한 뒤 왼쪽 이름에 결과 객체를 연결합니다. type(value)는 그 시점에 연결된 객체의 자료형을 확인합니다.

같은 리스트 공유와 재대입의 차이

second = first를 실행하면 리스트 복사본이 생기는 대신 두 이름이 같은 리스트를 가리킵니다. 다음 예제는 공유 중인 객체의 변경과 이름의 재대입을 차례로 비교합니다.

first = [1, 2]
second = first
second.append(3)
print(first)
print(second)
print(first is second)
first = [0]
print(first)
print(second)
print(first is second)
[1, 2, 3]
[1, 2, 3]
True
[0]
[1, 2, 3]
False

append는 공유 리스트를 변경하므로 first로 읽어도 3이 추가되어 있습니다. 그러나 first = [0]은 first만 새 리스트에 연결합니다. second는 기존 리스트를 계속 참조합니다. 두 이름 중 하나를 바꾸는 것과 두 이름이 바라보는 객체를 수정하는 것을 구분해야 합니다.

동작 의미 공유 중인 다른 이름에 미치는 영향
second = first 같은 객체를 참조하는 이름 추가 이후 가변 객체 변경을 함께 관찰
second.append(3) 리스트 객체 변경 같은 리스트를 읽으면 변경된 내용 표시
first = [0] first를 다른 객체에 재연결 second의 연결은 유지

불변 객체와 +=의 동작

정수·문자열·튜플은 불변(immutable) 자료형이고, 리스트·딕셔너리는 가변(mutable) 자료형입니다. 불변이란 이름에 새 값을 대입할 수 없다는 뜻이 아니라 해당 객체 자체를 제자리에서 바꾸지 못한다는 뜻입니다.

first = 10
second = first
first += 1
print(first)
print(second)
11
10

정수 객체 10을 11로 수정한 것이 아니라 first를 덧셈 결과에 다시 연결한 것입니다. second는 10을 그대로 가리킵니다. 같은 +=라도 리스트는 기존 객체를 변경할 수 있으므로 모든 자료형에서 재대입과 똑같다고 일반화하지 않습니다. 튜플 자체의 항목 연결은 바꿀 수 없지만, 튜플 안에 들어 있는 리스트의 내용은 바뀔 수 있다는 점도 별개입니다.

독립적인 데이터가 필요하면 복사 범위를 결정하기

리스트의 copy()는 바깥 리스트를 새로 만드는 얕은 복사입니다. 중첩된 객체의 참조는 공유하므로 안쪽 리스트까지 독립시키려는 경우와 구분합니다. 다음은 얕은 복사와 copy.deepcopy()를 같은 원본으로 비교하는 전체 예제입니다.

from copy import deepcopy

original = [[1], [2]]
shallow = original.copy()
deep = deepcopy(original)
shallow[0].append(9)
shallow.append([3])
print(original)
print(shallow)
print(deep)
print(original is shallow)
print(original[0] is shallow[0])
[[1, 9], [2]]
[[1, 9], [2], [3]]
[[1], [2]]
False
True

shallow의 안쪽 리스트에 9를 추가하면 original에도 보입니다. 반면 shallow의 바깥 리스트에 [3]을 붙여도 original의 항목 수는 늘지 않습니다. deep은 복사 시점의 중첩 내용을 독립적으로 유지합니다. 실제 코드에서 중첩 공유가 의도된 것인지 먼저 판단하고, 필요한 범위만 복사합니다. 깊은 복사는 비용이 들고 파일·소켓 같은 자원을 일반 데이터처럼 복제하는 수단도 아닙니다.

==는 값 비교, is는 정체성 비교

내장 리스트에서 ==는 내용이 같은지, is는 바로 같은 객체인지 비교합니다. 다음 두 리스트는 값이 같지만 각각 새로 만들어졌습니다.

left = [1, 2]
right = [1, 2]
alias = left
print(left == right)
print(left is right)
print(left is alias)
print(id(left) == id(alias))
missing = None
print(missing is None)
True
False
True
True
True

비교 연산자의 목적을 구분해 사용합니다. id()는 정체성을 나타내는 정수이며 실제 숫자는 실행마다 달라질 수 있습니다. 그 값이 모든 Python 구현에서 메모리 주소라는 보장도 없습니다. 숫자·문자열의 값 비교에 is를 쓰면 캐싱·재사용 여부에 기대는 실수가 생기므로 값에는 ==를 사용합니다. None 여부 확인에는 is None을 사용합니다.

del은 이름의 연결을 지우는 문장

del은 함수가 아니라 문장입니다. del first는 해당 이름의 연결을 제거합니다. 다른 이름이 같은 객체를 참조한다면 그 객체는 계속 사용할 수 있습니다.

first = [1, 2]
second = first
del first
print(second)
try:
    print(first)
except NameError:
    print("first: NameError")
try:
    del unknown_name
except NameError:
    print("unknown_name: NameError")
[1, 2]
first: NameError
unknown_name: NameError

first를 삭제한 뒤 second는 리스트를 출력하지만 first를 읽으면 NameError입니다. 존재하지 않는 이름의 삭제도 NameError입니다. del 문은 대상에 따라 리스트 항목이나 속성을 지울 수도 있어, 모든 del이 이름 삭제만 하는 것은 아닙니다. 이름 하나를 지웠다고 객체와 메모리가 즉시 사라진다고 보장하지 않습니다. 파일은 del에 맡기지 말고 with나 close로 닫습니다.

변수명 규칙과 읽기 쉬운 이름

변수명은 식별자 규칙을 따릅니다. 영문 기준으로 문자나 밑줄로 시작하고, 이후 숫자를 포함할 수 있으며 공백·하이픈은 넣지 않습니다. 대소문자를 구분합니다. Python은 허용된 유니코드 문자도 식별자로 지원하므로 ‘영문만 가능’이라는 규칙은 정확하지 않습니다.

import keyword

for name in ["user_name", "2name", "class", "first-name", "café", "match"]:
    valid = name.isidentifier() and not keyword.iskeyword(name)
    print(name, valid)
print("match soft keyword:", keyword.issoftkeyword("match"))
user_name True
2name False
class False
first-name False
café True
match True
match soft keyword: True

isidentifier()만으로는 class 같은 예약어를 거르지 못하므로 keyword.iskeyword()도 확인합니다. match는 이 환경에서 일반 예약어가 아니라 특정 문맥에서만 의미를 갖는 소프트 키워드입니다. 이런 문맥 규칙은 식별자·키워드 문법을 함께 봐야 합니다. 키워드 목록은 버전별로 달라질 수 있어 현재 실행기의 keyword 모듈로 확인합니다.

문법상 가능한 이름이라도 list, str처럼 내장 이름을 덮어쓰면 원래 기능을 쓰기 어려워집니다. 값의 역할을 드러내는 user_name, item_count처럼 이름을 정하고, NameError가 나면 대소문자·대입 여부·이름이 유효한 범위를 확인합니다.