Little Jay

Objected-Oriented Programming의 기둥 본문

Univ/Study

Objected-Oriented Programming의 기둥

Jay, Lee 2022. 1. 27. 17:59

객체 지향의 기둥

  1. 추상화(Abstraction)
  2. 캡슐화(Encapsulation)
  3. 상속(Inheritance)
  4. 다형성(Polymorphism)

1. Abstraction


파이썬 코드를 짤때는 추상화를 잘 설명을 해야합니다.

추상화는 이미 파이썬 자체에서 잘 되어있다고 볼 수 있을 것 같습니다.

이게 무슨 뜻이나면, 우리가 list 메소드를 쓸 때 append나 del 같은거는 처음 봤을 때 뭔지 모릅니다.

우리가 문법을 배워서 알면 되지만 처음 봤을때는 이게 어떤 뜻인지는 모르는것이죠.

하지만 이것이 대략적으로 어떤 기능을 하는지 직관적으로 유추를 할 수 있겠습니다.

우리가 사용자 정의의 클래스나 함수를 새로 만들었다면 우리의 코드를 읽는

다른 사람은 그게 정확히 뭘 의미하는지, 어떤 기능을 하는지 한눈에 파악하기 힘들게 됩니다.

그래서 추상화 한 것들을 잘 설명해야 합니다.

변수 이름을 구체적으로 쓰고, """ """ 같이 주석, 즉 documentaion화를 잘 해놔야 합니다.

 

class User:
    '''SNS의 유저를 나타내는 클래스'''
    count = 0

    def __init__(self, name, email, pw):
        '''이름, 이메일, 비밀번호를 인스턴스 변수로 갖고, 인스턴스가 생성될 때마다 클래스 변수 count를 1씩 증가시킨다'''
        self.name = name
        self.email = email
        self.pw = pw

        User.count += 1

    def say_hello(self):
        '''유저의 이름을 포함한 인사 메시지를 출력한다'''
        print("안녕하세요! 저는 {}입니다!".format(self.name))

    def __str__(self):
        '''유저 정보를 정의된 문자열 형태로 리턴한다'''
        return "사용자: {}, 이메일: {}, 비밀번호: ******".format(self.name, self.email)

    @classmethod
    def number_of_users(cls):
        '''총 유저 수를 출력하는 클래스 메소드'''
        print("총 유저 수는: {}입니다".format(cls.count))

help(User)

 

그리고 type hinting 기능들을 적극적으로 활용할 수 있습니다.

Python에서는 변수에 어떤 자료형이든 올 수 있기때문에 어떤 타입의 변수를 선언할 수 있습니다.

C++이나 JAVA같이 초기에 강력한 타입 검사를 지원하는 언어같은 경우는 걱정이 없지만,

일반적으로 Python, JS같은(물론 TypeScript는 제외) 언어에서 type hinting을 주게된다면

제 3자의 입장에서 코드를 읽기 편해집니다. 개발은 나 혼자 하는 것이 아닌 팀원과 하는 것이니깐요.

아래의 코드를 참조하여 type hinting이 무엇인지 파악하면 될 것 같습니다.

하지만 type hinting은 어디까지나 recommendation이지 이를 필수적으로 지켜야한다는 개념은 아닙니다.

class BankAccount:
	interest: float = 0.02  #이렇게 형을 미리 설명해주거

	def __init__(self, name, balance):
		self.name = name
		self.balance = balance
	
	def deposit(self, amount: float) -> None: #return 값을 -> 기호로 하면 좋겠다는거
		self.balance += amount

 

2. Encapsulation


캡슐화의 의미는 

  • 객체 일부 구현 내용에 대한 외부로부터의 직접적인 엑세스를 차단하는 것
  • 객체의 속성과 그것을 사용하는 행동을 하나로 묶는 것

이렇게 두 가지로 생각하시면 될 것 같습니다.

 

첫 번째 정의에 맞게 하는 방법은 변수, 인자, 함수들의 앞에 underscore 을 두개 붙이면 됩니다.

class Citizen:
    """주민 클래스"""
    drinking_age =19

    def __init__(self, name, age, resident_id):
        self.name = name
        self.age = age
        self.__resident_id = resident_id #이렇게!

    def authentificate(self, id_field):
        '''본인이 맞는지 확인하는 메소드'''
        return self.__resident_id = id_field

    def can_drink(self):
        '''음주 가능한 나이인지 확인하는 메소드'''
        return self.age >= Citizen.drinking_age

    def __str__(self):
        return self.name + "씨는" + str(self.age) + "살 입니다"

두 번째 정의에 맞게 쓰는 것은 내부에서 먼저 접근하고, 수정하는 것입니다.

주로 getter와 setter methods를 이용하는 것이죠.

def get_age(self): #getter method
        return self.__age

def set_age(self, value): #setter method
        self.__age = value

 

