当前位置: 首页 > news >正文

Python 的“self“参数是什么?

让我们从我们已经知道的开始:self - 方法中的第一个参数 - 指的是类实例:

class MyClass:
                  ┌─────────────────┐
                  ▼                 │
    def do_stuff(self, some_arg):   │
        print(some_arg)  ▲          │
                         │          │
                         │          │
                         │          │
                         │          │
instance = MyClass()     │          │
instance.do_stuff("whatever")       │
    │                               │
    └───────────────────────────────┘

此外,这个论点实际上不必称为 self - 它只是一个约定。例如,你可以像其他语言中常见的那样使用它。

上面的代码可能是自然而明显的,因为你一直在使用,但是我们只给了 .do_stuff() 一个参数 (some_arg),但该方法声明了两个 (self 和 , some_arg),好像也说不通。片段中的箭头显示 self 被翻译成实例,但它是如何真正传递的呢?

instance = MyClass()
MyClass.do_stuff(instance, "whatever")

Python 在内部所做的是将 instance.do_stuff("whatever") 转换为 MyClass.do_stuff(instance, "whatever")。我们可以在这里称之为“Python 魔法”,但如果我们想真正了解幕后发生的事情,我们需要了解 Python 方法是什么以及它们与函数的关系。

类属性/方法

在 Python 中,没有“方法”对象之类的东西——实际上方法只是常规函数。函数和方法之间的区别在于,方法是在类的命名空间中定义的,使它们成为该类的属性。

这些属性存储在类字典 __dict__ 中,我们可以直接访问或使用 vars 内置函数访问:

MyClass.__dict__["do_stuff"]
# <function MyClass.do_stuff at 0x7f132b73d550>
vars(MyClass)["do_stuff"]
# <function MyClass.do_stuff at 0x7f132b73d550>

访问它们的最常见方法是“类方法”方式:

print(MyClass.do_stuff)
# <function MyClass.do_stuff at 0x7f132b73d550>

在这里,我们使用类属性访问该函数,正如预期的那样打印 do_stuff 是 MyClass 的函数。然而,我们也可以使用实例属性访问它:

print(instance.do_stuff)
# <bound method MyClass.do_stuff of <__main__.MyClass object at 0x7ff80c78de50>

但在这种情况下,我们得到的是一个“绑定方法”而不是原始函数。Python 在这里为我们所做的是,它将类属性绑定到实例,创建了所谓的“绑定方法”。这个“绑定方法”是底层函数的包装,该函数已经将实例作为第一个参数(self)插入。

因此,方法是普通函数,它们的其他参数前附加了类实例(self)。

要了解这是如何发生的,我们需要看一下描述符协议。

描述符协议

描述符是方法背后的机制,它们是定义 __get__()、__set__() 或 __delete__() 方法的对象(类)。为了理解 self 是如何工作的,我们只考虑 __get__(),它有一个签名:

descr.__get__(self, instance, type=None) -> value

但是 __get__() 方法实际上做了什么?它允许我们自定义类中的属性查找 - 或者换句话说 - 自定义使用点符号访问类属性时发生的情况。考虑到方法实际上只是类的属性,这非常有用。这意味着我们可以使用 __get__ 方法来创建一个类的“绑定方法”。

为了让它更容易理解,让我们通过使用描述符实现一个“方法”来演示这一点。首先,我们创建一个函数对象的纯 Python 实现:

import types
class Function:
    def __get__(self, instance, objtype=None):
        if instance is None:
            return self
        return types.MethodType(self, instance)
    def __call__(self):
        return

上面的 Function 类实现了 __get__ ,这使它成为一个描述符。这个特殊方法在实例参数中接收类实例 - 如果这个参数是 None,我们知道 __get__ 方法是直接从一个类(例如 MyClass.do_stuff)调用的,所以我们只返回 self。但是,如果它是从类实例中调用的,例如 instance.do_stuff,那么我们返回 types.MethodType,这是一种手动创建“绑定方法”的方式。

此外,我们还提供了 __call__ 特殊方法。__init__ 是在调用类来初始化实例时调用的(例如 instance = MyClass()),而 __call__ 是在调用实例时调用的(例如 instance())。我们需要用这个,是因为 types.MethodType(self, instance) 中的 self 必须是可调用的。

现在我们有了自己的函数实现,我们可以使用它将方法绑定到类:

class MyClass:
    do_stuff = Function()
print(MyClass.__dict__["do_stuff"])  # __get__ not invoked
# <__main__.Function object at 0x7f229b046e50>
print(MyClass.do_stuff)  # __get__ invoked, but "instance" is None, "self" is returned
print(MyClass.do_stuff.__get__(None, MyClass))
# <__main__.Function object at 0x7f229b046e50>
instance = MyClass()
print(instance.do_stuff)  #  __get__ invoked and "instance" is not None, "MethodType" is returned
print(instance.do_stuff.__get__(instance, MyClass))
# <bound method ? of <__main__.MyClass object at 0x7fd526a33d30>

通过给 MyClass 一个 Function 类型的属性 do_stuff,我们大致模拟了 Python 在类的命名空间中定义方法时所做的事情。

综上所述,在instance.do_stuff等属性访问时,do_stuff在instance的属性字典(__dict__)中查找。如果 do_stuff 定义了 __get__ 方法,则调用 do_stuff.__get__ ,最终调用:

# For class invocation:
print(MyClass.__dict__['do_stuff'].__get__(None, MyClass))
# <__main__.Function object at 0x7f229b046e50>
# For instance invocation:
print(MyClass.__dict__['do_stuff'].__get__(instance, MyClass))
# Alternatively:
print(type(instance).__dict__['do_stuff'].__get__(instance, type(instance)))
# <bound method ? of <__main__.MyClass object at 0x7fd526a33d30>

正如我们现在所知 - 将返回一个绑定方法 - 一个围绕原始函数的可调用包装器,它的参数前面有 self !

如果想进一步探索这一点,可以类似地实现静态和类方法(https://docs.python.org/3.7/howto/descriptor.html#static-methods-and-class-methods)

为什么self在方法定义中?

我们现在知道它是如何工作的,但还有一个更哲学的问题——“为什么它必须出现在方法定义中?”

显式 self 方法参数是有争议的设计选择,但它是一种有利于简单性的选择。

Python 的自我体现了“越差越好”的设计理念——在此处进行了描述。这种设计理念的优先级是“简单”,定义为:

设计必须简单,包括实现和接口。实现比接口简单更重要...

这正是 self 的情况——一个简单的实现,以接口为代价,其中方法签名与其调用不匹配。

当然还有更多的原因为什么我们要明确的写self,或者说为什么它必须保留, Guido van Rossum 在博客文章中描述了其中一些(http://neopythonic.blogspot.com/2008/10/why-explicit-self-has-to-stay.html),文章回复了要求将其删除的提议。

Python 抽象了很多复杂性,但在我看来,深入研究低级细节和复杂性对于更好地理解该语言的工作原理非常有价值,当事情发生故障和高级故障排除/调试时,它可以派上用场不够。

此外,理解描述符实际上可能非常实用,因为它们有一些用例。虽然大多数时候你真的只需要@property 描述符,但在某些情况下自定义的描述符是有意义的,例如 SLQAlchemy 中的或者 e.g.自定义验证器。

 

相关文章:

  • 模拟前端ADC芯片LH001-91,用于开发心电、脑电医疗设备
  • CAPL 封装了的SeedKey解锁函数,高复用性
  • 异常数据处理——3σ原则、箱线图
  • vue3.0 + tsx 构建el-button
  • 基于JAVA的TCP网络QQ聊天工具系统
  • 【WLAN】【调试】netsh wlan常用命令使用说明
  • 教程图文详解 - 网络安全(第八章)
  • Python数据分析案例08——预测泰坦尼克号乘员的生存(机器学习全流程)
  • 计算机四级——操作系统原理常用知识点个人总结(上)
  • 【学姐面试宝典】—— 前端基础篇Ⅰ
  • c++之二叉树
  • 字符串训练赛
  • Android性能优化之【启动优化】
  • Java 集合与数据结构 · 接口 interfaces ·Collection 常用方法 · Map 常用方法
  • 面试面不过?大厂面试官是这样说的···
  • .pyc 想到的一些问题
  • 【跃迁之路】【669天】程序员高效学习方法论探索系列(实验阶段426-2018.12.13)...
  • Angular 4.x 动态创建组件
  • django开发-定时任务的使用
  • in typeof instanceof ===这些运算符有什么作用
  • php ci框架整合银盛支付
  • 给第三方使用接口的 URL 签名实现
  • 每天10道Java面试题,跟我走,offer有!
  • 一起来学SpringBoot | 第三篇:SpringBoot日志配置
  • ​Java并发新构件之Exchanger
  • ​LeetCode解法汇总2670. 找出不同元素数目差数组
  • ​插件化DPI在商用WIFI中的价值
  • #LLM入门|Prompt#1.7_文本拓展_Expanding
  • #pragma once
  • (14)目标检测_SSD训练代码基于pytorch搭建代码
  • (BFS)hdoj2377-Bus Pass
  • (JSP)EL——优化登录界面,获取对象,获取数据
  • (九)信息融合方式简介
  • (六)Hibernate的二级缓存
  • .NET Core中的去虚
  • .net 重复调用webservice_Java RMI 远程调用详解,优劣势说明
  • .net和php怎么连接,php和apache之间如何连接
  • .NET中的Exception处理(C#)
  • [.NET]桃源网络硬盘 v7.4
  • [].slice.call()将类数组转化为真正的数组
  • [14]内置对象
  • [Angular 基础] - 指令(directives)
  • [AutoSar]BSW_Com02 PDU详解
  • [AutoSar]BSW_OS 01 priority ceiling protocol(PCP)
  • [C++]类和对象【下】
  • [codevs1288] 埃及分数
  • [Deep Learning] 神经网络基础
  • [Electron]ipcMain.on和ipcMain.handle的区别
  • [json]定义、读写
  • [LeetCode] 93. Restore IP Addresses 复原IP地址
  • [leetcode] Multiply Strings
  • [python] dict类型变量写在文件中
  • [svc]后台运行程序screen or nohup
  • [WeChall] No Escape (Exploit, PHP, MySQL)
  • [Windows编程] SHLWAPI 和 CRT (C Runtime) 字符串处理函数的区别