Data Model
앞선 글에서는 Python에서 모든 데이터가 Object로 표현되고, Name이 Object에 Binding되는 구조와 Python 코드가 Bytecode와 Execution Frame을 통해 실행되는 과정을 살펴보았습니다.
이번에는 조금 다른 관점에서 Python 객체를 살펴보겠습니다.
a.foo()
value = d["name"]
length = len(a)
result = a + b
위 코드는 모두 익숙한 Python 문법이지만 객체가 어떤 규칙에 따라 이러한 문법에 참여하는지는 단순하지 않습니다.
Python은 이러한 객체의 동작을 Data Model이라는 일관된 규칙으로 정의합니다.
이번 글에서는 Attribute Lookup, Function과 Method, Descriptor, Method Binding, Class와 Instance 그리고 Special Method를 중심으로 Python 객체가 언어의 여러 문법과 어떻게 상호작용하는지 살펴보겠습니다.
Attribute
다음 두 코드는 비슷해 보이지만 서로 다른 이름 조회 방식을 사용합니다.
x
a.x
첫 번째 x는 앞선 Object Model에서 살펴본 Name Resolution입니다.

위와 같은 Scope 규칙을 통해 이름을 찾습니다.
반면 두 번째 a.x는 a라는 객체에서 x라는 Attribute를 조회하는 동작으로 서로 다른 과정입니다.
Class와 Instance Attribute
다음 클래스를 살펴보겠습니다.
class A:
class_value = 10
a = A()
a.instance_value = 20
class_value는 Class에 저장된 Attribute이고 instance_value는 a Instance에 저장된 Attribute입니다.
일반적인 사용자 정의 클래스에서는 다음과 같이 확인할 수 있습니다.
print(A.__dict__)
# {'__module__': '__main__', '__firstlineno__': 1, 'class_value': 10,
# '__static_attributes__': (), '__dict__': <attribute '__dict__' of 'A' objects>,
# '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None}
print(a.__dict__)
# {'instance_value': 20}
A.__dict__는 Class Namespace를 보여주며, a.__dict__는 Instance Attribute가 저장된 Namespace를 보여줍니다.
모든 객체가 반드시 __dict__를 가지는 것은 아니지만 일반적인 사용자 정의 객체에서는 이러한 구조를 확인할 수 있습니다.
Attribute Lookup
그렇다면 a.x를 실행할 때 Python은 어디에서 x를 찾을까요?
가장 단순하게는 Instance에서 먼저 찾고 없으면 Class와 부모 Class들을 탐색한다고 생각할 수 있습니다.
하지만 실제 Python의 Attribute Lookup에는 뒤에서 살펴볼 Descriptor가 개입하기 때문에 조금 더 복잡합니다.
일반적인 Instance Attribute Lookup을 단순화하면 다음과 같습니다.

Class와 부모 Class를 탐색하는 순서는 MRO(Method Resolution Order) 를 따르며, 이는 다음과 같이 해당 Class의 검색 순서를 확인할 수 있습니다.
print(A.__mro__)
Python 공식 Data Model에서는 일반적인 Attribute Access가 객체와 Class의 Namespace뿐만 아니라 Descriptor Protocol에 의해 달라질 수 있다고 설명합니다.
__getattribute__와 __getattr__
Attribute 조회에는 두 Special Method도 중요합니다.
__getattribute__()는 거의 모든 일반적인 Attribute 조회의 진입점입니다.
a.__getattribute__("x")
반면 __getattr__()는 일반적인 조회 과정에서 Attribute를 찾지 못했을 때 호출되는 fallback입니다.
예를 들어 다음과 같이 사용할 수 있습니다.
class A:
def __getattr__(self, name):
return f"{name}은 존재하지 않습니다."
a = A()
print(a.unknown)
Function & Method
다음과 같이 Function을 정의해보겠습니다.
def add(a, b):
return a + b
def 문이 실행되면 Python에는 Function Object가 생성되고 add라는 이름이 해당 객체에 Binding됩니다.
print(type(add))
# <class 'function'>
Function Object에는 대표적으로 다음과 같은 정보가 있습니다.
- __name__
- __globals__
- __defaults__
- __closure__
- __code__
여기서 앞선 Execution Model에서 살펴본 Code Object가 다시 등장합니다.
print(add.__code__)
Function Object와 Code Object는 서로 다른 객체입니다.

Code Object가 "어떤 코드를 실행할 것인가" 를 나타낸다면,
Function Object는 Code Object와 함께 "그 코드를 어떤 함수 환경에서 실행할 것인가" 에 대한 정보를 가지고 있습니다.
Python 공식 Data Model에서도 사용자 정의 Function Object의 __code__가 해당 함수의 compiled body를 나타내는 Code Object라고 정의합니다.
Class 내부의 Function
이번에는 Function을 Class 안에 정의해보겠습니다.
class A:
def foo(self):
return 10
Class Body가 실행되는 동안 foo라는 Function Object가 만들어지고 Class Namespace에 Binding됩니다.

실제로 namespace를 확인하면 Function Object를 볼 수 있습니다.
print(A.__dict__["foo"])
중요한 점은 Class 내부에 def를 작성했다고 해서 처음부터 특별한 Method Object가 저장되는 것은 아니라는 것입니다.
우선 Class Namespace에는 Function Object가 들어가고, 그 Function이 Instance를 통해 조회될 때 Method로 Binding됩니다.
Descriptor
앞에서 다음 두 표현이 서로 다른 결과를 만든다고 했습니다.
# Class를 통해 조회
A.foo
# Instance를 통해 조회
a.foo
왜 Class를 통해 조회하면 Function이고 Instance를 통해 조회하면 Method가 될까요?
이 동작의 기반이 Descriptor Protocol입니다.
Descriptor는 다음 Special Method 중 하나 이상을 정의한 객체입니다.
__get__()
__set__()
__delete__()
Python 공식 Descriptor Guide에서는 Descriptor를 Attribute Lookup의 동작을 제어할 수 있는 객체라고 설명합니다.
Function Descriptor
Python의 Function Object 자체가 Descriptor입니다.
Function은 __get__() 동작을 제공하며 Attribute가 어떤 방식으로 조회됐는지에 따라 다른 값을 반환합니다.
개념적으로 다음과 비슷합니다.
def __get__(self, obj, objtype=None):
if obj is None:
return self
return MethodType(self, obj)
Class를 통해 A.foo 를 조회하면 Instance가 없으므로 Function Object 자체를 반환합니다.

반면 a.foo 처럼 Instance를 통해 조회하면 Bound Method가 만들어집니다.

공식 Descriptor Guide에서도 Function의 __get__()이 Instance와 Function을 연결하여 Method를 만드는 메커니즘을 설명합니다.
Data Descriptor
Descriptor는 크게 두 종류로 나눌 수 있습니다.
- Data Descriptor
- __set__ 또는 __delete__ 정의
- Non-data Decriptor
- __get__ 만 정의
일반적인 Python Function은 Non-data Descriptor입니다.
대표적인 Data Descriptor는 property가 있습니다.
class User:
@property
def name(self):
return "Python"
property는 단순히 Function을 호출하는 것이 아니라 Attribute Access 자체를 제어합니다.
Descriptor가 중요한 이유는 Method뿐만 아니라 Python의 많은 기능이 이 Protocol 위에 만들어져 있기 때문입니다.
대표적으로
- Function Method Binding
- classmethod
- staticmethod
- property
- __slots__
등이 Descriptor를 활용합니다.
Method Binding
이제 다음 코드를 다시 보겠습니다.
class A:
def foo(self):
return 10
a = A()
다음 두 표현의 결과는 다릅니다.
print(A.foo)
# <function A.foo at 0x105500670>
print(a.foo)
# <bound method A.foo of <A object at 0x10559f770>>
a.foo에서 만들어진 객체가 Bound Method입니다.
Bound Method
Bound Method는 개념적으로 Function Object와 Binding된 Instance를 가지고 있습니다.
실제로 다음과 같이 확인할 수 있습니다.
method = a.foo
print(method.__func__) # 원래 Function Object A.foo
print(method.__self__) # Binding된 Instance a
공식 Data Model에서도 Bound Method의 __func__와 __self__를 각각 원래 Function과 Binding된 객체로 정의합니다.
Method 호출
이제 a.foo() 가 왜 self를 자동으로 받는지도 설명할 수 있습니다.

self라는 이름 자체는 Python Keyword가 아니기 때문에 다음과 같이 정의해도 문법적으로 가능합니다.
class A:
def foo(x):
print(x)
다만 Instance Method의 첫 번째 Parameter를 self라고 작성하는 것이 Python의 강한 관례입니다.
Instance Method
일반적인 Method는 Instance를 Binding합니다.
class A:
def foo(self, x):
...

Class Method
classmethod는 Instance 대신 Class를 Binding합니다.
class A:
@classmethod
def foo(cls):
print(cls)
다음 두 호출 모두 가능합니다.
A.foo()
a = A()
a.foo()
두 경우 모두 Binding되는 객체는 A입니다.

Descriptor Guide에서도 classmethod가 호출 전에 Class Reference를 Argument 앞에 추가한다고 설명합니다.
Static Method
staticmethod는 어떠한 객체도 자동으로 Binding하지 않습니다.
class A:
@staticmethod
def foo(x):
return x * 2
A.foo(10)
a.foo(10)
둘 모두 underlying Function을 그대로 호출합니다.
위에서 알아본 세 메서드의 차이는 호출 시 어떤 객체가 자동으로 Binding되는지와 어떤 상태를 다루는지에 있습니다.
| Method | 자동 Binding | 사용 목적 |
| Instance Method | Instance (self) | 특정 인스턴스의 상태 조회·변경 |
| Class Method | Class (cls) | 클래스 자체가 필요한 로직, 대체 생성자 |
| Static Method | 없음 | 클래스와 관련 있지만 Instance/Class 상태가 필요 없는 로직 |
Class & Instance
Python에서는 Class 역시 Object입니다.
class MyClass:
pass
obj = MyClass()
관계는 다음과 같습니다.

type(obj) is MyClass
# True
type(MyClass) is type
# True
즉, "MyClass는 obj의 Class이며, type은 MyClass의 Class입니다."
Class의 Class를 Metaclass라고 부릅니다.
따라서 일반적인 Python Class의 기본 Metaclass는 type입니다.
Class 생성
다음 Class Definition이 실행된다고 하겠습니다.
class MyClass:
x = 10
def foo(self):
pass
Python은 Class Body를 실행하기 위한 Namespace를 만들고 그 안에 이름을 Binding합니다.

일반적인 경우 이 Class를 만드는 Metaclass가 type입니다.

공식 Execution Model에서도 Class Definition의 Namespace가 최종적으로 Class Attribute Dictionary가 된다고 설명합니다.
Instance 생성
Class Object가 만들어진 뒤 다음처럼 Class를 호출하면 Instance가 생성됩니다.
obj = MyClass()
Python 언어 수준에서는 일반적으로 다음 Special Method와 연결됩니다.

__new__()는 새로운 Instance를 생성하여 반환하고, __init__()은 이미 생성된 Instance를 추가로 초기화합니다.
int, str, tuple과 같은 Immutable Type은 객체를 만든 이후 Value를 변경할 수 없기 때문에 이러한 타입은 일반적으로 실제 Value 설정을 __init__()보다 객체 생성 단계인 __new__()에서 처리합니다.
Custom Metaclass
Metaclass 역시 직접 정의할 수 있습니다.
class MyMeta(type):
pass
class MyClass(metaclass=MyMeta):
pass
type(MyClass) is MyMeta
# True

Metaclass는 Class 생성 과정 자체를 변경해야 하는 Framework나 Library에서 사용할 수 있지만 일반적인 Python 프로그램에서 직접 정의할 일은 많지 않습니다.
Special Method
Python에는 다음과 같은 특별한 이름의 Method가 있습니다.
- __getitem__
- __len__
- __add__
- __iter__
- __call__
- __new__
- __init__
이들을 Special Method 또는 흔히 Dunder Method라고 부릅니다.
dunder는 Double Underscore의 줄임말입니다.
이 이름들과 Python Syntax의 연결은 Python 언어가 미리 정해 놓은 Protocol입니다.
| Python 문법 | Special Method |
| x[key] | __getitem__ |
| x[key] = value | __setitem__ |
| del x[key] | __delitem__ |
| len(x) | __len__ |
| x + y | __add__ |
| x == y | __eq__ |
| iter(x) | __iter__ |
| x() | __call__ |
Python 공식 Data Model은 Class가 이러한 Special Method를 구현함으로써 Arithmetic, Subscription, Iteration 등 Python의 다양한 Syntax에 참여할 수 있도록 정의합니다.
Subscription
예를 들어 d[key]는 Python의 Subscription Protocol입니다.
d[key]

