[2021년 10월 업데이트]

안녕하세요. 스쿨오브웹의 이상희입니다.

지난 강좌에서는 오브젝트의 개념을 배웠습니다. 그리고 클래스 정의, 인스턴스 생성, self를 이용한 인스턴스 메소드와 인스턴스 변수 사용 등을 해봤습니다. 이번 강좌에서는 인스턴스 변수와는 조금 다른 개념인 클래스 변수에 대해서 알아보겠습니다.

클래스 변수란?

인스턴스 변수가 사람의 이름과 같이 각각의 인스턴스마다 가지고 있는 고유한 데이터라면, 클래스 변수는 한 단체의 단체명과 같이 같은 클래스로 만들어진 모든 인스턴스가 공유하는 데이터입니다.

어떤 회사가 직원들의 연봉을 매년 1회 인상해 주는데 특이하게도 전직원의 연봉을 똑같은 인상률로 인상해준다고 합니다. 올해는 회사 매출이 높아서 전직원의 연봉을 10%씩 올려준다고 하네요. (꿈같은 얘기네요 ㅋ) 이때, 모든 직원들에게 적용되는 공통 인상률이 클래스 변수로 사용할 수 있는 좋은 예입니다. 예제를 보면서 설명을 드리겠습니다.

원하시는 디렉터리에 “oop_3.py”라는 이름의 파일을 만들어 밑의 코드를 저장하고 실행하여 주십시오.

oop_3.py

class Employee:
    def __init__(self, first, last, pay):
        self.first = first
        self.last = last
        self.pay = pay
        self.email = first.lower() + '.' + last.lower() + '@schoolofweb.net'

    def full_name(self):
        return '{} {}'.format(self.first, self.last)

    def apply_raise(self):
        self.pay = int(self.pay * 1.1)  #1 연봉을 10% 인상합니다.

emp_1 = Employee('Sanghee', 'Lee', 50000)
emp_2 = Employee('Minjung', 'Kim', 60000)

print('# 기존 연봉')
print(emp_1.pay)  # 기존 연봉

print('\n# 인상률 적용')
emp_1.apply_raise()  # 인상률 적용

print('\n# 오른 연봉')
print(emp_1.pay)  # 오른 연봉
$ python oop_3.py
# 기존 연봉
50000

# 인상률 적용

# 오른 연봉
55000

#1에서 1.1 즉 10%의 인상률을 적용하여 연봉이 인상된것을 볼 수 있습니다. 그런데 위의 코드는 인상률을 하드코딩한 좋지 않은 예입니다. 만약 1.1의 인상률을 한 곳뿐만 아니라 여러 곳에서 사용을 한다면 어떨까요? 인상률이 바뀔 때마다 모든 곳의 숫자를 변경해야 하고, 혹시라도 수정에서 누락되는 부분이 있다면 큰 문제가 발생할 수도 있는거죠. 그래서 가장 이상적인 방법은 하드코딩을 하지않고 변수에 값을 할당하고 필요한 곳에서는 변수의 값을 참조하고, 변경이 필요할 때에는 변수의 값만 수정하는 방법이지요.

클래스 변수를 사용하여 코드를 수정해보겠습니다.

oop_3.py

class Employee:
    raise_amount = 1.1  # 1 클래스 변수 정의

    def __init__(self, first, last, pay):
        self.first = first
        self.last = last
        self.pay = pay
        self.email = first.lower() + '.' + last.lower() + '@schoolofweb.net'

    def full_name(self):
        return '{} {}'.format(self.first, self.last)

    def apply_raise(self):
        self.pay = int(self.pay * raise_amount)  # 2 클래스 변수 사용


emp_1 = Employee('Sanghee', 'Lee', 50000)
emp_2 = Employee('Minjung', 'Kim', 60000)

print('# 기존 연봉')
print(emp_1.pay)  # 기존 연봉

print('\n# 인상률 적용')
emp_1.apply_raise()  # 인상률 적용

print('\n# 오른 연봉')
print(emp_1.pay)  # 오른 연봉
$ python oop_3.py
# 기존 연봉
50000

