【Python】 之详解 python 反应式编程

这篇文章将深入详细地介绍 Python 反应式编程

反应式编程介绍

反应式编程(Reactive programming):是一种基于数据流(data stream)和变化传递(propagation of change)的声明式(declarative)的编程规范。

换句话说,就是使用异步数据流进行编程,这意味着可以在编程语言中很方便地表达静态或动态的数据流,而相关的计算模型会自动将变化的值通过数据流进行传播。

反应式编程最著名的实现是 ReactiveX,其为 Reactive Extensions 的缩写,一般简写为 Rx

☕️ Rx组成

Rx有5部分组成:

  • Observable:被观察者或者叫数据发射源,可以被观察者订阅,被观察者会将数据 push 给所有的订阅者
  • Observer/Subscriber:观察者或者订阅者,接收 Observable 推送的数据
  • Subscribe:订阅操作,把观察者订阅到 Observable 中
  • Operators:操作符,以对数据流进行各种操作,包括创建、转换、过滤、组装、合并、筛选等等
  • Schedulers:调度器,是 Rx 的线程池,可以指定线程池来操作中执行的任务。我们可以通过 subscribe_on 来指定 Observable 的任务在哪个线程池中执行,也可以通过 subscribe_on 来指定订阅者/观察者们在哪个线程执行

观察者(observer) 包含三个基本函数:

  • no_next():基本事件,用于传递项。
  • no_completed():事件队列的全部事件都完结。当不会再有新的 on_next() 发出时,需要触发 on_completed() 方法作为标志。
  • on_error():事件队列异常。在事件处理过程中出异常时,on_error() 会被触发,会发出错误消息,同时队列自动终止,不允许再有事件发出
注意:在一个正确运行的事件序列中, onCompleted() 和 onError() 有且只有一个,并且是事件序列中的最后一个。如果在队列中调用了其中一个,就不应该再调用另一个。

命令式编程和反应式编程的区别:

  • 命令式编程,重视控制(执行过程),以运算、循环、条件判断、跳转来完成任务,计算机为先的思维,指令驱动机器做事,容易引入大量状态变量
  • 反应式编程,重视任务的解决(执行结果),关注数据转换和转换的组合,属于人脑思维、任务驱动、分治的类型,有明确的输入和输出状态

Rx 的操作

Rx 的所有操作都是基于操作符 Operator 的,换句话说,操作符是 Rx 的核心。操作符又分为:创建操作符、转换操作符、过滤操作符、合并操作符等等,下面我们通过操作符来介绍 Rx 所支持的各种操作。

这里使用的 Rx 库版本为 RxPy 1.6.x

创建 Observable

我们先以一个代码为例来讲解如何创建 Observable

from rx import Observable, Observer


def push_five_strings(observer):
	"""
	订阅函数,用于调用观察者的 on_next、on_completed、on_error 方法
	"""
    observer.on_next("Alpha")
    observer.on_next("Beta")
    observer.on_next("Gamma")
    observer.on_next("Delta")
    observer.on_next("Epsilon")
    observer.on_completed()


class PrintObserver(Observer):
	"""
	继承Observer重写观察者的 on_next、on_completed、on_error 方法
	"""
    def on_next(self, value):
        print("Received {0}".format(value))

    def on_completed(self):
        print("Done!")

    def on_error(self, error):
        print("Error Occurred: {0}".format(error))

# 创建 Observable 对象
source = Observable.create(push_five_strings)

# 订阅观察者
source.subscribe(PrintObserver())

>>>>>
Received Alpha
Received Beta
Received Gamma
Received Delta
Received Epsilon
Done!

在这个例子中,使用的是 create 操作符来创建 Observable。create 操作符需要提供一个订阅函数,比如例子中的 push_five_strings(observer),这个订阅函数会接收一个观察者对象(observer),在订阅函数体里面通过调用 observer 对象的 on_next 方法来发射数据、通过调用 on_completed 方法来通知所有数据发射完成以及通过调用 on_error 方法来通知数据出错。创建完成后返回一个 Observable 对象 source,然后调用 Observable 对象的 subscribe 方法订阅一个观察者对象到 Observable 序列中,这个观察者对象就是类 PrintObserver 的一个实例对象 PrintObserver()。

一旦创建并订阅成功后,Observable 将会把观察者对象传给订阅函数 push_five_strings,然后执行订阅函数,在订阅函数里面连续调用了5次观察者的 on_next() 函数,并提供一个字符串作为数据值,最后调用观察者的 on_completed() 方法结束整个流程。

如果数据源是可以迭代的对象,我们还可以使用 from_ 操作符来创建 Observable 对象。from_ 操作符的功能就是把一些可迭代的对象或数据转换为一个 Observable 对象,比如:

from rx import Observable, Observer


class PrintObserver(Observer):
	"""
	继承Observer重写观察者的 on_next、on_completed、on_error 方法
	"""
    def on_next(self, value):
        print("Received {0}".format(value))

    def on_completed(self):
        print("Done!")

    def on_error(self, error):
        print("Error Occurred: {0}".format(error))

# 创建 Observable 对象
source = Observable.from_(["Alpha", "Beta", "Gamma", "Delta", "Epsilon"])

# 订阅观察者
source.subscribe(PrintObserver())

>>>>>
Received Alpha
Received Beta
Received Gamma
Received Delta
Received Epsilon

当然,我们还可以使用 1 到 3 个 lambda 函数来替代观察者对象:

from rx import Observable

source = Observable.from_(["Alpha", "Beta", "Gamma", "Delta", "Epsilon"])

source.subscribe(on_next=lambda value: print("Received {0}".format(value)),
                 on_completed=lambda: print("Done!"),
                 on_error=lambda error: print("Error Occurred: {0}".format(error))
                 )

subscribe 里面的三个 lambda 函数都是可选的,如果我们只给出一个 lambda 函数的话,则默认就是 on_next 的 lambda 函数,比如:

from rx import Observable

source = Observable.of(1, "Alpha", [2, 3])

source.subscribe(lambda i: print("Received data: {0}".format(i)))

>>>>>
Received data: 1
Received data: Alpha
Received data: [2, 3]

这里的 subscribe 函数只提供了一个函数,由于 on_next 参数位于第一个参数位置,所以,这个函数则传给了 on_next。

在这个例子中我们还使用了一个新的操作符:of 操作符,这个操作符相比 from_ 操作符来说比较方便一点,因为它使用的是可变参,直接把多种不同的数据源直接传进去就可以了,而 from_ 操作符则需要一个数据可迭代的参数,比如列表和元组。

如果只有存放在集合(列表、元组)里面的数据源怎么办?我们可以对集合进行解包

data = [1, 2, "Alpha"]  # 或者 data = (1, 2, "Alpha")
source = of(*data)      # 解包后相当于:source = of(1, 2, "Alpha")

数据流处理

我们可以使用 RxPy 里面的各种操作符(Operator)来对数据项进行处理,每个操作符都会返还一个新的 Observable 对象,这个新的 Observable 对象会对旧的 Observable 对象所发射的数据进行转换处理然后再把处理后的数据发射出去(相当于做了一个数据中转站一样)。我们以下面代码为例:

from rx import Observable, Observer

# 创建 Observable 对象
source = Observable.from_(["Alpha", "Beta", "Gamma", "Delta", "Epsilon"])

# 对数据项进行映射转换
lengths = source.map(lambda s: len(s))

# 过滤数据项
filterd = lengths.filter(lambda i: i >= 5)

# 开始订阅观察者
filterd.subscribe(on_next=lambda value: print("Received {0}".format(value)))

>>>>>
Received 5
Received 5
Received 5
Received 7

我们来仔细分析一下这个代码案例中的数据是怎么流动的:首先,我们通过 from_ 操作符创建一个原始的 Observable 对象 source,然后使用 map 操作符对 source 进行包装处理并得到一个新的 Observable 对象 lengths,map 操作符做的操作是:对来自 source 的每项数据进行映射转换,这里是把字符串 s 转换成对应字符串的长度。然后又使用 filter 操作符对 map 产生的 Observable 对象 lengths 进行包装处理得到另一个新的 Observable 对象 filterd,filter 操作符做的操作是:对来自 lengths 的每项数据进行过滤,这里过滤的条件是数据 i 的值必须大于等于 5,也就是说如果传过来的数据值只有大于等于 5 才会发射数据。最后使用 filterd 对象进行订阅,我们以第一个数据项 “Alpha” 为例来讲解订阅启动之后的数据流动:

"Alpha" >> map() >> 5 >> filter() >> true >> print("Received {0}".format(5))

整个数据流动就是这样:数据 “Alpha” 传递给 map 操作符后把数据转换成了整型 5,然后把数据 5 传给 filter 操作符过滤,5 >= 5 满足条件,然后把数据 5 交给观察者的 no_next。所以,如果传递过来的数据项的长度小于 5 时就不会发射出去,比如数据项 “Beta” 就会被过滤掉。

