본문 바로가기
파이썬 기초

[파이썬 기초] 5. 클래스

by 빈이름 2024. 6. 25.
1. 절차지향언어와 객체지향언어
2. 클래스
    2.1. 클래스의 속성
    2.2. 클래스의 상속
    2.3. 클래스의 활용

1. 절차지향언어와 객체지향언어

클래스에 대해 설명하기 전에 클래스의 등장배경에 대한 이론을 먼저 설명드리겠습니다.

 

코딩을 하는 방식은 절차지향언어객체지향언어 2가지로 나눌 수 있습니다.

 

절차지향언어란 위에서 아래로 순차적으로 명령어가 실행되는 언어를 말합니다. 쉽게 이해하자면 아직 클래스를 배우지 않은 여러분이 클래스를 사용하지 않고 작성하는 코드가 절차지향언어라고 볼 수 있습니다.

 

객체지향언어란 함수, 변수 등을 객체화하여 프로그래밍하고 실행하는 언어를 말합니다. 여기에는 클래스가 사용되죠.

 

두 방식의 차이를 보기위해 간단한 도형의 넓이를 구하는 프로그램을 예시로 들어보겠습니다. 절차지향언어의 경우 아래와 같이 코드를 작성할 수 있습니다.

def calculate_rectangle_area(width, height):
    return widht * height

width = 5
height = 10

rectangle_area = calculate_rectangle_area(width, height)

print("사각형의 넓이 :", rectangle_area)

이런 식으로 함수와 변수를 선언한 뒤, 함수를 호출하여 코드를 마치는 식입니다.

반면 객체지향언어의 경우 클래스를 활용해 아래와 같이 코드를 작성합니다.

class Area:
    def __init__(self, width, height):
        self.width = width
        self.height = height
        
    def calculate_rectangle_area(self):
        return self.width * self.height
        
area = Area(width=5, height=10)
rectangle_area = area.calculate_rectangle_area()

print("사각형의 넓이 :", rectangle_area)

아직 클래스를 배우진 않았지만, 보면 width, height 변수와 넓이를 계산하는 함수들이 "Area"라는 클래스에 들어가 있다라는 것을 대략 알 수 있습니다.

 

두 코드의 구조를 그림으로 나타내자면 아래와 같습니다.

절차지향언어는 모든 프로그램이 순서대로 작성되고 순서대로 실행되지만 객체지향언어는 하나의 객체에 관련 함수, 변수들을 작성하고 해당 객체를 사용하는 방식으로 코드가 작동한다.

 

그러나 두 코드 모두 실행하면 같은 결과를 내놓습니다. 그리고 이것만으론 코드가 동작하는 방식에 있어서도 큰 차이를 느끼지 못할 수도 있습니다. 두 코드의 차이점은 코드를 수정하는데서 더 크게 드러납니다.

 

절차지향언어의 경우 위에서 아래 순서로 순차적으로 코드를 실행하기 때문에 모든 코드가 유기적으로 연결되어 있습니다. 그렇기 때문에 하나를 수정할 경우 그와 연결되어 있는 다른 코드들을 함께 수정해야 합니다. 하지만 코드가 길어지고 복잡해질수록 함께 수정해야 할 코드들이 많아지며 수정하기도 어렵습니다.

 

반대로 객체지향언어의 경우 도형의 넓이를 구하는 것과 관련된 코드로 예시를 들자면, 관련된 코드들이 모두 Area라는 클래스에 들어 있기 때문에 Area 클래스만 수정하면 된다는 것을 직관적으로 알기 쉽고, 수정할 코드의 양도 절차지향에 비해 적습니다.

 

차이를 보기 위해 현재 코드에 도형의 색상을 출력하는 코드를 추가해 보겠습니다. 절차지향 언어의 경우 아래와 같이 코드가 수정될 겁니다.

def calculate_rectangle_area(width, height):
    return widht * height
    
def print_shape_info(width, height, color):
    area = calculate_rectangle_area(width, height)
    print(f"사각형의 넓이: {area}, 색상: {color}")

width = 5
height = 10
color = "red"

rectangle_area = print_shape_info(width, height, color)

색상과 넓이를 함께 출력하는 함수를 새로 정의했으며, color라는 새로운 전역 변수도 정의했고 새로운 함수를 호출하는 코드를 새로 작성했습니다.

 

반면 객체지향언어는 아래와 같이 코드를 수정하면 됩니다.

class Area:
    def __init__(self, width, height, color):
        self.width = width
        self.height = height
        self.color = color
        
    def calculate_rectangle_area(self):
        return self.width * self.height
        
    def print_shape_info(self):
        area = self.calculate_rectangle_area()
        print(f"사각형의 넓이 : {area}, 색상 : {self.color}")
        
