파알못 파이썬: 4. 클래스

Table of Content

파이썬을 공부한 내용을 정리한 글입니다. 파알못이라 이상하거나 틀린 내용이 있을 수 있습니다…

클래스와 객체(Class & Object)

클래스(Class)는 객체를 만들기 위한 일종의 골격, 청사진 같은 것입니다. 객체(Object)는 클래스를 기반으로 만들어진 인스턴스(Instance)를 가리킵니다.

클래스를 인간에 비유한다면 객체는 개인에 해당됩니다.

클래스 만들기

class Point:
    # 생성자
    def __init__(self, x, y):
        self.x = x # 클래스 내 멤버 변수
        self.y = y

    # 클래스의 모든 함수는 self 매개변수를 가짐
    def draw(self):
        print(f"Point ({self.x}, {self.y})")  # f-string

point = Point(1, 2)
point.draw()

print(type(point))  # <class '__main__.Point'>
print(isinstance(point, Point))  # point 객체가 Point의 인스턴스인지 출력 -> True
print(isinstance(point, int))  # point 객체가 int의 인스턴스인지 출력 -> False

클래스 명명법

파이썬에서 클래스 이름은 파스칼 표기법(각 단어를 대문자료 표기하는 방법)을 따릅니다.

self 매개변수

파이썬 클래스의 메소드는 self라는 매개변수를 가져야 합니다. self는 인스턴스 그 자체를 가리킵니다.

클래스의 생성자

파이썬 클래스의 생성자 이름은 __init__입니다. 참고로 양옆에 언더바 2개(__)가 붙는 함수를 매직 함수(Magic Method)라고 합니다.

클래스 속성과 인스턴스 속성(Class Attributes vs Instance Attributes)

클래스 속성은 클래스로부터 파생된 모든 인스턴스들이 공유하는 속성입니다.

인스턴스 속성은 각각의 인스턴스만이 갖는 고유한 속성입니다. 인스턴스 속성은 따로 수정하지 않는 한 클래스 속성의 값을 따라갑니다.

class Point:
    default_color = "Red"  # 클래스 속성 & 인스턴스 속성

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def draw(self):
        print(f"Point ({self.x}, {self.y})")  # f-string

point = Point(1, 2)
another = Point(3, 4)

# another 인스턴스 속성 수정
another.default_color = "Blue"

# point 인스턴스 속성: Red
print("point Instance Color:", point.default_color)
# another 인스턴스 속성: Red -> Blue
print("another Instance Color:", another.default_color)
# 클래스 속성: Red
print("Point Class Default Color:", Point.default_color)

# 클래스 속성은 수정 가능: Red -> Yellow
Point.default_color = "Yellow"
print("Modified Point Class Default Color:", Point.default_color)

# point 인스턴스 속성은 변경한 적이 없으므로 클래스 속성을 따라감: Red -> Yellow
print("point Instance Color:", point.default_color)
# another 인스턴스 속성은 위에서 Blue로 변경했으므로 Blue
print("another Instance Color:", another.default_color)

클래스 함수 vs 인스턴스 함수(Class Method vs Instance Method)

클래스 함수는 클래스에 소속된 함수로 클래스의 인스턴스를 생성할 필요 없이 호출이 가능한 함수입니다. 클래스 함수는 선언 시 @classmethod를 명시해줘야 하며 cls 매개변수를 가져야 합니다. cls는 클래스 그 자체를 가리킵니다.

인스턴스 함수는 생성된 인스턴스에 의해 호출이 가능한 함수입니다. 인스턴스 함수는 선언 시 self 매개변수를 가져야 합니다. self는 인스턴스 그 자체를 가리킵니다.

