1. 程式人生 > >Python 常見面試題

Python 常見面試題

Python語言特性

1 Python的函式引數傳遞

看兩個例子:

a = 1
def fun(a):
    a = 2
fun(a)
print a  # 1
a = []
def fun(a):
    a.append(1)
fun(a)
print a  # [1]

所有的變數都可以理解是記憶體中一個物件的“引用”,或者,也可以看似c中void*的感覺。

通過id來看引用a的記憶體地址可以比較理解:

a = 1
def fun(a):
    print "func_in",id(a)   # func_in 41322472
    a = 2
    print "re-point"
,id(a), id(2) # re-point 41322448 41322448 print "func_out",id(a), id(1) # func_out 41322472 41322472 fun(a) print a # 1

注:具體的值在不同電腦上執行時可能不同。

可以看到,在執行完a = 2之後,a引用中儲存的值,即記憶體地址發生變化,由原來1物件的所在的地址變成了2這個實體物件的記憶體地址。

而第2個例子a引用儲存的記憶體值就不會發生變化:

a = []
def fun(a):
    print "func_in",id(a)  # func_in 53629256
a.append(1) print "func_out",id(a) # func_out 53629256 fun(a) print a # [1]

這裡記住的是型別是屬於物件的,而不是變數。而物件有兩種,“可更改”(mutable)與“不可更改”(immutable)物件。在python中,strings, tuples, 和numbers是不可更改的物件,而 list, dict, set 等則是可以修改的物件。(這就是這個問題的重點)

當一個引用傳遞給函式的時候,函式自動複製一份引用,這個函式裡的引用和外邊的引用沒有半毛關係了.所以第一個例子裡函式把引用指向了一個不可變物件,當函式返回的時候,外面的引用沒半毛感覺.而第二個例子就不一樣了,函式內的引用指向的是可變物件,對它的操作就和定位了指標地址一樣,在記憶體裡進行修改.

2 Python中的元類(metaclass)

3 @staticmethod和@classmethod

Python其實有3個方法,即靜態方法(staticmethod),類方法(classmethod)和例項方法,如下:

def foo(x):
    print "executing foo(%s)"%(x)

class A(object):
    def foo(self,x):
        print "executing foo(%s,%s)"%(self,x)

    @classmethod
    def class_foo(cls,x):
        print "executing class_foo(%s,%s)"%(cls,x)

    @staticmethod
    def static_foo(x):
        print "executing static_foo(%s)"%x

a=A()

這裡先理解下函式引數裡面的self和cls.這個self和cls是對類或者例項的繫結,對於一般的函式來說我們可以這麼呼叫foo(x),這個函式就是最常用的,它的工作跟任何東西(類,例項)無關.對於例項方法,我們知道在類裡每次定義方法的時候都需要繫結這個例項,就是foo(self, x),為什麼要這麼做呢?因為例項方法的呼叫離不開例項,我們需要把例項自己傳給函式,呼叫的時候是這樣的a.foo(x)(其實是foo(a, x)).類方法一樣,只不過它傳遞的是類而不是例項,A.class_foo(x).注意這裡的self和cls可以替換別的引數,但是python的約定是這倆,還是不要改的好.

對於靜態方法其實和普通的方法一樣,不需要對誰進行繫結,唯一的區別是呼叫的時候需要使用a.static_foo(x)或者A.static_foo(x)來呼叫.

\ 例項方法 類方法 靜態方法
a = A() a.foo(x) a.class_foo(x) a.static_foo(x)
A 不可用 A.class_foo(x) A.static_foo(x)

更多關於這個問題:

4 類變數和例項變數

類變數:

​ 是可在類的所有例項之間共享的值(也就是說,它們不是單獨分配給每個例項的)。例如下例中,num_of_instance 就是類變數,用於跟蹤存在著多少個Test 的例項。

例項變數:

例項化之後,每個例項單獨擁有的變數。

class Test(object):  
    num_of_instance = 0  
    def __init__(self, name):  
        self.name = name  
        Test.num_of_instance += 1  
  
if __name__ == '__main__':  
    print Test.num_of_instance   # 0
    t1 = Test('jack')  
    print Test.num_of_instance   # 1
    t2 = Test('lucy')  
    print t1.name , t1.num_of_instance  # jack 2
    print t2.name , t2.num_of_instance  # lucy 2

補充的例子

class Person:
    name="aaa"

p1=Person()
p2=Person()
p1.name="bbb"
print p1.name  # bbb
print p2.name  # aaa
print Person.name  # aaa

這裡p1.name="bbb"是例項呼叫了類變數,這其實和上面第一個問題一樣,就是函式傳參的問題,p1.name一開始是指向的類變數name="aaa",但是在例項的作用域裡把類變數的引用改變了,就變成了一個例項變數,self.name不再引用Person的類變數name了.

可以看看下面的例子:

class Person:
    name=[]

p1=Person()
p2=Person()
p1.name.append(1)
print p1.name  # [1]
print p2.name  # [1]
print Person.name  # [1]

5 Python自省

這個也是python彪悍的特性.

自省就是面向物件的語言所寫的程式在執行時,所能知道物件的型別.簡單一句就是執行時能夠獲得物件的型別.比如type(),dir(),getattr(),hasattr(),isinstance().

a = [1,2,3]
b = {'a':1,'b':2,'c':3}
c = True
print type(a),type(b),type(c) # <type 'list'> <type 'dict'> <type 'bool'>
print isinstance(a,list)  # True

6 字典推導式

可能你見過列表推導時,卻沒有見過字典推導式,在2.7中才加入的:

d = {key: value for (key, value) in iterable}

7 Python中單下劃線和雙下劃線

>>> class MyClass():
...     def __init__(self):
...             self.__superprivate = "Hello"
...             self._semiprivate = ", world!"
...
>>> mc = MyClass()
>>> print mc.__superprivate
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: myClass instance has no attribute '__superprivate'
>>> print mc._semiprivate
, world!
>>> print mc.__dict__
{'_MyClass__superprivate': 'Hello', '_semiprivate': ', world!'}

__foo__:一種約定,Python內部的名字,用來區別其他使用者自定義的命名,以防衝突,就是例如__init__(),__del__(),__call__()這些特殊方法

_foo:一種約定,用來指定變數私有.程式設計師用來指定私有變數的一種方式.不能用from module import * 匯入,其他方面和公有一樣訪問;

__foo:這個有真正的意義:解析器用_classname__foo來代替這個名字,以區別和其他類相同的命名,它無法直接像公有成員一樣隨便訪問,通過物件名._類名__xxx這樣的方式可以訪問.

8 字串格式化:%和.format

.format在許多方面看起來更便利.對於%最煩人的是它無法同時傳遞一個變數和元組.你可能會想下面的程式碼不會有什麼問題:

"hi there %s" % name

但是,如果name恰好是(1,2,3),它將會丟擲一個TypeError異常.為了保證它總是正確的,你必須這樣做:

"hi there %s" % (name,)   # 提供一個單元素的陣列而不是一個引數

但是有點醜…format就沒有這些問題.你給的第二個問題也是這樣,.format好看多了.

你為什麼不用它?

  • 不知道它(在讀這個之前)
  • 為了和Python2.5相容(譬如logging庫建議使用%(issue #4))

9 迭代器和生成器

這裡有個關於生成器的建立問題面試官有考: 問: 將列表生成式中[]改成() 之後資料結構是否改變? 答案:是,從列表變為生成器

>>> L = [x*x for x in range(10)]
>>> L
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> g = (x*x for x in range(10))
>>> g
<generator object <genexpr> at 0x0000028F8B774200>

通過列表生成式,可以直接建立一個列表。但是,受到記憶體限制,列表容量肯定是有限的。而且,建立一個包含百萬元素的列表,不僅是佔用很大的記憶體空間,如:我們只需要訪問前面的幾個元素,後面大部分元素所佔的空間都是浪費的。因此,沒有必要建立完整的列表(節省大量記憶體空間)。在Python中,我們可以採用生成器:邊迴圈,邊計算的機制—>generator

10 *args and **kwargs

*args**kwargs只是為了方便並沒有強制使用它們.

當你不確定你的函式裡將要傳遞多少引數時你可以用*args.例如,它可以傳遞任意數量的引數:

>>> def print_everything(*args):
        for count, thing in enumerate(args):
...         print '{0}. {1}'.format(count, thing)
...
>>> print_everything('apple', 'banana', 'cabbage')
0. apple
1. banana
2. cabbage

相似的,**kwargs允許你使用沒有事先定義的引數名:

>>> def table_things(**kwargs):
...     for name, value in kwargs.items():
...         print '{0} = {1}'.format(name, value)
...
>>> table_things(apple = 'fruit', cabbage = 'vegetable')
cabbage = vegetable
apple = fruit

你也可以混著用.命名引數首先獲得引數值然後所有的其他引數都傳遞給*args**kwargs.命名引數在列表的最前端.例如:

def table_things(titlestring, **kwargs)

*args**kwargs可以同時在函式的定義中,但是*args必須在**kwargs前面.

當呼叫函式時你也可以用***語法.例如:

>>> def print_three_things(a, b, c):
...     print 'a = {0}, b = {1}, c = {2}'.format(a,b,c)
...
>>> mylist = ['aardvark', 'baboon', 'cat']
>>> print_three_things(*mylist)

a = aardvark, b = baboon, c = cat

就像你看到的一樣,它可以傳遞列表(或者元組)的每一項並把它們解包.注意必須與它們在函式裡的引數相吻合.當然,你也可以在函式定義或者函式呼叫時用*.

11 面向切面程式設計AOP和裝飾器

這個AOP一聽起來有點懵,同學面阿里的時候就被問懵了…

裝飾器是一個很著名的設計模式,經常被用於有切面需求的場景,較為經典的有插入日誌、效能測試、事務處理等。裝飾器是解決這類問題的絕佳設計,有了裝飾器,我們就可以抽離出大量函式中與函式功能本身無關的雷同程式碼並繼續重用。概括的講,裝飾器的作用就是為已經存在的物件新增額外的功能。

12 鴨子型別

“當看到一隻鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,那麼這隻鳥就可以被稱為鴨子。”

我們並不關心物件是什麼型別,到底是不是鴨子,只關心行為。

比如在python中,有很多file-like的東西,比如StringIO,GzipFile,socket。它們有很多相同的方法,我們把它們當作檔案使用。

又比如list.extend()方法中,我們並不關心它的引數是不是list,只要它是可迭代的,所以它的引數可以是list/tuple/dict/字串/生成器等.

鴨子型別在動態語言中經常使用,非常靈活,使得python不想java那樣專門去弄一大堆的設計模式。

13 Python中過載

函式過載主要是為了解決兩個問題。

  1. 可變引數型別。
  2. 可變引數個數。

另外,一個基本的設計原則是,僅僅當兩個函式除了引數型別和引數個數不同以外,其功能是完全相同的,此時才使用函式過載,如果兩個函式的功能其實不同,那麼不應當使用過載,而應當使用一個名字不同的函式。

好吧,那麼對於情況 1 ,函式功能相同,但是引數型別不同,python 如何處理?答案是根本不需要處理,因為 python 可以接受任何型別的引數,如果函式的功能相同,那麼不同的引數型別在 python 中很可能是相同的程式碼,沒有必要做成兩個不同函式。

那麼對於情況 2 ,函式功能相同,但引數個數不同,python 如何處理?大家知道,答案就是預設引數。對那些缺少的引數設定為預設引數即可解決問題。因為你假設函式功能相同,那麼那些缺少的引數終歸是需要用的。

好了,鑑於情況 1 跟 情況 2 都有了解決方案,python 自然就不需要函式過載了。

14 新式類和舊式類

這個面試官問了,我說了老半天,不知道他問的真正意圖是什麼.

新式類很早在2.2就出現了,所以舊式類完全是相容的問題,Python3裡的類全部都是新式類.這裡有一個MRO問題可以瞭解下(新式類是廣度優先,舊式類是深度優先),<Python核心程式設計>裡講的也很多.

一箇舊式類的深度優先的例子

class A():
    def foo1(self):
        print "A"
class B(A):
    def foo2(self):
        pass
class C(A):
    def foo1(self):
        print "C"
class D(B, C):
    pass

d = D()
d.foo1()

# A

按照經典類的查詢順序從左到右深度優先的規則,在訪問d.foo1()的時候,D這個類是沒有的…那麼往上查詢,先找到B,裡面沒有,深度優先,訪問A,找到了foo1(),所以這時候呼叫的是A的foo1(),從而導致C重寫的foo1()被繞過

15 __new____init__的區別

這個__new__確實很少見到,先做了解吧.

  1. __new__是一個靜態方法,而__init__是一個例項方法.
  2. __new__方法會返回一個建立的例項,而__init__什麼都不返回.
  3. 只有在__new__返回一個cls的例項時後面的__init__才能被呼叫.
  4. 當建立一個新例項時呼叫__new__,初始化一個例項時用__init__.

ps: __metaclass__是建立類時起作用.所以我們可以分別使用__metaclass__,__new____init__來分別在類建立,例項建立和例項初始化的時候做一些小手腳.

16 單例模式

​ 單例模式是一種常用的軟體設計模式。在它的核心結構中只包含一個被稱為單例類的特殊類。通過單例模式可以保證系統中一個類只有一個例項而且該例項易於外界訪問,從而方便對例項個數的控制並節約系統資源。如果希望在系統中某個類的物件只能存在一個,單例模式是最好的解決方案。

__new__()__init__()之前被呼叫,用於生成例項物件。利用這個方法和類的屬性的特點可以實現設計模式的單例模式。單例模式是指建立唯一物件,單例模式設計的類只能例項 這個絕對常考啊.絕對要記住1~2個方法,當時面試官是讓手寫的.

1 使用__new__方法

class Singleton(object):
    def __new__(cls, *args, **kw):
        if not hasattr(cls, '_instance'):
            orig = super(Singleton, cls)
            cls._instance = orig.__new__(cls, *args, **kw)
        return cls._instance

class MyClass(Singleton):
    a = 1

2 共享屬性

建立例項時把所有例項的__dict__指向同一個字典,這樣它們具有相同的屬性和方法.


class Borg(object):
    _state = {}
    def __new__(cls, *args, **kw):
        ob = super(Borg, cls).__new__(cls, *args, **kw)
        ob.__dict__ = cls._state
        return ob

class MyClass2(Borg):
    a = 1

3 裝飾器版本

def singleton(cls):
    instances = {}
    def getinstance(*args, **kw):
        if cls not in instances:
            instances[cls] = cls(*args, **kw)
        return instances[cls]
    return getinstance

@singleton
class MyClass:
  ...

4 import方法

作為python的模組是天然的單例模式

# mysingleton.py
class My_Singleton(object):
    def foo(self):
        pass

my_singleton = My_Singleton()

# to use
from mysingleton import my_singleton

my_singleton.foo()

17 Python中的作用域

Python 中,一個變數的作用域總是由在程式碼中被賦值的地方所決定的。

當 Python 遇到一個變數的話他會按照這樣的順序進行搜尋:

本地作用域(Local)→當前作用域被嵌入的本地作用域(Enclosing locals)→全域性/模組作用域(Global)→內建作用域(Built-in)

18 GIL執行緒全域性鎖

執行緒全域性鎖(Global Interpreter Lock),即Python為了保證執行緒安全而採取的獨立執行緒執行的限制,說白了就是一個核只能在同一時間執行一個執行緒.對於io密集型任務,python的多執行緒起到作用,但對於cpu密集型任務,python的多執行緒幾乎佔不到任何優勢,還有可能因為爭奪資源而變慢。

解決辦法就是多程序和下面的協程(協程也只是單CPU,但是能減小切換代價提升效能).

19 協程

知乎被問到了,呵呵噠,跪了

簡單點說協程是程序和執行緒的升級版,程序和執行緒都面臨著核心態和使用者態的切換問題而耗費許多切換時間,而協程就是使用者自己控制切換的時機,不再需要陷入系統的核心態.

Python裡最常見的yield就是協程的思想!可以檢視第九個問題.

20 閉包

閉包(closure)是函數語言程式設計的重要的語法結構。閉包也是一種組織程式碼的結構,它同樣提高了程式碼的可重複使用性。

當一個內嵌函式引用其外部作作用域的變數,我們就會得到一個閉包. 總結一下,建立一個閉包必須滿足以下幾點:

  1. 必須有一個內嵌函式
  2. 內嵌函式必須引用外部函式中的變數
  3. 外部函式的返回值必須是內嵌函式

感覺閉包還是有難度的,幾句話是說不明白的,還是查查相關資料.

重點是函式執行後並不會被撤銷,就像16題的instance字典一樣,當函式執行完後,instance並不被銷燬,而是繼續留在記憶體空間裡.這個功能類似類裡的類變數,只不過遷移到了函式上.

閉包就像個空心球一樣,你知道外面和裡面,但你不知道中間是什麼樣.

21 lambda函式

其實就是一個匿名函式,為什麼叫lambda?因為和後面的函數語言程式設計有關.

推薦: 知乎

22 Python函數語言程式設計

這個需要適當的瞭解一下吧,畢竟函數語言程式設計在Python中也做了引用.

推薦: 酷殼

python中函數語言程式設計支援:

filter 函式的功能相當於過濾器。呼叫一個布林函式bool_func來迭代遍歷每個seq中的元素;返回一個使bool_seq返回值為true的元素的序列。

>>>a = [1,2,3,4,5,6,7]
>>>b = filter(lambda x: x > 5, a)
>>>print b
>>>[6,7]

map函式是對一個序列的每個項依次執行函式,下面是對一個序列每個項都乘以2:

>>> a = map(lambda x:x*2,[1,2,3])
>>> list(a)
[2, 4, 6]

reduce函式是對一個序列的每個項迭代呼叫函式,下面是求3的階乘:

>>> reduce(lambda x,y:x*y,range(1,4))
6

23 Python裡的拷貝

引用和copy(),deepcopy()的區別

import copy
a = [1, 2, 3, 4, ['a', 'b']]  #原始物件

b = a  #賦值,傳物件的引用
c = copy.copy(a)  #物件拷貝,淺拷貝
d = copy.deepcopy(a)  #物件拷貝,深拷貝

a.append(5)  #修改物件a
a[4].append('c')  #修改物件a中的['a', 'b']陣列物件

print 'a = ', a
print 'b = ', b
print 'c = ', c
print 'd = ', d

輸出結果:
a =  [1, 2, 3, 4, ['a', 'b', 'c'], 5]
b =  [1, 2, 3, 4, ['a', 'b', 'c'], 5]
c =  [1, 2, 3, 4, ['a', 'b', 'c']]
d =  [1, 2, 3, 4, ['a', 'b']]

24 Python垃圾回收機制

Python GC主要使用引用計數(reference counting)來跟蹤和回收垃圾。在引用計數的基礎上,通過“標記-清除”(mark and sweep)解決容器物件可能產生的迴圈引用問題,通過“分代回收”(generation collection)以空間換時間的方法提高垃圾回收效率。

1 引用計數

PyObject是每個物件必有的內容,其中ob_refcnt就是做為引用計數。當一個物件有新的引用時,它的ob_refcnt就會增加,當引用它的物件被刪除,它的ob_refcnt就會減少.引用計數為0時,該物件生命就結束了。

優點:

  1. 簡單
  2. 實時性

缺點:

  1. 維護引用計數消耗資源
  2. 迴圈引用

2 標記-清除機制

基本思路是先按需分配,等到沒有空閒記憶體的時候從暫存器和程式棧上的引用出發,遍歷以物件為節點、以引用為邊構成的圖,把所有可以訪問到的物件打上標記,然後清掃一遍記憶體空間,把所有沒標記的物件釋放。

3 分代技術

分代回收的整體思想是:將系統中的所有記憶體塊根據其存活時間劃分為不同的集合,每個集合就成為一個“代”,垃圾收集頻率隨著“代”的存活時間的增大而減小,存活時間通常利用經過幾次垃圾回收來度量。

Python預設定義了三代物件集合,索引數越大,物件存活時間越長。

舉例: 當某些記憶體塊M經過了3次垃圾收集的清洗之後還存活時,我們就將記憶體塊M劃到一個集合A中去,而新分配的記憶體都劃分到集合B中去。當垃圾收集開始工作時,大多數情況都只對集合B進行垃圾回收,而對集合A進行垃圾回收要隔相當長一段時間後才進行,這就使得垃圾收集機制需要處理的記憶體少了,效率自然就提高了。在這個過程中,集合B中的某些記憶體塊由於存活時間長而會被轉移到集合A中,當然,集合A中實際上也存在一些垃圾,這些垃圾的回收會因為這種分代的機制而被延遲。

25 Python的List

26 Python的is

is是對比地址,==是對比值

27 read,readline和readlines

  • read 讀取整個檔案
  • readline 讀取下一行,使用生成器方法
  • readlines 讀取整個檔案到一個迭代器以供我們遍歷

28 Python2和3的區別

29 super init

super() lets you avoid referring to the base class explicitly, which can be nice. But the main advantage comes with multiple inheritance, where all sorts of fun stuff can happen. See the standard docs on super if you haven’t already.

Note that the syntax changed in Python 3.0: you can just say super().__init__() instead of super(ChildB, self).__init__() which IMO is quite a bit nicer.

30 range and xrange

都在迴圈時使用,xrange記憶體效能更好。 for i in range(0, 20): for i in xrange(0, 20): What is the difference between range and xrange functions in Python 2.X? range creates a list, so if you do range(1, 10000000) it creates a list in memory with 9999999 elements. xrange is a sequence object that evaluates lazily.

作業系統

1 select,poll和epoll

其實所有的I/O都是輪詢的方法,只不過實現的層面不同罷了.

這個問題可能有點深入了,但相信能回答出這個問題是對I/O多路複用有很好的瞭解了.其中tornado使用的就是epoll的.

基本上select有3個缺點:

  1. 連線數受限
  2. 查詢配對速度慢
  3. 資料由核心拷貝到使用者態

poll改善了第一個缺點

epoll改了三個缺點.

2 排程演算法

  1. 先來先服務(FCFS, First Come First Serve)
  2. 短作業優先(SJF, Shortest Job First)
  3. 最高優先權排程(Priority Scheduling)
  4. 時間片輪轉(RR, Round Robin)
  5. 多級反饋佇列排程(multilevel feedback queue scheduling)

實時排程演算法:

  1. 最早截至時間優先 EDF
  2. 最低鬆弛度優先 LLF

3 死鎖

原因:

  1. 競爭資源
  2. 程式推進順序不當

必要條件:

  1. 互斥條件
  2. 請求和保持條件
  3. 不剝奪條件
  4. 環路等待條件

處理死鎖基本方法:

  1. 預防死鎖(摒棄除1以外的條件)
  2. 避免死鎖(銀行家演算法)
  3. 檢測死鎖(資源分配圖)
  4. 解除死鎖
    1. 剝奪資源
    2. 撤銷程序

4 程式編譯與連結

Bulid過程可以分解為4個步驟:預處理(Prepressing), 編譯(Compilation)、彙編(Assembly)、連結(Linking)

以c語言為例:

1 預處理

預編譯過程主要處理那些原始檔中的以“#”開始的預編譯指令,主要處理規則有:

  1. 將所有的“#define”刪除,並展開所用的巨集定義
  2. 處理所有條件預編譯指令,比如“#if”、“#ifdef”、 “#elif”、“#endif”
  3. 處理“#include”預編譯指令,將被包含的檔案插入到該編譯指令的位置,注:此過程是遞迴進行的
  4. 刪除所有註釋
  5. 新增行號和檔名標識,以便於編譯時編譯器產生除錯用的行號資訊以及用於編譯時產生編譯錯誤或警告時可顯示行號
  6. 保留所有的#pragma編譯器指令。

2 編譯

編譯過程就是把預處理完的檔案進行一系列的詞法分析、語法分析、語義分析及優化後生成相應的彙編程式碼檔案。這個過程是整個程式構建的核心部分。

3 彙編

彙編器是將彙編程式碼轉化成機器可以執行的指令,每一條彙編語句幾乎都是一條機器指令。經過編譯、連結、彙編輸出的檔案成為目標檔案(Object File)

4 連結

連結的主要內容就是把各個模組之間相互引用的部分處理好,使各個模組可以正確的拼接。 連結的主要過程包塊 地址和空間的分配(Address and Storage Allocation)、符號決議(Symbol Resolution)和重定位(Relocation)等步驟。

5 靜態連結和動態連結

靜態連結方法:靜態連結的時候,載入程式碼就會把程式會用到的動態程式碼或動態程式碼的地址確定下來 靜態庫的連結可以使用靜態連結,動態連結庫也可以使用這種方法連結匯入庫

動態連結方法:使用這種方式的程式並不在一開始就完成動態連結,而是直到真正呼叫動態庫程式碼時,載入程式才計算(被呼叫的那部分)動態程式碼的邏輯地址,然後等到某