網(wǎng)站首頁 編程語言 正文
問題:
我們每天都要編寫一些Python程序,或者用來處理一些文本,或者是做一些系統(tǒng)管理工作。程序寫好后,只需要敲下python命令,便可將程序啟動起來并開始執(zhí)行:
$ python some-program.py
那么,一個文本形式的.py文件,是如何一步步轉換為能夠被CPU執(zhí)行的機器指令的呢?此外,程序執(zhí)行過程中可能會有.pyc文件生成,這些文件又有什么作用呢?
1. 執(zhí)行過程
雖然從行為上看Python更像Shell腳本這樣的解釋性語言,但實際上Python程序執(zhí)行原理本質上跟Java或者C#一樣,都可以歸納為虛擬機和字節(jié)碼。Python執(zhí)行程序分為兩步:先將程序代碼編譯成字節(jié)碼,然后啟動虛擬機執(zhí)行字節(jié)碼:
雖然Python命令也叫做Python解釋器,但跟其他腳本語言解釋器有本質區(qū)別。實際上,Python解釋器包含編譯器以及虛擬機兩部分。當Python解釋器啟動后,主要執(zhí)行以下兩個步驟:
編譯器將.py文件中的Python源碼編譯成字節(jié)碼虛擬機逐行執(zhí)行編譯器生成的字節(jié)碼
因此,.py文件中的Python語句并沒有直接轉換成機器指令,而是轉換成Python字節(jié)碼。
2. 字節(jié)碼
Python程序的編譯結果是字節(jié)碼,里面有很多關于Python運行的相關內容。因此,不管是為了更深入理解Python虛擬機運行機制,還是為了調優(yōu)Python程序運行效率,字節(jié)碼都是關鍵內容。
那么,Python字節(jié)碼到底長啥樣呢?我們如何才能獲得一個Python程序的字節(jié)碼呢——Python提供了一個內置函數(shù)compile用于即時編譯源碼。我們只需將待編譯源碼作為參數(shù)調用compile函數(shù),即可獲得源碼的編譯結果。
3. 源碼編譯
下面,我們通過compile函數(shù)來編譯一個程序:
源碼保存在demo.py文件中:
PI = 3.14
def circle_area(r):
return PI * r ** 2
class Person(object):
def __init__(self, name):
self.name = name
def say(self):
print('i am', self.name)
編譯之前需要將源碼從文件中讀取出來:
>>> text = open('D:\myspace\code\pythonCode\mix\demo.py').read()
>>> print(text)
PI = 3.14
def circle_area(r):
return PI * r ** 2
class Person(object):
def __init__(self, name):
self.name = name
def say(self):
print('i am', self.name)
然后調用compile函數(shù)來編譯源碼:
>>> result = compile(text,'D:\myspace\code\pythonCode\mix\demo.py', 'exec')
compile函數(shù)必填的參數(shù)有3個:
source:待編譯源碼
filename:源碼所在文件名
mode:編譯模式,exec表示將源碼當作一個模塊來編譯
三種編譯模式:
exec:用于編譯模塊源碼
single:用于編譯一個單獨的Python語句(交互式下)
eval:用于編譯一個eval表達式
4. PyCodeObject
通過compile函數(shù),我們獲得了最后的源碼編譯結果result:
>>> result
<code object <module> at 0x000001DEC2FCF680, file "D:\myspace\code\pythonCode\mix\demo.py", line 1>
>>> result.__class__
<class 'code'>
最終我們得到了一個code類型的對象,它對應的底層結構體是PyCodeObject
PyCodeObject源碼如下:
/* Bytecode object */
struct PyCodeObject {
PyObject_HEAD
int co_argcount; /* #arguments, except *args */
int co_posonlyargcount; /* #positional only arguments */
int co_kwonlyargcount; /* #keyword only arguments */
int co_nlocals; /* #local variables */
int co_stacksize; /* #entries needed for evaluation stack */
int co_flags; /* CO_..., see below */
int co_firstlineno; /* first source line number */
PyObject *co_code; /* instruction opcodes */
PyObject *co_consts; /* list (constants used) */
PyObject *co_names; /* list of strings (names used) */
PyObject *co_varnames; /* tuple of strings (local variable names) */
PyObject *co_freevars; /* tuple of strings (free variable names) */
PyObject *co_cellvars; /* tuple of strings (cell variable names) */
/* The rest aren't used in either hash or comparisons, except for co_name,
used in both. This is done to preserve the name and line number
for tracebacks and debuggers; otherwise, constant de-duplication
would collapse identical functions/lambdas defined on different lines.
*/
Py_ssize_t *co_cell2arg; /* Maps cell vars which are arguments. */
PyObject *co_filename; /* unicode (where it was loaded from) */
PyObject *co_name; /* unicode (name, for reference) */
PyObject *co_linetable; /* string (encoding addr<->lineno mapping) See
Objects/lnotab_notes.txt for details. */
void *co_zombieframe; /* for optimization only (see frameobject.c) */
PyObject *co_weakreflist; /* to support weakrefs to code objects */
/* Scratch space for extra data relating to the code object.
Type is a void* to keep the format private in codeobject.c to force
people to go through the proper APIs. */
void *co_extra;
/* Per opcodes just-in-time cache
*
* To reduce cache size, we use indirect mapping from opcode index to
* cache object:
* cache = co_opcache[co_opcache_map[next_instr - first_instr] - 1]
*/
// co_opcache_map is indexed by (next_instr - first_instr).
// * 0 means there is no cache for this opcode.
// * n > 0 means there is cache in co_opcache[n-1].
unsigned char *co_opcache_map;
_PyOpcache *co_opcache;
int co_opcache_flag; // used to determine when create a cache.
unsigned char co_opcache_size; // length of co_opcache.
};
代碼對象PyCodeObject用于存儲編譯結果,包括字節(jié)碼以及代碼涉及的常量、名字等等。關鍵字段包括:
字段 | 用途 |
---|---|
co_argcount | 參數(shù)個數(shù) |
co_kwonlyargcount | 關鍵字參數(shù)個數(shù) |
co_nlocals | 局部變量個數(shù) |
co_stacksize | 執(zhí)行代碼所需棧空間 |
co_flags | 標識 |
co_firstlineno | 代碼塊首行行號 |
co_code | 指令操作碼,即字節(jié)碼 |
co_consts | 常量列表 |
co_names | 名字列表 |
co_varnames | 局部變量名列表 |
下面打印看一下這些字段對應的數(shù)據(jù):
通過co_code字段獲得字節(jié)碼:
>>> result.co_code
b'd\x00Z\x00d\x01d\x02\x84\x00Z\x01G\x00d\x03d\x04\x84\x00d\x04e\x02\x83\x03Z\x03d\x05S\x00'
通過co_names字段獲得代碼對象涉及的所有名字:
>>> result.co_names
('PI', 'circle_area', 'object', 'Person')
通過co_consts字段獲得代碼對象涉及的所有常量:
>>> result.co_consts
(3.14, <code object circle_area at 0x0000023D04D3F310, file "D:\myspace\code\pythonCode\mix\demo.py", line 3>, 'circle_area', <code object Person at 0x0000023D04D3F5D0, file "D:\myspace\code\pythonCode\mix\demo.py", line 6>, 'Person', None)
可以看到,常量列表中還有兩個代碼對象,其中一個是circle_area函數(shù)體,另一個是Person類定義體。對應Python中作用域的劃分方式,可以自然聯(lián)想到:每個作用域對應一個代碼對象。如果這個假設成立,那么Person代碼對象的常量列表中應該還包括兩個代碼對象:init函數(shù)體和say函數(shù)體。下面取出Person類代碼對象來看一下:
>>> person_code = result.co_consts[3]
>>> person_code
<code object Person at 0x0000023D04D3F5D0, file "D:\myspace\code\pythonCode\mix\demo.py", line 6>
>>> person_code.co_consts
('Person', <code object __init__ at 0x0000023D04D3F470, file "D:\myspace\code\pythonCode\mix\demo.py", line 7>, 'Person.__init__', <code object say at 0x0000023D04D3F520, file "D:\myspace\code\pythonCode\mix\demo.py", line 10>, 'Person.say', None)
因此,我們得出結論:Python源碼編譯后,每個作用域都對應著一個代碼對象,子作用域代碼對象位于父作用域代碼對象的常量列表里,層級一一對應。
至此,我們對Python源碼的編譯結果——代碼對象PyCodeObject有了最基本的認識,后續(xù)會在虛擬機、函數(shù)機制、類機制中進一步學習。
5. 反編譯
字節(jié)碼是一串不可讀的字節(jié)序列,跟二進制機器碼一樣。如果想讀懂機器碼,可以將其反匯編,那么字節(jié)碼可以反編譯嗎?
通過dis模塊可以將字節(jié)碼反編譯:
>>> import dis
>>> dis.dis(result.co_code)
0 LOAD_CONST 0 (0)
2 STORE_NAME 0 (0)
4 LOAD_CONST 1 (1)
6 LOAD_CONST 2 (2)
8 MAKE_FUNCTION 0
10 STORE_NAME 1 (1)
12 LOAD_BUILD_CLASS
14 LOAD_CONST 3 (3)
16 LOAD_CONST 4 (4)
18 MAKE_FUNCTION 0
20 LOAD_CONST 4 (4)
22 LOAD_NAME 2 (2)
24 CALL_FUNCTION 3
26 STORE_NAME 3 (3)
28 LOAD_CONST 5 (5)
30 RETURN_VALUE
字節(jié)碼反編譯后的結果和匯編語言很類似。其中,第一列是字節(jié)碼的偏移量,第二列是指令,第三列是操作數(shù)。以第一條字節(jié)碼為例,LOAD_CONST指令將常量加載進棧,常量下標由操作數(shù)給出,而下標為0的常量是:
>>> result.co_consts[0]3.14
這樣,第一條字節(jié)碼的意義就明確了:將常量3.14加載到棧。
由于代碼對象保存了字節(jié)碼、常量、名字等上下文信息,因此直接對代碼對象進行反編譯可以得到更清晰的結果:
>>>dis.dis(result)
1 0 LOAD_CONST 0 (3.14)
2 STORE_NAME 0 (PI)
3 4 LOAD_CONST 1 (<code object circle_area at 0x0000023D04D3F310, file "D:\myspace\code\pythonCode\mix\demo.py", line 3>)
6 LOAD_CONST 2 ('circle_area')
8 MAKE_FUNCTION 0
10 STORE_NAME 1 (circle_area)
6 12 LOAD_BUILD_CLASS
14 LOAD_CONST 3 (<code object Person at 0x0000023D04D3F5D0, file "D:\myspace\code\pythonCode\mix\demo.py", line 6>)
16 LOAD_CONST 4 ('Person')
18 MAKE_FUNCTION 0
20 LOAD_CONST 4 ('Person')
22 LOAD_NAME 2 (object)
24 CALL_FUNCTION 3
26 STORE_NAME 3 (Person)
28 LOAD_CONST 5 (None)
30 RETURN_VALUE
Disassembly of <code object circle_area at 0x0000023D04D3F310, file "D:\myspace\code\pythonCode\mix\demo.py", line 3>:
4 0 LOAD_GLOBAL 0 (PI)
2 LOAD_FAST 0 (r)
4 LOAD_CONST 1 (2)
6 BINARY_POWER
8 BINARY_MULTIPLY
10 RETURN_VALUE
Disassembly of <code object Person at 0x0000023D04D3F5D0, file "D:\myspace\code\pythonCode\mix\demo.py", line 6>:
6 0 LOAD_NAME 0 (__name__)
2 STORE_NAME 1 (__module__)
4 LOAD_CONST 0 ('Person')
6 STORE_NAME 2 (__qualname__)
7 8 LOAD_CONST 1 (<code object __init__ at 0x0000023D04D3F470, file "D:\myspace\code\pythonCode\mix\demo.py", line 7>)
10 LOAD_CONST 2 ('Person.__init__')
12 MAKE_FUNCTION 0
14 STORE_NAME 3 (__init__)
10 16 LOAD_CONST 3 (<code object say at 0x0000023D04D3F520, file "D:\myspace\code\pythonCode\mix\demo.py", line 10>)
18 LOAD_CONST 4 ('Person.say')
20 MAKE_FUNCTION 0
22 STORE_NAME 4 (say)
24 LOAD_CONST 5 (None)
26 RETURN_VALUE
Disassembly of <code object __init__ at 0x0000023D04D3F470, file "D:\myspace\code\pythonCode\mix\demo.py", line 7>:
8 0 LOAD_FAST 1 (name)
2 LOAD_FAST 0 (self)
4 STORE_ATTR 0 (name)
6 LOAD_CONST 0 (None)
8 RETURN_VALUE
Disassembly of <code object say at 0x0000023D04D3F520, file "D:\myspace\code\pythonCode\mix\demo.py", line 10>:
11 0 LOAD_GLOBAL 0 (print)
2 LOAD_CONST 1 ('i am')
4 LOAD_FAST 0 (self)
6 LOAD_ATTR 1 (name)
8 CALL_FUNCTION 2
10 POP_TOP
12 LOAD_CONST 0 (None)
14 RETURN_VALUE
操作數(shù)指定的常量或名字的實際值在旁邊的括號內列出,此外,字節(jié)碼以語句為單位進行了分組,中間以空行隔開,語句的行號在字節(jié)碼前面給出。例如PI = 3.14這個語句就被會變成了兩條字節(jié)碼:
1 0 LOAD_CONST 0 (3.14)
2 STORE_NAME 0 (PI)
6. pyc
如果將demo作為模塊導入,Python將在demo.py文件所在目錄下生成.pyc文件:
>>> import demo
pyc文件會保存經(jīng)過序列化處理的代碼對象PyCodeObject。這樣一來,Python后續(xù)導入demo模塊時,直接讀取pyc文件并反序列化即可得到代碼對象,避免了重復編譯導致的開銷。只有demo.py有新修改(時間戳比.pyc文件新),Python才會重新編譯。
因此,對比Java而言:Python中的.py文件可以類比Java中的.java文件,都是源碼文件;而.pyc文件可以類比.class文件,都是編譯結果。只不過Java程序需要先用編譯器javac命令來編譯,再用虛擬機java命令來執(zhí)行;而Python解釋器把這兩個過程都完成了。
原文鏈接:https://blog.csdn.net/xiaoli_Jenny/article/details/123135825
相關推薦
- 2022-06-06 python?利用?PrettyTable?美化表格_python
- 2022-04-10 git push異常整理 error: failed to push some refs to
- 2023-05-29 postgresql數(shù)據(jù)庫配置文件postgresql.conf,pg_hba.conf,pg_id
- 2023-04-12 在redis中防止消息丟失的機制_Redis
- 2022-01-09 uview 使用scroll-view以及swiper 做tabs
- 2022-06-12 Dockerfile文件編寫及構建鏡像命令解析_docker
- 2024-02-27 Go 讀取控制臺輸入
- 2022-05-06 golang-操作sqlite3增刪改查
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細win安裝深度學習環(huán)境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結構-簡單動態(tài)字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支