原文:https://zhuanlan.zhihu.com/p/32764345

https://www.cnblogs.com/aademeng/articles/7262645.html------Python描述器(descriptor)

########################################################################################################################################################

https://blog.csdn.net/andybegin/article/details/80770108--------Python 描述符 (descriptor) 详解

对象属性的访问顺序: 
1. 实例属性 
2. 类属性 
3. 父类属性 
4. __getattr__()方法

每次属性查找,这个协议的方法实际上是由对象的特殊方法getattribute()调用。每次通过点号(ins.attribute)或者getattr(ins, ‘attribute’)函数调用都会隐式的调用getattribute(),它的属性查找的顺序如下: 
1. 验证属性是否为实例的类对象的数据描述符 
2. 查看该属性是否能在实例对象的dict中找到 
3. 查看该属性是否为实例的类对象的非数据描述符

换句话说:数据描述符优先于dict,而dict查找优先于非数据描述符。

########################################################################################################################################################

本文分为如下部分

  • 引言——用@property批量使用的例子来引出描述器的功能
  • 描述器的基本理论及简单实例
  • 描述器的调用机制
  • 描述器的细节
  • 实例方法、静态方法和类方法的描述器原理
  • property装饰器的原理
  • 描述器的应用
  • 参考资料

引言

前面python面向对象的文章中我们讲到过,我们可以用@property装饰器将方法包装成属性,这样的属性,相比于其他属性有一个优点就是可以在对属性赋值时,进行变量检查,举例代码如下

class A:
def __init__(self, name, score):
self.name = name # 普通属性
self._score = score @property
def score(self):
return self._score @score.setter
def score(self, value):
print('setting score here')
if isinstance(value, int):
self._score = value
else:
print('please input an int') a = A('Bob',90)
a.name # 'Bob'
a.score # 90
a.name = 1
a.name # 1 ,名字本身不应该允许赋值为数字,但是这里无法控制其赋值
a.score = 83
a.score # 83,当赋值为数值型的时候,可以顺利运行
a.score = 'bob' # please input an int
a.score # 83,赋值为字符串时,score没有被改变

当我们有很多这样的属性时,如果每一个都去使用@property,代码就会过于冗余。如下

class A:
def __init__(self, name, score, age):
self.name = name # 普通属性
self._score = score
self._age = age @property
def score(self):
return self._score @score.setter
def score(self, value):
print('setting score here')
if isinstance(value, int):
self._score = value
else:
print('please input an int') @property
def age(self):
return self._age @age.setter
def age(self, value):
print('setting age here')
if isinstance(value, int):
self._age = value
else:
print('please input an int') a = A('Bob', 90, 20)

因为每一次检验的方法都是一样的,所以最好有方法可以批量实现这件事,只写一次if isinstance。描述器就可以用来实现这件事。

为了能够更清楚地理解描述器如何实现,我们先跳开这个话题,先讲一讲描述器的基本理论。

描述器基本理论及简单实例

描述器功能强大,应用广泛,它可以控制我们访问属性、方法的行为,是@property、super、静态方法、类方法、甚至属性、实例背后的实现机制,是一种比较底层的设计,因此理解起来也会有一些困难。

定义:从描述器的创建来说,一个类中定义了__get____set____delete__中的一个或几个,这个类的实例就可以叫做一个描述器。

为了能更真切地体会描述器是什么,我们先看一个最简单的例子,这个例子不实现什么功能,只是使用了描述器

# 创建一个描述器的类,它的实例就是一个描述器
# 这个类要有__get__ __set__ 这样的方法
# 这种类是当做工具使用的,不单独使用
class M:
def __init__(self, x=1):
self.x = x def __get__(self, instance, owner):
return self.x def __set__(self, instance, value):
self.x = value # 调用描述器的类
class AA:
m = M() # m就是一个描述器 aa = AA()
aa.m # 1
aa.m = 2
aa.m # 2

