Python3 学习 第六章 抽象 ---20230726

#!usr/bin/env python  
# -*- coding:utf-8 _*-
""" 
@author:eivision 
@file: lecture6.py 
@time: 2023/07/22 
"""
# 第六章 抽 象
# 本章介绍如何将语句组合成函数,这让你能够告诉计算机如何完成任务,且只需说一次,无
# 需反复向计算机传达详细指令。本章详细介绍参数和作用域,还将讨论递归是什么及其在程序中的用途。

# 6.1 懒惰是一种美德
# 前面编写的程序都很小,但如果要编写大型程序,你很快就会遇到麻烦。想想看,如果你在
# 一个地方编写了一些代码,但需要在另一个地方再次使用,该如何办呢?例如,假设你编写了一
# 段代码,它计算一些斐波那契数(一种数列,其中每个数都是前两个数的和)。
# fibs = [0, 1]
# for i in range(8):
#  fibs.append(fibs[-2] + fibs[-1])
# 运行上述代码后,fibs将包含前10个斐波那契数。
# >>> fibs
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

# 如果你想一次计算前10个斐波那契数,上述代码刚好能满足需求。你甚至可以修改前述for循环,使其处理动态的范围,即让用户指定最终要得到的序列的长度。
# fibs = [0, 1]
# num = int(input('How many Fibonacci numbers do you want? '))
# for i in range(num-2):
#     fibs.append(fibs[-2] + fibs[-1])
# print(fibs)


# 如果要使用这些数字做其他事情,该如何办呢?当然,你可以在需要时再次编写这个循环,
# 但如果已编写好的代码更复杂呢(如下载一组网页并计算所有单词的使用频率)?在这种情况下,
# 你还愿意多次编写这些代码吗(每当需要时就编写一次)?不,真正的程序员是不会这样做的。
# 真正的程序员很懒。这里说的懒不是贬义词,而是说不做无谓的工作。
# 那么真正的程序员会如何做呢?让程序更抽象。要让前面的程序更抽象,可以像下面这样做:
# num = input('How many numbers do you want? ')
# print(fibs(num))

# 定义斐波那契数列函数并调用
# num = int(input('How many numbers do you want? '))
# def fibs(num):
#     fibs = [0, 1]
#     # num = int(input('How many numbers do you want? '))
#     for i in range(num - 2):
#         fibs.append(fibs[-2] + fibs[-1])
#     print(fibs)
#
# print(fibs(num))


# 在这里,只具体地编写了这个程序独特的部分(读取数字并打印结果)。实际上,斐波那契
# 数的计算是以抽象的方式完成的:你只是让计算机这样做,而没有具体地告诉它如何做。你创建
# 了一个名为fibs的函数,并在需要计算斐波那契数时调用它。如果需要在多个地方计算斐波那契数,这样做可节省很多精力。

# 6.2 抽象和结构
# 抽象可节省人力,但实际上还有个更重要的优点:抽象是程序能够被人理解的关键所在(无
# 论对编写程序还是阅读程序来说,这都至关重要)。

# 组织计算机程序时,你也采取类似的方式。程序应非常抽象,如下载网页、计算使用频率、
# 打印每个单词的使用频率。这很容易理解。下面就将前述简单描述转换为一个Python程序。
# page = download_page()
# freqs = compute_frequencies(page)
# for word, freq in freqs:
#  print(word, freq)
# 看到这些代码,任何人都知道这个程序是做什么的。然而,至于具体该如何做,你未置一词。
# 你只是让计算机去下载网页并计算使用频率,至于这些操作的具体细节,将在其他地方(独立的函数定义)中给出。

# 6.3 自定义函数
# 函数执行特定的操作并返回一个值,你可以调用它(调用时可能需要提供一些参数——放
# 在圆括号中的内容)。一般而言,要判断某个对象是否可调用,可使用内置函数callable。
# >>> import math
# >>> x = 1
# >>> y = math.sqrt
# >>> callable(x)
# False
# >>> callable(y)
# True

# 前一节说过,函数是结构化编程的核心。那么如何定义函数呢?使用def(表示定义函数)语句。

# def hello(name):
#     return 'Hello, ' + name + '!'
# print(hello('Alice'))

# 运行这些代码后,将有一个名为hello的新函数。它返回一个字符串,其中包含向唯一参数
# 指定的人发出的问候语。你可像使用内置函数那样使用这个函数。
# >>> print(hello('world'))
# Hello, world!
# >>> print(hello('Gumby'))
# Hello, Gumby!

