1. 程式人生 > >Python基礎面試中常常問道的問題

Python基礎面試中常常問道的問題

今天來積攢一波經驗。

python語法及其他基礎部分(儲存後續增加)

1、可變與不可變型別:
2、深拷貝和淺拷貝的實現方式、區別:
3、deepcopy如果你來設計,如何實現:
4、new()與init()的區別:
5、你知道的幾種設計模式:
6、編碼和解碼你瞭解過 麼
7、列表推到list comprehension和生成器的優劣
8、什麼是裝飾器,如果想在函式之後進行裝飾,應該怎麼做
9、手寫個使用裝飾器實現單例模式:
10、使用裝飾器的單例模式和使用其他方法的單例模式,在後續的使用中有什麼區別?
11、手寫 正則郵箱地址:
12、介紹下垃圾回收、引用計數\分代回收\孤立引用環:
13、多程序和多執行緒的區別、CPU密集型適合用什麼?
14、程序通訊的方式有幾種?
15、介紹一下協成,為何比執行緒還快?
16、range和xrange的區別
17、將IP字串“172.0.0.1”轉換為32位二進位制的函式。

以上問題下面進行總結一下。

1、可變與不可變型別
python中的物件分為:可變物件和不可變物件.

可變物件:列表(list)、字典(dict )
不可變物件:int、string、float、tuple

可變物件和不可變物件之間的區別:可變資料型別中,即便對資料進行更改,資料的id也不會發生變化,而不可變資料型別中,只要對資料的值進行更改,則資料的id就發生變化。
下面來舉個例子說明問題:

對於可變物件 list

>>> listtest1=[]
>>> id(listtest1)
42033680
>>> listtest2=listtest1   # listtest2和listtest1 指向同一個記憶體地址 42033680
>>> id(listtest1) 42033680 >>> listtest1.append(5) # 修改listtest1的值 >>> listtest1 # listtest1的內容發生了變化 [5] >>> id(listtest1) # 但是listtest1的記憶體地址並沒有發生變化,這是因為list物件是可變型別,所以修改list物件的值並不會開闢一片記憶體來儲存這個值,而是在原物件的基礎上進行修改,所以listtest1的id並未發生變化。同理,listtest2也一樣。
42033680 >>> listtest2 [5] >>> id(listtest2) 42033680

對於可變物件 dict

>>> dicttest1={}
>>> id(dicttest1)
42047360
>>> dicttest1["name"]="PanPanZhao"
>>> dicttest1
{'name': 'PanPanZhao'}
>>> id(dicttest1) # 記憶體地址並未發生變化
42047360

對於不可變物件int :

>>> x=y=5
>>> x
5
>>> y
5
>>> id(x)
33971488
>>> id(y)
33971488
>>> x=x+1 # x的值和記憶體地址發生了變化,
>>> x  
6
>>> id(x)
33971476
>>> y  # y的值和記憶體地址並未發生變化
5
>>> id(y)
33971488

總結一下:對於不可變型別int,無論建立多少個不可變物件,只要它們的值相同,就指向同一個記憶體地址,同樣的情況還適用於較短的字串。
但是對於其他不可變型別就不一樣了,對於float物件而言,建立多個float物件,即使它們的值相同,它們所指向的記憶體地址是不同的。
原因:python對int和短字串進行了快取,無論宣告多少個值相同的變數,實際上都指向同個記憶體地址。

1.1 可變與不可變型別的有什麼作用
答:python函式的引數傳遞:在python中規定函式的引數傳遞為引用傳遞,也就是說傳遞給函式的引數是這個變數所指向的記憶體地址。但是在C中,引數傳遞可以有值傳遞和引用傳遞,當需要修改外面引數的值的時候就才用引用傳遞(在引數前面加一個*,表示傳遞引數所指的記憶體地址),但不需要修改外部引數值的時候就採用值傳遞

那麼在python中如何實現和值傳遞和引用傳遞相似的功能呢?———可變型別和不可變型別就發揮作用了。當你需要實現函式引數引用傳遞的功能的時候,你就將型別為list ,dict的變數傳遞給相應的函式。當你需要實現函式值傳遞功能的時候,你就可以將型別為int,float,string,tuple型別的變數傳遞給函式當做引數。