# 인상률 적용
Traceback (most recent call last):
  File "C:\Users\CURTIS\Dev\Python\5G\add_host\oop_3.py", line 24, in <module>
    emp_1.apply_raise()  # 인상률 적용
  File "C:\Users\CURTIS\Dev\Python\5G\add_host\oop_3.py", line 14, in apply_raise
    self.pay = int(self.pay * raise_amount)  # 2 클래스 변수 사용
NameError: name 'raise_amount' is not defined

#1에서 클래스 변수를 정의하고 #2에서 참조를 했습니다. 옹… ‘raise_amount’는 정의되지 않은 이름이라고 네임에러가 떴습니다. 왜죠?!? 그렇습니다. 클래스 변수의 클래스 네임스페이스에 저장되어 있으니 클래스를 통해서 엑세스해야 하는거죠. 코드를 수정해 보죠.

oop_3.py

class Employee:
    raise_amount = 1.1

    def __init__(self, first, last, pay):
        self.first = first
        self.last = last
        self.pay = pay
        self.email = first.lower() + '.' + last.lower() + '@schoolofweb.net'

    def full_name(self):
        return '{} {}'.format(self.first, self.last)

    def apply_raise(self):
        self.pay = int(self.pay * Employee.raise_amount)  # 1 클래스 Employee를 사용하여 엑세스


emp_1 = Employee('Sanghee', 'Lee', 50000)
emp_2 = Employee('Minjung', 'Kim', 60000)

print('# 기존 연봉')
print(emp_1.pay)  # 기존 연봉

print('\n# 인상률 적용')
emp_1.apply_raise()  # 인상률 적용

print('\n# 오른 연봉')
print(emp_1.pay)  # 오른 연봉
$ python oop_3.py
# 기존 연봉
50000

# 인상률 적용

# 오른 연봉
55000

#1에서 클래스인 “Employee”를 통해서 “raise_amount”변수에 엑세스하여 봤습니다. 오~ 문제없이 실행 됐습니다.

오브젝트 네임스페이스

Employee” 대신에 인스턴스인 “self”를 통해서도 “raise_amount”에 엑세스가 가능할까요? 안되겠죠… 클래스 변수인데 “self”로 어떻게… 한번 해보죠.

oop_3.py

class Employee:
    raise_amount = 1.1

    def __init__(self, first, last, pay):
        self.first = first
        self.last = last
        self.pay = pay
        self.email = first.lower() + '.' + last.lower() + '@schoolofweb.net'

    def full_name(self):
        return '{} {}'.format(self.first, self.last)

    def apply_raise(self):
        self.pay = int(self.pay * self.raise_amount)  # 1 인스턴스인 self를 사용하여 엑세스


emp_1 = Employee('Sanghee', 'Lee', 50000)
emp_2 = Employee('Minjung', 'Kim', 60000)

print('# 기존 연봉')
print(emp_1.pay)  # 기존 연봉

print('\n# 인상률 적용')
emp_1.apply_raise()  # 인상률 적용

print('\n# 오른 연봉')
print(emp_1.pay)  # 오른 연봉
$ python oop_3.py
# 기존 연봉
50000

# 인상률 적용

# 오른 연봉
55000

아니 세상에 이런일이… 인스턴스를 통해서도 엑세스가 됐습니다. 왜일까요??? 파이썬은 밑의 그림과 같은 모양의 네임스페이스라는 것을 가지고 있습니다. 이 네임스페이스는 오브젝트의 이름들을 나눠서 관리하는데, 이름을 찾을때 “인스턴스 네임스페이스” → “클래스 네임스페이스” → “수퍼 클래스 네임스페이스”의 순서로 찾아갑니다. 하지만 반대로는 찾지 않습니다. 즉, 자식이 부모의 네임스페이스는 참조 할 수 있는데, 부모가 자식의 네임스페이스를 참조할 수는 없다는 겁니다. 위의 코드와 같이 “self.raise_amount”를 사용하면 파이썬은 처음엔 인스턴스 네임스페이스에서 “raise_amount”라는 이름을 찾고 없으면 클래스 네임스페이스에서 찾는 것입니다.

__dict__ 메소드를 사용하여 클래스와 인스턴스의 네임스페이스안을 살펴보죠

oop_3.py

