8 분 소요

함수

함수는 파이썬에서 코드를 재사용하고 조직화하기 위한 가장 중요한 수단이다. 경험적으로 같은 일을 반복하거나 비슷한 코드를 한 번 이상 실행해야 할 것이 예상되면 재사용 가능한 함수를 작성하는 것이 더 나을 것이다. 함수는 파이썬 명령들의 집합에 이름을 지어 좀 더 가독성이 좋은 코드를 작성할 수 있도록 해준다.

함수는 def 예약어로 정의하고 return 예약어를 사용해서 값을 반환한다.

def my_function(x, y, z=1.5):
    if z > 1:
        return z * (x + y)
    else:
        return z / (x + y)

return 문은 몇 개가 되든 상관없다. 함수 블록이 끝날 때까지 return 문이 없다면 None이 자동으로 반환된다.

함수는 여러 개의 일반 인자와 키워드 인자를 받을 수 있다. 키워드 인자는 흔히 기본값 또는 부수적인 인자를 지정하기 위해 사용한다. 위 함수에서는 x와 y는 일반 인자이며 z는 키워드 인자다.

my_function(5, 6, z=0.7)
0.06363636363636363
my_function(3.14, 7, 3.5)
35.49
my_function(10, 20)
45.0

함수의 키워드 인자는 항상 일반 인자 다음에 와야 한다는 규칙이 있다. 키워드 인자의 순서에는 제약이 없으므로 키워드 인자의 이름만 기억하고 있으면 된다.

네임스페이스, 스코프, 지역 함수

함수는 전역지역, 두 가지 스코프scope(영역)에서 변수를 참조한다. 변수의 스코프를 설명하는 다른 용어로 네임스페이스가 있다. 함수 내에서 선언된 변수는 기본적으로 모두 지역 네임스페이스에 속한다. 지역 네임스페이스는 함수가 호출될 때 생성되며 함수의 인자를 통해 즉시 생성되고 함수의 실행이 끝나면 사라진다.

def func():
    a = [] # 지역 네임스페이스 생성, 함수 종료 시 사라짐
    for i in range(5):
        a.append(i)

func() 함수를 호출하면 비어 있는 리스트 a가 생성되고 다섯 개의 원소가 리스트에 추가된다. 그리고 함수가 끝나면 이 리스트 a는 사라진다.

a = [] # a를 함수 밖에서 생성
def func():
    for i in range(5):
        a.append(i) # a는 사라지지 않음
a # func() 실행하기 전에 a는 빈 리스트
[]
func() # func() 실행
a # a에 다섯 개의 원소가 추가
[0, 1, 2, 3, 4]

함수 안에서 함수 밖의 변수 데이터를 변경해야 하는 경우가 있다. 이때는 함수에서 global 키워드를 이용하면 된다. global 키워드로 변수를 지정하면, 해당 함수에서는 지역 변수를 만들지 않고, 함수 바깥에 선언된 변수를 참조하게 된다.

a = None

def bind_a_variable():
    global a # 전역 변수 선언
    a = [] 

bind_a_variable()
print(a) # a가 None에서 빈 리스트로 변경
[]

global 예약어는 자주 사용하지 않도록 한다. 일반적으로 전역 변수는 시스템 전체의 상태를 저장하기 위한 용도로 사용한다. 만약 전역 변수를 많이 사용하면 클래스를 사용한 객체지향 프로그래밍이 적절한 상황이라는 반증이다.

여러 값 반환하기

하나의 함수는 반환 값이 아예 없을 수도 있고 여러 개의 값을 반환할 수도 있다.

def f():
    a = 5
    b = 6
    c = 7
    return a, b, c # 3개의 값을 반환

a, b, c = f() # f()는 (5, 6, 7)이라는 튜플을 반환

print(a, b, c)
5 6 7
return_value = f() # f()는 (5, 6, 7)이라는 튜플을 반환
print(return_value)
(5, 6, 7)

함수도 객체다

