06.函数和模块的使用.md 13.7 KB
Newer Older
骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
1 2 3 4
## 函数和模块的使用

在讲解本章节的内容之前,我们先来研究一道数学题,请说出下面的方程有多少组正整数解。

J
jackfrued 已提交
5
![$$x_1 + x_2 + x_3 + x_4 = 8$$](./res/formula_3.png)
骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
6 7 8

事实上,上面的问题等同于将8个苹果分成四组每组至少一个苹果有多少种方案。想到这一点问题的答案就呼之欲出了。

J
jackfrued 已提交
9
![$$C_M^N =\frac{M!}{N!(M-N)!}, \text{(M=7, N=3)} $$](./res/formula_4.png)
骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
10 11 12 13 14 15

可以用Python的程序来计算出这个值,代码如下所示。

```Python
"""
输入M和N计算C(M,N)
J
jackfrued 已提交
16 17 18

Version: 0.1
Author: 骆昊
骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
19 20 21 22 23 24 25 26 27
"""
m = int(input('m = '))
n = int(input('n = '))
fm = 1
for num in range(1, m + 1):
    fm *= num
fn = 1
for num in range(1, n + 1):
    fn *= num
28
fm_n = 1
骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
29
for num in range(1, m - n + 1):
30 31
    fm_n *= num
print(fm // fn // fm_n)
骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
32 33 34 35
```

### 函数的作用

J
jackfrued 已提交
36
不知道大家是否注意到,在上面的代码中,我们做了3次求阶乘,这样的代码实际上就是重复代码。编程大师*Martin Fowler*先生曾经说过:“**代码有很多种坏味道,重复是最坏的一种!**”,要写出高质量的代码首先要解决的就是重复代码的问题。对于上面的代码来说,我们可以将计算阶乘的功能封装到一个称之为“函数”的功能模块中,在需要计算阶乘的地方,我们只需要“调用”这个“函数”就可以了。
骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
37 38 39 40 41 42 43 44

### 定义函数

在Python中可以使用`def`关键字来定义函数,和变量一样每个函数也有一个响亮的名字,而且命名规则跟变量的命名规则是一致的。在函数名后面的圆括号中可以放置传递给函数的参数,这一点和数学上的函数非常相似,程序中函数的参数就相当于是数学上说的函数的自变量,而函数执行完成后我们可以通过`return`关键字来返回一个值,这相当于数学上说的函数的因变量。

在了解了如何定义函数后,我们可以对上面的代码进行重构,所谓重构就是在不影响代码执行结果的前提下对代码的结构进行调整,重构之后的代码如下所示。

```Python
45 46 47 48 49 50 51
"""
输入M和N计算C(M,N)

Version: 0.1
Author: 骆昊
"""
def fac(num):
J
jackfrued 已提交
52
    """求阶乘"""
骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
53 54 55 56 57 58 59 60 61
    result = 1
    for n in range(1, num + 1):
        result *= n
    return result


m = int(input('m = '))
n = int(input('n = '))
# 当需要计算阶乘的时候不用再写循环求阶乘而是直接调用已经定义好的函数
62
print(fac(m) // fac(n) // fac(m - n))
骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
63 64
```

65
> **说明:** Python的`math`模块中其实已经有一个名为`factorial`函数实现了阶乘运算,事实上求阶乘并不用自己定义函数。下面的例子中,我们讲的函数在Python标准库已经实现过了,我们这里是为了讲解函数的定义和使用才把它们又实现了一遍,**实际开发中并不建议做这种低级的重复劳动**。
骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
66 67 68 69


### 函数的参数

