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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
use core::time;
use sysinfo::{ProcessesToUpdate, System};
use tauri::State;
use tokio::time::interval;
use tracing::{debug, info, warn};
use crate::state::AppState;

pub struct Watcher<'a, T>
where
    T: Fn() + Send + 'static,
{
    pub process_name: &'a str,
    pub on_start: T,
    pub on_stop: T,
    pub is_running: bool,
}

impl<'a, T> Watcher<'a, T>
where
    T: Fn() + Send + 'static,
{
    async fn watch_process(&mut self) {
        info!("Starting to watch process: {}", self.process_name);
        let mut interval = interval(time::Duration::from_secs(2));
        let mut system = System::new_all();

        loop {
            interval.tick().await;
            system.refresh_processes(ProcessesToUpdate::All, true);

            let process_is_running = system
                .processes()
                .iter()
                .any(|(_, process)| process.name().eq_ignore_ascii_case(self.process_name));

            if self.is_running != process_is_running {
                self.is_running = process_is_running;

                if self.is_running {
                    (self.on_start)();
                } else {
                    (self.on_stop)();
                }
            }
        }
    }

    pub async fn start(&mut self, state: State<'_, AppState>) {
        let task_guard = state.watch_task.lock().await;

        if task_guard.is_some() {
            warn!("{} already in watching, skipping", self.process_name);
            return;
        }

        let task = tokio::spawn(self.watch_process()); // 这句出现生命周期问题
    }

    pub fn stop() {
// TODO
    }

}

报错具体语句

1
2
3
4
5
let task = tokio::spawn(self.watch_process());
// borrowed data escapes outside of method
// `self` escapes the method body here

// argument requires that `'a` must outlive `'static`

原因分析:

  1. self 的生命周期 ('a) vs. tokio::spawn 的生命周期 ('static):
    • Watcher 结构体定义了一个生命周期参数 'a,并且 process_name 字段的类型是 &'a str。这意味着 process_name 是一个借用的字符串切片,它的生命周期与 Watcher 实例的生命周期 'a 绑定。
    • tokio::spawn(self.watch_process()) 会创建一个新的 Tokio 任务。Tokio 任务通常需要捕获它们所使用的变量,并且这些变量必须具有 'static 生命周期。这是因为 Tokio 任务可以在任何时候被调度,并且它们的生命周期可能比创建它们的函数或方法的生命周期要长。
    • 当尝试将 self.watch_process()(一个方法,它隐式地借用了 self)传递给 tokio::spawn 时,Rust 编译器会发现 self 的生命周期 'a 可能比 tokio::spawn 所期望的 'static 生命周期要短。如果 'a 短于 'static,那么 self(以及它所引用的 process_name)就可能在任务还在运行时失效,导致悬空指针,这是 Rust 极力避免的。
  2. self 逃逸:
    • tokio::spawn 期望捕获的数据是 'static 的。当 self.watch_process() 被调用时,它需要访问 self(即 Watcher 实例)。因为 self 拥有一个非 'static 的生命周期 'a,并且 tokio::spawn 需要 'static,所以 self 的借用“逃逸”了 start 方法的局部作用域。

解决方案

方法一:让 Watcher 结构体本身拥有 'static 生命周期(如果可能)

如果 Watcher 结构体本身不需要借用任何外部数据(除了 'static 的数据),那么你可以尝试让它拥有 'static 生命周期。然而,由于 process_name: &'a str,这直接阻止了 Watcher 拥有 'static 生命周期。

方法二:将 Watcher 的所有权移交给任务(更常见)

最直接的解决方案是让 Watcher 实例的所有权转移到 tokio::spawn 创建的任务中。这意味着 Watcher 实例本身需要被移动到任务中,而不是被借用。

要做到这一点,你需要:

  1. 移除 Watcher 结构体中的生命周期参数 'a
  2. 将 process_name 的类型从 &'a str 改为 String 这样 process_name 就拥有了 'static 生命周期(或者至少是它自己的所有权,可以被移动)。
  3. 将 watch_process 方法标记为 async,并且让它接收 self 的所有权(self: Watcher<'static, T> 或直接 self)。
  4. 在 start 方法中,将 Watcher 的所有权移动到 tokio::spawn

设计思路

  • 启动任务: 当用户请求启动任务时,你需要:
    1. 获取 AppState 的锁。
    2. 检查 watch_task 是否已经是 Some
    3. 如果是 Some,可能需要先取消旧的任务(old_handle.abort()),然后等待它真正停止(old_handle.await,或者在 Tauri 中可能通过其他机制处理)。
    4. 使用 tokio::spawn() 启动新任务,得到一个 JoinHandle
    5. 将这个 JoinHandle 存入 watch_task 中(Some(new_handle))。
    6. 释放锁。
  • 停止任务: 当用户请求停止任务时,你需要:
    1. 获取 AppState 的锁。
    2. 检查 watch_task 是否是 Some
    3. 如果是 Some,获取 JoinHandle,调用 handle.abort() 来尝试取消它。
    4. 将 watch_task 设置回 None
    5. 释放锁。
  • 检查任务状态: 你可能还需要一个命令来检查是否有任务在运行。
    1. 获取 AppState 的锁。
    2. 检查 watch_task 是否是 Some
    3. 返回 true 或 false
    4. 释放锁。

代码实现

AppState结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use std::{sync::Arc, thread::JoinHandle};
use tokio::sync::Mutex;

pub struct AppState {
    pub watch_task: Arc<Mutex<Option<JoinHandle<()>>>>,
}

impl AppState {
    pub fn new() -> Self {
        AppState {
            watch_task: Arc::new(Mutex::new(None)),
        }
    }
}

impl Default for AppState {
    fn default() -> Self {
        Self::new()
    }
}

Tauri部分

引用AppState

1
2
    tauri::Builder::default()
.manage(AppState::new())

tauri::Builder::manage() 方法的作用:

  • manage() 方法是 Tauri 提供的用于管理应用程序全局状态的机制。
  • 当调用 builder.manage(some_value) 时,Tauri 会将 some_value 的一个克隆(如果 some_value 是 Clone 的)或者一个共享引用(如果 some_value 是 Arc 的) 存储在应用程序的内部状态管理器中。
  • 在例子中,AppState::new() 创建了一个 AppState 实例。由于 AppState 内部使用了 Arc,Tauri 实际上会将这个 Arc<Mutex<Option<JoinHandle<()>>>> 的共享引用存储起来。
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#[tauri::command]
pub async fn start_watching(state: State<'_, AppState>) -> Result<(), String> {
    // 检查是否已经在监听
    let mut task_guard = state.watch_task.lock().await;
    if task_guard.is_some() {
        return Err("Already watching".to_string());
    }

    let on_start = move || {
// 开启时运行的函数
    };

    let on_stop = move || {
// 关闭时运行的函数
    };

    let task = tokio::spawn(async move {
        watch_task(on_start, on_stop).await;
    });

    *task_guard = Some(task);
    println!("监听已启动");
    Ok(())
}

#[tauri::command]
pub async fn stop_watching(state: State<'_, AppState>) -> Result<(), String> {
    let mut task_guard = state.watch_task.lock().await;
    if let Some(task) = task_guard.take() {
        task.abort();
        println!("监听已停止");
        Ok(())
    } else {
        Err("Not watching".to_string())
    }
}


pub async fn watch_valorant<S, F>(on_start: S, on_stop: F)
where
    S: Fn() + Send + 'static,
    F: Fn() + Send + 'static,
{
    let mut sys = System::new_all();
    let mut interval = interval(time::Duration::from_secs(3));
    let mut valorant_running = false;

    loop {
        interval.tick().await;
        sys.refresh_all();

        let is_running = // 具体实现
        if is_running {
            on_start();
        } else {
            on_stop();
        }
    }
}

** tauri::State<'_, AppState>:**

  • tauri::State: 这是 Tauri 提供的一个用于在命令、事件等处理函数中访问已注册状态的类型。
  • <'_, AppState>: 这是一个生命周期和类型参数。
    • '_: 表示生命周期是匿名的,Tauri 会自动管理它的生命周期,确保它不会比应用程序本身活得更久。
    • AppState: 指定了我们要访问的状态类型是 AppState
  • 当你将 app_state: State<'_, AppState> 作为命令参数时,Tauri 会自动查找通过 builder.manage(AppState::new()) 注册的 AppState 实例,并安全地将其传递给你的函数。

** state.watch_task.lock().await:**

  • 返回的是一个 tokio::sync::MutexGuard<'_, Option<JoinHandle<()>>>
  • MutexGuard 是一个智能指针。它实现了 Deref 和 DerefMut trait,允许你像访问 Option<JoinHandle<()>> 本身一样访问它。

interval.tick().await;:

  • interval.tick(): 这个方法返回一个 Future(一个未来的值)。这个 Future 会在下一次间隔时间到达时完成。
  • .await: 这是异步 Rust 的核心关键字。当程序执行到 .await 时,如果 Future 还没有准备好(即 3 秒还没到),当前的任务(watch_valorant 函数的执行)就会被暂停,Tokio 运行时会去调度执行其他可以运行的任务。一旦 3 秒到了,interval.tick() 的 Future 就完成了,暂停的任务会被唤醒,继续执行 interval.tick().await; 后面的代码。

Error

总是记录错误

Warn

非关键但需要注意

Info

用户/运维关心的状态

Debug

开发者关心的内部状态

Trace

极端详细的内部流程

总结

级别 何时使用 示例 目标用户
**error!**​ 不可恢复的错误 数据库连接失败 运维/开发
**warn!**​ 可恢复的问题 缓存未命中 运维/开发
**info!**​ 业务状态变化 用户注册成功 用户/运维
**debug!**​ 内部状态/性能 SQL执行时间 开发者
**trace!**​ 详细执行流程 每个循环迭代 深度调试

关键原则

  1. **error!**​ 和 **warn!**​ 要少而精

  2. **info!**​ 要可读性强,适合非技术人员

  3. **debug!**​ 是默认的调试级别

  4. **trace!**​ 只在需要时开启,通常对性能有影响

直接调用Windows API是不安全的行为,所以相关操作都必须在unsafe块进行

获取当前计算机分辨率信息

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
pub fn get_current_display_mode() -> Result<DisplayMode, DisplayError> {
    unsafe {
        let mut devmode: DEVMODEW = mem::zeroed();
        devmode.dmSize = mem::size_of::<DEVMODEW>() as u16;
        debug!(
            "DEVMODEW size: {} bytes, as u16: {}",
            mem::size_of::<DEVMODEW>(),
            mem::size_of::<DEVMODEW>() as u16
        );
       
        // 枚举指定显示设备的显示模式
        let result = EnumDisplaySettingsW(
            PCWSTR::null(), // 默认的显示设备
            ENUM_CURRENT_SETTINGS, // 当前正在使用的显示设置
            &mut devmode, // 函数会将获取到的当前显示设置填充到这个结构体中
        );
        debug!("EnumDisplaySettingsW result: {:?}", result);

// 检查 API 调用的成功与否
        if result.as_bool() {
            Ok(DisplayMode {
                width: devmode.dmPelsWidth,
                height: devmode.dmPelsHeight,
                refresh_rate: devmode.dmDisplayFrequency,
                bits_per_pixel: devmode.dmBitsPerPel,
            })
        } else {
            Err(DisplayError::EnumFailed)
        }
    }
}

Windows API 期望结构体以特定方式初始化,以免结构体中包含垃圾值

let mut devmode: DEVMODEW = mem::zeroed();

为什么?因为程序运行的时候,被分配的一块内存也有可能是上一个程序用过的,你只定义了结构体的一部分字段,但是其他字段可能还有内存本来存在的值。

所以要调用zeroed()方法用 0 字节填充所有字段,避免了使用未初始化内存的风险。

devmode.dmSize = mem::size_of::<DEVMODEW>() as u16;

  • 为什么:
    • API 约定: Windows API 函数(如 EnumDisplaySettingsW)在接收 DEVMODEW 结构体时,需要知道这个结构体的大小,以便正确地读取和写入数据。dmSize 字段就是用来告知 API 结构体实际大小的。
    • 版本兼容性: DEVMODEW 结构体可能会在不同的 Windows 版本中有所变化(增加字段)。通过设置 dmSize,API 可以知道它正在处理的是哪个版本的结构体,从而避免读取超出实际大小的内存。
    • Rust 类型转换: mem::size_of::<DEVMODEW>() 返回的是 usize 类型,而 dmSize 需要的是 u16 类型,所以需要进行 as u16 的类型转换。

更改分辨率

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
pub fn change_display_mode(mode: &DisplayMode, permanent: bool) -> Result<(), DisplayError> {
    unsafe {
        let mut devmode: DEVMODEW = mem::zeroed();
        devmode.dmSize = mem::size_of::<DEVMODEW>() as u16;
        devmode.dmPelsWidth = mode.width;
        devmode.dmPelsHeight = mode.height
        devmode.dmDisplayFrequency = mode.refresh_rate;
        devmode.dmBitsPerPel = mode.bits_per_pixel;
        devmode.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_BITSPERPEL; // 指示有效字段
        debug!(
            "DEVMODEW size: {} bytes, as u16: {}",
            mem::size_of::<DEVMODEW>(),
            mem::size_of::<DEVMODEW>() as u16
        );

        let test_result = ChangeDisplaySettingsW(Some(&devmode), CDS_TEST);
        debug!("ChangeDisplaySettingsW (test) result: {:?}", test_result);

        if test_result != DISP_CHANGE_SUCCESSFUL {
            return Err(DisplayError::ChangeFailed(format!(
                "Test Failed: {:?}",
                test_result
            )));
        }

        // 除了CDS_UPDATEREGISTRY,其他标志都是临时的,系统重启后会恢复默认设置
        let flags = if permanent {
            CDS_UPDATEREGISTRY // 更新到注册表
        } else {
            CDS_TYPE(0) // 不是任何标志(空)
        };
        debug!("Changing display settings with flags: {:?}", flags);

        let result = ChangeDisplaySettingsW(Some(&devmode), flags);
        debug!("ChangeDisplaySettingsW result: {:?}", result);

        match result {
            DISP_CHANGE_SUCCESSFUL => Ok(()),
            DISP_CHANGE_RESTART => Err(DisplayError::ChangeFailed(
                "System restart required".to_string(),
            )),
            DISP_CHANGE_BADMODE => Err(DisplayError::ChangeFailed(
                "The graphics mode is not supported".to_string(),
            )),
            _ => Err(DisplayError::ChangeFailed(format!(
                "Unknown error: {:?}",
                result
            ))),
        }
    }
}

指定显示器版本

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
pub fn change_display_mode_for_monitor(
    device_name: String,
    mode: &DisplayMode,
    permanent: bool,
) -> Result<(), DisplayError> {
    let device_name_wide: Vec<u16> = OsStr::new(&device_name)
        .encode_wide()
        .chain(std::iter::once(0))
        .collect();
    debug!("Device name wide: {:?}", device_name_wide);

    unsafe {
        let mut devmode: DEVMODEW = mem::zeroed();
        devmode.dmSize = mem::size_of::<DEVMODEW>() as u16;
        devmode.dmPelsWidth = mode.width;
        devmode.dmPelsHeight = mode.height;
        devmode.dmDisplayFrequency = mode.refresh_rate;
        devmode.dmBitsPerPel = mode.bits_per_pixel;
        devmode.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_BITSPERPEL;
        debug!(
            "DEVMODEW size: {} bytes, as u16: {}",
            mem::size_of::<DEVMODEW>(),
            mem::size_of::<DEVMODEW>() as u16
        );

        let test_result = ChangeDisplaySettingsExW(
            PCWSTR::from_raw(device_name_wide.as_ptr()),
            Some(&devmode),
            None,
            CDS_TEST,
            None,

        );

        debug!("ChangeDisplaySettingsExW (test) result: {:?}", test_result);
        if test_result != DISP_CHANGE_SUCCESSFUL {
            return Err(DisplayError::ChangeFailed(format!(
                "Test Failed: {:?}",
                test_result
            )));
        }


        let flags = if permanent {
            CDS_UPDATEREGISTRY
        } else {
            CDS_TYPE(0)
        };
        debug!("Changing display settings with flags: {:?}", flags);

        let result = ChangeDisplaySettingsExW(
            PCWSTR::from_raw(device_name_wide.as_ptr()),
            Some(&devmode),
            None,
            flags,
            None,
        );
        debug!("ChangeDisplaySettingsExW result: {:?}", result);

        match result {
            DISP_CHANGE_SUCCESSFUL => Ok(()),
            DISP_CHANGE_RESTART => Err(DisplayError::ChangeFailed(
                "System restart required".to_string(),
            )),
            DISP_CHANGE_BADMODE => Err(DisplayError::ChangeFailed(
                "The graphics mode is not supported".to_string(),
            )),
            _ => Err(DisplayError::ChangeFailed(format!(
                "Unknown error: {:?}",
                result
            ))),
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
pub fn restore_default_settings() -> Result<(), DisplayError> {
    unsafe {
        let result = ChangeDisplaySettingsW(None, CDS_TYPE(0));
        debug!("ChangeDisplaySettingsW result: {:?}", result);

        if result == DISP_CHANGE_SUCCESSFUL {
            Ok(())
        } else {
            Err(DisplayError::ChangeFailed(format!("恢复失败: {:?}", result)))
        }
    }
}

列举显示器

用于给change_display_mode_for_monitor()提供精确的显示器名字

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
pub fn enumerate_monitors() -> HashMap<String, String> {
    let mut monitors = HashMap::new();

    unsafe {
        let mut device: DISPLAY_DEVICEW = mem::zeroed();
        device.cb = mem::size_of::<DISPLAY_DEVICEW>() as u32;
        debug!(
            "DISPLAY_DEVICEW size: {} bytes, as u16: {}",
            mem::size_of::<DISPLAY_DEVICEW>(),
            mem::size_of::<DISPLAY_DEVICEW>() as u16
        );

        let mut device_index = 0u32;
        loop {
            debug!("Enumerating device index: {}", device_index);
            let result = EnumDisplayDevicesW(PCWSTR::null(), device_index, &mut device, 0);
            debug!("EnumDisplayDevicesW result: {:?}", result);

            if !result.as_bool() {
                debug!("No more devices found, breaking loop.");
                break;
            }

            // 检查是否是活动的显示器
            debug!("Device StateFlags: {:?}", device.StateFlags);
            if (device.StateFlags & DISPLAY_DEVICE_ACTIVE) != DISPLAY_DEVICE_STATE_FLAGS(0) {
                let monitor_name = String::from_utf16_lossy(
                    &device.DeviceName[..device
                        .DeviceName
                        .iter()
                        .position(|&c| c == 0)
                        .unwrap_or(device.DeviceName.len())],
                );
                let gpu_name = String::from_utf16_lossy(
                    &device.DeviceString[..device
                        .DeviceString
                        .iter()
                        .position(|&c| c == 0)
                        .unwrap_or(device.DeviceString.len())],
                );
                monitors.insert(monitor_name, gpu_name);
                debug!(
                    "Found active monitor: {}, using gpu name: {}",
                    String::from_utf16_lossy(&device.DeviceName),
                    String::from_utf16_lossy(&device.DeviceString)
                );
            }
            device_index += 1;
        }
        monitors
    }
}
  • EnumDisplayDevicesW: 这是核心的 Windows API 函数。
    • 第一个参数 PCWSTR::null(): 通常用于指定要枚举的设备路径。在这里传递 null 表示枚举所有与系统相关的显示设备。
    • 第二个参数 device_index: 当前要枚举的设备的索引。
    • 第三个参数 &mut device: 一个指向 DISPLAY_DEVICEW 结构体的可变引用,API 将把找到的设备信息填充到这个结构体中。
    • 第四个参数 0: 标志位,通常用于控制枚举行为。0 表示默认行为。

装饰器

函数对象有一个__name__属性(注意:是前后各两个下划线),可以拿到函数的名字:

1
2
3
4
>>> now.__name__
'now'
>>> f.__name__
'now'

现在,假设我们要增强now()函数的功能,比如,在函数调用前后自动打印日志,但又不希望修改now()函数的定义,这种在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)。

本质上,decorator就是一个返回函数的高阶函数。所以,我们要定义一个能打印日志的decorator,可以定义如下:

1
2
3
4
5
def log(func):
def wrapper(*args, **kw):
print('call %s():' % func.__name__)
return func(*args, **kw)
return wrapper

观察上面的log,因为它是一个decorator,所以接受一个函数作为参数,并返回一个函数。我们要借助Python的@语法,把decorator置于函数的定义处:

1
2
3
@log
def now():
print('2024-6-1')

调用now()函数,不仅会运行now()函数本身,还会在运行now()函数前打印一行日志:

1
2
3
>>> now()
call now():
2024-6-1

@log放到now()函数的定义处,相当于执行了语句:

1
now = log(now)

由于log()是一个decorator,返回一个函数,所以,原来的now()函数仍然存在,只是现在同名的now变量指向了新的函数,于是调用now()将执行新函数,即在log()函数中返回的wrapper()函数。

wrapper()函数的参数定义是(*args, **kw),因此,wrapper()函数可以接受任意参数的调用。在wrapper()函数内,首先打印日志,再紧接着调用原始函数。

如果decorator本身需要传入参数,那就需要编写一个返回decorator的高阶函数,写出来会更复杂。比如,要自定义log的文本:

1
2
3
4
5
6
7
def log(text):
def decorator(func):
def wrapper(*args, **kw):
print('%s %s():' % (text, func.__name__))
return func(*args, **kw)
return wrapper
return decorator

这个3层嵌套的decorator用法如下:

1
2
3
@log('execute')
def now():
print('2024-6-1')

执行结果如下:

1
2
3
>>> now()
execute now():
2024-6-1

和两层嵌套的decorator相比,3层嵌套的效果是这样的:

1
>>> now = log('execute')(now)

我们来剖析上面的语句,首先执行log('execute'),返回的是decorator函数,再调用返回的函数,参数是now函数,返回值最终是wrapper函数。

以上两种decorator的定义都没有问题,但还差最后一步。因为我们讲了函数也是对象,它有__name__等属性,但你去看经过decorator装饰之后的函数,它们的__name__已经从原来的'now'变成了'wrapper'

1
2
>>> now.__name__
'wrapper'

因为返回的那个wrapper()函数名字就是'wrapper',所以,需要把原始函数的__name__等属性复制到wrapper()函数中,否则,有些依赖函数签名的代码执行就会出错。

不需要编写wrapper.__name__ = func.__name__这样的代码,Python内置的functools.wraps就是干这个事的,所以,一个完整的decorator的写法如下:

1
2
3
4
5
6
7
8
import functools

def log(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print('call %s():' % func.__name__)
return func(*args, **kw)
return wrapper

或者针对带参数的decorator:

1
2
3
4
5
6
7
8
9
10
import functools

def log(text):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print('%s %s():' % (text, func.__name__))
return func(*args, **kw)
return wrapper
return decorator

import functools是导入functools模块。模块的概念稍候讲解。现在,只需记住在定义wrapper()的前面加上@functools.wraps(func)即可。

偏函数

Python的functools模块提供了很多有用的功能,其中一个就是偏函数(Partial function)。要注意,这里的偏函数和数学意义上的偏函数不一样。

在介绍函数参数的时候,我们讲到,通过设定参数的默认值,可以降低函数调用的难度。而偏函数也可以做到这一点。举例如下:

int()函数可以把字符串转换为整数,当仅传入字符串时,int()函数默认按十进制转换:

1
2
>>> int('12345')
12345

int()函数还提供额外的base参数,默认值为10。如果传入base参数,就可以做N进制的转换:

1
2
3
4
>>> int('12345', base=8)
5349
>>> int('12345', 16)
74565

假设要转换大量的二进制字符串,每次都传入int(x, base=2)非常麻烦,于是,我们想到,可以定义一个int2()的函数,默认把base=2传进去:

1
2
def int2(x, base=2):
return int(x, base)

这样,我们转换二进制就非常方便了:

1
2
3
4
>>> int2('1000000')
64
>>> int2('1010101')
85

functools.partial就是帮助我们创建一个偏函数的,不需要我们自己定义int2(),可以直接使用下面的代码创建一个新的函数int2

1
2
3
4
5
6
>>> import functools
>>> int2 = functools.partial(int, base=2)
>>> int2('1000000')
64
>>> int2('1010101')
85

所以,简单总结functools.partial的作用就是,把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单。

注意到上面的新的int2函数,仅仅是把base参数重新设定默认值为2,但也可以在函数调用时传入其他值:

1
2
>>> int2('1000000', base=10)
1000000

最后,创建偏函数时,实际上可以接收函数对象、*args**kw这3个参数,当传入:

1
int2 = functools.partial(int, base=2)

实际上固定了int()函数的关键字参数base,也就是:

1
int2('10010')

相当于:

1
2
kw = { 'base': 2 }
int('10010', **kw)

当传入:

1
max2 = functools.partial(max, 10)

实际上会把10作为*args的一部分自动加到左边,也就是:

1
max2(5, 6, 7)

相当于:

1
2
args = (10, 5, 6, 7)
max(*args)

结果为10

多行打印

单引号或者双引号都可以(默认支持转义),r(raw string)也可写在行头

1
2
3
4
5
6
print('''
1
2
3
4/n
''')

整数除法

当是/的时候输出的是浮点数
当是//的时候只会输出小数点前的数,也就是整数部分
当是%的时候,取余

1
2
3
4
5
6
7
8
9
10
11
>>> 10 / 3
3.3333333333333335

>>> 9 / 3
3.0

>>> 10 // 3
3

>>> 10 % 3
1

字符串

对于单个字符的编码,Python提供了ord()函数获取字符的整数表示,chr()函数把编码转换为对应的字符:

1
2
3
4
5
6
7
8
9
10
11
>>> ord('A')
65

>>> ord('中')
20013

>>> chr(66)
'B'

>>> chr(25991)
'文'

如果知道字符的整数编码,还可以用十六进制这么写str:

1
2
>>> '\u4e2d\u6587'
'中文'

要注意区分'ABC'b'ABC',前者是str,后者虽然内容显示得和前者一样,但bytes的每个字符都只占用一个字节。

以Unicode表示的str通过encode()方法可以编码为指定的bytes,例如:

1
2
3
4
5
6
7
8
>>> 'ABC'.encode('ascii')
b'ABC'
>>> '中文'.encode('utf-8')
b'\xe4\xb8\xad\xe6\x96\x87'
>>> '中文'.encode('ascii')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)

纯英文的str可以用ASCII编码为bytes,内容是一样的,含有中文的str可以用UTF-8编码为bytes。含有中文的str无法用ASCII编码,因为中文编码的范围超过了ASCII编码的范围,Python会报错。

模式匹配

简单匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
age = 15

match age:
case x if x < 10:
print(f'< 10 years old: {x}')
case 10:
print('10 years old.')
case 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18:
print('11~18 years old.')
case 19:
print('19 years old.')
case _:
print('not sure.')

列表匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
args = ['gcc', 'hello.c', 'world.c']
# args = ['clean']
# args = ['gcc']

match args:
# 如果仅出现gcc,报错:
case ['gcc']:
print('gcc: missing source file(s).')
# 出现gcc,且至少指定了一个文件:
case ['gcc', file1, *files]:
print('gcc compile: ' + file1 + ', ' + ', '.join(files))
# 仅出现clean:
case ['clean']:
print('clean')
case _:
print('invalid command.')

Dict

要保证hash的正确性,作为key的对象就不能变。在Python中,字符串、整数等都是不可变的,因此,可以放心地作为key。而list是可变的,就不能作为key:

1
2
3
4
5
>>> key = [1, 2, 3]
>>> d[key] = 'a list'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'

Set

set和dict类似,也是一组key的集合,但不存储value。由于key不能重复,所以,在set中,没有重复的key。

要创建一个set,用{x,y,z,...}列出每个元素:

1
2
3
>>> s = {1, 2, 3}
>>> s
{1, 2, 3}

或者提供一个list作为输入集合:

1
2
3
>>> s = set([1, 2, 3])
>>> s
{1, 2, 3}

注意,传入的参数[1, 2, 3]是一个list,而显示的{1, 2, 3}只是告诉你这个set内部有1,2,3这3个元素,显示的顺序也不表示set是有序的。。

重复元素在set中自动被过滤:

1
2
3
>>> s = {1, 1, 2, 2, 3, 3}
>>> s
{1, 2, 3}

通过add(key)方法可以添加元素到set中,可以重复添加,但不会有效果:

1
2
3
4
5
6
>>> s.add(4)
>>> s
{1, 2, 3, 4}
>>> s.add(4)
>>> s
{1, 2, 3, 4}

通过remove(key)方法可以删除元素:

1
2
3
>>> s.remove(4)
>>> s
{1, 2, 3}

set可以看成数学意义上的无序和无重复元素的集合,因此,两个set可以做数学意义上的交集、并集等操作:

1
2
3
4
5
6
>>> s1 = {1, 2, 3}
>>> s2 = {2, 3, 4}
>>> s1 & s2
{2, 3}
>>> s1 | s2
{1, 2, 3, 4}

set和dict的唯一区别仅在于没有存储对应的value,但是,set的原理和dict一样,所以,同样不可以放入可变对象,因为无法判断两个可变对象是否相等,也就无法保证set内部“不会有重复元素”。试试把list放入set,看看是否会报错。

函数的参数

对参数类型做检查,只允许整数和浮点数类型的参数。数据类型检查可以用内置函数isinstance()实现:

1
2
3
4
5
6
7
def my_abs(x):
if not isinstance(x, (int, float)):
raise TypeError('bad operand type')
if x >= 0:
return x
else:
return -x

默认参数

默认参数很有用,但使用不当,也会掉坑里。默认参数有个最大的坑,演示如下:

先定义一个函数,传入一个list,添加一个END再返回:

1
2
3
def add_end(L=[]):
L.append('END')
return L

当你正常调用时,结果似乎不错:

1
2
3
4
>>> add_end([1, 2, 3])
[1, 2, 3, 'END']
>>> add_end(['x', 'y', 'z'])
['x', 'y', 'z', 'END']

当你使用默认参数调用时,一开始结果也是对的:

1
2
>>> add_end()
['END']

但是,再次调用add_end()时,结果就不对了:

1
2
3
4
>>> add_end()
['END', 'END']
>>> add_end()
['END', 'END', 'END']

很多初学者很疑惑,默认参数是[],但是函数似乎每次都“记住了”上次添加了'END'后的list。

原因解释如下:

Python函数在定义的时候,默认参数L的值就被计算出来了,即[],因为默认参数L也是一个变量,它指向对象[],每次调用该函数,如果改变了L的内容,则下次调用时,默认参数的内容就变了,不再是函数定义时的[]了。

特别注意

定义默认参数要牢记一点:默认参数必须指向不变对象!

要修改上面的例子,我们可以用None这个不变对象来实现:

1
2
3
4
5
def add_end(L=None):
if L is None:
L = []
L.append('END')
return L

现在,无论调用多少次,都不会有问题:

1
2
3
4
>>> add_end()
['END']
>>> add_end()
['END']

为什么要设计strNone这样的不变对象呢?因为不变对象一旦创建,对象内部的数据就不能修改,这样就减少了由于修改数据导致的错误。此外,由于对象不变,多任务环境下同时读取对象不需要加锁,同时读一点问题都没有。我们在编写程序时,如果可以设计一个不变对象,那就尽量设计成不变对象。

可变参数

1
2
3
4
>>> calc(1, 2, 3)
14
>>> calc(1, 3, 5, 7)
84

我们把函数的参数改为可变参数:

1
2
3
4
5
def calc(*numbers):
sum = 0
for n in numbers:
sum = sum + n * n
return sum

定义可变参数和定义一个list或tuple参数相比,仅仅在参数前面加了一个*号。在函数内部,参数numbers接收到的是一个tuple,因此,函数代码完全不变。但是,调用该函数时,可以传入任意个参数,包括0个参数:

1
2
3
4
>>> calc(1, 2)
5
>>> calc()
0

如果已经有一个list或者tuple,要调用一个可变参数怎么办?可以这样做:

1
2
3
>>> nums = [1, 2, 3]
>>> calc(nums[0], nums[1], nums[2])
14

这种写法当然是可行的,问题是太繁琐,所以Python允许你在list或tuple前面加一个*号,把list或tuple的元素变成可变参数传进去:

1
2
3
>>> nums = [1, 2, 3]
>>> calc(*nums)
14

*nums表示把nums这个list的所有元素作为可变参数传进去。这种写法相当有用,而且很常见。

关键词参数

可变参数允许你传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple。而关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict。请看示例:

1
2
def person(name, age, **kw):
print('name:', name, 'age:', age, 'other:', kw)

函数person除了必选参数nameage外,还接受关键字参数kw。在调用该函数时,可以只传入必选参数:

1
2
>>> person('Michael', 30)
name: Michael age: 30 other: {}

也可以传入任意个数的关键字参数:

1
2
3
4
>>> person('Bob', 35, city='Beijing')
name: Bob age: 35 other: {'city': 'Beijing'}
>>> person('Adam', 45, gender='M', job='Engineer')
name: Adam age: 45 other: {'gender': 'M', 'job': 'Engineer'}

关键字参数有什么用?它可以扩展函数的功能。比如,在person函数里,我们保证能接收到nameage这两个参数,但是,如果调用者愿意提供更多的参数,我们也能收到。试想你正在做一个用户注册的功能,除了用户名和年龄是必填项外,其他都是可选项,利用关键字参数来定义这个函数就能满足注册的需求。

和可变参数类似,也可以先组装出一个dict,然后,把该dict转换为关键字参数传进去:

1
2
3
>>> extra = {'city': 'Beijing', 'job': 'Engineer'}
>>> person('Jack', 24, city=extra['city'], job=extra['job'])
name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}

当然,上面复杂的调用可以用简化的写法:

1
2
3
>>> extra = {'city': 'Beijing', 'job': 'Engineer'}
>>> person('Jack', 24, **extra)
name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}

**extra表示把extra这个dict的所有key-value用关键字参数传入到函数的**kw参数,kw将获得一个dict,注意kw获得的dict是extra的一份拷贝,对kw的改动不会影响到函数外的extra

命名关键字参数

对于关键字参数,函数的调用者可以传入任意不受限制的关键字参数。至于到底传入了哪些,就需要在函数内部通过kw检查。

仍以person()函数为例,我们希望检查是否有cityjob参数:

1
2
3
4
5
6
7
8
def person(name, age, **kw):
if 'city' in kw:
# 有city参数
pass
if 'job' in kw:
# 有job参数
pass
print('name:', name, 'age:', age, 'other:', kw)

但是调用者仍可以传入不受限制的关键字参数:

1
>>> person('Jack', 24, city='Beijing', addr='Chaoyang', zipcode=123456)

如果要限制关键字参数的名字,就可以用命名关键字参数,例如,只接收cityjob作为关键字参数。这种方式定义的函数如下:

1
2
def person(name, age, *, city, job):
print(name, age, city, job)

和关键字参数**kw不同,命名关键字参数需要一个特殊分隔符**后面的参数被视为命名关键字参数。

调用方式如下:

1
2
>>> person('Jack', 24, city='Beijing', job='Engineer')
Jack 24 Beijing Engineer

如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符*了:

1
2
def person(name, age, *args, city, job):
print(name, age, args, city, job)

命名关键字参数必须传入参数名,这和位置参数不同。如果没有传入参数名,调用将报错:

1
2
3
4
>>> person('Jack', 24, 'Beijing', 'Engineer')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: person() missing 2 required keyword-only arguments: 'city' and 'job'

由于调用时缺少参数名cityjob,Python解释器把前两个参数视为位置参数,后两个参数传给*args,但缺少命名关键字参数导致报错。

命名关键字参数可以有缺省值,从而简化调用:

1
2
def person(name, age, *, city='Beijing', job):
print(name, age, city, job)

由于命名关键字参数city具有默认值,调用时,可不传入city参数:

1
2
>>> person('Jack', 24, job='Engineer')
Jack 24 Beijing Engineer

使用命名关键字参数时,要特别注意,如果没有可变参数,就必须加一个*作为特殊分隔符。如果缺少*,Python解释器将无法识别位置参数和命名关键字参数:

1
2
3
def person(name, age, city, job):
# 缺少 *,city和job被视为位置参数
pass

小结

Python的函数具有非常灵活的参数形态,既可以实现简单的调用,又可以传入非常复杂的参数。

默认参数一定要用不可变对象,如果是可变对象,程序运行时会有逻辑错误!

要注意定义可变参数和关键字参数的语法:

*args是可变参数,args接收的是一个tuple;

**kw是关键字参数,kw接收的是一个dict。

以及调用函数时如何传入可变参数和关键字参数的语法:

可变参数既可以直接传入:func(1, 2, 3),又可以先组装list或tuple,再通过*args传入:func(*(1, 2, 3))

关键字参数既可以直接传入:func(a=1, b=2),又可以先组装dict,再通过**kw传入:func(**{'a': 1, 'b': 2})

使用*args**kw是Python的习惯写法,当然也可以用其他参数名,但最好使用习惯用法。

命名的关键字参数是为了限制调用者可以传入的参数名,同时可以提供默认值。

定义命名的关键字参数在没有可变参数的情况下不要忘了写分隔符*,否则定义的将是位置参数。

递归函数

举个例子,我们来计算阶乘n! = 1 x 2 x 3 x ... x n,用函数fact(n)表示,可以看出:

fact(n)=n!=1×2×3×⋅⋅⋅×(n−1)×n=(n−1)!×n=fact(n−1)×n

所以,fact(n)可以表示为n x fact(n-1),只有n=1时需要特殊处理。

于是,fact(n)用递归的方式写出来就是:

1
2
3
4
def fact(n):
if n==1:
return 1
return n * fact(n - 1)

上面就是一个递归函数。可以试试:

1
2
3
4
5
6
>>> fact(1)
1
>>> fact(5)
120
>>> fact(100)
93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000

如果我们计算fact(5),可以根据函数定义看到计算过程如下:

1
2
3
4
5
6
7
8
9
10
=> fact(5)
=> 5 * fact(4)
=> 5 * (4 * fact(3))
=> 5 * (4 * (3 * fact(2)))
=> 5 * (4 * (3 * (2 * fact(1))))
=> 5 * (4 * (3 * (2 * 1)))
=> 5 * (4 * (3 * 2))
=> 5 * (4 * 6)
=> 5 * 24
=> 120

递归函数的优点是定义简单,逻辑清晰。理论上,所有的递归函数都可以写成循环的方式,但循环的逻辑不如递归清晰。

使用递归函数需要注意防止栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。可以试试fact(1000)

1
2
3
4
5
6
7
>>> fact(1000)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in fact
...
File "<stdin>", line 4, in fact
RuntimeError: maximum recursion depth exceeded in comparison

解决递归调用栈溢出的方法是通过尾递归优化,事实上尾递归和循环的效果是一样的,所以,把循环看成是一种特殊的尾递归函数也是可以的。

尾递归是指,在函数返回的时候,调用自身本身,并且,return语句不能包含表达式。这样,编译器或者解释器就可以把尾递归做优化,使递归本身无论调用多少次,都只占用一个栈帧,不会出现栈溢出的情况。

上面的fact(n)函数由于return n * fact(n - 1)引入了乘法表达式,所以就不是尾递归了。要改成尾递归方式,需要多一点代码,主要是要把每一步的乘积传入到递归函数中:

1
2
3
4
5
6
7
def fact(n):
return fact_iter(n, 1)

def fact_iter(num, product):
if num == 1:
return product
return fact_iter(num - 1, num * product)

可以看到,return fact_iter(num - 1, num * product)仅返回递归函数本身,num - 1num * product在函数调用前就会被计算,不影响函数调用。

fact(5)对应的fact_iter(5, 1)的调用如下:

1
2
3
4
5
6
=> fact_iter(5, 1)
=> fact_iter(4, 5)
=> fact_iter(3, 20)
=> fact_iter(2, 60)
=> fact_iter(1, 120)
=> 120

尾递归调用时,如果做了优化,栈不会增长,因此,无论多少次调用也不会导致栈溢出。

遗憾的是,大多数编程语言没有针对尾递归做优化,Python解释器也没有做优化,所以,即使把上面的fact(n)函数改成尾递归方式,也会导致栈溢出。

汉诺塔的递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def move(n, a, b, c):
    if n == 1:
        print(a, '-->', c)
    else:
        move(n-1, a, c, b)  # 将 n-1 个从 a 经过 c 移到 b
        print(a, '-->', c)  # 将最大的从 a 移到 c
        move(n-1, b, a, c)  # 将 n-1 个从 b 经过 a 移到 c

# 期待输出:
# A --> C
# A --> B
# C --> B
# A --> C
# B --> A
# B --> C
# A --> C

move(3, 'A', 'B', 'C')

1. 规则回顾

  • 有 3 根柱子 A(起点)、B(辅助)、C(终点)。

  • 有 n 个盘子,从上到下依次变小,在 A 上。

  • 一次只能移动一个盘子,并且大盘子不能放在小盘子上。

  • 目标:把所有盘子从 A 移动到 C(可借助 B)。


2. 递归思路

move(n, a, b, c)的意思是:

  • 将 n 个盘子从柱子 a 借助柱子 b 移动到柱子 c。

分解步骤(递归):

  1. 将上面 n-1 个盘子从 a 借助 c 移动到 b。

    → 调用 move(n-1, a, c, b)

  2. 将最大的盘子(第 n 个)从 a 直接移动到 c。

    → 打印 a --> c

  3. 将 b 上的 n-1 个盘子借助 a 移动到 c。

    → 调用 move(n-1, b, a, c)


3. 例子 n=3 的执行过程

初始状态

1
2
3
A: 3(largest) 2(mid) 1(smallest)
B: 空
C: 空

调用:move(3, 'A', 'B', 'C')


步骤 1.1

进入 move(3, 'A', 'B', 'C')

n=3,执行 else 分支:

  1. 调用 move(2, 'A', 'C', 'B')将上面 2 个盘子从 A 经过 C 移到 B。

步骤 2.1

进入 move(2, 'A', 'C', 'B')

n=2,执行 else 分支:

  1. 调用 move(1, 'A', 'B', 'C')将上面 1 个盘子从 A 经过 B 移到 C。

步骤 3.1

进入 move(1, 'A', 'B', 'C')

n=1,打印 A --> C

输出 1: A --> C

回到 move(2, 'A', 'C', 'B')的第 1 步完成。


步骤 3.2

move(2, 'A', 'C', 'B')的第 2 步:

打印 A --> B

输出 2: A --> B


步骤 3.3

move(2, 'A', 'C', 'B')的第 3 步:

调用 move(1, 'C', 'A', 'B')将 C 上的 1 个盘子(1号)从 C 经过 A 移到 B。

进入 move(1, 'C', 'A', 'B')

打印 C --> B

输出 3: C --> B

move(2, 'A', 'C', 'B')结束。


步骤 2.2

回到 move(3, 'A', 'B', 'C')的第 1 步完成,现在上面 2 个盘子在 B 上(顺序是小的在上),A 只剩下最大的 3 号盘。

第 2 步:打印 A --> C

输出 4: A --> C


步骤 2.3

move(3, 'A', 'B', 'C')的第 3 步:

调用 move(2, 'B', 'A', 'C')将 B 上的 2 个盘子经过 A 移到 C。


步骤 4.1

进入 move(2, 'B', 'A', 'C')

n=2,else 分支:

  1. 调用 move(1, 'B', 'C', 'A')将 B 上最小的 1 号盘从 B 经过 C 移到 A。

进入 move(1, 'B', 'C', 'A')

打印 B --> A

输出 5: B --> A

回到 move(2, 'B', 'A', 'C')第 1 步完成。


步骤 4.2

move(2, 'B', 'A', 'C')第 2 步:

打印 B --> C

输出 6: B --> C


步骤 4.3

move(2, 'B', 'A', 'C')第 3 步:

调用 move(1, 'A', 'B', 'C')将 A 上的 1 号盘从 A 移到 C。

进入 move(1, 'A', 'B', 'C')

打印 A --> C

输出 7: A --> C

move(2, 'B', 'A', 'C')结束,move(3, 'A', 'B', 'C')结束。


4. 最终输出顺序
1
2
3
4
5
6
7
1. A --> C
2. A --> B
3. C --> B
4. A --> C
5. B --> A
6. B --> C
7. A --> C

这样,我们就用递归完成了 3 个盘子从 A 到 C 的移动,且符合“大盘子不能压小盘子”的规则。

这种算法的思维重在推导,我们先从n=2的情况推导,而不用从脑袋先演算n=3的时候。

切片

Python切片的规则始终是 **[start:stop)**​ 左闭右开区间:

  • 包含 start索引的元素
  • 不包含 stop索引的元素
  • start开始,到 stop-1结束
1
2
3
4
5
6
7
8
9
10
11
12
>>> L = ['Michael', 'Sarah', 'Tracy', 'Bob', 'Jack']
# 正索引: 0 1 2 3 4
# 负索引: -5 -4 -3 -2 -1

# L[-2:] 相当于 L[3:]
# start = -2 (对应 'Bob'), stop 省略 (到列表末尾)
# 结果是 ['Bob', 'Jack'] ✅

# L[-2:-1]
# start = -2 (对应 'Bob'), stop = -1 (对应 'Jack')
# 包含-2位置的'Bob',不包含-1位置的'Jack'
# 结果是 ['Bob'] ✅

前10个数,每两个取一个:

1
2
>>> L[:10:2]
[0, 2, 4, 6, 8]

所有数,每5个取一个:

1
2
>>> L[::5]
[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95]

甚至什么都不写,只写[:]就可以原样复制一个list:

1
2
>>> L[:]
[0, 1, 2, 3, ..., 99]

tuple也是一种list,唯一区别是tuple不可变。因此,tuple也可以用切片操作,只是操作的结果仍是tuple:

1
2
>>> (0, 1, 2, 3, 4, 5)[:3]
(0, 1, 2)

字符串'xxx'也可以看成是一种list,每个元素就是一个字符。因此,字符串也可以用切片操作,只是操作结果仍是字符串:

1
2
3
4
>>> 'ABCDEFG'[:3]
'ABC'
>>> 'ABCDEFG'[::2]
'ACEG'

迭代

如果要对list实现类似Java那样的下标循环怎么办?Python内置的enumerate函数可以把一个list变成索引-元素对,这样就可以在for循环中同时迭代索引和元素本身:

1
2
3
4
5
6
>>> for i, value in enumerate(['A', 'B', 'C']):
... print(i, value)
...
0 A
1 B
2 C

列表生成式

举个例子,要生成list [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]可以用list(range(1, 11))

1
2
>>> list(range(1, 11))
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

但如果要生成[1x1, 2x2, 3x3, ..., 10x10]怎么做?方法一是循环:

1
2
3
4
5
6
>>> L = []
>>> for x in range(1, 11):
... L.append(x * x)
...
>>> L
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

但是循环太繁琐,而列表生成式则可以用一行语句代替循环生成上面的list:

1
2
>>> [x * x for x in range(1, 11)]
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

写列表生成式时,把要生成的元素x * x放到前面,后面跟for循环,就可以把list创建出来,十分有用,多写几次,很快就可以熟悉这种语法。

for循环后面还可以加上if判断,这样我们就可以筛选出仅偶数的平方:

1
2
>>> [x * x for x in range(1, 11) if x % 2 == 0]
[4, 16, 36, 64, 100]

还可以使用两层循环,可以生成全排列:

1
2
>>> [m + n for m in 'ABC' for n in 'XYZ']
['AX', 'AY', 'AZ', 'BX', 'BY', 'BZ', 'CX', 'CY', 'CZ']

三层和三层以上的循环就很少用到了。

运用列表生成式,可以写出非常简洁的代码。例如,列出当前目录下的所有文件和目录名,可以通过一行代码实现:

1
2
3
>>> import os # 导入os模块,模块的概念后面讲到
>>> [d for d in os.listdir('.')] # os.listdir可以列出文件和目录
['.emacs.d', '.ssh', '.Trash', 'Adlm', 'Applications', 'Desktop', 'Documents', 'Downloads', 'Library', 'Movies', 'Music', 'Pictures', '

map

Python内建了map()reduce()函数。

如果你读过Google的那篇大名鼎鼎的论文“MapReduce: Simplified Data Processing on Large Clusters”,你就能大概明白map/reduce的概念。

我们先看map。map()函数接收两个参数,一个是函数,一个是Iterablemap将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator返回。

举例说明,比如我们有一个函数f(x)=x2,要把这个函数作用在一个list [1, 2, 3, 4, 5, 6, 7, 8, 9]上,就可以用map()实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
            f(x) = x * x



┌───┬───┬───┬───┼───┬───┬───┬───┐
│ │ │ │ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼

[ 1 2 3 4 5 6 7 8 9 ]

│ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼

[ 1 4 9 16 25 36 49 64 81 ]

现在,我们用Python代码实现:

1
2
3
4
5
6
>>> def f(x):
... return x * x
...
>>> r = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> list(r)
[1, 4, 9, 16, 25, 36, 49, 64, 81]

map()传入的第一个参数是f,即函数对象本身。由于结果r是一个IteratorIterator是惰性序列,因此通过list()函数让它把整个序列都计算出来并返回一个list。

你可能会想,不需要map()函数,写一个循环,也可以计算出结果:

1
2
3
4
L = []
for n in [1, 2, 3, 4, 5, 6, 7, 8, 9]:
L.append(f(n))
print(L)

的确可以,但是,从上面的循环代码,能一眼看明白“把f(x)作用在list的每一个元素并把结果生成一个新的list”吗?

所以,map()作为高阶函数,事实上它把运算规则抽象了,因此,我们不但可以计算简单的f(x)=x2,还可以计算任意复杂的函数,比如,把这个list所有数字转为字符串:

1
2
>>> list(map(str, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
['1', '2', '3', '4', '5', '6', '7', '8', '9']

只需要一行代码。

再看reduce的用法。reduce把一个函数作用在一个序列[x1, x2, x3, ...]上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算,其效果就是:

1
reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4)

比方说对一个序列求和,就可以用reduce实现:

1
2
3
4
5
6
>>> from functools import reduce
>>> def add(x, y):
... return x + y
...
>>> reduce(add, [1, 3, 5, 7, 9])
25

当然求和运算可以直接用Python内建函数sum(),没必要动用reduce

但是如果要把序列[1, 3, 5, 7, 9]变换成整数13579reduce就可以派上用场:

1
2
3
4
5
6
>>> from functools import reduce
>>> def fn(x, y):
... return x * 10 + y
...
>>> reduce(fn, [1, 3, 5, 7, 9])
13579

这个例子本身没多大用处,但是,如果考虑到字符串str也是一个序列,对上面的例子稍加改动,配合map(),我们就可以写出把str转换为int的函数:

1
2
3
4
5
6
7
8
9
10
>>> from functools import reduce
>>> def fn(x, y):
... return x * 10 + y
...
>>> def char2num(s):
... digits = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}
... return digits[s]
...
>>> reduce(fn, map(char2num, '13579'))
13579

整理成一个str2int的函数就是:

1
2
3
4
5
6
7
8
9
10
from functools import reduce

DIGITS = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}

def str2int(s):
def fn(x, y):
return x * 10 + y
def char2num(s):
return DIGITS[s]
return reduce(fn, map(char2num, s))

还可以用lambda函数进一步简化成:

1
2
3
4
5
6
7
8
9
from functools import reduce

DIGITS = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}

def char2num(s):
return DIGITS[s]

def str2int(s):
return reduce(lambda x, y: x * 10 + y, map(char2num, s))

也就是说,假设Python没有提供int()函数,你完全可以自己写一个把字符串转化为整数的函数,而且只需要几行代码!

基础概念

1. 同步 (Synchronous)

任务执行必须按顺序来,一个任务完成后,下一个任务才能开始。就像你去餐厅点餐,必须等上一个菜吃完 / 上完,才能点下一个。

2. 异步 (Asynchronous)

任务执行不需要等待上一个任务完成,发起任务后可以去做其他事,等任务完成后会收到 “通知”(回调 / 事件)。就像点餐时先点完所有菜,不用等上一个菜上,后厨同时准备,菜做好了服务员会喊你。

3. 并发 (Concurrency)

多个任务在同一时间段内交替执行(看似同时进行),但同一时刻只有一个任务在执行。就像你一边写代码,一边回微信,一边喝水 —— 其实是大脑快速切换,同一时刻只做一件事。

核心:任务切换(上下文切换),利用等待时间(比如 IO 等待)处理其他任务。

  • 异步编程是实现并发的常见方式;
  • 单核 CPU 只能实现并发,无法实现并行。

4. 并行 (Parallelism)

多个任务在同一时刻真正同时执行。就像你和同事一起写代码,你们俩同一时刻都在敲键盘 —— 需要多核 CPU / 多个进程支持。

  • 概念划分
    • 并发、并行,是逻辑结构的设计模式。
    • 同步、异步,是逻辑调用方式。
    • 并发、并行是异步的 2 种实现方式。

单线程异步

1
2
#添加完整功能的tokio到项目中
cargo add tokio -F full
1
2
3
4
5
6
7
8
9
10
11
12
13
use tokio::runtime;

async fn hi() {
println!("Hello!");
}

fn main() {
let rt = runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(hi());
}

等效于

1
2
3
4
5
6
7
8
async fn hi() {
println!("Hello!");
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
hi().await;
}

注意

  1. 去除flavor = "current_thread"即可变为多线程异步
  2. async fn main() {}并非异步函数,通过安装cargo-expand可以了解到
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
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
async fn hi() {
{
::std::io::_print(format_args!("Hello!\n"));
};
}
fn main() {
let body = async {
hi().await;
};
#[allow(
clippy::expect_used,
clippy::diverging_sub_expression,
clippy::needless_return,
clippy::unwrap_in_result
)]
{
return tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("Failed building the Runtime")
.block_on(body);
}
}

