CODICT

5. Python의 객체와 클래스 본문

Programming/Python

5. Python의 객체와 클래스

Foxism 2020. 2. 6. 05:59

지금까지 문자열과 딕셔너리 같은 자료구조, 함수와 모듈 같은 코드구조를 익혔습니다. 이번 게시글에서는 커스텀 자료구조인 객체(Object)를 배웁니다.

 


 

1. 객체란 무엇인가?

 

이전 게시글에서 본 것처럼, 숫자에서 모듈까지 파이썬의 모든 것은 객체입니다. 하지만 파이썬은 특수 구문을 이용하여 대부분의 객체를 숨깁니다. num = 7을 입력했을 때 7이 담긴 정수 타입의 객체를 생성하고, 객체 참조(Object Reference)를 num에 할당합니다. 이번 게시글을 통해 객체를 직접 만들고, 기존 객체의 행동을 수정하면서, 객체를 자세히 들여다봅시다.

객체는 데이터(변수, 속성(Attribute)이라고 부름)와 코드(함수, 메소드(Method)라고 부름)를 모두 포함합니다. 개체는 어떤 구체적인 것의 유일한 인스턴스(Instance)를 나타냅니다. 예를 들어 값 7의 정수 객체는 덧셈이나 곱셈 같은 계산을 쉽게 해줍니다. 값 8은 또 다른 객체입니다. 이것은 파이썬에 7과 8에 속하는 정수 클래스가 있다는 뜻입니다. 문자열 'ccat'과 'duck' 또한 객체고, 이것은 앞에서 본  capitalize()나 replace() 같은 문자열 메소드를 갖고 있습니다.

아무도 생성하지 않은 새로운 객체를 생성할 때는 무엇을 퐇마하고 있는지 가리키는 클래스(Class)를 생성해야 합니다.

객체를 명사, 메소드를 동사라고 생각해봅시다. 객체는 각각의 사물을 나타내고, 메소드는 다른 사물과 어떻게 상호작용하는지 정의합니다.

모듈과 달리, 객체는 각자 다른 값을 가진 속성의 객체를 동시에 여러 개 생성할 수 있습니다. 객체는 마치 코드를 덧붙인 슈퍼 자료구조와 같습니다.

 

2. 클래스 선언하기: class

 

지난 게시글에서는 객체를 플라스틱 박스에 비유했습니다. 클래스는 박스를 만드는 틀에 비유할 수 있습니다. 예를 들어 String은 'cat', 'duck'과 같은 문자열 객체를 만드는 내장된 클래스입니다. 파이썬에는 리스트, 딕셔너리 등을 포함한 다른 표준 데이터 타입을 생성하는 많은 내장 클래스가 있습니다. 커스텀 객체를 생성하기 위해 먼저 class 키워드로 클래스를 정의합니다. 간단한 코드를 살펴봅시다.

사람에 대한 정보를 나타내는 객체를 정의한다고 가정합시다. 각 객체는 한 사람을 나타냅니다. 먼저 객체의 틀로 Person 클래스를 정의합니다. 계속해서 아주 간단한 클래스에서 실제로 뭔가를 하는 여러 개의 클래스를 만들어봅시다.

먼저 가장 간단한 빈 클래스를 만듭시다.

>>> class Person():
	pass

함수와 마찬가지로, 빈 클래스를 나타내기 위해 pass를 사용했습니다. 이것은 객체를 생성하기 위한 최소한의 정의입니다. 함수처럼 클래스 이름을 호출하여 클래스로부터 객체를 생성할 수 있습니다.

>>> someone = Person()

Person()은 Person 클래스로부터 개별 객체를 생성하고, someone ㅂ변수에 이 객체를 항당합니다. 그러나 Person 클래스는 빈 클래스기 때문에 생성한 someone 객체만 존재할 뿐 아무것도 할 수 없습니다. 실제로 이러한 클래스는 정의하지 않을 것입니다. 다음 코드를 살펴봅시다.

이번에는 특별한 파이썬 객체 초기화(Initialization) 메소드 __init__을 포함시킵니다.

>>> class Person():
	def __init__(self):
		pass

이것은 진짜 핑썬 클래스의 정의입니다. __init__()과 self가 생소합니다. __init__()은 특별한 메소드 이름입니다. 이 메소드는 클래스의 정의로부터 객체를 초기화합니다. self 인자는 객체 자신을 가리킵니다.

클래스에서 __init__()을 정의할 때, 첫 번째 매개변수는 self여야 합니다. 비록 파이썬에서 self는 예약어가 아니지만, 일반적으로 이렇게 사용합니다. 나중에 (자신을 포함하여!@) 다른 사람이 self를 봤을 때, 이것이 무엇을 의미하는지 추측하지 않아도 됩니다.

두 번째 Person 클래스의 정의 또한 아무것도 하지 않는 객체일 뿐이었습니다. 세 번째에서는 실제로 뭔가를 하는 간단한 객체를 생성해봅시다. 이번에는 name 매개변수를 초기화 메소드에 추가합니다.

>>> class Person():
	def __init__(self, name):
		self.name = name

이제 name 배개변수에 문자열을 전달하여 Person 클래스로부터 객체를 생성할 수 있습니다.

>>> hunter = Person('Elmer Fudd')

