Python 的原生 multithreading 支援

Simon Willison 這邊看到的:「Real Multithreading is Coming to Python - Learn How You Can Use It Now」,他引用的原文在「Real Multithreading is Coming to Python - Learn How You Can Use It Now」這邊,在講 Python 3.12 將會有原生 multithreading 支援。

Python 裡知名的 GIL 問題將會用 Per-Interpreter GIL 的技術來解決,把 GIL 的 global-based 改寫變成 thread-based:

With introduction of "Per-Interpreter GIL", individual Python interpreters don't share the same GIL anymore. This level of isolation allows each of these sub-interpreters to run really concurrently.

這算是基礎建設,之後應該會有蠻長的陣痛期要轉換,尤其是各個其他程式語言寫的 library 要考慮到 thread-safe 的問題。

話說回來,PHP 就沒繼續討論過 threading 這個問題了,大家還是繼續用 process 架構在搞 XD

Ruff:用 Rust 寫的 Python Linter

Hacker News Daily 上看到「Astral (astral.sh)」這個,網站在「Astral: Next-gen Python tooling」。

裡面提到的 Ruff 專案是一套用 Rust 寫的 Python Linter,主打就是速度,從官網提供的 benchmark 就可以看出來差距:

因為是 Python ecosystem 的東西,安裝可以直接用 pip 裝預設編好的套件,而不需要透過 cargo 自己編 (當然你想要還是可以用 cagro 編)。

feedgen 測了一下,速度是真的快,這樣就比較不會嫌棄了... 要注意會冒出 .ruff_cache/ 目錄,.gitignore 要加一下。

然後用預設值先掃出 unused import 修掉,其他的有機會再看要怎麼改。

MariaDB 以及 Trac 在 arm64 上的安裝

把一台本來跑在 Vultr 上的機器搬到 AWSus-east-1 上面,除了剛好把 Ubuntu 18.04 換成 Ubuntu 22.04 外,也把本來用 x86-64 架構的機器換成用 ARMt4g.micro (都是 1GB RAM)。

就效能上來說,t4g 機器的效能很不錯,這兩年 blog 跑的也都還算順,先前公司用起來感覺也很好,然後價錢更便宜,另外加上 AWS 的三年 RI 折扣大約是 4 折的價錢,算是會想要換的主因。

在確認應用跑得起來後,買三年 RI 是 $87.15/3y,所以機器本身的費用大約是 $29.05/y,就算加上 8GB 的 EBS (gp3) 空間費用,整體比本來在 Vultr 的 $6/mo 低不少。

上面跑的是我自己的 Trac,想搬到 AWS 上一陣子了,但有幾個不確定的因素,所以連假期間才有空多花一些時間確認。

第一個是 MySQL 的部份,我自己習慣用 Percona Server 的版本,但目前還沒有 arm64 的套件可以直接裝,要用的話就得自己編以及升級。

在 2021 年的時候 blog 搬到 AWS 的時候就遇過了,本來以為這次有機會,但看了一下還是沒支援,所以還是得用 MariaDB

第二個是 Trac 1.4 只能跑在 Python 2.7 上 (mailing list 上有在討論轉到 Python 3 的事情,但看起來官方的動力也不大...),這在 18.04 的時代是沒什麼問題,但 22.04 下面不知道會爛掉多少東西。

所以只能繼續用 pyenv 扛著,但已經有預期會遇到問題,加上這次又從 MySQL 轉到 MariaDB,應該也會有些地雷...

所以跳下去後遇到的問題就跟上面提到的類似,分成兩塊。

在 MariaDB 這邊第一個遇到問題是,雖然官方有提供 APT server,但沒有在 HTTPS server 上放新的 public key,所以一定得從 key server 撈。

GnuPG 就是沒有直接從 key server 下載變成檔案的功能,一定要先塞到 keystore 裡面再 export 出來,就覺得很...

所以就冒出利用 mktemp -d/tmp 下產生暫存目錄這樣的寫法,讓 GnuPG 把 keystore 放進去,這樣至少在重開機後就會消失:

export GNUPGHOME=$(mktemp -d); gpg --recv-keys --keyserver hkp://keyserver.ubuntu.com:80 0x177F4010FE56CA3336300305F1656F24C74CD1D8; gpg --export 0x177F4010FE56CA3336300305F1656F24C74CD1D8 | sudo tee /etc/apt/trusted.gpg.d/mariadb.gpg > /dev/null; unset GNUPGHOME

這邊為了安全性,還得把官方提供的 0xF1656F24C74CD1D8 換成 0x177F4010FE56CA3336300305F1656F24C74CD1D8

另外就是整理 MariaDB 需要的 my.cnf 內容,我是拿 Percona Server 5.7 的設定檔來改,只刪掉了跟 GTID 相關的設定就會動了。

而其他 MariaDB 遇到的問題主要是設計改變的問題,在 wiki 上有提到。

接下來是 Trac 1.4 的問題,本來的安裝是用 libmysqlclient-dev,然後再安裝 mysql-python

sudo apt install -y libmysqlclient-dev
pip install mysql-python PyMySQL Pygments Trac

但單純把 libmysqlclient-dev 換成 libmariadb-dev 後,mysql-python 還是編不動,照著錯誤訊息試著 workaround (像是試著把 /usr/bin/mysql_config 指到 /usr/bin/mariadb_config) 半天還是不過,最後找資料發現要改用 mysqlclient

sudo apt install -y libmariadb-dev
pip install mysqlclient PyMySQL Pygments Trac

搞定後後續就一路看錯誤訊息解就可以了...

拿 JupyterLab 生一些圖

先前看到「Python Data Visualisation」這篇就在研究要怎麼在 Linux 桌面環境下用 Python 搭配 Altair 生圖,在寫一些文件的時候會方便一些。

但先前一直卡住 (不是生不出來,而是流程不順),所以就放在 browser tab 上面一直沒動。直到前幾天看到 JupyterLab Desktop 改版,想說來看看改得如何:「Introducing the new JupyterLab Desktop!」。

先講一下結論,安裝不算太困難,界面也不差 (畢竟是重點),但 interface bug 很多,常常按下去沒反應 XD

第一次跑的時候我先把 Python 的環境指到 pyenv~/.shim/python3

先拿文章裡的範例丟進去跑:

import altair as alt
from vega_datasets import data

source = data.stocks()

alt.Chart(source).mark_line().encode(
    x='date',
    y='price',
    color='symbol',
    strokeDash='symbol',
)

會長這樣:

然後輸出圖表的右邊的 menu icon 展開後可以選 Save as PNG 存成圖片。

雖然不能直接存到 clipboard 方便我直接貼到 Imgur,但至少可以先打磨出想要的圖,再輸出成檔案處理...

Python 的 asyncio.create_task() 的設計地雷

今天的 Hacker News Daily 上看到「The Heisenbug lurking in your async code」這篇,HN 的討論則是在「A Heisenbug lurking in async Python (textualize.io)」這。

設計上面 asyncio.create_task() 傳回的物件只有被 weak reference 到,而不是一般的 reference,所以會導致 Python 在 GC 時就真的被收走了:

Important: Save a reference to the result of this function, to avoid a task disappearing mid-execution. The event loop only keeps weak references to tasks. A task that isn’t referenced elsewhere may get garbage collected at any time, even before it’s done. For reliable “fire-and-forget” background tasks, gather them in a collection:

在前一段有提到可以用 asyncio.TaskGroup.create_task() 來做,這也是官方建議的解法,不過這個是 3.11 才新增的功能:

Note: asyncio.TaskGroup.create_task() is a newer alternative that allows for convenient waiting for a group of related tasks.