area = Area(width=5, height=10, color="red")
rectangle_area = area.print_shape_info()

Area 클래스에 color라는 변수를 하나 추가했으며, Area 클래스 안에 도형의 정보를 출력하는 함수를 추가했습니다. 이에 따라 Area 클래스를 선언하는 부분(밑에서 2번째 줄)에 color 인자만 추가해줬으며, Area 클래스의 print_shape_info() 함수를 호출하는 것으로 코드를 마무리했습니다.

 

지금은 간단한 코드를 보고 있기도 하고 클래스가 익숙하지 않기 때문에 객체지향언어의 수정이 더 불편해 보이고 복잡해 보일 수도 있지만 클래스에 적응하고, 긴 코드를 작성하기 시작한다면 그 차이를 더 극명히 느낄 수 있을 겁니다.

 

간단하게 최종 정리를 해보겠습니다.

절차지향언어는,

  • 초기 코드 작성이 쉽고 간단하다.
  • 그러나 코드가 길어지고 복잡해질수록 수정이 어렵다.
  • 따라서 짧고 간단한 코드 작성에 적합하다.

객체지향언어는,

  • 처음에 코드 작성이 복잡하고 어려울 수도 있다.
  • 그러나 코드가 커질수록 수정하는 것이 용이하고, 코드를 이해하기 쉽다.
  • 따라서 큰 프로젝트를 수행할 때 적합하다.

그 외에도 절차지향언어가 객체지향언어보다 메모리도 덜 쓰고 속도도 더 빠를 수 있다는 장점을 갖습니다. 그러니 상황에 맞게 여러분이 코드 작성 방식을 선택해서 프로그래밍을 하시면 되겠습니다.

 

객체지향언어방식도 알기 위해선 클래스를 알아야겠죠? 그럼 지금부터 클래스에 대해 함께 알아보겠습니다!

2. 클래스

2.1. 클래스의 속성

그러면 이제부터 클래스는 어떻게 구현하고 어떻게 사용하는지 알아보도록 하겠습니다.

class Area:
    def __init__(self, width, height, color):
        self.width = width
        self.height = height
        self.color = color
        print("생성자가 실행되었습니다.")
        
    def calculate_rectangle_area(self):
        return self.width * self.height
        
    def print_shape_info(self):
        area = self.calculate_rectangle_area()
        print(f"사각형의 넓이 : {area}, 색상 : {self.color}")
        
area = Area(width=5, height=10, color='red')

 

class Area:

클래스는 'class' 뒤에 이름을 작성하는 식으로 쓸 수 있습니다. 그 아래에는 들여쓰기를 통해 클래스에 속하는 메소드들을 정의할 수 있습니다.

 

def __init__(self, width, height, color):

__init__이라는 이름의 함수는 클래스의 '생성자'를 나타냅니다. 생성자는 클래스가 정의될 때 실행되는 함수입니다.

클래스의 정의는 area = Area(width=5, height=10, color='red')와 같이 할 수 있는데, 여기에서 생성자의 코드가 실행됩니다. 그렇기 때문에 위 코드를 실행하면 "생성자가 실행되었습니다." 라는 문장이 출력될 겁니다.

생성자는 보통 클래스에서 필요한 변수들을 정의하는데 사용됩니다.

 

생성자는 함수와 같이 입력 인자를 받을 수 있으며, 이 인자들은 클래스를 정의할 때 입력해줘야 합니다. 다만 이 인자 중에서 맨 앞의 self는 클래스에 속한다는 것을 의미하는 것으로 self라는 이름으로 어떤 인자를 입력 받는다는 뜻이 아닙니다.

 

이와 같은 맥락으로 Area 클래스에 속하는 변수들도 앞에 self. 를 붙이는 것을 확인할 수 있습니다.

self는 클래스에 속한다는 것을 의미하며 self를 붙이지 않으면 같은 클래스 내의 다른 함수에서나 클래스를 통해 만들어진 객체에서도 접근할 수가 없습니다.

class Area:
    def __init__(self, a, b):
        self.a = a
        b = b
        
    def print_variables(self):
        print(self.a)
        print(b)
        
area = Area(a=3, b=5)
area.print_variables() # self.a는 출력되지만 b는 출력되지 않고 에러가 발생

위 코드에선 변수 a에는 self를 붙였지만 b에는 self를 붙이지 않았습니다. 그 결과, print_variables() 함수에서 변수 a는 성공적으로 출력하지만 변수 b는 찾지 못하고 에러를 발생시킵니다. 즉, self를 붙여야 Area 클래스에 포함될 수 있는 겁니다.

print(area.a)
print(area.b)

마찬가지로 Area 클래스를 이용한 area 객체에서 변수 a는 출력할 수 있지만 변수 b는 출력할 수 없습니다. 클래스를 제대로 활용하고자 한다면 self는 꼭 붙여주는 것이 좋겠죠?

 

