使用 Tensorflow 建模,你需要理解的三个概念

使用Tensorflow的基本套路是,先建立一个问题的模型,然后求解模型的参数。 建立模型要涉及到占位符(Placeholder)和变量(Variable),求解模型参数涉及优化器。 因此,只要理解了这三个概念,我们便可以使用Tensorflow构建自己的模型并求解模型的参数。

占位符(Placeholder)

使用 tf.placeholder 来创建一个占位符,创建时需要指定它的类型。 比如我们要建立一个数学公式 2x2 + 3x + 1,此时就需要将x定义为一个占位符。这样我们才能在求解公式的值时,动态地指定x的值。

import tensorflow as tf

x = tf.placeholder(tf.int32)    1
y = 2*x*x + 3*x + 1   2

sess = tf.Session()
r = sess.run(y, feed_dict={x:2})   3
  • 1 :创建一个占位符,其类型为int32
  • 2 :定义数学公式。其中自变量为x,函数值为y
  • 3 :求解给定自变量x为2时,函数值y的值。通过 feed_dict 参数指定一个占位符的值。 运行过后,r的值为15(2*22 + 3*2 + 1)

占位符有点像编程语言中变量的概念,但关键不同为:占位符不会保存值,当指定一个值给它后,它并不会将这个值保存起来,以便下次使用。而是你每次使用它时,都必须为它指定一个新值。而变量则会将上次的值一直保存,下次使用时你不必再指定新值,它会直接使用上次保存的值。

一个数学公式中的自变量正好需要这样:它本身没有状态(即不会保存上次的值),你每次求解公式的值时,都必须指定自变量的值。所以,占位符的作用为:

占位符提供了一种给模型指定自变量值的机制

一个基本规则为,在使用Tensorflow建模时,将模型的自变量和因变量定义为占位符,这就对了。

如果一个模型中包含了占位符,则在求解这个模型值时,Tensorflow会强制必须指定所有占位符的值,通过 feed_dict 。如上面例子所示,在求解y时,必须指定x的值。

如果没有指定x的值,如下所示:

r = sess.run(y)

则会报一个错误:

You must feed a value for placeholder tensor \'Placeholder\' with dtype

关于 feed_dict 参数

这个参数指定所有占位符的值。为一个dict。每个key指定的一个占位符的值。key值为占位符本身,如例子中的x,对应的值为占位符的值。

当模型中包含占位符时,则在求解模型值时,必须使用 feed_dict 来指定所有占位符的值。

关于 sess.run

这个函数用于求解一个结点的值。

变量(Variable)

使用tf.Variable创建一个变量。变量跟编程语言中的变量的概念非常相似,表示一个存储空间,能够保存一个值。变量在使用必须有一个初始值,你可以改变一个变量的值。

使用Tensoflow创建机器学习模型时,变量被用于表示模型的参数。一般来说,训练机器学习模型的过程,就是寻找模型参数的过程。在训练开始前,都会给参数一个初始值,然后逐渐修改变量的值,使得模型能够逼近训练数据。

以下是使用变量的一个例子。

a = tf.Variable(3, name='my_a')
b = tf.constant(3)
c = a*b

# a variable must be initilized
init = tf.global_variables_initializer()
self.sess.run(init)
r = self.sess.run(c) # r=9

变量在使用必须被初始化,通过调用 tf.globalvariablesinitializer() 来实现。如果没有初始化,则会报下面的错误。

Attempting to use uninitialized value my_a

修改变量的值

通过变量的 assign 函数来修改它的值。如下所示。

a = tf.Variable(3, name='my_a')
init = tf.global_variables_initializer()
sess.run(init)
assert(sess.run(a) == 3)

assign_to_4 = a.assign(4)
# Before run the operation, a is still 3. This is a little hard to understand
assert(sess.run(a) == 3)
r = sess.run(assign_to_4)
assert(sess.run(a) == 4)

但在训练机器学习模型时,并不需要自己修改变量的值,优化器会自动修改。

优化器(Optimizer)

当使用占位符和变量构造好模型及目标函数后,便可使用优化来求解模型的参数。使用占位符表示模型的自变量和因变量(即模型的X,Y),使用变量表示模型的参数。

总结

使用Tenforflow建模时,将模型的自变量和因变量定义为占位符(Placeholder),这样我们可以动态地指定自变量和因变量的值。

将模型的参数指定为变量(Variable),并给定一个初值,这样Tensorflow在求解模型参数过程中,其可以不断修改参数的值,直到模型能够和数据点吻合。

优化器是Tensorflow求解模型的工具,梯度下降是一种常用的优化器。

ASCII、 Unicode 和 UTF8

  • ASCII: 英文字母与数字编号的一一对应。每个英文字母对应一个编号。范围0~127
  • Unicode: 全世界所有语言中字符与数字编号的一一对应。也即为存在的每个字符指定一个唯一的编号。范围为0~0x10FFFF。

所以,

ASCII与Unicode是类似的东西,都是为一个字符指定一个唯一的数字编号

只不过Unicode的范围更大,能够表示更多的字符。

在计算机的世界里,只有数字,而不会有什么字符。一个字符在计算机看来就是一个数字。ASCII与Unicode就是将字符与数字一一对应起来的映射。比如对于字符'A',在计算机看来,它就是一个数字65。

当字符串被写入文件时,也是将字符串中每个字符对应的数字编号保存在文件。

以上是ASCII和Unicode的相同点。那么,二者有什么区别?

一个显著的区别是,对于同一段文本,二者保存到文件后占用的字节数不同。对于ASCII,每个数字编号占用一个字节。 而对于Unicode,每个编号则需要占用3个字节。因此对于同一段文本:'abcd',采用ASCII格式保存时,文件的大小为4个字节。 采用Unicode保存时,文件的大小则为12个字节。

由此也可看出,当待保存文本为纯英文字母时,

采用Unicode的存储效率太低了

UTF8便是为了解决Unicode存储效率低下而产生的。具体的规则就不讲了,先来看一下UTF8能够达到的效果。

对于相同的文本:'abcd',Unicode需要12个字节,而UTF8只需要4个字节(和ASCII一样,达到最优)。

UTF8之所以可以用一个字节存储英文字母,是因此它使用了变长的编码方式。也即,对于英文字母,它采用一个字节保存这个字符。对于英文字母之后的字符,它采用两个字节保存这个字符。对于再之后的字符,采用三个字节保存。最多采用四个字节保存一个字符。

所以UTF8对于存储英文字母的高效率来源于对之后字符保存效率的牺牲。这里的合理性在于:如果待保存的文本中字符大多数为英文字母,则存储效率能够提高,因为大多数字符都是采用一个字节保存。

总结来说,

UTF8是对Unicode在存储效率上的优化

以上便是三者的关系。

ASCII和Unicode都是为一个字符指定一个唯一的数字编号,Unicode能够表达更多的字符,相当于是ASCII的扩展。Unicode存在存储效率低下的问题,UTF8是在这个方面对Unicode的优化。

Python:怎样用线程将任务并行化?

如果待处理任务满足:

  1. 可拆分,即任务可以被拆分为多个子任务,或任务是多个相同的任务的集合;
  2. 任务不是CPU密集型的,如任务涉及到较多IO操作(如文件读取和网络数据处理)

则使用多线程将任务并行运行,能够提高运行效率。

假设待处理的任务为:有很多文件目录,对于每个文件目录,搜索匹配一个给定字符串的文件的所有行(相当于是实现grep的功能)。 则此处子任务为:给定一个目录,搜索匹配一个给定字符串的文件的所有行。总的任务为处理所有目录。

将子任务表示为一个函数T,如下所示:

def T(dir, pattern):
  print('searching pattern %s in dir %s' % (pattern, dir))
  ...

为每个子任务创建一个线程

要实现并行化,最简单的方法是为每一个子任务创建一个thread,thread处理完后退出。

from threading import Thread
from time import sleep

def T(dir, pattern):
  "This is just a stub that simulate a dir operation"
  sleep(1)
  print('searching pattern %s in dir %s' % (pattern, dir))

threads = []
dirs = ['a/b/c', 'a/b/d', 'b/c', 'd/f']
pattern = 'hello'