J
jackfrued 已提交
70
函数是绝大多数编程语言中都支持的一个代码的"构建块",但是Python中的函数与其他语言中的函数还是有很多不太相同的地方,其中一个显著的区别就是Python对函数参数的处理。在Python中,函数的参数可以有默认值,也支持使用可变参数,所以Python并不需要像其他语言一样支持[函数的重载](https://zh.wikipedia.org/wiki/%E5%87%BD%E6%95%B0%E9%87%8D%E8%BD%BD),因为我们在定义一个函数的时候可以让它有多种不同的使用方式,下面是两个小例子。
骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
71 72 73 74 75 76

```Python
from random import randint


def roll_dice(n=2):
J
jackfrued 已提交
77
    """摇色子"""
J
jackfrued 已提交
78 79 80 81
    total = 0
    for _ in range(n):
        total += randint(1, 6)
    return total
骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
82 83 84


def add(a=0, b=0, c=0):
J
jackfrued 已提交
85
    """三个数相加"""
J
jackfrued 已提交
86
    return a + b + c
骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105


# 如果没有指定参数那么使用默认值摇两颗色子
print(roll_dice())
# 摇三颗色子
print(roll_dice(3))
print(add())
print(add(1))
print(add(1, 2))
print(add(1, 2, 3))
# 传递参数时可以不按照设定的顺序进行传递
print(add(c=50, a=100, b=200))
```

我们给上面两个函数的参数都设定了默认值,这也就意味着如果在调用函数的时候如果没有传入对应参数的值时将使用该参数的默认值,所以在上面的代码中我们可以用各种不同的方式去调用`add`函数,这跟其他很多语言中函数重载的效果是一致的。

其实上面的`add`函数还有更好的实现方案,因为我们可能会对0个或多个参数进行加法运算,而具体有多少个参数是由调用者来决定,我们作为函数的设计者对这一点是一无所知的,因此在不确定参数个数的时候,我们可以使用可变参数,代码如下所示。

```Python
106
# 在参数名前面的*表示args是一个可变参数
骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
107
def add(*args):
J
jackfrued 已提交
108 109 110 111
    total = 0
    for val in args:
        total += val
    return total
骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
112 113


J
jackfrued 已提交
114
# 在调用add函数时可以传入0个或多个参数
骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
115 116 117 118 119 120 121 122 123 124 125 126 127
print(add())
print(add(1))
print(add(1, 2))
print(add(1, 2, 3))
print(add(1, 3, 5, 7, 9))
```

### 用模块管理函数

对于任何一种编程语言来说,给变量、函数这样的标识符起名字都是一个让人头疼的问题,因为我们会遇到命名冲突这种尴尬的情况。最简单的场景就是在同一个.py文件中定义了两个同名函数,由于Python没有函数重载的概念,那么后面的定义会覆盖之前的定义,也就意味着两个函数同名函数实际上只有一个是存在的。

```Python
def foo():
J
jackfrued 已提交
128
    print('hello, world!')
骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
129 130 131


def foo():
J
jackfrued 已提交
132
    print('goodbye, world!')
骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
133 134


135 136
# 下面的代码会输出什么呢?
foo()
骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
137 138 139 140
```

当然上面的这种情况我们很容易就能避免,但是如果项目是由多人协作进行团队开发的时候,团队中可能有多个程序员都定义了名为`foo`的函数,那么怎么解决这种命名冲突呢?答案其实很简单,Python中每个文件就代表了一个模块(module),我们在不同的模块中可以有同名的函数,在使用函数的时候我们通过`import`关键字导入指定的模块就可以区分到底要使用的是哪个模块中的`foo`函数,代码如下所示。

J
jackfrued 已提交
141
`module1.py`
骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
142 143 144 145 146 147

```Python
def foo():
    print('hello, world!')
```

J
jackfrued 已提交
148
`module2.py`
骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
149 150 151 152 153 154

```Python
def foo():
    print('goodbye, world!')
```

J
jackfrued 已提交
155
`test.py`
骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
156 157 158 159

```Python
from module1 import foo

160 161
# 输出hello, world!
foo()
骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
162 163 164

from module2 import foo

165 166
# 输出goodbye, world!
foo()
骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
167 168 169 170
```

也可以按照如下所示的方式来区分到底要使用哪一个`foo`函数。

J
jackfrued 已提交
171
`test.py`
骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
172 173 174 175 176 177 178 179 180 181 182

```Python
import module1 as m1
import module2 as m2

m1.foo()
m2.foo()
```

但是如果将代码写成了下面的样子,那么程序中调用的是最后导入的那个`foo`,因为后导入的foo覆盖了之前导入的`foo`

J
jackfrued 已提交
183
`test.py`
骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
184 185 186 187 188

```Python
from module1 import foo
from module2 import foo

189 190
# 输出goodbye, world!
foo()
骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
191 192
```

J
jackfrued 已提交
193
`test.py`
骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
194 195 196 197 198

```Python
from module2 import foo
from module1 import foo

199 200
# 输出hello, world!
foo()
骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
201 202
```

203
需要说明的是,如果我们导入的模块除了定义函数之外还有可以执行代码,那么Python解释器在导入这个模块时就会执行这些代码,事实上我们可能并不希望如此,因此如果我们在模块中编写了执行代码,最好是将这些执行代码放入如下所示的条件中,这样的话除非直接运行该模块,if条件下的这些代码是不会执行的,因为只有直接执行的模块的名字才是"\_\_main\_\_"
骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
204

J
jackfrued 已提交
205
`module3.py`
骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224

```Python
def foo():
    pass


def bar():
    pass


# __name__是Python中一个隐含的变量它代表了模块的名字
# 只有被Python解释器直接执行的模块的名字才是__main__
if __name__ == '__main__':
    print('call foo()')
    foo()
    print('call bar()')
    bar()
```

J
jackfrued 已提交
225
`test.py`
骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
226 227 228 229 230 231 232 233 234 235 236

```Python
import module3

# 导入module3时 不会执行模块中if条件成立时的代码 因为模块的名字是module3而不是__main__
```

### 练习

#### 练习1:实现计算求最大公约数和最小公倍数的函数。

J
jackfrued 已提交
237 238
参考答案:

骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
239 240
```Python
def gcd(x, y):
J
jackfrued 已提交
241
    """求最大公约数"""
J
jackfrued 已提交
242 243 244 245
    (x, y) = (y, x) if x > y else (x, y)
    for factor in range(x, 0, -1):
        if x % factor == 0 and y % factor == 0:
            return factor
骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
246 247 248


def lcm(x, y):
J
jackfrued 已提交
249
    """求最小公倍数"""
J
jackfrued 已提交
250
    return x * y // gcd(x, y)
骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
251 252 253 254
```

#### 练习2:实现判断一个数是不是回文数的函数。

J
jackfrued 已提交
255 256
参考答案:

骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
257 258
```Python
def is_palindrome(num):
J
jackfrued 已提交
259
    """判断一个数是不是回文数"""
J
jackfrued 已提交
260 261 262 263 264 265
    temp = num
    total = 0
    while temp > 0:
        total = total * 10 + temp % 10
        temp //= 10
    return total == num
骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
266 267 268 269
```

#### 练习3:实现判断一个数是不是素数的函数。

J
jackfrued 已提交
270 271
参考答案:

骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
272 273
```Python
def is_prime(num):
J
jackfrued 已提交
274
    """判断一个数是不是素数"""
275
    for factor in range(2, int(num ** 0.5) + 1):
J
jackfrued 已提交
276 277 278
        if num % factor == 0:
            return False
    return True if num != 1 else False
骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
279 280 281 282
```

#### 练习4:写一个程序判断输入的正整数是不是回文素数。

J
jackfrued 已提交
283 284
参考答案:

骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
285 286
```Python
if __name__ == '__main__':
J
jackfrued 已提交
287 288 289
    num = int(input('请输入正整数: '))
    if is_palindrome(num) and is_prime(num):
        print('%d是回文素数' % num)
骆昊的技术专栏's avatar
骆昊的技术专栏 已提交
290 291
```

J
jackfrued 已提交
292
> **注意**:通过上面的程序可以看出,当我们**将代码中重复出现的和相对独立的功能抽取成函数**后,我们可以**组合使用这些函数**来解决更为复杂的问题,这也是我们为什么要定义和使用函数的一个非常重要的原因。
293

294 295
### 变量的作用域

296 297 298 299
最后,我们来讨论一下Python中有关变量作用域的问题。

```Python
def foo():
J
jackfrued 已提交
300
    b = 'hello'
301

J
jackfrued 已提交
302 303
    # Python中可以在函数内部再定义函数
    def bar():
304
        c = True
305 306
        print(a)
        print(b)
307 308
        print(c)

J
jackfrued 已提交
309
    bar()
310 311 312 313
    # print(c)  # NameError: name 'c' is not defined


if __name__ == '__main__':
J
jackfrued 已提交
314
    a = 100
315
    # print(b)  # NameError: name 'b' is not defined
J
jackfrued 已提交
316
    foo()
317 318
```

J
jackfrued 已提交
319
上面的代码能够顺利的执行并且打印出100、hello和True,但我们注意到了,在`bar`函数的内部并没有定义`a``b`两个变量,那么`a``b`是从哪里来的。我们在上面代码的`if`分支中定义了一个变量`a`,这是一个全局变量(global variable),属于全局作用域,因为它没有定义在任何一个函数中。在上面的`foo`函数中我们定义了变量`b`,这是一个定义在函数中的局部变量(local variable),属于局部作用域,在`foo`函数的外部并不能访问到它;但对于`foo`函数内部的`bar`函数来说,变量`b`属于嵌套作用域,在`bar`函数中我们是可以访问到它的。`bar`函数中的变量`c`属于局部作用域,在`bar`函数之外是无法访问的。事实上,Python查找一个变量时会按照“局部作用域”、“嵌套作用域”、“全局作用域”和“内置作用域”的顺序进行搜索,前三者我们在上面的代码中已经看到了,所谓的“内置作用域”就是Python内置的那些标识符,我们之前用过的`input``print``int`等都属于内置作用域。
320 321 322 323 324

再看看下面这段代码,我们希望通过函数调用修改全局变量`a`的值,但实际上下面的代码是做不到的。

```Python
def foo():
J
jackfrued 已提交
325 326
    a = 200
    print(a)  # 200
327 328 329


if __name__ == '__main__':
J
jackfrued 已提交
330 331 332
    a = 100
    foo()
    print(a)  # 100
333 334 335 336 337 338
```

在调用`foo`函数后,我们发现`a`的值仍然是100,这是因为当我们在函数`foo`中写`a = 200`的时候,是重新定义了一个名字为`a`的局部变量,它跟全局作用域的`a`并不是同一个变量,因为局部作用域中有了自己的变量`a`,因此`foo`函数不再搜索全局作用域中的`a`。如果我们希望在`foo`函数中修改全局作用域中的`a`,代码如下所示。

```Python
def foo():
J
jackfrued 已提交
339 340 341
    global a
    a = 200
    print(a)  # 200
342 343 344


if __name__ == '__main__':
J
jackfrued 已提交
345 346 347
    a = 100
    foo()
    print(a)  # 200
348 349 350 351
```

我们可以使用`global`关键字来指示`foo`函数中的变量`a`来自于全局作用域,如果全局作用域中没有`a`,那么下面一行的代码就会定义变量`a`并将其置于全局作用域。同理,如果我们希望函数内部的函数能够修改嵌套作用域中的变量,可以使用`nonlocal`关键字来指示变量来自于嵌套作用域,请大家自行试验。

J
jackfrued 已提交
352
在实际开发中,我们应该尽量减少对全局变量的使用,因为全局变量的作用域和影响过于广泛,可能会发生意料之外的修改和使用,除此之外全局变量比局部变量拥有更长的生命周期,可能导致对象占用的内存长时间无法被[垃圾回收](https://zh.wikipedia.org/wiki/%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6_(%E8%A8%88%E7%AE%97%E6%A9%9F%E7%A7%91%E5%AD%B8))。事实上,减少对全局变量的使用,也是降低代码之间耦合度的一个重要举措,同时也是对[迪米特法则](https://zh.wikipedia.org/zh-hans/%E5%BE%97%E5%A2%A8%E5%BF%92%E8%80%B3%E5%AE%9A%E5%BE%8B)的践行。减少全局变量的使用就意味着我们应该尽量让变量的作用域在函数的内部,但是如果我们希望将一个局部变量的生命周期延长,使其在定义它的函数调用结束后依然可以使用它的值,这时候就需要使用[闭包](https://zh.wikipedia.org/wiki/%E9%97%AD%E5%8C%85_(%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6)),这个我们在后续的内容中进行讲解。
353

J
jackfrued 已提交
354
> **说明:** 很多人经常会将“闭包”和[“匿名函数”](https://zh.wikipedia.org/wiki/%E5%8C%BF%E5%90%8D%E5%87%BD%E6%95%B0)混为一谈,但实际上它们并不是一回事,如果想了解这个概念,可以看看[维基百科](https://zh.wikipedia.org/wiki/)的解释或者[知乎](https://www.zhihu.com/)上对这个概念的讨论。
355 356 357 358 359 360 361 362 363 364 365 366

说了那么多,其实结论很简单,从现在开始我们可以将Python代码按照下面的格式进行书写,这一点点的改进其实就是在我们理解了函数和作用域的基础上跨出的巨大的一步。

```Python
def main():
    # Todo: Add your code here
    pass


if __name__ == '__main__':
    main()
```