코드 추상화: 클래스와 객체 1부

지금까지 코드 추상화와 관련하여 함수와 모듈 두 가지 프로그래밍 기본 요소를 다뤘다. 여기서는 코드 추상화와 관련된 세번 째 프로그래밍 기본 요소인 클래스를 소개한다.

안내: python-textbook.readthedocs.io의 Classes 내용을 요약 및 수정한 내용입니다.

클래스와 자료형

클래스(class)는 서로 관련된 데이터와 해당 데이터를 다루는 함수들을 하나로 묶어 추상화하는 방법이다. 클래스는 기본적으로 문자열, 정수 또는 리스트와 같은 자료형의 일종이다. "python", 17, [1, 2, 3]을 각각 문자열, 정수, 리스트 자료형의 값이라 부르듯이 특정 클래스의 값에 해당하는 대상을 정의할 수 있다. 그런 대상을 해당 클래스의 인스턴스(instance)라 부른다.

사실 파이썬에서 다루는 모든 대상은 특정 클래스의 인스턴스이다. 예를 들어, "python", 17, [1, 2, 3] 각각은 str, int, list 클래스의 인스턴스들이다. 이와 같이, 특정 클래스의 인스턴스를 일반적으로 객체(object)라고 부른다. 심지어 클래스 자체도 type 클래스의 인스턴스이다. 특정 객체의 클래스, 즉, 자료형을 확인하려면 type() 함수를 활용한다.

주의: 일부 다른 언어에서는 상황이 다르다. 예를 들어, 자바 언어의 경우 정수, 부동소수점 등은 클래스와 아무 상관 없다.

In [1]:
type(str)
Out[1]:
type
In [2]:
type(int)
Out[2]:
type
In [3]:
type(list)
Out[3]:
type

속성과 메서드

사용할 객체를 디자인할 때 어떤 속성의 데이터들을 사용할 것인지, 그리고 그 값들을 어떻게 다룰 것인지 결정해야 한다. 속성을 저장하는 변수를 속성 변수 또는 그냥 속성, 속성을 다루는 함수를 메서드라고 부른다.

예를 들어 문자열 "python"은 어떤 형식으로든 python이라는 단어를 속성으로 갖고 있어야 하며, split, strip, find 등 문자열 메서드에 의해 이용될 수 있다. 반면에 [1, 2, 3]은 어떤 형식으로든 1, 2, 3을 속성으로 갖고 있어야 하며, append, pop, sort 등 리스트 메서드에 의해 이용될 수 있다.

클래스 정의와 사용법

다음은 개인정보를 저장하는 간단한 사용자 정의 클래스를 소개한다.

In [4]:
import datetime # date 클래스 사용 목적

class Person:

    def __init__(self, name, surname, birthdate, address, telephone, email):
        self.name = name
        self.surname = surname
        self.birthdate = birthdate

        self.address = address
        self.telephone = telephone
        self.email = email

    def age(self): # 나이 계산 함수
        today = datetime.date.today()
        age = today.year - self.birthdate.year

        if today < datetime.date(today.year, self.birthdate.month, self.birthdate.day):
            age -= 1

        return age

class 지정자와 클래스 이름 및 콜론으로 클래스 정의를 시작한다. 클래스의 본문은 함수의 경우처럼 들여 쓴다. 클래스 이름에 소괄호를 사용하여 상속할 부모 클래스들을 함수의 인자들처럼 나열하는 방식으로 명시할 수도 있다.

class 클래스이름(부모클래스1, ..., 부모클래스n):
    클래스본문

Person 클래스는 부모 클래스가 없으며, 부모 클래스가 없으면 괄호를 생략할 수 있다. 부모 클래스와 상속 개념은 이후에 자세히 다룬다.

매직 메서드와 사용자 정의 메서드

Person 클래스 내부에는 __init__age 두 함수가 정의되어 있다. 이 중에 __init__ 함수는 특별한 메서드이며, 이와같이 양끝이 밑줄 두 개로 감싸인 메서드를 매직 메서드(magic method)라 부른다. 반면에 age 함수는 사용자 정의 메서드(user-defined method)이다.

모든 클래스는 __init__ 메서드 이외에 다수의 매직 메서드를 기본적으로 포함한다. 하지만 클래스를 선언할 때 명시되지 않으면 기본으로 정의된 기능을 수행하며, 이에 대해서는 코드 추상화: 클래스와 객체 2부에서 다룰 것이다.

초기 설정 메서드

마치 함수를 호출하듯이 클래스 객체를 호출하면 해당 클래스의 새 인스턴스가 생성된다. 예를 들어, 아래와 같이 Jane Doe라는 사람의 개인 정보를 담은 객체를 생성하여 jDoe 변수에 할당할수 있다.

In [5]:
jDoe = Person(
    "Jane",
    "Doe",
    datetime.date(1992, 3, 12), # 년, 월, 일
    "No. 12 Short Street, Greenville",
    "555 456 0987",
    "jane.doe@example.com"
    )

클래스이름(인자1, ..., 인자n) 형식을 이용하여 클래스를 호출하면 해당 클래스의 인스턴스가 생성된 후 바로 해당 클래스의 __init__ 함수가 지정된 인자들과 함께 호출된다. 클래스의 객체를 선언할 때 사용되는 인자는 따라서 __init__ 함수 선언에서 사용된 매개변수에 해당하는 인자이어야 한다.

__init__ 함수는 생성된 객체의 속성을 초기 설정하는 일을 수행한다. 이런 의미에서 초기 설정 메서드 또는 초기 설정자 라고 부를 수 있다.

주의: __init__ 함수를 생성자(constructor)라고도 부르지만 기술적으로 정확하지 않은 표현이다.

예를 들어, jDoe 객체가 생성되자 마자 __init__ 함수가 아래와 같이 호출된다.

__init__(
    "Jane",
    "Doe",
    datetime.date(1992, 3, 12), # 년, 월, 일
    "No. 12 Short Street, Greenville",
    "555 456 0987",
    "jane.doe@example.com"
    )

즉, Jane Doe의 개인정보를 jDoe 객체내부에 해당 속성에 저장한다.

self 매개변수

self의 역할

__init__age 메서드 모두 첫째 매개변수로 self를 사용한다. 하지만 self에 해당하는 인자를 직접 요구하지는 않는다. 예를 들어, jDoe 객체를 생성할 때 자동으로 호출되는 __init__ 메서드는 앞서 보았듯이 self를 제외한 인자들을 이용하여 호출된다. 이유는, __init__ 함수가 호출될 때 이미 객체가 생성되어 있으며, 그 객체가 자동으로 첫째 인자로 사용되기 때문이다. age 메서드는 따라서 호출 될 때 아무런 인자도 사용하지 않는다. self에 대한 인자로 역시 이미 생성된 객체가 자동으로 입력되기 때문이다. 예를 들어, __init__ 함수가 실제로 호출되는 과정은 다음과 같다.

__init__(
    jDoe
    "Jane",
    "Doe",
    datetime.date(1992, 3, 12), # 년, 월, 일
    "No. 12 Short Street, Greenville",
    "555 456 0987",
    "jane.doe@example.com"
    )

속성 변수 선언

__init__ 함수의 매개변수 이름과 함수 본문에서 선언되는 속성 변수 이름이 동일할 필요는 없지만 역시 관례적으로 그렇게 한다. 다만, 속성 변수는 항상 다음과 같이 self와 점(.) 연산자로 구분되는 형식으로 사용되어야 한다.

self.속성 변수

이 방식은 __init__ 메서드에서 뿐만 아니라 모든 메서드 내부에서 선언 또는 사용되는 속성 변수에 대해서 적용된다.

주의: 다른 많은 언어에서는 self와 같은 매개변수를 사용하지 않는다. 따라서 해당 객체를 확인하거나 이용하려면 특별한 지정자를 활용해야 한다. 매개변수 이름을 self가 아닌 다른 변수를 사용해도 되지만, 관례적으로 self를 사용한다.

객체 활용

birthdate 매개변수에 의해 전달되는 값은 datetime 모듈에서 정의된 date 클래스의 인스턴스이다. 즉, 모든 객체는 변수 할당, 함수 호출, 리턴값 등에 사용될 수 있는 제1종 객체이다.

속성과 메서드 사용

생성된 객체는 해당 객체의 속성과 메서드를 통해 활용된다. 속성을 확인하고 메서드를 호출하는 방식은 다음과 같다.

객체이름.속성 변수

또는

객체이름.메서드(인자1, ..., 인자k)

예를 들어 jDoe의 이름, 이메일 주소에 해당하는 속성을 확인하려면 다음과 같이 실행한다.

In [6]:
print(jDoe.name)
print(jDoe.email)
Jane
jane.doe@example.com

반면에 나이를 확인하려면 age 메서드를 아래와 같이 호출한다.

In [7]:
print(jDoe.age())
28

PythonTutor 활용 1

지금까지의 설명을 PythonTutor: Person 클래스 1에서 코드를 실행하면서 확인할 수 있다.

연습 1

다음 변수들의 역할과 활동영역(scope)을 설명하라.

  1. Person
  2. jDoe
  3. surname
  4. self
  5. age (함수이름)
  6. age (age 함수 내부에서 선언된 변수)
  7. self.email
  8. jDoe.email
  9. self.age()
  10. jDoe.age()

모범답안

  1. Person: 클래스 이름. 전역변수.
  2. jDoe: Person 클래스의 인스턴스 이름. 전역변수.
  3. surname: __init__ 함수의 매개변수. __init__ 함수 본체에서만 사용되는 지역변수.
  4. self: 모든 메서드의 첫째 매개변수. 메서드가 호출될 경우 해당 객체로 대체됨. 지역변수.
  5. age (함수이름): Person 클래스의 메서드 이름. Person클래스 내부에서만 사용되는 지역변수.
  6. age (age 함수 내부에서 선언된 변수) age 메서드 내부에서만 사용되는 지역변수.
  7. self.email: 엄밀한 의미의 변수 아님. self 가 가리키는 객체의 내부에서 선언된 속성 변수 email을 가리키는 이름 역할 수행.
  8. jDoe.email: 이하 동일
  9. self.age(): 이하 동일
  10. jDoe.age(): 이하 동일

인스턴스 속성

클래스의 속성은 인스턴스 속성과 클래스 속성 두 종류로 나뉜다. 먼저 인스턴스 속성을 설명하고 이후에 클래스 속성을 다룬다.

self.name 등 메서드 내부에서 선언되는 변수를 인스턴스 속성 변수 또는 인스턴스 속성이라 부른다. 예를 들어, 아래 age 메서드를 아래와 같이 수정하면 age 메서드가 호출될 때 _age라는 인스턴스 속성이 선언된다.

In [8]:
import datetime

class Person:

    def __init__(self, name, surname, birthdate, address, telephone, email):
        self.name = name
        self.surname = surname
        self.birthdate = birthdate

        self.address = address
        self.telephone = telephone
        self.email = email

    def age(self):
        if hasattr(self, "_age"):   # _age 속성의 존재 여부 확인
            return self._age

        today = datetime.date.today()
        age = today.year - self.birthdate.year

        if today < datetime.date(today.year, self.birthdate.month, self.birthdate.day):
            age -= 1

        self._age = age
        return age
    
jDoe = Person(
    "Jane",
    "Doe",
    datetime.date(1992, 3, 12), # 년, 월, 일
    "No. 12 Short Street, Greenville",
    "555 456 0987",
    "jane.doe@example.com"
    )    

dScotty = Person(
    "Dana",
    "Scotty",
    datetime.date(1970, 5, 22), # 년, 월, 일
    "No. 2 Long Street, Bluecity",
    "444 654 0135",
    "dana.scotty@example.com"
    )    

인스턴스 속성 추가

이미 생성된 객체에서 독립적으로 새로운 속성과 새로운 메서드를 추가할 수 있다.

주의: C++ 등 일부 언어에서는 클래스를 정의할 때, 미리 객체 속성 목록을 지정해야 하며, 나중에 객체에 새 속성을 추가하지 못할 수 있다.

예를 들어, Person 클래스에는 애완동물 관련 속성을 저장하는 pets 인스턴스 변수가 없다. 따라서 jDoe 역시 애완동물 속성을 갖지 못한다.

In [9]:
jDoe.pets
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-9-da569883283f> in <module>
----> 1 jDoe.pets

AttributeError: 'Person' object has no attribute 'pets'

하지만 jDoe 스스로 애완동물 속성을 추가할 수 있다.

In [10]:
jDoe.pets = ['고양이', '고양이', '강아지']
In [11]:
jDoe.pets
Out[11]:
['고양이', '고양이', '강아지']

하지만 애완동물 속성을 jDoe만 갖는다. 예를 들어, Person 클래스의 다른 인스턴스는 여전히 애완동물 속성을 갖지 않는다.

In [12]:
dScotty.pets
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-12-53db20e7dd9c> in <module>
----> 1 dScotty.pets

AttributeError: 'Person' object has no attribute 'pets'

숨겨진 속성과 메서드

밑줄로 시작하는 속성 또는 메서드의 이름은 일반적으로 클래스 외부로 알려지면 안되는 것들을 가리킨다. 또한 _age 속성의 경우, 먼저 age 메서드가 최소 한 번 실행되어야 선언되기 때문에 임의로 사용하다보면 오류가 발생할 수 있다.

In [13]:
jDoe._age
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-13-01cf54fb661e> in <module>
----> 1 jDoe._age

AttributeError: 'Person' object has no attribute '_age'

age 함수를 먼저 호출해야 한다.

In [14]:
jDoe.age()
Out[14]:
28

이제 _age에 저장된 속성을 확인할 수 있다.

In [15]:
jDoe._age
Out[15]:
28

따라서 jDoe의 나이를 확인하기 위해서는 age 메서드를 호출하도록 하는 게 좋다. 즉, _age 인스턴스 변수는 외부에 노출하지 않고, 대신에 age 메서드를 사용하도록 권장해야 한다.

초기 설정 권장 사항

객체에서 사용되는 속성들은 초기 설정 과정에서 모두 선언하는 것이 좋다. 그렇지 않으면 앞서 pets_age 속성의 경우에서 보았듯이 오류가 발생할 확률이 높아진다.

객체가 생성되면 바로 __init__ 메서드가 실행되기 때문에, 생성되는 객체와 관련된 모든 속성을 __init__ 메서드 실행과정에서 초기 설정되도록 하는 것이 좋다. 예를 들어, 차라리 pets_age의 속성값을 아래와 같이 일단 비워두더라도 __init__ 메서드 본문에서 선언하는 것이 좋다.

In [16]:
import datetime

class Person:

    def __init__(self, name, surname, birthdate, address, telephone, email):
        self.name = name
        self.surname = surname
        self.birthdate = birthdate

        self.address = address
        self.telephone = telephone
        self.email = email
        
        self.pets = None   # 비워두기
        self._age = None    # 비워두기

    def age(self):
        if getattr(self, "_age") != None:   # _age 속성 업데이이트 여부 확인
            return self._age

        today = datetime.date.today()
        age = today.year - self.birthdate.year

        if today < datetime.date(today.year, self.birthdate.month, self.birthdate.day):
            age -= 1

        self._age = age
        return age
    
jDoe = Person(
    "Jane",
    "Doe",
    datetime.date(1992, 3, 12), # 년, 월, 일
    "No. 12 Short Street, Greenville",
    "555 456 0987",
    "jane.doe@example.com"
    )    

dScotty = Person(
    "Dana",
    "Scotty",
    datetime.date(1970, 5, 22), # 년, 월, 일
    "No. 2 Long Street, Bluecity",
    "444 654 0135",
    "dana.scotty@example.com"
    )    

이제 jDoe에서만 왜완동물을 추가해도 dScotty에서 애완동물을 확인할 때 오류가 발생하지 않는다.

In [17]:
jDoe.pets = ['고양이', '고양이', '강아지']
In [18]:
dScotty.pets # None 값이라 출력되지 않는다. 

_age 역시 age 메서드를 실행하지 않아도 오류를 발생시키지 않는다. 물론 아래와 같은 식으로 _age 속성을 확인하는 것은 피해야 한다.

In [19]:
dScotty._age

인스턴스 속성 관련 내장 함수: getattr, setattr, hasattr

파이썬은 인스턴스의 속성을 확인하거나 지정하는 세 개의 내장 함수를 지원한다.

getattr 함수

특정 객체의 특정 속성값을 확인해줄 때 사용하는 함수이다. 예를 들어, jDoepets 속성값을 다음과 같이 확인한다.

In [20]:
getattr(jDoe, "pets")
Out[20]:
['고양이', '고양이', '강아지']

물론 아래와 같이 하는 게 보다 편하다.

In [21]:
jDoe.pets
Out[21]:
['고양이', '고양이', '강아지']

하지만 특정 속성이 존재하지 않을 경우를 대비해야 할 때 getattr이 유용하다. 예를 들어, 취미 속성인 hobbies가 선언되어 있지 않을 경우 아래와 같이 확인할 수 있다.

In [22]:
getattr(jDoe, "hobbies", "해당사항 없음")
Out[22]:
'해당사항 없음'

또한 아래와 같이 여러 개의 속성에 대해 반복문 등을 작성할 때는 반드시 getattr을 사용해야 한다.

In [23]:
for attr in ["pets", "_age"]:
    print(getattr(jDoe, attr))
['고양이', '고양이', '강아지']
None

심지어 다음과 같이 활용할 수 있다.

In [24]:
for attr in ["pets", "_age", "hobbies"]:
    print(getattr(jDoe, attr, "해당사항 없음."))
['고양이', '고양이', '강아지']
None
해당사항 없음.

하지만 아래와 같이 작성하는 것은 불가능하다. 이유는 변수를 객체이름.변수 형식으로 사용할 수 없기 때문이다.

In [25]:
for attr in ["pets", "_age"]:
    print(jDoe.attr)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-25-03a1d20e76e8> in <module>
      1 for attr in ["pets", "_age"]:
----> 2     print(jDoe.attr)

AttributeError: 'Person' object has no attribute 'attr'

setattr 함수

setattr 함수는 객체의 속성을 지정할 때 사용한다. 예를 들어, dScotty의 애완동물 속성을 지정하려면 다음과 같이 한다.

In [26]:
dScotty.pets
In [27]:
setattr(dScotty, 'pets', ['고양이', '강아지'])
In [28]:
dScotty.pets
Out[28]:
['고양이', '강아지']

물론 아래와 같이 직접 지정할 수 있다.

In [29]:
dScotty.pets = ['흰고양이', '강아지', '검은고양이']
In [30]:
dScotty.pets
Out[30]:
['흰고양이', '강아지', '검은고양이']

새로운 속성을 지정할 수도 있다. 예를 들어, 원래 없었던 취미 속성 hobbies를 아래와 같이 추가할 수도 있다.

In [31]:
setattr(dScotty, 'hobbies', ['테니스', '배드민턴'])
In [32]:
dScotty.hobbies
Out[32]:
['테니스', '배드민턴']

setattr 또한 for 반복문과 함께 사용될 수 있다.

In [33]:
mydict = {'a': 10, 'b': 20, 'c':30}

for attr in ['a', 'b', 'c']:
    setattr(dScotty, attr, mydict[attr])

dScotty에 추가된 세 개의 속성 a, b, c의 값을 다음과 같이 확인할 수 있다.

주의: f-문자열과 r-문자열의 혼합사용에 주의하라.

In [34]:
for attr in ['a', 'b', 'c']:
    print(fr"'{attr}' : {getattr(dScotty, attr, mydict[attr])}")
'a' : 10
'b' : 20
'c' : 30

hasattr 함수

age 메서드의 정의에서 보았듯이 hasattr 함수는 특정 객체가 특정 속성을 가지고 있는지 여부를 판단한다. 예를 들어, dScotty 객체는 a 속성을 갖지만 d 속성은 갖지 않는다.

In [35]:
hasattr(dScotty, 'a')
Out[35]:
True
In [36]:
hasattr(dScotty, 'd')
Out[36]:
False

연습 2

Person 클래스를 다음 조건을 만족하도록 수정하라.

  • 객체가 생성될 때 바로 객체의 나이를 계산한다.
  • age 메서드를 호출했을 때, 마지막으로 나이 계산한 후부터 하루 이상 지났을 경우 새롭게 나이를 계산한다.

모범답안

