7장. 함수(function)-2

asda주asda·2022년 1월 19일
0

Python

목록 보기
9/31

참조값에 의한 인수 전달

변수전달

함수를 호출될 때 함수는 변수의 인자값만을 복사해서 전달받는다. 거의 모든 현대적인 프로그래밍 언어에서는 '변수의 값'만이 복사되어 전달된다. 이것을 값에 의한 호출(call-by-value)라고 한다. 값에 의한 전달(pass-by-value)도 동일한 의미이다.
예를 들면 다음과 같다.

def modify(n) :
	n=n+1

k = 10
print("k=", k}
modify(k)
print("k=", k)
결과: 
k = 10
k = 10

위의 코드에서 k의 값은 함수로 전달되었지만 k의 값은 호출 후에도 변경되지 않았다.
이것이 값에 의한 호출(call-by-value)이다.

문자열 전달

문자열 전달에서도 마찬가지다.

def modifyl(s):
	s += "To You“

msg = "Happy Birthday"
print("msg=", msg)
modifyi(msg)
print("msg=", msg)
결과:
msg = Happy Birthday
msg = Happy Birthday

이러한 이유는 숫자나 문자열이 변경 불가능한 객체(immutable object)이기 때문이다. 숫자나 문자열을 변경하게 되면 새로운 객체가 생성된다.

이를 확인하기 위해 참조값(객체의 주소)를 출력하는 id()함수가 있다. 이를 통해 문자열을 변경하면 문자열의 참조값이 어떻게 변하는지 확인할 수 있다.

>>> msg = "Happy Birthday"
>>> id(msg)
50871576 <-해쉬코드
>>> msg += "To You"
>>> id(msg)
50849984

해쉬코드: 객체를 힙이라는 영역에 16진수(0x...)의 형태로 메모리의 주소가 10진수 형태의 정수값으로 변환된다. 객체를 구별하는 유니크한 정수값

위의 코드에서 알 수 있드시 문자열을 조금이라도 변경시키면 객체의 주소는 바뀐다.

리스트 전달

def modify2(li):
	li += [100, 200]

list = [1, 2, 3, 4, 5]
print(list)
modify2(list)
print(list)

결과: 
[1,2,3,4,5]
[1,2,3,4,5,100,200]

리스트는 앞의 변경 불가능한 객체(문자열, 숫자)와 다르게 변경이 가능한 객체(mutable object)이다. 즉 위의 예제에서 함수에 리스트를 전달하면 아래의 그림과 같이 리스트의 원본에 영향일 끼친다.

리스트는 주소들의 주소를 가진다. 리스트들의 요소들은 각각의 주소를 가진다. 그리고 리스트는 그 주소들의 주소를 가진다. 그리고 그 주소들의 주소를 함수에 전달한다.(C언어로 치면 2중 포인터이다.) 이와 같이 함수에 주소를 전달하는 방식을 call-by-reference 라고 칭한다.

정리하면 다음과 같다.

리스트의 경우에 리스트의 참조값(주소)이 전달된다. 함수에서 참조값을 이용하여 리스트를 변경하면 리스트는 변경 기능하기 때문에 새로운 객체를 생성하지 않고 기존의 객체가 변경되는 것이다.

call-by-objective

def change(str):
    print("함수 내 전 :", str)
    print("함수 내 후주소:", id(str))
    str += "공부합니다."
    print("함수 내 후:",str)
    print("함수 내 후주소:",id(str))

msg = "안녕 나는 "
print("호출전 msg:"+msg)
print("호출 전 주소:",id(msg))
change(msg)
print("호후 msg:"+msg)
print("호출 후 주소:",id(msg))

출력 결과:
호출전 msg:안녕 나는 
호출 전 주소: 3031075524368
함수 내 전 : 안녕 나는 
함수 내 후주소: 3031075524368
함수 내 후: 안녕 나는 공부합니다.
함수 내 후주소: 3031075687536
호후 msg:안녕 나는 
호출 후 주소: 3031075524368

