面向设计师的Python基础教程 —第五课函数 & 面向过程编程

一 函数

在编程语言当中,函数通常是指一段具有特定语法,能够完成特定功能,并且在一定范围内能够重用的程序段。函数功能的出现是计算机语言历史上的一个重大转折,函数给编程带来了模块化、可复用性、可读性、适合并行和易于维护等特点,更重要的是,在我们前面所讲的三种基本程序结构上,函数给程序带来了更为灵活的组织方式,是当代高级计算机语言具备的最基本特征之一。目前仍有很多被广泛使用的计算机语言是基于纯函数思想的例如lisp、Haskell、Erlang等,这些语言只有纯函数,没有类似其他语言的变量和对象。函数式语言和函数式编程思想广泛用于科学研究领域,但并不适合编写与用户互动的应用程序开发。

1函数的定义

在Python中我们已经大量的接触过函数了,例如print()就是一个函数,不过这些函数属于自带的标准函数或第三方库内部函数,除此之外,我们可以在Python 中自定义函数。

在Python中我们使用def关键字来定义一个函数。

def 函数名(参数列表):

表达式

一个函数包含函数名,参数(可为空),函数主体部分,返回值。起语法结构如下,例如我们定义一个求平方函数和一个没有返回值的函数:

def square(x):

return x**2

def helloworld():

print(‘Hello World’)

其中参数和返回值可以为空,实际上Python传入值和返回值都是None,但是函数的表达式部分不能为空,如果什么也不做用一句pass即可,例如以下为定义一个最小化无任何功能的函数:

def a():

pass

2 调用函数

在Python中调用函数一般形式如下,如果函数有返回值,可以给变量赋值。

函数名(参数列表)

变量 = 函数名(参数列表)

>>> def square(x):

return x**2

print(x**2)

>>> square(5)

25

>>> b = square(6)

>>> b

36

3 关于参数

函数中的传入参数叫做“形式参数”,简称“形参”,例如你定义了一个函数传入参数a,并不意味着你必须传入名为a的变量进去;在调用这个函数时,传入的参数叫做“实际参数”,简称“实参”。

• 形参赋值

在定义函数时,形参允许赋一个默认值,如果相应位置的实参有值,则会覆盖默认参数,如果相应位置的实参无值,则采用默认参数。

>>> def f(a=1,b=2,c=3):

print(a,b,c)

>>> f()

1 2 3

>>> f(4,5,6)

4 5 6

>>> f(4,6)

4 6 3

>>> f (100,c=300)

100 2 300

当形参默认赋值是一个可变对象时,包括列表,字典或类实例等,注意这个赋值由于内存值的改变是会发生变化的。

>>> def f4(a, L=[]):

L.append(a)

print(L)

>>> f4(1)

[1]

>>> f4(2)

[1, 2]

>>> f4(3)

[1, 2, 3]

>>> f4(5,[3,6])

[3, 6, 5]

• 特殊形参

有些时候传入一个函数不确定有多少个参数,可以在形参前添加*来定义这类形参,通过添加*来定义形参。添加一个*时,所有出传入参数在函数内部被放在一个以变量名为名字的元组中;添加两个*时,所有传入参数在函数内部被放在一个以变量名为名字的字典中,此时传入参数必须是arg=value的形式。

>>> def f1(*x):

if len(x) == 0:

print (None)

else:

print (x)

>>> f1()

None

>>> f1(1)

(1,)

>>> f1()

(,)

>>> def f2(**x):

if len(x) == 0:

print (None)

else:

print (x)

>>> f2(4)

Traceback (most recent call last):

File “<pyshell#20>”, line 1, in <module>

f2(4)

TypeError: f2() takes 0 positional arguments but 1 was given

>>> f2(a=1,b=2)

{‘a’: 1, ‘b’: 2}

>>> def f3(a=1,b=2,*x,**y):

print (a,b,x,y)

>>> f3(x=1,y=2)

