2018最新BAT 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*的感覺。
這裡記住的是型別是屬於物件的,而不是變數。而物件有兩種,“可更改”(mutable)與“不可更改”(immutable)物件。在python中,strings, tuples, 和numbers是不可更改的物件,而list,dict等則是可以修改的物件。(這就是這個問題的重點)
當一個引用傳遞給函式的時候,函式自動複製一份引用,這個函式裡的引用和外邊的引用沒有半毛關係了.所以第一個例子裡函式把引用指向了一個不可變物件,當函式返回的時候,外面的引用沒半毛感覺.而第二個例子就不一樣了,函式內的引用指向的是可變物件,對它的操作就和定位了指標地址一樣,在記憶體裡進行修改.
2 Python中的元類(metaclass)
元類就是用來建立類的“東西”。你建立類就是為了建立類的例項物件,但是我們已經學習到了Python中的類也是物件。好吧,元類就是用來建立這些類(物件)的,元類就是類的類
3 @staticmethod和@classmethod
Python其實有3個方法,即靜態方法(staticmethod),類方法(classmethod)和例項方法,如下:
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(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 類變數和例項變數
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().
6 字典推導式
可能你見過列表推導時,卻沒有見過字典推導式,在2.7中才加入的:
d = {key: value for (key, value) in iterable} |
7 Python中單下劃線和雙下劃線
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
>>> 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內部的名字,用來區別其他使用者自定義的命名,以防衝突.
_foo:一種約定,用來指定變數私有.程式設計師用來指定私有變數的一種方式.
__foo:這個有真正的意義:解析器用_classname__foo來代替這個名字,以區別和其他類相同的命名.
詳情見:
http://www.zhihu.com/question/19754941
8 字串格式化:%和.format
.format在許多方面看起來更便利.對於%最煩人的是它無法同時傳遞一個變數和元組.你可能會想下面的程式碼不會有什麼問題:
Python:
"hi there %s" % name |
但是,如果name恰好是(1,2,3),它將會丟擲一個TypeError異常.為了保證它總是正確的,你必須這樣做:
"hi there %s" % (name,) # 提供一個單元素的陣列而不是一個引數 |
9 迭代器和生成器
在Python中,這種一邊迴圈一邊計算的機制,稱為生成器:generator。
可以被next()函式呼叫並不斷返回下一個值的物件稱為迭代器:Iterator。
這個是stackoverflow裡python排名第一的問題,值得一看: http://stackoverflow.com/questions/231767/what-does-the-yield-keyword-do-in-python
10 *args and **kwargs
用*args和**kwargs只是為了方便並沒有強制使用它們.
當你不確定你的函式裡將要傳遞多少引數時你可以用*args.例如,它可以傳遞任意數量的引數:
1 2 3 4 5 6 7 8 |
>>> 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允許你使用沒有事先定義的引數名:
1 2 3 4 5 6 7 |
>>> 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.命名引數在列表的最前端.例如:
1 |
def table_things(titlestring, **kwargs) |
*args和**kwargs可以同時在函式的定義中,但是*args必須在**kwargs前面.
當呼叫函式時你也可以用*和**語法.例如:
1 2 3 4 5 6 7 |
>>> 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 |
就像你看到的一樣,它可以傳遞列表(或者元組)的每一項並把它們解包.注意必須與它們在函式裡的引數相吻合.當然,你也可以在函式定義或者函式呼叫時用*.
http://stackoverflow.com/questions/3394835/args-and-kwargs
11 面向切面程式設計AOP和裝飾器
這個AOP一聽起來有點懵,同學面試的時候就被問懵了…
裝飾器是一個很著名的設計模式,經常被用於有切面需求的場景,較為經典的有插入日誌、效能測試、事務處理等。裝飾器是解決這類問題的絕佳設計,有了裝飾器,我們就可以抽離出大量函式中與函式功能本身無關的雷同程式碼並繼續重用。概括的講,裝飾器的作用就是為已經存在的物件新增額外的功能。
這個問題比較大,推薦: http://stackoverflow.com/questions/739654/how-can-i-make-a-chain-of-function-decorators-in-python
中文: http://taizilongxu.gitbooks.io/stackoverflow-about-python/content/3/README.html
12 鴨子型別
“當看到一隻鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,那麼這隻鳥就可以被稱為鴨子。”
我們並不關心物件是什麼型別,到底是不是鴨子,只關心行為。
比如在python中,有很多file-like的東西,比如StringIO,GzipFile,socket。它們有很多相同的方法,我們把它們當作檔案使用。
又比如list.extend()方法中,我們並不關心它的引數是不是list,只要它是可迭代的,所以它的引數可以是list/tuple/dict/字串/生成器等.
鴨子型別在動態語言中經常使用,非常靈活,使得python不想java那樣專門去弄一大堆的設計模式。
13 Python中過載
引自知乎:http://www.zhihu.com/question/20053359
函式過載主要是為了解決兩個問題。
- 可變引數型別。
- 可變引數個數。
另外,一個基本的設計原則是,僅僅當兩個函式除了引數型別和引數個數不同以外,其功能是完全相同的,此時才使用函式過載,如果兩個函式的功能其實不同,那麼不應當使用過載,而應當使用一個名字不同的函式。
好吧,那麼對於情況 1 ,函式功能相同,但是引數型別不同,python 如何處理?答案是根本不需要處理,因為 python 可以接受任何型別的引數,如果函式的功能相同,那麼不同的引數型別在 python 中很可能是相同的程式碼,沒有必要做成兩個不同函式。
那麼對於情況 2 ,函式功能相同,但引數個數不同,python 如何處理?大家知道,答案就是預設引數。對那些缺少的引數設定為預設引數即可解決問題。因為你假設函式功能相同,那麼那些缺少的引數終歸是需要用的。
好了,鑑於情況 1 跟 情況 2 都有了解決方案,python 自然就不需要函式過載了。
14 新式類和舊式類
這個面試官問了,我說了老半天,不知道他問的真正意圖是什麼.
這篇文章很好的介紹了新式類的特性: http://www.cnblogs.com/btchenguang/archive/2012/09/17/2689146.html
新式類很早在2.2就出現了,所以舊式類完全是相容的問題,Python3裡的類全部都是新式類.這裡有一個MRO問題可以瞭解下(新式類是廣度優先,舊式類是深度優先),<Python核心程式設計>裡講的也很多.
15 __new__和__init__的區別
這個__new__確實很少見到,先做了解吧.
- __new__是一個靜態方法,而__init__是一個例項方法.
- __new__方法會返回一個建立的例項,而__init__什麼都不返回.
- 只有在__new__返回一個cls的例項時後面的__init__才能被呼叫.
- 當建立一個新例項時呼叫__new__,初始化一個例項時用__init__.
ps: __metaclass__是建立類時起作用.所以我們可以分別使用__metaclass__,__new__和__init__來分別在類建立,例項建立和例項初始化的時候做一些小手腳.
16 單例模式
這個絕對常考啊.絕對要記住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__
指向同一個字典,這樣它們具有相同的屬性和方法.
1 2 3 4 5 6 7 8 9 |
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 裝飾器版本
1 2 3 4 5 6 7 8 9 10 11 |
def singleton(cls, *args, **kw): instances = {} def getinstance(): 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為了保證執行緒安全而採取的獨立執行緒執行的限制,說白了就是一個核只能在同一時間執行一個執行緒.
解決辦法就是多程序和下面的協程(協程也只是單CPU,但是能減小切換代價提升效能).
19 協程
簡單點說協程是程序和執行緒的升級版,程序和執行緒都面臨著核心態和使用者態的切換問題而耗費許多切換時間,而協程就是使用者自己控制切換的時機,不再需要陷入系統的核心態.
Python裡最常見的yield就是協程的思想!可以檢視第九個問題.
20 閉包
閉包(closure)是函數語言程式設計的重要的語法結構。閉包也是一種組織程式碼的結構,它同樣提高了程式碼的可重複使用性。
當一個內嵌函式引用其外部作作用域的變數,我們就會得到一個閉包. 總結一下,建立一個閉包必須滿足以下幾點:
- 必須有一個內嵌函式
- 內嵌函式必須引用外部函式中的變數
- 外部函式的返回值必須是內嵌函式
感覺閉包還是有難度的,幾句話是說不明白的,還是查查相關資料.
重點是函式執行後並不會被撤銷,就像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()的區別
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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時,該物件生命就結束了。
優點:
- 簡單
- 實時性
缺點:
- 維護引用計數消耗資源
- 迴圈引用
2 標記-清除機制
基本思路是先按需分配,等到沒有空閒記憶體的時候從暫存器和程式棧上的引用出發,遍歷以物件為節點、以引用為邊構成的圖,把所有可以訪問到的物件打上標記,然後清掃一遍記憶體空間,把所有沒標記的物件釋放。
3 分代技術
分代回收的整體思想是:將系統中的所有記憶體塊根據其存活時間劃分為不同的集合,每個集合就成為一個“代”,垃圾收集頻率隨著“代”的存活時間的增大而減小,存活時間通常利用經過幾次垃圾回收來度量。
Python預設定義了三代物件集合,索引數越大,物件存活時間越長。
舉例: 當某些記憶體塊M經過了3次垃圾收集的清洗之後還存活時,我們就將記憶體塊M劃到一個集合A中去,而新分配的記憶體都劃分到集合B中去。當垃圾收集開始工作時,大多數情況都只對集合B進行垃圾回收,而對集合A進行垃圾回收要隔相當長一段時間後才進行,這就使得垃圾收集機制需要處理的記憶體少了,效率自然就提高了。在這個過程中,集合B中的某些記憶體塊由於存活時間長而會被轉移到集合A中,當然,集合A中實際上也存在一些垃圾,這些垃圾的回收會因為這種分代的機制而被延遲。
25 Python裡面如何實現tuple和list的轉換?
答:tuple,可以說是不可變的list,訪問方式還是通過索引下標的方式。 當你明確定義個tuple是,如果僅有一個元素,必須帶有,例如:(1,)。 當然,在2.7以後的版,python裡還增加了命名式的tuple! 至於有什麼用,首先第一點,樓主玩過python都知道,python的函式可以有多返回值的,而python裡,多返回值,就是用tuple來表示,這是用的最廣的了, 比如說,你需要定義一個常量的列表,但你又不想使用list,那也可以是要你管tuple,例如: if a in ('A','B','C'):pass
26 Python的is
is是對比地址,==是對比值
27 read,readline和readlines
- read 讀取整個檔案
- readline 讀取下一行,使用生成器方法
- readlines 讀取整個檔案到一個迭代器以供我們遍歷
28 Python2和3的區別
大部分Python庫都同時支援Python 2.7.x和3.x版本的,所以不論選擇哪個版本都是可以的。但為了在使用Python時避開某些版本中一些常見的陷阱,或需要移植某個Python專案
使用__future__模組
print函式
整數除法
Unicode
xrange
觸發異常
處理異常
next()函式和.next()方法
For迴圈變數與全域性名稱空間洩漏
比較無序型別
使用input()解析輸入內容
返回可迭代物件,而不是列表
29到底什麼是Python?你可以在回答中與其他技術進行對比
答案
下面是一些關鍵點: