3、Python基础回顾:面向对象编程、时间模块与数学运算

好,咱们进入第三章。在正式开始搭建动画刷新系统之前,我得先带你把 Python 的几个核心基础模块过一遍。你可能会想:「这些基础东西,我早就会了。」嗯,我理解。但动画刷新系统对时间精度和对象管理的要求,跟写个爬虫或者数据处理脚本完全不一样。我当年第一次做实时渲染工具时,就是因为对 time.sleep() 的精度理解不到位,导致动画一卡一卡的,被测试同事追着骂了好几天。

所以这一章,咱们重点看三个东西:面向对象编程、时间模块、数学运算。它们是你后面所有动画逻辑的基石。

3.1 面向对象编程:把动画元素当成「对象」来管

面向对象编程,说白了就是把你的数据和对数据的操作打包在一起。在动画系统里,每一个小球、每一帧画面、每一个动画效果,都是一个对象。

3.1.1 类与对象的基本概念

类就是模板,对象就是根据模板造出来的具体实例。我习惯把类想象成「月饼模具」,对象就是「做出来的月饼」。模具只有一个,但你可以做出无数个月饼,每个月饼的馅料(数据)可以不同。

class Ball:
    """一个小球类"""
    def __init__(self, x, y, radius, color):
        self.x = x
        self.y = y
        self.radius = radius
        self.color = color
        self.speed_x = 0
        self.speed_y = 0

    def move(self):
        """移动小球"""
        self.x += self.speed_x
        self.y += self.speed_y

    def __str__(self):
        return f"Ball({self.x}, {self.y}, r={self.radius})"

# 创建两个小球对象
ball_a = Ball(100, 200, 20, "red")
ball_b = Ball(300, 150, 30, "blue")

你看,ball_a 和 ball_b 是两个独立的对象。它们有各自的坐标、半径、颜色,互不干扰。这就是面向对象最直观的好处——封装。

核心要点:__init__ 方法是构造函数,在创建对象时自动调用。self 代表对象本身,所有实例方法第一个参数必须是 self。

3.1.2 继承:让动画元素「复用」公共逻辑

在动画系统里,你会有很多种可动元素:小球、方块、文字、图片……它们都有共同的行为(移动、绘制),但具体实现不同。这时候继承就派上用场了。

class AnimatedObject:
    """所有动画对象的基类"""
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.visible = True

    def update(self):
        """更新状态,子类重写"""
        raise NotImplementedError("子类必须实现 update 方法")

    def draw(self, screen):
        """绘制到屏幕,子类重写"""
        raise NotImplementedError("子类必须实现 draw 方法")

class Ball(AnimatedObject):
    def __init__(self, x, y, radius):
        super().__init__(x, y)  # 调用父类构造函数
        self.radius = radius

    def update(self):
        # 小球的更新逻辑
        self.x += 1

    def draw(self, screen):
        # 小球的绘制逻辑
        pass  # 具体绘制代码后面再写

这里有个坑,我当年踩过:子类一定要调用 super().__init__()。如果你忘了,父类的 x 和 y 就永远不会被初始化。我曾经因为这个 bug 排查了整整一个下午,最后发现是少写了一行 super() 调用。

避坑指南:我曾经在写一个粒子系统时,把所有粒子都继承自同一个基类,但忘记在子类中调用 super().__init__(),结果所有粒子的坐标都是 None,画面上一片空白。记住:子类初始化时,先调父类的 __init__。

3.1.3 多态:同一接口,不同行为

多态让代码更灵活。你想想看,在动画循环里,你只需要遍历所有对象,调用它们的 update() 和 draw() 方法,而不需要关心它们具体是什么类型。这就是多态的魅力。

# 多态示例
objects = [Ball(100, 200, 20), Box(300, 150, 50, 50), Text(400, 300, "Hello")]

for obj in objects:
    obj.update()  # 每个对象执行自己的更新逻辑
    obj.draw(screen)  # 每个对象执行自己的绘制逻辑

我个人习惯在基类里用 raise NotImplementedError 来强制子类实现方法。这样如果你忘了实现某个方法,程序会立刻报错,而不是静默失败。

3.2 时间模块:动画的「心跳」

动画的本质,就是随时间变化画面。所以时间模块是动画系统的核心。Python 提供了 time 和 datetime 两个模块,咱们重点看 time。

3.2.1 time 模块:高精度计时

time.time() 返回当前时间戳(从 1970 年 1 月 1 日开始的秒数),精度很高。在动画里,我们用它来计算帧间隔。

import time

# 获取当前时间戳
start_time = time.time()
print(f"开始时间:{start_time}")

# 模拟动画循环
for i in range(5):
    time.sleep(0.016)  # 约 60 FPS
    elapsed = time.time() - start_time
    print(f"第 {i+1} 帧,已过 {elapsed:.3f} 秒")

小技巧:time.perf_counter() 比 time.time() 精度更高,适合做性能测量。在动画系统里,我通常用 perf_counter 来测量帧耗时。

3.2.2 帧率控制:别让动画跑飞了

如果不控制帧率,动画会跑得飞快,根本看不清。我们需要一个「帧率控制器」。

class FrameRateController:
    """帧率控制器"""
    def __init__(self, target_fps=60):
        self.target_fps = target_fps
        self.frame_duration = 1.0 / target_fps
        self.last_time = time.perf_counter()

    def wait_for_next_frame(self):
        """等待到下一帧应该开始的时间"""
        current_time = time.perf_counter()
        elapsed = current_time - self.last_time
        sleep_time = self.frame_duration - elapsed

        if sleep_time > 0:
            time.sleep(sleep_time)

        self.last_time = time.perf_counter()
        return time.perf_counter() - self.last_time  # 返回实际帧耗时

