学自廖雪峰巨佬的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))
>>> print(type(h))
要创建一个对象,type()函数依次传入3个参数:
除了使用type()动态创建类以外,要控制类的创建行为,还可以使用metaclass,直译为元类,意思是当我们定义了类后,就可以根据这个类创建出实例,所以:先定义类,然后创建实例,但是如果想创建出类呢?那就必须根据metaclass创建出类。
官方吐槽:metaclass是Python面向对象里最难理解,也是最难使用的魔术代码。正常情况下,你不会碰到需要使用metaclass的情况,所以,以下内容看不懂也没关系,因为基本上你不会用到。
[所以我也不想看了暂时,看了半个多小时啥也没看懂,以后用到再回头补]