객체 지향 프로그래밍의 4개의 기둥 (추상화, 캡슐화, 상속, 다형성)

코드잇의 객체 지향 프로그래밍의 4개의 기둥 강의를 보고 정리한 내용입니다.
문제될 시 삭제하겠습니다.

객체 지향 프로그래밍의 4개의 기둥

1. 추상화

01. 추상화

추상화란 ? 프로그래머들이 특정 코드를 사용할 때 필수적인 정보를 제외한 세부사항을 가리는 것

03. 추상화는 이때까지 쓰고 있었다.

  • 변수, 함수, 리스트

04. 추상화 잘하기 : 이름 잘 짓기

  • 어디에 쓰는 클래스이고 어떻게 사용할지 직관적으로 알 수 있도록

05. 추상화 더 잘하기 : 문서화하기

  • 문서화 (docstring) : 함수 선언 아래에 """ 함수에 대한 간략한 설명 """
  • help(class name) 함수를 사용하면, docstring 문서가 나옴

09. 여기서 잠깐! 파이썬의 type hinting

  • → 로 return 될 값을 넣어준다.
class BankAccount:
    def deposit(self, amount:float) -> None:
        self.balance += amount 

2. 캡슐화

01. 캡슐화의 필요성

캡슐화의 정의

  1. 객체의 일부 구현 내용에 대한 외부로부터의 직접적인 액세스를 차단하는 것
  2. 객체의 속성과 그것을 사용하는 행동을 하나로 묶는 것
    • __age 와 같은 변수에 get, set 메소드를 통해 접근하는 것

캡슐화 정리

  1. 클래스 밖에서 접근 못하게 할 변수, 메소드 정하기
  2. 변수나 메소드 이름 앞에 언더바 2개 붙이기
  3. 변수에 간접 접근할 수 있게 메소드 추가하기(getter / setter / 다른 용도의 메소드)

02. 객체 내부를 숨기는 법

  • __변수명을 사용하면, 클래스 밖에서 접근이 불가능함
class Citizen:
    drinking_age = 19

    def __init__(self, name, age, resident_id):
        self.name = name
        self.__age = age
        self.__resident_id = resident_id

    def __authenticate(self, id_field):
        return self.__resident_id == id_field 

kusik = Citizen("amy", 21, "134")
print(kusik.__age) # 클래스 밖에서 접근이 불가능하여 에러 남

03. 밑주 두개(__)와 특수 메소드들

  • 변수나 메소드 앞에 __(밑줄 두개)를 추가하면 클래스 외부에서 접근할 수 없음
  • __(밑줄 두개)가 변수나 메소드 앞 뒤에 위치하면 일반 변수나 메소드처럼 클래스 밖에서도 접근할 수 있음

04. 객체의 메소드를 통해 변수 접근하기 1

  • 함수 생성을 통해 외부에서 __ 변수를 사용가능하도록 함
class Citizen:
    def __init__(self, name, age, resident_id):
        self.name = name
        self.__age = age
        self.__resident_ids = resident_id

    def get_age(self):
        return self.__age

    def set_age(self, value):
        self.__age = value

young = Citizen("younghoon", 18, "123")
print(young.get_age()) # 18

young.set_age(25)
print(young.get_age()) # 25

05. 객체의 메소드를 통해 변수 접근하기 2

  • getter 메소드 : 변수의 값을 읽는 메소드
  • setter 메소드 : 변수의 값을 설정하는 메소드
  • getter / setter 메소드를 꼭 만들 필요는 없음
  • def authenticate(self, id_field): return self.__resident_id == id_field

06. 객체의 메소드를 통해 변수 접근하기 3

class Citizen:
    def __init__(self, name, age, resident_id):
        self.name = name
        self.set_age(age)
        self.__resident_ids = resident_id

    def set_age(self, value):
        if value < 0:
            print("나이는 0보다 작을 수 없습니다.")
            self.__age = 0
        else:
            self.__age = value

    def get_age(self):
        return self.__age

young = Citizen("young", -10, "876") # 나이는 0보다 작을 수 없습니다.
print(young.get_age())

07. 여기서 잠깐!파이썬의 캡슐화

  • 파이썬은 언어 차원에서 캡슐화를 지원하지 않음(java에서는 캡슐화가 완벽하게 됨)

네임 맹글링

  • 파이썬에서 __로 접근이 불가능한 이유는, __로 만들면 파이썬 자체에서 _클래스 이름을 변수 앞에 붙이기 때문이다.
    • ex) age는 _Citizenage 로, resident_id는 _Citizenresident_id 로 변경됨

