etc/introducing python: 처음 시작하는 파이썬

6장: 객체와 클래스

seul chan 2017. 1. 18. 14:56

6.1 객체란 무엇인가?

앞 장들에서 본 것처럼, 숫자에서 모듈까지 파이썬의 모든 것은 '객체'(object)이다. 하지만 파이썬은 특수 구문을 활용하여 대부분의 객체를 숨긴다. num = 7 을 입력했을 때 7이 담긴 정수 타입의 객체를 생성하고, 객체 참조 (object reference)를 num에 할당한다.

객체는 데이터(변수, attribute, 속성)와 코드(함수, method)를 모두 포함한다. 즉, 객체는 어떤 구체적인 것의 유일한 인스턴스(instance)를 나타낸다.

아무도 생성하지 않은 새로운 객체를 생성할 때에는 무엇을 포함하고 있는짖 가리키는 클래스를 생성해야 한다.

객체를 명사, 메서드를 동사로 생각해보자. 객체는 각각의 사물을 나타내고, 메서드는 다른 사물과 어떻게 상호작용 하는지를 정의한다.

모듈과 달리, 객체는 각각 다른 값을 가진 속성의 객체를 동시에 여러 개 생성할 수 있다.

6.2 클래스 선언하기: class

2장에서는 객체를 플라스틱 박스에 비유했다. 클래스는 박스를 만드는 틀에 비유할 수 있다. 예를 들어 String은 'cat'과 같은 문자열 객체(박스)를 만드는 내장된 클래스이다. 파이썬에는 list, dict를 포함한 많은 내장 클래스가 있다. 먼저 class 키워드로 클래스를 정의하여 커스텀 객체를 생성한다.

사람에 대한 정보를 나타내는 객체를 정의한다고 가정한다.

  • 각 객체는 한 사람을 나타낸다.
  • 객체의 틀로 Person 클래스를 정의한다. (d우선 pass를 통해 빈 클래스를 나타낸다)
In [1]:
# 6.2 예시
class Person():
    pass

# 함수처럼 클래스 이름을 호출하여 클래스에서 객체를 생성할 수 있다.
someone = Person()
# Person()은 Person 클래스로부터 개별 객체를 생성하고, someone 변수에 이 객체를 할당한다. 
  • Person()은 Person 클래스로부터 개별 객체를 생성하고, someone 변수에 이 객체를 할당한다.
  • 그러나 Person 클래스는 빈 클래스이기 때문에 someone 객체만 존재할 뿐 아무것도 없다.

이번 예시는 특별한 파이썬 객체 초기화 메서드인 init를 포함시킨다.

In [4]:
class Person():
    def __init__(self):
        pass
  • 이것은 진짜 파이썬 클래스의 정의이다.
  • init()는 특별한 메서드로 클래스의 정의로부터 객체를 초기화한다. (???)
  • self 인자는 객체 자신을 가르킨다.

    클래스에서 init를 정의할 때 첫 번 째 매개변수는 self여야 한다.

  • 세 번째에서는 실제로 뭔가를 하는 간단한 객체를 생성해보겠다.

  • 이번에는 name 매개변수를 초기화 메서드에 추가한다.
In [10]:
# 3
class Person():
    def __init__(self, name):
        self.name = name
# name 매개 변수에 문자열을 전달하여 Person 클래스에서 객체를 생성할 수 있다.
In [13]:
hunter = Person('Elmer Fudd')

위 코드가 어떻게 동작하는지 살펴보자

  • Person 클래스의 정의를 찾는다
  • 새 객체를 메모리에 초기화(생성)한다.
  • 객체의 init 메서드를 호출한다. 새롭게 생성된 객체를 self에 전달하고 인자(Elmer Fudd)를 name에 전달한다.
  • 객체에 name 값을 저장한다
  • 새로운 객체를 반환한다
  • hunter에 이 객체를 연결한다

전달한 name 값은 객체의 속성(attribute)에 저장되어 있다. 이 속성은 직접 읽고 쓸 수 있다.

