diff --git a/TEST/TEST01.ipynb b/TEST/TEST01.ipynb new file mode 100644 index 000000000..790ac74be --- /dev/null +++ b/TEST/TEST01.ipynb @@ -0,0 +1,66 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "数字1和数字2相加的结果为3.0。\n" + ] + } + ], + "source": [ + "# 数字求和:通过用户输入两个数字,并计算两个数字之和:\n", + "num1 = input(\"请输入第一个数字:\")\n", + "num2 = input(\"请输入第二个数字:\")\n", + "sum = float(num1) + float(num2)\n", + "print(f\"数字{num1}和数字{num2}相加的结果为{sum}。\")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "数字-8.0的平方根是(1.7319121124709868e-16+2.8284271247461903j)。\n" + ] + } + ], + "source": [ + "# 平方根:用户输入一个数字,并计算这个数的平方根\n", + "num = float(input(\"请输入一个数字:\"))\n", + "num_sqrt = num ** 0.5\n", + "print(f\"数字{num}的平方根是{num_sqrt}。\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "base", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/new-test-20251106.txt b/new-test-20251106.txt new file mode 100644 index 000000000..2b7e60900 --- /dev/null +++ b/new-test-20251106.txt @@ -0,0 +1 @@ +1232 \ No newline at end of file diff --git "a/\345\255\246\344\271\240\347\254\224\350\256\260/Pyhton\345\270\270\347\224\250\346\250\241\345\235\227\345\217\212\345\207\275\346\225\260.ipynb" "b/\345\255\246\344\271\240\347\254\224\350\256\260/Pyhton\345\270\270\347\224\250\346\250\241\345\235\227\345\217\212\345\207\275\346\225\260.ipynb" new file mode 100644 index 000000000..8e156661f --- /dev/null +++ "b/\345\255\246\344\271\240\347\254\224\350\256\260/Pyhton\345\270\270\347\224\250\346\250\241\345\235\227\345\217\212\345\207\275\346\225\260.ipynb" @@ -0,0 +1,221 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "time 模块\n", + "print()\n", + "str()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### print()函数\n", + "\n", + "```python\n", + "print('a','b','c') # 输出a b c,因为print参数sep默认为空格' '\n", + "\n", + "i = 1\n", + "print('i=',i)\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Help on built-in function print in module builtins:\n", + "\n", + "print(*args, sep=' ', end='\\n', file=None, flush=False)\n", + " Prints the values to a stream, or to sys.stdout by default.\n", + "\n", + " sep\n", + " string inserted between values, default a space.\n", + " end\n", + " string appended after the last value, default a newline.\n", + " file\n", + " a file-like object (stream); defaults to the current sys.stdout.\n", + " flush\n", + " whether to forcibly flush the stream.\n", + "\n" + ] + } + ], + "source": [ + "help(print)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 静态对象\n", + "静态对象一般指程序运行期间不会发生改变的对象,或者不需要实例化就可以直接使用的对象。包括:\n", + "- 模块:导入后可以直接使用的对象。\n", + "- 类的静态成员:无需实例化类即可访问的属性或方法。\n", + "- 不可变对象:值在创建后不会改变的对象。" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 数学函数\n", + "| 函数 | 返回值 (描述) | 注意事项 |\n", + "|------|---------------|----------|\n", + "| abs(x) | 返回数字的绝对值,如 `abs(-10)` 返回 `10` | 内置函数,复数则返回其大小 |\n", + "| fabs(x) | 以浮点数形式返回数字的绝对值,如 `math.fabs(-10)` 返回 `10.0` | |\n", + "| ceil(x) | 返回数字的上入整数,如 `math.ceil(4.1)` 返回 `5` | `math.ceil(-4.1)` 返回 `-4` |\n", + "| floor(x) | 返回数字的下舍整数,如 `math.floor(4.9)` 返回 `4` | |\n", + "| exp(x) | 返回 e 的 x 次幂,如 `math.exp(1)` 返回 `2.718281828459045` | |\n", + "| log(x) | 如 `math.log(math.e)` 返回 `1.0`,`math.log(100,10)` 返回 `2.0` | |\n", + "| sqrt(x) | 返回数字 x 的平方根,如 `math.sqrt(100)` 返回 `10.0` | |\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### xx函数\n", + "| round(x [,n]) | 返回浮点数 x 的四舍五入值,如给出 n 值,则代表舍入到小数点后的位数。|不适合精度要求高的情况;round(2.675,2)会返回2.67,“四舍六入”|\n", + "| sqrt(x) | 返回数字 x 的平方根。|math.sqrt(100)返回10.0|" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 随机数函数\n", + "随机数可以用于数学,游戏,安全等领域中,还经常被嵌入到算法中,用以提高算法效率,并提高程序的安全性。\n", + "需要先导入random模块\n", + "| 函数 | 描述 |注意事项|\n", + "|---------------|-------------|--------|\n", + "| `choice(seq)` | 从序列的元素中随机挑选一个元素,比如 `random.choice(range(10))`,从 0 到 9 中随机挑选一个整数。|seq可以是列表、元组或者字符串|\n", + "| `randrange([start,] stop [,step])` | 从指定范围内,按指定基数递增的集合中获取一个随机数,基数默认值为 1。||\n", + "| `random()` | 随机生成下一个实数,它在 `[0,1)` 范围内。 ||\n", + "| `seed([x])` | 改变随机数生成器的种子 `seed`。如果你不了解其原理,你不必特别去设定 `seed`,Python 会帮你选择 `seed`。 ||\n", + "| `shuffle(lst)`| 将序列的所有元素随机排序。|random.shuffle ()返回值是None|\n", + "| `uniform(x, y)` | 随机生成下一个实数,它在 `[x, y]` 范围内。|返回浮点数,闭区间|\n", + "\n", + "range()返回的是一个range对象,不是一个具体的列表或者元组。" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 三角函数\n", + "\n", + "| 函数 | 描述 |\n", + "|---------------|--------------------------|\n", + "| `acos(x)` | 返回 `x` 的反余弦弧度值。 |\n", + "| `asin(x)` | 返回 `x` 的反正弦弧度值。 |\n", + "| `atan(x)` | 返回 `x` 的反正切弧度值。 |\n", + "| `atan2(y, x)` | 返回给定的 `x` 和 `y` 坐标值的反正切值。 |\n", + "| `cos(x)` | 返回 `x` 弧度的余弦值。 |\n", + "| `hypot(x, y)` | 返回欧几里德范数 `sqrt(x*x + y*y)`。 |\n", + "| `sin(x)` | 返回 `x` 弧度的正弦值。 |\n", + "| `tan(x)` | 返回 `x` 弧度的正切值。 |\n", + "| `degrees(x)` | 将弧度转换为角度,如 `degrees(math.pi/2)` 返回 `90.0`。 |\n", + "| `radians(x)` | 将角度转换为弧度。 |\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "range 函数:\n", + "Python3 range() 函数返回的是一个可迭代对象(类型是对象),而不是列表类型, 所以打印的时候不会打印列表。" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## operator模块" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## map函数\n", + "把“同一个函数”依次作用到“可迭代对象的每一个元素”上,生成一个新的迭代器。\n", + "map(function,iterable)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Help on class map in module builtins:\n", + "\n", + "class map(object)\n", + " | map(func, *iterables) --> map object\n", + " |\n", + " | Make an iterator that computes the function using arguments from\n", + " | each of the iterables. Stops when the shortest iterable is exhausted.\n", + " |\n", + " | Methods defined here:\n", + " |\n", + " | __getattribute__(self, name, /)\n", + " | Return getattr(self, name).\n", + " |\n", + " | __iter__(self, /)\n", + " | Implement iter(self).\n", + " |\n", + " | __next__(self, /)\n", + " | Implement next(self).\n", + " |\n", + " | __reduce__(...)\n", + " | Return state information for pickling.\n", + " |\n", + " | ----------------------------------------------------------------------\n", + " | Static methods defined here:\n", + " |\n", + " | __new__(*args, **kwargs)\n", + " | Create and return a new object. See help(type) for accurate signature.\n", + "\n" + ] + } + ], + "source": [ + "help(map)\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "base", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git "a/\345\255\246\344\271\240\347\254\224\350\256\260/Python3\345\256\236\344\276\213.ipynb" "b/\345\255\246\344\271\240\347\254\224\350\256\260/Python3\345\256\236\344\276\213.ipynb" new file mode 100644 index 000000000..793d74623 --- /dev/null +++ "b/\345\255\246\344\271\240\347\254\224\350\256\260/Python3\345\256\236\344\276\213.ipynb" @@ -0,0 +1,117 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1.41\n", + "(1.189207115002721+1.1892071150027212j)\n" + ] + } + ], + "source": [ + "# 平方根\n", + "a = 2\n", + "print(f\"{a**0.5:0.2f}\") # 仅限于正数\n", + "\n", + "b = -8\n", + "import cmath\n", + "print(cmath.sqrt(b**0.5)) # 负数和复数使用" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 求解二次方程" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "您输入的参数组成的二次方程为:1.0x^2 + 20.5x + 66.333 = 0\n", + "该方程有2个实根!\n" + ] + } + ], + "source": [ + "# 求解二次方程\n", + "\n", + "## 初版回答:\n", + "## 这里用户要输入3个数字(),而且二次项系数不能为0。\n", + "try:\n", + " del a,b\n", + "except NameError:\n", + " print(\"变量已清空!\")\n", + "\n", + "s1 = input(\"请输入二次方程的参数a、b、c(以空格分开):\")\n", + "## 还要确保用户输入的是数字,不对要重复输入\n", + "a,b,c = map(float,s1.split()) ## 不一定为int,可能为float,但是float包含int,所以用float最稳妥?\n", + "## 这里面的map是一个迭代器?\n", + "print(f\"您输入的参数组成的二次方程为:{a}x^2 + {b}x + {c} = 0\")\n", + "\n", + "judge = b**2 - 4*a*c\n", + "\n", + "if judge > 0:\n", + " print(\"该方程有2个实根!\")\n", + "elif judge == 0:\n", + " print(\"该方程只有1个实根!\")\n", + "else:\n", + " print(\"该方程没有实根,但有两个负数根!\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Variable Type Data/Info\n", + "------------------------------\n", + "cmath module \n" + ] + } + ], + "source": [ + "# 求解二次方程\n", + "\n", + "# " + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "base", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git "a/\345\255\246\344\271\240\347\254\224\350\256\260/Python\345\237\272\347\241\200\347\237\245\350\257\206.ipynb" "b/\345\255\246\344\271\240\347\254\224\350\256\260/Python\345\237\272\347\241\200\347\237\245\350\257\206.ipynb" new file mode 100644 index 000000000..e73119981 --- /dev/null +++ "b/\345\255\246\344\271\240\347\254\224\350\256\260/Python\345\237\272\347\241\200\347\237\245\350\257\206.ipynb" @@ -0,0 +1,3297 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "59f1ad7d", + "metadata": {}, + "source": [ + "> **记笔记的目的**: \n", + "> \n", + "> ★笔记不是“完整教程”,而是理解+易错点+示例+练习 \n", + "> \n", + "> 每一节:\n", + "> - “一句话总结(核心)”\n", + "> - “示例(能运行、最简单)”\n", + "> - “易错点(踩过的坑)”\n", + "> - “应用场景(什么时候用)”" + ] + }, + { + "cell_type": "markdown", + "id": "8a126022", + "metadata": {}, + "source": [ + "> 什么是“一句话总结”: \n", + "> - 它是什么\n", + "> - 为什么存在(解决什么问题?)\n", + "> - 什么时候用" + ] + }, + { + "cell_type": "markdown", + "id": "08c8aa7f", + "metadata": {}, + "source": [ + "> Python基础知识结构划分: \n", + "> - Python的特点\n", + "> - 环境搭建\n", + "> - 基础语法 \n", + "> - 变量 \n", + "> - 基本数据类型(数据结构) \n", + "> - 分支、循环结构\n", + "> - 函数和模块\n", + "> - 面向对象编程" + ] + }, + { + "cell_type": "markdown", + "id": "7b990156", + "metadata": {}, + "source": [ + "### Python的特点\n", + "1.大小写敏感。\n", + "2.对象(Object)、类(Class)、实例(Instance)三者的区别和联系\n", + "|名词|Python中的角色|举例|\n", + "|----|-----|----|\n", + "|对象(Object)|一切都是对象,包括数字、字符串、函数、类、模块|1、\"ABC\"、len、int|\n", + "|类(Class)|描述对象结构的模板,本质也是对象|int、list、dict|\n", + "|实例(Instance)|由类创建出来的对象|1 是 int 的实例|" + ] + }, + { + "cell_type": "markdown", + "id": "e43860bf", + "metadata": {}, + "source": [ + "### 环境搭建\n", + "为了能够在机器上运行Python,要处理的问题:\n", + "- 环境变量的问题 \n", + "- 二进制、八进制、十进制、十六进制\n", + "二进制的补码问题。\n", + "\n", + "- ASCII码、Unicode码\n", + "Python中所有的字符串都是Unicode字符串。\n", + "#### 命令行参数\n", + "交互模式中,最后被输出的表达式结果被赋值给变量_,例如:\n", + "```shell\n", + ">>> tax = 12.5 / 100\n", + ">>> price = 100.50\n", + ">>> price * tax\n", + "12.5625\n", + ">>> price + _\n", + "113.0625\n", + ">>> round(_, 2)\n", + "113.06\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "d9525ae6", + "metadata": {}, + "source": [ + "### Python基础语法\n", + "即为书写Python必须遵循的规则。\n", + "#### 标识符\n", + "标识符即为变量的命名,第一个字符必须以字母(a-z, A-Z)或下划线 _ ,其他部分由字母、数字和下划线组成。 \n", + "注意:标识符不能包含特殊字符(如 - ¥ $),不能使用数字开头,不能使用关键字。\n", + "##### 保留关键字\n", + "保留关键字在Python中有特定用处,不能作为标识符使用。\n", + "一旦用了,就会报错。\n", + "#### 注释\n", + "```\n", + "# 单行注释\n", + "# 第二个注释\n", + "\n", + "'''\n", + "第三行注释\n", + "第四行注释\n", + "'''\n", + "\n", + "\"\"\"\n", + "第五行注释\n", + "第六行注释\n", + "\"\"\"\n", + "```\n", + "#### 行与缩进\n", + "Python中用缩进表示代码块,而不是大括号`{}`。 \n", + "Python中一条语句很长,可以用反斜杠`\\`来实现换行。\n", + "```\n", + "total = item_one + \\\n", + " item_two + \\\n", + " item_three\n", + "``` \n", + "Python中同一行也可以有多个语句,用分号;分隔。 \n", + "在类或函数之间一般会留一个空行,便于日后的维护和重构。 \n", + "当写示例时,可以使用pass这个占位语句,表示什么都不做,以避免语法错误。\n", + "\n", + "#### 常用基本功能\n", + "1.接收用户用户输入,`str1=input()` \n", + "接收到的str1类型为string,还是`` \n", + "2.导入模块,用import与from...import \n", + "导入模块后,Python要知道使用的函数、类、变量来自哪个模块。 \n", + "`from...import`直接引入目标名称,调用更短,但是容易发生命名冲突。\n", + "`from...import *`会导入模块总的所有函数、变量,除了_开头的名字,但是会污染当前代码的命名空间。 \n", + "3.print格式化输出 \n", + "print()默认输出是带换行符的,即其中参数end默认值为'\\n'。\n", + "\n", + "#### 变量\n", + "1.Python中所有变量都是“名字→对象”的引用(变量存的是对象的地址)。 \n", + "变量没有类型,只有对象有类型。变量本质上等同于“自动管理内存的安全指针”。 \n", + "\n", + "2.一般说的变量类型指的是“变量当前指向的对象的类型”。\n", + "\n", + "3.衍生问题: \n", + "(1)如果变量存的是内存的地址,那么为什么print(变量)会返回对象的值? \n", + "print()调用的是变量所指向对象的`__str__()、__repr__()`方法,而不是打印地址。 \n", + "如果要打印地址,可以用print(id(a))返回变量a在内存中的地址id。 \n", + "(2)`a=1`和`a+1`中,变量a是地址,那为什么可以计算a+1? \n", + "先找到变量a指向的对象(1),调用该对象的__add__()方法(a.__add__(1))生成一个新的对象(2),然后把这个新的对象返回。原先的a和1都不会被修改。 \n", + "如果是`a=a+1`呢,这时候a引用的对象会变成2。 \n", + "(3)不可变对象(int,str,tuple)不能改,只能换对象; \n", + "可变对象(list,dict,set)能改,引用不变。 \n", + "不可变对象不允许修改内部值,修改只能创新新对象,旧对象如果无人引用则会被自动回收。 或者用del语句删除? \n", + "```python\n", + "a = 10 # int类型不可变\n", + "b = a\n", + "a = 20 # 修改a的值,不会影响b的值,10,20是两个独立的对象\n", + "# b 指的还是原先的整数10对象\n", + "\n", + "c = [1,2,3] # list类型可变\n", + "d = c\n", + "d.append(4) # 修改d的值,c的值也会变化,c和d是同个引用\n", + "```\n", + "(4)type(变量a)的时候,将变量a引用的对象作为参数传入内置函数type(),由type()查看该对象内部的`__class__属性`\n", + "\n", + "4.多变量赋值的问题:\n", + "`a=b=c=1`\n", + "`a,b,c=1,2,'runoob'`\n", + "\n", + "5.调换两个变量的值\n", + "`a,b=b,a`" + ] + }, + { + "cell_type": "markdown", + "id": "828e1f34", + "metadata": {}, + "source": [ + "### 数据类型\n", + "#### 数据类型简介\n", + "1.常见数据类型:\n", + "- Number(数字、数值):包含int、float、bool、complex(复数,例如4+3j)\n", + "- String(字符串)\n", + "- bool(布尔类型)\n", + "- List(列表)\n", + "- Tuple(元组)\n", + "- Set(集合)\n", + "- Dictionary(字典) \n", + "\n", + "2.数据类型分类可以按照是否可变,划分为: \n", + "不可变数据类型:Number、String、Tuple \n", + "可变数据类型:List、Dictionary、Set\n", + "\n", + "3.判断数据类型 \n", + "(1)type(obj)用来查对象的实际类型(class),例如`type(a)`,isinstance()用来判断对象(实例)是不是某个类型(或其子类)的实例,例如`isinstance(a,int)` \n", + "(2)区别:type()严格匹配类型,不考虑继承;isinstance()会考虑继承关系。 \n", + "```python\n", + "class A:pass # pass占位语句\n", + "class B(A):pass\n", + "\n", + "b = B()\n", + "print(type(b) is B) # True\n", + "print(type(b) is A) # False\n", + "print(isinstance(b,B)) # True\n", + "print(isinstance(b,A)) # True\n", + "print(issubclass(B,A)) # 判断类的继承关系,True\n", + "```\n", + "(3)特殊情况:\n", + "- `type(a) == int`和`type(a) is int`有什么区别? \n", + "`==` 是比较两个对象的“值”是否相等,不推荐,原因在==依赖类的`__eq__`,不是很严格; \n", + "`is` 是判断两个对象是否是“同一个对象”(id一致),推荐。\n", + "补充说明:int在Python中也是一个对象(类对象)\n", + "- `type(A)`返回的是啥?\n", + "返回type,类也是对象,Python中所有类的类型都是type\n", + "- `isinstance(B,A)`为啥是False?\n", + "`isinstance()`是用于判断实例是否属于某个类或者某个父类。\n", + "如果要判断两个类的继承关系,可以用`issubclass()`\n", + "\n", + "\n", + "#### 各个数据类型介绍与使用\n", + "##### 数字\n", + "1.数字注意事项:\n", + "数字是不可变类型。\n", + "\n", + "2.数字运算 \n", + "常规运算(加减乘除),除法或者混合运算时(包含浮点数和整数)运算结果一定为浮点数 \n", + "特殊运算:取余(%)、整除(//)、乘方(**) \n", + "`10%3`返回余数1 \n", + "`10//3`返回整数3,即10按照3来分,可以分成3个整份 \n", + "`10**3`返回1000 `4**0.5`返回2.0 \n", + "\n", + "3.运算符\n", + "注意事项:\n", + "(1)//取整是向小的方向取整数,例如9//2等于4,-9//2等于-5\n", + "(2):=海象运算符,用于在表达式中同时进行赋值,并返回赋值的值。Python3.8版本新增。\n", + "用法举例:\n", + "```python\n", + "#传统写法\n", + "n = 10\n", + "if n > 5:\n", + " print(n)\n", + "#使用海象运算符\n", + "if (n:=10) > 5: # 现把n赋值为10,再返回这个赋值结果\n", + " print(n)\n", + "```\n", + "(3)位运算符\n", + "位运算符把数字看做二进制来进行计算。\n", + "```python\n", + "a = 60\n", + "print(f'{a:08b}') # a的二进制数为 00111100\n", + "b = 13\n", + "print(f'{b:08b}') # b的二进制数为 00001101\n", + "\n", + "# & 按位与:两个对应位置都是1,则结果位为1,否则为0\n", + "print(a&b) #输出12 # a&b为 00001100\n", + "\n", + "# | 按位或:两个对应位置有一个是1,则结果位为1,否则为0\n", + "print(a|b) #输出61 # a|b为 00111101\n", + "\n", + "# ^ 按位异或:两个对应位置不同时,结果为1,否则为0\n", + "print(a^b) #输出49 # a|b为 00110001\n", + "\n", + "# ~ 按位取反:把0变成1,把1变成0\n", + "print(~a) #输出-61 # ~a为 -0111101?\n", + "\n", + "# << 左移动若干位\n", + "print(a<<2) #输出240,相当于放大了2的2次方倍 # <> 左移动若干位\n", + "print(a>>2) #输出15,相当于缩小了2的2次方倍 # <>`、`<<` | 右移、左移 |\n", + "| `&` | 按位与 |\n", + "| `^`、`\\|` | 按位异或、按位或 |\n", + "| `<=`、`<`、`>`、`>=` | 小于等于、小于、大于、大于等于 |\n", + "| `==`、`!=` | 等于、不等于 |\n", + "| `is`、`is not` | 身份运算符 |\n", + "| `in`、`not in` | 成员运算符 |\n", + "| `not`、`or`、`and` | 逻辑运算符 |\n", + "| `=`、`+=`、`-=`、`*=`、`/=`、`%=`、`//=`、`**=`、`&=`、`\\|=`、`^=`、`>>=`、`<<=` | 赋值运算符|\n", + "\n", + "补充说明:\n", + "not是小写。\n", + "逻辑运算符有阻断。\n", + "\n", + "2.数值不同表示形式 \n", + "(1)整数:十进制、二进制、八进制、十六进制 \n", + "```python\n", + "print(0o12) #八进制,0o或者0O\n", + "print(0b1010) #二进制,0b或者0B\n", + "print(0xA) #十六进制,0x或者0X\n", + "#十六进制的数A B C D E F 分别表示十进制中的 10 11 12 13 14 15\n", + "\n", + "# print()默认输出数值是转换为十进制显示。\n", + "print(oct(97)) #oct()十进制转为八进制字符串,输出0o141\n", + "print(hex(97)) #hex()十进制转为十六进制字符串,输出0x61\n", + "print(bin(97)) #bin()十进制转为二进制字符串,输出0b1100001\n", + "```\n", + "(2)浮点数:主要是显示格式(小数点后几位)的问题 \n", + "见字符串部分输出格式表 \n", + "(3)复数:复数由实数部分和虚数部分构成,可以用a + bj,或者complex(a,b) 表示,复数的实部a和虚部b都是浮点型。如`3e+26j` " + ] + }, + { + "cell_type": "markdown", + "id": "3fb23002", + "metadata": {}, + "source": [ + "##### 字符串\n", + "1.字符串表示相关问题 \n", + "(1)反斜杠`\\`转义特殊字符,可以在字符串前面加一个`r`表示原始字符。\n", + "| 转义符 | 描述 | 示例代码 | 输出结果 |\n", + "|--------|----------|---------------|------------|\n", + "| `\\` | 续行符 | `print(\"line1 \\\\\\nline2\")` | `line1 line2` |\n", + "| `\\\\` | 反斜杠 | `print(\"\\\\\")` | `\\` |\n", + "| `\\'` | 单引号 | `print('\\'')` | `'` |\n", + "| `\\\"` | 双引号 | `print(\"\\\"\")` | `\"` |\n", + "| `\\a` | 响铃 | `print(\"\\a\")` | 响声 |\n", + "| `\\b` | 退格 | `print(\"Hello \\bWorld!\")` | `HelloWorld!` |\n", + "| `\\000` | 空字符 | `print(\"\\000\")` | 空(什么都不输出) |\n", + "| `\\n` | 换行 | `print(\"\\n\")` | 换行 |\n", + "| `\\v` | 纵表符 | `print(\"Hello\\vWorld!\")` | `Hello` 换行 `World!` |\n", + "| `\\t` | 横表符 | `print(\"Hello\\tWorld!\")` | `Hello World!` |\n", + "| `\\r` | 回车,将 `\\r` 后的内容移到字符串开头,并逐一替换开头部分的字符| `print(\"Hello\\rWorld!\")` | `World!` |\n", + "| `\\f` | 换页 | `print(\"Hello\\fWorld!\")` | `Hello` 换页 `World!` |\n", + "| `\\yyy` | 八进制 | `print(\"\\110\\145\")` | `He` |\n", + "| `\\xYY` | 十六进制,\\x开头,YY代表的字符 | `print(\"\\x48\\x65\")` | `He` |\n", + "| `\\other` | 普通字符 | 无特殊处理 | 无特殊处理 |\n", + "\n", + "例子:用\\r实现百分比进度\n", + "```python\n", + "import time\n", + "\n", + "for i in range(101): # 添加进度条图形,例如[#### ] 和百分比,例如30%\n", + " bar = '[' + '#' * (i) + ' ' * (100-i) + ']' # 100个\n", + " print(f'\\r{bar} {i:3}%',end='',flush=True) # flush选项的作用\n", + " time.sleep(0.02)\n", + "print()\n", + "```\n", + "(2)字符串的索引和截取:\n", + "区域是左闭右开,从0开始,-1为从末尾开始的位置。 \n", + "```python\n", + "str = 'Runoob'\n", + "print(str[0:-1]) # 'Runoob'\n", + "print(str[0]) # 'R'\n", + "print(str[2:5]) # 'noo'\n", + "print(str[2:]) # 'noob'\n", + "print(str[:2]) # 'Ru'\n", + "print(str[::-1]) # 'boonuR',默认步长为1,步长为-1时会反转\n", + "print(str[-1::-1]) # 'boonuR',默认步长为1,截取内的参数[start,end,step],当start为-1,end为空时,默认从右到左\n", + "```\n", + "\n", + "(3)字符串的运算符\n", + "```python\n", + "print(str * 2) # 'RunoobRunoob',*实现重复\n", + "print(str+'abc') # 'Runoobabc',+实现多个字符串连接\n", + "```\n", + "\n", + "(4)格式化输出:\n", + "有几种格式化方法\n", + "(a)用字符串格式符,例如%s(格式化字符串)、%d(格式化整数)\n", + "`print (\"我叫 %s 今年 %d 岁!\" % ('小明', 10))`\n", + "(b)用str.format(),用{}和:来代替以前的%\n", + "`{1} {0} {1}\".format(\"hello\", \"world\") # 设置指定位置`\n", + "(c)f-string,字面量格式化字符串,Python3.6之后的版本新增的格式化字符串\n", + "f后面跟着字符串,字符串中的表达式用大括号{}抱起来,会将表达式的结算结果带进输出的字符串里面\n", + "| 变量值 | 占位符 | 格式化结果 | 说明 |\n", + "| ----------- | ---------- | ------------| ---- |\n", + "| `3.1415926` | `{:.2f}` | `'3.14'` | 保留小数点后两位 |\n", + "| `3.1415926` | `{:+.2f}` | `'+3.14'` | 带符号保留小数点后两位 |\n", + "| `-1` | `{:+.2f}` | `'-1.00'` | 带符号保留小数点后两位 |\n", + "| `3.1415926` | `{:.0f}` | `'3'` | 不带小数 |\n", + "| `123` | `{:0>10d}` | `'0000000123'` | 左边补`0`,补够10位 |\n", + "| `123` | `{:x<10d}` | `'123xxxxxxx'` | 右边补`x` ,补够10位 |\n", + "| `123` | `{:>10d}` | `' 123'` | 左边补空格,补够10位 |\n", + "| `123` | `{:<10d}` | `'123 '` | 右边补空格,补够10位 |\n", + "| `123456789` | `{:,}` | `'123,456,789'` | 逗号分隔格式 |\n", + "| `0.123` | `{:.2%}` | `'12.30%'` | 百分比格式 |\n", + "| `123456789` | `{:.2e}` | `'1.23e+08'` | 科学计数法格式 |\n", + "\n", + "^, <, > 分别是居中、左对齐、右对齐,后面带宽度, : 号后面带填充的字符,只能是一个字符,不指定则默认是用空格填充。+ 表示在正数前显示 +,负数前显示 -; (空格)表示在正数前加空格\n", + "b、d、o、x 分别是二进制、十进制、八进制、十六进制。\n", + "此外我们可以使用大括号 {} 来转义大括号。\n", + "\n", + "(5)字符串不是特殊的列表,也不是特殊的元组;它是 Python 里独立的一种不可变序列类型(str),专门为文本处理而优化。\n", + "\n", + "(6)字符串用三引号,无需转义特殊字符,同时可以保持字符格式,所见即所得\n", + "\n", + "2.字符串内置的方法\n", + "字符串连接:`+` 、`''.join()`\n", + "| 分类 | 方法 | 描述 | 示例代码 |\n", + "|--------|-------------------------------|--------------------------------------|------------------------------------|\n", + "| 格式化方法 | capitalize() | 将字符串的第一个字符转换为大写。 | `'abc'.capitalize()` |\n", + "| | center(width, fillchar) | 返回一个指定宽度 `width` 居中的字符串,`fillchar` 为填充字符,默认为空格。 | `'abc'.center(10, '-')` |\n", + "| | ljust(width, fillchar) | 返回一个左对齐的字符串,使用 `fillchar` 填充至长度 `width`。 | `'abc'.ljust(10, '-')` |\n", + "| | rjust(width, fillchar) | 返回一个右对齐的字符串,使用 `fillchar` 填充至长度 `width`。 | `'abc'.rjust(10, '-')` |\n", + "| | zfill(width) | 返回长度为 `width` 的字符串,原字符串右对齐,前面填充 `0`。 | `'abc'.zfill(5)` |\n", + "| | title() | 返回标题化的字符串,即每个单词首字母大写,其余字母小写。 | `'hello world'.title()` |\n", + "| 查找方法 | find(str, beg=0, end=len()) | 检测 `str` 是否包含在字符串中,返回索引值,若不存在返回 -1。 | `'abc'.find('b', 0, 3)` |\n", + "| | index(str, beg=0, end=len()) | 类似于 `find()`,但若 `str` 不存在则抛出异常。 | `'abc'.index('b', 0, 3)` |\n", + "| | rfind(str, beg, end) | 类似于 `find()`,但从右边开始查找。 | `'abcabc'.rfind('a', 0, 6)` |\n", + "| | rindex(str, beg, end) | 类似于 `index()`,但从右边开始查找。 | `'abcabc'.rindex('a', 0, 6)` |\n", + "| 检查方法 | isalnum() | 检查字符串是否只由字母和数字组成,若是则返回 `True`,否则返回 `False`。 | `'abc123'.isalnum()` |\n", + "| | isalpha() | 检查字符串是否只由字母组成,若是则返回 `True`,否则返回 `False`。 | `'abc'.isalpha()` |\n", + "| | isdigit() | 检查字符串是否只包含数字,若是则返回 `True`,否则返回 `False`。 | `'123'.isdigit()` |\n", + "| | islower() | 检查字符串是否全为小写字母,若是则返回 `True`,否则返回 `False`。 | `'abc'.islower()` |\n", + "| | isupper() | 检查字符串是否全为大写字母,若是则返回 `True`,否则返回 `False`。 | `'ABC'.isupper()` |\n", + "| | isnumeric() | 检查字符串是否只包含数字字符,若是则返回 `True`,否则返回 `False`。 | `'123'.isnumeric()` |\n", + "| | isspace() | 检查字符串是否只包含空白字符,若是则返回 `True`,否则返回 `False`。 | `' '.isspace()` |\n", + "| | istitle() | 检查字符串是否是标题化的,若是则返回 `True`,否则返回 `False`。 | `'Hello World'.istitle()` |\n", + "| 替换方法 | replace(old, new, max) | 将字符串中的 `old` 替换为 `new`,若指定 `max` 则替换不超过 `max` 次。| `'abcabc'.replace('a', 'x', 1)`|\n", + "| 分割方法 | split(str, num) | 以 `str` 为分隔符截取字符串,若指定 `num`,则仅截取 `num+1` 个子字符串。 | `'a,b,c'.split(',')` |\n", + "| | splitlines(keepends) | 按行分隔字符串,若 `keepends=True`,则保留换行符。 | `'a\\\\nb'.splitlines()` |\n", + "| 去空格方法 | strip(chars) | 截掉字符串两端的空格或指定字符。 | `' abc '.strip()` |\n", + "| | lstrip(chars) | 截掉字符串左边的空格或指定字符。 | `' abc'.lstrip()` |\n", + "| | rstrip(chars) | 截掉字符串右边的空格或指定字符。 | `'abc '.rstrip()` |\n", + "| 连接方法 | join(seq) | 以指定字符串作为分隔符,将 `seq` 中的元素合并为一个新的字符串。 | `','.join(['a', 'b', 'c'])` |\n", + "| 大小写方法 | lower() | 将字符串中的所有大写字符转换为小写。 | `'ABC'.lower()` |\n", + "| | upper() | 将字符串中的所有小写字母转换为大写。 | `'abc'.upper()` |\n", + "| | swapcase() | 将字符串中的大写转换为小写,小写转换为大写。 | `'AbC'.swapcase()` |\n", + "| 映射方法 | maketrans(x, y) | 创建字符映射表,`x` 表示需要转换的字符,`y` 表示转换的目标字符。 | `str.maketrans('a', 'b')` |\n", + "| | translate(table, deletechars) | 根据 `table` 转换字符串的字符,`deletechars` 指定要过滤掉的字符。 | `'abc'.translate({97: 98})` |\n", + "| 其他方法 | len(string) | 返回字符串的长度。 | `len('abc')` |\n", + "| | expandtabs(tabsize=8) | 将字符串中的 tab 符号替换为空格,`tabsize` 指定空格数,默认为 8。 | `'\\tabc'.expandtabs(4)` |\n", + "| | max(str) | 返回字符串中最大的字符。 | `max('abc')` |\n", + "| | min(str) | 返回字符串中最小的字符。 | `min('abc')` |\n", + "s.strip() 会去掉字符串两端所有“空白字符”(whitespace),包括:\n", + "空格 ' ' 制表符 '\\t' 换行 '\\n' 回车 '\\r' 以及其他空白字符\n", + "3.字符串中的常量\n", + "`string.ascii_letters`:包含所有英文字母(大小写)。\n", + "`string.digits`:包含所有数字字符(0-9)。\n", + "`string.punctuation`:包含所有标点符号。" + ] + }, + { + "cell_type": "markdown", + "id": "4d473164", + "metadata": {}, + "source": [ + "##### 列表([])\n", + "1.列表也可以被索引和截取,截取后返回一个新列表,也可以用+、*运算。\n", + "2.列表中的元素可以改变,字符串中的元素不可以改变。\n", + "3.字符串和元组都属于序列(Sequence)类型,如下图所示:\n", + "Python有6个序列的内置类型,最常见的是列表和元组。\n", + "```python\n", + "object\n", + " └── collections.abc.Iterable\n", + " └── collections.abc.Sequence\n", + " ├── list(可变序列)\n", + " ├── tuple(不可变序列)\n", + " └── str(不可变序列,但元素必须是字符)\n", + "\n", + "```\n", + "4.列表内的元素不需要具有相同的类型。\n", + "5.列表支持嵌套\n", + "5.列表内置的方法\n", + "列表的数据可以修改或更新:\n", + "新增:append()方法可以添加列表项;\n", + "删除:del语句可以删除列表中的指定元素,按照索引制定。\n", + "pop()\n", + "6.列表脚本运算符\n", + "和字符串一样,+ * in \n", + "\n", + "7.列表的函数\n", + "len()列表元素个数\n", + "max(list)、min(list)返回列表元素最大值、最小值\n", + "list(seq)将序列转化为列表\n", + "seq -- 元素列表,可以是列表、元组、集合、字典,若为字典,则仅会将键(key)作为元素依次添加至原列表的末尾。\n", + "\n", + "8.列表的方法\n", + "| 序号 | 语法 | 功能解释 |\n", + "|------|-----------------------------|---------------------------------------------|\n", + "| 1 | `list.append(obj)` | 在列表末尾添加新的对象 |\n", + "| 2 | `list.count(obj)` | 统计某个元素在列表中出现的次数 |\n", + "| 3 | `list.extend(seq)` | 在列表末尾一次性追加另一个序列中的多个值 |\n", + "| 4 | `list.index(obj)` | 从列表中找出某个值第一个匹配项的索引位置 |\n", + "| 5 | `list.insert(index, obj)` | 将对象插入列表,index是要插入对象的索引位置|\n", + "| 6 | `list.pop([index=-1])` | 移除列表中的一个元素(默认最后一个),并返回该元素 |\n", + "| 7 | `list.remove(obj)` | 移除列表中某个值的第一个匹配项 |\n", + "| 8 | `list.reverse()` | 反向列表中元素 |\n", + "| 9 | `list.sort(key=None, reverse=False)` | 对原列表进行排序 |\n", + "| 10 | `list.clear()` | 清空列表,类似于del a[:] |\n", + "| 11 | `list.copy()` | 复制列表,并返回复制后的新列表 |\n", + "\n", + "`list.sort(key=None, reverse=False)`单独解释:\n", + "(1)默认排序规则:默认升序(从小到大)`reverse=False`,`reverse=True`的时候是降序(从大到小)。字母以及特殊字符的降序规则基于字符的 ASCII/Unicode 值。例如升序情况下,a(ASCII值为97)大于A(ASCII值为65),A排在a之前\n", + "(2)key是一个函数,该函数会作用于可迭代对象的每个元素,返回一个值,再以这个值对可迭代对象中的元素进行排序。\n", + "按字符串长度排序:key=len\n", + "按绝对值排序:key=abs\n", + "忽略大小写排序:key=str.lower\n", + "按元组的某个元素排序:key=lambda x: x[1]\n", + "\n", + "9.列表的函数和方法的区别\n", + "| 区别 | 列表的函数 | 列表的方法 |\n", + "|--------|---------------------|--------------------------------|\n", + "| 定义 | 全局函数,作用于列表或其他可迭代对象。通常为Python的内置函数 | 列表对象的成员函数,仅作用于列表。 |\n", + "| 调用方式 | `函数(列表)` | `列表.方法()` |\n", + "| 作用范围 | 可以作用于其他可迭代对象(如元组、集合)。 | 仅适用于列表。 |\n", + "| 示例 | `len(lst)`、`max(lst)` | `lst.append(4)`、`lst.sort()` |\n", + "\n", + "10.嵌套列表\n", + "```python\n", + "matrix = [ # 3X4的矩阵列表\n", + "[1, 2, 3, 4],\n", + "[5, 6, 7, 8],\n", + "[9, 10, 11, 12],\n", + "]\n", + "# 3X4 转为 4X3:\n", + "m2 = [[row(i) for row in matrix] for i in range(4)]\n", + "# 或者\n", + "transposed = []\n", + "for i in range(4):\n", + " transpoed.append([row[i] for row in matrix]) \n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d37fae74", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]\n", + "[1, 2, 3, 4]\n", + "[5, 6, 7, 8]\n", + "[9, 10, 11, 12]\n", + "[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]\n" + ] + } + ], + "source": [ + "matrix = [\n", + "[1, 2, 3, 4],\n", + "[5, 6, 7, 8],\n", + "[9, 10, 11, 12],\n", + "]\n", + "print(matrix)\n", + "for row in matrix:\n", + " print(row)\n", + "m2 = [[row[i] for row in matrix] for i in range(4)]\n", + "print(m2)" + ] + }, + { + "cell_type": "markdown", + "id": "6663e0af", + "metadata": {}, + "source": [ + "##### 元组(())\n", + "1.元组不能修改(修改删除个别元素),元组内的元素类型可以不同。元组支持切片和索引。\n", + "2.元组可以包含list列表这种可变的对象。\n", + "3.特殊表示\n", + "`tup1 = () # 空元组`\n", + "`tup2 = (20,) # 一个元素,需要在元素后添加逗号,否则就会成为数学运算中的括号`\n", + "4.元组支持 + += * in 迭代\n", + "5.元组的内置函数与list基本相同,除了 tuple(iterable),该函数可将可迭代系列转换为元组" + ] + }, + { + "cell_type": "markdown", + "id": "2597e067", + "metadata": {}, + "source": [ + "##### 字典({})\n", + "1.列表是有序的对象集合,字典是无序的对象集合。两者之间的区别在于:字典当中的元素是通过键来存取的(所以键必须唯一),而不是通过偏移存取。\n", + "2.字典是一种映射类型,是一个无序的键(唯一key) : 值(value) 的集合。\n", + "```python\n", + "dict1 = {}\n", + "dict1['one'] = \"1 - 菜鸟教程\"\n", + "dict1[2] = \"2 - 菜鸟工具\"\n", + "tinydict = {'name': 'runoob','code':1, 'site': 'www.runoob.com'}\n", + "\n", + "print (dict['one']) # 输出键为 'one' 的值,'1 - 菜鸟教程'\n", + "print (dict[2]) # 输出键为 2 的值,'2 - 菜鸟工具'\n", + "print (tinydict) # 输出完整的字典,'{'name': 'runoob', 'code': 1, 'site': 'www.runoob.com'}'\n", + "print (tinydict.keys()) # 输出所有键,'dict_keys(['name', 'code', 'site'])'\n", + "print (tinydict.values()) # 输出所有值,'dict_values(['runoob', 1, 'www.runoob.com'])'\n", + "```\n", + "3.字典的构造函数dict()\n", + "```python\n", + "dict([('Runoob', 1), ('Google', 2), ('Taobao', 3)]) # 列表形式\n", + "{'Runoob': 1, 'Google': 2, 'Taobao': 3}\n", + ">>> {x: x**2 for x in (2, 4, 6)} # 字典推导式形式\n", + "{2: 4, 4: 16, 6: 36}\n", + ">>> dict(Runoob=1, Google=2, Taobao=3) # a=1形式\n", + "{'Runoob': 1, 'Google': 2, 'Taobao': 3}\n", + "```\n", + "4.字典内置函数?内置方法?\n", + "len(dict1) 返回字典的键值对个数\n", + "str(dict1) 返回字典的字符串表示\n", + "内置方法:\n", + "| 序号 | 语法 | 功能解释 |\n", + "|------|-------------------------------|--------------------------------------------------------------------------|\n", + "| 1 | `dict.clear()` | 删除字典内所有元素,删除后字典对象还在 |\n", + "| 2 | `dict.copy()` | 返回一个字典的浅复制 |\n", + "| 3 | `dict.fromkeys(seq, val)` | 创建一个新字典,以序列 `seq` 中元素做字典的键,`val` 为字典所有键对应的初始值 |\n", + "| 4 | `dict.get(key, default=None)` | 返回指定键的值,如果键不在字典中返回 `default` 设置的默认值 |\n", + "| 5 | `key in dict` | 如果键在字典 `dict` 里返回 `True`,否则返回 `False` |\n", + "| 6 | `dict.items()` | 以列表返回一个视图对象,包含字典的键值对 |\n", + "| 7 | `dict.keys()` | 返回一个视图对象,包含字典的所有键 |\n", + "| 8 | `dict.setdefault(key, default=None)` | 和 `get()` 类似,但如果键不存在于字典中,将会添加键并将值设为 `default` |\n", + "| 9 | `dict.update(dict2)` | 把字典 `dict2` 的键/值对更新到 `dict` 里 |\n", + "| 10 | `dict.values()` | 返回一个视图对象,包含字典的所有值 |\n", + "| 11 | `dict.pop(key[, default])` | 删除字典 `key` 所对应的值,返回被删除的值 |\n", + "| 12 | `dict.popitem()` | 返回并删除字典中的最后一对键和值 |\n", + "\n", + "dict.items()、dict.keys()、dict.values()返回的是一个视图对象,是什么意思?视图就是给个窗口看到当前字典的内容\n", + "好像生成的是一个元组?\n", + "\n", + "5.字典的key是什么类型?\n", + "key必须可哈希(不可变类型,如str、int、tuple)\n", + "6.删除字典元素\n", + "del tinydict['key1'] 删除指定的键值对\n", + "tinydict.clear() 清空字典\n", + "del tinydict 删除字典\n", + "\n", + "7.遍历技巧\n", + "(1)字典的关键字和对应值,可以使用items()方法同时解读出来\n", + "`for k,v in dict1.items()`\n", + "(2)序列中遍历时,索引位置和对应值可以用enumerate()函数同时得到\n", + "`for i,v in enumerate(list1)`,`print(i,v)`i,v分别是索引位置和对应值\n", + "(3)同时遍历多个序列,可以用zip组合\n", + "for q,a in zip(list1,list2)\n", + "(4)反向遍历一个序列,先制定这个序列,然后调用reversed()函数\n", + "`for i in reversed(list)`\n", + "要顺序遍历一个序列,可以用sorted,sorted(list),不会修改原值" + ] + }, + { + "cell_type": "markdown", + "id": "58078555", + "metadata": {}, + "source": [ + "##### 集合({})\n", + "1.集合(Set)是一种无序、可变的数据类型,用于存储唯一的元素。\n", + "2.集合可以进行集合操作(交集、并集、差集)\n", + "3.创建一个空集合必须用 set() 而不是 { },因为 { } 是用来创建一个空字典。\n", + "```python\n", + "sites = {'Google', 'Taobao', 'Runoob', 'Facebook', 'Zhihu', 'Baidu'}\n", + "# 也可以用site()创建 sites = srt(('Google', 'Taobao', 'Runoob', 'Facebook', 'Zhihu', 'Baidu'))\n", + "print(sites) # 输出集合,重复的元素被自动去掉\n", + "\n", + "# 成员测试\n", + "if 'Runoob' in sites :\n", + " print('Runoob 在集合中')\n", + "else :\n", + " print('Runoob 不在集合中')\n", + "\n", + "# set可以将其他类型转换为集合\n", + "a = set('abracadabra')\n", + "b = set('alacazam')\n", + "\n", + "print(a) # 重复元素会被去掉,{'b', 'c', 'r', 'a', 'd'}\n", + "print(a - b) # a 和 b 的差集,{'r', 'b', 'd'}\n", + "print(a | b) # a 和 b 的并集,{'b', 'c', 'a', 'z', 'm', 'r', 'l', 'd'}\n", + "print(a & b) # a 和 b 的交集,{'c', 'a'}\n", + "print(a ^ b) # a 和 b 中不同时存在的元素,{'z', 'b', 'm', 'r', 'l', 'd'}\n", + "```\n", + "4.集合的基本操作\n", + "\n", + "| 方法 | 描述 |\n", + "|-------------------------------|----------------------------------------------------------------------|\n", + "| `add()` | 为集合添加元素 |\n", + "| `clear()` | 移除集合中的所有元素 |\n", + "| `copy()` | 拷贝一个集合 |\n", + "| `difference()` | 返回多个集合的差集,例如x.difference(y),返回在x中,不在y中的元素 |\n", + "| `difference_update()` | 移除两个集合中都存在的元素,没有返回值 |\n", + "| `discard()` | 删除集合中指定的元素 |\n", + "| `intersection()` | 返回集合的交集,两个集合都有的元素 |\n", + "| `intersection_update()` | 返回集合的交集,并更新当前集合 |\n", + "| `isdisjoint()` | 判断两个集合是否包含相同的元素,如果没有返回 `True`,否则返回 `False` |\n", + "| `issubset()` | 判断是否为子集,x.issubset(y),x中的所有元素是否都在y中|\n", + "| `issuperset()` | 判断是否为父集,x.issuperset(y),y中的所有元素都在x中 |\n", + "| `pop()` | 随机移除元素 |\n", + "| `remove()` | 移除指定元素 |\n", + "| `symmetric_difference()` | 返回两个集合中不重复的元素集合 |\n", + "| `symmetric_difference_update()` | 移除当前集合中在另外一个指定集合相同的元素,并将另外一个指定集合中不同的元素插入到当前集合中 |\n", + "| `union()` | 返回两个集合的并集 |\n", + "| `update()` | 给集合添加元素 |\n", + "| `len()` | 计算集合元素个数 |" + ] + }, + { + "cell_type": "markdown", + "id": "537eaaee", + "metadata": {}, + "source": [ + "#### 推导式\n", + "推导式可以从一个数据序列构建另一个数据序列的结构体,用于生成列表、元组、字典、集合和生成器。\n", + "\n", + "1.列表推导式\n", + "[表达式 for 变量 in 列表 if 条件]\n", + "`[out_exp_res for out_exp in input_list if condition]`\n", + "out_exp_res:列表生成元素表达式,可以是有返回值的函数。\n", + "for out_exp in input_list:迭代 input_list 将 out_exp 传入到 out_exp_res 表达式中。\n", + "if condition:条件语句,可以过滤列表中不符合条件的值。\n", + "\n", + "集合推导式、元组推导式与列表推导式基本相同。\n", + "元组推导式返回的结果是一个生成器对象,要用tuple()函数转换为元组。\n", + "\n", + "也可以判断两个条件,结果2选1:\n", + "结果值1 if 判断条件 else 结果2 for 变量名 in 原列表\n", + "```python\n", + "list1 = ['python', 'test1', 'test2']\n", + "list2 = [word.title() if word.startswith('p') else word.upper() for word in list1]\n", + "print(list2) # 输出['Python', 'TEST1', 'TEST2']\n", + "```\n", + "\n", + "2.字典推导式\n", + "`{key_expr: value_expr for value in collection if condition}`\n", + "key_expr:每次迭代,生成一个字典的键\n", + "value_expr:每次迭代,生成一个字典的值\n", + "collection:可迭代对象\n", + "\n", + "如果字典的键和值分别来自两个可迭代对象,可以使用 zip() 函数(将两个可迭代对象一一配对,生成键值对)结合字典推导式来生成字典:\n", + "`{key: value for key, value in zip(keys_iterable, values_iterable)}`\n", + "备注:两个可迭代对象长度不同,zip()会以较短的可迭代对象为准。" + ] + }, + { + "cell_type": "markdown", + "id": "f72a7ee2", + "metadata": {}, + "source": [ + "#### 迭代器\n", + "1.迭代器是一个可以记住遍历的位置的对象。\n", + "2.迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。迭代器只能往前不会后退。详细解释:\n", + "(1)迭代器是一个惰性对象,它不会一次性生成所有元素,而是按需生成。\n", + "(2)每次调用 next() 或在 for 循环中迭代时,迭代器会返回下一个元素,并将内部指针移动到下一个位置。\n", + "(3)一旦某个元素被访问过,迭代器的指针就不会再回到之前的位置。\n", + "\n", + "3.迭代器有两个基本的方法:iter() 和 next()。\n", + "\n", + "4.字符串,列表或元组对象都可用于创建迭代器:\n", + "```python\n", + "list1 = [1, 2, 3, 4]\n", + "it = iter(list1) # 创建迭代器对象\n", + "print(type(it)) # 输出: \n", + "\n", + "print(it) # 输出迭代器对象的内存地址\n", + "print(next(it)) # 输出: 1,指针移动到下一个元素\n", + "print(next(it)) # 输出: 2,指针移动到下一个元素\n", + "print(next(it)) # 输出: 3,指针移动到下一个元素\n", + "\n", + "for x in it: # 从当前指针位置开始迭代\n", + " print('迭代:', x) # 输出: 迭代: 4\n", + "```\n", + "\n", + "5.把一个类作为一个迭代器使用需要在类中实现两个方法 __iter__() 与 __next__() 。\n", + "\n", + "6.StopIteration用于标识迭代的完成。\n" + ] + }, + { + "cell_type": "markdown", + "id": "0be4e849", + "metadata": {}, + "source": [ + "#### 生成器\n", + "使用了yield的函数称为生成器。\n" + ] + }, + { + "cell_type": "markdown", + "id": "cbf50712", + "metadata": {}, + "source": [ + "#### 函数\n", + "1.python 函数的参数传递:\n", + "不可变类型:类似 C++ 的值传递,如整数、字符串、元组。如 fun(a),传递的只是 a 的值,没有影响 a 对象本身。如果在 fun(a) 内部修改 a 的值,则是新生成一个 a 的对象。\n", + "可变类型:类似 C++ 的引用传递,如 列表,字典。如 fun(la),则是将 la 真正的传过去,修改后 fun 外部的 la 也会受影响\n", + "\n", + "2.函数的参数类型:\n", + "必需参数:\n", + "`def printme(str):pass` 必须以正确的顺序,传入相应类型、数量的参数\n", + "关键字参数:\n", + "`def printme(age,name):pass`,可以通过`printme(name='Allen',age=16)`,不按照指定顺序来调用 \n", + "默认参数:\n", + "`def printme(name,age = 35):pass`,35是age参数的默认值,没有传age参数就用默认值\n", + "不定长参数:\n", + "`def printinfo(arg1, *vartuple)`,*vartuple,arg1之外的参数,可以用元组的形式导入,存在所有未命名的变量参数\n", + "参数带一个*是元组,两个**是字典,例如`def printinfo(arg1, **vardict)`调用时,用`printinfo(arg1,a=1,b=2)`\n", + "*的其他作用,单独出现星号时,其后的参数必须用关键字传入,如`def f(a,b,*,c)`里面的c,必须用关键字c=1传入\n", + "Python3.8的强制位置参数,`def f(a, b, /, c, d, *, e, f)`,形参 a 和 b 必须使用指定位置参数,c 或 d 可以是位置形参或关键字形参,而 e 和 f 要求为关键字形参。\n", + "\n", + "3.匿名函数\n", + "匿名的含义:不再使用def标准形式来定义一个函数。匿名的另一个意思是,lambda函数没有函数名称,只能通过赋值给变量或参数。\n", + "用lambda表达式来创建匿名函数,语法为:`lambda [arg1 [,arg2,.....argn]]:expression`,`expression`用于计算并返回函数的结果\n", + "匿名函数可以封装在一个函数内,可以使用同样的代码来创建多个匿名函数。\n", + "lambda 函数拥有自己的命名空间,且不能访问自己参数列表之外或全局命名空间里的参数?lambda里面的x的范围仅限于lambda语句本身。\n", + "\n", + "lambda函数通常与map()、filter()、reduce()一起使用,便于在集合上操作,例如:\n", + "把列表的元素翻倍:`list(map(lambda x:x*2,list1))`\n", + "筛选列表中的偶数:`list(filter(lambda x:x%2==0,list1))`,filter筛选,返回True的元素才留下\n", + "计算一个序列的累积乘积:`reduce(lambda x,y:x*y,list1)`,reduce合并(先算1、2,然后用1和2的合并结果与3计算,以此类推),先导入functools模块才可以用。\n", + "\n", + "4.return语句\n", + "return表达式用于退出函数,return不带参数值,调用函数会返回None。" + ] + }, + { + "cell_type": "markdown", + "id": "387f8138", + "metadata": {}, + "source": [ + "#### 装饰器\n", + "含义:装饰器用于动态地修改函数或类的行为,一般用于日志记录、性能分析、权限控制、缓存\n", + "基本语法:\n", + "```python\n", + "# 装饰器本质上是一个接收函数作为输入并返回一个新的包装过后的函数的对象。\n", + "def decorator_function(original_function):\n", + " def wrapper(*args, **kwargs):\n", + " \n", + " before_call_code() # 这里是在调用原始函数前添加的新功能\n", + " \n", + " result = original_function(*args, **kwargs)\n", + " \n", + " after_call_code()# 这里是在调用原始函数后添加的新功能\n", + " \n", + " return result\n", + " return wrapper\n", + "\n", + "# 使用装饰器\n", + "@decorator_function\n", + "def target_function(arg1, arg2):\n", + " pass # 原始函数的实现\n", + "\n", + "'''\n", + "等价于 target_function = time_logger(target_function)\n", + "'''\n", + "```\n", + "解释:使用装饰器时,会将目标函数作为参数传递给装饰器函数,装饰器函数除了会原原本本输出原始函数的原有输出,还会在调用原始函数前、后增加一些功能。\n", + "其中的wrapper函数,是实际会被调用的函数,既包含了原始函数的调用,也在后面增加了额外的行为。\n", + "\n", + "内置装饰器:\n", + "@staticmethod: 将方法定义为静态方法,不需要实例化类即可调用。\n", + "(这个方法/函数和这个类有关,所以就放在里面,而且不用实例化具体对象后再用,方便管理)\n", + "Python默认放在类里面的函数,都是给“对象”用的!\n", + "\n", + "@classmethod: 将方法定义为类方法,第一个参数是类本身(通常命名为 cls)。即站爱\n", + "(当我想操作整个类,而不是某个对象时)\n", + "\n", + "@property: 将方法转换为属性,使其可以像属性一样访问。就是不用加括号了。\n", + "\n", + "##### 类装饰器(后续再看)\n", + "可以动态修改类的行为。它接收一个类作为参数,并返回一个新的类或修改后的类。" + ] + }, + { + "cell_type": "markdown", + "id": "052f3e5c", + "metadata": {}, + "source": [ + "#### Python数据结构\n", + "\n", + "1.栈\n", + "(1)栈是一种后进先出(LIFO, Last-In-First-Out)数据结构,意味着最后添加的元素最先被移除。\n", + "(2)可以用列表来实现栈的功能。\n", + "(3)栈操作:\n", + "压入(Push): 将一个元素添加到栈的顶端。list1.append(x)\n", + "弹出(Pop): 移除并返回栈顶元素。list1.pop()\n", + "查看栈顶元素(Peek/Top): 返回栈顶元素而不移除它。list1[-1]\n", + "检查是否为空(IsEmpty): 检查栈是否为空。判断len(list1)==0\n", + "获取栈的大小(Size): 获取栈中元素的数量。len(list1)\n", + "\n", + "2.队列\n", + "队列是一种先进先出(FIFO, First-In-First-Out)的数据结构,意味着最早添加的元素最先被移除。\n", + "使用列表时,如果频繁地在列表的开头插入或删除元素,性能会受到影响,因为这些操作的时间复杂度是 O(n)。为了解决这个问题,Python 提供了 collections.deque,它是双端队列,可以在两端高效地添加和删除元素。\n", + "```python\n", + "from collections import deque\n", + "\n", + "# 创建一个空队列\n", + "queue = deque()\n", + "\n", + "# 向队尾添加元素\n", + "queue.append('a')\n", + "queue.append('b')\n", + "queue.append('c')\n", + "\n", + "print(\"队列状态:\", queue) # 输出: 队列状态: deque(['a', 'b', 'c'])\n", + "\n", + "# 从队首移除元素\n", + "first_element = queue.popleft()\n", + "print(\"移除的元素:\", first_element) # 输出: 移除的元素: a\n", + "print(\"队列状态:\", queue) # 输出: 队列状态: deque(['b', 'c'])\n", + "\n", + "# 查看队首元素(不移除)\n", + "front_element = queue[0]\n", + "print(\"队首元素:\", front_element) # 输出: 队首元素: b\n", + "\n", + "# 检查队列是否为空\n", + "is_empty = len(queue) == 0\n", + "print(\"队列是否为空:\", is_empty) # 输出: 队列是否为空: False\n", + "\n", + "# 获取队列大小\n", + "size = len(queue)\n", + "print(\"队列大小:\", size) # 输出: 队列大小: 2\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "9f7a2be8", + "metadata": {}, + "source": [ + "#### 模块\n", + "1.模块用于长久保存方法及变量。\n", + "2.模块可以用于代码复用、命名空间管理、重新组织代码(使程序结构清晰)\n", + "\n", + "3.`import`用于引入模块:\n", + "一个模块只会被导入一次。\n", + "可以设置模块的别名,`import modname as md`\n", + "import语句执行时,Python的查找路径为sys.path,即为:\n", + "(1)当前目录。\n", + "(2)环境变量 PYTHONPATH 指定的目录。\n", + "(3)Python 标准库目录。\n", + "(4).pth 文件中指定的目录。\n", + "\n", + "4.模块里面除了方法定义,还可以包括可执行代码。\n", + "可执行代码在引入模块的时候(仅第一次模块被导入时)会执行。\n", + "\n", + "5.每个某块都有自己的符号表(即命名空间),即为模块内的函数、变量只属于这个模块,不会影响外面的世界。\n", + "\n", + "6.__name__属性:一个模块被另一个程序第一次引入时,其主程序(指的是顶层代码,即未被函数或类封装的代码)将运行。\n", + "如果不想模块内的主程序在模块被引入时自动执行,可以在模块内的主程序设置条件:`if __name__ == '__main__':主程序代码`\n", + "原理为:每个模块都有一个 __name__ 属性。\n", + "如果模块是被直接运行,__name__ 的值为 __main__。\n", + "如果模块是被导入的,__name__ 的值为模块名。\n", + "\n", + "7.dir()函数\n", + "内置函数,可以找到对应模块所有的名称,以字符串列表形式返回。\n", + "\n", + "8.包\n", + "包可以理解为包含很多某块的文件夹,目的是减少模块间的命名冲突,方便代码管理和后续维护。\n", + "导入包时,Python会根据sys.path中的目录来寻找这个包中包含的子目录。目录只有包含一个叫做 __init__.py 的文件才会被认作是一个包。\n", + "如果包定义文件 __init__.py 存在一个叫做 __all__ 的列表变量,那么在使用 from package import * 的时候就把这个列表中的所有名字作为包内容导入。\n", + "使用 from Package import specific_submodule 这种方法永远不会有错。" + ] + }, + { + "cell_type": "markdown", + "id": "31e01658", + "metadata": {}, + "source": [ + "#### 标准模块\n", + "1.模块(module)、包(package)、库(library)\n", + "模块(module)∈包(package)∈库(library)\n", + "模块一般指单个.py文件;包指的是多个.py文件组成的文件夹,一般包含__init__.py文件;库指的是为某类功能提供的一整套可复用代码集合,例如requests(HTTP库)和pandas(数据分析库)等\n", + "2.标准库指的是按照python时自带的,不用pip install,直接import就可以用。\n", + "3.常见标准模块\n", + "| 模块名 | 功能描述 | 常用函数/变量 | 举例说明 | 用法说明 |\n", + "|--------------|-----------------------------------|----------------------------------------|---------------------------------------|---------------------------------------|\n", + "| `math` | 数学运算(如平方根、三角函数等) | `sqrt`, `sin`, `cos`, `pi`, `factorial`| `import math; print(math.sqrt(16))` | `sqrt`: 计算平方根;`sin`, `cos`: 计算正弦、余弦;`pi`: 圆周率;`factorial`: 计算阶乘 |\n", + "| `os` | 操作系统相关功能(如文件、目录操作)| `listdir`, `mkdir`, `remove`, `getcwd` | `import os; print(os.getcwd())` | `listdir`: 列出目录内容;`mkdir`: 创建目录;`remove`: 删除文件;`getcwd`: 获取当前工作目录 |\n", + "| `sys` | 系统相关的参数和函数 | `argv`, `path`, `exit`, `platform` | `import sys; print(sys.platform)` | `argv`: 命令行参数列表;`path`: 模块搜索路径;`exit`: 退出程序;`platform`: 获取操作系统名称 |\n", + "| `random` | 生成随机数 | `randint`, `choice`, `shuffle`, `random`| `import random; print(random.randint(1, 10))` | `randint`: 生成指定范围内的随机整数;`choice`: 随机选择一个元素;`shuffle`: 打乱序列;`random`: 生成0到1之间的随机浮点数 |\n", + "| `datetime` | 处理日期和时间 | `datetime`, `date`, `timedelta`, `now` | `from datetime import datetime; print(datetime.now())` | `datetime`: 日期时间类;`date`: 日期类;`timedelta`: 时间差类;`now`: 获取当前时间 |\n", + "| `json` | 处理 JSON 数据 | `load`, `loads`, `dump`, `dumps` | `import json; print(json.dumps({'a': 1}))` | `load`: 从文件加载JSON;`loads`: 从字符串加载JSON;`dump`: 将JSON写入文件;`dumps`: 将对象转换为JSON字符串 |\n", + "| `re` | 正则表达式操作 | `match`, `search`, `findall`, `sub` | `import re; print(re.findall(r'\\\\d+', 'abc123'))` | `match`: 从字符串开头匹配;`search`: 搜索匹配;`findall`: 查找所有匹配;`sub`: 替换匹配内容 |\n", + "| `collections`| 提供额外的数据结构(如 defaultdict、deque)| `Counter`, `defaultdict`, `deque`, `OrderedDict` | `from collections import Counter; print(Counter('abcabc'))` | `Counter`: 统计元素出现次数;`defaultdict`: 带默认值的字典;`deque`: 双端队列;`OrderedDict`: 有序字典 |\n", + "| `itertools` | 提供迭代器工具 | `count`, `cycle`, `permutations`, `combinations` | `import itertools; print(list(itertools.permutations('abc', 2)))` | `count`: 无限计数器;`cycle`: 无限循环迭代;`permutations`: 排列;`combinations`: 组合 |\n", + "| `functools` | 高阶函数工具(如 reduce、lru_cache)| `reduce`, `lru_cache`, `partial`, `wraps` | `from functools import reduce; print(reduce(lambda x, y: x + y, [1, 2, 3]))` | `reduce`: 累积计算;`lru_cache`: 缓存函数结果;`partial`: 创建偏函数;`wraps`: 保留原函数元信息 |\n", + "\n", + "4.标准模块的使用举例(业务需求出发)\n", + "(1)创建\\查找文件\n", + "os.getcwd() 获取当前工作目录\n", + "os.listdif(指定目录) 列出目录下的文件(返回列表)\n", + "glob.glob('*.py') 查找当前工作目录下,所有以'.py结尾'的文件\n", + "(2)在命令行运行Python脚本时,脚本文件名及后面跟的一些“额外文字”,会被Python收集起来放到sys.argv里,方便后续在代码里面读取并决定程序行为。\n", + "(3)字符串匹配,例如匹配是否\n", + "```python\n", + "import re\n", + "re.findall(r'\\bf[a-z]*', 'which foot or hand fell fastest') # 输出['foot', 'fell', 'fastest']\n", + "# 匹配规则解释:\n", + "# r'\\bf[a-z]*':匹配所有“以 f 开头的单词”(只考虑小写字母)。\n", + "# \\b单词边界(word boundary)。要求匹配从一个“单词的开始位置”开始(前面不是单词字符,后面是单词字符)。\n", + "# f:字母 f。\n", + "# [a-z]*:后面跟着 0 个或多个小写字母(a 到 z)。\n", + "\n", + "re.sub(r'(\\b[a-z]+) \\1', r'\\1', 'cat in the the hat') # 输出 'cat in the hat'\n", + "# 匹配规则解释:\n", + "# r'(\\b[a-z]+) \\1':是在找 重复单词(比如 \"the the\"),并把它替换成一个。\n", + "'''\n", + "(\\b[a-z]+):第 1 个捕获组\n", + "\\b:单词边界(从单词开始)\n", + "[a-z]+:一个或多个小写字母\n", + "用括号 (...) 包住表示“把这一段记下来”,编号为组 1\n", + "\n", + "空格 ' ':匹配一个空格\n", + "\n", + "\\1:反向引用(backreference)\n", + "表示“必须再出现一次 和第 1 组完全相同的内容”\n", + "也就是:前面捕获到什么单词,后面必须重复同一个单词\n", + "\n", + "替换串:r'\\1'\n", + "表示用“捕获组 1 的内容”替换整个匹配结果\n", + "所以 \"the the\" 被替换为 \"the\"\n", + "最终得到:'cat in the hat'。\n", + "'''\n", + "```\n", + "(4)放回抽样random.choices()和不放回抽样random.sample()\n", + "(5)用于处理从 urls 接收的数据的 urllib.request\n", + "```python\n", + "import json # json模块提供json的序列化\n", + "from urllib.request import urlopen, Request\n", + "# urlopen 发送请求并返回响应对象(response)\n", + "# Request:用来构造 HTTP 请求对象(URL、方法、headers等)\n", + "url = \"https://httpbin.org/json\" # 要访问的url,示例接口,会返回 JSON\n", + "\n", + "# 构造一个请求对象,里面包含请求目标url和请求头headers\n", + "req = Request( \n", + " url,\n", + " # 请求头是客户端发给服务器的一些元信息\n", + " headers={\n", + " \"User-Agent\": \"Mozilla/5.0\" # 有些网站不加 UA 会拒绝\n", + " },\n", + ")\n", + "# Request(...)默认是GET请求,如果要用POST,会传data\n", + "\n", + "# 然后发起请求并拿到响应对象 resp,并确保用完自动关闭连接/资源。\n", + "with urlopen(req, timeout=10) as resp: # resp是一个类文件对象\n", + " raw = resp.read() # 读取响应体的全部内容,返回bytes(网络传输的“原始内容”是字节序列,即为bytes)。不带参数的.read()会读取到EOF(响应结束),适用于较小的效应。\n", + " text = raw.decode(\"utf-8\") # 把字节raw按照UTF-8编码规则,解码成字符串str(因为JSON文本是一种文本协议,本质上是字符串)\n", + " data = json.loads(text) # 把JSON字符串转换为Python对象,这里面是dict\n", + "\n", + "print(data.keys()) # 返回的是dict_keys视图对象(不是列表),但是可迭代,如果想看到列表形式,可以print(list(data.keys()))\n", + "```\n", + "\n", + "JSON 数据类型映射到 Python 规则:\n", + "JSON object(花括号 {})→ Python dict\n", + "JSON array(方括号 [])→ Python list\n", + "JSON string → Python str\n", + "JSON number → Python int 或 float\n", + "JSON true/false → Python True/False\n", + "JSON null → Python None\n", + "(6)用于发送电子邮件的 smtplib:\n", + "```python\n", + "import smtplib # 标准库smtplib 提供 SMTP 协议客户端,用于“把邮件提交给 SMTP 服务器”。\n", + "from email.mime.text import MIMEText # 导入MIMEText类,邮件不是随便字符串,要符合MIME格式,MIMEText 用于构造纯文本/HTML邮件的正文部分。\n", + "from email.header import Header # 邮件头(如 Subject)里出现中文时,需要按 RFC 规范编码;Header(\"中文\", \"utf-8\") 会生成正确的头部编码形式,避免乱码\n", + "\n", + "# 以下为SMTP的基本配置(服务器、端口、账号)\n", + "smtp_host = \"smtp.163.com\" # SMTP服务器地址\n", + "smtp_port = 465 # SSL 端口(常见)\n", + "sender = \"13427527534@163.com\" # 发件人的邮箱账号 \n", + "password = \"\" # 注意:通常不是登录密码,是“授权码”\n", + "receiver = \"547651488@qq.com\" # 收件人的邮箱账号\n", + "\n", + "# 以下构造邮件内容\n", + "msg = MIMEText(\"这是一封测试邮件\", \"plain\", \"utf-8\") \n", + "# plain内容类型是纯文本,若是HTML就用html,utf-8为正文编码\n", + "msg[\"From\"] = sender \n", + "msg[\"To\"] = receiver # 干啥用的?\n", + "# msg[\"From\"]和msg[\"To\"]用处主要是 “邮件内容层面的发件人\\收件人信息(展示/语义/规范)”,不是 SMTP 投递层面的发件人\\收件人列表。\n", + "msg[\"Subject\"] = Header(\"Python smtplib 测试\", \"utf-8\") # 邮件主题\\标题\n", + "\n", + "# 建立到SMTP服务的SSL连接,并绑定为server\n", + "with smtplib.SMTP_SSL(smtp_host, smtp_port) as server:\n", + " server.login(sender, password) # 登录SMTP服务器\n", + " server.sendmail(sender, [receiver], msg.as_string())\n", + " # sender,SMTP层面的发件人\n", + " # [receiver]为收件人列表\n", + " # msg.as_string()把MIME对象序列化为RFC格式的原始邮件文本\n", + "print(\"sent\")\n", + "```\n", + "\n", + "(7)测试,单元测试\n", + "```python\n", + "# 待测试代码文件,calc.py\n", + "def add(a,b):\n", + "\treturn a+b\n", + "def div(a,b):\n", + "\treturn a/b\n", + "```\n", + "```python\n", + "# 测试用代码,test_calc.py\n", + "import unittest # unittest是标准库的单元测试框架\n", + "import calc\n", + "\n", + "class TestCalc(unittest.TestCase): # 定义一个测试类,该类继承自unittest.TestCase\n", + "\t# unittest.TestCase提供了各种断言方法(assertEqual等)和测试运行机制\n", + "\n", + " # 以下定义测试用例(测试用例方法名以test_开头,unittest会自动发现所有以test_开头的方法并执行)\n", + " # 注意:每个测试方法都自动建立了一个TestCalc实例(就是self)\n", + " def test_add(self): # 这里面的self指的是什么?self指的是当前这个测试类的一个实例对象,有这个对象才能调用从父类继承的方法\n", + "\t\tself.assertEqual(calc.add(1,2),3) # 必须相等的断言\n", + "\tdef test_div(self):\n", + "\t\tself.assertAlmostEqual(calc.div(1,3),1/3) # 基本相等,一般到小数点几位之后的精度\n", + " \tdef test_div_zero(self):\n", + "\t\twith self.assertRaises(ZeroDivisionError):\n", + "\t\t\tcalc.div(1,0) # 期望该代码会抛出ZeroDivisionError,否则就报错\n", + "if __name__ == \"__main__\":\n", + "\tunittest.main()\n", + "```\n", + "\n", + "(8)日期和时间\n", + "```python\n", + "import datetime\n", + "# 获取日期\n", + "current_datetime = datetime.datetime.now()\n", + "print(current_datetime) # 输出 2026-01-04 17:25:57.469107\n", + "current_date = datetime.date.today()\n", + "print(current_date) # 输出 2026-01-04\n", + "formatted_datetime = current_datetime.strftime(\"%Y-%m-%d %H:%M:%S\")\n", + "print(formatted_datetime) # 输出 2026-01-04 17:25:57\n", + "# %Y %m %d %H %M %S 这些都代表啥?\n", + "\n", + "# 创建具体日期\n", + "birthday = datetime.date(1997,3,17)\n", + "print(birthday) # 输出 1997-03-17\n", + "```\n", + "\n", + "(9)数据压缩\n", + "模块直接支持通用的数据打包和压缩格式:zlib,gzip,bz2,zipfile,以及 tarfile。\n", + "**“压缩”**的核心意思是:把数据用某种算法重新编码,让它占用的字节数更少(节省磁盘/传输带宽)。\n", + "**“打包”**的核心意思是:把多个文件/目录按一种容器格式组织到一起(变成一个“包”)。\n", + "\n", + "(10)性能度量\n", + "```python\n", + "from timeit import Timer\n", + "print(Timer('t=a; a=b; b=t', 'a=1; b=2').timeit()) # 输出 0.016925900003116112\n", + "print(Timer('a,b = b,a', 'a=1; b=2').timeit())\n", + "# 输出 0.01278959999763174\n", + "```\n", + "\n", + "以上我们看到的只是 Python3 标准库中的一部分模块,还有很多其他模块可以在官方文档中查看完整的标准库文档:https://docs.python.org/zh-cn/3/library/index.html" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "959e1bc4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0.016925900003116112\n", + "0.01278959999763174\n" + ] + } + ], + "source": [ + "from timeit import Timer\n", + "print(Timer('t=a; a=b; b=t', 'a=1; b=2').timeit())\n", + "\n", + "print(Timer('a,b = b,a', 'a=1; b=2').timeit())" + ] + }, + { + "cell_type": "markdown", + "id": "e94367dc", + "metadata": {}, + "source": [ + "#### Python魔法方法\n", + "魔法方法也成双下划线方法。\n", + "本质:魔法方法不是给你“直接调用”的,而是让Python在“特定时机”自动调用的。\n", + "例如,执行`a+b`时,Python自动调用了`a.__add__(b)`\n", + "目的:为了让“自定义对象”表现得像内置类型一样自然,可以直接(+ - * /、用len()、for...in、print()、==等,普通方法无法做到)\n", + "注意事项:\n", + "(1)魔法方法是Python解释器和对象之间的“协议接口”,实现了这个接口,Python才知道什么时候能for、能+、能len、能print\n", + "(2)\n", + "分类(四大类魔法方法):\n", + "1.对象创建与生命周期\n", + "(1)__new__:负责创建对象,返回一个实例\n", + "例如`obj = MyClass()`,Python做的其实是`obj=MyClass.__new__(MyClass)`,参数为cls\n", + "(2)__init__:不创建对象,而是“配置已经创建好的对象”,初始化对象的属性,`MyClass.__init__(obj)`,参数为self\n", + "(3)__del__:在对象被垃圾回收前调用,资源释放一般用with\n", + "2.对象表现(打印/转字符串)\n", + "Python很多地方需要用字符串(print、日志等),所以对象必须能够变成字符串。\n", + "(1)__str__:给“人”看的字符串。\n", + "(2)__repr__:给“程序员/调试”看的字符串。定义对象“真实、准确、不模糊”的表示。\n", + "__str__与__repr__的区别:\n", + "pirnt(obj),优先用__str__;\n", + "如果没有__str__,就用__repr__\n", + "如都没有,输出<__main__.Class object at 0x...>\n", + "3.容器/迭代协议\n", + "容器可以看做是一组行为,一个对象支持这些操作之一就会像个容器:len、for x in obj、obj[i]、x in obj\n", + "__len__\n", + "__iter__\n", + "__next__\n", + "__getitem__\n", + "4.运算符与比较\n", + "__add__\n", + "__eq__\n", + "__lt__\n", + "...\n", + "\n", + "#### 魔法变量" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5f0d4d4f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "<__main__.User at 0x22ba7c7b4a0>" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "class User:\n", + " def __repr__(self):\n", + " return 'User(id=1)'\n", + "u = User()\n", + "u" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7cf54385", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2\n" + ] + } + ], + "source": [ + "class MyNumbers:\n", + " def __iter__(self):\n", + " self.a = 1\n", + " return self\n", + "\n", + " def __next__(self):\n", + " x = self.a\n", + " self.a += 1\n", + " return self.a\n", + "\n", + "myclass = MyNumbers()\n", + "myiter = iter(myclass) # iter()是一个内置函数,内置函数的工作规则:如果对象有__iter__方法,就用对象的方法;否则尝试__getitem__(0),__getitem__(1)\n", + "# 所以,这一步等价于 myclass.__iter__,用的是我自己定义的Mynumbers.__iter__\n", + "\n", + "print(next(myiter))\n", + "\n", + "# 我这样做的含义是什么,我重写了__iter__()和__next()方法吗? 那myiter = iter(myclass)这一步用的是哪个iter方法?" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "9c5a47ca", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'a': 1, 'b': 2, 'c': 3}\n", + "\n", + "('g', 's', 'i', 'n', 't')\n" + ] + } + ], + "source": [ + "keys = ['a','b','c']\n", + "values = [1,2,3]\n", + "dicts1 = {key:value for key,value in zip(keys,values)}\n", + "tup1 = tuple({x for x in 'sitting'})\n", + "print(dicts1)\n", + "print(type(dicts1))\n", + "print(tup1)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "6f292d99", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[2, 4, 6]\n" + ] + } + ], + "source": [ + "names = [x for x in range(1,7) if x%2 == 0 ]\n", + "print(names)" + ] + }, + { + "cell_type": "markdown", + "id": "a1e3ef90", + "metadata": {}, + "source": [ + "#### 条件控制\n", + "1.if语句\n", + "```python\n", + "if condition_1: # condition_1 为True时,执行block_1\n", + " statement_block_1\n", + "elif condition_2: # condition_1 为False时,condition_2 为 False时,执行block_2\n", + " statement_block_2\n", + "else:\n", + " statement_block_3 # 否则执行block_3\n", + "```\n", + "\n", + "2.match...case条件判断(Python3.10版后引入的)\n", + "支持匹配值(多个值用|)、匹配类型、匹配结构(list、dict、class)、匹配并绑定变量、支持守卫条件(case if ...)\n", + "(1)基础语法\n", + "```python\n", + "match expression:\n", + " case pattern1:\n", + " # 处理pattern1的逻辑\n", + " case pattern2 if condition:\n", + " # 处理pattern2并且满足condition的逻辑\n", + " case _:\n", + " # 处理其他情况的逻辑\n", + "```\n", + "(2)可以匹配并绑定变量\n", + "```python\n", + "def match_example(item):\n", + " match item:\n", + " case (x, y) if x == y:\n", + " print(f\"匹配到相等的元组: {item}\")\n", + " case (x, y):\n", + " print(f\"匹配到元组: {item}\")\n", + " case _:\n", + " print(\"匹配到其他情况\")\n", + "\n", + "match_example((1, 1)) # 输出: 匹配到相等的元组: (1, 1)\n", + "match_example((1, 2)) # 输出: 匹配到元组: (1, 2)\n", + "match_example(\"other\") # 输出: 匹配到其他情况\n", + "```\n", + "\n", + "```python\n", + "# 复杂的接口返回,想取得关联关系(relation)是什么\n", + "resp = {\n", + " 'code':200,\n", + " 'data':{\n", + " 'company':{\n", + " 'id':'A123',\n", + " 'relationship':'控股'\n", + " }\n", + " }\n", + "}\n", + "# 传统写法\n", + "if resp.get('code') == 200:\n", + " if 'company' in resp['data']:\n", + " relation = resp['data']['company']['relationship']\n", + "# match:\n", + "match resp:\n", + " case {'code':200,'data':{'company':{'id':'A123','relationship':rel}}}: # 匹配并绑定变量\n", + " print('关联关系',rel)\n", + "```\n", + "\n", + "(3)可以匹配类型:\n", + "```python\n", + "class Circle:\n", + " def __init__(self, radius):\n", + " self.radius = radius\n", + "\n", + "class Rectangle:\n", + " def __init__(self, width, height):\n", + " self.width = width\n", + " self.height = height\n", + "\n", + "def match_shape(shape):\n", + " match shape:\n", + " case Circle(radius=1):\n", + " print(\"匹配到半径为1的圆\")\n", + " case Rectangle(width=1, height=2):\n", + " print(\"匹配到宽度为1,高度为2的矩形\")\n", + " case _:\n", + " print(\"匹配到其他形状\")\n", + "\n", + "match_shape(Circle(radius=1)) # 输出: 匹配到半径为1的圆\n", + "match_shape(Rectangle(width=1, height=2)) # 输出: 匹配到宽度为1,高度为2的矩形\n", + "match_shape(\"other\") # 输出: 匹配到其他形状\n", + "```\n", + "\n", + "(4)匹配对象类型:检查对象是不是某种类型,类似于isinstance;\n", + "匹配类对象:不仅检查类型,还要检查对象内部属性结构,并允许提取(绑定)内部的值\n" + ] + }, + { + "cell_type": "markdown", + "id": "f646ca2a", + "metadata": {}, + "source": [ + "#### 循环语句\n", + "1.for循环,用于遍历任何可迭代对象\n", + "\n", + "2.while循环,Python中没有do while循环\n", + "简单循环:while True:print('a')\n", + "\n", + "3.break,跳出当前循环\n", + "\n", + "4.continue,跳出当前循环块中的剩余语句,进行下一次循环" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e1924434", + "metadata": {}, + "outputs": [], + "source": [ + "n = 5\n", + "while n > 0:\n", + " n -= 1\n", + " if n == 2:\n", + " continue\n", + " print(n)\n", + "print('end!')" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "id": "8a6179b9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "关联关系 控股\n", + "控股\n" + ] + } + ], + "source": [ + "resp = {\n", + " 'code':200,\n", + " 'data':{\n", + " 'company':{\n", + " 'id':'A123',\n", + " 'relationship':'控股'\n", + " }\n", + " }\n", + "}\n", + "# 传统写法\n", + "if resp.get('code') == 200:\n", + " if 'company' in resp['data']:\n", + " relation = resp['data']['company']['relationship']\n", + "# match:\n", + "match resp:\n", + " case {'code':200,'data':{'company':{'id':'A123','relationship':rel}}}:\n", + " print('关联关系',rel)\n", + " \n", + "\n", + "print(relation)" + ] + }, + { + "cell_type": "markdown", + "id": "429b4e33", + "metadata": {}, + "source": [ + "#### Python 直接赋值、浅拷贝和深度拷贝解析:\n", + "(1)直接赋值:其实就是对象的引用(别名)。\n", + "b=a,即a和b都指向同一个对象。\n", + "(2)浅拷贝(copy):拷贝父对象,不会拷贝对象的内部的子对象。\n", + "b=a.copy(),a和b指向不同的对象,但是他们的子对象指向的还是一个对象(是引用)。\n", + "(3)深拷贝(deepcopy):copy 模块的 deepcopy 方法,完全拷贝了父对象及其子对象。\n", + "b = copy.deepcopy(a),a和b是独立的对象,其中的子对象也是不同的对象。\n", + "```python\n", + "# 字典浅拷贝\n", + "a = {1:[1,2,3]}\n", + "b = a.copy() \n", + "print(a,b) # 输出 {1: [1, 2, 3]} {1: [1, 2, 3]}\n", + "a[1].append(4) # a的子对象和b的子对象是同一个\n", + "print(a,b) # 输出 {1: [1, 2, 3, 4]} {1: [1, 2, 3, 4]}\n", + "\n", + "# 深拷贝\n", + "import copy\n", + "c = copy.deepcopy(a)\n", + "print(a,c) # 输出 {1: [1, 2, 3, 4]} {1: [1, 2, 3, 4]}\n", + "a[1].append(5)\n", + "print(a,c) # 输出 {1: [1, 2, 3, 4, 5]} {1: [1, 2, 3, 4]}\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6e71df9a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{1: [1, 2, 3]} {1: [1, 2, 3]}\n", + "{1: [1, 2, 3, 4]} {1: [1, 2, 3, 4]}\n", + "{1: [1, 2, 3, 4]} {1: [1, 2, 3, 4]}\n", + "{1: [1, 2, 3, 4, 5]} {1: [1, 2, 3, 4]}\n" + ] + } + ], + "source": [ + "a = {1:[1,2,3]}\n", + "b = a.copy() \n", + "print(a,b) # 输出 {1: [1, 2, 3]} {1: [1, 2, 3]}\n", + "a[1].append(4) # a的子对象和b的子对象是同一个\n", + "print(a,b) # 输出 {1: [1, 2, 3, 4]} {1: [1, 2, 3, 4]}\n", + "# 深拷贝\n", + "import copy\n", + "c = copy.deepcopy(a)\n", + "print(a,c) # 输出 {1: [1, 2, 3, 4]} {1: [1, 2, 3, 4]}\n", + "a[1].append(5)\n", + "print(a,c) # 输出 {1: [1, 2, 3, 4, 5]} {1: [1, 2, 3, 4]}" + ] + }, + { + "cell_type": "markdown", + "id": "7480e222", + "metadata": {}, + "source": [ + "##### bytes类型\n", + "bytes类型表示不可变的二进制序列,其中的元素是整数值(0-255之间的整数)。\n", + "常用于处理二进制数据(图像、音频、视频),网络编程中也用来传输二进制数据。\n", + "bytes与字符串类型一样,也支持切片、拼接、查找、替换等。\n", + "可以使用`b`前缀,或者`bytes()`函数来创建bytes类型对象。\n", + "\n", + "```python\n", + "x = b\"hello\"\n", + "y = bytes('world',encoding='utf-8')\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "60672bf2", + "metadata": {}, + "source": [ + "\n", + "\n", + "##### bool(布尔类型)\n", + "bool是int的子类,`True==1,False==0,issubclass(bool,int)`都返回True。 \n", + "布尔值可以被看作整数来使用,True等价于1,False等价于0。\n", + "`bool()`函数可以将其他类型的值转为布尔值,下面的值会被转为False:\n", + "None、False、零 (0、0.0、0j)、空序列(如 ''、()、[])和空映射(如 {})。其余均为True。 \n", + "常用示例:\n", + "```python\n", + "print(True and False) # False\n", + "print(True or False) # True\n", + "print(not True) # False\n", + "print(not False) # True\n", + "\n", + "x = 10\n", + "if x:\n", + " print(\"x is non-zero and thus True in a boolean context\")\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "9cf899d0", + "metadata": {}, + "source": [ + "#### 数据类型转换\n", + "##### 隐式类型转换\n", + "一般是较低数据类型(如整数)转换为较高数据类型(如浮点数),以避免数据丢失。\n", + "较低:一般指的是表示范围小,表示精度低,内存占用较小,存储结构简单;较高反之。\n", + "特殊://的结果不一定是整数,例如`7.0//2`返回`3.0`。\n", + "##### 显式类型转换\n", + "将对象的数据类型做强制转换。\n", + "```python\n", + "# int():将一个字符串或者数字转换为整型\n", + "print(int('25',base = 10)) # base为进制,默认十进制,明确进制时只能转换字符串\n", + "print(int(2)) # 输出2\n", + "print(int(0b10010) )#输出18,默认输出10进制\n", + "\n", + "# float():将一个数或者字符串转换为浮点数\n", + "print(float(25)) # 输出25.0\n", + "print(float('280')) # 输出280.0\n", + "\n", + "# str():将对象转为字符串,即返回一个对象的string格式\n", + "dict3 = {'runoob': 'runoob.com', 'google': 'google.com'}\n", + "print(dict3) # 输出{'runoob': 'runoob.com', 'google': 'google.com'}\n", + "\n", + "# complex():创建一个复数\n", + "print(complex(1,2)) # 输出(1+2j)\n", + "print(complex(1)) # 输出(1+0j)\n", + "print(complex('1')) # 输出(1+0j),1当做实部处理\n", + "print(complex('1+2j')) # 输出(1+2j)\n", + "\n", + "\n", + "\n", + "# repr():将对象转换为供解释器读取的形式?\n", + "# 将读取到的格式字符,比如换行符、制表符,转化为其相应的转义字符\n", + "s=\"物品\\t单价\\t数量\\n包子\\t1\\t2\"\n", + "print(str(s))\n", + "print(repr(s)) # 转义字符变为正常的字符\n", + "\n", + "# eval():用于执行一个字符串表达式,并返回表达式的值(有风险)\n", + "print(eval('1+2+3')) # 输出6\n", + "\n", + "# tuple(s)、list(s):将可迭代系列s(列表)转换为元组、列表\n", + "list1 = ['Google', 'Taobao', 'Runoob', 2]\n", + "tuple1 = tuple(list1)\n", + "print(tuple1) # 输出('Google', 'Taobao', 'Runoob', 2)\n", + "\n", + "# set():将一个可迭代对象对象转换为集合,可以用于去重、关系测试\n", + "x = set('runoob')\n", + "print(x) # 输出{'n', 'u', 'b', 'r', 'o'}\n", + "\n", + "# frozenset():返回一个冻结的集合,该集合不能添加或删除任何元素。\n", + "a = frozenset(range(10))\n", + "print(a) # 输出 frozenset({0, 1, 2, 3, 4, 5, 6, 7, 8, 9})\n", + "b = frozenset('runoob')\n", + "print(b) # 输出 frozenset({'r', 'o', 'n', 'b', 'u'})\n", + "\n", + "# chr(x):将一个整数转换为一个ASCII字符,x可以是十进制也可以是十六进制。\n", + "print(chr(10)) # 输出的是换行符(\\n),相当于print('\\n')\n", + "print(repr(chr(10))) # 输出'\\n'\n", + "\n", + "# ord():将一个字符转换为它的整数值。\n", + "# ord() 函数是 chr() 函数(对于8位的ASCII字符串)或 unichr() 函数(对于Unicode对象)的配对函数。\n", + "print(ord('a')) # 输出97\n", + "print(ord('A')) # 输出65\n", + "print(ord('\\n')) # 输出10\n", + "\n", + "# hex():将一个十进制整数转换为十六进制字符串。\n", + "print(hex(255)) # 输出0xff\n", + "\n", + "# oct():将一个整数转换为八进制字符串。\n", + "print(oct(10)) # 输出0o12\n", + "```\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "9e9d4622", + "metadata": {}, + "source": [ + "dict()构造字典的本质是:能够传入键值对(key,value),就能生成字典。\n", + "传入键值对的方式是:\n", + "1.直接传入关键字:\n", + "```python\n", + "dict3 = dict(a='a',b='b',t='t') # 直接传入关键字构造\n", + "# 等价于 dict3 = (a='a',b='b',t='t')\n", + "print(dict3) # 输出{'a': 'a', 'b': 'b', 't': 't'}\n", + "```\n", + "\n", + "2.用可迭代对象,可迭代对象里面是(key,value)的二元序列即可\n", + "```python\n", + "dict4 = dict([('one',1),('two',2),('three',3)])\n", + "print(dict4) # {'one': 1, 'two': 2, 'three': 3}\n", + "```\n", + "可迭代对象是其他形式(元组、列表、生成器)也可以。\n", + "```python\n", + "dict4 = dict((['a',1],['b',2]))\n", + "dict4 = dict(((1,'x'x),(2,'y')))\n", + "dict4 = dict((f'key{i}',i) for i in range(3))\n", + "```\n", + "原因:dict()会遍历这个列表,每个元素都可以拆分成一个键和一个值。\n", + "\n", + "3.用zip,zip(key,values)会生成一堆(key,value)原则,符合dict需要的格式\n", + "\n", + "4.用映射类型(Mapping)构造。\n", + "映射类型指的是:能通过“键→值”的方式存储和访问数据的容器。\n", + "字典是最典型的映射类型,其他映射类型包括collections.OrderedDict(有序字典)等\n", + "所有映射类型都要能够实现__getitem__、__iter__、__len__\n", + "dict()函数可以接受任何Mapping类型,例如:\n", + "```python\n", + "mapping1 = {'a':1,'b':2}\n", + "d = dict(mapping1)\n", + "```\n" + ] + }, + { + "cell_type": "markdown", + "id": "890c875d", + "metadata": {}, + "source": [ + "### 输入和输出\n", + "1.Python两种输出值的方式: 表达式语句和 print() 函数。\n", + "第三种方式是使用文件对象的 write() 方法,标准输出文件可以用 sys.stdout 引用。\n", + "如果你希望输出的形式更加多样,可以使用 str.format() 函数来格式化输出值。\n", + "如果你希望将输出的值转成字符串,可以使用 repr() 或 str() 函数来实现。\n", + "str(): 函数返回一个用户易读的表达形式。\n", + "repr(): 产生一个解释器易读的表达形式。\n", + "str.format() 的基本使用如下:\n", + "`print('{}网址: \"{}!\"'.format('菜鸟教程', 'www.runoob.com'))`,输出`菜鸟教程网址: \"www.runoob.com!\"`\n", + "{}内可以设置关键字,或者序号\n", + "\n", + "2.input() 内置函数从标准输入读入一行文本,默认的标准输入是键盘。\n", + "\n", + "3.读和写文件\n", + "(1)open()将会返回一个file对象,`f = open(filename, mode)`\n", + "filename指的是文件名字符串,含文件路径;\n", + "mode是打开文件的模式,具体如下:\n", + "| 模式 | 读 | 写 | 创建 | 覆盖 | 指针在开始 | 指针在结尾 |\n", + "|---|---|---|---|---|---|---|\n", + "| `r` | + | | | | + | |\n", + "| `r+` | + | + | | | + | |\n", + "| `w` | | + | + | + | + | |\n", + "| `w+` | + | + | + | + | + | |\n", + "| `a` | | + | + | | | + |\n", + "| `a+` | + | + | + | | | + |\n", + "一般用r代表只读,w用于写入(存在同名文件会被删除),用a来追加内容\n", + "还有一个常用参数是encoding,指的是以什么编码规则打开文件。\n", + "(2)f.readline()从文件中读取单独的一行(单独一行指的是读取到下一个换行符'\\n'之前的内容),返回一个字符串。当返回空字符串时,说明已经读取到最后一行,这时候如果想从文件开头重新读,那么可以使用f.seek(0)。\n", + "f.readlines()返回该文件中包含的所有行。\n", + "f.readline()和f.readlines()都是从文件对象的当前指针位置开始读取。\n", + "也可以用for line in f:print(line,end='')。\n", + "(3)f.write()将字符串(不是字符串要转为字符串)写到文件中,然后返回写入的字符串数(\\n换行符算一个字符数量)。\n", + "(4)为什么最后一定要f.close():目的是为了让操作系统知道,这个文件关闭了,不然这个文件一直都显示被占用状态,即无法被删除或重命名。\n", + "也是为了确保数据真的写入磁盘,而非仅保存在缓冲区(有的OS在缓冲区慢、或者执行.flush()、.close()、或程序正常结束时才会写入磁盘)\n", + "文件句柄一直被占用?文件句柄指的是,操作系统给程序的一张“文件使用许可证编号”,程序想读文件时,要先向操作系统申请,取得一个整数,这个整数代表对应资源。\n", + "可以用with关键字打开:在with代码块结束后,会自动关闭文件。\n", + "(5)f.tell()可以确认文件当前读写的位置文件指针的位置,返回一个整数,这个整数代表从文件开头字节数的偏移量。\n", + "(6)f.seek()可以用于改变文件指针的位置,如f.seek(0)用于返回文件开头。\n", + "f.seek(offset,whence),offset表示相对于whence参数的偏移量,whence的默认值为0,表示开头;值为1,表示当前位置;值为2,表示文件的结尾\n", + "seek(x,0) :从起始位置即文件首行首字符开始移动 x 个字符,seek(0)中的0是偏移量为0,默认的whence值为0,所以是从文件开头移动0个位置,即为指针回到文件开头\n", + "seek(x,1) :表示从当前位置往后移动x个字符\n", + "seek(-x,2):表示从文件的结尾往前移动x个字符\n", + "(7)file对象常用函数\n", + "| 序号 | 方法 | 描述 |\n", + "|---:|---|---|\n", + "| 1 | `file.close()` | 关闭文件。关闭后文件不能再进行读写操作。 |\n", + "| 2 | `file.flush()` | 刷新文件内部缓冲,立即把缓冲区数据写入文件(而不是等待缓冲区自动写入)。 |\n", + "| 3 | `file.fileno()` | 返回整型文件描述符(FD),可用于 `os.read` 等底层操作。 |\n", + "| 4 | `file.isatty()` | 如果文件连接到终端设备返回 `True`,否则返回 `False`。 |\n", + "| 5 | `file.next()` | Python 3 的文件对象不支持 `next()` 方法;读取下一行应使用内置 `next(file)` 或迭代文件对象。 |\n", + "| 6 | `file.read([size])` | 用于从文件读取指定的字符数(文本模式 t)或字节数(二进制模式 b),如果未给定参数 size 或 size 为负数则读取文件所有内容。 |\n", + "| 7 | `file.readline([size])` | 读取一整行(包含 `\\n`)。可指定最多读取的字节数。 |\n", + "| 8 | `file.readlines([sizeint])` | 读取所有行并返回列表;若 `sizeint>0`,读取“约” `sizeint` 字节的行(可能略大,因为要填充缓冲区)。 |\n", + "| 9 | `file.seek(offset[, whence])` | 移动文件读写指针到指定位置。 |\n", + "| 10 | `file.tell()` | 返回文件当前位置(位置标记)。 |\n", + "| 11 | `file.truncate([size])` | 截断文件为 `size` 个字符/字节(未提供则从当前位置截断);截断后后续内容被删除。 |\n", + "| 12 | `file.write(str)` | 写入字符串,返回写入的字符数。 |\n", + "| 13 | `file.writelines(sequence)` | 写入一个字符串序列;不会自动添加换行符,需要自行在元素中加 `\\n`。 |\n", + "\n", + "4.pickle模块\n", + "用途:实现基本的数据序列化(dump\\dumps)和反序列化(load\\loads),序列化操作的含义指的是将程序中运行的对象信息保存到文件中,永久存储;反序列化操作指的是能够从文件中创建上一次程序保存的对象。\n", + "通俗的讲就是为了永久保存对象,但是对象在内存中结构复杂,dump可以把对象转换为一串字符串保存(不是简单的文本输出),而且还可以直接从文件恢复这个对象。\n", + "```python\n", + "# 存入对象\n", + "import pickle\n", + "data = {\n", + " 'name':'Tome',\n", + " 'age':18,\n", + " 'scores':[90,95,88]\n", + "}\n", + "\n", + "with open('data.pkl','wb') as f: # wb 指的是二进制写,该文件原有内容会被删除\n", + " pickle.dump(data,f) # dump(需要保存的对象,文件对象)\n", + "\n", + "# 读取对象\n", + "with open('data.pkl','rb') as f: # rb 指的是二进制打开一个文件并设置只读\n", + " data2 = pickle.load(f)\n", + "print(data2)\n", + "print(type(data2))\n", + "```\n" + ] + }, + { + "cell_type": "markdown", + "id": "bd7e776b", + "metadata": {}, + "source": [ + "### OS模块\n", + "可以通过OS模块来执行 跨平台的 文件操作、目录操作、环境变量管理、进程管理等任务。\n", + "常用操作包含目录操作:\n", + "(1)获取当前工作目录:os.getcwd()\n", + "(2)改变当前工作目录:os.chdir(path)\n", + "(3)列出目录内容:os.listdir(path)\n", + "(4)创建目录:os.mkdir(path)\n", + "(5)删除目录:os.rmdir(path),目录不为空时,会抛出异常\n", + "(6)删除文件:os.remove(path)用于删除一个文件,文件不存在会异常\n", + "(7)重命名文件或目录:os.rename(src,dst),src为原始路径,dst是新的路径。\n", + "(8)获取环境变量:os.getenv(key)用于获取指定的环境变量。\n", + "(9)执行系统命令:用于在操作系统的shell中执行命令,命令执行后,返回命令的退出状态。" + ] + }, + { + "cell_type": "markdown", + "id": "6fab01dc", + "metadata": {}, + "source": [ + "### 错误和异常\n", + "1.语法错误(解析错)\n", + "2.异常\n", + "(1)含义:运行期间检测到的错误\n", + "(2)类型:异常有不同的类型\n", + "(3)异常处理:\n", + "try/except语法格式:\n", + "```python\n", + "try:\n", + " #执行的代码,发生异常时剩下的部分将被忽略\n", + "except 异常类型:\n", + " #异常类型相符时,发生异常时执行的代码,没有异常时忽略这段\n", + "else: #可选项,在try子句没有发生任何异常时执行,比直接和try子句放在一起好一些\n", + "finally:#不管有没有异常都会执行的代码\n", + "```\n", + "内层异常接不住,就传递给上层的try,如果一直没有任何外层捕获,异常最终就会冒泡到最外层,程序报 Traceback 并终止。\n", + "可以有多个except子句,一个子句也可以有多个异常,这些异常被放在一个括号里面成为一个元组,例如`except (RuntimeError, TypeError, NameError)`\n", + "最后一个except子句可以忽略异常的名称,它将被当作通配符使用,即为匹配所有异常。你可以使用这种方法打印一个错误信息,然后再次把异常抛出。\n", + "```python\n", + "import sys\n", + "try:\n", + " f = open('myfile.txt')\n", + " s = f.readline()\n", + " i = int(s.strip())\n", + "except OSError as err:\n", + " print(\"OS error: {0}\".format(err))\n", + "except ValueError:\n", + " print(\"Could not convert data to an integer.\")\n", + "except:\n", + " print(\"Unexpected error:\", sys.exc_info()[0])\n", + " raise # 将当前捕获到的异常,原样重新抛出\n", + "```\n", + "sys.exc_info()是一个三元组:(异常类型,异常对象,traceback)\n", + "\n", + "raise语句常用于抛出一个指定的异常,主动抛出。\n", + "raise(没有参数)的作用是,将当前捕获到的异常,原样重新抛出。意思是这个except只是记录一下新的异常类型,不吞掉错误。\n", + "\n", + "可以通过继承Exception类来自定义异常。\n", + "\n", + "3.assert断言\n", + "用于判断一个表达式,表达式为false时,触发异常。\n", + "`assert expression`等价于`if not expression:raise AssertionError`\n", + "\n", + "# 常见错误类型\n", + "UnboundLocalError:你在函数的局部作用域里引用了一个局部变量,但这个局部变量在引用前还没有被赋值(绑定值)。" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "44b882dd", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "执行到了下一步\n", + "Could not convert data to an integer.\n" + ] + } + ], + "source": [ + "import sys\n", + "try:\n", + " f = open('myfile.txt')\n", + " print('执行到了下一步')\n", + " s = f.readline()\n", + " i = int(s.strip())\n", + "except OSError as err:\n", + " print(\"OS error: {0}\".format(err))\n", + "except ValueError:\n", + " print(\"Could not convert data to an integer.\")\n", + "except:\n", + " print(\"Unexpected error:\", sys.exc_info()[0])\n", + " raise" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "f946ed11", + "metadata": {}, + "outputs": [], + "source": [ + "while True:\n", + " try:\n", + " x = input('请输入一个数字:')\n", + " break\n", + " except ValueError:\n", + " print('您输入的不是数字,请再次尝试输入!')" + ] + }, + { + "cell_type": "markdown", + "id": "580ec0e6", + "metadata": {}, + "source": [ + "#### 字符、字节与编码规则\n", + "1.字符是人看到的符号,字节是计算机实际存的长度,UTF-8编码规则下:\n", + "英文为1字节,常见中文字符为3字节,Emoji字符为4字节。\n", + "\n", + "2.编码规则是字符和字节之间的桥梁,常见的编码规则有UTF-8、UTF-16、GBK:\n", + "(1)UTF-8最常用,变长编码,英文省空间,向下兼容ASCII;\n", + "(2)UTF-16以2字节为基础,内部处理友好;\n", + "(3)GBK中文优先的本地编码,适合中文环境。\n" + ] + }, + { + "cell_type": "markdown", + "id": "fddce543", + "metadata": {}, + "source": [ + "#### with\n", + "with实际上封装了 try…except…finally 编码范式,便于使用。\n", + "实现原理:建立在上下文管理器之上。\n", + "上下文管理器是一个实现__enter__和__exit__方法的类,使用with语句会在嵌套模块末尾调用__exit__方法。\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "2c895b89", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "旅游计划城市: ['0# 北京', '1# 上海', '2# 广州', '3# 深圳', '4# 成都', '5# 杭州', '6# 西安', '7# 重庆', '8# 南京', '9# 苏州']\n", + "请输入有效的整数。\n", + "请输入有效的整数\n", + "最后的旅游城市为: ['8# 南京', '6# 西安', '3# 深圳', '9# 苏州', '0# 北京']\n" + ] + } + ], + "source": [ + "import random\n", + "import sys\n", + "try:\n", + " with open('5A.txt','r') as fp:\n", + " i = [line for line in fp if line[0].isdigit()]\n", + "except FileNotFoundError:\n", + " print(r\"文件'5A.txt'未找到,请检查文件路径\")\n", + " sys.exit()\n", + "\n", + "travelList = []\n", + "\n", + "# 读取城市列表\n", + "for index,item in enumerate(i):\n", + " temp_1 = item.strip()[item.index('.')+1:]\n", + " temp_1 = f\"{index}#{temp_1}\"\n", + " travelList.append(temp_1)\n", + "\n", + "print(\"旅游计划城市:\",travelList)\n", + "\n", + "# 删除旅游城市\n", + "city_num = input('输入不想旅游城市的个数:')\n", + "try:\n", + " for _ in range(int(city_num)):\n", + " index = int(input('输入不想旅游城市的序号(第1个城市索引为0)'))\n", + " if 0 <= index < len(travelList):\n", + " travelList.pop(index)\n", + " print(\"旅游计划城市:\",travelList)\n", + " else:\n", + " print(\"输入的序号超出范围。\")\n", + "except ValueError:\n", + " print(\"请输入有效的整数。\")\n", + "\n", + "# 修改旅游城市\n", + "city_num = input(\"请输入想修改计划旅游的城市个数:\")\n", + "try:\n", + " for _ in range(int(city_num)):\n", + " index = int(input(\"请输入想要修改计划旅游城市的序号:\"))\n", + " if 0 <= index < len(travelList):\n", + " new_name = input(f\"原名称为{travelList[index]},请输入要修改城市的名称:\")\n", + " travelList[index] = new_name\n", + " print(\"旅游计划城市:\",travelList)\n", + " else:\n", + " print(\"输入的序号超出范围!\")\n", + "except ValueError:\n", + " print(\"请输入有效的整数\")\n", + "\n", + "\n", + "random.shuffle(travelList)\n", + "print(\"最后的旅游城市为:\",travelList[:5])" + ] + }, + { + "cell_type": "markdown", + "id": "2c04f3f1", + "metadata": {}, + "source": [ + "### Python3 面向对象\n", + "1.类的构造方法(__init__)不是创建对 象的方法,而是初始化对象的方法,如果不主动写,Python也会进行自动调用一个默认的__init__。创造对象的方法是(__new__)。\n", + "使用场景:有需求时写,例如想要在对象生成时候就有相应的属性,事后加属性不是很规范;例如想强制传参,即不给特定的参数就不能创建这个对象。\n", + "\n", + "2.self代表类的实例(对象)本身。\n", + "\n", + "3.类的属性与方法\n", + "私有的含义,就是在类的外部不能直接访问或者使用。\n", + "(1)类的私有属性\n", + "在类里定义的、以双下划线开头(但不以双下划线结尾)的属性,比如 __name、__age。\n", + "它的核心作用是:让类外部“不能直接用原名字访问”这个属性,从而降低被误用/误改的风险(更接近“封装”的效果)。\n", + "实现机制:通过名称改写来实现,例如self.__name 在类 Student 内部会被解释器改写成:self._Student__name。所以类内部访问self.__name没问题,类外面写obj.__name就会报AttributeError\n", + "要访问的话,需要obj._Student__name\n", + "(2)类的方法\n", + "def来定义类的方法,类方法的参数必带self,self代表类的实例。\n", + "如果类中定义的函数,第一个参数不是self,那就不是实例方法。\n", + "(3)类的私有方法\n", + "__private_method:两个下划线开头,声明该方法为私有方法,只能在类的内部调用 ,不能在类的外部调用。self.__private_methods。\n", + "(4)类的专有方法:\n", + "类的专有方法:\n", + "__init__ : 构造函数,在生成对象时调用\n", + "__del__ : 析构函数,释放对象时使用\n", + "__repr__ : 打印,转换\n", + "__setitem__ : 按照索引赋值\n", + "__getitem__: 按照索引获取值\n", + "__len__: 获得长度\n", + "__cmp__: 比较运算\n", + "__call__: 函数调用\n", + "__add__: 加运算\n", + "__sub__: 减运算\n", + "__mul__: 乘运算\n", + "__truediv__: 除运算\n", + "__mod__: 求余运算\n", + "__pow__: 乘方\n", + "\n", + "为什么要有专用方法:普通方法只能 obj.method() 这样调用;\n", + "但如果你想让自定义对象也能:print(obj)、len(obj)、obj1 + obj2、obj[i]、for x in obj、obj == other 这些“语法级”的行为就需要通过专有方法来完成。\n", + "4.继承\n", + "多继承:继承多个父类\n", + "继承的方法,子类继承父类的方法。\n", + "\n", + "5.子类继承父类构造函数\n", + "情况一:子类需要自动调用父类的方法:子类不重写__init__()方法,实例化子类后,会自动调用父类的__init__()的方法。 这种情况在实例化子类对象的时候,需要传入父类__init__()方法所需要的参数。\n", + "情况二:子类不需要自动调用父类的方法:子类重写__init__()方法,实例化子类后,将不会自动调用父类的__init__()的方法。\n", + "情况三:子类重写__init__()方法又需要调用父类的方法:使用super关键词:\n", + "```python\n", + "super(子类,self).__init__(参数1,参数2,....)\n", + "class Son(Father):\n", + " def __init__(self, name): \n", + " super(Son, self).__init__(name)\n", + "```\n", + "\n", + "6.super()函数用于调用父类的方法\n", + "```python\n", + "class FooParent(object):\n", + " def __init__(self):\n", + " self.parent = 'I\\' the parent.'\n", + " print('Parent')\n", + " def bar(self,message):\n", + " print(f'{message} from Parent.')\n", + "\n", + "class FooChild(FooParent):\n", + " def __init__(self):\n", + " super().__init__() # 这一步不需要再传self\n", + " # super()已经知道当前的实例是谁,会自动把self传给父类方法\n", + " print('Child')\n", + "\n", + " def bar(self,message):\n", + " super().bar(message)\n", + " print('Child bar function')\n", + " print(self.parent)\n", + "\n", + "fooChild = FooChild()\n", + "fooChild.bar('Hello Wrold!')\n", + "```\n", + "\n", + "易错点:什么时候要self?\n", + "1.self有什么用?\n", + "self的作用:指代当前对象;存数据到对象里面;让不同对象互不干扰。\n", + "理解:`a = [1,2,3]` `a.append(4)`执行append的时候,实际上做的是`list.append(a,4)`,有了a,Python就知道往哪个对象里面添加值。这个a是被自动塞入的,在方法定义里面,就叫self。\n", + "\n", + "```python\n", + "# 有了self会怎么样?\n", + "class MyList:\n", + " def append(self,value):\n", + " pass\n", + "x = MyList()\n", + "x.append(10) # 我写的是这样,Python实际执行的是MyList.append(x,10),x被当成第一个参数自动传入了\n", + "\n", + "# 不用self,会怎么样?\n", + "class BadList:\n", + " def append(value):\n", + " pass\n", + "a = BadList()\n", + "b = BadList()\n", + "a.append(1)\n", + "b.append(2)\n", + "# Python区分不了,1和2到底加给谁,BadList(?,1)\n", + "```\n", + "需要记忆的点:self就是当前对象本身、调用对象.方法()时,Python会自动把对象当做self传进去。\n", + "\n", + "2.调用父类方法的时候,要不要self\n", + "调用父类方法时,不要self,原因在于:\n", + "super()会自动把self传给父类方法,如果再传就会产生2个self。\n", + "\n", + "如何理解 super()会自动把self传给父类方法 ?\n", + "super不是“父类”,不是单纯的简化父类名称,而是已绑定当前实例的、按照MRO规则定位到的下一个类的方法代理对象。\n", + "用处:多继承的情况下,一定要用super(),此时还是靠父类.方法的话,会破坏多继承的关系。\n", + "什么是MRO规则?Python在调用方法时,查找类的固定顺序表。\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "375c789c", + "metadata": {}, + "source": [ + "### 命名空间\n", + "原因:为了解决项目中命名冲突的问题\n", + "含义:命名空间(Namespace)是从名称到对象的映射,大部分的命名空间都是通过 Python 字典来实现的。\n", + "特点:同个命名空间内不能有重名,各个命名空间相互独立,没有关系。\n", + "\n", + "命名空间的种类及查找顺序:函数或类的局部命名空间→模块中的全局命名空间→Python语言内置的名称(如abs、char和异常名称等)\n", + "嵌套函数中外部函数的作用域属于一个非局部、非全局的作用域。\n", + "\n", + "命名空间的生命周期:取决于对象的作用域,如果对象执行完成,则该命名空间的生命周期就结束。\n", + "\n", + "局部变量的优先级高于全局变量。\n", + "当内部作用域想修改外部作用域的变量时,可以用global关键字和nonlocal关键字。其中global影响的是全局,nonlocal影响的是外层非全局作用域。\n", + "```python\n", + "num = 1\n", + "def fun1():\n", + " global num # 让程序不用创建局部变量,而是使用全局变量。\n", + " # 并非声明一个内部变量(nonlocal功能类似)\n", + " print(num) # 输出 1\n", + " num = 123\n", + " print(num) # 输出 123\n", + "fun1()\n", + "print(num) # 输出 123\n", + "```\n", + "\n", + "易错点:只要一个变量名在函数体内出现了赋值语句,Python 就会把它判定为该函数的“局部变量”(除非你显式声明 global num 或 nonlocal num)。以下代码会有错误:\n", + "```python\n", + "num = 1\n", + "def fun1():\n", + " num1 = num\n", + " print(num1) \n", + " num = 123 \n", + " print(num) \n", + "fun1()\n", + "print(num) \n", + "\n", + "# 函数体里出现对 num 的赋值,就会让 num 在该函数里变成局部变量\n", + "# 所以在调用num1 = num的时候,这句里面的num是局部变量,但是没有赋值,所以就会报错UnboundLocalError(先读后赋值)。\n", + "```\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "682ef5d8", + "metadata": {}, + "source": [ + "### Python虚拟环境创建\n", + "含义:独立运行的Python环境,允许在同一台机器上为不同的项目创建隔离的Python环境。每个虚拟环境都有自己的Python解释器、安装的包/库、环境变量。\n", + "作用:创建虚拟环境,实现项目隔离(不同项目需要不同版本的Python或第三方库)、避免冲突(防止全局Python环境被污染)、依赖管理(方便记录和分享项目的依赖关系)、安全测试项目新包\n", + "创建虚拟环境:\n", + "python -m venv .venv\n", + "\n", + "激活(PowerShell):\n", + ".\\.venv\\Scripts\\Activate.ps1\n", + "\n", + "激活(CMD):\n", + ".\\.venv\\Scripts\\activate.bat\n", + "\n", + "退出虚拟环境:\n", + "deactivate\n", + "\n", + "看当前 python/pip 是否在虚拟环境里:\n", + "where python\n", + "where pip\n", + "python -c \"import sys; print(sys.executable)\"\n", + "pip -V\n", + "\n", + "安装 / 卸载 / 查看包:\n", + "pip install requests\n", + "pip uninstall requests\n", + "pip list\n", + "pip show requests\n", + "\n", + "固化依赖 / 按依赖安装:\n", + "pip freeze > requirements.txt\n", + "pip install -r requirements.txt\n", + "\n", + "升级 pip(建议用这种写法)\n", + "python -m pip install --upgrade pip" + ] + }, + { + "cell_type": "markdown", + "id": "f0f47880", + "metadata": {}, + "source": [ + "### 类型注解\n", + "1.变量注解:\n", + "```python\n", + "name: str = \"Alice\" # 注解为字符串 (str)\n", + "age: int = 30 # 注解为整数 (int)\n", + "is_student: bool = False # 注解为布尔值 (bool)\n", + "scores: list = [95, 88, 91] # 注解为列表 (list)\n", + "```\n", + "2.函数注解:\n", + "```python\n", + "# 有类型注解的函数\n", + "def greet(first_name: str, last_name: str) -> str:\n", + "# first_name、last_name参数类型应为字符串\n", + "# ->str,这个函数执行后会返回一个字符串\n", + " full_name = first_name + \" \" + last_name\n", + " return \"Hello, \" + full_name\n", + "```\n", + "3.复杂类型注解\n", + "(1)容器类型\n", + "```python\n", + "from typing import List, Dict, Tuple, Set\n", + "# List[int] 表示这是一个只包含整数的列表\n", + "numbers: List[int] = [1, 2, 3, 4, 5]\n", + "# Dict[str, int] 表示这是一个键为字符串、值为整数的字典\n", + "student_scores: Dict[str, int] = {\"Alice\": 95, \"Bob\": 88}\n", + "# Tuple[int, str, bool] 表示这是一个包含整数、字符串、布尔值的元组\n", + "person_info: Tuple[int, str, bool] = (25, \"Alice\", True)\n", + "# Set[str] 表示这是一个只包含字符串的集合\n", + "unique_names: Set[str] = {\"Alice\", \"Bob\", \"Charlie\"}\n", + "```\n", + "(2)可选类型\n", + "```python\n", + "from typing import Optional\n", + "def find_student(name: str) -> Optional[str]:\n", + " \"\"\"根据名字查找学生,可能找到也可能返回None\"\"\"\n", + " students = {\"Alice\": \"A001\", \"Bob\": \"B002\"}\n", + " return students.get(name) # 可能返回字符串或None\n", + "# 等价于 Union[str, None]\n", + "# Optional表示,可能是某种类型,或者是None\n", + "```\n", + "(3)联合类型(Union)\n", + "可能是多种类型之一。\n", + "`def process_input(data: Union[str, int, List[int]]) -> None:`\n", + "可能是字符串、整数或者整数列表的输入\n", + "\n", + "可以使用Mypy进行静态类型检查,pip install mypy" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3837d1cb", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Parent\n", + "Child\n", + "Hello Wrold! from Parent.\n", + "Child bar function\n", + "I' the parent.\n" + ] + } + ], + "source": [ + "class FooParent(object):\n", + " def __init__(self):\n", + " self.parent = 'I\\' the parent.'\n", + " print('Parent')\n", + " def bar(self,message):\n", + " print(f'{message} from Parent.')\n", + "\n", + "class FooChild(FooParent):\n", + " def __init__(self):\n", + " super().__init__() # 这一步不需要再传self\n", + " # super()已经知道当前的实例是谁,会自动把self传给父类方法\n", + " print('Child')\n", + "\n", + " def bar(self,message):\n", + " super().bar(message)\n", + " print('Child bar function')\n", + " print(self.parent)\n", + "\n", + "fooChild = FooChild()\n", + "fooChild.bar('Hello Wrold!')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "edeac874", + "metadata": {}, + "outputs": [ + { + "ename": "TypeError", + "evalue": "FooParent.__init__() takes 1 positional argument but 2 were given", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[1;32mIn[11], line 18\u001b[0m\n\u001b[0;32m 15\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mChild bar function\u001b[39m\u001b[38;5;124m'\u001b[39m)\n\u001b[0;32m 16\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mparent)\n\u001b[1;32m---> 18\u001b[0m fooChild \u001b[38;5;241m=\u001b[39m FooChild()\n\u001b[0;32m 19\u001b[0m fooChild\u001b[38;5;241m.\u001b[39mbar(\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mHello Wrold!\u001b[39m\u001b[38;5;124m'\u001b[39m)\n", + "Cell \u001b[1;32mIn[11], line 10\u001b[0m, in \u001b[0;36mFooChild.__init__\u001b[1;34m(self)\u001b[0m\n\u001b[0;32m 9\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m__init__\u001b[39m(\u001b[38;5;28mself\u001b[39m):\n\u001b[1;32m---> 10\u001b[0m \u001b[38;5;28msuper\u001b[39m()\u001b[38;5;241m.\u001b[39m\u001b[38;5;21m__init__\u001b[39m(\u001b[38;5;28mself\u001b[39m)\n\u001b[0;32m 11\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mChild\u001b[39m\u001b[38;5;124m'\u001b[39m)\n", + "\u001b[1;31mTypeError\u001b[0m: FooParent.__init__() takes 1 positional argument but 2 were given" + ] + } + ], + "source": [ + "class FooParent(object):\n", + " def __init__(self):\n", + " self.parent = 'I\\' the parent.'\n", + " print('Parent')\n", + " def bar(self,message):\n", + " print(f'{message} from Parent.')\n", + "\n", + "class FooChild(FooParent):\n", + " def __init__(self):\n", + " super().__init__(self)\n", + " print('Child')\n", + "\n", + " def bar(self,message):\n", + " super().bar(self,message)\n", + " print('Child bar function')\n", + " print(self.parent)\n", + "\n", + "fooChild = FooChild()\n", + "fooChild.bar('Hello Wrold!')" + ] + }, + { + "cell_type": "markdown", + "id": "4b514b92", + "metadata": {}, + "source": [ + "### 函数\n", + "静态方法\n", + "### 模块\n", + "Python中的模块是独立的命名空间,模块中的函数和变量不会自动导入到全局命名空间。\n", + "模块,例如random模块,本身可以看做一个静态对象,可以通过random.choice()来调用random模块中的choice()函数,这种调用方式可以避免命名冲突。\n", + "\n", + "1.静态对象\n", + "(1)含义:静态对象是指在程序运行期间,其状态或行为不会发生改变,或者不需要实例化就可以直接使用的对象。\n", + "(2)常见类型:模块、类的静态成员、不可变对象\n", + "(3)应用场景:静态对象无需实例化即可使用,而动态对象是需要实例化之后才可以使用的对象。而且静态对象创建后不可变。\n", + "\n", + "类的方法\n" + ] + }, + { + "cell_type": "markdown", + "id": "32f5df29", + "metadata": {}, + "source": [ + "### 其他知识点\n", + "1.在 Jupyter Notebook 中执行过的变量会一直保留在当前 Kernel 的内存里 \n", + "可以运行`%whos`(显示变量名、类型、值)、`%who`(只显示变量名)、`dir()`(会把魔法变量也列出来)。 \n", + "可以用`del`语句,删除不想要的变量;或者重启Jupyter Kernel(最彻底)。 \n", + "原因:因为 Jupyter Notebook 的执行模式是 stateful(有状态): \n", + "前面 cell 定义的变量,后面 cell 可以继续用,就算你删除 cell,变量仍然保留,只有重启 Kernel 才会清掉所有变量。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ac6d4683", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b1d24a5a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Variable Type Data/Info\n", + "-----------------------------\n", + "dict2 dict n=0\n", + "dict3 dict n=2\n", + "list1 list n=4\n", + "s str 物品\t单价\t数量\\n包子\t1\t2\n", + "tuple1 tuple n=4\n", + "x set {'n', 'u', 'b', 'r', 'o'}\n" + ] + } + ], + "source": [ + "%whos" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0bd8ee68", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Runoo\n", + "R\n", + "noo\n", + "noob\n", + "Ru\n", + "boonuR\n", + "boonuR\n" + ] + } + ], + "source": [ + "str = 'Runoob'\n", + "print(str[0:-1]) # 'Runoob'\n", + "print(str[0]) # 'R'\n", + "print(str[2:5]) # 'noo'\n", + "print(str[2:]) # 'noob'\n", + "print(str[:2]) # 'Ru'\n", + "print(str[::-1]) # 'boonuR',默认步长为1,步长为-1时会反转\n", + "print(str[-1::-1]) # 'boonuR',默认步长为1,步长为-1时会反转" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3ecd9167", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'Runoob': 1, 'Google': 2, 'Taobao': 3}\n", + "{2: 4, 4: 16, 6: 36}\n", + "{'Runoob': 1, 'Google': 2, 'Taobao': 3}\n" + ] + } + ], + "source": [ + "del dict\n", + "\n", + "dict1 = dict([('Runoob', 1), ('Google', 2), ('Taobao', 3)])\n", + "print(dict1) # {'Runoob': 1, 'Google': 2, 'Taobao': 3}\n", + "dict2 = {x: x**2 for x in (2, 4, 6)}\n", + "print(dict2)# {2: 4, 4: 16, 6: 36}\n", + "dict3 = dict(Runoob=1, Google=2, Taobao=3)\n", + "print(dict3) # {'Runoob': 1, 'Google': 2, 'Taobao': 3}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "040e17dd", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'Zhihu', 'Facebook', 'Runoob', 'Taobao', 'Baidu', 'Google'}\n", + "Runoob 在集合中\n", + "{'b', 'c', 'r', 'a', 'd'}\n", + "{'b', 'd', 'r'}\n", + "{'m', 'b', 'c', 'r', 'a', 'd', 'z', 'l'}\n", + "{'a', 'c'}\n", + "{'m', 'b', 'z', 'l', 'r', 'd'}\n" + ] + } + ], + "source": [ + "sites = {'Google', 'Taobao', 'Runoob', 'Facebook', 'Zhihu', 'Baidu'}\n", + "print(sites) # 输出集合,重复的元素被自动去掉\n", + "\n", + "# 成员测试\n", + "if 'Runoob' in sites :\n", + " print('Runoob 在集合中')\n", + "else :\n", + " print('Runoob 不在集合中')\n", + "\n", + "# set可以进行集合运算\n", + "a = set('abracadabra')\n", + "b = set('alacazam')\n", + "\n", + "print(a)\n", + "print(a - b) # a 和 b 的差集\n", + "print(a | b) # a 和 b 的并集\n", + "print(a & b) # a 和 b 的交集\n", + "print(a ^ b) # a 和 b 中不同时存在的元素" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bc91bb2b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "True\n", + "False\n", + "True\n", + "True\n", + "True\n" + ] + } + ], + "source": [ + "class A:pass\n", + "class B(A):pass\n", + "\n", + "b = B()\n", + "print(type(b) is B) # True\n", + "print(type(b) is A) # False\n", + "print(isinstance(b,B)) # True\n", + "print(isinstance(b,A)) # True\n", + "print(issubclass(B,A)) # True" + ] + }, + { + "cell_type": "markdown", + "id": "73c336d4", + "metadata": {}, + "source": [ + "### Python错误和警告\n", + "1.`print(1 is True)`会有警告:\n", + "SyntaxWarning:\"is\" with 'int' literal. Did you mean \"==\"?\n", + "2.TypeError: 'dict' object is not callable\n", + "是因为先前的代码把dict类型覆盖成了一个字典对象,导致dict变为一个普通的对象,故报错不可调用。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0ae936f2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "人会说话\n", + "Allen is learnning 英语\n", + "\n", + "\n", + "False\n", + "True\n", + "True\n", + "False\n", + "False\n" + ] + } + ], + "source": [ + "class Person:\n", + "\n", + " def __init__(self,name,age):\n", + " self.name = name\n", + " self.age = age\n", + " \n", + " def speak(self):\n", + " print(\"人会说话\")\n", + "\n", + "class Student(Person):\n", + "\n", + " def __init__(self,name,age):\n", + " super().__init__(name,age)\n", + " \n", + " def learn(self,course):\n", + " print(f'{self.name} is learnning {course}')\n", + "\n", + "stu1 = Student('Allen',26)\n", + "stu1.speak()\n", + "stu1.learn('英语')\n", + "\n", + "print(type(stu1))\n", + "print(type(Student))\n", + "print(type(stu1) is Person)\n", + "print(isinstance(stu1,Person))\n", + "print(isinstance(stu1,Student))\n", + "print(isinstance(Student,Person))\n", + "print(isinstance(bool,int))" + ] + }, + { + "cell_type": "markdown", + "id": "bc2e5920", + "metadata": {}, + "source": [ + "- 运算符(优先级从高到低)\n", + " - 比较运算符:> >= < <= == !=\n", + " - 赋值运算符:= += *=\n", + " - 逻辑预算符:and or not\n", + " - 数学运算符: + - * / % // **" + ] + }, + { + "cell_type": "markdown", + "id": "1779142d", + "metadata": {}, + "source": [ + "> 字符串属于数据还是数据结构 \n", + "> 数字属于数据还是数据结构 \n", + "> 数据和数据结构有什么关系" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1181ca0f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "hello\n" + ] + } + ], + "source": [ + "a =10\n", + "a = 'hello'\n", + "print(a)" + ] + }, + { + "cell_type": "markdown", + "id": "b30f7a39", + "metadata": {}, + "source": [ + "### 变量\n", + "- 变量含义\n", + " Python中的变量代表内存中的一个位置?\n", + "- 变量命名规则 \n", + " - Python变量允许以数字+字符(理解为英文字母)+符号,如涉及多个单词之间建议用下划线 \n", + " - 常量为大写\n", + "- 变量类型(需要整合为表格)\n", + " - 可变类型\n", + " - 整数(int)\n", + " - 浮点数(float)\n", + " - 列表(list)\n", + " - \n", + " - 不可变类型\n", + " - 字符串(str)\n", + " - 元组(tuple)\n", + " - 字典(dict)\n", + " - 变量类型的转换\n", + " - 类型转换函数(应该是区分了强制转换和自动转换?)\n", + " - 转换为整数 `int(x)`,转换时可带进制\n", + " - 转换为浮点数`float(x)`\n", + " - 转换为字符串`str(x)`\n", + " - 转换为列表`list(x)`\n", + " - 转换为元组`tuple(x)`\n", + " - 转换为字典?\n", + " - 自动转换\n", + " - 带有浮点数的数学运算的结果,均为float类型 2/1 = 2.0\n", + " - \n", + "- 变量的声明\n", + " - 元组的声明,`tup1 = (1,)`,必须带括号,否则仅为仅为用于确定运算顺序的括号\n", + " - 字典的声明,\n", + " " + ] + }, + { + "cell_type": "markdown", + "id": "ec409422", + "metadata": {}, + "source": [ + "### 基本数据类型\n", + "数据类型对应的方法\n", + "string.split()?" + ] + }, + { + "cell_type": "markdown", + "id": "bcbe2001", + "metadata": {}, + "source": [ + "### 分支循环结构\n", + "- 分支结构 \n", + " if elif else" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c7644079", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "成绩优秀\n" + ] + } + ], + "source": [ + "a = 100\n", + "if a>90:\n", + " print('成绩优秀')\n", + "elif a>60:\n", + " print('成绩合格')\n", + "else:\n", + " print('成绩不合格')" + ] + }, + { + "cell_type": "markdown", + "id": "202e48e0", + "metadata": {}, + "source": [ + "- 循环结构 \n", + " while循环\n", + " for循环\n", + " range函数的问题" + ] + }, + { + "cell_type": "markdown", + "id": "a573f908", + "metadata": {}, + "source": [ + "### 函数\n", + "- 函数的定义\n", + " Python内的函数为\n", + "- 内置函数\n", + "- 装饰器\n", + "- 递归调用" + ] + }, + { + "cell_type": "markdown", + "id": "ff4a1328", + "metadata": {}, + "source": [ + "### 模块\n", + "- 标准库?" + ] + }, + { + "cell_type": "markdown", + "id": "6392017b", + "metadata": {}, + "source": [ + "### 面向对象\n", + "#### 面向对象的含义 \n", + "面向对象编程是一种编程范式(即程序设计的方法论)。\n", + "在面向对象编程的世界里,程序中的**数据和操作数据的函数是一个逻辑上的整体**,称之为**对象**。 \n", + "> 还有指令式变成、函数式编程等编程范式。\n", + "\n", + "对象可以接受消息,解决问题的方法就是创建对象并向对象发出各种各样的消息;通过消息传递,程序中的多个对象可以协同工作,这样就能构造出复杂的系统并解决现实中的问题。\n", + "> 面向对象编程:把一组数据和处理数据的方法组成**对象**,把行为相同的对象归纳为**类**,通过**封装**隐藏对象的内部细节,通过**继承**实现类的特化和泛化,通过**多态**实现基于对象类型的动态分派。\n", + "> - 封装:隐藏内部实现细节,往外暴露简单调用接口。例如在类中定义对象的方法就是封装,给对象发一个消息(即不用明确具体实现)就可以执行方法中的代码。\n", + "#### 面向对象编程三步走\n", + "第一步:定义类 \n", + "定义类就是要找到对象的公共属性或者方法 \n", + "第二步:创建对象 \n", + "第三步:给对象发消息 \n", + "Python内置的list、set、dict都是提前定义好的类,可以跳过第一步; \n", + "如果第一步、第二部都提前走了,就是某些内置对象,可以“开箱即用”。" + ] + }, + { + "cell_type": "markdown", + "id": "cd6c2b68", + "metadata": {}, + "source": [ + "#### 类和对象\n", + "含义:类是对象的蓝图和模板(例如人类),对象是类的实例(例如具体的人),是可以接受消息的实体。 \n", + "> 一切皆为对象,对象都有属性和行为,每个对象都是独一无二的,而且对象一定属于某个类。 \n", + "> 接收消息是什么意思?\n", + "\n", + "属性:属性是对象的静态特征,行为(方法)是对象的动态特征。具有共同特征的对象和属性和行为抽取出来就可以定义一个类。" + ] + }, + { + "cell_type": "markdown", + "id": "cb0d8728", + "metadata": {}, + "source": [ + "类的定义和使用:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "04fdc6f2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "<__main__.Student object at 0x0000024596C1A2D0>\n", + "<__main__.Student object at 0x00000245968E7D40>\n", + "0x24596c1a2d0 0x245968e7d40\n", + "王克章正在学习Python程序设计.\n", + "王大锤学生正在玩游戏.\n", + "王克章正在学习Python程序设计.\n", + "王大锤学生正在玩游戏.\n" + ] + } + ], + "source": [ + "### 类的创建 ###\n", + "class Student:\n", + "\n", + " def __init__(self,name,age): # 初始化方法,即常见对象时,\n", + " #首先会在内存中获取对象的所需的内存空间,然后会自动执行该方法,完成对内存的初始化操作\n", + " self.name = name # 对象属性赋初始化值\n", + " self.age = age # = 右侧的age是传入的参数\n", + "\n", + " def study(self,course_name):\n", + " print(f'{self.name}正在学习{course_name}.')\n", + "\n", + " def play(self):\n", + " print(f'{self.name}学生正在玩游戏.')\n", + "\n", + "### 类的使用 ### \n", + "stu1 = Student('王克章',28) # 构造器语法创建类的对象,此时会传入类的初始化参数\n", + "stu2 = Student('王大锤',25)\n", + "print(stu1) \n", + "# 输出 <__main__.Student object at 0x000002459692B2C0>\n", + "# 上面这个输出代表是一个Student类的对象,以及这个对象的以十六进制表示的内存地址\n", + "# __main__表示Student类定义在主模块中(当前脚本)\n", + "print(stu2) \n", + "# 输出 <__main__.Student object at 0x000002459692B2C0>\n", + "print(hex(id(stu1)),hex(id(stu2)))\n", + "# 函数hex()将整数转为十六进制字符串\n", + "# 函数id()返回对象在内存中的唯一标识(一般是内存地址),该标识在对象的全生命周期内保持一致\n", + "stu3 = stu2 # 并没有创建新的对象\n", + "# 变量存的是一个对象在内存中的逻辑地址(位置)\n", + "\n", + "### 调用对象的方法 ###\n", + "Student.study(stu1,'Python程序设计')# 类.方法(接收消息的对象,方法入参) 来调用\n", + "Student.play(stu2)\n", + "stu1.study('Python程序设计')# 对象.方法(仅为方法入参)来调用\n", + "stu2.play()" + ] + }, + { + "cell_type": "markdown", + "id": "52825732", + "metadata": {}, + "source": [ + "> 写在类里面的函数我们通常称之为方法,方法就是对象的行为,也就是对象可以接收的消息。方法的第一个参数通常都是self,它代表了接收这个消息的对象本身。 \n", + "> 这个self怎么理解?\n", + "> 是不是类中定义的所有函数的第一个参数都要是self?" + ] + }, + { + "cell_type": "markdown", + "id": "aa224ddb", + "metadata": {}, + "source": [ + "#### 面向对象案例" + ] + }, + { + "cell_type": "markdown", + "id": "b339fc8b", + "metadata": {}, + "source": [ + "##### 例子1:时钟\n", + "> 要求:定义一个类描述数字时钟,提供走字和显示时间的功能。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4922cdf1", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "12:26:34\n", + "12:26:35\n", + "12:26:36\n", + "12:26:37\n" + ] + }, + { + "ename": "KeyboardInterrupt", + "evalue": "", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)", + "Cell \u001b[1;32mIn[5], line 25\u001b[0m\n\u001b[0;32m 23\u001b[0m \u001b[38;5;28;01mwhile\u001b[39;00m \u001b[38;5;28;01mTrue\u001b[39;00m:\n\u001b[0;32m 24\u001b[0m \u001b[38;5;28mprint\u001b[39m(clock1\u001b[38;5;241m.\u001b[39mshow())\n\u001b[1;32m---> 25\u001b[0m time\u001b[38;5;241m.\u001b[39msleep(\u001b[38;5;241m1\u001b[39m)\n\u001b[0;32m 26\u001b[0m clock1\u001b[38;5;241m.\u001b[39mrun()\n", + "\u001b[1;31mKeyboardInterrupt\u001b[0m: " + ] + } + ], + "source": [ + "import time\n", + "\n", + "class Clock: # 定义时钟类\n", + " def __init__(self,hour=0,minute=0,second=0):\n", + " self.hour = hour\n", + " self.min = minute\n", + " self.sec = second\n", + " \n", + " def run(self): # 走字\n", + " self.sec += 1\n", + " if self.sec == 60:\n", + " self.sec = 0\n", + " self.min += 1\n", + " if self.min == 60:\n", + " self.min = 0\n", + " self.hour += 1\n", + " if self.hour == 24:\n", + " self.hour = 0\n", + " def show(self):\n", + " return f'{self.hour:>2d}:{self.min:0>2d}:{self.sec:>02d}'\n", + "\n", + "clock1 = Clock(12,26) # 创建时钟对象\n", + "while True:\n", + " print(clock1.show())\n", + " time.sleep(1)\n", + " clock1.run() " + ] + }, + { + "cell_type": "markdown", + "id": "c22975b2", + "metadata": {}, + "source": [ + "##### 例2:平面上的点\n", + "> 要求:定义一个类描述平面上的点,提供计算到另一个点距离的方法。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dca8b607", + "metadata": {}, + "outputs": [], + "source": [ + "class Point:\n", + " \"\"\"平面上的点\"\"\"\n", + "\n", + " def __init__(self,x=0,y=0):\n", + " self.x,self.y = x,y\n", + " \n", + " def distance_to(self,other):# other表示另一个对象\n", + " dx = self.x - other.x\n", + " dy = self.y - other.y\n", + " return (dx*dx + dy*dy)**0.5\n", + " \n", + " def __str__(self): # 这个是什么意思?\n", + " # 可以理解为对象的字符串表示形式,就是调整print(对象)显示的内容\n", + " return f'({self.x},{self.y})'\n", + "\n", + "p1 = Point(3,5)\n", + "p2 = Point(6,9)\n", + "print(p1)\n", + "print(p2)\n", + "print(p1.distance_to(p2))\n" + ] + }, + { + "cell_type": "markdown", + "id": "c5cdb3b0", + "metadata": {}, + "source": [ + "#### 可见性和属性装饰器" + ] + }, + { + "cell_type": "markdown", + "id": "207cad3b", + "metadata": {}, + "source": [ + "对象的属性被设置成私有(private,属性前加__)或受保护(protected,属性前加_),以避免外界直接访问这些属性。\n", + "> 为什么要不允许外界直接访问?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a32d115d", + "metadata": {}, + "outputs": [], + "source": [ + "class Student:\n", + " __slots__ = ('name','age')\n", + " def __init__(self,name,age):\n", + " self.__name = name\n", + " self.__age = age\n", + " def study(self,course_name):\n", + " print(f'{self.__name}正在学习{course_name}.')\n", + "stu = Student('王大锤',20)\n", + "stu.sex = '男' # 给stu这个对象动态添加sex属性\n", + "print(stu.sex)\n", + "stu.study('Python程序设计') # 可以间接访问,不能直接访问\n", + "print(stu.__name) # 报错'Student' object has no attribute '__name'" + ] + }, + { + "cell_type": "markdown", + "id": "82abe644", + "metadata": {}, + "source": [ + "#### 动态属性\n", + "Python在运行时可以改变其结构,属于动态语言。\n", + "可以动态为对象添加属性(有点类似于可变类型数据)" + ] + }, + { + "cell_type": "markdown", + "id": "cec875c5", + "metadata": {}, + "source": [ + "> 名称改写机制(Name Mangling)是 Python 中的一种机制,用于避免子类覆盖父类的私有属性或方法。它会将以双下划线 __ 开头但不以双下划线结尾的属性或方法名,自动改写为 _类名__属性名 的形式。 \n", + "> 名称改写的主要目的是为了实现类的封装,避免子类意外覆盖父类的私有属性或方法。\n" + ] + }, + { + "cell_type": "markdown", + "id": "b944aefb", + "metadata": {}, + "source": [ + "class Triangle(object): 这里面加不加object有什么区别 " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "391876e3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "print(Student)" + ] + }, + { + "cell_type": "markdown", + "id": "2093e7f3", + "metadata": {}, + "source": [ + "#### 继承和多态\n", + "例如Teacher类继承自Person类" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "158e54f0", + "metadata": {}, + "outputs": [], + "source": [ + "class Person:\n", + " def __init__(self,name,age):\n", + " self.name = name\n", + " self.age = age\n", + " def eat(self):\n", + " print(f'{self.name}正在吃饭。')\n", + " def sleep(self):\n", + " print(f'{self.name}正在睡觉。')\n", + "\n", + "class Teacher(Person): # Teacher类继承自Person类\n", + " def __init__(self,name,age,title): \n", + " super().__init__(name,age) \n", + " # 子类中通过super().__init__()来调用父类初始化方法\n", + " self.title = title\n", + " # 子类定义自己特有的属性和方法\n", + " def teach(self,course_name):\n", + " print(f'{self.name}{self.titel}正在讲授{course_name}.')\n" + ] + }, + { + "cell_type": "markdown", + "id": "a6374de7", + "metadata": {}, + "source": [ + "#### 面向对象编程举例" + ] + }, + { + "cell_type": "markdown", + "id": "75cb9380", + "metadata": {}, + "source": [ + "##### 例子1:扑克牌游戏\n", + "说明:简单起见,我们的扑克只有52张牌(没有大小王),游戏需要将 52 张牌发到 4 个玩家的手上,每个玩家手上有 13 张牌,按照黑桃、红心、草花、方块的顺序和点数从小到大排列,暂时不实现其他的功能。" + ] + }, + { + "cell_type": "markdown", + "id": "cc705878", + "metadata": {}, + "source": [ + "> 发现不止一个对象或者类,要考虑类之间的关系: \n", + "> - is-a(继承)\n", + "> - has-a(关联)\n", + "> - use-a(依赖) \n", + "> 以以上例子为例,因为一副扑克有(has-a)52张牌,所以扑克和牌是has-a关系(关联);因为玩家手上有(has-a)牌,而且玩家使用了(use-a)牌,所以玩家和牌之间不仅有关联关系,还有依赖关系。\n", + ">\n", + "> 疑问:分这三类有什么具体用处?\n", + ">\n", + "> 类的属性和方法: \n", + "> 牌:属性包括花色和点数\n", + "> 扑克:属性包括牌剩余牌数;方法包括被抓牌然后减少牌数\n", + "> 玩家:属性包括手里牌的张数;方法包括抓牌、排序\n", + "\n", + "5 行话理解如何设计类(非常关键) \n", + "① 圈名词 → 找类、属性 \n", + "不是所有名词都是类,现实中独立存在的实体、有自己的状态和行为、在系统中是会变化的(这种变化应该是相对复杂的,用类可以保存状态并提供操作状态的方法,这种状态的变化不是单一变量可以处理的)、会在系统中被多个对象使用(需要复用)的才可能成为类\n", + "② 圈动词 → 找方法 \n", + "③ 基于“现实世界职责”分给合适的类 \n", + "④ 让每个类只负责“它自己能做的事” \n", + "⑤ 用组合(has-a)连接类,用方法调用(use-a)产生交互\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d5d5d6ca", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Suite.SPADE:0\n", + "Suite.HEART:1\n", + "Suite.CLUB:2\n", + "Suite.DIAMOND:3\n", + "♠5\n", + "♥K\n", + "东邪:[♠2, ♠3, ♠7, ♠8, ♠10, ♠J, ♠Q, ♥9, ♣A, ♣2, ♣5, ♣6, ♦K]\n", + "西毒:[♠9, ♥A, ♥5, ♥7, ♥8, ♥Q, ♣3, ♣9, ♣Q, ♦2, ♦4, ♦7, ♦9]\n", + "南帝:[♠5, ♠6, ♠K, ♥2, ♥3, ♥10, ♥J, ♦A, ♦3, ♦5, ♦8, ♦10, ♦J]\n", + "北丐:[♠A, ♠4, ♥4, ♥6, ♥K, ♣4, ♣7, ♣8, ♣10, ♣J, ♣K, ♦6, ♦Q]\n" + ] + } + ], + "source": [ + "from enum import Enum # Python中创建枚举值要引入enum模块\n", + "\n", + "class Suite(Enum): # 新建一个Suite类来继承Enum类\n", + " \"\"\"牌花色的枚举值\"\"\"\n", + " SPADE,HEART,CLUB,DIAMOND = range(4)\n", + "\n", + "for suite in Suite: # Python中的枚举值可以迭代\n", + " print(f'{suite}:{suite.value}')\n", + "\n", + "# 定义牌这个类\n", + "# 牌类初始化时,牌一定是有指定的花色和大小\n", + "\n", + "class Card:\n", + " \"\"\"牌\"\"\"\n", + "\n", + " def __init__(self,suite,face):\n", + " self.suite = suite\n", + " self.face = face\n", + " \n", + " # __repr__(self):定义对象的开发者友好字符串表示,\n", + " # repr(obj) 或直接在交互式环境中输入对象时会调用。\n", + " def __repr__(self):\n", + " suites = '♠♥♣♦'\n", + " faces = ['','A','2','3','4','5','6',\n", + " '7','8','9','10','J','Q','K']\n", + " return f'{suites[self.suite.value]}{faces[self.face]}'\n", + " # 返回牌的花色和点数\n", + " \n", + " def __lt__(self,other):\n", + " if self.suite == other.suite:\n", + " return self.face < other.face # 花色相同比较点数大小\n", + " return self.suite.value < other.suite.value # 花色不同比较花色对应值\n", + "\n", + "card1 = Card(Suite.SPADE,5)\n", + "card2 = Card(Suite.HEART,13)\n", + "print(card1)\n", + "print(card2)\n", + "\n", + "# 定义扑克类\n", + "# 扑克类初始化时一定是指定的52张牌,\n", + "# 另外还有洗牌、发牌的方法,有还剩多少张牌的属性\n", + "import random\n", + "\n", + "class Poker:\n", + " \"\"\"扑克\"\"\"\n", + "\n", + " def __init__(self):# 52张牌构成的列表\n", + " self.cards = [Card(suite,face)\n", + " for suite in Suite\n", + " for face in range(1,14)]\n", + " self.current = 0 # 记录发牌位置的属性\n", + "\n", + " def shuffle(self):\n", + " \"\"\"洗牌\"\"\"\n", + " self.current = 0\n", + " random.shuffle(self.cards)\n", + " # 通过random模块的shuffle函数实现随机乱序\n", + " \n", + " def deal(self):\n", + " \"\"\"发牌\"\"\"\n", + " card = self.cards[self.current]\n", + " self.current += 1\n", + " return card\n", + " \n", + " @property\n", + " def has_nex(self):\n", + " \"\"\"还有没有牌可以发\"\"\"\n", + " return self.current < len(self.cards)\n", + " \n", + "# 定义玩家类\n", + "# 玩家类有摸牌、整理手牌的方法\n", + "class Player:\n", + " \"\"\"玩家\"\"\"\n", + "\n", + " def __init__(self,name):\n", + " self.name = name\n", + " self.cards = [] # 玩家手里的牌\n", + "\n", + " def get_one(self,card):\n", + " \"\"\"摸牌\"\"\"\n", + " self.cards.append(card)\n", + " \n", + " def arrange(self):\n", + " \"\"\"整理手上的牌\"\"\"\n", + " self.cards.sort()\n", + " \n", + "# 创建四个玩家,并发牌到玩家手上\n", + "poker = Poker()\n", + "poker.shuffle()\n", + "players = [Player('东邪'),Player('西毒'),Player('南帝'),Player('北丐')]\n", + "# 将牌轮流发到每个玩家手上每人13张牌\n", + "for _ in range(13):\n", + " for player in players:\n", + " player.get_one(poker.deal())\n", + "# 玩家整理手上的牌输出名字和手牌\n", + "for player in players:\n", + " player.arrange()\n", + " print(f'{player.name}:',end='')\n", + " print(player.cards)\n", + "\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "base", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git "a/\345\255\246\344\271\240\347\254\224\350\256\260/git\345\267\245\344\275\234\346\265\201\347\250\213.md" "b/\345\255\246\344\271\240\347\254\224\350\256\260/git\345\267\245\344\275\234\346\265\201\347\250\213.md" new file mode 100644 index 000000000..a16b892f3 --- /dev/null +++ "b/\345\255\246\344\271\240\347\254\224\350\256\260/git\345\267\245\344\275\234\346\265\201\347\250\213.md" @@ -0,0 +1,60 @@ +## Git 工作流程 +本章节我们将为大家介绍 Git 的工作流程。 + +下图展示了 Git 的工作流程: +![Git 的工作流程](image.png) + +### 1、克隆仓库 +如果你要参与一个已有的项目,首先需要将远程仓库克隆到本地: +``` +git clone https://github.com/username/repo.git +cd repo +``` +> 问题1:如何只下载某个文件,不是克隆整个仓库 + +#### 如果确定远程为准,直接覆盖掉本地文件 +`git status`可以看未提交的修改(工作区+暂存区) +`nothing to commit,working tree clean`则为干干净净,表示无任何修改 +`git pull`只看提交记录是否一致,不管本地是否修改了文件。 +`git pull = git fetch + git merge`,git会比较本地当前分支的commit ID和远程当前分支的commit ID,如果两个ID一样,就会认为是`already up to date` +即使执行`git commit`,也是将文件在本地做了提交,需要再执行`git push origin master`才可以将本地的提交,提交到远程仓库 + +### 2、创建新分支 +为了避免直接在 main 或 master 分支上进行开发,通常会创建一个新的分支: +`git checkout -b new-feature` +### 3、工作目录 +在工作目录中进行代码编辑、添加新文件或删除不需要的文件。 +### 4、暂存文件 +将修改过的文件添加到暂存区,以便进行下一步的提交操作: +`git add filename` +或者添加所有修改的文件 +`git add .` +### 5、提交更改 +将暂存区的更改提交到本地仓库,并添加提交信息: +`git commit -m "Add new feature"` +### 6、拉取最新更改 +在推送本地更改之前,最好从远程仓库拉取最新的更改,以避免冲突: +`git pull origin main` +或者如果在新的分支上工作 +`git pull origin new-feature` +> 最新的更改,指的是最新远程仓库上面的信息吗? +### 7、推送更改 +将本地的提交推送到远程仓库: +`git push origin new-feature` +### 8、创建 Pull Request(PR) +在 GitHub 或其他托管平台上创建 Pull Request,邀请团队成员进行代码审查。PR 合并后,你的更改就会合并到主分支。 +### 9、合并更改 +在 PR 审核通过并合并后,可以将远程仓库的主分支合并到本地分支: +``` +git checkout main +git pull origin main +git merge new-feature +``` +### 10、删除分支 +如果不再需要新功能分支,可以将其删除: +`git branch -d new-feature` +或者从远程仓库删除分支: +`git push origin --delete new-feature` + +### 如果需要从github往本地下载文件怎么办, +只下载部分文件而且这些文件已经在本地了, \ No newline at end of file diff --git "a/\345\255\246\344\271\240\347\254\224\350\256\260/image.png" "b/\345\255\246\344\271\240\347\254\224\350\256\260/image.png" new file mode 100644 index 000000000..768579417 Binary files /dev/null and "b/\345\255\246\344\271\240\347\254\224\350\256\260/image.png" differ diff --git "a/\345\255\246\344\271\240\347\254\224\350\256\260/markdown\350\257\255\346\263\225.ipynb" "b/\345\255\246\344\271\240\347\254\224\350\256\260/markdown\350\257\255\346\263\225.ipynb" new file mode 100644 index 000000000..0e62a396e --- /dev/null +++ "b/\345\255\246\344\271\240\347\254\224\350\256\260/markdown\350\257\255\346\263\225.ipynb" @@ -0,0 +1,68 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "44897e58", + "metadata": {}, + "source": [ + "## 一、标题(#)\n", + "### 三级标题\n", + "#### 四级标题\n", + "\n", + "## 二、段落与换行\n", + "这是第一段。(空一行=新段落)\n", + "\n", + "这是第二段(行末两个空格=换行) \n", + "换行后的内容\n", + "## 三、粗体\n", + "**粗体** \n", + "*斜体* \n", + "~~删除线~~ \n", + "也可以组合 **加粗*内部斜体*组合**\n", + "## 四、列表\n", + "无序列表: \n", + "- 项目1 \n", + "- 项目2 \n", + " - 项目2.1 \n", + "\n", + "有序列表: \n", + "1. 项目1 \n", + "2. 项目2 \n", + "2.1 项目2.1 \n", + "2.2 项目2.2 \n", + "## 五、引用(>)\n", + "> 这是引用 \n", + "> 可以多行\n", + "## 六、代码\n", + "请执行 `print('Hello World')`\n", + "代码块: \n", + "```python\n", + "ded add(a,b):\n", + " return a + b\n", + "```\n", + "## 七、表格\n", + "|姓名|年龄|城市|\n", + "|------|------|------|\n", + "|张三|20|北京|\n", + "|李四|25|上海|\n", + "## 八、链接与图片\n", + "[超链接名称](http://www/baidu.com) \n", + "![超链接名称](https://www.baidu.com/img/flexible/logo/pc/result.png) \n", + "或者使用本地图片也可以\n", + "## 九、分割线(---或者***)\n", + "---\n", + "---\n", + "## 十、任务列表\n", + "- [x] 已完成任务 \n", + "- [ ] 待办任务\n" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git "a/\345\255\246\344\271\240\347\254\224\350\256\260/python_object_system_full.md" "b/\345\255\246\344\271\240\347\254\224\350\256\260/python_object_system_full.md" new file mode 100644 index 000000000..7dd27b44c --- /dev/null +++ "b/\345\255\246\344\271\240\347\254\224\350\256\260/python_object_system_full.md" @@ -0,0 +1,252 @@ +# Python 对象体系全景图(完整版本) + +## 📑 目录(TOC) + +- [1. Python 中的 object 是一切的根](#1-python-中的-object-是一切的根) +- [2. object → collections.abc + 行为体系](#2-object--collectionsabc-行为体系) + - [2.1 Iterable(可迭代)](#21-iterable可迭代) + - [2.2 Sequence(序列)](#22-sequence序列) + - [2.3 Set(集合)](#23-set集合) + - [2.4 Mapping(映射)](#24-mapping映射) + - [2.5 Iterator(迭代器)](#25-iterator迭代器) + - [2.6 Generator(生成器)](#26-generator生成器) + - [2.7 Hashable(可哈希)](#27-hashable可哈希) + - [2.8 Callable(可调用)](#28-callable可调用) +- [3. Python 数值体系(numbers 模块)](#3-python-数值体系numbers-模块) +- [4. I/O 类型](#4-io-类型) +- [5. 模块对象 module](#5-模块对象-module) +- [6. 类与实例体系](#6-类与实例体系) +- [7. 上下文管理器](#7-上下文管理器) +- [8. 特殊类型](#8-特殊类型) +- [9. 完整树形结构总结](#9-完整树形结构总结) +- [10. 终极总结](#10-终极总结) + +------------------------------------------------------------------------ + +# 1. Python 中的 object 是一切的根 + +Python 采用统一对象模型(Everything is an object)。 + +所有内容(int、list、dict、class、function...)都继承自 object。 + +------------------------------------------------------------------------ + +# 2. object → collections.abc 行为体系 + +collections.abc 定义了容器、序列、映射等行为规范。 + +------------------------------------------------------------------------ + +# 2.1 Iterable(可迭代) + + Iterable + ├── Container + ├── Sized + └── Sequence + +Iterable 典型类型: + +- list / tuple / dict / set\ +- str / bytes\ +- range\ +- file 对象\ +- generator + +------------------------------------------------------------------------ + +# 2.2 Sequence(序列) + +序列 = 有序 + 可索引。 + + Sequence + ├── list(可变) + ├── tuple(不可变) + ├── range(不可变) + ├── str(不可变字符序列) + └── bytes(不可变字节序列) + +可变序列: + + MutableSequence + └── list + +------------------------------------------------------------------------ + +# 2.3 Set(集合) + + Set + ├── set(可变) + └── frozenset(不可变) + +------------------------------------------------------------------------ + +# 2.4 Mapping(映射) + + Mapping + ├── dict(可变) + └── MappingProxyType(只读映射) + +------------------------------------------------------------------------ + +# 2.5 Iterator(迭代器) + + Iterator + ├── list_iterator + ├── tuple_iterator + ├── range_iterator + └── dict_*iterator + +只要实现了 **next** 和 **iter**,就是迭代器。 + +------------------------------------------------------------------------ + +# 2.6 Generator(生成器) + + Generator + └── generator(yield) + +------------------------------------------------------------------------ + +# 2.7 Hashable(可哈希) + +可作为字典 key 或 set 元素。 + + Hashable + ├── int + ├── float + ├── str + ├── bytes + ├── tuple(内部全可哈希) + └── frozenset + +------------------------------------------------------------------------ + +# 2.8 Callable(可调用) + +能执行 "()" 的就是 Callable。 + + Callable + ├── function + ├── method + ├── class(可实例化) + └── 实现 __call__ 的对象 + +示例: + +``` python +class A: + def __call__(self): + print("callable") +``` + +------------------------------------------------------------------------ + +# 3. Python 数值体系(numbers 模块) + + Number + ├── Integral → int + ├── Real → float + ├── Complex → complex + └── Rational → Fraction + +------------------------------------------------------------------------ + +# 4. I/O 类型 + + TextIOWrapper(文本文件) + BufferedReader + BufferedWriter + BytesIO + StringIO + +------------------------------------------------------------------------ + +# 5. 模块对象 module + + module(如 math、os、sys) + +模块也是对象。 + +------------------------------------------------------------------------ + +# 6. 类与实例体系 + + type(类的类型) + └── class A + └── A 实例对象 + +------------------------------------------------------------------------ + +# 7. 上下文管理器 + +只要实现了: + + __enter__() + __exit__() + +即可用于 with 语句。 + +典型例子:文件、锁、数据库连接等。 + +------------------------------------------------------------------------ + +# 8. 特殊类型 + + NoneType(None) + bool + ellipsis(...) + NotImplementedType + memoryview + bytearray(可变字节序列) + +------------------------------------------------------------------------ + +# 9. 完整树形结构总结 + + object + ├── Iterable + │ ├── Sequence(list, tuple, range, str, bytes) + │ ├── MutableSequence(list) + │ ├── Set(set, frozenset) + │ ├── Mapping(dict) + │ ├── Iterator(所有迭代器) + │ └── Generator + │ + ├── Hashable(int, str, tuple, frozenset, bytes) + │ + ├── Callable(函数、类、方法、__call__ 对象) + │ + ├── Number(int, float, complex, Fraction) + │ + ├── IO(文件对象、缓冲区) + │ + ├── module(math, os...) + │ + ├── type(所有类的类型) + │ + ├── 自定义 class 与实例 + │ + └── 特殊类型(NoneType, bool, ellipsis 等) + +------------------------------------------------------------------------ + +# 10. 终极总结 + +Python 的体系本质是: + +> **行为驱动(Protocol / ABC)+ 鸭子类型(Duck Typing)**\ +> 只要对象实现了对应方法(协议),就被视为该类。 + +例如: + +- 有 `__iter__` → Iterable\ +- 有 `__getitem__` → Sequence 行为\ +- 有 `__next__` → Iterator\ +- 有 `__call__` → Callable + +Python 是通过行为判断,而不是类型名字判断。 + +------------------------------------------------------------------------ + +(全文结束) diff --git "a/\345\255\246\344\271\240\347\254\224\350\256\260/\345\255\246\344\271\240\346\226\271\346\263\225\351\227\256\351\242\230.md" "b/\345\255\246\344\271\240\347\254\224\350\256\260/\345\255\246\344\271\240\346\226\271\346\263\225\351\227\256\351\242\230.md" new file mode 100644 index 000000000..e7528c2cd --- /dev/null +++ "b/\345\255\246\344\271\240\347\254\224\350\256\260/\345\255\246\344\271\240\346\226\271\346\263\225\351\227\256\351\242\230.md" @@ -0,0 +1,270 @@ + # 什么是“一句话总结”: + - 它是什么 + - 为什么存在(解决什么问题?) + - 什么时候用 + + +# 我的Python越学越乱的原因: +- 记得是“细节”,而不是“框架” +- 我学到的每一个知识点在脑中没有一个“抽象层级” +- 我不知道这个知识点解决什么问题 +- 新出现的类型、新的语法,对我来说都是孤立的 + + +# 核心观念:Python 的所有东西,都可以归为三类 +① 数据 +② 行为(函数) +③ 容器 + 协议(数据如何被操作) + +我看到的所有“新类型”“新用法”其实都只是: +- 一种新的数据结构 +- 一个函数/方法 +- 一种操作规则(协议,如迭代、上下文、可哈希、序列类型) +- 一个标准库里封装好的工具 + +# Python 三大核心概念:数据、行为、协议 + +本文件系统性地解释 Python 世界中所有“看似无穷无尽的知识点”背后的统一抽象模型。 +帮助你用底层原理建立“不会乱、不怕新知识”的学习体系。 + +--- + +# 📍 第一类:数据(Data) +Python 的一切基础都是“数据”。 +你看到的所有类型,本质上都是 **数据结构**。 + +## 🧩 常见基础数据类型(一句话总结) +| 数据类型 | 一句话总结 | +|---------|------------| +| **int** | 一个整数值 | +| **float** | 一个小数 | +| **str** | 一段文本(不可变) | +| **list** | 可变序列 | +| **tuple** | 不可变序列 | +| **dict** | 哈希表,用 key 查 value | +| **set** | 去重 + 集合运算 | +| **bytes** | 二进制序列 | +| **bool** | True / False | +| **class** | 用户自定义的数据结构 | + +--- + +## 🧩 你未来会看到的“新类型”(其实也是数据) +例如: + +- datetime +- Fraction +- Decimal +- pathlib.Path +- re.Match +- MySQLCursor +- Response(requests) +- BeautifulSoup +- numpy.ndarray +- pandas.DataFrame + +它们都不是“新奇怪物”,它们都是: + +> Python 内置或第三方库定义的 **新的数据结构(结构不同,本质相同)** + +### ✔ 结论 +**所有“新数据类型”都是“数据”,不是新概念,只是新结构。** + +--- + +# 📍 第二类:行为(Function) +数据能做什么,由“行为”决定。 + +行为的统一定义: + +## ⭐ 行为 = 函数 + 方法(绑定到对象的函数) + +--- + +## 🎯 示例(你看到的一切操作,都是行为) +| 代码 | 类型 | 说明 | +|------|------|-------| +| `len(a)` | 函数 | 获取长度 | +| `a.append(3)` | 方法 | list 的行为 | +| `d.get("name")` | 方法 | dict 的行为 | +| `re.match()` | 函数 | 正则匹配 | +| `random.randint()` | 函数 | 取随机数 | + +本质上它们都是: + +> 输入数据 → 行为处理 → 输出结果 +> (方法还会改变对象内部状态) + +--- + +## ✔ 为什么这能消除你的焦虑? +当你知道: + +- list 的操作是 list 定义的行为 +- dict 的操作是 dict 定义的行为 +- 库里的方法是库的作者定义的行为 + +你看到“新方法、新函数、新 API” +就不会觉得那是“新知识点”,它只是“行为”。 + +--- + +# 📍 第三类:容器 + 协议(规则) +这是 Python 最强大也最容易让初学者困惑的部分。 + +--- + +# 1️⃣ 什么是容器? +容器就是“能装数据的结构”。 + +常见容器: + +- list +- tuple +- dict +- set +- generator +- iterator +- file +- pandas.DataFrame +- numpy.ndarray + +它们结构不同,但本质相同: +**装数据 + 对外提供统一操作行为(协议)** + +--- + +# 2️⃣ 什么是协议? +协议 ≠ 网络协议 +协议 = **规定对象必须具备的能力(钩子方法)** + +只要该能力被实现,对象就能表现出某种“行为”。 + +--- + +# 🔥 Python 常见协议(必须掌握) + +## ① 可迭代协议(Iterable) +只要对象实现: + +``` +__iter__() +``` + +它就能: + +- 被 for 循环 +- 被 list() 转换 +- 被展开:`*obj` +- 被 sum()、max() 等函数使用 + +### 🔥 所有这些都能 for,因为它们实现了 iterable: +- list +- tuple +- dict +- str +- range +- pandas.Series +- generator + +它们长得不一样,但行为一样,是因为遵守了 **同一个协议**。 + +--- + +## ② 上下文管理协议(Context Manager) +只要对象实现: + +``` +__enter__() +__exit__() +``` + +它就能被 `with` 使用。 + +### 这些都能用 with: +- 文件对象 +- 数据库连接 +- 锁 +- Selenium 浏览器 +- Requests Session +- TensorFlow Session +- contextlib 生成的对象 + +它们毫不相关,却都能: + +```python +with obj: + ... +``` + +因为它们遵守同一个协议。 + +--- + +## ③ 可调用协议(Callable) +如果一个对象实现: + +``` +__call__() +``` + +那么它就可以作为函数调用: + +```python +obj() +``` + +Python 的装饰器、回调机制、类实例当函数,都是用这个协议。 + +--- + +## ④ 数字协议(算术运算) +对象实现这些方法后,就能进行数学运算: + +``` +__add__() → + +__sub__() → - +__mul__() → * +``` + +numpy / pandas 的底层,就是依靠这些协议。 + +--- + +## ⑤ 序列协议(像数组一样) +只要实现: + +``` +__getitem__() +__len__() +``` + +它就能: + +- 被索引:`obj[0]` +- 被切片:`obj[1:3]` +- 被遍历 + +--- + +# 📌 最终总结(非常关键) + +## ⭐ Python 所有看似复杂的东西,都能归类为三件事: + +### **① 数据(Data)—— 类型本质是数据结构** +### **② 行为(Function/Method)—— 数据能做什么** +### **③ 协议(Protocol)—— 对象具备哪些能力** + +--- + +# ✔ 这意味着: + +- 新类型?→ 属于“数据” +- 新方法?→ 属于“行为” +- 能被 for?→ 遵守迭代协议 +- 能被 with?→ 遵守上下文协议 +- 能被调用?→ 遵守可调用协议 + +所有新东西都“有位置”,不会让你困惑。 + +--- diff --git "a/\345\255\246\344\271\240\347\254\224\350\256\260/\347\254\224\350\256\260\346\225\264\347\220\206.md" "b/\345\255\246\344\271\240\347\254\224\350\256\260/\347\254\224\350\256\260\346\225\264\347\220\206.md" new file mode 100644 index 000000000..521051f11 --- /dev/null +++ "b/\345\255\246\344\271\240\347\254\224\350\256\260/\347\254\224\350\256\260\346\225\264\347\220\206.md" @@ -0,0 +1,8140 @@ + +## 前言(原文保留) + +> **记笔记的目的**: +> +> ★笔记不是“完整教程”,而是理解+易错点+示例+练习 +> +> 每一节: +> - “一句话总结(核心)” +> - “示例(能运行、最简单)” +> - “易错点(踩过的坑)” +> - “应用场景(什么时候用)” + +> 什么是“一句话总结”: +> - 它是什么 +> - 为什么存在(解决什么问题?) +> - 什么时候用 + + +## 01 环境与运行 +### (一)安装与版本 +#### 1.Python 安装、解释器选择 +##### 环境变量的含义: + +##### 解释器是啥: +解释器(Interpreter)就是用来运行 Python 代码的程序。你写的 .py 文件本身不能直接被 Windows 执行,需要由解释器读取并执行。在 Python 里,“解释器”通常指你电脑上的 python.exe(Windows)这一套运行环境。 + +解释器具体做什么: +- 读取你的代码(源代码) +- 解析语法、编译成字节码(.pyc) +- 交给 Python 虚拟机执行 +- 提供标准库、管理内存、异常处理等运行时能力 + +多个解释器的情况(`where python`查看当前解释器路径): +- 系统 Python +- Anaconda/Miniconda 的 Python +- VS Code 选择的虚拟环境里的 Python(.venv\Scripts\python.exe) +它们都是“解释器”,只是路径和安装环境不同。 + +#### 2.版本差异(Python3.x) +### (二)开发工具 +#### 1.VS Code 基础配置、常用快捷键 +- 如何修改配置 +- 如何正确使用侧边栏AI +- 常用快捷键 +#### 2.代码格式化与提示(pyright/pylance) +#### 3.VSCode里面的KerNel内核 +### (三)虚拟环境 +#### 1.含义及作用 +含义:独立运行的Python环境,允许在同一台机器上为不同的项目创建隔离的Python环境。每个虚拟环境都有自己的Python解释器、安装的包/库、环境变量。 + +作用:创建虚拟环境,实现项目隔离(不同项目需要不同版本的Python或第三方库)、避免冲突(防止全局Python环境被污染)、依赖管理(方便记录和分享项目的依赖关系)、安全测试项目新包 +#### 2.虚拟环境管理 +```python +# 创建虚拟环境: +python -m venv .venv + +# 激活(PowerShell): +.\.venv\Scripts\Activate.ps1 +# 激活(CMD): +.\.venv\Scripts\activate.bat + +# 退出虚拟环境: +deactivate + +# 看当前 python/pip 是否在虚拟环境里: +where python +where pip +python -c "import sys; print(sys.executable)" +pip -V + +# 安装 / 卸载 / 查看包: +pip install requests +pip uninstall requests +pip list +pip show requests + +# 固化依赖 / 按依赖安装: +pip freeze > requirements.txt +pip install -r requirements.txt + +# 升级 pip(建议用这种写法) +python -m pip install --upgrade pip +``` +### (四)运行方式 +#### 1.脚本运行 +`python xxx.py` +#### 2.模块运行 +`python -m package.module` +#### 3.交互式 +- Jupyter Notebook +- shell等 +交互模式中,最后被输出的表达式结果被赋值给变量_,例如: +```shell +>>> tax = 12.5 / 100 +>>> price = 100.50 +>>> price * tax +12.5625 +>>> price + _ +113.0625 +>>> round(_, 2) +113.06 +``` +#### 4.常见命令行窗口 +| 命令行窗口/工具 | 类型 | 主要作用/特点 | 适合场景 | 常见打开方式(Windows) | +|---|---|---|---|---| +| 命令提示符(cmd.exe) | Shell | 传统 Windows 命令行,兼容性最好,适合基本命令与批处理 | 基础文件操作、运行 `.bat`、执行系统命令 | 开始菜单搜索 `cmd` | +| PowerShell(Windows PowerShell / PowerShell 7) | Shell | 更强的脚本与自动化能力,“对象管道”,命令功能更丰富 | 自动化脚本、系统管理、批量处理 | 开始菜单搜索 `PowerShell`(建议 PowerShell 7) | +| Windows Terminal(wt) | 终端宿主/容器 | 多标签/分屏终端,可同时承载 cmd/PowerShell/WSL 等 | 统一管理多个终端会话 | 开始菜单搜索 “Windows Terminal” 或运行 `wt` | +| Git Bash | Shell(类 Unix) | 提供 bash 环境与常见 Unix 命令(如 `ls/cp/mv/grep`),方便配合 Git | 使用类 Linux 命令、Git 操作 | 安装 Git for Windows 后打开 “Git Bash” | +| WSL 终端(Ubuntu 等) | Linux Shell 环境 | 在 Windows 上运行 Linux 用户态,命令链更贴近服务器环境 | Linux 开发/部署一致性、使用原生 Linux 工具链 | Windows Terminal 选择 Ubuntu 或运行 `wsl` | +| VS Code 集成终端 | IDE 内终端 | 在 VS Code 内运行 cmd/PowerShell/Git Bash/WSL,工作目录与项目联动 | 边写代码边运行、项目内调试 | VS Code:`Ctrl + \`` 打开,右上角选择 shell | + +--- + +#### 4.常见命令速查(按用途汇总) + +| 用途 | cmd(命令提示符) | PowerShell | bash/WSL(Git Bash / Linux) | +|---|---|---|---| +| 查看当前位置/切换目录/列目录 | `cd` / `cd /d d:\github` / `dir` | `Get-Location` / `Set-Location D:\github` / `ls` | `pwd` / `cd /d/github` / `ls` | +| 创建目录 | `mkdir test` | `mkdir test` | `mkdir test` | +| 查看文件内容 | `type a.txt` | `cat a.txt` | `cat a.txt` | +| 复制文件 | `copy a.txt b.txt` | `Copy-Item a.txt b.txt` | `cp a.txt b.txt` | +| 移动文件 | `move a.txt .\sub\` | `Move-Item a.txt .\sub\` | `mv a.txt sub/` | +| 删除文件 | `del a.txt` | `Remove-Item a.txt` | `rm a.txt` | +| 删除目录(慎用) | `rmdir /s /q folder` | `Remove-Item folder -Recurse -Force` | `rm -rf folder` | +| 文本搜索 | `findstr /s /n "keyword" *.py` | `Select-String -Path *.py -Pattern "keyword"` | `grep -RIn "keyword" .` | +| 网络/端口排查 | `ping github.com` / `ipconfig` / `netstat -ano \| findstr :8000` | `Test-NetConnection github.com -Port 443` +(也可用左列命令) | `ping` / `ip a`(或 `ifconfig`)/ `ss -lntp`(WSL) | +| 进程查看/结束 | `tasklist` / `taskkill /PID 1234 /F` | `Get-Process` / `Stop-Process -Id 1234 -Force` | `ps aux` / `kill -9 1234` | +| Python 常用 | `python --version` / `python -m venv .venv` / `.\.venv\Scripts\activate` / `python -m pip install -U pip` / `pip install requests` / `python xxx.py` / `python -m package.module` | 同 cmd(激活命令同样适用) | `python3 --version` / `python3 -m venv .venv` / `source .venv/bin/activate` / `python3 -m pip install -U pip` / `pip install requests` / `python3 xxx.py` | +| Git 常用 | `git status` / `git add .` / `git commit -m "msg"` / `git pull` / `git push` / `git branch` / `git checkout -b new-branch` | 同 cmd | 同 cmd | + +--- + +#### 4.VS Code 里选择终端类型 +| 操作 | 说明 | +|---|---| +| 打开集成终端 | `Ctrl + \``(反引号) | +| 切换 shell | 终端右上角下拉:PowerShell / cmd / Git Bash / WSL | + +--- + +#### 5.存在问题 +如何知道当前使用的Python是哪个版本,对应的包是什么? + +--- + +## 02 基础语法与编码规范 +### (一)基本语法 +#### 1.缩进与代码块、注释 +##### (1)缩进与代码块 +- Python中用缩进表示代码块,而不是大括号`{}`。 +- Python中一条语句很长,可以用反斜杠`\`来实现换行。 +```python +total = item_one + \ + item_two + \ + item_three +``` +- Python中同一行也可以有多个语句,用分号;分隔。 +- 在类或函数之间一般会留一个空行,便于日后的维护和重构。 +- 当写示例时,可以使用pass这个占位语句,表示什么都不做,以避免语法错误。 +##### (2)注释 +``` +# 单行注释 +# 第二个注释 + +''' +第三行注释 +第四行注释 +''' + +""" +第五行注释 +第六行注释 +""" +``` +#### 2.命名与规范 +##### (1)变量命名规则 +语法规则(硬性) +- 只能由 字母、数字、下划线 _ 组成 +- 不能以数字开头:1a 不行,a1 可以 +- 区分大小写:name 和 Name 是两个变量 +- 不能用关键字:如 class、def、for、None 等。可用 keyword.kwlist 查看关键字列表 +- 避免用内置名当变量:比如 list = [] 会覆盖内置 list 类型名(不推荐) + +常见命名习惯(约定): +- 用有意义的名字:total_price 比 tp 更清晰 +- 多个单词用下划线分隔:user_name(snake_case) + +##### (2)最佳实践PEP8 +PEP8是 Python 官方的代码风格规范(写代码的“排版/命名/布局”建议),目的是让代码更统一、更好读,最常见的几条: +- 变量/函数名:小写 + 下划线 snake_case。例:get_user_info +- 类名:大驼峰 CapWords。例:UserProfile +- 常量:全大写 + 下划线。例:MAX_RETRY = 3 +- 缩进:4 个空格(不要 tab) +- 每行长度:通常建议不超过 79/99(工具会自动处理) +- 运算符两边空格:a = b + c +- 导入顺序:标准库、第三方库、本地代码分组导入 + +### (二)理解变量 +#### 1.变量的含义 +含义:Python中所有变量都是“名字→对象”的引用(变量存的是对象的地址)。 + +变量没有类型: +- 变量没有类型,只有对象有类型。 +- 变量本质上等同于“自动管理内存的安全指针”。 +- 一般说的变量类型指的是“变量当前指向的对象的类型”。 + + +- 几个问题(大部分使用了Python的魔法方法): + +a.如果变量存的是内存的地址,那么为什么print(变量)会返回对象的值? +print()调用的是变量所指向对象的`__str__()、__repr__()`方法,而不是打印地址。 +如果要打印地址,可以用print(id(a))返回变量a在内存中的地址id。 + +b.`a=1`和`a+1`中,变量a是地址,那为什么可以计算a+1? +先找到变量a指向的对象(1),调用该对象的__add__()方法(a.__add__(1))生成一个新的对象(2),然后把这个新的对象返回。原先的a和1都不会被修改。 +如果是`a=a+1`呢,这时候a引用的对象会变成2。 + +c.不可变对象(int,str,tuple)不能改,只能换对象; +可变对象(list,dict,set)能改,引用不变。 +不可变对象不允许修改内部值,修改只能创建新对象,旧对象如果无人引用则会被自动回收。 + +```python +a = 10 # int类型不可变 +b = a +a = 20 # 修改a的值,不会影响b的值,10,20是两个独立的对象 +# b 指的还是原先的整数10对象 + +c = [1,2,3] # list类型可变 +d = c +d.append(4) # 修改d的值,c的值也会变化,c和d是同个引用 +``` + +d.用del语句删除的是什么?是将对象从内存中直接删除吗? +del 删除的是名字(引用),不是“直接把对象从内存里抹掉”。而对象是否被释放取决于:它还有没有其他引用。 + +e.type(变量a)的时候,将变量a引用的对象作为参数传入内置函数type(),由type()查看该对象内部的`__class__属性` + +#### 2.变量声明 +- 元组的声明,`tup1 = (1,)`,必须带括号,否则仅为仅为用于确定运算顺序的括号 +- 字典的声明? + +#### 3.变量类型 +可变类型和不可变类型 +变量类型的转换: +- 类型转换函数(应该是区分了强制转换和自动转换?) +- 转换为整数 `int(x)`,转换时可带进制 +- 转换为浮点数`float(x)` +- 转换为字符串`str(x)` +- 转换为列表`list(x)` +- 转换为元组`tuple(x)` +- 转换为字典? + +- 自动转换 +- 带有浮点数的数学运算的结果,均为float类型 2/1 = 2.0 + +4.多变量赋值的问题: +`a=b=c=1` +`a,b,c=1,2,'runoob'` + +5.调换两个变量的值 +`a,b=b,a` + +### (三)命名空间与作用域 +作用:为了解决项目中命名冲突的问题。 + +含义:命名空间(Namespace)是从名称到对象的映射,大部分的命名空间都是通过 Python 字典来实现的。 + +特点:同个命名空间内不能有重名,各个命名空间相互独立,没有关系。 + +命名空间的种类及查找顺序:函数或类的局部命名空间→模块中的全局命名空间→外部函数中外层非全局命名空间→Python语言内置的名称(如abs、char和异常名称等) + +命名空间和作用域的关系: +- 命名空间(namespace):“名字 → 对象”的映射表(字典一样的东西),用来存放/管理变量名。 +- 作用域(scope):代码中某个名字“能在哪些位置被访问/查找”的范围规则。 +- 二者关系:作用域决定查哪个命名空间;命名空间负责“存了什么名字”。 + +修改命名空间的方法:局部变量的优先级高于全局变量。 +当内部作用域想修改外部作用域的变量时,可以用global关键字和nonlocal关键字。其中global影响的是全局,nonlocal影响的是外层非全局作用域。 + +```python +num = 1 +def fun1(): + global num # 让程序不用创建局部变量,而是使用全局变量。 + # 并非声明一个内部变量(nonlocal功能类似) + print(num) # 输出 1 + num = 123 + print(num) # 输出 123 +fun1() +print(num) # 输出 123 +``` + +易错点:赋值会改变作用域判定。 +只要一个变量名在函数体内出现了赋值语句,Python 就会把它判定为该函数的“局部变量”(除非你显式声明 global num 或 nonlocal num)。以下代码会有错误: +```python +num = 1 +def fun1(): + num1 = num + print(num1) + num = 123 + print(num) +fun1() +print(num) + +# 函数体里出现对 num 的赋值,就会让 num 在该函数里变成局部变量 +# 所以在调用num1 = num的时候,这句里面的num是局部变量,但是没有赋值,所以就会报错UnboundLocalError(先读后赋值)。 +``` +(待确认) +- 输入与输出 + - `input()` / `print()`(sep/end) + - 格式化输出:f-string、`format()` +- 常见易错点 + - 缩进错误、编码问题、字符串引号混用 + + +--- + +## 03 数据类型与数据结构 +### 3.1 类型体系总览 +#### 3.1.1 数据类型、数据结构、变量类型、对象类型之间的区别和联系? +- 数据类型(data type):值/对象的类别,如 int、str、list、dict。决定能做什么操作/有哪些方法。 +- 对象类型(object type):某个具体对象的类型,基本等同“数据类型”,用 type(obj) 查看。 +- 变量类型(variable type):Python 里变量名本身没固定类型;通常指“变量当前指向的对象类型”。 +- 数据结构(data structure):组织数据的方式与常见操作/效率。Python 的 list/dict/set 既是类型,也代表典型数据结构。 +> 联系:变量(名字)引用对象;对象有类型(数据类型/对象类型);某些类型实现了特定数据结构来组织数据。 + +#### 3.1.2 数据类型的用处? +- 知道能用哪些操作:str.split()、list.append()、dict.get() 等。 +- 减少错误、便于调试:避免类型不匹配(如 input() 是 str)。 +- 选对结构更高效:set/dict 查找通常比 list 快。 +- 提升可读性与可维护性:配合类型注解让接口更清晰。 + +#### 3.1.3 动态类型、强类型(strongly typed) +动态类型:类型不绑定在变量名上,而是绑定在对象上;同一个变量名运行过程中可以指向不同类型的对象。 +强类型:不同类型之间不会被“偷偷自动转换”来完成运算;类型不匹配通常直接报错,要求你显式转换。 + +#### 3.1.4 `type()` / `isinstance()` 的区别与使用场景 +- type(obj)用来查对象的实际类型(class),例如`type(a)`。 +- isinstance()用来判断对象(实例)是不是某个类型(或其子类)的实例,例如`isinstance(a,int)` +> 区别:type()严格匹配类型,不考虑继承;isinstance()会考虑继承关系。 +```python +class A:pass # pass占位语句 +class B(A):pass + +b = B() +print(type(b) is B) # True +print(type(b) is A) # False +print(isinstance(b,B)) # True +print(isinstance(b,A)) # True +print(issubclass(B,A)) # 判断类的继承关系,True +``` + +几个问题: +- `type(a) == int`和`type(a) is int`有什么区别? +`==` 是比较两个对象的“值”是否相等,不推荐,原因在`==`依赖类的`__eq__`,不是很严格; +`is` 是判断两个对象是否是“同一个对象”(id一致),推荐。 +补充说明:int在Python中也是一个对象(类对象) +- `type(A)`返回的是啥?这里面的A是类 +返回type,类也是对象,Python中所有类的类型都是type +- `isinstance(B,A)`为啥是False? +`isinstance()`是用于判断实例是否属于某个类或者某个父类。 +如果要判断两个类的继承关系,可以用`issubclass()` + +#### 3.1.4 真值测试(truthiness) +真值测试(truthiness)指的是:在 if、while、and/or/not 这类需要“判断真/假”的场景里,Python 会把任何对象自动当成 True 或 False 来用(不一定非得是 True/False)。哪些值为False: +- None +- 数值的零:0、0.0、0j、Decimal(0) 等 +- 空的容器/序列:''、[]、()、{}、set() +- 空的 range:range(0) + + +### 3.2 数值类型(Numbers) +#### 3.2.1 数值类型分类 +##### 3.2.1.1 int 整数 +十进制、二进制、八进制、十六进制 +```python +# print()默认输出数值是转换为十进制显示。 +print(0o12) #八进制,0o或者0O +print(0b1010) #二进制,0b或者0B +print(0xA) #十六进制,0x或者0X +#十六进制的数A B C D E F 分别表示十进制中的 10 11 12 13 14 15 + +print(oct(97)) #oct()十进制转为八进制字符串,输出0o141 +print(hex(97)) #hex()十进制转为十六进制字符串,输出0x61 +print(bin(97)) #bin()十进制转为二进制字符串,输出0b1100001 +``` +##### 3.2.1.2 float 浮点数 +1)浮点数本质:Python 的 `float` 基本等同于 **IEEE 754 双精度浮点数**(64 位) +很多十进制小数无法用二进制有限表示(类似 1/3 不能用有限小数表示一样) +0.1表示成二进制是: + +2)浮点数误差: +- 某些看似简单的计算会出现微小误差,例如: +```python +0.1 + 0.2 # 0.30000000000000004 +``` +- 比较的话,`a==b`会失败,可以用`math.isclose()`来做近似比较 +```python +import math +math.isclose(0.1 + 0.2, 0.3) # True +``` +- 金额场景避免用float: + - 用 `decimal.Decimal`(高精度十进制) + - 或使用“整数分”(把金额乘 100 变成 int 存储) + +3)round规则简介: +- `round(x, n)`:把 `x` 四舍五入到小数点后 `n` 位,返回结果类型通常仍是 `float`(或 `int` 当 n 省略时)。 +- 关键点:Python使用“ties to even”(银行家舍入) +即 当“正好在中间”(末位是 5 且后面全是 0 的理想情况)时,会舍入到**最近的偶数**: +```python +round(1.5) # 2 +round(2.5) # 2 (2 是偶数) +round(3.5) # 4 +``` +- round 错觉 +```python +round(2.675,2) # 2.67而不是2.68 +``` +原因在于浮点误差,所以财务/报表类需求,建议用 `Decimal` 并指定舍入模式。 +4)输出格式 +```python +# (1)print 默认显示 +x = 0.1 + 0.2 +print(x) # 0.30000000000000004 + +# (2)用 f-string 控制小数位(保留 2 位::.2f) +x = 1 / 3 +print(f"{x:.2f}") # 0.33 + +# (3)科学计数法输出(e) +x = 123456789.0 +print(f"{x:e}") # 1.234568e+08 +print(f"{x:.3e}") # 1.235e+08 + +# (4)百分比格式(:% 会乘以 100 并加 %) +ratio = 0.1234 +print(f"{ratio:.2%}") # 12.34% + +# (5)对齐与宽度(总宽度 8,保留 2 位:8.2f) +x = 12.3456 +print(f"|{x:8.2f}|") # | 12.35| + +# (6)更完整/可复现表示:repr +x = 0.1 + 0.2 +print(repr(x)) # '0.30000000000000004' +``` + +##### 3.2.1.3 complex 复数 +复数由实数部分和虚数部分构成,可以用a + bj,或者complex(a,b) 表示,复数的实部a和虚部b都是浮点型。如`3e+26j` + +##### 3.2.1.3 decimal.Decimal 高精度小数 +1)Decimal解决的问题: +float有二进制误差`0.1+0.2!=0.3`,0.1转化为二进制是一个无限循环数。 +Decimal用十进制表示存储和计算,可以精确表示0.1这种十进制小数,适合: +- 金额/财务/报表 +- 需要可控舍入规则(例如“四舍五入”“银行家舍入”“向上取整”等) +- 需要固定小数位(如保留 2 位) + +2)基本创建方式,建议从字符串或int构建: +```python +from decimal import Decimal +Decimal("0.1") + Decimal("0.2") # Decimal('0.3') +``` +从float创建,会将float的误差也带进来。因为float里面的0.1本身就是不准确的。 + +3)Decimal不能和float混算,会报错TypeError + +4)精度与上下文(context) +- Decimal 的运算精度/舍入等由 **上下文**控制:`getcontext()` +- 常见:设置有效数字精度 +```python +from decimal import getcontext +getcontext().prec = 28 # 默认通常就是 28(有效数字) +``` +> `prec` 是“有效数字位数”,不是“小数点后位数”。 + + +5)舍入( rounding )与 `quantize()`(最常用) +- 保留小数位、按指定规则舍入:用 `quantize()` +- 例:保留 2 位小数(常用于金额) +```python +from decimal import Decimal, ROUND_HALF_UP +x = Decimal("2.675") +x.quantize(Decimal("0.00"), rounding=ROUND_HALF_UP) # Decimal('2.68') +``` + +常见舍入模式(了解并会用 1~2 个): +- `ROUND_HALF_UP`:传统四舍五入(0.5 进位) +- `ROUND_HALF_EVEN`:银行家舍入(ties to even) +- `ROUND_DOWN`:向 0 方向截断 +- `ROUND_FLOOR`:向负无穷取整 + +##### 其他类型 +- `fractions.Fraction`(可选):分数 +#### 3.2.2 数值运算 +##### 3.2.2.1 运算符 +1)运算符分类总览 +- 算术运算符:`+ - * / // % **` +- 比较运算符:`== != < <= > >=` +- 逻辑运算符:`and or not` +- 赋值运算符:`= += -= *= /= //= %= **= :=` +- 位运算符:`& | ^ ~ << >>` +- 成员运算符:`in not in` +- 身份运算符:`is is not` +- (了解)条件表达式:`x if cond else y` + +2)算数运算符 +- `/` 真除法:结果一定是 `float` +- `//` 地板除:向下取整(对负数要注意),-9//2等于-5 +- `%` 取模:和 `//` 的关系:`a == (a//b)*b + (a%b)` +- `**` 幂 +- `+` 在不同类型上的含义(运算符重载) + - `int/float`:加法 + - `str/list/tuple`:拼接 +- 数值类型的混合运算(int + float -> float) + +3)比较运算符与链式比较 +- 基本比较:`== != < <= > >=` +- 链式比较:`0 < x < 10` 等价于 `0 < x and x < 10` +- 不同类型比较:Python3 通常不允许随便比较(如 `1 < "2"` 会报错) + +4)逻辑运算符(短路与返回值) +- 短路:`and/or` 可能不执行右边表达式 +展开说说:当左边结果已经足够决定整体结果,右边就不会执行。 +`a and b`,当a为True时,会继续计算b;当a为False时,不会计算b,结果肯定为False +`a or b`,当a为True,结果肯定为True,不会计算b +- 返回值不是布尔:返回“最后计算的那个操作数” +and/or是表达式运算符,会返回参与运算的某个操作数本身,而不是强制返回True/False。 + - `a or b`:返回第一个 truthy(真值),否则返回 b +```python +print("" or "default") # "default"("" 是 falsy) +print("hello" or "default") # "hello"(第一个 truthy) +print(0 or 10) # 10 +print([] or [1, 2]) # [1, 2] +``` +一般用于提供默认值,例如: +```python +name = input().strip() +name = name or "匿名用户" # 不输入名称就默认为“匿名用户” +``` + - `a and b`:返回第一个 falsy(假值),否则返回 b +```python +print(0 and 10) # 0(第一个 falsy) +print(5 and 10) # 10(a 为 truthy,所以返回 b) +print("hi" and "") # ""(b 是最后计算的操作数) +print([1] and [2, 3]) # [2, 3] +``` +一般用于前置条件判断: +```python +user = {"name": "Tom"} +print(user and user.get("name")) # "Tom" + +user = None +print(user and user.get("name")) # None(短路,不会执行右边,避免报错) +``` +- `not x`:总是返回 bool +not 是逻辑非运算,它会把 x 先做真值测试,然后取反,结果一定是 True 或 False。 +- 真值测试(truthiness):空容器/0/None 为 False + +5)赋值运算符与“重新绑定” +- `=` 是绑定名字到对象,不是“把值塞进变量” +- 增量赋值:`+=` 等 + - 对不可变类型:通常会创建新对象再绑定 + - 对可变类型:可能就地修改(例如 list 的 `+=`) +- 海象运算符:`:=`(表达式内赋值,Python 3.8+),用法举例: +```python +#传统写法 +n = 10 +if n > 5: + print(n) +#使用海象运算符 +if (n:=10) > 5: # 现把n赋值为10,再返回这个赋值结果 + print(n) +``` + +6)身份 vs 相等(非常重要) +- `==`:值是否相等(`__eq__`) +- `is`:是否同一个对象(比较 `id`) +`id()`能辅助理解,但不要把它当“内存地址”的语义依赖,`id(obj)` 在 CPython 上通常对应对象的内存地址(实现细节),你可以用它观察对象身份是否相同,但不要写依赖 `id` 的逻辑。 +- `None` 比较:用 `is None` / `is not None` +- 小整数/字符串驻留:不要依赖 `is` 做值比较 +Python 为了性能,可能会把某些常用对象(小整数、字符串)复用(实现细节),导致看起来像值比较。值比较永远用“==” +```python +a = 256 +b = 256 +print(a == b) # True +print(a is b) # 可能 True(实现细节:小整数缓存) +``` +```python +a = 1000 +b = 1000 +print(a is b) # 可能 False(不要依赖) +``` + +7)成员运算符(in / not in) +- 序列:按元素查找 +- 字典:`in` 查的是 key(不是 value) +- 自定义对象可通过 `__contains__` 定义行为 + +8)位运算符(知道含义与应用场景) +- 与/或/异或:`& | ^` +- 取反:`~x == -(x+1)` +- 左移/右移:`<< >>` +- 应用:掩码、权限位、二进制处理(了解) + +位运算符把数字看做二进制来进行计算。 +```python +a = 60 +print(f'{a:08b}') # a的二进制数为 00111100 +b = 13 +print(f'{b:08b}') # b的二进制数为 00001101 + +# & 按位与:两个对应位置都是1,则结果位为1,否则为0 +print(a&b) #输出12 # a&b为 00001100 + +# | 按位或:两个对应位置有一个是1,则结果位为1,否则为0 +print(a|b) #输出61 # a|b为 00111101 + +# ^ 按位异或:两个对应位置不同时,结果为1,否则为0 +print(a^b) #输出49 # a|b为 00110001 + +# ~ 按位取反:把0变成1,把1变成0 +print(~a) #输出-61 # ~a为 -0111101? + +# << 左移动若干位 +print(a<<2) #输出240,相当于放大了2的2次方倍 # <> 左移动若干位 +print(a>>2) #输出15,相当于缩小了2的2次方倍 # <>`、`<<` | 右移、左移 | +| `&` | 按位与 | +| `^`、`\|` | 按位异或、按位或 | +| `<=`、`<`、`>`、`>=` | 小于等于、小于、大于、大于等于 | +| `==`、`!=` | 等于、不等于 | +| `is`、`is not` | 身份运算符 | +| `in`、`not in` | 成员运算符 | +| `not`、`or`、`and` | 逻辑运算符 | +| `=`、`+=`、`-=`、`*=`、`/=`、`%=`、`//=`、`**=`、`&=`、`\|=`、`^=`、`>>=`、`<<=` | 赋值运算符| + + +10)可选扩展:运算符重载(面向对象相关) +运算符重载=“把运算符变成方法调用”,让你的类像内置类型一样自然地参与 `+`、比较、`abs()` 等操作。 +通过实现魔法方法(dunder methods)让自定义类支持 `+ - * < ==` 等运算符。例如你写 `a + b`,Python 实际会尝试调用:`a.__add__(b)`(加法) +- 例如 `__add__`、`__mul__`、`__lt__` 等 +- 让自定义类支持 `+`、比较等操作 + +常见运算符与对应魔法方法: +- 算术: + - `+` → `__add__`(反向:`__radd__`) + - `-` → `__sub__`(反向:`__rsub__`) + - `*` → `__mul__`(反向:`__rmul__`) + - `/` → `__truediv__` + - `//` → `__floordiv__` + - `%` → `__mod__` + - `**` → `__pow__` +- 比较: + - `==` → `__eq__` + - `<` → `__lt__` + - `<=` → `__le__` + - `>` → `__gt__` + - `>=` → `__ge__` +- 一元: + - `-x` → `__neg__` + - `+x` → `__pos__` + - `abs(x)` → `__abs__` +- 增量赋值(可选): + - `+=` → `__iadd__`(若未实现,退回到 `__add__` 并重新绑定) + + +最小示例:让自定义“向量”支持 `+` 和比较。 +```python +from dataclasses import dataclass +import math + +# @dataclass 是标准库dataclasses提供的装饰器,用来自动生成样板代码。 +# forzen = True 表示这个数据类是不可变的(immutable): +# - 创建实例后,不能再修改字段值(不能给 x、y 重新赋值) +# - 更像“值对象”(value object),适合表示坐标、配置、常量数据等 +# - 也更安全:避免被意外修改 +# 此外,@dataclass的另外一个作用就是:根据类里写的“类型注解字段”自动帮你生成 __init__(以及 __repr__、__eq__ 等)。所以你虽然没手写初始化方法,但类在定义完成后已经“被补上”了初始化逻辑。 +@dataclass(frozen=True) +class Vec2: + x: float + y: float + + # v1 + v2 + def __add__(self, other): + if not isinstance(other, Vec2): + return NotImplemented # 类型不匹配时返回`NotImplemented` + return Vec2(self.x + other.x, self.y + other.y) + + # v1 * 3(标量乘) + def __mul__(self, k): + if not isinstance(k, (int, float)): + return NotImplemented + return Vec2(self.x * k, self.y * k) + + # 3 * v1(反向乘) + def __rmul__(self, k): + return self.__mul__(k) + + # v 的“大小”(模长) + def norm(self) -> float: + return math.hypot(self.x, self.y) + # math.hypot(x, y) 用来计算直角三角形斜边长度,也就是二维向量的欧几里得范数(模长),math.hypot(3,4)为5 + + # v1 < v2(按模长比较) + def __lt__(self, other): + if not isinstance(other, Vec2): + return NotImplemented + return self.norm() < other.norm() + +v1 = Vec2(1, 2) +v2 = Vec2(3, 4) + +print(v1 + v2) # Vec2(x=4, y=6) +print(v1 * 2) # Vec2(x=2, y=4) +print(2 * v1) # Vec2(x=2, y=4) +print(v1 < v2) # True(按 norm 比较) + +``` +##### 3.2.2.2 常用函数 +常用函数: +- 绝对值`abs()`、四舍五入`round()` + +- 幂运算`pow(x,y,m)`,先做幂运算,再取模 +等价于(x**y)%m,区别在于pow(x,y,m)会用更高效的算法,在y很大时速度和内容都好很多 + +- `divmod(a,b)`一次得到`(商,余数)`,等价`//`和`%`的组合 +```python +divmod(17, 5) # (3, 2) 因为 17 = 3*5 + 2 +q, r = divmod(17, 5) +``` + +### 3.3 布尔类型(bool) +- `True/False` 与 `1/0` 的关系(`bool` 是 `int` 的子类) +所以布尔值可以参与整数运算,`True` 在数值上等于 `1`,`False` 在数值上等于 `0`。 + +- 逻辑运算:`and/or/not` 的短路规则与返回值特点 +见3.2.2.1运算符4)逻辑运算符(短路与返回值)。 + +### 3.4 空值(NoneType) +- `None` 是一个特殊的单例对象(singleton),类型是 `NoneType`: +```python +type(None) # +``` +- 语义通常表示: + - **缺省值/未提供**(没有传参数、没有配置) + - **不存在/找不到**(查询失败、没有结果) + - **尚未初始化**(占位) + - **函数未返回值**(默认返回 None) +- 比较:用 `is None` / `is not None`(不要用 `== None`) +is 比较对象身份,语义最准确。是不是None本质是判断是否指向同一个对象。 + +### 3.5 字符串(str)与文本处理 +#### 3.5.1 str 本质与关键特性 +1)`str` 是**不可变类型**(immutable):对字符串的“修改”会生成新字符串 +2)序列类型:支持下标、切片、遍历、len()、in 判断 +#### 3.5.3 编码基础:Unicode、UTF-8(概念) +1)Python中所有的字符串都是Unicode字符串。 + +2)**Unicode**是一套字符集合+编号规则,每个“字符”分配统一编号(码点),例如: + - `"A"` 的码点是 `U+0041`,`U+`表示这是Unicode码点,`0041`是十六进制,等于十进制65 + - `"中"` 的码点是 `U+4E2D` + +3)**UTF-8**:把Unicode码点编码(例如`A`的字符编码`U+0041`)成**字节序列(bytes)**的规则(可变长度 1~4 字节)。 + +4)**字节序列**是一个字符(或一段文本)在存储/传输时真正落到介质的那串8位一组的数据(也就是很多个byte)。不是我看到的字形,而是文件里/内存里/网络包里面的原始数据。 + +5)进一步理解: +我看见的字符`"A"`,Unicode给字符`"A"`分配的统一编号(码点)为`U+0041`,按照UTF-8编码规则,转换成对应的字节序列是`0x41`。 +我们说的 0x41、65、01000001 只是不同的“写法”(十六进制/十进制/二进制文本表示),本质上是同一份比特。 + +6)再进一步理解: + - 磁盘/网络层:只有bytes(字节序列) + - 文本抽象层:Unicode码点(字符是谁) + - 显示层:字体渲染成你看到的字形(glyph) +同一个字符在不同字体下长的不一样,但是码点一样; +同一个码点用UTF-8/UTF-16编出来的字节序列也不一样。 + +7)业务场景 +- 编码(Unicode 字符 → UTF-8 字节)场景: +当你把字符串写到文件/网络/数据库时会发生,比如: + • 你保存一个.md 文件为 UTF-8 + • print() 输出到终端(终端也需要字节流) + • 发 HTTP 请求 body +这时会把字符 A (U+0041) 编成字节 0x41。 +- 解码(UTF-8 字节 → Unicode 字符)场景: +当你从文件/网络读入字节并要当作文本使用时会发生,比如: + • 打开文件读取内容 + • 浏览器收到网页字节流并显示 + • 程序读取 socket 的数据并当作字符串处理 +这时把字节 0x41 解成字符 A (U+0041)。 +```python +s = "A中" +b = s.encode("utf-8") # str -> bytes +print(b) # b'A\xe4\xb8\xad' +# b的类型是字节串(bytes),print(b)时,Python会把这些原始字节用一种“人能读/能复制回去”的方式显示出来: +# 可打印的ASCII字节(0x20~0x7E)会直接显示成字符 +# 其他不可打印/非ASCII的字节会用转义形式显示:\xHH(HH为两位十六进制) +# 即"中"对应的3个字节分别为 0xE4 0xB8 0xAD +s2 = b.decode("utf-8") # bytes -> str +print(s2) # A中 +``` + +#### 3.5.3 常用操作:切片、拼接、查找、替换、分割、join +##### 3.5.3.1 切片(slice) +- 基本:s[start:stop:step](左闭右开) +- 常用写法: +s[:n] 前 n 个 +s[n:] 从 n 到结尾 +s[-n:] 最后 n 个 +s[::-1] 反转(Unicode 字符层面反转;对“人类视觉字符”如某些组合字符/emoji 可能不符合直觉) + +```python +str = 'Runoob' +print(str[0:-1]) # 'Runoo' +print(str[0]) # 'R' 其实也是索引 +print(str[2:5]) # 'noo' +print(str[2:]) # 'noob' +print(str[:2]) # 'Ru' +print(str[::-1]) # 'boonuR',默认步长为1,步长为-1时会反转 +print(str[-1::-1]) # 'boonuR',默认步长为1,截取内的参数[start,end,step],当start为-1,end为空时,默认从右到左 +``` +- 易错点 + - 切片不会越界报错(比索引更安全) +```python +s = "abc" +print(s[:10]) # 'abc' 不报错 +# print(s[10]) # IndexError +``` + - 下标为负时 +依旧左闭右开,例:s = "abcdef": +下标: 0 1 2 3 4 5 +字符: a b c d e f +负下标: -6-5-4-3-2-1 + +##### 3.5.3.2 拼接(concat) +小量拼接:+ / += 可用 +多段拼接(尤其循环):用 ''.join(parts)(性能更好) +```python +a = "hello" +b = "world" +print(a + " " + b) # 'hello world' +``` + +##### 3.5.3.3 查找(search) +常见三套:in / find / index +- "sub" in s:只关心“有没有”(返回 bool) +- s.find("sub"):返回起始下标,找不到返回 -1(不抛异常) +- s.index("sub"):找不到抛 ValueError(适合“必须存在”的逻辑) +```python +s = "hello world" +print("wor" in s) # True +print(s.find("wor")) # 6 +print(s.find("xxx")) # -1 +# print(s.index("xxx")) # ValueError +``` +补充:计数与位置 +s.count(sub):出现次数 +s.rfind(sub) / s.rindex(sub):从右找 + +##### 3.5.3.4 替换(replace) +- s.replace(old, new[, count]):返回新字符串 +补充:count最多替换count次匹配到的old,不写默认替换所有匹配。 +```python +s = "a-b-c-b" +print(s.replace("-", "_")) # 'a_b_c_b' +print(s.replace("b", "B", 1)) # 'a-B-c-b' +``` +- 易错点:替换是“按字面文本”,不是正则;需要模式匹配用,例如要把所有的数字替换成#,replace做不到,要用正则替换:re.sub(pattern, repl, string, count=0) +pattern 是正则表达式,比如 r"\d+" 表示“一个或多个数字” +```python +import re + +s = "a12b003c" +print(re.sub(r"\d+", "#", s)) # 'a#b#c' (把连续数字段替换) +``` +##### 3.5.3.4 格式化:f-string / format / % +- f-string(首选) +可读性强、速度快(一般场景) +- 常用格式: + - 保留小数:{x:.2f}、{:+.2f}(带+符号2位小数) + - 宽度对齐:{s:>10}(右对齐)、{:0>10d}(右对齐10位,不够补0)、{s:<10}(左对齐)、` + - 千分位:{n:,} + - 百分比:{ratio:.2%}(自动 ×100 + %) + - b、d、o、x 分别是二进制、十进制、八进制、十六进制。 +```python +name = "Tom" +score = 93.456 +n = 1234567 + +print(f"{name=} {score:.2f}") # name='Tom' 93.46 +print(f"{n:,}") # 1,234,567 +print(f"{0.1234:.1%}") # 12.3% +``` + +- str.format() +旧项目常见;功能同样强 +`print("name={0}, score={1:.2f}".format("Tom", 93.456))` + +- %(了解即可) +`print("score=%.2f" % 93.456)` +易错点:别拿 % 去拼很复杂的对象,维护性差。 + + +##### 3.5.3.5 转义/原始字符串 r''、多行字符串 +- 转义字符: +\n 换行、\t 制表、\\ 反斜杠、\' \" 引号 +- 原始字符串 r'': +作用:让反斜杠不再当转义(常用于正则、Windows 路径) +```python +path = r"D:\learnforlive\python\20260106" +# 如果不加r其中的\l \p \202 都会被认作转义字符,而且\p这些还会报语法错误 +print(path) +``` +易错点(重要):原始字符串不能以奇数个反斜杠结尾(会把结尾引号“逃逸”) +```python +# bad = r"C:\temp\" # 语法错误 +ok = r"C:\temp" + "\\" +``` +- 多行字符串(三引号) +常用于 docstring(文档字符串,Python 里写在模块、函数、类、方法定义处的第一条字符串字面量,用来给代码写说明文档) 或大段文本 +```python +text = """第一行 +第二行""" +``` + +##### 3.5.3.6 去除空白 strip/lstrip/rstrip +- 默认去掉两端空白(空格、\n、\t 等都算空白) +- 传参时不是“去掉某个子串”,而是“去掉这些字符集合” +```python +s = " hello \n" +print(s.strip()) # 'hello' + +x = "==abc==" +print(x.strip("=")) # 'abc' +``` +- 易错点(字符集合坑): +```python +s = "abcba" +print(s.strip("ab")) # 'c'(去掉两端所有 a/b 字符,不是去掉 "ab" 子串) +``` +##### 3.5.3.7 判断开头结尾 startswith/endswith +- 支持元组:一次判断多个前缀/后缀 +```python +filename = "report.csv" +print(filename.endswith((".csv", ".txt"))) # True +``` + +##### 3.5.3.8 大小写转换:lower/upper/title/casefold +- lower 全部转小写 +- upper 全部转大写 +- title 每个“词”的首字母大写,对缩写、撇号、连字符、特殊词形可能不符合直觉。 +- casefold 更激进、更适合比较的大小写折叠,要做不区分大小的相等比较,优先: +```python +def ieq(x: str, y: str) -> bool: + return x.casefold() == y.casefold() +``` +- swapcase() 大小写反转 +##### 3.5.3.9 分割:split +- s.split(sep=None,maxsplit=1) +- 返回列表,列表元素是分割出来的子串 +- sep是分割标识,默认按照任意空白分割,连续空白会被当成一个分隔,同时会自动去掉两端空白的影响 +```python +s = " a b\tc \n" +print(s.split()) # ['a', 'b', 'c'] +``` +- 常规用法,指定特定的分隔符: + - 分隔符会被精确匹配 + - 连续分隔符会产生空字符串 + - 开头/结尾是分隔符也会产生空字符串 +```python +csv = "tom,18,beijing" +print(csv.split(",")) # ['tom', '18', 'beijing'] + +s = "a,,b," +print(s.split(",")) # ['a', '', 'b', ''] +``` +- maxsplit表示最多分割多少次,默认全部分割,maxsplit=1:最多切一刀(得到最多 2 段),常用于“只想分成两段”的场景 +- rsplit():从右侧开始分割,常用于只切最后一段(文件扩展名、最后一个路径分隔符等) +- 高级用法:s.partition(sep):返回三元组 (head, sep, tail),只分割第一次出现的 sep。rpartition同理,从右往左找。 +```python +csv = "tom,18,beijing" +s = "key=value=more" +print(s.partition("=")) # ('key', '=', 'value=more') +print(s.rpartition("=")) # ('key=value', '=', 'more') +``` + - 找不到分隔符时的行为(很好用) +partition 找不到:返回 (s, '', '') + - 适用场景:只想切成“两段”(key/value、前缀/后缀、文件名/扩展名等),并且希望:总是得到固定 3 段结构(不用判断 list 长度)。“分隔符是否存在”也能从中间那段 sep 是否为空直接判断。 + +##### 3.5.3.10 判断字符串形态:isdecimal / isdigit / isnumeric / isalpha / isalnum / isspace +- 数字判断(从严格到宽松) + - isdecimal():十进制数字(最严格;常用于“输入必须是十进制整数”) + - isdigit():数字字符(包含一些“上标数字”等) + - isnumeric():最宽(还包含一些“罗马数字、中文数字”等数值字符),适用于“各种数字符号都算数字”的情况 + - 补充:判断输入的情况,更多直接用try:int()/float(),因为int()/float()能处理很多isxxx() 覆盖不到的情况,例如: + - 前后空白:" 12 " + - 正负号:"-3", "+7" + - 小数/科学计数法(float):"3.14", "1e-3" +```python +def to_int(s: str): + try: + return int(s) # 自动允许前后空白、+/- 号 + except ValueError: + return None + +def to_float(s: str): + try: + return float(s) # 支持 3.14 / 1e-3 / +inf / nan 等 + except ValueError: + return None + +print(to_int(" -12 ")) # -12 +print(to_int("3.14")) # None(int 不能直接解析小数) +print(to_float("3.14")) # 3.14 +print(to_float("1e-3")) # 0.001 +``` + +##### 3.5.3.11 连接:join/+ +1)`join`把多个字符串高效拼起来(推荐): +- 语法:`sep.join(iterable_of_str)` +- 含义:用 `sep` 作为分隔符,把一组字符串连接成一个新字符串 +- 适用:循环拼接、大量拼接、构造 CSV/日志/多段文本 +```python +parts = ["a", "b", "c"] +s = ",".join(parts) # "a,b,c" +s2 = "".join(parts) # "abc" +``` +- 易错点:待拼接元素必须都是 `str`** +```python +nums = [1, 2, 3] +",".join(map(str, nums)) # "1,2,3" +# ",".join(nums) # TypeError +``` + +2)`+`适合少量拼接 +- `a + b` 会生成新字符串;少量拼接可读性高 +- 但在循环里反复 `+=` 容易产生大量临时字符串(性能差) +```python +# 少量:OK +s = "hello" + "," + "world" + +# 大量:用 join 更好 +parts = [] +for i in range(10000): + parts.append(str(i)) +s = "".join(parts) +``` + + +##### 3.5.3.11 映射:maketrans / translate +1)用途:批量“字符级替换/删除” +- `replace()`:更像“子串替换”(old/new 是字符串片段) +- `translate()`:更像“按字符表逐个替换”,对单字符映射特别高效 +2)用法: +- `str.maketrans(...)`:生成翻译表(table) + - 操作方式1:两个等长字符串:一一映射 +```python +table = str.maketrans("abc", "123") +print("a-b-c".translate(table)) # "1-2-3" +``` + - 操作方式2:用 dict 指定映射(更灵活) +```python +table = {ord("你"): "您", ord("吗"): ""} # 删除可用映射到空字符串 +# 为什么用ord +# 因为str.translate(table) 最终需要的是“码点(int) → 替换内容”的表(这是它的规范)。 +# 你可以用两种方式得到这个表: +# ·自己写 ord(...)(手工把字符转码点) +# ·用 str.maketrans(...)(它会自动把字符键转成 ord 后的整数键) +print("你好吗".translate(table)) # "您好" +``` +- `translate(table)`:按表替换 +- 字符逐个处理:看到某字符就查表替换,映射值可以是: + - 字符串(替换成该字符串) + - `None`(删除该字符) + - 整数(映射到另一个字符的码点) +##### 3.5.3.12 字符串常量 +要先`import string` +- `string.ascii_letters`:包含所有英文字母(大小写)。 +`abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ` +- `string.digits`:包含所有数字字符(0-9)。 +`0123456789'` +- `string.punctuation`:包含所有标点符号。 +`!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~` + +用途:输入校验/过滤 +```python +import string + +s = "a1_b!" +allowed = set(string.ascii_letters + string.digits + "_") +ok = all(ch in allowed for ch in s) # 是否只包含字母数字下划线 +# all这个语法啥意思? +# all(iterable) 是内置函数:当且仅当 iterable 里所有元素都为真值(truthy)时返回 True;只要有一个为 False 就返回 False(而且会短路停止继续检查)。 +``` + +##### 3.5.3.13 其他方法 len / max / min +1)`len(s)`:字符串长度(字符个数,不是字节数) +```python +len("中") # 1 +len("中文") # 2 + +# 字节长度要看编码后的 bytes +len("中".encode("utf-8")) # 3 +``` +2)`max(s)` / `min(s)`:按字符的“码点顺序”比较 +- 会在字符串中找“最大的字符”和“最小的字符” +- 比较规则是 Unicode 码点顺序,并非“字典序/拼音序/大小写无关” +- 适用:简单字符范围判断;不适合做自然语言排序 +```python +max("abc") # 'c' +min("abc") # 'a' + +min("aA") # 'A'(通常大写字母码点更小) +max("aA") # 'a' +``` +**想忽略大小写比较:配合 `key=str.lower`(或 `str.casefold` 更强)** +```python +max("aA", key=str.lower) # 'a' 或 'A'(取决于实现与稳定性,但比较基于 lower 后的值) +``` +- ##### 3.5.3.14 str.lower、str.lower()、x.lower、x.lower()有啥区别? +实质上是两个问题: + - “属性/方法本身” vs “调用方法得到结果” +- str(类型) vs x (某个字符串对象) + +- str.lower +str是字符串类型(class),lower是这个类型上的一个方法对象(函数/描述符)。str.lower表示拿到这个方法本身。 +`print(str.lower) # ` +- str.lower() +一般会报错,原因在于lower是实例方法,调用时需要一个具体的字符串(例如“ABC”)。 +需要手工传入一个字符串当做self才行,例如: +`str.lower("ABC") # 等价于 "ABC".lower() ` +- x.lower +x是一个具体的字符串对象,例如x = "Hello" +x.lower表示:从这个对象上取出绑定方法(bound method),仅是取出,不会执行 +`print("Hello".lower) # ` + +x.lower和str.lower的区别: +- x.lower已经绑定了self=x,也就是准备好了要对谁做lower +- str.lower还没绑定具体对象(没self) + +- x.lower() +这才是真正执行了转小写操作:返回一个新字符串,把x转为小写 +x本身不变(字符串不可变) + +注意:str是类型名,代表字符串这种类型,就像int代表整数类型。 + + + +### 3.6 字节序列(bytes / bytearray)(建议加一节,很多人漏) +- `str` vs `bytes` 的区别 +- 编码/解码:`.encode()` / `.decode()` +- 使用场景:文件、网络、二进制数据 + +### 3.7 序列类型(Sequence):list / tuple / range +字符串和元组都属于序列(Sequence)类型,如下图所示: +Python有6个序列的内置类型,最常见的是列表和元组。 +```python +object + └── collections.abc.Iterable + └── collections.abc.Sequence + ├── list(可变序列) + ├── tuple(不可变序列) + └── str(不可变序列,但元素必须是字符) +``` +#### 3.7.1 list(列表) +##### 3.7.1.1 列表含义 +- 有序(保持插入顺序) +- 可变(mutable):可原地增删改 +- 可存放任意类型对象(可混合) +- 支持重复元素 + +```python +a = [1, "x", 3.14, [1, 2]] +``` +##### 3.7.1.2 创建方式 +- 字面量:`[]` +- `list(iterable)`:把可迭代对象转成列表 +- 列表推导式(常用) +```python +a = [] +b = list("abc") # ['a', 'b', 'c'] +c = [i*i for i in range(5)] # [0, 1, 4, 9, 16] +``` + +##### 3.7.1.3 访问:索引与切片 +- `a[i]` / `a[-1]`:按索引取值 +- `a[start:stop:step]`:切片返回新列表,同字符串 +```python +a = [0, 1, 2, 3, 4] +a[0] # 0 +a[-1] # 4 +a[1:4] # [1, 2, 3] +a[::-1] # [4, 3, 2, 1, 0] +``` +##### 3.7.1.4 列表增 +- `append(x)`:末尾追加一个元素 +- `extend(iterable)`:把 iterable 里的元素逐个加入(相当于拼接) +- `insert(i, x)`:在索引 i 处插入(整体后移) +```python +a = [1, 2] +a.append(3) # [1, 2, 3] +a.extend([4, 5]) # [1, 2, 3, 4, 5] +a.insert(1, 99) # [1, 99, 2, 3, 4, 5] +``` +- 常见坑(append vs extend): +```python +a = [1, 2] +a.append([3, 4]) # [1, 2, [3, 4]] +a.extend([3, 4]) # [1, 2, 3, 4] +``` + +- 拼接多列表也可以用itertools.chain +```python +from itertools import chain + +list(chain.from_iterable([[1, 2], [3, 4], [5]])) # [1, 2, 3, 4, 5] +``` +##### 3.7.1.5 列表删 +- `pop(i=-1)`:删除并返回索引 i 的元素(默认末尾) +- `remove(x)`:删除第一个等于 x 的元素(找不到抛 ValueError) +- `clear()`:清空列表 +- `del a[i]` / `del a[i:j]`:删除指定位置/切片 +```python +a = [10, 20, 30, 20] +a.pop() # 20,a -> [10, 20, 30] +a.remove(20) # a -> [10, 30](只删第一个 20) +a.clear() # a -> [] +``` +##### 3.7.1.6 列表改:索引赋值 / 切片赋值 +- `a[i] = v`:改单个元素 +- `a[i:j] = iterable`:切片替换/插入/删除(很灵活) +```python +a = [0, 1, 2, 3, 4] +a[0] = 99 # [99, 1, 2, 3, 4] +a[1:3] = [7, 8, 9] # [99, 7, 8, 9, 3, 4](长度可变) +a[2:4] = [] # [99, 7, 3, 4](相当于删除) +``` +##### 3.7.1.7 列表查:in / index / count +- `x in a`:是否包含 +- `a.index(x[, start[, end]])`:返回首个匹配索引(找不到抛 ValueError) +- `a.count(x)`:计数 +```python +a = [1, 2, 2, 3] +2 in a # True +a.index(2) # 1 +a.count(2) # 2 +``` +##### 3.7.1.8 遍历与常见模式 +- 直接遍历元素:`for x in a` +- 遍历索引:`for i in range(len(a))`(不推荐优先) +- 同时拿到索引和值:`enumerate(a)` +```python +for i, x in enumerate(["a", "b"]): + print(i, x) # i是索引,x是列表元素 +``` +##### 3.7.1.9 排序:`sort()` vs `sorted()`(重点) +- `list.sort()`:**原地排序**,返回 `None` +- `sorted(iterable)`:返回**新列表**,原对象不变(可用于任何可迭代对象) + +```python +a = [3, 1, 2] +a.sort() +print(a) # [1, 2, 3] + +b = [3, 1, 2] +c = sorted(b) +print(b) # [3, 1, 2] +print(c) # [1, 2, 3] +``` +- 常用参数: + - `key=`:指定排序关键字(非常常用)。key是一个函数,该函数会作用于可迭代对象的每个元素,返回一个值,再以这个值对可迭代对象中的元素进行排序。 + - 按字符串长度排序:key=len + - 按绝对值排序:key=abs + - 忽略大小写排序:key=str.lower + - 按元组的某个元素排序:key=lambda x: x[1] + - `reverse=`:默认为`False`,即为默认升序(从小到大)。`reverse=True`的时候是降序(从大到小)。字母以及特殊字符的降序规则基于字符的 ASCII/Unicode 值。例如升序情况下,a(ASCII值为97)大于A(ASCII值为65),A排在a之前 +```python +words = ["Bob", "alice", "Tom"] +sorted(words, key=str.lower) # 忽略大小写排序 + +items = [{"age": 18}, {"age": 3}] +sorted(items, key=lambda d: d["age"]) +``` +```python +# 按“元组的第 2 个元素”(下标 1)排序 +data = [("Tom", 18), ("Alice", 16), ("Bob", 20), ("Eve", 18)] + +# 方式1:返回新列表(推荐) +sorted_data = sorted(data, key=lambda x: x[1]) +print(sorted_data) +# [('Alice', 16), ('Tom', 18), ('Eve', 18), ('Bob', 20)] +``` + + +- 稳定性(了解):Python 排序是**稳定排序** +即 key 相同的元素排序后相对顺序不变(利于多关键字排序) +```python +data = [("Tom", 18), ("Eve", 18), ("Bob", 20), ("Alice", 18)] + +# 按年龄排序(key 相同的 18 有三个),这里的key是排序依据 +sorted_data = sorted(data, key=lambda x: x[1]) +print(sorted_data) +# [('Tom', 18), ('Eve', 18), ('Alice', 18), ('Bob', 20)] +``` +此时,年龄都是18的三个人,在原列表内是Tom -> Eve -> Alice,排序后仍然是Tom -> Eve -> Alice,这就叫稳定。基于此,可以先排次要关键字,再排序主要关键字。(符合常识) + +##### 3.7.1.10 复制:引用/浅拷贝(高频坑) +1)`b = a` 不是复制,是“同一个对象的两个名字”(引用) +- `a`、`b` 指向同一份列表对象 +- 修改其中一个,会影响另一个 +```python +a = [1, 2, 3] +b = a +b.append(99) +print(a) # [1, 2, 3, 99] +print(b) # [1, 2, 3, 99] +print(a is b) # True +``` + +2)浅拷贝(shallow copy)是什么? +浅拷贝会创建一个**新的外层列表对象**,但元素仍然是原来那些元素对象的引用。 +常见浅拷贝方式: +- `a.copy()` +- `a[:]` +- `list(a)` + +```python +a = [1, 2, 3] +b = a.copy() # a和b指向不同的对象,但是如果有子对象,子对象还是同一个(引用) + +print(a is b) # False(外层对象不同) +``` + +对“元素是不可变类型”的列表来说,浅拷贝通常足够: +```python +a = [1, 2, 3] +b = a[:] +b[0] = 100 +print(a) # [1, 2, 3] +print(b) # [100, 2, 3] +``` +3)高频坑:嵌套结构(内层仍共享引用) +如果列表里装的是可变对象(如子列表、字典),浅拷贝只复制外层,**内层对象仍然共享**。 +```python +a = [[1], [2], [3]] +b = a.copy() + +print(a is b) # False(外层不同) +print(a[0] is b[0]) # True(内层同一个对象) + +b[0].append(99) +print(a) # [[1, 99], [2], [3]] (a 也变了) +print(b) # [[1, 99], [2], [3]] +``` + +原因:`b[0]` 和 `a[0]` 指向同一个子列表对象。 + +4)深拷贝(deep copy):嵌套结构要真正“全复制” +当你需要把嵌套结构全部复制(包含内层对象也复制),用 `copy.deepcopy()`: + +```python +import copy + +a = [[1], [2], [3]] +b = copy.deepcopy(a) + +b[0].append(99) +print(a) # [[1], [2], [3]] +print(b) # [[1, 99], [2], [3]] +print(a[0] is b[0]) # False +``` +5)经验总结(复习版) +- `b = a`:同一对象两个引用(改一个,另一个也变) +- 浅拷贝:复制外层容器(外层不同;内层元素引用可能共享) +- 深拷贝:递归复制整棵结构(内外都独立,但更耗时/更耗内存) + +##### 3.7.1.11 常用内置函数配合 +1)`len(a)`返回序列中元素个数 +2)`min(a)` / `max(a)`:最小/最大元素 +- 要求元素之间**可以比较**(同类或有可比较规则),空列表会报错 +- 对字符串是按字符的 Unicode 码点顺序比较(不是拼音/字典序意义 +- **key 参数(非常常用)** + - `min(iterable, key=...)` / `max(iterable, key=...)` + - 含义:按 key 函数计算的结果来比较,但返回原元素 +```python +words = ["Bob", "alice", "Tom"] +min(words, key=str.lower) # 'alice'(忽略大小写比较) + +people = [("Tom", 18), ("Alice", 16), ("Bob", 20)] +max(people, key=lambda x: x[1]) # ('Bob', 20)(按年龄最大) +``` +3)`sum(a[, start])`:求和 +- 常用于数值列表求和 +```python +sum([1, 2, 3]) # 6 +sum([1, 2, 3], 10) # 16(从 start=10 开始加) +``` +4)`any(iterable)`:是否“存在一个为真” +- 只要 iterable 里**有一个元素为 truthy**,就返回 True +- 否则返回 False +- 具有短路:找到第一个 truthy 就停止 + +```python +any([0, "", None, 2]) # True(2 是 truthy) +any([0, "", None]) # False +any([]) # False(空序列没有“真”) +``` +5)`all(iterable)`:是否“全部为真” +- 只有当 iterable 里**所有元素都为 truthy**,才返回 True +- 只要出现一个 falsy 就返回 False(短路) +- 对空序列:`all([]) == True`(“真空真”) + +```python +all([1, 2, 3]) # True +all([1, 0, 3]) # False(0 是 falsy) +all([]) # True +``` + +6)`list(seq)`将序列转化为列表 +seq -- 元素列表,可以是列表、元组、集合、字典,若为字典,则仅会将键(key)作为元素依次添加至原列表的末尾。 + +7)典型应用场景(很实用) +**检查字符串是否全是允许字符** +```python +import string + +s = "a1_b" +allowed = set(string.ascii_letters + string.digits + "_") +ok = all(ch in allowed for ch in s) # True/False +``` +**是否存在某类元素** +```python +nums = [3, -1, 5] +has_negative = any(n < 0 for n in nums) # True +``` + +##### 3.7.1.12 嵌套列表 +1)嵌套列表是啥? +嵌套列表就是“列表的元素还是列表”,常用来表示: +- 矩阵/二维表格(rows × cols) +- 多组数据分组 +- 树/层级结构的简单表示 +```python +matrix = [ + [1, 2, 3], + [4, 5, 6], +] +``` +2)嵌套列表访问 +- `matrix[i]` 取第 i 行(仍是 list) +- `matrix[i][j]` 取第 i 行第 j 列元素 + +```python +matrix[0] # [1, 2, 3] +matrix[0][1] # 2 +``` +3)嵌套列表遍历(最常见写法) +```python +for row in matrix: + for x in row: + print(x) +``` +同时拿索引: +```python +for i, row in enumerate(matrix): + for j, x in enumerate(row): + print(i, j, x) +``` + +4)创建二维列表的高频坑:不要用 `[[0]*m]*n` +`[[0]*m]*n` 会让 n 行**都指向同一个内层列表对象**,改一行会“全变”。 +```python +bad = [[0]*3]*2 # *在列表上做的是重复引用,不是深拷贝,将[0,0,0]*2时,实际上将同一个内存列表对象的引用复制了2分到外层列表里 +bad[0][0] = 9 +print(bad) # [[9, 0, 0], [9, 0, 0]] (两行一起变) +``` + +正确创建方式:用列表推导式,通过列表推导式的每次循环,创建一个新的内存列表对象。确保每一行是新对象 +```python +good = [[0]*3 for _ in range(2)] +good[0][0] = 9 +print(good) # [[9, 0, 0], [0, 0, 0]] +``` + +5)浅拷贝 vs 深拷贝(嵌套结构的核心坑) +- 外层浅拷贝不会复制内层:内层仍共享,改内层会互相影响 +```python +a = [[1], [2]] +b = a.copy() +b[0].append(9) +print(a) # [[1, 9], [2]] +``` + +- 要完全独立(复制内层),用深拷贝: +```python +import copy +a = [[1], [2]] +b = copy.deepcopy(a) +b[0].append(9) +print(a) # [[1], [2]] +print(b) # [[1, 9], [2]] +``` + +6)常见操作:二维数据处理技巧 +**取某一列** +```python +matrix = [[1, 2, 3], [4, 5, 6]] +col1 = [row[1] for row in matrix] # [2, 5] +``` + +**矩阵转置(行列互换)** +- 用 `zip(*matrix)`(返回迭代器,转 list/tuple 才能看到) +```python +matrix = [[1, 2, 3], [4, 5, 6]] +t = list(map(list, zip(*matrix))) # [[1, 4], [2, 5], [3, 6]] +``` +**理解上述代码`list(map(list, zip(*matrix))) `** +- `*`叫序列解包(unpacking),*matrix实际上做的是将matrix分成了2个列表[1,2,3]和[4,5,6],所以`zip(*matrix)`等价于`zip([1, 2, 3], [4, 5, 6])` +- zip(a, b, ...):把同位置元素“拉链式”配对。zip 会把多个可迭代对象按索引对齐,组成一个个元组: +```python +zip([1,2,3], [4,5,6]) +# 产生: (1,4), (2,5), (3,6) +``` +注意:zip返回的是迭代器,list(...)才会把它“展开出来显示。” +那到底迭代器是啥? + +- map(list, ...): + - map(f, iterable)可以理解为,将一个函数f一次作用到一串元素上,得到一串新的结果(迭代器)。等价于下面这种循环: +```python +result = [] +for x in iterable: + result.append(f(x)) +``` + - map(list, zip(*matrix)) 做的事情,是把 zip 产生的每个元组转成 list。 +zip 产出的每一行是元组 (1,4) 这种;如果你想要列表 [1,4],就把每个元组套一层 list(...)。 +```python +list(map(list, [(1, 4), (2, 5), (3, 6)])) +# [[1, 4], [2, 5], [3, 6]] +``` + - 最外层的list是啥作用?最外层的 list(...) 是把 map(...) 的结果一次性收集成列表,否则你拿到的是一个 迭代器对象,不会直接显示出内容,而且很多场景需要“可重复使用/可索引”的列表。也就是说,map这一步产生的还是一个迭代器。 + - 等价、更直观的写法:通常更建议用列表推导式(同样需要外层构造列表): +`t = [list(col) for col in zip(*matrix)]` + +**展开(flatten,一维化)** +```python +matrix = [[1, 2], [3, 4, 5]] +flat = [x for row in matrix for x in row] # [1, 2, 3, 4, 5] +``` +- 理解这个推导式:[x for row in matrix for x in row] +嵌套循环版本的列表推导式,语法规则是`[表达式 for 变量1 in 可迭代1 for 变量2 in 可迭代2(变量1) ...]`,它等价于把多个 for 循环按顺序写在一起(从左到右),后面的 for 写在前一个 for 的循环体里面。等价于: +```python +matrix = [[1, 2], [3, 4, 5]] + +flat = [] +for row in matrix: + for x in row: + flat.append(x) + +print(flat) # [1, 2, 3, 4, 5] +``` + - for row in matrix 在前:先“按行拿到容器” + - for x in row 在后:再“从容器里取元素” + - 最前面的 x:每次取到的元素要放进结果里 + +##### 3.7.1.13 性能/复杂度(了解但很实用) +Python 的 `list` 底层更接近“**动态数组**”(一块连续内存 + 容量自动扩张),因此在**末尾**操作很快,在**头部/中间**插入删除会慢。 +- `append`:均摊 O(1) +- `pop()`(末尾):O(1) +- `insert(0, x)` / `pop(0)`:O(n)(需要整体移动) +- 频繁头部操作建议用 `collections.deque`,`deque`(双端队列)底层是分段结构,设计目标就是两端高效进出: + - `deque.append(x)`:右端入队 O(1) + - `deque.appendleft(x)`:左端入队 O(1) + - `deque.pop()`:右端出队 O(1) + - `deque.popleft()`:左端出队 O(1) +```python +from collections import deque + +q = deque([1, 2, 3]) +q.append(4) # deque([1, 2, 3, 4]) +q.appendleft(0) # deque([0, 1, 2, 3, 4]) +q.popleft() # 0,deque([1, 2, 3, 4]) +``` + +#### 3.7.2 tuple(元组) +##### 3.7.2.1 元组的不可变性 +- 元组是**不可变类型**(immutable),一旦创建,元素的值和顺序都不能修改。 +- 不可变性带来的特点: + - **安全性**:元组的内容不会被意外修改。 + - **可哈希性**:元组可以作为字典的键(`dict key`)或集合的元素(`set`),但前提是元组的所有元素也必须是可哈希的(如数字、字符串、元组等)。 +```python +t = (1, 2, 3) +# t[0] = 99 # TypeError: 'tuple' object does not support item assignment +``` + +##### 3.7.2.2 元组的使用场景 +1)作为字典的键 +- 字典的键要求是**可哈希的**,而元组是不可变的,因此可以作为键。 +- 常用于表示多维数据、复合键等场景。 + +```python +# 元组作为字典键 +coordinates = {(0, 0): "origin", (1, 2): "point A"} +print(coordinates[(1, 2)]) # "point A" +``` +- 注意:如果元组中包含可变对象(如列表),则不能作为键,因为可变对象不可哈希。 +2)返回多个值 +- 函数可以通过元组返回多个值,避免使用多个 `return` 或复杂的结构。 +- 调用时可以直接用**多变量解包**接收返回值。 +```python +def get_point(): + return (1, 2) # 返回一个元组 + +x, y = get_point() # 解包 +print(x, y) # 1 2 +``` + +- 也可以用元组表示函数的多个输出: +```python +def calculate(a, b): + return a + b, a * b # 返回一个元组 + +result = calculate(3, 4) +print(result) # (7, 12) +``` +3)作为不可变的“轻量级容器” +- 元组适合用来存储一组逻辑上相关但不需要修改的数据。 +- 常见场景: + - 表示坐标点 `(x, y)` + - 表示 RGB 颜色 `(r, g, b)` + - 表示数据库记录的一行数据 + +4)作为序列的“只读”版本 +- 元组可以用来表示“只读”的序列,避免意外修改。 +- 例如,函数参数中传递元组,表示调用者不希望函数修改数据。 + +```python +def process(data): + # data[0] = 99 # TypeError: 'tuple' object does not support item assignment + print(data) + +process((1, 2, 3)) +``` +5)元组的常用方法(元组方法很少) +- `t.count(x)`:统计元素 `x` 在元组中出现的次数。 +- `t.index(x)`:返回元素 `x` 的第一个索引(找不到会抛 `ValueError`)。 + +```python +t = (1, 2, 3, 2) +print(t.count(2)) # 2 +print(t.index(2)) # 1 +``` + +##### 3.7.2.3 解包(unpacking)补充说明 +1)解包核心概念:把“容器里的多个元素”一次性赋值给多个变量 +- 左边写多个变量 +- 右边放一个可迭代对象(tuple/list/str/range/...) +- 元素个数必须匹配(除非用了 `*` 收集) +```python +x, y = (1, 2) # ✅ tuple 解包 +a, b = [3, 4] # ✅ list 解包 +c, d = "hi" # ✅ 字符串也是可迭代:c='h', d='i' +``` +2)打包(packing)与解包(unpacking)是一对 +```python +t = 1, 2, 3 # 打包:得到 (1, 2, 3) +a, b, c = t # 解包:拆开赋值 +``` +3)常见报错:数量不匹配(没有 `*` 时必须一一对应) +```python +# x, y = (1, 2, 3) # ValueError: too many values to unpack (expected 2) +# x, y, z = (1, 2) # ValueError: not enough values to unpack (expected 3, got 2) +``` +4)`*` 也可以用于解包(星号解包 / 扩展解包) +- `*name` 会把“多出来的部分”收集成一个 list +- 一次解包里最多只能有一个 `*name` + +```python +a, *mid, b = (1, 2, 3, 4, 5) +# a=1, mid=[2,3,4], b=5 + +first, *rest = [10, 20, 30] +# first=10, rest=[20,30] + +*head, last = "abcd" +# head=['a','b','c'], last='d' +``` + +限制: +```python +# a, *b, *c = (1, 2, 3) # SyntaxError:一个解包里不能有两个 * +``` +5)典型应用:交换变量(不需要临时变量) +```python +x, y = 1, 2 +x, y = y, x +print(x, y) # 2 1 +``` +6)`*` 在“函数调用”里也能解包参数(与赋值解包是两个常用场景) +- `f(*seq)`:把序列拆成多个位置参数 +- `f(**d)`:把字典拆成多个关键字参数(key 必须是字符串且是合法参数名) + +```python +nums = (2, 10) +print(pow(*nums)) # 等价于 pow(2, 10) + +kwargs = {"sep": "-", "end": "!\n"} +print("a", "b", "c", **kwargs) # a-b-c! +``` +注意:在函数调用里,** 表示把一个 dict 解包成关键字参数(keyword arguments)。 +等价于`print("a", "b", "c", sep="-", end="!\n")` +使用条件: +- kwargs 必须是“映射”(通常是 dict)。 +- dict 的 key 必须是字符串,并且是合法的参数名(对内置函数/普通函数都一样)。 +- key 必须对应函数支持的关键字参数,否则会报错。 + +#### 3.7.3 range +##### 3.7.3.1 range含义 +`range` 是一个**惰性(lazy)的整数序列对象**:它表示“等差数列”,**不一次性生成所有数字**,常用于 `for` 循环计数。 +- 惰性的含义: + - `range(10**9)` 也不会立刻占用巨大内存,因为它只保存:`start/stop/step` + - 真正取值是在“迭代”时逐个算出来 +```python +r = range(1_000_000_000) # 这里面的下划线是数字字面量分隔符,类似于",",只是为了数字更好度,不影响数值大小 +print(r) # range(0, 1000000000) +print(len(r)) # 1000000000(可直接算长度) +``` +> 对比:`list(range(...))` 会真的创建一个大列表,占内存很大,慎用。 + +##### 3.7.3.2 基本写法 +1)`range(stop)`:从 0 开始,到 stop-1(左闭右开) +```python +list(range(5)) # [0, 1, 2, 3, 4] +``` +2)`range(start, stop)`:从 start 开始,到 stop-1 +```python +list(range(2, 6)) # [2, 3, 4, 5] +``` +3)`range(start, stop, step)`:步长为 step(可为负) +```python +list(range(1, 10, 2)) # [1, 3, 5, 7, 9] +list(range(10, 0, -3)) # [10, 7, 4, 1] +``` +##### 3.7.3.3 常用用法(循环计数) +```python +for i in range(5): + print(i) # 0 1 2 3 4 +``` + +倒序循环: +```python +for i in range(5, 0, -1): + print(i) # 5 4 3 2 1 +``` + +步长循环: +```python +for i in range(0, 10, 3): + print(i) # 0, 3, 6, 9 +``` +##### 3.7.3.4 range 也是“序列”(可索引、可切片、可 in 判断) +```python +r = range(2, 10, 2) # 2,4,6,8 + +r[0] # 2 +r[-1] # 8 +list(r[1:3]) # [4, 6] + +6 in r # True +7 in r # False +``` +##### 3.7.3.5 易错点 +1)左闭右开(stop 不包含) +```python +list(range(1, 4)) # [1, 2, 3](不包含 4) +``` + +2)step 不能为 0 +```python +# range(1, 10, 0) # ValueError: range() arg 3 must not be zero +``` + +3)负步长时,start/stop 的方向要匹配 +```python +list(range(1, 5, -1)) # [](方向不对,结果为空) +list(range(5, 1, -1)) # [5, 4, 3, 2] +``` + +### 3.8 映射类型(Mapping):dict +#### 3.8.1 一句话总结(核心) +`dict`(字典)是“**key -> value**”的映射结构,特点是:**按 key 快速查找(通常 O(1))**,常用于存配置、做索引、统计计数、结构化数据。 + +#### 3.8.2 创建(create) +1)字面量创建 +```python +d = {"name": "Tom", "age": 18} +empty = {} # 空字典 +``` + +2)`dict(...)` 创建(常用于 key 是合法标识符) +```python +d = dict(name="Tom", age=18) +# 等价于 d = {"name":"Tom","age":18} +print(d) # {'name': 'Tom', 'age': 18} +``` +key是合法标识符的含义,指的是key必须能写成Python的变量名,否则就会报错: +- 只能包含:字母、数字、下划线 _ +- 不能以数字开头 +- 不能是关键字(如 class, def, for, None 等) + +3)从键值对序列创建 +```python +pairs = [("a", 1), ("b", 2)] +d = dict(pairs) # {'a': 1, 'b': 2} +``` + +4)字典推导式(常用) +```python +d = {x: x*x for x in range(5)} # {0:0, 1:1, 2:4, 3:9, 4:16} +``` + +#### 3.8.3 访问(read):`[]` vs `get()`(重点) +1)`d[key]` +- key 存在:返回 value +- key 不存在:抛 `KeyError`(适合“必须存在,否则就是 bug/异常”的逻辑) + +```python +d = {"a": 1} +print(d["a"]) # 1 +# print(d["x"]) # KeyError +``` + +2)`d.get(key, default=None)` +- key 存在:返回 value +- key 不存在:返回 default(默认是 None),**不抛异常** +- 适合“可选字段/可能缺失”的逻辑 + +```python +d = {"a": 1} +print(d.get("a")) # 1 +print(d.get("x")) # None +print(d.get("x", 0)) # 0 +``` + +#### 3.8.4 增/改/删(CRUD) +1)新增/修改:赋值 +```python +d = {} +d["name"] = "Tom" # 新增 +d["name"] = "Alice" # 修改 +``` + +2)删除 +- `pop(key[, default])`:删除并返回 value;缺失且无 default 会 KeyError +- `del d[key]`:缺失会 KeyError +- `clear()`:清空 + +```python +d = {"a": 1, "b": 2} +v = d.pop("a") # v=1, d -> {"b":2} +d.pop("x", None) # 不存在也不会报错,设置了default +# del d["x"] # KeyError +d.clear() # {} +``` +#### 3.8.5 遍历:`keys()` / `values()` / `items()`(重点) +```python +d = {"a": 1, "b": 2} + +for k in d: # 默认遍历 key(等价于 for k in d.keys()) + # list(d)也是把字典d迭代出来的元素收集成列表,字典默认迭代的是key + print(k) # a b + +for k in d.keys(): + print(k) # a b + +for v in d.values(): # 1 2 + print(v) + +for k, v in d.items(): # 最常用:同时拿 key/value + print(k, v) # a 1 b 2 +``` +**易错点** +- `in` 判断在 dict 上判断的是 **key** +```python +d = {"a": 1} +print("a" in d) # True +print(1 in d) # False(不会查 value) +``` +- 不能边遍历边改: +遍历 dict 的时候(遍历 d / d.keys() / d.items() 等),不要同时增删键(改变字典大小/结构),否则 Python 会报: +`RuntimeError: dictionary changed size during iteration` + +#### 3.8.6 常用操作:`update()` / `setdefault()` / `fromkeys()` +1)`update(...)`:批量更新,有同名key会被覆盖对应value +```python +d = {"a": 1, "b": 2} +d.update({"b": 99, "c": 3}) +print(d) # {'a': 1, 'b': 99, 'c': 3} +``` +2)`setdefault(key, default)` +- 如果 key 存在:返回已有 value,不改变字典 +- 如果 key 不存在:把 key 插入且 value=default,并返回 default +- 常用于“分组/聚合”初始化 + +```python +d = {} +d.setdefault("tags", []).append("python") +d.setdefault("tags", []).append("basic") +print(d) # {'tags': ['python', 'basic']} +``` +> 备注:更推荐的写法通常是 `collections.defaultdict(list)`(标准库),但 `setdefault` 在不想引入额外结构时很方便。 + + +> 备注:分组/聚合指的是:把一堆数据按某个规则(key)归类到不同桶里(分组),并在每个桶里累积结果(聚合:收集列表、计数、求和等)。setdefault 常用来在第一次见到某个 key 时自动放一个默认容器,避免手写 if key not in d: d[key]=...。 +例:分组(按年龄把人名分组) +```python +people = [("Tom", 18), ("Eve", 18), ("Bob", 20), ("Alice", 20)] + +groups = {} # age -> [names] +for name, age in people: + groups.setdefault(age, []).append(name) + # 各次循环结构: + # 第1次循环:name为Tom,age为18,18这个key在groups里面不存在,所以导入18这个key,返回[],.append方法将列表变为['Tom'],groups结果为{18:['Tom']} + # 第2次循环:name为Eve,age为18,18这个key在groups里面存在,所以返回['Tom'],.append方法将列表变为['Tom','Eve'],groups结果为{18:['Tom','Eve']} + +print(groups) +# {18: ['Tom', 'Eve'], 20: ['Bob', 'Alice']} +``` +3)`dict.fromkeys(keys, value=None)`:用一组 key 初始化字典 +```python +d = dict.fromkeys(["a", "b", "c"], 0) # {'a': 0, 'b': 0, 'c': 0} +``` +**高频坑:value 是可变对象会共享** +```python +bad = dict.fromkeys(["a", "b"], []) # 这里面的[]对应同一个对象 +bad["a"].append(1) +print(bad) # {'a': [1], 'b': [1]} (共享同一个列表) +``` + +#### 3.8.7 字典推导式(comprehension) +```python +words = ["Tom", "Alice", "Bob"] +d = {w: len(w) for w in words} # {'Tom': 3, 'Alice': 5, 'Bob': 3} +``` + +带过滤: +```python +d = {x: x*x for x in range(10) if x % 2 == 0} +``` + +#### 3.8.8 哈希与 key 的要求(可哈希对象)(重点) +1)dict 的 key 必须: +- 可哈希(`hash(key)` 能算出来) +- 且在作为 key 的生命周期内保持不变(因此通常是**不可变类型**) + +2)常见可作为 key 的类型: +- `int` / `float` / `bool` / `str` / `None` +- `tuple`(前提:tuple 内元素也都可哈希) +- `frozenset` + +```python +d = {(1, 2): "point"} # ✅ +# d = {([1,2]): "x"} # ❌ list 不可哈希:TypeError +``` +#### 3.8.9 插入顺序(Python 3.7+):dict 保持插入顺序(了解) +- 从 Python 3.7 开始,`dict` 保证按插入顺序迭代(3.6 是 CPython 实现细节) +```python +d = {"b": 2, "a": 1, "c": 3} +print(list(d.keys())) # ['b', 'a', 'c'] +``` + +#### 3.8.10 常见应用场景(写代码很常用) +1)计数(频率统计) +```python +s = "abca" +count = {} +for ch in s: + count[ch] = count.get(ch, 0) + 1 +print(count) # {'a': 2, 'b': 1, 'c': 1} +``` + +2)分组 +```python +people = [("Tom", 18), ("Eve", 18), ("Bob", 20)] +groups = {} +for name, age in people: + groups.setdefault(age, []).append(name) +print(groups) # {18: ['Tom', 'Eve'], 20: ['Bob']} +``` + +3)结构化数据(像一行记录) +```python +user = {"id": 1, "name": "Tom", "tags": ["vip", "beta"]} +``` + + +### 3.9 集合类型(Set):set / frozenset +- 去重、集合运算:并/交/差/对称差 +- 可变 set vs 不可变 frozenset +- 常见方法:`add/update/remove/discard` + +### 3.10 不可变 vs 可变(重点常见坑) +#### 3.10.1 为什么要区分可变和不可变类型? +不是拍脑袋想出来的,是为了让语言在性能、正确性、安全性以及数据结构上实现上都更好用。具体如下: +- 让“共享引用”变得可控(避免莫名其妙被改动) +Python变量本质是“名字→对象”的引用,同一个对象会被多个名字引用。 +不可变类型一旦被创建就不能修改,多个地方引用也没关系。 +可变类型可以原地修改,某一处的修改会影响其他引用者,存在风险。 +- 支撑“哈希”和“字典/集合”的底层要求 +dict和set这种高性能数据结构,依赖hash(哈希值)来快速定位元素,要求作为key的对象在其生命周期内保持不变,所以key只能用不可变类型。 +- 性能优化:不可变容易复用、更容易缓存 +不可变因为不会被修改,所以可以安全服用,解释器更敢做优化。这里面提到的缓存是什么意思? +- 让函数调用语义更清晰(“会不会改到我传进去的东西”) +Python参数传递是“传对象引用”。可变/不可变直接影响API设计: +传入可变类型(list、dict),在函数内部已改,外面也变(副作用); +传入不可变类型(int、str、tuple),函数内部“修改”的方式其实是创建新的对象,外面不变。 +```python +def f(x): + x += 1 +a = 1 +f(a) +print(a) # 还是输出1,因为int不可变,所以内部的a是对应一个新的int对象,外部的a不变 +``` +能让我在读代码时快速判断:哪些参数可能被原地改动。 + +#### 3.10.2 可变默认参数的坑 +问题:函数的默认参数只在函数定义时计算一次,即def语句执行那一刻就会创建一个函数对象,同时把默认参数表达式立刻求值一次,并把得到的那个列表对象保存到函数对象里(作为“默认值”缓存起来)。后面每次调用如果不传 lst,就会复用这一份列表对象。 +如果默认值是可变对象(如 []、{}),后续每次调用都会复用同一个对象,导致“越用越多”。 + +```python +def add_item(x, lst=[]): + lst.append(x) + return lst + +print(add_item(1)) # [1] +print(add_item(2)) # [1, 2] <-- 你可能以为是 [2],但不是 +print(add_item(3)) # [1, 2, 3] + +# 代码从上到下的顺序执行,对于函数,会先执行def add_item(x,lst=[])这一个函数定义语句,并创建一个函数对象add_item,并把默认参数lst=[]当场求值一次,把这个列表对象存到add_item.__defaults__ +# !!! 函数定义块里面的语句,在函数定义时不会执行,只在调用时执行 +# 默认参数的默认值属于函数对象的一部分:在执行 def 语句创建函数对象时,默认值表达式会被求值一次并保存。 +# 调用函数时,如果某个形参未提供实参,则该形参绑定到保存的默认值对象(引用)。 +# 若默认值是可变对象(如 list/dict),多次调用可能共享并累积修改。 +``` +原因:lst 的默认值那个 [] 只创建了一次,所有调用共享它。 + +正确写法:用 None 做默认值(不可变),在函数内部再创建新列表。 +```python +def add_item(x, lst=None): + if lst is None: + lst = [] # 这个lst会在这次调用函数后消失? + lst.append(x) + return lst +``` + +- 不可变:int/float/bool/str/tuple/frozenset/None +- 可变:list/dict/set +- “修改”与“重新绑定”的区别 +- 可变默认参数陷阱(可放到函数章节,但这里需要预告) + +### 3.11 拷贝与引用(重点) +- 赋值只是引用:`a = b` +- 浅拷贝 vs 深拷贝:`copy.copy()` / `copy.deepcopy()` +- 切片拷贝、`dict.copy()` 的浅拷贝性质 +- 循环引用与对象结构的影响(了解) + +### 3.12 比较与身份:`==` vs `is` +- 值相等(`==`)与对象身份(`is`) +- `id()` 的含义(对象标识;不是“内存地址”的承诺) +- 小整数/字符串驻留现象(了解,避免当作语义依赖) + +### 3.13 类型转换与输入输出常见点 +- 显式转换:`int()`、`float()`、`str()`、`list()`、`tuple()`、`set()` +- `input()` 永远返回 str,如何转换与错误处理 +- `repr()` vs `str()`(与调试输出关联) + +### 3.14 (可选)枚举与常量 +- `Enum` 的用途与基本用法(你笔记里已经有) +- 常量命名约定:全大写 + +### 3.15 hash值与hash算法 + +--- + +## 04 控制流 +### 4.1 分支结构:`if / elif / else` + +#### 4.1.1 一句话总结(核心) +- 分支结构用来让程序根据条件选择不同路径执行:**“满足条件就做 A,否则做 B”**。 +- `if/elif/else` 的判断依赖“真值测试(truthiness)”,不一定非得是 `True/False`。 + +--- + +#### 4.1.2 基本语法(最常用) +```python +score = 85 + +if score >= 90: + print("A") +elif score >= 60: + print("Pass") +else: + print("Fail") +``` + +要点: +- `elif` 可以有多个,也可以没有 +- `else` 可选 +- 条件从上到下判断,**命中一个分支后就不会再判断后面的分支** + +--- + +#### 4.1.3 真值测试(truthiness)在 if 里的含义(高频) +Python 在 `if cond:` 里会把 `cond` 当成“真/假”判断: +- falsy(当作 False):`None`、`0`、`0.0`、`""`、`[]`、`{}`、`set()`、`range(0)` 等空/零 +- 其他大多数对象都 truthy(当作 True) + +```python +if []: + print("不会执行") +if "hello": + print("会执行") +``` + +--- + +#### 4.1.4 常见写法:判断“是否为空/是否存在”(更 Pythonic) +```python +name = input("name: ").strip() + +if name: # name有值就为True + print("hello", name) +else: + print("匿名用户") +``` + +--- + +#### 4.1.5 链式比较(可读性强) +```python +x = 7 +if 0 < x < 10: + print("x 在 1~9") +``` +等价于:`0 < x and x < 10`,且 + +--- + +#### 4.1.6 逻辑运算与短路(在 if 条件里很常见) +- `A and B`:A 为 False 时,B 不会执行(短路) +- `A or B`:A 为 True 时,B 不会执行(短路) + +```python +user = None +# 利用短路避免报错:user 为 None 时不会执行右侧 +if user and user.get("name"): + print("has name") +``` + +--- + +#### 4.1.7 `is None`:判断空值的标准姿势(重点) +```python +x = None +if x is None: + print("x 是 None") +``` + +--- + +#### 4.1.8 常见坑:条件里用「可变对象」做默认值判断 +比如 `dict.get()` 返回 `None`,别和空列表/空字符串混在一起判断语义: +```python +d = {} +v = d.get("key") # None + +if v is None: # 表示“缺失” + ... +elif v == "": # 表示“存在但为空字符串” + ... +``` + +--- + +#### 4.1.9 条件表达式(三元表达式)(常用但别滥用) +```python +x = 10 +msg = "big" if x > 5 else "small" +``` + +建议:表达式简单时用它;复杂逻辑用普通 if/elif/else。 + +--- + +#### 4.1.11 多分支选择:字典映射替代长 elif(可选技巧) +当分支只是“输入 -> 输出”映射时,字典更清晰: + +```python +status = "done" +msg_map = {"pending": "待处理", "done": "已完成", "failed": "失败"} + +print(msg_map.get(status, "未知状态")) +``` + +如果是“状态集合 + 行为”,更推荐用 `Enum`(见 3.14)。 + +--- + +#### 4.1.12 小练习(建议) +1)输入分数(0~100),输出等级 A/B/C/D +2)输入字符串,判断是否为空;为空输出“匿名用户”,否则输出原字符串 +3)输入年龄,判断是否在 [18, 60) 区间 + + +### 4.2 循环结构`for` / `while`/`break` / `continue` / `pass` +#### 4.2.1 一句话总结(核心) +- `for`:用于**遍历可迭代对象**(列表/字符串/字典/集合/生成器/`range` 等),适合“知道要遍历什么”的场景。 +- `while`:用于**条件循环**(条件为真就一直循环),适合“不确定次数、直到满足条件为止”的场景。 +- `break`:立刻**结束当前这一层**循环。 +- `continue`:立刻**跳过本轮**,进入下一轮。 +- `pass`:**占位符**,语法需要但暂时不做任何事。 + +#### 4.2.2 `for` 循环:遍历可迭代对象(最常用) +```python +# 1) 遍历列表 +nums = [10, 20, 30] +for x in nums: + print(x) + +# 2) 遍历字符串 +for ch in "hello": + print(ch) + +# 3) 配合 range 遍历次数 +for i in range(5): # 0..4 + print(i) +``` + +要点: +- `range(n)` 生成 `0 ~ n-1`(左闭右开) +- `range(start, stop, step)` 支持起止和步长(可为负) + +```python +for i in range(10, 0, -2): # 10, 8, 6, 4, 2 + print(i) +``` + +#### 4.2.3 `while` 循环:条件为真就重复执行 +```python +i = 0 +while i < 3: + print(i) + i += 1 +``` + +要点: +- `while` 更适合“次数不固定”的逻辑(例如输入校验、重试、等待条件成立) +- 常见坑:忘记更新条件变量导致死循环 + +#### 4.2.4 `break`:提前结束循环(只影响最近一层) +```python +nums = [3, 5, 7, 9] +target = 7 + +for x in nums: + if x == target: + print("found") + break + print("checking", x) +``` + +要点: +- `break` 只能跳出**当前这一层**循环 +- 在嵌套循环里只会跳出内层;要跳出多层通常用:标记变量 / 写成函数并 `return` +- 跳出多层循环示例: +场景:二维列表里找某个目标值,一旦找到就要立刻停止所有循环。 +**写法 1:标记变量(flag)** +```python +matrix = [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], +] +target = 5 + +found = False +pos = None + +for i, row in enumerate(matrix): + for j, x in enumerate(row): + if x == target: + found = True + pos = (i, j) + break # 只能跳出内层 for + if found: + break # 再跳出外层 for + +print(found, pos) # True (1, 1),(1,1)指的是第二行第二列 +``` + +要点: +- 内层 `break` 只退出内层循环 +- 外层通过 `if found: break` 再退出一次 + +--- + +**写法 2:写成函数,用 `return`(更干净)** +```python +def find_pos(matrix, target): + for i, row in enumerate(matrix): + for j, x in enumerate(row): + if x == target: + return i, j # 直接结束函数,相当于跳出所有循环 + return None + +matrix = [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], +] +print(find_pos(matrix, 5)) # (1, 1) +print(find_pos(matrix, 10)) # None +``` + +要点: +- `return` 会终止函数执行,因此天然“跳出多层循环” +- 这种方式在实际项目里通常更推荐(逻辑更清晰、可复用) + +--- + +#### 4.2.5 `continue`:跳过本轮,进入下一轮 +```python +for i in range(1, 6): + if i % 2 == 0: + continue + print(i) # 1, 3, 5 +``` + +要点: +- `continue` 会跳过本轮后续语句,直接进入下一次迭代 + +--- + +#### 4.2.6 `pass`:占位符(什么也不做) +```python +# 1) 先占位,后续再补逻辑 +def todo(): + pass + +# 2) 分支暂时不处理 +x = 10 +if x > 0: + pass +else: + print("x <= 0") +``` + +要点: +- `pass` ≠ `continue` + - `pass`:什么都不做,但仍继续执行本轮后面的语句 + - `continue`:立刻结束本轮,开始下一轮 + +--- + +#### 4.2.7 `for/while ... else`(可选但很实用) +当循环**正常结束**(没有被 `break` 打断)时,会执行 `else`: +```python +nums = [1, 3, 5] +target = 2 + +for x in nums: + if x == target: + print("found") + break +else: + print("not found") # 没触发 break 才会执行 +``` + +要点: +- `else` 表达的是:“循环没被 break 打断时的兜底逻辑” + +--- + +#### 4.2.8 易错点(高频) +1)`for` 遍历字典默认遍历的是 key +```python +d = {"a": 1, "b": 2} + +for k in d: + print(k) + +for k, v in d.items(): + print(k, v) +``` + +2)循环中修改列表(尤其是删除)容易漏处理 +不推荐: +```python +lst = [1, -2, -3, 4] +for x in lst: + if x < 0: + lst.remove(x) # 容易漏删/跳项 +``` +漏删/跳项的原因在于, +- `for x in lst:` 的迭代本质是按“索引从 0 往后走”,你在循环中 `remove/pop/del` 会让列表**缩短并整体左移**,导致“下一个元素”被挪到当前索引位置,但循环的索引却已经准备去看下一位,于是就**跳过了**某些元素。 +正确/推荐写法: +**写法 1:列表推导式(最推荐,清晰)** +```python +lst = [1, -2, -3, 4] +lst = [x for x in lst if x >= 0] +print(lst) # [1, 4] +``` + +**写法 2:遍历副本(对原列表做修改)** +```python +lst = [1, -2, -3, 4] +for x in lst[:]: # lst[:] 是浅拷贝(副本) + if x < 0: + lst.remove(x) +print(lst) # [1, 4] +``` + +**写法 3:倒序按索引删除(不易漏)** +```python +lst = [1, -2, -3, 4] +for i in range(len(lst) - 1, -1, -1): + if lst[i] < 0: + lst.pop(i) +print(lst) # [1, 4] +``` + +3)`while True` 一定要有明确出口 +```python +while True: + s = input("输入 q 退出:").strip() + if s == "q": + break +``` +### 4.3 推导式(comprehension):列表 / 字典 / 集合(高频、写法更 Pythonic) + +#### 4.3.1 一句话总结(核心) +- 推导式用一句表达式把“**生成 + 过滤 + 转换**”写在一起:更简洁、通常也更快。 +- 常见三类:列表推导式 `[...]`、字典推导式 `{k: v ...}`、集合推导式 `{...}`。 +- 语法核心:`结果表达式 for 变量 in 可迭代对象 if 条件` + +--- + +#### 4.3.2 列表推导式(List Comprehension) +1)最基本:映射(map) +```python +nums = [1, 2, 3, 4] +squares = [x * x for x in nums] +print(squares) # [1, 4, 9, 16] +``` + +2)带过滤:筛选(filter) +```python +nums = [1, 2, 3, 4, 5, 6] +evens = [x for x in nums if x % 2 == 0] +print(evens) # [2, 4, 6] +``` + +3)映射 + 过滤(最常用组合) +```python +nums = [1, 2, 3, 4, 5, 6] +result = [x * x for x in nums if x % 2 == 0] +print(result) # [4, 16, 36] +``` + +4)嵌套推导式(扁平化 flatten) +```python +matrix = [[1, 2], [3, 4, 5]] +flat = [x for row in matrix for x in row] +print(flat) # [1, 2, 3, 4, 5] +``` + +要点: +- 推导式里 `for` 的顺序和嵌套循环一致:左边的 `for` 在外层,右边的 `for` 在内层。 +- 不要为了“炫技”写太长:超过 1~2 层嵌套或条件复杂,建议改回普通循环。 + +--- + +#### 4.3.3 字典推导式(Dict Comprehension) +1)最基本:把列表变成 dict +```python +words = ["Tom", "Alice", "Bob"] +d = {w: len(w) for w in words} +print(d) # {'Tom': 3, 'Alice': 5, 'Bob': 3} +``` + +2)过滤:只保留满足条件的键值对 +```python +scores = {"Tom": 95, "Bob": 58, "Alice": 88} +passed = {name: sc for name, sc in scores.items() if sc >= 60} +print(passed) # {'Tom': 95, 'Alice': 88} +``` + +3)key/value 交换(注意:value 必须可哈希且 key 不能重复) +```python +d = {"a": 1, "b": 2} +rev = {v: k for k, v in d.items()} +print(rev) # {1: 'a', 2: 'b'} +``` + +易错点: +- dict 的 key 不能重复;推导式里如果重复,后面的会覆盖前面的。 + +--- + +#### 4.3.4 集合推导式(Set Comprehension) +集合推导式和列表推导式很像,但结果是 set(自动去重): +```python +nums = [1, 2, 2, 3, 3, 3] +s = {x for x in nums} +print(s) # {1, 2, 3} +``` + +常用场景:去重 + 过滤/映射 +```python +names = ["Tom", "tom", "ALICE", "Alice"] +norm = {n.lower() for n in names} +print(norm) # {'tom', 'alice'} +``` + +--- + +#### 4.3.5 生成器表达式(可选补充,但很常用) +1)如果你只需要“迭代使用”,不想一次性生成列表,用生成器表达式更省内存: +```python +nums = range(10**8) +total = sum(x * x for x in nums if x % 2 == 0) # 注意这里是 () 不是 [] +``` + +要点: +- `[]` 会生成列表(占内存) +- `()` 生成的是生成器(惰性计算),适合 `sum/any/all/max/min` 这类聚合函数 +- Python没有元组推导式,`(表达式 for ... in ... if ...)`生成一个生成器 + - `(x*x for ...)` → 生成器(惰性) + - `tuple(x*x for ...)` → 真正的元组(一次性生成) + - tuple 真正的关键语法是 **逗号**,不是括号。 +```python +a = (1) # int,不是 tuple +b = (1,) # tuple,注意逗号 +print(type(a), type(b)) +``` + +2)生成器有个重要特性:只能遍历一次(会“耗尽”) +```python +gen = (x for x in range(3)) + +print(list(gen)) # [0, 1, 2] +print(list(gen)) # [] 第二次就空了,因为已经被消耗完 +``` + +要点: +- 生成器是一次性的“数据流” +- 如果你要反复使用结果,就用列表 `[]` 或 `list(gen)` 把它保存下来 + + +3)一句话总结什么时候用生成器 +- 数据量大、只需要“遍历一次/做聚合”(`sum/any/all/max/min`)→ 用生成器表达式更合适 +- 需要多次遍历、需要索引/切片、需要保存结果 → 用列表推导式更合适 + + +--- + +#### 4.3.6 易错点(高频) +1)不要在推导式里写复杂副作用(如 print、修改外部变量) +- 推导式的目标是“构造结果”,副作用会降低可读性。 + +2)嵌套推导式顺序写反会出错 +```python +# 正确:先 row,再 x +flat = [x for row in matrix for x in row] +``` + +3)当逻辑复杂时,普通循环更清晰 +- 经验:推导式最好控制在“一行可读完”的复杂度。 + +--- + +### 4.4 (可选)match-case(Python 3.10+):结构化模式匹配 + +#### 4.4.1 一句话总结(核心) +- `match-case` 是“更强的 switch”:不仅能按值匹配,还能做**解构(拆包)**、**类型/结构匹配**、**守卫条件(if)**。 +- 适合:分支多、结构清晰的“按模式分发”逻辑(解析指令、处理不同数据结构、消息路由等)。 + +--- + +#### 4.4.2 基本语法(按值匹配) +```python +cmd = "start" + +match cmd: + case "start": + print("启动") + case "stop": + print("停止") + case _: + print("未知命令") +``` + +要点: +- `case _:` 相当于默认分支 default +- 从上到下匹配,命中一个 case 就结束 + +--- + +#### 4.4.3 多值合并(`|`) +```python +status = 404 + +match status: + case 200 | 201: + print("success") + case 400 | 401 | 403 | 404: + print("client error") + case _: + print("other") +``` + +--- + +#### 4.4.4 解构匹配:匹配序列/元组/列表 +```python +point = (10, 20) + +match point: + case (0, 0): + print("origin") + case (x, 0): + print("on x-axis:", x) + case (0, y): + print("on y-axis:", y) + case (x, y): + print("point:", x, y) +``` + +要点: +- `case (x, y)` 会把元素绑定到变量 `x/y`(相当于解包) +- 这类绑定变量只在 case 分支内部有效(作用域类似普通代码块) + +--- + +#### 4.4.5 字典匹配:按 key 结构匹配(相当于更强的if-elif) +```python +data = {"type": "login", "user": "Tom"} + +match data: + case {"type": "login", "user": user}: + print("login:", user) + case {"type": "logout", "user": user}: + print("logout:", user) + case _: + print("unknown message") +``` + +要点: +- 字典匹配关注“结构”:需要哪些 key、这些 key 的值是什么/绑定到谁 +- 额外的 key 不会导致匹配失败(只要包含所需 key) +- 直观理解:match-case 把“结构检查 + 取值绑定”合成一步,更像在说:“如果 data 长这样:{type=login 且有 user},那就把 user 拿出来处理” + +--- + +#### 4.4.6 守卫条件(`case ... if ...`) +```python +age = 17 + +match age: + case x if x < 0: + print("invalid") + case x if x < 18: + print("minor") + case _: + print("adult") +``` + +要点: +- `if` 是守卫条件:模式匹配成功后还要满足 if 才算命中 + +--- + +#### 4.4.7 易错点(高频) +1)`match-case` 不是 `if-elif` 的简单替代 +- 它更适合“按模式分发”,普通布尔条件判断用 `if-elif` 更直观。 + +2)注意“大小写变量绑定”语义 +- 在 `case x:` 里,`x` 通常会被当作“变量绑定”,而不是常量比较。 +**详解:** +在 `match-case` 里: + - `case "start":` 这种 **字面量** → 表示“拿来比较的常量”,只在值等于 `"start"` 时匹配。 + - `case x:` 这种 **裸名字(bare name)** → 通常不是拿来比较的常量,而是表示“把当前值绑定给变量 x”,因此它几乎**总会匹配成功**(相当于默认分支,但还能把值保存到 x)。 +**例子:`case x:` 会“吞掉一切”** +```python +value = "stop" + +match value: + case "start": + print("start") + case x: + print("catch all:", x) # 会执行:catch all: stop +``` +解释: + - `case x:` 的含义是“把 value 绑定到变量 x”,所以只要走到这里,就会匹配并执行。 + - 这也是为什么它经常被用来写“兜底分支”,类似 `case _:`,只不过 `x` 能拿到原值。 + - 兜底更建议用`case _:` + +- 常量一般直接写字面量(如 `"start"`, `404`)或用限定名(如 `SomeEnum.A`)。 + - 实战建议:常量用字面量/Enum,避免写 `case SOME_NAME:` 这种造成歧义的形式。 + - `case "xxx" / 123 / True / None:`:常量比较 + - `case EnumType.MEMBER:`:常量比较(推荐) + - 如果用`case EnumType:`,`EnumType`也是会被当做变了绑定 + +3)版本要求 +- 需要 Python 3.10+,否则语法错误。 + + +--- + +## 05 函数与作用域(重点) +### 5.1 函数基础:定义 / 返回值 / docstring + +#### 5.1.1 一句话总结(核心) +- 函数(function)用来把一段逻辑**封装成可复用的“工具”**:输入(参数)→ 处理 → 输出(返回值)。 +- 函数让代码更清晰:减少重复、方便测试、方便维护。 +- Python 函数可以返回任何对象,也可以返回多个值(实际上是返回一个元组)。 + +--- + +#### 5.1.2 函数的定义(define) +基本语法: +```python +def add(a, b): + return a + b +``` + +要点: +- `def` 定义函数,函数名建议用小写+下划线(snake_case):`get_user_name` +- 参数写在括号里,冒号 `:` 后面是函数体 +- 函数体必须缩进(一般 4 个空格) + +--- + +#### 5.1.3 调用函数(call) +```python +def greet(name): + print("hello", name) + +greet("Tom") # hello Tom +``` + +要点: +- 只有在“调用”时函数体才会执行 +- 函数名本身是一个对象(可赋值给变量) +```python +def f(x): + return x + 1 + +g = f +print(g(10)) # 11 +``` + +--- + +#### 5.1.4 返回值(return)机制(重点) +1)`return` 的作用 +- 结束函数执行 +- 把结果返回给调用者 + +```python +def square(x): + return x * x + +y = square(3) +print(y) # 9 +``` + +2)如果没有写 `return` 会怎样? +- Python 默认返回 `None` +```python +def foo(): + x = 1 + 2 + +print(foo()) # None +``` + +3)`return` 后面的语句不会执行(提前结束) +```python +def test(): + print("A") + return 123 + print("B") # 永远不会执行 + +print(test()) # A \n 123 +``` + +4)返回多个值(本质是返回 tuple) +```python +def divmod2(a, b): + q = a // b + r = a % b + return q, r # 返回的是一个元组 + +q, r = divmod2(10, 3) +print(q, r) # 3 1 +``` + +要点: +- `return q, r` 等价于 `return (q, r)` +- 调用侧可以用解包:`q, r = ...` + +--- + +#### 5.1.5 `print` 和 `return` 的区别(高频) +- `print(...)`:把内容输出到控制台(副作用),不等于“返回值” +- `return ...`:把结果交给调用者(可继续计算/保存) + +```python +def f1(x): + print(x * 2) + +def f2(x): + return x * 2 + +a = f1(10) +b = f2(10) + +print(a) # None(因为 f1 没 return) +print(b) # 20 +``` + +经验: +- 业务函数尽量 `return`,打印留给最外层(或日志) + +--- + +#### 5.1.6 docstring(文档字符串):写给人和工具看的说明(重点) +docstring 是函数体的第一段字符串,用于说明函数用途、参数、返回值等。 +```python +def add(a, b): + """Return the sum of a and b.""" + return a + b +``` + +要点: +- docstring 会被工具读取(IDE 提示、自动文档、`help()`、`__doc__`) +```python +print(add.__doc__) +help(add) +``` + +--- + +#### 5.1.7 docstring 推荐写法(简洁实用) +常用结构(简洁版): +```python +def clamp(x, low, high): + """Clamp x to the inclusive range [low, high]. + + Args: + x: Value to clamp. + low: Lower bound. + high: Upper bound. + + Returns: + The clamped value. + """ + if x < low: + return low + if x > high: + return high + return x +``` + +要点: +- 第一行一句话说明用途(动词开头更清晰) +- 复杂函数再写 Args/Returns +- 保持简短、准确、与代码一致 + +--- + +#### 5.1.8 易错点(高频) +1)函数只定义不调用,不会执行 +```python +def hello(): + print("hi") + +# hello # 只是名字 +hello() # 才会执行 +``` + +2)把 `print` 当返回值 +- 需要结果参与计算时,必须用 `return` + +3)忘记处理 `None` +- 没写 return 或某些分支没 return → 可能返回 None,引发后续 bug + +--- + +### 5.2 参数系统(重点):位置参数 / 关键字参数 / 默认参数 / `*args` / `**kwargs` + +#### 5.2.1 一句话总结(核心) +- 参数传参方式主要两类:**位置参数**(按顺序)与 **关键字参数**(按名字)。 +- 默认参数让调用更方便,但要注意:**默认值只在函数定义时计算一次**,可变对象会导致“共享坑”。 +- `*args` 用来接收多余的位置参数(变长参数),`**kwargs` 用来接收多余的关键字参数。 + +--- + +#### 5.2.2 位置参数(positional arguments) +位置参数靠“顺序”绑定到形参: +```python +def power(base, exp): + return base ** exp + +print(power(2, 3)) # base=2, exp=3 -> 8 +``` + +要点: +- 位置参数最直观,但可读性可能不如关键字参数(尤其是参数多时) + +--- + +#### 5.2.3 关键字参数(keyword arguments) +关键字参数靠“参数名”绑定,顺序可以打乱: +```python +def power(base, exp): + return base ** exp + +print(power(exp=3, base=2)) # 8 +``` + +要点: +- 关键字参数可读性强:看到调用就知道含义 +- 常用于参数较多的函数或可选参数较多的函数 + +--- + +#### 5.2.4 混用规则(高频) +```python +def f(a, b, c): + return a, b, c + +print(f(1, 2, c=3)) # ✅ 位置在前,关键字在后(必须) +# print(f(a=1, 2, 3)) # ❌ 语法错误:关键字参数后面不能再跟位置参数 +``` + +要点: +- **位置参数必须在前,关键字参数必须在后** +- 同一个参数不能被赋值两次: +```python +# f(1, a=2, c=3) # ❌ TypeError: a got multiple values +``` + +--- + +#### 5.2.5 默认参数(default arguments) +默认参数让调用更方便: +```python +def greet(name, msg="hello"): + return f"{msg}, {name}" + +print(greet("Tom")) # hello, Tom +print(greet("Tom", "hi")) # hi, Tom +``` + +要点: +- 默认参数是“可选参数”,不传就用默认值 +- 默认参数一般放在后面(否则调用会不自然) + +--- + +#### 5.2.6 默认参数的坑:可变默认参数(超级高频) +**关键原因:默认值只在函数定义时计算一次**,后续所有调用共享同一个默认对象。 + +错误示例(会越积越多): +```python +def add_item(x, lst=[]): + lst.append(x) + return lst + +print(add_item(1)) # [1] +print(add_item(2)) # [1, 2] <-- 共享同一个 lst +print(add_item(3)) # [1, 2, 3] +``` + +正确写法(用 `None` 作为哨兵值): +```python +def add_item(x, lst=None): + if lst is None: + lst = [] + lst.append(x) + return lst + +print(add_item(1)) # [1] +print(add_item(2)) # [2] +``` + +要点: +- 默认参数如果是 `[] / {} / set()` 这类可变对象,基本都要警惕共享问题 +- 推荐模式:默认值用 `None`,函数内创建新对象 + +--- + +#### 5.2.7 可变参数:`*args`(接收变长位置参数) +`*args` 会把多出来的位置参数打包成一个 tuple: +```python +def total(*args): + return sum(args) + +print(total(1, 2, 3)) # 6 +print(total()) # 0 +``` + +要点: +- `args` 只是惯例名字,你也可以写 `*nums`,但一般用 `args` +- 在函数体内,`args` 是 tuple(不可变) + +--- + +#### 5.2.8 可变参数:`**kwargs`(接收变长关键字参数) +`**kwargs` 会把多出来的关键字参数打包成一个 dict: +```python +def show(**kwargs): + return kwargs + +print(show(a=1, b=2)) # {'a': 1, 'b': 2} +``` + +要点: +- `kwargs` 只是惯例名字(keyword arguments) +- 在函数体内,`kwargs` 是 dict + +--- + +#### 5.2.9 `*args` + `**kwargs` 混用(常见签名) +```python +def demo(a, b=0, *args, **kwargs): + print("a:", a) + print("b:", b) + print("args:", args) + print("kwargs:", kwargs) + +demo(1, 2, 3, 4, x=10, y=20) +# a: 1 +# b: 2 +# args: (3, 4) +# kwargs: {'x': 10, 'y': 20} +``` + +要点: +- 形参顺序(记忆版): + - 普通参数 → 默认参数 → `*args` → `**kwargs` + +--- + +#### 5.2.10 调用时的解包:`*` 和 `**`(非常常用) +把序列/元组拆成位置参数: +```python +def add(a, b): + return a + b + +pair = (2, 3) +print(add(*pair)) # 等价于 add(2, 3) +``` + +把 dict 拆成关键字参数: +```python +def greet(name, msg="hello"): + return f"{msg}, {name}" + +d = {"name": "Tom", "msg": "hi"} +print(greet(**d)) # 等价于 greet(name="Tom", msg="hi") +``` + +要点: +- `**d` 的 key 必须是字符串且是合法参数名,并且函数能接收这些参数 +- 不匹配会报错(多/少参数都会报) + +--- + +#### 5.2.11 易错点(高频) +1)位置参数必须在关键字参数前 +2)可变默认参数共享 +3)`**kwargs` 并不会“自动忽略错误参数” +- 只有当函数签名允许 `**kwargs` 接收多余关键字参数时才行,否则会 TypeError + +--- + +#### 5.2.12 其他知识点 +1)在函数参数列表里出现单独的 `*`,表示:**从 `*` 后面的参数开始,只能用“关键字参数”传入**,不能再用位置参数传。 +```python +def log(msg, *, level="INFO"): + print(f"[{level}] {msg}") +``` + +这里: +- `msg`:可以用位置参数传,也可以用关键字传 +- `level`:因为在 `*` 后面,所以**必须用关键字传** + +### 5.3 作用域(Scope)与名字查找:LEGB / `global` / `nonlocal` + +#### 5.3.1 一句话总结(核心) +- 作用域决定“名字(变量名)从哪里找、能不能改”。 +- Python 查找名字遵循 **LEGB**:Local → Enclosing → Global → Built-in。 +- 在函数里**给某个名字赋值**时,默认它是“局部变量”;想修改外层变量需要 `global`(全局)或 `nonlocal`(闭包外层)。 + +--- + +#### 5.3.2 LEGB 规则(名字查找顺序) +当你在代码里写 `name`,Python 会按下面顺序找它是谁: + +1)**L(Local)局部作用域**:当前函数内部定义的名字 +2)**E(Enclosing)外层函数作用域**:嵌套函数的外层函数里定义的名字(闭包) +3)**G(Global)全局作用域**:当前模块(.py 文件)顶层定义的名字 +4)**B(Built-in)内置作用域**:`len`、`print`、`sum` 等内置名字 + +示例(看查找顺序): +```python +x = "global" + +def outer(): + x = "enclosing" + + def inner(): + x = "local" + print(x) # local + + inner() + +outer() +``` + +--- + +#### 5.3.3 局部变量的高频坑:赋值会让名字变“局部” +很多人会遇到这种错误: +```python +x = 10 + +def foo(): + print(x) # 你以为会打印 10 + x = 20 # 但这里对 x 赋值了 + +foo() +``` + +会报错: +- `UnboundLocalError: local variable 'x' referenced before assignment` + +原因: +- 只要函数体里出现对 `x` 的赋值语句(`x = ...`),Python 就把 `x` 认定为**局部变量**。 +- 于是 `print(x)` 在使用局部 x,但局部 x 还没赋值,就报错。 + +--- + +#### 5.3.4 `global`:声明“我要用/改的是全局变量” +当你想在函数里修改模块级变量时,用 `global`: +```python +count = 0 + +def inc(): + global count + count += 1 + +inc() +inc() +print(count) # 2 +``` + +要点: +- `global` 影响的是“名字绑定(赋值)”,不是读取: + - 读全局变量不需要 `global` + - 要“在函数里对全局变量赋值/重绑定”才需要 `global` + +不推荐滥用: +- 全局变量会让代码难以维护、难以测试 +- 更建议:把状态作为参数传入,或用类封装状态 +举例: + + + +--- + +#### 5.3.5 `nonlocal`:声明“我要用/改的是外层函数变量(闭包)” +`nonlocal` 用于嵌套函数里修改外层函数的变量(E:Enclosing 作用域): +```python +def make_counter(): + n = 0 + + def inc(): + nonlocal n + n += 1 + return n + + return inc + +counter = make_counter() # 将make_counter函数的执行结果,也就是inc函数赋值给counter +print(counter()) # 1 +print(counter()) # 2 +``` + +要点: +- `nonlocal` 只能用于“外层函数”作用域(enclosing) +- 不能用来指向全局;全局要用 `global` +- 如果外层没有这个名字,会报错: + - `SyntaxError: no binding for nonlocal 'n' found` + +--- + +#### 5.3.6 `global` vs `nonlocal` 如何区分(记忆法) +- **想改模块顶层变量** → `global` +- **想改外层函数变量(闭包)** → `nonlocal` + +--- + +#### 5.3.7 易错点(高频) +1)“读”可以跨作用域,“写/赋值”默认是局部 +- 读全局:可以直接读 +- 写全局:必须 `global` +- 写外层函数变量:必须 `nonlocal` + +2)不要用变量名覆盖内置函数名(影响 B:Built-in) +```python +list = [1, 2, 3] +# list("abc") # ❌ TypeError,因为 list 被你覆盖成列表对象了 +``` + +--- +#### 5.3.8 为什么不推荐滥用全局变量?(以及更好的替代方式) + +1)全局变量的问题是什么? +全局变量让函数“偷偷依赖外部状态”,会带来这些麻烦: +- **不好维护**:函数的行为不只取决于参数,还取决于外部全局变量当前是什么值。 +- **不好测试**:写单元测试时,你得先把全局变量初始化成某个值,测完还要恢复,不然测试互相影响。 +- **容易出 bug**:多个地方都能改同一个全局变量,调用顺序不同,结果就不同。 + +--- + +2)坏例子:用全局变量保存状态(不推荐) +```python +# 全局状态 +count = 0 + +def inc(): + global count + count += 1 + return count + +def reset(): + global count + count = 0 + +print(inc()) # 1 +print(inc()) # 2 +reset() +print(inc()) # 1 +``` + +问题: +- `inc()` 的结果依赖外部 `count` 当前是多少 +- 如果别的地方也在改 `count`,你很难定位问题 + +--- + +3)替代方案 A:把状态作为参数传入(纯函数风格,最易测试) +```python +def inc(count): + return count + 1 + +def reset(): + return 0 + +count = 0 +count = inc(count) # 1 +count = inc(count) # 2 +count = reset() # 0 +count = inc(count) # 1 +print(count) +``` + +优点: +- `inc(count)` 输出只由输入决定(更可控) +- 测试很简单:给定输入,断言输出即可 + +--- + +4)替代方案 B:用类封装状态(面向对象,适合复杂状态) +```python +class Counter: + def __init__(self): + self.count = 0 + + def inc(self): + self.count += 1 + return self.count + + def reset(self): + self.count = 0 + +c1 = Counter() +print(c1.inc()) # 1 +print(c1.inc()) # 2 + +c2 = Counter() +print(c2.inc()) # 1 # 互不影响,因为是不同对象的状态 +``` + +优点: +- 状态跟着对象走,不会“全局共享” +- 可以创建多个实例,互不影响(这在工程里很常见) + +--- + +5)一句话选择建议 +- 状态很简单、流程函数式:优先“参数传入/返回新状态” +- 状态多、需要很多相关操作:用“类封装状态” +- 只有确实需要“全局唯一”的配置/常量(例如 `API_URL`)才考虑全局变量(且尽量只读) + +#### 5.3.9 其他知识点 + +### 5.4 高级特性(进阶但高频):闭包 / 装饰器 / 迭代器与生成器(`yield`)/ lambda(了解) + +#### 5.4.1 一句话总结(核心) +- **闭包(closure)**:函数“记住”并携带外层作用域变量(Enclosing),形成可持久化状态。 +- **装饰器(decorator)**:`@decorator` 等价于 `func = decorator(func)`,给函数“无侵入”加功能。 +- **迭代器(iterator)**:实现迭代协议的对象,能被 `next()` 逐个取值直到 `StopIteration`。 +- **生成器(generator)/ `yield`**:用 `yield` 写“按需产出”的迭代器(惰性、可暂停继续)。 +- **lambda**:匿名小函数表达式,适合短逻辑(常见在 `key=`),复杂逻辑别用。 + +--- + +#### 5.4.2 闭包(closure) +##### 5.4.2.1 闭包的含义 +函数在返回/离开定义它的作用域后,仍然“记住并能使用”它定义时外层作用域里的变量。可以理解为: +`闭包 = 内部函数 + 它捕获的外层变量(自由变量)` +##### 5.4.2.2 闭包的主要用途 +A. 保留状态(stateful function) +函数调用结束后,外层变量本来应该销毁;闭包会把它“留住”,下次还能继续用(你笔记里的计数器就是典型)。 + +B. 固化配置(partial application) +把一部分参数先“预填好”,得到一个更专用的新函数(如固定前缀、固定系数、固定路径、固定超时等)。 + +C. 隐藏实现细节(封装) +外部只能通过返回的函数进行操作,内部变量外面拿不到(比全局变量干净)。 + +##### 5.4.2.3 最小示例:计数器(闭包 = 函数 + 外层变量) +```python +def make_counter(): + n = 0 + + def inc(): + nonlocal n + n += 1 + return n + + return inc + +counter = make_counter() +print(counter()) # 1 +print(counter()) # 2 +``` + +要点: +- `inc` 返回后依然能访问/修改外层的 `n`(这就是“记住”状态)。 + +**解释:为什么make_counter()结束后,n还会存在?** +- make_counter() 结束后,它的局部作用域会结束,但里面的局部变量对象(这里是 n)是否销毁取决于:还有没有引用指向它。 +- 在闭包里,inc这个内部函数(赋值给了counter,counter一直存在),引用了外层变量n,所以n并没有“无人引用”,不会被销毁。 +- 在没有任何引用指向这个闭包函数(以及它的closure)时,例如`counter = None`,此时inc可能被回收,其持有的closure也会被回收,n才会随之释放。 + +##### 5.4.2.4 常见用途 +- 固定部分参数(配置固化) +```python +def make_adder(k): + def add(x): + return x + k + return add + +add10 = make_adder(10) +print(add10(5)) # 15 +``` + +##### 5.4.2.5 易错点:循环变量闭包“晚绑定” +1)错误示例(调用时 i 已变成循环结束值): +```python +funcs = [] +for i in range(3): + funcs.append(lambda: i) + +print([f() for f in funcs]) # [2, 2, 2] +``` +- 等价于: +```python +funcs = [] +for i in range(3): + def f(): + return i # 注意:这里用的是外层的 i + funcs.append(f) + +print([func() for func in funcs]) +``` +- 关键点:f 里没有自己的 i,它引用的是外层循环的那个 i。 +- 为什么输出是 [2, 2, 2]? +- 执行过程拆解: + - 循环 3 次,把 3 个函数丢进 funcs + - 循环结束后,外层变量 i 的最终值是 2 + - 你最后才调用这 3 个函数:它们都会去读同一个外层 i + - 所以都读到 2 +**这就是“晚绑定”:绑定的是变量名 i,不是当时的数值。** +错误原因:闭包捕获循环变量是按名字引用,调用时才取值 → 循环结束后都变成最后值 + +2)修复:用默认参数把当前 i “绑定住” +```python +funcs = [] +for i in range(3): + funcs.append(lambda i=i: i) + +print([f() for f in funcs]) # [0, 1, 2] +``` +- 这里 lambda i=i: i 的含义是: + - 这个 lambda 有一个参数也叫 i + - 它的默认值是外层当次循环的 i + - 默认参数在函数定义时就会求值并保存(这点和笔记里“可变默认参数坑”是同一个机制,只是这里我们反过来利用它) + +等价于: +```python +funcs = [] +for i in range(3): + def f(i=i): # 把当前 i 作为默认值保存下来 + return i + funcs.append(f) + +print([f() for f in funcs]) # [0, 1, 2] +``` +--- +#### 5.4.3 装饰器(decorator) + +##### 5.4.3.1 装饰器本质 +- 装饰器就是一个“**接收函数 → 返回新函数**”的函数。 +- `@trace` 等价于:`add = trace(add)` + 也就是:把原函数 `add` 传给 `trace`,返回一个新函数(wrapper),并用 `add` 这个名字指向新函数。 + +最小示例:打印调用前后 +```python +def trace(fn): # fn是预期“被装饰的原函数” + def wrapper(*args, **kwargs): # 定义内部函数wrapper + print("call:", fn.__name__) # 调用wrapper会执行这一行,fn.__name__指的是原函数的名字 + result = fn(*args, **kwargs) # 调用原函数fn,把wrapper收到的参数原样转发过去,返回值存到result + print("done:", fn.__name__) # 原函数调用结束再打印一行“done” + return result # wrapper 把原函数的返回值原样返回给调用者(不改变业务结果,只“加了日志”)。 + return wrapper # `trace(fn)` 的返回值是 `wrapper` 这个函数对象。 + +@trace +def add(a, b): + return a + b +#等价于以下两步: +#def add(a,b): +# return a + b +#add = trace(add) +``` +@trace这一块的执行过程是: +1. Python 先创建原始函数对象 `add`(内容是 `return a + b`)。 +2. 然后执行 `add = trace(add)`: + - 把“原始 add 函数对象”传入 `trace`,此时在 `trace` 内部执行`def wrapper(...)`语句,创建 `wrapper`。 + - `trace` 返回 `wrapper`。 + - 名字 `add` 被重新绑定为这个 `wrapper`。 +3. 结果:**原来那个 add 函数对象还在(被 wrapper 闭包捕获在 fn 里),但外部名字 add 指向的是 wrapper。** + +```python +print(add(2, 3)) +#输出结果是: +# call:add +# done:add +# 5 +``` +`print(add(2, 3))`这一步的执行过程是: +这里调用的 `add` 已经不是原来的 add 了,而是 `wrapper`: +1. 执行 `add(2, 3)` → 实际是在执行 `wrapper(2, 3)`。 +2. wrapper 内部依次做: + - 打印:`call: add` + - 调用原函数:`fn(2, 3)`(fn 就是原始 add)→ 计算得到 `5` + - 打印:`done: add` + - `return 5` +3. 外层 `print(...)` 把返回值 `5` 打印出来。 + +--- + +##### 5.4.3.2 装饰器的“含义”(为什么要有装饰器) +装饰器解决的问题: +当你有很多函数都需要“同一类额外功能”(横切关注点),比如: +- 统一日志打印 / 记录参数 +- 统计耗时(性能监控) +- 权限校验 +- 重试(网络请求/IO) +- 缓存(避免重复计算) +- 参数校验/类型检查 +如果每个函数里都手写一遍这些“附加逻辑”,会: +- 大量重复代码 +- 业务逻辑和附加逻辑混在一起,难维护 +装饰器的意义:**不改/少改原函数代码,在外层统一“包一层”加功能**(无侵入增强)。 + +--- + +##### 5.4.3.3 常见用例(典型场景) +1)统一计时(统计函数耗时) +```python +import time +from functools import wraps + +def timer(fn): + @wraps(fn) + def wrapper(*args, **kwargs): + t0 = time.perf_counter() + # time.perf_counter() 用来做性能计时的函数:返回一个高精度、单调递增的时间计数值(单位:秒,float)。 + try: + return fn(*args, **kwargs) + finally: + t1 = time.perf_counter() + print(f"{fn.__name__} took {t1 - t0:.6f}s") + return wrapper + +@timer +def work(): + s = 0 + for i in range(1000000): + s += i + return s + +work() +``` +备注: +`@wraps(fn)` 是标准库 `functools.wraps` 提供的**装饰器**,作用是:把“被装饰的原函数 `fn`”的一些**元信息**复制到你写的 `wrapper` 上,让 `wrapper` “看起来像” 原来的函数。 +让装饰后的函数在日志、IDE、help、调试时仍然显示为原函数,更易维护。 + + +如果不加 `@wraps(fn)`,装饰之后: + +- `work.__name__` 会变成 `"wrapper"`(而不是 `"work"`) +- `work.__doc__` 会变成 `wrapper` 的 docstring(通常是 `None`) +- `help(work)` / IDE 提示 / 调试栈信息会更难看懂 + + +2)简单权限校验(调用前检查) +```python +from functools import wraps + +def require_admin(fn): + @wraps(fn) + def wrapper(user, *args, **kwargs): + if user.get("role") != "admin": + raise PermissionError("admin only") + return fn(user, *args, **kwargs) + return wrapper + +@require_admin +def delete_user(user, uid): + return f"deleted {uid}" + +admin = {"name": "Tom", "role": "admin"} +guest = {"name": "Eve", "role": "guest"} + +print(delete_user(admin, 100)) +# print(delete_user(guest, 100)) # PermissionError +``` + +3)缓存(避免重复计算) +```python +from functools import wraps + +def simple_cache(fn): + cache = {} + @wraps(fn) + def wrapper(*args): + if args in cache: + return cache[args] + cache[args] = fn(*args) + return cache[args] + return wrapper + +@simple_cache +def fib(n): + if n < 2: + return n + return fib(n - 1) + fib(n - 2) + +print(fib(30)) # 会明显更快 +``` + +--- + +##### 5.4.3.4 带参数的装饰器(装饰器工厂) +带参数装饰器 = “再多包一层”,用来生成不同配置的装饰器,比如 `@retry(times=3)`、`@rate_limit(qps=10)`。 + +```python +def retry(times): + def deco(fn): + def wrapper(*args, **kwargs): + last_err = None + for _ in range(times): + try: + return fn(*args, **kwargs) + except Exception as e: + last_err = e + raise last_err + return wrapper + return deco + +@retry(times=3) # 重复三次 +def unstable(): + ... +``` + +--- + +##### 5.4.3.5 易错点:装饰后函数名 / docstring 丢失(用 `functools.wraps`) +不加 `wraps` 会导致 `__name__ / __doc__` 变成 wrapper 的: +```python +from functools import wraps + +def trace(fn): + @wraps(fn) + def wrapper(*args, **kwargs): + print("call:", fn.__name__) + return fn(*args, **kwargs) + return wrapper +``` + +要点: +- `@wraps(fn)` 会把原函数的元信息复制到 wrapper 上(IDE/调试/文档更友好)。 +- 也能让报错栈、日志里显示的函数名更准确。 + + +--- + +#### 5.4.4 迭代器与生成器(`yield`) + +##### 5.4.4.1 先区分 3 个概念:Iterable / Iterator / Generator +- **可迭代对象(Iterable)**:能被 `for` 遍历(通常实现 `__iter__`),例如 `list/str/dict/range`。 +- **迭代器(Iterator)**:能 `next()`,并且 `iter(it) is it` 为 True(iter(迭代器对象)必须返回它自己,也就是同一个跌大气对象)。 +为什么要这样规定? +因为 for 循环内部本质上会做: +1.it = iter(obj) +2.不断 next(it) 取值 +如果传进来的本来就是迭代器,那么 iter(it) 直接返回自己,循环就能直接用它继续迭代。 +- **生成器(Generator)**:一种“用 `yield` 写出来的迭代器”。 + +##### 5.4.4.2 迭代器最直观的样子:`iter()` + `next()` +```python +it = iter([10, 20, 30]) +print(next(it)) # 10 +print(next(it)) # 20 +print(next(it)) # 30 +# next(it) # StopIteration +``` + +##### 5.4.4.3 `yield`:把函数变成“可暂停的迭代器” +```python +def count_up_to(n): + i = 1 + while i <= n: + yield i # 产出一个值,并暂停在这里 + i += 1 + +gen = count_up_to(3) +print(next(gen)) # 1 +print(next(gen)) # 2 +print(next(gen)) # 3 +# next(gen) # StopIteration +``` + +要点: +- 遇到 `yield` 会“产出一个值 + 暂停”;下次继续从暂停处往下跑。 +- 生成器是惰性的:不遍历就不计算,适合大数据流式处理。 + +##### 5.4.4.4 `return` 在生成器里表示“结束” +```python +def g(): + yield 1 + return + yield 2 # 永远到不了 + +print(list(g())) # [1] +``` + +##### 5.4.4.5 典型应用场景 +- 大文件/大数据逐行处理、无限序列、管道式数据处理(边产生边消费),避免一次性占内存。 + +--- + +#### 5.4.5 lambda(了解) + +##### 5.4.5.1 一句话总结(核心) +- `lambda` 是 **匿名函数表达式**:用一行快速创建一个“小函数”,常用于把一个简单逻辑当作参数传给别的函数(最常见是 `key=`)。 +- 语法:`lambda 参数: 表达式` + 只能写一个**表达式**(expression),表达式的结果就是返回值。 + +--- + +##### 5.4.5.2 lambda 的含义与解释(和 def 对照) +lambda 不是新语法糖的“新函数类型”,它创建的仍然是普通函数对象,只是没有名字(默认名为 `""`)。 + +```python +# lambda 版本 +f = lambda x: x + 1 # 这时候,f是一个函数对象 +print(f(10)) # 11 + +# 等价 def 版本 +def f2(x): + return x + 1 +print(f2(10)) # 11 +``` + +要点: +- `lambda x: x + 1` 等价于“定义一个函数,参数是 x,返回 x+1” +- lambda 里写的表达式会被自动“return”,不需要写 return + +--- + +##### 5.4.5.3 常见用法(最常见:排序/最值的 key) +1)排序:`sorted(..., key=...)` +```python +data = [("Tom", 18), ("Alice", 16), ("Bob", 20)] +print(sorted(data, key=lambda x: x[1])) # 按年龄排序 +# [('Alice', 16), ('Tom', 18), ('Bob', 20)] +``` + +2)按字典字段排序(字典元素很常见) +```python +users = [{"name": "Tom", "age": 18}, {"name": "Alice", "age": 16}] +print(sorted(users, key=lambda d: d["age"])) +# [{'name': 'Alice', 'age': 16}, {'name': 'Tom', 'age': 18}] +``` + +3)多关键字排序(返回 tuple) +```python +data = [("Tom", 18), ("Bob", 18), ("Alice", 16)] +print(sorted(data, key=lambda x: (x[1], x[0]))) # 先按年龄,再按名字 +# [('Alice', 16), ('Bob', 18), ('Tom', 18)] +``` + +4)配合 `min/max` 找最大/最小(非常常用) +```python +people = [("Tom", 18), ("Alice", 16), ("Bob", 20)] +print(max(people, key=lambda x: x[1])) # ('Bob', 20) +``` + +--- + +##### 5.4.5.4 什么时候别用 lambda(重要) +lambda 适合“短逻辑、一次性”。出现下面情况建议用 `def`: +- 逻辑复杂(需要多步处理/多条件) +- 需要注释解释 +- 需要复用(多个地方都要用同一个逻辑) +- 需要更好地调试/测试(lambda 名字是 ``,报错栈不直观) + +示例:用 def 更清晰 +```python +def by_age(item): + return item[1] + +print(sorted(data, key=by_age)) +``` + +--- + +##### 5.4.5.5 易错点(高频) + +1)lambda表达式本身就是函数对象,有以下三种调用方式: +- 先赋值给变量再调用(最常见) +```python +f = lambda x: x + 1 +print(f(10)) # 11 +``` +- 不赋值,直接“定义并立刻调用”(IIFE写法) +```python +print((lambda x: x + 1)(10)) # 11 +# 注意要有括号:(lambda ...)(...) +``` +- 作为参数传给别的函数,让别的函数在内部调用 +```python +data = [("Tom", 18), ("Alice", 16)] +print(sorted(data, key=lambda x: x[1])) +``` +这里没有把 lambda 赋值给变量,但 sorted 会在排序过程中调用它。 + +2)闭包“晚绑定”(lambda 放在循环里非常容易踩坑) +问题:lambda 捕获的是变量 `i` 的“名字”,不是当次循环的值;等调用时才取值;循环结束后 `i` 变成最后值,所以全部一样。 + +错误示例: +```python +funcs = [] +for i in range(3): + funcs.append(lambda: i) + +print([f() for f in funcs]) # [2, 2, 2] +``` + +修复:用默认参数把当次 i “绑定/冻结”住(默认值在函数定义时就会保存) +```python +funcs = [] +for i in range(3): + funcs.append(lambda i=i: i) + +print([f() for f in funcs]) # [0, 1, 2] +``` + +3)lambda 里只能写表达式,不能写语句块 +下面这些不能直接写进 lambda(它们是语句):`for/while/try/except/return/print(作为语句块的一部分)/赋值语句` 等。 +(注意:`print(...)` 本身是函数调用表达式,虽然能写,但通常不建议把“副作用”塞进 lambda。) + +4)`key=` 里访问字段要注意 KeyError(数据不齐时) +```python +users = [{"name": "Tom"}, {"name": "Alice", "age": 16}] +# lambda d: d["age"] # 可能 KeyError +print(sorted(users, key=lambda d: d.get("age", 0))) # 缺省给 0 +``` + +5)别把复杂逻辑硬塞进一行(可读性急剧下降) +比如大量嵌套三元表达式、复杂短路判断等:能跑,但不推荐;用 `def` 更清晰。 + +--- + +##### 5.4.5.6 小结(复习版) +- `lambda` = 一行匿名函数:`lambda 参数: 表达式` +- 最常见用途:`sorted/min/max(..., key=lambda ...)` +- 复杂逻辑/需要复用/需要注释:用 `def` +- 循环里创建 lambda 要小心“晚绑定”,用 `lambda i=i: i` 固定住当次值 + + +--- + +## 06 模块 / 包与导入机制 + +### 6.1 模块(module)与包(package) + +#### 6.1.1 一句话总结(核心) +- **模块(module)**:一个 `.py` 文件就是一个模块,用来组织代码(变量/函数/类),方便复用与分层。 +- **包(package)**:一个“包含多个模块的目录”,用于组织更大的代码结构;通常目录里会有 `__init__.py`(让该目录成为包,并可做包级初始化)。 +- **导入(import)**:把模块/包里的名字加载到当前程序中;第一次导入会执行模块顶层代码,后续会使用缓存(`sys.modules`)。 + +--- + +### 6.2 包结构(package layout)是什么 + +#### 6.2.1 最小包结构(传统写法:有 `__init__.py`) +示例目录: +```text +myproj/ + main.py + mypkg/ + __init__.py + utils.py + mathx.py +``` + +含义: +- `mypkg/` 是一个包 +- `mypkg/utils.py` 是包里的一个模块 +- `mypkg/mathx.py` 是包里的另一个模块 + +导入方式: +```python +from mypkg import utils +from mypkg.utils import some_func +import mypkg.mathx as mx +``` + +--- + +### 6.3 `__init__.py` 是什么?有什么用? + +#### 6.3.1 作用 1:把目录标记为“包”(历史 & 现实) +- **历史上(Python 2/早期 Python 3)**:没有 `__init__.py` 就不能当包导入。 +- **现在(Python 3.3+)**:存在“命名空间包”(namespace package),某些场景下即使没有 `__init__.py` 也能导入。 +- 但在日常项目里:**建议仍然保留 `__init__.py`**,结构更明确,工具支持更好,行为更可控。 + +#### 6.3.2 作用 2:包级初始化代码(import 该包时会执行) +当你执行: +```python +import mypkg +``` +会发生: +1. Python 找到 `mypkg` 包目录 +2. 执行 `mypkg/__init__.py` 顶层代码(只在首次 import 时执行) +3. 得到 `mypkg` 这个包对象(可理解为一个模块对象) + +注意: +- **不建议在 `__init__.py` 里写太重的逻辑**(比如连接数据库、发请求),否则“导入就有副作用”。 + +#### 6.3.3 作用 3:控制“包对外暴露哪些 API”(常用) +你可以在 `__init__.py` 里把常用对象“提升到包级”,让使用者更方便: + +```python +# mypkg/__init__.py +from .utils import parse_config +from .mathx import add +# (1)这两句如果不写,用户在导入模块时只能 +# from mypkg.text_utils import normalize +# from mypkg.stats import word_count +# (2)隐含效果:触发其他模块加载 +# 执行import mypkg时,Python会先执行mypkg/__init__.py,于是这两句导入会把名字放到 mypkg 这个包对象上: +# mypkg.normalize 可用 +# mypkg.word_count 可用 +# 因此用户可以更简洁: +from mypkg import normalize, word_count +# 或 +import mypkg +mypkg.normalize(" hi ") +mypkg.word_count("hi there") +# 这就是“对外暴露公共 API”:外部只需要记住 mypkg,不用关心内部模块拆分细节。 + + +__all__ = ["parse_config", "add"] +# 影响from mypkg import * 能导入的范围 +``` + +这样用户就能: +```python +from mypkg import add, parse_config # __init__.py里面不写,导入就会报错 +``` + +说明: +- `__all__` 主要影响 `from mypkg import *`(不推荐在生产代码里使用 `*`),但它也能作为“包公共接口”的说明文档。 + +#### 6.3.4 作用 4:组织子包、提供默认导入(可选) +例如: +```text +mypkg/ + __init__.py + core/ + __init__.py + service.py +``` + +导入: +```python +from mypkg.core.service import Service +``` + +--- + +### 6.4 `__init__.py` 里建议/不建议写什么(经验) +✅ 建议: +- 定义 `__version__` +- 统一导出常用 API(薄薄一层) +- 轻量常量、类型别名(typing) +- 设置 `__all__` + +❌ 不建议: +- 大量 import(会导致导入慢、循环依赖风险上升) +- 有明显副作用的代码(写文件、网络请求、启动线程等) + +--- + +### 6.5 常见坑:循环导入(circular import)(提示) +当 A import B,B 又 import A,会遇到 “partially initialized module” 之类问题。 + +常见避免方式: +- 把公共依赖抽到第三个模块(比如 `common.py`) +- 延迟导入:在函数内部 import(只在需要时导入) +- 减少 `__init__.py` 里做“全家桶导入” + +--- + +### 6.6 小例子:从包中导入 vs 直接跑模块 +建议用“模块方式运行”以保证相对导入稳定: +```bash +python -m mypkg.utils +# -m 是把后面的东西当做模块来运行 +# mypkg.utils是模块名,等价于文件mypkg/utils.py +``` +它做的事(直觉版): +- 从当前工作目录出发,把 `mypkg` 当作一个包 +- 在“包上下文”里运行 `mypkg/utils.py` +- 因此 **相对导入**(比如 `from .mathx import add`)能正常工作(这里面的.指的就是相对导入) + + +而不是: +```bash +python mypkg/utils.py # 这个是当脚本跑,如果脚本在包内部,且用了相对导入一般会报错 +``` + +原因: +- 直接运行子模块时,包上下文可能丢失,导致相对导入失败(`attempted relative import...`)。 + +### 6.7 导入方式:绝对导入 / 相对导入 + +#### 6.7.1 一句话总结(核心) +- **绝对导入**:从“项目根/顶层包名”出发写全路径,最清晰、最推荐。 +- **相对导入**:从“当前包内位置”出发用 `.` / `..` 导入,只能在包内模块使用,适合包内重构与内部引用。 +- 导入能否成功,核心取决于:**Python 的导入搜索路径 `sys.path` 里有没有“顶层包所在目录”**。 + +--- + +#### 6.7.2 先给一个目录结构(方便理解) +```text +myproj/ + main.py + mypkg/ + __init__.py + mathx.py + utils.py + core/ + __init__.py + service.py +``` + +--- + +#### 6.7.3 绝对导入(absolute import) +含义:从顶层包名开始写完整路径。 + +示例: +```python +# 在 main.py 里(项目入口) +from mypkg.utils import hello +from mypkg.core.service import Service +``` + +优点: +- 可读性强:一眼知道从哪里来 +- 不受当前文件位置影响,重构更稳 +- 工具(IDE/类型检查)支持更好 + +缺点: +- 包名写长一些(但通常值得) + +--- + +#### 6.7.4 相对导入(relative import) +含义:在包内部模块之间导入,用 `.` 表示当前包,`..` 表示上一级包。 + +规则(记忆版): +- `from .x import y`:从**当前包**的 `x` 模块导入 `y` +- `from ..x import y`:从**上一级包**的 `x` 模块导入 `y` +- 只能用于 `from ... import ...` 这种形式(相对 import 基本不用 `import .x`) + +示例:`mypkg/utils.py` 想用同包的 `mathx.py` +```python +from .mathx import add +``` + +示例:`mypkg/core/service.py` 想用上一级包的 `utils.py` +```python +from ..utils import hello +``` + +优点: +- 包内移动文件/重构时更方便(相对路径不一定变) +- 明确表达“这是包内部依赖” + +缺点 / 易错点: +- 如果你把包内模块当脚本直接运行(例如 `python mypkg/utils.py`),相对导入常会失败 +- 读者有时需要结合目录结构才能理解 `..` 到底指哪里 + +--- + +### 6.8 `sys.path` 与导入搜索路径(重点) + +#### 6.8.1 `sys.path` 是什么? +`sys.path` 是一个列表:**Python 导入模块时会按顺序在这些目录里找**。 + +你可以打印看看: +```python +import sys +print(sys.path) +``` + +#### 6.8.2 Python 导入大致怎么找? +当你执行: +```python +import mypkg +``` + +Python 会在 `sys.path` 的每个目录里查找: +- 是否存在 `mypkg/`(包目录)或 `mypkg.py`(模块文件) +- 找到就加载(并缓存到 `sys.modules`),否则继续找下一个路径 +- 都找不到就报:`ModuleNotFoundError` + +#### 6.8.3 `sys.path` 里通常有哪些东西?(直觉版) +常见包含: +1. **当前运行脚本所在目录**(非常关键) +2. 环境变量 `PYTHONPATH` 里配置的目录(如果你配了) +3. 标准库目录 +4. site-packages(你 pip 安装第三方库的地方) + +#### 6.8.4 为什么 `python -m mypkg.utils` 更稳定? +因为 `-m` 会以“模块方式”运行,让 Python 更容易建立正确的包上下文(`__package__` 等),并且通常在项目根目录执行时,项目根会在 `sys.path` 中,从而: +- 绝对导入 `import mypkg...` 能找到顶层包 +- 相对导入 `from .xxx import yyy` 能正确解析 + +而 `python mypkg/utils.py` 是把文件当“顶层脚本”跑,包上下文可能丢失,所以相对导入容易报错。 + +--- + +### 6.9 常见坑(很实用) + +#### 6.9.1 `ModuleNotFoundError`(找不到模块) +典型原因: +- 运行时工作目录不对(你不在项目根目录运行) +- 顶层包所在目录不在 `sys.path` + +排查方法: +```python +import os, sys +print("cwd:", os.getcwd()) +print("sys.path[0]:", sys.path[0]) +``` + +解决建议(优先级从推荐到不推荐): +1)在项目根目录用 `python -m ...` 运行 +2)用 VS Code 选择正确的工作目录 / 正确的解释器(虚拟环境) +3)把项目做成可安装包(工程化方式,后面可以补:`pyproject.toml`) +4)不要在代码里随便 `sys.path.append(...)`(临时救火可以,但不优雅) + +#### 6.9.2 包内模块命名别跟标准库重名 +例如你写了 `time.py`、`random.py`、`json.py`,可能会把标准库同名模块“遮住”导致怪问题。 + +--- + + +### 6.10 程序入口:`if __name__ == "__main__":`(重点) + +#### 6.10.1 一句话总结(核心) +- `if __name__ == "__main__":` 用来区分:**这个文件是“被直接运行的入口脚本”** 还是 **被别的文件 import 的模块**。 +- 目的:避免“import 一下就执行一堆代码”的副作用,把“运行入口”放在保护里。 + +--- + +#### 6.10.2 `__name__` 是什么? +- 每个模块(`.py` 文件)都有一个全局变量 `__name__` +- 当模块被 **import** 时:`__name__` 是模块名(例如 `"mypkg.utils"`) +- 当文件被 **直接运行**(`python xxx.py`)时:`__name__` 会被设置成 `"__main__"` + +--- + +#### 6.10.3 最小示例:直接运行 vs 被导入 +文件:`demo.py` +```python +print("top-level code running") + +def add(a, b): + return a + b + +if __name__ == "__main__": + print("this is main") + print(add(2, 3)) +``` + +情况 A:直接运行 +```bash +python demo.py +``` +输出: + +top-level code running +this is main +5 + + +情况 B:被导入 +文件:`other.py` +```python +import demo +print("import done") +``` + +运行: +```bash +python other.py +``` + +输出: +top-level code running +import done + + +观察点: +- `demo.py` 顶层的 `print("top-level...")` 仍会执行(因为 import 会执行模块顶层代码) +- 但 `if __name__ == "__main__":` 下面的代码 **不会执行**(因为此时 `demo.__name__ == "demo"`) + +--- + +#### 6.10.5 为什么要这样写?(解决什么问题) +没有 main guard 时,经常会出现: +- 你写了测试代码/演示代码 +- 结果别人 `import` 你的模块时,这些演示代码也跟着执行(副作用) + +有了 main guard: +- 模块可以放心被 import,当作“库/工具” +- 入口逻辑只在直接运行时执行 + +--- + +#### 6.10.5 推荐写法:把入口逻辑放进 `main()`,再用 main guard 调用 +好处:结构清晰,也更容易测试(main 里能分解调用)。 + +```python +def main(): + # 写命令行交互 / 实际运行流程 + ... + +if __name__ == "__main__": + main() +``` + +--- + +#### 6.10.6 和 `python -m package.module` 的关系(你前面章节相关) +当你这样运行: +```bash +python -m mypkg.utils +``` +- `mypkg.utils` 这个模块也是“被当作入口运行”的 +- 因此在 `mypkg/utils.py` 内部:`__name__ == "__main__"` 也会成立 +- 所以同样可以把“演示/自测/CLI入口”写在 main guard 里 + +--- + +#### 6.10.7 笔记可记的结论(复习版) +- `__name__ == "__main__"`:只在“作为入口运行”时为真 +- 用它保护入口代码,避免 import 产生副作用 +- 推荐:`def main(): ...` + `if __name__ == "__main__": main()` + +--- + +## 07 标准库常用模块(按主题) + +### 7.1 文件与路径:`pathlib` / `os`(重点) + +#### 7.1.1 一句话总结(核心) +- **`pathlib`**:面向对象的“路径工具箱”,用 `Path` 表示路径,拼接/遍历/读写更直观,**新代码优先用它**。 +- **`os` / `os.path`**:更底层、更传统;包含环境变量、进程信息、目录操作等;路径处理也能做,但写起来不如 `pathlib` 清晰。 +- 一般经验:**“路径本身”用 `pathlib`;“系统/环境/进程相关”多用 `os`**。 + +**路径就是字符串,干嘛费劲要用Path?** +解释: +路径当然可以用字符串,但 Path 的价值在于:把“路径”当成一个有行为的对象,让拼接、判断、遍历、读写、跨平台兼容都更安全、更少坑: + +1) 跨平台自动处理分隔符 +字符串拼路径你得自己管 \ 和 /: +```python +path = "data" + "/" + "input.txt" # Windows/Linux 都能凑合,但容易写乱 +Path 会按当前系统生成正确格式: + +from pathlib import Path +p = Path("data") / "input.txt" # 用 / 运算符拼接 +print(p) # Windows: data\input.txt; Linux: data/input.txt +``` + +2) 拼接更不容易出错(少一个斜杠都不行的坑) +字符串常见坑:多/少 /、重复分隔符、漏掉目录名。 + +`Path("data") / "input.txt"` 语义明确:就是“目录 + 文件名”。 + +3) 自带一堆常用操作:判断、创建、遍历、拆分 +字符串要靠 `os.path`,写起来更散;`Path` 是面向对象的“一站式”。 +```python +p.exists() # 是否存在 +p.is_file() # 是不是文件 +p.parent # 上级目录 +p.name # 文件名 input.txt +p.suffix # 后缀 .txt +p.stem # 主文件名 input +p.with_suffix(".csv") # 改后缀 +``` + +4) 文件读写更顺手 +```python +p.write_text("hi", encoding="utf-8") +text = p.read_text(encoding="utf-8") +``` + +5) 目录操作更方便(递归遍历、glob) +```python +for f in Path("data").glob("*.txt"): + print(f) +``` + +--- + +#### 7.1.2 `pathlib`:常用操作(建议优先掌握) +先记住:`Path` 有两套能力: +1)路径拼接/判断/遍历(不直接读写) +2)便捷读写(`read_text/write_text/read_bytes/write_bytes`) + +##### 7.1.2.1 创建 Path / 路径拼接(最常用) +```python +from pathlib import Path + +p = Path("data") / "input.txt" # 用 / 拼接路径(跨平台) +print(p) # data\input.txt(Windows显示反斜杠) +``` + +常用来源: +```python +from pathlib import Path + +cwd = Path.cwd() # 当前工作目录 +home = Path.home() # 用户目录 +script_dir = Path(__file__).resolve().parent # 当前脚本所在目录(脚本运行时常用) +# Path(__file__)把当前这个 .py 文件的路径转换成一个 pathlib.Path 对象,方便后续做路径拼接、取父目录等操作。 +# Path.resolve()把一个路径规范化为绝对路径,例如 data/../data/input.txt 中的..转为绝对路径 + +``` +注意:在交互式环境/Notebook 里,__file__ 可能不存在,会报 NameError。这种环境一般用 Path.cwd()。 + +##### 7.1.2.2 判断存在、类型 +```python +from pathlib import Path + +p = Path("data/input.txt") +print(p.exists()) # 是否存在 +print(p.is_file()) # 是否文件 +print(p.is_dir()) # 是否目录,啥意思? +``` + +##### 7.1.2.3 创建目录(mkdir) +```python +from pathlib import Path + +Path("logs").mkdir(exist_ok=True) # 已存在就不报错 +Path("a/b/c").mkdir(parents=True, exist_ok=True) # 连续创建多级目录 +# parents=True的含义是:如果p的上级不存在,也会一并创建 +# exist_ok=True的含义是:目录已经存在,也不会报错 +``` + +##### 7.1.2.4 遍历目录(glob / rglob) +1)Path.glob()和Path.rglob()都是按照“通配符模式”来找文件。 +2)glob当前目录一级匹配;rglob递归匹配所有的层级(依次往下层文件夹找) +3)常用 pattern(通配符)规则 + • *:匹配任意长度字符(不含路径分隔符) + • ?:匹配任意单个字符 + • [...]:字符集合,例如 [0-9] + • **:匹配任意层级目录(常用于 glob 里做递归) +例子: +```python +p.glob("2026-??-*.csv") # 2026-01-xx.csv 这种 +p.glob("img_[0-9].png") # img_1.png ~ img_9.png +``` +```python +from pathlib import Path + +root = Path("data") + +for f in root.glob("*.txt"): # 只匹配 data 下的 txt + print(f) + +for f in root.rglob("*.py"): # 递归匹配子目录所有 py + print(f) +``` + +##### 7.1.2.5 读写文件(小文件很方便) +```python +from pathlib import Path + +p = Path("data") / "hello.txt" +p.write_text("hello\n", encoding="utf-8") # 写文件 +text = p.read_text(encoding="utf-8") # 读文件 +print(text) +``` + +读写二进制: +```python +from pathlib import Path + +p = Path("data") / "img.bin" +p.write_bytes(b"\x01\x02") +data = p.read_bytes() +``` + +> 大文件建议用 `open()` + 流式读取(后面“文件操作”小节会补)。 + +##### 7.1.2.6 获取文件名/后缀/父目录(非常常用) +```python +from pathlib import Path + +p = Path(r"D:\github\proj\readme.md") +print(p.name) # readme.md(文件名) +print(p.stem) # readme(不带后缀) +print(p.suffix) # .md (仅后缀) +print(p.parent) # D:\github\proj +print(p.suffixes) # ['.md'](多后缀会更明显,如 .tar.gz) +``` + +--- + +#### 7.1.3 `os` / `os.path`:什么时候用? +##### 7.1.3.1 环境变量(常用) +1)环境变量一般用来解决啥问题? +核心就是:**把“配置”从代码里拿出去**,让同一份代码在不同机器/不同环境下运行时行为不同,而不用改代码。 + +常见配置类型: +- 机密信息:API Key / 数据库密码(不写进代码、不提交到 Git) +- 环境区分:开发 / 测试 / 生产(DEV/TEST/PROD) +- 路径/端口:输出目录、日志目录、服务端口 +- 开关:是否开启 debug、是否打印详细日志 + +一句话:**环境变量 = 运行时配置入口**。 + + +2)实际例子 A:用环境变量切换“开发/生产”配置(最常见) +假设你写了一个程序,开发环境要打印更多日志,生产环境要少输出。 + +```python +import os + +DEBUG = os.environ.get("DEBUG", "0") == "1" # 缺省当作关闭 +# == 优先级高于 = ,先判断os.environ.get("DEBUG", "0") 和 "1" 是否相等。 +# os.environ.get("DEBUG", "0")的含义: +# (1)os.envirson类似“dict一样的东西”,保存当前进程的所有环境变量。 +# (2)os.envirson.get("DEBUG", "0"),如果“DEBUG”在环境变量里面存在,就返回对应值,否则返回“0” +# 然后将os.environ.get("DEBUG", "0")获取的值,与“1”比较,隐含的意思是“1”在系统环境变量里面表示“开始调试” + +def log(msg): + if DEBUG: + print("[DEBUG]", msg) + +log("connecting to db...") +``` + +3)环境变量如何设置? +Windows 怎么设置环境变量后再运行? +PowerShell: +```powershell +$env:DEBUG="1" +python app.py +``` + +cmd: +```bat +set DEBUG=1 +python app.py +``` + +解决的问题: +- 开关无需改代码 +- 本地/服务器不同输出策略 + + + +注意:**set DEBUG = 1 是临时性设置环境变变量** +这里面的`set DEBUG = 1`是临时性设置环境变量,只对: +- 当前这个 **cmd 窗口/会话** +- 以及它启动的子进程(比如你紧接着运行的 `python app.py`) +有效。 +一旦关闭这个cmd窗口,这个`DEBUG`就没了,新打开一个cmd也不会继承。 +cmd 里永久设置一般用 `setx`(写入用户/系统环境变量)。 + + +```python +import os + +print(os.environ.get("PATH")) # 读环境变量 +os.environ["MY_ENV"] = "1" # 写环境变量(仅对当前进程有效) +``` + +##### 7.1.3.2 当前工作目录/切换目录 +1)工作目录是啥? +**一句话:工作目录(Current Working Directory, CWD)是“程序运行时默认看哪里找文件/往哪里写文件”的基准点。** +它不是“文件被移动到哪里”,而是“程序此刻把哪个目录当成默认目录”。 + +2)工作目录的作用是啥? + +2.1)相对路径的解析 +当你写: +```python +open("data/a.txt") +``` +这里的 `"data/a.txt"` 是**相对路径**,它会被解释成: +- `当前工作目录 + data/a.txt` + +所以如果 CWD 不同,程序找的文件位置就不同,容易出现 `FileNotFoundError`。 + +2.2)默认输出位置(创建文件/写文件) +当你写: +```python +open("out.txt", "w") +``` +如果你没写绝对路径,那么 `out.txt` 会被创建在**当前工作目录**下。 + +3)`os.chdir(...)` 切换了什么?真的切换了吗? +`os.chdir(r"D:\github")` **真的切换了**,但切换的是: +- **当前进程的工作目录**(只影响这个 Python 进程以及它启动的子进程) + +它不会: +- 把你的文件“搬家” +- 改变磁盘上文件真实的位置 +> 你可以理解为:只是把“默认视角/默认起点”换了。 + +```python +import os + +print(os.getcwd()) # 当前工作目录 +os.chdir(r"D:\github") # 切换工作目录 +``` + + +##### 7.1.3.3 目录列举 & 文件操作(传统写法) +```python +import os + +print(os.listdir(".")) # 列出当前目录内容(名字列表,非完整路径) +os.makedirs("a/b/c", exist_ok=True) # 创建多级目录 +``` + +##### 7.1.3.4 `os.path`(老项目常见,但新代码更推荐 pathlib) +```python +import os + +p = os.path.join("data", "input.txt") # 拼接路径 +print(os.path.exists(p)) # 是否存在 +print(os.path.dirname(p)) # 父目录 +print(os.path.basename(p)) # 文件名 +``` + +--- + +##### 7.1.3.5 `pathlib` vs `os.path` 对照(理解即可) +同一个任务:拼路径 + 判断存在 + +`pathlib`: +```python +from pathlib import Path + +p = Path("data") / "input.txt" +if p.exists(): + print("ok") +``` + +`os.path`: +```python +import os + +p = os.path.join("data", "input.txt") +if os.path.exists(p): + print("ok") +``` + +结论: +- 新代码:优先 `pathlib`(可读性更好) +- 需要环境变量/进程/系统操作:用 `os`(但路径尽量还是 `Path`) + +--- + +### 7.1.4 易错点(高频) +1)**不要手写路径字符串拼接** +```python +# 不推荐(跨平台容易出问题) +path = "data/" + "a.txt" +``` +推荐: +```python +from pathlib import Path +path = Path("data") / "a.txt" +``` + +2)相对路径“相对的是工作目录(cwd)”,不是“脚本所在目录” +- 很多 `FileNotFoundError` 的根源:你以为相对脚本,其实相对 cwd +排查: +```python +from pathlib import Path +print(Path.cwd()) +``` +需要“相对脚本”的话: +```python +from pathlib import Path +BASE_DIR = Path(__file__).resolve().parent +p = BASE_DIR / "data" / "input.txt" +``` + +3)Windows 路径转义问题 +- `"D:\new\test"` 里的 `\n` 会被当作换行转义 +推荐用原始字符串或 pathlib: +```python +from pathlib import Path +p = Path(r"D:\new\test") +``` + +--- + +### 7.1.5 常见应用场景(什么时候用) +- 读取配置/数据文件:`Path(...).read_text()` / `open()` +- 批量处理文件(重命名、过滤后缀、遍历目录):`glob/rglob` +- 构建日志目录/输出目录:`mkdir(parents=True, exist_ok=True)` +- 依赖环境变量切换行为:`os.environ.get("ENV")` + +--- + +### 7.1.6 具体应用例子:用 `pathlib` + `os` 做一个“整理文件夹”的小工具(可直接跑) + +需求(真实场景): +- 从 `data/` 目录里找出所有 `.txt` 文件 +- 按“修改日期(YYYY-MM-DD)”分文件夹归档到 `out/` +- 输出一个汇总 `report.txt` +- 同时演示:`pathlib`(路径/遍历/读写) + `os`(环境变量/获取系统信息) + +```python +from pathlib import Path +import os +import shutil +from datetime import datetime + +def organize_txt_files(src_dir="data", out_dir="out"): + src = Path(src_dir) + dst = Path(out_dir) + + # 1) 用 os 读取环境变量(系统/环境相关更适合 os) + # 例:允许用环境变量覆盖输出目录 + dst = Path(os.environ.get("OUT_DIR", str(dst))) + + # 2) 创建输出目录(pathlib) + dst.mkdir(parents=True, exist_ok=True) + + moved = 0 + lines = [] + + # 3) 遍历文件(pathlib) + for p in src.rglob("*.txt"): # 递归找所有 txt + if not p.is_file(): + continue + + # 4) 获取文件修改时间(pathlib.stat()) + mtime = p.stat().st_mtime + # `p.stat()` 会向操作系统查询这个文件的“元信息”(类似 `ls -l`/文件属性),返回一个 `os.stat_result` 对象。 + # `.st_mtime` 是其中的一个字段:**最后修改时间(modified time)**。 + # `mtime` 的值是一个时间戳(timestamp),类型通常是 `float` 或 `int`。 + # 单位是**秒**,表示“从 Unix 纪元开始(1970-01-01 00:00:00 UTC)到现在的秒数”。 + day = datetime.fromtimestamp(mtime).strftime("%Y-%m-%d") + # datetime.fromtimestamp(mtime) 把时间戳转换为一个datetime对象,fromtimestamp用的是本地时区时间(Windows上就是当前设置的时区) + # .strftime("%Y-%m-%d") 把datetime格式化成为字符串 + # - `%Y`:四位年份(2026) + # - `%m`:两位月份(01~12) + # - `%d`:两位日期(01~31) + + # 5) 目标子文件夹(pathlib 拼路径) + day_dir = dst / day + day_dir.mkdir(parents=True, exist_ok=True) + + # 6) 移动文件(文件操作可用 shutil;路径用 Path) + target = day_dir / p.name + + # 防止同名覆盖:如果已存在就加后缀 + if target.exists(): + target = day_dir / f"{p.stem}_copy{p.suffix}" + + shutil.move(str(p), str(target)) + # str(p)和str(target),直接传也可以 + moved += 1 + + # 7) 记录报告信息 + lines.append(f"moved: {p} -> {target}") + + # 8) 写报告(pathlib) + report = dst / "report.txt" + report.write_text( + "\n".join([ + f"cwd={Path.cwd()}", + f"python={os.sys.executable}", # os.sys 也行,但通常直接 import sys 更清晰 + # `sys.executable` 是:**当前正在运行这段代码的 Python 解释器路径**。 + f"src={src.resolve()}", + f"dst={dst.resolve()}", + f"moved_files={moved}", + "-" * 40, + *lines + ]), + encoding="utf-8" + ) + + print(f"done: moved {moved} files, report -> {report}") + +if __name__ == "__main__": + organize_txt_files() +``` + +### 7.1.7 相关知识点补充 +#### 7.1.7.1 `shutil` +1)shutil是干啥的? +`shutil` 是标准库的 **“高级文件操作”** 模块,常用来做: +- 复制文件:`shutil.copy2` +- 复制目录:`shutil.copytree` +- 移动/重命名:`shutil.move` +- 删除目录树:`shutil.rmtree` +- 压缩/解压:`shutil.make_archive` / `shutil.unpack_archive` + +2) `shutil.move(src, dst)` 的行为 +```python +shutil.move(src, dst) +``` +含义:把 `src` **移动**到 `dst`。 +返回最终的目标路径(字符串)。 + + + +- 文件与路径 + - `pathlib`、`os` +- 系统与进程 + - `sys`、`subprocess` +- 时间日期 + - `datetime`、`time` +- 数学与随机 + - `math`、`random` +- 数据结构与工具 + - `collections`、`itertools`(可选) +- 序列化 + - `json`、`pickle`(注意安全边界) +- 正则表达式 + - `re` +- 日志 + - `logging` +- 并发(可选进阶) + - `threading`、`multiprocessing`、`asyncio` + +--- + + + +## 08 错误 / 异常 / 调试 + +### 8.1 异常基础:异常体系、常见异常 + +#### 8.1.1 一句话总结(核心) +- **异常(Exception)**:程序运行中出现“不符合预期/无法继续”的情况,Python 用“抛出异常”来中断正常流程并携带错误信息。 +- 你可以选择:**让异常直接报错退出**(暴露问题),或 **捕获异常并处理**(恢复/兜底/重试/给用户友好提示)。 + +#### 8.1.2 异常体系(层级关系) +大致结构(记住最常见这一层就够): +- `BaseException`(最顶层,通常不直接捕获) + - `SystemExit`(`sys.exit()` 触发,程序要退出) + - `KeyboardInterrupt`(Ctrl+C) + - `GeneratorExit` + - `Exception`(**我们日常代码主要处理的异常基类**) + - `ValueError`(值不合法,如 `int("abc")`) + - `TypeError`(类型不匹配,如 `len(3)`) + - `KeyError`(字典缺 key) + - `IndexError`(列表索引越界) + - `AttributeError`(对象没这个属性/方法) + - `FileNotFoundError` / `OSError`(文件/系统相关) + - `ZeroDivisionError`(除以 0) + - `ImportError` / `ModuleNotFoundError` + - ... + +经验: +- **一般捕获 `Exception` 就好**;不要随便捕 `BaseException`,否则 Ctrl+C/退出都会被你吞掉。 + +#### 8.1.3 常见异常与例子(你笔记里可直接贴) +```python +int("abc") # ValueError +len(3) # TypeError +d = {}; d["x"] # KeyError +lst = [1]; lst[9] # IndexError +"hi".not_exist() # AttributeError +1 / 0 # ZeroDivisionError +open("no.txt") # FileNotFoundError +``` + +--- + +### 8.2 `try/except/else/finally`(重点) + +#### 8.2.1 一句话总结(核心) +- `try`:放“可能出错”的代码,发生错误的行之后的代码不会执行 +- `except`:捕获并处理异常 +- `else`:**没出异常才会执行**(把“成功后的逻辑”放这里更清晰) +- `finally`:**无论是否异常都会执行**(常用于资源清理) + +--- + +#### 8.2.2 最常用结构:try + except +```python +try: + x = int(input("age: ")) +except ValueError: + print("请输入整数") +``` + +要点: +- except 后面写你要捕获的异常类型(越具体越好) +- 不要用“裸 except:”乱抓(会吞掉很多你不该吞的错误) + +--- + +#### 8.2.3 捕获多个异常 +```python +try: + x = int("abc") +except (ValueError, TypeError) as e: + print("bad input:", e) +# 输出:bad input: invalid literal for int() with base 10: 'abc' +``` +注意: +- 这里面的e是异常对象本身(例如 ValueError(...) 的实例),里面带着错误信息、类型等内容。 +- print("bad input:", e) 里的 e 就是把这个异常对象打印出来;print 会调用它的字符串表示(大多就是你看到的那段报错信息)。 +- 目的是为了方便你在 except 块里查看/记录具体原因(打印日志、写文件、上报等) +--- + +#### 8.2.4 `else`:成功路径放这里(很推荐) +```python +try: + x = int("123") +except ValueError: + print("bad") +else: + # 只有 try 没抛异常才会走到这里 + print("ok:", x) +``` + +好处: +- 你不用把“成功逻辑”塞在 try 里,避免 try 块过大(try 块越大,越容易把不该捕获的错误也包进去)。 + +--- + +#### 8.2.5 `finally`:无论如何都执行(清理现场) +```python +f = None +try: + f = open("a.txt", "r", encoding="utf-8") + data = f.read() +finally: + if f is not None: + f.close() +``` + +更 Pythonic 的方式是用 `with`(上下文管理)自动 close,但 `finally` 的思想要懂: +- 释放资源:文件句柄、锁、临时文件、连接等 +- 记录日志:无论成功失败都要记一条 + +--- + +#### 8.2.6 一个常见误解:except 捕获后程序会继续吗? +- 捕获到了并且你没再 `raise`:程序会从 except/else/finally 后继续往下走。 +- 没捕获到:异常向外抛,程序可能终止(或被更外层 try 捕获)。 + +--- + +### 8.3 `raise`(手动抛异常)与自定义异常 + +#### 8.3.1 一句话总结(核心) +- `raise` 用来**主动报错**:当你检测到“不合法的状态/参数”,就抛出异常让调用方处理。 +- 自定义异常用来**表达更清晰的业务错误语义**(比 ValueError 更准确)。 + +--- + +#### 8.3.2 `raise` 的基本用法 +```python +def set_age(age: int): + if age < 0: + raise ValueError("age must be >= 0") + return age +``` + +目的: +- 及早失败(fail fast) +- 给出明确错误信息,方便定位 + +--- + +#### 8.3.3 重新抛出(保持原始堆栈) +```python +try: + 1 / 0 +except ZeroDivisionError: + print("log something") + raise # 原样再抛出(保留原始 traceback) +``` +1)这样做的目的: +- 在“记录/补充信息”的同时,不吞掉异常,并且保留异常发生的“真实位置(原始堆栈)”,让错误还能被正确发现和定位。 +- 如果不这样做,就会导致异常被捕获,程序继续往下执行,外层代码完全不知道这里发生了异常。 + +2)什么是保留原始堆栈? +- traceback(堆栈)就是“异常发生时的调用路径”(从哪里一路调用到出错那一行)。 +- 保留原始堆栈的意思是:异常最终仍然指向真正出错的代码行(这里是 `1 / 0`),而不是指向你在 `except` 里处理异常的那一行。 + +3)为什么要先 `except` 再 `raise`: + +- 你想 **记录日志 / 打点 / 上报**(带上额外上下文),但又不想把错误吞掉。 +- 你想 **做清理或收尾**(关闭资源、回滚局部状态等),然后仍然让外层知道“这次失败了”。 +- 你想 **分层处理**:这一层只负责补充信息,更外层再决定重试/返回错误/终止程序。 + +4)一个关键细节(很常考也很常踩坑): + +- `raise`(不带参数)= 原样再抛出当前捕获到的异常,最能保留原始 traceback。 +- `raise e`(带异常变量)通常会让 traceback 更偏向“从这一行重新抛出”,定位信息可能变差;如果只是“继续往外抛”,优先用裸 `raise`。 + +5)更常见的写法(既能打日志又能保留堆栈): + +```python +try: + 1 / 0 +except ZeroDivisionError as e: + print("log something:", e) + raise +``` + + +--- + +#### 8.3.4 异常包装:`raise NewError(...) from e`(推荐掌握) +1)**当你想把底层异常“包装成业务异常”,同时保留原因链。详细解释如下** +- 底层异常包括: + - 文件不存在:`FileNotFoundError`(属于 `OSError`) + - 没权限:`PermissionError` + - 路径不合法:`OSError`等 + +- 底层异常仅对开发者有用,对于业务来讲是看不懂的,业务需要知道**语义更明确**的业务异常。 + - “配置加载失败”(业务语义) → `ConfigError` + +- 同时,对应的底层异常也不能丢掉(否则无法进行排查。)。 + + +```python +class ConfigError(Exception): + pass +# 自定义异常类,表示“配置相关错误”。 +# 继承 `Exception`,说明这是普通业务异常(可捕获处理)。 + +def load_config(path: str): + try: + with open(path, "r", encoding="utf-8") as f: + return f.read() + except OSError as e: +# 捕获 `OSError` 及其子类(`FileNotFoundError` / `PermissionError` 都是它的子类)。 +# `e` 是捕获到的底层异常对象(带原始错误信息)。 + + raise ConfigError(f"failed to load config: {path}") from e +# 抛出一个新的异常:`ConfigError(...)` +# **关键点:`from e`** 表示“这个新异常是由 e 导致的”,把 `e` 作为 **cause(原因)** 链接上去。 +``` + +2)**加了`from e`的效果** +- traceback 会显示两段异常,并且明确告诉你: + - **The above exception was the direct cause of the following exception** +- 你既有业务语义(ConfigError),也保留了底层原因(e) + +你可以把它理解成: +**“我告诉 Python:这个 ConfigError 的根本原因是 e,请把因果关系展示出来。”** +效果: +- 报错信息对业务更友好(ConfigError) +- 也能看到底层原因(from e 保留异常链) + +3)**OSError是什么意思?** +`OSError`(Operating System Error)表示:**跟操作系统/底层资源交互时失败**产生的异常。 +典型场景: +- 文件/目录:打开、读写、删除、改名、权限 +- 进程/系统调用:创建子进程、信号、某些系统接口 +- 某些网络/IO(更底层的错误也可能表现为 `OSError`) + +常见子类(你最常遇到的): +- `FileNotFoundError`:文件/目录不存在(打开一个不存在的文件) +- `PermissionError`:权限不足(没有读写权限) +- `IsADirectoryError`:把目录当文件用 +- `NotADirectoryError`:把文件当目录用 +- (还有很多,但不必都背) + +例子: +```python +open("no_such_file.txt", "r", encoding="utf-8") # FileNotFoundError (是 OSError 的子类) +``` + +4)OSError 和 Exception 有什么区别和联系? +**联系(继承关系)** +`OSError` **是** `Exception` 的子类: + +```text +BaseException + └── Exception + └── OSError + ├── FileNotFoundError + ├── PermissionError + └── ... +``` +所以: +- `except Exception:` 能抓到 `OSError` +- `except OSError:` 只能抓“系统相关”的那一类错误(更具体) + + +**区别(语义范围不同)** +- `Exception`:很大的一类“普通异常”的基类(包括 ValueError/TypeError/KeyError/OSError...)。 + 它回答的是:“出错了(一般意义上的错)”。 + +- `OSError`:专门表示“跟操作系统交互失败”的错误。 + 它回答的是:“在访问文件/目录/权限/系统资源时失败了”。 + +举例对比: +```python +int("abc") # ValueError(不是 OSError:这是“数据格式不对”) +open("a.txt") # 可能 OSError(例如不存在/权限不够) +``` + +**什么时候该捕获 OSError?什么时候捕获 Exception?** +- 你只想处理“文件/系统资源相关”的失败 → **捕获 `OSError`(更推荐、更精准)** + ```python + try: + Path("config.json").read_text(encoding="utf-8") + except OSError as e: + ... + ``` + +- 你在写很外层的“兜底入口”(例如 main 入口防止程序直接崩)→ 可以捕获 `Exception`,但要记录日志/再抛出,别悄悄吞掉: + ```python + try: + main() + except Exception as e: + print("fatal:", e) + raise + ``` + +--- + +#### 8.3.5 自定义异常怎么写?(最小标准写法) +**为什么要自定义异常类?** +要先自定义 `ConfigError`,本质是为了:**把一堆底层异常(OSError 的各种子类)在“业务层”统一成一个更有语义、更好处理的异常类型**。 + +如果不自定义异常类,你只能: +- 要么把底层异常原样抛出去(上层要处理很多种异常) +- 要么随便抛 `Exception/ValueError`(语义不清晰,不利于分层) + + +```python +class BizError(Exception): + """Base class for business errors.""" + pass + +class PermissionDenied(BizError): + pass +``` + +经验建议: +- 自定义异常通常继承 `Exception`(或继承你项目的业务异常基类) +- 命名用 `xxxError` / `xxxException` / `PermissionDenied` 这类清晰语义 + +--- + +### 8.4 warnings(了解) + +1)一句话总结(核心) +- **warnings** 是“比异常轻”的提示机制:程序还能继续跑,但 Python/库想提醒你“这个用法可能有问题/将来会坏/不推荐”。 + +2)常见 warning 类型(认识即可) +- `DeprecationWarning` / `FutureWarning`:接口将废弃/未来行为会变 +- `UserWarning`:库作者给用户的提醒 +- `ResourceWarning`:资源可能没正确释放(比如文件没关) +- `RuntimeWarning`:运行时出现可疑情况但不致命 + +3)怎么触发/怎么处理(最常用) +```python +import warnings + +warnings.warn("this is a warning", UserWarning) +``` + +控制 warnings(常见写法): +```python +import warnings + +# 让某类 warning 直接显示(有些默认不显示) +warnings.simplefilter("default") + +# 忽略某类 warning(慎用,容易把重要提醒屏蔽) +warnings.filterwarnings("ignore", category=DeprecationWarning) + +# 把 warning 当异常抛出(严格模式,测试/CI 很常用) +warnings.filterwarnings("error", category=DeprecationWarning) +``` + +实战建议: +- 本地开发时建议保留 warnings(能早发现“将来会坏”的用法) +- CI/测试里可以把关键 warnings 设为 error,逼自己尽早修 + +### 8.5 调试方法 + +#### 8.5.1 断点调试(VS Code)(最推荐掌握) +1)一句话总结 +- 断点调试让程序“停下来”,你可以逐行执行、查看变量、观察调用栈,定位 bug 的速度通常最快。 + +2)你在 VS Code 常用的调试操作 +- 打断点:行号左侧点一下出现红点 +- F5:启动调试 +- F10:Step Over(执行下一行,不进函数) +- F11:Step Into(进入函数内部) +- Shift+F11:Step Out(跳出当前函数) +- Shift+F5:停止 +- 观察窗口: + - Variables(变量) + - Watch(添加表达式,比如 `len(lst)`) + - Call Stack(调用栈:谁调用了谁) + - Debug Console(调试控制台:可输入表达式查询当前状态) + +3)两个超级实用的断点技巧 +- **条件断点**:只在满足条件时停下(避免循环里停太多次) + 右键断点 → Add Conditional Breakpoint + 例:`i == 100` 或 `x is None` +- **Logpoint(日志点)**:不暂停,只打印(适合快速观察) + 右键断点 → Add Logpoint + 例:输出 `i={i}, x={x}` + +--- + +#### 8.5.2 `pdb`(了解,但很实用,尤其在服务器/无 IDE 时) +1)一句话总结 +- `pdb` 是 Python 自带的命令行调试器:在终端里单步、看变量、看堆栈。 + +2)最常用用法:在怀疑位置插一行断点 +```python +import pdb; pdb.set_trace() +``` +程序跑到这里会停住,你可以输入命令。 + +3)几个最常用命令(记这几个就够) +- `l`:list,显示附近代码 +- `n`:next,执行下一行(不进函数) +- `s`:step,进入函数 +- `c`:continue,继续跑到下一个断点 +- `p expr`:打印表达式(如 `p x`, `p len(lst)`) +- `pp expr`:更漂亮的打印(pretty print) +- `w`:where,查看调用栈 +- `q`:quit,退出(会终止程序) + +4)更简单的替代:`breakpoint()`(Python 3.7+) +```python +breakpoint() +``` +默认就会进入 pdb(除非你配置了别的调试器)。 + +--- + +#### 8.5.3 日志定位(logging)(工程里最常用) +1)一句话总结(核心) +- `logging` 用来记录程序运行过程中的信息(debug/info/warning/error),比 `print` 更可控:能分级、能写文件、能统一格式、能在生产环境追问题。 + +2)为什么不用 print?(你记住 3 个点就行) +- print 没等级:无法区分调试信息和错误信息 +- print 不好统一管理:开关、格式、输出位置都不好控制 +- 线上排查需要持久化:日志写文件/收集系统,print 不够用 + +3)最小可运行示例(建议你笔记里放这个) +```python +import logging + +# basicConfig决定日志怎么显示、显示哪些级别(即为日志的全局基础配置) +logging.basicConfig( + level=logging.INFO,# 表示只显示 INFO及以上 的日志,越往下越详细,如DEBUG更详细 + format="%(asctime)s %(levelname)s %(name)s: %(message)s" + # 定义日志输出格式 + # `%(asctime)s`:时间 + # `%(levelname)s`:级别名(INFO/WARNING/ERROR) + # `%(name)s`:logger 的名字(这里是 `__name__`,通常是模块名) + # `%(message)s`:你实际写的日志内容 +) + + +logger = logging.getLogger(__name__) +# `getLogger(name)` 获取一个 logger 对象(可理解为“带名字的日志记录器”) +# `__name__` 在模块里通常是模块名(比如 `mypkg.utils`),在直接运行脚本时可能是 `"__main__"` +# 好处:大项目里每个模块一个 logger,日志里能看到“是谁打的日志”,方便定位来源。 + + +def divide(a, b): + logger.debug("divide called: a=%s b=%s", a, b) + # `logger.debug/info/warning/...` 只是“记日志”,方便排查问题,不会影响程序流程(除非你自己 raise) + # logger:日志记录器对象 + # .debug(...):记录一条 **DEBUG 级别** 的日志(最详细、最“啰嗦”的那一档) + # `"divide called: a=%s b=%s"`:日志模板字符串标准写法,%s是占位符,a,b是要填进占位符的变量 + # 注意点:日志配置为logging.INFO时,看不到这条DEBUG类型的日志 + + if b == 0: + logger.warning("b is zero, cannot divide") + raise ValueError("b cannot be 0") + # 当b==0时,会先记录一个WARNING日志,告知参数不合法 + # 然后raise ValueError(...),抛出异常,抛出异常的情况下,下一句 return a / b 不会执行 + return a / b + + +# `logger.exception(...)` 只能在 `except` 里用,它会把 **异常堆栈 traceback** 一起记下来(所以特别适合排错) + +try: + print(divide(10, 0)) +except Exception: + # 为啥这里面不写ValueError? + # 两种都可以,Exception更宽泛;ValueError更精准 + logger.exception("divide failed") # 自动把 traceback 也记下来 + # 代码出错进入except时,Python里有一个正在处理的异常,例如ValueError, + # 此时 logger.exception(...)的作用是: + # 1. 记录一条日志(级别是 **ERROR**) + # 2. 把异常信息 + 完整 traceback 一起输出(或写到文件/其他 handler) + # 从而非常适合排查问题:你不仅知道“失败了”,还能知道: + # - 具体异常类型是什么(ValueError/OSError/...) + # - 具体在哪一行报错 + # - 谁调用了谁(调用链) +``` + +**运行后输出内容** +```python +2026-01-14 10:33:17,579 DEBUG __main__: divide called: a=10 b=0 +2026-01-14 10:33:17,579 WARNING __main__: b is zero, cannot divide +2026-01-14 10:33:17,579 ERROR __main__: divide failed +Traceback (most recent call last): + File "D:\python_work\test20251218.py", line 18, in + print(divide(10, 0)) + File "D:\python_work\test20251218.py", line 14, in divide + raise ValueError("b cannot be 0") +ValueError: b cannot be 0 +[Finished in 204ms] +``` +**Traceback(...)堆栈信息,它在告诉你三件事:** +a)异常类型和信息是什么 +最后一行: +- 异常类型:`ValueError` +- 异常信息(message):`b cannot be 0` + +b)错误发生在哪一行 +- 在 `test20251218.py` 第 14 行 `raise ValueError(...)` 抛出的 + +c)是怎么一路调用到那里出错的(调用链) +- 顶层(第 18 行)调用了 `print(divide(10, 0))` +- 进入 `divide` 后,在第 14 行抛异常 + +> 这就是 `logger.exception(...)` 的价值:**不仅说“失败了”,还把“在哪失败、为什么失败、谁调用的”全打印出来。** + + +logger对象的属性与方法: + + +你需要理解的点: +- `logging.basicConfig(...)`:做一次全局基本配置(级别、格式) +- `logger = logging.getLogger(__name__)`:每个模块一个 logger(最佳实践) +- `logger.debug/info/warning/error`:不同严重程度 +- `logger.exception(...)`:只能在 except 里用,**会自动记录堆栈 traceback**(排错神器) +- 用 `%s` 方式传参(不要用 f-string 拼日志更好):避免无谓字符串拼接、日志系统能更好处理 + +4)日志级别(从低到高) +- DEBUG:开发调试细节 +- INFO:关键流程信息(启动成功、加载配置成功) +- WARNING:可疑但还能跑(重试、参数不推荐) +- ERROR:发生错误,但程序可能还能继续 +- CRITICAL:致命错误,通常要退出 + +--- + +**你写笔记的“结论句”(可直接抄)** +- warnings:**提醒你“将来可能坏/不推荐”,但不一定立即中断程序** +- 断点调试:**定位 bug 最快的交互手段(看变量/调用栈/逐行跑)** +- pdb:**没有 IDE 也能调试(服务器/终端)** +- logging:**生产环境排错主要靠它;用 logger + 分级 + exception 堆栈** + + + +### 8.6 易错点(高频) + +1)不要用 `except:` 抓一切 +- 会把 `KeyboardInterrupt` 等也吞掉(尤其用 `BaseException` 更危险) +- 推荐:捕 `Exception` 或更具体类型 + +2)try 块别写太大 +- try 包太多逻辑会掩盖错误来源 +- 推荐:try 只包“可能失败的那几行” + +3)不要用异常当正常流程(在热路径会慢且可读性差) +- 例如用 KeyError 控制普通分支,一般更推荐 `dict.get()` / `in` + +--- + +### 8.7 小练习(建议) +1)写一个 `safe_int(s, default=0)`:转换失败返回 default +```python +def safe_int(s,default=0): + try: + return int(s) + except ValueError as e: + return default # 工具函数内一般不加print + +print(safe_int("A")) +``` +2)写一个 `load_json(path)`:文件不存在/JSON 解析失败时抛 `ConfigError`(用 `from e`) +```python +# 例子:读取 JSON 配置,并把底层异常统一包装成 ConfigError(保留异常链) +# 说明: +# - 文件不存在/没权限 -> OSError 系列 +# - JSON 格式错误 -> json.JSONDecodeError(它是 ValueError 的子类) +# - 统一对外抛 ConfigError,但 from e 保留真正原因,便于排查 + +import json +from pathlib import Path + +class ConfigError(Exception): + """配置加载/解析失败""" + pass + +def load_json_config(path: str | Path) -> dict: + p = Path(path) + + # 1) 读文件:可能抛 OSError(FileNotFoundError/PermissionError/...) + try: + text = p.read_text(encoding="utf-8") + except OSError as e: + raise ConfigError(f"failed to read config file: {p}") from e + + # 2) 解析 JSON:可能抛 JSONDecodeError(ValueError 子类) + try: + return json.loads(text) + except json.JSONDecodeError as e: + # e.lineno / e.colno / e.msg 都很有用:定位第几行第几列坏了 + raise ConfigError( + f"invalid JSON in config: {p} (line {e.lineno}, col {e.colno}): {e.msg}" + ) from e + +# ====== 演示调用侧怎么处理(通常在入口处做)====== +if __name__ == "__main__": + try: + cfg = load_json_config("config.json") + print("config loaded:", cfg) + except ConfigError as e: + # 给用户/业务层看的:语义明确 + print("CONFIG ERROR:", e) + + # 给开发者排查看的:还可以继续把底层原因打印出来(可选) + # e.__cause__ 就是 from e 连接的那个底层异常 + print("CAUSE:", repr(e.__cause__)) + raise # 开发阶段建议继续抛出,保留完整 traceback +``` + +3)写一个 `divide(a, b)`:b==0 时 `raise ValueError`,并写调用侧 try/except 打印友好提示 +```python +def divide(a,b): + if b == 0: + raise ValueError("b cannot be 0") + # ("b cannot be 0")是创建这个异常对象时传入的说明文字,这里面是错误信息(异常的message) + # print(e)或者print(repr(e))的时候会显示 "b cannot be 0" + return a / b + +try: + print(divide(10,0)) +except ValueError as e: + print("友好提示:除数不能为0") +``` + +--- + +## 09 面向对象(OOP) + +### 9.1 一句话总结(核心) +- **类(class)**:一种“自定义类型/模板”,描述一类对象有什么数据(属性)和行为(方法)。 +- **对象(object / instance)**:类创建出来的具体实例。 +- **构造器(constructor)**:通常指 `__init__`,负责在“创建对象时初始化属性”。 + +--- + +### 9.2 最小可运行示例(先看整体) +```python +class Student: + # 类属性(所有实例共享) + school = "No.1 Middle School" + + def __init__(self, name: str, age: int): + # 实例属性(每个实例一份) + self.name = name + self.age = age + + # 实例方法(第一个参数必须是 self) + def say_hi(self): + return f"hi, I'm {self.name}, age={self.age}, school={self.school}" + +s1 = Student("Tom", 18) +s2 = Student("Alice", 16) + +print(s1.say_hi()) +print(s2.say_hi()) +``` + +--- + +### 9.3 类与对象到底是什么关系? +- `Student` 是类(类型) +- `s1 = Student("Tom", 18)` 是在 **调用类** 来创建对象(实例) +- 你可以用 `type` 看清楚: +```python +print(type(s1)) # +# 这个理解?s1 的类型是 Student(它定义在 __main__ 这个模块里) +print(isinstance(s1, Student)) # True +``` + +--- + +### 9.4 属性(Attribute) + +#### 9.4.1 实例属性:`self.xxx` +实例属性属于“某一个对象”,不同对象可以不同。 +```python +s1.name = "Tom" +s2.name = "Alice" +``` +它们存在于对象自己的 `__dict__` 里: +```python +print(s1.__dict__) # {'name': 'Tom', 'age': 18} +# s1.__dict__是啥? +# `__dict__` 是对象的“属性字典”(更准确说:**实例属性存放的地方**)。 +# 也就是说: +# - 你在 `__init__` 里写的 `self.name = name`、`self.age = age` +# - 实际上就是往 `s1.__dict__` 这个字典里塞键值对 +``` + +#### 9.4.2 类属性:`ClassName.xxx` +类属性属于“类本身”,多个对象默认共享同一份。 +```python +print(Student.school) # No.1 Middle School +print(s1.school) # 也能访问(查找顺序会先找实例,再找类) +``` + +**类属性 vs 实例属性的高频坑(一定要懂)** +如果你对实例赋值同名属性,会“遮住”类属性(只对该实例生效): +```python +Student.school = "A School" +# 这样是直接修改类的属性吗?可以 +print(s1.school) # A School + +s1.school = "B School" # 给 s1 创建了一个同名“实例属性” +# 对象创建好之后,还可以新增属性吗? 可以 +print(s1.school) # B School +print(s2.school) # A School(不受影响) +print(Student.school) # A School +``` + +--- + +### 9.5 方法(Method) + +#### 9.5.1 实例方法(最常用) +定义时第一个参数是 `self`,调用时不用传,Python 会自动把对象传进去: +```python +class Student: + def say_hi(self): + return "hi" + +s = Student() +print(s.say_hi()) # 等价于 Student.say_hi(s) +``` + +**self 是什么?** +- `self` 就是“当前这个对象本身”(你在用哪个对象调用方法,self 就指向哪个对象) + +--- + +#### 9.5.2 `@classmethod` 类方法(用得也很多) +第一个参数是 `cls`(类本身),常用于: +- 写“替代构造器”(从别的格式创建对象) +- 操作/读取类级别数据 + +```python +class Student: + def __init__(self, name): + self.name = name + + @classmethod + def from_fullname(cls, fullname: str): + # cls 就是 Student(或子类) + name = fullname.strip() + return cls(name) + +s = Student.from_fullname(" Tom ") +print(s.name) # Tom +``` + +**问题:为什么还要替代构造器?** +“替代构造器”(通常用 `@classmethod` 写)不是必须的;**正常的 `__init__` 构造当然够用**。 +它存在的价值是:当“创建对象的入口不止一种”时,让代码 **更清晰、更安全、更好维护**。 +下面用最常见的几个理由解释“为什么还需要”。 + +--- + +**1)不同输入格式创建对象:把解析逻辑集中在类里** + +正常构造要求你已经有“干净参数”: + +```python +s = Student("Tom") +``` + +但现实里经常拿到的是“脏数据/复合格式”,比如: +- `"Tom"` 前后有空格 +- `"Tom|18"` 一串文本 +- JSON / dict / 数据库行 + +如果每个调用方都自己 `strip()/split()/校验`,会到处重复、标准不一致。 + +用替代构造器集中处理: + +```python +class Student: + def __init__(self, name): + self.name = name + + @classmethod + def from_fullname(cls, fullname: str): + return cls(fullname.strip()) +``` + +调用方只管: +```python +s = Student.from_fullname(" Tom ") +``` + +--- + +**2)表达更清晰的“创建意图”(可读性)** + +`Student(...)` 只能表达“创建 Student”,不能表达“从什么来源创建”。 + +对比: + +```python +Student("Tom") # 这是什么?原始名字?带空格?从文件读来的? +Student.from_fullname(" Tom ") # 一眼知道:从 fullname 处理后创建 +Student.from_json(path) # 一眼知道:从 JSON 创建 +Student.from_dict(d) # 一眼知道:从 dict 创建 +``` + +这在团队协作/读代码时很有价值。 + +--- + +**3)支持继承:子类调用时能自动构造子类(cls 的关键作用)** + +这是 `@classmethod` 最大的“技术价值”。 + +如果你写死: + +```python +return Student(name) # 写死类名 +``` + +那子类调用会“造错对象”(Student的子类调用,结果构造出一个父类Student)。 + +正确写法是: + +```python +return cls(name) # 用 cls,子类会自动变成子类 +``` + +示例: + +```python +class Student: + def __init__(self, name): + self.name = name + + @classmethod + def from_fullname(cls, fullname): + return cls(fullname.strip()) + +class Graduate(Student): + pass + +g = Graduate.from_fullname(" Alice ") +print(type(g)) # Graduate(而不是 Student) +``` + +--- + +**4)把 `__init__` 保持“干净”:只负责初始化,不负责复杂解析/IO** +经验:`__init__` 最好做两类事: +- 保存字段(`self.xxx = ...`) +- 基本校验 + +不要在 `__init__` 里塞大量“解析/读文件/请求网络”的逻辑,否则: +- 构造过程变得不可控(容易慢、容易失败) +- 测试更难写 + +于是常见模式是: +- `__init__`:接收“已经处理好的参数” +- `from_xxx`:负责“把外部输入转换成参数”,再 `cls(...)` + + +--- + +#### 9.5.3 `@staticmethod` 静态方法(了解) +它不需要 `self` 也不需要 `cls`,更像“放在类命名空间里的普通函数”。常用于: +- 逻辑上属于该类,但不依赖对象/类状态的小工具函数 +- “归类/组织代码”用 +```python +class Student: + @staticmethod + def is_valid_age(age): + return isinstance(age, int) and age >= 0 + +print(Student.is_valid_age(18)) # True +``` + +--- + +### 9.6 构造器:`__init__`(创建对象时初始化) + +#### 9.6.1 你调用 `Student("Tom", 18)` 发生了什么? +粗略理解为两步: +1) `__new__`:分配对象(很少自己写) +2) `__init__`:初始化对象(最常写) + +你常写的是 `__init__`: +```python +class Student: + def __init__(self, name, age): + self.name = name + self.age = age +``` + +#### 9.6.2 `__init__` 里一般做什么? +- 给实例属性赋初值(最常见) +- 做参数校验(必要时) +```python +class Student: + def __init__(self, name, age): + if age < 0: + raise ValueError("age must be >= 0") + self.name = name + self.age = age +``` + +--- + +### 9.7 三大特性:封装 / 继承 / 多态 + +--- + +#### 9.7.1 封装(Encapsulation) + +**1)一句话总结(核心)** +- **把数据(属性)和操作数据的逻辑(方法)包在一起**,并通过“对外接口”(方法 / `@property`)控制外部访问,避免对象出现非法状态、避免外部随意修改导致不一致。 +- Python 没有绝对私有,但你可以用两种方式“提高成本/控制修改” + - _balance 这种单下划线,只是一个命名约定,类的外部还是可以直接修改 + - __balance 这种双下划线,外部`a.__balance = -1`会报错,但仍可通过 `a._Account__balance` 访问(所以也不是绝对私有)。 +**2)示例(能运行、最简单)** + +```python +class Account: + def __init__(self, balance: int): + # 单下划线:约定“内部属性”,外部别直接改 + # _balance前面的下划线,只是一个命名约定,不是真正私有,在类的外部 a._balance = 1000 还是可以直接修改 + self._balance = balance + + def deposit(self, amount: int): + if amount <= 0: + raise ValueError("amount must be > 0") + self._balance += amount + + def withdraw(self, amount: int): + if amount <= 0: + raise ValueError("amount must be > 0") + if amount > self._balance: + raise ValueError("insufficient funds") + self._balance -= amount + + def get_balance(self): + return self._balance + +a = Account(100) +a.deposit(50) +a.withdraw(30) +print(a.get_balance()) # 120 +``` + +**如果一定要外部不能修改`_balance`,可以用`@property`做校验(常用、推荐)** +```python +class Account: + def __init__(self, balance: int): + self._balance = 0 + self.balance = balance # 走 setter + + @property + def balance(self): + return self._balance + # - `balance(self)` 是一个方法,但加了 `@property` 后,你可以像访问属性一样用它: + # - `a.balance` 实际等价于调用:`a.balance()`(概念上) + # - 它是 **getter(读取方法)**:负责“读余额” + + + @balance.setter + def balance(self, value: int): + if value < 0: + raise ValueError("balance must be >= 0") + self._balance = value + # - 这是 **setter(写入方法)**:负责“写余额” + # - 当你写:a.balance = 100,Python 实际会调用:Account.balance.fset(a, 100) (概念上) + # - 也就是执行你写的 `def balance(self, value)` 这段代码 +``` +这样外部改 `a.balance = -1` 会直接报错(但外部仍然能改 `a._balance`,只是“更不该这么做”)。 + +**你要记住的关键点(笔记版)** +- `self._balance`:内部真实存储(约定内部用) +- `balance`(property):对外公开接口 +- `a.balance`:读时走 getter +- `a.balance = x`:写时走 setter(可校验、可拦截) + +##### **`@property`啥作用?** +1)一句话总结(核心) +- `@property` 可以把一个“方法”伪装成“属性”来访问:你写 `obj.balance`,看起来像访问属性,实际背后调用的是 `balance(self)` 这个方法。 +- 它的价值是:**对外暴露为“属性”,对内可以做校验/转换/只读控制**,从而更好地“封装”。 +2)它解决什么问题?(为什么不用直接暴露 `_balance`) +如果你直接让外部改 `_balance`: +```python +a._balance = -999 +``` +不会报错,但会把对象状态改成非法(余额为负数)——破坏封装。 + +用 `@property` 后,你希望外部写: +```python +a.balance = -999 +``` +直接报错,逼外部通过“合法接口”修改。 + + + +**3)易错点(踩过的坑)** +- Python 的“私有”多是**约定**: + - `_x`:约定内部使用(不强制) + - `__x`:名称改写(name mangling),也不是绝对私有(仍能通过 `_Class__x` 访问,不推荐) +- 不要让外部直接改关键属性绕过校验: + - `a._balance = -999` 虽然能写,但破坏了封装(不推荐) +- `print` 不是“接口”:更推荐提供方法/属性来读写数据,统一校验逻辑 + +**4)应用场景(什么时候用)** +- 需要保证对象状态合法/一致:余额、库存、年龄、权限状态等 +- 需要把校验逻辑集中到类内部,避免到处写重复校验 + +--- + +#### 9.7.2 继承(Inheritance) + +**1)一句话总结(核心)** +- **子类复用父类的属性/方法**,并可扩展或重写(override),用来表达“is-a(是一个)”关系:子类是一种更具体的父类。 + +**2)示例(能运行、最简单)** +```python +class Animal: + def speak(self): + return "..." + +class Dog(Animal): + def speak(self): # 重写父类方法(override) + return "wang" + +class Cat(Animal): + def speak(self): + return "miao" + +print(Dog().speak()) # wang +print(Cat().speak()) # miao +``` + +**3)易错点(踩过的坑)** +- 继承不是“为了少写代码”硬套:要满足稳定的 **is-a** 关系 +- 重写方法时: + - 方法名/参数最好保持一致(否则多态不好用) +- 需要父类初始化时要用 `super()`: +```python +class Base: + def __init__(self, name): + self.name = name + +class User(Base): + def __init__(self, name, role): + super().__init__(name) # 先初始化父类部分 + self.role = role +``` +- 调用父类方法的时候,要不要self +调用父类方法时,不要self,原因在于: +super()会自动把self传给父类方法,如果再传就会产生2个self。 + +- 如何理解 super()会自动把self传给父类方法 ? +super不是“父类”,不是单纯的简化父类名称,而是已绑定当前实例的、按照MRO规则定位到的下一个类的方法代理对象。 +用处:多继承的情况下,一定要用super(),此时还是靠父类.方法的话,会破坏多继承的关系。 +什么是MRO规则?Python在调用方法时,查找类的固定顺序表。 +**4)应用场景(什么时候用)** +- 多个类共享同一套接口/通用行为(比如 `Animal.speak()`、`Shape.area()`) +- 希望一组类型在调用侧能“统一使用”(为多态做铺垫) + +**5)具体例子** +**例子 1:统一“导出器”接口(CSV/JSON 两种实现)** +场景:你有一批数据(list[dict]),要支持导出成不同格式。 +调用方不想写一堆 if fmt == ...,只想统一调用 export()。 + +代码实现效果:**同一份 `rows` 数据,可以用不同“导出器”对象导出成不同格式;调用方只需要统一调用 `export()`。** + +```python + +import csv # 标准库,用来写 CSV 文件 +import json # 标准库,用来把 Python 对象转成 JSON 字符串 +from abc import ABC, abstractmethod # 用于定义“抽象基类”(规定子类必须实现哪些方法) +# 抽象基类是啥?见下 +from pathlib import Path # `pathlib` 的路径对象,比字符串拼路径更安全方便 + +# 定义“导出器接口”(抽象基类) +class Exporter(ABC): # 定义一个父类 `Exporter`,继承自ABC,表示其是一个“抽象基类”,主要用于规定接口(不是用来直接实例化的) + """导出器基类:规定统一接口 export(rows, path)""" + + @abstractmethod # `@abstractmethod`:标记这是“抽象方法”,表示子类必须实现这个方法,否则子类不能实例化 + def export(self, rows: list[dict], path: str | Path) -> None: # -> None 表示不返回值(只负责把文件写出去) + raise NotImplementedError + # raise NotImplementedError 理论上不会被直接执行(因为抽象类不应被实例化);写在这只是“兜底提示”:如果有人绕过抽象机制调用了父类实现,就报错提醒“没实现”。 + # 这句话概念上理解:你可以把它读成中文: + # “我(父类 Exporter)不提供 export 的具体做法;谁继承我,就必须自己实现 export。否则你一调用就报错。” + +# CSVExporter:子类实现 CSV 导出 +class CSVExporter(Exporter): + def export(self, rows: list[dict], path: str | Path) -> None: # 子类实现了export(),所以可实例化、可用 + p = Path(path) + if not rows: + # 为啥要写空文件夹p.write_text("")? + # 因为即使没有数据,调用方也期望导出操作可以生成一个文件。 + p.write_text("", encoding="utf-8") # 当rows为空列表时,not rows 为True,这个时候才执行写入操作 + # 这个时候写入了一个空文件(避免下面rows[0]报错) + # 因为rows为空列表[]时,rows[0]会报错IndexError索引错误 + return # 这个单独的return的作用是结束函数,不需要返回结果 + + fieldnames = list(rows[0].keys()) + # 这里面取出第一行数据的所有键(默认所有行字典的key都一致) + with p.open("w", newline="", encoding="utf-8") as f: + # - `"w"`:写文件(覆盖) + # - `newline=""`:写 CSV 建议这样写,避免 Windows 空行问题 + # 创建一个CSV写入器 DictWriter + w = csv.DictWriter(f, fieldnames=fieldnames) + w.writeheader() # 写 CSV 表头(列名行) + w.writerows(rows) # 把整个 `rows`(很多 dict)逐行写入 CSV 文件 + + +class JSONExporter(Exporter): + def export(self, rows: list[dict], path: str | Path) -> None: + p = Path(path) + p.write_text(json.dumps(rows, ensure_ascii=False, indent=2), encoding="utf-8") + # json.dumps(...)把rows转成字符串 + # - `ensure_ascii=False`:允许中文直接输出(不转成 `\u4e2d` 这种形式) + # - `indent=2`:格式化缩进 2 个空格(更好读) + # p.writer_text(...)把json字符串写入文件 + +# 调用法:统一入口 save_report +def save_report(exporter: Exporter, rows: list[dict], out_path: str) -> None: + # 调用方只认 “Exporter接口” + # `exporter: Exporter`:表示你传进来的对象“至少是一个 Exporter”(或者其子类) + exporter.export(rows, out_path) + +rows = [{"name": "Tom", "age": 18}, {"name": "Alice", "age": 16}] +save_report(CSVExporter(), rows, "report.csv") +save_report(JSONExporter(), rows, "report.json") +print("done") +``` + +你能从这个例子看到: +- `Exporter` 是“父类/接口”,规定能力边界 +- `CSVExporter/JSONExporter` 是子类,复用“接口约束”,各自实现细节 +- 调用方 `save_report()` 不需要关心子类类型 → 更容易扩展(以后加 XMLExporter 不用改调用方) + + +**例子 2:自定义业务异常体系(用继承做“分类”)** +场景:业务里错误很多,但你希望调用方能: +- 一次性捕获所有业务错误:`except BizError` +- 也能针对具体错误分开处理:`except AuthError` / `ConfigError` + +```python +class BizError(Exception): + """业务异常基类(所有业务错误统一从这里继承)""" + # 这种写法是可以的 + # Python 要求 `class` 代码块里至少要有一条语句;docstring 就算一条语句(一个字符串字面量表达式)。 +class ConfigError(BizError): + """配置错误""" + +class AuthError(BizError): + """认证/权限错误""" + +def load_config(): + raise ConfigError("config missing") + +def login(): + raise AuthError("token invalid") + +try: + load_config() + login() +except ConfigError as e: + print("配置问题:", e) +except AuthError as e: + print("登录问题:", e) +except BizError as e: + print("其他业务错误:", e) +``` +**这种异常类为什么“看起来啥都没做”?** +因为自定义异常很多时候只是为了: +- **起一个更有语义的名字**(让业务层捕获更清晰) +- **形成继承层级**(能 `except BizError` 一次抓住一类业务错误) + +它不需要额外属性/方法也能工作: +```python +raise ConfigError("config missing") +``` +`ConfigError` 继承自 `Exception`,已经具备异常的所有基础行为(message、traceback 等)。 + +**例子 3:支付方式(一个父类接口,多种子类实现)** +场景:订单支付可以是微信/支付宝/银行卡,但调用方只想调用 `pay(amount)`。 + +```python +from abc import ABC, abstractmethod + +class Payment(ABC): + @abstractmethod + def pay(self, amount: int) -> str: + raise NotImplementedError + +class WeChatPay(Payment): + def pay(self, amount: int) -> str: + return f"WeChat paid {amount}" + +class AliPay(Payment): + def pay(self, amount: int) -> str: + return f"AliPay paid {amount}" + +def checkout(payment: Payment, amount: int): + # 统一入口:不关心具体支付类型 + print(payment.pay(amount)) + +checkout(WeChatPay(), 100) +checkout(AliPay(), 200) +``` + +--- + +##### **补充说明** +**1)抽象基类(ABC)+ 抽象方法(@abstractmethod)到底是干啥的?** +**一句话总结** +- **抽象基类**用来“规定接口/规范”:要求子类必须实现某些方法。 +- **抽象方法**就是“必须由子类实现的方法”,父类只声明,不提供可用实现。 + +**2)`@abstractmethod` 的效果是什么?** +关键效果:**如果子类没有实现所有抽象方法,它就不能被实例化**。 + +在你这个例子里,父类 `Exporter` 的意思是: +> 任何导出器都必须有一个 `export(rows, path)` 方法,不然就不算导出器。 +例如(子类忘了实现 export): +```python +from abc import ABC, abstractmethod + +class Exporter(ABC): + @abstractmethod + def export(self, rows, path): + raise NotImplementedError + +class BadExporter(Exporter): + pass + +BadExporter() # TypeError: Can't instantiate abstract class BadExporter ... +``` + +也就是说:根本不让你“造出”一个不完整的导出器对象,避免运行到一半才发现少方法。 + + +**3)那为什么抽象方法里还要写 `raise NotImplementedError`?** +**一句话总结** +- `@abstractmethod` 是“类层面的限制”(阻止你实例化不完整子类) +- `raise NotImplementedError` 是“运行时兜底”(如果你真的调用到了父类这段实现,就明确报错:**你没实现**) + +#### 9.7.3 多态(Polymorphism) + +**1)一句话总结(核心)** +- **同一种调用方式(同一个接口/同名方法)**,传入不同对象会表现出不同结果;调用方不需要关心具体类型,只关心“能不能这么用”。 + +**2)示例(能运行、最简单)** +```python +class Dog: + def speak(self): + return "wang" + +class Cat: + def speak(self): + return "miao" + +def make_it_speak(x): + # 不关心 x 是 Dog 还是 Cat,只要它有 speak() 就行 + print(x.speak()) + +make_it_speak(Dog()) # wang +make_it_speak(Cat()) # miao +``` + +**3)易错点(踩过的坑)** +- Python 的多态更偏“鸭子类型”(duck typing): + - 不要求必须继承同一个父类 + - 只要对象“长得像”(有同样的方法)就行 +- 如果不同类的方法名/参数不一致,调用方就会被迫写很多 `if/elif`(失去多态价值) + +**4)应用场景(什么时候用)** +- 写可扩展代码:新增类型只要实现同样方法,调用方代码不改 +- 典型:支付方式 `pay()`、不同存储后端 `save()`、不同解析器 `parse()` + +--- + +#### 9.7.4 三者放一起的理解(复习版) +- **封装**:把“内部状态”收口,外部通过接口访问(可校验、可控) +- **继承**:复用与扩展父类能力(建立类型层次) +- **多态**:统一接口调用,不同对象不同表现(对扩展友好) + +--- + +#### 9.7.5 (补充建议)组合优于继承(了解但很常用) +##### 一句话总结 +- “has-a(有一个)”关系优先用组合,耦合更低;“is-a(是一个)”才用继承。 + +##### 示例(能运行、最简单) +```python +class Engine: + def start(self): + return "engine start" + +class Car: + def __init__(self): + self.engine = Engine() # 组合:Car 有 Engine + + def start(self): + return self.engine.start() + +c = Car() +print(c.start()) # engine start +``` + + + + +### 9.7 你需要记住的“查找规则”(理解很多现象的关键) +当你写 `obj.attr` 时,Python 大致按这个顺序找: +1) 先找对象自己的实例属性(`obj.__dict__`) +2) 找类属性(`obj.__class__` 及其父类) +3) 找不到就 `AttributeError` + +所以才会出现: +- 实例属性会遮住同名类属性 +- `obj.method()` 能工作:因为 `method` 在类里定义,实例查不到就去类里找,然后绑定成方法 + +--- + +### 9.8 新手高频易错点(建议写进笔记) +1) 在类体里写 `name = ""` 不等于“每个对象都有 name” +- 那是 **类属性**,所有实例共享;通常你想要的是在 `__init__` 里写 `self.name = ...` + +2) 忘记 `self` +```python +class A: + def f(): # ❌ 少了 self + pass +``` +调用 `a.f()` 会报类型错误。 + +3) 实例方法里要用 `self.xxx` 访问属性 +写 `name` 只是局部变量/未定义名字,不是属性。 + +--- + + +### 9.9 数据类(推荐):`dataclasses.dataclass` + +--- + +#### 9.9.1 一句话总结(核心) +- `@dataclass` 用来快速定义“主要用来存数据”的类:**自动生成大量样板代码**(`__init__`、`__repr__`、`__eq__` 等),让你少写重复代码、类更清爽、更不容易写错。 +- 适合:**数据结构/DTO/配置/坐标点/返回结果对象**(字段固定、行为较少) + +--- + +#### 9.9.2 示例(能运行、最简单) +```python +from dataclasses import dataclass + +@dataclass +class User: + name: str + age: int + +u = User("Tom", 18) +print(u) # User(name='Tom', age=18) (自动生成 __repr__) +print(u.name) # Tom (自动生成 __init__) +print(u == User("Tom", 18)) # True (自动生成 __eq__) +``` + +--- + +#### 9.9.3 `@dataclass` 默认帮你生成了什么? +对上面的 `User`,等价于你手写了这些(概念上): +- `__init__(self, name, age)`:初始化字段 +- `__repr__(self)`:打印友好的对象表示 +- `__eq__(self, other)`:字段相等性比较(按字段逐个比) + +你可以用一句话记: +- **减少样板代码**:不用重复写构造器/打印/比较 + +--- + +#### 9.9.4 常用参数(最常用的 4 个) + +##### 1)`frozen=True`:不可变(更安全,像“值对象”) +```python +from dataclasses import dataclass + +@dataclass(frozen=True) +class Point: + x: int + y: int + +p = Point(1, 2) +# p.x = 10 # ❌ 会报错:FrozenInstanceError(不允许修改) +``` +用途: +- 坐标、配置、常量数据:不希望被改 +- 可作为 dict key(前提:字段本身也可哈希) + +##### 2)`order=True`:自动生成排序比较(< <= > >=) +```python +from dataclasses import dataclass + +@dataclass(order=True) +class User: + age: int + name: str + +print(User(18, "Tom") < User(20, "Bob")) # True(按字段顺序比较:先 age 再 name) +``` +注意: +- 比较顺序由字段定义的先后决定,优先比较排序在前的字段 + +##### 3)`slots=True`(Python 3.10+):更省内存 + 更限制随意加属性 +```python +from dataclasses import dataclass + +@dataclass(slots=True) +class User: + name: str + age: int + +u = User("Tom", 18) +# u.extra = 1 # ❌ 通常会报 AttributeError(不允许随便加新属性) +``` +用途: +- 大量对象时节省内存 +- 防止随意动态加属性(更像“固定结构”) + +##### 4)`kw_only=True`:强制关键字参数传入(API 更清晰) +```python +from dataclasses import dataclass + +@dataclass(kw_only=True) +class User: + name: str + age: int + +# User("Tom", 18) # ❌ 位置参数不允许 +u = User(name="Tom", age=18) # ✅ +``` + +--- + +#### 9.9.5 字段的默认值与 `default_factory`(重要易错点) +##### 1)普通默认值 +```python +from dataclasses import dataclass + +@dataclass +class User: + name: str + age: int = 18 +``` + +##### 2)可变默认值:必须用 `default_factory`(高频坑) +不要这样写: +```python +# ❌ 错误:list 是可变对象,dataclass 会直接报错阻止你 +# @dataclass +# class Bad: +# tags: list[str] = [] +``` + +正确写法: +```python +from dataclasses import dataclass, field + +@dataclass +class User: + name: str + tags: list[str] = field(default_factory=list) + +u1 = User("Tom") +u2 = User("Alice") +u1.tags.append("vip") +print(u1.tags) # ['vip'] +print(u2.tags) # [](不会共享) +``` + +> 记忆句:**可变默认值(list/dict/set)用 `field(default_factory=...)`,防止可变类型默认值被不同对象共享** + +--- + +#### 9.9.6 `__post_init__`:初始化后的校验/派生字段 +当你仍想做校验(类似你手写 `__init__` 里那种校验),用 `__post_init__`: + +```python +from dataclasses import dataclass + +@dataclass +class User: + name: str + age: int + + def __post_init__(self): + if self.age < 0: + raise ValueError("age must be >= 0") +``` + +说明: +- `dataclass` 先自动生成 `__init__` 把字段赋值 +- 然后自动调用 `__post_init__` 让你补充校验/计算 + +--- + +#### 9.9.7 易错点(踩过的坑) +- `@dataclass` 不是“必须用”,它适合“以数据为主”的类;业务逻辑很重的类用普通 class 也很正常 +- 可变默认值坑:用 `default_factory` +- `order=True` 的比较顺序是“字段顺序”,字段顺序写错会导致排序语义不符合预期 +- `frozen=True` 不是绝对安全(仍可通过一些底层方式绕过),但足够表达“不要改”的意图 + +--- + +#### 9.9.8 应用场景(什么时候用) +- 作为“数据载体”:配置对象、DTO、数据库/接口返回结构 +- 需要很多字段 + 频繁创建对象 +- 希望对象打印出来可读(调试友好) +- 希望天然支持相等比较(例如测试断言) + +#### 9.9.9 `dataclasses.dataclass` 一个实际例子:接口返回的 Response DTO +场景:你写了个“获取用户信息”的函数(模拟从数据库/接口拿到 dict),然后要把结果返回给调用方。 +如果直接返回 dict,调用方到处写 `user["name"]`、字段名还可能拼错。 + +用 `@dataclass` 定义一个 DTO,把 dict 转成结构化对象。 + +```python +from dataclasses import dataclass +from datetime import datetime +from typing import Any + +# 1) 定义 DTO:字段 + 类型 + 少量校验/派生字段 +@dataclass(slots=True) +class UserDTO: + id: int + name: str + age: int + created_at: datetime + + def __post_init__(self): # @dataclass情况下,做初始化后的校验 + if self.age < 0: + raise ValueError("age must be >= 0") + + @classmethod + def from_dict(cls, d: dict[str, Any]) -> "UserDTO": + # 2) 替代构造器:把“外部脏数据/字符串时间”解析成干净字段 + return cls( + id=int(d["id"]), + name=str(d["name"]).strip(), + age=int(d["age"]), + created_at=datetime.fromisoformat(d["created_at"]), + ) + + def to_dict(self) -> dict[str, Any]: + # 3) 需要输出成 JSON 时,把 datetime 转 string(JSON 不认识 datetime 对象) + return { + "id": self.id, + "name": self.name, + "age": self.age, + "created_at": self.created_at.isoformat(), + } + + +# ===== 下面模拟“外部数据来源” ===== +def fetch_user_row_from_db(uid: int) -> dict[str, Any]: + # 真实项目里可能来自数据库/HTTP/缓存,这里用 dict 模拟 + return { + "id": uid, + "name": " Tom ", + "age": "18", # 注意:外部给的可能是字符串 + "created_at": "2026-01-15T12:00:00", + } + + +# ===== 调用侧 ===== +row = fetch_user_row_from_db(1) +user = UserDTO.from_dict(row) # dict -> DTO(结构化),思路:在数据进入业务层的入口处,把dict统一转换为结构化对象 +print(user) # 自动 __repr__:UserDTO(id=1, name='Tom', age=18, created_at=...) +print(user.name) # 点属性访问,比 user["name"] 更安全清晰 + +payload = user.to_dict() # DTO -> dict(便于 JSON 序列化) +print(payload) +``` + +--- + +### 9.10 设计习惯汇总:Python 面向对象(OOP)写代码更“稳”的做法(含例子) + +--- + +#### 9.10.1 组合优于继承(Composition over Inheritance) +**一句话总结(核心)** +- “has-a(有一个)”用组合;“is-a(是一种)”才用继承。组合耦合更低、可替换、好测试。 + +**示例(能运行、最简单)** +```python +class FileStorage: + def save(self, key: str, data: str) -> None: + print(f"save {key}: {data}") + +class UserService: + def __init__(self, storage: FileStorage): + self.storage = storage # 组合:服务“拥有”一个存储组件 + + def create_user(self, name: str) -> None: + self.storage.save(f"user:{name}", "created") + +svc = UserService(FileStorage()) +svc.create_user("Tom") +``` + +**应用场景** +- 存储后端可换(file/db/redis)、支付方式可换、策略可换(更像插件) + +--- + +#### 9.10.2 面向接口编程(依赖抽象,不依赖具体实现) +**一句话总结(核心)** +- 调用方依赖“能力/协议”,不要依赖某个具体类;新增实现时不改调用方。 + +**示例(用 Protocol 表达接口,Pythonic)** +**这段代码在解决什么问题?(整体意思)** +你想写一个 `UserService`(用户服务),它需要“把数据保存起来”。 +如果你把 `UserService` 写死依赖某个具体存储(比如 `MemoryStorage`): + +- 以后想换成 `FileStorage` / `DBStorage` / `RedisStorage` +- 就得改 `UserService` 的代码(耦合太强) + +**面向接口编程**的做法是: +- `UserService` 只依赖“能力”(能 `save(key, data)`) +- 不依赖“具体实现”(到底存到内存、文件、数据库) + +这样新增/替换存储实现时,`UserService` 不用改。 + +```python +from typing import Protocol +# - `Protocol` 用来定义一种“接口/协议”:**只要一个类满足这些方法签名,就算实现了这个接口** +# - 它是“类型注解层面”的东西,主要服务于 IDE / 类型检查(pyright/mypy) + +class Storage(Protocol): # 定义一个协议(接口),语义:任何“存储器”都应该满足这个协议 + def save(self, key: str, data: str) -> None: ... # 末尾的 `...`(Ellipsis)表示“这里不写实现,只是声明接口” + # 声明:实现 Storage 的对象必须有一个方法: + # 方法名:save + # 参数有key、data + # 返回值是None + # 注意:这不是抽象基类 ABC,它不会强制阻止你实例化;它主要用于**静态类型检查**。 + +class MemoryStorage: + def __init__(self): + self.data = {} # `self.data = {}`:用一个字典作为“存储容器” + + def save(self, key: str, data: str) -> None: + self.data[key] = data # 把数据存进字典,key存key,value存data + # 以上定义的这个 内存存储 具体实现类,恰好会实现save(),符合Storage协议(即使没有些class MemoryStorage(Storage)) + +class UserService: + # 以上定义业务服务类,实现用户相关逻辑 + def __init__(self, storage: Storage): + self.storage = storage + # - `UserService` 构造器接收一个 `storage` + # - 重点:类型注解写的是 `Storage`(协议),不是 `MemoryStorage` + # - 含义:**只要你传进来的对象“有 save()”,就行** + # - `self.storage = storage`:保存依赖(依赖注入/DI 的最小形式) + + def create_user(self, name: str) -> None: + self.storage.save(f"user:{name}", "created") + # create_user是业务方法:创建用户 + # f"user:{name}" 是key, "created" 是 value +svc = UserService(MemoryStorage()) +# - `MemoryStorage()`:创建一个“内存存储”实例 +# - 把它传给 `UserService(...)`:注入依赖 +# - 得到 `svc`(服务对象) +svc.create_user("Tom") +``` + +--- + +#### 9.10.3 单一职责(SRP):类/方法只做一件事 +**一句话总结(核心)** +- 一个类职责越多越难改;把“业务逻辑”“IO(文件/网络)”“解析/校验”拆开。 + +**示例:把解析与模型分开(DTO + 业务)** +```python +from dataclasses import dataclass + +@dataclass +class UserDTO: + name: str + age: int + + @classmethod + def from_dict(cls, d: dict) -> "UserDTO": + return cls(name=str(d["name"]).strip(), age=int(d["age"])) + +class UserService: + def is_adult(self, user: UserDTO) -> bool: + return user.age >= 18 +``` + +--- + +#### 9.10.5 保持 `__init__` 干净:初始化 + 基本校验即可 +**一句话总结(核心)** +- `__init__` 不做重 IO/复杂解析。复杂输入用 `@classmethod from_xxx` 处理。 + +**示例** +```python +class User: + def __init__(self, name: str, age: int): + if age < 0: + raise ValueError("age must be >= 0") + self.name = name + self.age = age + + @classmethod + def from_dict(cls, d: dict) -> "User": + return cls(name=str(d["name"]).strip(), age=int(d["age"])) +``` + +--- + +#### 9.10.5 封装:对外给“稳定接口”,内部字段别裸露乱改 +**一句话总结(核心)** +- 外部通过方法/`@property` 访问;内部字段用 `_x` 约定;关键约束集中校验。 + +**示例** +```python +class Account: + def __init__(self, balance: int): + self._balance = 0 + self.balance = balance # 走 setter + + @property + def balance(self) -> int: + return self._balance + + @balance.setter + def balance(self, value: int) -> None: + if value < 0: + raise ValueError("balance must be >= 0") + self._balance = value +``` + +--- + +#### 9.10.6 多态:让调用方“只写一次”,扩展靠新增类 +**一句话总结(核心)** +- 调用方只依赖统一方法名/接口;新增类型不用改调用方(开闭原则)。 + +**示例:导出器 `export()`(你笔记已有同款)** +```python +from abc import ABC, abstractmethod + +class Exporter(ABC): + @abstractmethod + def export(self, rows, path): ... + +class CSVExporter(Exporter): + def export(self, rows, path): + print("export csv:", path) + +class JSONExporter(Exporter): + def export(self, rows, path): + print("export json:", path) + +def save_report(exporter: Exporter, rows, path): + exporter.export(rows, path) + +save_report(CSVExporter(), [{"a": 1}], "a.csv") +save_report(JSONExporter(), [{"a": 1}], "a.json") +``` + +--- + +#### 9.10.7 少用继承层级,避免“深继承/多继承复杂度” +**一句话总结(核心)** +- 继承层级深、父类改动影响大;多继承涉及 MRO/super 链,维护成本高。能组合就组合。 + +**示例:用组合替代“功能混合继承”** +```python +class Logger: + def log(self, msg: str) -> None: + print("[log]", msg) + +class Service: + def __init__(self, logger: Logger): + self.logger = logger + + def run(self): + self.logger.log("running...") +``` + +--- + +#### 9.10.8 数据为主的对象:优先 `@dataclass`,少写样板代码 +**一句话总结(核心)** +- DTO/配置/返回结果等“字段固定、行为少”的类,用 dataclass 更清晰。 + +**示例** +```python +from dataclasses import dataclass, field + +@dataclass(slots=True) +class Config: + host: str + port: int = 3306 + tags: list[str] = field(default_factory=list) +``` + +--- + +#### 9.10.9 一页总结(你可以直接贴到笔记) +- 组合优于继承(has-a 用组合,is-a 才继承) +- 面向接口编程(依赖抽象/协议,方便替换与扩展) +- 单一职责(业务/IO/解析拆分) +- `__init__` 保持干净(复杂输入用 from_xxx) +- 封装内部状态(方法/property 控制访问) +- 用多态消灭 if/elif 分发(统一接口,新增类型不改调用方) +- 避免深继承/多继承(复杂度高) +- 数据类用 `@dataclass`(DTO/配置/结果对象) + + +--- + +## 10 类型注解(Typing) + +### 10.1 一句话总结(核心) +- **类型注解(type hints)**是写在代码里的“说明书”:告诉别人(也告诉 IDE/类型检查器)**这个变量/参数/返回值应该是什么类型**。 +- 它主要解决:**可读性 + 自动补全/跳转 + 提前发现类型错误(静态检查)**。 +- 重要:**类型注解默认不影响运行**(Python 运行时通常不会因为你标了类型就自动检查,除非你自己加校验或用 pydantic 等工具)。 + +--- + +### 10.2 基础注解:参数与返回值(最常用) +#### 10.2.1 最小示例 +```python +def add(a: int, b: int) -> int: + return a + b +``` + +逐个解释: +- `a: int`:参数 a 期望是 int +- `b: int`:参数 b 期望是 int +- `-> int`:返回值期望是 int + +> 运行时你仍然可以传错类型(Python 不会自动拦): +> 但 IDE/pyright/mypy 会提示你“这里类型不匹配”。 + +--- + +### 10.3 常用容器类型:`list[str]`、`dict[str, int]` +#### 10.3.1 `list[str]`:字符串列表 +```python +def join_names(names: list[str]) -> str: + return ",".join(names) +``` +含义:`names` 是一个列表,列表元素都是 `str`。 + +#### 10.3.2 `dict[str, int]`:key 为 str、value 为 int 的字典 +```python +def total_score(scores: dict[str, int]) -> int: + return sum(scores.values()) +``` +含义: +- 键(key)是 `str` +- 值(value)是 `int` + +--- + +### 10.4 版本差异(你之前用 Python 3.7,很关键) +你笔记里写的这些: + +- `list[str]`、`dict[str, int]`:需要 **Python 3.9+** +- `str | Path`:需要 **Python 3.10+** + +如果你用 **Python 3.7**,要写成旧写法: + +```python +from typing import List, Dict + +def f(names: List[str], scores: Dict[str, int]) -> int: + ... +``` + +> 建议:笔记如果面向新版本学习,直接以 **Python 3.10+** 为主(语法更清爽)。 + +--- + +### 10.5 typing 常用工具 + +#### 10.5.1 `Optional` / `Union`(本质:`T | None` / `A | B`) +**一句话总结(核心)** +- `T | None`:要么是 `T`,要么是 `None` +- `A | B`:要么是 `A`,要么是 `B` +- 用来把“可能缺失/多类型输入”的事实写进接口里,逼自己处理边界情况 + +**示例(能运行、最简单)** +```python +def find_user_name(uid: int) -> str | None: + if uid == 1: + return "Tom" + return None + +name = find_user_name(2) +if name is None: + print("not found") +else: + print(name.upper()) +``` + +```python +def parse_age(x: str | int) -> int: + return int(x) + +print(parse_age("18")) +print(parse_age(20)) +``` + +**易错点** +- `str | None` 的值可能是 `None`:使用前必须判空,否则会 TypeError/AttributeError。 + +--- + +#### 10.5.2 `TypedDict`(了解:给“字典形状”做类型约束) +**一句话总结(核心)** +- `TypedDict` 用来描述 dict 的固定结构:有哪些 key、每个 key 的 value 是什么类型。 +- 适合:你从 JSON/接口/数据库拿到 dict,但想要更强提示与更少“拼 key 出错”。 + +**示例(能运行、最简单)** +```python +from typing import TypedDict + +class UserRow(TypedDict): + id: int + name: str + age: int + +def get_user() -> UserRow: + return {"id": 1, "name": "Tom", "age": 18} + +u = get_user() +print(u["name"]) +``` + +**易错点** +- `TypedDict` 主要用于静态检查;运行时不会自动阻止你塞错类型。 +- 结构很自由/动态的 dict 不适合 TypedDict。 + +--- + +#### 10.5.3 `Protocol`(了解:面向接口/能力编程) +**一句话总结(核心)** +- `Protocol` 用来定义“能力/接口”:**不要求继承,只要方法签名满足就算实现**(鸭子类型 + 静态检查)。 +- 让你的业务类只依赖能力,不依赖具体实现,从而更易扩展、更易测试。 + +**示例(能运行、最简单)** +```python +from typing import Protocol + +class Storage(Protocol): + def save(self, key: str, data: str) -> None: ... + +class MemoryStorage: + def __init__(self): + self.data: dict[str, str] = {} + + def save(self, key: str, data: str) -> None: + self.data[key] = data + +class UserService: + def __init__(self, storage: Storage): + self.storage = storage + + def create_user(self, name: str) -> None: + self.storage.save(f"user:{name}", "created") + +store = MemoryStorage() +svc = UserService(store) +svc.create_user("Tom") +print(store.data) # {'user:Tom': 'created'} +``` + +**易错点** +- `Protocol` 是类型系统概念:运行时不会强制检查“你传入的对象是否真的实现了接口”,但 IDE/pyright 会提示。 + +--- + +### 10.5 工具链:pyright / mypy + +#### 10.5.1 pyright(VS Code 友好) +- pyright 是静态类型检查器;VS Code 的 Pylance 会基于它提供类型提示/报错提示。 +- 建议学习阶段把 VS Code 的类型检查开到 `basic`,逐步提高。 + +(可选)VS Code 设置项: +- `python.analysis.typeCheckingMode`: `"off" | "basic" | "strict"` + +#### 10.5.2 mypy(可选) +- mypy 也是类型检查器,常用于 CI 里强制类型检查。 +- 如果你主要依赖 VS Code 的提示,pyright 足够;团队工程化再考虑 mypy。 + + +--- + + +## 11 魔法方法 / Python 数据模型(dunder methods) + +### 11.1 一句话总结(核心) +- “魔法方法/dunder 方法”是 Python 数据模型的一部分:**让你的类像内置类型一样工作**。 +- 你写的很多语法糖其实都会被翻译成魔法方法调用,例如: + - `print(x)` → 调 `x.__str__()` 或 `x.__repr__()` + - `x == y` → 调 `x.__eq__(y)` + - `len(x)` → 调 `x.__len__()` + - `for i in x` → 调 `x.__iter__()`(或退化到 `__getitem__`) + - `with ... as f:` → 调 `__enter__` / `__exit__` + +--- + +## 11.2 表示与调试:`__repr__` / `__str__` + +### 11.2.1 一句话总结(核心) +- `__repr__`:给开发者看的,目标是“**尽量可复现/可定位**”,用于调试、交互式环境、日志。 +- `__str__`:给用户看的,目标是“**更友好/更可读**”,用于 `print()` 等显示。 + +> 经验:`repr(x)` 偏“程序员视角”,`str(x)` 偏“用户视角”。 + +### 11.2.2 示例(能运行、最简单) +```python +class User: + def __init__(self, name: str, age: int): + self.name = name + self.age = age + + def __repr__(self) -> str: + # 推荐:包含类名 + 关键字段,便于定位 + return f"User(name={self.name!r}, age={self.age})" + # !r 表示对 name 用 repr() 格式(字符串会带引号,更适合调试) + + def __str__(self) -> str: + # 更友好展示 + return f"{self.name}({self.age})" + +u = User("Tom", 18) +print(u) # Tom(18) -> __str__ +print(repr(u)) # User(name='Tom', age=18) -> __repr__ +``` + +### 11.2.3 易错点(踩过的坑) +- `__repr__` 返回的一定要是 `str`,不要返回别的类型。 +- 写 `__str__` 时别丢掉关键信息;线上日志通常更想看到 `repr` 那种详细信息。 +- 如果只实现 `__repr__` 而不实现 `__str__`:`str(x)` 会退回使用 `__repr__`(通常没问题)。 + +### 11.2.4 应用场景(什么时候用) +- 定义 DTO / 配置 / 业务对象时:打印对象能一眼看懂内容(调试效率大幅提升) +- 日志与报错信息:`repr(obj)` 更适合排查 + +--- + +## 11.3 比较与排序:`__eq__` / `__lt__` 等 + +### 11.3.1 一句话总结(核心) +- `==` 由 `__eq__` 决定;排序由 `__lt__`(以及可能的 `__le__` / `__gt__` / `__ge__`)决定。 +- 当类型不支持比较,或比较对象类型不匹配时,返回 `NotImplemented` 让 Python 尝试右侧比较或最终报错。 + +### 11.3.2 示例:只实现相等比较(==) +```python +class Point: + def __init__(self, x: int, y: int): + self.x = x + self.y = y + + def __eq__(self, other) -> bool: + if not isinstance(other, Point): + return NotImplemented + return (self.x, self.y) == (other.x, other.y) + +print(Point(1, 2) == Point(1, 2)) # True +print(Point(1, 2) == (1, 2)) # False(会走 NotImplemented -> False 或 TypeError,取决于对方实现) +``` + +### 11.3.3 示例:实现可排序(<),并用 `functools.total_ordering` 补全其它比较 +```python +from functools import total_ordering + +@total_ordering +class User: + def __init__(self, name: str, score: int): + self.name = name + self.score = score + + def __eq__(self, other) -> bool: + if not isinstance(other, User): + return NotImplemented + return self.score == other.score + + def __lt__(self, other) -> bool: + if not isinstance(other, User): + return NotImplemented + return self.score < other.score + +users = [User("A", 80), User("B", 90), User("C", 85)] +users.sort() +print([u.score for u in users]) # [80, 85, 90] +``` + +### 11.3.4 易错点(踩过的坑) +- **不要在类型不匹配时返回 False**,建议返回 `NotImplemented`: + - `False` 表示“我明确比较结果就是不相等” + - `NotImplemented` 表示“我不知道怎么比”,让 Python 决定下一步(更符合协议) +- 只实现 `__lt__` 并不一定能支持所有比较运算符(`<=`/`>` 等);需要你实现更多,或用 `@total_ordering`。 +- 如果你的对象可变且用来参与比较/作为 key,值变动可能导致排序结果/集合行为出问题(一般建议“值对象”用不可变)。 + +### 11.3.5 应用场景(什么时候用) +- 自定义“值对象”(坐标、金额、版本号、时间段等) +- 需要排序:排行榜、优先队列、按业务字段排序 + +--- + +## 11.4 容器与迭代协议:`__len__` / `__iter__` / `__getitem__` + +### 11.4.1 一句话总结(核心) +- `__len__`:让对象支持 `len(obj)`(并影响 truthiness:长度为 0 时 `bool(obj)` 通常为 False) +- `__iter__`:让对象可 `for x in obj` 迭代 +- `__getitem__`:让对象支持下标访问 `obj[i]`(可用于构建“序列/映射风格”对象;有时也能让对象可迭代) + +### 11.4.2 示例:自定义一个“行集合”,支持 len 与迭代 +```python +class Rows: + def __init__(self, items: list[str]): + self._items = items + + def __len__(self) -> int: + return len(self._items) + + def __iter__(self): + # 直接把迭代委托给内部 list + return iter(self._items) + +r = Rows(["a", "b", "c"]) +print(len(r)) # 3 +for x in r: + print(x) # a b c +print(bool(r)) # True(len > 0) +print(bool(Rows([]))) # False(len == 0) +``` + +### 11.4.3 示例:实现 `__getitem__`,支持下标与切片 +```python +class IntRange: + def __init__(self, start: int, stop: int): + self.start = start + self.stop = stop + + def __len__(self) -> int: + return max(0, self.stop - self.start) + + def __getitem__(self, idx): + # 支持切片 + if isinstance(idx, slice): + start, stop, step = idx.indices(len(self)) + return [self[i] for i in range(start, stop, step)] + + # 支持 int 下标(含负数) + if idx < 0: + idx += len(self) + if idx < 0 or idx >= len(self): + raise IndexError(idx) + return self.start + idx + +ir = IntRange(10, 15) # 10..14 +print(ir[0]) # 10 +print(ir[-1]) # 14 +print(ir[1:4]) # [11, 12, 13] +``` + +### 11.4.4 易错点(踩过的坑) +- `__iter__` 应该返回一个迭代器;最简单就是 `return iter(self._items)`。 +- 只实现 `__getitem__` 也可能被 `for` 当作可迭代对象(Python 会尝试从索引 0 开始取,直到 `IndexError`),但这属于“退化路径”,**建议你要迭代就实现 `__iter__`**。 +- `__len__` 必须返回非负整数,否则会报错。 + +### 11.4.5 应用场景(什么时候用) +- 包装容器:对 list/dict 做业务封装(限制写操作、加校验、加统计) +- 自定义序列/视图:分页结果、只读集合、数据窗口(window) + +--- + +## 11.5 上下文管理:`with`、`__enter__` / `__exit__` + +### 11.5.1 一句话总结(核心) +- `with` 用于管理“需要成对出现的操作”:**获取资源 → 使用 → 释放资源**。 +- `__enter__`:进入 with 时做准备,并返回 `as` 后绑定的对象 +- `__exit__`:离开 with 时做清理;即使 with 内部异常也会执行(类似 try/finally) + +### 11.5.2 示例:写一个计时器上下文(最常见) +```python +import time + +class Timer: + def __enter__(self): + self.t0 = time.perf_counter() + return self # as timer 绑定到这个对象 + + def __exit__(self, exc_type, exc, tb): + # exc_type/exc/tb:如果 with 内抛异常,这里能拿到异常信息;否则都是 None + dt = time.perf_counter() - self.t0 + print(f"cost: {dt:.6f}s") + return False # False 表示“不要吞异常”,异常照常往外抛 + +with Timer(): + s = 0 + for i in range(1_000_000): + s += i +``` + +### 11.5.3 示例:用 `__exit__` 选择“吞掉某类异常”(了解) +```python +class IgnoreZeroDivision: + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, tb): + if exc_type is ZeroDivisionError: + print("ignored ZeroDivisionError") + return True # True 表示“吞掉异常”,with 外不会再看到异常 + return False + +with IgnoreZeroDivision(): + 1 / 0 +print("still running") +``` + +### 11.5.4 易错点(踩过的坑) +- `__exit__` 的返回值非常关键: + - `True`:吞掉异常(不再向外抛) + - `False`:不吞异常(让异常继续抛出) +- 绝大多数场景不要吞异常,除非你很确定这是你要的控制流。 +- `with open(...)` 之所以安全,就是因为文件对象实现了上下文管理,保证 close 会被调用。 + +### 11.5.5 应用场景(什么时候用) +- 文件/网络连接/锁:确保释放资源(最常见) +- 数据库事务:成功提交、失败回滚 +- 临时修改环境(例如切换目录/设置临时配置),退出时恢复 + + + +--- + +## 12 工程化与质量保障(建议补齐) + +### 12.1 Git 基础工作流(分支、提交、pull/push、PR) + +#### 12.1.1 一句话总结(核心) +- Git 用来做**版本控制**:记录每次改动(commit),支持多人协作(branch/merge/PR),在出问题时可以回退(revert/reset)。 +- 日常开发最稳的流程: + 1)从远端拉最新代码 `pull` + 2)新建分支开发 `checkout -b` + 3)小步提交 `add/commit` + 4)推送到远端 `push -u` + 5)开 PR(Pull Request)让别人 review 并合并到主分支 + +--- + +#### 12.1.2 核心概念速记(先把名词对齐) +- **仓库(repo)**:一个项目的版本库(包含 `.git/`) +- **工作区(working tree)**:你在磁盘上看到的代码文件 +- **暂存区(staging area / index)**:`git add` 后进入的“待提交清单” +- **提交(commit)**:一次“快照 + 说明”(不可变历史) +- **分支(branch)**:一条独立的提交链(比如 `main`、`feature/login`) +- **远端(remote)**:例如 GitHub 上的仓库,通常叫 `origin` +- **PR(Pull Request)**:把你的分支改动请求合并到目标分支(如 `main`),并进行 review + +--- + +#### 12.1.3 最常用命令(入门必会 10 个) +```bash +git status # 看当前状态:改了什么、是否已暂存 +git add # 把文件加入暂存区 +git add . # 把当前目录下所有改动加入暂存区(谨慎:确认无杂文件) +git commit -m "msg" # 创建一次提交 +git log --oneline --graph # 简洁查看提交历史(非常好用) + +git branch # 查看本地分支 +git switch -c # 新建并切换分支(推荐) +git switch # 切换分支 + +git pull # 拉取并合并远端最新代码(= fetch + merge/rebase) +git push # 推送到远端 +``` + +--- + +#### 12.1.4 一个完整工作流示例(从 main 开分支 → 提交 → 推送 → 开 PR) +假设你要做一个功能:新增“导出 CSV”。 + +##### 0)先确保本地在 main 且是最新 +```bash +git switch main +git pull +``` + +##### 1)从 main 拉出新分支(命名建议带 feature/ fix/ docs/) +```bash +git switch -c feature/csv-export +``` + +##### 2)开发过程中随时看状态 +```bash +git status +``` + +##### 3)把改动加入暂存区(建议逐个文件 add,更可控) +```bash +git add path/to/file.py +git add path/to/another_file.py +``` + +##### 4)提交(commit message 尽量表达“做了什么”) +```bash +git commit -m "Add CSV exporter" +``` + +##### 5)推送到远端(第一次推送建议带 -u 建立跟踪关系) +```bash +git push -u origin feature/csv-export +``` + +##### 6)在 GitHub 上开 PR +- base:`main` +- compare:`feature/csv-export` +- 填写说明:做了什么、怎么验证、是否影响兼容性 +- 通过 review 后合并(merge/squash/rebase 取决于团队策略) + +--- + +#### 12.1.5 pull / push 到底是什么? +- `git pull`:从远端把最新提交拿下来并合并到当前分支 + 等价于:`git fetch` + `git merge`(或配置成 `rebase`) +- `git push`:把你本地分支新提交推到远端 + +常见检查远端地址: +```bash +git remote -v +``` + +--- + +#### 12.1.6 PR(Pull Request)在解决什么问题? +- **Review**:让别人检查你的改动(逻辑、风格、风险) +- **讨论与记录**:PR 里可以讨论方案、保留决策记录 +- **自动化检查**:CI(测试、lint、格式化)常挂在 PR 上,自动拦不合格代码 +- **可追溯**:未来出问题能追到“谁改的/为什么改/讨论过程” + +--- + +#### 12.1.7 易错点(踩过的坑) +1)在 `main` 上直接开发并提交 +- 风险大:难做 review、难回滚、难并行多个功能 +- 推荐:任何改动(哪怕是小改 docs)也从分支开始 + +2)`git add .` 把不该提交的文件加进去了(尤其是 `.venv/`, `__pycache__/`, `.idea/`) +- 推荐先 `git status` 看清楚,再 add +- 配好 `.gitignore` + +3)提交信息太随意:`update`, `fix`, `aaa` +- 推荐写法:动词开头 + 说明范围 + 例:`Fix config loading when file missing` / `Add user DTO parsing` + +4)冲突(conflict)不敢处理 +- 冲突本质:同一段代码两边都改了,Git 不知道选谁 +- 处理原则:理解业务后“保留正确结果”,再跑测试验证 + +--- + +#### 12.1.8 冲突处理(最小流程) +```bash +git pull # 产生冲突时 Git 会提示哪些文件冲突 +git status # 查看冲突文件 +# 手动打开冲突文件,解决 <<<<<<>>>>>> 标记 +git add # 标记“已解决” +git commit # 完成这次合并提交(merge commit) +``` + +--- + +#### 12.1.9 VS Code 里怎么用(很实用) +- 左侧 Source Control 面板(版本控制图标) + - 可以看到改动文件、逐行 diff + - 勾选要暂存的文件(stage) + - 填写提交信息并 commit +- 解决冲突时 VS Code 会提供:Accept Current / Incoming / Both / Compare + +--- + +#### 12.1.10 什么时候用分支?(应用场景) +- 任何需要改代码的事情:新功能、修 bug、重构、写文档、调整配置 +- 需要并行处理多个任务时:每个任务一个分支互不干扰 +- 需要回滚/灰度:保留独立分支便于回退 + +--- + +#### 12.1.11 具体例子 +你这个场景本质是:**两台电脑都改了同一个仓库**,A 电脑先 `push` 到远端;B 电脑本地也有改动,但还没同步远端,于是 B 执行 `git pull` 时出现冲突/报错。 + +“pull 报错”常见分 3 类,我按**你看到的报错类型**给对应处理法(你只要对照你的提示信息走即可)。 + +--- + +**0)先做一件事:确认你 B 电脑本地有没有未提交改动** +在 B 电脑的项目目录里: + +```bash +git status +``` + +你会看到两类关键状态: +- Working tree 有改动(未 commit) +- 或者没有改动,但分支落后远端 + +下面按不同情况处理。 + +--- + +**1)情况 A:pull 提示 “本地有改动会被覆盖(please commit or stash)”** +典型报错类似: +- `Your local changes to the following files would be overwritten by merge` +- `Please commit your changes or stash them before you merge` + +**处理办法(推荐:stash,先把本地改动临时收起来再拉)** +```bash +git stash push -m "wip on laptop before pull" +git pull +git stash pop +``` + +- `stash`:把你当前未提交的改动先存到一个“临时栈”里(工作区恢复干净) +- `pull`:此时能顺利合并远端 +- `stash pop`:再把你刚才的改动“放回来” +- 如果 `stash pop` 产生冲突,就按第 3 节“解决冲突”处理。 + +> 如果你确定本地改动不需要了(谨慎),也可以直接丢弃: +```bash +git reset --hard +git pull +``` + +--- + +**2)情况 B:pull 提示 “有未完成的 merge(MERGE_HEAD exists)”** +典型报错类似: +- `You have not concluded your merge (MERGE_HEAD exists).` +- `Please, commit your changes before merging.` +- 或 status 显示 `Unmerged paths` + +说明:你之前 pull/merge 到一半冲突了,但还没解决。 + +**处理办法** +1)看看哪些文件冲突: +```bash +git status +``` + +2)打开冲突文件,解决这些标记(手工保留正确内容): +``` +<<<<<<< HEAD +你的本地版本 +======= +远端版本 +>>>>>>> origin/main +``` + +3)标记为已解决并完成合并提交: +```bash +git add -A +git commit +``` + +然后再: +```bash +git pull +``` + +> 如果你想放弃这次合并(直接回到合并前状态): +```bash +git merge --abort +``` + +--- + +**3)情况 C:pull 成功但产生冲突(conflict),怎么解?** +这是最常见的“同时改了一段代码”。 + +**冲突处理最小流程** +```bash +git status # 找出冲突文件 +# 打开冲突文件,手动编辑,删掉冲突标记,保留最终正确内容 +git add <冲突文件1> <冲突文件2> +git commit +``` + +在 VS Code 里体验更好: +- 冲突文件打开后会出现按钮:`Accept Current / Incoming / Both / Compare` +- 处理完后保存,再 `git add` + `git commit` + +--- + +**4)更推荐的日常习惯(减少这种报错)** +在 B 电脑开始写之前,先: +```bash +git pull +``` + +如果你已经改了一些但还没提交,想先同步远端再继续: +```bash +git stash +git pull +git stash pop +``` + +--- + +##### 12.1.12 冲突处理(最小流程) + +``` +<<<<<<< HEAD +你的本地版本 +======= +远端版本 +>>>>>>> origin/main +``` +你看到的这些标记,本质是 Git 在告诉你:“同一段代码两边都改了,我不知道选哪边”,所以把**两份内容都塞进文件里**,让你人工决定。 + +下面给你一个“照着做就行”的操作流程(VS Code 最推荐),再给命令行方式。 + +--- + +**1)先明确:HEAD / origin/main 分别是谁?** +冲突块长这样: + +```text +<<<<<<< HEAD +你的本地版本 +======= +远端版本 +>>>>>>> origin/main +``` + +含义: +- `<<<<<<< HEAD` 到 `=======`:**当前分支/当前工作区这一侧**(通常是你本地的改动) +- `=======` 到 `>>>>>>> origin/main`:**另一侧**(通常是你拉下来的远端分支那边的改动) + +最终目标:你要把文件改成“冲突解决后的最终正确内容”,并且**删掉这 3 行标记**: +- `<<<<<<< ...` +- `=======` +- `>>>>>>> ...` + +--- + +**2)VS Code 里怎么解决(最简单、最直观)** + +**步骤 0:先看冲突文件有哪些** +在仓库目录执行: +```bash +git status +``` +你会看到类似: +- `both modified: 笔记整理.md` +- 或多个文件 + +**步骤 1:在 VS Code 打开冲突文件** +- 直接在 Source Control 面板(左侧分支图标)里点击冲突文件 +- 或在文件树里打开 + +打开后 VS Code 通常会给你一个“冲突解决 UI”,在冲突片段上方会出现按钮,例如: +- **Accept Current Change**(接受当前改动 = HEAD 那一段) +- **Accept Incoming Change**(接受传入改动 = origin/main 那一段) +- **Accept Both Changes**(两段都保留,按顺序拼在一起) +- **Compare Changes**(并排对比) + +**步骤 2:选一种方式(最常见 3 种)** +**A. 只要本地版本(保留 HEAD)** +点:**Accept Current Change** +效果:只留下 `<<<<<<< HEAD` 上面那块内容,自动删掉冲突标记和另一块。 + +**B. 只要远端版本(保留 origin/main)** +点:**Accept Incoming Change** +效果:只留下远端那块内容。 + +**C. 两边都要(手工合并)** +点:**Accept Both Changes**,然后你再人工调整顺序/去重/改成你想要的最终文本。 + +> 笔记类文件(Markdown)很多时候用 C:两边内容都保留,再整理一下结构最合适。 + +**步骤 3:保存文件** +`Ctrl+S` 保存。 + +**步骤 4:告诉 Git “我解决好了”** +```bash +git add 笔记整理.md +``` + +> 多个文件就 `git add -A`(代表把所有已解决的冲突文件都标记为 resolved)。 + +**步骤 5:完成合并提交** +```bash +git commit +``` +它会生成一个 merge commit(一般默认信息就行)。 + +--- + +**3)如果 VS Code 没弹按钮:纯手工怎么改?** +你就按规则改: + +原文件是: +```text +<<<<<<< HEAD +A版本 +======= +B版本 +>>>>>>> origin/main +``` + +你要做的是:决定最终内容,比如你要 A+B 合并成: + +```text +A版本 +B版本 +``` + +然后**把三条标记行删掉**。保存即可。 + +--- + +**4)做完后怎么确认冲突解决干净了?** +```bash +git status +``` + +理想状态: +- 不再看到 `unmerged paths` +- 文件不再显示 `both modified` + +也可以搜一下冲突标记是否还残留: +```bash +git grep "<<<<<<<" +git grep ">>>>>>>" +git grep "=======" +``` + +--- + +**5)两句关键提醒(很重要)** +1) **解决冲突不是“选一个按钮就完了”** +你必须确认代码/文档逻辑是否正确(尤其是代码文件要跑一下测试/运行一下)。 + +1) **不要把冲突标记提交上去** +如果你提交时文件里还留着 `<<<<<<<`,以后会非常难维护。 + +--- + + +### 12.2 测试(unittest / pytest(推荐)) + +#### 12.2.1 一句话总结(核心) +- 测试(test)就是用代码验证代码:**给定输入 → 运行 → 断言输出/行为符合预期**。 +- 主要解决的问题: + - 改代码时不怕“改坏别处”(回归保障) + - 把需求固定成可执行规则(比口头/注释更可靠) + - 帮你重构:有测试撑腰才敢动 +- Python 常用两套: + - `unittest`:标准库自带(面向类、风格偏 Java) + - `pytest`:第三方(更简洁、fixture 强大、生态更常用,推荐) + +--- + +#### 12.2.2 unittest(标准库) + +##### 12.2.2.1 最小结构(能跑就行) +约定: +- 测试文件名:`test_*.py` +- 测试类继承:`unittest.TestCase` +- 测试方法以:`test_` 开头 + +```python +import unittest +# 导入标准库 `unittest`(Python 自带的单元测试框架),里面提供 `TestCase`、各种断言方法、测试运行器等 + +# 被测试对象 add函数 +def add(a: int, b: int) -> int: + return a + b + +# 定义一个测试类TestAdd,继承自unittest.TestCase,表示“这是一个测试用例集合” +# `unittest` 会自动发现继承自 `TestCase` 的类,并执行其中以 `test_` 开头的方法 +class TestAdd(unittest.TestCase): + + # 定义一个测试方法,名字以test_开头。 + # `unittest` 约定:**只有 `test_` 开头的方法才会被当作测试来运行** + # `self` 是 `TestCase` 实例(测试运行时 unittest 会创建它) + def test_add_ok(self): + + # self.assertEqual(x,y):断言 x == y ,不相等就报告失败,并显示出期望值/实际值。 + self.assertEqual(add(1, 2), 3) + + def test_add_negative(self): + self.assertEqual(add(-1, 2), 1) + +if __name__ == "__main__": # 入口保护 + unittest.main() + # 启动unittest的测试运行器(test runner),会执行如下步骤: + # 1) 根据规则在当前模块里收集测试(`TestCase` 子类 + `test_` 方法) + # 2)逐个运行测试方法 + # 3) 输出测试结果(通过/失败/错误统计) + # 4)进程退出码:全通过通常是 0;有失败/错误通常是非 0(CI 会用这个判断是否通过) + +``` + +以上代码输出以下内容: +```python +.. # 每个 . 代表一个测试用例(一个test_...方法)通过了 +---------------------------------------------------------------------- # 分割线,把每个测试点的输出和最终统计分开 +Ran 2 tests in 0.000s + +OK # 所有测试全通过 +[Finished in 224ms] +``` + +**如果失败,通常会显示F:** +```python +F. +====================================================================== +FAIL: test_add_negative (__main__.TestAdd.test_add_negative) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "D:\python_work\test20251218.py", line 21, in test_add_negative + self.assertEqual(add(-1, 2), 4) + ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^ +AssertionError: 1 != 4 + +---------------------------------------------------------------------- +Ran 2 tests in 0.001s + +FAILED (failures=1) +[Finished in 233ms with exit code 1] +[cmd: ['py', '-u', 'D:\\python_work\\test20251218.py']] +[dir: D:\python_work] +[path: C:\Program Files (x86)\Common Files\Oracle\Java\javapath;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\System32\OpenSSH\;C:\Program Files\CMITControlCenter\;C:\Program Files\TortoiseSVN\bin;C:\Program Files\dotnet\;D:\MiKTeX\miktex\bin\x64\;C:\Program Files\Git\cmd;C:\Users\US\AppData\Local\Programs\Python\Python37\Scripts\;C:\Users\US\AppData\Local\Programs\Python\Python37\;C:\Users\US\AppData\Local\Microsoft\WindowsApps;D:\VSCODE\Microsoft VS Code\bin;D:\intj\IntelliJ IDEA Community Edition 2025.1.3\bin;;D:\PyCharm\PyCharm 2025.1.3.1\bin;] +``` + +##### 12.2.2.2 常用断言(记住最常用的几个) +- `self.assertEqual(a, b)`:相等 +- `self.assertTrue(expr)` / `self.assertFalse(expr)`:布尔判断 +- `self.assertIn(x, container)`:包含 +- `self.assertRaises(SomeError)`:断言会抛出异常 + +示例:断言异常 +```python +import unittest + +def div(a: int, b: int) -> float: + if b == 0: + raise ValueError("b cannot be 0") + return a / b + +class TestDiv(unittest.TestCase): + def test_div_zero(self): + with self.assertRaises(ValueError): + div(10, 0) +``` + +##### 12.2.2.3 怎么运行(Windows) +在项目目录: +```bash +python -m unittest -v +``` + +--- + +#### 12.2.3 pytest(推荐) + +##### 12.2.3.1 为什么推荐 pytest? +- 用 `assert` 直接断言(更 Pythonic) +- 写测试更短、更清晰 +- fixture(夹具)机制非常强:测试数据准备/清理更优雅 +- 报错输出更友好(失败时会把左/右值对比展开) + +安装: +```bash +python -m pip install -U pytest +``` + +##### 12.2.3.2 最小示例(最常见写法) +文件:`test_calc.py` +```python +def add(a: int, b: int) -> int: + return a + b + +def test_add_ok(): + assert add(1, 2) == 3 +``` + +运行: +```bash +pytest -q +# 或更详细: +pytest -vv +``` + +##### 12.2.3.3 断言异常(pytest.raises) +```python +import pytest + +def div(a: int, b: int) -> float: + if b == 0: + raise ValueError("b cannot be 0") + return a / b + +def test_div_zero(): + with pytest.raises(ValueError, match="cannot be 0"): + div(10, 0) +``` + +##### 12.2.3.4 参数化(减少重复测试,非常常用) +```python +import pytest + +def add(a: int, b: int) -> int: + return a + b + +@pytest.mark.parametrize( + ("a", "b", "expected"), + [ + (1, 2, 3), + (0, 0, 0), + (-1, 2, 1), + ], +) +def test_add_cases(a, b, expected): + assert add(a, b) == expected +``` + +##### 12.2.3.5 fixture(了解但很关键:准备环境/测试数据) +```python +import pytest + +@pytest.fixture +def user(): + return {"name": "Tom", "age": 18} + +def test_user_name(user): + assert user["name"] == "Tom" +``` + +--- + +#### 12.2.4 pytest vs unittest 怎么选? +- 学习/个人项目/大多数工程:**pytest(推荐)** +- 不想装第三方、或公司历史包袱:unittest 也完全能用 +- 实战里常见:pytest + 插件生态(coverage、mock、xdist 并行等) + +--- + +#### 12.2.5 VS Code 里怎么跑测试(很实用) +1)装扩展:Python(微软官方) +2)打开命令面板:`Ctrl + Shift + P` +3)选择: +- `Python: Configure Tests` +- 选择 `pytest`(推荐)或 `unittest` +4)之后在 Testing 面板里可以: +- 发现/运行单个测试 +- 看到失败堆栈与断言对比 + +--- + +#### 12.2.6 易错点(踩过的坑) +1)测试文件/函数命名不符合规则 → 发现不到测试 +- pytest 默认找:`test_*.py`、`*_test.py`、函数名 `test_*` + +2)断言写 `print` 不写 `assert` +- print 只能“看”,不能自动判断对错;测试要用断言 + +3)测试相互污染(共享全局状态/写同一个文件) +- 用 fixture/临时目录隔离 +- 测试尽量“可重复运行、顺序无关” + +4)把“外部依赖”也当真测(网络/真实 DB) +- 单元测试应尽量快、稳定:外部依赖应 mock 或用测试替身 +- 集成测试再测真实依赖(分层) + +--- + +#### 12.2.7 应用场景(什么时候写测试) +- 你要重构一段逻辑:先补测试再大胆改 +- 修 bug:先写一个“能复现 bug 的测试”,修复后测试应通过(防止回归) +- 对外接口(工具函数/库函数/核心业务规则):必须有测试 +- 数据处理/解析(JSON/csv/日志解析):用参数化测试覆盖边界条件 + +--- + + +### 12.3 文档与规范(README、注释、docstring) + +#### 12.3.1 一句话总结(核心) +- 文档与规范的目标不是“写得花”,而是让别人(包括未来的你)能快速回答: + - 这项目/这段代码**干嘛的** + - **怎么安装/怎么运行/怎么测试** + - 输入输出是什么、有哪些边界条件、失败会怎样 +- 经验:文档不是一次性任务,**跟代码一起演进**;过期文档比没文档更糟。 + +--- + +#### 12.3.2 README(项目入口文档) + +##### 12.3.2.1 README 应该写什么?(最常用清单) +建议按这个顺序写(读者最容易上手): +1) 项目简介(是什么 + 解决什么问题) +2) 环境要求(Python 版本、系统、依赖) +3) 安装(pip/requirements/uv/poetry 任选其一) +4) 运行方式(最常用命令) +5) 用法示例(输入/输出示例,复制即可跑) +6) 测试(怎么跑 pytest/unittest) +7) 项目结构(目录说明) +8) 配置(环境变量/配置文件) +9) 常见问题(FAQ) +10) License(可选) + +##### 12.3.2.2 README 最小模板(可直接用) +**Project Name** + +**简介** +一句话说明:这个项目做什么。 + +**环境要求** +- Python: 3.14+ +- OS: Windows/macOS/Linux +- 依赖:见 requirements.txt / pyproject.toml + +**安装** +```bash +python -m venv .venv +# Windows PowerShell +.\.venv\Scripts\Activate.ps1 + +python -m pip install -U pip +python -m pip install -r requirements.txt +``` + +**运行** +```bash +python -m your_package.main --help +# 或 +python main.py +``` + +**示例** +```bash +python -m your_package.main --input data/in.txt --output data/out.txt +``` + +**测试** +```bash +pytest -q +``` + +**项目结构** +- your_package/:核心代码 +- tests/:测试 +- docs/:文档(可选) + + +##### 12.3.2.3 易错点 +- README 写了但**跑不起来**:命令过期/依赖缺失/路径不对 +- 缺少“最小可运行示例”:读者不知道怎么验证安装成功 +- 忘了写 Python 版本要求,导致别人用错解释器(尤其你笔记里涉及新语法) + +##### 12.3.2.4 应用场景(什么时候写) +- 任何需要别人运行/复现的项目(哪怕是你自己下周再看) +- 交作业/作品集/开源项目:README 就是“门面”+“使用说明” + +--- + +#### 12.3.3 注释(comments):解释“为什么”,不是重复“是什么” + +##### 12.3.3.1 一句话总结(核心) +- 注释的价值在于解释 **why(为什么这么做)**、约束条件、边界情况、坑点。 +- 代码本身通常已经能说明 what(做了什么),不要写“翻译式注释”。 + +##### 12.3.3.2 示例:好的注释 vs 不好的注释 + +**不好的注释(重复代码)** +```python +i = i + 1 # i加1 +``` + +**更好的注释(解释原因/约束)** +```python +# 这里用 while 而不用 for:因为需要在遇到非法输入时重试,次数不固定 +while True: + ... +``` + +**解释 tricky 行为(例如性能/兼容/边界)** +```python +# 使用 dict.get 而不是 d[key]:缺失时返回默认值,避免 KeyError 影响主流程 +value = d.get("key", 0) +``` + +##### 12.3.3.3 注释风格建议(够用就行) +- 行内注释:`# ...`(与代码间隔两个空格) +- 块注释:多行 `# ...`(解释一个代码块的意图) +- TODO 注释:标记待办(建议带原因/负责人/日期更工程化) +```python +# TODO: support CSV export (needs quoting rules for commas) +``` + +##### 12.3.3.4 易错点 +- 注释与代码不一致(最坑):改了代码忘了改注释 +- 注释写实现细节(容易过期),建议写“意图/约束/为什么” + +##### 12.3.3.5 应用场景 +- 业务规则有“反直觉条件” +- 性能/安全/兼容性考虑导致写法特殊 +- 临时绕过某个外部系统 bug(必须写清楚原因与链接) + +--- + +#### 12.3.4 docstring(文档字符串):写给调用者 & 工具看的接口说明 + +##### 12.3.4.1 一句话总结(核心) +- docstring 是写在 **模块/函数/类/方法** 定义处的第一段字符串,用于描述用途、参数、返回值、异常等。 +- IDE、`help()`、自动文档工具会读取 docstring,因此它是“对外 API 的说明书”。 + +##### 12.3.4.2 示例:函数 docstring(推荐简洁版) +```python +def clamp(x: float, low: float, high: float) -> float: + """Clamp x to the inclusive range [low, high]. + + Args: + x: Value to clamp. + low: Lower bound. + high: Upper bound. + + Returns: + The clamped value. + + Raises: + ValueError: If low > high. + """ + if low > high: + raise ValueError("low must be <= high") + return max(low, min(high, x)) +``` + +要点: +- 第一行:一句话说明用途(动词开头更清晰) +- 复杂函数再写 Args/Returns/Raises +- 与类型注解互补:类型注解写“类型”,docstring 写“语义与规则” + +##### 12.3.4.3 示例:类 docstring(说明职责/不变量) +```python +class Account: + """Bank account domain model. + + Invariants: + - balance is always >= 0 + """ + ... +``` + +##### 12.3.4.4 怎么查看 docstring(理解它确实被工具使用) +```python +print(clamp.__doc__) +help(clamp) +``` + +##### 12.3.4.5 易错点 +- docstring 写太长导致没人看:优先写“最常用信息” +- 只写“参数是什么”但不写“规则”:例如边界条件、单位、是否允许 None、是否会抛异常 +- 过期:函数行为变了但 docstring 没更新 + +##### 12.3.4.6 应用场景 +- 提供给别人调用的函数/类(公共 API) +- 边界复杂:例如解析/校验/格式转换 +- 你希望 IDE 提示里直接显示使用方法 + +--- + +##### 12.3.5 一个实用结论(写给自己看的) +- README:告诉别人“项目怎么用”(项目入口) +- 注释:解释“为什么这样写”(意图/坑点) +- docstring:告诉别人“这个函数/类怎么用”(接口契约) + + +### 12.4 代码质量(ruff / black / isort) + +#### 12.4.1 一句话总结(核心) +- **代码质量工具**解决两件事: + 1) **统一风格**(格式化:别人看得懂、减少无意义 diff) + 2) **提前发现问题**(静态检查:未使用导入、潜在 bug、可读性问题) +- 三个常见工具: + - **black**:只负责“格式化代码”(formatter),目标是统一风格、减少争论 + - **isort**:只负责“整理 import 顺序/分组”(import formatter) + - **ruff**:速度很快的 linter +(可选)formatter,能替代大量 flake8 规则,并内置许多自动修复 + +> 实战建议(简单、够用):**ruff(lint + fix) + black(format)** +> 如果你用 ruff 的 `lint.isort` 规则,也可以不单独装 isort。 + +--- + +#### 12.4.2 ruff(推荐:lint + 自动修复) + +##### 12.4.2.1 ruff 能抓什么? +典型能发现: +- 未使用导入 / 未使用变量 +- import 顺序问题 +- `== None` 等不规范写法 +- 某些明显 bug(例如可疑的比较、异常处理不当) +- 复杂度/可读性规则(可选) + +##### 12.4.2.2 安装 +```bash +python -m pip install -U ruff +``` + +##### 12.4.2.3 最常用命令 +```bash +ruff check . # 扫描当前项目(只报问题,不改) +ruff check . --fix # 自动修复能修的(例如删掉未使用导入) +ruff check . --fix --unsafe-fixes # 更激进的修复(谨慎) +``` + +只检查某个文件: +```bash +ruff check path/to/file.py +``` + +--- + +#### 12.4.3 black(格式化:统一代码风格) + +##### 12.4.3.1 一句话总结 +- black 是“强制统一风格”的格式化工具:你不用纠结空格/换行/引号风格,交给它就行。 +- 它不会试图“改你的逻辑”,只改排版(可读性/一致性)。 + +##### 12.4.3.2 安装 +```bash +python -m pip install -U black +``` + +##### 12.4.3.3 最常用命令 +```bash +black . # 格式化整个项目 +black path/to/file.py +``` + +只检查是否需要格式化(CI 常用): +```bash +black . --check +``` + +--- + +#### 12.4.4 isort(按需:专门整理 import) + +##### 12.4.4.1 一句话总结 +- isort 把 import 自动整理成“标准库 / 第三方 / 本地”分组,并按字母排序,保持一致性。 +- 如果你已经用 ruff 的 isort 规则(见下方配置),可以不单独装 isort。 + +##### 12.4.4.2 安装 +```bash +python -m pip install -U isort +``` + +##### 12.4.4.3 常用命令 +```bash +isort . +isort path/to/file.py +``` + +--- + +#### 12.4.5 推荐配置:pyproject.toml(一个文件统一管理) +把配置放到项目根目录的 `pyproject.toml`,工具会自动读取。 + +```toml +[tool.black] +line-length = 88 +target-version = ["py314"] + +[tool.ruff] +line-length = 88 + +[tool.ruff.lint] +# 常用且性价比高的一组规则:E/F=基础错误;I=import排序;UP=现代写法;B=常见bug;SIM=简化;RUF=ruff自带 +select = ["E", "F", "I", "UP", "B", "SIM", "RUF"] +# 如果你想先少一点规则,避免一下子报太多,可先用:["E", "F", "I"] + +# 常见忽略:E501 是行长度(通常交给 black 管) +ignore = ["E501"] + +[tool.ruff.lint.isort] +known-first-party = ["your_package_name"] # 你的项目包名(有的话就填) +``` + +--- + +#### 12.4.6 VS Code 里怎么用(落地) +1) 装扩展:Python(微软官方) + Pylance +2) 建议在 VS Code 设置里: +- Format On Save:保存自动格式化 +- 默认格式化器选择 black(或 ruff formatter) + +常用做法: +- 保存时自动 `black` +- 提交前手动跑: + - `ruff check . --fix` + - `black .` + +--- + +#### 12.4.7 易错点(踩过的坑) +1) **同时启用 black 和 ruff formatter**:可能重复格式化、互相打架 +- 建议只选一个当“格式化器”(大多数人选 black) +- ruff 先主要负责 lint + fix + +2) **规则一次开太多**:项目会瞬间报一堆问题 +- 建议先 `select = ["E", "F", "I"]` 起步,稳定后再加 `UP/B/SIM` + +3) **只在本地跑,不在提交/CI 里跑**:团队协作时会失控 +- 最少做到:提交前跑一次(或者后面你可以补 pre-commit) + +--- + +#### 12.4.8 应用场景(什么时候用) +- 个人项目:减少风格纠结、减少低级错误 +- 团队协作:统一风格,PR diff 更干净 +- 重构:ruff 能提前报出“你把 import/变量弄坏了” +- CI:`ruff check .` + `black --check .` 作为质量门禁 + +--- + +### 12.5 打包与发布(了解:pyproject.toml、依赖与版本号语义) + +#### 12.5.1 一句话总结(核心) +- **打包(packaging)**:把你的代码做成“可安装的包”(别人可以 `pip install ...` 来用),并把依赖/版本/入口命令统一声明。 +- **pyproject.toml**:现代 Python 项目的“项目元数据中心”(项目名、版本、依赖、构建方式、工具配置等)。 +- **版本号语义(SemVer)**:用 `MAJOR.MINOR.PATCH` 表达兼容性变化,方便协作与升级。 + +--- + +#### 12.5.2 你需要先分清 3 件事(最容易混) +1) **项目(project)**:一个仓库/一个工程(包含源码、测试、配置等) +2) **包(package/import 包)**:能被 `import xxx` 的那坨代码(通常是一个目录 `xxx/`) +3) **分发包(distribution)**:你上传到 PyPI 或本地安装的“安装包”,`pip install xxx` 安装的是它 +> distribution 名称不一定等于 import 包名(但建议保持一致,少踩坑) + +--- + +#### 12.5.3 pyproject.toml 是什么?放哪里? +- 放在项目根目录(和 `README.md` 同级) +- 它可以包含两大类内容: + 1) **打包元数据**:项目名/版本/依赖/作者/许可证/入口命令等 + 2) **工具配置**:ruff/black/pytest 等配置(你前面已经在用) + +--- + +#### 12.5.4 一个“最小可用”的 pyproject.toml(示例) +下面示例以最常见的构建后端 **setuptools** 为例(够用、易理解)。 + +```toml +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "demo_pkg" +version = "0.1.0" +description = "A small demo package" +readme = "README.md" +requires-python = ">=3.14" +dependencies = [ + "requests>=2.31", +] + +# 可选:提供命令行入口(安装后可直接在终端运行 demo) +[project.scripts] +demo = "demo_pkg.cli:main" +``` + +你还需要把源码组织成可 import 的包,比如: +```text +demo_pkg/ + __init__.py + cli.py +pyproject.toml +README.md +``` + +其中 `demo_pkg/cli.py` 里: +```python +def main() -> None: + print("hello from demo") +``` + +安装后就能在命令行运行: +```bash +demo +``` + +--- + +#### 12.5.5 依赖怎么写?(你只需要掌握两层) +在 `pyproject.toml` 的: +- `dependencies = [...]`:运行依赖(别人安装你的包就必须有) +- `optional-dependencies`:可选依赖(按功能分组,如 dev/test) + +示例(推荐做法:把测试工具放 dev 组): +```toml +[project] +dependencies = ["requests>=2.31"] + +[project.optional-dependencies] +dev = [ + "pytest>=8", + "ruff>=0.6", + "black>=24.0", +] +``` + +安装“开发依赖”: +```bash +python -m pip install -e ".[dev]" +``` +- `-e` 表示可编辑安装(你改源码立刻生效,适合开发) + +--- + +#### 12.5.6 版本号语义(SemVer:建议记住的规则) +形式:`MAJOR.MINOR.PATCH` + +- **PATCH(补丁)**:只修 bug、不改接口 + - 例:`1.2.3 -> 1.2.4` +- **MINOR(小版本)**:加功能,但保持向后兼容 + - 例:`1.2.3 -> 1.3.0` +- **MAJOR(大版本)**:不兼容变更(breaking changes) + - 例:`1.2.3 -> 2.0.0` + +> 你不做开源也建议按这个思路写版本:它能逼你“每次发布到底破坏兼容了吗”。 + +--- + +#### 12.5.7 依赖版本约束的常见写法(理解即可) +- `requests>=2.31`:至少 2.31(允许升级到更高版本) +- `requests==2.31.0`:锁死版本(非必要不推荐,除非你在做可复现环境) +- `requests>=2.31,<3`:既允许补丁/小版本升级,又避免未来大版本破坏兼容(很常见、推荐) + +--- + +#### 12.5.8 怎么“构建/打包/本地安装”(了解即可) +常见步骤(本地验证自己的包能装): + +1) 安装构建工具: +```bash +python -m pip install -U build +``` + +2) 在项目根目录构建: +```bash +python -m build +``` + +会生成: +- `dist/*.whl`(wheel,推荐的二进制分发形式) +- `dist/*.tar.gz`(源码包) + +3) 本地安装 wheel 验证: +```bash +python -m pip install dist\demo_pkg-0.1.0-py3-none-any.whl +``` + +--- + +#### 12.5.9 易错点(踩过的坑) +1) 把“项目名”和“包名(import 名)”弄混 +- `name = "demo-pkg"`(distribution 名) +- `import demo_pkg`(包名) +- 建议:两者统一成 `demo_pkg`,少出事 + +2) 忘记写 `requires-python` +- 别人可能用低版本 Python 安装,然后运行时报奇怪语法错误 + +3) 运行依赖 vs 开发依赖不分 +- pytest/ruff/black 应放到 `optional-dependencies.dev`,不要塞进运行依赖 + +4) 只写了 pyproject,但源码目录结构不对 +- 没有 `your_pkg/__init__.py` 时,很多情况下就不是“可导入的包” + +--- + +#### 12.5.10 应用场景(什么时候用) +- 你要把工具代码复用到多个项目:做成包然后 `pip install -e .` +- 团队协作:统一依赖声明、统一版本、统一质量工具配置 +- 做 CLI 工具:通过 `[project.scripts]` 提供命令行入口 +- 想发到 PyPI(开源/内部源):需要完整打包元数据 + 构建流程 + + + +--- + +## 附录:速查 / 踩坑 / 片段库 +- 常用代码片段(文件读写、路径处理、JSON 解析等) +- 常见坑清单 + - 可变默认参数、浅拷贝/深拷贝、编码、浮点误差、作用域(global/nonlocal) +- 小练习与小项目索引(按章节对应) \ No newline at end of file