Python進階版:定義類時應用的9種最佳做法

2020-12-13 讀芯術

全文共9544字,預計學習時長24分鐘

圖源:unsplash

作為一種OOP語言,Python通過支持以對象為主的各種功能來處理數據和功能。例如,數據結構是所有對象,包括原始類型(例如整數和字符串),而在某些其他語言中,原始類型則不視為對象。對於另一個實例,函數是所有對象,它們僅僅是定義了其他對象的屬性(例如類或模塊)。

儘管可以使用內置數據類型,而且無需創建任何自定義類就能編寫一組函數,但隨著項目範圍的擴大,代碼可能會越來越難維護。這些單獨代碼部分的主題並不相同,儘管有很多信息是相關的,但管理它們之間的聯繫卻並不簡單。

在這些情況下,定義自己的類就很划得來了,這樣一來你可以對相關信息進行分組並且改善項目的結構設計。而且由於你即將處理更少的分段代碼,代碼庫的長期可維護性將得到改善。但要注意,僅當以正確方式完成類聲明時,操作才可以實現,定義自定義類的益處才能超過管理它們的支出。

1.好的命名

定義自己的類,就好比在代碼庫中添加了一位新成員。因此應該給類起個好名字。雖然類名的唯一限制是合法Python變量的規則(例如,不能以數字開頭),但是有一些好用的方法來命名類。

· 使用易於發音的名詞。在參與團隊項目時,這一點尤其重要。在小組演講中,你恐怕不願意這樣講:「在這種情況下,我們創建Zgnehst類的實例。」 另外,易於發音也意味著名稱不應太長,使用三個以上的單詞來定義類名簡直無法想像。一個字是最佳,兩個字其次,三個字不能再多啦!

· 反映其存儲的數據和預期功能。就像在現實生活中一樣——當看到男性化的名字時,我們就會默認這個孩子是男孩。同樣的方式也適用於類名(或通常的任何其他變量),命名規則很簡單——不要讓人感覺奇怪。如果要處理學生的信息,那麼該課程應該命名為Student,KiddosAtCampus並不是一個常規的好名字。

· 遵循命名約定。應該對類名使用駱駝拼寫法,例如GoodName。以下是非常規類名稱的不完整列表:goodName,Good_Name,good_name以及GOodnAme。遵循命名約定是為了使意圖表現明確。在別人閱讀你的代碼時,可以毫無疑問地假定命名為GoodName的對象是一個類。

也有適用於屬性和功能的命名規則和約定,以下各節將在使用情況下簡要提及,但是總體原理是相同的。

2.顯式實例屬性

在大多數情況下,我們都想定義自己的實例初始化方法(即__init__)。在此種方法中,設置了新創建的類實例的初始狀態。但是,Python並沒有限制可以在何處使用自定義類定義實例屬性。換句話說,你可以在創建實例之後的後續操作中定義其他實例屬性。

classStudent:

def__init__(self, first_name, last_name):

self.first_name = first_name

self.last_name = last_name

defverify_registration_status(self):

status = self.get_status()

self.status_verified = status =="registered"

defget_guardian_name(self):

self.guardian ="Goodman"

defget_status(self):

# get the registration status from a database

status =query_database(self.first_name, self.last_name)

return status

初始化方法

如上所示,可以通過指定學生的名字和姓氏來創建「學生」類的實例。稍後,在調用實例方法(即verify_registration_status)時,將設置「學生實例」的status屬性。

但這不是理想的模式,因為如果在整個類中散布了各種實例屬性,那麼該類就無法明確實例對象擁有哪些數據。因此,最佳做法是將實例的屬性放在__init__方法中,這樣代碼閱讀器就可以通過單一位置來了解你的類的數據結構,如下所示:

classStudent:

def__init__(self, first_name, last_name):

self.first_name = first_name

self.last_name = last_name

self.status_verified =None

self.guardian =None

更好的初始化方法

對於最初無法設置的那些實例屬性的問題,可以使用佔位符值(例如None)進行設置。儘管沒什麼好擔心的,但是當忘記調用某些實例方法來設置適用的實例屬性時,此更改還有助於防止可能的錯誤,從而導致AttributeError(『Student』 object has noattribute 『status_verified』)。

