用 Python 十年之后的一些感悟

linux系统宝塔服务器temp文件夹里总是被上传病毒php脚本 服务器时不时的会发送短信说你服务器有病毒,找到了这个tmp文件,删除了之后又有了。确实是有很多人就这么无聊,每天都攻击你的服务器。找了很久的原因,网上也提供了一大堆方法,但都是很简陋的答案,具体怎么操作并没有说。没办法只能自己去排查这个问题。从两个角度来考虑,第一是服务器系统问题,比如宝塔的漏洞。但一时半会看不出问题来。只能去排查自己的项目代码。最后了解到代码确实是存在漏洞的。但代码中也确实了很多的校验,为什么会存在漏洞了?这就不得不从上传的过程开始说起。 阅读详情

点击上方“编程派”,选择设为“设为星标”

优质文章,第一时间送达!

作者:laisky(基于 CC BY 4.0 授权许可)

原题:Python之路(内容略有调整)

来源:https://laisky.com/p/python-road

一、概述

本文起源于我在 Twitter 上发布的关于 Python 经历的一系列话题。

出于某些原因,想记录一下我过去数年使用 Python 的经验和一些感悟。毕竟算是一门把我带入互联网行业的语言,而我近期已经几乎不再写 Py 代码, 做一个记录,也许会对他人起到些微的帮助,也算是纪念与感恩了。

二、摘录

推文地址:https://twitter.com/ppcelery/status/1159620182089728000


最早接触 py 是 2010 年左右,那之前主要是使用 c、fortran 和 matlab 做数值运算。当时在做一些文件文本处理时觉得很麻烦,后来看到 NASA 说要用 py 取代 matlab,就去接触了 py。

python 那极为简洁与优美的语法给了当时的我极大的震撼,时至今日,写 py 代码对我而言依然是一种带有艺术意味的享受。


首先开宗明义的说一句:python 并不慢,至少不够慢。拿一个 web 后端来说,一台垃圾 4 核虚机,跑 4 个同步阻塞的 django,假设 django 上合理利用线程分担了阻塞操作,假设每节点每秒可以处理 50 个请求(超低估),在白天的 10 小时内就可以处理 720 万请求。而这种机器跑一天仅需要 20 块钱。


在学习 Python 以前需要强调的是:基础语法非常重要。虽然我们都不推崇过多的死记硬背,但是少量必要的死背是以后所有复杂思维活动的基础,就像五十音对于日语,通假字和常用动名词对于文言文,你不会就是不行。

一般认为,这包括数据类型(值/引用)、作用域(scope)、keyword、builtin 函数等


关于 Python 版本的选择,很多公司老项目依然在用 2.6、2.7,新项目的话建议至少选择 3.6(拥有稳定的 asyncio)。

  • 从 2.7 到 3.4 https://blog.laisky.com/p/whats-new-in-python3-4/

  • 从 3.4 到 3.5 https://blog.laisky.com/p/whats-new-in-python3-5/

  • 从 3.5 到 3.6 https://blog.laisky.com/p/whats-new-in-python3-6/

  • 从 3.6 到 3.7 https://docs.python.org/zh-cn/3/whatsnew/3.7.html


关于版本最后在说几点,建议在本地和服务器上都通过 pyenv 来管理版本,而不要去动系统自带的 python(以免引起额外的麻烦) https://blog.laisky.com/p/pyenv/

另外一点就是,如果你想写一个兼容 2、3 的工具包,你可以考虑使用 future http://python-future.org/compatible_idioms.html

最后提醒一下,2to3 这个脚本是有可能出错的。


学完基础就可以开始动手写代码了,这时候应该谨记遵守一些“通行规范”,几年前给公司内分享时做过一个摘要:

  • 风格指引 https://laisky.github.io/style-guide-cn/style-guides/source-code-style-guides/

  • 一些注意事项 https://laisky.github.io/style-guide-cn/style-guides/consensuses/


有了一定的实践经验后,你应该学习更多的包来提高自己的代码水平。

  • 值得学习的内建包 https://pymotw.com/3/

  • 值得了解的第三方包 https://github.com/vinta/awesome-python


因为 py 的哲学(import this)建议应该有且仅有一个完美的方式做一件事,所以建议优先采用且完善既有项目而不建议过多的造轮子。


一个小插曲,写这段的 Tim Peters 就是发明 timsort 的那位????。

https://en.wikipedia.org/wiki/Tim_Peters_(software_engineer)


有空时候,建议尽可能的完整读教材和文档,建立系统性的知识体系,这可以极大的提升你的眼界和思维能力。我自己读过且觉得值得推荐的针对 py 的书籍有:

  • https://docs.python.org/3/

  • learning python

  • 核心编程

  • 改进Python的91个建议

  • Python高手之路

  • Python源码剖析

  • 数据结构与算法:Python语言描述


如果你真的很喜欢 Python 的话,那我觉得你应该也会喜欢阅读 PEP,记得几年前我只要有空就会去翻阅 PEP,这相当于是 Py 的 RFC,里面记录了几乎每一项语法的设计理念与目的。我特别喜欢的 PEP 有:

  • 8

  • 3148

  • 380

  • 484 & 3107

  • 492: async

  • 440

  • 3132

  • 495 你甚至能学到历史知识????


以前听别人讲过一个比喻,静态语言是吃冒菜,一次性烫好。而动态语言是涮火锅,吃一点涮一点。

那么我觉得,GIL 就是仅有一双筷子的火锅,即使你菜很多,一次也只能涮一个。

