最新Python筆試題2017 涵蓋知識面廣泛

pythontab發表於2017-04-21

引言

想找一份Python開發工作嗎?那你很可能得證明自己知道如何使用Python。下面這些問題涉及了與Python相關的許多技能,問題的關注點主要是語言本身,不是某個特定的包或模組。每一個問題都可以擴充為一個教程,如果可能的話。某些問題甚至會涉及多個領域。


我之前還沒有出過和這些題目一樣難的面試題,如果你能輕鬆地回答出來的話,趕緊去找份工作吧!

問題1

到底什麼是Python?你可以在回答中與其他技術進行對比(也鼓勵這樣做)。


答案

下面是一些關鍵點:

Python是一種解釋型語言。這就是說,與C語言和C的衍生語言不同,Python程式碼在執行之前不需要編譯。其他解釋型語言還包括PHP和Ruby。

Python是動態型別語言,指的是你在宣告變數時,不需要說明變數的型別。你可以直接編寫類似x=111和x="I'm a string"這樣的程式碼,程式不會報錯。

Python非常適合物件導向的程式設計(OOP),因為它支援透過組合(composition)與繼承(inheritance)的方式定義類(class)。Python中沒有訪問說明符(access specifier,類似C++中的public和private),這麼設計的依據是“大家都是成年人了”。

在Python語言中,函式是第一類物件(first-class objects)。這指的是它們可以被指定給變數,函式既能返回函式型別,也可以接受函式作為輸入。類(class)也是第一類物件。

Python程式碼編寫快,但是執行速度比編譯語言通常要慢。好在Python允許加入基於C語言編寫的擴充套件,因此我們能夠最佳化程式碼,消除瓶頸,這點通常是可以實現的。numpy就是一個很好地例子,它的執行速度真的非常快,因為很多算術運算其實並不是透過Python實現的。

Python用途非常廣泛——網路應用,自動化,科學建模,大資料應用,等等。它也常被用作“膠水語言”,幫助其他語言和元件改善執行狀況。

Python讓困難的事情變得容易,因此程式設計師可以專注於演算法和資料結構的設計,而不用處理底層的細節。


問題2

補充缺失的程式碼

def print_directory_contents(sPath):
    """
    這個函式接受資料夾的名稱作為輸入引數,
    返回該資料夾中檔案的路徑,
    以及其包含資料夾中檔案的路徑。
    """
    # 補充程式碼


答案

def print_directory_contents(sPath):
    import os                                       
    for sChild in os.listdir(sPath):                
        sChildPath = os.path.join(sPath,sChild)
        if os.path.isdir(sChildPath):
            print_directory_contents(sChildPath)
        else:
            print sChildPath

特別要注意以下幾點:


命名規範要統一。如果樣本程式碼中能夠看出命名規範,遵循其已有的規範。

遞迴函式需要遞迴併終止。確保你明白其中的原理,否則你將面臨無休無止的呼叫棧(callstack)。

我們使用os模組與作業系統進行互動,同時做到互動方式是可以跨平臺的。你可以把程式碼寫成sChildPath = sPath + '/' + sChild,但是這個在Windows系統上會出錯。

熟悉基礎模組是非常有價值的,但是別想破腦袋都背下來,記住Google是你工作中的良師益友。

如果你不明白程式碼的預期功能,就大膽提問。

堅持KISS原則!保持簡單,不過腦子就能懂!

為什麼提這個問題:


說明面試者對與作業系統互動的基礎知識

遞迴真是太好用啦


問題3

閱讀下面的程式碼,寫出A0,A1至An的最終值。

A0 = dict(zip(('a','b','c','d','e'),(1,2,3,4,5)))
A1 = range(10)
A2 = [i for i in A1 if i in A0]
A3 = [A0[s] for s in A0]
A4 = [i for i in A1 if i in A3]
A5 = {i:i*i for i in A1}
A6 = [[i,i*i] for i in A1]

答案

