Python

在 IPython 中自动重新导入包

在使用 IPython 交互性测试编写的函数的时候,可以打开自动重新导入包的功能,这样每次保存后就可以直接测试了。

In [1]: %load_ext autoreload

In [2]: %autoreload 2

其中三个数字的含义是:

  • %autoreload 0 – 关闭自动重新导入
  • %autoreload 1 – 只在 import 语句重新导入
  • %autoreload 2 – 调用的时候自动重新导入

如果想要在 IPython 中自动启用

$ ipython profile create
$ vim ~/.ipython/profile_default/ipython_config.py
c.InteractiveShellApp.extensions = ['autoreload']
c.InteractiveShellApp.exec_lines = ['%autoreload 2']

macOS 中如何正确安装 pycurl

Reinstall the curl libraries

brew install curl --with-openssl

Install pycurl with correct environment and paths

export PYCURL_SSL_LIBRARY=openssl
pip uninstall pycurl 
pip install --no-cache-dir --global-option=build_ext --global-option="-L/usr/local/opt/openssl/lib" --global-option="-I/usr/local/opt/openssl/include"  pycurl

Python 微型ORM Peewee 教程

Python 中最著名的 ORM 自然是 sqlalchemy 了,但是 sqlalchemy 有些年头了,体积庞大,略显笨重。Peewee 还比较年轻,历史包袱比较少,也仅仅支持 Postgres、MySQL、Sqlite 这三种互联网公司最常见的数据库,所以整体上来说是比较轻量的。

peewee 在创建模型的时候就设定了数据库链接,个人感觉这个设计似乎不是很好。不过好在可以先不指定参数,而在实际使用的时候再链接数据库。

创建模型

from peewee import *

db = SqliteDatabase(None)

class Person(Model):
    name = CharField()
    birthday = DateField()

    class Meta:
        database = db # This model uses the "people.db" database.

class Pet(Model):
    owner = ForeignKeyField(Person, backref='pets')
    name = CharField()
    animal_type = CharField()

    class Meta:
        database = db # this model uses the "people.db" database

在 django 的 ORM 中,我们可以直接使用 FIELD_id 这样来访问一个外键的 id。这个在 peewee 中也是支持的。但是在设置的时候却不需要加上 _id 的后缀。在使用 where 语句的时候也不需要使用后缀。

event_id = ticket.event_id
ticket.event = new_event_id
Ticket.select().where(event == desired_event_id)

连接和创建数据库

db.init(**args)
db.connect()
db.create_table([Person])

增删改查

存储数据

跟 django 的 ORM 貌似是一样的。

from datetime import date
uncle_bob = Person(name='Bob', birthday=date(1960, 1, 15))
uncle_bob.save() # bob is now stored in the database
grandma = Person.create(name='Grandma', birthday=date(1935, 3, 1))
bob_kitty = Pet.create(owner=uncle_bob, name='Kitty', animal_type='cat')  # 带有外键的宠物

删除数据

herb_mittens.delete_instance() # he had a great life
# Returns: 1

更新数据

herb_fido.owner = uncle_bob
herb_fido.save()

读取数据

基本的语法是 Model.select().where(**coditions).get()

query = Pet.select().where(Pet.animal_type == 'cat')
for pet in query:
    print(pet.name, pet.owner.name)  # 注意这里有 N+1 问题,N 指的是获取 owner.name 

grandma = Person.get(Person.name == 'Grandma L.')

# 可以使用 join 解决 N+1 问题
query = (Pet
         .select(Pet, Person)
         .join(Person)
         .where(Pet.animal_type == 'cat'))
         .order_by(Pet.name)  # 或者 Pet.name.desc() 逆序排列

for pet in query:
    print(pet.name, pet.owner.name)

# prints:
# Kitty Bob
# Mittens Jr Herb

可以直接使用 | 来作为查询条件,这个相比 django 需要使用 Q 来说,设计地非常优雅。

d1940 = date(1940, 1, 1)
d1960 = date(1960, 1, 1)
query = (Person
         .select()
         .where((Person.birthday < d1940) | (Person.birthday > d1960)))

for person in query:
    print(person.name, person.birthday)

# prints:
# Bob 1960-01-15
# Grandma L. 1935-03-01

peewee 模仿 django 实现了 get_or_create 的方法。

保持连接

在使用数据库的时候,可能会遇到连接丢失的问题。peewee 提供了一个 Mixin 可以在连接丢失时候重连,这点比 django 方便多了。

from peewee import MySQLDatabase
from playhouse.shortcuts import ReconnectMixin

class ReconnectMySQLDatabase(ReconnectMixin, MySQLDatabase):
    pass

db = ReconnectMySQLDatabase('my_app', ...)

拓展

最后也是最牛逼的一点,可以使用 pwiz 工具从已有的数据库产生 peewee 的模型文件:

python -m pwiz -e postgresql charles_blog > blog_models.py

参考

  1. https://stackoverflow.com/questions/45345549/peewee-mysql-server-has-gone-away-error/57797698#57797698

Python 环境变量的一个坑

Python 中可以使用 os.environ 操作环境变量,前几天看到了其他几个函数 os.getenv 和 os.putenv。然而 os.putenv 是一个大坑,os.putenv 之后,在后面的 os.getenv 中并不能读出来。囧

# 参考

1. https://mail.python.org/pipermail/python-list/2013-June/650294.html

如何调试 Python 的 Core Dump

如果需要记录 Core Dump 的原因,首先需要使用 faulthandler 参数启动 Python

“`
python -X faulthandler main.py
“`

出 core 之后,可以使用 gdb 调试

“`
gdb python core
“`

参考

1. https://stackoverflow.com/questions/2663841/python-tracing-a-segmentation-fault/2664232#2664232

使用 prctl 在父进程退出的时候安全退出子进程

在 Linux 中, 当子进程退出的时候, 父进程可以收到信号, 但是当父进程退出的时候, 子进程并不会受到信号. 这样就造成了在父进程崩溃的时候, 子进程并不能同时退出, 而是一直会在后台运行, 比如下面的例子:

“`
import os
import time

def loop_print():
import time
while True:
print(‘child alive, %s’ % time.time())
time.sleep(1)

try:
pid = os.fork()
except OSError:
pass

if pid != 0: # parent
print(‘parent sleep for 2’)
time.sleep(2)
print(‘parent quit’)
else:
loop_print()
“`

当父进程退出的时候, 子进程一直在不断地 print, 而没有退出.

# naive 的方法, 使用 multiprocessing 库

昨天我已经吐槽过标准库的 multiprocessing 有很多坑, 不出所望, 在这个问题上 multiprocessing 依然提供了半个解法, 只解决了一半问题……

在使用 multiprocessing 库创建进程的时候, 可以设置 `Process.daemon = True`, 这个属性又是模仿 threading 库的 API 来的.

正常情况下, 当一个程序收到 SIGTERM 或者 SIGHUP 等信号的时候, multiprocessing 会调用每个子进程的 terminate 方法, 这样会给每个子进程发送 SIGTERM 信号, 子进程就可以优雅退出. 然而, 当异常发生的时候, 父进程挂了, 比如说收到了 SIGKILL 信号, 那么子进程就得不到收割, 也就变成了孤儿进程.

所以说, multiprocessing 库只解决了半个问题, 真遇到问题的时候就会坑你一把.

# 正确解决方法

Linux 提供了 prctl 系统调用, 可以由子进程向内核注册父进程退出时候收到什么信号, 我们只要注册一个 SIGTERM 信号就好了.

在 Python 中可以使用 python-prctl 这个包.

## 安装

“`
# apt install libcap-dev && pip install python-prctl
“`

## 使用

以上面的程序为例:

“`
import os
import time

def loop_print():
import time
import prctl
import signal
prctl.set_pdeathsig(signal.SIGTERM)
while True:
print(‘child alive, %s’ % time.time())
time.sleep(1)

try:
pid = os.fork()
except OSError:
pass

if pid != 0: # parent
print(‘parent sleep for 2’)
time.sleep(2)
print(‘parent quit’)
else:
loop_print()
“`

这次我们看到, 在父进程退出的同时, 子进程也推出了.

“`
parent sleep for 2
child alive, 1539676057.5094635
child alive, 1539676058.5105338
parent quit
“`

吐槽一下 Python 混乱的 threading 和 multiprocessing

最近要写一个库往 influxdb 中打点, 因为要被很多程序使用, 而又要创建新的进程, 为了避免引起使用方的异常, 简单深入了解了下 Python 的并发控制, 这才发现标准库真是坑. 之前没过多考虑过, 只是凭感觉在 CPU 密集的时候使用 multiprocessing, 而默认使用 threading, 其实两个还是有很多不一样的, 除了都是并发执行以外还有很大的不同. Python 中试图用 threading 和 multiprocessing 实现类似的接口来统一两方面, 结果导致更混乱了. 本文探讨几个坑.

# 在多线程环境中 fork

首先不谈 Python, 我们思考一下, 在多线程环境下如果执行 fork 会怎样? 在新的进程中, 会不会所有线程都在运行? 答案是否定的, **在 fork 之后, 只有执行 fork 的线程在运行**, 而其他线程都不会运行. 这是 POSIX 标准规定的:

> A process shall be created with a single thread. If a multi-threaded process calls fork(), the new process shall contain a replica of the calling thread and its entire address space, possibly including the states of mutexes and other resources. Consequently, to avoid errors, the child process may only execute async-signal-safe operations until such time as one of the exec functions is called. Fork handlers may be established by means of the pthread_atfork() function in order to maintain application invariants across fork() calls.

但是这时候其他线程持有的锁并不会自动转化到当前线程, 所以可能造成死锁. 关于在多线程程序中执行 fork 会造成的问题, 有好多文章有详细的讨论:

1. http://www.linuxprogrammingblog.com/threads-and-fork-think-twice-before-using-them
2. https://stackoverflow.com/questions/1073954/fork-and-existing-threads/1074663#1074663

# 在 python 的 daemon thread 中 fork 又会怎样

在 Python 中可以把线程设置为 daemon 状态, 如果一个进程中只有 daemon thread, 这个进程就会自动退出. 那么问题来了, 如果我们 daemon thread 中执行 fork 会怎样呢?

理论上来说, 既然 fork 之后只有一个线程, 而这个线程又是 daemon 线程, 那么显然这个进程应该直接退出的, 然而并不会这样, 这个进程会一直运行, 直到该线程退出. 这是因为 fork 之后, 唯一的线程自动成为了 main thread, 而 Python 中硬编码了 main thread 不是 daemon thread, 所以这个线程不会退出.

参考:

1. https://stackoverflow.com/questions/31055960/is-it-a-python-bug-that-the-main-thread-of-a-process-created-in-a-daemon-thread

# 在新创建的进程中创建线程又会怎样