출력 결과를 보면 함수에 전달되는 것이 정확하게는 '주소값'이다. 하지만 그 주소값의 내용(문자열, 숫자)에 조금이라도 수정(연산)된다면 새로운 객체를 만들어 그 주소값을 가진다.
파이썬의 경우 타 언어의 call-by-value의 개념과는 조금 다르다. 그 이유는 파이썬은 모든 것을 '객체'로 판단하기 때문이다. 이러한 파이썬의 특성이 존재하므로 call-by-objective라고도 칭한다. (C언어에서는 값이 복사만 될 뿐이다.)
또한 리스트 같은 경우는 함수에 수정을 가해도 새로운 객체를 만드는 것이 아니기에 call-by-reference in objective 라고 칭할 수 있다.

지역 변수와 전역 변수

지역 변수와 전역 변수

파이썬에는 지역 변수와 전역 변수가 존재한다. 지역 변수는 함수 안에서 선언된 변수이며 전역 변수는 함수 외부에서 선언된 함수이다.

지역 변수(local variable)

함수 안에 정의된 변수는 지역 변수(local variable)라고 불리며 함수 내에서만 사용이 가능하다. 지역 변수는 함수가 호출될 때, 생성되고 함수가 종료되면 소멸되어서 더 이상 사용할 수 없다. 이것을 변수의 영역(scope)이라고 부른다. 함수의 매개변수도 지역변수이다.

예를 들면 다음과 같다.

def sub():
     s = "나는 치킨이 조앙!"
     print(s)

sub()
출력 결과:
나는 치킨이 조앙!

위의 함수 sub에서 문자열을 담고 있는 지역 변수s가 생성되었다. 하지만 함수를 벗어나게 되면 지역변수 s는 소멸된다.

def sub():
	s = "바나나가 좋음!"
	print(s)

sub()
print(s)
출력 결과:
...
NameError: name 's’ is not defined

만일 지역변수 s를 함수 바깥쪽에서 출력을 한다면 다음과 같이 's'가 정의되지 않았다며 오류가 발생한다. 이를 통해 앞에서 말해왔던 지역 변수는 함수 내에서만 사용가능하며 함수가 끝날 시 자동으로 소멸됨을 알 수 있다.

전역 변수(global variable)

함수의 외부에 정의된 변수를 전역 변수(global variable)라고 한다. 파이썬에서 다루는 전역 변수는 타 언어에 비해 간단한 방식이다. 파이썬에서는 다른 이야기가 없으면 함수에서 선언된 변수는 무조건 지역 변수이다.

def sub():
     print(s)

s = "나는 치킨이 조아!"
sub()

출력결과:
나는 치킨이 조아!

위의 코드에서 변수 s는 함수 외부에서 선언된 전역 변수이다. sub()을 호출하기 전에 선언 되었으며 함수 sub()는 호출되기 전에는 실행되지 않는다. 위의 코드에 알 수 있드시 전역 변수는 함수 내에서 별다른 선언없이 활용이 가능하다.

만일 전역 변수 s를 함수 내에서 값을 변경하는 것이 가능할까? 아래의 코드를 통해 알 수 있다.

def sub():
     s = "치킨 별로 맛없성!"
     print(s)

s = "나는 치킨이 조아!"
sub()
print(s)

출력 결과:
치킨 별로 맛없성!
나는 치킨이 조아!

위의 코드의 함수에서 변수 s에 값을 저장하면 파이썬은 우리가 지역 변수 s를 정의한 것으로 간주한다. 따라서 함수 안의 s는 전역 변수로 간주하는 것이 아닌 지역 변수로 간주하는 것이다.
중요한 파이썬의 원칙중 하나는 다음과 같다.

함수 안에서 변수에 어떤 값을 저장하면 무조건 지역변수로 간주한다.
디폴트(default)가 지역 변수 인 것이다.

만일 함수 내에 전역 변수를 활용하고 그 변수를 지역 변수로 바꾸는 것이 가능할까?

def sub():
     print(s)
     s = "치킨 별로 맛없성!"
     print(s)

s = "나는 치킨이 조아!"
sub()
print(s)

출력 결과:
UnboundLocalError: local variable 's' referenced before
assignment

이 때의 경우는 오류가 발생한다. 함수 내에서 전역 변수의 값을 바꿔 활용하는 것은 불가하다.
하지만 global키워드를 사용한다면 가능하다. 이는 파이썬 인터프리터에게 전역 변수를 함수 내에서 활용하겠다고 전달하는 키워드이다.

def sub():
     global s
     print(s)
     s = "치킨 별로 맛없성!"
     print(s)

s = "나는 치킨이 좋아!"
sub(s)
print(s)

출력 결과:
나는 치킨이 좋아!
치킨 별로 맛없성!
치킨 별로 맛없성!

정리하면 다음과 같다.

  • 'global키워드'를 통해 전역 변수의 값을 함수 내에서 변경이 가능하다.
  • 파이썬에서는 변수에 값을 할당한다는 것 자체가 변수의 선언과 동일한 것이다.
  • 만일 'global키워드'를 사용하지 않고 파이썬의 함수 안에 변수의 값을 저장하면 기본적으로 지역 변수가 선언 된다.

전역 변수와 지역 변수를 활용해 프로그램을 다음과 같은 프로그램을 만들 수 있다. 이 예제를 이해할 필요가 있다.

def sub(x, y):       # 함수의 매개 변수도 지역 변수의 일종이다.
	global a      #함수 안에서 전역 변수 a를 사용하겠다는 의미.
	a=7 
	x, y = y, x
	b=3
	print(a, b, x, y)

a, b, x, y = 1, 2, 3, 4
sub(x, y)
print(a, b, x, y)

출력결과:
7343
7234

매개변수와 지역변수의 관계

함수가 외부로 부터 값을 전달받는데 사용되는 매개변수도 일종의 지역변수이다.
매개변수는 함수의 선언부에 존재하며 함수가 호출될 때 비로소 메모리에 할당된다.

def test(list):
    list += [4,5]
    print(list)
    list = [4,5]
    print(list)

list = [0,1,2,3]
test(list)
print(list)

출력결과:
[0, 1, 2, 3, 4, 5]
[4, 5]
[0, 1, 2, 3, 4, 5]

위의 코드에서 함수의 list += [4,5]는 함수 외의 list의 참조값을 받아 그 원형에 4,5를 추가하는 것이다. 그리고 그 다음의 list = [4,5]는 그 원형의 리스트에 4,5로 덮는 것이 아닌 지역 변수 list를 선언하여 거기에 4,5를 넣는것이다.

즉 함수가 시작될 때는 전역변수 list와 매개변수 list는 동일한 리스트의 참조값을 가진다. 하지만 함수 안에서 매개변수 mylist에 다른 리스트를 할당하면 전역변수와는 다른 리스트를 가리킨다.

여러 개의 값 반환하기

파이썬은 다른 언어들과 다르게 return 반환값으로 여러 개의 값을 가질 수 있다.
엄연하게는 튜플(tuple)이라는 값 하나만 반환한다.

def sub():
	return 1, 2, 3
a, b, c = sub()
print(a, b, c)

출력 결과:
123

위의 코드에서 여러 개의 값이 반환되는 원리는 다음과 같다.
return 1,2,3은 retrun (1,2,3)과 같다. 즉 1, 2, 3은 튜플(tuple) (1, 2, 3)과 같다.
결국 튜플 1개가 반환되고 이 튜플을 받아서 변수 a b c에 값을 풀어서 저장하는 것이다.

튜플(tuple)은 몇 가지 점을 제외하곤 리스트와 거의 비슷하며 다른 점을 다음과 같다.

  • 리스트는 [ ]으로 둘러싸여 있지만 튜플은 ( )으로 둘러싼다.
  • 리스트는 그 값을 생성, 삭제, 수정이 가능하지만 튜플은 그 값을 바꿀 수 없다.

무명 함수(람다식)

무명 함수

무명 함수는 이름은 없고 몸체만 가지는 함수이다. 파이썬에서의 무명 함수는 lambda 키워드로 만들어진다.
무명 함수는 여러 개의 인수를 가질 수 있으나 반환값은 하나만 가지고 있어야 한다. 무명 함수에서는 print()를 호출할 수 없으며 계산만 가능하다. 또 자신만의 이름 공간을 가지고 있고 전역변수를 참조할 수 없다.

# 무명 함수를 활용한 덧셈 
sum = lambda x, y: x+y

print(sum(1,2))

# 일반 함수를 활용한 덧셈
def get_sum(x,y):
     return x+y
     
print(sum(1,2))

위의 코드에서 get_sum()과 sum()은 동일한 작업을 하며 동일한 방식으로 사용할 수 있다. 람다 함수에서는 return 키워드를 사용할 필요가 없다. 람다 함수에서는 항상 반환되는 수식만 써 주면 된다.
함수를 필요로 하는 곳에 람다 함수를 놓을 수 있으며 람다 함수를 반드시 변수에 할당할 필요도 없다.

그렇다면 람다 함수는 어디에 사용되는 것일까? 람다 함수는 코드 안에 함수를 포함하는 곳에서 어디든지 사용될 수 있다. 하지만 통상적으로 GUI 프로그램에서 이벤트(사건)를 처리하는 콜백 함수(callback handler)에서 많이 사용된다. 콜백 함수를 간단하게 람다 함수로 작성하여서 포함시키는 것이다.

# 람다함수를 출력을 하면 결과론으로 함수 객체를 출력하는 형태가 된다.
print(lambda x, y: x+y)
# 아래 코드가 실질적으로 람다식을 이용한 무명함수를 직접 호출한 형태
print((lambda x, y: x + y)(10,50))

출력결과:
<function main.<locals>.<lambda> at 0x00000202E4AA2670>
60

'무명함수' 그 자체를 출력하면 메모리 주소와 함께 무명 함수를 출력한다. 이 무명함수의 매개변수 괄호를 이용해 값을 대입해 사용할 수 있다. 아래는 이를 이용해 무명함수 리스트를 만들어 활용한 예제이다.

# 리스트안에 람다함수가 있는 형태
L = [  lambda x: x ** 2,
        lambda x: x ** 3,
        lambda x: x ** 4 ]

for f in L:
     print(f(3))

출력 결과:
9
27
81

모듈이란?

모듈의 개념

파이썬에서는 파일에 함수들을 저장하고 인터프리터에서 사용하는 방법을 제공한다. 이러한 함수나 변수들을 모아 놓은 파일을 모듈(module)이라고 한다. 모듈 안에 있는 함수들은 import 문장으로 다른 모듈로 포함될 수 있다.
모듈 중에서 main 모듈은 최상위 수준에서 실행되는 스크립트를 의미한다.

모듈을 사용하는 가장 큰 이유는 '관리'이다.
프로그램이 길어지면, 유지 보수를 쉽게 하기 위해 여러 개의 파일로 분할할 수 있다. 또한 파일을 사용하면 한번 작성한 편리한 함수를 복사하지 않고 여러 프로그램에서 사용할 수 있다.

모듈의 활용

파일 이름은 파이썬 모듈 이름에 .py 확장자를 붙이면 된다. 모듈 안에서는 모듈의 이름은 __name__의 값(문자열)으로 접근이 가능하다. 예를 들어 fibo.py 파일에 텍스트 편집기를 사용하여 다음과 같은 내용을 저장하였다고 하자.

<fibo.py>

# 피보나치 수열 모듈

def fib(n):    # 피보나치 수열을 화면에 출력한다. 
    a, b = 0, 1
    while b < n:
        print(b, end=' ')
        a, b = b, a+b
    print()

<모듈 사용법>

# 방법1: import ~~~
# 이 방법의 장점은 어느 파일의 어떤 함수인지 직관적으로 파악하기 용이하다.
import fibo

fibo.fib(1000)
1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987

print(fibo.__name__)
'fibo’

# 방법2: from ~~~ import *
fib(500)
1 1 2 3 5 8 13 21 34 55 89 144 233 377

print(fibo.__name__)
'fibo’

__name__

__name__이라는 변수는 원레 부터 파이썬에 포함되어 있는 변수, 즉 내장 전역 변수이다. 이 변수에는 해당 파이썬파일(.py)의 이름, 즉 모듈의 이름을 담게 된다.
예를 들어 파일의 이름이 fuction.py이면 __name__은 fuction을 담게된다.

그런데 이 함수를 '그 파일 내에서' 위의 예시로한다면 fuction.py 내에서 실행시킨다면 __name__은 무조건 __main__을 담게 된다.

