0%

Python Basic Notes

PYTHON 基础

1. python 流程格式

1.1 变量赋值、运算与字符串表示、处理、判断

1
2
3
4
5
6
7
8
9
10
name1 = name2 = name3 = '文件系统'
name1, name2, name3 = "文件", "系统", "管理"
name = """
ni
hao
a
"""


##变量多行用 """....."""

变量运算表
| 运算 | 描述 |
| :——-: | :—————————————: |
| + - / | 加减乘除 (+可用于连接string) |
| % | 取模(102%100=2) |
| * | 幂次方(2
3=8) |
| // | 取除数(20//3=6) |

字符串输出表

方式 意思
:, 每 3 个 0 就用逗号隔开,比如 1,000
:b 该数字的二进制
:d 整数型
:f 小数模式
:% 百分比模式

1 字符串表示 ( %百分号模式 与 format功能 与 f格式化字符串)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
name = "hm"
age = 24
print("我的名字是 %s !我 %d 岁了" % (name, age)) #百分号模式
print("我的名字是 {} !我 {} 岁了".format(name, age))#format功能
print(f"我的名字是 {name} !我 {age} 岁了") #f 格式化
print("我 {:.3f} 米高".format(1.12345))
print("我 {ht:.1f} 米高".format(ht=1.12345))
print("我 {:3d} 米高".format(1))
print("我 {:3d} 米高".format(21))
print("You scored {:.2%}".format(2.1234))
score = 2.1234
print(f"You scored {score:.2%}")
print(f"You scored {12:5d}")
#我的名字是 hm !我 24 岁了
#我 1.123 米高
#我 1.1 米高
#我 1 米高
#我 21 米高
#You scored 212.34%
#You scored 212.34%
#You scored 12

2 字符串处理与判断

string.strip() 去除两端的空白符
string.replace(“原始”,”新的”) 替换字符
string.lower() 全部做小写处理
string.upper() 全部做大写处理
string.title() 仅开头的字母大写
string.split(“|”) 按要求分割以|进行分割
“,”.join([]) 按要求合并中间添加,号
string.startswith(“你”) 判断是否为你字段开头
string.endswith(“哈哈”) 判断是否为哈哈字段结尾
 

1.2 条件判断、循环、跳出跳过(if/for/while/break/continue)

1
2
3
4
5
6
7
8
9
10
11
12
## IF
# 冒号":"是必要格式不要遗失
if a==b :
print('ds')
elif(a>=b):
print('es')
else:
print('ys')

a = 1
b=2
c = b if a>=b else a #简单的if-else 可以写成一行的简写模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#  python 从0开始 区间取值均为前闭后开:[3,10) 基础
# for 循环处理有明确长度的序列,那么你第一个想到的应该是 。
# while 循环无限长序列或者运行无限次数,用条件来限制他的循环次数。
for i in range(3,10):
print("文件"+ str(i)) # int型与string型不能直接连接需转换int型为string型(文件1)
print("文件", str(i)) #(文件 1) 中间有空格

l = [i*2 for i in range(3,10)] # 简单的for 可以写成一行的简写模式
l = [i*2 for i in range(3,10) if i%2==0] # 简单的for+if 可以写成一行的简写模式
a=0
while a != 10:
a += 1
print(a)

# break:最主要的目的就是为了个性化的终止当前循环.
# continue: 判断一个数据如果不能被处理的话,我就把它跳过,接着处理下一个数据。
for i in range(10):
if i % 2 == 0:
continue # 打印所以奇数,跳过偶数
if i == 9:
break # 如果 i=9跳出循环。
print(i)

 

1.3 数据类型

1.3.1 List列表、Dict字典、Tuple元组、Set集合

list “[]”—— 具有顺序、可存放不同类型;但是

1
2
3
4
5
6
7
8
9
10
11
files = [1, "file", ["2", 3.2],"ha"]
print( files[2:4]) # [["2", 3.2],"ha"] 取[2,3)
print( files[-3:]) # ["file", ["2", 3.2],"ha"] 从后往前取三个
print( files[2][1]) # 3.2
for f in files:
if f == "f3.txt":
print("I got f3.txt")
(OR)
for i in range(len(files)):
if files[i] == "f3.txt":
print("I got f3.txt")

Dict “{“key”:value}”—— 注意字典中的元素是没有顺序的; key-value型
1
2
files = {"ID": 111, "passport": "my passport", "books": [1,2,3]}
print(files["books"]) # [1,2,3]

Tuple “()”—— 存放数据值不能被改变,常用来存放有些常数或固定值。特别是代码交由别人使用时,你没法控制,也不希望他们改变你的这些固定值。
1
files = ("file1", "file2", "file3")

Set “set([])”或”{}”—— Set常用于去重,set里面只会存在非重复的元素,不管你往里面加了多少相同元素,这些相同元素都会坍缩成一个。 这种特性,我们就可以运用它来做交集并集等操作。注意集合中的元素也没有顺序的。
1
2
3
4
5
6
7
my_files = set(["file1", "file2", "file3"])
your_files = {"file1", "file3", "file5"}
my_files.add("file4") #添加
my_files.remove("file3") #移除
print("交集 ", your_files.intersection(my_files))
print("并集 ", your_files.union(my_files))
print("补集 ", your_files.difference(my_files))

list 内元素的增加与删除
1
2
3
4
5
6
7
8
9
10
11
12
13
14
files = []
files.append("f"+str(1)+".txt") # 添加元素
print(files)
files.pop() # 从最后一个开始 pop 出 删除元素
print( files)
# 扩充入另一个列表
files.extend(["f3.txt", "f4.txt"])
# 按位置添加
files.insert(1, "file5.txt") # 添加入第1位(首位是0哦)
# 移除某索引
del files[1]
# 移除某值
files.remove("f3.txt") # 当存在多个f3.txt时,从0开始移除第一个f3.txt元素


 

1.3.2 arrly数组 —— Numpy库

 

1.3.3 arrly表 —— Panda库

 

1.4 一些函数的妙用

1.4.1enumerate 自动加 index

