객체지향 프로그래밍(Object-Oriented Programming, OOP)은 절차지향프로그래밍처럼 프로그램 전체를 하나로 묶어서 구현하는 방식이 아니라 프로그램을 구성하는 객체들을 중심으로 구현하는 방식.
프로그램이 어떤 작업을 수행하기 위해서는 데이터, 데이터를 처리하는 행위의 2가지 요소가 필요함.
일반적으로 데이터는 변수에 넣어서 사용하고, 데이터를 처리하는 행위는 함수로 구성하여 실행시킨다.
변수나 함수는 멤버(member) 또는 속성(attribute)라고 함.
관련 속성과 행동을 개별 객체로 묶어 프로그램을 구성하는 방법.
클래스라는 자료형을 이용해서 객체를 생성하고 인스턴스를 활용해서 다른 이름의 객체로 저장하는 시스템을 가지고 있음. OOP의 기본전제는 기능(클래스, 함수, 변수)을 재사용이 가능하도록 설계 및 프로그래밍했는지.
장점
단점
속성과 행동으로 이루어진것. 데이터와 데이터를 처리하는 함수를 함께 묶은 것을 의미. 클래스에 정의된 속성과 기능을 실제로 가지고 있는 실체(instance). 현실에 존재하던 가상에 존재하던 모두 객체가 될 수 있음.
클래스는 객체를 생성하기 위한 설계도와 같은 역할을 함. 클래스는 특정유형의 객체들이 공통적으로 가지고 있어야할 속성과 기능(메소드)을 정의함.
파이썬은 순수 객체지향 언어
# 모두 클래스
print(type(1))
print(type("a"))
print(type([]))
print(type({}))
print(type(()))
<class 'int'>
<class 'str'>
<class 'list'>
<class 'dict'>
<class 'tuple'>
데이터와 데이터를 처리하는 메소드를 함께 묶어서 하나의 객체로 캡슐화. 이를 통해 외부에서 객체의 내부상태에 직접 접근하지 않고도 객체의 메소드를 통해 상호작용할 수 있음.
데이터와 데이터를 처리하는 메소드를 외부에서 직접적으로 접근하지 못하도록 제한하는것.
객체 내부의 데이터가 보호되고, 외부에서는 메소드를 통해서만 객체에 접근할 수 있음.
내부 속성(변수)과 함수를 하나로 묶어서 클래스로 선언하여 객체의 데이터와 내부 구현 세부사항을 캡슐화.
캡슐화하지 않으면 속성, 메소드에 직접 접근하게됨. 속성, 메소드에 직접 접근하는 코드가 많을수록 유지보수 하기 어려워짐.
하지만 파이썬은 언어 차원에서는 캡슐화를 지원하지 않는다. 참고로 다른 객체지향언어인 자바는 private라는 키워드를 변수 앞에 붙이면 외부로부터 접근이 차단됨.
파이썬은 의도된 사용법을 지시하는 프로그래밍 관례를 따른다.
__를 사용한 캡슐화
언더스코어(__)로 시작하는 변수는 private member로 간주되어, 클래스 외부에서 직접 접근할 수 없음.
class Citizen:
def __init__(self, name, resident_id):
self.name = name
self.__resident_id = resident_id
# Citizen 인스턴스 생성
citizen_1 = Citizen("poke", "12345-678910")
print(citizen_1.name)
print(citizen_1.__resident)
poke
AttributeError: 'Citizen' object has no attribute '__resident'
하지만 dir()함수를 사용하여 객체가 가지고 있는 모든 속성과 메소드 이름을 확인해보자.
print(dir(citizen_1))
['_Citizen__resident_id', '__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__', 'name']
출력된 결과를 보면 private으로 설정한 __resident_id가 변형된 형태인 _Citizen__resident_id로 출력된 것을 확인할 수 있다. 파이썬은 private을 지원하지 않기 때문에 네임 맹글링(name mangling) 기법을 사용한다. 주로 클래스 내부에서 private 속성을 다룰 때 사용되는 기법으로, 클래스 정의에서 변수명이 언더스코어로 시작하면 파이썬 인터프리터는 자동으로 그 변수의 이름을 변경(클래스명변수명)하여 클래스 외부에서 변경된 변수명을 명시적으로 사용하지 않는 한 직접적으로 접근할 수 없다(하위 클래스에서도 직접적인 접근 어려움).
# 변경된 변수명을 명시하면 접근 가능
print(citizen_1._Citizen__resident_id)
12345-678910
데코레이터를 사용한 캡슐화
@property, @함수명.setter를 사용하여 데이터에 대한 접근과 수정을 엄격하게 통제함(데이터의 무결성 보장).
@property는 클래스의 필드를 직접적으로 외부에 노출하지 않고 메소드를 통해서만 간접적으로 접근하게함. @함수명.setter는 객체의 속성을 설정할때 사용되는 설정자(setter)로, 외부에서 값을 설정하려고 할때 호출되어, 전달된 값의 유효성을 체크하고 조건을 만족할때만 내부 속성값을 변경하도록 함.
class Person:
def __init__(self, name, age):
self.name = name
self._age = age # 초기 나이 설정
@property
def age(self):
return self._age # 나이를 반환하는 메서드
@age.setter
def age(self, value):
if value < 0:
raise ValueError("Age cannot be negative")
self._age = value # 유효성 검사 후 나이를 설정
# Person 인스턴스 생성
person_1 = Person("poke", 20)
# age 속성처럼 접근
print(person_1.age)
# age 속성에 값을 할당
person_1.age = 30
print(person_1.age)
person_1.age = -30
print(person_1.age)
20
30
ValueError: Age cannot be negative
이 데코레이터를 통해 실제 _age를 사용할때 age라고 쓰면됨. print(person_1.age)이 실행되면 데코레이터 덕에 @property가 달려있는 age메소드(getter)가 실행됨. person_1.ate = 30이 실행되면 데코레이터 덕에 @age.setter가 자동으로 실행됨.
_변수명의 변수가 있을 경우 변수명으로 함수를 만들고 그 위에 @property, @변수명.setter를 만들어주면됨.

_를 사용한 private은 약한 private을 나타낸다. 외부에서도 접근가능하지만, 클래스 내부 또는 하위 클래스에서만 접근하도록 의도된 변수(내부적으로 사용될 것임을 나타냄).
person_1._age = 40
print(person_1._age)
40
하나의 클래스가 다른 클래스의 속성과 메소드를 물려받아 재사용할 수 있음.
기존클래스의 기능을 그대로 사용할 수도 있고, 필요에 따라 일부 기능을 변경(재정의)하거나 새로운 기능을 추가할 수 있음.
클래스간의 계층 구조를 형성할 수 있음.
class Parent: # 부모클래스/기본클래스(base class)/상위클래스(superclass)
...
class Child(Parent): # 자식클래스/파생클래스(derived class)/하위클래스(subclass)
... # 부모클래스를 매개변수로 보냄.
# 부모클래스
class Person:
def __init__(self, fname, lname):
self.firstname = fname
self.lastname = lname
def printname(self):
print(self.firstname, self.lastname)
# 자식클래스
class Student(Person):
pass # 부모클래스의 모든 기능을 그대로 유지.
Student 클래스는 Person 클래스의 생성자를 상속받음. Student 클래스의 인스턴스를 만들때, Person클래스이 __init__생성자가 호출되어 해당 객체의 fname과 lname 속성이 초기화됨.
아래 코드는 상속을 사용하여 기존 클래스의 코드를 재사용하고 일부 기능을 추가한 예제이다.
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
def description(self):
return f"{self.title} by {self.author}"
class Ebook(Book): # Book 클래스를 상속받음.
def __init__(self, title, author, file_format): # Student클래스의 생성자.
Book.__init__(self, title, author) # 부모클래스의 생성자를 직접 호출
self.file_format = file_format
def description(self):
return f"{self.title} by {self.author}, Format: {self.file_format}"
# Book 객체 생성
book = Book("1984", "George Orwell")
print(book.description())
# Ebook 객체 생성
ebook = Ebook("1984", "George Orwell", "PDF")
print(ebook.description())
아래 코드는 에러
class A:
def __init__(self):
self.title = "A"
class B(A):
def __init__(self):
self.subtitle = "B"
subtitle = B()
print(subtitle.subtitle)
print(subtitle.title)
B
AttributeError: 'B' object has no attribute 'title'
A클래스의 init 메소드가 호출되지 않았기 때문에 subtitle.title이 실행되지 않음.
mro(Method Resolution Order)
mro() 함수는 메소드 해석 순서를 설명하는데 사용되는 함수. 어떤 클래스의 상속관계를 보여줌.
print(Book.mro())
print(EBook.mro())
[<class '__main__.Book'>, <class 'object'>]
[<class '__main__.Ebook'>, <class '__main__.Book'>, <class 'object'>]
Ebook -> Book -> object로 가는 상속관계를 보여줌.(object는 모든 클래스의 조상)
isinstance
주어진 인스턴스가 특정 클래스에 포함된 인스턴스인지 확인. 반환값은 Boolean.
# isinstance(인스턴스의 유형을 검사할 객체, 클래스)
isinstance(object, classinfo)
print(isinstance(ebook, Book))
print(isinstance(ebook, Ebook))
print(isinstance(ebook, object))
print(isinstance(book, Ebook))
True
True
True
False
issubclass
한 클래스가 다른 클래스의 서브클래스인지, 즉 상속관계에 있는지 검사하는데 사용. 반환값은 Boolean.
# issubclass(서브클래스 관계를 확인할 클래스, 클래스)
issubclass(class, classinfo)
print(issubclass(Ebook, Book))
print(issubclass(Book, Ebook))
True
False
overriding 오버라이딩
오버라이딩은 자식클래스에서 부모클래스의 메소들르 재정의하는 행위. 자식클래스는 상속받은 부모클래스의 이름, 매개변수는 부모클래스에서 정의된것과 동일하게 유지하지만, 메소드 내부 로직을 변경할 수 있음.
부모의 변수를 오바라이딩하려면 똑같은 변수명으로 오버라이드 하면된다.
super().__init__로 상속을 명시할 수 있음.(이렇게 안해도 자동으로 상속됨)
class Person:
def speak(self):
return "Hello, I am a person."
class Student(Person):
def speak(self):
return "Hello, I am a student." # 기능을 변경하여 다른 출력을 제공
x = Student()
x.speak()
'Hello, I am a student.' # 부모클래스의 speak()은 무시되고 자식클래스의 speak()메소드가 수행됨.
부모와 자식이 같은 이름의 메소드를 가지고 있다면 mro에서 제일 빠른 순번의 것이 먼저 호출됨.
즉, 자식에서 오버라이딩 한 메소드가 호출된다.
이와 비슷한 이름으로 오버로딩(Overloading)이라는 것이 있는데 이는 전혀 다른 개념. 오버로딩은 같은 메서드가 인수의 자료형이나 개수를 다르게 받을 수 있는 것. C++, Java 등에서는 지원하지만, 파이썬에서는 오버로딩을 지원하지 않으므로 프로그래머가 내부적으로 알아서 처리해야 한다.
super()
자식클래스의 생성자에서 부모클래스의 메소드나 속성을 명시적으로 부모클래스의 이름을 사용하지 않고 호출할때 사용됨. 기본클래스가 여러번 초기화되는 것을 방지함. 만약 상속구조가 변경되어도 super()부분은 수정할 필요가 없음.
class Person:
def speak(self):
return "Hello, I am a person."
class Student(Person):
def speak(self):
parent_msg = super().speak() # super()를 이용해 부모 클래스의 메서드 호출
return f"{parent_msg} Hello, I am a student." # 기능을 변경하여 다른 출력을 제공
x = Student()
x.speak()
'Hello, I am a person. Hello, I am a student.'
다중상속
여러 클래스로부터 상속하도록 클래스에 정의할 수 있음. 파이썬은 다중상속이 가능함. 자바는 상속 딱 하나만 가능함.
class Mother:
...
class Father:
...
class Child(Mother, Father): # Child클래스는 Mother, Father클래스의 특징을 모두 상속받음.
...
파이썬은 다중상속환경에서 super()을 사용하지 않고 부모클래스를 직접 호출하면, MRO가 제대로 관리되지 않아 같은 부모 클래스가 여러번 호출될 수 있음.
class A:
def __init__(self):
print("A")
class B(A):
def __init__(self):
A.__init__(self) # 직접 호출
print("B")
class C(A):
def __init__(self):
A.__init__(self) # 직접 호출
print("C")
class D(B, C):
def __init__(self):
B.__init__(self)
C.__init__(self)
print("D")
D.mro()
[__main__.D, __main__.B, __main__.C, __main__.A, object]
super()를 사용하면 파이썬의 MRO 알고리즘이 자동으로 다음에 초기화할 클래스를 결정하므로, 모든 클래스가 적절한 순서로 한번씩만 초기화됨.
class A:
def __init__(self):
print("A")
class B(A):
def __init__(self):
super().__init__()
print("B")
class C(A):
def __init__(self):
super().__init__()
print("C")
class D(B, C):
def __init__(self):
super().__init__()
print("D")
D.mro()
[__main__.D, __main__.B, __main__.C, __main__.A, object]
다형성은 "many forms"를 의미하며, 여러 형태를 가질 수 있는 능력을 의미. 많은 객체나 클래스에서 실행할 수 있는 동일한 이름의 방법/기능/연산자.
예를들어 다양한 객체에서 사용할 수 있는 len()함수가 있다.
# String
my_str = "Hello World!"
print(len(my_str))
# Tuple
my_tuple = ("apple", "banana", "cherry")
print(len(my_tuple))
# Dictionary
my_dict = {
"brand": "Ford",
"model": "Mustang",
"year": 1964
}
print(len(my_dict))
12
3
3
여러 클래스가 동일한 이름의 메소드를 가질 수 있지만, 각 클래스마다 해당 메소드의 구현이 다를 수 있음. 이를 통해 동일한 메소드 호출로 다양한 객체들의 행동을 다르게 처리할 수 있음.
class Car:
def __init__(self, brand, model):
self.brand = brand
self.model = model
def move(self):
print("Drive!")
class Boat:
def __init__(self, brand, model):
self.brand = brand
self.model = model
def move(self):
print("Sail!")
class Plane:
def __init__(self, brand, model):
self.brand = brand
self.model = model
def move(self):
print("Fly!")
car1 = Car("Ford", "Mustang") # Create a Car class
boat1 = Boat("Ibiza", "Touring 20") # Create a Boat class
plane1 = Plane("Boeing", "747") # Create a Plane class
for x in (car1, boat1, plane1): # 다형성으로 세 클래스의 동일한 메소드를 실행할 수 있음.
x.move()
Drive!
Sail!
Fly!
서브클래스에서 부모클래스의 메소드를 재정의할 수 있음. 다양한 클래스에서 같은 기능을 다른 방식으로 실행할 수 있음. 상속과 유사하다고 느껴질 수 있지만, 상속은 상위클래스의 기능(함수, 변수)을 재사용함.
# 메소드 오버라이딩
class Animal:
def speak(self):
return "Some sound"
class Dog(Animal):
def speak(self):
return "Woof"
class Cat(Animal):
def speak(self):
return "Meow"
# 다형성을 이용
animals = [Dog(), Cat(), Animal()]
for animal in animals:
print(animal.speak()) # 각 객체의 speak 메서드 호출
특정 코드를 사용할 때 필수적인 정보를 제외한 세부사항을 가리는것. object의 기능에 따라 추상클래스(상위클래스)를 상속받아 개별적으로 클래스(하위클래스)를 생성한다. 기본적으로 추상메소드를 선언하며 실제 실행되는 기능은 보여지지 않는다. 실제 실행되는 기능은 선언된 추상클래스를 상속받은 다른 클래스의 메소드에서 확인할 수 있다.
by Robert C.Martin
Single Responsibility Principle (단일 책임 원칙)
하나의 클래스는 하나의 책임을 져야한다는 원칙. 책임이란 변화를 유발하는 한가지 이유. 하나의 클래스를 수정할 이유는 하나뿐이며, 다른 이유로 클래스를 수정해야한다면 추상화가 잘못되어 하나의 클래스에 너무 많은 책임이 있다는 걸 뜻함.
Open/Closed Principle (개방-폐쇄 원칙)
모듈이 한 측면에서는 개방되어 있으면서도 다른 측면에서는 폐쇄되어야한다는 원칙.
확장 가능하여 새로운 기느을 추가하기 좋게 개방되어 있어야하며, 새로 추가한 기능 때문에 기존 코드가 수정되지 않도록 폐쇄적이여야함.
Liskov Substitution Principle (리스코프 치환 원칙)
어떤 하위타입을 사용해도 실행에 다른 결과는 같아야한다. 프로그램을 변경하지 않고 하위 타입의 객체로 치환이 가능해야한다는 원칙.
Interface Segregation Principle (인터페이스 분리 원칙)
인터페이스 분리 원칙은 여러개의 메서드를 가진 인터페이스가 있다면 매우 정확하고 구체적인 구분에 따라 더 적은 수의 메서드를 가진 여러개의 메서드로 분할하는 것이 좋다는 원칙
Dependency Inversion Principle (의존성 역전 원칙)
의존성 역전 원칙은 상위 클래스는 하위 클래스 구현에 의존해서는 안되며, 하위 클래스는 상위 클래스의 인터페이스에 의존하도록 해야한다는 원칙. 코드가 세부사항이나 구체적인 구현에 적응하도록하지 않고 추상화된 객체에 적응하도록 하는 것.