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__
属性呢?主要有两点好处
-
节省内存:不会创建动态数据结构
__dict__
来存储属性p = Person('Jay', 19) p.__dict__ # 没有 __slots__ 属性 # {'name': 'Jay', 'age': 19} ps.__slots__ # 没有 __dict__ 属性 # ('name', 'age')
-
提高属性访问速度,
__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
函数来访问父类的构造函数,并使用父类的构造函数来初始化 name
和 age
属性,并添加一个新的属性 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
中使用方法解析顺序(MRO
,Method 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
类继承自 Vehicle
和 Flyable
,注意这两个顺序,使用 __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.setter
:func
为被装饰的函数,为其添加赋值方法func.deleter
:func
为被装饰的函数,为其添加删除方法
对于那些被封装起来属性,我们的本意可能是不希望用户直接去修改,即使要修改,也要对新的值进行类型检查,是否符合规则,例如
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
属性封装成私有属性,并提供的 getter
、setter
和 deleter
方法,修改属性值时,必须满足条件才会成功执行,在删除属性时,我们仅仅将其赋值为空字符串
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:
内置函数 next
与 f.__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__
下面几节都是类的高级话题,可以跳过,有需求的读者可以继续阅读。
本站资源均来自互联网,仅供研究学习,禁止违法使用和商用,产生法律纠纷本站概不负责!如果侵犯了您的权益请与我们联系!
转载请注明出处: 免费源码网-免费的源码资源网站 » Python 入门 —— 面向对象编程
发表评论 取消回复