list와 dict가 동일한 문법을 사용하지만 실제 의미는 Type마다 다릅니다.
- list[index] : Index를 이용해 Element 조회
- dict[key] : Key의 Hash를 이용해 Value 조회
동일한 Python 문법에 서로 다른 객체가 자신만의 구현을 제공하는 것입니다.
Callable
__call__()을 구현하면 일반 객체도 함수처럼 호출할 수 있습니다.
class A:
def __call__(self):
print("hello")
a = A()
a()
여기에서 a() 는 새로운 A Instance를 생성하는 것이 아닌 이미 존재하는 a 객체에 대해 Call Protocol을 수행합니다.

Special Method Lookup
Special Method에는 일반 Attribute와 다른 중요한 규칙이 있습니다.
다음과 같이 Instance에 직접 __len__을 넣어보겠습니다.
class A:
pass
a = A()
a.__len__ = lambda: 10
이 경우 직접 a.__len__() 이라고 호출하는 것은 가능할 수 있지만 len(a) 는 정상적으로 동작하지 않습니다.
왜냐하면 Special Method의 암시적 호출은 일반적으로 Instance Namespace를 거치지 않고 Type에서 직접 조회하기 때문입니다.

class A:
def __len__(self):
return 10
Python 공식 Data Model에서도 implicit special method lookup이 Instance Attribute를 우회하고 Type을 기준으로 이루어진다고 설명합니다.
Python Protocol과 CPython Slot
지금까지 살펴본 내용은 Python 언어의 Data Model입니다.
이 단계에서는 CPython의 C 구조를 알 필요가 없지만 이후 Object Internals에서 CPython 구현으로 내려가면 일부 Special Method는 PyTypeObject의 Slot과 연결됩니다.
예를 들면 다음과 같은 것들이 있습니다.

Equality
Object Model에서 살펴본 것처럼 ==는 두 Object의 Identity를 비교하는 연산이 아닙니다.
Python에서는 Type이 두 Object를 어떤 기준으로 동등하다고 볼 것인지 정의할 수 있으며, 이 과정에 __eq__ Special Method가 사용됩니다.
class User:
def __init__(self, user_id):
self.user_id = user_id
def __eq__(self, other):
return self.user_id == other.user_id
두 Object의 Identity는 다르지만 User는 user_id가 같으면 동등한 것으로 정의했습니다.
이제 서로 다른 두 Object를 생성해 비교해보겠습니다.
a = User(10)
b = User(10)
print(a is b) # False
print(a == b) # True
개념적으로 다음과 같이 이해할 수 있습니다.

다만 a == b가 단순히 a.__eq__(b)라는 Method Call로 치환되는 것은 아닙니다.
Python의 Rich Comparison Protocol은 Operand의 Type과 상속 관계를 고려해 적절한 비교 구현을 선택하며 비교를 처리할 수 없는 경우 NotImplemented를 반환하여 상대 Operand의 비교 구현을 시도할 수도 있습니다.
여기서 is와 ==의 중요한 차이는 ==의 의미는 __eq__를 통해 Type이 정의할 수 있지만,
is의 Identity 비교 자체는 사용자 코드에서 재정의할 수 없습니다.
마치며
Python의 Data Model은 객체가 Attribute Lookup, Method 호출 그리고 Python Syntax에 어떻게 참여하는지를 정의합니다.
Class에 저장된 Function은 Descriptor Protocol을 통해 Instance와 Binding되어 Bound Method가 되고 classmethod와 staticmethod는 이 Binding 방식을 변경합니다.
또한 __getitem__, __len__, __call__과 같은 Special Method를 통해 객체는 Python의 여러 문법에 자신의 동작을 제공할 수 있습니다.
이러한 구조를 이해하면 이후 CPython의 PyTypeObject, tp_call, tp_new와 같은 내부 구현을 Python 언어 수준의 동작과 자연스럽게 연결할 수 있습니다.
참고 문서
'Lang > Python' 카테고리의 다른 글
| [Python] (6) Object Internals (0) | 2026.09.26 |
|---|---|
| [Python] (5) Memory Management (0) | 2026.09.26 |
| [Python] (3) Execution Model (0) | 2026.09.22 |
| [Python] (2) Object Model (0) | 2026.09.21 |
| [Python] (1) GIL (0) | 2026.09.21 |