多態方法調用的解析和分派

方法調用并不等同于方法執行,方法調用階段唯一的任務就是確定被調用方法的版本(即調用哪一個方法),暫時還不涉及方法內部的具體運行過程。在程序運行時,進行方法調用是最普遍、最頻繁的操作,Class文件的編譯過程中不包含傳統編譯中的連接步驟,一切方法調用在Class文件里面存儲的都只是符號引用,而不是方法在實際運行時內存布局中的入口地址(相當于之前說的直接引用)。這個特性給Java帶來了更強大的動態擴展能力,但也使得Java方法調用過程變得相對復雜起來,需要在類加載期間,甚至到運行期間才能確定目標方法的直接引用。

解析調用一定是個靜態的過程,在編譯期間就完全確定,在類裝載的解析階段就會把涉及的符號引用全部轉變為可確定的直接引用,不會延遲到運行期再去完成。而分派(Dispatch)調用則可能是靜態的也可能是動態的,根據分派依據的宗量數可分為單分派和多分派。這兩類分派方式的兩兩組合就構成了靜態單分派、靜態多分派、動態單分派、動態多分派4種分派組合情況。

解析

所有方法調用中的目標方法在Class文件里面都是一個常量池中的符號引用,在類加載的解析階段,會將其中的一部分符號引用轉化為直接引用,這種解析能成立的前提是:方法在程序真正運行之前就有一個可確定的調用版本,并且這個方法的調用版本在運行期是不可改變的。換句話說,調用目標在程序代碼寫好、編譯器進行編譯時就必須確定下來。這類方法的調用稱為解析(Resolution)。在Java語言中符合“編譯期可知,運行期不可變”這個要求的方法,主要包括靜態方法和私有方法兩大類,前者與類型直接關聯,后者在外部不可被訪問,這兩種方法各自的特點決定了它們都不可能通過繼承或別的方式重寫其他版本,因此它們都適合在類加載階段進行解析。

與之相對應的是,在Java虛擬機里面提供了5條方法調用字節碼指令,分別如下。

invokestatic:調用靜態方法。

invokespecial:調用實例構造器<init>方法、私有方法和父類方法。

invokevirtual:調用所有的虛方法。

invokeinterface:調用接口方法,會在運行時再確定一個實現此接口的對象。

invokedynamic:先在運行時動態解析出調用點限定符所引用的方法,然后再執行該方法,在此之前的4條調用指令,分派邏輯是固化在Java虛擬機內部的,而invokedynamic指令的分派邏輯是由用戶所設定的引導方法決定的。

只要能被invokestatic和invokespecial指令調用的方法,都可以在解析階段中確定唯一的調用版本,符合這個條件的有靜態方法、私有方法、實例構造器、父類方法4類,它們在類加載的時候就會把符號引用解析為該方法的直接引用。這些方法可以稱為非虛方法,與之相反,其他方法稱為虛方法(除去final方法)。

Java中的非虛方法除了使用invokestatic、invokespecial調用的方法之外還有一種,就是被final修飾的方法。雖然final方法是使用invokevirtual指令來調用的,但是由于它無法被覆蓋,沒有其他版本,所以也無須對方法接收者進行多態選擇,又或者說多態選擇的結果肯定是唯一的。在Java語言規范中明確說明了final方法是一種非虛方法。

分派

Java是一門面向對象的程序語言,因為Java具備面向對象的3個基本特征:繼承、封裝和多態。分派調用過程將會揭示多態性特征的一些最基本的體現,如“重載”和“重寫”在Java虛擬機之中是如何實現的,這里的實現當然不是語法上該如何寫,我們關心的依然是虛擬機如何確定正確的目標方法

靜態分派

public class StaticDispatch{static abstract class Human{}static class Man extends Human{}static class Woman extends Human{}public void sayHello(Human guy){System.out.println("hello,guy!");}public void sayHello(Man guy){System.out.println("hello,gentleman!");}public void sayHello(Woman guy){System.out.println("hello,lady!");}public static void main(String[]args){Human man=new Man();Human woman=new Woman();StaticDispatch sr=new StaticDispatch();sr.sayHello(man);sr.sayHello(woman);}
}hello,guy!
hello,guy!