聽說python只允許引用傳遞是為方便記憶體管理,因為python使用的記憶體回收機制是計數器回收,就是每塊記憶體上有一個計數器,表示當前有多少個物件指向該記憶體。每當一個變數不再使用時,就讓該計數器-1,有新物件指向該記憶體時就讓計數器+1,當計時器為0時,就可以收回這塊記憶體了。當然我覺得它肯定不止用了計數器吧,應該還有其他的技術,比如分代回收什麼的。不再討論之列,就這樣了

以上是對第一個問題的理解。參考網址

2、深拷貝和淺拷貝的實現方式以及區別
我們從三個方面來講解這個問題。
2.1 Python中直接賦值
python中的直接賦值相當於傳遞物件的引用而已,原物件改變,被賦值的物件也會改變。下面給大家舉個列子。

>>> print [id(i) for i in love]
[35128144, 34888764, 41771536]
>>> lover=love   # 賦值方式:這裡love和lover物件指向同一個記憶體地址 
>>> print id(lover)
34550848
>>> print [id(i) for i in lover]
[35128144, 34888764, 41771536]
>>> love[0]="You"  # 修改love的第一個元素,那麼lover的第一個元素也會做相應的修改
>>> love
['You', 24, ['am', 'loving', 'you']]
>>> print id(love)
34550848
>>> print [id(i) for i in love]
[41760936, 34888764, 41771536]
>>> print lover    # 看這裡的lover也做了相應的修改,只不過對原始物件中的不可變元素做修改後,既改變了這個物件的值,也改變了這個物件的地址。
['You', 24, ['am', 'loving', 'you']]
>>> love[1]=25
>>> love
['You', 25, ['am', 'loving', 'you']]
>>> print id(love)
34550848
>>> print [id(i) for i in love]
[41760936, 34888752, 41771536]
>>> print id(lover)
34550848
>>> print [id(i) for i in lover]
[41760936, 34888752, 41771536]
love[2].append("too")
>>> love
['You', 25, ['am', 'loving', 'you', 'too']]
>>> lover        # 看這裡的lover也做了相應的修改,只不過對原始物件中的可變元素做修改後,只是改變了這個物件的值,不會改變這個物件的地址。
['You', 25, ['am', 'loving', 'you', 'too']]
>>> print [id(i) for i in love]
[41760936, 34888752, 41771536]
>>> print [id(i) for i in lover]
[41760936, 34888752, 41771536]
>>> 

這裡給大家放一張網上的圖片,以加深理解。
賦值操作的示意圖
2.2 Python中淺拷貝
copy淺拷貝,沒有拷貝子物件(這裡應該指的是可變物件),只是將新物件給了一個新的首地址,裡面的資料和地址都和原始資料相同,所以原始資料改變,子物件會改變,給大家放一張圖就明白了。
python中的淺拷貝示意圖
用程式碼表示出來就是:

>>> import copy
>>> love=["I",24,["am","loving","you"]]
>>> lover=copy.copy(love)     # 這裡是淺拷貝的方法
>>> id(love)
35140672
>>> id(lover)  # 淺拷貝後love和lover這兩個物件的記憶體地址是不同的
41229848
>>> love[0]="You"
>>> id(love)
35140672
>>> id(lover)
41229848
>>> [id(i) for i in love]
[42547536, 35478588, 42558488]      # 在love中改變不可變物件會既會改變不可變物件的值,也會不可變物件的地址
>>> [id(i) for i in lover]
[35717968, 35478588, 42558488]     # 對比可以發現,love和lover中第一個元素的地址發生了變化
>>> love[2].append("too")  
>>> love
['You', 24, ['am', 'loving', 'you', 'too']]
>>> lover
['I', 24, ['am', 'loving', 'you', 'too']]
>>> [id(i) for i in love]       # 在love中改變可變物件的值,只會改變可變物件的值,其地址是不會發生改變的。
[42547536, 35478588, 42558488]
>>> [id(i) for i in lover]
[35717968, 35478588, 42558488]

2.3 Python中深拷貝

python中的深拷貝,包含對原始物件的子物件的深拷貝,所以,對原拷貝物件的改變不會對深拷貝後物件沒有任何影響。下面放一張圖供大家理解。

python中深拷貝的示意圖
這裡舉個例子加深理解:

>>> import copy
>>> love=["I",24,["am","loving","you"]]
>>> lover=copy.deepcopy(love)
>>> id(love)
41050760
>>> id(lover)
41050880     # 二者的首地址不同
>>> [id(x) for x in love]
[6226768, 5987388, 41049184]   
>>> [id(x) for x in lover]
[6226768, 5987388, 41051680]   # 二者的可變子物件的地址也不相同
>>> love[1]=18
>>> love
['I', 18, ['am', 'loving', 'you']]   # 對love中不可變物件的修改不影響lover中對應的子物件
>>> lover
['I', 24, ['am', 'loving', 'you']]
>>> [id(x) for x in love]
[6226768, 5987460, 41049184]
>>> [id(x) for x in lover]
[6226768, 5987388, 41051680]
>>> love[2].append("too")
>>> love
['I', 18, ['am', 'loving', 'you', 'too']]   # 對love中可變物件的修改不會影響到lover物件中對應的子物件
>>> [id(x) for x in love]
[6226768, 5987460, 41049184]
>>> [id(x) for x in lover]
[6226768, 5987388, 41051680]
>>> lover
['I', 24, ['am', 'loving', 'you']]   綜上可以發現,深拷貝的兩個物件,各自改變後,互不影響。

總結:

1、賦值:簡單地拷貝物件的引用,兩個物件的id相同。
2、淺拷貝:建立一個新的組合物件,這個新物件與原物件共享記憶體中的子物件。
3、深拷貝:建立一個新的組合物件,同時遞迴地拷貝所有子物件,新的組合物件與原物件沒有任何關聯。雖然實際上會共享不可變的子物件,但不影響它們的相互獨立性。

淺拷貝和深拷貝的不同僅僅是對組合物件來說,所謂的組合物件就是包含了其它物件的物件,如列表,類例項。而對於數字、字串以及其它“原子”型別,沒有拷貝一說,產生的都是原物件的引用。
以上學習的參考網址:python中的賦值、深拷貝和淺拷貝
python變數和物件的關係,以及賦值、淺拷貝、深拷貝的關係

3、python中的deepcopy如果你來設計,如何實現

4、__new__()與__init__()的區別
new方法是類建立例項的方法, 建立物件時呼叫,返回當前物件的一個例項
init()方法是類例項建立之後呼叫,用於對當前物件的一些初始化,沒有返回值。
new()方法和init()方法所接收的引數是一樣的。
需要注意的幾點:
1、init()函式並不相當於C++或者C#中的建構函式,因為在執行init()函式的時候,例項已經構造出來了
2、子類可以不重寫init()方法,例項化子類時可以自動呼叫超類中已定義的init()方法。但是如果重寫了init(),例項化子類時將不會再 隱式的去呼叫超類中已定義的init()程式碼。
3、如果重寫了init(),為了能使用或擴充套件超類中的行為,最好顯式的呼叫超類的init()方法

下面舉個例子來說明一下:

class smallApple(object):
    def __init__(self,name):
        print "__init__() is called"
        self.name=name

    def __new__(cls,name="smallApple"):
        print "__new__() is called"
        return super(smallApple,cls).__new__(cls,name)
執行輸出:
>>> s=smallApple("It's yours")
__new__() is called
__init__() is called
>>> 

1.s=smallApple(“It’s yours”)
2.首先執行使用name引數來執行smallApplen類的new方法,這個new方法會 返回smallApple類的一個例項(通常情況下是使用 super(smallApple, cls).new(cls, … …) 這樣的方式),
3.然後利用這個例項來呼叫類的init方法,上一步裡面new產生的例項也就是 init裡面的的 self
所以,initnew 最主要的區別在於:
1.init 通常用於初始化一個新例項,控制這個初始化的過程,比如新增一些屬性, 做一些額外的操作,發生在類例項被建立完以後。它是例項級別的方法。
2.new 通常用於控制生成一個新例項的過程。它是類級別的方法

因為類每一次例項化後產生的過程都是通過new來控制的,所以通過過載new方法,我們 可以很簡單的實現單例模。那我們利用new()方法來實現單例模式的一種:

# -*- coding: cp936 -*-
class smallApple(object):
    def __init__(self,name="It's yours"):
        print "__init__() is called"
        self.name=name

    def __new__(cls,name="smallApple"):
        print "__new__() is called"
        # make sure the instance is unique
        if not hasattr(cls,"instance"):
              cls.instance= super(smallApple,cls).__new__(cls,name)
        return cls.instance
