Python 강좌 : 제 37강 - 속성(Attribute)
속성(Attribute)
속성(Attribute)은 클래스 내부에 포함돼 있는 메서드나 변수를 의미합니다.
객체.이름처럼 마침표(.) 뒤에 작성하는 이름은 모두 속성입니다. instance.value의 value, "abc".upper의 upper, math.pi의 pi가 모두 속성이며, 메서드도 호출할 수 있는 값을 가진 속성입니다.
Python에서 속성(Attribute)은 크게 클래스 속성과 인스턴스 속성으로 나뉩니다.
클래스 속성은 클래스 내부의 메서드 단계와 동일한 영역에 위치한 변수를 의미합니다.
클래스 속성은 클래스에 하나만 존재하므로, 클래스 속성을 변경하면 해당 클래스의 모든 인스턴스에 동일하게 영향을 미칩니다.
인스턴스 속성은 self를 통해 할당된 인스턴스만의 변수를 의미합니다.
주로, __init__이나 메서드 내부에서 할당된 변수를 의미합니다.
이번 강좌에서는 두 속성의 차이와 Python이 속성을 찾는 순서를 먼저 살펴본 다음, 속성을 읽고·쓰고·지울 때 호출되는 매직 메서드와 getattr() 같은 내장 함수를 다룹니다.
- Tip :
self는 자기자신을 의미합니다. 즉,인스턴스를 지칭합니다.
클래스 속성과 인스턴스 속성 정의
class 클래스명:
클래스 속성 = 값
def __init__(self, *args, **kwargs):
self.인스턴스 속성 = 값
...클래스 속성
클래스 속성은 메서드와 동일 단계에 작성하게 됩니다.
self를 사용하지 않고 정의합니다. 그러므로 모든 인스턴스에 동일하게 영향을 미칩니다.
인스턴스가 생성될 때 초기화되지 않으므로, 클래스명.클래스 속성으로 참조가 가능합니다.
클래스 속성은 class 문이 실행될 때 한 번만 만들어져 클래스 자체에 저장됩니다. 그러므로 모든 인스턴스가 공유해야 하는 상수나 기본값, 생성된 인스턴스의 개수 같은 값을 저장할 때 사용합니다.
인스턴스 속성
인스턴스 속성은 인스턴스를 초기화할 때 생성됩니다.
self를 사용해 정의합니다. 그러므로 인스턴스 내에서만 영향을 미칩니다.
인스턴스가 생성될 때나 생성된 후 할당되므로, 인스턴스명.인스턴스 속성으로 참조가 가능합니다.
인스턴스 속성은 인스턴스마다 따로 가지는 __dict__ 사전에 저장됩니다. 그러므로 이름이나 좌표처럼 인스턴스마다 값이 달라야 하는 데이터를 저장할 때 사용합니다.
class Daeheeyun:
class_value = 0
def __init__(self):
self.instance_value = 0
def set_class_value(self):
Daeheeyun.class_value = 10
def set_instance_value(self):
self.class_value = 20
instance1 = Daeheeyun()
instance2 = Daeheeyun()
print("--클래스 속성 변경--")
instance1.set_class_value()
print(instance1.class_value, instance2.class_value)
print("--인스턴스 속성 변경--")
instance1.set_instance_value()
print(instance1.class_value, instance2.class_value)
print("--속성(Attribute) 출력--")
print(instance1.__dict__)
print(instance2.__dict__)- 결과
- –클래스 속성 변경–
10 10
–인스턴스 속성 변경–
20 10
–속성(Attribute) 출력–
{‘instance_value’: 0, ‘class_value’: 20}
{‘instance_value’: 0}
Daeheeyun 클래스
instance1과 instance2를 생성해 instance1에 대해서만 작업을 진행합니다.
set_class_value 메서드는 클래스 속성을 변경하는 메서드입니다.
set_instance_value 메서드는 인스턴스 속성을 변경하는 메서드입니다.
set_class_value 메서드
instance1에 클래스 값을 변경하면, Daeheeyun에 대한 동일한 속성이 일괄 변경됩니다.
즉, instance2을 변경하지 않아도 클래스 자체의 값이 변경되어 instance2의 값도 변경됩니다.
Daeheeyun.class_value = 10은 클래스 이름을 통해 할당하므로, 어느 인스턴스에서 호출하더라도 클래스에 저장된 하나의 값이 바뀝니다. 그러므로 첫 번째 출력은 10 10입니다.
set_instance_value 메서드
instance1에 인스턴스 값을 변경하면, instance1에 대한 속성만 변경됩니다.
self.class_value = 20은 클래스 속성을 변경하지 않고, instance1에 같은 이름의 인스턴스 속성을 새로 만듭니다. 즉, instance2에는 영향을 미치지 않습니다.
속성을 찾을 때는 인스턴스 속성을 먼저 찾고, 없으면 클래스 속성을 찾습니다. 그러므로 instance1.class_value는 새로 만든 인스턴스 속성 20을 반환합니다.
instance2에는 인스턴스 속성 class_value가 없으므로, 클래스 속성 10을 그대로 반환합니다. 그러므로 두 번째 출력은 20 10입니다.
- Tip : 인스턴스를 통해 클래스 속성을 변경하려면
type(self).class_value = 값이나클래스명.class_value = 값처럼 클래스를 통해 할당해야 합니다.
__dict__ 속성
__dict__는 현재 인스턴스에 할당된 인스턴스 속성만 출력합니다.
instance1의 인스턴스 속성은 instance_value와 인스턴스화 된 class_value입니다.
instance2의 인스턴스 속성은 instance_value입니다.
instance2에서 set_instance_value 메서드를 통해 class_value를 인스턴스화 하지 않아, 클래스 속성이 유지되기 때문입니다.
instance1의 __dict__에 class_value가 있다는 것은, 클래스 속성 class_value를 가리는(shadowing) 인스턴스 속성이 생겼다는 의미입니다. del instance1.class_value로 인스턴스 속성을 제거하면 다시 클래스 속성이 보이게 됩니다.
가변 클래스 속성 주의
class Cart:
items = []
def add(self, item):
self.items.append(item)
a = Cart()
b = Cart()
a.add("사과")
print(b.items)
print(a.__dict__, Cart.items)- 결과
- [‘사과’]
{} [‘사과’]
클래스 속성에 리스트나 사전 같은 가변 객체를 할당하면, 모든 인스턴스가 같은 객체를 공유하게 됩니다.
self.items.append(item)은 self.items = ...처럼 할당하는 것이 아니라, 찾아온 리스트를 수정하는 것입니다. 할당이 아니므로 인스턴스 속성이 새로 만들어지지 않고, 클래스 속성인 리스트에 "사과"가 추가됩니다.
그러므로 a에만 추가했지만 b.items에도 "사과"가 보이며, a.__dict__는 비어 있습니다.
인스턴스마다 따로 가져야 하는 리스트나 사전은 반드시 __init__에서 self.items = []처럼 인스턴스 속성으로 만들어야 합니다.
속성 조회 순서
instance.name처럼 속성을 읽을 때, Python은 정해진 순서대로 여러 장소를 차례로 확인하고 가장 먼저 찾은 값을 반환합니다.
앞의 예제에서 instance1.class_value가 클래스 속성 대신 인스턴스 속성을 반환한 것도 이 순서 때문입니다.
- 인스턴스의
__dict__:self.이름 = 값으로 할당된 인스턴스 속성을 먼저 확인합니다. - 클래스의
__dict__: 인스턴스에 없다면, 인스턴스를 만든 클래스에 정의된 클래스 속성과 메서드를 확인합니다. - 부모 클래스의
__dict__: 클래스에도 없다면, 상속받은 부모 클래스를__mro__의 순서대로 하나씩 확인합니다. 마지막은 모든 클래스의 부모인object입니다. __getattr__: 모든 곳에서 찾지 못하면AttributeError가 발생합니다. 단, 클래스에__getattr__이 정의되어 있다면 오류 대신__getattr__이 호출됩니다.
class Parent:
kind = "부모 클래스 속성"
level = "부모 클래스에만 있는 속성"
class Child(Parent):
kind = "자식 클래스 속성"
def __getattr__(self, name):
return f"__getattr__ 호출 : {name}"
obj = Child()
obj.kind = "인스턴스 속성"
print(obj.kind)
del obj.kind
print(obj.kind)
print(obj.level)
print(obj.nothing)
print(Child.__mro__)- 결과
- 인스턴스 속성
자식 클래스 속성
부모 클래스에만 있는 속성
__getattr__ 호출 : nothing
(<class ‘__main__.Child’>, <class ‘__main__.Parent’>, <class ‘object’>)
결과를 한 줄씩 읽으면 조회 순서를 그대로 확인할 수 있습니다.
obj.kind는 인스턴스의__dict__에kind가 있으므로 1단계에서인스턴스 속성을 반환합니다.Child와Parent에도kind가 있지만 확인하지 않습니다.del obj.kind로 인스턴스 속성을 제거하면, 1단계에서 찾지 못하고 2단계의Child클래스에서자식 클래스 속성을 찾습니다.obj.level은 인스턴스와Child에 모두 없으므로, 3단계의Parent클래스에서 찾습니다.obj.nothing은 어디에도 없으므로, 4단계의__getattr__이 호출됩니다.
__mro__는 메서드 결정 순서(Method Resolution Order)로, 2·3단계에서 클래스를 확인하는 순서를 튜플로 보여줍니다.
- Tip : 정확하게는 1단계보다 앞서 클래스에
property같은데이터 디스크립터(Data Descriptor)가 있는지 먼저 확인합니다. 그러므로@property로 정의한 속성은 인스턴스__dict__에 같은 이름의 값이 있어도property가 우선합니다.property는 제 38강에서 다룹니다.
class Daeheeyun:
@property
def value(self):
return "property 값"
instance = Daeheeyun()
instance.__dict__["value"] = "인스턴스 값"
print(instance.__dict__)
print(instance.value)- 결과
- {‘value’: ‘인스턴스 값’}
property 값
인스턴스의 __dict__에 value를 직접 넣었지만, instance.value는 클래스의 property를 반환합니다.
이 규칙 덕분에 property는 인스턴스가 같은 이름의 속성을 가지더라도 항상 값을 읽고 쓰는 과정을 제어할 수 있습니다.
속성 관리 함수
속성 이름이 실행 중에 문자열로 정해지는 경우에는 instance.value처럼 마침표로 작성할 수 없습니다.
이때 getattr(), setattr(), hasattr(), delattr() 내장 함수를 사용하면, 문자열로 된 이름을 이용해 속성을 다룰 수 있습니다.
이 함수들은 마침표 문법과 똑같은 매직 메서드를 호출하므로, 뒤에서 다루는 매직 메서드를 재정의하면 함수에도 동일하게 적용됩니다.
class Daeheeyun:
def __init__(self):
self.value = 0
instance = Daeheeyun()
print(getattr(instance, "value"))
print(getattr(instance, "nothing", "기본값"))
setattr(instance, "site", "076923")
print(hasattr(instance, "site"), hasattr(instance, "nothing"))
delattr(instance, "site")
print(vars(instance))
name = "value"
print(getattr(instance, name) == instance.value)- 결과
- 0
기본값
True False
{‘value’: 0}
True
getattr(instance, "value")는 instance.value와 같으며, 세 번째 인자로 기본값을 전달하면 속성이 없을 때 오류 대신 기본값을 반환합니다.
setattr(instance, "site", "076923")은 instance.site = "076923"과 같습니다. 이어서 hasattr()로 확인하면 site는 True, 없는 nothing은 False입니다.
delattr(instance, "site")는 del instance.site와 같으며, vars(instance)는 instance.__dict__를 반환하므로 site가 제거된 것을 확인할 수 있습니다.
마지막 줄처럼 속성 이름을 변수에 저장해 두고 사용할 수 있다는 점이 마침표 문법과의 차이입니다.
| 함수 | 같은 문법 | 호출하는 매직 메서드 |
|---|---|---|
getattr(obj, name[, default]) |
obj.name |
__getattribute__ → 실패 시 __getattr__ |
setattr(obj, name, value) |
obj.name = value |
__setattr__ |
delattr(obj, name) |
del obj.name |
__delattr__ |
hasattr(obj, name) |
없음 | getattr()을 호출해 AttributeError 여부로 판단 |
vars(obj) |
obj.__dict__ |
없음 |
dir(obj) |
없음 | __dir__ |
매직 메서드(Magic Method)
클래스의 속성을 사용할 때, 속성을 관리하는 메서드를 통해 속성이 정의되거나 할당됩니다.
이 메서드들을 통해 속성을 관리할 수 있습니다. 속성을 관리하는 매직 메서드를 재정의해 속성을 관리합니다.
instance.value를 읽으면 __getattribute__가, instance.value = 값으로 할당하면 __setattr__이, del instance.value로 제거하면 __delattr__이 호출됩니다. 앞에서 살펴본 속성 조회 순서는 바로 object의 기본 __getattribute__가 처리하는 과정입니다.
이 메서드들을 재정의하면 속성에 접근할 때마다 로그를 남기거나, 값을 검사하거나, 없는 속성에 기본값을 제공하는 등의 작업을 추가할 수 있습니다.
__getattr__ : 존재하지 않는 속성 호출
class Daeheeyun:
def __init__(self):
self.value = 0
def __getattr__(self, name):
return name + "은 존재하지 않습니다."
instance = Daeheeyun()
print(instance.value)
print(instance.nothing)- 결과
- 0
nothing은 존재하지 않습니다.
존재하지 않는 속성을 참조한다면, 일반적으로 AttributeError가 발생합니다.
하지만, __getattr__ 메서드를 정의한다면 존재하지 않는 속성을 호출할 때, __getattr__ 메서드가 실행됩니다.
이를 통해 오류를 발생시키지 않고, 속성을 새로 정의하거나 무시할 수 있습니다.
instance.value는 인스턴스의 __dict__에서 찾을 수 있으므로 __getattr__이 호출되지 않고 0을 반환합니다. instance.nothing은 속성 조회의 마지막 단계까지 찾지 못했으므로 __getattr__(self, "nothing")이 호출됩니다.
__getattr__ 메서드 내부에서도 self.value처럼 인스턴스의 다른 속성에 접근할 수 있습니다.
단, __getattr__ 내부에서 존재하지 않는 속성에 접근하면 다시 __getattr__가 호출되어 무한 재귀에 빠질 수 있으므로 주의해야 합니다.
처리할 수 없는 이름이라면 raise AttributeError(name)으로 오류를 발생시키는 것이 좋습니다. 그래야 hasattr() 같은 함수가 올바르게 동작합니다.
class Daeheeyun:
def __getattr__(self, name):
return None
instance = Daeheeyun()
print(hasattr(instance, "anything"))
class Fixed:
def __getattr__(self, name):
if name.startswith("default_"):
return 0
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
fixed = Fixed()
print(fixed.default_size)
print(hasattr(fixed, "anything"))- 결과
- True
0
False
Daeheeyun의 __getattr__은 어떤 이름이든 None을 반환하므로, hasattr()은 존재하지 않는 anything도 있다고 판단해 True를 반환합니다. 오타가 난 속성 이름도 오류 없이 None이 되므로, 실수를 발견하기 어려워집니다.
Fixed는 default_로 시작하는 이름만 처리하고, 나머지는 AttributeError를 발생시킵니다. 그러므로 fixed.default_size는 0을 반환하고, hasattr(fixed, "anything")은 올바르게 False를 반환합니다.
__getattribute__ : 속성 호출
class Daeheeyun:
def __init__(self):
self.value = 0
def __getattribute__(self, name):
try:
return super().__getattribute__(name)
except AttributeError:
value = "Empty"
setattr(self, name, value)
return value
instance = Daeheeyun()
print(instance.value)
print(instance.nothing)- 결과
- 0
Empty
속성을 호출한다면, __getattribute__ 메서드가 실행됩니다.
어떠한 속성이나 메서드를 호출한다면 __getattribute__ 메서드를 거치게 됩니다.
__getattr__ 메서드는 존재하지 않는 속성만 호출되지만, __getattribute__는 모든 속성이 호출됩니다.
존재하지 않는 속성을 호출했다면, super().__getattribute__(name)에서 AttributeError가 발생합니다.
예제는 이 오류를 처리해 "Empty"라는 값을 속성으로 할당하고 반환합니다.
setattr()로 할당했으므로, 다음에 instance.nothing에 접근할 때는 super().__getattribute__(name)이 인스턴스 __dict__에서 "Empty"를 바로 찾습니다.
__getattribute__ 내부에서 self.value처럼 속성에 접근하면 다시 __getattribute__가 호출되어 무한 재귀에 빠지므로, 반드시 super().__getattribute__(name)이나 object.__getattribute__(self, name)으로 접근합니다.
class Daeheeyun:
def __init__(self):
self.value = 0
def __getattribute__(self, name):
print("__getattribute__ :", name)
return super().__getattribute__(name)
def __getattr__(self, name):
print("__getattr__ :", name)
return None
instance = Daeheeyun()
print(instance.value)
print(instance.nothing)- 결과
- __getattribute__ : value
0
__getattribute__ : nothing
__getattr__ : nothing
None
__getattribute__와 __getattr__를 함께 정의하면, 모든 속성 접근은 먼저 __getattribute__를 거칩니다.
__getattribute__에서 AttributeError가 발생한 경우에만 __getattr__가 이어서 호출됩니다.
instance.value는 __getattribute__에서 값을 찾았으므로 한 줄만 출력되고, instance.nothing은 __getattribute__가 AttributeError를 발생시킨 뒤 __getattr__이 호출되어 두 줄이 출력됩니다.
__getattribute__는 메서드 호출을 포함한 모든 속성 접근에 끼어들기 때문에, 잘못 작성하면 클래스 전체가 동작하지 않거나 느려질 수 있습니다. 없는 속성만 처리하면 되는 경우가 대부분이므로, 일반적으로는 __getattr__을 사용하고 __getattribute__는 모든 접근을 기록해야 하는 경우처럼 꼭 필요할 때만 재정의합니다.
-
Tip :
getattr()함수를 통해__getattribute__를 호출할 수 있습니다.getattr(instance, "value")와 같이 사용하며,getattr(instance, "nothing", "기본값")처럼 속성이 없을 때 반환할 기본값을 지정할 수 있습니다. -
Tip :
setattr()함수는 속성을 정의하는 함수입니다.setattr(instance, name, value)와 같이 사용합니다.
__setattr__ : 속성 할당
class Daeheeyun:
def __init__(self):
self.value = 0
def __setattr__(self, name, value):
return super().__setattr__(name, value * 2)
instance = Daeheeyun()
instance.value = 10
instance.nothing = 30
print(instance.value)
print(instance.nothing)- 결과
- 20
60
속성을 변경하거나 할당한다면 __setattr__을 통해 값이 할당됩니다.
instance.value = 10과 setattr(instance, "value", 10)은 모두 __setattr__(self, "value", 10)을 호출합니다. 예제는 전달받은 값을 두 배로 저장하므로 20과 60이 출력됩니다.
__getattr__과 달리, __setattr__은 이미 있는 속성을 변경할 때와 새로운 속성을 만들 때 모두 호출됩니다.
주의사항으로는 __setattr__ 내부에서 속성을 변경하거나 할당한다면, 다시 __setattr__가 실행됩니다.
그러므로, 재귀 되어 무한루프에 빠지게 됩니다.
그러므로 __setattr__ 메서드 내부에서는 self.name = value처럼 직접 할당하지 않고, 예제처럼 super().__setattr__(name, value)를 호출해 할당합니다.
__init__의 self.value = 0도 __setattr__를 거치므로, 예제에서는 처음부터 0 * 2가 할당됩니다.
class Daeheeyun:
def __setattr__(self, name, value):
setattr(self, name, value * 2)
instance = Daeheeyun()
try:
instance.value = 10
except RecursionError:
print("RecursionError 발생")- 결과
- RecursionError 발생
setattr()도 __setattr__을 호출하므로, __setattr__ 안에서 setattr()을 사용하면 자기 자신을 끝없이 호출하게 됩니다.
Python은 호출이 너무 깊어지면 RecursionError를 발생시켜 실행을 멈춥니다. super().__setattr__() 대신 self.__dict__[name] = value처럼 인스턴스 사전에 직접 저장하는 방법도 재귀를 피할 수 있습니다.
class Config:
def __init__(self):
super().__setattr__("_locked", False)
self.host = "localhost"
self.port = 8080
self._locked = True
def __setattr__(self, name, value):
if getattr(self, "_locked", False):
raise AttributeError(f"{name} 속성은 변경할 수 없습니다.")
super().__setattr__(name, value)
config = Config()
print(config.host, config.port)
try:
config.port = 9000
except AttributeError as e:
print("AttributeError:", e)- 결과
- localhost 8080
AttributeError: port 속성은 변경할 수 없습니다.
__setattr__을 활용하면 초기화가 끝난 뒤 속성을 변경할 수 없는 객체를 만들 수 있습니다.
__init__에서 _locked를 False로 두고 host와 port를 할당한 다음, 마지막에 _locked를 True로 바꿉니다. 이후의 모든 할당은 __setattr__에서 _locked를 확인해 AttributeError를 발생시킵니다.
첫 줄은 super().__setattr__()으로 잠금 상태를 저장하는 _locked를 검사 없이 먼저 만들어 둡니다. getattr(self, "_locked", False)의 기본값 False는 _locked가 아직 없는 경우를 대비한 안전장치입니다.
__delattr__ : 속성 제거
class Daeheeyun:
def __init__(self):
self.value = 0
def __delattr__(self, name):
print("제거 :", name)
return super().__delattr__(name)
instance = Daeheeyun()
del instance.value- 결과
- 제거 : value
속성을 제거하면 __delattr__가 호출됩니다.
del이나, delattr() 함수를 통해 속성을 제거할 때 실행됩니다.
return super().__delattr__(name) 방식 이외에도, return 없이 del self.__dict__[name]처럼 속성을 제거할 수 있습니다.
super().__delattr__(name)을 호출하지 않으면 메시지만 출력되고 속성은 실제로 제거되지 않습니다. 이를 이용하면 특정 속성의 제거를 막을 수 있습니다.
- Tip :
delattr()함수는 속성을 제거하는 함수입니다.delattr(instance, "value")와 같이 사용합니다.
class Daeheeyun:
def __init__(self):
self.value = 0
def __delattr__(self, name):
if name == "value":
raise AttributeError("value 속성은 제거할 수 없습니다.")
super().__delattr__(name)
instance = Daeheeyun()
instance.temp = 1
del instance.temp
print(instance.__dict__)
try:
delattr(instance, "value")
except AttributeError as e:
print("AttributeError:", e)- 결과
- {‘value’: 0}
AttributeError: value 속성은 제거할 수 없습니다.
temp는 super().__delattr__()로 정상적으로 제거되어 __dict__에 value만 남습니다.
value는 __delattr__에서 AttributeError를 발생시키므로, delattr()로 제거하려 해도 제거되지 않습니다.
__slots__ : 속성 제한, __dir__ : 속성 보기
class Daeheeyun:
__slots__ = ["value"]
def __init__(self):
self.value = 0
def __dir__(self):
return sorted(super().__dir__(), key=str.upper)
instance = Daeheeyun()
print(instance.__slots__)
print(instance.__dir__())- 결과
- [‘value’]
[‘value’, ‘__class__’, ‘__delattr__’, ‘__dir__’, ‘__doc__’, ‘__eq__’, ‘__firstlineno__’, ‘__format__’, ‘__getattribute__’, ‘__getstate__’, ‘__ge__’, ‘__gt__’, ‘__hash__’, ‘__init_subclass__’, ‘__init__’, ‘__le__’, ‘__lt__’, ‘__module__’, ‘__new__’, ‘__ne__’, ‘__reduce_ex__’, ‘__reduce__’, ‘__repr__’, ‘__setattr__’, ‘__sizeof__’, ‘__slots__’, ‘__static_attributes__’, ‘__str__’, ‘__subclasshook__’]
__slots__ : 속성 제한
__slots__은 클래스 내부에서 사용가능한 속성의 이름을 제한합니다.
즉, 사용할 속성(변수)의 이름을 미리 정의합니다. 다른 이름을 가진 속성명은 할당할 수 없습니다.
__slots__으로 속성이 제한되면, __dict__를 생성하지 않습니다. 그러므로 인스턴스마다 사전을 만들지 않아 메모리를 절약할 수 있습니다.
__slots__은 매직 메서드가 아닌 클래스 속성이며, 리스트나 튜플로 속성 이름을 작성합니다. Python은 클래스를 만들 때 이 이름마다 값을 저장할 고정된 자리를 마련합니다.
class Daeheeyun:
__slots__ = ["value"]
def __init__(self):
self.value = 0
instance = Daeheeyun()
try:
instance.other = 1
except AttributeError as e:
print("AttributeError:", e)
print(hasattr(instance, "__dict__"))- 결과
- AttributeError: ‘Daeheeyun’ object has no attribute ‘other’ and no __dict__ for setting new attributes
False
__slots__에 없는 other 속성을 할당하면 AttributeError가 발생합니다.
hasattr(instance, "__dict__")가 False이므로, 인스턴스에 __dict__ 사전이 만들어지지 않은 것을 확인할 수 있습니다.
__slots__은 같은 클래스의 인스턴스를 수십만 개 이상 만들어 메모리가 중요한 경우나, 오타로 인해 엉뚱한 속성이 새로 만들어지는 실수를 막고 싶을 때 사용합니다.
class Daeheeyun:
__slots__ = ["value"]
value = 0- 결과
- ValueError: ‘value’ in __slots__ conflicts with class variable
__slots__에 작성한 이름과 같은 이름의 클래스 속성을 함께 정의하면, 클래스를 만드는 시점에 ValueError가 발생합니다.
__slots__의 이름은 이미 클래스에 자리가 마련되어 있으므로, 기본값이 필요하다면 클래스 속성이 아닌 __init__에서 할당해야 합니다.
class Base:
__slots__ = ["value"]
class Child(Base):
pass
child = Child()
child.other = 1
print(child.__dict__)- 결과
- {‘other’: 1}
__slots__은 상속될 때도 주의해야 합니다.
부모 클래스에 __slots__이 있더라도 자식 클래스에서 __slots__을 정의하지 않으면, 자식 클래스의 인스턴스에는 다시 __dict__가 만들어져 아무 속성이나 할당할 수 있게 됩니다.
자식 클래스에서도 속성을 제한하려면, 자식 클래스에 추가할 속성만 담은 __slots__을 다시 정의해야 합니다.
__dir__ : 속성 보기
__dir__은 클래스 내부에서 사용 가능한 모든 속성을 출력합니다.
dir(instance)를 통해서도 호출이 가능하며, dir()은 __dir__의 결과를 정렬된 리스트로 반환합니다.
예제는 key=str.upper로 대소문자를 구분하지 않고 정렬했으므로, value가 가장 앞에 출력됩니다.
__dict__가 인스턴스에 직접 저장된 속성만 보여주는 것과 달리, dir()은 클래스와 부모 클래스에서 물려받은 메서드까지 사용할 수 있는 이름 전체를 보여줍니다. 그러므로 처음 보는 객체에 어떤 메서드가 있는지 확인할 때 유용합니다.
__dir__을 재정의하면 __getattr__로 동적으로 제공하는 이름을 목록에 추가하는 등, dir()과 자동 완성 기능에 보여줄 이름을 조절할 수 있습니다.
- Tip : 출력되는 기본 속성은 Python 버전에 따라 다릅니다.
__getstate__는Python 3.11,__firstlineno__와__static_attributes__는Python 3.13에서 추가되었습니다. 특수 속성은 제 39강에서 자세히 다룹니다.
정리
| 이름 | 호출하는 문법/함수 | 반환값/특징 |
|---|---|---|
| 클래스 속성 | 클래스명.이름, 인스턴스.이름 |
모든 인스턴스가 공유 |
| 인스턴스 속성 | self.이름 = 값 |
인스턴스의 __dict__에 저장 |
__getattribute__(self, name) |
obj.name, getattr() |
모든 속성 읽기에 호출 |
__getattr__(self, name) |
__getattribute__가 AttributeError를 발생시킬 때 |
없는 속성만 처리, 못 하면 AttributeError |
__setattr__(self, name, value) |
obj.name = value, setattr() |
새 속성·기존 속성 모두, 내부에서는 super() 사용 |
__delattr__(self, name) |
del obj.name, delattr() |
제거를 막거나 기록 가능 |
__dir__(self) |
dir() |
이름 목록, dir()이 정렬 |
__slots__ |
클래스 속성 | __dict__ 없음, 속성 이름 제한 |
속성을 읽을 때는 인스턴스 __dict__ → 클래스 → 부모 클래스(__mro__ 순서) 순서로 찾고, 모두 실패하면 __getattr__이 호출되거나 AttributeError가 발생합니다.
속성 매직 메서드 내부에서는 self.이름으로 다시 접근하지 말고 super()의 메서드를 호출해 무한 재귀를 피해야 합니다.
댓글 남기기