스터디/파이썬 스터디 강의자료

[3팀/김경은] 8주차 파이썬 스터디 - 객체 지향 프로그래밍

경은 2023. 5. 19. 08:33

데이터 과학을 위한 파이썬 프로그래밍 교재를 사용하여 작성한 강의자료입니다.

8차시_객체지향_강의안.pdf
0.94MB
8차시_객체지향_과제.pdf
3.02MB

  • (객체 지향 프로그래밍Object Oriented Programming, OOP)의 개념을 배우기 전에 이러한 개념을 왜 사용하는지에 대한 고민이 필요하다. 이것은 앞에서 함수를 배울 때처럼 여러 사람이 프로그램을 개발할 때 어떤 방법이 좋은지에 대한 고민과 같다. 조금 더 넓게 생각하면

💡 다른 사람이 작성한 코드를 어떻게 사용하면 좋을지에 대한 답이 바로 객체 지향 프로그래밍이다.

  • 객체 지향 프로그래밍은 내가 아니라 남이 만든 코드를 재사용하고 싶을 때 사용하는 대표적인 방법이 다. 함수처럼 어떤 기능을 함수 코드로 묶어 두는 것이 아니라, 어떤 기능을 수행하는 하나의 단일 프로그램을 객체라고 하는 코드로 만들어 다른 프로그래머가 재사용할 수 있도록 한다. 이것은 컴퓨터공학의 오래된 프로그래밍 기법 중 하나이다.
  • 객체와 클래스
    • 객체 지향 프로그래밍을 이해하기 위해서는 객체(object)와 클래스(class)의 개념을 알아야 한다.
    • 객체는 실생활에 존재하는 실제적인 물건 또는 개념을 뜻하며, 속성 (attribute)과 행동(action)으로 구성된다. 객체 지향 프로그래밍은 이러한 객체의 개념을 활용하여 프로그램으로 표현하는 기법이다. 속성은 변수(variable)로, 행동은 함수(function)로 정의된다.
    • 예를 들어. 인공지능 축구 프로그램을 만든다고 가정하자. 이 프로그램을 만들기 위해서는 어 떤 종류의 객체가 필요한지 생각해야 한다. 간단하게 EA스포츠의 FIFA 시리즈와 같은 게임을 생각해보자. 이 게임을 구성하는 단위로 심판, 선수, 팀이 있으며, 이러한 개념을 객체라고 할 수 있다. ‘선수’ 라는 객체의 경우 선수, 이름. 포지션, 소속팀 등이 하나의 속성이라고 할 수 있고, ‘공을 차다’, ‘패스하다’와 같은 개념은 행동으로 생각할 수 있다.
    개념 설명 예시
    객체 (object) 실생활에 존재하는 실제적인 물건 또는 개념 심판, 선수, 팀
    속성 (attribute) 객체가 가지고 있는 변수 선수의 이름, 포지션. 소속팀
    행동 (action) 객체가 실제로 작동시키는 함수, 메서드 공을 차다, 패스하다
  • 사실 객체는 하나의 프로그램에서 여러 개가 사용될 수도 있다. 따라서 객체들을 위한 설계도를 만들어야 한다.
  • 이를 프로그램에서는 클래스(class)라고 한다. 클래스는 객체가 가져야 할 기본 정보를 담은 코드로 일종의 설계도 코드라고 생각하면 된다.
  • 예를 들어, 축구 선수라는 객체를 만든다면 선수의 이름, 포지션, 소속팀 등은 기본적으로 가지고 있어야 할 속성이다. 이러한 정보를 클래스에 담고 실제 생성되는 객체에 는 이동국, 손흥민 같은 선수 이름을 할당한다.
  • 이렇게 해서 실제로 생성되는 객체를 인스턴스(instance)라고 한다.

붕어빵틀과 붕어빵

  • 붕어빵 가게에는 붕어빵틀이 있고, 이를 이용해 여러 종류의 붕어빵을 만들 수 있다. 이전에는 주로 팥빵과 크림빵 두 종류였다면 최근에는 커스터드나 블루베리 같은 다양한 종류의 붕어빵이 만들어지고 있다.
  • 즉, 잘 만든 붕어빵틀이 있다면 새로운 종류의 다양한 붕어빵을 만드는 것은 어렵지 않다.
  • 다시 말해, 잘 만든 클래스 코드가 있다면 이 코드로 다양한 종류의 인스턴스를 생성할 수 있다는 것이다. 쉬운 개념이지만 코드를 보기 전에는 잘 이해되지 않을 수 있다.

파이썬의 객체 지향 프로그래밍

  • 클래스 구현하기
  • 이전 절에서는 인공지능 축구 프로그램을 예로 들어 객체 지향 프로그래밍의 용어를 설명하였다. 실제로 축구 선수 클래스를 파이썬에서 구현하는 예제부터 시작하려고 한다.

  1. 예약어인 class를 코드의 맨 앞에 입력하고, 만들고자 하는 클래스 이름을 작성한다.
  2. 그다음으로 상속받아야 하는 다른 클래스의 이름을 괄호 안에 넣는다.

[참고]

상속은 기존에 만든 클래스의 특징을 그대로 이어받아 사용하는 것을 말한다. 객체 지향 프로그래밍의 장점 중 하나는 재사용성이고, 이를 손쉽게 하는 것이 바로 상속이다.

파이썬에서 자주 사용하는 작명 기법

클래스의 이름을 선언할 때 한 가지 특이한 점은 기존과 다르게 첫 글자와 중간 글자가 대문자라는 것이다. 이것은 클래스를 선언할 때 사용하는 작명 기법에 의한 것이다. 파이썬뿐만 아니라 모든 컴퓨터 프로그래밍 언어에서 변수, 클래스, 함수명을 짓는 작명 기법이 있다.

작명 기법 설명

snake_case 띄어쓰기 부분에 를 추가하여 변수의 이름을 지정한다. 파이썬 함수나 변수명에 사용된다.
CamelCase 띄어쓰기 부분에 대문자를 사용하여 변수의 이름을 지정한다. 낙타의 혹처럼 생겼다 하여 Camel이라고 부르고, 주로 파이썬 클래스명에 사용된다.
  • 속성의 선언
    • 먼저 클래스의 속성을 추가하는 선언에 대해 알아보자.
    • 속성에 대한 정보를 선언하기 위해서는 _ init_()라는 예약 함수를 사용한다.
    • 파이썬 클래스에서는 몇 개의 예약 함수가 있는데. 대표적인 것이 init()이고, 이 외에 __ str __, __ add __등이 있다. 이러한 함수들은 약속된 형태의 작업을 수행시켜 준다.
    class SoccerPlayer(object):
        def__init__(self, name, position, back_number):
        self.name = name
        self.position = position
        self.back_number = back_number
    
    • __ init() __함수는 이 클래스에서 사용할 변수를 정의하는 함수이다.
    • __ init() __함수의 첫 번째 매개변수는 반드시 self 변수를 사용해야 한다. self 변수는 클래스에서 생성된 인스턴스에 접근하는 예약어이다. 생성된 인스턴스를 지정하는 변수라고 생각하면 된다.
    • self 뒤의 매개변수들은 실제로 클래스가 가진 속성으로 축구 선수의 이름, 포지션, 등 번호 등 이다. 이 값들은 실제로 생성되는 인스턴스에 할당된다.
    • 할당되는 코드는 self.name = name 이다. 이는 딕셔너리형과 비슷한데, 생성된 인스턴스에 있는 name 변수에 매개변수로 입력된 name이라는 값을 할당한다는 뜻이다. 클래스의 변수는 ‘self.변수이름’으로 __ init() __ 함수에서 자유롭게 생성할 수 있다.
    • 물론 파이썬은 인터프리터 언어이고 동적 타이핑 언어 이므로, 클래스 내 다른 함수에서도 이와 같은 속성의 생성이 가능하다. 그러나 일반적으로 __ init() __ 내에서만 새로운 속성을 생성해야만 다른 프로그래머가 이 클래스를 사용할 때 헷갈리지 않을 것이다.

함수의 선언

  • 함수는 이 클래스가 할 수 있는 다양한 동작을 정의할 수 있다. 예를 들어 축구 선수라면 등번호 교체라는 행동을 할 수 있다.
class SoccerPlayer(object):
    def change_back_number(self, new_number):
        print("선수의 등번호를 변경한다: From %d to %d" % (self .back_number, new_number)) 
        self.back_number = new_number
  • 클래스 내에서의 함수도 기존 함수와 크게 다르지 않다. 함수의 이름을 쓰고 매개변수를 사용하면 된다.
  • 여기서 가장 큰 차이점은 바로 self를 매개변수에 반드시 넣어야 한다는 것이다. self가 있어야만 실제로 인스턴스가 사용할 수 있는 함수로 선언된다.

_의 쓰임

  • 일반적으로 파이썬에서 _의 쓰임은 개수에 따라 여러 가지로 나눌 수 있다. 예를 들어 _ 1개는 이후로 쓰이지 않을 변수에 특별한 이름을 부여하고 싶지 않을 때 사용한다.
for _ in range(10):
print("Hello, World")
#출력 결과

