05.Python高级编程

1 ==,is的使用

  • is 是比较两个引用是否指向了同一个对象(地址引用比较)。
  • == 是比较两个对象是否相等。(比较的数值)

2 深拷贝、浅拷贝、copy.copy

2.1 浅拷贝

浅拷贝:

  1. 拷贝的是地址引用。可以找到共同的内容
  2. 一方修改了,另一方受影响
a = [1,2,3,4]
b = a
print(id(a)) #2540558597256
print(id(b)) #2540558597256
a.append(5)
print(a)     #[1, 2, 3, 4, 5]
print(b)     #[1, 2, 3, 4, 5]

2.2 深拷贝

深拷贝:

  1. 深拷贝的是内容一样。地址不一样。
  2. 一方修改了,另一方不受影响
  • b = copy.deepcopy(a)

    b得到的内容与a的内容完全一样,地址不一样。

    就算a中有对象引用,b中对应的引用的对象依然是内容一样,地址不一样。

    递归拷贝

    注意:
    如果是一个不可变对象(内部存储还是不可变对象),深拷贝的结果 = 浅拷贝,地址一样

2.3 copy.copy

copy.copy

  1. copy.copy这个函数结果会因为是可变或者不可变导致结果不同
  2. 只能拷贝一层。根据类型有关。如果是列表(可变类型),深拷贝。如果是元组(不可变)浅拷贝
  3. 如果里面还有嵌套的对象,浅拷贝
  • b = copy.copy(a)
import copy

a = [1,2,3,4]

#相当于深拷贝
b = copy.copy(a)

print(id(a))
print(id(b))
a.append(5)
print(a)
print(b)

print('*'*50)

a = (1,2,3,4)

#相当于浅拷贝
b = copy.copy(a)

print(id(a))
print(id(b))


----------------------------

a = [11,22,33]
b = [44,55,66]
c = [a,b]

d = copy.copy(c)

print(id(c))
print(id(d))
print(c)
print(d)

print('*'*50)

a.append(120)
#c[0].append(120)
print(c)
print(d)

print('*'*50)

a = [11,22,33]
b = [44,55,66]
c = (a,b)

d = copy.copy(c)

print(id(c))
print(id(d))
print(c)
print(d)

print('*'*50)

a.append(120)
#c[0].append(120)
print(c)
print(d)

3 属性property

3.1私有属性添加getter和setter方法

私有的内容,对外不能直接访问。
如果想对外访问,需要提供可以访问的方法,比如这里的getMoney,setMoney

调用的时候比较麻烦。
能不能像操作属性一样呢?

属性名 = property(get,set)
class Money(object):
    def __init__(self):
        self.__money = 0

    def getMoney(self):
        return self.__money

    def setMoney(self, value):
        if isinstance(value, int):
            self.__money = value
        else:
            print("error:不是整型数字")

m = Money()
print(m.getMoney())
m.setMoney(10)
print(m.getMoney())

3.2 使用property升级getter和setter方法

class Money(object):
    def __init__(self):
        self.__money = 0

    def getMoney(self):
        return self.__money

    def setMoney(self, value):
        if isinstance(value, int):
            self.__money = value
        else:
            print("error:不是整型数字")

    money = property(getMoney,setMoney)


m.money = 120       #相当于上面的m.setMoney(120)
print(m.money)      #相当于上面的m.getMoney
#或者是
print(Money.money)

3.3 使用property取代getter和setter方法

class Money(object):
    def __init__(self):
        self.__money = 0

    @property
    def money(self):
        return self.__money

    @money.setter
    def money(self, value):
        if isinstance(value, int):
            self.__money = value
        else:
            print("error:不是整型数字")

m = Money()
print(m.money)

m.money = 120

print(m.money)

4 生成器

4.1 什么是生成器

如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器:generator。

4.2 创建生成器方法1

要创建一个生成器,有很多种方法。第一种方法很简单,只要把一个列表生成式的 [ ] 改成 ( )

#列表生成式
ls = [x for x in range(100)]
print(ls)

#生成器
ge = (x**2 for x in range(1000))
print(ge)
print(type(ge))

i = 0
while i<19:
    next(ge)
    i+=1
print(next(ge))

总结

