[Python][Selenium 3.141.0] 이게 왜 되지? (import 작동 원리)
이전 포스팅 :
[Python][Selenium 3.141.0] Chromedriver 실행시 잘못된 Timeout object가 전달되는 건에 대한 분석
개요 :수주 프로젝트 코딩 중 Selenium 3.141.0 버전에서 Chromedriver와 구버전 Selenium간의 호환성 문제로 의심되는 ValueError가 발생하였음이를 해결하기 위한 방법을 찾기 위해 코드를 상세히 분석함.우
mhd329.tistory.com
위의 포스팅과 연계되는 글이다.
사실 궁금증을 해결한 시점으로부터 꽤 지나긴 했는데, 적어두는것이 좋을 것 같아서 적게 되었다.
우선 위 포스팅의 마지막 부분에서 나는 아래와 같은 방식으로 해결했었다.
class Driver:
def __init__(self, *args: (str), **kwargs) -> None:
timeout.Timeout.from_float = FixedTimeout.from_float # 땜질한 부분.
naver_kin_logger.info(f"Driver generated.")
self.executable_path = kwargs["executable_path"]
self.options = Options()
if "debugpy" not in sys.modules: # VSCode 디버그 모드가 아닌 경우 args 추가
for option in args:
self.options.add_argument(option)
def make_driver(self) -> webdriver:
driver = webdriver.Chrome(
executable_path=self.executable_path,
options=self.options,
)
driver_version = driver.capabilities["chrome"]["chromedriverVersion"].split()[0]
logger.info(f"Chrome driver version {driver_version}")
return driver
이것이 몽키패칭 방식으로 수정한 코드인데, init 부분의 첫 번째 줄이 그 부분이다. `timeout.Timeout.from_float` 의 값을 `FixedTimeout.from_float` 로 강제 할당함으로써 버그를 수정한 경우이다.
그리고 FixedTimeout은 아래처럼 되어있다.
from urllib3.util.timeout import Timeout
class FixedTimeout(Timeout):
@classmethod
def from_float(cls, timeout) -> Timeout:
# Timeout은 urllib3.util.Timeout의 기본값을 따릅니다.
# >>> timeout = urllib3.util.Timeout(connect=2.0, read=7.0)
return Timeout(read=7.0, connect=2.0)
보다시피 원본 문제있던 Timeout 클래스를 상속받아서 새로운 Timeout을 만들어주는 클래스이다.
이걸 그대로 쓰다가, 어떤 일을 계기로 아래와 같이 수정했다.
from selenium.webdriver.remote.remote_connection import RemoteConnection
class FixedRemoteConnection(RemoteConnection):
RemoteConnection.set_timeout(10)
여기까지 보면 문제는 없어보인다.
근데 문제는, Driver 클래스를 아래처럼 바꿨는데도 잘 동작한다는 점이었다...
class Driver:
def __init__(self, *args: (str)) -> None:
# from .utils import FixedTimeout
# from urllib3.util import timeout
# timeout.Timeout.from_float = FixedTimeout.from_float
self.options = Options()
self.executable_path = None
self.driver: DriverType = None
self.options.add_experimental_option("excludeSwitches", ["enable-logging"]) # 시작 시 나오는 DevTools listening on... 로그 비활성화
if "debugpy" not in sys.modules: # VSCode 디버그 모드가 아닌 경우 args 추가
for option in args:
self.options.add_argument(option)
...
자잘한 부분은 제외하고 위쪽에 주석 처리된 부분이 핵심이다.
나는 분명 몽키패칭한 부분을 주석 처리했는데, 여전히 잘 돌아가고 있었다. 이게 대체 왜 되는걸까?
조사 결과 비밀은 파이썬 import 방식에 있었다.
내가 `FixedRemoteConnection` 클래스를 직접 사용하지 않았더라도, `FixedRemoteConnection` 클래스를 포함한 `utils.py` 가 import 될 때, 그 내부의 모든 코드가 한 번 실행되기 때문이다.
파이썬에서 모듈이 로딩되는 과정은 다음과 같다.
1. 파이썬은 모듈을 처음 로드할 때, 해당 모듈의 최상위 코드를 한 번 실행시킨다.
2. 그러면서 모듈에 정의된 함수, 클래스, 변수들이 로딩되고 모든 코드가 실행된다.
3. 두 번째 로드 이상인 경우는 다시 실행하지 않고 `sys.modules` 캐시에서 찾는다.
즉, 클래스나 함수 정의나 그 정의를 포함한 실행 코드도 실행되기 때문에 `RemoteConnection.set_timeout(10)` 를 실제 사용하지 않았지만 실행이 된 것이다. `FixedRemoteConnection` 클래스를 정의하는 이 과정에서 클래스 내부의 코드인 `RemoteConnection.set_timeout(10)` 부분이 실행되고, 변경된 값이 적용되어 버그가 사라진 것이었다.
사이드 이펙트를 방지하는 방법
즉, 내가 의도하지 않았음에도 발현되었으니 사이드이펙트인 것이고, 이번에는 좋은 방향으로 작용했지만 이걸 모르고 다른 경우에 적용되었더라면 매우 골치가 아팠을것 같다.
이를 방지하기 위해서는 클래스를 정의할 때 아래와 같이 바로 실행되지 않게 작성해야 한다.
from selenium.webdriver.remote.remote_connection import RemoteConnection
class FixedRemoteConnection(RemoteConnection):
@staticmethod
def fix_timeout():
RemoteConnection.set_timeout(10)
클래스는 정의되었지만 그 안에 메서드로 정의해놓았기 때문에 자동 실행되지 않는다.
아니면 그냥 전역 스코프에서 함수를 정의해서 호출할 때만 쓸 수 있게 만들던지 해야한다.
from selenium.webdriver.remote.remote_connection import RemoteConnection
def fix_timeout():
RemoteConnection.set_timeout(10)