데보션앱 소개페이지 바로가기
로그인 선택

신고하기

CLOSE
신고사유 (대표 사유 1개)
상세내용 (선택)
0/200
  • 신고한 게시글은 더 이상 보이지 않습니다.
  • 이용약관과 운영정책에 따라 신고사유에 해당하는지 검토 후 조치됩니다.
  • 허위 신고인 경우, 신고자의 서비스 이용이 제한될 수 있으니 유의하시어 신중하게 신고해 주세요.
(이 회원이 작성한 모든 댓글과 커뮤니티 게시물이 보이지 않고, 알림도 오지 않습니다.)

미리보기

커뮤니티

      1,234

      badge 23.06.15

      글 등록

      카테고리를 선택해주세요.

      DEVOTEE를 활성화 시키면
      지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.

      버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.

      임시저장함에 저장되었습니다. 저장일시 : 2022.5.17 14:29:08

      임시저장함

      제목을 선택하시면 이어서 작성이 가능하며,
      최대 20건까지 저장합니다.
      컨텐츠 유형, 제목, 저장일시, 삭제로 이뤄진 임시저장 목록
      컨텐츠 유형 제목 저장일 삭제

      데보션 블로그 게재 요청

      CLOSE
      • *
      • *

      본인인증

      효율적인 데보션 서비스 이용 및
      고객님의 소중한 개인정보보호를 위해
      본인인증을 진행해주세요. 본인인증 미 진행 시 로그인이 제한됩니다.
      본인인증 실패

      본인인증 로그인에 실패하였습니다.
      회원이 아니시거나 본인인증 등록이
      완료되지 않은 사용자입니다.

      회원정보 연결

      [Python] Class의 연산자, 특수 메서드, 상속, 그리고 pydantic

      jangwooseok2 23.05.22
      6,982 13 0

      안녕하세요. hynix dram노광 포토기술팀 장우석 TL입니다.

      이 글에서는 python의 class에 대해서 이야기하려고 합니다.


      조금 더 정확하게 말하면, python 공식 문서의 이 두 단락을 읽으려고 해요.

      별로 딱딱한 글은 아닐 거고, 에세이와 기술 문서 사이 쯤 되는 두서없는, 긴 글을 쓰려고 해요.



      이 글은 python 기초강의는 아닙니다. 다만 python의 class에 대해 조금 더 깊이 이해하고 싶으신 분들께 도움 되었으면 좋겠습니다.


      1. intro - pydantic 고치기

      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 item

      https://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 공식 문서를 정독해볼 필요가 있습니다.

      먼저 연산자와 특수 메서드 이야기를 할게요.


      2. python class의 연산자, 특수 메서드

      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() #angel

      python의 인스턴스는 내장 데이터(속성, 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))  #100

      str 함수는 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 클래스가 뭔가? 를 설명해야 겠네요.

      이걸 알려면 상속에 대한 이야기를 해야 합니다.


      3. 상속

      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) #happy

      python은 다중 상속도 지원합니다. 여러 클래스를 상속 받거나, 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를 사용하면 함수의 인자를 위처럼 부를 수 있어요.)

      댓글 0

      DEVOTEE를 활성화 시키면
      지금 작성한 댓글에 AI가 댓글을 달아줍니다.

      jangwooseok2 님의 최신 블로그

      더보기
      동영상 기고하기