# 很不错吧?如果编写一个函数,返回一个由斐波那契数组成的列表呢?很容易!只需使用前
# 面介绍的代码,但不从用户那里读取数字,而是通过参数来获取。
# def fibs(num):
#     result = [0, 1]
#     for i in range(num-2):
#         result.append(result[-2] + result[-1])
#     return result
# 执行这些代码后,解释器就知道如何计算斐波那契数了。现在你不用再关心这些细节,而只需调用函数fibs。
# print(fibs(10))
# >>> fibs(10)
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
# >>> fibs(15)
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377]
# 在这个示例中,num和result也可以使用其他名字,但return语句非常重要。return语句用于
# 从函数返回值(在前面的hello函数中,return语句的作用也是一样的)。

# 6.3.1 给函数编写文档
# 要给函数编写文档,以确保其他人能够理解,可添加注释(以#打头的内容)。还有另一种
# 编写注释的方式,就是添加独立的字符串。在有些地方,如def语句后面(以及模块和类的开
# 头,这将在第7章和第10章详细介绍),添加这样的字符串很有用。放在函数开头的字符串称为
# 文档字符串(docstring),将作为函数的一部分存储起来。下面的代码演示了如何给函数添加文档字符串:

# def square(x):
#  'Calculates the square of the number x.'
#  return x * x
# 可以像下面这样访问文档字符串:
# >>> square.__doc__
# 'Calculates the square of the number x.'

# 注意 __doc__是函数的一个属性。属性将在第7章详细介绍。属性名中的双下划线表示这是一
# 个特殊的属性。特殊(“魔法”)属性将在第9章讨论。

# 特殊的内置函数help很有用。在交互式解释器中,可使用它获取有关函数的信息,其中包含
# 函数的文档字符串。
# >>> help(square)
# Help on function square in module __main__:
# square(x)
# Calculates the square of the number x.
# 在第10章,你还会遇到函数help。

# 6.3.2 其实并不是函数的函数
# 数学意义上的函数总是返回根据参数计算得到的结果。在Python中,有些函数什么都不返回。
# 在诸如Pascal等的语言中,这样的函数可能另有其名(如过程),但在Python中,函数就是函数,
# 即使它严格来说并非函数。什么都不返回的函数不包含return语句,或者包含return语句,但没
# 有在return后面指定值。

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

# 这里使用return语句只是为了结束函数。
# >>> x = test()
# This is printed

# 如你所见,跳过了第二条print语句。(这有点像在循环中使用break,但跳出的是函数。)既
# 然test什么都不返回,那么x指向的是什么呢?下面就来看看:
# >>> x
# >>>
# 什么都没有。再仔细地看看。
# >>> print(x)
# None
# 这是一个你熟悉的值:None。由此可知,所有的函数都返回值。如果你没有告诉它们该返回什么,将返回None。

# 警告 不要让这种默认行为带来麻烦。如果你在if之类的语句中返回值,务必确保其他分支也
# 返回值,以免在调用者期望函数返回一个序列时(举个例子),不小心返回了None。

# 6.4 参数魔法
# 函数使用起来很简单,创建起来也不那么复杂,但要习惯参数的工作原理就不那么容易了。 先从简单的着手。
# 6.4.1 值从哪里来
# 定义函数时,你可能心存疑虑:参数的值是怎么来的呢?
# 通常,你不用为此操心。编写函数旨在为当前程序(甚至其他程序)提供服务,你的职责是
# 确保它在提供的参数正确时完成任务,并在参数不对时以显而易见的方式失败。(为此,通常使
# 用断言或异常。异常将在第8章详细介绍。)

# 注意 在def语句中,位于函数名后面的变量通常称为形参,而调用函数时提供的值称为实参,
# 但本书基本不对此做严格的区分。在很重要的情况下,我会将实参称为值,以便将其与
# 类似于变量的形参区分开来。

# 6.4.2 我能修改参数吗
# 函数通过参数获得了一系列的值,你能对其进行修改吗?如果这样做,结果将如何?参数不
# 过是变量而已,行为与你预期的完全相同。在函数内部给参数赋值对外部没有任何影响

# >>> def try_to_change(n):
# ... n = 'Mr. Gumby'
# ...
# >>> name = 'Mrs. Entity'
# >>> try_to_change(name)
# >>> name
# 'Mrs. Entity'

# 在try_to_change内,将新值赋给了参数n,但如你所见,这对变量name没有影响。说到底,
# 这是一个完全不同的变量。传递并修改参数的效果类似于下面这样:
# >>> name = 'Mrs. Entity'
# >>> n = name # 与传递参数的效果几乎相同
# >>> n = 'Mr. Gumby' # 这是在函数内进行的
# >>> name
# 'Mrs. Entity

