CODICT

3. Python의 코드 구조 본문

Programming/Python

3. Python의 코드 구조

Foxism 2019. 12. 2. 19:07

지난 게시글에서 많은 데이터 타입과 자료구조에 대한 코드를 살펴봤지만, 코드가 그렇기 길지 않았습니다. 대부분의 코드는 대화식 인터프리터를 사용하는 짧은 코드였습니다. 이제부터는 데이터가 아닌 파이썬 코드를 어떻게 구조화하는지 살펴보겠습니다.

수많은 컴퓨터 언어는 코드의 시작 부분과 끝 부분을 표시하기 위해 키워드 혹은 중괄호와 같은 문자를 사용합니다. 그리고 코드 작성자와 다른 사람이 코드를 좀 더 읽기 쉽도록 일관된 들여 쓰기를 하는 것은 좋은 습관입니다. 심지어 개발 도구에는 코드의 라인을 잘 정리해주는 기능도 있습니다.

파이썬이 설계될 때, 설계자인 귀도 반 로섬은 코드 구분을 위해 괄호를 입력하지 않더라도 들여 쓰기로 프로그램의 구조를 정의하는 것이 충분하다고 생각했습니다. 파이썬은 프로그램의 구조를 정의하기 위해 공백을 사용하는 흔치 않은 언어입니다. 다른 언어를 사용해본 사람들이 파이썬을 처음 접하게 될 때, 이것이 이상하게 느껴질 수도 있습니다. 하지만 파이썬 코드를 작성하다 보면 들여 쓰기가 자연스럽게 느껴집니다. 코드를 적게 입력하고, 그 시간에 더 많은 일을 하는 데 익숙해져 보세요!


1. 코멘트 달기: #

 

프로그램에서 코멘트(Comment)는 인터프리터에 의해 무시되는 텍스트의 한 부분입니다. 코드를 설명하거나 나중에 어떤 문제를 고치기 위해 표시를 하는 등 다양한 목적으로 코멘트를 사용할 수 있습니다. # 문자를 이용해서 코멘트를 표시하는데, # 문자가 시작된 곳부터 그 라인의 마지막까지가 코멘트입니다. 다음과 같이 한 라인 전체에 코멘트를 달 수 있습니다.

>>> #60초가 1분입니다.
>>> minute = second // 60

물론 코드 끝에도 달 수 있습니다.

>>> minute = second // 60 #60초가 1분 입니다.

# 문자는 해시(Hash), 샤프(Sharp), 파운드(Pound), 옥토쏘르프(Octothorpe)라는 많은 이름이 있습니다. 어떤 이름을 부르든지 간에 코멘트는 # 문자부터 그 라인의 끝까지 효력을 발휘합니다. 파이썬에는 여러 라인을 처리하는 주석이 없습니다. 명시적으로 각 행이나 부분에 #을 붙여야 합니다. 참고로, # 문자가 문자열 안에 있다면, 코멘트가 아닌 평범한 문자로 돌아가게 됩니다.

다른 사이트에서는 #이 아닌 방법(예를 들면 따옴표 3개를 연달아 사용하는 방법)으로 코멘트를 달 수 있다고 합니다. 그러나 이 방법은 변수에 할당되지 않은 객체를 만드는 것이지, 기능만 주석의 역할을 할 뿐 주석은 아닙니다.

 

2. 라인 유지하기: \

 

라인이 적당하게 짧다면 프로그램을 더 쉽게 읽을 수 있습니다. 한 라인에서 권장하는 최대 문자수는 80자입니다. 이 길이 안에 넣고 싶은 코드를 모두 입력할 수 없다면 백 슬래시 문자를 입력한 후 다음 라인에 계속 입력합니다. 라인의 끝에 백 슬래시를 입력하면, 파이썬은 다음 라인을 여전히 같은 라인으로 인식합니다.

예를 들어 작은 문자열을 긴 문자열로 만들고 싶을 때 여러 단계에 걸쳐 문자열을 붙일 수 있습니다.

>>> apb = ''
>>> apb += 'abcd'
>>> apb += 'efgh'

혹은 \문자를 사용해서 한 번의 과정으로 문자열을 붙입니다.

>>> apb = 'abcd' + \
      'efgh'

파이썬 표현식이 여러 줄에 걸쳐 있는 경우에도 라인 유지가 필요합니다.

>>> 1 + 2 +
SyntaxError: invalid syntax
>>> 1 + 2+ \
  3
#6

 

3. 비교하기: if, elif, else

 

지금까지는 거의 대부분 데이터 타입과 자료구조에 대해 이야기했습니다. 이제부터는 프로그램의 데이터를 다루어서 코드 구조를 만드는 과정의 첫 단계가 시작됩니다. 첫 번째 코드는 부울 변수 clear의 값을 확인하고, 적절한 문자열을 출력하는 작은 프로그램으로 시작하겠습니다.

>>> clear = True
>>> if clear:
	print("pass")
    else:
	print("cleaning")

	
#pass

if와 else는 조건(여기서는 clear  변수의 값)이 참(True)인지 거짓(False)인지 확인하는 파이썬의 선언문(Statement)입니다. print()는 일반적으로 화면에 객체를 출력하는 파이썬의 내장 함수(Built-in Function)입니다.

참고로, 다른 프로그래밍 언어와는 다르게, 파이썬의 if 조건 테스트에서는 괄호가 필요 없습니다. 대신 콜론(:)을 사용해야 하는데, 콜론을 입력하지 않으면 에러 메시지가 출력됩니다.

각 print() 라인은 조건 테스트 아래에서 들여 쓰기가 되어 있습니다. 각 하위 부분에서 스페이스를 4칸씩 사용했습니다. 들여 쓰기는 같은 칸수로 왼쪽에서 들여 써야 합니다. pep-8이라 부르는 파이썬 코드 권장 스타일에서는 들여 쓰기를 4칸의 스페이스를 사용하는 것으로 명시되어 있습니다. 탭 혹은 탭과 스페이스를 혼합해서 들여 쓰기를 하면 탭 크기에 따라 들여 쓰기 공백의 수가 흐트러지기 때문에 되도록이면 혼용하지 마세요.

앞의 코드에서 수행했던 몇 가지 일을 아래에 나열했습니다. 더 자세한 사항은 진행하면서 설명하겠습니다.

  • 부울 변수 clear에 True를 할당했습니다.

  • if와 else를 사용해서 조건 테스트를 수행했습니다. clear의 값에 따라 다른 코드가 실행됩니다.

  • 텍스트를 출력하기 위해 print() 함수를 호출했습니다.

여러 단계에 걸쳐 조건 테스트 안에서 조건 테스트를 할 수 있습니다.

>>> furry = True
>>> small = True
>>> if furry:
	if small:
		print("It's a cat")
	else:
		print("It's a bear")
    else:
	if small:
		print("It's a skink")
	else:
		print("It's a human")

		
#It's a cat

파이썬에서는 들여 쓰기를 보면 if와 else 부분이 어떻게 짝을 이루는지 알 수 있습니다. 첫 번째 조건 테스트를 furry를 확인합니다. furry가 True이므로 들여 쓰기 된 small을 조건 테스트합니다. 그리고 또한 small에 True를 할당했기 때문에 다음 라인을 실행하여 It's a cat을 출력합니다. 두 개 이상의 조건 테스트가 있다면 if, elif(else if를 의미), else를 사용합니다.

>>> color = "puce"
>>> if color == "red":
	print("It's red")
    elif color == "green":
	print("It's green")
    elif color == "puce":
	print("It's puce")
    else:
	print("IDK")

	
#It's puce

위 예제에서는 ==연산자로 같은지 테스트했습니다. 파이썬의 비교 연산자는 다음과 같습니다.

비교 연산자

의미

==

같다

!=

다르다

<

보다 작다(미만)

<=

보다 작거나 같다(이하)

>

보다 크다(초과)

>=

보다 크거나 같다(이상)

in

멤버십

비교 연산자는 부울 값 True 혹은  False를 반환합니다. 비교 연산자를 사용해 보겠습니다.

>>> x = 7
>>> x == 5
#False
>>> x == 7
#True
>>> 5 < x
#True
>>> x < 10
#True

두 값이 같은지 확인하려면 ==기호를 사용합니다. 변수에 값을 할당할 때에는 등호를 사용합니다. 이 두 기호를 혼동하지 않게 주의하세요.

만약 동시에 여러 개의 식을 비교해야 한다면 최종 부울 값을 판단하기 위해 and, or, not과 같은 부울 연산자를 사용할 수 있습니다.

부울 연산자는 비교 연산자보다 우선순위가 낮습니다. 이 말은 비교 연산을 먼저 실행한 후 부울 연산을 실행한다는 것을 의미합니다. 다음 코드를 보면, x에 7을 할당했고, 5 < x는 True고, x < 10 또한 True이기 때문에 마지막 True and True를 계산하여 True가 반환되는 것입니다.

>>> x = 7
>>> 5 < x and x < 10
#True

파이썬에서는 하나의 변수를 다음과 같이 여러 번 비교하는 것을 허용합니다.

>>> 5 < x < 10
#True

 

3.1. True와 False

 

확인할 요소가 부울형이 아니라면 True와 False를 어떻게 구분할까요? False값은 명시적으로 False라고 할 필요가 없습니다. 예를 들어 다음은 모두 False로 간주됩니다.

요소

False 표기법

null

None

정수 0

0

부동소수점수 0

0.0

빈 문자열

''

빈 리스트

[]

빈 튜플

()

빈 딕셔너리

{}

빈 셋

set()

이 외의 다른 모든 것들은 전부 True로 간주됩니다. 파이썬 프로그램은 데이터 자료구조가 False 조건인지 확인하기 위해 진실(Truthiness) 혹은 거짓(Falsiness)의 정의를 사용합니다.

>>> lt = []
>>> if lt:
	print("There is something in here")
    else:
	print("It's empty")

	
#It's empty

변수가 아닌 표현식을 테스트한다면, 파이썬은 표현식을 계산하고 부울 결과를 반환합니다. 만약 다음과 같이 코드를 입력하면 파이썬은 color == "red"를 계산합니다. color 변수에 문자열 "puce"를 할당했으므로 color == "red"는 False가 되는 것입니다.

>>> color = "puce"
>>> color == "red"
#False

 

4. 반복하기: while

 

if, elif, else는 위에서부터 아래로 테스트를 실행합니다. 한 번 이상 뭔가 실행하려 할 때에는 루프(Loop)가 필요합니다. 파이썬에서 가장 간단한 반복문은 while문입니다. 숫자 1에서 5까지 실행하는 간단한 루프 예제를 살펴보겠습니다.

>>> count = 1
>>> while count <= 5:
	print(count)
	count += 1

	
#1
#2
#3
#4
#5

먼저 count 변수에 1을 할당했습니다. while문은 count의 값이 5보다 작거나 같은지 계속 비교합니다. 루프 문 안에서는 count 변수의 값을 출력하고 count += 1 문장에 의해 count 변수의 값이 1 증가합니다. 파이썬은 루프 문의 맨 위로 돌아가서 다시 비교를 시작합니다. 이제 count 변수의 값은 2입니다. 또다시 while문의 내용을 실행하고 count 변수의 값은 3이 됩니다.

반복문은 count 변수가 5에서 6으로 증가할 때까지 계속 수행됩니다. 다음 반복에서 count <= 5는 False이므로 while문이 끝납니다. 그리고 다음 라인으로 이동합니다.

 

4.1. 중단하기: break

 

어떤 일이 일어날 때까지 반복하고 싶지만, 어떤 일이 언제 일어날지 확실하지 않다면, 무한루프 속에 break 문을 사용합니다. 이번 예제는 input() 함수를 사용하여 키보드에서 한 라인을 읽은 후 첫 번째 문자를 대문자로 출력합니다. 그리고 문자 q를 입력하면 반복문을 종료합니다.

>>> while True:
	stuff = input("String to capitalize [type q to quit]: ")
	if stuff == 'q':
		break
	print(stuff.capitalize())

	
#String to capitalize [type q to quit]: test
#Test
#String to capitalize [type q to quit]: hey, it works
#Hey, it works
#String to capitalize [type q to quit]: q

 

4.2. 건너뛰기: continue

 

반복문을 중단하고 싶지는 않지만 몇몇 이유로 다음 루프로 건너뛰고 싶은 때가 있습니다. 다음 코드는 정수가 홀수일 때에는 그 수의 제곱을 출력하고, 짝수일 때에는 다음 루프로 건너뛰는 코드입니다. 이전과 같이 q를 입력하면 반복문을 종료합니다.

>>> while True:
    value = input("Integer, please [q to quit]: ")
    if value == 'q':
        break
    number = int(value)
    if number % 2 == 0:
        continue
    print(number, "squared is", number ** 2)

	
#Integer, please [q to quit]: 1
#1 squared is 1
#Integer, please [q to quit]: 2
#Integer, please [q to quit]: 3
#3 squared is 9
#Integer, please [q to quit]: 4
#Integer, please [q to quit]: 5
#5 squared is 25
#Integer, please [q to quit]: q

 

4.3. break 확인하기: else

 

break는 어떤 것을 체크하여 그것을 발견했을 경우 종료하는 while 문을 작성할 때 사용합니다. while 문이 모두 실행되었지만 발견하지 못했을 경우에는 else가 실행됩니다.

>>> numbers = [1, 3, 5]
>>> position = 0
>>> while position < len(numbers):
	number = numbers[position]
	if number % 2 == 0:
		print('Found even number', number)
		break
	position += 1
    else:
	print('No even number found')

	
#No even number found

참고로, else의 사용이 비 직관적인 것처럼 보일 수도 있습니다. 이 경우에는 그냥 else를 break를 체크하는 역할이라고 생각하시면 편합니다.

 

5. 순회하기: for

 

파이썬에서 이터레이터(Iterator)는 자주 유용하게 쓰입니다. 자료구조가 얼마나 큰지, 어떻게 구현되었는지에 관계없이 자료구조를 순회할 수 있도록 해줍니다. 심지어 바로 생성되는 데이터도 순회할 수 있습니다. 데이터가 메모리에 맞지 않더라도 데이터 스트림을 처리할 수 있도록 허용해줍니다.

다음과 같이 파이썬 문법에 맞게 시퀀스를 순회할 수 있습니다.

>>> rabbits = ['Flopsy', 'Mopsy', 'Cottontail', 'Peter']
>>> current = 0
>>> while current < len(rabbits):
	print(rabbits[current])
	current += 1

	
#Flopsy
#Mopsy
#Cottontail
#Peter

그러나 파이써닉한 우아한 방법이 있습니다.

>>> rabbits = ['Flopsy', 'Mopsy', 'Cottontail', 'Peter']
>>> for rabbit in rabbits:
	print(rabbit)

	
#Flopsy
#Mopsy
#Cottontail
#Peter

rabbits 리스트는 문자열, 튜플, 딕셔너리, 셋 등과 같이 순회 가능한(Iterable) 객체입니다. 튜플이나 리스트는 한 번에 한 항목을 순회합니다. 문자열은 다음과 같이 한 번에 한 문자를 순회합니다.

>>> word = 'cat'
>>> for letter in word:
	print(letter)

	
#c
#a
#t

딕셔너리(혹은 딕셔너리의 keys() 함수)의 순회는 키를 반환합니다. 다음의 코드를 보시죠.

>>> money = {'james': 30000, 'tim': 2000, 'alice': 10000}
>>> for person in money:
	print(person)

	
#james
#tim
#alice

키보다 값을 순회하려면 딕셔너리의 values() 함수를 사용하면 됩니다.

>>> for value in money.values():
	print(value)

	
#30000
#2000
#10000

튜플의 형식으로 딕셔너리의 키와 값을 모두 반환하기 위해서는 items() 함수를 사용합니다.

>>> for item in money.items():
	print(item)

	
#('james', 30000)
#('tim', 2000)
#('alice', 10000)

한 번에 튜플 하나씩 할당할 수 있습니다. 각 튜플은 items()에 의해 반환됩니다. 다음의 코드는 튜플의 첫 번째 내용(키)은 person에, 두 번째 내용(값)은 value에 할당하여 출력합니다.

>>> for person, value in money.items():
	print('Name  =', person, 'and money =', value)

	
#Name  = james and money = 30000
#Name  = tim and money = 2000
#Name  = alice and money = 10000

 

5.1. 중단하기: break

 

for 문의 break는 while 문의 break와 똑같이 작동합니다.

 

5.2. 건너뛰기: continue

 

for 문의 continue는 while 문의 continue와 똑같이 작동합니다.

 

5.3. break 확인하기: else

 

while 문과 같이, for 문에서도 모든 항목을 순회했는지 확인하는 부가적인 옵션의 else 문이 있습니다. for 문에서 break  문이 호출되지 않으면 else 문이 실행됩니다. 즉 else 문은 break 문에 의해 반복문이 중단되었는지 확인합니다. 다음 코드에서 for 문은 치즈의 이름을 출력하고, 치즈 가게에 없는 치즈가 발견되면 반복을 중단합니다.

>>> cheeses = []
>>> for cheese in cheeses:
	print('This shop has some lovely', cheese)
	break
    else:
	print('This is not much of a cheese shop, is it?')

	
#This is not much of a cheese shop, is it?

참고로, while 문과 마찬가지로 for 문의 else도 비 직관적으로 보일 수 있습니다. for 문을 뭔가를 찾는 것으로 생각하고, 찾지 못했을 때에는 else 문이 호출된다고 생각하면 좀 더 이해하기 쉬울 것입니다. else 문 없이 이와 같은 효과를 얻으려면 다음 예제와 같이 원하는 값을 찾았는지 여부를 나타내는 변수를 사용하면 됩니다.

>>> cheeses = []
>>> found_one = False
>>> for cheese in cheeses:
	found_one = True
	print('This shop has some lovely', cheese)
	break

>>> if not found_one:
	print('This is not much of a cheese shop, is it?')

	
#This is not much of a cheese shop, is it?

 

5.4. 여럿 시퀀스 순회하기: zip()

 

