Python 3 快速入門 3 —— 模塊與類


本文假設你已經有一門面向對象編程語言基礎,如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中時,通過appendextend函數可以將目標路徑手動加入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.CA是包,BA的子包,C可以是B的子包也可以是模塊。包在磁盤上的表現就是目錄或者說是路徑,以包結構A.B.C為例,若C為模塊,那么對應的路徑為項目路徑/A/B/C.py。同時Python只把含 __init__.py 文件的目錄當成包。(后面解釋這個文件的用處)

以之前創建的python-learn項目為例,在根目錄下創建包com.winlsr,然后將fib.py移動到com.winlsr下,目錄結構如下:

image-20211129185325055

從包中導入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

實例rec0rec1創建后的內存分布如下:

image-20211201101121363

對實例rec0rec1中的成員變量進行修改:

# 修改實例的成員變量
>>> 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

根據結果可以發現rec0rec1實例之間的成員變量(不可變 immutable 類型)相互之間不影響,修改后它們的內存分布如下:

image-20211201110133650

注意,如果直接定義在類中的變量是可變(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對象,實例lixiaolongxuzheng內存分布如下:

創建后實例后,調用add_movie()之前的內存分布:

image-20211130222432854

調用add_movie()之后的內存分布:

image-20211130223038163

為了使每個演員的參演電影列表相互獨立,在創建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的形式來直接訪問實例的成員變量。但通常我們不希望實例中的成員變量被直接訪問,而是通過gettersetter來訪問,這需要我們將成員變量設置為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如下:

image-20211201142925442

繼承

Python繼承語法如下,所有類都默認繼承:

class DerivedClassName(BaseClassName):
    <statement-1>
    .
    .
    .
    <statement-n>

下面我們在com.winlsr包下創建person模塊並定義Person類,作為ActorTeacher類的基類:

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如下:

image-20211201195241710

Python 也支持一種多重繼承。 帶有多個基類的類定義語句如下所示:

class DerivedClassName(Base1, Base2, Base3):
    <statement-1>
    .
    .
    .
    <statement-n>

派生類實例如果某一屬性在 DerivedClassName 中未找到,則會到 Base1 中搜索它,然后(遞歸地)到 Base1 的基類中搜索,如果在那里未找到,再到 Base2 中搜索,依此類推。

其他

推薦學習官方文檔的迭代器生成器生成器表達式

結語

教程到這里就結束了,最后推薦大家再去看看廖雪峰老師講解的異常處理IO的內容(也可以用到的時候再看),他比官網講解的更有條理。學完這些內容就基本入門了,今后可以根據自己應用的領域再進一步學習即可,比如深度學習、web開發等。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM