python Inheritance vs Composition

yo·2021년 10월 9일
0


이 글은 이 자료(https://realpython.com/inheritance-composition-python/)를 정리한 것입니다.

What are Inheritance and Composition

inheritance

  • horse class가 animal class상속받음.(because horse is an animal).
  • 이때 코드에서 모든 animal class는 horse class로 대체될 수 있어야 함.(Liskov substitution principle)

Composition

  • Composite has a Component
  • UML에서 숫자 1은 instance 수를 의미한다.

python inheritance 개요

  • 파이썬에서 모든 것은 object다. modules, class, function, 전부 다.
  • 상속은 객체 지향 프로그래밍 언어에서 필수.
  • 파이썬은 다중상속을 지원하는 몇 안되는 언어 중 하나이다.
  • 파이썬을 사용한다면 본인도 모르는 사이에 이미 상속을 사용하고 있는 것이다.

The Object Super class

  • dir는 list of all the members를 return한다.
  • myClass에 특별한 members를 정의하지 않았음에도 여러 값이 출력되는 이유는 클래스가 기본적으로 object를 상속받기 때문이다.
  • 그 증거로 dir의 값이 Myclass와 object가 거의 같다.(__dict__, __weakref__정도만 다르다)
class MyClass:
    pass

c = MyClass()
print(dir(c))
['__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__']
obj = object()
print(dir(obj))

['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
  • class Myclass: 와 class Myclass(object):는 결국 같다.
  • 굳이 후자처럼 할 필요는 없음. (파이썬 2에서는 후자처럼 해야 했다고 함)

Exception

  • 파이썬에서 모든 클래스는 object를 상속 받는데, exception은 예외다.
  • BaseException은 에러정의를 위한 클래스로, 새로운 에러 타입을 만들기 위해서는 이 클래스 혹은 이 클래스를 상속받은 클래스를 상속받아야 한다.
  • 예시 1. (object 상속)
class MyError:
    pass

raise MyError

Traceback (most recent call last):
  File "class_test.py", line 15, in <module>
    raise MyError
TypeError: exceptions must derive from BaseException
  • 예시 2. (Exception 상속)
class MyError(Exception):
    pass

raise MyError

Traceback (most recent call last):
  File "class_test.py", line 15, in <module>
    raise MyError
__main__.MyError

class 상속 예시

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, hour_rate):
        super().__init__(id, name)
        self.hours_worked = hours_worked
        self.hour_rate = hour_rate

    def calculate_payroll(self):
        return self.hours_worked * self.hour_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


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
])



Calculating Payroll
===================
Payroll for: 1 - John Smith
- Check amount: 1500

Payroll for: 2 - Jane Doe
- Check amount: 600

Payroll for: 3 - Kevin Bacon
- Check amount: 1250

  • 문제: employee객체를 caculate_payroll 메서드가 없기 때문에 employee객체를 만들어 PayrollSystem에 돌리면 에러가 난다.
>>> employee = hr.Employee(1, 'Invalid')
>>> payroll_system = hr.PayrollSystem()
>>> payroll_system.calculate_payroll([employee])

Payroll for: 1 - Invalid
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/hr.py", line 39, in calculate_payroll
    print(f'- Check amount: {employee.calculate_payroll()}')
AttributeError: 'Employee' object has no attribute 'calculate_payroll'

해결: Employee클래스를 Abstract Base Class로 만든다.

Abstract Base Classes in Python

  • abstract class는 상속만을 위해 사용된다.
  • 즉, abstract class는 인스턴스화 되지 않는다.
  • abc module을 사용해 abstract class를 만든다.
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
  • 위 코드로 인해 두가지 효과가 생긴다.
  1. Employee클래스의 인스턴스화 방지
  2. Employee클래스를 상속받은 클래스는 abstractmethod를 override해야 함.
>>> import hr
>>> employee = hr.Employee(1, 'abstract')

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class Employee with abstract methods 
calculate_payroll
  • 실제 Employee클래스를 인스턴스화 하면 에러가 나는데, abstractmethod가 오버라이드 되지 않았기 때문이다.

