점프 투 파이썬 : https://wikidocs.net/book/1
파이썬 기본을 갈고 닦자 : https://wikidocs.net/16031
코딩 도장 : https://dojang.io/mod/page/view.php?id=2378
이전에 클래스 맴버변수는 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
를 통해 데이터를 넣어봤다. 그리고 확인보면 instanceBag
은 bag1
, bag2
가 각각 ['name']
, ['age']
로 다르게 나왔다. 이를 통해 instanceBag
은 인스턴에서 관리되고 있음을 알 수 있다.
이와 달리 클래스 변수 Bag
은 클래스 자체에서 관리하고 있기 때문에 bag1
과 bag2
가 넣은 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
를 찾는다고 하자.
bag1
에 __dict__
를 실행하여 해당 변수(bag
)가 있는 지 확인한다.bag1
의 클래스인 Bag
에 __dict__
를 실행하여 해당 변수(bag
)가 있는 지 확인한다.AttributeError
를 반환한다.그럼 클래스에서 관리하는 변수가 있다면, 클래스에서 관리하는 메서드도 있을 것 아닌가?? 것이 바로 정적 메서드, 클래스 메서드
이다. 이는 인스턴스.메서드()
로 호출하는 방식이 아닌 클래스.메서드()
로 호출하는 방식이다. 물론 그렇다고 인스턴스.메서드()
로 호출이 불가능한 것은 아니다. 다만 추천을 안한다는 것이다.
정적 메서드
를 선언하는 방식은 재밌는 문법을 사용하는데, @staticmethod
를 메서드 위에 붙여주면 된다. @
문법은 '데코레이터 문법'이라고 하는데 추후에 배울 것이다.
class Calculator:
@staticmethod
def add(a, b):
return a + b
클래스 메서드
를 선언하는 방식은 @classmethod
를 메서드 위에 붙여주면 된다. 단, 인자의 첫번째에 class
를 넣어주어야 하는데, 이는 호출 시 생략해도 된다.
class Calculator:
@classmethod
def add(cls, a, b):
return a + b
cls
는 class
로 안써주면 에러가 발생한다.
사실 둘이 하는 기능은 똑같다. 다만 상속에 있어서의 차이가 있다.
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'
인스턴스 메서드는 클래스에서 호출하면 에러가 발생하는 것을 확인할 수 있다. 재밌는 것은 @classmethod
인 sub
의 인자로 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
TestClass
는 ParentClass
를 상속받았다. 때문에 TestClass
는 ParentClass
의 static_printName
과 class_printName
을 실행할 수 있다.
이 때, static_printName
은 ParentClass
의 name
을 호출하여 ParentClass
가 나온다. 반면, class_printName
은 cls
를 받는데, 이는 class
이다. 이 class
가 바로 현재 자신(class_printName
)을 호출하는 class
를 말한다.
때문에 현재 class_printName
을 호출하는 class
는 TestClass
이므로 name
이 TestClass
가 나오는 것이다.
정리하면 static_printName
와 class_printName
의 차이는 class_printName
는 class
를 입력받아 해당 메서드를 호출할 때의 클래스에 기준으로 메서드를 맞게 실행한다는 특징이 있다.
클래스에 국한된 정적 메서드, 클래스 메서드
는 순수 함수
여야 의미가 있다.
순수함수:
부수효과(side effect)
가 없고 항상 같은 인자에는 같은 결과를 내는 함수를 말한다. 부수 효과란 함수 외부의 상태(변수)를 변경하거나 또는 함수로 들어온 매개변수의 값을 변경하여 외부에 영향을 미치는 것을 말한다. 즉, 순수함수는 외부의 상태는 변경하지 않고, 입력된 인자에 대해서 항상 멱등성이 보장되어야 한다는 것이다.
정리하면 순수함수는 외부 상태를 어떤 식으로든 변경하지않고, 외부 상태에도 의존하지 않아 동일한 인자에 대해 동일한 결과를 반환하는 함수를 말한다
가령 다음과 같은 경우는 순수함수가 아니다.
a = 2
def unpureDef():
global a
a = 20
unpureDef()
print(a) # 20
다음은 대놓고 전역변수인 a값을 변경하기 때문에 순수함수가 아니다.
a = []
def unpureDef(value):
value.append(2)
unpureDef(a)
print(a) # [2]
그냥 매개변수의 값을 변경하는 것은 문제가 안된다. 어차피 매개변수는 지역변수이기 때문이다. 그러나 list
처럼 참조변수가 넘어가는 가변
변수들은 변경된 상태가 함수가 종료되어도 유지된다. 이런 경우는 순수함수가 아니다.
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
이렇게 된다.
상속은 무언가를 물려받는다는 의미이다. 기능을 물려주는 클래스를 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
PowCalculator
는 PowCalculator(Calculator)
로 Calculator
를 상속받았다. 때문에 Calculator
의 속성과 메서드를 사용할 수 있게 되었다.
def pow
에서는 self.a와 self.b
가 있는데 이는 PowCalculator
가 아니라 Calculator
의 맴버변수이다. 이들을 가져와서 자식 클래스에서 자신의 메서드에 사용하는 것이다.
또한, 자식 클래스인 PowCalculator
에서 Calculator
의 메서드인 add
를 호출할 수도 있다.
이것이 상속이 가지는 힘이다.
정리: 상속을 받으면 자식 클래스는 부모 클래스의 속성과 메서드를 사용할 수 있게된다.
클래스의 상속 관계를 확인하고 싶을 때는 issubclass
함수를 사용한다. 즉, 클래스가 부모 클래스를 상속받은 자식 클래스인지를 확인한다. 맞으면 True
, 아니면 False
이다.
사용 방법은 다음과 같다.
issubclass(자식, 부모) # 자식이 부모 클래스를 상속받았나?
print(issubclass(PowCalculator,Calculator)) # True
상속 관계와 포함 관계의 차이는 무엇일까??
자식 is-a 부모
관계이다. 누군가 has-a 무엇을
관계이다.무슨 말인지 헷갈릴텐데, A is-a B
관계인 상속
은 A와 B가 동등한 관계일 때 사용한다. 즉, A
는 B
가 될 수 있다.
가령, 학생
은 사람
의 속성과 메서드를 상속받을 수 있다. 왜냐하면 학생
은 사람
이기 때문이다. 즉 학생 is a 사람
이다. 반대로, 사람
은 학생
을 상속받을 수 없다. 사람 is a 학생
은 안되기 때문이다.
class People:
pass
class Student(People):
pass
People
과 Student
는 is a
관계이기 때문에 상속 관계이다.
포함관계는 A has-a B
관계인데, 이는 동등 관계가 아니라, A
가 B
를 가지고 있을 때 쓰는 관계이다.
가령, 사람
은 감정
을 가지고 있다. 그렇다면 사람 has a 감정
이 성립한다. 이 경우에는 상속 관계가 아닌 포함 관계가 된다. 포함 관계는 코드 상에서 A
클래스가 B
를 맴버 변수로 갖고 있으면 된다.
class People:
def __init__(self):
self.mind = "두근두근"
People
과 mind
는 has a
관계이기 때문에 포함 관계이다.
이전의 코드에서 자식 클래스의 생성자를 추가하여 실행해보도록 하자
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)
다음과 같이 말이다. 동작하는 기능은 같지만, 어떤 부모 클래스를 어디서 호출하고 있다는 것을 명확하게 표현할 수 있는 방법이다. 이것을 사용하는 이유는 파이썬이 다중 상속을 지원하기 때문이다.
파이썬은 메서드 오버로딩은 불가능하지만, 메서드 오버라이딩은 가능하다. 오버라이딩
은 자식 클래스가 부모 클래스와 똑같은 메서드(매개변수, 이름 모두 같아야 한다)를 정의하여 부모 클래스의 메서드는 무시하고 자식 클래스의 메서드를 실행시키는 것을 말한다.
정리하면 다음과 같다.
이미 우린 위에서 __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
을 호출하고 리턴값까지 받을 수 있다.
다중 상속을 지원하는 언어는 대표적으로 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
학생은 사람이다.
, 학생은 미성년자이다.
라고 가정을 해보자. 그렇다면 학생
은 사람
과 미성년자
를 다중 상속할 수 있다.
위의 예시와 같이 학생
은 사람
과 미성년자
가 가진 메서드를 호출할 수 있으며, 속성이 있으면 가질 수 있다.
그런데 다중 상속에는 아주 중요한 문제가 있다.
다중 상속의 가장 큰 문제점인 죽음의 다이아몬드이다. 다음 예시를 보면 쉽게 이해가 간다.
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, C
는 A
를 상속받고, D
는 B, 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
따라서 다음과 같이 죽음의 다이몬드에서 B
의 greeting
을 실행한 것은 mro
순서가 D
다음 B
이기 때문이다.
사실 mro
의 순서는 굉장히 간단합니다. 수직적인 상속관계에서 자신에게 가장 가까운 부모가 자신보다 먼 부모보다 mro
가 가깝다. 이는 A
보다 B,C
가 가까운 것을 통해 알 수 있다.
또한, 수평적인 형제라인에서는 먼저 상속한 순서대로 mro
가 형성된다. 즉, B, C
는 서로 형제이지만, B
가 먼저 상속을 하였으므로 D
의 두 부모 B, C
중 B
가 `mro
에 더 먼저 나오게되는 것이다.
정리하면 다음과 같다.
그런데 mro
결과를 잘보면 이런게 있다.
<class 'object'>
분명 상속을 받은 적이 없는데, object
클래스를 상속받았다고 나온다. 이는 시스템에서 알아서 해준 것이다.
사실 파이썬에서 object
모든 클래스의 조상이다. 그래서 int
의 mro
를 출력하면 자기 자신과 object
가 나온다.
print(int.mro()) # [<class 'int'>, <class 'object'>]
즉, 모든 타입들, 모든 클래스는 object
를 상속받으므로 파이썬의 모든 타입은 객체이다.