Python Object Oriented Programming
객체 지향 프로그래밍이란?
Python을 통해 개발하는데는 여러 방법이 있다. 그 중에서 만들어 놓은 코드를 재사용 하고 싶은 경우 활용 할 수 있는 프로그래밍 방법이다. 내가 타인의 코드를 활용하기 위해서, 혹은 타인이 나의 코드를 활용하기 쉽도록 구조화 한 것으로, Python 외의 언어에서도 지원하는 프로그래밍 방법이다.
Class and Object 개요
Class는 객체의 설계도에 해당한다. 이 설계도에서 생성되는 구현체를 Instance라 부른다.Object는 현실 세계에 존재하는 모든 것을 의미하며, 이는 속성과 행동을 가진다. 이를 프로그래밍 상에서 표현하면, 객체가 된다. 속성은 Field
행동은 Method
로 표현된다.
특징은 아래와 같다.
- Class의 Name Convention은 CamelCase를 따른다.
- Class는 함수와 비슷하게 선언하나,
def
대신class
,parameter
대신상속 받을 Class
를 입력한다.
- Class내의 모든 Member는 parameter첫 자리에
self
를 기재한다.
self
는 생성된 Instance 자신을 가르키는 키워드이다.
__
로 시작하는 속성이나 메서드는 특수한 예약함수나, 맨글링으로 사용된다는 의미를 갖는다.
__init__
은 instance를 초기화 하는 예약 함수로, 자동으로 콜 백된다.
__str__
은print
함수에 instance를 넣었을 때 출력되는 값을 정하는 속성이다. 이 속성이 없는 경우에는 메모리주소만 출력 된다.
- Method(객체의 행동)를 정의하는 방법은
class
블록 내에서def
로 정의하는 것이다.
OOP Example
Note를 정리하는 프로그램을 만들어 보자. 조건은 아래와 같다.
- Note에 뭔가 적을 수 있다.
- Note에는 Content가 있고, 이를 제거할 수도 있다.
- 두 개의 Note는 하나로 합칠 수 있다.
- Note는 Notebook에 삽입된다.
- Notebook은 Note가 삽입 될 때 페이지를 생성하며, 최대 300 page까지 저장할 수 있다.
- 300 page를 넘어가면 Note를 삽입할 수 없다.
우선, class는 Notebook과 Note 두 개를 만들어야 한다. 어떤 Field와 Method가 필요할까? 이를 정리해보자.
- Note
- Field
- content
- Method
- write_content
- remove_all
- Field
- Notebook
- Field
- title
- page_number
- notes
- Method
- add_note
- remove_note
- get_number_of_pages
- Field
이를 구현하면 다음과 같을 것이다.
# Note
class Note(object) :
# initializer
def __init__(self, content='') :
self.content = content
# Methods
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 self.content
# Notebook
class Notebook(object) :
# initializer
def __init__(self, title) :
self.title = title
self.page_number = 1
self.notes = {}
# Methods
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.page_number += 1
else :
if len(self.notes.keys()) == 300 :
print("page가 가득 찼습니다. 최대 300 page까지 저장 가능합니다.")
else :
print(f"빈 page는 {set(range(300))-set(notes.keys())}입니다.")
def remove_note(self, page_number) :
if page_number in self.notes.keys() :
return self.notes.pop(page_number)
else :
print("해당 page는 존재하지 않습니다.")
def get_number_of_pages(self) :
return len(self.notes.keys())
OOP의 특징
실제 세상을 프로그래밍 세계에 모델링 하고자 한다. 이를 잘 하기 위해 크게 3가지 개념이 도입된다.
- Inheritance (상속)
- Polymorphism (다형성)
- Visibility (캡슐화)
상속이란, Class 생성 시 특정 Class로부터 Field와 Method를 물려받는 개념을 의미한다. 최상위 조상 클래스는 object
이며, 모든 Class는 자동으로 object
의 상속을 받게 된다. 자식 Class 내에서는 super()
메서드를 통해 부모의 Member를 호출할 수 있다.
다형성이란, 같은 이름의 메서드를 다르게 사용할 수 있는 것을 의미한다. 부모 Class로부터 물려받은 Member를 자식 Class가 Overwrite하게 되면, 자식 Class의 Member가 호출된다. 부모 Class는 자식 Class가 상속을 받을 때 해당 Member를 구현하도록 raise NotImplementedError
로 강제할 수 있다.
캡슐화란, 객체의 정보를 볼 수 있는 레벨을 조절하는 것이다. 필요에 따라 정보의 수정을 방지하거나, 접근할 수 없도록 설정할 수 있다. 타 Class간의 필요한 만큼 정보 공유를 할 수 있도록 설계하거나, Interface만으로 사용할 수 있도록 설계할 수 있다.
Class 내의 Field에 __
를 앞에 붙임으로써 외부에서의 접근을 제한할 수 있다. 다른 Method에서 일부의 정보를 return
하는 형식으로 정보를 보호하며, 제공할 수 있다.
Decorator
Decorator를 이해하기 위해선 아래의 개념들을 이해해야 한다.
- First-class objects
- Inner function
- Decorator
First-class objects는 변수에도 할당할 수 있고, 데이터 구조에도 할당 할 수 있는 객체를 의미한다. Python에서는 모든 함수가 First-class object이다. 그렇기에 argument로 전달 가능하면서, 동시에 return value로도 사용할 수 있다.
>>> obj = [1,2,3]
>>> def func(x) :
... return x
>>> list(map(func, obj))
[1, 2, 3]
Inner function은 함수 내에 있는 함수를 의미한다. 함수 블록 내에서 함수를 정의하는 구조이며, 바깥 함수의 변수를 사용할 수 있다. 또한, Inner function 자체를 return할 수도 있다. 이를 Closures라고 한다.
>>> def print_msg(msg) :
... def printer(num=0) :
... print(' '*num + msg)
... return printer
>>> another = print_msg("Hello, Python!")
>>> type(another)
<class 'function'>
>>> another()
Hello, Python!
>>> another(10)
Hello, Python!
Decorator는 복잡한 Closures 함수를 간단하게 변경해준 것이다.
>>> def star(func) :
... def inner(*args, **kwargs) :
... print(args[1]*30)
... func(*args, **kwargs)
... print(args[1]*30)
... return inner
...
>>> @star
... def printer(msg, mark) :
... print(msg)
...
>>> printer("Hello", "&")
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
Hello
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
>>> def generate_power(exponent) :
... def wrapper(f) :
... def inner(*args) :
... result = f(*args)
... return exponent ** result
... return inner
... return wrapper
...
>>> @generate_power(2)
... def raise_two(n) :
... return n**2
...
>>> raise_two(7)
562949953421312
>>> 2 ** 49
562949953421312
Module and Project
Module과 Project란?
남이 만들어 놓은 코드를 활용하거나, 나의 코드를 남이 활용할 수 있도록 구조화 한 체계의 일부이다. 코드는 Module안에 위치하고, Module을 묶어 Package로 관리하는 구조를 가진다.
Module은 어떤 대상의 부분 혹은 조각을 의미한다. 프로그램에서는 작은 프로그램 조각을 의미하며, 모듈을 모아서 하나의 큰 프로그램을 개발할 수 있는 것이다. Python 언어 자체에서도 이미 만들어 놓은 Built-in Module이 있다.
Package는 모듈을 모아놓은 단위이며, 하나의 프로그램의 기능을 한다. 프로그램을 이렇게 Package형태로 공개하면, 다른 프로그램에서 해당 기능을 사용하기 쉽다는 장점이 있다. 나 역시 남이 모듈화 해놓은 프로그램은 내가 다시 구현할 필요가 없으며, API를 통해 이를 쉽게 사용할 수 있다. 이처럼 공개된 Package를 하나의 Project로 취급한다.
Module
Module은 하나의 Python Script File을 의미한다. 이 모듈의 기능은 import
문을 통해 load하여 사용할 수 있다. 이렇게 Module을 import하기 위해선 같은 Directory내에 있거나, sys.path
에 경로 설정이 되어 있어야 한다.
Built-in module은 기본적으로 sys.path
에 경로 설정이 되어 있기 때문에 다른 조치 없이 import가 가능하다. Module을 import하게 되면, import된 파일은 __pycache__
디렉토리 내에 .pyc
형태로 컴파일 되어 저장된다.
Module을 import할 때에는 범위를 설정할 수 있다. from module import function
혹은 from package import module
등의 방법이 있다. 또한, import pandas as pd
와 같이 Module의 Alias를 설정해서 사용할 수 있다.
Package
하나의 대형 프로젝트를 만드는 코드의 묶음을 의미한다. 다양한 Module이 Directory의 체계로 구성되어 있는 형태이다. 각 Driectory에서는 필요한 기능을 구현한 Module이 위치한다. 다양한 오픈 소스들이 이처럼 Package 형태로 관리되고 있다. 예시는 다음과 같다.
game
├── __init__.py
├── __main__.py
├── image
│ ├── __init__.py
│ ├── character.py
│ └── object.py
├── sound
│ ├── __init__.py
│ ├── bgm.py
│ └── echo.py
└── stage
├── __init__.py
├── main.py
└── sub.py
Python의 Low Version에서는 Package 관리를 위해 __init__.py
, __main__.py
과 같은 키워드 파일명이 사용되었다.
__init__.py
파일은 현재 Directory가 Package임을 알리는 초기화 스크립트이다. Python version 3.3 이하에서는 반드시 필요했으나, 이상의 버전에서는 해당 파일이 없더라도 해당 Directory를 Package로 간주하여 작동한다.
__main__.py
파일은 Package 자체를 실행할 때 실행될 Main Method를 작성해 놓는 파일에 해당한다.
Virtual Environment
Project를 수행하기 위해 필요한 Package를 설치하고 사용하게 되는데, 이 환경을 구축할 때 충돌이 일어날 수 있다. 이를 미연에 방지하면서, 또 공유 및 재사용 할 수 있는 방법으로 가상환경을 사용할 수 있다.
대표적인 가상환경과 Package를 관리해주는 도구로는 virtualenv
와 conda
가 있다.전통적으로 virtualenv
와 pip
를 통해 가상환경을 많이 구했으나, pip
로 Package가 C
로 작성된 경우 Compiled 되지 않는 이슈가 있었다.
이에, Package를 자동으로 Compile 하여 설치해주는 conda
가 대세가 되었다. conda
사용예시는 아래와 같다.
$ conda create -n my_project python=3.9
... (installing) ...
$ conda activate my_project
(my_project) $ ...
(my_project) $ conda install package
(my_project) $ conda deactivate
$ ...
과제
- Morsecode
Morsecode
일부 상황에서 value에 해당하는 key를 찾아야 했다. key와 value의 위치를 뒤집고 싶어 코드를 고민하다 아래와 같이 구현했다.
>>> dict = {'key1' : 'val1', 'key2' : 'val2', 'key3' : 'val3'}
>>> reversed_dict = {v:k for k,v in dict.items()}
>>> reversed_dict
{'val1': 'key1', 'val2': 'key2', 'val3': 'key3'}
개인 학습
- Iterator & Generator
- Magic Method
- Decorator
Iterator와 Generator란?
Iterator 객체란, Iterator Protocol을 지키며, 원소를 필요할 때마다 하나씩 반환하는 객체이다. Generator는 Iterator를 쉽게 생성해주는 역할을 수행하는 객체이다. (Link)
Iterator Protocol & Iterable Object & Iterator
Iterator Protocol은 Iterable Object를 만드는 규칙을 의미한다. 이 규칙에 맞게 구현한다면, Iterable Object가 되는 것이며, 해당 객체의 멤버는 __iter__
를 갖게 된다.
>>> lst = [1,2,3]
>>> dir(lst)
[ ..., '__iter__', ... ]
Iterator는 Iterable 객체이면서 동시에 Lazy Loding을 하는 객체를 의미한다. Iterator는 Iterable Object와 다르게 생성 당시에 배열과 값을 만들어 놓지 않기 때문에 memory 사용에서 큰 차이를 보인다.
Iterator는 Iterable 객체에 Magic Method인 iter()
를 통해 생성할 수 있다. Iterator의 Member인 __next__
를 Magic Method로 호출하면 순회하며 요소를 반환 받을 수 있다.
>>> lst = [1,2,3]
>>> iterator_lst = iter(lst)
>>>
>>> import sys
>>> sys.getsizeof(iterator_lst)
48
>>> sys.getsizeof(lst)
80
>>>
>>> next(iterator_lst)
1
>>> next(iterator_lst)
2
>>> next(iterator_lst)
3
>>> next(iterator_lst)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Generator
Generator는 Iterator를 쉽게 만들어주는 역할을 수행한다. 두 가지의 방법으로 Iterator를 만들 수 있다. 첫 번째는 yield
키워드의 사용이고, 두 번째는 Generator Expression이다. 두 방법 모두 Iterator Protocol로 생성된 것을 확인할 수 있다.
>>> def make_generator() :
... for i in range(3) :
... yield i
...
>>> gen = make_generator()
>>> gen
<generator object make_generator at 0x0000014D9C43F7B0>
>>>
>>> generator_expression = (x for x in range(3))
>>> generator_expression
<generator object <genexpr> at 0x0000014D9C415C80>
>>>
>>> '__iter__' in dir(gen) and '__iter__' in dir(generator_expression)
True
yield
는 함수의 값을 return해주며, next
가 호출 될 때 까지 현재 상테에서 머물다가 호출 됐을 때 연산을 수행한다. 즉, next
가 호울 됐을 때 연산을 수행하고 return 한다.
>>> next(gen)
0
>>> next(gen)
1
>>> next(gen)
2
Generator Expression은 List Comprehension과 유사하나, 소괄호를 사용하는 차이점을 가진다. List Comprehension은 배열의 크기만큼 미리 할당하나, Generator Expression은 호출 될 때 연산을 하는 것을 확인할 수 있다.
>>> L = [ 1,2,3]
>>> def print_iter(iter):
... for element in iter:
... print(element)
...
>>> def lazy_return(num):
... print("sleep 1s")
... time.sleep(1)
... return num
...
>>> comprehension_list = [ lazy_return(i) for i in L ]
sleep 1s
sleep 1s
sleep 1s
>>> print_iter(comprehension_list)
1
2
3
>>>
>>> generator_exp = ( lazy_return(i) for i in L )
>>> print_iter(generator_exp)
sleep 1s
1
sleep 1s
2
sleep 1s
3
Magic Method란?
Magic Method란, 특별한 이름을 가진 Method들을 재정의 함으로써 인터프리터가 객체를 만들거나 표현하거나 연산을 하는데 도움을 줄 수 있는 Method를 의미한다. __
로 시작하고, 끝내는 형태로 구현된다. (Link)
Magic Method의 역할
Class의 Instance 생성에 관여할 수 있다. __new__
는 인스턴스 생성 시 가장 먼저 실행되는 메서드이며, __init__
은 초기화 메서드이다. 이와 같은 메서드들은 따로 선언하지 않더라도, 인터프리터가 Default로 구현한다.
>>> class NumBox:
... def __new__(cls, *args, **kwargs):
... if len(args) < 1: # 인자가 들어오지 않은 경우
... return None # None을 반환
... else: # 인자가 들어온 경우
... return super(NumBox, cls).__new__(cls) # object를 반환
...
... def __init__(self, num=None):
... self.num = num # 받은 인자 num을 인스턴스 변수로 지정
...
>>>
>>> no_args = NumBox()
>>> type(no_args)
<class 'NoneType'>
>>>
>>> input_arg = NumBox(111)
>>> type(input_arg)
<class '__main__.NumBox'>
>>> class nothing() :
... pass
...
>>> dir(nothing)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
객체를 표현하는데 도움을 줄 수 있다. __str__
과 __repr__
는 print
함수 등에 의해 호출 될 때 무엇을 출력할 지를 정해줄 수 있다.
>>> class StrBox:
... def __init__(self, string):
... self.string = string
...
... def __repr__(self):
... return "A('{}')".format(self.string)
...
>>> s = StrBox('this is test')
>>> print(s)
A('this is test')
객체의 연산을 정의해놓을 수 있다. 아래의 Magic Method는 구현하지 않더라도, 연산자에 의해 호출 됐을 때 아래와 같이 연산된다.
>>> class StrBox(str):
... def __new__(cls, string):
... return str.__new__(cls, string)
... def __lt__(self, other):
... return len(self) < len(other)
... def __le__(self, other):
... return len(self) <= len(other)
... def __gt__(self, other):
... return len(self) > len(other)
... def __ge__(self, other):
... return len(self) >= len(other)
... def __eq__(self, other):
... return len(self) == len(other)
... def __ne__(self, other):
... return len(self) != len(other)
이외에도 위에서 보았듯 __iter__
Method가 있으면, iter()
에 의해 데이터 형태가 변환되는 등의 기능을 미리 구현해놓을 수 있다.
Decorator란?
Decorator는, 함수를 인자로 받아서 새로운 함수를 만들어 반환하는 함수라 할 수 있다. 이를 통해 함수 실행 전 특정동작을 하게 하는 걸 간단하게 만들 수 있다. (Link)
Closure
Decorator 이전에, Clousure의 목적과 실행 순서에 대해 알아야 하겠다. Closure는 부모함수의 변수나 정보를 가두는 역할을 한다. 어떤 정보를 기반으로 연산을 수행하고 싶으나, 그 정보의 접근을 제한하여 노출이나 수정을 막고자 할 때 수행된다. 주로 Facoty 패턴을 구현할 때 사용되며, 설정값을 노출하지 않으면서 사용할 수 있도록 구현한다. Closure 조건은 다음과 같다.
- Nested 구조를 갖춰야 한다.
- Inner 함수가 부모 함수의 변수나 정보를 사용해야 한다.
- 부모 함수는 Inner 함수 자체를 return 해야 한다.
예제는 아래와 같으며, 부모함수 호출 시 실행 순서는 line1 -> line2 -> line4가 된다. 부모 함수로부터 return 받은 함수에 값을 전달하면, line3가 실행되며 최종적인 값을 return 받을 수 있다.
>>> def generate_power(base_number): #line1
... def nth_power(power): #line2
... return base_number ** power #line3
... return nth_power #line4
...
>>> calculate_power_of_two = generate_power(2)
>>> calculate_power_of_two(7)
128
Decorator 실행순서
Decorator의 실행순서를 이해하기 위해 예시를 보고 실행 순서를 확인한다.
- 모든 함수가 정의 된 다음, line8에 의해 부모 함수가 호출되며,
func
에는delivery_ok
함수가 저장된다.
delivery_check
변수는 정의된wrapper
함수를 저장한다.
- line9에 의해
delivery_check
변수에 저장된wrapper
함수를 실행된다.
- line3이 실행되므로,
datetime.now()
가 출력된다.
- line4가 실행되므로,
func
에 저장된delivery_ok
함수가 실행된다.
- line7이 실행되므로, "배송완료"가 출력된다.
>>> import datetime
>>>
>>> def decorator(func) : # line1
... def wrapper() : # line2
... print(datetime.datetime.now()) # line3
... return func() # line4
... return wrapper # line5
...
>>> def delivery_ok() : # line6
... print("배송완료") # line7
...
>>> delivery_check = decorator(delivery_ok) # line8
>>> delivery_check() # line9
2021-01-22 12:11:22.782815
배송완료
Decorator에서 전달인자 받기
위의 예시는 아무런 전달인자가 없는 상태였으므로, 전달인자를 받아 원하는 결과를 출력해보겠다. 실행 순서는 위와 같다.
delivery_chek
에wrapper
함수가 저장된다.
wrapper
함수에 key:value 형태로 값을 전달한다.
wrapper
함수의kwargs
에는{where='송파',company='한진'}
가 저장된다.
func
에 저장된delivery_ok
함수에 해당 값이 전달되며 아래 내용이 출력된다.
>>> import datetime
>>>
>>> def decorator(func):
... def wrapper(*args, **kwargs):
... print(datetime.datetime.now())
... return func(**kwargs)
... return wrapper
...
>>> def delivery_ok(**kwargs):
... print("배송완료")
... if 'where' in kwargs:
... print(f"배송지는 {kwargs['where']} 입니다.")
...
>>> delivery_check = decorator(delivery_ok)
>>> delivery_check(where='송파',company='한진')
2021-01-22 12:28:02.123222
배송완료
배송지는 송파 입니다.
Use '@'
위와 같이 사용할수도 있으나, 매번 부모 함수를 호출해야하는 번거로움이 있다. 이를 해결하는 방법으로는 본 함수(delivery_ok
)위에 @
기호로 decorator 사용을 정의할 수 있다. 실행 순서는 동일하나, dlivery_check
와 같은 변수를 사용하지 않아도 된다는 장점이 있다.
>>> import datetime
>>>
>>> def decorator(func):
... def wrapper(*args, **kwargs):
... print(datetime.datetime.now())
... return func(**kwargs)
... return wrapper
...
>>> @decorator
... def delivery_ok(**kwargs):
... print("배송완료")
... if 'where' in kwargs:
... print(f"배송지는 {kwargs['where']} 입니다.")
...
>>> delivery_ok(where='송파',company='한진')
2021-01-22 12:37:14.023198
배송완료
배송지는 송파 입니다.
'네이버 부스트캠프 AI Tech' 카테고리의 다른 글
[U] Day 06 - Numpy / 벡터 / 행렬 (0) | 2021.03.25 |
---|---|
[U] Day 05 - 파이썬으로 데이터 다루기 (0) | 2021.03.25 |
[U] Day 03 - 파이썬 기초 문법 II (0) | 2021.03.25 |
[U] Day 02 - 파이썬 기초 문법 (0) | 2021.03.25 |
[U] Day 01 - 파이썬 / AI 개발환경 준비하기 (0) | 2021.03.25 |
Uploaded by Notion2Tistory v1.1.0