메인
투자 노트

Tricky Python

@8/24/2022

클래스 생성자에서 기계적인 표준 할당 코드 피하기

__init__ 함수에서 별다른 요구사항 없이 인자를 받아서 인스턴스에 할당해줄 일이 많다. 대개 이런식으로 코드가 반복되게 된다.
class Hello(object): def __init__(self, a, b, c, d, e, f): self.a = a self.b = b self.c = c self.d = d self.e = e self.f = f
Python
복사
아래 코드는 위와 똑같이 작동한다. 이렇게 하면 한줄로 끝낼 수 있다.
class Hello(object): def __init__(self, a, b, c, d, e, f): self.__dict__.update({k: v for k, v in locals().items() if k != "self"})
Python
복사
다만 명확한 단점이 있다. Python 공식문서에서 이런식으로 사용하지 말라고 한다. 그리고 코드가 의미를 잘 표현하지 못해서 처음 보는 사람은 이걸 이해할 수 없다.
locals(), globals()는 변수 스코프를 딕셔너리로 추상화 해주는 함수이며 해당 딕셔너리를 통해서 변수 스코프와 상호작용 할 수 있다.
사실 이거 쓰면 변수 스코프를 마음대로 가지고 놀 수 있다.
locals()['ten'] = 10 ten # 이제 ten이라는 변수를 사용할 수 있다.
Python
복사

Python 인터프리터를 망가뜨리는 방법

globals().clear() globals()["__builtins__"].clear()
Python
복사
딱 이 두줄이면 다 망가진다. 왜 그런지 알아보자
python의 내장(모든 builtin object들)은 모두 __builtins__ 변수 안에 숨어있다. 다 꺼내서 없애버리면 어떻게 될까?
모든게 __builtins__ 안에 들어있다. 우리가 맨날 쓰는 print 함수도 들어있다.
>>> __builtins__ <module 'builtins' (built-in)> >>> __builtins__.print("안녕 나는 프린트 함수") 안녕 나는 프린트 함수
Python
복사
사실 print의 정체는 __builtins__.print 이다.
globals()를 통해서 __builtins__ 변수를 볼 수 있다.
>>> globals() {'__name__': '__main__', ... , '__builtins__': <module 'builtins' (built-in)> }
Python
복사
전역에서는 globals()와 locals()가 동일하다. 그래서 locals()로 해도 결과는 같다.
__builtins__는 모듈 객체로 감싸져 있어서 조작이 불가능하다.
>>> type(globals()["__builtins__"]) <class 'module'>
Python
복사
globals()함수는 모듈에서 작동하는거니까 globals()["__builtins__"].globals() 로 접근하면 되는거 아닌가? 생각할 수 있는데 그렇게 해도 결과는 같다.
clear()를 사용해서 모든 전역변수를 삭제하면 __builtins__가 자동으로 복구되는데, 복구된 __builtins__은 기존과 다르게 dict 객체이다.
>>> globals().clear() >>> type(globals()["__builtins__"]) <class 'dict'>
Python
복사
왜 모듈이 딕셔너리로 바뀌는지는 모르겠다. 아예 모듈의 객체를 모두 불러오나?
이제 dict 객체가 가진 각종 메서드를 사용해서 python의 기본 객체들을 없애거나 변경할 수 있다. 먼저 print 함수를 없애보자
>>> globals()["__builtins__"].pop("print") >>> print("hello") Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'print' is not defined
Python
복사
이제 print를 못쓴다
그냥 다 없앤다.
globals()["__builtins__"].clear()
Python
복사
이젠 뭐 그냥 아무것도 못한다. globals() , locals() 함수도 없어져서 더 이상 스코프에 뭐가 남았는지 확인도 못한다. exit()도 없어져서 python을 끄지도 못한다.

누가 나를 import하였는가?

A가 B를 import했다. A는 B의 객체들을 자기것처럼 막 사용하는데 B는 누가 자신을 import 했는지조차 알 수 없다.
상위 모듈에 접근하는 방법
__main__ 을 사용하면 자신을 import한 상위 모듈에 접근할 수 있다. ( python이 실행된 진입 지점을 __main__이라고 한다. )
import B hello = "안녕 나는 A야"
Python
복사
A.py
import __main__ print(__main__) # -> <module '__main__' from '.../A.py'> print(__main__.hello) # -> "안녕 나는 A야"
Python
복사
B.py
만약에 A1, A2, A3이라는 모듈이 있고 이 세개의 모듈이 B를 import해서 사용할 때 B는 각 모듈의 요구사항에 맞춰서 객체를 제공해 줄 수 있다. 그리고 configuration driven 한 개발을 할 때도 상위 모듈이 가진 구성 정보에 따라서 하위 모듈이 다르게 작동해야 하는 경우 쓸 수 있다.
근데 사실 결론은 “굳이 이렇게 하지 않아도 된다" 이다. 그냥 옵션을 통해 분기하는 함수가 있으면 해결되기 때문이다. __main__을 써보고 싶어서 안달이 난 경우 사용하자.

신상품!

모든 변경사항을 소개하진 못하고 눈에 밟힌것만 코드로 소개했다. 궁금하면 아래 문서들을 읽어보길 바란다.
def http_error(status): match status: case 400: return "Bad request" case 401 | 403 | 404: return "Not allowed" case 418: return "I'm a teapot" case _: return "Something's wrong with the internet"
Python
복사
case 구문이 생겼다. 상세한 사용법은 여기 참조 / 정확한 명칭은 Structural Pattern Matching
a = [1,2,3] b = [6,6,6] for i in *a,*b: print(i)
Python
복사
for 구문에서 iterable을 언페킹으로 이을 수 있다.
with ( open("a.txt") as a, open("b.txt") as b, ): a_content = a.read() b_content = b.read() print(a_content, b_content)
Python
복사
with as 구문에서 한번에 여러개의 객체를 사용할 수 있다
>>> d1 = {'a':1 ,'b':2} >>> d2 = {'c':3 ,'d':4} >>> d1|d2 {'a': 1, 'b': 2, 'c': 3, 'd': 4}
Python
복사
| 연산자로 셔너리를 더할 수 있다.
def square(number: int | float) -> int | float: return number ** 2
Python
복사
타입 힌트에서 Union[int, float] 을 int | float 으로 표기할 수 있다.

함수의 default 인자는 사실 기본값이 아니다.

이게 뭔소리냐면 default 인자 정의는 그냥 함수 객체의 속성으로 할당될 뿐이지 엄밀하게 명시된 기본값을 제공해주지는 못한다.
def func(lst=[]): lst.append(10) return lst
Python
복사
길게 설명할것도 없고 이건 누가 봐도 빈 리스트가 기본값이다. 매개변수가 전달되지 않으면 당연히 빈 리스트에 10을 추가해서 [10]을 주는게 함수의 기본 동작이어야 한다. 그런데 실제로는 아래 코드처럼 된다.
>>> func() [10] >>> func() [10, 10] >>> func() [10, 10, 10]
Python
복사
호출할 때 마다 10이 계속 추가된다. 도대체 왜 이럴까…? 함수도 그냥 객체다. default 인자는 객체의 __defaults__ 속성으로 할당된다. 함수는 <class 'function'> 클래스의 인스턴스일 뿐이다.
def func(hello="안녕"): # func.__defaults__[0] = "안녕" ...
Python
복사

함수 고장내기

함수는 그냥 객체고 객체는 속성과 메서드를 사용해서 조작할 수 있다. 즉 이미 정의된 함수도 외부에서 조작할 수 있다.
requests.get 함수를 좀 만져보려고 한다. requests.get 함수는 URL을 받아서 HTTP 요청을 해서 응답을 반환해준다. URL을 입력하지 않으면 에러가 발생한다.
>>> import requests >>> requests.get("https://www.naver.com") <Response [200]> >>> requests.get() # URL을 입력하지 않으면 에러가 발생한다. Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: get() missing 1 required positional argument: 'url'
Python
복사
이 함수의 기본 URL인자를 https://www.google.com 으로 만들어보자
>>> requests.get.__defaults__ = ("https://www.google.com", None) >>> requests.get() # URL을 입력하지 않아도 google에 요청을 보낸다 <Response [200]>
Python
복사
이제 requests.get 함수는 디폴트로 구글에 요청을 보내는 함수로 변했다. 이렇게 코드로 함수 정의를 변경할 수 있다. print나 np.array같은 builtins 함수들은 변경할 수 없다. builtins 함수들은 python으로 작성된 함수가 아니고 C로 되어 있는것 같다.
함수를 정의할 때 default 인자는 반드시 immutable(변경 불가능한)객체만 할당하도록 하자

객체 구조의 심연

모든 클래스는 object 클래스를 상속받는다.
>>> int.mro() [<class 'int'>, <class 'object'>] >>> list.mro() [<class 'list'>, <class 'object'>] >>> import io >>> io.BytesIO.mro() [<class '_io.BytesIO'>, <class '_io._BufferedIOBase'>, <class '_io._IOBase'>, <class 'object'>]
Python
복사
mro 메서드를 사용하면 해당 클래스의 상속 구조를 확인할 수 있다.
상수를 정의할 때 가볍고 구분 가능한 객체가 필요한 경우 object를 사용하기를 추천한다.
>>> GO_SIGNAL = object() >>> STOP_SIGNAL = object() >>> GO_SIGNAL == STOP_SIGNAL False >>> GO_SIGNAL is STOP_SIGNAL False >>> GO_SIGNAL <object object at 0x1005607d0> >>> STOP_SIGNAL <object object at 0x100560810>
Python
복사
진짜 아무것도 없는거라서 가장 가볍다.

type 의 정체

위에서 모든 클래스의 근원은 object 라고 했다. 모든 메타클래스의 근원은 type 이다.
type에 특정 객체를 넣어서 호출하면 객체의 타입을 알려주는데 이건 그냥 부가기능일 뿐이다.
a = 10 type(a) == a.__class__ # <class 'int'>
Python
복사
그냥 해당 객체의 __class__ 속성에 접근해도 <class 'int'> 라고 뜬다.
( 메타클래스 → 클래스 → 인스턴스 ) 메타클래스가 클래스를 만들고 클래스는 인스턴스를 만든다. 메타클래스는 실제 코딩을 하면서 쓸일은 거의 생기지 않지만 알아두면 시야가 확실이 넓어진다.
모듈 단위의 통일성은 인스턴스로 구현하고 패키지 단위의 통일성은 클래스를 통해 구현할 수 있다. 그리고 클래스 정의를 통해 데이터 모델링을 할때는 dataclass라는 내장 모듈을 쓰면 해결된다. 내가 말하고 싶은것은 이렇게 메타클래스를 사용해보기 딱 좋아 구미가 당기는 상황을 자주 마주하게 될것이고 조금만 고민하면 간단하게 해결되는 문제임에도, 메타클래스를 사용해보기 위해서 코드의 복잡성을 높이는 것만은 꼭 피해야 한다는 것이다.
Python 하면 떠오르는 보편적인 주제이기도 하고 잘 써놓은 설명이 너무나도 많아서 몇가지 자료만 첨부해둔다.