2021-08-23 读书笔记:Python 学习手册(4)

本文介绍了Python中类设计的关键概念,如运算符重载、特殊方法(__init__,__del__,__add__等)的应用,以及类的继承、多态、封装和析构。此外,还涵盖了模块设计、迭代器、属性操作、装饰器和元类等内容。
摘要由CSDN通过智能技术生成

读书笔记:Python 学习手册(4)

结于2021-08-23;
OREILY的书籍,可读性很强,入门类,而且这本书很厚;

  • 第六部分 类和OOP(2)

第六部分 模块 ———— 第29章 运算符重载

本章继续类机制:运算符重载;

运算符重载:在类方法中拦截内置的操作;当类的实例出现在内置操作中,Python自动调用你的方法;

  • 类可以重载所有的Python表达式运算符,
  • 类也可以重载打印、函数调用、属性点号运算等内置运算;
  • 重载使类实例的行为像内置类型;
  • 重载是通过提供特殊名称的类方法来实现的;
  • 运算符重载的方法可被继承;
__init__ # 构造函数
__del__ # 析构函数
__add__ # +
__or__ # | 、 or
__repr__, __str__ # 打印 转换
__call__ # 函数调用
__getattr__ # 点号运算
__setattr__ # 属性赋值
__delattr__ # 属性删除
__getattribute__ # 属性获取
__getitem__ # 索引运算(包括切片)
__setitem__ # 索引赋值(包括切片)
__delitem__ # 索引删除(包括切片)
__len__ #  len(X)
__bool__ # bool(X)
__contains__ # 成员关系测试 item in X
__index__ # 整数值 如hex(X)、O[X]
__enter__, __exit__ # 环境管理器 with obj as var:
__get__, __set__, __delete__ # 描述符属性 获取 赋值和删除
...

__lt__ # <>
__gt__ # >
__eq__ # ==
__ne__ # !=

对内置对象所能做的事,几乎都有相应的特殊名称的重载方法;多数重载方法只用在需要对象行为表现得像内置类型一样的高级程序中;

# number.py
class Number:
  def __init__(self,start):
    self.data = start

  def __sub__(self, other):
    return Number(self.data - other)  

from number import Number
X = Number(5)
Y = X - 2
Y.data # 3

# 
class Indexer:
  def __getitem__(self, index):
    return index ** 2

X = Indexer()
X[2] # 4

# 分片边界绑定到一个分片对象
L[2::1] 
L[slice(2, None, 1)] # 二者等价

# __getitem__ 也是针对分片(一个分片对象)的调用


def __setitem__(self, index, value):
  self.data[index] = value


__getitem__也可以是Python中一种重载迭代的方式;for循环每次循环都会调用类的该方法,并持续匹配更高的偏移值;

任何会响应索引运算的内置或用户定义的对象,同样会响应迭代;任何支持for循环的类也会自动支持Python所有迭代环境(如:in测试、列表解析、内置函数map、列表元组转换);

迭代器对象 iternext

虽然使用__getitem__对迭代也有效,但是__iter__才是优先级更高的响应方法;前者是对对象进行索引运算,后者则是在支持迭代协议;先找__iter__,再找__getitem__

内置函数iter会尝试寻找__iter__来实现迭代环境,返回的是一个迭代器对象;之后重复调用迭代对象的next方法(next(I)I.__next__()是相同的);

class Squares:
  def __inite__(self, start, stop):
    self.value = start - 1
    self.stop = stop
  def __iter__(self):
    return self
  def __next__(self):
    if self.value == self.stop:
      raise StopIteration
    self.value += 1
    return self.value ** 2

for i in Squares(1,5): # 这个对象循环一次 之后 就变为空了,每次循环 都需新建一个迭代器对象
  print(i, end='')     
# 1 4 9 16 25

有时__iter__会比___getitem_难用,迭代器是用来迭代的,不是随机的索引运算;

上边的例子,也可以用生成器函数编写:

def gsquares(start, stop):
  for i in range(start, stop):
    yield i ** 2

for i in gsquares(1, 5):
  print(i)

多迭代器

  • 生成器函数和表达式,以及map和zip这样的内置函数,都是单迭代器
  • range内置函数和其他的内置类型,如列表,支持地理位置的多个活跃迭代器

