CHAPTER 13 클래스와 객체

유동헌·2021년 9월 23일
0

열혈파이썬_기초편

목록 보기
13/14

01 전역변수와 지역변수

변수의 선언 위치가 중요함

변수를 다음과 같이 두 가지로 나눠 볼 수 있다

  1. 함수 안에 선언되는 함수 → 지역변수
  2. 함수 밖에 선언되는 변수 → 전역변수

다음 예에서 보이듯이, 함수 안에 선언된 함수 안에서만 접근이 가능하다. 물론 이는 매개변수도 마찬가지. 매개변수도 함수 안에서만 접근 가능하다. 즉 지역변수의 범주에 매개변수도 포함이 된다.

def func(n): # 매개변수 n도 지역변수 범주에 포함된다 (지역변수의 일종이다)
    lv = n + 1 # 지역변수 lv의 선언, 그리고 매개변수 n에 접근
    print(lv) # 지역변수 lv에 접근
    
func(12)

# 출력
13

📌 함수 밖에서 변수에 접근하는 모습

def func(n):
    lv = n + 1
    print(lv)
    
print(lv)

📌 변수에 접근하지 못함!

사실 지역변수는 함수 내에서 만들어졌다가 함수를 벗어나면 사라지는 변수이다. 따라서 함수 밖에서 접근이 가능할 리 없다.

>>> count = 100
>>> count += 1
>>> def func():
...     print(count)
...
>>> func()
101

📌 반면 함수 밖에서 선언되는 전역 변수는 선언된 이후 어디서든 접근이 가능.

cnt = 100

def func():
    cnt = 0
    print(cnt)

func()
print(cnt)

📌 이 경우에는 "지역변수 cnt을 선언하고 여기에 0을 저장한다" 라는 뜻이 되고 변수를 하나 더 만든 셈이 된다.

전역변수와 동일한 이름의 지역변수가 만들어지면 함수 내에서는 지역변수에만 접근을 하게 된다.

cnt = 100

def func():
    global cnt
    cnt = 0
    print(cnt)
    
func()
print(cnt)

📌 원하는 바가 지역변수의 선언이 아니라 전역변수에 0을 저장하는 것이라면, global로 시작하는 선언을 해주면 된다.

함수 내에서 전역변수에 접근하는 상황에서는 global 선언을 해야한다.

02 객체지향 프로그래밍

클래스와 객체를 이해하기 전에 가장 먼저 등장하는 개념이 객체지향

OOP와 상관없이 내게 필요한 클래스를 만들고 또 객체를 만들 수 있어야 한다.

03 클래스와 객체 이전의 프로그램에 대한 반성

아빠의 나이를 관리하는 간단한 프로그램이 필요하다가 가정하자.

fa_age = 39

def up_fa_age():
    global fa_age
    fa_age += 1
    
def get_fa_age():
    return fa_age

def main():
    print("2019년...")
    print("아빠", get_fa_age)
    print("2020년...")
    up_fa_age()
    print("아빠", get_fa_age)
    
print(main())

# 출력
2019...
아빠 <function get_fa_age at 0x7f92080e8820>
2020...
아빠 <function get_fa_age at 0x7f92080e8820>

📌 이렇게 작성을 할 수 있는데, 객체로 나온다.

🔥 방법을 모르겠다

이렇듯 함수를 만들어서 전역변수에 접근하는 방식은(전역변수에 직접 접근하지 않고) 코드를 더 안정적이고 이해하기 쉽게 만든다.

fa_age = 39

def up_fa_age():
    global fa_age
    fa_age += 1
    
def get_fa_age():
    return fa_age

mo_age = 35

def up_mo_age():
    global mo_age
    mo_age += 1
    
def get_mo_age():
    return mo_age

def main():
    print("2019년...")
    print("아빠", get_fa_age)
    print("2020년...")
    up_fa_age()
    up_mo_age()
    print("아빠", get_fa_age)
    print("엄마", get_mo_age)
    
print(main())

📌 엄마를 추가했을 때 이렇게 코드가 늘어난다. 그럼 가족 모두를 추가한다면? 많이 늘어난다.

04 클래스와 객체의 이해

똑같은 객체를 만들어 낼 수 있는 설계도 = 클래스