但是,对于 I/O bound 的操作,你不必一直夹着菜,而是可以夹一些扔到锅里,这样就可以同时涮很多,提高并行效率。


GIL 在一个进程内,解释器仅能同时解释执行一条语句,这为 py 提供了天然的语句级线程安全,从很多意义上说,这都极大的简化了并行编程的难度。对于 I/O 型应用,多线程并不会受到多大影响。对于 CPU 型应用,编写一个基于 Queue 的多进程 worker 其实也就是几行的事。

(订正:应为伪指令级的线程安全)

from time import sleep
from concurrent.futures import ProcessPoolExecutor, wait
from multiprocessing import Manager, Queue


N_PARALLEL = 5


def worker(i: int, q: Queue) -> None:
    print(f'worker {i} start')
    while 1:
        data = q.get()
        if data is None:  # 采用毒丸(poison pill)方式来结束进程池
            q.put(data)
            print(f'worker {i} exit')
            return

        print(f'dealing with data {data}...')
        sleep(1)




def main():
    executor = ProcessPoolExecutor(max_workers=N_PARALLEL)  # 控制并发量
    with Manager() as manager:
        queue = manager.Queue(maxsize=50)  # 控制缓存量

        workers = [executor.submit(worker, i, queue) for i in range(N_PARALLEL)]
        for i in range(50):
            queue.put(i)

        print('all task data submitted')

        queue.put(None)
        wait(workers)
        print('all done')


main()

我经常给新人讲,是否能谨慎的对待并行编程,是一个区分初级和资深后端开发的分水岭。业界有一句老话:“没有正确的并行程序,只有不够量的并行度”,由此可见并行开发的复杂程度。

我个人认为思考并行时主要是在考虑两个问题:同步控制和资源用量。


对于同步控制,你在 thread, multiprocessing, asyncio 几个包里都会发现一系列的工具:

  • Lock 互斥锁

  • RLock 可重入锁

  • Queue 队列

  • Condition 条件锁

  • Event 事件锁

  • Semaphore 信号量