main()里面的兩次sayHello()方法調用,在方法接收者已經確定是對象“sr”的前提下,使用哪個重載版本,就完全取決于傳入參數的數量和數據類型。代碼中刻意地定義了兩個靜態類型相同但實際類型不同的變量,但虛擬機(準確地說是編譯器)在重載時是通過參數的靜態類型而不是實際類型作為判定依據的。并且靜態類型是編譯期可知的,因此,在編譯階段,Javac編譯器會根據參數的靜態類型決定使用哪個重載版本,所以選擇了sayHello(Human)作為調用目標,并把這個方法的符號引用寫到main()方法里的兩條invokevirtual指令的參數中。所有依賴靜態類型來定位方法執行版本的分派動作稱為靜態分派。靜態分派的典型應用是方法重載。靜態分派發生在編譯階段,因此確定靜態分派的動作實際上不是由虛擬機來執行的。另外,編譯器雖然能確定出方法的重載版本,但在很多情況下這個重載版本并不是“唯一的”,往往只能確定一個“更加合適的”版本。這種模糊的結論在由0和1構成的計算機世界中算是比較“稀罕”的事情,產生這種模糊結論的主要原因是字面量不需要定義,所以字面量沒有顯式的靜態類型,它的靜態類型只能通過語言上的規則去理解和推斷。何為“更加合適的”版本。

public class Overload{public static void sayHello(Object arg){System.out.println("hello Object");}public static void sayHello(int arg){System.out.println("hello int");}public static void sayHello(long arg){System.out.println("hello long");}public static void sayHello(Character arg){System.out.println("hello Character");}public static void sayHello(char arg){System.out.println("hello char");}public static void sayHello(char……arg){System.out.println("hello char……");}public static void sayHello(Serializable arg){System.out.println("hello Serializable");}public static void main(String[]args){sayHello('a');}
}

上面的代碼運行后會輸出:

hello char

這很好理解,'a'是一個char類型的數據,自然會尋找參數類型為char的重載方法,如果注釋掉sayHello(char arg)方法,那輸出會變為:

hello int

這時發生了一次自動類型轉換,'a'除了可以代表一個字符串,還可以代表數字97(字符'a'的Unicode數值為十進制數字97),因此參數類型為int的重載也是合適的。我們繼續注釋掉sayHello(int arg)方法,那輸出會變為:

hello long

這時發生了兩次自動類型轉換,'a'轉型為整數97之后,進一步轉型為長整數97L,匹配了參數類型為long的重載。筆者在代碼中沒有寫其他的類型如float、double等的重載,不過實際上自動轉型還能繼續發生多次,按照char->int->long->float->double的順序轉型進行匹配。但不會匹配到byte和short類型的重載,因為char到byte或short的轉型是不安全的。我們繼續注釋掉sayHello(long arg)方法,那輸出會變為:

hello Character

這時發生了一次自動裝箱,'a'被包裝為它的封裝類型java.lang.Character,所以匹配到了參數類型為Character的重載,繼續注釋掉sayHello(Character arg)方法,那輸出會變為:

hello Serializable

這個輸出可能會讓人感覺摸不著頭腦,一個字符或數字與序列化有什么關系?出現hello Serializable,是因為java.lang.Serializable是java.lang.Character類實現的一個接口,當自動裝箱之后發現還是找不到裝箱類,但是找到了裝箱類實現了的接口類型,所以緊接著又發生一次自動轉型。char可以轉型成int,但是Character是絕對不會轉型為Integer的,它只能安全地轉型為它實現的接口或父類。Character還實現了另外一個接口java.lang.Comparable<Character>,如果同時出現兩個參數分別為Serializable和Comparable<Character>的重載方法,那它們在此時的優先級是一樣的。編譯器無法確定要自動轉型為哪種類型,會提示類型模糊,拒絕編譯。程序必須在調用時顯式地指定字面量的靜態類型,如:sayHello((Comparable<Character>)'a'),才能編譯通過。下面繼續注釋掉sayHello(Serializable arg)方法,輸出會變為:

hello Object

這時是char裝箱后轉型為父類了,如果有多個父類,那將在繼承關系中從下往上開始搜索,越接近上層的優先級越低。即使方法調用傳入的參數值為null時,這個規則仍然適用。我們把sayHello(Object arg)也注釋掉,輸出將會變為:

hello char……

7個重載方法已經被注釋得只剩一個了,可見變長參數的重載優先級是最低的,這時候字符'a'被當做了一個數組元素。使用的是char類型的變長參數,在驗證時還可以選擇int類型、Character類型、Object類型等的變長參數重載來把上面的過程重新演示一遍。但要注意的是,有一些在單個參數中能成立的自動轉型,如char轉型為int,在變長參數中是不成立的。

另外還有一點可能比較容易混淆:解析與分派這兩者之間的關系并不是二選一的排他關系,它們是在不同層次上去篩選、確定目標方法的過程。例如,前面說過,靜態方法會在類加載期就進行解析,而靜態方法顯然也是可以擁有重載版本的,選擇重載版本的過程也是通過靜態分派完成的。

動態分派

?

public class DynamicDispatch{static abstract class Human{protected abstract void sayHello();}static class Man extends Human{@Overrideprotected void sayHello(){System.out.println("man say hello");}}static class Woman extends Human{@Overrideprotected void sayHello(){System.out.println("woman say hello");}}public static void main(String[]args){Human man=new Man();Human woman=new Woman();man.sayHello();woman.sayHello();man=new Woman();man.sayHello();}
}
運行結果:
man say hello
woman say hello
woman say hello

虛擬機是如何知道要調用哪個方法的?顯然這里不可能再根據靜態類型來決定,因為靜態類型同樣都是Human的兩個變量man和woman在調用sayHello()方法時執行了不同的行為,并且變量man在兩次調用中執行了不同的方法。導致這個現象的原因很明顯,是這兩個變量的實際類型不同,Java虛擬機是如何根據實際類型來分派方法執行版本的呢?

main()方法的字節碼
public static void main(java.lang.String[]);
Code:
Stack=2,Locals=3,Args_size=1
0:new#16;//class org/fenixsoft/polymorphic/Dynamic-Dispatch $Man
3:dup
4:invokespecial#18;//Method org/fenixsoft/polymorphic/Dynamic-Dispatch $Man."<init>":()V
7:astore_1
8:new#19;//class org/fenixsoft/polymorphic/Dynamic-Dispatch $Woman
11:dup
12:invokespecial#21;//Method org/fenixsoft/polymorphic/DynamicDispatch $Woman."<init>":()V
15:astore_2
16:aload_1
17:invokevirtual#22;//Method org/fenixsoft/polymorphic/Dynamic-Dispatch $Human.sayHello:()V
20:aload_2
21:invokevirtual#22;//Method org/fenixsoft/polymorphic/Dynamic-Dispatch $Human.sayHello:()V
24:new#19;//class org/fenixsoft/polymorphic/Dynamic-Dispatch $Woman
27:dup
28:invokespecial#21;//Method org/fenixsoft/polymorphic/DynamicDispatch $Woman."<init>":()V
31:astore_1
32:aload_1
33:invokevirtual#22;//Method org/fenixsoft/polymorphic/DynamicDispatch $Human.sayHello:()V
36:return
0~15行的字節碼是準備動作,作用是建立man和woman的內存空間、調用Man和Woman類型的實例構造器,將這兩個實例的引用存放在第1、2個局部變量表Slot之中,
這個動作也就對應了代碼中的這兩句: Human man
= newMan(); Human woman = newWoman();

接下來的16~21句是關鍵部分,16、20兩句分別把剛剛創建的兩個對象的引用壓到棧頂,這兩個對象是將要執行的sayHello()方法的所有者,稱為接收者(Receiver);17和21句是方法調用指令,這兩條調用指令單從字節碼角度來看,無論是指令(都是invokevirtual)還是參數(都是常量池中第22項的常量,注釋顯示了這個常量是Human.sayHello()的符號引用)完全一樣的,但是這兩句指令最終執行的目標方法并不相同。

原因就需要從invokevirtual指令的多態查找過程開始說起,invokevirtual指令的運行時解析過程大致分為以下幾個步驟:

1)找到操作數棧頂的第一個元素所指向的對象的實際類型,記作C。

2)如果在類型C中找到與常量中的描述符和簡單名稱都相符的方法,則進行訪問權限校驗,如果通過則返回這個方法的直接引用,查找過程結束;如果不通過,則返回java.lang.IllegalAccessError異常。