# 这里的结果显而易见:变量n变了,但变量name没变。同样,在函数内部重新关联参数(即
# 给它赋值)时,函数外部的变量不受影响
# 注意 参数存储在局部作用域内。作用域将在本章稍后讨论。

# 字符串(以及数和元组)是不可变的(immutable),这意味着你不能修改它们(即只能替换
# 为新值)。因此这些类型作为参数没什么可说的。但如果参数为可变的数据结构(如列表)呢?
# >>> def change(n):
# ... n[0] = 'Mr. Gumby'
# ...
# >>> names = ['Mrs. Entity', 'Mrs. Thing']
# >>> change(names)
# >>> names
# ['Mr. Gumby', 'Mrs. Thing']
# 在这个示例中,也在函数内修改了参数,但这个示例与前一个示例之间存在一个重要的不同。
# 在前一个示例中,只是给局部变量赋了新值,而在这个示例中,修改了变量关联到的列表。这很
# 奇怪吧?其实不那么奇怪。下面再这样做一次,但这次不使用函数调用。
# >>> names = ['Mrs. Entity', 'Mrs. Thing']
# >>> n = names # 再次假装传递名字作为参数
# >>> n[0] = 'Mr. Gumby' # 修改列表
# >>> names
# ['Mr. Gumby', 'Mrs. Thing']
# 这样的情况你早就见过。将同一个列表赋给两个变量时,这两个变量将同时指向这个列表。
# 就这么简单。要避免这样的结果,必须创建列表的副本。对序列执行切片操作时,返回的切片都
# 是副本。因此,如果你创建覆盖整个列表的切片,得到的将是列表的副本。

# >>> names = ['Mrs. Entity', 'Mrs. Thing']
# >>> n = names[:]
# 现在n和names包含两个相等但不同的列表。
# >>> n is names
# False
# >>> n == names
# True

# 现在如果(像在函数change中那样)修改n,将不会影响names。
# >>> n[0] = 'Mr. Gumby'
# >>> n
# ['Mr. Gumby', 'Mrs. Thing']
# >>> names
# ['Mrs. Entity', 'Mrs. Thing']

# 下面来尝试结合使用这种技巧和函数change。
# >>> change(names[:])
# >>> names
# ['Mrs. Entity', 'Mrs. Thing']
# 注意到参数n包含的是副本,因此原始列表是安全的。

# 1. 为何要修改参数
# 在提高程序的抽象程度方面,使用函数来修改数据结构(如列表或字典)是一种不错的方式。
# 假设你要编写一个程序,让它存储姓名,并让用户能够根据名字、中间名或姓找人。为此,你可
# 能使用一个类似于下面的数据结构:
# storage = {}
# storage['first'] = {}
# storage['middle'] = {}
# storage['last'] = {}
# 数据结构storage是一个字典,包含3个键:'first'、'middle'和'last'。在每个键下都存储
# 了一个字典。这些子字典的键为姓名(名字、中间名或姓),而值为人员列表。例如,要将作者
# 加入这个数据结构中,可以像下面这样做:
# >>> me = 'Magnus Lie Hetland'
# >>> storage['first']['Magnus'] = [me]
# >>> storage['middle']['Lie'] = [me]
# >>> storage['last']['Hetland'] = [me]

# 每个键下都存储了一个人员列表。在这个例子里,这些列表只包含作者。
# 现在,要获取中间名为Lie的人员名单,可像下面这样做:
# >>> storage['middle']['Lie']
# ['Magnus Lie Hetland']

# 如你所见,将人员添加到这个数据结构中有点繁琐,在多个人的名字、中间名或姓相同时尤
# 其如此,因为在这种情况下需要对存储在名字、中间名或姓下的列表进行扩展。下面来添加我的
# 妹妹,并假设我们不知道数据库中存储了什么内容。
# >>> my_sister = 'Anne Lie Hetland'
# >>> storage['first'].setdefault('Anne', []).append(my_sister)
# >>> storage['middle'].setdefault('Lie', []).append(my_sister)
# >>> storage['last'].setdefault('Hetland', []).append(my_sister)
# >>> storage['first']['Anne']
# ['Anne Lie Hetland']
# >>> storage['middle']['Lie']
# ['Magnus Lie Hetland', 'Anne Lie Hetland']
# 可以想见,编写充斥着这种更新的大型程序时,代码将很快变得混乱不堪。

# 抽象的关键在于隐藏所有的更新细节,为此可使用函数。下面首先来创建一个初始化数据结构的函数。
# def init(data):
#  data['first'] = {}
#  data['middle'] = {}
#  data['last'] = {}
# 这里只是将初始化语句移到了一个函数中。你可像下面这样使用这个函数:
# >>> storage = {}
# >>> init(storage)
# >>> storage
# {'middle': {}, 'last': {}, 'first': {}}
# 如你所见,这个函数承担了初始化职责,让代码的可读性高了很多。

# 注意 在字典中,键的排列顺序是不固定的,因此打印字典时,每次的顺序都可能不同。如果
# 你在解释器中打印出来的顺序不同,请不用担心。

# 下面先来编写获取人员姓名的函数,再接着编写存储人员姓名的函数。
# def lookup(data, label, name):
#  return data[label].get(name)

# 函数lookup接受参数label(如'middle')和name(如'Lie'),并返回一个由全名组成的列表。
# 换而言之,如果已经存储了作者的姓名,就可以像下面这样做:
# >>> lookup(storage, 'middle', 'Lie')
# ['Magnus Lie Hetland']
# 请注意,返回的是存储在数据结构中的列表。因此如果对返回的列表进行修改,将影响数据
# 结构。(未找到任何人时除外,因为在这种情况下返回的是None。)

# 下面来编写将人员存储到数据结构中的函数。(如果不能马上看懂这个函数,也不用担心。)
#
# def lookup(data, label, name):
#     return data[label].get(name)
#
# def store(data, full_name):
#     names = full_name.split()
#     if len(names) == 2:
#         names.insert(1, '')
#     labels = 'first', 'middle', 'last'
#     for label, name in zip(labels, names):
#          people = lookup(data, label, name)
#          if people:
#              people.append(full_name)
#          else:
#              data[label][name] = [full_name]
# 函数store执行如下步骤。
# (1) 将参数data和full_name提供给这个函数。这些参数被设置为从外部获得的值。
# (2) 通过拆分full_name创建一个名为names的列表。
# (3) 如果names的长度为2(只有名字和姓),就将中间名设置为空字符串。
# (4) 将'first'、'middle'和'last'存储在元组labels中(也可使用列表,这里使用元组只是为
# 了省略方括号)。
# (5) 使用函数zip将标签和对应的名字合并,以便对每个标签名字对执行如下操作:
#  获取属于该标签和名字的列表;
#  将full_name附加到该列表末尾或插入一个新列表。

# 下面来尝试运行该程序:
# >>> MyNames = {}
# >>> init(MyNames)
# >>> store(MyNames, 'Magnus Lie Hetland')
# >>> lookup(MyNames, 'middle', 'Lie')
# ['Magnus Lie Hetland']
# 注意 这种程序非常适合使用面向对象编程,这将在下一章介绍。

# 2. 如果参数是不可变的
# 在有些语言(如C++、Pascal和Ada)中,经常需要给参数赋值并让这种修改影响函数外部的变
# 量。在Python中,没法直接这样做,只能修改参数对象本身。但如果参数是不可变的(如数)呢?
# 不好意思,没办法。在这种情况下,应从函数返回所有需要的值(如果需要返回多个值,就
# 以元组的方式返回它们)。例如,可以像下面这样编写将变量的值加1的函数:
# >>> def inc(x): return x + 1
# ...
# >>> foo = 10
# >>> foo = inc(foo)
# >>> foo
# 11

# 如果一定要修改参数,可玩点花样,比如将值放在列表中,如下所示:
# >>> def inc(x): x[0] = x[0] + 1
# ...
# >>> foo = [10]
# >>> inc(foo)
# >>> foo
# [11]
# 但更清晰的解决方案是返回修改后的值。

# 6.4.3 关键字参数和默认值
# 前面使用的参数都是位置参数,因为它们的位置至关重要——事实上比名称还重要。本节介
# 绍的技巧让你能够完全忽略位置。要熟悉这种技巧需要一段时间,但随着程序规模的增大,你很快就会发现它很有用。

# 请看下面两个函数:
# def hello_1(greeting, name):
#  print('{}, {}!'.format(greeting, name))
# def hello_2(name, greeting):
#  print('{}, {}!'.format(name, greeting))

# 这两个函数的功能完全相同,只是参数的排列顺序相反。
# >>> hello_1('Hello', 'world')
# Hello, world!
# >>> hello_2('Hello', 'world')
# Hello, world!

# 有时候,参数的排列顺序可能难以记住,尤其是参数很多时。为了简化调用工作,可指定参数的名称。
# >>> hello_1(greeting='Hello', name='world')
# Hello, world!
# 在这里,参数的顺序无关紧要。
# >>> hello_1(name='world', greeting='Hello')
# Hello, world!
# 不过名称很重要(你可能猜到了)。
# >>> hello_2(greeting='Hello', name='world')
# world, Hello!