In [37]:
import datetime

class Person:

    def __init__(self, name, surname, birthdate, address, telephone, email):
        self.name = name
        self.surname = surname
        self.birthdate = birthdate

        self.address = address
        self.telephone = telephone
        self.email = email

        self._age = None
        self._age_last_recalculated = None

        self._recalculate_age()              # 나이 확인

    def _recalculate_age(self):              # 나이 계산 함수
        today = datetime.date.today()
        age = today.year - self.birthdate.year

        if today < datetime.date(today.year, self.birthdate.month, self.birthdate.day):
            age -= 1

        self._age = age
        self._age_last_recalculated = today   # 마지막 나이 확인 날짜 기억하기

    def age(self):
        if (datetime.date.today() > self._age_last_recalculated):
            self._recalculate_age()

        return self._age

주의사항

함수를 정의한다고해서 바로 함수가 실행되는 것은 아니듯이, 클래스 또한 선언된 후 아무 것도 실행하지 않는다. 다만 클래스가 선언되었다는 것을 파이썬이 알게되는 것 뿐이다. 따라서 클래스 내부에서 속성이나 메서드를 선언할 때 다른 속성이나 메서드를 활용할 수 있다.

예를 들어, __init__ 메서드 정의에 self._recalculate 메서드를 활용해도 아무런 문제가 없다. 왜냐하면, Person 클래스의 선언이 완료되었을 때는 이미 _recalculate 메서드가 정의되어 있을 것이기 때문이다. 물론 그렇지 않다면 실행중에 오류가 발생할 것이다. 그리고 __init__ 메서드가 호출될 때는 이미 Person 클래스의 인스턴스가 이미 생성되어 있어야 한다. 따라서 이 객체가 self._recalculateself에 자동으로 삽입되어 초기 설정이 문제 없이 진행된다.

PythonTutor 활용 2

PythonTutor: Person 클래스 2에서 위 설명을 실행하면서 확인할 수 있다.

클래스 속성

__init__ 메서드에 의해 Person 클래스의 인스턴스의 속성을 초기 설정할 때 선언되는 속성 변수는 생성된 인스턴스 고유의 속성을 다룬다. 반면에 클래스 속성은 기본적으로 특정 클래스와 밀접하게 관련된 상수의 역할을 수행하는 값을 정의하는 데에 사용되며, 해당 클래스의 모든 인스턴스에서 공유된다. 클래스 속성은 많은 면에서 인스턴스 속성과 유사한 기능을 갖지만 주의해야 할 사항이 있다.

클래스 속성 활용법 1

클래스 속성은 메서드 밖에서 선언되며, self를 사용하지 않고 일반적인 변수 선언 방식을 사용한다. 들여쓰기는 메서드와 동일한 수준으로 사용된다. 예를 들어, Person 클래스에 호칭(title)으로 사용될 항목들을 TITLES 라는 클래스 변수에 선언하려면 다음과 같이 한다.

In [38]:
class Person:

    TITLES = ('Dr', 'Mr', 'Mrs', 'Ms')

    def __init__(self, title, name, surname):
        if title not in self.TITLES:               # 메서드 내부에서 클래스 변수 사용
            raise ValueError("%s is not a valid title." % title)

        self.title = title
        self.name = name
        self.surname = surname

메서드 내부에서 클래스 속성을 사용하려면 인스턴스 속성의 경우처럼 self와 함께 사용된다. 여기서 self는 기본적으로 클래스 자신, 즉 위의 경우에는 Person 클래스를 가리킨다. 하지만 해당 클래스의 모든 인스턴스도 클래스 속성을 공유한다. 따라서 경우에 따라서는 self.TITLESself가 해당 클래스의 인스턴스가 될 수도 있다. 예를 들어, 다음 jDoe 객체 역시 TITLES 속성을 공유한다.

In [39]:
jDoe = Person("Dr", "Jane", "Doe")    
In [40]:
jDoe.TITLES
Out[40]:
('Dr', 'Mr', 'Mrs', 'Ms')

그런데 클래스 속성은 인스턴스 없이도 확인이 가능하다. 클래스 속성이라 불리는 이유가 바로 여기에 있다.

