4. 流程控制

我在做振镜打标系统时,最早遇到的一个问题就是:程序怎么知道振镜准备好了?怎么控制打标顺序?说白了,这些都得靠流程控制来解决。写自动化脚本,不会控制流程,那就跟手动操作没区别。

这一章,我把条件判断和循环控制放在一起讲。因为它们在实际项目里从来不分家。你想想看,打标时是不是得先判断设备状态,然后循环处理每一个点位,遇到异常还得跳出或跳过?来,我们一个一个拆开说。

4.1 条件判断:if-elif-else

条件判断,说白了就是让程序"看情况办事"。在振镜打标里,最常见的场景是根据振镜的状态决定下一步操作。

基本语法长这样:

if 条件1:
    执行代码块1
elif 条件2:
    执行代码块2
else:
    执行代码块3

条件从if开始,中间可以加多个elif,最后用else兜底。哪个条件先满足,就执行哪个分支,剩下的全跳过。记住,elif和else不是必须的,但if必须有。

关键记忆点: 条件判断是"短路"的——一旦命中一个分支,后面的就不再检查了。这在写多条件逻辑时特别重要。

运算符速查表

运算符 含义 振镜打标示例
== 等于 status == 'ready'
!= 不等于 status != 'error'
> 大于 curr_x > limit_x
< 小于 count < max_count
>= 大于等于 speed >= 100
<= 小于等于 offset <= tolerance
and 与 ready and not error
or 或 status == 'idle' or status == 'ready'
not 非 not is_error

实战案例:振镜状态机

我在项目中遇到过一种情况:振镜控制卡在某个状态不动了,程序却还在往下发指令。后来加了一段状态判断,问题就解决了。

status = get_galvo_status()

if status == 'ready':
    start_marking()
    print("开始打标")
elif status == 'busy':
    print("振镜忙,等待中...")
    wait_for_completion()
elif status == 'error':
    print("振镜报错,执行复位")
    reset_galvo()
else:
    print("未知状态,重启通信")
    restart_connection()

这段代码里,elif用了两次,对应三种已知状态。最后的else是兜底——不管前面漏了什么,都不会让程序卡死。

我的习惯:写条件判断时永远加一个else分支,哪怕里面只放一行日志。这样调试的时候一眼就能看出是否出现了预期之外的情况。

4.2 while循环

为什么要用while?举个例子:振镜在打标前需要移动到指定位置,但移动需要时间。你不能假设"指令发出就到了",得循环检测位置反馈,直到到达目标才往下走。

基本语法:

while 条件:
    循环体代码

条件为True就继续循环,为False就退出。注意,如果条件永远为True,就变成死循环了。这在某些场景下是有意为之,但大多数时候是个坑。

实战案例:等待振镜就绪

import time

def wait_for_galvo(timeout=5.0):
    """等待振镜就绪,超时返回False"""
    start = time.time()
    while not is_galvo_ready():
        if time.time() - start > timeout:
            print("等待超时!")
            return False
        time.sleep(0.05)
    return True

这个循环每50毫秒检查一次振镜状态。加了超时保护,不会无限等下去。我在项目里曾经吃过没加超时的亏——振镜硬件异常,程序卡在while里出不来,整个流程都冻住了。

我曾经踩过一个坑:写while循环时忘了更新条件变量,结果死循环把CPU跑满了。振镜控制是实时性要求高的场景,CPU被占满会导致通信丢包,打标位置偏移。所以while里一定要有让条件变化的语句,或者加time.sleep()让出CPU。

4.3 for循环

for循环用来遍历一个"可迭代对象"——列表、元组、字符串、range等。在振镜打标里,最常见的用法是遍历打标点列表。

基本语法:

for 变量 in 可迭代对象:
    循环体代码

实战案例:遍历打标点

mark_points = [
    (10.0, 20.0),
    (30.0, 50.0),
    (60.0, 80.0),
    (100.0, 120.0)
]

for index, (x, y) in enumerate(mark_points):
    print(f"处理第 {index+1} 个点: ({x}, {y})")
    move_to(x, y)
    fire_laser()

enumerate()是我很喜欢的一个内置函数,它同时返回索引和元素值,省得自己手动计数。

