親クラスの位置引数(positional argument)が子クラスで迷子になっちゃう件。

きっかけは。

まだそんなに開発が進んでいないが、とっても気になるリポジトリを見つけた。

github.com

scrapy-selenium
要するに、Scrapy上でSeleniumを使用できるらしい。
これのイイところは、Downloader MiddlewareでSeleniumの設定をやれるようにしようとしてるところ。

「やれるようにしている」と表現したのは、まだwait_timescreen_shotくらいしか実装されていないからだ。。

設計自体はなかなか良さそうなので、これを使って試してみようとしたのだ。

とっころが、

エラッた。

Traceback (most recent call last):
  File "/home/kimoton/.pyenv/versions/3.6.5/lib/python3.6/site-packages/s                                     crapy/core/engine.py", line 127, in _next_request
    request = next(slot.start_requests)
  File "/home/kimoton/project/private/FlightScraper/FlightScraper/FlightS                                     craper/spiders/toscrape_xpath.py", line 14, in start_requests
    self.parse_result,
  File "/home/kimoton/.pyenv/versions/3.6.5/lib/python3.6/site-packages/s                                     crapy_selenium/http.py", line 29, in __init__
    super().__init__(*args, **kwargs)
TypeError: __init__() missing 1 required positional argument: 'url'

このエラーが、なかなか面白かった。

エラーが出たクラス

エラーの根源は、以下のSeleniumRequestクラスの初期化関数であった。

class SeleniumRequest(Request):
    def __init__(self, wait_time=None, wait_until=None, screenshot=False, *args, **kwargs):
        self.wait_time = wait_time
        self.wait_until = wait_until
        self.screenshot = screenshot

        super().__init__(*args, **kwargs)

ここでは、可変長引数を使って、親クラスであるRequestクラスの初期化関数(__init__())で使用されている引数を受けられるようにしていた。 つまり開発者は、Requestクラスと同じように扱えるようにしようと、そう思っていたに違いない。。

しかし、ここには決定的なミスがあった。

親クラスのRequestクラスを見てみよう。

親クラスのRequestクラス

class Request(object_ref):

    def __init__(self, url, callback=None, method='GET', headers=None, body=None,
                 cookies=None, meta=None, encoding='utf-8', priority=0,
                 dont_filter=False, errback=None, flags=None):

        self._encoding = encoding  # this one has to be set first
        self.method = str(method).upper()
        self._set_url(url)
        self._set_body(body)
        assert isinstance(priority, int), "Request priority not an integer: %r" % priority
        self.priority = priority

        if callback is not None and not callable(callback):
            raise TypeError('callback must be a callable, got %s' % type(callback).__name__)
        if errback is not None and not callable(errback):
            raise TypeError('errback must be a callable, got %s' % type(errback).__name__)
        assert callback or not errback, "Cannot use errback without a callback"
        self.callback = callback
        self.errback = errback

        self.cookies = cookies or {}
        self.headers = Headers(headers or {}, encoding=encoding)
        self.dont_filter = dont_filter

        self._meta = dict(meta) if meta else None
        self.flags = [] if flags is None else list(flags)

着目してほしいのは、url引数が、位置引数(positional argument)として渡されている点だ。

これを他の引数を指定している子クラスで使用しようとすると、引数のpositionが変わってしまうことになる。

簡単なクラスでテスト

挙動を再確認するため、簡単なクラスを作成する。

親クラス(parent

class parent():
    def __init__(self, one, two=None, three=None):
        self.one = one
        self.two = two
        self.three = three

子クラス(child

class child(parent):
    def __init__(self, child_one, child_two, child_three, *args, **kwargs):
        self.child_one = child_one
        self.child_two = child_two
        self.child_three = child_three
        super().__init__(*args, **kwargs)

として、

>>> c = child(1, 2, 3)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-24-8a18c3a05594> in <module>
----> 1 c = child(1, 2, 3)

<ipython-input-20-42d673207a6e> in __init__(self, child_one, child_two, child_three, *args, **kwargs)
      4         self.child_two = child_two
      5         self.child_three = child_three
----> 6         super().__init__(*args, **kwargs)
      7

TypeError: __init__() missing 1 required positional argument: 'one'

うん。再現性が取れました。 親クラスの第1引数(one)は、子クラスでは第4引数となっているはずですね。

試しに

>>> child(1, 2, 3, 4)

>>> c.one
Out[26]: 4

親クラスで第1引数だったものが子クラスの第4引数に格納されてしまったことが確認できました。

結局どうすればよかったのか

親クラスと同じような引数指定にしたかったら、これでいいんでないかな。。

class SeleniumRequest(Request):
    def __init__(self, url, wait_time=None, wait_until=None, screenshot=False, *args, **kwargs):
        self.wait_time = wait_time
        self.wait_until = wait_until
        self.screenshot = screenshot

        super().__init__(url, *args, **kwargs)

ちゃんとした答えはわからんです。
指摘してほしいです。