实验步骤

点击【桌面连接】进入实验环境。

1 开发准备

1.1 打开编程IDE-Spyder

  1. 双击桌面上的【LXTerminal】打开终端

  2. 切换到root用户

1
sudo su

紧接着粘贴student用户的密码,方法参见4.1小节。

  1. 敲入如下命令启动Spyder:
1
spyder
  1. 将Spyder的工作目录设置为/root(默认为/root),如下图所示:

img图1 Spyder工作目录

1.2 再打开一个新的终端

我们编写程序是在IDE(比如Spyder)下实现,但是我们运行程序一般都是在终端运行的,即命令行模式下运行。原因很简单,把程序部署在服务器后,程序总不能在Spyder里面跑,一定是在命令行模式下跑。

在本实验中,编写代码在Spyder环境下,运行程序在终端环境下。

再打开一个终端的步骤如下:

  1. 双击桌面上的【LXTerminal】打开终端

  2. 切换到root用户

1
sudo su

紧接着粘贴student用户的密码。

  1. 进入到代码所在目录
1
cd /root

后面我们称这个终端叫新打开的终端。

2 原理探究

我们都知道电影的原理,连续的画面是由一帧帧单独的图片构成的。动态进度条同理,即在同一行内连续播放不同的图案。要想在同一行内连续播放不同的图案,就必须知道如何在同一行内输出时覆盖掉之前存在的字符。下面我们一起来探究这个问题。

2.1 多行输出

使用Spyder新建一个Python文件,命名为example1.py

print()函数是最常见的输出方式。

每隔0.3秒便print()一行数字,代码如下:

1
2
3
4
import time
for i in range(5):
time.sleep(0.3)
print(str(i)*10)

按Ctrl+s组合键保存代码。

在新打开的终端中输入下面命令执行程序:

1
python example1.py

观察运行效果,可以下载附件中的1.mp4,查看运行效果是否一致。附件下载方式参考4.3小节。

代码输出了很多行。但是对于进度条,应该在1行输出。

5.2.2 改进–在同一行输出

使用Spyder新建一个Python文件,命名为example2.py

print()函数的参数end控制着是否换行。

end的值默认是\n。替换为空字符即可不换行。

1
2
3
4
import time
for i in range(5):
time.sleep(0.3)
print(str(i)*10, end='')

按Ctrl+s组合键保存代码。

在新打开的终端中输入下面命令执行程序:

1
python example2.py

观察运行效果,可以下载附件中的2.mp4,查看运行效果是否一致。

现在所有的数字输出到了一行。

但我们很明显的发现,5个字符串是一次性同时输出的,和进度条的效果是不匹配的。

2.3 改进–打破一次性同时输出

使用Spyder新建一个Python文件,命名为example3.py

因为Python中的标准输入输出默认是使用行缓冲的。而对于行缓冲来说,缓冲区读取到换行符 \n 或回车符 \r 时才会刷新缓冲区。所以,我们需要使用 sys.stdout.flush() 函数在每次迭代时刷新缓冲区。

1
2
3
4
5
6
import time
import sys
for i in range(5):
time.sleep(0.3)
print(str(i)*10, end='')
sys.stdout.flush()

按Ctrl+s组合键保存代码。

在新打开的终端中输入下面命令执行程序:

1
python example3.py

观察运行效果,可以下载附件中的3.mp4,查看运行效果是否一致。

我们发现,现在已经不是一次性同时输出了。但是仍然有一个问题:字符串是在末尾追加,而不是把前面的字符覆盖。

2.4 改进–覆盖前面字符

使用Spyder新建一个Python文件,命名为example4.py

显示进度条时,应该是让新输出的字符覆盖已存在的字符。为了解决这个问题,我们只需要使用一个字符:回车符 \r。回车符的作用是回到行首,这时我们新输出的字符将会覆盖同一行内已存在的字符。

1
2
3
4
5
6
7
import time
import sys
for i in range(5):
time.sleep(0.3)
print('\r', end='')
print(str(i)*10, end='')
sys.stdout.flush()

按Ctrl+s组合键保存代码。

在新打开的终端中输入下面命令执行程序:

1
python example4.py

观察运行效果,可以下载附件中的4.mp4,查看运行效果是否一致。

2.5 改进–简化代码

使用Spyder新建一个Python文件,命名为example5.py

函数print()默认end=’\n’,而sys.stdout.write()默认sep=’ '。使用sys.stdout.write()更简洁一些。

1
2
3
4
5
6
7
import time
import sys
for i in range(5):
time.sleep(0.3)
sys.stdout.write('\r')
sys.stdout.write(str(i)*10)
sys.stdout.flush()

按Ctrl+s组合键保存代码。

在新打开的终端中输入下面命令执行程序:

1
python example5.py

输出结果和5.2.4一模一样。

2.6 简单的进度条

使用Spyder新建一个Python文件,命名为example6.py