Hello, World 
Hello, World 
Hello, World
Hello, World 
Hello, World 
Hello, World
Hello, World 
Hello, World 
Hello, World
Hello, World 
  • 위 코드는 ‘Hello, World’를 화면에 10번 출력하는 함수이다. 횟수를 세는 _ 변수는 특별한 용도가 없으므로 뒤에서 사용되지 않는다. 따라서 _를 임의의 변수명 대신에 사용한다.
  • 또 다른 용도로 _ 2개를 사용하면 특수한 예약 함수나 변수를 의미한다. 대표적으로 __ str() __ 이나 __ init() __ 같은 함수이다. __ str() __ 함수는 클래스로 인스턴스를 생성했을 때, 그 인스턴스 자체를 print() 함수로 화면에 출력하면 나오는 값을 뜻한다.

인스턴스 사용하기

생성된 클래스를 인스턴스로 호출해 사용하는 방법

  • 인스턴스는 클래스에서 실제적인 데이터가 입력되어 사용할 수 있는 형태의 객체를 뜻한다. 앞에서 붕어빵 틀을 클래스에, 붕어빵을 인스턴스에 비유했는데, 인스턴스는 여러 가지 재료가 들어간 붕어빵 자체에 비유할 수 있다.
  • 앞에서 클래스를 구현할 때 SoccerPlayer라는 이름으로 생성하였다. 이 클래스는 기본 설계도이므로 선수의 이름이나 등번호가 아직 할당되지 않았다. 실제로 인스턴스를 사용하기 위해서는 이러한 정보를 할당한 상태로 사용해야 한다.

  • 클래스에서 인스턴스를 호출하는 방법은 위의 사진과 같다. 먼저 클래스 이름을 호출하고, 앞에서 정의했던 __ init() __ 함수의 매개변수에 맞추어 값을 입력한다.
  • 이것은 함수에서 배운 초깃값 지정과 같은 개념이다. 여기서 self 변수에는 아무런 값도 할당하지 않는다.
  • jinhyun이라는 이름의 인스턴스가 SoccerPlayer라는 클래스를 기반으로 생성되는 것을 확인할 수 있다. 이 jinhyun이라는 인스턴스 자체가 SoccerPlayer 클래스에서 self에 할당된다.

실제 코드

# 전체 SoccerPlayer 코드
class SoccerPlayer(object):
    def __init__(self, name, position, back_number):
        self.name = name
        self.position = position
        self.back_number = back_number
    def change_back_number(self, new_number):
        print("선수의 등번호를 변경한다: From %d to %d" % (self.back_number, new_
number))
        self.back_number = new_number
    def __str__(self):
        return "Hello, My name is %s. I play in %s in center." % (self.name,
self.position) 

# SoccerPlayer를 사용하는 instance 코드 
jinhyun = SoccerPlayer("Jinhyun", "MF", 10)

print("현재 선수의 등번호는:", jinhyun.back_number)
jinhyun.change_back_number(5)
print("현재 선수의 등번호는:", jinhyun.back_number)
#실행 결과

현재 선수의 등번호는: 10                    #16행 실행 결과
선수의 등번호를 변경한다: From 10 to 5      #17행 실행 결과
현재 선수의 등번호는: 5                     #18행 실행 결과
  • jinhyun = SoccerPlayer("Jinhyun", "MF", 10) 코드로 인스턴스를 새롭게 만들었다. 만들어진 인스턴스인 jinhyun은 name, position, back_number에 각각 Jinhyun, MF, 10이 할당되었다. 할당된 값을 만들어진 인스턴스에서 사용하기 위해서 jinhyun.back_number 형식으로 인스턴스 내의 값을 호출하고 있다.
  • 여기서 중요한 것은 인스턴스가 생성된 후에는 해당 인스턴스의 이름으로 값을 할당하거나 함수를 부르면 되지만, 클래스 내에서는 self로 호출된다. 즉, 생성된 인스턴스인 jinhyun과 클래스 내 self가 같은 역할을 하는 것이다.
  • 함수를 호출할 때도 인스턴스의 이름과 함수명을 사용한다. 여기서는 jinhyun. change_back_number(5) 를 사용해 클래스 내의 함수를 사용하였다. 위의 코드에 이어서 19행으로 print( jinhyun)을 입력하면 다음과 같은 결과가 출력된다.
Hello, My name is Jinhyun. I play in MF in center.
  • 생성된 인스턴스인 jinhyun을 print() 함수에서 사용했을 때 나타나는 결과이다.
  • 이는 위의 코드의 10 , 11행에서 클래스 내 함수로 선언되었기 때문이다. 10행에서 __ str() __ 함수로 선언된 부분이 print() 함수를 사용하면 반환되는 함수이다.
  • 인스턴스의 정보를 표시하거나 구분할 때 __srt__문을 사용하면 된다.
  • 이처럼 예약 함수는 특정 조건에서 작동하는 함수로 유용하다.

