PEP 3133 – 역할 소개
- Author:
- Collin Winter <collinwinter at google.com>
- Status:
- Rejected
- Type:
- Standards Track
- Requires:
- 3115, 3129
- Created:
- 01-May-2007
- Python-Version:
- 3.0
- Post-History:
- 13-May-2007
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
거부 공지
이 PEP는 PEP 3119가 더 합리적이고 더 최소주의적인 접근 방식으로 나아가도록 하는 데 기여했습니다. 하지만 PEP 3119의 최신 버전을 고려하면, 저는 그쪽을 훨씬 더 선호합니다. GvR.
초록
Python의 기존 객체 모델은 객체를 구현에 따라 구성합니다. 객체를 그 역할을 어떻게 수행하는지(구현)가 아니라 더 큰 시스템에서 맡는 역할(의도)에 따라 구성하는 것이 바람직한 경우가 많으며, Python처럼 덕 타이핑에 기반한 언어에서는 특히 그렇습니다. 이 PEP는 객체를 구현이 아닌 의도에 따라 조직하는 메커니즘인 역할(role)이라는 개념을 도입합니다.
근거
태초에 객체가 있었습니다. 객체 덕분에 프로그래머는 함수와 상태를 결합하고, 다형성과 상속 같은 개념을 통해 코드 재사용성을 높일 수 있었으며, 보십시오, 그것은 좋았습니다. 그러나 상속과 다형성만으로는 충분하지 않은 때가 왔습니다. 개와 나무가 모두 발명되면서, 우리는 더 이상 “이것이 ‘bark’를 이해하는가?”를 아는 것만으로 만족할 수 없게 되었습니다. 이제 우리는 주어진 객체가 “bark”를 무슨 뜻으로 여기는지 알아야 했습니다.
한 가지 해법이자 여기에서 자세히 설명하는 해법은 역할로, 이는 전통적인 클래스/인스턴스 체계와 직교하면서 상호 보완적인 메커니즘입니다. 클래스가 상태와 구현을 다루는 반면, 역할 메커니즘은 주어진 클래스에 구현된 행동만을 전적으로 다룹니다.
이 시스템은 원래 “트레이트(traits)”라고 불렸으며 Squeak Smalltalk용으로 구현되었습니다 [4]. 이후 이 시스템은 Perl 6 [3]에서 사용할 수 있도록 개조되었는데, 그곳에서는 “roles”라고 불리며, 이 개념은 주로 그곳을 바탕으로 현재 Python 3에 맞게 해석되고 있습니다. Python 3에서도 “roles”라는 이름을 유지할 것입니다.
간단히 말해, 역할은 객체가 무엇을 하는지 알려 주고, 클래스는 객체가 그것을 어떻게 하는지 알려 줍니다.
이 PEP에서는 주어진 객체가 이해하는 “bark”가 나무와 같은 것인지 개와 같은 것인지를 쉽게 판별할 수 있게 해 주는 Python 3용 시스템의 개요를 설명하겠습니다. (더 진지한 예도 있을 수 있습니다.)
문법에 관한 참고 사항
이 PEP의 문법 제안은 잠정적이며, 논의를 위한 시안(strawman)으로 간주해야 합니다. 이 PEP가 의존하는 필수 요소들, 즉 PEP 3115의 클래스 정의 문법과 PEP 3129의 클래스 데코레이터는 아직 공식화되는 중이며 변경될 수 있습니다. 물론 함수 이름은 사소한 문제를 두고 길게 이어지는 논쟁(bikeshedding)의 대상이 될 것입니다.
역할 수행하기
정적 역할 할당
Tree와 Dog 클래스를 정의하는 것부터 시작합시다
class Tree(Vegetable):
def bark(self):
return self.is_rough()
class Dog(Animal):
def bark(self):
return self.goes_ruff()
둘 다 같은 시그니처의 bark() 메서드를 구현하지만, 하는 일은 전혀 다릅니다. 우리가 기대하는 것을 구별할 방법이 필요합니다. 상속과 단순한 isinstance() 검사에 의존하면 코드 재사용이 제한되거나, 타당한지와 관계없이 개와 비슷한 모든 클래스가 Dog를 상속하도록 강제되거나, 혹은 둘 다일 수 있습니다. 역할이 도움이 될 수 있는지 살펴봅시다.
@perform_role(Doglike)
class Dog(Animal):
...
@perform_role(Treelike)
class Tree(Vegetable):
...
@perform_role(SitThere)
class Rock(Mineral):
...
PEP 3129의 클래스 데코레이터를 사용하여 특정 역할 하나 또는 여러 개를 클래스에 연결합니다. 이제 클라이언트 코드는 전달받은 객체가 Doglike 역할을 수행하는지 검증할 수 있으므로, Wolf, LaughingHyena, Aibo [1] 인스턴스도 처리할 수 있습니다.
역할은 일반적인 상속을 통해 조합할 수 있습니다:
@perform_role(Guard, MummysLittleDarling)
class GermanShepherd(Dog):
def guard(self, the_precious):
while True:
if intruder_near(the_precious):
self.growl()
def get_petted(self):
self.swallow_pride()
여기서 GermanShepherd 인스턴스는 세 가지 역할을 수행합니다. Guard와 MummysLittleDarling은 직접 적용되고, Doglike는 Dog로부터 상속됩니다.
런타임에 역할 부여하기
데코레이터가 제공하는 문법적 설탕을 풀어 쓰면 런타임에도 역할을 부여할 수 있습니다.
다른 모듈에서 Robot 클래스를 임포트한다고 가정해 봅시다. Robot이 이미 우리의 Guard 인터페이스를 구현하고 있다는 것을 알고 있으므로, 경비 관련 코드와도 잘 어울려 동작하기를 바랍니다.
>>> perform(Guard)(Robot)
이는 즉시 적용되며 Robot의 모든 인스턴스에 영향을 줍니다.
역할에 관해 질문하기
로봇 군대에게 경비라고 알려 주었더라도, 가끔 그들을 점검하여 여전히 임무를 수행하고 있는지 확인하고 싶습니다.
>>> performs(our_robot, Guard)
True
저기 있는 저 로봇은 어떻습니까?
>>> performs(that_robot_over_there, Guard)
True
performs() 함수는 주어진 객체가 주어진 역할을 수행하는지 묻는 데 사용합니다. 그러나 클래스에게 그 인스턴스들이 역할을 수행하는지 묻는 데에는 사용할 수 없습니다:
>>> performs(Robot, Guard)
False
이는 Robot 클래스가 Robot 인스턴스와 서로 바꿔 쓸 수 있는 것이 아니기 때문입니다.
새로운 역할 정의하기
빈 역할
역할은 일반 클래스처럼 정의하지만, Role 메타클래스를 사용합니다.
class Doglike(metaclass=Role):
...
메타클래스는 5가 int이고 tuple이 type인 것과 같은 방식으로 Doglike가 Role임을 나타내는 데 사용됩니다.
상속을 통해 역할 조합하기
역할은 다른 역할을 상속할 수 있으며, 이는 그 역할들을 조합하는 효과를 냅니다. 여기서 Dog의 인스턴스는 Doglike와 FourLegs 역할을 모두 수행합니다.
class FourLegs(metaclass=Role):
pass
class Doglike(FourLegs, Carnivor):
pass
@perform_role(Doglike)
class Dog(Mammal):
pass
구체 메서드 요구하기
지금까지는 빈 역할만 정의했는데, 이는 그다지 유용하지 않습니다. 이제 Doglike 역할을 수행한다고 주장하는 모든 클래스가 bark() 메서드를 정의하도록 요구해 봅시다:
class Doglike(FourLegs):
def bark(self):
pass
메서드를 “추상”으로 표시하는 플래그를 다는 데 데코레이터가 필요하지 않으며, 그 메서드는 절대 호출되지 않습니다. 즉, 메서드에 담긴 코드가 있더라도 아무런 의미가 없습니다. 역할은 오직 추상 메서드만 제공합니다. 구체적인 기본 구현은 믹스인처럼 더 적합한 다른 메커니즘에 맡깁니다.
역할을 정의하고 어떤 클래스가 그 역할을 수행한다고 주장했다면, 그 주장을 반드시 검증해야 합니다. 여기서 프로그래머는 역할이 요구하는 메서드 중 하나의 철자를 잘못 적었습니다.
@perform_role(FourLegs)
class Horse(Mammal):
def run_like_teh_wind(self)
...
이렇게 하면 역할 시스템은 run_like_the_wind() 메서드가 없다고 알리며 예외를 발생시킵니다. 역할 시스템은 클래스가 특정 역할을 수행한다고 표시되는 즉시 이러한 검사를 수행합니다.
구체 메서드는 역할이 요구하는 시그니처와 정확히 일치해야 합니다. 여기서는 bark()의 구체 버전을 정의하여 역할을 수행하려 했지만, 약간 빗나갔습니다.
@perform_role(Doglike)
class Coyote(Mammal):
def bark(self, target=moon):
pass
이 메서드의 시그니처는 Doglike 역할이 기대하던 것과 정확히 일치하지 않으므로, 역할 시스템이 약간 투정을 부리게 됩니다.
메커니즘
다음은 Python에서 역할을 어떻게 표현할 수 있을지에 대한 시안입니다. 여기의 예제는 Python 인터프리터를 변경하지 않고도 역할 메커니즘을 구현할 수 있는 방식으로 작성되었습니다. (예제는 Curtis Poe가 쓴 Perl 6 역할에 관한 글 [2]에서 가져와 수정했습니다.)
- 정적 클래스 역할 할당
@perform_role(Thieving) class Elf(Character): ...
perform_role()은 여러 인자를 받으므로, 다음과 같이 작성해도 됩니다:@perform_role(Thieving, Spying, Archer) class Elf(Character): ...
이제
Elf클래스는Thieving,Spying,Archer역할을 모두 수행합니다. - 인스턴스 질의하기
if performs(my_elf, Thieving): ...
performs()의 두 번째 인자는__contains__()메서드를 가진 것이라면 무엇이든 될 수 있으므로, 다음과 같이 작성해도 됩니다:if performs(my_elf, set([Thieving, Spying, BoyScout])): ...
isinstance()와 마찬가지로, 표현식이 참이 되려면 객체가 집합에 있는 역할 중 하나만 수행하면 됩니다.
추상 베이스 클래스와의 관계
이 PEP의 초기 초안 [5]에서는 역할을 PEP 3119에서 제안된 추상 베이스 클래스와 경쟁하는 것으로 구상했습니다. 추가적인 논의와 숙고 끝에, 다음과 같이 타협안과 책임 및 사용 사례의 분담이 마련되었습니다:
- 역할은 객체의 의미론과 추상적 능력을 나타내는 방법을 제공합니다. 역할은 추상 메서드를 정의할 수 있지만, 이는 특정한 의미론의 집합에 접근하는 인터페이스를 명시하기 위한 방법으로서만 그렇습니다.
Ordering역할은 어떤 순서 연산자 집합이 정의되어 있을 것을 요구할 수 있습니다.class Ordering(metaclass=Role): def __ge__(self, other): pass def __le__(self, other): pass def __ne__(self, other): pass # ...and so on
이렇게 하면 특정 구현에 제약을 두거나 신경 쓰지 않고도 더 큰 시스템 안에서 객체가 맡는 역할이나 함수를 나타낼 수 있습니다.
- 반면에 추상 베이스 클래스는 공통적이고 개별적인 구현 단위를 재사용하는 방법입니다. 예를 들어, 다른 연산자를 이용해 여러 순서 연산자를 구현하는
OrderingMixin을 정의할 수 있습니다.class OrderingMixin: def __ge__(self, other): return self > other or self == other def __le__(self, other): return self < other or self == other def __ne__(self, other): return not self == other # ...and so on
이 추상 베이스 클래스, 더 정확히 말하면 구체적인 믹스인을 사용하면, 프로그래머는 제한된 연산자 집합만 정의하고 나머지는 사실상 믹스인이 “유도”하도록 할 수 있습니다.
이 두 직교적인 시스템을 결합함으로써, a) 기능을 제공하는 것과 b) 이 기능이 존재하며 사용 가능하다는 것을 소비자 시스템에 알리는 것을 모두 할 수 있습니다. 예를 들어, 위의 OrderingMixin 클래스는 Ordering 역할에 표현된 인터페이스와 의미론을 충족하므로, 이 믹스인이 해당 역할을 수행한다고 말합니다:
@perform_role(Ordering)
class OrderingMixin:
def __ge__(self, other):
return self > other or self == other
def __le__(self, other):
return self < other or self == other
def __ne__(self, other):
return not self == other
# ...and so on
이제 이 믹스인을 사용하는 모든 클래스는 자동으로 – 즉, 프로그래머의 추가적인 노력 없이 – Ordering 역할을 수행하는 것으로 표시됩니다.
관심사를 서로 다른 두 개의 직교적인 시스템으로 분리하는 것은 각각을 따로 사용할 수 있게 해 주므로 바람직합니다. 예를 들어, 컨테이너가 해시 값을 결정할 때 그 내용물을 고려한다는 것을 나타내는 RecursiveHash 역할을 제공하는 서드파티 패키지를 생각해 봅시다. Python의 내장 tuple과 frozenset 클래스는 이 의미론을 따르므로, 이들에 RecursiveHash 역할을 적용할 수 있습니다.
>>> perform_role(RecursiveHash)(tuple)
>>> perform_role(RecursiveHash)(frozenset)
이제 RecursiveHash 객체를 사용하는 모든 코드는 튜플과 frozenset도 사용할 수 있게 됩니다.
미해결 문제
인스턴스가 자신의 클래스와 다른 역할을 수행하도록 허용하기
Perl 6에서는 인스턴스가 자신의 클래스와 다른 역할을 수행할 수 있습니다. 이러한 변경은 해당 인스턴스 하나에만 국한되며 클래스의 다른 인스턴스에는 영향을 주지 않습니다. 예를 들면 다음과 같습니다:
my_elf = Elf()
my_elf.goes_on_quest()
my_elf.becomes_evil()
now_performs(my_elf, Thieving) # Only this one elf is a thief
my_elf.steals(["purses", "candy", "kisses"])
Perl 6에서는 인스턴스의 원래 부모를 상속하고 추가 역할을 수행하는 익명 클래스를 생성하여 이를 처리합니다. Python 3에서도 이것이 가능하지만, 그것이 바람직한지는 또 다른 문제입니다.
물론 이 기능이 포함된다면 찰스 디킨스의 작품을 Python으로 표현하기가 훨씬 쉬워질 것입니다:
>>> from literature import role, BildungsRoman
>>> from dickens import Urchin, Gentleman
>>>
>>> with BildungsRoman() as OliverTwist:
... mr_brownlow = Gentleman()
... oliver, artful_dodger = Urchin(), Urchin()
... now_performs(artful_dodger, [role.Thief, role.Scoundrel])
...
... oliver.has_adventures_with(ArtfulDodger)
... mr_brownlow.adopt_orphan(oliver)
... now_performs(oliver, role.RichWard)
속성 요구하기
Neal Norwitz는 메서드를 요구하는 데 사용하는 것과 같은 메커니즘으로 속성의 존재 여부를 단언할 수 있는 기능을 요청했습니다. 역할은 클래스 정의 시점에 효력이 발생하고, 대다수의 속성은 런타임에 클래스의 __init__() 메서드에서 정의되므로, 메서드와 같은 시점에 속성을 검사할 좋은 방법은 없어 보입니다.
그렇더라도 문서화 목적만을 위해서라도 강제되지 않는 속성을 역할 정의에 포함하는 것이 바람직할 수 있습니다.
역할의 역할
제안된 의미 체계에서는 역할이 자기 자신의 역할을 가질 수 있습니다.
@perform_role(Y)
class X(metaclass=Role):
...
이것이 가능하기는 하지만, 역할은 일반적으로 인스턴스화되지 않으므로 의미가 없습니다. 이 표현에 의미를 부여하는 것에 대해 비공개 논의가 있었지만, 지금까지 좋은 아이디어는 나오지 않았습니다.
class_performs()
현재로서는 클래스에 그 인스턴스가 특정 역할을 수행하는지 물어볼 수 없습니다. 다음이 가능하도록 performs()에 대응하는 기능을 제공하는 것이 바람직할 수 있습니다
>>> isinstance(my_dwarf, Dwarf)
True
>>> performs(my_dwarf, Surly)
True
>>> performs(Dwarf, Surly)
False
>>> class_performs(Dwarf, Surly)
True
더 보기 좋은 동적 역할 할당
이 PEP의 초기 초안에는 클래스에 역할을 동적으로 할당하는 별도의 메커니즘이 포함되어 있었습니다. 이 메커니즘은 다음과 같이 표기했습니다
>>> now_perform(Dwarf, GoldMiner)
데코레이터가 제공하는 문법 설탕을 풀어 쓰면 이와 동일한 기능을 이미 사용할 수 있습니다:
>>> perform_role(GoldMiner)(Dwarf)
쟁점은 동적 역할 할당이 전용 표기법을 마련할 만큼 충분히 중요한가 하는 것입니다.
문법 지원
이 PEP에서 제시한 표현 방식은 역할 시스템을 독립 패키지로 배포할 수 있도록 설계되었지만, 역할을 정의하고 할당하고 조회하기 위한 특별한 문법을 추가하는 것이 바람직할 수 있습니다. 한 가지 예로 role 키워드를 들 수 있으며, 이 키워드는 다음을
class MyRole(metaclass=Role):
...
다음으로 변환합니다
role MyRole:
...
역할 할당에는 PEP 3115에서 제안된 클래스 정의 인자를 활용할 수 있습니다:
class MyClass(performs=MyRole):
...
구현
참조 구현은 곧 공개될 예정입니다.
감사의 글
역할과 추상 베이스 클래스의 차이점, 겹치는 부분, 세부 사항을 정리하기 위해 몇 시간에 걸쳐 직접 만나 논의해 준 Jeffery Yasskin, Talin, Guido van Rossum에게 감사드립니다.
참고 문헌
Copyright
This document has been placed in the public domain.