for dir in dirs:
  thread = Thread(target=T, args=(dir, pattern))   1
  thread.start()   2
  threads.append(thread)

for thread in threads:
  thread.join()   3

print('Main thread end here')
  • 1 :创建一个Thread对象,target参数指定这个thread待执行的函数,args参数指定target函数的输入参数
  • 2 :启动这个thread。 T(dir, pattern)将被调用
  • 3 :等待,直到这个thread结束。整个for循环表示主进程会等待所有子线程结束后再退出

程序的运行结果为:

searching pattern hello in dir a/b/csearching pattern hello in dir d/f
searching pattern hello in dir b/c
 searching pattern hello in dir a/b/d

Main thread end here

可以看出由于线程是并行运行的,部分输出会交叠。但主进程的打印总在最后。

以上例子中对于每个dir都需要创建一个thread。如果dir的数目较多,则会创建太多的thread,影响运行效率。 较好的方式是限制总线程的数目。

限制线程数目

可以使用信号量(semaphore)来限制同时运行的最大线程数目。如下所示:

from threading import Thread, BoundedSemaphore
from time import sleep

def T(dir, pattern):
  "This is just a stub that simulate a dir operation"
  sleep(1)
  print('searching pattern %s in dir %s' % (pattern, dir))

threads = []
dirs = ['a/b/c', 'a/b/d', 'b/c', 'd/f']
pattern = 'hello'

maxjobs = BoundedSemaphore(2)   1
def wrapper(dir, pattern):
  T(dir, pattern)
  maxjobs.release()   2

for dir in dirs:
  maxjobs.acquire()   3
  thread = Thread(target=wrapper, args=(dir, pattern))
  thread.start()
  threads.append(thread)

for thread in threads:
  thread.join() 

print('Main thread end here')
  • 1 :创建一个有2个资源的信号量。一个信号量代表总的可用的资源数目,这里表示同时运行的最大线程数目为2。
  • 2 :在线程结束时释放资源。运行在子线程中。
  • 3 :在启动一个线程前,先获取一个资源。如果当前已经有2个线程在运行,则会阻塞,直到其中一个线程结束。 运行在主线程中。

当限制了最大运行线程数为2后,由于只有2个线程同时运行,程序的输出更加有序,几乎总是为:

searching pattern hello in dir a/b/c
searching pattern hello in dir a/b/d
searching pattern hello in dir b/c
searching pattern hello in dir d/f
Main thread end here

以上实现中为每个子任务创建一个线程进行处理,然后通过信号量限制同时运行的线程的数目。如果子任务很多,这种方法会创建太多的线程。更好的方法 是使用线程池。

使用线程池(Thread Pool)

即预先创建一定数目的线程,形成一个线程池。每个线程持续处理多个子任务(而不是处理一个就退出)。这样做的好处是:创建的线程数目会比较固定。

那么,每个线程处理哪些子任务呢?一种方法为:预先将所有子任务均分给每个线程。如下所示:

from threading import Thread
from time import sleep

def T(dir, pattern):
  "This is just a stub that simulate a dir operation"
  sleep(1)
  print('searching pattern %s in dir %s' % (pattern, dir))

dirs = ['a/b/c', 'a/b/d', 'b/c', 'd/f']
pattern = 'hello'

def wrapper(dirs, pattern):   1
  for dir in dirs:
    T(dir, pattern)

threadsPool = [   2
  Thread(target=wrapper, args=(dirs[0:2], pattern)),
  Thread(target=wrapper, args=(dirs[2:], pattern)),
]

for thread in threadsPool:   3
  thread.start()

for thread in threadsPool:
  thread.join()

print('Main thread end here')
  • 1 :这个函数能够处理多个dir,将作为线程的target函数
  • 2 :创建一个有2个线程的线程池。并事先分配子任务给每个线程。线程1处理前两个dir,线程2处理后两个dir
  • 3 :启动线程池中所有线程

程序的输出结果为:

searching pattern hello in dir a/b/csearching pattern hello in dir b/c

searching pattern hello in dir d/f
 searching pattern hello in dir a/b/d
Main thread end here

这种方法存在以下问题:

  1. 子任务分配可能不均。导致每个线程运行时间差别可能较大,则整体运行时长可能被拖长
  2. 只能处理所有子任务都预先知道的情况,无法处理子任务实时出现的情况

如果有一种方法,能够让线程知道当前所有的待处理子任务,线程一旦空闲,便可以从中获取一个任务进行处理,则以上问题都可以解决。任务队列便是解决方案。

使用消息队列

可以使用Queue实现一个任务队列,用于在线程间传递子任务。主线程将所有待处理子任务放置在队列中,子线程从队列中获取子任务去处理。 如下所有(注:以下代码只运行于Python 2,因为Queue只存在于Python 2) :

from threading import Thread
from time import sleep
import Queue

def T(dir, pattern):
  "This is just a stub that simulate a dir operation"
  sleep(1)
  print('searching pattern %s in dir %s' % (pattern, dir))

dirs = ['a/b/c', 'a/b/d', 'b/c', 'd/f']
pattern = 'hello'

taskQueue = Queue.Queue()   1

def wrapper():
  while True:
    try:
      dir = taskQueue.get(True, 0.1)   2
      T(dir, pattern)
    except Queue.Empty:
	continue

threadsPool = [Thread(target=wrapper) for i in range(2)]   3

for thread in threadsPool: 
  thread.start()    4

for dir in dirs:
  taskQueue.put(dir)   5

for thread in threadsPool:
  thread.join()
print('Main thread end here')
  • 1 :创建一个任务队列
  • 2 :子线程从任务队列中获取一个任务。第一个参数为True,表示如果没有任务,会等待。第二个参数表示最长等待0.1秒 如果在0.1秒后仍然没有任务,则会抛出一个Queue.Empty的异常
  • 3 :创建有2个线程的线程池。注意target函数wrapper没有任何参数
  • 4 :启动所有线程
  • 5 :主线程将所有子任务放置在任务队列中,以供子线程获取处理。由于子线程已经被启动,则子线程会立即获取到任务并处理

程序的输出为:

searching pattern hello in dir a/b/c
searching pattern hello in dir a/b/d
searching pattern hello in dir b/c
 searching pattern hello in dir d/f

从中可以看出主进程的打印结果并没有出来,程序会一直运行,而不退出。这个问题的原因是:目前的实现中,子线程为一个无限循环, 因此其永远不会终止。因此,必须有一种机制来结束子进程。

终止子进程

一种简单方法为,可以在任务队列中放置一个特殊元素,作为终止符。当子线程从任务队列中获取这个终止符后,便自行退出。如下所示,使用None作为终止符。

from threading import Thread
from time import sleep
import Queue

def T(dir, pattern):
  "This is just a stub that simulate a dir operation"
  sleep(1)
  print('searching pattern %s in dir %s' % (pattern, dir))

dirs = ['a/b/c', 'a/b/d', 'b/c', 'd/f']
pattern = 'hello'

taskQueue = Queue.Queue()

def wrapper():
  while True:
    try:
      dir = taskQueue.get(True, 0.1)
      if dir is None:   1
	taskQueue.put(dir)   2
	break

      T(dir, pattern)
    except Queue.Empty:
	continue

threadsPool = [Thread(target=wrapper) for i in range(2)]

for thread in threadsPool:
  thread.start()

for dir in dirs:
  taskQueue.put(dir)

taskQueue.put(None)   3

for thread in threadsPool:
  thread.join()
print('Main thread end here')
  • 1 :如果任务为终止符(此处为None),则退出
  • 2 :将这个终止符重新放回任务队列。因为只有一个终止符,如果不放回,则其它子线程获取不到,也就无法终止
  • 3 :将终止符放在任务队列。注意必须放置在末尾,否则终止符后的任务无法得到处理

修改过后,程序能够正常运行,主进程能够正常退出了。

searching pattern hello in dir a/b/csearching pattern hello in dir a/b/d

searching pattern hello in dir b/c
 searching pattern hello in dir d/f
Main thread end here

总结

要并行化处理子任务,最简单的方法是为每个子任务创建一个线程去处理。这种方法的缺点是:如果子任务非常多,则需要创建的线程数目会非常多。 并且同时运行的线程数目也会较多。通过使用信号量来限制同时运行的线程数目,通过线程池来避免创建过多的线程。

与每个线程处理一个任务不同,线程池中每个线程会处理多个子任务。这带来一个问题:每个子线程如何知道要处理哪些子任务。 一种方法是预先将所有子任务均分给每个线程,而更灵活的方法则是通过任务队列,由子线程自行决定要处理哪些任务。

使用线程池时,线程主函数通常实现为一个无限循环,因此需要考虑如何终止线程。可以在任务队列中放置一个终止符来告诉线程没有更多任务, 因此其可以终止。

Python 装饰器(Decorator)

装饰器的语法为 @dec_name ,置于函数定义之前。如:

import atexit

@atexit.register
def goodbye():
  print('Goodbye!')

print('Script end here')

atexit.register 是一个装饰器,它的作用是将被装饰的函数注册为在程序结束时执行。函数 goodbye 是被装饰的函数。

程序的运行结果是:

Script end here
Goodbye!

可见函数 goodbye 在程序结束后被自动调用。

另一个常见的例子是 @property ,装饰类的成员函数,将其转换为一个描述符。

class Foo:
  @property
  def attr(self):
    print('attr called')
    return 'attr value'

foo = Foo()

等价语法

语句块

@atexit.register
def goodbye():
  print('Goodbye!')

等价于

def goodbye():
  print('Goodbye!')
goodbye = atexit.register(goodbye)

这两种写法在作用上完全等价。

从第二种写法,更容易看出装饰器的原理。装饰器实际上是一个函数(或callable),其输入、返回值为:

  说明 示例中的对应
输入 被装饰的函数 goodbye
返回值 变换后的函数或任意对象  

返回值会被赋值给原来指向输入函数的变量,如示例中的 goodbye 。此时变量 goodbye 将指向装饰器的返回值,而不是原来的函数定义。返回值一般为一个函数,这个函数是在输入参数函数添加了一些额外操作的版本。

如下面这个装饰器对原始函数添加了一个操作:每次调用这个函数时,打印函数的输入参数及返回值。

def trace(func):
  def wrapper(*args, **kwargs):   1
    print('Enter. Args: %s, kwargs: %s' % (args, kwargs))   2
    rv = func(*args, **kwargs)   3
    print('Exit. Return value: %s' % (rv))   4
    return rv

  return wrapper

@trace
def area(height, width):
  print('area called')
  return height * width

area(2, 3)   5
  1. 1 :定义一个新函数,这个函数将作为装饰器的返回值,来替换原函数
  2. 2, 4 : 打印输入参数、返回值。这是这个装饰器所定义的操作
  3. 3 :调用原函数
  4. 5 :此时 area 实际上是 1 处定义的 wrapper 函数

程序的运行结果为:

Enter. Args: (2, 3), kwargs: {}
area called
Exit. Return value: 6

如果不使用装饰器,则必须将以上打印输入参数及返回值的语句直接写在 area 函数里,如:

def area(height, width):
  print('Enter. Args: %s, %s' % (height, width))
  print('area called')
  rv = height * width
  print('Exit. Return value: %s' % (rv))
  return rv

area(2, 3)

程序的运行结果与使用装饰器时相同。但使用装饰器的好处为:

  1. 打印输入参数及返回值这个操作可被重用

    如对于一个新的函数 foo ,装饰器 trace 可以直接拿来使用,而无须在函数内部重复写两条 print 语句。

    @trace
    def foo(val):
      return 'return value'
    

    一个装饰器实际上定义了一种可重复使用的操作

  2. 函数的功能更单纯

    area 函数的功能是计算面积,而调试语句与其功能无关。使用装饰器可以将与函数功能无关的语句提取出来。 因此函数可以写地更小。

    使用装饰器,相当于将两个小函数组合起来,组成功能更强大的函数

修正名称

以上例子中有一个缺陷,函数 areatrace 装饰后,其名称变为 wrapper ,而非 areaprint(area) 的结果为:

<function wrapper at 0x10df45668>

wrapper 这个名称来源于 trace 中定义的 wrapper 函数。

可以通过 functools.wraps 来修正这个问题。

from functools import wraps 

def trace(func):
  @wraps(func) 
  def wrapper(*args, **kwargs):
    print('Enter. Args: %s, kwargs: %s' % (args, kwargs))
    rv = func(*args, **kwargs)
    print('Exit. Return value: %s' % (rv))
    return rv

  return wrapper

@trace
def area(height, width):
  print('area called')
  return height * width

即使用 functools.wraps 来装饰 wrapper 。此时 print(area) 的结果为:

<function area at 0x10e8371b8>

函数的名称能够正确显示。

接收参数

以上例子中 trace 这个装饰器在使用时不接受参数。如果想传入参数,如传入被装饰函数的名称,可以这么做:

from functools import wraps

def trace(name):
  def wrapper(func):
    @wraps(func)
    def wrapped(*args, **kwargs):
      print('Enter %s. Args: %s, kwargs: %s' % (name, args, kwargs))
      rv = func(*args, **kwargs)
      print('Exit %s. Return value: %s' % (name, rv))
      return rv

    return wrapped
  return wrapper

@trace('area')
def area(height, width):
  print('area called')
  return height * width

area(2, 3)

程序的运行结果为:

Enter area. Args: (2, 3), kwargs: {}
area called
Exit area. Return value: 6

将函数名称传入后,在日志同时打印出函数名,日志更加清晰。

@trace('area') 是如何工作的?

这里其实包含了两个步骤。 @trace('area') 等价于:

dec = trace('area')
@dec
def area(height, width): ...

即先触发函数调用 trace('area') ,得到一个返回值,这个返回值为 wrapper 函数。 而这个函数才是真正的装饰器,然后使用这个装饰器装饰函数。

多重装饰器

装饰器可以叠加使用,如:

@dec1
@dec2
def foo():pass

等价的代码为:

def foo():pass
foo = dec2(foo)
foo = dec1(foo)

即装饰器依次装饰函数,靠近函数定义的装饰器优先。相当于串联起来。

Python描述符(Descriptor)

先看一个例子,@property。被@property修饰的成员函数,将变为一个描述符。这是最简单的创建描述符的方式。

class Foo:
  @property
  def attr(self):
    print('getting attr')
    return 'attr value'

  def bar(self): pass

foo = Foo()

上面这个例子中, attr 是类 Foo 的一个成员函数,可通过语句 foo.attr() 被调用。 但当它被 @property 修饰后,这个成员函数将不再是一个函数,而变为一个描述符。 bar 是一个未被修饰的成员函数。 type(Foo.attr)type(Foo.bar) 的结果分别为:

<type 'property'>
<type 'instancemethod'>

attr 的类型为 property (注:一个 property 类型的对象总是一个描述符), bar 的类型为 instancemethod ,也即一个常规的成员函数。

此时 attr 将无法再被调用,当尝试调用它时,语句 foo.attr() 将抛出错误:

TypeError: 'str' object is not callable

让我们来理解这个错误。

首先来看 foo.attr 的值:

attr value

其类型 type(foo.attr)

str

foo.attr 的类型为 str ,因此便有了以上的错误,一个 str 对象无法被调用。其值为'attr value',正好是原始 attr 函数的返回值。 因此语句 foo.attr 实际上触发了原始 attr 函数的调用,并且将函数的返回值作为其值。实际上语句 print(foo.attr) 的输出为:

getting attr
attr value

进一步验证了执行语句 foo.attr 时,原始的 attr 函数被调用。

发生了什么?当执行一个访问对象属性的语句 foo.attr 时,结果一个函数调用被触发!这便是描述符的作用:将属性访问转变为函数调用,并由这个函数来控制这个属性的值(也即函数的返回值),以及在返回值前做定制化的操作。此时可以给描述符一个简要定义:

描述符是类的一个属性,控制类实例对象访问这个属性时如何返回值及做哪些额外操作

这留给程序员的空间是巨大的。。

描述符协议

任何实现了描述符协议的类都可以作为描述符类。描述符协议为一组成员函数定义,包括:

函数 作用 返回值 是否必须
__get__(self, obj, type) 获取属性值 属性的值
__set__(self, obj, value) 设置属性的值 None
__delete__(self, obj) 删除属性 None

