«   2025/08   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
Archives
Today
Total
반응형
관리 메뉴

go.od_planter

[python] Class, 객체, __init__, self, 매직메서드(특별한 메서드), 자동 호출, 연산자 오버로딩 본문

Language/Python

[python] Class, 객체, __init__, self, 매직메서드(특별한 메서드), 자동 호출, 연산자 오버로딩

go.od_planter 2024. 9. 5. 21:27

 

1. 클래스와 객체

 

클래스(Class): 객체의 청사진(템플릿)을 정의하는 것. 예를 들어, 사람을 나타내는 클래스는 이름, 나이, 성별 등의 속성을 가질 수 있어.

객체(Object): 클래스를 기반으로 생성된 실제 데이터. 사람이라는 클래스를 기반으로 각각의 사람 객체를 생성할 수 있어.

 

2. __init__ 함수

 

__init__은 클래스가 초기화될 때 자동으로 호출되는 메서드로, 객체가 생성될 때 필요한 속성을 초기화하는 역할을 해.

self는 해당 객체를 가리키는 파라미터로, 클래스 안에서 정의된 메서드가 객체 자신의 속성에 접근할 수 있도록 해줘.

 

class Person:
    def __init__(self, name, age):  # 초기화 메서드
        self.name = name  # 객체의 이름 속성
        self.age = age    # 객체의 나이 속성

    def introduce(self):
        print(f"안녕하세요, 저는 {self.name}이고, {self.age}살입니다.")

 

위 코드를 보면:

 

__init__ 메서드는 nameage라는 두 매개변수를 받아서 self.nameself.age에 각각 할당해. 이렇게 하면 객체가 생성될 때마다 이름과 나이를 설정할 수 있어.

introduce 메서드는 self를 사용해서 객체의 속성에 접근하고, 이름과 나이를 출력해.

 

객체 생성 예시:

person1 = Person("철수", 25)  # Person 클래스의 객체 생성
person2 = Person("영희", 30)  # 또 다른 객체 생성

person1.introduce()  # "안녕하세요, 저는 철수이고, 25살입니다." 출력
person2.introduce()  # "안녕하세요, 저는 영희이고, 30살입니다." 출력

 

핵심 개념:

 

__init__: 객체가 생성될 때 실행되는 함수로, 객체의 속성을 초기화.

self: 객체 자기 자신을 가리키며, 클래스 내에서 그 객체의 속성이나 메서드에 접근할 때 사용.

 

 

1. 초기화가 없는 클래스도 있을까?

 

네, 초기화가 없는 클래스도 존재할 수 있어요. __init__ 함수는 선택 사항이기 때문에 꼭 포함하지 않아도 돼요. 만약 클래스가 속성을 초기화하거나 어떤 설정을 할 필요가 없다면, __init__ 함수 없이도 클래스를 만들 수 있어요.

 

class SimpleClass:
    def greet(self):
        print("Hello, World!")

 

이 클래스에는 __init__ 함수가 없어요. 그럼에도 객체를 만들고 메서드를 사용할 수 있습니다:

 

obj = SimpleClass()
obj.greet()  # 출력: "Hello, World!"

 

2. 초기화 함수명은 꼭 __init__이어야만 할까?

 

네, 초기화 함수는 반드시 __init__이어야 해요. __init__은 Python에서 객체가 생성될 때 자동으로 호출되는 특별한 메서드 이름이기 때문에, 다른 이름으로 사용할 수는 없어요.

 

하지만 Python에는 다른 특별한 메서드들도 많이 있어요. 예를 들어, __str__은 객체를 문자열로 표현할 때, __del__은 객체가 삭제될 때 호출되는 함수예요. 이와 같은 특별한 메서드들은 모두 정해진 이름이 있어야 자동으로 호출됩니다.

 

3. self 대신 다른 이름을 쓸 수 있을까?

 

사실 self라는 단어는 관례일 뿐, 다른 단어를 사용할 수도 있어요. Python에서 self는 객체 자신을 가리키는 인자일 뿐이고, 반드시 “self”라는 이름이어야 하는 건 아니에요. 다만, 가독성관습 때문에 대부분의 Python 개발자들이 self라는 이름을 사용해요.

 