使用enumerate函数替换 手动计数器 count 的存在.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
count = 0
l = [11,22,33,44]
for data in l:
if count == 2:
data += 11
l[count] = data
count += 1
print(l)

l = [11,22,33,44]
for count, data in enumerate(l,start=0): #还可以初始化 count 开始的值
if count == 2:
data += 11
l[count] = data
print(l)
#>[11, 22, 44, 44]

1.4.2 Zip 同时迭代

同时处理多个列表,并把它们做成一个字典。

1
2
3
4
5
6
7
name = ["a", "b", "c"]
score = [1,2,3]
bonus = [1,0,1]
d = {}
for n, s, b in zip(name, score, bonus):
d[n]=s+b
print(d)

1.4.3 reverse & reversed 翻转列表

同时处理多个列表,并把它们做成一个字典。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#自己就地反转
l = [1,2,3]
l.reverse()
print(l)
#>[3, 2, 1]
(or)
#用在 for 循环里的翻转迭代器
for i in reversed(l):
print(i)
#>3 2 1
(or)
#copy 出一个浅拷贝副本的反转
_l = l[::-1]
print(_l)
#>[3, 2, 1]

1.4.4 Deep Copy & Shallow Copy 复制要点

Deep copy 就是我们通常意义上的复制,把东西全部再造了一遍,彻底成为了两个独立的个体。
Shallow Copy, 其实也有一点地址拷贝的意思,通过地址进行映射。 所以真实的实体是没有被复制的,我只复制了这个实体的一个映射地址而已。
注:Python 他在创造之初,就有这么个约定,列表中直接存放的数值、字符,与存放class 实例,列表,字典不同。 对数值、字符的复制,直接是复制的值,而不是一个映射地址。
程序数据的无端更改其实很有可能就是浅复制造成的,改变了不该被改变的值。但是浅复制的优势就是快。复制是需要内存和时间的,因为浅复制没有真正复制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#存放的数值、字符时相当于Deep Copy
l = [1,2,3]
_l = l.copy()
_l[0] = -1
print(_l)
print(l)
#> [-1, 2, 3]
#> [1, 2, 3]

#存放的列表时 .copy只是浅拷贝Shallow Copy
l = [[1],[2],3]
_l = l.copy()
_l[0][0] = -1
print(_l)
print(l)
#> [[-1], [2], 3]
#> [[-1], [2], 3]

#存放的列表时 如何深拷贝哪?
from copy import deepcopy
l = [[1],[2],3]
_l = deepcopy(l)
_l[0][0] = -1
print(_l)
print(l)
#> [[-1], [2], 3]
#> [[1], [2], 3]

2. python 框架格式

2.1 函数(Function) — 类 (Class) — 模块 (Module)

函数功能 :目的用于基础功能复用,简化程序流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
g=3
# 传入变量 其中a=1, b=1, c=1为参数默认值(可以不设置),
# 当未传入a,b,c参数时令其为默认。
def f(x, a=1, b=1, c=1):
global g
#默认时局部变量不影响函数外部,除非golbal声明,
#但是要注意全局变量不能同时函数传入变量
g=5
return a*x**2 + b*x + c*3 # 返回变量
print(f(2, 1, 1, 0)) # 忽略参数名,按顺序传参
print(f(x=2, a=1, b=1, c=0)) # 写上参数名,按名字传参
print(f(a=1, c=0, x=2, b=1)) # 若用参数名,可以打乱顺序传参
print(f(x, a=2)) # 等价于传入 x=2,a=2, b=1, c=1
print(g) # 5

:在程序中描述具体的一个物体。 比如一只猫有哪些特征,它能做哪些动作。 工程师想出了一个在代码中设置猫特征和动作的办法, 这就有了 class 类的概念。
  类的目的 : 基于梳理逻辑步骤形成类,用于统筹管理函数,为函数分类使之条理清晰。 e.g. 数据处理类(用于实现读写文件、数据提取、正则化、json序列化传输等函数功能)、求解问题类(问题参数、编解码目标函数计算、初始化种群、甘特图绘制等函数)、算法模块(naga-ii\moea/d\RL等类,每个类里包含诸如选择交叉变异的函数)、工具类(获取前沿面、轮盘赌等函数)

a) 类的封装: 封装自己的属性和功能,把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果存在不想被外界使用的方法,我们可私有化不提供方法给外界访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 用 class File 来创建一个大概念(类),注意我们通常约定类的名字要首字母大写。 
# 然后用 my_file = File() 来创建一个具体的文件。
# self 类内部的索引,用于获取该类的属性或者功能
class Fjsp_RMT():
def __init__(self, job_num, machine_num, p1, p2, parm_data): #初始化实例
self.job_num = job_num # 工件数
self.machine_num = machine_num # 机器数
self.p1 = p1 # 全局选择的概率
self._p2 = p2 # 局部选择的概率 (_开头我不想让别人用这个变量,但是还是可以用)
self.__deleted = False # __开头我不让别人用这个变量
self.product, self.machines, self.F_cog,self.F_time,
self.F_cost= parm_data[0], parm_data[1],parm_data[2], parm_data[3],parm_data[4]
def get_info(self):
# 类内部的功能返回时均带self
return "工件数为:"+ str(self.job_num) + " ,机器数为:" +str(self.machine_num)
def __force_delete(self): # 我不让别人使用这个功能
self.__deleted = True
return True
def _soft_delete(self): # 我不想让别人使用这个功能
self.__force_delete() # 我自己可以在内部随便调用
return True
fjsp_rmt=Fjsp_RMT(4, 3, 0.3, 0.4, [1,2,3,4,5])
print(fjsp_rmt.get_info())

| 私有形式 | 特点 |
| :———————-: | :————————————————————————————: |
| _ 一个下划线开头 | 弱隐藏 不想让别人用 (别人在必要情况下还是可以用的) |
| __ 两个下划线开头 | 强隐藏 不让别人用 |

b) 类的继承与多态:
继承:是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。集成特点如下:

  1. 父类变,子类就必须变
  2. 继承破坏了封装,对于父类而言,它的实现细节对与子类来说都是透明的。
  3. 继承是一种强耦合关系