그렇지만 언더스코어 두개를 사용하는거는 정식 코딩스타일이 아닙니다.

언더스코어 두개를 사용하는게 데이터를 private하게 만드는 것은 맞지만,

파이썬 스타일 가이드는 언더스코어 하나를 쓰라고 권장하고 있습니다. 

사실 이렇게 되면 private하게 사용을 못하게 됩니다. 그냥 출력하라고하면 출력이 되버리지요.

그렇기 때문에 이것은 개발자 들의 약속이라고 생각해서 써야합니다.

C++이나 JAVA같이 강력하게 private, public을 구분하지 못합니다.

 

그리고 getter, setter를 함수로 적기 귀찮은 경우가 있습니다.

이런 경우 매번 함수를 호출해줘야하기 때문이죠.

이런 경우 decoration이라는 기능으로 처리할 수 있습니다.

class Citizen:
    """주민 클래스"""
    drinking_age =19

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

    def authentificate(self, id_field):
        '''본인이 맞는지 확인하는 메소드'''
        return self._resident_id == id_field

    def can_drink(self):
        '''음주 가능한 나이인지 확인하는 메소드'''
        return self._age >= Citizen.drinking_age

    def __str__(self):
        return self.name + "씨는" + str(self._age) + "살 입니다"

    @property    #getter
    def age(self):
        print('나이 리턴')
        return self._age

    @age.setter  #setter
    def age(self, value):
        print('age setting')
        if value < 0:
            print('age can\'t be below 0')
            self._age = 0
        else:
            self._age = value

lee = Citizen('jay', 18, '1234567')
print(lee.age)
lee.age = 30
print(lee.age)

이렇게 decoration으로 처리를 하면 좋은 점은 lee.age 이렇게 할 때 init 즉 생성자쪽에서 처리한 age가 아니라

아래쪽에서 정의한 함수 age로 이동하게 됩니다. 

즉, lee.age = 30 이라고 했을때는 아래쪽의 setterr 메소드가 실행이 됩니다.

 

3. Inheritance


상속은 별다른 내용이 없습니다.

파이썬의 상속도 다중 상속이 가능하고,

이 상속을 통해 코드를 간결화 시킬 수 있다 정도의 이정이 있는 것 같습니다.

 

4. Polymorphism


다형성.

한국 말로도 어려운 단어인 것 같습니다.

우선 아래의 예시를 보겠습니다.

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 f"밑변 {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 f"반지름 {self.radius}인 원"

class Cylinder:
    """원통 클래스"""
    def __init__(self, radius, height):
        self.radius = radius
        self.height = height

    def __str__(self):
        return "밑면 반지름 {}, 높이 {}인 원기둥".format(self.radius, self.height)



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(self):
        return sum([shape.perimeter() for shape in self.shapes])

    def __str__(self):
        res_str = "Shape in palate"
        for shape in self.shapes:
            res_str += str(shape) + '\n'
        return res_str

Paint 클래스에서 shape가 어떤 형인지 알고 그리고 아래의 total_area, total_perimeter는

어떻게 알고 shape.area() 같은 메소드를 사용할 수 있게 하는 것일까라는 의문을 던질 수 있습니다.

이게 바로 파이썬의 다형성이라는 개념입니다.  일단은 쓰고, 안되면 에러 나는 것이죠.

예를 들어 Cylinder Class 를 넣으면 에러가 나게 됩니다.

 

그래서 이 에러를 해결해주기 위해 아래와 같은거로 보완을 해주긴 하지만 여전히 문제가 존재합니다.

 

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 f"밑변 {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 f"반지름 {self.radius}인 원"

class Cylinder:
    """원통 클래스"""
    def __init__(self, radius, height):
        self.radius = radius
        self.height = height

    def __str__(self):
        return "밑면 반지름 {}, 높이 {}인 원기둥".format(self.radius, self.height)

class EquallateralTriangle(Shape):
    def __init__(self, side):
        self.side = side


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

    def add_shape(self, shape):
        if isinstance(shape, Shape):
            self.shapes.append(shape)
        else:
            print('you cannot add the shapes')


    def total_area_of_shapes(self):
        return sum([shape.area() for shape in self.shapes])

    def total_perimeter_of_shapes(self):
        return sum([shape.perimeter() for shape in self.shapes])

    def __str__(self):
        res_str = "Shape in palate"
        for shape in self.shapes:
            res_str += str(shape) + '\n'
        return res_str

이렇게 되면 triangle 처럼 Shape로 상속하고 오버라이딩을 하지 않으면 결국 또 에러가 나버리게 됩니다.

 

그렇다면 Shape를 상속할 때 강제로 오버라이딩을 할 수 있게 하는 방법은 없을까요?

이 질문에 대해서 Python에서는 추상 클래스를 지원합니다.

 

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

    @abstractmethod
    def perimeter(self):
        pass
Comments