예시:

class Person:
    def __init__(this, name, age):  # 'self' 대신 'this' 사용
        this.name = name
        this.age = age

    def introduce(this):
        print(f"안녕하세요, 저는 {this.name}이고, {this.age}살입니다.")

 

위 코드에서 self 대신 this를 사용했어요. 이 코드도 정상적으로 작동해요:

p = Person("철수", 25)
p.introduce()  # 출력: "안녕하세요, 저는 철수이고, 25살입니다."

 

하지만, 대부분의 Python 개발자들은 self를 사용하는 것이 규칙처럼 자리 잡혀 있기 때문에, 다른 사람과 협업하거나 코드를 공유할 때는 self를 사용하는 것이 좋습니다.

 

결론

 

초기화가 없는 클래스도 만들 수 있다.

**__init__**은 반드시 그 이름으로 사용해야 한다. 다른 이름으로 대체할 수 없다.

**self**는 다른 이름으로 사용할 수 있지만, 관습적으로 self를 사용하는 것이 좋다.

 

 

주요 매직 메서드와 그 사용법

 

1. __init__(self, ...):

객체가 생성될 때 호출되어 객체의 초기 속성을 설정.

예시:

class MyClass:
    def __init__(self, value):
        self.value = value

 

2. __str__(self):

print() 함수로 객체를 출력할 때, 객체를 사람이 읽기 쉬운 문자열로 반환하는 메서드.

예시:

class Person:
    def __init__(self, name):
        self.name = name
    
    def __str__(self):
        return f"Person(name={self.name})"
    
p = Person("철수")
print(p)  # 출력: Person(name=철수)

 

3. __repr__(self):

__str__과 비슷하지만, 개발자가 이해할 수 있는 문자열을 반환하는 것이 목적. 디버깅할 때 주로 사용.

예시:

class Person:
    def __init__(self, name):
        self.name = name
    
    def __repr__(self):
        return f"Person(name={self.name})"
    
p = Person("철수")
print(repr(p))  # 출력: Person(name=철수)

 

 

4. __len__(self):

객체의 길이를 반환하는 메서드로, len() 함수 호출 시 동작.

예시:

class CustomList:
    def __init__(self, data):
        self.data = data
    
    def __len__(self):
        return len(self.data)
    
my_list = CustomList([1, 2, 3, 4])
print(len(my_list))  # 출력: 4

 

 

5. __getitem__(self, key):

객체에서 인덱스를 이용해 값을 가져올 때 사용.

예시:

class CustomList:
    def __init__(self, data):
        self.data = data
    
    def __getitem__(self, index):
        return self.data[index]
    
my_list = CustomList([10, 20, 30])
print(my_list[1])  # 출력: 20

 

 

6. __setitem__(self, key, value):

객체에서 인덱스를 통해 값을 설정할 때 호출.

예시:

class CustomList:
    def __init__(self, data):
        self.data = data
    
    def __setitem__(self, index, value):
        self.data[index] = value
    
my_list = CustomList([10, 20, 30])
my_list[1] = 200
print(my_list.data)  # 출력: [10, 200, 30]

 

 

7. __add__(self, other):

+ 연산자를 사용할 때 호출되어 두 객체를 더하는 기능을 재정의할 수 있어요.

예시:

class Number:
    def __init__(self, value):
        self.value = value
    
    def __add__(self, other):
        return Number(self.value + other.value)
    
    def __str__(self):
        return str(self.value)
    
num1 = Number(10)
num2 = Number(20)
result = num1 + num2
print(result)  # 출력: 30

 

 

8. __eq__(self, other):

두 객체가 == 연산자로 비교될 때 호출되어 비교를 재정의할 수 있어요.

예시:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def __eq__(self, other):
        return self.name == other.name and self.age == other.age
    
p1 = Person("철수", 25)
p2 = Person("철수", 25)
print(p1 == p2)  # 출력: True

 

 