多态:若子类重写了父类中的某些方法,在调用该些方法的时候,必定是使用子类中定义的这些方法(动态连接、动态调用)。实现多态有三个必要条件:继承、重写、调用父类的属性/函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Fjsp_RMT_singe(Fjsp_RMT):  # 继承了 Fjsp_RMT 的属性和功能
def __init__(self, job_num, machine_num, p1, p2, parm_data, add):
# 将共用属性的设置导入 File 父类
#继承父类super的init参数时属于外部无需self 加入self 会报错
super().__init__(job_num, machine_num, p1, p2, parm_data)
self.add = add
def get_more_info(self): # 也可以在子类里复用父类功能
return self.get_info() + ",singe:"+ str(self.add)
fjsp_rmt_singe=Fjsp_RMT_singe(4, 3, 0.3, 0.4, [1,2,3,4,5],(1080, 720))
print(fjsp_rmt_singe.get_info()) # 调用父类的功能
print(fjsp_rmt_singe.p1) # 调用父类的属性
print(fjsp_rmt_singe.add) # 调用自己的属性
print(fjsp_rmt_singe.get_more_info()) # 调用自己加工父类的功能

模块 :module主要是为了一个相对比较大的工程,涉及到多个文件之间的互相调用关系。
  算法模块(包括naga-ii\moea/d\RL等类,每个类里包含诸如参数等属性以及选择、交叉、变异的函数功能)。

统一规范:不涉及python包时均使用 import as A 形式;涉及到 python包时采用from A import 形式并与init.py配合(原因:一般通用类都用 import numpy as np的形式,而python包中基本都是自己创建的程序类有实际用途所以包是空壳子没必要带着)

1
2
3
4
5
6
 #**如下代码显示了模块调用、导入的两种形式**
# FJSP_RMT.py 和 main.py 在同一目录下 (main调用FJSP_RMT时)
直接 import 即可:
import FJSP_RMT as problem
或者
from b import *

上述两者的区别是:

  1. 如果用 import FJSP_RMT as problem,我们在调用FJSP_RMT.py中定义的函数fun1()或类class1()时,需要写成 problem.fun1()或problem.class1();
  2. 如果用 from FJSP_RMT import *,我们在调用FJSP_RMT.py中定义的函数fun1()或类class1()时,可以直接写成 fun1()或class1();
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # Algorithm模块下的NSGA2.py 和 main.py 在不同级的目录下 (main调用Algorithm时)
    from Algorithm import *
    # 在Algorithm模块下的__init__.py中输入该行代码 .NSGA2代表返回上一目录后找到NSGA2.py
    from .NSGA2 import NSGA2
    则我们在调用NSGA2.py中定义的函数fun1()或类class1()时,可以直接写成 fun1()或class1();

    import Algorithm.NSGA2 as NSGA2
    并把Algorithm模块下的__init__.py清空
    则我们在调用NSGA2.py中定义的函数fun1()或类class1()时,可以直接写成 NSGA2.fun1()或NSGA2.class1()
     

    2.2 异常处理(try-except\raise) 与 单元测试 (Unittest)

    异常处理(try-except)
      代码出错不可避免,即便自己编写代码做到无错误 ,如果你的代码基于他人的代码,别人代码出错。即使别人的代码不出错,但是也有可能物理环境会出错呀,比如 CPU 超了,某人的服务运行失败,你恰好又是基于他提供的服务,你也跟着超时了。这些都会让你的程序报错。异常处理就是帮助我们去处理错误,预估到可能会报什么错误然后用try-except 结构包住处理,达到不妨碍接下来的代码执行。
    1
    2
    3
    4
    5
    6
    7
    8
    #try-except 结构 同时处理多个异常 
    d = {"name": "f1", "age": 2}
    l = [1,2,3]
    try:
    v = d["gender"]
    l[3] = 4
    except (KeyError, IndexError) as e:
    print("key or index error for:", e)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #try-except 结构 分开处理多个异常
    d = {"name": "f1", "age": 2}
    l = [1,2,3]
    try:
    v = d["gender"]
    l[3] = 4
    except KeyError as e:
    print("key error for:", e)
    d["gender"] = "x"
    except IndexError as e:
    print("index error for:", e)
    l.append(4)
    print(d)
    print(l)
    还有一个 try-except-else 的模式,在 else 中处理没有报错的情况。报错情况下不会执行else下的代码,不报错会执行。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # try-except-else 结构

    l = [1,2,3]
    try:
    l[3] = 4
    except IndexError as e:
    print(e)
    else:
    print("no error, now in else")
    如果 else 是为了执行没有异常的状况,那么 finally 就是为了执行 不管有没有异常 的情况。无论有报错还是没报错,finally 下面的代码都会运行。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # try-except-finally 结构

    l = [1,2,3,4]
    try:
    l[3] = 4
    except IndexError as e:
    print(e)
    finally:
    print("reach finally")
    上面这两种模式主要用在什么 case 当中呢?你想一下,是不是有些时候,不管你有没有报错,你都想让程序去执行什么。我们甚至都不需要为任何异常做任何处理。 这种时候也就是说,你有异常,我不让你终止主程序,你没有异常吧,万事大吉。
    1
    2
    3
    4
    try:
    dddd = dddddd
    finally:
    print("I know there is error, so what?")
    raise手动触发异常
       raise 是你为别人犯错留下的证据,或者是告诉别人你怎么犯错的。这个信息对于别人 dubug 你的代码十分有好处。另一种情况是,你写了成百上千行代码,你也不能全记住代码的每一个细节。所以一旦报错,你也需要一个友善的错误信息提示,这时用 raise 准没错。
    1
    2
    3
    4
    5
    6
    7
    def no_negative(num):
    if num < 0:
    raise ValueError("I said no negative")
    return num
    print(no_negative(-1))
    # Traceback (most recent call last):
    # ValueError: I said no negative
