Python 入门 —— 面向对象编程

面向对象编程是一种编程范式,通过将对象作为程序的基本单元,每个对象之间可以相互传递信息,并通过各自的方法对信息进行处理,从而达到程序处理的目的。

面向过程编程则是将程序视为一系列顺序执行命令的集合,通过将程序分割为一个个功能函数,降低程序的复杂度。简单来说,面向对象更像是团队合作,组内每个成员具有明确任务和职责;而面向过程更像是排队,一个接一个,后一个接前一个。

面向对象,首先需要明确两个概念:类和对象。是对客观世界的抽象,是对具有相同特性和行为的对象进行抽象剥离;而对象是类的实例,是客观存在的事物。

举例来说,人类与人即是类与对象的关系,人类并不是客观存在的事物,它是对所有人的统称,人类的包含一些固有属性包括:性别、年龄、肤色等,人类也存在一些行为:吃、看、想等。而每个人都是一个客观存在的,虽然都有性别、年龄但各不相同(男女老少),每个人的行为方式基本一样,但也存在差异(中国人说中国话、英国人说英语)。

回想一下我们前面所介绍的内置数据结构,其中就包含了类的概念:数据结构+算法,数据结构定义了类的固有属性及组织方式,使用算法来定义类的行为方式。注意类与数据结构的区别,两者并不是一样的。

面向对象的三个特性:

  • 封装:隐藏不需要与外部交互的代码的实现细节,仅保留部分接口。眼睛将我们看到的事物景象传递到大脑,这是一个非常复杂的转换过程,交给专业的人去探寻就行,我们要做的只是用眼睛去发现和感受美。
  • 继承:顾名思义,一个类从另一个类中继承其属性和方法,并可以重写或添加某些属性和方法。基于人类,我们又可以分出黄种人、黑人和白人,各自拥有不同的肤色,但是都是人类,具有人类的特征和行为方式。
  • 多态:由继承所产生的不同类能够对同一消息做出不同反应。同样是说话,中国人说的是汉语、英国人说的是英语。

下面我们将从 Python 的角度来分别来介绍两种不同的面向对象编程。Python 本身便是一门面向对象编程语言,在设计之初便加入了面向对象功能,在 Python 中,一切皆为对象。

类与实例

定义类

Python 中,使用 class 关键字来定义类,我们定义一个不具有任何功能 Person

class Person:
    pass

其中, pass 是空语句,作为一个占位符,不执行任何操作,同时保持程序结构的完整性,可以在后续为其添加功能。

创建一个 Person 类的实例对象 p

p = Person()

类的属性和方法都是使用 . 运算符来访问,相当于访问实例对象命名空间内的变量。

类和实例对象的命名空间以动态数据结构(字典)的方式实现,可以使用内置属性 __dict__ 来访问。

p.__dict__
# {}

vars 函数可以访问任何对象的 __dict__ 属性值,如果对象没有该属性,则会引发异常

vars(p)
# {}

或者使用 dir 函数来访问,相当于 object.__dict__.keys(),但是会对列表进行排序并添加一些系统属性

dir(p)
# [
#     '__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__'
# ]

实例属性和方法

Python 是一门动态类型语言,我们可以在创建一个类的实例之后,为其添加属性和方法,例如

# 实例添加属性
p.name = 'Tom'
# 为类添加方法
def say_hello(self):
    print('Hello', self.name)
Person.say_hello = say_hello

p.__dict__
# {'name': 'Tom'}

但是我们通常不会这么做,一般都是在类的内部进行定义,并使用专门的构造函数来定义实例的创建

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
        
    def say_hello(self):
        print('Hello', self.name)

其中 __init__Python 众多魔法方法中的一个,是类的初始化函数,定义如何初始化一个类实例对象。例如,上述初始化函数表示在创建一个对象时,需要传入两个参数,第一个参数赋值给实例的 name 属性,第二个参数赋值给实例的 age 属性。

实例方法的第一个参数 self 代表的是实例本身,即每创建一个新的实例,self 都会指向这一实例而不是类,每个实例存储在不同的内存地址中。因此,self.name 访问的是其指向的实例的 name 属性,而不是其他实例或类本身的属性。

记住:所有实例属性和实例方法的第一个参数都表示实例本身,且所有实例方法只能通过实例对象来调用。例如,我们创建两个实例对象

tom = Person(name='Tom', age=19)
jay = Person(name='jay', age=20)
tom.say_hello()
# Hello Tom
jay.say_hello()
# Hello jay

不同的实例对象的 say_hello,引用了其本身的属性值,而不是其他实例对象的值。