좋은 순회(Iteration)의 방법이 하나 더 있습니다. zip() 함수를 사용해서 여러 시퀀스를 병렬로 순회하는 것입니다.

>>> days = ['Monday', 'Tuesday', 'Wednesday']
>>> fruits = ['banana', 'orange', 'peach']
>>> drinks = ['coffee', 'tea', 'beer']
>>> desserts = ['tiramisu', 'ice cream', 'pie', 'pudding']
>>> for day, fruit, drink, dessert in zip(days, fruits, drinks, desserts):
	print(day, ": drink", drink, "- eat", fruit, "- enjoy", dessert)

	
#Monday : drink coffee - eat banana - enjoy tiramisu
#Tuesday : drink tea - eat orange - enjoy ice cream
#Wednesday : drink beer - eat peach - enjoy pie

여러 시퀀스 중 가장 짧은 시퀀스가 완료되면 zip()은 멈춥니다. 위 예제에서는 리스트 중 하나(desserts)가 다른 리스트보다 깁니다. 그래서 다른 리스트를 모두 확장하지 않는 한 pudding을 얻을 수 없습니다.

이전 게시글에서 딕셔너리에 대해 배울 때, dict() 함수로 튜플, 리스트, 문자열과 같은 두 항목의 시퀀스로부터 딕셔너리를 생성하는 방법을 살펴봤습니다. 또한 zip() 함수로 여러 시퀀스를 순회하며, 동일한 오프셋에 있는 항목으로부터 튜플을 만들 수 있습니다. 영어와 프랑스어 단어에 대응하는 두 개의 튜플을 만들어보겠습니다.

>>> english = 'Monday', 'Tuesday', 'Wednesday'
>>> french = 'Lundi', 'Mardi', 'Mercredi'

두 개의 튜플을 만들기 위해 zip()을 사용합니다. zip()에 의해 반환되는 값은 튜플이나 리스트 자신이 아니라 하나로 반환될 수 있는 순회 가능한 값입니다.

>>> list(zip(english, french))
#[('Monday', 'Lundi'), ('Tuesday', 'Mardi'), ('Wednesday', 'Mercredi')]

zip()의 결과를 dict()에 넣으면, 작은 영어-프랑스어 사전이 생성되었습니다.

>>> dict(zip(english, french))
#{'Monday': 'Lundi', 'Tuesday': 'Mardi', 'Wednesday': 'Mercredi'}

 

5.5. 숫자 시퀀스 생성하기: range()

 

range() 함수는 리스트나 튜플 같은 자료구조를 생성하여 저장하지 않더라도 특정 범위 내에서 숫자 스트림을 반환합니다. 이것은 컴퓨터의 메모리를 전부 사용하지 않고, 프로그램의 충돌 없이 아주 큰 범위를 생성할 수 있게 해 줍니다.

range() 함수는 슬라이스의 사용법과 비슷합니다. range(start, stop, step) 형식을 사용합니다. 만약 start를 생략하면 범위는 0에서 시작합니다. 그리고 stop은 꼭 입력해야 하는 값입니다. 슬라이스와 마찬가지로 범위의 끝은 stop의 바로 직전 값입니다. step의 기본값은 1이다. step을 -1로 지정하여 끝에서부터 거꾸로 진행할 수도 있습니다.

zip(), range()와 같은 함수는 순회 가능한 객체를 반환합니다. 그러므로 for... in 형태로 값을 순회할 수 있습니다. 또한 객체를 리스트와 같은 시퀀스로 변환할 수 있습니다. range()로 0, 1, 2의 숫자 시퀀스를 만들어보겠습니다.

>>> for x in range(0, 3):
	print(x)

	
#0
#1
#2
>>> list(range(0, 3))
#[0, 1, 2]

거꾸로 진행하는 2에서 0의 숫자 시퀀스를 만들어보겠습니다.

>>> for x in range(2, -1, -1):
	print(x)

	
#2
#1
#0
>>> list(range(2, -1, -1))
#[2, 1, 0]

다음 코드는 0에서 10까지 2칸씩 진행하는 짝수 리스트를 만듭니다.

>>> list(range(0, 11, 2))
#[0, 2, 4, 6, 8, 10]

 

5.6. 기타 이터레이터

 

추후에 작성될 게시글에서는 파일에 대한 순회에 대해 서술하고, 직접 정의한 객체를 순회가 가능하도록 설정하는 방법에 대해 서술할 계획입니다.

 

6. 컴프리헨션

 

컴프리헨션(Comprehension; 함축)은 하나 이상의 이터레이터로부터 파이썬의 자료구조를 만드는 콤팩트한 방법입니다. 컴프리헨션은 비교적 간편한 구문으로 반복문과 조건 테스트를 결합할 수 있도록 해줍니다. 때때로 컴프리헨션을 사용하는 것은 초급 이상의 단계에서 파이썬을 어느 정도 알고 있다는 것을 의미합니다. 즉, 더 파이써닉하게 사용한다는 것을 의미합니다.

 

6.1. 리스트 컴프리헨션

 

1부터 5까지의 정수 리스트를 다음과 같이 한 번에 하나씩 만들 수 있습니다.

>>> number_list = []
>>> number_list.append(1)
>>> number_list.append(2)
>>> number_list.append(3)
>>> number_list.append(4)
>>> number_list.append(5)
>>> number_list
#[1, 2, 3, 4, 5]

또한 이터레이터와 range() 함수를 사용하여 만들 수 있습니다.

>>> number_list = []
>>> for number in range(1, 6):
	number_list.append(number)

	
>>> number_list
#[1, 2, 3, 4, 5]

혹은 리스트에 직접 range()를 넣어서 결과를 반환할 수 있습니다.

>>> number_list = list(range(1, 6))
>>> number_list
#[1, 2, 3, 4, 5]

이러한 모든 접근 방식은 유효하고, 동일한 결과를 생성합니다. 그러나 리스트 컴프리헨션을 사용해서 리스트를 만드는 것이 조금 더 파이써닉한 방법입니다.

[표현식 for 항목 in 순회 가능한 객체]

리스트 컴프리헨션으로 정수 리스트를 만들어보겠습니다.

>>> number_list = [number for number in range(1, 6)]
>>> number_list
#[1, 2, 3, 4, 5]

첫 라인을 보면, 목록에 대한 값을 생성하는 첫 번째 number 변수가 필요합니다. 이것은 루프의 결과를 number_list 변수에 넣어줍니다. 두 번째 number 변수는 for 문의 일부입니다. 다음 코드는 첫 번째 number 변수를 보여주기 위해 표현식을 바꿨습니다.

>>> number_list = [number - 1 for number in range(1, 6)]
>>> number_list
#[0, 1, 2, 3, 4]

리스트 컴프리헨션은 대괄호 안에 루프가 있습니다. 이 컴프리헨션 코드는 실제로 이전 코드보다 간단하지 않지만, 더 많은 것을 수행합니다. 리스트 컴프리헨션은 다음과 같이 조건 표현식을 포함할 수 있습니다.

[표현식 for 항목 in 순회 가능한 객체 if 조건]

1과 5 사이에서 홀수 리스트를 만드는 새 컴프리헨션을 만들어보겠습니다(number % 2는 홀수일 때 True고, 짝수일 때 False입니다).

>>> a_list = [number for number in range(1, 6) if number % 2 == 1]
>>> a_list
#[1, 3, 5]

이제 컴프리헨션이 지금까지 사용했던 방식보다 좀 더 콤팩트 해졌습니다.

>>> a_list = []
>>> for number in range(1, 6):
	if number % 2 == 1:
		a_list.append(number)

		
>>> a_list
#[1, 3, 5]

마지막으로 루프가 중첩될 수 있는 것처럼, 컴프리헨션에서 루프에 상응하는 하나 이상의 for 문을 사용할 수 있습니다. 이것을 보기 위해 먼저 일반적인 중첩 루프를 사용해서 결과를 출력해보겠습니다.

>>> rows = range(1, 4)
>>> cols = range(1, 3)
>>> for row in rows:
	for col in cols:
		print(row, col)

		
#1 1
#1 2
#2 1
#2 2
#3 1
#3 2

이제 컴프리헨션을 사용해보겠습니다. (row, col) 튜플 리스트를 만들어서 cells 변수에 할당합니다.

>>> rows = range(1, 4)
>>> cols = range(1, 3)
>>> cells = [(row, col) for row in rows for col in cols]
>>> for cell in cells:
	print(cell)

	
#(1, 1)
#(1, 2)
#(2, 1)
#(2, 2)
#(3, 1)
#(3, 2)

그리고 cells 리스트를 순회한 것처럼, 각 튜플로부터 row와 col의 값만 출력하기 위해 튜플 언패킹(Unpacking)을 할 수 있습니다.

>>> for row, col in cells:
	print(row, col)

	
#1 1
#1 2
#2 1
#2 2
#3 1
#3 2

리스트 컴프리헨션의 for row와 for col 코드에서 자신의 if 테스트를 만들 수 있습니다.

 

6.2. 딕셔너리 컴프리헨션

 