1 2 () {‘y’: 2, ‘x’: 1}

>>> f3(1,2)

1 2 () {}

>>> f3(1,2,3,4)

1 2 (3, 4) {}

>>> f3(1,2,3,4,x=1,y=2)

1 2 (3, 4) {‘y’: 2, ‘x’: 1}

>>> f3(1,2,x=1)

1 2 () {‘x’: 1}

>>> f3(1,2,af=1)

1 2 () {‘af’: 1}

最后一个例子可以看出函数参数在调用过程中按照参数定义的顺序被依次解析。

4 多重函数与函数及变量定义域问题

函数中可以定义一个闭包(或称做子函数),除非有特殊需要,或者你是一个函数式编程高手,这种方法一般不太被提倡。对于面向过程或者面向对象的编程来说,越复杂的代码组织结构越不利于代码维护。不过或许你有志于成为一个使用Python进行函数式编程的高手,有许多人都在探索Python作为纯粹的函数编程语言范式。

我们来看看闭包,闭包定义在一个函数的内部的函数,很多教材对闭包的定义十分晦涩,例如“闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数”其实就把其当中函数内的函数就行。闭包处于函数内,因此其作用域和变量作用域就是非常重要的问题。例如以下例子:

>>> def add():

def add1(x):

return x + x

return add1

>>> a = add()

>>> a

<function add.<locals>.add1 at 0x00000000020E2840>

>>> a(19)

38

这就是一个闭包函数,我们知道Python中一切都是对象,因此闭包函数可以像普通函数一样赋值给一个变量。我们可以从add函数中来调用add1函数,如果在作用域之外直接调用add1函数就会出现语法错误。

>>> add1(19)

Traceback (most recent call last):

File “<pyshell#7>”, line 1, in <module>

add1(19)

NameError: name ‘add1’ is not defined

闭包中的变量可以使用外部变量或外部局部变量(父函数中的变量),但是一般情况下无法修改外部变量或外部局部变量。

>>> a = 1

>>> b = 2

>>> def x():

def y():

c = b**(a+b)

print(c)

return y()

>>> x()

8

如果这里我们企图修改全局变量a的值,会出错且提示局部变量a并不存在:

>>> def x():

def y():

a = b**(a+b)

print(c)

return y()

>>> x()

Traceback (most recent call last):

File “<pyshell#31>”, line 1, in <module>

x()

File “<pyshell#30>”, line 5, in x

return y()

File “<pyshell#30>”, line 3, in y

a = b**(a+b)

UnboundLocalError: local variable ‘a’ referenced before assignment

>>>

在Python3.x中,我们可以使用global和nonlocal标识符在任何地方声明可修改的全局变量和外部局部变量。例如:

>>> a = 1

>>> b = 2

>>> def z():

c = 3

def za():

global a,b

nonlocal c

a = a**2

b = b**2

c = c**2

print(a,b,c)

return za()

>>> z()

1 4 9

>>>

使用函数或者是多重函数时,时刻谨记变量的定义域问题。

5 函数作为参数和返回值

前面的例子已经遇到函数作为返回值,例如我们在一个函数中定义一个闭包,这个函数可以作为返回值赋值给任意一个变量(见上小节的例子)。同时函数也可作为参数传入到其他函数中:

>>> def add(a,b):

return a+b

>>> def test(fun,a,b):

print(fun(a,b))

>>>

>>> c = add

>>> test(c,1,3)

4

>>>

6 匿名函数lambda

• 什么是lambda函数

lambda函数是一类特殊的函数,叫做匿名函数。lambda函数是前面所述的函数式编程语言例如Lisp、Haskell等语言中的重要部分,Python中借鉴了这些语言的此项设计。

在Python中使用lambda关键字来创建一个匿名函数,形式如下:

lambda 参数列表 : 表达式

准确的说lambda函数在Python3中不是一个函数,而是一个对象,它不是一个完整的语句,例如如果你在Python中写下了以下这句话,将毫无意义,Python解释器在读取这行代码时会初始化一个函数对象,但马上会丢弃,因为它不存在任何的返回值。