其实,实例方法的第一个参数名称可以是任意的(重要的是位置,而不是名字),如 myself 也是可以的,但是最好都统一写出 self

class Person:
    def __init__(myself, name, age):
        myself.name = name
        myself.age = age

类属性和方法

不同于实例方法和属性,类属性和方法是在所有实例中共享的,提供类的默认行为。类属性可以使用类名类访问,例如

class Person:
    counter = 0

    def __init__(self, name, age):
        self.name = name
        self.age = age
        Person.counter += 1
        
tom = Person(name='Tom', age=19)
tom.counter
# 1
jay = Person(name='jay', age=20)
tom.counter
# 2
jay.counter
# 2
Person.counter
# 2

注意:我们将 counter 属性的定义放在构造函数外面,可以看到,该变量在所有实例中共享同一份内存地址。

定义类方法需要使用到装饰器 @classmethod,可以理解为将函数装饰成类方法。类似于实例方法,类方法的第一个参数表示的是类,一般用 cls 来表示,当然你也可以使用其他名称。

class Person:
    counter = 0
    
    def __init__(self, name, age):
        self.name = name
        self.age = age
        Person.counter += 1

    @classmethod
    def number(cls):
        return cls.counter
      
tom = Person(name='Tom', age=19)
print(tom.number())
# 1
jay = Person(name='jay', age=20)
print(Person.number())
# 2

静态方法

静态方法使用装饰器 @staticmethod 来声明,其行为与普通函数一样,只是放在了类的内部定义,使用方法与类方法一样。

class Person:
    counter = 0
    
    def __init__(self, name, age):
        self.name = name
        self.age = age
        Person.counter += 1
    
    @staticmethod
    def get_count():
        return Person.counter

tom = Person(name='Tom', age=19)
print(tom.get_count())
# 1
jay = Person(name='jay', age=20)
Person.get_count()
# 2

一般实例属性和方法比较常用,其定义与外部定义也非常类似,实例属性相当于在类内部环境中的全局变量,实例方法和类方法都只是固定了第一个参数的指向。使用起来也是比较简单的,基本都可以通过实例来进行调用,而我们编程时主要也是面向实例对象。

__slots__ 属性

Python 是一门动态语言,允许我们在程序运行时为对象绑定新的属性和方法,也可以将已有的属性和方法进行解绑,如果我们想要限制自定义类的成员,可以通过 __slots__ 属性进行限定。

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

class PersonSlots:
    __slots__ = ('name', 'age')
    
    def __init__(self, name, age):
        self.name = name
        self.age = age
        
ps = PersonSlots('Tom', 20)
ps.name
# 'Tom'
ps.sex = 'female'
# AttributeError: 'PersonSlots' object has no attribute 'sex'
vars(ps)
# TypeError: vars() argument must have __dict__ attribute

为什么要使用 __slots__ 属性呢?主要有两点好处

  1. 节省内存:不会创建动态数据结构 __dict__ 来存储属性

    p = Person('Jay', 19)
    p.__dict__    # 没有 __slots__ 属性
    # {'name': 'Jay', 'age': 19}
    ps.__slots__  # 没有 __dict__ 属性
    # ('name', 'age')
    
  2. 提高属性访问速度,__slots__ 是静态数据结构,因此无法添加新的属性

    %timeit p.name
    # 52.8 ns ± 1.08 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
    %timeit ps.name
    # 45.9 ns ± 1.72 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
    

继承与多态

单继承

类继承可以扩展现有类的功能,例如,我们定义一个 Person

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
        
    def say_hello(self):
        print('Hello', self.name)

并定义一个 Employee 类继承自 Person

class Employee(Person):
    def __init__(self, name, age, job_title):
        super().__init__(name, age)
        self.job_title = job_title

我们使用 super 函数来访问父类的构造函数,并使用父类的构造函数来初始化 nameage 属性,并添加一个新的属性 job_title

tom = Employee(name='Tom', age=19, job_title='Developer')
tom.say_hello()
# Hello Tom
isinstance(tom, Person)
# True
isinstance(tom, Employee)
# True
issubclass(Employee, Person)
# True
issubclass(Person, object)
# TRUE

所有类都隐式地继承自顶层 object 类。子类会继承父类的属性和方法,定义与父类同名的属性或方法相当于对其重写

class Employee(Person):
    def __init__(self, name, age, job_title):
        super().__init__(name, age)
        self.job_title = job_title
        
    def say_hello(self):
        super().say_hello()
        print('Employee :', self.name)
        
