C语言基础学习PYTHON——基础学习D04                                

 

20180810内容纲要:

  1 内置函数

  2 装饰器

  3 生成器

  4 迭代器

  5 软件目录结构规范

  6 小结

 

1 内置函数

 

内置函数方法详解:https://docs.python.org/3/library/functions.html?highlight=built

 1 #Author:ZhangKanghui
 2 
 3 print(all([0,-5,4]))
 4 print(all([1,-5,4]))
 5 #all()    return true if all elements of the iterable are true (or if the iterable is empty)
 6 #any()    即是有一个真即为真。
 7 
 8 a =ascii([1,2])
 9 print(type(a),a)
10 print(type(a),[a])
11 #ascii()   把数据对象变成可打印的字符串形式
12 
13 #bin()       #把数字十进制转二进制
14 print(bool(0))
15 print(bool(1))
16 print(bool([]))
17 print(bool([1]))
18 
19 a =bytes("abcde",encoding="utf-8")
20 print(a.capitalize(),a)                     #字符串不可以被修改,因此字节格式的字符串也不能被修改
21 b =bytearray("abcde",encoding="utf-8")    #bytearray()变成列表,这样就可以修改了
22 print(b[0])                                #以ascii码输出第一个字节
23 b[1] = 100
24 print(b)
25 
26 
27 print(callable([]))                         #判断是否可调用,就看能不能加()的方式调用
28 def  a():pass
29 print(callable(a))
30 
31 print(chr(98))
32 print(ord('b'))
33 
34 
35 print(divmod(5,3))                          #输出结果(商,余数)
36 #eval()                                      #把一个字符串变成字典
37 
38 def var():
39     local_var =333
40     print(locals())
41 var()
42 print(globals())                            #程序的所有变量以key,value字典形式输出
43 print(globals().get('local_var'))                            #程序的所有变量以key,value字典形式输出
44 
45 
46 print(hash('Kanghui'))
47 
48 print(hex(15))              #转十六进制
49 print(oct(1))               #转八进制
50 print(pow(3,5))             #3的5次方
51 d ='<code object <module> at 0x03379230, file "", line 1>'
52 print(repr(d))              #转成字符串
53 print(round(1.433,2))       #保留两位小数
54 f =range(20)
55 #print(d[slice(2,5)])       这是什么鬼?说好的切片呢
56 
57 a ={6:30,8:7,1:4,99:27,18:16}
58 #print(sorted(a))        #只是把字典中的key排序了,没有value的值
59 #如何实现带有value的key排序呢/
60 #print(sorted(a.items()))        #排序完成后变成一个列表,因为字典默认是无序的
61 #那么按value排序怎么办呢?
62 print(sorted(a.items(),key =lambda x:x[1]))
63 print(a)
64 
65 
66 m =[1,2,3,4]
67 n =['a','b','c','d']
68 print(zip(m,n))
69 for i in zip(m,n):
70     print(i)

部分示例

 

2 装饰器

  a 高阶函数

  b 嵌套函数

高阶函数+嵌套函数》=装饰器

    定义:本质就是函数,为其他函数添加附加功能。

   原则:

1.不能修改被装饰函数的源代码

2.不能修改被装饰函数的调用方式

a 高阶函数

高阶函数1:貌似是一个装饰器,car装饰bar。虽然没有改变bar的源代码为bar添加了附加功能,但是改变了调用方式

 1 import  time
 2 
 3 def bar():
 4     time.sleep(3)
 5     print('in the bar')
 6 
 7 def car(func):
 8     start_time =time.time()
 9     func()
10     stop_time =time.time()
11     print('the func run time is %s' %(stop_time-start_time))
12 
13 car(bar)

高阶函数1

高阶函数2:

 1 import time
 2 def bar():
 3     time.sleep(3)
 4     print('in the bar')
 5 def car(func):
 6     print(func)
 7     return func
 8 
 9 
10 # print(car(bar))
11 #二者的区别
12 # a=car(bar())     #将bar()的执行结果/返回值传给func,这样就不符合高阶函数的定义
13 # b=car(bar)       #将bar的内存地址传给func,
14 # print(a)
15 # print(b)
16 t =car(bar)       #将bar的内存地址传给func先执行函数car里面的print(bar的内存地址),然后返回执行函数bar
17 t()
18 #此时,把t赋给bar
19 bar=car(bar)
20 bar()
21 #现在好像不修改函数的调用方式也能为其添加附加功能,但是给bar重新定义,将原来函数中的bar给覆盖掉了

