據說你會 Python ?

前言

最近以爲 Python 太「簡單了」,因而在朋友面前放肆了一把:「我以爲 Python 是世界上最簡單的語言!」。朋友嘴角閃過了一絲輕蔑的微笑(心裏 OS:Naive!,做爲一個 Python 開發者,我必需要給你一點人生經驗,否則你不知道天高地厚!)因而朋友給我了一份滿分 100 分的題,而後這篇文章就是記錄下作這套題所踩過的坑。express

1.列表生成器

描述app

下面的代碼會報錯,爲何?ide

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 。ui

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))

或者this

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))

補充code

感謝評論區幾位提出的意見,這裏我給一份官方文檔的說明吧:
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))

參考連接 Python2 Execution-Model:Naming-and-Binding , Python3 Execution-Model:Resolution-of-Names。聽說這是 PEP 227 中新增的提案,我回去會進一步詳細考證。再次拜謝評論區 @沒頭腦很着急 @塗偉忠 @Cholerae 三位的勘誤指正。繼承

2.裝飾器

描述ip

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

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

可是運行在方法上會報錯,爲何?

@Timeit
def func():
    time.sleep(1)
    return "invoking function func"


if __name__ == '__main__':
    func()  # output: elapsed time is 1.00044410133

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

答案

使用類裝飾器後,在調用 func 函數的過程當中其對應的 instance 並不會傳遞給 call 方法,形成其 mehtod unbound ,那麼解決方法是什麼呢?描述符賽高

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__ 中新增長了一個 key 爲 call 的 item,可是當咱們執行 a() 時,由於涉及特殊方法的調用,所以咱們的調用過程不會從 a.__dict__ 中尋找屬性,而是從 tyee(a).__dict__ 中尋找屬性。所以,就會出現如上所述的狀況。

4.描述符

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

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 ,那麼這是爲何呢?這就要先從 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 ,從而產生內存泄露。那麼這裏該怎麼解決呢?兩種方法
第一種:

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 所產生的字典的 key 對於對象的引用是弱引用類型,其不會形成內存引用計數的增長,所以不會形成內存泄露。同理,若是咱們爲了不 value 對於對象的強引用,咱們可使用 WeakValueDictionary 。
第二種:在 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__


class A(object):  # Python 3 是 class A(object,metaclass=TraceAttribute):
    __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 沒事閱讀官方文檔,早日達到裝逼如風,常伴吾身的境界。

相關文章
相關標籤/搜索