Python基础教程4--函数
本文主要介绍Python的函数及其相关内容。
前面讲的都是语句,接下来我们看下如何将语句组织成函数,有了函数就不用每次都反复输入具体代码了,而只需要调用函数即可完成任务。
函数是经过封装,用来实现特定功能的代码集合,调用函数会返回结果给用户,函数也可以接收参数。
封装函数是一个抽象和封装的过程,是组织程序的关键,好的代码一定是经过不断抽象和封装提炼而成的。
创建函数
创建函数很简单,使用def语句即可
def hello(name): # 函数名为hello, 参数为name
return 'Hello, ' + name +'!' # 返回值
这样就生成了一个名字为hello的函数,它接收并使用一个名字参数,生成一个问候语,并将其返回。
我们再看一个生成斐波那契数列的函数示例
def fibs(num):
"""
This is the function document
A function that generates the Fibonacci sequence
:param num:
:return:
"""
result = [0, 1]
for i in range(num - 2):
result.append(result[-2] + result[-1])
return result
print(fibs(6))
#[0, 1, 1, 2, 3, 5]
print(fibs(10))
#[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
编写文档是个好习惯,可以在def语句之后,函数语句块之前通过字符串为函数添加文档字符串。 文档请参考模块探究
函数返回值
函数通过return语句返回结果
- 如果函数没有返回值,return语句可以省略,如果省略,默认返回为None,这类函数也称为过程函数。
- 如果返回多个值,则可以返回一个元组、列表或者字典。
- return以后的函数代码不会被执行,类似于break,不过这里是跳出函数。
参数
def语句定义函数时,函数名后边的变量称为函数的形参,调用函数是提供的值称为实参。
一般的值可以分为简单数据类型和复杂数据类型
- 简单数据类型: 主要是数字/字符串/布尔/None这些不可变值,简单数据类型作为实参,传递的是值,在函数内部改变参数的值,不会对外部的值造成影响。
>>> def change(t): t = 'Python3' # 虽然改变了参数的值,但是因为是值传递,不会对外部变量造成影响 >>> text = 'Python2' >>> change(text) >>> text # text的值保持不变 'Python2'
如果想要通过函数修改简单数据类型的值,可以从函数返回所需的值,并将其赋给原来的变量
>>> def change(t): return 'Python3' >>> text = 'Python2' >>> text = change(text) # 将返回结果赋给原来的变量 >>> text 'Python3'
- 复杂数据类型: 主要是列表/字典/集合/对象这些可变值对象,复杂数据类型作为实参,传递的是内存地址引用,在函数内部改变参数的值,会影响外部的值,因为操作的是同一份数据。
>>> def change(d): d['name'] = 'Python3' # 改变参数的值,会影响外部变量的值,因为操作的是同一个引用数据 >>> d = {'name': 'Python2'} >>> change(d) >>> d # 字典d的值发生了变化 {'name': 'Python3'}
如果要避免这种影响,可以先拷贝一份数据副本,然后再传递给函数。
位置参数和关键字参数
参数分为位置参数和关键字参数,请注意以下内容都是针对调用函数时的传参操作,跟函数定义无关
- 位置参数: 到目前我们用到的参数都是位置参数,传递到函数的值是通过参数的位置决定的。
def hello(greeting, name): return greeting + ', ' + name
使用时,将参数按顺序传递
>>> hello('Hello','World') 'Hello, World'
- 关键字参数: 通过参数名提供的参数称为关键字参数,传递到函数的值是通过参数的名字决定的。
事实上,参数名可能比位置更重要,尤其是程序的规模越来越大,参数越来越多的时候。
考虑以下两个函数
def hello1(greeting, name): return greeting + ', ' + name def hello2(name, greeting): return greeting + ', ' + name
两个函数的内部实现代码完全一致,只是参数的顺序反了一下
>>> hello1('Hello','World') 'Hello, World' >>> hello2('World',"Hello") 'Hello, World'
虽然输出结果一样,但是会让人很疑惑,可能也很难记住,到底是要先传greeting还是先传name。
而通过关键字参数就可以很容易的明确每个参数的作用,而不用记住到底第几个参数是干什么的。
>>> hello1(greeting='Hello', name='World') 'Hello, World' >>> hello2(greeting='Hello', name='World') 'Hello, World'
关键字参数还有一个很强大的功能就是可以在函数中给参数提供默认值
def hello(name, greeting='Hello'): # 如果greeting参数不传递,会使用默认参数值'Hello' return greeting + ', ' + name
调用时,有默认值的参数可以省略
>>> hello(name='World') # 省略greeting参数,使用默认值'Hello' 'Hello, World' >>> hello(greeting='Hi', name='World') # 默认参数也可以传递 'Hi, World'
- 联合使用: 位置参数和关键字参数是可以联合使用的,把位置参数放在关键字参数之前即可。
def hello(name, greeting='Hello'): # 如果greeting参数不传递,会使用默认参数值'Hello' return greeting + ', ' + name
>>> hello('World') # 位置参数,省略greeting参数 'Hello, World' >>> hello('World',"Hi") # 位置参数 'Hi, World' >>> hello(name='World') # 关键字参数,省略greeting参数 'Hi, World' >>> hello(name='World',greeting="Hi") # 关键字参数 'Hi, World' >>> hello('World',greeting="Hi") # 位置参数+关键字参数,位置参数在前,关键字参数在后 'Hi, World' >>> hello() # 产生异常,因为name没有默认值 TypeError: hello() missing 1 required positional argument: 'name'
虽然语法支持,但实际开发中,应该避免这种混合使用
收集参数
- 收集位置参数
有时候让用户传递任意数量/名称的参数是非常有用的,比如要写一个sum函数,支持任意数量的数字相加,可以通过以下方式实现
def sum(*params): print(params) result = 0 for item in params: result += item return result print(sum(1, 2)) # 3 print(sum(1, 2, 3, 4, 5, 6)) # 21
这里的参数params前面加了一个星号*,打印一下params,可以看到params分别为(1, 2)和(1, 2, 3, 4, 5, 6)。
参数前的星号将位置参数值放置在一个元组中,通过遍历就可以使用元组中的参数值。
而且收集位置参数还可以和其它位置参数联合使用,用来收集"其余的位置参数"。
def sum(init, *params): # 除了init值以外的参数,都收集到了params元组中 result = init for item in params: result += item return result print(sum(10, 1, 2)) # 13 print(sum(100, 1, 2, 3, 4, 5, 6)) # 121
如果不提供任何供收集的参数,params就是一个空元组()
- 收集关键字参数
位置参数可以通过星号*收集,关键字参数则可以通过两个星号**进行收集, 收集到的关键字参数放置在一个字典中。
下面看一个计算正方体体积的函数
def calc_vol(**params): print(params) return params.get('length') * params.get('width') * params.get('height') print(calc_vol(length=2, width=3, height=6)) # 36 #打印出的params为:{'length': 2, 'width': 3, 'height': 6}
同收集位置参数一样,收集关键字参数也可以和其他关键字参数联合使用,用来收集"其余的关键字参数"
- 联合使用: 收集位置参数和收集关键字参数可以联合使用。
我们看一个简单的例子
def print_params(x, y, z=3, *pospar, **keypar): print(x, y, z) print(pospar) print(keypar) print_params(1, 2, 3, 4, 5, 6, foo=1, bar=2) #打印结果为 #1 2 3 #(4, 5, 6) #{'foo': 1, 'bar': 2}
函数装饰器是收集参数的一个典型应用,经过某些操作以后,再将所有参数都传递给具体的调用函数,而不用关心参数的个数和参数名。
def login_required():
def decorate(func):
@wraps(func)
def wrapper(*args, **kwargs):
if _check_login() is False:
return abort(401, response='forbidden')
return func(*args, **kwargs) # 如果校验通过,将参数传递给具体的调用函数
return wrapper
return decorate
只能通过关键字传递的参数
有时我们想设计一个函数,可以接收可变数量的参数,但要求必须有一个或多个关键字参数形式的"选项",或者只想接收关键字参数,类似这种需求就可以使用keyword-only参数模式。
只能通过关键字传递的参数--指的是只能由关键字提供并且永远不会由位置参数自动填充的参数。
>>> def func_keyword_only(a, *args, b, c=3, **kwargs): # 在*args后的参数,如果没有默认值,必须以关键字的方式传递,否则会引发异常
pass
>>> func_keyword_only(1,2) # 引发异常
TypeError: func_keyword_only() missing 1 required keyword-only argument: 'b'
>>> func_keyword_only(1, 2,b=2) # b必须以关键字方式传递
如果不需要位置参数,要求所有的参数都要以关键字的方式传递,可以将*作为第一个形参。
>>> def func_keyword_only(*,a,b): # *作为第一个参数
pass
>>> func_keyword_only(1,2) # 引发异常
TypeError: func_keyword_only() takes 0 positional arguments but 2 were given
>>> func_keyword_only(a=1,b=2) # *后的参数必须以关键字的方式传递
参数传递-参数收集的逆过程
以上讨论了如何将参数收集为元组或字典,事实上,可以通过*和**执行相反的操作,将列表、元组或字典传递为函数的参数。
- 将列表/元组作为位置参数传递: 如果想要把一个元组或列表传递为函数的位置参数,可以在列表或元组前加星号*作为参数调用函数。
还是上述的sum函数,我们将列表和元组作为调用参数
def sum(*params): result = 0 for item in params: result += item return result numbers_tuple = (1, 2) print(sum(*numbers_tuple)) # 将元组作为位置参数调用函数 numbers_list = [1, 2, 3, 4, 5, 6] print(sum(1, 2, 3, 4, 5, 6)) # 将列表作为位置参数调用函数
- 将字典作为关键字参数传递: 在字典前加两个星号**,就可以将字典作为关键字参数调用函数。
def hello(name, greeting='Hello'): return greeting + ', ' + name d = {'name':'World','greeting':'Hello'} print(hello(**d)) # 将字典作为关键字参数调用函数 #打印结果为'Hello, World'
请注意,如果不是用*或**,参数就变成了普通的单个位置参数
参数传递混用
通过*传递位置参数、通过**传递关键字参数和一般的传递位置参数和关键字参数可以混合使用,不过要遵循如下混用规则
- 位置参数pospar要在关键字参数keypar前面
- *pospar参数要在**keypar参数前面
>>> arr = ['Lucy', 18]
>>> obj = {'state': 'United States'}
>>> 'My name is {0}, {1} years old, and I am from the {state}'.format(*arr,**obj) # 位置参数要在关键字参数之前
>>> tup = ['Adam', 'Tom']
>>> dic = {"age": 18}
>>> 'Hello {1}, my name is {0}, {age} years old, and I am from the {state}'.format('Lucy', state='United States', *tup, **dic) # pospar,keypar,*pospar,**keypar
>>> 'Hello {0}, my name is {2}, {age} years old, and I am from the {state}'.format(*tup, 'Lucy', state='United States', **dic) # *pospar,pospar,keypar,**keypar
>>> 'Hello {1}, my name is {0}, {age} years old, and I am from the {state}'.format('Lucy', *tup, state='United States', **dic) # pospar,*pospar,keypar,**keypar
>>> 'Hello {0}, my name is {2}, {age} years old, and I am from the {state}'.format(*tup, 'Lucy', state='United States', **dic) # *pospar,pospar,keypar,**keypar
>>> 'Hello {0}, my name is {2}, {age} years old, and I am from the {state}'.format(*tup, 'Lucy', **dic, state='United States') # *pospar,pospar,**keypar,keypar
#以上语句的执行结果都是
#'Hello Adam, my name is Lucy, 18 years old, and I am from the United States'
递归
函数中除了可以调用其他的函数,也可以调用函数自身,而递归就是在当前函数A中调用函数自身A的函数逻辑。
我们来看一个经典的递归函数用例 - 计算阶乘,n的阶乘结果为n * (n-1) * (n-2) *...* 1,通过递归实现阶乘计算代码如下
def factorial(n):
if n == 1:
return 1
else:
return n * factorial(n - 1) # 调用自身函数,并使用返回的相应结果
print(factorial(6))
#打印结果为720
匿名函数
匿名函数是没有显示地指定函数名的函数,一般通过def定义函数,都会有一个函数名,而有时候,我们并不需要这个函数名,尤其是函数作为其它函数的参数的时候。
比如经常经常在过滤函数中用到的用于进行判断的函数,直接将函数作为参数传递到过滤函数中就可以了,这个判断函数也不会复用,所以没必要先声明一个函数,然后再传递到过滤函数中,这个时候使用匿名函数就非常方便。
- 语法
匿名函数的语法是: lambda [arg1 [,arg2, ... argN]] : expression
lambda用来表示匿名函数,可以传入多个参数,但只能有一个表达式,表达式的结果就是匿名函数的返回值。
>>> add = lambda x, y: x + y >>> add(2,3) 5
以上代码和下面代码效果一样
>>> def add(x,y): return x + y
匿名函数因其语法限制,不能包含其他的语言特性了,不能包括多个语句、条件表达式、迭代以及异常处理等等,所以不适于编写复杂应用。
其适用于编写大量计算表达式值的短小函数或者需要用户提供回调函数的程序,比如过滤、排序等操作。
以下示例演示了使用匿名函数进行列表过滤
>>> numbers = [1, 2, 3, 4, 5, 6] >>> result = filter(lambda x: x%2==0, numbers) # 匿名函数判断数字是否是偶数,是偶数返回True,否则返回False >>> list(result) [2, 4, 6]
- 引用外部变量
定义匿名函数时,可能会使用某些函数外的变量,我们看一个例子
>>> i = 10 >>> a = lambda x: x + i >>> i = 20 >>> b = lambda x: x + i
现在考虑一个问题,a(10)和b(10)的结果分别是多少? 是20/30么? 我们打印一下结果
>>> a(10) 30 >>> b(10) 30
可以看到结果并不是20/30,而都是30。
这其中的原因就在于lambda表达式中的i是一个自由变量,是在运行时绑定值,而不是定义时就绑定,在调用这个lambda表达式的时候,i的值是执行时的值。
>>> i = 100 >>> a(10) 110
如果想让某个匿名函数在定义时就捕获到值,可以通过参数默认值的方式,将引用到的外部变量,作为参数的默认值。
函数定义时,参数默认值的赋值语句(i=i)立刻执行,将值赋给了参数i,函数体中用到的i是函数的参数,是函数内部变量,和外部的变量i没有关系(碰巧同名而已)。
>>> i = 10 >>> a = lambda x,i=i: x + i >>> i = 20 >>> b = lambda x,i_=i: x + i_ #使用另外的参数名,可能更好理解原理 >>> a(10) 20 >>> b(10) 30
- 经典问题
我们再看一个可能会经常遇到的问题,请问以下函数的结果是什么?
>>> funcs = [lambda x: x+n for n in range(6)] >>> funcs[0](10) 15
可以看到funcs[0](10)的结果是15,而不是10,我们分析下原因,通过列表推导式创建函数列表,等价于以下代码
>>> funcs = [] >>> for n in range(6): funcs.append(lambda x: x + n) # 对于lambda函数,n是引用的外部变量
对于funcs列表中的匿名函数来说,n是一个外部变量,循环结束时,n变成了5,又因为匿名函数引用外部变量是运行时绑定的值,所以func[0](10)的结果是10+5=15。
如果希望函数在定义时就记住每次的迭代值,可以将迭代值作为参数的默认值
>>> funcs = [lambda x, n=n: x+n for n in range(6)] >>> funcs[0](10) 10
作用域/命名空间
变量、函数都存在于一个"不可见"的字典中,通过键(变量名/函数名)来引用具体的值,这个字典就叫做作用域或命名空间,只有在其命名空间中,才能访问到这些变量或函数。
可以通过vars()函数获取到这个字典
>>> x =1
>>> scope = vars()
>>> type(scope) # 是一个字典类型对象
< class 'dict' >
>>> scope.get('x') # 可以获取到变量
1
有如下几个不同的作用域
- 全局作用域
顾名思义,全局作用域就是存在于全局的字典,全局可访问对象都存在于这个字典中
>>> x =1 >>> scope = vars() >>> scope.get('x') 1
- 函数作用域
当调用函数时,会创建一个函数命名空间,它作用于函数代码块,变量和参数值只在函数内部起作用,不会影响外部或全局作用域中变量的值。
>>> x = 1 >>> def foo(x,y): print(vars()) x = 100 >>> foo(1,2) {'x': 1, 'y': 2} # 函数作用域字典 >>> x # 不会影响外部变量的值 1
如果局部变量或参数的名字和全局/外层函数中的变量名相同,局部变量会屏蔽同名全局/外层函数中的同名变量。
虽然不建议在在函数内部引用或设置同名全局变量,但是也提供了使用方法,可以通过global关键字将变量声明为全局变量
>>> gv = 10 >>> x = 20 >>> def foo(x,y): print(vars()) print(globals().get('x')) # 通过globals()函数获取全局的变量字典,然后再获取字典中的值,此处为20 global gv # 通过global关键字将变量声明为全局变量,如果没有global关键字,则gv自动成为局部变量 gv = 100 >>> foo(1,2) {'x': 1, 'y': 2} # 函数作用域字典 20 >>> gv # 在函数内部修改了全局变量的值 100
global可用于在函数中初始化全局变量。
另外还有一个关键字可用于访问外层变量nolocal
>>> def outer_fun(): # 外层函数 a = 1 def fun(): # 内层函数 nonlocal a # a为外层函数(outer_fun)作用域的变量a,而不是局部变量 print(a) # 输出1 a = 2 fun() print(a) #输出2 >>> outer_fun()
global和nolocal虽然都能访问当前函数作用域以外的变量,但是两者用法有所区别
- global--表示将变量声明为全局变量
- nonlocal--表示将变量声明为外层变量,是外层函数的局部变量,而且不能是全局变量
以下两段代码都会报错
>>> a = 1 >>> def fun(): nonlocal a # nonlocal声明的变量不能是全局变量,引发异常 print(a) a = 2 >>> fun() SyntaxError: no binding for nonlocal 'a' found >>> def outer_fun(): a = 1 def fun(): global a # 没有声明全局变量a,而且有一个同名的外层的局部变量,所以引发异常 print(a) a = 2 fun() print(a) >>> outer_fun() NameError: name 'a' is not defined >>> a = 10 # 声明一个全局变量后,运行正常,fun函数中指向的是全局变量(10),而不是outer_fun的局部变量(1) 10 1
函数作用域也可以嵌套,即子函数可以访问其祖先函数(可能有很多层级)作用域中的值,这种行为有个名字叫做闭包。
>>> def foo(x): def bar(y): print(vars()) print(x, y) # 子函数可以访问父函数作用域中的变量 return bar >>> foo(1)(2) {'y': 2, 'x': 1} 1 2
- 类作用域
类是面向对象编程的基础,所有位于class内部的代码都在类命名空间中执行,这个命名空间可以由类内部所有成员访问。(类的执行其实就是执行代码块)
像类中的成员方法一样,类作用域中的变量除了可以被类内部成员访问外,也可以被所有实例访问。
有一点需要注意,如果为实例设置类中同名的属性,将会屏蔽类作用域中的变量。我们看一个示例
class Counter: count = 0 # 在类作用域内定义了一个可供所有成员和实例访问的变量 def __init__(self): Counter.count += 1 c1 = Counter() print(Counter.count) c2 = Counter() print(Counter.count) print(c1.count) # 类实例可以访问类作用域内的变量 c1.count = 'two' # 新的count设置到了c1实例上,屏蔽了类作用域的变量 print(c1.count) # c1实例访问的是实例特性中的变量 print(c2.count) # c2实例还是访问的类作用域中的变量
常用函数
以下是一些经常在编程过程中使用到的函数
- getattr(object, name[, default])
返回object对象(可以是对象实例也可以是class类)的属性值,属性是指可以通过点号.访问的值,getattr(obj,'name')==obj.name 。
属性不存在的情况,如果提供了default值,则返回default值,否则引发AttributeError异常。
经常用于访问对象的不确定属性名的属性值。
>>> class Person: class_name = 'person' def __init__(self, name, age): self.name = name self.age = age >>> lucy = Person('Lucy', 18) >>> getattr(lucy, 'name') # ==lucy.name 'Lucy' >>> getattr(lucy, 'age') # ==lucy.age 18 >>> getattr(lucy, 'city') # ==lucy.city AttributeError: 'Person' object has no attribute 'city' >>> getattr(lucy, 'city', 'Shanghai') # 提供默认值 'Shanghai' >>> getattr(Person,'class_name') # ==Person.class_name 'Person'
请注意字典的key是不能通过点号进行访问的,不是属性值。
- setattr(object, name, value)
用于设置属性值,如果属性存在,则替换属性值,否则添加属性值。
>>> setattr(lucy, 'age', '20') # 替换属性值 >>> lucy.age 20 >>> setattr(lucy, 'city', 'Shanghai') # 添加属性值 >>> lucy.city 'Shanghai'
请注意,类似于getattr,setattr一般不能为字典设置属性值,否则会引发异常。
>>> obj = {} >>> setattr(obj, 'city', 'Shanghai') # 抛出异常 AttributeError: 'dict' object has no attribute 'city'
- delattr(object, name)
删除对象的属性,如果属性值不存在会引发异常。
>>> delattr(lucy, 'city') >>> lucy.city # 属性值被删除,不能通过点号访问 AttributeError: 'Person' object has no attribute 'city' >>> delattr(lucy, 'address') # 属性值不能存在,引发异常 AttributeError: address
- hasattr(object, name)
用于判断对象是否包含对应的属性。
>>> hasattr(lucy, 'name') True >>> hasattr(lucy, 'city') False
- callable(object)
用于检查一个对象是否是可调用的,对于函数、方法、lambda函数、 类(构造函数)以及类中实现了__call__方法的类实例, 它都返回True。
一般用于检查对象的是否包含某个函数
>>> callable(None) False >>> callable('abc') False >>> callable([1, 2, 3]) False >>> callable(filter) # 函数 True >>> class A: pass >>> a = A(); >>> callable(A) # A是可调用的构造函数 True >>> callable(a) # a是一个不可调用的对象实例 False >>> class B: def __call__(self): # 类实现了__call__方法 return 'call result' >>> b = B() >>> callable(B) True >>> callable(b) # 类实现了__call__方法,所以对象实例可调用 True >>> b() 'call result'
- filter(function, iterable)
用于过滤序列等可迭代对象,返回一个由符合条件元素组成的迭代器对象。
等价于item for item in iterable if function(item),请参考列表推导式
>>> numbers = [1, 2, 3, 4, 5, 6] >>> result = filter(lambda x: x%2==0, numbers) #这里用到了匿名函数 >>> type(result) # 返回结果是一个filter类的迭代器对象 < class 'filter' > >>> list(result) # 可以通过list函数转换为列表使用 [2, 4, 6]
- map(function, iterable, ...)
根据提供的函数对指定的可迭代对象做映射,将一个或多个序列中的元素作为参数依次调用function函数,返回由每次调用function函数的返回值组成的迭代器对象。
>>> result = map(lambda x: x ** 2, [1, 2, 3, 4, 5]) #对列表中的每个值进行乘方操作,并将乘方的结果作为函数的返回值 >>> type(result) # 返回结果是一个map类的迭代器对象 < class 'map'> >>> list(result) [1, 4, 9, 16, 25] >>> result = map(lambda x, y: x + y, [1, 3, 5, 7, 9], [2, 4, 6, 8, 10]) # 多个序列,以(1,2)/(3,4)/(5,6)/(7,8)/(9,10)作为参数调用函数 >>> list(result) [3, 7, 11, 15, 19]
filter和map经常用于函数式编程
- type(object)
获取对象的类型,经常用于参数类型检查和校验
>>> type(0) < class 'int'> >>> type([]) < class 'list'> >>> type({}) < class 'dict'> >>> type({}) == dict True
- isinstance(object, classinfo)
判断一个对象是否是一个已知的类型,经常用于参数类型检查和校验
>>> isinstance(0,int) True >>> isinstance('abc',str) True
type和isinstance都可以用来进行类型判断,但是这两个方法使用起来会有一个主要区别
- type() 不会认为子类是一种父类类型,即不考虑继承关系,常用于内置类型实例检查,像int/str/dict等,一般很少实现这些类的子类
- isinstance() 会认为子类是一种父类类型,即考虑继承关系,常用于有继承关系的类实例类型检查
>>> class A: pass >>> class B(A): pass >>> b = B() >>> type(b) < class 'B'> >>> type(b) == B True >>> type(b) == A # 不考虑继承关系 False >>> isinstance(b, B) True >>> isinstance(b, A) # 考虑继承关系 True
- range(stop)/range(start, stop[, step])
创建一个包含整数元素的可迭代对象,常用于编写测试代码
>>> list(range(10)) # 等价于range(0,10,1) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] >>> list(range(10,20)) # 等价于range(10,20,1) [10, 11, 12, 13, 14, 15, 16, 17, 18, 19] >>> list(range(10,20,2)) [10, 12, 14, 16, 18]
如果想要深入了解代码的执行过程和作用域变化,可以通过Python Tutor跟踪代码的运行。