🌐

4.6.1. 클래스

1. 클래스

클래스는 데이터와 기능을 가지고 있는 인스턴트 객체를 생성하기 위한 역할을 합니다. 파이썬은 대표적인 객체지향 프로그래밍 언어이며 클래스는 일종의 설계도면입니다.
파이썬은 이 설계도면을 보며 하나의 인스턴스 객체를 만들어 냅니다. 그리고 인스턴스 객체를 선언하여 사용할 수 있게 됩니다.
입력
class Car(object): #앞 부분을 대문자로 사용합니다. MaxSpeed = 300 MaxPeople = 5 def move(self, x): print(x, '의 스피드로 움직이고 있습니다.') def stop(self): print('멈췄습니다.') k5 = Car() k3 = Car() k5.move(10) k5.stop() k3.move(5) k3.stop() print(k5.MaxSpeed) print(k3.MaxSpeed)
 
출력
10 의 스피드로 움직이고 있습니다. 멈췄습니다. 5 의 스피드로 움직이고 있습니다. 멈췄습니다. 300 300
 

1.1 클래스 변수

클래스 변수는 클래스 바로 하위에 자리하고 있는 변수들 입니다.이 클래스 변수는 아래의 예시와 같이 클래스 이름을 통해서 접근할 수 있습니다.
(주의: 변수 이름으로 접근하지 않습니다!)
입력
class Car(object): # 클래스 변수 위치 (파이썬 규약에 따라 indent로 결정) kinds = [] MaxSpeed = 300 MaxPeople = 5 def move(self, x): print(x, '의 스피드로 움직이고 있습니다.') def stop(self): print('멈췄습니다.') k5 = Car() Car.kinds.append('k5') k3 = Car() Car.kinds.append('k3') print(k5.kinds)
출력
['k5', 'k3']
 
이 클래스 변수는 해당 클래스를 통해 만들어진 모든 인스턴스 객체들이 공유하는 변수 값입니다. 각 인스턴스 객체들 각자가 관리하고 있는 변수는 인스턴스 변수라고 합니다.
이 클래스 변수를 다룰 때는 조금 주의해야 할 것이 있습니다.
주소나 포인터의 개념에 대해 아직 익숙하지 않다면 해당 부분에서는 클래스 변수에 접근할 때는 인스턴스 객체의 변수명이 아닌 클래스명을 통해 접근해야 한다는 것만 알고 넘어가도 좋습니다.
 
위의 예시에서는 인스턴스를 생성할 때마다 어떤 인스턴스가 만들어졌는지 확인하기 위해서 생성할 때마다 Car.kinds 에 접근하여 해당 배열에 어떤 자동차가 추가되었는지를 표시하고 있습니다.
하지만 이렇게 처음 시작시에 일일이 메서드를 호출해서 하는 것은 너무 불편하고 실수가 생길 수 밖에 없는 작업입니다. 이런 불편을 해소하기 위해 __init__ 메서드가 있습니다.
 
입력
class Car(object): kinds = [] speed = 300 def add_kinds(self, name): self.kinds.append(name) def change_speed(self, speed): self.speed = speed k5 = Car() k3 = Car() k5.add_kinds('k5') k3.add_kinds('k3') k5.change_speed(500) k3.change_speed(250) print('k5.kinds:', k5.kinds) print('k3.kinds:', k3.kinds) print('k5.speed:',k5.speed) print('k3.speed:',k3.speed)
 
출력
k5.kinds: ['k5', 'k3'] k3 kinds: ['k5', 'k3'] k5.speed: 500 k3.speed: 250
 
클래스 변수를 다룰 때는 클래스 자체(예시에서는 Car 객체)를 이용하여 다루어야 하지만, 위의 예시에서는 각 인스턴스 객체들이 각각 자신의 메서드를 통해 접근해서 다루고 있습니다.
 
kinds 는 원래 기대했던 클래스 변수처럼 결과가 나왔고, speed는 그렇지 않는 것을 볼 수 있습니다. 이 차이는 kinds는 배열이고, speed는 값이기 때문입니다.
 
kinds 배열의 경우에는 각 인스턴스 객체가 해당 배열의 주소를 참조하고 있기 때문에, 그 배열의 주소에 접근하여 조작해서 마치 클래스 변수와 동일하게 동작을 하게 된 것입니다. 반면에 speed 의 경우에는 단순히 값이기 때문에 주소가 따로 없으므로 각 인스턴스 객체가 값만을 가지고 있습니다. 따라서 각 인스턴스 객체들의 값이 따로 변하게 됩니다.
 

2. __init__ 함수

__init__ 메서드는 다른 프로그래밍 언어에서의 생성자(constructor) 역할을 하는 클래스 메서드입니다.
이 메서드는 인스턴스 객체를 생성할 때 자동으로 실행됩니다.
 

2.1 인스턴스 변수

인스턴스 객체들이 모두가 공통으로 공유하는 값이 클래스 변수라면, 인스턴스 변수는 각 인스턴스 객체들 각자가 가지고 있는 값입니다.
인스턴스 변수는 init 메서드 안에서 선언을 해주어야 합니다. 이곳에서 선언된 변수들은 각 인스턴스 객체들의 변수로 활동할 수 있습니다.
 
입력
class Car(object): MaxSpeed = 300 # 공유하는 영역 MaxPeoeple = 5 # 공유하는 영역 def __init__(self, 이름): #self는 자신만의 영역 self.name = 이름 def move(self, x): print(self.name, x, '의 스피드로 움직이고 있습니다.') def stop(self): print('멈췄습니다.') k5 = Car('케이파이브') k3 = Car('케이쓰리') k5.move(100) k3.move(200)
 
출력
케이파이브 100 의 스피드로 달리고 있습니다. 케이쓰리 200 의 스피드로 달리고 있습니다
 
예제에서 보시다시피 init이라는 메서드를 호출하지 않고 Car('케이파이브')로 인스턴스 객체를 생성하면서 매개변수로 name을 넣어주는 것을 확인할 수 있습니다.
초기화 함수를 사용하기 위해서는 반드시 __init__을 사용하고, 매개변수는 self로 선언해야합니다.
그렇다면 self는 뭘까요? 앞서 클래스 변수를 모든 인스턴스 객체들이 공유했다면, self의 영역은 다른 인스턴스에 해당 변수를 공유하지 않는 고유 영역이라 볼 수 있습니다.
 

3. 특별 메소드(magic method)

파이썬의 클래스에는 기본적으로 내장하고 있는 특별 메소드들이 있습니다. 파이썬에서는 이런 내장하고 있는 특별 메소드들을 쉽게 재정의하여 사용할 수 있게 되어있습니다.
마치 마법처럼 간단하게 수정할 수 있다고 해서 매직 매소드라고 부릅니다. 혹은 더블 언더바(__)를 쓰고 있는 점에서 줄여서 던더(dunder) 메소드라고도 부릅니다.
 
입력
class Car(object): kinds = [] MaxSpeed = 300 MaxPeoeple = 5 def __init__(self, 이름): self.name = 이름 self.kinds.append(이름) def move(self, x): print(self.name, x, '의 스피드로 움직이고 있습니다.') print(self.kinds) self.stop() def stop(self): print('멈췄습니다.') k5 = Car('케이파이브') k5.move(100) k3 = Car('케이쓰리') k3.move(200)
 
출력
케이파이브 100 의 스피드로 움직이고 있습니다. ['케이파이브'] 멈췄습니다. 케이쓰리 200 의 스피드로 움직이고 있습니다. ['케이파이브', '케이쓰리'] 멈췄습니다.
 
kinds공유하는 영역이기 때문에 어떤 종류를 생산하는지에 대해서 알 수 있게 됩니다.
그리고 바로 메서드 안에서 다른 메서드도 호출 할 수 있는데요, move에서 stop을 호출하기 때문에 출발함과 동시에 멈추게 되며 결과값을 출력해주죠.
더 자세하기 들어가보자면, 클래스를 정의한 블록 내에서 존재하는 클래스 속성과 메서드는 클래스 네임 스페이스(변수가 객체를 바인딩할 때 그 둘 사이의 관계를 저장하고 있는 공간)에 등록이 됩니다. 인스턴스 속성은 __init__메서드 내에서 self.이름을 주고 할당이 되는 경우에만 인스턴스 네임 스페이스에 등록이 됩니다.
즉 쉽게 말해 위 코드에서 kindMaxSpeed, MaxPeople은 인스턴스 변수이므로 클래스 네임 스페이스에 등록 되어있고, self.name은 인스턴스 네임 스페이스에 등록이 되어있습니다.
 