리스트 못지않게 딕셔너리 또한 컴프리헨션이 있습니다. 다음 형식은 왠지 익숙해 보입니다.

{키_표현식 : 값_표현식 for 표현식 in 순회 가능한 객체}

리스트 컴프리헨션과 같이 딕셔너리 컴프리헨션 또한 if 테스트와 다중 for 문을 가질 수 있습니다.

>>> word = 'letters'
>>> letter_counts = {letter: word.count(letter) for letter in word}
>>> letter_counts
#{'l': 1, 'e': 2, 't': 2, 'r': 1, 's': 1}

문자열 'letters'의 각 일곱 글자를 반복해서 글자가 몇 번 나왔는지 그 수를 셉니다. e와 t 모두 두 번식 세기 때문에 두 번의 word.count(letter) 사용은 시간을 낭비합니다. 그러나 두 번째 e를 셀 때는 딕셔너리에 이미 존재하는 항목을 단지 교체만 하기 때문에 아무런 해가 되지 않습니다. t도 마찬가지입니다. 조금 더 파이써닉한 코드를 살펴봅시다.

>>> word = 'letters'
>>> letter_counts = {letter: word.count(letter) for letter in set(word)}
>>> letter_counts
#{'s': 1, 'l': 1, 'e': 2, 'r': 1, 't': 2}

딕셔너리의 키가 이전 코드와는 다르게 정렬되어 있습니다. 왜냐하면 set(word)를 순회하는 것은 문자열 word를 순회하는 것과 다르게 문자를 반환하기 때문입니다.

 

6.3. 셋 컴프리헨션

 

셋 컴프리헨션 역시 존재합니다. 간단한 형식은 리스트나 딕셔너리 컴프리헨션과 비슷한 모양을 하고 있습니다.

{표현식 for 표현식 in 순회 가능한 객체}

if 테스트와 다중 for 절이 있는 긴 형식 또한 셋 컴프리헨션에서 사용할 수 있습니다.

>>> a_set = {number for number in range(1, 6) if number % 3 == 1}
>>> a_set
#{1, 4}

 

6.4. 제너레이터 컴프리헨션

 

튜플은 컴프리헨션이 없습니다. 아마 리스트 컴프리헨션의 대괄호를 괄호로 바꿔서 사용하면 튜플 컴프리헨션이 생성될 것이라고 생각할 것입니다. 그리고 다음과 같이 입력했을 때 예외가 나타나지 않기 때문에 뭔가 잘 동작하는 것처럼 보입니다.

>>> number_thing = {number for number in range(1, 6)}

괄호 안의 내용은 제너레이터 컴프리헨션(Generator Comprehension)입니다. 그리고 이것은 제너레이터 객체를 반환합니다.

>>> number_thing = (number for number in range(1, 6))
>>> type(number_thing)
#<class 'generator'>

제너레이터에 관한 좀 더 자세한 내용은 하단에 작성하겠습니다. 제너레이터는 이터레이터에 데이터를 제공하는 하나의 방법입니다.

다음과 같이 제너레이터 객체를 바로 순회할 수 있습니다.

>>> for number in number_thing:
	print(number)

	
#1
#2
#3
#4
#5

혹은 리스트 컴프리헨션처럼 만들기 위해 제너레이터 컴프리헨션에 list() 호출을 랩핑(Wrapping)할 수 있습니다.

>>> number_thing = (number for number in range(1, 6))
>>> number_list = list(number_thing)
>>> number_list
#[1, 2, 3, 4, 5]

참고로, 제너레이터는 한 번만 실행될 수 있습니다. 리스트, 셋, 문자열, 딕셔너리는 메모리에 존재하지만,  제너레이터는 즉석에서 그 값을 생성하고, 이터레이터를 통해서 한 번에 값을 하나씩 처리합니다. 제너레이터는 이 값을 기억하지 않으므로 다시 시작하거나 제너레이터를 백업할 수 없습니다.

만약 이 제너레이터를 다시 순회하려 하면 아무것도 볼 수 없을 것입니다.

>>> try_again = list(number_thing)
>>> try_again
#[]

여기에서 했던 것처럼 제너레이터 컴프리헨션으로부터 제너레이터를 생성할 수 있습니다. 혹은 제너레이터 함수를 사용해서 생성할 수 있습니다. 먼저 함수를 살펴본 후 제너레이터 함수의 특별한 경우를 살펴보겠습니다.

 

7. 함수

 

지금까지 모든 파이썬 코드는 작은 코드 조각이었습니다. 이 코드 조각은 작은 일을 처리하기에는 좋습니다. 그러나 누구도 이러한 코드 조각을 반복해서 입력하는 것을 원하지는 않을 것입니다. 그래서 더 큰 코드 조각을 다루기 쉽게 관리하는 몇 가지 방법이 필요합니다.

첫 번째 단계로 코드의 재사용을 위한 함수(Function)가 있습니다. 함수는 이름이 붙여진 코드 조각이고, 다른 것으로부터 분리되어 있습니다. 함수는 입력 매개변수(Parameter)로 모든 타입을 여러 개 취할 수 있습니다. 그리고 반환(Return) 값으로 모든 타입을 여러 개 반환할 수 있습니다.

함수는 두 가지 작업을 수행합니다.

  • 정의하기(Define)

  • 호출하기(Call)

파이썬 함수를 정의하기 위해서는 def와 함수 이름, 괄호를 입력합니다. 괄호 안에는 옵션으로 매개변수를 입력할 수 있습니다. 그리고 마지막으로 콜론을 붙입니다. 함수 이름은 변수 이름과 동일한 규칙으로 작성합니다(이름의 첫 글자는 영문자와 언더스코어를 사용하고, 이름은 영문자, 숫자, 언더스코어만 사용할 수 있습니다).

매개변수가 없는 함수를 정의하고 호출해보겠습니다. 다음은 아주 간단한 파이썬 함수입니다.

>>> def do_nothing():
	pass

이와 같이 함수에 매개변수가 없더라도, 함수를 정의하기 위해 콜론을 입력해야 합니다. 함수 다음 라인은 if 문에서 들여 쓰기를 한 것과 마찬가지로 들여 쓰기를 해야 합니다. 이 함수가 아무것도 하지 않는다는 것을 보여주기 위해 pass 문을 사용했습니다. 이것은 마치 책에서 의도적으로 페이지를 비운 것과 같습니다.

함수 이름과 괄호를 입력해서 함수를 호출해보겠습니다. 앞서 말했던 것처럼 아무 일도 일어나지 않습니다.

>>> do_nothing()
>>> 

매개변수가 없는 함수를 정의하고 호출해보겠습니다. 함수는 한 단어를 출력합니다.

>>> def print_a_word():
	print("Python")

	
>>> print_a_word()
#Python

print_any_word() 함수를 출력하면 파이썬은 함수 내의 코드를 실행합니다 이 코드에서는 한 단어를 출력하고 메인 프로그램으로 넘어갑니다.

매개변수가 없지만 True 값을 반환하는 함수를 정의해보겠습니다.

>>> def InJeong():
	return True

if 문에서 이 함수를 호출하여 반환되는 값으로 조건 테스트를 할 수 있습니다.

>>> if InJeong():
	print('INJEONG!!!')
    else:
	print('NO INJEONG!!!')

	
#INJEONG!!!

함수를 if 문의 조건 테스트, while 문과 같은 루프와 함께 사용한다면 이전에 할 수 없었던 일들을 할 수 있게 됩니다.

이제 괄호에 뭔가를 넣어볼 때가 됐습니다. anything이라는 매개변수와 함께 echo() 함수를 정의해 보겠습니다. 두 번의 anythiing 변수 사이에 스페이스를 넣어서 return 문으로 값을 반환합니다.

>>> def echo(anything):
	return anything + ' ' + anything

문자열 'Rumplestiltskin'과 echo()를 호출해보겠습니다.

>>> echo('Rumplestiltskin')
#'Rumplestiltskin Rumplestiltskin'

함수로 전달한 값을 인자(Argument)라고 부릅니다. 인자와 함수를 호출하면 인자의 값은 함수 내에서 해당하는 매개변수(Parameter)에 복사됩니다. 이전 예제에서 'Rumplestiltskin' 문자열 인자와 함께 echo() 함수를 호출했습니다. 이 문자열 값은 echo()의 anything 매개변수에 복사됩니다. 그러고 나서 두 문자열 사이의 스페이스 문자열을 호출자(Caller)에 반환합니다.

위에서 살펴본 코드는 아주 기본적인 것입니다. 다음 코드에서는 if 문을 사용하고, 인자를 취하는 함수를 작성해보겠습니다. 앞서 if... elif... else 문에서 사용한 코드를 활용할 것입니다. color라는 문자열 매개변수가 있는 commentary 함수를 정의합니다. color 값에 따라 이를 설명하는 문자열을 호출자에 반환합니다.

>>> def commentary(color):
	if color == 'red':
		return "It's a tomato."
	elif color == "green":
		return "It's a green pepper."
	elif color == 'bee purple':
		return "I don't know what it is, but only bees can see it."
	else:
		return "I've never heard of the color " + color + "."