异常名称 描述
BaseException 所有异常的基类
SystemExit 解释器请求退出
KeyboardInterrupt 用户中断执行(通常是输入^C)
Exception 常规错误的基类
StopIteration 迭代器没有更多的值
GeneratorExit 生成器(generator)发生异常来通知退出
StandardError 所有的内建标准异常的基类
ArithmeticError 所有数值计算错误的基类
FloatingPointError 浮点计算错误
OverflowError 数值运算超出最大限制
ZeroDivisionError 除(或取模)零 (所有数据类型)
AssertionError 断言语句失败
AttributeError 对象没有这个属性
EOFError 没有内建输入,到达EOF 标记
EnvironmentError 操作系统错误的基类
IOError 输入/输出操作失败
OSError 操作系统错误
WindowsError 系统调用失败
ImportError 导入模块/对象失败
LookupError 无效数据查询的基类
IndexError 序列中没有此索引(index)
KeyError 映射中没有这个键
MemoryError 内存溢出错误(对于Python 解释器不是致命的)
NameError 未声明/初始化对象 (没有属性)
UnboundLocalError 访问未初始化的本地变量
ReferenceError 弱引用(Weak reference)试图访问已经垃圾回收了的对象
RuntimeError 一般的运行时错误
NotImplementedError 尚未实现的方法
SyntaxError Python 语法错误
IndentationError 缩进错误
TabError Tab 和空格混用
SystemError 一般的解释器系统错误
TypeError 对类型无效的操作
ValueError 传入无效的参数
UnicodeError Unicode 相关的错误
UnicodeDecodeError Unicode 解码时的错误
UnicodeEncodeError Unicode 编码时错误
UnicodeTranslateError Unicode 转换时错误
Warning 警告的基类
DeprecationWarning 关于被弃用的特征的警告
FutureWarning 关于构造将来语义会有改变的警告
OverflowWarning 旧的关于自动提升为长整型(long)的警告
PendingDeprecationWarning 关于特性将会被废弃的警告
RuntimeWarning 可疑的运行时行为(runtime behavior)的警告
SyntaxWarning 可疑的语法的警告
UserWarning 用户代码生成的警告

单元测试 (Unittest)
代码调试是编写代码的至关重要的一环。尤其是你要写一些功能服务给别人用的时候,免不了要先自测有没有问题。保证我写的,真的是我想要的。
使用场景: 程序的代码量小,且项目功能之间无耦合关系时,可运行程序打断点进行debug。但是当处理的代码量比较大,牵扯到的资源相对多,我很多时候并不知道哪里会不会有错误。 所以这种情况非常依赖于测试。让自动化测试帮我处理任何改动可能带来的问题。

unittest 规范:首先 unittest 不会被其他人使用到,纯粹是你自己为了验证自己写的代码有没有问题的方式。一般般采用 XXXX.py与XXXX_test.py的形成进行单元测试。另外,你可以按照 unittest 当中的 case 为蓝本,去完善你原函数的功能。 就好像有了一个目标,你要为了这个目标去开发功能一样。这样就可以先写 unittest 当中的 case,比如下面,我不会先写 my_func 里面的内容,而是先把我的测试和要验收的指标写好。然后后面我再开发功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# main.py  主文件用于编写程序
def my_func1(a):
if a == 1:
return 2
elif a == -1:
return 3
else:
return 1

def my_func2(b):
if b != "yes":
raise ValueError("you can only say yes!")
else:
return True

# main_test.py 测试文件用于编写程序
import unittest
import main as m

class MyTestCase(unittest.TestCase): # 固定格式
def test_something1(self): # 可自定义函数功能名称
self.assertEqual(2, m.my_func1(1)) # 测试功能
self.assertEqual(3, m.my_func1(-1))
for i in range(-100, 100):
if i == 1 or i == -1:
continue
self.assertEqual(1, m.my_func1(i))

def test_something2(self):
self.assertTrue(m.my_func2("yes"))
with self.assertRaises(ValueError):
m.my_func2("nononono")

if __name__ == '__main__':
unittest.main()
# 当仅仅想测试某一个函数功能时,定义一个 suite 替换 unittest.main()
suite = unittest.TestSuite()
suite.addTest(MyTestCase('test_something1'))
unittest.TextTestRunner().run(suite)

assert 含义
assertEqual(a, b) a == b
assertNotEqual(a, b) a != b
assertTrue(condition) condition 是不是 True
assertFalse(condition) condition 是不是 False
assertGreater(a, b) a > b
assertGreaterThan(a, b) a >= b
assertLess(a, b) a < b
assertLessEqual(a, b) a <= b
assertIs(a, b) a is b,a 和 b 是不是同一对象
assertIsNot(a, b) a is not b,a 和 b 是不是不同对象
assertIsNone(a) a is None,a 是不是 None
assertIsNotNone(a) a is not None,a 不是 None?
assertIn(a, b) a in b, a 在 b 里面?
assertNotIn(a, b) a not in b,a 不在 b 里?
assertRaises(err) 通常和 with 一起用,判断 with 里的功能是否会报错

2.3 生成器与装饰器

生成器 Generator,一种占用更小内存的方式处理循环迭代,可以说生成器就是为循环设计的。生成器是一种优化代码程序节省占用内存的方法,如果当你的循环内存不足时,或者运行很慢时,生成器是你需要考虑的。

在循环的时候,我们的目的是为了每次循环拿到一些特定数据,然后为这些数据做处理。但是无可避免的会在内存中记录这些数值, 当需要记录的数值很多的时候,我们的内存可能就吃不消了。而生成器就是用来现用现存的
,我们只在需要这个数据的时候生成它,生成完了我就不用了,也不需要记录。这种时候将会节约我很多内存的需求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def need_return():
for item in range(5):
if item % 2 == 0:
print("我要扔出去一个 item=%d 了" % item)
yield item # 这里就会返回给下面的 for 循环
print("我又回到里面了")

for i in need_return():
print("我在外面接到了一个 item=%d\n" % i)
>>
#我要扔出去一个 item=0 了
#我在外面接到了一个 item=0
#
#我又回到里面了
#我要扔出去一个 item=2 了
#我在外面接到了一个 item=2
#
#我又回到里面了
#我要扔出去一个 item=4 了
#我在外面接到了一个 item=4
#
#我又回到里面了