파이썬 기반으로 우리가 원하는 형태의 객체를 만들기 위해서는 먼저 그 객체의 설계도에 해당하는 클래스라는 것을 만들어야 한다. 다시 말해서 설계도에 해당하는 클래스를 정의해야 한다.

앞서 우리는 아빠의 나이 정보 관리를 위해서 다음 이름의 변수와 함수들을 만들어 보았다.

fa_age
up_fa_age(), get_fa_age()

그리고 엄마의 나이 정보 관리를 위해서 위의 세 가지를 한 세트 더 추가하였다. 즉 관리하고자 하는 사람의 수가 늘 때마다 위의 세 가지를 매번 추가해야 한다. 따라서 위의 내용을 모두 담은 클래스를 만들어서 이 문제를 해결하고자 한다. 일단 클래스 안에 담을 변수의 이름을 다음과 같이 수정하겠다(이 클래스가 아빠 나이만을 대상으로 하지 않으므로)

fa_age => age

up_fa_age => up_age()

get_fa_age => get_age()

그리고 앞서 정의한 함수 up_fa_age()는 다음과 같은데,

def up_fa_age():
	global fa_age
	fa_age() += 1

이것을 클래스라는 설계도에 포함시키려면 다음과 같이 수정해야 한다.

def up_age(self): # self가 왜 등장을 했는지는 아직 모름. 단 self는 매개변수임
	self.age += 1 # age가 아니라 self.age

일단 global 선언은 필요 없다. 클래스 안에 담길 변수 age는 전역변수가 아니기 때문이다. 그리고 self라는 매개변수가 등장했다. 솔직히 이 부분이 신경 쓰일 텐데 이것이 필요한 이유에 대해서는 잠시 후에 설명하기로 하고, 이어서 다음 함수도 클래스에 포함시키기 위한 형태로 수정해 보겠다.

def get_fa_age():
	return fa_age

def get_age(self): # 왜? 모름. 매개변수라는 것까지만 알고 있음. 
	return self.age # 여기도 아직 모름. 

self라는 매개변수의 등장만 아니면 이전 함수와 차이가 없다. 자, 그럼 이 두 함수를 클래스에 담아보겠다. 클래스의 이름을 AgeInfo라고 하겠다.

class AgeInfo: # 클래스 정의
	def up_age(self): # 클래스 안에 담긴 up_age 함수 
		self.age += 1 
	def get_age(self): # 클래스 안에 담긴 get_age 함수
		return self.age

# 만약 self가 없다면?

class AgeInfo: # 클래스 정의
	def up_age(): # 클래스 안에 담긴 up_age 함수 
		age += 1 
	def get_age(): # 클래스 안에 담긴 get_age 함수
		return age

# 기존 함수와 같은 모양이다.

📌 이 클래스의 함수를 호출할 때, self라는 매개변수가 없다고 생각하고 함수를 호출해야 할 것이다.

변수 age는? 그런데 파이썬이 알아서 넣어준다(실제와는 차이가 있는 설명. 지금은 일단 이렇게 이해하자) 위의 두 함수만 봐도 age라는 변수가 필요하다는 판단이 서지 않는가? 이 부분과 관련해서 다음 예를 보자. 이 예제를 통해 객체를 생성하는 방법까지 함께 설명하겠다.

class AgeInfo: # 클래스 AgeInfo의 정의
    def up_age(self):
        self.age += 1
    def get_age(self):
        return self.age
    
def main():
    fa = AgeInfo() # AgeInfo의 객체를 생성하고 이를 변수 fa에 저장
    fa.age = 39 # fa에 저정된 객체의 변수 age에 39를 저장
        
    print("현재 아빠 나이...")
    print("아빠", fa.get_age()) # get_age 호출할 때 self에 값 전달하지 않음 
        
    print("1년 뒤...")
    fa.up_age() # up_age 호출할 때 self에 값 전달하지 않음
    print("아빠", fa.get_age()) # get_age 호출할 때 self에 값 전달하지 않음
        
main()

# 출력
현재 아빠 나이...
아빠 39
1년 뒤...
아빠 40

예제에서 이 함수를 보면,

fa = AgeInfo()

AgeInfo라는 클래스(설계도)를 기반으로 객체가 생성되고, 이 객체를 변수 fa에 저장하게 된다. 그리고 이때 생성된 객체를 그림으로 그려보면,

# self 생략

def up_age():
	age += 1
def get_age():
	return age

