23.06.15
DEVOTEE를 활성화 시키면
지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.
버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.
| 컨텐츠 유형 | 제목 | 저장일 | 삭제 |
|---|
본인인증 로그인에 실패하였습니다.
회원이 아니시거나 본인인증 등록이
완료되지 않은 사용자입니다.
안녕하세요. hynix dram노광 포토기술팀 장우석 TL입니다.
이 글에서는 python의 class에 대해서 이야기하려고 합니다.
조금 더 정확하게 말하면, python 공식 문서의 이 두 단락을 읽으려고 해요.
별로 딱딱한 글은 아닐 거고, 에세이와 기술 문서 사이 쯤 되는 두서없는, 긴 글을 쓰려고 해요.
https://docs.python.org/ko/3/reference/datamodel.html#special-method-names ->3. 데이터 모델 - 3.3 특수 메서드 이름
이 글은 python 기초강의는 아닙니다. 다만 python의 class에 대해 조금 더 깊이 이해하고 싶으신 분들께 도움 되었으면 좋겠습니다.
from typing import Union
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
name: str
description: Union[str, None] = None
price: float
tax: Union[float, None] = None
app = FastAPI()
@app.post("/items/")
async def create_item(item: Item):
return itemhttps://fastapi.tiangolo.com/ko/tutorial/body/#__tabbed_1_2
작년에 fastapi를 프로젝트에 처음 도입했을 때, docs 읽다가 이런 생각을 했어요.
'이 근본 없는 클래스 선언 방식은 뭐지? pydantic?'
from pydantic import BaseModel, validator
class Person(BaseModel):
name: str
age: int
@validator('age')
def check_validation(cls, v):
if v < 30:
raise ValueError('거짓말!')
return v
def say(self):
print(self.name)
johnDoe = Person(name='johnDoe',age=31) #정상적으로 class 생성됩니다.
johnDoe.say() #johnDoe
janeEyre = Person(name='jane',age='Eyre')
''' type을 체크해줍니다
ValidationError: 1 validation error for Person
age
value is not a valid integer (type=type_error.integer)
'''
jws = Person(name='jangwooseok', age=29)
''' validator로 적합성 판단 가능
1 validation error for Person
age
거짓말! (type=value_error)
'''그래서 pydantic을 공부했습니다.
이건, pydantic 공식 문서(https://docs.pydantic.dev/usage/validators/)에 있는 예제를 살짝 고친 건데요.
pydantic은 데이터의 적합성 체크를 도와주는 라이브러리 입니다. 데이터 타입 체크도 해주고, validator도 깔끔하게 선언해줄 수 있어요.
좋죠.
from pydantic import BaseModel
from datetime import datetime
class User(BaseModel):
username : str
password : str
def __init__(self,**kwrg):
super().__init__(**kwrg)
self.create_tm = datetime.now()
User(username='jangwooseok',password='1234')
#ValueError: "User" object has no field "create_tm"문제는 제가 이런 코드를 작성했을 때 나타났습니다.
저는 인스턴스가 새로 만들어질 때마다, 그 인스턴스가 생성된 시간을 기록하고 싶었어요.
create_tm = datetime.now()를 argument로 넣어주는 것도 방법이겠지만, 저는 argument가 엄청 많은 메서드를 싫어해서, 가능하면 __init__ 함수 안에 create_tm 속성을 자동으로 넣어주는 코드를 집어넣고 싶었어요.
근데 valueError가 발생하죠. User 객체엔 create_tm 속성을 위한 공간이 없다고 나와요.
pydantic Basemodel을 상속받은 클래스로 찍어낸 인스턴스에는 새로운 속성을 부여해줄 수가 없습니다.
from pydantic import BaseModel
from datetime import datetime
class User(BaseModel):
username : str
password : str
__setattr__ = object.__setattr__
def __init__(self,**kwrg):
super().__init__(**kwrg)
self.create_tm = datetime.now()
a = User(username='jangwooseok',password='abcde')
print(a.create_tm) #2023-04-18 20:42:59.493654저는 15분 쯤 고민하다가 이런 코드를 추가해서 해결했어요.
더 좋은 방법이 있을 수 있겠지만...
__setattr__ 함수를 object로부터 가져와서 덮어씌워주면 됩니다.
저, __setattr__ 함수는 갑자기 어디서 튀어나온 건가, 궁금하시면... 거기서부터, 이제 python 공식 문서를 정독해볼 필요가 있습니다.
먼저 연산자와 특수 메서드 이야기를 할게요.
class someDataRobot:
name = 'angel' #attribute
def say(self,word): #method
print(word)
def introduce(self):
print(self.name)
instance = someDataRobot()
instance.say(word='applepie!') #applepie!
print(instance.name) #angel
instance.introduce() #angelpython의 인스턴스는 내장 데이터(속성, attribute)와 액션(메서드, method)를 가진 일종의 데이터 로봇입니다.
class는 데이터 로봇의 설계도면입니다. 속성과 메서드가 적혀 있어요.
class someDataRobot:
name = 'angel'
def __init__(self):
print('인스턴스가 생성될 때 실행됩니다')
def __call__(self):
print('인스턴스가 호출될 때 실행됩니다')
def __del__(self):
print('인스턴스가 파괴될 때 실행됩니다')
angel = someDataRobot() #인스턴스가 생성될 때 실행됩니다
angel() #인스턴스가 호출될 때 실행됩니다 -> () 붙이는 걸 호출이라 합니다.
#인스턴스가 파괴될 때 실행됩니다 -> 코드 실행이 끝나면서 가비지 컬렉터가 인스턴스를 파괴파이선에는 미리 할당 된 특수 메서드가 있습니다.
아마, 코딩하면서 가장 많이 보실 특수 메서드는 __init__일 거에요.
__call__ 도 알아두면 생각보다 쓰실 일이 많을 거고요.
가끔, 무슨 api에서 callable이라던가 callback 함수 같은 걸 달라고 하는데 함수 말고 인스턴스를 주고 싶을 때, __call__을 디파인해서 주시면 됩니다.
class someDataRobot:
name = 'angel' #attribute
def __str__(self): #https://docs.python.org/ko/3/library/stdtypes.html#str
return 'hello'
def __eq__(self,num):
return num == 9
def __add__(self,num): #https://docs.python.org/ko/3/reference/datamodel.html#emulating-numeric-types
return 5+num
def __len__(self):
return 100
happy = someDataRobot()
print(str(happy)) #hello
print(happy == 9) #True
print(happy + 2) #7
print(len(happy)) #100str 함수는 1차적으로 주어진 객체를 인코딩하려고 하고, 만약 인코딩이 안 되는 객체일 경우에는 __str__ 메서드를 실행하여 리턴 값을 돌려줍니다.
==연산자나 + 연산자도 보면 1차적으로는 비교 연산이나 덧셈 연산을 하고, 만약 그게 어려운 객체라고 판단 되면 __eq__ 메서드나 __add__ 메서드를 실행해서 리턴값을 돌려줍니다.
len도 비슷하게 작동합니다.
(그 외 특수연산자들은 https://docs.python.org/ko/3/reference/datamodel.html#special-method-names)
class someDataRobot:
def __enter__(self):
print('with 문이 시작 될 때 실행됩니다')
def __exit__(self, exc_type, exc_value, traceback):
print('with 문이 끝날 때 실행됩니다')
with someDataRobot() as f: #with 문이 시작 될 때 실행됩니다
print('happy hynix photo engr!') #happy hynix photo engr!
#with 문이 끝날 때 실행됩니다requests.session이나 open 함수 등을 사용할 때 종종 보이는 with 문은 __enter__로 시작해서 __exit__로 끝납니다.
class someDataRobot:
def __contains__(self,obj):
return obj in [1,2,3,4]
def __iter__(self):
yield [6,7,8,9]
print(1 in someDataRobot()) #True
for i in someDataRobot():
print(i) #[6, 7, 8, 9]멤버쉽 연산자(in)은 조금 복잡한데, 1차적으로 __contains__를 호출하고, __contains__가 정의 되어 있지 않으면 __iter__를 시도합니다.
__iter__는 순회를 구현하기 위한 함수인데, 저기서 yield가 좀 눈에 밟힙니다.
class someDataRobot:
i = 0
def __iter__(self):
return self
def __next__(self):
self.i += 1
if self.i > 5:
raise StopIteration
return self.i
for j in someDataRobot():
print(j)yield는 generator를 구현하기 위한 일종의 문법적 설탕입니다. yield 없이 제네레이터를 구현하면 위와 같습니다.
class someDataRobot:
dummy = 'test'
instance1 = someDataRobot()
#__setattr__ 은 어떤 인스턴스에 속성이나 method를 부여하는 함수
instance1.__setattr__('myphone','galaxy')
def say():
print('hello world!')
instance1.__setattr__('action',say)
print(instance1.myphone) #galaxy
instance1.action() #hello world!__setattr__ 는 인스턴스에 속성과 메서드를 부여하거나 수정합니다.
__getattribute__는 속성과 메서드를 조회합니다.
__delattr__ 는 속성과 메서드를 삭제합니다.
from pydantic import BaseModel
from datetime import datetime
class User(BaseModel):
username : str
password : str
def __init__(self,**kwrg):
super().__init__(**kwrg)
self.create_tm = datetime.now()
User(username='jangwooseok',password='1234')
#ValueError: "User" object has no field "create_tm"이제 다시 이 이야기를 할 수 있겠죠.
pydantic Basemodel을 상속 받은 클래스에 어떤 속성을 추가하려고 할 때 valueError 가 발생한 건, Basemodel이 __setattr__ 함수를 건드렸기 때문입니다.(정확히 말하면 오버라이드 한 거죠.)
저는 이걸 다시 원상 복귀 시켜야 겠다는 생각에 object 클래스에 정의된 __setattr__ 를 가져와서 엎어쳐준 거고요.
그러면 왜 object 클래스에서 __setattr__를 가져왔는가? object 클래스가 뭔가? 를 설명해야 겠네요.
이걸 알려면 상속에 대한 이야기를 해야 합니다.
class parent:
attr1 = 'test'
class child(parent):
attr2 = 'happy'
print(child.attr1) #test
print(child.attr2) #happy상속은 특정 클래스에 정의된 속성과 메서드를 다른 클래스에 부여해주고 싶을 때 사용하는 기능입니다.
관례적으로
부여 받는 클래스를 자식 클래스
부여해주는 클래스를 부모 클래스라고 합니다.
class father:
attr1 = 'test'
class mother:
attr2 = 'pie'
class child(father,mother): #다중 상속 기능
attr3 = 'happy'
print(child.attr1) #test
print(child.attr2) #pie
print(child.attr3) #happypython은 다중 상속도 지원합니다. 여러 클래스를 상속 받거나, 2중 3중 겹겹이 상속 받거나, 할 수 있어요.
class father:
attr1 = 'test'
class mother:
attr1 = 'pie'
class child(father,mother):
attr2 = 'happy'
print(child.attr1) #-> father에도 mother에도 attr1가 있는데, 자식에는 어떤 attr1이 들어가나?그런데, 다중 상속은 여러 경합 문제를 발생 시킵니다.
father도 attr1을 가지고, mother도 attr1를 지니는데, father와 mother를 상속 받은 child에는 어떤 attr1이 들어갈까요?
class adam:
attr1 = 'there will be light!'
class father(adam):
attr1 = 'test'
class mother(adam):
attr2 = 'pie'
class child(father,mother):
attr3 = 'happy'
print(child.attr1) #-> child에는 어떤 attr1이 들어가나?혹은 이런, 트라이앵글을 만들 수도 있겠죠. 상속 관계는 무한정 복잡해질 수 있을 거예요.
class adam:
attr1 = 'there will be light!'
class father(adam):
attr1 = 'test'
class mother(adam):
attr2 = 'pie'
class child(father,mother):
attr3 = 'happy'
#https://www.python.org/download/releases/2.3/mro/
print(child.__mro__) #(<class '__main__.child'>, <class '__main__.father'>, <class '__main__.mother'>, <class '__main__.adam'>, <class 'object'>)
print(child.attr1) #test모든 python class에는 메서드와 속성의 조회 순서를 결정하는 __mro__ 속성이 주어집니다.
클래스가 선언 될 때 __mro__ 가 계산되고, 저 순서에 따라서 메서드와 속성을 검색합니다.
child의 mro는
child -> father -> mother -> adam 이니
__getattribute__ 함수가 attr1을 father에서 가져왔네요. adam에 있는 attr1은 무시 되었습니다.
이렇게 부모 클래스의 속성과 메서드를 자식 클래스의 것으로 덮어 씌우게 되는 현상을 메서드 오버라이드라고 부릅니다.
import numpy as np
import pandas as pd
class parent:
attr1 = 'test'
class child(parent):
attr3 = 'happy'
print(child.__mro__) #(<class '__main__.child'>, <class '__main__.parent'>, <class 'object'>)
print(np.ndarray.__mro__) #(<class 'numpy.ndarray'>, <class 'object'>)
print(pd.DataFrame.__mro__)
#(<class 'pandas.core.frame.DataFrame'>,
# <class 'pandas.core.generic.NDFrame'>,
# <class 'pandas.core.base.PandasObject'>,
# <class 'pandas.core.accessor.DirNamesMixin'>,
# <class 'pandas.core.indexing.IndexingMixin'>,
# <class 'pandas.core.arraylike.OpsMixin'>, <class 'object'>)
print(list.__mro__)
#(<class 'list'>, <class 'object'>)그런데 특이한 게 눈에 띄실 거에요.
<class 'object'>
저는 이걸 child에 상속해준 적이 없는데, 상속 되었죠.
ndarray나 dataframe 같은 널리널리 쓰이는 상용 라이브러리들도 그렇고
list나 int나 str도 모두 object 클래스를 상속합니다.
object is a base for all classes. It has methods that are common to all instances of Python classes. This function does not accept any arguments.
(https://docs.python.org/ko/3/library/functions.html#object)
object 클래스는 python의 모든 클래스의 베이스 입니다.
모든 클래스는 object 클래스를 상속 받음으로, object 클래스의 메서드와 속성은 어떤 인스턴스에서도 사용 가능합니다.
이것이 python 처음 배울 때 선배님들이 말씀하셨던, 모든 것은 객체(object)다, 는 문장의 의미입니다. 저는 저게 참 아리송한 말이라고 생각했었는데요.
import inspect
class someDataRobot:
dummy = 'test'
target_class = someDataRobot
for argv_name, argv in inspect.getmembers(target_class):
try:
print(argv_name, argv.__qualname__)
except:
print(argv_name, argv)
'''
__setattr__ object.__setattr__
__getattribute__ object.__getattribute__
'''기본적으로 setattr나 getattrbute 같은, 모든 클래스가 공통으로 가지고 있는 특수 메서드는 object로부터 옵니다.
from pydantic import BaseModel
import inspect
class Person(BaseModel):
name: str
age: int
target_class = Person
for argv_name, argv in inspect.getmembers(target_class):
try:
print(argv_name, argv.__qualname__)
except:
print(argv_name, argv)
'''
__setattr__ BaseModel.__setattr__ -> setattr가 basemodel의 것으로 엎어쳐져있다.
__getattribute__ object.__getattribute__
'''Basemodel을 상속받은 클래스의 method들을 조사해보면, __setattr__가 Basemodel에서 선언된 __setattr__로 덮어씌워져 있는 걸 볼 수 있습니다.
이걸 object의 __setattr__로 덮어씌워주면, basemodel을 상속 받은 클래스에도 어떤 인자를 추가하거나 해줄 수 있게 됩니다.(물론 pydantic을 만드신 분들이 의도한 방법은 아니겠지만요.)
import inspect
import pandas as pd
cl = pd.DataFrame
for argv_name, argv in inspect.getmembers(cl):
try:
print(argv_name,inspect.signature(argv))
except:
continue
'''
truncate (self: 'NDFrameT', before=None, after=None, axis=None, copy: 'bool_t' = True) -> 'NDFrameT'
tshift (self: 'NDFrameT', periods: 'int' = 1, freq=None, axis: 'Axis' = 0) -> 'NDFrameT'
tz_convert (self: 'NDFrameT', tz, axis=0, level=None, copy: 'bool_t' = True) -> 'NDFrameT'
tz_localize (self: 'NDFrameT', tz, axis=0, level=None, copy: 'bool_t' = True, ambiguous='raise', nonexistent: 'str' = 'raise') -> 'NDFrameT'
unstack (self, level: 'Level' = -1, fill_value=None)
update (self, other, join: 'str' = 'left', overwrite: 'bool' = True, filter_func=None, errors: 'str' = 'ignore') -> 'None'
'''이상입니다.
하고 싶은 이야기는 끝났지만,
뱀다리를 붙이면, inspect 모듈을 사용해보시는 걸 추천드릴게요. 파이선 내장 모듈입니다. docs가 부실한, 처음 보는 클래스를 마주할 때 도움됩니다.
(inspect.signature를 사용하면 함수의 인자를 위처럼 부를 수 있어요.)
DEVOTEE를 활성화 시키면
지금 작성한 댓글에 AI가 댓글을 달아줍니다.