第二章 面向对象进阶
- 可以给对象赋予一个实例函数 # p.run = types.MethodType(run,p) , # 需要提前导入:import types run为函数 run( )
- python是动态语⾔,动态编程语⾔ 是 ⾼级程序设计语⾔ 的⼀个类别,在计算机科学领域已被⼴泛应⽤。它是⼀类在 运⾏时可以改变其结构 的语⾔ :例如新的函数、对象、甚⾄代码可以被引进,已有的函数可以被删除或是其他结构上的变化,动态语⾔⽬前⾮常具有活⼒。
class Person:
def __init__(self, name = None, age = None):
self.name = name
self.age = age
p1 = Person('诸葛亮', '21')
p1.name # '诸葛亮'
p1.sex = '男'
p1.sex # '男'
- 删除对象与属性的方法:
- del 对象.属性名
- delattr (对象, "属性名")
- 我们知道,正常情况下,当我们定义了一个class,创建了一个class的实例后,我们可以给该实例绑定任何属性和方法,这就是动态语言的灵活性。先定义class:然后尝试给实例绑定一个属性,还可以绑定一个方法,但是一个实例方法对另一个实例不起作用,那就得给类整个类绑定一个方法或属性,这样所有的实例都可以调用需要注意的是我们的动态语言在运行后还能修改的,但是静态语言是不可以的,这就会造成不严谨。
1、'__slots__'
正常情况下,当我们定义了一个class,创建了一个class的实例后,我们可以给该实例绑定任何属性和方法,这就是动态语言的灵活性。
但是,如果我们想要限制class的属性怎么办?比如,上例只允许对人类的实例添加 name 和 age 属性。
为了达到限制的目的,Python允许在定义class的时候,定义一个特殊的 __slots__ 变量,来限制该class能添加 的属性:
- 动态语⾔:可以在运⾏的过程中,修改代码.
- 静态语⾔:编译时已经确定好代码,运⾏过程中不能修改.
- 为了达到限制的目的,Python允许在定义class的时候,定义一个特殊的 __slots__ 变量,来限制该class能添加的属性:
- 需要注意一点的是:使⽤ __slots__ 要注意, __slots__ 定义的属性仅对当前类实例起作⽤,对继承的⼦类是不起作⽤的
class Student(object):
# 用tuple定义允许绑定的属性名称
__slots__ = ('name', 'age')
s1 = Student()
s1.name = "刘备"
s1.age = 20
s1.score = 100 # 不能赋值'score', 'Student' object has no attribute 'score'
2、@property
我们可以看到类中的属性被随意修改,很明显不能这样操作,为了限制score的范围,可以通过一个 set_score( ) 方法来设置成绩,再通过一个 get_score( ) 来获取成绩,这样,在 set_score( ) 方法里,就可以检查参数
class Student:
def get_score(self):
return self._score
def set_score(self, value):
if not isinstance(value, int):
raise ValueError('score must be an integer!')
if value < 0 or value > 100:
raise ValueError('score must between 0 ~ 100!')
self._score = value
s = Student()
s.set_score(60) # ok!
s.get_score() # 60
s.set_score(9999) # 报错, score must between 0 ~ 100!
- 我们发现,当我们设置限定score的范围的时候,超过对应的范围set_score设置值时raise方法会自动给我们抛出异常,但是上面的方法太过于复杂,所以引入我们的一个装饰器,装饰器是可以给函数动态加上功能。对于类的方法,装饰器一样起作用。Python内置的 @property 装饰器就是负责把一个方法变成属性调用的
- @property 的实现比较复杂,先观察使用。把一个 getter方法 变成属性,只需要加上 @property 就可以了,此时, @property 本身又创建了另一个装饰器 @score.setter ,负责把一个setter方法变成属性赋值,于是,我们就拥有一个可控的属性操作
class Student(object):
def __init__(self):
self.__score = 10
@property
def score(self):
return self.__score
@score.setter
def score(self, value):
if not isinstance(value, int):
raise ValueError('score must be an integer!')
if value < 0 or value > 100:
raise ValueError('score must between 0 ~ 100!')
self.__score = value
s = Student()
s.score=50
print(s.score) # 50
3、多重继承
- 继承是面向对象编程的一个重要的方式,因为通过继承,子类就可以扩展父类的功能:
- 通过多重继承,一个子类就可以同时获得多个父类的所有功能。
- MixIn的目的就是给一个类增加多个功能,这样,在设计类的时候,我们优先考虑通过多重继承来组合多个MixIn的功能,而不是设计多层次的复杂的继承关系
- 这样一来,我们不需要复杂而庞大的继承链,只要选择组合不同的类的功能,就可以快速构造出所需的子类
4、定制类
- __len__() 方法我们也知道是为了能让class作用于 len() 函数。
- __slots__方法:
- __init__方法:
__str__方法:
def __str__(self): # 用于定制对象的描述信息
return 'Student object (name: %s)' % self.name
- 这是因为直接显示变量调用的不是 __str__() ,而是 __repr__() ,两者的区别是 __str__() 返回用户看到的字符串,而 __repr__() 返回程序开发者看到的字符串,也就是说, __repr__() 是为调试服务的。
- 解决办法是再定义一个 __repr__() 。但是通常 __str__() 和 __repr__() 代码都是一样的,所以,有个偷懒的写法:__repr__ = __str__
class Student(object):
def __init__(self, name):
self.name = name
def __str__(self):
return 'Student object (name=%s)' % self.name
__repr__ = __str__
__iter__方法:
# person类默认不是可迭代对象,变成一个可迭代对象,必须返回一个迭代器
- 如果一个类想被用于 for ... in 循环,类似list或tuple那样,就必须实现一个 __iter__() 方法,该方法返回一个迭代对象,然后,Python的for循环就会不断调用该迭代对象的 __next__() 方法拿到循环的下一个值,直到遇到 StopIteration 错误时退出循环。我们以斐波那契数列为例,写一个Fib类,可以作用于for循环:
- 通常结合:【 def __next__(self): 】 进行使用
class Fib(object):
def __init__(self):
self.a, self.b = 0, 1 # 初始化两个计数器a,b
def __iter__(self):
return self # 实例本身就是迭代对象,故返回自己
def __next__(self):
self.a, self.b = self.b, self.a + self.b # 计算下一个值
if self.a > 100000: # 退出循环的条件
raise StopIteration()
return self.a # 返回下一个值
for n in Fib():
print(n)
__getitem__ 方法:
- Fib实例虽然能作用于for循环,看起来和list有点像,但是,把它当成list来使用还是不行,比如,取第5个元素就不行
- 要表现得像list那样按照下标取出元素,需要实现 __getitem__() 方法:通过下标进行取值
- 此外,如果把对象看成 dict , __getitem__() 的参数也可能是一个可以作key的object,例如 str 。与之对应的是 __setitem__() 方法,把对象视作list或dict来对集合赋值。最后,还有一个 __delitem__() 方法,用于删除某个元素。
- 总之,通过上面的方法,我们自己定义的类表现得和Python自带的list、tuple、dict没什么区别,这完全归功于动态语言的“鸭子类型”,不需要强制继承某个接口。
class Fib(object):
def __getitem__(self, n):
a, b = 1, 1
for x in range(n):
a, b = b, a + b
return a
f = Fib()
f[2] # 1
__getattr__方法:
- 正常情况下,当我们调用类的方法或属性时,如果不存在,就会报错。
- 要避免这个错误,除了可以加上一个 score 属性外,Python还有另一个机制,那就是写一个 __getattr__() 方法,动态返回一个属性。
- 当调用不存在的属性时,比如 score ,Python 解释器会试图调用 __getattr__(self, 'score') 来尝试获得属性,这样,我们就有机会返回 score 的值。
- 只有在没有找到属性的情况下,才调用 __getattr__ ,已有的属性,比如 name ,不会在 __getattr__中查找。
- 此外,注意到任意调用如 s.abc 都会返回 None ,这是因为我们定义的 __getattr__ 默认返回就是 None 。要让class只响应特定的几个属性,我们就要按照约定,抛出 AttributeError 的错误。
- 这实际上可以把一个类的所有属性和方法调用全部动态化处理了,不需要任何特殊手段。这种完全动态调用的特性有什么实际作用呢?作用就是,可以针对完全动态的情况作调用。
class Student(object):
def __init__(self):
self.name = 'Michael'
def __getattr__(self, attr):
if attr=='score':
return 99
s = Student()
s.score # 99
__call__方法:
- 一个对象实例可以有自己的属性和方法,当我们调用实例方法时,我们用 instance.method() 来调用。能不能直接在实例本身上调用呢?
- __call__() 还可以定义参数。对实例进行直接调用就好比对一个函数进行调用一样,所以你完全可以把对象看成函数,把函数看成对象,因为这两者之间本来就没啥根本的区别。
- 如果你把对象看成函数,那么函数本身其实也可以在运行期动态创建出来,因为类的实例都是运行期创建出来的,这么一来,我们就模糊了对象和函数的界限。
- 那么,怎么判断一个变量是对象还是函数呢?其实,更多的时候,我们需要判断一个对象是否能被调用,能被调用的对象就是一个 Callable 对象,比如函数和我们上面定义的带有 __call__() 的类
- 通过 callable() 函数,我们就可以判断一个对象是否是“可调用”对象。
class Student(object):
def __init__(self, name):
self.name = name
def __call__(self):
print('My name is %s.' % self.name)
s = Student('Michael')
s() # My name is Michael.
5、枚举类
from enum import Enum
Month = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'))
for name, member in Month.__members__.items():
print(name, '=>', member, ',', member.value) # Jan => Month.Jan , 1
- 当我们需要定义常量时,一个办法是用大写变量通过整数来定义
- 但是这样的定义的类型是 int ,并且仍然是变量,并且在运算中,无法时时创建对应的值,当然这种指代是以更好的方式去使用变量数值。
- 这里存在更好的方法是为这样的枚举类型定义一个class类型,然后,每个常量都是class的一个唯一实例。Python提供了 Enum 类来实现这个功能。
- 这样我们就获得了 Month 类型的枚举类,可以直接使用 Month.Jan 来引用一个常量,或者枚举它的所有成员通过for循环进行取值,Month.members是Month的内置变量,可以打印成员标签。
- 如果在这个方法调用上.items()函数所有成员都打印出来,数据类型为元组
6、元类
- Python属于动态类型的语言,而动态语言和静态语言最大的不同,就是函数和类的定义,不是编译时创建的,而是运行时动态创建的,比方说我们要定义一个 Hello 的class,就写一个 hello.py 模块
- 这里是用来 type() 函数,可以查看一个类型或变量,的类型, Hello 是一个class,它的类型就是 type ,而 h是一个实例,它的类型就是class Hello
- 我们通过 type() 函数创建的类和直接写class是完全一样的,因为Python解释器遇到class定义时,仅仅是扫描一下class定义的语法,然后调用 type() 函数创建出class
- 除了使用 type() 动态创建类以外,要控制类的创建行为,还可以使用metaclass,也就是元类 语法:type(name, bases, dict)
- 当我们定义了类以后,就可以根据这个类创建出实例,所以:先定义类,然后创建实例
- 但是如果我们想创建出类呢?那就必须根据metaclass创建出类,所以:先定义metaclass,然后创建类元类就是用来创建这些类(对象)的,元类就是类的类
- __new__()方法接收到的参数依次是:
- 1. 当前准备创建的类的对象;
- 2. 类的名字;
- 3. 类继承的父类集合;
- 4. 类的方法集合。
- 函数type实际上是一个元类。type就是Python在背后用来创建所有类的元类。现在你想知道那为什么type会全部采用小写形式而不是Type呢?有可能也是为了和str保持一致性,str是用来创建字符串对象的类,而int是用来创建整数对象的类。type就是创建类对象的类。你可以通过检查class属性来看到这一点。Python中所有的东西,注意,我是指所有的东西——都是对象。这包括整数、字符串、函数以及类。它们全部都是对象,而且它们都是从一个类创建而来,这个类就是type。
- 元类就是创建类这种对象的东西。type就是Python的内建元类
- 元类的主要目的就是为了当创建类时能够自动地改变类。通常,你会为API做这样的事情,你希望可以创建符合当前上下文的类。
- metaclass实际上可以被任意调用,它并不需要是一个正式的类。