標籤:

Python里到底什麼才算method?

眾所周知,Python已經成為了計算機等級考試二級科目。

可是,最近在學習Python過程中,我碰到了一個很小的問題。萬萬沒想到,想了幾個小時也沒想明白。不但如此,連StackOverflow上也找不到合理的解釋。

我有一個微小的ContextManager:

&>&>&> class A:
... def __enter__(self): pass
... def __exit__(self, et, ev, tb): pass
...
&>&>&> a = A()
&>&>&> with a:
... print(1)
...
1

我又有一個弄巧成拙的ContextManager,這裡看不出有什麼問題:

&>&>&> class B:
... def __enter__(self): pass
... __exit__ = a.__exit__
...
&>&>&> b = B()
&>&>&> with b:
... print(1)
...
1

把ContextManager放到ExitStack里:

&>&>&> from contextlib import ExitStack
&>&>&> s = ExitStack()
&>&>&> s.enter_context(b)

現在問題來了,這竟然出異常了:

&>&>&> with s:
... print(1)
...
1
Traceback (most recent call last):
File "&", line 2, in &
File "/usr/lib64/python3.6/contextlib.py", line 380, in __exit__
raise exc_details[1]
File "/usr/lib64/python3.6/contextlib.py", line 365, in __exit__
if cb(*exc_details):
File "/usr/lib64/python3.6/contextlib.py", line 284, in _exit_wrapper
return cm_exit(cm, *exc_details)
TypeError: __exit__() takes 4 positional arguments but 5 were given

所以,Python里到底什麼才算method?

求教,真誠的。

------------------------------

補充:

在Python里的function和method,這裡用到的功能大致可以理解成:

class Function:
def __get__(self, instance, owner):
return Method(self, instance)

class Method:
def __init__(self, f, o):
self.f = f
self.o = o
def __call__(self, *args, **kwargs):
return self.f(self.o, *args, **kwargs)

因為method是沒有__get__的,所以調用b.__exit__時,self還是a。

可是ExitStack里畫蛇添足,大致是這麼調用的:

type(cm).__exit__(cm, ...)

於是就多塞了一個self進去。

畢竟contextlib是標準庫。這麼做一定是有原因的,否則誰吃飽了撐的,多寫代碼。而ExitStack從3.3開始就在那裡了。這一定不是Bug。


額,忘了說後面用的代碼是contextlib的碼源

特化了的函數啊,你怎麼還 exit = a.exit啊,這不是一個東西啊,你 a.exit is A.exit看看呀,你print (a.exit)看看啊。

你一定不是不理解partial, 這肯定不是啥bug。


@6hu2t32 死貨兔子。

_cm_type = type(cm)
_exit = _cm_type.__exit__
result = _cm_type.__enter__(cm)
self._push_cm_exit(cm, _exit)
return result

你自己看是不是因為特化的緣故。

因為通過type(inst).__exit__拿方法spec,但是因為它的_exit_根本就不是生成bound method的function spec,所以在B初始化對象時並不會特化這個已經特化的參數。

但是你又想用它拿spec, 然後

def _push_cm_exit(self, cm, cm_exit):
"""Helper to correctly register callbacks to __exit__ methods"""
def _exit_wrapper(*exc_details):
return cm_exit(cm, *exc_details)
_exit_wrapper.__self__ = cm
self.push(_exit_wrapper)

來,大聲告訴我,exit_wrapper返回的是什麼。

你的b的__exit__ = (exc_type, exc_val, exc_tb) =&> ...
你的B的__exit__ = (self, exc_type, exc_val, exc_tb) =&> ...
_push_cm_exit希望的參數
cm : 你的b沒毛病,是一個對象
cm_exit: (self, exc_type, exc_val, exc_tb) =&> ...
你傳的cm_exit是a.__exit__是個bound method, 人家要spec

求求你,別秀了,"標準庫"沒錯,錯的是你不守convention,當然, exit元方法就不該對你可見。

while self._exit_callbacks:
cb = self._exit_callbacks.pop()
try:
if cb(*exc_details):
suppressed_exc = True
pending_raise = False
exc_details = (None, None, None)
以下省略:)


補充,這個和普通的partial還是有一點區別的。

def f(a, b):
pass
f2 = partial(f, 1)

不是這種。


先從最初的問題開始說起,在Python中,凡是符合(類).(名稱)為callable的,例如B.myfunc,都可以叫做method。如果B有個實例b,則它們都可以通過b.myfunc()的形式來調用。有的時候,如果b的某個成員為callable,但不是來自類B,也可以認為形式上是一個method,可以理解為一種「假」的method,這裡暫不包括在內。

nethod的區別在於它們響應__get__的方式。完全不響應的是staticmethod,通過實例調用時沒有額外參數;附加類為參數的叫classmethod,不僅可以通過b.myfunc()調用,B.myfunc()也會自動加上類作為參數。最後最常見的也就是instancemethod,從實例調用時自動附近實例作為第一個參數。

系統庫這裡是使用instancemethod的等價形式調用,這沒有問題,因為語法規定__exit__本就應該是instancemethod,而你的__exit__是個boundmethod,不支持__get__,實際相當於staticmethod,自然就不匹配了,使用with不報錯只是這一版實現偶然支持而已。


手機答沒具體測試,看代碼應該是a的一個bounded method被轉化成了B的unbounded method了吧


可以去看看 PEP。

於是你可以在 PEP 343 -- The "with" Statement 中看到這一段:

mgr = (EXPR)
exit = type(mgr).__exit__ # Not calling it yet
value = type(mgr).__enter__(mgr)
exc = True
try:
try:
VAR = value # Only if "as VAR" is present
BLOCK
except:
# The exceptional case is handled here
exc = False
if not exit(mgr, *sys.exc_info()):
raise
# The exception is swallowed if exit() returns true
finally:
# The normal and non-local-goto cases are handled here
if exc:
exit(mgr, None, None, None)

於是你會發現原來是


Python在類外面定義的叫做function,在類中定義的叫做method,準確的說叫unbound method,在實例化的時候,有一個將實例和method綁定的過程,a.foo這種方法就叫做bound method。

bound method在被調用的時候會自動把綁定的實例作為參數壓如參數棧。

關鍵在於你的B的exit是a這個實例的bound method,而調用的時候又通過b這個實例來調用,所以調用的時候會把a和b這兩個實例都壓入棧中。

你應該把B的exit改成A.exit,而不是a.exit。

PS: 我沒加下劃線因為知乎會把它變成斜體


一個猜測,不一定對

可能是因為

&>&>&> a.__exit__
&&>
&>&>&> b.__exit__
&&>

b 的 exit 是 a 里的 bound method 所以 b 和 a 分別被當做 self 一共結算了兩次,所以導致了多傳了一個參數的 Error


&>&>&> class A(object):
... def m(self):
... print(么么噠, self)
...
&>&>&> A.m
&>&>&> A().m
&>&>&> A().m()
么么噠 &<__main__.A object at 0x104a42630&>

------------------- 分割線 -------------------

&>&>&> def f(a, b):
... pass
...
&>&>&> f
&
&>&>&> m = f.__get__(aha)
&>&>&> m
&

------------------- 分割線 -------------------

&>&>&> class B(object):
... m = A().m
...
&>&>&> B().m()
么么噠 &<__main__.A object at 0x104a52b70&>

這裡 print 出來的是 a 對象,而不是 b 對象。

感覺 lz 的這個操作很不科學

&>&>&> class B:
... def __enter__(self): pass
... __exit__ = a.__exit__


推薦閱讀:

python中如何理解裝飾器代碼?
如何通過html來執行python腳本?
Python訪問網頁報錯,ValueError: unknown url type,求問什麼原因?
django1.9如何配置static文件夾,從而訪問js,css等靜態文件?
python 使用 threading.thread(target="")指明函數入口,如果函數有返回值,如何得到這個返回值?

TAG:Python | Python3x |