개발자입니다
[클린코드 파이썬] 10장: 파이썬다운 함수 만들기 본문
함수명
함수는 통상적으로 동작을 수행하기 때문에, 대체로 이름에 동사가 들어간다. 또한 행위 대상을 설명하는 명사가 들어가기도 한다. 예를 들어 refreshConnection(), setPassword(), extract_version()
클래스나 모듈의 일부인 메소드의 이름에는 명사가 필요하지 않을 수 있다. SatelliteConnection 클래스의 reset() 메소드나 webbrowser 모듈의 open() 함수는 이미 이해에 필요한 맥락을 제공한다.
약어나 너무 짧은 이름보다는, 길고 설명적인 이름을 사용하는 편이 좋다. 수학자라면 gcd()라는 함수만으로도 두 수의 최대공약수를 반환한다는 사실을 즉시 간파할 수 있지만, 일반 사람들은 getGreatestCommonDenominator()라고 써야 좀 더 이해하기 쉽다.
all, any, date, email, file, format, hash, id, input, list, min, max, object, open, random, set, str, sum, test, type과 같은 파이썬의 내장 함수 또는 모듈 이름은 절대 사용하지 말자.
함수 크기의 트레이드 오프
함수는 되도록 짧아야 하고, 한 화면을 넘어갈 정도로 길어서는 안 된다고 말하는 프로그래머도 있다. 수백 줄짜리 함수보다는 수십 줄밖에 안 되는 함수가 비교적 이해하기 쉽다.
간혹, "짧을수록 좋다"는 지침을 극단적으로 받아들여 모든 함수가 기껏해야 서너 줄 안쪽의 코드여야 한다고 주장하는 이들도 있다.
내 경험으론, 이상적인 함수는 30행 이하여야 하고, 결코 200행을 넘어서면 안 된다.
함수 파라미터와 인수
함수의 파라미터parameter는 함수의 def 문의 괄호 사이에 있는 변수 이름인 반면, 인수argument는 함수를 호출할 때 괄호 사이에 들어가느 ㄴ값이다.
0~3개 정도의 파라미터는 괜찮지만 5~6개 이상은 아무래도 너무 많다는 규칙을 세워두는 편이 좋다. 함수가 지나치게 복잡해진다면, 파라미터가 적고 길이도 짧은 함수로 분할하는 방법을 고려하는 것이 가장 좋다.
기본 인수
함수 파라미터의 복잡도를 줄이는 한 가지 방법은 파라미터에 대한 기본 인수를 제공하는 것이다. 기본 인수default argument는 함수를 호출할 때 변수를 지정하지 않는 경우 인수로 사용되는 값이다.
def introduction(name, greeting='Hello'):
print(greeting + ', ' + name)
introduction('Alice')
# Hello, Alice
introduction('재호', '안녕하세요')
# 안녕하세요, 재호
기본 인수가 있는 파라미터는 항상 기본 인수가 없는 파라미터 뒤에 위치해야 한다.
*와 **를 사용해 함수에 인수 전달하기
인수 그룹을 따로따로 함수로 넘기기 위해 *와 ** 구문(보통 '스타'와 '스타스타'로 발음)을 사용할 수 있다. * 구문을 사용하면 (리스트나 튜플 같은) 반복가능 객체의 아이템을 전달할 수 있다. ** 구문을 사용하면 (딕셔너리 같은) 매핑 객체의 키-값 쌍을 개별 인수들로 전달할 수 있다.
예를 들어 print() 함수는 여러 인수를 취할 수 있다.
print('cat', 'dog', 'moose')
# cat dog moose
함수 호출에서 인수의 위치가 어떤 파라미터에 할당되는 인수인지를 결정하기 때문에 이러한 인수를 위치기반 인수positional argument라고 한다. 반면에 이러한 문자열들을 리스트에 저장한 다음에 이 리스트를 전달하려고 시도하면, print() 함수는 리스트를 하나의 값으로 출력하려 시도한다고 생각할 것이다.
args = ['cat', 'dog', 'moose']
print(args)
# ['cat', 'dog', 'moose']
* 구문을 사용해 리스트의 아이템들(또는 반복가능 데이터 타입)을 개별 위치기반 인수로 해석하면 된다.
args = ['cat', 'dog', 'moose']
print(*args)
# cat dog moose
** 구문을 사용해 (딕셔너리 등의) 매핑 데이터 타입을 개별 키워드 인수로 전달할 수 있다. 키워드 인수는 파라미터 이름과 등호 앞에 붙는다. 예를 들어 print() 함수에는 표시되는 인수 사이에 넣을 문자열을 지정하는 sep 키워드 인수가 있다. 기본적으로 단일 공백 문자열(' ')로 설정된다.
print('cat', 'dog', 'moose', sep='=')
# cat-dog-moose
kwargsForPrint = {'sep': '-'}
print('cat', 'dog', 'moose', **kwargsForPrint)
# cat-dog-moose
*를 사용해 가변인수 함수 만들기
임의 개수의 인수를 받아서 이들을 한데 곱하는 product() 함수를 만들어 보자.
def product(*args):
result = 1
for num in args:
result *= num
return result
product(3, 3)
# 9
product(2, 1, 2, 3)
# 12
함수 내부에서, args는 모든 위치기반 인수를 포함하는 표준적인 파이썬 튜플일 뿐이다. 엄밀히 따지면 이 파라미터가 *로 시작하기만 하면 어떤 이름도 가능하지만, 보통은 관례에 따라 args라는 이름을 붙인다.
다음과 같은 내장 sum() 함수가 바로 이렇게 만들어져 있다.
sum([2, 1, 2,3])
# 8
sum() 함수는 하나의 반복가능 인수를 기대하므로, 여러 인수를 넘기면 다음과 같은 예외가 발생한다.
sum(2, 1, 2, 3)
# Traceback (most recent call last):
한편, 여러 값 중에서 최솟값 또는 최댓값을 찾는 내장 min()과 max() 함수는 단일 반복가능한 인수나 여러 개의 개별 인수를 모두 받아들인다.
min([2, 1, 3, 5, 8])
# 1
min(2, 1, 3, 5, 8)
# 1
**를 사용해 가변인수 함수 만들기
def 문에서 * 구문은 가변적인 위치기반 인수를 나타내지만, ** 구문은 가변적인 선택적 키워드 인수를 나타낸다.
118개에 달하는, 알려진 모든 원소에 대한 파라미터를 갖는 다음과 같은 formMolecule() 함수를 생각해 보자.
def forMolecule(hydrogen, helium, lithium, berylium, boron, ...생략...
수소(hydrogen) 파라미터에 대해 2를, 산소(oxygen) 파라미터에 대해 1을 전달해서 물(water)를 반환하는 방식은 부담스럽고 가독성이 떨어진다.
formMolecule(2, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, ...생략...
# 'water'
각 변수가 기본 인수를 갖도록 명명된 키워드 파라미터를 사용하여 함수 호출에서 파라미터를 전달할 필요가 없게 만들면, 함수 관리가 더 수월해진다.
def formMolecule(hydrogen=0, helium=0, lithium=0, berylium=0, ...생략...
이렇게 하면 기본 인수와 다른 값을 갖는 파라미터에 대한 인수만 명세하면 되기 때문에 formMolecule()을 호출하기가 더 쉬워진다.
formMolecule(hydrogen=2, oxygen=1)
# 'water'
하지만 아직도 118개의 파라미터 이름이 달린, 다루기 힘든 def 문이 남아있다.
키워드 인수에 대한 ** 구문을 사용해 딕셔너리에서 모든 파라미터와 해당 인수를 키-값 쌍으로 수집할 수 있다. 엄밀히 따지면 ** 파라미터는 아무 이름이나 붙일 수 있지만, 보통은 관례에 따라 kwargs라는 이름을 붙인다.
def formMolecules(**kwargs):
if len(kwargs) == 2 and kwargs['hydrogen'] == 2 and kwargs['oxygen'] == 1:
return 'water'
# (함수의 나머지 코드가 여기 들어간다)
formMolecules(hydrogen=2, oxygen=1)
# 'water'
*와 **로 래퍼 함수 만들기
def 문에서 *와 ** 구문에 대한 일반적인 사용 사례는 래퍼wrapper 함수를 만드는 것인데, 래퍼 함수는 인수를 다른 함수로 전달하고 해당 함수의 결괏값을 반환한다.
def printLower(*args, **kwargs):
args = list(args)
for i, value in enumerate(args):
args[i] = str(value).lower()
return print(*args, **kwargs)
name = 'Albert'
printLower('Hello,', name)
# hello, albert
printLower('DOG', 'CAT', 'MOOSE', sep=', ')
# dog, cat, moose
함수형 프로그래밍
함수형 프로그래밍funtional programming은 전역 변수나 어떠한 외부 상태(하드 드라이브의 파일, 인터넷 연결, 데이터베이스 등)도 수정하지 않고 계산 수행 목적의 함수 작성을 강조하는 프로그래밍 패러다임이다. 파이썬 프로그램에서 주로 사용 가능한 함수형 기능은 부수효과가 없는 함수, 고차 함수, 람다 함수다.
부수효과
부수효과는 함수가 자신의 코드와 지역 변수 바깥에 존재하는 프로그램의 각 부분에 가하는 모든 변화를 말한다.
def subtract(number1, number2):
return number1 - number2
subtract(123, 987)
# -864
이 subtract() 함수에는 부수효과가 없다.
이제 다음과 같이 TOTAL이라는 이름의 전역 변수에 숫자 인수를 더하는 addToTotal() 함수를 생각해 보자.
TOTAL = 0
def addToTotal(amount):
global TOTAL
TOTAL += amount
return TOTAL
addToTotal(10)
# 10
addToTotal(10)
# 20
addToTotal(9999)
# 10019
TOTAL
# 10019
addToTotal() 함수에는 이 함수 밖에 존재하는 요소인 TOTAL 전역 변수를 수정하는 부수효과가 있다.
또 다른 부수효과로, 함수 외부에서 참조된 가변 객체를 제자리에서 바꿔치기해서 변경하는 것도 들 수 있다.
def removeLastCatFromList(petSpecies):
if len(petSpecies) > 0 and petSpecies[-1] == 'cat':
petSpecies.pop()
myPets = ['dog', 'cat', 'bird', 'cat']
removeLastCatFromList(myPets)
myPets
# ['dog', 'cat', 'bird']
이와 관련된 개념으로 결정론적 함수deterministic function가 있는데, 이는 항상 같은 인수에 대해 같은 결괏값을 반환한다. subtract(123, 987) 함수 호출은 항상 -864를 반환한다. 비결정론적 함수nondeterministic function는 동일한 인수를 전달한다고 해서 항상 동일한 값을 반환하는 것은 아니다. 예를 들어 random.randint(1, 10)을 호출하면, 1에서 10 사이의 임의의 정수가 반환된다.
결정론적이고 부수효과가 없는 함수를 순수 함수pure function라고 부른다.
파이썬에서는 언제라도 순수 함수를 작성할 수 있으며, 또 그렇게 작성해야 마땅하다.
고차원 함수
고차원 함수는 다른 함수를 인수로 넘기거나 반환할 수 있다.
def callItTwice(func, *args, **kwargs):
func(*args, **kwargs)
func(*args, **kwargs)
callItTwice(print, 'Hello, world!'):
# Hello, world!
# Hello, world!
파이썬에서 함수는 일급 객체이므로 여느 다른 객체와 성질이 같다. 즉 함수를 변수에 저장하거나 인수로 전달하거나 반환값으로 사용할 수 있다.
람다 함수
람다 함수lambda function는 익명 함수anonymous function 또는 이름 없는 함수nameless function라고도 부르며, 이름이 없고 코드가 오직 하나의 return 문으로만 구성된 단순화된 함수다.
def rectanglePerimeter(rect):
return (rect[0] * 2) + (rect[1] * 2)
myRectangle = [4, 10]
rectanglePerimeter(myRectangle)
# 28
이와 동치인 람다 함수는 다음과 같을 것이다.
lambda rect: (rect[0] * 2) + (rect[1] * 2)
다음처럼 def 문이 하는 작업을 효과적으로 모사할 수 있다.
rectanglePerimeter = lambda rect: (rect[0] * 2) + (rect[1] * 2)
rectanglePerimeter([4, 10])
# 28
람다 함수 구문은 다른 함수 호출에 대한 인수 역할을 하기 위한 작은 함수를 지정하는 데 도움이 된다. 예를 들어 sorted() 함수에는 함수를 지정할 수 있는 key라는 이름의 키워드 인수가 있다.
rects = [[10, 2], [3, 6], [2, 4], [3, 9], [10, 7], [9, 9]]
sorted(rects, key=lambda rect: (rect[0] * 2) + (rect[1] * 2))
# [[2, 4], [3, 6], [10, 2], [3, 9], [10, 7], [9, 9]]
리스트 컴프리헨션을 이용한 매핑과 필터링
다음 코드에서는 리스트 컴프리헨션을 사용해 map() 함수를 모사한다.
[str(n) for n in [8, 16, 18, 19, 12, 1, 6, 7]]
# ['8', '16', '18', '19', '12', '1', '6', '7']
그리고 다음 코드에서는 리스트 컴프리헨션을 사용해 filter() 함수를 모사한다.
[n for n in [8, 16, 18, 19, 12, 1, 6, 7] if n % 2 == 0]
# [8, 16, 18, 12, 6]
결괏값은 항상 동일한 데이터 타입이어야 한다
함수를 예측 가능하게 하려면 단일 데이터 타입 값만 반환하도록 노력해야 한다.
import random
def returnsTwoTypes():
if random.randint(1, 2) == 1:
return 42
else:
return 'forty two'
이 함수를 호출하는 코드를 작성할 때 가능한 한 여러 데이터 타입을 다 다뤄야 한다는 사실을 간혹 잊기 쉽다.
특히 주의해야 할 경우가 하나 있는데, 함수가 항상 None을 반환하는 것이 아니라면 아예 None을 반환하지 않아야 한다. None 값은 NoneType 데이터 타입의 유일한 값이다.
예외 발생시키기 vs 에러 코드 반환하기
index() 문자열 메소드는 부분 문자열을 찾을 수 없는 경우, find()와 달리 ValueError 예외를 발생시킨다. 이 예외를 처리하지 않으면 프로그램 작동이 중단되며 이러한 동작은 에러를 감추지 않는다는 점에서 선호될 때가 많다.
'Python > 클린 코드, 이제는 파이썬이다' 카테고리의 다른 글
[클린코드 파이썬] 13장: 빅오를 활용한 알고리즘 성능 분석과 개선 (3) | 2024.09.06 |
---|---|
[클린코드 파이썬] 11장: 주석과 타입 힌트 (2) | 2024.09.04 |
[클린코드 파이썬] 8장: 파이썬에서 빠지기 쉬운 함정들 (0) | 2024.09.02 |
[클린코드 파이썬] 6장: 파이썬다운 코드를 작성하는 법 (5) | 2024.09.01 |
[클린코드 파이썬] 5장: 코드 악취 감지와 대응 (4) | 2024.09.01 |