코드가 어떻게 동작하는지 살펴봅시다.

  • Person 클래스의 정의를 찾습니다.

  • 새 객체를 메모리에 초기화(생성)합니다.

  • 객체의 __init__ 메소드를 호출합니다. 새롭게 생성된 객체를 self에 전달하고, 인자('Elmer Fudd')를 name에 전달합니다.

  • 객체에 name 값을 저장합니다.

  • 새로운 객체를 반환합니다.

  • hunter에 이 객체를 연결합니다.

이 새로운 객체는 파이썬의 다른 객체의 생성 과정과 같습니다. 이 객체는 리스트, 튜플, 딕셔너리 또는 셋의 요소로 사용할 수 있습니다. 이 객체를 함수에 인자로 전달할 수 있고, 함수에서 그 결과를 반환할 수 있습니다.

우리가 전달한 name 값에 무엇이 있는지 살펴봅시다. 그것은 객체의 속성(Attribute)에 저장되어 있습니다. 이 속성은 직접 읽고 쓸 수 있습니다.

>>> print('The mighy hunter: ', hunter.name)
#The mighy hunter:  Elmer Fudd

Person 클래스 정의에서 name 속성을 self.name으로 접근하는 것을 기억하십시오. hunter와 같은 객체를 생성할 때, 이것을 hunter.name이라 여깁니다.

모든 클래스 정의에서 __init__ 메소드를 가질 필요는 없습니다. __init__ 메소드는 같은 클래스에서 생성된 다른 객체를 구분하기 위해 필요한 다른 무언가를 수행합니다.

 

3. 상속

 

어떤 코딩 문제를 해결하려 할 때는 우선 필요한 대부분의 기능을 수행하는 기존 클래스를 찾을 것입니다. 그리고 기존 클래스에 필요한 기능을 추가할 것입니다. 이 때 어떻게 해야 할까요? 기존 클래스를 수정하려 하면 클래스를 더 복합하게 만들게 될 것이고, 코드를 잘못 건드려 수행할 수 없게 만들 수도 있습니다.

물론 기존 클래스를 자르기/붙여넣기로 새 클래스를 만들어서, 새 코드에 병합(Merge)할 수도 있습니다. 그러나 이것은 우리가 관리해야 할 코드가 더 많아진다는 것을 의미합니다. 그리고 같은일을 수행하는 기존 클래스와 새로운 클래스가 서로 다른 곳에 있기 때문에 혼란스러워집니다.

이 문제는 상속(Inheritance)으로 해결할 수 있습니다. 기존 클래스에서 일부를 추가하거나 변경하여 새 클래스를 생성합니다. 이것은 코드를 재사용(Reuse)하는 아주 좋은 방법입니다. 상속을 이용하면 새로운 클래스는 기존 클래스를 복사하지 않고, 기존 클래스의 모든 코드를 쓸 수 있습니다.

필요한 것만 추가/변경하여 새 클래스를 정의합니다. 그리고 이것은 기존 클래스의 행동(Behavior)을 오버라이드(Override; 재정의)합니다. 기존 클래스는 부모(Parent) 클래스, 슈퍼(Super) 클래스, 베이스(Base) 클래스라고 부릅니다. 새 클래스는 자식(Child) 클래스, 서브(Sub) 클래스, 파생된(Derived) 클래스라고 부릅니다. 이 용어들은 객체 지향 프로그래밍(Object-Oriented Programming)에서 다르게 사용될 수 있습니다.

다음 코드를 통해 상속을 사용해봅시다. 빈 클래스 Car를 정의합니다. 그리고 Car의 서브 클래스 Yugo를 정의합니다. 서브 클래스는 같은 class 키워드를 사용하지만, 괄호 안에 부모 클래스의 이름을 지정합니다.(아래의 class Yugo(Car)).

>>> class Car():
	pass
>>> class Yugo(Car):
	pass

각 클래스로부터 객체를 생성합니다.

>>> give_me_a_car = Car()
>>> give_me_a_yugo = Yugo()

자식 클래스는 부모 클래스를 구체화(Specialization)한 것입니다. 객체 지향 용어로 Yugo는 Car입니다. give_me_a_yugo 객체는 Yugo 클래스의 인스턴스지만, 또한 Car 클래스가 할 수 있는 어떤 것을 상속받습니다.

실제로 뭔가는 하는 Car와 Yugo의 새로운 클래스를 정의해봅시다.

>>> class Car():
	def exclaim(self):
		print("I'm a Car!")
	
>>> class Yugo(Car):
	pass

클래스로부터 객체를 만들고, exclaim 메소드를 호출합니다.

>>> give_me_a_car = Car()
>>> give_me_a_yugo = Yugo()
>>> give_me_a_car.exclaim()
#I'm a Car!
>>> give_me_a_yugo.exclaim()
#I'm a Car!

특별히 아무것도 하지 않고, Yugo는 Car로부터 exclaim() 메소드를 상속받았습니다. Yugo는 자신이 Car라고 말하고 있습니다. Yugo의 정체성이 불분명해 보입니다. 다음 절에서는 이것으로 무엇을 할 수 있는지 살펴보겠습니다.

 

4. 메소드 오버라이드

 

방금 본 것처럼 새 클래스는 먼저 부모 클래스로부터 모든 것을 상속받습니다. 좀 더 나아가서 부모 메소드를 어떻게 대체 혹은 오버라이드(Override)하는지 살펴볼 것입니다. Yugo는 아마도 어떤 식으로든 Car와 달라야 합니다. 만약 그렇지 않다면 새로운 클래스를 정의하는 이유를 생각해봐야합니다. Yugo에 대한 exclaim() 메소드를 어떻게 바꾸는지 살펴봅시다.