如果一个类实现了以上成员函数,则它便是一个描述符类,其实例对象便是一个描述符

下面是一个自定义的描述符的实现。

class MyDescriptor:
  def __init__(self):
    self.data = None
  def __get__(self, obj, type):
    print('get called')
    return self.data
  def __set__(self, obj, value):
    print('set called')
    self.data = value
  def __delete__(self, obj):
    print('delete called')
    del self.data

class Foo:
  attr = MyDescriptor()

foo = Foo()

示例中 MyDescriptor 实现了描述符协议(也即实现了 __get__, __set__, __delete__ 函数),因此其为一个描述符类。 Fooattr 属性为 MyDescriptor 类的实例对象,因此它是一个描述符。

print(foo.attr) 的输出为:

get called
None

可见当访问 fooattr 属性时, MyDescriptor__get__ 函数被调用。

foo.attr = 'new value' 的输出为:

set called

可见当为 attr 设置一个新值时, MyDescriptor__set__ 函数被调用。

再运行 print(foo.attr) ,输出为:

get called
new value

可见新值已被设置。

del foo.attr 的输出为:

delete called

可见当为删除属性 attr 时, MyDescriptor__delete__ 函数被调用。

再执行 print(foo.attr)AttributeError 被抛出:

get called
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "1.py", line 6, in __get__
    return self.data
AttributeError: 'MyDescriptor' object has no attribute 'data'

可见属性 attr 已被删除。

参数意义

__get__(self, obj, type) 函数各个参数的意义为:

参数 意义 例子中的对应
self 描述符对象本身 Foo.attr
obj 使用描述符的对象实例 foo
type obj的类型 Foo

__set__(self, obj, value) 函数的self和obj参数的意义同 __get__ ,value的意义为:

参数 意义 例子中的对应
value 属性的新值 'new value'

__delete__(self, obj) 函数的self和obj参数的意义同 __get__

(全文完)

Python对象属性查询

对于语句x.y,即获取对象x的y属性,其查询过程为:

  1. 查询y是否存在于 x.__dict__ 。 存在则返回
  2. 查询y是否为x的类C(及其父类)的属性。 存在则返回
  3. 查询失败,抛出 AttributeError。

以上步骤为简化过程,仅包含了对象级别属性及类级别属性的处理,同时也忽略了一些特殊情况及描述符的处理。 第一条规则处理对象级属性,第二条规则处理类级属性。这两条规则能够覆盖大多数情况。

以下是一个例子:

class Foo:
  attr1 = 'value1'
  def __init__(self, attr2):
    self.attr2 = attr2

foo = Foo('value2')
assert(foo.attr2 is foo.__dict__['attr2'])
assert(foo.attr1 is Foo.attr1)

foo.attr2是一个对象级属性,foo.attr1是一个类级属性。 assert(foo.attr2 is foo.__dict__['attr2']) 为True表明foo.attr2与 foo.__dict__ 是相同的,即其通过第一条规则查询得到。 assert(foo.attr1 is Foo.attr1) 为True表明foo.attr1与 Foo.attr1是相同的,所以其通过第二条规则查询得到。

规则2的补充

以上第2条规则提到:查询y是否为x的类C(及其父类)的属性。即为了获取x.y,去查询C.y,其中C为x的类型。C.y具体是如何查询的呢?

C.y与x.y的查询机制相同,此时C便是一个当前的对象(事实上Python类本身是一个类型为type的对象实例)。 所以查询C.y,将以上步骤中x替换为C,则得到C.y的具体步骤:

  1. 查询y是否存在于 C.__dict__ 。 存在则返回
  2. 查询y是否为C的类CC(此时为type)的属性。 存在则返回
  3. 查询失败,抛出 AttributeError。

以下例子可以验证。

class Foo:
  attr1 = 'value1'
  def __init__(self, attr2):
    self.attr2 = attr2

print(type(Foo)) # <type 'classobj'>
assert(Foo.attr1 is Foo.__dict__['attr1'])    # True

type(Foo) 的结果为 <type 'classobj'> 。所以Foo是类型type的一个对象实例。attr1是Foo的一个属性,保存在 Foo.__dict__ 中(这一点,跟其它自定义对象实例一致)。

由此可见,Foo(类本身)及foo(Foo的一个对象实例)在属性查询上,并没有什么不同。他们的不同点为Foo由Python解析器自动创建(当其解析class Foo语句时),而foo一般由程序自己创建(foo = Foo('value2'))。

描述符的处理

对于x.y,当属性y是一个描述符时, Python2和Python3的处理规则有所不同。对于Python2,查询规则变为:

  1. 查询y是否存在于 x.__dict__ 。 存在则返回
  2. 查询y是否为描述符。若是则通过描述符来处理属性获取
  3. 查询y是否为x的类C(及其父类)的属性。 存在则返回
  4. 查询失败,抛出 AttributeError。

对于Python3,查询规则为:

  1. 查询y是否为描述符。若是则通过描述符来处理属性获取
  2. 查询y是否存在于 x.__dict__ 。 存在则返回
  3. 查询y是否为x的类C(及其父类)的属性。 存在则返回
  4. 查询失败,抛出 AttributeError。

区别在于描述符属性与对象级属性(通过 x.__dict__ 获取的属性)的优先级不同。在Python2中,对象级属性优先级更高,而在Python3中,描述符属性优先级更高。

先来看一个例子。

class Foo:
  attr1 = 'value1'
  def __init__(self, attr2):
    self.__dict__['attr2'] = attr2

  @property
  def attr2(self):
    return 'value2, descriptor attr'

foo = Foo('value2, instance attr')
# Python2的输出为: value2, instance attr
# Python3的输出为: value2, descriptor attr
print(foo.attr2) 

通过@property修饰后,函数attr2变为一个描述符。当通过foo.attr2获取attr2这个属性时,函数attr2会被调用,其返回值即为这个属性的值。

同时在 __init__ 函数中,创建了一个同名的attr2实例级属性。需要注意此时不能使用语句 self.attr2 = attr2 来创建这个实例级属性,因为此时描述符已发生作用,attr2不会被保存在 self.__dict__ 中,而是直接通过描述符函数处理。

属性y是否为描述符

对于语句x.y,记对象x的类型为C, C.y的类型为D。如果

  1. C.y存在,即hasattr(C, 'y') 为True
  2. 且 D包含一个 __get__ 属性, 即hasattr(D, '__get__' )为True

则可判定y为一个描述符。

第1个条件表示y为x的一个类级属性,即y为C的属性。第2个条件表示这个属性的类型包含一个 __get__ 属性。 等价的Python代码如下:

is_descriptor = False
C = x.__class__
if hasattr(C, 'y'):
  D = C.y.__class__
  if hasattr(D, '__get__'):
    is_descriptor = True

数据描述符及非数据描述符

如果一个描述符仅包含 __get__ 属性,而没有 __set__ 属性,则它为一个非数据描述符,也即只能获取这个属性的值,而不能修改它的值。

如果一个描述符同时包含 __get____set__ 属性,则它为一个数据描述符,此时即可获取属性值,也可修改它的值。

对于Python3,之前提到的规则只适用于数据描述符。如果一个属性是一个非数据描述符,则实例级属性( x.__dict__['attr'] )的优先级高于描述符的优先级,所以查询规则变为:

  1. 查询y是否为数据描述符。若是则通过描述符来处理属性获取
  2. 查询y是否存在于 x.__dict__ 。 存在则返回
  3. 查询y是否为非数据描述符。若是则通过描述符来处理属性获取
  4. 查询y是否为x的类C(及其父类)的属性。 存在则返回
  5. 查询失败,抛出 AttributeError。

通过以下例子,可以看出这样做的逻辑。假设y是一个非数据描述符,且y不存在于 x.__dict__ 中。

  1. 执行语句 x.y,第1、2条规则均不满足条件,因此第3条规则发生作用,属性值通过描述符来得到。
  2. 执行语句 x.y = 'new value', 由于y为一个非数据描述符,因此无法通过这个描述符修改这个属性的值。因此y会被保存在 x.__dict__ 中。
  3. 再次执行 x.y,第2条规则满足条件,此时返回 x.__dict__ 中保存的值

