當前位置:
首頁 > 知識 > 聽說你會Python?做幾道題唄

聽說你會Python?做幾道題唄

點擊上方

Python開發

」,選擇「置頂公眾號」


關鍵時刻,第一時間送達!



前言


最近覺得 Python 太"簡單了",於是在師父川爺面前放肆了一把:"我覺得 Python 是世界上最簡單的語言!"。於是川爺嘴角閃過了一絲輕蔑的微笑(內心 OS:Naive,作為一個 Python 開發者,我必須要給你一點人生經驗,不然你不知道天高地厚!)於是川爺給我了一份滿分 100 分的題,然後這篇文章就是記錄下做這套題所踩過的坑。

1.列表生成器


描述


下面的代碼會報錯,為什麼?

  1. class

    A

    (

    object

    ):

  2.    x

    =

    1

  3.    gen

    =

    (

    x

    for

    _

    in

    xrange

    (

    10

    ))

     

    # gen=(x for _ in range(10))

  4. if

    __name__

    ==

    "__main__"

    :

  5.    

    print

    (

    list

    (

    A

    .

    gen

    ))


答案


這個問題是變數作用域問題,在 

gen 

=(

 x 

for

 _ 

in

 xrange 

(

10

))

 中 

gen

 是一個 

generator

 ,在 

generator

 中變數有自己的一套作用域,與其餘作用域空間相互隔離。因此,將會出現這樣的 

NameError

:

name 

" x "

 

is

 

not

 

defined

 的問題,那麼解決方案是什麼呢?答案是:用 lambda 。

  1. class

    A

    (

    object

    ):

  2.    x

    =

    1

  3.    gen

    =

    (

    lambda

    x

    :

    (

    x

    for

    _

    in

    xrange

    (

    10

    )))(

  4.        x

    )

     

    # gen=(x for _ in range(10))

  5. if

    __name__

    ==

    "__main__"

    :

  6.    

    print

    (

    list

    (

    A

    .

    gen

    ))


或者這樣

  1. class

    A

    (

    object

    ):

  2.    x

    =

    1

  3.    gen

    =

    (

    A

    .

    x

    for

    _

    in

    xrange

    (

    10

    ))

     

    # gen=(x for _ in range(10))

  4. if

    __name__

    ==

    "__main__"

    :

  5.    

    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 :

  1. class

    A

    :

  2.    a

    =

    42

  3.    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.裝飾器


描述


我想寫一個類裝飾器用來度量函數/方法運行時間

  1. import

    time

  2. class

    Timeit

    (

    object

    ):

  3.    

    def

    __init__

    (

    self

    ,

    func

    ):

  4.        self

    .

    _wrapped

    =

    func

  5.    

    def

    __call__

    (

    self

    ,

    *

    args

    ,

    **

    kws

    ):

  6.        start_time

    =

    time

    .

    time

    ()

  7.        result

    =

    self

    .

    _wrapped

    (*

    args

    ,

    **

    kws

    )

  8.        

    print

    (

    "elapsed time is %s "

    %

    (

    time

    .

    time

    ()

    -

    start_time

    ))

  9.        

    return

    result


這個裝飾器能夠運行在普通函數上:

  1. @Timeit

  2. def

    func

    ():

  3.    time

    .

    sleep

    (

    1

    )

  4.    

    return

    "invoking function func"

  5. if

    __name__

    ==

    "__main__"

    :

  6.    func

    ()

     

    # output: elapsed time is 1.00044410133


但是運行在方法上會報錯,為什麼?

  1. class

    A

    (

    object

    ):

  2.    

    @Timeit

  3.    

    def

    func

    (

    self

    ):

  4.        time

    .

    sleep

    (

    1

    )

  5.        

    return

    "invoking method func"

  6. if

    __name__

    ==

    "__main__"

    :

  7.    a

    =

    A

    ()

  8.    a

    .

    func

    ()

     

    # Boom!


如果我堅持使用類裝飾器,應該如何修改?


答案


使用類裝飾器後,在調用 

func

 函數的過程中其對應的 instance 並不會傳遞給 

__call__

 方法,造成其 

mehtod unbound

 ,那麼解決方法是什麼呢?描述符賽高

  1. class

    Timeit

    (

    object

    ):

  2.    

    def

    __init__

    (

    self

    ,

    func

    ):

  3.        self

    .

    func

    =

    func

  4.    

    def

    __call__

    (

    self

    ,

    *

    args

    ,

    **

    kwargs

    ):

  5.        

    print

    (

    "invoking Timer"

    )

  6.    

    def

    __get__

    (

    self

    ,

    instance

    ,

    owner

    ):

  7.        

    return

    lambda

    *

    args

    ,

    **

    kwargs

    :

    self

    .

    func

    (

    instance

    ,

    *

    args

    ,

    **

    kwargs

    )


3. Python 調用機制


描述


我們知道 

__call__

 方法可以用來重載圓括弧調用,好的,以為問題就這麼簡單? Naive !

  1. class

    A

    (

    object

    ):

  2.    

    def

    __call__

    (

    self

    ):

  3.        

    print

    (

    "invoking __call__ from A!"

    )

  4. if

    __name__

    ==

    "__main__"

    :

  5.    a

    =

    A

    ()

  6.    a

    ()

     

    # output: invoking __call__ from A


現在我們可以看到 

()

 似乎等價於 

a

.

__call__ 

()

 ,看起來很 Easy 對吧,好的,我現在想作死,又寫出了如下的代碼,

  1. a

    .

    __call__

    =

    lambda

    :

    "invoking __call__ from lambda"

  2. a

    .

    __call__

    ()

  3. # output:invoking __call__ from lambda

  4. a

    ()

  5. # output:invoking __call__ from 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 ):


同時官方也給出了一個例子:

  1. class

    C

    (

    object

    ):

  2.    

    pass

  3. c

    =

    C

    ()

  4. c

    .

    __len__

    =

    lambda

    :

    5

  5. len

    (

    c

    )

  6. # Traceback (most recent call last):

  7. #  File "<stdin>", line 1, in <module>

  8. # TypeError: object of type "C" has no len()


回到我們的例子上來,當我們在執行 

a

.

__call__ 

=

 

lambda

 

:

" invoking __call__ from lambda "

 時,的確在我們在 

a

.

__dict__

 中新增加了一個 key 為 

__call__

 的 item ,但是當我們執行 

()

 時,因為涉及特殊方法的調用,因此我們的調用過程不會從 

a

.

__dict__

 中尋找屬性,而是從 

tyee 

(

 a 

).

 __dict__

 中尋找屬性。因此,就會出現如上所述的情況。


4.描述符


描述


我想寫一個 Exam 類,其屬性 math 為 [ 0,100 ] 的整數,若賦值時不在此範圍內則拋出異常,我決定用描述符來實現這個需求。

  1. class

    Grade

    (

    object

    ):

  2.    

    def

    __init__

    (

    self

    ):

  3.        self

    .

    _score

    =

    0

  4.    

    def

    __get__

    (

    self

    ,

    instance

    ,

    owner

    ):

  5.        

    return

    self

    .

    _score

  6.    

    def

    __set__

    (

    self

    ,

    instance

    ,

    value

    ):

  7.        

    if

    0

    <=

    value

    <=

    100

    :

  8.            self

    .

    _score

    =

    value

  9.        

    else

    :

  10.            

    raise

    ValueError

    (

    "grade must be between 0 and 100"

    )

  11. class

    Exam

    (

    object

    ):

  12.    math

    =

    Grade

    ()

  13.    

    def

    __init__

    (

    self

    ,

    math

    ):

  14.        self

    .

    math

    =

    math

  15. if

    __name__

    ==

    "__main__"

    :

  16.    niche

    =

    Exam

    (

    math

    =

    90

    )

  17.    

    print

    (

    niche

    .

    math

    )

  18.    

    # output : 90

  19.    snake

    =

    Exam

    (

    math

    =

    75

    )

  20.    

    print

    (

    snake

    .

    math

    )

  21.    

    # output : 75

  22.    snake

    .

    math

    =

    120

  23.    

    # output: ValueError:grade must be between 0 and 100!


