클래스에 의해 생성된 객체는 항상 유일한 객체를 유지하기 위해 별도의 식별 레퍼런스를 가진다. 같은 객체인지 아닌지 비교할 때 이 레퍼런스를 비교하면 된다!
클래스와 객체의 관계는?
우선 클래스가 설계도이고, 객체는 클래스라는 설계도로 생성된 대상을 뜻한다. 그래서 객체를 클래스의 인스턴스라고도 부른다. 이러한 클래스와 객체의 관계는 어떻게 되어 있을까?
우선 아무것도 하지 않는 클래스를 정의한다.
class Klass(object):
pass
object 클래스를 상속받는다고 표기해주었지만, 상속을 임의로 주지 않아도 object 클래스를 상속한다. 이에 대한 자세한 설명은 아래 링크를 참고하면 좋을 것 같다.
내장함수 issubclass로 상속 관계를 확인해보자. object와는 당연히 상속 관계이지만 파이썬 메타 클래스 type과는 상속 관계가 아니다. 이를 확인한 이유는 두 클래스가 파이썬에서 중요한 역할을 하기 때문이다.
class Klass(object):
pass
print(issubclass(Klass, object))
print(issubclass(Klass, type))
[결과]
True
False
메타 클래스 type이 중요한 이유는 type이 객체에 대한 자료형을 확인하는 용도에 그치지 않고 클래스를 생성하는 역할을 하기 때문이다. 실제로 isinstance 함수를 통해 확인하면 메타 클래스 type과 Klass가 생성 관계인 것을 알 수 있다. 다른 int, dict, object 클래스 등등이 모두 메타 클래스로 생성된다.
isinstance(Klass, type)
[결과]
True
물론 최상위 클래스 object는 모든 클래스가 상속하고, 이는 메타 클래스 type도 예외가 아니다.
isinstance(type,object)
[결과]
True
객체 내부 검사(object introspection)하기
클래스와 객체는 제각각 별도의 이름공간이 있다. 객체는 클래스 상속 관계에 있는 이름공간을 모두 참조해서 사용할 수 있다.
한글로 클래스 속성을 만들고, 객체에 한글로 인스턴스 속성을 정의한 객체를 만들어보자.
class Klass(object):
클래스속성 = "Klass"
def __init__(self, name):
self.인스턴스속성= name
k = Klass("달")
객체가 참조할 수 있는 이름공간은 객체를 만든 클래스와 이 클래스가 상속한 상위 클래스이다. dir함수로 클래스의 속성과 메소드를 출력해보자. object 클래스의 스페셜 속성이나 메소드들도 모두 출력되는 것을 볼 수 있다.
cnt = 0
for i in dir(k):
print(i, end = ", ")
cnt += 1
if cnt % 5 == 0:
print()
[결과]
__class__, __delattr__, __dict__, __dir__, __doc__,
__eq__, __format__, __ge__, __getattribute__, __gt__,
__hash__, __init__, __init_subclass__, __le__, __lt__,
__module__, __ne__, __new__, __reduce__, __reduce_ex__,
__repr__, __setattr__, __sizeof__, __str__, __subclasshook__,
__weakref__, 인스턴스속성, 클래스속성,
주석을 통해 하나하나 알아보자.
print(Klass.__dict__, end="\n\n") # 클래스의 이름공간
print(k.__dict__) # 객체 내의 이름공간
[결과]
mappingproxy({'__module__': '__main__',
'클래스속성': 'Klass',
'__init__': <function __main__.Klass.__init__(self, name)>,
'__dict__': <attribute '__dict__' of 'Klass' objects>,
'__weakref__': <attribute '__weakref__' of 'Klass' objects>,
'__doc__': None})
{'인스턴스속성': '달'}
object.__init__ # 최상위 클래스의 초기화 함수 조회
object().__init__ # 최상위 클래스로 객체를 생성한 후 초기화 메소드 조회
[결과]
<slot wrapper '__init__' of 'object' objects>
<method-wrapper '__init__' of object object at 0x0000021FB2251790>
초기화 함수는 클래스에 정의된 것은 함수이고 객체에서 접근하면 메소드라는 것을 알 수 있다!
객체는 항상 유일해야 한다
객체는 항상 유일해야 하며, 이를 유지하기 위해 레퍼런스를 가진다. 그러나 파이썬 내부적으로 유일성을 준수하지 않는 경우가 발생한다.
정수는 항상 유일한 객체를 만들고 같은 정수의 값은 항상 유일한 객체이다. 그러나 두 변수에 같은 값을 할당하고 레퍼런스를 비교해보면 같지 않다는 것을 알 수 있다.
x = 3000
y = 3000
print(x is y)
id(x), id(y)
[결과]
False
(2335184359408, 2335184356240)
이처럼 파이썬은 내부적으로 필요할 때마다 새로운 객체를 만들어서 처리한다. 정수, 실수, 문자열, 튜플 등은 변경 불가능해서 유일한 객체로 처리해야 하지만 내부적으로 새로운 객체를 만들 수 있으므로 유일성은 값으로 비교해야 한다.
x == y
[결과]
True
dataclass란?
클래스를 정의할 때는 초기화 함수 __init__을 정의해 객체의 속성을 만든다. 그러나 속성이 많아지면 모든 것을 작성하기 불편해 dataclass를 도입해 타입 힌트만 작성해도 객체의 속성을 자동으로 생성하는 방법을 사용한다.
만들어진 클래스 이름공간에 __init__이 있는지 확인하면 존재하는 것을 확인할 수 있다.
from dataclasses import dataclass
@dataclass
class Person:
name: str
age: int
Person.__dict__["__init__"]
[결과]
<function __main__.Person.__init__(self, name: str, age: int) -> None>
또한 이런 방식을 사용하면 객체를 생성할 때 임의로 새로운 속성을 추가할 수 있다. 이름공간이 공개되어 있기 때문이다.
p = Person("Park", 23)
p.sex = "male"
p.__dict__
[결과]
{'name': 'Park', 'age': 23, 'sex': 'male'}
dataclass에 더 자세히 알아보자. dataclass의 매개변수를 세팅하면 내부의 스페셜 메소드 생성을 조종할 수 있다.
- init : __init__ 생성
- repr : 객체 실행 환경에서 출력하는 __repr__ 생성
- eq : 객체 비교 생성
- order : 비교 연산에 관한 스페셜 메소드 생성
- unsafe_hash : __hash__ 생성
- frozen : __setattr__, __delattr__ 생성
클래스에 더 이상 객체 속성을 런타임에서 추가할 수 없도록 frozen=True로 지정하면 위처럼 객체 속성을 추가할 수 없다.
변경도 불가능하다!
참고
- 한권으로 개발자가 원하던 파이썬 심화 A to Z, 문용준/문성혁 저
'◎ Python > 파이썬 심화 (책)' 카테고리의 다른 글
[파이썬 심화] 16. 최상위 클래스 object (다시 공부 필요) (0) | 2022.09.14 |
---|---|
[파이썬 심화] 15. 특별한 내장 클래스 (0) | 2022.09.13 |
[파이썬 심화] 13. 클래스 정의하기 (1) | 2022.09.06 |
[파이썬 심화] 12. 함수에 여러 가지 종류의 매개변수를 설정할 수 있다! (1) | 2022.09.05 |
[파이썬 심화] 11. 함수 정의하기 (0) | 2022.09.02 |
자기계발 블로그