迭代器和生成器
在 Python 中,迭代器(Iterator)是一种通过循环访问数据集合的方式,可以逐个访问集合中的元素,而不需要提前将整个集合加载到内存中。Python 中的迭代器通常是基于可迭代对象(Iterable)来实现的,例如列表、元组、字典、字符串等。
生成器(Generator)是一种特殊的迭代器,可以在每次循环中动态地生成数据,而不是一次性生成所有数据。生成器非常适合处理大量数据,因为它们只在必要时才计算并生成需要的数据,而不是一次性生成所有数据占用大量内存。
它们之间的区别在于,迭代器是一种通过定义类来实现的,必须实现 __iter__()
和 __next__()
两个方法,每个方法的含义、用法和返回值在实现时都需要考虑到所有细节。而生成器则比较简单,可以通过关键字 yield
来生成数据,每次调用生成器时会自动从上一个 yield
语句处继续执行,直到生成器结束或者遇到 return
语句。在 Python 中,生成器通常通过函数来定义,例如:
def my_generator(num):
for i in range(num):
yield i
该生成器函数用于生成 0
至 num - 1
的整数,可以通过 for
循环来访问生成器中的元素,例如:
for item in my_generator(10):
print(item)
这里的 my_generator(10)
返回一个生成器对象,它会动态地生成 0
至 9
的整数,循环直到生成器结束或者遇到 return
语句。
迭代器
到目前为止,您可能已经注意到大多数容器对象都可以使用 for 语句:
for element in [1, 2, 3]:
print(element)
for element in (1, 2, 3):
print(element)
for key in {'one':1, 'two':2}:
print(key)
for char in "123":
print(char)
for line in open("myfile.txt"):
print(line, end='')
这种访问风格清晰、简洁又方便。 迭代器的使用非常普遍并使得 Python 成为一个统一的整体。 在幕后,for 语句会在容器对象上调用 iter()。 该函数返回一个定义了 __next__() 方法的迭代器对象,此方法将逐一访问容器中的元素。 当元素用尽时,__next__() 将引发 StopIteration 异常来通知终止 for
循环。 你可以使用 next() 内置函数来调用 __next__() 方法;这个例子显示了它的运作方式:
s='abcd'
it=iter(s)
for i in range(5):
print(next(it))
a
b
c
d
StopIteration
看过迭代器协议的幕后机制,给你的类添加迭代器行为就很容易了。 定义一个 __iter__()
方法来返回一个带有 __next__() 方法的对象。 如果类已定义了 __next__()
,则 __iter__()
可以简单地返回 self
:
class Reverse:
"""Iterator for looping over a sequence backwards."""
def __init__(self, data):
self.data = data
self.index = len(data)
def __iter__(self):
return self
def __next__(self):
if self.index == 0:
raise StopIteration
self.index = self.index - 1
return self.data[self.index]
rev=Reverse('abcd')
for i in rev:
print(i)
d
c
b
a
迭代器
迭代器是一个可以记住遍历的位置的对象。迭代器对象从第一个元素开始访问,直到所有的元素被访问完结束。迭代器只能往前不会后退。可迭代对象的本质
分析 可迭代对象 进行迭代的过程,发现每迭代一次(即在for...in...中每循环一次)都会返回对象中的下一条数据,一直向后读取数据直到迭代了所有数据后结束。那么,在这个过程中就应该有一个“人”去记录每次访问到了第几条数据,以便每次迭代都可以返回下一条数据。我们把这个能帮助我们进行数据迭代的“人”称为迭代器(Iterator)
可迭代对象的本质就是可以向我们提供一个这样的中间“人”,即迭代器帮助我们对其进行迭代遍历使用。
list、tuple等都是可迭代对象,我们可以通过iter()函数获取这些可迭代对象的迭代器。然后我们可以对获取到的迭代器不断使用next()函数来获取下一条数据。
__next__方法
迭代器是用来帮助我们记录每次迭代访问到的位置,当我们对迭代器使用next()函数的时候,迭代器会向我们返回它所记录位置的下一个位置的数据。
- 凡是可作用于
for
循环的对象都是Iterable
类型;- 凡是可作用于
next()
函数的对象都是Iterator
类型- 集合数据类型如
list
、dict
、str
等是Iterable
但不是Iterator
,不过可以通过iter()
函数获得一个Iterator
对象
生成器
生成器 是一个用于创建迭代器的简单而强大的工具。 它们的写法类似于标准的函数,但当它们要返回数据时会使用 yield 语句。 每次在生成器上调用 next() 时,它会从上次离开的位置恢复执行(它会记住上次执行语句时的所有数据值)。 一个显示如何非常容易地创建生成器的示例如下:
def reverse(data):
for index in range(len(data)-1, -1, -1):
yield data[index]
# for i in range(8,-1,-1):
# yield i
for char in reverse('golf'):
print(char)
可以用生成器来完成的操作同样可以用前一节所描述的基于类的迭代器来完成。 但生成器的写法更为紧凑,因为它会自动创建 __iter__()
和 __next__() 方法。
另一个关键特性在于局部变量和执行状态会在每次调用之间自动保存。 这使得该函数相比使用 self.index
和 self.data
这种实例变量的方式更易编写且更为清晰。
除了会自动创建方法和保存程序状态,当生成器终结时,它们还会自动引发 StopIteration。 这些特性结合在一起,使得创建迭代器能与编写常规函数一样容易。
生成器表达式
某些简单的生成器可以写成简洁的表达式代码,所用语法类似列表推导式,但外层为圆括号而非方括号。 这种表达式被设计用于生成器将立即被外层函数所使用的情况。 生成器表达式相比完整的生成器更紧凑但较不灵活,相比等效的列表推导式则更为节省内存。
sum(i*i for i in range(10))
xvec = [10, 20, 30]
yvec = [7, 5, 3]
sum(x*y for x,y in zip(xvec, yvec))
page='''a
bb
ccc'''
unique_words = set(word for line in page for word in line.split())
print(unique_words)
# valedictorian = max((student.gpa, student.name) for student in graduates)
# list(data[i] for i in range(len(data)-1, -1, -1))
s='abc'
for i,si in enumerate(s):
print(i,si)
{'c', 'a', 'b'}
0 a
1 b
2 c
私有变量
那种仅限从一个对象内部访问的“私有”实例变量在 Python 中并不存在。 但是,大多数 Python 代码都遵循这样一个约定:带有一个下划线的名称 (例如 _spam
) 应该被当作是 API 的非公有部分 (无论它是函数、方法或是数据成员)。 这应当被视为一个实现细节,可能不经通知即加以改变。
class A:
def __init__(self):
self.data = []
self.__a=1
self.b=1
class B(A):
def __int__(self):
print(self.__a)
print(self.b)
b=B()
仍然可以访问和修改
通过列表生成式,我们可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。
所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的
list
,从而节省大量的空间。通俗的理解:
在Python中,这种一边循环一边计算的机制,称为生成器:generator
有一个能具有迭代器的功能,且比它更加简单的方式:生成器(
generator
)生成器是一类特殊的迭代器
创建生成器
1.只要把一个列表生成式的[ ]
改成( )
nums = [x for x in range(5)]
print(type(nums))
print(nums)
nums2 = (x for x in range(5))
print(type(nums2))
print(nums2)
class fibitetator():
def __init__(self):
self.num1=1
self.num2=1
def __next__(self):
self.num1,self.num2=self.num2,self.num1+self.num2
return self.num1
def __iter__(self):
return self
fi=fibitetator()
for i in range(10):
print(fi.__next__(),end='\t')
print()
def fib_generator():
'''代码2(是生成器,能实现斐波那契数列)'''
num1 = 1
num2 = 1
while True:
temp_num = num1
num1, num2 = num2, num1+num2
# return temp_num # 方式1代码
yield temp_num
fib = fib_generator()
print(next(fib))
print(next(fib))
print(next(fib))
print(next(fib))
<class 'list'>
[0, 1, 2, 3, 4]
<class 'generator'>
<generator object <genexpr> at 0x00000217EB940AC0>
1 2 3 5 8 13 21 34 55 89
1
1
2
3
-
区别仅在于最外层的
[ ]
和( )
,nums
是一个列表,而nums2
是一个生成器 -
可以直接打印出列表
nums
的每一个元素,而对于生成器nums2
,我们可以按照迭代器的使用方法来使用,即可以通过next()
函数、for
循环、list()
等方法使用
在使用生成器实现的方式中,我们将原本在迭代器__next__
方法中实现的基本逻辑放到一个函数中来实现,但是将每次迭代返回数值的return
换成了yield
,此时新定义的函数便不再是函数,而是一个生成器了
简单来说:只要在def函数
中有yield
关键字的 就称为 生成器
此时按照调用函数的方式( fib = fib_generator()
)就不再是执行函数体了,而是会返回一个生成器对象,然后就可以按照使用迭代器(因为生成器是一种特殊的迭代器)的方式来使用生成器了
-
return
接收一个函数,且有返回值 -
yield
暂停执行一个函数,且有返回值
def generator_test():
while True:
print("--1--")
num = yield 100
print("--2--", "num=", num)
g = generator_test()
print(next(g))
print(next(g))
print(next(g))
--1--
100
--2-- num= None
--1--
100
--2-- num= None
--1--
100
## 使用`send`唤醒
# 我们除了可以使用`next()`函数来唤醒,让生成器继续执行外,还可以使用`send()`函数来唤醒执行。
# 使用`send()`函数的一个好处是:可以在唤醒的同时向断点处传入一个附加数据
def generator_test():
while True:
print("--1--")
num = yield 100
print("--2--", "num=", num)
g = generator_test()
# 第一次调用,会将yield 100执行完毕后暂停代码执行,注意此时 并没有给num值
print(next(g))
# print(next(g))
# print(next(g))
# send会让生成器从上次停止的位置 继续开始执行,并且会将11传递到生成器中
# 当做上一次执行yield 100 这个表达式的结果
# 然后在第4行代码,就可以想象成 num = 11了,因为100代表上次yield 100的结果
# 直到遇到下一次的yield 暂停运行,并且把100返回,此时就也打印了100
print(g.send(11))
# 与上次send(11)类似,只不过此次将22当做给yield 100这个表示的结果 给num
print(g.send(22))
--1--
100
--2-- num= 11
--1--
100
--2-- num= 22
--1--
100
# 在Python中,生成器(generator)可以通过send()方法发送值进去,这个值会成为生成器函数中yield表达式的返回值。
# 可以看到,send()可以向暂停的生成器发送值,这个值会成为对应的yield表达式的返回。
# 发送的值可以在生成器函数内部进行处理,实现与调用方的双向交互。如果不用send(),生成器就变成了简单的迭代器。
# 需要注意,第一次必须使用next()启动生成器,不能使用send()。否则会抛出TypeError异常。
# 一个生成器函数
def counter(start=0):
n = start
while True:
result = yield n
print(result)
n += 1
# 创建生成器对象
gen = counter(start=5)
# 启动生成器,发送第一个值None
print(next(gen)) # 5
# 发送值到生成器内部,yield表达式返回该值
print(gen.send(100)) # 100
# 又恢复正常的生成器工作
print(next(gen)) # 6
5
100
6
None
7
使用了
yield
关键字的函数不再是函数,而是生成器
yield
关键字有两点作用:
保存当前运行状态(断点),然后暂停执行,即将生成器(函数)挂起
将
yield
关键字后面表达式的值作为返回值返回,此时可以理解为起到了return
的作用可以使用
next()
函数让生成器从断点处继续执行,即唤醒生成器(函数)Python3中的生成器可以使用
return
返回最终运行的返回值生成器是这样一个函数,它记住上一次返回时在函数体中的位置。对生成器函数的第二次(或第 n 次)调用跳转至该函数中间,而上次调用的所有局部变量都保持不变。生成器不仅“记住”了它数据状态;生成器还“记住”了它在流控制构造(在命令式编程中,这种构造不只是数据值)中的位置。
生成器的特点:
存储的是生成数据的方式(即算法),而不是存储生成的数据,因此节约内存
线程的并行
from threading import Lock, Thread
from concurrent.futures import ThreadPoolExecutor
l1 = Lock() # 创建锁l1
l2 = Lock() # 创建锁l2
l1.acquire() # 对l1加锁
l2.acquire() # 对l2加锁
def first():
print(1)
l1.release() # 释放锁l1
def second():
with l1: # 获取锁l1
print(2)
l2.release() # 释放锁l2
def third():
with l2: # 获取锁l2
print(3)
pool = ThreadPoolExecutor(max_workers=3) # 创建线程池
future1 = pool.submit(third) # 提交执行third()的任务
future2 = pool.submit(second) # 提交执行second()的任务
future3 = pool.submit(first) # 提交执行first()的任务
1
2
3
交替使用锁
from threading import Lock, Thread
import time
fooLock = Lock() # 创建foo锁
barLock = Lock() # 创建bar锁
barLock.acquire() # 初始化时,bar锁上锁
def foo():
for i in range(10):
fooLock.acquire() # 获取foo锁
print("foo")
barLock.release() # 释放bar锁
def bar():
for i in range(10):
barLock.acquire() # 获取bar锁
print("bar")
fooLock.release() # 释放foo锁
t1 = Thread(target=foo) # 创建执行foo的线程t1
t2 = Thread(target=bar) # 创建执行bar的线程t2
t1.start() # 启动t1线程
t2.start() # 启动t2线程
with
与“上下文管理器”
1. 目的
如果你有阅读源码的习惯,可能会看到一些优秀的代码经常出现带有 with
关键字的语句,它通常用在什么场景呢?
对于系统资源如文件、数据库连接、socket 而言,应用程序打开这些资源并执行完业务逻辑之后,必须做的一件事就是要关闭(断开)该资源。
比如 Python
程序打开一个文件,往文件中写内容,写完之后,就要关闭该文件,否则会出现什么情况呢?极端情况下会出现Too many open files
的错误,因为系统允许你打开的最大文件数量是有限的。
同样,对于数据库,如果连接数过多而没有及时关闭的话,就可能会出现 Can not connect to MySQL server Too many connections
,因为数据库连接是一种非常昂贵的资源,不可能无限制的被创建
2. 关闭文件
来看看如何正确关闭一个文件
2.1 普通版
def m1():
f = open("output.txt", "w")
f.write("python之禅")
f.close()
这样写有一个潜在的问题,如果在调用 write
的过程中,出现了异常进而导致后续代码无法继续执行,close
方法无法被正常调用,因此资源就会一直被该程序占用者释放。那么该如何改进代码呢?
2.2 进阶版
def m2():
f = open("output.txt", "w")
try:
f.write("python之禅")
except IOError:
print("oops error")
finally:
f.close()
改良版本的程序是对可能发生异常的代码处进行 try
捕获,使用 try/finally
语句,该语句表示如果在 try
代码块中程序出现了异常,后续代码就不再执行,而直接跳转到except
代码块。而无论如何,finally
块的代码最终都会被执行。因此,只要把close
放在 finally
代码中,文件就一定会关闭。
2.3 高级版
def m3():
with open("output.txt", "r") as f:
f.write("Python之禅")
一种更加简洁、优雅的方式就是用 with 关键字。open
方法的返回值赋值给变量f
,当离开with
代码块的时候,系统会自动调用f.close()
方法, with
的作用和使用 try/finally
语句是一样的。那么它的实现原理是什么?在讲 with
的原理前要涉及到另外一个概念,就是上下文管理器(Context Manager)
3. 上下文
什么是上下文(context)
上下文在不同的地方表示不同的含义,要感性理解。context其实说白了,和文章的上下文是一个意思,在通俗一点,我觉得叫环境更好。....
林冲大叫一声“啊也!”....
问:这句话林冲的“啊也”表达了林冲怎样的心里?
答:啊你妈个头啊!
看,一篇文章,给你摘录一段,没前没后,你读不懂,因为有语境,就是语言环境存在,一段话说了什么,要通过上下文(文章的上下文)来推断。
app点击一个按钮进入一个新的界面,也要保存你是在哪个屏幕跳过来的等等信息,以便你点击返回的时候能正确跳回,如果之前没有存储, 肯定就无法正确跳回了。
看这些都是上下文的典型例子,理解成环境就可以,(而且上下文虽然叫上下文,但是程序里面一般都只有上文而已,只是叫的好听叫上下文。。进程中断在操作系统中是有上有下的,不过这个高深的问题就不要深究了。。。)
4. 上下文管理器
任何实现了 __enter__()
和 __exit__()
方法的对象都可称之为上下文管理器,上下文管理器对象可以使用 with
关键字。
如果这样来说的话,文件(file)对象也实现了上下文管理器
那么文件对象是如何实现这两个方法的呢?
我们可以模拟实现一个自己的文件类,让该类实现 __enter__()
和__exit__()
方法
class File():
def __init__(self, filename, mode):
self.filename = filename
self.mode = mode
def __enter__(self):
print("entering")
self.f = open(self.filename, self.mode)
return self.f
def __exit__(self, *args):
print("will exit")
self.f.close()
__enter__()
方法返回资源对象,这里就是你将要打开的那个文件对象,__exit__()
方法处理一些清除工作。
因为 File 类实现了上下文管理器,现在就可以使用 with
语句了
with File('out.txt', 'w') as f:
print("writing")
f.write('hello, python')
这样,你就无需显示地调用close
方法了,由系统自动去调用,哪怕中间遇到异常 close
方法也会被调用
5. 实现上下文管理器的另外方式
Python
还提供了一个 contextmanager
的装饰器,更进一步简化了上下文管理器的实现方式。
通过yield
将函数分割成两部分,yield
之前的语句在__enter__
方法中执行,yield
之后的语句在 __exit__
方法中执行。紧跟在 yield 后面的值是函数的返回值。
from contextlib import contextmanager
@contextmanager
def my_open(path, mode):
f = open(path, mode)
yield f
f.close()
调用
with my_open('out.txt', 'w') as f:
f.write("hello , the simplest context manager")
6. 总结
Python 提供了 with 语法用于简化资源操作的后续清除操作,是 try/finally 的替代方法,实现原理建立在上下文管理器之上。此外,Python 还提供了一个 contextmanager 装饰器,更进一步简化上下管理器的实现方式
闭包
闭包函数有三个特性:
- 函数中嵌套函数
- 外层函数返回内存嵌套的函数名
- 嵌套函数对外部作用域有一个非全局变量的引用
例子
def person(name):
def say(content):
print("(%s):%s" % (name, content))
return say
p1 = person("张三")
p2 = person("李四")
p1("你努力了吗?")
p2("为啥努力!")
p1("你确定不要努力吗?")
p2("嗯,确定?")
p1("那可就不要要怪别人努力了啊")
p2("别人与我何关!")
p1("隔壁那户人家姓xxxx")
p2("( ⊙ o ⊙ )啊!")
# 稍加完善代码:
def who(name):
def do(content):
print("(%s):%s" % (name, content))
return do
print('-'*30)
zhangsan = who("张三")
lisi = who("李四")
zhangsan("你努力了吗?")
lisi("为啥努力!")
zhangsan("你确定不要努力吗?")
lisi("嗯,确定?")
zhangsan("那可就不要要怪别人努力了啊")
lisi("别人与我何关!")
zhangsan("隔壁那户人家姓xxxx")
lisi("( ⊙ o ⊙ )啊!")
(张三):你努力了吗?
(李四):为啥努力!
(张三):你确定不要努力吗?
(李四):嗯,确定?
(张三):那可就不要要怪别人努力了啊
(李四):别人与我何关!
(张三):隔壁那户人家姓xxxx
(李四):( ⊙ o ⊙ )啊!
------------------------------
(张三):你努力了吗?
(李四):为啥努力!
(张三):你确定不要努力吗?
(李四):嗯,确定?
(张三):那可就不要要怪别人努力了啊
(李四):别人与我何关!
(张三):隔壁那户人家姓xxxx
(李四):( ⊙ o ⊙ )啊!
什么是闭包
闭包(closure) 定义非常抽象,很难看懂
下面尝试从概念上去理解一下闭包
在一些语言中,在函数中可以(嵌套)定义另一个函数时,如果内部的函数引用了外部的函数的变量,则可能产生闭包。闭包可以用来在一个函数与一组“私有”变量之间创建关联关系。在给定函数被多次调用的过程中,这些私有变量能够保持其持久性。 —— 维基百科`https://zh.wikipedia.org/wiki/闭包_(计算机科学)`
用比较容易懂的人话说:就是当某个函数被当成对象返回时,夹带了外部变量,就形成了一个闭包
可以这样理解,闭包就是能够读取其他函数内部变量的函数,简化版的类
def make_printer(msg): # 可以认为是 外部函数
def printer(): # 可以认为是 内部函数
print(msg)
return printer # 返回的内部函数的引用
printer = make_printer('Good!')
printer()
Good!
提高代码可复用性的
def line_conf(a, b):
# a、b作为内置变量
def line(x):
return a*x + b
return line
line1 = line_conf(1, 1)
line2 = line_conf(4, 5)
print(line1(5))
print(line2(5))
# 这个例子中,函数`line`与变量`a`,`b`构成闭包。
# 在创建闭包的时候,我们通过`line_conf`的参数`a`,`b`设置了这两个变量的取值,这样就确定了函数的最终形式(`y = x + 1`和`y = 4x + 5`)。
# 如果需要修改这条线的信息,只需要变换参数`a`,`b`,就可以获得不同的直线表达函数。
# 由此,我们可以看到,闭包也具有提高代码可复用性的作用
# 如果没有闭包,我们需要每次创建直线函数的时候同时说明`a`,`b`,`x`。这样,我们就需要更多的参数传递,也减少了代码的可移植性
# 修改外部函数中的变量
def counter(start=0):
def add_one():
nonlocal start
start += 1
return start
return add_one
c1 = counter(5) # 创建一个闭包
print(c1())
print(c1())
c2 = counter(50) # 创建另外一个闭包
print(c2())
print(c2())
print(c1())
print(c1())
print(c2())
print(c2())
6
7
51
52
8
9
53
54
如上面的代码中,调用了2次counter
,也就意味着创建了2个闭包,并且每个闭包之间没有任何关系。
大家是否有种感觉,好像闭包与对象有些类似。确实是这样的,对象其实可通俗的理解为数据(属性)+功能(方法),而闭包也可以理解为数据+功能,只不过此时数据是外部函数中的那些局部变量或者形参,而功能则是内部函数。对象适合完成较为复杂的功能,而闭包则更轻量
闭包应用
对象与闭包区别,使用方法一样,比对象代码量更小,但是只适合方法只有一个的情况
# 实例对象
class Line5(object):
def __init__(self, k, b):
self.k = k
self.b = b
def __call__(self, x):
print(self.k * x + self.b)
line_5_1 = Line5(1, 2)
# 对象.方法()
# 对象()
line_5_1(0)
line_5_1(1)
line_5_1(2)
line_5_2 = Line5(11, 22)
line_5_2(0)
line_5_2(1)
line_5_2(2)
# 缺点:为了计算多条线上的y值,所以需要保存多个k, b的值,因此用了很多个实例对象, 浪费资源
# 闭包
def line_6(k, b):
def create_y(x):
print(k*x+b)
return create_y
line_6_1 = line_6(1, 2)
line_6_1(0)
line_6_1(1)
line_6_1(2)
line_6_2 = line_6(11, 22)
line_6_2(0)
line_6_2(1)
line_6_2(2)
下面应用案例是理解闭包的经典题目,模拟了一个人站在原点,然后向X、Y轴进行移动,每次移动后及时打印当前的位置
def create():
pos = [0, 0] # 坐标系统原点
def player(direction, step):
# 这里应该首先判断参数direction,step的合法性,比如direction不能斜着走,step不能为负等
# 然后还要对新生成的x,y坐标的合法性进行判断处理,这里主要是想介绍闭包,就不详细写了
new_x = pos[0] + direction[0] * step
new_y = pos[1] + direction[1] * step
pos[0] = new_x
pos[1] = new_y
return pos
return player
player = create() # 创建棋子player,起点为原点
print(player([1, 0], 10)) # 向x轴正方向移动10步
print(player([0, 1], 20)) # 向y轴正方向移动20步
print(player([-1, 0], 10)) # 向x轴负方向移动10步
有时我们需要对某些文件的特殊行进行分析,先要提取出这些特殊行
例如,需要取得文件"result.txt"中含有"163.com"关键字的行,看如下代码
def make_filter(keep):
def the_filter(file_name):
file = open(file_name)
lines = file.readlines()
file.close()
filter_doc = [i for i in lines if keep in i]
return filter_doc
return the_filter
filter = make_filter("163.com")
filter_result = filter("result.txt")
总结
闭包定义是在函数内再嵌套函数
闭包是可以访问另一个函数局部作用域中变量的函数
闭包可以读取另外一个函数内部的变量
闭包可以让参数和变量不会被垃圾回收机制回收,始终保持在内存中(而普通的函数调用结束后 会被Python解释器自动释放局部变量)
装饰器
在装饰器这一部分主要讲解以下几种情形:
- 函数装饰器
- 类装饰器
- 装饰器链
- 带参数的装饰器
装饰器的作用
装饰器本质上是一个 Python 函数或类,它可以让其他函数或类在不需要做任何代码修改的前提下增加额外功能,装饰器的返回值也是一个函数/类对象。它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景,装饰器是解决这类问题的绝佳设计。有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码到装饰器中并继续重用。[3]
在网上,各种用的比较多的案例的是,如果我们有非常多的函数,我们现在希望统计每一个函数的运行时间以及打印其参数应该怎么做。
比较智障的方法是:修改函数原来的内容,加上统计时间的代码和打印参数代码。但结合前面的闭包的知识,我们应该可以提出这样一种方法
问题引入
初创公司有N个业务部门,基础平台部门负责提供底层的功能,如:
-
数据库操作
-
Redis调用
-
监控API等功能
其他业务部门使用基础功能时,只需调用基础平台提供的功能即可,如下:
############### 基础平台提供的功能如下 ###############
def f1():
print('f1')
def f2():
print('f2')
def f3():
print('f3')
def f4():
print('f4')
############### 业务部门A 调用基础平台提供的功能 ###############
f1()
f2()
f3()
f4()
############### 业务部门B 调用基础平台提供的功能 ###############
f1()
f2()
f3()
f4()
目前公司有条不紊的进行着,但是以前基础平台的开发人员在写代码时候没有关注验证权限
相关的问题,
即:基础平台的提供的功能现在是可以被任何人使用。这种做法肯定是不妥的,因为这样会容易泄露功能代码
现在需要对基础平台的所有功能进行重构,为平台提供的所有功能添加验证机制,
即:执行功能前,先进行验证
想想,如果是你接手这个任务,你会怎么做呢?
写代码要遵循
开放封闭
原则,虽然在这个原则是用的面向对象开发,但是也适用于函数式编程,简单来说,它规定已经实现的功能代码不允许被修改,但可以被扩展,即:
封闭:已实现的功能代码块
开放:对扩展开发
def check_login(func):
def inner():
# 验证1
# 验证2
# 验证3
func()
return inner
@check_login
def f1():
print('f1')
例子1:
import time
# 装饰器
# 在装饰器函数fa中,需要用一个内部函数wrapper来包装原函数,并在wrapper中调用原函数f。
# 在调用原函数时,需要传入参数args和kwargs。
def check(f):
def wrapper(*args, **kwargs):
time1 = time.time()
f(*args, **kwargs)
time2 = time.time()
print('时间:',time2-time1)
return wrapper
@check
def f(x):
a=0
for i in range(x):
a+=i
f(10**6)
f(10**8)
例子2,使用装饰器实现协议验证和账号、Cookie验证:
from functools import wraps
# 验证协议装饰器
def check_protocol(func):
@wraps(func)
def wrapper(request, *args, **kwargs):
if request.scheme != 'https':
return 'Error - HTTPS required'
return func(request, *args, **kwargs)
return wrapper
# 验证账号装饰器
def check_auth(func):
@wraps(func)
def wrapper(request, *args, **kwargs):
if request.headers.get('X-Auth') != '123456':
return 'Error - Invalid auth token'
return func(request, *args, **kwargs)
return wrapper
# 验证Cookie装饰器
def check_cookie(func):
@wraps(func)
def wrapper(request, *args, **kwargs):
if request.cookies.get('session') != 'asdf1234':
return 'Error - Invalid cookie value'
return func(request, *args, **kwargs)
return wrapper
@check_protocol
@check_auth
@check_cookie
def handle_request(request):
# 处理请求的代码
pass
# 这里通过三个装饰器检查协议、账号认证和Cookie,只有全部通过验证,才会调用真正的请求处理函数。
装饰器等价
先调用装饰器函数,在调用修饰的函数
def check_login(func):
def inner():
# 验证1
if "admin" != input("请输入用户名:"):
return "用户名不正确"
# 验证2
if "123456" != input("请输入密码:"):
return "密码不正确"
# 验证3
if "7788" != input("请输入手机短信验证码:"):
return "验证码不正确"
func()
return inner
# @check_login
def f1():
print('f1')
# 下一行代码完成了第18行@check_login的作用
f1 = check_login(f1)
f1() # 调用f1函数
闭包 和 装饰器的区别
其实实现装饰器用到了闭包,只是将闭包中外部函数的返回值 给了 被装饰函数名字,从而修改了函数指向而已
那么,有个问题需要想想:在使用普通闭包 与 将闭包用作实现装饰器 时,有什么不同吗?
闭包例子
def who(name):
def do(content):
print("(%s):%s" % (name, content))
return do
zhangsan = who("张三")
lisi = who("李四")
zhangsan("你努力了吗?")
lisi("为啥努力!")
zhangsan("你确定不要努力吗?")
lisi("嗯,确定?")
zhangsan("那可就不要要怪别人努力了啊")
lisi("别人与我何关!")
zhangsan("隔壁那户人家姓xxxx")
lisi("( ⊙ o ⊙ )啊!")
装饰器例子
def log(func):
def call():
ret = func()
if ret and isinstance(ret , str):
with open("log.txt", "w") as f:
f.write(ret)
return ret
return call
@log
def print_hello():
return "hello world"
print(print_hello())
小总结:
- 普通闭包:内部函数将使用的外部变量当做数据来用
- 将闭包当做装饰器:内部函数将使用的外部变量当做可调用的对象(例如函数)来调用
- 装饰器(decorator)功能
- 引入日志
- 函数执行时间统计
- 执行函数前预备处理
- 执行函数后清理功能
- 权限校验等场景
- 缓存
对有参数的函数进行装饰
from time import ctime, sleep
def timefun(func):
def wrapped_func(*args, **kwargs):
# args=(3,5,7)
# kwargs = {}
print("%s called at %s"%(func.__name__, ctime()))
# Convert a time in seconds since the Epoch to a string in local time.
func(*args, **kwargs) # 对args,kwargs进行拆包,此时相当于func(3,5,7)
return wrapped_func
@timefun
def foo(a, b, c):
# a=3,b=5,c=7
print(a+b+c) # 15
foo(3,5,7) # 将3,5,7这些未命名参数传递到第5行函数中,此时args用元组存储
foo called at Tue Nov 7 23:09:30 2023
15
foo called at Tue Nov 7 23:09:30 2023
I am foo
对带有`return`的函数进行装饰
from time import ctime, sleep
def timefun(func):
def wrapped_func():
print("%s called at %s" % (func.__name__, ctime()))
# 这里应该加上return ,及时func指向的函数没有返回值,那么默认也是None,
# 此时无非是return None而已,也是可以的
# func() # 接收不到返回值
return func()
return wrapped_func
@timefun
def foo():
print("I am foo")
@timefun
def get_info():
return '----hahah---'
foo()
sleep(2)
foo()
print(get_info())
foo called at Tue Nov 7 23:09:32 2023
I am foo
get_info called at Tue Nov 7 23:09:32 2023
----hahah---
一般情况下为了让装饰器更通用,可以有return
,即使这个被装饰的函数默认没有返回值也不会有问题,因为此时相当于return None
多个装饰器对同一个函数装饰
# 多个装饰器对同一个函数装饰
# 定义函数:完成包裹数据
def makeBold(fn):
def wrapped():
return "<b>" + fn() + "</b>"
return wrapped
# 定义函数:完成包裹数据
def makeItalic(fn):
def wrapped():
return "<i>" + fn() + "</i>"
return wrapped
@makeBold
def test1():
return "hello world-1"
@makeItalic
def test2():
return "hello world-2"
@makeBold
@makeItalic
def test3():
return "hello world-3"
print(test1())
print(test2())
print(test3())
<b>hello world-1</b>
<i>hello world-2</i>
<b><i>hello world-3</i></b>
用类当做装饰器
装饰器函数其实是这样一个接口约束,它必须接受一个callable对象作为参数,然后返回一个callable对象。在Python中一般callable对象都是函数,但也有例外。只要某个对象重写了 `__call__()` 方法,那么这个对象就是callable的。
class Test():
def __call__(self):
print('call me!')
t = Test()
t()
call me!
class Test(object):
def __init__(self, func): #先初始化类
print("---初始化---")
print("func name is %s" % func.__name__)
self.__func = func
def __call__(self): #再调用函数
print("---装饰器中的功能---")
self.__func()
@Test
def test():
print("----test---") #最后一步
test()
---初始化---
func name is test
---装饰器中的功能---
----test---
总结:
-
装饰器:能够快速将函数的指向修改,能够在不修改代码的前提下,给函数添加功能的方式
-
装饰器功能:给函数添加功能
-
特点:不修改原函数代码,还能添加功能;只能在原函数运行之前或者之后添加,不能在原函数运行一半时添加
-
实现过程:1. 将原函数的引用当做实参传递到闭包中 2. 修改原函数的指向为闭包中的内部函数
-
装饰器实际上用到了闭包,只不过在给外层函数传递参数时,传递的是需要被装饰的函数引用而已
-
装饰器还用到了引用,即在Python中,
a=xx
那么无论xx
是列表、字典还是对象,一定是a
指向它,而不是存储它
封装、继承、多态
当谈到封装、继承和多态时,通常是在面向对象编程 (OOP) 的上下文中讨论的。
封装 (Encapsulation) 示例:
class Person:
def __init__(self, name, age):
self.__name = name # 使用双下划线前缀将属性变为私有
self.__age = age
def get_name(self):
return self.__name
def set_name(self, new_name):
if len(new_name) > 0:
self.__name = new_name
def get_age(self):
return self.__age
def set_age(self, new_age):
if new_age >= 0:
self.__age = new_age
# 创建一个Person对象并访问属性
person = Person("Alice", 30)
print(person.get_name()) # 输出:Alice
person.set_age(32) # 设置年龄
print(person.get_age()) # 输出:32
在这个示例中,我们通过将属性标记为私有(使用双下划线前缀)来封装数据,并提供公共方法(get_name
、set_name
、get_age
、set_age
)来访问和修改这些属性。这样,我们可以控制属性的访问和修改,以确保数据的安全性。
使用面向对象来开发,示例代码如下:
- 类:理解为模板
- 对象:理解为实物
- 在使用面向过程编程时,当需要对数据处理时,需要考虑用哪个模板中哪个函数来进行操作,但是当用面向对象编程时,因为已经将数据存储到了这个独立的空间中,这个独立的空间(即对象)中通过一个特殊的变量(
__class__
)能够获取到类(模板),而且这个类中的方法是有一定数量的,与此类无关的将不会出现在本类中,因此需要对数据处理时,可以很快速的定位到需要的方法是谁 这样更方便 - 全局变量是只能有1份的,多个函数需要多个备份时,往往需要利用其它的变量来进行储存;而通过封装 会将用来存储数据的这个变量 变为了对象中的一个“全局”变量,只要对象不一样那么这个变量就可以再有1份,所以这样更方便
- 代码划分更清晰
继承 (Inheritance) 示例:
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
pass
class Dog(Animal):
def speak(self):
return f"{self.name} 说:汪汪汪!"
class Cat(Animal):
def speak(self):
return f"{self.name} 说:喵喵喵!"
# 创建一个Dog对象和一个Cat对象,并调用speak方法
dog = Dog("旺财")
cat = Cat("小花")
print(dog.speak()) # 输出:旺财 说:汪汪汪!
print(cat.speak()) # 输出:小花 说:喵喵喵!
在这个示例中,我们定义了一个基类 Animal
,然后创建了两个派生类 Dog
和 Cat
,它们继承了 Animal
的属性和方法。每个派生类都可以覆盖基类的方法(例如 speak
),以实现自己的行为。
- 能够提升代码的重用率,即开发一个类,可以在多个子功能中直接使用
- 继承能够有效的进行代码的管理,当某个类有问题只要修改这个类就行,而其继承这个类的子类往往不需要就修改
多态 (Polymorphism) 示例:
class Shape:
def area(self):
pass
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius * self.radius
class Rectangle(Shape):
def __init__(self, length, width):
self.length = length
self.width = width
def area(self):
return self.length * self.width
# 创建一个Shape的列表,包含不同类型的图形对象
shapes = [Circle(5), Rectangle(4, 6), Circle(3)]
# 计算并输出每个图形的面积
for shape in shapes:
print(f"面积:{shape.area()}")
在这个示例中,我们定义了一个基类 Shape
,以及两个派生类 Circle
和 Rectangle
,它们都具有一个名为 area
的方法。通过多态,我们可以在一个列表中存储不同类型的图形对象,然后通过统一的方法调用来计算它们的面积。这就是多态的核心思想,不同的对象可以对相同的方法做出不同的响应。
class MiniOS(object):
"""MiniOS 操作系统类 """
def __init__(self, name):
self.name = name
self.apps = [] # 安装的应用程序名称列表
def __str__(self):
return "%s 安装的软件列表为 %s" % (self.name, str(self.apps))
def install_app(self, app):
# 判断是否已经安装了软件
if app.name in self.apps:
print("已经安装了 %s,无需再次安装" % app.name)
else:
app.install()
self.apps.append(app.name)
class App(object):
def __init__(self, name, version, desc):
self.name = name
self.version = version
self.desc = desc
def __str__(self):
return "%s 的当前版本是 %s - %s" % (self.name, self.version, self.desc)
def install(self):
print("将 %s [%s] 的执行程序复制到程序目录..." % (self.name, self.version))
class PyCharm(App):
pass
class Chrome(App):
def install(self):
print("正在解压缩安装程序...")
super().install()
linux = MiniOS("Linux")
print(linux)
pycharm = PyCharm("PyCharm", "1.0", "python 开发的 IDE 环境")
chrome = Chrome("Chrome", "2.0", "谷歌浏览器")
chrome2 = Chrome("Chrome2", "3.0", "古哥浏览器")
linux.install_app(pycharm)
linux.install_app(chrome)
linux.install_app(chrome)
linux.install_app(chrome2)
print(linux)
Linux 安装的软件列表为 []
将 PyCharm [1.0] 的执行程序复制到程序目录...
正在解压缩安装程序...
将 Chrome [2.0] 的执行程序复制到程序目录...
已经安装了 Chrome,无需再次安装
正在解压缩安装程序...
将 Chrome2 [3.0] 的执行程序复制到程序目录...
Linux 安装的软件列表为 ['PyCharm', 'Chrome', 'Chrome2']
多态需要用到继承,重写,调用某个方法时,要看是父类创建的实例,还是子类创建的实例,实例不同调用的方法不同
关键点:
- 定义了一个基类App,它具有name、version、desc属性,以及一个install方法用于安装。
- 定义了两个子类PyCharm和Chrome,继承自App。
- Chrome类对install方法进行了重写,添加了自定义的安装逻辑。
- 在MainOS类中,通过install_app方法安装不同的App实例对象,调用其install方法。
- 对于同一个install_app方法,传入不同的类的实例,会调用各自类中不同的install实现。这就是多态。
- 多态允许我们基于同一接口编写可扩展和可维护的代码。子类可以改变父类的行为,同时保持接口一致。
所以多态的关键是:
- 有继承关系
- 子类覆盖父类方法
- 对不同子类实例调用相同的方法
这使得调用者可以一致地调用方法,而具体执行代码依赖于运行时对象的类型。这就是多态
-
面向过程开发,简单、开发前期快速,越往后越复杂,适合小工程
-
面向对象开发,复杂、开发前期较慢,越往后开发越方便,适合大工程
没有最好的开发模式,只有经过多多练习,见的多了,感受多了,自然也就能够在不同的任务、不同的工程,使用合适的方式进行开发
静态方法和类方法
类属性、实例属性
它们在定义和使用中有所区别,而最本质的区别是内存中保存的位置不同,
-
实例属性属于对象
-
类属性属于类
class Province(object):
# 类属性
country = 'china'
def __init__(self, name):
# 实例属性
self.name = name
# 创建一个实例对象
obj = Province('shangdong')
# 直接访问实例属性
print(obj.name)
# 直接访问类属性
Province.country
由上述代码可以看出【实例属性需要通过对象来访问】【类属性通过类访问】,在使用上可以看出实例属性和类属性的归属是不同的。
其在内容的存储方式类似如下图:
由上图看出:
-
类属性在内存中只保存一份
-
实例属性在每个对象中都要保存一份
-
通过类创建实例对象时,如果每个对象需要具有相同名字的属性,那么就使用类属性,用一份既可
实例方法、静态方法和类方法
方法包括:实例方法、静态方法和类方法,三种方法在内存中都归属于类,区别在于调用方式不同。
-
实例方法:由对象调用;至少一个self参数;执行实例方法时,自动将调用该方法的对象赋值给self;
-
类方法:由类调用; 至少一个cls参数;执行类方法时,自动将调用该方法的类赋值给cls;
-
静态方法:由类调用;无默认参数;
class Foo(object):
def __init__(self, name):
self.name = name
def ord_func(self):
""" 定义实例方法,至少有一个self参数 """
# print(self.name)
print('实例方法')
@classmethod
def class_func(cls):
""" 定义类方法,至少有一个cls参数 """
print('类方法')
@staticmethod
def static_func():
""" 定义静态方法 ,无默认参数"""
print('静态方法')
f = Foo("china")
# 调用实例方法
f.ord_func()
# 调用类方法
Foo.class_func()
# 调用静态方法
Foo.static_func()
实例方法
类方法
静态方法
- 相同点:对于所有的方法而言,均属于类,所以 在内存中也只保存一份
- 不同点:方法调用者不同、调用方法时自动传入的参数不同。
# 实例方法
# self参数表示当前对象的实例
class Person:
def __init__(self, name):
self.name = name
def say_hello(self):
print(f'Hello, my name is {self.name}')
p = Person('John')
p.say_hello() # 调用实例方法,自动传递当前实例p作为self参数
# 静态方法
# 使用@staticmethod装饰器声明,不需要默认的self参数
class Person:
def __init__(self, name):
self.name = name
@staticmethod
def hello():
print('Hello!')
Person.hello() # 直接使用类名调用静态方法,不需要实例化
# 类方法
# 使用@classmethod装饰器声明,默认参数cls代表当前类本身
class Person:
count = 0 # 类属性
def __init__(self, name):
self.name = name
Person.count += 1
@classmethod
def print_count(cls):
print(f'Number of people: {cls.count}')
Person.print_count() # 使用类名调用,将类本身作为参数传递
person=Person('')
person.print_count()
Hello, my name is John
Hello!
Number of people: 0
Number of people: 1
# 实例方法需要实例化对象后才能调用,自动传递self参数
# 静态方法不需要实例化,通过类名直接调用
# 类方法也不需要实例化,会将类本身作为第一个参数传递
多继承以及MRO顺序
1. 多继承中调用父类方式不同结果不同
2.1 单独调用父类的方法
# coding=utf-8
print("******多继承使用类名.__init__ 发生的状态******")
class Parent(object):
def __init__(self, name):
print('parent的init开始被调用')
self.name = name
print('parent的init结束被调用')
class Son1(Parent):
def __init__(self, name, age):
print('Son1的init开始被调用')
self.age = age
Parent.__init__(self, name)
print('Son1的init结束被调用')
class Son2(Parent):
def __init__(self, name, gender):
print('Son2的init开始被调用')
self.gender = gender
Parent.__init__(self, name)
print('Son2的init结束被调用')
class Grandson(Son1, Son2):
def __init__(self, name, age, gender):
print('Grandson的init开始被调用')
Son1.__init__(self, name, age) # 单独调用父类的初始化方法
Son2.__init__(self, name, gender)
print('Grandson的init结束被调用')
gs = Grandson('grandson', 12, '男')
print('姓名:', gs.name)
print('年龄:', gs.age)
print('性别:', gs.gender)
print("******多继承使用类名.__init__ 发生的状态******\n\n")
复制Error复制成功...
运行结果:
******多继承使用类名.__init__ 发生的状态******
Grandson的init开始被调用
Son1的init开始被调用
parent的init开始被调用
parent的init结束被调用
Son1的init结束被调用
Son2的init开始被调用
parent的init开始被调用
parent的init结束被调用
Son2的init结束被调用
Grandson的init结束被调用
姓名: grandson
年龄: 12
性别: 男
******多继承使用类名.__init__ 发生的状态******复制Error复制成功...
2.2 多继承中super调用被重写的父类方法
print("******多继承使用super().__init__ 发生的状态******")
class Parent(object):
def __init__(self, name, *args, **kwargs): # 为避免多继承报错,使用不定长参数,接受参数
print('parent的init开始被调用')
self.name = name
print('parent的init结束被调用')
class Son1(Parent):
def __init__(self, name, age, *args, **kwargs): # 为避免多继承报错,使用不定长参数,接受参数
print('Son1的init开始被调用')
self.age = age
super().__init__(name, *args, **kwargs) # 为避免多继承报错,使用不定长参数,接受参数
print('Son1的init结束被调用')
class Son2(Parent):
def __init__(self, name, gender, *args, **kwargs): # 为避免多继承报错,使用不定长参数,接受参数
print('Son2的init开始被调用')
self.gender = gender
super().__init__(name, *args, **kwargs) # 为避免多继承报错,使用不定长参数,接受参数
print('Son2的init结束被调用')
class Grandson(Son1, Son2):
def __init__(self, name, age, gender):
print('Grandson的init开始被调用')
# 多继承时,相对于使用类名.__init__方法,要把每个父类全部写一遍
# 而super只用一句话,执行了全部父类的方法,这也是为何多继承需要全部传参的一个原因
# super(Grandson, self).__init__(name, age, gender)
super().__init__(name, age, gender)
print('Grandson的init结束被调用')
print(Grandson.__mro__)
gs = Grandson('grandson', 12, '男')
print('姓名:', gs.name)
print('年龄:', gs.age)
print('性别:', gs.gender)
print("******多继承使用super().__init__ 发生的状态******\n\n")复制Error复制成功...
运行结果:
******多继承使用super().__init__ 发生的状态******
(<class '__main__.Grandson'>, <class '__main__.Son1'>, <class '__main__.Son2'>, <class '__main__.Parent'>, <class 'object'>)
Grandson的init开始被调用
Son1的init开始被调用
Son2的init开始被调用
parent的init开始被调用
parent的init结束被调用
Son2的init结束被调用
Son1的init结束被调用
Grandson的init结束被调用
姓名: grandson
年龄: 12
性别: 男
******多继承使用super().__init__ 发生的状态******复制Error复制成功...
2.3 注意
以上2个代码执行的结果不同
-
如果2个子类中都继承了父类,当在子类中通过父类名调用时,parent被执行了2次
-
如果2个子类中都继承了父类,当在子类中通过super调用时,parent被执行了1次
2. 单继承中super
print("******单继承使用super().__init__ 发生的状态******")
class Parent(object):
def __init__(self, name):
print('parent的init开始被调用')
self.name = name
print('parent的init结束被调用')
def p(self): #继承父类的方法
print('父类')
class Son1(Parent):
def __init__(self, name, age):
print('Son1的init开始被调用')
self.age = age
super().__init__(name) # 单继承不能提供全部参数
print('Son1的init结束被调用')
class Grandson(Son1):
def __init__(self, name, age, gender):
print('Grandson的init开始被调用')
super().__init__(name, age) # 单继承不能提供全部参数
print('Grandson的init结束被调用')
gs = Grandson('grandson', 12, '男')
print('姓名:', gs.name)
print('年龄:', gs.age)
print('年龄:', gs.p())
# print('性别:', gs.gender)
print("******单继承使用super().__init__ 发生的状态******\n\n")
******单继承使用super().__init__ 发生的状态******
Grandson的init开始被调用
Son1的init开始被调用
parent的init开始被调用
parent的init结束被调用
Son1的init结束被调用
Grandson的init结束被调用
姓名: grandson
年龄: 12
父类
年龄: None
******单继承使用super().__init__ 发生的状态******
总结
-
super().__init__
相对于类名.__init__
,在单继承上用法基本无差 -
但在多继承上有区别,
super
方法能保证每个父类的方法只会执行一次,而使用类名的方法会导致方法被执行多次,具体看前面的输出结果 -
多继承时,使用
super
方法,对父类的传参数,由于super
的算法导致的原因,必须把参数全部传递,否则会报错 -
单继承时,使用
super
方法,则不能全部传递,只能传父类方法所需的参数,否则会报错 -
多继承时,相对于使用类名.init方法,要把每个父类全部写一遍, 而使用super方法,只需写一句话便执行了全部父类的方法,这也是为何多继承需要全部传参的一个原因
小试牛刀(以下为面试题)
以下的代码的输出将是什么? 说出你的答案并解释。
class Parent(object):
x = 1
class Child1(Parent):
pass
class Child2(Parent):
pass
print(Parent.x, Child1.x, Child2.x)
Child1.x = 2
print(Parent.x, Child1.x, Child2.x)
Parent.x = 3
print(Parent.x, Child1.x, Child2.x)
复制Error复制成功...
答案, 以上代码的输出是:
1 1 1
1 2 1
3 2 3
使你困惑或是惊奇的是关于最后一行的输出是 3 2 3 而不是 3 2 1。为什么改变了 Parent.x 的值还会改变 Child2.x 的值,但是同时 Child1.x 值却没有改变?
这个答案的关键是,在 Python 中,类变量在内部是作为字典处理的。如果一个变量的名字没有在当前类的字典中发现,将搜索祖先类(比如父类)直到被引用的变量名被找到(如果这个被引用的变量名既没有在自己所在的类又没有在祖先类中找到,会引发一个 AttributeError 异常 )。
因此,在父类中设置 x = 1 会使得类变量 x 在引用该类和其任何子类中的值为 1。这就是因为第一个 print 语句的输出是 1 1 1。
随后,如果任何它的子类重写了该值(例如,我们执行语句 Child1.x = 2),然后,该值仅仅在子类中被改变。这就是为什么第二个 print 语句的输出是 1 2 1。
最后,如果该值在父类中被改变(例如,我们执行语句 Parent.x = 3),这个改变会影响到任何未重写该值的子类当中的值(在这个示例中被影响的子类是 Child2)。这就是为什么第三个 print 输出是 3 2 3。