3)否則,按照繼承關系從下往上依次對C的各個父類進行第2步的搜索和驗證過程。

4)如果始終沒有找到合適的方法,則拋出java.lang.AbstractMethodError異常。

由于invokevirtual指令執行的第一步就是在運行期確定接收者的實際類型,所以兩次調用中的invokevirtual指令把常量池中的類方法符號引用解析到了不同的直接引用上,這個過程就是Java語言中方法重寫的本質。我們把這種在運行期根據實際類型確定方法執行版本的分派過程稱為動態分派

單分派與多分派

方法的接收者方法的參數統稱為方法的宗量,這個定義最早應該來源于《Java與模式》一書。根據分派基于多少種宗量,可以將分派劃分為單分派和多分派兩種。單分派是根據一個宗量對目標方法進行選擇,多分派則是根據多于一個宗量對目標方法進行選擇。

列舉了一個Father和Son一起來做出“一個艱難的決定”的例子。

public class Dispatch{static class QQ{}static class_360{}public static class Father{public void hardChoice(QQ arg){System.out.println("father choose qq");}public void hardChoice(_360 arg){System.out.println("father choose 360");}}public static class Son extends Father{public void hardChoice(QQ arg){System.out.println("son choose qq");}public void hardChoice(_360 arg){System.out.println("son choose 360");}}public static void main(String[]args){Father father=new Father();Father son=new Son();father.hardChoice(new_360());son.hardChoice(new QQ());}
}
運行結果:
father choose 360
son choose qq

在main函數中調用了兩次hardChoice()方法,這兩次hardChoice()方法的選擇結果在程序輸出中已經顯示得很清楚了。

我們來看看編譯階段編譯器的選擇過程,也就是靜態分派的過程。這時選擇目標方法的依據有兩點:一是靜態類型是Father還是Son,二是方法參數是QQ還是360。這次選擇結果的最終產物是產生了兩條invokevirtual指令,兩條指令的參數分別為常量池中指向Father.hardChoice(360)及Father.hardChoice(QQ)方法的符號引用因為是根據兩個宗量進行選擇,所以Java語言的靜態分派屬于多分派類型。

再看看運行階段虛擬機的選擇,也就是動態分派的過程。在執行“son.hardChoice(new QQ())”這句代碼時,更準確地說,是在執行這句代碼所對應的invokevirtual指令時,由于編譯期已經決定目標方法的簽名必須為hardChoice(QQ),虛擬機此時不會關心傳遞過來的參數“QQ”到底是“騰訊QQ”還是“奇瑞QQ”,因為這時參數的靜態類型、實際類型都對方法的選擇不會構成任何影響,唯一可以影響虛擬機選擇的因素只有此方法的接受者的實際類型是Father還是Son。因為只有一個宗量作為選擇依據,所以Java語言的動態分派屬于單分派類型。

?

虛擬機動態分派的實現

由于動態分派是非常頻繁的動作,而且動態分派的方法版本選擇過程需要運行時在類的方法元數據中搜索合適的目標方法,因此在虛擬機的實際實現中基于性能的考慮,大部分實現都不會真正地進行如此頻繁的搜索。面對這種情況,最常用的“穩定優化”手段就是為類在方法區中建立一個虛方法表(Vritual Method Table,也稱為vtable,與此對應的,在invokeinterface執行時也會用到接口方法表——Inteface Method Table,簡稱itable),使用虛方法表索引來代替元數據查找以提高性能。

虛方法表中存放著各個方法的實際入口地址。如果某個方法在子類中沒有被重寫,那子類的虛方法表里面的地址入口和父類相同方法的地址入口是一致的,都指向父類的實現入口。如果子類中重寫了這個方法,子類方法表中的地址將會替換為指向子類實現版本的入口地址。Son重寫了來自Father的全部方法,因此Son的方法表沒有指向Father類型數據的箭頭。但是Son和Father都沒有重寫來自Object的方法,所以它們的方法表中所有從Object繼承來的方法都指向了Object的數據類型。

為了程序實現上的方便,具有相同簽名的方法,在父類、子類的虛方法表中都應當具有一樣的索引序號,這樣當類型變換時,僅需要變更查找的方法表,就可以從不同的虛方法表中按索引轉換出所需的入口地址。方法表一般在類加載的連接階段進行初始化,準備了類的變量初始值后,虛擬機會把該類的方法表也初始化完畢。