클래스를 사용하는 이유

  • 핵심은 자신의 코드를 다른 사람이 손쉽게 사용할 수 있도록 설계하기 위함이다. 만약 단순히 데이터를 저장하기 위한 수단으로 사용한다면 이차원 리스트나 Namedtuple 형태를 사용해도 문제는 없다. 그러나 때로는 자신이 만든 코드가 데이터 저장뿐 아니라 다른 역할을 해야 할 때도 있다.
  • 즉, 데이터를 변환하거나 데이터베이스에 저장하는 등의 역할이 필요할 때가 있다. 이것을 리스트와 함수로 각각 만들어 공유하는 것보다 하나의 객체로 생성해 다른 사람들에게 배포한 다면 작업이 훨씬 더 쉬워진다.
# 데이터
names = ["Messi", "Ramos", "Ronaldo", "Park", "Buffon"] 
positions = ["MF", "DF", "CF", "WF", "GK"]
numbers = [10, 4, 7, 13, 1]

# 이차원 리스트
players = [[name, position, number] for name, position, number in zip(names, 
positions, numbers)]
print(players) 
print(players[0])

# 전체 SoccerPlayer 코드
class SoccerPlayer(object):
    def __init__(self, name, position, back_number):
         self.name = name
         self.position = position
         self .back_number = back_number
    def change_back_number(self, new_number):
         print("선수의 등번호를 변경한다: From %d to %d" % (self .back_number, new_
number))
         self .back_number = new_number
    def __str__(self):
         return "Hello, My name is %s. I play in %s in center." % (self .name,
self.position)
  • 또한, 코드를 좀 더 손쉽게 선언할 수 있다는 장점도 있다.
  • 단순히 이차원 리스트로 선언할 수 있는 것을 객체 지향 프로그래밍의 개념을 적용시켜 좀 더 명확하게 저장된 데이터를 확인할 수 있다. 특히 다른 사람들이 결과를 사용할 때 이 데이터가 무엇을 위한 데이터인지를 명확히 알 수 있다.
# 클래스-인스턴스
player_objects = [SoccerPlayer(name, position, number) for name, position,
number in zip(names, positions, numbers)] 
print(player_objects[0])
[['Messi', 'MF', 10], ['Ramos’, ’DF', 4], ['Ronaldo', 'CF', 7], ['Park', 'WF', 13], 
['Buffon', 'GK', 1]]                     # 8행 실행 결과
['Messi', 'MF', 10]                      # 9행 실행 결과
Hello, My name is Messi. I play in MF in center. # 25행 실행 결과

 

실습

노트북 프로그램 만들기 설계

  • 노트북 프로그램 → 컴퓨터 형태의 노트북을 뜻하는 것이 아니라 흔히 공책의 기능으로 사용하는 노트 앱을 말한다.
  • 객체 지향 프로그래밍을 이용하여 프로그램을 만들기 위해 가장 먼저 해야 할 일은 프로그램 설계이다.
    • 이 프로그램은 어떤 객체가 필요할까?
    • 어떤 기능을 어떻게 정의해 사용해야 할까?
    • 어떤 데이터를 저장해야 할까?

사용자의 요구 사항

  • 노트(note)를 정리하는 프로그램이다.
  • 사용자는 노트에 콘텐츠를 적을 수 있다.
  • 노트는 노트북(notebook)에 삽입된다.
  • 노트북은타이틀(title)이있다.
  • 노트북은 노트가 삽입될 때 페이지를 생성하며, 최대 300페이지까지 저장할 수 있다.
  • 300페이지를 넘기면 노트를 더이상 삽입하지 못한다.

우리가 만들어야 하는 객체에 대한 정보가 있는데, 사용자, 노트, 노트북 등이 객체가 된다.

저장하는 데이터, 즉 변수 정의

  • 가장 기본적인 것은 입력해야 할 콘텐츠가 있고, 노트마다 페 이지가 생성되어야 한다. 또 노트북은 노트라고 하는 객체와 타이틀을 포함해야 한다.

기능function에 대한 정의

  • 노트북의 입장과 노트의 입장
  • 즉 각각의 객체별로 기능을 정의해야 한다. 노트는 내용을 입력하는 함수와 지우는 함수가 필요하다. 그리고 노트북은 노트를 추가하거나 지우는 등의 함수가 필요하다.
  • 그 외 300페이지 등에 대한 로직을 따로 정의하여 함수 안에 넣어야 한 다.

구분 Notebook Note

메서드 add_note , remove_note get_number_of_pages write_content, remove_all
변수 title  
page_number    
notes contents  

새로운 Notebook을 생성하는 코드

>>> wise_saying_notebook = NoteBook("명언 노트") 
>>> wise_saying_notebook.add_note(note_1)
>>> wise_saying_notebook.add_note(note_2)
>>> wise_saying_notebook.add_note(note_3)
>>> wise_saying_notebook.add_note(note_4)
  • 새로운 노트북을 생성한 후 기존의 Note들을 add_note() 함수로 추가한다.