逻辑在于,当为属性设置一个新值后(第2步),再获取这个属性的值时(第3步),这个值应该与之前设置的值相同,而不是仍然为之前通过描述符返回的值。所以在这种情况下, x.__dict__ 的优先级需要更高。

成员函数是非数据描述符

类成员函数的实现为一个非数据描述符。根据以上规则,对象的属性会覆盖对象的同名成员函数。

以下为一个例子。

# encoding:utf8
class Foo:
  def __init__(self, value):
    self.attr = value

  def attr(self):
    print('attr method')

  def attr2(self):
    print('attr2 method')

foo = Foo('attr value')
assert(foo.attr is foo.__dict__['attr'])
foo.attr() # 错误,attr不是成员函数,无法调用
foo.attr2() # 正确,输出为 attr2 method

对象foo包含一个属性attr,同时包含一个同名函数attr。assert 语句为True表明foo.attr为属性值,而非那个同名函数。

Encoding and Decoding

任何文件,想要保存在硬盘中时,首先要转换成某种编码。如UTF8,ASCII。编码的意思是一个字节流,因为硬盘中是以字节为单位来保存数据的。 刚开始可以简单的认为,在硬盘上保存的数据都是经过编码后的数据。那么这些数据就有一种编码格式。

每个操作系统都有默认的文件编码方式。文件被保存时,它首先被转换为这个编码格式的字节流。

一个python源文件也不例外,它在硬盘中也是以某种编码的形式保存的。这样,当python的解释器读取源文件时,必须知道这个文件的编码格式,才能读取到文件本身的内容。默认情况下,python解释器认为文件总是以ASCII的编码方式保存。也即每个字节都表示一个字符。每个字节的值总是在0到127之间。如果文件是以其他编码方式保存的,则可以通过加注释的形式,告诉解释器这个文件实际的编码方式。以下行告诉解释器文件的编码方式是UTF8。

# encoding:utf8

如果源文件中包含非ASCII字符,则必须包含上述行,否则文件无法加载,因为Python默认文件为ASCII编码的,当读取到一个非ASCII字符时,这个字符无法被读取。

文件被读取出来后,它在内存的表示是什么样的呢?首先,将文件从硬盘中读取到内存中的过程,有一个解码的过程。对于ASCII,文件在硬盘中保存的格式和内存中的格式是一样的,都是对应的数字。也即解码和编码对应的内容是相同的。

对于UTF8,解码后则为Unicode。为什么需要编码?因为编码可以节省保存空间。如果不是这个因素,则编导和解码没有必要存在。UTF8和UTF16分别是编码Unicode的不同的编码方式。Unicode由两个字节组成,所有的字符都要占两个字节。但通过UTF8编码之后,大多数常用的字符都可以用一个字节来编码,达到节省存储空间的目的。同时有些字符需要三个字节才能编码,但由于这些字符相对较少,所以整体来说,存储空间会变小。关于Unicode和UTF8关系可看这里

Python中的字符串是用什么格式保存的呢?

字符串保存有两种格式:一个是8bit的字符串,这个字符串是编码过后的。另外一种是Unicode字符串,这个字符串是未编码的。 这篇文章有详细的介绍。

对于一个8bit的字符串,其包含的内容就是解码后的内容。具体的编码格式有可能是就ASCII,也有可能 是UTF8。也可以认为字符串就是一个8bit的字节序列。一个容易弄混淆的概念是,尽管这个字符串是已经编码过后的内容,它仍然可以继续被编码。

s = 'abc'
s2 = s.encode()

这是一种错误的用法,除了Unicode字符串,编码一个8bit字符串是错误的用法。但Python在这里做了一个workaround,即当其发现encode函数被一个8bit字符串调用时,它首先会将这个字符串decode为Unicode字符串,然后在这个Unicode字符串上调用encode函数。以上代码与以下代码等价。

s = 'abc'
s2 = s.decode().encode()

其中s.decode()的结果是一个Unicode字符串。

编码时出现的一个错误是,unicodedecordererror ascii codec can decode …..

在解析器中输入字符串儿

具体的过程是:解析器得到这个字符串时,已经是被编码过后的。相当于是操作系统传递给解析器的。而传输的格式也是编码后的内容。而具体的编码格式是由操作系统决定的。也即解析器默认使用的编码格式。

Encoding 是将Unicode字符串转化为8bit字符串。有多种编码格式供选择,如UTF8,UTF16,UTF32。但也可以使用ASCII作为编码格式。此时编码规则为:

  • 如果Unicode的值小于128,则转化为相应的ASCII字符
  • 否则,则抛出一个异常。

以上编码过程对于latin1也是一样,只不过此时Unicode的值小于256都被认为正常的字符。

这篇文章里讲解了Unicode编码为UTF8的优点。

Python Unicode的实现应该就是一个数字数组,每个元素是一个数字,这个数字表示Unicode的值。

Python Cookbook 读书笔记(五)

chapter 8: Classes and Objects

8.1. Changing the String Representation of Instances

It's good practice to define both _repr_() and _str_()

the _repr_() method of a class: the literal representation of a object

eval(repr(x)) = x It it not possiable to create an object from the repr(x) results, then the repr(x) result should be enclosed in '<>'

the _str_() method of a class: the toString method of a object

The method will be called when the object is passed to print() function If _str_() is not provided, then _repr_() will be used.

the format function: positional field, by {N}, N means the nth parameter

ValueError: cannot switch from manual field specification to automatic field numbering If you put a numbers to a field, then you should put numbers to all field.

a = '{0}, {1},  {1}'.format(1, 2)
print(a)

Get an object's attribute by {N.atttname} syntax

import itertools
a = '{0}, {0.chain}, {0.permutations}'.format(itertools)
print(a)

for {0!r} or {0!s}, '!r' means use _repr_(), '!s' means use _str_(). '!s' is the default value.

8.2. Customizing String Formatting

the format(aobj[, formatspec]) builtin function

The function is equal to: aobj._format_(formatspec) 而一般的aobj._format_(spec) 的实现是调用 str.format(…) 函数来实现。

str.format(…) method 还支持关键字参数来指定field name.(问题:当关键字参数与普通参数混合时会发生什么?) {:spec} 中的 spec 会传给 aobj._format_(formatspec) 作为参数。 spec 可以为任意字符串,它可以作为参数传递给aobj._format_() method.

str.format(aobj)时, 到底是哪个method会被调用呢? From below codes, it can be see that if format method is defined, then format will be called. else str will be called, when the object is formated by the str.format(…) method. For str(aobj), aobj._str__ will always be called.

class Point:
    def __init__(self, x, y):
	self.x = x
	self.y = y

    def __repr__(self):
	print("__repr__ called")
	return 'Point({0.x}, {0.y})'.format(self)

    def __str__(self):
	print("__str__ called")
	return '({0.x}, {0.y})'.format(self)

    def __format__(self, spec):
	print("__format__ called. spec: %s." % spec)
	return '({0.x}, {0.y})'.format(self)

p = Point(2, 3)
a = '{}'.format(p)
print(a)
print(p)

[NOT FINISHED]8.3. Making Objects Support the Context-Management Protocol, that is, the with statement

To provide with statement support, just define two methods:

  1. _enter_(self)
  2. _exit_(self, excty, excval, tb)
class SaveVar:
    def __init__(self, avar):
	self.avar = avar
    def __enter__(self):
	print("__enter__ called")

8.5. Encapsulating Names in a Class

one underscore _ means private variable, just convention, you can still access that variable outside of a class

class Person:
    def __init__(self, name, age):
	self._name = name
	self._age = age
    def __str__(self):
	return '(name: {}, age: {})'.format(self._name, self._age)

p = Person('Jim', 23)
print(p)
print(p._name, p._age)

two underscore __ means name mangling, when used for inheritance

the variable will be renamed to _C_name. Then it will not override the super class's variable. Because it is also has one leading underscore, so the rules for one underscore also applies.

_age is renamed to _Person_age:

class Person:
    def __init__(self, name, age):
	self._name = name
	self.__age = age
    def __str__(self):
	return '(name: {}, age: {})'.format(self._name, self.__age)

