모든 게시물은 macOS Monterey 12.0.1 버전을 기준으로 작성하였습니다.
부스트캠프 AI Tech 3기 예비 캠퍼를 위한 Pre-Course 강좌를 바탕으로 작성하였습니다.
Python Object Oriented Programming
파이썬뿐만 아니라 자바와 C ++ 역시 OOP 기반의 언어들이다.
데이터 흐름에 기반한 절차지향적 프로그래밍 방법이 하드웨어의 엄청난 성장과 함께 한계점을 보였고,
큰 문제를 작게 쪼개는 것이 아니라 작은 문제들을 해결할 수 있는 객체들을 만든 뒤에 이 객체들을 조합하여
큰 문제를 해결하는 Bottom-Up 방식이 등장한다.
따라서 객체간 독립성이 높아 코드 수정이 필요할 때도 적은 범위의 코드 수정으로 문제 해결이 가능하고
이는 유지 관리 비용을 낮출 수 있다는 장점을 만들어 준다.
객체는 속성(Attribute)과 행동(Action)을 가진 일종의 물건이다. 그렇다면 '객체 지향'이란 무엇일까?
OOP에 대해 이야기할 때면 Class와 Instance에 대한 이야기를 많이 한다.
Class는 어떤 객체의 설계도이고, 그 실제 구현체가 Instance = 객체이다.
붕어빵 틀과 붕어빵이라고 비유한단다. 틀은 하나인데 팥, 말차, 고구마, 슈크림 붕어빵을 만들 수 있다.
개념이 확 이해되고 와닿진 않는다. 역시 이해는 구현이다.
self를 강조하여 주셨는데 class 내에서는 생성된 instance를 self라고 표현하고
class 밖에서는 생성된 instance를 객체명(사용자가 정의한 변수)을 통해 표현한다.
지금까지 변수명은 끌리는대로 snake_case와 CamelCase를 혼용하여 사용하고 있었는데
함수/변수명에는 snake_case, Class 이름에는 CamelCase를 사용한다는 규칙이 있었다..
그리고 오늘 배웠던 부분에서 가장 놀랐던 부분. 전공과목에서 매번 나오는 과제를 해결하기 위한 목적으로만
공부하였다는 사실이 여실히 드러나는 부분이었다. class를 정의하고 나서 객체를 프린트하면 알 수 없는 문자들만
바라보곤 했었는데 언더바 2개에 비밀이 숨겨져 있었다.
__는 특수한 예약 함수나 변수와 함수명 변경(맹글링)에 사용된다.
그 예로 __main__, __add__, __str__, __eq__ 등이 있다.
소스코드와 결과로 사용되는 예시를 확인하자.
__str__ 을 이용하여 생성한 객체를 print 하면 원하는 문구가 출력될 수 있게 설정할 수 있다.
또 다른 예로 __add__ 를 사용한 것이다.
아래 잘 정리되어 있으니 확인하여 공부하면 좋겠다.
https://corikachu.github.io/articles/python/python-magic-method
OOP, 특히 객체의 특징은 Attribute와 Method로 설명할 수 있음을 복기하자.
Attribute는 def __init__ (self, att1, att2, ... , ) 에서 정의하고
Method는 def method_name(self, argument1) 를 통해 정의한다.
OOP Implementation Example
위 조건을 바탕으로 Notebook과 Note 두 가지 Class를 만들어보자.
고민해보라고 시간을 주셨는데 방향성조차 잘 잡히지 않았다..
이제 코드를 볼텐데 그대로 옮겼는데 원하는 결과가 나오지 않아서 자체적으로 수정을 했다.
class Note(object):
def __init__(self, content = None):
self.content = content
def write_content(self, content):
self.content = content
def remove_all(self):
self.content = ""
def __add__(self, other):
return self.content + other.content
def __str__(self):
return "노트에 적힌 내용입니다 : %s" %(self.content)
class NoteBook(object):
def __init__(self, title):
self.title = title
self.page_number = 1
self.notes = {}
def add_note(self, note, page = 0):
if self.page_number < 300:
if page == 0:
self.notes[self.page_number] = note
self.page_number += 1
else:
# self.notes = {page : note} % 기존 코드
self.notes[page] = note # % 내가 수정한 코드
self.page_number += 1
else:
print("Page가 모두 채워졌습니다.")
def remove_note(self, page_number):
if page_number in self.notes.keys():
return self.notes.pop(page_number)
else:
print("해당 페이지는 존재하지 않습니다.")
def get_number_of_pages(self):
return len(self.notes.keys())
기존 코드의 문제점은 ※ 물론 내가 생각할 때 ※
자, 처음에 페이지 수를 입력하지 않은 노트를 추가하고, 이어서 페이지 수를 100이라고 쓴 노트를 추가한다.
첫 노트의 페이지 수가 기입되지 않았으니 if 문에 걸려 notes 딕셔너리의 key 값 = self.page_number = 1 에
첫 노트가 들어갈 것이다. 후에 페이지 100 노트가 들어오면 else 문에 걸려서 self.notes
즉, 딕셔너리 자체를 {page : note}로 초기화한다.
첫 번째 넣어놨던 노트가 사라지게 되는 것이다.
따라서 딕셔너리 자체를 초기화 할 게 아니라
notes 딕셔너리의 page = 100 이라는 key에 노트를 넣어야겠다고 판단했다.
OOP Characteristics : 실제 세상을 컴퓨터 세상에 모델링한다.
개노답 삼형제라고 하던데 쟤네들 때문인지 네트워크 문제로 아래부터 싹 날아가서 다시 쓴다 ^ㅡ^
첫째, Inheritance (상속)
Person이라는 부모 class의 속성에는 name과 age라는 두 가지 속성이 있는데
자식 class인 Korean 클래스가 그 속성을 그대로 상속 받았기에 속성을 그대로 이용할 수 있다.
class 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), "살 입니다.")
def __str__(self):
print("저의 이름은 ", self.name, "이구요, 제 나이는 ", str(self.age), "살 입니다.")
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): # 새로운 Method 추가
print("열심히 일을 합니다.")
def about_me(self): # 부모 클래스 함수 재정의
super().about_me() # 부모 클래스 함수 사용
print("제 급여는 ", self.salary, "원 이구요, 제 입사일은 ", self.hire_date, "입니다.")
Employee라는 자식 클래스를 정의하는 방법을 살펴보자.
super().__init__(name, age, gender) 을 보면 super() 명령어는 부모 클래스의 init을 불러오는 역할이다.
아래 about_me 메서드를 정의하는 때에도 부모 클래스의 함수에 추가적으로 작업을 할 수도 있다.
따라서 "저의 이름은 Daeho 이구요, 제 나이는 34 살 입니다." 는 부모 클래스에서 정의한 메서드인데
super().about_me()로 먼저 호출하고, 뒤에 "제 급여는 ~~~~"을 추가적으로 붙인 것을 확인할 수 있다.
둘째, Polymorphism (다형성)
같은 이름 메소드의 내부 로직을 다르게 해서 상속을 하거나 조금 다르게 사용할 수 있다.
부모 클래스에서 상속을 받을 때, 같은 이름이지만 다른 역할을 하게 만들 수 있다.
class Animal:
def __init__(self, name):
self.name = name
def talk(self):
raise NotImplementedError("Subclass must implement abstract method")
class Cat(Animal):
def talk(self):
return 'Meow!'
class Dog(Animal):
def talk(self):
return 'Woof! Woof!'
Animal이라는 부모 클래스로부터 상속을 받는 Cat과 Dog 클래스를 만들었는데
talk()라는 함수를 각각 다르게 만들 수 있다.
즉, Cat과 Dog의 비슷한 역할을 하는 함수 이름을 굳이 다르게 만들 필요가 없다는 것이다.
이처럼 같은 이름을 쓰되, 목적에 따라 내부 구현을 약간씩 다르게 만드는 것을 Polymorphism이라 한다.
셋째, Visibility (가시성)
앞서 우리가 객체를 만들고 그 안에 Attribute나 Method를 만들어 놨는데
누구나 객체 안에 모든 변수를 볼 필요가 없다.
- 객체를 사용하는 사용자가 임의로 정보를 수정하는 것 방지
- 필요 없는 정보에는 접근할 필요가 없으니 미리 예빵
- 만약 제품으로 판매한다면 소스의 보호
즉, 객체의 정보를 볼 수 있는 정도를 조절하는 것으로 다른 표현으로는 Encapsulation이 있다.
캡슐화 또는 정보 은닉이라고도 하며, Class 설계에 있어 Class 사이의 간섭/정보 공유를 최소화한다.
예를 들어 확인하는 것이 이해하기에 좋다.
언더바 두 개, __를 이용해야 한다. Private 변수라고 해서 외부에서 얘를 불러서 사용하지 못하게 만드는 것이다.
분명 Inventory 클래스에는 items라는 attribute가 있는데 없다고 AttributeError가 발생한다.
만약 언더바 두 개 없이 Attribute를 정의하였다면 클래스 밖에서 접근이 가능하다.
분명 우리의 목적은 Product 타입의 객체만 items 리스트에 넣는 것인데
언더바로 Private 변수 선언을 하지 않으니 Class 밖에서도 접근 후 추가가 가능했다.
하지만 아무도 사용하지 못하게 한다면 또 그래서 생기는 문제들이 있을 것이다.
그 Visibilty의 정도를 조정하는 방법에 대하여 생각해보자.
가장 많이 사용되는 방법은 @property 라는 decorator를 활용하여 밖에서는 여전히 접근하지 못하지만
내부에서는 접근할 수 있게끔 하는 방법이 있다.
class Product():
pass
class Inventory():
def __init__(self):
#self.items = []
self.__items = []
@property
def items(self):
return self.__items
def add_new_item(self, product):
if type(product) == Product:
#self.items.append(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를 my_inventory.__items 이런식으로 접근하려면 접근이 불가능하지만
@property는함수명을 Attribute 변수명처럼 사용할 수 있게 해줘서
data = my_inventory.items
data.append('abc') 이런 식으로 정보 추가가 가능하다.
Decorate ?
그렇다면 decorate에 대해서 조금 더 알아보자.
이를 이해하기 위한 개념들이 있다.
- first-class objects
- inner function
- decorator
첫째, First-class Objects
변수에도, 데이터 구조에도 할당이 가능한 객체를 말한다.
파라미터 혹은 리턴 값으로 사용이 가능한데 이는 함수 자체가 파라미터 혹은 리턴 값으로 사용된다는 것이다.
생각해보면 map 함수의 파라미터로 함수가 쓰였었다. 파이썬의 함수는 일급함수다.
def square(x):
return x * x
f = square
-> 함수를 변수로 사용하였다.
f(5)
def cube(x):
return x*x*x
def formula(method, argument_list):
return [method(value) for value in argument_list]
-> 함수를 파라미터로 사용하였다.
언제는 square, 언제는 cube를 쓰고 싶은데 그럴 때마다 formula를 만드는 것보다
method에 넣으면 훨씬 효율적이다. 이처럼 구조와 체계를 만들 수 있다는 것이 장점이다.
둘째, Inner Function
함수 내에 또 다른 함수가 존재한다.
Inner Function을 Return 값으로 반환하는 형태를 Closures라 하는데 사실 이해가 어려웠다.
어떤 함수의 중요한 변수나 로직에 대한 접근과 임의의 수정을 제한하면서 기능을 사용하고 싶을때 이용한다..?
https://tibetsandfox.tistory.com/9
그나마 이해를 돕는 블로그가 있어 첨부하고 넘어간다.
또 다른 예로는 두 수를 곱하는 간단한 함수를 만든다고 하자.
임의의 두 수를 곱하는 함수를 만드는 것이 목적이라면
def multiply_of_n(number, n):
return number * n
위와 같이 하고 말겠지만 9와 곱해지는 수, 예를 들어 9 x 1 ~ 9 x 100 까지의 값이 필요하다면
저 함수만으로 원하는 값을 구하기 어려울 것이다. 아래와 같이 해결할 수는 있겠다..
def multiply_of_number_n(base_number):
def n_multiply(n):
return base_number * n
return n_multiply
# 미리 외부함수의 parameter값을 고정
calculate_nine_mul_n = multiply_of_number_n(9)
for i in range(1,101):
print(calculate_nine_mul_n(i)
셋째, Decorator
Decorator는 복잡한 클로져 함수를 간단하게 만들 수 있다.
@star의 printer 함수가 star 함수의 input으로 들어간다.
printer 함수에 의해 "Hello"가 @star의 msg로 들어가고
inner function이 돌아가면서 별표가 먼저 그려지고
func 즉, printer가 돌아간 후 다시 별표가 그려지는 원리다.
또, 약간의 코드에 변형을 가해주면 아래와 같은 결과도 만들 수 있다.
즉, star라는 decorator를 이용하여 func 앞뒤로 꾸며주는 작업을 하는 것이다.