클래스 인스턴스를 메모리에 할당해 “생성” = Allocator, Instantiator
“생성”된 인스턴스를 “사용 준비 시킴” = Constructor, Initializer
__init__은 Constructor, __new__는 Allocator
__new__는 방어적 프로그래밍에 사용 가능
2019년 12월 18일, __init__()은 생성자?
class Foo:
def __init__(self, value_a, value_b):
self.a = value_a
self.b = value_b
foo = Foo(7, 9) # __init__ is called
print(foo.a , foo.b) # 7 , 9
__new__와 __init__의 차이
먼저, 이 포스팅에서는 클래스 객체(object)와 클래스 인스턴스(instance)를 구분없이 사용함을 알려드립니다. 또 글 처음부터 끝까지 원문에 달린 코멘트와 제가 이해한 내용을 바탕으로 종합해 정리한 내용이 주를 이룹니다.
오늘 포스팅은 C++, Java 등 여러 객체지향 언어를 다루다가 파이썬에 입문한 분들과 파이썬만 사용하시는 분들 모두를 위한 글입니다. 오늘은 생성(Construct)과 초기화(Initialize)의 개념을 우리가 어떻게 바라보아야 할 지에 관해 논하려고 합니다.
이는 그리 복잡하지는 않지만 다소 혼란스러울 수 있는 주제입니다. 객체 지향 프로그래밍 언어(java, python, C++, …) 사용자라면 생성자(Constructor)에 관해 들어본 적이 있을 것입니다. 그리고 파이썬 사용자로서 클래스를 다뤄본 사람이라면 __init__ 메소드는 클래스 “생성자 메소드”라는 것도 알고 있을 것입니다.
그런데 한 가지 재미있는 점은 바로 파이썬 클래스에서 __init__ 메소드는 클래스 오브젝트에 메모리를 할당하지 않는다라는 것입니다. 따라서 __init__은 클래스 인스턴스를 생성하지 않습니다. 즉, __init__은 생성자 메소드로 불리기에 애매하다는 점입니다.
즉, __init__ 메소드는 클래스 인스터스 형태인 객체(Object)가 생성(Created/Instantiated)되어 초기화(Initialized)되는 즉시 호출(Called)되기는 합니다만, 객체에 메모리를 할당하지 않는특수한 메소드라는 것입니다.
그러면 객체에 메모리를 할당(Allocate)하는 주인공은 누구냐? 바로 __new__ 메소드입니다. 파이썬에서 객체를 생성해보면 __init__이 실행되기 전에 항상 __new__가 먼저 실행되며, 이 때 객체에 메모리가 할당됩니다.
class Point():
def __new__(cls,*args,**kwargs):
print("From new")
print(cls)
print(args)
print(kwargs)
# create our object and return it
obj = super().__new__(cls)
return obj
def __init__(self, x = 0, y = 0):
print("From init")
self.x = x
self.y = y
>>> p2 = Point(3,4)
From new
<class '__main__.Point'>
(3, 4)
{}
From init
위 코드를 보시면 아시겠지만, __new__메소드는
- __init__보다 먼저 실행되며
- 클래스 자기 자신(cls)을 숨겨진 파라미터로 받으며
- 반드시 object를 return함
을 알 수 있습니다. 예시 코드에서는 Point 클래스를 포함해 모든 클래스의 부모 클래스인 Object 클래스의 객체(object)를 반환합니다. 즉, object를 “생성”해 반환한다는 점에서 __new__ 메소드가 오히려 더 생성자 메소드에 가까워 보일 수도 있습니다.
객체의 Allocator/Instantiator는 __new__
객체의 Constructor/Initializer는 __init__
하지만 여기서 중요한 개념을 하나 소개합니다. 우리가 아는 “생성자”는 사실 객체를 “생성”하지 않습니다. 다른 객체 지향 언어들에서는 메모리에 주소를 할당하는 방식으로 클래스 인스턴스를 생성하는 함수를 “생성자”라고 부르지 않습니다. C++에서는 이를 배분자(Allocator), Java에서는 클래스 인스턴스를 메모리에 할당하는 Static Factory Method라고 부릅니다.
즉, __new__ 메소드는 객체를 생성해 반환하는데 이를 생성자로 부르기에는 적절치 않다는 것입니다. 개인적으로 __new__ 메소드는 차라리 인스턴시에이터(Instantiator)라고 부르는 것이 더 적절해 보입니다.
그렇다면, “생성자”는 어떤 역할을 하는 함수를 말하는 것일까요? 네, 우리가 알고 있는 __init__의 기능이 바로 생성자 역할입니다.
즉, __init__은 결국 생성자(Constructor)가 맞지만, 이 “생성(Construct)”은 우리가 알고 있는 “객체 생성” 기능과는 다르다는 것을 말하는 것입니다. “생성”이란 __new__ 메소드로 만든(Instantiate) 인스턴스를 사용자가 원하는 대로 사용하도록 커스토마이징(Customizing/Initiating)함을 말합니다. 예를 들어 self.x=x, self.y=y 와 같이 클래스 인스턴스에 프로퍼티(Property)를 부여하는 등 인스턴스 사용을 위한 초기 세팅을 주는 것이 바로 생성(Construct 또는 Initiate)인 것입니다.
(역자 주 – 사실 생성자를 다른 객체 지향 언어에서처럼 매우 엄밀하게 정의한다면 파이썬의 __init__과 __new__ 둘 다 생성자에 해당되지 않습니다. 파이썬에는 C++와 Java에서 정의하는 Constructor가 존재하지 않으며, 다만 유사한 작업을 수행한다는 차원에서 Constructor Expression이라고 표현할 수도 있습니다.)
물론, 여기까지 읽으셨다면 느끼셨겠지만 여러분들은 __init__과 __new__ 메소드의 본래 목적에는 별 관심이 없습니다. 따라서 보통 __init__으로 열심히 인스턴스를 수정하기만 하면 되고, __new__는 그냥 자동으로 실행되도록 냅둡니다.
__new__의 응용:
__new__를 사용해 생성할 객체 수를 제한하기
__new__ 메소드는 그럼 어디에 쓸모가 있을까? “점”을 의미하는 위 Point 클래스의 인스턴스 4개를 사용해 “사각형”을 의미하는 RectPoint 클래스를 만들어 보겠습니다. Point 클래스 인스턴스 4개는 이 사각형의 4개 꼭지점을 의미합니다.
당연하게도, 사각형은 꼭지점을 4개만 가질 수 있으므로 RectPoint 클래스는 Point 클래스 인스턴스를 “4개까지만” 가질 수 있어야 하며, 5번째 Point 인스턴스를 가질 수 없도록 제약을 걸어야 합니다(역자 주 – 이는 방어적 프로그래밍에 해당합니다).
class RectPoint(Point):
MAX_Inst = 4
Inst_created = 0
def __new__(cls,*args,**kwargs):
if (cls.Inst_created >= cls.MAX_Inst):
raise ValueError("Cannot create more objects")
cls.Inst_created += 1
return super().__new__(cls)
이를 위해 먼저 RectPoint 클래스는 Point 클래스를 상속하도록 만듭니다. 그 다음, 가질 수 있는 클래스 인스턴스 숫자를 최대 4개로 제한합니다. 여기서 RectPoint는 Point 클래스를 상속했으므로 RectPoint의 클래스 인스턴스는 Point 클래스의 인스턴스이기도 합니다.
RectPoint가 가지는 Point 개수를 4개로 제한하기 위해 클래스 변수인 MAX_Inst와 Inst_created를 사용하며, 인스턴스를 생성하는 __new__메소드가 호출될 때 마다 클래스 변수 Inst_created의 값을 증가시켜 나갑니다.
>>> p1 = RectPoint(0,0)
>>> p2 = RectPoint(1,0)
>>> p3 = RectPoint(1,1)
>>> p4 = RectPoint(0,1)
>>>
>>> p5 = RectPoint(2,2)
Traceback (most recent call last):
...
ValueError: Cannot create more objects
코드 결과를 보시면 5번째 점을 RectPoint 클래스로 만드려고 하면 밸류에러를 뱉게함으로써, 인스턴스를 잘못 생성하는 것을 미리 차단함을 볼 수 있습니다.