看起來一切正常。不過這裡面有個巨大的問題,嘗試說明是什麼問題 為了解決這個問題,我改寫了 Grade 描述符如下:

  1. class

    Grad

    (

    object

    ):

  2.    

    def

    __init__

    (

    self

    ):

  3.        self

    .

    _grade_pool

    =

    {}

  4.    

    def

    __get__

    (

    self

    ,

    instance

    ,

    owner

    ):

  5.        

    return

    self

    .

    _grade_pool

    .

    get

    (

    instance

    ,

    None

    )

  6.    

    def

    __set__

    (

    self

    ,

    instance

    ,

    value

    ):

  7.        

    if

    0

    <=

    value

    <=

    100

    :

  8.            _grade_pool

    =

    self

    .

    __dict__

    .

    setdefault

    (

    "_grade_pool"

    ,

    {})

  9.            _grade_pool

    [

    instance

    ]

    =

    value

  10.        

    else

    :

  11.            

    raise

    ValueError

    (

    "fuck"

    )


不過這樣會導致更大的問題,請問該怎麼解決這個問題?


答案


1. 第一個問題的其實很簡單,如果你再運行一次 

print

 

(

 niche

.

math 

)

 你就會發現,輸出值是 

75

 ,那麼這是為什麼呢?這就要先從 Python 的調用機制說起了。我們如果調用一個屬性,那麼其順序是優先從實例的 

__dict__

 里查找,然後如果沒有查找到的話,那麼一次查詢類字典,父類字典,直到徹底查不到為止。


好的,現在回到我們的問題,我們發現,在我們的類 

Exam

 中,其 

self

.

math

 的調用過程是,首先在實例化後的實例的 

__dict__

 中進行查找,沒有找到,接著往上一級,在我們的類 

Exam

 中進行查找,好的找到了,返回。那麼這意味著,我們對於 

self

.

math

 的所有操作都是對於類變數 

math

 的操作。因此造成變數污染的問題。那麼該則怎麼解決呢?很多同志可能會說,恩,在 

__set__

 函數中將值設置到具體的實例字典不就行了。 


那麼這樣可不可以呢?答案是,很明顯不得行啊,至於為什麼,就涉及到我們 Python 描述符的機制了,描述符指的是實現了描述符協議的特殊的類,三個描述符協議指的是 

__get__

 , "* set 

" , 

__delete__

 以及 Python 3.6 中新增的 

__set_name__

 方法,其中實現了 

__get__

 以及 

__set__

 / 

__delete__

 / 

__set_name__

 的是 *

 Data descriptors * ,而只實現了 

__get__

 的是 

Non

 

-

 

Data

 descriptor

 。


那麼有什麼區別呢,前面說了, *我們如果調用一個屬性,那麼其順序是優先從實例的 

__dict__

 里查找,然後如果沒有查找到的話,那麼一次查詢類字典,父類字典,直到徹底查不到為止。

 


但是,這裡沒有考慮描述符的因素進去,如果將描述符因素考慮進去,那麼正確的表述應該是

我們如果調用一個屬性,那麼其順序是優先從實例的 

__dict__

 里查找,然後如果沒有查找到的話,那麼一次查詢類字典,父類字典,直到徹底查不到為止。其中如果在類實例字典中的該屬性是一個 

Data

 descriptors

 ,那麼無論實例字典中存在該屬性與否,無條件走描述符協議進行調用,在類實例字典中的該屬性是一個 

Non

 

-

 

Data

 descriptors

 ,那麼優先調用實例字典中的屬性值而不觸發描述符協議,如果實例字典中不存在該屬性值,那麼觸發 

Non

 

-

 

Data

 descriptor

 的描述符協議

。回到之前的問題,我們即使在 

__set__

 將具體的屬性寫入實例字典中,但是由於類字典中存在著 

Data

 descriptors

,因此,我們在調用 

math

 屬性時,依舊會觸發描述符協議。


2.經過改良的做法,利用 

dict

 的 key 唯一性,將具體的值與實例進行綁定,但是同時帶來了內存泄露的問題。那麼為什麼會造成內存泄露呢,首先複習下我們的 

dict

 的特性, 

dict

 最重要的一個特性,就是凡可 hash 的對象皆可為 key , 

dict

 通過利用的 hash 值的唯一性(嚴格意義上來講並不是唯一,而是其 hash 值碰撞幾率極小,近似認定其唯一)來保證 key 的不重複性,同時(敲黑板,重點來了), 