p = Person('Jim', 23)
print(p)
print(dir(p))
print(p._name, p._Person__age)

8.6. Creating Managed Attributes, with @property decorator/annotation, add a setter, getter, deleter to a field

Steps:

  1. first create a property object by @property decorator, on a getter method. The name of the getter should be the same with the attribute field.
  2. create the setter object: by @attributename.setter, on a setter method. The name of the setter should be the same with the attribute field.
  3. the getter, setter function are a way to define what will be called when the attribute with the same name is get, set. e.g. the attribute name is 'foo', then the 'foo' attribute will be a object that has methods: 'getter', 'setter', 'deleter'. You can choose any name to store the real value for this attribute, but the most common value will be add a underscore, that is 'foo'. Type of 'foo' is <class 'property'>
    class Person:
        def __init__(self, name, age):
    	# here the name attribute is depend on the def name(self) getter function. Not the reverse.
    	self.name = name
    	self.age = age
        def __str__(self):
    	return '(name: {}, age: {})'.format(self.name, self.age)
    
        @property
        def name(self):
    	print("getting name")
    	return self.nameL
    
        @name.setter
        def name(self, name):
    	print("setting name")
    	if not isinstance(name, str):
    	    raise TypeError
    
    	self.nameL = name
    
    p = Person('Jim', 23)
    p.name = "Tom"
    print(p)
    print(dir(p))
    print(type(Person.name), dir(Person.name))
    

create caculate attribute by @property, getter, setter, then the attribute works like a attribute, not a method

Seems a good application of @property.

class Circle:
    def __init__(self, radis):
	self.radis = radis

    @property
    def area(self):
	print("getting area")
	return self.radis*self.radis*3.14

p = Circle(4)
print(p.radis)
print(p.area)

8.7. Calling a Method on a Parent Class, by super() function

There are many format

super() # unbound
super(type, obj) # isinstance(obj, type)
super(type, type2) # issubclass(type2, type). issubclass(object, object) is True

8.9. Creating a New Kind of Class or Instance Attribute, by creating a descriptor class for the type

如果一个类定义了三个函数: get, set, delete, 则它是一个descriptor, 可能通过它来为一个instance的attribute添加一些get, set时的函数。

@property 只是descriptor的一种表象, descriptor是最底层,最灵活的实现,在库中大量使用。 TODO: 可以再研究下基于descriptor, @property的实现。

调用顺序:如果descriptor对应的class attribute 存在, 则总会优先调用这个descriptor的函数,来获取或设置attribute的值。 但当descriptor只定义了_get_方法时,则如果同名的变量在instance._dict_中存在,则会优先从instance._dict_中获取。

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

    def __get__(self, instance, cls):
	print("__get__ method called, name: %s" % self.name)
	# If instance is None, then it is the class attribute
	if instance:
	    return instance.__dict__[self.name]
	else:
	    return instance

    def __set__(self, instance, value):
	print("__set__ method called, name %s, value: %s" % (self.name, value))
	instance.__dict__[self.name] = value

class Point:
    # 关键的是量的值,输入参数的值只是用于内部实现的。并且Integer的实现中使用instance.__dict__保存数据也只是一种实现方式。
    # Point.x决定了atribute的名称为x
    x = Integer('z')
    def __init__(self, x, y):
	self.x = x
	self.y = y

    def __str__(self):
	return '({0.x}, {0.y})'.format(self)


p = Point(3, 2)
print("p.x")
print(p.x)
print(p.__dict__)
# setattr(p, 'x', 5)
p.__dict__['x'] = 5
print(p.x)
print(p.__dict__)

8.10. Using Lazily Computed Properties, an application of descriptor

8.11. Simplifying the Initialization of Data Structures, by define a common base class

# python  is very flexiable
class Structure:
    _fields = []
    def __init__(self, *args):
	if len(self._fields) != len(args):
	    raise TypeError('Expected {} arguments'.format(len(self._fields)))
	for k, v in zip(self._fields, args):
	    setattr(self, k, v)
    def __str__(self):
	return '({})'.format(', '.join('{}: {}'.format(f, getattr(self, f)) for f in self._fields))

class Point(Structure):
    _fields = ['x', 'y']
    # def __str__(self):
    #     return '(x: {0.x}, y: {0.y})'.format(self)

class Circle(Structure):
    _fields = ['radius']
    # def __str__(self):
    #     return '(radius: {0.radius})'.format(self)

p = Point(1, 2)
print(p)
p = Circle(3)
print(p)

class attributes can also be accessed by instance object, such as self, but only when the same instance attribute not exists

class Foo:
    class_attr = "ABC"
    def __init__(self, a):
	self.a = a

f = Foo('BB')
print(f.class_attr, f.a)
print(f.class_attr is Foo.class_attr)

class Bar:
    class_attr = "ABC"
    def __init__(self, a):
	self.class_attr = a
	self.a = a

b = Bar('BB')
print(b.class_attr, b.a)
print(b.class_attr is Bar.class_attr)

8.12. Defining an Interface or Abstract Base Class

create an abstract base class, or interface, by abc.ABCMeta, abc.abstractmethod

A abstract class can't be initialized.

from abc import ABCMeta, abstractmethod
class IStream(metaclass=ABCMeta):
    @abstractmethod
    def read(self, maxbytes=-1):
	pass
    @abstractmethod
    def write(self, data):
	pass

# typical usage:
def  foo(obj):
    if isinstance(obj, IStream):
	# processing an IStream here
	pass

a = IStream()

register another class to a 'sub class ' of a abstract base class, by abc.register(cls) function

Then isinstance(obj, AbstractBaseClass) will be True. This let another class which is not a subclass of a base class, but can still pass the isinstance() test, which means implementing a interface.

import io
# Register the built-in I/O classes as supporting our interface
IStream.register(io.IOBase)
# Open a normal file and type check
f = open('foo.txt')
isinstance(f, IStream) # Returns True

8.13. Implementing a Data Model or Type System, by descriptor

感觉根之前小节讲到的descriptor相同,只不过用了继承的方式写了很多细小的descriptor。

what is a descriptor? and its usage

A descriptor is a class attribute object, which has get, set, or delete method, is used to define how a instance attribute is get, set, and delete. When an instance attribute is get, the descriptor's get method will be called. The same thing applys to set and delete

In descriptor's get, set methods, we must use instance._dict_[xxx] to get a attribute. If we use getattr(instance, xxx) to get that attribute, then there will be a recursion error as below, because the getattr() function will trigger a new call of get method. RecursionError: maximum recursion depth exceeded while calling a Python object

The relationship between the descriptor object and an instance attribute:

  1. if the descriptor object is assigned to a class attribute with name 'attributea', then it will control the instance attribute with the same name.
  2. but there is one exception: if only the get method of a descriptor is defined, then the instance attribute with the same name will be not be controled by the descriptor, it will be get directly from the dict.

a test:

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

    def __get__(self, instance, cls):
	if instance:
	    print('Getting attribute {}, value is {}'.format(self.name, instance.__dict__[self.name]))
	    return instance.__dict__[self.name]
	    # return getattr(instance, self.name)
	else:
	    return instance


    def __set__(self, instance, value):
	print('Setting attribute {} to {}'.format(self.name, value))
	instance.__dict__[self.name] = value

class Circle:
    radius = TraceDescriptor('radius')
    def __init__(self,  radius):
	self.radius = radius

c =  Circle(4)

print(c.radius)

8.16. Defining More Than One Constructor in a Class, use a class method

GP: Always only assign values in the default constructor(init), and do other things by other constructors

import time

class Date:
    def __init__(self, y, m,  d):
	self.year = y
	self.month = m
	self.day =  d

    @classmethod
    def today(cls):
	t = time.localtime()
	return cls(t.tm_year, t.tm_mon, t.tm_mday)

    def __str__(self):
	return '({0.year}, {0.month}, {0.day})'.format(self)

d1 = Date(2017, 1, 2)
d2 = Date.today()
print(d1, d2)

8.17. Creating an Instance Without Invoking init

the object._new_(*args, **kwargs) method: create a bare object

Every object has a new_method, which is inheritantanted from type._new.

The parameter should be a type object.

When you want to create an object from a json, this method can be used.

import aspk_common as AC
class Foo(AC.Structure):
    _fields = ['x']

f = Foo(2)
g = Foo.__new__(Foo)
print(f)
print(f.__dict__)
print(g.__dict__)
print(dir(f))
print(dir(g))

Problem: how an object is constructed?

I guess first create a bare object by calling the new method, then call the object's init method.

8.18. Extending Classes with Mixins

mixin classes, used to extend function of a class, class customization, by multiple inheritance

SOLVED, see another comment. How below codes works? For 'super()._getitem_(key)', why dict._getitem__ method will be called?

After figuring out MRO, then I know how a mixin class works: Mixin class is used to customize an existing class. It make use of MRO of multiple inheritance. Suppose 'Base' is the class to be customized, 'Mixin' is the mixin class, 'Foo' is the result class, then the typical syntax is:

class Foo(Mixin, Base):
    pass

That is, put the mixin class as the first parent class, and the Base class as the second class. Then e.g. you want change a method of Base's behavier, such as 'foo', then you can just define a method named 'foo' in Mixin, and doing some work, then call 'super().foo(…)' to call Base's foo method.

Works like a decorator pattern.

But what's difference between this method and by directly define the 'foo' method in Foo? => maybe the main benifet is that by putting the codes to a Mixin class, the codes can be easily reused.

import aspk_common as AC
class Logging:
    __slots__ = ()
    def __getitem__(self, key):
	print('Getting {}'.format(key))
	print('self: {}\nsuper: {}'.format(self, super()))
	return super().__getitem__(key)

class LoggingDict(Logging, dict):
    pass

d = LoggingDict()
d['x'] = 2
print(d['x'])

mutiple inheritance: how method/attribue are resolved if they exists in more than one super classes

A method/attribute is resolved in the order of all parent class given. e.g: class Foo(A, B) if a method 'aaa' is defined in both A and B, then A.aaa will be used.

python multiple inheritance, super and MRO(method resolution order)

Guoid's words: http://python-history.blogspot.fi/2010/06/method-resolution-order.html depth first, from left to right, then delete all same classes expect the last one. Then diamond problem is solved.

For below code snippets: From the printout, super() will return the next class in MRO(method resolve order) list, given a current class. The next class can be a real parent class for current class, or if the real parent class not exists, then the next class will be the next parent class of the current instance. For both two conditions, they are always the same class in MRO.

For below codes: the MRO is [C, A, B].

  • So super() in class C's result is A
  • super() in class A is B
  • super() in class B is object(I guess)
class A:
    def foo(self):
	print("A")
	print(super())
	super().foo()
class B:
    def foo(self):
	print("B")
	print(super())

class C(A, B):
    def foo(self):
	print("C")
	print(super())
	super().foo()

o = C()
o.foo()
print("MRO of C: ", C.__class__.__mro__)
print("MRO() of C: ", C.__class__.mro(C))

8.19. Implementing Stateful Objects or State Machines

Implementing the state pattern, by creating class for each state. In a class for one state, only define the method use to handle the current state, all other methods should raise a 'NotImplementedError'. Will see this latter

8.20. Calling a Method on an Object Given the Name As a String, by getattr

A method is just an attribute of an object, so first get the method by 'getattr' given string name

class Foo:
    def foo(self):
	print("foo")

f = Foo()
getattr(f, 'foo')()

8.20. Calling a Method on an Object Given the Name As a String, by operator.methodcaller(name, *args)

The benifit of methodcaller is that it will fix all parameters of the method. So if the method will be called given same parameters for many differenntt object, this method might be better

class Foo:
    def foo(self, x, y):
	print("foo: {}, {}".format(x, y))

f = Foo()
import operator
operator.methodcaller('foo', 3, 4)(f)

8.21. Implementing the Visitor Pattern

感觉这里所说的vistor pattern主要是对用于处理包含不同类型对象的list. 用于通用处理。 基于类型系统的visitor pattern, 是通过在不同的基础类中的accept函数来实现 dispatch table的。相当于把dispatch table也耦合在基础类定义中了。 但最本质的目的是对于不同类型的对象,客户代码使用相同的代码进行处理。

将dispatch table 做在哪里,只影响一点点写法,对最终达到的效果没影响。

例子:

class Visitor:
    def visit(self, node):
	methname = 'visit_' + type(node).__name__
	meth = getattr(self, methname, None)
	if meth is None:
	    meth = self.generic_visit
	return meth(node)

    def generic_visit(self, node):
	raise RuntimeError('No {} method'.format('visit_' + type(node).__name__))

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

class RegularFile(File):
    def read_content(self):
	return "This is the content for file {}".format(self.name)

class Directory(File):
    def children(self):
	'''Return all children names as a list'''
	return [RegularFile('a.txt'), RegularFile('b.exe')]

class Symbolic(File):
    def real(self):
	'''Return real file this symbolic point to'''
	return RegularFile('dd.txt')

class CatVisitor(Visitor):
    '''Implement cat command for a File object.'''
    def  visit_RegularFile(self, node):
	print('content for regular file {}'.format(node.name))
	print(node.read_content())
    def visit_Directory(self, node):
	print('content for directory {}'.format(node.name))
	for f in node.children():
	    self.visit(f)
    def visit_Symbolic(self, node):
	print('content for symbolic file {}'.format(node.name))
	self.visit(node.real())

files = [RegularFile('foo.txt'), Directory('bar'), RegularFile('a.txt'), Symbolic('aa.c')]
visitor = CatVisitor()
for file in files:
    visitor.visit(file)
    print()

dispatch table in python, decided by object type

将所有处理函数写在一个类中, 提供一个根据待处理对象类型分发的函数。 这个作为dispatch 基类。然后再定义针对每种类型的visit函数就行了。 这里类有两个目的:

  1. 定义dispatch table
  2. 对一组函数的名字空间吧。
  3. 以下例子中实现的 Dispatcher class 是通用的,可以共用。
class Dispatcher:
    def visit(self, node):
	methname = 'visit_' + type(node).__name__
	meth = getattr(self, methname, None)
	if meth is None:
	    meth = self.generic_visit
	return meth(node)

    def generic_visit(self, node):
	raise RuntimeError('No {} method'.format('visit_' + type(node).__name__))

class FooDispatcher(Dispatcher):
    def visit_RegularFile(self, node):
	pass
    def visit_Directory(self, node):
	pass

8.23. Managing Memory in Cyclic Data Structures, by weakref.ref(aobject)

When cyclic reference exists, the some object will never be deleted, because its reference coutns is large than 0. A weakref is just a reference that don't increase the reference count. To dereference, just call it like a function. If the referenced object still exists, the object will be returne, otherwise None will be returned.

For a tree structure, the book give an example of reference the parent node by weakref.

Note: you can't weakref to 'int', 'str', …

import weakref
class Node:
    pass
a = Node()
b = weakref.ref(a)
# c = a     # if this line exists, then a will not be deleted after 'del a', then the second call to b() will still return a

print(b())
del a
print(b())

8.24. Making Classes Support Comparison Operations, by define many comparision builtin method: eq, lt, le, gt, ge, ne

8.25. Creating Cached Instances, by create a factory method(a class method)

If the parameter are the same, then return an existing object.

Python 对象属性(一)

所有的Python 对象都有属性,可以通过 obj.name 这样的语法来获取属性,通过 obj.name = 'Tom' 这样的语法添加一个属性或更新属性的值(如果这个属性已存在),通过 del obj.name 删除一个属性。

特殊属性和自定义属性

根据属性保存位置的不同,可以将属性分类为特殊属性和自定义属性。先来看一个例子。

class Foo:
    pass

f = Foo()
print(f.__dict__)
f.name = "Tom"
print(f.__dict__)
print(f.__dict__['name'] is f.name)
print(f.__class__)
f.__dict__['__class__'] = 'My class'
print(f.__class__)

f.__dict__['bar'] = 'xxx'
print(f.bar)

结果为:

{}
{'name': 'Tom'}
True
<class '__main__.Foo'>
<class '__main__.Foo'>
xxx

