编写 Python 类的各种技巧和窍门
简介
有关 Python 内编写类的各种技巧和方法(构建和初始化、重载操作符、类描述、属性访问控制、自定义序列、反射机制、可调用对象、上下文管理、构建描述符对象、Pickling)。 你可以把它当作一个教程,进阶,或者使用参考;我希望它能够成为一份针对 Python 方法的用户友好指南。
内容目录
介绍
构建和初始化
使操作符在自定义类内工作
神奇方法——比较
神奇方法——数字
描述你的类
属性访问控制
制作自定义序列
反射
可调用对象
上下文管理
构建描述符对象
Pickling 你的对象
总结
附录:如何调用神奇方法
1.介绍
这份指南是几个月内最有价值的 Blog 投稿精华。它的主题是向大家讲述 Python 中的神奇方法。
何为神奇方法呢?它们是面向 Python 中的一切,是一些特殊的方法允许在自己的定义类中定义增加“神奇”的功能。它们总是使用双下划线(比如 __ init__ 或 __ lt__),但它们的文档没有很好地把它们表现出来。所有这些神奇方法都出现在Python的官方文档中,但内容相对分散,组织结构也显得松散。还有你会难以发现一个实例(虽然他们被设计很棒,在语言参考中被详细描述,可之后就会伴随着枯燥的语法描述等)。
所以,为了解决我认为在 Python 文档中的一大败笔,我打算用更多纯英语,实例驱动的文档来说明 Python 的神奇方法。然后我就开始花了几周的时间来写 blog,而现在我已经完成了它们,并将它们合订成一份指南。
我希望你喜欢它。把它当作一个教程,进阶,或者使用参考;我希望它能够成为一份针对 Python 方法的用户友好指南。
2.构建和初始化
相信大家都熟悉这个最基础的神奇方法 __ init__。它令你能自定义一个对象的初始化行为。而当我调用x=SomeClass() 时,__ init__ 并不是最先被调用的。实际上有一个叫做 __ new__ 的方法,事实上是它创建了实例,它传递任何参数给初始化程序来达到创建的目的。在对象生命周期结束时,调用 __ del__。让我们更近地观察下这 3 个神奇方法吧:
- __ new__(cls,[...)
一个对象的实例化时 __ new__ 是第一个被调用的方法。在类中传递其他任何参数到 __ init__。__new__很少被使用,这样做确实有其目的,特别是当一个子类继承一个不可改变的类型(一个元组或一个字符串)时。我不打算再继续深入追求 __ new __ 的细节了,因为这不会产生多大用处,因为在 Python Docs 内已经涵盖了一份巨详细的说明了。
- __ init__(self,[...)
类的初始化。它会获得初始构建调用传过来的任何东西(举例来说就是,当我们调用x=SomeClass(10,'foo'),__ init__ 就会把传过来的 10 和 'foo' 作为参数。__init__在 Python 的类定义中几乎普遍被使用)
- __ del__(self)
如果 __ new__和 __ init__ 是对象的构造器,那么 __ del__ 就是析构器。它不实现声明为del x(这样的代码不会解释成 x.__ del__())的行为。相反,它定义为当一个对象被垃圾回收时的行为。这可能对可能需要额外清理的对象相当有用,比如 sockets 或文件对象。但要小心,如果对象仍处于存活状态而当被解释退出时,__ del__ 没有保证就会被执行,因此这样的__ del__ 不能作为良好的编码规范的替代。(就像当你完成操作总是要关闭一次连接。但事实上,__ del__ 几乎永远不会执行,就因为它处于不安全情况被调用了。使用时保持警惕!)
把上述这些内容合在一起,就成了一份 __ init__ 和 __ del__ 的实际使用用例:
from os.path import join
class FileObject:
'''对文件对象的包装,确保文件在关闭时得到删除'''
def __init__(self, filepath='~', filename='sample.txt'):
# 按filepath,读写模式打开名为filename的文件
self.file=open(join(filepath,filename), 'r+')
def __del__(self):
self.file.close()
del self.file
3.使操作符在自定义类内工作
使用 Python 神奇方法的优势之一就是它提供了一种简单的方式能让对象的行为像内建类型。这意味着你可以避免用丑陋,反直觉和非标准方法执行基本运算。在某些语言中,通常会这样做:
if instance.equals(other_instance):
# do something
你也应该在 Python 确实会这样做,但同时它会增加用户的疑惑以及不必要的冗长。不同的库可能会对相同的运算采用不同的命名,这使得用户比平常干了更多的事。依靠神奇方法的力量,你可以定义一个方法(比如 __ eq__),然后带代替我们真实的意图:
if instance == other_instance:
# do something
现在你看到的是神奇方法力量的一部分。绝大多数都允许我们定义为运算符本身的意义,当用在我们自己定义的类上就像它们是内建类型。
3.1 神奇方法——比较
Python 有一整套神奇方法被设计用来通过操作符实现对象间直观的比较,而非别扭的方法调用。它们同样提供了一套覆盖 Python 对象比较的默认行为(通过引用)。以下是这些方法的列表以及做法:
__ cmp__(self, other)
__ cmp__是神奇方法中最基础的一个。实际上它实现所有比较操作符行为(<,==,!=,等),但它有可能不按你想要的方法工作(例如,一个实例是否等于另一个这取决于比较的准则,以及一个实例是否大于其他的这也取决于其他的准则)。如果 self < other,那 __ cmp__ 应当返回一个负整数;如果 self == other,则返回 0;如果 self > other,则返回正整数。它通常是最好的定义,而不需要你一次就全定义好它们,但当你需要用类似的准则进行所有的比较时,__ cmp__ 会是一个很好的方式,帮你节省重复性和提高明确度。
__ eq__(self, other)
定义了相等操作符,==的行为。
__ ne__(self, other)
定义了不相等操作符,!= 的行为。
__ lt__(self, other)
定义了小于操作符,< 的行为。
__ gt__(self, other)
定义了大于操作符,> 的行为。
__ le__(self, other)
定义了小于等于操作符,<=的行为。
__ ge__(self, other)
定义了大于等于操作符,>= 的行为。
举一个例子,设想对单词进行类定义。我们可能希望能够按内部对 string 的默认比较行为,即字典序(通过字母)来比较单词,也希望能够基于某些其他的准则,像是长度或音节数。在本例中,我们通过单词长度排序,以下给出实现:
class Word(str):
'''单词类,比较定义是基于单词长度的'''
def __new__(cls, word):
# 注意,我们使用了__new__,这是因为str是一个不可变类型,
# 所以我们必须更早地初始化它(在创建时)
if ' ' in word:
print "单词内含有空格,截断到第一部分"
word = word[:word.index(' ')] # 在出现第一个空格之前全是字符了现在
return str.__new__(cls, word)
def __gt__(self, other):
return len(self) > len(other)
def __lt__(self, other):
return len(self) < len(other)
def __ge__(self, other):
return len(self) >= len(other)
def __le__(self, other):
return len(self) <= len(other)
现在,我们可以创建 2 个单词(通过 Word('foo') 和 Word('bar'))并基于它们的长度进行比较了。注意,我们没有定义 __ eq__ 和 __ ne__。这是因为这可能导致某些怪异的行为(特别是当比较 Word('foo') == Word('bar') 将会得到 True 的结果)。基于单词长度的相等比较会令人摸不清头脑,因此我们就沿用了str 本身的相等比较的实现。
现在可能是一个好时机来提醒你一下,你不必重载每一个比较相关的神奇方法来获得各种比较。标准库已经友好地为我们在模板 functools 中提供了一个装饰(decorator)类,定义了所有比较方法。你可以只重载 __ eq__ 和一个其他的方法(比如 __ gt__,__ lt__,等)。这个特性只在 Python2.7(后?)适用,但当你有机会的话应该尝试一下,它会为你省下大量的时间和麻烦。你可以通过在你自己的重载方法在加上 @total_ordering 来使用。
3.2 神奇方法——数字
就像你可以通过重载比较操作符的途径来创建你自己的类实例,你同样可以重载数字操作符。系好你们的安全带,朋友们,还有很多呢。处于本文组织的需要,我会把数字的神奇方法分割成5块:一元操作符,常规算术操作符,反射算术操作符,增量赋值,类型转换。
一元操作符
一元运算和函数仅有一个操作数,比如负数,绝对值等
__ pos__(self)
实现一元正数的行为(如:+some_object)
__ neg__(self)
实现负数的行为(如: -some_object)
__ abs__(self)
实现内建 abs() 函数的行为
__ invert__(self)
实现用~操作符进行的取反行为。你可以参考 Wiki:bitwise operations 来解释这个运算符究竟会干什么
常规算术操作符
现在我们涵盖了基本的二元运算符:+,-,* 等等。其中大部分都是不言自明的。
__ add__(self, other)
实现加法
__ sub__(self, other)
实现减法
__ mul__(self, other)
实现乘法
__ floordiv__(self, other)
实现地板除法,使用 // 操作符
__ div__(self, other)
实现传统除法,使用 / 操作符
__ truediv__(self, other)
实现真正除法。注意,只有当你 from __ future__ import division 时才会有效
__ mod__(self, other)
实现求模,使用 % 操作符
__ divmod__(self, other)
实现内建函数 divmod() 的行为
__ pow__(self, other)
实现乘方,使用 ** 操作符
__ lshift__(self, other)
实现左按位位移,使用 << 操作符
__ rshift__(self, other)
实现右按位位移,使用 >> 操作符
__ and__(self, other)
实现按位与,使用 & 操作符
__ or__(self, other)
实现按位或,使用 | 操作符
__ xor__(self, other)
实现按位异或,使用 ^ 操作符
反射算术操作符
你知道我会如何解释反射算术操作符?你们中的有些人或许会觉得它很大,很可怕,是国外的概念。但它实际上很简单,下面给一个例子:
some_object + other
这是“常规的”加法。而反射其实相当于一回事,除了操作数改变了改变下位置:
other + some_object
因此,所有这些神奇的方法会做同样的事等价于常规算术操作符,除了改变操作数的位置关系,比如第一个操作数和自身作为第二个。此外没有其他的操作方式。在大多数情况下,反射算术操作的结果等价于常规算术操作,所以你尽可以在刚重载完 __ radd__就调用 __ add__。干脆痛快:
__radd__(self, other)
实现反射加法
__rsub__(self, other)
实现反射减法
__rmul__(self, other)
实现反射乘法
__rfloordiv__(self, other)
实现反射地板除,用 // 操作符
__rdiv__(self, other)
实现传统除法,用 / 操作符
__rturediv__(self, other)
实现真实除法,注意,只有当你 from __future__ import division 时才会有效
__rmod__(self, other)
实现反射求模,用 % 操作符
__rdivmod__(self, other)
实现内置函数 divmod() 的长除行为,当调用 divmod(other,self) 时被调用
__rpow__(self, other)
实现反射乘方,用 ** 操作符
__rlshift__(self, other)
实现反射的左按位位移,使用 << 操作符
__rrshift__(self, other)
实现反射的右按位位移,使用 >> 操作符
__rand__(self, other)
实现反射的按位与,使用 & 操作符
__ror__(self, other)
实现反射的按位或,使用 | 操作符
__rxor__(self, other)
实现反射的按位异或,使用 ^ 操作符
增量赋值
Python 也有各种各样的神奇方法允许用户自定义增量赋值行为。你可能已经熟悉增量赋值,它结合了“常规的”操作符和赋值。如果你仍不明白我在说什么,下面有一个例子:
x = 5
x += 1 # 等价 x = x + 1
这些方法都不会有返回值,因为赋值在 Python 中不会有任何返回值。反而它们只是改变类的状态。列表如下:
__iadd__(self, other)
实现加法和赋值
__isub__(self, other)
实现减法和赋值
__imul__(self, other)
实现乘法和赋值
__ifloordiv__(self, other)
实现地板除和赋值,用 //= 操作符
__idiv__(self, other)
实现传统除法和赋值,用 /= 操作符
__iturediv__(self, other)
实现真实除法和赋值,注意,只有当你 from __future__ import division 时才会有效
__imod__(self, other)
实现求模和赋值,用 %= 操作符
__ipow__(self, other)
实现乘方和赋值,用 **= 操作符
__ilshift__(self, other)
实现左按位位移和赋值,使用 <<= 操作符
__irshift__(self, other)
实现右按位位移和赋值,使用 >>= 操作符
__iand__(self, other)
实现按位与和赋值,使用 &= 操作符
__ior__(self, other)
实现按位或和赋值,使用 |= 操作符
__ixor__(self, other)
实现按位异或和赋值,使用 ^= 操作符
类型转换的神奇方法
Python 也有一组神奇方法被设计用来实现内置类型转换函数的行为,如 float()
__int__(self)
实现到 int 的类型转换
__long__(self)
实现到 long 的类型转换
__float__(self)
实现到 float 的类型转换
__complex__(self)
实现到复数的类型转换
__oct__(self)
实现到 8 进制的类型转换
__hex__(self)
实现到 16 进制的类型转换
__index__(self)
实现一个当对象被切片到 int 的类型转换。如果你自定义了一个数值类型,考虑到它可能被切片,所以你应该重载__index__
__trunc__(self)
当 math.trunc(self) 被调用时调用。__trunc__ 应当返回一个整型的截断,(通常是 long)
__coerce__(self, other)
该方法用来实现混合模式的算术。如果类型转换不可能那 __coerce__ 应当返回 None。 否则,它应当返回一对包含 self 和 other(2 元组),且调整到具有相同的类型
4.描述你的类
用一个字符串来说明一个类这通常是有用的。 在 Python 中提供了一些方法让你可以在你自己的类中自定义内建函数返回你的类行为的描述。
__str__(self)
当你定义的类中一个实例调用了 str(),用于给它定义行为
__repr__(self)
当你定义的类中一个实例调用了 repr(),用于给它定义行为。 str() 和 repr() 主要的区别在于它的阅读对象。 repr() 产生的输出主要为计算机可读(在很多情况下,这甚至可能是一些有效的 Python 代码),而 str() 则是为了让人类可读。
__unicode__(self)
当你定义的类中一个实例调用了 unicode(),用于给它定义行为。 unicode() 像是 str(),只不过它返回一个 unicode 字符串。 警惕!如果用户用你的类中的一个实例调用了 str(),而你仅定义了 __unicode__(),那它是不会工作的。 以防万一,你应当总是定义好 __str__(),哪怕用户不会使用 unicode
__hash__(self)
当你定义的类中一个实例调用了 hash(),用于给它定义行为。 它必须返回一个整型,而且它的结果是用于来在字典中作为快速键比对。
__nonzero__(self)
当你定义的类中一个实例调用了 bool(),用于给它定义行为。 返回 True 或 False,取决于你是否考虑一个实例是 True 或 False 的。
我们已经相当漂亮地干完了神奇方法无聊的部分(无示例),至此我们已经讨论了一些基础的神奇方法,是时候让我们向高级话题移动了。
5.属性访问控制
有许多从其他语言阵营转到 Python 来的人抱怨 Python 对类缺乏真正的封装(比如,没有办法自定义 private 属性,已经给出 public 的 getter 和 setter)。 这可不是真相哟:Python 通过神奇的方法实现了大量的封装,而不是通过明确的方法或字段修饰符。
请看:
__ getattr__(self, name)
你可以为用户在试图访问不存在(不论是存在或尚未建立)的类属性时定义其行为。 这对捕捉和重定向常见的拼写错误,给出使用属性警告是有用的(只要你愿意,你仍旧可选计算,返回那个属性)或抛出一个 AttributeError异常。 这个方法只适用于访问一个不存在的属性,所以,这不算一个真正封装的解决之道。
__ setattr__(self, name, value)
不像 __ getattr__,__ setattr__ 是一个封装的解决方案。 它允许你为一个属性赋值时候的行为,不论这个属性是否存在。 这意味着你可以给属性值的任意变化自定义规则。 然而,你需要在意的是你要小心使用 __ setattr__,在稍后的列表中会作为例子给出。
__ delattr__
这等价于 __ setattr__, 但是作为删除类属性而不是 set 它们。 它需要相同的预防措施,就像 __ setattr__,防止无限递归(当在 __ delattr__ 中调用 del self.name 会引起无限递归)。
__ getattribute__(self, name)
__ getattribute__ 良好地适合它的同伴们 __ setattr__ 和 __ delattr__。 可我却不建议你使用它。__ getattribute__ 只能在新式类中使用(在 Python 的最新版本中,所有的类都是新式类,在稍旧的版本中你可以通过继承 object 类来创建一个新式类。 它允许你定规则,在任何时候不管一个类属性的值那时候是否可访问的。) 它会因为他的同伴中的出错连坐受到某些无限递归问题的困扰(这时你可以通过调用基类的__ getattribute__ 方法来防止发生)。 当 __ getattribute__ 被实现而又只调用了该方法如果__ getattribute__ 被显式调用或抛出一个 AttributeError 异常,同时也主要避免了对 __ getattr__ 的依赖。 这个方法可以使用(毕竟,这是你自己的选择),不过我不推荐它是因为它有一个小小的用例(虽说比较少见,但我们需要特殊行为以获取一个值而不是赋值)以及它真的很难做到实现 0bug。
你可以很容易地在你自定义任何类属性访问方法时引发一个问题。参考这个例子:
def __setattr__(self, name, value):
self.name = value
# 当每次给一个类属性赋值时,会调用__setattr__(),这就形成了递归
# 因为它真正的含义是 self.__setattr__('name', value)
# 所以这方法不停地调用它自己,变成了一个无法退出的递归最终引发crash
def __setattr__(self, name, value):
self.__dict__[name] = value # 给字典中的name赋值
# 在此自定义行为
再一次,Python 的神奇方法向我们展示了其难以置信的能力,同时巨大的力量也伴随着重大的责任。 重要的是让你明白正确使用神奇方法,这样你就不会破坏其他代码。
那么,我们在关于定制类属性访问中学习了什么? 不要轻易地使用,事实上它过于强大以及反直觉。 这也是它为何存在的理由:Python 寻求干坏事的可能性,但会把它们弄得很难。 *是至高无上的,所以你可以做任何你想做的事情。 以下是一个关于特殊属性访问方法的实际例子(注意,我们使用 super 因为并非所有类都有 __ dict__类属性):
class AccessCounter:
'''一个类包含一个值和实现了一个访问计数器。
当值每次发生变化时,计数器+1'''
def __init__(self, val):
super(AccessCounter, self).__setattr__('counter',0)
super(AccessCounter, self).__setattr__('value', val)
def __setattr__(self, name, value):
if name == 'value':
super(AccessCounter, self).__setattr__('counter', self.counter + 1)
# Make this unconditional.
# 如果你想阻止其他属性被创建,抛出AttributeError(name)异常
super(AccessCounter, self).__setattr__(name, value)
def __delattr__(self, name)
if name == 'value':
super(AccessCounter, self).__setattr__('counter', self.counter + 1)
super(AccessCounter, self).__delattr__(name)
6.制作自定义序列
很有多种方式可以让你的类表现得像内建序列(字典,元组,列表,字符串等)。 这些是我迄今为止最喜欢的神奇方法了,因为不合理的控制它们赋予了你一种魔术般地让你的类实例整个全局函数数组漂亮工作的方式。 在我们开始讲解这个内容之前,让我们先快速理清需求。
需求
现在我们正在谈论如何创建你自己的序列。 也是什么谈一谈 protocol 了。 protocol 在某些地方跟接口很相似。 接口在其他语言中,是一组给定的方法,而你必须定义它们。 然而,在 Python 中 protocol 是完全非正式的,而且不要求显式声明去实现。 更进一步说,它们更像是准则。
为何我们现在要谈论 protocol? 因为在 Python 中要实现自定义容器类型会涉及使用到这其中某些 protocol。
首先,有一个 protocol 是为定义不可变容器的:为了制作一个不可变容器,你只需要定义 __ len__ 和__ getitem__(稍后详述)。 可变容器 protocol 要求所有不可变容器增加 __ setitem__ 和 __ delitem__。 然后,如果你希望你的对象是可迭代的,那你还得定义一个会返回迭代器 iterator 的 __ iter__ 方法。 并且这个迭代器必须遵守一个迭代 protocol,也就是要求迭代器有回调方法 __ iter__ (返回自身)和 next。
隐藏在容器背后的魔法
已经迫不及待了?以下便是容器使用的神奇魔法:
__ len__(self)
返回容器的长度。部分 protocol 同时支持可变和不可变容器
__ getitem__(self, key)
定义当某一个 item 被访问时的行为,使用 self[key] 表示法。 这个同样也是部分可变和不可变容器 protocol。 这也可抛出适当的异常: TypeError 当 key 的类型错误,或没有值对应 Key 时。
__ setitem__(self, key, value)
定义当某一个 item 被赋值时候的行为,使用 self[key]=value 表示法。 这也是部分可变和不可变容器 protocol。 再一次重申,你应当在适当之处抛出 KeyError 和 TypeError 异常。
__delitem__(self, key)
定义当某一个 item 被删除(例如 del self[key])时的行为。 这仅是部分可变容器的 protocol。在一个无效key 被使用后,你必须抛出一个合适的异常。
__ iter__(self)
应该给容器返回一个迭代器。 迭代器会返回若干内容,大多使用内建函数 iter() 表示。 当一个容器使用形如 for x in container: 的循环。 迭代器本身就是其对象,同时也要定义好一个 __iter__ 方法来返回自身。
__ reversed__(self)
当定义调用内建函数 reversed() 时的行为。应该返回一个反向版本的列表。
__ contains__(self, item)
__ contains__ 为成员关系,用 in 和 not in 测试时定义行为。 那你会问这个为何不是一个序列的 protocol 的一部分? 这是因为当 __contains__ 未定义,Python 就会遍历序列,如果遇到正在寻找的 item 就会返回True。
__ concat__(self, other)
最后,你可通过 __concat__ 定义你的序列和另外一个序列的连接。 应该从 self 和 other 返回一个新构建的序列。 当调用 2 个序列时 __concat__ 涉及操作符 +
一个例子
在我们的例子中,让我们看一下一个 list 实现的某些基础功能性的构建。 可能会让你想起你使用的其他语言(比如 Haskell)。
class FunctionalList:
'''类覆盖了一个list的某些额外的功能性魔法,像head,
tail,init,last,drop,and take'''
def __init__(self, values=None):
if values is None:
self.values = []
else:
self.values = values
def __len__(self):
return len(self.values)
def __getitem__(self, key):
# 如果key是非法的类型和值,那么list valuse会抛出异常
return self.values[key]
def __setitem__(self, key, value):
self.values[key] = value
def __delitem__(self, key):
del self.values[key]
def __iter__(self):
return iter(self.values)
def __reversed__(self):
return reversed(self.values)
def append(self, value):
self.values.append(value)
def head(self):
# 获得第一个元素
return self.values[0]
def tail(self):
# 获得在第一个元素后的其他所有元素
return self.values[1:]
def init(self):
# 获得除最后一个元素的序列
return self.values[:-1]
def last(last):
# 获得最后一个元素
return self.values[-1]
def drop(self, n):
# 获得除前n个元素的序列
return self.values[n:]
def take(self, n):
# 获得前n个元素
return self.values[:n]
通过这个(轻量的)有用的例子你知道了如何实现你自己的序列。 当然,还有很多更有用的应用,但是它们其中的很多已经被标准库实现了,像 Counter, OrderedDict, NamedTuple
7.反射
你也可以通过定义神奇方法来控制如何反射使用内建函数 isinstance() 和 issubclass() 的行为。 这些神奇方法是:
__instancecheck__(self, instance)
检查一个实例是否是你定义类中的一个实例(比如,isinstance(instance, class))
__subclasscheck__(self, subclass)
检查一个类是否是你定义类的子类(比如,issubclass(subclass, class))
这对于神奇方法的用例情况来说可能较小,可它的确是真的。 我并不想花费太多的时间在反射方法上面,因为他们不是那么地重要。 不过它们反映了在 Python 中关于面对对象编程一些重要的东西,而且在 Python 中的普遍:总是在找一种简单的方式来做某些事情,即使它能被用到的不多。 这些神奇方法似乎看上去不那么有用,但当你需要使用它们的时候你会感激它们的存在(和你阅读的这本指南!)。
8.可调用对象
正如你可能已经知道,在 Python 中函数是第一类对象。 这就意味着它们可以被传递到函数和方法,就像是任何类型的对象。 这真是一种难以置信强大的特性。
这是 Python 中一个特别的神奇方法,它允许你的类实例像函数。 所以你可以“调用”它们,把他们当做参数传递给函数等等。 这是另一个强大又便利的特性让 Python 的编程变得更可爱了。
__ call__(self, [args...])
允许类实例像函数一样被调用。 本质上,这意味着 x() 等价于 x.__ call__()。 注意,__ call__ 需要的参数数目是可变的,也就是说可以对任何函数按你的喜好定义参数的数目定义 __ call__
__ call__ 可能对于那些经常改变状态的实例来说是极其有用的。 “调用”实例是一种顺应直觉且优雅的方式来改变对象的状态。 下面一个例子是一个类表示一个实体在一个平面上的位置:
class Entity:
'''描述实体的类,被调用的时候更新实体的位置'''
def __init__(self, size, x, y):
self.x, self.y = x, y
self.size = size
def __call__(self, x, y):
'''改变实体的位置'''
self.x, self.y = x, y
#省略...
9.上下文管理
在 Python2.5 里引入了一个新关键字(with)使得一个新方法得到了代码复用。 上下文管理这个概念在 Python 中早已不是新鲜事了(之前它作为库的一部分被实现过),但直到 PEP343 才作为第一个类语言结构取得了重要地位而被接受。 你有可能早就已经见识过 with 声明:
with open('foo.txt') as bar:
# 对bar执行某些动作
上下文管理允许对对象进行设置和清理动作,用 with 声明进行已经封装的操作。 上下文操作的行为取决于 2 个神奇方法:
__ enter__(self)
定义块用 with 声明创建出来时上下文管理应该在块开始做什么。 注意,enter 的返回值必须绑定 with 声明的目标,或是 as 后面的名称。
__ exit__(self, exception_type, exception_value, traceback)
定义在块执行(或终止)之后上下文管理应该做什么。 它可以用来处理异常,进行清理,或行动处于块之后某些总是被立即处理的事。 如果块执行成功的话,excepteion_type,exception_value,和 traceback 将会置None。 否则,你可以选择去处理异常,或者让用户自己去处理。 如果你想处理,确保在全部都完成之后__ exit__ 会返回 True。 如果你不想让上下文管理处理异常,那就让它发生好了。
__ enter__ 和 __ exit__ 对那些已有良好定义和对设置,清理行为有共同行为的特殊类是有用。 你也可以使用这些方法去创建封装其他对象通用的上下文管理。 看下面的例子:
class Closer:
'''用with声明一个上下文管理用一个close方法自动关闭一个对象'''
def __init__(self, obj):
self.obj = obj
def __enter__(self):
return self.obj # 绑定目标
def __exit__(self, exception_type, exception_val, trace):
try:
self.obj.close()
except AttributeError: #obj不具备close
print 'Not closable.'
return True # 成功处理异常
以下是一个对于 Closer 实际应用的一个例子,使用一个 FTP 连接进行的演示(一个可关闭的套接字):
>>> from magicmethods import Closer
>>> from ftplib import :;;
>>> with Closer(FTP('ftp.somsite.com')) as conn:
... conn.dir()
...
# 省略的输出
>>> conn.dir()
# 一个很长的AttributeError消息, 不能关闭使用的一个连接
>>> with Closer(int(5)) as i:
... i += 1
...
Not closeable.
>>> i
6
瞧见我们如何漂亮地封装处理正确或不正确的用例了吗?那就是上下文管理和神奇方法的威力。
10.构建描述符对象
描述符可以改变其他对象,也可以是访问类中任一的 getting,setting,deleting。 描述符不意味着孤立;相反,它们意味着会被它们的所有者类控制。 当建立面向对象数据库或那些拥有相互依赖的属性的类时,描述符是有用的。 当描述符在几个不同单元或描述计算属性时显得更为有用。
作为一个描述符,一个类必须至少实现 __ get__,__ set__,和 __delete__中的一个。 让我们快点看一下这些神奇方法吧:
__ get__(self, instance, owner)
当描述符的值被取回时定义其行为。instance 是 owner 对象的一个实例,owner 是所有类。
__ set__(self, instance, value)
当描述符的值被改变时定义其行为。instance 是 owner 对象的一个实例,value 是设置的描述符的值
__ delete__(self, instance)
当描述符的值被删除时定义其行为。instance 是 owner 对象的一个实例。
现在,有一个有用的描述符应用例子:单位转换策略
class Meter(object):
'''米描述符'''
def __init__(self, value=0.0):
self.value = float(value)
def __get__(self, instance, owner):
return self.value
def __set__(self, instance, value):
self.value = float(value)
class Foot(object):
'''英尺描述符'''
def __get__(self, instance, owner):
return instance.meter * 3.2808
def __set__(self, instance, value):
instance.meter = float(value) / 3.2808
class Distance(object):
'''表示距离的类,控制2个描述符:feet和meters'''
meter = Meter()
foot = Foot()
11.Pickling 你的对象
假如你花时间和其他 Pythonistas 打交道,那么你至少有可能听到过 Pickling 这个词。 Pickling 是一种对 Python 数据结构的序列化过程。 如果你需要存储一个对象,之后再取回它(通常是为了缓存)那么它就显得格外地有用了。 同时,它也是产生忧虑和困惑的主要来源。
Pickling 是那么地重要以至于它不仅有自己专属的模块(pickle),还有自己的 protocol 和神奇方法与其相伴。 但首先用简要的文字来解释下如何 pickle 已经存在的类型(如果你已经懂了可以随意跳过这部分内容)
Pickling:盐水中的快速浸泡
让我们跳入 pickling。 话说你有一个词典你想要保存它并在稍后取回。 你可以把它的内容写到一个文件中去,需要非常小心地确保你写了正确的语法,然后用 exec() 或处理文件的输入取回写入的内容。 但这是不稳定的:如果你你在纯文本中保存重要的数据,它有可能被几种方法改变,导致你的程序 crash 或在你的计算机上运行了恶意代码而出错。 于是,我们准备 pickle 它:
import pickle
data = {'foo': [1,2,3],
'bar': ('Hello','world!'),
'baz': True}
jar = open('data.pk1', 'wb')
pickle.dump(data, jar) # 把pickled数据写入jar文件
jar.close()
好了现在,已经过去了几个小时。 我们希望拿回数据,而我们需要做的事仅仅是 unpickle 它:
import pickle
pk1_file = open('data.pk1','rb') #连接pickled数据
data = pickle.load(pk1_file) #把数据load到一个变量中去
print data
pk1_file.close()
发生了什么事?正如你的预期,我们获得了 data。
现在,我要给你一些忠告:pickling 并非完美。 Pickle 文件很容易因意外或出于故意行为而被损毁。 Pickling 可能比起使用纯文本文件安全些,但它仍旧有可能会被用来跑恶意代码。 还有因为 Python 版本的不兼容问题,所以不要期望发布 Pickled 对象,也不要期望人们能够打开它们。 但是,它依然是一个强大的缓存工具和其他常见序列化任务。
Pickling你自定义的对象
Pickling 不仅可用在内建类型上,还可以用于遵守 pickle 协议的任何类。 pickle 协议有 4 个可选方法用于定制 Python 对象如何运行(这跟 C 扩展有点不同,但那不在我们讨论的范围内):
__ getinitargs__(self)
如果你想当你的类 unpickled 时调用 __ init__,那你可以定义__ getinitargs__,该方法应该返回一个元组的参数,然后你可以把他传递给 __ init__。注意,该方法仅适用于旧式类。
__ getnewargs__(self)
对于新式类,你可以影响有哪些参数会被传递到 __new__ 进行 unpickling。 该方法同样应该返回一个元组参数,然后能传递给 __new__
__getstate__(self)
代替对象的 __dict__ 属性被保存。 当对象 pickled,你可返回一个自定义的状态被保存。 当对象 unpickled 时,这个状态将会被 __setstate__ 使用。
__setstate__(self, state)
对象 unpickled 时,如果 __setstate__ 定义对象状态会传递来代替直接用对象的 __dict__ 属性。 这正好跟__getstate__ 手牵手:当二者都被定义了,你可以描述对象的 pickled 状态,任何你想要的。
一个例子:
我们的例子是 Slate 类,它会记忆它曾经的值和已经写入的值。 然而,当这特殊的 slate 每一次 pickle 都会被清空:当前值不会被保存。
import time
class Slate:
'''存储一个字符串和一个变更log,当Pickle时会忘记它的值'''
def __init__(self, value):
self.value = value
self.last_change = time.asctime()
self.history = {}
def change(self, new_value):
# 改变值,提交最后的值到历史记录
self.history[self.last_change] = self.value
self.value = new_value
self.last_change = time.asctime()
def print_changes(self):
print 'Changelog for Slate object:'
for k, v in self.history.items():
print '%st %s' % (k, v)
def __getstate__(self):
# 故意不返回self.value 或 self.last_change.
# 当unpickle,我们希望有一块空白的"slate"
return self.history
def __setstate__(self, state):
# 让 self.history = state 和 last_change 和 value被定义
self.history = state
self.value, self.last_change = None, None
12.总结
这份指南的目标就是任何人读一读它,不管读者们是否具备 Python 或面对对象的编程经验。 如果你正准备学习 Python,那你已经获得了编写功能丰富,优雅,易用的类的宝贵知识。 如果你是一名中级 Python 程序员,你有可能已经拾起了一些新概念和策略和一些好的方法来减少你和你的用户编写的代码量。 如果你是一名 Pythonista 专家,你可能已经回顾了某些你可能已经被你遗忘的知识点,或着你又学习到了一些新技巧。 不管你的的经验等级,我希望这次 Python 神奇方法的旅程达到了真正神奇的效果。(我无法控制自己在最后不用个双关语)
附录:如果调用神奇方法
Python 中的一些神奇方法直接映射到内建函数;在这种情况下,调用它们的方法是相当明显的。 然而,在其他情况下,那些调用方法就不这么明显了。 本附录致力于揭开能够引导神奇方法被调用的非明显语法。
神奇方法 调用方法 说明
`__new__(cls [,...])` `instance = MyClass(arg1, arg2)` `__new__` 在创建实例的时候被调用
`__init__(self [,...])` `instance = MyClass(arg1, arg2)` `__init__` 在创建实例的时候被调用
`__cmp__(self, other)` `self == other`, `self > other`, 等 在比较的时候调用
`__pos__(self)` `+self` 一元加运算符
`__neg__(self)` `-self` 一元减运算符
`__invert__(self)` `~self` 取反运算符
`__index__(self)` `x[self]` 对象被作为索引使用的时候
`__nonzero__(self)` `bool(self)` 对象的布尔值
`__getattr__(self, name)` `self.name # name不存在` 访问一个不存在的属性时
`__setattr__(self, name, val)` `self.name = val` 对一个属性赋值时
`__delattr__(self, name)` `del self.name` 删除一个属性时
`__getattribute(self, name)` `self.name` 访问任何属性时
`__getitem__(self, key)` `self[key]` 使用索引访问元素时
`__setitem__(self, key, val)` `self[key] = val` 对某个索引值赋值时
`__delitem__(self, key)` `del self[key]` 删除某个索引值时
`__iter__(self)` `for x in self` 迭代时
`__contains__(self, value)` `value in self`, `value not in self` 使用 `in` 操作测试关系时
`__concat__(self, value)` `self + other` 连接两个对象时
`__call__(self [,...])` `self(args)` “调用”对象时
`__enter__(self)` `with self as x:` `with` 语句上下文管理
`__exit__(self, exc, val, trace)` `with self as x:` `with` 语句上下文管理
`__getstate__(self)` `pickle.dump(pkl_file, self)` 序列化
`__setstate__(self)` `data = pickle.load(pkl_file)` 序列化
希望这张表格可以帮你扫清你有关语法涉及到神奇方法的问题。
推荐阅读
-
SSM三大框架基础面试题-一、Spring篇 什么是Spring框架? Spring是一种轻量级框架,提高开发人员的开发效率以及系统的可维护性。 我们一般说的Spring框架就是Spring Framework,它是很多模块的集合,使用这些模块可以很方便地协助我们进行开发。这些模块是核心容器、数据访问/集成、Web、AOP(面向切面编程)、工具、消息和测试模块。比如Core Container中的Core组件是Spring所有组件的核心,Beans组件和Context组件是实现IOC和DI的基础,AOP组件用来实现面向切面编程。 Spring的6个特征: 核心技术:依赖注入(DI),AOP,事件(Events),资源,i18n,验证,数据绑定,类型转换,SpEL。 测试:模拟对象,TestContext框架,Spring MVC测试,WebTestClient。 数据访问:事务,DAO支持,JDBC,ORM,编组XML。 Web支持:Spring MVC和Spring WebFlux Web框架。 集成:远程处理,JMS,JCA,JMX,电子邮件,任务,调度,缓存。 语言:Kotlin,Groovy,动态语言。 列举一些重要的Spring模块? Spring Core:核心,可以说Spring其他所有的功能都依赖于该类库。主要提供IOC和DI功能。 Spring Aspects:该模块为与AspectJ的集成提供支持。 Spring AOP:提供面向切面的编程实现。 Spring JDBC:Java数据库连接。 Spring JMS:Java消息服务。 Spring ORM:用于支持Hibernate等ORM工具。 Spring Web:为创建Web应用程序提供支持。 Spring Test:提供了对JUnit和TestNG测试的支持。 谈谈自己对于Spring IOC和AOP的理解 IOC(Inversion Of Controll,控制反转)是一种设计思想: 在程序中手动创建对象的控制权,交由给Spring框架来管理。IOC在其他语言中也有应用,并非Spring特有。IOC容器实际上就是一个Map(key, value),Map中存放的是各种对象。 将对象之间的相互依赖关系交给IOC容器来管理,并由IOC容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。IOC容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。在实际项目中一个Service类可能由几百甚至上千个类作为它的底层,假如我们需要实例化这个Service,可能要每次都搞清楚这个Service所有底层类的构造函数,这可能会把人逼疯。如果利用IOC的话,你只需要配置好,然后在需要的地方引用就行了,大大增加了项目的可维护性且降低了开发难度。 Spring中的bean的作用域有哪些? 1.singleton:该bean实例为单例 2.prototype:每次请求都会创建一个新的bean实例(多例)。 3.request:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效。 4.session:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP session内有效。 5.global-session:全局session作用域,仅仅在基于Portlet的Web应用中才有意义,Spring5中已经没有了。Portlet是能够生成语义代码(例如HTML)片段的小型Java Web插件。它们基于Portlet容器,可以像Servlet一样处理HTTP请求。但是与Servlet不同,每个Portlet都有不同的会话。 Spring中的单例bean的线程安全问题了解吗? 概念用于理解:大部分时候我们并没有在系统中使用多线程,所以很少有人会关注这个问题。单例bean存在线程问题,主要是因为当多个线程操作同一个对象的时候,对这个对象的非静态成员变量的写操作会存在线程安全问题。 有两种常见的解决方案(用于回答的点): 1.在bean对象中尽量避免定义可变的成员变量(不太现实)。 2.在类中定义一个ThreadLocal成员变量,将需要的可变成员变量保存在ThreadLocal(线程本地化对象)中(推荐的一种方式)。 ThreadLocal解决多线程变量共享问题(参考博客):https://segmentfault.com/a/1190000009236777 Spring中Bean的生命周期: 1.Bean容器找到配置文件中Spring Bean的定义。 2.Bean容器利用Java Reflection API创建一个Bean的实例。 3.如果涉及到一些属性值,利用set方法设置一些属性值。 4.如果Bean实现了BeanNameAware接口,调用setBeanName方法,传入Bean的名字。 5.如果Bean实现了BeanClassLoaderAware接口,调用setBeanClassLoader方法,传入ClassLoader对象的实例。 6.如果Bean实现了BeanFactoryAware接口,调用setBeanClassFacotory方法,传入ClassLoader对象的实例。 7.与上面的类似,如果实现了其他*Aware接口,就调用相应的方法。 8.如果有和加载这个Bean的Spring容器相关的BeanPostProcessor对象,执postProcessBeforeInitialization方法。 9.如果Bean实现了InitializingBean接口,执行afeterPropertiesSet方法。 10.如果Bean在配置文件中的定义包含init-method属性,执行指定的方法。 11.如果有和加载这个Bean的Spring容器相关的BeanPostProcess对象,执行postProcessAfterInitialization方法。 12.当要销毁Bean的时候,如果Bean实现了DisposableBean接口,执行destroy方法。 13.当要销毁Bean的时候,如果Bean在配置文件中的定义包含destroy-method属性,执行指定的方法。 Spring框架中用到了哪些设计模式? 1.工厂设计模式:Spring使用工厂模式通过BeanFactory和ApplicationContext创建bean对象。 2.代理设计模式:Spring AOP功能的实现。 3.单例设计模式:Spring中的bean默认都是单例的。 4.模板方法模式:Spring中的jdbcTemplate、hibernateTemplate等以Template结尾的对数据库操作的类,它们就使用到了模板模式。 5.包装器设计模式:我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。 6.观察者模式:Spring事件驱动模型就是观察者模式很经典的一个应用。 7.适配器模式:Spring AOP的增强或通知(Advice)使用到了适配器模式、Spring MVC中也是用到了适配器模式适配Controller。 还有很多。。。。。。。 @Component和@Bean的区别是什么 1.作用对象不同。@Component注解作用于类,而@Bean注解作用于方法。 2.@Component注解通常是通过类路径扫描来自动侦测以及自动装配到Spring容器中(我们可以使用@ComponentScan注解定义要扫描的路径)。@Bean注解通常是在标有该注解的方法中定义产生这个bean,告诉Spring这是某个类的实例,当我需要用它的时候还给我。 3.@Bean注解比@Component注解的自定义性更强,而且很多地方只能通过@Bean注解来注册bean。比如当引用第三方库的类需要装配到Spring容器的时候,就只能通过@Bean注解来实现。 @Configuration public class AppConfig { @Bean public TransferService transferService { return new TransferServiceImpl; } } <beans> <bean id="transferService" class="com.kk.TransferServiceImpl"/> </beans> @Bean public OneService getService(status) { case (status) { when 1: return new serviceImpl1; when 2: return new serviceImpl2; when 3: return new serviceImpl3; } } 将一个类声明为Spring的bean的注解有哪些? 声明bean的注解: @Component 组件,没有明确的角色 @Service 在业务逻辑层使用(service层) @Repository 在数据访问层使用(dao层) @Controller 在展现层使用,控制器的声明 注入bean的注解: @Autowired:由Spring提供 @Inject:由JSR-330提供 @Resource:由JSR-250提供 *扩:JSR 是 java 规范标准 Spring事务管理的方式有几种? 1.编程式事务:在代码中硬编码(不推荐使用)。 2.声明式事务:在配置文件中配置(推荐使用),分为基于XML的声明式事务和基于注解的声明式事务。 Spring事务中的隔离级别有哪几种? 在TransactionDefinition接口中定义了五个表示隔离级别的常量:ISOLATION_DEFAULT:使用后端数据库默认的隔离级别,Mysql默认采用的REPEATABLE_READ隔离级别;Oracle默认采用的READ_COMMITTED隔离级别。ISOLATION_READ_UNCOMMITTED:最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。ISOLATION_READ_COMMITTED:允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生ISOLATION_REPEATABLE_READ:对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。ISOLATION_SERIALIZABLE:最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。 Spring事务中有哪几种事务传播行为? 在TransactionDefinition接口中定义了八个表示事务传播行为的常量。 支持当前事务的情况:PROPAGATION_REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。PROPAGATION_SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。PROPAGATION_MANDATORY: 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)。 不支持当前事务的情况:PROPAGATION_REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当前事务挂起。PROPAGATION_NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。PROPAGATION_NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常。 其他情况:PROPAGATION_NESTED: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于PROPAGATION_REQUIRED。 二、SpringMVC篇 什么是Spring MVC ?简单介绍下你对springMVC的理解? Spring MVC是一个基于Java的实现了MVC设计模式的请求驱动类型的轻量级Web框架,通过把Model,View,Controller分离,将web层进行职责解耦,把复杂的web应用分成逻辑清晰的几部分,简化开发,减少出错,方便组内开发人员之间的配合。 Spring MVC的工作原理了解嘛? image.png Springmvc的优点: (1)可以支持各种视图技术,而不仅仅局限于JSP; (2)与Spring框架集成(如IoC容器、AOP等); (3)清晰的角色分配:前端控制器(dispatcherServlet) , 请求到处理器映射(handlerMapping), 处理器适配器(HandlerAdapter), 视图解析器(ViewResolver)。 (4) 支持各种请求资源的映射策略。 Spring MVC的主要组件? (1)前端控制器 DispatcherServlet(不需要程序员开发) 作用:接收请求、响应结果,相当于转发器,有了DispatcherServlet 就减少了其它组件之间的耦合度。 (2)处理器映射器HandlerMapping(不需要程序员开发) 作用:根据请求的URL来查找Handler (3)处理器适配器HandlerAdapter 注意:在编写Handler的时候要按照HandlerAdapter要求的规则去编写,这样适配器HandlerAdapter才可以正确的去执行Handler。 (4)处理器Handler(需要程序员开发) (5)视图解析器 ViewResolver(不需要程序员开发) 作用:进行视图的解析,根据视图逻辑名解析成真正的视图(view) (6)视图View(需要程序员开发jsp) View是一个接口, 它的实现类支持不同的视图类型(jsp,freemarker,pdf等等) springMVC和struts2的区别有哪些? (1)springmvc的入口是一个servlet即前端控制器(DispatchServlet),而struts2入口是一个filter过虑器(StrutsPrepareAndExecuteFilter)。 (2)springmvc是基于方法开发(一个url对应一个方法),请求参数传递到方法的形参,可以设计为单例或多例(建议单例),struts2是基于类开发,传递参数是通过类的属性,只能设计为多例。 (3)Struts采用值栈存储请求和响应的数据,通过OGNL存取数据,springmvc通过参数解析器是将request请求内容解析,并给方法形参赋值,将数据和视图封装成ModelAndView对象,最后又将ModelAndView中的模型数据通过reques域传输到页面。Jsp视图解析器默认使用jstl。 SpringMVC怎么样设定重定向和转发的? (1)转发:在返回值前面加"forward:",譬如"forward:user.do?name=method4" (2)重定向:在返回值前面加"redirect:",譬如"redirect:http://www.baidu.com" SpringMvc怎么和AJAX相互调用的? 通过Jackson框架就可以把Java里面的对象直接转化成Js可以识别的Json对象。具体步骤如下 : (1)加入Jackson.jar (2)在配置文件中配置json的映射 (3)在接受Ajax方法里面可以直接返回Object,List等,但方法前面要加上@ResponseBody注解。 如何解决POST请求中文乱码问题,GET的又如何处理呢? (1)解决post请求乱码问题: 在web.xml中配置一个CharacterEncodingFilter过滤器,设置成utf-8; <filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>utf-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> (2)get请求中文参数出现乱码解决方法有两个: ①修改tomcat配置文件添加编码与工程编码一致,如下: <ConnectorURIEncoding="utf-8" connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443"/> ②另外一种方法对参数进行重新编码: String userName = new String(request.getParamter("userName").getBytes("ISO8859-1"),"utf-8") ISO8859-1是tomcat默认编码,需要将tomcat编码后的内容按utf-8编码。 Spring MVC的异常处理 ? 统一异常处理: Spring MVC处理异常有3种方式: (1)使用Spring MVC提供的简单异常处理器SimpleMappingExceptionResolver; (2)实现Spring的异常处理接口HandlerExceptionResolver 自定义自己的异常处理器; (3)使用@ExceptionHandler注解实现异常处理; 统一异常处理的博客:https://blog.csdn.net/ctwy291314/article/details/81983103 SpringMVC的控制器是不是单例模式,如果是,有什么问题,怎么解决? 是单例模式,所以在多线程访问的时候有线程安全问题,不要用同步,会影响性能的,解决方案是在控制器里面不能写成员变量。(此题目类似于上面Spring 中 第5题 有两种解决方案) SpringMVC常用的注解有哪些? @RequestMapping:用于处理请求 url 映射的注解,可用于类或方法上。用于类上,则表示类中的所有响应请求的方法都是以该地址作为父路径。 @RequestBody:注解实现接收http请求的json数据,将json转换为java对象。 @ResponseBody:注解实现将conreoller方法返回对象转化为json对象响应给客户。 SpingMvc中的控制器的注解一般用那个,有没有别的注解可以替代? 一般用@Controller注解,也可以使用@RestController,@RestController注解相当于@ResponseBody + @Controller,表示是表现层,除此之外,一般不用别的注解代替。 如果在拦截请求中,我想拦截get方式提交的方法,怎么配置? 可以在@RequestMapping注解里面加上method=RequestMethod.GET。 怎样在方法里面得到Request,或者Session? 直接在方法的形参中声明request,SpringMVC就自动把request对象传入。 如果想在拦截的方法里面得到从前台传入的参数,怎么得到? 直接在形参里面声明这个参数就可以,但必须名字和传过来的参数一样。 如果前台有很多个参数传入,并且这些参数都是一个对象的,那么怎么样快速得到这个对象? 直接在方法中声明这个对象,SpringMVC就自动会把属性赋值到这个对象里面。 SpringMVC中函数的返回值是什么? 返回值可以有很多类型,有String, ModelAndView。ModelAndView类把视图和数据都合并的一起的。 SpringMVC用什么对象从后台向前台传递数据的? 通过ModelMap对象,可以在这个对象里面调用put方法,把对象加到里面,前台就可以拿到数据。 怎么样把ModelMap里面的数据放入Session里面? 可以在类上面加上@SessionAttributes注解,里面包含的字符串就是要放入session里面的key。 SpringMvc里面拦截器是怎么写的: 有两种写法,一种是实现HandlerInterceptor接口,另外一种是继承适配器类,接着在接口方法当中,实现处理逻辑;然后在SpringMvc的配置文件中配置拦截器即可: <!-- 配置SpringMvc的拦截器 --> <mvc:interceptors> <!-- 配置一个拦截器的Bean就可以了 默认是对所有请求都拦截 --> <bean id="myInterceptor" class="com.zwp.action.MyHandlerInterceptor"></bean> <!-- 只针对部分请求拦截 --> <mvc:interceptor> <mvc:mapping path="/modelMap.do" /> <bean class="com.zwp.action.MyHandlerInterceptorAdapter" /> </mvc:interceptor> </mvc:interceptors> 注解原理: 注解本质是一个继承了Annotation的特殊接口,其具体实现类是Java运行时生成的动态代理类。我们通过反射获取注解时,返回的是Java运行时生成的动态代理对象。通过代理对象调用自定义注解的方法,会最终调用AnnotationInvocationHandler的invoke方法。该方法会从memberValues这个Map中索引出对应的值。而memberValues的来源是Java常量池 三、Mybatis篇 什么是MyBatis? MyBatis是一个可以自定义SQL、存储过程和高级映射的持久层框架。 讲下MyBatis的缓存 MyBatis的缓存分为一级缓存和二级缓存,一级缓存放在session里面,默认就有, 二级缓存放在它的命名空间里,默认是不打开的,使用二级缓存属性类需要实现Serializable序列化接口, 可在它的映射文件中配置<cache/> Mybatis是如何进行分页的?分页插件的原理是什么? 1)Mybatis使用RowBounds对象进行分页,也可以直接编写sql实现分页,也可以使用Mybatis的分页插件。 2)分页插件的原理:实现Mybatis提供的接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql。 举例:select * from student,拦截sql后重写为:select t.* from (select * from student)t limit 0,10 简述Mybatis的插件运行原理,以及如何编写一个插件? 1)Mybatis仅可以编写针对ParameterHandler、ResultSetHandler、StatementHandler、 Executor这4种接口的插件,Mybatis通过动态代理, 为需要拦截的接口生成代理对象以实现接口方法拦截功能, 每当执行这4种接口对象的方法时,就会进入拦截方法, 具体就是InvocationHandler的invoke方法,当然, 只会拦截那些你指定需要拦截的方法。 2)实现Mybatis的Interceptor接口并复写intercept方法, 然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可, 记住,别忘了在配置文件中配置你编写的插件。 Mybatis动态sql是做什么的?都有哪些动态sql?能简述一下动态sql的执行原理不? 1)Mybatis动态sql可以让我们在Xml映射文件内, 以标签的形式编写动态sql,完成逻辑判断和动态拼接sql的功能。 2)Mybatis提供了9种动态sql标签:trim|where|set|foreach|if|choose|when|otherwise|bind。 3)其执行原理为,使用OGNL从sql参数对象中计算表达式的值, 根据表达式的值动态拼接sql,以此来完成动态sql的功能。 #{}和${}的区别是什么? 1)#{}是预编译处理,${}是字符串替换。 2)Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值(有效的防止SQL注入); 3)Mybatis在处理${}时,就是把${}替换成变量的值。 为什么说Mybatis是半自动ORM映射工具?它与全自动的区别在哪里? Hibernate属于全自动ORM映射工具, 使用Hibernate查询关联对象或者关联集合对象时, 可以根据对象关系模型直接获取,所以它是全自动的。 而Mybatis在查询关联对象或关联集合对象时, 需要手动编写sql来完成,所以,称之为半自动ORM映射工具。 Mybatis是否支持延迟加载?如果支持,它的实现原理是什么? 1)Mybatis仅支持association关联对象和collection关联集合对象的延迟加载, association指的就是一对一,collection指的就是一对多查询。 在Mybatis配置文件中, 可以配置是否启用延迟加载lazyLoadingEnabled=true|false。 2)它的原理是,使用CGLIB创建目标对象的代理对象, 当调用目标方法时,进入拦截器方法, 比如调用a.getB.getName, 拦截器invoke方法发现a.getB是null值, 那么就会单独发送事先保存好的查询关联B对象的sql, 把B查询上来,然后调用a.setB(b), 于是a的对象b属性就有值了, 接着完成a.getB.getName方法的调用。 这就是延迟加载的基本原理。 MyBatis与Hibernate有哪些不同? 1)Mybatis和hibernate不同,它不完全是一个ORM框架, 因为MyBatis需要程序员自己编写Sql语句, 不过mybatis可以通过XML或注解方式灵活配置要运行的sql语句, 并将java对象和sql语句映射生成最终执行的sql, 最后将sql执行的结果再映射生成java对象。 2)Mybatis学习门槛低,简单易学,程序员直接编写原生态sql, 可严格控制sql执行性能,灵活度高,非常适合对关系数据模型要求不高的软件开发, 例如互联网软件、企业运营类软件等,因为这类软件需求变化频繁, 一但需求变化要求成果输出迅速。但是灵活的前提是mybatis无法做到数据库无关性, 如果需要实现支持多种数据库的软件则需要自定义多套sql映射文件,工作量大。 3)Hibernate对象/关系映射能力强,数据库无关性好, 对于关系模型要求高的软件(例如需求固定的定制化软件) 如果用hibernate开发可以节省很多代码,提高效率。 但是Hibernate的缺点是学习门槛高,要精通门槛更高, 而且怎么设计O/R映射,在性能和对象模型之间如何权衡, 以及怎样用好Hibernate需要具有很强的经验和能力才行。 总之,按照用户的需求在有限的资源环境下只要能做出维护性、 扩展性良好的软件架构都是好架构,所以框架只有适合才是最好。 MyBatis的好处是什么? 1)MyBatis把sql语句从Java源程序中独立出来,放在单独的XML文件中编写, 给程序的维护带来了很大便利。 2)MyBatis封装了底层JDBC API的调用细节,并能自动将结果集转换成Java Bean对象, 大大简化了Java数据库编程的重复工作。 3)因为MyBatis需要程序员自己去编写sql语句, 程序员可以结合数据库自身的特点灵活控制sql语句, 因此能够实现比Hibernate等全自动orm框架更高的查询效率,能够完成复杂查询。 简述Mybatis的Xml映射文件和Mybatis内部数据结构之间的映射关系? Mybatis将所有Xml配置信息都封装到All-In-One重量级对象Configuration内部。 在Xml映射文件中,<parameterMap>标签会被解析为ParameterMap对象, 其每个子元素会被解析为ParameterMapping对象。 <resultMap>标签会被解析为ResultMap对象, 其每个子元素会被解析为ResultMapping对象。 每一个<select>、<insert>、<update>、<delete> 标签均会被解析为MappedStatement对象, 标签内的sql会被解析为BoundSql对象。 什么是MyBatis的接口绑定,有什么好处? 接口映射就是在MyBatis中任意定义接口,然后把接口里面的方法和SQL语句绑定, 我们直接调用接口方法就可以,这样比起原来了SqlSession提供的方法我们可以有更加灵活的选择和设置. 接口绑定有几种实现方式,分别是怎么实现的? 接口绑定有两种实现方式,一种是通过注解绑定,就是在接口的方法上面加 上@Select@Update等注解里面包含Sql语句来绑定, 另外一种就是通过xml里面写SQL来绑定,在这种情况下, 要指定xml映射文件里面的namespace必须为接口的全路径名. 什么情况下用注解绑定,什么情况下用xml绑定? 当Sql语句比较简单时候,用注解绑定;当SQL语句比较复杂时候,用xml绑定,一般用xml绑定的比较多 MyBatis实现一对一有几种方式?具体怎么操作的? 有联合查询和嵌套查询,联合查询是几个表联合查询,只查询一次, 通过在resultMap里面配置association节点配置一对一的类就可以完成; 嵌套查询是先查一个表,根据这个表里面的结果的外键id, 去再另外一个表里面查询数据,也是通过association配置, 但另外一个表的查询通过select属性配置。 Mybatis能执行一对一、一对多的关联查询吗?都有哪些实现方式,以及它们之间的区别? 能,Mybatis不仅可以执行一对一、一对多的关联查询, 还可以执行多对一,多对多的关联查询,多对一查询, 其实就是一对一查询,只需要把selectOne修改为selectList即可; 多对多查询,其实就是一对多查询,只需要把selectOne修改为selectList即可。 关联对象查询,有两种实现方式,一种是单独发送一个sql去查询关联对象, 赋给主对象,然后返回主对象。另一种是使用嵌套查询,嵌套查询的含义为使用join查询, 一部分列是A对象的属性值,另外一部分列是关联对象B的属性值, 好处是只发一个sql查询,就可以把主对象和其关联对象查出来。 MyBatis里面的动态Sql是怎么设定的?用什么语法? MyBatis里面的动态Sql一般是通过if节点来实现,通过OGNL语法来实现, 但是如果要写的完整,必须配合where,trim节点,where节点是判断包含节点有 内容就插入where,否则不插入,trim节点是用来判断如果动态语句是以and 或or 开始,那么会自动把这个and或者or取掉。 Mybatis是如何将sql执行结果封装为目标对象并返回的?都有哪些映射形式? 第一种是使用<resultMap>标签,逐一定义列名和对象属性名之间的映射关系。 第二种是使用sql列的别名功能,将列别名书写为对象属性名, 比如T_NAME AS NAME,对象属性名一般是name,小写, 但是列名不区分大小写,Mybatis会忽略列名大小写,
-
包婷婷 (201550484)作业一 统计软件简介与数据操作-SPSS(Statistical Product and Service Solutions),"统计产品与服务解决方案"软件。最初软件全称为"(SolutionsStatistical Package for the Social Sciences),但是随着SPSS产品服务领域的扩大和服务深度的增加,SPSS公司已于2000年正式将英文全称更改为"统计产品与服务解决方案",标志着SPSS的战略方向正在做出重大调整。为IBM公司推出的一系列用于统计学分析运算、数据挖掘、预测分析和决策支持任务的软件产品及相关服务的总称SPSS,有Windows和Mac OS X等版本。 1984年SPSS总部首先推出了世界上第一个统计分析软件微机版本SPSS/PC+,开创了SPSS微机系列产品的开发方向,极大地扩充了它的应用范围,并使其能很快地应用于自然科学、技术科学、社会科学的各个领域。世界上许多有影响的报刊杂志纷纷就SPSS的自动统计绘图、数据的深入分析、使用方便、功能齐全等方面给予了高度的评价。 R统计软件介绍 R是一套完整的数据处理、计算和制图软件系统。其功能包括:数据存储和处理系统;数组运算工具(其向量、矩阵运算方面功能尤其强大);完整连贯的统计分析工具;优秀的统计制图功能;简便而强大的编程语言:可操纵数据的输入和输出,可实现分支、循环,用户可自定义功能。 与其说R是一种统计软件,还不如说R是一种数学计算的环境,因为R并不是仅仅提供若干统计程序、使用者只需指定数据库和若干参数便可进行一个统计分析。R的思想是:它可以提供一些集成的统计工具,但更大量的是它提供各种数学计算、统计计算的函数,从而使使用者能灵活机动的进行数据分析,甚至创造出符合需要的新的统计计算方法。 该语言的语法表面上类似 C,但在语义上是函数设计语言(functional programming language)的变种并且和Lisp 以及 APL有很强的兼容性。特别的是,它允许在"语言上计算"(computing on the language)。这使得它可以把表达式作为函数的输入参数,而这种做法对统计模拟和绘图非常有用。 R是一个免费的*软件,它有UNIX、LINUX、MacOS和WINDOWS版本,都是可以免费下载和使用的。在R主页那儿可以下载到R的安装程序、各种外挂程序和文档。在R的安装程序中只包含了8个基础模块,其他外在模块可以通过CRAN获得。 二、R语言 R是用于统计分析、绘图的语言和操作环境。R是属于GNU系统的一个*、免费、源代码开放的软件,它是一个用于统计计算和统计制图的优秀工具。 R作为一种统计分析软件,是集统计分析与图形显示于一体的。它可以运行于UNIX,Windows和Macintosh的操作系统上,而且嵌入了一个非常方便实用的帮助系统,相比于其他统计分析软件,R还有以下特点: 1.R是*软件。这意味着它是完全免费,开放源代码的。可以在它的网站及其镜像中下载任何有关的安装程序、源代码、程序包及其源代码、文档资料。标准的安装文件身自身就带有许多模块和内嵌统计函数,安装好后可以直接实现许多常用的统计功能。[2] 2.R是一种可编程的语言。作为一个开放的统计编程环境,语法通俗易懂,很容易学会和掌握语言的语法。而且学会之后,我们可以编制自己的函数来扩展现有的语言。这也就是为什么它的更新速度比一般统计软件,如,SPSS,SAS等快得多。大多数最新的统计方法和技术都可以在R中直接得到。[2] 3. 所有R的函数和数据集是保存在程序包里面的。只有当一个包被载入时,它的内容才可以被访问。一些常用、基本的程序包已经被收入了标准安装文件中,随着新的统计分析方法的出现,标准安装文件中所包含的程序包也随着版本的更新而不断变化。在另外版安装文件中,已经包含的程序包有:base一R的基础模块、mle一极大似然估计模块、ts一时间序列分析模块、mva一多元统计分析模块、survival一生存分析模块等等.[2] 4.R具有很强的互动性。除了图形输出是在另外的窗口处,它的输入输出窗口都是在同一个窗口进行的,输入语法中如果出现错误会马上在窗口口中得到提示,对以前输入过的命令有记忆功能,可以随时再现、编辑修改以满足用户的需要。输出的图形可以直接保存为JPG,BMP,PNG等图片格式,还可以直接保存为PDF文件。另外,和其他编程语言和数据库之间有很好的接口。[2] 5.如果加入R的帮助邮件列表一,每天都可能会收到几十份关于R的邮件资讯。可以和全球一流的统计计算方面的专家讨论各种问题,可以说是全世界最大、最前沿的统计学家思维的聚集地.[2] R是基于S语言的一个GNU项目,所以也可以当作S语言的一种实现,通常用S语言编写的代码都可以不作修改的在R环境下运行。 R的语法是来自Scheme。R的使用与S-PLUS有很多类似之处,这两种语言有一定的兼容性。S-PLUS的使用手册,只要稍加修改就可作为R的使用手册。所以有人说:R,是S-PLUS的一个“克隆”。 但是请不要忘了:R是免费的(R is free)。R语言源代码托管在github,具体地址可以看参考资料。[3] 。 R语言的下载可以通过CRAN的镜像来查找。 R语言有域名为.cn的下载地址,有六个,其中两个由Datagurn,由 中国科学技术大学提供的。R语言Windows版,其中由两个下载地点是Datagurn和 USTC提供的。 三、stata Stata 是一套提供其使用者数据分析、数据管理以及绘制专业图表的完整及整合性统计软件。它提供许许多多功能,包含线性混合模型、均衡重复反复及多项式普罗比模式。用Stata绘制的统计图形相当精美。 新版本的STATA采用最具亲和力的窗口接口,使用者自行建立程序时,软件能提供具有直接命令式的语法。Stata提供完整的使用手册,包含统计样本建立、解释、模型与语法、文献等超过一万余页的出版品。 除此之外,Stata软件可以透过网络实时更新每天的最新功能,更可以得知世界各地的使用者对于STATA公司提出的问题与解决之道。使用者也可以透过Stata. Journal获得许许多多的相关讯息以及书籍介绍等。另外一个获取庞大资源的管道就是Statalist,它是一个独立的listserver,每月交替提供使用者超过1000个讯息以及50个程序。 四、PYTHON
-
编写 Python 类的各种技巧和窍门