>>> wise_saying_notebook = NoteBook("명언 노트") 
>>> wise_saying_notebook.add_note(note_1)
>>> wise_saying_notebook.add_note(note_2)
>>> wise_saying_notebook.add_note(note_3)
>>> wise_saying_notebook.add_note(note_4)
>>> print(wise_saying_notebook.get_number_of_all_pages())
4
>>> print(wise_saying_notebook.get_number_of_all_characters()) 
159
  • Note 4장을 추가하였으므로 get_number_of_all_pages( ) 함수를 사용하면 총 페이지 수가 출력되고, get_number_of_all characters() 함수를 사용하면 총 글자 수가 출력된다.
>>> wise_saying_notebook.remove_note(3)
>>> print(wise_saying_notebook.get_number_of_all_pages()) 
3
>>>
>>> wise_saying_notebook.add_note(note_1, 100)
해당 페이지에는 이미 노트가 존재합니다.
>>>
>>> for i in range(300):
...   wise_saying_notebook.add_note(note_1, i)
해당 페이지에는 이미 노트가 존재합니다.
해당 페이지에는 이미 노트가 존재합니다.
해당 페이지에는 이미 노트가 존재합니다.
해당 페이지에는 이미 노트가 존재합니다.
>>> print(wise_saying_notebook.get_number_of_all_pages()) 
300
  • 노트의 삭제나 추가도 여러 가지 명령어로 가능하다.
  • 특정 Note를 지우는 remove_note()를 사용하거나 객체 지향 프로그래밍 개념으로 새로운 빈 노트를 임의로 추가할 수도 있다. 기존 페이지에 노트를 추가하려고 하면 오류 메시지도 출력된다.

객체 지향 프로그래밍의 특징

  • 객체 지향 프로그래밍은 절차적인 프로그래밍과 비교해 여러 가지 특징이 있다.
  • 객체 지향 프로그래밍의 기본 철학에는 실생활을 모델링한다는 개념이 있기 때문에 대부분이 실생활에 존재하는 다양한 개념을 쉽게 프로그래밍하기 위한 특징들이다. 객체 지향 프로그래밍의 특징에는 크게 상속, 다형성, 가시성이 있다. 모두 객체 지향 프로그래밍뿐만 아니라 완성된 파이썬 프로그래밍을 하는데도 매우 중요한 개념이다.

상속

  • 상속(inheritance)은 이름 그대로 무엇인가를 내려받는 것을 뜻한다.
  • 부모클래스에 정의된 속성과 메서드를 자식 클래스가 물려받아 사용한다는 것이다.
class Person(object): 
    pass
  • class라는 예약어 다음에 클래스명으로 Person을 쓰고 () 안에 object를 입력하였다. 여기서 object가 바로 Person 클래스의 부모 클래스이다.
  • object는 파이썬에서 사용하는 가장 기본 객체 base object 이며, 파이썬 언어가 객체 지향 프로그래밍이므로 모든 변수는 객체이다. 예를 들어 파이썬의 문자열형 변수에 대해 다음과 같이 객체 이름을 확인할 수 있다.
>>> a = "abc" 
>>> type(a) 
<class 'str'>

객체 자세히 알아보기

  • 지금까지 문자열형이라고 불렀지만 내부적으로는 객체로 처리되었다. 파이썬의 모든 객체는 object 객체를 상속한다.
  • 객체 지향 프로그래밍에서 상속은 부모 클래스로부터 값과 메서드를 물려받아 자식 클래스를 생성한다는 뜻이다. 즉, 자식이 부모의 특성을 그대로 포함한 채 생성되는 것이다.
>>> class Person(object):
...     def __init__(self, name, age):
...         self.name = name
...         self.age = age

>>> class Korean(Person):
        pass