08. 캡슐화와 파이썬의 문화

  • 파이썬은 언어에서 캡슐화를 지원하지는 않지만 문화를 가지고 있음
  • _ 언더바 한개로 만든 변수 / 메소드 클래스는 외부에서 직접 접근하지 말라는 경고 표시로 사용하고, 파이썬 개발자는 약속처럼 생각하고 지킴
  • _ 언더바 한개로 만든 변수 / 메소드에도 접근이 가능함 ( 언더바 한개가 아무런 기능이 없음. 단지 경고 표시일 뿐임)

09. 데코레이터를 사용한 캡슐화

  • 캡슐화 전 사용하던 코드를 캡슐화 후 수정하지 않아도 됨
class Citizen:
    def __init__(self, name, age, resident_id):
        self.name = name
        self.set_age(age)
        self.__resident_ids = resident_id

    def set_age(self, value):
        if value < 0:
            print("나이는 0보다 작을 수 없습니다.")
            self._age = 0
        else: self._age = value

    @property
    def age(self): # _age의 getter method
        print("나이를 리턴합니다.")
        return self._age

    @age.setter
    def age(self, value):  # _age의 setter method
        print("나이를 설정합니다.")
        if value < 0:
            print("나이는 0보다 작을 수 없습니다.")
            self._age = 0
        else:
            self._age = value

young = Citizen("young", -10, "876")
print(young.age) # @property age를 실행하라는 의미가 됨
young.age = 30
print(young.age) # @age.setter age를 실행하라는 의미가 됨


#나이는 0보다 작을 수 없습니다.
#나이를 리턴합니다.
#0
#나이를 설정합니다.
#나이를 리턴합니다.
#30

10. 객체를 사용할 땐 최대한 메소드로

  • 변수를 직접 사용하는 것을 최소화할 수록 유지보수하기 쉬운 코드가 됨

3. 상속

01. 중복되는 코드

02. 상속이란?

상속이란 ? 두 클래스 사이에 부모-자식 관계를 설정하는 것

자식 class는 부모 class의 모든 메소드와 변수를 물려 받음

03. 부모 클래스 정의하기

부모 클래스 : Employee

자식 클래스 : Cashier, DeliveryMan

A는 B다 라고 말할 수 있을 때 상속관계를 설정할 수 있음

계산대 직원은 직원이다. 배달원은 직원이다.

04. 상속1 (부모로부터 물려받기)

# 부모 클래스 
class Employee:
    company_name = "코드잇 버거"
    raise_percentage = 1.03

    def __init__(self, name, wage):
        self.name = name
        self.wage = wage

    def raise_pay(self):
        self.wage *= self.raise_percentage

    def __str__(self):
        return Employee.company_name + " 직원 : " + self.name

# 자식 클래스 
class Cashier(Employee): # 부모 클래스가 Employee, 자식클래스가 Cashier 로 설정됨
    pass

# 자식 클래스
class DeliveryMan(Employee): # 부모 클래스가 Employee, 자식클래스가 Cashier 로 설정됨
    pass

younghoon = DeliveryMan("강영훈", 8900)
younghoon.raise_pay()
print(younghoon.wage)  # 9167.0
print(younghoon)  # 코드잇 버거 직원: 강영훈 

05. 상속과 관련된 메소드와 함수들

01. mro 메소드

# 04번 상속1 (부모로부터 물려받기) 코드에서 아래 mro 함수 프린트 
print(Cashier.mro())
  • Cashier 클래스가 상속하는 부모 클래스를 볼 수 있음.
[<class '__main__.Cashier'>, <class '__main__.Employee'>, <class 'object'>]
print(list.mro())
[<class 'list'>, <class 'object'>]
print(IndentationError.mro())
[<class 'IndentationError'>, <class 'SyntaxError'>, <class 'Exception'>, <class 'BaseException'>, <class 'object'>]

02. isinstance 함수

  • 어떤 인스턴스가 주어진 클래스의 인스턴스인지를 알려줌
  • isinstance(검사할 인스턴스, 기준클래스)
young = Cashier("강영훈", 8900)

print(isinstance(young, Cashier)) # 출력: True
print(isinstance(young, DeliveryMan)) # 출력: False
print(isinstance(young, Employee)) # 출력: True , 자식클래스로 만든 인스턴스는 부모 클래스의 인스턴스이기도 하다. 