在普通进程中, 进程在所有非daemon 的线程退出之后才会推出, 但是在新创建的进程中, 不论创建的线程是 daemon thread 还是不是 daemon thread 都会在主线程退出后退出. 这是 Python 的一个 [bug](https://bugs.python.org/issue18966), 这个 bug 最早在 2013-09-08 01:20 报告出来, 而直到 2017-08-16 18:54 的 Python 3.7 才修复…

如何复现这个 bug

“`
#!/usr/bin/env python

from multiprocessing import Process
from threading import Thread
from time import sleep

def proc():
mythread = Thread(target=run)
mythread.start()
print(‘Thread.daemon = %s’ % mythread.daemon)
sleep(4)
#mythread.join()

def run():
for i in range(10):
sleep(1)
print(‘Tick: %s’ % i)

if __name__ == ‘__main__’:
p = Process(target=proc)
p.start()
print(‘Process.daemon = %s’ % p.daemon)
“`

下面大概说下这个 bug 的原因:

1. 普通进程会调用 `sys.exit()` 退出, 在这个函数中会调用 `thread.join()` 也就是会等待其他线程运行结束
2. 在 Python 3.4 之前, 默认只会使用 fork 创建线程, 而对于 fork 创建的线程, 会使用 `os._exit()` 退出, 也就是不会调用 `thread.join()`. 所以也就不会等待其他线程退出
3. 在 Python 3.4 中引入了对 `spawn` 系统调用的支持, 可以通过 `multiprocessing.set_start_method` 来设定创建进程使用的系统调用. 而使用 `spawn` 调用创建的进程会通过 `sys.exit()` 退出, 也就避免了这个 bug 的影响. 而使用 `fork` 创建的进程依然受到这个 bug 的影响.
4. 在 Python 3.7 中终于在添加了 `thread._shutdown` 的调用, 也就是会 join 其他的 thread.

# fork vs spawn 造成的 OS 平台差异性

我们知道, 在 `*nix` 系统中创建一个一个新的进程可以使用系统调用 `fork`, 父进程的所有资源都会被复制到子进程中, 当然是 Copy On Write 的. 如果要执行一个新的程序, 必须在 `fork` 之后调用 `exec*` 家族的系统调用, 后来 Linux 中添加了 `spawn` 系统调用, `spawn` 和 `fork` 的不同是, 他是从头创建了一个新的子程序, 而不是像 `fork` 一样复制了一份父进程.

而在 Windows 上, 从来没有类似 `fork` 的系统调用, 只有类似 `spawn` 的系统调用, 也就是从头创建一个新的程序.

对于 Python 的影响. 在 `*nix` 操作系统上, 当使用 multiprocessing 的时候, 默认调用的是 fork, 在新的进程中所有导入的包都已经在了, 所以不会再 import 一次. 而在 Windows 系统上, 使用 multiprocessing 创建新的进程时, 所有包都会被在新进程中重新 import 一遍, 如果 import 操作是对外部系统有副作用的, 就会造成不同.

当然如上文所述, 在 Python 3.4 之后可以选择创建进程时使用的系统调用, 如果选择了 `spawn`, 那么在各个平台上行为就是统一的了.

参考:

1. 为什么要区别 fork 和 exec: https://www.zhihu.com/question/66902460
2. fork 和 spawn 造成的有趣影响: https://zhuanlan.zhihu.com/p/39542342
2. https://stackoverflow.com/questions/38236211/why-multiprocessing-process-behave-differently-on-windows-and-linux-for-global-o

# fork 和 asyncio

多进程和 Event Loop 也可能引起一些问题, [这篇文章](http://4fish.xyz/posts/asyncio-concurrency/) 给了一个很好的例子:

假设现在有一个场景,主进程运行着一个event loop,在某个时候会fork出一个子进程,子进程再去运行一个新建的event loop:

“`
async def coro(loop):
pid = os.fork()
if pid != 0: # parent
pass
else: # child
cloop = asyncio.new_event_loop()
cloop.run_forever()

loop = asyncio.get_event_loop()
asyncio.ensure_future(coro(loop), loop=loop)
loop.run_forever()
loop.close()
“`

这段代码看起来没有什么问题, 在子进程中开了一个新的 Event Loop, 然而在 Python 3.5 和以下, 在真正运行时会报错:

“`

cloop.run_forever()
File “/Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5/asyncio/base_events.py”, line 411, in run_forever
‘Cannot run the event loop while another loop is running’)
RuntimeError: Cannot run the event loop while another loop is running
“`

原因就在于标准库的 Event Loop 没有考虑多进程环境, 只是使用一个 thread local 来表示当前的 loop, 在多线程条件下, 这样当然是可以的, 但是在 fork 之后, 数据结构全部都得到了复制, 因此子进程就会检查到已经有 event loop 在运行了.

在 Python 3.6 中, 这个问题得到了简单粗暴的修复, 在每个 loop 上都标记一个 pid, 检查的时候再加上 pid 验证是不是当前进程就好了.

总而言之, 尽量不要同时使用多进程和多线程, 如果非要用的话, 首先尽早创建好需要的进程, 然后在进程中再开始创建线程或者开启 Event Loop.

还有一篇文章没看, 用空了再看下吧, 是讲 multiprocessing.Pool 的坑:

1. https://codewithoutrules.com/2018/09/04/python-multiprocessing/

Python 高性能请求库 aiohttp 的基本用法

aiohttp 是 Python 异步编程最常用的一个 web 请求库了, 依托于 asyncio, 性能非常吓人. 下面列举几个常见的用法:

# 最基础: 并发下载网页

“`
import aiohttp
import asyncio

async def fetch(session, url):
async with session.get(url) as response:
return await response.text()

async def main():
urls = [
‘http://python.org’,
‘https://google.com’,
‘http://yifei.me’
]
tasks = []
async with aiohttp.ClientSession() as session:
for url in urls:
tasks.append(fetch(session, url))
htmls = await asyncio.gather(*tasks)
for html in htmls:
print(html[:100])

if __name__ == ‘__main__’:
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
“`

requests cookies 为空的一个坑

有时候,requests 返回的 cookies 会为空,原因是链接发生了 301/302 跳转,而 cookies 是跟着第一个响应返回的,第二个响应没有返回 Set-Cookie header。所以直接读取 r.cookies 是空的,而在 session.cookies 中是有数据的。

解决方法是直接读 s.cookies。

“`
s = requests.Session()
r = s.get(‘http://httpbin.org/cookies/set?foo=bar’)
cookies = requests.utils.dict_from_cookiejar(s.cookies)
s.cookies.clear()
“`

不过需要注意的是如果在多线程环境中使用 session 需要注意锁的问题,建议把 session 设置成 thread local 的类型。

Python 中的 GC(垃圾回收)

# 引用计数(reference counting)

CPython 中默认使用的垃圾回收算法是 Reference Counting。也就是对每个元素标记有多少个其他元素引用了它,当引用数降到零的时候就删除。

1. 当对象增加一个引用,比如赋值给变量,属性或者传入一个方法,引用计数执行加1运算。
2. 当对象减少一个引用,比如变量离开作用域,属性被赋值为另一个对象引用,属性所在的对象被回收或者之前传入参数的方法返回,引用计数执行减1操作。
3. 当引用计数变为0,代表该对象不被引用,可以标记成垃圾进行回收。

为了解决循环引用的问题,CPython 使用了 Cyclic GC,遍历所有的环,并且把每一个元素的引用减一,来检测每一个引用环是不是循环应用。

![](https://ws2.sinaimg.cn/large/006tKfTcly1ftg5mu2087j30we0i6gv5.jpg)

# 标记删除(Mark and Sweep)

1. 从某一个已知的还活着的对象开始,便利对象,如果经过了某个对象就认为是活着的
2. 如果没有被标记的就删除

避免了循环引用的问题

![](https://ws1.sinaimg.cn/large/006tKfTcly1ftf9x2kejlj30wo0ic11s.jpg)

实际的处理过程

![](https://ws4.sinaimg.cn/large/006tKfTcly1ftfa55k2e7j30wi0ick3k.jpg)

![](https://ws3.sinaimg.cn/large/006tKfTcly1ftfa6amfmmj30wc0iedr8.jpg)

Pluggable
Generational
Incremental

参考资料:

1. https://www.youtube.com/watch?v=iHVs_HkjdmI
2. https://droidyue.com/blog/2015/06/05/how-garbage-collector-handles-circular-references/
3. https://www.cnblogs.com/Xjng/p/5128269.html
4. https://foofish.net/python-gc.html