dict

 中的 

key

 引用是強引用類型,會造成對應對象的引用計數的增加,可能造成對象無法被 gc ,從而產生內存泄露。那麼這裡該怎麼解決呢?兩種方法 第一種:

  1. class

    Grad

    (

    object

    ):

  2.    

    def

    __init__

    (

    self

    ):

  3.        

    import

    weakref

  4.        self

    .

    _grade_pool

    =

    weakref

    .

    WeakKeyDictionary

    ()

  5.    

    def

    __get__

    (

    self

    ,

    instance

    ,

    owner

    ):

  6.        

    return

    self

    .

    _grade_pool

    .

    get

    (

    instance

    ,

    None

    )

  7.    

    def

    __set__

    (

    self

    ,

    instance

    ,

    value

    ):

  8.        

    if

    0

    <=

    value

    <=

    100

    :

  9.            _grade_pool

    =

    self

    .

    __dict__

    .

    setdefault

    (

    "_grade_pool"

    ,

    {})

  10.            _grade_pool

    [

    instance

    ]

    =

    value

  11.        

    else

    :

  12.            

    raise

    ValueError

    (

    "fuck"

    )


weakref 庫中的 

WeakKeyDictionary

 所產生的字典的 key 對於對象的引用是弱引用類型,其不會造成內存引用計數的增加,因此不會造成內存泄露。同理,如果我們為了避免 value 對於對象的強引用,我們可以使用 

WeakValueDictionary

 。


第二種:在 Python 3.6 中,實現的 PEP 487 提案,為描述符新增加了一個協議,我們可以用其來綁定對應的對象:

  1. class

    Grad

    (

    object

    ):

  2.    

    def

    __get__

    (

    self

    ,

    instance

    ,

    owner

    ):

  3.        

    return

    instance

    .

    __dict__

    [

    self

    .

    key

    ]

  4.    

    def

    __set__

    (

    self

    ,

    instance

    ,

    value

    ):

  5.        

    if

    0

    <=

    value

    <=

    100

    :

  6.            instance

    .

    __dict__

    [

    self

    .

    key

    ]

    =

    value

  7.        

    else

    :

  8.            

    raise

    ValueError

    (

    "fuck"

    )

  9.    

    def

    __set_name__

    (

    self

    ,

    owner

    ,

    name

    ):

  10.        self

    .

    key

    =

    name


這道題涉及的東西比較多,這裡給出一點參考鏈接, invoking-descriptors , Descriptor HowTo Guide , PEP 487 , what`s new in Python 3.6 。


5. Python 繼承機制


描述


試求出以下代碼的輸出結果。

  1. class

    Init

    (

    object

    ):

  2.    

    def

    __init__

    (

    self

    ,

    value

    ):

  3.        self

    .

    val

    =

    value

  4. class

    Add2

    (

    Init

    ):

  5.    

    def

    __init__

    (

    self

    ,

    val

    ):

  6.        super

    (

    Add2

    ,

    self

    ).

    __init__

    (

    val

    )

  7.        self

    .

    val

    +=

    2

  8. class

    Mul5

    (

    Init

    ):

  9.    

    def

    __init__

    (

    self

    ,

    val

    ):

  10.        super

    (

    Mul5

    ,

    self

    ).

    __init__

    (

    val

    )

  11.        self

    .

    val

    *=

    5

  12. class

    Pro

    (

    Mul5

    ,

    Add2

    ):

  13.    

    pass

  14. class

    Incr

    (

    Pro

    ):

  15.    csup

    =

    super

    (

    Pro

    )

  16.    

    def

    __init__

    (

    self

    ,

    val

    ):

  17.        self

    .

    csup

    .

    __init__

    (

    val

    )

  18.        self

    .

    val

    +=

    1

  19. p

    =

    Incr

    (

    5

    )

  20. print

    (

    p

    .

    val

    )


答案


輸出是 36 ,具體可以參考 New-style Classes , multiple-inheritance


6. Python 特殊方法


描述


我寫了一個通過重載 * new * 方法來實現單例模式的類。

  1. class

    Singleton

    (

    object

    ):

  2.    _instance

    =

    None

  3.    

    def

    __new__

    (

    cls

    ,

    *

    args

    ,

    **

    kwargs

    ):

  4.        

    if

    cls

    .

    _instance

    :

  5.            

    return

    cls

    .

    _instance

  6.        cls

    .

    _isntance

    =

    cv

    =

    object

    .

    __new__

    (

    cls

    ,

    *

    args

    ,

    **

    kwargs

    )

  7.        

    return

    cv

  8. sin1

    =

    Singleton

    ()

  9. sin2

    =

    Singleton

    ()

  10. print

    (

    sin1

    is

    sin2

    )

  11. # output: True


現在我有一堆類要實現為單例模式,所以我打算照葫蘆畫瓢寫一個元類,這樣可以讓代碼復用:

  1. class

    SingleMeta

    (

    type

    ):

  2.    

    def

    __init__

    (

    cls

    ,

    name

    ,

    bases

    ,

    dict

    ):

  3.        cls

    .

    _instance

    =

    None

  4.        __new__o

    =

    cls

    .

    __new__

  5.        

    def

    __new__

    (

    cls

    ,

    *

    args

    ,

    **

    kwargs

    ):

  6.            

    if

    cls

    .

    _instance

    :

  7.                

    return

    cls

    .

    _instance

  8.            cls

    .

    _instance

    =

    cv

    =

    __new__o

    (

    cls

    ,

    *

    args

    ,

    **

    kwargs

    )

  9.            

    return

    cv

  10.        cls

    .

    __new__

    =

    __new__

  11. class

    A

    (

    object

    ):

  12.    __metaclass__

    =

    SingleMeta

  13. a1

    =

    A

    ()

     

    # what`s the fuck