문자열 인자 'blue'를 사용하여 commentary() 함수를 호출해보겠습니다.

>>> comment = commentary('blue')

함수는 다음과 같이 동작합니다.

  • 'blue'라는 값을 함수 내의 color 매개변수에 할당합니다.

  • if... elif... else 문을 실행합니다.

  • 문자열을 반환합니다.

  • 문자열을 comment 변수에 할당합니다.

comment 변수를 출력해보겠습니다.

>>> print(comment)
#I've never heard of the color blue.

함수의 인자는 개수에 상관없이 모든 타입의 인자를 취할 수 있습니다. 반환 값도 마찬가지로 개수에 상관없이 모든 타입을 반환할 수 있습니다. 만약 함수가 명시적으로 return을 호출하지 않으면, 호출자는 반환 값으로 None을 얻습니다.

>>> print(do_nothing())
#None

None은 아무것도 없다는 것을 뜻하는 파이썬의 특별한 값입니다. None이 부울로 평가될 때에는 False처럼 보이지만, 부울 값의 False와는 다릅니다.

>>> thing = None
>>> if thing:
	print("It's some thing")
    else:
	print("It's no thing")

	
#It's no thing

부울 값 False와 None을 구분하기 위해 is 연산자를 사용하겠습니다.

>>> if thing is None:
	print("It's nothing")
    else:
	print("It's something")

	
#It's nothing

이것은 아주 미묘한 차이처럼 보이지만 꽤 중요합니다. 빠뜨린 빈 값을 구분하기 위해 None을 사용했습니다. 정수 혹은 부동소수점수, 빈 문자열, 빈 리스트, 빈 튜플, 빈 딕셔너리, 빈 셋은 모두 False지만, None과 같지 않다는 것을 명심하세요.

인자가 None인지 출력하는 함수를 작성해보겠습니다.

>>> def is_none(thing):
	if thing is None:
		print("It's None")
	elif thing:
		print("It's True")
	else:
		print("It's False")

이제 몇 가지 테스트를 해봅시다.

>>> is_none(None)
#It's None
>>> is_none(True)
#It's True
>>> is_none(False)
#It's False
>>> is_none(0)
#It's False
>>> is_none(0.0)
#It's False
>>> is_none(())
#It's False
>>> is_none([])
#It's False
>>> is_none({})
#It's False
>>> is_none(set())
#It's False

 

7.1. 위치 인자

 

파이썬은 다른 언어에 비해 함수의 인자를 유연하고 독특하게 처리합니다. 인자의 가장 익숙한 타입은 값을 순서대로 상응하는 매개변수에 복사하는 위치 인자(Positional Arguments)입니다.

다음 함수는 위치 인자로 딕셔너리를 만들어서 반환합니다.

>>> def menu(wine, entree, dessert):
	return {'wine': wine, 'entree': entree, 'dessert': dessert}

>>> menu('chardonnay', 'chicken', 'cake')
#{'wine': 'chardonnay', 'entree': 'chicken', 'dessert': 'cake'}

매우 일반적이지만 위치 인자의 단점은 각 위치의 의미를 알아야 한다는 것입니다. 다음 코드에서 첫 번째 인자 대신 마지막 인자를 wine으로 menu()를 호출하면 매우 이상한 식사가 될 것입니다.

>>> menu('beef', 'bagle', 'bordeaux')
#{'wine': 'beef', 'entree': 'bagle', 'dessert': 'bordeaux'}

 

7.2. 키워드 인자

 

위치 인자의 혼동을 피하기 위해 매개변수에 상응하는 이름을 인자에 지정할 수 있습니다. 심지어 인자를 함수의 정의와 다른 순서로 지정할 수 있습니다.

>>> menu(entree = 'beef', dessert = 'bagle', wine = 'bordeaux')
#{'wine': 'bordeaux', 'entree': 'beef', 'dessert': 'bagle'}

위치 인자와 키워드 인자를 섞어서 쓸 수 있습니다. wine을 첫 번째 인자로, entree와 dessert를 키워드 인자로 지정해보겠습니다.

>>> menu('frontenac', dessert = 'flan', entree = 'fish')
#{'wine': 'frontenac', 'entree': 'fish', 'dessert': 'flan'}

위치 인자와 키워드 인자로 함수를 호출한다면 위치 인자가 먼저 와야 합니다.

 

7.3. 기본 매개 변숫값 지정하기

 

매개변수에 기본값을 지정할 수 있습니다. 호출자가 대응하는 인자를 제공하지 않으면 기본값을 사용합니다. 이 단조로운 기능은 꽤 유용합니다. 이전 코드를 사용해보겠습니다.

>>> def menu(wine, entree, dessert = 'pudding'):
	return {'wine': wine, 'entree': entree, 'dessert': dessert}

dessert 인자 없이 menu()를 호출해보겠습니다.

>>> menu('chardonnay', 'chicken')
#{'wine': 'chardonnay', 'entree': 'chicken', 'dessert': 'pudding'}

dessert 인자를 입력하면 기본값 대신 입력한 인자가 사용됩니다.

>>> menu('chardonnay', 'chicken', 'cake')
#{'wine': 'chardonnay', 'entree': 'chicken', 'dessert': 'cake'}

참고로, 기본 인자 값은 함수가 실행될 때 계산되지 않고, 함수를 정의할 때 계산됩니다. 리스트 혹은 딕셔너리와 같은 변경 가능한 데이터 타입을 기본 인자로 사용하는 실수를 하지 않도록 주의하세요.

다음 코드는 buggy() 함수를 실행할 때마다 빈 result 리스트에 arg 인자를 추가한 후 한 항목이 있는 리스트를 출력할 것이라고 예상됩니다. 그러나 예상과는 달리 함수를 처음 호출했을 때만 result 리스트가 비어 있게 됩니다. 두 번째로 호출하면 result 리스트에는 이전 호출에서 생긴 한 항목이 들어 있습니다.

>>> def buggy(arg, result = []):
	result.append(arg)
	print(result)

	
>>> buggy('a')
#['a']
>>> buggy('b')
#['a', 'b']

다음과 같이 작성하면 예상대로 작동할 것입니다.

>>> def works(arg):
	result = []
	result.append(arg)
	return result

>>> works('a')
#['a']
>>> works('b')
#['b']

첫 번째 인자 호출을 가리키기 위해 매개변수에 다른 값을 넣어서 수정할 수 있습니다.

>>> def nonbuggy(arg, result = None):
	if result is None:
		result = []
	result.append(arg)
	print(result)

	
>>> nonbuggy('a')
#['a']
>>> nonbuggy('b')
#['b']

 

7.4. 위치 인자 모으기: *

 

C와 C++ 프로그래밍 경험이 있다면 파이썬 프로그램에서 애스터리스크(*)를 어떤 것을 가리키는 포인터로 생각할 수도 있습니다. 하지만 파이썬에는 포인터가 없습니다.

함수의 매개변수에 애스터리스크를 사용할 때, 애스터리스크는 매개변수에서 위치 인자 변수들을 튜플로 묶습니다. print_args() 함수에 인자를 전달하여, args 매개변수의 튜플 결과를 살펴봅시다.

>>> def print_args(*args):
	print('Positional argument tuple:', args)

함수를 인자 없이 호출하면 *args에는 아무것도 없습니다.

>>> print_args()
#Positional argument tuple: ()

인자를 넣어서 args 튜플을 출력해보겠습니다.

>>> print_args(3, 2, 1, 'wait!', 'uh...')
#Positional argument tuple: (3, 2, 1, 'wait!', 'uh...')

가변 인자를 사용하는 print()와 같은 함수는 매우 유용합니다. 함수에 위치 인자를 지정할 때 맨 끝에 *args를 써서 나머지 인자를 모두 취하게 할 수 있습니다.

>>> print_more('cap', 'gloves', 'scarf', 'monocle', 'mustache wax')
#Need this one: cap
#Need this one too: gloves
#All the rest: ('scarf', 'monocle', 'mustache wax')

*를 사용할 때 가변 인자의 이름으로 굳이 args를 사용할 필요는 없지만 관용적으로 args를 사용합니다.

 

7.5. 키워드 인자 모으기: **

 

키워드 인자를 딕셔너리로 묶기 위해 두 개의 애스터리스크(**)를 사용할 수 있습니다. 인자의 이름은 키고, 값은 이 키에 대응하는 딕셔너리 값입니다. 다음 예제는 print_kwargs() 함수를 정의하여 키워드 인자를 출력합니다.

>>> def print_kwargs(**kwargs):
	print('Keyword arguments:', kwargs)

키워드 인자를 넣어서 함수를 출력해보겠습니다.

>>> print_kwargs(wine = 'merlot', entree = 'mutton', dessert = 'macaroon')
#Keyword arguments: {'wine': 'merlot', 'entree': 'mutton', 'dessert': 'macaroon'}

함수 안에 kwargs 딕셔너리가 있습니다.

위치 매개변수와 *args, **kwargs를 섞어서 사용하려면 이들을 순서대로 배치해야 합니다. 그리고 args와 마찬가지로 키워드 매개변수의 이름을 kwargs로 할 필요는 없지만 관용적으로 kwargs를 사용합니다.

 

7.6. docstring

 

파이썬의 철학(Zen of Python)에는 '가독성은 중요하다(Readability counts)'라는 구절이 들어있습니다. 함수 몸체 시작 부분에 문자열을 포함시켜 함수 정의에 문서(Documentation)를 붙일 수 있습니다. 이것이 바로 함수의 docstring입니다.

>>> def echo(anything):
	'echo returns its input argument'
	return anything

docstring은 길게 작성할 수 있으며, 서식(Formatting)을 추가할 수도 있습니다.

>>> def print_if_true(thing, check):
    '''
    Prints the first argument if a second argument is true.
    The operation is:
	1. Check whether the *second* argument is true.
	2. If it is, print the *first* argument.
    '''
    if check:
	print(thing)

함수의 docstring을 출력하려면 help() 함수를 호출합니다. 함수 인자의 리스트와 서식화된 docstring을 읽기 위해 함수 이름을 인자로 전달합니다.

>>> help(echo)
#Help on function echo in module __main__:
#
#echo(anything)
#    echo returns its input argument

만약 서식 없는 docstring을 그대로 보고 싶다면 다음과 같이 합니다.

>>> print(echo.__doc__)
#echo returns its input argument

이상하게 보이는 __doc__은 docstring의 내부 이름인 함수 내의 변수입니다. 잠시 후에 이것에 대해 자세히 설명하도록 하겠습니다.

 

7.7. 일등시민: 함수

 

'모든 것이 객체(Object)다'라는 파이썬의 철학은 파이선의 만트라(Mantra; 기도, 명상 때 외우는 주문)이기도 합니다. 객체는 숫자, 문자열, 튜플, 리스트, 딕셔너리, 그리고 함수를 포함합니다. 파이썬에서 함수는 일등 시민(First-class citizen)입니다. 이 뜻은 함수를 변수에 할당할 수 있고, 다른 함수에서 이를 인자로 쓸 수 있으며, 함수에서 이를 반환할 수 있다는 것입니다. 이와 같이 파이썬은 몇몇 다른 언어에서는 불가능한 기능을 제공합니다.

이를 테스트하기 위해 다음과 같이 아무 인자도 없는 answer() 함수를 정의하겠습니다. 이 함수는 숫자 42를 출력합니다.

>>> def answer():
	print(42)

이 함수를 실행하면 다음과 같은 결과가 나옵니다.

>>> answer()
#42

이제 run_something이라는 함수를 정의해보겠습니다. 함수를 실행하는 func 매개변수가 있습니다. 함수 내부에서는 func 매개변수로 함수를 호출합니다.

>>> def run_something(func):
	func()

run_something() 함수에 answer 인자를 넣으면 다른 모든 인자와 마찬가지로 이 함수를 데이터처럼 사용합니다.

>>> run_something(answer)
#42

answer()를 전달하는 것이 아니라 answer를 전달했습니다. 파이썬에서 괄호는 함수를 호출한다는 의미입니다. 괄호가 없으면 함수를 다른 모든 객체와 마찬가지로 간주합니다. 파이썬에서 모든 것은 객체이기 때문입니다.

>>> type(run_something)
#<class 'function'>

인자를 넣어서 함수를 실행해보겠습니다. 먼저 두 숫자 인자 arg1, arg2를 더한 값을 출력하는 add_args() 함수를 정의합니다.

>>> def add_args(arg1, arg2):
	print(arg1 + arg2)

add_args()의 타입은 무엇일까요?

>>> type(add_args)
#<class 'function'>

이번에는 세 인자를 취하는 run_something_with_args() 함수를 호출해보겠습니다.

  • func: 실행할 함수

  • arg1: func 함수의 첫 번째 인자

  • arg2: func 함수의 두 번째 인자

>>> def run_something_with_args(func, arg1, arg2):
	func(arg1, arg2)

run_something_with_args()를 호출할 때, 호출자에 의해 전달된 함수는 func 매개변수에 할당 됩니다. 그리고 arg1과 arg2는 인자 목록의 값을 얻습니다. 그러고 나서 인자와 함께 func(arg1, arg2) 함수가 실행됩니다. 왜냐하면 객체 앞의 괄호는 함수를 실행하라는 뜻이기 때문입니다.

add_args 함수 이름과 인자 5, 9를 run_something_with_args()에 넣어서 실험해보겠습니다.

>>> run_something_with_args(add_args, 5, 9)
#14

run_something_with_args() 함수 내의 add_args 함수 이름은 func 매개변수에 할당됩니다. 그리고 숫자 5와 9는 각각 arg1, arg2에 할당됩니다. 결국 add_args(5, 9)를 실행합니다.

또한 이것을 *args, **kwargs 인자와 결합해서 사용할 수 있습니다.

여러 개의 위치 인자를 취하는 함수를 정의해보겠습니다. sum() 함수를 사용해서 이 인자들을 더한 값을 반환합니다.

>>> def sum_args(*args):
	return sum(args)

sum() 함수는 순회 가능한 숫자(정수 혹은 부동소수점수) 인자의 값을 모두 더하는 파이썬 내장 함수입니다.

함수와 여러 개의 위치 인자를 취하는 새로운 run_with_positional_args() 함수를 정의합니다.

>>> def run_with_positional_args(func, *args):
	return func(*args)

이 함수를 다음과 같이 호출해보겠습니다.

>>> run_with_positional_args(sum_args, 1, 2, 3, 4)
#10

함수를 리스트, 튜플, 셋, 딕셔너리의 요소로 사용할 수 있습니다. 함수는 불변하기 때문에 딕셔너리의 키로도 사용할 수 있습니다.

 

7.8. 내부 함수

 

함수 안에 또 다른 함수를 정의할 수 있습니다.

>>> def outer(a, b):
	def inner(c, d):
		return c + d
	return inner(a, b)

>>> outer(4, 7)
#11

내부(Inner) 함수는 루프나 코드 중복을 피하기 위해 또 다른 함수 내에 어떤 복잡한 작업을 한 번 이상 수행할 때 유용하게 사용됩니다. 다음 문자열 예제에서 내부 함수는 인자에 텍스트를 붙여줍니다.

>>> def knights(saying):
	def inner(quote):
		return "We are the knights who say: '%s'" % quote
	return inner(saying)

>>> knights('Ni!')
#"We are the knights who say: 'Ni!'"

 

7.9. 클로져

 

내부 함수는 클로져(Closure)처럼 행동할 수 있습니다. 클로져는 다른 함수에 의해 동적으로 생성됩니다. 그리고 바깥 함수로부터 생성된 변숫값을 변경하고, 저장할 수 있는 함수입니다.

다음 코드는 앞의 '내부 함수'에서 작성한 knights() 예제입니다. 이 함수를 새로운 knights2() 함수로 정의합니다. 이 함수는 이전과는 달리 inner2()라는 클로져를 사용하기 때문에 똑같은 함수가 아닙니다. 그 차이를 살펴보겠습니다.

  • inner2()는 인자를 취하지 않고, 외부 함수의 변수를 직접 사용합니다.

  • knights2()는 inner2 함수 이름을 호출하지 않고, 이를 반환합니다.

>>> def knights2(saying):
	def inner2():
		return "We are the knights who say: '%s'" % saying
	return inner2

inner2() 함수는 knights2() 함수가 전달받은 saying 변수를 알고 있습니다. 코드에서 return inner2 라인은 (호출되지 않은) inner2 함수의 특별한 복사본을 반환합니다. 이것이 외부 함수에 의해 동적으로 생성되고, 그 함수의 변숫값을 알고 있는 함수인 클로져입니다.

다른 인자를 넣어서 knights2() 함수를 두 번 호출해보겠습니다.

>>> a = knights2('Duck')
>>> b = knights2('Hasenpfeffer')

a와 b의 타입은 무엇일까요?

>>> type(a)
#<class 'function'>
>>> type(b)
#<class 'function'>

이들은 함수지만, 클로져이기도 합니다.

>>> a
#<function knights2.<locals>.inner2 at 0x000001D9831BF730>
>>> b
#<function knights2.<locals>.inner2 at 0x000001D9831BF7B8>

이들을 호출하면, knights2() 함수에 전달되어 사용된 saying을 기억합니다.

>>> a()
#"We are the knights who say: 'Duck'"
>>> b()
#"We are the knights who say: 'Hasenpfeffer'"

 

7.10. 익명 함수: lambda()

 

파이썬의 람다 함수(Lambda Function)는 단일문으로 표현되는 익명 함수(Anonymous Function)입니다. edit_story() 함수를 정의해보겠습니다. 인자는 다음과 같습니다.

  • words: words 리스트

  • func: words의 각 word 문자열에 적용되는 함수

>>> def edit_story(words, func):
	for word in words:
		print(func(word))

이제 words 리스트와 각 word에 적용할 함수가 필요합니다. 대충 아무 단어로 리스트를 만들어 보겠습니다.

>>> sound = ['thud', 'meow', 'thud', 'hiss']

그리고 각 word의 첫 글자를 대문자로 만들고 느낌표를 붙여주는 함수를 정의하겠습니다.

