python学习笔记---面向对象高级编程【廖雪峰】
面向对象高级编程
数据封装、继承和多态只是面向对象程序设计中最基础的3个概念。在Python中,面向对象还有很多高级特性,允许我们写出非常强大的功能。
我们会讨论多重继承、定制类、元类等概念。
使用__slots__
正常情况下,当我们定义了一个class,创建了一个class的实例后,我们可以给该实例==(而不是该对象)==绑定任何属性和方法,这就是动态语言的灵活性。
# 先定义class
class Student(object):
pass
# 尝试给实例绑定一个属性
>>> s = Student()
>>> s.name = 'Michael' # 动态给实例绑定一个属性
>>> print(s.name)
Michael
# 尝试给实例绑定一个方法
>>> def set_age(self, age): # 定义一个函数作为实例方法
... self.age = age
...
>>> from types import MethodType
>>> s.set_age = MethodType(set_age, s) # 给实例绑定一个方法
>>> s.set_age(25) # 调用实例方法
>>> s.age # 测试结果
25
给一个实例绑定的方法,对另一个实例是不起作用的
>>> s2 = Student() # 创建新的实例
>>> s2.set_age(25) # 尝试调用方法
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'set_age'
为了给所有实例都绑定方法,可以给class绑定方法:
>>> def set_score(self, score):
... self.score = score
...
>>> Student.set_score = set_score
使用__slots__
但是,如果我们想要限制实例的属性怎么办?比如,只允许对Student实例添加name
和age
属性。
为了达到限制的目的,Python允许在定义class的时候,定义一个特殊的__slots__
变量,来限制该class实例能添加的属性:
class Student(object):
__slots__ = ('name', 'age') # 用tuple定义允许绑定的属性名称
>>> s = Student() # 创建新的实例
>>> s.name = 'Michael' # 绑定属性'name'
>>> s.age = 25 # 绑定属性'age'
>>> s.score = 99 # 绑定属性'score'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'score'
使用
__slots__
要注意,__slots__
定义的属性仅对当前类实例起作用,对继承的子类是不起作用的:>>> class GraduateStudent(Student): ... pass ... >>> g = GraduateStudent() >>> g.score = 9999
除非在子类中也定义
__slots__
,这样,子类实例允许定义的属性就是自身的__slots__
加上父类的__slots__
。
注意事项
# 给类的绑定如果不用
>>> Student.set_score = set_score
# 改为:Student.set_score = MethodType(set_score, Student)
为什么就会出现,s2.set_score影响其他实例的score,每个实例set_score都会影响所有实例?
Python3之后,MethodType都是默认python2里面的无None参数。如果用MethodType绑定方法到类上,定义的超级类方法,如果实例按照优先级调用到这个超级类方法,那修改的都是Class的属性,例如score之类,就会出现每个实例都修改同一个Class的属性,后者覆盖、未调用这个方法的新实例直接采用Class的这个类属性、等情况
# ①正常情况
from types import MethodType
class Student(object):
score = 0
pass
def set_score(self, score):
self.score = score
# Student.set_score = MethodType(set_score, Student)
Student.set_score = set_score
s = Student() # 创建新的实例
print(s.score) # 0
s.set_score(100)
print(s.score) # 100
s1 = Student() # 创建新的实例
print(s1.score) # 0
s1.set_score(99)
print(s1.score) # 99
print(s.score) # 100
# ②异常情况
from types import MethodType
class Student(object):
score = 0
pass
def set_score(self, score):
self.score = score
Student.set_score = MethodType(set_score, Student)
# Student.set_score = set_score
s = Student() # 创建新的实例
print(s.score) # 0
s.set_score(100)
print(s.score) # 100
s1 = Student() # 创建新的实例
print(s1.score) # [100]
s1.set_score(99)
print(s1.score) # 99
print(s.score) # [99]
使用@property
https://zhuanlan.zhihu.com/p/64487092
https://zhuanlan.zhihu.com/p/311503904
@property
广泛应用在类的定义中,可以让调用者写出简短的代码,同时保证**对参数进行必要的检查**,这样,程序运行时就减少了出错的可能性。python的@property是python的一种装饰器,是用来修饰方法的。
作用:
我们可以使用@property装饰器来创建只读属性,@property装饰器会将方法转换为相同名称的只读属性,可以与所定义的属性配合使用,这样可以防止属性被修改。
通过@property作用与与属性同名的函数,我们就可以想访问属性那样去访问函数方法,那么这个函数内部是如何对属性进行操作的,对象实例就不得而知,从而实现对属性的保护。
使用场景:
1.修饰方法,是方法可以像属性一样访问。
class DataSet(object):
@property
def method_with_property(self): ##含有@property
return 15
def method_without_property(self): ##不含@property
return 15
l = DataSet()
print(l.method_with_property) # 加了@property后,可以用调用属性的形式来调用方法,后面不需要加()。
>>> 15
print(l.method_without_property()) #没有加@property , 必须使用正常的调用方法的形式,即在后面加()
>>> 15
class DataSet(object):
@property
def method_with_property(self): ##含有@property
return 15
l = DataSet()
print(l.method_with_property()) # 加了@property后,可以用调用属性的形式来调用方法,后面不需要加()。
>>> 报错
如果使用property进行修饰后,又在调用的时候,方法后面添加了(), 那么就会显示错误信息:TypeError: ‘int’ object is not callable,也就是说添加@property 后,这个方法就变成了一个属性,如果后面加入了(),那么就是当作函数来调用,而它却不是callable(可调用)的。
class DataSet(object):
def method_without_property(self): ##不含@property
return 15
l = DataSet()
print(l.method_without_property) #没有加@property , 必须使用正常的调用方法的形式,即在后面加()
>>> 方法存放的地址
没有使用property修饰,它是一种方法,如果把括号去掉,不会报错输出的就会是方法存放的地址。
2.与所定义的属性配合使用,这样可以防止属性被修改。
由于python进行属性的定义时,没办法设置私有属性,因此要通过@property的方法来进行设置。这样==可以隐藏属性名,让用户进行使用的时候无法随意修改==。
类中 单下划线和双下划线的属性,在类外部,均可以使用实例化对象访问并且修改。
区别在于:双下划线的属性名 FileName 被解释器重写为 _ClassName__FileName ,所以访问或者修改它的时候是 实例名._ClassName__FileName ; 而单下划线的属性 _FileName,依然可以用 实例名._FileName 访问或修改
class DataSet(object):
def __init__(self):
self._images = 1
self._labels = 2 #定义属性的名称
@property
def images(self): #方法加入@property后,这个方法相当于一个属性,这个属性可以让用户进行使用,而且用户有没办法随意修改。
return self._images
@property
def labels(self):
return self._labels
l = DataSet()
#用户进行属性调用的时候,直接调用images即可,而不用知道属性名_images,因此用户无法更改属性,从而保护了类的属性。
print(l.images) # 加了@property后,可以用调用属性的形式来调用方法,后面不需要加()。
请问如果想设置setter修改器是不是必须先设置这个访问器而且两个的名字得是一样的?
Demo
class Person:
def __init__(self, name):
self.name1 = name
self.name2 = '小白'
# 利用property装饰器将获取name方法转换为获取对象的属性
@property
def name(self):
return self.name1 + '!'
# 利用property装饰器将设置name方法转换为获取对象的属性
@name.setter # @属性名.setter
def name3(self, n):
self.name1 = '小绿' if n == '小灰' else '小宝'
p = Person('小黑')
print(p.name, p.name1, p.name2, p.name3)
p.name3 = '小灰'
print(p.name, p.name1, p.name2, p.name3)
p.name3 = '小2'
print(p.name, p.name1, p.name2, p.name3)
p.name = '123'
Output:
小黑! 小黑 小白 小黑!
小绿! 小绿 小白 小绿!
小宝! 小宝 小白 小宝!
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-110-90af6f048a4f> in <module>
----> 1 p.name = '123'
AttributeError: can't set attribute
上图中的例子,我们可以直观的感受到 @property 装饰器将调用方法改为了调用属性,即 p.name() 改为了 p.name。
另外,@name.setter 装饰器不仅将调用方法改为了获取指定对象的属性,即 p.name3 对应于 p.name() 亦 p.name;
此外,==对其赋值时==相当于调用了方法,即有 p.name3 = n 对应于 p.name3(n)。
值得留意的是,上述例子背后其实是在操作私有属性 p.name,使用者是透过 setter 方法来管理输入的值,并对 p.name 等属性参数进行赋值影响,直接对私有属性 p.name 进行赋值是不被允许的。
Case 1
我们可以用其来对属性的赋值做判断和异常检测。
# Python program showing the use of
# @property from https://www.geeksforgeeks.org/getter-and-setter-in-python/
class Geeks:
def __init__(self):
self._age = 0
# using property decorator
# a getter function
@property
def age(self):
print("getter method called")
return self._age
# a setter function
@age.setter
def age(self, a):
if(a < 18):
raise ValueError("Sorry you age is below eligibility criteria")
print("setter method called")
self._age = a
Case 2
另一种写法就是可以将 setter 和 getter 作为私有方法隐藏起来:
# https://www.datacamp.com/community/tutorials/property-getters-setters
class FinalClass:
def __init__(self, var):
## calling the set_a() method to set the value 'a' by checking certain conditions
self.__set_a(var)
## getter method to get the properties using an object
def __get_a(self):
return self.__a
## setter method to change the value 'a' using an object
def __set_a(self, var):
## condition to check whether var is suitable or not
if var > 0 and var % 2 == 0:
self.__a = var
else:
self.__a = 2
a = property(__get_a, __set_a)
廖雪峰的官方网站
class Student(object):
@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 = 60 # OK,实际转化为s.score(60);s.score相当于s.score()
>>> s.score # OK,实际转化为s.get_score()
60
>>> s.score = 9999
Traceback (most recent call last):
...
ValueError: score must between 0 ~ 100!
要特别注意:属性的方法名不要和实例变量重名。例如,以下的代码是错误的:
class Student(object):
# 方法名称和实例变量均为birth:
@property
def birth(self):
return self.birth
这是因为调用s.birth
时,首先转换为方法调用,在执行return self.birth
时,又视为访问self
的属性,于是又转换为方法调用,造成无限递归,最终导致栈溢出报错RecursionError
。
多重继承
通过多重继承,一个子类就可以同时获得多个父类的所有功能。
# 首先,主要的类层次仍按照哺乳类和鸟类设计
class Animal(object):
pass
# 大类:
class Mammal(Animal):
pass
class Bird(Animal):
pass
# 各种动物:
class Dog(Mammal):
pass
class Bat(Mammal):
pass
class Parrot(Bird):
pass
class Ostrich(Bird):
pass
# 给动物再加上Runnable和Flyable的功能
class Runnable(object):
def run(self):
print('Running...')
class Flyable(object):
def fly(self):
print('Flying...')
# 对于需要Runnable功能的动物,就多继承一个Runnable
class Dog(Mammal, Runnable):
pass
class Bat(Mammal, Flyable):
pass
MixIn
MixIn是多重继承的一种具体实现
在设计类的继承关系时,通常,主线都是单一继承下来的,例如,
Ostrich
继承自Bird
。但是,如果需要“混入”额外的功能,通过多重继承就可以实现,比如,让Ostrich
除了继承自Bird
外,再同时继承Runnable
。这种设计通常称之为MixIn。
个人理解:MixIn就是将多重继承的思考方式简化了,不再需要考虑复杂的层次关系。只需要考虑单一继承链,然后再额外的功能类可以不考虑。
例如:男人类,单一继承链上他属于人类,如果还要为这个男人类添加其它功能,比如抽烟类,那就用MixIn的写法加进来,但是无需你考虑抽烟类和人类有什么关系。
class Man(Person,ChouyanMixIn)
class Dog(Mammal, RunnableMixIn, CarnivorousMixIn):
pass
# 为了更好地看出继承关系,我们把Runnable和Flyable改为RunnableMixIn和FlyableMixIn。类似的,你还可以定义出肉食动物CarnivorousMixIn和植食动物HerbivoresMixIn,让某个动物同时拥有好几个MixIn:
定制类
Python的class允许定义许多**定制方法,可以让我们非常方便地生成特定的类**。
前期已经学过的
__slots__
、__len__
__str__
>>> class Student(object):
... def __init__(self, name):
... self.name = name
...
>>> print(Student('Michael'))
<__main__.Student object at 0x109afb190>
>>> class Student(object):
... def __init__(self, name):
... self.name = name
... def __str__(self):
... return 'Student object (name: %s)' % self.name
...
>>> print(Student('Michael'))
Student object (name: Michael)
>>> s = Student('Michael')
>>> s
<__main__.Student object at 0x109afb310>
这是因为直接显示变量调用的不是
__str__()
,而是__repr__()
,两者的区别是==__str__()
返回用户看到的字符串,而__repr__()
返回程序开发者看到的字符串==,也就是说,__repr__()
是为调试服务的。
__iter__
如果一个类想被用于for ... in
循环,类似list或tuple那样,就必须实现一个__iter__()
方法,该方法返回一个迭代对象,然后,Python的for循环就会不断调用该迭代对象的__next__()
方法拿到循环的下一个值,直到遇到StopIteration
错误时退出循环。
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)
...
1
1
2
3
5
...
46368
75025
__getitem__
利用动态语言的“鸭子类型”,通过下面的一系列方法,我们自己定义的类表现得和Python自带的list、tuple、dict没什么区别。
Fib实例虽然能作用于for循环,看起来和list有点像,但是,把它当成list来使用还是不行,比如,取第5个元素:
>>> Fib()[5]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'Fib' object does not support indexing
要表现得像list那样按照下标取出元素,需要实现__getitem__()
方法:
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[0]
1
>>> f[1]
1
>>> f[2]
2
>>> f[3]
3
>>> f[10]
89
>>> f[100]
573147844013817084101
但是list有个神奇的切片方法:
>>> list(range(100))[5:10]
[5, 6, 7, 8, 9]
对于Fib却报错。原因是__getitem__()
传入的参数可能是一个int,也可能是一个切片对象slice
,所以要做判断:
class Fib(object):
def __getitem__(self, n):
if isinstance(n, int): # n是索引
a, b = 1, 1
for x in range(n):
a, b = b, a + b
return a
if isinstance(n, slice): # n是切片
start = n.start
stop = n.stop
if start is None:
start = 0
a, b = 1, 1
L = []
for x in range(stop):
if x >= start:
L.append(a)
a, b = b, a + b
return L
现在试试Fib的切片:
>>> f = Fib()
>>> f[0:5]
[1, 1, 2, 3, 5]
>>> f[:10]
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
但是没有对step参数作处理:
>>> f[:10:2]
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
__getattr__
一种完全动态调用的特性,可以针对完全动态的情况作调用。
正常情况下,当我们调用类的方法或属性时,如果不存在,就会报错。
class Student(object):
def __init__(self):
self.name = 'Michael'
>>> s = Student()
>>> print(s.name)
Michael
>>> print(s.score)
Traceback (most recent call last):
...
AttributeError: 'Student' object has no attribute 'score'
要避免这个错误,除了可以加上一个score
属性外,Python还有另一个机制,那就是写一个__getattr__()
方法,动态返回一个属性。修改如下:
class Student(object):
def __init__(self):
self.name = 'Michael'
def __getattr__(self, attr):
if attr=='score':
return 99
当调用不存在的属性时,比如score
,Python解释器会试图调用__getattr__(self, 'score')
来尝试获得属性,这样,我们就有机会返回score
的值:
>>> s = Student()
>>> s.name
'Michael'
>>> s.score
99
返回函数也是完全可以的:
class Student(object): def __getattr__(self, attr): if attr=='age': return lambda: 25
只是调用方式要变为:
>>> s.age() 25
注意,只有在没有找到属性的情况下,才调用__getattr__
,已有的属性,比如name
,不会在__getattr__
中查找。
此外,注意到任意调用如s.abc
都会返回None
,这是因为我们定义的__getattr__
默认返回就是None
。要让class只响应特定的几个属性,我们就要按照约定,抛出AttributeError
的错误:
class Student(object):
def __getattr__(self, attr):
if attr=='age':
return lambda: 25
raise AttributeError('\'Student\' object has no attribute \'%s\'' % attr)
这实际上可以把一个类的所有属性和方法调用全部动态化处理了,不需要任何特殊手段。
作用
举个例子:
现在很多网站都搞REST API,比如新浪微博、豆瓣啥的,调用API的URL类似:
- http://api.server/user/friends
- http://api.server/user/timeline/list
如果要写SDK,给每个URL对应的API都写一个方法,那得累死,而且,API一旦改动,SDK也要改。
利用完全动态的__getattr__
,我们可以写出一个链式调用:
class Chain(object):
def __init__(self, path=''):
self._path = path
def __getattr__(self, path):
return Chain('%s/%s' % (self._path, path))
def __str__(self):
return self._path
__repr__ = __str__
试试:
>>> Chain().status.user.timeline.list
'/status/user/timeline/list'
这样,无论API怎么变,SDK都可以根据URL实现完全动态的调用,而且,不随API的增加而改变!
__call__
一个对象实例可以有自己的属性和方法,当我们调用实例方法时,我们用instance.method()
来调用。能不能**直接在实例本身上调用**呢?在Python中,答案是肯定的。
任何类,只需要定义一个__call__()
方法,就可以直接对实例进行调用。请看示例:
class Student(object):
def __init__(self, name):
self.name = name
def __call__(self):
print('My name is %s.' % self.name)
调用方式如下:
>>> s = Student('Michael')
>>> s() # self参数不要传入
My name is Michael.
__call__()
还可以定义参数。对实例进行直接调用就好比对一个函数进行调用一样,所以你完全可以把对象看成函数,把函数看成对象,因为这两者之间本来就没啥根本的区别。
判断一个对象是否能被调用
如果你把对象看成函数,那么函数本身其实也可以在运行期动态创建出来,因为类的实例都是运行期创建出来的,这么一来,我们就模糊了对象和函数的界限。
那么,怎么判断一个变量是对象还是函数呢?其实,更多的时候,我们需要判断一个对象是否能被调用,能被调用的对象就是一个Callable
对象,比如函数和我们上面定义的带有__call__()
的类实例:
>>> callable(Student())
True
>>> callable(max)
True
>>> callable([1, 2, 3])
False
>>> callable(None)
False
>>> callable('str')
False
通过callable()
函数,我们就可以判断一个对象是否是“可调用”对象。
使用枚举类Enum
Enum
可以把一组相关常量定义在一个class中,且class不可变,而且成员可以直接比较。https://blog.csdn.net/qdPython/article/details/127210034
1.定义
在某些情况下,一个类的对象是有限且固定的,比如季节类,它只有 4 个对象;再比如行星类,目前只有 8 个对象。这种实例有限且固定的类,在 Python 中被称为枚举类。
程序有两种方式来定义枚举类:
直接使用 Enum 列出多个枚举值来创建枚举类。
通过继承 Enum 基类来派生枚举类。
2.使用
使用 Enum 列出多个枚举值来创建枚举类
如下程序示范了直接使用 Enum 列出多个枚举值来创建枚举类:
import enum
#定义枚举类
Season = enum.Enum('Season', ('SPRING', 'SUMMER', 'FALL', 'WINTER'))
上面程序使用 Enum() 函数(就是 Enum 的构造方法)来创建枚举类,该构造方法的==第一个参数是枚举类的类名;第二个参数是一个元组,用于列出所有枚举值。==
在定义了上面的 Season 枚举类之后,程序可直接通过枚举值进行前问,这些枚举值都是该枚举的成员,每个成员都有 name、value 两个属性,其中 name 属性值为该枚举值的变量名,value 代表该枚举值的序号(序号通常从 1 开始)。
例如,如下代码测试了枚举成员的用法:
#直接访问指定枚举对象
print(Season.SPRING)
> Season.SPRING
#访问枚举成员的变量名
print(Season.SPRING.name)
> SPRING
#访问枚举成员的值
print(Season.SPRING.value)
> 1
通过继承 Enum 基类来派生枚举类
也可通过枚举变量名或枚举值来访问指定枚举对象。
#根据枚举变量名访问枚举对象
print(Season['SUMMER']) #输出Season.SUMMER
#根据枚举值访问枚举对象 #输出Season.FALL
print(Season(3))
Python 还为枚举提供了一个==__members__
属性,该属性返回一个 dict 字典,字典包含了该枚举的所有枚举实例==。
程序可通过遍历 __members__
属性来访问枚举的所有实例。
#遍历所有的成员
for name, member in Season.__members__.items():
print(member, '--->', name,'--->', member.value)
#输出如下:
Season.SPRING ---> SPRING ---> 1
Season.SUMMER ---> SUMMER ---> 2
Season.FALL ---> FALL ---> 3
Season.WINTER ---> WINTER ---> 4
通过继承 Enum 基类来派生枚举类
如果要定义更复杂的枚举,则可通过继承 Enum 来派生枚举类,在这种方式下程序就可以为枚举额外定义方法了。
import enum
class Orientation(enum.Enum):
#为序列指定值
EAST = '东'
SOUTH = '南'
WEST = '西'
NORTH = '北'
def info(self):
print('这是一个定义方向为【%s】的枚举'% self.value)
print(Orientation.SOUTH)
print(Orientation.SOUTH.value)
#通过枚举变量访问枚举
print(Orientation['WEST'])
#通过枚举值访问枚举
print(Orientation('北'))
#调用枚举的info()方法
Orientation.EAST.info()
#循环遍历Orientation枚举的所有成员
for name, member in Orientation.__members__.items():
print(member, '--->', name, '--->', member.value)
#输出如下:
Orientation.SOUTH
南
Orientation.WEST
Orientation.NORTH
这是一个定义方向【东】的枚举
Orientation.EAST ---> EAST ---> 东
Orientation.SOUTH ---> SOUTH ---> 南
Orientation.WEST ---> WEST ---> 西
Orientation.NORTH ---> NORTH ---> 北
上面程序通过继承 Enum 派生了 Orientation 枚举类,通过这种方式派生的枚举类**既可额外定义方法,如上面的 info() 方法所示,也可为枚举指定 value(value 的值默认是 1、2、3、…)**。
虽然此时 Orientation 枚举的 value 是由类型,但该枚举同样可通过 value 来访问特定枚举,如上面程序中的 Orientation(‘南’),这是完全允许的。
3.枚举构造器
枚举也是类,因此枚举也可以定义构造器。为枚举定义构造器之后,在定义枚举实例时必须为构造器参数设置值。
import enum
class Gender(enum.Enum):
MALE = '男', '阳刚之力'
FEMALE = '女', '柔顺之美'
def __init__(self, cn_name, desc):
self._cn_name = cn_name
self._desc = desc
@property
def desc(self):
return self._desc
@property
def cn_name(self):
return self._cn_name
#访问FEMALE的name
print('FEMALE的name:', Gender.FEMALE.name)
#访问FEMALE的value
print('FEMALE的value:', Gender.FEMALE.value)
#访问自定义的cn_name属性
print('FEMALE的cn_name:', Gender.FEMALE.cn_name)
# 访问自定义的desc属性
print('FEMALE的desc:', Gender.FEMALE.desc)
> FEMALE的name: FEMALE
> FEMALE的value: ('女', '柔顺之美')
> FEMALE的cn_name: 女
> FEMALE的desc: 柔顺之美
上面程序定义了 Gender 枚举类,并为它定义了一个构造器,调用该构造器需要传入 cn_name 和 desc 两个参数,因此程序使用如下代码来定义 Gender 的枚举值。
MALE = '男', '阳刚之力'
FEMALE = '女', '柔顺之美
上面代码为 MALE 枚举指定的 value 是‘男’和‘阳刚之力’这两个字符串,其实它们会被**自动封装成元组后传给 MALE 的 value 属性**;而且此处传入的‘男’和‘阳刚之力’ 这两个参数值正好分别传给 cnname 和 desc 两个参数。
简单来说,枚举的构造器需要几个参数,此处就必须指定几个值。
使用元类
type()
动态语言和静态语言最大的不同,就是函数和类的定义,不是编译时定义的,而是运行时动态创建的。
class Hello(object):
def hello(self, name='world'):
print('Hello, %s.' % name)
>>> from hello import Hello
>>> h = Hello()
>>> h.hello()
Hello, world.
>>> print(type(Hello))
<class 'type'>
>>> print(type(h))
<class 'hello.Hello'>
type()
函数可以查看一个类型或变量的类型,Hello
是一个class,它的类型就是type
,而h
是一个实例,它的类型就是class Hello
。
创建class的方法
class的定义是运行时动态创建的,而创建class的方法就是使用type()
函数。
type()
函数既可以返回一个对象的类型,又可以**创建出新的类型**,比如,我们可以通过type()
函数创建出Hello
类,而无需通过class Hello(object)...
的定义:
>>> def fn(self, name='world'): # 先定义函数
... print('Hello, %s.' % name)
...
>>> Hello = type('Hello', (object,), dict(hello=fn)) # 创建Hello class
>>> h = Hello()
>>> h.hello()
Hello, world.
>>> print(type(Hello))
<class 'type'>
>>> print(type(h))
<class '__main__.Hello'>
要创建一个class对象,
type()
函数依次传入3个参数:
- class的名称;
- 继承的父类集合,注意Python支持多重继承,如果只有一个父类,别忘了tuple的单元素写法;
- class的方法名称与函数绑定,这里我们把函数
fn
绑定到方法名hello
上。
通过type()
函数创建的类和直接写class是完全一样的,因为Python解释器遇到class定义时,仅仅是扫描一下class定义的语法,然后调用type()
函数创建出class。
正常情况下,我们都用class Xxx...
来定义类,但是,type()
函数也允许我们动态创建出类来,也就是说,动态语言本身支持运行期动态创建类,这和静态语言有非常大的不同,要在静态语言运行期创建类,必须构造源代码字符串再调用编译器,或者借助一些工具生成字节码实现,本质上都是动态编译,会非常复杂。
metaclass
除了使用
type()
动态创建类以外,要**控制类的创建行为**,还可以使用metaclass。
metaclass,直译为元类,简单的解释就是:
当我们定义了类以后,就可以根据这个类创建出实例,所以:先定义类,然后创建实例。
但是如果我们想创建出类呢?那就必须根据metaclass创建出类,所以:先定义metaclass,然后创建类。
连接起来就是:先定义metaclass,就可以创建类,最后创建实例。
所以,metaclass允许你创建类或者修改类。换句话说,你可以把类看成是metaclass创建出来的“实例”。
MetaClass元类,本质也是一个类,但和普通类的用法不同,它可以对类内部的定义(包括类属性和类方法)进行动态的修改。可以这么说,使用元类的主要目的就是为了实现在创建类时,能够动态地改变类中定义的属性或者方法。
举个例子,根据实际场景的需要,我们要为多个类添加一个 name 属性和一个 say() 方法。显然有多种方法可以实现,但其中一种方法就是使用 MetaClass 元类。
如果在创建类时,想用 MetaClass 元类动态地修改内部的属性或者方法,则类的创建过程将变得复杂:先创建 MetaClass 元类,然后用元类去创建类,最后使用该类的实例化对象实现功能。
和前面章节创建的类不同,如果想把一个类设计成 MetaClass 元类,其必须符合以下条件:
- 必须显式继承自 type 类;
- 类中需要定义并实现
__new__()
方法,该方法一定要返回该类的一个实例对象,因为在使用元类创建类时,该__new__()
方法会自动被执行,用来修改新建的类。
#定义一个元类
class FirstMetaClass(type):
# cls代表动态修改的类
# name代表动态修改的类名
# bases代表被动态修改的类的所有父类
# attr代表被动态修改的类的所有属性、方法组成的字典
def __new__(cls, name, bases, attrs):
# 动态为该类添加一个name属性
attrs['name'] = "C语言中文网"
attrs['say'] = lambda self: print("调用 say() 实例方法")
return super().__new__(cls,name,bases,attrs)
此程序中,首先可以断定 FirstMetaClass 是一个类。其次,由于该类继承自 type 类,并且内部实现了 __new__()
方法,因此可以断定 FirstMetaCLass 是一个元类。
可以看到,在这个元类的 __new__()
方法中,手动添加了一个 name 属性和 say() 方法。这意味着,通过 FirstMetaClass 元类创建的类,会额外添加 name 属性和 say() 方法。通过如下代码,可以验证这个结论:
#定义类时,指定元类
class CLanguage(object,metaclass=FirstMetaClass):
pass
clangs = CLanguage()
print(clangs.name)
clangs.say()
可以看到,在创建类时,通过在标注父类的同时指定元类(格式为metaclass=元类名
),则当 Python 解释器在创建这该类时,FirstMetaClass 元类中的 __new__
方法就会被调用,从而实现动态修改类属性或者类方法的目的。
运行上面的程序,输出结果为:
C语言中文网
调用 say() 实例方法
__new__()
方法详解
__new__()
是一种负责**创建类实例**的静态方法,它无需使用 staticmethod 装饰器修饰,且该方法会==优先 __init__()
初始化方法被调用==。
①一般情况下,覆写 __new__()
的实现将会使用合适的参数调用其超类的 super().___new__()
,并在返回之前修改实例。例如:
class demoClass:
instances_created = 0
def __new__(cls,*args,**kwargs):
print("__new__():",cls,args,kwargs)
instance = super().__new__(cls) # 覆写__new__()的实现将会使用合适的参数调用其超类的 super().__new__(),并在返回之前修改实例
instance.number = cls.instances_created
cls.instances_created += 1
return instance
def __init__(self,attribute):
print("__init__():",self,attribute)
self.attribute = attribute
test1 = demoClass("abc")
test2 = demoClass("xyz")
print(test1.number,test1.instances_created)
print(test2.number,test2.instances_created)
输出结果为:
__new__(): <class '__main__.demoClass'> ('abc',) {}
__init__(): <__main__.demoClass object at 0x0000026FC0DF8080> abc
__new__(): <class '__main__.demoClass'> ('xyz',) {}
__init__(): <__main__.demoClass object at 0x0000026FC0DED358> xyz
0 2
1 2
②__new__()
通常会返回该类的一个实例
但有时也可能会返回其他类的实例,如果发生了这种情况,则会跳过对
__init__()
方法的调用。而在某些情况下(比如需要修改不可变类实例(Python 的某些内置类型)的创建行为),利用这一点会事半功倍。比如:class nonZero(int): def __new__(cls,value): return super().__new__(cls,value) if value != 0 else None def __init__(self,skipped_value): #此例中会跳过此方法 print("__init__()") super().__init__() print(type(nonZero(-12))) print(type(nonZero(0)))
__init__() <class '__main__.nonZero'> <class 'NoneType'>
③什么情况下使用 __new__()
在
__init__()
不够用的时候。
例如,前面例子中对 Python **不可变的内置类型(如 int、str、float 等)**进行了子类化,这是因为一旦创建了这样不可变的对象实例,就无法在 __init__()
方法中对其进行修改。
④注意事项
有些读者可能会认为,__new__()
对执行重要的对象初始化很有用,如果用户忘记使用 super(),可能会漏掉这一初始化。虽然这听上去很合理,但有一个主要的缺点,即如果使用这样的方法,那么即便初始化过程已经是预期的行为,程序员明确跳过初始化步骤也会变得更加困难。不仅如此,它还破坏了“__init__()
中执行所有初始化工作”的潜规则。
注意,由于 __new__()
不限于返回同一个类的实例,所以很容易被滥用,不负责任地使用这种方法可能会对代码有害,所以要谨慎使用。一般来说,对于特定问题,最好搜索其他可用的解决方案,最好不要影响对象的创建过程,使其违背程序员的预期。比如说,前面提到的覆写不可变类型初始化的例子,完全可以用工厂方法(一种设计模式)来替代。