In [14]:
print(hunter.name)
Elmer Fudd

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

모든 클래스에서 init 메서드를 가질 필요는 없다. init는 같은 클래스에서 생성된 다른 객체를 구분하기 위해 필요한 뭔가를 수행하는 역할을 한다.

6.3 상속

어떤 문제를 해결하려고 할 때는 우선 필요한 기능을 수행하는 기존 클래스를 찾고, 여기에 필요한 기능을 추가할 것이다. 이를 새로 수정하려고 하면 많은 문제가 발생할것이다.

이 문제는 상속(inheritance)으로 해결할 수 있다. 기존 클래스에서 일부를 추가하거나 변경하여 새 클래스를 생성하는데 기존 클래스를 복사하지 않고 모든 코드를 쓸 수 있다.

필요한 것만 추가/변경하여 새 클래스를 정의하는데, 이는 기존 클래스의 behavior를 override(재정의) 하는 것이다.

  • 기존 클래스는 부모 클래스, 슈퍼 클래스, 베이스 클래스
  • 새 클래스는 자식 클래스, 서브 클래스, derived 클래스라고 부른다.

다음 예제에서 상속을 사용해보겠다.

In [15]:
class Car():
    pass
class Yugo(Car):
    pass

무슨 말인지 모르겠다. 자식 클래스 (Yugo)는 부모 클래스(Car)를 구체화 한 것이랜다. 실제로 뭔가를 하는 Car, Yugo의 새로운 클래스를 정의해보자

In [19]:
class Car():
    def exclaim(self):
        print("I'm a Car")
class Yugo(Car):
    pass
In [20]:
# 클래스에서 객체를 만들고 exclaim 메서드를 호출한다.
give_me_a_car = Car()
give_me_a_yugo = Yugo()
give_me_a_car.exclaim()
I'm a Car
In [22]:
give_me_a_yugo.exclaim()
I'm a Car

특별히 아무것도 하지 않고 Yugo는 Car로부터 exclaim() 메서드를 상속받았다. Yugo의 정체성은 대체 무엇일까?? 이걸로 뭘 할수 있을까? 다음 절에서 알려준다...

6.4 메서드 오버라이드

방금 본것처럼 새 클래스는 먼저 부모 클래스로부터 모든 것을 상속받는다. 더 나아가서 부모 메서드를 어떻게 override 하는지 살펴볼 것이다.

Yugo에 대한 exclaim() 메서드를 어떻게 바꾸는지 살펴보자

