파이썬 마을에 올라온 질문을 읽다가
답변을 하려고보니, 파이썬을 처음 접하는 분들이 빈번하게 궁금해 하시는 문제인 것 같아서,
좀 더 깊게 후벼파서 글로 만들어 봅니다.
질문의 요지는 아래 소스에서 a~f까지 딕셔너리를 임의로 섞고 싶은데 목록이 많을 수도 있고
자주 바뀌기도 하니까 좀 더 좋은 방법이 없을까 하는 것입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
import random
a = {‘aa’: 1}
b = {‘bb’: 2}
c = {‘cc’: 3}
d = {‘dd’: 4}
e = {‘ee’: 5}
f = {‘ff’: 6}
this = [a, b, c, d, e, f]
random.shuffle(this)
print this
|
이거랑 완전히 똑같은 상황이 자주 일어나지는 않겠지만, 그래도 종종 처음엔 네임스페이스에
변수를 왕창 넣어놓고 이걸 어떻게 해보고 싶은 경우가 있긴 하니까 그래도 파이썬 프로그램들이
자주 사용하는 패턴들로 해결해 보겠습니다.
[1] 정면승부! 네임스페이스 직접 접근해서 거시기하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
import random
import inspect
def shufflelocaldicts():
outerframe = inspect.getouterframes(inspect.currentframe())[1][0]
names, objs = [], []
for name, obj in outerframe.f_locals.iteritems():
if isinstance(obj, dict):
names.append(name)
objs.append(obj)
random.shuffle(objs)
for name, obj in zip(names, objs):
outerframe.f_locals[name] = obj
a = {‘aa’: 1}
b = {‘bb’: 2}
c = {‘cc’: 3}
print a, b, c
shufflelocaldicts()
print a, b, c
shufflelocaldicts()
print a, b, c
|
첫 번째로는 원래 프로그램 자체는 전혀 안 건드리고 그냥 원하는 작업만 하는 방법입니다.
(변수 개수는 소스코드 길이를 줄이려고 3개로 줄였습니다.)
shufflelocaldicts()를 호출하면 바깥 프레임에 접근해서 바깥 네임스페이스의 딕셔너리를 모두
가져다가 섞은 다음에 다시 넣어줍니다.
이건 고칠 부분이 줄기는 하지만, 모든 하이테크 방법들이 그렇듯
깨지기도 쉽고 사용하는 사람들이 제대로 알지 않으면 엉뚱하게 흘러갈 수도 있는 잠재적 위험성이 있습니다.
[2] 구차하더라도 열심히 고치기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
import random
shufflers = []
_ = shufflers.append
a = {‘aa’: 1}; _(‘a’)
b = {‘bb’: 2}; _(‘b’)
c = {‘cc’: 3}; _(‘c’)
def shuffledicts():
shuffled = shufflers[:]
random.shuffle(shuffled)
gdict = globals()
values = [gdict[k] for k in shufflers]
for newname, obj in zip(shuffled, values):
gdict[newname] = obj
print a, b, c
shuffledicts()
print a, b, c
shuffledicts()
print a, b, c
|
두 번째 방법으로 바깥 네임스페이스를 검색하는 흑마법을 쓰지 않고 일일이 등록해주는 것이
있습니다. 이 방법은 일일이 등록해야한다는 것이 약간 중복의 냄새도 있긴 하지만, 단순하고
잘 작동하고 명시적이라서 잘 보인다는 점에서 상당히 자주 사용되는 패턴입니다. 그렇지만 역시
이 경우에는 그다지 썩 좋지는 않네요.
[3] 좀 머리를 굴려보자
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
import random
import inspect
a = {‘aa’: 1} # !SHUFFLE!
b = {‘bb’: 2} # !SHUFFLE!
c = {‘cc’: 3} # !SHUFFLE!
shufflers = [] # !SHUFFLELIST!
def generate():
import re, StringIO
pat_entry = re.compile(r‘^([A-Za-z0-9_]+).*#.*!SHUFFLE!$’)
pat_slist = re.compile(r‘^([^[]+)\[[^]]*\](.*#.*!SHUFFLELIST!)$’)
srcout = StringIO.StringIO()
names = []
for line in open(__file__):
entry = pat_entry.match(line)
if entry:
names.append(entry.groups()[0])
slist = pat_slist.match(line)
if slist:
head, tail = slist.groups()
print >> srcout, ‘%s[%s]%s’ % (head, ‘, ‘.join(map(repr, names)), tail)
else:
srcout.write(line)
open(__file__, ‘w’).write(srcout.getvalue())
def shuffledicts():
shuffled = shufflers[:]
random.shuffle(shuffled)
gdict = globals()
values = [gdict[k] for k in shufflers]
for newname, obj in zip(shuffled, values):
gdict[newname] = obj
def run():
print a, b, c
shuffledicts()
print a, b, c
shuffledicts()
print a, b, c
if __name__ == ‘__main__’:
print “Regenerating the source code..”
generate()
|
이번에는 노장 프로그래머들이 동적 언어들이 그렇게 활발하기 전에 예전에 자주 사용했던 방법으로,
태그를 보고 코드를 자동생성하는 방법입니다. 요새도 전혀 사용되지 않는 것은 아닌데, 프로그램이
변경될 때마다 스크립트를 한 번씩 실행해 주면 자동으로 소스의 주석을 보고 [] 사이의 빈칸을 채워줍니다.
이 기법은 파이썬 프로그램보다는 C프로그램에서 보통 유용하게 쓰이는데요,
cog같은 툴들을 쓰면 좀 더 간편하게 할 수 있습니다. 이건 뭐 대체로 잘 작동하기는 하지만, 어쩌다
소스코드 업데이트가 빠지면 재앙이 생기기도 하고, 그다지 멋지지 않다는게 좀 흠입니다. 주석에 의존한다는게
특히 좀 찝찝하죠. -o-
[4] 객체지향 좀 배운 사람인 척 티내보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
import random
class ListItemProxy(object):
def __init__(self, obj, idx):
self.__obj = obj
self.__idx = idx
def __getattr__(self, name):
if name.startswith(‘__’):
return self.__dict__[name]
else:
return getattr(self.__getobj__(), name)
def __repr__(self):
return repr(self.__getobj__())
def __getobj__(self):
return self.__obj[self.__idx]
def __setitem__(self, name, value):
return self.__getobj__().__setitem__(name, value)
def __getitem__(self, name):
return self.__getobj__().__getitem__(name)
def __detitem__(self, name):
return self.__getobj__().__detitem__(name)
deck = [{‘aa’: 1}, {‘bb’: 2}, {‘cc’: 3}]
a = ListItemProxy(deck, 0)
b = ListItemProxy(deck, 1)
c = ListItemProxy(deck, 2)
print a, b, c
random.shuffle(deck)
print a, b, c
random.shuffle(deck)
print a, b, c
|
앞에서 썼던 방법들은 직관적이고 단순하기는 해도
역시 너무 무식한 방법같은 냄새가 납니다.
이번엔 파이썬에서 객체 꽁수를 배우고 한참 기분낼 때 아주 재미있게 하는 객체 오버라이딩으로
위임(proxy) 객체 만들어주기입니다. 대형 프로젝트에서는 이런 스타일을 종종 사용하기도 하지만,
짧은 스크립트에서 쓰면 좀 격에 안 맞기는 하죠. 직접 딕셔너리를 만들어서 그걸 어쩌고 저쩌고하는
대신에 리스트에 원래 걸 넣어두고 그 첫번째, 두번째~ 등을 간접적으로 작업을 지원해 주도록 하는 방법입니다.
이 방법은 shuffle이 매우 빠른 게 장점이고, 참조가 리스트 하나 안에 모여서 깔끔하게 정리가 돼 있는게
좋습니다. 그런데 문제는 역시 위임이 중간에 거치기 때문에 속도상 불이익이 있고, 위임을 제대로 하려면
위 소스보다 훨씬 길게 짜야 한다는 거겠죠.
[5] 딕셔너리들을 모두 관리하는 오버마인드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
import random
class sdict(dict):
alldicts = []
def __init__(self, *args, **kwds):
dict.__init__(self, *args, **kwds)
self.alldicts.append(self)
@classmethod
def shuffle(klass):
dicts = []
for d in klass.alldicts:
dicts.append({})
dicts[–1].update(d)
d.clear()
random.shuffle(dicts)
for d, o in zip(klass.alldicts, dicts):
d.update(o)
a = sdict({‘aa’: 1})
b = sdict({‘bb’: 2})
c = sdict({‘cc’: 3})
print a, b, c
sdict.shuffle()
print a, b, c
c[‘rr’] = 9
sdict.shuffle()
print a, b, c
|
이번엔 딕셔너리 자체를 상속받아서, 2번에서처럼 따로 등록하는 대신 딕셔너리 초기화 도중에
자동으로 등록되게 하고, 4번처럼 위임을 구질구질하게 하지 않고도 자체가 그냥 딕셔너리가 되게 했습니다.
요것도 어느 정도 장점이 있긴 하지만, 역시 딕셔너리를 만들 때 마다 일일이 캐스팅을 해 줘야하고,
원래 객체의 참조를 건드리려면 역시 이름 없이는 할 수 없기 때문에, 참조를 놔둔채로 내용을 바꾸기 위해서
내용을 싹 비워서 다른데 옮겼다가 다시 부어주는 좀 심각하게 비싼 작업이 들어가는게 단점입니다.
[6] 억지로 구겨넣지 말고 원래 모양을 바꿔보자
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
import random
class ShuffleDeck(object):
def shuffle(self):
names = self.__dict__.keys()
random.shuffle(names)
for k, v in zip(names, self.__dict__.values()):
self.__dict__[k] = v
deck = ShuffleDeck()
deck.a = {‘aa’: 1}
deck.b = {‘bb’: 2}
deck.c = {‘cc’: 3}
print deck.a, deck.b, deck.c
deck.shuffle()
print deck.a, deck.b, deck.c
deck.shuffle()
print deck.a, deck.b, deck.c
|
앞에서 썼던 여러 구질구질한 방법들은 결국 네임스페이스에 흩어져 있는 것들을 어떻게든
관리를 하려다보니 생기는 문제입니다. 그냥 네임스페이스에 흩어지지 않게 따로 모아두면
결국 그렇게 고생 안 해도 간단하게 해결됩니다. 굳이 __dict__를 쓰지 않더라도 그냥 리스트에
번호붙여서 넣어서 쓰거나, 다양한 깔끔한 방법이 네임스페이스에 흩어놓는 것만 포기하면
마구 생겨납니다.
결국은!
이 경우에 우선 문제를 해결하려다보면 앞에서 소개해 드렸던 다양한 방법으로 억지로 해결할 수도 있습니다.
다 만들어 놓은 프로그램을 막판에 약간 어떻게든 고쳐서 출시를 해야하는 경우나, 당장 여자친구가
얼른 안 오면 삐진다고 문자를 수십통을 보내고 있는데 과장님은 얼른 고쳐내라고 옆에서 1분마다 보채고 있을 때
뭐 이런 극한 상황에서나 얼른 쓰고 도망갈 때 쓸만한 방법이겠죠;
역시 이 문제의 경우에는 질문 자체의 요구사항을
파이썬에서 쉽게 해결할 수 있는 방법으로 바꾸는 게 중요합니다. 딕셔너리 여러개를 마구 섞는 문제라고 최종 문제를
보면 상상력의 한계가 있지만, 그것보다 원초적으로 다뤄야하는 문제를 보면 딕셔너리를 여러개 마구 섞는게
아니라 간단하게 해결할 수 있는 다른 자료구조가 반드시 있을 것 같은 예감이 강렬하게 드는군요. -O-; (그리고 마지막으로 하나 덧붙이고 싶은 것은, 생각보다 원래 질문의 코드도 보기에는 좀 그렇더라도 복잡한 방법을 안 썼다는 점에서 그런대로 쓸만한 코드입니다. 물론 문제 자체를 바꾸는게 더 좋을 것 같긴 하지만요;)