그 외 자주 사용하는 매직 메서드들

 

__del__(self): 객체가 삭제될 때 호출.

__call__(self, ...): 객체를 함수처럼 호출할 수 있게 해줌.

__iter__(self): 객체를 반복 가능하게 만들어주는 메서드.

__next__(self): 반복자(iterator)의 다음 값을 반환.

 

매직 메서드를 활용하는 팁

 

1. 클래스의 기능 확장: 매직 메서드를 통해 기본 Python 연산자(+, -, ==, !=, [] 등)를 재정의하여 클래스의 기능을 확장할 수 있어요.

2. 사용자 정의 자료형 만들기: __getitem__, __setitem__ 등을 사용하여 사용자 정의 자료형(리스트나 사전과 같은)을 쉽게 만들 수 있어요.

 

 

 

 

Python의 동작 방식

 

Python에서는 [](대괄호)을 사용해 객체의 특정 요소에 접근할 때, 내부적으로 __getitem__() 메서드가 호출돼요. 이 과정은 Python이 제공하는 매직 메서드의 자동 호출 덕분인데, 이는 Python이 다양한 연산자나 메서드에 대해 어떻게 반응할지를 정의한 규칙 덕분이에요.

 

동작 원리

 

1. 객체에 대괄호를 사용: 예를 들어, obj[1]이라고 쓰면 Python은 해당 객체에서 1이라는 인덱스나 키에 해당하는 값을 찾으려고 시도해요.

2. __getitem__() 호출: Python은 obj[1]이라는 구문을 보면, 그 객체에 __getitem__이라는 메서드가 정의되어 있는지 확인해요. 만약 정의되어 있다면, 그 메서드를 자동으로 호출하고, 1이라는 인덱스를 __getitem__(self, key)key 인자로 전달해요.

3. 결과 반환: __getitem__() 메서드는 그 인덱스에 해당하는 값을 찾아서 반환해요. 그러면 obj[1]의 결과로 그 값을 얻게 되는 거죠.

 

예시를 통해 자세히 살펴보자

class MyList:
    def __init__(self, data):
        self.data = data  # 리스트 형태의 데이터를 저장

    def __getitem__(self, index):
        # 대괄호로 호출할 때 이 함수가 작동한다
        return self.data[index]

# 객체 생성
my_list = MyList([10, 20, 30])

# 대괄호로 값 가져오기
print(my_list[1])  # 출력: 20

 

 

 

위 코드에서는 my_list[1]을 호출했을 때 내부적으로 다음과 같은 일이 일어나:

 

1. my_list[1]이 호출되면 Python은 my_list 객체의 __getitem__() 메서드를 자동으로 찾는다.

2. __getitem__() 메서드에 1이라는 인덱스 값이 전달되고, 이 메서드는 self.data[1]을 반환한다.

3. 최종적으로 my_list[1]20이라는 값을 반환한다.

 

즉, []를 사용하면 Python은 그 객체가 __getitem__ 메서드를 가지고 있는지 확인하고, 있으면 그 메서드를 호출해 값을 반환하게 되는 거야.

 

내부 동작 과정

 

Python의 연산자 오버로딩 덕분에 [] 같은 연산자를 사용할 때 Python이 자동으로 해당 매직 메서드(__getitem__, __setitem__, __delitem__ 등)를 호출해.

obj[key] 구문은 실제로는 obj.__getitem__(key)와 동일해.

 

결론

 

obj[1] 같은 대괄호 표기법을 사용할 때, Python은 자동으로 __getitem__ 메서드를 호출해서 인덱스나 키에 해당하는 값을 가져와.

이 방식은 Python이 객체지향적으로 다양한 자료형을 다룰 수 있게 해주는 중요한 기능 중 하나야.

 

이걸 이해하면 리스트, 딕셔너리, 그리고 사용자 정의 객체에서도 이런 동작을 쉽게 구현할 수 있어.

 

 

**my_list[1]**은 내부적으로 **my_list.__getitem__(1)**으로 변환되어 호출돼요.