# 像这样使用名称指定的参数称为关键字参数,主要优点是有助于澄清各个参数的作用。这样,
# 函数调用不再像下面这样怪异而神秘:
# >>> store('Mr. Brainsample', 10, 20, 13, 5)
# 可以像下面这样做:
# >>> store(patient='Mr. Brainsample', hour=10, minute=20, day=13, month=5)

# 虽然这样做的输入量多些,但每个参数的作用清晰明了。另外,参数的顺序错了也没关系。
# 然而,关键字参数最大的优点在于,可以指定默认值。
# def hello_3(greeting='Hello', name='world'):
#  print('{}, {}!'.format(greeting, name))
# 像这样给参数指定默认值后,调用函数时可不提供它!可以根据需要,一个参数值也不提供、
# 提供部分参数值或提供全部参数值。
# >>> hello_3()
# Hello, world!
# >>> hello_3('Greetings')
# Greetings, world!
# >>> hello_3('Greetings', 'universe')
# Greetings, universe!

# 如你所见,仅使用位置参数就很好,只不过如果要提供参数name,必须同时提供参数
# greeting。如果只想提供参数name,并让参数greeting使用默认值呢?相信你已猜到该怎么做了。
# >>> hello_3(name='Gumby')
# Hello, Gumby!
# 很巧妙吧?还不止这些。你可结合使用位置参数和关键字参数,但必须先指定所有的位置参
# 数,否则解释器将不知道它们是哪个参数(即不知道参数对应的位置)。

# 例如,函数hello可能要求必须指定姓名,而问候语和标点是可选的。
# def hello_4(name, greeting='Hello', punctuation='!'):
#  print('{}, {}{}'.format(greeting, name, punctuation))
# 调用这个函数的方式很多,下面是其中的一些:
# >>> hello_4('Mars')
# Hello, Mars!
# >>> hello_4('Mars', 'Howdy')
# Howdy, Mars!
# >>> hello_4('Mars', 'Howdy', '...')
# Howdy, Mars...
# >>> hello_4('Mars', punctuation='.')
# Hello, Mars.
# >>> hello_4('Mars', greeting='Top of the morning to ya')
# Top of the morning to ya, Mars!
# >>> hello_4()
# Traceback (most recent call last):
#  File "<stdin>", line 1, in <module>
# TypeError: hello_4() missing 1 required positional argument: 'name'

# 6.4.4 收集参数(一个星号* : 接受多个参数值;两个星号**:收集关键字参数)
# 有时候,允许用户提供任意数量的参数很有用。例如,在本章前面的姓名存储示例中(参见
# 6.4.2节),每次只能存储一个姓名。如果能够像下面这样同时存储多个姓名就好了:
# >>> store(data, name1, name2, name3)

# 为此,应允许用户提供任意数量的姓名。实际上,这实现起来并不难。
# 请尝试使用下面这样的函数定义:
# def print_params(*params):
#  print(params)
# 这里好像只指定了一个参数,但它前面有一个星号。这是什么意思呢?尝试使用一个参数来
# 调用这个函数,看看结果如何。

# >>> print_params('Testing')
# ('Testing',)
# 注意到打印的是一个元组,因为里面有一个逗号。这么说,前面有星号的参数将被放在元组
# 中?复数params应该提供了线索。
# >>> print_params(1, 2, 3)
# (1, 2, 3)

# 参数前面的星号将提供的所有值都放在一个元组中,也就是将这些值收集起来。这样的行为
# 我们在5.2.1节见过:赋值时带星号的变量收集多余的值。它收集的是列表而不是元组中多余的值,
# 但除此之外,这两种用法很像。下面再来编写一个函数:

# def print_params_2(title, *params):
#  print(title)
#  print(params)
# 并尝试调用它:
# >>> print_params_2('Params:', 1, 2, 3)
# Params:
# (1, 2, 3)

# 因此星号意味着收集余下的位置参数。如果没有可供收集的参数,params将是一个空元组。
# >>> print_params_2('Nothing:')
# Nothing:
# ()
# 与赋值时一样,带星号的参数也可放在其他位置(而不是最后),但不同的是,在这种情况
# 下你需要做些额外的工作:使用名称来指定后续参数。
# >>> def in_the_middle(x, *y, z):
# ... print(x, y, z)
# ...
# >>> in_the_middle(1, 2, 3, 4, 5, z=7)
# 1 (2, 3, 4, 5) 7
# >>> in_the_middle(1, 2, 3, 4, 5, 7)
# Traceback (most recent call last):
#  File "<stdin>", line 1, in <module>
# TypeError: in_the_middle() missing 1 required keyword-only argument: 'z'

