【Python基础教程】 第6章 抽象

             *     ,MMM8&&&.            *
                  MMMM88&&&&&    .
                 MMMM88&&&&&&&
     *           MMM88&&&&&&&&
                 MMM88&&&&&&&&
                 'MMM88&&&&&&'
                   'MMM8&&&'      *    
          |\___/|     /\___/\
          )     (     )    ~( .              '
         =\     /=   =\~    /=
           )===(       ) ~ (
          /     \     /     \
          |     |     ) ~   (
         /       \   /     ~ \
         \       /   \~     ~/
  jy__/\_/\__  _/_/\_/\__~__/_/\_/\_/\_/\__ww
  |  |  |  |( (  |  |  | ))  |  |  |  |  |  |
  |  |  |  | ) ) |  |  |//|  |  |  |  |  |  |
  |  |  |  |(_(  |  |  (( |  |  |  |  |  |  |     
  |  |  |  |  |  |  |  |\)|  |  |  |  |  |  |
  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |


如何将语句组合成函数,这让你能够告诉计算机如何完成任务,且只需说一次,无需反复向计算机传达详细指令。


6.1 懒惰是一种美德

定义函数,在需要时直接调用,这就是以抽象的方式完成功能的实现。比如:

print(sum_1_add_1)

函数“sum_1_add_1” 实现了计算 “1+1” 的计功能,通过函数对其进行抽象,可以增强程序的可视化和阅读性。


6.2 抽象和结构

比如,下载网页、计算使用频率、打印每个单词的使用频率,将上述步骤简单描述转换为一个Python程序为:

page = dowmload_page()
freqs = compute_frequencies(page)
for word,freq in freqs:
    pring("{} 的频率为 {}:".format(word,freq))

看到这些代码,任何人都知道这个程序是做什么的。至于这些操作的具体细节,将在独立的函数定义中给出。


6.3 自定义函数

函数执行特定的操作并返回一个值,你可以调用它(可能需要提供参数)。要判断每个对象是否可调用,可使用内置函数callable。

import math
x = 1
y = math.sqrt
print(callable(x))
print(callable(y))

[Out]:False
[Out]:True

使用 def 定义函数(斐波那契数列)

def fibs(num):
    result = [0,1]
    for i in range(num-2):
        result.append(resule[-2] + result[-1])
    return result

[In] :fibs(5)
[Out]:[0,1,1,2,3]

6.3.1 给函数编写文档

给函数编写文档,以确保其他人能够理解,可添加注释。也可在def语句后面添加独立的字符串作以注释。这个字符串称为文档字符串(docstring)

def square(x):
    '对输入值进行平方运算'
    return x*x

还可以像下面这样访问文档字符串:

[In]square.__doc__
[Out]:'对输入值进行平方运算'

特殊的内置函数 help,可以获取有关函数的信息,包括文档字符串

[In] :help(square)
[Out]:help on function square in module __main__:
      square(x)
          对输入值进行平方运算

6.3.2 其实并不是函数的函数

def test():
    print('This is printed')
    return
    print('This is not')

[In] :x = test()
[Out]:This is printed

上面的代码中,return 返回了一个空值,跳过了下面的print语句。

print(x)

[Out]:None #返回空值

6.4 参数魔法

6.4.1 值从哪里来

在 def 语句中,位于函数名后面的变量通常称为形参,而调用函数时提供的值称为实参,我们将实参称为

6.4.2 我能修改参数吗

在函数内部给参数复制对外部没有任何影响。

name = 'Ling Yu'
def try_to_change(n):
    name = 'Wei Wu'

[In] :try_to_change(name)
      name
[Out]:'Ling Yu'

在 try_to_change 内,将新值赋给了函数内部参数 name ,但是并没有对外部变量name造成影响。因为这是两个完全不同的变量。传递并修改参数的效果类似下面。

name = 'Ling Yu'#外部
name = name     #内部=外部
name = 'Wei Wu' #内部
name            #外部

[Out]:'Ling Yu'

结果显而易见,内部参数name变了,但外部变量name没变。同样,在函数内部重新关联参数(即给它赋值)时,函数外部的变量不受影响。这是因为参数存储在局部作用域内,即函数内部的参数,跳出函数后就不在作用。

字符串(以及数和元组)是不可变的。但如果参数为可变的数据结构(比如列表)呢?

def change(n):
    n[0] = 'Gouzi Er'
names = ['Wei Wu','Ling Yu']
change(names)
print(names)

[Out]:['Gouzi Er','Ling Yu']

在这个示例中,也在函数内部修改了参数。但是,在前一个示例中,只是给局部变量赋予了新值,并没有影响到外部变量;而在这里,修改了关联到的列表。可以不用函数,作如下表示。

names = ['Wei Wu','Ling Yu']
n = names = ['Wei Wu','Ling Yu']
n[0] = 'Gouzi Er'
n = names = ['Gouzi Er','Ling Yu']

因为内部参数 n 和外部变量 names 都指向同一个列表,函数 change 对列表进行了修改。若想避免这种情况,就需要创建列表的副本。操作为对整个列表进行切片,进而得到列表的副本。

names = ['Wei Wu','Ling Yu']
n = names[:]

现在n和names包含两个相等不同的列表。

n == names
[Out]:True

n is names
[Out]:False

现在,我们像在函数中一样修改参数n,将不会影响外部变量names:

names = ['Wei Wu','Ling Yu']
n = names[:] = ['Wei Wu','Ling Yu']
n[0] = 'Gouzi Er'
n = ['Gouzi Er','Ling Yu']
names = ['Wei Wu','Ling Yu'] 

我们尝试结合使用这种技巧和函数change

change(names[:])
names
[Out]:['Wei Wu','Ling Yu']

1 为何要修改参数?

在提高程序的抽象程度方面,使用函数来修改数据结构是一种不错的方式。加入你要编写一个程序,让它存储姓名,并让用户能够根据名字、中间名或姓找人。为此,可以使用一个类似于下面的数据结构。

storage = {}
storage['first'] = {}
storage['middle'] = {}
storage['last'] = {}

# Like: storage['last']['Wu'] = 'Wei Wu'

数据结构 storage 是一个字典,包含“first”、“middle”和“last”三个键,值为子字典,用于存储完整姓名。可以如下实现我们上述的功能。

#初始化数据结构
def init(data):
    data['first'] = {}
    data['middle'] = {}
    data['last'] = {}
    return data

# 根据键和名字字段查找 name
def lookup(data,key,part_name):
    return data[key].get(part_name) # 返回字段对应的full_name列表

# 编写将人员存储在到数据结构中的函数
def store(data, full_name):
    names = full_name.split() #英文名,例如 Wei Wu,以空格划分字段
    if len(names) == 2:
        names.insert(1,' ') # 两个字段的名字,middle字段以空格填充
    keys = 'first','middle','last'

    for key , part_name in zip(keys,names):
        people = lookup(data, key, part_name)
        # 已有该字段,将 full_name 加入该字段下的列表中
        if people:
            people.append(full_name)
            # 等同 data[key][part_name].append(full_name)
        # 若该字段下为空,将该 full_name 加入列表
        else:
            data[key][part_name] = [full_name]

测试:

myname = {}
init(myname)
store(myname,'Wei Wu')
store(myname,'Gou Zi Wu')
lookup(myname,'last','Wu')

[Out]:['Wei Wu', 'Gou Zi Wu']

其中,

people = lookup(data,label,name)
if people:
    people.append(full_name)

people 指向 name 下的列表,因此对people append元素,即是对data[laebl][name]下 append 元素。

2 如果参数时不可变的

在 Python 中,只能修改参数对象本身,而无法给参数赋值并让这种修改影响函数外部的变量(C++,Pascal和Ada),但如果参数不可变,应从函数返回所有需要的值。比如:

# 编写将变量的值加1的函数
def inc(x):
    return x + 1

#
def inc(x):
    x[0] = x[0] + 1

清晰的解决方案是返回修改后的值

6.4.3 关键字参数和默认值

前面使用的参数都是位置参数,比如:

def hello(b,a):
    print('{},{}!'.formt(a,b))

[In] :hello('hello','word')
[Out]:word hello!

# 顺序搞错了,应该按照下面的顺序输入参数

[In] :hello('word','hello')
[Out]:hello word!

我们发现,如果不为参数指定名字,单凭借参数在代码中的为位置去输入参数,将会带来相当大的麻烦。因此,有如下方式:

def hello(a = 'hello', b = 'word'):
    print('{},{}!'.format(a,b))

在这里,参数的顺序无关紧要,只要给对应的参数赋值就可以了。这些使用名称制定的参数称为关键字参数。当函数中参数很多时,可以使每个参数的作用清晰明了。同时还可以指定参数的默认值。比如上面的 a 的默认值就是‘hello’。

6.4.4 收集参数

def print_params(*params):
    print(params)

[In] :print_params(1,2,3)
[Out]:(1,2,3)

我们发现,参数params前面的星号将提供的所有值都放在一个元组中,也就是将这些值收集起来。

但星号不会收集关键字参数。比如:

def print_params(*y):
    print(y)

[In] :print_params(x=7)
[Out]:报错

上面之所以会报错,是因为没有收集到关键字参数。要收集关键字参数,可以使用两个星号。

def print_params(**y):
    pring(y)

[In] :print_params(x=1,y=2)
[Out]:{'x':1,'y':2}

得到了一个字典,而不是元组

结合收集参数,我们可以对前面姓名存储的函数 store 进行改变

#原代码
# 编写将人员存储在到数据结构中的函数
def store(data, full_name):
    names = full_name.split() #英文名,例如 Wei Wu,以空格划分字段
    if len(names) == 2:
        names.insert(1,' ') # 两个字段的名字,middle字段以空格填充
    keys = 'first','middle','last'

    for key , part_name in zip(keys,names):
        people = lookup(data, key, part_name)
        # 已有该字段,将 full_name 加入该字段下的列表中
        if people:
            people.append(full_name)
            # 等同 data[key][part_name].append(full_name)
        # 若该字段下为空,将该 full_name 加入列表
        else:
            data[key][part_name] = [full_name]


# 修改后的代码
def store(data, *full_name):
    for full_name in full_names:
        names = full_name.split() #英文名,例如 Wei Wu,以空格划分字段
        if len(names) == 2:
            names.insert(1,' ') # 两个字段的名字,middle字段以空格填充
        keys = 'first','middle','last'

        for key , part_name in zip(keys,names):
            people = lookup(data, key, part_name)
            # 已有该字段,将 full_name 加入该字段下的列表中
            if people:
                people.append(full_name)
                # 等同 data[key][part_name].append(full_name)
            # 若该字段下为空,将该 full_name 加入列表
            else:
                data[key][part_name] = [full_name]

可以输入多个full_name参数,不用像前面一样调用多次函数。

6.4.5 分配参数

# 分配参数-元组
def add(x,y):
    pring(x+y)
params = (1,2)

[In] :add(*params)
[Out]:3


# 分配参数-字典
def hello(a = 'hello',b = 'word'):
    print('{},{}!'.format(a,b))
params = {'a':Love you,'b':Ling yu}

[In] :hello(**params)
[Out]:Love you,Ling yu!

在编程中,尽量不用这种方式!会挨打的!!!


6.4.6 练习使用参数

def story(**kwds):
    return 'Once upon a time,there was a {job} called {name}.'.format_map(kwds)

#[In] :story(job = '职业',name = '程序猿')
#[Out]:'Once upon a time,there was a 职业 called 程序猿.'

#[In] :params = {'job':'汪','name':'单身狗'}
       story(**params)
#[Out]:'Once upon a time,there was a 汪 called 单身狗.'    


def power(x,y,*others):
    if others:
        print('Received redundant parameters:',others)
    return pow(x,y)

#[In] :power(2,3)
#[Out]:8

#[In] :power(2,2,'hello word!')
#[Out]:Received redundant parameters: ('hello word!',)
       4


def interval(start, stop = None, step = 1):
    'Imitates range() for step > 0'
    if stop is None:
        start,stop = 0, start
    result = []

    i = start
    while i < stop:
        result.append(i)
        i += step
    return result

#[In] :interval(5)
#[Out]:[0,1,2,3,4]

#[In] :power(*interval(5))
#[Out]:Received redundant parameters: (2,3,4)
       0

6.5 作用域

变量到底是什么?可以将其视为指向值的名称当执行赋值语句 x=1 后,名称 x 指向值 1。这与使用字典时一样(键指向值)。有一个名为 vars 的内置函数,它返回了一个看不见的字典。

In [31]: x = 1
In [32]: scope = vars()
In [33]: scope['x']
Out[33]: 1

警告:不应修改 vars 返回的字典,根据官方文档的说法,这样做的结果时不确定的。


这种“看不见的字典”称为命名空间作用域

# 局部变量
In [1]: def foo():x=42
In [2]: x=1
In [3]: foo()
In [4]: x
Out[1]: 1

局部变量不会影响全局变量。因此,局部变量可以与全局变量重名。

可以在函数中访问全局变量,但是不建议使用,因为容易导致 BUG。示意如下:

# 在函数中访问全局变量
In [1]: def test(a):
    ...:     print(a + b)
In [2]: b = ' Oh!'
In [3]: test('Wow')
Out[1]: Wow Oh!

# 若函数内有一个同全部变量同名的局部变量,出现遮盖问题,无法直接调用。
In [1]: a = ' Oh!'
In [2]: def test(a):
    ...:     print(a + globals()['a'])
In [3]: test('Wow')
Out[1]: Wow Oh!

重新关联全局变量,告诉函数这个变量是全局变量,.

# 通过函数修改全局变量
In [51]: x = 1
In [52]: def change_global():
    ...:     global x
    ...:     x += 1
In [53]: change_global()
In [54]: x
Out[54]: 2

作用域嵌套

# 赋值顺序从外向里
In [57]: def mutiplier(factor):
    ...:     def mutiplier_by_factor(number):
    ...:         print('{} * {} = {}'.format(number,factor, number*factor))
    ...:     return mutiplier_by_factor

In [58]: mutiplier(4)(6)
6 * 4 = 24

6.6 递归

什么是递归?如下解释。

递归[名词]:参见“递归”

def 递归:
    return 递归

6.6.1 两个经典案例:阶乘和幂


# 阶乘 n个人排成一队有多少种方式,# n * n-1 * n-2 * ... * 1

#普通方法
In [61]: def factorial(n):
    ...:     result = n
    ...:     for i in range(n-1,0,-1):
    ...:         result *= i
    ...:     return result
In [68]: factorial(5)
Out[68]: 120

# 用递归的方法
# 如果n==1,那么直接返回1
# 如果n>1,那么阶乘为n-1的阶乘*n
In [67]: def factorial(n):
    ...:     if n == 1:
    ...:         return 1
    ...:     else:
    ...:         return n * factorial(n-1)

In [68]: factorial(5)
Out[68]: 120
# 幂

#普通方法
In [69]: def power(x,n):
    ...:     result = 1
    ...:     for i in range(n):
    ...:         result *= x
    ...:     return result

In [70]: power(2,3)
Out[70]: 8


# 递归方法
# 如果n==0,那么直接返回1
# 如果n>1,那么为power(x,n-1)与x的乘积
In [71]: def power(x,n):
    ...:     if n == 0:
    ...:         return 1
    ...:     else:
    ...:         return x*power(x,n-1)

In [72]: power(2,3)
Out[72]: 8

6.6.2 另一个经典案例:二分查找

比如,给定[1,1000]范围擦价格:
(1)是500吗?
(2)不是 → 比500大还是小?
(3)小 → 是 250 吗?
(4) ……

我们用这种思想来从序列中查找目标值位置。

有序列 sequence = [1,3,6,7,15,21,30],目标值为21,位置索引为 5
(1)上下限为sequence长度和0
(2)用上面的思想找到 21 的位置

In [81]: def search(squence,number,lower=0,upper=None):
    ...:     if upper == None:
    ...:         upper = len(squence) - 1
    ...:     if lower == upper:
    ...:         assert number == squence[upper]
    ...:         return number
    ...:     else:
    ...:         middle = (lower + upper) // 2
    ...:         if number > squence[middle]:
    ...:             return search(squence,number,middle+1,upper)
    ...:         elif number < squence[middle]:
    ...:             return search(ssquence,number,lower,middle)
    ...:         else: return middle

In [82]: a = [1,3,6,7,15,21,30]

In [83]: search(a,21)
Out[83]: 5

函数式编程

#(1) map(function,list),将list传递给函数 function,返回函数结果
list(map(str,range(10)))
等价于
[str(i) for i in range(10)]



#(2) filter(function,list),将list传递给函数function,根据布尔返回值对list中元素进行过滤
# Python isalnum() 方法检测字符串是否由字母和数字组成
In [94]: def func(x):
    ...:     return x.isalnum()

In [95]: seq = ['foo','x41','>!','$&*']

In [96]: list(filter(func,seq))
Out[96]: ['foo', 'x41']

#用列表推导替换
In [97]: [x for x in seq if x.isalnum()]
Out[97]: ['foo', 'x41']


#(3) reduce()传入的函数 f 必须接收两个参数,reduce()对list的每个元素反复调用函数f。将每次函数结果当作第一个参数,list下一个元素作第二个参数,并返回最终结果值。若接收3个参数,则第三个参数为初始值,此时只调用list中的1个元素。

# (1+2)+3
In [104]: from functools import reduce
In [105]: numbers = [1,2,3]
In [106]: reduce(lambda x,y : x+y, numbers)
Out[106]: 6

#若有第三个参数为 100, 则计算为: ( (100+1)+2 )+3
In [107]: reduce(lambda x,y : x+y, numbers,100)
Out[107]: 106

猜你喜欢

转载自blog.csdn.net/weixin_37392582/article/details/80327854