Python OOP

냥린이·2021년 12월 8일
0

객체지향

목록 보기
2/2

Python에서 Class와 Object를 다루는 방법을 알아보자

OOP란?

OOP란 예를 들어 수강신청 프로그램 작성 시 수강신청 관련 주체(교수, 학생, 관리자)들의 행동(수강신청, 과목 입력)과 데이터(수강과목, 강의 과목)를 중심으로 작성 후 연결하는 것이다.

객체는 실생활에서 일종의 물건으로 속성attribute행동action을 가진다. OOP는 이러한 객체 개념을 프로그램으로 표현하며 속성은 variable, 행동은 method로 표현된다.

설계도에 해당하는 Class와 실제 구현체인 Instance로 나뉜다. 붕어빵 틀과 붕어빵의 관계라고 이해하면 편하다.

Objects in Python

class SoccerPlayer(object): 
	def__init__(self, name, position, back_number):
    	self.name = name
        self.position= position
        self.back_number= back_number

    def change_back_number(self, new_number):
        print("선수의등번호를변경합니다: From%dto%d" % (self.back_number, new_number))
        self.back_number= new_number

더 간단하게 살펴보면,

class Soccer Player(object):
	pass
    
sonny = SoccerPlyaer()

여기서 Sonny는 객체가 되고 SoccerPlayer는 클래스가 된다.
같은 class에서 생성된 객체는 서로 다른 정보를 지닌 객체이며(is로 확인 가능) 메모리 주소도 다르다.

이때 class에서 속성을 추가해줬다면 객체를 생성할 때 () 안에 인자를 전달해주어야 한다.

class SoccerPlayer(object): 
	def__init__(self, name : str, position : str, back_number : int):
    	self.name = name
        self.position= position
        self.back_number= back_number
        
sonny = SoccerPlayer("son", "FW", 7)    

하나씩 자세히 살펴보자

Class 선언

class SoccerPlayer(object): 

Class는 예약어, SoccerPlayer는 class 이름, object는 상속받는 객체명이다.
Python3에서는 자동 상속이 되므로 (object)는 생략 가능하다.

Python Naming Rule
Function/Variable: 띄어쓰기 부분에 _ 를 추가한다. (snake_case)
Class: 띄어쓰기를 하지 않고 단어 첫글자를 대문자로 써서 이어붙인다. (CamelCase)
파이썬 네이밍룰 더 알아보기

Attribute 추가

def__init__(self, name, position, back_number):

__init__ 이라는 객체 초기화 예약 함수의 파라미터에 self와 여러 속성 정보를 써주면 된다.
self.attribute = attribute로 초기 정보를 선언해서 객체의 속성을 초기화 해준다.
이때 파라미터로 받은 attribute와 self.attribute는 같은 것이 아니다.
다만 파라미터로 받은 attribute를 self.attribute에 할당해주는 것이다.

named tuple과 유사한 면이 있다.

Python Magic Method

Python에서 '__'의 의미
특수한 예약 함수나 변수 그리고 함수명 자동 변경(맨글링)으로 사용
ex. __main__, __add__, __str__, __eq__

더블 언더스코어 더 알아보기

class SoccerPlayer(object): 
	def__init__(self, name : str, position : str, back_number : int):
    	self.name = name
        self.position= position
        self.back_number= back_number
        
sonny = SoccerPlayer("son", "FW", 7)  

print(sonny)

sonny 객체를 출력하면 메모리 주소가 나온다. (<main.SoccerPlayer object at ~~>)

classSoccerPlayer(object):
	def__str__(self):
    	return"Hello, Mynameis%s. Iplayin%sincenter" % \
        (self.name, self.position)
        
jinhyun= SoccerPlayer("Jinhyun", "MF", 10)
print(jinhyun)

여기서 Jinhyun 객체를 출력하면 'Hello, My name is Jinhyun. My back number is 10'이 나온다.

classSoccerPlayer(object):
  def __add__(self, other):
      return self.name + other.name
      
abc = SoccerPlayer("son", "FW", 7)
park = SoccerPlayer("park", "WF", 13)

abc + park

위처럼 여러 객체의 연산도 가능하다. 위에서 abc+park의 결과는 'sonpark'이다.

Methon 구현

class SoccerPlayer(object):
	def change_back_number(self, new_number):
    	print("선수의등번호를변경합니다: From%dto%d" % \
        (self.back_number, new_number))
    self.back_number= new_number

method(action) 추가는 일반 함수와 동일하나 반드시 self를 추가해야 class 함수로 인정된다.

Object(Intstance) 사용하기

jinhyun= SoccerPlayer("Jinhyun", "MF", 10)
객체명 = Class명(init함수 Interface, 초기값)

Self란?
생성된 Instance(객체) 자신을 의미

jihyun.back_number = 20

위처럼 객체의 속성에 직접 접근해서 속성을 바꾸는 방법도 있지만 이는 권장되지 않는다. 이유는 후술

class SoccerPlayer(object): 
	def__init__(self, name, position, back_number):
    	self.name = name
        self.position= position
        self.back_number= back_number

    def change_back_number(self, new_number):
        print("선수의등번호를변경합니다: From%dto%d" % (self.back_number, new_number))
        self.back_number= new_number
 
jinjyun.back_number = 20

위처럼 속성을 변경하는 함수를 만들고 그를 호출해서 속성을 변경하는 것이 권장된다. (이때 self를 사용한다).

Example of Implementation

요구사항

Note를정리하는프로그램
-사용자는Note에뭔가를적을수있다.
-Note에는Content가있고, 내용을제거할수있다.
-두개의노트북을합쳐하나로만들수있다.
-Note는Notebook에삽입된다.
-Notebook은Note가삽일될때페이지를생성하며,최고300페이지까지저장가능하다
-300 페이지가넘으면더이상노트를삽입하지못한다.

Class Scheme

SchemeNotebookNote
methodadd_notewrite_content
remove_noteremove_all
number_of_pages
variabletitlecontent
page_number
notes

Implementation


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 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.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())

Use

from 파일명 import Note
from 파일명 import Notebook

my_notebook = Notebook("팀랩 강의노트")
new_note1 = Note("아 수업하기 싫다")
new_note2 = Note("아 수업하기 싫다")

my_notebook.add_note(new_note1, 100)
my_notebook.add_note(new_note2)

print(my_notebook.notes[100]) // 해당 페이지가 비어있다면 에러 출력

my_notebook.get_number_of_pages()

my_notebook.notes[2] = Note("안녕") // 직접 추가해 줄 수도 있음

Class를 다른 곳에서 불러와 쓰고 싶다면 위와 같이 패키지처럼 호출하면 된다.

OOP 특성

객체 지향은 실제 세상을 모델링 하는 것

  1. Inheritance 상속
  2. Polymorphism 다형성
  3. Visibility 가시성 (Hidden Class)

상속

부모 클래스로부터 속성과 메소드가 물려받은 자식 클래스를 생성하는 것

class Person(object):
	def __init__(self, name, age):
    	self.name = name
        self.age= age
        
    def __str__(self):
    	return "저의 이름은 {0}이고 나이는 {1} 입니다.".format(
        self.name, self.age
        )
        
class Korean(Person):
	pass

first_korean= Korean("Sungchul", 35)
print(first_korean)

Korean 클래스는 Person을 상속 받고 Person의 속성 그대로 사용 가능하다.

class Person(object): # 부모클래스Person 선언
	def __init__(self, name, age, gender):
    	self.name = name
        self.age = age
        self.gender = gender
        
    def about_me(self): # Method 선언
    	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): # 새로운메서드추가
    	print("열심히 일을 합니다.")
        
    def about_me(self): # 부모클래스함수재정의
    	super().about_me() # 부모클래스함수사용
        print("제 급여는", self.salary, "원이구요, 제 입사일은", self.hire_date, " 입니다.")
        
    myPerson = Person("John", 34, "Male")
    myPerson.about_me()
    # 출력: 저의 이름은 John이구요, 제 나이는 34 살 입니다.
    MyEmployee = Employee("Daeho", 34, "Male", 300000, "2012/03/01")
    MyEmployee.about_me()
    # 출력: 저의 이름은 Daeho이구요, 제 나이는 34 살 입니다.
    # 출력: 제 급여는 300000 원 이구요, 제 입사일은 2012/03/01 입니다.

super().__init__으로 부모 클래스의 속성을 그대로 가져올 수 있고, 이후 self.attribute로 새로운 속성을 추가할 수 있다. super().about_me()처럼 메소드를 상속받는 것도 가능하다.

다형성

OOP의 매우 중요한 개념이다.
본래 다형성이란 같은 기능인데 세부적인 구현이 다를 경우인데, 파이썬의 경우
다이나믹 타이핑 특성으로 인해 같은 부모 클래스의 상속에서 주로 발생한다.
쉽게 말하면 같은 이름 메소드의 내부 로직을 다르게 작성하는 것이다.

class Animal:
	def__init__(self, name): # Constructor of the class
    	self.name = name
    def talk(self): # Abstract method, defined by convention only
    		raise NotImplementedError("Subclass must implement abstract method")
            