이는 Python이 연산자 오버로딩을 통해 [] 연산자를 처리하는 방법을 정의했기 때문에 가능한 일이에요.

두 표현은 동등하게 동작하지만, 대괄호([]) 표기법이 더 간결하고 직관적이기 때문에 일반적으로 많이 사용돼요.

 

 

추가

직관적으로 보면 **my_list[1]**이 내부적으로 __getitem__을 호출하는 것처럼 보이기 때문에, 한 단계를 더 거친다고 생각할 수 있어. 하지만 실제로는 두 방법의 연산 속도는 동일해.

 

이유는 다음과 같아:

 

1. Python의 최적화:

Python은 연산자([], +, - 등)와 관련된 작업을 매우 효율적으로 처리하도록 설계돼 있어요. my_list[1]과 같은 대괄호 표기법은 Python에서 매우 빈번하게 사용되기 때문에, Python 인터프리터는 이를 효율적으로 처리할 수 있도록 내부적으로 최적화되어 있어요. 즉, my_list[1]my_list.__getitem__(1)을 호출하는 것과 성능 차이가 없도록 최적화된 상태입니다.

2. 컴파일 단계에서 처리:

my_list[1]은 Python 바이트코드로 컴파일될 때 바로 __getitem__() 메서드를 호출하도록 바뀌어요. Python이 실행하는 바이트코드를 보면, 두 방법 모두 결국 __getitem__ 호출로 변환됩니다. 그래서 이 차이는 실제 실행 속도에 영향을 미치지 않아요.

 

실제 바이트코드 확인

 

Python 바이트코드를 확인하면, my_list[1]my_list.__getitem__(1) 모두 동일한 방식으로 처리된다는 걸 알 수 있어요. 바이트코드는 Python이 코드를 실행할 때 사용하는 중간 단계 코드로, 이를 통해 Python이 두 방법을 동일하게 처리한다는 것을 확인할 수 있어요.

 

예시

import dis

class MyList:
    def __init__(self, data):
        self.data = data
    
    def __getitem__(self, index):
        return self.data[index]

my_list = MyList([10, 20, 30])

# 대괄호를 사용할 때의 바이트코드
print("my_list[1] 바이트코드")
dis.dis('my_list[1]')

# __getitem__을 직접 호출할 때의 바이트코드
print("\nmy_list.__getitem__(1) 바이트코드")
dis.dis('my_list.__getitem__(1)')

 

이 코드를 실행해 보면, 두 방법 모두 동일하게 __getitem__() 메서드를 호출하는 바이트코드로 변환됩니다.

 

결론

 

**my_list[1]**과 **my_list.__getitem__(1)**은 Python에서 동일한 방식으로 처리되기 때문에, 연산 속도는 차이가 없다.

Python은 이러한 연산자 오버로딩을 효율적으로 처리하기 때문에, 대괄호([]) 표기법을 사용하는 것이 직관적이고 편리하면서도 성능에 영향을 미치지 않아요.

 

 

1. dis.dis: Python 바이트코드 디스어셈블러

 

dis.dis디스어셈블러로, Python 코드가 실행되기 전에 컴파일되는 바이트코드를 보여주는 도구예요. Python 코드는 먼저 바이트코드로 컴파일된 후, 이 바이트코드를 Python 인터프리터가 실행하게 돼요. **dis.dis()**를 사용하면, Python 코드가 실제로 어떻게 바이트코드로 변환되는지 확인할 수 있어요.

 

2. 바이트코드란?

 

**바이트코드(bytecode)**는 Python 인터프리터가 실행하는 중간 코드 형태예요. 우리가 작성한 Python 소스 코드는 먼저 바이트코드로 변환된 후, 이 바이트코드를 Python 가상 머신(PVM)이 실행해요. 바이트코드는 사람에게는 읽기 어려운 형식이지만, Python 인터프리터가 실행하기에는 더 빠르고 효율적인 형태예요.

 

3. 두 바이트코드가 다른 이유

 

