#67 转载:Python 异步编程与数据库

2020-11-22

这是大神 zzzeek 2015 年发表的一篇文章,详细介绍了关于 SQLAlchemy 与异步编程的一些事情。解答了我关于如何实现异步编程的一些疑惑。
我曾反复阅读这篇文章好多遍,以求能够更加准确地领会到大佬阐述的意思。我认为每个 Python 的使用者都应该阅读阅读。

#66 RQ 任务队列

2020-10-19

RQ (Redis Queue) is a simple Python library for queueing jobs and processing them in the background with workers. It is backed by Redis and it is designed to have a low barrier to entry. It can be integrated in your web stack easily.

翻译:RQ (Redis Queue)是一个简单的 Python 库,用于将作业排队并在后台与 worker 一起处理它们。它由 Redis 支持,其设计具有较低的进入门槛。它可以很容易地集成到您的 web 堆栈中。

This project has been inspired by the good parts of Celery, Resque and this snippet, and has been created as a lightweight alternative to existing queueing frameworks, with a low barrier to entry.

示例

启动 worker 进程:

rq worker
rq worker --url redis://:secrets@example.com:1234/9

项目中添加任务:

from redis import Redis
from rq import Queue

# 队列
q = Queue(connection=Redis())

# 添加任务
from my_module import count_words_at_url
result = q.enqueue(count_words_at_url, 'http://nvie.com')

# 关于重试
from rq import Retry
# 失败之后立即重试
queue.enqueue(say_hello, retry=Retry(max=3))
# 失败之后间隔指定时间重试
queue.enqueue(say_hello, retry=Retry(max=3, interval=[10, 30, 60]))

获取结果:


参考资料与拓展阅读

#65 Python 删除文件

2020-09-27

平时删除文件都是 os.unlink 和 os.remove 中随便选一个,今天突然想看看这两个方法有什么不一样。

remove 和 unlink 实际上来自 Modules/posixmodule.c
可以看到这两个方法实际上相同。

/*[clinic input]
os.remove = os.unlink
Remove a file (same as unlink()).
If dir_fd is not None, it should be a file descriptor open to a directory,
  and path should be relative; path will then be relative to that directory.
dir_fd may not be implemented on your platform.
  If it is unavailable, using it will raise a NotImplementedError.
[clinic start generated code]*/

static PyObject *
os_remove_impl(PyObject *module, path_t *path, int dir_fd)
/*[clinic end generated code: output=a8535b28f0068883 input=e05c5ab55cd30983]*/
{
    return os_unlink_impl(module, path, dir_fd);
}

Python 3 的 Path 对象中也有一个 unlink 方法(pathlib.Path.unlink):

def unlink(self, missing_ok=False):
    """
    Remove this file or link.
    If the path is a directory, use rmdir() instead.
    """
    try:
        os.unlink(self)
    except FileNotFoundError:
        if not missing_ok:
            raise

顺便对删除目录做一个整理:

# os.remove(path: StrOrBytesPath, *, dir_fd: int | None = ...)
# os.unlink(path: StrOrBytesPath, *, dir_fd: int | None = ...)

os.mkdir(path: StrOrBytesPath, mode: int = ..., *, dir_fd: int | None = ...)
os.rmdir(path: StrOrBytesPath, *, dir_fd: int | None = ...)

os.makedirs(name: StrOrBytesPath, mode: int = ..., exist_ok: bool = ...)
os.removedirs(name: StrOrBytesPath)

pathlib.Path.rmdir -> os.rmdir

shutil.rmtree(path, ignore_errors=False, onerror=None, *, dir_fd=None)

#62 Python GUI 开发框架

2020-05-29
  • Tkinter 可以认为是 Python 官方支持的 GUI 框架,接近标准库的地位,样式风格就是极简。
    基于 Tcl/Tk。

  • PyQt Riverbank Computing 提供的第三方 Python 绑定,非常知名。
    风险:GPL 协议,并不是和 Qt 一样的 LGPL。也就是说,使用 PyQt 开发的软件必须开源,除非购买商业授权。特别不建议使用
    没有看到 GitHub 仓库。

  • PySide Qt 官方 Python 绑定,也叫 Qt for Python。
    据说当年 Qt 的持有者,Nokia 公司,找 Riverbank Computing 谈 PyQt 的授权问题,没有谈成,因而从新开发了这个项目。
    没有 GitHub 仓库,代码可以在 官方 cgit 上看到。
    注意:这里就指 pyside2,和更老的 pyside 区分开来。

  • PyGObject GTK 官方 Python 绑定
    原来叫 PyGTK,多好,不知道为什么改成这个名字
    https://gitlab.gnome.org/GNOME/pygobject
    https://github.com/GNOME/pygobject

  • kivy

  • wxPython https://github.com/wxWidgets/Phoenix

  • PySimpleGUI

  • DearPyGui
  • pywebview

  • beeware/toga

  • python-eel/Eel
  • flexxui/flexx 基于 Web 技术

gooey 可以快速实现命令行 GUI 化。

#61 logging 增加 TRACE 级别日志

2020-05-21

logging 内部的服务级别:

DEBUG       10
INFO        20
WARNING     30
ERROR       40
CRITICAL    50

根据使用习惯,INFO 是重要信息,DEBUG 是普通信息。线上也是开到 DEBUG 级别。
然后调试信息也是通过 DEBUG 服务打印,然后通过 conf.DEBUG_MODE 来区分是不是要打印这种 DEBUG 级别的调试信息。

觉得不甚方便,想了一下,有两种思路:

  1. 将普通信息也通过 INFO 日志打印,在日志内容中插入部分标识来表示是重要信息,比如 “&NOTICE”。
  2. 增加一个 TRACE 级别日志。

方案一感觉相对合理一些,但是对于已有项目还是方案二好。

实现

def trace(self, message, *args, **kwargs):
    if self.isEnabledFor(TRACE):
        self._log(TRACE, message, args, **kwargs)


TRACE = logging.TRACE = 5
logging.addLevelName(TRACE, 'TRACE')
logging.Logger.trace = trace

参考:syslog 日志级别

  • EMERG:系统不可用
  • ALERT:需要立即采取行动
  • CRIT:关键错误
  • ERR:一般错误
  • WARNING:警告
  • NOTICE:一般通知
  • INFO:信息性消息
  • DEBUG:调试级别的消息

参考:nodejs winston 日志级别

  • error:错误
  • warn:警告
  • info:一般信息
  • http:HTTP 请求
  • verbose:详细信息
  • debug:调试信息
  • silly:非常详细的调试信息

参考:java log4j 日志级别

  • FATAL:致命
  • ERROR:错误
  • WARN:警告
  • INFO:信息
  • DEBUG:调试
  • TRACE:跟踪

#60 PyCryptodome

2020-02-05

和 PyCrypto 的关系

PyCrypto 是 Python 界最知名的加密模块,它提供了一系列的加密算法,包括对称加密、非对称加密、哈希算法、签名算法等。
不过有一个很大的问题:上一个版本 2.6.1 发布于 2013-10-18,已经很多年没有维护了。

PyCryptodome 是 PyCrypto 的分叉,该项目在统一套代码的基础上提供了两种包:pycryptodomepycryptodomex

  1. 前者保持对 PyCrypto 的兼容,所有的代码都在 Crypto 名称下,
  2. 后者丢掉了历史包袱,放弃对 PyCrypto 的兼容,所有代码都在 Cryptodome 名称下。

  3. https://pypi.org/project/pycryptodome/

  4. https://pypi.org/project/pycryptodomex/
  5. https://www.pycryptodome.org/en/latest/
  6. https://github.com/Legrandin/pycryptodome/

其他加密模块

示例


#59 Python 打开文件的方式

2020-01-10

有同事排查 Python 项目问题的时候指出一处 open 没有关闭可能会导致句柄泄露 Handle Leak。
PS: 句柄泄漏的危害:大量资源占用可能导致性能下降,甚至由于可打开文件数达到极限,服务无法继续向外提供服务。

我看了之后告诉他,此处函数退出之后句柄会自动关闭,他还不信,下去自己研究了一会儿,可能是百度一下,过一会儿说好像确实是这样,不过他仍然很疑惑,那么 with open 的作用是什么呢?

一般我们常用上下文管理的方式(with open)来打开文件,这样可以自动关闭句柄,这是一个好的实践。

  1. 退出函数之后文件描述符的自动关闭是 CPython GC (垃圾回收机制) 的特性,非 Python 语言规范。
  2. 根据 CPython 的 GC 策略(引用计数),如果有引用,文件描述符不会被关闭,这是一个非常严重的潜在风险。
    大部分时候我们可能没有引用文件描述符,但是不能排除可能性。
    万一出现句柄泄漏,在代码库中排查可能的未关闭引用会比较麻烦。
  3. 上下文管理会自动处理异常,相当于我们的 close 方法放在 finally 块中。

无关的事情:进程退出时的句柄

进程退出时如果有没有关闭的句柄,

  1. 如果程序正常退出,
  2. 语言可能会处理一道,清理相关数据
  3. 系统会处理一道
  4. 如果程序异常退出,则只有靠系统了

至少我看到 POSIX 中有相关规定,无论任何原因或任何方式的退出,都应该:

All of the file descriptors, directory streams, conversion descriptors, and message catalog descriptors open in the calling process shall be closed.

PS: 其中提到的:

  1. 文件描述符 File Descriptors
  2. 目录流 Directory Streams
  3. 转换描述符 Conversion Descriptors
  4. 消息编码描述符 Message Catalog Descriptors

后面三个是个啥?

#58 Python DataClass

2020-01-02
import dataclasses

@dataclasses.dataclass
class User:
    user_id: int
    user_name: str
    first_name: str
    last_name: str
    age: int = 0

    def __post_init__(self):
        self.full_name = f'{self.first_name} {self.last_name}'

u = User(1, 'admin', 'Jim', 'Green')
print(u)
# User(user_id=1, user_name='admin', first_name='Jim', last_name='Green', age=0)
print(u.full_name)
# Jim Green
u.full_name = 'Han Meimei'
print(u.full_name)
# Han Meimei
print(dataclasses.asdict(u))
# {'user_id': 1, 'user_name': 'admin', 'first_name': 'Jim', 'last_name': 'Green', 'age': 0}
print(dataclasses.astuple(u))
# (1, 'admin', 'Jim', 'Green', 0)

相当于:

class User:
    def __init__(self, user_id: int, user_name: str, first_name: str, last_name: str, age: int = 0):
        self.user_id = user_id
        self.user_name = user_name
        self.first_name = first_name
        self.last_name = last_name
        self.full_name = f'{self.first_name} {self.last_name}'
        self.age = age

    def __str__(self):
        return f"{self.__class__.__name__}(user_id={self.user_id}, user_name='{self.user_name}', first_name='{self.first_name}', last_name='{self.last_name}, age={self.age}')"

    def asdict(self):
        return {
            'user_id': self.user_id,
            'user_name': self.user_name,
            'first_name': self.first_name,
            'last_name': self.last_name,
            'age': self.age,
        }

    def astuple(self):
        return self.user_id, self.user_name, self.first_name, self.last_name, self.age,

u = User(1, 'admin', 'Jim', 'Green')
print(u)
print(u.full_name)
u.full_name = 'Han Meimei'
print(u.full_name)
print(u.asdict())
print(u.astuple())

其他特性

@dataclasses.dataclass(*, init=True, repr=True, eq=True, order=False,
    unsafe_hash=False, frozen=False, match_args=True, kw_only=False, slots=False,
    weakref_slot=False)
  • init 是否生成 __init__ 方法(默认)
  • repr 是否生成 __repr__ 方法(默认)
  • eq 是否生成 __eq__ 方法(默认)
  • order 是否生成 __lt____le____gt____ge__ 方法
  • unsafe_hash 是否生成 __hash__ 方法
    能 hash 的是不会变化对象,否则会出问题,需要谨慎使用。
  • frozen 是否允许对字段赋值
  • match_args
  • kw_only 是否仅关键词传参
  • slots 是否生成 __slots__ 方法
  • weakref_slot 是否添加 __wrakref__ 槽位(弱引用)
    指定 weakref_slot=True 而不同时指定 slots=True 将会导致错误。