# 星号不会收集关键字参数。
# >>> print_params_2('Hmm...', something=42)
# Traceback (most recent call last):
#  File "<stdin>", line 1, in <module>
# TypeError: print_params_2() got an unexpected keyword argument 'something'
# 要收集关键字参数,可使用两个星号。
# >>> def print_params_3(**params):
# ... print(params)
# ...
# >>> print_params_3(x=1, y=2, z=3)
# {'z': 3, 'x': 1, 'y': 2}
# 如你所见,这样得到的是一个字典而不是元组。可结合使用这些技术。

# def print_params_4(x, y, z=3, *pospar, **keypar):
#  print(x, y, z)
#  print(pospar)
#  print(keypar)
# 其效果与预期的相同。
# >>> print_params_4(1, 2, 3, 5, 6, 7, foo=1, bar=2)
# 1 2 3
# (5, 6, 7)
# {'foo': 1, 'bar': 2}
# >>> print_params_4(1, 2)
# 1 2 3
# ()
# {}

# 通过结合使用这些技术,可做的事情很多。如果你想知道结合方式的工作原理(或是否可以
# 这样结合),动手试一试即可!在下一节你将看到,不管在函数定义中是否使用了*和**,都可在
# 函数调用中使用它们

# 现在回到最初的问题:如何在姓名存储示例中使用这种技术?解决方案如下:
# def store(data, *full_names):
#  for full_name in full_names:
#  names = full_name.split()
#  if len(names) == 2: names.insert(1, '')
#  labels = 'first', 'middle', 'last'
#  for label, name in zip(labels, names):
#  people = lookup(data, label, name)
#  if people:
#  people.append(full_name)
#  else:
#  data[label][name] = [full_name]
# 这个函数调用起来与只接受一个姓名的前一版一样容易。
# >>> d = {}
# >>> init(d)
# >>> store(d, 'Han Solo')
# 但现在你也可以这样做:
# >>> store(d, 'Luke Skywalker', 'Anakin Skywalker')
# >>> lookup(d, 'last', 'Skywalker')
# ['Luke Skywalker', 'Anakin Skywalker']

# 6.4.5 分配参数
# 前面介绍了如何将参数收集到元组和字典中,但用同样的两个运算符(*和**)也可执行相
# 反的操作。与收集参数相反的操作是什么呢?假设有如下函数:
# def add(x, y):
#  return x + y

# 同时假设还有一个元组,其中包含两个你要相加的数。
# params = (1, 2)

# 这与前面执行的操作差不多是相反的:不是收集参数,而是分配参数。这是通过在调用函数
# (而不是定义函数)时使用运算符*实现的。
# >>> add(*params)
# 3

# 这种做法也可用于参数列表的一部分,条件是这部分位于参数列表末尾。通过使用运算符**,
# 可将字典中的值分配给关键字参数。如果你像前面那样定义了函数hello_3,就可像下面这样做:
# >>> params = {'name': 'Sir Robin', 'greeting': 'Well met'}
# >>> hello_3(**params)
# Well met, Sir Robin!

# 如果在定义和调用函数时都使用*或**,将只传递元组或字典。因此还不如不使用它们,还可省却些麻烦。
# >>> def with_stars(**kwds):
# ... print(kwds['name'], 'is', kwds['age'], 'years old')
# ...
# >>> def without_stars(kwds):
# ... print(kwds['name'], 'is', kwds['age'], 'years old')
# ...
# >>> args = {'name': 'Mr. Gumby', 'age': 42}
# >>> with_stars(**args)
# Mr. Gumby is 42 years old
# >>> without_stars(args)
# Mr. Gumby is 42 years old

# 如你所见,对于函数with_stars,我在定义和调用它时都使用了星号,而对于函数without_
# stars,我在定义和调用它时都没有使用,但这两种做法的效果相同。因此,只有在定义函数(允
# 许可变数量的参数)或调用函数时(拆分字典或序列)使用,星号才能发挥作用。

# 6.4.6 练习使用参数
# 面对如此之多的参数提供和接受方式,很容易犯晕。下面来看一个综合示例。首先来定义一些函数。

# def story(**kwds):
#  return 'Once upon a time, there was a ' \
#  '{job} called {name}.'.format_map(kwds)
# def power(x, y, *others):
#  if others:
#  print('Received redundant parameters:', others)
#  return pow(x, y)
# def interval(start, stop=None, step=1):
#  'Imitates range() for step > 0'
#  if stop is None: # 如果没有给参数stop指定值,
#  start, stop = 0, start # 就调整参数start和stop的值
#  result = []
#  i = start # 从start开始往上数
#  while i < stop: # 数到stop位置
#  result.append(i) # 将当前数的数附加到result末尾
#  i += step # 增加到当前数和step(> 0)之和
#  return result