多线程异步

函数解析

tokio::spawn()

示例代码

1
2
3
let task = tokio::spawn(async move {
watch_valorant(on_start, on_stop).await;
});

tokio::spawn 接受一个Future表达式,会立即返回一个 JoinHandle<T>,其中 T 是异步任务(async move { ... } 块)的返回值类型。

  • JoinHandle 是一个句柄,你可以用它来:
    • 等待任务完成: 使用 task.await。这将返回异步任务的返回值。
    • 取消任务: 使用 task.abort()

async move { ... }:
- 这是一个异步闭包(async closure)
- async: 关键字表明这是一个异步函数或闭包,它可以在等待 I/O 或其他异步操作时暂停执行,而不会阻塞整个线程。
- move: 关键字表示这个闭包会获取它所捕获的所有变量的所有权。这意味着闭包内部的代码将拥有这些变量,而不是仅仅借用它们。这对于在 tokio::spawn 中启动的任务非常重要,因为任务可能会在函数作用域之外执行,所以它需要拥有它自己的数据副本或所有权。

Mutex

use tokio::sync::Mutex;:
- tokio::sync::Mutex 是 Tokio 运行时提供的异步互斥锁
- 与标准的 std::sync::Mutex 不同,tokio::sync::Mutex 是异步的。这意味着当一个任务尝试获取锁但锁已经被另一个任务持有(即发生阻塞)时,它不会阻塞整个线程,而是会让出 CPU 时间,允许其他任务运行,直到锁被释放。这对于在异步环境中(如 Tauri 应用中)管理共享资源非常重要。
- 这里的 Mutex 用来确保在任何时候,只有一个任务可以访问和修改 Option<JoinHandle<()>>。这可以防止在启动新任务和取消旧任务时发生数据不一致的问题。

