앞선 글에서는 Python 클래스에서 사용하는 인스턴스 메서드, 클래스 메서드, 정적 메서드의 차이를 살펴봤습니다.
이번에는 조금 더 내부적인 내용을 알아보겠습니다.
Python에서 객체를 만들고 다음과 같이 속성을 추가하면:
person.name = "철수"
person.age = 20
이 값들은 객체 내부에서 어떻게 관리되고 있을까요?
이때 알아두면 좋은 것이 바로 **__dict__**입니다.
그리고 Python에서는 객체의 속성을 __dict__ 대신 미리 정해진 형태로 관리할 수 있도록 **__slots__**라는 기능도 제공합니다.
이번 글에서는 __dict__와 __slots__가 무엇인지, 둘 사이에는 어떤 차이가 있는지 살펴보겠습니다.
객체의 속성 확인하기
먼저 간단한 클래스를 만들어보겠습니다.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
그리고 객체를 하나 만들어봅니다.
person = Person("철수", 20)
이 객체가 어떤 속성을 가지고 있는지 확인하려면 __dict__를 사용할 수 있습니다.
print(person.__dict__)
결과:
{'name': '철수', 'age': 20}
객체에 저장되어 있는 name과 age가 딕셔너리 형태로 나타나는 것을 확인할 수 있습니다.
즉, __dict__를 이용하면 객체의 속성을 확인할 수 있습니다.
__dict__란?
__dict__는 Python 객체가 가지고 있는 속성 정보를 딕셔너리 형태로 확인할 수 있도록 제공되는 특별한 속성입니다.
예를 들어:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
다음 객체를 만들면:
person = Person("철수", 20)
개념적으로 다음과 같이 볼 수 있습니다.
person
│
└── __dict__
├── name → "철수"
└── age → 20
실제로 person.__dict__를 출력하면:
print(person.__dict__)
다음과 같은 결과를 확인할 수 있습니다.
{'name': '철수', 'age': 20}
__dict__를 직접 수정할 수도 있다
__dict__는 단순히 읽기만 할 수 있는 것이 아닙니다.
직접 값을 추가할 수도 있습니다.
person.__dict__["address"] = "서울"
그러면:
print(person.address)
결과:
서울
__dict__에 address가 추가되었기 때문입니다.
print(person.__dict__)
결과:
{'name': '철수', 'age': 20, 'address': '서울'}
이처럼 Python 객체는 기본적으로 새로운 속성을 동적으로 추가할 수 있습니다.
클래스에도 __dict__가 있다
__dict__는 객체에만 존재하는 것은 아닙니다.
클래스에서도 사용할 수 있습니다.
class Person:
species = "Human"
def introduce(self):
print("안녕하세요")
다음과 같이 확인할 수 있습니다.
print(Person.__dict__)
그러면 클래스에 정의된 여러 정보가 들어 있는 것을 확인할 수 있습니다.
예를 들어 다음과 같은 항목들이 포함될 수 있습니다.
species
introduce
__module__
__dict__
__weakref__
__doc__
...
즉,
Person.__dict__
은 클래스 자체의 속성 정보를 확인하는 데 사용할 수 있고,
person.__dict__
은 특정 인스턴스의 속성 정보를 확인하는 데 사용할 수 있습니다.
둘은 서로 다른 대상의 __dict__입니다.
__dict__의 장점
__dict__를 사용하는 방식은 Python의 객체를 유연하게 사용할 수 있도록 해줍니다.
예를 들어 처음에는 name과 age만 가지고 있던 객체에 새로운 속성을 추가할 수 있습니다.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
person = Person("철수", 20)
person.address = "서울"
person.email = "example@example.com"
처음 클래스에서 address나 email을 정의하지 않았더라도 새로운 속성을 추가할 수 있습니다.
print(person.__dict__)
결과:
{
'name': '철수',
'age': 20,
'address': '서울',
'email': 'example@example.com'
}
이런 유연성은 Python 객체의 중요한 특징 중 하나입니다.
하지만 반대로 이런 방식이 항상 좋은 것은 아닙니다.
객체가 많아지면 어떻게 될까?
객체 하나만 사용하는 경우에는 __dict__의 특성을 크게 신경 쓰지 않아도 됩니다.
하지만 같은 클래스로 매우 많은 객체를 만들어야 하는 상황이라면 이야기가 달라질 수 있습니다.
예를 들어:
people = []
for i in range(100000):
people.append(Person(f"사람{i}", i))
처럼 매우 많은 객체를 생성한다고 생각해봅시다.
각 객체가 자유롭게 속성을 관리할 수 있도록 __dict__를 사용하는 방식에는 객체의 속성 정보를 관리하기 위한 추가적인 메모리 사용이 발생할 수 있습니다.
이런 상황에서 고려할 수 있는 것이 __slots__입니다.
__slots__란?
__slots__는 클래스에서 사용할 인스턴스 속성을 미리 지정하는 기능입니다.
예를 들어 다음과 같이 작성할 수 있습니다.
class Person:
__slots__ = ("name", "age")
def __init__(self, name, age):
self.name = name
self.age = age
여기서는 name과 age를 사용할 속성으로 미리 지정했습니다.
person = Person("철수", 20)
다음과 같이 사용하는 것은 정상적으로 동작합니다.
print(person.name)
print(person.age)
결과:
철수
20
__slots__를 사용하면 새로운 속성을 추가할 수 없다
__slots__의 중요한 특징입니다.
다음 코드를 실행해보겠습니다.
person.address = "서울"
그러면 일반적인 __dict__ 기반 객체와 달리 새로운 속성을 추가할 수 없습니다.
대략 다음과 같은 오류가 발생합니다.
AttributeError
왜냐하면 __slots__에 address를 지정하지 않았기 때문입니다.
class Person:
__slots__ = ("name", "age")
이 클래스에서는 인스턴스가 사용할 수 있는 속성을 name과 age로 제한합니다.
필요하다면 처음부터 추가할 속성을 지정해야 합니다.
class Person:
__slots__ = ("name", "age", "address")
그러면 다음과 같이 사용할 수 있습니다.
person.address = "서울"
__slots__를 사용하면 __dict__가 없을 수 있다
다음 클래스를 살펴보겠습니다.
class Person:
__slots__ = ("name", "age")
객체를 만든 후:
person = Person()
다음 코드를 실행하면:
print(person.__dict__)
일반적인 객체처럼 __dict__를 사용할 수 없습니다.
__slots__를 지정하면 기본적으로 인스턴스의 __dict__가 생성되지 않기 때문입니다.
즉 일반적인 클래스와 비교하면 다음과 같습니다.
일반 클래스
객체
├── __dict__
│ ├── name
│ └── age
└── 기타 객체 정보
__slots__ 사용
객체
├── name
└── age
실제 내부 동작은 이것보다 더 복잡하지만, 기본적인 개념은 이렇게 이해하면 됩니다.
__dict__와 __slots__ 비교
두 방식을 비교하면 다음과 같습니다.
| 구분 | __dict__ | __slots__ |
|---|---|---|
| 속성 추가 | 자유롭게 가능 | 지정된 속성만 가능 |
| 동적 속성 추가 | 가능 | 기본적으로 불가능 |
__dict__ | 사용 가능 | 기본적으로 없음 |
| 메모리 사용 | 상대적으로 많을 수 있음 | 객체가 많을 때 줄이는 데 도움 |
| 유연성 | 높음 | 낮음 |
| 사용 방식 | 일반적인 클래스 | 속성이 고정된 객체가 많은 경우 고려 |
여기서 중요한 것은 __slots__가 무조건 더 좋은 것은 아니라는 점입니다.
__slots__가 메모리를 줄이는 이유
일반적인 Python 객체는 속성을 동적으로 관리할 수 있도록 __dict__를 사용합니다.
하지만 __slots__를 사용하면 이러한 속성 관리 방식을 제한하고 지정된 속성을 중심으로 관리할 수 있습니다.
따라서 동일한 형태의 객체를 아주 많이 생성하는 경우 객체마다 필요한 메모리를 줄이는 데 도움이 될 수 있습니다.
예를 들어 센서 데이터처럼 동일한 구조의 객체를 대량으로 생성해야 한다고 생각해보겠습니다.
class Sensor:
__slots__ = ("temperature", "pressure", "rpm")
def __init__(self, temperature, pressure, rpm):
self.temperature = temperature
self.pressure = pressure
self.rpm = rpm
이런 구조에서는 모든 Sensor 객체가 동일한 세 가지 속성을 사용합니다.
따라서 객체에 새로운 속성을 자유롭게 추가할 필요가 없다면 __slots__를 고려할 수 있습니다.
다만 실제 프로그램에서는 객체의 수, 속성의 수, Python 구현과 버전 등에 따라 메모리 사용량이 달라질 수 있으므로 항상 __slots__가 큰 차이를 만든다고 단정해서는 안 됩니다.
__slots__ 사용 시 주의할 점
__slots__는 편리하지만 몇 가지 주의할 부분이 있습니다.
1. 동적으로 속성을 추가할 수 없다
앞서 살펴본 것처럼 다음과 같은 코드는 동작하지 않습니다.
person.address = "서울"
필요한 속성이 있다면 __slots__에 미리 지정해야 합니다.
2. __dict__를 사용할 수 없다
__slots__를 사용하면 기본적으로 인스턴스의 __dict__가 생성되지 않습니다.
따라서 다음과 같은 코드에 의존하고 있었다면 문제가 발생할 수 있습니다.
person.__dict__
3. 모든 클래스에서 사용할 필요는 없다
객체가 몇 개 생성되지 않거나 속성을 자유롭게 추가해야 하는 프로그램이라면 일반적인 클래스가 더 편리할 수 있습니다.
즉 다음처럼 생각하는 것이 좋습니다.
속성이 자유롭게 변경될 필요가 있다
↓
일반적인 클래스
속성이 고정되어 있고
동일한 객체를 매우 많이 생성한다
↓
__slots__ 고려
__slots__에 __dict__를 직접 추가할 수도 있다
조금 더 알아두면 좋은 내용도 있습니다.
__slots__를 사용하면서도 동적으로 속성을 추가해야 하는 특별한 경우라면 __dict__를 슬롯에 직접 포함할 수 있습니다.
class Person:
__slots__ = ("name", "age", "__dict__")
def __init__(self, name, age):
self.name = name
self.age = age
이렇게 하면 __dict__를 사용할 수 있습니다.
person = Person("철수", 20)
person.address = "서울"
print(person.__dict__)
결과:
{'address': '서울'}
다만 이렇게 하면 __slots__를 사용하는 목적 중 하나인 속성 관리 제한이 약해질 수 있습니다.
따라서 특별한 이유가 없다면 무조건 추가하기보다는 정말 동적 속성이 필요한지 먼저 확인하는 것이 좋습니다.
__slots__와 상속
__slots__는 상속을 사용할 때도 주의해야 합니다.
예를 들어 부모 클래스에서:
class Person:
__slots__ = ("name",)
자식 클래스에서:
class Student(Person):
__slots__ = ("school",)
처럼 사용할 수 있습니다.
이 경우 Student 객체는 부모 클래스에서 정의한 name과 자식 클래스에서 정의한 school을 사용할 수 있습니다.
student = Student()
student.name = "철수"
student.school = "고등학교"
따라서 __slots__를 사용하는 클래스에서 상속을 적용할 때는 부모와 자식 클래스의 슬롯 구조도 함께 고려해야 합니다.
상속에 대해서는 뒤에서 별도의 글을 통해 자세히 살펴볼 예정입니다.
__dict__와 __slots__를 어떻게 선택할까?
처음부터 __slots__를 사용해야 하는 것은 아닙니다.
일반적인 Python 클래스에서는 __dict__를 사용하는 기본 동작을 그대로 활용하는 경우가 많습니다.
예를 들어:
class User:
def __init__(self, name):
self.name = name
정도로 충분한 상황이라면 굳이 __slots__를 사용할 필요가 없습니다.
반면 다음과 같은 조건이라면 __slots__를 고려할 수 있습니다.
- 동일한 구조의 객체가 매우 많이 생성된다.
- 사용할 속성이 명확하게 정해져 있다.
- 객체에 새로운 속성을 추가할 필요가 없다.
- 메모리 사용량을 줄이는 것이 중요하다.
결국 중요한 것은 __slots__가 더 좋은 문법이라는 것이 아니라 목적에 따라 선택하는 것입니다.
마무리
Python 클래스에서 객체의 속성을 관리하는 기본적인 방식에는 __dict__가 있습니다.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
객체의 속성을 확인하면:
person = Person("철수", 20)
print(person.__dict__)
다음과 같이 확인할 수 있습니다.
{'name': '철수', 'age': 20}
반면 __slots__를 사용하면 사용할 속성을 미리 지정할 수 있습니다.
class Person:
__slots__ = ("name", "age")
이 경우 지정되지 않은 속성을 동적으로 추가할 수 없고, 기본적으로 인스턴스의 __dict__도 사용할 수 없습니다.
핵심만 정리하면 다음과 같습니다.
__dict__는 객체의 속성을 딕셔너리 형태로 관리할 수 있게 해준다.- 일반적인 Python 클래스에서는
__dict__기반의 속성 관리가 사용된다. __dict__를 사용하면 객체에 새로운 속성을 동적으로 추가할 수 있다.__slots__는 사용할 인스턴스 속성을 미리 지정하는 기능이다.__slots__를 사용하면 동적 속성 추가가 제한된다.__slots__를 사용하면 기본적으로 인스턴스의__dict__가 생성되지 않는다.- 동일한 구조의 객체가 매우 많을 때 메모리 사용을 줄이는 데 도움이 될 수 있다.
- 그렇다고 모든 클래스에
__slots__를 사용하는 것이 좋은 것은 아니다.
다음 글에서는 클래스의 속성을 직접 노출하지 않고 property를 이용해 안전하게 읽고 변경하는 방법을 알아보겠습니다. @property와 setter를 이용하면 단순한 변수처럼 보이는 속성에 값 검증이나 추가 로직을 넣을 수 있습니다.