资讯专栏INFORMATION COLUMN

Python学习之路7-函数

pekonchan / 2355人阅读

摘要:函数名应尽量只有小写字母和下划线。传递参数位置参数必选参数这就是要求实参的顺序和形参的顺序相同。比如上述函数如果给形参指定默认值,则可以看出这个函数主要是用来描述狗这种宠物的。在的函数中,使用语句来返回值。

《Python编程:从入门到实践》笔记。
本章主要介绍Python中函数的操作,包括函数的概念,定义,如何传参等,最后还有小部分模块的概念。
1. 定义函数 1.1 一般函数

函数是带名字的代码块,该代码块是完成特定工作的固定代码序列。如果程序中多次出现相同或相似的代码块,则应将这段代码提取出来,编写成函数,然后多次调用。通过编写函数可以避免重复工作,使程序的编写、阅读、测试和修复更容易。请使用描述性的函数名来命名函数,以大致表明函数的功能,这样即使没有注释也能容易理解。函数名应尽量只有小写字母和下划线。以下是两个最基本的函数,有参数与无参函数:

# 定义无参函数
def greet_user1():
    """显示简单的问候语"""
    print("Hello!")

# 定义有参函数
def greet_user2(username):
    """显示简单的问候语"""
    print("Hello, " + username.title() + "!")

# 调用函数
greet_user1()
greet_user2("jesse")

# 结果:
Hello!
Hello, Jesse!

在调用函数前,必须先定义函数!即函数的定义部分必须在调用语句之前。
上述代码中的三引号字符串叫做文档字符串,他们既可以被用作代码注释,也可用于自动生成有关程序中函数的文档。

实参和形参
这两个概念经常被搞混,函数定义中的参数叫做形参,比如上述函数greet_user2(username)中的username就是形参;传递给函数的参数叫做实参,比如在调用greet_user2("jesse")时的"jesse"就是实参。

1.2 空函数

如果想定义一个什么都不做的函数,可以使用pass语句

def do_nothing():
    pass

如果为了让程序能跑起来,但暂时又不写这个函数,可以使用pass语句。这里pass用作占位符。

2. 传递参数 2.1 位置参数(必选参数)

这就是要求实参的顺序和形参的顺序相同。