In [41]:
Person.TITLES
Out[41]:
('Dr', 'Mr', 'Mrs', 'Ms')

주의사항

클래스는 인스턴스 속성에 액세스 할 수 없다. 이유는 인스턴스 속성은 인스턴스를 생성할 때만 의미를 갖기 때문이다.

In [42]:
Person.surname
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-42-dfa90b2620d2> in <module>
----> 1 Person.surname

AttributeError: type object 'Person' has no attribute 'surname'

클래스 속성 업데이트 주의점 1

클래스와 객체 모두 클래스 속성을 업데이트 할 수 있다. 하지만 클래스 속성을 업데이트 하면 경우에 따라 특정 인스턴스에 혼란을 야기할 수 있다. 따라서 클래스 속성은 변하지 않는 상수처럼 취급해야 한다.

PythonTutor 활용 3

아래 코드를 PythonTutor: 클래스 속성 업데이트에서 실행하면서 클래스 변수를 업데이트할 때 발생할 수 있는 문제를 확인해볼 수 있다.

In [43]:
class Person:

    TITLES = ('Dr', 'Mr', 'Mrs', 'Ms')             # 튜플 사용

    def __init__(self, title, name, surname):
        if title not in self.TITLES:               # 메서드 내부에서 클래스 변수 사용
            raise ValueError("%s is not a valid title." % title)

        self.title = title
        self.name = name
        self.surname = surname
        
jDoe = Person('Dr', "Jane", "Doe")
print(f"Person:\t {Person.TITLES}")
print(f"jDoe:\t {jDoe.TITLES}\n")

Person.TITLES = ('Prof', 'Dr', 'Mr', 'Mrs', 'Ms')   # 모든 인스턴스에 영향끼침
print(f"Person:\t {Person.TITLES}")
print(f"jDoe:\t {jDoe.TITLES}\n")

jDoe.TITLES = ('Dr', 'Mr', 'Ms')                    # 이 경우 jDoe 만의 TITLES 속성 생성됨.
print(f"Person:\t {Person.TITLES}")                 # 따라서 클래스의 TITLES와 별개 속성을 가짐 
print(f"jDoe:\t {jDoe.TITLES}\n")  

Person.TITLES = ('Dr', 'Mr', 'Mrs', 'Ms')           # 이제 클래스 속성을 업데이트 하더라도
print(f"Person:\t {Person.TITLES}")                 # jDoe의 TITLES 속성에 영향 없음.
print(f"jDoe:\t {jDoe.TITLES}")
Person:	 ('Dr', 'Mr', 'Mrs', 'Ms')
jDoe:	 ('Dr', 'Mr', 'Mrs', 'Ms')

Person:	 ('Prof', 'Dr', 'Mr', 'Mrs', 'Ms')
jDoe:	 ('Prof', 'Dr', 'Mr', 'Mrs', 'Ms')

Person:	 ('Prof', 'Dr', 'Mr', 'Mrs', 'Ms')
jDoe:	 ('Dr', 'Mr', 'Ms')

Person:	 ('Dr', 'Mr', 'Mrs', 'Ms')
jDoe:	 ('Dr', 'Mr', 'Ms')

클래스 속성 활용법 2

클래스 속성을 사용하여 기본 속성 값을 제공 할 수도 있습니다.

In [44]:
class Person:
    married = False              # 결혼 여부 확인. 기본값은 미혼

    def mark_as_married(self):   # 기혼자 처리
        self.married = True

클래스 속성을 업데이트할 때 주의해야할 사항에서 설명하였듯이 클래스 속성 변수와 동일한 이름의 속성 변수를 인스턴스에서 새로 설정하면 클래스 속성이 인스턴스 속성으로 전환된다. 그리고 동일한 이름의 속성 변수가 존재할 때 객체는 인스턴스 속성을 우선적으로 사용한다.

위 코드에서 married 클래스 변수는 결혼여부를 저장하여 기본값으로 False, 즉, 미혼으로 지정해 놓았다. 그런데 아래와 같이 인스턴스를 생성할 때마다 각각 독립적으로 결혼여부를 지정할 수 있다.

주의: __init__ 메서드가 선언되어 있지 않다. 즉, 인스턴스를 생성할 때 초기 설정을 진행하지 않는다.