从上面的例子可以看出,不同的操作符对源数据进行的操作可能有很大的差异,比如 map 会对源数据进行转换,而 filter 只是对数据进行筛选并不改变数据项的内容。


操作符的链式操作

上面的例子存在一个问题,当有多个操作符同时进行操作时会产生多个 Observable 对象,有时候,我们并想要保存这些操作符产生的临时对象,Rx 给我们提供了一种操作符的链式操作,我们把上个例子改写成这样:

from rx import Observable

Observable.from_(["Alpha", "Beta", "Gamma", "Delta", "Epsilon"]) \
    .map(lambda s: len(s)) \
    .filter(lambda i: i >= 5) \
    .subscribe(lambda value: print("Received {0}".format(value)))

这种写法就清晰和易读许多了,一般我们都是建议写成这种链式操作。


事件发送

除了数据之外,Observables 还可以发送事件。通过以相同的方式对待数据和事件,还可以把他们组合起来使用,比如使用 interval 操作符生成的 Observables 会每隔一定的毫秒数就会发射一个整数数据,并且不会停止,也就是不会调用 on_completed() 函数。下面的例子展示了这种用法:

from rx import Observable

Observable.interval(1000) \
    .map(lambda i: "收到了数据 {0}".format(i)) \
    .subscribe(lambda s: print(s))

input("---- 按任意退出 -----\n")

>>>>>
---- 按任意退出 -----
收到了数据 0
收到了数据 1
收到了数据 2
收到了数据 3

上面的例子中,有人可能会困惑为什么要使用 input() 函数?是这样子的,由于 interval 操作符是在一个独立的线程(使用的是 TimeoutScheduler)中运行的,所以为了在 interval 创建的 Observables 发射数据前阻止应用程序过早的退出,而使用 input() 来阻塞主线程直到有按键输入,即 input() 结束。如果把 input() 去掉的话,一运行程序就会马上结束,丝毫看不到任何输出。


多点广播(Multicasting)

Observables 中的每一个订阅者(Subscriber) 通常会收到一个独立的发射流,也就是说他们之间互不影响。比如,发射三个随机整数到其中两个订阅者时,这两个订阅者收到的数据都是不一样的:

from rx import Observable
from random import randint

emission_ints = Observable.range(1, 3).map(lambda i: randint(1, 100))

emission_ints.subscribe(lambda data: print("subscribe_1 收到数据:{0}".format(data)))
emission_ints.subscribe(lambda data: print("subscribe_2 收到数据:{0}".format(data)))

>>>>>
subscribe_1 收到数据:64
subscribe_1 收到数据:42
subscribe_1 收到数据:18
subscribe_2 收到数据:70
subscribe_2 收到数据:33
subscribe_2 收到数据:63

为了让 Observable 推送同一个发射数据给所有订阅者(而不是为每个订阅者产生一个独立的发射流),我们可以使用 publish 操作符来把一个普通的 Observable 转换为可连接的 Observable(connectable Observable)然后再使用 connect 操作符把所有订阅者连接起来:

from rx import Observable
from random import randint

emission_ints = Observable.range(1, 3).map(lambda i: randint(1, 100)).publish()

emission_ints.subscribe(lambda data: print("subscribe_1 收到数据:{0}".format(data)))
emission_ints.subscribe(lambda data: print("subscribe_2 收到数据:{0}".format(data)))

emission_ints.connect()

>>>>>
subscribe_1 收到数据:91
subscribe_2 收到数据:91
subscribe_1 收到数据:61
subscribe_2 收到数据:61
subscribe_1 收到数据:43
subscribe_2 收到数据:43

publish 操作符的作用相当于是把 Observable 进行冷却,直到把所有的订阅者都放到同一个广播发射流之中。所以,务必确保在调用 connect 之前,把所有订阅者都建立起来,否则,connect 之后建立的订阅者都会丢失之前的数据。

当然我们还可以通过 auto_connect 来指定当订阅者数量达到多少时自动 connect 并开始发送数据:

from rx import Observable
from random import randint

emission_ints = Observable.range(1, 3).map(lambda i: randint(1, 100)).publish().auto_connect(2)

emission_ints.subscribe(lambda data: print("subscribe_1 收到数据:{0}".format(data)))
emission_ints.subscribe(lambda data: print("subscribe_2 收到数据:{0}".format(data)))

>>>>>
subscribe_1 收到数据:6
subscribe_2 收到数据:6
subscribe_1 收到数据:91
subscribe_2 收到数据:91
subscribe_1 收到数据:30
subscribe_2 收到数据:30