定义生成器类
用一个 class 也是可以表示一个迭代器,生成器的。 如果我们将上面的逻辑转化成 class,这个 class 可能相对比较复杂,但是也意味着你可以有更多设置和控制发生在这个 class 里面。 里面我们申明了用于生成器的两个 method,__iter__ 和 __next__。

__iter__ 的意思是,当我在外面 for 循环进行迭代时,我返回什么?在下面例子中,我就把自己这个 class 本身返回回去,继续让自己做迭代就好了。

__next__ 的意思是每次迭代的时候,我的函数会放出来什么元素。下面的功能中实现的就是放出来一个被计算过的 item 元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class NeedReturn:
def __init__(self, init_value=0):
self.tmp = init_value
self.item = 0

def __iter__(self):
return self

def __next__(self):
while True:
if self.item == self.tmp:
self.tmp *= 2
return self.item % 等价于yield
self.item += 1
if self.item == 300:
raise StopIteration

for i in NeedReturn(10):
print(i)
>>
#10
#20
#40
#80
#160

装饰器 Decorator 它是一个装饰 Python Function 的东西。 Function 为什么要被装饰?那就是我们想为这个 Function 做些额外的事情。但是只为一个 Function 做这件事,那我们还不如直接改掉这个 Function,让它直接干这件事就好了。 Decorator 的好处就是,我们可以给批量的 Function 都做这件事。当有一批 Function 都要做些前置或者后置的工作,我们可以统一给他们装修,用一个装饰器统一处理从而减轻你的开发量,特别是针对于不同 function,要做同样的前置处理或后置处理的时候。

比如数据库的处理,来了一条数据,我先要验证这个数据的准确性,然后进行个性化 function 的处理,最后将加工后的数据写入数据库。
网页个人页的鉴权,用户每一个点击,我都先需要鉴权,才能做后续不同的 function。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def authorization(fn):  #authorization 与@authorization一致即可
def check_and_do(name):
if name != "hm": # 鉴权
print(name + " has no right!")
return
res = fn(name)
return res
return check_and_do #返回值check_and_do与函数功能check_and_do一致即可

@authorization #装饰器格式
def outer1(name):
print(name+" outer1")

@authorization
def outer2(name):
print(name+" outer2")

@authorization
def outer3(name):
print(name+" outer3")

outer1("hm")
outer2("hmm")
outer3("hm")
>>
#hm outer1
#hmm has no right!
#hm outer3

2.4 工程问题编程思路

问题聚焦: 我们面临一个什么具体问题需要进行解决,最终简化问题提取关键核心点(实际存在并且是有研究或者工程应用价值)
产品定位: 为解决此问题我们需要做什么,实现什么功能来创造价值。

程序设计: 梳理需要实现的各个模块功能间的逻辑关系,形成模块-类-功能的编程架构;定制解决方案系统设计功能的开发,尽可能的成系统编程、可复用代码从而优化编程架构。
程序开发: 基于形成的模块-类-功能的编程架构进行具体开发,并根据开发过程中遇到的问题不断完善流程和克服问题。


3. python 文件数据处理

3.1文件数据读写

3.1.1 基础读写

1
2
3
4
5
6
7
8
9
10
11
12
13
# with结构(将文件的打开和关闭嵌入到了一个 with 架构中不用担心忘记关闭文件)
with open("new_file2.txt", "w") as f:
f.write("some text...\n 2nd line\n") # 在文件里写入
f.writelines(["some text for file2...\n", "2nd line\n"])
# f.writelines与f.write等价,但必须在元素末尾加 `\n`来换行不然多出来是黏连的。
with open("new_file2.txt", "r") as f:
print(f.read()) # 在文件里读取
f.seek(0) # 将开始读的位置从写入的最后位置调到开头
print(f.readlines()) # 与f.read()等价
f.seek(0)
print(f.readline()) #读取当前行,取代一次性全部读取,不让内存被一次性占满。
# 注:f.read()和f.readlines()使用后指针移到最后一行开始位置;
# f.readline()使用后指针自动移到下一行开始位置。
1
2
3
4
5
6
7
8
9
10
11
12
# 编码格式与中文乱码
# 按二进制 binary读写时 “wb”,"rb",
with open("chinese.txt", "wb") as f:
f.write("这是中文的,this is Chinese")# 报错
f.write("这是中文的,this is Chinese".encode("utf-8"))
# 写入时将string → a bytes-like object 并按utf-8格式
with open("chinese.txt", "rb", ) as f:
print(f.read().decode('utf-8'))
# 读取时将a bytes-like object → string 按utf-8格式
(或者)
with open("chinese.txt", "r", encoding="utf-8" ) as f:
print(f.read())
MODE 含义
w (创建)写文本
r 读文本,文件不存在会报错
a 在文本最后添加
wb 写二进制 binary
rb 读二进制 binary
ab 添加二进制
w+ 又可以读又可以(创建)写
r+ 又可以读又可以写, 文件不存在会报错
a+ 可读写,在文本最后添加
x 创建

3.1.2 Numpy库中的数据读写

3.1.3 Panda库中的数据读写

3.2文件目录管理(OS库)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import os 
import shutil
print("当前目录:", os.getcwd())
print("当前目录里有什么:", os.listdir())

#创建一个 project 的文件夹其中exist_ok=True 当project存在时不报错。(默认为False)
os.makedirs("user", exist_ok=True) # 创建Dictionary文件夹
# 用户注册
if os.path.exists("user/hm"):
print("user exist")
else:
os.makedirs("user/hm")
print("user created")
print(os.listdir("user"))

# 用户注销
if os.path.exists("user/hm"):
os.removedirs("user/hm") #只能删除文件夹为空的文件夹
# 注意 shutil 库太强大了,它会清空整个目录防止误删,防止误删,防止误删!
shutil.rmtree("user/mofan")
print("user removed")
else:
print("user not exist")

# 用户更名
os.rename("user/hm", "user/hm_new")
print(os.listdir("user"))

