[지식/이론][python] 객체지향 프로그래밍에 관하여
object: 파이썬에서의 최상위 클래스
object는 파이썬에서의 최상위 클래스이다. 어떤 클래스가 어떠한 상속도 받지 않아도 암묵적으로 object를 상속받는다고 한다. 다음의 코드는
class NoInherit(object):
pass
다음의 코드와 동일하다.
class NoInherit():
pass
class NoInherit2:
pass
그럼에도 object를 클래스에 상속하도록 명시하는 이유는 다음이 있다.
- 파이썬 2.2 이상 버전에서는 C언어와의 호환성 문제로 파이썬 내장 자료형을 상속받아서 사용자 정의 클래스를 만드는 데에 문제가 있었다고 한다. 그래서 이러한 문제점을 해결하기 위해 위 코드에서처럼 object를 명시적으로 상속시켜야 했다. 그런데 파이썬 3부터는 이러한 문제가 해결되어 굳이 object를 명시하지 않아도 된다. 그럼에도, 이전 파이썬 버전과의 호환성을 위해 가끔 object를 명시하여 상속하도록 적는 경우도 있다고 한다.
- 사용자 정의 클래스가 다른 클래스로부터 상속받지 않아도, object라는 최상위 클래스로부터 암시적으로 상속받는 것을 명시적으로 표기하기 위함. 이것은 개발자 각자의 취향.
특수화 (specialization)
특수화는 상위 클래스의 모든 속성을 상속하여 새로운 하위 클래스를 만드는 것을 의미한다. 상위 클래스의 모든 메서드들은 이를 상속받는 하위 클래스에서 재정의(override)할 수 있다. (재구현, re-implemented 이라고도 한다) 상속은 is-a 관계이다. 이 말이 무슨 의미냐면, 사람 클래스가 있고 이를 상속받는 한국인 클래스가 있다고 하면 “모든 한국인은 사람이다”라는 논리가 성립하는데, 이러한 논리 관계가 is-a관계이다. is-a 관계는 보통 그 역은 성립되지 않는다. 앞선 예에서도 “모든 사람은 한국인이다”가 성립되지 않는다. 참고로 has-a 관계도 있는데, 연필 클래스와 지우개 클래스가 있을 때, 연필 위에 지우개를 탑재한 지우개 연필을 구현한다면 연필 클래스는 지우개 클래스를 소유하게 된다. 이러한 관계가 has-a 관계이다.
상속과 오버라이딩에 관한 자세한 사항은 객체와 클래스 (Object and class) 문서 참고.
다형성 (polymorphism)
동적 메서드 바인딩이라고도 불리는 다형성은 상위 클래스의 메서드를 서브 클래스 내에서 오버라이딩할 수 있는 성질을 의미한다. 서브 클래스에서 상위 클래스의 메서드를 오버라이딩한 후 서브 클래스 객체를 생성하고 해당 메서드를 호출하면 기본적으로 서브 클래스의 메서드가 호출된다. 만약 서브 클래스 객체에서 상위 클래스의 메서드를 호출하고자 한다면 super() 메서드를 사용하면 된다. 다형성에 관한 내용은 객체와 클래스 (Object and class) 문서에서도 확인할 수 있다.
합성(composition)과 집합화 (aggregation)
- 합성
합성은 한 클래스 내에서 다른 클래스의 객체를 가져와 그 객체의 인스턴스 변수 및 메서드를 이용하는 것을 말한다. 다음은 합성을 사용한 예제이다.
class Eraser():
"""
지우개 역할을 하는 클래스.
"""
def __init__(self, shape: str, strength: int):
"""
Attributes
shape: 지우개 모양
strength: 지우개가 잘 지워지는 정도. 1~10. 10일 수록 잘 지워짐
"""
self.shape = shape
self.strength = strength
def erase(self, one_element: list, erase_index: int):
"""
연필로 쓴 것을 지운다.
연필로 작성한 내용들을 리스트로 모은 one_element의
erase_index번째 요소를 지운다.
"""
deleted_data = one_element.pop(erase_index)
print(f"{deleted_data}를 지우개로 지웠습니다.")
class Pencil():
"""
연필 역할을 하는 클래스.
"""
def __init__(self, kind: str, length: int):
"""
Attributes
kind: 연필 종류. 예) 2B, 4H
length: 연필의 길이 (단위: cm)
contents_to_write: 연필로 쓰고자 하는 내용들을 저장.
"""
self.kind = kind
self.length = length
self.contents_to_write = []
self.attaching_eraser = Eraser("원기둥", "3") # composition
def write(self, new_data: str):
"""
연필로 무언가를 작성한다.
"""
self.contents_to_write.append(new_data)
print(f'{new_data}라는 내용을 연필로 작성하였습니다.')
def show_contents(self):
"""
연필로 작성한 내용을 보기.
"""
return self.contents_to_write
def erase_using_top_of_pencil(self, erase_index: int):
"""
연필 끝에 부착된 지우개로 내용을 지운다.
"""
self.attaching_eraser.erase(self.contents_to_write, erase_index)
if __name__ == '__main__':
pencil = Pencil("2B", 15)
contents = ['필기', '낙서', '편지글']
for content in contents:
pencil.write(content)
print(pencil.show_contents())
pencil.erase_using_top_of_pencil(1)
print(pencil.show_contents())
필기라는 내용을 연필로 작성하였습니다.
낙서라는 내용을 연필로 작성하였습니다.
편지글라는 내용을 연필로 작성하였습니다.
['필기', '낙서', '편지글']
낙서를 지우개로 지웠습니다.
['필기', '편지글']
위 예제의 Pencil 클래스 내부에서 Eraser 클래스의 객체를 변수에 할당하고, 해당 Eraser 객체의 메서드를 Pencil 클래스의 인스턴스 메서드 내에서 사용하는 것을 볼 수 있다. 이것이 합성이다. 굳이 Eraser 클래스를 상속받지 않고도 해당 클래스 객체의 기능들을 사용할 수 있는 방법이다.
Pencil 클래스 내부에서 전혀 관련이 없었던 Eraser 클래스의 객체를 생성함으로써 해당 객체는 Pencil 클래스에 강한 의존성을 띄게 되었다. 따라서 Pencil 클래스의 객체를 만약 삭제한다면 (위 예제에서 del pencil) 해당 객체 내부에 존재하던 Eraser 클래스의 객체도 같이 삭제된다.
반대로, 만약 Eraser 클래스 정의 자체를 삭제한다면 Pencil 클래스 내부에 선언된 Eraser 클래스의 객체 부분에서도 에러가 발생할 것이다. 즉, 합성으로 연결된 두 클래스의 객체는 서로 강한 관계 및 의존성을 띄게 된다.
어떤 자료에서는 합성이 has-a 관계라고 하는데, 또 다른 곳에서는 has-a 관계는 집합화에 해당하고, 합성은 part-of 관계로 설명하기도 한다. “one object IS PART-OF ANOTHER OBJECT” 즉, 한 객체가 다른 객체의 일부(part-of)가 되기 때문이다. 합성의 의존성을 더 강조하기 위해 이렇게 표현한 것 같다.
보통 상속은 암묵적 선언이라고 하고, 합성은 명시적 선언이라고도 한다.
합성은 집합화의 한 종류이다. 집합화는 바로 다음에 설명할 것이다.
- 집합화
집합화는 한 클래스의 객체가 다른 클래스의 객체에 접근하는 것을 말하며, 보통 합성처럼 한 클래스의 내부에 다른 클래스의 객체를 형성하는 방식이 아니라, 한 클래스의 객체의 생성자 또는 메서드의 매개변수에 다른 클래스의 객체를 대입하는 방식이다. 다음은 앞선 예제 1-1을 집합화 방식으로 바꾼 예제이다.
class Eraser():
"""
지우개 역할을 하는 클래스.
"""
def __init__(self, shape: str, strength: int):
"""
Attributes
shape: 지우개 모양
strength: 지우개가 잘 지워지는 정도. 1~10. 10일 수록 잘 지워짐
"""
self.shape = shape
self.strength = strength
def erase(self, elements: list, erase_index: int):
"""
연필로 쓴 것을 지운다.
연필로 작성한 내용들을 리스트로 모은 one_element의
erase_index번째 요소를 지운다.
"""
deleted_data = elements.pop(erase_index)
print(f"{deleted_data}를 지우개로 지웠습니다.")
class Pencil():
"""
연필 역할을 하는 클래스.
"""
def __init__(self, kind: str, length: int, eraser_obj: Eraser):
"""
Attributes
kind: 연필 종류. 예) 2B, 4H
length: 연필의 길이 (단위: cm)
contents_to_write: 연필로 쓰고자 하는 내용들을 저장.
"""
self.kind = kind
self.length = length
self.contents_to_write = []
self.attaching_eraser = eraser_obj # Aggregation
def write(self, new_data: str):
"""
연필로 무언가를 작성한다.
"""
self.contents_to_write.append(new_data)
print(f'{new_data}라는 내용을 연필로 작성하였습니다.')
def show_contents(self):
"""
연필로 작성한 내용을 보기.
"""
return self.contents_to_write
def erase_using_top_of_pencil(self, erase_index: int):
"""
연필 끝에 부착된 지우개로 내용을 지운다.
"""
self.attaching_eraser.erase(self.contents_to_write, erase_index)
if __name__ == '__main__':
eraser = Eraser("원기둥", "3")
pencil = Pencil("2B", 15, eraser) # eraser 객체를 pencil 객체 생성자의 인자로 대입한다.
contents = ['필기', '낙서', '편지글']
for content in contents:
pencil.write(content)
print(pencil.show_contents())
pencil.erase_using_top_of_pencil(1)
print(pencil.show_contents())
필기라는 내용을 연필로 작성하였습니다.
낙서라는 내용을 연필로 작성하였습니다.
편지글라는 내용을 연필로 작성하였습니다.
['필기', '낙서', '편지글']
낙서를 지우개로 지웠습니다.
['필기', '편지글']
결과는 합성 방식과 동일하다. 그러나 위 예제에서 쓰인 방식인 집합화는 합성과 다르게 eraser 객체를 pencil 클래스 안에서 생성하지 않고 각자 독립적으로 객체를 생성한 뒤, pencil 객체 생성자의 인자로 eraser 객체를 대입하고 있다. 이러한 방식이 집합화이다.
이러한 특성으로, 집합화는 합성과 달리 두 클래스가 좀 더 약한 연관 관계를 가진다. 합성처럼 한 클래스 내부에 다른 클래스의 객체를 생성하는 방식이 아닌, 두 객체를 독립적으로 생성하고 한 객체를 다른 객체의 인자로 대입하는 방식이기에 집합화는 독립성을 띈다. 위 예제에서도 pencil 객체를 삭제한다고 해서 eraser 객체에 문제가 생기진 않는다.
위 예제에서 pencil 객체가 eraser 객체를 인자로 가졌다. 집합화는 has-a 관계라고도 불린다.
앞서 합성은 두 클래스 간에 강한 의존 관계를 가진다고 했다. 즉 쌍방으로 연결되어 있다. 그러나 집합화는 단방향성을 띈다. 즉, pencil이 eraser를 가지는 것이지 eraser가 pencil을 가지는 것은 아니기 때문이다.
다른 클래스의 일부 기능만을 가져와서 사용하고픈데 상속은 다른 클래스의 전체를 상속하므로 이를 피하고 일부 기능만을 가져오고자 할 때 합성 및 집합화를 사용한다. 그리고 두 클래스 간에 의존성을 피하고자 할 때는 합성 대신 집합화를 사용하면 되겠다.
상속의 문제점들로 인해 합성을 사용하는 것이 나은 더 자세한 이유는 References의 [5]에서 자세히 설명되어있다.
Reference
[1] 지은이: 미아 스타인, 옮긴이: 최길우, “파이썬 자료구조와 알고리즘”, (2019, 한빛미디어)
[2]
Python OOPS - Aggregation and Composition - GeeksforGeeks
[3]
[4]
[5]
💠 상속을 자제하고 합성(Composition)을 이용하자
[6]
[7]
[Python] 클래스 선언 시 object 상속을 하는 이유
[8]
This content is licensed under
CC BY-NC 4.0
댓글남기기