前幾天和一個小伙子聊天時,發現了一個問題,他從零開始學Python,學完串列、字典和函式等基礎之后,就開始往爬蟲方向進階學習,結果又花了一個多月的時間,啥也沒學成,反而寸步難行,

其實這個問題的主要原因還是在于Python入門知識沒學好,我所說的Python入門知識指的不僅僅是串列等基礎知識,還包括面向物件和行程執行緒等高級編程內容,
這可能是初學者不太清楚該怎么打基礎,然后不巧的是所選擇的學習資源就沒有好好講高級編程這一塊的內容,導致了后面學起來吃力,欲速則不達,所以要往Python爬蟲等方向學習的小伙伴們,我個人覺得應當先把入門知識學好,
今天給大家寫了一篇關于Python高級編程這一塊的教學文章,在你學完了串列等基礎之后,學學這一塊的內容,將會讓你的進階之路更加輕松順暢!
先來看一下Python高級編程包括哪些東西:

文章目錄
- 一、面向物件編程
- ?(一)面向物件的思想
- ?(二)面向物件的兩個重要概念:類和物件
- 1.物件和類的概念
- 2.物件詳解
- 3.類的詳解
- (1)屬性詳解
- (2)類的方法詳解
- ?(三)面向物件的三大特性
- 1.封裝
- 2.繼承
- 3.多型和多型性
- 二、模塊與包
- 👳(一)模塊
- 1.定義
- 2.模塊匯入
- 3.定義別名
- 4.自定義模塊
- 5.模塊的測驗
- 👳(二)包
- 1.定義
- 2.包的創建
- 3.包的匯入
- 4.包的使用
- 👳(三)模塊與包的作用
- 👳(四)第三方庫的安裝
- 1.通過pip安裝
- 2.通過Pycharm安裝
- 3.通過渠道下載.whl檔案安裝
- 三、檔案處理
- 💼(一)檔案的定義和操作
- 💼(二)檔案的基本操作
- 💼(三)檔案的方法與屬性
- 💼(四)os模塊
- 四、例外
- ?(一)例外的定義
- ?(二)例外的處理
- 1.try-except
- 2.try-except-except
- 3.try-except-else
- 4.try-except-finally
- 5.頂層類Exception
- ?(三)自定義例外
- 五、正則運算式
- ??(一)re模塊
- ??(二)正則運算式
- 1.字串的匹配
- ??(三)正則的使用
- 1.編譯正則
- 2.正則物件的使用方法
- 3.Match object 的操作方法
- 4.re模塊的函式
- 六、行程執行緒
- 🔰(一)多任務作業系統
- 1.何為行程?
- 2.何為執行緒?
- 🔰 (二)Python的多行程multiprocessing(包)
- 1.Process——行程類
- 2.獲取當前行程的編號
- 🔰 (三)多執行緒Threading模塊
- 1.多執行緒的類Thread類
- 2.繼承Thread類
- 七、容器/迭代物件/生成器
- 🚩 (一)容器
- 🚩 (二)可迭代物件
- 🚩 (三)迭代器
- 🚩 (四)序列
- 🚩 (五)串列推導式
- 🚩 (六)生成器
- 1.生成器的第一種創建方法:生成器運算式
- 2.生成器的第二種創建方法:yield
- 八、修飾器
- 🏁 (一)修飾器的概念和作用
- 1.什么是修飾器?
- 2.修飾器的作用
- 🏁(二)修飾器的使用
- 1.使用說明
- 2.基本使用方式
- 3.其他使用方式:函式嵌套
- 4.其他使用方式:閉包
- 5.其他使用方式:被修飾的函式有引數的形式
- 6.其他使用方式:有引數的修飾器,無引數的函式,使用內嵌函式收取引數
- 🏁(三)Python內置的修飾器
- 1.property
- 2.staticmethod -- 靜態方法
- 3.classmethod
一、面向物件編程
?(一)面向物件的思想
面向物件是一個抽象的編程思維,很多編程語言都有的一種思想,Python是純的面向物件的語言,簡單來說可以理解為注重如何解決問題,而不是去研究底層怎么實作,
面向物件和面向程序是兩種不同的思想,面向物件編程的語言有Python等語言,面向程序編程的語言有C等語言,在寫代碼的編程思維是不同的,

舉個例子來理解這兩種思想,拿洗衣服為例,
用手洗衣服一般是這么干的:找盆-放水-加洗衣粉-浸泡衣服-搓洗-擰干-沖洗-擰干-晾曬,
這是一種面向程序的思想,一旦中間哪個環節不知道怎么辦,洗衣服就洗不好,比如說你不知道怎么搓洗導致衣服洗不干凈,比如說搓洗完之后沒有擰干直接晾曬導致一地水等等,某1個環節出錯了,都達不到想要的洗衣服效果,
用洗衣機洗衣服一般是這么干的:打開洗衣機-把衣服放進去-加洗衣粉-按開始按鈕-晾曬,
這是一種面向物件的思想,你不用知道把衣服放進去之后洗衣機是怎么洗的,你只要知道把衣服放進去后倒洗衣粉然后按按鈕就可以了,

同樣的,在編程語言當中,體現是不一樣的,C語言寫代碼的時候你得關注著你的記憶體等底層東西,但Python寫代碼的時候我們幾乎很少去關注底層的東西,注重點在于用什么方法去解決問題就可以了,
在正式涉及代碼之前,有一段概念性的東西是需要理解的,理解了這些基礎概念之后,我們才能更好地進入面向物件編程,

?(二)面向物件的兩個重要概念:類和物件
1.物件和類的概念
這兩個概念放在一起談可能更容易理解一些,可以簡單理解為類是產生物件的模板,
上面我們說過洗衣機的例子,如果說洗衣機是物件,那么制造這種洗衣機的圖紙就是類;如果說雞蛋是物件,那么母雞就是類;如果說狗是物件,那么狗類就是類,
在Python中,萬物皆物件,變數是物件,函式是物件,字串、串列、元組等等都是物件,

2.物件詳解
(1)物件的組成: 物件=屬性+方法
物件是由屬性和方法組成的,屬性可以理解為物件有什么,方法可以理解為物件能做什么,
(2)物件的屬性有哪些特征
- 物件的屬性可以是抽象的屬性
比如說洗衣機是一個物件,那么洗衣機是有顏色的,顏色就是1個屬性,但顏色是個抽象東西,因為它可以是紅色、白色、黑色等等顏色,
- 物件的屬性可以是另一個物件
比如說計算機是1個物件,那么計算機有硬碟,硬碟就是計算機的一個屬性,所以說物件的屬性可以是另一個物件,
- 大物件由小物件所組成
比如說滑鼠是一個物件,滑鼠的滾輪是1個物件,滑鼠里面的單片機是1個物件等等,你會發現,滑鼠這個大物件是由多個小物件組成,

(3)物件的方法
有1個規則先了解一下,物件的方法可以被自己呼叫,也可以被別的物件呼叫,后面我會詳細講解,
在物件這里我們暫時不講代碼,因為還要先講類的知識,才能更好地理解代碼里的類和物件,下面會講到的,
3.類的詳解
上面有提到過,類是產生物件的模板,那么類就是物件的抽象化,物件是類的具象化,
既然 物件=屬性+方法,同樣的,類=屬性+方法,
(1)創建類
創建類的方法:
class 類名:
屬性1
屬性2
方法1
方法2
#1個類中可以有多個屬性和方法,也可以只有屬性或者只有方法
舉例,創建1個學生類,并使用它創建物件
#創建Student類
class Student:
name = "小明" #學生類的name屬性
age = 18 #學生類的age屬性
def learn(self): #學生類的learn方法
print("學生的學習方法被呼叫了")
#創建Student類的物件
s1 = Student() #Student()就是創建的1個類物件,只是便于簡寫,我把它傳給了s1
s1.learn() #物件的方法
print(s1.age) #執行物件的屬性
#上面這兩行代碼其實可以直接寫成Student().learn() ,也同樣是利用類創建了物件并呼叫物件的方法
執行結果:
其實創建Student類的物件時,后面的3行代碼還可以寫成這樣,效果是一樣:
Student().learn() #創類的物件并使用物件的方法
print(Student().age) #創建類的物件并執行物件的屬性
回過頭來,我們再從代碼和概念結合的角度來理解一下什么是屬性和什么是方法,
類屬性就是類里面有什么,比如說類里面有name和age,所以name和age是Student這個類的屬性,
類方法是類能做什么,比如說類里面有learn(self)這個函式,它能執行“學生的學習方法被呼叫了”,也就是這個類能執行什么能干什么,所以learn(self)這個函式就是類的方法,