在命名規則方面,應使用小寫字母命名屬性,並遵循蛇形命名法——如果使用多個單詞,請在它們之間使用下劃線連接。此外,所有名稱都應對其存儲的數據有具有意義的指示(例如first_name比fn更好)。

3.使用屬性——但要精簡

圖源:unsplash

有些人在具備其他OOP語言(例如Java)背景的情況下學習Python編碼,並且習慣於為實例的屬性創建getter和setter。可以通過在Python中使用屬性裝飾器來模仿這一模式。以下代碼展示了使用屬性裝飾器實現getter和setter的基本形式:

classStudent:

def__init__(self, first_name, last_name):

self.first_name = first_name

self.last_name = last_name

@property

defname(self):

print("Getter for the name")

returnf"{self.first_name}{self.last_name}"

@name.setter

defname(self, name):

print("Setter for the name")

self.first_name, self.last_name = name.split()

屬性裝飾

創建此屬性後,儘管它是通過內部函數實現的,我們仍然可以使用點符號將其用作常規屬性。

>>> student =Student("John", "Smith")

... print("StudentName:", student.name)

... student.name ="JohnnySmith"

... print("Aftersetting:", student.name)

...

Getterfor the name

StudentName: JohnSmith

Setterfor the name

Getterfor the name

After setting: JohnnySmith

使用屬性

使用屬性實現的優點包括驗證正確的值設置(檢查是否使用字符串,而不是使用整數)和只讀訪問權限(通過不實現setter方法)。但應該同時使用屬性,如果自定義類如下所示,可能會很讓人分心——屬性太多了!

classStudent:

def__init__(self, first_name, last_name):

self._first_name = first_name

self._last_name = last_name

@property

deffirst_name(self):

return self._first_name

@property

deflast_name(self):

return self._last_name

@property

defname(self):

returnf"{self._first_name}{self._last_name}"

濫用屬性

在大多數情況下,這些屬性可以用實例屬性代替,因此我們可以訪問它們並直接設置它們。除非對使用上述屬性的好處有特定的需求(例如:值驗證),否則使用屬性優先於在Python中創建屬性。

4.定義有意義的字符串表示法

在Python中,名稱前後帶有雙下劃線的函數稱為特殊方法或魔術方法,有些人將其稱為dunder方法。這些方法對解釋器的基本操作有特殊的用法,包括我們先前介紹的__init__方法。__repr__和__str__這兩種特殊方法對於創建自定義類的正確字符串表示法至關重要,這將為代碼閱讀器提供有關類的更直觀信息。

它們之間的主要區別在於__repr__方法定義了字符串,你可以使用該字符串通過調用eval(repr(「therepr」))重新創建對象,而__str__方法定義的字符串則更具描述性,並允許更多定製。換句話說,你可以認為__repr__方法中定義的字符串由開發人員查看,而__str__方法中使用的字符串由常規用戶查看。請看以下示例:

classStudent:

def__init__(self, first_name, last_name):

self.first_name = first_name

self.last_name = last_name

def__repr__(self):

returnf"Student({self.first_name!r}, {self.last_name!r})"

def__str__(self):

returnf"Student: {self.first_name}{self.last_name}"

字符串表示法的實現

請注意,在__repr__方法的實現中,f字符串使用!r來顯示帶引號的這些字符串,因為使用格式正確的字符串構造實例很有必要。如果不使用!r格式,則字符串將為Student(John, Smith),這不是構造「學生」實例的正確方法。

來看看這些實現如何為我們顯示字符串:在交互式解釋器中訪問對象時會調用__repr__方法,而在列印對象時默認會調用__str__方法。

>>> student =Student("David", "Johnson")

>>> student

Student('David', 'Johnson')

>>>print(student)

Student: DavidJohnson

字符串表示法

5.實例方法,類方法和靜態方法

在一個類中,我們可以定義三種方法:實例方法、類方法和靜態方法。我們需要考慮針對所關注的功能應使用哪些方法,以下是一些常規準則。

圖源:unsplash

例如,如果方法與單個實例對象有關,那麼需要訪問或更新實例的特定屬性。在這種情況下,應使用實例方法。這些方法具有如下簽名:def do_something(self):,其中self自變量引用調用該方法的實例對象。

如果方法與單個實例對象無關,則應考慮使用類方法或靜態方法。可以使用適用的修飾符輕鬆定義這兩種方法:類方法(classmethod)和靜態方法(staticmethod)。

兩者之間的區別在於,類方法允許你訪問或更新與類相關的屬性,而靜態方法則獨立於任何實例或類本身。類方法的一個常見示例是提供一種方便的實例化方法,而靜態方法可以只是一個實用函數。請看以下代碼示例:

classStudent:

def__init__(self,first_name, last_name):

self.first_name = first_name

self.last_name = last_name

defbegin_study(self):

print(f"{self.first_name}{self.last_name} beginsstudying.")

@classmethod

deffrom_dict(cls,name_info):

first_name = name_info['first_name']

last_name = name_info['last_name']

returncls(first_name,last_name)

@staticmethod

defshow_duties():

return"Study,Play, Sleep"

不同的方法

也可以用類似的方式創建類屬性。與前面討論的實例屬性不同,類屬性由所有實例對象共享,並且它們應當反映一些獨立於各個實例對象的特徵。

6.使用私有屬性進行封裝

在為項目編寫自定義類時,需要考慮封裝問題,尤其期望其他人也使用你的類的話就更應如此。當類的功能增長時,某些功能或屬性僅與類內數據處理相關。換句話說,除了類之外,這些函數都將不會被調用,並且除你之外其他使用類的用戶甚至不會在意這些函數的實現細節。在這些情況下,應該考慮封裝。

按照慣例,應用封裝的一種重要方法是為屬性和函數加上下劃線或兩個下劃線。二者之間有著細微的區別:帶有下劃線的被認為是受保護的,而帶有兩個下劃線的被認為是私有的,這涉及在創建後進行名稱處理。

從本質上來說,像這樣命名屬性和功能,是在告訴IDE(即集成開發環境,例如PyCharm),儘管在Python中不存在真正的私有屬性,但它們不會在類之外被訪問。

classStudent:

defget_mean_gpa(self):

grades = self._get_grades()

gpa_list =Student._converted_gpa_from_grades(grades)

returnsum(gpa_list) /len(gpa_list)

def_get_grades(self):

# fetch grades from a database

grades = [99, 100, 94, 88]

return grades

@staticmethod

def_converted_gpa_from_grades(grades):

# convert the grades to GPA

gpa_list = [4.0, 4.0, 3.7, 3.4]

return gpa_list

封裝

上面的代碼展示了一個簡單的封裝示例。如果想了解學生的評價GPA,那麼我們可以使用get_mean_gpa方法獲得GPA。用戶不需要知道平均GPA的計算方式,我們可以通過在函數名稱前添加下劃線來保護相關方法。

這一最佳做法的主要收穫是,與用戶使用你的代碼相關的公共API,僅公開最少的數量。對於僅在內部使用的那些代碼,請將其設置為受保護的方法或私有方法。

圖源:unsplash

7.分離關注點和解耦

隨著項目的發展,你會發現自己正在處理更多數據,如果你只堅持使用一個類會變得很麻煩。繼續以「學生」類為例,假設學生在學校吃午餐,並且每個人都有一個餐飲帳戶,可以用來支付餐費。從理論上講,我們可以處理學生類中與帳戶相關的數據和功能,如下所示:

classStudent:

def__init__(self, first_name, last_name, student_id):

self.first_name = first_name

self.last_name = last_name

self.student_id = student_id

defcheck_account_balance(self):

account_number =get_account_number(self.student_id)

balance =get_balance(account_number)

return balance

defload_money(self, amount):

account_number =get_account_number(self.student_id)

balance =get_balance(account_number)

balance += amount

update_balance(account_number, balance)

混合功能

上面的代碼向展示了一些有關檢查帳戶餘額和向帳戶充值的偽代碼,這兩種偽代碼都在Student類中實現。還有更多與該帳戶相關的操作,例如凍結丟失的卡、合併帳戶——實施所有這些操作會使「學生」類越來越大,從而使維護變得越來越困難。你應該分離這些職責並使學生類不負責這些與帳戶相關的功能,即一種稱為解耦的設計模式。

classStudent:

def__init__(self, first_name, last_name, student_id):

self.first_name = first_name

self.last_name = last_name

self.student_id = student_id

self.account =Account(self.student_id)

defcheck_account_balance(self):

return self.account.get_balance()

defload_money(self, amount):

self.account.load_money(amount)

classAccount:

def__init__(self, student_id):

self.student_id = student_id

# get additional information from the database

self.balance =400

defget_balance(self):

# Theoretically, student.account.balance will work, but just in case

# we need to have additional steps to check, such as query the database

# again to make sure the data is up to date

return self.balance

defload_money(self, amount):

# get the balance from the database

self.balance += amount

self.save_to_database()

分離關注點

上面的代碼展示了我們如何使用附加的Account類來設計數據結構。如你所見,我們將所有與帳戶相關的操作移至Account類。要實現檢索學生的帳戶信息的功能,學生類將通過從Account類中檢索信息來處理。如果想實現更多與該類相關的功能,只需簡單地更新Account類即可。

設計模式的主要要點是,希望各個類具有單獨的關注點。通過將這些職責分開,你的類將變小,處理較小的代碼組件會使將來的更改變得更容易。

8.考慮使用__slots__進行優化

如果你的類主要用於存儲數據的數據容器,那麼可以考慮使用__slots__來優化類的性能。它不僅可以提高屬性訪問的速度,還可以節省內存,如果需要創建數千個或更多實例對象,就是它發揮大作用之處啦。

原因是,對於常規類,實例屬性是通過內部託管的字典存儲的。相比之下,通過使用__slots__,實例屬性將使用在幕後使用C語言實現的與數組相關的數據結構存儲,並且以更高的效率優化了它們的性能。

classStudentRegular:

def__init__(self,first_name, last_name):

self.first_name = first_name

self.last_name = last_name

classStudentSlot:

__slots__ = ['first_name', 'last_name']

def__init__(self,first_name, last_name):

self.first_name = first_name

self.last_name = last_name

在類的定義中使用__slots__

上面的代碼展示了如何在類中實現__slots__的簡單示例。具體來說,將所有屬性列為一個序列,這將在數據存儲中創建一對一匹配,以加快訪問速度並減少內存消耗。如前所述,常規類使用字典進行屬性訪問,但不使用已實現__slots__的字典。以下代碼證實了這一點:

>>> student_r =StudentRegular('John', 'Smith')

>>>student_r.__dict__

{'first_name': 'John', 'last_name': 'Smith'}

>>> student_s =StudentSlot('John', 'Smith')

>>>student_s.__dict__

Traceback (most recentcall last):

File"<input>", line 1, in <module>

AttributeError: 'StudentSlot' object has noattribute '__dict__'

具有__slots__的類中沒有__dict__