반면 다른 .py파일에서 fuction.py 파일을 import해서 fuction.__name__을 한다면 name에는 fuction이란 이름 값이 담기게 된다.

보통 프로그램의 시작점을 지정해주기 위해 밑의 코드를 작성한다.

def main():
## 몸체

if __name__ == "__main__":
main()

참조: https://lovelydiary.tistory.com/297

함수를 사용한 프로그램 설계

한 대의 자동차를 만들기 위해서는 수백 개의 협력업체에서 부품을 공급해야 한다. 이들 부품을 자동차 공장에서 조립하면 자동차가 생산된다. 이와 같은 원리를 프로그램에 대해서도 적용할 수 있다.

윈도우나 한글과 같은 커다란 프로그램의 모든 코드가 하나의 함수 안에 들어 있다고 가정해보자. 흔히 대형 프로그램의 코드는 줄이 수만 라인이 넘는다. 이것이 하나의 함수 안에 들어 있다면 코드를 작성한 사람도 시간이 지나면 이해하거나 디버깅하기가 어려울 것이다.

이에 대한 문제의 해결책은 코드들을 작은 조각으로 분리하는 것이다. 파이썬에서는 작은 조각이 함수에 해당한다. 복잡하고 규모가 큰 프로그램은 여러 개의 함수로 나누어서 작성되어야 한다.

먼저 주어진 문제를 분석한 후에, 보다 단순하고 이해하기 쉬운 문제들로 나누게 된다. 문제가 충분히 작게 나누어지면 각 문제를 해결하는 절차를 함수로 작성한다.
문제를 한 번에 해결하려고 하지 말고 더 작은 크기의 문제들로 분해한다. 문제가 충분히 작아질 때까지 계속해서 분해한다.
문제가 충분히 작아졌으면 각각의 문제를 함수로 작성한다. 이들 함수들을 조립하면 최종 프로그램이 완성된다.

함수들은 특징적인 한 가지 작업(기능)만을 맡아야 한다. 하나의 함수가 여러 가지 작업을 하면 안 된다. 다른 것과 구별되는 한가지의 작업만을 하여야 한다. 만약 함수 안에서 여러 작업들이 섞여 있다면 각각을 다른 함수들로 분리하여야 한다. 이런 식으로 함수를 사용하게 되면 함수들을 작업별로 분류할 수 있어서 소스 코드의 가독성이 높아진다.

C언어 또는 자바와 같은 프로그래밍 언어에서는 항상 main() 이라는 함수를 시작으로 프로그램을 실행시킨다.

하지만 파이썬에는 두 가지 특징이 있다.

  • 들여쓰기를 통해 코드의 실행의 레벨을 결정한다. 들여 쓰기를 하지 않은 부분이 레벨이 높다.
    • 코드를 실행할 때 레벨에 따라 실행한다
  • main함수가 존재하지 않는다.

이에 프로그래머들은 프로그램의 시작점을 결정하기 위해 __name__을 이용한다.

모듈에 if __name__=="__main__"이라는 조건문을 넣어주고 그 아래는 직접 실행시켰을 때만 실행되길 원하는 코드들을 넣어주는 것으로 생각하면 쉬울 것이다.

예를 들면 다음과 같다.
다음은 여러명의 성적을 받아 오름차순으로 출력하는 프로그램이다.

def readList():
	nlist = []
	flag = True;
	while flag :
		number = int(input("숫자를 입력하시오: "))
		if number < 0:
			flag = False
		else :
			nlist.append(number)
	return nlist

def processList(nlist):
	nlist.sort()
	return nlist


def printList(nlist):
	for i in nlist:
		print("성적=", i)

def main():
	nlist = readList()
	processList(nlist)
	printList(nlist)
# 만일 이 파일이 실행된다면 __name__은 __main__의 값을 가지기에 main()함수가 실행된다.
if __name__ == "__main__":     # 프로그램 시작점
	main()        

## 출력결과
숫자를 입력하시오: 30
숫자를 입력하시오: 50
숫자를 입력하시오: 10
숫자를 입력하시오: 90
숫자를 입력하시오: 60
숫자를 입력하시오: -1
성적= 10
성적= 30
성적= 50
성적= 60
성적= 90

0개의 댓글

관련 채용 정보