-
7.1 함수와 인수
- 반복되는 코드
프로그램의 유사한 작업을 코드중복이라 부른다. 이처럼 반복되는 코드는 한번 정의해놓고 계속 사용하는 것이 좋다. 함수는 일련의 코드 블록에 이름을 붙여 정의한 것이다.
def 함수명(인수 목록): 본체
def 키워드(define)를 앞에 쓴 후 함수의 이름을 정의한다. 함수명은 명칭이므로 자유롭게 붙일 수 있되 동작을 잘 표현하는 동사로 붙이는 것이 좋다. 인수 목록은 호출원이 함수로 전달하는 작업거리이며 함수 내부에서 사용한다. 본체는 함수 동작을 처리하는 코드를 기술한다.
함수(인수 목록)
파이썬은 소스를 처음부터 순서대로 읽어 실행하는 인터프리터 언어이다. 그래서 함수 호출 전에 함수가 정의되어 있어야 한다.
print("~4 = ", calcsum(4)) # error, is not defined print("~10 = ", calcsum(10)) def calcsum(n): sum = 0 for num in range(n+1): sum += num return sum
다음은 에러처리이다. is not defined라는 에러를 낸다. 함수 정의문을 호출문보다 앞으로 옮기면 문제가 해결된다.
-> black box : 까맣기 때문에 안이 안 보인다. 즉, 원리를 이해할 수 없지만 값은 안다.
-> white box : 투명하기 때문에 안이 보인다. 즉, 원리를 이해할 수 있다.
함수는 black box와 같다. 즉 이용만 할 수 있으면 된다.
모듈(module)(규격화)
-> 부품화한다 = 모듈화한다.
-> 전체에서 떼어낼 수 있는 것.
-> sw에서는 코드블럭이 부품을 의미한다.
=> 모듈을 사용할 경우
- 코드 재사용성이 높아진다.
- 블랙박스화 시킬 수 있다.
- 인수
인수는 호출원에서 함수로 전달되는 작업거리며 호출하는 쪽과 함수를 연결한다는 의미로 매개변수라고도 부른다. 인수는 개수 제한이 없어 얼마든지 많이 전달할 수 있고, 아예 없을 수도 있다.
인수가 없으면 함수 동작이 항상 같아 활용성이 떨어진다. 따라서 인수는 함수 동작에 변화를 주어 활용성을 높인다.
함수 정의문의 인수를 형식 인수라고 하며 함수 호출문에서 전달하는 인수를 실인수라고 한다.
만일 인수의 수가 맞지 않으면, missing argument라는 에러가 뜬다.
한 파일에 함수 여러개를 정의해도 상관은 없으나, 이름은 달라야 한다. 뒤의 함수로 앞의 함수를 덮어쓰기 때문이다.
- 리턴값
리턴값은 함수의 실행 결과를 호출원으로 돌려주는 값이다. 함수의 입장에서 볼 때 인수는 입력값이고 리턴은 출력값이다.
인수는 여러 개가 있을 수 있지만, 리턴값은 딱 하나밖에 없다. 리턴값을 반환할 때 return 명령 뒤에 반환할 값을 지정한다.
값을 리턴하는 함수의 호출문은 그 자체가 하나의 값이기 때문에 수식 내에서 바로 사용 가능하다.
리턴값이 꼭 있어야 하는 것은 아니며 반환값이 없는 경우도 있다. 이럴 경우 None을 리턴한다.
None은 숫자와 연산할 수 없다. 연산하는 경우 에러를 발생시킨다.
def printsum(n): sum = 0 for num in range(n + 1): sum += num print("~", n, "=", sum) # 유연성 떨어짐 printsum(4)
위 같은 경우는 함수 하나에서 합계를 구하고 출력까지 진행한다. 이는 유연성이 떨어진다. 차라리 변수에 대입 연산자를 이용해 리턴값을 저장하겠다.
단일 책임의 원칙 - 한 가지 함수는 한 가지 일을 한다.
- pass 명령
pass 명령은 아무 동작도 하지 않는 명령이다. 해석기는 pass 명령을 만나면 그냥 무시하고 건너뛴다.
def calctotal(): # :으로 끝났으니 밑에 코드가 있어야 한다. pass # 빈 코드임을 나타내는 pass 명령 필요
함수뿐만 아니라 클래스나 조건문 등을 작성할 때도 빈 코드를 일단 만들어둘 때는 pass 명령을 사용한다.
- 함수는 반드시 코드블럭이 있어야 한다.
- 실제 구현을 나중으로 미루고자 할 때는 pass를 지정한다.
습관적으로 무조건 함수를 만드는 연습을 하자
- 가변 인수(인수의 개수가 고정되어 있지 않다. ex)print())
함수를 호출할 때 함수 정의문의 형식 인수 개수만큼 실인수를 전달해야 한다.
하지만, 가변인수는 임의 개수의 인수를 받는다.
인수 이름 앞에 * 기호를 붙이면 이 자리에 여러 개의 인수가 올 수 있다.
def 함수명(*인수명): 명령 블럭
가변 인수는 튜플로 묶여서 전달된다.
def intsum(*ints): # 컬렉션(튜플)로 받아온다. sum = 0 for num in ints: # 컬렉션이기 때문에 for문도 가능하다. sum += num return sum print(intsum(1, 2, 3)) print(intsum(5, 7, 8, 9, 10))
가변인수는
- 일반 인수 뒤에만 올 수 있다. (가변 인수 선언 이후엔 모든 인수를 포함하기 때문에)
- 하나만 사용 가능하다.
intsum(s, *ints) # 가능 intsum(*ints, s) # 에러, 일반 인수 뒤에만 intsum(*ints, *nums) # 에러, 하나만 사용 가능
- 인수의 기본값(인수의 값을 주지 않을 경우, 디폴트 지정)
인수를 추가할 경우 호출원에서 작업 지시를 섬세하게 전달할 수 있어 활용성이 높아진다. 하지만 인수가 추가됨에 따라 기본적인 경우에도 인수를 전달해야 하는 불편함이 있다.
이처럼 잘 바뀌지 않는 인수는 인수 목록에 기본값으로 지정해둔다.
기본값이 있는 인수는 일종의 옵션으로 취급되어 생략 가능하다. 실인수를 생략하면 기본값을 전달한 것으로 가정한다.
def calcstep(begin, end, step = 1): # 세 번째 인수의 기본값 지정
def calcstep(begin, end, step = 1): # step 인수 전달되지 않았다면 1 사용, 아니면 그 값 사용 sum = 0 for num in range(begin, end + 1, step): sum += num return sum print("1~10", calcstep(1, 10, 2)) print("1~100", calcstep(1, 100))
그러나 중간의 인수가 기본값을 가지면 호출문에서 이를 생략할 수 없기 때문에 기본값을 가지는 인수 뒤에 일반 인수가 올 수 없다.
calcstep(begin, step = 1, end): # X # calstep(1, 100) 호출문이 step을 100으로 지정한 건지 end를 100으로 자정한 것인지 알 수 없다. calcstep(begin = 1, step, end = 1): # 건너뛰기 X
그렇기 때문에 중간은 생략할 수 없고 끝 부분만 생략할 수 있으므로 기본값을 가지는 인수는 목록 뒤쪽에 적어야 한다.
- 키워드 인수
함수를 호출할 때 함수 정의문에 선언된 순서대로 실인수를 전달하며 호출문에 나타나는 순서에 따라 형식 인수에 차례대로 대입된다. 순서대로 인수 전달 방식을 위치 인수(Positional Argument)라고 하는데, 이는 호출문이 짧고 간단하지만 인수의 수가 많아지면 헷갈리게 된다.
그럴 때 인수 순서가 아닌 인수의 명칭으로 매칭하는 키워드 인수를 사용하거나 혼용한다.
def calcstep(begin, end, step): sum = 0 for num in range(begin, end + 1, step): sum += num return sum print("3~5=", calcstep(3, 5, 1)) # 위치 기반 호출만 print("3~5=", calcstep(begin = 3, end = 5, step = 1)) # 키워드만, 키워드는 순서 상관 X print("3~5=", calcstep(end = 5, step = 1, begin = 3)) print("3~5=", calcstep(3, 5, step = 1)) # 혼용, 반드시 위치 기반 먼저 해석, 그 다음 키워드 방식 print("3~5=", calcstep(3, step = 1, end = 5))
위치 인수와 키워드 인수를 혼용할 경우, 위치 인수가 항상 먼저 와야 한다. 앞쪽에 키워드 인수가 왔으면 뒤쪽에는 워치 인수가 올 수 없다.
키워드 인수는 맨 끝에 와야 하고, 위치 인수는 키워드 인수 앞에 있어야 한다.
- 키워드 가변 인수(사전 이용)
키워드 인수를 가변 개수 전달할 때는 인수 목록에 ** 기호를 붙인다. 호출원에서 여러 개의 키워드 인수를 전달하면 인수의 이름과 값의 쌍을 사전으로 만들어 전달한다. 함수 내부에서는 사전을 읽듯 인수값을 꺼내 사용한다.
def calcstep(**args): begin = args["begin"] end = args["end"] step = args["step"] sum = 0 for num in range(begin, end + 1, step): sum += num return sum print("3~5=", calcstep(begin = 3, end = 5, step = 1)) # 사전으로 만들어 인수 전달 print("3~5=", calcstep(step = 1, end = 5, begin = 3))
사전은 원래 순서가 없는 집합이기 때문에 인수의 전달 순서는 상관 없으며, 함수 내부에서도 전달 순서를 알 수 없다. 즉 사전의 키로부터 인수값을 추출하여 사용할 수 있으면 된다.
위의 예제는 반드시 키워드 인수만 넘겨야 한다. 위치 인수를 넘기면 에러 처리가 된다. 또, 세 개의 인수를 다 사용하고 있으므로 세 인수를 모두 전달해야 한다.
위치 인수와 키워드 인수를 동시에 가변으로 취할 수도 있다. 이때는 위치 인수가 먼저 오고 키워드 인수가 뒤에 온다.
def calcscore(name, *score, **option): # 위치 인수, 가변 인수, 키워드 가변 인수 print(name) sum = 0 for s in score: sum += s print("총점 : ", sum) if(option["avg"] == True): # 키워드 가변 인수 사용하므로 값 넘어와야 함 print("평균 : ", sum / len(score)) else: print("평균 옵션이 없습니다.") calcscore("원", 88, 99, 77, avg = True) calcscore("엄", 100, 23, 55, 65, avg = False)
일반 인수인 name은 반드시 첫 번째 인수로 전달해야 하고, 가변 위치 인수 score는 점수의 개수가 몇 개든 상관 없다. 함수 호출문의 첫 번째 인수를 제외하고 이름 없는 인수가 튜플로 묶여서 score 인수로 전달된다. 마지막 option 가변 키워드 인수에서는 성적을 처리하는 방식을 지정하는 옵션값이 온다. 많은 옵션 포함 가능하며, 옵션의 이름과 값이 사전으로 묶여 전달된다.
즉, 위치 가변 인수와 키워드 가변 인수를 둘 다 받으면 임의의 모든 인수를 개수에 상관없이 다 받을 수 있다는 소리이다.
7.3 변수의 범위
값 저장 -> 변수
- 지역 변수(local var) -> 사용 범위 한정(특정 지역, 함수 내에서만)
함수 내부에 선언하는 변수를 지역 변수라고 한다.
def calcsum(n): # n은 호출할 때 초기화가 된다. sum = 0 # 지역변수 sum은 여기서 초기화 for num in range(n+1): sum += num return sum
위의 예에서 변수는 총 3개(n, sum, num)이다.
셋은 지역변수이므로 함수 밖에서 사용 불가하다.
def kim(): temp = "김과장의 함수" # 지역 변수 temp print(temp) kim() # 호출 print(temp) # 함수 밖 호출, error, NameError: name 'temp' is not defined # 이 시점에는 temp가 존재하지 않는다. # 함수가 끝나면 스택에서 pop되기 때문에
지역 변수의 사용 범위를 함수 내부로 제한하는 이유는 이름 충돌을 피하고 함수의 동작에 필요한 모든 것을 내부에 포함시켜 독립성을 향상시키기 위해서이다.
def kim(): temp = "감과장의 함수" print(temp) def lee(): temp = 2 ** 10 return temp def park(a): temp = a * 2 print(temp) kim() print(lee()) park(6) # call by value # 서로 다른 함수에 이름만 같은 다른 지역 변수들
위의 함수들 안의 temp들은 이름만 같은 각기 다른 지역 변수들이다.
지역 변수는 함수가 호출될 때만 잠시 생겼다 사라져 동시에 존재하지 않으며 사용 범위도 다르다. 그래서 다른 함수의 지역 변수와 이름 충돌을 걱정할 필요가 없다.
또한 지역 변수는 함수 동작에 필요한 모든 것을 내부에 소유할 수 있기 때문에 재활용성을 향상시킨다. 예를 들어, calcsum 내부에는 함수 동작에 필요한 지역 변수를 모두 포함하고 있어 함수 정의문만 복사하면 다른 프로젝트에서도 바로 사용 가능하다.
이외에도 지속 시간이 함수 실행 중으로 제한되어 필요할 때만 기억장소를 차지하므로 메모리를 절약한다. 지역 변수로 에러가 생겨도 사용 범위가 좁아 문제를 금방 찾을 수 있으며 디버깅이 쉽다.
- 전역 변수(global var)(탑 레벨에서 사용된 변수)
지역 변수와 반대로 함수 바깥에서 선언하는 변수를 전역 변수라고 한다. 지역 변수는 함수 소속이어서 함수 내부에서만 참조할 수 있는 반면에 전역 변수는 프로그램 소속이어서 어디에서나 참조할 수 있다.
물론 함수 안에서도 쓸 수 있다.
salerate = 0.9 def kim(): print("오늘 할인율 :", salerate) def lee(): price = 1000 # 지역변수 print("가격 :", price * salerate) kim() salerate = 1.1 lee()
kim() 함수 내에서 salerate를 읽기 위한 작업은 위와 같다. 함수 내부에서 변수 읽기는 먼저 스택을 보고 없으면 전역 파트를 본다.
함수에서 전역 변수를 읽는 행위는 문제가 없지만, 쓰는 데에는 주의가 필요하다. 함수부에서 처음 대입하는 변수는 항상 지역 변수로 간주된다. 파이썬은 명시적인 선언이 없고 대입에 의해 변수를 생성하다보니 초기화하는 장소에 따라서 범위가 결정된다.
price = 1000 # 전역 변수 price def sale(): price = 500 # sale 함수 내에서만 쓰이는 지역 변수 price # 첫 줄 price와 다른 변수이다. sale() # 바뀌었다고 생각했는데 print(price) # 예전 값 출력
함수 내 변수 쓰기 작업은 전역 변수를 덮어쓰지 않는다. 원칙으로 write 연산할 때는 지역 변수를 우선으로 새로 선언한다.
sale 함수를 호출하고 가격을 다시 출력했는데 이상하게도 가격은 500원이 되지 않고 1000원이다.
이렇게 되는 이유는 함수 내부에서 초기화하는 변수는 전역 변수와 이름이 같더라도 항상 지역 변수로 간주되기 때문이다. 그래서 sale 함수 내에서 선언한 price는 전역 변수 price와 아무 상관이 없는 별도의 지역 변수이다.
지역 변수와 전역 변수는 이름 같아도 다르다.
price = 1000 def sale(): price = 500 print("sale", id(price)) sale() print("global", id(price))
출력 내용 sale 2346670205136 global 2346670204912
id 함수는 변수의 고유한 식별자를 조사하는데 주로 값을 저장하는 주소를 리턴한다.
위의 예제를 통해 전역 변수 price와 지역 변수 price는 주소가 다름을 알 수 있다. 즉, 지역 price를 바꾼다고 해서 전역 price가 바뀌는 것은 아니다.
전역 변수와 같은 이름의 지역 변수를 선언할 수 없다면 함수 재활용을 할 때 전역 변수와의 이름 충돌이 빈번해진다. 예를 들어 calcsum 함수를 다른 프로젝트로 가져갔는데 그곳에서 sum이라는 전역 변수가 있다면 지역 변수 sum의 이름을 바꿔야 하는 문제가 있다.
그래서 지역, 전역끼리는 이름이 같아도 상관없되 함수 내부에서는 지역 변수가 우선이라는 규칙을 적용한다.
하지만, 함수 내에서 전역 변수를 사용하고자 할 때가 있다. 이럴 경우에는 global 명령을 사용한다.
price = 1000 def sale(): global price # 함수 내부에서 전역 변수를 사용하고 싶을 때는 global 명령어 사용 price = 500 sale() print(price)
하지만, 가급적 write를 할 때는(read는 상관 X) 지역 변수로 사용하는 것을 추천한다. 오류 전파가 쉽기 때문이다. 필요하다면 g_price 식으로 이름에 전역임을 표시하여 충돌을 원천적으로 방지하는 것이 합리적이다.
- docstring
함수의 재활용성을 높이려면 자세한 함수 사용법이나 인수의 의미, 주의 사항 등을 문서로 남겨야 한다. 이때 docstring을 사용한다.
def calcsum(n): """1 ~ n까지의 합계를 구해 리턴한다.""" # def 아래 docstring sum = 0 for i in range(n+1): sum += i return sum help(calcsum)
출력 화면 Help on function calcsum in module __main__: calcsum(n) 1 ~ n까지의 합계를 구해 리턴한다.
def 아래에 """ """를 사용해 문서화하고, 함수를 사용하는 다른 사람들을 위해 자세한 사용법을 함수 내부에 문서화해두는 것이 좋다. help(함수명)으로 도움말을 읽을 수 있다.
함수의 장점
- 반복을 제거한다.
- 수정하기 수월하다. (함수 내부만 수정하면 ok)
- 재사용이 쉽다.
- 복잡한 세부 논리를 숨겨 작업 자체에 집중할 수 있게 하므로 실수의 가능성을 줄여준다.
'Python' 카테고리의 다른 글
09. 리스트와 튜플 (0) 2021.01.20 08. 문자열 관리 (0) 2021.01.20 06. 반복문 (0) 2021.01.20 05. 조건문 (0) 2021.01.20 04. 연산자 (0) 2021.01.20