这个例子中,auto_connect(2) 表示当订阅者数量达到 2 时就开始发送数据,换句话说,如果没有达到指定的订阅者数量的话就不会发送数据。


组合 Observable

除了可以连接订阅者之外,我们什么还可以把多个不同的 Observable 组合在一起,比如使用 Observable.merge()、Observable.concat()、Observable.zip() 等等。甚至这些 Observable 来自不同的线程,组合在一起之后都是安全的。

下面我们通过组合 from_ 和 interval 产生的 Observable 来进行讲解:

from rx import Observable

word = Observable.from_(["Alpha", "Beta", "Gamma", "Delta", "Epsilon"])
interval = Observable.interval(1000)

Observable.zip(word, interval, lambda w, i: (w, i)) \
    .subscribe(lambda data: print(data))

input("---- 按任意退出 -----\n")

>>>>>
---- 按任意退出 -----
('Alpha', 0)
('Beta', 1)
('Gamma', 2)
('Delta', 3)
('Epsilon', 4)

这个例子中的 zip 操作符做的处理是:每次提取一个从 from_ 和 interval 产生的数据并把这两个数据组合成一个元组。当然由于组合的操作符包含 interval,所以同样为了避免提前结束程序,而使用 input 来阻塞组线程。

下面我们再介绍一下另一个合并操作符:flat_map,这个操作符会把其它 Observable 发射的数据进行映射,然后合并成一个 Observable,这种操作和简称为扁平化(flatten)操作。比如下面对 SQL 操作的例子:

from sqlalchemy import create_engine, text
from rx import Observable

engine = create_engine('sqlite:///rexon_metals.db')
conn = engine.connect()


def customer_for_id(customer_id):
    stmt = text("SELECT * FROM CUSTOMER WHERE CUSTOMER_ID = :id")
    return Observable.from_(conn.execute(stmt, id=customer_id))


# Query customers with IDs 1, 3, and 5
Observable.from_([1, 3, 5]) \
    .flat_map(lambda id: customer_for_id(id)) \
    .subscribe(lambda r: print(r))

>>>>>
(1, 'LITE Industrial', 'Southwest', '729 Ravine Way', 'Irving', 'TX', 75014)
(3, 'Re-Barre Construction', 'Southwest', '9043 Windy Dr', 'Irving', 'TX', 75032)
(5, 'Marsh Lane Metal Works', 'Southeast', '9143 Marsh Ln', 'Avondale', 'LA', 79782)

并发

为了实现并发操作,我们需要使用两个操作符:subscribe_on() 和 observe_on()。这两个操作符都需要提供一个调度器(scheduler)来决定数据源和订阅者在哪个线程中工作。我们可以使用 ThreadPoolScheduler 来创建一个可复用的工作线程池,这个后面会详细介绍。

下面是 subscribe_on 和 observe_on 之间的区别:

  • subscribe_on:用于指定数据源 Observable 在哪个调度器中运行,即 subscribe_on() 括号中的 scheduler 就是数据源 Observable 所运行的 scheduler
  • observe_on:用于指定订阅者或观察者在哪个调度器中运行,即 observe_on() 括号中的 scheduler 就是订阅者所运行的 scheduler

这里需要注意的是有些操作符,比如 interval 和 delay,本身就有默认的 scheduler,所以,这些操作符会忽略提供的调度器。

下面我们通过一个例子来演示如何使用这两个操作符来并发操作:

import time
import multiprocessing
from threading import current_thread

from rx import Observable
from rx.concurrency import ThreadPoolScheduler

# 根据 CPU 的核心个数创建线程池调度器
optimal_thread_count = multiprocessing.cpu_count()
pool_scheduler = ThreadPoolScheduler(optimal_thread_count)

# 通过休眠来模拟长时间运算
def intense_calculation(value):
    time.sleep(1)
    print("Observable run in: {0}".format(current_thread().name))
    return value

Observable.range(1, 3) \
    .map(lambda i: intense_calculation(i)) \
    .observe_on(pool_scheduler) \
    .subscribe(on_next=lambda i: print("subscriber run in: {0} data: {1}".format(current_thread().name, i)))

>>>>>
Observable run in: MainThread
subscriber run in: ThreadPoolExecutor-1_0 data: 1
Observable run in: MainThread
subscriber run in: ThreadPoolExecutor-1_0 data: 2
Observable run in: MainThread
subscriber run in: ThreadPoolExecutor-1_2 data: 3