클래스를 사용하기 위해선 클래스를 이용해 객체를 생성해야 합니다. 객체는 같은 클래스를 통해 만들어지더라도 서로 다른 객체로 취급됩니다.

class Area:
    def __init__(self, width, height, color):
        self.width = width
        self.height = height
        self.color = color
        
    def calculate_rectangle_area(self):
        return self.width * self.height
        
    def print_shape_info(self):
        area = self.calculate_rectangle_area()
        print(f"사각형의 넓이 : {area}, 색상 : {self.color}")
        
# 객체생성
area1 = Area(width=5, height=10, color='red')
area2 = Area(width=8, height=7, color='blue')

area1.print_shape_info()
area2.print_shape_info()

위 코드에서 area1과 area2는 같은 Area 클래스를 이용해 만든 객체지만 둘은 서로 다른 객체입니다. 따라서  print_shape_info() 함수를 이용해 area1과 area2의 정보를 출력했을 때도 서로 다른 값을 출력합니다.

2.2. 클래스의 상속

클래스를 사용해 프로그래밍을 하는 것은 코드의 유지보수에도 도움이 되지만 이 클래스를 재사용할 수 있다는 것에도 장점이 있습니다.

클래스를 재사용하는데는 클래스의 상속 기능이 사용됩니다. 상속이란, 말 그대로 클래스의 기능들을 상속받는다는 말입니다. 코드를 보면서 이해해 볼까요?

class MotherClass:
    def __init__(self):
        self.a = 10
        self.b = 20
        print("Init mother class")
        
    def mother_func(self):
        print("Mother!!!!!!!!!!")
        
    def speak(self):
        print("I'm mother class")
        
class ChildClass(MotherClass):
    def __init__(self):
        super().__init__()
        self.c = 30
        print("Init child class")
        
    def speak(self):
        print("I'm child class")
        
mother = MotherClass()
print("---------------------------")
child = ChildClass()


'''
Init mother class
---------------------------
Init mother class
Init child class
'''

위 코드에는 2개의 클래스가 있습니다. ChildClass는 MotherClass를 상속받았습니다.

 

다른 클래스를 상속받을 땐 클래스 이름 옆에 괄호를 열고 상속받을 클래스의 이름을 적어줘야 합니다.

class ChildClass(MotherClass):
    def __init__(self):
        super().__init__()
        self.c = 30
        print("Init child class")

그리고 다른 클래스를 상속받을 땐 반드시 생성자에서 상위 클래스의 생성자를 실행해줘야합니다. 이는 ChildClass의 생성

자에 있는 super().__init__() 코드를 통해 수행할 수 있습니다.

 

그러면 왜 위 코드의 출력결과에 "Init mother class"가 2번 등장했는지 알 수 있겠죠? ChildClass의 생성자에서 MotherClass의 생성자를 실행하기 때문에 위 코드에서 "Init mother class"가 2번 출력되는 겁니다.

 

그럼 이제부터 클래스 상속을 활용해서 일어나는 마법들을 알아보겠습니다. 우선 ChildClass는 MotherClass를 상속 받았기 때문에 MotherClass의 변수 self.a, self.b에 접근이 가능합니다. 그리고 이는 함수도 마찬가지입니다. ChildClass에서 MotherClass의 mother_func() 함수도 사용이 가능합니다.

print(child.a) # 10
print(child.b) # 20

child.mother_func() # "Mother!!!!!!!!!!"

 

그리고 ChildClass는 MotherClass의 메소드를 재정의하는 것이 가능합니다. 코드를 보면 MotherClass와 ChildClass 2개 모두 speak() 함수를 갖고 있는 걸 볼 수 있습니다.

이는 ChildClass가 MotherClass의 speak() 함수를 상속 받기는 했지만, speak() 함수를 재정의(override)한 것입니다. 쉽게 말하면 ChildClass만의 speak() 함수를 다시 만든 거라고 생각하면 됩니다.

mother.speak() # "I'm mother class"
child.speak() # "I'm child class"

보면 MotherClass의 speak() 기능은 완전히 소실하고 ChildClass의 speak()만 실행되는 것을 볼 수 있습니다. 이렇듯 하위 클래스(ChildClass)에서 상위 클래스(MotherClass)의 기능을 재정의하는 것이 가능합니다. 그리고 물론, 하위 클래스에서 해당 기능을 재정의했다고 해서 상위 클래스에 영향을 주지는 않습니다.

 

그리고 이와 같은 맥락으로 하위 클래스에만 있는 메소드는 상위 클래스에 영향을 주지 않습니다. 즉, ChildClass의 변수 c는 MotherClass에서 활용할 수 없습니다.