下面将运用上面的知识实现一个最简单的进度条,以此来深入理解进度条的工作原理。

1
2
3
4
5
6
7
8
9
10
11
12
13
import time
import sys
progress_dict = {
'0':'[#####--------------------] 20%',
'1':'[##########---------------] 40%',
'2':'[###############----------] 60%',
'3':'[####################-----] 80%',
'4':'[#########################] 100%'}
for i in range(5):
time.sleep(1)
sys.stdout.write('\r')
sys.stdout.write(progress_dict[str(i)])
sys.stdout.flush()

按Ctrl+s组合键保存代码。

在新打开的终端中输入下面命令执行程序:

1
python example6.py

3 编写进度条函数

使用Spyder新建一个Python文件,命名为progressbar.py

为了让代码具有通用性,我们编写一个函数,叫show_process()。

1
2
3
4
5
6
7
# -*- coding: utf-8 -*-

import sys
import time


def show_process(current_step, max_step, message):

接下来定义3个全局变量:

1)进度条的长度。进度条太长了不好看,太短了不好看。在这里我们定义50个字符的长度

2)已完成的进度符号。已完成的进度用#表示

3)未完成的进度符号。未完成的进度用-表示

这3个全局变量决定了进度条的效果如下图所示:

img图17 进度条效果

定义3个全局变量的代码如下:

1
2
3
COMPLETED_CHAR = '#' # 进度条已完成的符号
UNCOMPLETED_CHAR = '-' # 进度条未完成的符号
MAX_SYMBOL = 50 # 一共有多少个符号(#和-)

下面是进度条最难的一步:计算应该显示几个#,计算应该显示几个-,计算进度百分比。代码如下:

1
2
3
num_completed_char = int(current_step * MAX_SYMBOL / max_step) #计算显示多少个COMPLETED_CHAR
num_uncompleted_char = MAX_SYMBOL - num_completed_char #计算显示多少个UNCOMPLETED_CHAR
percent = current_step * 100.0 / max_step #计算完成进度

使用sys.stdout.write()函数将内容显示出来,代码如下:

1
2
3
4
sys.stdout.write('\r')
process_bar = '[' + COMPLETED_CHAR * num_completed_char + UNCOMPLETED_CHAR * num_uncompleted_char + ']' + ' %.2f' % percent + '% ' + message
sys.stdout.write(process_bar) #这两句打印字符到终端
sys.stdout.flush()
1
2
3
4
sys.stdout.write('\r')
process_bar = '[' + COMPLETED_CHAR * num_completed_char + UNCOMPLETED_CHAR * num_uncompleted_char + ']' + ' %.2f' % percent + '% ' + message
sys.stdout.write(process_bar) #这两句打印字符到终端
sys.stdout.flush()

如果完成了,输出一个标识,代码如下:

1
2
if current_step >= max_step:
print('\nDone.')

进度条的函数完成后,我们来模拟一个场景。

下载150张图片;每下载一张图片需要0.2秒钟。

代码如下:

1
2
3
4
5
6
7
# 场景:下载150张图片;每下载一张图片需要0.2秒钟。
if __name__=='__main__':
image_count = 150
for i in range(1, image_count+1):
message = 'downloading image_%d.jpg (%d/%d)...' %(i, i, image_count)
show_process(i, image_count, message)
time.sleep(0.2)

按Ctrl+s组合键保存代码。

在新打开的终端中输入下面命令执行程序:

1
python progressbar.py

progressbar.py的完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# -*- coding: utf-8 -*-

import sys
import time


def show_process(current_step, max_step, message):
COMPLETED_CHAR = '#' # 进度条已完成的符号
UNCOMPLETED_CHAR = '-' # 进度条未完成的符号
MAX_SYMBOL = 50 # 一共有多少个符号(#和-)


num_completed_char = int(current_step * MAX_SYMBOL / max_step) #计算显示多少个COMPLETED_CHAR
num_uncompleted_char = MAX_SYMBOL - num_completed_char #计算显示多少个UNCOMPLETED_CHAR
percent = current_step * 100.0 / max_step #计算完成进度

sys.stdout.write('\r')
process_bar = '[' + COMPLETED_CHAR * num_completed_char + UNCOMPLETED_CHAR * num_uncompleted_char + ']' + ' %.2f' % percent + '% ' + message
sys.stdout.write(process_bar) #这两句打印字符到终端
sys.stdout.flush()

if current_step >= max_step:
print('\nDone.')


# 场景:下载150张图片;每下载一张图片需要0.2秒钟。
if __name__=='__main__':
image_count = 150
for i in range(1, image_count+1):
message = 'downloading image_%d.jpg (%d/%d)...' %(i, i, image_count)
show_process(i, image_count, message)
time.sleep(0.2)

4 实验总结

本实验掌握了命令行动态进度条的原理,并加深了对输出缓冲区、Python函数的理解。