哎呀,好氣啊,為啥這會報錯啊,我明明之前用這種方法給 

__getattribute__

 打補丁的,下面這段代碼能夠捕獲一切屬性調用並列印參數

  1. class

    TraceAttribute

    (

    type

    ):

  2.    

    def

    __init__

    (

    cls

    ,

    name

    ,

    bases

    ,

    dict

    ):

  3.        __getattribute__o

    =

    cls

    .

    __getattribute__

  4.        

    def

    __getattribute__

    (

    self

    ,

    *

    args

    ,

    **

    kwargs

    ):

  5.            

    print

    (

    "__getattribute__:"

    ,

    args

    ,

    kwargs

    )

  6.            

    return

    __getattribute__o

    (

    self

    ,

    *

    args

    ,

    **

    kwargs

    )

  7.        cls

    .

    __getattribute__

    =

    __getattribute__

  8. # Python 3 是 class A(object,metaclass=TraceAttribute):

  9. class

    A

    (

    object

    ):

  10.    __metaclass__

    =

    TraceAttribute

  11.    a

    =

    1

  12.    b

    =

    2

  13. a

    =

    A

    ()

  14. a

    .

    a

  15. # output: __getattribute__:("a",){}

  16. a

    .

    b


試解釋為什麼給 * getattribute * 打補丁成功,而 * new * 打補丁失敗。 如果我堅持使用元類給 * new * 打補丁來實現單例模式,應該怎麼修改?


答案


其實這是最氣人的一點,類里的 

__new__

 是一個 

staticmethod

 因此替換的時候必須以 

staticmethod

 進行替換。答案如下:

  1. class

    SingleMeta

    (

    type

    ):

  2.    

    def

    __init__

    (

    cls

    ,

    name

    ,

    bases

    ,

    dict

    ):

  3.        cls

    .

    _instance

    =

    None

  4.        __new__o

    =

    cls

    .

    __new__

  5.        

    @staticmethod

  6.        

    def

    __new__

    (

    cls

    ,

    *

    args

    ,

    **

    kwargs

    ):

  7.            

    if

    cls

    .

    _instance

    :

  8.                

    return

    cls

    .

    _instance

  9.            cls

    .

    _instance

    =

    cv

    =

    __new__o

    (

    cls

    ,

    *

    args

    ,

    **

    kwargs

    )

  10.            

    return

    cv

  11.        cls

    .

    __new__

    =

    __new__

  12. class

    A

    (

    object

    ):

  13.    __metaclass__

    =

    SingleMeta

  14. 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大神】

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 Python開發 的精彩文章:

Python Web部署方式總結
MYSQL 入門全套

TAG:Python開發 |