这篇文章将深入详细地介绍 Python 反应式编程 |
反应式编程(Reactive programming):是一种基于数据流(data stream)和变化传递(propagation of change)的声明式(declarative)的编程规范。
换句话说,就是使用异步数据流进行编程,这意味着可以在编程语言中很方便地表达静态或动态的数据流,而相关的计算模型会自动将变化的值通过数据流进行传播。
反应式编程最著名的实现是 ReactiveX,其为 Reactive Extensions 的缩写,一般简写为 Rx
☕️ Rx组成
Rx有5部分组成:
观察者(observer) 包含三个基本函数:
注意:在一个正确运行的事件序列中, onCompleted() 和 onError() 有且只有一个,并且是事件序列中的最后一个。如果在队列中调用了其中一个,就不应该再调用另一个。 |
命令式编程和反应式编程的区别:
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 之间的区别:
这里需要注意的是有些操作符,比如 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
下面介绍一下各种调度器及其功能:
关于 RxPY 就先介绍这么多吧,如果想要了解更多的只是可以参考官方文档 RxPY
下面我们对 Rx 的使用进行总结一下:
学习反应式编程主要在于思维转换,因为之前主要使用同步式命令式编程的思维写程序,突然要换成以流的方式编写,思维必须要做转换,比如如何通过使用类似匹配、过滤和组合等转换函数构建集合,如何使用功能组成转换集合等等,当思维转变后,一切都会变得非常自然和顺滑