파이썬을 배워보자 14일차 - 클래스2 (정적 메서드, 클래스 메서드, 상속)

0

Python

목록 보기
14/18

점프 투 파이썬 : https://wikidocs.net/book/1
파이썬 기본을 갈고 닦자 : https://wikidocs.net/16031
코딩 도장 : https://dojang.io/mod/page/view.php?id=2378

클래스2

1. 클래스 속성과 정적 메서드, 클래스 메서드

1.1 클래스 속성(변수)

이전에 클래스 맴버변수는 self.맴버변수로 선언하고 사용할 수 있다고 했다. 이는 사실 인스턴스의 맴버변수이다. 즉, 클래스 자체에서 값을 가지고 있는 것이 아닌, 인스턴스에 부여해주는 맴버변수인 것이다.

그렇다면 클래스 자체에 변수를 담을 수는 없을까?? 가령 Calculator 클래스에서 맴버변수로 PI = 3.14라고 잡아보자

class Calculator:
    def __init__(self):
        self.PI = 3.14    

이렇게 사용해야할 것이다. 그런데 생각해보면, PI값은 인스턴스에서 관리할 필요없이 클래스 자체에서가지고 있어 다른 곳에서 가져와 사용할 수 있도록하면 된다.

그래서 클래스 속성(변수)가 있는 것이다.

class 클래스이름:
    속성 =

헷갈리지 말자, 클래스 속성은 클래스 자체에 속하는 것이고, 클래스 맴버변수는 인스턴스에 부여되는 값이라, 인스턴스에서 관리한다.

class Calculator:
    PI = 3.14
    def printPI(self):
        print(self.PI)
        
calc1 = Calculator()
calc2 = Calculator()

calc1.printPI() # 3.14
calc2.printPI() # 3.14

다음과 같이 인스턴스 calc1, calc2에서 클래스 변수를 함께 쓰고 있는 것을 확인할 수 있다. 즉, 클래스 변수는 인스턴스 모두 공유하며 사용할 수 있는 것이다. 아직 와닿지 않은데, 리스트로 표현해보면 더 와닿는다.

class Bag:
    bag = []
    def __init__(self):
        self.instanBag = []
    def push_data(self,data):
        self.bag.append(data)
        self.instanBag.append(data)
        
bag1 = Bag()
bag2 = Bag()

bag1.push_data("name")
bag2.push_data("age")

# member variable(instance variable)
print(bag1.instanBag) # ['name']
print(bag2.instanBag) # ['age']

# class variable
print(bag1.bag) # ['name', 'age']
print(bag2.bag) # ['name', 'age']
print(Bag.bag) # ['name', 'age']

클래스 변수 : bag
맴버 변수(인스턴스 변수) : instanceBag

이 둘의 데이터를 채우는 메서드 push_data를 통해 데이터를 넣어봤다. 그리고 확인보면 instanceBagbag1, bag2가 각각 ['name'], ['age']로 다르게 나왔다. 이를 통해 instanceBag은 인스턴에서 관리되고 있음을 알 수 있다.

이와 달리 클래스 변수 Bag은 클래스 자체에서 관리하고 있기 때문에 bag1bag2가 넣은 data들이 모두 반영되어있음을 확인할 수 있다.

또한 호출하는 방식도 재밌는게, 인스턴스에서 클래스 변수를 호출할 수 있다.

파이썬은 속성(변수), 메서드를 호출하면 먼저 인스턴스에 해당 이름의 속성이나 메서드가 있는 지 확인하고 없으면 클래스 순으로 찾는다. 그래서 인스턴스 속성이 없으면 클래스를 찾으므로 클래스 변수가 동작해도 문제가 없는 것이다.

# class variable
print(bag1.bag) # ['name', 'age']
print(bag2.bag) # ['name', 'age']
print(Bag.bag) # ['name', 'age']

하지만 개인적으로 이는 맴버변수와 다를 바 없어보여서 좋아하진 않는다. 마지막 부분인 Bag.bag가 가장 좋은 클래스 변수 호출 방법 같아보인다.

또한, 클래스 내부에서도 클래스 변수 호출 방법은 클래스.변수로 쓰는게 좋다. self.변수로 쓰면 맴버변수와 헷갈리기 때문이다.

