聽說你會 Python ?

pythontab發表於2018-07-19

最近覺得 Python 太“簡單了”,看看下面的題目再告訴我你會Python吧~


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

參考連結 Python2 Execution-Model:Naming-and-Binding , Python3 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 函式的過程中其對應的 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__() 

答案

原因在於,在 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 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("errors")

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

答案

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("errors")

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("errors")
    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()

哎呀,好氣啊,為啥這會報錯啊,我明明之前用這種方法給 __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 沒事閱讀官方文件


相關文章