Python科普系列——类与方法(上篇)
欢迎来到新的系列,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
的输出结果可以看到,无论是类还是对象,内部都包含了大量的字段名,不仅如此,类和对象的字段名实际上高度相似,唯二的差异也分别是我们自己定义的字段 x
和 y
,此处注意是字段(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}));接下来分别对对象的属性进行赋值,包括数值 x
和 y
,以及一个会基于 t.x
和 t.y
进行运算处理的函数 plus
(一般我们更习惯于称之为方法);最后就是使用这一手动创建的对象,可以看到 t.x
和t.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
不难发现,从这样的视角来看,一个类的装配也大致分为三步:
- “初始化阶段”——此阶段会创建一个指定名称的类对象。
- “继承阶段”——此阶段会尝试在类对象上建立与已有类的继承关系。
- “装配阶段”——次阶段会将类所需的各个属性,装配至类对象上。
至此,经过了三个阶段后,一个类对象创建完毕,并且在使用上和正常定义的类并无差别。
延伸思考6: collections
库中的 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科普系列——类与方法(上篇)的更多相关文章
- Python科普系列——类与方法(下篇)
书接上回,继续来讲讲关于类及其方法的一些冷知识和烫知识.本篇将重点讲讲类中的另一个重要元素--方法,也和上篇一样用各种神奇的例子,从原理和机制的角度为你还原一个不一样的Python.在阅读本篇之前,推 ...
- 全面了解python中的类,对象,方法,属性
全面了解python中的类,对象,方法,属性 python中一切皆为对象,所谓对象:我自己就是一个对象,我玩的电脑就是对象,坐着的椅子就是对象,家里养的小狗也是一个对象...... 我们通过描述属性( ...
- NO.7:自学python之路------类的方法、异常处理、socket网络编程
引言 我visual studio 2017就算体积巨大.启动巨慢.功能简陋也不会安装PyCharm的,嘿呀,真香.好吧,为了实现socket网络编程,更换了软件. 正文 静态方法 只是在名义上归类管 ...
- python 对过时类或方法添加删除线的方法
class Cat(Animal): def __init__(self): import warnings warnings.warn("Cat类带删除线了", Deprecat ...
- python中对类的方法中参数self的理解
我们通过下面的代码来对参数self进行理解 #coding:utf-8 2 class washer(): 3 def wash(self): 4 print("洗衣服") 5 p ...
- 第8.7节 Python类__new__方法和构造方法关系深入剖析:__new__方法执行结果对__init__的影响案例详解
一. 引言 前面章节介绍了类中的构造方法和__new__方法,并分析了二者执行的先后顺序关系.__new__方法在__init__方法前执行,__new__方法执行后才返回实例对象,也就是说__new ...
- 第8.15节 Python重写自定义类的__repr__方法
一. 引言 前面两节分别介绍了Python类中的__str__和__repr__方法的作用和语法,所有新式类都支持这两个方法,因为object类实现了这两个方法,但实际上各位开发者在自定义类的过程中, ...
- Python笔记(5)类__方法与继承
方法 类方法@classmethod,实例方法,静态方法@staticmethod,特殊方法,__init__ 形式上的区别:调用是通过类和实例进行,不能直接调用,有自己的特殊参数,如__init__ ...
- python学习道路(day8note)(抽象类,类的方法,异常处理,socket编程)
1.#面向对象 #抽象接口 === 抽象类 #就是架构师给你一个架子,你们去写,如果满足不了直接报错 #python2 print("python2---抽象类".center(2 ...
随机推荐
- P3273-[SCOI2011]棘手的操作【线段树,并查集】
正题 题目链接:https://www.luogu.com.cn/problem/P3273 题目大意 \(n\)个点有权值,要求支持操作 连接两个点 单点加权 联通块加权 全图加权 单点询问 联通块 ...
- Redis之品鉴之旅(一)
Redis之品鉴之旅(一) 好知识就如好酒,需要我们坐下来,静静的慢慢的去品鉴.Redis作为主流nosql数据库,在提升性能的方面是不可或缺的.下面就拿好小板凳,我们慢慢的来一一品鉴. 1)redi ...
- JDBC连接mariadb时使用依赖
问题描述:最近在尝试使用mariadb,使用idea开发,在使用jdbc连接数据库的时候,一般会用到driver,user,pwd,url,使用mysql的例子太多了,也有很多详细的教程,但是现在尝试 ...
- kubelet源码分析——监控Pod变更
前言 前文介绍Pod无论是启动时还是关闭时,处理是由kubelet的主循环syncLoop开始执行逻辑,而syncLoop的入参是一条传递变更Pod的通道,显然syncLoop往后的逻辑属于消费者一方 ...
- 实验4:开源控制器实践——OpenDaylight
实验4:开源控制器实践--OpenDaylight 一.实验目的 能够独立完成OpenDaylight控制器的安装配置: 能够使用Postman工具调用OpenDaylight API接口下发流表. ...
- 看动画学算法之:栈stack
目录 简介 栈的构成 栈的实现 使用数组来实现栈 使用动态数组来实现栈 使用链表来实现 简介 栈应该是一种非常简单并且非常有用的数据结构了.栈的特点就是先进后出FILO或者后进先出LIFO. 实际上很 ...
- 9.亿级流量电商系统JVM模型参数预估方案
1. 需求分析 大促在即,拥有亿级流量的电商平台开发了一个订单系统,我们应该如何来预估其并发量?如何根据并发量来合理配置JVM参数呢? 假设,现在有一个场景,一个电商平台,比如京东,需要承担每天上亿的 ...
- Kettle的安装及简单使用
Kettle的安装及简单使用 目录 Kettle的安装及简单使用 一.kettle概述 二.kettle安装部署和使用 Windows下安装 案例1:MySQL to MySQL 案例2:使用作业执行 ...
- 生产环境全链路压测平台 Takin
什么是Takin? Takin是基于Java的开源系统,可以在无业务代码侵入的情况下,嵌入到各个应用程序节点,实现生产环境的全链路性能测试,适用于复杂的微服务架构系统. Takin核心原理图 Taki ...
- 全连接层dense作用
参考来源