본문 바로가기
프로그래밍 기초 스터디/파이썬 - Python

파이썬 공부 ⑤ 함수, 변수의 범위(지역, 전역변수), 제너레이터

by 공부하는우니 2021. 6. 7.

함수는 너무나도 프로그래밍에서 자주 쓰이는 것이기 때문에, 파이썬에서만 특이한 점 몇가지 짚어보고 가겠습니다.

 

파이썬에서만 활용되는 함수의 특징 몇가지를 간단하게 요약하면

 

- 호출시 키워드 지정 가능 (호출시에는 키워드를 일부만 지정한다면 오른쪽에 몰아넣는다)

- 정의시 디폴트 인수도 일부만 지정하는 경우 오른쪽에 몰아넣는다

- 가인수에 *를 추가하면 튜플로, **를 추가하면 딕셔너리로 참조한다

- 람다함수로 간편하게 정의

- 함수 내에서 변수는 기본적으로 지역변수로 활용되며, 전역변수를 참조할 때엔 global이나 nonlocal로 미리 변수를 선언한다

 


함수의 정의

 

def 함수명(인수1, 인수2, ...):

    내용

    return 반환값(리턴값)

 

프로그래밍 상식으로 알아두면 좋을 내용인데,

함수 내부에서만 파라미터인 인수1, 인수2는 '가인수'라 칭하고,

실제로 함수를 사용할 때 가인수에 할당하는 인수를 '실인수' 라고 합니다.

 

def add(a, b):
    x = a + b
    return x

print(add(1, 2))

 

함수를 실제로 실행하는 add(1, 2)에서 1, 2가 각각 실인수, 함수가 호출되었을 때 변수로서 지정하는 a, b를 각각 가인수라고 하는거죠. 여태까지 의미는 알지만 정확한 용어는 모르고 있었는데 하나 배웠네요!

 

다른 여러 프로그래밍에서도 그렇지만, 파이썬에서도 비슷한 형태로 함수를 정의합니다.

위의 빨간, 파란색 글자만 봐도 어느정도의 내용은 파악하실 수 있을텐데,

일반적인 프로그래밍 언어와 마찬가지로

 

- 함수명 지정과, 함수를 정의하겠다고 하는 의미인 def는 필수입니다. (콜론 : 까지)

- 당연히 인수는 꼳 받을 필요가 없으며

- 리턴값도 당연히 꼭 줄 필요는 없습니다

 

특이한 것 몇가지를 살펴보면,

 

 

아무 것도 없는 함수 pass

def add(a, b):
    pass

 

아무 내용도 없는 함수를 정의할 때 pass키워드를 사용합니다. (어디에 쓰는지 아직 모르겠네요)

 

 

키워드를 이용한 함수 호출

def divide(a, b):
    return a/b

def test(a, b, c):
    print(a, b, c)


def test2(a, b, c, d):
    print(a, b, c, d)

print(divide(1, 2))
print(divide(b=2, a=1))
test('a', 'b', 'c')
test(a='a', b='b', c='c')
test('a', c='cc', b='bb')
test('aaa', 'bbb', c='cc')
# test('aaa', b='bbb', 'ccc') #에러
# test(a='aaa', 'bbb', 'ccc') #에러


########## 결과
0.5
0.5
a b c
a b c
a bb cc
aaa bbb cc

 

보통의 프로그래밍 언어(ex, C)에서는 함수를 호출할 때 가인수를 함수를 정의하였을 때 지정한 순서대로 입력합니다.

파이썬에서도 마찬가지입니다만, 추가적으로 키워드를 이용해서 사용자가 원하는 순서대로 가인수를 할당할 수 있습니다.

 

다만, 키워드를 일부만 할당할 경우

- 할당하지 않는 키워드에 해당하는 실인수는 순서대로 입력됩니다.

- 할당하지 않는 키워드에 해당하는 실인수는 모두 왼쪽에 배치합니다.