In [45]:
jDoe = Person()
jDoe.mark_as_married()
print(jDoe.married)
True

클래스 변수로서는 속성값이 변하지 않았다.

In [46]:
Person.married
Out[46]:
False

따라서 새로운 인스턴스를 생성하도 기본값은 미혼이다.

In [47]:
dScotty = Person()
print(dScotty.married)
False

클래스 속성 업데이트 주의점 2

클래스 속성값이 리스트, 사전 처럼 수정 가능한 자료형(mutable data type)의 경우 인스턴스 속성 변경이 클래스 속성 변경에 영향을 끼칠 수 있다.

In [48]:
class Person:
    pets = []

    def add_pet(self, pet):
        self.pets.append(pet)

jane = Person()
bob = Person()

jane.add_pet("cat")             # jane의 고양이 애완동물 추가
                                # pets의 값이 리스트임. 기존 리스트를 수정함.

print(f"jane:\t{jane.pets}")
print(f"bob:\t{bob.pets}")      # 그런데 bob까지 영향 받음.
jane:	['cat']
bob:	['cat']

PythonTutor 활용 4

위 코드를 PythonTutor: 클래스 속성 주의점 2에서 실행하며 문제점을 확인할 수 있다.

문제 해결책

클래스 속성을 사용하는 대신에 __init__ 메서드를 이용하여 인스턴스를 생성할 때마다 기본값으로 초기 설정하는 방식을 사용해야 한다. 이유는 pets와 같은 속성은 속성값을 공유하는 목적이 아니라 모든 인스턴스에서 사용될 속성 변수를 미리 지정하는 역할을 수행하기 때문이다.

In [49]:
class Person:

    def __init__(self):
        self.pets = []

    def add_pet(self, pet):
        self.pets.append(pet)

jane = Person()
bob = Person()

jane.add_pet("cat")

print(f"jane:\t{jane.pets}")
print(f"bob:\t{bob.pets}")      # 서로 영향 주지 않음.
jane:	['cat']
bob:	[]

클래스 속성 변수를 메서드 인자로 사용하기

클래스 변수를 메서드 내에서 사용하려면 self와 함께 사용해야 한다. 하지만 메서드 매개변수의 인자로 사용할 때는 self가 필요없다.

주의: 메서드 내부에서만 self가 클래스 자신, 또는 생성된 인스턴스 둘 중 하나의 의미를 갖는다. self를 사용하는 진짜 이유가 여기에 있다.

예를 들어, 아래 정의에서 클래스 변수인 TITLES__init__ 메서드의 allowed_titles 라는 옵션변수의 기본값으로 사용되었다.

In [50]:
class Person:
    TITLES = ('Dr', 'Mr', 'Mrs', 'Ms')

    def __init__(self, title, name, surname, allowed_titles=TITLES):
        if title not in allowed_titles:
            raise ValueError("%s is not a valid title." % title)

        self.title = title
        self.name = name
        self.surname = surname

연습 3

아래 정의에서 사용된 세 개의 속성 name, surname, profession 의 차이점을 설명하라. 또한 서로 다른 객체에서 해당 속성들이 가질 수 있는 값들을 설명하라.

In [51]:
class Smith:
    surname = "Smith"
    profession = "smith"

    def __init__(self, name, profession=None):
        self.name = name
        if profession is not None:
            self.profession = profession

모범답안

  • name:
    • 인스턴스 속성.
    • 인스턴스를 생성할 때 지정되는 이름을 속성값으로 사용.
  • surname:
    • 클래스 속성.
    • 생성되는 모든 인스턴스의 성으로 사용됨.
    • 인스턴스 스스로 개명하더라도 클래스 속성값은 변하지 않음.
  • profession:
    • 클래스 속성.
    • 인스턴스를 생성할 때 직업을 따로 지정하지 않으면 기본값 smith(대장장이) 사용.
    • 하지만 인스턴스를 생성할 때 직업을 지정하면 인스턴스 속성으로 변경되어 객체 고유의 직업이 저장됨.

PythonTutor 활용 5

PythonTutor: 대장장이에서 코드를 실행하며 위 내용을 확인할 수 있다.