Python Object Oriented Programming

💡
객체 지향 프로그래밍에 대해 알아본다.

객체 지향 프로그래밍이란?

Python을 통해 개발하는데는 여러 방법이 있다. 그 중에서 만들어 놓은 코드를 재사용 하고 싶은 경우 활용 할 수 있는 프로그래밍 방법이다. 내가 타인의 코드를 활용하기 위해서, 혹은 타인이 나의 코드를 활용하기 쉽도록 구조화 한 것으로, Python 외의 언어에서도 지원하는 프로그래밍 방법이다.

Class and Object 개요

Class는 객체의 설계도에 해당한다. 이 설계도에서 생성되는 구현체를 Instance라 부른다.Object는 현실 세계에 존재하는 모든 을 의미하며, 이는 속성과 행동을 가진다. 이를 프로그래밍 상에서 표현하면, 객체가 된다. 속성은 Field 행동은 Method로 표현된다. 특징은 아래와 같다.

  • Class의 Name Convention은 CamelCase를 따른다.
  • Class는 함수와 비슷하게 선언하나, def 대신 classparameter 대신 상속 받을 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
  • Notebook
    • Field
      • title
      • page_number
      • notes
    • Method
      • add_note
      • remove_note
      • get_number_of_pages

이를 구현하면 다음과 같을 것이다.

# 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
 배송완료
 배송지는 송파 입니다.

+ Recent posts