為什麼要學習python中的高階知識

樑健發表於2017-11-20

前言

python相對於其它語言來說入門簡單。入門後無論是web開發,還是爬蟲都可以應付得了。大部分的工作都是在現有的框架下,繼承某類,按照要求重寫某些方法,然後就可以完成工作了。python進階的那些知識,各種資料型別的合理使用,閉包,各種各樣的協議,抽象基類,協程,屬性描述符,超程式設計等等,平時寫程式碼很少很少很少很少用到。作為面試官之一,面試了應該有20來個人了吧,就連裝飾器(閉包的應用方向之一)也只有少數幾個用過,甚至有的工作了3,4年的都基本沒有自己寫過裝飾器。不會用到的原因很簡單,這些進階知識,一般是在寫框架,寫庫等等才會用到,而這樣的機會是很少的。這樣來說,貌似沒有一個很好的理由去學習那些進階的python知識。雖說很少有機會去寫,但是會有很多機會去看。各種框架程式碼,庫基本上都會用到那些進階的知識,不懂基本上看別人的程式碼就會出現,每個字母我都認識,但我就是不知道他是用來幹啥的,為什麼要這樣用。總得來說就是,學進階的python知識是為了看懂別人的程式碼,在這個基礎上學習大牛們的優秀的用法,識別各種亂七八糟的用法。

識別各種亂七八糟的用法

下面那段程式碼是前人寫的,作用是給每個執行緒都生成一個物件,然後線上程中共享這個物件。localmap這字典生命週期是和程式一樣,每個執行緒都呼叫,隨著執行緒不斷的開關,localmap越來越大,導致了記憶體洩露。於是,有了版本2。

from threading import local

def threadlocal(localmap={}):
    thread_name = threading.currentThread().getName()
    if thread_name not in localmap:
        # 執行緒共享的變數,其它執行緒無法訪問
        localmap[thread_name] = local()
    return localmap[thread_name]複製程式碼

這裡是版本2。之前記憶體洩露是因為執行緒的生命結束後,沒有將localmap中對應的變數刪除,導致的。既然如此,就想辦法刪掉那些不需要的項就好了。問題雖然是解決了,但是糟糕的解決辦法。這段程式碼犯了兩個嚴重的錯誤。1. 對於全域性收集類的,不應該干擾收集物件的生命週期,這裡可以用弱引用的容器,引用計數沒有增加,不會影響物件的生命週期。2. threading庫中的local用錯了,它本身就實現了執行緒共享,執行緒間互不干涉,只需定義一個全域性local物件,線上程中使用就好了。如果不懂得弱引用容器的使用場景,是識別不出這亂七八糟的用法的。當然,對於問題2,這個是對local這個庫的誤解,一知半解的就用了,鬧出的笑話。

from threading import local

def threadlocal(localmap={}):
    thread_name = threading.currentThread().getName()
    if thread_name not in localmap:
        # 執行緒共享的變數,其它執行緒無法訪問
        localmap[thread_name] = local()

    if not order_process():
       # 獲取當前程式的執行緒
        current_threads = [obj.getName() for obj in threading.enumerate()]
        for key in localmap.keys():
            if key not in current_threads:
                localmap.pop(key)
    return localmap[thread_name]複製程式碼

看懂程式碼

繼續上面的例子。知道local的用法後,就有點好奇,它是怎麼實現的。去看實現的程式碼如下。這段程式碼涉及到的知識有,我就不解釋那些東西該怎麼用了,有興趣的去翻流暢的python這本進階書。

  • 例項建立與銷燬:__new__, __init__, __del__
  • 屬性管理:__getattribute__, __setattr__,__delattr__
  • 雜:__slots__
  • 鎖:RLock
  • 垃圾回收機制

缺乏上面的知識是看懂local的實現是怎樣的,只會一臉懵逼。local這個類實現的大體思路是,在建立local物件時,同時線上程物件的屬性字典__dict__裡面儲存了local物件的屬性字典,然後每次訪問加鎖,將執行緒物件儲存的local物件的__dict__屬性,載入local中。這裡的操作很值得玩味,也很好的解決了上面的錯誤示範中的記憶體洩露的問題。想要看懂的話,去了解一下垃圾回收機制,然後再慢慢把味一下。這裡的神操作,我覺得還是用弱引用來弄比較好,之所以不用,可能是這個庫寫的時候,還沒弱引用那個庫或者是說弱引用效能相對來說沒這神操作的效能好等等吧。話說,用來弱引用可以不用加鎖,併發效能應該會更高。

class _localbase(object):
    __slots__ = '_local__key', '_local__args', '_local__lock'

    def __new__(cls, *args, **kw):
        self = object.__new__(cls)
        key = '_local__key', 'thread.local.' + str(id(self))
        object.__setattr__(self, '_local__key', key)
        object.__setattr__(self, '_local__args', (args, kw))
        object.__setattr__(self, '_local__lock', RLock())

        if (args or kw) and (cls.__init__ is object.__init__):
            raise TypeError("Initialization arguments are not supported")

        # We need to create the thread dict in anticipation of
        # __init__ being called, to make sure we don't call it
        # again ourselves.
        dict = object.__getattribute__(self, '__dict__')
        current_thread().__dict__[key] = dict

        return self

def _patch(self):
    key = object.__getattribute__(self, '_local__key')
    d = current_thread().__dict__.get(key)
    if d is None:
        d = {}
        current_thread().__dict__[key] = d
        object.__setattr__(self, '__dict__', d)

        # we have a new instance dict, so call out __init__ if we have
        # one
        cls = type(self)
        if cls.__init__ is not object.__init__:
            args, kw = object.__getattribute__(self, '_local__args')
            cls.__init__(self, *args, **kw)
    else:
        object.__setattr__(self, '__dict__', d)

class local(_localbase):

    def __getattribute__(self, name):
        lock = object.__getattribute__(self, '_local__lock')
        lock.acquire()
        try:
            _patch(self)
            return object.__getattribute__(self, name)
        finally:
            lock.release()

    def __setattr__(self, name, value):
        if name == '__dict__':
            raise AttributeError(
                "%r object attribute '__dict__' is read-only"
                % self.__class__.__name__)
        lock = object.__getattribute__(self, '_local__lock')
        lock.acquire()
        try:
            _patch(self)
            return object.__setattr__(self, name, value)
        finally:
            lock.release()

    def __delattr__(self, name):
        if name == '__dict__':
            raise AttributeError(
                "%r object attribute '__dict__' is read-only"
                % self.__class__.__name__)
        lock = object.__getattribute__(self, '_local__lock')
        lock.acquire()
        try:
            _patch(self)
            return object.__delattr__(self, name)
        finally:
            lock.release()

    def __del__(self):
        import threading

        key = object.__getattribute__(self, '_local__key')

        try:
            # We use the non-locking API since we might already hold the lock
            # (__del__ can be called at any point by the cyclic GC).
            threads = threading._enumerate()
        except:
            # If enumerating the current threads fails, as it seems to do
            # during shutdown, we'll skip cleanup under the assumption
            # that there is nothing to clean up.
            return

        for thread in threads:
            try:
                __dict__ = thread.__dict__
            except AttributeError:
                # Thread is dying, rest in peace.
                continue

            if key in __dict__:
                try:
                    del __dict__[key]
                except KeyError:
                    pass # didn't have anything in this thread複製程式碼

結束語

在寫程式碼的過程中,確實是很少用到python的進階知識,但是看程式碼的過程中是會經常用到的。如果想要知其然,知其所以然還是要學習python的進階知識的。最後,雖然超級大牛說只有1%的python程式設計師會用到python那些高深的知識,但夢想還是要有的,萬一有機會成為那1%,但沒有知識儲備,機會就溜掉了。

相關文章