class Bag:
    bag = []
    def __init__(self):
        self.instanBag = []
    def push_data(self,data):
        Bag.bag.append(data) # changed -> self.bag.appned(data)
        self.instanBag.append(data)

다음처럼 클래스 변수임을 확실하게 표현하는 것이 좋다.

인스턴스와 클래스에는 __dict__라는 속성이 있는데, 이 속성을 이용하면 어떤 속성들과 메서드를 가지고 있는 지 확인할 수 있다.

print(bag1.__dict__) # {'instanBag': ['name']}
print(Bag.__dict__)
#  {'__module__': '__main__', 'bag': ['name', 'age'], '__init__': <function Bag.__init__ at 0x0000019EAACBEB00>, 'push_data': <function Bag.push_data at 0x0000019EAACBEB90>, '__dict__': <attribute '__dict__' of 'Bag' objects>, '__weakref__': <attribute '__weakref__' of 'Bag' objects>, '__doc__': None}

__dict__가 바로 클래스 변수와 인스턴스 맴버변수를 찾는 기준이 된다. 가령 클래스 변수인 bag를 찾는다고 하자.

  1. 인스턴스 bag1__dict__를 실행하여 해당 변수(bag)가 있는 지 확인한다.
  2. 없으면 bag1의 클래스인 Bag__dict__를 실행하여 해당 변수(bag)가 있는 지 확인한다.
  3. 인스턴스, 클래스에 해당 변수가 없다면 AttributeError를 반환한다.

1.2 정적 메서드, 클래스 메서드

그럼 클래스에서 관리하는 변수가 있다면, 클래스에서 관리하는 메서드도 있을 것 아닌가?? 것이 바로 정적 메서드, 클래스 메서드이다. 이는 인스턴스.메서드()로 호출하는 방식이 아닌 클래스.메서드()로 호출하는 방식이다. 물론 그렇다고 인스턴스.메서드() 로 호출이 불가능한 것은 아니다. 다만 추천을 안한다는 것이다.

정적 메서드를 선언하는 방식은 재밌는 문법을 사용하는데, @staticmethod를 메서드 위에 붙여주면 된다. @문법은 '데코레이터 문법'이라고 하는데 추후에 배울 것이다.

class Calculator:
    @staticmethod
    def add(a, b):
        return a + b

클래스 메서드를 선언하는 방식은 @classmethod를 메서드 위에 붙여주면 된다. 단, 인자의 첫번째에 class를 넣어주어야 하는데, 이는 호출 시 생략해도 된다.

class Calculator:
    @classmethod
    def add(cls, a, b):
        return a + b

clsclass로 안써주면 에러가 발생한다.

사실 둘이 하는 기능은 똑같다. 다만 상속에 있어서의 차이가 있다.

class Calculator:
    @staticmethod
    def add(a, b):
        return a + b
    
    @classmethod
    def sub(cls,a,b):
        return a - b
    
    def mul(self,a,b):
        return a * b

print(Calculator.add(1,2)) # 3
print(Calculator.sub(3,1)) # 2
print(Calculator.mul(2,3)) # TypeError: Calculator.mul() missing 1 required positional argument: 'b'

인스턴스 메서드는 클래스에서 호출하면 에러가 발생하는 것을 확인할 수 있다. 재밌는 것은 @classmethodsub의 인자로 cls를 받는데, 호출할 때는 Caculator.sub(3,1)cls인자를 무시하고 호출할 수 있다. 이는 시스템에서 알아서 cls를 넘겨주기 때문이다.

정적 메서드와 클래스 메서드는 무슨 차이가 있을까?? 이는 상속에 있어서 차이가 있다. 상속은 뒤에 나올텐데 지금은 그냥 그런게 있구나 싶은 상태에서 보면 된다.

B가 A를 상속받았다란 A라는 클래스의 속성, 메서드를 B가 갖게 되었다는 것이다. 그래서 B는 A의 속성, 메서드를 쓸 수 있다.

class ParentClass:
    name ="ParentClass"
    
    @staticmethod
    def static_printName():
        print(ParentClass.name)
    
    @classmethod
    def class_printName(cls):
        print(cls.name)
    
