Python学习笔记(六)类和对象

“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。当实例对象有需要进行明确的初始化步骤时,编写此方法进行初始化。

在创建一个实例对象时,构造器的运行流程如下

  1. 首先调用 __new__ 方法返回类的实例;
  2. 然后实例调用类的 __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)。当该对象的生命周期结束,即以下两种情况之一发生时,

  1. 使用 del 关键字删除实例对象
  2. 创建的实例对象没有被变量引用,即(参考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__ 方法:

  1. 右操作数为左操作数的子类,会调用右操作数的 __radd__
i = StrangeInt(2)
other = 1
print(other + i)
# __radd__ of instance 5219059016 is being called.
# -1
  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.
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值