두 가지 코드를 보면 바이트코드가 약간 다르게 나오는 걸 알 수 있어요. 하지만 두 코드 모두 결과적으로는 동일한 동작을 수행해요. 그 차이는 대괄호 표기법([])과 메서드 호출(.__getitem__()) 방식의 차이에서 비롯됩니다.

 

첫 번째 바이트코드: my_list[1]

  0           0 RESUME                   0
  1           2 LOAD_NAME                0 (my_list)
              4 LOAD_CONST               0 (1)
              6 BINARY_SUBSCR
             10 RETURN_VALUE

 

LOAD_NAME 0 (my_list): my_list라는 이름을 로드합니다.

LOAD_CONST 0 (1): 인덱스 1을 로드합니다.

BINARY_SUBSCR: 대괄호([]) 표기법을 이용한 구독 연산자로, 이것은 자동으로 __getitem__()을 호출합니다.

RETURN_VALUE: 결과를 반환합니다.

 

BINARY_SUBSCR[] 연산자를 사용했을 때 Python이 내부적으로 __getitem__ 메서드를 호출하는 연산이에요.

 

 

두 번째 바이트코드: my_list.__getitem__(1

  0           0 RESUME                   0
  1           2 LOAD_NAME                0 (my_list)
              4 LOAD_ATTR                3 (NULL|self + __getitem__)
             24 LOAD_CONST               0 (1)
             26 CALL                     1
             34 RETURN_VALUE

 

 

LOAD_NAME 0 (my_list): my_list라는 이름을 로드합니다.

LOAD_ATTR 3 (NULL|self + __getitem__): my_list 객체의 __getitem__ 메서드를 로드합니다.

LOAD_CONST 0 (1): 인덱스 1을 로드합니다.

CALL 1: 로드된 __getitem__ 메서드를 호출합니다.

RETURN_VALUE: 결과를 반환합니다.

 

여기서 **LOAD_ATTR**은 객체의 속성, 즉 __getitem__ 메서드를 가져오고, **CALL**은 그 메서드를 실행하는 동작을 나타냅니다.

 

4. 왜 두 바이트코드가 다른가?

 

**my_list[1]**은 연산자([]) 오버로딩에 의해 동작하며, Python은 이를 최적화된 방식으로 처리하기 위해 BINARY_SUBSCR라는 바이트코드를 사용해요. 이 바이트코드는 바로 __getitem__을 호출하는 것이 아닌, 최적화된 구독 연산자로 동작합니다.

반면 **my_list.__getitem__(1)**은 메서드를 명시적으로 호출하는 방식이므로, LOAD_ATTRCALL이라는 두 개의 바이트코드를 통해 먼저 메서드를 가져온 다음에 호출하는 방식입니다.

 

5. 속도 차이가 있을까?

 

비록 바이트코드가 다르게 보이긴 하지만, 실질적으로 속도 차이는 거의 없다고 볼 수 있어요. Python 인터프리터가 [] 연산자를 사용하는 경우 매우 최적화된 방식으로 동작하므로, __getitem__()을 명시적으로 호출하는 것과 성능 차이가 거의 없어요.

 

그러나 이론적으로 보면 my_list[1]더 빠를 수는 있어요, 왜냐하면:

 

BINARY_SUBSCR는 직접적으로 구독 연산을 처리하는 반면,

my_list.__getitem__(1)LOAD_ATTRCALL을 통해 한 단계 더 거쳐야 하기 때문입니다.

 

하지만 이 차이는 매우 미세하기 때문에, 실제 성능 차이는 눈에 띄지 않을 가능성이 커요.

 

결론

 

my_list[1]my_list.__getitem__(1)의 바이트코드는 다르게 보이지만, 결과는 동일합니다.

[] 연산자는 Python에서 최적화된 구독 연산자로 처리되어 바이트코드에서 BINARY_SUBSCR를 사용합니다.

실질적인 성능 차이는 거의 없으며, 일반적인 상황에서는 []를 사용하는 것이 더 간결하고 직관적입니다.

728x90
반응형