파이썬에서 함수도 객체이므로 다른 언어에서는 힘든 객체 생성 표현을 쉽게 할 수 있다. 데이터를 정제하기 위해 다음과 같은 문자열 리스트를 변형해야 한다고 가정하자.

states = ['Alabama', 'Georgia!', 'Georgia', 'georgia', 'FlOrIda',
          'southcarolina##', 'West virginia?']

엉망인 데이터의 분석을 위해 문자열 리스트를 정형화할 필요가 있는데, 공백 문자를 제거하고 필요 없는 문장 부호를 제거하거나 대소문자를 맞추는 등의 전처리 작업이 필요하다. 이는 내장 문자열 메서드와 정규 표현식을 위한 re 표준 라이브러리를 이용해서 쉽게 해결할 수 있다.

import re

def clean_strings(strings):
    result = []
    for value in strings:
        value = value.strip() # 문자열의 시작과 끝의 공백 제거
        value = re.sub('[!#?]', '', value) # 문장 부호 제거
        value = value.title() # 단어 앞만 대문자로 변경
        result.append(value) # 위 과정을 거친 value 값을 result에 추가
    return result
clean_strings(states)
['Alabama',
 'Georgia',
 'Georgia',
 'Georgia',
 'Florida',
 'Southcarolina',
 'West Virginia']

다른 유용한 접근법으로는 적용할 함수를 리스트에 담아두고 각각의 문자열에 적용하는 것이다.

def remove_punctuation(value):
    return re.sub('[!#?]', '', value)

clean_ops = [str.strip, remove_punctuation, str.title] # 공백 문자 제거, 문장 부호 제거, 맨 앞만 대문자로 변경

def clean_strings(strings, ops):
    result = []
    for value in strings:
        for function in ops:
            value = function(value)
        result.append(value)
    return result
clean_strings(states, clean_ops)
['Alabama',
 'Georgia',
 'Georgia',
 'Georgia',
 'Florida',
 'Southcarolina',
 'West Virginia']

이와 같이 좀 더 실용적인 패턴은 문자열 변형을 상위 레벨에서 쉽게 처리할 수 있다.

순차적 자료형에 대해 함수를 적용하는 내장 함수인 map 함수를 이용해서 함수를 인자로 사용할 수도 있다.

for x in map(remove_punctuation, states):
    print(x)
Alabama
Georgia
Georgia
georgia
FlOrIda
southcarolina
West virginia

익명 함수

파이썬은 익명anonymous함수 혹은 람다lambda함수라고 하는 값을 반환하는 단순한 한 문장으로 이루어진 함수를 지원한다. lambda 예약어로 정의하며, 이는 ‘익명 함수를 선언한다’라는 의미다. 특정한 기능을 수행하는 함수를 한 줄에 작성할 수 있다는 점이 특징이다.

# 일반적인 함수 사용법
def short_function(x):
    return x * 2

short_function(2)
4
# lambda 함수 사용법
equiv_anon = lambda x: x * 2

equiv_anon(2)
4

람다 함수를 사용하면 실제 함수를 선언하거나 람다 함수를 지역 변수에 대입하는 것보다 코드를 적게 쓰고 더 간결해진다.

def apply_to_list(some_list, f):
    return [f(x) for x in some_list]

ints = [4, 0, 1, 5, 6]
apply_to_list(ints, lambda x: x * 2) # 함수에서 함수를 인자로 받는 경우
[8, 0, 2, 10, 12]

다음은 문자열 리스트를 각 문자열에서 다양한 문자가 포함된 순서로 정렬하는 예제다.

strings = ['foo', 'card', 'bar', 'aaaa', 'abab']

리스트의 sort 메서드에 람다 함수를 넘겨 다음과 같이 정렬할 수 있다.

strings.sort(key=lambda x: len(set(list(x)))) # 각 문자열을 list에 넣고 set에 넣으면 중복을 제거한
                                              # 고유 문자만 남고 len()함수로 개수를 구하여 기준으로 삼음
strings
['aaaa', 'foo', 'abab', 'bar', 'card']

커링: 일부 인자만 취하기

커링은 함수에서 일부 인자만 취하는 새로운 함수를 만드는 기법이다.

def add_numbers(x, y):
    return x + y
add_five = lambda y: add_numbers(5, y) #  하나의 변수만 인자로 받아 5를 더해주는 새로운 함수 add_five 생성

add_five(2)
7
add_five(1, 2) # 인자로 2개를 넣으면 TypeError 발생

add_numbers의 두 번째 인자를 커링했다. 내장 functools 모듈의 partial 함수를 이용하면 이 과정을 단순화할 수 있다.

from functools import partial
add_five = partial(add_numbers, 5)

add_five(3)
8

제네레이터

파이썬은 리스트 내의 객체나 파일의 각 로우 같은 순차적인 자료를 순회하는 일관적인 방법을 제공한다. 이터레이터 프로토콜을 이용해 순회 가능한 객체를 만들 수 있다. 예를 들어 사전을 순회하면 사전의 키가 반환된다.

some_dict = {'a': 1, 'b': 2, 'c': 3}

for key in some_dict:
    print(key)
a
b
c

for key in some_dict라고 작성하면 파이썬 인터프리터는 some_dict에서 이터레이터를 생성한다.

dict_iterator = iter(some_dict)

dict_iterator
<dict_keyiterator at 0x1a9799f89f0>

이터레이터는 for 문 같은 컨텍스트에서 사용될 경우 객체를 반환한다. 리스트나 리스트와 유사한 객체를 취하는 대부분의 메서드는 순회 가능한 객체도 허용한다. 여기에는 min, max, sum 같은 내장 메서드와 list, tuple 같은 자료구조를 생성하는 메서드도 포함된다.

list(dict_iterator)
['a', 'b', 'c']

제네레이터는 순회 가능한 객체를 생성하는 간단한 방법이다. 일반 함수는 실행되면 단일 값을 반환하는 반면 제네레이터는 순차적인 값을 매 요청 시마다 하나씩 반환한다. 제네레이터를 생성하려면 함수에서 return을 하는 대신 yield 예약어를 사용한다.

def squares(n=10):
    print('Generating squares from 1 to {0}'.format(n ** 2))
    for i in range(1, n+1):
        yield i ** 2

제네레이터를 호출하더라도 코드가 즉각적으로 실행되지 않는다.

gen = squares()

gen
<generator object squares at 0x000001A97967B740>

next()로 접근할 수 있다. 대신 한 번 호출하면 다음 순회 값으로 넘어간다.

next(gen)
Generating squares from 1 to 100
1

제네레이터로부터 값을 요청하면 그때서야 제네레이터의 코드가 실행된다.

for x in gen:
    print(x, end=' ') # 원래는 1, 4, 9, ..., 81, 100 이 맞지만 앞에서 next()로 1을 넘김
4 9 16 25 36 49 64 81 100 

제네레이터 표현식

제네레이터를 생성하는 더 간단한 방법은 제네레이터 표현식을 사용하는 것이다. 괄호를 사용해서 제네레이터를 생성할 수 있다.

gen = (x ** 2 for x in range(100))

gen

<generator object at 0x000001A97967B7B0>

위 코드와 동일한 코드다.

def _make_gen():
    for x in range(100):
        yield x ** 2
gen = _make_gen()

제네레이터 표현식은 리스트 표현식을 인자로 받는 어떤 파이썬 함수에서도 사용할 수 있다.

sum(x ** 2 for x in range(100))
328350
dict((i, i ** 2) for i in range(5))
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

itertools 모듈

표준 라이브러리인 itertools 모듈은 일반 데이터 알고리즘을 위한 많은 제네레이터를 포함하고 있다.

import itertools

first_letter = lambda x: x[0]

names = ['Alan', 'Adam', 'Wes', 'Will', 'Albert', 'Steven']

for letter, names in itertools.groupby(names, first_letter):
    print(letter, list(names))
