当前位置:   article > 正文

深度学习之Python面向对象高级编程篇

深度学习之Python面向对象高级编程篇

使用__slots__

创建了一个实例,可以给他绑定任何属性和方法,这些对另一个实例不起作用。而给类class绑定方法,所有实例都可以调用。在动态语言中,甚至支持在程序运行过程中动态地给class添加功能。

想要限制实例的属性,比如只能添加nameage属性,需要在定义class的时候,定义一个特殊的__slots__ 变量,来限制该class实例能添加的属性。

class Student(object):
    __slots__ = ('name', 'age') # 用tuple定义允许绑定的属性名称
  • 1
  • 2
>>> 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'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

注意!__slots__定义的属性仅对当前类实例起作用,对继承的子类是不起作用的:

>>> class GraduateStudent(Student):
...     pass
...
>>> g = GraduateStudent()
>>> g.score = 9999
  • 1
  • 2
  • 3
  • 4
  • 5

除非在子类中也定义__slots__,这样,子类实例允许定义的属性就是自身的__slots__加上父类的__slots__

使用@property

在绑定属性时,直接把属性暴露出去,就没办法检查参数。为了限制score的范围,可以通过一个set_score()方法来设置成绩,再通过一个get_score()来获取成绩,这样,在set_score()方法里,就可以检查参数。但是,这样又略显复杂。

装饰器(decorator) 可以给函数动态加上功能。对于类的方法,装饰器一样起作用。Python内置的@property装饰器就是负责 把一个方法变成属性调用的:

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

@property的实现比较复杂,先考察如何使用。把一个getter方法变成属性,只需要加上@property就可以了,此时,@property本身又创建了另一个装饰器@score.setter,负责把一个setter方法变成属性赋值,于是,我们就拥有一个可控的属性操作:

>>> s = Student()
>>> s.score = 60 # OK,实际转化为s.set_score(60)
>>> s.score # OK,实际转化为s.get_score()
60
>>> s.score = 9999
Traceback (most recent call last):
  ...
ValueError: score must between 0 ~ 100!
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

有了@property,在对实例属性操作的时候,实际上是通过gettersetter方法来实现的。

还可以定义只读属性,只定义getter方法,不定义setter方法就是一个只读属性:

class Student(object):

    @property
    def birth(self):
        return self._birth

    @birth.setter
    def birth(self, value):
        self._birth = value

    @property
    def age(self):
        return 2015 - self._birth
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

上面的birth可读写属性,而age就是一个只读属性。

要特别注意:属性的方法名不要和实例变量重名。

class Student(object):

    # 方法名称和实例变量均为birth:
    @property
    def birth(self):
        return self.birth
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这是因为调用s.birth时,首先转换为方法调用,在执行return self.birth时,又视为访问self的属性,于是又转换为方法调用造成无限递归,最终导致栈溢出报错RecursionError

练习

请利用@property给一个Screen对象加上widthheight属性,以及一个只读属性resolution

函数实现:

class Screen(object):

    @property
    def width(self):
        return self._width
    @width.setter
    def width(self, width):
        self._width = width

    @property
    def height(self):
        return self._height
    @height.setter
    def height(self, height):
        self._height = height

    @property
    def resolution(self):
        return self._width * self._height
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

测试:

# 测试:
s = Screen()
s.width = 1024
s.height = 768
print('resolution =', s.resolution)
if s.resolution == 786432:
    print('测试通过!')
else:
    print('测试失败!')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

测试结果:

PS D:\MyCode\PythonCode\Test> & D:/Tool/Python/Python311/python.exe d:/MyCode/PythonCode/Test/hello.py
resolution = 786432
测试通过!
  • 1
  • 2
  • 3

多重继承

通过多重继承,一个子类就可以同时获得多个父类的所有功能。

class Dog(Mammal, Runnable):
    pass
  • 1
  • 2

在设计类的继承关系时,通常,主线都是单一继承下来的,例如,Dog继承自Mammal。但是,如果需要“混入”额外的功能,通过多重继承就可以实现,比如,让Dog除了继承自Mammal外,再同时继承Runnable。这种设计通常称之为MixIn。为了更好地看出继承关系,把Runnable改为RunnableMixIn

class Dog(Mammal, RunnableMixIn):
    pass
  • 1
  • 2

MixIn的目的就是给一个类增加多个功能,所以,在设计类的时候,我们优先考虑通过多重继承来组合多个MixIn的功能,而不是设计多层次的复杂的继承关系。

Python自带的很多库也使用了MixIn。举个例子,Python自带了TCPServerUDPServer这两类网络服务,而要同时服务多个用户就必须使用多进程或多线程模型,这两种模型由ForkingMixInThreadingMixIn提供。通过组合,我们就可以创造出合适的服务来。

比如,编写一个多进程模式的TCP服务,定义如下:

class MyTCPServer(TCPServer, ForkingMixIn):
    pass
  • 1
  • 2

编写一个多线程模式的UDP服务,定义如下:

class MyUDPServer(UDPServer, ThreadingMixIn):
    pass
  • 1
  • 2

如果你打算搞一个更先进的协程模型,可以编写一个CoroutineMixIn