有關使用__slots__的詳細討論可以在Stack Overflow找到答案,你也可以從官方文檔中找到更多信息(https://docs.python.org/3/reference/datamodel.html)。

需要注意,使用__slots__會有一個副作用——它會阻止你動態創建其他屬性。有人建議將其作為一種控制類擁有的屬性的機制,但這並不是它的設計初衷。

9.文件

最後我們必須討論一下類的文檔。我們需要明白編寫文檔並不能替代任何代碼,編寫大量文檔並不能提高代碼的性能,也不一定會使代碼更具可讀性。如果必須依靠文檔字符串來澄清代碼,那麼你的代碼很可能有問題。

以下代碼將向大家展示一個程式設計師可能犯的錯誤——使用不必要的注釋來補償錯誤的代碼(即,在這種情況下,無意義的變量名)。相比之下,一些有好名字的好代碼甚至不需要注釋。

# how many billable hours

a =6

# the hourly rate

b =100

# total charge

c = a * b

# The above vs.the below with no comments

billable_hours =6

hourly_rate =100

total_charge = billable_hours * hourly_rate

失敗解釋案例

我並不是說反對寫評論和文檔字符串,這實際上取決於自己的實例。如果你的代碼被多個人使用或多次使用(例如,你是唯一一個多次訪問同一代碼的人),那麼就應考慮編寫一些好的注釋。

這些注釋可以幫助你自己或者團隊夥伴閱讀你的代碼,但是他們都不可以假定你的代碼完全按照注釋中的說明進行。換句話說,編寫好的代碼始終是需要牢記的頭等大事。

如果最終用戶要使用代碼的特定部分,那麼需要編寫文檔字符串,因為這些人對相關的代碼庫並不熟悉。他們只想知道如何使用相關的API,而文檔字符串將構成幫助菜單的基礎。因此,作為程式設計師,你有責任確保提供有關如何使用程序的明確說明。

本文中回顧了定義自己的類時需要考慮的重要因素。編寫的代碼越多,你就越會發現在定義類之前牢記這些原則的重要性。定義類時,請不斷練習這些準則,好的設計會在以後節省很多開發時間。

留言點讚關注

我們一起分享AI學習與發展的乾貨

如轉載,請後臺留言,遵守轉載規範

相關焦點

  • 如何在Visual Studio調整python可啟動文件
    >python類和對象在Visual Studio開發工具中,創建python應用程式,默認有個python文件,這個為主啟動文件;如果再創建一個python文件,想要運行這個文件,需要將其設置為主啟動文件,就可以調試了。
  • 介紹一下什麼是python(瘋狂Python)
    普通人,非專業搞編程的人,也能快速上手,也許你會問我python能幹什麼用,它的用處可多了:比如web開發(django,flask等),網絡爬蟲(scrapy等),數據分析(numpy,pandas,matplotlib庫等),人工智慧(Tenseflow等)等等應用的地方很多。那麼就讓我們從一個小白學起吧!1. Python是一種電腦程式設計語言。
  • 「Python」每日一練:設計圓類計算周長和面積、設計動物類
    編程題1、設計一個 Circle(圓)類,包括半徑和顏色屬性,編寫構造方法和其他方法,計算圓的周長和面積。請編寫程序驗證類的功能。2、設計一個 Animal(動物)類,包括顏色屬性和叫方法。要求:Fish類繼承自 Animal類,重寫構造方法和叫方法。編程思路1、Python中類的定義。這裡要思考的是:半徑和顏色應該為類成員還是實例成員呢?因為這兩個屬性非實例成員的共同屬性,因此將半徑和顏色設置為實例成員,直接在構造方法中定義。2、考察類繼承的寫法。
  • Python真的值得學習嗎
    1991年初python公布了第一個公開發行版本。由於它是用C語言實現的,所以從一出生,Python已經具有了:類,函數,以及以模塊為基礎的拓展系統等。 尤其是2017年人工智慧概念的興起,python的關注度也是越來越高。儘管目前它仍然無法和java等程式語言抗衡,但隨著未來人工智慧權重的提高,掌握python也就成為一種必然。它的開發效率比 C, Java 高很多,很多的創業公司當初為了把想法快速變成現實產品從而獲得投資資金,首選 Python作為開發語言。這估計也是當年推動python流行起來的另一個原因吧。
  • Python 連接開放航空交通數據,輕鬆構建航班跟蹤應用!
    通常人類只有五種感官。我認為,既然我們生活在數字時代,那麼科技就是我們的第六感。這篇文章將介紹的航班跟蹤就是如此。現在已有很多航班跟蹤應用,如flightradar24、FlightAware、flightview等,我們能夠通過它們監控飛機在地球上的位置。本文不打算討論這些應用,而是要探討如何用Python製作自己的航班跟蹤應用。
  • Python2 已終結,入手Python 3,你需要這30個技巧
    它的工作機制是這樣的這種方式在返回值的數量很少時是可以的,但是如果返回值超過 3 個,那它們就該被放到一個(數據)類中了。7. 使用數據類Python 從 3.7 開始提供數據類功能。debug 時列印出一個數據類數據類型需要你輸入提示,這樣 bug 量會大大減少下面展示了一個實用的數據類例子:想更深入的了解數據類,可以參考:https://realpython.com/python-data-classes
  • python-docx設置表格對齊方式
    包中要使用table.alignment、cell.vertical_alignment和paragraph.alignment進行設置,筆者總結了python-docx包中表格和單元格等2種設置對齊方式,並在文章最後將文章主要內容製作了思維導圖。
  • 好程式設計師Python培訓分享Python如何調用RPC接口
    需要安裝的python包如下: 1、grpc安裝 pip install grpcio2、grpc的python protobuf相關的編譯工具 pip install grpcio-tools 3、protobuf相關python依賴庫 pip install protobuf 4、一些常見原型的生成python類的集合:
  • 如何在6個月內學會Python?
    因為不知道將語言應用在何處,不久便放棄了學習的想法,這兩種語言的學習都以失敗告終。因為不曾有任何的應用案例,筆者也沒有學過那些語言的語法。但是,在學習Python時,情況有所不同。筆者想提高數據科學技能並將職業規劃轉向數據分析領域,學習python就是該計劃的一部分。
  • python:控制流程 - for迭代循環
    #1. for迭代li = [1, 5, 6, 9, 3, 2]for i in li:print(i)2.後面需要接上可迭代對象for會依次取出可迭代對象中的元素5.continue的用法:continue和break類似,但是continue不會終止循環,而是結束本次循環,跳到下次循環for主要應用在遍歷
  • 軟體工程師的試煉之地:53道Python面試問答題
    在我初學python時,我以為它們是相同的……卻出現了一些bug。因此,為了記錄,is表示檢查身份,而==表示檢查相等性。可通過一個例子來解釋。創建一些列表並將其分配給名稱。請注意,b指向與下面的a相同的對象。
  • python教程第8課:python基礎之Tuple元祖
    如果在預先可以確定這個集合的值,並且使用的僅僅是遍歷查詢操作,那麼推薦使用tuple1)創建空元組,創建元祖使用小括號()來定義,裡面的元素使用逗號隔開>#輸出結果,下面的() 即表示tuple元祖類型數據()2)創建只有一個元素的元組(只有一個元素時,
  • Python突破12306最後一道防線,實現自動搶票(附源碼)
    (依賴自己的網絡環境太厲害,還有機器的好壞)Splinter是一個使用Python開發的開源Web應用測試工具,它可以幫你實現自動瀏覽站點和與其進行交互,Splinter執行的時候會自動打開你指定的瀏覽器,訪問指定的URL。然後你所開發的模擬的任何行為,都會自動完成,你只需要坐在電腦面前,像看電影一樣看著屏幕上各種動作自動完成然後收集結果即可。
  • 來一點Python面向對象第一級進階的東西
    這一概念的提出很快引發了計算機科學領域關於應用反射性的研究。它首先被程序語言的設計領域所採用,並在Lisp和面向對象方面取得了成績。python面向對象中的反射:通過字符串的形式操作對象相關屬性.python中的一切事物都是對象(都可以使用反射)四個可以實現自省的函數下列方法適用類和對象(一切皆對象,類本身也是一個對象)
  • python實現釘釘自動打卡
    說下思路吧用python調用adb命令通過x,y點位來操作手機,在寫個定時器就好了,簡單吧。下載安裝包並配置環境變量python3環境安裝adb 配置環境變量下載adb : 網上太多了,找一個就好了,我要和時間賽跑 (> _ <……)配置環境變量: 這個是基礎,python
  • Python 炫技操作:安裝包的八種方法
    使用 pipxpipx 是一個專門用於安裝和管理 cli 應用程式的工具,使用它安裝的 Python 包會單獨安裝到一個全新的獨有虛擬環境。由於它是一個第三方工具,因此在使用它之前,需要先安裝$ python3 -m pip install  $ python3 -m userpath append ~/.
  • Python開發簡單爬蟲【學習資料總結】
    ;另一方面,會將新的URL補充進URL管理器,若有URL管理器中含有新的URL,則重複上述步驟,直到爬取完所有的URL 6、最後,調度器會調動應用的方法,將價值數據輸出到需要的格式。
  • Python裝飾器以及高級用法
    大多數人在學習Python時都跟裝飾器做過鬥爭,所以如果這對你來說很奇怪,不要感到沮喪,因為同樣的大多數人都可以克服這種苦難。在本教程中,我將逐步介紹了解裝飾器的過程。首先我假設你已經可以編寫基本函數和基本類。如果你不能做這些事,那麼我建議你在回到這裡之前先學習如何去做到編寫基本函數和基本類(除非你迷路了,在這種情況下你可以原諒)。
  • Windows 10的8種最佳動態壁紙應用程式
    因此,如果您正在尋找一種獲得Windows 10最佳動態壁紙的方法,那麼您將在這裡找到所有解決方案。我們已經提到了Windows 10的免費和付費動態壁紙應用程式,因此您可以輕鬆選擇合適的程序。關於這一點,讓我們繼續整理清單。
  • Python 3.8.0來了!
    調試構建使用與發布構建相同的 ABI 發布構建和調試構建現在都是 ABI 兼容的:定義Py_DEBUG宏不會再啟用Py_TRACE_REFS宏,它引入了唯一的 ABI 不兼容性。