欢迎来到新的系列,up又开新坑了~~

实际上,Python作为一门易用性见长的语言,看上去简单,却仍然有很多值得一说的内容,因此这个系列会把Python中比较有意思的地方也给科普一遍。而另一方面,关于Python的学习资料在中文互联网上已经随处可见,虽然大都是入门向、实用向的,不过资料覆盖面也已经挺全乎的了。所以这个系列将会着重去讲一些现有中文资料里不常见到的硬核内容,尝试去用另外一个视角去讲解Python,也因此,这个系列更适合有最基本Python使用基础,对基本概念有初步认识的读者。

本文将会着重讲讲关于类的事情,尤其是类的方法。考虑到treevalue系列的第三篇也即将推出,并且也会较多涉及到关于类和方法相关的内容,因此本文和下篇也会有所侧重,主要从原理的角度讲解类和方法的本质,以方便理解。而对于略过的部分,后续也将考虑另开文章进行详细讲解。

对象是如何被构造的

首先,让我们来一块想一个终极问题——对象是怎么来的?这看起来答案显而易见——对象不就是构造函数构造出来的么?但实际上这么说并不准确,要说到Python对象是如何被构造的,就不得不说三个特殊的方法: __new____init____del__

首先 __init__ 应该用过Python的都不陌生,但是另外两个分别是什么就未必了解了。我们来看一个最为直观的例子

class T:
    def __init__(self, x, y):
        print('Initializing T', x, y)
        self.x = x
        self.y = y     def __new__(cls, *args, **kwargs):
        print('Creating new T', args, kwargs)
        return object.__new__(cls)     def __del__(self):
        print('Deleting T') if __name__ == '__main__':
    t = T(1, 2)
    print('t is initialized.') # Creating new T (1, 2) {}
# Initializing T 1 2
# t is initialized.
# Deleting T

通过这个例子会发现,执行的顺序大致如下图所示

具体来说,分为以下几个阶段:

  • “从无到有”——通过 __new__ 方法,创建一个新的初始对象,并将此模板对象作为 self 传入给后续的 __init__ 方法。
  • “组装配件”——通过 __init__ 方法,基于之前生成的函数初始对象进行装饰(也就是常说的字段赋值)。这一过程类似于工厂模式,并非在创造而是在加工。经过了这一步处理的对象,才算是正式完成了对象的初始化,这一初始化完毕的对象也会传回到调用构造函数之处,作为一个真正的实例参与到业务逻辑中
  • “对象销毁”——当对象的生命周期结束之时,通过 __del__ 方法,处理掉当前对象下于初始化阶段组装的全部“配件”。处理完毕后,该对象将被销毁,对象的生命周期就此终止

也就是说,我们所日常认知的Python对象,其实是经历了__new____init__两个阶段构造出来的实例,也正是这样构造出来的对象,支撑了我们在Python中几乎所有的数据模型及其业务逻辑。

延伸思考1__new____del__ 分别有什么样的应用场景?

延伸思考2:如果需要定义一个类,且需要在任意时刻了解其所有处于活动状态的实例对象并进行查询,应该如何去实现?

欢迎评论区讨论!

类与对象的本质

首先说到Python中的类,关于类及其方法的基本介绍,可以参考Runoob:Python3 面向对象,里面有面向初学者的详细介绍,而对于面向对象的基本编程思想,维基百科上也有比较详细的介绍,此处不作展开。

我们就从类的定义形态开始,讲讲类的本质是什么。首先我们来看一个最简单的类定义

class MyClass:
cvalue = 233 def __init__(self, x):
self.__x = x def getvalue(self, plus):
return self.__x + plus @classmethod
def getcvalue(cls, plus):
return cls.cvalue + plus

这就是一种挺典型的类定义了,在进行面向对象编程的时候也很常见。除了类之外,我们还都知道,有一种数据类型叫做 dict ,即字典类型,该数据结构可以视为一个基于键值对,并支持增删查改的映射结构,一个典型的例子如下所示

h = {
'a': 1,
'b': 'this is a str value',
'c': ['first', '2nd', 3],
'd': {
'content': 'nested dict is okay',
}
}

你可能会感到奇怪,为什么我会突然笔锋一转,说起了字典类型。那我问你——要是我告诉你,类、对象和字典本质上是差不多的,你会不会感到难以置信呢?首先先说结论——在Python中,类、对象和字典类型,都是典型的映射结构。可以看下如下的这个例子,里面是一个最为简单的类,并通过 dir__dict__ 来展示了类与对象的部分内部结构

class T:
    def __init__(self, x, y):
        self.x = x
        self.y = y if __name__ == '__main__':
    t = T(1, 2)
    print(dir(t))
    print(t.__dict__)
    print(dir(T))
    print(T.__dict__) # ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'x', 'y']
# {'x': 1, 'y': 2}
# ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
# {'__module__': '__main__', '__init__': <function T.__init__ at 0x7f43dc5f4e18>, '__dict__': <attribute '__dict__' of 'T' objects>, '__weakref__': <attribute '__weakref__' of 'T' objects>, '__doc__': None}

通过 dir 的输出结果可以看到,无论是类还是对象,内部都包含了大量的字段名,不仅如此,类和对象的字段名实际上高度相似,唯二的差异也分别是我们自己定义的字段 xy ,此处注意是字段(field)不是属性(property),虽然一般情况下这两个概念常常不作区分,但是此处需要消除歧义。因为实际上在Python中,类本质上也是一种对象,名为类对象的对象,如果说上述例子里对象 t 的类型为 T ,则类对象 T 的类型为 type ,基于这一点我们可以先建立起一个将类和对象统一起来的概念

而在上面的例子中,我们除了执行 dir 函数之外,还访问了对象的 __dict__ 值。而在对象 t 中,得到的值为 {'x': 1, 'y': 2}回忆一下上一章所述的类的构造方式,再看看类 T__init__ 方法中的实现

class T:
def __init__(self, x, y):
print('Initializing T', x, y)
self.x = x
self.y = y

把这两件事放在一起看,有没有联想到什么?没错,在这个例子里__dict__中读取到的值就是在构造过程 __init__ 中赋的值

不仅如此,我们再看看如果类之间存在继承关系,会发生什么,例如下面的例子

class T:
    def __init__(self, x, y):
        self.x = x
        self.y = y class TP(T):
    def __init__(self, x, y):
        T.__init__(self, x, y)
        self.total = x + y class TM(TP):
    def __init__(self, x, y):
        TP.__init__(self, x, y)
        self.mul = x * y if __name__ == '__main__':
    t = TM(3, 5)
    print(t.__dict__) # {'x': 3, 'y': 5, 'total': 8, 'mul': 15}

可以看到几级父类上 __init__ 赋的值都在 __dict__ 中。这一现象如果结合前一章对 __init__ 原理的解释,则成因是显而易见的——构造函数__init__的本质是一个工厂函数,从这个角度来看,则 TM.__init__ 也是一个工厂函数,而其内部直接或间接调用了 TP.__init__T.__init__ 这两个属于父类的工厂函数,因此可以将内部的装饰效果一并应用于当前对象中,形成类似类继承的效果

延伸思考3:如果对已经构造完毕的对象的某未定义的属性进行直接赋值(例如 t.undefined = 10 ),会发生什么现象?

延伸思考4:如何解释上面的现象?与构造函数中的属性赋值有何异同?

延伸思考5:类似的,如果将 t 赋值为 object() ,执行延伸思考3中的赋值操作,会发生什么现象?如何解释这一现象?(可以参考官方文档

欢迎评论区讨论!

如何手动制造一个对象

基于以上的分析,对类和对象的本质已经初见端倪——类和对象本质上也是一种映射结构,这一结构中存值的那一部分位于 __dict__ ,而存储业务逻辑的部分则是各个函数,它们在 dir(t) 中均可以找到名称,并且可以通过 getattr 进行访问(实际上在Python中,函数也同样是一个对象)。

因此,我们可以基于上述的原理,尝试构造一个简易的对象出来。例如下面的例子

class MyObject(object):
    pass if __name__ == '__main__':
    t = MyObject()  # the same as __new__
    t.x = 2  # the same as __init__
    t.y = 5     def plus(z):
        return t.x + t.y + z     t.plus = plus  # the same as function def     print(t.x, t.y)
    print(t.plus(233))

首先在第6行,我们模仿 __new__ 方法的思路,手动创建一个空对象(注意不能直接用 object ,而需要继承一层,具体原因详见[官方文档中的Note部分](https://{'x': 3, 'y': 5, 'total': 8, 'mul': 15}));接下来分别对对象的属性进行赋值,包括数值 xy ,以及一个会基于 t.xt.y 进行运算处理的函数 plus (一般我们更习惯于称之为方法);最后就是使用这一手动创建的对象,可以看到 t.xt.y均可正常使用,并且方法t.plus(z)也可以被正常调用。经过这一系列操作,一个手工创建的对象就产生了,而且从使用者的角度来看,也和正常实例化的对象并无差异

如何手动制造一个类

不仅对象,类也是可以手动制造出来的。话不多说,我们先看看来自官方文档的构造 type 类说明

class type(object)

class type(name, bases, dict, **kwds)

With one argument, return the type of an object. The return value is a type object and generally the same object as returned by object.class.

The isinstance() built-in function is recommended for testing the type of an object, because it takes subclasses into account.

With three arguments, return a new type object. This is essentially a dynamic form of the class statement. The name string is the class name and becomes the name attribute. The bases tuple contains the base classes and becomes the bases attribute; if empty, object, the ultimate base of all classes, is added. The dict dictionary contains attribute and method definitions for the class body; it may be copied or wrapped before becoming the dict attribute. The following two statements create identical type objects:

看起来挺长,不过后续附了一个最为简明扼要的例子

# first code
class X:
a = 1 # second code, the same as the former one
X = type('X', (), dict(a=1))

所以其实依然不难理解,简单来说就是三个基本参数:

  • 名称( name )——字面意思,表示构造的类名
  • 基类( bases )——字面意思,表示所需要继承的基类
  • 字典( dict )——即需要赋予对象的属性

因此基于以上的原理,我们可以构造出来一个自己的类,就像这样

def __init__(self, x, y):
    self.x = x
    self.y = y def plus(self, z):
    return self.x + self.y + z XYTuple = type('XYTuple', (), dict(
    __init__=__init__,
    plus=plus,
)) if __name__ == '__main__':
    t = XYTuple(2, 5)
    print(t.x, t.y)
    print(t.plus(233)) # 2 5
# 240 # The definition of class is exactly the same as :
# class XYTuple:
# def __init__(self, x, y):
# self.x = x
# self.y = y
#
# def plus(self, z):
# return self.x + self.y + z

不难发现,从这样的视角来看,一个类的装配也大致分为三步:

  • “初始化阶段”——此阶段会创建一个指定名称的类对象
  • “继承阶段”——此阶段会尝试在类对象上建立与已有类的继承关系。
  • “装配阶段”——次阶段会将类所需的各个属性,装配至类对象上。

至此,经过了三个阶段后,一个类对象创建完毕,并且在使用上和正常定义的类并无差别。

延伸思考6collections 库中的 namedtuple 函数是如何构造一个类出来的?可以阅读一下源代码进行分析。

欢迎评论区讨论!

私有字段的本质

对于了解Python面向对象或学习过Java、C++等其他语言的读者,应该对私有字段这个东西并不陌生(如果还不够了解的话可以看看Python3 面向对象 - 类的私有属性)。在Python中,我们所熟知的私有字段大致是如下的形态

class T:
def __init__(self):
self.__private = 1 # private field, starts with __
self._protected = 2 # protected field, starts with _
self.public = 3 # public field, starts with alphabets

简单来说就是:

  • 私有字段,仅可以被类内部访问,以双下划线开头
  • 保护字段,可以被当前类及其子类访问,以单下划线开头
  • 公有字段,可以被自由访问,以字符开头

因此对上面的例子中,实际访问效果如下

t = T()
t.__private # Attribute Error!
t._protected # 2
t.public # 3

保护字段和公有字段是可以被访问到的,但是一般情况下保护字段并不推荐在当前类或子类以外的地方进行访问(实际上当你这么做的时候,不少IDE都会报出明确的warning),而私有字段则无法访问,直接访问会导致报错。

看起来似乎一切很正常,但是让我们来看看上面例子中变量 t 内部都有什么

t.__dict__  # {'_T__private': 1, '_protected': 2, 'public': 3}

其中 public_protected 是意料之内的,但是除此之外还包含一个_T__private,并且其值正是在构造函数中所赋予的值。基于这一点,我们再来做个实验

t._T__private  # 1

发现私有字段居然也可以被访问。至此,我们可以得出一个结论——在Python中,并不存在严格意义上的私有字段,我们所知道的私有字段本质上更像一种语法糖效果,而保护字段则干脆是被摆在明面上的。

从这个角度来看不难发现,在Python中这些字段之所以还能起到私有字段或保护字段应有的效果,本质上靠的是开发者意义上的约束,而非语言系统本身的强制力。这一点和Java等静态语言存在本质上的差异,在Java中定义的私有字段一般无法通过正常途径进行访问,即便通过反射机制强制读取,也需要绕开一系列机制。

延伸思考7:类似Python的私有字段处理方式还在哪些语言中有所体验?类似Java的呢?

延伸思考8:以上的两种处理方式分别体现了什么样的思维方式?有何优劣?分别适合什么样的开发者与应用场景?

欢迎评论区讨论!

后续预告

本文重点针对类的特性,从原理角度进行了分析。在本系列的下一篇中,会重点针对类的方法和属性进行讲解,以及treevalue第三弹也将会在不久后推出,敬请期待。

此外,欢迎欢迎了解OpenDILab的开源项目:

以及我本人的几个开源项目(部分仍在开发或完善中):

Python科普系列——类与方法(上篇)的更多相关文章

  1. Python科普系列——类与方法(下篇)

    书接上回,继续来讲讲关于类及其方法的一些冷知识和烫知识.本篇将重点讲讲类中的另一个重要元素--方法,也和上篇一样用各种神奇的例子,从原理和机制的角度为你还原一个不一样的Python.在阅读本篇之前,推 ...

  2. 全面了解python中的类,对象,方法,属性

    全面了解python中的类,对象,方法,属性 python中一切皆为对象,所谓对象:我自己就是一个对象,我玩的电脑就是对象,坐着的椅子就是对象,家里养的小狗也是一个对象...... 我们通过描述属性( ...

  3. NO.7:自学python之路------类的方法、异常处理、socket网络编程

    引言 我visual studio 2017就算体积巨大.启动巨慢.功能简陋也不会安装PyCharm的,嘿呀,真香.好吧,为了实现socket网络编程,更换了软件. 正文 静态方法 只是在名义上归类管 ...

  4. python 对过时类或方法添加删除线的方法

    class Cat(Animal): def __init__(self): import warnings warnings.warn("Cat类带删除线了", Deprecat ...

  5. python中对类的方法中参数self的理解

    我们通过下面的代码来对参数self进行理解 #coding:utf-8 2 class washer(): 3 def wash(self): 4 print("洗衣服") 5 p ...

  6. 第8.7节 Python类__new__方法和构造方法关系深入剖析:__new__方法执行结果对__init__的影响案例详解

    一. 引言 前面章节介绍了类中的构造方法和__new__方法,并分析了二者执行的先后顺序关系.__new__方法在__init__方法前执行,__new__方法执行后才返回实例对象,也就是说__new ...

  7. 第8.15节 Python重写自定义类的__repr__方法

    一. 引言 前面两节分别介绍了Python类中的__str__和__repr__方法的作用和语法,所有新式类都支持这两个方法,因为object类实现了这两个方法,但实际上各位开发者在自定义类的过程中, ...

  8. Python笔记(5)类__方法与继承

    方法 类方法@classmethod,实例方法,静态方法@staticmethod,特殊方法,__init__ 形式上的区别:调用是通过类和实例进行,不能直接调用,有自己的特殊参数,如__init__ ...

  9. python学习道路(day8note)(抽象类,类的方法,异常处理,socket编程)

    1.#面向对象 #抽象接口 === 抽象类 #就是架构师给你一个架子,你们去写,如果满足不了直接报错 #python2 print("python2---抽象类".center(2 ...

随机推荐

  1. 面试必问:Java 垃圾回收机制

    摘要:垃圾回收机制是守护线程的最佳示例,因为它始终在后台运行. 本文分享自华为云社区<一文带你了解Java 中的垃圾回收机制>,作者:海拥. 介绍 在 C/C++ 中,程序员负责对象的创建 ...

  2. ServerManager.exe 0xc0000135 应用程序错误(Windows Server 2016、2019)

    前言 将 Windows Server 2019或2016 .NET Framework移除. IIS卸载后,服务器管理器.控制面板部分功能.事件查看器等都无法正常开启. 解决办法 打开CMD,输入D ...

  3. 看动画学算法之:doublyLinkedList

    目录 简介 doublyLinkedList的构建 doublyLinkedList的操作 头部插入 尾部插入 插入给定的位置 删除指定位置的节点 简介 今天我们来学习一下复杂一点的LinkedLis ...

  4. 解决springboot 配置文件未映射静态资源文件 导致shiro拦截静态资源的问题

    ---------------------------------------------------------------------------------------------------- ...

  5. ORACLE 坏块的模拟和查看

    坏块的模拟和查看使用bbed工具修改数据文件的块,然后使用dbv和rman工具查看坏块. 1.创建数据:根据dbv查看没有坏块Total Pages Marked Corrupt : 0create ...

  6. FastAPI(64)- Settings and Environment Variables 配置项和环境变量

    背景 在许多情况下,应用程序可能需要一些外部设置或配置,例如密钥.数据库凭据.电子邮件服务凭据等. 大多数这些设置都是可变的(可以更改),例如数据库 URL,很多可能是敏感数据,比如密码 出于这个原因 ...

  7. 深入剖析 Spring WebFlux

    一.WebFlux 简介 WebFlux 是 Spring Framework5.0 中引入的一种新的反应式Web框架.通过Reactor项目实现Reactive Streams规范,完全异步和非阻塞 ...

  8. 专访阿里云 Serverless 负责人:无服务器不会让后端失业

    2012 年,云基础设施服务提供商 Iron.io 的副总裁 Ken 谈到软件开发行业的未来,首次提出了 Serverless 的概念,为云中运行的应用程序描述了一种全新的系统体系架构.此后,以 AW ...

  9. $\Large{\LaTeX}$ 常用公式

    $$\Large{\LaTeX}$$: \[\Large{\LaTeX} \] $ $ 表示行内 $$ $$ 表示独立 $\operatorname{lcm}(x)$\(\operatorname{l ...

  10. LOJ6469 Magic(trie)

    纪念我菜的真实的一场模拟赛 首先看到这个题目,一开始就很毒瘤.一定是没有办法直接做的. 我们考虑转化问题 假设,我们选择枚举\(x\),其中\(x\)是\(10\)的若干次方,那么我们只需要求有多少对 ...