2019年2月27日

[python] Inheritance & Hack with mock

最近工作的內容因為會用到 python,不過因為 python 實在太過自由了,有些特性對於習慣寫 C++的我來說實在有點不好控制,導致寫好的檢查是有可能被使用者繞過去的,舉例來說,確認使用者是否有 licesce 這件事就極度重要。這邊就筆記一些目前有想到的方式

雖然 python 本來就沒秘密可言,但是連藏都不藏直接開放出來也有點扯,更何況為了速度考量,目前看到比較好的方式大概是用 cython 把 python code 編成 share object。一來可以藏住實作細節,二來執行速度會提升到跟寫 C 接近。

只是雖然如此,python 依然有許多方式 (像是用 help 或是故意讓程式丟出 exception 看 call stack) 可以看到實作中的蛛絲馬跡。況且雖然 python 在命名時如果用 _ 開頭的名字用 help 看不到 (視同 private),如果透過別的方式找到名字的話,其實你要用它也是不會擋你的

除此之外,python 還有 mock 跟 patch 這種東西,雖然原意是 unittest 中常用的技巧,把一些跟目前要測的邏輯無關的部分取代成一個自己可控且可預期行為,以此來驗證帶側部分的行為是否正確。不過後來想想這東西似乎也有機會用來當作 hack 的手段之一。

最後 (雖然我覺得還有其他手段),python 的繼承本身機制就很有趣了 (像是 __init__ 跟 __del__ 這種似乎可以當作 constructor 跟 destructor 的東西在繼承體系下的呼叫方式就跟 C++ 天差地遠)。舉個例子來說:

from unittest.mock import Mock

class _Base:
    def __init__(self):
        print ("_Base.__init__")
    def _check(self):
        print ("_Base._check()")
  
class Derive(_Base):
    def __init__(self):
        super().__init__()
        print ("Derive.__init__")
    def run(self):
        self._check()
        super(Derive, self)._check()
        print ("Derive.run()")
  
class FakeDerive(Derive):
    def __init__(self):
        print ("FakeDerive.__init__")
    def _check(self):
        print ("fake check")

if __name__ == '__main__':
    obj = FakeDerive()
    obj.run()

輸出結果是這樣:

FakeDerive.__init__
fake check
_Base._check()
Derive.run()

從有標色的那三行應該就會發現基本上 python 在繼承體系下,只要你沒有特別標明要用 base class 的 method,一律都是用 derive class 提供的實作。以此為前提,一旦寫的時候沒有注意到這點其實就很容易被使用者繞過你辛苦寫好的檢查...

上面的例子是單純從繼承體系上可以鑽的洞來看,搭配 mock & patch 就可以玩出新天地:

from unittest import mock

class _Base:
    def __init__(self):
        print ("_Base.__init__")
    def _check(self):
        print ("_Base._check()")
  
class FakeBase:
    def _check(self):
        print ("fake check")
  
class Derive(_Base):
    def __init__(self):
        super().__init__()
        print ("Derive.__init__")
    def run(self):
        super(Derive, self)._check()
        print ("Derive.run()")
  
class FakeDerive(Derive):
    def __init__(self):
        print ("Fake init")
    @mock.patch.object(_Base, '_check', FakeBase._check)
    def run(self):
        print ("Enter fake run")
        super(FakeDerive, self).run()


obj = FakeDerive()
obj.run()

輸出結果是這樣:

Fake init
Enter fake run
fake check
Derive.run()

上面這個例子幹的事情呢,簡單來說就是先弄一個 FakeBase 用來覆蓋掉本來在 Derive.run() 中會被呼叫的 super(Derive, self)._check() ,並且弄一個 FakeDerive 直接繼承 Derive (這邊假設 Derive 是使用者無法存取/修改的程式碼)

標紅色的那行 @mock 做的事情就是把 Derive 中的 super(Derive, self)._check() (等同於 _Base._check) 替換成 FakeBase._check,這樣一來執行結果就會像上面列出來的那樣達到我們的目的:
  • 不會修改到 _Base, Derive 的程式碼
  • 依然可以使用原本寫在 Derive 中的程式碼
  • 只替換掉我想替換掉的部分 (這個例子中就是 _Base._check)
然後對,原本被我埋在 Derive 中的檢查就這樣被使用者 hack 掉了...= =

沒有留言:

張貼留言