range() 的用法

需要按次数循环时,用range():

# 打标3次
for i in range(3):
    print(f"第 {i+1} 次打标")
    fire_laser()

range(3)生成0、1、2三个数,循环执行3次。还可以写range(1, 10, 2)表示从1到9,步长为2。

我建议:遍历列表时优先用for item in list,而不是用下标for i in range(len(list))。前者更简洁、更Pythonic,也不容易越界。

4.4 break与continue

这两个关键字是循环的"控制手柄"。它们解决的是同一个问题:循环不能一直按部就班地跑,中途需要干预。

  • break:立即退出整个循环,不再执行剩余迭代。
  • continue:跳过本次循环的剩余代码,直接进入下一次迭代。

实战案例:打标异常处理

points = [(10, 20), (30, 50), (-1, -1), (60, 80), (200, 300)]

for x, y in points:
    # 检查坐标是否超出振镜范围
    if x < 0 or y < 0:
        print(f"跳过无效点: ({x}, {y})")
        continue

    if x > 150 or y > 150:
        print(f"超出打标范围,停止打标: ({x}, {y})")
        break

    print(f"打标点: ({x}, {y})")
    move_to(x, y)
    fire_laser()

这段代码里:continue跳过负数坐标的点,程序继续处理下一个;break则在遇到超出范围的坐标时,直接停止整个打标流程。

为什么会这样设计?你想想看,坐标异常可能是数据错误,跳过就行;但超出硬件范围是严重问题,必须停下来保护设备。

区分要点: break是"不干了,全停";continue是"这个不行,下一个"。

嵌套循环中的break

注意,break只退出它所在的那一层循环,不是所有循环。多层嵌套时,需要逐层处理。

# 遍历多组打标数据
for group in mark_groups:
    for point in group:
        if is_fault(point):
            print("遇到故障点,停止当前组")
            break
    else:
        continue
    break

这里用了for...else语法——循环正常结束(没被break)时执行else块;被break了就不执行。这个写法在Python里很常见,其他语言不一定有。

4.5 综合案例:振镜打标流程控制

把上面所有知识点串起来,写一个完整的打标控制函数:

def run_marking_job(points, max_retry=3):
    """执行打标任务,带状态判断和异常处理"""
    retry_count = 0
    completed = 0

    while retry_count < max_retry:
        # 检查振镜状态
        status = get_galvo_status()
        if status == 'error':
            print("振镜异常,无法继续")
            return False
        elif status != 'ready':
            print("振镜未就绪,等待...")
            time.sleep(0.1)
            continue

        # 遍历打标点
        for point in points:
            x, y = point
            if x < 0 or y < 0:
                print(f"跳过无效点 ({x}, {y})")
                continue

            if not move_to(x, y):
                print(f"移动到 ({x}, {y}) 失败,重试")
                retry_count += 1
                break

            fire_laser()
            completed += 1
        else:
            # 所有点都打完了
            print(f"打标完成,共 {completed} 个点")
            return True

        # 如果走到这里,说明内层循环被break了
        print(f"重试第 {retry_count} 次")

    print("重试次数用尽,任务失败")
    return False

这个函数用了while做重试控制,for遍历点位,if-elif-else判断状态,continue跳过无效点,break在失败时打断内层循环。你看,流程控制的几个要素全用上了。

记得我刚开始做振镜控制时,把逻辑写成了顺序执行——从头到尾一条线。结果一遇到异常就整个崩掉。后来学会了用条件判断和循环把代码组织成"状态机"的样子,鲁棒性一下就上去了。

重要提醒:实际项目中,振镜控制通常有硬件看门狗。如果你的Python程序因为死循环或阻塞导致通信超时,看门狗会强制复位硬件。所以写循环时一定要有超时或次数限制,别让程序"死等"。

好了,流程控制就讲到这里。下面这张图展示了本章所有知识点的关系,你可以把它当做一个"速查地图"来用。

Python流程控制知识体系 条件判断分支 循环控制分支 流程控制 条件判断 循环控制 if-elif-else while循环 for循环 break continue break:立即退出当前循环 | continue:跳过本轮循环剩余代码,进入下一轮