要支持多个迭代器的效果,__iter__只需迭代器定义新的状态对象,而不是返回self;

# 仔细看这个示例
class SkipIterator:
  def __init__(self, wrapped):
    self.wrapped = wrapped
    self.offset = 0
  def __next__(self):
    if self.offset >= len(self.wrapped)
      raise StopIteration
    else:
      item = self.wraped[self.offset]
      self.offset += 2
      return item

class SkipObject:
  def __init__(self, wrapped):
    self.wrapped = wrapped
  def __iter__(self):
    return SkipIterator(self.wrapped)

if __name__ == '__main__':
  alpha = 'abc'
  skipper = SkipObject(alpha)
  I = iter(skipper)
  print(next(I)) # 0

  for x in skipper:
     for y in skipper:
       print(x+y)
# aa ac ca cc

迭代器是一次产生一个值,而不是把所有值生成后的列表载入内存;节省了实际空间;

成员关系:contains itergetitem

在迭代领域,类通常把in成员关系运算符实现为一个迭代

  • 可以使用__iter____getitem__方法;
  • 特定方法的话,使用__contains__
  • __contains__优先于__iter__
  • __iter__优先于__getitem__
def __contains__(self, x):
  return x in self.data

__getitem__方法往往更为通用,它拦截了迭代、显式索引、分片(使用分片对象);而在更加现实的场景下__iter__方法可能更容易被编写,因为它不必管理整数索引;__contains__更多的是考虑最为一种特殊情况优化成员关系;

属性引用: getattrsetattr

拦截属性点号运算:

  • 当通过未定义(不存在)属性名和实例进行点号运算时,就会被属性名称作为字符串调用这个方法;
  • 如果Python可通过其继承树搜索流程找到这个属性,该方法不会被调用;
def __getattr__(self, attrname):
  if attrname == 'age':
    return 40
  else:
    raise AttributeError, attrname

对于类不知道如何处理的属性,这个方法会引发内置的AttributeError异常;

__setattr__方法:

  • 会拦截所有属性的赋值语句;
  • self.attr = value => self.__setattr__('attr', value)
  • 注意不能在该方法中再次调用self属性赋值,否则会死循环,导致栈溢出;
  • 如果仍想赋值,可以使用self.__dict__['name'] = x,而不是self.name = x

__getattribute__方法:

  • 拦截所有的属性获取,不只是那些未定义的;
  • 同样,使用它时,要很小心的避免循环;

模拟实例属性私有性(一部分)

class PrivateExc(Exception):pass
# 为私有属性 赋值会触发异常
class Privacy:
  def __setattr__(self, attrname, value):
    if attrname in self.privates:
      raise PrivateExc(attrname, self)
    else:
      self.__dict__[attrname] = value

class Test2(Privacy):
  privates = ['name', 'pay']
  def __init__(self):
    self.__dict__['name'] = 'Tom'

reprstr

  • __str__方法,在print顶层打印和调用str内置函数时,会优先使用它;此外的其他需要格式化字符串的场景,均使用__repr__方法;
  • 没有__str__方法,所有场景均使用__repr__方法;
  • 两个方法都必须返回字符串;

为了定制所有环境,请编写__repr__,而不是__str__

右侧加法 和 原处加法:raddiadd

仅当+右侧对象是实例对象,而左侧对象不是时,会调用__radd__;结合__add__提供了运算符的交换性;

class Computer:
  def __init__(self, val):
    self.val = val
  def __add__(self, other):
    if isinstance(other, Computer):
      other = other.val
    return Computer(self.val + other)
  def __radd__(self, other):
    return Computer(other + self.val)

x = Computer(88)
y = Computer(99)

x + 2 # __add__ 结果的是一个新实例
1 + y # __radd__ 结果的是一个新实例

x + y # __add__ 结果的是一个新实例

为实现+=原处扩展相加,编写__iadd____add__;前者空缺会使用后者;

def __iadd__(self, other):
  self.val += other
  return self

每个二元运算符都有类似的右侧和原处重载方法,如__mul__ __rmul__ __imul__

Call表达式:call

把实例当做函数一样调用,会使用__call__方法;

class Callee:
  def __call__(self, *pargs, **kargs):
    pass

C = Callee()
C(1,2,3)
C(*[1,2], **dict(c=3, d=4))