高阶函数2

如何实现不改变源代码和调用方式的情况下实现装饰器呢?

b 嵌套函数

嵌套函数:

1 #Author:ZhangKanghui
2 
3 def foo():
4     print('in the foo')
5     def bar():                      #此处的函数bar即变量,局部变量。只能在嵌套函数内局部调用
6         print('in the bar')
7 
8 #bar()    这里不能调用函数bar可以类比局部变量

嵌套函数

接下来,把高阶函数和嵌套函数结合起来有很么用呢?

 

>>装饰器:

 1 #Author:ZhangKanghui
 2 #装饰器
 3 
 4 import time
 5 def timer(func):                    #高阶函数,返回值中包含函数名
 6     def dec():                      #嵌套函数
 7         start_time = time.time()
 8         func()                      #run  bar()
 9         stop_time = time.time()     #添加功能
10         print('the func run time is %s' % (stop_time - start_time))
11     return dec                     #返回函数dec的内存地址
12 #@timer                              #bar=timer(bar)
13 def bar():
14     time.sleep(3)
15     print('in the bar')
16 bar=timer(bar)
17 bar()                               #bar()=dec()
18 # 如果想要传参数
19 @timer
20 def test2(name):
21     print("test2:",name)
22 test2()

装饰器(基础版)

 1 #Author:ZhangKanghui
 2 #模拟网站,一个函数就是一个网页,部分网页需要登录,添加验证功能
 3 
 4 user,passwd = 'Kanghui','abc123'            #先写个特殊情况下指定用户账号密码
 5 import time
 6 def auth(func):
 7     def wrapper(*args,**kwargs):
 8         user_name =input("user_name:").strip()      #去掉两头的空格和回车
 9         password =input("passwd:").strip()
10 
11         if user ==user_name and passwd ==password:
12             print("\033[32;1mUser has passed authentication\033[0m")
13             func(*args,**kwargs)
14 
15         else:
16             exit("\031[32;1mInvalid password\033[0m")
17     return wrapper
18 
19 def index():
20     print("welcome to index page")
21 @auth
22 def bbs():
23     print("welcome to bbs page")
24 
25 index()
26 bbs()
27 #此时这个装饰器的基本功能已经完成。虽然正常情况下只需登录一次即可。
28 #接下来有个问题:没有改变函数源代码和调用方式但是函数执行以后的返回值发生了变化
29 @auth
30 def home():
31     print("welcome to home page")
32     return "from home"
33 print(home())           #调用home()相当于调用wrapper()
34 #如何获取到home的返回值
35 # 将func()             换成return  func(*args,**kwargs)     #这个可以使home()获取到自己的返回值

装饰器(进阶版)

 1 #Author:ZhangKanghui
 2 user,passwd = 'Kanghui','abc123'            #先写个特殊情况下指定用户账号密码
 3 import time
 4 def auth(auth_type):
 5     print("auth_type:",auth_type)
 6     def out_wrapper(func):
 7         def wrapper(*args,**kwargs):
 8             print("wrapper func args:",*args,**kwargs)
 9             if auth_type =='local':
10                 user_name =input("user_name:").strip()      #去掉两头的空格和回车
11                 password =input("passwd:").strip()
12 
13                 if user ==user_name and passwd ==password:
14                     print("\033[32;1mUser has passed authentication\033[0m")
15                     func(*args,**kwargs)
16 
17                 else:
18                     exit("\031[32;1mInvalid password\033[0m")
19             elif auth_type=='ldap':
20                 print("搞毛线")
21 
22         return wrapper
23     return out_wrapper
24 def index():
25     print("welcome to index page")
26 @auth(auth_type ='local')
27 def bbs():
28     print("welcome to bbs page")
29 @auth(auth_type ='ldap')
30 def home():
31     print("welcome to home page")
32     return "from home"

装饰器(高级版)

那么装饰器是如何实现在不改变源代码和调用方式的情况下为函数添加附加功能的呢?可以通过以上三个案例debug一下,看一下程序运行过程。

装饰器的作用;

  • 封闭:已实现的功能代码块不应该被修改
  • 开放:对现有功能的扩展开放

这里还有一个额外扩展练习:

 1 user_status = False #用户登录了就把这个改成True
 2 
 3 def login(auth_type): #把要执行的模块从这里传进来
 4     def auth(func):
 5         def inner(*args,**kwargs):#再定义一层函数
 6             if auth_type == "qq":
 7                 _username = "alex" #假装这是DB里存的用户信息
 8                 _password = "abc!23" #假装这是DB里存的用户信息
 9                 global user_status
10 
11                 if user_status == False:
12                     username = input("user:")
13                     password = input("pasword:")
14 
15                     if username == _username and password == _password:
16                         print("welcome login....")
17                         user_status = True
18                     else:
19                         print("wrong username or password!")
20 
21                 if user_status == True:
22                     return func(*args,**kwargs) # 看这里看这里,只要验证通过了,就调用相应功能
23             else:
24                 print("only support qq ")
25         return inner #用户调用login时,只会返回inner的内存地址,下次再调用时加上()才会执行inner函数
26 
27     return auth
28 
29 def home():
30     print("---首页----")
31 
32 @login('qq')
33 def america():
34     #login() #执行前加上验证
35     print("----欧美专区----")
36 
37 def japan():
38     print("----日韩专区----")
39 
40 @login('weibo')
41 def henan(style):
42     '''
43     :param style: 喜欢看什么类型的,就传进来
44     :return:
45     '''
46     #login() #执行前加上验证
47     print("----河南专区----")
48 
49 home()
50 # america = login(america) #你在这里相当于把america这个函数替换了
51 #henan = login(henan)
52 
53 # #那用户调用时依然写
54 america()
55 
56 # henan("3p")

隔壁老王讲述装饰器

 

3 生成器

>>生成器generator:

  1.只有在调用时才会生成相应的数据

  2.只记录当前位置

  3.只有一个__next__()方法逐个调用生成器中的数据

先来做个对比:

列表生成式:

 1 列表生成式:
 2     >>>[i*2 for i in range(10)]
 3     [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
 4     
 5      a=[]
 6     >>> for i in range(10):
 7     ...     a.append(i*2)
 8     ...
 9     >>> a
10     [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
11     
12     >>> ( i*2 for i in range(10) )
13     <generator object <genexpr> at 0x0155B030>

cmd列表生成式

生成器方式:

 1 通过生成器的方式
 2     >>> b=( i*2 for i in range(10) )
 3     >>> for i in b:
 4     ...     print(i)
 5     ...
 6     0
 7     2
 8     4
 9     6
10     8
11     10
12     12
13     14
14     16
15     18

cmd生成器方式

 

[]和()的区别在于:[]这个在访问之前,所有的数据都已经准备完毕。而()只是提供了一种算法数据暂时不存在,只有访问的时候才会存在

这样能够节省内存空间。不信可以试试这个

1 a=[for i*2 in range(100000000)]
2 
3     a[1000]
4 
5     b=(for i*2 in range(100000000))
6 
7     b[1000]
8 
9     TypeError: 'generator' object is not subscriptable

试试你的电脑怎么样

那除了循环还有什么方式可以调用生成器中的数据呢?

1 >>> b=( i*2 for i in range(10) )
2 
3 >>> b.__next__()
4 
5 0

view code

generator非常强大,如果推算的算法比较复杂,用类似列表的生成式的for循环无法实现的时候,还可以用函数实现。

比如著名的斐波那契数列(Fibonacci)除第一个和第二个外,任一个数都可以由前两个数相加得到。

1,1,2,3,5,8,13,…

 1 #Author:ZhangKanghui
 2 
 3 #斐波那契数列的推算
 4 '''
 5 def fib(max):
 6     n,a,b =0,0,1
 7     while n<max:
 8         print(b)
 9         a,b =b, a+b         #注意赋值语句
10         #一般会理解成:a=0   b=1    a=b     a=1
11         #b=a+b      b=1+1=2     但实际上b=0+1=1。。。。下面有详解
12         n=n+1
13     return 'done'
14 fib(10)
15 '''
16 
17 #注意赋值语句: a,b =b, a+b
18 #t =[a,a+b]          #t是一个tumple
19 #a =t[0]
20 #b=t[1]
21 #但不必显式写出临时变量t就可以赋值。

斐波那契数列的推算

#此时,离生成器只有一步之遥,fib函数只是定义了斐波那契数列推算的规则,这种逻辑很类似生成器
#此时,只需要吧print(b)改成yeild b神奇的事情就会发生了

 1 def fib(max):
 2     n,a,b =0,0,1
 3     while n<max:
 4         yield b
 5         a,b =b, a+b
 6         n=n+1
 7     return 'done'
 8 print(fib(10))
 9 #<generator object fib at 0x003BCDE0>
10 b=fib(10)
11 print(b.__next__())
12 print("i am your dad")
13 print(b.__next__())
14 print("hello world")
15 print(b.__next__())
16 #此时,我们能够完成随时在生成器访问值的过程中添加其他操作。
17 '''
18 print("---new strat---")
19 for i in b:
20     print(i)
21     '''
22 #并且可以随时再次接着继续访问生成器中的值
23 #那么最终会打印输出  return "done"吗?
24     #在for 循环中就不会打印。但是...一路next呢?

生成器之斐波那契数列

在一路next之后,出现异常该怎么抓住异常呢?

 1 def fib(max):
 2     n,a,b =0,0,1
 3     while n<max:
 4         yield b
 5         a,b =b, a+b
 6         n=n+1
 7     return 'done'
 8 print(fib(10))
 9 #<generator object fib at 0x003BCDE0>
10 b=fib(10)
11 print(b.__next__())
12 print("i am your dad")
13 print(b.__next__())
14 print("hello world")
15 print(b.__next__())
16 #此时,我们能够完成随时在生成器访问值的过程中添加其他操作。
17 '''
18 print("---new strat---")
19 for i in b:
20     print(i)
21     '''
22 #并且可以随时再次接着继续访问生成器中的值
23 #那么最终会打印输出  return "done"吗?
24     #在for 循环中就不会打印。但是...一路next呢?
25 print(b.__next__())
26 print(b.__next__())
27 print(b.__next__())
28 print(b.__next__())
29 print(b.__next__())
30 print(b.__next__())
31 print(b.__next__())
32 print(b.__next__())
33 print(b.__next__())
34 #异常   :StopIteration: done
35 #那么我们如何抓住这个异常呢?下面是一段捕获异常的代码

先来看看异常

1 g=fib(6)
2 while True:
3     try:
4         x =next(g)
5         print('g',x)
6     except StopIteration as e:
7         print("Generator return value:",e.value)
8         break

如何捕获异常

generator和函数的执行流程不一样。函数是顺序执行,遇到return语句或者最后一行函数语句就返回。而变成generator的函数,

在每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行。

补充一条:

1 def send(self,value)
2     '''Resume the generator and “send” a value that becomes the result of the current yield-expression.'''

send介绍

还可通过yield实现在单线程的情况下实现并发运算的效果:

 1 #Author:ZhangKanghui
 2 import time
 3 def consumer(name):
 4     print("%s is ready to eat baozi" %name)
 5     while True:
 6         baozi =yield     #保存当前状态
 7 
 8         print("baozi %s is coming,%s miximixi" %(baozi,name))
 9 
10 '''        
11 c =consumer('Kanghui')
12 c.__next__()
13 b1 ='beef'
14 c.send(b1)
15 c.__next__()            #此时包子并没有传值进去,只是在调用yield,继续执行yield之前保存的状态
16 '''
17 def producer(name):
18     c1 =consumer('A')               #这样只是把函数变成生成器还没开始运行,所以下面要调用next、
19     c2 =consumer('B')
20     c1.__next__()
21     c2.__next__()
22     print("Your dad is ready to make baozi")
23     for i in range(10):
24         time.sleep(1)
25         print("Have made two")
26         c1.send(i)
27         c2.send(i)
28 
29 producer('Kanghui')
30 #这样就能实现交互式地

生成器并发运算

 

4 迭代器

可直接作用于for循环的数据类型:

  一、集合数据类型:list、tuple、dict、set、str等

  二、generat,包括生成器和带yield的generator function

这些可以直接作用于for循环的对象统称为可迭代对象:Iterable

可以使用isinstance()判断一个对象是一个什么样的类型。

1 >>> from collections import Iterable
2 >>> isinstance([],Iterable)
3 True

isinstance用法1

可以被next()函数调用并不断返回下一个值的对象成为迭代器:Interator 。

1 >>> from collections import Iterator
2 >>> isinstance((x for x in range(10)),Iterator)
3 True
4 >>> isinstance([],Iterator)
5 False

isinstance用法2

虽然list、dict、str是Iterable 但是却不是Iterator

把这些变成Iterator可以使用iter()函数

1 >>> isinstance(iter([]),Iterator)
2 True

iter函数

这是为什么呢?因为在Python中的Iterator对象表示的是一个数据流,Iterator对象可以被next()函数调用并返回下个数据,直到没有数据抛出

StopIteration错误。可以把这个数据流看做是一个有序序列,但不知道长度,只能通过next()函数实现按需计算下一个数据,所以Iterator是

惰性的,只有在需要返回下一个数据时它才会计算。

Iterator甚至可以是一个无限大的数据流,例如全体自然数,而使用list是不可能存储全体自然数的。

 

Python的for循环实际上就是通过不断调用的next()函数实现的。  

 1 #Author:ZhangKanghui
 2 
 3 for x in [1,2,3,4,5]:
 4     pass
 5 #实际上就等价于:
 6 it = iter([1,2,3,4,5])          #迭代器
 7 while True:
 8     try:
 9         x =next(it)
10     except StopIteration:
11         break

for循环的本质

 

5 软件目录结构规范

目录的组织方式:在Stackoverflow的上,能看到大家对Python目录结构的讨论。点击这里

大概都会是这个样子。


如果你想写一个开源软件,目录该如何组织,可以参考这篇文章
可以参考Redis源码中Readme的写法,这里面简洁但是清晰的描述了Redis功能和源码结构。
一般来说,用setup.py来管理代码的打包、安装、部署问题。业界标准的写法是用Python流行的打包工具setuptools来管理这些事情。
setuptools的文档比较庞大,刚接触的话,可能不太好找到切入点。学习技术的方式就是看他人是怎么用的,可以参考一下Python的一个Web框架,flask是如何写的: setup.py
https://jeffknupp.com/blog/2013/08/16/open-sourcing-a-python-project-the-right-way/
https://github.com/antirez/redis#what-is-redis
https://github.com/pallets/flask/blob/master/setup.py
https://pip.readthedocs.io/en/1.1/requirements.html

6 小结
当你陷入绝望的时候,那就放弃吧!

  1 #===============>star.py
  2 import sys,os
  3 BASE_DIR=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  4 sys.path.append(BASE_DIR)
  5 
  6 from core import src
  7 
  8 if __name__ == '__main__':
  9     src.run()
 10 #===============>settings.py
 11 import os
 12 
 13 BASE_DIR=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 14 DB_PATH=os.path.join(BASE_DIR,'db','db.json')
 15 LOG_PATH=os.path.join(BASE_DIR,'log','access.log')
 16 LOGIN_TIMEOUT=5
 17 
 18 """
 19 logging配置
 20 """
 21 # 定义三种日志输出格式
 22 standard_format = '[%(asctime)s][%(threadName)s:%(thread)d][task_id:%(name)s][%(filename)s:%(lineno)d]' \
 23                   '[%(levelname)s][%(message)s]' #其中name为getlogger指定的名字
 24 simple_format = '[%(levelname)s][%(asctime)s][%(filename)s:%(lineno)d]%(message)s'
 25 id_simple_format = '[%(levelname)s][%(asctime)s] %(message)s'
 26 
 27 # log配置字典
 28 LOGGING_DIC = {
 29     'version': 1,
 30     'disable_existing_loggers': False,
 31     'formatters': {
 32         'standard': {
 33             'format': standard_format
 34         },
 35         'simple': {
 36             'format': simple_format
 37         },
 38     },
 39     'filters': {},
 40     'handlers': {
 41         #打印到终端的日志
 42         'console': {
 43             'level': 'DEBUG',
 44             'class': 'logging.StreamHandler',  # 打印到屏幕
 45             'formatter': 'simple'
 46         },
 47         #打印到文件的日志,收集info及以上的日志
 48         'default': {
 49             'level': 'DEBUG',
 50             'class': 'logging.handlers.RotatingFileHandler',  # 保存到文件
 51             'formatter': 'standard',
 52             'filename': LOG_PATH,  # 日志文件
 53             'maxBytes': 1024*1024*5,  # 日志大小 5M
 54             'backupCount': 5,
 55             'encoding': 'utf-8',  # 日志文件的编码,再也不用担心中文log乱码了
 56         },
 57     },
 58     'loggers': {
 59         #logging.getLogger(__name__)拿到的logger配置
 60         '': {
 61             'handlers': ['default', 'console'],  # 这里把上面定义的两个handler都加上,即log数据既写入文件又打印到屏幕
 62             'level': 'DEBUG',
 63             'propagate': True,  # 向上(更高level的logger)传递
 64         },
 65     },
 66 }
 67 
 68 
 69 #===============>src.py
 70 from conf import settings
 71 from lib import common
 72 import time
 73 
 74 logger=common.get_logger(__name__)
 75 
 76 current_user={'user':None,'login_time':None,'timeout':int(settings.LOGIN_TIMEOUT)}
 77 def auth(func):
 78     def wrapper(*args,**kwargs):
 79         if current_user['user']:
 80             interval=time.time()-current_user['login_time']
 81             if interval < current_user['timeout']:
 82                 return func(*args,**kwargs)
 83         name = input('name>>: ')
 84         password = input('password>>: ')
 85         db=common.conn_db()
 86         if db.get(name):
 87             if password == db.get(name).get('password'):
 88                 logger.info('登录成功')
 89                 current_user['user']=name
 90                 current_user['login_time']=time.time()
 91                 return func(*args,**kwargs)
 92         else:
 93             logger.error('用户名不存在')
 94 
 95     return wrapper
 96 
 97 @auth
 98 def buy():
 99     print('buy...')
100 
101 @auth
102 def run():
103 
104     print('''
105     1 购物
106     2 查看余额
107     3 转账
108     ''')
109     while True:
110         choice = input('>>: ').strip()
111         if not choice:continue
112         if choice == '1':
113             buy()
114 
115 
116 
117 #===============>db.json
118 {"egon": {"password": "123", "money": 3000}, "alex": {"password": "alex3714", "money": 30000}, "wsb": {"password": "3714", "money": 20000}}
119 
120 #===============>common.py
121 from conf import settings
122 import logging
123 import logging.config
124 import json
125 
126 def get_logger(name):
127     logging.config.dictConfig(settings.LOGGING_DIC)  # 导入上面定义的logging配置
128     logger = logging.getLogger(name)  # 生成一个log实例
129     return logger
130 
131 
132 def conn_db():
133     db_path=settings.DB_PATH
134     dic=json.load(open(db_path,'r',encoding='utf-8'))
135     return dic
136 
137 
138 #===============>access.log
139 [2017-10-21 19:08:20,285][MainThread:10900][task_id:core.src][src.py:19][INFO][登录成功]
140 [2017-10-21 19:08:32,206][MainThread:10900][task_id:core.src][src.py:19][INFO][登录成功]
141 [2017-10-21 19:08:37,166][MainThread:10900][task_id:core.src][src.py:24][ERROR][用户名不存在]
142 [2017-10-21 19:08:39,535][MainThread:10900][task_id:core.src][src.py:24][ERROR][用户名不存在]
143 [2017-10-21 19:08:40,797][MainThread:10900][task_id:core.src][src.py:24][ERROR][用户名不存在]
144 [2017-10-21 19:08:47,093][MainThread:10900][task_id:core.src][src.py:24][ERROR][用户名不存在]
145 [2017-10-21 19:09:01,997][MainThread:10900][task_id:core.src][src.py:19][INFO][登录成功]
146 [2017-10-21 19:09:05,781][MainThread:10900][task_id:core.src][src.py:24][ERROR][用户名不存在]
147 [2017-10-21 19:09:29,878][MainThread:8812][task_id:core.src][src.py:19][INFO][登录成功]
148 [2017-10-21 19:09:54,117][MainThread:9884][task_id:core.src][src.py:19][INFO][登录成功]

从入门到放弃

版权声明:本文为zhangkanghui原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/zhangkanghui/p/9453641.html