(1)屬性詳解
我們前面介紹過物件的屬性和類的屬性,那么我們來看一下兩者的區別和特征,
- 類屬性
1. 類屬性定義在類的內部,任何方法之外
1. 創建類的時候直接創建
2. 類名.新的屬性名=屬性值
2. 使用類屬性:
1. 物件.類屬性
2. 類.類屬性
3. 類屬性的特點:
1. 類屬性是屬于類物件的
2. 物件共用類屬性,如果類屬性變化,所有的物件也會改變
4. 類屬性有且只有一份,牽一發而動全域
- 物件屬性
物件屬性定義在方法內部
1. 創建
1. 物件.實體屬性名=屬性值
2. 方法內部創建:self.屬性名 = 屬性值
2. 使用物件屬性
物件.實體屬性名
3. 物件屬性各自擁有互相獨立
4. 物件屬性和類屬性發生命名沖突
1. 以物件屬性名優先
2. 物件屬性名會遮蔽同名的類屬性
(2)類的方法詳解
- 類方法的宣告
def 函式名(self,引數1,引數2,....)
- self
1.self默認傳遞,不需要傳值
2.self指代的是當前的實體(物件)
3.一定會有self傳遞,但名字不一定必須叫self,可以叫aa
4.self后面可以跟多個引數,用“,”隔開
- 方法的呼叫
1.物件.方法名(自動傳遞物件),例如 s2.fun1()
2.類名.方法名(類名()) #“類名()”相當于創建物件
注:方法的呼叫與函式類似,誰呼叫就回傳給誰
- 初始化的方法(特殊的類的方法)
def __init__():
1.這個方法不需要被呼叫,只要創建物件就會自動執行
2.這個方法只能回傳None值,無法回傳其他型別的值
3.如果要給init傳遞多個引數,只能通過 類名(多個引數) 的方式去傳遞
4.如果類里面沒有定義init,默認去呼叫父類
5.如果類里面重復定義了多個init方法,會被最后一個init方法覆寫
舉例:創建物件,查看默認執行的init并傳遞多個值;呼叫1個類的方法,確認該方法被呼叫并執行傳遞的值,
#創建類
class Student:
name = "小莫"
age = "18"
def __init__(self,aa,bb):
print("init被呼叫了")
print(aa)
print(bb)
def fun1(self):
print("函式1")
print(self)
def fun2(self):
print("函式2")
Student.fun1(Student(100,200)) #創建物件并傳遞多個值給init
執行結果為:
init被呼叫了
100
200
函式1
<__main__.Student object at 0x000001DAD8168400> #self值的存盤地址
?(三)面向物件的三大特性
三大特性:封裝、繼承、多型,
1.封裝
1.物件都有明確的邊界,把屬性和方法保護在邊界之內,(安全性)
2.封裝的力度適中,
3.封裝的原則
(1)將不需要對外提供的內容進行隱藏,
(2)隱藏屬性,提供公共的方法對其訪問
私有屬性:__name="xxx"
2.繼承
繼承是父類與子類的關系,比如狗類和二哈,狗類就是父類,二哈是子類,
(1)定義形式(類的完整形式)
class 子類的類名(父類的類名):
屬性
方法
(2)父類:基類,超類
object————頂層類
如果物件沒有書寫繼承關系,默認繼承object
(3)繼承的特點
- 子類可以繼續父類的屬性和方法
舉例:用子類創建的物件呼叫父類的屬性和方法
# 定義父類
class A:
name1 = "父類的屬性1"
def aa(self):
print("父類的方法1")
# 定義子類
class B(A):
name2 = "子類的屬性1"
def bb(self):
print("子類的方法1")
n = B()
print(n.name2) #呼叫子類的屬性
n.bb() #呼叫子類的方法
print(n.name1) #呼叫父類的屬性
n.aa() #呼叫父類的方法
執行結果為:
子類的屬性1
子類的方法1
父類的屬性1
父類的方法1
-
可擴展性,父類擴展了,子類也得到擴展,
-
如果子類沒有構造方法,物件會去呼叫父類的,
-
如果子類有自己的構造方法,則不會去呼叫父類的,
-
子類的構造方法可以呼叫父類的構造方法,呼叫可以有以下兩種方式:
父類的類名.__init__(self) #手動傳遞self
super().__init__() #不需要加self
- 多繼承
一個子類可以繼承多個父類,
# 定義父類A
class A:
name_a = "父類A的屬性1"
def aa(self):
print("父類A的方法1")
# 定義父類B
class B:
name_b = "父類B的屬性1"
def bb(self):
print("父類B的方法1")
#定義子類C
class C(A,B): #繼承兩個父類
pass #跳過
n = C()
print(n.name_a)
print(n.name_b)
n.aa()
n.bb()
執行結果為:
父類A的屬性1
父類B的屬性1
父類A的方法1
父類B的方法1
注:多繼承有好有壞,優點是增強了可拓展性,缺點則是繼承關系復雜之后容易混亂邏輯,難看懂,同時也會占用大量資源,比如著名的鉆石繼承問題,主要涉及mro和C3演算法,不懂的可以百度一下,
(4)方法覆寫
- 子類中的方法與父類中的方法同名,則覆寫父類,
#定義父類
class Animal:
def eat(self):
print("動物會吃東西")
#定義子類狗
class dog(Animal):
def eat(self):
print("狗會吃東西")
d = dog()
d.eat()
執行結果為:
狗會吃東西
-
子類覆寫了覆寫的方法之后,本質上并沒有替換父類的方法,父類的方法依然存在并可以給其他子類呼叫,
-
方法覆寫的前提:子類的方法名必須和父類的完全相同
(5)方法多載
出現多個方法名一樣的方法(函式),Python中通過默認值進行方法多載,

3.多型和多型性
多型:一類事物有多種形態,是一種使用物件的方式,子類重寫父類的方法,呼叫不同的子類物件的相同父類的方法,可以產生不同的效果,
舉例:以王者榮耀選英雄為例,英雄類(Hero)為父類,該父類中有一個方法叫stroke(技能);程咬金和后裔分別為兩個子類,兩個子類中也有stroke這個方法;呼叫者為另一個父類,有一個選擇英雄的方法,該方法需要呼叫stroke這個類方法,最后呼叫不同的子類(程咬金或后裔)物件,得到不同的技能描述,
#定義父類Hero
class Hero :
def stroke(self): #技能函式
print("英雄技能")
#定義子類程咬金
class ChengYaoJin(Hero):
def stroke(self): #技能函式
print("一技能跳躍,二技能旋轉,大招回血")
#定義子類后裔
class HouYi(Hero):
def stroke(self): #技能函式
print("一技能多發,二技能射圈,大招大鳥眩暈")
#定義呼叫者
class Person:
def chose_hero(self,hero): #玩家選擇英雄
hero.stroke()
c = ChengYaoJin() #程咬金
x = HouYi() #后裔
p = Person() #玩家
p.chose_hero(c) #玩家選擇程咬金,"c"換成"x"就會產生不同的結果
執行結果為:
一技能跳躍,二技能旋轉,大招回血
多型性:向不同的物件發送不同的資訊,不同的物件接收到資訊后,會做出不同的反應,
#定義鳥類
class Bird:
def fly(self):
print("鳥會飛")
#定義飛機類
class Plane:
def fly(self):
print("飛機會飛")
#定義火箭類
class Rocket:
def fly(self):
print("火箭會飛")
#函式呼叫父類的fly方法
def fun1(obj):
obj.fly()
fun1(Bird()) #呼叫鳥類的fly方法
fun1(Plane()) #呼叫飛機類的fly方法
fun1(Rocket()) #呼叫火箭類的fly方法
執行結果為:
鳥會飛
飛機會飛
火箭會飛
二、模塊與包
👳(一)模塊
1.定義
模塊指的是Python的程式檔案(源檔案),模塊的檔案名就是模塊名加上.py,里面包含了Python物件定義和Python陳述句,模塊包含了定義函式、類和執行代碼等等,一般情況下不要去修改模塊,以免模塊失效,
2.模塊匯入
Python中允許匯入的形式來進行模塊呼叫,Python中的模塊也是物件,模塊中定義的所有全域變數,匯入后都成為模塊的屬性,
語法1: import 模塊名
如果要匯入多個模塊,可以只用1個import來匯入多個模塊,多個模塊之間用逗號隔開即可,但是在Python的PEP8風格里面,不建議這么做,所以要匯入幾個模塊,就寫幾個import來進行挨個匯入,
例子:匯入math模塊并呼叫sqrt()開平方的功能對9進行開平方
import math
num = math.sqrt(9)
print(num)
輸出結果:
3.0
提示一下,在Python中進行運算操作默認回傳的是float型別,所以是3.0,
語法2: from 模塊名 import 功能1,功能2
注意這里的功能后面不加括號,
例子:匯入math模塊并呼叫sqrt()開平方的功能對9進行開平方
from math import sqrt
num = sqrt(9)
print(num)
輸出結果:
3.0
語法3:from 模塊名 import
一般情況下,這個語法可以匯入模塊的所有功能,當你要用某個模塊的多個功能的時候,就不用挨個寫了,這里我就不舉例了,都是一樣的,
注意點:
這個方式并非在所有情況下都能匯入模塊中的所有功能,
如果模塊中有__all__=["功能1","功能2"]這樣的宣告,
那么匯入的模塊只能用功能1和功能2,
如果模塊中還有功能3等等功能,但是沒有在__all__=[串列]中宣告,則無法呼叫,
例子:(這個例子需要看完下面的自定義模塊才看得懂)
新建了1個叫module1的模塊,模塊代碼有2個功能:
def fun1() : #實作a+b并輸出結果
print("fun1")
def fun2() : #實作a+b并輸出結果
print("fun2")
__all__ = ["fun1"] #宣告只能呼叫fun1()
在另一個Python檔案中用語法3的方式匯入模塊中的所有方法:
from module1 import *
fun1()
fun2()
執行結果只列印出來fun1,然后就報錯提示fun2()沒有被定義,無法識別,這就是因為
在module1中用__all__宣告了可呼叫的方法只有fun1(),
3.定義別名
有的模塊或者模塊內的功能名字比較長,多次使用的時候不方便,可以進行自定義模塊或者功能的名字,
1.模塊自定義別名: import 模塊名 as 別名
例子:自定義time模塊為別名t并使用,
import time as t
t.sleep(3) #延遲三秒
print("hello world")
程式執行3秒后輸出:
hello world
2.功能自定義別名: from 模塊名 import 功能名 as 別名
例子:匯入time模塊并自定義sleep()功能為s這個名字
from time import sleep as s
s(5) #延時5秒
print("hello world")
程式執行5秒后輸出:
hello world
4.自定義模塊
每個人都能生成自定義模塊來進行呼叫,自定義模塊就是Python檔案,我們寫代碼時創建的Python檔案就相當于1個模塊,
注意點:被呼叫的模塊盡量放在當前Python檔案相同目錄下,否則匯入時要宣告所在檔案夾才能匯入,
例子:自定義1個模塊,在另一個Python檔案中進行呼叫,
新建1個名為module1的Python檔案,代碼如下:
def fun1(a,b) : #實作a+b并輸出結果
print(a+b)
相同目錄下新建另一個Python檔案,呼叫module1.py這個模塊:
import module1
module1.fun1(20,30)
運行當前Python檔案結果:
50
5.模塊的測驗
每個模塊匯入的時候都默認被執行一遍,但同時在模塊內部又存在著很多的內部測驗代碼,為了避免匯入模塊時執行了模塊內部的測驗代碼,于是就牽扯到一個方法:
很多模塊在內部都有測驗方法:
if __name__ == "__main__":
代碼1
這個方法能夠實作一個功能,在模塊中執行的話,就會執行代碼1的代碼,在其他檔案匯入該模塊的時候,則不會執行代碼1的代碼,所以一般模塊內部的測驗都放在了代碼1當中,
為什么?神奇的點就在于__name__,它在當前檔案中執行的結果是__main__,在其他檔案匯入時執行的結果是模塊名,所以利用這一點,用上if陳述句就能判斷模塊執行到底是在當前檔案執行還是被匯入執行,
舉例:
新建1個Python叫module1,作為模塊,代碼如下:
print(__name__) #列印__name__
執行結果:
__main__
再新建1個Python檔案,匯入剛才建好的module1.py模塊:
import module1
執行結果:
module1
在當前檔案的執行和被匯入時執行,結果是不一樣的,所以它成為了模塊的內部測驗方法,
注意點:在自定義的模塊中,不建議寫while循壞,不然匯入的程序中一直在執行模塊里面的while循壞,可能會跳不出來,也就是一直在匯入模塊,其他代碼執行不到,
👳(二)包
1.定義
包就是將有聯系的模塊放在同一個檔案夾下,并且該檔案夾里有“__init__.py”這個檔案,這個檔案夾就叫做包,
包的特征:
1.包也是物件
2.必須有__init__.py檔案
3.__init__.py檔案是包的構造方法,控制著包的匯入行為,一般是個空包
4.包里面不僅可以有模塊,還可以有子包
2.包的創建
在Pycharm軟體中新建1個專案檔案,創建完成后,打開Pycharm創建1個專案檔案夾→點擊檔案夾→右鍵彈出選項→New→Python Package→完成創建,創建的新檔案夾就是包,里面自動創建了init檔案,