class Point:
    # 생성자: 인스턴스 자체를 가리키는 self 매개변수를 가져야 함
    def __init__(self, x, y):
        self.x = x
        self.y = y

    # 클래스 메소드: @classmethod를 명시해야 하며 클래스 자체를 가리키는 cls 매개변수를 가져야 함
    @classmethod
    def zero(cls):
        return cls(0, 0)  # 생성자 호출

    # 인스턴스 메소드: 인스턴스 자체를 가리키는 self 매개변수를 가져야 함
    def draw(self):
        print(f"Point ({self.x}, {self.y})")  # f-string

point = Point.zero()  # 클래스 메소드 호출
point.draw()  # 인스턴스 메소드 호출
# Point.draw() # 에러: draw()는 인스턴스 메소드이므로 인스턴스에서 호출해야 함

매직 함수(Magic Methods)

매직 함수는 함수의 이름 앞뒤에 언더바 2개(__)가 붙은 함수입니다. 이 함수는 객체나 클래스를 어떻게 사용하는지에 따라 파이썬 인터프리터에 의해 자동으로 호출됩니다.

예를 들어 클래스를 선언할 때 생성자 __init__를 선언했습니다. 이 생성자는 우리가 직접 호출하지 않았습니다. 단지 point = Point(1, 2) 라는 클래스의 인스턴스 생성 코드에 의해 호출됐을 따름입니다.

또 하나 예를 들어 클래스 내에 __str__ 매직 함수를 구현한다면 print() 함수로 인스턴스의 값을 어떻게 출력할 것인지 정할 수 있습니다.

class Point:
    # 생성자: 매직 함수
    def __init__(self, x, y):
        self.x = x
        self.y = y

    # 매직 함수: 
    def __str__(self):
        return f"이 Point 인스턴스의 값은 ({self.x}, {self.y}) 입니다."  # f-string

point = Point(1, 2)
print(point)  # 출력: 이 Point 인스턴스의 값은 (1, 2) 입니다.
print(str(point))  # 출력: 이 Point 인스턴스의 값은 (1, 2) 입니다.

매직 함수의 종류에 관해선 https://rszalski.github.io/magicmethods 를 참고하기 바랍니다.

객체 비교

아래 파이썬 코드를 돌려보면 True가 나올까요, False가 나올까요?

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

point = Point(1, 2)
other = Point(1, 2)
print(point == other)

두 객체는 값은 서로 같지만 서로 위치한 메모리 주소가 달라 참조하는 위치가 다르기 때문에 False가 출력됩니다. 두 객체의 참조가 아닌 값으로 비교하도록 하려면 비교 매직 함수(Comparison magic methods)를 클래스에 구현해야 합니다.

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    # equal 매직 함수 구현
    def __eq__(self, other):
        return self.x == other.x and self.y == other.y  # 값 비교

    # grater than 매직 함수 구현
    def __gt__(self, other):
        return self.x > other.x and self.y > other.y

point = Point(10, 20)
other = Point(1, 2)
print(point == other)  # __eq__ 매직 함수에 의해 값 비교
print(point > other)  # __gt__ 매직 함수에 의해 값 비교

이 외의 비교 매직 함수는 https://rszalski.github.io/magicmethods/#comparisons 를 참고하기 바랍니다.

산술 연산(Arithmetic Operations)

클래스 인스턴스의 산술 연산(+, -, *, / 등)을 위해 클래스 내에 산술 연산 매직 함수를 구현할 수 있습니다.

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    # 산술 연산(+) 매직 함수
    def __add__(self, other):
        return Point(self.x + other.x, self.y + other.y)

point = Point(10, 20)
other = Point(1, 2)

combined = point + other  # __add__ 매직 함수에 의한 연산
print(combined.x)
print(combined.y)

이 외의 산술 연산 매직 함수는 https://rszalski.github.io/magicmethods/#numeric 를 참고하기 바랍니다.

커스텀 컨테이너 만들기

딕셔너리 등의 자료를 클래스 내에 구현하는 경우 입맞에 맞게 자료구조를 변경할 수 있습니다.

