상속과 구성 Python OOP 가이드

2024. 1. 22. 19:15python/intermediate

상속 및 구성은 ​​객체 지향 프로그래밍에서 두 가지 중요한 개념입니다. 이는 객체 지향 설계의 구성 요소이며 프로그래머가 재사용 가능한 코드를 작성하는 데 도움이 됩니다.

  • Python에서 상속 사용
  • 상속을 사용하는 모델 클래스 계층
  • Python에서 다중 상속을 사용하고 단점을 이해
  • 컴포지션을 사용하여 복잡한 개체 만들기
  • 구성을 적용하여 기존 코드 재사용
  • 구성을 통해 런타임 시 애플리케이션 동작 변경

상속과 구성이란 무엇입니까?

상속 및 구성은 객체 지향 프로그래밍의 두 가지 주요 개념으로 두 클래스 개념 사이의 관계를 모델링합니다. 들은 애플리케이션 설계를 주도하고 새로운 기능이 추가되거나 요구 사항이 변경됨에 따라 애플리케이션이 어떻게 발전해야 하는지 결정합니다.

둘 다 코드 재사용을 가능하게 하지만 이를 수행하는 방식은 다릅니다.

상속이란 무엇입니까?

상속은 소위 is 관계를 모델링합니다. 이는 Base 클래스에서 상속되는 Derived 클래스가 있을 때 Derived가 Base의 특수 버전과 같은 관계를 생성했음을 의미합니다. 상속은 통합 모델링 언어 또는 UML을 사용하여 다음과 같이 표현됩니다.

 

이 모델은 상단에 클래스 이름이 있는 상자로 클래스를 나타냅니다. 기본 클래스를 가리키는 파생 클래스의 화살표로 상속 관계를 나타냅니다. 확장이라는 단어가 일반적으로 화살표에 추가됩니다.

참고: 상속 관계에서:

  • 다른 클래스에서 상속되는 클래스를 파생 클래스, 하위 클래스 또는 하위 유형이라고 합니다.
  • 다른 클래스가 파생되는 클래스를 기본 클래스 또는 슈퍼 클래스라고 합니다.
  • 파생 클래스는 기본 클래스를 파생, 상속 또는 확장한다고 합니다.

기본 클래스가 있고 Animal 이 클래스에서 파생되어 Horse 클래스를 생성한다고 가정해 보겠습니다. 상속 관계에 따르면 Horse 은 Animal입니다. 이는 Horse가 인터페이스 및 Animal 구현을 상속하고 응용프로그램에서 Horse 객체를 Animal객체를 대체하기 위해 사용함을 의미합니다.

이를 리스코프 대체 원칙이라고 합니다. 원칙에 따르면 S가 T의 하위 유형인 경우 T 유형의 객체를 S 프로그램의 동작을 변경하지 않는 유형의 객체로 대체합니다.

구성이란 무엇입니까?

구성은 관계를 모델링하는 개념입니다. 다른 유형의 객체를 결합하여 복잡한 유형을 생성할 수 있습니다. 이는 클래스가 Composite 다른 클래스의 개체를 포함할 수 있음을 의미합니다 Component. 이 관계는 Composite 가 또 다른 클래스 Component을 가지고 있음을 의미합니다.

UML은 다음과 같이 구성을 나타냅니다.

 

모델은 복합 클래스에서 다이아몬드로 시작하여 구성 요소 클래스를 가리키는 선을 통해 구성을 나타냅니다. 복합 측면은 관계의 카디널리티를 표현할 수 있습니다. 카디널리티는 Composite 클래스가 포함된 Component 인스턴스의 유효한 범위 또는 수를 나타냅니다.

위 다이어그램에서 1은 Composite 클래스에 Component 유형의 개체가 하나 포함되어 있음을 나타냅니다. 아레의 방법으로 카디널리티를 표현할 수 있습니다.

  • 숫자는 Composite에 포함된 Component 인스턴스의 수를 나타냅니다.
  • * 기호는 Composite 클래스가 가변 개수의 Component 인스턴스를 포함할 수 있음을 나타냅니다.
  • 범위 1..4은 Composite 클래스가 Component 인스턴스 범위를 포함할 수 있음을 나타냅니다. 1..*.와 같이 최소 및 최대 인스턴스 수 또는 최소 및 다수 인스턴스로 범위를 나타냅니다.

참고: 다른 클래스의 개체를 포함하는 클래스를 일반적으로 복합이라고 하며, 더 복잡한 유형을 만드는 데 사용되는 클래스를 구성 요소라고 합니다.

예를 들어, Horse 클래스는 Tail 유형의 다른 개체로 구성될 수 있습니다. Composition을 사용하면 Horse has a Tail라고 말하여 해당 관계를 표현할 수 있습니다.

컴포지션을 사용하면 다른 클래스의 인터페이스와 구현을 상속하는 대신 다른 개체에 개체를 추가하여 코드를 재사용할 수 있습니다. Horse 및 Dog 클래스 모두 한 클래스에서 다른 클래스를 파생시키지 않고도 합성을 통해 Tail 기능을 활용할 수 있습니다.

Python의 상속 개요

Python의 모든 것은 객체입니다. 모듈은 객체이고, 클래스 정의와 함수는 객체이며, 물론 클래스에서 생성된 객체도 객체입니다.

상속은 모든 객체 지향 프로그래밍 언어의 필수 기능입니다. 이는 Python이 상속을 지원한다는 의미이며, 나중에 살펴보겠지만 다중 상속을 지원하는 몇 안 되는 언어 중 하나입니다.

클래스를 사용하여 Python 코드를 작성할 때 상속을 사용하고 있다는 사실을 모르더라도 상속을 사용하게 됩니다.

객체 슈퍼 클래스

Python에서 상속을 확인하는 가장 쉬운 방법은 Python 대화형 셸로 이동하여 약간의 코드를 작성하는 것입니다.

In [ ]:
class EmptyClass:
    pass

당신이 선언한 EmptyClass은 많은 기능을 수행하지는 않지만 가장 기본적인 상속 개념을 설명합니다. 이제 클래스가 선언되었으므로 클래스의 인스턴스를 만들고 dir() 함수를 사용하여 해당 멤버를 나열할 수 있습니다.

In [ ]:
c = EmptyClass()
dir(c)

dir() 함수는 지정된 개체의 모든 구성원 목록을 반환합니다. EmptyClass에 회원을 선언하지 않았는데 목록은 어디서 나온 건가요?

In [ ]:
o = object()
dir(o)

보시다시피 두 목록은 거의 동일합니다. EmptyClass에는 세 개의 추가 구성원이 있습니다.

  • __dict__
  • __module__
  • __weakref__

그러나 object 클래스의 모든 단일 구성원은 EmptyClass에도 존재합니다.

Python에서 생성하는 모든 클래스는 암시적으로 object에서 파생되기 때문입니다. 좀 더 명시적으로 class EmptyClass(object):라고 쓸 수도 있지만 중복되고 불필요합니다.

예외는 예외다

Python에서 생성하는 모든 클래스는 암시적으로 object에서 파생됩니다. 그러나 이 규칙에는 한 가지 예외가 있습니다. 예외를 발생시켜 오류를 나타내는 데 사용되는 클래스입니다.

일반 Python 클래스를 예외처럼 처리하려고 하면 raise Python에서 TypeError를 표시합니다.

In [ ]:
class NotAnError:
    pass

raise NotAnError()

오류 유형을 표시하기 위해 새 클래스를 생성했습니다. 그런 다음 예외 신호를 보내기 위해 클래스를 발생시키려고 했습니다. Python은 실제로 예외를 발생시키지만 출력에는 예외가 NotAnError가 아닌 TypeError 유형이고 모두 exceptions must derive from BaseException유형이라고 나와 있습니다.

BaseException은 모든 오류 유형에 대해 제공되는 기본 클래스입니다. 새 오류 유형을 생성하려면 BaseException 또는 파생 클래스 중 하나에서 클래스를 파생해야 합니다. Python의 규칙은 Exception에서 사용자 정의 오류 유형을 파생시키고, 이는 다시 BaseException에서 파생되는 것입니다.

오류 유형을 정의하는 올바른 방법은 다음과 같습니다.

In [ ]:
class AnError(Exception):
    pass

raise AnError()

이 예에서 AnError는 에서 암시적으로 상속하는 대신 Exception에서 명시적으로 상속합니다. 이러한 변경으로 사용자 정의 예외를 생성하기 위한 요구 사항이 충족되었으며 이제 새 예외 클래스를 발생시킬 수 있습니다. objectAnErrorAnError를 발생시키면 Python이 유형의 오류를 발생시켰다는 내용이 출력에 올바르게 표시됩니다.

클래스 계층 만들기

상속은 관련 클래스의 계층 구조를 만드는 데 사용하는 메커니즘입니다. 이러한 관련 클래스는 기본 클래스가 정의하는 공통 인터페이스를 공유합니다. 파생 클래스는 해당되는 경우 특정 구현을 제공하여 인터페이스를 특수화할 수 있습니다.

이 섹션에서는 HR 시스템 모델링을 시작합니다. 그 과정에서 상속의 사용을 살펴보고 파생 클래스가 기본 클래스 인터페이스의 구체적인 구현을 제공할 수 있는 방법을 살펴보겠습니다.

HR 시스템에서는 회사 직원의 급여를 처리해야 하는데, 급여 계산 방식에 따라 직원 유형이 다릅니다.

먼저 급여를 처리하는 PayrollSystem 클래스를 구현합니다.

In [ ]:
class PayrollSystem:
    def calculate_payroll(self, employees):
        print("Calculating Payroll")
        print("===================")
        for employee in employees:
            print(f"Payroll for: {employee.id} - {employee.name}")
            print(f"- Check amount: {employee.calculate_payroll()}")
            print("")

PayrollSystem은 직원 컬렉션을 가져와 .calculate_payroll() 메소드를 사용하여 그들의 .id, .name과 각 직원 개체에 노출된 .calculate_payroll() 메소드를 구현하여 인쇄 합니다.

이제 모든 직원 유형에 대한 공통 인터페이스를 처리하는 기본 클래스 Employee를 구현합니다.

In [ ]:
class Employee:
    def __init__(self, id, name):
        self.id = id
        self.name = name

Employee는 모든 직원 유형의 기본 클래스입니다. 즉, Employee는 .id 및 .name로 구성됩니다.

HR 시스템에서는 처리되는 모든 Employee 작업이 직원의 주급을 반환하는 .calculate_payroll() 인터페이스를 제공해야 합니다. 해당 인터페이스의 구현은 Employee. 유형에 따라 다릅니다.

예를 들어 관리직 근로자는 고정된 급여를 받기 때문에 매주 동일한 금액을 받습니다.

In [ ]:
class SalaryEmployee(Employee):
    def __init__(self, id, name, weekly_salary):
        super().__init__(id, name)
        self.weekly_salary = weekly_salary

    def calculate_payroll(self):
        return self.weekly_salary

클래스는 기본 클래스에 필요한 .id 및 .name로 초기화되며 super()를 사용하여 기본 클래스의 멤버를 초기화합니다. Supercharge Your Classes With Python super()에서 super()에 관한 모든 것을 읽을 수 있습니다.

SalaryEmployee 또한 직원의 주당 수입을 나타내는 weekly_salary 초기화 매개변수가 필요합니다.

클래스는 HR 시스템에서 사용하는 필수 .calculate_payroll() 메소드를 제공합니다. 구현에서는 weekly_salary.에 저장된 금액만 반환합니다.

회사에서는 시간제 급여를 받는 제조 근로자도 고용하므로 HR 시스템에 HourlyEmployee 다음을 추가합니다.

In [ ]:
class HourlyEmployee(Employee):
    def __init__(self, id, name, hours_worked, hourly_rate):
        super().__init__(id, name)
        self.hours_worked = hours_worked
        self.hourly_rate = hourly_rate

    def calculate_payroll(self):
        return self.hours_worked * self.hourly_rate

HourlyEmployee 클래스는 기본 클래스와 마찬가지로 .id 및 .name과 급여 계산에 필요한 hours_worked 및 hourly_rate로 초기화됩니다. 근무 시간에 시급을 곱하여 .calculate_payroll() 메소드를 구현합니다.

마지막으로 회사는 고정 급여와 판매에 따른 커미션을 받는 판매 직원을 고용하므로 CommissionEmployee 클래스를 만듭니다.

In [ ]:
class CommissionEmployee(SalaryEmployee):
    def __init__(self, id, name, weekly_salary, commission):
        super().__init__(id, name, weekly_salary)
        self.commission = commission

    def calculate_payroll(self):
        fixed = super().calculate_payroll()
        return fixed + self.commission

terminal에서 편집기를 열고 hr.py를 만들어 클래스 계층 만들기에 있는 5개의 모듈을 복사해 넣으세요

두 클래스 모두 고려해야 할 weekly_salary가 있기 때문에 SalaryEmployee에서 CommissionEmployee를 초기화합니다. 동시에 해당 직원의 매출을 기반으로 commission 값으로 CommissionEmployee를 초기화 합니다.

.calculate_payroll()을 사용하면 기본 클래스 구현을 활용하여 fixed 급여를 검색하고 커미션 값을 추가합니다.

CommissionEmployee은 SalaryEmployee에서 파생되므로 weekly_salary 속성에 직접 액세스할 수 있으며 다음을 구현할 수 있습니다. .calculate_payroll()에 해당 속성의 값을 사용합니다.

속성에 직접 액세스할 때의 문제는 SalaryEmployee.calculate_payroll() 구현이 변경되면 CommissionEmployee.calculate_payroll() 구현도 변경해야 한다는 것입니다. 기본 클래스에 이미 구현된 메서드를 사용하고 필요에 따라 기능을 확장하는 것이 더 좋습니다.

시스템에 대한 첫 번째 클래스 계층 구조를 만들었습니다. 클래스의 UML 다이어그램은 다음과 같습니다.

다이어그램은 클래스의 상속 계층 구조를 보여줍니다. 파생 클래스는 PayrollSystem에 필요한 IPayrollCalculator 인터페이스를 구현합니다. PayrollSystem.calculate_payroll() 구현에서는 employees 컬렉션의 객체에 .id 및 .name과 .calculate_payroll() 구현을 포함된 것을 요구합니다.

참고: 인터페이스는 UML 다이어그램의 클래스와 유사하게 표시되며, 인터페이스 이름 위에 인터페이스라는 단어가 있습니다. 인터페이스 이름에는 일반적으로 대문자 I. 접두사가 붙습니다.

Python에서는 인터페이스를 명시적으로 구현하지 않습니다. 대신, 인터페이스는 사용되는 속성과 다른 함수 및 메서드에서 호출되는 메서드에 의해 정의됩니다.

다음으로 새 파일을 만들고 이름을 program.py로 지정합니다. 이 프로그램은 직원을 생성하고 이를 급여 시스템에 전달하여 급여를 처리합니다.

import hr salary_employee = hr.SalaryEmployee(1, "John Smith", 1500) hourly_employee = hr.HourlyEmployee(2, "Jane Doe", 40, 15) commission_employee = hr.CommissionEmployee(3, "Kevin Bacon", 1000, 250) payroll_system = hr.PayrollSystem() payroll_system.calculate_payroll( [salary_employee, hourly_employee, commission_employee] )

명령줄에서 프로그램을 실행하고 결과를 확인할 수 있습니다.

In [ ]:
!python program.py

프로그램은 파생 클래스 각각에 하나씩 세 개의 직원 개체를 생성합니다. 그런 다음 급여 시스템을 생성하고 직원 목록을 .calculate_payroll() 메소드에 전달합니다. 이 메서드는 각 직원의 급여를 계산하고 결과를 인쇄합니다.

Employee 기본 클래스가 어떻게 .calculate_payroll() 메소드를 정의하지 않는지 확인하세요. 즉, 일반 Employee 객체를 생성하여 PayrollSystem에 전달하면 오류가 발생한다는 의미입니다.

Python의 추상 기본 클래스

위 예제의 Employee 클래스는 추상 기본 클래스라고 합니다. 추상 기본 클래스는 상속되기 위해 존재하지만 인스턴스화되지는 않습니다. Python은 추상 기본 클래스를 공식적으로 정의하기 위한 abc 모듈을 제공합니다.

클래스 이름에 앞에 밑줄을 사용하여 해당 클래스의 객체를 생성해서는 안 된다는 점을 알릴 수 있습니다. 밑줄은 코드의 오용을 방지하는 친숙한 방법을 제공하지만 열성적인 사용자가 해당 클래스의 인스턴스를 만드는 것을 막지는 않습니다.

Python 표준 라이브러리의 abc 모듈은 추상 기본 클래스에서 객체 생성을 방지하는 기능을 제공합니다.

Employee 클래스의 구현을 수정하여 인스턴스화할 수 없도록 할 수 있습니다.

from abc import ABC, abstractmethod # ... class Employee(ABC): def __init__(self, id, name): self.id = id self.name = name @abstractmethod def calculate_payroll(self): pass

구현 상속과 인터페이스 상속

한 클래스를 다른 클래스에서 파생시키면 파생 클래스는 다음을 모두 상속합니다.

  • 기본 클래스 인터페이스: 파생 클래스는 기본 클래스의 모든 메서드, 속성 및 특성을 상속합니다.
  • 기본 클래스 구현: 파생 클래스는 클래스 인터페이스를 구현하는 코드를 상속합니다.

대부분의 경우 클래스의 구현을 상속하고 싶지만 다양한 상황에서 객체를 사용할 수 있도록 여러 인터페이스를 구현하고 싶을 것입니다.

현대 프로그래밍 언어는 이러한 기본 개념을 염두에 두고 설계되었습니다. 단일 클래스에서 상속할 수 있지만 여러 인터페이스를 구현할 수도 있습니다.

파이썬에서는 인터페이스를 명시적으로 선언할 필요가 없습니다. 원하는 인터페이스를 구현하는 모든 개체를 다른 개체 대신 사용할 수 있습니다. 이를 덕 타이핑이라고 합니다. 덕 타이핑은 일반적으로 오리처럼 걷고 오리처럼 꽥꽥거린다면 오리임에 틀림없다라고 설명됩니다. 즉, 오리처럼 행동하면 오리로 간주되기에 충분합니다.

이를 설명하기 위해 이제 위의 예에 DisgruntledEmployee 클래스를 추가할 것입니다. 이 클래스는 Employee에서 파생되지 않습니다. disgruntled.py라는 새 파일을 만들고 다음 코드를 추가하세요.

In [ ]:
# disgruntled.py

class DisgruntledEmployee:
    def __init__(self, id, name):
        self.id = id
        self.name = name

    def calculate_payroll(self):
        return 1_000_000

DisgruntledEmployee 클래스는 Employee에서 파생되지 않지만 PayrollSystem에 필요한 것과 동일한 인터페이스를 노출합니다. PayrollSystem.calculate_payroll()에는 다음 인터페이스를 구현하는 개체 목록이 필요합니다.

  • .id 직원의 ID를 반환하는 속성 또는 속성
  • .name 직원의 이름을 나타내는 속성 또는 속성
  • .calculate_payroll() 매개변수를 사용하지 않고 처리할 급여 금액을 반환하는 메소드

DisgruntledEmployee 수업은 이러한 모든 요구 사항을 충족하므로 PayrollSystem 여전히 급여를 계산할 수 있습니다.

프로그램을 수정하여 DisgruntledEmployee 클래스를 사용할 수 있습니다:

# program.py import hr import disgruntled salary_employee = hr.SalaryEmployee(1, "John Smith", 1500) hourly_employee = hr.HourlyEmployee(2, "Jane Doe", 40, 15) commission_employee = hr.CommissionEmployee(3, "Kevin Bacon", 1000, 250) disgruntled_employee = disgruntled.DisgruntledEmployee(20000, "Anonymous") payroll_system = hr.PayrollSystem() payroll_system.calculate_payroll( [ salary_employee, hourly_employee, commission_employee, disgruntled_employee, ] )

프로그램은 DisgruntledEmployee 개체를 생성하고 이를 PayrollSystem 처리하는 목록에 추가합니다. 이제 프로그램을 실행하고 출력을 확인할 수 있습니다.

# terminal python program.py

보시다시피 PayrollSystem는 원하는 인터페이스를 충족하므로 새 개체를 계속 처리할 수 있습니다.

프로그램에서 개체를 재사용하기 위해 특정 클래스에서 파생할 필요가 없으므로 원하는 인터페이스를 구현하는 대신 상속을 사용해야 하는 이유가 궁금할 수 있습니다. 다음 규칙은 이러한 결정을 내리는 데 도움이 될 수 있습니다.

상속을 사용하여 구현 재사용: 파생 클래스는 기본 클래스 구현의 대부분을 활용해야 합니다. 또한 is 관계를 모델링해야 합니다. Customer 클래스에는 .id 및 .name도 있을 수 있지만 Customer는 클래스가 아닙니다. Employee 따라서 이 경우 상속을 사용하면 안 됩니다.

재사용할 인터페이스 구현: 애플리케이션의 특정 부분에서 클래스를 재사용하려는 경우 클래스에서 필요한 인터페이스를 구현하지만 그렇게 하지 않습니다. 기본 클래스를 제공하거나 다른 클래스에서 상속할 필요는 없습니다.

이제 위의 예를 정리하여 다음 주제로 넘어갈 수 있습니다. disgruntled.py 파일을 삭제한 다음 hr 모듈을 원래 상태로 수정할 수 있습니다.

# hr.py class PayrollSystem: def calculate_payroll(self, employees): print("Calculating Payroll") print("===================") for employee in employees: print(f"Payroll for: {employee.id} - {employee.name}") print(f"- Check amount: {employee.calculate_payroll()}") print("") class Employee: def __init__(self, id, name): self.id = id self.name = name class SalaryEmployee(Employee): def __init__(self, id, name, weekly_salary): super().__init__(id, name) self.weekly_salary = weekly_salary def calculate_payroll(self): return self.weekly_salary class HourlyEmployee(Employee): def __init__(self, id, name, hours_worked, hourly_rate): super().__init__(id, name) self.hours_worked = hours_worked self.hourly_rate = hourly_rate def calculate_payroll(self): return self.hours_worked * self.hourly_rate class CommissionEmployee(SalaryEmployee): def __init__(self, id, name, weekly_salary, commission): super().__init__(id, name, weekly_salary) self.commission = commission def calculate_payroll(self): fixed = super().calculate_payroll() return fixed + self.commission

기본적으로 파생 클래스인 Employee 클래스의 .id 및 .name 속성 구현을 상속합니다. .calculate_payroll() 는 PayrollSystem.calculate_payroll() 메소드에 대한 인터페이스일 뿐이므로 기본 클래스에서 구현할 필요가 없습니다.

CommissionEmployee 클래스가 SalaryEmployee에서 어떻게 파생되는지 확인하세요. 즉, CommissionEmployee는 SalaryEmployee의 구현과 인터페이스를 상속받습니다. CommissionEmployee.calculate_payroll() 메소드가 자체 버전을 구현하기 위해 super().calculate_payroll()의 결과에 의존하기 때문에 기본 클래스 구현을 어떻게 활용하는지 확인할 수 있습니다.

'python > intermediate' 카테고리의 다른 글

Python3 중급 주제  (0) 2024.01.24
프롬프트 엔지니어링 실제 사례  (1) 2024.01.23
python-for-data-analysis-II  (0) 2024.01.21
python-for-data-analysis  (0) 2024.01.20
Python에서 JSON 데이터 작업  (1) 2024.01.07