3.包的匯入
常規匯入方法主要有2種,
方法1:import 包名.模塊名.目標
方法2:import 包名.子包.模塊名.目標
這里所說的目標可以是變數、函式等等物件,具體的在下面會講到,
4.包的使用
使用形式1:常規使用1
import 包名.模塊名
包名.模塊名.功能
舉例:創建1個包,在另一個.py檔案中使用,要求.py檔案不包含在創建的包內,
步驟1,打開Pycharm新建一個專案,創建1個包,命名為demo,包里面新建1個.py檔案,命名為my_module,代碼如下:
a = 100
def fun1():
print("這是1個包")
步驟2,打開另1個.py檔案,匯入已創建的包
import demo.my_module
print(demo.my_module.a)
demo.my_module.fun1()
執行結果:
100
這是1個包
使用形式2:常規使用2
from 包名 import 模塊名
模塊名.功能
模塊名.變數
舉例:我沿用上面已經創建好的demo包,直接在步驟2中修改代碼,用這個新方式去匯入包并使用,
from demo import my_module
print(my_module.a)
my_module.fun1()
執行結果是一樣的:
100
這是1個包
使用形式3:另起別名并使用
import 包名.模塊名 as 別名
別名.功能
別名.變數
上面使用形式1中可能有人會發現,匯入模塊后的使用不太方便,又要包名又要模塊名的,能不能簡單點?當然可以,直接給包里的模塊另起1個縮短的別名,后面直接用別名就可以了,
沿用上面的使用形式1的例子,這里我就不改動步驟1了,我直接在步驟2中進行另起別名,
import demo.my_module as n1 #另起別名為n1
print(n1.a)
n1.fun1()
執行結果:
100
這是1個包
結果是一樣的,但如果代碼比較長的話,使用別名會方便很多,
當然了,這里也可以用使用形式2的方式匯入包并另起別名,使用時用別名就可以了,這個方式我就不舉例了,你們自己可以去試試,
使用方式4:匯入某個功能
from 包名.模塊名 import 功能1
功能1
這里我也沿用已創建好的包,直接在另一個.py檔案中進行使用,
from demo.my_module import fun1
fun1()
執行結果:
這是1個包
使用方式5:匯入所有功能
在模塊匯入的時候我們介紹了import * 這個方式去匯入模塊里面的所有功能,在這里也可以這么入匯入包里面的模塊的所有功能,
舉例,名叫“demo”包里面有1個模塊叫“hhhh”,模塊的代碼如下:
def fun1():
print("這是功能1")
def fun2():
print("這是功能2")
然后我們在另一個.py檔案中呼叫一下;
from demo.hhhh import *
fun1()
fun2()
執行結果:
這是功能1
這是功能2
6.關于all的使用
關于__all__的使用在模塊的時候有介紹過,那是控制可以被匯入的功能串列,但在包里面,__all__是控制可以被匯入的模塊串列,即宣告哪些模塊可以被匯入,
包里面的__all__是在__init__檔案中宣告的,而不是在哪個模塊中寫的,
舉例:一個包里面有多個模塊,其中被all宣告的模塊可以被匯入,不宣告的不可以被匯入,
步驟1,在名為demo的包里面有hhhh和my_module兩個模塊,但在int檔案中被all宣告只有hhhh檔案可以使用,int檔案的代碼如下:
__all__ = [
"hhhh" # hhhh模塊允許被匯入
]
步驟2,在新的.py檔案中匯入demo包里面的hhhh和my_module兩個模塊:
from demo import * #匯入包里面的所有模塊
hhhh.fun1() #呼叫hhhh模塊的fun1功能,輸出“這是功能1”
my_module.fun1() #呼叫my_module模塊的fun1功能,輸出“這是1個包”
執行結果:

很明顯,雖然用 * 匯入了所有模塊,但被all宣告可匯入的hhhh模塊是可以被使用的,沒被all宣告的my_module模塊是不能使用的,系統無法識別,
👳(三)模塊與包的作用
1.提高代碼的可重用性,好用的代碼不止你1個人可以用,很多人都可以重復使用它,
2.提高代碼的可讀性,如果所有的代碼都放在1個.py檔案中,那代碼就太長了,增加了理解和維護難度,所以可以把一些常用的代碼封裝成包和模塊,起1個望文生義的名字,需要的時候直接用就行,減少了代碼的數量,提高了可讀性,
3.減少代碼的冗余,模塊里面封裝的一些方法,我們直接給引數去使用就可以了,不用把方法再寫一遍,占用記憶體,也就減少了代碼的冗余,
👳(四)第三方庫的安裝
Python雖然有很多自帶的模塊和包,簡稱內置模塊,但只會使用內置模塊還不夠,畢竟內置模塊有限,我們經常會用到第三方的庫,這個時候學習怎么安裝第三方的庫(包)是很必要的,
今天來介紹3種第三方模塊與包的安裝方法,
1.通過pip安裝
可以通過包管理命令pip去進行第三模塊與包的下載和安裝,前提是你安裝的Python是照著我前面在【Python基礎入門】那篇文章所講的方式去安裝,所有的選項都勾選了,這樣你就不用配置環境變數了,首先查看一下你的pip是否能用,
方法:WIN+R調出運行視窗→輸入cmd→如果出現下面的pip資訊則是可以使用pip

如果出現紅字提示“cmd中 ’pip‘不是內部或外部命令,也不是可運行的程式或批處理檔案”,那么你就手動配置一下環境變數吧,實在不行就回過頭跟著我說的安裝Python的步驟去重新裝吧,
回到pip如何安裝第三方模塊與包的問題,首先我們得知道我們要安裝的第三方模塊與包是叫什么名字,比如Pillow這個第三方庫,這是Python下非常強大的處理影像的工具庫,安裝方法是:
1.WIN+R調出運行視窗
2.輸入cmd
3.輸入 pip install Pillow
4.等待下載和安裝的完成

有時候會出現很多紅字提示下載失敗,這個很正常,原因可能是:
(1).pip版本過低,升級一下pip版本,在黑視窗輸入:python -m pip install -U pip
(2)網路不好,多下載幾遍就可以了
2.通過Pycharm安裝
通過pip安裝是不需要打開軟體的,但我們也可以通過Pycharm進行安裝,方法如下:
點擊左上角的 File → Settings → Project:專案名 → Python interpreter → 點擊 + →
輸入你想安裝的包名,選擇你看上的 → 點擊 Install Package → 等待下載和安裝



3.通過渠道下載.whl檔案安裝
可以通過官網等渠道去搜索和下載你想要的包,官網:https://pypi.org/ ,搜索你要下載的第三方庫,比如說Pillow這個庫,直接搜就可以了:

然后選擇你要下載的檔案,比如Pillow8.3.2這個版本,進入下載頁面,點擊 Download files,

進去之后有很多的版本型號,建議選擇與你符合的版本,否則可能裝不了,這里要注意Python版本、系統和電腦的位數,

比如說我的Python裝的是3.9.6的,所以我下的是cp39里面的,電腦是64位,用的是Windows系統,所以我下的版本是 Pillow-8.3.2-cp39-cp39-win_amd64.whl 這個檔案,

下載好之后,WIN+R 打開命令列輸入CMD,在黑視窗中輸入 pip install 檔案路徑下的檔案名,比如我下載后存放的路徑是D:\谷歌瀏覽器,所以我在黑視窗輸入的代碼是:
pip install D:\谷歌瀏覽器\Pillow-8.3.2-cp39-cp39-win_amd64.whl
然后回車進行安裝,等待安裝完成,

三、檔案處理
💼(一)檔案的定義和操作
計算機中的檔案通常是指計算機硬碟為載體的、存盤在計算機中的資訊集合,主要的表現形式為視頻、音頻、圖片以及檔案四類,比如執行性檔案.exe、檔案檔案.txt、網頁檔案.html等等,