是個容易忘記然後中雷的東西,畢竟有個功能性接近的 threading,是可以抱持著 fire-and-forget 的心態在用,但這邊不是 threading XD

Pony ORM

Simon Willison 的 blog 上看到的東西:「Python’s “Disappointing” Superpowers」,裡面提到的原文是「Python’s “Disappointing” Superpowers」這篇,在講 Python 的工具。

雖然是說「disappointing」,但實際上是反義,在原文裡面提到了很多特別的工具,其中 Pony ORM 算是我覺得最有趣的了,他的寫法就非常的 Python:

select(c for c in Customer if sum(c.orders.price) > 1000)

也可以用 lambda 的形式來寫:

Customer.select(lambda c: sum(c.orders.total_price) > 1000)

這樣會產生出對應的 SQL:

SELECT "c"."id"
FROM "customer" "c"
  LEFT JOIN "order" "order-1"
    ON "c"."id" = "order-1"."customer"
GROUP BY "c"."id"
HAVING coalesce(SUM("order-1"."total_price"), 0) > 1000

不會產生 syntax error 的原因是因為他直接解讀 bytecode 分析,產生出對應的 SQL query:

A normal understanding of generator expressions suggests that the select function is consuming a generator. But that couldn’t explain the behaviour here. Instead, it actually introspects the frame object of the calling code, then decompiles the byte code of the generator expression object it finds, and builds a Query based on the AST objects.

用這樣的設計來達到語法的自由度。

看了一下也有一些 integration,像是 Flask 的「Integration with flask」與 FastAPI 的「Integration with FastAPI」。

不過應該是先看看,目前 Python 上用的主力還是 Django,有自己的 ORM 架構...

Debian 移除 Python 2 套件

Hacker News 首頁上看到 Debian 移除 Python 2 的消息:「Python 2 removed from Debian (debian.org)」,對應的 ticket 在這邊:「#1027108 - RM: python2.7 -- RoQA; Obsolete - Debian Bug report logs」。

Python 2 從 2015 年喊 EoL 喊很久,終於在 2020/04/20 發行最後一版 Python 2.7.18

大多數的套件應該都有 Python 3 的支援了,有需要的人 (像是還是沒支援 Python 3 的 Trac) 可以透過 pyenv 建立 Python 2 的環境出來跑,或是丟進 Docker 裡面跑。

銀河的歷史又翻過了一頁...

Python 上的 reals 套件 (需要 3.10+ 以上才能裝)

看到「A lightweight python3 library for arithmetic with real numbers.」這個有趣的 Python 延伸套件,可以用他進行高精度的實數運算...

一開始在 Python 3.9 環境裝,結果就跳出需要 3.10+ 的環境,想了一下,開了一個 Docker container 裝 pyenv 來測,測過以後覺得還蠻有趣的,看起來之後把預設環境變成 3.10+ 應該會裝起來用...

這個 reals 的重點在於保證顯示數字的正確性:

It allows you to compute approximations to an arbitrary degree of precision, and, contrary to most other libraries, guarantees that all digits it displays are correct.

目前支援的常數與操作有這些:

Constants: pi, e, phi
Functions related to powers: sqrt, exp, log
Operators: negation, addition, subtraction, multiplication, division, powers
Trigonometric functions: sin, sinh, csc, csch, cos, cosh, sec, sech, tan, tanh, cot, coth

用法的部份,先把 reals 拉進來:

>>> from reals import sqrt

然後用法算直覺:

>>> sqrt2 = sqrt(2)
>>> sqrt2
<reals._real.Real object at 0x10d182560 (approximate value: 1.41421)>
>>> sqrt2.evaluate(10)
'1.4142135624'
>>> '{:.10f}'.format(sqrt2)
'1.4142135624'
>>> sqrt2.to_decimal(10)
Decimal('1.4142135624')

不過作者有提到效能沒有處理到很好,所以應該是拿來快速做一些運算得到結果而已。