입력
class Car(object): kinds = [] MaxSpeed = 300 MaxPeoeple = 5 def __init__(self, 이름): self.name = 이름 self.kinds.append(이름) def __add__(self, obj):#obj : 더할 요소 # 더하기를 정의하고 있는 기본 매직메서드 return 'hello world' def move(self, x): print(self.name, x, '의 스피드로 움직이고 있습니다.') print(self.kinds) self.stop() def stop(self): print('멈췄습니다.') k5 = Car('케이파이브') k3 = Car('케이쓰리') print(k5 + k3)
 
출력
'hello world'
 
인스턴스.__dir__를 입력하면 볼 수 있는 기본 매직 메서드 중 __add__메서드를 수정해 보았습니다. 우리가 알고있는 'add'라는 개념은 보통 무언가(obj)를 더하는 메서드이죠? 여기서는 메서드를 수정해서 'hello world'가 출력되게 해보았습니다.
출력해보니 이상없이 실행되는 것을 확인할 수 있네요. 즉, 여러분들 만의 메서드를 얼마든지 재정의 할 수 있다는 뜻 입니다.
 
입력
... def __add__(self, obj): return 'hello world', obj.name def __str__(self): #제목을 알려줍니다. return self.name ... print(k5 + k3) print(k5)
 
출력
('hello world', '케이쓰리') 케이파이브
 
여기서는 obj에 대해서 더 자세히 알아보도록 하겠습니다. 우선 obj는 뭘까요? return 값 뒤에 출력해봅시다.
결과 값을 보니 '케이쓰리'라고 출력되는 것을 확인할 수 있네요.
그렇다면 obj는 a + b일 때 a에 더하는 값인 b이고 self.name은 자기 자신인 a라고 보면 되겠네요. __str__은 자신의 이름(제목)을 알려주는 매직 메서드 입니다.
 
 
입력
... @staticmethod #decorator def 스피드배속(현재스피드, 배속할스피드) print(f'현재 {현재스피드 * 배속할스피드}의 스피드로 달리고 있습니다.') Car.스피드배속(100, 2)
 
출력
현재 200의 스피도로 달리고 있습니다.
 
클래스를 직접 써야 할 때 self없이 사용할 수 있는 정적 메서드 입니다.
self값을 인자값으로 주지 않아도 되니 더 간결해지겠죠? @staticmethod는 데코레이터 라고 합니다.
여기까지 자주쓰는 매직 메서드들을 직접 실행해 보면서 설명해 드렸는데요, 이 매직 메서드에 대해 더 알아보고 싶으시면 파이썬 공식 문서를 참고해주세요.
 

4. 상속

입력
class Car(object): maxSpeed = 300 maxPeople = 5 def move(self, x): print(x, '의 스피드로 달리고 있습니다.') def stop(self): print('멈췄습니다.') class HybridCar(Car): battery = 1000 batteryKM = 300 class ElectricCar(HybridCar): battery = 2000 batteryKM = 600 k5 = HybridCar() electricCarK5 = ElectricCar() k5.maxSpeed print(electricCarK5.maxSpeed) print(electricCarK5.battery)
 
출력
300 2000
 
클래스를 상속하는 방법은 위의 예시처럼 새로운 클래스의 ()안에 상속할 클래스명을 적어주는 것입니다.
HybridCar 클래스는 기존의 Car 클래스가 사용하던 데이터와 기능들을 모두 사용할 수 있으며, 추가적인 기능을 만들었습니다. 또한 maxPeople 의 데이터를 새로 덮어씌워 자기 자신만의 클래스 변수를 새로 정의할 수 있습니다.
여기에서 상속을 하는 Car를 부모 클래스(슈퍼 클래스)라고 부르고 HybridCar를 자식 클래스(서브 클래스)라고 부릅니다.
 

5. 메서드 오버라이딩

같은 이름이나 함수를 덮어 쓰는 것을 메서드 오버라이딩 이라고 합니다.
입력
class Car(object): maxSpeed = 300 maxPeople = 5 def move(self, x): print(x, '의 스피드로 달리고 있습니다.') def stop(self): print('멈췄습니다.') class HybridCar(Car): battery = 1000 batteryKM = 300 class ElectricCar(HybridCar): battery = 2000 batteryKM = 600 def move(self, x): print(self.batteryKM, '만큼 달릴 수 있습니다.') print(x, '스피드로 달리고 있습니다.') k5 = HybridCar() electricCarK5 = ElectricCar() k5.maxspeed electricCarK5.maxspeed electricCarK5.battery electricCarK5.move(10)
 
출력
600 만큼 달릴 수 있습니다. 10 스피드로 달리고 있습니다.
여기서는 movebattery, batteryKM를 덮어썼네요.

6. 다중 상속

여러 클래스를 상속받을 때는 아래와 같이 사용하면 됩니다. 상속을 받는 법은 비슷합니다.
입력
class Car(object): maxSpeed = 300 maxPeople = 5 def move(self, x): print(x, '의 스피드로 달리고 있습니다.') def stop(self): print('멈췄습니다.') class HybridCar(Car): battery = 1000 batteryKM = 300 class ElectricCar(HybridCar): battery = 2000 batteryKM = 600 def move(self, x): print(self.batteryKM, '만큼 달릴 수 있습니다.') print(x, '스피드로 달리고 있습니다.') k5 = HybridCar() electricCarK5 = ElectricCar() k5.maxSpeed electricCarK5.maxSpeed electricCarK5.battery electricCarK5.move(10)
 
출력
Error #MRO : 어떤 클래스를 어떤 순서로 받을것인지의 선택을 말합니다.
 
다중 상속에서 유념해야할 점은 어떤 클래스를 어떤 순서로 정할건지에 대한 정리가 필요합니다.
 
입력
class Car(object): maxSpeed = 300 maxPeople = 5 def move(self, x): print(x, '의 스피드로 움직이고 있습니다.') def stop(self): print('멈췄습니다.') class HybridCar(Car): battery = 1000 batteryKM = 300 class ElectricCar(Car): battery = 2000 batteryKM = 600 def move(self, x): print(self.batteryKM, '만큼 달릴 수 있습니다.') print(x, '스피드로 달리고 있습니다.') class HybridElectricCar(HybridCar, ElctricCar): pass k5 = HybridElectricCar() print(K5.battery)
 
출력
1000
다중 상속을 받은 HybridElectricCar 에서 battery를 출력해보면 어떤 값이 나올까요?
정답은 HybridCar의 배터리 값을 나타내게 됩니다.
HybridElectricCar의 MRO(Method Resolution Order)를 출력해보도록 할게요.
입력
HybridElectricCar.mro()
 
출력
[<class '__main__.HybridElectricCar'>, <class '__main__.HybridCar'>, <class '__main__.ElectricCar'>, <class '__main__.Car'>, <class 'object'>]
자기 자신 바로 다음에 HybridCar 가 있죠? 그럼 그 값을 가져가게 됩니다. 같은 값들이 뒤에 있다고 하더라도요.
 
여기에서 다중 상속을 할 때에 상속을 하는 것에 대한 우선 순위를 더 자세히 보도록 하겠습니다.
입력
class Car(object): maxSpeed = 300 maxPeople = 5 def __init__(self, name): self.kinds = [] self.kinds.append(name) def move(self, x): print(x, '의 스피드로 움직이고 있습니다.') def stop(self): print('멈췄습니다.') class MaxWheel(object): wheel = 19 def move(self, x): print(x, '의 스피드로 큰 차가 이동 중입니다.') class MiniWheel(object): wheel = 15 maxSpeed = 250 def move(self, x): print(x, '의 스피드로 작은 차가 이동 중입니다.') class TestWheel(object): def move(self, x): print(x, '의 스피드로 테스트 차가 이동 중입니다.') class LargeCar(Car, MaxWheel): pass class CompactCar(Car, MiniWheel): pass class TestCar(Car, MaxWheel, TestWheel): pass my_car = LargeCar('big') your_car = CompactCar('mini') test_car = TestCar('test') print(my_car.wheel) print(your_car.wheel) print(your_car.maxSpeed) your_car.move(100) test_car.move(100)
 
출력
19 15 300 100 의 스피드로 움직이고 있습니다. 100 의 스피드로 움직이고 있습니다.
 
 
위의 예제에서 볼 수 있듯이 먼저 매개변수로 오는 것의 데이터와 메소드가 우선순위가 되는 것을 볼 수 있습니다.
TestCar를 예로 들면 같은 데이터, 메소드가 있을 경우 다음과 같은 우선 순위를 가지게 됩니다.
 
TestCar > Car > MaxWheel > TestWheel