class Employee:
    raise_amount = 1.1

    def __init__(self, first, last, pay):
        self.first = first
        self.last = last
        self.pay = pay
        self.email = first.lower() + '.' + last.lower() + '@schoolofweb.net'

    def full_name(self):
        return '{} {}'.format(self.first, self.last)

    def apply_raise(self):
        self.pay = int(self.pay * self.raise_amount)


emp_1 = Employee('Sanghee', 'Lee', 50000)
emp_2 = Employee('Minjung', 'Kim', 60000)

print('# 인스턴스의 네임스페이스 참조')
print(emp_1.__dict__)

print('\n# 클래스의 네임스페이스 참조')
print(Employee.__dict__)
$ python oop_3.py
# 인스턴스의 네임스페이스 참조
{'first': 'Sanghee', 'last': 'Lee', 'pay': 50000, 'email': 'sanghee.lee@schoolofweb.net'}

# 클래스의 네임스페이스 참조
{'__module__': '__main__', 'raise_amount': 1.1, '__init__': <function Employee.__init__ at 0x000001E37154F790>, 'full_name': <function Employee.full_name at 0x000001E37154F820>, 'apply_raise': <function Employee.apply_raise at 0x000
001E37154F8B0>, '__dict__': <attribute '__dict__' of 'Employee' objects>, '__weakref__': <attribute '__weakref__' of 'Employee' objects>, '__doc__': None}

위의 결과를 보시면 아시겠지만 emp_1 인스턴스의 네임스페이스에는 ‘raise_amount’가 없고, Employee 클래스 오브젝트의 네임스페이스에만 존재하고 있는 것을 볼 수 있습니다. 아래의 예제를 보시면 조금 더 이해가 되실겁니다.

oop_3.py

class SuperClass:
    super_var = '수퍼 네임스페이스에 있는 변수입니다.'


class MyClass(SuperClass):
    class_var = '클래스 네임스페이스에 있는 변수입니다.'

    def __init__(self):
        self.instance_var = '인스턴스 네임스페이스에 있는 변수입니다.'


my_instance = MyClass()

# 엑세스 가능한 경우
print('my_instance.instance_var')
print(my_instance.instance_var)
print('\nmy_instance.class_var')
print(my_instance.class_var)
print('\nmy_instance.super_var')
print(my_instance.super_var)
print('\nMyClass.class_var')
print(MyClass.class_var)
print('\nMyClass.super_var')
print(MyClass.super_var)
print('\nSuperClass.super_var')
print(SuperClass.super_var)
print('-' * 30)

# 엑세스 불가능한 경우
try:
    print(SuperClass.class_var)
except:
    print('SuperClass.class_var')
    print('class_var를 찾을 수가 없습니다...')

try:
    print(MyClass.instance_var)
except:
    print('\nMyClass.instance_var')
    print('instance_var를 찾을 수가 없습니다...')
$ python oop_3.py
my_instance.instance_var
인스턴스 네임스페이스에 있는 변수입니다.

my_instance.class_var
클래스 네임스페이스에 있는 변수입니다.

my_instance.super_var
수퍼 네임스페이스에 있는 변수입니다.

MyClass.class_var
클래스 네임스페이스에 있는 변수입니다.

MyClass.super_var
수퍼 네임스페이스에 있는 변수입니다.

SuperClass.super_var
수퍼 네임스페이스에 있는 변수입니다.
------------------------------
SuperClass.class_var
class_var를 찾을 수가 없습니다...

MyClass.instance_var
instance_var를 찾을 수가 없습니다...

이제 어느 정도 이해가 되셨을거라고 생각합니다. 그럼 다시 처음 예제로 돌아가겠습니다.

지금까지 매년 똑같은 인상률을 모든 직원에게 적용하던 회사의 정책이 바꿨습니다. 올해의 우수 사원으로 뽑힌 ‘Sanghee Lee’에게만 특별 인상률인 20%를 적용하겠다고 하네요. 😭  감사합니다… ㅎ 이런 경우에는 클래스 변수를 사용할 수 없을까요? 이런 경우에는 클래스 변수와 인스턴스 변수를 둘 다 사용하시면 됩니다. 밑의 코드를 보시죠.

oop_3.py