class TestClass(ParentClass):
    name = "TestClass"

TestClass.static_printName() # ParentClass
TestClass.class_printName() # TestClass

TestClassParentClass를 상속받았다. 때문에 TestClassParentClassstatic_printNameclass_printName을 실행할 수 있다.

이 때, static_printNameParentClassname을 호출하여 ParentClass가 나온다. 반면, class_printNamecls를 받는데, 이는 class이다. 이 class가 바로 현재 자신(class_printName)을 호출하는 class를 말한다.

때문에 현재 class_printName을 호출하는 classTestClass이므로 nameTestClass가 나오는 것이다.

정리하면 static_printNameclass_printName의 차이는 class_printNameclass를 입력받아 해당 메서드를 호출할 때의 클래스에 기준으로 메서드를 맞게 실행한다는 특징이 있다.

1.3 왜 정적 메서드, 클래스 메서드를 사용해야할까??

클래스에 국한된 정적 메서드, 클래스 메서드순수 함수여야 의미가 있다.

순수함수: 부수효과(side effect)가 없고 항상 같은 인자에는 같은 결과를 내는 함수를 말한다. 부수 효과란 함수 외부의 상태(변수)를 변경하거나 또는 함수로 들어온 매개변수의 값을 변경하여 외부에 영향을 미치는 것을 말한다. 즉, 순수함수는 외부의 상태는 변경하지 않고, 입력된 인자에 대해서 항상 멱등성이 보장되어야 한다는 것이다.

정리하면 순수함수는 외부 상태를 어떤 식으로든 변경하지않고, 외부 상태에도 의존하지 않아 동일한 인자에 대해 동일한 결과를 반환하는 함수를 말한다

가령 다음과 같은 경우는 순수함수가 아니다.

  1. 외부 변수를 변경하는 경우
a = 2

def unpureDef():
    global a
    a = 20

unpureDef()
print(a) # 20

다음은 대놓고 전역변수인 a값을 변경하기 때문에 순수함수가 아니다.

  1. 매개변수를 통해 값을 변경
a = []

def unpureDef(value):
    value.append(2)

unpureDef(a)
print(a) # [2]

그냥 매개변수의 값을 변경하는 것은 문제가 안된다. 어차피 매개변수는 지역변수이기 때문이다. 그러나 list처럼 참조변수가 넘어가는 가변 변수들은 변경된 상태가 함수가 종료되어도 유지된다. 이런 경우는 순수함수가 아니다.

  1. 같은 인자에 같은 결과
c  = 3

def unpure(a, b):
    if c == 3:
        return a + b
    else:
        return a - b

print(unpure(10,20)) # 30
c = 4 
print(unpure(10,20)) # -10

다음의 경우는 같은 인자 unpure(10,20)을 넘겨주었음에도 불구하고 결과가 달라진다. 이는 해당 함수가 외부 상태에 의존한다는 특성이 있으므로 순수함수가 아니다.

순수함수를 사용하는 이유는 모듈화 수준을 높여 유지, 보수에 좋기 때문이다.

따라서, 계산기 같이 항상 같은 입력에 같은 결과를 내보내며 외부 상태를 변경하지 않는 메서드는 순수함수이므로 클래스의 정적 메서드로 다루어지는 것이 유지보수에 좋다.

class Calculator:
    @staticmethod
    def add(a,b):
        return a + b

print(Calculator.add(1,2)) # 3

이렇게 된다.

2. 클래스의 상속

상속은 무언가를 물려받는다는 의미이다. 기능을 물려주는 클래스를 base class(기반 클래스) 또는 parent class(부모 클래스)라고 하고, 상속 받아 새롭게 만드는 클래스를 derived class(파생 클래스), children class(자식 클래스)라고 한다.

다음과 같은 관계도로 그릴 수 있으며, 상속 받은 클래스인 자식 클래스부모 클래스속성, 메서드를 사용할 수 있다.

상속은 다음과 같이 받을 수 있다.

class 상속받을클래스(상속할클래스):
    본문

이렇게하면 상속받을클래스상속할클래스의 속성과 메서드를 사용할 수 있게 된다.

계산기 예제를 보도록 하자

class Calculator:
    def __init__(self, a , b):
        self.a = a
        self.b = b
    
    def add(self):
        return self.a + self.b

    def sub(self):
        return self.a - self.b
    
    def mul(self):
        return self.a * self.b
    
    def div(self):
        return self.a // self.b

다음의 계산기에 제곱 기능인 pow를 제공한다고 하자, Calculator에 속성하나만 추가하면 되긴하지만, 상속을 통해서도 구현할 수 있다.

class Calculator:
    def __init__(self, a , b):
        self.a = a
        self.b = b
    
    def add(self):
        return self.a + self.b

    def sub(self):
        return self.a - self.b
    
    def mul(self):
        return self.a * self.b
    
    def div(self):
        return self.a // self.b
    
class PowCalculator(Calculator):
    def pow(self):
        res = self.add()
        print(res) # 5
        return self.a ** self.b

pCal = PowCalculator(3,2)
print(pCal.pow()) # 9

PowCalculatorPowCalculator(Calculator)Calculator를 상속받았다. 때문에 Calculator의 속성과 메서드를 사용할 수 있게 되었다.

def pow에서는 self.a와 self.b가 있는데 이는 PowCalculator가 아니라 Calculator의 맴버변수이다. 이들을 가져와서 자식 클래스에서 자신의 메서드에 사용하는 것이다.

또한, 자식 클래스인 PowCalculator에서 Calculator의 메서드인 add를 호출할 수도 있다.

이것이 상속이 가지는 힘이다.

정리: 상속을 받으면 자식 클래스는 부모 클래스의 속성과 메서드를 사용할 수 있게된다.

2.1 상속 관계 확인하기

클래스의 상속 관계를 확인하고 싶을 때는 issubclass 함수를 사용한다. 즉, 클래스가 부모 클래스를 상속받은 자식 클래스인지를 확인한다. 맞으면 True, 아니면 False이다.

사용 방법은 다음과 같다.

issubclass(자식, 부모) # 자식이 부모 클래스를 상속받았나?
print(issubclass(PowCalculator,Calculator)) # True

2.2 상속 관계와 포함 관계

상속 관계와 포함 관계의 차이는 무엇일까??

  1. 상속 관계: 상속 관계는 자식 is-a 부모 관계이다.
  2. 포함 관계: 포함 관계는 누군가 has-a 무엇을 관계이다.

무슨 말인지 헷갈릴텐데, A is-a B관계인 상속A와 B가 동등한 관계일 때 사용한다. 즉, AB가 될 수 있다.

가령, 학생사람의 속성과 메서드를 상속받을 수 있다. 왜냐하면 학생사람이기 때문이다. 즉 학생 is a 사람이다. 반대로, 사람학생을 상속받을 수 없다. 사람 is a 학생은 안되기 때문이다.

class People:
    pass

class Student(People):
    pass

PeopleStudentis a 관계이기 때문에 상속 관계이다.

포함관계는 A has-a B관계인데, 이는 동등 관계가 아니라, AB를 가지고 있을 때 쓰는 관계이다.

가령, 사람감정을 가지고 있다. 그렇다면 사람 has a 감정이 성립한다. 이 경우에는 상속 관계가 아닌 포함 관계가 된다. 포함 관계는 코드 상에서 A클래스가 B를 맴버 변수로 갖고 있으면 된다.

class People:
    def __init__(self):
        self.mind = "두근두근"

Peoplemindhas a관계이기 때문에 포함 관계이다.

2.3 부모 클래스 초기화하기

이전의 코드에서 자식 클래스의 생성자를 추가하여 실행해보도록 하자

class Calculator:
    def __init__(self, a , b):
        self.a = a
        self.b = b
        print("Cal start")

    def add(self):
        return self.a + self.b
        
class PowCalculator(Calculator):
    def __init__(self,label):
        print(label)

    def pow(self):
        res = self.add()
        print(res) 
        return self.a ** self.b

pCal = PowCalculator("sub")
print(pCal.pow()) # 'PowCalculator' object has no attribute 'a'

다음을 보면, 에러가 발생하는데 self.a가 없다는 에러이다. 왜냐하면 부모 클래스인 Calculator의 생성자 함수인 __init__이 실행되지 않았기 때문이다. 그래서 self.a , self.b가 정의되지 않은 것이다.

그렇다면 어떻게 부모 클래스의 생성자를 실행시켜줄 수 있을까?? 부모 클래스에서 정의된 메서드에 접근하기위해서 self.메서드()를 썼으니까, self.__init__()를 쓰면 되지 않을까?? 그런데, 그건 현재 자식 클래스의 self.__init__()를 지칭하기 때문에 의미가 없다.

그래서 자식 클래스와 부모 클래스의 메서드가 이름이 같은 경우, 부모 클래스의 메서드를 지칭해서 실행시켜줄 수 있는 방법이 있다. 바로 super()를 사용하면 해당 문제를 해결할 수 있다.

super().메서드()
class Calculator:
    def __init__(self, a , b):
        self.a = a
        self.b = b
        print("Cal start")

    def add(self):
        return self.a + self.b

class PowCalculator(Calculator):
    def __init__(self,a,b,label):
        super().__init__(a,b)
        print(label)

    def pow(self):
        res = self.add()
        print(res) # 5
        return self.a ** self.b

pCal = PowCalculator(3,2,"sub") # Cal start 다음으로 sub가 실행된다.
print(pCal.pow()) # 9

super().__init__()을 추가하여 부모 클래스의 생성자를 실행시켜준 것이다. 부모 클래스의 생성자가 먼저 실행되기 때문에 자식 클래스의 label이 늦게 출력된다.

물론, 자식 클래스에서 생성자 메서드인 __init__을 사용하지 않으면 부모 클래스의 생성자 메서드 __init__과 충돌하지 않아 문제가 발생하지 않는다.

super()을 좀 더 명확하게 사용하는 방법이 있다. 첫번째는 인자는 자식 클래스를 두 번째는 self를 넘겨주는 것이다.

super(자식클래스, self).메서드()
class PowCalculator(Calculator):
    def __init__(self,a,b,label):
        super(PowCalculator, self).__init__(a,b)
        print(label)

다음과 같이 말이다. 동작하는 기능은 같지만, 어떤 부모 클래스를 어디서 호출하고 있다는 것을 명확하게 표현할 수 있는 방법이다. 이것을 사용하는 이유는 파이썬이 다중 상속을 지원하기 때문이다.

2.4 메서드 오버라이딩

파이썬은 메서드 오버로딩은 불가능하지만, 메서드 오버라이딩은 가능하다. 오버라이딩은 자식 클래스가 부모 클래스와 똑같은 메서드(매개변수, 이름 모두 같아야 한다)를 정의하여 부모 클래스의 메서드는 무시하고 자식 클래스의 메서드를 실행시키는 것을 말한다.

정리하면 다음과 같다.

  1. 오버로딩 : 함수이름이 같고, 매개변수 타입이나 매개변수 수가 달라야한다. 상속 관계가 아닌 하나의 클래스 안에서의 관계이다.
  2. 오버라이드 : 함수이름이 같고, 매개변수 타입, 매개변수 수가 같아야 한다. 상속 관계를 가져야 한다.
    중요한 것은 둘 다 반환 타입이랑은 관련없다 많이들 반환타입도 연관있다고 생각하는데 관련없다.

이미 우린 위에서 __init__을 오버라이딩 한 것이다.

class Person:
    def greeting(self):
        return 123942

class Student(Person):
    def greeting(self):
        return "hello!!"

student = Student()
print(student.greeting()) # hello!!

다음의 예제를 보면 Person 클래스의 greeting을 자식 클래스인 Student에서 greeting으로 오버라이딩한 것이다. 리턴 값도 다르고 타입도 다르다.

그런데, 여기서 만약 부모 클래스의 메서드를 호출하고 싶다면 어떻게 해야할까?? 그건 이전에 배운 super()을 사용하면 된다. super()은 부모 클래스가 가진 속성, 메서드에 접근이 가능하기 때문이다.

class Person:
    def greeting(self):
        return 123942

class Student(Person):
    def greeting(self):
        ret = super().greeting()
        print(ret) # 123942
        return "hello!!"

student = Student()
print(student.greeting()) # hello!!

다음과 같이 super().greeting()을 하면 부모 클래스의 greeting을 호출하고 리턴값까지 받을 수 있다.

2.6 다중 상속 사용하기

다중 상속을 지원하는 언어는 대표적으로 c++이고, 단일 상속을 지원하는 언어는 java이다. 재밌게도 파이썬은 다중 상속을 지원한다.

다중 상속의 모습은 다음과 같다.

class 부모클래스1:
    본문

class 부모클래스2:
    본문

class 자식클래스(부모클래스1, 부모클래스2):
    본문

굉장히 간단하게 다중 상속을 구현할 수 있다.

다음의 다중 상속 예시를 보도록 하자

class Person:
    def eat(self):
        print("yam yam")

class Minor:
    def smoking(self):
        print("don't do that")

class Student(Person, Minor):
    def greeting(self):
        print("hello world")

student = Student()
student.eat() # yam yam
student.smoking() # don't do that
student.greeting() # hello world

학생은 사람이다., 학생은 미성년자이다.라고 가정을 해보자. 그렇다면 학생사람미성년자를 다중 상속할 수 있다.

위의 예시와 같이 학생사람미성년자가 가진 메서드를 호출할 수 있으며, 속성이 있으면 가질 수 있다.

그런데 다중 상속에는 아주 중요한 문제가 있다.

2.7 다이아몬드 상속

다중 상속의 가장 큰 문제점인 죽음의 다이아몬드이다. 다음 예시를 보면 쉽게 이해가 간다.

class A:
    def greeting(self):
        print("hello A")

class B(A):
    def greeting(self):
        print("hello B")
    
class C(A):
    def greeting(self):
        print("hello C")
    
class D(B, C):
    pass

d = D()
d.greeting() # hello B    

그림으로 표현하면 다음과 같다.

B, CA를 상속받고, DB, C를 상속받는다. 이 때 A, B ,C모두 greeting메서드를 갖고 있을 때, D는 누구의 greeting메서드를 호출할까? 라는 것이다.

다중 상속이 가진 가장 큰 문제점이며, 이를 죽음의 다이아몬드라고 부른다.

이에 대한 해결책으로 파이썬은 메서드 탐색 순서(Method Resolution Order, MRO)를 따른다.

메서드 탐색 순서는 다음을 통해 얻을 수 있다.

class.mro()
class.__mro__

두 개 모두 같은 결과를 얻을 수 있다.

그럼 D에서 mro가 무엇인지 찾아보도록 하자

print(D.mro()) # [<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]

D 클래스mro는 자기 자신 D 먼저, 그 다음 B, C, A이다.

d.greeting() # hello B    

따라서 다음과 같이 죽음의 다이몬드에서 Bgreeting을 실행한 것은 mro순서가 D다음 B이기 때문이다.

사실 mro의 순서는 굉장히 간단합니다. 수직적인 상속관계에서 자신에게 가장 가까운 부모가 자신보다 먼 부모보다 mro가 가깝다. 이는 A보다 B,C가 가까운 것을 통해 알 수 있다.

또한, 수평적인 형제라인에서는 먼저 상속한 순서대로 mro가 형성된다. 즉, B, C는 서로 형제이지만, B가 먼저 상속을 하였으므로 D의 두 부모 B, CB가 `mro에 더 먼저 나오게되는 것이다.

정리하면 다음과 같다.

  1. 가까울 수록 mro는 높다. 즉, 직계 부모가 할아버지 라인보다 더 mro가 높다.
  2. 부모들 중 가장 먼저 상속한 클래스가 자식 클래스에서 mro가 더 높다.

그런데 mro 결과를 잘보면 이런게 있다.

<class 'object'>

분명 상속을 받은 적이 없는데, object 클래스를 상속받았다고 나온다. 이는 시스템에서 알아서 해준 것이다.

2.8 object

사실 파이썬에서 object모든 클래스의 조상이다. 그래서 intmro를 출력하면 자기 자신과 object가 나온다.

print(int.mro()) # [<class 'int'>, <class 'object'>]

즉, 모든 타입들, 모든 클래스는 object를 상속받으므로 파이썬의 모든 타입은 객체이다.

0개의 댓글