print(child.c) # 30
print(mother.c) # 에러 발생.

2.3. 클래스의 활용

클래스를 활용해 간단한 프로그램을 하나 작성해 보겠습니다. 학생들의 출석을 관리하는 클래스 하나와 학생들의 성적을 관리하는 클래스를 구현해 보겠습니다. 여기서 성적부 클래스는 출석부 클래스를 상속받아서 기능을 구현할 겁니다.

코드를 보기 전에 직접 구현해 보시는 것도 좋을 것 같습니다. 코드에 정답은 없으니까 자유롭게 구현해 보세요!

 

저도 한번 코드를 작성해 보겠습니다. 우선 출석부 클래스부터 코드를 구현해 보겠습니다. 우선 출석부 클래스를 만들면서 학생 명단을 담을 리스트를 구현하겠습니다.

class StudentAttendance:
    def __init__(self, student_list = []):
        self.student_list = student_list

저는 student_list를 객체 선언할 때 인자로 입력 받도록 구현했습니다. 그리고 여기에 학생 이름을 추가하는 함수와 출석을 부르는 함수도 추가해 보겠습니다.

class StudentAttendance:
    def __init__(self, student_list = []):
        self.student_list = student_list
        
    def call_attendance(self):
        for i, student in enumerate(self.student_list):
            print(f"{i+1}번 {student}")
            
    def add_student(self, name):
        self.student_list.append(name)

여기까진 크게 어렵지 않을 것 같습니다.

 

이제 상속을 활용해 성적부 클래스도 만들어 보겠습니다. 우선 생성자부터 만들어 보죠.

class StudentScore(StudentAttendance):
    def __init__(self, student_list = []):
        super().__init__(student_list)
        self.scores = [-1] * len(self.student_list)

 

성적부 클래스는 출석부 클래스를 상속 받습니다. 이 때, 출석부 클래스는 생성자를 실행할 때 student_list라는 입력 인자를 받아야 합니다. 그렇기 때문에 성적부 클래스에서 출석부 클래스의 생성자를 실행할 때 student_list라는 인자를 꼭 입력해 줘야 합니다. 그래서 성적부 클래스도 student_list 인자를 입력 받도록 설정한 뒤, 출석부 클래스의 생성자를 호출할 때 입력 인자로 전달했습니다.

더불어 학생 성적을 담을 리스트도 정의를 해줬습니다. 아직 입력되지 않은 성적은 임의로 -1로 표시했습니다.

 

이제 성적을 추가하는 함수와 성적을 보여주는 함수를 작성해야 합니다.

class StudentScore(StudentAttendance):
    def __init__(self, student_list = []):
        super().__init__(student_list)
        self.scores = [-1] * len(self.student_list)
        
    def write_score(self, name, score):
        idx = self.student_list.index(name)
        self.scores[idx] = score
        
    def show_score(self):
        for name, score in zip(self.student_list, self.scores):
            print(f"{name} : {score}")
            
    def add_student(self, name):
        self.student_list.append(name)
        self.scores.append(-1)

성적 출력 코드는 크게 어려운 것이 없을 것 같습니다. 저는 성적을 입력하는 함수를 학생 이름과 성적을 함께 입력 받도록 구현했습니다. 그래서 해당 학생의 이름의 인덱스를 검색한 뒤, self.scores에서 해당 인덱스에 해당하는 값을 입력 받은 점수로 갱신하는 방식으로 구현했습니다.

 

add_student() 함수도 재정의 해줬습니다. 학생 이름을 추가하면서 student_list의 길이가 늘어나는것과 같이 scores 리스트의 길이도 늘어나야 하기 때문입니다.

 

이렇게 만든 프로그램을 활용하려면 아래와 같이 활용할 수 있겠죠. 성적부 클래스가 출석부 클래스를 상속 받았기 때문에 성적부 클래스만 사용해도 모든 기능을 다 사용할 수 있습니다.

student_list = ["민수", "지수"]

Scores = StudentScore(student_list)

Scores.add_studnet("영희")

Scores.write_score("민수", 92)
Scores.write_score("지수", 80)
Scores.write_score("영희", 77)
Scores.show_score()

Scores.call_attendance()

 

이렇게 클래스에 대해서 알아봤습니다. 처음엔 좀 복잡하고 어려울 수도 있습니다만... 코딩은 역시 많이 해보면서 익숙해지는 것이 최고인 것 같습니다. 그리고 원래 코딩은 이런 지식들을 달달 외워서 코딩하는게 아니라 까먹으면 그때 그때 다시 찾아보면서 하는 거기 때문에 너무 어렵게 받아 들이지 않으셨으면 좋겠습니다. 우선 클래스에 대한 설명은 여기서 마치겠습니다. 감사합니다.