# 下面来尝试调用这些函数。
# >>> print(story(job='king', name='Gumby'))
# Once upon a time, there was a king called Gumby.
# >>> print(story(name='Sir Robin', job='brave knight'))
# Once upon a time, there was a brave knight called Sir Robin.
# >>> params = {'job': 'language', 'name': 'Python'}
# >>> print(story(**params))
# Once upon a time, there was a language called Python.
# >>> del params['job']
# >>> print(story(job='stroke of genius', **params))
# Once upon a time, there was a stroke of genius called Python.
# >>> power(2, 3)
# 8
# >>> power(3, 2)
# 9
# >>> power(y=3, x=2)
# 8
# >>> params = (5,) * 2
# >>> power(*params)
# 3125
# >>> power(3, 3, 'Hello, world')
# Received redundant parameters: ('Hello, world',)
# 27
# >>> interval(10)
# [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# >>> interval(1, 5)
# [1, 2, 3, 4]
# >>> interval(3, 12, 4)
# [3, 7, 11]
# >>> power(*interval(3, 7))
# Received redundant parameters: (5, 6)
# 81

# 6.5 作用域
# 变量到底是什么呢?可将其视为指向值的名称。因此,执行赋值语句x = 1后,名称x指向值
# 1。这几乎与使用字典时一样(字典中的键指向值),只是你使用的是“看不见”的字典。实际上,
# 这种解释已经离真相不远。有一个名为vars的内置函数,它返回这个不可见的字典:
# >>> x = 1
# >>> scope = vars()
# >>> scope['x']
# 1
# >>> scope['x'] += 1
# >>> x
# 2
# 警告 一般而言,不应修改vars返回的字典,因为根据Python官方文档的说法,这样做的结果是
# 不确定的。换而言之,可能得不到你想要的结果。

# 这种“看不见的字典”称为命名空间或作用域。那么有多少个命名空间呢?除全局作用域外,
# 每个函数调用都将创建一个。
# >>> def foo(): x = 42
# ...
# >>> x = 1
# >>> foo()
# >>> x
# 1

# 在这里,函数foo修改(重新关联)了变量x,但当你最终查看时,它根本没变。这是因为调
# 用foo时创建了一个新的命名空间,供foo中的代码块使用。赋值语句x = 42是在这个内部作用域
# (局部命名空间)中执行的,不影响外部(全局)作用域内的x。在函数内使用的变量称为局部变
# 量(与之相对的是全局变量)。参数类似于局部变量,因此参数与全局变量同名不会有任何问题。

# >>> def output(x): print(x)
# ...
# >>> x = 1
# >>> y = 2
# >>> output(y)
# 2
# 到目前为止一切顺利。但如果要在函数中访问全局变量呢?如果只是想读取这种变量的值
# (不重新关联它),通常不会有任何问题。

# >>> def combine(parameter): print(parameter + external)
# ...
# >>> external = 'berry'
# >>> combine('Shrub')
# Shrubberry
# 警告 像这样访问全局变量是众多bug的根源。务必慎用全局变量。

# 重新关联全局变量(使其指向新值)是另一码事。在函数内部给变量赋值时,该变量默认为
# 局部变量,除非你明确地告诉Python它是全局变量。那么如何将这一点告知Python呢?
# >>> x = 1
# >>> def change_global():
# ... global x
# ... x = x + 1
# ...
# >>> change_global()
# >>> x
# 2

# 6.6 递归
# 前面深入介绍了如何创建和调用函数。你知道,函数可调用其他函数,但可能让你感到惊讶
# 的是,函数还可调用自己。
# 如果你以前没有遇到这种情况,可能想知道递归是什么意思。简单地说,递归意味着引用(这里是调用)自身。
# 下面是一个递归式函数定义:
# def recursion():
#  return recursion()

# 这个函数中的递归称为无穷递归(就像以while True打头且不包含break和return语句的循环
# 被称为无限循环一样),因为它从理论上说永远不会结束。你想要的是能对你有所帮助的递归函
# 数,这样的递归函数通常包含下面两部分。
#  基线条件(针对最小的问题):满足这种条件时函数将直接返回一个值。
#  递归条件:包含一个或多个调用,这些调用旨在解决问题的一部分。

# 6.6.1 两个经典案例:阶乘和幂
# 本节探讨两个经典的递归函数。首先,假设你要计算数字n的阶乘。n的阶乘为n × (n-1) × (n -2) × … × 1,
# 在数学领域的用途非常广泛。例如,计算将n个人排成一队有多少种方式。如何计算
# 阶乘呢?可使用循环。
#
# def function(n):
#     result = n
#     for i in range(1,n):
#         result *= i
#     return result
#
# print(function(3))