tom = Employee(name='Tom', age=19, job_title='Developer')
tom.say_hello()
# Hello Tom
# Employee : Tom

多继承

Python 允许多重继承,即子类可以继承自多个父类

class A:
    pass

class B:
    pass

class C(A, B):
    pass

继承自多个类,不可避免的一个问题就是多个父类之间存在同名属性和方法的问题,Python 中使用方法解析顺序(MROMethod Resolution Order)来解决这一问题,其核心是 C3 算法。可以使用 mro 函数来获取类的搜索顺序

C.mro()
# [__main__.C, __main__.A, __main__.B, object]

从左至右依次扫描类中是否存在搜索的属性或方法,找到之后便直接执行并不再继续搜索。例如

class Vehicle:
    def __init__(self, name, weight):
        self.name = name
        self.weight = weight

    def start(self):
        print("Let's go go go!")
        
    def accelerate(self):
        print('Run faster!')
        
class Flyable:
    def __init__(self, wing=True):
        self.wing = wing
        
    def fly(self):
        print('I can fly')
        
    def accelerate(self):
        print('Fly faster!')

class Spaceship(Vehicle, Flyable):
    def __init__(self, name, weight, wing):
        super().__init__(name, weight)
        self.wing = wing

Spaceship 类继承自 VehicleFlyable,注意这两个顺序,使用 __mro__ 属性也可以查看搜索列表

Spaceship.__mro__
# (__main__.Spaceship, __main__.Vehicle, __main__.Flyable, object)

因此,在使用 super 调用父类的构造函数时,先找到的是 Vehicle

s = Spaceship('Varyag', 1000, False)
s.fly()
# I can fly
s.accelerate()
# Run faster!
s.start()
# Let's go go go!

多态

多态能够使不同对象对同一消息做出不同的响应。例如

class Person:     
    def speak(self):
        pass
    
class Chinese(Person):
    def speak(self):
        print('I can speak Chinese')
        
class American(Person):
    def speak(self):
        print('I can speak English')
        
def speak(obj):
    obj.speak()

我们定义两个继承自 Person 的类,并分别实现 speak 方法,并定义一个函数,传入一个对象,并调用对象的 speak 方法

c = Chinese()
a = American()
speak(a)
# I can speak English
speak(c)
# I can speak Chinese

但是这里会有一个问题,由于 Python 是动态语言不会检查对象的类型,因此我们只要为 speak 函数传入的对象含有 speak 方法,该函数就会正常运行。例如

class Pretenders:
    def speak(self):
        print('Pretenders')

p = Pretenders()
speak(p)
# Pretenders

这也被称为鸭子类型,即当一只鸟不管是走路、游泳还是叫起来都与鸭子很像,那么这只鸟就可以被称为鸭子。在这里,不需要关注对象的类型,只要保证它们具有相同的行为即可。

封装

封装是为了隐藏程序某一部分的实现细节,在程序外部不可见,只将必要的接口暴露在外面。Python 对于类成员是没有严格的访问控制,默认情况下所有成员都是公开可访问的。

Python 中私有化成员的方式也很简单,只需将成员名称以双下划线开头的命名方式来声明,也有人说使用单下划线的方式来声明私有成员,但这并不会影响成员的访问,只能算是大家约定俗成的习惯。

伪私有成员

如果要设置私有属性或私有方法,可以用

class Person:
    __count = 1
    def __init__(self, name, age):
        self.name = name
        self.age = age
        self.__number = Person.__count
        self.__increase()
        
    def __increase(self):
        Person.__count += 1
        
    def get_number(self):
        print(self.__number)

调用私有成员会抛出异常

tom = Person(name='Tom', age=19)
tom.__number
# AttributeError: 'Person' object has no attribute '__number'
tom.__increase()
# AttributeError: type object 'Person' has no attribute '__count'
Person.__count
# AttributeError: type object 'Person' has no attribute '__count'
tom.get_number()
# 1
jay = Person(name='jay', age=20)
jay.get_number()
# 2

这里说的私有成员并不是不可访问,其实是 Python 将其变换了一个名称,所以称为伪私有成员。我们可以使用 __dict__ 来查看实例对象和类的属性

tom.__dict__
# {'name': 'Tom', 'age': 19, '_Person__number': 1}
tom._Person__number
# 1
Person.__dict__
# mappingproxy({'__module__': '__main__',
#               '_Person__count': 3,
#               '__init__': <function __main__.Person.__init__(self, name, age)>,
#               '_Person__increase': <function __main__.Person.__increase(self)>,
#               'get_number': <function __main__.Person.get_number(self)>,
#               '__dict__': <attribute '__dict__' of 'Person' objects>,
#               '__weakref__': <attribute '__weakref__' of 'Person' objects>,
#               '__doc__': None})
Person._Person__count
# 3

可以看到,私有成员都被重命名为“下划线+类名+成员名”的方式。

特性

特性(property)可以把一个特定属性的访问和赋值操作指向对应的函数或方法,使得我们能够在属性访问和赋值时加入自动运行的代码、并 拦截属性删除或为属性提供文档。

可以使用内置函数 property 为属性添加访问、赋值和删除方法

class Person:
    def __init__(self, name):
        self._name = name
        
    def get_name(self):
        print('get name')
        return self._name
    
    def set_name(self, value):
        print('change name')
        self._name = value
    
    def del_name(self):
        print('delete name')
        del self._name
        
    name = property(fget=get_name, fset=set_name, fdel=del_name, doc="name property doc")
    
tom = Person('Tom')
tom.name
# get name
# 'Tom'
tom.name = 'Robert'
# change name
tom.name
# get name
# 'Robert'
del tom.name
# delete name

使用装饰器 @property 也可以达到相同的目的,包含三种使用方式

  • property:将函数封装为属性,只可访问,无法修改
  • func.setterfunc 为被装饰的函数,为其添加赋值方法
  • func.deleterfunc 为被装饰的函数,为其添加删除方法

对于那些被封装起来属性,我们的本意可能是不希望用户直接去修改,即使要修改,也要对新的值进行类型检查,是否符合规则,例如

class Person:
    def __init__(self, name, age):
        self.__name = name
        self.age = age
        
    @property
    def name(self):
        return self.__name
    
    @name.setter
    def name(self, new_name):
        # 新的名称必须为字符串且全部为英文字母
        if isinstance(new_name, str) and new_name.isalpha():
            self.__name = new_name
        else:
            print('invalid name')
            
    @name.deleter
    def name(self):
        self.__name = ''

我们将 name 属性封装成私有属性,并提供的 gettersetterdeleter 方法,修改属性值时,必须满足条件才会成功执行,在删除属性时,我们仅仅将其赋值为空字符串

p = Person('Tom', 20)
p.name
# 'Tom'
p.name = 123
# invalid name
p.name = 'Jay'
p.name
# 'Jay'
del p.name
p.name
# ’‘

使用 @property 也可以将一个函数装饰成一个特殊的计算属性,让函数的行为看起来是和属性一样

class Rectangle:
    def __init__(self, length, width):
        self.length = length
        self.width = width
        
    @property
    def area(self):
        return self.length * self.width
    
    @property
    def perimeter(self):
        return 2 * (self.length + self.width)
      
r = Rectangle(10, 8)
r.area
# 80
r.length = 12
r.area, r.perimeter
# (96, 40)

当我们想直接修改或删除 property 属性时,会抛出异常。当然,我们也不会去为计算属性进行赋值

r.area = 10
# AttributeError: can't set attribute
del r.perimeter
# AttributeError: can't delete attribute

运算符重载

何为运算符重载,即在自定义类中拦截内置的操作,当对自定义类的实例执行了内置操作,会自动调用你所定义的对应的魔法方法,使自定义类的行为看起来像是内置类。

我们前面介绍了一个魔法方法 __init__ 是专门用于定义类的构造函数,什么是魔法方法,就是为类绑定的特殊方法,可以为自定义类添加一些额外的功能(如,获取长度、切片、算术和逻辑运算等所有内置对象能做的事),它们都是以双下划线开头和结尾的方法。

常见的运算符重载方法

方法功能
__new__创建类实例的静态方法,在构造函数之前被调用
__init__构造函数
__del__析构函数
__repr____str__打印及字符串转换
__call__函数调用
__len__计算长度
__bool__布尔测试
__contains__成员关系测试
__getattr__点号运算
__getattribute____setattr____delattr__属性的获取、设置、删除
__getitem____setitem____delitem__索引与切片、赋值和删除
__iter____next__迭代
__enter____exit__上下文管理器
__lt____gt____le____ge____eq____ne__比较运算
__add____sub____mul____true__div__算术运算
__and____or____xor__逻辑运算

如果没有定义相应的运算符重载方法,大多数内置函数都无法应用到类实例上

属性引用

当我们访问属性时,有两个方法会被调用:__getattr____getattribute__,两者之间的区别在于,不论访问的对象属性是否存在,都会首先执行 __getattribute__,而 __getattr__ 是点运算法拦截器,是属性访问的最后一道防线,如果属性不存在将会抛出异常

class Attribute:
    def __init__(self, name):
        self.name = name
        
    def __getattribute__(self, attr):
        print('getattribute')
        return object.__getattribute__(self, attr)
        
    def __getattr__(self, attr):
        print('getattr')
        raise AttributeError(attr + " con't access!")
        
a = Attribute('Tom')
print(a.name)
# getattribute
# Tom
a.age
# getattribute
# getattr
# AttributeError: age con't access!

__getattribute__ 方法中,我们调用了父类 object 中相应的方法,直接返回属性值。

注意:任何对属性的访问(包括方法)都会调用 __getattribute__ 方法,因此在自定义该方法时这很容易造成递归调用,例如

class Attribute:
    def __init__(self, name):
        self.name = name
        
    def __getattribute__(self, attr):
        print('getattribute')
        if attr.lower() == 'name':
            return object.__getattribute__(self, attr)
        else:
            return self.other()
        
    def other(self):
        print('other')
        
a = Attribute('Tom')
a.age
# RecursionError: maximum recursion depth exceeded while calling a Python object

在访问属性的小写形式不是 name 时,将会调用 other 方法,但是访问 other 方法之前会优先进入 __getattribute__,导致递归调用

__setattr__ 是属性赋值的拦截器,所有试图对属性赋值的操作都会调用 __setattr__ 方法,当我们需要定义该函数时,便不能直接使用 self.attr = value,而需要用到我们前面提到的内置属性 __dict__,或者调用父类的 __setattr__ 方法

class Attribute:
    def __init__(self, name, age):
        self.name = name
        self.age = age
        
    def __setattr__(self, attr, value):
        print('Set value')
        self.__dict__[attr] = value
        
a = Attribute('Tom', 19)
# Set value
# Set value
a.age
# 19

可以看到,在构造函数内部的赋值也会调用 __setattr__ 方法。在重载该方法时需要谨慎,如果不把属性添加到 __dict__ 中,将会导致属性不可用

class Attribute:
    def __init__(self, name, age):
        self.name = name
        self.age = age
        
    def __setattr__(self, attr, value):
        print('Set value')
        if attr in self.__dict__:
            self.__dict__[attr] = value
        else:
            print('Pass')
            
a = Attribute('Tom', 19)
# Set value
# Pass
# Set value
# Pass

属性的访问也可以使用内置函数 getattr,相当于点运算,还可以设置属性不存在时返回的默认值,或抛出异常,而 内置函数 setattr 可用于设置属性值, hasattr 可以用来判断对象是否存在某一属性

class Attribute:
    def __init__(self, name, age):
        self.name = name
        self.age = age
        
a = Attribute('Tom', 19)
getattr(a, 'name')
# 'Tom'
getattr(a, 'sex')
# AttributeError: 'Attribute' object has no attribute 'sex'
getattr(a, 'sex', 'female')
# 'female'
hasattr(a, 'sex')
# False
hasattr(a, 'age')
# True
setattr(a, 'sex', 'female')
hasattr(a, 'sex')
# True
getattr(a, 'sex')
# 'female'

索引和分片

对于实例的索引运算,会自动调用 __getitem__ 方法,而 __ setitem__ 主要用于修改对应索引处的值,__delitem__ 可以删除指定索引处的值。例如

class Container:
    def __init__(self, data):
        self.data = data
        
    def __getitem__(self, index):
        print('Get value')
        return self.data[index]
    
    def __setitem__(self, index, value):
        print('Set value')
        self.data[index] = value
        
    def __delitem__(self, index):
        print('Delete value')
        del self.data[index]
        
c = Container([48, 52, 1.08, 7, 1000, 124, 7])
c[1]
# Get value
# 52
c[1::2]
# Get value
# [52, 7, 124]
c[4] = -1
# Set value
c[4]
# Get value
# -1
del c[0]
# Delete value
c[0]
# Get value
# 52

__getitem__ 方法也可以让实例对象具有迭代功能,for 循环每次循环时都会调用类的 __getitem__ 方法,并持续添加更高的偏移量

class Container:
    def __init__(self, data):
        self.data = data
        
    def __getitem__(self, index):
        return self.data[index]
      
c = Container([48, 52, 1.08, 7, 1000, 124, 7])
for i in c:
    print(i, end=' ')
# 48 52 1.08 7 1000 124 7

而任何支持 for 循环的类也会自动支持 Python 所有的迭代环境,如成员关系、列表解析等

'a' in c
# False
[i // 2 for i in c]
# [24, 26, 0.0, 3, 500, 62, 3]
a, b, c, *_ = c
a, b, d
# (48, 52, 7)
_
# [1.08, 7, 1000, 124]

迭代器

尽管 __getitem__ 也可以支持迭代,但它只是一个退而求其次的方法,Python 中所有迭代都会优先尝试 __iter__ 方法,在其未定义的情况下,才会尝试 __getitem__

迭代是通过内置函数 iter 去搜索 __iter__ 方法,该方法会返回一个迭代器对象(实现了 __next__ 方法的对象),然后重复调用该迭代器对象的 next 方法来不断获取值,直到发生 StopIteration 异常。

class Fibonacci:
    def __init__(self, n):
        self.n = n
        self.a, self.b = 0, 1
        
    def __iter__(self):
        return self
    
    def __next__(self):
        tmp = self.a
        if self.a > self.n:
            raise StopIteration
        self.a, self.b = self.b, self.a + self.b
        return tmp

在这里迭代器对象就是 self,在斐波那契值大于给定值时,使用 raise 来抛出异常,表示迭代结束

for i in Fibonacci(1000):
    print(i, end=' ')
# 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987
f = Fibonacci(10)
f.__next__()
# 0
next(f)
# 1
...
next(f)
# StopIteration: 

内置函数 nextf.__next__ 是等同的,当然,上面的例子改成生成器会更简单

def fib(n):
    a, b = 0, 1
    while a < n:
        yield a
        a, b = b, a+b
f = Fibonacci(10)
for i in fib(10):
    print(i, end=' ')
# 0 1 1 2 3 5 8 

多个迭代器对象

迭代器也可以是一个独立的类,保存其自己的状态信息,从而允许为相同数据创建多个迭代器,例如

class AlphaIterator:
    def __init__(self, alpha):
        self.alpha = alpha
        self.offset = 0
        
    def __next__(self):
        if self.offset >= len(self.alpha):
            raise StopIteration
        value = self.alpha[self.offset]
        self.offset += 1
        return value
    
class Alpha:
    def __init__(self, alpha):
        self.alpha = alpha
        
    def __iter__(self):
        return AlphaIterator(self.alpha)

我们定义了一个用于遍历字符串的迭代器 AlphaIterator,而在 Alpha 中不再返回其自身,因为其未定义 __next__ 方法,并不是一个可迭代对象。

alpha = Alpha('ABCD')
alpha_iter = iter(alpha)
next(alpha_iter), next(alpha_iter)
# ('A', 'B')

我们使用 iter 函数来获取 alpha 中的可迭代对象,然后使用 next 获取迭代器的值

for i in alpha:
    for j in alpha:
        print(i + j, end=' ')
# AA AB AC AD BA BB BC BD CA CB CC CD DA DB DC DD 

每个循环都会获取独立的迭代器对象来记录自己的状态信息

成员关系

成员关系 in 通常被实现为一个迭代,即 __iter____getitem__ 可以支持成员运算,如果要添加特定的成员关系,可以使用 __contains__ 将成员关系定义为一个特定的映射关系或序列搜索方法。该方法优先于 __iter__,而 __iter__ 优先于 __getitem__

class Fibonacci:
    def __init__(self, n):
        self.n = n
        self.a, self.b = 0, 1
        self.seq = []
        
    def __contains__(self, value):
        print('contains: ')
        return value in self.seq
    
    def __iter__(self):
        return self
    
    def __next__(self):
        print('next', end=' ')
        tmp = self.a
        if self.a > self.n:
            raise StopIteration
        self.a, self.b = self.b, self.a + self.b
        self.seq.append(tmp)
        return tmp
      
    def __getitem__(self, index):
        print('get', end=' ')
        return self.seq[index]

测试执行顺序,由于该类是一个迭代器,因此需要先获取值

f = Fibonacci(1000)
for i in f:
    print(i, end=' ')
# next 0 next 1 next 1 next 2 next 3 next 5 next 8 next 13 next 21 next 34 next 55 next 89 next 144 next 233 next 377 next 610 next 987 next 
587 in f
# contains: 
# False
89 in f
# contains: 
# True

当我们注释掉 __contains__ 方法后

f = Fibonacci(1000)
587 in f
# next next next next next next next next next next next next next next next next next next 
# False
89 in f
# next 
# False
f = Fibonacci(1000)
89 in f
# next next next next next next next next next next next next 
# True

可以看到,调用迭代器不断去寻找需要判断的值,当我们再次测试时,由于迭代器已经遍历完,所以找不到值,需要重新创建对象

可以看到, __getitem__ 是没有用到的,优先级最低

class Get:
    def __init__(self, data):
        self.data = data
        
    def __getitem__(self, index):
        print('Get', end=' ')
        return self.data[index]
      
g = Get([0, 1, 1, 2, 3, 5, 8])
4 in g
# Get Get Get Get Get Get Get Get 
# False
5 in g
# Get Get Get Get Get Get 
# True

字符串转换

当我们创建自定义类时,希望在打印类对象时能够有更好的展现形式,而不是输出一串地址,这里需要用到 __str__ (用于打印和调用 str 内置函数时 的输出)和 __repr__(用于其他环境)。

两者之间的区别在于,__repr__ 可用于任何地方,当定义了 __str__ 时,print 和 str 函数会优先使用该方法,如果没有定义,在打印时会使用 __repr__,反之并不成立

class AddStr:
    def __init__(self, value):
        self.value = value
        
    def __str__(self):
        return '[data]: {}'.format(self.value)
      
a = AddStr(10)
a
# <__main__.AddStr at 0x7f55d2335a00>
print(a)
# [data]: 10
str(a)
# '[data]: 10'
class AddRepr:
    def __init__(self, value):
        self.value = value
        
    def __repr__(self):
        return '[data = {}]'.format(self.value)
      
a = AddRepr(5)
a
# [data = 5]
print(a)
# [data = 5]
str(a)
# '[data = 5]'
class AddBoth:
    def __init__(self, value):
        self.value = value
        
    def __repr__(self):
        return '[data = {}]'.format(self.value)
    
    def __str__(self):
        return '[data]: {}'.format(self.value)
      
a = AddBoth(123)
a
# [data = 123]
print(a)
# [data]: 123
str(a)
# '[data]: 123'

右侧运算与原地运算

二元算术运算符都有右侧运算和原地运算,所谓右侧运算即当实例对象在运算符右侧时调用的方法,一般只有在要求运算符具有交换性质时才会用到。例如

class Number:
    def __init__(self, num):
        self.number = num
        
    def __mul__(self, other):
        print('mul')
        if isinstance(other, Number):
            other = other.number
        return Number(self.number * other)
            
    def __rmul__(self, other):
        print('rmul')
        if isinstance(other, Number):
            other = other.number
        return Number(other * self.number)
            
    def __imul__(self, other):
        print('imul')
        if isinstance(other, Number):
            other = other.number
        self.number *= other
        return self
    
    def __repr__(self):
        return "<Number>: %s" % self.number
            
a = Number(10)
b = Number(5)
a * b
# mul
# <Number>: 50
3 * a  # 右侧乘法,调用 __rmul__
# rmul
# <Number>: 30
a *= 2
# imul
a
# <Number>: 20

对象在运算符右侧,需要定义对应 r 开头(__rmul__)的方法,原地运算可以实现以 i 开头(__imul__)的方法,如果没有定义则会调用未加前缀(__mul__)的方法

可调用对象

__call__ 方法用于重载类对象的括号运算符,可以让实例对象像普通函数一样可调用

class ChangeColor:
    def __init__(self, colors):
        self.colors = colors
        
    def __call__(self, index):
        return self.colors[index]
      
c = ChangeColor(colors=['blue', 'yellow', 'red', 'green'])
c(2)
# 'red'
c(0)
# 'blue'

判断对象是否为可调用对象,可以使用内置函数 hasattr 来判断是否存在 __call__ 属性

hasattr(c.colors, '__call__')
# False
hasattr(c, '__call__')
# True
hasattr(hasattr, '__call__')  # 内置函数
# True

或者更简便的 callable 函数

callable(c)
# True
callable(c.colors)
# False
callable(hasattr)
# True

该方法常见于 API 接口函数,例如我们定义一个按钮用于切换颜色,并定义一个颜色类存储颜色属性并记录状态信息,并将切换颜色作为一个回调函数

class Colors:
    def __init__(self, colors):
        self.colors = colors
        self.index = True
        
    def __call__(self):
        self.index = not self.index
        print(self.colors[self.index])
    
class Button:
    def __init__(self, callback):
        self.callback = callback
    
    def click(self):
        # do something
        self.callback()
        
c = Colors(colors=['blue', 'yellow'])
b = Button(c)
b.click()
# blue
b.click()
# yellow

比较运算

一般在需要对实例对象进行排序时会定义相应的比较运算, Python 中有 6 钟比较运算:><>=<===!=,不同于二元算术运算符,比较运算没有右端形式,对于大于(大于等于)或小于(小于等于)操作,如果只定义其中一个,那么另一个运算会相应的进行取反操作,例如

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
        
    def __le__(self, other):
        return self.age <= other.age
    
    def __lt__(self, other):
        return self.age < other.age
      
tom = Person('Tom', 19)
jay = Person('Jay', 20)
tom >= jay, tom > jay
# (False, False)
tom <= jay, tom < jay
# (True, True)

但是对于相等与不等操作却有些区别

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def __eq__(self, other):
        print('equal', end=' ')
        return self.age == other.age
      
tom = Person('Tom', 19)
jay = Person('Jay', 20)
lux = Person('Lux', 19)
tom == lux, tom != jay
# equal equal 
# (True, True)

当只定义 __eq__ 方法时,不相等操作会将该函数的结果取反,该函数调用两次

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def __ne__(self, other):
        print('not equal', end=' ')
        return self.age != other.age
      
tom = Person('Tom', 19)
jay = Person('Jay', 20)
lux = Person('Lux', 19)
tom == lux, tom != jay
# not equal
# (False, True)
tom == tom
# True

可以看到,当只定义了 __ne__ 时,判断相等并不是对其取反,而且该方法只调用了一次,猜测可能调用了内置的 is 来判断相等

布尔测试

当我们对一个自定义类的实例对象进行布尔判断时,默认是 True,可以定义 __bool__ 方法来测试对象布尔值

class String:
    def __init__(self, data):
        self.data = data
        
    def __bool__(self):
        return isinstance(self.data, str)
      
s = String(123)
bool(s)
# False
s = String('aaa')
bool(s)
# True

如果没有定义 __bool__ 方法,则会退而求其次寻找 __len__ 方法,当该方法返回 0 时,则对象为假

class String:
    def __init__(self, data):
        self.data = data
        
    def __len__(self):
        return len(self.data) > 0
      
s = String('aaa')
bool(s)
# True
s = String('')
bool(s)
# False

上下文管理器

上下文管理器用于在某些语句的上下文执行一段代码,可以在运行这部分代码之前,进行一些预处理,以及在执行完代码后做一些清理工作。例如,对文件的读写操作,需要在退出前关闭文件,对数据库的读写,需要先连接数据库,读写完成之后需要关闭数据库连接。

上下文管理器使用两个方法来定义:

  • __enter__:运行代码之前调用该方法
  • __exit__:运行代码之后或者代码出现异常时调用该方法,接受额外的三个参数,分别代表:异常类型、异常内容、异常位置,当代码块未发生异常时这些参数的值都为 None
import sqlite3

class Fruits:
    def __init__(self, db):
        self.db = db
        self.conn = None
        
    def __enter__(self):
        print('Connect to %s' % self.db)
        self.conn = sqlite3.connect(self.db)
        return self
        
    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type:
            pass
        else:
            print('Success')
        self.conn.close()
        self.conn = None
        
    def create(self):
      	print('Create database')
        cur = self.conn.cursor()
        # Create table
        cur.execute('''CREATE TABLE stocks
                       (date text, trans text, item text, count real, price real)''')
        # Insert a row of data
        cur.execute("INSERT INTO stocks VALUES ('2006-01-05','BUY','banana',12, 15.14)")
        cur.execute("INSERT INTO stocks VALUES ('2006-03-28', 'BUY', 'apple', 50, 45.0)")
        cur.execute("INSERT INTO stocks VALUES ('2006-04-06', 'SELL', 'cherry', 10, 53.0)")
        cur.execute("INSERT INTO stocks VALUES ('2006-04-05', 'BUY', 'watermelon', 3, 42.0)")
        # Save (commit) the changes
        self.conn.commit()
        
    def query(self):
        cur = self.conn.cursor()
        # query
        for row in cur.execute('SELECT * FROM stocks ORDER BY price'):
            print(row)

我们定义了一个操作数据库的类,只需传入一个数据库文件名,我们导入了标准库 sqlite3 用于操作数据库,并定义了两个方法用于创建和查询数据库,可以不用关心这两个方法的实现,知道它们的功能即可。

上下文管理器主要使用 with...as 语句来调用

with Fruits('fruits.db') as db:
    db.create()
    db.query()
# Connect to fruits.db
# Create database
# ('2006-01-05', 'BUY', 'banana', 12.0, 15.14)
# ('2006-04-05', 'BUY', 'watermelon', 3.0, 42.0)
# ('2006-03-28', 'BUY', 'apple', 50.0, 45.0)
# ('2006-04-06', 'SELL', 'cherry', 10.0, 53.0)
# Success

可以看到,在执行 with...as 代码块的前后分别执行了 __enter____exit__

下面几节都是类的高级话题,可以跳过,有需求的读者可以继续阅读。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部