-----------支持作者请转发本文-----------
李宁老师已经在「极客起源」 微信公众号推出《Python编程思想》电子书,囊括了Python的核心技术,以及Python的主要函数库的使用方法。读者可以在「极客起源」 公众号中输入 160442 开始学习。
-----------正文-----------
方法是类或对象行为的抽象,但 Python的方法本章上也是函数,其定义方式、调用方式和函数都非常相似,因此 Python的方法并不仅仅是单纯的方法,它与函数也有莫大的关系。
1. 在类中调用实例方法
在前面的文章讲过,在 Python的类体中定义的方法默认都是实例方法,前面也示范了通过对象来调用实例方法。但要提醒大家的是,Python的类在很大程度上是一个命名空间。当程序在类体中定义变量、定义方法时,与前面介绍的定义变量、定义函数其实并没有太大的不同。对比如下代码。
示例代码:class_demo1.py
# 定义全局空间的test函数
def test ():
print("全局空间的test方法")
# 全局空间的name变量
name = 'Bill'
class Dog:
# 定义Dog空间的run函数
def run():
print("Dog空间的run方法")
# 定义Bird空间的bar变量
value = 123
# 调用全局空间的函数和变量
test()
print(name)
# 调用Bird空间的函数和变量
Dog.run()
print(Dog.value)
上面代码在全局空间和Dog类(Dog空间)中分别定义了test函数和name变量,从定义它们的代码来看,几乎没有任何区别,只是在Dog类中定义它们时需要缩进。
接下来程序在调用Dog空间内的value变量和run函数(方法)时,只要添加Dog.前缀即可,这说明完全可以通过Dog类来调用run函数(方法)。
现在问题来了,如果使用类调用实例方法,那么该方法的第一个参数(self)怎么自动绑定呢?
例如如下程序:
示例代码:class_demo2.py
class Person:
def run (self):
print(self, '正在跑步...')
# 通过类调用实例方法
Person.run()
运行这段代码,程序会抛出如下异常:
TypeError: run() missing 1 required positional argument: 'self'
在这段代码中,run方法缺少传入的self参数,所以导致程序出错。这说明在使用类调用实例方法时, Python不会自动为第1个参数绑定调用者。实际上也没法自动绑定,因此实例方法的调用者是类本身,而不是对象。
如果程序依然希望使用类来调用实例方法,则必须手动为方法的第1个参数传入参数值。例如,使用下面的代码:
class Person:
def run (self):
print(self, '正在跑步...')
# 通过类调用实例方法
# Person.run()
person = Person()
# 显式为方法的第一个参数绑定参数值
Person.run(person)
这段代码显式地为 run方法的第1个参数绑定了参数值,这样的调用效果完全等同于执行 person.run()方法。实际上,当通过Person类调用run实例方法时, Python只要求手动为第1个参数绑定参数值,并不要求必须绑定Person对象,因此也可使用如下代码进行调用。
# 显式地为方法的第一个参数绑定Python字符串参数值
Person.run('Python')
如果按上面方式进行绑定,那么Python字符串就会被传给run()方法的第1个参数self。因此,运行上面代码,将会看到如下输出结果:
Python 正在跑步...
Python的类可以调用实例方法,但使用类调用实例方法时,Python不会自动为方法的第1个参数self绑定参数值。程序必须显式地为第1个参数self传入方法调用者。这种调用方式被称为“未绑定调用”。
2. 类方法与静态方法
实际上, Python完全支持定义类方法,甚至支持定义静态方法。Python的类方法和静态方法类似,它们都推荐使用类来调用(其实也可使用对象来调用)。类方法和静态方法的区别:Python会自动绑定类方法的第1个参数,类方法的第1个参数(通常建议参数名为cls)会自动绑定到类本身。但对于静态方法则不会自动绑定。
使用@ classmethod修饰的方法就是类方法,使用@ staticmethod修饰的方法就是静态方法。
下面代码演示了定义类方法和静态方法。
示例代码:class_static_method.py
class Pandas:
# classmethod修饰的方法是类方法
@classmethod
def run (cls):
print('类方法run: ', cls)
# staticmethod修饰的方法是静态方法
@staticmethod
def printName (p):
print('静态方法info: ', p)
# 调用类方法,Dog类会自动绑定到第一个参数
Pandas.run()
# 调用静态方法,不会自动绑定,因此程序必须手动绑定第1个参数
Pandas.printName('小团子')
# 创建Bird对象
p = Pandas()
# 使用对象调用run()类方法,其实依然还是使用类调用,
# 因此第1个参数依然被自动绑定到Pandas类
p.run()
# 使用对象调用printName静态方法,其实依然还是使用类调用,
# 因此程序必须为第一个参数执行绑定
p.printName('小团子')
从这段代码可以看出,使用@classmethod修饰的方法是类方法,该类方法定义了一个cls参数,该参数会被自动绑定到Pandas类本身,不管程序是使用类还是对象调用该方法,Python始终都会将类方法的第1个参数绑定到类本身。
这段代码还使用 @staticmethod定义了一个静态方法,程序同样既可使用类调用静态方法,也可使用对象调用静态方法,不管用哪种方式调用,Python都不会为静态方法执行自动绑定。
在使用 Python编程时,一般不需要使用类方法或静态方法,程序完全可以使用函数来代替类方法或静态方法。但是在特殊的场景(例如,使用工厂模式)下,类方法或静态方法也是不错的选择。
3. 函数装饰器
前面介绍的@staticmethod和@classmethod的本质就是函数装饰器,其中 staticmethod和classmethod都是 Python内置的函数。
使用@符号引用已有的函数(比如@staticmethod和@classmethod)后,可用于修饰其他函数,装饰被修饰的函数。那么我们是否可以开发自定义的函数装饰器呢?答案是肯定的。
当程序使用“@函数”(比如函数X)装饰另一个函数(比如函数Y)时,实际上完成如下两步:
(1) 将被修饰的函数(函数Y)作为参数传给@符号引用的函数(函数A);
(2)将函数Y替换(装饰)成第(1)步的返回值;
从上面介绍不难看出,被“@函数”修饰的函数不再是原来的函数,而是被替换成一个新的东西。
为了让大家更清楚函数装饰器的作用,下面看一个非常简单的示例。
示例代码:decorator_demo.py
def funX(fn):
print('X')
fn() # 执行传入的fn参数
return 'Python'
'''
下面装饰效果相当于:funX(funY),
funY将会替换(装饰)成该语句的返回值;由于funX()函数返回Python,因此funB就是Python
'''
@funX
def funY():
print('funY')
print(funY) # Python
上面程序使用@funX修饰funY,这意味着程序要完成如下两步操作:
(1)将funY作为 funX的参数,也就是相当于执行funX(funY);
(2) 将funY替换成第(1)步执行的结果,funX()执行完成后返回Python,因此funY就不再是函数,而是被替换成一个字符串;
运行这段代码,可以看到如下输出结果:
X funY Python
通过这个例子,相信读者对函数装饰器的执行关系已经有了一个较为清晰的认识,但读者可能会产生另一个疑问:这个函数装饰器导致被修饰的函数变成了字符串,那么函数装饰器有什么用?
别忘记了,被修饰的函数总是被替换成@符号所引用的函数的返回值,因此被修饰的函数会变成什么,完全由@符号所引用的函数的返回值决定。如果@符号所引用的函数的返回值是函数,那么被修饰的函数在替换之后还是函数。
下面程序演示了更复杂的函数装饰器(接前面的程序)。
def process(fn):
# 定义一个嵌套函数
def print_info(*args):
print('-------1-------', args)
n = args[0]
print('-------2-------', n ** 3)
# 查看传给process函数的fn函数
print(fn.__name__)
fn(n * (n + 1))
print("*" * 20)
return fn(n * (n - 1))
return print_info
'''
下面装饰效果相当于:process(my_value),
my_value将会替换(装饰)成该语句的返回值;由于process()函数返回print_info函数,因此funY就是print_info
'''
@process
def my_value(a):
print("-----my_value函数------", a)
# 打印my_value函数,将看到实际上是bar函数
print(my_value) #
# 下面代码看上去是调用my_value(),其实是调用print_info()函数
my_value(10)
my_value(6, 5)
上面程序定义了一个装饰器函数process,该函数执行完成后并不是返回普通值,而是返回print_info函数(这是关键),这意味着被该@process修饰的函数最终都会被替换成print_info函数。
上面程序使用@process修饰 my_value()函数,因此程序同样会执行process(my_value),并将 my_value替换成process函数的返回值print_info函数。所以,在这段代码中打印 my_value函数时,实际上输出的是print_info函数,这说明my_value已经被替换成print_info函数。接下来程序两次调用 my_value函数,实际上就是调用print_info函数。
运行上面程序,可以看到如下输出结果:
<function process.<locals>.print_info at 0x7fb4880a55f0>
-------1------- (10,)
-------2------- 1000
my_value
-----my_value函数------ 110
********************
-----my_value函数------ 90
-------1------- (6, 5)
-------2------- 216
my_value
-----my_value函数------ 42
********************
-----my_value函数------ 30
通过@符号来修饰函数是 Python的一个非常实用的功能,它既可以在被修饰函数的前面添加些额外的处理逻辑(比如权限检查),也可以在被修饰函数的后面添加一些额外的处理逻辑(比如记录日志),还可以在目标方法抛出异常时进行一些修复操作。这种改变不需要修改被修饰函数的代码,只要增加一个修饰即可。
其实前面介绍的这种在被修饰函数之前、之后、拋出异常后增加某种处理逻辑的方式,就是其他编程语言中的AOP( Aspect Orient Programming,面向切面编程)。
下面例子示范了如何通过函数装饰器为函数添加权限检查的功能。程序代码如下:
示例代码:auth_demo.py
def auth(fn):
def verify_auth(*args):
# 用一条语句模拟执行权限检查
print("----模拟执行权限检查----")
# 回调要装饰的目标函数
fn(*args)
return verify_auth
@auth
def test(a, b,c,d):
print(f"执行test函数,参数a: {a}, 参数b: {b}, 参数d: {c},参数d: {d}" )
# 调用test()函数,其实是调用装饰后返回的verify_auth函数
test(123, 55, 135,66)
上面程序使用@auth修饰了test()函数,这会使得 test()函数被替换成 auth函数所返回的 verify_auth函数,而 verify_auth函数的执行流程如下:
(1)先执行权限检查;
(2)回调被修饰的目标函数;
也就是说,verify_auth函数就为被修饰函数添加了一个权限检查的功能。运行该程序,,可以看到如下输出结果:
----模拟执行权限检查----
执行test函数,参数a: 123, 参数b: 55, 参数d: 135,参数d: 66
-----------支持作者请转发本文-----------