“Life is short, You need Python” – Bruce Eckel
Environment
- OS: macOS Mojave
- Python version: 3.7
- IDE: Jupyter Notebook
文章目录
0. 写在前面
个人在 Python 之前只会 C 语言,因此学习 Python 是第一次接触面向对象编程(Object Oriented Programming,OOP),而类和对象 OOP 中的核心概念,类是对象的载体,这部分可以说是关键。Python 中一切皆对象,通过对魔法方法(magic method)的使用能够理解到这个真理。
OOP 符合客观世界的运作。譬如不同肤色、国籍、性别、年龄的人,每一个都可以视作一个对象,共同之处是都为“人”,因此可以提取共同特征并抽象为一个类,名为“人”。那么属于这个类的对象的属性就有所不同,如肤色有黄、黑和白,国籍有更多不同;每个对象可以完成的事情定义方法,也有所不同。
可见,一个类有三个部分需要关注:名称、属性和方法。
另外,OOP 有三个特性
- 封装性:隐藏一个对象的具体实现,对外仅提供部分接口,使实现和使用分开;
- 继承性:子类自动继承父类的属性和方法,增加了代码的复用性;
- 多态性:子类中可以对继承了父类的方法进行重写,于是调用不同子类中同一名字的方法将得到不同的结果,增加了灵活性。
1. 类的定义与实例化
使用 class
关键字定义一个类,类名格式常用驼峰体
class LittleDog:
pass
定义好一个类之后,便产生了一个类对象,对其实例化得到实例对象。实例化就是像调用函数那样,如下
class Human: # 一个类对象
pass
human = Human() # 一个实例对象
1.1 属性
在一个类中定义属性,可以为类属性或实例属性
- 实例属性:在
__int__(self)
(之后介绍)中定义的、格式为self.属性名
的变量;该属性必须要实例化得到实例对象后,使用实例名.属性名
访问或修改; - 类属性:不在
__int__(self)
中定义的、名字之前不带self
的变量;该属性可以使用类名.属性名
访问或修改,也可以使用实例名.属性名
访问或修改。
class Beauty:
def __init__(self):
self.boobs = 'big'
legs = 'long'
beauty = Beauty() # 实例化
print(beauty.boobs) # 调用实例属性。打印出 big
print(Beauty.legs) # 调用类属性。打印出 long
print(beauty.legs) # 通过实例调用类属性。打印出 long
# 企图通过类调用实例属性
print(Beauty.boobs) # 报错 AttributeError
可以将另一个类的实例对象作为该类的实例属性
class Screen:
def __init__(self):
self.size = 13.3
class Keyboard:
def __init__(self):
self.num_buttons = 87
class Laptop:
def __init__(self):
self.screen = Screen()
self.keyboard = Keyboard()
self.year = 2018
laptop = Laptop()
print(laptop.screen.size) # 13.3
print(laptop.keyboard.num_buttons) # 87
注意
- 若实例属性与类属性同名,则对实例对象调用该属性时将忽略类属性的值,始终得到实例属性的值
class Human:
def __init__(self):
self.speed = 'fast'
speed = 'slow'
human = Human()
print(human.speed) # fast
# 调用类属性依然得到类属性
print(Human.speed) # 打印出 slow
- 对实例对象本不存在的一个属性赋值,则直接对该实例对象创建该实例属性
class Human:
def __int__(self):
self.num_head = 1
human = Human()
human.num_hands = 2
print(human.num_hands) # 2
1.2 方法
在类中 def
用定义的函数称为方法,可分为类方法和实例方法
- 定义实例方法时,第一个参数为
self
;欲调用该方法必须先实例化得到实例对象后,使用实例对象名.方法(参数)
; - 定义类方法时,没有
self
参数;该方法使用类名.方法(必要的参数)
调用。
class Human:
def run(self):
print('An instantce is runing')
def walk():
print('A class is walking')
human = Human()
human.run() # An instantce is runing
Human.walk() # A class is walking
# 企图通过实例对象调用类方法
human.walk() # 报错 TypeError
注意,若属性名与方法名相同,则方法被覆盖
class Human:
def __init__(self):
self.num_hands = 2
def num_hands(self):
print('The number of hands is 2.')
human = Human()
human.num_legs # 2
human.num_legs() # 报错 TypeError
1.3 self 参数
可见,实例属性和实例方法都需要有实例对象之后才能使用。因此,实例属性定义时的 self
和实例方法的第一个参数 self
代表的就是具体的实例对象。对一个类对象实例化产生实例对象之后,属性和方法便被绑定到实例对象上
class Human:
def __init__(self):
self.num_head = 1
num_hands = 2
def say(self):
print("This is human instance")
human = Human()
# 显示实例属性
print(human.__dict__) # {'num_head': 1}
# 由于属性和方法已被绑定到实例,对象删除类对象之后依然可以调用
del Human
human.say() # This is human instance
2. 继承
2.1 单继承
使用 class 子类名(父类名)
进行定义,子类将获得父类的属性和方法;子类又称派生类(derived class),父类又称基类(base class)。
class Human:
def __init__(self):
self.num_hands = 2
def say(self):
print("This is a human.")
class Chinese(Human):
def walk(self):
print('A Chinese is walking.')
chinese = Chinese()
chinese.walk() # A Chinese is walking.
chinese.say() # This is a human.
print(chinese.num_hands) # 2
object 是所有类的基类,在 Python 3.x 中,不显示地指定继承的父类时,即为 class 类名(object)
class Human:
pass
# 等价于
class Human(object):
pass
2.2 多继承
子类能够同时获得多个父类的属性和方法
class YellowRace:
def bar(self):
print("My skin colour is yellow.")
class Asian:
def baz(self):
print("I'm Asian.")
class Chinese(YellowRace, Asian):
pass
chinese = Chinese()
chinese.bar() # My skin colour is yellow.
chinese.baz() # I'm Asian.
注意需少用和慎用多继承,多继承可能造成钻石继承问题,从而产生意外的bug。钻石继承详细内容可以阅读fishc的关于钻石继承文章。
2.3 方法重写
在子类中重新编写父类拥有的方法,即 def
一个同名方法。在调用方法时,先在子类中查找,若找到重写的方法,则调用之;若没有,则到父类中查找并调用。
class Parent:
def circle(self):
print('Turn two rounds.')
def jump(self):
print('Do the high jump.')
class Child(Parent):
def circle(self):
print('Turn one round.')
child = Child()
child.circle() # Turn one round.
child.jump() # Do the high jump.
2.4 super 函数
由 2.3 中可知,若子类定义中有与父类同名的属性或方法,则父类中的属性或方法将被覆盖。
如果不希望子类中重写的方法覆盖掉父类中同名的方法,而是能够运行父类的方法再额外执行其他功能,那么可以调用 super()
函数。super 函数通过方法解析顺序(Method Resolution Order,MRO)解决这个问题,更详细内容可以参考文章 Python: super 没那么简单。
class Parent:
def circle(self):
print('Turn two rounds.')
def jump(self):
print('Do the high jump.')
class Child(Parent):
def circle(self):
super().circle()
print('Turn one round.')
child = Child()
child.circle()
# Turn two rounds.
# Turn one round.
3. 一些关于类的常用函数
3.1 从属判断
issubclass(cls, class_or_tuple)
判断一个类 class 是否是另一类的同类或其子类
class Human:
pass
class Chinese(Human):
pass
issubclass(Chinese, (Chinese, Human)) # True
isinstance(obj, class_or_tuple)
判断一个对象是否是某一类的同类或其子类
class Human:
pass
class Chinese(Human):
pass
human = Human()
chinese = Chinese()
isinstance(chinese, (Chinese, Human)) # True
3.2 属性相关
hasattr(obj, name)
判断某对象是否有某属性
class Human:
def __init__(self):
self.num_hands = 2
human = Human()
hasattr(human, 'num_hands') # 以字符串的形式传入进行判断
getattr(object, name[, default])
返回对象指定的属性的值;传入字符串给 default 时,若访问的属性不存在,将返回传入的字符串
class Human:
def __init__(self):
self.num_hands = 2
human = Human()
getattr(human, 'num_hands') # 2
getattr(human, 'num_hand') # 报错 AttributeError
getattr(human, 'num_hand', 'The attribute you called dose not exist!')
# 'The attribute you called dose not exist!'
setattr(object, attr_name, value)
为对象创建一个实例属性并赋值,若已存在该实例属性,则原来的被覆盖
class Human:
def __init__(self):
self.num_hands = 2
human = Human()
setattr(human, 'num_eyes', 2)
getattr(human, 'num_eyes') # 2
delattr()
删除对象中指定的实例属性
class Human:
def __init__(self):
self.num_hands = 2
human = Human()
delattr(human, 'num_hands')
getattr(human, 'num_hands', 'The attribute you called dose not exist!')
# 'The attribute you called dose not exist!'
3.3 property 函数
property 函数使得实例方法能够以属性的形式被访问、赋值和删除。
# 语法
property(
fget=None, # 用作访问属性值的函数
fset=None, # 用作设置属性值的函数
fdel=None, # 用作删除属性的函数
doc=None # 文档,默认为 None,将引用 fget 方法的文档
)
若不传入 fset
参数,那么属性将保持初始化定义类时的值,无法修改。
例
class Human:
def __init__(self):
self._skin_colour = 'Yellow'
def getSkinColour(self):
print('Getting the attribute...')
return self._skin_colour
def setSkinColour(self, colour):
print('Setting the attribute...')
self._skin_colour = colour
def delSkinColour(self):
print('Removing the attribute')
del self._skin_colour
skin_colour = property(
fget=getSkinColour,
fset=setSkinColour,
fdel=delSkinColour,
doc='Skin colour'
)
human = Human()
print(Human.skin_colour.__doc__)
# Skin colour
print(human.skin_colour)
# Getting the attribute...
# Yellow
human.skin_colour = 'Brown'
# Setting the attribute...
del human.skin_colour
# Removing the attribute
更简洁的写法是 property 函数作为装饰器,内容在Python基础(十)深浅拷贝与三大器 Part 5.5文章。
4. 魔法方法
一些实例方法的首尾分别被加上了两个下划线,这些方法称为魔法方法,在对实例对象进行某些操作时,特定的魔法方法将被调用。
关于魔法方法汇总已有不少,如fishc的Python魔法方法详解文章,此处不在赘述。以下记录一些关键的理解和使用。
4.1 构造器
__new__
和 __init__
方法共同构成了 Python 对象的构造器 (constructor)。
-
__new__(cls, 创建实例对象时必要的参数)
返回值为实例化的实例对象,cls
参数表示要实例化的类,将由解释器自动提供。该方法属于类级别的方法; -
__init__(self, 实例化时必要的参数)
返回值永远为None
。当实例对象有需要进行明确的初始化步骤时,编写此方法进行初始化。
在创建一个实例对象时,构造器的运行流程如下
- 首先调用
__new__
方法返回类的实例; - 然后实例调用类的
__init__
方法进行必要的初始化。
class Human:
def __new__(cls, skin_colour):
print('__new__ of class {} is being called.'.format(id(cls)))
return super(Human, cls).__new__(cls)
def __init__(self, skin_colour):
print('__init__ of instance {} is being called.'.format(id(self)))
# 有需要初始化的实例属性,便在 constructor 设置参数
self.skin_colour = skin_colour
human = Human(skin_colour='white')
# __new__ of class 140395860116984 is being called.
# __init__ of instance 5215463184 is being called.
一般使用过程中只需要重写 __init__
方法,而不对 __new__
方法进行重写。当需要一个继承了不可变类型的类,且又需要修改该类时,可以通过重写 __new__
方法实现,如下例
class NegativeNumber(float):
"""输入的数成为负数"""
def __new__(cls, value):
return super(NegativeNumber, cls).__new__(cls, -abs(value))
i = NegativeNumber(1)
print(i) # -1
4.2 解构器
__del__(self)
方法是 Python 对象的解构器(destructor)。当该对象的生命周期结束,即以下两种情况之一发生时,
- 使用
del
关键字删除实例对象 - 创建的实例对象没有被变量引用,即(参考Python垃圾回收机制上、Python垃圾回收机制中、Python垃圾回收机制下)
该方法将被调用。
class Human:
def __init__(self, skin_colour):
print('__init__ of instance {} is being called.'.format(id(self)))
# 有需要初始化的实例属性,便在 constructor 设置参数
self.skin_colour = skin_colour
def __del__(self):
print('destructor of instance {} is being called.'.format(id(self)))
human = Human(skin_colour='white')
# __init__ of instance 5215496008 is being called.
human = True
# destructor of instance 5215496008 is being called.
4.3 属性相关
__getattribute__(self, name)
在访问属性时首先被调用__getattr__(self, name)
在访问的属性不存在时被调用__setattr__(self, name, value)
在为实例属性赋值时被调用__delattr__(self, name)
在使用del
删除实例属性时被调用
class Human:
def __init__(self, skin_colour='yellow'):
self.skin_colour = 'yellow'
def __getattribute__(self, name): # 首先被调用的魔法方法
print('__getattribute__ of {} is being called.'.format(id(self)))
return super(Human, self).__getattribute__(name)
def __getattr__(self, name):
print('__getattr__ of {} is being called.'.format(id(self)))
def __setattr__(self, name, value):
print('__setattr__ of {} is being called.'.format(id(self)))
super(Human, self).__setattr__(name, value)
def __delattr__(self, name):
print('__delattr__ of {} is being called.'.format(id(self)))
super(Human, self).__delattr__(name)
human = Human() # 实例化时就发生了赋值操作
# __setattr__ of 5217042384 is being called.
human.weight
# __getattribute__ of 5217042384 is being called.
# __getattr__ of 5217042384 is being called.
human.skin_colour
# __getattribute__ of 5217042384 is being called.
# 'yellow'
human.skin_colour = 'brown'
# __setattr__ of 5217042384 is being called.
del human.skin_colour
# __delattr__ of 5217042384 is being called.
注意,若在 __setattr__
中若不使用 super()
函数而是直接赋值,则会造成无限递归
class Human:
def __init__(self, skin_colour='yellow'):
self.skin_colour = 'yellow'
def __setattr__(self, name, value):
print('__setattr__ of {} is being called.'.format(id(self)))
super(Human, self).__setattr__(name, value)
self.name = value
human = Human()
4.4 算数运算
__add__(self, other)
实现实例对象与右边另一对象的相加
class DiyChar(str):
"""一个字符,与数字相加时将使用该字符对象的ASCII码进行相加"""
def __add__(self, other):
print('__add__ of instance {} is being called.'.format(id(self)))
return int.__add__(ord(self), other)
char = DiyChar('a')
print(char + 3) # 100
__add__
方法为右运算对应的 __radd__
实现实例对象与右边另一对象的相加
class StrangeInt(int):
"""
当实例对象作为右操作数,左操作数为 int 时,
相加将变为相减
"""
def __radd__(self, other):
print('__radd__ of instance {} is being called.'.format(id(self)))
return super(StrangeInt, self).__rsub__(other)
当以下两种情况下,会调用 __radd__
方法:
- 右操作数为左操作数的子类,会调用右操作数的
__radd__
i = StrangeInt(2)
other = 1
print(other + i)
# __radd__ of instance 5219059016 is being called.
# -1
- 左操作数的运算符的
__add__
不支持时,将调用右操作数的__radd__
方法
s = 'xyz'
print(s + i)
# __radd__ of instance 5222118920 is being called.
# 报错 TypeError
4.5 组合数据类型相关
__getitem__(self, key)
访问元素时被调用__setitem__(self, key, value)
修改元素时被调用__delitem__(self, key)
使用del
删除元素时被调用
class InvList(list):
"""一个默认为反向索引的列表"""
def __getitem__(self, key):
print('__getitem__ of instance {} is being called.'.format(id(self)))
return super(InvList, self).__getitem__(len(self) - key - 1)
def __setitem__(self, key, value):
print('__setitem__ of instance {} is being called.'.format(id(self)))
super(InvList, self).__setitem__(len(self) - key - 1, value)
def __delitem__(self, key):
print('__delitem__ of instance {} is being called.'.format(id(self)))
super(InvList, self).__delitem__(len(self) - key - 1)
dls = InvList([9, 5, 2, 7])
print(dls)
# __getitem__ of instance 5222403992 is being called.
# 5
dls[1] = 66
print(dls)
# __setitem__ of instance 5222403992 is being called.
# [9, 5, 66, 7]
del dls[0]
# __delitem__ of instance 5222403992 is being called.
print(dls) # [9, 5, 66]
4.6 描述符
至少具有以下三个魔法方法之一的某个类(类A),
__get__(self, instance, owner)
访问被描述的实例属性时被调用__set__(self, instance, value)
对被描述的属性赋值时被调用__delete__(self, instance)
删除被描述的实例属性时被调用
实例化后得到的对象作为另一个类(类B)的类属性,那么类A 就是描述符。调用时,instance 将传入类B的实例化对象,owner 将传入类B,由解释器自动提供。
这个 B 类的类属性将作用于 B 类中同名的实例属性,改变其访问的方式。原本,对实例属性的访问是通过这个对象的 __dict__
,而被描述的属性访问将通过描述符协议,即描述符类中定义的方法。更详细的可以参考知乎的问答如何理解 Python 的 Descriptor?和这篇cnblogs的博客。
class SkinColour:
def __init__(self, attr_name):
self.attr_name = attr_name
def __get__(self, instance, owner):
print('__get__ of descriptor instance id {} is being called.'.format(id(self)))
print("Descriped instance:", id(instance))
print("It's owner:", id(owner))
return instance.__dict__[self.attr_name]
def __set__(self, instance, value):
print('__set__ of descriptor instance {} is being called.'.format(id(self)))
print("Descriped instance:", id(instance))
instance.__dict__[self.attr_name] = value
def __delete__(self, instance):
print('__delete__ of descriptor instance {} is being called.'.format(id(self)))
class Human:
skin_colour = SkinColour('skin_colour')
def __init__(self, skin_colour):
self.skin_colour = skin_colour
human = Human('yellow')
# __set__ of descriptor instance 5222567328 is being called.
# Descriped instance: 5222568288
human.weight = 0 # 未被描述的属性不受影响
human.skin_colour = 'yellow'
# _set__ of descriptor instance 5222567328 is being called.
# Descriped instance: 5222568288
temp = human.skin_colour
# __get__ of descriptor instance id 5222567328 is being called.
# Descriped instance: 5222568288
# It's owner: 140395804068616
del human.skin_colour
# __delete__ of descriptor instance 5222567328 is being called.