 |  |
|
난이도 : 중급 Cesar Otero, Consultant, Freelance
원문 게재일 : 2009 년 1 월 30 일 번역 게재일 : 2009 년 3 월 31 일 Python 3은 Guido van Rossum이 개발한 강력한 범용 프로그래밍 언어의 최신
버전입니다. 이 버전에서는 이전 버전인 2.x 제품군과의 호환성이 지원되지는 않지만 일부 구문
문제가 정리되었습니다. 첫
번째 기사에서 이어지는 이 두 번째 기사에서는 새로운 Python 기능을
좀 더 살펴보고 추상 기본 클래스, 메타클래스 및 데코레이터의 변경 사항과 같은 고급 주제에
대해 자세히 설명합니다.
Python 3000 또는 Py3K이라고도 하는 Python
버전 3에 대한 이전 기사에서는 새로운 print() 함수, bytes
데이터 유형 및 string 유형의 변경 사항과 같이 이전 버전과의 호환성을
지원하지 않는 몇 가지 기본적인 변경 사항에 대해 살펴보았다. 이 연재 기사의 Part 2에서는 ABC(Abstract
Base Class), 메타클래스, 함수 주석 및 데코레이터, 정수 리터럴 지원, 숫자 유형 계층 구조, 예외 발생
및 처리와 관련된 변경 사항 등과 같은 고급 주제를 설명한다. 이러한 변경 사항도 대부분 버전 2.x
제품군과 호환되지 않는다.
클래스 데코레이터
이전 버전의 Python에서는 메소드 변환을 메소드 정의 이후에 수행해야 한다. 메소드의
길이가 긴 경우에는 이 요구 사항으로 인해 정의의 중요한 구성 요소가 외부 인터페이스의 정의와 동떨어지게 된다.
이에 대한 자세한 설명은 PEP(Python Enhancement Proposal) 318(참고자료에서
제공하는 링크 참조)에서 볼 수 있다. 다음 코드에서는 변환 요구 사항의 예를 보여 준다.
Listing 1. 버전 3 이전의 Python에서 사용하는 메소드 변환 방법
def myMethod(self):
# do something
myMethod = transformMethod(myMethod)
|
Python 버전 2.4에서는 동일한 메소드 이름을 여러 번 재사용하지 않으면서
가독성을 높이기 위해 메소드 데코레이터를 도입했다.
데코레이터는 다른 메소드를 수정하고 메소드 또는 호출 가능한 다른
오브젝트를 리턴하는 메소드이다. 데코레이터 이름 앞에 @ 기호를
추가하여 데코레이터임을 표시한다. 이러한 구문은 Java™ 주석과 비슷하다. Listing
2에서는 실제 데코레이터 예를 보여 준다.
Listing 2. 데코레이터 메소드
@transformMethod
def myMethod(self):
# do something
|
데코레이터는 문법적 편의를 높여주는 요소이다. Wikipedia에서는 데코레이터를
"기능에 영향을 주지 않고 사용 편의성을 높여 주기 위해 컴퓨터 언어의 구문에 추가되는 요소"라고
설명하고 있다. 데코레이터는 주로 정적 메소드에 대한 주석을 작성하는 데 사용된다. Listing 1과
Listing 2는 동일한 기능을 수행하지만 Listing 2를 파악하기가 더 쉽다.
데코레이터는 다음과 같이 다른 메소드를 정의하듯이 정의한다.
def mod(method):
method.__name__ = "John"
return method
@mod
def modMe():
pass
print(modMe.__name__)
|
게다가 Python 3에서는 메소드뿐만 아니라 클래스에 대해서도 데코레이터가
지원된다. 다음은 버전 3 이전의 Python에서 사용하는 코드이다.
class myClass:
pass
myClass = doSomethingOrNotWithClass(myClass)
|
위 코드 대신 다음과 같은 코드를 사용할 수 있다.
@doSomethingOrNotWithClass
class myClass:
pass
|
메타클래스
메타클래스는 다른 클래스를 인스턴스로 가지고 있는 클래스이다. Python
3에서는 다른 메타클래스를 만들거나 런타임에 클래스를 동적으로 만드는 데 사용되는 내장
metaclass 유형이 계속 지원되므로 다음과 같은 구문을 사용할 수 있다.
>>>aClass = type('className',
(object,),
{'magicMethod': lambda cls : print("blah blah")})
|
이 구문은 클래스 이름과 같은 문자열, 상속된 오브젝트 Tuple(비어 있을 수 있음)
및 추가할 수 있는 메소드가 포함된 Dictionary(비어 있을 수 있음)를 인수로 받는다. 물론 다음과 같이
type에서 상속받아 고유한 메타클래스를 만들 수도 있다.
class meta(type):
def __new__(cls, className, baseClasses, dictOfMethods):
return type.__new__(cls, className, baseClasses, dictOfMethods)
|
 |