我们分析一下上面这个例子

  • 创建aa实例和普通类没什么区别,我们从aa.m开始看
  • aa.m是aa实例调用了m这个类属性,然而这个类属性不是普通的值,而是一个描述器,所以我们从访问这个类属性变成了访问这个描述器
  • 如果调用时得到的是一个描述器,python内部就会自动触发一套使用机制
  • 访问的话自动触发描述器的__get__方法
  • 修改设置的话就自动触发描述器的__set__方法
  • 这里就是aa.m触发了__get__方法,得到的是self.x的值,在前面__init__中定义的为1
  • aa.m = 2则触发了__set__方法,赋的值2传到value参数之中,改变了self.x的值,所以下一次aa.m调用的值也改变了

进一步思考:当访问一个属性时,我们可以不直接给一个值,而是接一个描述器,让访问和修改设置时自动调用__get__方法和__set__方法。再在__get__方法和__set__方法中进行某种处理,就可以实现更改操作属性行为的目的。这就是描述器做的事情。

相信有的读者已经想到了,开头引言部分的例子,就是用描述器这样实现的。在讲具体如何实现之前,我们要先了解更多关于描述器的调用机制

描述器的调用机制

aa.m命令其实是查找m属性的过程,程序会先到哪里找,没有的话再到哪里找,这是有一个顺序的,说明访问顺序时需要用到__dict__方法。

先看下面的代码了解一下__dict__方法

class C:
x = 1
def __init__(self, y):
self.y = y def fun(self):
print(self.y) c = C(2)
# 实例有哪些属性
print(c.__dict__) # {'y': 2}
# 类有什么属性
print(C.__dict__) # 里面有 x fun
print(type(c).__dict__) # 和上一条一样 print(vars(c)) # __dict__ 也可以用 vars 函数替代,功能完全相同 # 调用
c.fun() # 2
c.__dict__['y'] # 2
# type(c).__dict__['fun']() # 报错,说明函数不是这么调用的

__dict__方法返回的是一个字典,类和实例都可以调用,键就是类或实例所拥有的属性、方法,可以用这个字典访问属性,但是方法就不能这样直接访问,原因我们之后再说。

下面我们来说一下,当我们调用aa.m时的访问顺序

  • 程序会先查找 aa.__dict__['m'] 是否存在
  • 不存在再到type(aa).__dict__['m']中查找
  • 然后找type(aa)的父类
  • 期间找到的是普通值就输出,如果找到的是一个描述器,则调用__get__方法

下面我们来看一下__get__方法的调用机制

class M:
def __init__(self):
self.x = 1 def __get__(self, instance, owner):
return self.x def __set__(self, instance, value):
self.x = value # 调用描述器的类
class AA:
m = M() # m就是一个描述器
n = 2
def __init__(self, score):
self.score = score aa = AA(3)
print(aa.__dict__) # {'score': 3}
print(aa.score) # 3, 在 aa.__dict__ 中寻找,找到了score直接返回
print(aa.__dict__['score']) # 3, 上面的调用机制实际上是这样的 print(type(aa).__dict__) # 里面有n和m
print(aa.n) # 2, 在aa.__dict__中找不到n,于是到type(aa).__dict__中找到了n,并返回其值
print(type(aa).__dict__['n']) # 2, 其实是上面一条的调用机制 print(aa.m) # 1, 在aa.__dict__中找不到n,于是到type(aa).__dict__中找到了m
# m是一个描述器对象,于是调用__get__方法,将self.x的值返回,即1
print(type(aa).__dict__['m'].__get__(aa,AA)) # 1, 上面一条的调用方式是这样的
# __get__的定义中,除了self,还有instance和owner,其实分别表示的就是描述器所在的实例和类,这里的细节我们后文会讲 print('-'*20)
print(AA.m) # 1, 也是一样调用了描述器
print(AA.__dict__['m'].__get__(None, AA)) # 类相当于调用这个