>>> class Car():
	def exclaim(self):
		print("I'm a Car!")

>>> class Yugo(Car):
	def exclaim(self):
		print("I'm a Yugo!")

두 클래스로부터 각각 객체를 생성합니다.

>>> give_me_a_car = Car()
>>> give_me_a_yugo = Yugo()

각 객체의 exclaim() 메소드를 호출해봅시다.

>>> give_me_a_car.exclaim()
#I'm a Car!
>>> give_me_a_yugo.exclaim()
#I'm a Yugo!

이 코드에서는 exclaim() 메소드를 오버라이드했습니다. 우리는 __init__() 메소드를  포함한 모든 메소드를 오버라이드할 수 있습니다. 아래에 앞에서 사용한 Person 클래스의 또 다른 코드가 있습니다. 의사를 나타내는 MDPerson과 변호사를 나타내는 JDPerson 서브클래스를 만들어봅시다.

>>> class Person():
	def __init__(self, name):
		self.name = name

		
>>> class MDPerson(Person):
	def __init__(self, name):
		self.name = "Doctor " + name

		
>>> class JDPerson(Person):
	def __init__(self, name):
		self.name = name + ", Esquire"

이러한 경우 __init__() 초기화 메소드는 부모 클래스의 Person과 같은 인자를 취하지만, 객체의 인스턴스 내부에서는 다른 name 값을 저장합니다.

>>> person = Person('Fudd')
>>> doctor = MDPerson('Fudd')
>>> lawyer = JDPerson('Fudd')
>>> print(person.name)
#Fudd
>>> print(doctor.name)
#Doctor Fudd
>>> print(lawyer.name)
#Fudd, Esquire

 

5. 메소드 추가하기

 

자식 클래스는 또한 부모 클래스에 없는 메소드를 추가할 수 있습니다. Car 클래스와 Yugo 클래스로 돌아가서 Yugo 클래스에만 있는 새로운 메소드 need_a_push()를 정의해봅시다.

>>> class Car():
	def exclaim(self):
		print("I'm a Car!")

		
>>> class Yugo(Car):
	def exclaim(self):
		print("I'm a Yugo!")
	def need_a_push(self):
		print("A little help here?")

이제 Car와 Yugo 객체를 생성합니다.

>>> give_me_a_car = Car()
>>> give_me_a_yugo = Yugo()

Yugo 객체는 need_a_push() 메소드 호출에 대답할 수 있습니다.

>>> give_me_a_yugo.need_a_push()
#A little help here?

Yugo는 Car가 하지 못하는 뭔가를 할 수 있으며, Yugo의 독특한 개성을 나타낼 수 있습니다.

 

6. 부모에게 도움 받기: super

 

앞에서 자식 클래스에 부모 클래스에 없는 메소드를 추가하거나, 부모 클래스의 메소드를 오버라이드하는 방법을 살펴봤습니다. 자식 클래스에서 부모 클래스의 메소드를 호출하고 싶다면 어떻게 해야할까요? super() 메소드를 사용하면 됩니다. 이메일 주소를 가진 Person 클래스를 나타내는 EmailPerson이라는 새로운 클래스를 정의해봅시다. 먼저 Person 클래스의 정의는 다음과 같습니다.

>>> class Person():
	def __init__(self, name):
		self.name = name

서브 클래스의 __init__() 메소드에 email 매개변수가 추가되었습니다.

>>> class EmailPerson(Person):
	def __init__(self, name, email):
		super().__init__(name)
		self.email = email

자식 클래스에서 __init__() 메소드를 정의하면 부모 클래스의 __init__() 메소들르 대체하는 것이기 때문에 더 이상 자동으로 부모 클래스의 __init__() 메소드가 호출되지 않습니다. 그러므로 이것을 명시적으로 호출해야 합니다. 코드에서 무슨 일이 일어나는지 살펴봅시다.

  • super() 메소드는 부모 클래스(Person)의 정의를 얻는다.

  • __init__() 메소드는 Person.__init__() 메소드를 호출합니다. 이 메소드는 self 인자를 슈퍼 클래스로 전달하는 역할을 합니다. 그러므로 슈퍼 클래스에 어떤 선택적 인자를 제공하기만 하면 도비니다. 이 경우 Person()에서 받는 인자는 name입니다.

  • self.email = email은 EmailPerson 클래스를 Person 클래스와 다르게 만들어주는 새로운 코드입니다.

그럼 EmailPerson 객체를 만들어봅시다.

>>> bob = EmailPerson('Bob Frapples', 'bob@frapples.com')

우리는 name과 email 속성에 접근할 수 있습니다.

>>> bob.name
#'Bob Frapples'
>>> bob.email
#'bob@frapples.com'

왜 자식 클래스에서 다음과 같이 정의하지 않았을까요?

>>> class EmailPerson(Person):
	def __init__(self, name, email):
		self.name = name
		self.email = email

물론 이와 같이 정의할 수는 있지만, 상속을 사용할 수 없게 됩니다. 일반 Person 객체와 마찬가지로 Person 클래스를 활용하기 위해 super()를 사용했습니다. super() 메소드 사용에 대한 또 다른 이점이 있습니다. 만약 Person 클래스의 정의가 나중에 바뀌면 Person 클래스로부터 상속받은 EmailPerson 클래스의 속성과 메소드에 변경사항이 반영됩니다.

자식 클래스가 자신의 방식으로 뭔가를 처리하지만, 아직 부모 클래스로부터 뭔가를 필요로 할 때(현실에서의 부모/자식처럼)는 super() 메소드를 사용합니다.

 

7. 자신: self

 

(공백 사용 외에) 파이썬에 대한 또 다른 비판은 인스턴스 메소드의 첫 번째 인자로 self를 포함해야 한다는 것입니다(이전 코드에서 봤던 메소드의 종류). 파이썬은 적잘한 객체의 속성과 메소드를 찾기 위해 self 인자를 사용합니다. 다음 코드를 통해 객체의 메소드를 어떻게 호출하고, 파이썬에서 실제로 은밀하게 무엇을 처리하는지 살펴봅시다.

이전 코드의 Car 클래스를 기억하십니까? exclaim() 메소드를 다시 호출해봅시다.

>>> car = Car()
>>> car.exclaim()
#I'm a Car!

파이썬이 은밀하게 처리하는 일은 다음과 같습니다.

  • car 객체의 Car 클래스를 찾는다.

  • car 객체를 Car 클래스의 exclaim() 메소드의 self 매개변수에 전달합니다.

그냥 재미를 위해 다음과 같은 방법으로 메소드를 실행할 수 있습니다. 이것은 일반 car.exclaim() 구문과 똑같이 동작합니다.

>>> Car.exclaim(car)
#I'm a Car!

하지만 이렇게 더 긴 문장을 사용할 이유는 없습니다.

 

8. get/set 속성값과 프로퍼티

 

어떤 객체 지향 언어에서는 외부로부터 바로 접근할 수 없는 private 객체 속성을 지원합니다. 프로그래머는 private 속성의 값을 읽고 쓰기 위해 getter 메소드와 setter 메소드를 사용합니다.

파이썬에서는 getter나 setter 메소드가 필요 없습니다. 왜냐하면 모든 속성과 메소드는 public이고, 우리가 예상한대로 쉽게 동작하기 때문입니다. 만약 속성에 직접 접근하는 것이 부담스럽다면 getter와 setter 메소드를 작성할 수 있습니다. 그러나 파이썬스럽게 프로퍼티(Propertty)를 사용합시다.

이 코드에서는 hidden_name이라는 속성으로 Duck 클래스를 정의합니다(다음 절에서 속성이 private를 유지하도록 이름을 짓는 더 좋은 방법을 배울 것입니다). 이 속성을 외부엣 직접 접근하지 못하도록 getter(get_name())와 setter(set_name()) 메소드를 정의합니다. 각 메소드가 언제 호출되는지 알기 위해 print() 문을 추가합니다. 마지막으로 이 메소드들을 name 속성의 프로퍼티로 정의합니다.

>>> class Duck():
	def __init__(self, input_name):
		self.hidden_name = input_name
	def get_name(self):
		print('Inside the getter')
		return self.hidden_name
	def set_name(self, input_name):
		print('Inside the setter')
		self.hidden_name = input_name
	name = property(get_name, set_name)

새 메소드들은 마지막 라인 전까지 평범한 getter와 setter 메소드처럼 행동합니다. 마지막 라인에서 두 메소드를 name이라는 속성의 프로퍼티로 정의합니다. property()의 첫 번째 인자는 getter 메소드, 두 번째 인자는 setter 메소드입니다. 이제 Duck 객체의 name을 참조할 때 get_name() 메소드를 호출해서 hidden_name 값을 반환합니다.

>>> fowl = Duck('Howard')
>>> fowl.name
#Inside the getter
#'Howard'

여전히 보통의 getter 메소드처럼 get_name() 메소드를 직접 호출할 수 있습니다.

>>> fowl.get_name()
#Inside the getter
#'Howard'

name 속성에 값을 할당하면 set_name() 메소드를 호출합니다.

>>> fowl.name = 'Daffy'
#Inside the setter
>>> fowl.name
#Inside the getter
#'Daffy'

set_name() 메소드를 여전히 직접 호출할 수 있습니다

>>> fowl.set_name('Daffy')
#Inside the setter
>>> fowl.name
#Inside the getter
#'Daffy'

프로퍼티를 정의하는 또 다른 방법은 데코레이터를 사용하는 것입니다. 다음 코드는 두 개의 다른 메소드를 정의합니다. 각 메소드는 name() 이지만, 서로 다른 데코레이터를 사용합니다.

  • getter 메소드 앞에 @property 데코레이터를 쓴다.

  • setter 메소드 앞에 @name.setter 데코레이터를 쓴다.

다음은 데코레이터를 사용한 코드입니다.

>>> class Duck():
	def __init__(self, input_name):
		self.hidden_name = input_name
	@property
	def name(self):
		print('Inside the getter')
		return self.hidden_name
	@name.setter
	def name(self, input_name):
		print('Inside the setter')
		self.hidden_name = input_name

여전히 name을 속성처럼 접근할 수 있습니다. 하지만 get_name() 메소드와 set_name() 메소드가 보이지 않습니다.

>>> fowl = Duck('Howard')
>>> fowl.name
#Inside the getter
#'Howard'
>>> fowl.name = 'Donald'
#Inside the setter
>>> fowl.name
#Inside the getter
#'Donald'

참고로, 어떤 사람이 hidden_name 속성을 알고 있다면, 그들은 fowl.hidden_name으로 이 속성을 바로 읽고 쓸 수 있습니다. 다음 절에서 private 속성의 이름을 짓는 특수한 방법을 배울 것입니다.

이전 코드에서 객체에 저장된 속성(hidden_name)을 참조하기 위해 name 프로퍼티를 사용했습니다. 또한 프로퍼티는 계산된 값을 참조할 수 있습니다. radius 속성과 계산된 diameter 프로퍼티를 가진 circle 클래스를 정의해봅시다.

>>> class Circle():
	def __init__(self, radius):
		self.radius = radius
	@property
	def diameter(self):
		return 2 * self.radius

radius 속성의 초깃값으로 Circle 객체를 만듭니다.

>>> c = Circle(5)
>>> c.radius
#5

radius와 같은 속성(Attribute)처럼 diameter를 참조할 수 있습니다.

>>> c.diameter
#10

재미있는 점은 radius 속성을 언제든지 바꿀 수 있다는 것입니다. 그리고 diameter 프로퍼티는 현재 radius 값으로부터 계산됩니다.

>>> c.radius = 7
>>> c.diameter
#14

속성에 대한 setter 프로퍼티를 명시하지 않는다면 외부로부터 이 속성을 설정할 수 없습니다. 이것은 읽기 전용(Read-Only) 속성입니다.

>>> c.diameter = 20
#Traceback (most recent call last):
#  File "<pyshell#164>", line 1, in <module>
#    c.diameter = 20
#AttributeError: can't set attribute

직접 속성을 접근하는 것보다 프로퍼티를 통해서 접근하면 큰 이점이 있습니다. 예를 들어 만약 속성의 정의를 바꾸려면 모든 호출자를 수정할 필요 없이 클래스 정의에 있는 코드만 수정하면 됩니다.

 

9. private 네임 맹글링

 

이전 절의 Duck 클래스 코드에서 (완전하지 않지만) 숨겨진 hidden_name 속성을 호출했습니다. 파이썬은 클래스 정의 외부에서 볼 수 없도록 하는 속성에 대한 네이밍 컨벤션(Naming Convention)이 있습니다. 속성 이름 앞에 두 언더스코어(__)를 붙이면 됩니다.

다음과 같이 hidden_name을 __name으로 바꿔봅시다.

>>> class Duck():
	def __init__(self, input_name):
		self.__name = input_name
	@property
	def name(self):
		print('Inside the getter')
		return self.__name
	@name.setter
	def name(self, input_name):
		print('Inside the setter')
		self.__name = input_name

우리가 작성한 코드가 잘 작동하는지 살펴봅시다.

>>> fowl = Duck('Howard')
>>> fowl.name
#Inside the getter
#'Howard'
>>> fowl.name = 'Donald'
#Inside the setter
>>> fowl.name
#Inside the getter
#'Donald'

아무런 문제가 없어 보입니다. 그러나 __name 속성을 바로 접근할 수 없습니다.

>>> fowl.__name
#Traceback (most recent call last):
#  File "<pyshell#181>", line 1, in <module>
#    fowl.__name
#AttributeError: 'Duck' object has no attribute '__name'

이 네이밍 컨벤션은 속성을 private로 만들지 않지만, 파이썬은 이 속성이 우연히 외부 코드에서 발견할 수 없도록 이름을 맹글링(Mangling)했습니다. 이것이 궁금하다면 다음 코드를 살펴봅시다.

>>> fowl._Duck__name
#'Donald'

inside the getter를 출력하지 않았습니다. 비록 이것이 속성을 완벽하게 보호할 수는 없지만, 네임 맹글링은 속성의 의도적인 직접 접근을 어렵게 만듭니다.

 

10. 메소드 타입

 

어떤 데이터(속성)와 함수(메소드)는 클래스 자신의 일부고, 어떤 것은 클래스로부터 생성된 객체의 일부입니다.

클래스 정의에서 메소드의 첫 번째 인자가 self라면 이 메소드는 인스턴스 메소드(Instance Method)입니다. 이것은 일반적인 클래스를 생성할 때의 메소드 타입입니다. 인스턴스 메소드의 첫 번째 매개변수는 self고, 파이썬은 이 메소드를 호출할 때 객체를 전달합니다.

이와 반대로 클래스 메소드(Class Method)는 클래스 전체에 영향을 미칩니다. 클래스에 대한 어떤 변화는 모든 객체에 영향을 미칩니다. 클래스 정의에서 함수에 @classmethod 데코레이터가 있다면 이것은 클래스 메소드입니다. 또한 이 메소드의 첫 번째 매개변수는 클래스 자신입니다. 파이썬에서는 보통 이 클래스의 매개변수를 cls로 씁니다. class는 예약어기 때문에 사용할 수 없습니다. A 클래스에서 객체 인스턴스가 얼마나 만들어졌는지에 대한 클래스 메소드를 정의해봅시다.

>>> class A():
	count = 0
	def __init__(self):
		A.count += 1
	def exclaim(self):
		print("I'm an A!")
	@classmethod
	def kids(cls):
		print("A has", cls.count, "little objects.")

		
>>> easy_a = A()
>>> breezy_a = A()
>>> wheezy_a = A()
>>> A.kids()
#A has 3 little objects.

self.count(객체 인스턴스 속성)를 참조하기보다 A.count(클래스 속성)를 참조했습니다. kids() 메소드에서 A.count를 사용할 수 있었지만 cls.count를 사용했습니다.

클래스 정의에서 메소드의 세 번째 타입은 클래스나 객체에 영향을 미치지 못합니다. 이 메소드는 단지 편의를 위해 존재합니다. 정적 메소드는 @staticmethod 데코레이터가 붙어있고, 첫 번째 매개변수로 self나 cls가 없습니다. CoyoteWeapon 클래스의 commercial 메소드 코드를 살펴봅시다.

>>> class CoyoteWeapon():
	@staticmethod
	def commercial():
		print('This CoyoteWeapon has been brought to you by D3C1')

		
>>> CoyoteWeapon.commercial()
#This CoyoteWeapon has been brought to you by D3C1

이 메소드를 접근하기 위해 CoyoteWeapon 클래스에서 객체를 생성할 필요가 없습니다.

 

11. 덕 타이핑

 

파이썬은 다형성(Polymorphism)을 느슨하게 구현했습니다. 이것은 클래스에 상관없이 같은 동작을 다른 객체에 적용할 수 있다는 것을 의미합니다.

세 Quote 클래스에서 같은 __init__() 이니셜라이저를 사용해봅시다. 클래스에 다음 두 메소드를 추가합니다.

  • who() 메소드는 저장된 person 문자열의 값을 반환합니다.

  • says() 메소드는 특정 구두점과 함께 저장된 words 문자열을 반환합니다.

다음과 같이 구현합시다.

>>> class Quote():
	def __init__(self, person, words):
		self.person = person
		self.words = words
	def who(self):
		return self.person
	def says(self):
		return self.words + '.'

	
>>> class QuestionQuote(Quote):
	def says(self):
		return self.words + '?'

	
>>> class ExclamationQuote(Quote):
	def says(self):
		return self.words + '!'

QuestionQuote와 ExclamationQuote 클래스에서 초기화 함수를 쓰지 않았습니다. 그러므로 부모의 __init__() 메소드를 오버라이드하지 않습니다. 파이썬은 자동으로 부모 클래스 Quote의 __init__() 메소드를 호출해서 인스턴즈 변수 person과 words를 저장합니다. 그러므로 서브클래스 QuestionQuote와 ExclamationQuote에서 생성된 객체의 self.words에 접근할 수 있습니다.

객체를 만들어서 결과를 살펴봅시다.

>>> hunter = Quote('Elmer Fudd', "I'm hunting wabbits")
>>> print(hunter.who(), 'says:', hunter.says())
#Elmer Fudd says: I'm hunting wabbits.
>>> hunted1 = QuestionQuote('Bugs Bunny', "What's up, doc")
>>> print(hunted1.who(), 'says:', hunted1.says())
#Bugs Bunny says: What's up, doc?
>>> hunted2 = ExclamationQuote('Daffy Duck', "It's rabbit season")
>>> print(hunted2.who(), 'says:', hunted2.says())
#Daffy Duck says: It's rabbit season!

세 개의 서로 다른 says() 메소드는 세 클래스에 대해 서로 다른 동작을 제공합니다. 이것은 객체 지향 언어에서 전통적인 다형성의 특징입니다. 더 나아가 파이썬은 who()와 says() 메소드를 갖고 있는 모든 객체에서 이 메소드를 실행할 수 있게 해줍니다. Quote 클래스와 관계 없는 BabblingBrook 클래스를 정의해봅시다.

>>> class BabblingBrook():
	def who(self):
		return 'Brook'
	def says(self):
		return 'Babble'

	
>>> brook = BabblingBrook()

다양한 객체의 who()와 says() 메소드를 실행해봅시다. brook 객체는 다른 객체와 전혀 관계가 없습니다.

>>> def who_says(obj):
	print(obj.who(), 'says', obj.says())


>>> who_says(hunter)
#Elmer Fudd says I'm hunting wabbits.
>>> who_says(hunted1)
#Bugs Bunny says What's up, doc?
>>> who_says(hunted2)
#Daffy Duck says It's rabbit season!
>>> who_says(brook)
#Brook says Babble

예전부터 이러한 행위를 덕 타이핑(Duck Typing)이라고 불렀습니다.

 

12. 특수 메소드

 

우리는 기본적인 객체를 생성하고 사용할 수 있습니다. 이제 좀 더 깊이 들어가 봅시다.

a = 3 + 8과 같은 뭔가를 입력했을 때, 값 3과 8이 정수 객체고, + 기호로 더하라는 것을 어떻게 알까요? 또한 = 기호를 사용하여 어떻게 결과를 얻을까요? 파이썬의 특수 메소드를 사용하면 이러한 연산자를 사용할 수 있습니다.

이 메소드의 이름은 두 언더스코어(__)로 시작하고 끝납니다. 우리는 이미 __init__() 메소드를 봤습니다. 이 메소드는 클래스의 정의로부터 생성된 새로운 객체를 초기화하고, 어떤 인자를 전달받습니다.

간단한 Word 클래스와 두 단어를 비교(대소문자 무시)하는 equals() 메소드가 있다고 가정합시다. 즉, 'ha'와 'HA'는 같은 단어로 간주합니다.