ps:方法表是分派調用的“穩定優化”手段,虛擬機除了使用方法表之外,在條件允許的情況下,還會使用內聯緩存(Inline Cache)和基于“類型繼承關系分析”(Class Hierarchy Analysis,CHA)技術的守護內聯(Guarded Inlining)兩種非穩定的“激進優化”手段來獲得更高的性能,關于這兩種優化技術的原理和運作過程,可以參考JIT晚期運行期。

?

?

?

?

?

?

?

?

?

?

?

?

轉載于:https://www.cnblogs.com/wade-luffy/p/6058075.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/456982.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/456982.shtml
英文地址,請注明出處:http://en.pswp.cn/news/456982.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

ES6:Set和Map

Set Set:類似數組,但是成員的值都是唯一的,沒有重復。Set本身是一個構造函數,用來生成Set數據結構。他包含的方法:add: 添加某個值,返回Set結構本身。delete: 刪除某個值,返回一個布爾值,表示是…

九九乘法表[循環嵌套]

#九九乘法表 # 1*11 # 1*22 2*24 # 1*33 2*36 3*39 # ...#循環嵌套 #行數 i 1 while i < 9:# 打印每行的內容j 1while j < i:print("%d * %d %3d " % (i, j, i * j), end)j 1print() # 換行i 1while嵌套&#xff1a;w 1 while w < 10: #外層循…

關于用VS寫C程序運行時出現燙字以及亂碼的問題的原因

最近在復習C語言寫程序時&#xff0c;突然碰到標題上的這種情況&#xff0c;后來經過上網查找以及逐步調試才發現原來是在打印數組的時候“越界”導致的&#xff0c;因為程序在默認初始化char類型的數組時&#xff0c;初始化的值是“燙”字&#xff0c;一般情況下是字符串未初始…

javascript函數調用的各種方法!!

在JavaScript中一共有下面4種調用方式&#xff1a; (1) 基本函數調用 (2)方法調用 (3)構造器調用 (4)通過call()和apply()進行調用 1. 基本函數調用 普通函數調用模式&#xff0c;如&#xff1a; JavaScript code?1234function fn(o){…… }fn({x:1});在基本函數調用中&#x…

ARM TK1 安裝kinect驅動

首先安裝usb庫 $ git clone https://github.com/libusb/libusb.git 編譯libusb需要的工具 $ sudo apt-get install autoconf autogen $ sudo apt-get install libtool $ sudo apt-get install libudev* 編譯安裝 $ sudo ./autogen.sh $ sudo make $ sudo make install $ sudo l…

如何在一個html頁面中提交兩個post,如何在同一個頁面上從Django和Ajax獲得多個post請求?...

我一整天都在為這事犯愁。似乎什么都沒用。這是我的情況。在我有一個Django表單&#xff0c;有兩個字段&#xff1a;redirect_from&#xff0c;redirect_to。此表單有兩個提交按鈕&#xff1a;Validate和{}。當頁面加載時&#xff0c;Submit被隱藏&#xff0c;只顯示Validate。…

大數據入門:各種大數據技術的介紹

大數據我們都知道hadoop&#xff0c;可是還會各種各樣的技術進入我們的視野&#xff1a;Spark&#xff0c;Storm&#xff0c;impala&#xff0c;讓我們都反映不過來。為了能夠更好的架構大數據項目&#xff0c;這里整理一下&#xff0c;供技術人員&#xff0c;項目經理&#xf…

高可用與負載均衡(5)之基于客戶端的負載均衡

什么是客戶端負載均衡 基于客戶端的負載均衡&#xff0c;簡單的說就是在客戶端程序里面&#xff0c;自己設定一個調度算法&#xff0c;在向服務器發起請求的時候&#xff0c;先執行調度算法計算出向哪臺服務器發起請求&#xff0c;然后再發起請求給服務器。 基于客戶端負載均衡…

Variant 與 內存泄露

http://blog.chinaunix.net/uid-10386087-id-2959221.html 今天遇到一個內存泄露的問題。是師兄檢測出來的。Variant類型在使用后要Clear否則會造成內存泄露&#xff0c;為什么呢&#xff1f; Google一下找到下面一篇文章&#xff0c;主要介紹了Com的內存泄露&#xff0c;中間有…

安裝安全類軟件進行了android簽名漏洞修補,魅族MX3怎么升級固件體驗最新比較穩定的版本...

魅族mx3固件怎么升級?flyme os系統會持續更新&#xff0c;升級魅族MX3手機系統需先下載MX3的升級固件&#xff0c;升級固件分為體驗版和穩定版。魅族MX3固件有體驗版和穩定版兩種&#xff0c;顧名思義&#xff0c;體驗版為最新版但相比穩定版來說存在更多的漏洞&#xff0c;升…

linux su切換用戶提示Authentication failture的解決辦法

由于ubtun系統默認是沒有激活root用戶的&#xff0c;需要我們手工進行操作&#xff0c;在命令行界面下&#xff0c;或者在終端中輸入如下命令&#xff1a; sudo passwd Password&#xff1a;你當前的密碼 Enter new UNIX password&#xff1a;這個是root的密碼 Retype new …

@property

class Person(object):def __init__(self, name,age):#屬性直接對外暴露#self.age age#限制訪問self.__age ageself.__name namedef getAge(self):return self.__agedef setAge(self,age):if age<0:age 0self.__age age#方法名為受限制的變量去掉雙下劃線propertydef a…

ubuntu入門知識

1、linux系統發展歷史 unix -> Linux -> ubuntu linux發展軌跡圖 2、ubuntu下載和安裝 推薦使用長期支持版本&#xff1a; 10.04,12.04,14.04或LTS版本 安裝環境VMware虛擬機 3、安裝之后創建root sudo passwd root 輸入root用戶密碼即可 4、安裝軟件&#xff1a; 更新軟…

html 二級試題,計算機二級考試WEB試題及答案

計算機二級考試WEB試題及答案當前主要的 WEB數據庫訪問技術有哪些?答&#xff1a;到目前為止&#xff0c;WEB數據庫訪問技術主要分為兩大類&#xff1a;(1)公共網關接口技術(CGI);CGI 是 WEB 服務器運行時外部程序的規范&#xff0c;按照 CGI 編寫的程序可以擴展服務器的功能&…

細數阿里云服務器的十二種典型應用場景

原文鏈接&#xff1a;http://click.aliyun.com/m/13910/免費開通大數據服務&#xff1a;https://www.aliyun.com/product/odps文章轉載&#xff1a;小白楊1990如今&#xff0c;阿里云的產品可謂是多種多樣&#xff0c;紛繁復雜。面對各種各樣的技術和產品&#xff0c;ECS、RDS、…

動態給實例添加屬性和方法

from types import MethodType#創建一個空類 class Person(object):__slots__ ("name","age","speak","height")per Person() #動態添加屬性&#xff0c;這體現了動態語言的特點(靈活&#xff09;per.name "tom" print(…

android導入項目出現style錯誤,menu錯誤

android導入項目出現style錯誤&#xff0c;menu錯誤 style //查看 res/values/styles.xml 下的報錯點。<style name"AppBaseTheme" parent"Theme.AppCompat.Light"> //把這個改成 <style name"AppBaseTheme" parent"android:The…

Vim的基本操作總結

最近在學習Linux基礎的時候&#xff0c;對Vim的基本操作時遇到很多問題&#xff0c;如編輯錯誤&#xff0c;無法退出Vim等。通過一系列的學習后才解決了這些問題&#xff0c;希望這個過程能對后來者有所幫助 先對Vim的三種模式做個大致的介紹&#xff1a; Vi有三種基本工作模式…

html股票數據代碼,股票數據的網站抓取(4.2)代碼優化

#codingutf-8from selenium import webdriverimport timeimport osimport reimport sysimport threadingimport Queueimport Tkinter as tkfrom selenium.common.exceptions import NoSuchElementExceptiondef myinit():reload(sys)sys.setdefaultencoding(utf8)#獲取屏幕分辨率…

對象屬性和類屬性

class Person(object):#這里的屬性實際上屬于類屬性&#xff08;用類名調用&#xff09;name "person"def __init__(self,name):#對象屬性self.name nameprint(Person.name) per Person("tom") #對象屬性的優先級高于類屬性 print(per.name) #動態的給對…