ex) test('a', b='b', c='c') 처럼 할당하지 않는 실인수를 왼쪽에 배치해야 합니다.

 

 

 

인자를 튜플로 받기 (*)

def sum(*a):
    sum = 0
    for i in a:
        sum+=i
    return sum

print(sum(1, 2, 3, 4))


########## 결과
10

 

가인수 앞에 *를 붙이면 인수를 튜플로 받을 수 있게 됩니다.

 

 

인자를 딕셔너리로 받기 (**)

def sum(**a):
    sum = 0
    for i in a.values():
        sum+=i
    return sum
    
def sum2(a, b, c, d):
    return a+b+c+d
    
print(sum(a=1, b=2, c=3, d=4))
dict = {'a':1, 'b':2, 'c':3, 'd':4}
print(sum2(**dict))

########## 결과
10
10

 

- 가인수 앞에 **를 붙이면 인수를 딕셔너리로 받을 수 있게 됩니다.

- 함수 호출 시 **를 붙이면 딕셔너리를 전개하여 받을 수 있습니다.

 

 

디폴트 인수

def add1(a=10, b=10, c=10):
    return a+b+c

def add2(a, b=10, c=10):
    return a+b+c

# add3 에러
# def add3(a=10, b, c):
#     return a+b+c

print(add1())
print(add2(1))

########## 결과
30
21

 

가인수에 디폴트 값을 설정할 수 있습니다. 인수를 따로 지정하지 않으면 디폴트 값으로 실인수를 설정합니다.

- 다만 디폴트 인수는 가인수를 정의할 때 오른쪽부터 설정할 수 있습니다.

ex) 위의 add3처럼 디폴트 인수를 왼쪽에만 지정하는 것이 불가능합니다.

 

 

함수 오브젝트

def longlonglonglongfunction(a):
    print(a)

short = longlonglonglongfunction

short('print')


########## 결과
print

 

함수를 호출할 때는 가인수가 없는 경우에도 ()를 작성하였습니다만, ()를 붙이지 않는 경우 변수로서 활용 할 수 있습니다 (함수 자체를 나타냄)

 

 

함수의 네스트(로컬 함수)

def test(a, b):
    def add(c, d):
        return c+d
    print(add(a, b))
    
print(test(1, 2))
# print(add(1, 2)) # 오류


########## 결과
3

 

함수 내부에 함수를 또다시 정의할 수 있습니다. 당연히 이 경우 함수 내부에서(test 내부에서) 만 호출이 가능합니다.

 

 

익명함수 (람다함수)

def add1(a, b):
    return a+b

add2 = lambda a, b : a+b

print(add1(1, 2))
print(add2(3, 4))

########## 결과
3
7

 

람다함수명 = lambda 인수 : 리턴값

 

으로 함수를 간단하게 정의할 수 있습니다.

 

* 매우 자주 사용되므로 꼭 체크

 

 

콜백 함수(Callback)

def test(a, b, callback):
    print (callback(a, b))

def add(a, b):
    return a+b

def minus(a, b):
    return a-b

test(1, 2, add)
test(1, 2, minus)
test(1, 2, lambda a, b : a+b)
test(1, 2, lambda a, b : a-b)


########## 결과
3
-1
3
-1

 

굳이 언급하지 않아도 자연스럽게 알게 될 내용이지만, 용어를 정리하는 차원에서 언급하는 내용입니다.

위의 테스트 예제에서 test함수의 실인수로 callback이라는 이름의 함수를 가인수로 받습니다 (add, minus)

이처럼 함수를 정의할 때, 함수를 인자로 받을 수 있는데, 그때 인자로 받는 함수를 "콜백함수" 라 정의합니다.

 

- 이 때, 함수를 따로 정의하여도 되지만 람다함수를 이용하여 간편하게 기술하는 것도 가능합니다.

 

 

변수의 범위 : 지역변수(로컬 변수), 전역변수(글로벌 변수)

a = 0
b = 10
c = 100