此外还有特例,与描述器的种类有关

  • 同时定义了__get____set__方法的描述器称为资料描述器
  • 只定义了__get__的描述器称为非资料描述器
  • 二者的区别是:当属性名和描述器名相同时,在访问这个同名属性时,如果是资料描述器就会先访问描述器,如果是非资料描述器就会先访问属性 举例如下
# 既有__get__又有__set__,是一个资料描述器
class M:
def __init__(self):
self.x = 1 def __get__(self, instance, owner):
print('get m here') # 打印一些信息,看这个方法何时被调用
return self.x def __set__(self, instance, value):
print('set m here') # 打印一些信息,看这个方法何时被调用
self.x = value + 1 # 这里设置一个+1来更清楚了解调用机制 # 只有__get__是一个非资料描述器
class N:
def __init__(self):
self.x = 1 def __get__(self, instance, owner):
print('get n here') # 打印一些信息,看这个方法何时被调用
return self.x # 调用描述器的类
class AA:
m = M() # m就是一个描述器
n = N()
def __init__(self, m, n):
self.m = m # 属性m和描述器m名字相同,调用时发生一些冲突
self.n = n # 非资料描述器的情况,与m对比 aa = AA(2,5)
print(aa.__dict__) # 只有n没有m, 因为资料描述器同名时,不会访问到属性,会直接访问描述器,所以属性里就查不到m这个属性了
print(AA.__dict__) # m和n都有
print(aa.n) # 5, 非资料描述器同名时调用的是属性,为传入的5
print(AA.n) # 1, 如果是类来访问,就调用的是描述器,返回self.x的值 print(aa.m) # 3, 其实在aa=AA(2,5)创建实例时,进行了属性赋值,其中相当于进行了aa.m=2
# 但是aa调用m时却不是常规地调用属性m,而是资料描述器m
# 所以定义实例aa时,其实触发了m的__set__方法,将2传给value,self.x变成3
# aa.m调用时也访问的是描述器,返回self.x即3的结果
# 其实看打印信息也能看出什么时候调用了__get__和__set__ aa.m = 6 # 另外对属性赋值也是调用了m的__set__方法
print(aa.m) # 7,调用__get__方法 print('-'*20)
# 在代码中显式调用__get__方法
print(AA.__dict__['n'].__get__(None, AA)) # 1
print(AA.__dict__['n'].__get__(aa, AA)) # 1

注:要想制作一个只读的资料描述器,需要同时定义 __set__ 和 __get__,并在 __set__ 中引发一个 AttributeError 异常。定义一个引发异常的 __set__ 方法就足够让一个描述器成为资料描述器。

描述器的细节

本节分为如下两个部分

  • 调用描述器的原理
  • __get____set__方法中的参数解释

1.首先是调用描述器的原理 当调用一个属性,而属性指向一个描述器时,为什么就会去调用这个描述器呢,其实这是由object.__getattribute__()方法控制的,其中object是新式类定义时默认继承的类,即py2这么写的class(object)中的object。新定义的一个类继承了object类,也就继承了__getattribute__方法。当访问一个属性比如b.x时,会自动调用这个方法 __getattribute__()的定义如下

def __getattribute__(self, key):
"Emulate type_getattro() in Objects/typeobject.c"
v = object.__getattribute__(self, key)
if hasattr(v, '__get__'):
return v.__get__(None, self)
return v

上面的定义显示,如果b.x是一个描述器对象,即能找到__get__方法,则会调用这个get方法,否则就使用普通的属性。 如果在一个类中重写__getattribute__,将会改变描述器的行为,甚至将描述器这一功能关闭。

2.__get____set__方法中的参数解释 官网中标明了这三个方法需要传入哪些参数,还有这些方法的返回结果是什么,如下所示

descr.__get__(self, obj, type=None) --> value
descr.__set__(self, obj, value) --> None
descr.__delete__(self, obj) --> None

我们要了解的就是self obj type value分别是什么 看下面一个例子

class M:
def __init__(self, name):
self.name = name def __get__(self, obj, type):
print('get第一个参数self: ', self.name)
print('get第二个参数obj: ', obj.age)
print('get第三个参数type: ', type.name) def __set__(self, obj, value):
obj.__dict__[self.name] = value class A:
name = 'Bob'
m = M('age')
def __init__(self, age):
self.age = age a = A(20) # age是20
a.m
# get第一个参数self: age
# get第二个参数obj: 20
# get第三个参数type: Bob
a.m = 30
a.age # 30

总结如下

  • self是描述器类M中的实例
  • obj是调用描述器的类a中的实例
  • type是调用描述器的类A
  • value是对这个属性赋值时传入的值,即上面的30

上面的代码逻辑如下

  • a.m访问描述器,调用__get__方法
  • 三次打印分别调用了m.name a.age A.name
  • a.m = 30调用了__set__方法,令a(即obj)的属性中的'age'(即M('age')这里传入的self.name)为30

实例方法、静态方法和类方法的描述器原理

本节说明访问些方法其实都访问的是描述器,并说明它们调用顺序是怎样的,以及类方法和静态方法描述器的python定义。

class B:
@classmethod
def print_classname(cls):
print('Bob') @staticmethod
def print_staticname():
print('my name is bob') def print_name(self):
print('this name') b = B()
b.print_classname() # 调用类方法
b.print_staticname() # 调用静态方法
b.print_name() # 调用实例方法
print(B.__dict__) # 里面有实例方法、静态方法和类方法
# 但其实字典里的还不是可以直接调用的函数
print(B.__dict__['print_classname'])
print(b.print_classname) # 和上不一样
print(B.__dict__['print_staticname'])
print(b.print_staticname) # 和上不一样
print(B.__dict__['print_name'])
print(b.print_name) # 和上不一样 # <classmethod object at 0x0000024A92DA67B8>
# <bound method B.print_classname of <class '__main__.B'>>
# <staticmethod object at 0x0000024A92DA6860>
# <function B.print_staticname at 0x0000024A92D889D8>
# <function B.print_name at 0x0000024A92D88158>
# <bound method B.print_name of <__main__.B object at 0x0000024A92DA6828>>

上面结果表明,实例直接调用时,类方法和实例方法都是bound method,而静态方法是function。因为静态方法本身就是定义在类里面的函数,所以不属于方法范畴。

除此之外,由于实例直接调用后得到的结果可以直接接一个括号,当成函数来调用。而使用字典调用时,得到的结果和实例调用都不一样,所以它们是不可以直接接括号当成函数使用的。

其实从显示的结果我们可以看出,静态方法和类方法用字典调用得到的其实分别是staticmethod和classmethod两个类的对象,这两个类其实是定义描述器的类,所以用字典访问的两个方法得到的都是描述器对象。它们需要用一个__get__方法才可以在后面接括号当成函数调用。

而普通实例方法用字典调用得到的是一个function即函数,理论上是可以用括号直接调用的,但是调用时报错说少了self参数,其实它也是描述器对象,用通过__get__方法将self传入来调用

三种方法本质上调用__get__方法的情况展示如下

B.__dict__['print_classname'].__get__(None, B)()
B.__dict__['print_staticname'].__get__(None, B)()
B.__dict__['print_name'].__get__(b, B)() print(B.__dict__['print_classname'].__get__(None, B))
print(B.__dict__['print_staticname'].__get__(None, B)) print(B.__dict__['print_name'])
print(B.__dict__['print_name'].__get__(None, B)) # 这是不传入实例即self的情况,和直接从字典调用结果相同,在python2中是一个unbound method
print(B.__dict__['print_name'].__get__(b, B)) # B.print_name() # 报错,说少输入一个self参数
# B.print_name(B()) # this name 输入实例即不会报错

