聽說你會Python?做幾道題唄
點擊上方
「
Python開發
」,選擇「置頂公眾號」
關鍵時刻,第一時間送達!
前言
最近覺得 Python 太"簡單了",於是在師父川爺面前放肆了一把:"我覺得 Python 是世界上最簡單的語言!"。於是川爺嘴角閃過了一絲輕蔑的微笑(內心 OS:Naive,作為一個 Python 開發者,我必須要給你一點人生經驗,不然你不知道天高地厚!)於是川爺給我了一份滿分 100 分的題,然後這篇文章就是記錄下做這套題所踩過的坑。
1.列表生成器
描述
下面的代碼會報錯,為什麼?
class
A
(
object
):
x
=
1
gen
=
(
x
for
_
in
xrange
(
10
))
# gen=(x for _ in range(10))
if
__name__
==
"__main__"
:
print
(
list
(
A
.
gen
))
答案
這個問題是變數作用域問題,在 gen =( x for _ in xrange ( 10
))
中gen
是一個 generator
,在 generator
中變數有自己的一套作用域,與其餘作用域空間相互隔離。因此,將會出現這樣的 NameError
:
name
" x "
is
not
defined
的問題,那麼解決方案是什麼呢?答案是:用 lambda 。class
A
(
object
):
x
=
1
gen
=
(
lambda
x
:
(
x
for
_
in
xrange
(
10
)))(
x
)
# gen=(x for _ in range(10))
if
__name__
==
"__main__"
:
print
(
list
(
A
.
gen
))
或者這樣
class
A
(
object
):
x
=
1
gen
=
(
A
.
x
for
_
in
xrange
(
10
))
# gen=(x for _ in range(10))
if
__name__
==
"__main__"
:
print
(
list
(
A
.
gen
))
補充
感謝評論區幾位提出的意見,這裡我給一份官方文檔的說明吧:
The scope of names defined in a class block is limited to the class block ; it does not extend to the code blocks of methods - this includes comprehensions and generator expressions since they are implemented using a function scope. This means that the following will fail :
class
A
:
a
=
42
b
=
list
(
a
+
i
for
i
in
range
(
10
))
參考鏈接 Python 2 Execution-Model:Naming-and-Binding , Python 3 Execution-Model:Resolution-of-Names。據說這是 PEP 227 中新增的提案,我回去會進一步詳細考證。再次拜謝評論區 @沒頭腦很著急 @塗偉忠 @ Cholerae 三位的勘誤指正。
2.裝飾器
描述
我想寫一個類裝飾器用來度量函數/方法運行時間
import
time
class
Timeit
(
object
):
def
__init__
(
self
,
func
):
self
.
_wrapped
=
func
def
__call__
(
self
,
*
args
,
**
kws
):
start_time
=
time
.
time
()
result
=
self
.
_wrapped
(*
args
,
**
kws
)
print
(
"elapsed time is %s "
%
(
time
.
time
()
-
start_time
))
return
result
這個裝飾器能夠運行在普通函數上:
@Timeit
def
func
():
time
.
sleep
(
1
)
return
"invoking function func"
if
__name__
==
"__main__"
:
func
()
# output: elapsed time is 1.00044410133
但是運行在方法上會報錯,為什麼?
class
A
(
object
):
@Timeit
def
func
(
self
):
time
.
sleep
(
1
)
return
"invoking method func"
if
__name__
==
"__main__"
:
a
=
A
()
a
.
func
()
# Boom!
如果我堅持使用類裝飾器,應該如何修改?
答案
使用類裝飾器後,在調用 func __call__ mehtod unbound
函數的過程中其對應的 instance 並不會傳遞給
方法,造成其
,那麼解決方法是什麼呢?描述符賽高
class
Timeit
(
object
):
def
__init__
(
self
,
func
):
self
.
func
=
func
def
__call__
(
self
,
*
args
,
**
kwargs
):
print
(
"invoking Timer"
)
def
__get__
(
self
,
instance
,
owner
):
return
lambda
*
args
,
**
kwargs
:
self
.
func
(
instance
,
*
args
,
**
kwargs
)
3. Python 調用機制
描述
我們知道 __call__
方法可以用來重載圓括弧調用,好的,以為問題就這麼簡單? Naive !
class
A
(
object
):
def
__call__
(
self
):
print
(
"invoking __call__ from A!"
)
if
__name__
==
"__main__"
:
a
=
A
()
a
()
# output: invoking __call__ from A
現在我們可以看到 a () a . __call__ ()
似乎等價於
,看起來很 Easy 對吧,好的,我現在想作死,又寫出了如下的代碼,
a
.
__call__
=
lambda
:
"invoking __call__ from lambda"
a
.
__call__
()
# output:invoking __call__ from lambda
a
()
# output:invoking __call__ from A!
請大佬們解釋下,為什麼 a () a . __call__ ()
沒有調用出
(此題由 USTC 王子博前輩提出 )
答案
原因在於,在 Python 中,新式類( new class )的內建特殊方法,和實例的屬性字典是相互隔離的,具體可以看看 Python 官方文檔對於這一情況的說明
For new - style classes , implicit invocations of special methods are only guaranteed to work correctly if defined on an object " s type , not in the object " s instance dictionary. That behaviour is the reason why the following code raises an exception ( unlike the equivalent example with old - style classes ):
同時官方也給出了一個例子:
class
C
(
object
):
pass
c
=
C
()
c
.
__len__
=
lambda
:
5
len
(
c
)
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# TypeError: object of type "C" has no len()
回到我們的例子上來,當我們在執行 a . __call__ = lambda : " invoking __call__ from lambda " a . __dict__ __call__ a () a . __dict__ tyee ( a ). __dict__
時,的確在我們在
中新增加了一個 key 為
的 item ,但是當我們執行
時,因為涉及特殊方法的調用,因此我們的調用過程不會從
中尋找屬性,而是從
中尋找屬性。因此,就會出現如上所述的情況。
4.描述符
描述
我想寫一個 Exam 類,其屬性 math 為 [ 0,100 ] 的整數,若賦值時不在此範圍內則拋出異常,我決定用描述符來實現這個需求。
class
Grade
(
object
):
def
__init__
(
self
):
self
.
_score
=
0
def
__get__
(
self
,
instance
,
owner
):
return
self
.
_score
def
__set__
(
self
,
instance
,
value
):
if
0
<=
value
<=
100
:
self
.
_score
=
value
else
:
raise
ValueError
(
"grade must be between 0 and 100"
)
class
Exam
(
object
):
math
=
Grade
()
def
__init__
(
self
,
math
):
self
.
math
=
math
if
__name__
==
"__main__"
:
niche
=
Exam
(
math
=
90
)
print
(
niche
.
math
)
# output : 90
snake
=
Exam
(
math
=
75
)
print
(
snake
.
math
)
# output : 75
snake
.
math
=
120
# output: ValueError:grade must be between 0 and 100!
看起來一切正常。不過這裡面有個巨大的問題,嘗試說明是什麼問題 為了解決這個問題,我改寫了 Grade 描述符如下:
class
Grad
(
object
):
def
__init__
(
self
):
self
.
_grade_pool
=
{}
def
__get__
(
self
,
instance
,
owner
):
return
self
.
_grade_pool
.
get
(
instance
,
None
)
def
__set__
(
self
,
instance
,
value
):
if
0
<=
value
<=
100
:
_grade_pool
=
self
.
__dict__
.
setdefault
(
"_grade_pool"
,
{})
_grade_pool
[
instance
]
=
value
else
:
raise
ValueError
(
"fuck"
)
不過這樣會導致更大的問題,請問該怎麼解決這個問題?
答案
1. 第一個問題的其實很簡單,如果你再運行一次 print ( niche . math ) 75 __dict__
你就會發現,輸出值是
,那麼這是為什麼呢?這就要先從 Python 的調用機制說起了。我們如果調用一個屬性,那麼其順序是優先從實例的
里查找,然後如果沒有查找到的話,那麼一次查詢類字典,父類字典,直到徹底查不到為止。
好的,現在回到我們的問題,我們發現,在我們的類 Exam self . math __dict__ Exam self . math math __set__
中,其
的調用過程是,首先在實例化後的實例的
中進行查找,沒有找到,接著往上一級,在我們的類
中進行查找,好的找到了,返回。那麼這意味著,我們對於
的所有操作都是對於類變數
的操作。因此造成變數污染的問題。那麼該則怎麼解決呢?很多同志可能會說,恩,在
函數中將值設置到具體的實例字典不就行了。
那麼這樣可不可以呢?答案是,很明顯不得行啊,至於為什麼,就涉及到我們 Python 描述符的機制了,描述符指的是實現了描述符協議的特殊的類,三個描述符協議指的是 __get__ " , __delete__ __set_name__ __get__ __set__ __delete__ __set_name__
, "* set
以及 Python 3.6 中新增的
方法,其中實現了
以及
/
/
的是 *
Data descriptors * ,而只實現了 __get__ Non - Data descriptor
的是
。
那麼有什麼區別呢,前面說了, *我們如果調用一個屬性,那麼其順序是優先從實例的 __dict__
里查找,然後如果沒有查找到的話,那麼一次查詢類字典,父類字典,直到徹底查不到為止。
但是,這裡沒有考慮描述符的因素進去,如果將描述符因素考慮進去,那麼正確的表述應該是
我們如果調用一個屬性,那麼其順序是優先從實例的 __dict__ Data descriptors Non - Data descriptors Non - Data descriptor
里查找,然後如果沒有查找到的話,那麼一次查詢類字典,父類字典,直到徹底查不到為止。其中如果在類實例字典中的該屬性是一個
,那麼無論實例字典中存在該屬性與否,無條件走描述符協議進行調用,在類實例字典中的該屬性是一個
,那麼優先調用實例字典中的屬性值而不觸發描述符協議,如果實例字典中不存在該屬性值,那麼觸發
的描述符協議
__set__
將具體的屬性寫入實例字典中,但是由於類字典中存在著 Data
descriptors
,因此,我們在調用 math
屬性時,依舊會觸發描述符協議。2.經過改良的做法,利用 dict dict dict dict dict key
的 key 唯一性,將具體的值與實例進行綁定,但是同時帶來了內存泄露的問題。那麼為什麼會造成內存泄露呢,首先複習下我們的
的特性,
最重要的一個特性,就是凡可 hash 的對象皆可為 key ,
通過利用的 hash 值的唯一性(嚴格意義上來講並不是唯一,而是其 hash 值碰撞幾率極小,近似認定其唯一)來保證 key 的不重複性,同時(敲黑板,重點來了),
中的
引用是強引用類型,會造成對應對象的引用計數的增加,可能造成對象無法被 gc ,從而產生內存泄露。那麼這裡該怎麼解決呢?兩種方法 第一種:
class
Grad
(
object
):
def
__init__
(
self
):
import
weakref
self
.
_grade_pool
=
weakref
.
WeakKeyDictionary
()
def
__get__
(
self
,
instance
,
owner
):
return
self
.
_grade_pool
.
get
(
instance
,
None
)
def
__set__
(
self
,
instance
,
value
):
if
0
<=
value
<=
100
:
_grade_pool
=
self
.
__dict__
.
setdefault
(
"_grade_pool"
,
{})
_grade_pool
[
instance
]
=
value
else
:
raise
ValueError
(
"fuck"
)
weakref 庫中的 WeakKeyDictionary WeakValueDictionary
所產生的字典的 key 對於對象的引用是弱引用類型,其不會造成內存引用計數的增加,因此不會造成內存泄露。同理,如果我們為了避免 value 對於對象的強引用,我們可以使用
。
第二種:在 Python 3.6 中,實現的 PEP 487 提案,為描述符新增加了一個協議,我們可以用其來綁定對應的對象:
class
Grad
(
object
):
def
__get__
(
self
,
instance
,
owner
):
return
instance
.
__dict__
[
self
.
key
]
def
__set__
(
self
,
instance
,
value
):
if
0
<=
value
<=
100
:
instance
.
__dict__
[
self
.
key
]
=
value
else
:
raise
ValueError
(
"fuck"
)
def
__set_name__
(
self
,
owner
,
name
):
self
.
key
=
name
這道題涉及的東西比較多,這裡給出一點參考鏈接, invoking-descriptors , Descriptor HowTo Guide , PEP 487 , what`s new in Python 3.6 。
5. Python 繼承機制
描述
試求出以下代碼的輸出結果。
class
Init
(
object
):
def
__init__
(
self
,
value
):
self
.
val
=
value
class
Add2
(
Init
):
def
__init__
(
self
,
val
):
super
(
Add2
,
self
).
__init__
(
val
)
self
.
val
+=
2
class
Mul5
(
Init
):
def
__init__
(
self
,
val
):
super
(
Mul5
,
self
).
__init__
(
val
)
self
.
val
*=
5
class
Pro
(
Mul5
,
Add2
):
pass
class
Incr
(
Pro
):
csup
=
super
(
Pro
)
def
__init__
(
self
,
val
):
self
.
csup
.
__init__
(
val
)
self
.
val
+=
1
p
=
Incr
(
5
)
print
(
p
.
val
)
答案
輸出是 36 ,具體可以參考 New-style Classes , multiple-inheritance
6. Python 特殊方法
描述
我寫了一個通過重載 * new * 方法來實現單例模式的類。
class
Singleton
(
object
):
_instance
=
None
def
__new__
(
cls
,
*
args
,
**
kwargs
):
if
cls
.
_instance
:
return
cls
.
_instance
cls
.
_isntance
=
cv
=
object
.
__new__
(
cls
,
*
args
,
**
kwargs
)
return
cv
sin1
=
Singleton
()
sin2
=
Singleton
()
print
(
sin1
is
sin2
)
# output: True
現在我有一堆類要實現為單例模式,所以我打算照葫蘆畫瓢寫一個元類,這樣可以讓代碼復用:
class
SingleMeta
(
type
):
def
__init__
(
cls
,
name
,
bases
,
dict
):
cls
.
_instance
=
None
__new__o
=
cls
.
__new__
def
__new__
(
cls
,
*
args
,
**
kwargs
):
if
cls
.
_instance
:
return
cls
.
_instance
cls
.
_instance
=
cv
=
__new__o
(
cls
,
*
args
,
**
kwargs
)
return
cv
cls
.
__new__
=
__new__
class
A
(
object
):
__metaclass__
=
SingleMeta
a1
=
A
()
# what`s the fuck
哎呀,好氣啊,為啥這會報錯啊,我明明之前用這種方法給 __getattribute__
打補丁的,下面這段代碼能夠捕獲一切屬性調用並列印參數
class
TraceAttribute
(
type
):
def
__init__
(
cls
,
name
,
bases
,
dict
):
__getattribute__o
=
cls
.
__getattribute__
def
__getattribute__
(
self
,
*
args
,
**
kwargs
):
print
(
"__getattribute__:"
,
args
,
kwargs
)
return
__getattribute__o
(
self
,
*
args
,
**
kwargs
)
cls
.
__getattribute__
=
__getattribute__
# Python 3 是 class A(object,metaclass=TraceAttribute):
class
A
(
object
):
__metaclass__
=
TraceAttribute
a
=
1
b
=
2
a
=
A
()
a
.
a
# output: __getattribute__:("a",){}
a
.
b
試解釋為什麼給 * getattribute * 打補丁成功,而 * new * 打補丁失敗。 如果我堅持使用元類給 * new * 打補丁來實現單例模式,應該怎麼修改?
答案
其實這是最氣人的一點,類里的 __new__ staticmethod staticmethod
是一個
因此替換的時候必須以
進行替換。答案如下:
class
SingleMeta
(
type
):
def
__init__
(
cls
,
name
,
bases
,
dict
):
cls
.
_instance
=
None
__new__o
=
cls
.
__new__
@staticmethod
def
__new__
(
cls
,
*
args
,
**
kwargs
):
if
cls
.
_instance
:
return
cls
.
_instance
cls
.
_instance
=
cv
=
__new__o
(
cls
,
*
args
,
**
kwargs
)
return
cv
cls
.
__new__
=
__new__
class
A
(
object
):
__metaclass__
=
SingleMeta
print
(
A
()
is
A
())
# output: True
結語
感謝師父大人的一套題讓我開啟新世界的大門,恩,博客上沒法艾特,只能傳遞心意了。說實話 Python 的動態特性可以讓其用眾多 black magic
去實現一些很舒服的功能,當然這也對我們對語言特性及坑的掌握也變得更嚴格了,願各位 Pythoner 沒事閱讀官方文檔,早日達到
裝逼如風,常伴吾身
的境界。
作者:manjusaka
原文鏈接:http://manjusaka.itscoder.com/2016/11/18/Someone-tell-me-that-you-think-Python-is-simple/
Python開發整理髮布,轉載請聯繫作者獲得授權
【點擊成為Java大神】


TAG:Python開發 |