专栏首页极客起源Python编程思想(25):方法深度解析

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

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

李宁老师已经在「极客起源」 微信公众号推出《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

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

本文分享自微信公众号 - 极客起源(geekculture),作者:geekori

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-06-24

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Python编程思想(3):数字及其相关运算

    Python 提供了三种数值类型:int(整型),float(浮点型)和complex(复数)。

    蒙娜丽宁
  • Python编程思想(16):压缩与排序列表

    使用zip()函数可以把两个列表“压缩”成一个zip对象(可迭代对象),这样可以使用一个循环并行遍历两个列表。为了测试zip()函数的功能,可以使用下面的代码:

    蒙娜丽宁
  • Python多继承、super与MRO算法

    本文主要以Python3.x为例讲解Python多继承、super以及MRO算法。

    蒙娜丽宁
  • 走近 Python (类比 JS)

    Python 是一门运用很广泛的语言,自动化脚本、爬虫,甚至在深度学习领域也都有 Python 的身影。作为一名前端开发者,也了解 ES6 中的很多特性借鉴自 ...

    牧云云
  • 走近 Python (类比 JS)

    Python 是一门运用很广泛的语言,自动化脚本、爬虫,甚至在深度学习领域也都有 Python 的身影。作为一名前端开发者,也了解 ES6 中的很多特性借鉴自 ...

    小莹莹
  • 2组语法,1个函数,教你学会用Python做数据分析!

    大家好,我是大鹏,城市数据团联合发起人,致力于Python数据分析、数据可视化的应用与教学。

    小小詹同学
  • 手把手教你用Python爬中国电影票房数据

    和很多同学接触过程中,我发现自学Python数据分析的一个难点是资料繁多,过于复杂。大部分网上的资料总是从Python语法教起,夹杂着大量Python开发的知识...

    挖数
  • 1小时学Python,看这篇就够了

    大家好,我是大鹏,城市数据团联合发起人,致力于Python数据分析、数据可视化的应用与教学。

    商业新知
  • 自学 Python 只需要这3步

    大家好,我是大鹏,城市数据团联合发起人,致力于Python数据分析、数据可视化的应用与教学。

    昱良
  • 走近 Python (类比 JS)

    Python 是一门运用很广泛的语言,自动化脚本、爬虫,甚至在深度学习领域也都有 Python 的身影。作为一名前端开发者,也了解 ES6 中的很多特性借鉴自 ...

    程序员宝库

扫码关注云+社区

领取腾讯云代金券