A0 = {'a': 1, 'c': 3, 'b': 2, 'e': 5, 'd': 4}
A1 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
A2 = []
A3 = [1, 3, 2, 5, 4]
A4 = [1, 2, 3, 4, 5]
A5 = {0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81}
A6 = [[0, 0], [1, 1], [2, 4], [3, 9], [4, 16], [5, 25], [6, 36], [7, 49], [8, 64], [9, 81]]

為什麼提這個問題:


列表解析(list comprehension)十分節約時間,對很多人來說也是一個大的學習障礙。

如果你讀懂了這些程式碼,就很可能可以寫下正確地值。

其中部分程式碼故意寫的怪怪的。因為你共事的人之中也會有怪人。

問題4

Python和多執行緒(multi-threading)。這是個好主意碼?列舉一些讓Python程式碼以並行方式執行的方法。


答案


Python並不支援真正意義上的多執行緒。Python中提供了多執行緒包,但是如果你想透過多執行緒提高程式碼的速度,使用多執行緒包並不是個好主意。Python中有一個被稱為Global Interpreter Lock(GIL)的東西,它會確保任何時候你的多個執行緒中,只有一個被執行。執行緒的執行速度非常之快,會讓你誤以為執行緒是並行執行的,但是實際上都是輪流執行。經過GIL這一道關卡處理,會增加執行的開銷。這意味著,如果你想提高程式碼的執行速度,使用threading包並不是一個很好的方法。


不過還是有很多理由促使我們使用threading包的。如果你想同時執行一些任務,而且不考慮效率問題,那麼使用這個包是完全沒問題的,而且也很方便。但是大部分情況下,並不是這麼一回事,你會希望把多執行緒的部分外包給作業系統完成(透過開啟多個程式),或者是某些呼叫你的Python程式碼的外部程式(例如Spark或Hadoop),又或者是你的Python程式碼呼叫的其他程式碼(例如,你可以在Python中呼叫C函式,用於處理開銷較大的多執行緒工作)。


為什麼提這個問題


因為GIL就是個混賬東西(A-hole)。很多人花費大量的時間,試圖尋找自己多執行緒程式碼中的瓶頸,直到他們明白GIL的存在。


問題5

你如何管理不同版本的程式碼?


答案


版本管理!被問到這個問題的時候,你應該要表現得很興奮,甚至告訴他們你是如何使用Git(或是其他你最喜歡的工具)追蹤自己和奶奶的書信往來。我偏向於使用Git作為版本控制系統(VCS),但還有其他的選擇,比如subversion(SVN)。


為什麼提這個問題:


因為沒有版本控制的程式碼,就像沒有杯子的咖啡。有時候我們需要寫一些一次性的、可以隨手扔掉的指令碼,這種情況下不作版本控制沒關係。但是如果你面對的是大量的程式碼,使用版本控制系統是有利的。版本控制能夠幫你追蹤誰對程式碼庫做了什麼操作;發現新引入了什麼bug;管理你的軟體的不同版本和發行版;在團隊成員中分享原始碼;部署及其他自動化處理。它能讓你回滾到出現問題之前的版本,單憑這點就特別棒了。還有其他的好功能。怎麼一個棒字了得!


問題6

下面程式碼會輸出什麼:

def f(x,l=[]):
    for i in range(x):
        l.append(i*i)
    print l
f(2)
f(3,[3,2,1])
f(3)

答案:

[0, 1]
[3, 2, 1, 0, 1, 4]
[0, 1, 0, 1, 4]

呃?


第一個函式呼叫十分明顯,for迴圈先後將0和1新增至了空列表l中。l是變數的名字,指向記憶體中儲存的一個列表。第二個函式呼叫在一塊新的記憶體中建立了新的列表。l這時指向了新生成的列表。之後再往新列表中新增0、1、2和4。很棒吧。第三個函式呼叫的結果就有些奇怪了。它使用了之前記憶體地址中儲存的舊列表。這就是為什麼它的前兩個元素是0和1了。


不明白的話就試著執行下面的程式碼吧:

l_mem = []
l = l_mem           # the first call
for i in range(2):
    l.append(i*i)
print l             # [0, 1]
l = [3,2,1]         # the second call
for i in range(3):
    l.append(i*i)
