객체 지향 프로그래밍에서 클래스는 상속 관계를 가질 수 있으며, 자식 클래스 타입의 객체는 부모 클래스 타입으로 간주될 수 있습니다. 이때 부모 클래스에 정의된 메서드가 자식 클래스에서 재정의되었다면, 해당 메서드를 호출했을 때 객체의 실제 타입에 따라 다른 동작을 수행하게 됩니다. 이러한 특성을 다형성(Polymorphism)이라고 합니다.
예를 들어, Human이라는 기본 클래스에서 Employee와 Student 클래스를 파생시키고, 각 클래스에서 자신의 역할을 설명하는 describe_role() 메서드를 구현해 보겠습니다.
class Human:
def __init__(self, name_val, age_val):
self.name = name_val
self.age = age_val
def describe_role(self):
"""인간의 기본적인 역할을 설명합니다."""
return f"저는 {self.name}입니다. 일반적인 활동을 합니다."
class Employee(Human):
def __init__(self, name_val, age_val, dept_val):
super().__init__(name_val, age_val)
self.department = dept_val
def describe_role(self):
"""직원의 역할을 설명합니다."""
return f"저는 {self.name}이고, {self.department} 부서에서 근무하는 직원입니다."
class Student(Human):
def __init__(self, name_val, age_val, major_val):
super().__init__(name_val, age_val)
self.major = major_val
def describe_role(self):
"""학생의 역할을 설명합니다."""
return f"저는 {self.name}이고, {self.major}를 전공하는 학생입니다."
이제 다양한 타입의 객체를 받아 describe_role() 메서드를 호출하는 함수를 만들어 보면, 인자로 전달되는 객체의 실제 타입에 따라 적절한 메서드가 실행되는 것을 확인할 수 있습니다.
def introduce_entity(entity_object):
"""다양한 엔티티의 역할을 출력합니다."""
print(entity_object.describe_role())
# 각 클래스의 인스턴스 생성
base_human_entity = Human('김철수', 30)
workplace_employee = Employee('박영희', 25, '개발')
university_student_entity = Student('이지은', 20, '컴퓨터공학')
# 함수 호출 결과
introduce_entity(base_human_entity)
introduce_entity(workplace_employee)
introduce_entity(university_student_entity)
# 예상 출력:
# 저는 김철수입니다. 일반적인 활동을 합니다.
# 저는 박영희이고, 개발 부서에서 근무하는 직원입니다.
# 저는 이지은이고, 컴퓨터공학을 전공하는 학생입니다.
위 예시에서 introduce_entity() 함수는 entity_object라는 단일 매개변수를 받지만, Human, Employee, Student 객체에 대해 모두 올바르게 동작합니다. 이는 메서드 호출이 객체의 실제 타입에 따라 다르게 해석되는 다형성의 핵심입니다. workplace_employee.describe_role()이 호출될 때, 파이썬은 먼저 Employee 클래스에 해당 메서드가 정의되어 있는지 확인하고, 있다면 이를 사용합니다. 없다면 상속 체인을 따라 위로 올라가면서 메서드를 찾게 됩니다.
파이썬의 동적 타이핑과 덕 타이핑
파이썬은 동적 언어이므로, introduce_entity(entity_object) 함수의 entity_object 매개변수는 반드시 Human 클래스나 그 자식 클래스의 인스턴스일 필요가 없습니다. describe_role() 메서드를 가지고 있기만 하다면 어떤 객체든 전달될 수 있으며, 함수는 정상적으로 동작합니다. 이러한 특성을 덕 타이핑(Duck Typing)이라고 합니다. "오리처럼 걷고 오리처럼 꽥꽥거린다면, 그것은 오리이다"라는 말처럼, 객체가 특정 메서드를 구현하고 있다면 해당 타입처럼 취급될 수 있습니다.
관련 없는 클래스로 덕 타이핑을 보여드리겠습니다.
class Robot:
def __init__(self, model_id):
self.model = model_id
def describe_role(self):
"""로봇의 역할을 설명합니다."""
return f"저는 로봇 {self.model}입니다. 지정된 작업을 수행합니다."
# 로봇 인스턴스 생성
ai_robot_unit = Robot('Bumblebee-7')
# describe_role() 메서드를 가지고 있으므로 introduce_entity 함수에 전달 가능
introduce_entity(ai_robot_unit)
# 예상 출력:
# 저는 로봇 Bumblebee-7입니다. 지정된 작업을 수행합니다.
이러한 유연성은 파이썬과 같은 동적 언어와 Java와 같은 정적 언어 간의 큰 차이점 중 하나입니다. 파이썬에서는 인스턴스 메서드 호출 시 타입을 미리 검사하지 않고, 해당 메서드가 존재하고 매개변수가 올바르다면 런타임에 호출이 진행됩니다.
실용적인 예시: json.load()와 파일 유사 객체
파이썬의 open() 함수는 디스크 파일을 열고 File 객체를 반환하며, 이 객체는 파일 내용을 읽는 read() 메서드를 가지고 있습니다. json.load() 함수는 이러한 File 객체로부터 JSON 데이터를 읽어 파싱하는 데 사용됩니다.
예를 들어, 파일에서 JSON 데이터를 읽는 일반적인 방법은 다음과 같습니다.
import json
# 예를 들어, 'sample_data.json' 파일에 `["itemA", "itemB"]` 와 같은 내용이 있다고 가정합니다.
# with open('sample_data.json', 'r', encoding='utf-8') as file_descriptor:
# parsed_json_result = json.load(file_descriptor)
# print(parsed_json_result)
파이썬의 동적 특성 덕분에 json.load()는 반드시 실제 File 객체로부터 내용을 읽어야 하는 것은 아닙니다. read() 메서드를 구현하여 파일처럼 동작하는 객체라면 어떤 객체든 "파일 유사 객체(File-like Object)"로 간주되어 json.load()에 전달될 수 있습니다.
다음은 문자열 r'{"products": ["Laptop", "Mouse", "Keyboard"]}'를 파일 유사 객체로 래핑하여 json.load()가 파싱할 수 있도록 하는 예시입니다.
import json
class StringReadStream:
"""메모리 내 문자열을 파일처럼 읽을 수 있도록 하는 클래스."""
def __init__(self, content_string):
self._data = content_string
self._current_position = 0
def read(self, size=-1):
"""
문자열의 일부 또는 전체를 읽습니다.
size가 -1이면 전체를 읽고, 아니면 size만큼 읽습니다.
"""
if self._current_position >= len(self._data):
return "" # 더 이상 읽을 내용이 없음
if size == -1:
# 남은 모든 데이터를 읽음
chunk = self._data[self._current_position:]
self._current_position = len(self._data)
return chunk
else:
# 지정된 크기만큼 데이터를 읽음
end_pos = min(self._current_position + size, len(self._data))
chunk = self._data[self._current_position:end_pos]
self._current_position = end_pos
return chunk
# 파싱할 JSON 형식의 문자열
sample_json_string = r'{"products": ["Laptop", "Mouse", "Keyboard"], "quantity": 3}'
# 사용자 정의 파일 유사 객체 인스턴스 생성
string_data_source = StringReadStream(sample_json_string)
# json.load() 함수에 전달하여 파싱
parsed_content_from_string = json.load(string_data_source)
print(parsed_content_from_string)
StringReadStream 클래스는 실제 파일을 다루지 않으면서도 read() 메서드를 구현하여 파일 객체처럼 동작합니다. 이 덕분에 json.load()는 이 객체를 파일처럼 취급하고 JSON 데이터를 성공적으로 파싱할 수 있습니다. 이는 파이썬의 동적 타이핑과 덕 타이핑이 실제 코드에서 어떻게 활용되는지 보여주는 좋은 예시입니다.