所以说我们平常调用的方法都是本质上在调用描述器对象,访问描述器时自动调用__get__方法。

上面调用时注意到,前两个__get__的第一个参数都是None,而实例方法是一个b,这是因为实例方法需要具体的实例来调用而不能用类直接调用。在python2中,用类直接调用实例方法得到的是一个unbound method,用实例调用才是一个bound method,(在python3删除了unbound method的概念,改为function),而类方法本身就可以被类调用,所以参数是None时就是一个bound method了。所以说__get__的第一个参数使用b可以理解成方法的bound过程。

既然三种方法都是调用了描述器对象,那么这些对象都是各自类的实例,它们的类是如何定义的呢?python中这些类的定义是用底层的C语言实现的,为了理解其工作原理,这里展示一个用python语言实现classmethod装饰器的方法,(来源),即构建能产生类方法对应描述器对象的类。

class myclassmethod(object):
def __init__(self, method):
self.method = method
def __get__(self, instance, cls):
return lambda *args, **kw: self.method(cls, *args, **kw) class Myclass:
x = 3
@myclassmethod
def method(cls, a):
print(cls.x+a) m = Myclass()
Myclass.method(a=2)

下面我们分析一下上述代码

  • 我们看到使用@myclassmethod装饰器达到的效果和使用@classmethod装饰器没有什么区别
  • 首先定义了myclassmethod类,里面使用了__get__方法,所以它的实例会是一个描述器对象
  • myclassmethod当做装饰器作用于method函数,根据装饰器的知识,相当于这样设置method=myclassmethod(method)
  • 调用Myclass.method()时调用了改变后了的method方法,即myclassmethod(method)(a)
  • myclassmethod(method)这是myclassmethod类的一个实例,即一个描述器,此处访问于是调用__get__方法,返回一个匿名函数
  • __get__中其实是将owner(cls)部分传入method方法,因为methon在Myclass类中调用,这个owner也就是Myclass类。这一步其实是提前传入了method的第一个参数cls,后面的参数a由myclassmethod(method)(a)第二个括号调用
  • 仔细分析上面的定义与调用过程,我们会发现,我们常常说的类方法第一个参数要是cls,其实是不对的,第一个参数是任意都可以,它只是占第一个位置,用于接收类实例引用类属性,随便换成任意变量都可以,用cls只是约定俗成的。比如下面的代码正常运行
class Myclass:
x = 3
@classmethod
def method(b, a):
print(b.x+a) m = Myclass()
Myclass.method(a=2) # 5