def test1():
    a = 1
    print(a)  # 바로 위의 a=1 지역변수가 출력됨

    global b # b는 전역변수로 활용하겠다는 선언 (함수 내에서)
    b = 20 # 전역변수 b를 변경함
    print(b) # 전역변수 b

    def test2():
        c = 2
        print(c) # 바로 위의 c=2 지역변수가 출력됨
        global c
        print(c)
    test2()

print(a) # 1번째줄의 전역변수 a = 0이 출력됨
print(b) # 함수 내에서 b=20이 적용되기 전 전역변수 b=10이 출력됨
test1()
print(b) # 함수 내에서 b=20으로 변경됨



########## 결과
0
10
1
20
2
20

 

- 기본적으로 파이썬에서는 함수 내부에서만 활용되는 지역변수 개념을 이용합니다.

- 함수 밖에 정의된 전역변수는 함수 내에서 활용할 수 있지만, 같은 이름을 같는 변수를 함수 내에서 새로 정의한다면, 지역변수로만 참조할 수 있습니다.

- 함수 밖에 정의된 전역변수를 참조하기 위해선, global 혹은 nonlocal을 사용합니다.

- global 이나 nonlocal을 사용할 때엔, 전역변수를 함수 내에서 사용하기 전에 반드시 먼저 선언합니다.

ex) test2 내부에서처럼 c=2 라고 지역변수를 전역변수와 같은 이름으로 선언하고 사용한 이후에 global c로 전역변수로 다시 사용하고자 할 경우 에러가 발생합니다.

 

 

제너레이터(Generator)

def gen(maximum):
    for i in range(maximum):
        print('check')
        yield i
        print('check2')

generator = gen(5)
print(next(generator)) # check, 0
print(next(generator)) # check2, check, 1
print(next(generator)) # check2, check, 2
print(next(generator)) # check2, check, 3
print(next(generator)) # check2, check, 4
# print(next(generator)) # 오류

######### 결과
check
0
check2
check
1
check2
check
2
check2
check
3
check2
check
4

 

제너레이터는 한글로 명확하게 정의하기어렵지만, 반복되는 내용(iterable)을 생성해주는 함수입니다.

딥러닝에서 미니배치(mini batch)를 구현할 때 매우 자주 사용되므로 꼭 알아두는 것이 좋겠네요.

 

정의는 

 

def 함수명(인수):

    함수내용 1

    yield 리턴값

    함수내용 2

 

사용할 함수명 = 함수명(인수)

next(사용할 함수명)

 

일반적인 함수와의 차이점이라면, 함수 내부에 return 대신 yield가 사용된다는 점.

함수를 호출한 이후 next로 다시 호출한다는 점 입니다.

 

위의 예시 코드를 본다면 바로 이해하실 수 있을텐데요,

 

generator = gen(5) 로 맨 처음 함수를 호출한 경우 아무런 일이 일어나지 않습니다 (실제로는 그렇지 않지만 눈에 보이지만 않을 뿐입니다)

이후 next(generator)로 다시 호출하는 경우 실질적으로 제너레이터 함수 내부에서 함수가 yield까지 진행됩니다.

그 이후 next(generator)로 다시 호출하면 yield 이후의 구문이 실행되어 다시 yield가 나올 때 까지 진행됩니다.

 

일반적인 함수처럼 return을 사용하면 되지 않나요? 라고 한다면 당연히 답은 Yes입니다. 당연히 제너레이터와 같은 출력을 주는 함수를 만들 수 있습니다.

 

다만 제너레이터를 사용하는 이유는, 눈에는 보이지 않는 이유 때문입니다.

일반적인 함수를 사용하면, 함수는 호출 이후에 함수에서 사용된 내용을 메모리에서 모두 클리어하게됩니다.

하지만 제너레이터를 사용하면 함수가 호출되었을 때의 내용을 메모리상에 저장한 채 그대로 다시 호출할 수 있게 되는 점이 가장 큰 차이점입니다.

 

 

댓글