Implementation Inheritance vs Interface Inheritance

이해안됨

The Class Explosion Problem

Inheriting Multiple Classes

Composition in Python

  • Composition은 oo design개념으로 has a 관계로 표현된다.
  • composite class가 component 클래스를 갖는다.
  • 즉, composite class는 다른 클래스의 component를 갖는다.
  • composite class는 component class의 인터페이스를 상속받는 대신에 그 구현만 재사용 할 수 있게 해준다.
  • composition는 두 클래스를 loosely coupled하게 해준다.(장점)
  • 어느 한 클래스가 변경되도 상대에게 거의 영향을 주지 않기 때문에 코드를 유연하게 해준다.
  • inheritance vs composition 디자인 중에서 후자가 보통 더 유연하다.
  • 기존 employee클래스에서 init에 썼던 id, name도 composition이다.(잘 납득 안됨)
# In contacts.py

class Address:
    def __init__(self, street, city, state, zipcode, street2=''):
        self.street = street
        self.street2 = street2
        self.city = city
        self.state = state
        self.zipcode = zipcode

    def __str__(self):
        lines = [self.street]
        if self.street2:
            lines.append(self.street2)
        lines.append(f'{self.city}, {self.state} {self.zipcode}')
        return '\n'.join(lines)
# In employees.py

class Employee:
    def __init__(self, id, name):
        self.id = id
        self.name = name
        self.address = None

# In 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()}')
            if employee.address:
                print('- Sent to:')
                print(employee.address)
            print('')
# In program.py

import hr
import employees
import productivity
import contacts

manager = employees.Manager(1, 'Mary Poppins', 3000)
manager.address = contacts.Address(
    '121 Admin Rd', 
    'Concord', 
    'NH', 
    '03301'
)
secretary = employees.Secretary(2, 'John Smith', 1500)
secretary.address = contacts.Address(
    '67 Paperwork Ave.', 
    'Manchester', 
    'NH', 
    '03101'
)
sales_guy = employees.SalesPerson(3, 'Kevin Bacon', 1000, 250)
factory_worker = employees.FactoryWorker(4, 'Jane Doe', 40, 15)
temporary_secretary = employees.TemporarySecretary(5, 'Robin Williams', 40, 9)
employees = [
    manager,
    secretary,
    sales_guy,
    factory_worker,
    temporary_secretary,
]
productivity_system = productivity.ProductivitySystem()
productivity_system.track(employees, 40)
payroll_system = hr.PayrollSystem()
payroll_system.calculate_payroll(employees)
$ python program.py

Tracking Employee Productivity
==============================
Mary Poppins: screams and yells for {hours} hours.
John Smith: expends {hours} hours doing office paperwork.
Kevin Bacon: expends {hours} hours on the phone.
Jane Doe: manufactures gadgets for {hours} hours.
Robin Williams: expends {hours} hours doing office paperwork.

Calculating Payroll
===================
Payroll for: 1 - Mary Poppins
- Check amount: 3000
- Sent to:
121 Admin Rd
Concord, NH 03301

Payroll for: 2 - John Smith
- Check amount: 1500
- Sent to:
67 Paperwork Ave.
Manchester, NH 03101

Payroll for: 3 - Kevin Bacon
- Check amount: 1250

Payroll for: 4 - Jane Doe
- Check amount: 600

Payroll for: 5 - Robin Williams
- Check amount: 360
  • Employee class는 Address object에 대해 알 필요 없이 Address의 implementation을 사용하고 있다. so flexible.
  • Address의 class를 바꿔도 Emplyee에는 거의 영향이 안간다.

Flexible Designs With Composition

Customizing Behavior With Composition

Choosing Between Inheritance and Composition in Python

Inheritance to Model “Is A” Relationship

Mixing Features With Mixin Classes

Composition to Model “Has A” Relationship

Composition to Change Run-Time Behavior

Choosing Between Inheritance and Composition in Python

Conclusion

Recommended Reading

profile
Never stop asking why

0개의 댓글