键值对

1
2
3
4
name = "Zack"
age = "18"
favorite.pet = "Cat"
favorite.hobby = "Draw"
1
2
3
4
5
6
7
8
{
"name": "Zack",
"age": "18",
"favorite": {
"pet": "Cat",
"hobby": "Drwa"
}
}

注意

  1. 在toml中,顶层表中的键值对无法后置,像上述nameage不能放在favorite的下面
  2. 在toml中,结构不受缩进影响

字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
family = '''
Zack
Jack
'''

# 上述等效于
family = """
Zack
Jack
"""

# 上述等效于
family = """
Zack\
Jack\
"""
1
2
3
{
"family": "Zack\nJack"
}

注意

  1. 单行字面量不支持转义
  2. ”“” 双行字面量作用下,可加 \ 显式,也可不加

数组

toml的数组跟json的非常相像

1
2
3
4
5
6
hobby = [
[
"game",
"running",
]
]
1
2
3
4
5
6
7
8
{
"hobby": [
[
"game",
"running"
]
]
}

注意

  1. 在toml中,数组可以跨行,意味着可以将数组的每个元素都写在单独的一行
  2. 在toml中,数组支持尾逗号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
profile.age = "18"
profile.name = "Zack"
profile.jobs.Efficlab = true
about.pets.cat = "Meow"