# 代码:
def describe_pet(animal_type, pet_name):
    """显示宠物的信息"""
    print("
I have a " + animal_type + ".")
    print("My " + animal_type + ""s name is " + pet_name.title() + ".")

describe_pet("hamster", "harry")
describe_pet("dog", "willie")

# 结果:
I have a hamster.
My hamster"s name is Harry.

I have a dog.
My dog"s name is Willie.

对于位置参数,应该注意实参的传递顺序,如果顺序不对,结果会出乎意料:有可能报错,如果不报错,函数所要表达的意思可能改变。

# 代码:
describe_pet("willie", "dog")

# 结果:
I have a willie.           # 尴尬
My willie"s name is Dog.
2.2 关键字参数(传实参时)

如果函数的形参过多,则很难记住每个位置的参数是用来干什么的,如果用键值对的方式传递实参,这个问题就能迎刃而解,这就是关键字参数。在传递参数时,直接将形参与实参关联,这样就不用在意实参的位置,依然以上述代码为例,函数定义不变:

# 代码:
describe_pet(animal_type="hamster", pet_name="harry")
describe_pet(pet_name="harry", animal_type="hamster")

# 结果:
I have a hamster.
My hamster"s name is Harry.

I have a hamster.
My hamster"s name is Harry.

请注意,这是一种传递参数的方法!在调用函数时使用!

2.3 默认参数(定义函数时,形参)

编写函数时可以为每个形参指定默认值,给形参指定了默认值之后,在调用函数时可以省略相应的实参。使用默认值可以简化函数调用,也可清楚地指出函数的典型用法。比如上述describe_pet()函数如果给形参animal_type指定默认值“dog”,则可以看出这个函数主要是用来描述狗这种宠物的。

# 代码:
def describe_pet(pet_name, animal_type="dog"):
    """显示宠物的信息"""
    print("
I have a " + animal_type + ".")
    print("My " + animal_type + ""s name is " + pet_name.title() + ".")


describe_pet(pet_name="willie")
describe_pet("happy")
describe_pet("lili", "cat")

# 结果:
I have a dog.
My dog"s name is Willie.

I have a dog.
My dog"s name is Happy.

I have a cat.
My cat"s name is Lili.

在函数调用时,如果给形参提供了实参,Python将使用指定的实参;否则将使用形参的默认值。
注意:默认参数是在函数定义时使用!在定义函数时带有默认值的形参必须在没有默认值的形参后面!

还有一点值得注意:默认参数必须指向不变对象!请看以下代码:

# 代码:
def add_end(temp=[]):
    """在传入的列表最后添加“end”"""
    temp.append("end")
    return temp


print(add_end([1, 2, 3]))
print(add_end(["a", "b", "c"]))
print(add_end())
print(add_end())
print(add_end())

# 结果:
[1, 2, 3, "end"]
["a", "b", "c", "end"]
["end"]
["end", "end"]
["end", "end", "end"]

当给这个函数传递了参数时,结果是正确的,而且,在没有传递参数且第一次调用时,返回结果也是正确的,然而,没有传递参数且第二次、第三次调用时,结果则成了问题。这是因为,Python在函数定义的时候,默认参数的值就被计算了出来,形参只要不指向新的值,它就会一直指向这个默认值,但如果这个默认值是个可变对象,就会出现上述情况。
要修正上述例子,可以使用Nonestr之类的不变对象。如下:

def add_end(temp=None):
    """在传入的列表最后添加“end”"""
    if temp is None:
        temp = []
    temp.append("end")
    return temp

print(add_end())
print(add_end())

# 结果:
["end"]
["end"]

补充--设计不变对象的原因
①对象一旦创建则不可修改,可以减少因修改数据而产生的错误;
②由于对象不可修改,在多任务环境下不需要加锁,同时读不会出错。所以,我们在设计一个对象时,能设计成不变对象则设计成不变对象。

3. 返回值 3.1 返回简单值

函数并非总是直接显示输出,它可以处理一些数据并返回一个或一组值。在Python的函数中,使用return语句来返回值。以下是一个参数可选的带有返回值的函数例子:

# 代码:
def get_formatted_name(first_name, last_name, middel_name=""):
    """返回标准格式的姓名"""
    if middel_name:
        full_name = first_name + " " + middel_name + " " + last_name
    else:
        full_name = first_name + " " + last_name

    return full_name.title()


musician = get_formatted_name("jimi", "hendrix")
print(musician)

musician = get_formatted_name("john", "hooker", "lee")
print(musician)

# 结果:
Jimi Hendrix
John Lee Hooker
3.2 返回字典

Python函数可以返回任何类型的值,包括列表和字典等复杂的数据结构。

# 代码:
def build_person(first_name, las_name, age=""):
    """返回一个字典,其中包含一个人的信息"""
    person = {"first": first_name, "last": las_name}
    if age:
        person["age"] = age
    return person


musician = build_person("jimi", "hendrix", age=27)
print(musician)

# 结果:
{"first": "jimi", "last": "hendrix", "age": 27}
3.3 返回多个值

return语句后面用逗号分隔多个值,则可返回多个值:

# 代码:
def return_mult():
    return 1, 2

a, b = return_mult()
print("a = " + str(a) + "
b = " + str(b))

# 结果:
a = 1
b = 2

但其实这是个假象,其实函数返回的是一个元组(Tuple),只是最后对元组进行了解包,然后对ab进行了平行赋值。

# 代码:
print(return_mult())

# 结果:
(1, 2)

如果函数返回多个值,但有些值并不想要,则这些位置的值可以用下划线_进行接收:

def return_mult():
    return 1, 2, 3
    
a, _, _ = return_mult()
4. 传递列表

将列表传递给函数,函数可以直接访问其内容或对其进行修改。用函数处理列表可以提高效率。
以下代码是一个打印程序,将未打印的设计在打印后转移到另一个列表中,此代码中未使用函数:

# 代码:
# 未打印列表
unprinted_designs = ["iphone case", "robot pendant", "dodecahedron"]
completed_models = []

# 模拟打印过程,知道没有未打印的设计为止,并将已打印的设计移动到“完成列表”
while unprinted_designs:
    current_design = unprinted_designs.pop()

    # 模拟打印过程
    print("Printing model: " + current_design)
    completed_models.append(current_design)

print("
The following models have been printed:")
for completed_model in completed_models:
    print(completed_model)

# 结果:
Printing model: dodecahedron
Printing model: robot pendant
Printing model: iphone case

The following models have been printed:
dodecahedron
robot pendant
iphone case

现在用两个函数来重组这些代码:

# 两个函数:
def print_models(unprinted_designs, completed_models):
    """模拟打印过程,知道没有未打印的设计为止,并将已打印的设计移动到“完成列表”"""
    while unprinted_designs:
        current_design = unprinted_designs.pop()

        # 模拟打印过程
        print("Printing model: " + current_design)
        completed_models.append(current_design)


def show_completed_models(completed_models):
    print("
The following models have been printed:")
    for completed_model in completed_models:
        print(completed_model)

# 主程序代码:
unprinted_designs = ["iphone case", "robot pendant", "dodecahedron"]
completed_models = []

print_models(unprinted_designs, completed_models)
show_completed_models(completed_models)

从以上代码可以看出,使用了函数后,主程序变为了短短四行。
相比于没有使用函数的代码,使用了函数后代码更易读也更容易维护。
在编写函数时,尽量每个函数只负责一项功能,如果一个函数负责的功能太多,应将其分成多个函数。同时,函数里面还能调用另一个函数;函数里也能再定义函数!

禁止函数修改列表
有时候需要禁止函数修改列表,以上述代码为例,print_models()函数在执行完成后清空了未打印列表unprinted_design,但有时我们并不希望这个列表被清空,而是留作备案。为解决此问题,可以向函数传递副本而不是原件,如下:

# 不用改变函数定义,在函数调用时使用切片操作:
print_models(unprinted_designs[:], completed_models)

如果从C/C++的角度来看(没有研究过Python底层代码,这里仅是猜测),实参unprinted_designs是一个指针,当他传递给函数时,形参得到了这个变量的一个拷贝,形参也指向了内存中的那片区域,所以能直接修改。而当使用切片传递拷贝时,Python先在内存中复制一遍实参unprinted_designs指向的数据,并给这片数据的地址赋给一个临时的变量,然后再将这个临时变量传递给形参。

5. 传递任意数量的参数 5.1 结合使用位置参数(必选参数)和任意数量参数(*args)

有时候你并不知道要向函数传递多少个参数,比如制作披萨,你不知道顾客要多少种配料,此时使用带一个星号*的形参,来定义函数:

# 代码:
def make_pizza(*toppings):
    """打印顾客点的所有配料"""
    print(toppings)
    
make_pizza()    # 不传参数
make_pizza("pepperoni")
make_pizza("mushrooms", "green peppers", "extra cheese")

# 结果:
()
("pepperoni",)
("mushrooms", "green peppers", "extra cheese")

从结果可以看出,以可变参数的方式传入值时,Python将值封装成了一个元组,即使是只传入了一个值。

补充:多个参数都在一个列表里面,如果一个元素一个元素的传递,则代码会很难看,可以使用如下方式传递参数,任以上述make_pizza()函数为例:

toppings = ["mushrooms", "green peppers", "extra cheese"]
make_pizza(*toppings)    # 这里是在执行函数,而不是在定义函数!

在后面的“任意关键字参数”小节中,也可用这种方式传值,只不过得用双星号**

注意:如果要让函数接收不同类型的参数,必须将可变参数放在最后,因为Python先匹配位置参数和关键字参数,再将剩余的参数封装到最后一个可变参数中。

# 代码:
def make_pizza(size, *toppings):
    """概述要制作的披萨"""
    print("
Making a " + str(size) + "-inch pizza with the following toppings:")
    for topping in toppings:
        print("- " + topping)

make_pizza(16, "pepperoni")
make_pizza(12, "mushrooms", "green peppers", "extra cheese")

# 结果:
Making a 16-inch pizza with the following toppings:
- pepperoni

Making a 12-inch pizza with the following toppings:
- mushrooms
- green peppers
- extra cheese
5.2 使用任意数量的关键字参数(**kw)

有时候需要传入任意数量的参数,并且还要知道这些参数是用来干什么的,此时函数需要能够接受任意数量的关键字参数,这里使用双星号**来实现:

# 代码:
def build_profile(first, last, **user_info):
    print(user_info)

    """创建一个字典,其中包含我们知道的有关用户的一切"""
    profile = {}
    profile["first_name"] = first
    profile["last_name"] = last
    for key, value in user_info.items():
        profile[key] = value
    return profile

user_profile = build_profile("albert", "einstein", location="princeton", field="physics")
print(user_profile)

# 结果:
{"location": "princeton", "field": "physics"}
{"first_name": "albert", "last_name": "einstein", "location": "princeton", "field": physics"}

从上述结果可以看出,Python将任意关键字参数封装成一个字典。这里也要注意,指示任意关键字参数的形参必须放到最后!

区分---命名关键字参数(也叫命名参数)
上述代码可以传递任意数量的关键字参数,但有时需要限制传入的关键字参数,比如上述build_profile()函数除了传入firstlast这两个必选参数之外,还必须且只能传入agecountry这两个参数(一个不多,一个不少)时,则需要用到命名关键字参数,它使用一个星号分隔必选参数和命名关键字参数,如下:

# 代码:
# 星号后面的为命名关键字参数
def build_profile(first, last, *, age, country="China"):
    """创建一个字典,其中包含我们知道的有关用户的一切"""
    profile = {}
    profile["first_name"] = first
    profile["last_name"] = last
    profile["age"] = age
    profile["country"] = country
    return profile


print(build_profile("albert", "einstein", country="USA", age=20))
print(build_profile("albert", "einstein", age=20))
print(build_profile(age=20, country="USA", first="albert", last="einstein"))
# print(build_profile("albert", "einstein"))

# 结果:
# 如果不看最后一个print(),则代码的执行结果如下:
{"first_name": "albert", "last_name": "einstein", "age": 20, "country": "USA"}
{"first_name": "albert", "last_name": "einstein", "age": 20, "country": "China"}
{"first_name": "albert", "last_name": "einstein", "age": 20, "country": "USA"}

# 如果将最后一个print()的注释去掉,则代码会报错

从以上结果可以看出命名关键字参数必须每个都赋值,可以有默认值,有默认值的可以不用再赋值;命名关键字之间可以交换顺序,如果要和前面的必选参数也交换顺序,则必须使用关键字参数的方式传递实参。

为什么有命名关键字参数
(网上搜的答案,个人暂时认为这种参数可以被位置参数给替换掉)命名参数配合默认参数使用可以简化代码,比如在写类的构造函数时,有10个参数,8个有合理的默认值,那么可以将这8个定义为命名关键字参数,前两个就是必须赋值的位置参数。这样,在后面生成对象时,如果要替换默认值:
①要么按顺序给后面8个参数替换默认值(C++做法);
②要么用关键字参数的传值方式给这8个关键字不一定按顺序来赋值(Python做法);
③要么混合①②的做法,不过容易混淆。(也就是全用必选参数,前面一部分按顺序赋值,后面一部分用关键字参数赋值)

一点感想:但如果是笔者自己写代码,暂时更偏向于全用必选参数,带默认值,即如下定义形式:

def func(a, b, c="test1", d="test2", e="test3", f="test4", g="test5"):
    pass

而不是如下形式:

def func(a, b, *, c="test1", d="test2", e="test3", f="test4", g="test5"):
    pass

可能笔者才疏学浅,暂时还没领会到这种方式的精髓之处。
不过上述是没有可变参数的情况,如果是以如下形式定义函数:

def func(a, b="test", c="test2", *args):
    pass

在以如下形式调用时则会报错:

func("test1", c="test3", b="test2", "test4")

# 结果:
SyntaxError: positional argument follows keyword argument

可以看出,Python在这里将test4解释为了位置参数,但笔者是想将其作为可变参数。所以笔者推测,在以下情况时,使用命名关键字参数比较好:
必选参数数量不少(其中有些参数的默认值不常变动),后面又跟有可变参数,由于必选参数很多,不容易记住位置,如果不用命名参数,按照上述关键字方式调用函数则会出错,所以此时将这些有合理默认值的必选参数变为命名关键字参数,则可以使用关键字参数不按顺序传值。但如果没有可变参数时,笔者还是倾向于使用带默认值的必选参数。

还有一点值得注意:命名关键字参数可以和可变参数(*args)混用,此时语法如下:

def func(a, b, *agrs, c, d):
    pass

这里cd为命名关键字参数,并且前面也不用加单个星号进行区分了,但是,如果和可变数量关键字参数(**kw)进行混用,命名关键字不能在可变数量关键字参数之前,即不存在如下函数定义形式:

def func(a, b, **kw, c, d):
    pass

如果这样定义,Pycharm会标红(其他IDE没用过,不知道提不提示)。

综上所述:Python中一共有五中参数类型,即必选参数(位置参数),默认参数(带默认值的参数),可变参数(args),命名关键字参数和关键字参数(数量可变,*kw),这五种可以同时混用,但是必须遵照如下顺序:
(从左到右)必选参数、默认参数、可变参数、命名关键字参数和关键字参数。以下是这两个参数混用的几个例子:

def func1(a, b, *, c, d, **kw):
    """
    a, b 为必选参数
    c, d 为命名关键字参数
    kw 为关键字参数,可包含多对
    """
    pass


def func2(a, b="test", *args, c, d="test2", **kw):
    """
    :param a: 必选参数
    :param b: 带默认值的必选参数
    :param args: 可变参数
    :param c: 命名关键字参数
    :param d: 带默认值的命名关键字参数
    :param kw: 关键字参数,可包含多对 
    """
    pass

常用的包含任意数量关键字,且不区分参数类型的函数定义方式如下

def func(*args, **kw):
    pass

def func(*args, **kwargs):
    pass
6. 将函数存储在模块(Module)中

在python中,一个.py文件就是一个模块。使用模块的最大好处就是提高了代码的可维护性。其次,代码不用从零开始编写,一个模块编写完成后,可以在其他地方被调用。再次,可以避免函数名和变量名冲突,不同模块可以有相同的函数名和变量名。

6.1 导入整个模块

要让函数是可以导入的,得先创建模块。以上述make_pizza()函数为例,将其余代码删掉,只保留这一个函数,然后再在当前目录中创建一个making_pizzas.py的文件,执行如下代码以导入整个模块:

# making_pizzas.py文件:
import pizza

pizza.make_pizza(12, "mushromms", "green peppers", "extra cheese")
pizza.make_pizza(16, "pepperoni")

# 结果:
Making a 12-inch pizza with the following toppings:
- mushromms
- green peppers
- extra cheese

Making a 16-inch pizza with the following toppings:
- pepperoni

以这种方式导入模块时,按如下方式调用函数:

module_name.function_name()
6.2 导入某模块中特定的函数

语法结构为:

# 导入一个函数
from module_name import function_name

# 导入多个函数,逗号分隔
from module_name import func1, func2, func3

# 以此方式导入模块式,直接以函数名调用函数,前面不用加模块名

仍以上述pizza.py为例:

from pizza import make_pizza

make_pizza(12, "mushromms", "green peppers", "extra cheese")
make_pizza(16, "pepperoni")
6.3 模块补充

别名
当函数名发生冲突,或者函数名、模块名太长时,可以取一个简短的名称,类似“外号”,以上述代码为例:

# 函数取别名
from pizza import make_pizza as mp

mp(12, "mushromms", "green peppers", "extra cheese")
mp(16, "pepperoni")

# -------------------- 另一个文件 ------------------------------
# 模块取别名
import pizza as p
p.make_pizza(12, "mushromms", "green peppers", "extra cheese")
p.make_pizza(16, "pepperoni")

导入模块中的所有函数
例如导入pizza模块中的所有函数:

from pizza import *

make_pizza(12, "mushromms", "green peppers", "extra cheese")
make_pizza(16, "pepperoni")

然而,使用并非自己编写的大型模块时,最好不要采用这种导入方法,因为如果模块中有函数或变量和你自己写的函数、变量同名,结果将有问题。所以,一般的做法是要么只导入你需要的函数,要么导入整个模块并用句点表示法。


Python中的包就是一个文件夹,但这个文件夹下面必须包含名为__init__.py的文件(前后都是双下划线),包中可以放多个模块,组织结构与Java包类似。

迎大家关注我的微信公众号"代码港" & 个人网站 www.vpointer.net ~

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/41795.html

相关文章

  • Python 进阶之路 (九) 再立Flag, 社区最全的itertools深度解析(上)

    摘要:例如,以下对两个的相应元素求和这个例子很好的解释了如何构建中所谓的迭代器代数的函数的含义。为简单起见,假设输入的长度可被整除。接受两个参数一个可迭代的正整数最终会在中个元素的所有组合的元组上产生一个迭代器。 前言 大家好,今天想和大家分享一下我的itertools学习体验及心得,itertools是一个Python的自带库,内含多种非常实用的方法,我简单学习了一下,发现可以大大提升工作...

    tuantuan 评论0 收藏0
  • Python学习利器——我的小白 Anaconda安装之路

    摘要:学习利器我的小白安装之路序易用,但用好却不易,其中比较头疼的就是包管理和不同版本的问题,特别是当你使用的时候。另外值得一提的是,并不仅仅管理的工具包,它也能安装非的包。 Python学习利器——我的小白 Anaconda安装之路 序 Python易用,但用好却不易,其中比较头疼的就是包管理和Python不同版本的问题,特别是当你使用Windows的时候。为了解决这些问题,有不少发行版的...

    余学文 评论0 收藏0
  • Python学习之路20-数据模型

    摘要:前言数据模型其实是对框架的描述,它规范了这门语言自身构件模块的接口,这些模块包括但不限于序列迭代器函数类和上下文管理器。上述类实现了方法,它可用于需要布尔值的上下文中等。但多亏了它是特殊方法,我们也可以把用于自定义数据类型。 《流畅的Python》笔记。本篇是Python进阶篇的开始。本篇主要是对Python特殊方法的概述。 1. 前言 数据模型其实是对Python框架的描述,它规范了...

    ad6623 评论0 收藏0
  • Python学习之路27-对象引用、可变性和垃圾回收

    摘要:函数的参数作为引用时唯一支持的参数传递模式是共享传参,它指函数的形参获得实参中各个引用的副本,即形参是实参的别名。而在上面这个例子中,类的属性实际上是形参所指向的对象所指对象,的别名。 《流畅的Python》笔记本篇是面向对象惯用方法的第一篇,一共六篇。本篇主要是一些概念性的讨论,内容有:Python中的变量,对象标识,值,别名,元组的某些特性,深浅复制,引用,函数参数,垃圾回收,de...

    Batkid 评论0 收藏0
  • Python学习之路29-序列的修改、散列和切片

    摘要:具体方法和上一篇一样,也是用各个分量的哈希值进行异或运算,由于的分量可能很多,这里我们使用函数来归约异或值。每个分量被映射成了它们的哈希值,这些哈希值再归约成一个值这里的传入了第三个参数,并且建议最好传入第三个参数。 《流畅的Python》笔记。本篇是面向对象惯用方法的第三篇。本篇将以上一篇中的Vector2d为基础,定义多维向量Vector。 1. 前言 自定义Vector类的行为...

    马忠志 评论0 收藏0

发表评论

0条评论

pekonchan

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<