키워드 전용 인수
Python 3에서는 함수 인수를 "매개변수 슬롯"에 할당하는 방법이 변경되었다. 전달할
매개변수에 별표(*)를 사용할 수 있으며, 이 경우 가변 길이 인수를 사용할
수 없다. 명명된 인수가 * 뒤에 있어야 한다(예: def
meth(*, arg1): pass). Python 설명서나 PEP 3102에서 자세한 정보를 볼 수 있다(참고자료에서
제공하는 링크 참조).
|
|
참고: 마지막 두 예제가 잘 이해되지 않는 경우에는 메타클래스에 대한 David Mertz와
Michele Simionato의 연재 기사를 읽어보기를 권한다. 참고자료에서 해당
링크를 제공한다.
이제 클래스 정의를 살펴보자. 클래스 정의에서는 기본 클래스 목록 뒤에 키워드
인수를 사용할 수 있으며, 일반적으로 클래스는 class Foo(*bases, **kwds):
pass의 형태로 정의된다. 메타클래스는 키워드 인수 metaclass를
통해 클래스 정의에 전달된다. 예를 들면, 다음과 같다.
>>>class aClass(baseClass1, baseClass2, metaclass = aMetaClass): pass
|
기존 메타클래스 구문에서는 다음과 같이 메타클래스를 내장 속성 __metaclass__에 할당한다.
class Test(object):
__metaclass__ = type
|
버전 3에서는 새로운 속성인 __prepare__가 추가되었다. 이 속성을
사용하여 새 클래스 네임스페이스에 대한 Dictionary를 만들 수 있다. 이 속성은 클래스 본문이 평가되기 전에
호출된다(Listing 3 참조).
Listing 3. __prepare__ 속성을 사용하는 간단한 메타클래스
def meth():
print("Calling method")
class MyMeta(type):
@classmethod
def __prepare__(cls, name, baseClasses):
return {'meth':meth}
def __new__(cls, name, baseClasses, classdict):
return type.__new__(cls, name, baseClasses, classdict)
class Test(metaclass = MyMeta):
def __init__(self):
pass
attr = 'an attribute'
t = Test()
print(t.attr)
|
PEP 3115에 실린 예제의 일부를 가져온 Listing 4의 흥미로운 예제에서는
클래스 메소드의 선언 순서를 유지하면서 클래스 메소드의 이름 목록이 포함된 메타클래스를
만든다.
Listing 4. 클래스 구성원의 순서를 유지하는 메타클래스
# The custom dictionary
class member_table(dict):
def __init__(self):
self.member_names = []
def __setitem__(self, key, value):
# if the key is not already defined, add to the
# list of keys.
if key not in self:
self.member_names.append(key)
# Call superclass
dict.__setitem__(self, key, value)
# The metaclass
class OrderedClass(type):
# The prepare function
@classmethod
def __prepare__(metacls, name, bases): # No keywords in this case
return member_table()
# The metaclass invocation
def __new__(cls, name, bases, classdict):
# Note that we replace the classdict with a regular
# dict before passing it to the superclass, so that we
# don't continue to record member names after the class
# has been created.
result = type.__new__(cls, name, bases, dict(classdict))
result.member_names = classdict.member_names
return result
|
메타클래스가 변경된 데는 몇 가지 이유가 있다. 오브젝트는 순서를 고려하지
않은 채로 Dictionary에 메소드를 저장한다. 하지만 선언된 클래스 구성원의 순서가 유지되었을
때 더욱 효과적으로 수행할 수 있는 작업도 있다. 클래스를 만들 때 초기에 메타클래스를
포함시키면 정보를 사용 가능한 상태로 유지하면서 순서도 유지할 수 있다. 예를 들어, 이 방법은
C 구조체를 만들 때 유용하게 사용할 수 있다. 앞으로는 이 새로운 메커니즘으로 인해 클래스를
만드는 동안 생성된 클래스 네임스페이스의 본문에 기호를 삽입한 후 기호의 참조를 전달할 수도
있게 될 것이다. PEP 3115에서는 구문을 변경하게 된 미학적인 이유도 설명하고 있지만 이는
객관적으로 입증할 수 없기 때문에 논쟁 수준에 머물러 있다. (참고자료에서
PEP 3115에 대한 링크를 볼 수 있다.)
추상 기본 클래스
이 연재
기사의 첫 번째 기사에서 말했던 것처럼 ABC는 인스턴스화할 수 없는 클래스이다. Java 또는
C++ 언어를 사용하던 프로그래머라면 이 개념에 익숙해져야 한다. Python 3에서는 ABC 작업을
지원하기 위해 새로운 프레임워크인 abc가 추가되었다.
abc 모듈에는 하나의 메타클래스(ABCMeta)와
데코레이터(@abstractmethod 및 @abstractproperty)가
있다. ABC에 @abstractmethod나 @abstractproperty가
있으면 ABC가 인스턴스화되지 않는다. 이 경우에는 데코레이터를 서브클래스에서 재정의해야 한다. 예를 들어, 다음 코드를 살펴보자.
>>>from abc import *
>>>class C(metaclass = ABCMeta): pass
>>>c = C()
|
위 코드는 정상적으로 실행된다. 그러나 다음과 같이 해서는 안 된다.
>>>from abc import *
>>>class C(metaclass = ABCMeta):
... @abstractmethod
... def absMethod(self):
... pass
>>>c = C()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class C with abstract methods absMethod
|
대신 다음과 같은 코드를 사용하는 것이 좋다.
>>>class B(C):
... def absMethod(self):
... print("Now a concrete method")
>>>b = B()
>>>b.absMethod()
Now a concrete method
|
ABCMeta 클래스는 __instancecheck__
및 __subclasscheck__ 속성을 재정의하며 내장 함수인 isinstance()와
issubclass()를 오버로드하는 데 사용할 수 있다. ABCMeta에서
제공하는 register() 메소드를 사용하여 가상 서브클래스를
ABC에 추가할 수 있다. 다음은 간단한 예제이다.
>>>class TestABC(metaclass=ABCMeta): pass
>>>TestABC.register(list)
>>>TestABC.__instancecheck__([])
True
|
이 예제는 isinstance(list, TestABC)와 동일한
기능을 수행한다. Python 3에서는 __issubclass__ 대신 __instancecheck__를
사용하고, __issubclass__ 대신 __subclasscheck__를
사용한다는 것을 알 수 있으며 버전 3의 표현이 더 자연스럽게 보인다. 예를 들어, superclass.__isinstance__(subclass)에
대한 인수의 역인 isinstance(subclass, superclass)는 혼동을
초래할 수 있는 반면 superclass.__instancecheck__(subclass)
구문이 훨씬 쉽게 알아볼 수 있다.
다음과 같이 collections 모듈 내에서 여러
ABC를 사용하여 클래스에서 특정 인터페이스를 제공하는지 여부를 테스트할 수 있다.
>>>from collections import Iterable
>>>issubclass(list, Iterable)
True
|
표 1에서는 collections 프레임워크의 ABC를 보여 준다.
표 1. collections 프레임워크의 ABC
| ABC | 상속 |
|---|
Container
| |
Hashable
| |
Iterable
| |
Iterator
|
Iterable
|
Sized
| |
Callable
| |
Sequence
| Sized,
Iterable,
Container
|
MutableSequence
|
Sequence
|
Set
| Sized,
Iterable,
Container
|
MutableSet
|
Set
|
Mapping
| Sized,
Iterable,
Container
|
MutableMapping
|
Mapping
|
MappingView
|
Sized
|
KeysView
| MappingView,
Set |
ItemsView
| MappingView,
Set |
ValuesView
|
MappingView
|
 |