lambda x: print (x)

刚才讲到,lambda匿名函数是一个对象,因此它可以整体赋值给一个变量(函数可以将自己的标识符赋值给一个变量,这点两者比较类似),或者作为列表、元组或者字典中的对象,被赋值的变量将拥有函数属性,可以接收参数并返回lambda表达式的值(lambda匿名函数没有return方法,但它可以返回值,这是一种隐含存在)例如:

a = lambda x:x**2

print(a(2))

L = [lambda x:x**2,lambda x:x**3]

print(L)

print(L[0](10))

>>> ================================ RESTART ================================

>>>

4

[<function <lambda> at 0x00000000020E2840>, <function <lambda> at 0x0000000003336510>]

100

• lambda函数模拟条件分支

lambda函数不能像普通函数一样,可以跨行编写,lambda整体只能在一行解决,因此要在lambda函数中实现条件控制就不能使用if-else语句了,需要用到其他替代。前面我们讲过三元运算符,许多同学可能认为它除了代码简短之外没有什么特殊用途了。在lambda函数中使用三元运算符实现条件控制便是它的用途之一。例如以下这个例子:

>>> a = lambda x: ((x/2 == int(x/2)) and “偶数” or “奇数”)

>>> a(5)

‘奇数’

>>> a(4)

‘偶数’

当然你也可以活用这个方法实现多重分支,例如用lambda实现比较四个数的大小,并按从大到小依次输出,留着本课练习。

• lambda函数模拟循环

lambda函数模拟循环需要用到map()函数,map()函数一般用法为:map(function, sequence)即对sequence一个序列内的元素依次执行function功能,例如以下例子:

>>> F=lambda x: map((lambda y: y**2), x)

>>> L1 = list(range(10))

>>> print(list(F(L1)))