>>> def enliven(word):
	return word.capitalize() + '!'

둘을 섞어봅시다.

>>> edit_story(sound, enliven)
#Thud!
#Meow!
#Thud!
#Hiss!

람다를 사용해보겠습니다. enliven() 함수를 람다로 간단하게 바꿀 수 있습니다.

>>> edit_story(sound, lambda word: word.capitalize() + '!')
#Thud!
#Meow!
#Thud!
#Hiss!

람다에서 하나의 word 인자를 취했습니다. 람다의 콜론(:)과 닫는 괄호 사이에 있는 것이 함수의 정의 부분입니다.

대부분의 경우 enliven()과 같은 실제 함수를 사용하는 것이 람다를 사용하는 것보다 훨씬 더 명확합니다. 람다는 많은 작은 함수를 정의하고, 이들을 호출해서 얻은 모든 결괏값을 저장해야 하는 경우에 유용합니다. 특히 콜백 함수를 정의하는 그래픽 유저 인터페이스(GUI)에서 람다를 사용할 수 있습니다.

여담으로 익명 함수는 제가 심심할 때 하는 한 줄 코딩에서 애용하는 것입니다.

 

8. 제너레이터

 

제너레이터(Generator)는 파이썬의 시퀀스를 생성하는 객체입니다. 제너레이터로, 전체 시퀀스를 한 번에 메모리에 생성하고 정렬할 필요 없이, 잠재적으로 아주 큰 시퀀스를 순회할 수 있습니다. 제너레이터는 이터레이터에 대한 데이터의 소스로 자주 사용됩니다. 이전 코드를 기억한다면, 우리는 이미 제너레이터 중 하나인 range() 함수를 사용했습니다. range()는 일련의 정수를 생성합니다. 파이썬 2의 range()는 메모리에 제한적인 리스트를 반환합니다. 또한 파이썬 2의 xrange()가 있는데, 이는 파이썬 3의 일반적인 range()가 되었습니다. 다음은 정수 1에서 100가지 더하는 코드입니다.

>>> sum(range(1, 101))
#5050

제너레이터를 순회할 때마다 마지막으로 호출된 항목을 기억하고 다음 값을 반환합니다. 제너레이터는 일반 함수와 다릅니다. 일반 함수는 이전 호출에 대한 메모리가 없고, 항상 똑같은 상태로 첫 번째 라인부터 수행합니다.

잠재적으로 큰 시퀀스를 생성하고, 제너레이터 컴프리헨션에 대한 코드가 아주 긴 경우에는 제너레이터 함수를 사용하면 됩니다. 이것은 일반 함수지만 return 문으로 값을 반환하지 않고, yield 문으로 값을 반환합니다. 우리만의 range() 함수를 작성해보겠습니다.

>>> def my_range(first = 0, last = 10, step = 1):
	number = first
	while number < last:
		yield number
		number += step

다음과 같이 함수를 실행합니다.

>>> my_range
#<function my_range at 0x000001D983187F28>

다음과 같이 제너레이터 객체를 반환합니다.

>>> ranger = my_range(1, 5)
>>> ranger
#<generator object my_range at 0x000001D983154E58>

이 제너레이터 객체를 순회할 수 있습니다.

>>> for x in ranger:
	print(x)

	
#1
#2
#3
#4

 

9. 데코레이터

 

가끔씩 소스 코드를 바꾸지 않고, 사용하고 있는 함수를 수정하고 싶을 때가 있습니다. 일반적인 예는 함수에 전달된 인자를 보기 위해 디버깅 문을 추가하는 것입니다.

데코레이터(Decorator)는 하나의 함수를 취해서 또 다른 함수를 반환하는 함수입니다. 이 강력한 방법을 사용하기 위해서는 다음과 같은 무기를 사용합니다.

  • *args와 **kwargs

  • 내부 함수

  • 함수 인자

document_it() 함수는 다음과 같이 데코레이터를 정의합니다.

  • 함수 이름과 인자 값을 출력합니다.

  • 인자로 함수를 실행합니다.

  • 결과를 출력합니다.

  • 수정된 함수를 사용할 수 있도록 반환합니다.

코드는 다음과 같습니다.

>>> def document_it(func):
	def new_function(*args, **kwargs):
		print('Running function:', func.__name__)
		print('Positional arguments:', args)
		print('Keyword arguments:', kwargs)
		result = func(*args, **args)
		print('Resuilt:', result)
		return result
	return new_function

document_it() 함수에 어떤 func 함수 이름을 전달하든지 간에 document_it() 함수에 추가 선언문이 포함된 새 함수를 얻습니다. 데코레이터는 실제로 func 함수로부터 코드를 실행하지 않습니다. 하지만 document_it() 함수로부터 func를 호출하여 결과뿐만 아니라 새로운 함수를 얻습니다.

그러면 데코레이터를 어떻게 사용할까요? 수동으로 데코레이터를 적용해보겠습니다

>>> def document_it(func):
	def new_function(*args, **kwargs):
		print('Running function:', func.__name__)
		print('Positional arguments:', args)
		print('Keyword arguments:', kwargs)
		result = func(*args, **kwargs)
		print('Resuilt:', result)
		return result
	return new_function

>>> def add_ints(a, b):
	return a + b

>>> add_ints(3, 5)
#8
>>> cooler_add_ints = document_it(add_ints)
>>> cooler_add_ints(3, 5)
#Running function: add_ints
#Positional arguments: (3, 5)
#Keyword arguments: {}
#Resuilt: 8
#8

위와 같이 수동으로 데코레이터를 할당하는 대신, 다음과 같이 데코레이터를 사용하고 싶은 함수에 그냥 '@데코레이터_이름'을 추가합니다.

>>> @document_it
    def add_ints(a, b):
	return a + b

>>> add_ints(3, 5)
#Running function: add_ints
#Positional arguments: (3, 5)
#Keyword arguments: {}
#Resuilt: 8
#8

함수는 여러 개의 데코레이터를 가질 수 있습니다. 결과(Result)를 제곱하는 sqare_it() 데코레이터를 작성해보겠습니다.

>>> def square_it(func):
	def new_function(*args, **kwargs):
		result = func(*args, **kwargs)
		return result ** 2
	return new_function

함수에서 가장 가까운(def 바로 위) 데코레이터를 먼저 실행한 후, 그 위의 데코레이터가 실행됩니다. 이 예제에서 순서를 바꿔도 똑같은 결과를 얻지만, 중간 과정이 바뀝니다.

>>> @document_it
	@square_it
	def add_ints(a, b):
		return a + b

>>> add_ints(3, 5)
#Running function: new_function
#Positional arguments: (3, 5)
#Keyword arguments: {}
#Resuilt: 64
#64

데코레이터의 순서를 바꿔보겠습니다.

>>> @square_it
	@document_it
	def add_ints(a, b):
		return a + b

>>> add_ints(3, 5)
#Running function: add_ints
#Positional arguments: (3, 5)
#Keyword arguments: {}
#Resuilt: 8
#64

 

10. 네임스페이스와 스코프

 

이름(Name)은 사용되는 위치에 따라 다른 것을 참조할 수 있습니다. 파이선 프로그램에는 다양한 네임스페이스(Namespace)가 있습니다. 네임스페이스는 특정 이름이 유일하고, 다른 네임스페이스에서의 같은 이름과 관계가 없는 것을 말합니다.

각 함수는 자신의 네임스페이스를 정의합니다. 메인 프로그램에서 x라는 변수를 정의하고, 함수에서 x라는 변수를 정의했을 때, 이들은 서로 다른 것을 참조합니다. 하지만 이 경계를 넘을 수 있습니다. 다양한 방법으로 다른 네임스페이스의 이름을 접근할 수 있습니다.

메인 프로그램은 전역 네임스페이스를 정의합니다. 이와 같이 이 네임스페이스의 변수들은 전역 변수입니다.

함수로부터 전역 변수(Global Variable)의 값을 얻을 수 있습니다.

>>> animal = 'fruitbat'
>>> def print_global():
	print('inside print_global:', animal)

	
>>> print('at the top level:', animal)
#at the top level: fruitbat
>>> print_global()
#inside print_global: fruitbat

함수에서 전역 변수의 값을 얻어서 바꾸려 하면 에러가 발생합니다.

>>> def change_and_print_global():
	print('inside change_and_print_global:', animal)
	animal = 'wombat'
	print('after the change:', animal)

	
>>> change_and_print_global()
#Traceback (most recent call last):
#  File "<pyshell#213>", line 1, in <module>
#    change_and_print_global()
#  File "<pyshell#212>", line 2, in change_and_print_global
#    print('inside change_and_print_global:', animal)
#UnboundLocalError: local variable 'animal' referenced before assignment

전역 변수를 바꾸려 한다면, 또 다른 이름의 animal 변수를 변경하려 한다. 다음은 함수 내에 있는 지역 변수입니다.

>>> def change_local():
	animal = 'wombat'
	print('inside change_local:', animal, id(animal))

	
>>> change_local()
#inside change_local: wombat 2461666952168
>>> animal
#'fruitbat'
>>> id(animal)
#2461667015344