#文件目录多种检验(os.path)
with open("user/hm/a.txt", "w") as f:
f.write("nothing")
print(os.path.isfile("user/hm/a.txt")) # True
print(os.path.exists("user/hm/a.txt")) # True
print(os.path.isdir("user/hm/a.txt")) # False
print(os.path.isdir("user/hm")) # True

#文件的复制
def copy(path):
dir_name, filename = os.path.split(path)
new_filename = "new2_" + filename # 新文件名
new_path = os.path.join(dir_name, new_filename) # 目录重组
shutil.copy2(path, new_path) # 复制文件
return os.path.isfile(new_path), new_path
copied, new_path = copy("user/hm/a.txt")
if copied:
print("copied to:", new_path)
else:
print("copy failed")

(感性认识)Dictionary和package的使用,存储资源等不需要执行诸多种类的程序时采用Dictionary 否则用package

 Dictionary在pycharm中是一个文件夹目录,放置资源文件,对应于在进行JavaWeb开发时用于放置css/js文件的目录,或者说在进行物体识别时,用来存储背景图像的文件夹。该文件夹其中并不包含_ _ init.py_ _文件。
 Python package 包文件夹而言,与Dictionary不同之处在于其会自动创建_ _ init.py_ _ 文件。其包括一组模块和一个 _ _ init.py_ _ 文件。该包的使用与Python的import机制有关,这关乎到你的哪些.py文件是对外可访问的。有些时候,如果一个包下有很多模块,在调用方import如此多模块是很费事,且不优雅的,此时可以通过修改 _ _ init_ _.py来完成该任务。
 

3.3正则表达式匹配(未完成)

  在文件、文字查找、匹配、替换、处理时,少不了要根据特定规则来处理对应文字的情况。 正则表达式 Regular Expression(regex —— re库)就是通过用一些规则或者模板来帮你找到文字,替换文字的工具。

正则库功能表
| 功能 | 说明 | 举例 |
| :—————-: | :———————————————————————————————————————————: | :—————————————————————————————————————————————: |
| re.search() | 扫描查找整个字符串,找到第一个模式匹配的 | re.search(r”run”, I run to you) > ‘run’ |
| re.match() | 从字符的最开头匹配,找到第一个模式匹配的即使用 re.M 多行匹配,也是从最最开头开始匹配 | re.match(r”run”, I run to you) > None |
| re.findall() | 返回一个不重复的 pattern 的匹配列表 | re.findall(rr[ua]n, I run to you. you ran to him) > [‘run’, ‘ran’] |
| re.finditer() | 和 findall 一样,只是用迭代器的方式使用 | for item in re.finditer(rr[ua]n, I run to you. you ran to him): |
| re.split() | 用正则分开字符串 | re.split(rr[ua]n, I run to you. you ran to him) > [‘I ‘, ‘ to you. you ‘, ‘ to him’] |
| re.sub() | 用正则替换字符 | re.sub(rr[ua]n, jump, I run to you. you ran to him) > ‘I jump to you. you jump to him’ |
| re.subn() | 和 sub 一样,额外返回一个替代次数 | re.subn(rr[ua]n, jump, I run to you. you ran to him) > (‘I jump to you. you jump to him’, 2) |

通用匹配方式表
| 特定匹配标识 | 表达含义 | 实际范围 |
| :—————: | :———————————————————: | :———————————————————————————-: |
| \d | 任何数字 | [0-9] |
| \D | 不是数字的 | |
| \s | 任何空白字符 | [ \t\n\r\f\v] |
| \S | 空白字符以外的 | |
| \w | 任何大小写字母,数字和 _ | [a-zA-Z0-9_] |
| \W | \w 以外的 | |
| \b | 匹配一个单词边界 | 比如 er\b 可以匹配 never 中的 er,但不能匹配 verb 中的 er |
| \B | 匹配非单词边界 | 比如 er\B 能匹配 verb 中的 er,但不能匹配 never 中的 er |
| \\ | 强制匹配 \ | |
| . | 匹配任何字符 (除了 \n) | |
| ? | 前面的模式可有可无 | |
| * | 重复零次或多次 | |
| + | 重复一次或多次 | |
| {n,m} | 重复 n 至 m 次 | |
| {n} | 重复 n 次 | |
| +? | 非贪婪,最小方式匹配 + | |
| ? | 非贪婪,最小方式匹配 | |
| ?? | 非贪婪,最小方式匹配 ? | |
| ^ | 匹配一行开头,在 re.M 下,每行开头都匹配 | |
| $ | 匹配一行结尾,在 re.M 下,每行结尾都匹配 | |
| \A | 匹配最开始,在 re.M 下,也从文本最开始 | |
| \B | 匹配最结尾,在 re.M 下,也从文本最结尾 | |

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# (举例说明)注册管理,验证邮箱是否有效。
import re
# 先compile解析好一个正则 pattern,然后直接用这个 pattern 去执行查找。
#当需要循环查找时将compile放在循环前,search放在循环内执行从而节省执行时间
ptn = re.compile(r"\w+?@\w+?\.com")
# “\w ” 表示任何大小写字母,数字和 _ “+?”表示前面的模式至少匹配一次。
# 当识别 “@” 的时候做其前面的非贪婪模式匹配“\.”表示.的字面含义。如果只有“.”表示匹配任何字符。
matched = ptn.search("huangming98@163.com")
print(matched)

# 将compile解析和模式查找放在一条语句里。
matched = re.search(r"\w+?@\w+?\.com", "huangming98@163.com")
#用 r"xxx" 来写一个 pattern,r 代表原生字符串,当成一个规则来记住所写 pattern 时,都需写上一个 r 在前面。
print( matched)
# Ans: <re.Match object; span=(0, 19), match='huangming98@163.com'>
# span=(0, 19)代表着在原始字符串中,我们找到的 pattern 是从哪一位到哪一位
1
2
3
4
#同时满足多种条件的pattern写法
re.search(r"ran|run", "I run to you")
re.search(r"r[au]n", "I run to you")
re.search(r"f(ou|i)nd", "I found you")
1
2
3
4
5
6
7
#中文识别
re.search(r"不.*?爱", "我不是很爱你")
#<re.Match object; span=(1, 5), match='不是很爱'>
re.search(r"[\u4e00-\u9fa5]+", "我爱Python是的。")
#<re.Match object; span=(0, 2), match='我爱'>
re.search(r"[\u4e00-\u9fa5!?。,¥【】「」]+", "我爱你,是的!")
#<re.Match object; span=(0, 7), match='我爱你,是的!'>

在模式中获取特定信息
我们能用 group()获取到特定位置的信息。只要我们在正则表达中,加入一个 () 选定要截取返回的位置, 他就直接返回括号里的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
## re.finditer提供了 file.group(0) 这种全匹配的信息,而re.findall未提供全匹配信息。
string = "I have 2021-02-01.jpg, 2021-02-02.jpg, 2021-02-03.jpg"
match = re.finditer(r"(\d+?)-(\d+?)-(\d+?)\.jpg", string)
for file in match:
print("matched string:", file.group(0), ",year:", file.group(1),
", month:", file.group(2), ", day:", file.group(3))
# > matched string: 2021-02-01.jpg ,year: 2021 , month: 02 , day: 01
# > matched string: 2021-02-02.jpg ,year: 2021 , month: 02 , day: 02
# > matched string: 2021-02-03.jpg ,year: 2021 , month: 02 , day: 03

match = re.findall(r"(\d+?)-(\d+?)-(\d+?)\.jpg", string)
for file in match:
print("year:", file[0], ", month:", file[1], ", day:", file[2])
# > year: 2021 , month: 02 , day: 01
# > year: 2021 , month: 02 , day: 02
# > year: 2021 , month: 02 , day: 03

名字索引
有时候 group 的信息太多了,括号写得太多,依靠数字位置索引识别信息容易错配?此时,我们还能用一个名字来索引匹配好的字段, 然后用 group(“索引”) 的方式获取到对应的片段。注意,re.findall 不提供名字索引的方法, re.search或者 re.finditer 可以用名字索引。为了索引,我们需要在括号中写上 ?P<索引名> 这种模式。
1
2
3
4
5
6
match = re.finditer(r"(?P<y>\d+?)-(?P<m>\d+?)-(?P<d>\d+?)\.jpg", string)
for file in match:
print("matched string:", file.group(0),
", year:", file.group("y"),
", month:", file.group("m"),
", day:", file.group("d"))

多模式匹配
在正则中还有一些特别的 flags,可以在 re.search,re.match(),re.findall() 等功能中使用。 主要目的也是方便我们编写正则,和用更简单的方法处理更复杂的表达式。
| 模式 | 全称 | 说明 |
| :—-: | :————————: | :———————————————————————————————: |
| re.I | re.IGNORECASE | 忽略大小写 |
| re.M | re.MULTILINE | 多行模式,改变’^’和’$’的行为 |
| re.S | re.DOTALL | 点任意匹配模式,改变’.’的行为, 使”.“可以匹配任意字符 |
| re.L | re.LOCALE | 使预定字符类 \w \W \b \B \s \S 取决于当前区域设定 |
| re.U | re.UNICODE | 使预定字符类 \w \W \b \B \s \S \d \D 取决于unicode定义的字符属性 |
| re.X | re.VERBOSE详细模式 | 这个模式下正则表达式可以是多行,忽略空白字符,并可以加入注释。 |
1
2
3
4
5
6
7
8
9
10
11
12
# 用 ^ran 固定样式开头,我是匹配不到第二行的 ran to you 的,所以我们得加上一个 re.M flag。 
# 注意我们提到过的 re.search() 和 re.match() 不一样,re.match() 是不管有没有 re.M flag,我的匹配都是按照最头上开始匹配。
#所以在下面的实验中,re.match() 匹配不到任何东西。
ptn = r"^ran"
string = """I
ran to you"""
print("without re.M:", re.search(ptn, string))
print("with re.M:", re.search(ptn, string, flags=re.M))
print("with re.M and match:", re.match(ptn, string, flags=re.M))
# > without re.M: None
# > with re.M: <re.Match object; span=(2, 5), match='ran'>
# > with re.M and match: None

1
2
3
4
5
6
7
8
9
# 如果你想用多种 flags,也是可以的,
#> 比如我想同时用 re.M, re.I,你只需要这样书写re.M|re.I:
ptn = r"^ran"
string = """I
Ran to you"""
print(re.search(ptn, string, flags=re.M|re.I))
print(re.search(r"(?im)^ran", string))
re.search(r"(?im)^ran", string)
# > <re.Match object; span=(2, 5), match='Ran'>

小案例
这个问题就是针对文件夹内所有的文件,将里面提到的 huangming98.github.io 改成 huangm.cn。
首先需要简化问题,简化出来的话,其实就是要有三个步骤。

  1. 遍历所有的文本文件
  2. 读取文件中文本字段
  3. 替换掉特定字段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import os
#找到所有的文本并遍历
for filename in os.listdir("files"):
file_path = os.path.join("files", filename)
#读取文件中文本字段
with open(file_path, "r") as f1:
string = f1.read()
#替换掉特定字段
new_string = re.sub(r"huangming98.github.io", "huangm.cn", string)
#测试验证;将原文本替换过的文字拷贝到新文件中,原文件不改变
with open(os.path.join("files", "new_"+filename), "w") as f2:
f2.write(new_string)

#判断操作是否正确
for filename in os.listdir("files"):
if filename.startswith("new_"):
continue
file_path = os.path.join("files", "new_"+filename)
with open(file_path, "r") as f:
print(file_path, ": ", f.read())

 

3.3序列化数据传输(Json and Pickle)

  序列化(Serialization):把不同类型的数据打包保存在电脑硬盘中,或者用于数据的传输(云服务)。

Pickle

1
2
3
4
5
6
7
8
9
import os
data = {"filename": "f1.txt", "create_time": "today", "size": 111}
pickle.dumps(datas) # 将python对象编码成Bytes字符串
with open("data.pkl", "wb") as f:
pickle.dump(data, f) # pickle打包到文件data.pkl里
with open("data.pkl", "rb") as f:
data = pickle.load(f) # 用 open 的方式把文件都读出来,然后再用 pickle 对其解析
os.listdir()
print(data)

