前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Python编程思想(25):方法深度解析

Python编程思想(25):方法深度解析

作者头像
蒙娜丽宁
发布2020-07-02 16:39:49
6050
发布2020-07-02 16:39:49
举报
文章被收录于专栏:极客起源

-----------支持作者请转发本文-----------

李宁老师已经在「极客起源」 微信公众号推出《Python编程思想》电子书,囊括了Python的核心技术,以及Python的主要函数库的使用方法。读者可以在「极客起源」 公众号中输入 160442 开始学习。

-----------正文-----------

方法是类或对象行为的抽象,但 Python的方法本章上也是函数,其定义方式、调用方式和函数都非常相似,因此 Python的方法并不仅仅是单纯的方法,它与函数也有莫大的关系。

1. 在类中调用实例方法

在前面的文章讲过,在 Python的类体中定义的方法默认都是实例方法,前面也示范了通过对象来调用实例方法。但要提醒大家的是,Python的类在很大程度上是一个命名空间。当程序在类体中定义变量、定义方法时,与前面介绍的定义变量、定义函数其实并没有太大的不同。对比如下代码。

示例代码:class_demo1.py

代码语言:javascript
复制
# 定义全局空间的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

代码语言:javascript
复制
class Person:
    def run (self):
        print(self, '正在跑步...')
# 通过类调用实例方法
Person.run()

运行这段代码,程序会抛出如下异常:

代码语言:javascript
复制
TypeError: run() missing 1 required positional argument: 'self'

在这段代码中,run方法缺少传入的self参数,所以导致程序出错。这说明在使用类调用实例方法时, Python不会自动为第1个参数绑定调用者。实际上也没法自动绑定,因此实例方法的调用者是类本身,而不是对象。

如果程序依然希望使用类来调用实例方法,则必须手动为方法的第1个参数传入参数值。例如,使用下面的代码:

代码语言:javascript
复制
class Person:
    def run (self):
        print(self, '正在跑步...')
# 通过类调用实例方法
# Person.run()
person = Person()
# 显式为方法的第一个参数绑定参数值
Person.run(person)

这段代码显式地为 run方法的第1个参数绑定了参数值,这样的调用效果完全等同于执行 person.run()方法。实际上,当通过Person类调用run实例方法时, Python只要求手动为第1个参数绑定参数值,并不要求必须绑定Person对象,因此也可使用如下代码进行调用。

代码语言:javascript
复制
# 显式地为方法的第一个参数绑定Python字符串参数值
Person.run('Python')

如果按上面方式进行绑定,那么Python字符串就会被传给run()方法的第1个参数self。因此,运行上面代码,将会看到如下输出结果:

代码语言:javascript
复制
Python 正在跑步...

Python的类可以调用实例方法,但使用类调用实例方法时,Python不会自动为方法的第1个参数self绑定参数值。程序必须显式地为第1个参数self传入方法调用者。这种调用方式被称为“未绑定调用”。

2. 类方法与静态方法

实际上, Python完全支持定义类方法,甚至支持定义静态方法。Python的类方法和静态方法类似,它们都推荐使用类来调用(其实也可使用对象来调用)。类方法和静态方法的区别:Python会自动绑定类方法的第1个参数,类方法的第1个参数(通常建议参数名为cls)会自动绑定到类本身。但对于静态方法则不会自动绑定。

使用@ classmethod修饰的方法就是类方法,使用@ staticmethod修饰的方法就是静态方法。

下面代码演示了定义类方法和静态方法。

示例代码:class_static_method.py

代码语言:javascript
复制
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

代码语言:javascript
复制
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

通过这个例子,相信读者对函数装饰器的执行关系已经有了一个较为清晰的认识,但读者可能会产生另一个疑问:这个函数装饰器导致被修饰的函数变成了字符串,那么函数装饰器有什么用?

别忘记了,被修饰的函数总是被替换成@符号所引用的函数的返回值,因此被修饰的函数会变成什么,完全由@符号所引用的函数的返回值决定。如果@符号所引用的函数的返回值是函数,那么被修饰的函数在替换之后还是函数。

下面程序演示了更复杂的函数装饰器(接前面的程序)。

代码语言:javascript
复制
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函数。

运行上面程序,可以看到如下输出结果:

代码语言:javascript
复制
<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

代码语言:javascript
复制
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函数就为被修饰函数添加了一个权限检查的功能。运行该程序,,可以看到如下输出结果:

代码语言:javascript
复制
----模拟执行权限检查----
执行test函数,参数a: 123, 参数b: 55, 参数d: 135,参数d: 66

-----------支持作者请转发本文-----------

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-06-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 极客起源 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档