# 測試
>>> obj1=smallApple("i am a")
__new__() is called
__init__() is called
>>> obj2=smallApple("i am a")
__new__() is called
__init__() is called
>>> obj1 is obj2
True
>>> obj1.age=22
>>> obj2.age
22
>>> smallApple.instance
<__main__.smallApple object at 0x027D3BF0>
>>> hex(id(obj1))
'0x27d3bf0'
>>> hex(id(obj2))
'0x27d3bf0'
>>> 

其實,大多數時候是不需要寫new()函式的,那麼什麼情況下需要寫new()函式呢?

當繼承不可變類或者metaclass的時候,需要用到new()方法。
下面來舉個例子來說明,當實現int型別恆為正數的類 ,是因為對於int這種 不可變的物件,我們只有過載它的new方法才能起到自定義的作用。

class PositiveInt(int):
     def __new__(cls,value):
         return super(PositiveInt,cls).__new__(cls,abs(value))
# 輸出
>>> PositiveInt(-16)
16

5、你知道的幾種設計模式
一、單例模式
二、簡單工廠模式
三、抽象工廠模式
四、建造者模式
下面依次舉例這四種設計模式:
1、單例模式
單例模式(Singleton Pattern)是一種常見的設計模式,該模式的目的是確保一個類只有一個例項.
在Python中可以用多種方式來實現單例模式:

  1. 使用python的模組
  2. 使用python的new()方法
  3. 使用python的裝飾器(decorator)
  4. 使用python的元類(metaclass)

1.1 使用python的模組
python的模組其實就是天然的單例模式,因為python的模組在第一次載入的時,執行模組程式碼,生成.pyc檔案,當再次需要匯入這個模組的時候,python會再次載入第一次生成的.pyc檔案,而不會執行模組的程式碼。所以,我們只需要將相關的函式和資料放在一個模組中,就可以實現單例模式了,下面是具體的做法。

# 下面是類的定義
class Singleton(object):
    def Unique(self):
        print "I am the Unique Methold"

MySingleton=Singleton()
# 下面是匯入模組後的執行結果
>>> from pattern import MySingleton
>>> MySingleton.Unique()
I am the Unique Methold

以上是匯入模組成功的方式,下面給出一個錯誤的方式。

# 以下是類定義
class Singleton(object):
    def Unique(): # 這裡類定義有誤,引數沒有加self
        print "I am the Unique Methold"

MySingleton=Singleton()
# 以下是模組的匯入的程式碼
>>> from pattern import MySingleton
>>> MySingleton.Unique()   

Traceback (most recent call last):
  File "<pyshell#1>", line 1, in <module>
    MySingleton.Unique()
TypeError: Unique() takes no arguments (1 given)   # 因為類定義中的程式碼有誤,所以模組匯入的時候會出現錯誤,那我們把pattern模組中的類定義程式碼修改過來後,再次import時

>>> from pattern import MySingleton
>>> MySingleton.Unique()

Traceback (most recent call last):
  File "<pyshell#3>", line 1, in <module>
    MySingleton.Unique()
TypeError: Unique() takes no arguments (1 given)
>>> 
# 還是不可以,這說明,不管import多少次模組,只要shell不關閉,import的模組都是第一次import時生成的.pyc檔案

1.2 使用python的new()方法
為了使類只出現一個例項,那麼我們可以控制例項的建立過程來達到這個目的、下面看程式碼:

 # l類的定義
class SingletonTwo(object):

    def __new__(cls,name="you"):
        if not hasattr(cls,"instance"):
            cls.instance=super(SingletonTwo,cls).__new__(cls,name)
        return cls.instance

    def __init__(self,name="you"):
        print "get name already"
        self.name=name

# 模組測試
>>> obj1=SingletonTwo("11111")
get name already
>>> obj1.name
'11111'
>>> obj2=SingletonTwo("222222")
get name already
>>> obj2.name
'222222'
>>> obj1==obj2
True
>>> hex(id(obj1))
'0x290f9d0'
>>> hex(id(obj2))
'0x290f9d0'
>>> obj1.name="gwijgoir"
>>> obj2.name
'gwijgoir'
 # 這樣保證了這個類只有一個例項,從程式碼裡面可以看到例項obj1和obj2的地址相同,但是它們的name在最開始卻不是相同的,但是修改obj1的那麼後,obj2的name也會變跟obj1一樣,所以這個地方我會再探究一下、、、、、、

1.3 python中的裝飾器(decorater)來實現單例模式
未完待續、、、、、、

1.4 python中的元類(metaclass)來實現單例模式
未完待續、、、、、