03. issubclass 함수

  • 한 클래스가 다른 클래스의 자식 클래스인지를 알려줌
  • issubclass(검사할 인스턴스, 기준되는 부모 클래스)
print(issubclass(Cashier, Employee)) # 출력: True
print(issubclass(Cashier, object)) # 출력: True
print(issubclass(Manager, Employee)) # 출력: True
print(issubclass(Employee, list)) # 출력: False

06. 상속2 (오버라이딩)

  • 오버라이딩 : 부모 클래스에서 물려받은 변수나 메소드를 자식클래스에서 자식에 맞게 변경해서 사용하는 것
  • super 함수 : 부모클래스의 메소드를 호출할 수 있음
# 부모 클래스 
class Employee:
    company_name = "코드잇 버거"
    raise_percentage = 1.03

    def __init__(self, name, wage):
        self.name = name
        self.wage = wage

    def raise_pay(self):
        self.wage *= self.raise_percentage

    def __str__(self):
        return Employee.company_name + " 직원 : " + self.name

# 자식 클래스 
class Cashier(Employee): # 부모 클래스가 Employee에 없는 number_sold 변수를 실행함
    raise_percentage = 1.05 # 변수 오버라이딩

    def __init__(self, name, wage, number_sold):
        super().__init__(self, name, wage) # Employee.__init__(self, name, wage) 와 같음
        self.number_sold = number_sold

    def __str__(self):
        return Cashier.company_name + " 계산대 직원 : " + self.name

# 자식 클래스
class DeliveryMan(Employee): 
    pass

younghoon = Cashier("강영훈", 8900, 4)
print(younghoon)  # 코드잇 버거 계산대 직원 강영훈
print(younghoon.raise_percentage)

07. 상속3 (mro)

  • mro : 클래스가 상속받는 부모 클래스들이 순서대로 담김 리스트를 리턴
class Employee:
    company_name = "코드잇 버거"
    raise_percentage = 1.03

    def __init__(self, name, wage):
        self.name = name
        self.wage = wage

class Cashier(Employee):
    raise_percentage = 1.05

    def __init__(self, name, wage, number_sold=0):
        super().__init__(name, wage)
        self.number_sold = number_sold

print(Cashier.mro())   # [<class '__main__.Cashier'>, <class '__main__.Employee'>, <class 'object'>]

08. 상속 4 (기능 추가하기)

class Cashier(Employee):
    raise_percentage = 1.05

    def __init__(self, name, wage, number_sold=0):
        super().__init__(name, wage)
        self.number_sold = number_sold

    def take_order(self, money_received):
        if Cashier.burger_price > money_received:
            print("돈이 충분하지 않습니다. 돈을 다시 계산해서 주세요!")
            return money_received
        else:
            self.number_sold += 1 
            change = money_received - Cashier.burger_price
            return change

young = Cashier("강영훈", 8900, 0)
print(young.take_order(7000))

09. 상속 정리

 

12. 다중상속

class Egineer:
    def __init__(self, favorite_language):
        self. favorite_language = favorite_language

    def program(self):
        print("{}으로 프로그래밍 합니다.".format(self.favorite_language))

class TennisPlayer:
    def __init__(self, tennis_level):
        self.tennis_level = tennis_level

    def play_tennis(self):
        print("{} 반에서 테니스를 칩니다. ".format(self.tennis_level))

class EngineerTennisPlayer(Engineer, TennisPlayer): # Engineer, TennisPlayer 둘다로부터 변수와 메소드를 물려 받음
    def __init__(self, favorite_language, tennis_level):
      # 다중상속한 경우, super() 했을 때 어느 클래스로 상속할 것인지를 알 수 없음
        Engineer.__init__(self, favorite_language)
        TennisPlayer.__init__(self, tennis_level)

younghoon = EngineerTennisPlayer("파이썬", "초급")
younghoon.program()
younghoon.play_tennis()

13. 다중 상속의 위험성

  • 다중상속 시 동일 메소드 호출할 때 mro 로 확인했을 때 먼저 호출되는 클래스의 메소드가 호출됨
  1. 부모클래스끼리 같은 이름의 메소드를 갖지 않도록 하기
  2. 같은 이름의 메소드는 자식클래스에서 오버라이딩
  3. 사용하고 싶다면 추상클래스 여러 개를 다중 상속받는 형식으로 사용해보기

4. 다형성

같은 이름의 메소드가 클래스 혹은 객체에 따라 다르게 동작하도록 구현되는 것

다형성 : 여러 가지의 형태를 갖는 성질

01. 클래스 다형성1

  • shapes 변수에 들어가는 Rectangle, Circle 클래스 (둘다 동일한 메소드를 가짐)
from math import pi

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height 

    def area(self):
        return self.width * self.height 

    def perimeter(self):
        return 2*self.width * 2*self.height 

    def __str__(self):
        return " 밑변 {} , 높이 {}인 직사각형".format(self.width, self.height)

class Circle:
    def __init__(self, radius):
        self.radius= radius

    def area(self):
        return pi * self.radius * self.radius 

    def perimeter(self):
        return 2 * pi * self.radius

    def __str__(self):
        return "반지름 {}인 원".format(self.radius)

class Paint:
    def __init__(self):
        self.shapes = []

    def add_shape(self, shape):
        self.shapes.append(shape)

    def total_area_of_shapes(self):
        """그림판에 있는 모든 도형의 넓이의 합"""
        return sum([shape.area() for shape in self.shapes]) # 함수를 실행함

    def total_perimeter_of_shapes(shape(self):
        """그림판에 있는 모든 도형의 넓이의 합"""
        return sum([shape.area() for shape in self.shapes])

    def __str__(self):
        res_str = "그림판 안에 있는 도형들"
        for shape in self.shapes:
            res_str += str(shape) + "\n"
        return res_str

02. 클래스 다형성2

rectangle = Rectangle(3, 7)
circle = Circle(4)

paint_program = Paint()
paint_program.add_shape(rectangle)
paint_program.add_shape(circle)

print(paint_program.total_area_of_shapes)
print(paint_program.total_perimeter_of_shapes)

03. 상속없는 다형성의 한계

  • 동일한 메소드가 없는 경우도 있기 때문에, 에러가 날 수 있음
  • 아래와 같이 특정 클래스의 인스턴스인 경우에만 추가하도록 할 수도 있지만(특정클래스가 늘어날 수록 그 클래스를 모두 적기에는 무리가 있으므로) 상속을 활용한 다형성을 적용하는 방법을 배워보도록 하자
def add_shape(self, shape):
        """그림판에 도형을 추가한다"""
        if isinstance(shape, Circle) or isinstance(shape, Rectangle):
            self.shapes.append(shape)
        else:
            print("넓이, 둘레를 구하는 메소드가 없는 도형은 추가할 수 없습니다!")

04. 상속을 활용한 다형성 1 (일반 상속)

  • 무조건 있어야 하는 함수를 공통의 부모 클래스로 만들어서 자식 클래스에서 상속 받을 수 있음
from math import pi

class Shape:
    """도형 클래스"""
    def area(self):
        """도형의 넓이를 리턴한다. 자식 클래스가 오버라이딩 할 것"""
        pass

    def perimeter(self):
        """도형의 둘레를 리턴한다. 자식 클래스가 오버라이딩 할 것"""
        pass

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height 

    def area(self):
        return self.width * self.height 

    def perimeter(self):
        return 2*self.width * 2*self.height 

    def __str__(self):
        return " 밑변 {} , 높이 {}인 직사각형".format(self.width, self.height)

class Circle(Shape):
    def __init__(self, radius):
        self.radius= radius

    def area(self):
        return pi * self.radius * self.radius 

    def perimeter(self):
        return 2 * pi * self.radius

    def __str__(self):
        return "반지름 {}인 원".format(self.radius)

class Paint:
    def __init__(self):
        self.shapes = []

    def add_shape(self, shape):
        if isinstance(shape, Shape): # Shape 클래스를 Rectangle과 Circle 클래스에서 상속했기 때문에 이렇게 사용할 수 있음
            self.shapes.append(shape)
        else:
            print("넓이, 둘레를 구하느 메소드가 없는 도형은 추가할 수 없습니다.")

    def total_area_of_shapes(self):
        """그림판에 있는 모든 도형의 넓이의 합"""
        return sum([shape.area() for shape in self.shapes]) # 함수를 실행함

    def total_perimeter_of_shapes(shape(self):
        """그림판에 있는 모든 도형의 넓이의 합"""
        return sum([shape.area() for shape in self.shapes])

    def __str__(self):
        res_str = "그림판 안에 있는 도형들"
        for shape in self.shapes:
            res_str += str(shape) + "\n"
        return res_str

06. 상속을 활용한 다형성3 (추상클래스 개념)

  • 추상클래스 : 여러 클래스들의 공통점을 추상화해서 모아놓은 클래스
  • ABC : Abstract Base Class
  • 함수 선언 위에 @abstractmethod 를 붙이면 추상 클래스
  • 추상 메소드가 1개 이상 있으면 추상 클래스
  • 추상 클래스로는 인스터스를 만들 수 없음.
  • 추상메소드 (ex) area, perimeter method 는 자식 클래스에서 반드시 오버라이딩 해서 구현해야 함
  • 오버라이딩의 방향을 제시하기 위해 추상 메소드에는 type hinting을 해주는 것이 좋음
  • @abstractmethod 를 사용하지 않으면 일반 메소드 이고, 반드시 오버라이딩 해야되는 추상 메소드와는 다르게 일반 메소드는 물려받은 그대로 사용할지, 오버라이딩 할지를 자식 클래스에서 결정함
from math import pi
from abc import ABC, abstractmethod

class Shape(ABC) -> float: # ABC를 상속받아 추상클래스로 만듬
    """도형 클래스"""

    @abstractmethod # 추상메소드
    def area(self) -> float:
        """도형의 넓이를 리턴한다. 자식 클래스가 오버라이딩 할 것"""
        pass

    @abstractmethod    
    def perimeter(self):
        """도형의 둘레를 리턴한다. 자식 클래스가 오버라이딩 할 것"""
        pass

07. 상속을 활용한 다형성4 (추상클래스 활용)

class EquilateralTriangle(Shape):
    """정삼각형 클래스 """
    def __init__(self, side):
        self.side = side

    def area(self): # 오버라이딩
        return sqrt(3) * self.side * self.side / 4

    def perimeter(self): # 오버라이딩
        return 3 * self.side 

09. 추상 클래스 더 알아보기

지금까지 추상 메소드의 내용으로 그냥 pass만 썼는데, 사실 아래와 같이 추상 메소드 안에 다른 내용을 써도 됨

from abc import ABC, abstractmethod

class Shape(ABC):
    """도형 클래스"""
    @abstractmethod
    def area(self) -> float:
        """도형의 넓이를 리턴한다: 자식 클래스가 오버라이딩할 것"""
        print("도형의 넓이 계산 중!")# ---------------- 추가된 코드    @abstractmethod
    def perimeter(self) -> float:
        """도형의 둘레를 리턴한다: 자식 클래스가 오버라이딩할 것"""
        pass
  • 어차피 추상 클래스는 상속받는 자식 클래스에서 오버라이딩 해야 하지만 경우에 따라 유용할 때가 있음
  • 자식 클래스에서 오버라이딩 할 때 super 함수로 미리 채워진 내용을 가져와서 재활용할 수 있음
class Rectangle(Shape):
    """직사각형 클래스"""
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        """직사각형의 넓이를 리턴한다"""
        super().area()# ---------------- 부모의 메소드를 가져다 씀return self.width * self.height

    def perimeter(self):
        """직사각형의 둘레를 리턴한다"""
        return 2*self.width + 2*self.height

rectangle = Rectangle(3, 4)
print(rectangle.area())# 출력: 도형의 넓이 계산 중! 12

 

10. 추상 클래스 다중 상속 받기

  1. 추상 클래스 다중 상속은 일반적으로 많이 사용한다.
  2. 다중 상속받는 부모 추상 클래스들이 추상 메소드로만 이뤄져 있으면 아무 문제 없이 다중 상속받을 수 있다.
  3. 다중 상속받는 부모 추상 클래스들 간에 이름이 겹치는 일반 메소드가 있으면 일반 클래스를 다중 상속받을 때와 동일한 문제가 생길 수 있다.

11. 함수/메소드 다형성

  • 옵셔널 파라미터 : 기본 값을 미리 지정해준 파라미터, 파라미터 중 가장 뒤에 정의해야 함
def print_name(first_name, email=""):
  • None : 아무값도 없다. 함수 호출 시 어떤 파라미터에 값을 전달하지 않은 경우
  • 갯수가 정해지지 않은 파라미터 *변수명으로 넣어줌
    • *변수명 tuple에 담겨서 전달됨
def print_message(message, *numbers):
    print(message)
    return sum(numbers)

print(print_message("test", 7,3,5)
print(print_message(7,3,5, message="test")

댓글

Designed by JB FACTORY