# 这种实现可行,而且直截了当。大致而言,它是这样做的:首先将result设置为n,再将其
# 依次乘以1到n-1的每个数字,最后返回result。但如果你愿意,可采取不同的做法。关键在于阶
# 乘的数学定义,可表述如下。
#  1的阶乘为1。
#  对于大于1的数字n,其阶乘为n-1的阶乘再乘以n。
# 如你所见,这个定义与本节开头的定义完全等价。

# 下面来考虑如何使用函数来实现这个定义。理解这个定义后,实现起来其实非常简单。
# def function(n):
#     if n == 1:
#         return 1
#     else:
#         return n * function(n-1)
#
# print(function(3))

# 再来看一个示例。假设你要计算幂,就像内置函数pow和运算符**所做的那样。要定义一
# 个数字的整数次幂,有多种方式,但先来看一个简单的定义:power(x, n)(x的n次幂)是将数
# 字x自乘n - 1次的结果,即将n个x相乘的结果。换而言之,power(2, 3)是2自乘两次的结果,
# 即2 × 2 × 2 = 8。

# def function(x,n):
#     result = 1
#     for i in range(n):
#         result *= x
#     return result
#
# print(function(2,3))


# 这是一个非常简单的小型函数,但也可将定义修改成递归式的。
#  对于任何数字x,power(x, 0)都为1。
#  n>0时,power(x, n)为power(x, n-1)与x的乘积。
# 如你所见,这种定义提供的结果与更简单的迭代定义完全相同。理解定义是最难的,而实现
# 起来很容易。

# def function(x,n):
#     if n == 0:
#         return 1
#     else:
#         return x * function(x,n-1)
#
# print(function(2,3))

# 6.6.2 另一个经典案例:二分查找
# 下面来看看最后一个递归示例——二分查找算法。
# 这里的关键是,这种算法自然而然地引出了递归式定义和实现。先来回顾一下定义,确保你
# 知道该如何做。
#  如果上限和下限相同,就说明它们都指向数字所在的位置,因此将这个数字返回。
#  否则,找出区间的中间位置(上限和下限的平均值),再确定数字在左半部分还是右半部
# 分。然后在继续在数字所在的那部分中查找。

# 现在可以实现二分查找了。
# def search(sequence, number, lower, upper):
#  if lower == upper:
#  assert number == sequence[upper]
#  return upper
#  else:
#  middle = (lower + upper) // 2
#  if number > sequence[middle]:
#  return search(sequence, number, middle + 1, upper)
#  else:
#  return search(sequence, number, lower, middle)



# 6.7 小结
# 本章介绍了抽象的基本知识以及函数。
#  抽象:抽象是隐藏不必要细节的艺术。通过定义处理细节的函数,可让程序更抽象。
#  函数定义:函数是使用def语句定义的。函数由语句块组成,它们从外部接受值(参数),
# 并可能返回一个或多个值(计算结果)。
#  参数:函数通过参数(调用函数时被设置的变量)接收所需的信息。在Python中,参数有
# 两类:位置参数和关键字参数。通过给参数指定默认值,可使其变成可选的。
#  作用域:变量存储在作用域(也叫命名空间)中。在Python中,作用域分两大类:全局作
# 用域和局部作用域。作用域可以嵌套。
#  递归:函数可调用自身,这称为递归。可使用递归完成的任何任务都可使用循环来完成,
# 但有时使用递归函数的可读性更高。
#  函数式编程:Python提供了一些函数式编程工具,其中包括lambda表达式以及函数map、
# filter和reduce。

# 6.7.1 本章介绍的新函数
# 函 数                                               描 述
# map(func, seq[, seq, ...])                对序列中的所有元素执行函数
# filter(func, seq)                         返回一个列表,其中包含对其执行函数时结果为真的所有元素
# reduce(func, seq[, initial])              等价于 func(func(func(seq[0], seq[1]), seq[2]), ...)
# sum(seq)                                  返回 seq 中所有元素的和
# apply(func[, args[, kwargs]])             调用函数(还提供要传递给函数的参数)

# 6.7.2 预告
# 下一章将介绍面向对象编程,让你能够进一步提高程序的抽象程度。你将学习如何创建自
# 定义类型(类),并将其与Python提供的类型(如字符串、列表和字典)一起使用,这让你能
# 够编写出质量更高的程序。阅读完下一章后,你将能够编写出大型程序,同时不会在源代码中
# 迷失方向。





















  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

weixin_44119674

觉得有帮助,鼓励下吧

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值