print l             # [3, 2, 1, 0, 1, 4]
l = l_mem           # the third call
for i in range(3):
    l.append(i*i)
print l             # [0, 1, 0, 1, 4]


問題7

“猴子補丁”(monkey patching)指的是什麼?這種做法好嗎?


答案


“猴子補丁”就是指,在函式或物件已經定義之後,再去改變它們的行為。


舉個例子:

import datetime
datetime.datetime.now = lambda: datetime.datetime(2012, 12, 12)

大部分情況下,這是種很不好的做法 - 因為函式在程式碼庫中的行為最好是都保持一致。打“猴子補丁”的原因可能是為了測試。mock包對實現這個目的很有幫助。


為什麼提這個問題?


答對這個問題說明你對單元測試的方法有一定了解。你如果提到要避免“猴子補丁”,可以說明你不是那種喜歡花裡胡哨程式碼的程式設計師(公司裡就有這種人,跟他們共事真是糟糕透了),而是更注重可維護性。還記得KISS原則碼?答對這個問題還說明你明白一些Python底層運作的方式,函式實際是如何儲存、呼叫等等。


另外:如果你沒讀過mock模組的話,真的值得花時間讀一讀。這個模組非常有用。


問題8

這兩個引數是什麼意思:*args,**kwargs?我們為什麼要使用它們?


答案


如果我們不確定要往函式中傳入多少個引數,或者我們想往函式中以列表和元組的形式傳引數時,那就使要用*args;如果我們不知道要往函式中傳入多少個關鍵詞引數,或者想傳入字典的值作為關鍵詞引數時,那就要使用**kwargs。args和kwargs這兩個識別符號是約定俗成的用法,你當然還可以用*bob和**billy,但是這樣就並不太妥。


下面是具體的示例:

def f(*args,**kwargs): print args, kwargs
l = [1,2,3]
t = (4,5,6)
d = {'a':7,'b':8,'c':9}
f()
f(1,2,3)                    # (1, 2, 3) {}
f(1,2,3,"pythontab")           # (1, 2, 3, 'pythontab') {}
f(a=1,b=2,c=3)              # () {'a': 1, 'c': 3, 'b': 2}
f(a=1,b=2,c=3,zzz="hi")     # () {'a': 1, 'c': 3, 'b': 2, 'zzz': 'hi'}
f(1,2,3,a=1,b=2,c=3)        # (1, 2, 3) {'a': 1, 'c': 3, 'b': 2}
f(*l,**d)                   # (1, 2, 3) {'a': 7, 'c': 9, 'b': 8}
f(*t,**d)                   # (4, 5, 6) {'a': 7, 'c': 9, 'b': 8}
f(1,2,*t)                   # (1, 2, 4, 5, 6) {}
f(q="winning",**d)          # () {'a': 7, 'q': 'winning', 'c': 9, 'b': 8}
f(1,2,*t,q="winning",**d)   # (1, 2, 4, 5, 6) {'a': 7, 'q': 'winning', 'c': 9, 'b': 8}
def f2(arg1,arg2,*args,**kwargs): print arg1,arg2, args, kwargs
f2(1,2,3)                       # 1 2 (3,) {}
f2(1,2,3,"pythontab")              # 1 2 (3, 'pythontab') {}
f2(arg1=1,arg2=2,c=3)           # 1 2 () {'c': 3}
f2(arg1=1,arg2=2,c=3,zzz="hi")  # 1 2 () {'c': 3, 'zzz': 'hi'}
f2(1,2,3,a=1,b=2,c=3)           # 1 2 (3,) {'a': 1, 'c': 3, 'b': 2}
f2(*l,**d)                   # 1 2 (3,) {'a': 7, 'c': 9, 'b': 8}
f2(*t,**d)                   # 4 5 (6,) {'a': 7, 'c': 9, 'b': 8}
f2(1,2,*t)                   # 1 2 (4, 5, 6) {}
f2(1,1,q="winning",**d)      # 1 1 () {'a': 7, 'q': 'winning', 'c': 9, 'b': 8}
f2(1,2,*t,q="winning",**d)   # 1 2 (4, 5, 6) {'a': 7, 'q': 'winning', 'c': 9, 'b': 8}

