本文假設你已經有一門物件導向程式語言基礎,如Java
等,且希望快速瞭解並使用Python
語言。本文對重點語法和資料結構以及用法進行詳細說明,同時對一些難以理解的點進行了圖解,以便大家快速入門。一些較偏的知識點在大家入門以後根據實際需要再查詢官方文件即可,學習時切忌鬍子眉毛一把抓。同時,一定要跟著示例多動手寫程式碼。學習一門新語言時推薦大家同時去刷leetcode
,一來可以快速熟悉新語言的使用,二來也為今後找工作奠定基礎。推薦直接在網頁上刷leetcode
,因為面試的時候一般會讓你直接在網頁編寫程式碼。leetcode
刷題路徑可以按我推薦的方式去刷。以下程式碼中,以 >>>
和 ...
開頭的行是互動模式下的程式碼部分,>?
開頭的行是互動模式下的輸入,其他行是輸出。python
程式碼中使用 #
開啟行註釋。
模組
模組是包含 Python
定義和語句的檔案,檔案字尾名為 .py
,檔名即是模組名。在pycharm
中建立名為python-learn
的專案,然後建立fib.py
檔案,並輸入以下程式碼後儲存:
# 斐波拉契數列
def print_fib(n: int) -> None:
"""列印斐波拉契數列
:param n: 數列截至範圍
:return: None
"""
a, b = 0, 1
while a < n:
print(a, end=' ')
a, b = b, a+b
print()
def get_fib(n: int) -> list:
"""獲取斐波拉契數列
:param n: 數列截至範圍
:return: 存有數列的list
"""
result = []
a, b = 0, 1
while a < n:
result.append(a)
a, b = b, a+b
return result
接著,開啟pycharm
中的Python Console
,使用import
語句匯入該模組:
# 匯入 fib 模組
>>> import fib
# 使用 fib 模組中定義的函式
>>> fib.print_fib(10)
0 1 1 2 3 5 8
>>> fib.get_fib(10)
[0, 1, 1, 2, 3, 5, 8]
# 如果經常使用某個函式,可以把它賦值給區域性變數
>>> print_fib = fib.print_fib
>>> print_fib(10)
0 1 1 2 3 5 8
# 每個模組中都有一個特殊變數 __name__ 記錄著模組的名字
>>> print(fib.__name__)
fib
使用as
關鍵字為匯入的模組指定別名:
# 匯入 fib 並指定別名為 fibonacci
>>> import fib as fibonacci
>>> fibonacci.get_fib(10)
[0, 1, 1, 2, 3, 5, 8]
# 模組名依然為 fib
>>> print(fibonacci.__name__)
fib
當我們通過import
語句匯入模組時:
- 首先查詢要匯入的模組是否為內建模組;
- 沒找到就會根據
sys.path
(list)中的路徑繼續查詢。(sys.path
預設值包含:當前路徑、環境變數PYTHONPATH
中的路徑等)
檢視sys.path
的值:
>>> print("======= start =======")
... for path in sys.path:
... print(path)
... print("======= end =======")
======= start =======
C:\software\jetbrains\PyCharm 2021.2.3\plugins\python\helpers\pydev
C:\software\jetbrains\PyCharm 2021.2.3\plugins\python\helpers\pycharm_display
C:\software\jetbrains\PyCharm 2021.2.3\plugins\python\helpers\third_party\thriftpy
C:\software\jetbrains\PyCharm 2021.2.3\plugins\python\helpers\pydev
C:\software\anaconda3\envs\python-learn\python310.zip
C:\software\anaconda3\envs\python-learn\DLLs
C:\software\anaconda3\envs\python-learn\lib
C:\software\anaconda3\envs\python-learn
C:\software\anaconda3\envs\python-learn\lib\site-packages
C:\software\jetbrains\PyCharm 2021.2.3\plugins\python\helpers\pycharm_matplotlib_backend
D:\code\python\python-learn # fib 模組所在目錄
D:/code/python/python-learn # 對應linux路徑形式
======= end =======
當我們要匯入的模組路徑不在sys.path
中時,通過append
或extend
函式可以將目標路徑手動加入sys.path
中。前面的例子裡,為什麼我們沒有手動將專案路徑加入sys.path
中就可以匯入fib
模組呢?答案是pycharm
幫我們做了。在專案中,當我們開啟Python Console
時,pycharm
執行了以下指令碼:
sys.path.extend(['D:\\code\\python\\python-learn', 'D:/code/python/python-learn'])
注意:為了保證執行效率,每次直譯器會話只匯入一次模組。如果更改了模組內容,必須重啟直譯器;僅互動測試一個模組時,也可以使用 importlib.reload()
,例如 import importlib; importlib.reload(modulename)
。
以指令碼方式執行模組
.py
檔案(模組)還可以通過python
直譯器以指令碼的方式執行。在pycharm
專案中開啟Terminal
並執行以下命令可以解釋執行fib
模組(也可點選圖形介面的執行按鈕):
python fib.py
執行fib.py
時,直譯器從上到下依次解釋執行,由於程式碼中沒有任何輸出動作所以終端沒有任何輸出。
當一個.py
檔案(模組)被當作指令碼執行時,會被認為是程式的入口,類似於其他語言中的main
函式,於是python
直譯器會將該模組的特殊變數__name__
置為__main__
。
現在,我們給fib.py
檔案新增一些內容:
# 斐波拉契數列
def print_fib(n: int) -> None:
"""列印斐波拉契數列
:param n: 數列截至範圍
:return: None
"""
a, b = 0, 1
while a < n:
print(a, end=' ')
a, b = b, a+b
print()
def get_fib(n: int) -> list:
"""獲取斐波拉契數列
:param n: 數列截至範圍
:return: 存有數列的list
"""
result = []
a, b = 0, 1
while a < n:
result.append(a)
a, b = b, a+b
return result
# 只有當前檔案(模組)被當作指令碼執行時 if 語句才為真
if __name__ == "__main__":
import sys
# argv[0]始終為檔名
print(sys.argv[0], end=" ")
# 傳入的第一個引數
print(sys.argv[1], end=" ")
# 傳入的第二個引數
print(sys.argv[2])
# 測試 print_fib 函式
print_fib(int(sys.argv[1]))
# 測試 get_fib 函式
fibSeries = get_fib(int(sys.argv[2]))
print(fibSeries)
輸入以下命令並執行:
PS D:\code\python\python-learn> python fib.py 10 20
fib.py 10 20
0 1 1 2 3 5 8
[0, 1, 1, 2, 3, 5, 8, 13]
包
如果你瞭解Java
,那麼Python
中的包你就不會感到陌生。包是名稱空間的一種實現方式,不同包中的同名模組互不影響。包的寫法類似於A.B.C
,A
是包,B
是A
的子包,C
可以是B的子包也可以是模組。包在磁碟上的表現就是目錄或者說是路徑,以包結構A.B.C
為例,若C
為模組,那麼對應的路徑為專案路徑/A/B/C.py
。同時Python
只把含 __init__.py
檔案的目錄當成包。(後面解釋這個檔案的用處)
以之前建立的python-learn
專案為例,在根目錄下建立包com.winlsr
,然後將fib.py
移動到com.winlsr
下,目錄結構如下:
從包中匯入fib
模組:
# 匯入 fib 模組
>>> import com.winlsr.fib
# 使用時必須引用模組的全名
>>> com.winlsr.fib.print_fib(10)
0 1 1 2 3 5 8
使用from package import ...
匯入模組、函式(建議重啟一下直譯器):
# 匯入 fib 模組
>>> from com.winlsr import fib
# 使用時直接輸入模組名
>>> fib.print_fib(10)
0 1 1 2 3 5 8
# 直接匯入 fib 模組中的 print_fib 函式
>>> from com.winlsr.fib import print_fib
# 直接使用方法即可
>>> print_fib(10)
0 1 1 2 3 5 8
使用 from package import item
時,item 可以是包的子模組(或子包),也可以是包中定義的函式、類或變數等其他名稱。使用 import item
時,item
只可以是模組或包。有的同學會疑問,匯入包有什麼用?以當前專案為例,我們匯入com
包(建議重啟一下直譯器):
# 匯入 com 包
>>> import com
>>> com
# 包其實也是模組,對應的檔案是包下的 __init__.py 檔案?
<module 'com'="" from="" 'd:\\code\\python\\python-learn\\com\\__init__.py'="">
# 驗證猜想:
# 將該 __init__.py 檔案中新增如下內容
def print_info():
print("name :", __name__)
# 重啟直譯器後再次匯入 com 包
>>> import com
# 成功呼叫 __init__.py 檔案中定義的函式,猜想正確
>>> com.print_info()
name : com
從包中匯入 *
from package import *
不會匯入package
中的任何模組或子包,除非你在該package
下的__init__.py
檔案中新增了如下顯示說明:
__all__ = ["子模組名1", "子包名1"]
新增該說明後,執行from package import *
語句會匯入指定的子模組、子包。
同樣以之前建立的python-learn
專案為例,我們執行如語句(建議重啟一下直譯器):
# 希望匯入 com.winlsr 包下的 fib 模組
>>> from com.winlsr import *
# 發現並沒有匯入
>>> fib
Traceback (most recent call last):
File "<input>", line 1, in <module>
NameError: name 'fib' is not defined
在 com.winlsr
下的__init__.py
檔案中新增如下內容:
__all__ = ["fib"]
執行同樣的語句(建議重啟一下直譯器):
>>> from com.winlsr import *
# 匯入成功
>>> fib
<module 'com.winlsr.fib'="" from="" 'd:\\code\\python\\python-learn\\com\\winlsr\\fib.py'="">
注意:通常不建議採用該小結講解的方法匯入模組或子包,而是採用from package import specific_submodule
。
dir()
函式
內建函式 dir()
用於查詢模組或子包中定義的名稱(變數、模組(子包)、函式等),返回結果是經過排序的字串列表。沒有引數時,dir()
列出當前定義的名稱。
>>> from com.winlsr import fib
>>> dir(fib)
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'get_fib', 'print_fib']
>>> dir()
['__builtins__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'fib', 'sys']
類
封裝
Python
中定義類使用class
關鍵字,類中可以定義變數和函式,起到封裝的作用。沿用之前建立的專案,在com.winlsr
包中建立rectangle
模組並定義矩形類:
class Rectangle:
# 一個類只能有一個建構函式
def __init__(self): # 無參構造,self 類似於 this,指向例項物件本身
self.length = 0.0 # 建構函式中對例項物件的成員變數width和length進行初始化
self.width = 0.0 # 只有建立的例項才有這兩個變數
# 計算面積
def area(self):
return self.length * self.width
# 計算周長
def perimeter(self):
return (self.length + self.width) * 2
開啟Python Console
,輸入以下語句來使用Rectangle
類:
>>> from com.winlsr.rectangle import Rectangle
# 使用無參構造建立類的例項物件
>>> rec = Rectangle()
# 呼叫rec例項的方法
>>> rec.perimeter()
0.0
>>> rec.area()
0.0
>>> rec.length
0.0
# Rectangle 類中沒有length這個成員變數,只有例項物件中才有
>>> Rectangle.length
Traceback (most recent call last):
File "<input>", line 1, in <module>
AttributeError: type object 'Rectangle' has no attribute 'length'
Python
中例項物件的成員變數也可以不在__init__()
中定義和初始化,而是直接在類中定義並初始化。原來的Rectangle
類可以改寫為:
class Rectangle:
# 成員變數直接定義在類中並初始化,類和例項都有兩個變數
length = 0.0
width = 0.0
def area(self):
return self.length * self.width
def perimeter(self):
return (self.length + self.width) * 2
注意,如果變數直接定義在類中,那麼建立例項時,例項中的變數是類中變數的淺拷貝。重啟直譯器執行以下語句:
>>> from com.winlsr.rectangle import Rectangle
>>> rec0 = Rectangle()
>>> rec0.width is Rectangle.width
True
>>> rec1 = Rectangle()
>>> rec1.length is Rectangle.length
True
例項rec0
和rec1
建立後的記憶體分佈如下:
對例項rec0
和rec1
中的成員變數進行修改:
# 修改例項的成員變數
>>> rec0.width = 1.0
... rec0.length = 2.0
... rec1.width = 3.0
... rec1.length = 4.0
# 列印
>>> print(Rectangle.width)
... print(Rectangle.length)
... print(rec0.width)
... print(rec0.length)
... print(rec1.width)
... print(rec1.length)
0.0
0.0
1.0
2.0
3.0
4.0
根據結果可以發現rec0
和rec1
例項之間的成員變數(不可變 immutable 型別)相互之間不影響,修改後它們的記憶體分佈如下:
注意,如果直接定義在類中的變數是可變(mutable)型別,使用時就應該謹慎。如下,依然在com.winlsr
包中建立actor
模組並定義演員類:
class Actor:
# 參演的電影
movies = []
def get_movies(self):
return self.movies
def add_movie(self, movie):
self.movies.append(movie)
使用Actor
類:
>>> from com.winlsr.actor import Actor
... lixiaolong = Actor()
... xuzheng = Actor()
... lixiaolong.add_movie("猛龍過江")
... xuzheng.add_movie("我不是藥神")
... print(lixiaolong.get_movies())
... print(xuzheng.get_movies())
# 發現兩個物件的 movies list 是共享的
['猛龍過江', '我不是藥神']
print(xuzheng.get_movies())
['猛龍過江', '我不是藥神']
以上情況是因為多個Actor
類的例項中的movies
變數(引用)指向同一個list
物件,例項lixiaolong
和xuzheng
記憶體分佈如下:
建立後例項後,呼叫add_movie()
之前的記憶體分佈:
呼叫add_movie()
之後的記憶體分佈:
為了使每個演員的參演電影列表相互獨立,在建立Actor
類的例項時應該為每個例項建立一個新的list
:
class Actor:
# 參演的電影
# 該語句沒有用,可以刪掉。例項在建立時會通過建構函式改變指向movies的指向
movies = []
def __init__(self):
self.movies = []
def get_movies(self):
return self.movies
def add_movie(self, movie):
self.movies.append(movie)
重啟直譯器,再次執行以下語句:
>>> from com.winlsr.actor import Actor
... lixiaolong = Actor()
... xuzheng = Actor()
... lixiaolong.add_movie("猛龍過江")
... xuzheng.add_movie("我不是藥神")
... print(lixiaolong.get_movies())
... print(xuzheng.get_movies())
['猛龍過江']
['我不是藥神']
綜上,根據前面的示例,如果你不希望多個例項之間共享變數,建議直接將變數定義在__init__
函式中。最後,Python
支援靜態語言不支援的例項屬性動態繫結特性:
# 給 lixiaolong 這個例項動態新增 age 屬性
>>> lixiaolong.age = 41
>>> lixiaolong.age
41
# 不影響其他例項
>>> xuzheng.age
Traceback (most recent call last):
File "<input>", line 1, in <module>
AttributeError: 'Actor' object has no attribute 'age'
訪問控制
前面的例項中,我們可以通過instance.var
的形式來直接訪問例項的成員變數。但通常我們不希望例項中的成員變數被直接訪問,而是通過getter
和setter
來訪問,這需要我們將成員變數設定為private
。
Python
中:
- 帶有一個下劃線的變數,形如
_var
應該被當作是API
(常用於模組中)的非公有部分 (函式或是資料成員)。雖然可以正常訪問,但我們應遵循這樣一個約定; - 類中私有成員變數應當用兩個字首下劃線,至多一個字尾下劃線標識,形如:
__var
。但該變數並不是真正的不能訪問,這是因為Python
實現的機制是”名稱改寫“。這種機制在執行時會將__var
改為_classname__var
,但你仍然可以通過_classname__var
來訪問。 - 形如
__var__
的變數是特殊變數,可以訪問,但通常我們不需要定義此類變數。
實驗驗證,將Rectangle
類改為如下程式碼:
class Rectangle:
def __init__(self, length, width):
self.__length = length
self.__width = width
def area(self):
return self.__length * self.__width
def perimeter(self):
return (self.__length + self.__width) * 2
重啟直譯器,執行如下語句:
>>> from com.winlsr.rectangle import Rectangle
>>> rec = Rectangle(12.0, 24.0)
# 直接訪問私有變數 __width,失敗
>>> print(rec.__width)
Traceback (most recent call last):
File "<input>", line 1, in <module>
AttributeError: 'Rectangle' object has no attribute '__width'
# 訪問私有變數改寫後的名稱 _Rectangle__width,成功
>>> print(rec._Rectangle__width)
24.0
對應Python Console
如下:
繼承
Python
繼承語法如下,所有類都預設繼承:
class DerivedClassName(BaseClassName):
<statement-1>
.
.
.
<statement-n>
下面我們在com.winlsr
包下建立person
模組並定義Person
類,作為Actor
和Teacher
類的基類:
class Person:
def __init__(self, name, id_number):
self.__name = name
self.__id_number = id_number
def set_name(self, name):
self.__name = name
def get_name(self):
return self.__name
def set_id_number(self, id_number):
self.__id_number = id_number
def get_id_number(self):
return self.__id_number
在com.winlsr
包下建立actor
模組並定義Actor
類,它是Person
類的派生類:
from .person import Person
class Actor(Person):
def __init__(self, name, id_number):
self.__movies = []
# 三種呼叫父類建構函式的方式
super().__init__(name, id_number)
# super(Actor, self).__init__(name, id_number)
# Person.__init__(self, name, id_number)
def add_movie(self, movie):
self.__movies.append(movie)
def print_info(self):
print(self.get_name(), self.get_id_number(), self.__movies, sep=" : ")
在com.winlsr
包下建立teacher
模組並定義Teacher
類,它也是Person
類的派生類:
from .person import Person
class Teacher(Person):
# 無建構函式,建立物件時會呼叫父類建構函式
def print_info(self):
print(self.get_name(), self.get_id_number(), sep=" : ")
重啟直譯器,執行如下語句:
>>> from com.winlsr.actor import Actor
>>> actor = Actor("xuzheng", 123456789)
>>> actor.add_movie("我不是藥神")
>>> actor.print_info()
xuzheng : 123456789 : ['我不是藥神']
>>> from com.winlsr.teacher import Teacher
>>> teacher = Teacher()
# Teacher 類中沒有定義 __init__(),這裡會呼叫父類 Person 的構造並傳入引數
>>> teacher = Teacher("lsl", 123459876)
>>> teacher.print_info()
lsl : 123459876
對應Python Console
如下:
Python
也支援一種多重繼承。 帶有多個基類的類定義語句如下所示:
class DerivedClassName(Base1, Base2, Base3):
<statement-1>
.
.
.
<statement-n>
派生類例項如果某一屬性在 DerivedClassName
中未找到,則會到 Base1
中搜尋它,然後(遞迴地)到 Base1
的基類中搜尋,如果在那裡未找到,再到 Base2
中搜尋,依此類推。
其他
結語
教程到這裡就結束了,最後推薦大家再去看看廖雪峰老師講解的異常處理
和IO
的內容(也可以用到的時候再看),他比官網講解的更有條理。學完這些內容就基本入門了,今後可以根據自己應用的領域再進一步學習即可,比如深度學習、web開發等。