五、函数式编程

  

  python代码运行的时候遇到函数时如何处理的:

  从python解释器开始执行之后,就在内存中开辟了一个空间

  每当遇到一个变量的时候,就把变量名和值之间的对应关系记录下来

  但是,当遇到函数定义的时候解释器只是象征性的将函数名读入内存,表示知道这个函数的存在了,至于函数内部的变量和逻辑解释器根本不关心

  等执行到函数调用的时候,python解释器会再开辟一块内存来存储这个函数里面的内容,这个时候,才关注函数里面有哪些变量,而函数中的变量会存储在新开辟出来的内存中,函数中的变量只有在函数的内部使用,并且会随着函数执行完毕,这块内存中的所有内容也会被清空。

  我们给这个‘存放名字和值的关系’的空间叫做-------命名空间

  代码在运行开始,创建的存储‘变量名与值的关系’的空间叫做全局命名空间,在函数的运行中开辟的临时空间叫做局部命名空间,存放内置的叫做内置命名空间

  三种命名空间之间的加载和取值顺序:

  加载顺序:内置命名空间(程序运行前加载)>全局命名空间(程序运行中:从上到下加载)>局部命名空间(程序运行中:调用时才加载)

  取值:在局部调用:局部命名空间----->全局命名空间----->内置命名空间

扫描二维码关注公众号,回复: 7651622 查看本文章

       在全局调用:全局命名空间----->内置命名空间

   作用域

  作用域就是作用范围,按照生效范围可以分为全局作用域和局部作用域

  全局作用域:包含内置名称空间、全局名称空间,在整个文件的任意位置都能被引用、全局有效

  局部作用域:局部名称空间,只能在局部范围内生效

  global关键字:内部作用域想要对外部作用域的变量进行修改,global关键字在一个函数中来修改全局变量,在一个函数外使用global关键字没有效果,因为该变量本身就是全局变量

a = 10
def func():
    global a
    a = 20
print(a)
func()
print(a)
10
20

  global是对整个环境下的变量起作用,而不是对函数内的变量起作用

x = 0
def outer():
    x = 1
    def inner():
        global x 
        x = 2
        print('inner:',x)
    inner()
    print('outer:',x)
outer()
print('global:',x)
inner: 2
outer: 1
global: 2

   函数的嵌套定义

def f1():
    print('in f1')
    def f2():
        print('in f2')
    f2()
f1()
in f1
in f2

  函数的作用域链

def f1():
    a = 1
    def f2():
        print(a)
    f2()
f1()
1

 

def f1():
    a = 1
    def f2():
        a = 2
    f2()
    print('a in f1 : ',a)
f1()
a in f1 :  1

  nonlocal关键字

  1)外部必须有这个变量

  2)在内部函数声明nonlocal变量之前不能再出现同名变量

  3)内部修改这个变量如果想在外部有这个变量的第一层函数中生效

x = 0
def outer():
    x = 1
    def inner():
        nonlocal x 
        x = 2
        print('inner:',x)
    inner()
    print('outer:',x)
outer()
print('global:',x)
inner: 2
outer: 2
global: 0

  当使用nonlocal时,就声明了该变量不止是嵌套函数inner()里面才有效,而是在整个大函数里面都有效,但是并未改变全局变量

def f1():
    a = 1
    def f2():
        nonlocal a 
        a = 2
    f2()
    print('a in f1:',a)
f1()
a in f1: 2

  

五、函数式编程

  1.高阶函数
  高阶函数英文叫Higher-order function
  变量可以指向函数
  可见,abs(-10)是函数调用,而abs是函数本身
  f = abs
  函数本身也可以赋值给变量,即:变量可以指向函数
  f(-10)
  说明变量f现在已经指向了abs函数本身,直接调用abs()函数和调用变量f()完全相同

  函数名也是变量
  abs=10
  abs(-19) #不能执行了,因为abs = 10
  把abs指向10后,就无法通过abs(-10)调用该函数了,因为abs这个变量已经不指向求绝对值函数而是指向一个整数10
  当然实际代码绝对不能这么写,这里是为了说明函数名也是变量,要恢复abs函数,请重启python交互环境

  传入函数
  既然变量可以指向函数,函数的参数能接受变量,那么一个函数就可以接受另一个函数作为参数,这种函数就称之为高阶函数
  一个函数可以接受另一个函数作为参数,这种函数为高阶函数
  一个简单的高阶函数:

def add(x,y,f):
    return f(x)+f(y)
add(1,-2,abs) #3

  map/reduce

  python 内建了 map()和reduce()函数
  map()函数接受了两个参数,一个是函数,一个是Iterable,map将传入的函数依次作用到序列的每个元素,并把结果作为Iterator返回
  要把函数作用在list:[1,2,3,4,5,6,7,8,9]上,就可以用map()实现

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

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

def f(x):
    return x*x
L = []
for n in [1,2,3,4,5,6,7,8,9]:
    L.append(f(n))
print(L)
[1, 4, 9, 16, 25, 36, 49, 64, 81]

  这样也可以,但是你能一眼看明白“把f(x)作用在list的每一个元素并把结果生成一个新的list吗”
  所以,map()作为高阶函数,事实上他把运算规则抽象了,因此,我们不但可以计算简单的f(x)=x2方,还可以计算任意复杂的函数
  比如,把这个list所有数字转为字符串,只需要一行代码,其中str是函数

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

  reduce

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

reduce(f,[x1,x2,x3,x4]) = f(f(f(x1,x2),x3),x4) #第一个和第二个作用,结果:a
                                               #结果a和第三个作用,结果:b
                                               #结果b和第四个作用,结果:c,,,以此类推

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

from functools import reduce    #1+3
def add(x,y):                   #4(1+3)+5
    return x+y                  #9(4+5)...
reduce(add,[1,3,5,7,9])         #29                      

  当然求和运算可以直接用python内建函数sum(),没必要动用reduce,但是如果要把序列[1,3,5,7,9]变换成整数13579,reduce就可以派上用场:

from functools import reduce #x=1,y=3
def fn(x,y):                 #x=13,y=5
    return x*10 + y          #x=135,y=7
reduce(fn,[1,3,5,7,9])       #x=1357,y=9
#结果:13579

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

from functools import reduce
def fn(x,y):
    return x*10+y
def char2num(s):
    digits = {'0':0,'1':1,'2':2,'3':3,'4':4,'5':5,'6':6,'7':7,'8':8,'9':9}
    return digits[s] a = map(char2num,'13579')#因为map()是惰性的,所以一个一个放进去 a #结果:<map at 0x5ddab90> list(a) #[1, 3, 5, 7, 9]
reduce(fn,list(map(char2num,'13579'))) #等价于reduce(fn,[1, 3, 5, 7, 9])

  整理在一起
 

DIGITS = {'0':0,'1':1,'2':2,'3':3,'4':4,'5':5,'6':6,'7':7,'8':8,'9':9}
def str2int(s):
    def fn(x,y):
        return x*10+y
    def char2num(s):
        return DIGITS[s] return reduce(fn,map(char2num,s))#可能是因为map是惰性的,所以运行不出来,另外为什么不加list,是因为reduce里面可以接受元组;map第二个数作为前面函数的参数 str2int('12345')
12345

  filter

  python内建的filter()函数用于过滤序列

  filter()函数,是 Python 内置的另一个有用的高阶函数,filter()函数接收一个函数 f 和一个list,这个函数 f 的作用是对每个元素进行判断,返回 True或 False,filter()根据判断结果自动过滤掉不符合条件的元素,返回由符合条件元素组成的新list。
  和map()类似,filter()也接受一个函数和一个序列、和map()不同的是,filter()把传入的函数依次作用于每个元素,然后根据返回值是True还是False决定保留还是丢弃该元素
  例如,一个list中,删掉偶数,只保留奇数,可以这么写:

def is_odd(n):
    return n%2 == 1 # ==了就是True
list(filter(is_odd,[1,2,4,5,6,9,10,15]))
[1, 5, 9, 15]  

  把一个序列中的空字符串删掉,可以这么写:

def not_empty(s):
    return s and s.strip() #strip()参数为空时为删除空白符;.strip()方法作用是把字符串头尾的指定字符去掉,比如“000xxx000”,s.strip()为“xxx”
#None类型没有strip方法,执行会报错。前面加s,当s为None时,执行到and前面已经可以判定为False,and后的部分就不会被执行了。
#变量A and 变量B 返回的一定是个bool值,如果 x 为 False,x and y 返回 False,否则它返回 y 的运算值。并且 None没有strip()方法。
list(filter(not_empty,['A','','B',None,'c','']))

  可见用filter()这个高阶函数,关键在于正确实现一个’筛选‘函数
  注意到filter()函数返回的是一个Iterator,也就是一个惰性序列,所以要强迫filter()完成计算结果,需要用list()函数获得所有结果并返回list
  用filter求素数
  '''计算素数的一个方法是埃氏筛法,它的算法理解起来非常简单:
  首先,列出从2开始的所有自然数,构造一个序列:
  2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...
  取序列的第一个数2,它一定是素数,然后用2把序列的2的倍数筛掉:取新序列的第一个数3,它一定是素数,然后用3把序列的3的倍数筛掉:新序列的第一个数5,然后用5把序列的5的倍数筛掉:不断筛下去,就可以得到所有的素数。
  用python来实现这个算法,可以先构造一个从3开始的奇数序列:

def _odd_iter():
    n = 1
    while True:
        n = n+2
        yield n #注意这是一个生成器,并且是一个无限序列
#while True语句中一定要有结束该循环的break语句,否则会一直循环下去        

  然后定义一个筛选函数
def _not_divisible(n):
return lambda x:x%n>0
#最后,定义一个生成器,不断返回下一个素数
def primes():
yield2
it = _odd_iter()#初始序列
while True:
n = next(it) #返回序列的第一个数
yield n 
it = filter(_not_divisible(n),it)#构造新序列,把不能被整除的挑出来
#由于primes()也是一个无限序列,所以调用时需要设置一个退出循环的条件:
#难道说filter以及map、reduce里面的第二部分可以是iterator吗??????????
#打印1000以内的素数
for n in primes():
if n<1000:
print(n)
else:
break
#注意到Iterator是惰性计算的序列,所以我们可以用python表示‘全体自然数’,‘全体素数’这样的序列,而代码非常简洁

  sorted排序算法
  如果是数字,可以直接比较,但是如果是字符串或者是两个dict呢,直接比较数字上的大小是没有意义的,因此,比较的过程必须通过函数抽象出来
  python内置的sorted()函数就可以对list进行排序

sorted([36,5,-12,9,-21])
[-21, -12, 5, 9, 36]

  此外,sorted函数也是一个高阶函数,他还可以接受一个key函数来实现自定义的排序,例如按照sin函数大小排序

import math
sorted([36,5,-12,9,-21],key=math.sin)
[36, 5, -21, 9, -12]

  key指定的函数将作用于list的每一个元素上,并根据key函数返回的结果进行排序,对比原始的list和经过key=math.sin处理过的list

lis = [36,5,-12,9,-21]  #这个是sorted([36,5,-12,9,-21])的结果
keys = [36,5,-21,9,-12] #这个是输出sorted([36,5,-12,9,-21],key=math.sin)的结果,返回的还是原始序列的数

  我们在看一个字符串排序的例子

sorted(['bob','about','Zoo','Credit'])
['Credit', 'Zoo', 'about', 'bob']

  默认情况下,对字符串排序,是按照ASCII的大小比较的,由于'Z'<'a',结果,大写字母Z会排在小写字母a的前面
  注意是,大写排在小写的前面
  反向排序

sorted(['bob','about','Zoo','Credit'],key = str.lower, reverse = True)
['Zoo', 'Credit', 'bob', 'about']

  从上面例子可以看出,高阶函数的抽象能力是非常强大的,而且,核心代码可以保持得非常简洁

  2.返回函数
  函数作为返回值
  高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回
  我们来实现一个可变参数的求和

def calc_sum(*args):
    ax = 0
    for n in args:
        ax = ax+n
    return ax

  但是不需要立刻求和,而是在后面的代码中,根据需要在计算怎么办?可以不反悔求和的结果,而是返回求和的函数

def lazy_sum(*args):
    def sum():
        ax = 0
        for n in args:
            ax = ax + n 
        return ax return sum #sum是函数

  当我们调用lazy_sum()时,返回的并不是求和结果,而是求和函数(可能是函数是sum(),返回的是sum吧)

f = lazy_sum(1,3,5,7,9)
f
<function __main__.lazy_sum.<locals>.sum()>

  调用函数f时,才真正计算求和的结果

def lazy_sum(*args):
    def sum(): #相当于在原来的部分上加了def sum():    return sum
        ax = 0
        for n in args:
            ax = ax + n 
        return ax
    return sum #sum是函数
f = lazy_sum(1,3,5,7,9) f()
25

  在这个例子中,我们在函数lazy_sum中又定义了函数sum,并且,内部函数sum可以引用外部函数lazy_sum的参数和局部变量,
  当lazy_sum返回函数sum时,相关参数和变量都保存在返回的函数中,这种称为‘闭包(closure)’的程序结构拥有极大的威力
  请再注意一点,当我们调用lazy_sum()时,每次调用都会返回一个新的函数,即使传入相同的参数

f1 = lazy_sum(1,3,5,7,9)
f2 = lazy_sum(1,3,5,7,9)
f1 == f2
False

  f1 和 f2的调用结果互不影响

  闭包

def func():
    name = 'eva'
    def inner():
        print(name)

  闭包函数:内部函数包含对外部作用域而非全局作用域名字的引用,该内部函数(函数内部定义的函数)称为闭包函数

  我们知道函数内的变量要想在函数外部用,可以直接返回这个变量,那么如果我们想在函数外部调用函数内部的函数,自然是直接把这个函数名字返回就好了

def func():
    name = 'eva'
    def inner():
        print(name) #内部函数inner对外部name的引用
    return inner
f = func()
f()
eva

  判断闭包函数的方法

#输出的__colsure__有cell元素:是闭包函数
def func():
    name = 'eva'
    def inner():
        print(name)
    print(inner.__closure__)
    return inner
f = func()
f() 
print('-------------------------------------')
#输出的__closure__为None:不是闭包函数
name = 'egon'
def func2():
    def inner():
        print(name)
    print(inner.__closure__)
    return inner
f2 = func2()
f2()
(<cell at 0x0497D3F0: str object at 0x04938D60>,)
eva
-------------------------------------
None
egon

  闭包嵌套

def wrapper():
    money = 1000
    def func():
        name = 'eva'
        def inner():
            print(name,money)
        return inner
    return func
f = wrapper()
i = f()
i()
eva 1000

  命名空间:

  一共有三种命名空间从大范围到小范围顺序:内置命名空间,全局命名空间,局部命名空间

  作用域:小范围可以用大范围的,但是大范围不能用小范围的

  注意到返回的函数在其定义内部引用了局部变量(在函数内定义的量)args,所以,当一个函数返回了一个函数后,其内部的局部变量还被新函数引用,所以,闭包用起来简单实现起来可不容易

  另一个需要注意的问题是,返回的函数并没有立刻执行,而是直到调用f()才执行,我们来看一个例子:

def count():
    fs = []
    for i in range(1, 4):
        def f():
            return i*i
        fs.append(f)
    return fs f1, f2, f3 = count()
f1()
f2()
f3()

  在上面的例子中,每次循环,都创建了一个新的函数,然后,把创建的3个函数都返回了
  你可能认为调用f1(),f2()和f3()结果应该是1,4,9,但实际结果是都为9

9
9
9 

  返回的fs的列表为[f,f,f],当执行f1,f2,f3=count()的时候,f1=f,f2=f,f3=f,这个时候变量i的值已经变成了3,所以最后结果都为9

  原因就是当count()函数返回了3个函数时,这3个函数所引用的变量 i 的值已经变成了3。由于f1、f2、f3并没有被调用,所以,此时他们并未计算 i*i

  如果一定要引用循环变量怎么办,方法是在创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变:

def count():
    fs = []
    for i in range(1, 4):
        def f():
            return i*i
        fs.append(f())
    return fs f1, f2, f3 = count() 

  或

def count():
    def f(j):
        def g():
            return j*j
        return g
    fs = [] for i in range(1, 4): fs.append(f(i)) return fs f1, f2, f3 = count() f1() f2() f3()
1
4
9

  3.匿名函数

  当我们在传入函数时,有些时候,不需要显式地定义函数,直接传入匿名函数更方便
  在python中,对匿名函数提供了有限支持,还是以map()函数为例,计算f(x)=x2时,除了定义一个f(x)外,还可以传入匿名函数

list(map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
[1, 4, 9, 16, 25, 36, 49, 64, 81]

  通过对比可以看出,匿名函数lambda x:x * x实际上就是:

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

  关键字lambda表示匿名函数,冒号前面的x表示函数参数
  匿名函数有个限制,就是只能有一个表达式,不用写return,返回值就是该表达式的结果

  用匿名函数有个好处,因为函数没有名字,不必担心函数名冲突,此外,匿名函数也是一个函数对象,也可以把匿名函数赋值给一个变函数变量,再利用变量来调用该函数

f = lambda x:x*x
f(5)
25

  同样,也可以把匿名函数作为返回值返回,比如

def build(x,y):
    return lambda :x*x +y*y
build(1,5)
<function __main__.build.<locals>.<lambda>()>
def build(x,y):
    return lambda :x*x +y*y
build(1,5)()
26

  4.装饰器

  由于函数也是一个对象,而且函数对象可以赋值给变量,所以,通过变量也能调用该函数

def now():
    print('2015-3-25')
f = now
f() #或f = now() 
#2015-3-25

  函数对象有一个__name__属性,可以拿到函数的名字:

def now():
    print('2015-3-25')
f = now
f() #或f = now() 
now.__name__ 
f.__name__
now
now

  现在,假设我们要增强now()函数的功能,比如,在函数调用前后自动打印日志,但又不希望修改now()函数的定义,这种代码运行期间动态增加功能的方式,叫做‘装饰器(Decorator)
  本质上,decorator就是一个返回函数的高阶函数,所以,我们要定义一个能打印日志的decorator,可以定义如下

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

  观察上面的log,因为他是一个decorator,所以接受一个函数作为参数,我们要借助python的@语法,把decorator置于函数的定义处now

@log
def now():
    print('2015-3-25')

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

now()
call ^now():
2015-3-25

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

now = log(now)

  由于log()是一个decorator,返回一个函数,所以,原来的now()函数仍然存在,只是现在同名的now变量指向了新的函数,于是调用now()将执行新函数,即在log()函数中返回的wrapper()函数
#wrapper()函数的参数定义是(*args,**kw),因此,wrapper函数可以接受任意参数的调用,在wrapper()函数内,首先打印日志,再紧接着调用原始函数
#如果decorator本身需要传入参数,那就需要编写一个返回decorator的高阶函数,写出来会更复杂,比如,要自定义log的文本

#什么是装饰器?
#python装饰器(functional decorators)就是用于拓展原来函数功能的一种函数,目的是在不改变原函数名(或类名)的情况下,给函数增加新的功能
#这个函数的特殊之处在于它的返回值也是一个函数,这个函数是内嵌‘原’函数的函数
#一般而言,我们要想拓展原来函数代码,最直接的方法就是侵入代码里面修改,例如:
import time
def f():
print('hello')
time.sleep(1)#python time sleep()函数推迟调用线程的运行,可通过参数secs指秒数,表示进程挂起的时间
print('world')
f()#就是说先打印hello,过一秒打印world

#这是我们最原始的一个函数,然后我们试图记录下这个函数执行的总时间,最简单的做法就是改动原来的代码
import time 
def f():
start_time = time.time()
print('hello')
time.sleep(1)
print('world')
end_time = time.time()
execution_time = (end_time - start_time)*1000
print('time is %d ms' %execution_time)
f()

#但是实际工作中,有些时候核心代码并不可以直接去改,所以在不该动原代码的情况下,我们可以再定义一个函数(但是生效需要再次执行函数)
import time
def deoo(funo):
start_time = time.time()
f()
end_time = time.time()
execution_time = (end_time - start_time)*1000
print('time is %d ms' %execution_time)
def f():
print('hello')
time.sleep(1)
print('world')
if __name__ == '__main__':
deoo(f)
print('f.__name__is',f.__name__)
#一个python文件有两种使用方法,第一是直接作为脚本执行,第二是import到其他的python脚本中被调用(模块重用)执行。因此if__name__=='main':的作用是控制这两种情况执行代码的过程
#在if__name__ == 'main':下的代码只有在第一种情况下(即文件作为脚本直接执行)
#简而言之,__name__就是当前模块名,当模块被直接运行时候模块名称为__main__,当模块被直接执行的时候,代码被执行,当模块被导入的时候,代码不运行
#举个例子:在test.py中写入如下代码:
print('i am the first')
if __name__ == '__main__':
print('i am the second')
#即,if__name__=='__main__':语句之前和之后的代码都被执行
#然后在用一文件夹新建名称为import_test.py的脚本,只输入如下代码
import test
#执行import_test.py脚本,输出结果如下:
print('i am the first')
if __name__ == '__main__':
print('i am the second')#虽然此处的输出结果两个都输出了,但是如果是在创建了test.py的话,第二个是不会执行的

  5.偏函数
  python中的functools模块提供了很多有用的功能,其中一个就是偏函数(Partial function),要注意,这里的偏函数和数学意义上的偏函数不一样
  在介绍函数参数的时候,我们讲到,通过设定参数的默认值,可以降低函数调用的难度,而偏偏函数也可以做到这一点,举例如下:
  int()函数可以把字符串转换为整数,当仅传入字符串时,int()函数默认按十进制转换:

int('12345')

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

int('12345',base=8) #5349
int('12345',base=16) #74565

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

def int2(x,base=2):
    return int(x,base)
int2('1000000')
int2('1010101')

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

import functools
int2 = functools.partial(int,base=2)
int2('1010101')

  所以,简单总结functools.partial的作用就是,把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数就会更简单
  注意到上面的新的int2函数,仅仅是把base参数重写设定默认值为2,但也可以在函数调用时传入其他值

int2('1000000',base=10)

  最后,创建偏函数时,实际上可以接受函数对象,#args和**kw这3个函数,当传入:int2 = functools.partial(int,base=2),实际上固定了int()函数的关键字参数base,也就是:
int2('10000')
  相当于:

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

  当传入:

max2 = functools.partial(max,10)

  实际上会把10作为*args的一部分自动加到左边,也就是:max2(5,6,7)
  相当于:

args = (10,5,6,7)
max2(*args)

  总结:当函数的参数个数太多,需要简化时,使用functools.partial可以创建一个函数,这个新函数可以固定住原函数的部分参数,从而在调用时更简单

猜你喜欢

转载自www.cnblogs.com/blogbo/p/11747271.html
今日推荐