-
Notifications
You must be signed in to change notification settings - Fork 0
2.09. 내용정리: 9일차
객체지향이란, 하나의 프로그래밍 패러다임으로써 객체(object)를 중심으로 사고하면서 프로그래밍을 하는 것을 의미한다.
-
객체(Object): 속성(특성 및 특징)과 행동(행위)을 가지는 주체를 의미한다.
- 객체의 속성(특성과 특징; attribute)은 객체가 가지는 데이터(변수)를 의미하며, 객체에 정의된 변수를 멤버 변수(member variable) 라고 한다.
- 일반적으로
Python
,C++
에서는 멤버 변수,JAVA
에서는 필드(field; 혹은 멤버 필드) 라고 표현한다. - 객체의 속성을 의미하는 어트리뷰트(attribute) 라는 표현도 자주 쓰인다.
- 일반적으로
- 객체의 행동은 함수를 의미하며, 객체에 정의된 함수를 메서드(method) 라고 한다.
- 일반적으로
C++
에서는 멤버함수(member function),Python
,JAVA
에서는 메서드 라고 한다.
- 일반적으로
- 객체의 속성(특성과 특징; attribute)은 객체가 가지는 데이터(변수)를 의미하며, 객체에 정의된 변수를 멤버 변수(member variable) 라고 한다.
-
클래스(Class): 객체를 프로그래밍 언어로 구현한 것을 의미한다.
- 클래스에 정의된 변수를 클래스 변수(class variable) 혹은 클래스 전역 변수라고 한다.
-
인스턴스(Instance): 정의된 클래스를 선언하여 메모리에 생성된 객체를 의미한다.
- 인스턴스화(Instantiate): 새로운 객체를 생성한다는 의미로써, 메모리에 새로운 객체를 생성시킨다는 것읠 의미한다.
- 인스턴스 변수(instance variable): 인스턴스화를 통해 생성된 객체가 가지고 있는 멤버 변수를 의미하며, 인스턴스화를 통해서만 사용이 가능하다.
- 생성자(Constructor): 객체가 생성될 때(인스턴스화) 호출되는 함수(메서드)이며, 주로 객체의 초기화 작업을 담당한다.
-
추상화(Abstraction): 어떤 종류의 대상들에 대해 그것이 가져야 할 핵심적인 특징들을 가지는 모델을 만드는 것이다.
- 즉, 객체가 가진 수많은 특징과 행동들 중에서 프로그래밍에 필요한 핵심적인 요소들만을 모델로 구현하는 것을 의미한다.
-
상속(Inheritance): 상위 객체(부모 객체라고도 함)로부터 하위 객체(자식 객체라고도 함)가 특성 및 특징과 행동을 물려받는 것을 의미한다.
- 오버라이딩(Overriding): 상위 객체가 가지고 있는 속성과 메서드를 하위 객체에서 재정의하여 사용하는 것을 의미한다.
-
캡슐화(Encapsulation): 객체가 가지고 있는 속성을 캡슐처럼 감싸 객체가 가진 데이터를 보호하는 것을 의미하며,
C++
과JAVA
와 같은 객체지향 프로그래밍 언어에서는 아래와 같은 키워드로 접근 단계를 나눈다.- private: 객체 내부에서만 접근 가능한 데이터
- protected: 다른 객체에서는 접근 불가능하지만, 상속을 받은 하위 객체는 접근 가능한 데이터
- public: 객체 외부에서도 접근 가능한 데이터
-
다형성(Polymorphism): 하나의 객체가 여러 가지 형태를 취해 상황에 따라 다른 방식으로 해석이 가능한 것을 의미한다.
-
오버로딩(Overloading): 같은 이름의 여러 메서드가 다른 매개 변수를 갖도록 하는 것을 의미한다.
- 다른 객체지향 프로그래밍 언어에서는 지원하는 기술이지만, 파이썬에서는 메서드 오버로딩은 불가능하다고 한다.
- 단, 파이썬에서는 연산자 오버로딩(Operator Overloading; 인스턴스 객체끼리 서로 연산을 할 수 있게끔 기존에 있는 연산자의 기능을 바꾸어 중복으로 정의하는 것)은 가능하다고 한다.
-
오버로딩(Overloading): 같은 이름의 여러 메서드가 다른 매개 변수를 갖도록 하는 것을 의미한다.
파이썬 공식 문서의 용어집의 객체(object)
항목 에서는 다음과 같이 설명하고 있다.
상태 (어트리뷰트나 값) 를 갖고 동작 (메서드) 이 정의된 모든 데이터. 또한, 모든 뉴스타일 클래스 의 최종적인 베이스 클래스이다.
이를 요약하자면 다음과 같다.
- 객체란, 속성과 행위를 갖는 주체이다.
- 파이썬에서의 모든 타입(type)은 객체이다.
- 파이썬에서 모든 타입의 상위(부모) 클래스는
object
클래스이다.
파이썬 공식 문서의 용어집의 클래스(class)
항목 에서는 다음과 같이 설명하고 있다.
사용자 정의 객체들을 만들기 위한 주형이다. 클래스 정의는 보통 클래스의 인스턴스를 대상으로 연산하는 메서드 정의들을 포함한다.
이를 요약하자면 다음과 같다.
- 클래스란, 객체의 구조(속성과 행위)을 프로그래밍 언어로 정의한 것이다.
- 객체의 클래스는 초기화를 통해 제어한다.
- 클래스를 작성하기 위해서는
class
키워드 사용하여 새로운 클래스를 작성한다. - 또한, 파이썬 명명 규칙 (PEP8) 에 의해 클래스명은 파스칼 표기법으로, 변수와 함수(메서드)는 스네이크 표기법으로 정의한다.
- 추상화란, 객체가 가진 수많은 특징과 행동들 중에서 프로그래밍에 필요한 핵심적인 요소들만을 모델로 구현하는 것을 의미한다.
- 모델을 정의하는 방식은 클래스로 정의한다.
- 클래스를 정의할 때는
class
키워드를 사용한다. - PEP8에 의해 암묵적인 규칙으로 클래스 명칭은 파스칼 표기법으로 정의한다.
class MyClass:
pass
- 이전에 배운 독스트링을 활용하여 클래스에 대한 설명문을 기입할 수 있다.
- 독스트링을 확인하려면
클래스.__doc__
와 같이 사용한다.
class MyClass:
'''
이 클래스는 'MyClass' 입니다.
'''
print(MyClass.__doc__)
- 생성자란, 객체가 생성될 때 호출되는 함수이다.
- 주로 객체의 초기화 작업을 담당한다.
- 파이썬에서 생성자는
def __init__(self, ...):
와 같이 정의한다.
class MyClass:
def __init__(self):
print('생성자 호출')
정의한 클래스를 사용하기 위해서는 객체를 생성(인스턴스화) 한 후에 사용한다.
class MyClass:
def __init__(self):
print('생성자 호출')
my_class = MyClass()
print(type(my_class))
클래스 전역변수 란, 말 그대로 전역에서 사용한 변수로, 클래스 외부에서도 사용이 가능한 변수를 의미한다.
class MyClass:
a = 10
클래스에 선언된 변수와 매서드는 접근 연산자 .
을 사용하여 데이터에 접근할 수 있다.
class MyClass:
a = 10
a = MyClass.a
print(a)
멤버 변수 (혹은 인스턴스 변수) 란, 클래스 내부에서만 사용이 가능한 변수이며, 외부에서 접근하고자 하는 경우에는 클래스를 인스턴스 시킨 후 접근 연산자 .
를 통해 접근이 가능하다.
class MyClass:
def __init__(self):
# 생성자에 클래스 멤버 변수 'a' 정의
self.a = 10
마찬가지로, 인스턴스화를 한 후에 접근 연산자 .
을 사용하여 데이터에 접근할 수 있다.
class MyClass:
def __init__(self):
# 생성자에 클래스 멤버 변수 'a' 정의
self.a = 10
# 객체생성 (인스턴스화)
my_class = MyClass()
a = my_class.a
print(a)
- 메서드(method) 란, 클래스에 정의된 함수를 의미한다.
- 생성자 역시 메서드이다.
- 접근 연산자
.
를 통해 메서드를 호출할 수 있다. - 메서드의 첫번째 매개변수는 항상
self
를 받는다.- 파이썬의 암묵적인 명명 규칙에 의해
self
라는 변수명으로 통일하여 사용한다. - 따라서
self
말고 다른 매개변수 명을 지어도 상관은 없다.
- 파이썬의 암묵적인 명명 규칙에 의해
class MyClass:
def __init__(self):
self.a = 10
def my_method(self):
self.a = 20
my_class = MyClass()
print(my_class.a)
# 메서드를 호출할때 역시 '.' 연산자를 통해서 호출한다.
my_class.my_method()
print(my_class.a)
메서드 역시 독스트링을 사용하여 메서드에 대한 설명문을 기재할 수 있다.
class MyClass:
def f(self, x, y):
'''
덧셈을 해주는 함수입니다.
x: 숫자 타입
y: 숫자 타입
'''
return x + y
print(MyClass.f.__doc__)
-
self
는 새롭게 생성된 객체, 즉 인스턴스화된 객체를 의미한다.
여러 참고서에서는 클래스를 붕어빵을 만드는 틀, 객체는 붕어빵으로 비유해서 설명하였다.
# 새로운 클래스 'MyClass' 정의
class MyClass:
# 클래스 변수 'a'
a = 1
def __init__(self):
# 인스턴스 변수 'a'
self.a = 10
# 클래스 변수 'a'를 의미한다.
print(MyClass.a)
# 새로운 객체 'my_class' 인스턴스화
my_class = MyClass()
# 새롭게 생성된 객체 'my_class'의 변수 'a'를 의미한다.
print(my_class.a)
# 새로운 객체 'my_class2' 인스턴스화
my_class2 = MyClass()
# 새롭게 생성된 객체 'my_class'의 변수 'a'의 값을 '20'으로 변경
my_class2.a = 20
# '10'이 출력된다.
print(my_class.a)
# '20'이 출력된다.
print(my_class2.a)
인스턴스란, 클래스로 만든 객체를 의미하며, 클래스를 객체로 만드는 것을 인스턴스화 라고 한다.
- 예를 들어,
a = Cookie()
이렇게 만든a
는 객체이다. - 그리고 객체
a
는Cookie
의 인스턴스이다. - 즉, 인스턴스라는 말은 특정 객체(
a
)가 어떤 클래스(Cookie
)의 객체인지를 관계 위주로 설명할 때 사용한다. - 따라서
'a'는 인스턴스
보다는'a'는 객체
라는 표현이 어울리며,'a'는 'Cookie'의 객체
보다는'a'는 'Cookie'의 인스턴스
라고 표현한다.
- 상속이란, 상위(부모) 객체로부터 하위(자식) 객체가 속성과 행위를 물려받는 것을 의미한다.
- 문법은
class 클래스명(상위 클래스명):
으로 정의한다.
먼저, 상위 클래스 A
를 정의한다.
class A:
a = 10
def test(self):
print('클래스 A')
그 다음 위에서 정의한 클래스 A
를 상속받는 하위 클래스 B
를 정의한다.
class B(A):
pass
그 다음 아래의 코드를 실행해본다.
b = B()
print(b.a)
b.test()
분명 클래스 B
에는 멤버 변수 a
와 메서드 test
가 정의되어있지 않음에도 불구하고 멤버 변수 a
에 접근이 가능하며, 메서드 test
역시 잘 실행된다.
아래의 코드를 살펴보자.
class MyClass:
pass
- 위의
MyClass
라는 클래스는MyClass(object)
라는object
객체를 상속받는 과정이 생략된 표현이다. - 생략이 가능한 이유는 앞서 언급했듯, 파이썬의 모든 타입은 객체이며, 객체의 모든 최상위 클래스는
object
클래스이기 때문이다.
- 일반적으로 상속은 기존 클래스를 변경하지 않고 기능을 추가하거나 기존 기능을 변경하려고 할 때 사용한다.
- "클래스에 기능을 추가하고 싶으면 기존 클래스를 수정하면 되는데 왜 굳이 상속을 받아서 처리해야 하지?" 라는 의문이 들 수도 있다.
- 하지만 기존 클래스가 라이브러리 형태로 제공되거나(다른 사람이 만든 클래스를 가져와서 사용하는 경우) 수정이 허용되지 않는 상황이라면 상속을 사용해야 한다.
오버라이딩이란, 상위 객체로부터 물려받은 속성과 메서드를 재정의하는 것을 의미한다.
아래는 클래스 A
로부터 상속받은 클래스 B
에서 멤버 변수 a
와 메서드 test
를 재정의(메서드 오버라이딩)하는 예시이다.
class A:
a = 10
def test(self):
print('클래스 A')
class B(A):
a = 20
def test(self):
print('클래스 B')
a = A()
b = B()
# '10'이 출력된다.
print('클래스 A의 멤버 변수 a:', a.a)
# '20'이 출력된다.
print('클래스 B의 멤버 변수 b:', b.a)
# '클래스 A'가 출력된다.
a.test()
# '클래스 B'가 출력된다.
b.test()
- 상위 클래스를 다른 말로 슈퍼(Super) 클래스 라고도 부른다.
-
super
함수는 하위 객체에서 상위 객체의 내용을 사용하고 싶은 경우 사용하는 함수이다.
아래는 super
함수를 사용하는 예시이다.
class A:
a = 10
def test(self):
print('클래스 A')
class B(A):
a = 20
def test(self):
super().test()
a = A()
b = B()
# '클래스 A'가 출력된다.
a.test()
# '클래스 A'가 출력된다.
b.test()
-
super().__init__()
라는 코드를 통해 상위 클래스의 속성과 메서드를 자동으로 불러와서 해당 클래스에서도 사용이 가능하도록 해준다.-
super(상위 클래스, self).__init__()
와 같이 좀 더 명시적으로 표현하여 사용할 수 있다.
-
- 즉, 상위 클래스의 생성자를 호출하여 하위 클래스에서도 상위 클래스에 정의된 요소들을 초기화하는 방식으로 응용이 가능하다.
class Person:
def __init__(self, name):
self.name = name
def saying_my_name(self):
print(f'저는 {self.name}입니다.')
class Loser(Person):
def __init__(self, name, age):
# super 함수를 통해 상위 클래스의 생성자를 호출한다.
# 'super(Person, self).__init__()'으로 사용해도 된다.
super().__init__(name)
self.age = age
def saying_my_age(self):
print(f'저는 올해 {self.age}살 입니다.')
me = Loser('흔한 찐따', 28)
me.saying_my_name()
me.saying_my_age()
- 다중 상속이란, 말 그대로 여러 클래스를 다중으로 상속받는 것을 의미한다.
- 사용하는 방법은
class 클래스명(상위 클래스1, 상위 클래스2, ...)
과 같이 사용한다.
아래는 클래스 A
와 클래스 B
를 동시에 상속받는 클래스 C
를 정의한 예시이다.
class A:
def a(self):
print('클래스 A')
class B:
def b(self):
print('클래스 B')
class C(A, B):
pass
클래스 C
는 클래스 A
가 가지고 있는 메서드 a
와 클래스 B
가 가지고 있는 메서드 b
를 사용할 수 있다.
c = C()
c.a()
c.b()
다이아몬드 상속이란, 클래스의 관계도가 마치 다이아몬드 형태의 마름모꼴로 형성되어 있다고 해서 붙여진 명칭이다.
아래의 예시는 다이아몬드 상속의 간단한 예시이다.
class A:
def hello(self):
print('안녕하세요, A 입니다.')
class B(A):
def hello(self):
print('안녕하세요, B 입니다.')
class C(A):
def hello(self):
print('안녕하세요, C 입니다.')
class D(B, C):
pass
- 위의 예제에서 클래스
A
를 상속받는 클래스B
와C
가 있다. - 그리고 그것을 상속받는 클래스
D
가 있다. - 이 경우, 클래스
D
에서hello
메서드를 호출하게 되면 어떤 상위 클래스의 메서드가 호출이 되는가? - 이 문제점을 바로 다이아몬드 문제(Diamond Problem) 라고 한다.
- 다중 상속은 위와 같이 클래스 관계도가 꼬여서 혼란을 줄 수 있는 다이아몬드 문제를 발생시킨다.
- 따라서 아예 여느 객체지향 프로그래밍 언어(ex.
JAVA
) 같은 경우에는 다중 상속을 지원하지 않는다. -
C++
같은 경우,virtual
키워드를 통해 해결이 가능하도록 했다. -
Python
에서는 메서드 탐색 순서(MRO; Method Resolution Order) 라는 것에 우선 순위를 두어 해결하였는데, 이를 확인하는 방법은mro
메서드를 통해 확인할 수 있다.
아래의 코드를 실행하면 다음과 같이 결과가 나온다.
d = D()
mro = d.mro()
print(mro)
결과
[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
-
mro
에 따르면 클래스D
의 메서드 호출 순서는 자기 자신D
, 그 다음이 클래스B
이다. - 따라서
D
로 인스턴스를 만들고hello
메서드를 호출하면 클래스B
의hello
가 호출된다(D
는hello
메서드가 없으므로).
- 추상 클래스란, 메서드의 목록만 가진 클래스이며 상속받는 클래스에서 메서드 구현을 강제하기 위해 사용한다.
- 사용하는 방법은
abc
모듈에 있는ABCMeta
클래스를import
해서 사용한다.- 참고로,
abc
는abstract base class
의 약자이다.
- 참고로,
- 그리고 클래스의 괄호
( )
안에metaclass=ABCMeta
를 지정한다.- 메타 클래스(Meta Class) 란, 클래스를 만드는 클래스를 의미한다.
- 메서드를 만들 때 위에
@abstractmethod
를 붙여서 추상 메서드로 지정한다. - 사용하는 방식은 아래와 같다.
from abc import *
class 추상클래스이름(metaclass=ABCMeta):
@abstractmethod
def 메서드이름(self):
코드
- 먼저, 아래의 예시처럼
StudentBase
라는 추상 클래스를 정의한다. - 그 다음 추상 클래스
StudentBase
라는 클래스를 상속받는Student
클래스를 정의한다.
from abc import *
class StudentBase(metaclass=ABCMeta):
# '@abstractmethod'를 붙여주면 추상 메서드가 된다.
@abstractmethod
def study(self):
# 추상 메서드는 호출할 일이 없으므로 빈 메서드로 만든다.
pass
@abstractmethod
def go_to_school(self):
# 추상 메서드는 호출할 일이 없으므로 빈 메서드로 만든다.
pass
class Student(StudentBase):
def study(self):
print('공부하기')
loser = Student()
loser.study()
결과 위의 코드를 실행시켜보면 다음과 같은 에러가 발생한다.
Traceback (most recent call last):
File "C:\Users\iamjjintta\test.py", line 16, in <module>
loser = Student()
TypeError: Can't instantiate abstract class Student with abstract methods go_to_school
원인
- 추상 클래스
StudentBase
에서는 추상 메서드로study
와go_to_school
를 정의했다. - 하지만 추상 클래스
StudentBase
를 상속받은 클래스Student
에서는study
메서드만 구현하고,go_to_school
메서드는 구현하지 않았으므로 에러가 발생한다. - 따라서 추상 클래스를 상속받았다면
@abstractmethod
가 붙은 추상 메서드를 모두 구현해야 한다.- 참고로
@
가 붙은 것을 데코레이터(Decorator) 라고 한다.
- 참고로
- 다음과 같이 클래스
Student
에서go_to_school
메서드도 구현해준다.
from abc import *
class StudentBase(metaclass=ABCMeta):
# '@abstractmethod'를 붙여주면 추상 메서드가 된다.
@abstractmethod
def study(self):
# 추상 메서드는 호출할 일이 없으므로 빈 메서드로 만든다.
pass
@abstractmethod
def go_to_school(self):
# 추상 메서드는 호출할 일이 없으므로 빈 메서드로 만든다.
pass
class Student(StudentBase):
def study(self):
print('공부하기')
def go_to_school(self):
print('학교가기')
loser = Student()
loser.study()
loser.go_to_school()
- 정리하자면, 추상 클래스는 인스턴스로 만들 때는 사용하지 않으며 오로지 상속에만 사용한다.
- 그리고 파생 클래스에서 반드시 구현해야 할 메서드를 정해 줄 때 사용한다.
- 캡슐화란, 객체가 가지고 있는 속성을 캡슐처럼 감싸 객체가 가진 데이터를 보호하는 것을 의미한다.
- 클래스를 작성할 때는 내부적인 세부사항을 캡슐화하여 데이터를 보호하는 것이 일반적이다.
- 파이썬에서는 변수명 앞에
_
기호를 붙여서 캡슐화를 시킨다.
- 프라이빗(private): 객체 내부에서만 접근 가능한 데이터
- 프로텍티드(protected): 다른 객체에서는 접근 불가능하지만, 상속을 받은 하위 객체는 접근 가능한 데이터
- 퍼블릭(public): 객체 외부에서도 접근 가능한 데이터
- 클래스의 주역할은 객체의 데이터와 내부 구현 세부사항을 캡슐화하는 것이다.
- 그러나, 외부에서 객체를 조작하는 데 사용할 퍼블릭 데이터도 클래스에 정의해야 한다.
- 구현 세부사항과 퍼블릭 데이터를 구분하는 것이 중요하다.
파이썬에서는 객체의 거의 모든 것이 오픈되어 있다.
- 객체의 내부를 쉽게 조사할 수 있다.
- 원하는 대로 바꿀 수 있다.
- 접근 제어에 대한 강력한 개념은 없다. (ex. 프라이빗 클래스 멤버).
이는 내부 사항을 구현할 때 격리하고자 하는 경우 문제가 된다.
- 파이썬은 의도된 사용법을 지시하는 프로그래밍 관례를 따른다.
- 이러한 관례는 명명(naming) 규칙에 기반을 둔다.
- 언어에서 규칙을 강요하기보다는 프로그래머가 자발적으로 준수하는 것이 일반적이다.
이름이 _
로 시작하는 어트리뷰트는 프라이빗(private)으로 간주한다.
class Person:
def __init__(self, name):
self._name = '아무개'
앞에서 언급한 것과 같이, 이것은 프로그래밍 스타일일 뿐이며, 여전히 해당 변수에 접근하고 변경할 수 있다.
me = Person('흔한 찐따')
print(me._name)
me._name = '안 흔한 찐따'
print(me._name)
-
_
로 시작하는 이름은, 그것이 변수든 함수든 모듈명이든, 모두 내부 구현으로 간주하는 것이 일반적인 규칙이다. - 만약
_
로 시작하는 이름을 직접 사용하고 있다면 뭔가 잘못 하고 있는 것이다. - 따라서
_
로 시작하는 이름에 접근할 수 있는 고수준(High-level)의 기능을 찾아봐야 한다.
아래의 경우, 단순하게 self
매개 변수를 통한 인스턴스 변수를 정의하였다.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
그리고 아래의 코드를 실행해보았다.
me = Person('흔한 찐따', 28)
print(me.name)
print(me.age)
me.name = '안 흔한 찐따'
print(me.name)
me.age = 27.5
print(me.age)
me.age = '뭘까요...?'
print(me.age)
내가 원하는 것은 age
라는 인스턴스 변수는 무조건 int
타입이어야만 하는데, 그것이 되지 않고 있다.
- 접근자(accessor) 메서드를 도입하는 방법이 있다.
- 값을 불러오는 메서드를 겟터(Getter), 값을 변경하는 메서드를 셋터(Setter) 메서드라고 한다.
class Person:
def __init__(self, name, age):
self.set_name(name)
self.set_age(age)
# getter 메서드
def get_name(self):
return self._name
def get_age(self):
return self._age
# setter 메서드
def set_name(self, name):
# 이름은 문자열 타입으로 바꿔서 지정해준다.
self._name = str(name)
def set_age(self, age):
# 나이는 정수 타입으로 바꿔서 지정해준다.
self._age = int(age)
# 음의 정수는 사용이 불가능하도록 해준다.
if self._age < 0:
self._age = 0
me = Person('흔한 찐따', 28)
print(me.get_name())
print(me.get_age())
me.set_name('안 흔한 찐따')
print(me.get_name())
# 음의 정수인 경우, 0이 된다.
me.set_age(-10)
print(me.get_age())
# 아래를 실행하려고 하면 int 타입이 아니므로 에러가 발생하게 된다.
me.set_age('뭘까요...?')
print(me.get_age())
그러나 기존의 코드들을 전부 변경해줘야 하는 문제가 발생한다.
- 프로퍼티란, getter와 setter를 가지는 멤버 변수를 의미한다.
-
@property
를 붙여서 사용한다.-
@
기호가 붙어서 이것 역시 데코레이터(decorator) 라고 하는데, 아직 잘 모르겠다.
-
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
@property
def name(self):
return self._name
@name.setter
def name(self, name):
self._name = str(name)
@property
def age(self):
return self._age
@age.setter
def age(self, age):
self._age = int(age)
if self._age < 0:
self._age = 0
me = Person('흔한 찐따', 28)
print(me.name)
print(me.age)
me.name = '안 흔한 찐따'
print(me.name)
# 음의 정수인 경우, 0이 된다.
me.age = -10
print(me.age)
# 아래를 실행하려고 하면 int 타입이 아니므로 에러가 발생하게 된다.
me.age = '뭘까요...?'
print(me.age)
이제 평범한 프로퍼티에 대한 접근은 @property
와 @age.setter
하에서 getter와 setter 메서드를 통해 운용된다.
다형성이란, 하나의 객체가 여러 가지 형태를 취해 상황에 따라 다른 방식으로 해석이 가능한 것을 의미한다.
- 아래의 예시는 클래스
Dog
와 클래스Dog
를 상속받는Maltese
라는 클래스를 정의하여berk
라는 메서드를 실행한다. - 동일한 이름의 메서드를 실행시키지만, 동작하는 방식이 다른 것을 확인할 수 있다.
class Dog:
def berk(self):
print('멍멍!')
class Maltese(Dog):
def berk(self):
print('왕왕!')
d = Dog()
m = Maltese()
dogs = [d, m]
for dog in dogs:
dog.berk()
- 오버로딩이란, 같은 이름의 여러 메서드가 다른 매개 변수를 갖도록 하는 것을 의미한다.
- 다른 객체지향 프로그래밍 언어에서는 지원하는 기술이지만, 파이썬에서는 메서드 오버로딩은 불가능하다고 한다.
- 단, 파이썬에서는 연산자 오버로딩(Operator Overloading; 인스턴스 객체끼리 서로 연산을 할 수 있게끔 기존에 있는 연산자의 기능을 바꾸어 중복으로 정의하는 것)은 가능하다고 한다.
- 파이썬에서의 연산자 오버로딩은 아래의 매직 메서드 라는 것을 통해 지원한다.
- 매직 메서드 라는 특수 메서드를 사용함으로써, 파이썬의 메서드를 여러 면에서 커스터마이즈할 수 있다.
- 클래스에 특수 메서드를 정의할 수 있다.
- 파이썬 인터프리터는 이러한 메서드들을 특별하게 다룬다.
- 메서드 앞에 항상
__
가 붙는다.- ex. 생성자인
__init__
역시 특수 메서드이다.
- ex. 생성자인
객체에는 str
과 repr
두 가지 문자열 표현이 존재한다.
str
함수를 사용하였을 때, 어떠한 타입이든 문자열 타입으로 반환되는 것을 확인할 수 있다.
a = 10
b = str(a)
print(b)
print(type(b))
즉, str
함수는 문자열 객체로 변환시켜주는 역할을 하는데, 이때 클래스 내부의 매직 메서드인 __str__
을 통해 반환된다.
class A:
def __str__(self):
return '클래스 A'
a = A()
# '클래스 A'가 출력된다.
print(a)
-
repr
이란,representation
의 약어로, 파이썬의 대화형 인터프리터(REPL)에서 출력되는 고유한 형태를 반환한다. -
repr
은 어떤 객체의 '본질'보다는 외부에 노출되는, 사용자가 이해할 수 있는 객체의 모습을 표현한다. -
repr
함수는 어떤 객체의 '출력될 수 있는 표현(printable representation)'을 문자열의 형태로 반환한다. -
repr
함수는 개발자를 위한 상세한 표현에 사용된다. - 다시 말해 해당 객체를 설명해줄 수 있는, 그리고 화면에 출력될 수 있는 문자열 표현을 반환하는 것이다.
a = 10
b = repr(a)
print(b)
print(type(b))
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return f'{self.name}(이)라는 사람'
def __repr__(self):
return f'Person({self.name}, {self.age})'
loser = Person('흔한 찐따', 28)
print(loser)
print(repr(loser))
참고: 관례적으로,
__repr__
이 반환하는 문자열을eval
함수에 전달하면 객체를 다시 만들 수 있도록 한다. 그렇게 하는 것이 불가능하다면, 그 대신 읽기 쉬운 표현을 사용한다.
- 두 메서드는 객체의 문자열 표현을 반환한다.
- 위의 예제에서도 확인했듯, 두 메서드는 객체가 어떤 데이터 타입이든지간에 객체의 문자열 표현을 반환한다.
- 둘의 차이는 본질적으로 의도된 사용처가 다르다는 데서 기인한다.
-
__str__
는 태생적인 목적 자체가 인자를 '문자열화'해 반환하라는 것이다. - 즉,
__str__
의 본질적인 목적은 객체를 '표현하는 것(representation)'에 있다기보다는 추가적인 가공이나 다른 데이터와 호환될 수 있도록 문자열화하는 데에 있다. -
__repr__
은 본 목적이 객체를 인간이 이해할 수 있는 평문으로 '표현'하라는 것이다. -
__str__
가 서로 다른 자료형 간에 인터페이스를 제공하기 위해서 존재한다면,__repr__
은 해당 객체를 인간이 이해할 수 있는 표현으로 나타내기 위한 용도이다. - 다시 말해,
__str__
은 문자열화를 위한 매직 메서드이고__repr__
은 객체에 대한 표현과 설명을 위한 메서드이다.
다음과 같은 메서드가 수학 연산에 사용되며, 이를 연산자 오버로딩이라고 한다.
연산자 | 매직 메서드 |
---|---|
a + b | a.__add__(b) |
a - b | a.__sub__(b) |
a * b | a.__mul__(b) |
a / b | a.__truediv__(b) |
a // b | a.__floordiv__(b) |
a % b | a.__mod__(b) |
a << b | a.__lshift__(b) |
a >> b | a.__rshift__(b) |
a & b | a.__and__(b) |
a | b | a.__or__(b) |
a ^ b | a.__xor__(b) |
a ** b | a.__pow__(b) |
-a | a.__neg__() |
~a | a.__invert__() |
abs(a) | a.__abs__() |
다음과 같은 상황을 가정해보자.
- 어떤 방(room)이 있다.
- 그 방에 사람이 들어간다.
- 방에 사람이 들어갈 때마다 인원수를 체크한다.
위와 같은 경우를 가정하여 방에 대한 클래스 Room
을 정의하고, 사람에 대한 클래스 Person
을 정의했다고 해보자.
그렇다면 아래처럼 정의해볼 수 있겠다.
class Room:
def __init__(self, people=[]):
self.people = people
def join(self, person):
self.people.append(person)
print(person, '님이 입장하셨습니다.')
def __str__(self):
return f'총 인원수: {len(self.people)}'
class Person:
def __init__(self, name):
self.name = str(name)
def __str__(self):
return self.name
room = Room()
p1 = Person('흔한 찐따')
p2 = Person('안 흔한 찐따')
room.join(p1)
room.join(p2)
print(room)
하지만 room
이라는 객체에 p1
과 p2
객체를 각각 더할 때마다 추가가 되는 구조로 만들고 싶을 경우, 다음과 같이 정의할 수 있다.
class Room:
def __init__(self, people=[]):
self.people = people
def __str__(self):
return f'총 인원수: {len(self.people)}'
def __add__(self, person):
self.people.append(person)
print(person, '님이 입장하셨습니다.')
return Room(self.people)
class Person:
def __init__(self, name):
self.name = str(name)
def __str__(self):
return self.name
room = Room()
p1 = Person('흔한 찐따')
p2 = Person('안 흔한 찐따')
room = room + p1
room = room + p2
print(room)
컨테이너를 구현하는 데 다음과 같은 메서드를 사용한다.
호출 방식 | 매직 메서드 |
---|---|
len(x) | x.__len__() |
x[a] | x.__getitem__(a) |
x[a] = v | x.__setitem__(a,v) |
del x[a] | x.__delitem__(a) |
위의 예제에서 Room
과 Person
클래스를 응용하였다.
class Room:
def __init__(self, people=[]):
self.people = people
def __str__(self):
return f'총 인원수: {len(self)}'
def __add__(self, person):
self.people.append(person)
print(person, '님이 입장하셨습니다.')
return Room(self.people)
def __len__(self):
return len(self.people)
def __getitem__(self, i):
return self.people[i]
def __setitem__(self, i, person):
self.people[i] = person
print(i, '번째 방에', person, '님이 입장하셨습니다.')
def __delitem__(self, i):
out = self.people.pop(i)
print(out, '님이 퇴장하셨습니다.')
class Person:
def __init__(self, name):
self.name = str(name)
def __str__(self):
return self.name
room = Room()
p1 = Person('흔한 찐따')
p2 = Person('안 흔한 찐따')
room = room + p1
room = room + p2
print(room)
print(room[0])
del room[0]
print(room)
print(room[0])
p3 = Person('희귀한 찐따')
room[0] = p3
print(room)
- 정적 메서드란, 클래스와 연결되어 있지만, 해당 클래스의 특정 인스턴스와는 연결되어 있지 않는 메서드를 의미한다.
- 이러한 메서드에는 클래스의 객체가 입력 인수로 필요하지 않는다.
- 즉,
self
라는 인스턴스 매개 변수가 필요로 하지 않는다.
- 즉,
- 따라서, 클래스의 객체를 생성하지 않고 정적 메서드를 호출할 수 있다.
- 사용하는 방법은 정의한 메서드 윗줄에
@staticmethod
를 명시하면 된다.- 이 역시
@
기호가 붙어있어 데코레이터라고 한다.
- 이 역시
아래는 정적 메서드를 정의하는 예시이다.
class MyClass:
# 정적 메서드 정의
@staticmethod
def f():
print('정적 메서드 호출')
- 정적 메서드를 호출할 때에는 다음과 같이
클래스명.정적_메서드()
와 같이 호출할 수 있다. - 물론, 인스턴스화 시킨 후에도 호출이 가능하다.
# 정적 메서드 호출
MyClass.f()
# 인스턴스 객체 생성 후 호출
m = MyClass()
m.f()
- 정적 메서드는 코드를 실행하기 전에 클래스의 인스턴스를 생성하지 않으려는 경우에 유용하다.
- 이와 같은 접근 방식에서는 클래스의 내부 작업에 대한 "캡슐화"를 유지하지만, 값을 반환하는 데 클래스의 인스턴스가 필요하지는 않는다.
- 정적 메서드는 메서드의 실행이 외부 상태에 영향을 끼치지 않는 순수 함수(pure function) 를 만들 때 사용한다.
- 순수 함수는 부수 효과(side effect) 가 없고, 입력 값이 같으면 언제나 같은 출력 값을 반환한다.
- 즉, 정적 메서드는 인스턴스의 상태를 변화시키지 않는 메서드를 만들 때 사용한다.
정적 메서드 역시 하위 클래스에서 재정의 할 수 있다.
class A:
@staticmethod
def f(x, y):
return x + y
class B(A):
@staticmethod
def f(x, y):
return x - y
- 파이썬의 자료형도 인스턴스 메서드와 정적, 클래스 메서드로 나뉘어져 있다.
- 예를 들어,
set
에 요소를 더할 때는 인스턴스 메서드를 사용하고, 합집합을 구할 때는 정적 메서드를 사용하도록 만들어져 있다.
a = {1, 2, 3, 4}
# 인스턴스 메서드
a.update({5})
# {1, 2, 3, 4, 5}
print(a)
# 정적(클래스) 메서드
b = set.union({1, 2, 3, 4}, {5})
# {1, 2, 3, 4, 5}
print(b)
이처럼 인스턴스의 내용을 변경해야 할 때는 update
메서드와 같이 인스턴스 메서드로 작성하면 되고, 인스턴스 내용과는 상관없이 결과만 구하면 될 때는 set.union
와 같이 정적 메서드로 작성하면 된다.
-
동적 메서드란, 프로그램 실행 중(즉, 런타임 중) 동작하는 메서드가 달라질 수 있는 메서드를 의미한다.
- 즉, 객체지향의 특징 중 다형성과 직접적인 연관이 있다.
- 특정한 이벤트가 발생할 때 수행되는 이벤트(Event) 함수로써 응용하는 것이 가능하다.
동적 메서드를 정의하는 방식은 여러 가지가 있는데, 다음과 같다.
-
getattr
함수는getattr(object, 'name')
와 같이 사용하며,object
라는 객체 내부의name
이라는 멤버를 반환한다. -
setattr
함수는setattr(object, 'name', value)
와 같이 사용하며,object
라는 객체의 내부에 존재하는 속성name
의 값을value
로 바꾸거나, 새로운 속성을 생성하여 값을 부여한다.
아래는 gattattr(인스턴스 객체, 메서드명)
으로 인스턴스 객체
안의 메소드 메서드명
을 호출하는 예시이다.
class MyClass:
def a(self):
print("메서드 'a' 호출")
def b(self):
print("메서드 'b' 호출")
def c(self):
print("메서드 'c' 호출")
m = MyClass()
method_list = ['a', 'b', 'c']
for method in method_list:
dynamic_method = getattr(m, method)
dynamic_method()
아래처럼 setattr
함수를 사용하여 직접 메서드를 지정할 수 있다.
class MyClass:
def __init__(self, n):
# 'n'이 1보다 클 경우, 함수 'f'는 덧셈 함수가 된다.
# 아닐 경우, 함수 'f'는 뺄셈 함수가 된다.
if n > 1:
setattr(self, 'f', lambda x, y: x + y)
else:
setattr(self, 'f', lambda x, y: x - y)
m = MyClass(10)
y = m.f(1, 2)
print(y)
m2 = MyClass(0)
y2 = m2.f(1, 2)
print(y2)
-
eval
함수와exec
함수 모두 파이썬에서 기본적으로 제공하는 내장 함수이다. - 문자열로 표현된 파이썬 코드를 실행할 때 사용한다.
-
eval
함수의 경우, 문자열로 표현된 파이썬 식(Expression) 을 인수로 받아서 파이썬 컴파일 코드로 변환시킨다. - 이로써 파이썬 인터프리터에서 이를 번역하여 실행할 수 있다.
a = 1
b = eval('a + 4')
print(b)
-
eval
함수는 식만을 처리할 수 있기 때문에 문(Statement) 을 인수로 받으면 구문 오류인SyntaxError
가 발생한다.
a = 1
# SyntaxError 발생
eval('a = a + 1')
print(b)
-
exec
함수는 문자열로 표현된 문을 인수로 받아서 파이썬 컴파일 코드로 변환시킨다. - 이로써 파이썬 인터 프리터에서 이를 번역하여 실행할 수 있다.
a = 5
exec('a = a + 1')
print(a)
exec
함수를 응용하여 동적 메서드를 만드는 방식은 아래와 같다.
class MyClass:
def __init__(self, n):
code = '''
def dynamic_method(self, x):
self.n += x
return self.n
'''
self.n = n
attr = { 'dynamic_method': code.strip() }
exec(attr['dynamic_method'], attr)
setattr(MyClass, 'dynamic_method', attr['dynamic_method'])
m = MyClass(1)
print(m.dynamic_method(1))
print(m.dynamic_method(1))
print(m.dynamic_method(1))
- 정적(static) 이란, 한번 정해놓으면 변하지 않고 계속 유지되는 성질을 의미한다.
- 이와 반대의 의미인 동적(dynamic) 이란, 상황에 따라서 실시간으로 변하는 성질을 의미한다.
- 컴퓨터 공학에서 만약 처음 정해놓은 것이 그대로 계속 유지되기 원한다면 정적으로 한다.
- 만약 상황에 따라 그때 그때마다 달라지는 설정을 하고 싶으면 동적으로 한다.
이번에는 정적 메서드와 비슷하지만 약간의 차이점이 있는 클래스 메서드에 대해 살펴보고자 한다.
- 클래스 메서드는 정적 메서드처럼 인스턴스 없이 호출할 수 있다는 점은 같다.
- 하지만 클래스 메서드는 메서드 안에서 클래스 속성, 클래스 메서드에 접근해야 할 때 사용한다.
- 정의하는 방법은 메서드 위에
@classmethod
를 붙여 정의한다. - 이때, 클래스 메서드는 첫번째 매개 변수에 관례적으로
cls
를 지정해야 한다.- 참고로,
cls
는class
에서 따온 것이다.
- 참고로,
클래스 메서드를 정의하는 방법은 다음과 같다.
class 클래스이름:
@classmethod
def 메서드(cls, 매개변수1, 매개변수2):
코드
아래는 클래스 Person
을 정의하고 인스턴스가 몇 개 만들어졌는지 출력하는 클래스 메서드를 구현한 예시이다.
class Person:
# 클래스 속성 'count'를 정의한다.
count = 0
def __init__(self):
# 인스턴스가 생성될 때 클래스 속성 'count'에 1을 더한다.
Person.count += 1
@classmethod
def print_count(cls):
# 'cls' 매개 변수로 클래스 속성에 접근한다.
print(f'{cls.count}명 생성되었습니다.')
a = Person()
b = Person()
# '2명 생성되었습니다.'가 출력된다.
Person.print_count()