💼(二)檔案的基本操作
在現實中,我們對檔案進行操作可以大致總結為“打開→操作(閱讀、洗掉、修改等)→保存→關閉”,在Python當中依然是如此,在用Python開始檔案操作之前,我們先學幾個方法,
1.open(name,mode) ----打開檔案
這是Python打開檔案的方法,用于打開一個檔案,回傳的是一個檔案物件,
name指的是檔案名,一定要寫全,何為寫全?就是要寫清楚 存盤路徑+檔案名+后綴 ,
為何要寫這么全?因為就算是相同的存盤路徑下,檔案名相同的檔案也可能不止一個,只要后綴不一樣,計算機是允許存在同名檔案,所以不寫全的話,計算機是不知道你指的是誰
mode是打開檔案的模式,默認是r,也就是只讀的方式,mode的方式有很多,比如讀、寫等等,后面我們會講到,
2.write(“內容") ------寫
顧名思義,就是向檔案物件中寫入內容,
3.read() -------讀
向檔案中寫入內容,括號里面可以寫數字也可以不寫,不寫的話默認是讀取全部內容,寫數字則表示讀取X個字符,比如說read(6)則讀取檔案物件的6個字符,
4.close() ------關閉檔案
關閉檔案的方法,如果你在進行檔案操作之后不進行關閉檔案,則檔案一直處于打開和操作的狀態,會占用記憶體,
5.案例
在了解了這4個基本方法之后,我們來開始做1個小案例:新建一個專案,然后新建一個名為“檔案”的Python檔案用于寫代碼,然后我們用寫的方式向1個叫“檔案1”的.txt檔案寫入“hello world”,代碼如下:
f = open("檔案一.txt",'w') #以寫入的方式,打開檔案
f.write("hello world") #向檔案一中寫入內容
f.close() #關閉檔案
前面講過 open() 這個方法回傳的是一個檔案物件,所以我們用f進行接收一下,這是沒有運行前的界面:

運行后:

運行后生成了一個新的名為“檔案一.txt”檔案,打開它之后就能看到我們輸入的內容,進行寫操作時,如果檔案不存在,則默認會創建一個,
同樣的,我們也可以對這個檔案進行讀的操作:
f = open("檔案一.txt",'r') #以寫入的方式,打開檔案
print(f.read()) #讀檔案
f.close() #關閉檔案
運行結果:
hello world
這就是一個最基本的檔案操作流程,
在這里要注意一點,open(name,mode)在一開始寫的時候,mode就已經決定了你能做什么操作,也就是說如果你在開始寫代碼的時候寫的是:
f = open("檔案一.txt",'r') #只讀的方式打開檔案
那么后面你想進行write()的寫操作是會報錯的,因為mode里面宣告了r只讀模式,所以你沒有寫的權限,這一點要注意一下,
6.mode的其他操作模式
mode中有很多的操作模式,我們以表格的方式來看看:
| 模式 | 描述 |
|---|---|
| r | 以只讀的形式打開檔案,檔案的指標在開頭 |
| r+ | 讀寫,檔案指標在開頭 |
| rb | 以二進制的形式,只讀檔案指標在開頭 |
| w | 只寫,檔案不存在,則創建新的,存在則覆寫,指標在開頭 |
| w+ | 讀寫,檔案不存在,則創建新的,存在則覆寫,指標在開頭 |
| wb | 只寫,以二進制的形式 |
| a | 追加模式,檔案指標在結尾 |
| a+ | 讀寫,不存在則創建,存在直接追加 |
| ab | 以二進制形式追加 |
指標在這里可以理解為游標,它在哪里,你的操作就從哪里開始,
舉例:新建1個名為 “test” 的.txt檔案,第一次向里面寫入aaa,第二次向里面寫入bbb,
f = open("test.txt", 'a+')
f.write("aaa")
f.close()
f = open("test.txt") # 默認只讀模式
print(f.read()) # 列印內容
f = open("test.txt", 'a+')
f.write("bbb")
f.close()
f = open("test.txt") # 默認只讀模式
print(f.read()) # 再次列印內容
運行結果:
aaa
aaabbb
💼(三)檔案的方法與屬性
1.file的物件屬性
有三個常用的方法可用于查看檔案物件的屬性:
1. closed
如果檔案物件已關閉,回傳True,否則回傳False
2. mode
回傳檔案物件的訪問模式
3. name
回傳檔案的名稱
案例:對檔案進行操作(隨意),查看被操作的檔案名、操作模式和是否關閉,
f = open("test.txt", 'a+')
f.write("aaa")
f.close()
print(f.closed) #查看是否關閉
print(f.name) #查看檔案名字
print(f.mode) #查看操作模式
運行結果:
True
test.txt
a+
2.file的物件方法
檔案的方法有很多,前面我們已經講過一點,比如read()和write(),但還有一些常用的方法需要掌握的,比如下面的:
1. close()
關閉檔案---非常重要
2. read([count])
讀取檔案中的內容
count:位元組數量
3. readlines()
讀取所有內容,打包成串列
4. readline()
讀取一行資料,追加讀取,讀取過的不能再次讀取
5. seek(offset,[from])
修改指標的位置:從from位置移動了offset個位元組
from:0則表示從起始位置,1則表示從當前位置開始,2則表示從末尾開始
oofset:要移動的位元組數
6. write()
向檔案中寫入內容
舉例:向test.txt檔案中寫入aaabbbccc,將檔案中的內容輸出為串列,
f = open("test.txt", 'a+')
f.write("aaabbbccc")
f.close()
f = open("test.txt")
print(f.readlines())
f.close()
運行結果:
['aaabbbcccaaabbbccc']
💼(四)os模塊
os模塊是一個用于訪問作業系統的模塊,在進行檔案操作的時候常會用到它,模塊在使用之前要進行匯入,
import os
1.關于檔案的功能
1.os.rename(原檔案名,新的檔案名) ——檔案重命名
2.os.remove(檔案名) ——洗掉檔案
如果不說明路徑,則在源代碼所在檔案夾下尋找,尋找不到會報錯,
若想洗掉指定檔案夾下的檔案,檔案名則需要具體路徑,例如 os.remove(r"D:\test_1\檔案名"),r防止斜杠發生轉義
舉例:已有檔案test1.txt,將其修改成test20.txt,
import os
os.rename("test1.txt","test20.txt")
運行結果:

2.檔案夾的功能
1.os.mkdir(檔案夾名) ——創建檔案夾
2.os.rmdir(檔案夾名) ——洗掉檔案夾
3.os.getced() ——獲取當前目錄
4.os.chdir(目錄) ——切換目錄
5.os.listdir(目錄) ——獲取當前檔案夾下所有的檔案或者檔案夾,回傳一個串列
os.listdir("aa") #獲取aa檔案夾下的所有檔案或檔案夾,回傳一個串列
舉例:在現有檔案夾venv里面新建一個新建檔案夾,
import os
os.chdir(r"D:\檔案\venv") #切換到venv檔案夾下,r是防止轉義
os.mkdir("新建檔案夾") #在venv檔案夾下創建一個新建檔案夾
print(os.getcwd()) #輸出當前目錄位置
運行結果:

四、例外
?(一)例外的定義
例外是一個事件,該事件在程式執行程序中發生,影響程式的正常執行,一般情況下,Python無法正常處理程式時就會發生一個例外,
例如我在代碼中只寫了一個變數a,運行程式,pycharm無法識別這個變數,所以報錯,便是出現了例外,

?(二)例外的處理
所以我們需要掌握處理例外的方法,處理例外的方法有很多種,接下來我們一個個來看,
1.try-except
它能夠將可能出錯的代碼進行處理,處理后報錯的紅色字體將會轉換成簡短的、正常的字體,用法如下:
try:
有可能出現例外的代碼
except 例外型別 as 變數
處理后的代碼
舉例:直接列印變數a會報錯,

經過tyr-except處理過一下:
try:
print(a)
except NameError as s:
print(s)
再次運行看效果:

飄紅的報錯資訊變得簡短且顏色正常,看起來這個例外是不是顯得舒服多了?
這里的tyr-except并不影響代碼的運行,如果你的代碼沒有報錯,你就算是寫了tyr-except,它也只會執行try那行代碼,那行代碼沒有錯誤,那就不會執行except里面的代碼,
例如我們來一個正常的:

2.try-except-except
這種方法和前面的try-except寫法是差不多的,只是后面再增加了一個except,可用于判斷多種可能報錯的情況,
例如:有兩行代碼可能會報錯,兩種不同型別的例外,但不想讓它飄紅,
try:
1 / 0
print(a)
except NameError as s: # 第一種寫法,用as+變數
print(s)
except ZeroDivisionError: # 第二種寫法,自定義輸出內容
print("除數不能為0") # 自定義輸出的內容
運行結果:

雖然報錯,但沒有飄紅,這里注意一下except的兩種寫法,
try-except的寫法很靈活的,我們同樣可以用元組把可能報錯的例外型別囊括進去,避免寫多行except,例如:

3.try-except-else
如果沒有例外,則執行else里面的代碼,例如:

4.try-except-finally
不管代碼是否有例外,最后都會執行finally里面的代碼,例如:

5.頂層類Exception
except后面其實可以不加錯誤型別,因為系統會默認認為后面的錯誤是型別是Exception,這是1個頂層類,包含了所有的出錯型別,

?(三)自定義例外
有沒有發現,前面我們去做基本的例外捕獲時,每次可能出錯的地方就得寫一個try-except,如果有多個地方可能會出錯呢?是否我們需要寫多個try-except?又或者理論上代碼可以運行,但我想定一下規矩,凡是不符合我規矩的行為,我都讓它出現例外,比如密碼長度超出我規定的長度,我想讓程式出現例外,
自定義例外可用于引發一個例外(拋出一個例外),由關鍵字raise引發,
舉例:模擬用戶輸入密碼的情景,用戶輸入的密碼不能低于6位數,自定義一個例外,用于檢測用戶輸入的密碼是否符合規定,不符合則引發例外,提示當前輸入的密碼長度和最小密碼長度不能低于6位數,
class MyError(Exception): # 例外捕獲的類
def __init__(self, length, min_len): # length為用戶輸入的密碼長度,min_len為規定的最小長度
self.length = length
self.min_len = min_len
# 設定拋出例外的描述資訊
def __str__(self):
return "你輸入的長度是%s,不能少于%s" % (self.length, self.min_len)
def main():
try:
con = input("請輸入密碼:") # 獲取用戶輸入的密碼
l = len(con) # 獲取用戶輸入的密碼長度
if l < 6:
raise MyError(l, 6) # 長度低于設定的6位數則引發例外
except Exception as ss: # 有錯誤則提示
print(ss)
else:
print("您的密碼輸入完畢") # 沒有錯誤則執行
main()
運行結果:

從上面的代碼中我們又用到了之前面向物件板塊里面的類和實體物件的知識,忘記的趕緊去復習吧,除此之外,這里還結合了前面的try-except,還有我們的關鍵字raise引起例外捕獲,
五、正則運算式
??(一)re模塊
在講正則運算式之前,我們首先得知道哪里用得到正則運算式,正則運算式是用在findall()方法當中,大多數的字串檢索都可以通過findall()來完成,
1.匯入re模塊
在使用正則運算式之前,需要匯入re模塊,
import re
2.findall()的語法:
匯入了re模塊之后就可以使用findall()方法了,那么我們必須要清楚findall()的語法是怎么規定的,
findall(正則運算式,目標字串)
不難看出findall()的是由正則運算式和目標字串組成,目標字串就是你要檢索的東西,那么如何檢索則是通過正則運算式來進行操作,也就是我們今天的重點,
使用findall()之后回傳的結果是一個串列,串列中是符合正則要求的字串
??(二)正則運算式
1.字串的匹配
(1)普通字符
大多數的字母和字符都可以進行自身匹配,
import re
a = "abc123+-*"
b = re.findall('abc',a)
print(b)
輸出結果:
['abc']
(2)元字符
元字符指的是. ^ $ ? + {} \ []之類的特殊字符,通過它們我們可以對目標字串進行個性化檢索,回傳我們要的結果,
這里我給大家介紹10個常用的元字符以及它們的用法,這里我先給大家做1個簡單的匯總,便于記憶,下面會挨個講解每一個元字符的使用,

** 1?? []**
[] 的使用方式主要有以下三種:
- 常用來指定一個字符集,
s = "a123456b"
rule = "a[0-9][1-6][1-6][1-6][1-6][1-6]b" #這里暫時先用這種麻煩點的方法,后面有更容易的,不用敲這么多[1-6]
l = re.findall(rule,s)
print(l)
輸出結果為:
['a123456b']
- 可以表示一個范圍,
例如要在字串"abcabcaccaac"中選出abc元素:
s = "abcabcaccaac"
rule = "a[a,b,c]c" # rule = "a[a-z0-9][a-z0-9][a-z0-9][a-z0-9]c"
l = re.findall(rule, s)
print(l)
輸出結果為:
['abc', 'abc', 'acc', 'aac']
- [] 內的元字符不起作用,只表示普通字符,
例如要在字串“caabcabcaabc”中選出“caa”:
print(re.findall("caa[a,^]", "caa^bcabcaabc"))
輸出結果為:
['caa^']
注意點:當在[]的第一個位置時,表示除了a以外的都進行匹配,例如把[]中的和a換一下位置:
print(re.findall("caa[^,a]", "caa^bcabcaabc"))
輸出:
['caa^', 'caab']
2??^
^ 通常用來匹配行首,例如:
print(re.findall("^abca", "abcabcabc"))
輸出結果:
['abca']
3?? $
$ 通常用來匹配行尾,例如:
print(re.findall("abc$", "accabcabc"))
輸出結果:
['abc']
4?? \
? 反斜杠后面可以加不同的字符表示不同的特殊含義,常見的有以下3種,
- \d:匹配任何十進制數等價于[0-9]
print(re.findall("c\d\d\da", "abc123abc"))
輸出結果為:
['c123a']
\可以轉義成普通字符,例如:
print(re.findall("\^abc", "^abc^abc"))
輸出結果:
['^abc', '^abc']
5?? s
匹配任何的空白字符例如:
print(re.findall("\s\s", "a c"))
輸出結果:
[' ', ' ']
6?? \w
匹配任何字母數字和下劃線,等價于[a-zA-Z0-9_],例如:
print(re.findall("\w\w\w", "abc12_"))
輸出:
['abc', '12_']
7?? {n}
{n}可以避免重復寫,比如前面我們用\w時寫了3次\w,而這里我們這需要用用上{n}就可以,n表示匹配的次數,例如:
print(re.findall("\w{2}", "abc12_"))
輸出結果:
['ab', 'c1', '2_']
8?? *
*表示匹配零次或多次(盡可能的多去匹配),例如:
print(re.findall("010-\d*", "010-123456789"))
輸出:
['010-123456789']
9?? +
+表示匹配一次或多次,例如
print(re.findall("010-\d+", "010-123456789"))
輸出:
['010-123456789']
🔟 .
.是個點,這里不是很明顯,它用來操作除了換行符以外的任何字符,例如:
print(re.findall(".", "010\n?!"))
輸出:
['0', '1', '0', '?', '!']
1?? 1?? ?
?表示匹配一次或零次
print(re.findall("010-\d?", "010-123456789"))
輸出:
['010-1']
這里要注意一下貪婪模式和非貪婪模式,
貪婪模式:盡可能多的去匹配資料,表現為\d后面加某個元字符,例如\d*:
print(re.findall("010-\d*", "010-123456789"))
輸出:
['010-123456789']
非貪婪模式:盡可能少的去匹配資料,表現為\d后面加?,例如\d?
print(re.findall("010-\d*?", "010-123456789"))
輸出為:
['010-']
1??2??{m,n}
m,n指的是十進制數,表示最少重復m次,最多重復n次,例如:
print(re.findall("010-\d{3,5}", "010-123456789"))
輸出:
['010-12345']
加上?表示盡可能少的去匹配
print(re.findall("010-\d{3,5}?", "010-123456789"))
輸出:
['010-123']
{m,n}還有其他的一些靈活的寫法,比如:
- {1,} 相當于前面提過的 + 的效果
- {0,1} 相當于前面提過的 ? 的效果
- {0,} 相當于前面提過的 * 的效果
關于常用的元字符以及使用方法就先到這里,我們再來看看正則的其他知識,
??(三)正則的使用
1.編譯正則
在Python中,re模塊可通過compile() 方法來編譯正則,re.compile(正則運算式),例如:
s = "010-123456789"
rule = "010-\d*"
rule_compile = re.compile(rule) #回傳一個物件
# print(rule_compile)
s_compile = rule_compile.findall(s)
print(s_compile) #列印compile()回傳的物件是什么
輸出結果:
['010-123456789']
2.正則物件的使用方法
正則物件的使用方法不僅僅是通過我們前面所介紹的 findall() 來使用,還可以通過其他的方法進行使用,效果是不一樣的,這里我做個簡單的總結:
(1)findall()
找到re匹配的所有字串,回傳一個串列
(2)search()
掃描字串,找到這個re匹配的位置(僅僅是第一個查到的)
(3)match()
決定re是否在字串剛開始的位置(匹配行首)
就拿上面的 compile()編譯正則之后回傳的物件來做舉例,我們這里不用 findall() ,用 match() 來看一下結果如何:
s = "010-123456789"
rule = "010-\d*"
rule_compile = re.compile(rule) # 回傳一個物件
# print(rule_compile)
s_compile = rule_compile.match(s)
print(s_compile) # 列印compile()回傳的物件是什么
輸出:
<re.Match object; span=(0, 13), match='010-123456789'>
可以看出結果是1個match 物件,開始下標位置為0~13,match為 010-123456789 ,既然回傳的是物件,那么接下來我們來講講這個match 物件的一些操作方法,
3.Match object 的操作方法
這里先介紹一下方法,后面我再舉例,Match物件常見的使用方法有以下幾個:
(1)group()
回傳re匹配的字串
(2)start()
回傳匹配開始的位置
(3)end()
回傳匹配結束的位置
(4)span()
回傳一個元組:(開始,結束)的位置
舉例:用span()來對search()回傳的物件進行操作:
s = "010-123456789"
rule = "010-\d*"
rule_compile = re.compile(rule) # 回傳一個物件
s_compile = rule_compile.match(s)
print(s_compile.span()) #用span()處理回傳的物件
結果為:
(0, 13)
4.re模塊的函式
re模塊中除了上面介紹的findall()函式之外,還有其他的函式,來做一個介紹:
(1)findall()
根據正則運算式回傳匹配到的所有字串,這個我就不多說了,前面都是在介紹它,
(2)sub(正則,新字串,原字串)
sub() 函式的功能是替換字串,例如:
s = "abcabcacc" #原字串
l = re.sub("abc","ddd",s) #通過sub()處理過的字串
print(l)
輸出:
ddddddacc #把abc全部替換成ddd
(3)subn(正則,新字串,原字串)
subn()的作用是替換字串,并回傳替換的次數
s = "abcabcacc" #原字串
l = re.subn("abc","ddd",s) #通過sub()處理過的字串
print(l)
輸出:
('ddddddacc', 2)
(4)split()
split()分割字串,例如:
s = "abcabcacc"
l = re.split("b",s)
print(l)
輸出結果:
['a', 'ca', 'cacc']
六、行程執行緒
🔰(一)多任務作業系統
作業系統可以執行多個任務,比如我們的Windows系統,除了目前在執行的、你能看得到的幾個任務,還有很多后臺正在執行的任務,可以用Ctrl+Alt+Del鍵調出任務管理器看一下就知道了,

我的電腦配置經常會看到有幾核處理器的屬性,例如我的電腦是12核的,也就是說電腦最多能同時執行12個任務,最多運行12個行程同時進行,

但為什么我們的電腦卻能夠同時運行幾百個任務呢?

其實這得益于于作業系統的任務調度,大部分的作業系統是采用搶占時間片的形式進行調度,系統在極其微小的時間內,在多個任務之間進行極快速的切換,比如說8核的作業系統理論上1秒鐘之內只能同時執行8個任務,但是系統在1秒鐘之內可能在上百個任務之間進行切換,A任務執行一下、B任務執行一下、C任務執行一下…結果1秒鐘之內很多任務都能被執行到,造成了肉眼可見的幾百個任務在一直執行,
術語叫“宏觀并行,微觀串行”,實際上電腦在極端的時間內只能執行不超過配置核數的任務數,8核還是只能執行8個任務,

1.何為行程?
既然講到了任務,那么行程就是任務,1個行程就相當于1個任務,是作業系統分配資源的最小單位,在python中,想要實作多任務可以使用行程來完成,行程是實作多任務的一種方式,
2.何為執行緒?
行程的多個子任務就稱之為執行緒,執行緒是行程的最小執行單位, 一個行程可以有很多執行緒,每個執行緒執行的任務都不一樣,

Python既支持多行程又支持多執行緒,接下來我們就開始進入Python的行程與執行緒的學習,
🔰 (二)Python的多行程multiprocessing(包)
如果你利用多行程,你的Python代碼是從頭到尾逐行執行的,這其實就是在執行1個行程,這一點應該很好理解,
要想更多利用CPU資源,我們可以利用多行程,這里介紹一個Python多行程時常用的包multiprocessing,它擁有很多的功能,比如子行程、通訊、共享、執行不同的形式等等,我們來了解一些常用的,
1.Process——行程類
Process是multiprocessing里面的一個行程類,通過它就能實作多行程,我們先來看一下它的用法,后面我們會有實際的例子去講述,
Process(target,name,args,kwargs)
- target是目標,在哪里新開行程讓系統去執行?得給系統一個目標,
- name是行程的名字,你可以設定也可以不設定,默認是Process-N,N是從1,2,3…N,系統默認從小到大取名,
- args和kwargs是引數,可用于傳遞到目標,
Process里面有很多方法,其中最常用的就是start()啟動行程的方法,
行程名.start() #開始行程
舉例:寫好的代碼如下,我想看看開啟和沒開啟多行程呼叫函式的效果,
import time
#2個要同時執行的函式
def music() :
for i in range(5): #執行5次
print("聽音樂中...")
time.sleep(0.2) #延遲0.2s,目的是讓效果對比更明顯一些
def movie():
for i in range(5):
print("看視頻中...")
time.sleep(0.2) #延遲0.2s
music()
movie()
print("主行程執行完畢")
在沒有開啟多行程時,執行效果如下:

可以看到,這是很正常的運行情況,程式從上運行到下,逐行運行,music()里面的三次回圈沒有執行完畢就不會執行movie()里面,以及這兩個函式如果沒有執行完畢,就不會執行最后一行的print(“主行程執行完畢”),
我們再來看在上面案例的代碼中加入多行程:
import time
import multiprocessing
# 2個要同時執行的函式
def music():
for i in range(5): # 執行5次
print("聽音樂中...")
time.sleep(0.2) # 延遲0.2s,目的是讓效果對比更明顯一些
def movie():
for i in range(5):
print("看視頻中...")
time.sleep(0.2) # 延遲0.2s
if __name__ == "__main__": # 解決Windows系統下呼叫包時的遞回問題
# 創建子行程
music_process = multiprocessing.Process(target=music)
movie_process = multiprocessing.Process(target=movie)
# 啟用行程
music_process.start()
movie_process.start()
print("主行程執行完畢")
代碼中我加入了一個if陳述句來判斷__name__這個,為什么?因為在Windows系統下, multiprocessing這個包會發生遞回現象,就是會在“匯入模塊—呼叫模塊”之間反復執行,不信你可以把if陳述句去掉,把里面的代碼全部放到外面來執行就會報錯,這是Windows系統下會發生的一個現象,mac、linux等系統是不用加ifl來做判斷的,
關于__name__ = "main"這個知識點我在模塊與包的初始化時候有講過,不懂的可以回去看一下,
運行效果:

可以看出來,這開啟行程之后,代碼運行時是有3個行程同時進行的,一個是從上往下執行的主行程,執行到下面輸出“主行程執行完畢”,另外兩個子行程去執行music()和movie()行程,從他們的執行速度來看,它們是同時在進行的,所以沒有像剛才那樣非要等其中一個函式里面的代碼執行3遍才開始第2個函式,
同樣的代碼,你們的執行效果可能會跟我有所差異,因為效果是根據系統當前的狀況去隨機分配的,但并不影響你能看出來它的結果是多執行緒在進行,
最后補充一下,前面我們講過Process里面有args和kwargs可進行引數傳遞,args是普遍引數的傳遞,kwargs是以字典的形式進行引數傳遞,我們還是以上面的代碼為例,進行一下有引數傳遞的多進行,

2.獲取當前行程的編號
前面我們講到了代碼執行時有多個行程在同時進行任務,那么怎么樣查看當前行程的編號來得知目前有哪些行程在運行呢?哪些是主行程哪些是子行程呢?3個方法,我們先來看一下方法,后面再結合例子一起使用,
(1)獲取當前行程的編號:
需要用到一個os模塊里面的getpid()方法,用法如下:
os.getpid()
(2)獲取當前行程的名字
這里用的還是multiprocessing包,里面有個current_process()的方法,用法如下:
multiprocessing.current_process()
(3)獲取當前父行程(主行程)的編號
子行程是屬于哪個父行程的?這個用的是os模塊里面的getppid() ,用法如下:
os.getppid()

那么方法都看到了,我們來在剛才的例子的基礎上,獲取并列印一下當前行程的名字、編號以及父行程的編號,
import time
import multiprocessing
import os
# 2個要同時執行的函式
def music():
print("music子行程名字:", multiprocessing.current_process())
print("music子行程編號:", os.getpid())
print("music所屬主行程的編號:", os.getppid())
for i in range(5): # 執行5次
print("聽音樂中...")
time.sleep(0.2) # 延遲0.2s,目的是讓效果對比更明顯一些
def movie(a, b):
print("movie子行程名字:", multiprocessing.current_process())
print("movie子行程編號:", os.getpid())
print("movie所屬主行程的編號:", os.getppid())
for i in range(5):
print("看視頻中...")
time.sleep(0.2) # 延遲0.2s
if __name__ == "__main__": # 解決Windows系統下呼叫包時的遞回問題
# 創建子行程
music_process = multiprocessing.Process(target=music)
movie_process = multiprocessing.Process(target=movie, kwargs={"a": 30, "b": 40})
# 啟用行程
music_process.start()
movie_process.start()
print("主行程編號:",os.getpid())
運行結果:

可以只要我們使用獲取執行緒的方法的執行緒,都能被列印出來編號和名字,
🔰 (三)多執行緒Threading模塊
多行程能同時運行幾個任務,前面我們講過行程的最小單位是執行緒,那么執行緒也同樣可以進行多個任務,如果一個行程只有1個任務(主行程),那么也可以說是只有1個執行緒,就比如我們不使用多行程運行代碼的時候,這時候就可以說1個主行程或1個主執行緒,
1.多執行緒的類Thread類
多執行緒常用的一個模塊是threading,里面有個教Thread的類,跟前面我們將多行程時用到的Process類差不多,我們先來看看用法:
Thread(target=None,name=None,args=(),kwargs=None)
- target:可執行目標
- name:執行緒的名字默認Thread-N
- args/kwargs:目標引數
同樣的,多執行緒也要有開啟的方法,跟前面的也差不多:
start()
還有獲取執行緒名字的方法:
threading.current_thread()
知道了這些知識點,我們開始舉例:用跟上面差不多的例子去使用一下我們的多執行緒,
import threading,time
def music(name,loop):
for i in range(loop):
print("聽音樂 %s , 第%s次"%(name,i))
time.sleep(0.2)
def movie(name,loop):
for i in range(loop):
print("看電影%s , 第%s次"%(name,i))
time.sleep(0.2)
if __name__ =="__main__":
music_thread = threading.Thread(target=music,args=("最親的人",3))
movie_thread = threading.Thread(target=movie,args=("唐探2",3))
music_thread.start()
movie_thread.start()
print("主執行緒執行完畢")
運行結果:
聽音樂 最親的人 , 第0次
看電影唐探2 , 第0次
主執行緒執行完畢
聽音樂 最親的人 , 第1次看電影唐探2 , 第1次
看電影唐探2 , 第2次聽音樂 最親的人 , 第2次
可以看出來,我們的多執行緒其實是跟多行程差不多的,同樣可以運行多個任務,這里我們還增加了引數的使用,

2.繼承Thread類
我們除了用上面的方法實作多執行緒任務,還可以用繼承類的方式去實作多執行緒,
舉例:通過多執行緒的方式,去列印“涼涼”和“頭發沒了",
import threading,time
#多執行緒的創建
class MyThread(threading.Thread):
def __init__(self,name): #初始化
super().__init__() #呼叫父類Thread的初始化方法
self.name = name #name變成實體屬性
def run(self):
#執行緒要做的事情
for i in range(5):
print(self.name)
time.sleep(0.2)
#實體化子執行緒
t1 = MyThread("涼涼")
t2 = MyThread("頭發沒了")
t1.start()
t2.start()
MyThread這個類是我們自己創建的,它是繼承于父類threading.Thread ,同時我們需要寫上MyThread的初始化方法,每當被呼叫的時候把準備作業做好,super().int() 這個我們也講過了,在前面的面向物件時有講過,不懂的可以去看看面向物件那篇文章的內容,
運行結果:
涼涼
頭發沒了
涼涼
頭發沒了
涼涼頭發沒了
涼涼頭發沒了
涼涼
頭發沒了
隨機效果是有的,你們的效果和我的可能會不一樣,每臺電腦在運行多執行緒代碼時,哪個執行緒能夠搶到時間片誰就先執行,
通過類Thread繼承一樣可以實作多執行緒,
七、容器/迭代物件/生成器
🚩 (一)容器
在Python中,容器是把多種元素組織在一起的資料結構,容器中的元素就可以逐個迭代獲取,說白了,它的作用就像它的名字一樣:用來存放東西(資料),
容器實際上是不存在的,它并不是一種資料型別,只是人為的一種概念,只是為了方便學習所創造的一個概念詞,它可以用成員關系運算子(in或not in)來判斷物件是否在容器里面,
當然了,它不是我創造的,我沒有那么大本事哈,是官方創造的好吧,你也不用擔心我是在教你一些奇奇怪怪的名詞,說出去別人都聽不懂…python中都是這么叫的,

常見的容器型別有串列(list)、元組(tuple)、字串(str)、字典(dict)以及集合(set ),
既然容器里面的資料是可以迭代獲取的,那么我們又得來學一個新概念:可迭代物件,
🚩 (二)可迭代物件
什么是可迭代物件?
在python中,可迭代物件并不是指某種具體的資料型別,它是指存盤了元素的一個容器物件,
也就是說,如果容器里面沒有存盤資料,那它就不是可迭代物件,并不是所有的容器都是可迭代物件,容器包含但并不僅限于可迭代物件,
注意兩個點:
1.很多容器都是可迭代物件(容器包含了可迭代物件),
2.一個可迭代物件是不能獨立的進行迭代的,迭代是通過for來完成的,凡是可迭代物件都可以直接使用for回圈進行訪問,
for回圈大家應該不陌生吧?有沒有想過,for回圈內部是怎么實作的?比如說這個for回圈的例子,為什么能輸出串列里的每一個元素?它的內部是怎么實作的?

其實for回圈做了兩件事情:
1.使用 __iter__() 回傳1個迭代器,迭代器在下面會講,這里先知道有這么個東西,
2.使用 __next__() 獲取迭代器中的每一個元素,
那么我們不用for回圈來輸出串列里的每一個元素,
l = [1,2,3,4]
# for i in l:
# print(i)
ite =l.__iter__() #接收一下ietr()干了什么
print(ite) #列印
print(ite.__next__()) #for回圈干第2件事情的時候做的第1步
print(ite.__next__()) #for回圈干第2件事情的時候做的第2步
print(ite.__next__()) #for回圈干第2件事情的時候做的第3步
print(ite.__next__()) #for回圈干第2件事情的時候做的第4步
輸出結果:

可以看出來,如果我們去掉哪行列印ite的代碼,執行效果就是跟for回圈輸出串列里面的每一個元素是一樣的,for回圈里面限定了范圍是4次,實際上就執行了1次__iter__()和4次__next__(),也就是說for回圈訪問迭代物件的本質就是通過這么去實作的,
而且,for回圈本質上干的那兩件事情,缺一不可,也就是說如果沒有__iter__()先回傳了迭代器,__next()__也無法獲取到元素,恰恰說明了前面說要注意的兩點中的第2點:一個可迭代物件是不能獨立的進行迭代的,
有兩個內置函式跟它們原理是一樣的,本質相同,一般要用的話用內置函式要方便一些,起碼不用寫那么多下劃線:
內置函式 iter() 的本質是 __inter__() ,也是回傳一個迭代器,
內置函式 next() 的本質是 __next__(),也是有了迭代器之后獲取元素,

可以看出來結果也是一模一樣的,既然講到了迭代器,那么就來看看什么是迭代器,

🚩 (三)迭代器
通過上面的for回圈例子我們大概也能看得出來,
只要是實作了__iter__()和__next__()的物件,就是迭代器,迭代器是一個可迭代物件,
總之,迭代器是有__iter__()生成,可以通過__next__()進行呼叫,
既然如此,我們在學Python基礎的時候講過range()是一個可迭代物件,那么它也是可以通過__iter__()生成一個迭代器的,

🚩 (四)序列
序列在【賦值陳述句】那個專題文章中我有提過,這里再講一下,序列也是一個抽象的概念,它包含了串列、元組和字串,它本身是不存在的,也是便于學習所創造的一個概念詞,
可迭代物件包含序列,既然序列包含了串列、元組和字串,前面我們的例子中也涉及到 了,所以說序列可以被iter()和next()使用,
序列可以分為有限序列和無限序列,有限序列就是有范圍的,比如說range(10)就已經限定了范圍,相反的,無限序列也就是沒有限定范圍的序列,
我們來生成一個無限序列,這里需要用到1個新模塊itertools,itertools用于高效回圈的迭代函式集合,它下面有一個方法count(),可生成迭代器且無范圍,可以理解為無限迭代器,

通過這個例子我們可以看出來,只要執行一次,next()就會獲取一次迭代器里面的內容并逐次獲取,我這里只寫了4個next(),你多寫幾次就會多輸出幾次,
像next()這種什么時候需要就什么時候呼叫的機制叫做懶加載機制,也叫懶漢式加載;
相反地就有餓漢式加載,比如for回圈這種的,只要一執行就會把可迭代器里面的所有物件都獲取,
🚩 (五)串列推導式
串列推導式跟生成器有關,在講生成器之前,需要先知道什么是串列推導式,串列推導式就是生成串列的一種方法,語法是這樣的:
l = [i for i in 可迭代物件]
i表示要放進串列里的物件,for回圈是一個式子,
比如我們用串列推導式來生成一個串列試試:
l = [i for i in range(5)]
print(l)
運行結果:
[0, 1, 2, 3, 4]
運用串列推導式可以很方便地生成我們想要的串列,
同時它也有很多靈活的用法,比如在后面加上條件判斷
l = [i for i in range(5) if 4<5]
print(l)
運行結果:
[0, 1, 2, 3, 4]
if后面的條件判斷為真,則可以正常生成串列,如果為假,則串列推導式是無效的,此時的l將是一個空串列,
還有其他靈活的用法,比如操作前面的i,比如讓i的數值全都翻2倍:

我們把迭代物件換一下,換成字串,也同樣可以輸出,只是*在字串里面表示重復運算子,所以效果變成了這樣:

不僅如此,前面的i*2我們還可以用函式來進行操作,比如:

總而言之,串列推導式就是用來快速和自定義生成串列的一種方法,很靈活,
那么有人可能會舉一反三了,串列推導式都是用 [] 來進行操作的,那如果用()來操作行嗎?它會不會生成一個元組?我們來看看:

[] 換成()之后,回傳的是一個生成器generrator ,那么下面我們再來講講生成器:
🚩 (六)生成器
生成器是真實存在于Python中的物件,與容器這種概念詞是不同的,它是可以直接通過next()進行呼叫的,
1.生成器的第一種創建方法:生成器運算式
第一種創建方法跟串列推導式是差不多的,就是 [] 換成了():
l = (i for i in 可迭代物件)
比如我們來生成一個生成器,看看能不能用next()直接呼叫:
l = (i for i in "abcd")
print(next(l))
運行結果:
a
可以看出,生成器是可以直接呼叫的,那么既然生成器可以被next()呼叫,那么生成器就是一個特殊的迭代器,是一個可迭代物件,
2.生成器的第二種創建方法:yield
除了用上面那種方法創建生成器,還可以用yield來創建,方法如下:
yield 關鍵字
比如說我們用一個函式中包含yield來創建生成器:
def fun():
a = 10
while 1:
a += 1
yield a
b = fun()
print(b)
運行結果:
<generator object fun at 0x000001F2AD95E900>
結果就是生成了一個生成器,而且此時的函式fun()就已經不再是一個函式了,它是一個生成器,只要函式中出現了yield,函式就變成了生成器,
為什么while回圈沒有一直執行?先不著急,我們輸出看看:
def fun():
a = 10
while 1:
a += 1
yield a
b = fun()
print(next(b))
print(next(b))
print(next(b))
運行結果:
11
12
13
我呼叫了三次,所以它就運行了三次,while回圈雖然存在,但是卻不起作用,是因為前面我們提過的懶漢式加載,
什么時候需要了,什么時候用next()呼叫,就是懶漢式加載,不像餓漢式加載那樣,提前生成了所有物件,如果這里換成for回圈來完成,比如:
def fun():
a = 10
while 1:
a += 1
print(a)
b = fun()
運行之后程式將會進入死回圈,一直給a自加1,你可以試試看效果,這就是餓漢式加載提前生成了迭代器并呼叫了全部迭代器物件,餓漢式加載占用資源的放大鏡,

最后用一張圖來總結一下它們的關系:

八、修飾器
🏁 (一)修飾器的概念和作用
1.什么是修飾器?
修飾器又叫裝飾器,本身也是一個函式,是在原有的函式或者是方法上增添一些額外的功能,
2.修飾器的作用
概括的講,裝飾器的作用就是為已經存在的物件添加額外的功能,
比如說這個函式是注冊的功能,但有時候這個用戶在執行這個操作的時候,他是已注冊的用戶,我這個函式已經寫好了,不想動它了,那么我們就可以通過修飾器來給這個函式增加一個登錄的功能,
它經常用于有切面需求的場景,比如:插入日志、性能測驗、事務處理、快取、權限校驗等場景,裝飾器是解決這類問題的絕佳設計,有了裝飾器,我們就可以抽離出大量與函式功能本身無關的雷同代碼并繼續重用,

修飾器的具體操作,我們來慢慢學習,
🏁(二)修飾器的使用
1.使用說明
在使用修飾器之前,我們得記住幾個關于修飾器的使用說明:
(1)修飾器的關鍵字是 @ ,Python代碼中只要出現了它,你就可以想到是修飾器了,
(2)修飾器修飾的是函式或者是方法,不能修飾一個類,
(3)修飾器必須出現在被修飾函式或者方法的前一行,不能夠將修飾器定義在函式的同行,
例子:

雖然修飾器本身是一個函式,但它的出現是有規定的,我上面的修飾器就沒有出現在被修飾函式或者方法的前1行,所以連 print(“龍叔”) 這一行代碼都執行不了,
(4)修飾器本身是一個函式,將被修飾的函式作為引數,傳遞給修飾器,執行修飾器中功能,回傳傳遞進來的函式物件,呼叫回傳出來的函式,
這幾個點很重要,下面我們通過修飾器的多種使用方式來加深理解,

2.基本使用方式
如果被修飾的函式不呼叫,則執行@后面的函式,并把被修飾的函式當做引數傳遞過去了,則修飾器函式的回傳值可以是任意值,
例子:
def dec(a): #修飾器函式
print(a) #看一下形參傳了什么
print("helloworld")
return None
@dec #使用修飾器
def funA(): #被修飾的函式
pass
運行結果:
<function funA at 0x0000018D849BB670>
helloworld
首先我們可以看出來,這就是很簡單的一個修飾器的使用例子,用 @dec 來呼叫dec()修飾器函式,而被修飾的函式funA()是沒有進行什么操作的,
其次我們再看,被修飾的函式funA()不呼叫什么,但卻被當做引數傳遞給了修飾器函式dec(),所以dec()需要一個形參來接受傳遞值,我用的是a作為形參,如果你去掉a的話,系統就會報錯,因為被修飾的函式funA()回傳的值沒有東西接受,你可以試試,
最后還有1點,被修飾函式funA()并沒有被呼叫,注意到了嗎?所以修飾器函式dec()回傳什么都可以,上面回傳的是None,你回傳一個“有病”也可以的,回傳什么都可以的,下面我們再看被修飾函式 funA() 被呼叫的情況,

如果被修飾的函式呼叫了,直接執行修飾器,并把被修飾的函式當做引數專遞給修飾器,但是修飾器的回傳值必須是當前引數,
例子:
def dec(a):
print(a)
print("helloworld")
return "有病"
@dec #使用修飾器函式
def funA():
pass
funA() #呼叫被修飾函式
運行結果:
<function funA at 0x000001F78A48B670>
helloworld
Traceback (most recent call last):
File "E:\Python-learn\可迭代物件\修飾器\修飾器_test.py", line 11, in <module>
funA()
TypeError: 'str' object is not callable
出現錯誤了,說字串str不能被迭代,為什么?其實原因就是有修飾器存在并使用的情況下,被修飾函式又被呼叫了,這個時候回傳值就不能是任意值了,此時的修飾器函式只能回傳傳遞值,你可以試試把 return “有病” 改成 return a 就可以正常輸出了,又或者你去掉 funA() 這一行代碼,不呼叫被修斯函式,則輸出也是正常的,
def dec(a):
print(a)
print("helloworld")
return a
@dec
def funA():
pass
funA()
運行結果:
<function funA at 0x0000020F5664B670>
helloworld
一切正常,
我們再來看看修飾器的執行邏輯是怎么樣的:
def dec(a):
print(a)
print("修飾器函式")
return a
@dec
def funA():
print("被修飾函式")
funA()
print("龍叔")
運行結果:
<function funA at 0x000001D90E75B670>
修飾器函式
被修飾函式
龍叔
從這個運行結果我們可以看出修飾器的運行邏輯是:被修飾的函式 funA() 被呼叫了,但卻沒有被直接執行,而是直接執行修飾器dec,并把被修飾的函式 funA() 當做引數專遞給修飾器dec,執行修飾函式dec()里面的代碼,回傳傳遞值a后才執行的被修飾函式funA()里面的代碼,最后走完了被修飾函式funA() 才執行剩余的代碼,
我用個比較草一點的圖來表示一下:

3.其他使用方式:函式嵌套
前面我們講過了修飾器的基本使用方式,修飾器的使用方式很多,還可以使用函式嵌套的方式,
用個簡單例子來演示一下:
def A(x): #修飾器函式
def B(): #修飾器函式里面嵌套的函式
print("B")
B()
return x
@A #使用修飾器
def C(): #被修飾函式
print("C")
C() #呼叫被修飾函式
運行結果:
B
C
從運行結果來看,被修飾函式里面也是可以進行函式嵌套的,
4.其他使用方式:閉包
閉包的也是屬于函式里的一種,只是比較特殊,關于閉包的知識我這里就不復述了,在【Python基礎】里面我們有講過關于閉包的知識,忘了的可以去看看或者百度一下,我們來看看修飾器里面的閉包是怎么使用的,
def A(x):
def B():
print("B")
return x() # C 無差別呼叫只能一層,這里是無法通過x呼叫
return B
@A
def C():
print("C")
C()
運行結果:
B
C
可以看出在修飾器函式中,閉包也是可以正常使用的,

5.其他使用方式:被修飾的函式有引數的形式
如果被修飾的函式有引數傳遞,引數只能傳給修飾器函式里面的內嵌函式,
def A(x): #修飾器函式
print(x)
def B(aa, bbb): # 內嵌函式,接收被修飾函式傳遞的引數
print("B")
print(aa, bbb)
return x(aa, bbb) # C
return B
@A
def C(n, nn): #被修飾函式
print("C")
C("10", "20")
運行結果:
<function C at 0x00000206BED6B670>
B
10 20
C
可以看出來,雖然被修飾函式 C()傳遞了引數給修飾器函式 A() ,但是默認傳遞的還是C()這個物件,修飾器函式A()接受的還是被修飾函式C(),引數傳到了修飾器函式A()里面的函式B(),
就算你在A()里面加兩個形參,它也接受不了,只會報錯,前面在修飾器的使用說明我們已經說了,“被修飾的函式作為引數,傳遞給修飾器”,所以修飾器函式接受的只是被修飾函式這個物件,其他如果要傳遞引數,那么修飾器函式里面就就得有其他函式來接受傳遞的引數,
6.其他使用方式:有引數的修飾器,無引數的函式,使用內嵌函式收取引數
如果修飾器有引數但被修飾函式卻沒有引數的情況下,只能使用內嵌函式來收取引數,
def fun(a=20):
print(a)
def c(bb):
print(bb)
return bb # 可以無差別呼叫,因為是在第二層才接收的funB,相當于第一層
return c
@fun(30)
def funB():
print("xixixi")
funB()
運行結果:
30
<function funB at 0x0000025DAE4DD0D0>
xixixi
🏁(三)Python內置的修飾器
前面我們所講的都是我們自定義的修飾器,在Python中是有內置的修飾器,我們來學習一下常見的三種Python內置修飾器:staticmethod,classmethod,property,
它們的作用就是把類中的方法變為靜態方法,包括類的類屬性和類方法,具體的使用,我們來簡單的看一下,
1.property
property可以將方法變為屬性,被修飾的方法名必須和property下方的方法名一樣,property只能用于私有屬性,
我們來做一個對比,我們在不使用property的情況下,創建一個類并使用類里面的方法,是這樣的:
class A:
def __init__(self):
self.age = 20 #實體屬性
def getattr(self): #列印實體屬性
print(self.age)
def setattr(self,newage): #給實體屬性賦值
self.age = newage
a = A()
a.setattr(30)
a.getattr()
運行結果:
30
沒有什么問題,運行結果正常,那我們用內置修飾器property來試試看有何不同,
在使用內置修飾器property之前,我們得補充一個點:私有屬性,在函式內部創建的屬性是私有屬性,在不經過特殊處理之前,無法在函式外部使用,私有屬性前面加兩個下劃線表示,比如類里面的__name就表示私有屬性,我們通過一個簡單的例子來看看:

可以看出來,私有屬性在類內部是可以進行訪問的,在外部是不行的(做了一些處理之后是可以訪問的,這里就不介紹了,可以網上查一下),
回到我們的內置修飾器property,在前面不使用property的例子中,我們做一下修改,加入內置修飾器property,
class A:
def __init__(self):
self.__age = 20
@property
def age(self): #被修飾的方法
return self.__age
@age.getter
def age(self): #被修飾的方法
return self.__age
@age.setter
def age(self, newage): #被修飾的方法
self.__age = newage
a = A()
a.age = 200
print(a.age)
運行結果:
200
從這個例子我們可以看出,內置修飾器property是可以用于私有屬性,并且可以將方法變為屬性,比如a.age就是在呼叫屬性而不是方法了,在類中雖然多次出現了age()這個方法但卻沒有因為方法覆寫而報錯,也是因為property的存在,property規定了被修飾的方法名必須和property下方的方法名一樣,

2.staticmethod – 靜態方法
內置修飾器staticmethod是一個靜態方法,功能是將被修飾的方法從類中抽離出來,成為獨立的函式,該函式不能訪問類的屬性,
我們先寫一個簡單的類,通過使用與不使用staticmethod來做一個對比,不使用的情況下
class B:
def __init__(self, name):
self.name = name
def eat(self): # 列印傳遞的值
print(self.name)
b = B("龍叔")
b.eat()
運行結果:
龍叔
這個沒有什么問題,再來看看使用了staticmethod,直接加上看看:

報錯了,原因就是加上了@staticmethod 后,eat()這個方法就變成了一個普通的函式,它的位置雖然在類里面,但實際上卻相當于一個普通的函式,并不是類的方法了,所以你得給它傳遞一個值,不然形參self就沒有值傳遞從而報錯了,
正確的寫法應該是:

我們來總結一下這個staticmethod 靜態方法:
1. 這個函式是一個普通函式,只有這個類能用
2. 靜態方法可以設定引數,也可以不需要引數了(self)
3. 該函式不能訪問類的屬性
3.classmethod
被classmethod修飾的方法,與實體方法的區別是接收的第一個引數不是self,而是cls(當前類的具體型別),被修飾的方法無法訪問實體屬性,但是可以訪問類屬性,
class B:
age = 10
def __init__(self, name):
self.name = name
def sleep(self): #列印
print(self)
@classmethod
def eat(cls): # 被修飾的函式
print(cls) #看看傳遞的是類還是值
print(cls.age) #訪問類的屬性
print(self.name) #訪問實體物件的屬性
b = B("龍叔")
b.sleep()
b.eat()
運行結果:
<__main__.B object at 0x0000024FD7B3CFA0>
<class '__main__.B'>
10
Traceback (most recent call last):
File "D:\pythonProject1\專題2.py", line 21, in <module>
b.eat()
File "D:\pythonProject1\專題2.py", line 14, in eat
print(self.name)
NameError: name 'self' is not defined
通過結果可以看出,從sleep()和eat() 列印的物件來看,sleep()傳遞的物件是創建的實體物件b,而被修飾器修飾的函式eat()傳遞的是類;從eat()訪問類的屬性和實體屬性來看,訪問類的屬性是沒有問題的,但訪問實體物件的屬性時就報錯了,
所以驗證了前面講的:被classmethod修飾的方法,與實體方法的區別是接收的第一個引數不是self,而是cls(當前類的具體型別),被修飾的方法無法訪問實體屬性,但是可以訪問類屬性,
~
運行結果:
<__main__.B object at 0x0000024FD7B3CFA0>
<class '__main__.B'>
10
Traceback (most recent call last):
File "D:\pythonProject1\專題2.py", line 21, in <module>
b.eat()
File "D:\pythonProject1\專題2.py", line 14, in eat
print(self.name)
NameError: name 'self' is not defined
通過結果可以看出,從sleep()和eat() 列印的物件來看,sleep()傳遞的物件是創建的實體物件b,而被修飾器修飾的函式eat()傳遞的是類;從eat()訪問類的屬性和實體屬性來看,訪問類的屬性是沒有問題的,但訪問實體物件的屬性時就報錯了,
所以驗證了前面講的:被classmethod修飾的方法,與實體方法的區別是接收的第一個引數不是self,而是cls(當前類的具體型別),被修飾的方法無法訪問實體屬性,但是可以訪問類屬性,
今天的分享就到這里,歡迎大家在評論區留言交流!

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/323363.html
標籤:其他
上一篇:SpringBoot+WebSocket實時監控例外
下一篇:一個困擾程式員很多年的問題:微服務領域SpringCloud這么火,為何還要學習SpringCloud Alibaba?