위 그림으로 표현한 객체 안에는 두 개의 함수가 있다. 그리고 그 두 함수에서 변수 age에 접근하고 있다. 따라서 위의 객체가 제대로 동작하려면 다음과 같이 객체 안에 age가 있어야 한다.

age

def up_age():
	age += 1
def get_age():
	return age

파이썬이 위 그림과 같이 객체에 변수 age를 넣어준다. 실제로 객체 안에 변수 age가 존재한다는 사실은 예제의 다음 문장을 통해서도 알 수 있다. 이런 문장이 동작을 하려면,

fa.age = 39

변수 이름에 이어 점을 찍는 것은, 해당 변수에 저장된 객체가 변수나 함수에 접근하는 행위이다. 즉 위의 문장이 실행되면 fa에 저장된 객체의 변수 age에 39가 저장되어 다음의 상태가 된다.

age = 39

def up_age():
	age += 1
def get_age():
	return age

그리고 예제에서 다음 방식으로 이 객체 안에 있는 두 함수를 호출하였다.

fa.get_age()
fa.up_age()

매개변수 self를 위해 어떤 값도 전달하지 않았음에 주목!

그렇다면 객체 안에 존재하는 변수 age는 어떤 종류의 변수일까? 우리는 지역변수와 전역변수를 알고 있다. 그러나 객체 안의 변수는 이 둘과는 부류가 다른 인스턴스 변수다. 더불어 객체 안에 있는 함수는 메서드라고 한다. 더 정확하는 인스턴스 메서드다.

인스턴스는 객체의 또 다른 표현이다. 사실 두 표현에는 의미적인 차이가 조금 있지만 보통은 동일하게 취급이 되므로 지금은 두 표현의 차이에 신경쓰지 않아도 된다.

즉, 다음 문장을 보며

fa = AgeInfo()

다음과 같이 말해도 되지만, AgeInfo의 객체가 생성되어서 변수 fa에 저장되었다,

다음과 같이 말해도 된다. AgeInfo의 인스턴스가 생성되어서 변수 fa에 저장되었다.

따라서 인스턴스 변수와 인스턴스 메서드가 의미하는 바를 다음과 같이 정리할 수도 있다.

인스턴스 변수 = 인스턴스(객체) 안에 존재하는 변수

인스턴스 메서드 = 인스턴스(객체) 안에 존재하는 메서드(함수)

05 나이 정보 관리하는 이전 예제의 수정 결과

class AgeInfo:
    def up_age(self):
        self.age += 1
    def get_age(self):
        return self.age
    
def main():
    fa = AgeInfo()
    mo = AgeInfo()
    me = AgeInfo()
    
    fa.age = 39
    mo.age = 34
    me.age = 9
    
    sum = fa.get_age() + mo.get_age() + me.get_age()
    print("가족 전체의 나이는...")
    print(sum)
    
    print("내년에는....")
    fa.up_age()
    mo.up_age()
    me.up_age()
    sum = fa.get_age() + mo.get_age() + me.get_age()
    print(sum)
main()

# 출력
가족 전체의 나이는...
82
내년에는....
85

일단 위 예제의 다음 세 문장이 실행되면,

fa = AgeInfo()
mo = AgeInfo()
me = AgeInfo()

다음과 같이 총 세 개의 객체가 생성된다.

age
def up_age():
	age += 1
def get_age():
	return age

age
def up_age():
	age += 1
def get_age():
	return age

age
def up_age():
	age += 1
def get_age():
	return age

그리고 아래 세 문장이 실행이 되면서,

fa.age = 39
mo.age = 34
me.age = 9

각 객체 안에 있는 변수 age의 값이 다음과 같이 초기화된다.(초기화된다는 것은 값이 처음 저장된다는 뜻이다)

age = 39

def up_age():
	age += 1
def get_age():
	return age

age = 34

def up_age():
	age += 1
def get_age():
	return age

age = 9

def up_age():
	age += 1
def get_age():
	return age

이어서 예제에서는 가족 나이의 합을 계산해서 출력하고 있다. 그리고 클래스를 만들었기 때문에 가족이 추가되더라도 그에 따른 변수나 함수를 추가할 필요가 없다. 그저 객체만 하나 더 생성하면 되는 것이다. 그리고 이 것이 클래스가 주는 장점이다.

06 self

바로 앞에 나왔던 코드에 대해, 조금 이상하지 않은가? 중복되는 부분이 너무 많지 않은가?

