Python学习笔记:面向对象高级编程 - Go语言中文社区

Python学习笔记:面向对象高级编程


Python学习笔记:面向对象高级编程

学自廖雪峰巨佬的Python3教程:https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/00143186738532805c392f2cc09446caf3236c34e3f980f000

1.前面讲到过,可以给对象和类绑定方法,但是如果想要限制对象的属性,比如只允许对Student实例添加name和age属性,就可以在定义Class的时候,定义一个特殊的__slots__变量来进行限制

class Student(object):
    __slots__ = ('name', 'age') # 用tuple定义允许绑定的属性名称

__slots__定义的属性仅对当前类实例起作用,对继承的子类是不起作用的,除非在子类中也定义

2.之前提到可以用getter和setter方法来获取数据,但是又没有直接获取属性这么简单,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

>>> 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!

把一个getter方法变成属性,只需要加上@property就可以了,此时,@property本身又创建了另一个装饰器@score.setter,负责把一个setter方法变成属性赋值

代码如下:

#!/usr/bin/env python
# -*- coding: utf-8 -*-


class Screen(object):

    @property
    def width(self):
        return self._width

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

    @property
    def height(self):
        return self._height

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

    @property
    def resolution(self):
        return self._height * self._width

# 测试:
s = Screen()
s.width = 1024
s.height = 768
print('resolution =', s.resolution)
if s.resolution == 786432:
    print('测试通过!')
else:
    print('测试失败!')

3.多重继承

只需要在括号里写上多个类名就行,就可以继承这些类的属性方法

class Bat(Mammal, Flyable):
    pass

多继承的设计通常称之为MixIn

4.定制类

__slots__:用于限制允许绑定的属性
__len__:能让class作用于len()函数
__str__:用于自定义打印属性时的输出,类似于Java里的toString
__repr__:如果直接将属性赋予变量,直接敲变量不用print的时候,会输出内存和类型信息,将__str__的值赋予它就行
__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__:虽然上面的例子已经可以作用于for循环,看起来和list差不多了,但是不能直接取值,如果要像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有个切片方法,对于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

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

但是没有对负数和step参数作处理,可见正确实现一个__getitem__多累

__getattr__:正常情况下,当我们调用类的方法或属性时,如果不存在,就会报错,要避免这种错误,除了可以加上缺少的属性时,Python还有另一个机制,就是写一个__getattr__()方法,动态返回一个属性,注意,这个方法只会在没有找到属性的情况下才调用。

class Student(object):

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

    def __getattr__(self, attr):
        if attr=='score':
            return 99

这样的话如果调用的是score属性,就会返回99,但如果调用其他没有在该方法中定义的值,则会返回None。

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

举个例子:

现在很多网站都搞REST API,比如新浪微博、豆瓣啥的,调用API的URL类似:

如果要写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的增加而改变

这里借用评论区的分析对上面代码进行分析:

1.Chain是类名,进行()运算,即调用__init__,会生成一个实例c1=Chain(path='')
2.对实例c1进行.运算,增加一个status属性,即调用__getattr(self,path),会生成一个新实例c2=Chain(path='/status')
3.以此类推,最后生成'/status/user/timeline/list'

__call__:一般调用实例方法时,是用instance.method()来调用,但只要定义一个__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(object_name)函数来判断

回到上面的问题,有些REST API会把参数放到URL中,比如GihHub的API:

GET /users/:user/repos

调用时,需要把:user替换为实际用户名,通过写出这样的链式调用就可以非常方便的调用API了

Chain().users('michael').repos

代码如下:

#!/usr/bin/env python
# -*- coding: utf-8 -*-


class Chain(object):

    # 固有属性
    def __init__(self, path=''):
        self._path = path

    # 动态调用属性
    def __getattr__(self, path):
        return Chain('%s/%s' % (self._path, path))

    # 定义打印实例时显示的是path
    def __str__(self):
        return self._path

    # 定义在Console直接敲变量时显示的是path
    __repr__ = __str__

    # 直接调用实例时需要调用的方法,当带参数调用时,会把参数在path后面带左斜杠输出,path左边读到的所有字符作为一个整体
    def __call__(self, param):
        return Chain('%s/%s' % (self._path, param))


s2=Chain("www.db.com").users('michael').repo
print(s2)

对s2来做代码过程分析:

Chain('www.db.com'):调用__init__方法,返回对象c1
.users:调用__getattr__方法,增加一个users属性
('michael'):调用__call__方法,将参数加入链条中
.repo:调用__getattr__方法,增加一个repos属性

最后打印出来的结果为:www.db.com/users/michael/repo

5.枚举类

众所周知,Python没有常量一说,通常是用变量名大写来骗自己,但其实还是变量,因此可以用枚举类型来实现它

from enum import Enum

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

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

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

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

@unique装饰器用于检查重复值,以保证没有重复

访问枚举类型的N个方法:

>>> 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

代码如下:

emm?

6.前面提到,type()函数可以查看一个类型或变量的类型,比如Hello是一个class,它的类型就是type,而h是一个实例,它的类型就是class Hello,而class的定义是运行时动态创建的,当Python解释器载入Hello模块时,就会依次执行该模块的所有语句,使用type()函数创建出一个Hello的class对象。因此可知,type()函数可以创建出新的类型,而无需定义

>>> 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'>

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

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

除了使用type()动态创建类以外,要控制类的创建行为,还可以使用metaclass,直译为元类,意思是当我们定义了类后,就可以根据这个类创建出实例,所以:先定义类,然后创建实例,但是如果想创建出类呢?那就必须根据metaclass创建出类。

官方吐槽:metaclass是Python面向对象里最难理解,也是最难使用的魔术代码。正常情况下,你不会碰到需要使用metaclass的情况,所以,以下内容看不懂也没关系,因为基本上你不会用到。

[所以我也不想看了暂时,看了半个多小时啥也没看懂,以后用到再回头补]

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/lrxcmwy2/article/details/85230462
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-03-06 22:58:53
  • 阅读 ( 991 )
  • 分类:

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