现在关注几个函数和类的进阶概念:迭代器,生成器,闭包,装饰器等。
迭代器 Python 内置的列表等可以支持 for 遍历,本质上是因为列表等提供了迭代所需要的接口,我们可以让自定义类也支持迭代遍历。
首先,for 遍历的实质,以下面的例子说明
1 2 3 4 5 6 7 8 9 s = 'abcd' it = iter (s) print (next (it)) print (next (it)) print (next (it)) print (next (it)) print (next (it))
对于一个自定义容器类 Demo,我们希望它支持迭代器遍历,那么需要实现如下的内容:
容器类(或者说可迭代对象)提供 Demo.__iter__ 方法,返回一个迭代器类 Iter,迭代器需要记录当前状态
迭代器类提供 Iter.__next__ 方法,返回容器中的元素,并且在迭代完成后抛出 StopIteration 异常,这个异常会被 for 语句自动捕获。(Python 在实现迭代器时,故意通过抛出异常作为结束的标记,异常在这种情景下反而是正常的)
例如一个反向迭代器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class ReverseIter : def __init__ (self,container ): self .data = container.data self .index = len (self .data) def __next__ (self ): if self .index == 0 : raise StopIteration self .index -= 1 return self .data[self .index] class DemoContainer : def __init__ (self,data ): self .data = data def __iter__ (self ): return ReverseIter(self ) for i in DemoContainer('abcde' ): print (i,end=' ' ) ''' e d c b a '''
生成器 含有 yield 关键字的函数就是生成器,可以视作一个函数化的迭代器,或者说将函数变成了一个有状态的可迭代的对象。
在调用生成器函数时,返回的其实不是函数或者它的结果,而是一个生成器对象,生成器自动支持迭代器协议,这个生成器对象是有状态的,可以不断调用 next 来遍历,最终可能抛异常终止;或者直接使用 for 语句进行遍历。
生成器在执行遇到 yield 关键字时抛出结果,暂停,等到下一次调用生成器时从此位置继续运行,直到遇到 yield 或者 return。
例如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 def fibonacci (): cnt = 0 a, b = 0 , 1 while cnt < 10 : cnt += 1 yield b a, b = b, a + b for i in fibonacci(): print (i,end=' ' ) ''' 1 1 2 3 5 8 13 21 34 55 ''' s = fibonacci() print (next (s)) print (next (s)) print (next (s)) print (next (s))
这个例子中,我们可以不提供迭代次数上限,此时不仅可以计算任意长的数列项,而且最重要的是:计算中不需要占用大量内存,只需要维持一个有内部状态的函数对象。(其实用类也可以实现,这里 Python 直接提供是因为对于 Python 解释器而言没有困难,函数也是对象,并不存在真实的调用栈)
注意:
只要含有关键字yield,无论是否会执行到这个语句,整个函数都会变成生成器
对于生成器,它的return只是一个结束标志,它不会把后面的值返回给调用者,而是会被自动识别为StopIteration,即抛出迭代终止的异常
这两点可以通过下面的例子呈现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 def gen_data (num ): if num > 5 : for i in range (num): yield i return "hi" else : return num for num in gen_data(6 ): print (num) for num in gen_data(4 ): print (num)
一个更一般的二阶线性递推数列生成器示例如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 def second_order_sequence (a0, a1, p, q ): """ a_n = p * a_{n-1} + q * a_{n-2} """ yield a0 yield a1 while True : a_next = p * a1 + q * a0 yield a_next a0, a1 = a1, a_next gen = second_order_sequence(1 , 1 , 1 , -2 ) for _ in range (20 ): print (next (gen), end=' ' )
注:yield 语句可以不加返回值,此时 yield 语句会返回 None。
需要说明的是,不建议对迭代器和生成器使用 in 判断成员是否存在,因为它会不断遍历直到终止或找到,并且不会记住已经产生的值,因此实际行为很可能与预期不同。
例如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 it = iter ([1 , 2 , 3 , 4 , 5 ]) print (3 in it) print (5 in it) print (3 in it) def squares (): for i in range (1 , 10 ): yield i * i gen = squares() print (9 in gen) print (16 in gen) print (16 in gen)
with 语句 Python 提供了 with 语句,可以用于简化资源管理,例如文件读写,数据库连接等,在进入和退出过程中自动执行某些操作(通常是获取和释放资源),避免忘记释放资源。
典型的 with 结构如下
1 2 with expression as variable:
大致等价于如下语句,注意无论抛出异常或正常返回,都会执行finally语句
1 2 3 4 5 6 manager = expression variable = manager.__enter__() try : finally : manager.__exit__(*sys.exc_info())
由此也可以看出,with 语句要求 manager 实现 __enter__ 和 __exit__ 方法,对这两个方法的接口和返回值也有一些要求。
例如读写文件
1 2 with open ("data.txt" , "r" ) as f: data = f.read()
相当于
1 2 3 4 5 f = open ("data.txt" , "r" ) try : data = f.read() finally : f.close()
查看源码(_pyio.py)可知,确实就是直接实现了这两个特殊方法,并且在 __exit__ 方法中调用了 close 方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class IOBase (metaclass=abc.ABCMeta): def __enter__ (self ): """Context management protocol. Returns self (an instance of IOBase).""" self ._checkClosed() return self def __exit__ (self, *args ): """Context management protocol. Calls close()""" self .close()
自定义类型实现了对应方法之后,同样也可以支持 with 语句,满足要求的类通常被称为上下文管理器。
例如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class DemoContext : def __enter__ (self ): print ("Entering context" ) return self def __exit__ (self, exc_type, exc_value, traceback ): print ("Exiting context" ) with DemoContext() as f: print ("Inside block" )
一个实际的例子是通过自定义类型和 with 语句实现代码块计时(下面还有一种基于装饰器的做法)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import timeclass Timer : def __init__ (self, name: str | None = None ): self .name = name def __enter__ (self ): self .start = time.perf_counter() return self def __exit__ (self, *args ): self .end = time.perf_counter() self .interval = self .end - self .start if self .name: print (f"[{self.name} ] Elapsed time: {self.interval:.4 f} seconds" ) else : print (f"Elapsed time: {self.interval:.4 f} seconds" ) with Timer(): time.sleep(1.5 ) with Timer("demo" ): time.sleep(1.5 )
输出形如
1 2 Elapsed time: 1.5003 seconds [demo] Elapsed time: 1.5008 seconds
time.perf_counter() 的计时精度比 time.time() 更高,适用于性能测试和基准测量。
闭包
在计算机科学中,闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。
Python 中的闭包概念,以一个例子说明:
1 2 3 4 5 6 7 8 9 10 11 def print_msg (): msg = "hello,world" def printer (): print (msg) return printer another = print_msg() another()
在 Python 中一切都是对象,包括函数也是对象,因此不像 C/C++ 存在严格的函数调用栈,局部变量的生存周期并不需要那么严格,Python 提供了一个延长局部变量生存周期的方式——闭包。
如上例,外层函数 print_msg 和内层函数 printer,内层函数可以访问外层函数的局部变量 msg,我们调用 print_msg 返回内层函数,此时虽然外层函数已经退出,但是内层函数 printer 被获取了,因此内层函数可以访问的 msg 也被保留下来,仍然可以被调用。
Python 在实现上,对每一个函数对象都附带了一个 __closure__ 属性,是记录当前的上下文中自由变量的元组,例如前文中的外层函数和内层函数
1 2 3 4 print_msg.__closure__ another.__closure__
闭包的应用场景:
1 2 3 4 5 6 7 8 9 10 11 12 13 def init (): msg = 'hi' num = 10 def inner_func (): return msg,num return inner_func a = init() x1,x2 = a() print (x1,x2)
对于简单的只有一个方法的类,可以换成闭包来进行等价实现,见下例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Add : def __init__ (self,x ): self .x = x def __call__ (self,y ): return self .x + y a = Add(3 ); print (a(5 )) def add (x ): def inner_func (y ): return x+y return inner_func b = add(3 ); print (b(5 ))
简而言之,闭包是包含上下文的函数,或者说函数以及相应的上下文。 在 C++中也支持实现闭包:通过 lambda 表达式,它可以捕获自己所需的局部变量,以值拷贝或者引用的形式;通过仿函数也是一样的道理。 在 Python 中同样的道理:既可以使用支持调用的自定义类来实现具有上下文的函数,或者直接使用函数闭包功能,由于 Python 函数也是对象,因此实际上函数和类没有太多的区别,存在内部状态的函数就是 Python 的闭包。
装饰器 装饰器可以对函数进行自定义的包装:当一个函数被装饰器装饰时,Python 解释器会将原有函数作为参数传递给装饰器函数,然后将装饰器函数返回的新函数绑定到原有函数的名称上,从而实现对原有函数的增强。
装饰器 @ 其实就是函数闭包的一个语法糖。
装饰器语法如下,其中装饰器 decorator 或者含参数的装饰器 decorator(args)
1 2 3 4 5 6 7 8 9 10 11 @decorator def func (*args,**kwargs ): ... @decorator(args ) def func (*args,**kwargs ): ...
注意:被装饰的函数名 func 是最后的变量名称,但是实际上并不存在 func 重新赋值的过程,Python 不会临时把 func 绑定到被装饰前的函数上。可以理解为存在一个临时的变量名称 func_temp_xxx(不会和已有变量重名)指向了这个函数对象,然后调用装饰器函数
1 2 3 4 5 @decorator def func_temp_xxx (*args,**kwargs ): ... func = decorator(args)(func_temp_xxx)
两个经典情景为函数调用计时和调用日志。下文中的 wrapper(包装器,包装纸)并不是必须的关键字,只是习惯上都会使用这个词。
基础装饰器 例一,使用装饰器对函数调用计时:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import timedef timer (func ): def wrapper (*args, **kwargs ): t1 = time.time() func(*args, **kwargs) t2 = time.time() cost_time = t2-t1 print ("Time cost: {}s" .format (cost_time)) return wrapper @timer def want_sleep (sleep_time ): print ("want_sleep" ) time.sleep(sleep_time) def want_sleep2_temp (sleep_time ): print ("want_sleep2" ) time.sleep(sleep_time) want_sleep2 = timer(want_sleep2_temp) want_sleep(3 ) ''' want_sleep Time cost: 3.0067501068115234s ''' want_sleep2(3 ) ''' want_sleep2 Time cost: 3.010910749435425s '''
例二,自定义调用日志函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 def logger (func ): def wrapper (*args, **kwargs ): print ('[begin {}]' .format (func.__name__)) func(*args, **kwargs) print ('[end {}]' .format (func.__name__)) return wrapper @logger def test (x ): print ("hello, " ,x) test("Alex" ) ''' [begin test] hello, Alex [end test] '''
需要注意的是,在使用装饰器时,函数名以及其它属性等已经发生了改变。 如果希望保留原有的属性信息,可以使用 functools.wraps 装饰器将被装饰函数的属性以原样传递给 wrapper,两种情况的对比如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 from functools import wrapsdef log_before (func ): @wraps(func ) def wrapper (*args, **kwargs ): print (f"Calling {func.__name__} ..." ) return func(*args, **kwargs) return wrapper def log_before_no_wraps (func ): def wrapper (*args, **kwargs ): print (f"Calling {func.__name__} ..." ) return func(*args, **kwargs) return wrapper @log_before def hello1 (name ): print (f"Hello, {name} " ) @log_before_no_wraps def hello2 (name ): print (f"Hello, {name} " ) print (hello1.__name__) print (hello2.__name__)
带参数的装饰器 需要两层嵌套,第一层会读取装饰器参数,然后返回一个无参数装饰器,注意每一层返回的对象。例如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 def logger (level ): def logger_with_level (func ): def wrapper (*args, **kwargs ): print ('[{}][begin {}]' .format (level,func.__name__)) func(*args, **kwargs) print ('[{}][end {}]' .format (level,func.__name__)) return wrapper return logger_with_level @logger("debug" ) def test (x ): print ("hello, " ,x) @logger("info" ) def test2 (x ): print ("hi, " ,x) test("Alex" ) ''' [debug][begin test] hello, Alex [debug][end test] ''' test2("Bob" ) ''' [info][begin test2] hi, Bob [info][end test2] '''
注意:装饰器的作用效果(尤其是含参数时)是在 @XXX 所属的函数被定义的那一刻决定的,对于模块中的,则是在加载的时刻起作用的。因此,必须在被装饰函数定义之前设置参数,在定义之后修改参数是无效的。
基于类的装饰器 前面介绍的都是基于函数的闭包特性而得到的装饰器,装饰器本身是一个函数,但是实际上定义了 __call__ 的自定义类也可以实现同样的效果,并且由于类的接口更多,可以实现更丰富的功能。
如果是不带参数的装饰器,可以使用 __init__ 接收被装饰函数,使用 __call__ 接收函数参数并调用被装饰函数,此时我们不再需要定义或返回 wrapper。
如果是带参数的装饰器,多了一层逻辑,可以使用 __init__ 接收装饰器参数,使用 __call__ 接收被装饰函数,在 __call__ 内部定义 wrapper 并返回。
不带参数的类装饰器比较简单,下面的例子是带参数的调用日志装饰器,可以设置日志等级并过滤输出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Logger : log_level = "DEBUG" level_map = {"DEBUG" :0 ,"INFO" :1 ,"ERROR" :2 } def __init__ (self,level ): self .level = level def __call__ (self,func ): def wrapper_true (*args,**kwargs ): print ('[{}][begin {}]' .format (self .level,func.__name__)) func(*args, **kwargs) print ('[{}][end {}]' .format (self .level,func.__name__)) def wrapper_false (*args,**kwargs ): func(*args, **kwargs) if (Logger.level_map[self .level] >= Logger.level_map[Logger.log_level]): return wrapper_true else : return wrapper_false
需要注意的是,装饰器在函数定义时起作用,因此全局的调用日志等级的设置必须在函数定义之前。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 Logger.log_level = "ERROR" @Logger(level="DEBUG" ) def test1 (x ): print ("hello," ,x) @Logger("INFO" ) def test2 (x ): print ("hi," ,x) @Logger("ERROR" ) def test3 (x ): print ("hey," ,x) test1('Alex' ) test2('Bob' ) test3('John' ) ''' [ERROR][begin test3] hey, John [ERROR][end test3] '''
如上所示,因为全局调用日志等级设置为 ERROR,只有第三个函数调用时触发并生成提示信息。
多个装饰器 装饰器可以复合使用,复合的效果与前后顺序有关,例如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 def say (msg ): def say_msg (func ): def wrapper (*args, **kwargs ): print ("[begin]" ,msg) func(*args, **kwargs) print ("[end]" ,msg) return wrapper return say_msg @ say("hi" ) @ say("hello" ) def test1 (): print ("test1" ) @ say("hello" ) @ say("hi" ) def test2 (): print ("test2" ) test1() ''' [begin] hi [begin] hello test1 [end] hello [end] hi ''' test2() ''' [begin] hello [begin] hi test2 [end] hi [end] hello '''
基于装饰器的单例模式 在 Python 中有很多种方法可以实现单例模式,一个常见的写法是基于装饰器的,这里可以使用装饰器函数,也可以使用装饰器类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 def Singleton (cls ): _instance = {} def get_instance (): print ("1:" ) if cls.__name__ not in _instance: print ("2:" ) _instance[cls.__name__] = cls() return _instance[cls.__name__] return get_instance @Singleton class Demo : def __init__ (self ): print ("3:" ) pass a = Demo() ''' 1: 2: 3: ''' b = Demo() ''' 1: ''' print (id (a) == id (b))
偏函数 Python 的 patrial 函数可以固定函数的某些参数,形成一个新的函数,对含有多个函数的复杂函数简化调用方式。它的实现原理大致如下
1 2 3 4 5 6 7 8 9 10 def partial (func, *args, **keywords ): def newfunc (*fargs, **fkeywords ): newkeywords = keywords.copy() newkeywords.update(fkeywords) return func(*args, *fargs, **newkeywords) newfunc.func = func newfunc.args = args newfunc.keywords = keywords return newfunc
几个例子如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from functools import partialdef add (a,b ): print (f"a={a} ,b={b} ,sum={a+b} " ) add(1 ,2 ) partial(add,1 )(3 ) partial(add,b=1 )(3 ) partial(add,b=1 )(a=2 ,b=3 )
@property property 是一个内置函数,但是通常以装饰器的形式调用,这个函数解决的问题就是 Python 中实例的属性增删读写太过自由,不能方便地对参数进行限制。
property 函数支持将最多 3 个类方法和一个字符串绑定起来,对外统一提供一个伪装属性,property 的参数分别是读取,修改,删除和说明字符串,当然 4 个接口并不是都需要的,可以缺省,常用的就是前两个
1 2 3 4 5 6 7 property (fget=None , fset=None , fdel=None , doc=None )age = property (get_age,set_age,del_age,"This is age" ) age = property (get_age,set_age,del_age) age = property (get_age,set_age) age = property (get_age)
也可以使用如下的写法,每次传递一个参数,装饰器 @property 就是这样调用的。
1 2 3 4 5 6 7 8 9 10 age = property () age = age.getter(get_age) age = age.setter(set_age) age = age.deleter(del_age) age = property (set_age) age = age.setter(set_age) age = age.deleter(del_age)
下面的例子很好地解释了 property,这里我们在内部存储的属性是 _age,为这个属性的读写提供了接口 get_age 和 set_age,然后使用 property 函数将它们绑定起来,对外部提供了一个伪装属性 age。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 class Data : def __init__ (self,age ): self .age = age def get_age (self ): print ("call get_age" ) return self ._age def set_age (self,age ): print ("call set_age" ) self ._age = age def del_age (self ): print ("call del_age" ) del self ._age age = property (get_age,set_age,del_age) a = Data(1 ) print (a.age)a.age = 2 print (a.age)del a.age
或者,我们可以使用装饰器实现,这里把所有的相关接口都使用 age 这个名称,由于 Python 不会立即把被装饰函数绑定到函数名,因此并不会导致重名覆盖问题。按照 property 的顺序,要求 @property 对应的是读取接口,并且出现在最前面。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 class Data : def __init__ (self,age ): self .age = age @property def age (self ): print ("call get_age" ) return self ._age @age.setter def age (self,age ): print ("call set_age" ) self ._age = age @age.deleter def age (self ): print ("call del_age" ) del self ._age a = Data(1 ) print (a.age)a.age = 2 print (a.age)del a.age
通过这里 property 提供的伪属性,我们可以让类达成如下几个效果
只读不可修改的属性:设置 property 但是不设置 setter
不可删除的属性:让 deleter 绑定的接口总是触发异常
注意:在__init__方法中,我们很容易直接写为self._age = age,这会导致初始化时的赋值直接绕过setter,可能导致_age属性处于非法值的状态。
类方法与静态方法 在这一部分,我们区分类的不同方法:
普通方法(实例方法)(默认的方法,不使用任何装饰器)
类方法(基于 @classmethod 装饰器)
静态方法(基于 @staticmethod 装饰器)
它们都可以通过类名或实例名调用,但是对于参数的处理逻辑不同:
对于实例方法,第一个参数建议是 self,代表实例对象,在方法内部使用self.xxx读写的是实例对象的属性
以类名调用时,第一个参数 self 需要提供一个实例对象;
以实例名调用时,会将当前实例对象传入作为第一个参数(通常即self)
对于类方法,第一个参数建议是 cls,代表类对象,在方法内部使用cls.xxx或<className>.xxx读写的都是类对象的属性,无论以类名还是实例名调用,都要求缺省第一个参数 cls,会将类对象或者实例所属的类对象传入作为第一个参数(通常即cls),
对于静态方法,不需要任何特殊参数,也不会进行任何的自动传参,相当于一个普通函数,以类名还是实例调用没有区别
对于类方法和静态方法,虽然使用类名和实例名调用没有区别,但还是建议使用类名调用。
例如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 class Demo : def f1 (self,v ): print ("call normal method" ) print (f"self={self} ,v={v} " ) @classmethod def f2 (cls,v ): print ("call class method" ) print (f"cls={cls} ,v={v} " ) @staticmethod def f3 (v ): print ("call static method" ) print (f"v={v} " ) a = Demo() a.f1(1 ) Demo.f1(a,1 ) ''' call normal method self=<__main__.Demo object at 0x00000237F340AD90>,v=1 call normal method self=<__main__.Demo object at 0x00000237F340AD90>,v=1 ''' a.f2(2 ) Demo.f2(2 ) ''' call class method cls=<class '__main__.Demo'>,v=2 call class method cls=<class '__main__.Demo'>,v=2 ''' a.f3(3 ) Demo.f3(3 ) ''' call static method v=3 call static method v=3 '''
推导式 列表推导式的格式为
1 2 3 4 5 [out_exp_res for out_exp in input_list] or [out_exp_res for out_exp in input_list if condition]
例如
1 2 3 4 lis = ['Bob' ,'Tom' ,'alice' ,'Jerry' ,'Wendy' ,'Smith' ] lis2 = [name.upper() for name in lis if len (name)>3 ] print (lis2)
字典推导式的格式为
1 2 3 4 5 { key_expr: value_expr for value in collection } or { key_expr: value_expr for value in collection if condition }
例如
1 2 3 4 lis = ['Google' ,'Runoob' , 'Taobao' ] dic = {key:len (key) for key in lis} print (dic)
集合推导式的格式为
1 2 3 4 5 { expression for item in Sequence } or { expression for item in Sequence if conditional }
例如
1 2 3 s = {i**2 for i in (1 ,2 ,3 )} print (s)
元组推导式(或者说生成器表达式 )的格式为
1 2 3 4 5 (expression for item in Sequence ) or (expression for item in Sequence if conditional )
注意它返回的不是一个元组,而是一个生成器对象! 因此和列表推导式不同,它是惰性求值的,并不会把所有项立刻计算出来,可以使用 tuple 转化为元组
例如
1 2 3 4 5 a = (x for x in range (1 ,10 )) print (a)print (tuple (a))
反射 Python 作为一个典型的动态语言,提供了一些内建函数(如 getattr()、setattr()、hasattr() 等)来支持反射机制。
什么是反射?对于 Python,Java 这样的动态语言来说,反射简单来说就是可以使用字符串(例如类名,属性或方法名称)去获取、访问和修改对象的属性和方法等的机制,这通常是通过类型擦除以及运行时的某种查表操作实现的。
反射的优缺点都很明显:优点是非常灵活,可以用来实现插件系统等;缺点是反射存在安全问题和运行时的性能问题(通过反射调用比直接调用慢)。
下面以几个例子说明:考虑一个简单的自定义类型
1 2 3 4 5 6 class Person : def __init__ (self, name ): self .name = name def greet (self ): print (f"Hello, my name is {self.name} " )
通过 getattr() 可以动态获取方法
1 2 3 4 person = Person("John" ) method = getattr (person, "greet" ) method()
通过 setattr() 可以动态修改属性,包括修改已存在的属性的值,以及添加新属性
1 2 3 4 5 6 7 8 9 person = Person("John" ) print (person.name) setattr (person, "name" , "Alice" )print (person.name) setattr (person, "age" , 10 )print (person.age)
通过 hasattr() 可以动态检查属性是否存在
1 2 3 person = Person("John" ) print (hasattr (person, "greet" )) print (hasattr (person, "age" ))
对于 C++ 来说,虽然反射的需求很强烈,但是目前最新的C++23仍然不支持反射,未来可能会支持。与动态语言的反射(称为动态反射)不同,C++ 所追求的反射是发生在编译期的(称为静态反射),通常不会给运行期带来额外的性能开销。
补充 Python 提供了一些内置的装饰器,例如 @functools.lru_cache 可以缓存函数的调用结果,避免重复计算,这里的 LRU 是缓存策略 Least Recently Used,即最近最少使用策略:当缓存达到上限时,会淘汰最长时间未被访问的缓存项。
例如可以给 fibonacci 函数添加缓存
1 2 3 4 5 6 7 8 9 10 11 def fib_no_cache (n: int ) -> int : if n < 2 : return n return fib_no_cache(n - 1 ) + fib_no_cache(n - 2 ) @functools.lru_cache(maxsize=None ) def fib_cached (n: int ) -> int : if n < 2 : return n return fib_cached(n - 1 ) + fib_cached(n - 2 )
对于递归函数,缓存的效果是非常明显的,尤其对 Python 更是如此,例如这里 n 取 40 时,无缓存的耗时约 10 秒,带缓存的耗时仅为 0.0001 秒,可以忽略不计。
实际上,除了使用内置的 @functools.lru_cache 装饰器,也可以手动加上缓存(测试发现比 lru_cache 快一点)
1 2 3 4 5 6 7 8 9 10 11 def fib_man_cache (n: int , cache=None ) -> int : if cache is None : cache = {} if n in cache: return cache[n] if n < 2 : cache[n] = n return n result = fib_man_cache(n - 1 , cache) + fib_man_cache(n - 2 , cache) cache[n] = result return result
测试代码如下(使用了上文定义的计时器)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 n = 40 print (f"n = {n} " )with Timer("No cache" ): print (fib_no_cache(n)) with Timer("lru_Cached" ): print (fib_cached(n)) with Timer("Manual cache" ): print (fib_man_cache(n)) n = 500 print (f"n = {n} " )with Timer("lru_Cached" ): print (fib_cached(n)) with Timer("Manual cache" ): print (fib_man_cache(n))
@contextlib.contextmanager 为了实现上下文管理,定义一个满足要求的自定义类型有点复杂,因为需求只是在进入和退出时执行一次特定代码,Python 提供的内置装饰器 @contextlib.contextmanager 可以利用生成器函数的 yield 语句,实现对应版本的上下文管理器。
@contextlib.contextmanager 可以把生成器函数变成上下文管理器:
yield 可以无返回值,如果有返回值,则会绑定给 as 绑定的变量;
yield 之前的代码相当于 __enter__ 方法的内容,在进入上下文时执行;
yield 之后的代码相当于 __exit__ 方法的内容,在退出上下文时执行。
例如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import contextlib@contextlib.contextmanager def my_context (): print ("Enter context" ) yield "hi" print ("Exit context" ) with my_context() as x: print ("Inside with block" ) print (f"{x=} " )
例如前面的计时器可以用 @contextlib.contextmanager 来实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import timeimport contextlib@contextlib.contextmanager def timer (name: str | None = None ): start = time.perf_counter() try : yield finally : end = time.perf_counter() interval = end - start if name: print (f"[{name} ] Elapsed time: {interval:.4 f} seconds" ) else : print (f"Elapsed time: {interval:.4 f} seconds" )
@dataclass Python 的自定义类型有时并不好用,尤其是我们只需要一个简单的类来保存一些数据时。
例如一个学生信息类
1 2 3 4 5 6 7 8 class Student : def __init__ (self, idnumber: int , name: str , age: int , gender: str ): self .idnumber = idnumber self .name = name self .age = age self .gender = gender tom1 = Student(1 , "tom" , 22 , "male" )
为了便于检查,我们希望这个数据类可以支持 print 函数展示信息,以及判断相等(所有属性都相等),需要手动实现__repr__和__eq__方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class Student : def __init__ (self, idnumber: int , name: str , age: int , gender: str ): self .idnumber = idnumber self .name = name self .age = age self .gender = gender def __repr__ (self ): return f"Student(idnumber={self.idnumber!r} , name={self.name!r} , age={self.age!r} , gender={self.gender!r} )" def __eq__ (self, other ): if not isinstance (other, Student): return NotImplemented return ( self .idnumber == other.idnumber and self .name == other.name and self .age == other.age and self .gender == other.gender ) tom1 = Student(1 , "tom" , 22 , "male" ) print (tom1)tom2 = Student(2 , "Tom" , 22 , "male" ) print (tom2)print (tom1 == tom2)
这个过程太复杂了,标准库提供的 @dataclasses.dataclass 可以极大地简化上述代码(必须给每个属性提供类型)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import dataclasses@dataclasses.dataclass class Student : idnumber: int name: str age: int gender: str tom1 = Student(1 , "tom" , 22 , "male" ) print (tom1)tom2 = Student(2 , "Tom" , 22 , "male" ) print (tom2)print (tom1 == tom2)
下面用例子来说明 @dataclasses.dataclass 的更多用法:
可以给属性提供默认值,在构造时可以缺省
属性的值仍然可以修改,两个实例相等当且仅当所有属性的值相等
可以用 dataclasses.asdict 或 dataclasses.astuple 将 dataclass 实例转换为字典或元组(支持递归)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @dataclasses.dataclass class Point : x: float = 0.0 y: float = 0.0 p = Point() print (p) q = Point(y=1.0 ) print (q) q.y = 0.0 print (q) print (p == q) print (dataclasses.asdict(p)) print (dataclasses.astuple(p))
给装饰器加上frozen=True后,属性值就变成了只读,这是一个常见的需求
1 2 3 4 5 6 7 8 @dataclasses.dataclass(frozen=True ) class ImmutablePoint : x: float y: float p = ImmutablePoint(1 , 2 ) p.x = 1