Collections
collections 프레임워크는 컨테이너 데이터 유형, deque(double-ended
queue) 및 기본 Dictionary(defaultdict)로 구성되어 있다. deque는
앞이나 뒤에서 추가 및 검색이 가능한 데이터 구조다. Python 3 설명서에 따르면 defaultdict
컨테이너는 내장 Dictionary의 서브클래스로 "메소드 한 개를 재정의하고 쓰기 가능한 인스턴스
변수 한 개를 추가한다". 무엇보다도 이 컨테이너는 Dictionary처럼 작동한다. 또한 collections
프레임워크는 데이터 유형 팩토리 함수인 namedtuple()을 제공한다.
|
|
ABC 유형 계층 구조
Python 3에서는 숫자 클래스를 나타내는 ABC의 유형 계층 구조가 지원된다. 이러한
ABC는 numbers 모듈에 있으며, Number,
Complex, Real,
Rational 및 Integral을
포함한다. 그림 1에서는 숫자 계층 구조를 보여 준다. 이러한 계층 구조를 사용하여 고유한 숫자
유형 또는 다른 숫자 ABC를 구현할 수 있다.
그림 1. 숫자 계층 구조
 |
숫자 타워
Python의 숫자 계층 구조는 Scheme 언어의 숫자 타워에서 영감을 받아 설계되었다.
|
|
새로운 모듈인 fractions은 숫자 ABC인 Rational을
구현한다. 이 모듈은 유리수 계산을 지원한다. dir(fractions.Fraction)을
사용해 보면 imag, real 및 __complex__와
같은 속성이 있다는 것을 알 수 있다. 이는 숫자 타워에 따라 Rationals이
Complex에서 상속받는 Reals에서 상속받기 때문이다.
예외 발생 및 처리하기
Python 3에서는 통사적 모호성 문제를 처리하기 위해 except
절이 변경되었다. Python 버전 2.5에서는 다음과 같은 try . . . except
구문을 사용한다.
>>>try:
... x = float('not a number')
... except (ValueError, NameError):
... print "can't convert type"
|
위 코드는 다음과 같이 잘못 작성될 수 있다.
>>> try:
... x = float('not a number')
... except ValueError, NameError:
... print "can't convert type"
|
위 코드의 문제점은 ValueError가 발생하면
인터프리터가 해당 예외 오브젝트를 NameError라는 이름에
바인딩하기 때문에 ValueError 예외가 처리되지 않는다는
것이다. 다음 예제에서 이를 확인할 수 있다.
>>> try:
... x = float('blah')
... except ValueError, NameError:
... print "NameError is ", NameError
...
NameError is invalid literal for float(): not a number
|
따라서 예외 오브젝트를 다른 이름에 바인딩하려는 경우에는 쉼표(,)를
키워드 as로 대체하여 모호성을 제거해야 한다. 여러 개의
예외를 처리하려면 괄호(())를 사용해야 한다. Listing 5의
코드에서는 Python 3에서 유효한 두 가지 예제를 보여 준다.
Listing 5. Python 3의 예외 처리
# bind ValueError object to local name ex
try:
x = float('blah')
except ValueError as ex:
print("value exception occurred ", ex)
# catch two different exceptions simultaneously
try:
x = float('blah')
except (ValueError, NameError):
print("caught both types of exceptions")
|
예외 처리 방법 중 하나인 묵시적 또는 명시적 예외 체인도
변경되었다. Listing 6에서는 묵시적 예외 체인의 예를 보여 준다.
Listing 6. Python 3의 묵시적 예외 체인
def divide(a, b):
try:
print(a/b)
except Exception as exc:
def log(exc):
fid = open('logfile.txt') # missing 'w'
print(exc, file=fid)
fid.close()
log(exc)
divide(1,0)
|
divide() 메소드에서 0으로 나누어지면서 ZeroDivisionError 예외가 발생한다. 그리고 예외 절의 중첩된
log() 메소드 내에서 print(exc, file=fid)가
파일에 쓰기를 시도하지만 해당 파일이 쓰기를 위해 열려 있지 않다. 이 경우 Python 3에서는
Listing 7과 같은 예외가 발생한다.
Listing 7. 묵시적으로 연결된 예외에 대한 추적 기록 예제
Traceback (most recent call last):
File "chainExceptionExample1.py", line 3, in divide
print(a/b)
ZeroDivisionError: int division or modulo by zero
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "chainExceptionExample1.py", line 12, in <module>
divide(1,0)
File "chainExceptionExample1.py", line 10, in divide
log(exc)
File "chainExceptionExample1.py", line 7, in log
print(exc, file=fid)
File "/opt/python3.0/lib/python3.0/io.py", line 1492, in write
self.buffer.write(b)
File "/opt/python3.0/lib/python3.0/io.py", line 696, in write
self._unsupported("write")
File "/opt/python3.0/lib/python3.0/io.py", line 322, in _unsupported
(self.__class__.__name__, name))
io.UnsupportedOperation: BufferedReader.write() not supported
|
두 예외가 모두 처리되었다는 것을 알 수 있다. 이전 버전의 Python에서는
ZeroDivisionError가 유실되었다. 그렇다면 어떻게 해서 두 예외가
모두 처리된 것일까? 이제부터는 모든 예외 오브젝트에 ZeroDivisionError과
같은 __context__ 속성이 있다. 이 경우에는 발생한 IOError의
__context__ 속성에 ZeroDivisionError가 "유지된다".
__context__ 외에도 예외 오브젝트에는 항상 None으로
초기화되는 __cause__ 속성도 있다. 이 속성은 예외의 원인을
명시적으로 기록하기 위해 사용된다. __cause__ 속성은 다음과
같은 구문으로 설정된다.
>>> raise EXCEPTION from CAUSE
|
이 구문은 다음과 같은 코드와 동일하다.
>>>exception = EXCEPTION
>>>exception.__cause__ = CAUSE
>>>raise exception
|
그러나 첫 번째 코드가 훨씬 보기 좋다. Listing 8은 명시적 예외 체인을 보여 주는 예제이다.
Listing 8. Python 3의 명시적 예외 체인
class CustomError(Exception):
pass
try:
fid = open("aFile.txt") # missing 'w' again
print("blah blah blah", file=fid)
except IOError as exc:
raise CustomError('something went wrong') from exc
|
이전 예제에서는 파일이 쓰기를 위해 열려 있지 않았기 때문에 print()
함수에서 예외가 발생한다. Listing 9에서는 추적 기록을 보여 준다.
Listing 9. 예외 추적 기록
Traceback (most recent call last):
File "chainExceptionExample2.py", line 5, in <module>
fid = open("aFile.txt")
File "/opt/python3.0/lib/python3.0/io.py", line 278, in __new__
return open(*args, **kwargs)
File "/opt/python3.0/lib/python3.0/io.py", line 222, in open
closefd)
File "/opt/python3.0/lib/python3.0/io.py", line 615, in __init__
_fileio._FileIO.__init__(self, name, mode, closefd)
IOError: [Errno 2] No such file or directory: 'aFile.txt'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "chainExceptionExample2.py", line 8, in <modulei>
raise CustomError('something went wrong') from exc
__main__.CustomError: something went wrong
|
추적 기록을 보면 "The above exception was the direct cause of the following
exception" 행 뒤에 CustomError, "something went wrong"을 발생시킨
다른 추적 기록이 있다는 것을 알 수 있다.
마지막으로, 예외 오브젝트에 추가된 또 다른 속성은 __traceback__
속성이다. 발생한 예외에 __traceback__ 속성이 없으면 새로운 추적
기록이 설정된다. 다음은 간단한 예제이다.
from traceback import format_tb
try:
1/0
except Exception as exc:
print(format_tb(exc.__traceback__)[0])
|
format_tb는 하나의 예외만 있는 목록을 리턴한다.
 |