為什麼提這個問題?


有時候,我們需要往函式中傳入未知個數的引數或關鍵詞引數。有時候,我們也希望把引數或關鍵詞引數儲存起來,以備以後使用。有時候,僅僅是為了節省時間。


問題9

下面這些是什麼意思:@classmethod, @staticmethod, @property?


回答背景知識


這些都是裝飾器(decorator)。裝飾器是一種特殊的函式,要麼接受函式作為輸入引數,並返回一個函式,要麼接受一個類作為輸入引數,並返回一個類。@標記是語法糖(syntactic sugar),可以讓你以簡單易讀得方式裝飾目標物件。

@my_decorator
def my_func(stuff):
    do_things
Is equivalent to
def my_func(stuff):
    do_things
my_func = my_decorator(my_func)

你可以在本網站上找到介紹裝飾器工作原理的教材。


真正的答案


@classmethod, @staticmethod和@property這三個裝飾器的使用物件是在類中定義的函式。下面的例子展示了它們的用法和行為:

class MyClass(object):
    def __init__(self):
        self._some_property = "properties are nice"
        self._some_other_property = "VERY nice"
    def normal_method(*args,**kwargs):
        print "calling normal_method({0},{1})".format(args,kwargs)
    @classmethod
    def class_method(*args,**kwargs):
        print "calling class_method({0},{1})".format(args,kwargs)
    @staticmethod
    def static_method(*args,**kwargs):
        print "calling static_method({0},{1})".format(args,kwargs)
    @property
    def some_property(self,*args,**kwargs):
        print "calling some_property getter({0},{1},{2})".format(self,args,kwargs)
        return self._some_property
    @some_property.setter
    def some_property(self,*args,**kwargs):
        print "calling some_property setter({0},{1},{2})".format(self,args,kwargs)
        self._some_property = args[0]
    @property
    def some_other_property(self,*args,**kwargs):
        print "calling some_other_property getter({0},{1},{2})".format(self,args,kwargs)
        return self._some_other_property
o = MyClass()
# 未裝飾的方法還是正常的行為方式,需要當前的類例項(self)作為第一個引數。
o.normal_method 
# <bound method MyClass.normal_method of <__main__.MyClass instance at 0x7fdd2537ea28>>
o.normal_method() 
# normal_method((<__main__.MyClass instance at 0x7fdd2537ea28>,),{})
o.normal_method(1,2,x=3,y=4) 
# normal_method((<__main__.MyClass instance at 0x7fdd2537ea28>, 1, 2),{'y': 4, 'x': 3})
# 類方法的第一個引數永遠是該類
o.class_method
# <bound method classobj.class_method of <class __main__.MyClass at 0x7fdd2536a390>>
o.class_method()
# class_method((<class __main__.MyClass at 0x7fdd2536a390>,),{})
o.class_method(1,2,x=3,y=4)
# class_method((<class __main__.MyClass at 0x7fdd2536a390>, 1, 2),{'y': 4, 'x': 3})
# 靜態方法(static method)中除了你呼叫時傳入的引數以外,沒有其他的引數。
o.static_method
# <function static_method at 0x7fdd25375848>
o.static_method()
# static_method((),{})
o.static_method(1,2,x=3,y=4)
# static_method((1, 2),{'y': 4, 'x': 3})
# @property是實現getter和setter方法的一種方式。直接呼叫它們是錯誤的。
# “只讀”屬性可以透過只定義getter方法,不定義setter方法實現。
o.some_property
# 呼叫some_property的getter(<__main__.MyClass instance at 0x7fb2b70877e8>,(),{})
# 'properties are nice'
# “屬性”是很好的功能
o.some_property()
# calling some_property getter(<__main__.MyClass instance at 0x7fb2b70877e8>,(),{})
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
# TypeError: 'str' object is not callable
o.some_other_property
# calling some_other_property getter(<__main__.MyClass instance at 0x7fb2b70877e8>,(),{})
# 'VERY nice'
# o.some_other_property()
# calling some_other_property getter(<__main__.MyClass instance at 0x7fb2b70877e8>,(),{})
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
# TypeError: 'str' object is not callable
o.some_property = "pythontab"
# calling some_property setter(<__main__.MyClass object at 0x7fb2b7077890>,('pythontab',),{})
o.some_property
# calling some_property getter(<__main__.MyClass object at 0x7fb2b7077890>,(),{})
# 'pythontab'
o.some_other_property = "pythontab.com"
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
# AttributeError: can't set attribute
o.some_other_property
# calling some_other_property getter(<__main__.MyClass object at 0x7fb2b7077890>,(),{})


問題10

簡要描述Python的垃圾回收機制(garbage collection)。


答案


這裡能說的很多。你應該提到下面幾個主要的點:


Python在記憶體中儲存了每個物件的引用計數(reference count)。如果計數值變成0,那麼相應的物件就會小時,分配給該物件的記憶體就會釋放出來用作他用。

偶爾也會出現引用迴圈(reference cycle)。垃圾回收器會定時尋找這個迴圈,並將其回收。舉個例子,假設有兩個物件o1和o2,而且符合o1.x == o2和o2.x == o1這兩個條件。如果o1和o2沒有其他程式碼引用,那麼它們就不應該繼續存在。但它們的引用計數都是1。

Python中使用了某些啟發式演算法(heuristics)來加速垃圾回收。例如,越晚建立的物件更有可能被回收。物件被建立之後,垃圾回收器會分配它們所屬的代(generation)。每個物件都會被分配一個代,而被分配更年輕代的物件是優先被處理的。


問題11

將下面的函式按照執行效率高低排序。它們都接受由0至1之間的數字構成的列表作為輸入。這個列表可以很長。一個輸入列表的示例如下:[random.random() for i in range(100000)]。你如何證明自己的答案是正確的。

def f1(lIn):
    l1 = sorted(lIn)
    l2 = [i for i in l1 if i<0.5]
    return [i*i for i in l2]
def f2(lIn):
    l1 = [i for i in lIn if i<0.5]
    l2 = sorted(l1)
    return [i*i for i in l2]
def f3(lIn):
    l1 = [i*i for i in lIn]
    l2 = sorted(l1)
    return [i for i in l1 if i<(0.5*0.5)]

答案


按執行效率從高到低排列:f2、f1和f3。要證明這個答案是對的,你應該知道如何分析自己程式碼的效能。Python中有一個很好的程式分析包,可以滿足這個需求。

import cProfile
lIn = [random.random() for i in range(100000)]
cProfile.run('f1(lIn)')
cProfile.run('f2(lIn)')
cProfile.run('f3(lIn)')

為了向大家進行完整地說明,下面我們給出上述分析程式碼的輸出結果:

>>> cProfile.run('f1(lIn)')
         4 function calls in 0.045 seconds
   Ordered by: standard name
   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.009    0.009    0.044    0.044 <stdin>:1(f1)
        1    0.001    0.001    0.045    0.045 <string>:1(<module>)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
        1    0.035    0.035    0.035    0.035 {sorted}
>>> cProfile.run('f2(lIn)')
         4 function calls in 0.024 seconds
   Ordered by: standard name
   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.008    0.008    0.023    0.023 <stdin>:1(f2)
        1    0.001    0.001    0.024    0.024 <string>:1(<module>)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
        1    0.016    0.016    0.016    0.016 {sorted}
>>> cProfile.run('f3(lIn)')
         4 function calls in 0.055 seconds
   Ordered by: standard name
   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.016    0.016    0.054    0.054 <stdin>:1(f3)
        1    0.001    0.001    0.055    0.055 <string>:1(<module>)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
        1    0.038    0.038    0.038    0.038 {sorted}

為什麼提這個問題?


定位並避免程式碼瓶頸是非常有價值的技能。想要編寫許多高效的程式碼,最終都要回答常識上來——在上面的例子中,如果列表較小的話,很明顯是先進行排序更快,因此如果你可以在排序前先進行篩選,那通常都是比較好的做法。其他不顯而易見的問題仍然可以透過恰當的工具來定位。因此瞭解這些工具是有好處的。


相關文章