다음의 첫 번째 코드는 평범한 메소드 equal()을 사용합니다. self.text는 Word 객체의 문자열입니다. 그리고 equals() 메소드는 text와 word2(다른 Word 객체)의 텍스트 문자열을 비교합니다.

>>> class Word():
	def __init__(self, text):
		self.text = text
	def equals(self, word2):
		return self.text.lower() == word2.text.lower()

그러고 나서 세 개의 서로 다른 텍스트 문자열로 Word 객체를 생성합니다.

>>> first = Word('ha')
>>> second = Word('HA')
>>> third = Word('eh')

문자열 'ha'와 'HA'를 소문자로 바꾸면 이 둘은 똑같습니다.

>>> first.equals(second)
#True

문자열 'eh'와 'ha'는 다릅니다.

>>> first.equals(third)
#False

문자열을 소문자로 바꿔서 비교하는 equal() 메소드를 정의했습니다. 파이썬의 내장된 타입처럼 first == second와 같이 비교했다면 좋을 것입니다. 이렇게 구현해봅시다. equal() 메소드를 특수 이름의 __eq__() 메소드로 바꿔봅시다.

>>> class Word():
	def __init__(self, text):
		self.text = text
	def __eq__(self, word2):
		return self.text.lower() == word2.text.lower()

코드가 잘 동작하는지 살펴봅시다.

>>> first = Word('ha')
>>> second = Word('HA')
>>> third = Word('eh')
>>> first == second
#True
>>> first == third
#False

__eq__()는 같은지 판별하는 파이썬의 특수 메소드 이름입니다. 아래의 표에 유용한 특수 메소드의 이름이 나열되어 있습니다. 우선, 비교 연산을 위한 특수 메소드입니다.

__eq__(self, other)

self == other

__ne__(self, other)

self != other

__lt__(self, other)

self < other

__gt__(self, other)

self > other

__le__(self, other)

self <= other

__ge__(self, other)

self >= other

다음은, 산술 연산을 위한 특수 메소드입니다.

__add__(self, other)

self + other

__sub__(self, other)

self - other

__mul__(self, other)

self * other

__floordiv__(self, other)

self // other

_truediv__(self, other)

self / other

__mod__(self, other)

self % other

__pow__(self, other)

self ** other

+(__add__() 특수 메소드)와 -(__sub__() 특수 메소드) 같은 산술 연산자의 사용에는 제한이 없습니다. 예를 들어 파이썬의 문자열 객체는 연결(Concatenation)을 위해 + 연산자를 쓰고, 반복(Duplication)을 위해 * 연산자를 씁니다. 많은 특수 메소드의 이름이 파이썬 공식 웹 페이지에 문서화 되어 있습니다. 그 외 일반적인 메소드는 다음을 참조하세요.

__str__(self)

str(self)

__repr__(self)

repr(self)

__len__(self)

len(self)

__init__() 외에도 __str__()을 사용하여 객체를 문자열로 출력하는 우리만의 메소드를 만들 수 있습니다. 추후 작성될 게시글에서 배울 문자열 포매터(String Formatter)와 print(), str()을 사용하면 됩니다. 대화식 인터프리터는 변수의 결과를 출력하기 위해 __repr__() 함수를 사용합니다. __str__() 또는 __repr__()을 정의하지 않으면 객체의 기본 문자열을 출력합니다.

>>> first = Word('ha')
>>> first
#<__main__.Word object at 0x0000024FFFC74DA0>
>>> print(first)
#<__main__.Word object at 0x0000024FFFC74DA0>

__str__()과 __repr__() 메소드를 추가하여 Word 클래스를 예쁘게 만들어봅시다.

>>> class Word():
	def __init__(self, text):
		self.text = text
	def __eq__(self, word2):
		return self.text.lower() == word2.text.lower()
	def __str__(self):
		return self.text
	def __repr__(self):
		return "Word('" + self.text + "')"

특수 메소드에 대해 좀 더 알고 싶다면, 위에서 언급한 파이썬 문서를 참고하세요.

 

13. 컴포지션

 

자식 클래스가 부모 클래스처럼 행동하고 싶을 때, 상속은 좋은 기술입니다. 프로그래머는 정교한 상속 계층구조의 사용에 유혹될 수 있지만, 컴포지션(Composition) 또는 어그리게이션(Aggregation)의 사용이 더 적절한 경우가 있습니다. 오리는 조류이지만, 꼬리를 갖고 있습니다. 꼬리는 오리에 속하지 않지만 오리의 일부입니다. 다음 예제에서는 bill와 tail 객체를 만들어서 새로운 duck 객체에 부여합니다.

>>> class Bill():
	def __init__(self, description):
		self.description = description

		
>>> class Tail():
	def __init__(self, length):
		self.length = length

		
>>> class Duck():
	def __init__(self, bill, tail):
		self. bill = bill
		self.tail = tail
	def about(self):
		print('This duck has a', self.bill.description, 'bill and a', self.tail.length, 'tail')

		
>>> tail = Tail('long')
>>> bill = Bill('wide orange')
>>> duck = Duck(bill, tail)
>>> duck.about()
#This duck has a wide orange bill and a long tail

 

14. 클래스와 객체, 그리고 모듈을 사용해야 할 때

 

