对象
访问限制
在外部, 如果未加限制, 我们可以自由地修改一个实例的属性. 如果要让内部属性不被外部直接访问, 我们可以在属性的名称前面加上两个下划线__
. 这样属性就变成了一个"私有"属性, 外部"无法"访问.
例子
定义类:
使用类:
Tip
双下划线开头的变量不是真的无法从外面访问, 无法访问这样的变量是因为解释器将__[var]
变成了_[class]__[var]
, 我们仍然可以通过_[class]__[var]
的形式访问__[var]
变量.
笔记
- 变量名类似
__[var]__
的是特殊变量, 是可以直接访问的, 不是私有变量 - 变量名类似
_[var]
的, 是可以被外部访问的, 但是按照规定, 虽然可以被访问, 但是请将这种变量视为私有变量 - 变量名类似
__[var]
的, 是"无法"被外部访问的
注意
请不要从外部直接设置私有变量, 否则设置的不是那个私有变量, 设置的是另一个变量.
getter和setter
如果要访问或者修改这个"私有"属性, 可以使用getter和setter.
例子
笔记
getter和setter的优势是你可以自定义在读取或者写入属性时发生的操作, 比如参数检查, 避免传入无效参数等等...
绑定限制
动态绑定的灵活性
正常情况下, 我们创建了一个实例之后, 我们可以给该实例绑定任何属性和方法, 这就是动态语言的灵活性.
例子
定义类:
尝试绑定属性:
尝试绑定方法:
>>> def set_age(self, age):
... self.age = age
...
>>> from types import MethodType
>>> s.set_age = MethodType(set_age, s)
>>> s.set_age(18)
>>> s.age
18
注意
给实例绑定一个方法, 对另一个实例是没有作用的
例子
__slots__
如果我们要限制实例的属性该怎么办? 在定义类的时候, 可以用__slot__
这个特殊类属性来限制实例能够添加的属性.
例子
定义类:
尝试绑定属性:
>>> s = Student
>>> s.name = "wenzexu"
>>> s.age = 18
>>> s.score = 99
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'score'
由于score
不在__slot__
当中, 所以无法绑定score
属性.
注意
__slot__
定义的属性仅对当前的实例起作用, 对继承的字类是不起作用的.
除非在字类的定义中也定义__slots__
, 这样, 字类实例允许定义的属性是自身的__slots__
加上父类的__slots__
.
@property
前面讲到, 我们可以用getter, setter方法进行参数检查, 格式化输出... 这种方法略显复杂, 那么如何用类似属性这样的方式来访问/修改实例的属性呢? 我们可以用装饰器的方法, 这里用到的装饰器是内置的@property
, 把一个getter方法变成以属性的方式输出, 只需要添加@property
; 把一个setter方法变成以属性的方式赋值, 只需要添加@[property].setter
.
例子
定义类:
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
以属性的方式读取/修改实例属性:
Tip
如果只定义getter方法, 不定义setter方法就是一个只读属性.
注意
属性的方法名不要和实例的变量重名.
多重继承
一个类继承自不止一个父类, 这叫做多重继承.
例子
class Animal(object):
pass
class Mammal(Animal):
pass
class Runnable(object):
def run(self):
print("Running...")
class Jumpable(object):
def jump(self):
print("Jumping...")
class Dog(Mammal, Runnable, Jumpable):
pass
Dog
类通过多重继承, 获得了Mammal
和Runnable
类的所有功能:
定制类
在类的一些属性中有一些属性/方法是形如__[property]__
的, 这种属性/方法可以帮助我们定制一个类, 如__slots__
可以进行绑定限制.
__str__()
__str__()
特殊方法用于打印实例.
例子
Tip
单独定义一个__str__()
方法后, 直接输出变量输出的还是一串不好看的字符串, 因为直接显示变量调用的是__repr__()
方法. 两者的区别是__str__()
方法返回的是用户看到的字符串, 而__repr__()
方法返回的是开发者看到的字符串.
这可以通过定义__repr__
方法解决.
???+ example "例子"
定义类:
```py
class Student(object):
def __init__(self, name):
self.name = name
def __str__(self):
return 'Student object (name=%s)' % self.name
__repr__ = __str__
```
打印类:
```
>>> print(Student('wenzexu'))
Student object (name: wenzexu)
>>> s = Student('wenzexu')
>>> s
Student object (name: wenzexu)
```
__iter__()
__iter__()
特殊方法可以把一个实例变为迭代器. 该方法返回一个迭代器, 然后for循环就会不断调用该迭代对象的__next__()
方法拿到下一个值, 知道遇到StopIteration
错误退出循环.
例子
定义类:
class Fib(object):
def __init__(self):
self.a, self.b = 0, 1
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循环:
__getitem__()
__getitem__()
特殊方法可以把一个实例变为类似于列表一样的可迭代对象, 可以通过下标的方式取值.
例子
定义类:
class Fib(object):
def __getitem__(self, n):
a, b = 1, 1
for x in range(n):
a, b = b, a + b
return a
用下标访问:
注意
上述的例子无法处理切片的问题, 因为__getitem__()
方法默认为传入的参数是一个int, 所以要做一些改进:
例子
定义类:
class Fib(object):
def __getitem__(self, n):
if isinstance(n, int):
a, b = 1, 1
for x in range(n):
a, b = b, a + b
return a
if isinstance(n, slice):
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
测试切片:
这个例子没有对负数/步长做处理, 所以正确实现一个__getitem__()
方法还是有很多工作要做的.
Tip
如果把对象看成是字典, 我们可以定义__getitem__()
方法接受键值.
笔记
除了__getitem__()
方法, 还有__setitem__()
和__delitem__()
方法, 分别对对象元素进行赋值和删除操作. 总之, 通过上述方法, 我们可以自己定义和Python自带的列表, 元组, 字典类似的类.
__getattr__()
__getattr__()
特殊方法可以解决访问不存在的属性或者不存在的方法的时候报错的问题.
例子
定义类:
调用不存在的属性:
>>> s = Student()
>>> print(s.name)
Michael
>>> print(s.score)
Traceback (most recent call last):
...
AttributeError: 'Student' object has no attribute 'score'
添加__getattr__()
方法:
class Student(object):
def __init__(self):
self.name = 'Michael'
def __getattr__(self, attr):
if attr=='score':
return 99
调用不存在的属性:
笔记
__call__()
__call__()
特殊方法可以在调用实例本身的时候, 执行__call__()
方法.
例子
定义类:
class Student(object):
def __init__(self, name):
self.name = name
def __call__(self):
print('My name is %s.' % self.name)
调用自己:
Tip
__call__()
方法中还可以定义参数, 所以对实例的调用和对普通函数的调用差不多了. 可以完全把对象看为函数, 把函数看为对象. 函数的实例在运行期间动态的创建出来, 所以函数和对象之间的界限会变得很模糊.
Tip
可以通过callable
方法判断一个对象是否是"可调用"对象.
上下文管理器
with
语句用于方便事前和事后执行代码. 任何实现了__enter__()
和__exit__()
方法的对象, 都可以用于with
语句.
例子
下面的Query
实现了__enter__()
和__exit__()
方法, 所以由它产生的对象可以用于with
语句:
class Query(object):
def __init__(self, name):
self.name = name
def __enter__(self):
print('Begin')
return self
def __exit__(self, exc_type, exc_value, traceback):
if exc_type:
print('Error')
else:
print('End')
def query(self):
print('Query info about %s...' % self.name)
这样就可以将自己写的资源用于with
语句:
执行过程:
with
语句首先会调用Query('wenzexu')
对象的__enter__()
方法__enter__()
方法打印'Begin'
, 然后返回self
, 即当前的Query('wenzexu')
对象with
语句将__enter__()
方法的返回值赋值给变量q
, 即q
就是Query('wenzexu')
这个对象- 执行
with
语句块内部的代码, 即q.query()
, 这将打印'Query info about example ...'
- 执行完成后, 无论代码是否抛出异常,
with
语句都会执行Query('wenzexu')
对象的__exit__()
方法 - 如果代码块中没有异常发生,
__exit__()
方法将会打印'End'
; 如果有异常发生, 则打印'Error'
枚举类
使用枚举类(Enum
)可以定义常量.
例子
输入:
from enum import Enum
class HttpStatus(Enum):
OK = (200, "OK")
NOT_FOUND = (404, "Not Found")
INTERNAL_SERVER_ERROR = (500, "Internal Server Error")
def __init__(self, code, message):
self.code = code
self.message = message
print(HttpStatus.OK)
print(HttpStatus.NOT_FOUND)
print(HttpStatus.INTERNAL_SERVER_ERROR)
print(HttpStatus.OK.code)
print(HttpStatus.OK.message)
输出:
笔记
Enum
类在背后做了特殊处理, 使得这些类属性最后是一个该类的对象. 具体过程为根据该类属性的初始值调用__init__
方法创建对象, 并赋值给该类属性.
类的创建
type()
在Python中, 类是在运行时创建的, 不管是已经写死的类也好还是用type()
动态创建的类也罢. 这里, 我们着重看动态创建的类, 因为它比较灵活, 可以在运行的时候根据用户的输入, 程序的状态确定一个类的结构. type()
函数和反射密切相关.
例子
>>> name = input("请输入你的名字: ")
>>> 请输入你的名字: wenzexu
>>> def fn(self):
... print('hello, %s.' % name) # 根据用户的输入, 确定这个类的方法
...
>>> Hello = type('Hello', (object,), dict(hello=fn))
>>> h = Hello()
>>> h.hello()
Hello, wenzexu.
>>> print(type(Hello))
print(type(Hello))
>>> print(type(h))
<class '__main__.Hello'>
笔记
要创建一个类, type()
函数需要3个参数:
- 类的名称
- 继承的父类的集合, 注意如果只有一个父类, 需要加逗号
- 类的方法名称和函数的绑定
Tip
- 正常情况下, 写死的类如
class [class] ...
其实也是通过type()
创建的, 区别就是一开始结构已经写死了, 但是后续其结构是否发生变化取决于是否使用反射修改了结构. -
类在Python中可以被赋值给其他变量名, 变量名只是对类的引用.
元类
元类, metaclass, 可以用于创建类. 流程为: 先定义元类, 再创建类, 最后创建实例.
例子
定义元类:
class ListMetaclass(type):
def __new__(cls, name, bases, attrs):
attrs['add'] = lambda self, value: self.append(value)
return type.__new__(cls, name, bases, attrs)
注意
元类是类的模版, 所以必须继承type
类型.
使用元类创建类:
在传入关键字参数metaclass
的时候, 它指示解释器在创建MyList
的时候, 要通过ListMetaclass.__new__()
来创建.
笔记
__new__()
方法接收到的参数依次是:
- 当前准备创建的类的对象
- 类的名字
- 类继承的父类集合
- 类的方法集合
测试MyList
是否可以调用add()
方法:
使用元类修改类定义比较变态, 在大多数情况下, 直接在MyList
的类定义上加入add()
方法比这简单多了, 但是也有少数情况下, 需要用到元类, 比如ORM, 见例子2.
施工中...
-
访问限制. (n.d.). Retrieved June 14, 2024, from https://www.liaoxuefeng.com/wiki/1016959663602400/1017496679217440 ↩
-
使用__slots__. (n.d.). Retrieved June 14, 2024, from https://www.liaoxuefeng.com/wiki/1016959663602400/1017501655757856 ↩
-
多重继承. (n.d.). Retrieved June 14, 2024, from https://www.liaoxuefeng.com/wiki/1016959663602400/1017502939956896 ↩
-
使用@property. (n.d.). Retrieved June 14, 2024, from https://www.liaoxuefeng.com/wiki/1016959663602400/1017502538658208 ↩
-
定制类. (n.d.). Retrieved June 14, 2024, from https://www.liaoxuefeng.com/wiki/1016959663602400/1017590712115904 ↩
-
使用枚举类. (n.d.). Retrieved June 14, 2024, from https://www.liaoxuefeng.com/wiki/1016959663602400/1017595944503424 ↩
-
使用元类. (n.d.). Retrieved June 14, 2024, from https://www.liaoxuefeng.com/wiki/1016959663602400/1017592449371072 ↩