下面看一下staticmethod类的等价python定义(来源

class mystaticmethod:
def __init__(self, callable):
self.f = callable
def __get__(self, obj, type=None):
return self.f class Myclass:
x = 3
@mystaticmethod
def method(a, b):
print(a + b) m = Myclass()
m.method(a=2, b=3)

注:从源码角度来理解静态方法和类方法

  • 静态方法相当于不自动传入实例对象作为方法的第一个参数,类方法相当于将默认传入的第一个参数由实例改为类
  • 使用@classmethod后无论类调用还是实例调用,都会自动转入类作为第一个参数,不用手动传入就可以调用类属性,而没有@classmethod的需要手动传入类
  • 既不用@classmethod也不用@staticmethod则类调用时不会自动传入参数,实例调用时自动传入实例作为第一个参数
  • 所以说加@classmethod是为了更方便调用类属性,加@staticmethod是为了防止自动传入的实例的干扰
  • 除此之外要说明一点:当属性和方法重名时,调用会自动访问属性,是因为这些方法调用的描述器都是非资料描述器。而当我们使用@property装饰器后,自动调用的就是新定义的get set方法,是因为@property装饰器是资料描述器

property装饰器的原理

到这里我们可以讲一讲开头提出的问题了,即@property装饰器是如何使用描述器实现的,调用机制是怎样的,如何通过描述器达到精简多次使用@property装饰器的问题。

首先要明确,property有两种调用形式,一种是用装饰器,一种是用类似函数的形式,下面会用引言中的例子分别说明两种形式的调用机制。

下面贴出property的等价python定义(来源于官网的中文翻译

class Property(object):
"Emulate PyProperty_Type() in Objects/descrobject.c" def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
if doc is None and fget is not None:
doc = fget.__doc__
self.__doc__ = doc def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError("unreadable attribute")
return self.fget(obj) def __set__(self, obj, value):
if self.fset is None:
raise AttributeError("can't set attribute")
self.fset(obj, value) def __delete__(self, obj):
if self.fdel is None:
raise AttributeError("can't delete attribute")
self.fdel(obj) def getter(self, fget):
return type(self)(fget, self.fset, self.fdel, self.__doc__) def setter(self, fset):
return type(self)(self.fget, fset, self.fdel, self.__doc__) def deleter(self, fdel):
return type(self)(self.fget, self.fset, fdel, self.__doc__)

从上面的定义中我们可以看出,定义时分为两个部分,一个是__get__等方法的定义,另一部分是getter等方法的定义,同时注意到这个类要传入fget等三个函数作为属性。getter等方法的定义是为了让它可以完美地使用装饰器形式,我们先不看这一部分,先看看不是使用第一种即不使用装饰器的形式的调用机制。

# 类似函数的形式
class A:
def __init__(self, name, score):
self.name = name # 普通属性
self.score = score def getscore(self):
return self._score def setscore(self, value):
print('setting score here')
if isinstance(value, int):
self._score = value
else:
print('please input an int') score = property(getscore, setscore) a = A('Bob',90)
a.name # 'Bob'
a.score # 90
a.score = 'bob' # please input an int

分析上述调用score的过程

  • 初始化时即开始访问score,发现有两个选项,一个是属性,另一个是property(getscore, setscore)对象,因为后者中定义了__get____set__方法,因此是一个资料描述器,具有比属性更高的优先级,所以这里就访问了描述器
  • 因为初始化时是对属性进行设置,所以自动调用了描述器的__set__方法
  • __set__中对fset属性进行检查,这里即传入的setscore,不是None,所以调用了fsetsetscore方法,这就实现了设置属性时使用自定义函数进行检查的目的
  • __get__也是一样,查询score时,调用__get__方法,触发了getscore方法

下面是另一种使用property的方法

# 装饰器形式,即引言中的形式
class A:
def __init__(self, name, score):
self.name = name # 普通属性
self.score = score @property
def score(self):
print('getting score here')
return self._score @score.setter
def score(self, value):
print('setting score here')
if isinstance(value, int):
self._score = value
else:
print('please input an int') a = A('Bob',90)
# a.name # 'Bob'
# a.score # 90
# a.score = 'bob' # please input an int

下面进行分析

  • 在第一种使用方法中,是将函数作为传入property中,所以可以想到是否可以用装饰器来封装
  • get部分很简单,访问score时,加上装饰器变成访问property(score)这个描述器,这个score也作为fget参数传入__get__中指定调用时的操作
  • 而set部分就不行了,于是有了setter等方法的定义
  • 使用了propertysetter装饰器的两个方法的命名都还是score,一般同名的方法后面的会覆盖前面的,所以调用时调用的是后面的setter装饰器处理过的score,是以如果两个装饰器定义的位置调换,将无法进行属性赋值操作。
  • 而调用setter装饰器的score时,面临一个问题,装饰器score.setter是什么呢?是scoresetter方法,而score是什么呢,不是下面定义的这个score,因为那个score只相当于参数传入。自动向其他位置寻找有没有现成的score,发现了一个,是property修饰过的score,这是个描述器,根据property的定义,里面确实有一个setter方法,返回的是property类传入fset后的结果,还是一个描述器,这个描述器传入了fgetfset,这就是最新的score了,以后实例只要调用或修改score,使用的都是这个描述器
  • 如果还有del则装饰器中的score找到的是setter处理过的score,最新的score就会是三个函数都传入的score
  • 对最新的score的调用及赋值删除都跟前面一样了

property的原理就讲到这里,从它的定义我们可以知道它其实就是将我们设置的检查等函数传入get set等方法中,让我们可以自由对属性进行操作。它是一个框架,让我们可以方便传入其他操作,当很多对象都要进行相同操作的话,重复就是难免的。如果想要避免重复,只有自己写一个类似property的框架,这个框架不是传入我们希望的操作了,而是就把这些操作放在框架里面,这个框架因为只能实现一种操作而不具有普适性,但是却能大大减少当前问题代码重复问题

下面使用描述器定义了Checkint类之后,会发现A类简洁了非常多

class Checkint:

    def __init__(self, name):
self.name = name def __get__(self, instance, owner):
if instance is None:
return self
else:
return instance.__dict__[self.name] def __set__(self, instance, value):
if isinstance(value, int):
instance.__dict__[self.name] = value
else:
print('please input an integer') # 类似函数的形式
class A:
score = Checkint('score')
age = Checkint('age') def __init__(self, name, score, age):
self.name = name # 普通属性
self.score = score
self.age = age a = A('Bob', 90, 30)
a.name # 'Bob'
a.score # 90
# a.score = 'bob' # please input an int
# a.age='a' # please input an integer

描述器的应用

因为我本人也刚刚学描述器不久,对它的应用还不是非常了解,下面只列举我现在能想到的它有什么用,以后如果想到其他的再补充

  • 首先是上文提到的,它是实例方法、静态方法、类方法、property的实现原理
  • 当访问属性、赋值属性、删除属性,出现冗余操作,或者苦思无法找到答案时,可以求助于描述器
  • 具体使用1:缓存。比如调用一个类的方法要计算比较长的时间,这个结果还会被其他方法反复使用,我们不想每次使用和这个相关的函数都要把这个方法重新运行一遍,于是可以设计出第一次计算后将结果缓存下来,以后调用都使用存下来的结果。只要使用描述器在__get__方法中,在判断语句下,obj.__dict__[self.name] = value。这样每次再调用这个方法都会从这个字典中取得值,而不是重新运行这个方法。(例子来源最后的那个例子)

最新文章

  1. OutofMemory之PermGen介绍
  2. C 标准库系列之limits.h
  3. 建设商城网站ecshop如何开启伪静态
  4. Android jni简便开发流程
  5. vim 简单配置
  6. opencv 手写选择题阅卷 (二)字符识别
  7. HDU 4123 Bob’s Race 树的直径 RMQ
  8. Porsche Piwis Tester II “No VCI has been detected”,how to do?
  9. Bzoj 3694: 最短路 树链剖分
  10. ueditor的工具按钮配置
  11. Gephi安装
  12. SqlBulkCopy 批量insert
  13. win10 UWP 发邮件
  14. [HNOI 2004]L语言
  15. 使用docker安装mysql服务
  16. 从零开始学 Web 之 jQuery(一)jQuery的概念,页面加载事件
  17. Nginx负载均衡初识
  18. JSP内置对象——page对象
  19. mysql中字符集和排序规则说明
  20. C#--类成员

热门文章

  1. ICMP协议、DNS、ARP协议、ping、DHCP协议
  2. 第02章:MongoDB安装
  3. UVa 11762 Race to 1 (数学期望 + 记忆化搜索)
  4. Curry化函数
  5. i2c总线,核心,驱动详解
  6. VB网络编程中Winsock的使用
  7. python psutil简单示例
  8. Spring的下载与安装
  9. Attach()和Detach()函数
  10. css设置自适应屏幕高度