生成器保存的是算法,每次调用 next(G) ,就计算出 G 的下一个元素的值,直到计算到最后一个元素,没有更多的元素时,抛出 StopIteration 的异常。当然,这种不断调用 next() 实在是太变态了,正确的方法是使用 for 循环,因为生成器也是可迭代对象。所以,我们创建了一个生成器后,基本上永远不会调用 next() ,而是通过 for 循环来迭代它,并且不需要关心 StopIteration 异常。

4.3 创建生成器方法2 yield 值

yield 值

  1. 调用函数,得到一个生成器对象。这个函数没有执行
  2. next调用1得到的对象,如果遇到了yield,代码会阻塞,next的返回值就yield后的值
def fib(times):
    print('0....')
    n = 0
    a,b = 0,1
    while n<times:
        print('1....')
        yield b
        print('2....')
        a,b = b,a+b
        n+=1
    print('3....')


ge = fib(5)
print(ge)
print('*'*50)
m = 0
while m<5:
    print(next(ge))
    m+=1

4.4 send的使用

第一种和第二种,一旦生成器确定,算法不能改变。

这里的例子,定义了变量(temp),可以使用send发送参数,发给这里变量。

根据这个变量的值的不同,可以改变算法的逻辑。

所以,这种写法的作用:在运行过程中,可以改变算法

def gen():
    i = 0
    while i<1000:
        temp = yield i  
        if temp==True:
            #逻辑代码
            print('执行A计划')
            i+1
        else:
            #逻辑代码
            print('执行B计划')
            i+=2

myGenerator = gen()
ret = next(myGenerator)
print(ret)
#1、为当前停止的代码的左侧变量赋值
#2、生成器往下走一个行,返回yield值
ret = myGenerator.send(True)
print(ret)
ret = myGenerator.send(False)
print(ret)

4.5 模拟多任务实现方式之一:模拟协程

import time

def test1():
    while True:
        print("--王者荣耀--")
        #time.sleep(2)
        yield None

def test2():
    while True:
        print("--music--")
        yield None

def main():
    t1 = test1()
    t2 = test2()
    while True:
        t1.__next__()
        t2.__next__()

main()

4.6 总结

总结

  • 生成器是这样一个函数,它记住上一次返回时在函数体中的位置。对生成器函数的第二次(或第 n 次)调用跳转至该函数中间,而上次调用的所有局部变量都保持不变。
  • 生成器不仅“记住”了它数据状态;生成器还“记住”了它在流控制构造(在命令式编程中,这种构造不只是数据值)中的位置。

生成器的特点:

  1. 节约内存
  2. 迭代到下一次的调用时,所使用的参数都是第一次所保留下的,即是说,在整个所有函数调用的参数都是第一次所调用时保留的,而不是新创建的

5 迭代器

迭代是访问集合元素的一种方式。迭代器是一个可以记住遍历的位置的对象。迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。迭代器只能往前不会后退。

5.1 可迭代对象

以直接作用于 for 循环的数据类型有以下几种:

  • 一类是集合数据类型,如 list 、 tuple 、 dict 、 set 、 str 等;
  • 一类是 generator ,包括生成器和带 yield 的generator function。

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

5.2 迭代器

isinstance(对象,Iterable)
如果结果为True
只是表示,这些对象以使用for循环迭代遍历,可以使用next

import collections


ge = (x for x in range(10))
print(isinstance(ge,collections.Iterable))  #True
print(isinstance(ge,collections.Iterator))  #True

print(next(ge))  #0

print('************************华丽的分割线************************')

ls = [x for x in range(10)]
print(isinstance(ls,collections.Iterable))  #True
print(isinstance(ls,collections.Iterator))  #False
for i in ls:
    print(i)       #0-9的数

5.3 iter()函数

  • 生成器都是 Iterator 对象,但 list 、 dict 、 str 虽然是 Iterable ,却不是 Iterator 。
  • 把 list 、 dict 、 str 等 Iterable 变成 Iterator 可以使用 iter() 函数:
ls = [33,4,5,6,7,8]
it=iter(ls)
for i in range(len(ls)):
    print(next(it)) #33-8的数

总结

  • 凡是可作用于 for 循环的对象都是 Iterable 类型;
  • 凡是可作用于 next() 函数的对象都是 Iterator 类型
  • 集合数据类型如 list 、 dict 、 str 等是 Iterable 但不是 Iterator ,不过可以通过 iter() 函数获得一个 Iterator 对象。
  • 目的是在使用集合的时候,减少占用的内容。

6 闭包

6.1 函数引用

def test1():
    print("--- in test1 func----")

#调用函数
test1()

#此是ret引用函数
ret = test1

print(id(ret))     #18779264
print(id(test1))   #18779264,地址都一样,说明指向同一个内存地址

#通过引用调用函数
ret()              #--- in test1 func----

6.2 闭包

  • 在函数内部再定义一个函数,并且这个内部函数用到了外部函数的局部变量
  • 那么将这个内部函数以及用到的一些局部变量称之为闭包
def outer(num):
    print('outer...')
    def inner():
        print('num=%s'%num)
    return inner

ret = outer(100) #outer...
ret()            #num=100
ret()            #num=100
def line_conf(a, b):
    def line(x):
        return a*x + b
    return line


line1 = line_conf(2,3)
print(line1(10))   #23

line2 = line_conf(3,4)
print(line2(10))   #34

闭包的优缺点

  1. 闭包似优化了变量,原来需要类对象完成的工作,闭包也可以完成
  2. 由于闭包引用了外部函数的局部变量,则外部函数的局部变量没有及时释放,消耗内存

7 装饰器

  • 装饰器,装饰器是闭包现象的一种体现,功能就是在运行原来功能基础上,加上一些其它功能,比如权限的验证,比如日志的记录等等。不修改原来的代码,进行功能的扩展。

7.1 简单的装饰器

def outer(func):
    print('outer...')
    def inner():
        print('inner...')
        func()
    return inner


def save():
    print('save...')


ret = outer(save) #outer
ret()             #inner...   save...
def login(func):
    def inner():
        name = input('输入用户名:')
        pwd = input('输入密码:')
        if name=='laowang' and pwd=='123':
            func()
        else:
            print('赶紧去登录。。。。。。')
    return inner


@login
def save():
    print('save...')

save()

7.2 多个装饰器

  • 多个装饰器,按照从内往外(从下往上)先后顺序执行
  • 为了方便记忆:可以理解为先写后运行,先写后结束
def makeBold(fn):
    print('makeBold...')
    def wrapped():
        return "--1--" + fn() + "--1--"
    return wrapped

def makeItalic(fn):
    print('makeItalic...')
    def wrapped():
        return "--2--" + fn() + "--2--"
    return wrapped

@makeBold
@makeItalic
def test():
    return "hello world-test"

print(test())
#结果
#makeItalic...
#makeBold...
#--1----2--hello world-test--2----1--

7.3 装饰器功能

  1. 引入日志
  2. 函数执行时间统计
  3. 执行函数前预备处理
  4. 执行函数后清理功能
  5. 权限校验等场景
  6. 异常的处理
  7. 缓存

7.4 装饰器示例

  1. 无参数的函数
from time import ctime, sleep

def timefun(func):
    def wrappedfunc():
        print("%s called at %s"%(func.__name__, ctime()))
        func()
    return wrappedfunc

@timefun
def foo():
    print("I am foo")

foo()  #foo called at Thu Jul 20 16:03:59 2017
       #I am foo
sleep(2)
foo()
foo = timefun(foo)
#foo先作为参数赋值给func后,foo接收指向timefun返回的wrappedfunc
foo()
#调用foo(),即等价调用wrappedfunc()
#内部函数wrappedfunc被引用,所以外部函数的func变量(自由变量)并没有释放
#func里保存的是原foo函数对象
  1. 被装饰的函数有参数
def timefun(func):
    def wrappedfunc(a, b):
        print("%s called at %s"%(func.__name__, ctime()))
        print(a, b)
        func(a, b)
    return wrappedfunc

@timefun
def foo(a, b):
    print(a+b)

foo(3,5)
#foo called at Thu Jul 20 16:07:48 2017
#3 5
#8
  1. 被装饰的函数有不定长参数
from time import ctime, sleep

def timefun(func):
    def wrappedfunc(*args, **kwargs):
        print(args)
        print(kwargs)
        print("%s called at %s"%(func.__name__, ctime()))
        func(*args,**kwargs)
    return wrappedfunc

@timefun
def foo(a, b, c,num):
    print(a+b+c)
    print(num)

foo(3,5,7,num=123)
#(3, 5, 7)
#{'num': 123}
#foo called at Thu Jul 20 16:11:06 2017
#15
#123
  1. 装饰器中的return
from time import ctime, sleep

def timefun_arg(pre="hello"):
    def timefun(func):
        def wrappedfunc():
            print("%s called at %s %s"%(func.__name__, ctime(), pre))
            return func()
        return wrappedfunc
    return timefun

@timefun_arg("wangcai")
def foo():
    print("I am foo")

@timefun_arg("python")
def too():
    print("I am too")

foo()
#foo called at Thu Jul 20 16:23:48 2017 wangcai
#I am foo
print('************************华丽的分割线************************')
too()
#too called at Thu Jul 20 16:23:48 2017 python
#I am too

总结:
一般情况下为了让装饰器更通用,可以有return

  1. 装饰器带参数,在原有装饰器的基础上,设置外部变量
def haha(x):
    def outer(func):
        def inner():
            if x%2==0:
                return func()+'八'
            else:
                return '八'+func()
        return inner
    return outer

@haha(1)
def laowang():
     return '老王'

print(laowang())  #八老王
  1. 类装饰器(扩展,非重点)
    装饰器函数其实是这样一个接口约束,它必须接受一个callable对象作为参数,然后返回一个callable对象。在Python中一般callable对象都是函数,但也有例外。只要某个对象重写了__call__()方法,那么这个对象就是callable的。
class Test(object):
    def __init__(self, func):
        print("---初始化---")
        print("func name is %s"%func.__name__)
        self.__func = func
    def __call__(self):
        print("---装饰器中的功能---")
        self.__func()
#说明:
#1. 当用Test来装作装饰器对test函数进行装饰的时候,首先会创建Test的实例对象
#    并且会把test这个函数名当做参数传递到__init__方法中
#    即在__init__方法中的func变量指向了test函数体
#
#2. test函数相当于指向了用Test创建出来的实例对象
#
#3. 当在使用test()进行调用时,就相当于让这个对象(),因此会调用这个对象的__call__方法
#
#4. 为了能够在__call__方法中调用原来test指向的函数体,所以在__init__方法中就需要一个实例属性来保存这个函数体的引用
#    所以才有了self.__func = func这句代码,从而在调用__call__方法中能够调用到test之前的函数体
@Test
def test():
    print("----test---")
test()
showpy()#如果把这句话注释,重新运行程序,依然会看到"--初始化--"
运行结果如下:
---初始化---
func name is test
---装饰器中的功能---
----test---

8 python是动态语言

8.1 动态语言的定义

动态编程语言是高级程序设计语言的一个类别,在计算机科学领域已被广泛应用。它是一类 在运行时可以改变其结构的语言 :例如新的函数、对象、甚至代码可以被引进,已有的函数可以被删除或是其他结构上的变化。动态语言目前非常具有活力。例如JavaScript便是一个动态语言,除此之外如 PHP 、 Ruby 、 Python 等也都属于动态语言,而 C 、 C++ 等语言则不属于动态语言。----来自维基百科

8.2 运行的过程中给类、对象绑定(添加)属性、方法

class mv_hero:
    def __init__(self,name,HP,MP):
        self.name = name
        self.HP = HP
        self.MP = MP
#调用,实例化
gtx = mv_hero('cjr',10,10)
print(gtx.name) #cjr
#添加一个实例属性
gtx.XP = 100
print(gtx.XP) #100
#添加一个类属性
mv_hero.xx = 99
#gtx.xx = 111
print(gtx.xx) #99

#定义一个类方法
@classmethod
def atteck(cls):
    cls.num = 100
#定义一个静态方法
@staticmethod
def test_hero():
    print('--test_hero--')

#给mv_hero类绑定类方法
mv_hero.atteck = atteck
#调用刚才绑定的类方法
mv_hero.atteck()
print(mv_hero.num)   #100

#给mv_hero类绑定静态方法
mv_hero.test_hero = test_hero
#调用刚才绑定的静态方法
mv_hero.test_hero()  #--test_hero--