>>> first_korean = Korean("Sungchul", 35) 
>>> print(first_korean.name)
Sungchul
  • 이 코드에서는 먼저 Person 클래스를 생성하였다.
  • Person 클래스에는 생성자 init() 함수를 만들어 name과 age에 관련된 정보를 입력할 수 있도록 하였다.
  • 다음으로 Korean 클래스를 만들면서 Person 클래스를 상속받았다. class Korean(Person)과 같이 작성하면 간단히 상속 받을 수 있다. 그리고 pass는 별도의 내용 없이 클래스만 존재한다는 뜻이다.
  • 즉, 별도의 내용이 없는 Korean 클래스 인스턴스를 만든 것이다. Korean 클래스는 별도의 생성자는 없지만, Person 클래스가 가진 생성자를 그대로 사용하여 인스턴스를 만들었다. 그래서 Person 클래스에서 생성한 변수를 그대로 사용할 수 있다. 이러한 객체 지향 프로그래밍의 특징을 상속이라고 한다.

  • 상속을 도표로 표현하면 위와 같다. 사각형이 클래스이고, 화살표는 각 클래스의 상속 관계이다. 즉, Person 클래스를 Employee가 상속받고, 이 클래스를 다시 한번 Manager, Staff, Hourly 등이 상속받는 것이다.
  • 일반적으로 상속을 하게 되면 부모 클래스보다 자식 클래스의 정보가 더 구체화된다. 객체 지향 프로그래밍으로 프로그램을 작성할 때 이러한 상속 구조를 설계해야 한다.
  • 특히 상속이 진행될수록 부모 클래스에 대해 각 클래스의 기능이 구체화되도록 부모 객체에는 일반적인 기능을, 자식 객체에는 상세한 기능을 넣어야 한다.
  • 그리고 같은 일을 하는 메서드라 하더라도 부모 객체보다 자식 객체에 좀 더 많은 정보를 줄 수도 있다. 이를 ‘부모 클래스의 메서드를 재정의한다’라고 한다.
class Person(object):      #부모 클래스 Person 선언
    def __init__(self, name, age, gender):
        self.name = name
        self.age = age
        self.gender = gender

    def about_me(self):    #메서드 선언
        print("저의 이름은", self.name, "이고요, 제 나이는", str(self.age), "살입니다.")
  • 먼저 부모 클래스가 Person이다. name, age, gender에 대해 변수를 선언하였고, about_ me 함수를 사용하여 생성된 인스턴스가 자신을 설명할 수 있도록 하였다. 사실 str() 함수에 들어가도 되는 클래스이지만 임의의 about_me 클래스를 생성하였다.

다음으로 상속 받는 Employee 클래스

class Employee(Person):            # 부모 클래스 Person으로부터 상속 
    def __init__(self, name, age, gender, salary, hire_date):
        super().__init__(name,age,gender) # 부모객체사용 
        self.salary = salary
        self.hire_date = hire_date       #속성값 추가

    def do_work(self):                    #새로운 메서드 추가
        print("열심히 일을한다.")     
     
    def about_me(self):                   #부모 클래스 함수 재정의
        super().about_me()                #부모 클래스 함수 사용
        print ("제 급여는", self.salary, "원이고, 제 입사일은", self.hire_date, "입니다.")
  • Person 클래스가 단순히 사람에 대한 정보를 정의했다면, Employee 클래스는 사람에 대한 정의와 함께 일하는 시간과 월급에 대한 변수를 추가한다. 즉 init() 함수를 재정의 한다.
  • 이때 부모 클래스의 init () 함수를 그대로 사용하려면 별도의 init() 함수를 만들지 않아도 된다. 하지만 기존 함수를 사용하면서 새로운 내용을 추가하기 위해서는 자식 클래스에 init() 함수를 생성할 때 super().init(매개변수) 라고 입력한다.
  • 여기서 super()는 부모 클래스를 가리킨다. 즉, 부모 클래스의 __ init()__ 함수를 그대로 사용한다는 뜻이다. 그리고 그 아래에는 자식 클래스에서 필요한 새로운 변수를 추가하면 된다.
  • 이러한 함수의 재정의를 오버라이딩( overriding)이라고 한다. 오버라이딩은 상속 시 함수 이름과 필요한 매개변수는 그대로 유지하면서 함수의 수행 코드를 변경하는 것이다.
  • 같은 방식으로 about_me() 함수가 오버라이딩된 것을 확인할 수 있다. Person과 Employee에 대한 설명을 추가한 것이다. 위 코드들은 설명의 용이성을 위해 텍스트 형태의 설명만 추가하였지만 실제로는 프로그램에 사용하기 위한 다양한 기능이 추가될 수 있다.
  • do_work처럼 자식 클래스에만 필요한 새로운 함수를 생성할 수도 있다. 앞서 설명했듯이 자 식 클래스는 부모 클래스보다 더 상세한 일을 한다. 따라서 자식 클래스가 새로운 기능을 수행할 수 있도록 코드를 작성해야 한다.
  • 파이썬에서는 이외에도 다양한 상속 기능을 지원한다. 예를 들어, 1개 이상의 클래스 특징을 상속하는 다중상속을 지원하기도 한다. 이러한 세부적인 기능은 파이썬을 사용하여 본격적인 객체 지향 프로그래밍을 할 때 필요하다.

다형성

  • 다형성 (polymorphism)은 같은 이름의 메서드가 다른 기능을 하는 것을 말한다. 사실 이 기능은 상속에서 이미 확인하였다. 예를 들어, about_me라는 함수를 부모 클래스와 자식 클래스가 서로 다르게 구현했는데, 이것도 일종의 다형성이다.