with 명령문
Python 버전 2.6부터는 with가 키워드이므로
더 이상 __future__를 통해 이 기능을 가져오지 않아도 된다.
|
|
정수 리터럴 지원 및 구문
Python에서는 8진, 10진 및 16진와 같은 다양한 기수를 사용하는 정수의 문자열
리터럴이 지원되며 이제는 2진도 추가되었다. 8진 리터럴의 표현이 변경되었다. 이제부터 8진
리터럴은 0o 또는 0O(즉, 0 뒤에 대문자
또는 소문자 o가 있는) 접두부와 평가될 리터럴로 표현된다. 예를 들어, 8진수
13 또는 10진수 11은 다음과 같이 표현된다.
새로운 2진 리터럴 표현에는 0b 또는 0B(즉,
0 뒤에 대문자 또는 소문자 b가 있는) 접두부가 추가된다. 10진수 21은 다음과 같은 2진수로 표현된다.
oct() 및 hex() 메소드가 제거되었다.
함수 주석
함수 주석은 컴파일 시간에 매개변수와 같은 함수의 부분을 표현식에
연결한다. 함수 주석 자체는 아무런 의미도 가지고 있지 않다. 즉, 써드파티 라이브러리에서
처리하지 않는 한 함수 주석은 처리되지 않는다. 함수 주석의 목적은 함수의 매개변수나
리턴값에 대한 주석 처리 방법을 표준화하는 것이다. 함수 주석의 구문은 다음과 같다.
def methodName(param1: expression1, ..., paramN: expressionN)->ExpressionForReturnType:
...
|
다음 예제에서는 함수의 매개변수에 대한 주석을 보여 준다.
def execute(program:"name of program to be executed", error:"if something goes wrong"):
...
|
다음 예제에서는 함수의 리턴값에 대한 주석을 달았다. 이렇게 하면 함수의
리턴 유형을 쉽게 확인할 수 있다.
def getName() -> "isString":
...
|
함수 주석에 대한 전체 문법은 PEP 3107(참고자료에서 제공하는 링크 참조)에서 볼 수 있다.
결론
 |