在需要编写遵循函数调用对象,同时又能保留状态信息时,__call__很有用;比如编写回调函数时,你就可以编写一个这样的实例对象来替代函数进行传递;

Python中偶尔还会用两种其他方式,来把信息和对调函数联系起来:

  • 使用lambda表达式的默认参数:(lambda color='red': 'turn' + color)
  • 使用类的绑定方法,即直接传递 instance.method到callback参数;

关于 __bool____len__需要注意一点,在进行布尔测试时,如果没有实现__bool__,就会根据__len__方法的返回值是否为0,进行判断(非零为真);如果两个方法都没有定义,那么对象将毫无疑义的被看做真;

对象析构函数:del

  • 产生实例:调用构造函数__init__
  • 实例空间被回收(垃圾回收时):调用析构函数__del__,会自动执行

但Python中的析构函数并不常用:

  • 原因一:Python在实例回收时,会自动收回实例所拥有的所有空间
  • 原因二:一般也无法轻易预测实例何时回收,通常最好是在有意调用的方法中(try/finally)编写代码终止活动;
  • 某些情况下,系统表还会引用该对象,导致析构函数无法执行;

循环引用会阻止垃圾回收,一个可选的循环检测器,是默认可用的,但是只有没有__del__时才可用;

在C实现的Python中,不需要在析构函数中关闭由实例打开的文件,他们在回收时会被自动关闭;

后续还会学习其他新的重载方法:__enter__ __exit__ __get__ __set__


第30章 类的设计

  • 设计模式:继承 组合 委托 工厂
  • 伪私有属性 多继承 边界方法
  • 继承:基于Python属性查找
  • 多态:调用x.method的意义取决于x的类型
  • 封装:数据隐藏,方法和运算符实现行为

注意:有些OOP语言把多态定义成基于参数类型标记的重载函数,但在Python中没有类型声明,因此行不通;多个同名方法仅最后一个会生效;

Python中多态是基于对象接口的,而不是类型;


class Employee:
  def __init__(self,name):
    self.name = name

klass = Employee
obj = klass(klass.__name__)

相比继承,组合不是集合的成员关系,而是组件,是整体的组成部分;

  • 继承:is-a
  • 组合:has-a
class PizzaShop:
  # 容器 控制器
  def __init__(self):
    # 嵌入其他对象
    self.server = Server('')
    self.chef = PizzaRobot('')
    self.oven = Oven()

流处理器基于类组合的实现:

def processor(reader, converter, writer):
  while 1:
    data = reader.read()
    if not data:break
    data = converter(data)
    writer.write(data)

# 类组合
class Processor:
  def __init__(self, reader, writer):
    self.reader = reader
    self.writer = writer
  def process(self):
    while 1:
      data = self.reader.readline()
      if not data: break
      data = self.converter(data)
      self.writer.write(data)
  def converter(self, data):
    assert False, 'converter must bu defined'

# 该转换器 需子类填充 
class Uppercase(Processor):
  def converter(self,data):
    return data.ipper()

import sys
obj = Uppercase(open('spam.txt', sys.stdout))
obj.process()

在较大型系统中,继承和组合往往很常用,而且是互补的;

类和持续性

import pickle
object = someClass()
file = open(filename, 'wb')
pickle.dump(object, file) # 把内存的对象转换为序列化的字节流(可保存为文件 也可以通过网络请求发出去)

import pickle
file = open(filename, 'rb')
object = pickle.load(file)



import shelve
object = someClass()
dbase = shelve.open('filename')
dbase['key'] = object

import shelve
dbase = shelve.open('filename')
object = dbase['key']

委托 delegation

通常指控制器对象内嵌其他对象,而把运算请求传给那些对象;控制器负责管理;

在Python中,委托通常以__getattr__钩子方法实现,因为这个方法会拦截对不存在属性的获取;

# 包装者类
class wrapper:
  def __init__(self, object):
    # 被包装对象
    self.wrapped = object
  def __getattr__(self, attrname):
    return getattr(self.wrapped, attrname)

x = wrapper([1,2,3])
x.append(4)

类的伪私有属性

压缩的变量名,有时会被误认为私有,所以也称之为伪私有;它其实是一种把类所创建的变量名局部化的方式而已主要是为避免实例内命名空间冲突,并不能阻止外部对它的读取;