코드에서 클래스와 모듈의 사용 기준은 다음과 같습니다.

  • 비슷한 행동(메소드)을 하지만 내부 상태(속성)가 다른 개별 인스턴스가 필요할 때, 객체는 매우 유용합니다.

  • 클래스는 상속을 지원하지만, 모듈은 상속을 지원하지 않습니다.

  • 어떤 한 가지 일만 수행한다면 모듈이 가장 좋은 선택일 것입니다. 프로그램에서 파이썬 모듈이 참조된 횟수에 상관 없이 단 하나의 복사본만 불러옵니다(자바와 C++ 프로그래머를 위해 설명하자면, 파이썬 모듈을 싱글턴(Singleton) 처럼 쓸 수 있습니다).

  • 여러 함수에 인자로 전달될 수 있는 여러 값을 포함한 여러 변수가 있다면, 클래스를 정의하는 것이 더 좋습니다. 예를 들어 화상 이미지를 나타내기 위해 size나 color를 딕셔너리의 키로 사용한다고 가정해보겠습니다. 프로그램에서 각 이미지에 대한 딕셔너리를 생성하고, scale()과 transform() 같은 함수에 인자를 전달할 수 있습니다. 키와 함수를추가하면 코드가 지저분해질 수도 있습니다. size와 color를 속성으로 하고 scale()과 transform()을 메소드로 하는 이미지 클래스를 정의하는 것이 더 일관성 있습니다. 색상 이미지에 대한 모든 데이터와 메소드를 한 곳에 정의할 수 있기 때문입니다.

  • 가장 간단한 문제 해결 방법을 사용합니다. 딕셔너리, 리스트, 튜플은 모듈보다 더 작고, 간단하며, 빠릅니다. 그리고 일반적으로 모듈은 클래스보다 더 간단합니다.

자료구조를 과하게 엔지니어링하는 것을 피하세요. 객체보다 튜플이 더 낫습니다(네임드 튜플을 써보세요). getter/setter 함수보다 간단한 필드(Field)가 더 낫습니다. ...(중략)... 내장된 데이터 타입은 우리의 친구입니다. 숫자, 문자열, 튜플, 리스트, 셋, 딕셔너리를 사용하세요. 또한 데크와 같은 컬렉션 라이브러리를 활용하세요.
- 귀도 반 로섬(Guido van Rossum)

 

14.1. 네임드 튜플

 

귀도 반 로섬이 조금 전에 네임드 튜플(Named Tuple)을 언급했기 대문에, 여기서 네임드 튜플을 다뤄봅시다. 네임드 튜플은 튜플의 서브클래스입니다. 이름(.name)과 위치([offset])로 값에 접근할 수 있습니다.

이전 절의 코드를 활용하겠습니다. Duck 클래스를 네임드 튜플로, bill과 tail을 간단한 문자열 속성으로 변환합니다. 그리고 두 인자를 취하는 namedtuple 함수를 호출합니다.

  • 이름

  • 스페이스로 구분된 필드 이름의 문자열

파이썬에서 네임드 튜플은 자동으로 지원되지 않습니다. 그래서 네임드 튜플을 쓰기 전에 모듈을 불러와야 합니다. 다음 예제의 첫 번째 줄에서 namedtuple을 불러오고 있습니다.

>>> from collections import namedtuple
>>> Duck = namedtuple('Duck', 'bill tail')
>>> duck = Duck('wider orange', 'long')
>>> duck
#Duck(bill='wider orange', tail='long')
>>> duck.bill
#'wider orange'
>>> duck.tail
#'long'

또한 딕셔너리에서 네임드 튜플을 만들 수 있습니다.

>>> parts = {'bill': 'wide orange', 'tail': 'long'}
>>> duck2 = Duck(**parts)
>>> duck2
#Duck(bill='wide orange', tail='long')

**parts는 키워드 인자(Keyword Argument)입니다. parts 딕셔너리에서 키와 값을 추출하여 Duck()의 인자로 제공합니다. 다음 코드와 효과가 같습니다.

>>> duck2 = Duck(bill = 'wide orange', tail = 'long')

네임드 튜플은 불변합니다. 하지만 필드를 바궈서 도 다른 네임드 튜플을 반환할 수 있습니다.

>>> duck3 = duck2._replace(tail = 'magnificent', bill = 'crushing')
>>> duck3
#Duck(bill='crushing', tail='magnificent')

duck을 딕셔너리로 정의합니다.

>>> duck_dict = {'bill': 'wide orange', 'tail': 'long'}
>>> duck_dict
#{'bill': 'wide orange', 'tail': 'long'}

딕셔너리에 필드를 추가합니다.

>>> duck_dict['color'] = 'green'
>>> duck_dict
#{'bill': 'wide orange', 'tail': 'long', 'color': 'green'}

딕셔너리는 네임드 튜플이 아닙니다.

>>> duck.color = 'green'
#Traceback (most recent call last):
#  File "<pyshell#328>", line 1, in <module>
#    duck.color = 'green'
#AttributeError: 'Duck' object has no attribute 'color'

정리하면, 네임드 튜플의 특징은 다음과 같습니다.

  • 불변하는 객체처럼 행동한다.

  • 객체보다 공간 효율성과 시간 효율성이 더 좋다.

  • 딕셔너리 형식의 괄호([]) 대신, 점(.) 표기법으로 속성을 접근할 수 있다.

  • 네임드 튜플을 딕셔너리의 키처럼 쓸 수 있다.