Easter egg
필자는 antigravity를 Python 3에 추가된 최고의 모듈이라고 생각한다. Python을
시작한 후 명령행에 import antigravity를 입력해 보면 절대 실망하지 않을 것이다.
|
|
Python 3의 최종 릴리스는 2008년 12월에 발표되었다. 그때 이후로 필자는 몇몇
블로그를 둘러보면서 사람들이 이전 버전과의 비호환성 문제에 어떻게 대응하고 있는지 살펴볼 수
있었다. 필자가 살펴본 블로그에서는 공식적인 의견의 일치를 기대하기도 어려웠을 뿐만 아니라
상반된 의견이 극명하게 갈려 있었다. Linux® 개발 커뮤니티의 일부 회원들은 실제로 이식해야
하는 코드의 양이 너무 많아서 버전 3으로 이주하지 않을 것으로 보인다. 그에 반해 많은 웹
개발자들은 변경된 유니코드 지원이라는 한 가지 장점 때문에라도 이주하게 될 것이다.
새 버전으로 이주하지 않을거라면 그 전에 PEP와 개발 메일링 목록을 충분히
살펴본 후 새 버전으로의 이식 여부를 결정해도 늦지 않을 것이다. PEP에서는 구현에 대한
설명뿐 아니라 변경하게 된 합리적인 이유와 그로 인해 얻게 되는 장점에 대한 설명도 볼
수 있다. 이러한 변경 사항은 심도 깊은 논의와 검토를 거쳐서 이루어진 것이다. 이 연재
기사에서는 일반 Python 프로그래머가 모든 PEP를 살펴보지 않고도 변경된 사항을 빠르게
파악할 수 있도록 도와주기 위해 중요한 주제만을 선정해서 다루었다.
참고자료 교육
제품 및 기술 얻기
필자소개  | 
|  | Cesar Otero는 프리랜서로 활동하는 Java 및 Python 컨설턴트이다. 전자 공학을 전공했으며 수학을 부전공으로 이수했다. |
기사에 대한 평가
 |
| 이 문서 북마킹 하기
|
|  |