[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

• lambda函数用途简析

比较以下常规def函数与lambda函数可以发现,lambda函数在Python中实际上并没有只能用lambda实现而不能用常规函数实现的地方,而且不能重复调用。这一方法的存在仅仅是对Haskell一类函数式编程语言方法的搬用,许多人的确就这一点质疑lambda函数在Python中存在的必要性,甚至有人在要求在下一个版本的Python中取消lambda函数。但是lambda真的没有必要性么?

我们来看上节课练习提出的问题,“如何对字符串和数字同时存在的列表进行排序,并要求数字在字符串前面”,下去认真思考的同学会发现,sort()函数有一个key参数可选,key参数需要赋值一个函数对象,用来作为排序的依据,那么这个问题就非常简单。我们只要定义以下的函数赋值给key参数即可:

def sortkey(x):

return str(x)

a.sort(key = sortkey)

当然更简便的方法可以直接使用lambda函数来赋值给key参数:

a.sort(key = key=lambda x:str(x))

不光光是简洁,lambda函数的使用还有以下用处,我们知道。Python中的函数是可以反复使用的,也就是说一旦Python解释器初始化一个函数时,在没有得到回收机制的处理前,这个函数将永远驻留于内存中。而如上例中的key参数在sort完成后边予以回收,因此善用lambda对于较大的程序而言可以节省宝贵的内存资源。

总结一下,lambda函数具有以下用处:

A精简,某些时候让代码更易懂,

B 节省内存资源

C如一次性餐具使用一样方便,不用给它费脑筋取名,还可以乱扔“垃圾”,无需考虑回收问题。

二 面向过程的编程

2.1 何谓面向过程

面向过程是一种以解决事件步骤为中心思想的编程思想,也就是将我们要完成的任务一步一步分解,然后依次调用这些步骤来完成整个任务。当然任务中也许会有重复的步骤但位于不同的任务阶段,那么我们可以通过编程语言的函数功能来实现这些重复调用。

面向对象这一编程思想贴近我们思考问题的方式,同时也贴近任何编程语言的本质,我们不断使用数据对数据进行操作,直到得到我们需要的结果。

面向过程语言依赖于函数与执行结构,也就是我们前面所讲的三种基本结构。例如你不能先调用某个函数,然后再来定义它。近十余年以来,更为流行和认为更为优越的一种编程思想叫做面向对象的编程,下节课内容会讲。面向对象的编程思想将所要完成的工作分成一个一个对象,对象具有数据输入、处理数据、输出数据等功能,把各个必要的对象组成一个结构化的网络,让他们互相协作来完成这个任务。

且不论面向对象面向过程谁优谁劣,但两者的适用范围也是十分明显的。个人认为,在我们建筑工程领域,生成一个特定几何对象或是完成一项工程依靠面向过程的编程思想要更为贴切一些。纵观网络上的许多Rhino.Python例子或Grasshopper Python例子,除了一些与显示效果或UI界面相关的代码,绝大部分都是以面向过程的方式进行编程。因此这种编程方式也是你们以后大多数情况会用到的编程方式。

2.2 案例-二维凸包问题

什么是凸包,用几何学的语言描述非常简单,也就是包含一系列点集的最小路径外轮廓,就是这个点集的凸包。例如如下左图平面上为何点集的凸多边形就是一个二维凸包,右图为何三维空间点集的多面体就是一个三维凸包。

clip_image002clip_image004

凸包问题是计算机几何学的一个经典问题,也是目前的三维软件中都依赖的算法之一。虽然凸包问题看似是一个非常简单的问题,但这个问题有大批科学家都参与其中进行了研究,真正得到解决还是在计算机诞生之后。我在本课的文件包中有相关的资料,各位可以参阅。我们这里使用面向过程的思想来解决这个问题。

二维凸包的算法有很多,我这里采用一个比较简单的方法,简单是指理解起来比较简单,但算法执行的效率却不高,叫卷包裹法(Gift Wrapping)

首先我们要判断一个点集中最外侧的一个点p0,判断方法很简单,我们只需找到横坐标最小的一个点,如果存在多个横坐标最小的点,那么再选取纵坐标最小的点,使用列表的sort(key=fun)方法即可,然后我们把这组点分成两个部分,已选部分和未选部分:

clip_image006

然后我们需要判断下一个点,我们来看这样一个事实,假设我们从逆时针方向选定了下1个点p1,那么我们从p0连接一个向量到p1,那么p0到其余任意一个点的向量连线必须进行顺时针转动才能到达p1,我们可以通过这种方式来选择p1。

clip_image008

我们在高中几何课本中学习过两个向量之间的夹角公式,如下:

clip_image010

clip_image012同时二维向量也有一个叉积法则,二维向量a<x1,y1>和b<x2,y2>的叉积为一个实数Cp=x1*y2-x2*y1,如果Cp为正则表示A逆时针旋转向B 否则为顺时针(图1)。

计算向量夹角公式实在太复杂,因此我们这里有没有必要如下图2选择某点,选择向量和其他向量的每个夹角,而是直接判断与选定某点向量到该点到其他点向量是顺时针转动还是逆时针转动即可(图3)

clip_image014

clip_image016clip_image018

然后每选择一个点,便把该点加入已选点中,在未选点中剔除该点直到选择出最后一个点,然后依次连线即可。

从编程的角度,我们将整个过程分解如下,首先建立点集,这里通过随机方式。(在Turtle环境下编程)。

def adddots(n):

Range = range(-300,300)

xs=random.sample(Range,n)

ys=random.sample(Range,n)

dots = []

for i in range(n):

pu()

goto(xs[i],ys[i])

pd()

dot(10,‘black’)

dots.append([xs[i],ys[i]])

return dots

然后我们设定第一个选点和初始状态的已选点集和未选点集,这里有一个小技巧,为了保证最终连线为一个封闭图形,我将pt0点剔除后再放到了未选点集的末尾。

pt_unpick = adddots(N)

pt_unpick.sort()

pt0 = pt_unpick[0]

pt_unpick.remove(pt0)

pt_unpick.append(pt0)

pt_pick = [pt0]

pt_next = pt0

然后我们需要定义向量叉积算法:

def vectorcross(pt,pts):

pt_choose = pts[0]

for i in range(1,len(pts)):

vec1 = [pt_choose[0]-pt[0],pt_choose[1]-pt[1]]

vec2 = [pts[i][0]-pt[0],pts[i][1]-pt[1]]

veccross = vec1[0]*vec2[1]-vec1[1]*vec2[0]

if veccross < 0:

pt_choose = pts[i]

return pt_choose

再之后通过循环调用向量叉积来选择下一个点,并在从未选剔除,加入已选,直到选择最后一个点为止:

while True:

pt_next = vectorcross(pt_next,pt_unpick)

pt_unpick.remove(pt_next)

pt_pick.append(pt_next)

if pt_next == pt0:

pt_pick.append(pt0)

break

最后调用turtle绘图即可,整个完整的代码如下(6_turcullholl.py):

from turtle import *

from turtle import Screen, mainloop

import sys

import random

N = int(numinput(“请输入生成点的个数”, 100, 10, 8))

def adddots(n):

Range = range(-300,300)

xs=random.sample(Range,n)

ys=random.sample(Range,n)

dots = []

for i in range(n):

pu()

goto(xs[i],ys[i])

pd()

dot(10,‘black’)

dots.append([xs[i],ys[i]])

return dots

def vectorcross(pt,pts):

pt_choose = pts[0]

for i in range(1,len(pts)):

vec1 = [pt_choose[0]-pt[0],pt_choose[1]-pt[1]]

vec2 = [pts[i][0]-pt[0],pts[i][1]-pt[1]]

veccross = vec1[0]*vec2[1]-vec1[1]*vec2[0]

if veccross < 0:

pt_choose = pts[i]

return pt_choose

def main():

pt_unpick = adddots(N)

print(pt_unpick)

pt_unpick.sort()

print(pt_unpick)

pt0 = pt_unpick[0]

pt_unpick.remove(pt0)

pt_unpick.append(pt0)

pt_pick = [pt0]

pt_next = pt0

while True:

pt_next = vectorcross(pt_next,pt_unpick)

pt_unpick.remove(pt_next)

pt_pick.append(pt_next)

if pt_next == pt0:

pt_pick.append(pt0)

break

shape(‘circle’)

shapesize(0.01,0.01,0.01)

pu()

pensize(2)

pencolor(‘blue’)

goto(pt_pick[0][0],pt_pick[0][1])

pd()

for i in range(1,len(pt_pick)):

goto(pt_pick[i][0],pt_pick[i][1])

screen = Screen()

screen.delay(0)

main()

mainloop()

clip_image020

clip_image022

同时我在课程代码文件中提供了另一个文件7_turcullhollbyclass.py,这里我使用了面向对象的思维来设定每个点,使得完成的最终效果中可以实时拖动每个点即时生成一个凸包。像这类需要与用户交互的程序面向对象是最好的解决方式,下一课再见。

class Dots(Turtle):

def __init__(self, x, y, i):

screen = Screen()

screen.delay(0)

Turtle.__init__(self)

self.pu()

self.shape(“circle”)

self.shapesize(0.6,0.6,1)

self.speed(0)

self.setpos((x,y))

self.ondrag(self.drag)

self.onrelease(self.dynamic)

self.i = i

def drag(self, x, y):

self.color(‘red’)

self.sety(y)

self.setx(x)

def dynamic(self,x,y):

self.color(‘black’)

global dots

dots[self.i] = [x,y,self.i]

main()

clip_image023

本课练习:按课后资料中堆栈的扫描法算法来独立解决二维凸包问题。

完成的作业有疑问的可以发到i@alwayswdc.com,我会尽量帮助您。

发表评论

电子邮件地址不会被公开。 必填项已用*标注