다형성을 사용하는 이유

  • 객체 지향 프로그래밍을 사용하는 이유를 생각하면 간단하다. 객체 지향 프로그래밍은 다른 사람의 코드를 쉽게 재사용하기 위해 사용한다고 언급하였다. 이를 위해서는 내부적인 구현 과정은 잘 모르더라도 그 함수나 클래스의 역할은 명확히 알 필요가 있다.
  • 인터넷에서 데이터를 모으는 프로그램을 크롤러 crawler라고 하는데. 이번에는 뉴스를 모으는 크롤러를 만들어보면서 설명하겠다.
  • 먼저 부모 Crawler 클래스를 만들고 do_crawling이라 는 함수를 생성한다. 그리고 Crawler 클래스를 상속시켜 NaverCrawler와 DaumCrawler를 만든다. 이제 두 자식 클래스는 모두 do_crawling 함수를 갖게 된다.
  • 같은 이름이지만 구현 내용은 다르다. 각각 클래스에서 구현되는 내부 로직에 차이가 있는데, 이를 함수의 다형성이라고 한다. 여기서 Crawler 클래스들의 do_crawling 함수는 각각 네이버와 다음에서 뉴스를 가져오는 역할을 한다. 해당 클래스의 사용자는 데이터를 어떻게 가져오는지 모르지만 결과는 두 클래스 모두 같다는 것을 알 수 있다.
n_crawler = NaverCrawler()
d_crawler = DaumCrawler()
cralwers = [n_crawler, d_crawler]
news=[]
for cralwer in cralwers:
    news.append(cralwer.do_crawling())
  • 해당 클래스의 사용자 입장에서는 함수의 이름만 알면 같은 형태로 사용할 수 있지만, 클래스의 개발자 입장에서는 내부적인 구현을 각 클래스별 다르게 개발할 필요가 있다.
  • 이를 다형성이라고 하는데, 다형성을 사용하면 프로그램을 작성할 때 사용자가 좀 더 쉽게 클래스를 사용 할 수 있다.
class Animal:
    def __init__(self, name):
        self.name = name
    def talk(self):
        raise NotlmplementedErrorC’Subclass must implement abstract method")
 
class Cat(Animal):
    def talk(self):
        return 'Meow!'
 
class Dog(Animal):
    def talk(self):
        return 'Woof! Woof!'
 
animals=[Cat('Missy'), Cat('Mr.Mistoffelees'), Dog('Lassie')]
for animal in animals:
    print(animal.name + ':' + animal.talk())
#15~17행 실행 결과

Missy: Meow!
Mr. Mistoffelees: Meow! 
Lassie: Woof! Woof!
  • 위 코드는 의사코드로 실제 작동되지는 않고 단지 다형성을 설명 하기 위해 만든 코드이다.
  • 코드에서 부모 클래스는 Animal이며, Cat과 Dog는 Animal 클래스를 상속받는다. 핵심 함수는 talk로, 각각 두 동물 클래스의 역할이 다른 것을 확인할 수 있다. Animal 클래스는 아직 설명하지 않았지만 NotlmplementedError라는 클래스를 호출한다. 이 클래스는 자식 클래스에만 해당 함수를 사용할 수 있도록 한다. 따라서 두 클래스가 내부 로직에서 같은 이름의 함수를 사용하여 결과를 출력하도록 한다. 정의된 모든 클래스는 15〜 17행과 같이 사용할 수 있다.

가시성

  • 가시성은 객체의 정보를 볼 수 있는 레벨을 조절하여 객체의 정보 접근을 숨기는 것을 말하는데, 이 특징은 다양한 이름으로 불리고 있다.
  • 파이썬에서는 가시성이라고 하지만 좀 더 중요한 핵심 개념은 캡슐화(encapsulation)와 정보 은닉 (information hiding)이다.
  • 파이썬에서는 객체의 재사용을 위해 각 객체가 무슨 역할을 하는지 알아야 한다. 하지만 그와 동시에 구현의 세부적인 내용을 모두 알 필요는 없다. 단지 사용하는 방법만 알면 된다. 즉,객체의 매개변수 인터페이스만 명확히 알면 사용할 수 있다.
  • 이러한 개념을 캡슐화라고 하며, 객체의 세부 내용은 모른 채 객체의 사용법만 알고 사용한다는 뜻이다. 동시에 필요한 정보는 숨겨야 한다.
  • 캡슐화 와 정보은닉으로 표현은 다르게 하지만 둘 다 코드의 내부구현을 잘해서 외부에서 쉽게 사용하게 하고, 코드의 세부적인 내용은 모르게 한다는 측면에서 비슷한 의미로 사용된다.