여기서 무슨 일이 일어날까요? 첫 번째 줄에서 문자열 'fruitbat'을 전역 변수 animal에 할당했습니다. change_local() 함수 또한 이름이 animal인 변수를 갖지만, 그것은 로컬(Local) 네임스페이스 안에 있습니다.

각 객체의 유일한 값을 출력하기 위해 그리고 change_local() 함수 내의 animal 변수가 메인 프로그램의 animal 변수와 같지 않다는 것을 증명하기 위해 파이썬 함수 id()를 사용했습니다.

함수 내의 지역 변수(Local Variable)가 아닌 전역 변수를 접근하기 위해 global 키워드를 사용해서 전역 변수의 접근을 명시해야 합니다(파이썬 철학: 명확한 것이 함축적인 것보다 낫다).

>>> animal = 'fruitbat'
>>> def change_and_print_global():
	global animal
	animal = 'wombat'
	print('inside change_and_print_global:', animal)

	
>>> animal
#'fruitbat'
>>> change_and_print_global()
#inside change_and_print_global: wombat
>>> animal
#'wombat'

함수 안에 global 키워드를 사용하지 않으면 파이썬은 로컬 네임스페이스를 사용하고 변수는 지역 변수가 됩니다. 지역 변수는 함수를 수행한 뒤 사라집니다. 파이썬은 네임스페이스의 내용을 접근하기 위해 두 가지 함수를 제공합니다.

  • locals() 함수는 로컬 네임스페이스의 내용이 담긴 딕셔너리를 반환합니다.

  • globals() 함수는 글로벌 네임스페이스의 내용이 담긴 딕셔너리를 반환합니다.

이들을 사용해보겠습니다.

>>> animal = 'fruitbat'
>>> def change_local():
	animal = 'wombat'
	print('locals:', locals())

	
>>> animal
#'fruitbat'
>>> change_local()
#locals: {'animal': 'wombat'}
>>> print('globals:', globals())
#globals: {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'animal': 'fruitbat', 'change_local': <function change_local at 0x000001E66D6976A8>}
>>> animal
#'fruitbat'

change_local() 함수 내의 로컬 네임스페이스에는 animal 로컬 변수만 포함되어 있습니다. 메인 프로그램의 글로벌 네임스페이스에는 animal 글로벌 변수와 다른 여러 가지 것들이 포함되어 있습니다.

 

10.1. _과 __의 사용

 

두 언더스코어(__)로 시작하고 끝나는 이름은 파이썬 내의 사용을 위해 예약되어 있습니다. 그러므로 변수를 선언할 때 두 언더스코어를 사용하면 안 됩니다. 애플리케이션 개발자들이 이와 같은 변수 이름을 선택할 가능성이 낮아서, 이러한 네이밍 패턴을 선택한 것입니다.

예를 들어 함수의 이름은 시스템 변수 function.__name__에 있습니다. 그리고 함수의 docstring은 function.__doc__에 있습니다.

>>> def amazing():
	'''This is the amaing function
want to see it again?'''
	print('This function is named:', amazing.__name__)
	print('And its docsting is:', amazing.__doc__)

	
>>> amazing()
#This function is named: amazing
#And its docsting is: This is the amaing function
#want to see it again?

조금 전에 globals() 함수 출력 결과에서 봤듯이, 메인 프로그램은 특별한 이름 __main__으로 할당되어 있습니다.

 

11. 에러 처리하기: try, except

 

하거나 하지 않는 것이지, 시도해본다는 것은 없다(Do, or do not. There is no try).
- 요다(Yoda)

일부 언어에서 에러는 특수 함수의 반환 값으로 표시된다. 파이썬에서는 관련 에러가 발생할 때 실행되는 코드인 예외(Exception)를 사용합니다.

앞에서 위치의 범위를 벗어난 리스트나 튜플의 접근, 딕셔너리의 존재하지 않는 키와 같은 몇몇 예외를 봤습니다. 어떤 상황에서 실패할 수 있는 코드를 실행했을 때에는 모든 잠재적인 에러를 방지하기 위해 적절한 예외 처리가 필요합니다.

사용자에게 무슨 일이 일어나고 있는지 알리기 위해 예외가 발생할 수 있는 모든 곳에 예외 처리를 하는 것은 좋은 습관입니다. 이것으로 문제를 해결하지 못할 수도 있지만, 적어도 사용자에게 이 상황을 알리고, 정상적으로 프로그램을 종료할 수 있습니다. 만약 어떤 함수에서 예외가 발생하여 그곳에서 잡히지 않았다면, 호출한 함수에 일치하는 핸들러에 의해서 이 예외를 잡을 때까지 버블링(Bubbling)한다. 만약 여러분이 예외 처리에 대한 핸들러를 제공하지 않았다면, 파이썬은 에러 메시지와 오류가 발생한 위치에 대한 정보를 출력하고 프로그램을 종료합니다. 다음 코드를 보겠습니다.

>>> short_list = [1, 2, 3]
>>> position = 5
>>> short_list[position]
#Traceback (most recent call last):
#  File "<pyshell#165>", line 1, in <module>
#    short_list[position]
#IndexError: list index out of range

에러가 발생하도록 코드를 내버려 두는 것보다, 에러가 예상되는 코드에 try 문을 사용하고, 그 에러를 처리하기 위해 except 문을 사용합니다.

>>> short_list = [1, 2, 3]
>>> position = 5
>>> try:
	short_list[position]
    except:
	print('Need a position between 0 and', len(short_list) - 1, 'but got', position)

	
#Need a position between 0 and 2 but got 5

try 블록 안의 코드를 실행할 때 에러가 있다면 예외가 발생하고 except 블록 내의 코드가 실행된다. 만약 try 블록 안에 에러가 없다면 except 블록을 건너뜁니다.

위와 같이 인자 없는 except 문을 지정하는 것은 모든 예외 타입을 잡는다는 것을 말합니다. 두 개 이상의 예외 타입이 발생하면 각각 별도의 예외 핸들러를 제공하는 것이 가장 좋은 방법이다. 모든 예외를 처리하기 위해 그냥 except 문만 쓸 수도 있습니다. 이렇게 쓰는 것을 강제하지는 않지만, 이것은 포괄적인 예외처리 방식입니다('어떤 에러가 발생함'을 출력하는 것과 같습니다). 그러므로 각 에러에 대한 특정 예외 핸들러를 사용하는 것이 좋습니다.

예외 타입을 넘어 예외사항에 대한 세부 정보를 얻고 싶다면 다음과 같이 변수 이름에서 예외 객체 전체를 얻을 수 있습니다.

except 예외 타입 as 이름

다음 예제를 먼저 IndexError를 찾는다. 이것은 시퀀스에서 잘못된 위치를 입력할 때 발생하는 예외 타입입니다. 그리고 err 변수에 IndexError 예외를, other 변수에 다른 기타 예외를 저장합니다. 다음 예제는 사용자가 입력한 값에 따라 other 변수에 저장된 객체를 출력합니다.

>>> short_list = [1, 2, 3]
>>> while True:
	value = input('Position [q to quit]? ')
	if value == 'q':
		break
	try:
		position = int(value)
		print(short_list[position])
	except IndexError as err:
		print('Bad index:', position)
	except Exception as other:
		print('Something else broke:', other)

		
#Position [q to quit]? 1
#2
#Position [q to quit]? 0
#1
#Position [q to quit]? 2
#3
#Position [q to quit]? 3
#Bad index: 3
#Position [q to quit]? 2
#3
#Position [q to quit]? two
#Something else broke: invalid literal for int() with base 10: 'two'
#Position [q to quit]? q

3을 입력하면 예상한 대로 IndexError가 발생합니다. 그리고 two를 입력하면 모든 예외를 처리하는 두 번째 예외 핸들러 코드에서 int() 함수에 대한 예외가 발생합니다.

 

12. 예외 만들기

 

이전 절에서 예외 처리에 대해 배웠습니다. 그러나 IndexError와 같은 모든 예외는 파이썬 표준 라이브러리에 미리 정의되어 있는 것입니다. 우리는 필요한 예외 처리를 선택해서 사용할 수 있습니다. 또한 우리가 만든 프로그램에서 특별한 상황에 발생할 수 있는 예외를 처리하기 위해 예외 타입을 정의할 수 있습니다.

예외는 클래스고, Exception 클래스의 자식입니다. 다음 예제에서 words 문자열에 대문자가 있을 때 예외를 발생하는 UppercaseException 예외를 만들겠습니다.

>>> class UppercaseException(Exception):
	pass

>>> words = ['eeenie', 'meenie', 'miny', 'MO']
>>> for word in words:
	if word.isupper():
		raise UppercaseException(word)

	
#Traceback (most recent call last):
#  File "<pyshell#7>", line 3, in <module>
#    raise UppercaseException(word)
#UppercaseException: MO

심지어 UppercaseException에 대한 행동도 정의하지 않았습니다. 부모 클래스인 Exception은 예외가 발생했을 때 무엇을 출력하는지 알아내도록 처리하고 있습니다. 다음과 같이 예외 객체에 접근해서 내용을 출력할 수 있습니다.