从这个例子中,我们可以看出:

  • 我们为一个对象新添加的属性都会被保存在 'dict' 这个特殊属性中,'dict'的类型是字典。如在添加了 name 这个属性后, dict 的值变为 {'name': 'Tom'},添加前为一个空字典。
  • 对于我们自己新添加的属性,可以通过两种方式来访问这个属性: f.name 和 f.__dict__['name']。这两种方式的效果完全相同,因为它们代表的是同一个变量。因此可认为 f.name 是 f.__dict__['name'] 的一种简写方式。
  • 对于任意的属性,f.xyz 的解析过程为:1. 首先检查xyz这个属性在对象中是否存在,如存在,则返回这个属性。2. 否则再检查 xyz这个属性在f.__dict__是否存在,如存在,则返回这个属性。 如上面代码里,尽管我们通过直接在__dict__为对象添加一个名为__class__的属性,但由于这个属性对象本身就定义了,因此它还是原来的值。也即在以上第一步中即找到了这个属性。 而对于'bar' 属性,添加后再查询就是我们添加的值,在以上第2步中找到这个属性。

因此可将一个python对象理解为一个C语言中的结构体,这个结构体的成员在对象创建后,就固定了,无法再添加新的成员。在本文中我们称这些属性为特殊属性。但实际上我们又是可以为一个对象添加新的属性的,这是怎么做到的呢?答案是每个对象都有一个名为__dict__的特殊属性,这个属性的类型为字典,所有新添加的属性都会被保存在这个字典里,在本文中我们称新添加的属性为自定义属性。但python向用户屏蔽两种属性的差别,提供了统一的语法来访问它们,也即通过 obj.name 。这个在大多数情况下会带来便利性,但有时会带来含混,此时我们需要弄清楚到底一个属性是一个特殊属性还是自定义属性。

一些例子

class Foo:
    def __init__(self):
	self.id = 1
	self.name = 'Tom'

    def foo(self):
	pass

f = Foo()
print(f.__dict__)
print(f.foo)

结果为:

{'id': 1, 'name': 'Tom'}
<bound method Foo.foo of <__main__.Foo object at 0x000000B2509ECFD0>>
  • 可见一个类的__init__函数对self添加的属性,都将成为这个类实例对象的自定义属性,如例子中的id 和name两个属性。
  • 类中定义的函数,将成为类实例的特殊属性,如例子中的foo这个属性,它的类型是method.

对象属性的分类及一些例子

对于任何对象,其属性可分为两种(以下说的都是自己的属性,不包括父类的属性):

  1. 用户自定义属性。
  2. 语言定义属性

对于一个class object,其两种属性可能包含:

用户自定义属性 语言定义属性
所有类变量 mro
所有类函数 dict
所有类描述符  
doc  
module  

那么如何为一个class object添加自定义属性? 在定义一个类时,一个def 语句将添加一个类函数到自定义属性; 一个赋值语句将添加一个类变量到自定义属性。

注:其实对于class object, 它也是type class object 的instance

对于一个instance object, 其两种属性可能包含:

用户自定义属性 语言定义属性
所有属性 class
  dict

问题: str.__class__ 是一个语言定义属性还是继承自 其父类 object 的__class__ 用户自定义属性(也即object.__dict__['class'], 这个是一个'attribute')? 应该是一个语言定义属性,应该来自于 object.__dict__['class'], 也可能是

问题二: str.__mro__ 的结果是: (str, object). 但我不知道这个成员来自于何处。 str.__dict__和object.__dict__里都没有。

__dict__属性的作用

__dict__属性是一个语言定义属性,它用来保存所有的用户自定义属性。

属性的查找机制

x.name 等价于 getattr(x, 'name'),

Python Cookbook 读书笔记(四)

chapter 5: Files and I/O

5.1. Reading and Writing Text Data

open a file

the 't' in mode means text.

f = open('1.txt', 'rt') #read
# f = open('1.txt', 'wt') #write
# f = open('1.txt', 'at') #append

# specify codec
f = open('1.txt', 'rt', encoding='latin-1') #read
f = open('1.txt', 'wt', encoding='latin-1') #write

#disable newline translation, by use the open(newline='') option
f = open('1.txt', 'rt', newline='') #read

# specify what to do when encountering decoding/encoding errors, by use open(errors='...') option
f = open('1.txt', 'rt', errors='replace') #replace the char that can't be decoded to a unicode char U+fffd(which is the unicode replacemenet char)
f = open('1.txt', 'rt', errors='ignore') #just ignore the char that can't be decoded

read whole content of a file as a string

with open('1.txt', 'rt') as f:
    s = f.read()
    print(s)

read/iterate each line of a file, by just treat the file object as a generator

with open('1.txt', 'rt') as f:
    for line in f:
	print(line, end='')

write str to a file, by file.write(text) method

with open('2.txt', 'wt') as f:
    f.write('abced')

get system's default encoding

import sys
print(sys.getdefaultencoding())

5.2. Printing to a File, redirect stdout to a file, by use print(file=…) option

with open('2.txt', 'wt') as f:
    print("aaaaa", file=f)

Question: how to redirect stdout to a file system widely.

5.3. Printing with a Different Separator or Line Ending, by use print(sep=…, end=…) options

print(1, 'abc')
print(1, 'abc', sep=', ', end='##')
print()
row = (45, 'Hello', 'List', 4)
print(row)
print(*row)
print(row, sep=', ')
print(*row, sep=', ')

pass a sequence/list object to a function as N parameters instead of one, by using *listname

row = (45, 'Hello', 'List', 4)
print(row)
print(*row)
print(row, sep=', ')
print(*row, sep=', ')

5.4. Reading and Writing Binary Data(such as image, sound files)

By saying binary data, it means that there will no encoding/decoding works during writing/reading process. Use mode such as 'rb', 'wb', 'ab'.

当作为binary data读取时, 与作为text data相比,没有自动的decode, encode过程。

with open('2.txt', 'wb') as f:
    # f.write('aaabbb'.encode('latin-1'))
    f.write(b'aaabbb')

what is text string and byte string in python

Each element in a text string is also a text string, Each element in a byte string is a int

s = 'Hello'
print(type(s), s, sep=', ')
for c in s:
    print(type(c), c, sep=', ')

s = b'Hello'
print(type(s), s, sep=', ')
for c in s:
    print(type(c), c, sep=', ')

5.5. Writing to a File That Doesn't Already Exist, by set mode of open(…) function to 'x'

If the file already exists, then don't write, and will raise a FileExistsError exception

with open('2.txt', 'xt') as f:
    f.write('aaa bbb')

感觉这个根python的哲学有点类似,不事先做判断,而是用exception的方式。 具体的用法可能需要将它放在一个try catch里。

5.6. Performing I/O Operations on a String, by io.StringIO() or io.BytesIO()

a typecal application can be simulate a file when do unit testing.

5.7. Reading and Writing Compressed Datafiles, by use gzip.open(…), or bz2.open(…)

After open the file, other operations are just the same as normal file.

5.8. Iterating Over Fixed-Sized Records, by iter(callable, sentinel)

import functools
RECORD_SIZE = 2
with open('1.txt', 'rt') as f:
    for r in iter(functools.partial(f.read, RECORD_SIZE), ''):
	print(r, end='; ')

the functools.partial(func, *args, **kwargs) function: create a new callable from a given callable with some(partial) arguments fixed. Currying

from functools import partial

def max(a, b):
    if a>b: return a
    else: return b

mm = partial(max, 3)
print(mm(4))
print(mm(2))
print(mm())

写一个能够接收很多参数的函数,然后利用partial 来生成简易的使用接口。需要注意参数的顺序。

5.9. Reading Binary Data into a Mutable Buffer

5.10. Memory Mapping Binary Files, map a binary file to memory(byte array), my mmap.mmap(…) method

This is a general method to map file to memory, then you can random access the content of the file, such as by using slicing

After mapped, by change the value of the array will change the file's content. This is also a way for multiple intepreter comunication. Below is a general function that map a file to a byte array.

import os
import mmap

def memory_map(filename, access=mmap.ACCESS_WRITE):
    size = os.path.getsize(filename)
    fd = os.open(filename, os.O_RDWR)
    return mmap.mmap(fd, size, access=access)

# below is application of the function
f = memory_map('1.txt')
print(f[2:8])
f[0:3] = b'EEF'