In [4]:
class Car():
    def exclaim(self):
        print("I'I
    def exclaim(self):
        print("I'm a Yugo! Much like a Car, but more Yugo-ish")
In [5]:
# 두 클래스에서 각각 객체를 생성한다. 
give_me_a_car = Car()
give_me_a_yugo = Yugo()
In [6]:
# 각 객체의 exclaim 메서드를 호출한다.
give_me_a_car.exclaim()
give_me_a_yugo.exclaim()
I'm a car!
I'm a Yugo! Much like a Car, but more Yugo-ish

위 예제에서는 exclaim() 메서드를 오버라이드 했다. 앞서 만든 Person 클래스의 또다른 예제로 MDPerson 과 JDPerson 서브클래스를 만들어보자

In [8]:
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"
In [9]:
# 이 경우 __init__ 초기화 메서드는 부모 클래스의 Person과 같은 인자를 취하지만
# 객체의 인스턴트 내부에서는 다른 name 값을 저장한다.
person = Person('Fudd')
doctor = MDPerson('Fudd')
lawyer = JDPerson('Fudd')
print(person.name)
print(doctor.name)
print(lawyer.name)
Fudd
Doctor Fudd
Fudd, Esquire

6.5 메서드 추가하기

자식 클래스는 또한 부모 클래스에 없는 메서드를 추가할 수 있다. Yugo 클래스에만 있는 새로운 메서드인 need_a_push()를 정의해보자

In [10]:
class Car():
    def exclaim(self):
        print("I'm a car!")
class Yugo(Car):
    def exclaim(self):
        print("I'm a Yugo! Much like a car, but more yugo-ish")
    def need_a_push(self):
        print("A little help here?")
In [12]:
give_me_a_car = Car()
give_me_a_yugo = Yugo()

give_me_a_car.exclaim()
give_me_a_yugo.exclaim()
give_me_a_yugo.need_a_push()
I'm a car!
I'm a Yugo! Much like a car, but more yugo-ish
A little help here?

6.6 부모에게 도움 받기: super

자식 클래스에서 부모 클래스의 메서드를 호출하고싶다면 ?? super() 메서드를 사용하면 된다. (?? 도대체 무슨 말이지)

Person, EmailPerson 클래스 예제를 보면서 이해해보자...

In [15]:
class Person():
    def __init__(self, name):
        self.name = name
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 은 EamilPerson을 Person 클래스와 다르게 만들어주는 새로운 코드이다
In [16]:
# EamilPerson 객체를 만들어보자
bob = EmailPerson('Bob Frapples', 'bob@frapples.com')
In [18]:
# name, email 속성에 접근할 수 있다.
print(bob.name)
print(bob.email)
Bob Frapples
bob@frapples.com
In [19]:
# 자식 클래스에서 왜 다음과 같이 정의하지 않았을까?
# class EmailPerson(Person):
#     def __init__(self, name, email):
#         self.name = name
#         self.email = email

위와 같이 정의할 수 있지만, 상속을 사용할 수 없게 된다. 일반 Person 객체와 마찬가지로 Person 클래스를 활용하기 위해 super()를 사용했다.

만약 Person 클래스의 정의가 나중에 바뀌면 상속받은 EmailPerson 클래스의 속성, 메서드에 변경사항이 적용된다.

6.7 자신: self

파이썬은 적절한 객체의 속성과 메서드를 찾기 위해 self 인자를 사용한다.

이전 예제의 Car 클래스의 exlaim() 메서드를 사용하였을 때

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

  • car 객체의 Car 클래스를 찾는다
  • car 객체를 Car 클래스의 exclaim() 메서드의 self 매개변수에 전달한다.

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

이렇게 해도 똑같이 동작하지만, 이런 긴 문장을 사용할 이요가 없다..

6.8 get/set 속성값과 프로퍼티

파이썬의 모든 속성과 메서드는 public이고, 예상한대로 쉽게 동작한다. 만약 속성에 직접 접근하는 것이 부담스럽다면 getter, setter 메서드를 작성할 수 있다. 그러나 파이써닉하게 프로퍼티(property)를 사용하자. (???)

다음 예제에서는 hidden_name 이라는 속성으로 Duck 클래스를 정의한다. 이 속성을 외부에서 접근 못하도록 get_name, set_name 메서드를 정의하고 각 메서드가 언제 호출되는지 알기 위해 print()문을 추가하였다.

In [20]:
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)
In [21]:
fowl = Duck('Howard')
fowl.name
inside the getter
Out[21]:
'Howard'
In [22]:
fowl.get_name()
inside the getter
Out[22]:
'Howard'
In [23]:
fowl.name = 'Daffy'
inside the setter
In [24]:
fowl.name
inside the getter
Out[24]:
'Daffy'
In [25]:
fowl.set_name('Daffy')
inside the setter
In [26]:
fowl.name
inside the getter
Out[26]:
'Daffy'

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

  • getter 메서드 앞에 @property 데코레이터
  • setter 메서드 앞에 @name.setter 데코레이터
In [33]:
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
In [34]:
fowl = Duck('Howard')
fowl.name
inside the getter
Out[34]:
'Howard'
In [35]:
fowl.name = "Donald"
fowl.name
inside the setter
inside the getter
Out[35]:
'Donald'

어떤 객체에서 저장된 속성(hidden_name)을 참조하기 위해 name 프로퍼티를 사용했다. 또한 property는 계산된 값을 가질 수 있다.(???) radius 속성과 계산된 diameter property를 가진 circle class를 정의해보자

In [36]:
class Circle():
    def __init__(self, radius):
        self.radius = radius
    @property
    def diameter(self):
        return 2 * self.radius