从这个例子我们可以看出,通过 observe_on 操作符,订阅者的操作从 Observable 的主线程切换到了线程池调度器中执行,换句话说,如果没有使用 observe_on 来指定不同的线程池调度器,那么订阅者将会和 Observable 一同运行在主线程中。

下面我们来指定 Observable 所运行的调度器:

import time
import multiprocessing
from threading import current_thread

from rx import Observable
from rx.concurrency import ThreadPoolScheduler

# 根据 CPU 的核心个数创建线程池调度器
optimal_thread_count = multiprocessing.cpu_count()
pool_scheduler = ThreadPoolScheduler(optimal_thread_count)

# 通过休眠来模拟长时间运算
def intense_calculation(value):
    time.sleep(1)
    print("Observable run in: {0}".format(current_thread().name))
    return value

Observable.range(1, 3) \
    .map(lambda i: intense_calculation(i)) \
    .subscribe_on(pool_scheduler) \
    .subscribe(on_next=lambda i: print("subscriber run in: {0} data: {1}".format(current_thread().name, i)))

>>>>>
Observable run in: ThreadPoolExecutor-1_0
subscriber run in: ThreadPoolExecutor-1_0 data: 1
Observable run in: ThreadPoolExecutor-1_0
subscriber run in: ThreadPoolExecutor-1_0 data: 2
Observable run in: ThreadPoolExecutor-1_0
subscriber run in: ThreadPoolExecutor-1_0 data: 3

显然,从这个例子中我们可以看到,通过 subscribe_on 我们改变了 Observable 运行的调度器,让它运行在线程池调度器中,当然,subscriber 也跟着一起到了线程池调度器中。


python alignment

Rx 的部分操作符可以使用 python 的语法来代替,比如,concat 操作符可以使用运算符 + 来代替:

from rx import Observable

xs = Observable.from_([0, 1, 2])
ys = Observable.from_([3, 4, 5])
zs = xs + ys	# 拼接两个 Observable

zs.subscribe(lambda data: print("data: {0}".format(data)))

>>>>>
data: 0
data: 1
data: 2
data: 3
data: 4
data: 5

同时也可以使用运算符 *= 对数据源 Observable 的数据进行复制:

from rx import Observable

xs = Observable.from_([0, 1])
ys = xs * 3

ys.subscribe(lambda data: print("data: {0}".format(data)))

>>>>>
data: 0
data: 1
data: 0
data: 1
data: 0
data: 1

或者使用切片对数据源 Observable 的数据进行切割

from rx import Observable

xs = Observable.from_([0, 1, 2, 3, 4, 5, 6])
ys = xs[2:6]

ys.subscribe(lambda data: print("data: {0}".format(data)))

>>>>>
data: 2
data: 3
data: 4
data: 5

调度器 Schedulers

下面介绍一下各种调度器及其功能:

  • ThreadPoolScheduler:创建一个固定数量的线程器调度器
  • NewThreadScheduler:为每个订阅者创建新的线程
  • AsyncIOScheduler:和 AsyncIO 一起使用(要求 Python 3.4)
  • EventLeftEventScheduler:用于 Eventlet
  • IOLoopScheduler:用于 Tornado IOLoop
  • QtScheduler:用于 PyQt4、PyQt5 以及 PySide,可以参考 timeflies 的例子

关于 RxPY 就先介绍这么多吧,如果想要了解更多的只是可以参考官方文档 RxPY


总结:

下面我们对 Rx 的使用进行总结一下:

  • 理解 Rx 最关键的部分,就是理解 Rx 的流,包括流的源头(Observable)、操作 (Operation)、和终点 (Subscription)
  • 流的初始化函数,只有在被订阅时,才会执行。流的操作,只有在有数据传递过来时,才会进行,这⼀切都是异步的。(错误的理解了代码执行时机)
  • 在没有弄清楚 Operator 的意思和影响前,不要使用它
  • 小心那些不会 complete 的 observable 和收集类型的操作符比如 reduce, to_list, scan 等,必须等到 Observable complete,才会返回结果。如果发现你的操作链条完全不返回结果,看看是不是在不会 complete 的observable 上使用了收集型的操作符

学习反应式编程主要在于思维转换,因为之前主要使用同步式命令式编程的思维写程序,突然要换成以流的方式编写,思维必须要做转换,比如如何通过使用类似匹配、过滤和组合等转换函数构建集合,如何使用功能组成转换集合等等,当思维转变后,一切都会变得非常自然和顺滑

你可能感兴趣的:(Python,python,RxPy,反应式编程)