실제로 파이썬은 위 그림과 같은 형태로 객체를 생성하고 관리한다. 구체적인 공유 방식을 알아보겠다.

fa.up_age() # 변수 fa에 저장된 객체의 up_age 함수 호출

그러면 파이썬은 AgeInfo 클래스의 함수가 실제 저장된 위치로 가서 up_age 함수를 호출하되, fa를 인자로 전달하면서 다음과 같은 형태로 호출한다(물론 이러한 과정이 눈에 보이지는 않는다)

up_age(fa) # up_age 함수 호출하며 fa를 인자로 전달

즉 매개변수 self에 fa가 전달되므로 다음 형태로 함수 호출이 진행된다. (fa의 메모리 공간에 self라는 이름이 하나 더 붙어서 이 순간 self는 fa가 된다. 이는 10장 마지막 부분에서 리스트를 대상으로 설명했던 내용이다)

age = 39 / age = 35 / age = 2

def up_age(fa):
	fa.age += 1

def get_age(self):
	return self.age

# fa에 저장된 객체 기반의 up_age 함수 호출

그래서 fa에 저장된 객체의 age 값이 1 증가한다. 참고로 위 그림과 같은 형태로 우리가 직접 함수를 호출할 수도 있다. 이를 다음 예제를 통해 보이겠다. (self의 의미 파악 용도)

class AgeInfo:
    def up_age(self):
        self.age += 1
    def get_age(self):
        return self.age
    
def main():
    fa = AgeInfo()
    fa.age = 20 # 인스턴스 변수 age의 값 20으로 초기화
    
    fa.up_age() # fa에 저장된 객체의 up_age 함수 호출
    AgeInfo.up_age(fa) # 위와 동일한 기능의 문장
    
    print(fa.get_age()) # fa에 저장된 객체의 get_age 함수 호출 
    print(AgeInfo.get_age(fa)) # 위와 동일한 기능의 문장

main()

# 출력
22
22

위 예제를 통해서 알 수 있듯이 AgeInfo 클래스의 두 함수를 직접 호출하는 방법은 다음과 같다.

AgeInfo.up_age(....) -> AgeInfo의 up_age 메서드 호출 방법
AgeInfo.get_age(....) -> AgeInfo의 get_age 메서드 호출 방법

즉 우리가 다음과 같이 문장을 작성하면,

**fa.up_age()**

파이썬은 이 문장을 다음 형태로 바꿔서 함수를 호출한다.

**AgeInfo.up_age(fa)**

self의 대한 설명 끝!

07 self 이외의 매개변수를 갖는 함수들 정의해보기

앞서 정의한 AgeInfo 클래스의 인스턴스 메서드는 다음과 같이 매개변수로 self만 가지고 있다.

class AgeInfo():
		def up_age(self):
				self.age += 1
		def get_age(self):
				return self.age

그런데 인스턴스 메서드에도 얼마든지 매개변수를 추가할 수 있다. 그래서 이번에는 self 이외의 매개변수를 갖는 메서드를 클래스에 추가하려고 한다.

def set_age(self, n):
		self.age = n

이렇게 정의한 메서드를 호출할 때 self에는 파이썬이 알아서 전달하니, 우리는 두 번째 이후의 매개변수에만 값을 전달하면 된다. 그리고 위의 메서드는 다음과 같이 정의해도 된다.

def set_age(self, age):
		self.age = age # age는 매개변수, self.age는 인스턴스 변수

이렇게 정의하면 매개변수와 인스턴스 변수의 이름이 같아지지만, 함수 안에서 그냥 age라고 쓰면 이는 매개변수가 되고 self.age라 쓰면 이는 인스턴스 변수가 되기 때문에 이렇듯 이름이 같아도 된다.

class AgeInfo:
    def up_age(self):
        self.age += 1
    def get_age(self):
        return self.age
    def set_age(self, age):
        self.age = age

def main():
    fa = AgeInfo()
    fa.set_age(39)
    fa.up_age()
    print("1년 후 아빠 나이: ", fa.get_age())
    
main()

# 출력
1년 후 아빠 나이:  40

그리고 위 예제에서는 set_age 메세드를 추가로 정의했기 때문에 이전 예제의 다음과 같은 인스턴스 변수 초기화를,

fa.age = 39 # 인스턴스 변수에 직접 접근해서 초기화