伪私有变量名是可选的功能,更常用的则是用一个下划线编写内部名称,虽然这只是一个非正式的管理,让你知道这是一个不应该修改的名称;

变量名压缩的工作方式:

  • class语句内开头有两个下划线,但是结尾没有的变量名,会自动扩张;
  • 扩张方式:Spam类的__X => _Spam__X
  • 由于扩展的变量名和类名一起,变得独特,有助于避免同一层次中其他类所创建类似变量名的冲突;

伪私有属性使用场景:

class C1:
  def meth1(self):self.X = 88
  def meth2(self):print(self.X)

class C2:
  def metha(self):self.X = 99
  def methb(self):print(self.X)

class C3(C1, C2):pass

I = C3()
I.meth1()
I.metha()

I.meth2() # 99
I.methb() # 99

# 使用伪私有属性:避免了潜在的变量名冲突
class C1:
  def meth1(self):self.__X = 88
  def meth2(self):print(self.__X)

class C2:
  def metha(self):self.__X = 99
  def methb(self):print(self.__X)

class C3(C1, C2):pass

I = C3()
I.meth1()
I.metha()
print(I.__dict__) # {'_C2__X':99, '_C1__X':88}
I.meth2() # 88
I.methb() # 99

依然可以使用扩张后的变量名,如I._C1__X = 77

如果一个方法倾向于只在一个可能混合到其他类的类中使用,在前面使用双下划线,以确保该方法不会受到树中的其他名称的干扰,特别是在多继承的环境中;

方法是对象

方法也是对象,并且可以用与其他对象大部分相同的方式来广泛地使用;

无绑定方法对象 VS 绑定方法对象:

  • 实例对象点的方法,就是绑定方法对象;
  • 类对象点的方法,就是无绑定方法对象,在Python3中其实就是一个函数;
class Spam:
  def doit(self, message):
    print(message)

  def selfless(arg1, arg2):
    return arg1 + arg2

object1 = Spam()
object1.doit('he')

x = object1.doit # x 是 绑定方法对象
x('he')

t = Spam.doit # x 是 无绑定方法对象
t(object1, 'he') # 注意:这只是一个普通的函数调用

Spam.selfless(3, 4)

绑定方法 和 其他可调用对象

绑定方法可以作为一个通用对象处理;

class Number:
  def __init__(self, base):
    self.base = base
  def double(self):
    return self.base * 2

x = Number(2)
y = Number(3)

for act in [x.double, y.double]:
  # double分别绑定了 对象x和y
  act()

# 具有自己的内省信息
bound = x.double
bound.__self__ # 实例对象
bound.__self__.base # 实例对象的base属性
bound.__func__ # 函数对象

绑定方法 作为参数传递,当其调用时,self会引用原始实例对象,也就是绑定方法的对象;在作为回调函数调用时,这个方法就可以读取self实例的属性;如果不用绑定对象方法,而是只用一个简单的函数作为回调参数传入,其他状态信息往往需要通过全局变量保存;

常见的可调用对象:简单函数 lambda 实例继承__call__ 绑定实例方法,类也算;

多重继承:“混合”类

多重继承:类和其他实例继承了列出的所有超类的变量名;

搜索属性,会从左到右搜索类首行中的超类;Python3中,属性搜索处理沿着树层级、以更加广度优先的方式进行;

通常:多重继承是建模属于一个集合以上的对象的好方法;

用类实现一个通用的工具:列出附加给一个实例的属性
class ListInstance:
  def __str__(self):
    return '<Instance of %s, address %s:\n%s>' % (
      self.__class__.__name__,
      id(self), # 内存地址
      self.__attrnames()
    )

  # 通过扩展属性名包括类名,从而吧这样的名称本地化到包含的类中(对类属性和实例属性均如此)
  def __attrnames(self):
    result = ''
    for attr in sorted(self.__dict__):
      result += '\tname %s=%s\n' % (attr, self.__dict__[attr])
    return result

通过把ListInstance添加到一个类头部的超类列表中(混合进去),就可以让类在继承自己超类的同时,获得__str__

使用dir列出继承的属性
  • 扩展上边的类,以显示从一个实例可以访问的所有属性;
  • 技巧是使用dir内置函数,而不是扫描实例的__dict__字典(只有实例属性);