In [38]:
c = Circle(5)
c.radius
Out[38]:
5
In [41]:
c.diameter
Out[41]:
10
In [42]:
c.radius = 7
c.diameter
Out[42]:
14

6.9 private 네임 맹글링 (??)

클래스 정의 외부에서 봅 수 없도록 하는 속성에 대한 naming convention:

  • 속성 이름 앞에 두 언더스코어 (__)를 붙이면 된다.

다음은 hidden_name 을 __name으로 바꾼 것이다.

In [1]:
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
In [2]:
fowl = Duck('Howard')
fowl.name
inside the getter
Out[2]:
'Howard'
In [3]:
fowl.name = 'Donald'
inside the setter
In [4]:
fowl.name
inside the getter
Out[4]:
'Donald'
In [6]:
fowl.__name
# name 속성은 private의 속성은 아니지만 외부 코드에서 발견할 수 없도롞
# 이름을 'mangling' 했다.
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-6-5330466a95ea> in <module>()
----> 1 fowl.__name
      2 # name 속성은 private의 속성은 아니지만 외부 코드에서 발견할 수 없도롞
      3 # 이름을 'mangling' 했다.

AttributeError: 'Duck' object has no attribute '__name'
In [7]:
fowl._Duck__name
Out[7]:
'Donald'

6.10 메서드 타입

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

클래스 정의에서 메서드의 첫번째 인자가 self라면, 이는 instance method이다. 일반적인 클래스를 생성할 때의 메서드 타입으로, 첫번째 매개변수는 self이고 파이썬은 이 메서드를 호출할 때 객체를 전달한다.

이와 반대로 클래스 메서드는 클래스 전체에 영향을 미친다. 클래스에 대한 어떤 변화는 모든 객체에 영향을 미친다. 클래스 정의에서 함수에 @classmethod 데코레이터가 있다면 이것은 클래스 메서드이다. 이 메서드의 첫 매개변수는 클래스 자신이다. 보통 cls로 쓴다.

In [8]:
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.")
In [11]:
easy_a = A()
breezy_a = A()
wheezy_a = A()
A.kids()
A has 7 little objects.

6.11 덕 타이핑

새 Quote 클래스에서 같은 init() 이니셜라이저를 사용해보겠다

  • who() 메서드는 저장된 person 문자열의 값을 반환
  • says() 메서드는 특정 구두점과 함께 저장된 words 문자열을 반환
In [12]:
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 + '!'
In [13]:
hunter = Quote('Elmer Fudd', "i'm hunting wabbits")
print(hunter.who(), hunter.says())
Elmer Fudd i'm hunting wabbits.
In [14]:
hunted1 = QuestionQuote('Bugs Bunny', "What's up, doc")
print(hunted1.who(), hunted1.says())
Bugs Bunny What's up, doc?
In [15]:
hunted2 = ExclamationQuote('Daffy Duck', "It's rabbit season")
print(hunted2.who(), hunted2.says())
Daffy Duck It's rabbit season!

세개 서로 다른 says() 메서드는 세 클래스에 서로 다른 동작 (. ? !)을 제공한다. 이는 객체 지향 언어에서 전통적인 다형성의 특징이다. 나아가 파이썬은 who(), sayas() 메서드를 가지고 있는 모든 객체에서 이 메서드를 시행할 수 있도록 해준다. Quote 클래스와 관계없는 BabblingBrook 클래스를 정의해보자

In [16]:
class BabblingBrook():
    def who(self):
        return 'Brook'
    def says(self):
        return 'Babble'
brook = BabblingBrook()
In [17]:
# 다양한 객체의 who(), says() 메서드를 실행해보자. 
# brook 객체는 다른 객체와 전혀 관계가 없다. (그래서??)
def who_says(obj):
    print(obj.who(), obj.says())
In [18]:
who_says(hunter)
Elmer Fudd i'm hunting wabbits.
In [19]:
who_says(hunted1)
Bugs Bunny What's up, doc?
In [20]:
who_says(brook)
Brook Babble
In [21]:
# 이러한 행위를 "Duck typing" 이라고 부른다. (더 찾아보자)

