參考https://github.com/taizilongxu/interview_python#27-readreadline%E5%92%8Creadlines
1 Python的函數(shù)參數(shù)傳遞
看兩個例子
a = 1
def fun(a):
a = 2
fun(a)
print a # 1
a = []
def fun(a):
a.append(1)
fun(a)
print a # [1]
所有的變量都可以理解是內(nèi)存中一個對象的“引用”
通過id來看引用a的內(nèi)存地址可以比較理解:
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
可以看到溯乒,在執(zhí)行完a = 2之后,a引用中保存的值产舞,即內(nèi)存地址發(fā)生變化,由原來1對象的所在的地址變成了2這個實體對象的內(nèi)存地址灿里。
而第2個例子a引用保存的內(nèi)存值就不會發(fā)生變化:
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 等則是可以修改的對象吸祟。
當(dāng)一個引用傳遞給函數(shù)的時候,函數(shù)自動復(fù)制一份引用,這個函數(shù)里的引用和外邊的引用沒有半毛關(guān)系了.所以第一個例子里函數(shù)把引用指向了一個不可變對象,當(dāng)函數(shù)返回的時候,外面的引用沒半毛感覺.而第二個例子就不一樣了,函數(shù)內(nèi)的引用指向的是可變對象,對它的操作就和定位了指針地址一樣,在內(nèi)存里進(jìn)行修改.
【注】第二個函數(shù)內(nèi)部調(diào)用的是可變對象的方法瑟慈,如果改為賦值語句的話,就會等同于第一個函數(shù)效果屋匕,具體如下:
a = []
def fun(a):
print("func_in",id(a)) # func_in 53629256
a = [1, 2, 3]
print("func_in", id(a)) # func_in 53629300
print("func_out",id(a)) # func_out 53629256
fun(a)
print(a) # []
當(dāng)然葛碧,很容易想到如果傳入不可變類型的字符串形參,然后在函數(shù)值調(diào)用字符串方法修改字符串呢炒瘟?放心吹埠,Python已經(jīng)幫你想好了阻止這種現(xiàn)象的出現(xiàn)而違反這種約定的方法第步,因為字符串的方法必須重新賦值才對原有串起到修改的作用疮装,如下,重新賦值就等同于上述函數(shù)一了粘都,所以Python函數(shù)參數(shù)傳遞嚴(yán)格遵循引用規(guī)則廓推。
a = "abc"
def fun(a):
print("func_in",id(a)) # func_in 53629256
a.replace("b", "d") # a = a.replace("b", "d")
print("func_in", id(a)) # func_in 53629256
print("func_out",id(a)) # func_out 53629256
fun(a)
print(a) # abc
2 @staticmethod和@classmethod
Python其實有3個方法,即靜態(tài)方法(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()
這里先理解下函數(shù)參數(shù)里面的self和cls.這個self和cls是對類或者實例的綁定,對于一般的函數(shù)來說我們可以這么調(diào)用foo(x),這個函數(shù)就是最常用的,它的工作跟任何東西(類,實例)無關(guān).
對于實例方法,我們知道在類里每次定義方法的時候都需要綁定這個實例,就是foo(self, x),為什么要這么做呢?因為實例方法的調(diào)用離不開實例,我們需要把實例自己傳給函數(shù),調(diào)用的時候是這樣的a.foo(x)(其實是foo(a, x)).類方法一樣,只不過它傳遞的是類而不是實例,A.class_foo(x)(其實是foo(A, x)).注意這里的self和cls可以替換別的參數(shù),但是python的約定是這倆,還是不要改的好.
對于靜態(tài)方法其實和普通的方法一樣,不需要對誰進(jìn)行綁定,唯一的區(qū)別是調(diào)用的時候需要使用a.static_foo(x)或者A.static_foo(x)來調(diào)用.
\ | 實例方法 | 類方法 | 靜態(tài)方法 |
---|---|---|---|
a = A() | a.foo(x) | a.class_foo(x) | a.static_foo(x) |
A | 不可用 | A.class_foo(x) | A.static_foo(x) |
3 類變量和實例變量
類變量
是可在類的所有實例之間共享的值(也就是說,它們不是單獨分配給每個實例的)翩隧。例如下例中樊展,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
【補(bǔ)充】
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對象為自己創(chuàng)建了一個實例變量淑仆,之后p1.name中的name變量便不再是類屬性涝婉,而是p1對象自己的實例屬性,這種不通過init函數(shù)初始化實例屬性的方法是不好的蔗怠,一般慎用墩弯!
再看下面的例子:
class Person:
name=[]
p1=Person()
p2=Person()
p1.name.append(1)
print p1.name # [1]
print p2.name # [1]
print Person.name # [1]
類屬性為可變變量,對象.類屬性.方法()可以直接修改類屬性(慎用)寞射,這就回歸了我們問題一的函數(shù)參數(shù)傳遞渔工,一般修改類屬性還是類名.類屬性修改比較好。
4 Python自省
這個也是python彪悍的特性.
自省就是面向?qū)ο蟮恼Z言所寫的程序在運行時,所能知道對象的類型.簡單一句就是運行時能夠獲得對象的類型.比如:
type()---變量類型
dir()---返回對象的屬性列表
getattr()---返回一個對象屬性值(getattr(對象, 屬性))
hasattr()---判斷對象是否包含某種方法(hasattr(list, 'append'))
isinstance()---判斷一個對象是否是一個已知的類型(isinstance (a,int))
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
5 列表推導(dǎo)式和字典推導(dǎo)式
# 列表推導(dǎo)式
l= [squared(i) for i in range(30) if i % 3 is 0]
# 字典推導(dǎo)式
d = {key: value for (key, value) in iterable}
6 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內(nèi)部的名字,用來區(qū)別其他用戶自定義的命名,以防沖突桥温,就是例如__init__()
,__del__()
,__call__()
這些特殊方法
_foo
:一種約定,用來指定變量私有.程序員用來指定私有變量的一種方式.不能用from module import * 導(dǎo)入引矩,其他方面和公有一樣訪問;
__foo
:這個有真正的意義:解析器用_classname__foo
來代替這個名字,以區(qū)別和其他類相同的命名,它無法直接像公有成員一樣隨便訪問,通過對象名._類名__xxx
這樣的方式可以訪問.
7 字符串格式化:%和.format
.format在許多方面看起來更便利,對于%最煩人的是它無法同時傳遞一個變量和元組.你可能會想下面的代碼不會有什么問題:
"hi there %s" % name
但是,如果name恰好是(1,2,3),它將會拋出一個TypeError異常.為了保證它總是正確的,你必須這樣做:
"hi there %s" % (name,) # 提供一個單元素的數(shù)組而不是一個參數(shù)
但是有點丑脓魏。.format就沒有這些問題兰吟。
"hi there {}".format(name)
8 迭代器和生成器
這里有個關(guān)于生成器的創(chuàng)建問題面試官有考: 問: 將列表生成式中[]改成() 之后數(shù)據(jù)結(jié)構(gòu)是否改變? 答案:是茂翔,從列表變?yōu)樯善?/p>
>>> 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>
通過列表生成式混蔼,可以直接創(chuàng)建一個列表,將所有的值一次性加載到內(nèi)存中珊燎。但如果創(chuàng)建一個包含百萬元素的列表惭嚣,會占用很大的內(nèi)存空間,如:我們只需要訪問前面的幾個元素悔政,后面大部分元素所占的空間都是浪費的晚吞。因此,沒有必要創(chuàng)建完整的列表(節(jié)省大量內(nèi)存空間)谋国。
在Python中槽地,我們可以采用生成器:邊循環(huán),邊計算的機(jī)制—>generator芦瘾,生成器是將列表生成式的[]改成(),不會將所有的值一次性加載到內(nèi)存中,延遲計算,一次返回一個結(jié)果,它不會一次生成所有的結(jié)果,這對大數(shù)據(jù)量處理,非常有用捌蚊。如:
sum(x for x in range(10000000000))
sum([x for x in range(10000000000)])
第一個幾乎沒什么內(nèi)存占用,第二個內(nèi)存占有很多。
yield實現(xiàn)簡單的生成器
一般函數(shù)生成斐波那契數(shù)列
def create_num(n):
a, b = 0, 1
current_num = 0
while current_num < n:
print(a)
a, b = b, a + b
current_num += 1
create_num(5)
# 0
# 1
# 1
# 2
# 3
上述函數(shù)將數(shù)列直接打印近弟,但是我們需要將這個數(shù)列變?yōu)橐粋€可迭代對象缅糟,能夠用for循環(huán)去遍歷(當(dāng)然我們可以將上述函數(shù)進(jìn)行改造,使其生成一個列表祷愉,但如果是生成斐波那契數(shù)列前一百萬項窗宦,那么這個列表將占用很大內(nèi)存),可以使用yield將函數(shù)改為生成器二鳄,這樣需要的時候再計算赴涵,不會占用很大空間,實現(xiàn)很簡單订讼,只需將create_num函數(shù)中的print語句改為yield語句即可使create_num函數(shù)變?yōu)橐粋€生成器髓窜,代碼如下:
def create_num(n):
a, b = 0, 1
current_num = 0
while current_num < n:
# print(a)
yield a # 如果一個函數(shù)中有yield,那么這個函數(shù)就不再是函數(shù)躯嫉,而是一個生成器模板
a, b = b, a + b
current_num += 1
# 創(chuàng)建一個生成器對象
obj = create_num(5)
for num in obj:
print(num)
# 0
# 1
# 1
# 2
# 3
9 *args 和 **kwargs
用args和kwargs只是為了方便并沒有強(qiáng)制使用它們.
當(dāng)你不確定你的函數(shù)里將要傳遞多少參數(shù)時你可以用args.例如,它可以傳遞任意數(shù)量的參數(shù):
>>> 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允許你使用沒有事先定義的參數(shù)名:
>>> 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
你也可以混著用.命名參數(shù)首先獲得參數(shù)值然后所有的其他參數(shù)都傳遞給args和*kwargs.命名參數(shù)在列表的最前端.例如:
def table_things(titlestring, **kwargs)
args和kwargs可以同時在函數(shù)的定義中,但是args必須在kwargs前面.
當(dāng)調(diào)用函數(shù)時你也可以用和*語法.例如:
>>> 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
就像你看到的一樣,它可以傳遞列表(或者元組)的每一項并把它們解包.注意必須與它們在函數(shù)里的參數(shù)相吻合.當(dāng)然,你也可以在函數(shù)定義或者函數(shù)調(diào)用時用*.
當(dāng)理解了這些纱烘,試試下面:
def print_three_things(name, *args, **kwargs):
print(name)
for elem in args:
print(elem, end=" ")
print()
for k, v in kwargs.items():
print('{0} = {1}'.format(k, v), end=",")
print()
name = "xiaoming"
l = ['1', '2', '3']
d = {"age": 18, "height": "183cm"}
print_three_things(name, *l, **d)
# xiaoming
# 1 2 3
# age = 18,height = 183cm,
10 面向切面編程AOP和裝飾器
AOP
簡言之、這種在運行時祈餐,編譯時擂啥,類和方法加載時,動態(tài)地將代碼切入到類的指定方法帆阳、指定位置上的編程思想就是面向切面的編程哺壶。
我們管切入到指定類指定方法的代碼片段稱為切面屋吨,而切入到哪些類、哪些方法則叫切入點山宾。有了AOP至扰,我們就可以把幾個類共有的代碼,抽取到一個切片中资锰,等到需要時再切入對象中去敢课,從而改變其原有的行為。
優(yōu)點是:這樣的做法绷杜,對原有代碼毫無入侵性
裝飾器
裝飾器是一個很著名的設(shè)計模式直秆,經(jīng)常被用于有切面需求的場景,較為經(jīng)典的有插入日志鞭盟、性能測試圾结、事務(wù)處理等。裝飾器是解決這類問題的絕佳設(shè)計齿诉,有了裝飾器筝野,我們就可以抽離出大量函數(shù)中與函數(shù)功能本身無關(guān)的雷同代碼并繼續(xù)重用。
概括的講粤剧,裝飾器的作用就是為已經(jīng)存在的對象添加額外的功能
函數(shù)就是對象.因此,對象:
- 可以賦值給一個變量
- 可以在其他函數(shù)里定義
- 函數(shù)可以返回另一個函數(shù)
- 函數(shù)作為參數(shù)傳遞
簡單裝飾器
def decorator(fun):
def wrapper():
print("方法執(zhí)行前")
fun()
print("方法執(zhí)行后")
return wrapper
@decorator
# 相當(dāng)于my_fun = decorator(my_fun)
def my_fun():
print("需要裝飾的函數(shù)")
# 執(zhí)行
my_fun()
# 輸出:
# 方法執(zhí)行前
# 需要裝飾的函數(shù)
# 方法執(zhí)行后
在裝飾器函數(shù)里傳入?yún)?shù)
def decorator(fun):
print("裝飾器內(nèi)的語句") # 此處會在裝飾的時候執(zhí)行
def wrapper(arg1, arg2):
print("接收到兩個參數(shù)", arg1, arg2)
fun(arg1, arg2)
return wrapper
@decorator
def my_fun(first_name, last_name):
print("我的名字是", first_name, last_name)
# 輸出:
# 裝飾器內(nèi)的語句
my_fun("Peter", "Venkman")
# 輸出:
# 接收到兩個參數(shù) Peter Venkman
# 我的名字是 Peter Venkman
把參數(shù)傳給裝飾器
def decorator_out(decorator_arg1, decorator_arg2):
print("我是外層裝飾器歇竟,接收參數(shù)", decorator_arg1, decorator_arg2)
def decorator_in(fun):
print("我是內(nèi)層裝飾器,接收到外層裝飾器參數(shù)", decorator_arg1, decorator_arg2)
def wrapper(function_arg1, function_arg2) :
print("我是wrapper俊扳,可以接收所有參數(shù)途蒋,裝飾器參數(shù)有{0} {1}猛遍,被裝飾函數(shù)參數(shù)有{2} {3}" .format(decorator_arg1, decorator_arg2, function_arg1, function_arg2))
return fun(function_arg1, function_arg2)
return wrapper
return decorator_in
@decorator_out("Leonard", "Sheldon")
# 相當(dāng)于兩步
# temp = decorator_out("Leonard", "Sheldon")
# my_fun = temp(my_fun)
def my_fun(function_arg1, function_arg2):
print("我是被裝飾的函數(shù)馋记,只知道自己的參數(shù): {0} {1}".format(function_arg1, function_arg2))
# 輸出:
# 我是外層裝飾器,接收參數(shù) Leonard Sheldon
# 我是內(nèi)層裝飾器懊烤,接收到外層裝飾器參數(shù) Leonard Sheldon
my_fun("Rajesh", "Howard")
#輸出:
#我是wrapper梯醒,可以接收所有參數(shù),裝飾器參數(shù)有Leonard Sheldon腌紧,被裝飾函數(shù)參數(shù)有Rajesh Howard
# 我是被裝飾的函數(shù)茸习,只知道自己的參數(shù): Rajesh Howard
裝飾器的知識點
- 裝飾器使函數(shù)調(diào)用變慢了.一定要記住.
- 裝飾器不能被取消(有些人把裝飾器做成可以移除的但是沒有人會用)所以一旦一個函數(shù)被裝飾了.所有的代碼都會被裝飾.
- Python自身提供了幾個裝飾器,像property, staticmethod.
- Django用裝飾器管理緩存和視圖的權(quán)限.
11 Python中重載
函數(shù)重載主要是為了解決兩個問題。
- 可變參數(shù)類型壁肋。
- 可變參數(shù)個數(shù)号胚。
另外,一個基本的設(shè)計原則是浸遗,僅僅當(dāng)兩個函數(shù)除了參數(shù)類型和參數(shù)個數(shù)不同以外猫胁,其功能是完全相同的,此時才使用函數(shù)重載跛锌,如果兩個函數(shù)的功能其實不同弃秆,那么不應(yīng)當(dāng)使用重載,而應(yīng)當(dāng)使用一個名字不同的函數(shù)。
好吧菠赚,那么對于情況 1 脑豹,函數(shù)功能相同,但是參數(shù)類型不同衡查,python 如何處理瘩欺?答案是根本不需要處理,因為 python 可以接受任何類型的參數(shù)拌牲,如果函數(shù)的功能相同击碗,那么不同的參數(shù)類型在 python 中很可能是相同的代碼,沒有必要做成兩個不同函數(shù)们拙。
那么對于情況 2 稍途,函數(shù)功能相同,但參數(shù)個數(shù)不同砚婆,python 如何處理械拍?大家知道,答案就是缺省參數(shù)装盯。對那些缺少的參數(shù)設(shè)定為缺省參數(shù)即可解決問題坷虑。因為你假設(shè)函數(shù)功能相同,那么那些缺少的參數(shù)終歸是需要用的埂奈。
好了迄损,鑒于情況 1 跟 情況 2 都有了解決方案,python 自然就不需要函數(shù)重載了账磺。
12 新式類和舊式類
新式類很早在2.2就出現(xiàn)了,所以舊式類完全是兼容的問題,Python3里的類全部都是新式類.這里有一個MRO(Method Resolution Order芹敌, 方法解析順序)問題可以了解下(新式類繼承是根據(jù)C3算法,舊式類是深度優(yōu)先),<Python核心編程>里講的也很多.
- 一個舊式類的深度優(yōu)先的例子
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
按照經(jīng)典類的查找順序從左到右深度優(yōu)先的規(guī)則,在訪問d.foo1()的時候,D這個類是沒有的..那么往上查找,先找到B,里面沒有,深度優(yōu)先,訪問A,找到了foo1(),所以這時候調(diào)用的是A的foo1()垮抗,從而導(dǎo)致C重寫的foo1()被繞過.
而這個問題在新式類中得到修復(fù)氏捞,新的對象模型采用的是從左到右,廣度優(yōu)先的方式進(jìn)行查找冒版,在訪問d.foo1()的時候,D這個類是沒有的..那么往上查找,先找到B,里面沒有,廣度優(yōu)先,訪問C,找到了foo1(),所以這時候調(diào)用的是C的foo1()液茎,解決了C重寫的foo1()被繞過的問題.
13 __new__
和__init__
的區(qū)別
這個new確實很少見到,先做了解吧.
-
__new__
是一個靜態(tài)方法,而__init__
是一個實例方法. -
__new__
方法會返回一個創(chuàng)建的實例,而__init__
什么都不返回. - 只有在
__new__
返回一個cls的實例時后面的__init__
才能被調(diào)用. - 當(dāng)創(chuàng)建一個新實例時調(diào)用
__new__
,初始化一個實例時用__init__
.
【注】__metaclass__
是創(chuàng)建類時起作用.所以我們可以分別使用__metaclass__
,__new__
和__init__
來分別在類創(chuàng)建,實例創(chuàng)建和實例初始化的時候做一些小手腳.
14 單例模式
單例模式是一種常用的軟件設(shè)計模式。在它的核心結(jié)構(gòu)中只包含一個被稱為單例類的特殊類辞嗡。通過單例模式可以保證系統(tǒng)中一個類只有一個實例而且該實例易于外界訪問捆等,從而方便對實例個數(shù)的控制并節(jié)約系統(tǒng)資源。如果希望在系統(tǒng)中某個類的對象只能存在一個续室,單例模式是最好的解決方案栋烤。
new()在init()之前被調(diào)用,用于生成實例對象猎贴。利用這個方法和類的屬性的特點可以實現(xiàn)設(shè)計模式的單例模式班缎。單例模式是指創(chuàng)建唯一對象蝴光,單例模式設(shè)計的類只能實例一個對象 這個絕對常考啊.絕對要記住1~2個方法,當(dāng)時面試官是讓手寫的.
使用__new__
方法
class Singleton(object):
_instance = None
def __new__(cls, *args, **kw):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
singleton = Singleton()
共享屬性
創(chuàng)建實例時把所有實例的__dict__
指向同一個字典,這樣它們具有相同的屬性和方法.
class Borg(object):
_state = {}
def __new__(cls, *args, **kw):
ob = super().__new__(cls)
ob.__dict__ = cls._state
return ob
borg = Borg()
裝飾器版本
def singleton(cls):
instance = None
def getinstance(*args, **kw):
nonlocal instance # 修改外部裝飾器函數(shù)中變量instance达址,注意不能使用global
if instance is None:
instance = cls(*args, **kw)
return instance
return getinstance
@singleton
class MyClass:
...
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()
15 Python中的作用域
Python 中蔑祟,一個變量的作用域總是由在代碼中被賦值的地方所決定的。
當(dāng) Python 遇到一個變量的話他會按照這樣的順序進(jìn)行搜索:
本地作用域(Local)→當(dāng)前作用域被嵌入的本地作用域(Enclosing locals)→全局/模塊作用域(Global)→內(nèi)置作用域(Built-in)
16 GIL(Global Interpreter Lock)全局解釋器鎖
- Python語言和GIL沒有半毛錢關(guān)系沉唠,僅僅是因為歷史原因在Cpython解釋器(C語言編寫的Python解釋器)中疆虚,難以移除GIL
- 每個線程在執(zhí)行過程中都需要先獲取GIL,保證同一時刻只有一個線程可以執(zhí)行代碼
- 對于io密集型任務(wù)满葛,python的多線程起到作用径簿,因為遇到IO阻塞會自動釋放GIL鎖,但對于cpu(計算)密集型任務(wù)嘀韧,python的多線程幾乎占不到任何優(yōu)勢篇亭,還有可能因為爭奪資源而變慢
- Python使用多進(jìn)程可以利用多核CPU資源
- 多線程爬取比單線程性能有所提升,因為遇到IO阻塞會自動釋放GIL鎖
- 解決辦法(多線程利用多核CPU資源):
(1)更換解釋器锄贷,如Jpython(JAVA語言編寫的Python解釋器)
(2)在使用多線程的地方使用其他語言編寫程序
【注】Python中译蒂,對于io密集型任務(wù),使用線程或協(xié)程解決谊却,但對于cpu(計算)密集型任務(wù)柔昼,使用進(jìn)程解決
17 進(jìn)程、線程炎辨、協(xié)程
- 進(jìn)程是資源分配的單位
- 線程是操作系統(tǒng)調(diào)度的單位
- 進(jìn)程切換需要的資源很大捕透,效率很低
- 線程切換需要的資源一般,效率一般(不考慮GIL)
- 協(xié)程切換任務(wù)資源很小碴萧,效率高
- 多進(jìn)程乙嘀,多線程根據(jù)CPU核數(shù)不一樣可能是并行的,但協(xié)程是在一個線程中勿决,所以一定是并發(fā)的
簡單來說協(xié)程是進(jìn)程和線程的升級版乒躺,進(jìn)程和線程都面臨著內(nèi)核態(tài)和用戶態(tài)的切換問題而耗費許多切換時間招盲,而協(xié)程就是用戶自己控制切換的時機(jī)低缩,通過在線程中實現(xiàn)調(diào)度,避免了陷入內(nèi)核級別的上下文切換造成的性能損失曹货,進(jìn)而突破了線程在IO上的性能瓶頸咆繁。
Python里最常見的yield就是協(xié)程的思想!
18 lambda匿名函數(shù)
lambda函數(shù)不需要專門的名字來說明,通常來完成簡單的功能顶籽。
lambda x, y: x*y # 函數(shù)輸入是x和y玩般,返回它們的積x*y
19 閉包
匿名函數(shù)、函數(shù)礼饱、閉包坏为、對象當(dāng)做實參時究驴,有什么區(qū)別?
- 匿名函數(shù)能夠完成基本的簡單功能匀伏,傳遞的是這個函數(shù)的引用(只有功能)
- 函數(shù)能夠完成較為復(fù)雜的功能洒忧,傳遞的是這個函數(shù)的引用(只有功能)
- 閉包能夠完成較為復(fù)雜的功能,傳遞的是這個閉包中的函數(shù)以及數(shù)據(jù)(功能+數(shù)據(jù))够颠,相比對象占用極少空間
- 對象封裝較為復(fù)雜的數(shù)據(jù)和功能熙侍,傳遞很多數(shù)據(jù)和很多功能(功能+數(shù)據(jù))
當(dāng)一個內(nèi)嵌函數(shù)引用其外部作作用域的變量,我們就會得到一個閉包. 總結(jié)一下,創(chuàng)建一個閉包必須滿足以下幾點:
- 必須有一個內(nèi)嵌函數(shù)
- 內(nèi)嵌函數(shù)必須引用外部函數(shù)中的變量
- 外部函數(shù)的返回值必須是內(nèi)嵌函數(shù)
def line(k, b):
def create_y(x):
print(k * x + b)
return create_y
# 第一條線 y = x + 2
line1 = line(1, 2) # k = 1, b = 2會被保存在line1這個函數(shù)引用中,只需要賦值一次
line1(0)
line1(1)
line1(2)
# 第二條線 y = 2x + 3
line2 = line(2, 3)
line2(0)
line2(1)
line2(2)
【注】如果閉包外層函數(shù)定義了臨時變量履磨,要在內(nèi)層函數(shù)中進(jìn)行修改蛉抓,需用nonlocal關(guān)鍵字,如下:
def test1():
a = 1
def test2():
nonlocal a
print(a)
a = 2
print(a)
return test2
test1()()
# 1
# 2
如果去掉上述代碼中nonlocal a語句剃诅,那么程序在print(a)處就會報錯巷送,因為解釋器會把內(nèi)層test2函數(shù)中a = 2看成是定義的一個局部變量屯远,而在定義局部變量a前打印其值會報變量未定義的錯誤售滤,此處需留意,如問題14中使用裝飾器實現(xiàn)單例模式就用到此技巧防楷。
20 Python函數(shù)式編程
filter 函數(shù)
filter 函數(shù)的功能相當(dāng)于過濾器如筛。調(diào)用一個布爾函數(shù)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函數(shù)
map函數(shù)是對一個序列的每個項依次執(zhí)行函數(shù)杨刨,下面是對一個序列每個項都乘以2:
>>> a = map(lambda x:x*2,[1,2,3])
>>> list(a)
[2, 4, 6]
reduce函數(shù)
reduce函數(shù)是對一個序列的每個項迭代調(diào)用函數(shù)晤柄,下面是求3的階乘:
>>> reduce(lambda x,y:x*y,range(1,4))
6
21 Python里的拷貝
引用、copy()妖胀、deepcopy()的區(qū)別
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']數(shù)組對象
結(jié)果:
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']]
上述語句可理解為下圖:
- b = a爬坑,賦值,將b直接指向a指向的地址
- c = copy.copy(a)涂臣,淺拷貝盾计,重新指向一塊地址,只拷貝a頂層對象赁遗,對子對象不拷貝署辉,繼續(xù)指向原來的引用
- d = copy.deepcopy(a),深拷貝岩四,重新指向一塊地址哭尝,將a無論是頂層對象還是子對象都復(fù)制一份
【注】對于不可變類型,如:
import copy
a = (1, 2, (3, 4))
b = a
c = copy.copy(a)
d = copy.deepcopy(a)
id(a), id(b), id(c), id(d)相同剖煌,都是直接傳遞引用材鹦,因為a為不可變類型逝淹,并且子對象都是不可變類型
而對于:
import copy
a = (1, 2, [3, 4])
b = a
c = copy.copy(a)
d = copy.deepcopy(a)
id(a), id(b), id(c)相同,都是直接傳遞引用桶唐,id(d)不同创橄,因為雖然a為不可變類型,但子對象中有可變類型莽红,深拷貝會自動檢查子對象中是否有可變類型妥畏,若有,則重新指向一塊地址安吁,將所有對象復(fù)制一份
22 Python內(nèi)存管理機(jī)制
(1)Python的存儲問題
- 由于python中萬物皆對象醉蚁,所以python的存儲問題是對象的存儲問題,并且對于每個對象鬼店,python會分配一塊內(nèi)存空間去存儲它网棍。在定義一個變量時,變量指向這一對象妇智,其實該變量只是該對象的一個引用滥玷,保存該對象地址
- 對于整數(shù)和短小的字符等,python會執(zhí)行緩存機(jī)制巍棱,即將這些對象進(jìn)行緩存惑畴,不會為相同的對象分配多個內(nèi)存空間
- 容器對象,如列表航徙、元組如贷、字典等,存儲的其他對象到踏,僅僅是其他對象的引用杠袱,即地址,并不是這些對象本身
(2)引用計數(shù)
當(dāng)一個對象有新的引用時窝稿,它的引用計數(shù)就會增加楣富,當(dāng)引用它的對象被刪除,它的引用計數(shù)就會減少伴榔,引用計數(shù)為0時纹蝴,該對象生命就結(jié)束了。
(3)垃圾回收
- 當(dāng)內(nèi)存中有不再使用的部分時潮梯,垃圾收集器就會把他們清理掉骗灶。它會去檢查那些引用計數(shù)為0的對象,然后清除其在內(nèi)存的空間秉馏。
- 垃圾回收機(jī)制還有一個循環(huán)垃圾回收器, 確保釋放循環(huán)引用對象(a引用b, b引用a, 導(dǎo)致其引用計數(shù)永遠(yuǎn)不為0)。
(4)分代技術(shù)
- Python同時采用了分代(generation)回收的策略脱羡。這一策略的基本假設(shè)是萝究,存活時間越久的對象免都,越不可能在后面的程序中變成垃圾。我們的程序往往會產(chǎn)生大量的對象帆竹,許多對象很快產(chǎn)生和消失绕娘,但也有一些對象長期被使用。出于信任和效率栽连,對于這樣一些“長壽”對象险领,我們相信它們的用處,所以減少在垃圾回收中掃描它們的頻率秒紧。
- Python將所有的對象分為0绢陌,1,2三代熔恢。所有的新建對象都是0代對象脐湾。當(dāng)某一代對象經(jīng)歷過垃圾回收,依然存活叙淌,那么它就被歸入下一代對象秤掌。垃圾回收啟動時,一定會掃描所有的0代對象鹰霍。如果0代經(jīng)過一定次數(shù)垃圾回收闻鉴,那么就啟動對0代和1代的掃描清理。當(dāng)1代也經(jīng)歷了一定次數(shù)的垃圾回收后茂洒,那么會啟動對0椒拗,1,2获黔,即對所有對象進(jìn)行掃描蚀苛。
23 read,readline和readlines
- read():一次性讀取整個文件內(nèi)容。推薦使用read(size)方法玷氏,size越大運行時間越長
- readline():每次讀取一行內(nèi)容堵未。內(nèi)存不夠時使用,一般不太用
- readlines():一次性讀取整個文件內(nèi)容盏触,并按行返回到list渗蟹,方便我們遍歷
24 Python2和3的區(qū)別
(1)Unicode編碼
- Python2默認(rèn)ASCII編碼方式,但是ASCII編碼無法對中文等字符進(jìn)行有效編碼
- Python3默認(rèn)的編碼方式是UTF-8
(2)print
print語句沒有了赞辩,取而代之的是print()函數(shù)
(3)xrange
- 在 Python2中有xrange()和range()雌芽,前者是創(chuàng)建一個生成器,后者則是直接生成一個列表
- 在 Python3中辨嗽,只有range()世落,功能就是Python2中xrange() 那樣創(chuàng)建一個生成器,不再存在xrange()函數(shù)
(4)數(shù)據(jù)類型
Python3去除了long類型糟需,現(xiàn)在只有一種整型——int屉佳,但它的行為就像Python2中的long
25 常用正則表達(dá)式(匹配郵箱)
舉例:zhang_san-001@gmail.com
(1)分析郵件名稱部分
- 26個大小寫英文字母表示為
a-zA-Z
- 數(shù)字表示為
0-9
- 下劃線表示為
_
- 中劃線表示為
-
- 由于名稱是由若干個字母谷朝、數(shù)字、下劃線和中劃線組成武花,所以需要用到+表示多次出現(xiàn)
根據(jù)以上條件得出郵件名稱表達(dá)式:[a-zA-Z0-9_-]+
(2)分析域名部分
一般域名的規(guī)律為“N級域名.三級域名.二級域名.頂級域名”圆凰,比如qq.com
、www.qq.com
体箕、mp.weixin.qq.com
专钉、12-34.com.cn
,分析可得域名類似** .** .** .**
組成累铅。
-
**
部分可以表示為[a-zA-Z0-9_-]+
-
.**
部分可以表示為\.[a-zA-Z0-9_-]+
- 多個
.**
可以表示為(\.[a-zA-Z0-9_-]+)+
綜上所述跃须,域名部分可以表示為[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+
(3)最終表達(dá)式:
由于郵箱的基本格式為“名稱@域名”,需要使用^
匹配郵箱的開始部分争群,用$
匹配郵箱結(jié)束部分以保證郵箱前后不能有其他字符回怜,所以最終郵箱的正則表達(dá)式為:^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$