Json
  JSON (JavaScript Object Notation)是用于存储和交换数据的语法。最初是用 JavaScript 对象表示法编写的文本,但随后成为了一种常见格式,被包括Python在内的众多语言采用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import json
data = {"filename": "f1.txt", "create_time": "today", "size": 111}
json.dumps(data) # 将python对象编码成json字符串(字符串)
json.loads(data) # 将Json字符串解码成python对象
with open("data.json", "w") as f:
json.dump(data, f) # 将python中的对象转化成json储存到文件f中(文件流)

print("直接当纯文本读:")
with open("data.json", "r") as f:
print(f.read())

print("用 json 加载了读:")
with open("data.json", "r") as f:
new_data = json.load(f) # 加载json格式数据
print("字典读取:", new_data["filename"])

json.dumps()的一些参数 (重点)
  因为dumps编码以后的json格式输出比较的紧凑,如果不止一行看起来就不是特别好看,就像一堆乱码似的。所以,就推出了一些可选参数来让json码的可读性更高。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
json.dumps(obj, sort_keys=False, skipkeys=False, ensure_ascii=True, check_circular=True, 
allow_nan=True, cls=None, indent=None, separators=None,
encoding="utf-8", default=None)

obj:就是你要转化成json的对象。
sort_keys =True:是告诉编码器按照字典排序(a到z)输出。如果是字典类型的python对象,就把关键字按照字典排序。
indent:参数根据数据格式缩进显示,读起来更加清晰。
{"name": "\u4f60\u731c", "age": 19, "city": "\u56db\u5ddd"} 变为
{
"name": "\u4f60\u731c",
"age": 19,
"city": "\u56db\u5ddd"
}
separators=(',',':'):是分隔符的意思,参数意思分别为不同dict项之间的分隔符和dict项内key和value之间的分隔符,把:和,后面的空格都除去了。
{"name": "\u4f60\u731c", "age": 19, "city": "\u56db\u5ddd"}
{"name":"\u4f60\u731c","age":19,"city":"\u56db\u5ddd"}
skipkeys:默认值是False,如果dict的keys内的数据不是python的基本类型(str,unicode,int,long,float,bool,None),设置为False时,就会报TypeError错误。此时设置成True,则会跳过这类key。
ensure_ascii=True:默认输出ASCLL码,如果把这个该成False,就可以输出中文。
check_circular:如果check_circular为false,则跳过对容器类型的循环引用检查,循环引用将导致溢出错误(或更糟的情况)。

allow_nan:如果allow_nan为假,则ValueError将序列化超出范围的浮点值(nan、inf、-inf),严格遵守JSON规范,而不是使用JavaScript等价值(nan、Infinity、-Infinity)。

default:default(obj)是一个函数,它应该返回一个可序列化的obj版本或引发类型错误。默认值只会引发类型错误。

Pickle 与 Json 对比如下:
| 对比 | Pickle | Json |
| :—————: | :—————————————: | :————————————————————-: |
| 存储格式 | Python 特定的 Bytes 格式 | 通用 JSON text 格式,可用于常用的网络通讯中 |
| 数据种类 | 类,功能,字典,列表,元组等 | 基本和 Pickle 一样,但不能存类,功能 |
| 保存后可读性 | 不能直接阅读 | 能直接阅读 |
| 跨语言性 | 只能用在 Python | 可以跨多语言读写 |
| 处理时间 | 长(需编码数据) | 短(不需编码) |
| 安全性 | 不安全(除非你信任数据源) | 相对安全 |

 

python 相关连接

1. python基础

2. python100天

3.虚拟环境配置

1
2
3
4
conda create -n rich python=3.7  #创建环境
conda activate rich #激活环境
conda deactivate rich #退出环境
conda remove -n rich --all #删除环境

4. requirements

1
pip install -r requirements.txt

例如

1
2
3
4
5
6
pytest==6.2.5
pytest-json-report==1.4.1
pytest-metadata==1.11.0
pytest-ordering==0.6
PyTestReport==0.2.1
python-dateutil==2.8.2

5.1 教程

1
python -m torch.distributed.launch --nproc_per_node=1 --master_port=29500 tools/analysis_tools/benchmark.py $cofig $checkpoint --launcher pytorch

5.3 分布式训练

1
2
3
4
Export CUDA_VISIBLE_DEVICES=”0,1,2,3

CUDA_VISIBLE_DEVICES=”0,1,2,3” python -m torch.distributed.launch --nproc_per_node=4 tools/train.py $cofig $checkpoint --launcher pytorch

5.4 训练

1
2
3
4
Export CUDA_VISIBLE_DEVICES=2

CUDA_VISIBLE_DEVICES=2 python tools/train.py $cofig $checkpoint --gpus 2

5.5 测算模型参数量和计算量

1
2
python tools/analysis_tools/get_flops.py ${CONFIG_FILE} [--shape ${INPUT_SHAPE}]

5.6 指定GPU运行

1
2
3
4
export CUDA_VISIBLE_DEVICES=1 

CUDA_VISIBLE_DEVICE=1 python xxx.py …

5.7. Git

1
2
3
4
5
6
7
8
9
10
git init (初始化本地仓库)
git remote add origin git@github.com:open-mmlab/mmdetection.git
git add . (提交缓存)
git status
git commit -m “ 备注 ”

git pull --rebase origin master (同步)
or git pull + git pull origin master --allow-unrelated-histories 【允许不相关历史提交,并强制合并】
git push -u origin master (推送)

  • 冲突
1
2
3
4
5
6
7
8
git add . 

git rebase --continue

git pull --rebase origin master

git push origin master

6. OpenCV教程

7. pyechart

标题:Python Basic Notes

作者:黄铭

发布于2024-07-13 19:05:11

更新于2024-10-25 19:07:10

原始链接:https://www.huangm.cn/cn/2024/07/13/Python-Basic-Notes/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际(CC BY-NC-ND 4.0) 转载请保留原文链接及作者。