class Cat(Animal):
	def talk(self):
    	return'Meow!'
class Dog(Animal):
	def talk(self):
    	return 'Woof! Woof!'
        
animals= [Cat('Missy'),
          Cat('Mr. Mistoffelees'),
          Dog('Lassie')]

for animal in animals:
	print(animal.name + ': ' + animal.talk())

Animal 클래스에 talk 함수를 만든 후, cat와 dog에서 같은 이름의 함수를 조금씩 다르게 구현하는 것이 가능하다.

가시성

누구나 객체 안의 모든 변수를 볼 필요는 없으므로 객체의 정보를 볼 수 잇는 레벨을 조절할 필요가 있다.

  1. 객체를 사용하는 사용자가 임의로 정보를 수정하는 것 방지
  2. 필요 없는 정보에는 접근할 필요가 없음
  3. 제품으로 판매한다면 소스의 보호가 필요 (파이썬에서는 그다지 중요하지 않은 이슈)

Encapsulation (캡슐화, 정보 은닉)
Class를 설계할 때 클래스 간 간섭, 정보공유의 최소화하는 것이다. 캡슐을 던지듯 인터페이스만 알아서 써야한다.

예제1

class Product(object): # 비어있는 dummy class 생성
	pass
    
class Inventory(object):
	def__init__(self):
    	self.items= []
        self.test = "abc"
    def add_new_item(self, product):
    	if type(product) == 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= Inventory()
my_inventory.add_new_item(Product())
my_inventory.add_new_item(Product())
my_inventory
# 출력: new item added
# 출력: new item added
my_inventory.items.append("abc")
my_inventory.items
# 출력: 'abc', 'abc'

이 코드처럼 작성하면 Inventory 객체 생성 후 my_inventory.items.append('abc')처럼 product 외의 다른 형태로도 item을 넣을 수 있으며 my_inventory.items를 했을 때 items에 접근이 가능하고

예제2

  1. Product를 Inventory 객체에 추가
  2. Inventory에는 오직 Product 객체만 들어감
  3. Inventory에 Product가 몇 개인지 확인 필요
  4. Inventory에 Product items는 직접 접근 불가 (self.__items 선언 필요)
class Product(object): # 비어있는 dummy class 생성
	pass
    
class Inventory(object):
	def__init__(self):
    	self.__items= [] # Private 변수로 선언하여 타객체 접근 방지
    def add_new_item(self, product):
    	if type(product) == Product:
          self.__items.append(product)
          print("new item added")
        else:raise ValueError("InvalidItem")
    def get_number_of_items(self):
    	return len(self.__items)
        
my_inventory= Inventory()
my_inventory.add_new_item(Product())
my_inventory.add_new_item(Product())
my_inventory.__items # 에러 출력

my_inventory.__items를 했을 때 __items에 접근이 불가능하다. 맹글링을 통해 일종의 프라이빗처럼 선언했기 때문이다.

예제3

  1. Product 객체를 Inventory 객체에 추가
  2. Inventory에는 오직 Product 객체만 들어감
  3. Inventory에 Product가 몇 개인지 확인이 필요
  4. Inventory에 Product items 접근허용
class Inventory(object):
	def__init__(self):
    	self.__items= []
        
    @property # property decorator는 숨겨진 변수를 반환하게 해준다
    def items(self):
    	return self.__items # 그대로 반환하지 않고 copy 해주는 게 일반적
        
my_inventory= Inventory()
my_inventory.add_new_item(Product())
my_inventory.add_new_item(Product())
print(my_inventory.get_number_of_items())

items= my_inventory.items # property decorator로 함수를 변수처럼 호출
items.append(Product())
print(my_inventory.get_number_of_items())

my_inventory= Inventory()
my_inventory.add_new_item(Product())
my_inventory.add_new_item(Product())
my_inventory

my_inventory.__items # 에러 출력
my_inventory.items # 접근 가능

self.__items에 대해 외부에서는 여전히 접근이 안 되지만, property decorator를 이용해 내부에서 외부로 반환하는 함수를 만들어 둘 수 있다.

Decorator

위에서 잠깐 언급한 decorator를 좀 더 자세히 살펴보자
아래의 코드가 어떻게 동작하는지 잘 설명할 수 있다면 이 다음부터는 보지 않아도 무방하다.

class Student:
  def __init__(self, name, marks):
    self.name = name
    self.marks = marks
    # self.gotmarks = self.name + ' obtained ' + self.marks + ' marks'
    
  @property
  def gotmarks(self):
  	return self.name + ' obtained ' + self.marks + ' marks'