A ['Alan', 'Adam']
W ['Wes', 'Will']
A ['Albert']
S ['Steven']
data = ['A', 'B', 'C'] # 데이터 준비

print(data)
['A', 'B', 'C']
result = list(itertools.permutations(data, 3)) # 모든 순열 구하기

print(result)
[('A', 'B', 'C'), ('A', 'C', 'B'), ('B', 'A', 'C'), ('B', 'C', 'A'), ('C', 'A', 'B'), ('C', 'B', 'A')]
result = list(itertools.combinations(data, 2)) # 2개를 뽑는 모든 조합 구하기

print(result)
[('A', 'B'), ('A', 'C'), ('B', 'C')]
result = list(itertools.product(data, repeat=2)) # 2개를 뽑는 모든 순열 구하기(중복 허용)

print(result)
[('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'B'), ('B', 'C'), ('C', 'A'), ('C', 'B'), ('C', 'C')]
result = list(itertools.combinations_with_replacement(data, 2)) # 2개를 뽑는 모든 조합 구하기(중복 허용)

print(result)
[('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'B'), ('B', 'C'), ('C', 'C')]

유용한 itertools 함수

함수 설명
combinations(iterable, k) iterable에서 순서를 고려하지 않고 길이가 k인 모든 가능한 조합을 생성한다.
permutations(iterable, k) iterable에서 순서를 고려하여 길이가 k인 모든 가능한 조합을 생성한다.
gropby(iterable[, keyfunc]) iterable에서 각각의 고유한 키에 따라 그룹을 생성한다.
product(*iterables, repeat=1) iterable에서 카테시안 곱을 구한다. 중첩된 for 문 사용과 유사하다.
combinations_with_replacement(iterable, k) iterable에서 k개의 데이터를 뽑아 순서를 고려하지 않고 나열하는 모든 조합을 생성한다. 다만 원소를 중복해서 뽑는다.

에러와 예외 처리

견고한 프로그램을 작성하려면 파이썬의 오류와 예외를 잘 처리해야 한다.

float('1.2345')
1.2345
float('something') # ValueError 발생

적절하지 않은 입력에 대해서는 입력을 그대로 반환하는 개선된 float 함수를 작성한다고 가정하고 try/except 블록을 사용해서 float 함수를 호출하면 된다.

def attemt_float(x):
    try:
        return float(x)
    except:
        return x

except 블록에 있는 코드는 float(x)가 예외를 발생했을 때 실행된다.

attemt_float('1.2345')
1.2345
attemt_float('something') # 예외가 발생하지 않음
'something'

float 함수가 ValueError가 아닌 예외를 발생시키는 경우도 있다.

float((1, 2))

입력이 문자열이나 숫자가 아니라는 뜻의 TypeError는 정당한 오류이므로 그대로 두고 ValueError만 무시하고 싶다면 except 뒤에 처리할 예외의 종류를 적어준다.

def attempt_float(x):
    try:
        return float(x)
    except ValueError:
        return x
attempt_float((1, 2))

튜플을 사용해서 여러 개의 예외를 한 번에 처리할 수도 있다(괄호로 묶어준다).

def attempt_float(x):
    try:
        return float(x)
    except (TypeError, ValueError):
        return x
attempt_float((1, 2))
(1, 2)

예외를 무시하지 않고, try 블록의 코드가 성공적으로 수행되었는지 여부와 관계없이 실행시키고 싶은 코드는 finally 블록을 이용해서 적어준다.

f = open(path, 'w')

try:
    write_to_file(f)
finally:
    f.close() </code>

여기서 파일 핸들 f는 항상 닫히게 된다. 이와 유사하게 try 블록이 성공적으로 수행되었을 때만 else 블록을 사용해서 수행할 코드를 적어준다.

f = open(path, 'w') 

try:
    write_to_file(f)
except:
    print('Failed')
else:
    print('Succeeded')
finally:
    f.close() </code>

태그:

업데이트:

댓글남기기