# 以上等效于
[profile]
age = "18"
name = "Zack"

[about.pets]
cat = "Meow"

[profile.jobs]
Efficlab = true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"profile": {
"age": "18",
"name": "Zack",
"jobs": {
"Efficlab" = true
}
},
"about": {
"pets": {
"cat": "Meow"
}
}
}

注意

  1. 内联表只能用一行来写,如jobs = { Efficlab = true }

表数组

1
2
3
4
5
6
7
8
9
10
11
12
13
[[profile]]
name = "Zack"
age = "18"

[[profile]]
name = "Alice"
age = "17"

# 等效写法
profile = [
{ name = "Zack", age = "18" },
{ name = "Alice", age = "17"},
]
1
2
3
4
5
6
7
8
9
10
11
12
{
"profile": [
{
"name": "Zack",
"age": "18"
},
{
"name": "Alice",
"age": "17"
}
]
}

注释

toml支持用#来注释文本,而json不支持注释

Message

1
2
3
4
5
6
7
8
9
# 邮件建造示例
let email = Message::builder()
        .from("NoBody <nobody@domain.tld>".parse().unwrap())
        .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
        .to("Hei <hei@domain.tld>".parse().unwrap())
        .subject("Happy new year")
        .header(ContentType::TEXT_PLAIN)
        .body(String::from("Be happy!"))
        .unwrap();
  • from - 邮件发送人
  • to - 邮件收件人
  • reply_to - 邮件默认回复邮(发送的邮件(设置了该项),收到者点击回复邮件自动定向到设定的邮箱)
  • subject - 邮件主题
  • header - 内容形式
  • body - 邮件内容

SmtpTransport

1
2
3
4
5
// Open a remote connection to example
let mailer = SmtpTransport::from_url("smtps://username:password@smtp.example.com:465")?.build();
// Send the email
mailer.send(&email)?;
Ok(())
1
2
3
4
5
6
7
8
9
10
// Create the SMTPS transport
let sender = SmtpTransport::relay("smtp.example.com")?
// Add credentials for authentication
.credentials(Credentials::new(
"username".to_owned(),
"password".to_owned(),
))
// Optionally configure expected authentication mechanism
.authentication(vec![Mechanism::Plain])
.build();

求开路电压

选定所求支路,将其开路,通过KVL来求Uoc两端电压

求等效电阻

  1. 将独立源置零(去电压源就短路,去电流源就段路)
  2. 计算端口Uoc的等效电阻Req

最后将电路变换成,一个电压源Uoc和一个等效电阻的Req的电路(诺顿反之同理)

0%