class Employee:
    raise_amount = 1.1  # 클래스 변수를 사용하여 모든 직원에 적용

    def __init__(self, first, last, pay):
        self.first = first
        self.last = last
        self.pay = pay
        self.email = first.lower() + '.' + last.lower() + '@schoolofweb.net'

    def full_name(self):
        return '{} {}'.format(self.first, self.last)

    def apply_raise(self):
        self.pay = int(self.pay * self.raise_amount)  # 1 인스턴스 변수부터 참조를 합니다.


emp_1 = Employee('Sanghee', 'Lee', 50000)
emp_2 = Employee('Minjung', 'Kim', 60000)

emp_1.raise_amount = 1.2  # 인스턴스 변수를 사용하여 특별 인상률 적용

print('# emp_1 연봉 20% 인상')
print(emp_1.pay)
emp_1.apply_raise()
print(emp_1.pay)
print('# emp_2 연봉 10% 인상')
print(emp_2.pay)
emp_2.apply_raise()
print(emp_2.pay)
$ python oop_3.py
# emp_1 연봉 20% 인상
50000
60000
# emp_2 연봉 10% 인상
60000
66000

위와 같이 #1에서 self를 사용하여 인스턴스 변수부터 참조하기 시작하면 emp_1은 인스턴스 변수 네임스페이스에 raise_amount가 있어서 참조가 되고, emp_2는 없으니까 자동으로 클래스 변수인 raise_amount를 참조하는 것입니다.

그럼 이번에는 클래스 변수를 사용하기 좋은 다른 예를 들어보겠습니다.

회사의 직원 수를 관리해야 한다고 생각해보죠. 이런 경우에는 각각의 직원이 이 데이터를 가지고 있을 필요는 없고, 어는 한 곳에 데이터를 저장하고 참조하는게 가장 좋겠죠? 밑의 코드를 보시죠.

oop_3.py

class Employee:

    raise_amount = 1.1
    num_of_emps = 0  # 1 클래스 변수 정의

    def __init__(self, first, last, pay):
        self.first = first
        self.last = last
        self.pay = pay
        self.email = first.lower() + '.' + last.lower() + '@schoolofweb.net'

        Employee.num_of_emps += 1  # 2 인스턴스가 생성될 때마다 1씩 증가

    def __del__(self):
        Employee.num_of_emps -= 1  # 3 인스턴스가 제거될 때마다 1씩 감소

    def full_name(self):
        return '{} {}'.format(self.first, self.last)

    def apply_raise(self):
        self.pay = int(self.pay * self.raise_amount)  # 1 인스턴스 변수부터 참조를 합니다.


print(Employee.num_of_emps)  # 처음 직원 수
emp_1 = Employee('Sanghee', 'Lee', 50000)  # 직원 1명 입사 
emp_2 = Employee('Minjung', 'Kim', 60000)  # 직원 1명 입사
print(Employee.num_of_emps)  # 직원 수 확인

del emp_1  # 직원 1명 퇴사
del emp_2  # 직원 1명 퇴사
print(Employee.num_of_emps)  # 직원 수 확인
$ python oop_3.py
0
2
0

#1에서 클래스 “num_of_emps”라는 변수를 정의하고 인스턴스가 만들어질 때마다 실행되는 __init__메소드안(#2)에서 “num_of_emps”의 값을 1씩 증가 시켰습니다. 그리고 #3에서 deconstor인 __del__을 사용하여 인스턴스가 제거될때 마다 “num_of_emps”의 값을 1씩 감소 시켜봤습니다. 이렇게 클래스 변수를 사용하여 인스턴스 변수로는 관리하기 힘든 데이터를 쉽게 관리할 수가 있는 것입니다. 어떻게 보면 일반 함수에서 사용하는 전역 변수(global variable)와 비슷한 개념입니다.

어떠신가요? 이제 언제 인스턴스 변수를 사용하면 되고, 언제 클래스 변수를 사용해야 하는지 아시겠죠? 그런데 어떤 분들은 이런 생각을 하시고 계실 수 있습니다. “클래스 변수가 있다면 클래스 메소드도 있는게 아닌가?”라고 말이죠. 맞습니다. 인스턴스 메소드 이외에도 “클래스 메소드”와 “스택틱 메소드”라는 것이 있습니다. 이것들에 대해서는 다음 강좌에서 다루도록 하겠습니다. 수고하셨습니다~!