class MyTCPServer(TCPServer, CoroutineMixIn):
    pass
  • 1
  • 2

定制类

Python中除了__slots____len__(),还有很多可定制的方法,请参考Python的官方文档

__ str__

定义一个Student类,打印一个实例:

>>> class Student(object):
...     def __init__(self, name):
...         self.name = name
...
>>> print(Student('Michael'))
<__main__.Student object at 0x109afb190>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

打印出一堆<__main__.Student object at 0x109afb190>,不好看。可以定义__str__()方法,返回一个好看的字符串,而且容易看出实例内部重要的数据。

>>> 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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

直接敲变量不用print,打印出来的实例还是不好看:

>>> s = Student('Michael')
>>> s
<__main__.Student object at 0x109afb310>
  • 1
  • 2
  • 3

这是因为直接显示变量调用的不是__str__(),而是__repr__(),两者的区别是__str__()返回用户看到的字符串,而__repr__()返回程序开发者看到的字符串,也就是说,__repr__()是为调试服务的。

解决办法是再定义一个__repr__()。但是通常__str__()__repr__()代码都是一样的,所以,有个偷懒的写法:

class Student(object):
    def __init__(self, name):
        self.name = name
    def __str__(self):
        return 'Student object (name=%s)' % self.name
    __repr__ = __str__
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

__ iter__

如果一个类想被用于for ... in循环,类似list或tuple那样,就必须实现一个__iter__()方法,该方法返回一个迭代对象,然后,Python的for循环就会不断调用该迭代对象的__next__()方法拿到循环的下一个值,直到遇到StopIteration错误时退出循环。

以斐波那契数列为例,写一个Fib类,可以作用于for循环:

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 # 返回下一个值
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

现在,试试把Fib实例作用于for循环:

>>> for n in Fib():
...     print(n)
...
1
1
2
3
5
...
46368
75025
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

__ getitem__

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
  • 1
  • 2
  • 3
  • 4

要表现得像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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

现在,就可以按下标访问数列的任意一项了:

>>> f = Fib()
>>> f[0]
1
>>> f[1]
1
>>> f[10]
89
>>> f[100]
573147844013817084101
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

但是list的切片方法,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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

现在试试Fib的切片:

>>> f = Fib()
>>> f[0:5]
[1, 1, 2, 3, 5]
>>> f[:10]
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
  • 1
  • 2
  • 3
  • 4
  • 5

但是没有对step参数和负数作处理:

>>> f[:10:2]
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
  • 1
  • 2

此外,如果把对象看成字典dict,__getitem__()的参数也可能是一个可以作keyobject,例如str

与之对应的是__setitem__()方法,把对象视作list或dict来对集合赋值。最后,还有一个__delitem__()方法,用于删除某个元素

总之,通过上面的方法,我们自己定义的类表现得和Python自带的list、tuple、dict没什么区别,这完全归功于动态语言的 “鸭子类型”,不需要强制继承某个接口。

__ getattr__

正常情况下,当我们调用类的方法或属性时,如果不存在,就会报错。错误信息很清楚地告诉我们,没有找到score这个attribute

要避免这个错误,除了可以加上一个score属性外,Python还有另一个机制,那就是写一个__getattr__()方法,动态返回一个属性

class Student(object):

    def __init__(self):
        self.name = 'Michael'

    def __getattr__(self, attr):
        if attr=='score':
            return 99
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

当调用不存在的属性时,比如score,Python解释器会试图调用__getattr__(self, 'score')来尝试获得属性,这样,我们就有机会返回score的值:

>>> s = Student()
>>> s.name
'Michael'
>>> s.score
99
  • 1
  • 2
  • 3
  • 4
  • 5

返回函数也是完全可以的:

class Student(object):

    def __getattr__(self, attr):
        if attr=='age':
            return lambda: 25
  • 1
  • 2
  • 3
  • 4
  • 5

只是调用方式要变为:

>>> s.age()
25
  • 1
  • 2

注意,只有在没有找到属性的情况下,才调用__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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这实际上可以把一个类的所有属性和方法调用全部动态化处理了,不需要任何特殊手段。这种完全动态调用的特性可以针对完全动态的情况作调用。

__ call__

一个对象实例可以有自己的属性和方法,当我们调用实例方法时,我们用instance.method()来调用。任何类,想直接对实例进行调用,只需要定义一个__call__()方法,__call__()还可以定义参数。。请看示例:

class Student(object):
    def __init__(self, name):
        self.name = name

    def __call__(self):
        print('My name is %s.' % self.name)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

调用方式如下:

>>> s = Student('Michael')
>>> s() # self参数不要传入
My name is Michael.
  • 1
  • 2
  • 3

对实例进行直接调用就好比对一个函数进行调用一样。判断一个对象是否能被调用,能被调用的对象就是一个Callable对象,通过callable()函数,我们就可以判断一个对象是否是“可调用”对象。

>>> callable(Student())
True
>>> callable(max)
True
>>> callable([1, 2, 3])
False
>>> callable(None)
False
>>> callable('str')
False
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

使用枚举类

定义常量时,一个办法是用大写变量通过整数来定义。例如月份:

JAN = 1
FEB = 2
MAR = 3
...
NOV = 11
DEC = 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

好处是简单,缺点是类型是int,并且仍然是变量

更好的方法是为这样的枚举类型定义一个class类型,然后,每个常量都是class的一个唯一实例。Python提供了Enum类来实现这个功能:Enum可以把一组相关常量定义在一个class中,且class不可变,而且成员可以直接比较

from enum import Enum

Month = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'))
  • 1
  • 2
  • 3

这样我们就获得了Month类型的枚举类,可以直接使用Month.Jan来引用一个常量,或者枚举它的所有成员:

for name, member in Month.__members__.items():
    print(name, '=>', member, ',', member.value)
  • 1
  • 2

value属性则是自动赋给成员的int常量,默认从1开始计数。

需要更精确地控制枚举类型,可以从Enum派生出自定义类:

from enum import Enum, unique

@unique
class Weekday(Enum):
    Sun = 0 # Sun的value被设定为0
    Mon = 1
    Tue = 2
    Wed = 3
    Thu = 4
    Fri = 5
    Sat = 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

@unique装饰器可以帮助我们检查保证没有重复值

访问这些枚举类型可以有若干种方法:

>>> day1 = Weekday.Mon
>>> print(day1)
Weekday.Mon
>>> print(Weekday.Tue)
Weekday.Tue
>>> print(Weekday['Tue'])
Weekday.Tue
>>> print(Weekday.Tue.value)
2
>>> print(day1 == Weekday.Mon)
True
>>> print(day1 == Weekday.Tue)
False
>>> print(Weekday(1))
Weekday.Mon
>>> print(day1 == Weekday(1))
True
>>> Weekday(7)
Traceback (most recent call last):
  ...
ValueError: 7 is not a valid Weekday
>>> for name, member in Weekday.__members__.items():
...     print(name, '=>', member)
...
Sun => Weekday.Sun
Mon => Weekday.Mon
Tue => Weekday.Tue
Wed => Weekday.Wed
Thu => Weekday.Thu
Fri => Weekday.Fri
Sat => Weekday.Sat
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

可见,既可以用成员名称引用枚举常量,又可以直接根据value的值获得枚举常量

练习

把Student的gender属性改造为枚举类型,可以避免使用字符串:

函数实现:

# -*- coding: utf-8 -*-
from enum import Enum, unique

class Gender(Enum):
    Male = 0
    Female = 1

class Student(object):
    def __init__(self, name, gender):
        self.name = name
        self.gender = gender
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

测试:

bart = Student('Bart', Gender.Male)
if bart.gender == Gender.Male:
    print('测试通过!')
else:
    print('测试失败!')
  • 1
  • 2
  • 3
  • 4
  • 5

测试结果:

PS D:\MyCode\PythonCode\Test> & D:/Tool/Python/Python311/python.exe d:/MyCode/PythonCode/Test/hello.py
测试通过!
  • 1
  • 2

使用元类

type()

动态语言和静态语言最大的不同,就是函数和类的定义,不是编译时定义的,而是运行时动态创建的。

比方说我们要定义一个Hello的class,就写一个hello.py模块:

class Hello(object):
    def hello(self, name='world'):
        print('Hello, %s.' % name)
  • 1
  • 2
  • 3

当Python解释器载入hello模块时,就会依次执行该模块的所有语句,执行结果就是动态创建出一个Hello的class对象,测试如下:

>>> from hello import Hello
>>> h = Hello()
>>> h.hello()
Hello, world.
>>> print(type(Hello))
<class 'type'> # class的类型是type
>>> print(type(h))
<class 'hello.Hello'> # 实例的类型是class Hello
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

type()函数可以查看一个类型或变量的类型Hello是一个class,它的类型就是type,而h是一个实例,它的类型就是class Hello

我们说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'>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

要创建一个class对象,type()函数依次传入3个参数:

  • class的名称;
  • 继承的父类集合,注意Python支持多重继承,如果只有一个父类,别忘了tuple的单元素写法,要写上,,例如(1,)
  • class的方法名称与函数绑定,这里我们把函数fn绑定到方法名hello上。

通过type()函数创建的类和直接写class是完全一样的,因为Python解释器遇到class定义时,仅仅是扫描一下class定义的语法,然后也是调用type()函数创建出class。

metaclass

除了使用type()动态创建类以外,要控制类的创建行为,还可以使用metaclass

metaclass,直译为元类,简单的解释就是:

当我们定义了类以后,就可以根据这个类创建出实例,所以:先定义类,然后创建实例。
但是如果我们想创建出类呢?那就必须根据metaclass创建出类,所以:先定义metaclass,然后创建类。

连接起来就是:先定义metaclass,就可以创建类,最后创建实例。

所以,metaclass允许你创建类或者修改类。 换句话说,你 可以把类看成是metaclass创建出来的“实例”

metaclass是Python面向对象里最难理解,也是最难使用的魔术代码。正常情况下,你不会碰到需要使用metaclass的情况。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/凡人多烦事01/article/detail/345397
推荐阅读
相关标签
  

闽ICP备14008679号