1 默认参数的坑
def add_end(L=[]):
L.append("end")
return L
正常传参数时结果正常。
例1
if __name__ == "__main__":
print(add_end())
print(add_end([1]))
返回结果是:
['end']
[1, 'end']
例2
if __name__ == "__main__":
print(add_end())
print(add_end([1]))
返回结果是:
['end']
['end']
但是如果:
if __name__ == "__main__":
print(add_end())
print(add_end())
则返回结果是:
['end']
['end', 'end']
因为Python函数在定义的时候,默认参数L
的值就被计算出来了,即[]
,因为默认参数L
也是一个变量,它指向对象[]
,每次调用该函数,如果改变了L
的内容,则下次调用时,默认参数的内容就变了,不再是函数定义时的[]
了。
因此,默认参数需要指向不变对象。
2 可变参数
假如我们的函数需要传入多个(但是个数不定的)参数,可以使用可变参数*args
。例如:
sum(1,2,3)
sum(1,2,3,4,5,6)
sum()
对应的函数定义为:
def sum(*args):
s = 0
for i in args:
s += i
return s
但是,假如说我们已经有一个列表或者tuple,要传进去的话,可以采用下列形式:
l = [1,2,3,4,5,6]
sum(*l)
3 关键字参数
传入不受限制的关键字参数
def enroll(name, age, **kwargs):
for key, value in kwargs.items():
print(key, value)
if __name__ == "__main__":
enroll("Pgl", 23, nation="CN", grade=1)
传入受限制的关键字参数
def enroll(name, age, *, nation, grade):
'''
这意味着我们只能接受关键字nation和grade
'''
pass
对于任意函数,都可以通过类似func(*args, **kw)
的形式调用它,无论它的参数是如何定义的。
4 列表生成式
python内生的、简单却强大的创建list的生成式。
几个例子:
# 生成[1*1, 2*2, ..,100*100]:
[x*x for x in range(1, 101)]
# for循环后面还可以加上if判断,这样我们就可以筛选出仅偶数的平方:
[x*x for x in range(1, 101) if x%2 == 0]
# 可以使用两层循环,生成全排列:
[m+n for m in "python" for n in "PYTHON"]
# if...else 的用法:
[x if x % 2 == 0 else -x for x in range(0, 100)]
5 生成器(generator)
如果列表元素很多,比如一个列表占了500M,那么完整地生成这个列表之后再操作肯定是不划算的。
此时,列表元素可以按照算法推算出来,不必创建完整的list,节省了大量的空间。generator保存的就是这个算法。
创建generator的方法:
# 列表生成式的 [] 改成 ()
L = (x*x for x in range(10))
next(L)
next(L)
next(L)
next(L)
在计算到最后一个元素,没有更多的元素时,抛出StopIteration
的错误。
generator是可迭代对象,因此可以使用for
循环:
g = (x*x for x in range(10))
for n in g:
print(n)
某种程度上说,生成器表达的就是序列的推算规则。
比如,我们写一个Fibonacci数列的推算:
# 如果一个函数包含yield关键字,那么这个函数就是一个生成器。
def fib(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n += 1
return
生成器运行时在碰到yield之后,会将yield的值返回掉,但是下一次又会从yield处继续执行。直到没有多余的yield之后,会抛出StopIteration
异常。
6 迭代器
可以被用在for循环里的对象:
-
集合数据类型:list, tuple, dict, set, 字符串等。
-
生成器类型:生成器和带yield的函数。
都被称为可迭代对象Iterable。
可以使用isinstance()
判断一个对象是否是Iterable对象:
from collections.abc import Iterable
isinstance([1,2,3], Iterable)
可以被next()
函数调用并且不断返回下一个值的对象称为迭代器:Iterator
。
ls = [1, 2, 3]
print(isinstance(ls, Iterator))
# False
next(ls)
#Traceback (most recent call last):
# File "d:\vscode-workspace\hlwd.py", line 5, in <module>
# next(ls)
#TypeError: 'list' object is not an iterator
生成器都是 Iterator 对象,但是list, dict, str 是 Iterable ,而不是Iterator。
原因:Python的Iterator对象表示的是一个数据流,Iterator对象可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误。
因此,我们可以把这个数据流看做是一个有序序列,却不能提前知道序列的长度,只能不断通过next()
函数实现按需计算下一个数据,所以Iterator的计算是惰性的,只有在需要返回下一个数据时它才会计算。
Iterator甚至可以存储一个无限大的数据流,比如 N \mathbb{N} N。但是list不行。
总结:
Iterator惰性调用next方法(顺序生成一些东西)。
Iterable可以变成Iterator。
7 泛函编程
泛函编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!
map/reduce/filter函数
map(func, Iterable)
:把规则作用到 Iterable 上,把结果作为Iterator返回。
返回的结果是个惰性序列,需要转换成list之类的东西。
所以,map()
作为高阶函数,事实上它把运算规则抽象了。
reduce(func, Iterable)
:把一个函数作用在一个序列上,reduce把结果和序列的下一个元素做累计计算。
例如,把整数字符按顺序写成数:
from functools import reduce
def a(x, y):
return x + y
print(reduce( a, ['1', '3', '5', '7', '9'] ))
>>> 13579
filter(func, list)
func
参数确定判断准则,只把list里满足条件的东西拿出来。返回的是一个惰性的Iterator
。
def is_odd(n):
return n % 2 == 1
list(filter(is_odd, [1, 2, 4, 5, 6, 9, 10, 15]))
# 结果: [1, 5, 9, 15]
一个更经典的示例:生成素数
def _odd_iter():
n = 1
while True:
n = n + 2
yield n
def _not_divisible(n):
return lambda x: x % n > 0
def primes():
yield 2
it = _odd_iter() # 初始序列
while True:
n = next(it) # 返回序列的第一个数
yield n
it = filter(_not_divisible(n), it) # 构造新序列
# 打印1000以内的素数:
for n in primes():
if n < 1000:
print(n)
else:
break
排序函数
sorted(list, key=func)
根据key的返回值来对元素进行排序。
sorted([36, 5, -12, 9, -21], key=abs)
高阶函数的抽象能力是非常强大的,而且,核心代码可以保持得非常简洁。
闭包——返回一个函数
在一个函数里再定义函数。返回的是函数中定义的函数。
def f(a):
def g():
for i in range(a):
print(i)
return g
这里需要注意:返回闭包时返回函数不要引用任何循环变量,或者后续会发生变化的变量。
匿名函数lambda
略
装饰器
函数是对象,可以被赋值给变量。因此,通过变量也可以调动函数。
但是函数有__name__
属性,可以拿到函数的名字,而变量只是一个标签或者说是对对象的引用。
比如,变量f
指向print函数,print.__name__
是print,那么f.__name__
仍然是print。
我们希望在一个函数执行前和执行后能够再做一些事情,比如执行前输出点东西,那可以用到decorator。
import time
def tictoc(f):
def wrapper(*args, **kwargs):
t1 = time.time()
r = f(*args, **kwargs)
t2 = time.time()
print(t2 - t1)
return r
return wrapper
@tictoc
print("hello world!")
相当于执行了tictoc(print("hello world!"))
如果decorator里也需要加参数,比如根据不同的函数打印不同的结果,那么可以再套一层函数:
def log(text):
def tictoc(f):
def wrapper(*args, **kwargs):
t1 = time.time()
r = f(*args, **kwargs)
t2 = time.time()
print(f"running {
f.__name__} with {
text}")
print(t2 - t1)
return r
return wrapper
return tictoc
@log("logging")
print("hello world")
我们还需要更改原始参数的__name__
等属性,否则依赖于__name__
的代码执行会出错。
可以用functools.wraps
更改一手。
import functools
def log(text):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print('%s %s():' % (text, func.__name__))
return func(*args, **kw)
return wrapper
return decorator
偏函数
int('12345', base=2)
我们可以定义一个int2
函数,把base固定,从而简化调用。
def int2(x, base=2):
return int(x, base)
这种固定一个函数的某些参数使得函数的调用变得更简单的函数称为偏函数。
>>> import functools
>>> int2 = functools.partial(int, base=2)
>>> int2('1000000')
64
>>> int2('1010101')
85
创建偏函数时,实际上可以接收函数对象、*args
和**kw
这3个参数。
例如:
import functools
int2 = functools.partial(int, base=2)
k = {
'base': 10}
int2('2134643', **k)
例如:
max2 = functools.partial(max, 10)
# 实际上会把10作为*args的一部分自动加到左边
8 面向对象
访问限制
属性的名称前添加两个下划线__
。比如__name
,可以通过._ClassName__name
来访问。
但是以__
开头和结尾的变量/方法,是可以访问的特殊变量。
一个下划线开头的实例变量名,比如_name
,这样的实例变量外部是可以访问的,但是,按照约定俗成的规定,当你看到这样的变量时,意思就是,“虽然我可以被访问,但是,请把我视为私有变量,不要随意访问”。
继承和多态
继承:子类的同名方法会覆盖父类的同名方法。
多态:不同的类实现的同名方法都可以调用,不要求严格的继承体系。
9 面向对象高级编程
__slots__
在动态语言python中,可以给class绑定任何的属性和方法——动态语言的灵活性。
class Student():
pass
# 绑定属性
s = Student()
s.name = "Michael"
print(s.name)
def set_age(self, age):
self.age = age
# 给一个实例绑定方法,对另一个实例无效
from types import MethodType
s.set_age = MethodType(set_age, s)
# 给class绑定方法,允许我们在程序运行的过程中动态给class加上功能
def set_score(self, score):
self.score = score
Student.set_score = set_score
# slots允许我们指定对class实例添加的属性:
class UGStudents(Student):
__slots__ = ("name", "age")
s = UGStudents()
s.score = 99 # 报错
# 类中定义的__slots__对当前类起作用,对子类不起作用。
# 子类中起东西
@property
需求:检查参数,并且用类似于属性的简单形式访问变量。
使用@property装饰器。对于类的方法,装饰器一样可用。
class Student():
'''
仅仅定义属性的话就是只读的变量
'''
@property
def score(self):
return self._score
@score.setter
def score(self, value):
if not isinstance(value, int):
raise ValueError("value should be int")
self._score = value
注意:属性的方法名不能和实例变量重名。会爆栈。
class Student(object):
# 方法名称和实例变量均为birth:
@property
def birth(self):
return self.birth
调用birth属性时,会转成方法,返回时视为调用属性,再转成方法…于是栈爆了。
多重继承
略
定制类
__str__
打印得好看一些罢了
__repr__
返回程序开发者看到的字符串,为调试服务。
__iter__
返回一个迭代对象(用于for循环,接着调用__next__
方法惰性计算下一个值)
__getitem__
, __setitem__
, __delitem__
__getitem__
支持索引。__setitem__
也支持索引。设置对应索引的值。
但是复杂的切片功能需要另外设计。
__getattr__
在没有找到属性的情况下,才调用__getattr__
,已有的属性。
任意调用如s.abc
都会返回None
,这是因为我们定义的__getattr__
默认返回就是None