这个就不展开细谈了,属于另一个语言无关的大领域。(以前写过一个很简略的简介:并行编程中的各种锁(https://blog.laisky.com/p/concurrency-lock/))


对于资源控制,一般来说主要就是两个地方:

  • 缓存区有多大(Queue 长度)

  • 并发量有多大(workers 数量)


一般来说,前者直接确定了你内存的消耗量,最好选择一个恰好或略高于消费量的数。后者一般直接决定了你的 CPU 使用率,过高的并发量会增加切换开销,得不偿失。


既然提到了 workers,稍微简单展开一下“池”这个概念。我们经常提到线程池、进程池、连接池。说白了就是对于一些可重用的资源,不必每次都创建新的,而是使用完毕后回收留待下一个数据继续使用。比如你可以选择不断地开子线程,也可以选择预先开好一批线程,然后通过 queue 来不断的获取和处理数据。


所以说使用“池”的主要目的就是减少资源的消耗。另一个优点是,使用池可以非常方便的控制并发度(很多新人以为 Queue 是用来控制并发度的,这是错误的,Queue 控制的是缓存量)。

对于连接池,还有另一层好处,那就是端口资源是有限的,而且回收端口的速度很慢,你不断的创建连接会导致端口迅速耗尽。


这里做一个用语的订正。Queue 控制的应该是缓冲量(buffer),而不是缓存量(cache)。一般来说,我们习惯上将写入队列称为缓冲,将读取队列称为缓存(有源)。


对前面介绍的 python 中进程/线程做一个小结,线程池可以用来解决 I/O 的阻塞,而进程可以用来解决 GIL 对 CPU 的限制(因为每一个进程内都有一个 GIL)。所以你可以开 N 个(小于等于核数)进程池,然后在每一个进程中启动一个线程池,所有的线程池都可以订阅同一个 Queue,来实现真正的多核并行。


非常简单的描述一下进程/线程,对于操作系统而言,可以认为进程是资源的最小单位(在 PCB 内保存如图 1 的数据)。而线程是调度的最小单位。同一个进程内的线程共享除栈和寄存器外的所有数据。

所以在开发时候,要小心进程内多线程数据的冲突,也要注意多进程数据间的隔离(需要特别使用进程间通信)

  • 操作系统笔记:进程(https://blog.laisky.com/p/os-process/)

  • 操作系统笔记:调度(https://blog.laisky.com/p/os-scheduler/)


再简单的补充一下,进程间通信的手段有:管道、信号、消息队列、信号量、共享内存和套接字。不过在 Py 里,单机上最常用的进程间通信就是 multiprocessing 里的 Queue 和 sharedctypes。

顺带一提,因为 CPython 的 refcnt 机制,所以 COW(copy on write)并不可靠。


人们在见到别人的“错误写法”时,倾向于无视或吐槽讽刺。但是这个行为除了让自己爽一下外没有任何意义,不懂的还是不懂,最后真正发挥影响的还是那些能够描绘一整条学习路径的方法。

我一直希望能看到一个“朴素诚恳”的切合工程实践的教程,而不是网上流传的入门大全和网课兜售骗钱的框架调参速成。


关于进程间的内存隔离,补充一个简单直观的例子。可以看到普通变量 normal_v 在两个子进程内变成了两个独立的变量(都输出 1),而共享内存的 shared_v 仍然是同一个变量,分别输出了 1 和 2。

from time import sleep
from concurrent.futures import ProcessPoolExecutor, wait
from multiprocessing import Manager, Queue
from ctypes import c_int64


def worker(i, normal_v, shared_v):
    normal_v += 1            # 因为进程间内存隔离,所以每个进程都会得到 1
    shared_v.value += 1      # 因为使用了共享内存,所以会分别得到 1 和 2

    print(f'worker[{i}] got normal_v {normal_v}, shared_v {shared_v.value}')


def main():
    executor = ProcessPoolExecutor(max_workers=2)
    with Manager() as manager:
        lock = manager.Lock()
        shared_v = manager.Value(c_int64, 0, lock=lock)
        normal_v = 0

        workers = [executor.submit(worker, i, normal_v, shared_v) for i in range(2)]
        wait(workers)
        print('all done')


main()

从过去的工作经验中,我总结了一个简单粗暴的规矩:如果你要使用多进程,那么在程序启动的时候就把进程池启动起来,然后需要任何资源都请在进程内自行创建使用。如果有数据需要共享,一定要显式的采用共享内存或 queue 的方式进行传递。

见过太多在进程间共享不该共享的东西而导致的极为诡异的数据行为。


最早,一台机器从头到尾只能干一件事情。

后来,有了分时系统,我们可以开很多进程,同时干很多事。

但是进程的上下文切换开销太大,所以又有了线程,这样一个核可以一直跑一个进程,而仅需要切换进程内子线程的栈和寄存器。

直到遇到了 C10K 问题,人们发觉切换几万个线程还是挺重的,是否能更轻?


这里简单的展开一下,内存在操作系统中会被划分为内核态和用户态两部分,内核态供内核运行,用户态供普通的程序用。


应用程序通过系统 API(俗称 syscall)和内核发生交互。拿常见的 HTTP 请求来说,其实就是一次同步阻塞的 socket 调用,每次调用都会导致线程阻塞等待内核响应(内核陷入)。


而被阻塞的线程就会导致切换的发生。所以自然会问,能不能减少这种切换开销?换句话说,能不能在一个地方把事情做完,而不要切来切去的。

这个问题有两个解决思路,一是把所有的工作放进内核去做(略)。

另一个思路就是把尽可能多的工作放到用户态来做。这需要内核接口提供额外的支持:异步系统调用。


如 socket 这样的调用就支持非阻塞调用,调用后会拿到一个未就绪的 fp,将这个 fp 交给负责管理 I/O 多路复用的 selector,再注册好需要监听的事件和回调函数(或者像 tornado 一样采用定时 poll),就可以在事件就绪(如 HTTP 请求的返回已就绪)时执行相关函数。

https://github.com/tornadoweb/tornado/blob/f1824029db933d822f5b0d02583e4e6137f2bfd2/tornado/ioloop.py#L746


这样就可以实现在一个线程内,启动多个曾经会导致线程被切换的系统调用,然后在一个线程内监听这些调用的事件,谁先就绪就处理谁,将切换的开销降到了最小。

有一个需要特别注意的要点,你会发现主线程其实就是一个死循环,所有的调用都发生在这个循环之内。所以,你写的代码一定要避免任何阻塞。


听上去很美好,这是个万能方案吗?

很可惜不是的,最直接的一个问题是,并不是所有的 syscall 都提供了异步方法,对于这种调用,可以用线程池进行封装。对于 CPU 密集型调用,可以用进程池进行封装,asyncio 里提供了 executor 和协程进行联动的方法,这里提供一个线程池的简单例子,进程池其实同理。

from time import sleep
from asyncio import get_event_loop, sleep as asleep, gather, ensure_future
from concurrent.futures import ThreadPoolExecutor, wait, Future
from functools import wraps


executor = ThreadPoolExecutor(max_workers=10)
ioloop = get_event_loop()


def nonblocking(func) -> Future:
    @wraps(func)
    def wrapper(*args):
        return ioloop.run_in_executor(executor, func, *args)
    return wrapper


@nonblocking  # 用线程池封装没法协程化的普通阻塞程序
def foo(n: int):
    """假装我是个很耗时的阻塞调用"""
    print('start blocking task...')
    sleep(n)
    print('end blocking task')


async def coroutine_demo(n: int):
    """我就是个普通的协程"""

    # 协程内不能出现任何的阻塞调用,所谓一朝协程,永世协程
    # 那我偏要调一个普通的阻塞函数怎么办?
    # 最简单的办法,套一个线程池…
    await foo(n)


async def coroutine_demo_2():
    print('start coroutine task...')
    await asleep(1)
    print('end coroutine task')


async def coroutine_main():
    """一般我们会写一个 coroutine 的 main 函数,专门负责管理协程"""
    await gather(
        coroutine_demo(1),
        coroutine_demo_2()
    )


def main():
    ioloop.run_until_complete(coroutine_main())
    print('all done')


main()
  • Python3 asyncio 简介(https://blog.laisky.com/p/asyncio/)


上面的例子全部都基于 3.7,如果你还在使用 Py2,那么你也可以通过 gevent、tornado 用上协程。

我个人倾向于 tornado,因为更为白盒,而且写法和 3 接近,如果你也赞同,那么可以试试我以前给公司写的 kipp 库,基于 tornado 封装了更多的工具。

https://github.com/Laisky/kipp/blob/2bc5bda6e7f593f89be662f46fed350c9daabded/kipp/aio/__init__.py

Gevent Demo:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
Gevent Pool & Child Tasks
=========================

You can use gevent.pool.Pool to limit the concurrency of coroutines.

And you can create unlimit subtasks in each coroutine.


Benchmark
=========

    cost 2.675039052963257s for url http://httpbin.org/
    cost 2.66813588142395s for url http://httpbin.org/ip
    cost 2.674264907836914s for url http://httpbin.org/user-agent
    cost 2.6776888370513916s for url http://httpbin.org/get
    cost 3.97711181640625s for url http://httpbin.org/headers
    total cost 3.9886841773986816s
"""
import time

import gevent
from gevent.pool import Pool
import gevent.monkey


pool = Pool(10)  # set the concurrency limit
gevent.monkey.patch_socket()

try:
    import urllib2
except ImportError:
    import urllib.request as urllib2


TARGET_URLS = (
    'http://httpbin.org/',
    'http://httpbin.org/ip',
    'http://httpbin.org/user-agent',
    'http://httpbin.org/headers',
    'http://httpbin.org/get',
)


def demo_child_task():
    """Sub coroutine task"""
    gevent.sleep(2)


def demo_task(url):
    """Main coroutine

    You should wrap your each task into one entry coroutine,
    then spawn its own sub coroutine tasks.
    """
    start_ts = time.time()
    r = urllib2.urlopen(url)
    demo_child_task()
    print('cost {}s for url {}'.format(time.time() - start_ts, url))


def main():
    start_ts = time.time()
    pool.map(demo_task, TARGET_URLS)
    print('total cost {}s'.format(time.time() - start_ts))


if __name__ == '__main__':
    main()

tornado demo:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
cost 0.5578329563140869s, get http://httpbin.org/get
cost 0.5621621608734131s, get http://httpbin.org/ip
cost 0.5613000392913818s, get http://httpbin.org/user-agent
cost 0.5709919929504395s, get http://httpbin.org/
cost 0.572376012802124s, get http://httpbin.org/headers
total cost 0.5809519290924072s
"""
import time

import tornado
import tornado.web
import tornado.httpclient


TARGET_URLS = [
    'http://httpbin.org/',
    'http://httpbin.org/ip',
    'http://httpbin.org/user-agent',
    'http://httpbin.org/headers',
    'http://httpbin.org/get',
]


@tornado.gen.coroutine
def demo_hanlder(ioloop):
    for i, url in enumerate(TARGET_URLS):
        demo_task(url, ioloop=ioloop)


@tornado.gen.coroutine
def demo_task(url, ioloop=None):
    start_ts = time.time()
    http_client = tornado.httpclient.AsyncHTTPClient()
    r = yield http_client.fetch(url)
    # r is the response object
    end_ts = time.time()
    print('cost {}s, get {}'.format(end_ts - start_ts, url))
    TARGET_URLS.remove(url)
    if not TARGET_URLS:
        ioloop.stop()


def main():
    start_ts = time.time()
    ioloop = tornado.ioloop.IOLoop.instance()
    ioloop.add_future(demo_hanlder(ioloop), lambda f: None)
    ioloop.start()

    # total cost will equal to the longest task
    print('total cost {}s'.format(time.time() - start_ts))


if __name__ == '__main__':
    main()

kipp demo:

from time import sleep

from kipp.aio import coroutine2, run_until_complete, sleep, return_in_coroutine
from kipp.utils import ThreadPoolExecutor, get_logger


executor = ThreadPoolExecutor(10)
logger = get_logger()


@coroutine2
def coroutine_demo():
    logger.info('start coroutine_demo')
    yield sleep(1)
    logger.info('coroutine_demo  done')
    yield executor.submit(blocking_func)
    return_in_coroutine('yeo')


def blocking_func():
    logger.info('start blocking task...')
    sleep(1)
    logger.info('blocking task return')
    return 'hard'


@coroutine2
def coroutine_main():
    logger.info('start coroutine_main')
    r = yield coroutine_demo()
    logger.info('coroutine_demo return: {}'.format(r))

    yield sleep(1)
    return_in_coroutine('coroutine_main yo')


def main():
    f = coroutine_main()
    run_until_complete(f)
    logger.info('coroutine_main return: {}'.format(f.result()))


if __name__ == '__main__':
    main()

使用 tornado 时需要注意,因为它依赖 generator 来模拟协程,所以函数无法返回,只能用 raise gen.Return 来模拟。3.4 里引入了 yield from 到 3.6 的 async/await 才算彻底解决了这个问题。还有就是小心 tornado 里的 Future 不是线程安全的。

至于 gevent,容我吐个槽,求别再提 monkey_patch 了…


https://docs.python.org/3/library/asyncio-task.html 官方文档对于 asyncio 的描述很清晰易懂,推荐一读。一个小提示,async 函数被调用后会创建一个 coroutine,这时候该协程并不会运行,需要通过 ensure_future 或 create_task 方法生成 Task 后才会被调度执行。

另外,一个进程内不要创建多个 ioloop。


做一个小结,一个简单的做法是,启动程序后,分别创建一个进程池(进程数小于等于可用核数)、线程池和 ioloop,ioloop 负责调度一切的协程,遇到阻塞的调用时,I/O 型的扔进线程池,CPU 型的扔进进程池,这样代码逻辑简单,还能尽可能的利用机器性能。一个简单的完整示例:

"""
✗ python process_thread_coroutine.py

[2019-08-11 09:09:37,670Z - INFO - kipp] - main running...
[2019-08-11 09:09:37,671Z - INFO - kipp] - coroutine_main running...
[2019-08-11 09:09:37,671Z - INFO - kipp] - io_blocking_task running...
[2019-08-11 09:09:37,690Z - INFO - kipp] - coroutine_task running...
[2019-08-11 09:09:37,691Z - INFO - kipp] - coroutine_error running...
[2019-08-11 09:09:37,691Z - INFO - kipp] - coroutine_error end, cost 0.00s
[2019-08-11 09:09:37,693Z - INFO - kipp] - cpu_blocking_task running...
[2019-08-11 09:09:38,674Z - INFO - kipp] - io_blocking_task end, cost 1.00s
[2019-08-11 09:09:38,695Z - INFO - kipp] - coroutine_task end, cost 1.00s
[2019-08-11 09:09:39,580Z - INFO - kipp] - cpu_blocking_task end, cost 1.89s
[2019-08-11 09:09:39,582Z - INFO - kipp] - coroutine_main got [None, AttributeError('yo'), None, None]
[2019-08-11 09:09:39,582Z - INFO - kipp] - coroutine_main end, cost 1.91s
[2019-08-11 09:09:39,582Z - INFO - kipp] - main end, cost 1.91s
"""


from time import sleep, time
from asyncio import get_event_loop, sleep as asleep, gather, ensure_future, iscoroutine
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor, wait
from functools import wraps

from kipp.utils import get_logger


logger = get_logger()


N_FORK = 4
N_THREADS = 10

thread_executor = ThreadPoolExecutor(max_workers=N_THREADS)
process_executor = ProcessPoolExecutor(max_workers=N_FORK)
ioloop = get_event_loop()


def timer(func):
    @wraps(func)
    def wrapper(*args, **kw):
        logger.info(f"{func.__name__} running...")
        start_at = time()
        try:
            r = func(*args, **kw)
        finally:
            logger.info(f"{func.__name__} end, cost {time() - start_at:.2f}s")

    return wrapper


def async_timer(func):
    @wraps(func)
    async def wrapper(*args, **kw):
        logger.info(f"{func.__name__} running...")
        start_at = time()
        try:
            return await func(*args, **kw)
        finally:
            logger.info(f"{func.__name__} end, cost {time() - start_at:.2f}s")

    return wrapper


@timer
def io_blocking_task():
    """I/O 型阻塞调用"""
    sleep(1)


@timer
def cpu_blocking_task():
    """CPU 型阻塞调用"""
    for _ in range(1 << 26):
        pass


@async_timer
async def coroutine_task():
    """异步协程调用"""
    await asleep(1)


@async_timer
async def coroutine_error():
    """会抛出异常的协程调用"""
    raise AttributeError("yo")


@async_timer
async def coroutine_main():
    ioloop = get_event_loop()
    r = await gather(
        coroutine_task(),
        coroutine_error(),
        ioloop.run_in_executor(thread_executor, io_blocking_task),
        ioloop.run_in_executor(process_executor, cpu_blocking_task),
        return_exceptions=True,
    )
    logger.info(f"coroutine_main got {r}")


@timer
def main():
    get_event_loop().run_until_complete(coroutine_main())


if __name__ == "__main__":
    main()

学到这一步,你已经能够熟练的运用协程、线程、进程处理不同类型的任务。接着拿上面提到的垃圾 4 核虚机举例,你现在应该可以比较轻松的实现达到 1k QPS 的服务,在白天十小时里可以处理超过一亿请求,费用依然仅 20元/天。你还有什么借口说是因为 Python 慢呢?


人们在聊到语言/框架/工具性能时,考虑的是“当程序员尽可能的优化后,工具性能会成为最终的瓶颈,所以我们一定要选一个最快的”。

但事实上是,程序员本身才是性能的最大瓶颈,而工具真正体现出来的价值,是在程序员很烂时,所能提供的兜底性能。

如果你觉得自己并不是那个瓶颈,那也没必要来听我讲了


在性能优化上有两句老话:

  • 一定要针对瓶颈做优化

  • 过早优化是万恶之源

所以我觉得要开放、冷静地看待工具的性能。在一套完整的业务系统中,框架工具往往是耗时占比最低的那个,在扩容、缓存技术如此发达的今天,你已经很难说出工具性能不够这样的话了。

成长的空间很大,多在自己身上找原因。


一个经验观察,即使在工作中不断的实际练习,对于异步协程这种全新的思维模式,从学会到能在工作中熟练运用且不犯大错,比较聪明的人也需要一个月。

换成 go 也不会好很多,await 也能实现同步写法,而且你依然需要面对我前文提到过的同步控制和资源用量两个核心问题。


简单提一下性能分析,py 可以利用 cProfile、line_profiler、memory_profiler、vprof、objgraph 等工具生成耗时、内存占用、调用关系图、火焰图等。

关于性能分析领域的更多方法论和理念,推荐阅读《性能之巅》(过去做的关于性能之巅的部分摘抄 https://twitter.com/ppcelery/status/1051832271001382912)。

必须强调:优化必须要有足够的数据支撑,包括优化前和优化后。


性能优化其实是一个非常复杂的领域,虽然上面提到的工具可以生成各式各样的看上去就很厉害的图,但是优化不是简单的你看哪慢就去改哪,而是需要有极其扎实的基础知识和全局思维的。

而且,上述工具得出的指标,在性能尚未逼近极限时,可能会有相当大的误导性,使用的时候也要小心。


有一些较为普适的经验:

  • I/O 越少越好,尽量在内存里完成

  • 内存分配越少越好,尽量复用

  • 变量尽可能少,gc 友好

  • 尽量提高局部性

  • 尽量用内建函数,不要轻率造轮子

下列方法如非瓶颈不要轻易用:

  • 循环展开

  • 内存对齐

  • zero copy(mmap、sendfile)


测试是开发人员很容易忽视的一个环节,很多人认为交给 QA 即可,但其实测试也是开发过程中的一个重要组成部分,不但可以提高软件的交付质量,还可以增进你的代码组织能力。

最常见的划分可以称之为黑盒 & 白盒,前者是只针对接口行为的测试,后者是深入了解实现细节,针对实现方式进行的针对性测试。


对 Py 开发者而言,最简单实用的工具就是 unitest.TestCase 和 pytest,在包内任何以 test*.py 命名的文件,内含 TestCase 类的以 test* 命名的方法都会被执行。

测试方法也很简单,你给定入参,然后调用想要测试的函数,然后检查其返回是否符合需求,不符合就抛出异常。

https://docs.pytest.org/en/latest/

"""
test_demo.py
"""

from unittest import TestCase
from typing import List

def demo(l: List[int]) -> int:
    return l[0]

class DemoTestCase(TestCase):

    def setUp(self):
        print("first run")

    def tearDown(self):
        print("last run")

    def test_demo(self):
        data = []
        self.assertRaises(IndexError, demo, data)

开始写测试后,你才会意识到你的很多函数非常难以测试。因为它们可能有嵌套调用,可能有内含状态,可能有外部依赖等等。

但是需要强调的是,这不但不是不写测试的理由,这其实正是写测试的目的!

通过努力地写测试,会强迫你开始编写精简、功能单一、无状态、依赖注入、避免链式调用的函数。


一个简单直观的“好坏对比”,链式调用的函数很难测试,它内含了太多其他函数的调用,一旦测试就变成了一个“集成测试”。而将其按照步骤一一拆分后,就可以对其进行精细化的“单元测试”,这可以契合你开发的步伐,步步为营稳步推进。

"""
这是很糟糕的链式调用
"""

def main():
    func1()


def func1():
    return func2()

def func2():
    return func3()

def func3():
    return "shit"



"""
这样写会好很多
"""
def step1():
    return "yoo"


def step2(v):
    return f"hello, {v}"


def step3(v):
    return f"you know nothing, {v}"


def main():
    r1 = step1()
    r2 = step2(r1)
    step3(r2)

顺带一提,对于一些无法绕开的外部调用,如网络请求、数据库请求。单元测试的准则之一就是“排除一切外部因素”,你不应该发起任何真正的外部调用的,因为这会引入不可控的数据。正确做法是通过依赖注入 Mock 对象,或者通过 patch 去改写调用的接口对象。

以前写过一篇简介:https://blog.laisky.com/p/unittest-mock/


单元测试应该兼顾黑盒、白盒。你既应该编写面对接口的案例,也应该尽可能的试探内部的实现路径(增加覆盖率)。

你还可以逐渐地把线上遇到的各种 bug 都编写为案例,这些案例会成为项目宝贵的财富,为回归测试提供强有力的支持。而且有这么多测试案例提供保护,coding 的时候也会安心很多。


在单元测试的基础上,人们发展出了 TDD,但是在实践的过程中,发现有些“狡猾的”开发会针对案例的特例进行编程。为此,人们决定应该抛弃形式,回归本源,从方法论的高度来探寻测试的道路。其中光明一方,就是 PBT,试图通过描述问题的实质,来自动生成测试案例。

一篇简介:https://blog.laisky.com/p/pbt-hypothesis/

另一个黑暗的方向就是 Fuzzing,它干脆完全忽略函数的实现,贯彻黑盒到底,通过遗传算法,随机的生成入参,以测试到宇宙尽头的决心,对函数进行死缠烂打,发掘出正常人根本想不到猜不着的犄角旮旯里的 bug。

回复下方「关键词」,获取优质资源

回复关键词「 pybook03」,立即获取主页君与小伙伴一起翻译的《Think Python 2e》电子版

回复关键词「入门资料」,立即获取主页君整理的 10 本 Python 入门书的电子版

回复关键词「m」,立即获取Python精选优质文章合集

回复关键词「book 数字」,将数字替换成 0 及以上数字,有惊喜好礼哦~

题图:pexels,CC0 授权。

好文和朋友一起看~

【Unity URP渲染管线下Shader的自定义物体半透明和投影半透明效果_半透明案例分享】 Unity Shader Graph URP渲染管线下的自定义半透明效果_半透明案例分享 阅读详情

相关推荐

2018南京大学计算机夏令营机试

1. Count number of binary strings without consecutive 1’s Given a positive integer n(3≤n≤90), count all possible distinct binary strings of length n such that there are no consecutive 1's . Examples...

climber16的博客 1万+

Python线程池 ThreadPoolExecutor 的用法及实战

✨ 前言 从Python3.2开始,标准库为我们提供了 concurrent.futures 模块,它提供了 ThreadPoolExecutor (线程池)和ProcessPoolExecutor (进程池)两个类。 相比 threading 等模块,该模块通过 submit 返回的是一个 future 对象,它是一个未来可期的对象,通过它可以获悉线程的状态主线程(或进程)中可以获取某一个线程(...

weixin_30817749的博客 1572

恶意代码实验3:Word宏病毒实验

只要下次打开这个word文档,就会执行以上代码,并将自身复制到Normal.dot(word文档的公共模板)和当前文档的ThisDocument中,同时改变函数名(模板中为Document_Close,当前文档为Document_Open),此时所有的word文档打开和关闭时,都将运行以上的病毒代码,可以加入适当的恶意代码,影响word的正常使用,本例中只是简单的跳出一个提示框。回答不正确三次,即可自动跳出病毒程序,但那时已经出现几十个文档。1、打开的宏编辑器如图所示,即可编写病毒代码。

qq_63949327的博客 6505

Python中ThreadPoolExecutor的使用

Python中ThreadPoolExecutor的使用

qq_56447863的博客 3299

二、PCIE的TLP包的封包解包原理

熟悉 PCIE 的组建包的过程,以及内部的字段细节的定 义,掌握如何发送接收 Mrd(memory read TLP)、 Mwr(Memory write TLP)、Cpl (Completion TLP)和 Cpld(Completion with data TLP)命令包,深入理解 PCIE 底层协议工作过程

k7126685的博客 3929

innovus:Clock Gating Check设置与报告

看图后发现,在设置clock gating check之前报告也会有clock gating check的值,这个就是因为timing library中会像reg一样定 library setup time值,如下图。我们常用set_clock_gating_check -setup去过约束Place阶段reg2cgate的时序,以防CTS后由于tree不平导致的时序违例。我正在「拾陆楼」和朋友们讨论有趣的话题,你⼀起来吧?

m0_61544122的博客 1011

python 中线程池的使用

Python中已经有了threading模块,为什么还需要线程池呢,线程池又是什么东西呢?在介绍线程同步的信号量机制的时候,举得例子是爬虫的例子,需要控制同时爬取的线程数,例子中创建了20个线程,而同时只允许3个线程在运行,但是20个线程都需要创建和销毁,线程的创建是需要消耗系统资源的,有没有更好的方案呢?其实只需要三个线程就行了,每个线程各分配一个任务,剩下的任务排队等待,当某个线程完成了任务的时候,排队任务就可以安排给这个线程继续执行。 这就是线程池的思想(当然没这么简单),但是自己编写线程池很难写的比

weixin_43632687的博客 1469

时序预测 | MATLAB实现NGM(1,1)多变量灰色时间序列预测

时序预测 | MATLAB实现NGM(1,1)多变量灰色时间序列预测

机器学习之心的博客,关注并私信文章链接,获取对应文章源码和数据。 1514

python线程池提交任务

2. 方式提交 这种提交方式是一条一条地提交任务: 1. 可以提交不同的任务函数; 2. 线程池的线程在执行任务时出现异常,程序不会停止,而且也看不到对应的报错信息; 3. 得到的结果是乱序的。 3. 方式提交 这种提交方式可以分批次提交任务:

优快云1csdn1的博客 1513

华为OD机试真题 新系统2026-04-08 Python&JS 实现【配置操作失败数量统计】

模拟一个系统的命令行配置,包含添加、修改、删除三项操作,详情如下: 添加操作命令:add_rulerule_id=1rule_index = 18 修改操作命令: mod_rule rule_id= 1rule_index = 100 删除操作命令:del_rulerule_id=1

MISAYAONE的博客 586

109. SAP ABAP 应用程序中的锁机制和工作原理介绍

SAP ABAP 系统中的锁概念,是 SAP ABAP 为了保证`数据一致性`而引入的一种保护机制。 像 SAP 这种复杂的企业级应用,系统中同一时间段的同一份数据,可能会有多个 Transaction(终端用户或者 API )同时进行读写操作。为了防止出现数据不一致,SAP ABAP 引入了锁机制。 这种锁机制并非数据库服务器层面的锁,而是在 SAP ABAP 应用服务器层面上工作,加锁和解锁都由专门的 ABAP Function Module 来完成。

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 1467

简述Python并发编程----多线程

Python中实现并发编程的三种方案:多线程、多进程和异步I/O。并发编程的好处在于可以提升程序的执行效率以及改善用户体验;坏处在于并发的程序不容易开发和调试,同时对其他程序来说它并不友好。 1. 提升执行性能 - 让程序中没有因果关系的部分可以并发的执行 2. 改善用户体验 - 让耗时间的操作不会造成程序的假死 进程 - 操作系统分配内存的基本单位 - 一个进程可以包含一个或多个线程 线程 ...

qq_42684157的博客 576

ADB加密实例

主要介绍如何给adb命令增加加密访问

四儿家的小祖宗的博客 2911

十年感悟之 python之路

本文由 简悦 SimpRead 转码, 原文地址 https://laisky.com/p/python-road/ Changelog: updated at 2019/9/6 一、概述 本文起源于我在 Twitter 上发布的关于 Python 经历的一系列话题。 出于某些原因,想记录一下我过去数年使用 Python 的经验和一些感悟。 毕竟算是一门把我带入互联网行业的语言,...

weixin_30840253的博客 390

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

python 线程池_用 Python 十年之后的一些感悟

优质文章,第一时间送达!作者:laisky(基于 CC BY 4.0 授权许可)原题:Python之路(内容略有调整)来源:https://laisky.com/p/python-road一、概述本文起源于我在 Twitter 上发布的关于 Python 经历的一系列话题。出于某些原因,想记录一下我过去数年使用 Python 的经验和一些感悟。毕竟算是一门把我带入互联网行业的语言,而我近期已经几乎...

weixin_39929723的博客 151

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

2011年所买的书

今年买的书较多,同时也比较杂,有计算机的,旅行的,经济的 1.背包十年 http://book.360buy.com/10282846.html 这本书,已经全部看完了,这个世界,有理想的人很多,有人的理想成了梦想,有人的理想在若干年之后终于实现。有理想,更需要有行动去实现。 作者在留学以及后来的工作期间...

weixin_30292745的博客 108

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

一木.溪桥---Python学习之路

一木.溪桥---Python学习之路1.工作十年常常想起的一些人和一些事2.选择学习Python的初衷 1.工作十年常常想起的一些人和一些事 我是一名成套电气工程师,专业是电气工程及其自动化(供配电)。2010年参加工作,回想工作前一两年有幸得到很多前辈的指导和关照,现在虽然我们不在联系,但心中依然感激!在此特别感谢在展恒实习期的才军老大军哥、小伟哥、祥哥、杰哥…真的是很多哥呀!!!球场上,工作中,家乡菜,你们都很棒!(军哥,谢谢你当初的信任和关照,你的大哥风范我努力学习,希望有一天和你一样,可以照亮别人!

FaFrFu的博客 327

Python 从业十年是种什么体验?老程序员的一篇万字经验分享

本文起源于我在 Twitter 上发布的关于 Python 经历的一系列话题。出于某些原因,想记录一下我过去数年使用 Python 的经验和一些感悟。毕竟算是一门把我带入互联网行业的语言,而我近期已经几乎不再写 Py 代码, 做一个记录,也许会对他人起到些微的帮助,也算是纪念与感恩了。

Trb701012的博客 939

我与 优快云 的这十年 | 程序员有话说

作者| Eastmount责编 | 刘静出品 | 优快云博客十年, 说长不长,说短不短,人生进度条的八分之一,都是青春,都是热血。十年, 从看博客到写博客,笔耕不辍,从未停止。或许,对其他人来说写博客很平淡,但对我来说,它可能是我这十年最重要的决定和坚守之一;考试考研也好,找工作考博也罢,再苦再忙,每个月至少一篇仍未间断,仿佛只要继续分享博客,我就还是那个奋斗踏实的小伙子。十年, 通过CSD...

程序人生的博客 1033

十年小米测试工程师感悟——写给还在迷茫中的测试朋友!

十年测试心路历程: 大部分测试人员,最开始接触都是纯功能界面测试,随着工作年限,会接触到一些常用测试工具,比如抓包,数据库,linux等。 我大学学的计算机专业,有一定的编程基础。我的软件测试职业开始和大多数测试人员一样,一开始在一家电商公司做软件功能测试。 放弃了年薪二十万的offer,挑战自动化测试: 我当时在公司得到了领导的信任,待遇方面工资加上年终奖,年薪能达到二十万。 但是在由于当时公司规模和产品的局限性,导致自动化测试始终未能应用到公司实战项目中。我觉得外面的世界很大,自动化已经流行而且越来越普

程序员-小枫的博客 5240

python threadpoolexecutor 锁_Python 从业十年是种什么体验?

点击上方“咸鱼学Python”,选择“加为星标”第一时间关注Python技术干货!作者:laisky(基于 CC BY 4.0 授权许可)原题:Python之路(内容略有调整)来源:https://laisky.com/p/python-road一、概述本文起源于我在 Twitter 上发布的关于 Python 经历的一系列话题。出于某些原因,想记录一下我过去数年使用 Python 的经...

weixin_39724004的博客 301

在特殊时期的技术感悟

2020年的开篇就犹如排山倒海般的龙卷风强势袭来,我们身在其中的每个人面对困难时,表现得都足够坚强、和有勇气去直面应对。从未有过感叹人生如此不易;从未有过感叹认真对待每一秒钟有多么重要和必须;从未有过感叹活着就是努力和自律的结果;

weixin_36537129的博客 344

开发十年现状之我的工作经历

刚来北京的时候在一家做生鲜电商的互联网公司写php,后来php和java一起写,一共在这家公司待了4年多的时间,这个阶段还属于新手期,比较迷茫也没有养成良好的学习习惯,多数是在被动学习,心态很浮躁眼高手低,很可惜浪费了那么多的时间,现在回头看那个阶段技术上前后端分离开始在业内流行,后端开始脱离页面模板技术更多的去专注业务逻辑、web2.0迈向了web2.5、区块链开始流行但没多久又销声匿迹了、互联网行业也迎来了大爆发的末期开始逐渐平稳、P2P开始暴雷,除了这些也还有很多其它的事情发生。

weixin_42298427的博客 96

python课程结课感悟_python课程

文章目录Python简介Python发展历史Python特点python介绍python的优缺点python解释器Python简介Python是一个高层次的结合了解释性、编译性、互动性和面向对象的脚本语言。Python的设计具有很强的可读性,相比其他语言经常使用英文关键字,其他语言的一些标点符号,它具有比其他语言更有特色语法结构。**Python是一种解释型语言:**这意味着开发过程中没有了编译这...

weixin_39603050的博客 1387

python 线程池_Python 从业十年是种什么体验?老程序员的一篇万字经验分享!

我“接触” Python 已有十年了,当初我们要做一个网站,有个学弟只花两天,就用 Django 开发好了后台。那是我第一次感受到了 Python 的强大魅力。不过,我正式学习和使用它,才仅有两年时间……标题中说的“从业十年”,并不是指我,而是指文章的作者。像他拥有这么长的 Python 经验的程序员并不多见,写成文章分享出来的就更少见了。所以这篇文章还挺有价值的,内容很丰富,特分享给大家,建议收...

weixin_39728213的博客 90

Python 从业十年是种什么体验?

点击“Python编程与实战”,选择“置顶公众号”第一时间获取 Python 技术干货!作者:laisky(基于 CC BY 4.0 授权许可)原题:Python之路(内容略有调整)来源...

Python编程与实战的博客 423
上一篇: 3 个不可思议的 Python 返回值
下一篇: Python 爬到的数据被加密了,可怎么办?
codingpy
博客等级 码龄11年 1869粉丝 104原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值