这个控制器会保证每帧之间的间隔稳定在 1/60 秒左右。如果某帧处理时间过长,它会自动跳过等待,避免动画卡死。

注意:time.sleep() 的精度受操作系统影响。在 Windows 上,它的最小精度大约是 15 毫秒。如果你需要更高精度的定时,可以考虑用 pygame 的时钟或者 time.perf_counter 做忙等待。但忙等待会占用 CPU,别在移动设备上这么干。

3.2.3 datetime 模块:处理「人类可读」的时间

datetime 主要用于日志记录、时间戳格式化等场景。在动画系统里,你可能需要记录动画开始时间、计算动画持续时间等。

from datetime import datetime, timedelta

# 获取当前时间
now = datetime.now()
print(f"当前时间:{now.strftime('%Y-%m-%d %H:%M:%S')}")

# 计算动画结束时间
animation_duration = timedelta(seconds=5)
end_time = now + animation_duration
print(f"动画结束时间:{end_time.strftime('%H:%M:%S')}")

# 计算时间差
start = datetime.now()
# ... 执行动画 ...
end = datetime.now()
elapsed = end - start
print(f"动画耗时:{elapsed.total_seconds():.2f} 秒")

3.3 数学运算:动画的「物理引擎」

动画离不开数学。位置计算、速度变化、碰撞检测……全都依赖数学运算。Python 的 math 模块提供了我们需要的所有数学函数。

3.3.1 常用数学函数

函数 说明 动画应用场景
math.sin() 正弦函数 制作波浪运动、弹性效果
math.cos() 余弦函数 圆周运动、摆动动画
math.sqrt() 平方根 计算距离、碰撞检测
math.atan2() 反正切(带象限) 计算两点之间的角度
math.radians() 角度转弧度 将角度转换为弧度制
math.degrees() 弧度转角度 将弧度转换为角度制

3.3.2 实战:让小球做圆周运动

咱们来写一个实际例子。让一个小球围绕中心点做圆周运动。

import math

class OrbitingBall:
    """做圆周运动的小球"""
    def __init__(self, center_x, center_y, radius, speed):
        self.center_x = center_x
        self.center_y = center_y
        self.orbit_radius = radius  # 轨道半径
        self.speed = speed          # 角速度(弧度/秒)
        self.angle = 0              # 当前角度

    def update(self, dt):
        """更新位置,dt 是时间增量(秒)"""
        self.angle += self.speed * dt

        # 计算当前位置
        self.x = self.center_x + self.orbit_radius * math.cos(self.angle)
        self.y = self.center_y + self.orbit_radius * math.sin(self.angle)

    def get_position(self):
        return (self.x, self.y)

# 使用示例
ball = OrbitingBall(400, 300, 150, 1.5)  # 半径150,角速度1.5弧度/秒
ball.update(0.016)  # 更新一帧(约60 FPS)
print(f"小球位置:{ball.get_position()}")

这里用到了 math.cos 和 math.sin。你想想看,角度从 0 到 2π 变化时,cos 和 sin 的值正好构成一个圆。这就是圆周运动的数学本质。

3.3.3 向量运算:自己动手实现

Python 标准库没有内置的向量类,但我们可以自己写一个。在动画系统里,向量运算太常用了。

import math

class Vector2:
    """二维向量"""
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y

    def __add__(self, other):
        return Vector2(self.x + other.x, self.y + other.y)

    def __sub__(self, other):
        return Vector2(self.x - other.x, self.y - other.y)

    def __mul__(self, scalar):
        return Vector2(self.x * scalar, self.y * scalar)

    def length(self):
        """向量长度"""
        return math.sqrt(self.x ** 2 + self.y ** 2)

    def normalize(self):
        """归一化"""
        length = self.length()
        if length == 0:
            return Vector2(0, 0)
        return Vector2(self.x / length, self.y / length)

    def dot(self, other):
        """点积"""
        return self.x * other.x + self.y * other.y

    def distance_to(self, other):
        """计算到另一个向量的距离"""
        return (self - other).length()

    def __str__(self):
        return f"({self.x:.2f}, {self.y:.2f})"

为什么向量重要? 在动画系统里,位置、速度、加速度都是向量。有了向量类,你可以用 position += velocity * dt 这样的代码来更新位置,非常直观。我所有的动画项目都离不开这个基础类。

3.4 本章知识体系

下面这张图帮你梳理了本章的核心知识点和它们之间的关系。

第三章知识体系图 第三章:Python基础回顾 - 知识体系 面向对象编程 时间模块 数学运算 类与对象 继承与多态 封装与抽象 time.time() / perf_counter() 帧率控制(sleep) datetime 格式化 三角函数(sin/cos) 距离计算(sqrt) 向量运算 动画刷新系统 对象管理 + 时间控制 + 物理计算 三大模块缺一不可:面向对象管理「谁在动」,时间模块控制「什么时候动」,数学运算决定「怎么动」

3.5 本章小结

这一章的内容,说白了就是给后面的动画系统打地基。咱们过了三个东西:

  • 面向对象编程:用类来封装动画元素,用继承来复用代码,用多态来统一接口。这是你管理几十上百个动画对象的基础。
  • 时间模块:time.time() 和 time.perf_counter() 用来计时,time.sleep() 用来控制帧率。记住,datetime 更适合做日志,不适合做高精度计时。
  • 数学运算:三角函数做波形和圆周运动,sqrt 算距离,向量运算做位置和速度管理。自己实现一个 Vector2 类,后面你会感谢我的。

我的建议:别急着往后看。花 20 分钟把上面的代码都敲一遍,尤其是那个 Vector2 类和 FrameRateController。代码这东西,看十遍不如敲一遍。我在带新人时发现,凡是动手敲过代码的,后面学动画逻辑都特别快。