import types
#定义一个实例方法
def fly(self, speed):
    print("%s在移动, 速度是 %s"%(self.name, speed))
#给mv_hero中的对象绑定实例方法
gtx.fly = types.MethodType(fly, gtx)
#调用刚才的实例方法
gtx.fly(180)  #cjr在移动, 速度是 180

9 元类

现在回到我们的大主题上来,究竟是为什么你会去使用这样一种容易出错且晦涩的特性?好吧,一般来说,你根本就用不上它!

'''
    使用type创建类
    

    type的第3个参数
    1、字符串 类名
    2、元组   父类
    3、字典      类属性
'''


'''
class Test:
    pass
'''
Test = type('Test',(),{})
print(Test)


Test = type('Test',(),{'name':'老王','age':10})

print(Test.name)


class Fu:
    def fu(self):
        print('fu...')

Zi = type('Zi',(Fu,),{})
print(Zi.__mro__)


print('***************************************华丽的分割线***************************************')

def haha(self):
    print('haha...')

def __init__(self,num):
    print('self...')
    self.num = num

@classmethod
def hehe(cls):
    print('hehe...')

Test = type('Test',(),{'name':'老王','age':10,'haha':haha,'hehe':hehe,'__init__':__init__})

t = Test(110)
t.haha()

Test.hehe()

10 垃圾回收

10.1 小整数对象池

  • 整数在程序中的使用非常广泛,Python为了优化速度,使用了小整数对象池, 避免为整数频繁申请和销毁内存空间。
  • Python 对小整数的定义是 [-5, 257) 这些整数对象是提前建立好的,不会被垃圾回收。在一个 Python 的程序中,所有位于这个范围内的整数使用的都是同一个对象.
  • 同理,单个字母也是这样的。
  • 但是当定义2个相同的字符串时,引用计数为0,触发垃圾回收

10.2 大整数对象池

  • 每一个大整数,均创建一个新的对象

10.3 intern机制

python创建9个”HelloWorld”对象,让他只占用一个”HelloWorld”所占的内存空间,python中有这样一个机制——intern机制,靠引用计数去维护何时释放。

字符串(含有空格),不可修改,没开启intern机制,不共用对象,引用计数为0,销毁。

总结

  • 小整数[-5,257)共用对象,常驻内存
  • 单个字符共用对象,常驻内存
  • 单个单词,不可修改,默认开启intern机制,共用对象,引用计数为0,则销毁

10.4Garbage collection(GC垃圾回收)

python采用的是引用计数机制为主,标记-清除和分代收集两种机制为辅的策略

11 内建属性

12 内建函数

  1. map():map函数会根据提供的函数对指定序列做映射
map(function, sequence[, sequence, ...]) -> list
·function:是一个函数
·sequence:是一个或多个序列,取决于function需要几个参数
·返回值是一个列表
参数序列中的每一个元素分别调用function函数
返回包含每次function函数返回值的list。
注意:先转成list才能print
>>>def square(x) :            # 计算平方数
...     return x ** 2

>>> map(square, [1,2,3,4,5])   # 计算列表和:1+2+3+4+5
[1, 4, 9, 16, 25]
>>> map(lambda x: x ** 2, [1, 2, 3, 4, 5])  # 使用 lambda 匿名函数
[1, 4, 9, 16, 25]
 
# 提供了两个列表,对相同位置的列表数据进行相加
>>> map(lambda x, y: x + y, [1, 3, 5, 7, 9], [2, 4, 6, 8, 10])
[3, 7, 11, 15, 19]
  1. range():
range(start, stop[, step]) -> list of integers
·start:计数从start开始。默认是从0开始。例如range(5)等价于range(0,5);
·stop:到stop结束,但不包括stop.例如:range(0,5) 是[0, 1, 2, 3, 4]没有5
·step:每次跳跃的间距,默认为1。例如:range(0,5) 等价于range(0, 5, 1)
python2中range返回列表,python3中range返回一个迭代值。如果想得到列表,可通过list函数
  1. filter():filter函数会对指定序列执行过滤操作
filter(function or None, sequence) -> list, tuple, or string
·function:接受一个参数,返回布尔值True或False
·sequence:序列可以是str,tuple,list
·返回值是一个列表
过滤出列表中的所有奇数:

def is_odd(n):
    return n % 2 == 1
 
newlist = filter(is_odd, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
print(newlist)

输出结果 :
[1, 3, 5, 7, 9]
  1. sorted()

    sort 与 sorted 区别:
    sort 是应用在 list 上的方法,sorted 可以对所有可迭代的对象进行排序操作。
    list 的 sort 方法返回的是对已经存在的列表进行操作,而内建函数 sorted 方法返回的是一个新的 list,而不是在原来的基础上进行的操作。

sorted(iterable, key=None, reverse=False) --> new sorted list
reverse默认值为False,升序排序
>>>a = [5,7,6,3,4,1,2]
>>> b = sorted(a)       # 保留原列表
>>> a 
[5, 7, 6, 3, 4, 1, 2]
>>> b
[1, 2, 3, 4, 5, 6, 7]
 
>>> L=[('b',2),('a',1),('c',3),('d',4)]
>>> sorted(L, cmp=lambda x,y:cmp(x[1],y[1]))   # 利用cmp函数
[('a', 1), ('b', 2), ('c', 3), ('d', 4)]
>>> sorted(L, key=lambda x:x[1])               # 利用key
[('a', 1), ('b', 2), ('c', 3), ('d', 4)]
 
 
>>> students = [('john', 'A', 15), ('jane', 'B', 12), ('dave', 'B', 10)]
>>> sorted(students, key=lambda s: s[2])            # 按年龄排序
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]
 
>>> sorted(students, key=lambda s: s[2], reverse=True)       # 按降序
[('john', 'A', 15), ('jane', 'B', 12), ('dave', 'B', 10)]
  1. reduce():reduce函数会对参数序列中元素进行累积
reduce(function, sequence[, initial]) -> value
·function:该函数有两个参数
·sequence:序列可以是str,tuple,list
·initial:固定初始值

reduce依次从sequence中取一个元素,
和上一次调用function的结果做参数再次调用function。 

第一次调用function时,如果提供initial参数,
会以sequence中的第一个元素和initial作为参数调用function,
否则会以序列sequence中的前两个元素做参数调用function。 
注意function函数不能为None。

在Python3里,reduce函数已经被从全局名字空间里移除了,它现在被放置在fucntools模块里用的话要先引入:from functools import reduce

>>>def add(x, y) :            # 两数相加
...     return x + y
... 
>>> reduce(add, [1,2,3,4,5])   # 计算列表和:1+2+3+4+5
15
>>> reduce(lambda x, y: x+y, [1,2,3,4,5])  # 使用 lambda 匿名函数
15
def f(x,y):
    print('x=%s,y=%s'%(x,y))
    return x+y

#ret = functools.reduce(lambda x, y: x+y, ['a','b','c','d'],'o')
ret = functools.reduce(f, ['a','b','c','d'],'o')


运行结果:
x=o,y=a
x=oa,y=b
x=oab,y=c
x=oabc,y=d

13 functools

functools 是python2.5被引人的,一些工具函数放在此包里。

知道有这么个东西

14 模块进阶

14.1 常用标准库

标准库 说明
builtins 内建函数默认加载
sys 操作系统借口
functooks 常用的工具
json 编码和解码JSON对象
logging 记录日志、调试
multiprocessing 多进程
threading 多线程
copy 拷贝
time 时间
datetime 日期和时间
calendar 日历
hashlib 加密算法
random 生成随机数
re 字符串正则匹配
socket 标准的BSD Sockets API
shutil 文件和目录管理
glob 基于文件通配符搜索

更多标准库
http://python.usyiyi.cn/translate/python_352/library/index.html

15 编码风格

在pycharm里,Ctrl+Alt+L自动排版,排版的时候注意看一看就记住了

16 代码调试

pycharm
步骤:

  1. 设置断点
  2. shift+f9 开始调试
  3. 光标就在断点处停了。这一行没有运行的
  4. 下一行:f8
  5. 进入方法:f7
  6. 跳到下一个断点:alt+f9
  7. 进入方法,跳出这一步,shift+f8

猜你喜欢

转载自www.cnblogs.com/cjr0707/p/9695085.html