캡슐화를 사용해야 하는 이유

  • 여러 가지가 있지만 일단 클래스를 설계할 때 클래스 간 간섭 및 정보 공유를 최소화하여 개별 클래스가 단독으로도 잘 동작할 수 있도록 해야 하기 때문이다. 각 클래스가 강하게 연결되어 있다면 독립적으로 사용하기 어렵다. 또한, 사용자 입장에서는 상세한 내용을 모르더라도 인터페이스를 이해하면 클래스를 쉽게 사용할 수 있다. 파이썬에서는 이러한 개념을 가시성이라는 이름으로 적용시키고, 정보 은닉을 어떻게 할 것인가를 코드 레벨에서 조절한다.

실제 파이썬의 가시성 사용 방법 예시

  • 코드를 작성해야 하는 상황

<aside> 💡 Product 객체를 Inventory 객체에 추가 Inventory에는 오직 Product 객체만 들어감 Inventory에 Product가 몇 개인지 확인이 필요함 Inventory에 Product items는 직접 접근이 불가함

</aside>

class Product(object):
    pass

class Inventory(object):
    def  __ init__(self):
        self.__items =[]
    def add_new_item(self, product): 
        if type(product) == Product:
           self.__items.append(product)
           print("new item added") 
        else:
           raise ValueError("Invalid Item") 
    def get_number_of_items(self):
        return len(self.__items)

my_inventory = Inventory()
my_inventory.add_new_item(Product()) 
my_inventory.add_new_item(Product())

my_inventory.__items
#출력결과

new item added                         # 17행 실행 결과
new item added                         # 18행 실행 결과
Traceback (most recent call last):     # 20행 실행 결과
    File "visibilityl.py", line 20, in <module>
        my_inventory_iterns
AttributeError: 'Inventory' object has no attribute '__items'
  • 위 코드에서는 Inventory 객체에 add_new_item() 함수를 사용하여 새롭게 생성된 Product 객체를 넣어준다. 여기서 자료형을 비교하여 해당 객체가 Product 객체인지 확인 하고, 그렇지 않을 경우 ValueError가 발생한다.
  • items는 Product 객체가 들어가는 공간으로, get_number_of_items()를 사용하여 총 객체의 개수를 반환한다. 여기서 핵심은___items 변수이다. Inventory를 저장하는 공간으로, add_new_item()을 통해 Product 객체를 넣을 수 있다.

다른 프로그램이 add_new_item()이 아니라 직접 해당 객체에 접근해 새로운 값을 추가하려고 한다면 어떻게 할까?

  • 다른 코드에서는 잘 실행되다가 20행의 my_inventory._items에서 오류가 발생한다. 왜냐하면 _가 특수 역할을 하는 예약 문자로 클래스에서 변수에 두 개가 붙어있어 사용될 클래스 내부에서만 접근할 수 있고, 외부에는 호출하여 사용하지 못하기 때문이다.
  • 즉, 클래스 내부 용으로만 변수를 사용하고 싶다면 ‘__변수명’ 형태로 변수를 선언한다. 이러한 특징을 자바에 서는 private variable이라고도 한다. 즉, 가시성을 클래스 내로 한정하면서 값이 다르게 들어가는것을막을수있다. 이를정보은닉이라고한다.

이러한 정보를 클래스 외부에서 사용하기 위해서는 어떻게 해야 할까?

  • 데코레이터 (decorator)라고 불리는 ©property를 사용한다.
  • 데코레이터는 클래스의 각 메서드 상단에 삽입하여 해당 메서드의 기능을 추가하는 파이썬 문법이다.
class Inventory(object):
    def __init__(self):
        self.__items=[]          # private변수로선언(타인이접근못함)

    @property                    # property 데코레이터(숨겨진 변수 반환)
    def items(self):
        return self.__items
  • 처음 코드의 14행 뒷부분에 위의 코드를 추가하여 ©property를 사용하면 해당 변수를 외부에서 사용할 수 있다.
  • 다른 코드는 그대로 유지하고 마지막에 items라는 이름으로 메서드를 만들면서 ©property 를 메서드 상단에 입력한다. 그리고 외부에서 사용할 변수인 __items를 반환한다. 이렇게 코 드를 추가하면 다음과 같이 외부에서도 해당 메서드를 사용할 수 있다.
>>> my—inventory = Inventory()
>>> items = my_inventory.items 
>>> items.append(Product())
  • 이번 코드에서는 오류가 발생하지 않았다. 여기서 주목할 부분은 __items 변수의 원래 이름 이 아닌 items로 호출한다는 것이다. 바로 ©property를 붙인 함수 이름으로 실제 __items 를 사용할 수 있는 것이다. 이는 기존 private 변수를 누구나 사용할 수 있는 public 변수로 바꾸는 방법 중 하나이다.