아래 예제는 딕셔너리를 클래스에 구현하여 커스텀 컨테이너 형태로 만든 예제입니다. 딕셔너리에 키를 하나씩 등록할 때마다 값이 1씩 증가하며 대소문자 상관없이 동일하게 들어가게끔 구현하였습니다.

class TagCloud:
    def __init__(self):
        self.tags = {}  # 빈 사전

    def add(self, tag):
        # get(tag, value): 딕셔너리에서 tag에 해당하는 값을 찾아 반환. 값이 없으면 value를 반환
        self.tags[tag.lower()] = self.tags.get(tag.lower(), 0) + 1

    def __getitem__(self, tag):
        return self.tags.get(tag.lower(), 0)

    def __setitem__(self, tag, count):
        self.tags[tag.lower()] = count

    def __len__(self):
        return len(self.tags)

    def __iter__(self):
        return iter(self.tags)

cloud = TagCloud()

cloud.add("Python")  # add() 함수 호출
cloud.add("python")  # 대소문자 상관없이 모두 'python'이라는 키로 입력됨
cloud.add("PyThon")

cloud["python"] = 10  # __setitem__ 매직함수 호출
cloud["JavaScript"] = 20
cloud["React"] = 30

print(cloud["python"])  # __getitem__ 매직함수 호출
print(len(cloud))  # __len__ 매직함수 호출

for item in cloud:  # __iter__ 매직함수 호출
    print(item)

프라이빗 멤버(Private Members)

지금까지의 파이썬 코드는 외부에서 멤버 변수에 직접 접근이 가능합니다.

class TagCloud:
    def __init__(self):
        self.tags = {}

    def add(self, tag):
        self.tags[tag.lower()] = self.tags.get(tag.lower(), 0) + 1

    def __setitem__(self, tag, count):
        self.tags[tag.lower()] = count

cloud = TagCloud()
cloud.add("Python")
print(cloud.tags["python"])  # 멤버 변수에 직접 접근 가능

외부에서 멤버 변수에 직접 접근하는 것을 막으려면 멤버변수 이름 앞에 언더바 2개(__)를 붙입니다. 언더바 2개가 붙은 멤버변수는 외부에서 보이지 않는 프라이빗 멤버가 됩니다.

class TagCloud:
    def __init__(self):
        self.__tags = {}  # 빈 사전

    def add(self, tag):
        # get(tag, value): 딕셔너리에서 tag에 해당하는 값을 찾아 반환. 값이 없으면 value를 반환
        self.__tags[tag.lower()] = self.tags.get(tag.lower(), 0) + 1

    def __setitem__(self, tag, count):
        self.__tags[tag.lower()] = count

cloud = TagCloud()
cloud.add("Python")
# print(cloud.tags["python"])  # 멤버 변수에 직접 접근 불가

참고로, 우회하여 프라이빗 멤버에 직접 접근할 수 있는 방법이 있습니다. 클래스의 모든 속성을 지닌 __dict__ 딕셔너리에 정의된 이름을 통해 프라이빗 멤버에 직접 접근할 수 있습니다.

class TagCloud:
    def __init__(self):
        self.__tags = {}  # 빈 사전

    def add(self, tag):
        self.__tags[tag.lower()] = self.__tags.get(tag.lower(), 0) + 1

    def __setitem__(self, tag, count):
        self.__tags[tag.lower()] = count

cloud = TagCloud()
cloud.add("Python")

# __dict__: 클래스의 모든 속성을 지닌 딕셔너리
print(cloud.__dict__) # 출력값: {'_TagCloud__tags': {'python': 1}}

# 프라이빗 멤버에 직접 접근(tags -> _TagCloud__tags 이름을 통해)
print(cloud._TagCloud__tags) # 출력값: {'python': 1}

속성(Properties)

Java 등의 프로그래밍 언어에선 객체의 캡슐화를 위해 속성을 private로 선언한 후 getter, setter 함수를 통해 속성을 제어합니다. 아래 코드는 Java 식으로 구현한 코드입니다.

class Product:
    def __init__(self, price):
        self.set_price(price)

    def get_price(self):
        return self.__price

    def set_price(self, value):
        if(value < 0):
            raise ValueError("Prive cannot be negative.")
        self.__price = value

파이썬에서는 getter에 @property를, setter에 @PROPERTY_NAME.setter를 명시하여 속성을 구현합니다.

class Product:
    def __init__(self, price):
        self.__price = price

    @property  # price 프로퍼티의 getter
    def price(self):
        return self.__price

    @price.setter  # price 프로퍼티의 setter
    def price(self, value):
        if(value < 0):
            raise ValueError("Prive cannot be negative.")
        self.__price = value

product = Product(10)
product.price = 100 # setter를 통한 속성 값 변경
print(product.price) # getter를 통한 속성 값 출력

참고로 setter를 구현하지 않으면 해당 속성은 읽기전용이 됩니다.

class Product:
    def __init__(self, price):
        self.__price = price

    @property  # price 프로퍼티의 getter
    def price(self):
        return self.__price

product = Product(10)
# product.price = 100  # 에러! price 프로퍼티는 setter가 없으므로 읽기전용
print(product.price)  # getter를 통한 속성 값 출력

상속(Inheritance)

상속은 한 클래스의 멤버변수, 함수 등을 전달받는 것입니다. DRY(Don’t Repeat Yourself, 반복하지 마라) 원칙을 지키기 위한 방법이기도 합니다.

class Animal:
    def __init__(self):
        self.age = 1

    def eat(self):
        print("Eat")

class Mammal(Animal):  # Animal 클래스 상속
    def walk(self):
        print("Walk")

class Fish(Animal):  # Animal 클래스 상속
    def swim(self):
        print("Swim")

m = Mammal()
print(m.age) # Animal 클래스로부터 상속받은 멤버변수

object 클래스

object 클래스는 모든 클래스의 최상위 클래스입니다. 클래스 선언 시 상속할 클래스를 명시하지 않아도 암묵적으로 object 클래스를 상속받게 됩니다.

class Animal:  # 암묵적으로 object 클래스 상속
    # ...

class Mammal(Animal):  # Animal 클래스 상속
    # ...

o = object()  # object 클래스의 인스턴스
m = Mammal()

# 아래 출력 결과는 모두 True
print(isinstance(m, Mammal))
print(isinstance(m, Animal))
print(isinstance(m, object))
print(issubclass(Mammal, Animal))
print(issubclass(Animal, object))

부모 클래스의 생성자

부모 클래스의 생성자를 호출해야 부모 클래스에서 선언한 멤버변수와 함수에 접근이 가능합니다.

class Animal:
    def __init__(self):
        self.age = 1

    def eat(self):
        print("Eat")

class Mammal(Animal):
    def __init__(self):
        super().__init__()  # 부모 클래스의 생성자를 호출해야 상속받은 클래스의 멤버변수 및 함수에 접근 가능
        self.weight = 2

    def walk(self):
        print("Walk")

class Fish(Animal):
    def swim(self):
        print("Swim")

m = Mammal()
print(m.age)
print(m.weight)  # 상속받은 클래스의 멤버변수 접근

다단계 상속(Multi-level Inheritance)

다단계 상속은 여러 단계별로 클래스를 상속받는 것입니다. 다단계 상속은 프로그램의 복잡성을 야기합니다.

class Animal:
    def eat(self):
        pass

class Bird(Animal):
    def fly(self):
        pass

class Chicken(Bird): # 다단계 상속: Animal -> Bird -> Chicken)
    pass

다중 상속(Multiple inheritance)

다중 상속은 한 번에 여러 개의 클래스를 상속받는 것입니다. 파이썬은 다중 상속을 지원하지만 서로 연관성이 있는 클래스들을 다중 상속하면 버그를 유발하기 쉽습니다. 따라서 다중 상속을 사용해야 한다면 서로 연관성이 없는 클래스들을 상속받도록 구현하는 것이 좋습니다.