def __attrnames(self):
  result = ''
  for attr in dir(self):
    if attr[:2] == '__' and attr[-2:] == '__':
      result += '\tname %s=<>\n' % attr
    else:
      # getattr使用了继承搜索协议
      result += '\tname %s=%s\n' % (attr, getattr(self,attr))
  return result

# 还会打印从隐式超类object 继承的很多属性

注意:这里还会显示继承的方法,必须使用__str__,如果使用__repr__,这段代码将会循环;因为__repr__中试图显示一个方法的值,会再次触发__repr__

根据属性所在的类来分组属性

遍历整个类树,显示附加到每个对象上的属性:从一个实例的__class__到其类,然后递归地从类的__bases__到其所有超类,一路扫描对象的__dict__

class ListTree:
  def __str__(self):
    self.__visited = {}
    return '<Instance of {0}, address {1}:\n{2}{3}>'.format(
      # 实例对象 类名
      self.__class__.__name__
      # 实例对象内存地址
      id(self),
      # 对象属性
      self.__attrname(self, 0),
      # 类属性
      self.__listclass(self.__class__, 4)
    )

  def __listclass(self, aClass, indent):
    dots = '.' * indent
    if aClass in self.__visited:
      return '\n{0}<Class {1}:, address {2}: (see above)>\n'.format(
        dots,
        aClass.__name__,
        id(aClass)
      )
    else:
      self.__visited[aClass] = True # 类对象 做字典键 避免两次列出同样的类对象
      genabove = (self.__listclass(c, indent + 4) for c in aClass.__bases__) # 超类打印的信息字符串
      return '\n{0}<Class {1}, address {2}:\n{3}{4}{5}>\n'.format(
        dots,
        aClass.__name__,
        id(aClass),
        self.__attrname(aClass, indent),
        ''.join(genabove),
        dots
      )
  def __attrname(self, obj, indent):
    spaces = ' '*(indent + 4)
    result = ''
    for attr in sorted(obj.__dict__):
      if attr.startwith('__') and attr.endwith('__'):
        result += spaces +'{0}=<>\n'.format(attr)
      else:
        # getattr使用了继承搜索协议
        result += spaces +'{0}={1}\n'.format(attr, getattr(obj,attr))
  return result

class MyButton(ListTree, Button): pass

print(MyButton(text='spam'))

注意:这里打印属性,扫描的是实例字典,不能直接支持存储在slot中的属性;

类是对象:通用对象的工厂

# 把类传给会产生任意种类对象的函数
def fatory(aClass, *args, **kargs):
  return aClass(*args, **kargs)

工厂可以将代码和动态配置对象的构造细节隔离开;

与设计相关的高级话题:

  • 抽象超类
  • 装饰器
  • 类型子类
  • 静态方法和类方法
  • 管理属性
  • 元类

第31章 类的高级主题

这是OOP讨论的最后一部分:

  • 建立内置类型子类
  • 新式类的变化和扩展
  • 静态方法和类方法
  • 函数装饰器
  • 简单提及:特性 描述符 装饰器和元类

扩展内置类型

以支持更另类的数据结构:

  • 通过重载运算符扩展
  • 通过子类扩展类型(一般也是覆盖运算符)
  • 委托

新式类变化

  • 类 和 类型基本就是同义词,type(I)内置函数返回一个实例对应的类,通常是和I.__class__相同的;
  • 类 是 type类的实例,type对象产生类作为自己的实例;所有的类都继承自object;
  • 继承搜索顺序:先宽度优先搜索,再深度优先搜索;

Python中基于委托的类:__getattr__与内置函数

  • 针对内置函数的属性获取:__getattr____getattribute__方法不再针对内置运算的隐式属性获取而运行;即不再针对__X__等运算符重载方法而调用;例如,打印所使用的__str__方法,不会调用__getattr__
  • 要解决这一问题,__X__这样的方法必须在包装类中重定义,要么手动、要么使用工具,或者在超类中重新定义;
type([1,2,3]) # <type 'list'>
type(list) # <type 'type'>
list.__class__ # <type 'type'>


class C(object):pass

I = C()
type(I) # <class 'C'>
I.__class__ # <class 'C'>


c1, c2 = C(), C()
type(c1) == type(c2) # True

type(C) # <class 'type'>
isinstance(C, object) # true

类就是类型,一个实例的类型就是该实例的类;

每个类都由一个元类生成,元类是这样的一个类,要么是type自身,要么是从type扩展的子类;

实际上,类型自身派生自object,并且object派生自type,二者是不同对象:一个循环关系覆盖了对象模型,因而:类型是生成类的类;

# 所有的类对象的类都是type
type(type) # <class 'type'>
type(object) # <class 'type'>

# 所有类都派生自object
isinstance(type, object) # True
# object 派生自type
isinstance(object, type) # True

# 二者是不同对象
type is object # False 

钻石继承的变动

多重继承树钻石模式:

  • 经典类:先深度搜索,再从左至右
  • 新式类:则是宽度优先;先水平、再向上;(Python3都是新式类)
#       A.attr .meth
# B           C.meth  
#       D

# 有上述菱形继承结构
x = D()
x.attr # 检索方式为 D B C A

# 如果想要选择meth方法的搜索路径,可以在D类中明确选择
# 选择C的meth
class D(B,C):
  meth = C.meth

y = D()
y.meth # D C

# 选择B的meth
class D(B,C):
  meth = B.meth

z = D()
z.meth # D B A

如果你想要左侧超类的一部分以及右侧超类的一部分,可能就需要在子类中明确使用赋值语句,告诉Python要选择哪个同名属性;

新式类的扩展

slots实例:

  • 将字符串属性实名顺序赋值给特殊的__slots__类属性;
  • 限制类的实例将有的合法 属性集;
  • 优化内存和速度性能;
  • 一般在class语句顶层内将字符串名称顺序赋值给变量__slots__
  • 只有该列表内的这些变量名可赋值为 实例属性;
  • slot属性可以顺序存储以快速查找,而不是使用分配的字典;

实际上,有些带有slots的实例根本没有__dict__实行字典,某些情况下,两种属性源都需要查询以确保完整性;

getattr setattr dir等内置函数,相对slots和__dict__的属性,是比较中立的工具;

比如slots中的属性,可以是使用通用工具通过名称进行访问和设置:

class C:
  __slots__ = ['a','b']

X = C()
X.a = 1
X.a

X.__dict__ # slots中没有这个属性,因而会报错

getattr(X, 'a')
setattr(X, 'b', 2)

'a' in dir(X) # True
'b' in dir(X) # True

上边这里例子中,我们没有在slots中定义一个属性命名空间字典__dict__,因而不可能给不再slots列表中名称的赋值为实例的属性;

解决方法是让slots包含__dict__,从而考虑到一个属性空间字典的需求:

class D:
  __slots__ = ['a', 'b', '__dict__']
  c = 3 # 类属性是可以正常工作的
  def __init__(self):
    self.d = 4

x = D()
x.d # 4
x.__dict__ # {'d': 4}    

x.a # 会报错,因为此时a属性 还没有被赋值

x.a = 1
getattr(X, 'a'),getattr(X, 'c'),getattr(X, 'd')
# (1, 3, 4)

想要通用地列出所有实例属性的代码,需要考虑两种存储形式:

# 实例属性
for attr in list(getattr(X, '__dict__', [])) + getattr(X, '__slots__', []):
  print(attr, '=>', getattr(X, attr))

上述说明了一个实例继承其最低slots属性(实例的类,不包括继承树中的其他超类)中的slot名称:

  • 如果一个子类继承自一个没有slots的超类,那么超类的dict属性总是可访问的,使得子类中的一个slots无意义;
  • 如果一个类定义了超类相同的slot名称,超类slot定义的名称版本只有通过直接从超类获取其描述符才能访问;不同的slot名称均可被继承;
  • 由于一个slots声明的含义收到它出现的类的限制,所以子类将有一个dict,除非它们也定义一个slots
  • 通常从列出实例属性来看,多类中的slots可能需要手动类树爬升、dir用法,或者把slot名称当做不同名称领域的政策;
class E:
  __slots__ = ['c', 'd']

class D(E):
  __slots__ = ['c', 'd']

X = D()
X.a = 1
X.b = 2
X.c = 3

E.__slots__ # ['c', 'd']
D.__slots__ # ['a', '__dict__']
X.__slots__ # ['a', '__dict__']
X.__dict__ # {'b': 2}

# 打印实例属性 不包括超类的slots
for attr in list(getattr(X, '__dict__', [])) + getattr(X, '__slots__', []):
  print(attr, '=>', getattr(X, attr))
# b => 2
# a => 1
# __dict__ => {'b': 2}


dir(X) # 几乎所有属性,包括所有的slot names

类特性

property机制:

  • 读取所需动态计算的变量名,触发额外的方法调用;
  • 和slots一样,都是基于属性描述器(attributed descriptor)

特性是一种对象,赋值给类属性名称;

  • 获得 设置 删除运算的处理器 通过文档字符串调用内置函数property;
  • 在class顶层赋值 name = property(...),对类属性的读取,回传给property的一个读取方法;
  • 类似之前的__getattr__ __setattr__的替代做法;
class newprops(object):
  def getage(self):
    return 40
  def setage(self, value):
    self._age = value

  age = property(getage, setage, None, None) # get set del docs

x = newprops()
x.age # 40

x.age = 42
x._age # 42

x.job = 'work'
x.job # 'work'

相比于特性,之前应用过的__getattr____setattr__在某些应用依然更为动态和通用;

class classic:
  def __getattr__(self, name):
    if name == 'age':
      return 40
    else:
      raise AttibuteError
  def __setattr__(self, name, value):
    if name == 'age':
      self.__dict__['_age'] = value
    else:
      self.__dict__[name] = value

x = classic()
x.age # 40

x.age = 42
x._age # 42

x.job = 'work'
x.job # 'work'

后续还将看到 使用函数装饰器语法来编写特性是可能的;

getattribute 和描述符

__getattribute__可以让类拦截所有属性的引用;

Python支持 属性描述符:

  • 带有__get__ __set__方法的类;
  • 分配给类属性并且由实例继承
  • 能拦截对特定属性的读取和写入访问;
  • 从某种意义讲,是特性的一种通用形式;
  • 描述符还能用来实现之前的slots特性;

元类

元类 是子类化了type对象,并且拦截类创建调用的类;同时还为管理和扩展类对象 提供了定义良好的钩子;(后续详细介绍)

静态方法和类方法

  • 静态方法大致与一个类中的简单的无实例参数函数类似;
  • 类方法传递一个类参数 而不是一个实例参数;

在Python3中,无self的方法,将被看作是简单函数,通过类调用有效,从实例调用无效;

class Spam:
  numInstances = 0
  def __init__(self):
    Spam.numInstances = Spam.numInstances + 1
  # 无self的方法,具有静态方法特性;该方法仅能通过类名调用
  def printNumInstances():
    print(Spam.numInstances)

这种方法也不理想:

  • 这个函数可能与类的直接关系并不大,把它拿到类之外,上边的代码依然有效;
  • 这样的简单函数无法通过继承定制,子类不能通过重新定义这样的一个函数来直接替代或扩展它;

上边这种方式,我们实际是在通过类 调用类的函数属性,它通过实例去调用是无效的;但如果我们想要使用实例调用也有效,就要使用静态方法类方法

class Methods:
  # 实例方法
  def imeth(self, x):
    pass
  # 无self的简单函数,显式类名称调用
  def smeth(x):
    pass
  # 无self的简单函数,显式类名称调用
  def cmeth(cls, x):
    pass
  # 静态方法
  smeth = staticmethod(smeth)
  # 类方法
  cmeth = classmethod(cmeth)

obj = Methods()
# 实例方法调用方式
obj.imeth(1)
Methods.imeth(obj,2)

# 静态方法 可以通过类和实例调用
Methods.smeth(3)
obj.smeth(4)

# 类方法:自动把类(非实例)传入第一个参数,无论调用者是类还是实例
Methods.cmeth(5)
obj.cmeth(6)

注意:

  • 静态方法和类方法可以被子类继承和扩展;
  • 类方法在接收调用主体时,使用的是最具体(低层)的类;
  • 为计数场景的类属性赋值时,注意不要直接使用该参数直接修改属性值,因为cls参数不一定是超类,也有可能是子类;

由于类方法总是接收一个实例树中的最低类:

  • 静态方法 和 显式类名称 可能对于处理一个类本地的数据来说是更好的解决方案;
  • 类方法可能更适合处理对层级中的每个类不同的数据;
class Spam:
  numInstances = 0
  def count(cls):
    cls.numInstances += 1
  def __init__(self):
    self.count()
  count = classmethod(count)

class Sub(Spam):
  numInstances = 0
  def __init__(self):
    Spam.__init__(self)
  
class Other(Spam):
  numInstances = 0

x = Spam()
y1 = Sub()
y2 = Sub()
z1 = Other()
z2 = Other()
z3 = Other()

x.numInstances, y1.numInstances, z1.numInstances # (1,2,3)
Spam.numInstances, Sub.numInstances, Other.numInstances # (1,2,3)

装饰器语法:把一个函数应用于另一个函数的一种方法,可以让静态方法和类方法的设计更简单;

装饰器和元类(一)

函数装饰器:提供一种方式,替函数明确了特定的运算模式,也就是包了一层逻辑实现;

Python提供了一些内置函数装饰器,来做一些运算,如可以标识静态方法;还可以编写任意的装饰器;(用户自定义的也通常写成类,原始函数和其他数据作为状态信息)

语法上:

  • 函数装饰器 是 后边函数的运行时声明;
  • 写成一行,定义在def语句前,有@符号及紧跟着的元函数组成;
  • 元函数是管理另一个函数的函数;
# 对前面的静态方法 使用函数装饰器改写
class C:
  @staticmethod:
  def meth():
    pass

结果:调用方式时,会触发staticmethod装饰器效果,调用增加的一层逻辑,原始函数会在额外的逻辑层间接执行;(classmethod装饰器也有同样的效果)

staticmethod仍然是一个内置函数;它可以用于装饰器语法中,只是因为它接受一个函数并返回一个可调用对象;事实上,任何这样的函数都能这样用;

Python提供的很多内置函数,都可用作装饰器,我们也可以自己定制装饰器;(后续会详细介绍)

来看一个简单的装饰器例子:

# 这个例子展示了:装饰器可用于包裹携带任意数目参数的任何信息
class tracer:
  def __init__(self, func):
    self.calls = 0
    self.func = func
  def __call__(self, *args):
    self.calls += 1
    print(self.calls, self.func.__name__)
    self.func(*args) # *语法 解包参数

@tracer
def spam(a,b,c):
  print(a,b,c)

spam(1,2,3) # 1 spam \n 1 2 3 

# 结果:在原始函数上 新增了一层逻辑

上边的例子没有返回装饰器函数结果,也没有处理关键参数,也不能装饰类方法函数;

类装饰器 和 元类

类装饰器 类似于 函数装饰器;就像下边这样:

# 最终映射的逻辑是这样的
def decorator(aClass):
  pass
class C:
  pass
C = decorator(C)  
  
# 对应的装饰器语法 如下
def decorator(aClass):

@decorator
class C:
  pass


# 一个例子
def count(aClass):
  aClass.numInstances = 0
  return aClass

@count
class Spam:
  pass

元类:

  • 一种基于类的高级工具;用途和类装饰器有所重合;
  • 提供了一种可选模式:把一个类对象的创建导向到顶级type类的一个子类;
class Meta(type):
  def __new__(meta, classname, supers, classdict):
    ...
class C(metaclass=Meta):
  ...

元类通常重新定义type类的__new____init__方法,以实现对一个新的对象的创建和初始化的控制;

类装饰器 和 元类:都可以用来扩展一个类 或 返回一个任意的对象来替代它,几乎拥有无限可能、基于类的可能性的一种协议;

《Python 学习手册(3)》有关于抽象类的描述,就使用了元类;

类陷阱

在class语句外,修改类属性,会修改每个实例对象从类继承的该属性;

由于类属性由所有实例共享,所以如果一个类属性引用一个可变对象,那么从任何实例来原处修改该对象都会like影响到所有实例;

Python中基于委托的类:__getattr__与内置函数

参考【第31章 新式类变化】一节;

不要“过渡包装”,把程序代码包裹很多层,不易理解,尽量避免代码过于抽象,除非有不得不这样做的理由;

后续:

  • 使用函数装饰器编写特性
  • 详细的装饰器
  • 元类,类和实例管理;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值