【程式語言】pyc 與他們的產地

pyc 格式

python2

python2 的 magic number 和時間戳記都是 4 bytes

編譯 pyc

import

import 其他的 python 程式的時候,會把被引入的程式編譯成 .pyc 放到 __pycache__ 資料夾
這樣可以減少引入的時間

py_compile

1
2
import py_compile
py_compile.compile('test.py')

就會生成 .pyc 檔在 __pycache__ 資料夾

compileall

1
python -m compileall .

可以一次 compile 資料夾內所有檔案

反編譯 pyc

要 decompile pyc 可以使用 uncompyle6decompyle3pycdc

但是注意到 uncompyle6 對 3.6 版以上的支援沒有很好,可以看 這個 issue,作者沒錢拿也累了,而且新版本又多了新東西,cfg 更難分析,所以 fork 出去了 decompyle3 試圖重新整理並解決問題。

1
uncompyle6 test.pyc
1
2
3
4
5
6
7
8
9
10
11
# uncompyle6 version 3.3.2
# Python bytecode 3.7 (3394)
# Decompiled from: Python 3.7.0 (default, Oct 9 2018, 16:58:41)
# [GCC 5.4.0 20160609]
# Embedded file name: /home/oalieno/lib.py
# Size of source mod 2**32: 23 bytes


def f(x):
return x
# okay decompiling lib.cpython-37.pyc

marshal & dis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import marshal
import dis

# PyCodeObject
code = marshal.loads(open('test.pyc', 'rb').read()[16:])
code = compile('x = 1', 'filename', 'exec')

# bytecode
code.co_code

# disassemble PyCodeObject (with line number and some meta data)
dis.dis(code)

# disassemble bytecode (directly)
dis.dis(code.co_code)

用眼睛看 marshal dumps data

可以參考 marshal.c,格式都是一個 byte 的 type 加上後面一段 data,主要的程式碼在這裡 marshal.c line 953,這裡的 r_object 嘗試去讀一個 object 進來,裡面就用 switch case 去處理不同的 type。
比如 TYPE_INT 就是用 r_long 去讀 4 個 bytes 的 long 進來,所以 marshal.dumps(1) 就會長得像 b'\xe9\x01\x00\x00\x00',前面的 type 有時候會被 | 0x80,請看 marshal.c line 223,所以 0xe9 & (0x80 - 1) = ord('i')
另一個像是 TYPE_CODE 就先 r_long 了六次,讀了 argcount, posonlyargcount, kwonlyargcount, ... 進來,接下來才用 r_object 把 code 讀進來 ( 也就是 bytecode ),讀進來的 object 其實是 bytes 型態,也就是 bytecode 是用 bytes 型態存在 code object 裡面的,接下來再繼續把一些 consts, names, varnames, ... 讀進來。


  1. https://docs.python.org/3/library/dis.html
  2. http://unpyc.sourceforge.net/Opcodes.html
  3. https://kdr2.com/tech/main/1012-pyc-format.html
  4. https://late.am/post/2012/03/26/exploring-python-code-objects.html
Read more

【程式語言】Python GIL

GIL 解決了什麼問題

每個 python 的物件都有一個 reference count
可以透過 sys.getrefcount 這個函式查看

1
2
3
4
5
>>> import sys
>>> a = []
>>> b = a
>>> sys.getrefcount(a)
3

以上的例子,有 a, bsys.getrefcount 的參數三個 reference
在有很多 threads 的情況下我們必須防止這個 reference count 的 race condition
一個簡單的解決方法就是 GIL

為什麼選 GIL 作為解決方法

就因為簡單易用,讓開發者會想加入開發以及使用它 ( 正因為如此使得 python 這麼熱門 )

那 GIL 為什麼到現在都還沒被拿掉

因為很多提案雖然讓 multithread 效率增加但卻讓 singlethread 變慢了

python 3.2

在 python 3.2 稍微修改了 GIL 的運作機制 ( 小改進 )
原本在有 CPU-bound 和 IO-bound 的 threads 互搶時,IO-bound 要等很久才能拿回 GIL ( 詳情看這篇 )

有 GIL 怎辦

multiprocessing

用 multiprocessing 分許多 process,每個 process 會有獨立的 interpreter 和 memory space ( 不會有因為 GIL 卡住的問題 )
但是 multi-processing 比起 multi-threading 會有額外的 overhead

Alternative Python interpreters

可以用其他實作版本的 python interpreters
比如 Jython 和 IronPython 沒有 GIL


  1. https://realpython.com/python-gil/
  2. https://www.youtube.com/watch?v=Obt-vMVdM8s
  3. http://dabeaz.blogspot.com/2010/01/python-gil-visualized.html
  4. http://www.dabeaz.com/python/GIL.pdf
Read more

【程式語言】Python 的奇淫技巧

列了一些我新發現的各種花招

Function Attributes (python 2.1)

PEP 232

1
2
3
4
5
6
7
8
9
def a():
a.count += 1

a.count = 0

for i in range(10):
a()

print(a.count) # 10

跟 C++ 中,在 function 裡面宣告 static 變數差不多

Keyword-Only Arguments (python 3.0)

PEP 3102

1
2
def func(a, b, *, c = None):
pass
  • func(1, 2, 3) 不行
  • func(1, 2, c = 3) 這樣才合法

Additional Unpacking Generalizations (python 3.5)

PEP 448

1
2
3
a = [1, 2, 3]
b = [*a, 4, 5]
print(b) # [1, 2, 3, 4, 5]

Merge Dicts

1
2
3
4
a = {1: 2}
b = {3: 4}
c = {**a, **b}
print(c) # {1: 2, 3: 4}

Metaclasses

Python Metaclasses

1
2
3
4
5
6
7
8
9
10
11
12
13
class A:
pass

B = type('B',
(A,),
{
'attr': 10,
'func': lambda obj: obj.attr
})

b = B()
print(b.attr) # 10
print(b.func()) # 10
正常寫法
1
2
3
4
5
6
7
8
9
10
11
class A:
pass

class B(A):
attr = 10
def func(self):
return self.attr

b = B()
print(b.attr) # 10
print(b.func()) # 10

看看就好xD

shallow vs deep copy

Shallow vs Deep Copying of Python Objects

assign

1
2
3
4
5
6
7
8
9
10
a = [[1, 2, 3], [4, 5, 6]]
b = a

b.append('hello')
b[0].append('world')

print(a)
# [[1, 2, 3, 'world'], [4, 5, 6], 'hello']
print(b)
# [[1, 2, 3, 'world'], [4, 5, 6], 'hello']

copy

1
2
3
4
5
6
7
8
9
10
11
12
import copy

a = [[1, 2, 3], [4, 5, 6]]
b = copy.copy(a)

b.append('hello')
b[0].append('world')

print(a)
# [[1, 2, 3, 'world'], [4, 5, 6]]
print(b)
# [[1, 2, 3, 'world'], [4, 5, 6], 'new object']

deepcopy

1
2
3
4
5
6
7
8
9
10
11
12
import copy

a = [[1, 2, 3], [4, 5, 6]]
b = copy.deepcopy(a)

b.append('hello')
b[0].append('world')

print(a)
# [[1, 2, 3], [4, 5, 6]]
print(b)
# [[1, 2, 3, 'world'], [4, 5, 6], 'new object']

  • copy.copy (shallow) 只複製該物件,不會複製子物件
  • copy.deepcopy (deep) 會遞迴複製所有子物件

shallow copy on list

1
2
a = [1, 2, 3]
b = list(a)
1
2
a = [1, 2, 3]
b = a[:]
1
2
a = [1, 2, 3]
b = a.copy()
1
2
3
4
import copy

a = [1, 2, 3]
b = copy.copy(a)

annotations

function annotations

1
2
3
4
5
def func(a: int, b: list) -> int:
pass

print(func.__annotations__)
# {'a': <class 'int'>, 'b': <class 'list'>, 'return': <class 'int'>}

class annotations

1
2
3
4
5
class A():
var: int

print(A.__annotations__)
# {'var': <class 'int'>}

variable annotations

1
2
3
4
5
a: int
b: int = 2

print(__annotations__)
# {'a': <class 'int'>, 'b': <class 'int'>}

intern string

1
2
s = 'hello'
s = sys.intern(s)

把字串存進快取池,相同的字串只會存一次
做字串比對會比較快且節省空間


  1. http://guilload.com/python-string-interning/
  2. http://www.laurentluce.com/posts/python-string-objects-implementation/
Read more