Pyston 改變方向,將主推模組載入的方式使用

Pyston 專案是一個想要提供更快速的 Python,而前陣子決定改變開發的方向:「Announcing 3.7-3.10 support and a new direction」。

本來的 Pyston-full 是直接修改 CPython 的 codebase 加速:

Our original product, which we’re retroactively calling Pyston-full, is a fork of the entire CPython codebase. Having users install a fully-custom version of Python lets us make changes across the Python implementation, leading to the most optimizations and largest speedups.

但這種方式的安裝與維護都需要另外搞,而且因為 ABI 不相容的問題,遇到一些套件可能會需要自己編 (甚至自己改?),不能直接用編好的 binary:

The flip side is that it is fairly intensive to set up. While we believe Pyston-full is one of the most highly-compatible alternative Python implementations available, it can be difficult to switch Python implementations regardless of the ease of use of either implementation. Compounded on this, we decided to break the ABI which requires users to recompile extension modules. In theory this is not a big deal, but in practice the lack of available binary packages is a significant disincentive to use an alternative implementation.

這樣雖然有 30% 的效能提昇,但對使用者的吸引力不高,所以打算要轉變方向,讓使用者更容易使用,這也是決定發展可以用 pip 安裝的 Pyston-lite 版本:

The sum of all of this was that while we were very happy to achieve a 30% speedup with Pyston-full, it was very difficult to get people to start using it. We decided to try a different form factor: a pip-installable extension module called Pyston-lite.

但效能的提昇就不像 Pyston-full 這麼高,Pyston-lite 只剩下 10% 了:

So while it’s a bit difficult to accept that we are now providing a 10% speedup instead of 30%, we’ve decided that it’s much more important to provide something that people are willing to use.

另外在文末有列出各版本的效能提昇 (與 CPython 3.8 比較),可以看到 CPython 3.11rc2 的提昇其實跟 Pyston-lite 差不多,除非 Pyston-lite 可以把效能疊加上去,不然就有點尷尬了:

但 Pyston 要支援 3.11 看起來會花不少功夫:

In the longer-term future we are planning to submit our JIT upstream as well, but we expect retargeting it to 3.11 to be significantly more work than the other versions due to the extensive amount of changes that were made to the interpreter in that version.

不過手上一些既有的東西好像可以掛上去測看看...

用示波器看 UDP 封包...

Hacker News Daily 上看到「From Oscilloscope to Wireshark: A UDP Story」這篇講怎麼用示波器挖出 UDP 封包的方法。不是用邏輯分析儀,而是用示波器...

上面示波器的圖片可以查到作者是用 Tektronix6 Series MSO,看起來停賣了,但類似的型號應該是百萬等級...

所以真的是打算從 L1 層一路解到 L4 層:

The rest of this post will take us from these raw voltage waveforms all the way to decoded UDP packets. Hold on tight, we're going from L1 all the way to L4.

網路設備是 VSC7448 這顆 52-port 10Gbps switch:

不過這邊提到一秒可以打 30K 的 UDP 封包出來,對於這台 switch 應該是沒滿才對,而且速度上應該也不到 10Gbps,加上作者提到的是 QSGMII,有可能是跑 1Gbps 的速度在抓:

The oscilloscope doesn't have a built-in QSGMII analyzer (and we'll want to do fairly sophisticated processing of the data), so I wanted to export waveform data to my computer.

I knew that a device on the network was emitting about 30K UDP packets per second, or one packet every 33 µs. I configured the oscilloscope to collect 100M samples at 1 TSPS (tera-sample per second, 1012), which multiplies out to 100 µs of data; this means we should catch 1-3 UDP packets.

看起來只是抓個意思意思練練手而已,抓 1 到 3 個 UDP 封包。

後面就是一堆數學處理... 看起來前面有一小段程式碼是 Python,但後面的程式碼有人知道是什麼語言嗎?