JavaBeginnersTutorial-中文系列教程-二-
JavaBeginnersTutorial 中文系列教程(二)
原文:JavaBeginnersTutorial
协议:CC BY-NC-SA 4.0
Python 集
原文: https://javabeginnerstutorial.com/python-tutorial/python-3-set-2/
在上一篇文章中,我们讨论了 Python 列表。但是 Python 3 Set
是什么? 集合是唯一项目的无序集合。
无序表示这种类型的集合不允许建立索引,并且您无法像列表或元组那样通过索引访问它们的元素。
唯一表示即使将同一元素多次放入集合中,每个元素在集合中也只有一次。
自然,您可以像使用列表或元组一样将字符串中的类型混合在一起。
创建 Python 集
有一些方法可以创建集合。 基本版本是列出花括号({}
)之间的所有元素。 但是,最常见的用法是当您要从列表中删除重复项时。 在这种情况下,您可以将列表转换为集合,然后再次返回列表。 这将从集合的中间用法中将重复项从列表中删除,您将再次拥有列表。 为此,使用set
函数,该函数需要一个可迭代的参数。
>>> A = {1,2,3,4,5}
>>> A
{1, 2, 3, 4, 5}
>>> B = set((1,2,3,4,5))
>>> B
{1, 2, 3, 4, 5}
>>> C = set([1,2,5,4,3,4,5])
>>> C
{1, 2, 3, 4, 5}
>>> D = set(1,2,3,4,5)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: set expected at most 1 arguments, got 5
>>> D = set(1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'int' object is not iterable
如您所见,您不能将可变数量的参数作为参数或不可迭代的参数添加。
但是! 您可以传递字符串作为 set 函数的参数,因为(您可能还记得),字符串是 Python 中的字符列表。 在这种情况下,字符串将拆分为字符,并且每个字符在集合中仅存储一次。
>>> D = set("Spam! Spam! Spam!")
>>> D
{'p', 'm', ' ', 'a', 'S', '!'}
而且因为我已经提到过,您可以使用集合来过滤列表的重复项,因此这里是示例:
>>> l = [1,2,3,2,1,2,3,4,5,6,5,6,4,7,8,3,2,1,3,4,5,6]
>>> l
[1, 2, 3, 2, 1, 2, 3, 4, 5, 6, 5, 6, 4, 7, 8, 3, 2, 1, 3, 4, 5, 6]
>>> l = list(set(l))
>>> l
[1, 2, 3, 4, 5, 6, 7, 8]
此示例首先将l
转换为一个集合,该集合将删除所有重复的元素,然后将集合折回为允许进行索引的列表。 我在进行网站抓取并亲自编写抓取逻辑时使用了这种类型的过滤。
更改 Python 集
集是可变的(不是一成不变的),因此您可以更改它们的元素。 更改意味着添加和删除元素。 由于集合不支持索引,因此无法像列表一样更改给定索引的元素。 试想一下,如果您可以更改一组中的一个元素,将会发生什么? 必须每次在后台运行某种机制来过滤出可能的重复元素。 那将是无稽之谈,并且会使使用set
非常缓慢。
这意味着我们仅需添加和删除元素。 与列表不同,您不能使用加法运算符(加号+
)来扩展集合。 您必须使用add()
或update()
。 这两种方法的区别在于参数的数量和类型。
>>> A = {1}
>>> A
{1}
>>> A.add(2)
>>> A
{1, 2}
>>> A.add({3})
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'set'
>>> A.add((3))
>>> A
{1, 2, 3}
>>> A.add((3,4))
>>> A
{1, 2, 3, (3, 4)}
>>> A.add(5,6)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: add() takes exactly one argument (2 given)
>>> A.add([5,6],(7))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: add() takes exactly one argument (2 given)
>>> A.update([5,6],(7))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'int' object is not iterable
>>> A.update([5,6],(7,))
>>> A
{1, 2, 3, 5, 6, 7, (3, 4)}
>>> A.update([1,2,3,4])
>>> A
{1, 2, 3, 4, 5, 6, 7, (3, 4)}
如上例所示,您需要为add
函数提供一个不可变的元素(就像创建集合时一样)。 update
函数采用多个参数,这些参数必须是集合。 这些集合是不可变的,也不必只是必须是集合。 在所有情况下,都避免重复。
移除元素
有时您需要根据各种条件删除元素。 在这种情况下,您有一些方法在每种情况下的行为都会略有不同。
>>> A = {1, 2, 3, 4, 5, 6, 7, 8, 9, (3, 4)}
>>> A
{1, 2, 3, 4, 5, 6, 7, 8, 9, (3, 4)}
>>> A.remove((3,4))
>>> A
{1, 2, 3, 4, 5, 6, 7, 8, 9}
>>> A.remove(3,4)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: remove() takes exactly one argument (2 given)
>>> A.remove(5)
>>> A
{1, 2, 3, 4, 6, 7, 8, 9}
>>> A.remove(5)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 5
leanpub-start-insert
>>> A.discard(5)
leanpub-end-insert
>>> n = A.pop()
>>> n
1
>>> A
{2, 3, 4, 6, 7, 8, 9}
>>> A.clear()
>>> A
set()
>>> A.pop()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'pop from an empty set'
如您所见,remove
函数仅使用一个参数,而不是更多。 即使使用A.remove((3,4))
,我们也只提供一个元素—元组。 如果要添加多个元素,则会出现TypeError
。 如果该键不存在于集合中,则在尝试使用remove
摆脱键时会出现KeyError
。 但是,如果给定键不在集合中,则丢弃不会产生任何噪音。 它默默地发出有关该应用的通知,该应用不知道集合的状态,因此不再打扰。
pop
函数(就像我们已经知道的那样)采用集合中的一个元素,将其删除并返回。 如果集合为空,则将再次收到KeyError
。 似乎在使用pop
时总是返回第一个元素,但是您不知道集合中元素的顺序。 实现可能会改变它们存储元素的方式并记住:如果您自己运行应用,则不会看到集合中的元素。
冻结集
冻结集是一种特殊的集,它具有与普通集相同的属性,但是您不能更改其元素。 这意味着,一旦创建了冻结集,就必须遵守分配给它的值(自然地,重新分配变量始终是一种解决方案)。
我们来看一些示例。
>>> A = frozenset([1,2,3,4,5])
>>> B = {2,4,6,8,10}
>>> A | B
frozenset({1, 2, 3, 4, 5, 6, 8, 10})
>>> A & B
frozenset({2, 4})
>>> A.add(6)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'frozenset' object has no attribute 'add'
>>> A += {6}
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +=: 'frozenset' and 'set'
>>> A += frozenset({6})
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +=: 'frozenset' and 'frozenset'
如您在上面的示例中看到的那样,没有方法可以将新元素添加到冻结集中。 删除或更改元素也是如此。 好吧,frozenset
被冻结了,仅供使用。
参考文献
- 官方文档
Python 字典
原文: https://javabeginnerstutorial.com/python-tutorial/python3-dictionary-2/
在上一篇文章中,我们讨论了 Python 列表。 在这里,我将向您详细介绍 Python 字典。
字典是 Python 中的键值存储。 由于像哈希表这样的实现,并且键必须唯一,因此它们使对元素的快速访问成为可能。 键和值之间用冒号(:
)分隔,整个字典在花括号({}
)之间定义。
创建字典和访问元素
将创建一个空的字典,其中包含大括号和右括号:
>>> d = {}
>>> type(d)
<class 'dict'>
要拥有带有值的字典,我们必须在这些花括号之间添加键-值对。 键具有与set
元素相同的限制:它们必须是可哈希的,因此不允许使用列表,字典,集合,而只能使用不可变的值,例如数字,字符串,布尔值,元组和Frozensets
。
>>> d = {'name':'Gabor', 'age':31}
>>> d
{'name': 'Gabor', 'age': 31}
自然,您可以像真正的字典一样使用字典变量。 例如,您可以创建英语-德语词典来“翻译”一些单词。
>>> en_de = {'eggs':'Eier', 'sausage':'Würstchen','bacon':'Schinken', 'spam':'Spam'}
>>> en_de
{'bacon': 'Schinken', 'sausage': 'Würstchen', 'eggs': 'Eier', 'spam': 'Spam'}
>>> en_de['eggs']
'Eier'
>>> en_de['baked beans']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'baked beans'
>>> en_de.get('baked beans','unknown')
'unknown'
如您在上面看到的,访问元素可以通过它们的键来完成(例如示例中的en_de['eggs']
),但是,如果 python 词典中不存在键,则会出现KeyError
。
自然,可以通过使用get
方法来避免KeyError
,该方法接受可选的默认值。 如果未提供可选的默认值,则如果字典中不存在该键,则返回None
:
>>> bb = en_de.get('baked beans')
>>> print(bb)
None
如果您有两个列表,则可能需要将它们合并为对列表(最终是键值对)。 您可以使用zip
函数执行此操作。 之后,您可以将此压缩列表转换为字典,其中每对的第一个元素将是键,而对的第二个元素将是值。
>>> food = ['eggs', 'sausage','bacon','spam']
>>> preferences = ['yes','no','yes','no']
>>> food_preferences = dict(zip(food, preferences))
>>> food_preferences
{'bacon': 'yes', 'sausage': 'no', 'eggs': 'yes', 'spam': 'no'}
正如您在上面看到的那样,这是将两个列表转换成字典的一种精致且 Pythonic 的方式。
添加和删除元素
当然,您可以在字典中添加元素或从中删除元素。 方法与我们从列表中学到的方法完全相同,但让我们通过示例进行查看。
>>> d = {'eggs':2}
>>>
>>> d
{'eggs': 2}
>>> d['bacon'] = 1
>>> d
{'bacon': 1, 'eggs': 2}
>>> d.update({'spam':0})
>>> d
{'bacon': 1, 'eggs': 2, 'spam': 0}
>>> d.update([('spam',1)])
>>> d
{'bacon': 1, 'eggs': 2, 'spam': 1}
update
函数将字典作为参数或键值对列表。 元组是这些键值对的理想候选者。
要从字典中删除元素,可以使用众所周知的del
语句,并提供键和字典名称。 自然,如果字典中不存在该键,则将得到一个KeyError
。
pop
函数将删除具有给定键的元素,并返回与此键关联的值。 如果键不存在,您将收到KeyError
。 要解决此问题,您可以将pop
函数传递给默认值,当给定键不在字典中时会返回该默认值。
popitem
函数从字典中删除一个元素,然后将已删除的(键,值)对作为元组返回。 在这里,您不知道返回哪个元素。 如果字典为空,则您将收到KeyError
。
>>> food_preferences = {'bacon': 'yes', 'sausage': 'no', 'eggs': 'yes', 'spam': 'no'}
>>> del food_preferences['spam']
>>> food_preferences
{'bacon': 'yes', 'sausage': 'no', 'eggs': 'yes'}
>>> del food_preferences['spam']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'spam'
>>> food_preferences.pop('spam')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'spam'
>>> food_preferences.pop('eggs')
'yes'
>>> food_preferences
{'bacon': 'yes', 'sausage': 'no'}
>>> food_preferences.pop('spam','no')
'no'
>>> food_preferences.popitem()
('bacon', 'yes')
>>> food_preferences
{'sausage': 'no'}
>>> food_preferences.clear()
>>> food_preferences
{}
>>> food_preferences.popitem()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'popitem(): dictionary is empty'
字典中有特定的键吗?
有时,您只是想避免使用get
函数,因为如果请求的键不在字典中,则不需要任何默认值。 在这种情况下,可以在带有字典键的语句中使用它们。
>>> d = {'eggs': 1, 'sausage': 2, 'bacon': 1}
>>> d
{'bacon': 1, 'sausage': 2, 'eggs': 1}
>>> 'spam' in d
False
>>> 'eggs' in d
True
>>> 'Eggs' in d
False
如您在上面的示例中看到的,按键是按键敏感的。 这意味着字典可以包含键“垃圾邮件”,“垃圾邮件”和“垃圾邮件”,并且它们全部将引用不同的条目。
字典的键和值
有时,您仅需要信息字典中存在哪些键,或者您想知道其中存在哪些值,或者仅当键存在时才需要这些值。
在这种情况下,可以使用字典的键或值方法。 您可能会认为它们会给您带回字典中类似键或值的集合的对象。 让我们来看一个示例。
>>> d = {'first_name': 'Gabor', 'age':31, 'twitter':'@GHajba'}
>>> d
{'first_name': 'Gabor', 'age': 31, 'twitter': '@GHajba'}
>>> d.keys()
dict_keys(['first_name', 'age', 'twitter'])
>>> d.values()
dict_values(['Gabor', 31, '@GHajba'])
这些类似集合的对象不可索引,但是您可以稍后在循环中使用它们。 当您想查看字典中是否有值时,dict_values
对象很有用。
>>> d = {'first_name': 'Gabor', 'age':31, 'twitter':'@GHajba'}
>>> "Gabor" in d.values()
True
>>> "twitter" in d.values()
False
自然地,您可以使用list
函数并将这些类似于字典set
的对象转换为list
(或者使用set
关键字来设置set
或使用tuple
函数,您可以创建一个包含所有项目的元组-希望您能理解其中的内容) 。
>>> list(d.keys())
['first_name', 'age', 'twitter']
>>> tuple(d.values())
('Gabor', 31, '@GHajba')
>>> set(d.items())
{('first_name', 'Gabor'), ('age', 31), ('twitter', '@GHajba')}
如您在上面的示例中使用字典的 items 函数所看到的那样,您将获得一个成对的列表,其中第一个元素是键,第二个元素是值。
参考文献
- 字典
Python 条件语句
原文: https://javabeginnerstutorial.com/python-tutorial/python3-conditional-statements-2/
条件决策
在介绍了有关 Python 3 中变量的基本类型的大量介绍性文章之后,现在该继续介绍条件运算符了,这些条件运算符可帮助我们在运行时基于不同条件创建应用流。
到目前为止,我们只有一个流程:启动应用,(可能)需要用户输入并显示一个结果。 通过条件更改。 根据输入或运行时创建的其他参数(您知道,有时不需要输入),我们可以输出不同的结果。
我们来看一个简单的“游戏” FizzBuzz 示例。
游戏的描述非常简单:如果一个数字可以被 3 整除,那么我们将显示“Fizz
”;当一个数字可以被 5 整除时,我们将显示“Buzz
”;当一个数字被 3 和 5 整除时,我们将显示“FizzBuzz
”。 ; 否则,我们显示数字。
让我们将以下代码保存到名为fizzbuzz.py
的文件中:
n = int(input("Enter a number: "))
if n%15 == 0:
print("FizzBuzz")
elif n%3 == 0:
print("Fizz")
elif n%5 == 0:
print("Buzz")
else:
print(n)
如您所见,此应用现在受到了一定的限制,因为它在启动时仅执行一次,但是在下一篇文章中,我将介绍循环,然后我们将变得势不可挡,并且可以编写可以运行并运行的应用……希望您能理解这个想法。
如果我运行该应用,一些示例输出将如下所示:
MB:examples GHajba$ python3 fizzbuzz.py
Enter a number: 5
Buzz
MB:examples GHajba$ python3 fizzbuzz.py
Enter a number: 6
Fizz
MB:examples GHajba$ python3 fizzbuzz.py
Enter a number: 2
2
MB:examples GHajba$ python3 fizzbuzz.py
Enter a number: 45
FizzBuzz
关于条件
条件句根据某些条件改变应用流程。 在大多数情况下,这是基于某些变量的值,有时是基于输入。 这一切都取决于您的应用的设计方式。
一些开发人员说,在应用流中没有分支的情况下编写应用很困难。 好吧,我们已经看到了可能,但是这以一种不好的方式降低了用户体验。 最好基于会改变编程状态的条件创建分支。
上面显示的“FizzBuzz”示例是一个不错的脚本,它显示了通过分支我们可以在编码时做很多不同。
但是,以上示例涵盖了 Python 中if-then-else
构造的相同程序流。 if
语句需要一个表达式,其值必须为True
或False
。 如果表达式的计算结果为True
,则执行此语句后的代码块。 如果该语句的求值结果为False
,则跳过此块。
elif
语句再次要求一个求值结果为True
或False
的语句,并且仅在前一个语句被求值为False
时才被求值。 否则,甚至不会求值该块。
如果先前的if
块和所有先前的elif
块的求值为False
,则else
块执行。 这里不需要任何语句进行求值。 如果所有以前的表达式求值为False
,则else
块是默认情况。
条件块的基本构造如下:
- 一个
if
块 - 零到任何
elif
块 - 零或一个
else
块
这意味着if
块必须始终存在。 但是,此if
块不必后面带有任何elif
或else
语句。 但是,如果没有if
块,则不能单独拥有elif
或else
块。
if conditional_expression_1:
statement_block_1
elif conditional_expression_2:
statement_block_2
else:
statement_block_3
而且,如果您有多个if
语句,则将同时求值它们。 这意味着对于“FizzBuzz”示例,您不能使用简单的if
块。 这是因为在这种情况下,每个if
语句都会被求值,并且会导致您无法预期的结果。
n = int(input("Enter a number: "))
if n%15 == 0:
print("FizzBuzz")
if n%3 == 0:
print("Fizz")
if n%5 == 0:
print("Buzz")
else:
print(n)
MB:examples GHajba$ python fizzbuzz_wrong.py
Enter a number: 15
FizzBuzz
Fizz
Buzz
MB:examples GHajba$ python fizzbuzz_wrong.py
Enter a number: 3
Fizz
3
上面的代码示例演示了仅当前一个if
块的值为False
时,才对每个if
块求值,并且执行最后的else
。
生活不仅是对还是错
不幸的是有人说相反,因为布尔条件不仅是对还是错。 使用 Python 时,在每种情况下 0 的求值结果均为False
,其他所有数字均求值为True
。
集合也是如此(当然也包括字符串)。 如果集合为空,则它在条件语句中求值为False
,在任何其他情况下为True
。
没有一个总是求值为False
。
这就是为什么有人说很难用 Python 编写条件语句的原因。 但这始终取决于开发人员的口味。 “Python 禅宗”指出
显式胜于隐式。
但是,在这种情况下,隐式有时甚至更好。 这意味着您可以避免类似以下的构造:
if len(l) == 0:
# do something
if a == 0 or a is None:
# do something else
并用这些替换它们:
if not l:
# do something
if not a:
# do something else
Python 循环
原文: https://javabeginnerstutorial.com/python-tutorial/python3-loops-2/
Python 循环允许循环遍历程序代码并重复代码块,直到满足或不满足给定条件。 重复的代码称为循环的主体。
Python 有两种循环类型:for
循环和while
循环。
循环中的变量会在每次执行循环主体时更改其值,这些变量必须用作循环头部或主体中的条件以中断流程。
如果循环中没有终止条件,则可能会遇到无限循环或永无止境的循环,永不终止,您必须手动终止应用。 这些循环大多数是通过while
构造实现的。 让我向您展示示例:
while True:
print('I love Python!')
i = 0
while i < 1:
print('I love Python!')
在第一个示例中,循环的开头有一个条件,条件始终为True
,因此程序将永远将“我爱 Python”打印到控制台。 此循环永远不会终止。
第二个循环比较棘手,因为我在循环的开头使用了一个变量。 如果i
的值一次大于 1,则该循环最终将终止。 但是,由于循环的主体只会在控制台上打印,因此永远不会发生。
让我们详细研究一下循环类型,以了解循环的工作方式以及如何避免无限循环。
还有一个关于循环的好事:如果不再满足循环头中的条件,它们可以处理else
块,该块将被执行。 这是一个新的编程结构,对于经典的编程语言的程序员来说似乎很奇怪,因为经典的编程语言的循环没有其他部分。
修改循环主体中的控制流
在深入探讨循环之前,我们需要了解两种可修改循环控制流程的构造。 这些是突破并继续。 让我们看一下它们的总体工作原理,然后在有关循环本身的部分中使用示例。
break
顾名思义,break
语句会跳出循环。 大多数情况下,如果不应再继续循环,则在条件块内停止循环。
此语句立即结束循环,因此不再执行任何语句-在break
语句和循环的else
部分之后不在循环体内,也未执行。
while / for some_condition:
execute_this
do_this
break
this_does_not_executed
this_neither
else:
this_is_leaved_behind
this_too
在上面的示例中,我没有包括任何有条件的突破循环来演示语句的工作的条件。
continue
与break
语句平行,您可以告诉循环再次从头继续执行。 这意味着continue
语句后面的任何其他语句都不会执行,但是,循环将继续,直到循环头部的条件计算为False
为止。 在这种情况下,循环的else
块在循环结束时执行。
while / for some_condition:
execute_this
do_this
continue
this_does_not_executed
this_neither
else:
this_is_executed_at_the_end
this_too
现在,我们准备通过示例看一下循环本身。 你准备好了吗? 为什么不?
for
循环
for
循环旨在遍历集合或在有限的时间内执行循环主体。 循环的一般语法如下所示:
for variable in sequence:
statement_1
statement_2
...
statement_n
else:
else_statement_1
else_statement_2
...
else_statement_m
当然,else
块是可选的,因为在for
循环的主体中仅需要一个语句。
让我们举一个真实的示例:
>>> for m in menu:
... print(m)
... else:
... print("What do you want?")
...
eggs
sausage
bacon
spam
What do you want?
在上面的示例中,for 循环遍历列表中的元素,并将每个元素输出到控制台。 循环完成后,else 块将执行一次。
范围
在上面的示例中,一次又一次地定义数字列表似乎很麻烦。 这是 Python,因此必须有一些可利用开发人员的构造方法。 这是range
函数。
该函数需要一个参数:停止值; 和两个可选参数:start
和step
。
如果提供了唯一停靠点,则range
函数会生成一个从数字 0 到停靠点数字(不包括停靠点数字)的范围,步长为 1。
>>> for i in range(10):
... print(i)
...
0
1
2
3
4
5
6
7
8
9
如您在上面的示例中看到的那样,数字以 0 到 9 的步长打印从 0 到 9。
如果提供了起始参数,则range
函数会在起始编号和终止编号之间创建一个范围(同样,终止编号是唯一的)。
如果起始编号大于或等于终止编号,则不执行循环。
>>> for i in range(15,20):
... print(i)
...
15
16
17
18
19
>>> for i in range(25,10):
... print(i)
...
该步骤定义了要忽略的元素数,或者即使起始编号大于终止编号也要跳过起始和终止之间的编号。 在这种情况下,您必须提供步骤 -1。
>>> for i in range(1,10,2):
... print(i)
...
1
3
5
7
9
>>> for i in range(10,1,-1):
... print(i)
...
10
9
8
7
6
5
4
3
2
如您所见,范围函数用于执行循环有限的次数。
循环中断和继续
现在我们了解了for
循环的基础,让我们添加已知的控制流修改:中断并继续。
正如我之前告诉您的,如果您使用break
,则整个循环将终止。 您还记得从本节开始的for
循环示例吗? 现在,我们向其添加一个条件,以便在我们迭代通过的元素为“垃圾邮件”时打破该条件:
>>> menu = ['eggs', 'sausage', 'bacon', 'spam']
>>> for m in menu:
... if m == 'spam':
... break
... print(m)
... else:
... print("What do you want?")
...
eggs
sausage
bacon
>>> menu = ['eggs', 'sausage', 'bacon', 'spam']
>>> for m in menu:
... print(m)
... break
... else:
... print("What do you want?")
...
eggs
如果是for
循环,Continue
跳回到循环的开头,它跳到集合的下一个元素。 如果集合为空,则循环结束,然后执行else
循环。 我们再来看两个示例:一个带有条件继续,另一个带有自己的继续。
>>> menu = ['eggs', 'sausage', 'bacon', 'spam']
>>> for m in menu:
... if m == 'spam':
... continue
... print(m)
... else:
... print("What do you want?")
...
eggs
sausage
bacon
What do you want?
>>> menu = ['eggs', 'sausage', 'bacon', 'spam']
>>> for m in menu:
... continue
... print(m)
... else:
... print("What do you want?")
...
What do you want?
如您所见,continue
和break
之间的主要区别在于循环的流程:第一个循环返回并遍历其余元素,第二个循环终止整个循环。 当然,您可以将两者结合在一起以拥有自己的正确控制流程。
有副作用的循环
当然,将for
循环与列表一起使用时,您会产生副作用。 这是因为您要遍历的集合不是一成不变的,因此您可以在循环期间更改其值,这可能导致意外行为。 让我们来看一个示例:
>>> l = ['eggs']
>>> for e in l:
... if e == 'eggs':
... l += ['sausage']
... if e == 'sausage':
... l += 'spam'
... print(e)
...
eggs
sausage
s
p
a
m
>>> l
['eggs', 'sausage', 's', 'p', 'a', 'm']
如您所见,我们在for
循环执行期间修改了列表,因此当循环结束并返回到头进行求值时,它在列表中找到了新元素,因此循环继续执行。
为了避免这种情况,我们可以使用列表的副本在循环中进行迭代:
>>> l = ['eggs']
>>> for e in l[:]:
... if e == 'eggs':
... l += ['sausage']
... if e == 'sausage':
... l += 'spam'
... print(e)
...
eggs
while
循环
while
循环旨在无限期地执行循环的主体,直到达到条件为止。 使用for
循环,您只能执行有限数量的音调(取决于列表或您提供给它的范围)。
并且由于while
循环需要条件语句,因此您可以轻松地创建无限循环(如循环简介中所述)。
让我们看看while
循环是如何建立的:
while condition_evaluates_to_True:
statement_1
statement_2
...
statement_n
else:
else_statement_1
else_statement_2
...
else_statement_m
如您所见,该结构与for
循环几乎相同,但是在这里,您需要一个布尔条件,即计算结果为True
。 else
块是相同的,并在循环正常终止时执行。 如果在循环主体中遇到中断,则不会执行else
块。
>>> i = 0
>>> while i < 10:
... i += 1
... else:
... print("Finished loop, i has the value of ", i)
...
Finished loop, i has the value of 10
while
循环的主要用法是在具有用户交互作用的游戏或应用中,例如,您需要获取特定类型的输入(例如数字)或要执行逻辑直到游戏结束。 让我们看一个简单的示例,在该示例中,我们要求用户输入数字。
while True:
try:
a = int(input('Enter a number: '))
except ValueError:
print("This was not a number!")
continue
break
print("You entered: ", a)
如本例所示,如果用户未输入数字,则应用将打印出“这不是数字!” 由于执行了continue
语句,它再次执行了循环。 如果输入可以转换为数字,则break
语句将终止无尽的while
循环。
如果您运行该应用,则可能会得到以下信息:
Enter a number: enter
This was not a number!
Enter a number: a
This was not a number!
Enter a number: number
This was not a number!
Enter a number: 23j
This was not a number!
Enter a number: 42
You entered: 42
while 循环中断和继续
>>> i = 0
>>> while i < 10:
... continue
... i += 1
... else:
... print("Finished loop, i has the value of ", i)
...
这段代码不会停止,因为我永远不会递增,因此在继续调用表达式i < 10
之后,它会被一次又一次地赋值为False
,因此循环将无限执行。 如果启动了上面的示例,则可以通过按键盘上的CTRL-C
来停止它。
>>> i = 0
>>> while i < 10:
... break
... i += 1
... else:
... print("Finished loop, i has the value of ",i)
...
>>> i
0
如您所见,使用break
时,else
块不会执行。 在上面的示例中,i
的值完全没有变化。
自然,这些只是带有break
和Continue
的基本示例,大多数时候,您在条件表达式中使用它们。
Python 函数
原文: https://javabeginnerstutorial.com/python-tutorial/python-function/
现在,我们学习了 Python 编程的基础知识:变量,集合和循环。 如果您按照示例和教程进行操作,您可能会觉得有时我们会使用过多的代码,因此可以利用它。 但是也许您不知道该怎么做。
大多数时候,解决方案是引入函数。 我们遇到了其他模块的函数,就像字符串的isupper()
或random
模块的choice()
一样。
如果您学习过数学,那么您已经知道函数。 数学家可以与他们一起做非常讨厌的事情,但是程序员也可以。 函数的目的是产生仅由传递给函数的参数确定的结果。
在编程语言中,我们可以将函数视为黑盒,在其中我们发送定义数量的参数(0 和更多),然后该函数返回结果。 在 Python 中,即使您没有显式编写或查看return
语句,函数始终会返回结果。 这意味着,如果您知道另一种编程语言(例如 C++ 或 Java),那么您将了解void
函数。 在 Python 中没有这种类型-但是,当我告诉您返回值时,我们将在本文后面看到。
定义函数
使用def
语句创建一个函数。 通用语法如下所示:
def function_name(param_list):
function_body statements
参数列表包含零个或多个元素。 如果您调用函数,那么您说的是参数而不是参数,但这是术语,在我看来,即使您调用函数也只说参数也是可以的。 如果您不将其声明为可选参数,则这些参数是必需的。 我们将在下一节中查看可选参数。
每次调用函数时,都会执行其主体中的语句。 当然,您可以使用函数体内的pass
语句不执行任何操作-但在这种情况下,pass
语句也将执行。
如您所知:函数必须在其主体中至少包含一个语句。 没有它,您会得到一个错误:
>>> def no_body_function():
...
File "<stdin>", line 2
^
IndentationError: expected an indented block
嗯,这个错误消息并不是最能说明问题的,但是在这种情况下,编译器缺少缩进的块-至少有一个针对函数体的语句。
因此,我们编写一个简单的函数,它是交换计算器。 它获得两个参数:值和汇率,并返回更改后的值(值乘以汇率)。
def exchange(value, rate):
return value*rate
因此,每次定义函数时,请确保您具有缩进的主体。 如果您在 Python 的交互式解释器中遵循本文,则定义将如下所示:
>>> def exchange(value, rate):
... return value*rate
...
正如我之前提到的,您也可以使用没有return
语句的函数。 但是,大多数情况下,您不会使用这些函数,但是为了简洁起见,我们在这里也看一个示例:
>>> def no_return():
... print("This function has no return statement")
...
调用函数
我认为这很容易。 您已经知道如何调用函数,但让我们快速进行介绍。 如果您有一个函数定义,则可以通过传递正确的参数作为参数来调用它,这很不错。
正如我之前告诉您的,我们已经调用了函数。 我们首先调用的基本函数是print()
。 您可以不带任何参数地调用它,在这种情况下,它将在输出中输出换行符(空行)。 另外,我们可以传递任意数量的参数,每个参数之间用逗号(,
)分隔,它们也将被打印到输出中。
现在,我们调用上一节中定义的两个函数。
>>> no_return()
This function has no return statement
>>> exchange(123, 1.12)
137.76000000000002
如您所见,调用函数没有什么复杂的。
返回值
之前我曾说过,函数会返回值-即使您未显式编写return
语句。 现在是时候验证我的语句了,因此我将向您展示即使no_return()
函数也返回一个值,并且该值为None
。
要查看函数的return
语句,我们只需将函数调用包装到print()
函数调用中即可。
>>> print(no_return())
This function has no return statement
None
>>> print(exchange(123,1.12))
137.76000000000002
在这里您可以看到,即使没有return
语句的函数也返回None
。 这意味着在这种情况下,您必须小心使用返回值,因为使用None
时,仅在布尔表达式中使用它就几乎无能为力-当然要小心。
>>> result_value = no_return()
This function has no return statement
>>> result_value + 5
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'
>>> if result_value == False:
... print("Nothing useful...")
... else:
... print("Wow, we have got True back!")
...
Wow, we have got True back!
如您在上面的示例中看到的,例如,您不能在数学运算中使用None
,并且None
不为False
。
要修复示例的第二部分,我们可以像这样更改代码:
>>> if not result_value:
... print("Nothing useful...")
... else:
... print("Wow, we have got True back!")
...
Nothing useful...
只使用没有任何值的return
也是如此。 返回结果与没有返回语句的结果相同。 为什么要这么好? 例如,如果条件求值为true
,并且您想不返回任何内容,则希望终止函数。 自然地,您可以使用return None
,但是更多的 pythonic 解决方案将是简单地使用return
。
可选参数
您可以使用可选参数创建函数。 这意味着不必将这些参数传递给函数。 在这种情况下,将使用其默认值-如果可选参数获得其默认值,有时会跳过语句块。
可选参数必须遵循强制性参数,并且必须具有默认值。 当您调用函数并且不提供此参数时,将使用此值。
以前面介绍的交换函数为例。 快速提醒一下,这里是定义:
def exchange(value, rate):
return value * rate
如果我们尝试仅使用 1 个参数(带有值)来调用此交换函数,则会从解释器中收到错误消息:
>>> exchange(42)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: exchange() missing 1 required positional argument: 'rate'
现在,将rate
变量设为可选,并将其默认值设置为 1,以便能够以相同的货币调用此函数而无需进行任何兑换。
因此,解决方案是为函数中的rate
参数设置默认值,并将该值设置为 1。
要查看其工作原理,我还对代码进行了一些更改以显示当前汇率:
>>> def exchange(value, rate=1):
... print('Current exchange rate is', rate)
... return value * rate
现在rate
参数是可选的,我们可以调用带有或不带有rate
的函数:
>>> exchange(124,0.78)
Current exchange rate is 0.78
96.72
>>> exchange(325,1)
Current exchange rate is 1
325
>>> exchange(42)
Current exchange rate is 1
42
可选参数和必需参数的顺序很重要。 例如,如果我们更改顺序并将rate = 1
作为value
之前的第一个参数添加,我们将得到一个错误:
>>> def exchange(rate=1, value):
... return value * rate
...
File "<stdin>", line 1
SyntaxError: non-default argument follows default argument
如果您考虑一下,您将了解为什么会这样:如果我们提供一个论点,该怎么办。 是可选的还是必需的? 好吧,口译员无法分辨,也许您最终会得到错误的结果。
关键字参数
在学习 Python 时,您可能会遇到术语“关键字参数”。 实际上,它们与可选参数相同:它们具有名称和默认值。 这个名称是关键字,您可以使用它为该参数分配一个新值。
让我们再次看一下前面的示例:rate
是函数的关键字参数。 由于exchange
只有一个可选参数,因此您可以同时使用两种参数来调用它:
>>> exchange(42, 1.25)
52.5
>>> exchange(42, rate=1.25)
52.5
第二种情况是我们使用关键字参数。
现在再来看一个示例,其中有多个可选参数,以便您了解它如何与关键字参数一起实际使用。
该示例将非常基础:我们定义一个函数,该函数采用四个参数a
,b
,c
,d
并执行以下计算:a + b – c + d
。 要使其工作,它仅需要 2 个参数,两个是可选的。
>>> def sub_sum(a, b, c=0, d=0):
... return a + b - c + d
...
>>> sub_sum(12,33)
45
现在,我们可以选择传递变量c
和d
的值。 如果我们已经知道为c
提供值有两种选择。
>>> sub_sum(12,33,0,10)
55
>>> sub_sum(12,33,d=10)
55
如您所见,不必提供所有值,在调用函数时为d
分配值就足够了。 这就是为什么它们被称为“关键字参数”的原因。 您可能会想到:有些函数带有很多参数,大多数时候只需要它们的默认值即可。 因此,您无需传递它们(因此您不必知道默认值是什么),并且可以使用关键字的列表中某个位置的单个参数对函数调用进行微调。
进一步讲这个概念,我们可以用一种关键字语法调用函数,其方式是您在其他语言中无法想象的:您可以随意对值进行排序,直到为所有必需的参数提供它们的名称。
>>> sub_sum(b=12,a=33,d=10)
55
>>> sub_sum(d=10, 8, 11)
File "<stdin>", line 1
SyntaxError: positional argument follows keyword argument
如您所见,如果您弄乱了顺序,则不能省略必需参数的名称。 在这种情况下,需要命名它们,以使解释器知道您想要设置这些值。
关键字参数的陷阱
上面我们已经看到了一种使用关键字参数的方法。 但是,每个硬币都有两个面。 分配这些默认值后,让我们再深入一点。 它是在创建函数时完成的(因此,在解释器解析函数定义时),而不是在调用函数时完成。 这意味着只要将不可变类型用于命名参数/关键字参数,我们就看不到任何区别。
但是,当我们使用可变变量(例如列表)时,可能会出现问题:
>>> def append_if_short(s, lst=[]):
... if len(s) < 4:
... lst.append(s)
... return lst
...
在上面的示例中,如果s
的长度最大为 3,则将参数s
附加到参数lst
上。如果将两个参数都传递给函数,这似乎很好。 但是让我们有时调用此函数...
>>> append_if_short("one")
['one']
>>> append_if_short("two")
['one', 'two']
>>> append_if_short("three")
['one', 'two']
>>> append_if_short("four")
['one', 'two']
>>> append_if_short("five")
['one', 'two']
>>> append_if_short("six")
['one', 'two', 'six']
如您所见,这会导致意外的行为。 我们传入一个字符串,并返回一个包含比预期更多的元素的列表。
但是,这还不是全部。 我们会让事情变得更糟:
>>> def append_if_short(s, lst=[]):
... if len(s) < 4:
... lst.append(s)
... return lst
...
>>> result = append_if_short("one")
>>> result
['one']
>>> result.append('five')
>>> append_if_short('two')
['one', 'five', 'two']
在上面的示例中,我们向列表中添加了一个元素,该元素显然超过了 3 个字符-这又可能导致意外行为。
要解决此问题,请更改函数定义:
>>> def append_if_short(s, lst=None):
... if lst is None:
... lst = []
... if len(s) < 4:
... lst.append(s)
... return lst
...
>>> result = append_if_short("one")
>>> result
['one']
>>> result.append('five')
>>> append_if_short('two')
['two']
文档字符串
有时(我希望每次)您都会有记录函数的冲动。 您可以使用围绕函数定义的简单注释来完成此操作。
但是,您应该遵循一种常见的做法:文档字符串。 这些是简单的文档字符串,位于函数定义之后。 它们具有特殊的三引号格式,因为它们是描述您的函数的多行字符串。
有时文档比函数本身更长。 约定是使文档字符串的第一行成为简短的单行描述,然后在空白行后加上完整的描述,然后给出一些示例(如在交互式解释器中键入的内容)。
因此,让我们使用此指南并将文档添加到我们的append_if_short
函数中。
def append_if_short(s, lst=None):
""" Returns a list containing s if the length of s is smaller than 4.
s
是任何字符串,lst
是可选的列表类型参数。 如果未提供lst
,则新的lst
将分配一个新的空列表。 如果len < 4
,则将s
附加到lst
并返回lst
。
>>> append_if_short('one')
['one']
>>> append_if_short('two', ['one'])
['one', 'two']
>>> append_if_short('three', [])
[]
>>> append_if_short('four')
[]
"""
if lst is None:
lst = []
if len(s) < 4:
lst.append(s)
return lst
面向对象编程(OOP)
原文: https://javabeginnerstutorial.com/python-tutorial/object-oriented-programming-oop/
现在,我们进行了面向对象的编程。 曾经有过这样一种炒作:每种语言都是围绕对象设计的,而 Python 开发人员 Guido van Rossum 认为“为什么不呢?” 并添加了类以支持面向对象的开发。 一些 python 传福音者认为这是一个错误的决定,有些认为这是一个好方法……
面向对象是一个大话题。 可以写一本关于它的书,但我会坚持在文章的狭小范围内。 或最多有两篇有关 OO 的常规文章和 Python 中的 OO 文章。
一般的面向对象
面向对象技术大约在 60 年代后期,但直到 90 年代初才在开发人员中获得了发展空间。 我们将学习以下四个主要原则:
- 封装
- 数据抽象
- 继承
- 多态
如果我想模拟现实生活,我会说 OO 就像一家餐馆。 您可以有两种类型:一种可以在柜台上找到食物,也可以用食物自助服务。 另一个是您进餐的地方,它是由专业服务准备并带给您的。
带有自助服务的第一个版本是命令式语言使用的东西(例如 C 或简单的 Python 脚本),在这里每个人都可以访问所有内容,并且他们可以使用他们想要的东西。 在这种情况下,有时会将碗碟留在桌子上,因为带回碗碟是客户的工作。
第二个版本是 OO。 在那里您可以封装功能,并且只能访问那些公开可用的部分。 如果您已经使用 Java 或 C++ 开发过,您可能会知道公开,受保护和私有访问的概念。 在这种情况下,通过员工来获取食物并带回餐具。 他们知道从何处,何处放东西可以得到什么,而最终用户并不需要了解一切。
如果我们回头看面向对象,那么我们可以说一类是一个对象的定义,一个对象是指定类的实例。
一个类定义了未来对象具有的属性和函数,以及该语言是否使用访问限制,您可以在类定义中告诉公众可以访问哪些部分,该类的扩展还是仅内部使用。
现在是时候深入研究 OOP 的四大原则了。
封装
封装是指将数据和函数打包到单个组件中。 但是,在我们的情况下,进入一类,其他编程语言支持其他替代方法。 该类的函数将根据存储在该类的字段中的数据进行操作。
在某些编程语言中,封装用于隐藏信息,或更准确地说:限制对数据和函数的访问。 这些语言包括 C++ 和 Java,例如,您可以在其中使用private
,protected
和public
来限制对字段和方法的访问。 但是在 Python 中没有这样的限制级别。 您可以访问类的每个字段和函数。
但是,有一个约定没有写下来,但是每个 Python 开发人员都知道并且应该知道:名称以双下划线(__
)开头的类(字段和函数)的成员应视为私有的,不应调用或访问。
数据抽象
数据抽象强制将类型的抽象属性与实现细节之间的清晰区分。 抽象属性是那些使用此数据类型对客户端可见的属性(在我们的情况下为类,在其他编程语言中为接口定义),并且实现对客户端隐藏并为私有。
而且由于实现是私有的,因此可以随时间更改(例如,使代码更快),并且客户端不会注意到此更改,因为抽象保持不变。
好吧,在 Python 中,没有什么比其他 OO 语言的接口更好。 您只有一个类,这个类有它的字段和功能。 当然,您可以具有一个“公开”函数作为外部代码的接口,以及一个或多个实现该“公开”函数的逻辑的“私有”函数。 但这不是真正的抽象。
但是,Python 知道用于只读字段的解决方案,这些字段是通过所谓的“获取器”方法即时计算的。 当然,对于读写字段,Python 也使我们也可以使用“设置器”方法。 我们将在后面看到这两个示例。
继承
继承是 OOP 的一项关键功能,其中一个类基于另一类的模板/实现(从该类继承)。 这是一种代码重用的基本方法,您可以将子类之间的通用函数和信息封装到一个基类中。
继承模型有不同类型,但是最常见的两种是单继承和多继承。 Python 使用多重继承,这意味着一个类可以根据需要扩展任意多个类。
继承通常与对象组成混淆。 有时,新的开发人员会尝试解决继承的所有问题,即使继承应该是对象组合。 对象组合意味着您拥有另一个类的实例的属性,但是您的类没有扩展它。 如果您不知道需要哪一个,请记住以下简单的解决方案:
继承是 is-a 关系,意思是汽车是车辆。 对象组成是与的关系,意味着汽车具有车轮(或至少汽车具有车轮)。
多态
在 OOP 中,多态是为多个类型提供单个接口。 在 Python 中,这意味着您希望将超类作为参数(例如,执行isinstance()
检查)并在对象上调用该超类的通用方法。 现在,通过多态,将在所使用的子类中执行该方法的实际实现。
>>> class Animal:
... def sound(self):
... raise NotImplementedError
...
>>> class Dog:
... def sound(self):
... print('woof')
...
>>> class Dog(Animal):
... def sound(self):
... print('woof')
...
>>> class Cat(Animal):
... def sound(self):
... print('meow')
...
>>> def animal_sound(animal):
... if isinstance(animal, Animal):
... animal.sound()
... else:
... print("Not an animal, do not know how to make it sound")
...
>>> cat = Cat()
>>> dog = Dog()
>>> animal_sound(dog)
woof
>>> animal_sound(cat)
meow
如您在上面的示例中所看到的,animal_sound
函数验证该参数是Animal
,然后调用该特定动物的sound
方法。
什么时候使用 OO?
自然,OO 不是万能的油。 因此,在开发时应考虑使用 OOP。 在本节中,我将更深入地探讨何时应用本章的原理和技术。
确定何时使用面向对象的编程并不容易。 我们必须记住,对象具有数据和行为,这使事情变得复杂。 这就是为什么许多 Python 开发人员使用简单的数据结构(列表,集合,字典)和简单的函数的原因,除非确实需要额外的层抽象(我也是)。
现在,如果我们看到使用相同数据集调用函数,则可以考虑将数据封装到一个类中,然后将这些函数添加为类函数以表示行为。
一个简单的示例就是几何图形之外的东西。 在那里,您使用一个 2 元组(一对)来存储点。 点列表代表一个形状(多边形)。 因此,您首先要定义一个列表,其中包含一些表示点的对:
triangle = [(2,3), (5,7), (0,0)]
现在,如果您要计算该三角形的周长,可以编写一个如下所示的函数:
import math
def perimeter(points):
perimeter = 0
points_extended = points + [points[0]]
for i in range(len(points)):
perimeter += math.sqrt((points_extended[i][0] - points_extended[i+1][0])**2 + (points_extended[i][1] - points_extended[i+1][1])**2)
return perimeter
到达这一点之后,您可能会感觉到有一个对象封装了三角形的所有点(数据)和周长函数(行为)。 如果您想得更多,可以将三角形点的 x 和 y 坐标封装到另一个对象中,然后将两个点的距离计算添加到该对象中。
Python 中有一些类可用于此封装。
在下一篇文章中,我将深入探讨 Python 定义和使用对象(类)的方式,并且我必须事先告诉您还有许多您无法想象的方式。
Python 中的面向对象编程
原文: https://javabeginnerstutorial.com/python-tutorial/object-oriented-programming-in-python/
与在 Python 中一样,在开发程序时不必将代码创建为类,因为我们可以使用也称为过程编程的函数。 但是,过程序编程用于编写小型,简短和简单的程序,而面向对象的编程(OOP)程序随着程序的大小和复杂性的增长而变得越来越重要。 自从开发以来,Python 一直是面向对象的语言。
因此,让我们简要介绍一下所使用的面向对象的编程概念-
类
与其他编程语言相比,Python 中的类概念被添加了最少的新语法和语义。 Python 中类的概念是 C++ 和 Modula-3 中类的混合。 Python 类提供 OOP 的所有基本功能,例如允许多个基类的类继承,可以覆盖其基类的任何方法的派生类以及可以使用相同名称调用基类的方法的方法。
首先看看类:
在 Python 中,使用新的语法和语义引入了类。
类定义语法:
class ClassName:
<statement 1>
<statement 2>
.
.
.
<statement n>
示例:
下面给出的是一个简单的 Python 类的示例:
class Student: //common base class for all students
stuCount=0
def_init_(self, name, rollno):
self.name = name
self.rollno = rollno
Student.stuCount += 1
def displayCount( self ):
print “The number of students are: %d ” % Student.stuCount
def displayStudent( self ):
print “Name : ” , self.name , “, Roll No : ” , self.rollno
在上面的代码中,变量stuCount
是一个类变量,其值在学生类的所有实例之间共享。 可以从类内部或类外部以Student.stuCount
访问该变量。 第一个方法init()
是一种特殊的方法,称为类构造器,或者是在创建类的新实例时调用的初始化方法。 调用方法时,Python 本身会添加self
参数。
对象
对象是 Python 面向对象程序的基本构建块。
stu1 = Student(“Raj” , 34) // first object of student class
stu2 = Student(“Reema” , 12) //second object of student class
属性
属性是对象的特征。 __init__()
方法用于初始化对象的属性。 要访问对象的属性,我们对对象使用点运算符。 喜欢
stu1.displayStudent( )
stu2.displayStudent( )
print “The number of students are: %d ” % Student.stuCount
因此,完整的程序是:
class Student: //common base class for all students
stuCount=0
def_init_(self, name, rollno):
self.name = name
self.rollno = rollno
Student.stuCount += 1
def displayCount( self ):
print “The number of students are: %d ” % Student.stuCount
def displayStudent( self ):
print “Name : ” , self.name , “, Roll No : ” , self.rollno
stu1 = Student(“Raj” , 34) // first object of student class
stu2 = Student(“Reema” , 12) //second object of student class
stu1.displayStudent( )
stu2.displayStudent( )
print “The number of students are: %d ” % Student.stuCount
继承
继承 Python 中 OOP 的另一个功能,它是从现有类构建新类的一种方式,它们被称为派生类。 派生类是从基类派生或继承的。 继承的主要优点是可以重用代码,并可以降低程序的复杂性。 派生类扩展了基类的功能。
示例:
class Vehicle: // base class
def _init_( self ):
print “Calling vehicle class”
def vehicle1( self ):
print “Calling vehicle1 method”
class Car(Vehicle): //derived class
def _init_(self):
print “Calling car class”
def car1(self):
print “Mercedes,BMW,”
c= Car( ) //instance of car class
c.car1( ) //calling derived class method
c.vehicle1( ) //calling base class method
输出:
Calling car class
Mercedes,BMW
Calling vehicle class
Calling vehicle1 method
多态
多态是一个过程,其中函数以不同的方式用于不同的输入。 基本上,多态是如果类 B 从类 A 继承而来,则它不能继承类 A 的所有内容,因此可以继承类 A 的某些函数。
示例:
class Books:
def _init_(self, name=‘ ’):
self.name = name
def programming(self):
print “Programming books:”
class Python(Books):
def programming(self):
print “In python world”
class Java(Books):
def programming(self):
print “In java world”
b = Books( )
b.programming( )
p = Python( )
p.programming( )
j = Java( )
j.programming( )
输出:
Programming books:
In python world
In java world
运算符重载
在 Python 中,类可以使用特殊的方法名称进行操作,但是不能直接调用这些方法,只能使用特定的语法。
示例:
class Addition:
def _init_(self,a,b):
self.a = a
self.b =b
def _str_(self):
return ‘Addition (%d, %d) ’ % (self.a, self.b)
def _add_(self,other):
return Addition(self.a + other.a, self.b + other.b)
a1 = Addition(5,10)
a2 = Addition(2,3)
print a1 + a2
Python 3 中的异常处理
原文: https://javabeginnerstutorial.com/python-tutorial/exception-handling-python-3-5/
在本文中,我将向您介绍异常处理。 在本系列的前几篇文章中,我们使用异常处理来介绍一些罕见的异常情况(还记得猜数字游戏吗?),现在是时候解释如何在自己的应用中以及在什么时候使用它了。 去做吧。
什么是异常?
异常是在脚本/应用执行期间发生的错误。 顾名思义,异常很少发生,它们是常规工作流规则的异常。
在 Python 中,异常称为错误,使开发人员从其他语言进行迁移会造成混淆。 基本错误类是Exception
,其他异常是该类的子类-这使事情更加混乱。
每次引发异常时,它都会进入调用栈,直到代码块对其进行处理。 如果没有代码可以处理该异常,则解释器将接管工作,写出讨厌的错误消息并终止正在运行的脚本。
自然地,在错误处理块中,您可以引发一个新异常或将当前异常抛出到顶部。 这是 Web 应用中的常见做法,在 Web 应用中,您希望记录服务调用导致了异常,但是您也想通过用户界面通知用户错误。 为此,您必须转发异常。
捕捉异常
您可能还记得,在猜数字游戏中,我们已经使用了异常处理。 处理异常的一般规则是您需要一个try-except
块:
try:
block of code which might raise an error
except:
exception handling
如您所见,可能引发异常的代码进入try
块。 然后,错误处理将在except
块中发生。
但是使用普通格式,但以下情况除外:在大多数情况下,如上例所示,这是一种不好的做法。 这是因为通过这种方式,您可以处理给定块中的所有异常,有时这超出了您的期望。
让我们创建一个简单的示例,在该示例中,我们调用一个总是引发异常的函数:
def error_function():
return int('nine')
如果我们在交互式解释器中定义此函数并调用它,则会出现异常:
>>> error_function()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in error_function
ValueError: invalid literal for int() with base 10: 'nine'
现在,我们将此函数调用包装到try-except
块中:
>>> try:
... error_function()
... except:
... print('an exception occurred')
...
发生异常
在上面的示例中,我们捕获了try
块中可能引发的所有异常。 但是,正如我提到的那样,这是一个不好的做法。 最好捕获那些我们知道它们可能会来的异常,并让真正意外的异常上升,直到它们被处理为止。 为此,我们一开始只会捕获语法错误:
>>> try:
... error_function()
... except SyntaxError:
... print('an exception occurred')
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
File "<stdin>", line 2, in error_function
ValueError: invalid literal for int() with base 10: 'nine'
如您所见,在这种情况下,异常由解释器处理。 因此,更改异常处理以处理ValueErrors
:
>>> try:
... error_function()
... except ValueError:
... print('an exception occurred')
...
an exception occurred
写出异常
有时仅捕获异常是不够的,但是您必须记录错误原因(有时记录到日志文件或控制台中),以使用户或应用管理员知道正在发生的情况。
为此,您可以按如下所示编写先前的异常处理块:
>>> try:
... error_function()
... except ValueError as e:
... print('an exception occurred:', e)
...
发生异常:以int()
为基数 10 的无效文字:“nine
”
如您在上面的示例中看到的,您可以掌握异常对象本身,并将其写入控制台。 如果执行此操作,则会显示异常消息。
引发异常
我说的是提出异常。 如果您有耐心并且没有用 Google 搜索该怎么做,那么时机已到。 好吧,与我所指的一样:
引发异常
这就引发了一个普遍的异常,对此没有太多了解。 您可以在交互式解释器中尝试一下:
>>> raise Exception
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
Exception
>>> raise Exception("MY custom error")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
Exception: MY custom error
如您所见,您可以提供一条消息作为异常的构造器参数,并在解释器处理错误时显示此消息-或者您可以如上一节所述将其提取。
总结
有时会发生异常。 在某些情况下,您或者想要处理它们,有时只希望脚本/应用停止执行。 在本文中,我们研究了如何捕获和处理异常以及如何将根本原因打印到用户的方式。
Python 3:猜数字
原文: https://javabeginnerstutorial.com/python-tutorial/python-3-guess-the-number-2/
现在,让我们编写一个脚本,该脚本实现一个基本的“猜数字”游戏。 游戏规则为:
- 用户选择是否要猜测 1 到 100 或 1 到 1000 之间的数字
- 根据数字范围,用户拥有固定数目的猜测
- 应用生成一个数字进行猜测
- 用户输入一个数字
- 如果这是秘密号码,则应用祝贺用户并询问他/她是否想再玩一轮
- 否则,应用会告诉用户密码是否小于或大于提供的密码
- 猜测数增加
- 如果用户用尽了所有猜测,则应用告诉他/她的秘密号码,并询问用户是否要再玩一轮
如您所见,规则很简单,但实现起来似乎很复杂。 由您决定如何处理错误的输入类型(无数字)。
好吧,这并不像听起来那样困难。 让我们看一下我的解决方案的一些示例输出:
Should the secret number between 1 and 100 or 1 and 1000? 100
You have chosen 100, you will have 7 guesses to find the secret number.
I have chosen the secret number...
What's your guess? 34
The secret number is higher...
What's your guess? 54
The secret number is higher...
What's your guess? 66
Congrats, you have Won!
The secret number was 66
Do you want to play another round? (yes / no) yes
Should the secret number between 1 and 100 or 1 and 1000? 1000
You have chosen 1000, you will have 10 guesses to find the secret number.
I have chosen the secret number...
What's your guess? 500
The secret number is lower...
What's your guess? 400
The secret number is lower...
What's your guess? 300
The secret number is higher...
What's your guess? 350
The secret number is higher...
What's your guess? 375
The secret number is lower...
What's your guess? 370
The secret number is lower...
What's your guess? 360
The secret number is higher...
What's your guess? 365
The secret number is lower...
What's your guess? 364
The secret number is lower...
What's your guess? 363
The secret number is lower...
Sorry, you lose.
The secret number was 362
Do you want to play another round? (yes / no) no
如您所见,当猜测用完时,应用不会停止,并且如果用户获胜或失败,它会显示一条消息。
让我们看一下代码。
__author__ = 'GHajba'
import random
while True:
while True:
try:
max_number = int(input('Should the secret number between 1 and 100 or 1 and 1000? '))
except ValueError:
print("This was not a number!")
continue
if max_number != 100 and max_number != 1000:
continue
else:
break
if max_number == 100:
guess_count = 7
else:
guess_count = 10
print('You have chosen {}, you will have {} guesses to find the secret number.'.format(max_number, guess_count))
secret_number = random.randint(1, max_number)
print('I have chosen the secret number...')
guesses = 0
while guess_count - guesses:
try:
guesses += 1
guessed = int(input("What's your guess? "))
except ValueError:
continue
if guessed == secret_number:
print('Congrats, you have Won!')
break
elif guessed > secret_number:
print('The secret number is lower...')
else:
print('The secret number is higher...')
else:
print("Sorry, you lose.")
print("The secret number was ", secret_number)
answer = ''
while answer.lower() not in ['yes', 'no', 'y', 'n']:
answer = input("Do you want to play another round? (yes / no) ")
if 'no' == answer or 'n' == answer:
break
如您所见,代码非常繁琐,因为我们包含许多循环来验证输入并处理主游戏循环。 也许您以后会发现此代码根本不可读。 为了解决这个问题,我们将学习函数,然后重构这部分代码以使用函数。
但是,此脚本也有一些替代解决方案。 例如,您可以将中间的while
循环更改为for
循环:
for guesses in range(guess_count):
在这种情况下,循环遍历猜测范围,如果到达最后一个数字,则循环结束。
Python 3:猜数字 – 回顾
原文: https://javabeginnerstutorial.com/python-tutorial/python-3-guess-the-number-the-return/
我们之前已经看过这个示例。 在那里,我包括了很多处理用户输入和主要逻辑的循环……现在该重构应用以使用函数了。 这应该使代码易于阅读,以后我们可以重用部分代码。
这些可重复使用的部分之一是数字读数。 我们有两个要点,我们希望用户输入数字并编写了略微相同的代码。 这是开始重构并将其提取为一个函数的好地方。
因此,让我们将其提取为一个函数。 唯一的区别是我们打印出的询问用户输入数字的消息。 可以将其作为函数的参数来处理,因为这是唯一的可变部分。 因此,让我们看一下函数的定义:
def ask_user_for_number(message_text):
while True:
try:
return int(input(message_text))
except ValueError:
print("This was not a number!")
continue
如您所见,这是一种非常简单的方法,我们要求用户输入内容,如果输入的是数字,则将其返回。
自然,我们无法使用此方法处理将数字验证为 100 或 1000 的情况,因此我们也必须在那里修改代码块。
在此简单修改的最后,我们为应用提供了以下代码(并且具有相同的功能):
__author__ = 'GHajba'
import random
def ask_user_for_number(message_text):
while True:
try:
return int(input(message_text))
except ValueError:
print("This was not a number!")
continue
return number
while True:
max_number = 0
while max_number != 100 and max_number != 1000:
max_number = ask_user_for_number('Should the secret number between 1 and 100 or 1 and 1000? ')
if max_number == 100:
guess_count = 7
else:
guess_count = 10
print('You have chosen {}, you will have {} guesses to find the secret number.'.format(max_number, guess_count))
secret_number = random.randint(1, max_number)
print('I have chosen the secret number...')
guesses = 0
while guess_count - guesses:
guesses += 1
guessed = ask_user_for_number("What's your guess? ")
if guessed == secret_number:
print('Congrats, you have Won!')
break
elif guessed > secret_number:
print('The secret number is lower...')
else:
print('The secret number is higher...')
else:
print("Sorry, you lose.")
print("The secret number was ", secret_number)
answer = ''
while answer.lower() not in ['yes', 'no', 'y', 'n']:
answer = input("Do you want to play another round? (yes / no) ")
if 'no' == answer or 'n' == answer:
break
看起来很不错,没有重复的代码,而且非常精简。 但是,如果您对测试有所了解,您可能会说这段代码有一个很大的“主”块,很难测试。
我必须同意。 单元测试(尽管我将在下一章中进行介绍)在这里会很麻烦。 解决方案是将这个较大的主循环拆分为较小的函数,这些函数可以单独进行测试。
例如,求值用户是赢还是输。 为此,我们可以编写一个函数,该函数将秘密数字,猜测,猜测计数和猜测数字作为输入,并对消息进行求值以告知用户。 但是一个函数的四个参数很多,因此现在让我们对其进行划分。 基于这些输入,我将创建一个函数来告诉用户秘密数字是猜中的数字是更高还是更低。
def user_won(guessed, secret):
if guessed == secret_number:
print('Congrats, you have Won!')
return True
if guessed > secret_number:
print('The secret number is lower...')
else:
print('The secret number is higher...')
在user_won
函数中,我们使用return
语句指示用户是否赢了。 如果不是,则返回隐式None
,其结果为false
。
我们可以测试的另一件事是询问用户是否要再玩一轮。
def want_continue():
answer = ''
while answer.lower() not in ['yes', 'no', 'y', 'n']:
answer = input("Do you want to play another round? (yes / no) ")
return answer in ['yes','y']
完成所有这些更改后,让我再次向您显示完整代码:
__author__ = 'GHajba'
import random
def ask_user_for_number(message_text):
while True:
try:
return int(input(message_text))
except ValueError:
print("This was not a number!")
continue
return number
def user_won(guessed, secret):
if guessed == secret_number:
print('Congrats, you have Won!')
return True
if guessed > secret_number:
print('The secret number is lower...')
else:
print('The secret number is higher...')
def want_continue():
answer = ''
while answer.lower() not in ['yes', 'no', 'y', 'n']:
answer = input("Do you want to play another round? (yes / no) ")
return answer in ['yes','y']
while True:
max_number = 0
while max_number != 100 and max_number != 1000:
max_number = ask_user_for_number('Should the secret number between 1 and 100 or 1 and 1000? ')
if max_number == 100:
guess_count = 7
else:
guess_count = 10
print('You have chosen {}, you will have {} guesses to find the secret number.'.format(max_number, guess_count))
secret_number = random.randint(1, max_number)
print('I have chosen the secret number...')
guesses = 0
while guess_count - guesses:
guesses += 1
guessed = ask_user_for_number("What's your guess? ")
if user_won(guessed, secret_number):
break
else:
print("Sorry, you lose.")
print("The secret number was ", secret_number)
if not want_continue():
break
Python 生成器
原文: https://javabeginnerstutorial.com/python-tutorial/python-generator/
本文旨在为 python 生成器提供一些温和的介绍,并希望能激发读者为他们找到有趣的用法。
这是一个有点复杂的主题,包含许多小细节,因此我们将使用下面的约定来提供更多信息。 随意跳过这些内容,因为它们不是理解所必需的,但是对于好奇的人,它们应该提供更多的见解。
什么是生成器?
生成器是功能强大的编程工具,可用于对大型或计算昂贵的数据进行有效的迭代。 与其他迭代方法相比,它还拥有更简单的代码和更好的性能。
实现 python 生成器的主要方法是:
- 生成器函数(在 Python 2.2 中添加)
- 生成器表达式(在 Python 2.4 中添加)
首先了解一些核心概念是必不可少的,因此现在让我们深入研究。
迭代和可迭代对象
您可能已经知道, 迭代只是重复代码行的更正式术语,例如在while
和for
循环中。
基于可迭代对象的概念,Python 在该语言中内置了一种特别灵活的迭代方法,经常在for
循环中使用。 每当我们以前谈论for
循环时,它们始终对可迭代对象进行操作。
可迭代的对象通常是序列类型,例如列表,范围,元组或集合,在 Python 中,可迭代的对象意味着可以用for
之类的东西来迭代对象。
# These are all iterable objects that 'for' is operating on
my_list = [1,2,3,"Python",4]
my_tuple = (5,6,7,"Rocks")
my_set = {8,9,10}
for item in my_list:
print(item)
for item in my_tuple:
print(item)
for item in my_set:
print(item)
for x in range(5):
print(x)
在幕后,我们将对象称为“可迭代”这一事实意味着它公开了一个名为“
__iter__()
”的方法,该方法返回该对象的迭代器。
迭代器
迭代器是控制可迭代对象上的循环行为的对象。 其目的是跟踪到目前为止迭代已到达的位置。
当“for
”函数和相关函数在可迭代对象上运行时,它们实际上首先要从该对象请求一个迭代器。 如果失败,则将引发异常,否则,该迭代器将反复用于获取序列中的下一项,直到序列用尽。
实际上,这意味着“for
”可以循环可提供迭代器的任何对象,但不能在其他任何事情上循环。像int
或float
之类的对象不能与其一起使用,因为它们没有实现正确的方法。
# Example: for statement works over a list, but not over a float
my_list = [3,5,7,9]
my_float = 1.1
for item in my_list:
print(item)
for item in my_float:
print(item)
输出
3
5
7
9
Traceback (most recent call last):
File ".\examples\iterator_demo.py", line 23, in <module>
for item in my_float:
TypeError: 'float' object is not iterable
迭代器可以跟踪程序在循环中的位置,并在请求时提取下一个项目。
迭代器必须:
- 创建时将其设置为循环。
- 实现必须返回下一项的
__next __()
方法 - 循环结束时,
__next__()
方法还必须引发StopIteration()
异常。
通常,迭代器对象使用存储在其属性之一中的循环计数器或类似计数器来跟踪其位置。 这是一个如何在实践中使用的示例:
- 创建一个迭代器:将循环计数器设置为零。
- 迭代器的
__next__()
称为:检查循环计数器- 如果完成,则返回
StopIteration()
异常 - 如果还没有完成,请增加循环计数器并返回下一个项目
- 如果完成,则返回
生成器细节
生成器可以看作是迭代器的经过改进的版本,旨在由程序员以更直观的方式编写,并使用更少的代码。 生成器和迭代器之间存在细微的差异:
- 生成器是一种特殊的迭代器
- 他们定义了类似行为的函数
- 他们跟踪自己的内部状态(例如局部变量),这与迭代器不同
为了扩展最后一点,迭代器在每次循环时都以新状态开始-对
__next__()
的每次调用都是一个新的函数调用,需要自己设置并创建自己的状态。生成器不需要执行一次以上的设置-它可以重用上一次调用中的状态。 当运行相同的代码数千次时,这将变得更加高效。
在以下各节中,我们将介绍如何实现生成器,为什么从中受益于生成器以及一些可用代码示例。
进一步阅读:本文档中不涉及但与之紧密相关的高级主题包括:
- 将值发送到生成器(“
send()
”方法) - 连接生成器(
yield from
表达式) - 并发/并行处理/协程
在本文结尾处的一些参考文献中对它们进行了介绍,但是为了对所有这些概念进行很好的介绍,强烈建议特别使用 Dave Beazley 的PPT。
实现生成器
生成器函数
生成器函数可能是在 Python 中实现生成器的最简单方法,但与常规函数和循环相比,它们的学习曲线仍然稍高。
简而言之,生成器函数是一种特殊的函数,可以在运行时逐个产生其结果,而不必等待完成并立即返回所有结果。
它们很容易发现,因为您会注意到,使用关键字“yield
”返回值。 此处可以使用通常的“返回”,但只能退出该函数。
如果生成器没有设法“产生”任何数据,但是击中了“返回”,则调用者将返回一个空列表(
[]
)
以下是一些通常用于创建生成器函数的语法示例:
# Form 1 - Loop through a collection (an iterable) and apply some processing to each item
def generator_function(collection):
#setup statements here if needed
for item in collection:
#do some processing
return_value = apply_something_processing_to(item)
yield return_value
# Form 2 - Set up an arbitrary loop and return items that are generated by that - similar to range() function
def generator_function(start,stop,step):
#setup statements here
loop_counter = <initial value based on start>
loop_limit = <final value based on stop>
#might need to add one to limit to be inclusive of final value
loop_step = <value to increment counter by, based on step>
#step could be negative, to run backwards
while loop_counter != loop_limit:
#do some processing
return_value = generate_item_based_on(loop_counter)
#increment the counter
loop_counter += loop_step
yield return_value
# Form 3 - Illustrates return mechanism - imagine the processing we're doing requires some kind of setup beforehand, perhaps connecting to a network resource
def generator_function(collection):
#setup statements here if needed
setup_succeeded = do_some_setup()
if setup_succeeded:
for item in collection:
#do some processing
return_value = apply_something_processing_to(item)
yield return_value
else:
return
如注释中所述,第一种形式逐步遍历可迭代的项目集合,并对每个项目进行某种处理,然后再“yield
”。 虽然不是很令人兴奋,但对于简单的旧迭代当然可以实现,但是它的强大之处在于它可以成为其他生成器链的一部分。 由于每个项目需要更多的处理,生成器很快变得易于维护代码。
第二种形式是一个示例,可以适用于生成任意长度的值的序列,支持无限序列或无限循环序列等。 例如,该机制在处理数学或科学问题时非常有用。
这是调用生成器函数的一些示例语法:
#Form 1 - more readable
my_generator = generator_function(arguments)
for result in my_generator:
output(result)
#Form 2 - more concise
for result in generator_function(arguments):
output(result)
在第一种形式中,在第一条语句中设置了生成器,并将对它的引用存储在变量中。 然后由以下“for
”循环使用(或使用)它。
在第二种形式中,不存储生成器,而是由“for
”循环立即使用生成器。
实践中的生成器函数
这是说明创建和使用简单生成器函数的示例。 它将文本文件过滤为仅包含特定字符串的行。 它还显示了三种稍微不同的调用生成器的方式,有关详细信息,请参见注释:
#Example: Generator Functions
def filtered_text(text_lines,wanted_text):
""" Compares each line in text_lines to wanted_text
Yields the line if it matches """
for line in text_lines:
if wanted_text in line:
yield line
#slow method - read whole file into memory, then use the generator to filter text
#need to wait for the whole file to load before anything else can begin
#uses more memory
#not much benefit here!
with open("Programming_Books_List.txt",'r') as file_obj:
lots_of_text = file_obj.readlines()
matches = filtered_text(lots_of_text,"Python")
for match in matches:
print(match)
#faster method - use the file object as an iterator, filter it with the generator
#only needs to keep current line in memory
#current line is only read directly before use
#outputs each match directly after it is found (before the file has finished reading)
with open("Programming_Books_List.txt",'r') as file_obj:
matches = filtered_text(file_obj,"Python")
for match in matches:
print(match)
#sleeker method - this is doing the same as the faster method above, but in fewer lines of code
#instead of storing the generator object in a variable, it is immediately used in a for loop
#this is perhaps less readable, so it can be harder to debug
with open("Programming_Books_List.txt",'r') as file_obj:
for match in filtered_text(file_obj,"Python"):
print(match)
生成器表达式
生成器表达式是创建简单生成器函数的另一种方法。 这些趋向于更加简洁,通常导致单行代码,但并不总是像生成器函数那样可读。
它们的主要缺点是它们不如生成器函数灵活。 很难在生成器表达式中实现任何特别复杂的操作,因为您限于可以在单个表达式中轻松编写的内容。
一些示例语法可能会有所帮助:
#Form 1: basic form - iterate over all items, run some processing on each
new_generator = (apply_processing to(item) for item in iterable)
#Form 2: filter - rejects items if the condition is not true
new_generator = (item for item in iterable if condition)
#Form 3: combination of forms 1 and 2
new_generator = (apply_processing to(item) for item in iterable if condition)
它们看起来类似于列表推导,但实际上,列表推导会在返回之前将其整个输出构建到内存中的列表中,而生成器表达式一次只会返回其输出一项。
创建生成器后,即可使用与使用生成器函数几乎相同的方式来使用(或使用)生成器。 这里有一些示例:
#Form 1 - more readable
my_generator = (apply_processing to(item) for item in iterable)
for result in my_generator:
output(result)
#Form 2 - more concise
for result in (apply_processing to(item) for item in iterable):
output(result)
实践中的生成器表达式
这是上一本书的清单示例,使用表格 2 进行了覆盖:
#Example: Generator Expressions
with open("Programming_Books_List.txt",'r') as file_obj:
for match in (line for line in file_obj if "Python" in line):
print(match)
注意,仅需要这三行。 使用生成器函数执行此操作所需的最少行数为 7。 这要简洁得多,并能生成精美的代码,但不能在所有情况下都使用。
为什么要使用生成器?
在以下情况下,生成器特别有用:
- 对大量数据执行重复性任务,其中原始数据“仅需要一次”
- 在长数据序列上执行计算(可能适合或可能不适合内存-甚至可能是无限的!)
- 生成数据序列,其中仅应在需要时才计算每个项目(惰性求值)
- 对数据流中的多个项目执行一系列相同的操作(在管道中,类似于 Unix 管道)
在第一种情况下,如果数据本身不需要存储在内存中或再次引用,则生成器非常有效。 它们使程序员可以零碎地处理较小的数据块,并逐一产生结果。 程序绝对不需要保留先前迭代中的任何数据。
生成器带来的好处是:
- 减少内存使用
- 比其他迭代方法更快的速度和更少的开销
- 它们可以优雅地构造管道
在上一个书单搜索示例中,使用生成器并没有真正对性能或资源使用带来任何好处,因为这是一个非常简单的用例,并且源数据不是很大。 而且,所需的处理非常少,以致于可以使用其他方法轻松实现。
但是,如果每行所需的处理复杂得多怎么办? 也许是某种文本分析,自然语言处理或根据字典检查单词?
假设在该示例中,我们还希望获取每个书名,在十个不同的在线书店中进行搜索,然后返回最便宜的价格。 然后,让我们扩展源数据,例如我们将图书清单替换为 Amazon 可用的每本图书的副本。
在这样的规模下,问题变得如此之大,以至于传统的迭代将需要大量资源,并且相对难以以任何效率进行编码。
在这种情况下,使用生成器将大大简化代码,并且意味着可以在找到第一个书名后立即开始处理。 此外,即使处理非常大的源文件,开销也很小。
基本用法
无限序列
到目前为止,使用生成器生成斐波那契数列的一个相当陈词滥调的示例在计算机科学教学界是一个古老的偏爱,但仍然值得一看。
这是代码:
#Example: Fibonacci sequence using generators
def fibonacci(limit):
""" Generate the fibonacci sequence, stop when
we reach the specified limit """
current = 0
previous1 = 0
previous2 = 0
while current <= limit:
return_value = current
previous2 = previous1
previous1 = current
if current == 0:
current = 1
else:
current = previous1 + previous2
yield return_value
for term in fibonacci(144):
print(term)
其输出如下:
0
1
1
2
3
5
8
13
21
34
55
89
144
这是一个非常琐碎的用例,但它确实说明了一个事实,即生成器正在处理其本地变量中的前两个值的存储,并且这些值在迭代之间不会丢失。 没有其他数据被存储,因此该函数在整个生命周期(从第一个迭代到十万次迭代)中将使用几乎相同数量的内存。
高级用法
使用生成器作为管道
管道是可以看到生成器实际功率的地方。 它们可以通过简单地将生成器链接在一起来实现,以使一个生成器的输出传递到下一个生成器的输入。 当需要对一组数据依次执行多项操作时,它们非常有用。
正如 Dave Beazley 的精彩演讲(请参阅参考资料)中所指出的那样,系统程序员可以充分利用生成器。 比兹利没有产生任意的数学序列,而是演示了有用的技术,例如解析 Web 服务器日志,监视文件和网络端口,遍历文件和文件系统等等。
下面是我自己的示例,该示例混合使用了生成器函数和表达式来对多个文件执行文本搜索。 我将其设置为在当前目录的所有 Python 文件中搜索字符串“# TODO:
”。
每当我在代码中发现问题时,或者有一个以后想实现的想法时,我都想使用该表示法在尽可能靠近需要的地方插入待办事项。
它通常派上用场,但是在处理包含大量文件的大型项目时,这些说明可能会丢失!
这个示例有点费解,可以通过使用正则表达式(很有可能还有其他库或 OS 函数)进行改进,但是作为一个纯 Python 演示,它应该说明使用生成器可以实现的一些功能:
# pipeline_demo.py
#Example: Search for "# TODO:" at start of lines in Python
# files, to pick up what I need to work on next
import os
def print_filenames(filenames):
"""Prints out each filename, and returns it back to the pipeline"""
for filename in filenames:
print(filename)
yield filename
def file_read_lines(filenames):
"""Read every line from every file"""
for filename in filenames:
with open(filename,'r') as file_obj:
for line in file_obj:
yield line
#get a list of all python files in this directory
filenames_list = os.listdir(".")
#turn it into a generator
filenames = (filename for filename in filenames_list)
#filter to only Python files (*.py)
filenames = (filename for filename in filenames if filename.lower().endswith(".py"))
#print out current file name, then pop it back into the pipeline
filenames = print_filenames(filenames)
#pass the filenames into the file reader, get back the file contents
file_lines = file_read_lines(filenames)
#strip out leading spaces and tabs from the lines
file_lines = (line.lstrip(" \t") for line in file_lines)
#filter to just lines starting with "# TODO:"
filtered = (line for line in file_lines if line.startswith("# TODO:"))
#strip out trailing spaces, tabs and newlines
filtered = (line.rstrip() for line in filtered)
#display output
for item in filtered:
print(item)
# TODO: Write generator example
# TODO: Test on current folder
# TODO: Test on a line indented with spaces
# TODO: Test on a line indented with tabs
# TODO: Add more TODOs
输出:
test-TODOs.py
# TODO: Test finding a TODO in another file
test-noTODOs.py
pipeline_demo.py
# TODO: Write generator example
# TODO: Test on current folder
# TODO: Test on a line indented with spaces
# TODO: Test on a line indented with tabs
# TODO: Add more TODOs
这个想法可以进一步发展-如上所述,在系统编程中,甚至在系统管理空间中,都有无数的用例。 如果需要管理服务器上的日志文件,则这种技术将非常有价值。
请参阅以下参考资料,以获取有关该主题的更多信息和一些出色的示例。
您可以在 JBTAdmin Github 上获得与本文相关的所有代码。
参考书目/进一步阅读
标题:Python Wiki – 生成器
作者:多个
来源: Python Wiki
标题:Python Wiki – 迭代器
Authors: Multiple
来源: Python Wiki
标题:Python 生成器
作者:斯科特·罗宾逊
资料来源:滥用栈网站
标题:Python 实践手册 – 第 5 章。迭代器&生成器
作者:Anand Chitipothu
资料来源: Python 实践手册网站
标题:系统程序员的生成器技巧 – 版本 2.0
作者:David M. Beazley
资料来源: David Beazley 的网站
标题:Python 生成器的 2 大好处(以及它们如何永久改变了我)
作者:亚伦·麦克斯韦(Aaron Maxwell)
来源: O’Reilly 网站
下一篇文章
Hibernate 教程
Hibernate 框架基础
原文: https://javabeginnerstutorial.com/hibernate/hibernate-framework-basic/
什么是 Hibernate
Hibernate 是 Java 语言的对象关系映射(ORM)库,它提供了一个框架,用于将面向对象的域模型映射到传统的关系数据库。 Hibernate 通过使用高级对象处理功能代替与持久性相关的直接数据库访问,解决了对象关系阻抗不匹配的问题。
Hibernate 通过对象/关系映射促进了 Java 域对象的存储和检索。
Hibernate 功能
- 将 Java 类映射到数据库表&反之亦然
- 数据查询和检索工具
- 基于下划线数据库生成 SQL 查询。 并尝试使开发人员摆脱手工结果集处理和对象转换的麻烦。
- 使应用可移植到所有关系数据库。
- 通过提供不同级别的缓存(第一,第二和查询级别)来提高性能。
什么是方言
每个 SQL 供应商都有自己的一组受支持的语法。 这就是所谓的方言。 为了生成适当的 SQL 查询,Hibernate 需要知道需要生成哪个 DB 查询。 Hibernate 通过org.hibernate.dialect.Dialect
类及其子类为每个供应商完成此任务。
例如:
DB2:org.hibernate.dialect.DB2Dialect
MySQL:org.hibernate.dialect.MySQLDialect
什么是 HQL
HQL 是 Hibernate 查询语言的缩写。 这是 hibernate 提供的 SQL 启发式语言。 开发人员可以编写类似查询的 SQL 来处理数据对象。
连接池
Hibernate 具有自己的内部连接池,但它也支持某些第三方连接池供生产使用。
- c3p0 连接池
- Proxool 连接池
- 使用 JNDI 从应用服务器获得连接
Hibernate 关联
什么是 Hibernate 关联:数据库中的任何表都可以连接到同一数据库或其他数据库中的其他表。 这些表可以通过一些键(外键..)相互关联。 这种情况可以由关联处理。
例如:数据库中有 2 个表格,学生,和科目。 一个学生可以学习一个科目或一个以上的科目。 在这种情况下,每个学生在“学生”表中将只有一个条目,但“科目”表可能包含多个学生表中相应记录的记录。
关联映射用于将 Java 对象映射到 DB 表。
映射中涉及的实体:
- 类(持久性)(POJO)
- 数据库表
- 映射文件(
.hbm
)
与 POJO 相关的规则:
- POJO 类需要具有默认构造器,这是 Hibernate 所需的。
- 延迟加载不适用于最终类。 最好具有非最终类。
关联类型
单向关联:
- 多对一
- 一对一
- 一对多
与联接表的单向关联
- 多对一
- 一对一
- 一对多
- 多对多
双向关联
- 一对多/多对一
- 一对一
与联接表的双向关联
- 一对多/多对一
- 一对一
- 多对多
Hibernate 缓存
缓存是提高系统性能的一种方法,因为它可以减少对数据库的查询。 查询数据库始终会对性能产生影响,因为它是 I/O 操作。 而且 I/O 操作比仅使用应用内存的操作要慢得多。 有两种类型的缓存。
- 一级缓存(实体缓存)
- 二级缓存(查询缓存)
单击此处了解有关 Hibernate 缓存教程的更多详细信息。
在下一章中,我们将更详细地介绍这些科目。
Hibernate 4 入门教程
原文: https://javabeginnerstutorial.com/hibernate/hibernate-4-introduction/
Hibernate 4 简介
在本文中,我将向您展示如何使用 Hibernate 4 和一个简单的示例应用来尝试您所了解的内容。
什么是 Hibernate,我为什么要关心?
创建 Hibernate 是为了利用 Java 应用和关系数据库之间的连接,因为很难在数据库表和 Java 对象之间来回映射。
而且由于 Hibernate 这样做,它减少了 JDBC 查询执行和数据映射所消耗的开发时间。
获取 Hibernate
要获取最新版本的 Hibernate,只需访问此站点。 对于本文,我将使用4.3.10.Final
版本。
如果下载并解压缩了包,则可以在lib
文件夹中看到一些子文件夹。 使用 Hibernate 的任何项目都需要required
下的所有内容。 其他文件夹包含特殊情况的库。 例如,在jpa
下,您可以找到提供 JPA 实体管理器支持的库。
另外,您可以设置一个 Maven 项目并将 Hibernate 添加为依赖项。 在这种情况下,您无需担心 Hibernate 的其他必需依赖关系,这些依赖关系在捆绑下载的必需包中附带。 使用 Maven 更简单明了,因此我将使用 Maven 作为依赖项管理。
一个简单的示例
在简单的示例中,我将创建一个 Java 应用,该应用将有关书籍的信息存储在数据库中。 为了简单起见,该数据库将是 H2 内存数据库。
依赖关系是使用 Maven 管理的,输出是具有所有依赖关系的可执行 JAR。
实体
我将存储在数据库中的实体如下:
package example;
public class Book {
private String isbn;
private String title;
private String author;
Book() {
}
public Book(String isbn, String title, String author) {
this.isbn = isbn;
this.title = title;
this.author = author;
}
public String getIsbn() {
return this.isbn;
}
public String getTitle() {
return this.title;
}
public void setTitle(String title) {
this.title = title;
}
public String getAuthor() {
return this.author;
}
public void setAuthor(String author) {
this.author = author;
}
public void setIsbn(String isbn) {
this.isbn = isbn;
}
}
无参数构造器是所有持久类的必需条件,因为 Hibernate 每次反射都会创建对象实例。 在这种情况下,此构造器是私有的,以防止创建没有信息的书籍。
依赖项
为了使应用运行,我们在项目中需要两个依赖项:Hibernate 和 H2。 为此,将以下内容添加到pom.xml
中:
<dependencies>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>4.3.10.Final</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.187</version>
</dependency>
</dependencies>
现在我们准备继续。
配置 Hibernate
Hibernate 需要一些配置才能开始。 您需要将其包含在hibernate.cfg.xml
文件中。 它是普通的旧 XML。 它包含数据库连接属性和实体映射文件的包含位置。
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<!-- Database connection settings -->
<property name="connection.driver_class">org.h2.Driver</property>
<property name="connection.url">jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1;MVCC=TRUE</property>
<property name="connection.username">sa</property>
<property name="connection.password"/>
<!-- JDBC connection pool (use the built-in) -->
<property name="connection.pool_size">1</property>
<!-- SQL dialect -->
<property name="dialect">org.hibernate.dialect.H2Dialect</property>
<!-- Disable the second-level cache -->
<property name="cache.provider_class">org.hibernate.cache.internal.NoCacheProvider</property>
<!-- Echo all executed SQL to stdout -->
<property name="show_sql">true</property>
<!-- Drop and re-create the database schema on startup -->
<property name="hbm2ddl.auto">create</property>
<mapping resource="hibernate_example/hbm/Book.hbm.xml"/>
</session-factory>
</hibernate-configuration>
实体映射
为了将正确的字段映射到数据库中的正确列,Hibernate 需要实体的映射文件。 这些文件位于以实体名称开头的.hbm.xml
文件中。 在此示例中,Book.hbm.xml
。
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="hibernate_example">
<class name="Book" table="BOOKS">
<id name="isbn" type="string"/>
<property name="title"/>
<property name="author"/>
</class>
</hibernate-mapping>
main
方法
要在 Hibernate 中使用该应用,我们仍然需要一个入口点—在 Java 中,这是main
方法。 首先,我们需要进行一些配置,例如使用会话工厂创建会话。因此,让我们看一下代码的运行方式:
public class Main {
public static void main(String[] args) {
final Configuration configuration = new Configuration().configure();
final StandardServiceRegistryBuilder builder = new StandardServiceRegistryBuilder().applySettings(configuration.getProperties());
final SessionFactory factory = configuration.buildSessionFactory(builder.build());
final Session session = factory.openSession();
final Book book = new Book("93939398948 ", "Java 8", "Author");
session.beginTransaction();
session.save(book);
session.getTransaction().commit();
final List<Book> books = session.createCriteria(Book.class).list();
System.out.println("\n----\n");
System.out.println(MessageFormat.format("Storing {0} books in the database", books.size()));
for (final Book b : books) {
System.out.println(b);
}
System.out.println("\n----\n");
session.close();
factory.close();
}
}
运行该应用后,控制台应具有一些日志消息,并且将一本书添加到数据库中:
Storing 1 books in the database
Java 8 by JBT (ISBN:9393939894
—–
总结
Hibernate 提供了一个很好的功能,可以利用 Java 对象和关系数据库之间的映射。 当然,该示例应用并没有显示 Hibernate 的全部功能:为了获得更好的用户体验,您可以添加一个用户界面来在应用中创建和列出书籍。
在下一篇文章中,我将展示如何摆脱 XML 配置(某些开发人员称为“XML 地狱”)并改为使用注解。 因此,请继续关注。
您可以在此处找到并下载应用的源码。
Hibernate 4 注解配置
原文: https://javabeginnerstutorial.com/hibernate/hibernate-4-annotations-configuration/
在最后的介绍性文章中,我提到了所谓的“XML 地狱”,它是 XML 在 Hibernate 配置中的大量使用。 在本文中,我将介绍基于注解的配置,您可以在其中使用 Hibernate 在实体上的注解来减少所需的 XML 数量。
为什么注解很好
正如您将在示例代码中看到的那样:使用注解,您可以在实体类定义本身中看到属性映射是什么,因此您无需查找正确的.hbm.xml
文件即可查找映射定义。
而且有一个很好的副作用:您只需要修改实体。 例如,如果要向Book
类添加新的Date
字段,则还需要在Book.hbm.xml
文件中添加映射。 使用注解,这只是 Java 类中的更改。 您将在本文后面的示例中找到有关使用日期的示例。
一个示例
在此示例中,我将继续使用第一篇文章中介绍的Book
实体。 现在,我将转换 POJO 以使用注解而不是 XML 配置-在本系列的后续部分中,我将继续介绍。 这是因为注解比 XML 更易于阅读和查找。
实体
对于实体,我需要添加一些注解以表示与 XML 文件中相同的映射。
第一个是告诉 Hibernate Book
类是一个实体。 我们可以通过@Entity
注解来实现。 否则,Hibernate 将在启动时引发异常,并显示错误消息“hibernate_example.Book
”是未知实体。
下一个是表定义。 我将书添加到BOOKS
表中。 如果没有明确的表定义,Hibernate 会将实体添加到 BOOK
表中。 通常,我可以说没有表定义,实体将保存在与实体类相同名称的表中。 要命名表,我必须在类上使用@Table
。 该注解具有名为名称的参数。 此名称定义表的名称-因此我添加了此属性并将其命名为 BOOKS
。
最后,我将 ISBN 定义为实体的 ID。 这是通过@ID
注解完成的。
让我们看看整个修改后的实体:
@Entity
@Table(name = "BOOKS")
public class Book {
@Id
private String isbn;
private String title;
private String author;
// other parts stayed the same so I omit them here
}
配置
配置文件需要进行一次修改:我必须在最后将映射标记从使用资源更改为类,并提供映射类的限定名称 :
<hibernate-configuration>
<session-factory>
<!-- Database connection settings -->
<property name="connection.driver_class">org.h2.Driver</property>
<property name="connection.url">jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1;MVCC=TRUE</property>
<property name="connection.username">sa</property>
<property name="connection.password"/>
<!-- JDBC connection pool (use the built-in) -->
<property name="connection.pool_size">1</property>
<!-- SQL dialect -->
<property name="dialect">org.hibernate.dialect.H2Dialect</property>
<!-- Disable the second-level cache -->
<property name="cache.provider_class">org.hibernate.cache.internal.NoCacheProvider</property>
<!-- Echo all executed SQL to stdout -->
<property name="show_sql">true</property>
<!-- Drop and re-create the database schema on startup -->
<property name="hbm2ddl.auto">create</property>
<!-- The mapping information of entities -->
<mapping class="hibernate_example.Book"/>
</session-factory>
</hibernate-configuration>
映射文件
可以删除映射文件,因为 Hibernate 不再使用它了。
扩展实体
前面我提到过,添加日期字段有点复杂,因为映射日期并不是那么简单的动作。 为此,我需要在字段上使用@Temporal
注解,以告知 Hibernate 我要将日期存储在数据库中,并且当我读取数据库时希望返回日期信息。 javax.persistence.TemporalType
告诉我要存储哪种信息。 现在是日期。
@Temporal(TemporalType.DATE)
private Date published;
对于其他正在阅读您的代码的开发人员来说,这很清楚,已将字段published
映射为应用和数据库之间的 Date 类型。
对于 XML 配置,我将不得不使用以下配置:
<property name="date" type="date" />
运行应用
该应用也可以像上一篇文章中那样运行,并且结果相同。
总结
Hibernate 可以利用一组标准注解来消除映射 XML 文件的需要。 通过使用标准javax.persistence
注解,您可以从 Hibernate 切换到另一个实现标准接口的 ORM 解决方案。
但是,如今有些开发人员正在谈论“注解地狱”,因为您几乎可以使用注解配置任何内容,有时这会使应用的可读性变差。
在下一篇文章中,我将向您展示如何使用 Hibernate 建模实体之间的关系。
代码下载
您可以从 Github 此处下载特定章节的代码。
Hibernate 4 的实体关系
原文: https://javabeginnerstutorial.com/hibernate/entity-relations-with-hibernate-4/
上次我介绍了注解而不是 XML 配置。 现在,我将更深入地研究并展示如何创建实体关系并将其映射到数据库。
如果在示例中查看图书实体,您可能会想到:“为什么将图书的作者存储为字符串?” 你是对的。 这使得作者查询书籍几乎是不可能的,或者至少没有表现得那么出色。 而且两次输入之间的错别字会使情况更糟。
在本文中,我将进一步举例说明,然后将作者的字符串提取到另一个实体中。
作者实体
让我们从一个拥有作者信息的新实体开始。 为简单起见,它将仅包含作者的姓名和作者创建的书籍列表。
@Entity
@Table(name = "AUTHORS")
public class Author {
@Id
private String name;
@ManyToMany(mappedBy = "authors")
private final List<Book> books = new ArrayList<>();
private Author() {
}
public Author(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public List<Book> getBooks() {
return this.books;
}
@Override
public String toString() {
return MessageFormat.format("{0} has written {1} book(s).", this.name, this.books.size());
}
}
如您所见,这里有一个新的注解:@ManyToMany
。 这告诉 Hibernate Author
实体映射到其他Book
实体。 mappingBy
属性用于告诉 Hibernate 哪个实体是关系的所有者。 这主要用于一对多或多对一关系中。
将作者添加到Book
实体
现在,将作者字符串更改为作者列表。 它看起来与Author
实体完全相同:
@Entity
@Table(name = "BOOKS")
public class Book {
@Id
private String isbn;
private String title;
@ManyToMany
private final List<Author> authors = new ArrayList<>();
@Temporal(TemporalType.DATE)
@Column(name = "PUBLISHED_DATE")
private Date published;
private Book() {
}
public Book(String isbn, String title, Date published) {
this.isbn = isbn;
this.title = title;
this.published = published;
}
public String getIsbn() {
return this.isbn;
}
public String getTitle() {
return this.title;
}
public void setTitle(String title) {
this.title = title;
}
public List<Author> getAuthors() {
return this.authors;
}
public void setIsbn(String isbn) {
this.isbn = isbn;
}
public Date getPublished() {
return this.published;
}
public void setPublished(Date published) {
this.published = published;
}
@Override
public String toString() {
final String authorNames = this.authors.stream().map(Author::getName).collect(Collectors.joining(", "));
return MessageFormat.format("{0} by {1} (ISBN: {2}), published {3}", this.title, authorNames, this.isbn, this.published);
}
}
在这里,我没有在@ManyToMany
注解中添加任何参数,因为作者是所有者。
关系类型
当然,多对多关系不是唯一可用的关系。 如果需要,您也可以选择一对一,一对多或多对一。
一对一关系
对于一个实体,此关系类型仅包含一种引用类型,而对于另一实体也是如此。 在这种情况下,您可以选择用于存储引用的实体。例如,如果我将 ISBN 提取为具有其他一些属性的单独实体,则可能是书籍与 ISBN 号之间的关系:一本书具有一本 ISBN 和一本 ISBN 准确地指一本书。
一对多和多对一关系
在这种情况下,一个实体在其他实体中具有许多引用。 如果一本书只能有一位作者,那就是这种情况。 在这种情况下,引用 ID 将存储在BOOKS
表中,因为有一本书参考了其作者。 我们在作者实体中使用一对多,在Book
实体中使用多对一。
多对多关系
如您在第一个示例中所看到的,这种关系有点复杂。 在这里,我们需要一个单独的表格来包含书籍和作者之间的引用。 这就是所谓的联接表。 该表由 Hibernate 自动维护,但是您可以告诉该表如何命名。 如果您不选择名称,则 Hibernate 会使用实体名称,并用下划线将其分开,所有者名称为站点实体。 在此示例中,联接表名为:BOOKS_AUTHORS
。
更改main
方法
由于实体已更改,因此我也必须更改Main
类的main
方法。
final Book book = new Book("9781617291999", "Java 8 in Action", new Date());
session.beginTransaction();
Arrays.stream("Raoul-Gabriel Urma,Mario Fusco,Alan Mycroft".split(",")).map(name -> new Author(name)).forEach(author -> {
author.getBooks().add(book);
book.getAuthors().add(author);
session.save(author);
});
session.save(book);
session.getTransaction().commit();
如您所见,我使用一些 lambda 来动态创建作者。 有趣的部分是最后一个语句,forEach
块:我将这本书添加到当前作者的图书列表中,然后将当前作者添加到这本书的作者列表中。 以后需要在数据库中一起查找书籍和作者时,需要使用此引用(如果您手动查询或使用 Hibernate 加载数据集)。
现在的结果与上次有所不同:
----
Storing 1 books in the database
Java 8 in Action by Raoul-Gabriel Urma, Mario Fusco, Alan Mycroft (ISBN: 9781617291999), published 2015.06.29\. 16:57
----
如果我放弃声明book.getAuthors().add(author);
,则结果不包含作者的姓名:
----
Storing 1 books in the database
Java 8 in Action by (ISBN: 9781617291999), published 2015.06.29\. 16:58
----
总结
如您所见,有很多选项可以将实体关系映射到规范化数据库。 下次,我将向您展示如何将实体继承映射到数据库。
代码下载
您可以从 Github 此处下载特定章节的代码。
Hibernate 4 中的实体继承模型
原文: https://javabeginnerstutorial.com/hibernate/entity-inheritance-models-in-hibernate-4/
上一次我们研究了将实体关系映射到数据库。 现在该关注主要的对象关系:继承。
如您所知,对象可以彼此继承,以将通用函数移到超类中,并且子代中只有不同部分。 使用 Hibernate 也可以实现相同的目的:您可以将通用函数分组到父类中,并且仅将差异添加到子类中,还可以将此关系映射到数据库。
这个示例
我将在书本中继续使用该示例-但这意味着该示例现在很难理解,但我尝试使其对您来说更舒适。
人,作者和出版者
首先,我考虑了书本继承法,但最后得出了一个简单的人继承模型,该模型既可以是作者又可以是出版者。 每个人都有一个名字(这是主要标识符)和一个电子邮件地址。
作者维护一份书面书籍清单以及其出版者的参考文献(每个作者只能有一个出版者)。 出版商具有税号和其作者列表。
单表继承
第一个继承模型是单个表。 它将每个子类(顾名思义)存储在单个数据库表中。 这意味着您不能在子类的必需列上应用数据库级别的约束,因为其他子类没有此字段,这将引发完整性违例异常。
要告诉实体结构使用单表继承,请在基本实体的定义上添加以下代码:
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
为了区分存储在单个表中的实体类型,Hibernate(通常是 JPA)使用区分符列。 如果未提供区分符列的定义,则 Hibernate 将使用名为DTYPE
的字符串列,其长度为 31 个字符。
鉴别符列的默认值是实体的名称,因此在此示例中有两个鉴别符:author
和Publisher
。
通过使用@DiscriminatorColumn
和@DiscriminatorValue
,您可以覆盖这些默认值以适合您的架构和要求。
连接继承
第二种类型是连接继承。 在这种情况下,子类以 ID 作为公共标识符存储在单独的数据库表中,在这里,Hibernate 可以将请求的子代与基类一起连接。
要创建联接继承,只需将以下内容添加到父类定义中:
@Inheritance(strategy = InheritanceType.JOINED)
每个类的表继承
第三种类型是每个类模型的表,其中每个实例都有自己的表,表中包含所有信息。 这意味着很大的冗余,并且违反了数据库规范化规则,因为每个实例的父类的所有信息都存储在单个表中。
示例作者存储name
,emailAdress
和PublisherId
,而发布者存储name
,emailAdress
和taxId
。
并且没有为抽象类创建用于存储人员的表,因为 Hibernate 知道AbstractPerson
实体是抽象*,因此它不需要任何表来存储信息。
每个类的表继承可以与超类的以下注解一起使用:
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
使用哪一个?
这取决于您的应用。 但是,我从未见过任何应用在应用中使用按类分配表的策略,只有连接表或单个表。 有时大的继承树会导致由 Hibernate 创建的大联接查询,有时比单表解决方案要慢。
但是,有时更好的方法是跳过 Hibernate 并手动创建查询,以加快结果收集的速度,并避免 Hibernate 对数据结构及其如何收集信息的假设。
总结
继承策略对于通过维护的面向对象设计将实体建模到数据库是很好的。 但这会导致来自 Hibernate 的查询很大且很慢,但这并不意味着您应该避免使用它们。
在下一篇文章中,我将向您展示如何使用 HQL 和 JPQL 编写自己的 Hibernate 查询,因为main
方法中的当前查询不是最好的。
代码下载
您可以从 Github 此处下载特定章节的代码。
Hibernate 4 查询语言
原文: https://javabeginnerstutorial.com/hibernate/hibernate-4-with-query-languages/
在介绍了映射关系和继承的 Hibernate 4 入门之后,让我们更深入地了解如何获取数据库中存储的数据。
如果查看示例应用的main
方法,您会发现我正在使用从会话中生成的简单标准查询来列出和计数数据。 加载所有数据以对它们进行计数会过多,并且会占用太多内存。 还有其他方法,让我向他们展示。
HQL 和 JPQL
HQL 是 Hibernate 的默认查询语言(缩写为 Hibernate 查询语言)。 JPQL 用于更通用的 Java 持久性查询语言,它是 HQL 的子集。 这意味着有效的 JQPL 查询也是有效的 HQL 查询-但并非总是如此。
HQL 背后的主要思想是,您可以在查询中使用实体名称,而不是数据库中的表名称。 这使存储和查询更加透明,您可以在开发人员之间划分任务,因为编写查询的人只需要知道实体名称即可。
简单 HQL 查询是一个选择查询:
from Book
是的,您可以在仓库中看到这是一个有效的查询。
如果提及实体,则可以使用其简单名称或实体的限定名称(具有整个包结构)。
除了选择,JPQL 和 HQL 允许更新和删除语句。 HQL 也另外提供插入语句。
关于区分大小写的评论
HQL 和 JPQL 中的关键字不区分大小写,这意味着SELECT
,select
和SeLeCt
的含义相同。 但是实体名称区分大小写: hibernate_example.Book
与hibernate_example.BOOK
不同。
关于类型安全的评论
HQL 和 JPQL 是非类型安全的查询。 条件查询提供了一种类型安全的方法。
类型安全性是什么意思? 您不必从查询中将对象返回值转换为所需的实体对象。 这就是为什么使用 Hibernate 的默认查询机制时会收到编译时警告的原因。
创建和执行查询
幸运的是,创建和执行查询不需要特定的库,可以通过我们在主方法中已经拥有的会话对象来完成。
为了将查询与main
方法分开,我添加了信息库类,在其中实现了一些静态函数来查询数据库。
因此,让我们看一下查询创建的不同版本。 它们都具有相同的根:会话对象,但是它们是通过调用不同的方法执行的。
查询是执行查询的最简单方法,也是最常见的查询方法:
session.createQuery("from Book").list();
使用条件可以通过编程方式限制结果集(例如,您需要更少的查询输入,并且可以在调试时轻松注释掉条件行):
session.createCriteria(Author.class).add(Restrictions.like("name", "M%")).list();
上面的查询返回名称以字母M
开头的所有作者。
本机查询
有时,编写本机查询会很方便,因为您需要一些仅在您所使用的数据库中可用的特定方法。 幸运的是,Hibernate 也支持本地查询。
本机查询是普通的旧 SQL,您必须在其中使用表名而不是实体名。
区别在于结果类型。 例如,使用 SQL 查询对对象计数会使用 HQL 查询返回BigInteger
,而我们会返回 Long
。 因此,更改查询时间并不总是透明的,并且会在您的应用中引起一些讨厌的错误。
为什么本地查询会更好? 因为您可以消除 Hibernate 本质上执行的一些连接。
例如,对带有 Hibernate 的Authors
进行计数的查询最终会导致以下 SQL:
select count(*) from Author a inner join PERSONS p on a.name = p.name
但是,我们知道数据结构,因此可以说消除了连接:
return (BigInteger) session.createSQLQuery("select count(*) from author").uniqueResult();
缺点自然是我们失去了透明度,并且更改了继承模型以适应查询。
总结
HQL 和 JPQL 是 Hibernate 附带的本机查询语言。 无需任何先决条件即可使用它们。 但是,它们是非类型安全的,这意味着您必须将结果转换为相应的类型,如果无法完成转换,则可能导致运行时异常。
在本文中,我没有显示标准查询,参数化的和准备好的语句-但我将在以后的文章中进行介绍。
代码下载
您可以从 Github 此处下载特定章节的代码。
Hibernate 4 数据库配置
原文: https://javabeginnerstutorial.com/hibernate/hibernate-4-database-configuration/
在之前的文章中,我向您展示了如何入门 Hibernate 4:创建实体,管理关系和继承以及如何查询存储的数据。
但是,每次使用内存数据库时,这意味着每次停止应用时,插入的数据都会消失。
现在是时候向您展示如何切换到实际保留数据的另一个数据库,以后您可以检索它,以及必须重新配置以使事情按预期工作的方式。
变更数据库
我将在本文中的示例中使用本地 H2 数据库,但是,我将向您展示要在 Hibernate 中使用流行的关系数据库时需要更改的内容。
配置上的唯一区别是要使用的 Hibernate 驱动程序。 方言是 Hibernate 从其内部关键字(例如,注解)到数据库特定命令的映射。
在哪里更改
更改将在src/main/resources/
下的hibernate.cfg.xml
文件中进行。 在这里,您必须在<!– 数据库连接设置 –>
注解下的块中设置字段。
这意味着以下参数:
- 正确的数据库驱动程序的
driver_class
- 网址指向正确的数据库
- 合适用户的用户名
- 密码和正确的密码(对于 H2,大多数情况下未设置)
- 方言让 Hibernate 知道如何将内部关键字映射到数据库命令
Hibernate 方言
方言告诉 Hibernate 如何根据所连接的数据库版本来映射内部关键字,因为某些关键字在数据库的较新版本中是新的-否则它们会随时间而变化。
但是,Hibernate 是一个具有一些可选配置选项的工具。 大多数时候,Hibernate 从驱动程序的版本中知道要使用哪种方言。 因此,您可以省略方言配置-Hibernate 仍然可以使用。 如果遇到问题,请尝试传递正确的方言。
一些感兴趣的驱动程序
我将在这里列出一些常用数据库的驱动程序。 但是,这意味着在运行应用时,您也需要在类路径上使用驱动程序。
- MySQL5:用于 InnoDB 表的
hibernate.dialect.MySQL5InnoDBDialect
,用于其他的org.hibernate.dialect.MySQL5Dialect
。 - Oracle:9i 及更高版本的
jdbc.OracleDriver
- MS SQL Server:
microsoft.sqlserver.jdbc.SQLServerDriver
- PostgreSQL:
postgresql.Driver
这些是最常用的关系数据库。 要将这些驱动程序放在示例项目中的类路径上,只需将正确的 JAR 导入为项目的pom.xml
中的依赖项即可。
更新示例
现在该切换到永久性存储了。 在该示例中,我将使用 H2,因为它已经作为依赖项存在,并且与其他数据库不同,它不需要安装和配置,因此您也无需执行任何操作。
为了获得持久的 H2 存储,我只需将connection.url
从jdbc:h2:mem:db1;…
更改为jdbc:h2:file:./example;…
。 如您所见,区别在于使用文件而不是内存告诉 H2 它必须查找文件,并且该文件位置需要路径。
如果您的example.db
文件在本地不存在,请不要担心:如果缺少该文件,H2 将创建新的数据库文件。
我更改了应用,以在运行应用时将每个元素再次添加到数据库中。 这意味着,如果您运行该应用三次,您将在数据库中看到 3 本书-尽管它们具有相同的参数(当然 ID 除外)。 这应该给您一个简短的主意,即如何在应用后面将持久性存储与 Hibernate 4 一起使用。
总结
更改数据库并不难。 您只需更改驱动程序和连接参数即可。
代码下载
您可以从 Github 此处下载特定章节的代码。
Hibernate 4 批处理
原文: https://javabeginnerstutorial.com/hibernate/batch-execution-with-hibernate-4/
在本文中,我将为您简要介绍 Hibernate 批处理。
为什么要批处理?
因为这比打开一个事务向数据库插入 1000000(一百万)个条目并最后提交或为每个相同容量的插入打开事务要好。
批处理为您提供了管理此工具的正确工具:在 Hibernate 自动调用commit
之后定义一个限制,以将您的数据持久存储在应用后面的数据库中。
朴素的方法
public static void naiveApproach() {
final Configuration configuration = new Configuration().configure();
final StandardServiceRegistryBuilder builder = new StandardServiceRegistryBuilder().applySettings(configuration.getProperties());
final SessionFactory sessionFactory = configuration.buildSessionFactory(builder.build());
final Session session = sessionFactory.openSession();
final Transaction tx = session.beginTransaction();
for (int i = 0; i < 1_000_000; i++) {
final Book book = new Book("9781617291999", "Java 8 in Action", new Date());
session.save(book);
}
tx.commit();
session.close();
}
上面的代码做了我一开始所描述的:它打开一个事务并在插入 1000000 条记录后保存。 根据您的硬件,您可以到达循环的结尾并在结尾处调用一次提交。 如果您的内存不足,您的应用可能会抛出OutOfMemoryException
,因为 Hibernate 将所有新的Book
实例存储在内存中的第二级缓存中。
为了进行测试,我将插入次数增加到 10000000(一千万),以查看应用崩溃之前需要花费多长时间。 凭借近三百万本新书,我达到了应用的 2GB 内存。
这意味着,如果二级缓存消耗了应用可用的所有内存,则数据库条目将消失,您可以重新开始。
设置批处理大小,将二级缓存保持在较低水平
为了使二级缓存的大小保持较低,您可以在hibernate.cfg.xml
中引入批处理大小。 这将告诉 Hibernate 容器每隔 n 行要批量插入。 可以使用属性hibernate.jdbc.batch_size
设置批处理大小。
有趣的是,Hibernate 的文档不仅引入了此属性,而且还需要修改上面的朴素代码(我只会复制相关的for
循环):
for (int i = 0; i < 1_000_000; i++) {
final Book book = new Book("9781617291999", "Java 8 in Action", new Date());
session.save(book);
if(i % 50 == 0) { // 50 is the batch_size
session.flush();
session.clear();
}
}
如果仔细看一下上面的代码,您会看到解决方案是将批处理大小大块中的会话flush()
和clear()
保持为较低的二级缓存大小。
那么为什么要设置批处理大小?
很好的问题,我已经搜索了 Hibernate 的文档以找到有关此信息,但未找到任何信息。 但是,如果考虑到这一点,批处理可以通过将一堆语句组合在一起,从而使数据库有效地执行插入和更新语句。
批次大小无法保证
设置批处理限制并不能保证仅由于将数据刷新到数据库而使第二级缓存大小保持较小。
但是,还有一些透明的限制,您最终将看不到。
一个示例是如果您使用GenerationType.IDENTITY
,则 Hibernate 透明地禁用批处理。
第二个示例是 Hibernate 查看要一起批处理的语句:如果当前语句与前一个相同,则在未达到batch_size
的情况下将它们合并在一起。 在上面的示例中,语句相似,因此将它们批处理在一起。 但是,如果我们将“作者”和“书籍”一起添加到数据库中,则 Hibernate 会看到交替的语句,并且将从每个语句开始一个批处理组。 要解决此问题,可以使用hibernate.order_inserts
和hibernate.order_updates
属性。 这使 Hibernate 在插入之前对语句进行了排序,因此可以看到 50 个Book
插入可以一起批处理,并且 50 个Authors
可以一起批处理。
手动保存数据
我们已经解决了消耗大量内存的问题,但是插入时的异常又如何呢? 在大多数情况下,由于最后一个失败而回滚一百万次插入是不可行的。
解决方案是手动调用事务的提交以及批处理大小:
for (int i = 0; i < 1_000_000; i++) {
final Book book = new Book("9781617291999", "Java 8 in Action", new Date());
session.save(book);
if(i % 50 == 0) { // 50 is the batch_size
session.flush();
session.clear();
session.getTransaction().commit();
session.beginTransaction();
}
}
如果我们达到了batch_size
,则上面的代码将提交事务,并在会话上开始新事务,因为前一个事务已因提交而失效。
总结
批处理仅有助于将数据高效地存储在数据库中。 如果要使用某种故障转移机制,则需要实现手动提交策略。
代码下载
Hibernate 4 缓存
原文: https://javabeginnerstutorial.com/hibernate/caching-with-hibernate-4/
如果您有更大的应用,则应考虑性能以及如何提高性能。 缓存是执行此操作的一种方法,因为它可以减少对数据库的查询。 查询数据库始终会对性能产生影响,因为它是 I/O 操作。 而且 I/O 操作比仅使用应用内存的操作要慢得多。
缓存在您的应用和数据库之间起作用,以尽可能避免数据库命中次数。
实体缓存
默认情况下,Hibernate 使用第一级缓存,这意味着它将通过会话将实体存储在应用的内存中(为 Hibernate 分配)。 它是所有实体都必须传递的强制性缓存。 这有两个好处:如果经常访问该实体,则 Hibernate 会记住它;如果您的应用再次需要该实体,则将从会话的缓存中返回它; 第二个好处是,如果您在一个实体上执行多个更新,则 Hibernate 尝试将这些更新分组在一起,并延迟实际的数据库调用以减少 I/O 流量。 如果关闭会话,则存储在第一级缓存中的对象将被销毁并保存或更新到数据库。
您可以使用可选的二级缓存来扩展此缓存。 一级始终是强制性的,始终会首先被咨询。 二级缓存用于跨会话缓存对象。 对于二级缓存,有一些第三方解决方案可以与 Hibernate 一起使用。 Hibernate 提供了org.hibernate.cache
。 供应器必须实现CacheProvider
接口,以使 Hibernate 处理缓存。
市场上有一些缓存供应器,但是 Hibernate 希望您为整个应用选择一个供应器。
让我提及一些缓存供应器:
- 高速缓存
- OS 缓存
- Infinispan
在上面提到的那些之外,EHCache 是最流行和广泛使用的。
缓存策略
使用二级缓存时,必须记住一些缓存策略:
- 只读此策略应用于永远不会更新的持久对象。 这对于读取和缓存应用配置以及其他静态数据非常有用。 这是具有最佳性能的最简单的策略,因为没有重载可以检查是否在数据库中更新了实体。
- 读写此策略对由应用更新的实体很有用。 但是,如果数据是通过数据库或其他应用更新的,则 Hibernate 无法判断是否发生了更改,并且您的数据可能已过时。
- 非受限读写如果应用仅偶尔更新数据并且不需要严格的事务隔离,则此缓存策略可能是合适的。
- 事务性此缓存策略为完全事务性缓存供应器(例如 JBoss TreeCache)提供支持。 这样的缓存只能在 JTA 环境中使用,并且必须指定
transaction.manager_lookup_class
。
也不奇怪:EHCache 支持上述所有这四种策略,因此是开始使用二级缓存供应器的不错选择。
查询缓存
另外,对于实体,您也可以将查询存储在缓存中。 但是,此查询缓存与二级缓存紧密配合,因此,如果您使用二级缓存,则这只是一种参加的方式。
要使用查询缓存,还需要两个额外的缓存区域:一个用于缓存的查询结果,另一个用于在最后更新表时的时间戳。
仅当您具有经常使用相同参数运行的查询时,才使用查询缓存。
总结
要对应用性能产生积极影响,第一步是在数据库和应用之间使用一些缓存。 Hibernate 提供了一个简单的内存缓存,称为一级缓存,由于它是强制性的,因此您无需启用或配置它。
但是,您可以使用二级缓存扩展此功能,该缓存可跨应用的会话缓存对象。 如果您使用的是二级缓存,则可以通过通常使用相同参数调用的缓存查询来扩展缓存实体。
Hibernate 4 审计
原文: https://javabeginnerstutorial.com/hibernate/auditing-with-hibernate-4/
在本文中,我将向您简要介绍使用 Hibernate 进行实体审计,或更具体地说,是使用 Hibernate Envers,因为这是 Hibernate 核心的 3.x 版本的一部分。
顺便说一句:Envers 保留用于实体版本控制。
我为什么要关心审计?
如果您从事较大的项目(或将来某个时候),则审计将在您的应用中扮演重要角色,以监视谁更改了敏感数据。 有时仅监视谁进行了更改是不够的:您想知道已更改的内容以及哪些值也已更改。
为此,您可以创建自己的自定义解决方案或使用 Hibernate Envers,它是 Hibernate 核心的一部分,提供了开箱即用的功能。
仅提及一个场景:您是汽车零售应用的开发人员。 有人出售一辆豪华轿车的正常价格要低 80% 的汽车,却没人知道谁改变了价格。 最后,您会感到后悔,因为您没有监视,因此必须在很短的时间内解决此问题。
如果听起来很绝望,并且您想袖手旁观,请继续阅读。
Hibernate Envers 的易用性
要使 Hibernate Envers 正常工作,您需要将其导入到类路径中,或者如果使用 Maven,则将其作为依赖项:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-envers</artifactId>
<version>4.3.10.Final</version>
</dependency>
如果您的类路径上有 Envers,只需在要审计的实体或属性上使用 [受电子邮件保护] 注解:
@Entity
@Table(name = "BOOKS")
@Audited
public class Book {
// ... the body of the entity
}
在这种情况下,Hibernate 自动管理实体的版本控制。 它将创建一个包含实体表名称和后缀_AUD
的表。 在此示例BOOKS_AUD
中,它在那里存储实体的所有字段以及两个额外的列:REVTYPE
和REV
。
REVTYPE
是修订版本的类型,它可以采用值add
,mod
,del
分别插入,修改或删除。
REV
字段保存所存储条目的修订号。
如果仅使用@Audited
注解对实体的一个或某些字段进行注解,则*_AUD
表将包含两个额外的审计字段,即使用@Audited
注解的字段和实体的 ID。
Hibernate 创建了一个称为REVINFO
的额外表,其中在实体发生更改时REV
编号与时间戳进行映射。
自定义修订表
您可以根据需要自定义修订表,以使用所需的信息扩展核心功能。 正如我上面提到的,一种这样的情况是与实体更改一起记录与应用会话关联的当前用户(在 Web 应用的情况下)。
为此,您需要一个自定义@Entity
对象,该对象扩展了org.hibernate.envers.DefaultRevisionEntity
并具有@RevisionEntity
注解。 对于@RevisionEntity
,如果要更新表中的数据,则必须添加自定义监听器类。 在此示例中,它将是用户名。 该监听器必须实现org.hibernate.envers.RevisionListener
接口,如下所示。
@Entity
@RevisionEntity(AuditRevisionListener.class)
public class AuditEntity extends DefaultRevisionEntity {
private String username;
// getters and setters
}
public class AuditRevisionListener implements RevisionListener {
@Override
public void newRevision(Object revisionEntity) {
final AuditEntity auditEntity = (AuditEntity) revisionEntity;
// normally you would set here the name of the current user
auditEntity.setUsername("GHajba");
}
}
在生产过程中切换审计表
您应该关心的一件事是在生产(甚至测试)期间切换审计表。 引入新表后,修订 ID(审计表中REV
字段的内容)将立即重置并开始从 1 开始计数(如果您未指定其他任何要开始的序列)。 这意味着您必须知道何时切换以找到正确的修订信息,以找到正确的修订信息。
总结
如您所见,Hibernate 提供了一种很好的简便方法来对实体进行版本控制。 但是,我还是只是简单地探讨了各种可能性,但我希望我能给您一个好的起点。
代码下载
在此处下载 Hibernate 实体版本代码。
Hibernate 4 的并发控制
原文: https://javabeginnerstutorial.com/hibernate/concurrency-control-with-hibernate-4/
在大多数情况下,仅让数据库执行并发控制工作是可以的,但是有时您会遇到需要接管的应用。
在本文中,我将简要介绍乐观和悲观并发控制。
乐观并发控制
高并发应用中唯一一致的方法是带有版本控制的乐观并发控制。 此方法使用版本号或时间戳来检测冲突并防止更新丢失。
应用版本检查
使用这种方法,应用必须手动维护实体的版本。 这意味着开发人员有责任在操作它们之前从数据库加载实际的实体状态。 当对象被 Hibernate 刷新时,版本会自动增加-因此开发人员不需要增加此属性。
如果您的应用具有低并发性并跳过版本控制,则可以使用此方法。 在这种情况下,总是最后一次提交获胜,并且根据该状态更新对象。 这就是为什么总是需要在操作数据库之前先从数据库中加载实体的实际状态。
自动版本控制
要使用自动版本控制,只需在您希望在乐观锁定的版本控制下拥有的实体中添加一个字段或方法,然后使用 [受电子邮件保护] 注解对其进行注解。 如文档所述,此注解可用于以下类型:int
,Integer
,short
,Short
,long
,Long
,java.sql.Timestamp
。
如果在加载实体和将其刷新回数据库之间发生一些更新,则会从 Hibernate 收到一条错误消息:
线程“主”中的异常org.hibernate.StaleObjectStateException
:行已由另一个事务更新或删除(或未保存值的映射不正确):[hibernate_example.joined.Book#1]
悲观并发控制
正如我上次已经提到的那样,Hibernate 不会锁定内存中的对象,它将始终使用基础数据库的锁定机制。
但是, LockMode
类定义了一些可由 Hibernate 获得的机制:
WRITE
:在 Hibernate 更新或插入行时自动获取UPGRADE
:可以在明确的用户请求下使用SELECT…FOR UPDATE
在支持此语法的数据库上获取UPGRADE_NOWAIT
:可在 Oracle 下使用SELECT…FOR UPDATE NOWAIT
根据明确的用户请求获取READ
:Hibernate 读取数据时自动获取NONE
:表示没有锁,在事务结束时所有对象都切换到此锁模式PESSIMISTIC_FORCE_INCREMENT
:加载实体时强制增加版本。
上面提到的“显式用户请求”可以表示为以下调用之一:
- 为
LockOptions
参数指定LockMode
的load()
buildLockRequest()
setLockMode()
设置锁定模式的示例
上面我已经提到了一些锁定模式以及如何设置它们,现在是时候看看一些示例代码来了解它们的作用了。
getBooks(session).stream().forEach(b -> session.load(Book.class, b.getId(),
new LockOptions(LockMode.PESSIMISTIC_FORCE_INCREMENT)));
getBooks(session).stream().forEach(System.out::println);
上面的代码块有两件事:从数据库加载实体,设置一种用于强制版本增加的锁定模式,然后将书籍打印到控制台。 结果将如下所示:
Java 8 in Action by Raoul-Gabriel Urma, Mario Fusco, Alan Mycroft (ISBN: 9781617291999), published 2015.07.29\. 0:00 [1]
Java 8 in Action by Raoul-Gabriel Urma, Mario Fusco, Alan Mycroft (ISBN: 9781617291999), published 2015.07.29\. 0:00 [1]
Java 8 in Action by Raoul-Gabriel Urma, Mario Fusco, Alan Mycroft (ISBN: 9781617291999), published 2015.07.29\. 0:00 [1]
Java 8 in Action by Raoul-Gabriel Urma, Mario Fusco, Alan Mycroft (ISBN: 9781617291999), published 2015.07.29\. 16:14 [1]
每行末尾的方括号包含实体的版本号。
如果我们想将锁定移动到getBooks()
方法中,可以执行以下操作:
private static List<Book> getBooks(Session session) {
final Query query = session.createQuery("from Book b");
query.setLockMode("b", LockMode.PESSIMISTIC_FORCE_INCREMENT);
return query.list();
}
有趣的是setLockMode
方法的第一个字符串参数:它是实体使用此锁定的别名,您必须在查询的FROM
块中使用此别名。
使用此仓库方法加载书籍后,将书籍打印到控制台的结果可能是这样的:
Java 8 in Action by Raoul-Gabriel Urma, Mario Fusco, Alan Mycroft (ISBN: 9781617291999), published 2015.07.29\. 0:00 [1]
Java 8 in Action by Raoul-Gabriel Urma, Mario Fusco, Alan Mycroft (ISBN: 9781617291999), published 2015.07.29\. 0:00 [1]
Java 8 in Action by Raoul-Gabriel Urma, Mario Fusco, Alan Mycroft (ISBN: 9781617291999), published 2015.07.29\. 0:00 [1]
Java 8 in Action by Raoul-Gabriel Urma, Mario Fusco, Alan Mycroft (ISBN: 9781617291999), published 2015.07.29\. 16:15 [1]
一个有趣的事实
版本号仅在当前会话中更新,直到您更新实体并将其保存到数据库为止。
如果不这样做,您会在应用中看到版本增量,但是它们不会保存到数据库中,即使您使用某些*_FORCE_INCREMENT
策略。
| 1 | Raoul-Gabriel Urma, Mario Fusco, Alan Mycroft | 9781617291999 | 2015-07-29 | Java 8 in Action | 0 |
| 2 | Raoul-Gabriel Urma, Mario Fusco, Alan Mycroft | 9781617291999 | 2015-07-29 | Java 8 in Action | 0 |
| 3 | Raoul-Gabriel Urma, Mario Fusco, Alan Mycroft | 9781617291999 | 2015-07-29 | Java 8 in Action | 0 |
| 4 | Raoul-Gabriel Urma, Mario Fusco, Alan Mycroft | 9781617291999 | 2015-07-29 | Java 8 in Action | 0 |
如果我们不更新旧条目,则上面的块显示了数据库中的实体。 这些列从左到右如下:ID,作者,ISBN,出版,标题和版本。
要进行一些版本更改,请取消注释示例中的代码块,然后运行应用。 该块将书籍的日期更新为当前日期,并将实体保存回数据库中。 为此,您需要进行事务。
要开始事务,只需调用session.beginTransaction();
,如果完成调用session.getTransaction().commite()
, 完成并写入数据库或session.getTransaction().rollback()
以还原此事务中所做的所有更改。 或者,您可以存储由beginTransaction()
方法返回的事务对象,并在此事务实例上调用commit()
或rollback()
。
一些事务方式
如果我提到了事务,请允许我给您一些模式(甚至是反模式),以了解通常如何处理事务。
每个操作的会话(反模式)
这是一种反模式,因为如果使用此事务管理方法,则将为每个数据库调用打开和关闭会话。 即使您使用数据库的自动提交功能(在每次调用数据库后隐式调用提交),此操作也会完成。
每个请求的会话
这是用于事务的最常见模式。 名称中的请求与其中有来自客户端/用户(例如 Web 应用)的许多请求的系统有关。
常见的工作流程是,当此类请求到达系统时,将打开 Hibernate 会话,并保持打开状态,直到处理该请求的信息(更新存储的信息或检索要显示的内容)为止。
如果使用此模式,则在大多数情况下可以减少数据丢失,因为信息会连续保存在数据库中。
每个应用的会话
开发人员不同意这是模式还是反模式,因为有时应用很小,您确实可以打开一个事务并在最后调用commit
。 但是,对于大型应用而言,这是一种太糟糕的方法,在大型应用中,最终的应用故障会导致数据丢失,或者如果存在并发用户并且数据相关,则这些用户之间将无法处于同步状态。
总结
锁定可能会有些麻烦,但是 Hibernate 会通过各种锁定机制为我们提供帮助和帮助。 如果您想微调内置解决方案,也可以使用上述方法进行。
我们稍微深入研究事务,只是为了了解如何通常使用设计模式进行处理,以了解如何将数据存储在数据库中并保留这些信息。 您可以从此处下载代码。
Hibernate 4 的多租户
原文: https://javabeginnerstutorial.com/hibernate/multi-tenancy-with-hibernate-4/
多租户表示软件开发中的一种方法,其中单个应用同时为多个客户端提供服务。 这些客户也称为租户。 这种方法在软件即服务(SaaS)解决方案中非常普遍。
这种系统面临的挑战是租户之间的数据隔离:每个客户只能看到其信息,而不能访问其他客户的信息。
多租户方法
在客户端之间隔离数据的主要方法有 3 种:
- 单独的数据库
- 单独的模式
- 分区数据
即使您从名称中就知道了这个主意,也让我们来看看它们。
单独的数据库
在这种情况下,每个租户的数据都保存在物理上分开的数据库中。 这样可以确保应用只能访问此数据库,从而确保完整性。
此解决方案的一种方法是为每个租户建立一个数据库连接池,并根据与当前登录用户相关联的租户标识符来选择该池。
单独的模式
采用这种方法,我们只有一个数据库,但是客户的数据存储在单独的数据库模式中。
对于这种方法,主要有两个解决方案可以实现此目的:
第一种解决方案与以前的方法类似:为每个架构创建连接池,并根据当前用户的租户标识符选择连接池。
第二种解决方案将使用默认模式指向数据库本身,并使用SET SCHEMA
SQL 命令(或类似的如果数据库允许的话)对每个连接更改数据库模式。 在这种情况下,单个连接池将为所有租户提供服务,但是在使用此连接之前,它将使用用户的租户标识符将其更改为正确的架构。
分区数据
通过这种方法,所有数据都保存在单个数据库模式中。 租户之间的数据由一个区分值划分,该区分值的范围从单列到复杂的 SQL 公式。
使用这种方法,将再次有一个连接池,但是这次必须更改每个 SQL 查询以引用租户的鉴别值。
多租户和 Hibernate
要与 Hibernate 建立多租户连接,您必须从会话开始:
Session session = sessionFactory.withOptions().tenantIdentifier( yourTenantIdentifier ).openSession();
如您所见,在打开会话时,您需要提供租户标识符。 在这种情况下,您还必须提供一个MultiTenancyStrategy
,以让 Hibernate 知道要在数据库中寻找哪种策略(或者如果您启用了 Hibernate 的模式管理,则可以创建该策略):
NONE
:这是默认设置,如果您使用tenantIdentifier
打开会话,则会引发异常- 模式
- 数据库
- 判别器:Hibernate 计划从版本 5 开始支持此策略。
如果您没有为NONE
以外的其他策略提供租户标识符,则会出现异常。 当使用NONE
以外的策略时,必须指定一个MultiTenantConnectionProvider
。
可以在您的 Hibernate 配置中使用hibernate.multiTenancy
设置来设置这些策略(在本例中为hibernate.cfg.xml
文件)。
MultiTenantConnectionProvider
要使用两个可用的多租户供应器之一,我们需要配置MultiTenantConnectionProvider
。在这种特殊情况下,这意味着我们必须自己实现。
在本文的示例应用中,我添加了接口的最基本的实现。
是的,有两种实现方式,因为SCHEMA
和DATABASE
策略需要单独处理。
MultiTenantConnectionProviderImpl
类用于DATABASE
策略。
MultiTenantConnectionProviderWithDbPoolImpl
类用于SCHEMA
策略。
DatabasePool
实现的唯一问题是它使用 H2 数据库,而 H2 不知道USE
SQL 命令,因此在尝试运行应用时遇到异常。
关于租户 ID 的评论
如果您使用的是数据库,请使用字符串作为租户 ID(除非您使用DISCRIMINATOR
策略),因为架构必须具有文本名称,并且如果您提供数字(即使在CurrentTenantIdentifierResolverImpl
中作为字符串),也会出现数据库异常。
模式更新
当您在数据库中使用多租户时,无法选择 Hibernate 模式。 这是因为它只会在启动应用时更新默认架构-如果切换到另一个架构,则可能不会更新。
这意味着您必须手动创建要使用的架构,并将必需的表添加到这些架构。 或者,可以使用 Liquibase 或 FlyWay 之类的迁移工具来提供帮助。
下面的脚本显示了如何创建模式及其中的BOOKS
表。
如果不存在则创建模式示例;
使用示例;
create table Books (
id bigint generated by default as identity (start with 1),
title varchar(255),
isbn varchar(255),
authors varchar(255),
published_date date(8),
primary key (id)
);
总结
我们已经看到在我们的应用中实现多租户并不是不可能的,但是当前它需要一些编码才能在租户之间建立正确的连接池和映射,并且您必须照顾好数据库,模式和表。
Hibernate 4 连接池
原文: https://javabeginnerstutorial.com/hibernate/connection-pooling-with-hibernate-4/
我们已经进入了 Hibernate 4 的状态,在这里我们可以更改系统后面的数据库以用于生产用途(因此,H2 数据库就不存在了)。
现在该看一下连接池了,因为 Hibernate 的默认连接池机制是基本的,仅提供给开发和测试使用。
为了获得最佳性能和稳定性,需要使用第三方工具。 市场上有一些产品,其中大多数是免费的,您可以将它们与 Hibernate 一起轻松使用。
C3P0
就像《星球大战》中的黄金翻译和协议机器人一样,主要使用的连接池供应器称为 c3p0。
C3P0 是一个开源连接池,它具有一个 Hibernate 包,您可以将其添加为项目的依赖项,并准备配置该池:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-c3p0</artifactId>
<version>4.3.10.Final</version>
</dependency>
c3p0 连接池的最重要的配置属性如下:
c3p0.min_size
:池中的最小 JDBC 连接数。 Hibernate 默认值:1c3p0.max_size
:池中最大 JDBC 连接数。 Hibernate 默认值:100c3p0.timeout
:从池中删除空闲连接时(秒)。 Hibernate 默认值:0,永不过期。c3p0.max_statements
:将缓存准备好的语句数。 提高性能。 Hibernate 默认值:0,禁用缓存。c3p0.idle_test_period
– 自动验证连接之前的空闲时间(以秒为单位)。 Hibernate 默认值:0
C3P0 的配置
要使用您的应用配置 C3P0,您需要设置以下属性。 在这种情况下,让我们在hibernate.cfg.xml
中查看基于 XML 的已知配置:
<property name="hibernate.connection.provider_class">org.hibernate.c3p0.internal.C3P0ConnectionProvider</property>
<property name="hibernate.c3p0.min_size">1</property>
<property name="hibernate.c3p0.max_size">19</property>
<property name="hibernate.c3p0.timeout">120</property>
<property name="hibernate.c3p0.max_statements">10</property>
连接供应器的第一行是可选的,您不必输入它,Hibernate 会知道您正在使用 C3P0。 这是因为位置(包结构)在 Hibernate 版本之间可能会发生变化,并且每次更新到连接供应器类位于不同位置的较新版本时,都会遇到麻烦,这很容易让人感到头痛。
如果在启动服务器时仔细查看日志或控制台输出,则应该看到正在配置 C3P0:
org.hibernate.c3p0.internal.C3P0ConnectionProvider configure
Proxool
Proxool 是 C3P0 的替代连接池,但是它需要更多配置,因此我个人更喜欢 C3P0。
仅设置依赖项和属性是不够的,您需要一个额外的 XML 文件,其中包含 Proxool 的配置信息。
依赖项几乎与 C3P0 相同:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-proxool</artifactId>
<version>4.3.10.Final</version>
</dependency>
Proxool 的配置
如前所述,仅在hibernate.cfg.xml
文件中配置 Proxool 是不够的,您还需要一个单独的 XML 文件:
<property name="hibernate.connection.provider_class">org.hibernate.connection.ProxoolConnectionProvider</property>
<property name="hibernate.proxool.pool_alias">proxool</property>
<property name="hibernate.proxool.xml">proxool.xml</property>
与第一行的 C3P0 一样,hibernate.connection.provider_class
是可选的。
在hibernate.proxool.xml
属性中提到了 XML 文件。
proxool.xml
包含以下内容:
<proxool-config>
<proxool>
<alias>proxool</alias>
<driver-url>jdbc:h2:file:./example;DB_CLOSE_DELAY=-1;MVCC=TRUE</driver-url>
<driver-class>org.h2.Driver</driver-class>
<driver-properties>
<property name="user" value="sa"></property>
<property name="password" value=""></property>
</driver-properties>
<minimum-connection-count>10</minimum-connection-count>
<maximum-connection-count>20</maximum-connection-count>
</proxool>
</proxool-config>
如您所见,您必须在 Proxool 中提供数据库连接。 这是强制性的。 但是,由于这个原因,您可以从hibernate.cfg.xml
文件中保留连接配置。
Proxool 存在一个缺点:您必须定义要使用的方言。 使用 C3P0 和默认的 Hibernate 配置,您无需设置要使用的方言,但 Proxool 需要此信息。
如果一切都已设置好,则在启动应用时,您可以在日志中或控制台上看到以下条目:
org.hibernate.proxool.internal.ProxoolConnectionProvider
配置
一般关于配置
Hibernate 使用其魔力来根据您配置的属性来标识要使用的连接池供应器。 但是,您可以使用hibernate.connection.provider_class
属性定义连接供应器。
如果未配置连接池,则使用默认值。 启动应用时,它在日志或控制台输出中可见:
org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl configure
总结
有一些连接池供应器,您可以轻松地将它们与 Hibernate 绑定在一起,从而为您提供良好的数据库连接体验。
配置这些供应器很容易,因为大多数供应器都可以将其 Hibernate 集成包作为项目的依赖项导入,并使所有程序运行。
Hibernate 自举
原文: https://javabeginnerstutorial.com/hibernate/hibernate-bootstrapping/
在本文中,我将向您介绍 Hibernate 5 的新本机自举 API。
自举有什么好处?
如果您觉得需要对 Hibernate 的内部配置进行更多控制,则可以利用此新功能来实现此目标。 如果您有一个仅需要使用 Hibernate 而不需要其他 JPA 框架的简单项目,这将非常有用:借助自举 API,您可以在没有太多魔术的情况下启动和运行项目。
自然,每一朵玫瑰都有其刺:新的本机自举 API 的使用使配置更加复杂,但它比 JPA 自举 API 更强大。
为什么这比以前更好?
引入的新功能使您可以通过 Java 代码访问 API。 这意味着您不必依赖单一的 XML 配置,您可以在代码库中添加一些配置,只有当您提供新版本的软件时,该配置才能更改。
在某些情况下,这非常有用,因为您的应用不必依赖通过配置文件配置的属性的正确性,或者您可以在 Hibernate 中调整一些您不想通过外部文件更改的内部配置。 。
如何使用 API?
首先,您需要项目中正确的依赖项。 至于本系列文章中的所有示例,我正在使用 Hibernate 版本 5.3.6。 最终版本和 H2 版本 1.4.197。
我们需要创建一个StandardServiceRegistry
,一个元数据对象,并使用它来启动SessionFactory
。
hibernate.cfg.xml
大多数时候,我使用基于 XML 的配置文件hibernate.cfg.xml
来设置StandardServiceRegistry
:
<hibernate-configuration>
<session-factory>
<!-- Database connection settings -->
<property name="connection.driver_class">org.h2.Driver</property>
<property name="connection.url">jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1;MVCC=TRUE</property>
<property name="connection.username">sa</property>
<property name="connection.password"/>
<!-- JDBC connection pool (use the built-in) -->
<property name="connection.pool_size">1</property>
<!-- SQL dialect -->
<property name="dialect">org.hibernate.dialect.H2Dialect</property>
<!-- Echo all executed SQL to stdout -->
<property name="show_sql">true</property>
<!-- Drop and re-create the database schema on startup -->
<property name="hbm2ddl.auto">create</property>
<mapping class="hibernate_example.Book"/>
</session-factory>
</hibernate-configuration>
使用此文件可以使配置独立于源代码,并以结构化的方式概述配置。
final Configuration configuration = new Configuration().configure();
final StandardServiceRegistryBuilder builder = new StandardServiceRegistryBuilder().configure(“hibernate.cfg.xml”);
如示例代码所示,您需要告诉 Hibernate 您正在使用hibernate.cfg.xml
文件。 在以前的 Hibernate 版本中,您不需要指定使用此文件来设置服务注册表,因为这是默认行为。
程序配置
使用 Hibernate 5,您可以选择以 Java 代码编程配置 ORM。
让我们看看如何将先前显示的hibernate.cfg.xml
文件映射到 Java 代码中。 为此,我创建了一个名为HibernateUtil
的类,如下所示:
package hibernate_example;
import java.util.Properties;
import org.hibernate.SessionFactory;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.service.ServiceRegistry;
/**
* Utility class for bootstrapping Hibernate through Java code only.
*
* @author GHajba
*/
public class HibernateUtil {
protected HibernateUtil() {
}
public static SessionFactory createSessionFactory() {
MetadataSources metadataSources = new MetadataSources(configureServiceRegistry());
addClasses(metadataSources);
return metadataSources.buildMetadata()
.getSessionFactoryBuilder()
.build();
}
private static ServiceRegistry configureServiceRegistry() {
return new StandardServiceRegistryBuilder()
.applySettings(getProperties())
.build();
}
private static Properties getProperties() {
Properties properties = new Properties();
properties.put("hibernate.connection.driver_class", "org.h2.Driver");
properties.put("hibernate.connection.url", "jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1;MVCC=TRUE");
properties.put("hibernate.connection.username", "sa");
properties.put("hibernate.connection.password", "");
properties.put("hibernate.connection.pool_size", 1);
properties.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
properties.put("hibernate.show_sql", "true");
properties.put("hibernate.hbm2ddl.auto", "create");
properties.put("", "");
return properties;
}
private static void addClasses(MetadataSources metadataSources) {
metadataSources.addAnnotatedClass(Book.class);
}
}
如您所见,所有内容现在都在 Java 代码中。 这使得仅处理 Java 类变得很方便-但是想想如果您有很多实体,您会得到什么? 我认为,它将像下面的屏幕截图那样混乱您的代码库:
因此,我更喜欢将配置保存在单独的文件中,而不是在 Java 代码中-但这是我个人的看法。
hibernate.properties
让代码喘气的一种方法是将配置提取到属性文件中-在大多数情况下,该文件称为hibernate.properties
。
使用上面的示例,我们可以将配置提取到以下文件中:
hibernate.connection.driver_class=org.h2.Driver
hibernate.connection.url=jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1;MVCC=TRUE
hibernate.connection.username=sa
hibernate.connection.password=
hibernate.connection.pool_size=1
hibernate.dialect=org.hibernate.dialect.H2Dialect
hibernate.show_sql=true
hibernate.hbm2ddl.auto=create
现在,我们也必须改编HibernateUtil
类以使用此新文件:
package hibernate_example;
import java.io.IOException;
import java.util.Properties;
import org.hibernate.SessionFactory;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.service.ServiceRegistry;
/**
* Utility class for bootstrapping Hibernate through Java code and hibernate.properties file.
*
* @author GHajba
*/
public class HibernateUtil {
protected HibernateUtil() {
}
public static SessionFactory createSessionFactory() {
MetadataSources metadataSources = new MetadataSources(configureServiceRegistry());
addClasses(metadataSources);
return metadataSources.buildMetadata()
.getSessionFactoryBuilder()
.build();
}
private static ServiceRegistry configureServiceRegistry() {
return new StandardServiceRegistryBuilder()
.applySettings(getProperties())
.build();
}
private static Properties getProperties() {
Properties properties = new Properties();
try {
properties.load(HibernateUtil.class
.getResourceAsStream("/hibernate.properties"));
} catch (IOException e) {
throw new RuntimeException(e);
}
return properties;
}
private static void addClasses(MetadataSources metadataSources) {
metadataSources.addAnnotatedClass(Book.class);
}
}
总结
使用自举 API,您可以配置 Hibernate 应用-这使您的项目更加复杂,但是您可以将这些功能隐藏在服务类中。 如果只想在简单项目中使用 Hibernate,这将很方便。
如果您仔细阅读本系列文章随附的示例应用,您会发现我自己使用此 API 来使应用与 Hibernate 一起运行。