다음과 같이 메서드 호출의 형태로 대신할 수 있게 한다.

fa.set_age(39)

08 생성자

객체 생성 후에 반드시 해줘야 하는 일이 하나 있다 ⇒ 인스턴스 변수의 초기화

앞서 작성한 예제들을 보면 다음과 같이, 또는 set_age 메서드 호출을 통해 객체 생성 이후에 반드시 인스턴스 변수의 초기화를 진행했음을 알 수 있다.

def main():
	fa     = AgeInfo()
	fa.age = 20 

이렇듯 객체 안에 존재하는 모든 변수는 초기화를 해야 한다. 그리고 이러한 초기화는 객체 생성 후에 바로 하는 것이 일반적이다. 그래서 파이썬은 객체의 생성과 변수의 초기화를 동시에 진행할 수 있도록 생성자라는 것을 제공한다. 그림 이와 관련해서 다음 예를 보자.

class Const:
    def __init__(self):
        print("new~")
        
def main():
    o1 = Const()
    o2 = Const()
        
main()
__init__

이 메서드의 이름은 생성자이다. 이닛. 생성자는 객체 생성 시 자동으로 호출이 되는 특징이 있다. 위 예제의 실행 결과에서도 그러한 사실을 보여주고 있다. (물론 생성자도 다른 메서드들과 마찬가지로 매개변수로 self를 넣어줘야한다) 조금 더 구체적으로 설명한다면, 위 예제에서 다음 문장을 통해 첫 번째 객체를 생성했다.

o1 = Contst()

그러면 다음 형태의 객체가 생성된다. Const 클래스에 생성자만 있기 때문에 생성된 객체에도 생성자만 있다.

def __init__(self):
		print("new~")

그런데 이로써 끝이 아니라 이 객체의 생성자가 바로 이어서 자동으로 호출된다. 그래서 실행 결과에서 보면 new 문자열이 출력된 것이다. 그럼 이러한 생성자를 통해서 어떻게 인스턴스 변수를 초기화할 수 있을까?

class Const:
    def __init__(self, n1, n2):
        self.n1 = n1
        self.n2 = n2
    def show_data(self):
        print(self.n1, self.n2)
        
def main():
    o1 = Const(1, 2)
    o2 = Const(3 ,4)
    o1.show_data()
    o2.show_data()
    
main()

# 출력
1 2
3 4

위 예제의 다음 문장을 보자

o1 = Const(1, 2)

객체 생성 시 소괄호를 통해 1과 2를 전달하고 있다. 그리고 이렇게 전달된 값이 다음 생성자에 전달된다. 물론 매개변수 self를 제외하고 n1 - 1, n2 - 2가 전달된다.

def __init__(self, n1, n2):
		self.n1 = n1 # self.n1은 인스턴스 변수, n1은 매개변수
		self.n2 = n2 # 

그럼 이제 앞서 정의한 클래스 AgeInfo에 적절한 생성자를 넣어보자

class AgeInfo:
    def __init__(self, age):
        self.age = age
    def up_age(self):
        self.age += 1
    def get_age(self):
        return self.age

def main():
    fa = AgeInfo(39)
    fa.up_age()
    print("1년 후 아빠 나이: ", fa.get_age())
    
main()

# 출력
1년 후 아빠 나이:  40

이렇듯 클래스를 만들 때에는 생성자도 함께 넣어주어서 객체 생성과 동시에 그 객체의 모든 인스턴스 변수들을 적당한 값으로 초기화하는 것이 좋다.

09 사실 파이썬의 모든 데이터는(값은) 객체

우리는 이미 다음과 같은 실행 결과를 통해서 문자열이 객체임을 확인한 바 있다.

>>> s = "coffee"
>>> s.upper()
'COFFEE'
>>> n = 1000
>>> n.bit_length()
10
>>> f = 3.14
>>> f.is_integer()
False

이렇듯 우리가 정수나 실수를 입력하면 파이썬은 이를 객체로 만든다. 따라서 그 객체를 대상으로 인스턴스 메서드를 호출할 수 있는 것이다. 그런데 정수 객체나 실수 객체에 담겨 있는 메서드들은 우리가 보편적으로 사용하는 메서드들은 아니므로 지금은 이들이 객체라는 사실만 인지하고 있는 정도면 충분하다.

profile
지뢰찾기 개발자

0개의 댓글