class Flyer:
    def fly(self):
        pass

class Swimmer:
    def swim(self):
        pass

class FlyingFish(Flyer, Swimmer): # 다중 상속: Flyer, Swimmer -> FlyingFish
    pass

올바른 상속 예

class InvalidOperationError(Exception):  # 사용자 정의 예외 클래스: Exception 클래스 상속
    pass

class Stream:
    def __init__(self):
        self.opened = False

    def open(self):
        if self.opened:
            raise InvalidOperationError("Stream is already open.")
        self.opened = True

    def close(self):
        if not self.opened:
            raise InvalidOperationError("Stream is already closed.")
        self.opened = False

class FileStream(Stream):
    def read(self):
        print("Reading data from a file")

class NetworkStream(Stream):
    def read(self):
        print("Reading data from a network")

추상 클래스

추상 클래스는 함수의 목록만을 가진 클래스입니다. 추상 클래스를 상속받는 클래스는 추상 클래스가 가진 함수를 구현해야 합니다. 따라서 추상 클래스는 상속받는 클래스에서 함수 구현을 강제하기 위해 사용됩니다.

추상 클래스는 추상 베이스 클래스(abc, Abstract Base Class) 모듈을 가져온 후 ABC 클래를 상속받아 구현합니다. 추상 클래스의 함수 선언부 앞에 @abstractmethod를 붙여 해당 함수가 추상 함수임을 명시합니다.

from abc import ABC, abstractmethod  # abc: 추상 베이스 클래스(Abstract Base Class)

# 추상 클래스: 메소드의 목록만을 갖는 클래스
class Stream(ABC):
    # 추상 메소드: 이 클래스를 상속받는 클래스는 반드시 이 메소드를 구현해야 함
    @abstractmethod
    def read(self):
        pass

# 추상 메소드를 구현한 클래스: 아래 코드에서 에러 발생하지 않음
class FileStream(Stream):
    def read(self):
        print("FilreStream Read")

# 추상 메소드를 구현하지 않은 클래스: 아래 코드에서 에러 발생
class NetworkStream(Stream):
    pass

# s = Stream() # 에러 발생: 추상 클래스의 인스턴스는 생성 불가
fs = FileStream()
fs.read()
# ns = NetworkStream() # 에러 발생: NetworkStream에서 추상 메소드를 구현하지 않음

다형성(Polymorphism)

다형성은 말 그대로 다양한(Many) 형태(Forms)를 갖는 성질입니다. 다형성을 통해 명칭은 같지만 동작이 다른 함수를 여러 개 구현할 수 있습니다.

아래 예제는 서로 동작이 다른 draw() 함수를 구현한 예입니다.

from abc import ABC, abstractmethod

class UIControl(ABC):
    @abstractmethod
    def draw(self):
        pass

class TextBox(UIControl):
    def draw(self):
        print("TextBox")

class DropDownList(UIControl):
    def draw(self):
        print("DropDownList")

def draw(controls):
    for control in controls:
        control.draw()

ddl = DropDownList()
textbox = TextBox()
draw([ddl, textbox])

빌트인 타입 확장(Extending Built-in Types)

자료형(int, str, list 등)을 상속받은 클래스는 자료형을 용도에 맞게 수정 또는 확장하여 사용할 수 있습니다.

class Text(str):
    def duplicate(self):
        return self + self

class TrackableList(list):
    def append(self, object):
        print(f"{object}을(를) 추가합니다.")
        super().append(object)

text = Text("Python")
print(text.duplicate())

tlist = TrackableList()
tlist.append("1")
print(tlist)

NameTuple 예제

from collections import namedtuple

Point = namedtuple("Point", ["x", "y"])
p1 = Point(1, 2)
p2 = Point(x=1, y=2)

print(p1)
print(p1 == p2) # True

“파알못 파이썬: 4. 클래스”의 하나의 댓글

댓글 남기기