Decorator 더 알아보기

First-class objects inner function decorator

일급 함수(일급 객체)는 변수나 데이터 구조에 할당이 가능한 객체이다. 파라미터로 전달이 가능하며 리턴 값으로 사용된다. 따라서 파이썬의 모든 함수는 일급 함수이다.

함수를 변수로 사용
def square(x):
	return x * x
    
f = square
f(5)
함수를 파라미터로 사용
def square(x):
	return x * x
    
def cube(x):
	return x * x * x
    
def formula(method, argument_list):
	return [method(value) for value in argument_list]

argument_list는 [1, 2, 3, 4, 5] 배열이고,
method에는 square와 cube 같은 함수가 들어갈 수 있다.

Inner function

함수 내에 또 다른 함수가 존재하는 구조로 아주 많이 쓰인다.

def print_msg(msg):
	def printer():
    	print(msg)
    printer()

print_msg("Hello, Python")

closure는 inner function을 return 값으로 반환하는 것이다.

def print_msg(msg):
	def printer():
    	print(msg)
    return printer

another = print_msg("Hello, Python")
another()

클로저는 파이썬 뿐만 아니라 자바스크립트에서도 아주 많이 쓰인다.
또다른 예시를 살펴보자

def tag_func(tag, text):
  text = text
  tag = tag
  
  definner_func():
    return'<{0}>{1}<{0}>'.format(tag, text)
    
  returninner_func

h1_func = tag_func('title', "This is Python Class")
p_func= tag_func('p', "Data Academy")

다형성과 유사하게 같은 목적을 지니지만 구현이 다른 경우 클로저를 사용하면 유용한 경우가 많다.

Decorator function

decorator 는 복잡한 클로저 함수를 간단하게 만들어준다.

def star(func):
  def inner(*args, **kwargs):
    print("*"* 30)
    func(*args, **kwargs)
    print("*"* 30)
  return inner

@star
def printer(msg):
	print(msg)
printer("Hello")

printer 함수는 star 내부 함수로 func라는 파라미터로 전달된다.
맨 처음 inner 함수가 실행되면서 print 문이 수행되고
이후 star의 func 파라미터로 들어온 printer 함수가 실행되며
마지막 print 문이 수행되는 구조이다.

위 코드의 실행 결과는 아래와 같다.

*************************
Hello
*************************

메세지를 지정할 수도 있다. 위의 코드를 조금 수정해보면,

def star(func):
  def inner(*args, **kwargs):
    print(args[1] * 30)
    func(*args, **kwargs)
    print(arg[1] * 30)
  return inner

@star
def printer(msg, mark):
	print(msg)
printer("Hello" ,"T")

위 코드의 실행 결과는 아래와 같다.

TTTTTTTTTTTTTTTTTTTTTTTTTTTTTT
Hello
TTTTTTTTTTTTTTTTTTTTTTTTTTTTTT

이렇게 작성했을 때의 장점은 무엇일까?
decorator를 만들어 대상 함수를 wrapping 하면, 해당 함수에서 앞뒤로 꾸며질 구문을 정의해서 코드 재사용성을 높일 수 있다. 즉, 반복적인 작업을 decorator로 선언해두는 것이다. 여러 함수에서 쉽게 가져다 쓸 수 있고 반복 작업 수정이 필요할 때 decorator만 바꾸면 되므로 편하다.

def star(func):
  def inner(*args, **kwargs):
  	print("*" * 30)
    func(*args, **kwargs)
    print("*" * 30)
return inner

def percent(func):
  def inner(*args, **kwargs):
    print("T" * 30)
    func(*args, **kwargs)
    print("T" * 30)
  return inner

@star
@percent
def printer(msg):
	print(msg)
printer("Hello")

위 코드는 percent 함수에 printer가 들어가고 그것을 star가 감싸고 있는 다단계 형태이다. 실행 결과는 아래와 같다.

*************************
TTTTTTTTTTTTTTTTTTTTTTTTTTTTTT
Hello
TTTTTTTTTTTTTTTTTTTTTTTTTTTTTT
*************************

좀 더 복잡한 예제는 아래와 같다. decorator에 2라는 값을 넣어줬다. generate_power 함수의 exponent 파라미터에는 @generator_power(2)의 2가, wrapper의 f 파라미터에는 raise_two()가 들어간다. n은 inner의 *args에 전달되고 f(*args)는 raise_two(n)를 실행하는 것과 같다.

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

print(raise_two(7))

실행 결과는 2^49인 562949953421312이다.

profile
홀로서기 기록장

0개의 댓글