6.12 특수 메서드

파이썬의 특수 메서드를 사용하면 연산자들을 사용할 수 있다. 메서드의 이름은 언더스코어(__)로 시작하고 끝난다.

간단한 word 클래스와 두 단어를 비교하는 equals() 메소드가 있다고 가정하자.

다음의 첫번째 예제는 평범한 메서드 equal()을 사용한다. self.text는 Word 객체의 문자열이다. equals() 메서드는 text와 words2의 문자열을 비교한다. (??)

In [22]:
class Word():
    def __init__(self, text):
        self.text = text
    def equals(self, words):
        return self.text.lower() == words.text.lower()
In [23]:
first = Word('ha')
second = Word('HA')
third = Word('eh')
In [24]:
first.equals(second)
Out[24]:
True
In [25]:
first.equals(third)
Out[25]:
False

파이썬에 내장된 타입처럼 first == second와 같이 비교했다면 좋았을 것이다. equal() 메서드를 특수 이름의 eq() 메서드로 바꿔보자

In [36]:
class Word():
    def __init__(self, text):
        self.text = text
    def __eq__(self, words):
        return self.text.lower() == words.text.lower()
In [37]:
first = Word('ha')
second = Word('HA')
third = Word('eh')
In [38]:
first == second
Out[38]:
True
In [39]:
first == third
Out[39]:
False

eq()는 같은지 판별하는 특수 메서드 이름이다. 파이썬에는 다양한 특수 메서드들이 있다.

특수 메서드에 대해 좀 더 알기 위해서 파이썬 문서를 참고한다.

6.13 컴포지션

자식 클래스가 부모 클래스처럼 행동하고 싶을 때, 상속은 좋은 기술이다. (자식 is-a 부모) 하지만 컴포지션, 어그리게이션의 사용이 더 적절할 수 있다. (X has-a Y)

오리는 조류이지만 (오리 is_a 조류), 꼬리를 가지고 있다. (오리 has-a 꼬리)

다음 예제에서는 bill, tail 객체를 만들어서 duck 객체에 부여한다.

In [51]:
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')
In [52]:
tail = tail('long')
bill = bill('wide orange')
duck = Duck(bill, tail)
duck.about()
This duck has a wide orange bill and a long tail

6.14 클래스와 객체, 모듈은 언제 사용할까?

클래스, 모듈의 사용 기준은 다음과 같다

  • 비슷한 행동(메서드)을 하지만 내부 속성이 다른 개별 인스턴트가 필요할 때, 객체가 유용하다.
  • 클래스는 상속을 지원하고, 모듈은 지원하지 않는다.
  • 한 가지 일만 수행한다면 모듈이 가장 좋은 선택
  • 여러 함수에 인자로 전달될 수 있는 여러 값을 포함한 여러 변수가 있다면 클래스를 정의하는 것이 좋다.

6.14.1 네임드 튜플(named tuple)

네임드 튜플은 튜플의 서브클래스이다. 이름, 위치로 값에 접근할 수 있다.

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

  • 이름
  • 스페이스로 구분된 필드 이름의 문자열 파이썬에서 네임드 튜플은 자도으로 지원되지 않는다. 그래서 네임드 튜플을 쓰기 전에 모듈을 불러와야 한ㄷ나.
In [53]:
from collections import namedtuple
Duck = namedtuple('Duck', 'bill tail')
duck = Duck('wide orange', 'long')
duck
Out[53]:
Duck(bill='wide orange', tail='long')
In [54]:
duck.bill
Out[54]:
'wide orange'
In [55]:
duck.tail
Out[55]:
'long'
In [56]:
# 딕셔너리에서 네임드 튜플을 만들 수 있다.
parts = {
    'bill': 'wide orange', 
    'tail': 'long'
}
duck2 = Duck(**parts)
duck2
Out[56]:
Duck(bill='wide orange', tail='long')
In [ ]: