当前位置: 首页 > news >正文

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实例添加nameage属性。

为了达到限制的目的,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

另一种写法就是可以将 settergetter 作为私有方法隐藏起来:

# 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个参数

  1. class的名称
  2. 继承的父类集合,注意Python支持多重继承,如果只有一个父类,别忘了tuple的单元素写法;
  3. 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 元类,其必须符合以下条件:

  1. 必须显式继承自 type 类
  2. 类中需要定义并实现 __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__() 不限于返回同一个类的实例,所以很容易被滥用,不负责任地使用这种方法可能会对代码有害,所以要谨慎使用。一般来说,对于特定问题,最好搜索其他可用的解决方案,最好不要影响对象的创建过程,使其违背程序员的预期。比如说,前面提到的覆写不可变类型初始化的例子,完全可以用工厂方法(一种设计模式)来替代。

相关文章:

  • wordpress不要的代码注释掉/百度账号设置
  • 湘西建设监理协会网站/东莞网站推广的公司
  • 海淀区seo搜索引擎优化企业/刷关键词优化排名
  • 用网上的文章做网站行吗/下载安装百度
  • 西安市住房城乡建设委官方网站/seo搜索培训
  • wordpress 相应太慢/公司网站seo公司
  • 1813. 句子相似性 III
  • 【PCB专题】什么是工程咨询EQ(Engineer Questions)
  • 基于强化学习Q学习算法的AI下五子棋项目
  • pdf怎么压缩的小一点,这一招超级有效
  • java通过sessionID获取指定session,jetty通过sessionID获取指定session,Jetty的session源码分析
  • Python连接es笔记二之查询方式汇总
  • 光纤内窥镜物镜光学设计
  • 3.Python基础之流程控制
  • Unicorn反混淆:恢复被OLLVM保护的程序(一)
  • 二、django中的路由系统
  • Open3D (C++) 最小生成树用于法向量定向
  • Android面经_111道安卓基础问题(四大组件Activity、Service篇)